Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-11-14 11:41:52 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-11-14 11:41:52 +0300
commit585826cb22ecea5998a2c2a4675735c94bdeedac (patch)
tree5b05f0b30d33cef48963609e8a18a4dff260eab3
parentdf221d036e5d0c6c0ee4d55b9c97f481ee05dee8 (diff)
Add latest changes from gitlab-org/gitlab@16-6-stable-eev16.6.0-rc42
-rw-r--r--.eslintrc.yml4
-rw-r--r--.gitlab-ci.yml21
-rw-r--r--.gitlab/CODEOWNERS169
-rw-r--r--.gitlab/ci/cng/main.gitlab-ci.yml6
-rw-r--r--.gitlab/ci/database.gitlab-ci.yml2
-rw-r--r--.gitlab/ci/docs.gitlab-ci.yml5
-rw-r--r--.gitlab/ci/frontend.gitlab-ci.yml2
-rw-r--r--.gitlab/ci/gitlab-com/danger-review.gitlab-ci.yml23
-rw-r--r--.gitlab/ci/gitlab-gems.gitlab-ci.yml3
-rw-r--r--.gitlab/ci/global.gitlab-ci.yml25
-rw-r--r--.gitlab/ci/package-and-test-nightly/main.gitlab-ci.yml12
-rw-r--r--.gitlab/ci/package-and-test/main.gitlab-ci.yml24
-rw-r--r--.gitlab/ci/qa-common/main.gitlab-ci.yml10
-rw-r--r--.gitlab/ci/qa-common/rules.gitlab-ci.yml4
-rw-r--r--.gitlab/ci/rails.gitlab-ci.yml162
-rw-r--r--.gitlab/ci/rails/shared.gitlab-ci.yml14
-rw-r--r--.gitlab/ci/release-environments/main.gitlab-ci.yml20
-rw-r--r--.gitlab/ci/review-apps/main.gitlab-ci.yml18
-rw-r--r--.gitlab/ci/review-apps/qa.gitlab-ci.yml23
-rw-r--r--.gitlab/ci/review.gitlab-ci.yml21
-rw-r--r--.gitlab/ci/rules.gitlab-ci.yml57
-rw-r--r--.gitlab/ci/setup.gitlab-ci.yml7
-rw-r--r--.gitlab/ci/templates/gem.gitlab-ci.yml4
-rw-r--r--.gitlab/ci/test-on-gdk/main.gitlab-ci.yml50
-rw-r--r--.gitlab/issue_templates/AI Project Proposal.md142
-rw-r--r--.gitlab/issue_templates/Feature Flag Cleanup.md2
-rw-r--r--.gitlab/issue_templates/Feature Flag Roll Out.md4
-rw-r--r--.gitlab/issue_templates/Geo Replicate a new Git repository type.md23
-rw-r--r--.gitlab/issue_templates/Geo Replicate a new blob type.md34
-rw-r--r--.haml-lint.yml1
-rw-r--r--.haml-lint_todo.yml161
-rw-r--r--.projections.json.example8
-rw-r--r--.rubocop.yml23
-rw-r--r--.rubocop_todo/background_migration/dictionary_file.yml4
-rw-r--r--.rubocop_todo/capybara/testid_finders.yml37
-rw-r--r--.rubocop_todo/capybara/visibility_matcher.yml1
-rw-r--r--.rubocop_todo/factory_bot/create_list.yml1
-rw-r--r--.rubocop_todo/fips/sha1.yml3
-rw-r--r--.rubocop_todo/gemspec/deprecated_attribute_assignment.yml6
-rw-r--r--.rubocop_todo/gitlab/doc_url.yml1
-rw-r--r--.rubocop_todo/gitlab/namespaced_class.yml15
-rw-r--r--.rubocop_todo/gitlab/no_code_coverage_comment.yml2
-rw-r--r--.rubocop_todo/gitlab/service_response.yml1
-rw-r--r--.rubocop_todo/gitlab/strong_memoize_attr.yml12
-rw-r--r--.rubocop_todo/graphql/descriptions.yml1
-rw-r--r--.rubocop_todo/graphql/resource_not_available_error.yml2
-rw-r--r--.rubocop_todo/layout/argument_alignment.yml148
-rw-r--r--.rubocop_todo/layout/array_alignment.yml7
-rw-r--r--.rubocop_todo/layout/empty_line_after_magic_comment.yml29
-rw-r--r--.rubocop_todo/layout/first_array_element_indentation.yml4
-rw-r--r--.rubocop_todo/layout/first_hash_element_indentation.yml13
-rw-r--r--.rubocop_todo/layout/hash_alignment.yml2
-rw-r--r--.rubocop_todo/layout/line_continuation_leading_space.yml2
-rw-r--r--.rubocop_todo/layout/line_continuation_spacing.yml13
-rw-r--r--.rubocop_todo/layout/line_end_string_concatenation_indentation.yml12
-rw-r--r--.rubocop_todo/layout/line_length.yml137
-rw-r--r--.rubocop_todo/layout/parameter_alignment.yml1
-rw-r--r--.rubocop_todo/layout/space_in_lambda_literal.yml4
-rw-r--r--.rubocop_todo/layout/space_inside_parens.yml3
-rw-r--r--.rubocop_todo/layout/trailing_whitespace.yml1
-rw-r--r--.rubocop_todo/lint/ambiguous_operator_precedence.yml3
-rw-r--r--.rubocop_todo/lint/ambiguous_regexp_literal.yml1
-rw-r--r--.rubocop_todo/lint/assignment_in_condition.yml6
-rw-r--r--.rubocop_todo/lint/constant_definition_in_block.yml1
-rw-r--r--.rubocop_todo/lint/empty_block.yml5
-rw-r--r--.rubocop_todo/lint/missing_cop_enable_directive.yml22
-rw-r--r--.rubocop_todo/lint/no_return_in_begin_end_blocks.yml1
-rw-r--r--.rubocop_todo/lint/non_atomic_file_operation.yml3
-rw-r--r--.rubocop_todo/lint/or_assignment_to_constant.yml1
-rw-r--r--.rubocop_todo/lint/redundant_cop_disable_directive.yml8
-rw-r--r--.rubocop_todo/lint/redundant_dir_glob_sort.yml1
-rw-r--r--.rubocop_todo/lint/symbol_conversion.yml7
-rw-r--r--.rubocop_todo/lint/unused_block_argument.yml14
-rw-r--r--.rubocop_todo/lint/unused_method_argument.yml16
-rw-r--r--.rubocop_todo/metrics/abc_size.yml1
-rw-r--r--.rubocop_todo/metrics/parameter_lists.yml1
-rw-r--r--.rubocop_todo/migration/background_migration_base_class.yml12
-rw-r--r--.rubocop_todo/migration/background_migration_record.yml6
-rw-r--r--.rubocop_todo/migration/background_migrations.yml4
-rw-r--r--.rubocop_todo/naming/heredoc_delimiter_naming.yml2
-rw-r--r--.rubocop_todo/performance/map_compact.yml1
-rw-r--r--.rubocop_todo/performance/method_object_as_block.yml4
-rw-r--r--.rubocop_todo/performance/regexp_match.yml2
-rw-r--r--.rubocop_todo/performance/string_include.yml6
-rw-r--r--.rubocop_todo/performance/string_replacement.yml8
-rw-r--r--.rubocop_todo/rails/file_path.yml6
-rw-r--r--.rubocop_todo/rails/find_each.yml48
-rw-r--r--.rubocop_todo/rails/inverse_of.yml1
-rw-r--r--.rubocop_todo/rails/lexically_scoped_action_filter.yml1
-rw-r--r--.rubocop_todo/rails/negate_include.yml1
-rw-r--r--.rubocop_todo/rails/output_safety.yml3
-rw-r--r--.rubocop_todo/rails/pluck.yml7
-rw-r--r--.rubocop_todo/rails/require_dependency.yml40
-rw-r--r--.rubocop_todo/rails/time_zone.yml21
-rw-r--r--.rubocop_todo/rspec/any_instance_of.yml17
-rw-r--r--.rubocop_todo/rspec/avoid_conditional_statements.yml1
-rw-r--r--.rubocop_todo/rspec/before_all_role_assignment.yml45
-rw-r--r--.rubocop_todo/rspec/context_wording.yml87
-rw-r--r--.rubocop_todo/rspec/empty_line_after_hook.yml2
-rw-r--r--.rubocop_todo/rspec/expect_change.yml32
-rw-r--r--.rubocop_todo/rspec/expect_in_hook.yml9
-rw-r--r--.rubocop_todo/rspec/factory_bot/avoid_create.yml9
-rw-r--r--.rubocop_todo/rspec/factory_bot/excessive_create_list.yml4
-rw-r--r--.rubocop_todo/rspec/feature_category.yml160
-rw-r--r--.rubocop_todo/rspec/hooks_before_examples.yml12
-rw-r--r--.rubocop_todo/rspec/instance_variable.yml11
-rw-r--r--.rubocop_todo/rspec/multiple_memoized_helpers.yml1
-rw-r--r--.rubocop_todo/rspec/named_subject.yml3858
-rw-r--r--.rubocop_todo/rspec/repeated_example_group_description.yml5
-rw-r--r--.rubocop_todo/rspec/return_from_stub.yml17
-rw-r--r--.rubocop_todo/rspec/scattered_let.yml10
-rw-r--r--.rubocop_todo/rspec/specify_expected.yml54
-rw-r--r--.rubocop_todo/rspec/useless_dynamic_definition.yml10
-rw-r--r--.rubocop_todo/rspec/variable_definition.yml1
-rw-r--r--.rubocop_todo/rspec/verified_doubles.yml30
-rw-r--r--.rubocop_todo/search/namespaced_class.yml1
-rw-r--r--.rubocop_todo/sidekiq_load_balancing/worker_data_consistency.yml15
-rw-r--r--.rubocop_todo/style/accessor_grouping.yml5
-rw-r--r--.rubocop_todo/style/arguments_forwarding.yml173
-rw-r--r--.rubocop_todo/style/block_delimiters.yml69
-rw-r--r--.rubocop_todo/style/class_and_module_children.yml12
-rw-r--r--.rubocop_todo/style/empty_else.yml1
-rw-r--r--.rubocop_todo/style/empty_method.yml4
-rw-r--r--.rubocop_todo/style/explicit_block_argument.yml3
-rw-r--r--.rubocop_todo/style/format_string.yml15
-rw-r--r--.rubocop_todo/style/guard_clause.yml37
-rw-r--r--.rubocop_todo/style/hash_as_last_array_item.yml2
-rw-r--r--.rubocop_todo/style/hash_each_methods.yml4
-rw-r--r--.rubocop_todo/style/if_unless_modifier.yml24
-rw-r--r--.rubocop_todo/style/inline_disable_annotation.yml3403
-rw-r--r--.rubocop_todo/style/lambda.yml1
-rw-r--r--.rubocop_todo/style/numeric_literal_prefix.yml4
-rw-r--r--.rubocop_todo/style/percent_literal_delimiters.yml399
-rw-r--r--.rubocop_todo/style/redundant_interpolation.yml2
-rw-r--r--.rubocop_todo/style/redundant_regexp_escape.yml5
-rw-r--r--.rubocop_todo/style/redundant_return.yml102
-rw-r--r--.rubocop_todo/style/redundant_self.yml8
-rw-r--r--.rubocop_todo/style/sole_nested_conditional.yml2
-rw-r--r--.rubocop_todo/style/string_concatenation.yml6
-rw-r--r--.rubocop_todo/style/string_literals_in_interpolation.yml10
-rw-r--r--.rubocop_todo/style/symbol_proc.yml10
-rw-r--r--CHANGELOG.md39
-rw-r--r--CONTRIBUTING.md118
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--GITLAB_ELASTICSEARCH_INDEXER_VERSION2
-rw-r--r--GITLAB_KAS_VERSION2
-rw-r--r--GITLAB_PAGES_VERSION2
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--Gemfile70
-rw-r--r--Gemfile.checksum146
-rw-r--r--Gemfile.lock227
-rw-r--r--app/assets/javascripts/add_context_commits_modal/store/actions.js2
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/abuse_report_app.vue7
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/abuse_report_notes.vue92
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/activity_events_list.vue2
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/graphql/abuse_report.query.graphql13
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/labels_select.vue2
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_discussion.vue104
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_note.vue81
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_note_body.vue48
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/report_details.vue2
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/reported_content.vue4
-rw-r--r--app/assets/javascripts/admin/abuse_report/constants.js2
-rw-r--r--app/assets/javascripts/admin/abuse_report/graphql/abuse_report.query.graphql14
-rw-r--r--app/assets/javascripts/admin/abuse_report/graphql/abuse_report_labels.query.graphql (renamed from app/assets/javascripts/admin/abuse_report/components/graphql/abuse_report_labels.query.graphql)0
-rw-r--r--app/assets/javascripts/admin/abuse_report/graphql/create_abuse_report_label.mutation.graphql (renamed from app/assets/javascripts/admin/abuse_report/components/graphql/create_abuse_report_label.mutation.graphql)0
-rw-r--r--app/assets/javascripts/admin/abuse_report/graphql/notes/abuse_report_note.fragment.graphql30
-rw-r--r--app/assets/javascripts/admin/abuse_report/graphql/notes/abuse_report_note_permissions.fragment.graphql3
-rw-r--r--app/assets/javascripts/admin/abuse_report/graphql/notes/abuse_report_notes.query.graphql18
-rw-r--r--app/assets/javascripts/admin/abuse_report/graphql/notes/create_abuse_report_note.mutation.graphql18
-rw-r--r--app/assets/javascripts/admin/abuse_report/graphql/notes/delete_abuse_report_note.fragment.graphql8
-rw-r--r--app/assets/javascripts/admin/abuse_report/graphql/notes/update_abuse_report_note.mutation.graphql10
-rw-r--r--app/assets/javascripts/admin/background_migrations/index.js2
-rw-r--r--app/assets/javascripts/admin/broadcast_messages/components/message_form.vue7
-rw-r--r--app/assets/javascripts/admin/users/components/actions/index.js4
-rw-r--r--app/assets/javascripts/admin/users/components/actions/trust_user.vue62
-rw-r--r--app/assets/javascripts/admin/users/components/actions/untrust_user.vue56
-rw-r--r--app/assets/javascripts/admin/users/components/app.vue63
-rw-r--r--app/assets/javascripts/admin/users/components/user_avatar.vue67
-rw-r--r--app/assets/javascripts/admin/users/components/users_table.vue142
-rw-r--r--app/assets/javascripts/admin/users/constants.js6
-rw-r--r--app/assets/javascripts/alert.js2
-rw-r--r--app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue2
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/store/mutations.js4
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/store/state.js4
-rw-r--r--app/assets/javascripts/analytics/product_analytics/activity_charts_bundle.js28
-rw-r--r--app/assets/javascripts/analytics/product_analytics/components/activity_chart.vue45
-rw-r--r--app/assets/javascripts/analytics/shared/components/metric_tile.vue1
-rw-r--r--app/assets/javascripts/analytics/usage_trends/components/usage_trends_count_chart.vue2
-rw-r--r--app/assets/javascripts/analytics/usage_trends/components/users_chart.vue2
-rw-r--r--app/assets/javascripts/api/bulk_imports_api.js15
-rw-r--r--app/assets/javascripts/badges/components/badge_form.vue6
-rw-r--r--app/assets/javascripts/batch_comments/components/submit_dropdown.vue247
-rw-r--r--app/assets/javascripts/batch_comments/queries/can_approve.query.graphql11
-rw-r--r--app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js18
-rw-r--r--app/assets/javascripts/behaviors/index.js3
-rw-r--r--app/assets/javascripts/behaviors/load_startup_css.js15
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/keybindings.js7
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js2
-rw-r--r--app/assets/javascripts/blob/components/blob_header.vue21
-rw-r--r--app/assets/javascripts/blob/components/blob_header_viewer_switcher.vue34
-rw-r--r--app/assets/javascripts/blob/components/constants.js3
-rw-r--r--app/assets/javascripts/blob/filepath_form/components/template_selector.vue5
-rw-r--r--app/assets/javascripts/blob/pipeline_tour_success_modal.vue6
-rw-r--r--app/assets/javascripts/boards/boards_util.js4
-rw-r--r--app/assets/javascripts/boards/components/board_add_new_column_form.vue1
-rw-r--r--app/assets/javascripts/boards/components/board_card_inner.vue7
-rw-r--r--app/assets/javascripts/boards/components/board_content.vue1
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue19
-rw-r--r--app/assets/javascripts/boards/components/board_list_header.vue3
-rw-r--r--app/assets/javascripts/boards/components/new_board_button.vue2
-rw-r--r--app/assets/javascripts/boards/constants.js1
-rw-r--r--app/assets/javascripts/boards/graphql/cache_updates.js7
-rw-r--r--app/assets/javascripts/boards/stores/actions.js8
-rw-r--r--app/assets/javascripts/boards/stores/index.js15
-rw-r--r--app/assets/javascripts/branches/components/branch_more_actions.vue1
-rw-r--r--app/assets/javascripts/branches/components/delete_branch_modal.vue1
-rw-r--r--app/assets/javascripts/ci/admin/jobs_table/admin_jobs_table_app.vue3
-rw-r--r--app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue6
-rw-r--r--app/assets/javascripts/ci/catalog/components/details/ci_resource_components.vue37
-rw-r--r--app/assets/javascripts/ci/catalog/components/details/ci_resource_details.vue6
-rw-r--r--app/assets/javascripts/ci/catalog/components/details/ci_resource_header.vue11
-rw-r--r--app/assets/javascripts/ci/catalog/components/list/catalog_header.vue14
-rw-r--r--app/assets/javascripts/ci/catalog/components/list/ci_resources_list_item.vue11
-rw-r--r--app/assets/javascripts/ci/catalog/components/pages/ci_resources_page.vue112
-rw-r--r--app/assets/javascripts/ci/catalog/global_catalog.vue10
-rw-r--r--app/assets/javascripts/ci/catalog/graphql/fragments/catalog_resource.fragment.graphql1
-rw-r--r--app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resources.query.graphql16
-rw-r--r--app/assets/javascripts/ci/catalog/index.js37
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_variable_drawer.vue12
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_variable_modal.vue511
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_variable_settings.vue28
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_variable_table.vue8
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/constants.js2
-rw-r--r--app/assets/javascripts/ci/common/pipelines_table.vue10
-rw-r--r--app/assets/javascripts/ci/common/private/job_action_component.vue14
-rw-r--r--app/assets/javascripts/ci/common/private/job_links_layer.vue10
-rw-r--r--app/assets/javascripts/ci/common/private/job_name_component.vue2
-rw-r--r--app/assets/javascripts/ci/constants.js1
-rw-r--r--app/assets/javascripts/ci/job_details/components/job_header.vue6
-rw-r--r--app/assets/javascripts/ci/job_details/components/log/line_header.vue3
-rw-r--r--app/assets/javascripts/ci/job_details/components/sidebar/job_container_item.vue4
-rw-r--r--app/assets/javascripts/ci/job_details/components/sidebar/stages_dropdown.vue15
-rw-r--r--app/assets/javascripts/ci/job_details/job_app.vue2
-rw-r--r--app/assets/javascripts/ci/job_details/store/actions.js2
-rw-r--r--app/assets/javascripts/ci/job_details/store/utils.js45
-rw-r--r--app/assets/javascripts/ci/jobs_page/components/job_cells/job_cell.vue4
-rw-r--r--app/assets/javascripts/ci/jobs_page/components/job_cells/status_cell.vue6
-rw-r--r--app/assets/javascripts/ci/jobs_page/jobs_page_app.vue3
-rw-r--r--app/assets/javascripts/ci/pipeline_details/constants.js1
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/graph_component.vue18
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/graph_view_selector.vue20
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/job_item.vue6
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/linked_graph_wrapper.vue17
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipeline.vue28
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipelines_column.vue39
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/root_graph_layout.vue42
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/stage_column_component.vue60
-rw-r--r--app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue36
-rw-r--r--app/assets/javascripts/ci/pipeline_details/jobs/components/failed_jobs_table.vue6
-rw-r--r--app/assets/javascripts/ci/pipeline_details/pipeline_details_header.js4
-rw-r--r--app/assets/javascripts/ci/pipeline_details/pipeline_shared_client.js7
-rw-r--r--app/assets/javascripts/ci/pipeline_details/pipelines_index.js4
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/editor/ci_editor_header.vue2
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/file_nav/branch_switcher.vue97
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_status.vue13
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/options.js1
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/pipeline_editor_home.vue2
-rw-r--r--app/assets/javascripts/ci/pipeline_mini_graph/legacy_job_item.vue1
-rw-r--r--app/assets/javascripts/ci/pipeline_mini_graph/legacy_pipeline_stage.vue13
-rw-r--r--app/assets/javascripts/ci/pipeline_mini_graph/linked_pipelines_mini_list.vue14
-rw-r--r--app/assets/javascripts/ci/pipeline_new/components/pipeline_new_form.vue2
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue6
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions.vue3
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue7
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/empty_state/ios_templates.vue220
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/empty_state/no_ci_empty_state.vue33
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/pipeline_labels.vue28
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/pipeline_status_badge.vue11
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/pipeline_triggerer.vue4
-rw-r--r--app/assets/javascripts/ci/pipelines_page/constants.js3
-rw-r--r--app/assets/javascripts/ci/pipelines_page/pipelines.vue14
-rw-r--r--app/assets/javascripts/ci/runner/admin_new_runner/admin_new_runner_app.vue4
-rw-r--r--app/assets/javascripts/ci/runner/admin_runners/admin_runners_app.vue20
-rw-r--r--app/assets/javascripts/ci/runner/components/cells/runner_summary_cell.vue33
-rw-r--r--app/assets/javascripts/ci/runner/components/registration/registration_feedback_banner.vue41
-rw-r--r--app/assets/javascripts/ci/runner/components/registration/registration_instructions.vue4
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_created_at.vue72
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_details.vue18
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_header.vue29
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_jobs_table.vue6
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_list_header.vue17
-rw-r--r--app/assets/javascripts/ci/runner/components/search_tokens/tag_token.vue2
-rw-r--r--app/assets/javascripts/ci/runner/components/search_tokens/version_token_config.js12
-rw-r--r--app/assets/javascripts/ci/runner/constants.js7
-rw-r--r--app/assets/javascripts/ci/runner/graphql/edit/runner_fields_shared.fragment.graphql5
-rw-r--r--app/assets/javascripts/ci/runner/graphql/list/all_runners.query.graphql2
-rw-r--r--app/assets/javascripts/ci/runner/graphql/list/all_runners_count.query.graphql10
-rw-r--r--app/assets/javascripts/ci/runner/graphql/show/runner_details_shared.fragment.graphql5
-rw-r--r--app/assets/javascripts/ci/runner/graphql/show/runner_jobs.query.graphql2
-rw-r--r--app/assets/javascripts/ci/runner/group_new_runner/group_new_runner_app.vue4
-rw-r--r--app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue18
-rw-r--r--app/assets/javascripts/ci/runner/project_new_runner/project_new_runner_app.vue4
-rw-r--r--app/assets/javascripts/ci/runner/runner_search_utils.js10
-rw-r--r--app/assets/javascripts/ci/runner/sentry_utils.js2
-rw-r--r--app/assets/javascripts/ci/utils.js2
-rw-r--r--app/assets/javascripts/ci_secure_files/components/secure_files_list.vue2
-rw-r--r--app/assets/javascripts/ci_settings_pipeline_triggers/components/triggers_list.vue8
-rw-r--r--app/assets/javascripts/clusters_list/store/actions.js2
-rw-r--r--app/assets/javascripts/commons/gitlab_ui.js2
-rw-r--r--app/assets/javascripts/content_editor/components/suggestions_dropdown.vue2
-rw-r--r--app/assets/javascripts/content_editor/content_editor.stories.js1
-rw-r--r--app/assets/javascripts/content_editor/extensions/code_block_highlight.js9
-rw-r--r--app/assets/javascripts/content_editor/extensions/copy_paste.js40
-rw-r--r--app/assets/javascripts/content_editor/extensions/emoji.js4
-rw-r--r--app/assets/javascripts/content_editor/extensions/html_marks.js3
-rw-r--r--app/assets/javascripts/content_editor/extensions/loading.js56
-rw-r--r--app/assets/javascripts/content_editor/extensions/suggestions.js27
-rw-r--r--app/assets/javascripts/content_editor/extensions/word_break.js2
-rw-r--r--app/assets/javascripts/content_editor/services/content_editor.js2
-rw-r--r--app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_base.vue10
-rw-r--r--app/assets/javascripts/crm/organizations/components/graphql/update_customer_relations_organization.mutation.graphql10
-rw-r--r--app/assets/javascripts/crm/organizations/components/graphql/update_organization.mutation.graphql10
-rw-r--r--app/assets/javascripts/crm/organizations/components/organization_form_wrapper.vue4
-rw-r--r--app/assets/javascripts/custom_emoji/components/delete_item.vue2
-rw-r--r--app/assets/javascripts/deploy_freeze/components/deploy_freeze_modal.vue2
-rw-r--r--app/assets/javascripts/deprecated_notes.js2
-rw-r--r--app/assets/javascripts/design_management/components/delete_button.vue2
-rw-r--r--app/assets/javascripts/design_management/components/design_notes/design_discussion.vue3
-rw-r--r--app/assets/javascripts/design_management/components/design_notes/design_note.vue28
-rw-r--r--app/assets/javascripts/design_management/components/design_notes/design_note_signed_out.vue2
-rw-r--r--app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue4
-rw-r--r--app/assets/javascripts/design_management/components/design_overlay.vue2
-rw-r--r--app/assets/javascripts/design_management/components/list/item.vue17
-rw-r--r--app/assets/javascripts/design_management/pages/index.vue6
-rw-r--r--app/assets/javascripts/diffs/components/app.vue127
-rw-r--r--app/assets/javascripts/diffs/components/commit_item.vue2
-rw-r--r--app/assets/javascripts/diffs/components/diff_discussions.vue7
-rw-r--r--app/assets/javascripts/diffs/components/diff_file.vue5
-rw-r--r--app/assets/javascripts/diffs/components/graphql/get_mr_codequality_and_security_reports.query.graphql82
-rw-r--r--app/assets/javascripts/diffs/components/graphql/get_mr_codequality_reports.query.graphql46
-rw-r--r--app/assets/javascripts/diffs/components/shared/findings_drawer.vue165
-rw-r--r--app/assets/javascripts/diffs/components/shared/findings_drawer_item.vue30
-rw-r--r--app/assets/javascripts/diffs/components/tree_list.vue140
-rw-r--r--app/assets/javascripts/diffs/components/tree_list_height.vue108
-rw-r--r--app/assets/javascripts/diffs/constants.js1
-rw-r--r--app/assets/javascripts/diffs/index.js4
-rw-r--r--app/assets/javascripts/diffs/store/actions.js13
-rw-r--r--app/assets/javascripts/diffs/store/mutations.js7
-rw-r--r--app/assets/javascripts/diffs/store/utils.js2
-rw-r--r--app/assets/javascripts/diffs/utils/file_reviews.js2
-rw-r--r--app/assets/javascripts/diffs/utils/sort_findings_by_file.js11
-rw-r--r--app/assets/javascripts/editor/constants.js5
-rw-r--r--app/assets/javascripts/editor/schema/ci.json12
-rw-r--r--app/assets/javascripts/emoji/awards_app/store/actions.js2
-rw-r--r--app/assets/javascripts/emoji/components/picker.vue4
-rw-r--r--app/assets/javascripts/emoji/constants.js12
-rw-r--r--app/assets/javascripts/emoji/index.js107
-rw-r--r--app/assets/javascripts/ensure_data.js2
-rw-r--r--app/assets/javascripts/entrypoints/analytics.js6
-rw-r--r--app/assets/javascripts/environments/components/confirm_rollback_modal.vue2
-rw-r--r--app/assets/javascripts/environments/components/environment_form.vue1
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue2
-rw-r--r--app/assets/javascripts/environments/components/environments_app.vue30
-rw-r--r--app/assets/javascripts/environments/components/environments_table.vue8
-rw-r--r--app/assets/javascripts/environments/components/kubernetes_overview.vue2
-rw-r--r--app/assets/javascripts/environments/components/kubernetes_status_bar.vue16
-rw-r--r--app/assets/javascripts/environments/constants.js4
-rw-r--r--app/assets/javascripts/environments/environment_details/index.vue2
-rw-r--r--app/assets/javascripts/environments/graphql/client.js1
-rw-r--r--app/assets/javascripts/environments/graphql/queries/environment_app.query.graphql2
-rw-r--r--app/assets/javascripts/environments/graphql/queries/flux_helm_release_status.query.graphql15
-rw-r--r--app/assets/javascripts/environments/graphql/queries/flux_kustomization_status.query.graphql10
-rw-r--r--app/assets/javascripts/environments/graphql/queries/folder.query.graphql2
-rw-r--r--app/assets/javascripts/environments/graphql/resolvers/base.js4
-rw-r--r--app/assets/javascripts/environments/graphql/resolvers/flux.js116
-rw-r--r--app/assets/javascripts/environments/graphql/resolvers/kubernetes.js51
-rw-r--r--app/assets/javascripts/error_tracking/components/error_details.vue6
-rw-r--r--app/assets/javascripts/feature_flags/components/feature_flags.vue1
-rw-r--r--app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js19
-rw-r--r--app/assets/javascripts/filtered_search/available_dropdown_mappings.js23
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js2
-rw-r--r--app/assets/javascripts/gitlab_pages/components/pages_pipeline_wizard.vue2
-rw-r--r--app/assets/javascripts/graphql_shared/constants.js1
-rw-r--r--app/assets/javascripts/graphql_shared/fragments/issue.fragment.graphql1
-rw-r--r--app/assets/javascripts/graphql_shared/possible_types.json2
-rw-r--r--app/assets/javascripts/graphql_shared/queries/groups_autocomplete.query.graphql10
-rw-r--r--app/assets/javascripts/groups/components/item_actions.vue1
-rw-r--r--app/assets/javascripts/groups/settings/init_access_dropdown.js2
-rw-r--r--app/assets/javascripts/groups_projects/components/transfer_locations.vue6
-rw-r--r--app/assets/javascripts/header.js3
-rw-r--r--app/assets/javascripts/header_search/index.js2
-rw-r--r--app/assets/javascripts/header_search/init.js2
-rw-r--r--app/assets/javascripts/helpers/help_page_helper.js2
-rw-r--r--app/assets/javascripts/helpers/init_simple_app_helper.js29
-rw-r--r--app/assets/javascripts/ide/components/ide_status_bar.vue1
-rw-r--r--app/assets/javascripts/ide/components/jobs/detail/description.vue2
-rw-r--r--app/assets/javascripts/ide/components/pipelines/list.vue2
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue9
-rw-r--r--app/assets/javascripts/ide/lib/alerts/index.js3
-rw-r--r--app/assets/javascripts/ide/stores/modules/file_templates/getters.js3
-rw-r--r--app/assets/javascripts/ide/stores/modules/terminal/mutations.js2
-rw-r--r--app/assets/javascripts/import/constants.js12
-rw-r--r--app/assets/javascripts/import/details/components/bulk_import_details_app.vue44
-rw-r--r--app/assets/javascripts/import/details/components/import_details_app.vue38
-rw-r--r--app/assets/javascripts/import/details/components/import_details_table.vue96
-rw-r--r--app/assets/javascripts/import_entities/components/import_status.vue57
-rw-r--r--app/assets/javascripts/import_entities/constants.js53
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_status.vue83
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_table.vue21
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/services/local_storage_cache.js2
-rw-r--r--app/assets/javascripts/import_entities/import_projects/components/advanced_settings.vue2
-rw-r--r--app/assets/javascripts/import_entities/import_projects/components/github_organizations_box.vue2
-rw-r--r--app/assets/javascripts/import_entities/import_projects/components/github_status_table.vue1
-rw-r--r--app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue1
-rw-r--r--app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue12
-rw-r--r--app/assets/javascripts/import_entities/import_projects/store/actions.js2
-rw-r--r--app/assets/javascripts/incidents/components/incidents_list.vue3
-rw-r--r--app/assets/javascripts/integrations/edit/components/integration_form.vue2
-rw-r--r--app/assets/javascripts/integrations/overrides/components/integration_overrides.vue2
-rw-r--r--app/assets/javascripts/invite_members/components/invite_groups_modal.vue2
-rw-r--r--app/assets/javascripts/invite_members/components/invite_members_modal.vue80
-rw-r--r--app/assets/javascripts/invite_members/components/invite_modal_base.vue10
-rw-r--r--app/assets/javascripts/invite_members/components/members_token_select.vue12
-rw-r--r--app/assets/javascripts/invite_members/components/project_select.vue1
-rw-r--r--app/assets/javascripts/invite_members/init_invite_members_modal.js3
-rw-r--r--app/assets/javascripts/invite_members/utils/member_utils.js4
-rw-r--r--app/assets/javascripts/issuable/components/locked_badge.vue9
-rw-r--r--app/assets/javascripts/issuable/components/related_issuable_item.vue11
-rw-r--r--app/assets/javascripts/issuable/components/status_badge.vue10
-rw-r--r--app/assets/javascripts/issuable/popover/components/mr_popover.vue4
-rw-r--r--app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue5
-rw-r--r--app/assets/javascripts/issues/issue.js2
-rw-r--r--app/assets/javascripts/issues/list/components/issues_list_app.vue12
-rw-r--r--app/assets/javascripts/issues/service_desk/components/service_desk_list_app.vue5
-rw-r--r--app/assets/javascripts/issues/show/components/fields/description.vue1
-rw-r--r--app/assets/javascripts/issues/show/components/header_actions.vue259
-rw-r--r--app/assets/javascripts/issues/show/components/issue_header.vue10
-rw-r--r--app/assets/javascripts/issues/show/components/sticky_header.vue14
-rw-r--r--app/assets/javascripts/issues/show/utils/parse_data.js2
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/constants.js6
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions.vue58
-rw-r--r--app/assets/javascripts/lib/graphql.js14
-rw-r--r--app/assets/javascripts/lib/utils/color_utils.js2
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js15
-rw-r--r--app/assets/javascripts/lib/utils/constants.js3
-rw-r--r--app/assets/javascripts/lib/utils/datetime/timeago_utility.js16
-rw-r--r--app/assets/javascripts/lib/utils/forms.js22
-rw-r--r--app/assets/javascripts/lib/utils/keys.js1
-rw-r--r--app/assets/javascripts/main.js2
-rw-r--r--app/assets/javascripts/members/components/avatars/group_avatar.vue48
-rw-r--r--app/assets/javascripts/members/components/icons/private_icon.vue19
-rw-r--r--app/assets/javascripts/members/components/table/member_source.vue20
-rw-r--r--app/assets/javascripts/members/components/table/members_table.vue1
-rw-r--r--app/assets/javascripts/members/components/table/role_dropdown.vue70
-rw-r--r--app/assets/javascripts/members/store/actions.js5
-rw-r--r--app/assets/javascripts/members/utils.js34
-rw-r--r--app/assets/javascripts/merge_request_tabs.js7
-rw-r--r--app/assets/javascripts/merge_requests/components/sticky_header.vue37
-rw-r--r--app/assets/javascripts/milestones/components/milestone_combobox.vue206
-rw-r--r--app/assets/javascripts/milestones/stores/mutations.js4
-rw-r--r--app/assets/javascripts/ml/experiment_tracking/routes/experiments/index/constants.js4
-rw-r--r--app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/constants.js7
-rw-r--r--app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/ml_experiments_show.vue8
-rw-r--r--app/assets/javascripts/ml/model_registry/apps/index.js4
-rw-r--r--app/assets/javascripts/ml/model_registry/apps/index_ml_models.vue61
-rw-r--r--app/assets/javascripts/ml/model_registry/apps/show_ml_model.vue59
-rw-r--r--app/assets/javascripts/ml/model_registry/apps/show_ml_model_version.vue16
-rw-r--r--app/assets/javascripts/ml/model_registry/components/model_row.vue45
-rw-r--r--app/assets/javascripts/ml/model_registry/components/search_bar.vue71
-rw-r--r--app/assets/javascripts/ml/model_registry/constants.js13
-rw-r--r--app/assets/javascripts/ml/model_registry/routes/models/index/components/ml_models_index.vue49
-rw-r--r--app/assets/javascripts/ml/model_registry/routes/models/index/components/model_row.vue35
-rw-r--r--app/assets/javascripts/ml/model_registry/routes/models/index/index.js3
-rw-r--r--app/assets/javascripts/ml/model_registry/routes/models/index/translations.js16
-rw-r--r--app/assets/javascripts/ml/model_registry/translations.js16
-rw-r--r--app/assets/javascripts/mr_notes/init.js1
-rw-r--r--app/assets/javascripts/notes/components/comment_field_layout.vue8
-rw-r--r--app/assets/javascripts/notes/components/discussion_locked_widget.vue4
-rw-r--r--app/assets/javascripts/notes/components/discussion_resolve_button.vue6
-rw-r--r--app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue2
-rw-r--r--app/assets/javascripts/notes/components/note_signed_out_widget.vue2
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue4
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue5
-rw-r--r--app/assets/javascripts/notes/components/notes_activity_header.vue2
-rw-r--r--app/assets/javascripts/notes/stores/mutations.js5
-rw-r--r--app/assets/javascripts/observability/client.js133
-rw-r--r--app/assets/javascripts/observability/components/loader/constants.js20
-rw-r--r--app/assets/javascripts/observability/components/loader/index.vue139
-rw-r--r--app/assets/javascripts/observability/components/observability_container.vue58
-rw-r--r--app/assets/javascripts/observability/components/observability_empty_state.vue36
-rw-r--r--app/assets/javascripts/observability/components/provisioned_observability_container.vue95
-rw-r--r--app/assets/javascripts/observability/components/skeleton/index.vue151
-rw-r--r--app/assets/javascripts/observability/constants.js24
-rw-r--r--app/assets/javascripts/organizations/mock_data.js27
-rw-r--r--app/assets/javascripts/organizations/new/components/app.vue18
-rw-r--r--app/assets/javascripts/organizations/new/graphql/mutations/create_organization.mutation.graphql9
-rw-r--r--app/assets/javascripts/organizations/new/graphql/mutations/organization_create.mutation.graphql9
-rw-r--r--app/assets/javascripts/organizations/new/graphql/typedefs.graphql5
-rw-r--r--app/assets/javascripts/organizations/new/index.js3
-rw-r--r--app/assets/javascripts/organizations/profile/preferences/index.js41
-rw-r--r--app/assets/javascripts/organizations/settings/general/components/app.vue14
-rw-r--r--app/assets/javascripts/organizations/settings/general/components/organization_settings.vue77
-rw-r--r--app/assets/javascripts/organizations/settings/general/graphql/mutations/update_organization.mutation.graphql9
-rw-r--r--app/assets/javascripts/organizations/settings/general/graphql/typedefs.graphql5
-rw-r--r--app/assets/javascripts/organizations/settings/general/index.js38
-rw-r--r--app/assets/javascripts/organizations/shared/components/new_edit_form.vue117
-rw-r--r--app/assets/javascripts/organizations/shared/constants.js3
-rw-r--r--app/assets/javascripts/organizations/shared/graphql/queries/organization.query.graphql9
-rw-r--r--app/assets/javascripts/organizations/shared/graphql/resolvers.js6
-rw-r--r--app/assets/javascripts/organizations/users/components/app.vue51
-rw-r--r--app/assets/javascripts/organizations/users/graphql/organization_users.query.graphql17
-rw-r--r--app/assets/javascripts/organizations/users/index.js29
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue1
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue1
-rw-r--r--app/assets/javascripts/packages_and_registries/harbor_registry/components/list/harbor_list_row.vue7
-rw-r--r--app/assets/javascripts/packages_and_registries/harbor_registry/pages/details.vue1
-rw-r--r--app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/app.vue2
-rw-r--r--app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/details_title.vue2
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/additional_metadata.vue5
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/package_files.vue5
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/package_history.vue2
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/package_title.vue1
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/package_versions_list.vue2
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue7
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/list/publish_method.vue9
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/pages/list.vue2
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/components/packages_settings.vue13
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/constants.js1
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/graphql/fragments/package_settings_fields.fragment.graphql2
-rw-r--r--app/assets/javascripts/packages_and_registries/shared/components/package_path.vue2
-rw-r--r--app/assets/javascripts/packages_and_registries/shared/components/registry_list.vue2
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/appearances/preview_sign_in/index.js3
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/index.js2
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/service_usage_data/index.js3
-rw-r--r--app/assets/javascripts/pages/explore/catalog/index.js3
-rw-r--r--app/assets/javascripts/pages/import/bulk_imports/details/index.js20
-rw-r--r--app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue39
-rw-r--r--app/assets/javascripts/pages/import/bulk_imports/history/index.js3
-rw-r--r--app/assets/javascripts/pages/import/history/components/import_history_app.vue14
-rw-r--r--app/assets/javascripts/pages/organizations/organizations/users/index.js3
-rw-r--r--app/assets/javascripts/pages/organizations/settings/general/index.js3
-rw-r--r--app/assets/javascripts/pages/passwords/new/index.js3
-rw-r--r--app/assets/javascripts/pages/profiles/preferences/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/blob/show/index.js7
-rw-r--r--app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue50
-rw-r--r--app/assets/javascripts/pages/projects/forks/new/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/page.js3
-rw-r--r--app/assets/javascripts/pages/projects/ml/model_versions/show/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/ml/models/index/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/ml/models/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/product_analytics/graphs/index.js3
-rw-r--r--app/assets/javascripts/pages/registrations/new/index.js2
-rw-r--r--app/assets/javascripts/pages/sessions/new/index.js2
-rw-r--r--app/assets/javascripts/pages/users/index.js11
-rw-r--r--app/assets/javascripts/pages/users/show/index.js5
-rw-r--r--app/assets/javascripts/pages/users/user_tabs.js5
-rw-r--r--app/assets/javascripts/performance_bar/components/performance_bar_app.vue5
-rw-r--r--app/assets/javascripts/persistent_user_callouts.js2
-rw-r--r--app/assets/javascripts/pipeline_wizard/components/wrapper.vue3
-rw-r--r--app/assets/javascripts/pipeline_wizard/pipeline_wizard.vue3
-rw-r--r--app/assets/javascripts/pipeline_wizard/templates/pages.yml20
-rw-r--r--app/assets/javascripts/profile/edit/components/profile_edit_app.vue7
-rw-r--r--app/assets/javascripts/profile/preferences/components/profile_preferences.vue6
-rw-r--r--app/assets/javascripts/profile/profile.js8
-rw-r--r--app/assets/javascripts/projects/commit/components/commit_options_dropdown.vue7
-rw-r--r--app/assets/javascripts/projects/commit/components/form_modal.vue2
-rw-r--r--app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_status.vue12
-rw-r--r--app/assets/javascripts/projects/commit_box/info/init_commit_pipeline_mini_graph.js2
-rw-r--r--app/assets/javascripts/projects/commit_box/info/init_commit_pipeline_status.js2
-rw-r--r--app/assets/javascripts/projects/commits/store/actions.js2
-rw-r--r--app/assets/javascripts/projects/default_project_templates.js4
-rw-r--r--app/assets/javascripts/projects/new/components/new_project_url_select.vue6
-rw-r--r--app/assets/javascripts/projects/pipelines/charts/components/statistics_list.vue12
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue4
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/components/view/protection_row.vue1
-rw-r--r--app/assets/javascripts/projects/settings/components/default_branch_selector.vue1
-rw-r--r--app/assets/javascripts/projects/settings/init_access_dropdown.js3
-rw-r--r--app/assets/javascripts/projects/settings/repository/branch_rules/app.vue2
-rw-r--r--app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue4
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/custom_email_form.vue57
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/custom_email_wrapper.vue1
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue13
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue59
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/service_desk_template_dropdown.vue1
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/custom_email_constants.js7
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/index.js2
-rw-r--r--app/assets/javascripts/projects/tree/components/commit_pipeline_status.vue11
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_edit.js14
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_create.js2
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_edit.vue2
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_edit_list.js2
-rw-r--r--app/assets/javascripts/releases/components/app_edit_new.vue25
-rw-r--r--app/assets/javascripts/releases/components/release_block_header.vue2
-rw-r--r--app/assets/javascripts/repository/components/blob_button_group.vue7
-rw-r--r--app/assets/javascripts/repository/components/blob_content_viewer.vue17
-rw-r--r--app/assets/javascripts/repository/components/commit_info.vue18
-rw-r--r--app/assets/javascripts/repository/components/delete_blob_modal.vue2
-rw-r--r--app/assets/javascripts/repository/components/last_commit.vue13
-rw-r--r--app/assets/javascripts/repository/components/table/row.vue6
-rw-r--r--app/assets/javascripts/search/sidebar/components/app.vue24
-rw-r--r--app/assets/javascripts/search/sidebar/components/archived_filter/data.js11
-rw-r--r--app/assets/javascripts/search/sidebar/components/blobs_filters.vue7
-rw-r--r--app/assets/javascripts/search/sidebar/components/issues_filters.vue5
-rw-r--r--app/assets/javascripts/search/sidebar/components/label_filter/index.vue61
-rw-r--r--app/assets/javascripts/search/sidebar/components/merge_requests_filters.vue7
-rw-r--r--app/assets/javascripts/search/sidebar/components/wiki_blobs_filters.vue18
-rw-r--r--app/assets/javascripts/search/sidebar/constants/index.js2
-rw-r--r--app/assets/javascripts/search/store/getters.js39
-rw-r--r--app/assets/javascripts/security_configuration/components/training_provider_list.vue2
-rw-r--r--app/assets/javascripts/sentry/init_sentry.js6
-rw-r--r--app/assets/javascripts/sentry/legacy_index.js2
-rw-r--r--app/assets/javascripts/sentry/sentry_browser_wrapper.js16
-rw-r--r--app/assets/javascripts/service_ping_consent.js35
-rw-r--r--app/assets/javascripts/sessions/new/components/update_email.vue4
-rw-r--r--app/assets/javascripts/sessions/new/constants.js1
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignee_title.vue1
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignees.vue1
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_value.vue1
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_header.vue1
-rw-r--r--app/assets/javascripts/sidebar/components/lock/edit_form.vue6
-rw-r--r--app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue25
-rw-r--r--app/assets/javascripts/sidebar/components/move/issuable_move_dropdown.vue210
-rw-r--r--app/assets/javascripts/sidebar/components/participants/sidebar_participants_widget.vue3
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar.vue1
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/reviewers.vue7
-rw-r--r--app/assets/javascripts/sidebar/components/severity/sidebar_severity_widget.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/toggle/toggle_sidebar.vue10
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js3
-rw-r--r--app/assets/javascripts/sidebar/queries/constants.js4
-rw-r--r--app/assets/javascripts/silent_mode_settings/components/app.vue10
-rw-r--r--app/assets/javascripts/single_file_diff.js5
-rw-r--r--app/assets/javascripts/snippets/components/snippet_header.vue16
-rw-r--r--app/assets/javascripts/super_sidebar/components/create_menu.vue1
-rw-r--r--app/assets/javascripts/super_sidebar/components/flyout_menu.vue4
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/command_palette/command_palette_items.vue2
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue11
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/components/global_search_default_places.vue9
-rw-r--r--app/assets/javascripts/super_sidebar/components/menu_section.vue8
-rw-r--r--app/assets/javascripts/super_sidebar/components/nav_item.vue27
-rw-r--r--app/assets/javascripts/super_sidebar/components/pinned_section.vue6
-rw-r--r--app/assets/javascripts/super_sidebar/components/sidebar_menu.vue22
-rw-r--r--app/assets/javascripts/super_sidebar/components/user_bar.vue1
-rw-r--r--app/assets/javascripts/super_sidebar/components/user_menu.vue11
-rw-r--r--app/assets/javascripts/super_sidebar/super_sidebar_bundle.js12
-rw-r--r--app/assets/javascripts/super_sidebar/utils.js2
-rw-r--r--app/assets/javascripts/tags/components/delete_tag_modal.vue1
-rw-r--r--app/assets/javascripts/terraform/components/init_command_modal.vue11
-rw-r--r--app/assets/javascripts/terraform/components/states_table.vue8
-rw-r--r--app/assets/javascripts/time_tracking/components/timelogs_app.vue2
-rw-r--r--app/assets/javascripts/token_access/components/inbound_token_access.vue6
-rw-r--r--app/assets/javascripts/token_access/components/outbound_token_access.vue6
-rw-r--r--app/assets/javascripts/token_access/components/token_projects_table.vue11
-rw-r--r--app/assets/javascripts/token_access/graphql/cache_config.js14
-rw-r--r--app/assets/javascripts/token_access/index.js3
-rw-r--r--app/assets/javascripts/tracking/constants.js3
-rw-r--r--app/assets/javascripts/tracking/dispatch_snowplow_event.js2
-rw-r--r--app/assets/javascripts/users/profile/actions/components/user_actions_app.vue8
-rw-r--r--app/assets/javascripts/users/profile/components/report_abuse_button.vue58
-rw-r--r--app/assets/javascripts/users/profile/index.js23
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue24
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/checks/conflicts.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/checks/constants.js6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/checks/message.vue27
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/checks/rebase.stories.js85
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/checks/rebase.vue220
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/checks/unresolved_discussions.vue39
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/merge_checks.stories.js16
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/merge_checks.vue19
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue16
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue118
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/app.vue7
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue9
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/constants.js5
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/accessibility/index.js123
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/accessibility/index.vue154
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports.vue52
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/test_report/index.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mixins/approvals.js10
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js7
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue19
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/merge_checks.query.graphql5
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue4
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/chronic_duration_input.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_badge_link.vue157
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_icon.vue128
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_stats_dropdown.vue122
-rw-r--r--app/assets/javascripts/vue_shared/components/entity_select/constants.js10
-rw-r--r--app/assets/javascripts/vue_shared/components/entity_select/entity_select.vue16
-rw-r--r--app/assets/javascripts/vue_shared/components/entity_select/group_select.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/entity_select/organization_select.vue150
-rw-r--r--app/assets/javascripts/vue_shared/components/entity_select/project_select.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue36
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/user_token.vue106
-rw-r--r--app/assets/javascripts/vue_shared/components/form/errors_alert.stories.js21
-rw-r--r--app/assets/javascripts/vue_shared/components/form/errors_alert.vue42
-rw-r--r--app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue16
-rw-r--r--app/assets/javascripts/vue_shared/components/list_selector/constants.js6
-rw-r--r--app/assets/javascripts/vue_shared/components/list_selector/group_item.vue55
-rw-r--r--app/assets/javascripts/vue_shared/components/list_selector/index.vue193
-rw-r--r--app/assets/javascripts/vue_shared/components/list_selector/user_item.vue55
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown_drawer/utils/fetch.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/system_note.vue25
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/components/blame_info.vue20
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_new.vue66
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/queries/blame_data.query.graphql36
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/source_viewer_new.vue122
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/utils.js19
-rw-r--r--app/assets/javascripts/vue_shared/components/toggle_labels.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/users_table/constants.js3
-rw-r--r--app/assets/javascripts/vue_shared/components/users_table/user_avatar.vue76
-rw-r--r--app/assets/javascripts/vue_shared/components/users_table/users_table.vue110
-rw-r--r--app/assets/javascripts/vue_shared/components/web_ide_link.vue4
-rw-r--r--app/assets/javascripts/vue_shared/directives/safe_html.js2
-rw-r--r--app/assets/javascripts/vue_shared/gl_feature_flags_plugin.js11
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_body.vue6
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue2
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_show_root.vue1
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue17
-rw-r--r--app/assets/javascripts/webhooks/components/push_events.vue8
-rw-r--r--app/assets/javascripts/work_items/components/notes/system_note.vue22
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_add_note.vue10
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue7
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_note.vue11
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue4
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_note_awards_list.vue2
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_note_signed_out.vue5
-rw-r--r--app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue46
-rw-r--r--app/assets/javascripts/work_items/components/shared/work_item_links_menu.vue28
-rw-r--r--app/assets/javascripts/work_items/components/shared/work_item_token_input.vue126
-rw-r--r--app/assets/javascripts/work_items/components/work_item_actions.vue104
-rw-r--r--app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue4
-rw-r--r--app/assets/javascripts/work_items/components/work_item_award_emoji.vue17
-rw-r--r--app/assets/javascripts/work_items/components/work_item_created_updated.vue2
-rw-r--r--app/assets/javascripts/work_items/components/work_item_description.vue10
-rw-r--r--app/assets/javascripts/work_items/components/work_item_description_rendered.vue2
-rw-r--r--app/assets/javascripts/work_items/components/work_item_detail.vue132
-rw-r--r--app/assets/javascripts/work_items/components/work_item_due_date.vue2
-rw-r--r--app/assets/javascripts/work_items/components/work_item_labels.vue7
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_children_wrapper.vue11
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue9
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue23
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue16
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_tree_children.vue6
-rw-r--r--app/assets/javascripts/work_items/components/work_item_milestone.vue134
-rw-r--r--app/assets/javascripts/work_items/components/work_item_notes.vue5
-rw-r--r--app/assets/javascripts/work_items/components/work_item_parent.vue58
-rw-r--r--app/assets/javascripts/work_items/components/work_item_relationships/work_item_add_relationship_form.vue4
-rw-r--r--app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationship_list.vue6
-rw-r--r--app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationships.vue28
-rw-r--r--app/assets/javascripts/work_items/components/work_item_state_toggle.vue131
-rw-r--r--app/assets/javascripts/work_items/components/work_item_state_toggle_button.vue113
-rw-r--r--app/assets/javascripts/work_items/components/work_item_title.vue9
-rw-r--r--app/assets/javascripts/work_items/components/work_item_todos.vue9
-rw-r--r--app/assets/javascripts/work_items/constants.js27
-rw-r--r--app/assets/javascripts/work_items/graphql/award_emoji.query.graphql2
-rw-r--r--app/assets/javascripts/work_items/graphql/group_award_emoji.query.graphql27
-rw-r--r--app/assets/javascripts/work_items/graphql/group_work_items.query.graphql17
-rw-r--r--app/assets/javascripts/work_items/graphql/project_work_items.query.graphql11
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item.fragment.graphql4
-rw-r--r--app/assets/javascripts/work_items/list/components/work_items_list_app.vue2
-rw-r--r--app/assets/javascripts/work_items/pages/create_work_item.vue2
-rw-r--r--app/assets/stylesheets/application.scss4
-rw-r--r--app/assets/stylesheets/application_utilities.scss2
-rw-r--r--app/assets/stylesheets/components/detail_page.scss3
-rw-r--r--app/assets/stylesheets/framework/buttons.scss19
-rw-r--r--app/assets/stylesheets/framework/diffs.scss1
-rw-r--r--app/assets/stylesheets/framework/files.scss1
-rw-r--r--app/assets/stylesheets/framework/header.scss9
-rw-r--r--app/assets/stylesheets/framework/highlight.scss2
-rw-r--r--app/assets/stylesheets/framework/icons.scss87
-rw-r--r--app/assets/stylesheets/framework/layout.scss6
-rw-r--r--app/assets/stylesheets/framework/mixins.scss9
-rw-r--r--app/assets/stylesheets/framework/page_header.scss8
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss6
-rw-r--r--app/assets/stylesheets/framework/variables.scss18
-rw-r--r--app/assets/stylesheets/page_bundles/_system_note_styles.scss59
-rw-r--r--app/assets/stylesheets/page_bundles/boards.scss6
-rw-r--r--app/assets/stylesheets/page_bundles/branches.scss4
-rw-r--r--app/assets/stylesheets/page_bundles/build.scss8
-rw-r--r--app/assets/stylesheets/page_bundles/ci_status.scss1
-rw-r--r--app/assets/stylesheets/page_bundles/issuable.scss53
-rw-r--r--app/assets/stylesheets/page_bundles/merge_request.scss18
-rw-r--r--app/assets/stylesheets/page_bundles/merge_requests.scss99
-rw-r--r--app/assets/stylesheets/page_bundles/pipeline.scss86
-rw-r--r--app/assets/stylesheets/page_bundles/pipelines.scss4
-rw-r--r--app/assets/stylesheets/page_bundles/profile.scss6
-rw-r--r--app/assets/stylesheets/page_bundles/projects.scss8
-rw-r--r--app/assets/stylesheets/page_bundles/wiki.scss13
-rw-r--r--app/assets/stylesheets/page_bundles/work_items.scss25
-rw-r--r--app/assets/stylesheets/pages/commits.scss3
-rw-r--r--app/assets/stylesheets/pages/issues.scss19
-rw-r--r--app/assets/stylesheets/pages/notes.scss42
-rw-r--r--app/assets/stylesheets/print.scss1
-rw-r--r--app/assets/stylesheets/startup/_cloaking.scss15
-rw-r--r--app/assets/stylesheets/startup/startup-dark.scss1928
-rw-r--r--app/assets/stylesheets/startup/startup-general.scss1781
-rw-r--r--app/assets/stylesheets/startup/startup-signin.scss852
-rw-r--r--app/assets/stylesheets/themes/dark_mode_overrides.scss12
-rw-r--r--app/assets/stylesheets/themes/theme_blue.scss2
-rw-r--r--app/assets/stylesheets/themes/theme_gray.scss2
-rw-r--r--app/assets/stylesheets/themes/theme_green.scss2
-rw-r--r--app/assets/stylesheets/themes/theme_indigo.scss2
-rw-r--r--app/assets/stylesheets/themes/theme_light_blue.scss2
-rw-r--r--app/assets/stylesheets/themes/theme_light_gray.scss2
-rw-r--r--app/assets/stylesheets/themes/theme_light_green.scss2
-rw-r--r--app/assets/stylesheets/themes/theme_light_indigo.scss2
-rw-r--r--app/assets/stylesheets/themes/theme_light_red.scss2
-rw-r--r--app/assets/stylesheets/themes/theme_red.scss2
-rw-r--r--app/assets/stylesheets/tmp_utilities.scss32
-rw-r--r--app/assets/stylesheets/utilities.scss3
-rw-r--r--app/components/projects/ml/models_index_component.rb11
-rw-r--r--app/components/projects/ml/show_ml_model_component.rb15
-rw-r--r--app/components/projects/ml/show_ml_model_version_component.html.haml1
-rw-r--r--app/components/projects/ml/show_ml_model_version_component.rb32
-rw-r--r--app/controllers/acme_challenges_controller.rb4
-rw-r--r--app/controllers/admin/abuse_reports_controller.rb1
-rw-r--r--app/controllers/admin/application_settings_controller.rb21
-rw-r--r--app/controllers/admin/dashboard_controller.rb3
-rw-r--r--app/controllers/admin/spam_logs_controller.rb4
-rw-r--r--app/controllers/admin/users_controller.rb25
-rw-r--r--app/controllers/application_controller.rb2
-rw-r--r--app/controllers/autocomplete_controller.rb36
-rw-r--r--app/controllers/base_action_controller.rb31
-rw-r--r--app/controllers/chaos_controller.rb4
-rw-r--r--app/controllers/concerns/creates_commit.rb14
-rw-r--r--app/controllers/concerns/issuable_actions.rb2
-rw-r--r--app/controllers/concerns/render_access_tokens.rb1
-rw-r--r--app/controllers/concerns/wiki_actions.rb6
-rw-r--r--app/controllers/dashboard_controller.rb1
-rw-r--r--app/controllers/explore/catalog_controller.rb20
-rw-r--r--app/controllers/external_redirect/external_redirect_controller.rb36
-rw-r--r--app/controllers/groups/settings/applications_controller.rb2
-rw-r--r--app/controllers/groups/settings/ci_cd_controller.rb1
-rw-r--r--app/controllers/groups/work_items_controller.rb7
-rw-r--r--app/controllers/groups_controller.rb5
-rw-r--r--app/controllers/health_controller.rb4
-rw-r--r--app/controllers/import/bulk_imports_controller.rb8
-rw-r--r--app/controllers/jira_connect/subscriptions_controller.rb2
-rw-r--r--app/controllers/jwt_controller.rb8
-rw-r--r--app/controllers/metrics_controller.rb4
-rw-r--r--app/controllers/oauth/jira_dvcs/authorizations_controller.rb86
-rw-r--r--app/controllers/organizations/organizations_controller.rb4
-rw-r--r--app/controllers/profiles/comment_templates_controller.rb2
-rw-r--r--app/controllers/profiles/preferences_controller.rb1
-rw-r--r--app/controllers/profiles_controller.rb3
-rw-r--r--app/controllers/projects/application_controller.rb13
-rw-r--r--app/controllers/projects/artifacts_controller.rb6
-rw-r--r--app/controllers/projects/blob_controller.rb2
-rw-r--r--app/controllers/projects/environments_controller.rb15
-rw-r--r--app/controllers/projects/group_links_controller.rb51
-rw-r--r--app/controllers/projects/incidents_controller.rb2
-rw-r--r--app/controllers/projects/issues_controller.rb6
-rw-r--r--app/controllers/projects/jobs_controller.rb30
-rw-r--r--app/controllers/projects/merge_requests/drafts_controller.rb20
-rw-r--r--app/controllers/projects/merge_requests_controller.rb22
-rw-r--r--app/controllers/projects/ml/model_versions_controller.rb24
-rw-r--r--app/controllers/projects/ml/models_controller.rb31
-rw-r--r--app/controllers/projects/pipelines_controller.rb7
-rw-r--r--app/controllers/projects/raw_controller.rb3
-rw-r--r--app/controllers/projects/repositories_controller.rb2
-rw-r--r--app/controllers/projects/service_desk_controller.rb5
-rw-r--r--app/controllers/projects/settings/ci_cd_controller.rb1
-rw-r--r--app/controllers/projects/tree_controller.rb2
-rw-r--r--app/controllers/projects/work_items_controller.rb1
-rw-r--r--app/controllers/projects_controller.rb11
-rw-r--r--app/controllers/repositories/git_http_client_controller.rb4
-rw-r--r--app/controllers/repositories/git_http_controller.rb10
-rw-r--r--app/controllers/repositories/lfs_api_controller.rb4
-rw-r--r--app/controllers/search_controller.rb19
-rw-r--r--app/experiments/ios_specific_templates_experiment.rb32
-rw-r--r--app/finders/ci/catalog/resources/versions_finder.rb58
-rw-r--r--app/finders/ci/runners_finder.rb17
-rw-r--r--app/finders/data_transfer/mocked_transfer_finder.rb27
-rw-r--r--app/finders/merge_requests_finder.rb11
-rw-r--r--app/finders/organizations/user_organizations_finder.rb26
-rw-r--r--app/finders/packages/packages_finder.rb1
-rw-r--r--app/finders/packages/pypi/packages_finder.rb11
-rw-r--r--app/finders/projects/ml/model_finder.rb52
-rw-r--r--app/finders/projects_finder.rb14
-rw-r--r--app/finders/user_group_notification_settings_finder.rb17
-rw-r--r--app/graphql/mutations/base_mutation.rb6
-rw-r--r--app/graphql/mutations/ci/catalog/resources/create.rb36
-rw-r--r--app/graphql/mutations/ci/catalog/resources/unpublish.rb30
-rw-r--r--app/graphql/mutations/ci/job/cancel.rb2
-rw-r--r--app/graphql/mutations/ci/pipeline/cancel.rb2
-rw-r--r--app/graphql/mutations/commits/create.rb2
-rw-r--r--app/graphql/mutations/container_registry/protection/rule/create.rb63
-rw-r--r--app/graphql/mutations/merge_requests/accept.rb4
-rw-r--r--app/graphql/mutations/namespace/package_settings/update.rb10
-rw-r--r--app/graphql/mutations/organizations/create.rb35
-rw-r--r--app/graphql/mutations/packages/protection/rule/delete.rb40
-rw-r--r--app/graphql/mutations/saved_replies/base.rb4
-rw-r--r--app/graphql/mutations/saved_replies/create.rb2
-rw-r--r--app/graphql/mutations/saved_replies/destroy.rb2
-rw-r--r--app/graphql/mutations/saved_replies/update.rb2
-rw-r--r--app/graphql/resolvers/analytics/cycle_analytics/deployment_count_resolver.rb2
-rw-r--r--app/graphql/resolvers/analytics/cycle_analytics/issue_count_resolver.rb2
-rw-r--r--app/graphql/resolvers/ci/catalog/resource_resolver.rb48
-rw-r--r--app/graphql/resolvers/ci/catalog/resources_resolver.rb54
-rw-r--r--app/graphql/resolvers/ci/catalog/versions_resolver.rb24
-rw-r--r--app/graphql/resolvers/ci/runners_resolver.rb14
-rw-r--r--app/graphql/resolvers/concerns/caching_array_resolver.rb2
-rw-r--r--app/graphql/resolvers/concerns/work_items/shared_filter_arguments.rb7
-rw-r--r--app/graphql/resolvers/container_repository_tags_resolver.rb56
-rw-r--r--app/graphql/resolvers/data_transfer/group_data_transfer_resolver.rb16
-rw-r--r--app/graphql/resolvers/data_transfer/project_data_transfer_resolver.rb16
-rw-r--r--app/graphql/resolvers/group_issues_resolver.rb2
-rw-r--r--app/graphql/resolvers/issues/base_parent_resolver.rb9
-rw-r--r--app/graphql/resolvers/issues_resolver.rb7
-rw-r--r--app/graphql/resolvers/namespaces/work_items_resolver.rb2
-rw-r--r--app/graphql/resolvers/packages_base_resolver.rb6
-rw-r--r--app/graphql/resolvers/project_issues_resolver.rb2
-rw-r--r--app/graphql/resolvers/project_members_resolver.rb4
-rw-r--r--app/graphql/resolvers/project_milestones_resolver.rb1
-rw-r--r--app/graphql/resolvers/projects/snippets_resolver.rb4
-rw-r--r--app/graphql/resolvers/projects_resolver.rb42
-rw-r--r--app/graphql/resolvers/saved_reply_resolver.rb2
-rw-r--r--app/graphql/resolvers/snippets_resolver.rb4
-rw-r--r--app/graphql/resolvers/users/frecent_groups_resolver.rb23
-rw-r--r--app/graphql/resolvers/users/frecent_projects_resolver.rb21
-rw-r--r--app/graphql/resolvers/users/organizations_resolver.rb18
-rw-r--r--app/graphql/resolvers/users/snippets_resolver.rb4
-rw-r--r--app/graphql/resolvers/work_items/linked_items_resolver.rb26
-rw-r--r--app/graphql/types/abuse_report_type.rb9
-rw-r--r--app/graphql/types/analytics/cycle_analytics/value_stream_type.rb32
-rw-r--r--app/graphql/types/base_argument.rb22
-rw-r--r--app/graphql/types/base_input_object.rb2
-rw-r--r--app/graphql/types/ci/catalog/resource_scope_enum.rb14
-rw-r--r--app/graphql/types/ci/catalog/resource_sort_enum.rb19
-rw-r--r--app/graphql/types/ci/catalog/resource_type.rb120
-rw-r--r--app/graphql/types/ci/pipeline_status_enum.rb1
-rw-r--r--app/graphql/types/container_registry/protection/rule_access_level_enum.rb17
-rw-r--r--app/graphql/types/container_registry/protection/rule_type.rb41
-rw-r--r--app/graphql/types/container_repository_details_type.rb3
-rw-r--r--app/graphql/types/data_transfer/project_data_transfer_type.rb1
-rw-r--r--app/graphql/types/group_member_type.rb4
-rw-r--r--app/graphql/types/issuable_state_enum.rb5
-rw-r--r--app/graphql/types/merge_request_review_state_enum.rb8
-rw-r--r--app/graphql/types/merge_request_type.rb3
-rw-r--r--app/graphql/types/mutation_type.rb33
-rw-r--r--app/graphql/types/namespace/package_settings_type.rb15
-rw-r--r--app/graphql/types/notes/noteable_interface.rb2
-rw-r--r--app/graphql/types/organizations/organization_type.rb4
-rw-r--r--app/graphql/types/organizations/organization_user_badge_type.rb22
-rw-r--r--app/graphql/types/organizations/organization_user_type.rb4
-rw-r--r--app/graphql/types/packages/package_base_type.rb10
-rw-r--r--app/graphql/types/packages/protection/rule_type.rb5
-rw-r--r--app/graphql/types/packages/pypi/metadatum_type.rb9
-rw-r--r--app/graphql/types/permission_types/abuse_report.rb11
-rw-r--r--app/graphql/types/permission_types/base_permission_type.rb2
-rw-r--r--app/graphql/types/permission_types/ci/job.rb1
-rw-r--r--app/graphql/types/permission_types/ci/pipeline.rb1
-rw-r--r--app/graphql/types/permission_types/package.rb12
-rw-r--r--app/graphql/types/project_type.rb6
-rw-r--r--app/graphql/types/projects/detailed_import_status_type.rb39
-rw-r--r--app/graphql/types/query_type.rb22
-rw-r--r--app/graphql/types/security/codequality_reports_comparer/degradation_type.rb3
-rw-r--r--app/graphql/types/security/codequality_reports_comparer/report_generation_status_enum.rb16
-rw-r--r--app/graphql/types/security/codequality_reports_comparer/report_type.rb2
-rw-r--r--app/graphql/types/security/codequality_reports_comparer/status_enum.rb8
-rw-r--r--app/graphql/types/security/codequality_reports_comparer/summary_type.rb2
-rw-r--r--app/graphql/types/security/codequality_reports_comparer_type.rb7
-rw-r--r--app/graphql/types/user_interface.rb16
-rw-r--r--app/graphql/types/work_items/linked_item_type.rb22
-rw-r--r--app/graphql/types/work_items/widgets/linked_items_type.rb1
-rw-r--r--app/helpers/admin/user_actions_helper.rb15
-rw-r--r--app/helpers/application_helper.rb9
-rw-r--r--app/helpers/application_settings_helper.rb1
-rw-r--r--app/helpers/auth_helper.rb11
-rw-r--r--app/helpers/blob_helper.rb2
-rw-r--r--app/helpers/ci/catalog/resources_helper.rb4
-rw-r--r--app/helpers/ci/pipelines_helper.rb11
-rw-r--r--app/helpers/ci/status_helper.rb112
-rw-r--r--app/helpers/clusters_helper.rb2
-rw-r--r--app/helpers/colors_helper.rb12
-rw-r--r--app/helpers/dropdowns_helper.rb2
-rw-r--r--app/helpers/environment_helper.rb48
-rw-r--r--app/helpers/environments_helper.rb13
-rw-r--r--app/helpers/events_helper.rb44
-rw-r--r--app/helpers/graph_helper.rb13
-rw-r--r--app/helpers/ide_helper.rb10
-rw-r--r--app/helpers/members_helper.rb11
-rw-r--r--app/helpers/merge_requests_helper.rb11
-rw-r--r--app/helpers/nav/new_dropdown_helper.rb11
-rw-r--r--app/helpers/nav_helper.rb26
-rw-r--r--app/helpers/notes_helper.rb24
-rw-r--r--app/helpers/operations_helper.rb2
-rw-r--r--app/helpers/organizations/organization_helper.rb21
-rw-r--r--app/helpers/preferences_helper.rb8
-rw-r--r--app/helpers/projects/pipeline_helper.rb2
-rw-r--r--app/helpers/projects_helper.rb8
-rw-r--r--app/helpers/sidebars_helper.rb4
-rw-r--r--app/helpers/sorting_helper.rb30
-rw-r--r--app/helpers/users/callouts_helper.rb24
-rw-r--r--app/helpers/users_helper.rb33
-rw-r--r--app/helpers/visibility_level_helper.rb10
-rw-r--r--app/helpers/vite_helper.rb16
-rw-r--r--app/helpers/wiki_helper.rb8
-rw-r--r--app/mailers/emails/issues.rb2
-rw-r--r--app/mailers/emails/merge_requests.rb2
-rw-r--r--app/mailers/emails/service_desk.rb2
-rw-r--r--app/models/abuse_report.rb16
-rw-r--r--app/models/active_session.rb36
-rw-r--r--app/models/activity_pub.rb7
-rw-r--r--app/models/activity_pub/releases_subscription.rb22
-rw-r--r--app/models/ai/service_access_token.rb10
-rw-r--r--app/models/analytics/cycle_analytics/value_stream.rb6
-rw-r--r--app/models/application_setting.rb9
-rw-r--r--app/models/application_setting_implementation.rb12
-rw-r--r--app/models/bulk_imports/entity.rb4
-rw-r--r--app/models/bulk_imports/failure.rb4
-rw-r--r--app/models/ci/bridge.rb4
-rw-r--r--app/models/ci/build.rb22
-rw-r--r--app/models/ci/build_trace_chunks/redis_base.rb6
-rw-r--r--app/models/ci/build_trace_metadata.rb2
-rw-r--r--app/models/ci/catalog/components_project.rb7
-rw-r--r--app/models/ci/catalog/listing.rb49
-rw-r--r--app/models/ci/catalog/resource.rb44
-rw-r--r--app/models/ci/catalog/resources/component.rb2
-rw-r--r--app/models/ci/catalog/resources/version.rb96
-rw-r--r--app/models/ci/job_artifact.rb2
-rw-r--r--app/models/ci/job_token/scope.rb5
-rw-r--r--app/models/ci/pipeline.rb48
-rw-r--r--app/models/ci/ref.rb17
-rw-r--r--app/models/ci/runner.rb4
-rw-r--r--app/models/ci/runner_manager.rb21
-rw-r--r--app/models/ci/sources/pipeline.rb2
-rw-r--r--app/models/ci/stage.rb5
-rw-r--r--app/models/commit.rb8
-rw-r--r--app/models/commit_status.rb32
-rw-r--r--app/models/concerns/analytics/cycle_analytics/stage_event_model.rb1
-rw-r--r--app/models/concerns/can_move_repository_storage.rb19
-rw-r--r--app/models/concerns/ci/has_status.rb11
-rw-r--r--app/models/concerns/commit_signature.rb1
-rw-r--r--app/models/concerns/diff_positionable_note.rb1
-rw-r--r--app/models/concerns/enums/package_metadata.rb3
-rw-r--r--app/models/concerns/enums/sbom.rb3
-rw-r--r--app/models/concerns/merge_request_reviewer_state.rb3
-rw-r--r--app/models/concerns/repository_storage_movable.rb27
-rw-r--r--app/models/concerns/restricted_signup.rb1
-rw-r--r--app/models/concerns/token_authenticatable_strategies/base.rb2
-rw-r--r--app/models/concerns/use_sql_function_for_primary_key_lookups.rb39
-rw-r--r--app/models/concerns/users/visitable.rb39
-rw-r--r--app/models/container_repository.rb31
-rw-r--r--app/models/deployment.rb3
-rw-r--r--app/models/environment.rb8
-rw-r--r--app/models/group.rb93
-rw-r--r--app/models/guest.rb9
-rw-r--r--app/models/integration.rb21
-rw-r--r--app/models/integrations/apple_app_store.rb6
-rw-r--r--app/models/integrations/asana.rb6
-rw-r--r--app/models/integrations/assembla.rb4
-rw-r--r--app/models/integrations/bamboo.rb6
-rw-r--r--app/models/integrations/base_chat_notification.rb4
-rw-r--r--app/models/integrations/base_slack_notification.rb2
-rw-r--r--app/models/integrations/bugzilla.rb6
-rw-r--r--app/models/integrations/buildkite.rb12
-rw-r--r--app/models/integrations/campfire.rb6
-rw-r--r--app/models/integrations/clickup.rb6
-rw-r--r--app/models/integrations/confluence.rb4
-rw-r--r--app/models/integrations/custom_issue_tracker.rb6
-rw-r--r--app/models/integrations/datadog.rb6
-rw-r--r--app/models/integrations/discord.rb14
-rw-r--r--app/models/integrations/drone_ci.rb12
-rw-r--r--app/models/integrations/emails_on_push.rb4
-rw-r--r--app/models/integrations/ewm.rb6
-rw-r--r--app/models/integrations/external_wiki.rb14
-rw-r--r--app/models/integrations/gitlab_slack_application.rb4
-rw-r--r--app/models/integrations/google_play.rb6
-rw-r--r--app/models/integrations/hangouts_chat.rb6
-rw-r--r--app/models/integrations/harbor.rb26
-rw-r--r--app/models/integrations/irker.rb38
-rw-r--r--app/models/integrations/jenkins.rb6
-rw-r--r--app/models/integrations/jira.rb24
-rw-r--r--app/models/integrations/mattermost.rb6
-rw-r--r--app/models/integrations/mattermost_slash_commands.rb4
-rw-r--r--app/models/integrations/microsoft_teams.rb6
-rw-r--r--app/models/integrations/mock_ci.rb4
-rw-r--r--app/models/integrations/mock_monitoring.rb4
-rw-r--r--app/models/integrations/packagist.rb4
-rw-r--r--app/models/integrations/pipelines_email.rb4
-rw-r--r--app/models/integrations/pivotaltracker.rb6
-rw-r--r--app/models/integrations/prometheus.rb4
-rw-r--r--app/models/integrations/pumble.rb6
-rw-r--r--app/models/integrations/pushover.rb4
-rw-r--r--app/models/integrations/redmine.rb6
-rw-r--r--app/models/integrations/shimo.rb4
-rw-r--r--app/models/integrations/slack.rb4
-rw-r--r--app/models/integrations/slack_slash_commands.rb4
-rw-r--r--app/models/integrations/squash_tm.rb6
-rw-r--r--app/models/integrations/teamcity.rb6
-rw-r--r--app/models/integrations/telegram.rb6
-rw-r--r--app/models/integrations/unify_circuit.rb6
-rw-r--r--app/models/integrations/webex_teams.rb6
-rw-r--r--app/models/integrations/youtrack.rb6
-rw-r--r--app/models/integrations/zentao.rb8
-rw-r--r--app/models/member.rb11
-rw-r--r--app/models/members/members/members_with_parents.rb105
-rw-r--r--app/models/members/project_member.rb6
-rw-r--r--app/models/merge_request.rb58
-rw-r--r--app/models/merge_request_context_commit_diff_file.rb1
-rw-r--r--app/models/merge_request_diff_commit.rb10
-rw-r--r--app/models/ml/candidate.rb2
-rw-r--r--app/models/ml/model.rb14
-rw-r--r--app/models/ml/model_metadata.rb13
-rw-r--r--app/models/ml/model_version.rb19
-rw-r--r--app/models/namespace.rb31
-rw-r--r--app/models/namespace_setting.rb10
-rw-r--r--app/models/network/graph.rb20
-rw-r--r--app/models/note.rb6
-rw-r--r--app/models/organizations/organization.rb4
-rw-r--r--app/models/packages/npm/metadata_cache.rb6
-rw-r--r--app/models/packages/nuget/symbol.rb3
-rw-r--r--app/models/packages/protection/rule.rb8
-rw-r--r--app/models/packages/pypi/metadatum.rb16
-rw-r--r--app/models/packages/tag.rb7
-rw-r--r--app/models/pages/lookup_path.rb12
-rw-r--r--app/models/pages_deployment.rb17
-rw-r--r--app/models/pages_domain.rb22
-rw-r--r--app/models/personal_access_token.rb7
-rw-r--r--app/models/project.rb96
-rw-r--r--app/models/project_feature_usage.rb13
-rw-r--r--app/models/project_pages_metadatum.rb3
-rw-r--r--app/models/project_snippet.rb2
-rw-r--r--app/models/projects/repository_storage_move.rb5
-rw-r--r--app/models/protected_branch.rb1
-rw-r--r--app/models/repository.rb8
-rw-r--r--app/models/resource_label_event.rb2
-rw-r--r--app/models/service_desk/custom_email_credential.rb11
-rw-r--r--app/models/snippet.rb1
-rw-r--r--app/models/snippet_repository.rb5
-rw-r--r--app/models/system/broadcast_message.rb2
-rw-r--r--app/models/system_note_metadata.rb1
-rw-r--r--app/models/upload.rb2
-rw-r--r--app/models/user.rb24
-rw-r--r--app/models/user_custom_attribute.rb1
-rw-r--r--app/models/user_detail.rb23
-rw-r--r--app/models/user_preference.rb14
-rw-r--r--app/models/users/anonymous.rb11
-rw-r--r--app/models/users/callout.rb7
-rw-r--r--app/models/users/credit_card_validation.rb6
-rw-r--r--app/models/users/group_visit.rb7
-rw-r--r--app/models/users/phone_number_validation.rb4
-rw-r--r--app/models/users/project_visit.rb7
-rw-r--r--app/models/vs_code/settings/vs_code_setting.rb4
-rw-r--r--app/models/wiki_page.rb7
-rw-r--r--app/models/work_item.rb17
-rw-r--r--app/policies/abuse_report_policy.rb1
-rw-r--r--app/policies/analytics/cycle_analytics/value_stream_policy.rb9
-rw-r--r--app/policies/base_policy.rb2
-rw-r--r--app/policies/ci/build_policy.rb2
-rw-r--r--app/policies/ci/deployable_policy.rb5
-rw-r--r--app/policies/ci/pipeline_policy.rb6
-rw-r--r--app/policies/concerns/policy_actor.rb4
-rw-r--r--app/policies/container_registry/protection/rule_policy.rb9
-rw-r--r--app/policies/global_policy.rb4
-rw-r--r--app/policies/group_group_link_policy.rb8
-rw-r--r--app/policies/group_policy.rb1
-rw-r--r--app/policies/issue_policy.rb5
-rw-r--r--app/policies/namespaces/group_project_namespace_shared_policy.rb1
-rw-r--r--app/policies/project_group_link_policy.rb4
-rw-r--r--app/policies/project_import_state_policy.rb5
-rw-r--r--app/policies/project_policy.rb88
-rw-r--r--app/policies/user_policy.rb1
-rw-r--r--app/presenters/clusters/cluster_presenter.rb2
-rw-r--r--app/presenters/commit_status_presenter.rb1
-rw-r--r--app/presenters/member_presenter.rb9
-rw-r--r--app/presenters/ml/model_presenter.rb22
-rw-r--r--app/presenters/ml/model_version_presenter.rb25
-rw-r--r--app/presenters/project_presenter.rb23
-rw-r--r--app/presenters/projects/security/configuration_presenter.rb4
-rw-r--r--app/presenters/user_presenter.rb1
-rw-r--r--app/serializers/build_details_entity.rb2
-rw-r--r--app/serializers/ci/job_entity.rb2
-rw-r--r--app/serializers/ci/pipeline_entity.rb2
-rw-r--r--app/serializers/deployment_entity.rb2
-rw-r--r--app/serializers/group_link/group_group_link_entity.rb2
-rw-r--r--app/serializers/group_link/group_link_entity.rb22
-rw-r--r--app/serializers/group_link/project_group_link_entity.rb2
-rw-r--r--app/serializers/issue_entity.rb6
-rw-r--r--app/serializers/member_entity.rb5
-rw-r--r--app/serializers/merge_request_noteable_entity.rb8
-rw-r--r--app/serializers/merge_request_widget_entity.rb8
-rw-r--r--app/serializers/review_app_setup_entity.rb2
-rw-r--r--app/services/activity_pub/accept_follow_service.rb55
-rw-r--r--app/services/activity_pub/inbox_resolver_service.rb50
-rw-r--r--app/services/activity_pub/third_party_error.rb5
-rw-r--r--app/services/admin/plan_limits/update_service.rb52
-rw-r--r--app/services/auto_merge/base_service.rb25
-rw-r--r--app/services/boards/lists/move_service.rb7
-rw-r--r--app/services/bulk_imports/batched_relation_export_service.rb8
-rw-r--r--app/services/bulk_imports/file_download_service.rb4
-rw-r--r--app/services/bulk_imports/process_service.rb11
-rw-r--r--app/services/bulk_imports/relation_batch_export_service.rb14
-rw-r--r--app/services/bulk_imports/relation_export_service.rb8
-rw-r--r--app/services/ci/build_cancel_service.rb2
-rw-r--r--app/services/ci/cancel_pipeline_service.rb41
-rw-r--r--app/services/ci/catalog/resources/create_service.rb31
-rw-r--r--app/services/ci/catalog/resources/release_service.rb46
-rw-r--r--app/services/ci/catalog/resources/validate_service.rb26
-rw-r--r--app/services/ci/catalog/resources/versions/create_service.rb111
-rw-r--r--app/services/ci/destroy_pipeline_service.rb2
-rw-r--r--app/services/ci/enqueue_job_service.rb11
-rw-r--r--app/services/ci/pipeline_creation/cancel_redundant_pipelines_service.rb2
-rw-r--r--app/services/ci/pipelines/update_metadata_service.rb31
-rw-r--r--app/services/ci/refs/enqueue_pipelines_to_unlock_service.rb33
-rw-r--r--app/services/ci/retry_job_service.rb4
-rw-r--r--app/services/concerns/update_repository_storage_methods.rb4
-rw-r--r--app/services/container_registry/protection/create_rule_service.rb40
-rw-r--r--app/services/draft_notes/publish_service.rb4
-rw-r--r--app/services/environments/auto_recover_service.rb44
-rw-r--r--app/services/git/branch_hooks_service.rb1
-rw-r--r--app/services/google_cloud/generate_pipeline_service.rb2
-rw-r--r--app/services/groups/ssh_certificates/create_service.rb7
-rw-r--r--app/services/groups/ssh_certificates/destroy_service.rb9
-rw-r--r--app/services/import/validate_remote_git_endpoint_service.rb79
-rw-r--r--app/services/jira_connect_subscriptions/create_service.rb4
-rw-r--r--app/services/members/create_service.rb55
-rw-r--r--app/services/merge_requests/mark_reviewer_reviewed_service.rb21
-rw-r--r--app/services/merge_requests/mergeability/check_base_service.rb5
-rw-r--r--app/services/merge_requests/mergeability/check_ci_status_service.rb2
-rw-r--r--app/services/merge_requests/mergeability/check_discussions_status_service.rb2
-rw-r--r--app/services/merge_requests/mergeability/check_rebase_status_service.rb2
-rw-r--r--app/services/merge_requests/mergeability/detailed_merge_status_service.rb6
-rw-r--r--app/services/merge_requests/mergeability/run_checks_service.rb2
-rw-r--r--app/services/merge_requests/push_options_handler_service.rb11
-rw-r--r--app/services/merge_requests/update_reviewer_state_service.rb34
-rw-r--r--app/services/merge_requests/update_service.rb5
-rw-r--r--app/services/ml/create_candidate_service.rb37
-rw-r--r--app/services/ml/create_model_service.rb51
-rw-r--r--app/services/ml/experiment_tracking/candidate_repository.rb18
-rw-r--r--app/services/ml/find_model_service.rb14
-rw-r--r--app/services/ml/find_or_create_model_service.rb18
-rw-r--r--app/services/ml/find_or_create_model_version_service.rb15
-rw-r--r--app/services/ml/model_versions/get_model_version_service.rb21
-rw-r--r--app/services/ml/update_model_service.rb16
-rw-r--r--app/services/notes/create_service.rb6
-rw-r--r--app/services/notification_service.rb4
-rw-r--r--app/services/organizations/base_service.rb14
-rw-r--r--app/services/organizations/create_service.rb27
-rw-r--r--app/services/packages/ml_model/create_package_file_service.rb3
-rw-r--r--app/services/packages/npm/create_package_service.rb8
-rw-r--r--app/services/packages/nuget/check_duplicates_service.rb30
-rw-r--r--app/services/packages/nuget/extract_metadata_file_service.rb12
-rw-r--r--app/services/packages/nuget/metadata_extraction_service.rb15
-rw-r--r--app/services/packages/nuget/process_package_file_service.rb25
-rw-r--r--app/services/packages/nuget/symbols/create_symbol_files_service.rb20
-rw-r--r--app/services/packages/nuget/symbols/extract_signature_and_checksum_service.rb91
-rw-r--r--app/services/packages/nuget/symbols/extract_symbol_signature_service.rb63
-rw-r--r--app/services/packages/nuget/update_package_from_metadata_service.rb19
-rw-r--r--app/services/packages/protection/delete_rule_service.rb43
-rw-r--r--app/services/packages/pypi/create_package_service.rb8
-rw-r--r--app/services/packages/update_tags_service.rb3
-rw-r--r--app/services/pages/delete_service.rb2
-rw-r--r--app/services/personal_access_tokens/rotate_service.rb9
-rw-r--r--app/services/projects/container_repository/gitlab/delete_tags_service.rb2
-rw-r--r--app/services/projects/container_repository/third_party/delete_tags_service.rb2
-rw-r--r--app/services/projects/destroy_service.rb19
-rw-r--r--app/services/projects/fork_service.rb13
-rw-r--r--app/services/projects/group_links/destroy_service.rb33
-rw-r--r--app/services/projects/group_links/update_service.rb8
-rw-r--r--app/services/projects/update_pages_service.rb13
-rw-r--r--app/services/projects/update_repository_storage_service.rb4
-rw-r--r--app/services/projects/update_service.rb15
-rw-r--r--app/services/releases/base_service.rb4
-rw-r--r--app/services/releases/create_service.rb14
-rw-r--r--app/services/releases/destroy_service.rb2
-rw-r--r--app/services/releases/update_service.rb2
-rw-r--r--app/services/resource_access_tokens/create_service.rb10
-rw-r--r--app/services/resource_events/base_synthetic_notes_builder_service.rb5
-rw-r--r--app/services/resource_events/merge_into_notes_service.rb2
-rw-r--r--app/services/security/ci_configuration/sast_parser_service.rb8
-rw-r--r--app/services/service_desk/custom_email_verifications/update_service.rb6
-rw-r--r--app/services/service_desk/custom_emails/create_service.rb4
-rw-r--r--app/services/service_desk_settings/update_service.rb11
-rw-r--r--app/services/spam/spam_action_service.rb19
-rw-r--r--app/services/system_notes/issuables_service.rb2
-rw-r--r--app/services/users/refresh_authorized_projects_service.rb2
-rw-r--r--app/services/users/upsert_credit_card_validation_service.rb63
-rw-r--r--app/services/verify_pages_domain_service.rb2
-rw-r--r--app/services/vs_code/settings/delete_service.rb21
-rw-r--r--app/services/web_hook_service.rb12
-rw-r--r--app/validators/ip_cidr_array_validator.rb25
-rw-r--r--app/validators/ip_cidr_validator.rb45
-rw-r--r--app/validators/json_schemas/activity_pub_follow_payload.json53
-rw-r--r--app/validators/json_schemas/vulnerability_cvss_vectors.json4
-rw-r--r--app/views/admin/abuse_reports/show.html.haml1
-rw-r--r--app/views/admin/application_settings/_account_and_limit.html.haml3
-rw-r--r--app/views/admin/application_settings/_ci_cd.html.haml6
-rw-r--r--app/views/admin/application_settings/_diagramsnet.html.haml2
-rw-r--r--app/views/admin/application_settings/_email.html.haml2
-rw-r--r--app/views/admin/application_settings/_error_tracking.html.haml4
-rw-r--r--app/views/admin/application_settings/_floc.html.haml2
-rw-r--r--app/views/admin/application_settings/_gitlab_shell_operation_limits.html.haml4
-rw-r--r--app/views/admin/application_settings/_gitpod.html.haml2
-rw-r--r--app/views/admin/application_settings/_import_export_limits.html.haml3
-rw-r--r--app/views/admin/application_settings/_kroki.html.haml2
-rw-r--r--app/views/admin/application_settings/_localization.html.haml4
-rw-r--r--app/views/admin/application_settings/_outbound.html.haml2
-rw-r--r--app/views/admin/application_settings/_performance.html.haml6
-rw-r--r--app/views/admin/application_settings/_plantuml.html.haml2
-rw-r--r--app/views/admin/application_settings/_projects_api_limits.html.haml4
-rw-r--r--app/views/admin/application_settings/_repository_check.html.haml2
-rw-r--r--app/views/admin/application_settings/_repository_storage.html.haml6
-rw-r--r--app/views/admin/application_settings/_runner_registrars_form.html.haml2
-rw-r--r--app/views/admin/application_settings/_signin.html.haml4
-rw-r--r--app/views/admin/application_settings/_sourcegraph.html.haml2
-rw-r--r--app/views/admin/application_settings/_spam.html.haml4
-rw-r--r--app/views/admin/application_settings/_terms.html.haml2
-rw-r--r--app/views/admin/application_settings/_usage.html.haml24
-rw-r--r--app/views/admin/application_settings/general.html.haml6
-rw-r--r--app/views/admin/application_settings/metrics_and_profiling.html.haml8
-rw-r--r--app/views/admin/application_settings/network.html.haml26
-rw-r--r--app/views/admin/application_settings/preferences.html.haml10
-rw-r--r--app/views/admin/application_settings/reporting.html.haml2
-rw-r--r--app/views/admin/application_settings/repository.html.haml12
-rw-r--r--app/views/admin/application_settings/service_usage_data.html.haml29
-rw-r--r--app/views/admin/background_migrations/index.html.haml2
-rw-r--r--app/views/admin/dashboard/index.html.haml2
-rw-r--r--app/views/admin/dev_ops_report/_score.html.haml2
-rw-r--r--app/views/admin/spam_logs/_spam_log.html.haml16
-rw-r--r--app/views/admin/topics/_form.html.haml4
-rw-r--r--app/views/admin/topics/_topic.html.haml2
-rw-r--r--app/views/admin/topics/index.html.haml2
-rw-r--r--app/views/admin/users/_users.html.haml3
-rw-r--r--app/views/admin/users/projects.html.haml4
-rw-r--r--app/views/ci/status/_badge.html.haml13
-rw-r--r--app/views/ci/status/_icon.html.haml11
-rw-r--r--app/views/clusters/clusters/_advanced_settings.html.haml2
-rw-r--r--app/views/clusters/clusters/_deprecation_alert.html.haml2
-rw-r--r--app/views/clusters/clusters/_multiple_clusters_message.html.haml2
-rw-r--r--app/views/clusters/clusters/_namespace.html.haml2
-rw-r--r--app/views/clusters/clusters/_provider_details_form.html.haml32
-rw-r--r--app/views/clusters/clusters/cloud_providers/_cloud_provider_selector.html.haml6
-rw-r--r--app/views/clusters/clusters/connect.html.haml2
-rw-r--r--app/views/clusters/clusters/new_cluster_docs.html.haml2
-rw-r--r--app/views/clusters/clusters/show.html.haml8
-rw-r--r--app/views/clusters/clusters/user/_form.html.haml4
-rw-r--r--app/views/dashboard/_projects_head.html.haml5
-rw-r--r--app/views/dashboard/todos/_todo.html.haml2
-rw-r--r--app/views/dashboard/todos/index.html.haml16
-rw-r--r--app/views/devise/shared/_sign_in_link.html.haml2
-rw-r--r--app/views/devise/shared/_signup_box.html.haml75
-rw-r--r--app/views/devise/shared/_signup_box_form.html.haml73
-rw-r--r--app/views/devise/shared/_signup_omniauth_provider_list.haml5
-rw-r--r--app/views/discussions/_notes.html.haml2
-rw-r--r--app/views/events/_event.html.haml4
-rw-r--r--app/views/events/_event_scope.html.haml2
-rw-r--r--app/views/events/event/_common.html.haml4
-rw-r--r--app/views/events/event/_created_project.html.haml2
-rw-r--r--app/views/events/event/_design.html.haml2
-rw-r--r--app/views/events/event/_note.html.haml2
-rw-r--r--app/views/events/event/_private.html.haml6
-rw-r--r--app/views/events/event/_push.html.haml2
-rw-r--r--app/views/events/event/_wiki.html.haml2
-rw-r--r--app/views/explore/catalog/show.html.haml3
-rw-r--r--app/views/external_redirect/external_redirect/index.html.haml12
-rw-r--r--app/views/groups/_home_panel.html.haml2
-rw-r--r--app/views/groups/_import_group_from_another_instance_panel.html.haml6
-rw-r--r--app/views/groups/_import_group_from_file_panel.html.haml2
-rw-r--r--app/views/groups/_invite_members_modal.html.haml6
-rw-r--r--app/views/groups/projects.html.haml15
-rw-r--r--app/views/groups/settings/_export.html.haml4
-rw-r--r--app/views/groups/settings/_git_access_protocols.html.haml2
-rw-r--r--app/views/groups/settings/_permissions.html.haml3
-rw-r--r--app/views/groups/settings/_resource_access_token_creation.html.haml2
-rw-r--r--app/views/groups/settings/ci_cd/_auto_devops_form.html.haml2
-rw-r--r--app/views/import/bitbucket/status.html.haml4
-rw-r--r--app/views/import/bitbucket_server/new.html.haml5
-rw-r--r--app/views/import/bitbucket_server/status.html.haml4
-rw-r--r--app/views/import/bulk_imports/details.html.haml5
-rw-r--r--app/views/import/bulk_imports/history.html.haml2
-rw-r--r--app/views/import/fogbugz/new.html.haml4
-rw-r--r--app/views/import/fogbugz/status.html.haml4
-rw-r--r--app/views/import/gitea/new.html.haml10
-rw-r--r--app/views/import/gitea/status.html.haml5
-rw-r--r--app/views/import/github/new.html.haml13
-rw-r--r--app/views/import/github/status.html.haml4
-rw-r--r--app/views/import/gitlab_projects/new.html.haml6
-rw-r--r--app/views/import/shared/_new_project_form.html.haml4
-rw-r--r--app/views/invites/decline.html.haml2
-rw-r--r--app/views/layouts/_head.html.haml4
-rw-r--r--app/views/layouts/_page.html.haml4
-rw-r--r--app/views/layouts/application.html.haml4
-rw-r--r--app/views/layouts/devise.html.haml6
-rw-r--r--app/views/layouts/devise_empty.html.haml4
-rw-r--r--app/views/layouts/fullscreen.html.haml4
-rw-r--r--app/views/layouts/minimal.html.haml7
-rw-r--r--app/views/layouts/nav/_ask_duo_button.html.haml13
-rw-r--r--app/views/layouts/nav/_top_bar.html.haml2
-rw-r--r--app/views/layouts/signup_onboarding.html.haml4
-rw-r--r--app/views/layouts/terms.html.haml5
-rw-r--r--app/views/notify/github_gists_import_errors_email.html.haml2
-rw-r--r--app/views/notify/pages_domain_auto_ssl_failed_email.html.haml2
-rw-r--r--app/views/notify/pages_domain_auto_ssl_failed_email.text.haml2
-rw-r--r--app/views/notify/pages_domain_disabled_email.html.haml2
-rw-r--r--app/views/notify/pages_domain_disabled_email.text.haml2
-rw-r--r--app/views/notify/pages_domain_enabled_email.html.haml2
-rw-r--r--app/views/notify/pages_domain_enabled_email.text.haml2
-rw-r--r--app/views/notify/pages_domain_verification_failed_email.html.haml2
-rw-r--r--app/views/notify/pages_domain_verification_failed_email.text.haml2
-rw-r--r--app/views/notify/pages_domain_verification_succeeded_email.html.haml2
-rw-r--r--app/views/notify/pages_domain_verification_succeeded_email.text.haml2
-rw-r--r--app/views/organizations/organizations/users.html.haml4
-rw-r--r--app/views/organizations/settings/general.html.haml3
-rw-r--r--app/views/profiles/gpg_keys/index.html.haml2
-rw-r--r--app/views/profiles/keys/_key.html.haml2
-rw-r--r--app/views/profiles/keys/index.html.haml2
-rw-r--r--app/views/profiles/personal_access_tokens/index.html.haml2
-rw-r--r--app/views/profiles/preferences/show.html.haml9
-rw-r--r--app/views/profiles/show.html.haml6
-rw-r--r--app/views/profiles/two_factor_auths/show.html.haml4
-rw-r--r--app/views/projects/_errors.html.haml2
-rw-r--r--app/views/projects/_home_panel.html.haml8
-rw-r--r--app/views/projects/_import_project_pane.html.haml2
-rw-r--r--app/views/projects/_invite_members_empty_project.html.haml2
-rw-r--r--app/views/projects/_invite_members_modal.html.haml6
-rw-r--r--app/views/projects/_service_desk_settings.html.haml3
-rw-r--r--app/views/projects/artifacts/_tree_file.html.haml2
-rw-r--r--app/views/projects/blob/_editor.html.haml4
-rw-r--r--app/views/projects/blob/_pipeline_tour_success.html.haml2
-rw-r--r--app/views/projects/blob/show.html.haml3
-rw-r--r--app/views/projects/blob/viewers/_route_map.html.haml2
-rw-r--r--app/views/projects/blob/viewers/_route_map_loading.html.haml2
-rw-r--r--app/views/projects/branch_defaults/_branch_names_fields.html.haml2
-rw-r--r--app/views/projects/branch_defaults/_default_branch_fields.html.haml2
-rw-r--r--app/views/projects/branch_defaults/_show.html.haml2
-rw-r--r--app/views/projects/branch_rules/_show.html.haml2
-rw-r--r--app/views/projects/branches/_branch.html.haml6
-rw-r--r--app/views/projects/branches/_panel.html.haml2
-rw-r--r--app/views/projects/ci/builds/_build.html.haml10
-rw-r--r--app/views/projects/cleanup/_show.html.haml2
-rw-r--r--app/views/projects/commit/_commit_box.html.haml4
-rw-r--r--app/views/projects/commit/_signature_badge.html.haml6
-rw-r--r--app/views/projects/commits/_commit.html.haml4
-rw-r--r--app/views/projects/diffs/viewers/_collapsed.html.haml3
-rw-r--r--app/views/projects/edit.html.haml215
-rw-r--r--app/views/projects/environments/index.html.haml2
-rw-r--r--app/views/projects/feature_flags/new.html.haml2
-rw-r--r--app/views/projects/feature_flags_user_lists/edit.html.haml2
-rw-r--r--app/views/projects/feature_flags_user_lists/new.html.haml2
-rw-r--r--app/views/projects/find_file/show.html.haml2
-rw-r--r--app/views/projects/forks/index.html.haml2
-rw-r--r--app/views/projects/forks/new.html.haml1
-rw-r--r--app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml2
-rw-r--r--app/views/projects/issues/_new_branch.html.haml2
-rw-r--r--app/views/projects/issues/_related_branches.html.haml4
-rw-r--r--app/views/projects/issues/service_desk/_issue.html.haml2
-rw-r--r--app/views/projects/issues/service_desk/_issue_estimate.html.haml2
-rw-r--r--app/views/projects/jobs/_header.html.haml2
-rw-r--r--app/views/projects/merge_requests/_nav_btns.html.haml9
-rw-r--r--app/views/projects/merge_requests/_page.html.haml2
-rw-r--r--app/views/projects/mirrors/_authentication_method.html.haml2
-rw-r--r--app/views/projects/mirrors/_branch_filter.html.haml2
-rw-r--r--app/views/projects/mirrors/_mirror_repos.html.haml8
-rw-r--r--app/views/projects/mirrors/_mirror_repos_form.html.haml2
-rw-r--r--app/views/projects/mirrors/_mirror_repos_list.html.haml10
-rw-r--r--app/views/projects/mirrors/_mirror_repos_push.html.haml2
-rw-r--r--app/views/projects/mirrors/_ssh_host_keys.html.haml4
-rw-r--r--app/views/projects/ml/model_versions/show.html.haml6
-rw-r--r--app/views/projects/ml/models/index.html.haml2
-rw-r--r--app/views/projects/pages/_access.html.haml2
-rw-r--r--app/views/projects/pages/_waiting.html.haml2
-rw-r--r--app/views/projects/pages/new.html.haml7
-rw-r--r--app/views/projects/pages_domains/_certificate.html.haml2
-rw-r--r--app/views/projects/pages_domains/_dns.html.haml2
-rw-r--r--app/views/projects/pages_domains/_helper_text.html.haml2
-rw-r--r--app/views/projects/protected_tags/shared/_create_protected_tag.html.haml2
-rw-r--r--app/views/projects/protected_tags/shared/_dropdown.html.haml2
-rw-r--r--app/views/projects/protected_tags/shared/_index.html.haml2
-rw-r--r--app/views/projects/readme_templates/default.md.tt5
-rw-r--r--app/views/projects/runners/_group_runners.html.haml2
-rw-r--r--app/views/projects/runners/_runner.html.haml3
-rw-r--r--app/views/projects/settings/access_tokens/_form.html.haml2
-rw-r--r--app/views/projects/settings/ci_cd/_autodevops_form.html.haml8
-rw-r--r--app/views/projects/settings/merge_requests/_merge_request_merge_commit_template.html.haml2
-rw-r--r--app/views/projects/settings/merge_requests/_merge_request_merge_method_settings.html.haml6
-rw-r--r--app/views/projects/settings/merge_requests/_merge_request_merge_suggestions_settings.html.haml2
-rw-r--r--app/views/projects/settings/merge_requests/_merge_request_pipelines_and_threads_options.html.haml3
-rw-r--r--app/views/projects/settings/merge_requests/_merge_request_squash_commit_template.html.haml2
-rw-r--r--app/views/projects/settings/merge_requests/_merge_request_squash_options_settings.html.haml2
-rw-r--r--app/views/projects/settings/merge_requests/show.html.haml2
-rw-r--r--app/views/projects/settings/operations/_alert_management.html.haml2
-rw-r--r--app/views/projects/tags/_tag.html.haml2
-rw-r--r--app/views/projects/tree/_tree_header.html.haml3
-rw-r--r--app/views/projects/tree/show.html.haml3
-rw-r--r--app/views/projects/usage_quotas/index.html.haml2
-rw-r--r--app/views/protected_branches/shared/_create_protected_branch.html.haml15
-rw-r--r--app/views/protected_branches/shared/_index.html.haml2
-rw-r--r--app/views/protected_branches/shared/_protected_branch.html.haml12
-rw-r--r--app/views/pwa/manifest.json.erb2
-rw-r--r--app/views/search/show.html.haml5
-rw-r--r--app/views/shared/_auto_devops_callout.html.haml2
-rw-r--r--app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml2
-rw-r--r--app/views/shared/_ci_catalog_badge.html.haml1
-rw-r--r--app/views/shared/_commit_message_container.html.haml2
-rw-r--r--app/views/shared/_custom_attributes.html.haml2
-rw-r--r--app/views/shared/_md_preview.html.haml2
-rw-r--r--app/views/shared/_new_nav_announcement.html.haml33
-rw-r--r--app/views/shared/_new_nav_for_everyone_announcement.html.haml18
-rw-r--r--app/views/shared/_project_limit.html.haml2
-rw-r--r--app/views/shared/_registration_features_discovery_message.html.haml2
-rw-r--r--app/views/shared/_remote_mirror_update_button.html.haml2
-rw-r--r--app/views/shared/_service_ping_consent.html.haml6
-rw-r--r--app/views/shared/access_tokens/_form.html.haml2
-rw-r--r--app/views/shared/deploy_tokens/_form.html.haml18
-rw-r--r--app/views/shared/deploy_tokens/_index.html.haml2
-rw-r--r--app/views/shared/deploy_tokens/_new_deploy_token.html.haml8
-rw-r--r--app/views/shared/deploy_tokens/_table.html.haml2
-rw-r--r--app/views/shared/empty_states/_snippets.html.haml2
-rw-r--r--app/views/shared/integrations/gitlab_slack_application/_help.html.haml2
-rw-r--r--app/views/shared/integrations/gitlab_slack_application/_slack_integration_form.html.haml2
-rw-r--r--app/views/shared/integrations/mattermost_slash_commands/_help.html.haml2
-rw-r--r--app/views/shared/integrations/slack_slash_commands/_help.html.haml12
-rw-r--r--app/views/shared/issuable/_assignees.html.haml2
-rw-r--r--app/views/shared/issuable/_nav.html.haml2
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml5
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml2
-rw-r--r--app/views/shared/issuable/form/_type_selector.html.haml2
-rw-r--r--app/views/shared/milestones/_sidebar.html.haml5
-rw-r--r--app/views/shared/notes/_notes_with_form.html.haml4
-rw-r--r--app/views/shared/projects/_list.html.haml1
-rw-r--r--app/views/shared/projects/_project.html.haml5
-rw-r--r--app/views/shared/runners/_shared_runners_description.html.haml2
-rw-r--r--app/views/shared/web_hooks/_form.html.haml2
-rw-r--r--app/views/shared/wikis/_wiki_directory.html.haml4
-rw-r--r--app/views/shared/wikis/show.html.haml3
-rw-r--r--app/views/users/_cover_controls.html.haml2
-rw-r--r--app/views/users/_overview.html.haml4
-rw-r--r--app/views/users/_profile_basic_info.html.haml4
-rw-r--r--app/views/users/show.html.haml60
-rw-r--r--app/workers/abuse/spam_abuse_events_worker.rb60
-rw-r--r--app/workers/activity_pub/projects/releases_subscription_worker.rb39
-rw-r--r--app/workers/all_queues.yml96
-rw-r--r--app/workers/bulk_import_worker.rb17
-rw-r--r--app/workers/bulk_imports/entity_worker.rb25
-rw-r--r--app/workers/bulk_imports/export_request_worker.rb8
-rw-r--r--app/workers/bulk_imports/finish_batched_pipeline_worker.rb24
-rw-r--r--app/workers/bulk_imports/pipeline_batch_worker.rb84
-rw-r--r--app/workers/bulk_imports/pipeline_worker.rb67
-rw-r--r--app/workers/bulk_imports/relation_batch_export_worker.rb19
-rw-r--r--app/workers/bulk_imports/relation_export_worker.rb28
-rw-r--r--app/workers/bulk_imports/stuck_import_worker.rb17
-rw-r--r--app/workers/ci/cancel_pipeline_worker.rb2
-rw-r--r--app/workers/ci/initial_pipeline_process_worker.rb14
-rw-r--r--app/workers/ci/refs/unlock_previous_pipelines_worker.rb4
-rw-r--r--app/workers/concerns/gitlab/github_import/rescheduling_methods.rb3
-rw-r--r--app/workers/concerns/gitlab/github_import/stage_methods.rb27
-rw-r--r--app/workers/concerns/worker_attributes.rb4
-rw-r--r--app/workers/environments/auto_recover_worker.rb22
-rw-r--r--app/workers/environments/auto_stop_cron_worker.rb1
-rw-r--r--app/workers/gitlab/github_import/stage/import_attachments_worker.rb6
-rw-r--r--app/workers/gitlab/github_import/stage/import_base_data_worker.rb2
-rw-r--r--app/workers/gitlab/github_import/stage/import_collaborators_worker.rb3
-rw-r--r--app/workers/gitlab/github_import/stage/import_issue_events_worker.rb4
-rw-r--r--app/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker.rb4
-rw-r--r--app/workers/gitlab/github_import/stage/import_lfs_objects_worker.rb7
-rw-r--r--app/workers/gitlab/github_import/stage/import_notes_worker.rb4
-rw-r--r--app/workers/gitlab/github_import/stage/import_protected_branches_worker.rb4
-rw-r--r--app/workers/gitlab/github_import/stage/import_pull_requests_merged_by_worker.rb6
-rw-r--r--app/workers/gitlab/github_import/stage/import_pull_requests_review_requests_worker.rb6
-rw-r--r--app/workers/gitlab/github_import/stage/import_pull_requests_reviews_worker.rb6
-rw-r--r--app/workers/gitlab/github_import/stage/import_pull_requests_worker.rb6
-rw-r--r--app/workers/gitlab/import/advance_stage.rb6
-rw-r--r--app/workers/gitlab/jira_import/stage/import_issues_worker.rb9
-rw-r--r--app/workers/hashed_storage/base_worker.rb24
-rw-r--r--app/workers/hashed_storage/migrator_worker.rb18
-rw-r--r--app/workers/hashed_storage/project_migrate_worker.rb18
-rw-r--r--app/workers/hashed_storage/project_rollback_worker.rb18
-rw-r--r--app/workers/hashed_storage/rollbacker_worker.rb18
-rw-r--r--app/workers/merge_request_cleanup_refs_worker.rb2
-rw-r--r--app/workers/merge_requests/set_reviewer_reviewed_worker.rb21
-rw-r--r--app/workers/packages/cleanup_package_registry_worker.rb5
-rw-r--r--app/workers/packages/npm/cleanup_stale_metadata_cache_worker.rb42
-rw-r--r--app/workers/packages/nuget/extraction_worker.rb2
-rw-r--r--app/workers/projects/import_export/after_import_merge_requests_worker.rb21
-rw-r--r--app/workers/remove_expired_group_links_worker.rb2
-rw-r--r--app/workers/repository_fork_worker.rb22
-rw-r--r--app/workers/schedule_merge_request_cleanup_refs_worker.rb1
-rw-r--r--app/workers/tasks_to_be_done/create_worker.rb18
-rwxr-xr-xbin/gitlab-backup-cli14
-rw-r--r--config/application.rb2
-rw-r--r--config/feature_categories.yml4
-rw-r--r--config/feature_flags/development/abuse_report_notes.yml8
-rw-r--r--config/feature_flags/development/access_token_pagination.yml2
-rw-r--r--config/feature_flags/development/activity_filter_has_mr.yml2
-rw-r--r--config/feature_flags/development/activity_filter_has_remediations.yml8
-rw-r--r--config/feature_flags/development/admin_group_member.yml8
-rw-r--r--config/feature_flags/development/ai_assist_api.yml8
-rw-r--r--config/feature_flags/development/ai_self_discover.yml8
-rw-r--r--config/feature_flags/development/ambiguous_ref_modal.yml8
-rw-r--r--config/feature_flags/development/auto_devops_banner_disabled.yml2
-rw-r--r--config/feature_flags/development/blob_blame_info.yml8
-rw-r--r--config/feature_flags/development/build_service_proxy.yml4
-rw-r--r--config/feature_flags/development/bulk_import_deferred_workers.yml8
-rw-r--r--config/feature_flags/development/bulk_import_details_page.yml8
-rw-r--r--config/feature_flags/development/bulk_import_idempotent_workers.yml8
-rw-r--r--config/feature_flags/development/by_pass_two_factor_for_current_session.yml2
-rw-r--r--config/feature_flags/development/ci_catalog_create_metadata.yml8
-rw-r--r--config/feature_flags/development/ci_fix_performance_pipelines_json_endpoint.yml8
-rw-r--r--config/feature_flags/development/ci_job_artifacts_backlog_large_loop_limit.yml2
-rw-r--r--config/feature_flags/development/ci_require_credit_card_on_free_plan.yml2
-rw-r--r--config/feature_flags/development/ci_require_credit_card_on_trial_plan.yml2
-rw-r--r--config/feature_flags/development/ci_stop_unlock_pipelines.yml8
-rw-r--r--config/feature_flags/development/ci_unlock_non_successful_pipelines.yml8
-rw-r--r--config/feature_flags/development/ci_variable_drawer.yml8
-rw-r--r--config/feature_flags/development/code_suggestions_for_instance_admin_enabled.yml2
-rw-r--r--config/feature_flags/development/code_tasks.yml8
-rw-r--r--config/feature_flags/development/compare_project_authorization_linear_cte.yml2
-rw-r--r--config/feature_flags/development/composer_use_ssh_source_urls.yml8
-rw-r--r--config/feature_flags/development/container_registry_protected_containers.yml8
-rw-r--r--config/feature_flags/development/coop_header.yml8
-rw-r--r--config/feature_flags/development/create_deployment_only_for_processable_jobs.yml8
-rw-r--r--config/feature_flags/development/create_embeddings_with_vertex_ai.yml8
-rw-r--r--config/feature_flags/development/create_project_subscription_graphql_endpoint.yml8
-rw-r--r--config/feature_flags/development/custom_roles_in_members_page.yml2
-rw-r--r--config/feature_flags/development/custom_roles_ui_saas.yml2
-rw-r--r--config/feature_flags/development/data_transfer_monitoring_mock_data.yml8
-rw-r--r--config/feature_flags/development/disable_unsafe_regexp.yml2
-rw-r--r--config/feature_flags/development/display_cost_factored_storage_size_on_project_pages.yml8
-rw-r--r--config/feature_flags/development/do_not_run_safety_net_auth_refresh_jobs.yml2
-rw-r--r--config/feature_flags/development/explain_code_vertex_ai.yml8
-rw-r--r--config/feature_flags/development/forti_authenticator.yml2
-rw-r--r--config/feature_flags/development/forti_token_cloud.yml2
-rw-r--r--config/feature_flags/development/frecent_namespaces_suggestions.yml8
-rw-r--r--config/feature_flags/development/github_importer_raise_max_interruptions.yml8
-rw-r--r--config/feature_flags/development/global_ci_catalog.yml8
-rw-r--r--config/feature_flags/development/global_dependency_scanning_on_advisory_ingestion.yml8
-rw-r--r--config/feature_flags/development/group_multi_select_tokens.yml8
-rw-r--r--config/feature_flags/development/import_fallback_to_db_empty_cache.yml8
-rw-r--r--config/feature_flags/development/increase_jira_import_issues_timeout.yml8
-rw-r--r--config/feature_flags/development/inherit_higher_access_levels_no_cross_join.yml2
-rw-r--r--config/feature_flags/development/invert_omniauth_args_merging.yml8
-rw-r--r--config/feature_flags/development/issue_assignees_widget.yml8
-rw-r--r--config/feature_flags/development/jira_dvcs_end_of_life_amnesty.yml8
-rw-r--r--config/feature_flags/development/jwt_auth_space_delimited_scopes.yml8
-rw-r--r--config/feature_flags/development/k8s_watch_api.yml8
-rw-r--r--config/feature_flags/development/linear_project_authorization.yml2
-rw-r--r--config/feature_flags/development/log_git_streaming_audit_events.yml8
-rw-r--r--config/feature_flags/development/manage_project_access_tokens.yml4
-rw-r--r--config/feature_flags/development/mastodon_social_ui.yml8
-rw-r--r--config/feature_flags/development/member_expiring_email_notification.yml2
-rw-r--r--config/feature_flags/development/merge_request_refs_cleanup.yml8
-rw-r--r--config/feature_flags/development/mr_request_changes.yml8
-rw-r--r--config/feature_flags/development/new_pipeline_graph.yml8
-rw-r--r--config/feature_flags/development/nuget_duplicates_option.yml8
-rw-r--r--config/feature_flags/development/observability_metrics.yml8
-rw-r--r--config/feature_flags/development/oidc_issuer_url.yml8
-rw-r--r--config/feature_flags/development/only_highlight_discussions_requested.yml8
-rw-r--r--config/feature_flags/development/openai_experimentation.yml8
-rw-r--r--config/feature_flags/development/order_builds_for_group_runner.yml2
-rw-r--r--config/feature_flags/development/personal_snippet_reference_filters.yml2
-rw-r--r--config/feature_flags/development/preserve_unchanged_markdown.yml2
-rw-r--r--config/feature_flags/development/print_wiki.yml8
-rw-r--r--config/feature_flags/development/product_analytics_usage_quota.yml8
-rw-r--r--config/feature_flags/development/project_overwrite_service_tracking.yml2
-rw-r--r--config/feature_flags/development/project_tool_filter_with_scanner_name.yml8
-rw-r--r--config/feature_flags/development/rate_limit_oauth_api.yml2
-rw-r--r--config/feature_flags/development/reduce_duplicate_job_key_ttl.yml8
-rw-r--r--config/feature_flags/development/reduced_build_attributes_list_for_rules.yml8
-rw-r--r--config/feature_flags/development/reject_unsigned_commits_by_gitlab.yml2
-rw-r--r--config/feature_flags/development/remove_mr_blocking_constraints.yml8
-rw-r--r--config/feature_flags/development/replicate_object_pool_on_move.yml2
-rw-r--r--config/feature_flags/development/restrict_ci_job_token_for_public_and_internal_projects.yml8
-rw-r--r--config/feature_flags/development/restrict_pipeline_cancellation_by_role.yml8
-rw-r--r--config/feature_flags/development/restyle_login_page.yml2
-rw-r--r--config/feature_flags/development/rugged_commit_is_ancestor.yml8
-rw-r--r--config/feature_flags/development/rugged_commit_tree_entry.yml8
-rw-r--r--config/feature_flags/development/rugged_find_commit.yml8
-rw-r--r--config/feature_flags/development/rugged_list_commits_by_oid.yml8
-rw-r--r--config/feature_flags/development/rugged_tree_entries.yml8
-rw-r--r--config/feature_flags/development/rugged_tree_entry.yml8
-rw-r--r--config/feature_flags/development/runners_dashboard.yml8
-rw-r--r--config/feature_flags/development/saved_replies.yml8
-rw-r--r--config/feature_flags/development/search_issues_hide_archived_projects.yml8
-rw-r--r--config/feature_flags/development/search_merge_requests_hide_archived_projects.yml8
-rw-r--r--config/feature_flags/development/search_notes_hide_archived_projects.yml9
-rw-r--r--config/feature_flags/development/service_accounts_crud.yml2
-rw-r--r--config/feature_flags/development/service_desk_new_note_email_native_attachments.yml8
-rw-r--r--config/feature_flags/development/set_feature_flag_service.yml2
-rw-r--r--config/feature_flags/development/source_editor_toolbar.yml2
-rw-r--r--config/feature_flags/development/sourcegraph.yml2
-rw-r--r--config/feature_flags/development/specialized_worker_for_group_lock_update_auth_recalculation.yml2
-rw-r--r--config/feature_flags/development/summarize_notes_with_anthropic.yml8
-rw-r--r--config/feature_flags/development/super_sidebar_logged_out.yml8
-rw-r--r--config/feature_flags/development/super_sidebar_nav_enrolled.yml8
-rw-r--r--config/feature_flags/development/support_group_level_merge_checks_setting.yml2
-rw-r--r--config/feature_flags/development/two_factor_for_cli.yml2
-rw-r--r--config/feature_flags/development/unbatch_graphql_queries.yml8
-rw-r--r--config/feature_flags/development/use_embeddings_with_vertex.yml8
-rw-r--r--config/feature_flags/development/use_gitlab_http_v2.yml2
-rw-r--r--config/feature_flags/development/use_new_rule_finalize_approach.yml8
-rw-r--r--config/feature_flags/development/use_pipeline_wizard_for_pages.yml8
-rw-r--r--config/feature_flags/development/use_primary_and_secondary_stores_for_action_cable.yml8
-rw-r--r--config/feature_flags/development/use_primary_and_secondary_stores_for_shared_state.yml8
-rw-r--r--config/feature_flags/development/use_primary_store_as_default_for_action_cable.yml8
-rw-r--r--config/feature_flags/development/use_primary_store_as_default_for_shared_state.yml8
-rw-r--r--config/feature_flags/development/use_repository_list_tags_on_graphql.yml8
-rw-r--r--config/feature_flags/development/use_sql_functions_for_primary_key_lookups.yml8
-rw-r--r--config/feature_flags/development/user_profile_overflow_menu_vue.yml8
-rw-r--r--config/feature_flags/development/value_stream_dashboard_on_off_setting.yml8
-rw-r--r--config/feature_flags/development/verify_push_rules_for_first_commit.yml8
-rw-r--r--config/feature_flags/development/vscode_web_ide.yml2
-rw-r--r--config/feature_flags/development/vulnerability_report_grouping.yml8
-rw-r--r--config/feature_flags/development/webauthn_without_totp.yml2
-rw-r--r--config/feature_flags/development/widget_pipeline_pass_subscription_update.yml8
-rw-r--r--config/feature_flags/development/wiki_front_matter.yml4
-rw-r--r--config/feature_flags/development/wiki_front_matter_title.yml8
-rw-r--r--config/feature_flags/experiment/disable_network_graph_notes_count.yml8
-rw-r--r--config/feature_flags/experiment/ios_specific_templates.yml8
-rw-r--r--config/feature_flags/ops/automatic_lock_writes_on_partition_tables.yml8
-rw-r--r--config/feature_flags/ops/block_password_auth_for_saml_users.yml2
-rw-r--r--config/feature_flags/ops/code_suggestions_tokens_api.yml2
-rw-r--r--config/feature_flags/ops/enforce_ci_builds_pagination_limit.yml8
-rw-r--r--config/feature_flags/ops/enforce_memory_watchdog.yml2
-rw-r--r--config/feature_flags/ops/report_heap_dumps.yml2
-rw-r--r--config/feature_flags/ops/report_jemalloc_stats.yml2
-rw-r--r--config/feature_flags/ops/suggested_reviewers_internal_api.yml2
-rw-r--r--config/initializers/1_settings.rb10
-rw-r--r--config/initializers/7_redis.rb4
-rw-r--r--config/initializers/action_cable.rb13
-rw-r--r--config/initializers/active_record_renamed_table.rb6
-rw-r--r--config/initializers/database_query_analyzers.rb5
-rw-r--r--config/initializers/elastic_client_setup.rb3
-rw-r--r--config/initializers/peek.rb1
-rw-r--r--config/initializers/postgresql_cte.rb18
-rw-r--r--config/initializers/sidekiq.rb4
-rw-r--r--config/initializers/sprockets_patch.rb63
-rw-r--r--config/initializers/wikicloth_redos_patch.rb2
-rw-r--r--config/mail_room.yml1
-rw-r--r--config/metrics/counts_28d/20231102160653_i_quickactions_request_changes_monthly.yml23
-rw-r--r--config/metrics/counts_7d/20231102160653_i_quickactions_request_changes_weekly.yml23
-rw-r--r--config/metrics/counts_all/20210216180232_projects_jira_dvcs_cloud_active.yml4
-rw-r--r--config/metrics/schema.json237
-rw-r--r--config/metrics/schema/base.json170
-rw-r--r--config/metrics/schema/internal_events.json106
-rw-r--r--config/metrics/schema/redis.json95
-rw-r--r--config/metrics/schema/redis_hll.json103
-rw-r--r--config/metrics/schema/status.json33
-rw-r--r--config/metrics/settings/20210204124920_web_ide_clientside_preview_enabled.yml4
-rw-r--r--config/redis.yml.example12
-rw-r--r--config/routes.rb13
-rw-r--r--config/routes/admin.rb3
-rw-r--r--config/routes/explore.rb1
-rw-r--r--config/routes/import.rb1
-rw-r--r--config/routes/organizations.rb1
-rw-r--r--config/routes/project.rb5
-rw-r--r--config/settings.rb3
-rw-r--r--config/sidekiq_queues.yml20
-rw-r--r--config/webpack.config.js14
-rw-r--r--danger/analytics_instrumentation/Dangerfile2
-rw-r--r--danger/change_column_default/Dangerfile3
-rw-r--r--danger/ci_tables/Dangerfile2
-rw-r--r--danger/database/Dangerfile2
-rw-r--r--danger/documentation/Dangerfile7
-rw-r--r--danger/experiments/Dangerfile2
-rw-r--r--danger/feature_flag/Dangerfile1
-rw-r--r--danger/gitaly/Dangerfile29
-rw-r--r--danger/gitlab_schema_validation/Dangerfile3
-rw-r--r--danger/pajamas/Dangerfile1
-rw-r--r--danger/plugins/change_column_default.rb9
-rw-r--r--danger/plugins/gitlab_schema_validation.rb9
-rw-r--r--danger/plugins/todos.rb11
-rw-r--r--danger/rubocop/Dangerfile5
-rw-r--r--danger/saas_feature/Dangerfile47
-rw-r--r--danger/todos/Dangerfile3
-rw-r--r--data/deprecations/ 16_3_runner-terminationgracepriodseconds.yml22
-rw-r--r--data/deprecations/15-8-kas-private-tls.yml (renamed from data/deprecations/15.8-kas-private-tls.yml)0
-rw-r--r--data/deprecations/15-9-database-single-database-connection-conf.yml2
-rw-r--r--data/deprecations/16-0-deprecate-omnibus-grafana.yml2
-rw-r--r--data/deprecations/16-0-eol-windows-server-2004-and-20H2.yml (renamed from data/deprecations/16.0-eol-windows-server-2004-and-20H2.yml)0
-rw-r--r--data/deprecations/16-1-non-decomposed-mode-deprecation.yml6
-rw-r--r--data/deprecations/16-1-unified-approval-rules.yml2
-rw-r--r--data/deprecations/16-1-windows-cmd-runner-shell-executor.yml8
-rw-r--r--data/deprecations/16-2-custom_sign_in_fields.yml11
-rw-r--r--data/deprecations/16-3-geo-housekeeping-rake-tasks.yml22
-rw-r--r--data/deprecations/16-3-runner-terminationgracepriodseconds.yml22
-rw-r--r--data/deprecations/16-4-ci_job_token_scope_enabled-attribute-deprecation.yml2
-rw-r--r--data/deprecations/16-5-container-registry-support-storage-drivers-swift-oss.yml13
-rw-r--r--data/deprecations/16-6-deprecation-legacy-geo-prometheus-metrics.yml22
-rw-r--r--data/deprecations/16-6-file-type-variable-extension-deprecation.yml13
-rw-r--r--data/deprecations/16-6-lfs-integrity-check-feature-flag-deprecation.yml13
-rw-r--r--data/deprecations/16-6-maven-group-permissions.yml15
-rw-r--r--data/deprecations/16-6-package-deprecate-two-graphql-fields.yml13
-rw-r--r--data/deprecations/16-6-proxy-based-dast-deprecation.yml9
-rw-r--r--data/deprecations/16_2-custom_sign_in_fields.yml11
-rw-r--r--data/deprecations/17-0-github-rake-task.yml14
-rw-r--r--data/whats_new/202310220001_16_5.yml60
-rw-r--r--db/click_house/main/20230705124511_create_events.sql16
-rw-r--r--db/click_house/main/20230707151359_create_ci_finished_builds.sql33
-rw-r--r--db/click_house/main/20230719101806_create_ci_finished_builds_aggregated_queueing_delay_percentiles.sql11
-rw-r--r--db/click_house/main/20230724064832_create_contribution_analytics_events.sql13
-rw-r--r--db/click_house/main/20230724064918_contribution_analytics_events_materialized_view.sql16
-rw-r--r--db/click_house/main/20230808070520_create_events_cursor.sql9
-rw-r--r--db/click_house/main/20230808140217_create_ci_finished_builds_aggregated_queueing_delay_percentiles_mv.sql12
-rw-r--r--db/click_house/migrate/20230705124511_create_events.rb30
-rw-r--r--db/click_house/migrate/20230707151359_create_ci_finished_builds.rb47
-rw-r--r--db/click_house/migrate/20230719101806_create_ci_finished_builds_aggregated_queueing_delay_percentiles.rb25
-rw-r--r--db/click_house/migrate/20230724064832_create_contribution_analytics_events.rb27
-rw-r--r--db/click_house/migrate/20230724064918_create_contribution_analytics_events_materialized_view.rb30
-rw-r--r--db/click_house/migrate/20230808070520_create_sync_cursors.rb23
-rw-r--r--db/click_house/migrate/20230808140217_create_ci_finished_builds_aggregated_queueing_delay_percentiles_mv.rb26
-rw-r--r--db/click_house/migrate/20231106202300_modify_ci_finished_builds_settings.rb15
-rw-r--r--db/docs/activity_pub_releases_subscriptions.yml11
-rw-r--r--db/docs/approval_group_rules.yml10
-rw-r--r--db/docs/approval_group_rules_groups.yml9
-rw-r--r--db/docs/approval_group_rules_protected_branches.yml9
-rw-r--r--db/docs/approval_group_rules_users.yml9
-rw-r--r--db/docs/approval_project_rules.yml2
-rw-r--r--db/docs/audit_events_external_audit_event_destinations.yml2
-rw-r--r--db/docs/audit_events_google_cloud_logging_configurations.yml2
-rw-r--r--db/docs/audit_events_streaming_http_group_namespace_filters.yml10
-rw-r--r--db/docs/batched_background_migrations/backfill_packages_tags_project_id.yml9
-rw-r--r--db/docs/batched_background_migrations/delete_invalid_protected_branch_merge_access_levels.yml7
-rw-r--r--db/docs/batched_background_migrations/delete_invalid_protected_branch_push_access_levels.yml7
-rw-r--r--db/docs/batched_background_migrations/delete_invalid_protected_tag_create_access_levels.yml7
-rw-r--r--db/docs/compliance_framework_security_policies.yml10
-rw-r--r--db/docs/container_expiration_policies.yml2
-rw-r--r--db/docs/events.yml2
-rw-r--r--db/docs/fork_network_members.yml2
-rw-r--r--db/docs/fork_networks.yml2
-rw-r--r--db/docs/group_merge_request_approval_settings.yml2
-rw-r--r--db/docs/incident_management_timeline_event_tags.yml2
-rw-r--r--db/docs/internal_ids.yml2
-rw-r--r--db/docs/ip_restrictions.yml2
-rw-r--r--db/docs/labels.yml2
-rw-r--r--db/docs/lfs_file_locks.yml2
-rw-r--r--db/docs/ml_model_metadata.yml10
-rw-r--r--db/docs/namespace_aggregation_schedules.yml2
-rw-r--r--db/docs/namespace_commit_emails.yml2
-rw-r--r--db/docs/namespaces.yml7
-rw-r--r--db/docs/namespaces_sync_events.yml2
-rw-r--r--db/docs/onboarding_progresses.yml2
-rw-r--r--db/docs/p_ci_job_annotations.yml1
-rw-r--r--db/docs/path_locks.yml2
-rw-r--r--db/docs/project_ci_cd_settings.yml2
-rw-r--r--db/docs/project_compliance_standards_adherence.yml2
-rw-r--r--db/docs/project_group_links.yml2
-rw-r--r--db/docs/project_import_data.yml2
-rw-r--r--db/docs/project_pages_metadata.yml2
-rw-r--r--db/docs/project_repositories.yml2
-rw-r--r--db/docs/project_security_settings.yml2
-rw-r--r--db/docs/project_settings.yml2
-rw-r--r--db/docs/project_statistics.yml2
-rw-r--r--db/docs/project_wiki_repositories.yml2
-rw-r--r--db/docs/projects_sync_events.yml2
-rw-r--r--db/docs/protected_branch_merge_access_levels.yml2
-rw-r--r--db/docs/protected_branch_push_access_levels.yml2
-rw-r--r--db/docs/protected_branches.yml2
-rw-r--r--db/docs/push_rules.yml2
-rw-r--r--db/docs/remote_mirrors.yml2
-rw-r--r--db/docs/repository_languages.yml2
-rw-r--r--db/docs/security_orchestration_policy_configurations.yml2
-rw-r--r--db/docs/service_access_tokens.yml2
-rw-r--r--db/docs/topics.yml2
-rw-r--r--db/docs/web_hook_logs.yml2
-rw-r--r--db/docs/zoekt_nodes.yml10
-rw-r--r--db/docs/zoekt_shards.yml5
-rw-r--r--db/migrate/20230529182720_recreate_billable_index.rb2
-rw-r--r--db/migrate/20230529184716_recreated_activity_index.rb2
-rw-r--r--db/migrate/20230605043258_add_unconfirmed_created_at_index_to_users.rb2
-rw-r--r--db/migrate/20230926092914_add_approval_group_rules.rb34
-rw-r--r--db/migrate/20230926092944_add_approval_group_rules_groups.rb18
-rw-r--r--db/migrate/20230926093004_add_approval_group_rules_users.rb18
-rw-r--r--db/migrate/20230926093025_add_approval_group_rules_protected_branches.rb21
-rw-r--r--db/migrate/20230926093101_add_fk_to_approval_rule_on_approval_group_rules_users.rb18
-rw-r--r--db/migrate/20230926093144_add_fk_to_user_on_approval_group_rules_users.rb15
-rw-r--r--db/migrate/20230926093211_add_fk_to_approval_rule_on_approval_group_rules_groups.rb16
-rw-r--r--db/migrate/20230926093251_add_fk_to_group_on_approval_group_rules_groups.rb15
-rw-r--r--db/migrate/20230926105440_add_fk_to_approval_rule_on_approval_group_rules_protected_branches.rb18
-rw-r--r--db/migrate/20230926105931_add_fk_to_protected_branch_on_approval_group_rules_protected_branches.rb16
-rw-r--r--db/migrate/20230927124202_add_mastodon_to_user_details.rb21
-rw-r--r--db/migrate/20230928145555_add_fk_to_security_orchestration_policy_configuration_on_approval_group_rules.rb17
-rw-r--r--db/migrate/20230928145637_add_fk_to_scan_result_policy_on_approval_group_rules.rb16
-rw-r--r--db/migrate/20230929155123_migrate_disable_merge_trains_value.rb55
-rw-r--r--db/migrate/20231002162941_add_enable_artifact_external_redirect_warning_page_to_application_settings.rb10
-rw-r--r--db/migrate/20231005151816_add_created_at_to_status_check_responses.rb7
-rw-r--r--db/migrate/20231009115713_remove_duplicate_index_rule_type_four.rb16
-rw-r--r--db/migrate/20231013204933_remove_tasks_to_be_done_worker.rb15
-rw-r--r--db/migrate/20231017095738_create_activity_pub_releases_subscriptions.rb25
-rw-r--r--db/migrate/20231017114131_add_auto_canceled_by_partition_id_to_p_ci_builds.rb14
-rw-r--r--db/migrate/20231017134349_create_ml_model_metadata.rb19
-rw-r--r--db/migrate/20231017135207_add_fields_to_ml_model.rb23
-rw-r--r--db/migrate/20231017154804_add_index_to_status_check_responses_on_id_and_status.rb14
-rw-r--r--db/migrate/20231017181403_add_generated_to_diff_files.rb9
-rw-r--r--db/migrate/20231018140154_remove_hashed_storage_migration_workers_job_instances.rb21
-rw-r--r--db/migrate/20231018152419_add_text_limit_to_ml_models.rb13
-rw-r--r--db/migrate/20231019104211_add_file_sha256_to_packages_nuget_symbols.rb13
-rw-r--r--db/migrate/20231019122855_add_semver_index_ci_runner_machines.rb24
-rw-r--r--db/migrate/20231019145202_add_status_to_packages_npm_metadata_caches.rb7
-rw-r--r--db/migrate/20231019180421_add_name_description_to_catalog_resources.rb28
-rw-r--r--db/migrate/20231020020732_add_user_phone_number_validation_telesign_reference_xid_index.rb15
-rw-r--r--db/migrate/20231020074227_add_auto_canceled_by_partition_id_to_p_ci_builds_self_managed.rb13
-rw-r--r--db/migrate/20231020095624_create_audit_events_streaming_http_group_namespace_filters.rb22
-rw-r--r--db/migrate/20231020112541_add_column_model_version_id_to_ml_candidates.rb7
-rw-r--r--db/migrate/20231020181652_add_index_packages_npm_metadata_caches_on_id_and_project_id_and_status.rb18
-rw-r--r--db/migrate/20231023073841_add_indexes_to_project_compliance_standards_adherence.rb21
-rw-r--r--db/migrate/20231023114006_add_index_on_model_version_id_to_ml_candidates.rb15
-rw-r--r--db/migrate/20231023114551_add_fk_on_ml_candidates_to_ml_model_versions.rb15
-rw-r--r--db/migrate/20231023121955_add_description_to_ml_model_versions.rb9
-rw-r--r--db/migrate/20231023122508_add_text_limit_to_descriptions_on_ml_model_versions.rb13
-rw-r--r--db/migrate/20231024123444_add_archive_project_to_member_roles.rb9
-rw-r--r--db/migrate/20231024133234_add_source_package_name_to_sbom_component.rb28
-rw-r--r--db/migrate/20231024142236_add_fields_to_bulk_import_failures.rb12
-rw-r--r--db/migrate/20231024143457_add_text_limit_to_bulk_import_failures.rb16
-rw-r--r--db/migrate/20231024151916_add_index_unique_setting_type_on_vs_code_settings.rb18
-rw-r--r--db/migrate/20231024173744_add_path_to_catalog_resource_components.rb20
-rw-r--r--db/migrate/20231024212214_add_pipeline_cancel_role_restriction_enum.rb12
-rw-r--r--db/migrate/20231025123238_create_compliance_framework_security_policies.rb21
-rw-r--r--db/migrate/20231026050554_add_functions_for_primary_key_lookup.rb26
-rw-r--r--db/migrate/20231027052949_initialize_conversion_of_system_note_metadata_to_bigint.rb18
-rw-r--r--db/migrate/20231027064352_add_service_access_tokens_expiration_application_setting.rb11
-rw-r--r--db/migrate/20231027065205_add_service_access_tokens_expiration_namespace_setting.rb11
-rw-r--r--db/migrate/20231027084327_change_personal_access_tokens_remove_not_null_expires_at.rb17
-rw-r--r--db/migrate/20231030051837_add_project_id_to_packages_tags.rb10
-rw-r--r--db/migrate/20231030051838_add_index_to_packages_tags_project_id.rb15
-rw-r--r--db/migrate/20231030051839_add_foreign_key_to_packages_tags_project_id.rb16
-rw-r--r--db/migrate/20231030205639_update_default_package_metadata_purl_types.rb15
-rw-r--r--db/migrate/20231030205756_index_user_details_on_enterprise_group_id_and_user_id.rb22
-rw-r--r--db/migrate/20231031141439_add_smtp_authentication_to_service_desk_custom_email_credentials.rb10
-rw-r--r--db/migrate/20231031200433_add_framework_fk_to_compliance_framework_security_policies.rb19
-rw-r--r--db/migrate/20231031200645_add_policy_configuration_fk_to_compliance_framework_security_policies.rb19
-rw-r--r--db/migrate/20231102142553_add_zoekt_nodes.rb19
-rw-r--r--db/migrate/20231102142554_migrate_zoekt_shards_to_zoekt_nodes.rb34
-rw-r--r--db/migrate/20231102142555_add_zoekt_node_id_to_indexed_namespaces.rb13
-rw-r--r--db/migrate/20231102142565_add_zoekt_node_foreign_key_to_indexed_namespaces.rb17
-rw-r--r--db/migrate/20231103162825_add_wolfi_purl_type_to_package_metadata_purl_types.rb31
-rw-r--r--db/migrate/20231103195309_remove_deprecated_package_metadata_sync_worker.rb16
-rw-r--r--db/migrate/20231103223224_backfill_zoekt_node_id_on_indexed_namespaces.rb19
-rw-r--r--db/migrate/20231106145853_add_product_analytics_enabled_to_namespace_settings.rb9
-rw-r--r--db/migrate/20231106212340_add_visibility_level_to_catalog_resources.rb13
-rw-r--r--db/migrate/20231107062104_add_network_policy_egress_to_agent.rb22
-rw-r--r--db/migrate/20231107071201_add_project_authorizations_recalculated_at_to_user_details.rb11
-rw-r--r--db/migrate/20231107205734_add_update_namespace_name_to_application_settings.rb9
-rw-r--r--db/migrate/20231108072342_add_display_time_format_preference.rb10
-rw-r--r--db/migrate/20231108093031_add_allow_project_creation_for_guest_and_below_to_application_settings.rb9
-rw-r--r--db/migrate/20231109133153_drop_idx_namespaces_on_ldap_sync_last_successful_update_at_for_gitlab.rb28
-rw-r--r--db/post_migrate/20220531233600_remove_sse_usage_data_from_redis.rb2
-rw-r--r--db/post_migrate/20220617123022_add_unique_index_on_projects_on_runners_token.rb2
-rw-r--r--db/post_migrate/20220617123034_add_unique_index_on_projects_on_runners_token_encrypted.rb2
-rw-r--r--db/post_migrate/20220902204048_move_security_findings_table_to_gitlab_partitions_dynamic_schema.rb2
-rw-r--r--db/post_migrate/20220920135356_tiebreak_user_type_index.rb2
-rw-r--r--db/post_migrate/20221018232820_add_temp_index_for_user_details_fields.rb2
-rw-r--r--db/post_migrate/20221221150123_update_billable_users_index.rb2
-rw-r--r--db/post_migrate/20230131184319_update_billable_users_index_for_service_accounts.rb2
-rw-r--r--db/post_migrate/20230303154314_add_user_type_migration_indexes.rb2
-rw-r--r--db/post_migrate/20230310111859_recreate_user_type_migration_indexes.rb2
-rw-r--r--db/post_migrate/20230313150531_reschedule_migration_for_remediation.rb4
-rw-r--r--db/post_migrate/20230317004428_migrate_daily_redis_hll_events_to_weekly_aggregation.rb2
-rw-r--r--db/post_migrate/20230328111013_re_migrate_redis_slot_keys.rb2
-rw-r--r--db/post_migrate/20230405200858_requeue_backfill_project_wiki_repositories.rb4
-rw-r--r--db/post_migrate/20230508150219_reschedule_evidences_handling_unicode.rb4
-rw-r--r--db/post_migrate/20230522111534_reschedule_migration_for_links_from_metadata.rb4
-rw-r--r--db/post_migrate/20230619005223_change_unconfirmed_created_at_index_on_users.rb2
-rw-r--r--db/post_migrate/20230724150939_index_projects_on_namespace_id_and_repository_size_limit.rb2
-rw-r--r--db/post_migrate/20230728151058_add_auditor_index_to_users_table.rb2
-rw-r--r--db/post_migrate/20230913130629_index_org_id_on_projects.rb2
-rw-r--r--db/post_migrate/20231003045342_migrate_sidekiq_namespaced_jobs.rb59
-rw-r--r--db/post_migrate/20231003142534_add_build_timeout_index.rb2
-rw-r--r--db/post_migrate/20231009105056_index_users_on_email_domain_and_id.rb2
-rw-r--r--db/post_migrate/20231016001000_fix_design_user_mentions_design_id_note_id_index_for_self_managed.rb28
-rw-r--r--db/post_migrate/20231016173128_add_temporary_index_to_merge_access_levels.rb25
-rw-r--r--db/post_migrate/20231016173129_queue_delete_invalid_protected_branch_merge_access_levels.rb28
-rw-r--r--db/post_migrate/20231016194926_add_temporary_index_to_push_access_levels.rb25
-rw-r--r--db/post_migrate/20231016194927_queue_delete_invalid_protected_branch_push_access_levels.rb28
-rw-r--r--db/post_migrate/20231016194942_add_temporary_index_to_create_access_levels.rb25
-rw-r--r--db/post_migrate/20231016194943_queue_delete_invalid_protected_tag_create_access_levels.rb27
-rw-r--r--db/post_migrate/20231017172156_add_index_on_projects_for_adjourned_deletion.rb2
-rw-r--r--db/post_migrate/20231018083247_remove_users_email_opted_in_columns.rb22
-rw-r--r--db/post_migrate/20231018093625_drop_index_namespaces_on_shared_and_extra_runners_minutes_limit.rb18
-rw-r--r--db/post_migrate/20231018105749_remove_application_settings_marketing_emails_enabled_column.rb11
-rw-r--r--db/post_migrate/20231019003052_swap_columns_for_ci_pipelines_pipeline_id_bigint_v2.rb60
-rw-r--r--db/post_migrate/20231019084731_swap_columns_for_ci_stages_pipeline_id_bigint_v2.rb72
-rw-r--r--db/post_migrate/20231019223224_backfill_catalog_resources_name_and_description.rb24
-rw-r--r--db/post_migrate/20231020082425_remove_force_full_reconciliation_from_workspaces.rb9
-rw-r--r--db/post_migrate/20231020150211_delete_duplicated_index_scan_result_policies_on_policy_configuration_id.rb16
-rw-r--r--db/post_migrate/20231023083349_init_conversion_for_p_ci_builds.rb37
-rw-r--r--db/post_migrate/20231023113908_add_index_stopping_environments_on_updated_at.rb18
-rw-r--r--db/post_migrate/20231023164908_async_drop_index_users_on_accepted_term_id.rb19
-rw-r--r--db/post_migrate/20231024015915_drop_index_namespaces_on_created_at_for_gitlab_com.rb24
-rw-r--r--db/post_migrate/20231024025457_cleanup_bigint_conversion_for_ci_project_monthly_usages_shared_runners_duration.rb16
-rw-r--r--db/post_migrate/20231024025533_cleanup_bigint_conversion_for_ci_namespace_monthly_usages_shared_runners_duration.rb16
-rw-r--r--db/post_migrate/20231024025629_cleanup_ci_pipeline_chat_data_pipeline_id_bigint.rb29
-rw-r--r--db/post_migrate/20231024080150_cleanup_ci_sources_pipelines_pipeline_id_bigint.rb35
-rw-r--r--db/post_migrate/20231024124856_remove_redundant_group_stages_index.rb17
-rw-r--r--db/post_migrate/20231024125551_remove_redundant_mr_metrics_index_on_target_project_id.rb17
-rw-r--r--db/post_migrate/20231025025733_swap_columns_for_ci_pipelines_pipeline_id_bigint_for_self_host.rb54
-rw-r--r--db/post_migrate/20231025031337_cleanup_ci_pipeline_messages_pipeline_id_bigint.rb29
-rw-r--r--db/post_migrate/20231025031539_swap_columns_for_ci_stages_pipeline_id_bigint_for_self_host.rb66
-rw-r--r--db/post_migrate/20231026103346_drop_project_settings_jitsu_key.rb21
-rw-r--r--db/post_migrate/20231027013210_remove_partial_index_deployments_for_legacy_successful_deployments.rb20
-rw-r--r--db/post_migrate/20231027060443_backfill_system_note_metadata_id_for_bigint_conversion.rb18
-rw-r--r--db/post_migrate/20231027083355_remove_projects_duplicated_indexes.rb18
-rw-r--r--db/post_migrate/20231030051840_add_not_null_to_packages_tags_project_id.rb14
-rw-r--r--db/post_migrate/20231030071209_queue_backfill_packages_tags_project_id.rb28
-rw-r--r--db/post_migrate/20231030094755_add_index_to_catalog_resources_on_state.rb17
-rw-r--r--db/post_migrate/20231030095419_remove_temp_index_to_packages_on_project_id_when_npm_and_not_pending_destruction.rb23
-rw-r--r--db/post_migrate/20231030154117_insert_new_ultimate_trial_plan_into_plans.rb24
-rw-r--r--db/post_migrate/20231031134320_init_conversion_for_p_ci_builds_for_self_host.rb39
-rw-r--r--db/post_migrate/20231101130230_remove_in_product_marketing_emails_campaign_column.rb31
-rw-r--r--db/post_migrate/20231102083539_backfill_p_ci_builds_pipeline_id.rb34
-rw-r--r--db/post_migrate/20231102142557_remove_zoekt_shard_null_constraint_from_indexed_namespaces.rb14
-rw-r--r--db/post_migrate/20231103132849_add_state_index_for_snippet_repository_storage_move.rb17
-rw-r--r--db/post_migrate/20231105165706_drop_repositories_columns_from_geo_node_status_table.rb34
-rw-r--r--db/post_migrate/20231109183438_drop_merge_request_assignees_on_merge_request_id_index.rb19
-rw-r--r--db/schema_migrations/202309260929141
-rw-r--r--db/schema_migrations/202309260929441
-rw-r--r--db/schema_migrations/202309260930041
-rw-r--r--db/schema_migrations/202309260930251
-rw-r--r--db/schema_migrations/202309260931011
-rw-r--r--db/schema_migrations/202309260931441
-rw-r--r--db/schema_migrations/202309260932111
-rw-r--r--db/schema_migrations/202309260932511
-rw-r--r--db/schema_migrations/202309261054401
-rw-r--r--db/schema_migrations/202309261059311
-rw-r--r--db/schema_migrations/202309271242021
-rw-r--r--db/schema_migrations/202309281455551
-rw-r--r--db/schema_migrations/202309281456371
-rw-r--r--db/schema_migrations/202309291551231
-rw-r--r--db/schema_migrations/202310021629411
-rw-r--r--db/schema_migrations/202310030453421
-rw-r--r--db/schema_migrations/202310051518161
-rw-r--r--db/schema_migrations/202310091157131
-rw-r--r--db/schema_migrations/202310132049331
-rw-r--r--db/schema_migrations/202310160010001
-rw-r--r--db/schema_migrations/202310161731281
-rw-r--r--db/schema_migrations/202310161731291
-rw-r--r--db/schema_migrations/202310161949261
-rw-r--r--db/schema_migrations/202310161949271
-rw-r--r--db/schema_migrations/202310161949421
-rw-r--r--db/schema_migrations/202310161949431
-rw-r--r--db/schema_migrations/202310170957381
-rw-r--r--db/schema_migrations/202310171343491
-rw-r--r--db/schema_migrations/202310171352071
-rw-r--r--db/schema_migrations/202310171548041
-rw-r--r--db/schema_migrations/202310171814031
-rw-r--r--db/schema_migrations/202310180832471
-rw-r--r--db/schema_migrations/202310180936251
-rw-r--r--db/schema_migrations/202310181057491
-rw-r--r--db/schema_migrations/202310181401541
-rw-r--r--db/schema_migrations/202310181524191
-rw-r--r--db/schema_migrations/202310190030521
-rw-r--r--db/schema_migrations/202310190847311
-rw-r--r--db/schema_migrations/202310191042111
-rw-r--r--db/schema_migrations/202310191228551
-rw-r--r--db/schema_migrations/202310191452021
-rw-r--r--db/schema_migrations/202310191804211
-rw-r--r--db/schema_migrations/202310192232241
-rw-r--r--db/schema_migrations/202310200207321
-rw-r--r--db/schema_migrations/202310200742271
-rw-r--r--db/schema_migrations/202310200824251
-rw-r--r--db/schema_migrations/202310200956241
-rw-r--r--db/schema_migrations/202310201125411
-rw-r--r--db/schema_migrations/202310201502111
-rw-r--r--db/schema_migrations/202310201816521
-rw-r--r--db/schema_migrations/202310230738411
-rw-r--r--db/schema_migrations/202310230833491
-rw-r--r--db/schema_migrations/202310231139081
-rw-r--r--db/schema_migrations/202310231140061
-rw-r--r--db/schema_migrations/202310231145511
-rw-r--r--db/schema_migrations/202310231219551
-rw-r--r--db/schema_migrations/202310231225081
-rw-r--r--db/schema_migrations/202310231649081
-rw-r--r--db/schema_migrations/202310240159151
-rw-r--r--db/schema_migrations/202310240254571
-rw-r--r--db/schema_migrations/202310240255331
-rw-r--r--db/schema_migrations/202310240256291
-rw-r--r--db/schema_migrations/202310240801501
-rw-r--r--db/schema_migrations/202310241234441
-rw-r--r--db/schema_migrations/202310241248561
-rw-r--r--db/schema_migrations/202310241255511
-rw-r--r--db/schema_migrations/202310241332341
-rw-r--r--db/schema_migrations/202310241422361
-rw-r--r--db/schema_migrations/202310241434571
-rw-r--r--db/schema_migrations/202310241519161
-rw-r--r--db/schema_migrations/202310241737441
-rw-r--r--db/schema_migrations/202310242122141
-rw-r--r--db/schema_migrations/202310250257331
-rw-r--r--db/schema_migrations/202310250313371
-rw-r--r--db/schema_migrations/202310250315391
-rw-r--r--db/schema_migrations/202310251232381
-rw-r--r--db/schema_migrations/202310260505541
-rw-r--r--db/schema_migrations/202310261033461
-rw-r--r--db/schema_migrations/202310270132101
-rw-r--r--db/schema_migrations/202310270529491
-rw-r--r--db/schema_migrations/202310270604431
-rw-r--r--db/schema_migrations/202310270643521
-rw-r--r--db/schema_migrations/202310270652051
-rw-r--r--db/schema_migrations/202310270833551
-rw-r--r--db/schema_migrations/202310270843271
-rw-r--r--db/schema_migrations/202310300518371
-rw-r--r--db/schema_migrations/202310300518381
-rw-r--r--db/schema_migrations/202310300518391
-rw-r--r--db/schema_migrations/202310300518401
-rw-r--r--db/schema_migrations/202310300712091
-rw-r--r--db/schema_migrations/202310300947551
-rw-r--r--db/schema_migrations/202310300954191
-rw-r--r--db/schema_migrations/202310301541171
-rw-r--r--db/schema_migrations/202310302056391
-rw-r--r--db/schema_migrations/202310302057561
-rw-r--r--db/schema_migrations/202310311343201
-rw-r--r--db/schema_migrations/202310311414391
-rw-r--r--db/schema_migrations/202310312004331
-rw-r--r--db/schema_migrations/202310312006451
-rw-r--r--db/schema_migrations/202311011302301
-rw-r--r--db/schema_migrations/202311020835391
-rw-r--r--db/schema_migrations/202311021425531
-rw-r--r--db/schema_migrations/202311021425541
-rw-r--r--db/schema_migrations/202311021425551
-rw-r--r--db/schema_migrations/202311021425571
-rw-r--r--db/schema_migrations/202311021425651
-rw-r--r--db/schema_migrations/202311031328491
-rw-r--r--db/schema_migrations/202311031628251
-rw-r--r--db/schema_migrations/202311031953091
-rw-r--r--db/schema_migrations/202311032232241
-rw-r--r--db/schema_migrations/202311051657061
-rw-r--r--db/schema_migrations/202311061458531
-rw-r--r--db/schema_migrations/202311062123401
-rw-r--r--db/schema_migrations/202311070621041
-rw-r--r--db/schema_migrations/202311070712011
-rw-r--r--db/schema_migrations/202311072057341
-rw-r--r--db/schema_migrations/202311080723421
-rw-r--r--db/schema_migrations/202311080930311
-rw-r--r--db/schema_migrations/202311091331531
-rw-r--r--db/schema_migrations/202311091834381
-rw-r--r--db/structure.sql1035
-rw-r--r--doc/.vale/gitlab/LatinTerms.yml1
-rw-r--r--doc/.vale/gitlab/Wordy.yml1
-rw-r--r--doc/administration/audit_event_streaming/audit_event_types.md7
-rw-r--r--doc/administration/audit_event_streaming/graphql_api.md23
-rw-r--r--doc/administration/audit_event_streaming/index.md95
-rw-r--r--doc/administration/audit_events.md181
-rw-r--r--doc/administration/auditor_users.md3
-rw-r--r--doc/administration/auth/ldap/index.md2
-rw-r--r--doc/administration/backup_restore/backup_gitlab.md9
-rw-r--r--doc/administration/cicd.md102
-rw-r--r--doc/administration/dedicated/index.md50
-rw-r--r--doc/administration/geo/disaster_recovery/index.md1
-rw-r--r--doc/administration/geo/index.md3
-rw-r--r--doc/administration/geo/replication/troubleshooting.md8
-rw-r--r--doc/administration/geo/setup/index.md2
-rw-r--r--doc/administration/gitaly/configure_gitaly.md166
-rw-r--r--doc/administration/gitaly/img/gitaly_adaptive_concurrency_limit.pngbin0 -> 36052 bytes
-rw-r--r--doc/administration/gitaly/index.md86
-rw-r--r--doc/administration/gitaly/monitoring.md41
-rw-r--r--doc/administration/gitaly/recovery.md54
-rw-r--r--doc/administration/gitaly/troubleshooting.md37
-rw-r--r--doc/administration/inactive_project_deletion.md19
-rw-r--r--doc/administration/incoming_email.md7
-rw-r--r--doc/administration/instance_limits.md13
-rw-r--r--doc/administration/integration/plantuml.md9
-rw-r--r--doc/administration/logs/index.md6
-rw-r--r--doc/administration/logs/log_parsing.md20
-rw-r--r--doc/administration/merge_request_diffs.md117
-rw-r--r--doc/administration/moderate_users.md39
-rw-r--r--doc/administration/monitoring/performance/performance_bar.md4
-rw-r--r--doc/administration/monitoring/prometheus/gitlab_metrics.md10
-rw-r--r--doc/administration/monitoring/prometheus/index.md4
-rw-r--r--doc/administration/monitoring/prometheus/web_exporter.md8
-rw-r--r--doc/administration/operations/puma.md31
-rw-r--r--doc/administration/package_information/supported_os.md2
-rw-r--r--doc/administration/packages/container_registry.md44
-rw-r--r--doc/administration/pages/index.md4
-rw-r--r--doc/administration/postgresql/external.md7
-rw-r--r--doc/administration/postgresql/external_metrics.md33
-rw-r--r--doc/administration/postgresql/external_upgrade.md48
-rw-r--r--doc/administration/postgresql/index.md5
-rw-r--r--doc/administration/raketasks/geo.md84
-rw-r--r--doc/administration/raketasks/github_import.md6
-rw-r--r--doc/administration/reference_architectures/10k_users.md43
-rw-r--r--doc/administration/reference_architectures/1k_users.md37
-rw-r--r--doc/administration/reference_architectures/25k_users.md43
-rw-r--r--doc/administration/reference_architectures/2k_users.md33
-rw-r--r--doc/administration/reference_architectures/3k_users.md45
-rw-r--r--doc/administration/reference_architectures/50k_users.md43
-rw-r--r--doc/administration/reference_architectures/5k_users.md46
-rw-r--r--doc/administration/reference_architectures/index.md118
-rw-r--r--doc/administration/review_spam_logs.md40
-rw-r--r--doc/administration/settings/continuous_integration.md16
-rw-r--r--doc/administration/settings/gitaly_timeouts.md10
-rw-r--r--doc/administration/settings/jira_cloud_app.md76
-rw-r--r--doc/administration/settings/rate_limits_on_git_ssh_operations.md3
-rw-r--r--doc/administration/settings/scim_setup.md4
-rw-r--r--doc/administration/settings/sign_in_restrictions.md2
-rw-r--r--doc/administration/settings/slack_app.md8
-rw-r--r--doc/administration/settings/usage_statistics.md40
-rw-r--r--doc/administration/sidekiq/index.md37
-rw-r--r--doc/administration/sidekiq/processing_specific_job_classes.md37
-rw-r--r--doc/administration/sidekiq/sidekiq_troubleshooting.md22
-rw-r--r--doc/administration/silent_mode/index.md9
-rw-r--r--doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md4
-rw-r--r--doc/api/api_resources.md18
-rw-r--r--doc/api/bulk_imports.md23
-rw-r--r--doc/api/container_registry.md19
-rw-r--r--doc/api/dependency_list_export.md10
-rw-r--r--doc/api/deployments.md4
-rw-r--r--doc/api/geo_nodes.md14
-rw-r--r--doc/api/geo_sites.md3
-rw-r--r--doc/api/graphql/reference/index.md1236
-rw-r--r--doc/api/group_iterations.md4
-rw-r--r--doc/api/group_protected_environments.md17
-rw-r--r--doc/api/groups.md17
-rw-r--r--doc/api/import.md12
-rw-r--r--doc/api/invitations.md1
-rw-r--r--doc/api/iterations.md4
-rw-r--r--doc/api/jobs.md5
-rw-r--r--doc/api/lint.md4
-rw-r--r--doc/api/member_roles.md15
-rw-r--r--doc/api/merge_request_approvals.md2
-rw-r--r--doc/api/merge_requests.md4
-rw-r--r--doc/api/packages.md62
-rw-r--r--doc/api/pipelines.md56
-rw-r--r--doc/api/projects.md3
-rw-r--r--doc/api/protected_environments.md6
-rw-r--r--doc/api/rest/index.md1
-rw-r--r--doc/api/runners.md84
-rw-r--r--doc/api/saml.md12
-rw-r--r--doc/api/scim.md14
-rw-r--r--doc/api/settings.md13
-rw-r--r--doc/api/users.md12
-rw-r--r--doc/architecture/blueprints/cdot_orders/index.md265
-rw-r--r--doc/architecture/blueprints/cells/impacted_features/personal-access-tokens.md28
-rw-r--r--doc/architecture/blueprints/cells/index.md2
-rw-r--r--doc/architecture/blueprints/ci_pipeline_components/img/catalogs.pngbin30325 -> 0 bytes
-rw-r--r--doc/architecture/blueprints/ci_pipeline_components/index.md59
-rw-r--r--doc/architecture/blueprints/cloud_connector/decisions/001_lb_entry_point.md52
-rw-r--r--doc/architecture/blueprints/cloud_connector/index.md12
-rw-r--r--doc/architecture/blueprints/container_registry_metadata_database/index.md10
-rw-r--r--doc/architecture/blueprints/container_registry_metadata_database_self_managed_rollout/index.md2
-rw-r--r--doc/architecture/blueprints/email_ingestion/index.md2
-rw-r--r--doc/architecture/blueprints/feature_flags_usage_in_dev_and_ops/index.md285
-rw-r--r--doc/architecture/blueprints/gitlab_ml_experiments/index.md67
-rw-r--r--doc/architecture/blueprints/gitlab_steps/gitlab-ci.md247
-rw-r--r--doc/architecture/blueprints/gitlab_steps/index.md15
-rw-r--r--doc/architecture/blueprints/gitlab_steps/step-definition.md368
-rw-r--r--doc/architecture/blueprints/gitlab_steps/steps-syntactic-sugar.md66
-rw-r--r--doc/architecture/blueprints/google_artifact_registry_integration/index.md2
-rw-r--r--doc/architecture/blueprints/new_diffs.md29
-rw-r--r--doc/architecture/blueprints/observability_logging/diagrams.drawio1
-rw-r--r--doc/architecture/blueprints/observability_logging/index.md632
-rw-r--r--doc/architecture/blueprints/observability_logging/system_overview.pngbin0 -> 76330 bytes
-rw-r--r--doc/architecture/blueprints/organization/diagrams/organization-isolation-broken.drawio.pngbin0 -> 57795 bytes
-rw-r--r--doc/architecture/blueprints/organization/diagrams/organization-isolation.drawio.pngbin0 -> 56021 bytes
-rw-r--r--doc/architecture/blueprints/organization/index.md3
-rw-r--r--doc/architecture/blueprints/organization/isolation.md152
-rw-r--r--doc/architecture/blueprints/runner_admission_controller/index.md97
-rw-r--r--doc/architecture/blueprints/secret_detection/index.md124
-rw-r--r--doc/architecture/blueprints/secret_manager/decisions/002_gcp_kms.md101
-rw-r--r--doc/architecture/blueprints/secret_manager/decisions/003_go_service.md37
-rw-r--r--doc/architecture/blueprints/secret_manager/decisions/004_staleless_kms.md49
-rw-r--r--doc/architecture/blueprints/secret_manager/index.md18
-rw-r--r--doc/architecture/blueprints/work_items/index.md32
-rw-r--r--doc/ci/chatops/index.md61
-rw-r--r--doc/ci/cloud_services/azure/index.md21
-rw-r--r--doc/ci/cloud_services/google_cloud/index.md4
-rw-r--r--doc/ci/components/index.md94
-rw-r--r--doc/ci/debugging.md295
-rw-r--r--doc/ci/docker/using_docker_build.md4
-rw-r--r--doc/ci/docker/using_docker_images.md75
-rw-r--r--doc/ci/enable_or_disable_ci.md62
-rw-r--r--doc/ci/environments/deployment_approvals.md129
-rw-r--r--doc/ci/environments/kubernetes_dashboard.md14
-rw-r--r--doc/ci/index.md31
-rw-r--r--doc/ci/jobs/ci_job_token.md40
-rw-r--r--doc/ci/jobs/index.md67
-rw-r--r--doc/ci/jobs/job_control.md24
-rw-r--r--doc/ci/migration/bamboo.md780
-rw-r--r--doc/ci/migration/github_actions.md4
-rw-r--r--doc/ci/migration/jenkins.md4
-rw-r--r--doc/ci/pipelines/merge_request_pipelines.md22
-rw-r--r--doc/ci/pipelines/merge_trains.md35
-rw-r--r--doc/ci/pipelines/merged_results_pipelines.md13
-rw-r--r--doc/ci/pipelines/settings.md24
-rw-r--r--doc/ci/quick_start/index.md2
-rw-r--r--doc/ci/runners/new_creation_workflow.md23
-rw-r--r--doc/ci/runners/runners_scope.md6
-rw-r--r--doc/ci/runners/saas/linux_saas_runner.md2
-rw-r--r--doc/ci/runners/saas/macos_saas_runner.md28
-rw-r--r--doc/ci/secrets/azure_key_vault.md66
-rw-r--r--doc/ci/testing/browser_performance_testing.md3
-rw-r--r--doc/ci/testing/code_coverage.md5
-rw-r--r--doc/ci/testing/code_quality.md47
-rw-r--r--doc/ci/triggers/index.md1
-rw-r--r--doc/ci/troubleshooting.md558
-rw-r--r--doc/ci/variables/index.md72
-rw-r--r--doc/ci/variables/predefined_variables.md12
-rw-r--r--doc/ci/yaml/gitlab_ci_yaml.md92
-rw-r--r--doc/ci/yaml/img/job_running_v13_10.pngbin57525 -> 0 bytes
-rw-r--r--doc/ci/yaml/img/pipeline_status.pngbin54243 -> 0 bytes
-rw-r--r--doc/ci/yaml/img/rollback.pngbin41693 -> 0 bytes
-rw-r--r--doc/ci/yaml/index.md705
-rw-r--r--doc/ci/yaml/inputs.md86
-rw-r--r--doc/development/ai_architecture.md5
-rw-r--r--doc/development/ai_features/duo_chat.md37
-rw-r--r--doc/development/ai_features/index.md134
-rw-r--r--doc/development/api_graphql_styleguide.md9
-rw-r--r--doc/development/backend/create_source_code_be/gitaly_touch_points.md6
-rw-r--r--doc/development/bulk_import.md9
-rw-r--r--doc/development/cells/index.md1
-rw-r--r--doc/development/code_review.md10
-rw-r--r--doc/development/contributing/first_contribution.md2
-rw-r--r--doc/development/contributing/img/bot_ready.pngbin9367 -> 0 bytes
-rw-r--r--doc/development/contributing/img/bot_ready_v16_6.pngbin0 -> 7163 bytes
-rw-r--r--doc/development/dangerbot.md7
-rw-r--r--doc/development/database/avoiding_downtime_in_migrations.md11
-rw-r--r--doc/development/database/clickhouse/clickhouse_within_gitlab.md45
-rw-r--r--doc/development/database/database_lab.md2
-rw-r--r--doc/development/database/iterating_tables_in_batches.md4
-rw-r--r--doc/development/database/loose_foreign_keys.md6
-rw-r--r--doc/development/database/multiple_databases.md10
-rw-r--r--doc/development/database/understanding_explain_plans.md1
-rw-r--r--doc/development/development_processes.md57
-rw-r--r--doc/development/distributed_tracing.md4
-rw-r--r--doc/development/documentation/styleguide/index.md36
-rw-r--r--doc/development/documentation/styleguide/word_list.md30
-rw-r--r--doc/development/documentation/versions.md5
-rw-r--r--doc/development/documentation/workflow.md12
-rw-r--r--doc/development/ee_features.md24
-rw-r--r--doc/development/experiment_guide/implementing_experiments.md2
-rw-r--r--doc/development/export_csv.md2
-rw-r--r--doc/development/fe_guide/graphql.md37
-rw-r--r--doc/development/fe_guide/security.md51
-rw-r--r--doc/development/fe_guide/sentry.md5
-rw-r--r--doc/development/fe_guide/storybook.md34
-rw-r--r--doc/development/fe_guide/style/scss.md96
-rw-r--r--doc/development/fe_guide/style/typescript.md215
-rw-r--r--doc/development/fe_guide/type_hinting.md215
-rw-r--r--doc/development/feature_flags/controls.md11
-rw-r--r--doc/development/feature_flags/index.md6
-rw-r--r--doc/development/gems.md5
-rw-r--r--doc/development/gitaly.md43
-rw-r--r--doc/development/github_importer.md46
-rw-r--r--doc/development/i18n/externalization.md2
-rw-r--r--doc/development/i18n/proofreader.md1
-rw-r--r--doc/development/img/runner_fleet_dashboard.pngbin0 -> 38440 bytes
-rw-r--r--doc/development/index.md2
-rw-r--r--doc/development/internal_analytics/index.md53
-rw-r--r--doc/development/internal_analytics/internal_event_instrumentation/local_setup_and_debugging.md53
-rw-r--r--doc/development/internal_analytics/internal_event_instrumentation/quick_start.md24
-rw-r--r--doc/development/internal_analytics/metrics/metrics_dictionary.md2
-rw-r--r--doc/development/internal_analytics/service_ping/index.md119
-rw-r--r--doc/development/internal_api/index.md4
-rw-r--r--doc/development/migration_style_guide.md20
-rw-r--r--doc/development/permissions/custom_roles.md4
-rw-r--r--doc/development/pipelines/index.md33
-rw-r--r--doc/development/repository_storage_moves/index.md102
-rw-r--r--doc/development/rubocop_development_guide.md48
-rw-r--r--doc/development/ruby_upgrade.md14
-rw-r--r--doc/development/runner_fleet_dashboard.md245
-rw-r--r--doc/development/testing_guide/end_to_end/beginners_guide.md10
-rw-r--r--doc/development/testing_guide/end_to_end/capybara_to_chemlab_migration_guide.md38
-rw-r--r--doc/development/utilities.md2
-rw-r--r--doc/development/wikis.md3
-rw-r--r--doc/devsecops.md60
-rw-r--r--doc/gitlab-basics/start-using-git.md9
-rw-r--r--doc/install/aws/eks_clusters_aws.md49
-rw-r--r--doc/install/aws/gitlab_hybrid_on_aws.md380
-rw-r--r--doc/install/aws/gitlab_sre_for_aws.md98
-rw-r--r--doc/install/aws/index.md880
-rw-r--r--doc/install/aws/manual_install_aws.md859
-rw-r--r--doc/install/docker.md5
-rw-r--r--doc/install/installation.md2
-rw-r--r--doc/install/relative_url.md2
-rw-r--r--doc/install/requirements.md5
-rw-r--r--doc/integration/advanced_search/elasticsearch.md33
-rw-r--r--doc/integration/advanced_search/elasticsearch_troubleshooting.md10
-rw-r--r--doc/integration/jenkins.md1
-rw-r--r--doc/integration/jira/connect-app.md8
-rw-r--r--doc/integration/jira/development_panel.md2
-rw-r--r--doc/integration/jira/issues.md3
-rw-r--r--doc/integration/kerberos.md2
-rw-r--r--doc/integration/mattermost/index.md1
-rw-r--r--doc/integration/oauth2_generic.md3
-rw-r--r--doc/integration/shibboleth.md2
-rw-r--r--doc/operations/feature_flags.md25
-rw-r--r--doc/operations/incident_management/manage_incidents.md2
-rw-r--r--doc/policy/experiment-beta-support.md6
-rw-r--r--doc/security/email_verification.md10
-rw-r--r--doc/security/reset_user_password.md4
-rw-r--r--doc/security/token_overview.md33
-rw-r--r--doc/security/unlock_user.md10
-rw-r--r--doc/solutions/cloud/aws/gitaly_sre_for_aws.md91
-rw-r--r--doc/solutions/cloud/aws/gitlab_aws_integration.md103
-rw-r--r--doc/solutions/cloud/aws/gitlab_aws_partner_designations.md38
-rw-r--r--doc/solutions/cloud/aws/gitlab_instance_on_aws.md55
-rw-r--r--doc/solutions/cloud/aws/gitlab_single_box_on_aws.md51
-rw-r--r--doc/solutions/cloud/aws/img/all-aws-partner-designations.pngbin0 -> 12275 bytes
-rw-r--r--doc/solutions/cloud/aws/index.md84
-rw-r--r--doc/solutions/cloud/index.md13
-rw-r--r--doc/solutions/index.md19
-rw-r--r--doc/subscriptions/bronze_starter.md2
-rw-r--r--doc/subscriptions/gitlab_com/index.md13
-rw-r--r--doc/subscriptions/gitlab_dedicated/index.md5
-rw-r--r--doc/subscriptions/self_managed/index.md39
-rw-r--r--doc/topics/autodevops/cicd_variables.md3
-rw-r--r--doc/topics/autodevops/customize.md5
-rw-r--r--doc/topics/offline/quick_start_guide.md2
-rw-r--r--doc/tutorials/build_application.md2
-rw-r--r--doc/tutorials/left_sidebar/index.md6
-rw-r--r--doc/tutorials/product_analytics_onboarding_website_project/index.md139
-rw-r--r--doc/update/deprecations.md229
-rw-r--r--doc/update/versions/gitlab_15_changes.md10
-rw-r--r--doc/update/versions/gitlab_16_changes.md171
-rw-r--r--doc/user/ai_features.md130
-rw-r--r--doc/user/analytics/analytics_dashboards.md6
-rw-r--r--doc/user/analytics/dora_metrics.md68
-rw-r--r--doc/user/analytics/value_streams_dashboard.md4
-rw-r--r--doc/user/application_security/container_scanning/index.md140
-rw-r--r--doc/user/application_security/continuous_vulnerability_scanning/index.md5
-rw-r--r--doc/user/application_security/dast/browser_based.md33
-rw-r--r--doc/user/application_security/dast/checks/89.1.md37
-rw-r--r--doc/user/application_security/dast/checks/917.1.md33
-rw-r--r--doc/user/application_security/dast/checks/94.1.md53
-rw-r--r--doc/user/application_security/dast/checks/94.2.md51
-rw-r--r--doc/user/application_security/dast/checks/94.3.md45
-rw-r--r--doc/user/application_security/dast/checks/943.1.md30
-rw-r--r--doc/user/application_security/dast/checks/index.md6
-rw-r--r--doc/user/application_security/dast/proxy-based.md7
-rw-r--r--doc/user/application_security/dependency_scanning/index.md33
-rw-r--r--doc/user/application_security/get-started-security.md48
-rw-r--r--doc/user/application_security/index.md12
-rw-r--r--doc/user/application_security/policies/scan-execution-policies.md6
-rw-r--r--doc/user/application_security/policies/scan-result-policies.md179
-rw-r--r--doc/user/application_security/sast/customize_rulesets.md4
-rw-r--r--doc/user/application_security/sast/index.md4
-rw-r--r--doc/user/application_security/sast/rules.md2
-rw-r--r--doc/user/application_security/sast/troubleshooting.md10
-rw-r--r--doc/user/application_security/secret_detection/index.md50
-rw-r--r--doc/user/application_security/security_dashboard/img/group_security_dashboard.pngbin0 -> 234627 bytes
-rw-r--r--doc/user/application_security/security_dashboard/img/project_security_dashboard.pngbin0 -> 157184 bytes
-rw-r--r--doc/user/application_security/security_dashboard/img/security_center_dashboard_v15_10.pngbin22361 -> 0 bytes
-rw-r--r--doc/user/application_security/security_dashboard/index.md166
-rw-r--r--doc/user/application_security/terminology/index.md2
-rw-r--r--doc/user/application_security/vulnerabilities/img/create_mr_from_vulnerability_v13_4.pngbin16106 -> 0 bytes
-rw-r--r--doc/user/application_security/vulnerabilities/img/create_mr_from_vulnerability_v13_4_updated.pngbin0 -> 65832 bytes
-rw-r--r--doc/user/application_security/vulnerabilities/index.md15
-rw-r--r--doc/user/application_security/vulnerability_report/index.md85
-rw-r--r--doc/user/clusters/agent/gitops/example_repository_structure.md2
-rw-r--r--doc/user/clusters/agent/gitops/flux_oci_tutorial.md2
-rw-r--r--doc/user/clusters/agent/gitops/flux_tutorial.md1
-rw-r--r--doc/user/clusters/agent/install/index.md4
-rw-r--r--doc/user/clusters/agent/user_access.md59
-rw-r--r--doc/user/clusters/agent/vulnerabilities.md21
-rw-r--r--doc/user/compliance/compliance_center/index.md7
-rw-r--r--doc/user/compliance/license_list.md2
-rw-r--r--doc/user/compliance/license_scanning_of_cyclonedx_files/index.md9
-rw-r--r--doc/user/custom_roles.md89
-rw-r--r--doc/user/discussions/img/add_internal_note_v15_0.pngbin18963 -> 0 bytes
-rw-r--r--doc/user/discussions/img/add_internal_note_v16_6.pngbin0 -> 8531 bytes
-rw-r--r--doc/user/discussions/img/create_thread_v16_6.pngbin0 -> 14366 bytes
-rw-r--r--doc/user/discussions/img/discussion_comment.pngbin18323 -> 0 bytes
-rw-r--r--doc/user/discussions/img/quickly_assign_commenter_v13_1.pngbin43849 -> 0 bytes
-rw-r--r--doc/user/discussions/img/quickly_assign_commenter_v16_6.pngbin0 -> 11074 bytes
-rw-r--r--doc/user/discussions/index.md12
-rw-r--r--doc/user/feature_flags.md2
-rw-r--r--doc/user/free_push_limit.md4
-rw-r--r--doc/user/gitlab_duo_chat.md67
-rw-r--r--doc/user/group/access_and_permissions.md9
-rw-r--r--doc/user/group/epics/manage_epics.md2
-rw-r--r--doc/user/group/import/index.md21
-rw-r--r--doc/user/group/index.md6
-rw-r--r--doc/user/group/manage.md28
-rw-r--r--doc/user/group/reporting/git_abuse_rate_limit.md2
-rw-r--r--doc/user/group/saml_sso/group_sync.md2
-rw-r--r--doc/user/group/saml_sso/index.md40
-rw-r--r--doc/user/group/saml_sso/troubleshooting.md20
-rw-r--r--doc/user/group/saml_sso/troubleshooting_scim.md19
-rw-r--r--doc/user/group/value_stream_analytics/index.md15
-rw-r--r--doc/user/img/snippet_clone_button_v13_0.pngbin33081 -> 0 bytes
-rw-r--r--doc/user/img/snippet_intro_v13_11.pngbin15293 -> 0 bytes
-rw-r--r--doc/user/img/snippet_sample_v16_6.pngbin0 -> 34750 bytes
-rw-r--r--doc/user/infrastructure/clusters/connect/new_gke_cluster.md6
-rw-r--r--doc/user/infrastructure/iac/index.md1
-rw-r--r--doc/user/infrastructure/iac/mr_integration.md11
-rw-r--r--doc/user/infrastructure/iac/terraform_state.md10
-rw-r--r--doc/user/markdown.md3
-rw-r--r--doc/user/okrs.md18
-rw-r--r--doc/user/organization/index.md38
-rw-r--r--doc/user/packages/composer_repository/index.md2
-rw-r--r--doc/user/packages/container_registry/index.md2
-rw-r--r--doc/user/packages/container_registry/reduce_container_registry_storage.md51
-rw-r--r--doc/user/packages/container_registry/troubleshoot_container_registry.md27
-rw-r--r--doc/user/packages/generic_packages/index.md6
-rw-r--r--doc/user/packages/maven_repository/index.md97
-rw-r--r--doc/user/packages/npm_registry/index.md10
-rw-r--r--doc/user/packages/nuget_repository/index.md17
-rw-r--r--doc/user/packages/package_registry/supported_functionality.md6
-rw-r--r--doc/user/permissions.md11
-rw-r--r--doc/user/product_analytics/index.md56
-rw-r--r--doc/user/product_analytics/instrumentation/browser_sdk.md282
-rw-r--r--doc/user/product_analytics/instrumentation/index.md15
-rw-r--r--doc/user/profile/account/delete_account.md10
-rw-r--r--doc/user/profile/account/two_factor_authentication.md6
-rw-r--r--doc/user/profile/comment_templates.md9
-rw-r--r--doc/user/profile/img/comment_template_v16_6.pngbin0 -> 15154 bytes
-rw-r--r--doc/user/profile/img/saved_replies_dropdown_v16_0.pngbin16149 -> 0 bytes
-rw-r--r--doc/user/profile/index.md7
-rw-r--r--doc/user/profile/notifications.md30
-rw-r--r--doc/user/profile/personal_access_tokens.md36
-rw-r--r--doc/user/profile/preferences.md16
-rw-r--r--doc/user/profile/service_accounts.md4
-rw-r--r--doc/user/project/codeowners/index.md50
-rw-r--r--doc/user/project/deploy_tokens/index.md6
-rw-r--r--doc/user/project/import/github.md14
-rw-r--r--doc/user/project/import/jira.md4
-rw-r--r--doc/user/project/index.md17
-rw-r--r--doc/user/project/integrations/aws_codepipeline.md4
-rw-r--r--doc/user/project/integrations/gitlab_slack_application.md14
-rw-r--r--doc/user/project/issues/associate_zoom_meeting.md2
-rw-r--r--doc/user/project/issues/img/zoom-quickaction-button.pngbin43369 -> 0 bytes
-rw-r--r--doc/user/project/issues/img/zoom_quickaction_button_v16_6.pngbin0 -> 8668 bytes
-rw-r--r--doc/user/project/issues/issue_weight.md3
-rw-r--r--doc/user/project/members/index.md1
-rw-r--r--doc/user/project/members/share_project_with_groups.md5
-rw-r--r--doc/user/project/merge_requests/ai_in_merge_requests.md10
-rw-r--r--doc/user/project/merge_requests/approvals/settings.md23
-rw-r--r--doc/user/project/merge_requests/cherry_pick_changes.md13
-rw-r--r--doc/user/project/merge_requests/dependencies.md6
-rw-r--r--doc/user/project/merge_requests/drafts.md35
-rw-r--r--doc/user/project/merge_requests/index.md29
-rw-r--r--doc/user/project/merge_requests/merge_when_pipeline_succeeds.md2
-rw-r--r--doc/user/project/merge_requests/revert_changes.md2
-rw-r--r--doc/user/project/merge_requests/reviews/data_usage.md2
-rw-r--r--doc/user/project/merge_requests/reviews/img/comment-on-any-diff-line_v13_10.pngbin21304 -> 0 bytes
-rw-r--r--doc/user/project/merge_requests/reviews/img/comment_on_any_diff_line_v16_6.pngbin0 -> 12677 bytes
-rw-r--r--doc/user/project/merge_requests/reviews/img/mr_review_new_comment_v15_3.pngbin32927 -> 0 bytes
-rw-r--r--doc/user/project/merge_requests/reviews/img/mr_review_new_comment_v16_6.pngbin0 -> 11833 bytes
-rw-r--r--doc/user/project/merge_requests/reviews/img/mr_summary_comment_v15_4.pngbin61841 -> 0 bytes
-rw-r--r--doc/user/project/merge_requests/reviews/img/mr_summary_comment_v16_6.pngbin0 -> 16816 bytes
-rw-r--r--doc/user/project/merge_requests/reviews/index.md20
-rw-r--r--doc/user/project/merge_requests/status_checks.md4
-rw-r--r--doc/user/project/pages/public_folder.md15
-rw-r--r--doc/user/project/protected_branches.md47
-rw-r--r--doc/user/project/push_options.md3
-rw-r--r--doc/user/project/repository/branches/index.md37
-rw-r--r--doc/user/project/repository/code_suggestions/index.md34
-rw-r--r--doc/user/project/repository/code_suggestions/self_managed.md2
-rw-r--r--doc/user/project/repository/code_suggestions/troubleshooting.md3
-rw-r--r--doc/user/project/repository/forking_workflow.md7
-rw-r--r--doc/user/project/repository/reducing_the_repo_size_using_git.md7
-rw-r--r--doc/user/project/service_desk/configure.md2
-rw-r--r--doc/user/project/service_desk/using_service_desk.md5
-rw-r--r--doc/user/project/settings/project_access_tokens.md2
-rw-r--r--doc/user/project/system_notes.md18
-rw-r--r--doc/user/project/wiki/index.md6
-rw-r--r--doc/user/read_only_namespaces.md2
-rw-r--r--doc/user/report_abuse.md9
-rw-r--r--doc/user/reserved_names.md39
-rw-r--r--doc/user/search/index.md22
-rw-r--r--doc/user/shortcuts.md5
-rw-r--r--doc/user/snippets.md9
-rw-r--r--doc/user/storage_management_automation.md34
-rw-r--r--doc/user/tasks.md19
-rw-r--r--doc/user/usage_quotas.md137
-rw-r--r--doc/user/workspace/index.md12
-rw-r--r--fixtures/emojis/digests.json19114
-rw-r--r--gems/config/rubocop.yml3
-rw-r--r--gems/gitlab-backup-cli/.gitignore11
-rw-r--r--gems/gitlab-backup-cli/.gitlab-ci.yml4
-rw-r--r--gems/gitlab-backup-cli/.rspec3
-rw-r--r--gems/gitlab-backup-cli/.rubocop.yml2
-rw-r--r--gems/gitlab-backup-cli/Gemfile6
-rw-r--r--gems/gitlab-backup-cli/Gemfile.lock114
-rw-r--r--gems/gitlab-backup-cli/LICENSE.txt21
-rw-r--r--gems/gitlab-backup-cli/README.md7
-rw-r--r--gems/gitlab-backup-cli/Rakefile12
-rwxr-xr-xgems/gitlab-backup-cli/bin/console11
-rwxr-xr-xgems/gitlab-backup-cli/bin/setup8
-rw-r--r--gems/gitlab-backup-cli/gitlab-backup-cli.gemspec33
-rw-r--r--gems/gitlab-backup-cli/lib/gitlab/backup/cli.rb14
-rw-r--r--gems/gitlab-backup-cli/lib/gitlab/backup/cli/runner.rb34
-rw-r--r--gems/gitlab-backup-cli/lib/gitlab/backup/cli/version.rb9
-rw-r--r--gems/gitlab-backup-cli/sig/gitlab/backup/cli.rbs7
-rw-r--r--gems/gitlab-backup-cli/sig/gitlab/backup/cli/runner.rbs15
-rw-r--r--gems/gitlab-backup-cli/spec/gitlab/backup/cli_spec.rb7
-rw-r--r--gems/gitlab-backup-cli/spec/spec_helper.rb15
-rw-r--r--gems/gitlab-http/Gemfile.lock3
-rw-r--r--gems/gitlab-http/README.md17
-rw-r--r--gems/gitlab-http/gitlab-http.gemspec1
-rw-r--r--gems/gitlab-http/lib/gitlab/http_v2/client.rb74
-rw-r--r--gems/gitlab-http/lib/gitlab/http_v2/lazy_response.rb48
-rw-r--r--gems/gitlab-http/spec/gitlab/http_v2_spec.rb97
-rw-r--r--gems/gitlab-rspec/Gemfile.lock2
-rw-r--r--gems/gitlab-rspec/gitlab-rspec.gemspec2
-rw-r--r--gems/gitlab-utils/Gemfile.lock2
-rw-r--r--gems/gitlab-utils/lib/gitlab/utils/strong_memoize.rb2
-rw-r--r--gems/rspec_flaky/Gemfile.lock2
-rw-r--r--generator_templates/active_record/migration/create_table_migration.rb.tt1
-rw-r--r--generator_templates/active_record/migration/migration.rb.tt1
-rw-r--r--generator_templates/gitlab_internal_events/metric_definition.yml4
-rw-r--r--generator_templates/post_deployment_migration/post_deployment_migration/migration.rb.tt1
-rw-r--r--generator_templates/snowplow_event_definition/event_definition.yml27
-rw-r--r--glfm_specification/input/gitlab_flavored_markdown/glfm_internal_extensions.md228
-rw-r--r--glfm_specification/output_example_snapshots/examples_index.yml24
-rw-r--r--glfm_specification/output_example_snapshots/html.yml275
-rw-r--r--glfm_specification/output_example_snapshots/markdown.yml71
-rw-r--r--glfm_specification/output_example_snapshots/prosemirror_json.yml524
-rw-r--r--glfm_specification/output_example_snapshots/snapshot_spec.html272
-rw-r--r--glfm_specification/output_example_snapshots/snapshot_spec.md228
-rw-r--r--jest.config.base.js10
-rw-r--r--jest.config.contract.js2
-rw-r--r--jest.config.integration.js4
-rw-r--r--jest.config.js6
-rw-r--r--jest.config.scripts.js8
-rw-r--r--lefthook.yml6
-rw-r--r--lib/api/api.rb11
-rw-r--r--lib/api/bulk_imports.rb17
-rw-r--r--lib/api/ci/jobs.rb6
-rw-r--r--lib/api/ci/pipelines.rb29
-rw-r--r--lib/api/ci/runners.rb4
-rw-r--r--lib/api/commit_statuses.rb31
-rw-r--r--lib/api/concerns/packages/npm_endpoints.rb10
-rw-r--r--lib/api/entities/bulk_imports/entity_failure.rb10
-rw-r--r--lib/api/entities/commit_signature.rb17
-rw-r--r--lib/api/entities/group.rb3
-rw-r--r--lib/api/entities/ml/mlflow/model_versions/responses/get.rb17
-rw-r--r--lib/api/entities/ml/mlflow/model_versions/types/model_version.rb85
-rw-r--r--lib/api/entities/ml/mlflow/model_versions/types/model_version_tag.rb18
-rw-r--r--lib/api/entities/ml/mlflow/registered_model.rb18
-rw-r--r--lib/api/entities/wiki_page.rb4
-rw-r--r--lib/api/github/entities.rb219
-rw-r--r--lib/api/group_packages.rb6
-rw-r--r--lib/api/groups.rb1
-rw-r--r--lib/api/helm_packages.rb2
-rw-r--r--lib/api/helpers.rb20
-rw-r--r--lib/api/helpers/groups_helpers.rb3
-rw-r--r--lib/api/helpers/internal_helpers.rb4
-rw-r--r--lib/api/helpers/kubernetes/agent_helpers.rb2
-rw-r--r--lib/api/helpers/packages/npm.rb10
-rw-r--r--lib/api/helpers/pagination_strategies.rb6
-rw-r--r--lib/api/helpers/rate_limiter.rb4
-rw-r--r--lib/api/internal/base.rb25
-rw-r--r--lib/api/internal/shellhorse.rb74
-rw-r--r--lib/api/invitations.rb36
-rw-r--r--lib/api/lint.rb2
-rw-r--r--lib/api/maven_packages.rb4
-rw-r--r--lib/api/members.rb13
-rw-r--r--lib/api/merge_request_approvals.rb4
-rw-r--r--lib/api/ml/mlflow/api_helpers.rb8
-rw-r--r--lib/api/ml/mlflow/entrypoint.rb5
-rw-r--r--lib/api/ml/mlflow/experiments.rb5
-rw-r--r--lib/api/ml/mlflow/model_versions.rb32
-rw-r--r--lib/api/ml/mlflow/registered_models.rb96
-rw-r--r--lib/api/ml/mlflow/runs.rb5
-rw-r--r--lib/api/nuget_project_packages.rb2
-rw-r--r--lib/api/personal_access_tokens.rb8
-rw-r--r--lib/api/project_packages.rb4
-rw-r--r--lib/api/project_repository_storage_moves.rb10
-rw-r--r--lib/api/projects.rb10
-rw-r--r--lib/api/pypi_packages.rb7
-rw-r--r--lib/api/releases.rb23
-rw-r--r--lib/api/resource_access_tokens.rb6
-rw-r--r--lib/api/settings.rb1
-rw-r--r--lib/api/users.rb10
-rw-r--r--lib/api/v3/github.rb289
-rw-r--r--lib/api/vs_code/settings/entities/vs_code_setting_reference.rb23
-rw-r--r--lib/api/vs_code/settings/vs_code_settings_sync.rb87
-rw-r--r--lib/api/wikis.rb6
-rw-r--r--lib/atlassian/jira_connect/jira_user.rb6
-rw-r--r--lib/backup/gitaly_backup.rb2
-rw-r--r--lib/banzai/filter/asset_proxy_filter.rb3
-rw-r--r--lib/banzai/filter/math_filter.rb10
-rw-r--r--lib/banzai/filter/references/user_reference_filter.rb11
-rw-r--r--lib/banzai/reference_parser/user_parser.rb66
-rw-r--r--lib/bitbucket/representation/pull_request_comment.rb4
-rw-r--r--lib/bitbucket/representation/repo.rb4
-rw-r--r--lib/bulk_imports/clients/graphql.rb3
-rw-r--r--lib/bulk_imports/common/pipelines/entity_finisher.rb5
-rw-r--r--lib/bulk_imports/logger.rb11
-rw-r--r--lib/bulk_imports/ndjson_pipeline.rb2
-rw-r--r--lib/bulk_imports/pipeline/runner.rb40
-rw-r--r--lib/bulk_imports/pipeline_schema_info.rb36
-rw-r--r--lib/bulk_imports/projects/pipelines/merge_requests_pipeline.rb4
-rw-r--r--lib/bulk_imports/projects/pipelines/releases_pipeline.rb4
-rw-r--r--lib/bulk_imports/source_url_builder.rb57
-rw-r--r--lib/click_house/migration.rb89
-rw-r--r--lib/click_house/migration_support/migration_context.rb94
-rw-r--r--lib/click_house/migration_support/migration_error.rb54
-rw-r--r--lib/click_house/migration_support/migrator.rb160
-rw-r--r--lib/click_house/migration_support/schema_migration.rb71
-rw-r--r--lib/click_house/models/audit_event.rb55
-rw-r--r--lib/click_house/models/base_model.rb41
-rw-r--r--lib/container_registry/client.rb8
-rw-r--r--lib/container_registry/gitlab_api_client.rb20
-rw-r--r--lib/container_registry/tag.rb17
-rw-r--r--lib/generators/batched_background_migration/templates/queue_batched_background_migration.template2
-rw-r--r--lib/generators/gitlab/partitioning/templates/foreign_key_definition.rb.template2
-rw-r--r--lib/generators/gitlab/partitioning/templates/foreign_key_index.rb.template2
-rw-r--r--lib/generators/gitlab/partitioning/templates/foreign_key_removal.rb.template2
-rw-r--r--lib/generators/gitlab/partitioning/templates/foreign_key_validation.rb.template2
-rw-r--r--lib/generators/gitlab/snowplow_event_definition_generator.rb73
-rw-r--r--lib/generators/gitlab/usage_metric_definition/redis_hll_generator.rb5
-rw-r--r--lib/generators/gitlab/usage_metric_definition_generator.rb12
-rw-r--r--lib/gitlab.rb5
-rw-r--r--lib/gitlab/analytics/cycle_analytics/request_params.rb3
-rw-r--r--lib/gitlab/application_rate_limiter.rb1
-rw-r--r--lib/gitlab/auth.rb24
-rw-r--r--lib/gitlab/auth/saml/config.rb15
-rw-r--r--lib/gitlab/auth/two_factor_auth_verifier.rb2
-rw-r--r--lib/gitlab/background_migration/backfill_packages_tags_project_id.rb30
-rw-r--r--lib/gitlab/background_migration/batched_migration_job.rb2
-rw-r--r--lib/gitlab/background_migration/delete_invalid_protected_branch_merge_access_levels.rb28
-rw-r--r--lib/gitlab/background_migration/delete_invalid_protected_branch_push_access_levels.rb28
-rw-r--r--lib/gitlab/background_migration/delete_invalid_protected_tag_create_access_levels.rb28
-rw-r--r--lib/gitlab/base_doorkeeper_controller.rb4
-rw-r--r--lib/gitlab/bitbucket_import/importers/issue_importer.rb2
-rw-r--r--lib/gitlab/bitbucket_import/importers/issues_importer.rb18
-rw-r--r--lib/gitlab/bitbucket_import/importers/pull_request_importer.rb2
-rw-r--r--lib/gitlab/bitbucket_import/importers/pull_request_notes_importer.rb124
-rw-r--r--lib/gitlab/bitbucket_import/importers/repository_importer.rb11
-rw-r--r--lib/gitlab/bitbucket_import/loggable.rb4
-rw-r--r--lib/gitlab/checks/diff_check.rb3
-rw-r--r--lib/gitlab/checks/file_size_check/any_oversized_blobs.rb2
-rw-r--r--lib/gitlab/ci/ansi2json/line.rb9
-rw-r--r--lib/gitlab/ci/ansi2json/state.rb1
-rw-r--r--lib/gitlab/ci/build/context/build.rb10
-rw-r--r--lib/gitlab/ci/components/instance_path.rb4
-rw-r--r--lib/gitlab/ci/config/entry/job.rb23
-rw-r--r--lib/gitlab/ci/config/entry/pages.rb31
-rw-r--r--lib/gitlab/ci/config/entry/processable.rb2
-rw-r--r--lib/gitlab/ci/config/header/input.rb11
-rw-r--r--lib/gitlab/ci/config/interpolation/inputs/base_input.rb46
-rw-r--r--lib/gitlab/ci/config/interpolation/inputs/boolean_input.rb9
-rw-r--r--lib/gitlab/ci/config/interpolation/inputs/number_input.rb17
-rw-r--r--lib/gitlab/ci/config/interpolation/inputs/string_input.rb28
-rw-r--r--lib/gitlab/ci/jwt_v2.rb30
-rw-r--r--lib/gitlab/ci/parsers/sbom/cyclonedx_properties.rb7
-rw-r--r--lib/gitlab/ci/parsers/sbom/source/base_source.rb46
-rw-r--r--lib/gitlab/ci/parsers/sbom/source/container_scanning.rb41
-rw-r--r--lib/gitlab/ci/parsers/sbom/source/dependency_scanning.rb27
-rw-r--r--lib/gitlab/ci/parsers/security/common.rb2
-rw-r--r--lib/gitlab/ci/parsers/security/validators/schema_validator.rb16
-rw-r--r--lib/gitlab/ci/parsers/security/validators/schemas/15.0.7/cluster-image-scanning-report-format.json1085
-rw-r--r--lib/gitlab/ci/parsers/security/validators/schemas/15.0.7/container-scanning-report-format.json1017
-rw-r--r--lib/gitlab/ci/parsers/security/validators/schemas/15.0.7/coverage-fuzzing-report-format.json975
-rw-r--r--lib/gitlab/ci/parsers/security/validators/schemas/15.0.7/dast-report-format.json1380
-rw-r--r--lib/gitlab/ci/parsers/security/validators/schemas/15.0.7/dependency-scanning-report-format.json1083
-rw-r--r--lib/gitlab/ci/parsers/security/validators/schemas/15.0.7/sast-report-format.json970
-rw-r--r--lib/gitlab/ci/parsers/security/validators/schemas/15.0.7/secret-detection-report-format.json994
-rw-r--r--lib/gitlab/ci/status/build/cancelable.rb2
-rw-r--r--lib/gitlab/ci/status/composite.rb2
-rw-r--r--lib/gitlab/ci/status/waiting_for_callback.rb33
-rw-r--r--lib/gitlab/ci/templates/Cosign.gitlab-ci.yml4
-rw-r--r--lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Jobs/Build.latest.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Kaniko.gitlab-ci.yml23
-rw-r--r--lib/gitlab/ci/templates/Terraform/Base.latest.gitlab-ci.yml6
-rw-r--r--lib/gitlab/ci/templates/Verify/Browser-Performance.gitlab-ci.yml3
-rw-r--r--lib/gitlab/ci/templates/Verify/Browser-Performance.latest.gitlab-ci.yml3
-rw-r--r--lib/gitlab/ci/yaml_processor/dag.rb6
-rw-r--r--lib/gitlab/ci/yaml_processor/result.rb6
-rw-r--r--lib/gitlab/composer/version_index.rb3
-rw-r--r--lib/gitlab/config/entry/validators.rb3
-rw-r--r--lib/gitlab/config_checker/puma_rugged_checker.rb28
-rw-r--r--lib/gitlab/database/dictionary.rb60
-rw-r--r--lib/gitlab/database/dynamic_model_helpers.rb3
-rw-r--r--lib/gitlab/database/gitlab_schema.rb27
-rw-r--r--lib/gitlab/database/migration.rb6
-rw-r--r--lib/gitlab/database/migration_helpers.rb6
-rw-r--r--lib/gitlab/database/migration_helpers/convert_to_bigint.rb88
-rw-r--r--lib/gitlab/database/migration_helpers/wraparound_autovacuum.rb2
-rw-r--r--lib/gitlab/database/migrations/batched_background_migration_helpers.rb2
-rw-r--r--lib/gitlab/database/migrations/milestone_mixin.rb5
-rw-r--r--lib/gitlab/database/partitioning/partition_manager.rb19
-rw-r--r--lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb117
-rw-r--r--lib/gitlab/database/query_analyzers/ci/partitioning_routing_analyzer.rb2
-rw-r--r--lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch.rb47
-rw-r--r--lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/columns.rb91
-rw-r--r--lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/common_table_expressions.rb74
-rw-r--r--lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/froms.rb68
-rw-r--r--lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/node.rb118
-rw-r--r--lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/references.rb64
-rw-r--r--lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/select_stmt.rb81
-rw-r--r--lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/targets.rb97
-rw-r--r--lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/type.rb16
-rw-r--r--lib/gitlab/database/schema_cache_with_renamed_table.rb22
-rw-r--r--lib/gitlab/database/schema_cache_with_renamed_table_legacy.rb55
-rw-r--r--lib/gitlab/database/tables_locker.rb2
-rw-r--r--lib/gitlab/database/tables_truncate.rb2
-rw-r--r--lib/gitlab/discussions_diff/file_collection.rb14
-rw-r--r--lib/gitlab/email/handler/service_desk_handler.rb30
-rw-r--r--lib/gitlab/emoji.rb2
-rw-r--r--lib/gitlab/encrypted_command_base.rb14
-rw-r--r--lib/gitlab/encrypted_configuration.rb2
-rw-r--r--lib/gitlab/encrypted_ldap_command.rb2
-rw-r--r--lib/gitlab/encrypted_redis_command.rb56
-rw-r--r--lib/gitlab/file_detector.rb2
-rw-r--r--lib/gitlab/git/blame.rb7
-rw-r--r--lib/gitlab/git/blob.rb2
-rw-r--r--lib/gitlab/git/commit.rb3
-rw-r--r--lib/gitlab/git/ref.rb1
-rw-r--r--lib/gitlab/git/repository.rb1
-rw-r--r--lib/gitlab/git/rugged_impl/blob.rb107
-rw-r--r--lib/gitlab/git/rugged_impl/commit.rb115
-rw-r--r--lib/gitlab/git/rugged_impl/ref.rb20
-rw-r--r--lib/gitlab/git/rugged_impl/repository.rb79
-rw-r--r--lib/gitlab/git/rugged_impl/tree.rb147
-rw-r--r--lib/gitlab/git/rugged_impl/use_rugged.rb50
-rw-r--r--lib/gitlab/git/tree.rb5
-rw-r--r--lib/gitlab/git_access.rb4
-rw-r--r--lib/gitlab/git_access_project.rb2
-rw-r--r--lib/gitlab/git_audit_event.rb28
-rw-r--r--lib/gitlab/gitaly_client.rb14
-rw-r--r--lib/gitlab/gitaly_client/commit_service.rb9
-rw-r--r--lib/gitlab/gitaly_client/conflict_files_stitcher.rb10
-rw-r--r--lib/gitlab/gitaly_client/repository_service.rb7
-rw-r--r--lib/gitlab/gitaly_client/storage_settings.rb8
-rw-r--r--lib/gitlab/github_import/attachments_downloader.rb24
-rw-r--r--lib/gitlab/github_import/client.rb10
-rw-r--r--lib/gitlab/github_import/issuable_finder.rb14
-rw-r--r--lib/gitlab/github_import/job_delay_calculator.rb2
-rw-r--r--lib/gitlab/github_import/label_finder.rb16
-rw-r--r--lib/gitlab/github_import/milestone_finder.rb18
-rw-r--r--lib/gitlab/github_import/object_counter.rb2
-rw-r--r--lib/gitlab/github_import/parallel_scheduling.rb18
-rw-r--r--lib/gitlab/github_import/representation/to_hash.rb4
-rw-r--r--lib/gitlab/gon_helper.rb3
-rw-r--r--lib/gitlab/graphql/tracers/timer_tracer.rb4
-rw-r--r--lib/gitlab/group_search_results.rb2
-rw-r--r--lib/gitlab/identifier.rb9
-rw-r--r--lib/gitlab/import_export/command_line_util.rb21
-rw-r--r--lib/gitlab/import_export/error.rb4
-rw-r--r--lib/gitlab/import_export/project/sample/date_calculator.rb2
-rw-r--r--lib/gitlab/instrumentation/redis_base.rb6
-rw-r--r--lib/gitlab/instrumentation/redis_interceptor.rb4
-rw-r--r--lib/gitlab/instrumentation_helper.rb10
-rw-r--r--lib/gitlab/internal_events.rb2
-rw-r--r--lib/gitlab/issues/rebalancing/state.rb2
-rw-r--r--lib/gitlab/jira/http_client.rb15
-rw-r--r--lib/gitlab/jira/middleware.rb23
-rw-r--r--lib/gitlab/jira_import/base_importer.rb4
-rw-r--r--lib/gitlab/jira_import/issues_importer.rb2
-rw-r--r--lib/gitlab/job_waiter.rb29
-rw-r--r--lib/gitlab/kubernetes/kubeconfig/template.rb2
-rw-r--r--lib/gitlab/legacy_http.rb4
-rw-r--r--lib/gitlab/memory/reporter.rb8
-rw-r--r--lib/gitlab/memory/reports_uploader.rb4
-rw-r--r--lib/gitlab/merge_requests/mergeability/check_result.rb5
-rw-r--r--lib/gitlab/metrics/exporter/metrics_middleware.rb8
-rw-r--r--lib/gitlab/middleware/go.rb2
-rw-r--r--lib/gitlab/middleware/path_traversal_check.rb45
-rw-r--r--lib/gitlab/omniauth_initializer.rb11
-rw-r--r--lib/gitlab/optimistic_locking.rb4
-rw-r--r--lib/gitlab/pages/deployment_update.rb15
-rw-r--r--lib/gitlab/pagination/cursor_based_keyset.rb6
-rw-r--r--lib/gitlab/patch/sidekiq_cron_poller.rb2
-rw-r--r--lib/gitlab/patch/sidekiq_scheduled_enq.rb7
-rw-r--r--lib/gitlab/project_template.rb7
-rw-r--r--lib/gitlab/push_options.rb1
-rw-r--r--lib/gitlab/quick_actions/merge_request_actions.rb23
-rw-r--r--lib/gitlab/redis.rb1
-rw-r--r--lib/gitlab/redis/cluster_util.rb9
-rw-r--r--lib/gitlab/redis/multi_store.rb47
-rw-r--r--lib/gitlab/redis/pubsub.rb13
-rw-r--r--lib/gitlab/redis/shared_state.rb6
-rw-r--r--lib/gitlab/redis/wrapper.rb40
-rw-r--r--lib/gitlab/regex.rb1
-rw-r--r--lib/gitlab/regex/packages.rb8
-rw-r--r--lib/gitlab/regex/packages/protection/rules.rb15
-rw-r--r--lib/gitlab/request_forgery_protection.rb4
-rw-r--r--lib/gitlab/rugged_instrumentation.rb45
-rw-r--r--lib/gitlab/search_results.rb8
-rw-r--r--lib/gitlab/seeders/ci/catalog/resource_seeder.rb114
-rw-r--r--lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb11
-rw-r--r--lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies/deduplicates_when_scheduling.rb2
-rw-r--r--lib/gitlab/sidekiq_middleware/server_metrics.rb100
-rw-r--r--lib/gitlab/sidekiq_middleware/skip_jobs.rb13
-rw-r--r--lib/gitlab/sidekiq_status.rb13
-rw-r--r--lib/gitlab/tracking.rb3
-rw-r--r--lib/gitlab/url_builder.rb2
-rw-r--r--lib/gitlab/usage/metric_definition.rb23
-rw-r--r--lib/gitlab/usage_data.rb1
-rw-r--r--lib/gitlab/usage_data_counters/hll_redis_counter.rb5
-rw-r--r--lib/gitlab/usage_data_counters/redis_counter.rb7
-rw-r--r--lib/gitlab/workhorse.rb3
-rw-r--r--lib/peek/views/rugged.rb46
-rw-r--r--lib/sbom/purl_type/converter.rb1
-rw-r--r--lib/sidebars/admin/menus/admin_settings_menu.rb10
-rw-r--r--lib/sidebars/explore/menus/catalog_menu.rb34
-rw-r--r--lib/sidebars/explore/panel.rb1
-rw-r--r--lib/sidebars/menu.rb1
-rw-r--r--lib/sidebars/organizations/menus/manage_menu.rb9
-rw-r--r--lib/sidebars/projects/menus/ci_cd_menu.rb2
-rw-r--r--lib/sidebars/projects/menus/infrastructure_menu.rb2
-rw-r--r--lib/sidebars/projects/menus/scope_menu.rb2
-rw-r--r--lib/sidebars/projects/menus/settings_menu.rb4
-rw-r--r--lib/sidebars/projects/super_sidebar_menus/monitor_menu.rb1
-rw-r--r--lib/sidebars/user_settings/menus/comment_templates_menu.rb2
-rw-r--r--lib/tasks/gitlab/bulk_add_permission.rake2
-rw-r--r--lib/tasks/gitlab/click_house/migration.rake60
-rw-r--r--lib/tasks/gitlab/db.rake12
-rw-r--r--lib/tasks/gitlab/features.rake34
-rw-r--r--lib/tasks/gitlab/redis.rake23
-rw-r--r--lib/tasks/gitlab/seed/ci_catalog_resources.rake26
-rw-r--r--lib/tasks/gitlab/tw/codeowners.rake41
-rw-r--r--lib/tasks/tanuki_emoji.rake9
-rw-r--r--lib/unnested_in_filters/rewriter.rb3
-rw-r--r--lib/vs_code/settings.rb2
-rw-r--r--locale/gitlab.pot1498
-rw-r--r--metrics_server/metrics_server.rb2
-rw-r--r--package.json115
-rw-r--r--patches/leaflet+1.9.4.patch28
-rw-r--r--public/-/emojis/3/100.pngbin0 -> 3429 bytes
-rw-r--r--public/-/emojis/3/1234.pngbin0 -> 3097 bytes
-rw-r--r--public/-/emojis/3/8ball.pngbin0 -> 3702 bytes
-rw-r--r--public/-/emojis/3/a.pngbin0 -> 2804 bytes
-rw-r--r--public/-/emojis/3/ab.pngbin0 -> 3080 bytes
-rw-r--r--public/-/emojis/3/abc.pngbin0 -> 2999 bytes
-rw-r--r--public/-/emojis/3/abcd.pngbin0 -> 3352 bytes
-rw-r--r--public/-/emojis/3/accept.pngbin0 -> 3162 bytes
-rw-r--r--public/-/emojis/3/aerial_tramway.pngbin0 -> 3297 bytes
-rw-r--r--public/-/emojis/3/airplane.pngbin0 -> 5401 bytes
-rw-r--r--public/-/emojis/3/airplane_arriving.pngbin0 -> 4300 bytes
-rw-r--r--public/-/emojis/3/airplane_departure.pngbin0 -> 3980 bytes
-rw-r--r--public/-/emojis/3/airplane_small.pngbin0 -> 5373 bytes
-rw-r--r--public/-/emojis/3/alarm_clock.pngbin0 -> 6273 bytes
-rw-r--r--public/-/emojis/3/alembic.pngbin0 -> 4544 bytes
-rw-r--r--public/-/emojis/3/alien.pngbin0 -> 3191 bytes
-rw-r--r--public/-/emojis/3/ambulance.pngbin0 -> 3864 bytes
-rw-r--r--public/-/emojis/3/amphora.pngbin0 -> 4125 bytes
-rw-r--r--public/-/emojis/3/anchor.pngbin0 -> 3166 bytes
-rw-r--r--public/-/emojis/3/angel.pngbin0 -> 5329 bytes
-rw-r--r--public/-/emojis/3/angel_tone1.pngbin0 -> 5332 bytes
-rw-r--r--public/-/emojis/3/angel_tone2.pngbin0 -> 5196 bytes
-rw-r--r--public/-/emojis/3/angel_tone3.pngbin0 -> 5233 bytes
-rw-r--r--public/-/emojis/3/angel_tone4.pngbin0 -> 5068 bytes
-rw-r--r--public/-/emojis/3/angel_tone5.pngbin0 -> 5094 bytes
-rw-r--r--public/-/emojis/3/anger.pngbin0 -> 2791 bytes
-rw-r--r--public/-/emojis/3/anger_right.pngbin0 -> 4527 bytes
-rw-r--r--public/-/emojis/3/angry.pngbin0 -> 4815 bytes
-rw-r--r--public/-/emojis/3/anguished.pngbin0 -> 4897 bytes
-rw-r--r--public/-/emojis/3/ant.pngbin0 -> 4108 bytes
-rw-r--r--public/-/emojis/3/apple.pngbin0 -> 3432 bytes
-rw-r--r--public/-/emojis/3/aquarius.pngbin0 -> 5602 bytes
-rw-r--r--public/-/emojis/3/aries.pngbin0 -> 4474 bytes
-rw-r--r--public/-/emojis/3/arrow_backward.pngbin0 -> 1679 bytes
-rw-r--r--public/-/emojis/3/arrow_double_down.pngbin0 -> 2145 bytes
-rw-r--r--public/-/emojis/3/arrow_double_up.pngbin0 -> 2127 bytes
-rw-r--r--public/-/emojis/3/arrow_down.pngbin0 -> 1713 bytes
-rw-r--r--public/-/emojis/3/arrow_down_small.pngbin0 -> 1714 bytes
-rw-r--r--public/-/emojis/3/arrow_forward.pngbin0 -> 1656 bytes
-rw-r--r--public/-/emojis/3/arrow_heading_down.pngbin0 -> 2249 bytes
-rw-r--r--public/-/emojis/3/arrow_heading_up.pngbin0 -> 2328 bytes
-rw-r--r--public/-/emojis/3/arrow_left.pngbin0 -> 1690 bytes
-rw-r--r--public/-/emojis/3/arrow_lower_left.pngbin0 -> 1832 bytes
-rw-r--r--public/-/emojis/3/arrow_lower_right.pngbin0 -> 1803 bytes
-rw-r--r--public/-/emojis/3/arrow_right.pngbin0 -> 1680 bytes
-rw-r--r--public/-/emojis/3/arrow_right_hook.pngbin0 -> 2284 bytes
-rw-r--r--public/-/emojis/3/arrow_up.pngbin0 -> 1666 bytes
-rw-r--r--public/-/emojis/3/arrow_up_down.pngbin0 -> 2040 bytes
-rw-r--r--public/-/emojis/3/arrow_up_small.pngbin0 -> 1658 bytes
-rw-r--r--public/-/emojis/3/arrow_upper_left.pngbin0 -> 1784 bytes
-rw-r--r--public/-/emojis/3/arrow_upper_right.pngbin0 -> 1800 bytes
-rw-r--r--public/-/emojis/3/arrows_clockwise.pngbin0 -> 2736 bytes
-rw-r--r--public/-/emojis/3/arrows_counterclockwise.pngbin0 -> 2710 bytes
-rw-r--r--public/-/emojis/3/art.pngbin0 -> 4958 bytes
-rw-r--r--public/-/emojis/3/articulated_lorry.pngbin0 -> 3583 bytes
-rw-r--r--public/-/emojis/3/asterisk.pngbin0 -> 2668 bytes
-rw-r--r--public/-/emojis/3/astonished.pngbin0 -> 4894 bytes
-rw-r--r--public/-/emojis/3/athletic_shoe.pngbin0 -> 4116 bytes
-rw-r--r--public/-/emojis/3/atm.pngbin0 -> 3091 bytes
-rw-r--r--public/-/emojis/3/atom.pngbin0 -> 4941 bytes
-rw-r--r--public/-/emojis/3/avocado.pngbin0 -> 4585 bytes
-rw-r--r--public/-/emojis/3/b.pngbin0 -> 2390 bytes
-rw-r--r--public/-/emojis/3/baby.pngbin0 -> 3211 bytes
-rw-r--r--public/-/emojis/3/baby_bottle.pngbin0 -> 3677 bytes
-rw-r--r--public/-/emojis/3/baby_chick.pngbin0 -> 2407 bytes
-rw-r--r--public/-/emojis/3/baby_symbol.pngbin0 -> 3474 bytes
-rw-r--r--public/-/emojis/3/baby_tone1.pngbin0 -> 3341 bytes
-rw-r--r--public/-/emojis/3/baby_tone2.pngbin0 -> 3168 bytes
-rw-r--r--public/-/emojis/3/baby_tone3.pngbin0 -> 3107 bytes
-rw-r--r--public/-/emojis/3/baby_tone4.pngbin0 -> 3043 bytes
-rw-r--r--public/-/emojis/3/baby_tone5.pngbin0 -> 3127 bytes
-rw-r--r--public/-/emojis/3/back.pngbin0 -> 3529 bytes
-rw-r--r--public/-/emojis/3/bacon.pngbin0 -> 3805 bytes
-rw-r--r--public/-/emojis/3/badminton.pngbin0 -> 4891 bytes
-rw-r--r--public/-/emojis/3/baggage_claim.pngbin0 -> 2937 bytes
-rw-r--r--public/-/emojis/3/balloon.pngbin0 -> 1977 bytes
-rw-r--r--public/-/emojis/3/ballot_box.pngbin0 -> 3484 bytes
-rw-r--r--public/-/emojis/3/ballot_box_with_check.pngbin0 -> 2762 bytes
-rw-r--r--public/-/emojis/3/bamboo.pngbin0 -> 3829 bytes
-rw-r--r--public/-/emojis/3/banana.pngbin0 -> 3384 bytes
-rw-r--r--public/-/emojis/3/bangbang.pngbin0 -> 2312 bytes
-rw-r--r--public/-/emojis/3/bank.pngbin0 -> 4455 bytes
-rw-r--r--public/-/emojis/3/bar_chart.pngbin0 -> 2152 bytes
-rw-r--r--public/-/emojis/3/barber.pngbin0 -> 3154 bytes
-rw-r--r--public/-/emojis/3/baseball.pngbin0 -> 5149 bytes
-rw-r--r--public/-/emojis/3/basketball.pngbin0 -> 5158 bytes
-rw-r--r--public/-/emojis/3/basketball_player.pngbin0 -> 4215 bytes
-rw-r--r--public/-/emojis/3/basketball_player_tone1.pngbin0 -> 4273 bytes
-rw-r--r--public/-/emojis/3/basketball_player_tone2.pngbin0 -> 4256 bytes
-rw-r--r--public/-/emojis/3/basketball_player_tone3.pngbin0 -> 4236 bytes
-rw-r--r--public/-/emojis/3/basketball_player_tone4.pngbin0 -> 4274 bytes
-rw-r--r--public/-/emojis/3/basketball_player_tone5.pngbin0 -> 4291 bytes
-rw-r--r--public/-/emojis/3/bat.pngbin0 -> 3647 bytes
-rw-r--r--public/-/emojis/3/bath.pngbin0 -> 5134 bytes
-rw-r--r--public/-/emojis/3/bath_tone1.pngbin0 -> 5181 bytes
-rw-r--r--public/-/emojis/3/bath_tone2.pngbin0 -> 5127 bytes
-rw-r--r--public/-/emojis/3/bath_tone3.pngbin0 -> 5123 bytes
-rw-r--r--public/-/emojis/3/bath_tone4.pngbin0 -> 5123 bytes
-rw-r--r--public/-/emojis/3/bath_tone5.pngbin0 -> 5141 bytes
-rw-r--r--public/-/emojis/3/bathtub.pngbin0 -> 4064 bytes
-rw-r--r--public/-/emojis/3/battery.pngbin0 -> 2468 bytes
-rw-r--r--public/-/emojis/3/beach.pngbin0 -> 4471 bytes
-rw-r--r--public/-/emojis/3/beach_umbrella.pngbin0 -> 3787 bytes
-rw-r--r--public/-/emojis/3/bear.pngbin0 -> 3495 bytes
-rw-r--r--public/-/emojis/3/bed.pngbin0 -> 1635 bytes
-rw-r--r--public/-/emojis/3/bee.pngbin0 -> 4830 bytes
-rw-r--r--public/-/emojis/3/beer.pngbin0 -> 4702 bytes
-rw-r--r--public/-/emojis/3/beers.pngbin0 -> 6052 bytes
-rw-r--r--public/-/emojis/3/beetle.pngbin0 -> 5033 bytes
-rw-r--r--public/-/emojis/3/beginner.pngbin0 -> 2151 bytes
-rw-r--r--public/-/emojis/3/bell.pngbin0 -> 3524 bytes
-rw-r--r--public/-/emojis/3/bellhop.pngbin0 -> 3234 bytes
-rw-r--r--public/-/emojis/3/bento.pngbin0 -> 4736 bytes
-rw-r--r--public/-/emojis/3/bicyclist.pngbin0 -> 5822 bytes
-rw-r--r--public/-/emojis/3/bicyclist_tone1.pngbin0 -> 5879 bytes
-rw-r--r--public/-/emojis/3/bicyclist_tone2.pngbin0 -> 5865 bytes
-rw-r--r--public/-/emojis/3/bicyclist_tone3.pngbin0 -> 5852 bytes
-rw-r--r--public/-/emojis/3/bicyclist_tone4.pngbin0 -> 5872 bytes
-rw-r--r--public/-/emojis/3/bicyclist_tone5.pngbin0 -> 5889 bytes
-rw-r--r--public/-/emojis/3/bike.pngbin0 -> 5493 bytes
-rw-r--r--public/-/emojis/3/bikini.pngbin0 -> 4236 bytes
-rw-r--r--public/-/emojis/3/biohazard.pngbin0 -> 5855 bytes
-rw-r--r--public/-/emojis/3/bird.pngbin0 -> 2500 bytes
-rw-r--r--public/-/emojis/3/birthday.pngbin0 -> 5512 bytes
-rw-r--r--public/-/emojis/3/black_circle.pngbin0 -> 2741 bytes
-rw-r--r--public/-/emojis/3/black_heart.pngbin0 -> 2912 bytes
-rw-r--r--public/-/emojis/3/black_joker.pngbin0 -> 3545 bytes
-rw-r--r--public/-/emojis/3/black_large_square.pngbin0 -> 982 bytes
-rw-r--r--public/-/emojis/3/black_medium_small_square.pngbin0 -> 674 bytes
-rw-r--r--public/-/emojis/3/black_medium_square.pngbin0 -> 734 bytes
-rw-r--r--public/-/emojis/3/black_nib.pngbin0 -> 4179 bytes
-rw-r--r--public/-/emojis/3/black_small_square.pngbin0 -> 541 bytes
-rw-r--r--public/-/emojis/3/black_square_button.pngbin0 -> 1088 bytes
-rw-r--r--public/-/emojis/3/blossom.pngbin0 -> 5324 bytes
-rw-r--r--public/-/emojis/3/blowfish.pngbin0 -> 5323 bytes
-rw-r--r--public/-/emojis/3/blue_book.pngbin0 -> 1104 bytes
-rw-r--r--public/-/emojis/3/blue_car.pngbin0 -> 3663 bytes
-rw-r--r--public/-/emojis/3/blue_heart.pngbin0 -> 2994 bytes
-rw-r--r--public/-/emojis/3/blush.pngbin0 -> 5028 bytes
-rw-r--r--public/-/emojis/3/boar.pngbin0 -> 4472 bytes
-rw-r--r--public/-/emojis/3/bomb.pngbin0 -> 3413 bytes
-rw-r--r--public/-/emojis/3/book.pngbin0 -> 5014 bytes
-rw-r--r--public/-/emojis/3/bookmark.pngbin0 -> 2843 bytes
-rw-r--r--public/-/emojis/3/bookmark_tabs.pngbin0 -> 2553 bytes
-rw-r--r--public/-/emojis/3/books.pngbin0 -> 6804 bytes
-rw-r--r--public/-/emojis/3/boom.pngbin0 -> 6368 bytes
-rw-r--r--public/-/emojis/3/boot.pngbin0 -> 2652 bytes
-rw-r--r--public/-/emojis/3/bouquet.pngbin0 -> 5542 bytes
-rw-r--r--public/-/emojis/3/bow.pngbin0 -> 4683 bytes
-rw-r--r--public/-/emojis/3/bow_and_arrow.pngbin0 -> 3853 bytes
-rw-r--r--public/-/emojis/3/bow_tone1.pngbin0 -> 4868 bytes
-rw-r--r--public/-/emojis/3/bow_tone2.pngbin0 -> 4758 bytes
-rw-r--r--public/-/emojis/3/bow_tone3.pngbin0 -> 4660 bytes
-rw-r--r--public/-/emojis/3/bow_tone4.pngbin0 -> 4591 bytes
-rw-r--r--public/-/emojis/3/bow_tone5.pngbin0 -> 4676 bytes
-rw-r--r--public/-/emojis/3/bowling.pngbin0 -> 4860 bytes
-rw-r--r--public/-/emojis/3/boxing_glove.pngbin0 -> 2061 bytes
-rw-r--r--public/-/emojis/3/boy.pngbin0 -> 4356 bytes
-rw-r--r--public/-/emojis/3/boy_tone1.pngbin0 -> 4398 bytes
-rw-r--r--public/-/emojis/3/boy_tone2.pngbin0 -> 4319 bytes
-rw-r--r--public/-/emojis/3/boy_tone3.pngbin0 -> 4210 bytes
-rw-r--r--public/-/emojis/3/boy_tone4.pngbin0 -> 4045 bytes
-rw-r--r--public/-/emojis/3/boy_tone5.pngbin0 -> 4077 bytes
-rw-r--r--public/-/emojis/3/bread.pngbin0 -> 3361 bytes
-rw-r--r--public/-/emojis/3/bride_with_veil.pngbin0 -> 6663 bytes
-rw-r--r--public/-/emojis/3/bride_with_veil_tone1.pngbin0 -> 6669 bytes
-rw-r--r--public/-/emojis/3/bride_with_veil_tone2.pngbin0 -> 6360 bytes
-rw-r--r--public/-/emojis/3/bride_with_veil_tone3.pngbin0 -> 6513 bytes
-rw-r--r--public/-/emojis/3/bride_with_veil_tone4.pngbin0 -> 6409 bytes
-rw-r--r--public/-/emojis/3/bride_with_veil_tone5.pngbin0 -> 6437 bytes
-rw-r--r--public/-/emojis/3/bridge_at_night.pngbin0 -> 6752 bytes
-rw-r--r--public/-/emojis/3/briefcase.pngbin0 -> 2680 bytes
-rw-r--r--public/-/emojis/3/broken_heart.pngbin0 -> 3597 bytes
-rw-r--r--public/-/emojis/3/bug.pngbin0 -> 4362 bytes
-rw-r--r--public/-/emojis/3/bulb.pngbin0 -> 3532 bytes
-rw-r--r--public/-/emojis/3/bullettrain_front.pngbin0 -> 3061 bytes
-rw-r--r--public/-/emojis/3/bullettrain_side.pngbin0 -> 3117 bytes
-rw-r--r--public/-/emojis/3/burrito.pngbin0 -> 5742 bytes
-rw-r--r--public/-/emojis/3/bus.pngbin0 -> 3327 bytes
-rw-r--r--public/-/emojis/3/busstop.pngbin0 -> 3629 bytes
-rw-r--r--public/-/emojis/3/bust_in_silhouette.pngbin0 -> 1339 bytes
-rw-r--r--public/-/emojis/3/busts_in_silhouette.pngbin0 -> 1757 bytes
-rw-r--r--public/-/emojis/3/butterfly.pngbin0 -> 6362 bytes
-rw-r--r--public/-/emojis/3/cactus.pngbin0 -> 4517 bytes
-rw-r--r--public/-/emojis/3/cake.pngbin0 -> 4393 bytes
-rw-r--r--public/-/emojis/3/calendar.pngbin0 -> 3694 bytes
-rw-r--r--public/-/emojis/3/calendar_spiral.pngbin0 -> 5551 bytes
-rw-r--r--public/-/emojis/3/call_me.pngbin0 -> 3666 bytes
-rw-r--r--public/-/emojis/3/call_me_tone1.pngbin0 -> 3936 bytes
-rw-r--r--public/-/emojis/3/call_me_tone2.pngbin0 -> 4040 bytes
-rw-r--r--public/-/emojis/3/call_me_tone3.pngbin0 -> 3973 bytes
-rw-r--r--public/-/emojis/3/call_me_tone4.pngbin0 -> 3546 bytes
-rw-r--r--public/-/emojis/3/call_me_tone5.pngbin0 -> 3808 bytes
-rw-r--r--public/-/emojis/3/calling.pngbin0 -> 3136 bytes
-rw-r--r--public/-/emojis/3/camel.pngbin0 -> 3951 bytes
-rw-r--r--public/-/emojis/3/camera.pngbin0 -> 4170 bytes
-rw-r--r--public/-/emojis/3/camera_with_flash.pngbin0 -> 5079 bytes
-rw-r--r--public/-/emojis/3/camping.pngbin0 -> 4810 bytes
-rw-r--r--public/-/emojis/3/cancer.pngbin0 -> 5088 bytes
-rw-r--r--public/-/emojis/3/candle.pngbin0 -> 2797 bytes
-rw-r--r--public/-/emojis/3/candy.pngbin0 -> 3955 bytes
-rw-r--r--public/-/emojis/3/canoe.pngbin0 -> 2689 bytes
-rw-r--r--public/-/emojis/3/capital_abcd.pngbin0 -> 3330 bytes
-rw-r--r--public/-/emojis/3/capricorn.pngbin0 -> 4493 bytes
-rw-r--r--public/-/emojis/3/card_box.pngbin0 -> 1948 bytes
-rw-r--r--public/-/emojis/3/card_index.pngbin0 -> 3953 bytes
-rw-r--r--public/-/emojis/3/carousel_horse.pngbin0 -> 5794 bytes
-rw-r--r--public/-/emojis/3/carrot.pngbin0 -> 2795 bytes
-rw-r--r--public/-/emojis/3/cartwheel.pngbin0 -> 2970 bytes
-rw-r--r--public/-/emojis/3/cartwheel_tone1.pngbin0 -> 3043 bytes
-rw-r--r--public/-/emojis/3/cartwheel_tone2.pngbin0 -> 2979 bytes
-rw-r--r--public/-/emojis/3/cartwheel_tone3.pngbin0 -> 2968 bytes
-rw-r--r--public/-/emojis/3/cartwheel_tone4.pngbin0 -> 3002 bytes
-rw-r--r--public/-/emojis/3/cartwheel_tone5.pngbin0 -> 3012 bytes
-rw-r--r--public/-/emojis/3/cat.pngbin0 -> 4175 bytes
-rw-r--r--public/-/emojis/3/cat2.pngbin0 -> 3261 bytes
-rw-r--r--public/-/emojis/3/cd.pngbin0 -> 5270 bytes
-rw-r--r--public/-/emojis/3/chains.pngbin0 -> 4271 bytes
-rw-r--r--public/-/emojis/3/champagne.pngbin0 -> 3479 bytes
-rw-r--r--public/-/emojis/3/champagne_glass.pngbin0 -> 5376 bytes
-rw-r--r--public/-/emojis/3/chart.pngbin0 -> 4004 bytes
-rw-r--r--public/-/emojis/3/chart_with_downwards_trend.pngbin0 -> 4033 bytes
-rw-r--r--public/-/emojis/3/chart_with_upwards_trend.pngbin0 -> 4222 bytes
-rw-r--r--public/-/emojis/3/checkered_flag.pngbin0 -> 5404 bytes
-rw-r--r--public/-/emojis/3/cheese.pngbin0 -> 3157 bytes
-rw-r--r--public/-/emojis/3/cherries.pngbin0 -> 3910 bytes
-rw-r--r--public/-/emojis/3/cherry_blossom.pngbin0 -> 3387 bytes
-rw-r--r--public/-/emojis/3/chestnut.pngbin0 -> 4084 bytes
-rw-r--r--public/-/emojis/3/chicken.pngbin0 -> 3936 bytes
-rw-r--r--public/-/emojis/3/children_crossing.pngbin0 -> 3964 bytes
-rw-r--r--public/-/emojis/3/chipmunk.pngbin0 -> 4676 bytes
-rw-r--r--public/-/emojis/3/chocolate_bar.pngbin0 -> 4559 bytes
-rw-r--r--public/-/emojis/3/christmas_tree.pngbin0 -> 5452 bytes
-rw-r--r--public/-/emojis/3/church.pngbin0 -> 3873 bytes
-rw-r--r--public/-/emojis/3/cinema.pngbin0 -> 2555 bytes
-rw-r--r--public/-/emojis/3/circus_tent.pngbin0 -> 5116 bytes
-rw-r--r--public/-/emojis/3/city_dusk.pngbin0 -> 5760 bytes
-rw-r--r--public/-/emojis/3/city_sunset.pngbin0 -> 6407 bytes
-rw-r--r--public/-/emojis/3/cityscape.pngbin0 -> 5902 bytes
-rw-r--r--public/-/emojis/3/cl.pngbin0 -> 2540 bytes
-rw-r--r--public/-/emojis/3/clap.pngbin0 -> 4650 bytes
-rw-r--r--public/-/emojis/3/clap_tone1.pngbin0 -> 5025 bytes
-rw-r--r--public/-/emojis/3/clap_tone2.pngbin0 -> 4966 bytes
-rw-r--r--public/-/emojis/3/clap_tone3.pngbin0 -> 5007 bytes
-rw-r--r--public/-/emojis/3/clap_tone4.pngbin0 -> 4635 bytes
-rw-r--r--public/-/emojis/3/clap_tone5.pngbin0 -> 4828 bytes
-rw-r--r--public/-/emojis/3/clapper.pngbin0 -> 3826 bytes
-rw-r--r--public/-/emojis/3/classical_building.pngbin0 -> 3736 bytes
-rw-r--r--public/-/emojis/3/clipboard.pngbin0 -> 1924 bytes
-rw-r--r--public/-/emojis/3/clock.pngbin0 -> 4974 bytes
-rw-r--r--public/-/emojis/3/clock1.pngbin0 -> 4232 bytes
-rw-r--r--public/-/emojis/3/clock10.pngbin0 -> 4296 bytes
-rw-r--r--public/-/emojis/3/clock1030.pngbin0 -> 4284 bytes
-rw-r--r--public/-/emojis/3/clock11.pngbin0 -> 4176 bytes
-rw-r--r--public/-/emojis/3/clock1130.pngbin0 -> 4210 bytes
-rw-r--r--public/-/emojis/3/clock12.pngbin0 -> 4045 bytes
-rw-r--r--public/-/emojis/3/clock1230.pngbin0 -> 4225 bytes
-rw-r--r--public/-/emojis/3/clock130.pngbin0 -> 4250 bytes
-rw-r--r--public/-/emojis/3/clock2.pngbin0 -> 4238 bytes
-rw-r--r--public/-/emojis/3/clock230.pngbin0 -> 4203 bytes
-rw-r--r--public/-/emojis/3/clock3.pngbin0 -> 4058 bytes
-rw-r--r--public/-/emojis/3/clock330.pngbin0 -> 4248 bytes
-rw-r--r--public/-/emojis/3/clock4.pngbin0 -> 4299 bytes
-rw-r--r--public/-/emojis/3/clock430.pngbin0 -> 4257 bytes
-rw-r--r--public/-/emojis/3/clock5.pngbin0 -> 4205 bytes
-rw-r--r--public/-/emojis/3/clock530.pngbin0 -> 4176 bytes
-rw-r--r--public/-/emojis/3/clock6.pngbin0 -> 4078 bytes
-rw-r--r--public/-/emojis/3/clock630.pngbin0 -> 4182 bytes
-rw-r--r--public/-/emojis/3/clock7.pngbin0 -> 4197 bytes
-rw-r--r--public/-/emojis/3/clock730.pngbin0 -> 4235 bytes
-rw-r--r--public/-/emojis/3/clock8.pngbin0 -> 4219 bytes
-rw-r--r--public/-/emojis/3/clock830.pngbin0 -> 4216 bytes
-rw-r--r--public/-/emojis/3/clock9.pngbin0 -> 4065 bytes
-rw-r--r--public/-/emojis/3/clock930.pngbin0 -> 4240 bytes
-rw-r--r--public/-/emojis/3/closed_book.pngbin0 -> 1117 bytes
-rw-r--r--public/-/emojis/3/closed_lock_with_key.pngbin0 -> 3986 bytes
-rw-r--r--public/-/emojis/3/closed_umbrella.pngbin0 -> 3932 bytes
-rw-r--r--public/-/emojis/3/cloud.pngbin0 -> 1704 bytes
-rw-r--r--public/-/emojis/3/cloud_lightning.pngbin0 -> 2647 bytes
-rw-r--r--public/-/emojis/3/cloud_rain.pngbin0 -> 2700 bytes
-rw-r--r--public/-/emojis/3/cloud_snow.pngbin0 -> 2918 bytes
-rw-r--r--public/-/emojis/3/cloud_tornado.pngbin0 -> 4640 bytes
-rw-r--r--public/-/emojis/3/clown.pngbin0 -> 5987 bytes
-rw-r--r--public/-/emojis/3/clubs.pngbin0 -> 2188 bytes
-rw-r--r--public/-/emojis/3/cocktail.pngbin0 -> 4333 bytes
-rw-r--r--public/-/emojis/3/coffee.pngbin0 -> 4816 bytes
-rw-r--r--public/-/emojis/3/coffin.pngbin0 -> 4583 bytes
-rw-r--r--public/-/emojis/3/cold_sweat.pngbin0 -> 5402 bytes
-rw-r--r--public/-/emojis/3/comet.pngbin0 -> 5048 bytes
-rw-r--r--public/-/emojis/3/compression.pngbin0 -> 3724 bytes
-rw-r--r--public/-/emojis/3/computer.pngbin0 -> 3228 bytes
-rw-r--r--public/-/emojis/3/confetti_ball.pngbin0 -> 6051 bytes
-rw-r--r--public/-/emojis/3/confounded.pngbin0 -> 5187 bytes
-rw-r--r--public/-/emojis/3/confused.pngbin0 -> 4642 bytes
-rw-r--r--public/-/emojis/3/congratulations.pngbin0 -> 4555 bytes
-rw-r--r--public/-/emojis/3/construction.pngbin0 -> 3478 bytes
-rw-r--r--public/-/emojis/3/construction_site.pngbin0 -> 4264 bytes
-rw-r--r--public/-/emojis/3/construction_worker.pngbin0 -> 4896 bytes
-rw-r--r--public/-/emojis/3/construction_worker_tone1.pngbin0 -> 5067 bytes
-rw-r--r--public/-/emojis/3/construction_worker_tone2.pngbin0 -> 4795 bytes
-rw-r--r--public/-/emojis/3/construction_worker_tone3.pngbin0 -> 4830 bytes
-rw-r--r--public/-/emojis/3/construction_worker_tone4.pngbin0 -> 4730 bytes
-rw-r--r--public/-/emojis/3/construction_worker_tone5.pngbin0 -> 4717 bytes
-rw-r--r--public/-/emojis/3/control_knobs.pngbin0 -> 5684 bytes
-rw-r--r--public/-/emojis/3/convenience_store.pngbin0 -> 2698 bytes
-rw-r--r--public/-/emojis/3/cookie.pngbin0 -> 5938 bytes
-rw-r--r--public/-/emojis/3/cooking.pngbin0 -> 4770 bytes
-rw-r--r--public/-/emojis/3/cool.pngbin0 -> 2875 bytes
-rw-r--r--public/-/emojis/3/cop.pngbin0 -> 6019 bytes
-rw-r--r--public/-/emojis/3/cop_tone1.pngbin0 -> 6169 bytes
-rw-r--r--public/-/emojis/3/cop_tone2.pngbin0 -> 5920 bytes
-rw-r--r--public/-/emojis/3/cop_tone3.pngbin0 -> 5868 bytes
-rw-r--r--public/-/emojis/3/cop_tone4.pngbin0 -> 5772 bytes
-rw-r--r--public/-/emojis/3/cop_tone5.pngbin0 -> 5780 bytes
-rw-r--r--public/-/emojis/3/copyright.pngbin0 -> 5128 bytes
-rw-r--r--public/-/emojis/3/corn.pngbin0 -> 5563 bytes
-rw-r--r--public/-/emojis/3/couch.pngbin0 -> 2475 bytes
-rw-r--r--public/-/emojis/3/couple.pngbin0 -> 5173 bytes
-rw-r--r--public/-/emojis/3/couple_mm.pngbin0 -> 5810 bytes
-rw-r--r--public/-/emojis/3/couple_with_heart.pngbin0 -> 6553 bytes
-rw-r--r--public/-/emojis/3/couple_ww.pngbin0 -> 6149 bytes
-rw-r--r--public/-/emojis/3/couplekiss.pngbin0 -> 5525 bytes
-rw-r--r--public/-/emojis/3/cow.pngbin0 -> 4007 bytes
-rw-r--r--public/-/emojis/3/cow2.pngbin0 -> 4363 bytes
-rw-r--r--public/-/emojis/3/cowboy.pngbin0 -> 5300 bytes
-rw-r--r--public/-/emojis/3/crab.pngbin0 -> 4705 bytes
-rw-r--r--public/-/emojis/3/crayon.pngbin0 -> 2966 bytes
-rw-r--r--public/-/emojis/3/credit_card.pngbin0 -> 1178 bytes
-rw-r--r--public/-/emojis/3/crescent_moon.pngbin0 -> 2074 bytes
-rw-r--r--public/-/emojis/3/cricket.pngbin0 -> 3508 bytes
-rw-r--r--public/-/emojis/3/crocodile.pngbin0 -> 4846 bytes
-rw-r--r--public/-/emojis/3/croissant.pngbin0 -> 5061 bytes
-rw-r--r--public/-/emojis/3/cross.pngbin0 -> 1516 bytes
-rw-r--r--public/-/emojis/3/crossed_flags.pngbin0 -> 4828 bytes
-rw-r--r--public/-/emojis/3/crossed_swords.pngbin0 -> 4299 bytes
-rw-r--r--public/-/emojis/3/crown.pngbin0 -> 5591 bytes
-rw-r--r--public/-/emojis/3/cruise_ship.pngbin0 -> 4569 bytes
-rw-r--r--public/-/emojis/3/cry.pngbin0 -> 5234 bytes
-rw-r--r--public/-/emojis/3/crying_cat_face.pngbin0 -> 4784 bytes
-rw-r--r--public/-/emojis/3/crystal_ball.pngbin0 -> 6021 bytes
-rw-r--r--public/-/emojis/3/cucumber.pngbin0 -> 3510 bytes
-rw-r--r--public/-/emojis/3/cupid.pngbin0 -> 3724 bytes
-rw-r--r--public/-/emojis/3/curly_loop.pngbin0 -> 3415 bytes
-rw-r--r--public/-/emojis/3/currency_exchange.pngbin0 -> 4497 bytes
-rw-r--r--public/-/emojis/3/curry.pngbin0 -> 5423 bytes
-rw-r--r--public/-/emojis/3/custard.pngbin0 -> 4078 bytes
-rw-r--r--public/-/emojis/3/customs.pngbin0 -> 3252 bytes
-rw-r--r--public/-/emojis/3/cyclone.pngbin0 -> 2952 bytes
-rw-r--r--public/-/emojis/3/dagger.pngbin0 -> 3563 bytes
-rw-r--r--public/-/emojis/3/dancer.pngbin0 -> 4630 bytes
-rw-r--r--public/-/emojis/3/dancer_tone1.pngbin0 -> 4815 bytes
-rw-r--r--public/-/emojis/3/dancer_tone2.pngbin0 -> 4725 bytes
-rw-r--r--public/-/emojis/3/dancer_tone3.pngbin0 -> 4651 bytes
-rw-r--r--public/-/emojis/3/dancer_tone4.pngbin0 -> 4627 bytes
-rw-r--r--public/-/emojis/3/dancer_tone5.pngbin0 -> 4671 bytes
-rw-r--r--public/-/emojis/3/dancers.pngbin0 -> 7024 bytes
-rw-r--r--public/-/emojis/3/dango.pngbin0 -> 2521 bytes
-rw-r--r--public/-/emojis/3/dark_sunglasses.pngbin0 -> 2077 bytes
-rw-r--r--public/-/emojis/3/dart.pngbin0 -> 5170 bytes
-rw-r--r--public/-/emojis/3/dash.pngbin0 -> 3690 bytes
-rw-r--r--public/-/emojis/3/date.pngbin0 -> 2660 bytes
-rw-r--r--public/-/emojis/3/deciduous_tree.pngbin0 -> 3211 bytes
-rw-r--r--public/-/emojis/3/deer.pngbin0 -> 3056 bytes
-rw-r--r--public/-/emojis/3/department_store.pngbin0 -> 4122 bytes
-rw-r--r--public/-/emojis/3/desert.pngbin0 -> 4523 bytes
-rw-r--r--public/-/emojis/3/desktop.pngbin0 -> 3258 bytes
-rw-r--r--public/-/emojis/3/diamond_shape_with_a_dot_inside.pngbin0 -> 3773 bytes
-rw-r--r--public/-/emojis/3/diamonds.pngbin0 -> 1936 bytes
-rw-r--r--public/-/emojis/3/disappointed.pngbin0 -> 4443 bytes
-rw-r--r--public/-/emojis/3/disappointed_relieved.pngbin0 -> 5316 bytes
-rw-r--r--public/-/emojis/3/dividers.pngbin0 -> 1221 bytes
-rw-r--r--public/-/emojis/3/dizzy.pngbin0 -> 3519 bytes
-rw-r--r--public/-/emojis/3/dizzy_face.pngbin0 -> 5424 bytes
-rw-r--r--public/-/emojis/3/do_not_litter.pngbin0 -> 5991 bytes
-rw-r--r--public/-/emojis/3/dog.pngbin0 -> 3819 bytes
-rw-r--r--public/-/emojis/3/dog2.pngbin0 -> 4299 bytes
-rw-r--r--public/-/emojis/3/dollar.pngbin0 -> 2400 bytes
-rw-r--r--public/-/emojis/3/dolls.pngbin0 -> 6570 bytes
-rw-r--r--public/-/emojis/3/dolphin.pngbin0 -> 3429 bytes
-rw-r--r--public/-/emojis/3/door.pngbin0 -> 1048 bytes
-rw-r--r--public/-/emojis/3/doughnut.pngbin0 -> 5066 bytes
-rw-r--r--public/-/emojis/3/dove.pngbin0 -> 3487 bytes
-rw-r--r--public/-/emojis/3/dragon.pngbin0 -> 5676 bytes
-rw-r--r--public/-/emojis/3/dragon_face.pngbin0 -> 5024 bytes
-rw-r--r--public/-/emojis/3/dress.pngbin0 -> 2376 bytes
-rw-r--r--public/-/emojis/3/dromedary_camel.pngbin0 -> 3406 bytes
-rw-r--r--public/-/emojis/3/drooling_face.pngbin0 -> 5286 bytes
-rw-r--r--public/-/emojis/3/droplet.pngbin0 -> 1614 bytes
-rw-r--r--public/-/emojis/3/drum.pngbin0 -> 5433 bytes
-rw-r--r--public/-/emojis/3/duck.pngbin0 -> 3436 bytes
-rw-r--r--public/-/emojis/3/dvd.pngbin0 -> 5257 bytes
-rw-r--r--public/-/emojis/3/e-mail.pngbin0 -> 2597 bytes
-rw-r--r--public/-/emojis/3/eagle.pngbin0 -> 3506 bytes
-rw-r--r--public/-/emojis/3/ear.pngbin0 -> 3801 bytes
-rw-r--r--public/-/emojis/3/ear_of_rice.pngbin0 -> 3601 bytes
-rw-r--r--public/-/emojis/3/ear_tone1.pngbin0 -> 3643 bytes
-rw-r--r--public/-/emojis/3/ear_tone2.pngbin0 -> 3659 bytes
-rw-r--r--public/-/emojis/3/ear_tone3.pngbin0 -> 3702 bytes
-rw-r--r--public/-/emojis/3/ear_tone4.pngbin0 -> 3365 bytes
-rw-r--r--public/-/emojis/3/ear_tone5.pngbin0 -> 3432 bytes
-rw-r--r--public/-/emojis/3/earth_africa.pngbin0 -> 6073 bytes
-rw-r--r--public/-/emojis/3/earth_americas.pngbin0 -> 5651 bytes
-rw-r--r--public/-/emojis/3/earth_asia.pngbin0 -> 6108 bytes
-rw-r--r--public/-/emojis/3/egg.pngbin0 -> 2340 bytes
-rw-r--r--public/-/emojis/3/eggplant.pngbin0 -> 2665 bytes
-rw-r--r--public/-/emojis/3/eight.pngbin0 -> 2697 bytes
-rw-r--r--public/-/emojis/3/eight_pointed_black_star.pngbin0 -> 2657 bytes
-rw-r--r--public/-/emojis/3/eight_spoked_asterisk.pngbin0 -> 2695 bytes
-rw-r--r--public/-/emojis/3/eject.pngbin0 -> 1881 bytes
-rw-r--r--public/-/emojis/3/electric_plug.pngbin0 -> 3440 bytes
-rw-r--r--public/-/emojis/3/elephant.pngbin0 -> 3367 bytes
-rw-r--r--public/-/emojis/3/emojis.json12560
-rw-r--r--public/-/emojis/3/end.pngbin0 -> 2763 bytes
-rw-r--r--public/-/emojis/3/envelope.pngbin0 -> 2349 bytes
-rw-r--r--public/-/emojis/3/envelope_with_arrow.pngbin0 -> 2710 bytes
-rw-r--r--public/-/emojis/3/euro.pngbin0 -> 2357 bytes
-rw-r--r--public/-/emojis/3/european_castle.pngbin0 -> 4038 bytes
-rw-r--r--public/-/emojis/3/european_post_office.pngbin0 -> 3735 bytes
-rw-r--r--public/-/emojis/3/evergreen_tree.pngbin0 -> 4038 bytes
-rw-r--r--public/-/emojis/3/exclamation.pngbin0 -> 1320 bytes
-rw-r--r--public/-/emojis/3/expressionless.pngbin0 -> 3974 bytes
-rw-r--r--public/-/emojis/3/eye.pngbin0 -> 4113 bytes
-rw-r--r--public/-/emojis/3/eye_in_speech_bubble.pngbin0 -> 4306 bytes
-rw-r--r--public/-/emojis/3/eyeglasses.pngbin0 -> 2289 bytes
-rw-r--r--public/-/emojis/3/eyes.pngbin0 -> 4038 bytes
-rw-r--r--public/-/emojis/3/face_palm.pngbin0 -> 5371 bytes
-rw-r--r--public/-/emojis/3/face_palm_tone1.pngbin0 -> 5569 bytes
-rw-r--r--public/-/emojis/3/face_palm_tone2.pngbin0 -> 5362 bytes
-rw-r--r--public/-/emojis/3/face_palm_tone3.pngbin0 -> 5288 bytes
-rw-r--r--public/-/emojis/3/face_palm_tone4.pngbin0 -> 5135 bytes
-rw-r--r--public/-/emojis/3/face_palm_tone5.pngbin0 -> 5136 bytes
-rw-r--r--public/-/emojis/3/factory.pngbin0 -> 3573 bytes
-rw-r--r--public/-/emojis/3/fallen_leaf.pngbin0 -> 4567 bytes
-rw-r--r--public/-/emojis/3/family.pngbin0 -> 6948 bytes
-rw-r--r--public/-/emojis/3/family_mmb.pngbin0 -> 6105 bytes
-rw-r--r--public/-/emojis/3/family_mmbb.pngbin0 -> 7068 bytes
-rw-r--r--public/-/emojis/3/family_mmg.pngbin0 -> 6193 bytes
-rw-r--r--public/-/emojis/3/family_mmgb.pngbin0 -> 7166 bytes
-rw-r--r--public/-/emojis/3/family_mmgg.pngbin0 -> 7227 bytes
-rw-r--r--public/-/emojis/3/family_mwbb.pngbin0 -> 7391 bytes
-rw-r--r--public/-/emojis/3/family_mwg.pngbin0 -> 6476 bytes
-rw-r--r--public/-/emojis/3/family_mwgb.pngbin0 -> 7491 bytes
-rw-r--r--public/-/emojis/3/family_mwgg.pngbin0 -> 7567 bytes
-rw-r--r--public/-/emojis/3/family_wwb.pngbin0 -> 6449 bytes
-rw-r--r--public/-/emojis/3/family_wwbb.pngbin0 -> 7388 bytes
-rw-r--r--public/-/emojis/3/family_wwg.pngbin0 -> 6464 bytes
-rw-r--r--public/-/emojis/3/family_wwgb.pngbin0 -> 7472 bytes
-rw-r--r--public/-/emojis/3/family_wwgg.pngbin0 -> 7559 bytes
-rw-r--r--public/-/emojis/3/fast_forward.pngbin0 -> 2175 bytes
-rw-r--r--public/-/emojis/3/fax.pngbin0 -> 4402 bytes
-rw-r--r--public/-/emojis/3/fearful.pngbin0 -> 5029 bytes
-rw-r--r--public/-/emojis/3/feet.pngbin0 -> 2176 bytes
-rw-r--r--public/-/emojis/3/fencer.pngbin0 -> 4332 bytes
-rw-r--r--public/-/emojis/3/ferris_wheel.pngbin0 -> 7812 bytes
-rw-r--r--public/-/emojis/3/ferry.pngbin0 -> 4406 bytes
-rw-r--r--public/-/emojis/3/field_hockey.pngbin0 -> 3521 bytes
-rw-r--r--public/-/emojis/3/file_cabinet.pngbin0 -> 1937 bytes
-rw-r--r--public/-/emojis/3/file_folder.pngbin0 -> 1645 bytes
-rw-r--r--public/-/emojis/3/film_frames.pngbin0 -> 3374 bytes
-rw-r--r--public/-/emojis/3/fingers_crossed.pngbin0 -> 3592 bytes
-rw-r--r--public/-/emojis/3/fingers_crossed_tone1.pngbin0 -> 3952 bytes
-rw-r--r--public/-/emojis/3/fingers_crossed_tone2.pngbin0 -> 4026 bytes
-rw-r--r--public/-/emojis/3/fingers_crossed_tone3.pngbin0 -> 3880 bytes
-rw-r--r--public/-/emojis/3/fingers_crossed_tone4.pngbin0 -> 3518 bytes
-rw-r--r--public/-/emojis/3/fingers_crossed_tone5.pngbin0 -> 3678 bytes
-rw-r--r--public/-/emojis/3/fire.pngbin0 -> 3744 bytes
-rw-r--r--public/-/emojis/3/fire_engine.pngbin0 -> 3792 bytes
-rw-r--r--public/-/emojis/3/fireworks.pngbin0 -> 9670 bytes
-rw-r--r--public/-/emojis/3/first_place.pngbin0 -> 3269 bytes
-rw-r--r--public/-/emojis/3/first_quarter_moon.pngbin0 -> 3809 bytes
-rw-r--r--public/-/emojis/3/first_quarter_moon_with_face.pngbin0 -> 3386 bytes
-rw-r--r--public/-/emojis/3/fish.pngbin0 -> 3269 bytes
-rw-r--r--public/-/emojis/3/fish_cake.pngbin0 -> 5325 bytes
-rw-r--r--public/-/emojis/3/fishing_pole_and_fish.pngbin0 -> 3633 bytes
-rw-r--r--public/-/emojis/3/fist.pngbin0 -> 4433 bytes
-rw-r--r--public/-/emojis/3/fist_tone1.pngbin0 -> 4893 bytes
-rw-r--r--public/-/emojis/3/fist_tone2.pngbin0 -> 4935 bytes
-rw-r--r--public/-/emojis/3/fist_tone3.pngbin0 -> 4564 bytes
-rw-r--r--public/-/emojis/3/fist_tone4.pngbin0 -> 4289 bytes
-rw-r--r--public/-/emojis/3/fist_tone5.pngbin0 -> 4299 bytes
-rw-r--r--public/-/emojis/3/five.pngbin0 -> 2379 bytes
-rw-r--r--public/-/emojis/3/flag_ac.pngbin0 -> 5417 bytes
-rw-r--r--public/-/emojis/3/flag_ad.pngbin0 -> 3679 bytes
-rw-r--r--public/-/emojis/3/flag_ae.pngbin0 -> 2260 bytes
-rw-r--r--public/-/emojis/3/flag_af.pngbin0 -> 3840 bytes
-rw-r--r--public/-/emojis/3/flag_ag.pngbin0 -> 4348 bytes
-rw-r--r--public/-/emojis/3/flag_ai.pngbin0 -> 4868 bytes
-rw-r--r--public/-/emojis/3/flag_al.pngbin0 -> 4244 bytes
-rw-r--r--public/-/emojis/3/flag_am.pngbin0 -> 3180 bytes
-rw-r--r--public/-/emojis/3/flag_ao.pngbin0 -> 3976 bytes
-rw-r--r--public/-/emojis/3/flag_aq.pngbin0 -> 3687 bytes
-rw-r--r--public/-/emojis/3/flag_ar.pngbin0 -> 3575 bytes
-rw-r--r--public/-/emojis/3/flag_as.pngbin0 -> 4483 bytes
-rw-r--r--public/-/emojis/3/flag_at.pngbin0 -> 3109 bytes
-rw-r--r--public/-/emojis/3/flag_au.pngbin0 -> 4377 bytes
-rw-r--r--public/-/emojis/3/flag_aw.pngbin0 -> 4458 bytes
-rw-r--r--public/-/emojis/3/flag_ax.pngbin0 -> 4073 bytes
-rw-r--r--public/-/emojis/3/flag_az.pngbin0 -> 3840 bytes
-rw-r--r--public/-/emojis/3/flag_ba.pngbin0 -> 4022 bytes
-rw-r--r--public/-/emojis/3/flag_bb.pngbin0 -> 3240 bytes
-rw-r--r--public/-/emojis/3/flag_bd.pngbin0 -> 2956 bytes
-rw-r--r--public/-/emojis/3/flag_be.pngbin0 -> 2304 bytes
-rw-r--r--public/-/emojis/3/flag_bf.pngbin0 -> 3029 bytes
-rw-r--r--public/-/emojis/3/flag_bg.pngbin0 -> 3158 bytes
-rw-r--r--public/-/emojis/3/flag_bh.pngbin0 -> 3494 bytes
-rw-r--r--public/-/emojis/3/flag_bi.pngbin0 -> 5293 bytes
-rw-r--r--public/-/emojis/3/flag_bj.pngbin0 -> 2974 bytes
-rw-r--r--public/-/emojis/3/flag_bl.pngbin0 -> 5101 bytes
-rw-r--r--public/-/emojis/3/flag_black.pngbin0 -> 2831 bytes
-rw-r--r--public/-/emojis/3/flag_bm.pngbin0 -> 5562 bytes
-rw-r--r--public/-/emojis/3/flag_bn.pngbin0 -> 4677 bytes
-rw-r--r--public/-/emojis/3/flag_bo.pngbin0 -> 3130 bytes
-rw-r--r--public/-/emojis/3/flag_bq.pngbin0 -> 4110 bytes
-rw-r--r--public/-/emojis/3/flag_br.pngbin0 -> 4522 bytes
-rw-r--r--public/-/emojis/3/flag_bs.pngbin0 -> 3170 bytes
-rw-r--r--public/-/emojis/3/flag_bt.pngbin0 -> 4770 bytes
-rw-r--r--public/-/emojis/3/flag_bv.pngbin0 -> 4312 bytes
-rw-r--r--public/-/emojis/3/flag_bw.pngbin0 -> 3981 bytes
-rw-r--r--public/-/emojis/3/flag_by.pngbin0 -> 3969 bytes
-rw-r--r--public/-/emojis/3/flag_bz.pngbin0 -> 5952 bytes
-rw-r--r--public/-/emojis/3/flag_ca.pngbin0 -> 2441 bytes
-rw-r--r--public/-/emojis/3/flag_cc.pngbin0 -> 3093 bytes
-rw-r--r--public/-/emojis/3/flag_cd.pngbin0 -> 4558 bytes
-rw-r--r--public/-/emojis/3/flag_cf.pngbin0 -> 3796 bytes
-rw-r--r--public/-/emojis/3/flag_cg.pngbin0 -> 3593 bytes
-rw-r--r--public/-/emojis/3/flag_ch.pngbin0 -> 1830 bytes
-rw-r--r--public/-/emojis/3/flag_ci.pngbin0 -> 2322 bytes
-rw-r--r--public/-/emojis/3/flag_ck.pngbin0 -> 5675 bytes
-rw-r--r--public/-/emojis/3/flag_cl.pngbin0 -> 3265 bytes
-rw-r--r--public/-/emojis/3/flag_cm.pngbin0 -> 3159 bytes
-rw-r--r--public/-/emojis/3/flag_cn.pngbin0 -> 3457 bytes
-rw-r--r--public/-/emojis/3/flag_co.pngbin0 -> 3468 bytes
-rw-r--r--public/-/emojis/3/flag_cp.pngbin0 -> 2564 bytes
-rw-r--r--public/-/emojis/3/flag_cr.pngbin0 -> 3967 bytes
-rw-r--r--public/-/emojis/3/flag_cu.pngbin0 -> 4313 bytes
-rw-r--r--public/-/emojis/3/flag_cv.pngbin0 -> 4356 bytes
-rw-r--r--public/-/emojis/3/flag_cw.pngbin0 -> 3609 bytes
-rw-r--r--public/-/emojis/3/flag_cx.pngbin0 -> 4590 bytes
-rw-r--r--public/-/emojis/3/flag_cy.pngbin0 -> 3018 bytes
-rw-r--r--public/-/emojis/3/flag_cz.pngbin0 -> 3434 bytes
-rw-r--r--public/-/emojis/3/flag_de.pngbin0 -> 2217 bytes
-rw-r--r--public/-/emojis/3/flag_dg.pngbin0 -> 8831 bytes
-rw-r--r--public/-/emojis/3/flag_dj.pngbin0 -> 3765 bytes
-rw-r--r--public/-/emojis/3/flag_dk.pngbin0 -> 3547 bytes
-rw-r--r--public/-/emojis/3/flag_dm.pngbin0 -> 4407 bytes
-rw-r--r--public/-/emojis/3/flag_do.pngbin0 -> 3628 bytes
-rw-r--r--public/-/emojis/3/flag_dz.pngbin0 -> 3080 bytes
-rw-r--r--public/-/emojis/3/flag_ea.pngbin0 -> 3922 bytes
-rw-r--r--public/-/emojis/3/flag_ec.pngbin0 -> 4321 bytes
-rw-r--r--public/-/emojis/3/flag_ee.pngbin0 -> 2601 bytes
-rw-r--r--public/-/emojis/3/flag_eg.pngbin0 -> 2949 bytes
-rw-r--r--public/-/emojis/3/flag_eh.pngbin0 -> 3334 bytes
-rw-r--r--public/-/emojis/3/flag_er.pngbin0 -> 4782 bytes
-rw-r--r--public/-/emojis/3/flag_es.pngbin0 -> 3922 bytes
-rw-r--r--public/-/emojis/3/flag_et.pngbin0 -> 4771 bytes
-rw-r--r--public/-/emojis/3/flag_eu.pngbin0 -> 3474 bytes
-rw-r--r--public/-/emojis/3/flag_fi.pngbin0 -> 3009 bytes
-rw-r--r--public/-/emojis/3/flag_fj.pngbin0 -> 5649 bytes
-rw-r--r--public/-/emojis/3/flag_fk.pngbin0 -> 5268 bytes
-rw-r--r--public/-/emojis/3/flag_fm.pngbin0 -> 3552 bytes
-rw-r--r--public/-/emojis/3/flag_fo.pngbin0 -> 3753 bytes
-rw-r--r--public/-/emojis/3/flag_fr.pngbin0 -> 2564 bytes
-rw-r--r--public/-/emojis/3/flag_ga.pngbin0 -> 3416 bytes
-rw-r--r--public/-/emojis/3/flag_gb.pngbin0 -> 6636 bytes
-rw-r--r--public/-/emojis/3/flag_gd.pngbin0 -> 4878 bytes
-rw-r--r--public/-/emojis/3/flag_ge.pngbin0 -> 3036 bytes
-rw-r--r--public/-/emojis/3/flag_gf.pngbin0 -> 3584 bytes
-rw-r--r--public/-/emojis/3/flag_gg.pngbin0 -> 3414 bytes
-rw-r--r--public/-/emojis/3/flag_gh.pngbin0 -> 3656 bytes
-rw-r--r--public/-/emojis/3/flag_gi.pngbin0 -> 3416 bytes
-rw-r--r--public/-/emojis/3/flag_gl.pngbin0 -> 3523 bytes
-rw-r--r--public/-/emojis/3/flag_gm.pngbin0 -> 4338 bytes
-rw-r--r--public/-/emojis/3/flag_gn.pngbin0 -> 2866 bytes
-rw-r--r--public/-/emojis/3/flag_gp.pngbin0 -> 4367 bytes
-rw-r--r--public/-/emojis/3/flag_gq.pngbin0 -> 3911 bytes
-rw-r--r--public/-/emojis/3/flag_gr.pngbin0 -> 5192 bytes
-rw-r--r--public/-/emojis/3/flag_gs.pngbin0 -> 5561 bytes
-rw-r--r--public/-/emojis/3/flag_gt.pngbin0 -> 3411 bytes
-rw-r--r--public/-/emojis/3/flag_gu.pngbin0 -> 3457 bytes
-rw-r--r--public/-/emojis/3/flag_gw.pngbin0 -> 3274 bytes
-rw-r--r--public/-/emojis/3/flag_gy.pngbin0 -> 4879 bytes
-rw-r--r--public/-/emojis/3/flag_hk.pngbin0 -> 4245 bytes
-rw-r--r--public/-/emojis/3/flag_hm.pngbin0 -> 4377 bytes
-rw-r--r--public/-/emojis/3/flag_hn.pngbin0 -> 3320 bytes
-rw-r--r--public/-/emojis/3/flag_hr.pngbin0 -> 3448 bytes
-rw-r--r--public/-/emojis/3/flag_ht.pngbin0 -> 3418 bytes
-rw-r--r--public/-/emojis/3/flag_hu.pngbin0 -> 3128 bytes
-rw-r--r--public/-/emojis/3/flag_ic.pngbin0 -> 2379 bytes
-rw-r--r--public/-/emojis/3/flag_id.pngbin0 -> 1859 bytes
-rw-r--r--public/-/emojis/3/flag_ie.pngbin0 -> 2580 bytes
-rw-r--r--public/-/emojis/3/flag_il.pngbin0 -> 4462 bytes
-rw-r--r--public/-/emojis/3/flag_im.pngbin0 -> 3823 bytes
-rw-r--r--public/-/emojis/3/flag_in.pngbin0 -> 3492 bytes
-rw-r--r--public/-/emojis/3/flag_io.pngbin0 -> 8831 bytes
-rw-r--r--public/-/emojis/3/flag_iq.pngbin0 -> 3228 bytes
-rw-r--r--public/-/emojis/3/flag_ir.pngbin0 -> 3658 bytes
-rw-r--r--public/-/emojis/3/flag_is.pngbin0 -> 4244 bytes
-rw-r--r--public/-/emojis/3/flag_it.pngbin0 -> 2514 bytes
-rw-r--r--public/-/emojis/3/flag_je.pngbin0 -> 4803 bytes
-rw-r--r--public/-/emojis/3/flag_jm.pngbin0 -> 3939 bytes
-rw-r--r--public/-/emojis/3/flag_jo.pngbin0 -> 3327 bytes
-rw-r--r--public/-/emojis/3/flag_jp.pngbin0 -> 2594 bytes
-rw-r--r--public/-/emojis/3/flag_ke.pngbin0 -> 3724 bytes
-rw-r--r--public/-/emojis/3/flag_kg.pngbin0 -> 4093 bytes
-rw-r--r--public/-/emojis/3/flag_kh.pngbin0 -> 4306 bytes
-rw-r--r--public/-/emojis/3/flag_ki.pngbin0 -> 5861 bytes
-rw-r--r--public/-/emojis/3/flag_km.pngbin0 -> 4561 bytes
-rw-r--r--public/-/emojis/3/flag_kn.pngbin0 -> 4492 bytes
-rw-r--r--public/-/emojis/3/flag_kp.pngbin0 -> 4708 bytes
-rw-r--r--public/-/emojis/3/flag_kr.pngbin0 -> 4606 bytes
-rw-r--r--public/-/emojis/3/flag_kw.pngbin0 -> 3032 bytes
-rw-r--r--public/-/emojis/3/flag_ky.pngbin0 -> 5276 bytes
-rw-r--r--public/-/emojis/3/flag_kz.pngbin0 -> 3942 bytes
-rw-r--r--public/-/emojis/3/flag_la.pngbin0 -> 3789 bytes
-rw-r--r--public/-/emojis/3/flag_lb.pngbin0 -> 4040 bytes
-rw-r--r--public/-/emojis/3/flag_lc.pngbin0 -> 3733 bytes
-rw-r--r--public/-/emojis/3/flag_li.pngbin0 -> 3413 bytes
-rw-r--r--public/-/emojis/3/flag_lk.pngbin0 -> 4954 bytes
-rw-r--r--public/-/emojis/3/flag_lr.pngbin0 -> 5784 bytes
-rw-r--r--public/-/emojis/3/flag_ls.pngbin0 -> 3321 bytes
-rw-r--r--public/-/emojis/3/flag_lt.pngbin0 -> 3317 bytes
-rw-r--r--public/-/emojis/3/flag_lu.pngbin0 -> 2988 bytes
-rw-r--r--public/-/emojis/3/flag_lv.pngbin0 -> 3232 bytes
-rw-r--r--public/-/emojis/3/flag_ly.pngbin0 -> 2954 bytes
-rw-r--r--public/-/emojis/3/flag_ma.pngbin0 -> 3154 bytes
-rw-r--r--public/-/emojis/3/flag_mc.pngbin0 -> 2706 bytes
-rw-r--r--public/-/emojis/3/flag_md.pngbin0 -> 3617 bytes
-rw-r--r--public/-/emojis/3/flag_me.pngbin0 -> 4032 bytes
-rw-r--r--public/-/emojis/3/flag_mf.pngbin0 -> 2564 bytes
-rw-r--r--public/-/emojis/3/flag_mg.pngbin0 -> 2367 bytes
-rw-r--r--public/-/emojis/3/flag_mh.pngbin0 -> 4529 bytes
-rw-r--r--public/-/emojis/3/flag_mk.pngbin0 -> 4370 bytes
-rw-r--r--public/-/emojis/3/flag_ml.pngbin0 -> 3015 bytes
-rw-r--r--public/-/emojis/3/flag_mm.pngbin0 -> 4051 bytes
-rw-r--r--public/-/emojis/3/flag_mn.pngbin0 -> 3514 bytes
-rw-r--r--public/-/emojis/3/flag_mo.pngbin0 -> 3509 bytes
-rw-r--r--public/-/emojis/3/flag_mp.pngbin0 -> 4857 bytes
-rw-r--r--public/-/emojis/3/flag_mq.pngbin0 -> 5397 bytes
-rw-r--r--public/-/emojis/3/flag_mr.pngbin0 -> 3973 bytes
-rw-r--r--public/-/emojis/3/flag_ms.pngbin0 -> 4904 bytes
-rw-r--r--public/-/emojis/3/flag_mt.pngbin0 -> 2759 bytes
-rw-r--r--public/-/emojis/3/flag_mu.pngbin0 -> 3611 bytes
-rw-r--r--public/-/emojis/3/flag_mv.pngbin0 -> 3560 bytes
-rw-r--r--public/-/emojis/3/flag_mw.pngbin0 -> 3658 bytes
-rw-r--r--public/-/emojis/3/flag_mx.pngbin0 -> 3475 bytes
-rw-r--r--public/-/emojis/3/flag_my.pngbin0 -> 6217 bytes
-rw-r--r--public/-/emojis/3/flag_mz.pngbin0 -> 4380 bytes
-rw-r--r--public/-/emojis/3/flag_na.pngbin0 -> 4842 bytes
-rw-r--r--public/-/emojis/3/flag_nc.pngbin0 -> 4035 bytes
-rw-r--r--public/-/emojis/3/flag_ne.pngbin0 -> 3490 bytes
-rw-r--r--public/-/emojis/3/flag_nf.pngbin0 -> 3639 bytes
-rw-r--r--public/-/emojis/3/flag_ng.pngbin0 -> 2321 bytes
-rw-r--r--public/-/emojis/3/flag_ni.pngbin0 -> 3335 bytes
-rw-r--r--public/-/emojis/3/flag_nl.pngbin0 -> 3205 bytes
-rw-r--r--public/-/emojis/3/flag_no.pngbin0 -> 4312 bytes
-rw-r--r--public/-/emojis/3/flag_np.pngbin0 -> 3979 bytes
-rw-r--r--public/-/emojis/3/flag_nr.pngbin0 -> 3527 bytes
-rw-r--r--public/-/emojis/3/flag_nu.pngbin0 -> 4776 bytes
-rw-r--r--public/-/emojis/3/flag_nz.pngbin0 -> 4674 bytes
-rw-r--r--public/-/emojis/3/flag_om.pngbin0 -> 3384 bytes
-rw-r--r--public/-/emojis/3/flag_pa.pngbin0 -> 3381 bytes
-rw-r--r--public/-/emojis/3/flag_pe.pngbin0 -> 2762 bytes
-rw-r--r--public/-/emojis/3/flag_pf.pngbin0 -> 4073 bytes
-rw-r--r--public/-/emojis/3/flag_pg.pngbin0 -> 3751 bytes
-rw-r--r--public/-/emojis/3/flag_ph.pngbin0 -> 3984 bytes
-rw-r--r--public/-/emojis/3/flag_pk.pngbin0 -> 3332 bytes
-rw-r--r--public/-/emojis/3/flag_pl.pngbin0 -> 2692 bytes
-rw-r--r--public/-/emojis/3/flag_pm.pngbin0 -> 7375 bytes
-rw-r--r--public/-/emojis/3/flag_pn.pngbin0 -> 5825 bytes
-rw-r--r--public/-/emojis/3/flag_pr.pngbin0 -> 4023 bytes
-rw-r--r--public/-/emojis/3/flag_ps.pngbin0 -> 3028 bytes
-rw-r--r--public/-/emojis/3/flag_pt.pngbin0 -> 2899 bytes
-rw-r--r--public/-/emojis/3/flag_pw.pngbin0 -> 3569 bytes
-rw-r--r--public/-/emojis/3/flag_py.pngbin0 -> 3402 bytes
-rw-r--r--public/-/emojis/3/flag_qa.pngbin0 -> 2884 bytes
-rw-r--r--public/-/emojis/3/flag_re.pngbin0 -> 4952 bytes
-rw-r--r--public/-/emojis/3/flag_ro.pngbin0 -> 2870 bytes
-rw-r--r--public/-/emojis/3/flag_rs.pngbin0 -> 4417 bytes
-rw-r--r--public/-/emojis/3/flag_ru.pngbin0 -> 3165 bytes
-rw-r--r--public/-/emojis/3/flag_rw.pngbin0 -> 3669 bytes
-rw-r--r--public/-/emojis/3/flag_sa.pngbin0 -> 3905 bytes
-rw-r--r--public/-/emojis/3/flag_sb.pngbin0 -> 4590 bytes
-rw-r--r--public/-/emojis/3/flag_sc.pngbin0 -> 4103 bytes
-rw-r--r--public/-/emojis/3/flag_sd.pngbin0 -> 3020 bytes
-rw-r--r--public/-/emojis/3/flag_se.pngbin0 -> 3302 bytes
-rw-r--r--public/-/emojis/3/flag_sg.pngbin0 -> 3268 bytes
-rw-r--r--public/-/emojis/3/flag_sh.pngbin0 -> 4769 bytes
-rw-r--r--public/-/emojis/3/flag_si.pngbin0 -> 3628 bytes
-rw-r--r--public/-/emojis/3/flag_sj.pngbin0 -> 4312 bytes
-rw-r--r--public/-/emojis/3/flag_sk.pngbin0 -> 4146 bytes
-rw-r--r--public/-/emojis/3/flag_sl.pngbin0 -> 3018 bytes
-rw-r--r--public/-/emojis/3/flag_sm.pngbin0 -> 3857 bytes
-rw-r--r--public/-/emojis/3/flag_sn.pngbin0 -> 3237 bytes
-rw-r--r--public/-/emojis/3/flag_so.pngbin0 -> 3395 bytes
-rw-r--r--public/-/emojis/3/flag_sr.pngbin0 -> 4566 bytes
-rw-r--r--public/-/emojis/3/flag_ss.pngbin0 -> 4267 bytes
-rw-r--r--public/-/emojis/3/flag_st.pngbin0 -> 4037 bytes
-rw-r--r--public/-/emojis/3/flag_sv.pngbin0 -> 3683 bytes
-rw-r--r--public/-/emojis/3/flag_sx.pngbin0 -> 4158 bytes
-rw-r--r--public/-/emojis/3/flag_sy.pngbin0 -> 3140 bytes
-rw-r--r--public/-/emojis/3/flag_sz.pngbin0 -> 5784 bytes
-rw-r--r--public/-/emojis/3/flag_ta.pngbin0 -> 5942 bytes
-rw-r--r--public/-/emojis/3/flag_tc.pngbin0 -> 4778 bytes
-rw-r--r--public/-/emojis/3/flag_td.pngbin0 -> 2642 bytes
-rw-r--r--public/-/emojis/3/flag_tf.pngbin0 -> 3767 bytes
-rw-r--r--public/-/emojis/3/flag_tg.pngbin0 -> 4153 bytes
-rw-r--r--public/-/emojis/3/flag_th.pngbin0 -> 4119 bytes
-rw-r--r--public/-/emojis/3/flag_tj.pngbin0 -> 3015 bytes
-rw-r--r--public/-/emojis/3/flag_tk.pngbin0 -> 3998 bytes
-rw-r--r--public/-/emojis/3/flag_tl.pngbin0 -> 4012 bytes
-rw-r--r--public/-/emojis/3/flag_tm.pngbin0 -> 4418 bytes
-rw-r--r--public/-/emojis/3/flag_tn.pngbin0 -> 3610 bytes
-rw-r--r--public/-/emojis/3/flag_to.pngbin0 -> 2314 bytes
-rw-r--r--public/-/emojis/3/flag_tr.pngbin0 -> 3904 bytes
-rw-r--r--public/-/emojis/3/flag_tt.pngbin0 -> 4913 bytes
-rw-r--r--public/-/emojis/3/flag_tv.pngbin0 -> 5608 bytes
-rw-r--r--public/-/emojis/3/flag_tw.pngbin0 -> 2556 bytes
-rw-r--r--public/-/emojis/3/flag_tz.pngbin0 -> 4065 bytes
-rw-r--r--public/-/emojis/3/flag_ua.pngbin0 -> 2740 bytes
-rw-r--r--public/-/emojis/3/flag_ug.pngbin0 -> 3871 bytes
-rw-r--r--public/-/emojis/3/flag_um.pngbin0 -> 6869 bytes
-rw-r--r--public/-/emojis/3/flag_us.pngbin0 -> 6869 bytes
-rw-r--r--public/-/emojis/3/flag_uy.pngbin0 -> 5666 bytes
-rw-r--r--public/-/emojis/3/flag_uz.pngbin0 -> 3871 bytes
-rw-r--r--public/-/emojis/3/flag_va.pngbin0 -> 2980 bytes
-rw-r--r--public/-/emojis/3/flag_vc.pngbin0 -> 3489 bytes
-rw-r--r--public/-/emojis/3/flag_ve.pngbin0 -> 3871 bytes
-rw-r--r--public/-/emojis/3/flag_vg.pngbin0 -> 5297 bytes
-rw-r--r--public/-/emojis/3/flag_vi.pngbin0 -> 5591 bytes
-rw-r--r--public/-/emojis/3/flag_vn.pngbin0 -> 3524 bytes
-rw-r--r--public/-/emojis/3/flag_vu.pngbin0 -> 4768 bytes
-rw-r--r--public/-/emojis/3/flag_wf.pngbin0 -> 3650 bytes
-rw-r--r--public/-/emojis/3/flag_white.pngbin0 -> 3094 bytes
-rw-r--r--public/-/emojis/3/flag_ws.pngbin0 -> 3466 bytes
-rw-r--r--public/-/emojis/3/flag_xk.pngbin0 -> 4110 bytes
-rw-r--r--public/-/emojis/3/flag_ye.pngbin0 -> 2525 bytes
-rw-r--r--public/-/emojis/3/flag_yt.pngbin0 -> 5317 bytes
-rw-r--r--public/-/emojis/3/flag_za.pngbin0 -> 4932 bytes
-rw-r--r--public/-/emojis/3/flag_zm.pngbin0 -> 2816 bytes
-rw-r--r--public/-/emojis/3/flag_zw.pngbin0 -> 4175 bytes
-rw-r--r--public/-/emojis/3/flags.pngbin0 -> 6001 bytes
-rw-r--r--public/-/emojis/3/flashlight.pngbin0 -> 3717 bytes
-rw-r--r--public/-/emojis/3/fleur-de-lis.pngbin0 -> 4213 bytes
-rw-r--r--public/-/emojis/3/floppy_disk.pngbin0 -> 1145 bytes
-rw-r--r--public/-/emojis/3/flower_playing_cards.pngbin0 -> 1867 bytes
-rw-r--r--public/-/emojis/3/flushed.pngbin0 -> 5287 bytes
-rw-r--r--public/-/emojis/3/fog.pngbin0 -> 3554 bytes
-rw-r--r--public/-/emojis/3/foggy.pngbin0 -> 5564 bytes
-rw-r--r--public/-/emojis/3/football.pngbin0 -> 5526 bytes
-rw-r--r--public/-/emojis/3/footprints.pngbin0 -> 2400 bytes
-rw-r--r--public/-/emojis/3/fork_and_knife.pngbin0 -> 1967 bytes
-rw-r--r--public/-/emojis/3/fork_knife_plate.pngbin0 -> 3521 bytes
-rw-r--r--public/-/emojis/3/fountain.pngbin0 -> 5367 bytes
-rw-r--r--public/-/emojis/3/four.pngbin0 -> 1967 bytes
-rw-r--r--public/-/emojis/3/four_leaf_clover.pngbin0 -> 4088 bytes
-rw-r--r--public/-/emojis/3/fox.pngbin0 -> 4198 bytes
-rw-r--r--public/-/emojis/3/frame_photo.pngbin0 -> 3181 bytes
-rw-r--r--public/-/emojis/3/free.pngbin0 -> 2328 bytes
-rw-r--r--public/-/emojis/3/french_bread.pngbin0 -> 4176 bytes
-rw-r--r--public/-/emojis/3/fried_shrimp.pngbin0 -> 3197 bytes
-rw-r--r--public/-/emojis/3/fries.pngbin0 -> 4531 bytes
-rw-r--r--public/-/emojis/3/frog.pngbin0 -> 3157 bytes
-rw-r--r--public/-/emojis/3/frowning.pngbin0 -> 4514 bytes
-rw-r--r--public/-/emojis/3/frowning2.pngbin0 -> 4868 bytes
-rw-r--r--public/-/emojis/3/fuelpump.pngbin0 -> 4213 bytes
-rw-r--r--public/-/emojis/3/full_moon.pngbin0 -> 3799 bytes
-rw-r--r--public/-/emojis/3/full_moon_with_face.pngbin0 -> 5131 bytes
-rw-r--r--public/-/emojis/3/game_die.pngbin0 -> 4219 bytes
-rw-r--r--public/-/emojis/3/gay_pride_flag.pngbin0 -> 4447 bytes
-rw-r--r--public/-/emojis/3/gear.pngbin0 -> 3165 bytes
-rw-r--r--public/-/emojis/3/gem.pngbin0 -> 3371 bytes
-rw-r--r--public/-/emojis/3/gemini.pngbin0 -> 3896 bytes
-rw-r--r--public/-/emojis/3/ghost.pngbin0 -> 4181 bytes
-rw-r--r--public/-/emojis/3/gift.pngbin0 -> 4167 bytes
-rw-r--r--public/-/emojis/3/gift_heart.pngbin0 -> 4302 bytes
-rw-r--r--public/-/emojis/3/girl.pngbin0 -> 5150 bytes
-rw-r--r--public/-/emojis/3/girl_tone1.pngbin0 -> 5097 bytes
-rw-r--r--public/-/emojis/3/girl_tone2.pngbin0 -> 5086 bytes
-rw-r--r--public/-/emojis/3/girl_tone3.pngbin0 -> 4986 bytes
-rw-r--r--public/-/emojis/3/girl_tone4.pngbin0 -> 4701 bytes
-rw-r--r--public/-/emojis/3/girl_tone5.pngbin0 -> 4761 bytes
-rw-r--r--public/-/emojis/3/globe_with_meridians.pngbin0 -> 4630 bytes
-rw-r--r--public/-/emojis/3/goal.pngbin0 -> 5397 bytes
-rw-r--r--public/-/emojis/3/goat.pngbin0 -> 3653 bytes
-rw-r--r--public/-/emojis/3/golf.pngbin0 -> 2737 bytes
-rw-r--r--public/-/emojis/3/golfer.pngbin0 -> 3496 bytes
-rw-r--r--public/-/emojis/3/gorilla.pngbin0 -> 4435 bytes
-rw-r--r--public/-/emojis/3/grapes.pngbin0 -> 5191 bytes
-rw-r--r--public/-/emojis/3/green_apple.pngbin0 -> 3338 bytes
-rw-r--r--public/-/emojis/3/green_book.pngbin0 -> 1113 bytes
-rw-r--r--public/-/emojis/3/green_heart.pngbin0 -> 2929 bytes
-rw-r--r--public/-/emojis/3/grey_exclamation.pngbin0 -> 1314 bytes
-rw-r--r--public/-/emojis/3/grey_question.pngbin0 -> 2364 bytes
-rw-r--r--public/-/emojis/3/grimacing.pngbin0 -> 4875 bytes
-rw-r--r--public/-/emojis/3/grin.pngbin0 -> 5219 bytes
-rw-r--r--public/-/emojis/3/grinning.pngbin0 -> 5239 bytes
-rw-r--r--public/-/emojis/3/guardsman.pngbin0 -> 3910 bytes
-rw-r--r--public/-/emojis/3/guardsman_tone1.pngbin0 -> 4024 bytes
-rw-r--r--public/-/emojis/3/guardsman_tone2.pngbin0 -> 4001 bytes
-rw-r--r--public/-/emojis/3/guardsman_tone3.pngbin0 -> 3884 bytes
-rw-r--r--public/-/emojis/3/guardsman_tone4.pngbin0 -> 3777 bytes
-rw-r--r--public/-/emojis/3/guardsman_tone5.pngbin0 -> 3726 bytes
-rw-r--r--public/-/emojis/3/guitar.pngbin0 -> 4627 bytes
-rw-r--r--public/-/emojis/3/gun.pngbin0 -> 2282 bytes
-rw-r--r--public/-/emojis/3/haircut.pngbin0 -> 5526 bytes
-rw-r--r--public/-/emojis/3/haircut_tone1.pngbin0 -> 5692 bytes
-rw-r--r--public/-/emojis/3/haircut_tone2.pngbin0 -> 5496 bytes
-rw-r--r--public/-/emojis/3/haircut_tone3.pngbin0 -> 5453 bytes
-rw-r--r--public/-/emojis/3/haircut_tone4.pngbin0 -> 5330 bytes
-rw-r--r--public/-/emojis/3/haircut_tone5.pngbin0 -> 5415 bytes
-rw-r--r--public/-/emojis/3/hamburger.pngbin0 -> 6559 bytes
-rw-r--r--public/-/emojis/3/hammer.pngbin0 -> 2848 bytes
-rw-r--r--public/-/emojis/3/hammer_pick.pngbin0 -> 4505 bytes
-rw-r--r--public/-/emojis/3/hamster.pngbin0 -> 4285 bytes
-rw-r--r--public/-/emojis/3/hand_splayed.pngbin0 -> 3638 bytes
-rw-r--r--public/-/emojis/3/hand_splayed_tone1.pngbin0 -> 3809 bytes
-rw-r--r--public/-/emojis/3/hand_splayed_tone2.pngbin0 -> 3826 bytes
-rw-r--r--public/-/emojis/3/hand_splayed_tone3.pngbin0 -> 3842 bytes
-rw-r--r--public/-/emojis/3/hand_splayed_tone4.pngbin0 -> 3569 bytes
-rw-r--r--public/-/emojis/3/hand_splayed_tone5.pngbin0 -> 3589 bytes
-rw-r--r--public/-/emojis/3/handbag.pngbin0 -> 3952 bytes
-rw-r--r--public/-/emojis/3/handball.pngbin0 -> 4367 bytes
-rw-r--r--public/-/emojis/3/handball_tone1.pngbin0 -> 4413 bytes
-rw-r--r--public/-/emojis/3/handball_tone2.pngbin0 -> 4395 bytes
-rw-r--r--public/-/emojis/3/handball_tone3.pngbin0 -> 4363 bytes
-rw-r--r--public/-/emojis/3/handball_tone4.pngbin0 -> 4386 bytes
-rw-r--r--public/-/emojis/3/handball_tone5.pngbin0 -> 4440 bytes
-rw-r--r--public/-/emojis/3/handshake.pngbin0 -> 3653 bytes
-rw-r--r--public/-/emojis/3/handshake_tone1.pngbin0 -> 3974 bytes
-rw-r--r--public/-/emojis/3/handshake_tone2.pngbin0 -> 3984 bytes
-rw-r--r--public/-/emojis/3/handshake_tone3.pngbin0 -> 3872 bytes
-rw-r--r--public/-/emojis/3/handshake_tone4.pngbin0 -> 3532 bytes
-rw-r--r--public/-/emojis/3/handshake_tone5.pngbin0 -> 3738 bytes
-rw-r--r--public/-/emojis/3/hash.pngbin0 -> 2614 bytes
-rw-r--r--public/-/emojis/3/hatched_chick.pngbin0 -> 2478 bytes
-rw-r--r--public/-/emojis/3/hatching_chick.pngbin0 -> 3130 bytes
-rw-r--r--public/-/emojis/3/head_bandage.pngbin0 -> 5477 bytes
-rw-r--r--public/-/emojis/3/headphones.pngbin0 -> 4072 bytes
-rw-r--r--public/-/emojis/3/hear_no_evil.pngbin0 -> 3931 bytes
-rw-r--r--public/-/emojis/3/heart.pngbin0 -> 2771 bytes
-rw-r--r--public/-/emojis/3/heart_decoration.pngbin0 -> 2795 bytes
-rw-r--r--public/-/emojis/3/heart_exclamation.pngbin0 -> 2855 bytes
-rw-r--r--public/-/emojis/3/heart_eyes.pngbin0 -> 5144 bytes
-rw-r--r--public/-/emojis/3/heart_eyes_cat.pngbin0 -> 5410 bytes
-rw-r--r--public/-/emojis/3/heartbeat.pngbin0 -> 4506 bytes
-rw-r--r--public/-/emojis/3/heartpulse.pngbin0 -> 4016 bytes
-rw-r--r--public/-/emojis/3/hearts.pngbin0 -> 2209 bytes
-rw-r--r--public/-/emojis/3/heavy_check_mark.pngbin0 -> 2537 bytes
-rw-r--r--public/-/emojis/3/heavy_division_sign.pngbin0 -> 2077 bytes
-rw-r--r--public/-/emojis/3/heavy_dollar_sign.pngbin0 -> 3480 bytes
-rw-r--r--public/-/emojis/3/heavy_minus_sign.pngbin0 -> 661 bytes
-rw-r--r--public/-/emojis/3/heavy_multiplication_x.pngbin0 -> 3621 bytes
-rw-r--r--public/-/emojis/3/heavy_plus_sign.pngbin0 -> 1369 bytes
-rw-r--r--public/-/emojis/3/helicopter.pngbin0 -> 3722 bytes
-rw-r--r--public/-/emojis/3/helmet_with_cross.pngbin0 -> 3954 bytes
-rw-r--r--public/-/emojis/3/herb.pngbin0 -> 3941 bytes
-rw-r--r--public/-/emojis/3/hibiscus.pngbin0 -> 4720 bytes
-rw-r--r--public/-/emojis/3/high_brightness.pngbin0 -> 4362 bytes
-rw-r--r--public/-/emojis/3/high_heel.pngbin0 -> 3721 bytes
-rw-r--r--public/-/emojis/3/hockey.pngbin0 -> 2532 bytes
-rw-r--r--public/-/emojis/3/hole.pngbin0 -> 2503 bytes
-rw-r--r--public/-/emojis/3/homes.pngbin0 -> 4621 bytes
-rw-r--r--public/-/emojis/3/honey_pot.pngbin0 -> 3772 bytes
-rw-r--r--public/-/emojis/3/horse.pngbin0 -> 3539 bytes
-rw-r--r--public/-/emojis/3/horse_racing.pngbin0 -> 5233 bytes
-rw-r--r--public/-/emojis/3/horse_racing_tone1.pngbin0 -> 5275 bytes
-rw-r--r--public/-/emojis/3/horse_racing_tone2.pngbin0 -> 5251 bytes
-rw-r--r--public/-/emojis/3/horse_racing_tone3.pngbin0 -> 5249 bytes
-rw-r--r--public/-/emojis/3/horse_racing_tone4.pngbin0 -> 5216 bytes
-rw-r--r--public/-/emojis/3/horse_racing_tone5.pngbin0 -> 5218 bytes
-rw-r--r--public/-/emojis/3/hospital.pngbin0 -> 3394 bytes
-rw-r--r--public/-/emojis/3/hot_pepper.pngbin0 -> 2777 bytes
-rw-r--r--public/-/emojis/3/hotdog.pngbin0 -> 3562 bytes
-rw-r--r--public/-/emojis/3/hotel.pngbin0 -> 4353 bytes
-rw-r--r--public/-/emojis/3/hotsprings.pngbin0 -> 4638 bytes
-rw-r--r--public/-/emojis/3/hourglass.pngbin0 -> 3644 bytes
-rw-r--r--public/-/emojis/3/hourglass_flowing_sand.pngbin0 -> 4241 bytes
-rw-r--r--public/-/emojis/3/house.pngbin0 -> 4321 bytes
-rw-r--r--public/-/emojis/3/house_abandoned.pngbin0 -> 5488 bytes
-rw-r--r--public/-/emojis/3/house_with_garden.pngbin0 -> 4590 bytes
-rw-r--r--public/-/emojis/3/hugging.pngbin0 -> 6431 bytes
-rw-r--r--public/-/emojis/3/hushed.pngbin0 -> 4789 bytes
-rw-r--r--public/-/emojis/3/ice_cream.pngbin0 -> 4582 bytes
-rw-r--r--public/-/emojis/3/ice_skate.pngbin0 -> 4210 bytes
-rw-r--r--public/-/emojis/3/icecream.pngbin0 -> 3187 bytes
-rw-r--r--public/-/emojis/3/id.pngbin0 -> 2064 bytes
-rw-r--r--public/-/emojis/3/ideograph_advantage.pngbin0 -> 4578 bytes
-rw-r--r--public/-/emojis/3/imp.pngbin0 -> 5369 bytes
-rw-r--r--public/-/emojis/3/inbox_tray.pngbin0 -> 2325 bytes
-rw-r--r--public/-/emojis/3/incoming_envelope.pngbin0 -> 2582 bytes
-rw-r--r--public/-/emojis/3/information_desk_person.pngbin0 -> 5060 bytes
-rw-r--r--public/-/emojis/3/information_desk_person_tone1.pngbin0 -> 5246 bytes
-rw-r--r--public/-/emojis/3/information_desk_person_tone2.pngbin0 -> 4994 bytes
-rw-r--r--public/-/emojis/3/information_desk_person_tone3.pngbin0 -> 4974 bytes
-rw-r--r--public/-/emojis/3/information_desk_person_tone4.pngbin0 -> 4804 bytes
-rw-r--r--public/-/emojis/3/information_desk_person_tone5.pngbin0 -> 4824 bytes
-rw-r--r--public/-/emojis/3/information_source.pngbin0 -> 1537 bytes
-rw-r--r--public/-/emojis/3/innocent.pngbin0 -> 5344 bytes
-rw-r--r--public/-/emojis/3/interrobang.pngbin0 -> 3312 bytes
-rw-r--r--public/-/emojis/3/iphone.pngbin0 -> 2704 bytes
-rw-r--r--public/-/emojis/3/island.pngbin0 -> 4381 bytes
-rw-r--r--public/-/emojis/3/izakaya_lantern.pngbin0 -> 4639 bytes
-rw-r--r--public/-/emojis/3/jack_o_lantern.pngbin0 -> 4353 bytes
-rw-r--r--public/-/emojis/3/japan.pngbin0 -> 3058 bytes
-rw-r--r--public/-/emojis/3/japanese_castle.pngbin0 -> 6472 bytes
-rw-r--r--public/-/emojis/3/japanese_goblin.pngbin0 -> 6629 bytes
-rw-r--r--public/-/emojis/3/japanese_ogre.pngbin0 -> 7269 bytes
-rw-r--r--public/-/emojis/3/jeans.pngbin0 -> 2844 bytes
-rw-r--r--public/-/emojis/3/joy.pngbin0 -> 6101 bytes
-rw-r--r--public/-/emojis/3/joy_cat.pngbin0 -> 5353 bytes
-rw-r--r--public/-/emojis/3/joystick.pngbin0 -> 3192 bytes
-rw-r--r--public/-/emojis/3/juggling.pngbin0 -> 5236 bytes
-rw-r--r--public/-/emojis/3/juggling_tone1.pngbin0 -> 5329 bytes
-rw-r--r--public/-/emojis/3/juggling_tone2.pngbin0 -> 5222 bytes
-rw-r--r--public/-/emojis/3/juggling_tone3.pngbin0 -> 5193 bytes
-rw-r--r--public/-/emojis/3/juggling_tone4.pngbin0 -> 5139 bytes
-rw-r--r--public/-/emojis/3/juggling_tone5.pngbin0 -> 5127 bytes
-rw-r--r--public/-/emojis/3/kaaba.pngbin0 -> 3359 bytes
-rw-r--r--public/-/emojis/3/key.pngbin0 -> 3290 bytes
-rw-r--r--public/-/emojis/3/key2.pngbin0 -> 3953 bytes
-rw-r--r--public/-/emojis/3/keyboard.pngbin0 -> 3239 bytes
-rw-r--r--public/-/emojis/3/kimono.pngbin0 -> 6336 bytes
-rw-r--r--public/-/emojis/3/kiss.pngbin0 -> 1994 bytes
-rw-r--r--public/-/emojis/3/kiss_mm.pngbin0 -> 5116 bytes
-rw-r--r--public/-/emojis/3/kiss_ww.pngbin0 -> 5843 bytes
-rw-r--r--public/-/emojis/3/kissing.pngbin0 -> 4631 bytes
-rw-r--r--public/-/emojis/3/kissing_cat.pngbin0 -> 4854 bytes
-rw-r--r--public/-/emojis/3/kissing_closed_eyes.pngbin0 -> 5482 bytes
-rw-r--r--public/-/emojis/3/kissing_heart.pngbin0 -> 5214 bytes
-rw-r--r--public/-/emojis/3/kissing_smiling_eyes.pngbin0 -> 4661 bytes
-rw-r--r--public/-/emojis/3/kiwi.pngbin0 -> 6140 bytes
-rw-r--r--public/-/emojis/3/knife.pngbin0 -> 2527 bytes
-rw-r--r--public/-/emojis/3/koala.pngbin0 -> 3886 bytes
-rw-r--r--public/-/emojis/3/koko.pngbin0 -> 1734 bytes
-rw-r--r--public/-/emojis/3/label.pngbin0 -> 3461 bytes
-rw-r--r--public/-/emojis/3/large_blue_circle.pngbin0 -> 2907 bytes
-rw-r--r--public/-/emojis/3/large_blue_diamond.pngbin0 -> 2163 bytes
-rw-r--r--public/-/emojis/3/large_orange_diamond.pngbin0 -> 1856 bytes
-rw-r--r--public/-/emojis/3/last_quarter_moon.pngbin0 -> 3858 bytes
-rw-r--r--public/-/emojis/3/last_quarter_moon_with_face.pngbin0 -> 3463 bytes
-rw-r--r--public/-/emojis/3/laughing.pngbin0 -> 5571 bytes
-rw-r--r--public/-/emojis/3/leaves.pngbin0 -> 2772 bytes
-rw-r--r--public/-/emojis/3/ledger.pngbin0 -> 2798 bytes
-rw-r--r--public/-/emojis/3/left_facing_fist.pngbin0 -> 2921 bytes
-rw-r--r--public/-/emojis/3/left_facing_fist_tone1.pngbin0 -> 3218 bytes
-rw-r--r--public/-/emojis/3/left_facing_fist_tone2.pngbin0 -> 3382 bytes
-rw-r--r--public/-/emojis/3/left_facing_fist_tone3.pngbin0 -> 3153 bytes
-rw-r--r--public/-/emojis/3/left_facing_fist_tone4.pngbin0 -> 3051 bytes
-rw-r--r--public/-/emojis/3/left_facing_fist_tone5.pngbin0 -> 3079 bytes
-rw-r--r--public/-/emojis/3/left_luggage.pngbin0 -> 2814 bytes
-rw-r--r--public/-/emojis/3/left_right_arrow.pngbin0 -> 2108 bytes
-rw-r--r--public/-/emojis/3/leftwards_arrow_with_hook.pngbin0 -> 2369 bytes
-rw-r--r--public/-/emojis/3/lemon.pngbin0 -> 2862 bytes
-rw-r--r--public/-/emojis/3/leo.pngbin0 -> 4555 bytes
-rw-r--r--public/-/emojis/3/leopard.pngbin0 -> 5202 bytes
-rw-r--r--public/-/emojis/3/level_slider.pngbin0 -> 2629 bytes
-rw-r--r--public/-/emojis/3/levitate.pngbin0 -> 2902 bytes
-rw-r--r--public/-/emojis/3/libra.pngbin0 -> 4663 bytes
-rw-r--r--public/-/emojis/3/lifter.pngbin0 -> 4338 bytes
-rw-r--r--public/-/emojis/3/lifter_tone1.pngbin0 -> 4417 bytes
-rw-r--r--public/-/emojis/3/lifter_tone2.pngbin0 -> 4380 bytes
-rw-r--r--public/-/emojis/3/lifter_tone3.pngbin0 -> 4345 bytes
-rw-r--r--public/-/emojis/3/lifter_tone4.pngbin0 -> 4364 bytes
-rw-r--r--public/-/emojis/3/lifter_tone5.pngbin0 -> 4354 bytes
-rw-r--r--public/-/emojis/3/light_rail.pngbin0 -> 3096 bytes
-rw-r--r--public/-/emojis/3/link.pngbin0 -> 3539 bytes
-rw-r--r--public/-/emojis/3/lion_face.pngbin0 -> 4220 bytes
-rw-r--r--public/-/emojis/3/lips.pngbin0 -> 2561 bytes
-rw-r--r--public/-/emojis/3/lipstick.pngbin0 -> 2562 bytes
-rw-r--r--public/-/emojis/3/lizard.pngbin0 -> 4208 bytes
-rw-r--r--public/-/emojis/3/lock.pngbin0 -> 2401 bytes
-rw-r--r--public/-/emojis/3/lock_with_ink_pen.pngbin0 -> 4901 bytes
-rw-r--r--public/-/emojis/3/lollipop.pngbin0 -> 6431 bytes
-rw-r--r--public/-/emojis/3/loop.pngbin0 -> 4041 bytes
-rw-r--r--public/-/emojis/3/loud_sound.pngbin0 -> 5112 bytes
-rw-r--r--public/-/emojis/3/loudspeaker.pngbin0 -> 4557 bytes
-rw-r--r--public/-/emojis/3/love_hotel.pngbin0 -> 4943 bytes
-rw-r--r--public/-/emojis/3/love_letter.pngbin0 -> 2802 bytes
-rw-r--r--public/-/emojis/3/low_brightness.pngbin0 -> 3767 bytes
-rw-r--r--public/-/emojis/3/lying_face.pngbin0 -> 4932 bytes
-rw-r--r--public/-/emojis/3/m.pngbin0 -> 4118 bytes
-rw-r--r--public/-/emojis/3/mag.pngbin0 -> 4524 bytes
-rw-r--r--public/-/emojis/3/mag_right.pngbin0 -> 4430 bytes
-rw-r--r--public/-/emojis/3/mahjong.pngbin0 -> 2833 bytes
-rw-r--r--public/-/emojis/3/mailbox.pngbin0 -> 2341 bytes
-rw-r--r--public/-/emojis/3/mailbox_closed.pngbin0 -> 2162 bytes
-rw-r--r--public/-/emojis/3/mailbox_with_mail.pngbin0 -> 3167 bytes
-rw-r--r--public/-/emojis/3/mailbox_with_no_mail.pngbin0 -> 2159 bytes
-rw-r--r--public/-/emojis/3/man.pngbin0 -> 3977 bytes
-rw-r--r--public/-/emojis/3/man_dancing.pngbin0 -> 3863 bytes
-rw-r--r--public/-/emojis/3/man_dancing_tone1.pngbin0 -> 3862 bytes
-rw-r--r--public/-/emojis/3/man_dancing_tone2.pngbin0 -> 3871 bytes
-rw-r--r--public/-/emojis/3/man_dancing_tone3.pngbin0 -> 3872 bytes
-rw-r--r--public/-/emojis/3/man_dancing_tone4.pngbin0 -> 3833 bytes
-rw-r--r--public/-/emojis/3/man_dancing_tone5.pngbin0 -> 3870 bytes
-rw-r--r--public/-/emojis/3/man_in_tuxedo.pngbin0 -> 5576 bytes
-rw-r--r--public/-/emojis/3/man_in_tuxedo_tone1.pngbin0 -> 5679 bytes
-rw-r--r--public/-/emojis/3/man_in_tuxedo_tone2.pngbin0 -> 5524 bytes
-rw-r--r--public/-/emojis/3/man_in_tuxedo_tone3.pngbin0 -> 5439 bytes
-rw-r--r--public/-/emojis/3/man_in_tuxedo_tone4.pngbin0 -> 5358 bytes
-rw-r--r--public/-/emojis/3/man_in_tuxedo_tone5.pngbin0 -> 5379 bytes
-rw-r--r--public/-/emojis/3/man_tone1.pngbin0 -> 4081 bytes
-rw-r--r--public/-/emojis/3/man_tone2.pngbin0 -> 3984 bytes
-rw-r--r--public/-/emojis/3/man_tone3.pngbin0 -> 3858 bytes
-rw-r--r--public/-/emojis/3/man_tone4.pngbin0 -> 3681 bytes
-rw-r--r--public/-/emojis/3/man_tone5.pngbin0 -> 3730 bytes
-rw-r--r--public/-/emojis/3/man_with_gua_pi_mao.pngbin0 -> 4881 bytes
-rw-r--r--public/-/emojis/3/man_with_gua_pi_mao_tone1.pngbin0 -> 5086 bytes
-rw-r--r--public/-/emojis/3/man_with_gua_pi_mao_tone2.pngbin0 -> 4860 bytes
-rw-r--r--public/-/emojis/3/man_with_gua_pi_mao_tone3.pngbin0 -> 4779 bytes
-rw-r--r--public/-/emojis/3/man_with_gua_pi_mao_tone4.pngbin0 -> 4689 bytes
-rw-r--r--public/-/emojis/3/man_with_gua_pi_mao_tone5.pngbin0 -> 4689 bytes
-rw-r--r--public/-/emojis/3/man_with_turban.pngbin0 -> 5188 bytes
-rw-r--r--public/-/emojis/3/man_with_turban_tone1.pngbin0 -> 5267 bytes
-rw-r--r--public/-/emojis/3/man_with_turban_tone2.pngbin0 -> 4987 bytes
-rw-r--r--public/-/emojis/3/man_with_turban_tone3.pngbin0 -> 5024 bytes
-rw-r--r--public/-/emojis/3/man_with_turban_tone4.pngbin0 -> 5003 bytes
-rw-r--r--public/-/emojis/3/man_with_turban_tone5.pngbin0 -> 5022 bytes
-rw-r--r--public/-/emojis/3/mans_shoe.pngbin0 -> 2850 bytes
-rw-r--r--public/-/emojis/3/map.pngbin0 -> 5412 bytes
-rw-r--r--public/-/emojis/3/maple_leaf.pngbin0 -> 4187 bytes
-rw-r--r--public/-/emojis/3/martial_arts_uniform.pngbin0 -> 6113 bytes
-rw-r--r--public/-/emojis/3/mask.pngbin0 -> 4679 bytes
-rw-r--r--public/-/emojis/3/massage.pngbin0 -> 4900 bytes
-rw-r--r--public/-/emojis/3/massage_tone1.pngbin0 -> 5194 bytes
-rw-r--r--public/-/emojis/3/massage_tone2.pngbin0 -> 4766 bytes
-rw-r--r--public/-/emojis/3/massage_tone3.pngbin0 -> 4834 bytes
-rw-r--r--public/-/emojis/3/massage_tone4.pngbin0 -> 4717 bytes
-rw-r--r--public/-/emojis/3/massage_tone5.pngbin0 -> 4693 bytes
-rw-r--r--public/-/emojis/3/meat_on_bone.pngbin0 -> 4272 bytes
-rw-r--r--public/-/emojis/3/medal.pngbin0 -> 3268 bytes
-rw-r--r--public/-/emojis/3/mega.pngbin0 -> 4291 bytes
-rw-r--r--public/-/emojis/3/melon.pngbin0 -> 6709 bytes
-rw-r--r--public/-/emojis/3/menorah.pngbin0 -> 5397 bytes
-rw-r--r--public/-/emojis/3/mens.pngbin0 -> 2254 bytes
-rw-r--r--public/-/emojis/3/metal.pngbin0 -> 3185 bytes
-rw-r--r--public/-/emojis/3/metal_tone1.pngbin0 -> 3404 bytes
-rw-r--r--public/-/emojis/3/metal_tone2.pngbin0 -> 3452 bytes
-rw-r--r--public/-/emojis/3/metal_tone3.pngbin0 -> 3389 bytes
-rw-r--r--public/-/emojis/3/metal_tone4.pngbin0 -> 3047 bytes
-rw-r--r--public/-/emojis/3/metal_tone5.pngbin0 -> 3244 bytes
-rw-r--r--public/-/emojis/3/metro.pngbin0 -> 5833 bytes
-rw-r--r--public/-/emojis/3/microphone.pngbin0 -> 3348 bytes
-rw-r--r--public/-/emojis/3/microphone2.pngbin0 -> 5368 bytes
-rw-r--r--public/-/emojis/3/microscope.pngbin0 -> 4623 bytes
-rw-r--r--public/-/emojis/3/middle_finger.pngbin0 -> 2086 bytes
-rw-r--r--public/-/emojis/3/middle_finger_tone1.pngbin0 -> 2282 bytes
-rw-r--r--public/-/emojis/3/middle_finger_tone2.pngbin0 -> 2266 bytes
-rw-r--r--public/-/emojis/3/middle_finger_tone3.pngbin0 -> 2252 bytes
-rw-r--r--public/-/emojis/3/middle_finger_tone4.pngbin0 -> 2135 bytes
-rw-r--r--public/-/emojis/3/middle_finger_tone5.pngbin0 -> 2111 bytes
-rw-r--r--public/-/emojis/3/military_medal.pngbin0 -> 2798 bytes
-rw-r--r--public/-/emojis/3/milk.pngbin0 -> 3379 bytes
-rw-r--r--public/-/emojis/3/milky_way.pngbin0 -> 6311 bytes
-rw-r--r--public/-/emojis/3/minibus.pngbin0 -> 3124 bytes
-rw-r--r--public/-/emojis/3/minidisc.pngbin0 -> 5502 bytes
-rw-r--r--public/-/emojis/3/mobile_phone_off.pngbin0 -> 2710 bytes
-rw-r--r--public/-/emojis/3/money_mouth.pngbin0 -> 5760 bytes
-rw-r--r--public/-/emojis/3/money_with_wings.pngbin0 -> 5929 bytes
-rw-r--r--public/-/emojis/3/moneybag.pngbin0 -> 3729 bytes
-rw-r--r--public/-/emojis/3/monkey.pngbin0 -> 4705 bytes
-rw-r--r--public/-/emojis/3/monkey_face.pngbin0 -> 3541 bytes
-rw-r--r--public/-/emojis/3/monorail.pngbin0 -> 3324 bytes
-rw-r--r--public/-/emojis/3/mortar_board.pngbin0 -> 3435 bytes
-rw-r--r--public/-/emojis/3/mosque.pngbin0 -> 4307 bytes
-rw-r--r--public/-/emojis/3/motor_scooter.pngbin0 -> 4661 bytes
-rw-r--r--public/-/emojis/3/motorboat.pngbin0 -> 3003 bytes
-rw-r--r--public/-/emojis/3/motorcycle.pngbin0 -> 4778 bytes
-rw-r--r--public/-/emojis/3/motorway.pngbin0 -> 6009 bytes
-rw-r--r--public/-/emojis/3/mount_fuji.pngbin0 -> 3436 bytes
-rw-r--r--public/-/emojis/3/mountain.pngbin0 -> 4169 bytes
-rw-r--r--public/-/emojis/3/mountain_bicyclist.pngbin0 -> 6246 bytes
-rw-r--r--public/-/emojis/3/mountain_bicyclist_tone1.pngbin0 -> 6255 bytes
-rw-r--r--public/-/emojis/3/mountain_bicyclist_tone2.pngbin0 -> 6181 bytes
-rw-r--r--public/-/emojis/3/mountain_bicyclist_tone3.pngbin0 -> 6168 bytes
-rw-r--r--public/-/emojis/3/mountain_bicyclist_tone4.pngbin0 -> 6198 bytes
-rw-r--r--public/-/emojis/3/mountain_bicyclist_tone5.pngbin0 -> 6243 bytes
-rw-r--r--public/-/emojis/3/mountain_cableway.pngbin0 -> 3797 bytes
-rw-r--r--public/-/emojis/3/mountain_railway.pngbin0 -> 5247 bytes
-rw-r--r--public/-/emojis/3/mountain_snow.pngbin0 -> 4437 bytes
-rw-r--r--public/-/emojis/3/mouse.pngbin0 -> 3427 bytes
-rw-r--r--public/-/emojis/3/mouse2.pngbin0 -> 3171 bytes
-rw-r--r--public/-/emojis/3/mouse_three_button.pngbin0 -> 2529 bytes
-rw-r--r--public/-/emojis/3/movie_camera.pngbin0 -> 3494 bytes
-rw-r--r--public/-/emojis/3/moyai.pngbin0 -> 4229 bytes
-rw-r--r--public/-/emojis/3/mrs_claus.pngbin0 -> 5177 bytes
-rw-r--r--public/-/emojis/3/mrs_claus_tone1.pngbin0 -> 5073 bytes
-rw-r--r--public/-/emojis/3/mrs_claus_tone2.pngbin0 -> 5088 bytes
-rw-r--r--public/-/emojis/3/mrs_claus_tone3.pngbin0 -> 5135 bytes
-rw-r--r--public/-/emojis/3/mrs_claus_tone4.pngbin0 -> 5167 bytes
-rw-r--r--public/-/emojis/3/mrs_claus_tone5.pngbin0 -> 5262 bytes
-rw-r--r--public/-/emojis/3/muscle.pngbin0 -> 3278 bytes
-rw-r--r--public/-/emojis/3/muscle_tone1.pngbin0 -> 3499 bytes
-rw-r--r--public/-/emojis/3/muscle_tone2.pngbin0 -> 3604 bytes
-rw-r--r--public/-/emojis/3/muscle_tone3.pngbin0 -> 3520 bytes
-rw-r--r--public/-/emojis/3/muscle_tone4.pngbin0 -> 3247 bytes
-rw-r--r--public/-/emojis/3/muscle_tone5.pngbin0 -> 3411 bytes
-rw-r--r--public/-/emojis/3/mushroom.pngbin0 -> 3692 bytes
-rw-r--r--public/-/emojis/3/musical_keyboard.pngbin0 -> 1637 bytes
-rw-r--r--public/-/emojis/3/musical_note.pngbin0 -> 1158 bytes
-rw-r--r--public/-/emojis/3/musical_score.pngbin0 -> 2446 bytes
-rw-r--r--public/-/emojis/3/mute.pngbin0 -> 5449 bytes
-rw-r--r--public/-/emojis/3/nail_care.pngbin0 -> 5573 bytes
-rw-r--r--public/-/emojis/3/nail_care_tone1.pngbin0 -> 5858 bytes
-rw-r--r--public/-/emojis/3/nail_care_tone2.pngbin0 -> 5766 bytes
-rw-r--r--public/-/emojis/3/nail_care_tone3.pngbin0 -> 5589 bytes
-rw-r--r--public/-/emojis/3/nail_care_tone4.pngbin0 -> 5449 bytes
-rw-r--r--public/-/emojis/3/nail_care_tone5.pngbin0 -> 5477 bytes
-rw-r--r--public/-/emojis/3/name_badge.pngbin0 -> 3595 bytes
-rw-r--r--public/-/emojis/3/nauseated_face.pngbin0 -> 5393 bytes
-rw-r--r--public/-/emojis/3/necktie.pngbin0 -> 3757 bytes
-rw-r--r--public/-/emojis/3/negative_squared_cross_mark.pngbin0 -> 3087 bytes
-rw-r--r--public/-/emojis/3/nerd.pngbin0 -> 6275 bytes
-rw-r--r--public/-/emojis/3/neutral_face.pngbin0 -> 4235 bytes
-rw-r--r--public/-/emojis/3/new.pngbin0 -> 2672 bytes
-rw-r--r--public/-/emojis/3/new_moon.pngbin0 -> 3594 bytes
-rw-r--r--public/-/emojis/3/new_moon_with_face.pngbin0 -> 5492 bytes
-rw-r--r--public/-/emojis/3/newspaper.pngbin0 -> 2911 bytes
-rw-r--r--public/-/emojis/3/newspaper2.pngbin0 -> 5370 bytes
-rw-r--r--public/-/emojis/3/ng.pngbin0 -> 2864 bytes
-rw-r--r--public/-/emojis/3/night_with_stars.pngbin0 -> 6402 bytes
-rw-r--r--public/-/emojis/3/nine.pngbin0 -> 2515 bytes
-rw-r--r--public/-/emojis/3/no_bell.pngbin0 -> 6195 bytes
-rw-r--r--public/-/emojis/3/no_bicycles.pngbin0 -> 6930 bytes
-rw-r--r--public/-/emojis/3/no_entry.pngbin0 -> 2993 bytes
-rw-r--r--public/-/emojis/3/no_entry_sign.pngbin0 -> 3714 bytes
-rw-r--r--public/-/emojis/3/no_good.pngbin0 -> 5141 bytes
-rw-r--r--public/-/emojis/3/no_good_tone1.pngbin0 -> 5264 bytes
-rw-r--r--public/-/emojis/3/no_good_tone2.pngbin0 -> 5245 bytes
-rw-r--r--public/-/emojis/3/no_good_tone3.pngbin0 -> 5172 bytes
-rw-r--r--public/-/emojis/3/no_good_tone4.pngbin0 -> 5049 bytes
-rw-r--r--public/-/emojis/3/no_good_tone5.pngbin0 -> 5141 bytes
-rw-r--r--public/-/emojis/3/no_mobile_phones.pngbin0 -> 5254 bytes
-rw-r--r--public/-/emojis/3/no_mouth.pngbin0 -> 4108 bytes
-rw-r--r--public/-/emojis/3/no_pedestrians.pngbin0 -> 6025 bytes
-rw-r--r--public/-/emojis/3/no_smoking.pngbin0 -> 5308 bytes
-rw-r--r--public/-/emojis/3/non-potable_water.pngbin0 -> 6044 bytes
-rw-r--r--public/-/emojis/3/nose.pngbin0 -> 2560 bytes
-rw-r--r--public/-/emojis/3/nose_tone1.pngbin0 -> 2719 bytes
-rw-r--r--public/-/emojis/3/nose_tone2.pngbin0 -> 2655 bytes
-rw-r--r--public/-/emojis/3/nose_tone3.pngbin0 -> 2686 bytes
-rw-r--r--public/-/emojis/3/nose_tone4.pngbin0 -> 2527 bytes
-rw-r--r--public/-/emojis/3/nose_tone5.pngbin0 -> 2584 bytes
-rw-r--r--public/-/emojis/3/notebook.pngbin0 -> 6651 bytes
-rw-r--r--public/-/emojis/3/notebook_with_decorative_cover.pngbin0 -> 1693 bytes
-rw-r--r--public/-/emojis/3/notepad_spiral.pngbin0 -> 3343 bytes
-rw-r--r--public/-/emojis/3/notes.pngbin0 -> 2021 bytes
-rw-r--r--public/-/emojis/3/nut_and_bolt.pngbin0 -> 4134 bytes
-rw-r--r--public/-/emojis/3/o.pngbin0 -> 3903 bytes
-rw-r--r--public/-/emojis/3/o2.pngbin0 -> 3178 bytes
-rw-r--r--public/-/emojis/3/ocean.pngbin0 -> 4573 bytes
-rw-r--r--public/-/emojis/3/octagonal_sign.pngbin0 -> 1433 bytes
-rw-r--r--public/-/emojis/3/octopus.pngbin0 -> 5073 bytes
-rw-r--r--public/-/emojis/3/oden.pngbin0 -> 4554 bytes
-rw-r--r--public/-/emojis/3/office.pngbin0 -> 3224 bytes
-rw-r--r--public/-/emojis/3/oil.pngbin0 -> 3648 bytes
-rw-r--r--public/-/emojis/3/ok.pngbin0 -> 3253 bytes
-rw-r--r--public/-/emojis/3/ok_hand.pngbin0 -> 3675 bytes
-rw-r--r--public/-/emojis/3/ok_hand_tone1.pngbin0 -> 3914 bytes
-rw-r--r--public/-/emojis/3/ok_hand_tone2.pngbin0 -> 4012 bytes
-rw-r--r--public/-/emojis/3/ok_hand_tone3.pngbin0 -> 4000 bytes
-rw-r--r--public/-/emojis/3/ok_hand_tone4.pngbin0 -> 3636 bytes
-rw-r--r--public/-/emojis/3/ok_hand_tone5.pngbin0 -> 3828 bytes
-rw-r--r--public/-/emojis/3/ok_woman.pngbin0 -> 5058 bytes
-rw-r--r--public/-/emojis/3/ok_woman_tone1.pngbin0 -> 5439 bytes
-rw-r--r--public/-/emojis/3/ok_woman_tone2.pngbin0 -> 4948 bytes
-rw-r--r--public/-/emojis/3/ok_woman_tone3.pngbin0 -> 5049 bytes
-rw-r--r--public/-/emojis/3/ok_woman_tone4.pngbin0 -> 4958 bytes
-rw-r--r--public/-/emojis/3/ok_woman_tone5.pngbin0 -> 4971 bytes
-rw-r--r--public/-/emojis/3/older_man.pngbin0 -> 4097 bytes
-rw-r--r--public/-/emojis/3/older_man_tone1.pngbin0 -> 4087 bytes
-rw-r--r--public/-/emojis/3/older_man_tone2.pngbin0 -> 4094 bytes
-rw-r--r--public/-/emojis/3/older_man_tone3.pngbin0 -> 4047 bytes
-rw-r--r--public/-/emojis/3/older_man_tone4.pngbin0 -> 3948 bytes
-rw-r--r--public/-/emojis/3/older_man_tone5.pngbin0 -> 4168 bytes
-rw-r--r--public/-/emojis/3/older_woman.pngbin0 -> 4684 bytes
-rw-r--r--public/-/emojis/3/older_woman_tone1.pngbin0 -> 4662 bytes
-rw-r--r--public/-/emojis/3/older_woman_tone2.pngbin0 -> 4584 bytes
-rw-r--r--public/-/emojis/3/older_woman_tone3.pngbin0 -> 4478 bytes
-rw-r--r--public/-/emojis/3/older_woman_tone4.pngbin0 -> 4414 bytes
-rw-r--r--public/-/emojis/3/older_woman_tone5.pngbin0 -> 4598 bytes
-rw-r--r--public/-/emojis/3/om_symbol.pngbin0 -> 4522 bytes
-rw-r--r--public/-/emojis/3/on.pngbin0 -> 3208 bytes
-rw-r--r--public/-/emojis/3/oncoming_automobile.pngbin0 -> 4047 bytes
-rw-r--r--public/-/emojis/3/oncoming_bus.pngbin0 -> 3702 bytes
-rw-r--r--public/-/emojis/3/oncoming_police_car.pngbin0 -> 4042 bytes
-rw-r--r--public/-/emojis/3/oncoming_taxi.pngbin0 -> 4096 bytes
-rw-r--r--public/-/emojis/3/one.pngbin0 -> 1464 bytes
-rw-r--r--public/-/emojis/3/open_file_folder.pngbin0 -> 1871 bytes
-rw-r--r--public/-/emojis/3/open_hands.pngbin0 -> 3865 bytes
-rw-r--r--public/-/emojis/3/open_hands_tone1.pngbin0 -> 4024 bytes
-rw-r--r--public/-/emojis/3/open_hands_tone2.pngbin0 -> 4190 bytes
-rw-r--r--public/-/emojis/3/open_hands_tone3.pngbin0 -> 4103 bytes
-rw-r--r--public/-/emojis/3/open_hands_tone4.pngbin0 -> 3659 bytes
-rw-r--r--public/-/emojis/3/open_hands_tone5.pngbin0 -> 3941 bytes
-rw-r--r--public/-/emojis/3/open_mouth.pngbin0 -> 4562 bytes
-rw-r--r--public/-/emojis/3/ophiuchus.pngbin0 -> 4242 bytes
-rw-r--r--public/-/emojis/3/orange_book.pngbin0 -> 1120 bytes
-rw-r--r--public/-/emojis/3/orthodox_cross.pngbin0 -> 2447 bytes
-rw-r--r--public/-/emojis/3/outbox_tray.pngbin0 -> 2302 bytes
-rw-r--r--public/-/emojis/3/owl.pngbin0 -> 5134 bytes
-rw-r--r--public/-/emojis/3/ox.pngbin0 -> 3627 bytes
-rw-r--r--public/-/emojis/3/package.pngbin0 -> 3114 bytes
-rw-r--r--public/-/emojis/3/page_facing_up.pngbin0 -> 1186 bytes
-rw-r--r--public/-/emojis/3/page_with_curl.pngbin0 -> 1207 bytes
-rw-r--r--public/-/emojis/3/pager.pngbin0 -> 3441 bytes
-rw-r--r--public/-/emojis/3/paintbrush.pngbin0 -> 2635 bytes
-rw-r--r--public/-/emojis/3/palm_tree.pngbin0 -> 3932 bytes
-rw-r--r--public/-/emojis/3/pancakes.pngbin0 -> 7014 bytes
-rw-r--r--public/-/emojis/3/panda_face.pngbin0 -> 5397 bytes
-rw-r--r--public/-/emojis/3/paperclip.pngbin0 -> 5483 bytes
-rw-r--r--public/-/emojis/3/paperclips.pngbin0 -> 6905 bytes
-rw-r--r--public/-/emojis/3/park.pngbin0 -> 6976 bytes
-rw-r--r--public/-/emojis/3/parking.pngbin0 -> 1911 bytes
-rw-r--r--public/-/emojis/3/part_alternation_mark.pngbin0 -> 3247 bytes
-rw-r--r--public/-/emojis/3/partly_sunny.pngbin0 -> 2686 bytes
-rw-r--r--public/-/emojis/3/passport_control.pngbin0 -> 3363 bytes
-rw-r--r--public/-/emojis/3/pause_button.pngbin0 -> 1129 bytes
-rw-r--r--public/-/emojis/3/peace.pngbin0 -> 3962 bytes
-rw-r--r--public/-/emojis/3/peach.pngbin0 -> 4907 bytes
-rw-r--r--public/-/emojis/3/peanuts.pngbin0 -> 3417 bytes
-rw-r--r--public/-/emojis/3/pear.pngbin0 -> 2445 bytes
-rw-r--r--public/-/emojis/3/pen_ballpoint.pngbin0 -> 3639 bytes
-rw-r--r--public/-/emojis/3/pen_fountain.pngbin0 -> 3827 bytes
-rw-r--r--public/-/emojis/3/pencil.pngbin0 -> 4593 bytes
-rw-r--r--public/-/emojis/3/pencil2.pngbin0 -> 3004 bytes
-rw-r--r--public/-/emojis/3/penguin.pngbin0 -> 3286 bytes
-rw-r--r--public/-/emojis/3/pensive.pngbin0 -> 4716 bytes
-rw-r--r--public/-/emojis/3/performing_arts.pngbin0 -> 4317 bytes
-rw-r--r--public/-/emojis/3/persevere.pngbin0 -> 5289 bytes
-rw-r--r--public/-/emojis/3/person_frowning.pngbin0 -> 4403 bytes
-rw-r--r--public/-/emojis/3/person_frowning_tone1.pngbin0 -> 4543 bytes
-rw-r--r--public/-/emojis/3/person_frowning_tone2.pngbin0 -> 4309 bytes
-rw-r--r--public/-/emojis/3/person_frowning_tone3.pngbin0 -> 4310 bytes
-rw-r--r--public/-/emojis/3/person_frowning_tone4.pngbin0 -> 4190 bytes
-rw-r--r--public/-/emojis/3/person_frowning_tone5.pngbin0 -> 4247 bytes
-rw-r--r--public/-/emojis/3/person_with_blond_hair.pngbin0 -> 3983 bytes
-rw-r--r--public/-/emojis/3/person_with_blond_hair_tone1.pngbin0 -> 4280 bytes
-rw-r--r--public/-/emojis/3/person_with_blond_hair_tone2.pngbin0 -> 4305 bytes
-rw-r--r--public/-/emojis/3/person_with_blond_hair_tone3.pngbin0 -> 4318 bytes
-rw-r--r--public/-/emojis/3/person_with_blond_hair_tone4.pngbin0 -> 4234 bytes
-rw-r--r--public/-/emojis/3/person_with_blond_hair_tone5.pngbin0 -> 4366 bytes
-rw-r--r--public/-/emojis/3/person_with_pouting_face.pngbin0 -> 4824 bytes
-rw-r--r--public/-/emojis/3/person_with_pouting_face_tone1.pngbin0 -> 5056 bytes
-rw-r--r--public/-/emojis/3/person_with_pouting_face_tone2.pngbin0 -> 4856 bytes
-rw-r--r--public/-/emojis/3/person_with_pouting_face_tone3.pngbin0 -> 4770 bytes
-rw-r--r--public/-/emojis/3/person_with_pouting_face_tone4.pngbin0 -> 4675 bytes
-rw-r--r--public/-/emojis/3/person_with_pouting_face_tone5.pngbin0 -> 4807 bytes
-rw-r--r--public/-/emojis/3/pick.pngbin0 -> 2789 bytes
-rw-r--r--public/-/emojis/3/pig.pngbin0 -> 3649 bytes
-rw-r--r--public/-/emojis/3/pig2.pngbin0 -> 3112 bytes
-rw-r--r--public/-/emojis/3/pig_nose.pngbin0 -> 2677 bytes
-rw-r--r--public/-/emojis/3/pill.pngbin0 -> 2705 bytes
-rw-r--r--public/-/emojis/3/pineapple.pngbin0 -> 3974 bytes
-rw-r--r--public/-/emojis/3/ping_pong.pngbin0 -> 2968 bytes
-rw-r--r--public/-/emojis/3/pisces.pngbin0 -> 4576 bytes
-rw-r--r--public/-/emojis/3/pizza.pngbin0 -> 4654 bytes
-rw-r--r--public/-/emojis/3/place_of_worship.pngbin0 -> 3414 bytes
-rw-r--r--public/-/emojis/3/play_pause.pngbin0 -> 1906 bytes
-rw-r--r--public/-/emojis/3/point_down.pngbin0 -> 2314 bytes
-rw-r--r--public/-/emojis/3/point_down_tone1.pngbin0 -> 2519 bytes
-rw-r--r--public/-/emojis/3/point_down_tone2.pngbin0 -> 2513 bytes
-rw-r--r--public/-/emojis/3/point_down_tone3.pngbin0 -> 2413 bytes
-rw-r--r--public/-/emojis/3/point_down_tone4.pngbin0 -> 2301 bytes
-rw-r--r--public/-/emojis/3/point_down_tone5.pngbin0 -> 2356 bytes
-rw-r--r--public/-/emojis/3/point_left.pngbin0 -> 2143 bytes
-rw-r--r--public/-/emojis/3/point_left_tone1.pngbin0 -> 2330 bytes
-rw-r--r--public/-/emojis/3/point_left_tone2.pngbin0 -> 2309 bytes
-rw-r--r--public/-/emojis/3/point_left_tone3.pngbin0 -> 2386 bytes
-rw-r--r--public/-/emojis/3/point_left_tone4.pngbin0 -> 2210 bytes
-rw-r--r--public/-/emojis/3/point_left_tone5.pngbin0 -> 2312 bytes
-rw-r--r--public/-/emojis/3/point_right.pngbin0 -> 2175 bytes
-rw-r--r--public/-/emojis/3/point_right_tone1.pngbin0 -> 2344 bytes
-rw-r--r--public/-/emojis/3/point_right_tone2.pngbin0 -> 2325 bytes
-rw-r--r--public/-/emojis/3/point_right_tone3.pngbin0 -> 2276 bytes
-rw-r--r--public/-/emojis/3/point_right_tone4.pngbin0 -> 2143 bytes
-rw-r--r--public/-/emojis/3/point_right_tone5.pngbin0 -> 2172 bytes
-rw-r--r--public/-/emojis/3/point_up.pngbin0 -> 3044 bytes
-rw-r--r--public/-/emojis/3/point_up_2.pngbin0 -> 2272 bytes
-rw-r--r--public/-/emojis/3/point_up_2_tone1.pngbin0 -> 2457 bytes
-rw-r--r--public/-/emojis/3/point_up_2_tone2.pngbin0 -> 2442 bytes
-rw-r--r--public/-/emojis/3/point_up_2_tone3.pngbin0 -> 2385 bytes
-rw-r--r--public/-/emojis/3/point_up_2_tone4.pngbin0 -> 2290 bytes
-rw-r--r--public/-/emojis/3/point_up_2_tone5.pngbin0 -> 2351 bytes
-rw-r--r--public/-/emojis/3/point_up_tone1.pngbin0 -> 3231 bytes
-rw-r--r--public/-/emojis/3/point_up_tone2.pngbin0 -> 3255 bytes
-rw-r--r--public/-/emojis/3/point_up_tone3.pngbin0 -> 3176 bytes
-rw-r--r--public/-/emojis/3/point_up_tone4.pngbin0 -> 2818 bytes
-rw-r--r--public/-/emojis/3/point_up_tone5.pngbin0 -> 3005 bytes
-rw-r--r--public/-/emojis/3/police_car.pngbin0 -> 3987 bytes
-rw-r--r--public/-/emojis/3/poodle.pngbin0 -> 4467 bytes
-rw-r--r--public/-/emojis/3/poop.pngbin0 -> 3452 bytes
-rw-r--r--public/-/emojis/3/popcorn.pngbin0 -> 5674 bytes
-rw-r--r--public/-/emojis/3/post_office.pngbin0 -> 2559 bytes
-rw-r--r--public/-/emojis/3/postal_horn.pngbin0 -> 3491 bytes
-rw-r--r--public/-/emojis/3/postbox.pngbin0 -> 2569 bytes
-rw-r--r--public/-/emojis/3/potable_water.pngbin0 -> 3165 bytes
-rw-r--r--public/-/emojis/3/potato.pngbin0 -> 3683 bytes
-rw-r--r--public/-/emojis/3/pouch.pngbin0 -> 2529 bytes
-rw-r--r--public/-/emojis/3/poultry_leg.pngbin0 -> 2702 bytes
-rw-r--r--public/-/emojis/3/pound.pngbin0 -> 2390 bytes
-rw-r--r--public/-/emojis/3/pouting_cat.pngbin0 -> 4361 bytes
-rw-r--r--public/-/emojis/3/pray.pngbin0 -> 3399 bytes
-rw-r--r--public/-/emojis/3/pray_tone1.pngbin0 -> 3406 bytes
-rw-r--r--public/-/emojis/3/pray_tone2.pngbin0 -> 3437 bytes
-rw-r--r--public/-/emojis/3/pray_tone3.pngbin0 -> 3309 bytes
-rw-r--r--public/-/emojis/3/pray_tone4.pngbin0 -> 3278 bytes
-rw-r--r--public/-/emojis/3/pray_tone5.pngbin0 -> 3332 bytes
-rw-r--r--public/-/emojis/3/prayer_beads.pngbin0 -> 5496 bytes
-rw-r--r--public/-/emojis/3/pregnant_woman.pngbin0 -> 3089 bytes
-rw-r--r--public/-/emojis/3/pregnant_woman_tone1.pngbin0 -> 3104 bytes
-rw-r--r--public/-/emojis/3/pregnant_woman_tone2.pngbin0 -> 3095 bytes
-rw-r--r--public/-/emojis/3/pregnant_woman_tone3.pngbin0 -> 3038 bytes
-rw-r--r--public/-/emojis/3/pregnant_woman_tone4.pngbin0 -> 2988 bytes
-rw-r--r--public/-/emojis/3/pregnant_woman_tone5.pngbin0 -> 3042 bytes
-rw-r--r--public/-/emojis/3/prince.pngbin0 -> 4988 bytes
-rw-r--r--public/-/emojis/3/prince_tone1.pngbin0 -> 5158 bytes
-rw-r--r--public/-/emojis/3/prince_tone2.pngbin0 -> 4951 bytes
-rw-r--r--public/-/emojis/3/prince_tone3.pngbin0 -> 4971 bytes
-rw-r--r--public/-/emojis/3/prince_tone4.pngbin0 -> 4911 bytes
-rw-r--r--public/-/emojis/3/prince_tone5.pngbin0 -> 4979 bytes
-rw-r--r--public/-/emojis/3/princess.pngbin0 -> 5591 bytes
-rw-r--r--public/-/emojis/3/princess_tone1.pngbin0 -> 5621 bytes
-rw-r--r--public/-/emojis/3/princess_tone2.pngbin0 -> 5429 bytes
-rw-r--r--public/-/emojis/3/princess_tone3.pngbin0 -> 5535 bytes
-rw-r--r--public/-/emojis/3/princess_tone4.pngbin0 -> 5461 bytes
-rw-r--r--public/-/emojis/3/princess_tone5.pngbin0 -> 5534 bytes
-rw-r--r--public/-/emojis/3/printer.pngbin0 -> 3128 bytes
-rw-r--r--public/-/emojis/3/projector.pngbin0 -> 5684 bytes
-rw-r--r--public/-/emojis/3/punch.pngbin0 -> 3271 bytes
-rw-r--r--public/-/emojis/3/punch_tone1.pngbin0 -> 3011 bytes
-rw-r--r--public/-/emojis/3/punch_tone2.pngbin0 -> 3165 bytes
-rw-r--r--public/-/emojis/3/punch_tone3.pngbin0 -> 3592 bytes
-rw-r--r--public/-/emojis/3/punch_tone4.pngbin0 -> 3215 bytes
-rw-r--r--public/-/emojis/3/punch_tone5.pngbin0 -> 3327 bytes
-rw-r--r--public/-/emojis/3/purple_heart.pngbin0 -> 3051 bytes
-rw-r--r--public/-/emojis/3/purse.pngbin0 -> 3661 bytes
-rw-r--r--public/-/emojis/3/pushpin.pngbin0 -> 3160 bytes
-rw-r--r--public/-/emojis/3/put_litter_in_its_place.pngbin0 -> 3446 bytes
-rw-r--r--public/-/emojis/3/question.pngbin0 -> 2342 bytes
-rw-r--r--public/-/emojis/3/rabbit.pngbin0 -> 4230 bytes
-rw-r--r--public/-/emojis/3/rabbit2.pngbin0 -> 3614 bytes
-rw-r--r--public/-/emojis/3/race_car.pngbin0 -> 3228 bytes
-rw-r--r--public/-/emojis/3/racehorse.pngbin0 -> 4442 bytes
-rw-r--r--public/-/emojis/3/radio.pngbin0 -> 3597 bytes
-rw-r--r--public/-/emojis/3/radio_button.pngbin0 -> 4227 bytes
-rw-r--r--public/-/emojis/3/radioactive.pngbin0 -> 4640 bytes
-rw-r--r--public/-/emojis/3/rage.pngbin0 -> 5128 bytes
-rw-r--r--public/-/emojis/3/railway_car.pngbin0 -> 2707 bytes
-rw-r--r--public/-/emojis/3/railway_track.pngbin0 -> 4420 bytes
-rw-r--r--public/-/emojis/3/rainbow.pngbin0 -> 3488 bytes
-rw-r--r--public/-/emojis/3/raised_back_of_hand.pngbin0 -> 2913 bytes
-rw-r--r--public/-/emojis/3/raised_back_of_hand_tone1.pngbin0 -> 3126 bytes
-rw-r--r--public/-/emojis/3/raised_back_of_hand_tone2.pngbin0 -> 3156 bytes
-rw-r--r--public/-/emojis/3/raised_back_of_hand_tone3.pngbin0 -> 3053 bytes
-rw-r--r--public/-/emojis/3/raised_back_of_hand_tone4.pngbin0 -> 2886 bytes
-rw-r--r--public/-/emojis/3/raised_back_of_hand_tone5.pngbin0 -> 2930 bytes
-rw-r--r--public/-/emojis/3/raised_hand.pngbin0 -> 2896 bytes
-rw-r--r--public/-/emojis/3/raised_hand_tone1.pngbin0 -> 3120 bytes
-rw-r--r--public/-/emojis/3/raised_hand_tone2.pngbin0 -> 3123 bytes
-rw-r--r--public/-/emojis/3/raised_hand_tone3.pngbin0 -> 3090 bytes
-rw-r--r--public/-/emojis/3/raised_hand_tone4.pngbin0 -> 2860 bytes
-rw-r--r--public/-/emojis/3/raised_hand_tone5.pngbin0 -> 2917 bytes
-rw-r--r--public/-/emojis/3/raised_hands.pngbin0 -> 3902 bytes
-rw-r--r--public/-/emojis/3/raised_hands_tone1.pngbin0 -> 4107 bytes
-rw-r--r--public/-/emojis/3/raised_hands_tone2.pngbin0 -> 4189 bytes
-rw-r--r--public/-/emojis/3/raised_hands_tone3.pngbin0 -> 4084 bytes
-rw-r--r--public/-/emojis/3/raised_hands_tone4.pngbin0 -> 3756 bytes
-rw-r--r--public/-/emojis/3/raised_hands_tone5.pngbin0 -> 3954 bytes
-rw-r--r--public/-/emojis/3/raising_hand.pngbin0 -> 5099 bytes
-rw-r--r--public/-/emojis/3/raising_hand_tone1.pngbin0 -> 5338 bytes
-rw-r--r--public/-/emojis/3/raising_hand_tone2.pngbin0 -> 4972 bytes
-rw-r--r--public/-/emojis/3/raising_hand_tone3.pngbin0 -> 4973 bytes
-rw-r--r--public/-/emojis/3/raising_hand_tone4.pngbin0 -> 4848 bytes
-rw-r--r--public/-/emojis/3/raising_hand_tone5.pngbin0 -> 4883 bytes
-rw-r--r--public/-/emojis/3/ram.pngbin0 -> 4480 bytes
-rw-r--r--public/-/emojis/3/ramen.pngbin0 -> 5580 bytes
-rw-r--r--public/-/emojis/3/rat.pngbin0 -> 3436 bytes
-rw-r--r--public/-/emojis/3/record_button.pngbin0 -> 1872 bytes
-rw-r--r--public/-/emojis/3/recycle.pngbin0 -> 3648 bytes
-rw-r--r--public/-/emojis/3/red_car.pngbin0 -> 3692 bytes
-rw-r--r--public/-/emojis/3/red_circle.pngbin0 -> 2832 bytes
-rw-r--r--public/-/emojis/3/registered.pngbin0 -> 4675 bytes
-rw-r--r--public/-/emojis/3/relaxed.pngbin0 -> 5366 bytes
-rw-r--r--public/-/emojis/3/relieved.pngbin0 -> 4930 bytes
-rw-r--r--public/-/emojis/3/reminder_ribbon.pngbin0 -> 2344 bytes
-rw-r--r--public/-/emojis/3/repeat.pngbin0 -> 2820 bytes
-rw-r--r--public/-/emojis/3/repeat_one.pngbin0 -> 3247 bytes
-rw-r--r--public/-/emojis/3/restroom.pngbin0 -> 3589 bytes
-rw-r--r--public/-/emojis/3/revolving_hearts.pngbin0 -> 4560 bytes
-rw-r--r--public/-/emojis/3/rewind.pngbin0 -> 2223 bytes
-rw-r--r--public/-/emojis/3/rhino.pngbin0 -> 3630 bytes
-rw-r--r--public/-/emojis/3/ribbon.pngbin0 -> 4255 bytes
-rw-r--r--public/-/emojis/3/rice.pngbin0 -> 3765 bytes
-rw-r--r--public/-/emojis/3/rice_ball.pngbin0 -> 3858 bytes
-rw-r--r--public/-/emojis/3/rice_cracker.pngbin0 -> 4058 bytes
-rw-r--r--public/-/emojis/3/rice_scene.pngbin0 -> 7219 bytes
-rw-r--r--public/-/emojis/3/right_facing_fist.pngbin0 -> 2932 bytes
-rw-r--r--public/-/emojis/3/right_facing_fist_tone1.pngbin0 -> 3447 bytes
-rw-r--r--public/-/emojis/3/right_facing_fist_tone2.pngbin0 -> 3544 bytes
-rw-r--r--public/-/emojis/3/right_facing_fist_tone3.pngbin0 -> 3357 bytes
-rw-r--r--public/-/emojis/3/right_facing_fist_tone4.pngbin0 -> 3196 bytes
-rw-r--r--public/-/emojis/3/right_facing_fist_tone5.pngbin0 -> 3248 bytes
-rw-r--r--public/-/emojis/3/ring.pngbin0 -> 3808 bytes
-rw-r--r--public/-/emojis/3/robot.pngbin0 -> 3144 bytes
-rw-r--r--public/-/emojis/3/rocket.pngbin0 -> 5488 bytes
-rw-r--r--public/-/emojis/3/rofl.pngbin0 -> 6172 bytes
-rw-r--r--public/-/emojis/3/roller_coaster.pngbin0 -> 5186 bytes
-rw-r--r--public/-/emojis/3/rolling_eyes.pngbin0 -> 4685 bytes
-rw-r--r--public/-/emojis/3/rooster.pngbin0 -> 3854 bytes
-rw-r--r--public/-/emojis/3/rose.pngbin0 -> 3291 bytes
-rw-r--r--public/-/emojis/3/rosette.pngbin0 -> 4556 bytes
-rw-r--r--public/-/emojis/3/rotating_light.pngbin0 -> 4679 bytes
-rw-r--r--public/-/emojis/3/round_pushpin.pngbin0 -> 1555 bytes
-rw-r--r--public/-/emojis/3/rowboat.pngbin0 -> 4149 bytes
-rw-r--r--public/-/emojis/3/rowboat_tone1.pngbin0 -> 4210 bytes
-rw-r--r--public/-/emojis/3/rowboat_tone2.pngbin0 -> 4204 bytes
-rw-r--r--public/-/emojis/3/rowboat_tone3.pngbin0 -> 4175 bytes
-rw-r--r--public/-/emojis/3/rowboat_tone4.pngbin0 -> 4157 bytes
-rw-r--r--public/-/emojis/3/rowboat_tone5.pngbin0 -> 4183 bytes
-rw-r--r--public/-/emojis/3/rugby_football.pngbin0 -> 3937 bytes
-rw-r--r--public/-/emojis/3/runner.pngbin0 -> 3191 bytes
-rw-r--r--public/-/emojis/3/runner_tone1.pngbin0 -> 3280 bytes
-rw-r--r--public/-/emojis/3/runner_tone2.pngbin0 -> 3270 bytes
-rw-r--r--public/-/emojis/3/runner_tone3.pngbin0 -> 3231 bytes
-rw-r--r--public/-/emojis/3/runner_tone4.pngbin0 -> 3213 bytes
-rw-r--r--public/-/emojis/3/runner_tone5.pngbin0 -> 3285 bytes
-rw-r--r--public/-/emojis/3/running_shirt_with_sash.pngbin0 -> 3358 bytes
-rw-r--r--public/-/emojis/3/sa.pngbin0 -> 2386 bytes
-rw-r--r--public/-/emojis/3/sagittarius.pngbin0 -> 4468 bytes
-rw-r--r--public/-/emojis/3/sailboat.pngbin0 -> 3987 bytes
-rw-r--r--public/-/emojis/3/sake.pngbin0 -> 3311 bytes
-rw-r--r--public/-/emojis/3/salad.pngbin0 -> 5279 bytes
-rw-r--r--public/-/emojis/3/sandal.pngbin0 -> 2770 bytes
-rw-r--r--public/-/emojis/3/santa.pngbin0 -> 4289 bytes
-rw-r--r--public/-/emojis/3/santa_tone1.pngbin0 -> 4204 bytes
-rw-r--r--public/-/emojis/3/santa_tone2.pngbin0 -> 4284 bytes
-rw-r--r--public/-/emojis/3/santa_tone3.pngbin0 -> 4382 bytes
-rw-r--r--public/-/emojis/3/santa_tone4.pngbin0 -> 4409 bytes
-rw-r--r--public/-/emojis/3/santa_tone5.pngbin0 -> 4449 bytes
-rw-r--r--public/-/emojis/3/satellite.pngbin0 -> 5128 bytes
-rw-r--r--public/-/emojis/3/satellite_orbital.pngbin0 -> 4941 bytes
-rw-r--r--public/-/emojis/3/saxophone.pngbin0 -> 2954 bytes
-rw-r--r--public/-/emojis/3/scales.pngbin0 -> 4230 bytes
-rw-r--r--public/-/emojis/3/school.pngbin0 -> 4001 bytes
-rw-r--r--public/-/emojis/3/school_satchel.pngbin0 -> 5291 bytes
-rw-r--r--public/-/emojis/3/scissors.pngbin0 -> 3793 bytes
-rw-r--r--public/-/emojis/3/scooter.pngbin0 -> 3110 bytes
-rw-r--r--public/-/emojis/3/scorpion.pngbin0 -> 4502 bytes
-rw-r--r--public/-/emojis/3/scorpius.pngbin0 -> 4438 bytes
-rw-r--r--public/-/emojis/3/scream.pngbin0 -> 5649 bytes
-rw-r--r--public/-/emojis/3/scream_cat.pngbin0 -> 5280 bytes
-rw-r--r--public/-/emojis/3/scroll.pngbin0 -> 3417 bytes
-rw-r--r--public/-/emojis/3/seat.pngbin0 -> 3717 bytes
-rw-r--r--public/-/emojis/3/second_place.pngbin0 -> 3637 bytes
-rw-r--r--public/-/emojis/3/secret.pngbin0 -> 5359 bytes
-rw-r--r--public/-/emojis/3/see_no_evil.pngbin0 -> 4037 bytes
-rw-r--r--public/-/emojis/3/seedling.pngbin0 -> 2678 bytes
-rw-r--r--public/-/emojis/3/selfie.pngbin0 -> 3196 bytes
-rw-r--r--public/-/emojis/3/selfie_tone1.pngbin0 -> 3171 bytes
-rw-r--r--public/-/emojis/3/selfie_tone2.pngbin0 -> 3258 bytes
-rw-r--r--public/-/emojis/3/selfie_tone3.pngbin0 -> 3255 bytes
-rw-r--r--public/-/emojis/3/selfie_tone4.pngbin0 -> 3245 bytes
-rw-r--r--public/-/emojis/3/selfie_tone5.pngbin0 -> 3253 bytes
-rw-r--r--public/-/emojis/3/seven.pngbin0 -> 1981 bytes
-rw-r--r--public/-/emojis/3/shallow_pan_of_food.pngbin0 -> 7933 bytes
-rw-r--r--public/-/emojis/3/shamrock.pngbin0 -> 3812 bytes
-rw-r--r--public/-/emojis/3/shark.pngbin0 -> 3599 bytes
-rw-r--r--public/-/emojis/3/shaved_ice.pngbin0 -> 4374 bytes
-rw-r--r--public/-/emojis/3/sheep.pngbin0 -> 4608 bytes
-rw-r--r--public/-/emojis/3/shell.pngbin0 -> 3732 bytes
-rw-r--r--public/-/emojis/3/shield.pngbin0 -> 4162 bytes
-rw-r--r--public/-/emojis/3/shinto_shrine.pngbin0 -> 2517 bytes
-rw-r--r--public/-/emojis/3/ship.pngbin0 -> 4370 bytes
-rw-r--r--public/-/emojis/3/shirt.pngbin0 -> 2090 bytes
-rw-r--r--public/-/emojis/3/shopping_bags.pngbin0 -> 4273 bytes
-rw-r--r--public/-/emojis/3/shopping_cart.pngbin0 -> 3825 bytes
-rw-r--r--public/-/emojis/3/shower.pngbin0 -> 4662 bytes
-rw-r--r--public/-/emojis/3/shrimp.pngbin0 -> 3495 bytes
-rw-r--r--public/-/emojis/3/shrug.pngbin0 -> 4781 bytes
-rw-r--r--public/-/emojis/3/shrug_tone1.pngbin0 -> 4984 bytes
-rw-r--r--public/-/emojis/3/shrug_tone2.pngbin0 -> 4800 bytes
-rw-r--r--public/-/emojis/3/shrug_tone3.pngbin0 -> 4762 bytes
-rw-r--r--public/-/emojis/3/shrug_tone4.pngbin0 -> 4717 bytes
-rw-r--r--public/-/emojis/3/shrug_tone5.pngbin0 -> 4754 bytes
-rw-r--r--public/-/emojis/3/signal_strength.pngbin0 -> 1468 bytes
-rw-r--r--public/-/emojis/3/six.pngbin0 -> 2529 bytes
-rw-r--r--public/-/emojis/3/six_pointed_star.pngbin0 -> 3801 bytes
-rw-r--r--public/-/emojis/3/ski.pngbin0 -> 3862 bytes
-rw-r--r--public/-/emojis/3/skier.pngbin0 -> 5603 bytes
-rw-r--r--public/-/emojis/3/skull.pngbin0 -> 4059 bytes
-rw-r--r--public/-/emojis/3/skull_crossbones.pngbin0 -> 4347 bytes
-rw-r--r--public/-/emojis/3/sleeping.pngbin0 -> 5214 bytes
-rw-r--r--public/-/emojis/3/sleeping_accommodation.pngbin0 -> 2442 bytes
-rw-r--r--public/-/emojis/3/sleepy.pngbin0 -> 5248 bytes
-rw-r--r--public/-/emojis/3/slight_frown.pngbin0 -> 4636 bytes
-rw-r--r--public/-/emojis/3/slight_smile.pngbin0 -> 4644 bytes
-rw-r--r--public/-/emojis/3/slot_machine.pngbin0 -> 5163 bytes
-rw-r--r--public/-/emojis/3/small_blue_diamond.pngbin0 -> 977 bytes
-rw-r--r--public/-/emojis/3/small_orange_diamond.pngbin0 -> 950 bytes
-rw-r--r--public/-/emojis/3/small_red_triangle.pngbin0 -> 1371 bytes
-rw-r--r--public/-/emojis/3/small_red_triangle_down.pngbin0 -> 1435 bytes
-rw-r--r--public/-/emojis/3/smile.pngbin0 -> 5185 bytes
-rw-r--r--public/-/emojis/3/smile_cat.pngbin0 -> 4845 bytes
-rw-r--r--public/-/emojis/3/smiley.pngbin0 -> 5211 bytes
-rw-r--r--public/-/emojis/3/smiley_cat.pngbin0 -> 4458 bytes
-rw-r--r--public/-/emojis/3/smiling_imp.pngbin0 -> 5355 bytes
-rw-r--r--public/-/emojis/3/smirk.pngbin0 -> 4817 bytes
-rw-r--r--public/-/emojis/3/smirk_cat.pngbin0 -> 4383 bytes
-rw-r--r--public/-/emojis/3/smoking.pngbin0 -> 3110 bytes
-rw-r--r--public/-/emojis/3/snail.pngbin0 -> 4356 bytes
-rw-r--r--public/-/emojis/3/snake.pngbin0 -> 4040 bytes
-rw-r--r--public/-/emojis/3/sneezing_face.pngbin0 -> 6024 bytes
-rw-r--r--public/-/emojis/3/snowboarder.pngbin0 -> 5823 bytes
-rw-r--r--public/-/emojis/3/snowflake.pngbin0 -> 5219 bytes
-rw-r--r--public/-/emojis/3/snowman.pngbin0 -> 2907 bytes
-rw-r--r--public/-/emojis/3/snowman2.pngbin0 -> 3718 bytes
-rw-r--r--public/-/emojis/3/sob.pngbin0 -> 5597 bytes
-rw-r--r--public/-/emojis/3/soccer.pngbin0 -> 5272 bytes
-rw-r--r--public/-/emojis/3/soon.pngbin0 -> 3511 bytes
-rw-r--r--public/-/emojis/3/sos.pngbin0 -> 3896 bytes
-rw-r--r--public/-/emojis/3/sound.pngbin0 -> 3960 bytes
-rw-r--r--public/-/emojis/3/space_invader.pngbin0 -> 2411 bytes
-rw-r--r--public/-/emojis/3/spades.pngbin0 -> 2300 bytes
-rw-r--r--public/-/emojis/3/spaghetti.pngbin0 -> 5923 bytes
-rw-r--r--public/-/emojis/3/sparkle.pngbin0 -> 3257 bytes
-rw-r--r--public/-/emojis/3/sparkler.pngbin0 -> 9294 bytes
-rw-r--r--public/-/emojis/3/sparkles.pngbin0 -> 2796 bytes
-rw-r--r--public/-/emojis/3/sparkling_heart.pngbin0 -> 4578 bytes
-rw-r--r--public/-/emojis/3/speak_no_evil.pngbin0 -> 3986 bytes
-rw-r--r--public/-/emojis/3/speaker.pngbin0 -> 3686 bytes
-rw-r--r--public/-/emojis/3/speaking_head.pngbin0 -> 1746 bytes
-rw-r--r--public/-/emojis/3/speech_balloon.pngbin0 -> 3203 bytes
-rw-r--r--public/-/emojis/3/speech_left.pngbin0 -> 2709 bytes
-rw-r--r--public/-/emojis/3/speedboat.pngbin0 -> 2620 bytes
-rw-r--r--public/-/emojis/3/spider.pngbin0 -> 4025 bytes
-rw-r--r--public/-/emojis/3/spider_web.pngbin0 -> 3798 bytes
-rw-r--r--public/-/emojis/3/spoon.pngbin0 -> 2894 bytes
-rw-r--r--public/-/emojis/3/spy.pngbin0 -> 6337 bytes
-rw-r--r--public/-/emojis/3/spy_tone1.pngbin0 -> 6445 bytes
-rw-r--r--public/-/emojis/3/spy_tone2.pngbin0 -> 6304 bytes
-rw-r--r--public/-/emojis/3/spy_tone3.pngbin0 -> 6154 bytes
-rw-r--r--public/-/emojis/3/spy_tone4.pngbin0 -> 6081 bytes
-rw-r--r--public/-/emojis/3/spy_tone5.pngbin0 -> 6028 bytes
-rw-r--r--public/-/emojis/3/squid.pngbin0 -> 4763 bytes
-rw-r--r--public/-/emojis/3/stadium.pngbin0 -> 6540 bytes
-rw-r--r--public/-/emojis/3/star.pngbin0 -> 2222 bytes
-rw-r--r--public/-/emojis/3/star2.pngbin0 -> 3065 bytes
-rw-r--r--public/-/emojis/3/star_and_crescent.pngbin0 -> 3631 bytes
-rw-r--r--public/-/emojis/3/star_of_david.pngbin0 -> 3664 bytes
-rw-r--r--public/-/emojis/3/stars.pngbin0 -> 5233 bytes
-rw-r--r--public/-/emojis/3/station.pngbin0 -> 5407 bytes
-rw-r--r--public/-/emojis/3/statue_of_liberty.pngbin0 -> 5849 bytes
-rw-r--r--public/-/emojis/3/steam_locomotive.pngbin0 -> 5410 bytes
-rw-r--r--public/-/emojis/3/stew.pngbin0 -> 4054 bytes
-rw-r--r--public/-/emojis/3/stop_button.pngbin0 -> 1130 bytes
-rw-r--r--public/-/emojis/3/stopwatch.pngbin0 -> 4767 bytes
-rw-r--r--public/-/emojis/3/straight_ruler.pngbin0 -> 3199 bytes
-rw-r--r--public/-/emojis/3/strawberry.pngbin0 -> 4260 bytes
-rw-r--r--public/-/emojis/3/stuck_out_tongue.pngbin0 -> 4664 bytes
-rw-r--r--public/-/emojis/3/stuck_out_tongue_closed_eyes.pngbin0 -> 5088 bytes
-rw-r--r--public/-/emojis/3/stuck_out_tongue_winking_eye.pngbin0 -> 5175 bytes
-rw-r--r--public/-/emojis/3/stuffed_flatbread.pngbin0 -> 6153 bytes
-rw-r--r--public/-/emojis/3/sun_with_face.pngbin0 -> 5405 bytes
-rw-r--r--public/-/emojis/3/sunflower.pngbin0 -> 3686 bytes
-rw-r--r--public/-/emojis/3/sunglasses.pngbin0 -> 4783 bytes
-rw-r--r--public/-/emojis/3/sunny.pngbin0 -> 3127 bytes
-rw-r--r--public/-/emojis/3/sunrise.pngbin0 -> 5904 bytes
-rw-r--r--public/-/emojis/3/sunrise_over_mountains.pngbin0 -> 7314 bytes
-rw-r--r--public/-/emojis/3/surfer.pngbin0 -> 6505 bytes
-rw-r--r--public/-/emojis/3/surfer_tone1.pngbin0 -> 6533 bytes
-rw-r--r--public/-/emojis/3/surfer_tone2.pngbin0 -> 6530 bytes
-rw-r--r--public/-/emojis/3/surfer_tone3.pngbin0 -> 6536 bytes
-rw-r--r--public/-/emojis/3/surfer_tone4.pngbin0 -> 6551 bytes
-rw-r--r--public/-/emojis/3/surfer_tone5.pngbin0 -> 6613 bytes
-rw-r--r--public/-/emojis/3/sushi.pngbin0 -> 5586 bytes
-rw-r--r--public/-/emojis/3/suspension_railway.pngbin0 -> 4615 bytes
-rw-r--r--public/-/emojis/3/sweat.pngbin0 -> 4783 bytes
-rw-r--r--public/-/emojis/3/sweat_drops.pngbin0 -> 2832 bytes
-rw-r--r--public/-/emojis/3/sweat_smile.pngbin0 -> 5513 bytes
-rw-r--r--public/-/emojis/3/sweet_potato.pngbin0 -> 3646 bytes
-rw-r--r--public/-/emojis/3/swimmer.pngbin0 -> 2894 bytes
-rw-r--r--public/-/emojis/3/swimmer_tone1.pngbin0 -> 2968 bytes
-rw-r--r--public/-/emojis/3/swimmer_tone2.pngbin0 -> 2972 bytes
-rw-r--r--public/-/emojis/3/swimmer_tone3.pngbin0 -> 2984 bytes
-rw-r--r--public/-/emojis/3/swimmer_tone4.pngbin0 -> 2970 bytes
-rw-r--r--public/-/emojis/3/swimmer_tone5.pngbin0 -> 3002 bytes
-rw-r--r--public/-/emojis/3/symbols.pngbin0 -> 3442 bytes
-rw-r--r--public/-/emojis/3/synagogue.pngbin0 -> 4062 bytes
-rw-r--r--public/-/emojis/3/syringe.pngbin0 -> 3298 bytes
-rw-r--r--public/-/emojis/3/taco.pngbin0 -> 5095 bytes
-rw-r--r--public/-/emojis/3/tada.pngbin0 -> 5758 bytes
-rw-r--r--public/-/emojis/3/tanabata_tree.pngbin0 -> 4774 bytes
-rw-r--r--public/-/emojis/3/tangerine.pngbin0 -> 3009 bytes
-rw-r--r--public/-/emojis/3/taurus.pngbin0 -> 4716 bytes
-rw-r--r--public/-/emojis/3/taxi.pngbin0 -> 3989 bytes
-rw-r--r--public/-/emojis/3/tea.pngbin0 -> 2862 bytes
-rw-r--r--public/-/emojis/3/telephone.pngbin0 -> 3940 bytes
-rw-r--r--public/-/emojis/3/telephone_receiver.pngbin0 -> 2937 bytes
-rw-r--r--public/-/emojis/3/telescope.pngbin0 -> 3989 bytes
-rw-r--r--public/-/emojis/3/ten.pngbin0 -> 2573 bytes
-rw-r--r--public/-/emojis/3/tennis.pngbin0 -> 5650 bytes
-rw-r--r--public/-/emojis/3/tent.pngbin0 -> 2610 bytes
-rw-r--r--public/-/emojis/3/thermometer.pngbin0 -> 2433 bytes
-rw-r--r--public/-/emojis/3/thermometer_face.pngbin0 -> 5693 bytes
-rw-r--r--public/-/emojis/3/thinking.pngbin0 -> 5440 bytes
-rw-r--r--public/-/emojis/3/third_place.pngbin0 -> 3784 bytes
-rw-r--r--public/-/emojis/3/thought_balloon.pngbin0 -> 3237 bytes
-rw-r--r--public/-/emojis/3/three.pngbin0 -> 2580 bytes
-rw-r--r--public/-/emojis/3/thumbsdown.pngbin0 -> 3520 bytes
-rw-r--r--public/-/emojis/3/thumbsdown_tone1.pngbin0 -> 3749 bytes
-rw-r--r--public/-/emojis/3/thumbsdown_tone2.pngbin0 -> 3776 bytes
-rw-r--r--public/-/emojis/3/thumbsdown_tone3.pngbin0 -> 3679 bytes
-rw-r--r--public/-/emojis/3/thumbsdown_tone4.pngbin0 -> 3443 bytes
-rw-r--r--public/-/emojis/3/thumbsdown_tone5.pngbin0 -> 3501 bytes
-rw-r--r--public/-/emojis/3/thumbsup.pngbin0 -> 3475 bytes
-rw-r--r--public/-/emojis/3/thumbsup_tone1.pngbin0 -> 3754 bytes
-rw-r--r--public/-/emojis/3/thumbsup_tone2.pngbin0 -> 3737 bytes
-rw-r--r--public/-/emojis/3/thumbsup_tone3.pngbin0 -> 3670 bytes
-rw-r--r--public/-/emojis/3/thumbsup_tone4.pngbin0 -> 3403 bytes
-rw-r--r--public/-/emojis/3/thumbsup_tone5.pngbin0 -> 3474 bytes
-rw-r--r--public/-/emojis/3/thunder_cloud_rain.pngbin0 -> 3424 bytes
-rw-r--r--public/-/emojis/3/ticket.pngbin0 -> 2253 bytes
-rw-r--r--public/-/emojis/3/tickets.pngbin0 -> 1702 bytes
-rw-r--r--public/-/emojis/3/tiger.pngbin0 -> 6453 bytes
-rw-r--r--public/-/emojis/3/tiger2.pngbin0 -> 5531 bytes
-rw-r--r--public/-/emojis/3/timer.pngbin0 -> 4883 bytes
-rw-r--r--public/-/emojis/3/tired_face.pngbin0 -> 5606 bytes
-rw-r--r--public/-/emojis/3/tm.pngbin0 -> 1885 bytes
-rw-r--r--public/-/emojis/3/toilet.pngbin0 -> 2807 bytes
-rw-r--r--public/-/emojis/3/tokyo_tower.pngbin0 -> 2259 bytes
-rw-r--r--public/-/emojis/3/tomato.pngbin0 -> 3520 bytes
-rw-r--r--public/-/emojis/3/tone1.pngbin0 -> 406 bytes
-rw-r--r--public/-/emojis/3/tone2.pngbin0 -> 419 bytes
-rw-r--r--public/-/emojis/3/tone3.pngbin0 -> 414 bytes
-rw-r--r--public/-/emojis/3/tone4.pngbin0 -> 416 bytes
-rw-r--r--public/-/emojis/3/tone5.pngbin0 -> 405 bytes
-rw-r--r--public/-/emojis/3/tongue.pngbin0 -> 3705 bytes
-rw-r--r--public/-/emojis/3/tools.pngbin0 -> 5049 bytes
-rw-r--r--public/-/emojis/3/top.pngbin0 -> 2753 bytes
-rw-r--r--public/-/emojis/3/tophat.pngbin0 -> 3289 bytes
-rw-r--r--public/-/emojis/3/track_next.pngbin0 -> 2235 bytes
-rw-r--r--public/-/emojis/3/track_previous.pngbin0 -> 2252 bytes
-rw-r--r--public/-/emojis/3/trackball.pngbin0 -> 4137 bytes
-rw-r--r--public/-/emojis/3/tractor.pngbin0 -> 5972 bytes
-rw-r--r--public/-/emojis/3/traffic_light.pngbin0 -> 2502 bytes
-rw-r--r--public/-/emojis/3/train.pngbin0 -> 3318 bytes
-rw-r--r--public/-/emojis/3/train2.pngbin0 -> 4262 bytes
-rw-r--r--public/-/emojis/3/tram.pngbin0 -> 4236 bytes
-rw-r--r--public/-/emojis/3/triangular_flag_on_post.pngbin0 -> 3203 bytes
-rw-r--r--public/-/emojis/3/triangular_ruler.pngbin0 -> 3101 bytes
-rw-r--r--public/-/emojis/3/trident.pngbin0 -> 4253 bytes
-rw-r--r--public/-/emojis/3/triumph.pngbin0 -> 5870 bytes
-rw-r--r--public/-/emojis/3/trolleybus.pngbin0 -> 4107 bytes
-rw-r--r--public/-/emojis/3/trophy.pngbin0 -> 4374 bytes
-rw-r--r--public/-/emojis/3/tropical_drink.pngbin0 -> 3950 bytes
-rw-r--r--public/-/emojis/3/tropical_fish.pngbin0 -> 4411 bytes
-rw-r--r--public/-/emojis/3/truck.pngbin0 -> 3314 bytes
-rw-r--r--public/-/emojis/3/trumpet.pngbin0 -> 3443 bytes
-rw-r--r--public/-/emojis/3/tulip.pngbin0 -> 3162 bytes
-rw-r--r--public/-/emojis/3/tumbler_glass.pngbin0 -> 5194 bytes
-rw-r--r--public/-/emojis/3/turkey.pngbin0 -> 5886 bytes
-rw-r--r--public/-/emojis/3/turtle.pngbin0 -> 3718 bytes
-rw-r--r--public/-/emojis/3/tv.pngbin0 -> 2981 bytes
-rw-r--r--public/-/emojis/3/twisted_rightwards_arrows.pngbin0 -> 3645 bytes
-rw-r--r--public/-/emojis/3/two.pngbin0 -> 2368 bytes
-rw-r--r--public/-/emojis/3/two_hearts.pngbin0 -> 3543 bytes
-rw-r--r--public/-/emojis/3/two_men_holding_hands.pngbin0 -> 4990 bytes
-rw-r--r--public/-/emojis/3/two_women_holding_hands.pngbin0 -> 5302 bytes
-rw-r--r--public/-/emojis/3/u5272.pngbin0 -> 2808 bytes
-rw-r--r--public/-/emojis/3/u5408.pngbin0 -> 2840 bytes
-rw-r--r--public/-/emojis/3/u55b6.pngbin0 -> 2585 bytes
-rw-r--r--public/-/emojis/3/u6307.pngbin0 -> 2970 bytes
-rw-r--r--public/-/emojis/3/u6708.pngbin0 -> 2072 bytes
-rw-r--r--public/-/emojis/3/u6709.pngbin0 -> 2477 bytes
-rw-r--r--public/-/emojis/3/u6e80.pngbin0 -> 3696 bytes
-rw-r--r--public/-/emojis/3/u7121.pngbin0 -> 3254 bytes
-rw-r--r--public/-/emojis/3/u7533.pngbin0 -> 2115 bytes
-rw-r--r--public/-/emojis/3/u7981.pngbin0 -> 3651 bytes
-rw-r--r--public/-/emojis/3/u7a7a.pngbin0 -> 2572 bytes
-rw-r--r--public/-/emojis/3/umbrella.pngbin0 -> 4009 bytes
-rw-r--r--public/-/emojis/3/umbrella2.pngbin0 -> 2695 bytes
-rw-r--r--public/-/emojis/3/unamused.pngbin0 -> 4844 bytes
-rw-r--r--public/-/emojis/3/underage.pngbin0 -> 6503 bytes
-rw-r--r--public/-/emojis/3/unicorn.pngbin0 -> 4566 bytes
-rw-r--r--public/-/emojis/3/unlock.pngbin0 -> 2560 bytes
-rw-r--r--public/-/emojis/3/up.pngbin0 -> 2524 bytes
-rw-r--r--public/-/emojis/3/upside_down.pngbin0 -> 4771 bytes
-rw-r--r--public/-/emojis/3/urn.pngbin0 -> 3097 bytes
-rw-r--r--public/-/emojis/3/v.pngbin0 -> 3650 bytes
-rw-r--r--public/-/emojis/3/v_tone1.pngbin0 -> 3873 bytes
-rw-r--r--public/-/emojis/3/v_tone2.pngbin0 -> 3970 bytes
-rw-r--r--public/-/emojis/3/v_tone3.pngbin0 -> 3894 bytes
-rw-r--r--public/-/emojis/3/v_tone4.pngbin0 -> 3585 bytes
-rw-r--r--public/-/emojis/3/v_tone5.pngbin0 -> 3722 bytes
-rw-r--r--public/-/emojis/3/vertical_traffic_light.pngbin0 -> 2608 bytes
-rw-r--r--public/-/emojis/3/vhs.pngbin0 -> 2286 bytes
-rw-r--r--public/-/emojis/3/vibration_mode.pngbin0 -> 4065 bytes
-rw-r--r--public/-/emojis/3/video_camera.pngbin0 -> 2530 bytes
-rw-r--r--public/-/emojis/3/video_game.pngbin0 -> 3467 bytes
-rw-r--r--public/-/emojis/3/violin.pngbin0 -> 5262 bytes
-rw-r--r--public/-/emojis/3/virgo.pngbin0 -> 4848 bytes
-rw-r--r--public/-/emojis/3/volcano.pngbin0 -> 6192 bytes
-rw-r--r--public/-/emojis/3/volleyball.pngbin0 -> 6651 bytes
-rw-r--r--public/-/emojis/3/vs.pngbin0 -> 3290 bytes
-rw-r--r--public/-/emojis/3/vulcan.pngbin0 -> 3853 bytes
-rw-r--r--public/-/emojis/3/vulcan_tone1.pngbin0 -> 4142 bytes
-rw-r--r--public/-/emojis/3/vulcan_tone2.pngbin0 -> 4131 bytes
-rw-r--r--public/-/emojis/3/vulcan_tone3.pngbin0 -> 4061 bytes
-rw-r--r--public/-/emojis/3/vulcan_tone4.pngbin0 -> 3728 bytes
-rw-r--r--public/-/emojis/3/vulcan_tone5.pngbin0 -> 3878 bytes
-rw-r--r--public/-/emojis/3/walking.pngbin0 -> 2553 bytes
-rw-r--r--public/-/emojis/3/walking_tone1.pngbin0 -> 2613 bytes
-rw-r--r--public/-/emojis/3/walking_tone2.pngbin0 -> 2586 bytes
-rw-r--r--public/-/emojis/3/walking_tone3.pngbin0 -> 2547 bytes
-rw-r--r--public/-/emojis/3/walking_tone4.pngbin0 -> 2538 bytes
-rw-r--r--public/-/emojis/3/walking_tone5.pngbin0 -> 2577 bytes
-rw-r--r--public/-/emojis/3/waning_crescent_moon.pngbin0 -> 4258 bytes
-rw-r--r--public/-/emojis/3/waning_gibbous_moon.pngbin0 -> 4446 bytes
-rw-r--r--public/-/emojis/3/warning.pngbin0 -> 2659 bytes
-rw-r--r--public/-/emojis/3/wastebasket.pngbin0 -> 5979 bytes
-rw-r--r--public/-/emojis/3/watch.pngbin0 -> 4900 bytes
-rw-r--r--public/-/emojis/3/water_buffalo.pngbin0 -> 3875 bytes
-rw-r--r--public/-/emojis/3/water_polo.pngbin0 -> 4456 bytes
-rw-r--r--public/-/emojis/3/water_polo_tone1.pngbin0 -> 4577 bytes
-rw-r--r--public/-/emojis/3/water_polo_tone2.pngbin0 -> 4544 bytes
-rw-r--r--public/-/emojis/3/water_polo_tone3.pngbin0 -> 4534 bytes
-rw-r--r--public/-/emojis/3/water_polo_tone4.pngbin0 -> 4536 bytes
-rw-r--r--public/-/emojis/3/water_polo_tone5.pngbin0 -> 4593 bytes
-rw-r--r--public/-/emojis/3/watermelon.pngbin0 -> 4264 bytes
-rw-r--r--public/-/emojis/3/wave.pngbin0 -> 4474 bytes
-rw-r--r--public/-/emojis/3/wave_tone1.pngbin0 -> 4858 bytes
-rw-r--r--public/-/emojis/3/wave_tone2.pngbin0 -> 4912 bytes
-rw-r--r--public/-/emojis/3/wave_tone3.pngbin0 -> 4957 bytes
-rw-r--r--public/-/emojis/3/wave_tone4.pngbin0 -> 4639 bytes
-rw-r--r--public/-/emojis/3/wave_tone5.pngbin0 -> 4765 bytes
-rw-r--r--public/-/emojis/3/wavy_dash.pngbin0 -> 2238 bytes
-rw-r--r--public/-/emojis/3/waxing_crescent_moon.pngbin0 -> 4363 bytes
-rw-r--r--public/-/emojis/3/waxing_gibbous_moon.pngbin0 -> 4350 bytes
-rw-r--r--public/-/emojis/3/wc.pngbin0 -> 3464 bytes
-rw-r--r--public/-/emojis/3/weary.pngbin0 -> 5334 bytes
-rw-r--r--public/-/emojis/3/wedding.pngbin0 -> 5157 bytes
-rw-r--r--public/-/emojis/3/whale.pngbin0 -> 3872 bytes
-rw-r--r--public/-/emojis/3/whale2.pngbin0 -> 4330 bytes
-rw-r--r--public/-/emojis/3/wheel_of_dharma.pngbin0 -> 4707 bytes
-rw-r--r--public/-/emojis/3/wheelchair.pngbin0 -> 4014 bytes
-rw-r--r--public/-/emojis/3/white_check_mark.pngbin0 -> 2938 bytes
-rw-r--r--public/-/emojis/3/white_circle.pngbin0 -> 2714 bytes
-rw-r--r--public/-/emojis/3/white_flower.pngbin0 -> 4248 bytes
-rw-r--r--public/-/emojis/3/white_large_square.pngbin0 -> 946 bytes
-rw-r--r--public/-/emojis/3/white_medium_small_square.pngbin0 -> 657 bytes
-rw-r--r--public/-/emojis/3/white_medium_square.pngbin0 -> 693 bytes
-rw-r--r--public/-/emojis/3/white_small_square.pngbin0 -> 525 bytes
-rw-r--r--public/-/emojis/3/white_square_button.pngbin0 -> 1150 bytes
-rw-r--r--public/-/emojis/3/white_sun_cloud.pngbin0 -> 2314 bytes
-rw-r--r--public/-/emojis/3/white_sun_rain_cloud.pngbin0 -> 3147 bytes
-rw-r--r--public/-/emojis/3/white_sun_small_cloud.pngbin0 -> 3215 bytes
-rw-r--r--public/-/emojis/3/wilted_rose.pngbin0 -> 3372 bytes
-rw-r--r--public/-/emojis/3/wind_blowing_face.pngbin0 -> 4038 bytes
-rw-r--r--public/-/emojis/3/wind_chime.pngbin0 -> 3681 bytes
-rw-r--r--public/-/emojis/3/wine_glass.pngbin0 -> 3861 bytes
-rw-r--r--public/-/emojis/3/wink.pngbin0 -> 4847 bytes
-rw-r--r--public/-/emojis/3/wolf.pngbin0 -> 3840 bytes
-rw-r--r--public/-/emojis/3/woman.pngbin0 -> 4621 bytes
-rw-r--r--public/-/emojis/3/woman_tone1.pngbin0 -> 4677 bytes
-rw-r--r--public/-/emojis/3/woman_tone2.pngbin0 -> 4672 bytes
-rw-r--r--public/-/emojis/3/woman_tone3.pngbin0 -> 4448 bytes
-rw-r--r--public/-/emojis/3/woman_tone4.pngbin0 -> 4214 bytes
-rw-r--r--public/-/emojis/3/woman_tone5.pngbin0 -> 4272 bytes
-rw-r--r--public/-/emojis/3/womans_clothes.pngbin0 -> 2441 bytes
-rw-r--r--public/-/emojis/3/womans_hat.pngbin0 -> 4445 bytes
-rw-r--r--public/-/emojis/3/womens.pngbin0 -> 2469 bytes
-rw-r--r--public/-/emojis/3/worried.pngbin0 -> 4921 bytes
-rw-r--r--public/-/emojis/3/wrench.pngbin0 -> 3179 bytes
-rw-r--r--public/-/emojis/3/wrestlers.pngbin0 -> 5306 bytes
-rw-r--r--public/-/emojis/3/wrestlers_tone1.pngbin0 -> 5399 bytes
-rw-r--r--public/-/emojis/3/wrestlers_tone2.pngbin0 -> 5441 bytes
-rw-r--r--public/-/emojis/3/wrestlers_tone3.pngbin0 -> 5421 bytes
-rw-r--r--public/-/emojis/3/wrestlers_tone4.pngbin0 -> 5390 bytes
-rw-r--r--public/-/emojis/3/wrestlers_tone5.pngbin0 -> 5485 bytes
-rw-r--r--public/-/emojis/3/writing_hand.pngbin0 -> 4347 bytes
-rw-r--r--public/-/emojis/3/writing_hand_tone1.pngbin0 -> 4523 bytes
-rw-r--r--public/-/emojis/3/writing_hand_tone2.pngbin0 -> 4652 bytes
-rw-r--r--public/-/emojis/3/writing_hand_tone3.pngbin0 -> 4625 bytes
-rw-r--r--public/-/emojis/3/writing_hand_tone4.pngbin0 -> 4307 bytes
-rw-r--r--public/-/emojis/3/writing_hand_tone5.pngbin0 -> 4385 bytes
-rw-r--r--public/-/emojis/3/x.pngbin0 -> 3100 bytes
-rw-r--r--public/-/emojis/3/yellow_heart.pngbin0 -> 2958 bytes
-rw-r--r--public/-/emojis/3/yen.pngbin0 -> 2286 bytes
-rw-r--r--public/-/emojis/3/yin_yang.pngbin0 -> 3900 bytes
-rw-r--r--public/-/emojis/3/yum.pngbin0 -> 4930 bytes
-rw-r--r--public/-/emojis/3/zap.pngbin0 -> 2705 bytes
-rw-r--r--public/-/emojis/3/zero.pngbin0 -> 2234 bytes
-rw-r--r--public/-/emojis/3/zipper_mouth.pngbin0 -> 5280 bytes
-rw-r--r--public/-/emojis/3/zzz.pngbin0 -> 2030 bytes
-rw-r--r--qa/Gemfile4
-rw-r--r--qa/Gemfile.lock10
-rw-r--r--qa/Rakefile19
-rw-r--r--qa/gdk/Dockerfile.gdk4
-rw-r--r--qa/lib/gitlab/page/admin/subscription.rb2
-rw-r--r--qa/lib/gitlab/page/group/settings/usage_quotas.rb30
-rw-r--r--qa/lib/gitlab/page/group/settings/usage_quotas.stub.rb48
-rw-r--r--qa/lib/gitlab/page/subscriptions/new.rb15
-rw-r--r--qa/lib/gitlab/page/subscriptions/new.stub.rb24
-rw-r--r--qa/qa/factories/deploy_tokens.rb11
-rw-r--r--qa/qa/factories/designs.rb7
-rw-r--r--qa/qa/factories/issues.rb6
-rw-r--r--qa/qa/factories/packages.rb8
-rw-r--r--qa/qa/fixtures/mocks/import/github.yml98
-rw-r--r--qa/qa/flow/login.rb8
-rw-r--r--qa/qa/flow/purchase.rb16
-rw-r--r--qa/qa/flow/user_onboarding.rb6
-rw-r--r--qa/qa/page/admin/menu.rb2
-rw-r--r--qa/qa/page/admin/overview/users/index.rb2
-rw-r--r--qa/qa/page/admin/settings/metrics_and_profiling.rb4
-rw-r--r--qa/qa/page/component/ci_badge_link.rb52
-rw-r--r--qa/qa/page/component/ci_icon.rb54
-rw-r--r--qa/qa/page/component/commit_modal.rb2
-rw-r--r--qa/qa/page/component/deploy_token.rb72
-rw-r--r--qa/qa/page/component/design_management.rb58
-rw-r--r--qa/qa/page/component/import/gitlab.rb9
-rw-r--r--qa/qa/page/component/import/selection.rb6
-rw-r--r--qa/qa/page/dashboard/todos.rb12
-rw-r--r--qa/qa/page/file/edit.rb2
-rw-r--r--qa/qa/page/file/form.rb4
-rw-r--r--qa/qa/page/file/shared/commit_message.rb12
-rw-r--r--qa/qa/page/file/shared/editor.rb4
-rw-r--r--qa/qa/page/file/show.rb12
-rw-r--r--qa/qa/page/group/settings/group_deploy_tokens.rb55
-rw-r--r--qa/qa/page/group/settings/repository.rb4
-rw-r--r--qa/qa/page/group/sub_menus/main.rb2
-rw-r--r--qa/qa/page/main/login.rb7
-rw-r--r--qa/qa/page/main/menu.rb20
-rw-r--r--qa/qa/page/merge_request/show.rb4
-rw-r--r--qa/qa/page/profile/menu.rb10
-rw-r--r--qa/qa/page/project/branches/show.rb20
-rw-r--r--qa/qa/page/project/commit/show.rb34
-rw-r--r--qa/qa/page/project/import/github.rb43
-rw-r--r--qa/qa/page/project/issue/show.rb1
-rw-r--r--qa/qa/page/project/job/show.rb10
-rw-r--r--qa/qa/page/project/monitor/incidents/show.rb40
-rw-r--r--qa/qa/page/project/pipeline/index.rb4
-rw-r--r--qa/qa/page/project/pipeline/show.rb4
-rw-r--r--qa/qa/page/project/settings/branch_rules.rb12
-rw-r--r--qa/qa/page/project/settings/branch_rules_details.rb14
-rw-r--r--qa/qa/page/project/settings/ci_variables.rb25
-rw-r--r--qa/qa/page/project/settings/default_branch.rb32
-rw-r--r--qa/qa/page/project/settings/deploy_tokens.rb84
-rw-r--r--qa/qa/page/project/settings/merge_request.rb12
-rw-r--r--qa/qa/page/project/settings/mirroring_repositories.rb58
-rw-r--r--qa/qa/page/project/settings/pages.rb4
-rw-r--r--qa/qa/page/project/settings/project_deploy_tokens.rb13
-rw-r--r--qa/qa/page/project/settings/protected_branches.rb4
-rw-r--r--qa/qa/page/project/settings/protected_tags.rb18
-rw-r--r--qa/qa/page/project/settings/repository.rb30
-rw-r--r--qa/qa/page/project/show.rb8
-rw-r--r--qa/qa/page/project/sub_menus/main.rb2
-rw-r--r--qa/qa/page/project/web_ide/vscode.rb46
-rw-r--r--qa/qa/page/registration/sign_up.rb2
-rw-r--r--qa/qa/page/search/results.rb17
-rw-r--r--qa/qa/page/settings/common.rb2
-rw-r--r--qa/qa/page/sub_menus/common.rb2
-rw-r--r--qa/qa/page/sub_menus/main.rb4
-rw-r--r--qa/qa/page/user/show.rb2
-rw-r--r--qa/qa/resource/group_base.rb40
-rw-r--r--qa/qa/resource/group_deploy_token.rb2
-rw-r--r--qa/qa/resource/import_project.rb30
-rw-r--r--qa/qa/resource/issue.rb7
-rw-r--r--qa/qa/resource/personal_access_token.rb18
-rw-r--r--qa/qa/resource/project.rb8
-rw-r--r--qa/qa/runtime/canary.rb27
-rw-r--r--qa/qa/runtime/path.rb4
-rw-r--r--qa/qa/service/docker_run/base.rb8
-rw-r--r--qa/qa/service/docker_run/gitlab.rb5
-rw-r--r--qa/qa/specs/features/api/10_govern/group_access_token_spec.rb2
-rw-r--r--qa/qa/specs/features/api/10_govern/project_access_token_spec.rb2
-rw-r--r--qa/qa/specs/features/api/1_manage/import/import_github_repo_spec.rb10
-rw-r--r--qa/qa/specs/features/api/1_manage/import/import_large_github_repo_spec.rb207
-rw-r--r--qa/qa/specs/features/api/1_manage/migration/gitlab_migration_issue_spec.rb5
-rw-r--r--qa/qa/specs/features/api/1_manage/migration/gitlab_migration_large_project_spec.rb39
-rw-r--r--qa/qa/specs/features/api/1_manage/migration/gitlab_migration_mr_spec.rb9
-rw-r--r--qa/qa/specs/features/api/3_create/repository/default_branch_name_setting_spec.rb6
-rw-r--r--qa/qa/specs/features/api/4_verify/api_variable_inheritance_with_forward_pipeline_variables_spec.rb1
-rw-r--r--qa/qa/specs/features/api/4_verify/file_variable_downstream_pipeline_spec.rb7
-rw-r--r--qa/qa/specs/features/api/4_verify/file_variable_spec.rb11
-rw-r--r--qa/qa/specs/features/api/4_verify/job_downloads_artifacts_spec.rb57
-rw-r--r--qa/qa/specs/features/browser_ui/10_govern/group/group_access_token_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/10_govern/login/2fa_recovery_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/10_govern/login/2fa_ssh_recovery_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/10_govern/login/log_in_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/10_govern/login/log_in_with_2fa_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/10_govern/login/log_into_gitlab_via_ldap_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/10_govern/login/log_into_mattermost_via_gitlab_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/10_govern/login/login_via_instance_wide_saml_sso_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/10_govern/login/login_via_oauth_and_oidc_with_gitlab_as_idp_spec.rb13
-rw-r--r--qa/qa/specs/features/browser_ui/10_govern/login/oauth_login_with_facebook_spec.rb8
-rw-r--r--qa/qa/specs/features/browser_ui/10_govern/login/oauth_login_with_github_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/10_govern/login/register_spec.rb10
-rw-r--r--qa/qa/specs/features/browser_ui/10_govern/project/project_access_token_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/10_govern/user/impersonation_token_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/10_govern/user/user_access_termination_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/pages/new_static_page_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/file/edit_file_via_web_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb5
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_lfs_over_http_spec.rb8
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_over_http_spec.rb9
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/source_editor/source_editor_toolbar_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/web_ide/add_new_directory_in_web_ide_spec.rb7
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/web_ide/upload_new_file_in_web_ide_spec.rb7
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/web_ide_old/upload_new_file_in_web_ide_spec.rb91
-rw-r--r--qa/qa/specs/features/browser_ui/4_verify/ci_job_artifacts/expose_job_artifacts_in_mr_spec.rb13
-rw-r--r--qa/qa/specs/features/browser_ui/4_verify/ci_job_artifacts/unlocking_job_artifacts_across_parent_child_pipelines_spec.rb6
-rw-r--r--qa/qa/specs/features/browser_ui/4_verify/ci_job_artifacts/unlocking_job_artifacts_across_pipelines_spec.rb17
-rw-r--r--qa/qa/specs/features/browser_ui/4_verify/ci_project_artifacts/user_can_bulk_delete_artifacts_spec.rb15
-rw-r--r--qa/qa/specs/features/browser_ui/4_verify/ci_variable/pipeline_with_protected_variable_spec.rb6
-rw-r--r--qa/qa/specs/features/browser_ui/4_verify/ci_variable/prefill_variables_spec.rb4
-rw-r--r--qa/qa/specs/features/browser_ui/4_verify/ci_variable/raw_variables_defined_in_yaml_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/4_verify/ci_variable/ui_variable_inheritable_when_forward_pipeline_variables_true_spec.rb1
-rw-r--r--qa/qa/specs/features/browser_ui/4_verify/ci_variable/ui_variable_non_inheritable_when_forward_pipeline_variables_false_spec.rb1
-rw-r--r--qa/qa/specs/features/browser_ui/4_verify/pipeline/include_multiple_files_from_a_project_spec.rb3
-rw-r--r--qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_with_image_pull_policy_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/4_verify/pipeline/run_pipeline_with_manual_jobs_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/4_verify/runner/fleet_management/group_runner_counts_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/5_package/package_registry/composer_registry_spec.rb20
-rw-r--r--qa/qa/specs/features/browser_ui/5_package/package_registry/conan_repository_spec.rb18
-rw-r--r--qa/qa/specs/features/browser_ui/5_package/package_registry/generic_repository_spec.rb18
-rw-r--r--qa/qa/specs/features/browser_ui/5_package/package_registry/helm_registry_spec.rb31
-rw-r--r--qa/qa/specs/features/browser_ui/5_package/package_registry/maven/maven_group_level_spec.rb69
-rw-r--r--qa/qa/specs/features/browser_ui/5_package/package_registry/maven/maven_project_level_spec.rb58
-rw-r--r--qa/qa/specs/features/browser_ui/5_package/package_registry/maven_gradle_repository_spec.rb19
-rw-r--r--qa/qa/specs/features/browser_ui/5_package/package_registry/npm/npm_group_level_spec.rb84
-rw-r--r--qa/qa/specs/features/browser_ui/5_package/package_registry/npm/npm_instance_level_spec.rb76
-rw-r--r--qa/qa/specs/features/browser_ui/5_package/package_registry/npm/npm_project_level_spec.rb53
-rw-r--r--qa/qa/specs/features/browser_ui/5_package/package_registry/nuget/nuget_group_level_spec.rb81
-rw-r--r--qa/qa/specs/features/browser_ui/5_package/package_registry/nuget/nuget_project_level_spec.rb131
-rw-r--r--qa/qa/specs/features/browser_ui/5_package/package_registry/pypi_repository_spec.rb48
-rw-r--r--qa/qa/specs/features/browser_ui/5_package/package_registry/rubygems_registry_spec.rb72
-rw-r--r--qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb27
-rw-r--r--qa/qa/specs/features/browser_ui/9_data_stores/group/transfer_project_spec.rb7
-rw-r--r--qa/qa/specs/features/browser_ui/9_data_stores/project/create_project_spec.rb6
-rw-r--r--qa/qa/specs/features/shared_contexts/import/gitlab_group_migration_common.rb23
-rw-r--r--qa/qa/specs/features/shared_contexts/packages_registry_shared_context.rb27
-rw-r--r--qa/qa/specs/features/shared_contexts/variable_inheritance_shared_context.rb58
-rw-r--r--qa/qa/specs/features/shared_examples/merge_with_code_owner_shared_examples.rb23
-rw-r--r--qa/qa/support/helpers/plan.rb12
-rw-r--r--qa/qa/support/matchers/have_matcher.rb25
-rw-r--r--qa/qa/tools/ci/qa_changes.rb12
-rw-r--r--qa/qa/tools/delete_user_projects.rb9
-rw-r--r--qa/qa/tools/generate_import_test_group.rb123
-rw-r--r--qa/qa/tools/long_running_spec_reporter.rb2
-rw-r--r--rubocop/batched_background_migrations.rb32
-rw-r--r--rubocop/batched_background_migrations_dictionary.rb47
-rw-r--r--rubocop/cop/background_migration/dictionary_file.rb78
-rw-r--r--rubocop/cop/background_migration/missing_dictionary_file.rb59
-rw-r--r--rubocop/cop/gitlab/avoid_gitlab_instance_checks.rb2
-rw-r--r--rubocop/cop/gitlab/doc_url.rb2
-rw-r--r--rubocop/cop/gitlab/mark_used_feature_flags.rb11
-rw-r--r--rubocop/cop/migration/migration_with_milestone.rb28
-rw-r--r--rubocop/cop/migration/prevent_index_creation.rb4
-rw-r--r--rubocop/cop/migration/unfinished_dependencies.rb4
-rw-r--r--rubocop/cop/migration/with_lock_retries_disallowed_method.rb8
-rw-r--r--rubocop/cop/style/inline_disable_annotation.rb51
-rw-r--r--rubocop/rubocop-code_reuse.yml1
-rw-r--r--rubocop/rubocop.rb6
-rw-r--r--scripts/database/query_analyzers.rb23
-rw-r--r--scripts/database/query_analyzers/base.rb37
-rw-r--r--scripts/database/query_analyzers/multiple_partition_scan_detector.rb35
-rwxr-xr-xscripts/duo_chat/reporter.rb233
-rwxr-xr-xscripts/frontend/create_jsconfig.js14
-rw-r--r--scripts/frontend/startup_css/clean_css.js83
-rw-r--r--scripts/frontend/startup_css/constants.js108
-rw-r--r--scripts/frontend/startup_css/get_css_path.js22
-rw-r--r--scripts/frontend/startup_css/get_startup_css.js71
-rw-r--r--scripts/frontend/startup_css/main.js60
-rwxr-xr-xscripts/frontend/startup_css/setup.sh76
-rwxr-xr-xscripts/frontend/startup_css/startup_css_changed.sh51
-rw-r--r--scripts/frontend/startup_css/utils.js8
-rw-r--r--scripts/frontend/startup_css/write_startup_scss.js28
-rw-r--r--scripts/internal_events/monitor.rb52
-rw-r--r--scripts/lib/glfm/update_example_snapshots.rb2
-rwxr-xr-xscripts/lint-doc.sh28
-rwxr-xr-xscripts/lint-rugged50
-rwxr-xr-xscripts/lint/ruby-metrics-abc.rb62
-rwxr-xr-xscripts/merge-auto-explain-logs47
-rw-r--r--scripts/prepare_build.sh1
-rwxr-xr-xscripts/regenerate-schema62
-rw-r--r--scripts/review_apps/base-config.yaml6
-rw-r--r--scripts/rspec_helpers.sh10
-rwxr-xr-xscripts/static-analysis1
-rw-r--r--spec/click_house/migration_support/migration_context_spec.rb233
-rw-r--r--spec/components/projects/ml/models_index_component_spec.rb11
-rw-r--r--spec/components/projects/ml/show_ml_model_component_spec.rb7
-rw-r--r--spec/components/projects/ml/show_ml_model_version_component_spec.rb35
-rw-r--r--spec/controllers/admin/application_settings_controller_spec.rb94
-rw-r--r--spec/controllers/autocomplete_controller_spec.rb106
-rw-r--r--spec/controllers/groups/settings/applications_controller_spec.rb357
-rw-r--r--spec/controllers/import/bulk_imports_controller_spec.rb27
-rw-r--r--spec/controllers/oauth/jira_dvcs/authorizations_controller_spec.rb71
-rw-r--r--spec/controllers/profiles/notifications_controller_spec.rb2
-rw-r--r--spec/controllers/profiles_controller_spec.rb10
-rw-r--r--spec/controllers/projects/artifacts_controller_spec.rb28
-rw-r--r--spec/controllers/projects/blob_controller_spec.rb28
-rw-r--r--spec/controllers/projects/group_links_controller_spec.rb56
-rw-r--r--spec/controllers/projects/jobs_controller_spec.rb58
-rw-r--r--spec/controllers/projects/merge_requests/drafts_controller_spec.rb45
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb147
-rw-r--r--spec/controllers/projects/pages_controller_spec.rb12
-rw-r--r--spec/controllers/projects/tree_controller_spec.rb30
-rw-r--r--spec/controllers/repositories/git_http_controller_spec.rb20
-rw-r--r--spec/controllers/search_controller_spec.rb28
-rw-r--r--spec/db/docs_spec.rb7
-rw-r--r--spec/db/migration_spec.rb3
-rw-r--r--spec/experiments/ios_specific_templates_experiment_spec.rb62
-rw-r--r--spec/factories/activity_pub/releases_subscriptions.rb26
-rw-r--r--spec/factories/ai/service_access_tokens.rb5
-rw-r--r--spec/factories/ci/build_trace_chunks.rb16
-rw-r--r--spec/factories/ci/builds.rb5
-rw-r--r--spec/factories/ci/catalog/resources/components.rb2
-rw-r--r--spec/factories/ci/pipeline_artifacts.rb8
-rw-r--r--spec/factories/ci/reports/security/findings.rb2
-rw-r--r--spec/factories/commit_statuses.rb4
-rw-r--r--spec/factories/ml/model_metadata.rb10
-rw-r--r--spec/factories/ml/models.rb6
-rw-r--r--spec/factories/packages/npm/metadata_cache.rb10
-rw-r--r--spec/factories/packages/nuget/symbol.rb1
-rw-r--r--spec/factories/pages_domains.rb81
-rw-r--r--spec/factories/projects.rb33
-rw-r--r--spec/factories/snippet_repositories.rb8
-rw-r--r--spec/factories/terraform/state_version.rb8
-rw-r--r--spec/factories/users.rb4
-rw-r--r--spec/factories/users/phone_number_validations.rb1
-rw-r--r--spec/fast_spec_helper.rb1
-rw-r--r--spec/features/abuse_report_spec.rb120
-rw-r--r--spec/features/admin/admin_browse_spam_logs_spec.rb14
-rw-r--r--spec/features/admin/admin_deploy_keys_spec.rb10
-rw-r--r--spec/features/admin/admin_dev_ops_reports_spec.rb4
-rw-r--r--spec/features/admin/admin_groups_spec.rb2
-rw-r--r--spec/features/admin/admin_hooks_spec.rb6
-rw-r--r--spec/features/admin/admin_jobs_spec.rb2
-rw-r--r--spec/features/admin/admin_mode/logout_spec.rb15
-rw-r--r--spec/features/admin/admin_mode/workers_spec.rb6
-rw-r--r--spec/features/admin/admin_mode_spec.rb47
-rw-r--r--spec/features/admin/admin_projects_spec.rb2
-rw-r--r--spec/features/admin/admin_runners_spec.rb98
-rw-r--r--spec/features/admin/admin_sees_background_migrations_spec.rb16
-rw-r--r--spec/features/admin/admin_settings_spec.rb148
-rw-r--r--spec/features/admin/admin_users_spec.rb8
-rw-r--r--spec/features/admin/admin_uses_repository_checks_spec.rb2
-rw-r--r--spec/features/admin/broadcast_messages_spec.rb8
-rw-r--r--spec/features/admin/users/user_spec.rb26
-rw-r--r--spec/features/admin/users/users_spec.rb12
-rw-r--r--spec/features/admin_variables_spec.rb13
-rw-r--r--spec/features/alert_management/alert_details_spec.rb12
-rw-r--r--spec/features/alert_management/alert_management_list_spec.rb6
-rw-r--r--spec/features/boards/board_filters_spec.rb3
-rw-r--r--spec/features/boards/boards_spec.rb9
-rw-r--r--spec/features/boards/issue_ordering_spec.rb3
-rw-r--r--spec/features/boards/multiple_boards_spec.rb2
-rw-r--r--spec/features/boards/new_issue_spec.rb8
-rw-r--r--spec/features/boards/reload_boards_on_browser_back_spec.rb2
-rw-r--r--spec/features/boards/sidebar_assignee_spec.rb2
-rw-r--r--spec/features/boards/sidebar_labels_in_namespaces_spec.rb2
-rw-r--r--spec/features/boards/sidebar_labels_spec.rb1
-rw-r--r--spec/features/boards/sidebar_spec.rb1
-rw-r--r--spec/features/boards/user_adds_lists_to_board_spec.rb1
-rw-r--r--spec/features/boards/user_visits_board_spec.rb3
-rw-r--r--spec/features/broadcast_messages_spec.rb8
-rw-r--r--spec/features/calendar_spec.rb3
-rw-r--r--spec/features/callouts/registration_enabled_spec.rb2
-rw-r--r--spec/features/clusters/create_agent_spec.rb2
-rw-r--r--spec/features/commit_spec.rb8
-rw-r--r--spec/features/commits_spec.rb10
-rw-r--r--spec/features/contextual_sidebar_spec.rb32
-rw-r--r--spec/features/dashboard/activity_spec.rb6
-rw-r--r--spec/features/dashboard/group_dashboard_with_external_authorization_service_spec.rb43
-rw-r--r--spec/features/dashboard/group_spec.rb2
-rw-r--r--spec/features/dashboard/groups_list_spec.rb2
-rw-r--r--spec/features/dashboard/issuables_counter_spec.rb36
-rw-r--r--spec/features/dashboard/issues_spec.rb20
-rw-r--r--spec/features/dashboard/merge_requests_spec.rb46
-rw-r--r--spec/features/dashboard/milestones_spec.rb12
-rw-r--r--spec/features/dashboard/navbar_spec.rb4
-rw-r--r--spec/features/dashboard/projects_spec.rb16
-rw-r--r--spec/features/dashboard/shortcuts_spec.rb1
-rw-r--r--spec/features/dashboard/snippets_spec.rb8
-rw-r--r--spec/features/dashboard/todos/todos_spec.rb16
-rw-r--r--spec/features/explore/catalog_spec.rb80
-rw-r--r--spec/features/explore/navbar_spec.rb15
-rw-r--r--spec/features/explore/user_explores_projects_spec.rb35
-rw-r--r--spec/features/global_search_spec.rb17
-rw-r--r--spec/features/group_variables_spec.rb15
-rw-r--r--spec/features/groups/board_sidebar_spec.rb1
-rw-r--r--spec/features/groups/board_spec.rb4
-rw-r--r--spec/features/groups/container_registry_spec.rb6
-rw-r--r--spec/features/groups/dependency_proxy_spec.rb8
-rw-r--r--spec/features/groups/group_page_with_external_authorization_service_spec.rb16
-rw-r--r--spec/features/groups/group_settings_spec.rb2
-rw-r--r--spec/features/groups/members/request_access_spec.rb11
-rw-r--r--spec/features/groups/members/sort_members_spec.rb2
-rw-r--r--spec/features/groups/merge_requests_spec.rb6
-rw-r--r--spec/features/groups/navbar_spec.rb14
-rw-r--r--spec/features/groups/new_group_page_spec.rb41
-rw-r--r--spec/features/groups/packages_spec.rb4
-rw-r--r--spec/features/groups/settings/packages_and_registries_spec.rb32
-rw-r--r--spec/features/groups/show_spec.rb2
-rw-r--r--spec/features/groups/user_sees_package_sidebar_spec.rb15
-rw-r--r--spec/features/groups_spec.rb25
-rw-r--r--spec/features/help_dropdown_spec.rb89
-rw-r--r--spec/features/ide/user_opens_merge_request_spec.rb6
-rw-r--r--spec/features/invites_spec.rb3
-rw-r--r--spec/features/issuables/shortcuts_issuable_spec.rb4
-rw-r--r--spec/features/issues/discussion_lock_spec.rb6
-rw-r--r--spec/features/issues/filtered_search/visual_tokens_spec.rb6
-rw-r--r--spec/features/issues/form_spec.rb24
-rw-r--r--spec/features/issues/issue_sidebar_spec.rb183
-rw-r--r--spec/features/issues/issue_state_spec.rb4
-rw-r--r--spec/features/issues/move_spec.rb4
-rw-r--r--spec/features/issues/service_desk_spec.rb8
-rw-r--r--spec/features/issues/todo_spec.rb10
-rw-r--r--spec/features/issues/user_comments_on_issue_spec.rb4
-rw-r--r--spec/features/issues/user_creates_issue_spec.rb17
-rw-r--r--spec/features/issues/user_edits_issue_spec.rb181
-rw-r--r--spec/features/issues/user_interacts_with_awards_spec.rb16
-rw-r--r--spec/features/issues/user_uses_quick_actions_spec.rb6
-rw-r--r--spec/features/jira_connect/branches_spec.rb4
-rw-r--r--spec/features/jira_oauth_provider_authorize_spec.rb42
-rw-r--r--spec/features/labels_hierarchy_spec.rb4
-rw-r--r--spec/features/markdown/keyboard_shortcuts_spec.rb4
-rw-r--r--spec/features/merge_request/maintainer_edits_fork_spec.rb5
-rw-r--r--spec/features/merge_request/merge_request_discussion_lock_spec.rb4
-rw-r--r--spec/features/merge_request/user_awards_emoji_spec.rb2
-rw-r--r--spec/features/merge_request/user_edits_assignees_sidebar_spec.rb207
-rw-r--r--spec/features/merge_request/user_locks_discussion_spec.rb2
-rw-r--r--spec/features/merge_request/user_merges_immediately_spec.rb2
-rw-r--r--spec/features/merge_request/user_merges_merge_request_spec.rb5
-rw-r--r--spec/features/merge_request/user_opens_checkout_branch_modal_spec.rb10
-rw-r--r--spec/features/merge_request/user_reverts_merge_request_spec.rb15
-rw-r--r--spec/features/merge_request/user_sees_check_out_branch_modal_spec.rb8
-rw-r--r--spec/features/merge_request/user_sees_merge_request_file_tree_sidebar_spec.rb56
-rw-r--r--spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb14
-rw-r--r--spec/features/merge_request/user_sees_merge_widget_spec.rb5
-rw-r--r--spec/features/merge_request/user_sees_pipelines_spec.rb3
-rw-r--r--spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb8
-rw-r--r--spec/features/merge_request/user_uses_quick_actions_spec.rb6
-rw-r--r--spec/features/merge_requests/user_filters_by_source_branch_spec.rb65
-rw-r--r--spec/features/monitor_sidebar_link_spec.rb102
-rw-r--r--spec/features/nav/new_nav_callout_spec.rb64
-rw-r--r--spec/features/nav/new_nav_for_everyone_callout_spec.rb55
-rw-r--r--spec/features/nav/new_nav_invite_members_spec.rb2
-rw-r--r--spec/features/nav/new_nav_toggle_spec.rb59
-rw-r--r--spec/features/nav/pinned_nav_items_spec.rb2
-rw-r--r--spec/features/nav/top_nav_responsive_spec.rb101
-rw-r--r--spec/features/nav/top_nav_spec.rb51
-rw-r--r--spec/features/nav/top_nav_tooltip_spec.rb25
-rw-r--r--spec/features/profiles/two_factor_auths_spec.rb2
-rw-r--r--spec/features/profiles/user_edit_profile_spec.rb94
-rw-r--r--spec/features/profiles/user_visits_profile_account_page_spec.rb17
-rw-r--r--spec/features/profiles/user_visits_profile_authentication_log_spec.rb14
-rw-r--r--spec/features/profiles/user_visits_profile_preferences_page_spec.rb8
-rw-r--r--spec/features/profiles/user_visits_profile_spec.rb10
-rw-r--r--spec/features/profiles/user_visits_profile_ssh_keys_page_spec.rb17
-rw-r--r--spec/features/project_variables_spec.rb36
-rw-r--r--spec/features/projects/active_tabs_spec.rb94
-rw-r--r--spec/features/projects/blobs/blob_show_spec.rb4
-rw-r--r--spec/features/projects/blobs/edit_spec.rb2
-rw-r--r--spec/features/projects/branches/user_creates_branch_spec.rb2
-rw-r--r--spec/features/projects/branches_spec.rb6
-rw-r--r--spec/features/projects/ci/editor_spec.rb8
-rw-r--r--spec/features/projects/clusters/gcp_spec.rb4
-rw-r--r--spec/features/projects/clusters/user_spec.rb4
-rw-r--r--spec/features/projects/clusters_spec.rb6
-rw-r--r--spec/features/projects/commit/comments/user_adds_comment_spec.rb2
-rw-r--r--spec/features/projects/commit/mini_pipeline_graph_spec.rb4
-rw-r--r--spec/features/projects/commit/user_sees_pipelines_tab_spec.rb3
-rw-r--r--spec/features/projects/compare_spec.rb9
-rw-r--r--spec/features/projects/confluence/user_views_confluence_page_spec.rb8
-rw-r--r--spec/features/projects/environments/environments_spec.rb20
-rw-r--r--spec/features/projects/features_visibility_spec.rb85
-rw-r--r--spec/features/projects/files/project_owner_creates_license_file_spec.rb4
-rw-r--r--spec/features/projects/files/user_edits_files_spec.rb19
-rw-r--r--spec/features/projects/files/user_find_file_spec.rb14
-rw-r--r--spec/features/projects/files/user_reads_pipeline_status_spec.rb2
-rw-r--r--spec/features/projects/files/user_searches_for_files_spec.rb6
-rw-r--r--spec/features/projects/forks/fork_list_spec.rb2
-rw-r--r--spec/features/projects/graph_spec.rb4
-rw-r--r--spec/features/projects/integrations/user_activates_issue_tracker_spec.rb6
-rw-r--r--spec/features/projects/integrations/user_activates_jira_spec.rb14
-rw-r--r--spec/features/projects/issuable_templates_spec.rb21
-rw-r--r--spec/features/projects/issues/email_participants_spec.rb14
-rw-r--r--spec/features/projects/jobs/user_browses_jobs_spec.rb8
-rw-r--r--spec/features/projects/jobs_spec.rb2
-rw-r--r--spec/features/projects/members/sorting_spec.rb2
-rw-r--r--spec/features/projects/members/user_requests_access_spec.rb11
-rw-r--r--spec/features/projects/milestones/user_interacts_with_labels_spec.rb6
-rw-r--r--spec/features/projects/navbar_spec.rb34
-rw-r--r--spec/features/projects/network_graph_spec.rb8
-rw-r--r--spec/features/projects/new_project_spec.rb91
-rw-r--r--spec/features/projects/pages/user_configures_pages_pipeline_spec.rb38
-rw-r--r--spec/features/projects/pages/user_edits_settings_spec.rb16
-rw-r--r--spec/features/projects/pipelines/pipeline_spec.rb60
-rw-r--r--spec/features/projects/pipelines/pipelines_spec.rb49
-rw-r--r--spec/features/projects/releases/user_creates_release_spec.rb3
-rw-r--r--spec/features/projects/releases/user_views_edit_release_spec.rb8
-rw-r--r--spec/features/projects/settings/monitor_settings_spec.rb11
-rw-r--r--spec/features/projects/settings/registry_settings_cleanup_tags_spec.rb11
-rw-r--r--spec/features/projects/settings/registry_settings_spec.rb11
-rw-r--r--spec/features/projects/show/user_sees_collaboration_links_spec.rb8
-rw-r--r--spec/features/projects/snippets/show_spec.rb59
-rw-r--r--spec/features/projects/tree/tree_show_spec.rb18
-rw-r--r--spec/features/projects/user_sees_sidebar_spec.rb88
-rw-r--r--spec/features/projects/user_uses_shortcuts_spec.rb59
-rw-r--r--spec/features/projects/wikis_spec.rb2
-rw-r--r--spec/features/projects/work_items/linked_work_items_spec.rb4
-rw-r--r--spec/features/projects/work_items/work_item_children_spec.rb4
-rw-r--r--spec/features/projects/work_items/work_item_spec.rb35
-rw-r--r--spec/features/projects_spec.rb26
-rw-r--r--spec/features/search/user_searches_for_code_spec.rb2
-rw-r--r--spec/features/search/user_searches_for_comments_spec.rb2
-rw-r--r--spec/features/search/user_searches_for_commits_spec.rb2
-rw-r--r--spec/features/search/user_searches_for_issues_spec.rb2
-rw-r--r--spec/features/search/user_searches_for_merge_requests_spec.rb2
-rw-r--r--spec/features/search/user_searches_for_milestones_spec.rb2
-rw-r--r--spec/features/search/user_searches_for_projects_spec.rb2
-rw-r--r--spec/features/search/user_searches_for_users_spec.rb6
-rw-r--r--spec/features/search/user_searches_for_wiki_pages_spec.rb2
-rw-r--r--spec/features/search/user_uses_header_search_field_spec.rb82
-rw-r--r--spec/features/snippets/search_snippets_spec.rb2
-rw-r--r--spec/features/snippets/show_spec.rb66
-rw-r--r--spec/features/snippets/user_creates_snippet_spec.rb2
-rw-r--r--spec/features/task_lists_spec.rb6
-rw-r--r--spec/features/unsubscribe_links_spec.rb8
-rw-r--r--spec/features/uploads/user_uploads_avatar_to_profile_spec.rb5
-rw-r--r--spec/features/usage_stats_consent_spec.rb29
-rw-r--r--spec/features/user_can_display_performance_bar_spec.rb2
-rw-r--r--spec/features/user_sees_active_nav_items_spec.rb63
-rw-r--r--spec/features/user_sees_revert_modal_spec.rb7
-rw-r--r--spec/features/users/active_sessions_spec.rb18
-rw-r--r--spec/features/users/anonymous_sessions_spec.rb8
-rw-r--r--spec/features/users/email_verification_on_login_spec.rb6
-rw-r--r--spec/features/users/login_spec.rb34
-rw-r--r--spec/features/users/logout_spec.rb2
-rw-r--r--spec/features/users/overview_spec.rb43
-rw-r--r--spec/features/users/rss_spec.rb55
-rw-r--r--spec/features/users/show_spec.rb54
-rw-r--r--spec/features/users/signup_spec.rb2
-rw-r--r--spec/features/users/snippets_spec.rb12
-rw-r--r--spec/features/users/terms_spec.rb8
-rw-r--r--spec/features/users/user_browses_projects_on_user_page_spec.rb8
-rw-r--r--spec/features/webauthn_spec.rb14
-rw-r--r--spec/features/whats_new_spec.rb50
-rw-r--r--spec/finders/ci/catalog/resources/versions_finder_spec.rb106
-rw-r--r--spec/finders/ci/runners_finder_spec.rb64
-rw-r--r--spec/finders/data_transfer/mocked_transfer_finder_spec.rb22
-rw-r--r--spec/finders/milestones_finder_spec.rb2
-rw-r--r--spec/finders/organizations/user_organizations_finder_spec.rb50
-rw-r--r--spec/finders/packages/group_packages_finder_spec.rb2
-rw-r--r--spec/finders/packages/pypi/packages_finder_spec.rb10
-rw-r--r--spec/finders/personal_access_tokens_finder_spec.rb44
-rw-r--r--spec/finders/projects/ml/model_finder_spec.rb57
-rw-r--r--spec/finders/projects_finder_spec.rb31
-rw-r--r--spec/finders/user_group_notification_settings_finder_spec.rb50
-rw-r--r--spec/finders/vs_code/settings/settings_finder_spec.rb8
-rw-r--r--spec/fixtures/api/schemas/entities/admin_users_data_attributes_paths.json62
-rw-r--r--spec/fixtures/api/schemas/entities/member.json34
-rw-r--r--spec/fixtures/api/schemas/entities/merge_request_noteable.json6
-rw-r--r--spec/fixtures/api/schemas/graphql/packages/package_details.json12
-rw-r--r--spec/fixtures/api/schemas/graphql/packages/package_pypi_metadata.json40
-rw-r--r--spec/fixtures/api/schemas/group_link/group_link.json5
-rw-r--r--spec/fixtures/api/schemas/job/test_report_summary.json34
-rw-r--r--spec/fixtures/api/schemas/ml/get_latest_versions.json80
-rw-r--r--spec/fixtures/api/schemas/ml/get_model.json51
-rw-r--r--spec/fixtures/api/schemas/ml/update_model.json51
-rw-r--r--spec/fixtures/click_house/migrations/drop_table/1_create_some_table.rb14
-rw-r--r--spec/fixtures/click_house/migrations/drop_table/2_drop_some_table.rb11
-rw-r--r--spec/fixtures/click_house/migrations/duplicate_name/1_create_some_table.rb14
-rw-r--r--spec/fixtures/click_house/migrations/duplicate_name/2_create_some_table.rb14
-rw-r--r--spec/fixtures/click_house/migrations/duplicate_version/1_create_some_table.rb14
-rw-r--r--spec/fixtures/click_house/migrations/duplicate_version/1_drop_some_table.rb11
-rw-r--r--spec/fixtures/click_house/migrations/migration_with_error/1_migration_with_error.rb9
-rw-r--r--spec/fixtures/click_house/migrations/migrations_over_multiple_databases/1_create_some_table_on_main_db.rb15
-rw-r--r--spec/fixtures/click_house/migrations/migrations_over_multiple_databases/2_create_some_table_on_another_db.rb16
-rw-r--r--spec/fixtures/click_house/migrations/migrations_over_multiple_databases/3_change_some_table_on_main_db.rb11
-rw-r--r--spec/fixtures/click_house/migrations/plain_table_creation/1_create_some_table.rb14
-rw-r--r--spec/fixtures/click_house/migrations/plain_table_creation_on_invalid_database/1_create_some_table.rb16
-rw-r--r--spec/fixtures/click_house/migrations/table_creation_with_down_method/1_create_some_table.rb20
-rw-r--r--spec/fixtures/origin_cert_key.pem0
-rw-r--r--spec/fixtures/security_reports/master/gl-common-scanning-report.json4
-rw-r--r--spec/fixtures/tooling/danger/rubocop_todo/cop1.yml5
-rw-r--r--spec/fixtures/tooling/danger/rubocop_todo/cop2.yml4
-rw-r--r--spec/frontend/__helpers__/dom_shims/clipboard_event.js1
-rw-r--r--spec/frontend/__helpers__/dom_shims/drag_event.js1
-rw-r--r--spec/frontend/__helpers__/dom_shims/index.js2
-rw-r--r--spec/frontend/__helpers__/emoji.js11
-rw-r--r--spec/frontend/__helpers__/local_storage_helper.js3
-rw-r--r--spec/frontend/__helpers__/mock_observability_client.js19
-rw-r--r--spec/frontend/__mocks__/@gitlab/ui.js2
-rw-r--r--spec/frontend/admin/abuse_report/components/abuse_report_app_spec.js30
-rw-r--r--spec/frontend/admin/abuse_report/components/abuse_report_notes_spec.js98
-rw-r--r--spec/frontend/admin/abuse_report/components/labels_select_spec.js2
-rw-r--r--spec/frontend/admin/abuse_report/components/notes/__snapshots__/abuse_report_note_body_spec.js.snap15
-rw-r--r--spec/frontend/admin/abuse_report/components/notes/abuse_report_discussion_spec.js79
-rw-r--r--spec/frontend/admin/abuse_report/components/notes/abuse_report_note_body_spec.js27
-rw-r--r--spec/frontend/admin/abuse_report/components/notes/abuse_report_note_spec.js80
-rw-r--r--spec/frontend/admin/abuse_report/components/report_details_spec.js2
-rw-r--r--spec/frontend/admin/abuse_report/mock_data.js212
-rw-r--r--spec/frontend/admin/users/components/app_spec.js85
-rw-r--r--spec/frontend/admin/users/components/user_avatar_spec.js121
-rw-r--r--spec/frontend/admin/users/components/users_table_spec.js141
-rw-r--r--spec/frontend/admin/users/constants.js4
-rw-r--r--spec/frontend/alert_spec.js4
-rw-r--r--spec/frontend/analytics/cycle_analytics/components/filter_bar_spec.js2
-rw-r--r--spec/frontend/analytics/cycle_analytics/mock_data.js6
-rw-r--r--spec/frontend/analytics/cycle_analytics/store/mutations_spec.js4
-rw-r--r--spec/frontend/analytics/product_analytics/components/activity_chart_spec.js34
-rw-r--r--spec/frontend/analytics/shared/components/metric_tile_spec.js21
-rw-r--r--spec/frontend/authentication/two_factor_auth/components/recovery_codes_spec.js2
-rw-r--r--spec/frontend/awards_handler_spec.js31
-rw-r--r--spec/frontend/batch_comments/components/submit_dropdown_spec.js110
-rw-r--r--spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js2
-rw-r--r--spec/frontend/behaviors/gl_emoji_spec.js12
-rw-r--r--spec/frontend/behaviors/load_startup_css_spec.js48
-rw-r--r--spec/frontend/blob/components/blob_header_spec.js11
-rw-r--r--spec/frontend/blob/components/blob_header_viewer_switcher_spec.js33
-rw-r--r--spec/frontend/boards/board_card_inner_spec.js128
-rw-r--r--spec/frontend/boards/cache_updates_spec.js2
-rw-r--r--spec/frontend/boards/mock_data.js3
-rw-r--r--spec/frontend/boards/stores/actions_spec.js2
-rw-r--r--spec/frontend/ci/artifacts/components/job_artifacts_table_spec.js9
-rw-r--r--spec/frontend/ci/catalog/components/details/ci_resource_components_spec.js73
-rw-r--r--spec/frontend/ci/catalog/components/details/ci_resource_header_spec.js7
-rw-r--r--spec/frontend/ci/catalog/components/list/catalog_header_spec.js37
-rw-r--r--spec/frontend/ci/catalog/components/list/ci_resources_list_item_spec.js12
-rw-r--r--spec/frontend/ci/catalog/components/pages/ci_resources_page_spec.js211
-rw-r--r--spec/frontend/ci/catalog/global_catalog_spec.js17
-rw-r--r--spec/frontend/ci/catalog/index_spec.js48
-rw-r--r--spec/frontend/ci/catalog/mock.js53
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_variable_drawer_spec.js13
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js576
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_variable_settings_spec.js101
-rw-r--r--spec/frontend/ci/common/pipelines_table_spec.js8
-rw-r--r--spec/frontend/ci/job_details/components/job_header_spec.js6
-rw-r--r--spec/frontend/ci/job_details/components/log/line_header_spec.js21
-rw-r--r--spec/frontend/ci/job_details/components/log/mock_data.js66
-rw-r--r--spec/frontend/ci/job_details/components/sidebar/stages_dropdown_spec.js5
-rw-r--r--spec/frontend/ci/job_details/job_app_spec.js2
-rw-r--r--spec/frontend/ci/job_details/store/actions_spec.js24
-rw-r--r--spec/frontend/ci/job_details/store/mutations_spec.js90
-rw-r--r--spec/frontend/ci/job_details/store/utils_spec.js18
-rw-r--r--spec/frontend/ci/jobs_page/components/job_cells/job_cell_spec.js10
-rw-r--r--spec/frontend/ci/jobs_page/components/jobs_table_spec.js6
-rw-r--r--spec/frontend/ci/pipeline_details/graph/components/graph_component_spec.js91
-rw-r--r--spec/frontend/ci/pipeline_details/graph/components/job_item_spec.js24
-rw-r--r--spec/frontend/ci/pipeline_details/graph/components/job_name_component_spec.js2
-rw-r--r--spec/frontend/ci/pipeline_details/graph/components/linked_pipeline_spec.js2
-rw-r--r--spec/frontend/ci/pipeline_details/header/pipeline_details_header_spec.js22
-rw-r--r--spec/frontend/ci/pipeline_editor/components/file-nav/branch_switcher_spec.js134
-rw-r--r--spec/frontend/ci/pipeline_mini_graph/legacy_pipeline_stage_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_mini_graph/linked_pipelines_mini_list_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline_spec.js12
-rw-r--r--spec/frontend/ci/pipelines_page/components/empty_state/ios_templates_spec.js133
-rw-r--r--spec/frontend/ci/pipelines_page/components/empty_state/no_ci_empty_state_spec.js35
-rw-r--r--spec/frontend/ci/pipelines_page/components/pipeline_labels_spec.js44
-rw-r--r--spec/frontend/ci/pipelines_page/components/pipeline_triggerer_spec.js13
-rw-r--r--spec/frontend/ci/pipelines_page/pipelines_spec.js38
-rw-r--r--spec/frontend/ci/runner/admin_new_runner_app/admin_new_runner_app_spec.js6
-rw-r--r--spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js5
-rw-r--r--spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js54
-rw-r--r--spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js2
-rw-r--r--spec/frontend/ci/runner/components/registration/registration_feedback_banner_spec.js52
-rw-r--r--spec/frontend/ci/runner/components/registration/registration_token_spec.js2
-rw-r--r--spec/frontend/ci/runner/components/runner_created_at_spec.js97
-rw-r--r--spec/frontend/ci/runner/components/runner_filtered_search_bar_spec.js13
-rw-r--r--spec/frontend/ci/runner/components/runner_header_spec.js20
-rw-r--r--spec/frontend/ci/runner/components/runner_list_header_spec.js31
-rw-r--r--spec/frontend/ci/runner/components/runner_update_form_spec.js10
-rw-r--r--spec/frontend/ci/runner/group_new_runner_app/group_new_runner_app_spec.js6
-rw-r--r--spec/frontend/ci/runner/mock_data.js17
-rw-r--r--spec/frontend/ci/runner/project_new_runner_app/project_new_runner_app_spec.js6
-rw-r--r--spec/frontend/ci/runner/runner_edit/runner_edit_app_spec.js2
-rw-r--r--spec/frontend/ci/runner/sentry_utils_spec.js4
-rw-r--r--spec/frontend/clusters_list/components/clusters_spec.js4
-rw-r--r--spec/frontend/clusters_list/store/actions_spec.js2
-rw-r--r--spec/frontend/commit/components/commit_box_pipeline_status_spec.js12
-rw-r--r--spec/frontend/content_editor/extensions/code_block_highlight_spec.js12
-rw-r--r--spec/frontend/content_editor/extensions/copy_paste_spec.js112
-rw-r--r--spec/frontend/content_editor/extensions/horizontal_rule_spec.js2
-rw-r--r--spec/frontend/content_editor/extensions/html_marks_spec.js89
-rw-r--r--spec/frontend/content_editor/extensions/word_break_spec.js8
-rw-r--r--spec/frontend/contribution_events/components/contribution_event/contribution_event_base_spec.js2
-rw-r--r--spec/frontend/crm/organization_form_wrapper_spec.js8
-rw-r--r--spec/frontend/custom_emoji/components/delete_item_spec.js4
-rw-r--r--spec/frontend/design_management/components/design_notes/__snapshots__/design_note_signed_out_spec.js.snap4
-rw-r--r--spec/frontend/design_management/components/design_notes/__snapshots__/design_reply_form_spec.js.snap4
-rw-r--r--spec/frontend/design_management/components/design_notes/design_note_spec.js45
-rw-r--r--spec/frontend/design_management/components/design_overlay_spec.js2
-rw-r--r--spec/frontend/design_management/components/design_presentation_spec.js2
-rw-r--r--spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap12
-rw-r--r--spec/frontend/diffs/components/app_spec.js97
-rw-r--r--spec/frontend/diffs/components/diff_file_header_spec.js2
-rw-r--r--spec/frontend/diffs/components/diff_file_spec.js4
-rw-r--r--spec/frontend/diffs/components/shared/__snapshots__/findings_drawer_spec.js.snap281
-rw-r--r--spec/frontend/diffs/components/shared/findings_drawer_item_spec.js54
-rw-r--r--spec/frontend/diffs/components/shared/findings_drawer_spec.js35
-rw-r--r--spec/frontend/diffs/mock_data/findings_drawer.js33
-rw-r--r--spec/frontend/diffs/store/actions_spec.js4
-rw-r--r--spec/frontend/diffs/store/utils_spec.js2
-rw-r--r--spec/frontend/diffs/utils/sort_errors_by_file_spec.js52
-rw-r--r--spec/frontend/diffs/utils/sort_findings_by_file_spec.js66
-rw-r--r--spec/frontend/emoji/awards_app/store/actions_spec.js4
-rw-r--r--spec/frontend/emoji/index_spec.js266
-rw-r--r--spec/frontend/environments/environment_form_spec.js1
-rw-r--r--spec/frontend/environments/environments_app_spec.js47
-rw-r--r--spec/frontend/environments/graphql/mock_data.js10
-rw-r--r--spec/frontend/environments/graphql/resolvers/flux_spec.js309
-rw-r--r--spec/frontend/environments/graphql/resolvers/kubernetes_spec.js139
-rw-r--r--spec/frontend/environments/kubernetes_overview_spec.js2
-rw-r--r--spec/frontend/environments/kubernetes_status_bar_spec.js92
-rw-r--r--spec/frontend/environments/kubernetes_summary_spec.js6
-rw-r--r--spec/frontend/error_tracking/components/error_details_spec.js8
-rw-r--r--spec/frontend/fixtures/issues.rb2
-rw-r--r--spec/frontend/fixtures/merge_requests.rb8
-rw-r--r--spec/frontend/fixtures/snippet.rb2
-rw-r--r--spec/frontend/fixtures/startup_css.rb89
-rw-r--r--spec/frontend/groups/components/overview_tabs_spec.js4
-rw-r--r--spec/frontend/groups/members/utils_spec.js14
-rw-r--r--spec/frontend/header_spec.js9
-rw-r--r--spec/frontend/helpers/help_page_helper_spec.js1
-rw-r--r--spec/frontend/helpers/init_simple_app_helper_spec.js37
-rw-r--r--spec/frontend/ide/components/commit_sidebar/new_merge_request_option_spec.js28
-rw-r--r--spec/frontend/ide/components/jobs/detail/description_spec.js7
-rw-r--r--spec/frontend/ide/components/jobs/item_spec.js2
-rw-r--r--spec/frontend/ide/components/repo_editor_spec.js9
-rw-r--r--spec/frontend/import/details/components/bulk_import_details_app_spec.js18
-rw-r--r--spec/frontend/import/details/components/import_details_app_spec.js2
-rw-r--r--spec/frontend/import/details/components/import_details_table_spec.js66
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_status_spec.js99
-rw-r--r--spec/frontend/import_entities/import_projects/components/github_organizations_box_spec.js4
-rw-r--r--spec/frontend/incidents/components/incidents_list_spec.js2
-rw-r--r--spec/frontend/integrations/edit/components/dynamic_field_spec.js35
-rw-r--r--spec/frontend/integrations/edit/components/integration_form_spec.js4
-rw-r--r--spec/frontend/integrations/overrides/components/integration_overrides_spec.js2
-rw-r--r--spec/frontend/invite_members/components/invite_members_modal_spec.js60
-rw-r--r--spec/frontend/invite_members/components/members_token_select_spec.js15
-rw-r--r--spec/frontend/invite_members/utils/member_utils_spec.js16
-rw-r--r--spec/frontend/issuable/components/locked_badge_spec.js2
-rw-r--r--spec/frontend/issuable/components/status_badge_spec.js8
-rw-r--r--spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js4
-rw-r--r--spec/frontend/issues/issue_spec.js12
-rw-r--r--spec/frontend/issues/list/components/issues_list_app_spec.js4
-rw-r--r--spec/frontend/issues/service_desk/components/service_desk_list_app_spec.js4
-rw-r--r--spec/frontend/issues/show/components/header_actions_spec.js52
-rw-r--r--spec/frontend/issues/show/components/issue_header_spec.js6
-rw-r--r--spec/frontend/issues/show/components/sticky_header_spec.js12
-rw-r--r--spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions_spec.js44
-rw-r--r--spec/frontend/lib/utils/color_utils_spec.js10
-rw-r--r--spec/frontend/lib/utils/common_utils_spec.js24
-rw-r--r--spec/frontend/lib/utils/datetime/timeago_utility_spec.js19
-rw-r--r--spec/frontend/lib/utils/forms_spec.js28
-rw-r--r--spec/frontend/members/components/avatars/group_avatar_spec.js25
-rw-r--r--spec/frontend/members/components/icons/private_icon_spec.js30
-rw-r--r--spec/frontend/members/components/table/member_source_spec.js15
-rw-r--r--spec/frontend/members/components/table/members_table_spec.js19
-rw-r--r--spec/frontend/members/components/table/role_dropdown_spec.js12
-rw-r--r--spec/frontend/members/mock_data.js16
-rw-r--r--spec/frontend/members/store/actions_spec.js10
-rw-r--r--spec/frontend/members/utils_spec.js45
-rw-r--r--spec/frontend/merge_requests/components/sticky_header_spec.js48
-rw-r--r--spec/frontend/milestones/components/milestone_combobox_spec.js228
-rw-r--r--spec/frontend/milestones/stores/mutations_spec.js24
-rw-r--r--spec/frontend/ml/model_registry/apps/index_ml_models_spec.js83
-rw-r--r--spec/frontend/ml/model_registry/apps/show_ml_model_spec.js75
-rw-r--r--spec/frontend/ml/model_registry/apps/show_ml_model_version_spec.js15
-rw-r--r--spec/frontend/ml/model_registry/components/model_row_spec.js45
-rw-r--r--spec/frontend/ml/model_registry/components/search_bar_spec.js86
-rw-r--r--spec/frontend/ml/model_registry/mock_data.js49
-rw-r--r--spec/frontend/ml/model_registry/routes/models/index/components/ml_models_index_spec.js63
-rw-r--r--spec/frontend/ml/model_registry/routes/models/index/components/model_row_spec.js42
-rw-r--r--spec/frontend/notes/components/comment_field_layout_spec.js26
-rw-r--r--spec/frontend/observability/client_spec.js221
-rw-r--r--spec/frontend/observability/loader_spec.js103
-rw-r--r--spec/frontend/observability/observability_container_spec.js128
-rw-r--r--spec/frontend/observability/observability_empty_state_spec.js36
-rw-r--r--spec/frontend/observability/provisioned_observability_container_spec.js156
-rw-r--r--spec/frontend/observability/skeleton_spec.js144
-rw-r--r--spec/frontend/organizations/new/components/app_spec.js86
-rw-r--r--spec/frontend/organizations/settings/general/components/app_spec.js19
-rw-r--r--spec/frontend/organizations/settings/general/components/organization_settings_spec.js126
-rw-r--r--spec/frontend/organizations/shared/components/new_edit_form_spec.js118
-rw-r--r--spec/frontend/organizations/users/components/app_spec.js81
-rw-r--r--spec/frontend/organizations/users/mock_data.js22
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap2
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/additional_metadata_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/publish_method_spec.js.snap2
-rw-r--r--spec/frontend/packages_and_registries/package_registry/mock_data.js2
-rw-r--r--spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js56
-rw-r--r--spec/frontend/packages_and_registries/settings/group/mock_data.js2
-rw-r--r--spec/frontend/pages/admin/application_settings/appearances/preview_sign_in/index_spec.js10
-rw-r--r--spec/frontend/pages/groups/sso/index_spec.js10
-rw-r--r--spec/frontend/pages/import/bulk_imports/details/index_spec.js37
-rw-r--r--spec/frontend/pages/passwords/new/index_spec.js10
-rw-r--r--spec/frontend/pages/projects/forks/new/components/fork_form_spec.js42
-rw-r--r--spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js2
-rw-r--r--spec/frontend/pages/registrations/new/index_spec.js10
-rw-r--r--spec/frontend/pages/sessions/new/index_spec.js10
-rw-r--r--spec/frontend/pipeline_wizard/templates/pages_spec.js6
-rw-r--r--spec/frontend/projects/members/utils_spec.js14
-rw-r--r--spec/frontend/projects/pipelines/charts/components/__snapshots__/statistics_list_spec.js.snap6
-rw-r--r--spec/frontend/projects/pipelines/charts/mock_data.js6
-rw-r--r--spec/frontend/projects/settings_service_desk/components/custom_email_form_spec.js34
-rw-r--r--spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js5
-rw-r--r--spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js36
-rw-r--r--spec/frontend/protected_branches/protected_branch_edit_spec.js348
-rw-r--r--spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap6
-rw-r--r--spec/frontend/repository/components/blob_content_viewer_spec.js34
-rw-r--r--spec/frontend/repository/components/commit_info_spec.js21
-rw-r--r--spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap18
-rw-r--r--spec/frontend/search/sidebar/components/app_spec.js8
-rw-r--r--spec/frontend/search/sidebar/components/blobs_filters_spec.js39
-rw-r--r--spec/frontend/search/sidebar/components/issues_filters_spec.js34
-rw-r--r--spec/frontend/search/sidebar/components/label_filter_spec.js67
-rw-r--r--spec/frontend/search/sidebar/components/merge_requests_filters_spec.js36
-rw-r--r--spec/frontend/search/store/getters_spec.js19
-rw-r--r--spec/frontend/security_configuration/components/training_provider_list_spec.js2
-rw-r--r--spec/frontend/sentry/init_sentry_spec.js4
-rw-r--r--spec/frontend/sentry/sentry_browser_wrapper_spec.js29
-rw-r--r--spec/frontend/shortcuts_spec.js40
-rw-r--r--spec/frontend/sidebar/components/lock/__snapshots__/edit_form_spec.js.snap8
-rw-r--r--spec/frontend/sidebar/components/lock/issuable_lock_form_spec.js4
-rw-r--r--spec/frontend/sidebar/components/move/issuable_move_dropdown_spec.js358
-rw-r--r--spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js2
-rw-r--r--spec/frontend/silent_mode_settings/components/app_spec.js13
-rw-r--r--spec/frontend/snippets/components/snippet_header_spec.js16
-rw-r--r--spec/frontend/super_sidebar/components/global_search/components/global_search_default_places_spec.js13
-rw-r--r--spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js43
-rw-r--r--spec/frontend/super_sidebar/components/nav_item_spec.js13
-rw-r--r--spec/frontend/super_sidebar/components/user_bar_spec.js1
-rw-r--r--spec/frontend/super_sidebar/components/user_menu_spec.js13
-rw-r--r--spec/frontend/super_sidebar/mock_data.js1
-rw-r--r--spec/frontend/super_sidebar/utils_spec.js4
-rw-r--r--spec/frontend/terraform/components/init_command_modal_spec.js12
-rw-r--r--spec/frontend/time_tracking/components/timelogs_app_spec.js4
-rw-r--r--spec/frontend/token_access/token_projects_table_spec.js21
-rw-r--r--spec/frontend/tracking/dispatch_snowplow_event_spec.js4
-rw-r--r--spec/frontend/tracking/tracking_initialization_spec.js2
-rw-r--r--spec/frontend/users/profile/components/report_abuse_button_spec.js79
-rw-r--r--spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js60
-rw-r--r--spec/frontend/vue_merge_request_widget/components/checks/conflicts_spec.js13
-rw-r--r--spec/frontend/vue_merge_request_widget/components/checks/message_spec.js16
-rw-r--r--spec/frontend/vue_merge_request_widget/components/checks/rebase_spec.js323
-rw-r--r--spec/frontend/vue_merge_request_widget/components/checks/unresolved_discussions_spec.js49
-rw-r--r--spec/frontend/vue_merge_request_widget/components/merge_checks_spec.js103
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js12
-rw-r--r--spec/frontend/vue_merge_request_widget/components/widget/app_spec.js10
-rw-r--r--spec/frontend/vue_merge_request_widget/components/widget/dynamic_content_spec.js19
-rw-r--r--spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js2
-rw-r--r--spec/frontend/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports_spec.js63
-rw-r--r--spec/frontend/vue_merge_request_widget/extentions/accessibility/index_spec.js12
-rw-r--r--spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/ci_badge_link_spec.js158
-rw-r--r--spec/frontend/vue_shared/components/ci_icon_spec.js143
-rw-r--r--spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js52
-rw-r--r--spec/frontend/vue_shared/components/ensure_data_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/entity_select/entity_select_spec.js27
-rw-r--r--spec/frontend/vue_shared/components/entity_select/organization_select_spec.js179
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js21
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js52
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/user_token_spec.js111
-rw-r--r--spec/frontend/vue_shared/components/form/errors_alert_spec.js60
-rw-r--r--spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/list_selector/group_item_spec.js55
-rw-r--r--spec/frontend/vue_shared/components/list_selector/index_spec.js257
-rw-r--r--spec/frontend/vue_shared/components/list_selector/mock_data.js41
-rw-r--r--spec/frontend/vue_shared/components/list_selector/user_item_spec.js55
-rw-r--r--spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js25
-rw-r--r--spec/frontend/vue_shared/components/notes/__snapshots__/noteable_warning_spec.js.snap6
-rw-r--r--spec/frontend/vue_shared/components/notes/noteable_warning_spec.js18
-rw-r--r--spec/frontend/vue_shared/components/notes/system_note_spec.js10
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/components/blame_info_spec.js25
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/components/chunk_new_spec.js15
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/mock_data.js78
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/source_viewer_new_spec.js104
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/utils_spec.js14
-rw-r--r--spec/frontend/vue_shared/components/users_table/mock_data.js23
-rw-r--r--spec/frontend/vue_shared/components/users_table/user_avatar_spec.js139
-rw-r--r--spec/frontend/vue_shared/components/users_table/users_table_spec.js95
-rw-r--r--spec/frontend/vue_shared/components/web_ide_link_spec.js10
-rw-r--r--spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js1
-rw-r--r--spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js2
-rw-r--r--spec/frontend/vue_shared/issuable/show/components/issuable_title_spec.js43
-rw-r--r--spec/frontend/vue_shared/mixins/gl_feature_flags_mixin_spec.js2
-rw-r--r--spec/frontend/webhooks/components/__snapshots__/push_events_spec.js.snap76
-rw-r--r--spec/frontend/work_items/components/notes/system_note_spec.js10
-rw-r--r--spec/frontend/work_items/components/notes/work_item_comment_form_spec.js4
-rw-r--r--spec/frontend/work_items/components/notes/work_item_note_actions_spec.js4
-rw-r--r--spec/frontend/work_items/components/shared/work_item_link_child_contents_spec.js60
-rw-r--r--spec/frontend/work_items/components/shared/work_item_links_menu_spec.js30
-rw-r--r--spec/frontend/work_items/components/shared/work_item_token_input_spec.js119
-rw-r--r--spec/frontend/work_items/components/work_item_actions_spec.js29
-rw-r--r--spec/frontend/work_items/components/work_item_award_emoji_spec.js27
-rw-r--r--spec/frontend/work_items/components/work_item_description_spec.js2
-rw-r--r--spec/frontend/work_items/components/work_item_detail_spec.js40
-rw-r--r--spec/frontend/work_items/components/work_item_labels_spec.js33
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js1
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_links_spec.js19
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js19
-rw-r--r--spec/frontend/work_items/components/work_item_milestone_spec.js53
-rw-r--r--spec/frontend/work_items/components/work_item_parent_spec.js84
-rw-r--r--spec/frontend/work_items/components/work_item_relationships/__snapshots__/work_item_relationship_list_spec.js.snap1
-rw-r--r--spec/frontend/work_items/components/work_item_relationships/work_item_add_relationship_form_spec.js3
-rw-r--r--spec/frontend/work_items/components/work_item_relationships/work_item_relationships_spec.js33
-rw-r--r--spec/frontend/work_items/components/work_item_state_toggle_button_spec.js4
-rw-r--r--spec/frontend/work_items/components/work_item_title_spec.js20
-rw-r--r--spec/frontend/work_items/components/work_item_todos_spec.js2
-rw-r--r--spec/frontend/work_items/list/components/work_items_list_app_spec.js4
-rw-r--r--spec/frontend/work_items/mock_data.js121
-rw-r--r--spec/frontend_integration/snippets/snippets_notes_spec.js2
-rw-r--r--spec/frontend_integration/test_helpers/mock_server/routes/emojis.js2
-rw-r--r--spec/frontend_integration/test_helpers/mock_server/routes/graphql.js6
-rw-r--r--spec/graphql/mutations/base_mutation_spec.rb58
-rw-r--r--spec/graphql/mutations/ci/runner/delete_spec.rb2
-rw-r--r--spec/graphql/mutations/ci/runner/update_spec.rb2
-rw-r--r--spec/graphql/mutations/container_repositories/destroy_tags_spec.rb4
-rw-r--r--spec/graphql/mutations/merge_requests/update_spec.rb14
-rw-r--r--spec/graphql/mutations/namespace/package_settings/update_spec.rb12
-rw-r--r--spec/graphql/mutations/saved_replies/create_spec.rb34
-rw-r--r--spec/graphql/mutations/saved_replies/destroy_spec.rb26
-rw-r--r--spec/graphql/mutations/saved_replies/update_spec.rb34
-rw-r--r--spec/graphql/resolvers/ci/catalog/resource_resolver_spec.rb123
-rw-r--r--spec/graphql/resolvers/ci/catalog/resources_resolver_spec.rb71
-rw-r--r--spec/graphql/resolvers/ci/catalog/versions_resolver_spec.rb66
-rw-r--r--spec/graphql/resolvers/ci/runners_resolver_spec.rb28
-rw-r--r--spec/graphql/resolvers/container_repository_tags_resolver_spec.rb136
-rw-r--r--spec/graphql/resolvers/data_transfer/group_data_transfer_resolver_spec.rb20
-rw-r--r--spec/graphql/resolvers/data_transfer/project_data_transfer_resolver_spec.rb22
-rw-r--r--spec/graphql/resolvers/projects_resolver_spec.rb2
-rw-r--r--spec/graphql/resolvers/saved_reply_resolver_spec.rb28
-rw-r--r--spec/graphql/resolvers/users/frecent_groups_resolver_spec.rb7
-rw-r--r--spec/graphql/resolvers/users/frecent_projects_resolver_spec.rb7
-rw-r--r--spec/graphql/types/analytics/cycle_analytics/value_stream_type_spec.rb11
-rw-r--r--spec/graphql/types/base_argument_spec.rb39
-rw-r--r--spec/graphql/types/ci/catalog/resource_sort_enum_spec.rb13
-rw-r--r--spec/graphql/types/ci/catalog/resource_type_spec.rb28
-rw-r--r--spec/graphql/types/container_registry/protection/rule_access_level_enum_spec.rb9
-rw-r--r--spec/graphql/types/container_registry/protection/rule_type_spec.rb35
-rw-r--r--spec/graphql/types/data_transfer/project_data_transfer_type_spec.rb22
-rw-r--r--spec/graphql/types/merge_request_review_state_enum_spec.rb8
-rw-r--r--spec/graphql/types/notes/noteable_interface_spec.rb1
-rw-r--r--spec/graphql/types/organizations/organization_type_spec.rb2
-rw-r--r--spec/graphql/types/organizations/organization_user_badge_type_spec.rb10
-rw-r--r--spec/graphql/types/packages/package_base_type_spec.rb3
-rw-r--r--spec/graphql/types/packages/package_details_type_spec.rb6
-rw-r--r--spec/graphql/types/packages/package_type_spec.rb5
-rw-r--r--spec/graphql/types/packages/protection/rule_type_spec.rb6
-rw-r--r--spec/graphql/types/packages/pypi/metadatum_type_spec.rb9
-rw-r--r--spec/graphql/types/permission_types/abuse_report_spec.rb15
-rw-r--r--spec/graphql/types/permission_types/ci/job_spec.rb2
-rw-r--r--spec/graphql/types/permission_types/ci/pipeline_spec.rb13
-rw-r--r--spec/graphql/types/permission_types/package_spec.rb11
-rw-r--r--spec/graphql/types/project_type_spec.rb91
-rw-r--r--spec/graphql/types/projects/detailed_import_status_type_spec.rb23
-rw-r--r--spec/graphql/types/security/codequality_reports_comparer/report_generation_status_enum_spec.rb11
-rw-r--r--spec/graphql/types/security/codequality_reports_comparer/status_enum_spec.rb4
-rw-r--r--spec/graphql/types/security/codequality_reports_comparer_type_spec.rb2
-rw-r--r--spec/graphql/types/user_type_spec.rb1
-rw-r--r--spec/graphql/types/work_items/linked_item_type_spec.rb6
-rw-r--r--spec/helpers/admin/components_helper_spec.rb1
-rw-r--r--spec/helpers/admin/user_actions_helper_spec.rb59
-rw-r--r--spec/helpers/application_helper_spec.rb23
-rw-r--r--spec/helpers/auth_helper_spec.rb103
-rw-r--r--spec/helpers/blob_helper_spec.rb2
-rw-r--r--spec/helpers/ci/catalog/resources_helper_spec.rb27
-rw-r--r--spec/helpers/ci/jobs_helper_spec.rb1
-rw-r--r--spec/helpers/ci/pipeline_editor_helper_spec.rb2
-rw-r--r--spec/helpers/ci/pipelines_helper_spec.rb71
-rw-r--r--spec/helpers/ci/status_helper_spec.rb107
-rw-r--r--spec/helpers/colors_helper_spec.rb47
-rw-r--r--spec/helpers/environment_helper_spec.rb39
-rw-r--r--spec/helpers/environments_helper_spec.rb17
-rw-r--r--spec/helpers/events_helper_spec.rb112
-rw-r--r--spec/helpers/groups/group_members_helper_spec.rb12
-rw-r--r--spec/helpers/groups_helper_spec.rb4
-rw-r--r--spec/helpers/ide_helper_spec.rb2
-rw-r--r--spec/helpers/issuables_helper_spec.rb20
-rw-r--r--spec/helpers/members_helper_spec.rb15
-rw-r--r--spec/helpers/nav/new_dropdown_helper_spec.rb35
-rw-r--r--spec/helpers/nav_helper_spec.rb32
-rw-r--r--spec/helpers/operations_helper_spec.rb2
-rw-r--r--spec/helpers/organizations/organization_helper_spec.rb36
-rw-r--r--spec/helpers/preferences_helper_spec.rb10
-rw-r--r--spec/helpers/projects/pipeline_helper_spec.rb1
-rw-r--r--spec/helpers/projects_helper_spec.rb18
-rw-r--r--spec/helpers/search_helper_spec.rb24
-rw-r--r--spec/helpers/sidebars_helper_spec.rb13
-rw-r--r--spec/helpers/sorting_helper_spec.rb2
-rw-r--r--spec/helpers/users/callouts_helper_spec.rb22
-rw-r--r--spec/helpers/users_helper_spec.rb2
-rw-r--r--spec/helpers/visibility_level_helper_spec.rb30
-rw-r--r--spec/helpers/vite_helper_spec.rb59
-rw-r--r--spec/helpers/wiki_helper_spec.rb11
-rw-r--r--spec/initializers/action_cable_subscription_adapter_identifier_spec.rb3
-rw-r--r--spec/initializers/active_record_transaction_observer_spec.rb2
-rw-r--r--spec/initializers/diagnostic_reports_spec.rb2
-rw-r--r--spec/initializers/google_cloud_profiler_spec.rb2
-rw-r--r--spec/initializers/memory_watchdog_spec.rb2
-rw-r--r--spec/lib/api/entities/bulk_imports/entity_failure_spec.rb8
-rw-r--r--spec/lib/api/entities/wiki_page_spec.rb6
-rw-r--r--spec/lib/api/github/entities_spec.rb31
-rw-r--r--spec/lib/api/helpers/rate_limiter_spec.rb8
-rw-r--r--spec/lib/api/helpers_spec.rb75
-rw-r--r--spec/lib/atlassian/jira_connect/client_spec.rb87
-rw-r--r--spec/lib/backup/gitaly_backup_spec.rb41
-rw-r--r--spec/lib/banzai/filter/asset_proxy_filter_spec.rb101
-rw-r--r--spec/lib/banzai/filter/math_filter_spec.rb23
-rw-r--r--spec/lib/banzai/filter/references/user_reference_filter_spec.rb5
-rw-r--r--spec/lib/banzai/reference_parser/user_parser_spec.rb10
-rw-r--r--spec/lib/bitbucket/representation/pull_request_comment_spec.rb8
-rw-r--r--spec/lib/bitbucket/representation/repo_spec.rb7
-rw-r--r--spec/lib/bulk_imports/clients/graphql_spec.rb2
-rw-r--r--spec/lib/bulk_imports/common/pipelines/badges_pipeline_spec.rb12
-rw-r--r--spec/lib/bulk_imports/common/pipelines/entity_finisher_spec.rb5
-rw-r--r--spec/lib/bulk_imports/pipeline/runner_spec.rb161
-rw-r--r--spec/lib/bulk_imports/pipeline_schema_info_spec.rb60
-rw-r--r--spec/lib/bulk_imports/projects/pipelines/container_expiration_policy_pipeline_spec.rb4
-rw-r--r--spec/lib/bulk_imports/projects/pipelines/external_pull_requests_pipeline_spec.rb4
-rw-r--r--spec/lib/bulk_imports/projects/pipelines/merge_requests_pipeline_spec.rb11
-rw-r--r--spec/lib/bulk_imports/projects/pipelines/releases_pipeline_spec.rb2
-rw-r--r--spec/lib/bulk_imports/source_url_builder_spec.rb78
-rw-r--r--spec/lib/click_house/models/audit_event_spec.rb132
-rw-r--r--spec/lib/click_house/models/base_model_spec.rb117
-rw-r--r--spec/lib/container_registry/client_spec.rb14
-rw-r--r--spec/lib/container_registry/gitlab_api_client_spec.rb2
-rw-r--r--spec/lib/container_registry/tag_spec.rb83
-rw-r--r--spec/lib/generators/batched_background_migration/batched_background_migration_generator_spec.rb1
-rw-r--r--spec/lib/generators/batched_background_migration/expected_files/queue_my_batched_migration.txt4
-rw-r--r--spec/lib/generators/gitlab/snowplow_event_definition_generator_spec.rb95
-rw-r--r--spec/lib/generators/gitlab/usage_metric_definition/redis_hll_generator_spec.rb6
-rw-r--r--spec/lib/generators/gitlab/usage_metric_definition_generator_spec.rb21
-rw-r--r--spec/lib/generators/model/mocks/migration_file.txt3
-rw-r--r--spec/lib/generators/model/model_generator_spec.rb4
-rw-r--r--spec/lib/gitlab/alert_management/payload/base_spec.rb12
-rw-r--r--spec/lib/gitlab/analytics/cycle_analytics/average_spec.rb12
-rw-r--r--spec/lib/gitlab/asset_proxy_spec.rb2
-rw-r--r--spec/lib/gitlab/auth/ldap/auth_hash_spec.rb2
-rw-r--r--spec/lib/gitlab/auth/ldap/config_spec.rb10
-rw-r--r--spec/lib/gitlab/auth/ldap/person_spec.rb6
-rw-r--r--spec/lib/gitlab/auth/o_auth/user_spec.rb8
-rw-r--r--spec/lib/gitlab/auth/saml/auth_hash_spec.rb6
-rw-r--r--spec/lib/gitlab/auth/saml/config_spec.rb39
-rw-r--r--spec/lib/gitlab/auth/saml/user_spec.rb20
-rw-r--r--spec/lib/gitlab/auth_spec.rb145
-rw-r--r--spec/lib/gitlab/background_migration/backfill_packages_tags_project_id_spec.rb42
-rw-r--r--spec/lib/gitlab/background_migration/batched_migration_job_spec.rb103
-rw-r--r--spec/lib/gitlab/background_migration/copy_column_using_background_migration_job_spec.rb22
-rw-r--r--spec/lib/gitlab/background_migration/delete_invalid_protected_branch_merge_access_levels_spec.rb76
-rw-r--r--spec/lib/gitlab/background_migration/delete_invalid_protected_branch_push_access_levels_spec.rb76
-rw-r--r--spec/lib/gitlab/background_migration/delete_invalid_protected_tag_create_access_levels_spec.rb76
-rw-r--r--spec/lib/gitlab/background_migration/delete_orphaned_operational_vulnerabilities_spec.rb16
-rw-r--r--spec/lib/gitlab/background_migration/delete_orphans_approval_merge_request_rules_spec.rb9
-rw-r--r--spec/lib/gitlab/background_migration/delete_orphans_approval_project_rules_spec.rb9
-rw-r--r--spec/lib/gitlab/background_migration/destroy_invalid_group_members_spec.rb24
-rw-r--r--spec/lib/gitlab/background_migration/destroy_invalid_members_spec.rb84
-rw-r--r--spec/lib/gitlab/background_migration/destroy_invalid_project_members_spec.rb56
-rw-r--r--spec/lib/gitlab/background_migration/disable_legacy_open_source_licence_for_recent_public_projects_spec.rb19
-rw-r--r--spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_inactive_public_projects_spec.rb31
-rw-r--r--spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_no_issues_no_repo_projects_spec.rb37
-rw-r--r--spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_one_member_no_repo_projects_spec.rb37
-rw-r--r--spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_projects_less_than_five_mb_spec.rb33
-rw-r--r--spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_projects_less_than_one_mb_spec.rb33
-rw-r--r--spec/lib/gitlab/background_migration/expire_o_auth_tokens_spec.rb17
-rw-r--r--spec/lib/gitlab/background_migration/fix_incoherent_packages_size_on_project_statistics_spec.rb10
-rw-r--r--spec/lib/gitlab/background_migration/legacy_upload_mover_spec.rb56
-rw-r--r--spec/lib/gitlab/background_migration/remove_backfilled_job_artifacts_expire_at_spec.rb2
-rw-r--r--spec/lib/gitlab/batch_worker_context_spec.rb6
-rw-r--r--spec/lib/gitlab/bitbucket_import/importer_spec.rb2
-rw-r--r--spec/lib/gitlab/bitbucket_import/importers/issue_importer_spec.rb8
-rw-r--r--spec/lib/gitlab/bitbucket_import/importers/issues_importer_spec.rb47
-rw-r--r--spec/lib/gitlab/bitbucket_import/importers/issues_notes_importer_spec.rb6
-rw-r--r--spec/lib/gitlab/bitbucket_import/importers/pull_request_importer_spec.rb8
-rw-r--r--spec/lib/gitlab/bitbucket_import/importers/pull_request_notes_importer_spec.rb214
-rw-r--r--spec/lib/gitlab/bitbucket_import/importers/pull_requests_importer_spec.rb4
-rw-r--r--spec/lib/gitlab/bitbucket_import/importers/pull_requests_notes_importer_spec.rb4
-rw-r--r--spec/lib/gitlab/bitbucket_import/importers/repository_importer_spec.rb17
-rw-r--r--spec/lib/gitlab/bullet/exclusions_spec.rb2
-rw-r--r--spec/lib/gitlab/cache_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/ansi2html_spec.rb8
-rw-r--r--spec/lib/gitlab/ci/ansi2json/line_spec.rb27
-rw-r--r--spec/lib/gitlab/ci/ansi2json/state_spec.rb21
-rw-r--r--spec/lib/gitlab/ci/ansi2json_spec.rb28
-rw-r--r--spec/lib/gitlab/ci/badge/pipeline/status_spec.rb5
-rw-r--r--spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb20
-rw-r--r--spec/lib/gitlab/ci/build/context/build_spec.rb20
-rw-r--r--spec/lib/gitlab/ci/build/hook_spec.rb6
-rw-r--r--spec/lib/gitlab/ci/build/policy/changes_spec.rb53
-rw-r--r--spec/lib/gitlab/ci/build/policy/variables_spec.rb37
-rw-r--r--spec/lib/gitlab/ci/build/rules/rule/clause/changes_spec.rb7
-rw-r--r--spec/lib/gitlab/ci/config/entry/bridge_spec.rb60
-rw-r--r--spec/lib/gitlab/ci/config/entry/commands_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/entry/environment_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/entry/image_spec.rb6
-rw-r--r--spec/lib/gitlab/ci/config/entry/job_spec.rb76
-rw-r--r--spec/lib/gitlab/ci/config/entry/pages_spec.rb49
-rw-r--r--spec/lib/gitlab/ci/config/entry/policy_spec.rb3
-rw-r--r--spec/lib/gitlab/ci/config/entry/processable_spec.rb14
-rw-r--r--spec/lib/gitlab/ci/config/entry/root_spec.rb14
-rw-r--r--spec/lib/gitlab/ci/config/entry/service_spec.rb12
-rw-r--r--spec/lib/gitlab/ci/config/extendable/entry_spec.rb13
-rw-r--r--spec/lib/gitlab/ci/config/external/file/base_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/external/mapper_spec.rb12
-rw-r--r--spec/lib/gitlab/ci/config/header/input_spec.rb31
-rw-r--r--spec/lib/gitlab/ci/config/interpolation/inputs_spec.rb86
-rw-r--r--spec/lib/gitlab/ci/jwt_v2_spec.rb36
-rw-r--r--spec/lib/gitlab/ci/parsers/sbom/cyclonedx_properties_spec.rb36
-rw-r--r--spec/lib/gitlab/ci/parsers/sbom/source/container_scanning_spec.rb58
-rw-r--r--spec/lib/gitlab/ci/parsers/security/common_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/parsers/security/secret_detection_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/template_usage_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/lexeme/equals_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/pipeline/seed/build_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/reports/accessibility_reports_comparer_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/reports/accessibility_reports_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/reports/test_suite_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/composite_spec.rb59
-rw-r--r--spec/lib/gitlab/ci/status/stage/factory_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/waiting_for_callback_spec.rb37
-rw-r--r--spec/lib/gitlab/ci/tags/bulk_insert_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb6
-rw-r--r--spec/lib/gitlab/ci/templates/Jobs/sast_iac_latest_gitlab_ci_yaml_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb42
-rw-r--r--spec/lib/gitlab/ci/variables/collection/item_spec.rb8
-rw-r--r--spec/lib/gitlab/ci/yaml_processor/dag_spec.rb24
-rw-r--r--spec/lib/gitlab/ci/yaml_processor_spec.rb137
-rw-r--r--spec/lib/gitlab/composer/version_index_spec.rb26
-rw-r--r--spec/lib/gitlab/config/entry/factory_spec.rb12
-rw-r--r--spec/lib/gitlab/config/entry/validators_spec.rb2
-rw-r--r--spec/lib/gitlab/config_checker/puma_rugged_checker_spec.rb65
-rw-r--r--spec/lib/gitlab/conflict/file_spec.rb4
-rw-r--r--spec/lib/gitlab/data_builder/build_spec.rb2
-rw-r--r--spec/lib/gitlab/data_builder/pipeline_spec.rb2
-rw-r--r--spec/lib/gitlab/data_builder/push_spec.rb4
-rw-r--r--spec/lib/gitlab/database/background_migration/batched_job_spec.rb2
-rw-r--r--spec/lib/gitlab/database/background_migration/batched_job_transition_log_spec.rb4
-rw-r--r--spec/lib/gitlab/database/background_migration/batched_migration_spec.rb11
-rw-r--r--spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb22
-rw-r--r--spec/lib/gitlab/database/background_migration/prometheus_metrics_spec.rb13
-rw-r--r--spec/lib/gitlab/database/bulk_update_spec.rb2
-rw-r--r--spec/lib/gitlab/database/dictionary_spec.rb84
-rw-r--r--spec/lib/gitlab/database/dynamic_model_helpers_spec.rb67
-rw-r--r--spec/lib/gitlab/database/gitlab_schema_spec.rb4
-rw-r--r--spec/lib/gitlab/database/loose_foreign_keys_spec.rb16
-rw-r--r--spec/lib/gitlab/database/migration_helpers/cascading_namespace_settings_spec.rb2
-rw-r--r--spec/lib/gitlab/database/migration_helpers/convert_to_bigint_spec.rb133
-rw-r--r--spec/lib/gitlab/database/migration_helpers/wraparound_autovacuum_spec.rb30
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb40
-rw-r--r--spec/lib/gitlab/database/migrations/batched_background_migration_helpers_spec.rb6
-rw-r--r--spec/lib/gitlab/database/migrations/constraints_helpers_spec.rb40
-rw-r--r--spec/lib/gitlab/database/migrations/instrumentation_spec.rb16
-rw-r--r--spec/lib/gitlab/database/migrations/milestone_mixin_spec.rb13
-rw-r--r--spec/lib/gitlab/database/migrations/reestablished_connection_stack_spec.rb2
-rw-r--r--spec/lib/gitlab/database/migrations/test_batched_background_runner_spec.rb104
-rw-r--r--spec/lib/gitlab/database/no_cross_db_foreign_keys_spec.rb8
-rw-r--r--spec/lib/gitlab/database/no_new_tables_with_gitlab_main_schema_spec.rb63
-rw-r--r--spec/lib/gitlab/database/partitioning/detached_partition_dropper_spec.rb64
-rw-r--r--spec/lib/gitlab/database/partitioning/monthly_strategy_spec.rb36
-rw-r--r--spec/lib/gitlab/database/partitioning/partition_manager_spec.rb85
-rw-r--r--spec/lib/gitlab/database/partitioning/sliding_list_strategy_spec.rb27
-rw-r--r--spec/lib/gitlab/database/partitioning_migration_helpers/index_helpers_spec.rb7
-rw-r--r--spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb541
-rw-r--r--spec/lib/gitlab/database/postgres_constraint_spec.rb16
-rw-r--r--spec/lib/gitlab/database/postgres_index_spec.rb4
-rw-r--r--spec/lib/gitlab/database/query_analyzers/gitlab_schemas_metrics_spec.rb6
-rw-r--r--spec/lib/gitlab/database/query_analyzers/gitlab_schemas_validate_connection_spec.rb16
-rw-r--r--spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/columns_spec.rb88
-rw-r--r--spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/common_table_expressions_spec.rb97
-rw-r--r--spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/froms_spec.rb76
-rw-r--r--spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/node_spec.rb68
-rw-r--r--spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/references_spec.rb41
-rw-r--r--spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/select_stmt_spec.rb361
-rw-r--r--spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/targets_spec.rb94
-rw-r--r--spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch_spec.rb84
-rw-r--r--spec/lib/gitlab/database/reindexing_spec.rb2
-rw-r--r--spec/lib/gitlab/database/tables_locker_spec.rb44
-rw-r--r--spec/lib/gitlab/database/tables_truncate_spec.rb6
-rw-r--r--spec/lib/gitlab/database/transaction/observer_spec.rb2
-rw-r--r--spec/lib/gitlab/dependency_linker/base_linker_spec.rb4
-rw-r--r--spec/lib/gitlab/dependency_linker/cargo_toml_linker_spec.rb2
-rw-r--r--spec/lib/gitlab/dependency_linker/cartfile_linker_spec.rb2
-rw-r--r--spec/lib/gitlab/dependency_linker/composer_json_linker_spec.rb2
-rw-r--r--spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb2
-rw-r--r--spec/lib/gitlab/dependency_linker/gemspec_linker_spec.rb2
-rw-r--r--spec/lib/gitlab/dependency_linker/go_mod_linker_spec.rb2
-rw-r--r--spec/lib/gitlab/dependency_linker/go_sum_linker_spec.rb2
-rw-r--r--spec/lib/gitlab/dependency_linker/godeps_json_linker_spec.rb2
-rw-r--r--spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb2
-rw-r--r--spec/lib/gitlab/dependency_linker/podfile_linker_spec.rb2
-rw-r--r--spec/lib/gitlab/dependency_linker/podspec_json_linker_spec.rb2
-rw-r--r--spec/lib/gitlab/dependency_linker/podspec_linker_spec.rb2
-rw-r--r--spec/lib/gitlab/dependency_linker/requirements_txt_linker_spec.rb2
-rw-r--r--spec/lib/gitlab/diff/file_collection/compare_spec.rb8
-rw-r--r--spec/lib/gitlab/diff/file_collection/merge_request_diff_batch_spec.rb25
-rw-r--r--spec/lib/gitlab/diff/file_collection/paginated_merge_request_diff_spec.rb16
-rw-r--r--spec/lib/gitlab/diff/file_spec.rb5
-rw-r--r--spec/lib/gitlab/diff/formatters/file_formatter_spec.rb2
-rw-r--r--spec/lib/gitlab/diff/highlight_cache_spec.rb20
-rw-r--r--spec/lib/gitlab/diff/highlight_spec.rb10
-rw-r--r--spec/lib/gitlab/diff/inline_diff_marker_spec.rb6
-rw-r--r--spec/lib/gitlab/diff/line_spec.rb14
-rw-r--r--spec/lib/gitlab/diff/suggestion_diff_spec.rb13
-rw-r--r--spec/lib/gitlab/diff/suggestion_spec.rb22
-rw-r--r--spec/lib/gitlab/diff/suggestions_parser_spec.rb67
-rw-r--r--spec/lib/gitlab/discussions_diff/file_collection_spec.rb15
-rw-r--r--spec/lib/gitlab/email/handler/service_desk_handler_spec.rb92
-rw-r--r--spec/lib/gitlab/email/handler_spec.rb8
-rw-r--r--spec/lib/gitlab/email/receiver_spec.rb2
-rw-r--r--spec/lib/gitlab/encoding_helper_spec.rb2
-rw-r--r--spec/lib/gitlab/endpoint_attributes_spec.rb14
-rw-r--r--spec/lib/gitlab/error_tracking/stack_trace_highlight_decorator_spec.rb2
-rw-r--r--spec/lib/gitlab/external_authorization/client_spec.rb2
-rw-r--r--spec/lib/gitlab/favicon_spec.rb4
-rw-r--r--spec/lib/gitlab/feature_categories_spec.rb2
-rw-r--r--spec/lib/gitlab/file_detector_spec.rb18
-rw-r--r--spec/lib/gitlab/gfm/reference_rewriter_spec.rb10
-rw-r--r--spec/lib/gitlab/git/blame_spec.rb8
-rw-r--r--spec/lib/gitlab/git/blob_spec.rb12
-rw-r--r--spec/lib/gitlab/git/commit_spec.rb24
-rw-r--r--spec/lib/gitlab/git/merge_base_spec.rb8
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb10
-rw-r--r--spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb116
-rw-r--r--spec/lib/gitlab/git/tree_spec.rb118
-rw-r--r--spec/lib/gitlab/git_access_spec.rb2
-rw-r--r--spec/lib/gitlab/git_audit_event_spec.rb79
-rw-r--r--spec/lib/gitlab/gitaly_client/commit_service_spec.rb29
-rw-r--r--spec/lib/gitlab/gitaly_client/conflict_files_stitcher_spec.rb52
-rw-r--r--spec/lib/gitlab/gitaly_client/operation_service_spec.rb4
-rw-r--r--spec/lib/gitlab/gitaly_client/ref_service_spec.rb4
-rw-r--r--spec/lib/gitlab/gitaly_client/repository_service_spec.rb38
-rw-r--r--spec/lib/gitlab/gitaly_client/storage_settings_spec.rb18
-rw-r--r--spec/lib/gitlab/gitaly_client_spec.rb38
-rw-r--r--spec/lib/gitlab/github_import/attachments_downloader_spec.rb17
-rw-r--r--spec/lib/gitlab/github_import/client_spec.rb39
-rw-r--r--spec/lib/gitlab/github_import/importer/collaborators_importer_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/importer/diff_notes_importer_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/importer/issue_events_importer_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/importer/issues_importer_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/importer/lfs_objects_importer_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/importer/note_importer_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/importer/notes_importer_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/importer/protected_branches_importer_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/importer/pull_requests/review_requests_importer_spec.rb10
-rw-r--r--spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/issuable_finder_spec.rb64
-rw-r--r--spec/lib/gitlab/github_import/label_finder_spec.rb49
-rw-r--r--spec/lib/gitlab/github_import/milestone_finder_spec.rb57
-rw-r--r--spec/lib/gitlab/github_import/object_counter_spec.rb10
-rw-r--r--spec/lib/gitlab/github_import/parallel_scheduling_spec.rb12
-rw-r--r--spec/lib/gitlab/github_import/representation/to_hash_spec.rb12
-rw-r--r--spec/lib/gitlab/graphql/known_operations_spec.rb4
-rw-r--r--spec/lib/gitlab/graphql/tracers/metrics_tracer_spec.rb2
-rw-r--r--spec/lib/gitlab/group_search_results_spec.rb2
-rw-r--r--spec/lib/gitlab/hashed_path_spec.rb2
-rw-r--r--spec/lib/gitlab/health_checks/gitaly_check_spec.rb5
-rw-r--r--spec/lib/gitlab/highlight_spec.rb2
-rw-r--r--spec/lib/gitlab/i18n/translation_entry_spec.rb6
-rw-r--r--spec/lib/gitlab/import/import_failure_service_spec.rb38
-rw-r--r--spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml2
-rw-r--r--spec/lib/gitlab/import_export/attribute_cleaner_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/attributes_permitter_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/command_line_util_spec.rb18
-rw-r--r--spec/lib/gitlab/import_export/error_spec.rb10
-rw-r--r--spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/json/ndjson_writer_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/lfs_restorer_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/lfs_saver_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/project/tree_saver_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/saver_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/snippet_repo_restorer_spec.rb2
-rw-r--r--spec/lib/gitlab/import_sources_spec.rb8
-rw-r--r--spec/lib/gitlab/instrumentation/redis_cluster_validator_spec.rb38
-rw-r--r--spec/lib/gitlab/instrumentation_helper_spec.rb61
-rw-r--r--spec/lib/gitlab/issues/rebalancing/state_spec.rb18
-rw-r--r--spec/lib/gitlab/jira/middleware_spec.rb40
-rw-r--r--spec/lib/gitlab/jira_import/handle_labels_service_spec.rb6
-rw-r--r--spec/lib/gitlab/jira_import/issue_serializer_spec.rb4
-rw-r--r--spec/lib/gitlab/jira_import/labels_importer_spec.rb10
-rw-r--r--spec/lib/gitlab/job_waiter_spec.rb66
-rw-r--r--spec/lib/gitlab/kubernetes/kubectl_cmd_spec.rb4
-rw-r--r--spec/lib/gitlab/kubernetes/role_spec.rb6
-rw-r--r--spec/lib/gitlab/language_data_spec.rb2
-rw-r--r--spec/lib/gitlab/mail_room/mail_room_spec.rb2
-rw-r--r--spec/lib/gitlab/markup_helper_spec.rb8
-rw-r--r--spec/lib/gitlab/memory/instrumentation_spec.rb2
-rw-r--r--spec/lib/gitlab/memory/reporter_spec.rb2
-rw-r--r--spec/lib/gitlab/memory/reports/heap_dump_spec.rb2
-rw-r--r--spec/lib/gitlab/memory/watchdog/configurator_spec.rb2
-rw-r--r--spec/lib/gitlab/memory/watchdog/event_reporter_spec.rb2
-rw-r--r--spec/lib/gitlab/memory/watchdog/handlers/null_handler_spec.rb2
-rw-r--r--spec/lib/gitlab/memory/watchdog/handlers/puma_handler_spec.rb2
-rw-r--r--spec/lib/gitlab/memory/watchdog/handlers/sidekiq_handler_spec.rb2
-rw-r--r--spec/lib/gitlab/memory/watchdog/monitor/rss_memory_limit_spec.rb2
-rw-r--r--spec/lib/gitlab/memory/watchdog/sidekiq_event_reporter_spec.rb2
-rw-r--r--spec/lib/gitlab/memory/watchdog_spec.rb2
-rw-r--r--spec/lib/gitlab/merge_requests/mergeability/check_result_spec.rb24
-rw-r--r--spec/lib/gitlab/metrics/exporter/base_exporter_spec.rb2
-rw-r--r--spec/lib/gitlab/metrics/rails_slis_spec.rb2
-rw-r--r--spec/lib/gitlab/metrics/samplers/threads_sampler_spec.rb8
-rw-r--r--spec/lib/gitlab/metrics/subscribers/action_cable_spec.rb2
-rw-r--r--spec/lib/gitlab/middleware/go_spec.rb4
-rw-r--r--spec/lib/gitlab/middleware/multipart_spec.rb4
-rw-r--r--spec/lib/gitlab/middleware/path_traversal_check_spec.rb183
-rw-r--r--spec/lib/gitlab/omniauth_initializer_spec.rb119
-rw-r--r--spec/lib/gitlab/other_markup_spec.rb43
-rw-r--r--spec/lib/gitlab/pages/deployment_update_spec.rb2
-rw-r--r--spec/lib/gitlab/pages/virtual_host_finder_spec.rb35
-rw-r--r--spec/lib/gitlab/pagination/cursor_based_keyset_spec.rb20
-rw-r--r--spec/lib/gitlab/pagination/keyset/order_spec.rb2
-rw-r--r--spec/lib/gitlab/pagination/offset_header_builder_spec.rb8
-rw-r--r--spec/lib/gitlab/patch/sidekiq_scheduled_enq_spec.rb26
-rw-r--r--spec/lib/gitlab/path_regex_spec.rb14
-rw-r--r--spec/lib/gitlab/popen_spec.rb8
-rw-r--r--spec/lib/gitlab/process_management_spec.rb4
-rw-r--r--spec/lib/gitlab/process_supervisor_spec.rb12
-rw-r--r--spec/lib/gitlab/project_template_spec.rb2
-rw-r--r--spec/lib/gitlab/quick_actions/extractor_spec.rb2
-rw-r--r--spec/lib/gitlab/redis/multi_store_spec.rb421
-rw-r--r--spec/lib/gitlab/redis/pubsub_spec.rb8
-rw-r--r--spec/lib/gitlab/reference_extractor_spec.rb4
-rw-r--r--spec/lib/gitlab/regex_requires_app_spec.rb40
-rw-r--r--spec/lib/gitlab/repository_cache_adapter_spec.rb12
-rw-r--r--spec/lib/gitlab/repository_hash_cache_spec.rb10
-rw-r--r--spec/lib/gitlab/repository_set_cache_spec.rb4
-rw-r--r--spec/lib/gitlab/rugged_instrumentation_spec.rb27
-rw-r--r--spec/lib/gitlab/runtime_spec.rb2
-rw-r--r--spec/lib/gitlab/search/abuse_detection_spec.rb4
-rw-r--r--spec/lib/gitlab/search_results_spec.rb6
-rw-r--r--spec/lib/gitlab/security/scan_configuration_spec.rb4
-rw-r--r--spec/lib/gitlab/seeders/ci/catalog/resource_seeder_spec.rb101
-rw-r--r--spec/lib/gitlab/shard_health_cache_spec.rb4
-rw-r--r--spec/lib/gitlab/sidekiq_config/cli_methods_spec.rb2
-rw-r--r--spec/lib/gitlab/sidekiq_config/worker_matcher_spec.rb76
-rw-r--r--spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb8
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb16
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb31
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/skip_jobs_spec.rb15
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/worker_context/client_spec.rb10
-rw-r--r--spec/lib/gitlab/sidekiq_status_spec.rb22
-rw-r--r--spec/lib/gitlab/ssh_public_key_spec.rb36
-rw-r--r--spec/lib/gitlab/string_range_marker_spec.rb8
-rw-r--r--spec/lib/gitlab/string_regex_marker_spec.rb18
-rw-r--r--spec/lib/gitlab/suggestions/suggestion_set_spec.rb2
-rw-r--r--spec/lib/gitlab/task_helpers_spec.rb6
-rw-r--r--spec/lib/gitlab/tracking/event_definition_spec.rb8
-rw-r--r--spec/lib/gitlab/url_builder_spec.rb1
-rw-r--r--spec/lib/gitlab/url_sanitizer_spec.rb8
-rw-r--r--spec/lib/gitlab/usage/metric_definition_spec.rb166
-rw-r--r--spec/lib/gitlab/usage/metric_spec.rb4
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/aggregated_metric_spec.rb3
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/count_projects_with_jira_dvcs_integration_metric_spec.rb50
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/database_metric_spec.rb2
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/generic_metric_spec.rb2
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/numbers_metric_spec.rb2
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/redis_hll_metric_spec.rb3
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/redis_metric_spec.rb3
-rw-r--r--spec/lib/gitlab/usage/service_ping/instrumented_payload_spec.rb4
-rw-r--r--spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb88
-rw-r--r--spec/lib/gitlab/usage_data_counters/redis_counter_spec.rb48
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb8
-rw-r--r--spec/lib/gitlab/utils/log_limited_array_spec.rb2
-rw-r--r--spec/lib/gitlab/webpack/graphql_known_operations_spec.rb2
-rw-r--r--spec/lib/gitlab/wiki_pages/front_matter_parser_spec.rb2
-rw-r--r--spec/lib/gitlab/workhorse_spec.rb9
-rw-r--r--spec/lib/object_storage/config_spec.rb2
-rw-r--r--spec/lib/object_storage/direct_upload_spec.rb2
-rw-r--r--spec/lib/peek/views/rugged_spec.rb42
-rw-r--r--spec/lib/result_spec.rb2
-rw-r--r--spec/lib/rouge/formatters/html_gitlab_spec.rb10
-rw-r--r--spec/lib/safe_zip/entry_spec.rb8
-rw-r--r--spec/lib/safe_zip/extract_params_spec.rb4
-rw-r--r--spec/lib/safe_zip/extract_spec.rb10
-rw-r--r--spec/lib/sbom/purl_type/converter_spec.rb1
-rw-r--r--spec/lib/security/ci_configuration/container_scanning_build_action_spec.rb8
-rw-r--r--spec/lib/security/ci_configuration/sast_build_action_spec.rb14
-rw-r--r--spec/lib/security/ci_configuration/sast_iac_build_action_spec.rb8
-rw-r--r--spec/lib/security/ci_configuration/secret_detection_build_action_spec.rb8
-rw-r--r--spec/lib/sidebars/explore/menus/catalog_menu_spec.rb40
-rw-r--r--spec/lib/sidebars/menu_spec.rb6
-rw-r--r--spec/lib/sidebars/organizations/menus/manage_menu_spec.rb6
-rw-r--r--spec/lib/sidebars/projects/menus/scope_menu_spec.rb2
-rw-r--r--spec/lib/sidebars/projects/menus/security_compliance_menu_spec.rb3
-rw-r--r--spec/lib/sidebars/projects/menus/settings_menu_spec.rb12
-rw-r--r--spec/lib/sidebars/projects/super_sidebar_menus/monitor_menu_spec.rb1
-rw-r--r--spec/lib/sidebars/user_settings/menus/comment_templates_menu_spec.rb28
-rw-r--r--spec/lib/system_check/orphans/namespace_check_spec.rb14
-rw-r--r--spec/lib/system_check/orphans/repository_check_spec.rb22
-rw-r--r--spec/lib/system_check/sidekiq_check_spec.rb2
-rw-r--r--spec/lib/unnested_in_filters/dsl_spec.rb2
-rw-r--r--spec/lib/unnested_in_filters/rewriter_spec.rb12
-rw-r--r--spec/mailers/emails/pages_domains_spec.rb4
-rw-r--r--spec/metrics_server/metrics_server_spec.rb6
-rw-r--r--spec/migrations/20230929155123_migrate_disable_merge_trains_value_spec.rb83
-rw-r--r--spec/migrations/20231003045342_migrate_sidekiq_namespaced_jobs_spec.rb67
-rw-r--r--spec/migrations/20231016001000_fix_design_user_mentions_design_id_note_id_index_for_self_managed_spec.rb114
-rw-r--r--spec/migrations/20231016173129_queue_delete_invalid_protected_branch_merge_access_levels_spec.rb26
-rw-r--r--spec/migrations/20231016194927_queue_delete_invalid_protected_branch_push_access_levels_spec.rb26
-rw-r--r--spec/migrations/20231016194943_queue_delete_invalid_protected_tag_create_access_levels_spec.rb26
-rw-r--r--spec/migrations/20231019003052_swap_columns_for_ci_pipelines_pipeline_id_bigint_v2_spec.rb37
-rw-r--r--spec/migrations/20231019084731_swap_columns_for_ci_stages_pipeline_id_bigint_v2_spec.rb37
-rw-r--r--spec/migrations/20231019145202_add_status_to_packages_npm_metadata_caches_spec.rb22
-rw-r--r--spec/migrations/20231019223224_backfill_catalog_resources_name_and_description_spec.rb29
-rw-r--r--spec/migrations/20231020181652_add_index_packages_npm_metadata_caches_on_id_and_project_id_and_status_spec.rb24
-rw-r--r--spec/migrations/20231030071209_queue_backfill_packages_tags_project_id_spec.rb26
-rw-r--r--spec/migrations/20231102142554_migrate_zoekt_shards_to_zoekt_nodes_spec.rb44
-rw-r--r--spec/migrations/20231103223224_backfill_zoekt_node_id_on_indexed_namespaces_spec.rb79
-rw-r--r--spec/migrations/db/migrate/20231103162825_add_wolfi_purl_type_to_package_metadata_purl_types_spec.rb35
-rw-r--r--spec/migrations/schedule_fixing_security_scan_statuses_spec.rb11
-rw-r--r--spec/models/activity_pub/releases_subscription_spec.rb79
-rw-r--r--spec/models/ai/service_access_token_spec.rb21
-rw-r--r--spec/models/alert_management/http_integration_spec.rb6
-rw-r--r--spec/models/analytics/cycle_analytics/value_stream_spec.rb19
-rw-r--r--spec/models/appearance_spec.rb2
-rw-r--r--spec/models/application_setting_spec.rb40
-rw-r--r--spec/models/authentication_event_spec.rb4
-rw-r--r--spec/models/blob_viewer/base_spec.rb4
-rw-r--r--spec/models/bulk_imports/entity_spec.rb18
-rw-r--r--spec/models/bulk_imports/failure_spec.rb16
-rw-r--r--spec/models/ci/build_dependencies_spec.rb6
-rw-r--r--spec/models/ci/build_spec.rb76
-rw-r--r--spec/models/ci/build_trace_chunks/redis_spec.rb221
-rw-r--r--spec/models/ci/build_trace_chunks/redis_trace_chunks_spec.rb12
-rw-r--r--spec/models/ci/catalog/components_project_spec.rb49
-rw-r--r--spec/models/ci/catalog/listing_spec.rb196
-rw-r--r--spec/models/ci/catalog/resource_spec.rb141
-rw-r--r--spec/models/ci/catalog/resources/component_spec.rb17
-rw-r--r--spec/models/ci/catalog/resources/version_spec.rb91
-rw-r--r--spec/models/ci/job_artifact_spec.rb2
-rw-r--r--spec/models/ci/job_token/scope_spec.rb49
-rw-r--r--spec/models/ci/pipeline_spec.rb172
-rw-r--r--spec/models/ci/ref_spec.rb168
-rw-r--r--spec/models/ci/resource_group_spec.rb2
-rw-r--r--spec/models/ci/runner_manager_spec.rb64
-rw-r--r--spec/models/ci/runner_spec.rb37
-rw-r--r--spec/models/ci/stage_spec.rb12
-rw-r--r--spec/models/clusters/agent_spec.rb2
-rw-r--r--spec/models/clusters/platforms/kubernetes_spec.rb2
-rw-r--r--spec/models/commit_range_spec.rb2
-rw-r--r--spec/models/commit_spec.rb14
-rw-r--r--spec/models/commit_status_spec.rb22
-rw-r--r--spec/models/compare_spec.rb4
-rw-r--r--spec/models/concerns/awardable_spec.rb8
-rw-r--r--spec/models/concerns/case_sensitivity_spec.rb6
-rw-r--r--spec/models/concerns/ci/has_status_spec.rb24
-rw-r--r--spec/models/concerns/ci/partitionable/switch_spec.rb11
-rw-r--r--spec/models/concerns/enums/sbom_spec.rb3
-rw-r--r--spec/models/concerns/featurable_spec.rb2
-rw-r--r--spec/models/concerns/has_user_type_spec.rb2
-rw-r--r--spec/models/concerns/ignorable_columns_spec.rb4
-rw-r--r--spec/models/concerns/issuable_spec.rb4
-rw-r--r--spec/models/concerns/pg_full_text_searchable_spec.rb4
-rw-r--r--spec/models/concerns/project_features_compatibility_spec.rb6
-rw-r--r--spec/models/concerns/reactive_caching_spec.rb4
-rw-r--r--spec/models/concerns/reset_on_column_errors_spec.rb2
-rw-r--r--spec/models/concerns/sortable_spec.rb22
-rw-r--r--spec/models/concerns/token_authenticatable_spec.rb2
-rw-r--r--spec/models/concerns/use_sql_function_for_primary_key_lookups_spec.rb181
-rw-r--r--spec/models/container_repository_spec.rb105
-rw-r--r--spec/models/deployment_spec.rb10
-rw-r--r--spec/models/diff_viewer/base_spec.rb4
-rw-r--r--spec/models/email_spec.rb6
-rw-r--r--spec/models/environment_spec.rb82
-rw-r--r--spec/models/group_label_spec.rb2
-rw-r--r--spec/models/group_spec.rb40
-rw-r--r--spec/models/guest_spec.rb47
-rw-r--r--spec/models/instance_configuration_spec.rb4
-rw-r--r--spec/models/integration_spec.rb17
-rw-r--r--spec/models/integrations/base_chat_notification_spec.rb6
-rw-r--r--spec/models/integrations/buildkite_spec.rb2
-rw-r--r--spec/models/integrations/campfire_spec.rb2
-rw-r--r--spec/models/integrations/integration_list_spec.rb4
-rw-r--r--spec/models/integrations/jira_spec.rb23
-rw-r--r--spec/models/integrations/teamcity_spec.rb4
-rw-r--r--spec/models/issue_spec.rb8
-rw-r--r--spec/models/member_spec.rb16
-rw-r--r--spec/models/members/members/members_with_parents_spec.rb92
-rw-r--r--spec/models/members/project_member_spec.rb44
-rw-r--r--spec/models/merge_request_diff_spec.rb2
-rw-r--r--spec/models/merge_request_spec.rb94
-rw-r--r--spec/models/ml/candidate_spec.rb40
-rw-r--r--spec/models/ml/model_metadata_spec.rb26
-rw-r--r--spec/models/ml/model_spec.rb43
-rw-r--r--spec/models/ml/model_version_spec.rb73
-rw-r--r--spec/models/namespace_setting_spec.rb68
-rw-r--r--spec/models/namespace_spec.rb55
-rw-r--r--spec/models/namespace_statistics_spec.rb2
-rw-r--r--spec/models/network/graph_spec.rb11
-rw-r--r--spec/models/notification_recipient_spec.rb4
-rw-r--r--spec/models/organizations/organization_spec.rb9
-rw-r--r--spec/models/packages/npm/metadata_cache_spec.rb36
-rw-r--r--spec/models/packages/nuget/symbol_spec.rb1
-rw-r--r--spec/models/packages/package_spec.rb2
-rw-r--r--spec/models/packages/protection/rule_spec.rb104
-rw-r--r--spec/models/packages/pypi/metadatum_spec.rb27
-rw-r--r--spec/models/packages/tag_spec.rb19
-rw-r--r--spec/models/pages/lookup_path_spec.rb20
-rw-r--r--spec/models/pages_deployment_spec.rb84
-rw-r--r--spec/models/pages_domain_spec.rb30
-rw-r--r--spec/models/personal_access_token_spec.rb2
-rw-r--r--spec/models/project_feature_spec.rb4
-rw-r--r--spec/models/project_feature_usage_spec.rb173
-rw-r--r--spec/models/project_label_spec.rb2
-rw-r--r--spec/models/project_setting_spec.rb2
-rw-r--r--spec/models/project_snippet_spec.rb15
-rw-r--r--spec/models/project_spec.rb227
-rw-r--r--spec/models/projects/repository_storage_move_spec.rb26
-rw-r--r--spec/models/projects/topic_spec.rb2
-rw-r--r--spec/models/prometheus_metric_spec.rb20
-rw-r--r--spec/models/releases/link_spec.rb2
-rw-r--r--spec/models/repository_spec.rb79
-rw-r--r--spec/models/service_desk/custom_email_credential_spec.rb33
-rw-r--r--spec/models/snippet_repository_spec.rb4
-rw-r--r--spec/models/snippet_spec.rb20
-rw-r--r--spec/models/terraform/state_spec.rb2
-rw-r--r--spec/models/upload_spec.rb4
-rw-r--r--spec/models/user_detail_spec.rb23
-rw-r--r--spec/models/user_spec.rb54
-rw-r--r--spec/models/users/anonymous_spec.rb47
-rw-r--r--spec/models/users/credit_card_validation_spec.rb12
-rw-r--r--spec/models/users/group_visit_spec.rb25
-rw-r--r--spec/models/users/phone_number_validation_spec.rb46
-rw-r--r--spec/models/users/project_visit_spec.rb25
-rw-r--r--spec/models/vs_code/settings/vs_code_setting_spec.rb8
-rw-r--r--spec/models/web_ide_terminal_spec.rb6
-rw-r--r--spec/models/wiki_page_spec.rb25
-rw-r--r--spec/models/work_item_spec.rb6
-rw-r--r--spec/models/zoom_meeting_spec.rb4
-rw-r--r--spec/policies/abuse_report_policy_spec.rb2
-rw-r--r--spec/policies/ci/build_policy_spec.rb8
-rw-r--r--spec/policies/ci/pipeline_policy_spec.rb7
-rw-r--r--spec/policies/concerns/policy_actor_spec.rb6
-rw-r--r--spec/policies/global_policy_spec.rb7
-rw-r--r--spec/policies/group_group_link_policy_spec.rb63
-rw-r--r--spec/policies/issue_policy_spec.rb68
-rw-r--r--spec/policies/project_group_link_policy_spec.rb95
-rw-r--r--spec/policies/project_policy_spec.rb375
-rw-r--r--spec/policies/user_policy_spec.rb26
-rw-r--r--spec/policies/work_item_policy_spec.rb72
-rw-r--r--spec/presenters/ci/pipeline_artifacts/code_coverage_presenter_spec.rb8
-rw-r--r--spec/presenters/ci/pipeline_artifacts/code_quality_mr_diff_presenter_spec.rb8
-rw-r--r--spec/presenters/clusters/cluster_presenter_spec.rb2
-rw-r--r--spec/presenters/ml/model_presenter_spec.rb33
-rw-r--r--spec/presenters/ml/model_version_presenter_spec.rb28
-rw-r--r--spec/presenters/packages/nuget/packages_metadata_presenter_spec.rb2
-rw-r--r--spec/presenters/packages/nuget/search_results_presenter_spec.rb2
-rw-r--r--spec/presenters/project_presenter_spec.rb22
-rw-r--r--spec/presenters/projects/security/configuration_presenter_spec.rb2
-rw-r--r--spec/presenters/user_presenter_spec.rb22
-rw-r--r--spec/requests/acme_challenges_controller_spec.rb9
-rw-r--r--spec/requests/admin/users_controller_spec.rb50
-rw-r--r--spec/requests/api/badges_spec.rb2
-rw-r--r--spec/requests/api/bulk_imports_spec.rb15
-rw-r--r--spec/requests/api/ci/job_artifacts_spec.rb70
-rw-r--r--spec/requests/api/ci/jobs_spec.rb26
-rw-r--r--spec/requests/api/ci/pipelines_spec.rb154
-rw-r--r--spec/requests/api/ci/resource_groups_spec.rb4
-rw-r--r--spec/requests/api/ci/runner/jobs_artifacts_spec.rb11
-rw-r--r--spec/requests/api/ci/runner/jobs_request_post_spec.rb64
-rw-r--r--spec/requests/api/ci/runner/jobs_trace_spec.rb22
-rw-r--r--spec/requests/api/ci/runner/runners_post_spec.rb8
-rw-r--r--spec/requests/api/ci/runners_spec.rb51
-rw-r--r--spec/requests/api/ci/triggers_spec.rb2
-rw-r--r--spec/requests/api/commits_spec.rb10
-rw-r--r--spec/requests/api/container_repositories_spec.rb4
-rw-r--r--spec/requests/api/deployments_spec.rb2
-rw-r--r--spec/requests/api/geo_spec.rb2
-rw-r--r--spec/requests/api/graphql/abuse_report_spec.rb131
-rw-r--r--spec/requests/api/graphql/ci/catalog/resource_spec.rb341
-rw-r--r--spec/requests/api/graphql/ci/catalog/resources_spec.rb359
-rw-r--r--spec/requests/api/graphql/ci/manual_variables_spec.rb2
-rw-r--r--spec/requests/api/graphql/ci/runners_spec.rb87
-rw-r--r--spec/requests/api/graphql/container_repository/container_repository_details_spec.rb120
-rw-r--r--spec/requests/api/graphql/gitlab_schema_spec.rb6
-rw-r--r--spec/requests/api/graphql/group/container_repositories_spec.rb4
-rw-r--r--spec/requests/api/graphql/group/data_transfer_spec.rb42
-rw-r--r--spec/requests/api/graphql/group/milestones_spec.rb8
-rw-r--r--spec/requests/api/graphql/merge_requests/codequality_reports_comparer_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/admin/sidekiq_queues/delete_jobs_spec.rb12
-rw-r--r--spec/requests/api/graphql/mutations/alert_management/alerts/create_alert_issue_spec.rb32
-rw-r--r--spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb37
-rw-r--r--spec/requests/api/graphql/mutations/boards/issues/issue_move_list_spec.rb34
-rw-r--r--spec/requests/api/graphql/mutations/ci/catalog/resources/create_spec.rb40
-rw-r--r--spec/requests/api/graphql/mutations/ci/catalog/unpublish_spec.rb52
-rw-r--r--spec/requests/api/graphql/mutations/ci/pipeline_retry_spec.rb16
-rw-r--r--spec/requests/api/graphql/mutations/clusters/agent_tokens/agent_tokens/create_spec.rb4
-rw-r--r--spec/requests/api/graphql/mutations/clusters/agents/delete_spec.rb4
-rw-r--r--spec/requests/api/graphql/mutations/container_registry/protection/rule/create_spec.rb180
-rw-r--r--spec/requests/api/graphql/mutations/container_repository/destroy_tags_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/design_management/delete_spec.rb10
-rw-r--r--spec/requests/api/graphql/mutations/design_management/upload_spec.rb9
-rw-r--r--spec/requests/api/graphql/mutations/issues/link_alerts_spec.rb28
-rw-r--r--spec/requests/api/graphql/mutations/issues/move_spec.rb18
-rw-r--r--spec/requests/api/graphql/mutations/issues/set_confidential_spec.rb20
-rw-r--r--spec/requests/api/graphql/mutations/issues/set_crm_contacts_spec.rb26
-rw-r--r--spec/requests/api/graphql/mutations/issues/set_due_date_spec.rb22
-rw-r--r--spec/requests/api/graphql/mutations/issues/set_locked_spec.rb20
-rw-r--r--spec/requests/api/graphql/mutations/issues/set_severity_spec.rb20
-rw-r--r--spec/requests/api/graphql/mutations/issues/unlink_alerts_spec.rb28
-rw-r--r--spec/requests/api/graphql/mutations/merge_requests/reviewer_rereview_spec.rb12
-rw-r--r--spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb28
-rw-r--r--spec/requests/api/graphql/mutations/merge_requests/set_draft_spec.rb20
-rw-r--r--spec/requests/api/graphql/mutations/merge_requests/set_labels_spec.rb28
-rw-r--r--spec/requests/api/graphql/mutations/merge_requests/set_locked_spec.rb20
-rw-r--r--spec/requests/api/graphql/mutations/merge_requests/set_milestone_spec.rb24
-rw-r--r--spec/requests/api/graphql/mutations/merge_requests/set_reviewers_spec.rb28
-rw-r--r--spec/requests/api/graphql/mutations/merge_requests/set_time_estimate_spec.rb5
-rw-r--r--spec/requests/api/graphql/mutations/merge_requests/update_spec.rb17
-rw-r--r--spec/requests/api/graphql/mutations/namespace/package_settings/update_spec.rb20
-rw-r--r--spec/requests/api/graphql/mutations/notes/create/note_spec.rb43
-rw-r--r--spec/requests/api/graphql/mutations/notes/update/image_diff_note_spec.rb12
-rw-r--r--spec/requests/api/graphql/mutations/organizations/create_spec.rb64
-rw-r--r--spec/requests/api/graphql/mutations/packages/cleanup/policy/update_spec.rb18
-rw-r--r--spec/requests/api/graphql/mutations/packages/protection/rule/create_spec.rb232
-rw-r--r--spec/requests/api/graphql/mutations/packages/protection/rule/delete_spec.rb88
-rw-r--r--spec/requests/api/graphql/mutations/release_asset_links/update_spec.rb14
-rw-r--r--spec/requests/api/graphql/mutations/snippets/create_spec.rb10
-rw-r--r--spec/requests/api/graphql/mutations/snippets/destroy_spec.rb4
-rw-r--r--spec/requests/api/graphql/mutations/snippets/mark_as_spam_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/snippets/update_spec.rb38
-rw-r--r--spec/requests/api/graphql/mutations/todos/mark_all_done_spec.rb14
-rw-r--r--spec/requests/api/graphql/mutations/todos/mark_done_spec.rb20
-rw-r--r--spec/requests/api/graphql/mutations/todos/restore_many_spec.rb20
-rw-r--r--spec/requests/api/graphql/mutations/todos/restore_spec.rb20
-rw-r--r--spec/requests/api/graphql/namespace/root_storage_statistics_spec.rb9
-rw-r--r--spec/requests/api/graphql/organizations/organization_query_spec.rb7
-rw-r--r--spec/requests/api/graphql/project/base_service_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/container_repositories_spec.rb14
-rw-r--r--spec/requests/api/graphql/project/data_transfer_spec.rb42
-rw-r--r--spec/requests/api/graphql/project/environments_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/error_tracking/sentry_errors_request_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/issue_spec.rb4
-rw-r--r--spec/requests/api/graphql/project/jira_import_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/jira_projects_spec.rb8
-rw-r--r--spec/requests/api/graphql/project/merge_request_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/merge_requests_spec.rb5
-rw-r--r--spec/requests/api/graphql/project/release_spec.rb24
-rw-r--r--spec/requests/api/graphql/project/terraform/state_spec.rb4
-rw-r--r--spec/requests/api/graphql/project/terraform/states_spec.rb4
-rw-r--r--spec/requests/api/graphql/project/work_items_spec.rb17
-rw-r--r--spec/requests/api/graphql/projects/projects_spec.rb77
-rw-r--r--spec/requests/api/graphql/user_spec.rb32
-rw-r--r--spec/requests/api/graphql/work_item_spec.rb75
-rw-r--r--spec/requests/api/group_packages_spec.rb23
-rw-r--r--spec/requests/api/groups_spec.rb38
-rw-r--r--spec/requests/api/helm_packages_spec.rb6
-rw-r--r--spec/requests/api/internal/base_spec.rb50
-rw-r--r--spec/requests/api/internal/pages_spec.rb42
-rw-r--r--spec/requests/api/issues/get_group_issues_spec.rb2
-rw-r--r--spec/requests/api/issues/get_project_issues_spec.rb2
-rw-r--r--spec/requests/api/issues/issues_spec.rb4
-rw-r--r--spec/requests/api/issues/post_projects_issues_spec.rb8
-rw-r--r--spec/requests/api/issues/put_projects_issues_spec.rb2
-rw-r--r--spec/requests/api/maven_packages_spec.rb32
-rw-r--r--spec/requests/api/members_spec.rb48
-rw-r--r--spec/requests/api/merge_request_approvals_spec.rb24
-rw-r--r--spec/requests/api/merge_requests_spec.rb8
-rw-r--r--spec/requests/api/metadata_spec.rb8
-rw-r--r--spec/requests/api/ml/mlflow/model_versions_spec.rb86
-rw-r--r--spec/requests/api/ml/mlflow/registered_models_spec.rb203
-rw-r--r--spec/requests/api/npm_project_packages_spec.rb4
-rw-r--r--spec/requests/api/pages/pages_spec.rb13
-rw-r--r--spec/requests/api/personal_access_tokens_spec.rb12
-rw-r--r--spec/requests/api/project_attributes.yml1
-rw-r--r--spec/requests/api/project_container_repositories_spec.rb10
-rw-r--r--spec/requests/api/project_packages_spec.rb20
-rw-r--r--spec/requests/api/project_repository_storage_moves_spec.rb24
-rw-r--r--spec/requests/api/project_templates_spec.rb4
-rw-r--r--spec/requests/api/projects_spec.rb67
-rw-r--r--spec/requests/api/pypi_packages_spec.rb17
-rw-r--r--spec/requests/api/releases_spec.rb2
-rw-r--r--spec/requests/api/repositories_spec.rb6
-rw-r--r--spec/requests/api/resource_access_tokens_spec.rb16
-rw-r--r--spec/requests/api/search_spec.rb4
-rw-r--r--spec/requests/api/settings_spec.rb10
-rw-r--r--spec/requests/api/tags_spec.rb2
-rw-r--r--spec/requests/api/task_completion_status_spec.rb16
-rw-r--r--spec/requests/api/unleash_spec.rb2
-rw-r--r--spec/requests/api/users_spec.rb44
-rw-r--r--spec/requests/api/v3/github_spec.rb721
-rw-r--r--spec/requests/api/vs_code/settings/vs_code_settings_sync_spec.rb89
-rw-r--r--spec/requests/api/wikis_spec.rb30
-rw-r--r--spec/requests/application_controller_spec.rb15
-rw-r--r--spec/requests/chaos_controller_spec.rb14
-rw-r--r--spec/requests/explore/catalog_controller_spec.rb53
-rw-r--r--spec/requests/external_redirect/external_redirect_controller_spec.rb60
-rw-r--r--spec/requests/health_controller_spec.rb4
-rw-r--r--spec/requests/jira_authorizations_spec.rb88
-rw-r--r--spec/requests/jwt_controller_spec.rb55
-rw-r--r--spec/requests/lfs_http_spec.rb6
-rw-r--r--spec/requests/lfs_locks_api_spec.rb8
-rw-r--r--spec/requests/metrics_controller_spec.rb9
-rw-r--r--spec/requests/oauth/authorizations_controller_spec.rb4
-rw-r--r--spec/requests/organizations/organizations_controller_spec.rb6
-rw-r--r--spec/requests/profiles/comment_templates_controller_spec.rb22
-rw-r--r--spec/requests/projects/merge_requests_controller_spec.rb15
-rw-r--r--spec/requests/projects/ml/model_versions_controller_spec.rb72
-rw-r--r--spec/requests/projects/ml/models_controller_spec.rb81
-rw-r--r--spec/requests/projects/service_desk_controller_spec.rb10
-rw-r--r--spec/requests/registrations_controller_spec.rb6
-rw-r--r--spec/requests/search_controller_spec.rb1
-rw-r--r--spec/requests/sessions_spec.rb4
-rw-r--r--spec/requests/users_controller_spec.rb4
-rw-r--r--spec/routing/organizations/organizations_controller_routing_spec.rb5
-rw-r--r--spec/routing/uploads_routing_spec.rb2
-rw-r--r--spec/rubocop/batched_background_migrations_dictionary_spec.rb67
-rw-r--r--spec/rubocop/batched_background_migrations_spec.rb43
-rw-r--r--spec/rubocop/cop/background_migration/dictionary_file_spec.rb182
-rw-r--r--spec/rubocop/cop/background_migration/missing_dictionary_file_spec.rb137
-rw-r--r--spec/rubocop/cop/gitlab/doc_url_spec.rb6
-rw-r--r--spec/rubocop/cop/gitlab/feature_available_usage_spec.rb2
-rw-r--r--spec/rubocop/cop/gitlab/mark_used_feature_flags_spec.rb34
-rw-r--r--spec/rubocop/cop/migration/migration_record_spec.rb2
-rw-r--r--spec/rubocop/cop/migration/migration_with_milestone_spec.rb99
-rw-r--r--spec/rubocop/cop/migration/prevent_index_creation_spec.rb2
-rw-r--r--spec/rubocop/cop/migration/sidekiq_queue_migrate_spec.rb2
-rw-r--r--spec/rubocop/cop/migration/unfinished_dependencies_spec.rb2
-rw-r--r--spec/rubocop/cop/migration/with_lock_retries_disallowed_method_spec.rb2
-rw-r--r--spec/rubocop/cop/performance/readlines_each_spec.rb2
-rw-r--r--spec/rubocop/cop/style/inline_disable_annotation_spec.rb46
-rw-r--r--spec/rubocop/rubocop_spec.rb13
-rw-r--r--spec/scripts/lib/glfm/update_specification_spec.rb2
-rw-r--r--spec/serializers/build_details_entity_spec.rb4
-rw-r--r--spec/serializers/ci/job_entity_spec.rb2
-rw-r--r--spec/serializers/ci/pipeline_entity_spec.rb1
-rw-r--r--spec/serializers/container_repositories_serializer_spec.rb2
-rw-r--r--spec/serializers/diff_file_entity_spec.rb2
-rw-r--r--spec/serializers/group_child_entity_spec.rb6
-rw-r--r--spec/serializers/group_link/group_group_link_entity_spec.rb65
-rw-r--r--spec/serializers/group_link/project_group_link_entity_spec.rb62
-rw-r--r--spec/serializers/issue_entity_spec.rb2
-rw-r--r--spec/serializers/merge_request_widget_entity_spec.rb2
-rw-r--r--spec/serializers/review_app_setup_entity_spec.rb4
-rw-r--r--spec/services/activity_pub/accept_follow_service_spec.rb77
-rw-r--r--spec/services/activity_pub/inbox_resolver_service_spec.rb99
-rw-r--r--spec/services/admin/plan_limits/update_service_spec.rb76
-rw-r--r--spec/services/application_settings/update_service_spec.rb6
-rw-r--r--spec/services/auto_merge/base_service_spec.rb24
-rw-r--r--spec/services/award_emojis/copy_service_spec.rb2
-rw-r--r--spec/services/bulk_imports/batched_relation_export_service_spec.rb23
-rw-r--r--spec/services/bulk_imports/file_download_service_spec.rb9
-rw-r--r--spec/services/bulk_imports/lfs_objects_export_service_spec.rb2
-rw-r--r--spec/services/bulk_imports/process_service_spec.rb25
-rw-r--r--spec/services/bulk_imports/relation_batch_export_service_spec.rb28
-rw-r--r--spec/services/bulk_imports/relation_export_service_spec.rb35
-rw-r--r--spec/services/ci/cancel_pipeline_service_spec.rb17
-rw-r--r--spec/services/ci/catalog/resources/create_service_spec.rb49
-rw-r--r--spec/services/ci/catalog/resources/release_service_spec.rb62
-rw-r--r--spec/services/ci/catalog/resources/validate_service_spec.rb77
-rw-r--r--spec/services/ci/catalog/resources/versions/create_service_spec.rb180
-rw-r--r--spec/services/ci/create_pipeline_service/pre_post_stages_spec.rb6
-rw-r--r--spec/services/ci/create_pipeline_service/rules_spec.rb2
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb2
-rw-r--r--spec/services/ci/enqueue_job_service_spec.rb29
-rw-r--r--spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb88
-rw-r--r--spec/services/ci/pipelines/update_metadata_service_spec.rb34
-rw-r--r--spec/services/ci/play_build_service_spec.rb4
-rw-r--r--spec/services/ci/refs/enqueue_pipelines_to_unlock_service_spec.rb58
-rw-r--r--spec/services/ci/register_job_service_spec.rb8
-rw-r--r--spec/services/ci/retry_job_service_spec.rb16
-rw-r--r--spec/services/ci/retry_pipeline_service_spec.rb2
-rw-r--r--spec/services/ci/runners/register_runner_service_spec.rb4
-rw-r--r--spec/services/ci/stuck_builds/drop_pending_service_spec.rb2
-rw-r--r--spec/services/ci/stuck_builds/drop_running_service_spec.rb2
-rw-r--r--spec/services/ci/stuck_builds/drop_scheduled_service_spec.rb2
-rw-r--r--spec/services/clusters/kubernetes/create_or_update_service_account_service_spec.rb12
-rw-r--r--spec/services/container_registry/protection/create_rule_service_spec.rb145
-rw-r--r--spec/services/deployments/update_environment_service_spec.rb2
-rw-r--r--spec/services/design_management/copy_design_collection/copy_service_spec.rb2
-rw-r--r--spec/services/draft_notes/publish_service_spec.rb25
-rw-r--r--spec/services/environments/auto_recover_service_spec.rb99
-rw-r--r--spec/services/git/branch_hooks_service_spec.rb21
-rw-r--r--spec/services/git/branch_push_service_spec.rb2
-rw-r--r--spec/services/git/process_ref_changes_service_spec.rb2
-rw-r--r--spec/services/google_cloud/generate_pipeline_service_spec.rb16
-rw-r--r--spec/services/groups/update_statistics_service_spec.rb2
-rw-r--r--spec/services/import/gitlab_projects/create_project_service_spec.rb6
-rw-r--r--spec/services/import/validate_remote_git_endpoint_service_spec.rb43
-rw-r--r--spec/services/issuable/common_system_notes_service_spec.rb2
-rw-r--r--spec/services/issuable/discussions_list_service_spec.rb6
-rw-r--r--spec/services/issuable/process_assignees_spec.rb48
-rw-r--r--spec/services/issues/export_csv_service_spec.rb2
-rw-r--r--spec/services/issues/update_service_spec.rb19
-rw-r--r--spec/services/jira/requests/projects/list_service_spec.rb4
-rw-r--r--spec/services/jira_connect_subscriptions/create_service_spec.rb6
-rw-r--r--spec/services/lfs/file_transformer_spec.rb2
-rw-r--r--spec/services/merge_requests/conflicts/resolve_service_spec.rb8
-rw-r--r--spec/services/merge_requests/mark_reviewer_reviewed_service_spec.rb55
-rw-r--r--spec/services/merge_requests/merge_service_spec.rb4
-rw-r--r--spec/services/merge_requests/mergeability/check_ci_status_service_spec.rb41
-rw-r--r--spec/services/merge_requests/mergeability/check_discussions_status_service_spec.rb41
-rw-r--r--spec/services/merge_requests/mergeability/check_rebase_status_service_spec.rb41
-rw-r--r--spec/services/merge_requests/mergeability/run_checks_service_spec.rb20
-rw-r--r--spec/services/merge_requests/push_options_handler_service_spec.rb59
-rw-r--r--spec/services/merge_requests/pushed_branches_service_spec.rb4
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb2
-rw-r--r--spec/services/merge_requests/update_reviewer_state_service_spec.rb85
-rw-r--r--spec/services/merge_requests/update_service_spec.rb4
-rw-r--r--spec/services/ml/create_candidate_service_spec.rb57
-rw-r--r--spec/services/ml/create_model_service_spec.rb81
-rw-r--r--spec/services/ml/find_model_service_spec.rb29
-rw-r--r--spec/services/ml/find_or_create_model_service_spec.rb5
-rw-r--r--spec/services/ml/find_or_create_model_version_service_spec.rb12
-rw-r--r--spec/services/ml/model_versions/get_model_version_service_spec.rb28
-rw-r--r--spec/services/ml/update_model_service_spec.rb27
-rw-r--r--spec/services/notes/create_service_spec.rb73
-rw-r--r--spec/services/organizations/create_service_spec.rb40
-rw-r--r--spec/services/packages/create_dependency_service_spec.rb16
-rw-r--r--spec/services/packages/npm/create_package_service_spec.rb364
-rw-r--r--spec/services/packages/npm/generate_metadata_service_spec.rb2
-rw-r--r--spec/services/packages/nuget/check_duplicates_service_spec.rb4
-rw-r--r--spec/services/packages/nuget/create_dependency_service_spec.rb6
-rw-r--r--spec/services/packages/nuget/extract_metadata_file_service_spec.rb14
-rw-r--r--spec/services/packages/nuget/metadata_extraction_service_spec.rb7
-rw-r--r--spec/services/packages/nuget/process_package_file_service_spec.rb41
-rw-r--r--spec/services/packages/nuget/symbols/create_symbol_files_service_spec.rb46
-rw-r--r--spec/services/packages/nuget/symbols/extract_signature_and_checksum_service_spec.rb46
-rw-r--r--spec/services/packages/nuget/symbols/extract_symbol_signature_service_spec.rb23
-rw-r--r--spec/services/packages/nuget/update_package_from_metadata_service_spec.rb37
-rw-r--r--spec/services/packages/protection/create_rule_service_spec.rb2
-rw-r--r--spec/services/packages/protection/delete_rule_service_spec.rb92
-rw-r--r--spec/services/packages/pypi/create_package_service_spec.rb24
-rw-r--r--spec/services/packages/update_tags_service_spec.rb2
-rw-r--r--spec/services/pages/delete_service_spec.rb22
-rw-r--r--spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb2
-rw-r--r--spec/services/product_analytics/build_graph_service_spec.rb2
-rw-r--r--spec/services/projects/branches_by_mode_service_spec.rb2
-rw-r--r--spec/services/projects/container_repository/delete_tags_service_spec.rb4
-rw-r--r--spec/services/projects/container_repository/gitlab/delete_tags_service_spec.rb6
-rw-r--r--spec/services/projects/container_repository/third_party/delete_tags_service_spec.rb4
-rw-r--r--spec/services/projects/create_service_spec.rb2
-rw-r--r--spec/services/projects/destroy_service_spec.rb25
-rw-r--r--spec/services/projects/fork_service_spec.rb20
-rw-r--r--spec/services/projects/group_links/create_service_spec.rb95
-rw-r--r--spec/services/projects/group_links/destroy_service_spec.rb143
-rw-r--r--spec/services/projects/group_links/update_service_spec.rb121
-rw-r--r--spec/services/projects/lfs_pointers/lfs_link_service_spec.rb6
-rw-r--r--spec/services/projects/operations/update_service_spec.rb2
-rw-r--r--spec/services/projects/record_target_platforms_service_spec.rb12
-rw-r--r--spec/services/projects/update_pages_service_spec.rb82
-rw-r--r--spec/services/projects/update_repository_storage_service_spec.rb24
-rw-r--r--spec/services/projects/update_statistics_service_spec.rb14
-rw-r--r--spec/services/quick_actions/interpret_service_spec.rb82
-rw-r--r--spec/services/releases/create_service_spec.rb20
-rw-r--r--spec/services/service_desk/custom_email_verifications/update_service_spec.rb27
-rw-r--r--spec/services/service_desk/custom_emails/create_service_spec.rb29
-rw-r--r--spec/services/service_desk_settings/update_service_spec.rb28
-rw-r--r--spec/services/spam/spam_action_service_spec.rb59
-rw-r--r--spec/services/system_notes/issuables_service_spec.rb4
-rw-r--r--spec/services/upload_service_spec.rb2
-rw-r--r--spec/services/users/refresh_authorized_projects_service_spec.rb7
-rw-r--r--spec/services/users/upsert_credit_card_validation_service_spec.rb111
-rw-r--r--spec/services/vs_code/settings/delete_service_spec.rb21
-rw-r--r--spec/services/web_hook_service_spec.rb62
-rw-r--r--spec/sidekiq_cluster/sidekiq_cluster_spec.rb18
-rw-r--r--spec/spec_helper.rb15
-rw-r--r--spec/support/atlassian/jira_connect/schemata.rb36
-rw-r--r--spec/support/capybara.rb2
-rw-r--r--spec/support/capybara_slow_finder.rb4
-rw-r--r--spec/support/database/auto_explain.rb5
-rw-r--r--spec/support/database/click_house/hooks.rb34
-rw-r--r--spec/support/database/partitioning_routing_analyzer.rb7
-rw-r--r--spec/support/db_cleaner.rb6
-rw-r--r--spec/support/finder_collection_allowlist.yml1
-rw-r--r--spec/support/helpers/api_internal_base_helpers.rb10
-rw-r--r--spec/support/helpers/click_house_test_helpers.rb85
-rw-r--r--spec/support/helpers/crypto_helpers.rb7
-rw-r--r--spec/support/helpers/cycle_analytics_helpers.rb25
-rw-r--r--spec/support/helpers/cycle_analytics_helpers/test_generation.rb166
-rw-r--r--spec/support/helpers/database/duplicate_indexes.yml197
-rw-r--r--spec/support/helpers/email_helpers.rb3
-rw-r--r--spec/support/helpers/features/dom_helpers.rb4
-rw-r--r--spec/support/helpers/features/releases_helpers.rb16
-rw-r--r--spec/support/helpers/gitaly_setup.rb4
-rw-r--r--spec/support/helpers/gpg_helpers.rb2
-rw-r--r--spec/support/helpers/graphql_helpers.rb1
-rw-r--r--spec/support/helpers/listbox_helpers.rb4
-rw-r--r--spec/support/helpers/login_helpers.rb29
-rw-r--r--spec/support/helpers/navbar_structure_helper.rb82
-rw-r--r--spec/support/helpers/packages_manager_api_spec_helper.rb34
-rw-r--r--spec/support/helpers/packages_manager_api_spec_helpers.rb34
-rw-r--r--spec/support/helpers/prevent_set_operator_mismatch_helper.rb16
-rw-r--r--spec/support/helpers/project_template_test_helper.rb1
-rw-r--r--spec/support/helpers/prometheus_helpers.rb2
-rw-r--r--spec/support/helpers/repo_helpers.rb4
-rw-r--r--spec/support/helpers/search_helpers.rb26
-rw-r--r--spec/support/helpers/seed_repo.rb2
-rw-r--r--spec/support/helpers/stub_saas_features.rb4
-rw-r--r--spec/support/helpers/test_env.rb8
-rw-r--r--spec/support/helpers/usage_data_helpers.rb8
-rw-r--r--spec/support/import_export/configuration_helper.rb2
-rw-r--r--spec/support/import_export/export_file_helper.rb2
-rw-r--r--spec/support/matchers/markdown_matchers.rb2
-rw-r--r--spec/support/matchers/navigation_matcher.rb12
-rw-r--r--spec/support/redis.rb7
-rw-r--r--spec/support/rspec_order_todo.yml390
-rw-r--r--spec/support/shared_contexts/ci/catalog/resources/version_shared_context.rb33
-rw-r--r--spec/support/shared_contexts/controllers/ambiguous_ref_controller_shared_context.rb19
-rw-r--r--spec/support/shared_contexts/features/integrations/project_integrations_shared_context.rb2
-rw-r--r--spec/support/shared_contexts/graphql/resolvers/runners_resolver_shared_context.rb6
-rw-r--r--spec/support/shared_contexts/graphql/types/query_type_shared_context.rb2
-rw-r--r--spec/support/shared_contexts/lib/gitlab/sidekiq_middleware/server_metrics_shared_context.rb25
-rw-r--r--spec/support/shared_contexts/models/ci/job_token_scope.rb2
-rw-r--r--spec/support/shared_contexts/navbar_structure_context.rb172
-rw-r--r--spec/support/shared_contexts/policies/project_policy_shared_context.rb2
-rw-r--r--spec/support/shared_contexts/services/projects/container_repository/delete_tags_service_shared_context.rb22
-rw-r--r--spec/support/shared_examples/analytics/cycle_analytics/request_params_examples.rb12
-rw-r--r--spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/ci/deployable_policy_shared_examples.rb1
-rw-r--r--spec/support/shared_examples/ci/deployable_policy_shared_examples_ee.rb6
-rw-r--r--spec/support/shared_examples/ci/redis_shared_examples.rb222
-rw-r--r--spec/support/shared_examples/controllers/base_action_controller_shared_examples.rb26
-rw-r--r--spec/support/shared_examples/controllers/is_ambiguous_ref_examples.rb55
-rw-r--r--spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/database_health_status_indicators/prometheus_alert_based_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/features/2fa_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/features/content_editor_shared_examples.rb89
-rw-r--r--spec/support/shared_examples/features/dashboard/sidebar_shared_examples.rb12
-rw-r--r--spec/support/shared_examples/features/explore/sidebar_shared_examples.rb28
-rw-r--r--spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb26
-rw-r--r--spec/support/shared_examples/features/nav_sidebar_shared_examples.rb11
-rw-r--r--spec/support/shared_examples/features/navbar_shared_examples.rb13
-rw-r--r--spec/support/shared_examples/features/packages_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/features/page_description_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/features/project_features_apply_to_issuables_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/features/search/search_timeouts_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/features/snippets_shared_examples.rb26
-rw-r--r--spec/support/shared_examples/features/variable_list_env_scope_shared_examples.rb87
-rw-r--r--spec/support/shared_examples/features/variable_list_shared_examples.rb292
-rw-r--r--spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb9
-rw-r--r--spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb17
-rw-r--r--spec/support/shared_examples/features/work_items_shared_examples.rb225
-rw-r--r--spec/support/shared_examples/finders/issues_finder_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/graphql/notes_quick_actions_for_work_items_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/graphql/resolvers/data_transfer_resolver_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/graphql/resolvers/packages_resolvers_shared_examples.rb12
-rw-r--r--spec/support/shared_examples/graphql/resolvers/users/pages_visits_resolvers_shared_examples.rb39
-rw-r--r--spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/lib/banzai/filters/sanitization_filter_shared_examples.rb22
-rw-r--r--spec/support/shared_examples/lib/gitlab/import/advance_stage_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/lib/gitlab/middleware/multipart_shared_examples.rb24
-rw-r--r--spec/support/shared_examples/lib/sbom/package_url_shared_examples.rb23
-rw-r--r--spec/support/shared_examples/lib/wikis_api_examples.rb6
-rw-r--r--spec/support/shared_examples/mailers/notify_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/metrics/transaction_metrics_with_labels_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/models/application_setting_shared_examples.rb18
-rw-r--r--spec/support/shared_examples/models/concerns/can_move_repository_storage_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/models/concerns/repository_storage_movable_shared_examples.rb52
-rw-r--r--spec/support/shared_examples/models/diff_positionable_note_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/models/project_ci_cd_settings_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/models/users/pages_visits_shared_examples.rb104
-rw-r--r--spec/support/shared_examples/models/wiki_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/path_extraction_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/quick_actions/commit/tag_quick_action_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/quick_actions/issue/create_merge_request_quick_action_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/redis/redis_shared_examples.rb15
-rw-r--r--spec/support/shared_examples/requests/api/container_repositories_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb14
-rw-r--r--spec/support/shared_examples/requests/api/graphql/remote_development_shared_examples.rb48
-rw-r--r--spec/support/shared_examples/requests/api/graphql/work_item_list_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/requests/api/helm_packages_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/requests/api/integrations/github_enterprise_jira_dvcs_end_of_life_shared_examples.rb23
-rw-r--r--spec/support/shared_examples/requests/api/ml/mlflow/mlflow_shared_examples.rb31
-rw-r--r--spec/support/shared_examples/requests/api/multiple_and_scoped_issue_boards_shared_examples.rb1
-rw-r--r--spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb9
-rw-r--r--spec/support/shared_examples/requests/api/repository_storage_moves_shared_examples.rb100
-rw-r--r--spec/support/shared_examples/requests/api/rubygems_packages_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/sends_git_audit_streaming_event_shared_examples.rb41
-rw-r--r--spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/services/notification_service_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/services/protected_branches_shared_examples.rb31
-rw-r--r--spec/support/shared_examples/validators/url_validator_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/views/themed_layout_examples.rb6
-rw-r--r--spec/support/sidekiq_middleware.rb2
-rw-r--r--spec/support_specs/graphql/arguments_spec.rb2
-rw-r--r--spec/support_specs/helpers/active_record/query_recorder_spec.rb2
-rw-r--r--spec/support_specs/helpers/migrations_helpers_spec.rb4
-rw-r--r--spec/support_specs/helpers/stub_saas_features_spec.rb6
-rw-r--r--spec/support_specs/matchers/exceed_query_limit_helpers_spec.rb24
-rw-r--r--spec/tasks/gitlab/background_migrations_rake_spec.rb1
-rw-r--r--spec/tasks/gitlab/backup_rake_spec.rb4
-rw-r--r--spec/tasks/gitlab/click_house/migration_rake_spec.rb133
-rw-r--r--spec/tasks/gitlab/db_rake_spec.rb24
-rw-r--r--spec/tasks/gitlab/feature_categories_rake_spec.rb10
-rw-r--r--spec/tasks/gitlab/redis_rake_spec.rb188
-rw-r--r--spec/tooling/danger/analytics_instrumentation_spec.rb62
-rw-r--r--spec/tooling/danger/bulk_database_actions_spec.rb6
-rw-r--r--spec/tooling/danger/change_column_default_spec.rb104
-rw-r--r--spec/tooling/danger/clickhouse_spec.rb4
-rw-r--r--spec/tooling/danger/config_files_spec.rb4
-rw-r--r--spec/tooling/danger/customer_success_spec.rb28
-rw-r--r--spec/tooling/danger/database_dictionary_spec.rb2
-rw-r--r--spec/tooling/danger/database_spec.rb4
-rw-r--r--spec/tooling/danger/datateam_spec.rb60
-rw-r--r--spec/tooling/danger/experiments_spec.rb2
-rw-r--r--spec/tooling/danger/feature_flag_spec.rb4
-rw-r--r--spec/tooling/danger/gitlab_schema_validation_suggestion_spec.rb108
-rw-r--r--spec/tooling/danger/ignored_model_columns_spec.rb2
-rw-r--r--spec/tooling/danger/model_validations_spec.rb4
-rw-r--r--spec/tooling/danger/multiversion_spec.rb2
-rw-r--r--spec/tooling/danger/outdated_todo_spec.rb83
-rw-r--r--spec/tooling/danger/project_helper_spec.rb4
-rw-r--r--spec/tooling/danger/required_stops_spec.rb4
-rw-r--r--spec/tooling/danger/rubocop_inline_disable_suggestion_spec.rb39
-rw-r--r--spec/tooling/danger/saas_feature_spec.rb2
-rw-r--r--spec/tooling/danger/sidekiq_args_spec.rb4
-rw-r--r--spec/tooling/danger/sidekiq_queues_spec.rb14
-rw-r--r--spec/tooling/danger/specs/feature_category_suggestion_spec.rb1
-rw-r--r--spec/tooling/danger/specs/match_with_array_suggestion_spec.rb1
-rw-r--r--spec/tooling/danger/specs/project_factory_suggestion_spec.rb1
-rw-r--r--spec/tooling/danger/specs_spec.rb1
-rw-r--r--spec/tooling/danger/stable_branch_spec.rb4
-rw-r--r--spec/tooling/fixtures/change_column_default_migration.txt13
-rw-r--r--spec/tooling/lib/tooling/find_changes_spec.rb35
-rw-r--r--spec/tooling/lib/tooling/test_map_generator_spec.rb2
-rw-r--r--spec/tooling/quality/test_level_spec.rb4
-rw-r--r--spec/uploaders/attachment_uploader_spec.rb10
-rw-r--r--spec/uploaders/avatar_uploader_spec.rb10
-rw-r--r--spec/uploaders/ci/pipeline_artifact_uploader_spec.rb4
-rw-r--r--spec/uploaders/dependency_proxy/file_uploader_spec.rb4
-rw-r--r--spec/uploaders/design_management/design_v432x230_uploader_spec.rb14
-rw-r--r--spec/uploaders/external_diff_uploader_spec.rb8
-rw-r--r--spec/uploaders/import_export_uploader_spec.rb4
-rw-r--r--spec/uploaders/job_artifact_uploader_spec.rb4
-rw-r--r--spec/uploaders/lfs_object_uploader_spec.rb4
-rw-r--r--spec/uploaders/namespace_file_uploader_spec.rb6
-rw-r--r--spec/uploaders/object_storage_spec.rb4
-rw-r--r--spec/uploaders/packages/composer/cache_uploader_spec.rb4
-rw-r--r--spec/uploaders/packages/debian/component_file_uploader_spec.rb8
-rw-r--r--spec/uploaders/packages/debian/distribution_release_file_uploader_spec.rb8
-rw-r--r--spec/uploaders/packages/package_file_uploader_spec.rb4
-rw-r--r--spec/uploaders/pages/deployment_uploader_spec.rb4
-rw-r--r--spec/uploaders/personal_file_uploader_spec.rb14
-rw-r--r--spec/validators/any_field_validator_spec.rb4
-rw-r--r--spec/validators/ip_cidr_array_validator_spec.rb45
-rw-r--r--spec/validators/ip_cidr_validator_spec.rb45
-rw-r--r--spec/views/admin/application_settings/_repository_storage.html.haml_spec.rb2
-rw-r--r--spec/views/ci/status/_badge.html.haml_spec.rb92
-rw-r--r--spec/views/ci/status/_icon.html.haml_spec.rb6
-rw-r--r--spec/views/groups/edit.html.haml_spec.rb1
-rw-r--r--spec/views/layouts/_head.html.haml_spec.rb10
-rw-r--r--spec/views/layouts/application.html.haml_spec.rb1
-rw-r--r--spec/views/layouts/header/_new_dropdown.haml_spec.rb5
-rw-r--r--spec/views/layouts/header/_super_sidebar_logged_out.html.haml_spec.rb1
-rw-r--r--spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb4
-rw-r--r--spec/views/layouts/snippets.html.haml_spec.rb32
-rw-r--r--spec/views/projects/commit/branches.html.haml_spec.rb2
-rw-r--r--spec/views/projects/commits/_commit.html.haml_spec.rb6
-rw-r--r--spec/views/projects/issues/_related_branches.html.haml_spec.rb3
-rw-r--r--spec/views/projects/pages/new.html.haml_spec.rb28
-rw-r--r--spec/views/projects/tags/index.html.haml_spec.rb2
-rw-r--r--spec/views/search/show.html.haml_spec.rb7
-rw-r--r--spec/workers/abuse/spam_abuse_events_worker_spec.rb85
-rw-r--r--spec/workers/activity_pub/projects/releases_subscription_worker_spec.rb128
-rw-r--r--spec/workers/bulk_import_worker_spec.rb14
-rw-r--r--spec/workers/bulk_imports/entity_worker_spec.rb54
-rw-r--r--spec/workers/bulk_imports/export_request_worker_spec.rb3
-rw-r--r--spec/workers/bulk_imports/finish_batched_pipeline_worker_spec.rb117
-rw-r--r--spec/workers/bulk_imports/pipeline_batch_worker_spec.rb213
-rw-r--r--spec/workers/bulk_imports/pipeline_worker_spec.rb341
-rw-r--r--spec/workers/bulk_imports/relation_batch_export_worker_spec.rb19
-rw-r--r--spec/workers/bulk_imports/relation_export_worker_spec.rb16
-rw-r--r--spec/workers/bulk_imports/stuck_import_worker_spec.rb31
-rw-r--r--spec/workers/ci/cancel_pipeline_worker_spec.rb24
-rw-r--r--spec/workers/ci/initial_pipeline_process_worker_spec.rb26
-rw-r--r--spec/workers/ci/pipeline_success_unlock_artifacts_worker_spec.rb3
-rw-r--r--spec/workers/ci/refs/unlock_previous_pipelines_worker_spec.rb21
-rw-r--r--spec/workers/concerns/gitlab/github_import/stage_methods_spec.rb30
-rw-r--r--spec/workers/concerns/worker_attributes_spec.rb2
-rw-r--r--spec/workers/concerns/worker_context_spec.rb4
-rw-r--r--spec/workers/container_registry/migration/enqueuer_worker_spec.rb6
-rw-r--r--spec/workers/environments/auto_recover_worker_spec.rb68
-rw-r--r--spec/workers/environments/auto_stop_cron_worker_spec.rb8
-rw-r--r--spec/workers/every_sidekiq_worker_spec.rb19
-rw-r--r--spec/workers/gitlab/bitbucket_import/advance_stage_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/github_import/stage/import_attachments_worker_spec.rb4
-rw-r--r--spec/workers/gitlab/github_import/stage/import_base_data_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/github_import/stage/import_collaborators_worker_spec.rb8
-rw-r--r--spec/workers/gitlab/github_import/stage/import_issue_events_worker_spec.rb4
-rw-r--r--spec/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/github_import/stage/import_lfs_objects_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/github_import/stage/import_notes_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/github_import/stage/import_protected_branches_worker_spec.rb5
-rw-r--r--spec/workers/gitlab/github_import/stage/import_pull_requests_merged_by_worker_spec.rb5
-rw-r--r--spec/workers/gitlab/github_import/stage/import_pull_requests_review_requests_worker_spec.rb5
-rw-r--r--spec/workers/gitlab/github_import/stage/import_pull_requests_reviews_worker_spec.rb4
-rw-r--r--spec/workers/gitlab/github_import/stage/import_pull_requests_worker_spec.rb13
-rw-r--r--spec/workers/gitlab/jira_import/stage/import_issues_worker_spec.rb40
-rw-r--r--spec/workers/groups/update_statistics_worker_spec.rb2
-rw-r--r--spec/workers/jira_connect/sync_branch_worker_spec.rb2
-rw-r--r--spec/workers/merge_request_cleanup_refs_worker_spec.rb12
-rw-r--r--spec/workers/merge_requests/set_reviewer_reviewed_worker_spec.rb10
-rw-r--r--spec/workers/packages/cleanup_package_registry_worker_spec.rb22
-rw-r--r--spec/workers/packages/npm/cleanup_stale_metadata_cache_worker_spec.rb79
-rw-r--r--spec/workers/post_receive_spec.rb2
-rw-r--r--spec/workers/project_cache_worker_spec.rb14
-rw-r--r--spec/workers/projects/import_export/after_import_merge_requests_worker_spec.rb23
-rw-r--r--spec/workers/projects/record_target_platforms_worker_spec.rb2
-rw-r--r--spec/workers/repository_fork_worker_spec.rb30
-rw-r--r--spec/workers/schedule_merge_request_cleanup_refs_worker_spec.rb12
-rw-r--r--spec/workers/stuck_merge_jobs_worker_spec.rb6
-rw-r--r--spec/workers/tasks_to_be_done/create_worker_spec.rb22
-rw-r--r--spec/workers/update_project_statistics_worker_spec.rb2
-rw-r--r--storybook/config/addons/vuex_store/index.js16
-rw-r--r--storybook/config/preview.js2
-rw-r--r--tooling/danger/analytics_instrumentation.rb23
-rw-r--r--tooling/danger/change_column_default.rb31
-rw-r--r--tooling/danger/datateam.rb13
-rw-r--r--tooling/danger/gitlab_schema_validation_suggestion.rb35
-rw-r--r--tooling/danger/outdated_todo.rb58
-rw-r--r--tooling/danger/project_helper.rb2
-rw-r--r--tooling/danger/rubocop_inline_disable_suggestion.rb10
-rw-r--r--tooling/danger/stable_branch.rb2
-rw-r--r--tooling/danger/suggestion.rb20
-rwxr-xr-xtooling/lib/tooling/find_changes.rb9
-rw-r--r--tooling/quality/test_level.rb1
-rw-r--r--vendor/gems/cloud_profiler_agent/spec/cloud_profiler_agent/looper_spec.rb2
-rw-r--r--vendor/gems/cloud_profiler_agent/spec/cloud_profiler_agent/pprof_builder_spec.rb2
-rw-r--r--vendor/gems/devise-pbkdf2-encryptable/Gemfile.lock141
-rw-r--r--vendor/gems/devise-pbkdf2-encryptable/devise-pbkdf2-encryptable.gemspec4
-rw-r--r--vendor/gems/sidekiq-reliable-fetch/Gemfile.lock2
-rw-r--r--vendor/gems/sidekiq-reliable-fetch/gitlab-sidekiq-fetcher.gemspec2
-rw-r--r--vendor/gems/sidekiq-reliable-fetch/lib/sidekiq/base_reliable_fetch.rb2
-rw-r--r--vendor/gems/sidekiq-reliable-fetch/spec/base_reliable_fetch_spec.rb13
-rwxr-xr-xvendor/languages.yml2
-rw-r--r--vendor/project_templates/astro_tailwind.tar.gzbin0 -> 181171 bytes
-rw-r--r--vendor/project_templates/bridgetown.tar.gzbin42296 -> 38125 bytes
-rw-r--r--vendor/project_templates/middleman.tar.gzbin9631 -> 37406 bytes
-rw-r--r--vendor/project_templates/typo3_distribution.tar.gzbin76924 -> 77280 bytes
-rw-r--r--vite.config.js8
-rw-r--r--workhorse/go.mod3
-rw-r--r--workhorse/go.sum7
-rw-r--r--workhorse/internal/goredis/goredis.go186
-rw-r--r--workhorse/internal/goredis/goredis_test.go107
-rw-r--r--workhorse/internal/goredis/keywatcher.go236
-rw-r--r--workhorse/internal/goredis/keywatcher_test.go301
-rw-r--r--workhorse/internal/redis/keywatcher.go83
-rw-r--r--workhorse/internal/redis/keywatcher_test.go120
-rw-r--r--workhorse/internal/redis/redis.go336
-rw-r--r--workhorse/internal/redis/redis_test.go222
-rw-r--r--workhorse/main.go41
-rw-r--r--yarn.lock1152
6773 files changed, 119021 insertions, 57960 deletions
diff --git a/.eslintrc.yml b/.eslintrc.yml
index f98f7acc0ad..595f4fc7b9a 100644
--- a/.eslintrc.yml
+++ b/.eslintrc.yml
@@ -71,8 +71,6 @@ rules:
- sibling
- index
pathGroups:
- - pattern: '@sentry/browser'
- group: external
- pattern: ~/**
group: internal
- pattern: emojis/**
@@ -134,6 +132,8 @@ rules:
message: 'Import { Mousetrap } from ~/lib/mousetrap instead.'
- name: vuex
message: 'See our documentation on "Migrating from VueX" for tips on how to avoid adding new VueX stores.'
+ - name: '@sentry/browser'
+ message: Use "import * as Sentry from '~/sentry/sentry_browser_wrapper';" instead
unicorn/prefer-dom-node-dataset:
- error
no-unsanitized/method:
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 4559d7d43ba..9a24f58ad73 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -64,15 +64,15 @@ workflow:
# they serve no purpose and will run anyway when the changes are merged.
- if: '$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME =~ /^release-tools\/\d+\.\d+\.\d+-rc\d+$/ && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME =~ /^[\d-]+-stable(-ee)?$/ && $CI_PROJECT_PATH == "gitlab-org/gitlab"'
when: never
- # For merge requests running exclusively in Ruby 3.1
- - if: '$CI_MERGE_REQUEST_LABELS =~ /pipeline:run-in-ruby3_1/'
+ # For merge requests running exclusively in Ruby 3.0
+ - if: '$CI_MERGE_REQUEST_LABELS =~ /pipeline:run-in-ruby3_0/'
variables:
- <<: *next-ruby-variables
+ <<: *default-ruby-variables
PIPELINE_NAME: 'Ruby $RUBY_VERSION $CI_MERGE_REQUEST_EVENT_TYPE MR pipeline'
NO_SOURCEMAPS: 'true'
- if: '$CI_MERGE_REQUEST_LABELS =~ /Community contribution/'
variables:
- <<: *default-ruby-variables
+ <<: *next-ruby-variables
GITLAB_DEPENDENCY_PROXY_ADDRESS: ""
PIPELINE_NAME: 'Ruby $RUBY_VERSION $CI_MERGE_REQUEST_EVENT_TYPE MR pipeline (community contribution)'
NO_SOURCEMAPS: 'true'
@@ -83,7 +83,7 @@ workflow:
# For (detached) merge request pipelines.
- if: '$CI_MERGE_REQUEST_IID'
variables:
- <<: *default-ruby-variables
+ <<: *next-ruby-variables
<<: *default-merge-request-slow-tests-variables
PIPELINE_NAME: 'Ruby $RUBY_VERSION $CI_MERGE_REQUEST_EVENT_TYPE MR pipeline'
NO_SOURCEMAPS: 'true'
@@ -105,6 +105,11 @@ workflow:
<<: [*default-ruby-variables, *default-branch-pipeline-failure-variables]
GITLAB_DEPENDENCY_PROXY_ADDRESS: ""
PIPELINE_NAME: 'Ruby $RUBY_VERSION $CI_COMMIT_BRANCH branch pipeline (triggered by a project token)'
+ # For `$CI_DEFAULT_BRANCH` from wider community contributors, we don't want to run any pipelines on pushes,
+ # because normally we want to run merge request pipelines and scheduled pipelines, not for repository synchronization.
+ # This can avoid accidentally using up pipeline minutes quota while synchronizing the repository for wider community contributors.
+ - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CI_PIPELINE_SOURCE == "push" && $CI_PROJECT_NAMESPACE !~ /^gitlab(-org|-cn)?($|\/)/'
+ when: never
# For `$CI_DEFAULT_BRANCH` branch, create a pipeline (this includes on schedules, pushes, merges, etc.).
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
variables:
@@ -156,7 +161,7 @@ variables:
DOCKER_VERSION: "24.0.5"
RUBYGEMS_VERSION: "3.4"
GO_VERSION: "1.20"
- RUST_VERSION: "1.65"
+ RUST_VERSION: "1.73"
FLAKY_RSPEC_SUITE_REPORT_PATH: rspec/flaky/report-suite.json
FRONTEND_FIXTURES_MAPPING_PATH: crystalball/frontend_fixtures_mapping.json
@@ -228,3 +233,7 @@ include:
- remote: 'https://gitlab.com/gitlab-org/frontend/untamper-my-lockfile/-/raw/main/templates/merge_request_pipelines.yml'
rules:
- <<: *if-not-security-canonical-sync
+ - local: .gitlab/ci/gitlab-com/*.gitlab-ci.yml
+ rules:
+ - if: '$CI_SERVER_HOST == "gitlab.com"'
+ - if: '$CI_SERVER_HOST == "jihulab.com"'
diff --git a/.gitlab/CODEOWNERS b/.gitlab/CODEOWNERS
index ea20717943d..7143d32b2cd 100644
--- a/.gitlab/CODEOWNERS
+++ b/.gitlab/CODEOWNERS
@@ -325,9 +325,9 @@ Dangerfile
/ee/app/assets/javascripts/analytics/analytics_dashboards/index.js
/ee/app/assets/javascripts/analytics/analytics_dashboards/router.js
/ee/app/assets/javascripts/analytics/analytics_dashboards/constants.js
-/ee/app/assets/javascripts/analytics/analytics_dashboards/graphql/queries/get_all_product_analytics_dashboards.query.graphql
-/ee/app/assets/javascripts/analytics/analytics_dashboards/graphql/queries/get_product_analytics_dashboard.query.graphql
-/ee/app/assets/javascripts/analytics/analytics_dashboards/graphql/queries/get_all_product_analytics_visualizations.query.graphql
+/ee/app/assets/javascripts/analytics/analytics_dashboards/graphql/queries/get_all_customizable_dashboards.query.graphql
+/ee/app/assets/javascripts/analytics/analytics_dashboards/graphql/queries/get_customizable_dashboard.query.graphql
+/ee/app/assets/javascripts/analytics/analytics_dashboards/graphql/queries/get_all_customizable_visualizations.query.graphql
/ee/app/assets/javascripts/vue_shared/components/customizable_dashboard/customizable_dashboard.vue
/ee/app/assets/javascripts/vue_shared/components/customizable_dashboard/panels_base.vue
/ee/app/assets/javascripts/product_analytics/
@@ -467,6 +467,7 @@ lib/gitlab/checks/**
/doc/administration/consul.md @axil
/doc/administration/credentials_inventory.md @jglassman1
/doc/administration/custom_project_templates.md @msedlakjakubowski
+/doc/administration/dedicated/ @lyspin
/doc/administration/diff_limits.md @msedlakjakubowski
/doc/administration/docs_self_host.md @axil
/doc/administration/encrypted_configuration.md @axil
@@ -537,6 +538,7 @@ lib/gitlab/checks/**
/doc/administration/repository_storage_paths.md @eread
/doc/administration/restart_gitlab.md @axil
/doc/administration/review_abuse_reports.md @phillipwells
+/doc/administration/review_spam_logs.md @phillipwells
/doc/administration/server_hooks.md @eread
/doc/administration/settings/account_and_limit_settings.md @msedlakjakubowski
/doc/administration/settings/continuous_integration.md @marcel.amirault
@@ -757,95 +759,39 @@ lib/gitlab/checks/**
/doc/ci/services/ @fneill
/doc/ci/test_cases/ @msedlakjakubowski
/doc/ci/testing/code_quality.md @rdickenson
-/doc/development/activitypub/ @msedlakjakubowski
-/doc/development/advanced_search.md @ashrafkhamis
-/doc/development/ai_features/ @sselhorn
-/doc/development/application_limits.md @axil
-/doc/development/audit_event_guide/ @eread
-/doc/development/auto_devops.md @phillipwells
-/doc/development/avoiding_required_stops.md @axil
-/doc/development/backend/ @sselhorn
-/doc/development/backend/create_source_code_be/ @msedlakjakubowski
-/doc/development/build_test_package.md @axil
-/doc/development/bulk_import.md @eread @ashrafkhamis
-/doc/development/cached_queries.md @jglassman1
-/doc/development/cascading_settings.md @jglassman1
-/doc/development/chatops_on_gitlabcom.md @phillipwells
-/doc/development/cicd/ @marcel.amirault
-/doc/development/cloud_connector/ @jglassman1
-/doc/development/code_intelligence/ @aqualls
-/doc/development/code_owners/ @msedlakjakubowski
-/doc/development/code_suggestions/ @jglassman1
-/doc/development/contributing/ @sselhorn
-/doc/development/database/ @aqualls
-/doc/development/database/filtering_by_label.md @msedlakjakubowski
-/doc/development/database/multiple_databases.md @lciutacu
-/doc/development/database_review.md @aqualls
-/doc/development/developing_with_solargraph.md @msedlakjakubowski
-/doc/development/development_processes.md @sselhorn
-/doc/development/distribution/ @axil
+/doc/development/advanced_search.md @gitlab-org/search-team/migration-maintainers
+/doc/development/application_limits.md @gitlab-org/distribution
+/doc/development/audit_event_guide/ @gitlab-org/govern/security-policies-frontend @gitlab-org/govern/threat-insights-frontend-team @gitlab-org/govern/threat-insights-backend-team
+/doc/development/avoiding_required_stops.md @gitlab-org/distribution
+/doc/development/build_test_package.md @gitlab-org/distribution
+/doc/development/cascading_settings.md @gitlab-org/govern/authentication/approvers
+/doc/development/cells/ @abdwdd @alexpooley @manojmj
+/doc/development/cicd/ @gitlab-org/maintainers/cicd-verify
+/doc/development/contributing/verify/ @gitlab-org/maintainers/cicd-verify
+/doc/development/database/ @abdwdd @alexpooley @manojmj
+/doc/development/distribution/ @gitlab-org/distribution
/doc/development/documentation/ @sselhorn
-/doc/development/export_csv.md @eread @ashrafkhamis
-/doc/development/fe_guide/ @sselhorn
-/doc/development/fe_guide/customizable_dashboards.md @lciutacu
-/doc/development/fe_guide/merge_request_widget_extensions.md @aqualls
-/doc/development/fe_guide/source_editor.md @msedlakjakubowski
-/doc/development/feature_categorization/ @sselhorn
-/doc/development/feature_development.md @sselhorn
-/doc/development/feature_flags/ @sselhorn
-/doc/development/fips_compliance.md @msedlakjakubowski
-/doc/development/geo.md @axil
-/doc/development/geo/ @axil
-/doc/development/git_object_deduplication.md @eread
-/doc/development/gitaly.md @eread
-/doc/development/gitlab_flavored_markdown/ @ashrafkhamis
-/doc/development/gitlab_shell/ @msedlakjakubowski
-/doc/development/graphql_guide/ @eread @ashrafkhamis
-/doc/development/graphql_guide/batchloader.md @aqualls
-/doc/development/i18n/ @eread @ashrafkhamis
-/doc/development/identity_verification.md @phillipwells
-/doc/development/image_scaling.md @lciutacu
-/doc/development/import_export.md @eread @ashrafkhamis
-/doc/development/index.md @sselhorn
-/doc/development/integrations/ @eread @ashrafkhamis
-/doc/development/integrations/secure.md @rdickenson
-/doc/development/integrations/secure_partner_integration.md @rdickenson
-/doc/development/internal_analytics/ @lciutacu
-/doc/development/internal_api/ @msedlakjakubowski
-/doc/development/internal_users.md @sselhorn
-/doc/development/issuable-like-models.md @msedlakjakubowski
-/doc/development/issue_types.md @msedlakjakubowski
-/doc/development/kubernetes.md @phillipwells
-/doc/development/labels/ @sselhorn
-/doc/development/lfs.md @msedlakjakubowski
-/doc/development/maintenance_mode.md @axil
-/doc/development/merge_request_concepts/ @aqualls
-/doc/development/merge_request_concepts/rate_limits.md @msedlakjakubowski
-/doc/development/migration_style_guide.md @aqualls
-/doc/development/navigation_sidebar.md @sselhorn
-/doc/development/omnibus.md @axil
-/doc/development/organization/ @lciutacu
-/doc/development/packages/ @phillipwells
-/doc/development/packages/cleanup_policies.md @marcel.amirault
-/doc/development/packages/dependency_proxy.md @marcel.amirault
-/doc/development/permissions.md @jglassman1
-/doc/development/permissions/ @jglassman1
-/doc/development/policies.md @jglassman1
-/doc/development/project_templates.md @msedlakjakubowski
-/doc/development/rails_endpoints/ @msedlakjakubowski
-/doc/development/real_time.md @jglassman1
-/doc/development/rubocop_development_guide.md @sselhorn
-/doc/development/search/ @ashrafkhamis
-/doc/development/sec/ @rdickenson
-/doc/development/secure_coding_guidelines.md @sselhorn
-/doc/development/spam_protection_and_captcha/ @phillipwells
-/doc/development/sql.md @aqualls
-/doc/development/testing_guide/ @sselhorn
-/doc/development/value_stream_analytics.md @lciutacu
-/doc/development/value_stream_analytics/ @lciutacu
-/doc/development/work_items.md @msedlakjakubowski
-/doc/development/work_items_widgets.md @msedlakjakubowski
-/doc/development/workhorse/ @msedlakjakubowski
+/doc/development/fe_guide/customizable_dashboards.md @gitlab-org/analytics-section/product-analytics/engineers/frontend
+/doc/development/fe_guide/onboarding_course/ @gitlab-org/manage/foundations/engineering
+/doc/development/fe_guide/view_component.md @gitlab-org/manage/foundations/engineering
+/doc/development/git_object_deduplication.md @proglottis @toon
+/doc/development/gitaly.md @proglottis @toon
+/doc/development/gitlab_flavored_markdown/ @gitlab-org/maintainers/remote-development/backend @gitlab-org/maintainers/remote-development/frontend
+/doc/development/gitpod_internals.md @gl-quality/eng-prod
+/doc/development/image_scaling.md @abdwdd @alexpooley @manojmj
+/doc/development/internal_analytics/ @gitlab-org/analytics-section/product-analytics/engineers/frontend @gitlab-org/analytics-section/analytics-instrumentation/engineers
+/doc/development/navigation_sidebar.md @gitlab-org/manage/foundations/engineering
+/doc/development/omnibus.md @gitlab-org/distribution
+/doc/development/organization/ @abdwdd @alexpooley @manojmj
+/doc/development/permissions.md @gitlab-org/govern/authentication/approvers
+/doc/development/permissions/ @gitlab-org/govern/authentication/approvers
+/doc/development/permissions/custom_roles.md @gitlab-org/govern/authorization/approvers
+/doc/development/pipelines/ @gl-quality/eng-prod
+/doc/development/policies.md @gitlab-org/govern/authentication/approvers
+/doc/development/search/ @gitlab-org/search-team/migration-maintainers
+/doc/development/sec/ @gitlab-org/govern/threat-insights-frontend-team
+/doc/development/sec/gemnasium_analyzer_data.md @gitlab-org/secure/composition-analysis-be @gitlab-org/secure/static-analysis
+/doc/development/software_design.md @gl-quality/eng-prod
/doc/downgrade_ee_to_ce/ @axil
/doc/drawers/ @ashrafkhamis
/doc/editor_extensions/ @aqualls
@@ -877,7 +823,9 @@ lib/gitlab/checks/**
/doc/security/ @jglassman1
/doc/security/email_verification.md @phillipwells
/doc/security/identity_verification.md @phillipwells
+/doc/solutions/ @jfullam @brianwald @Darwinjs
/doc/subscriptions/ @fneill
+/doc/subscriptions/gitlab_dedicated/ @lyspin
/doc/topics/autodevops/ @phillipwells
/doc/topics/data_seeder.md @sselhorn
/doc/topics/git/ @msedlakjakubowski
@@ -896,6 +844,7 @@ lib/gitlab/checks/**
/doc/tutorials/install_gitlab_single_node/ @axil
/doc/tutorials/issue_triage/ @msedlakjakubowski
/doc/tutorials/move_personal_project_to_group/ @lciutacu
+/doc/tutorials/product_analytics_onboarding_website_project/ @lciutacu
/doc/tutorials/protected_workflow/ @aqualls
/doc/tutorials/scan_execution_policy/ @rdickenson
/doc/tutorials/scan_result_policy/ @rdickenson
@@ -916,7 +865,7 @@ lib/gitlab/checks/**
/doc/user/discussions/ @aqualls
/doc/user/emoji_reactions.md @msedlakjakubowski
/doc/user/enterprise_user/ @jglassman1
-/doc/user/feature_flags.md @sselhorn
+/doc/user/gitlab_duo_chat.md @sselhorn
/doc/user/group/ @lciutacu
/doc/user/group/clusters/ @phillipwells
/doc/user/group/compliance_frameworks.md @eread
@@ -982,7 +931,7 @@ lib/gitlab/checks/**
/doc/user/project/remote_development/ @ashrafkhamis
/doc/user/project/repository/code_suggestions/ @jglassman1
/doc/user/project/repository/file_finder.md @ashrafkhamis
-/doc/user/project/repository/managing_large_repositories.md @eread
+/doc/user/project/repository/monorepos/ @eread
/doc/user/project/repository/web_editor.md @ashrafkhamis
/doc/user/project/settings/import_export.md @eread @ashrafkhamis
/doc/user/project/settings/import_export_troubleshooting.md @eread @ashrafkhamis
@@ -995,7 +944,7 @@ lib/gitlab/checks/**
/doc/user/reserved_names.md @lciutacu
/doc/user/search/ @ashrafkhamis
/doc/user/search/command_palette.md @sselhorn
-/doc/user/shortcuts.md @ashrafkhamis
+/doc/user/shortcuts.md @sselhorn
/doc/user/snippets.md @msedlakjakubowski
/doc/user/ssh.md @jglassman1
/doc/user/storage_management_automation.md @fneill
@@ -1005,6 +954,18 @@ lib/gitlab/checks/**
/doc/user/workspace/ @ashrafkhamis
# End rake-managed-docs-block
+[Authorization] @gitlab-org/govern/authorization/approvers
+/config/initializers/declarative_policy.rb
+/config/initializers/declarative_policy_cached_attributes.rb
+/app/policies/
+/ee/app/policies/
+/ee/app/services/member_roles/
+/ee/app/graphql/types/member_roles/
+/ee/app/graphql/mutations/member_roles/
+/ee/app/graphql/resolvers/member_roles/
+/ee/spec/requests/custom_roles/
+/ee/lib/api/member_roles.rb
+
[Authentication] @gitlab-org/govern/authentication/approvers
/app/assets/javascripts/access_tokens/
/app/assets/javascripts/alerts_settings/graphql/mutations/reset_http_token.mutation.graphql
@@ -1212,7 +1173,6 @@ lib/gitlab/checks/**
/ee/lib/gitlab/geo/oauth/
/ee/lib/gitlab/kerberos/
/ee/lib/omni_auth/
-/ee/spec/requests/custom_roles/
/ee/lib/system_check/geo/authorized_keys_check.rb
/ee/lib/system_check/geo/authorized_keys_flag_check.rb
/lib/api/entities/impersonation_token.rb
@@ -1342,6 +1302,7 @@ lib/gitlab/checks/**
/**/javascripts/token_access/ @gitlab-org/ci-cd/verify/frontend
/**/javascripts/admin/application_settings/runner_token_expiration/ @gitlab-org/ci-cd/verify/frontend
/**/javascripts/usage_quotas/pipelines/ @gitlab-org/ci-cd/verify/frontend @sheldonled @aalakkad @kpalchyk
+/**/javascripts/editor/schema/ci.json @gitlab-org/ci-cd/verify/frontend
## Verify:Runner Fleet Backend
@@ -1446,23 +1407,6 @@ ee/lib/ee/api/entities/project.rb
/ee/app/views/shared/icons/_icon_audit_events_purple.svg
/ee/app/views/shared/promotions/_promote_audit_events.html.haml
/ee/app/workers/audit_events/audit_event_streaming_worker.rb
-/ee/config/events/1652263097_groups__audit_events__index_click_streams_tab.yml
-/ee/config/events/202108302307_admin_audit_logs_index_click_date_range_button.yml
-/ee/config/events/202108302307_groups__audit_events_controller_search_audit_event.yml
-/ee/config/events/202108302307_profiles_controller_search_audit_event.yml
-/ee/config/events/202108302307_projects__audit_events_controller_search_audit_event.yml
-/ee/config/events/202111041910_admin__audit_logs_controller_search_audit_event.yml
-/ee/config/metrics/counts_28d/20210216183930_g_compliance_audit_events_monthly.yml
-/ee/config/metrics/counts_28d/20210216183934_i_compliance_audit_events_monthly.yml
-/ee/config/metrics/counts_28d/20210216183942_a_compliance_audit_events_api_monthly.yml
-/ee/config/metrics/counts_28d/20211130085433_g_manage_compliance_audit_event_destinations.yml
-/ee/config/metrics/counts_7d/20210216183906_g_compliance_audit_events.yml
-/ee/config/metrics/counts_7d/20210216183908_i_compliance_audit_events.yml
-/ee/config/metrics/counts_7d/20210216183912_a_compliance_audit_events_api.yml
-/ee/config/metrics/counts_7d/20210216183928_g_compliance_audit_events_weekly.yml
-/ee/config/metrics/counts_7d/20210216183932_i_compliance_audit_events_weekly.yml
-/ee/config/metrics/counts_7d/20210216183940_a_compliance_audit_events_api_weekly.yml
-/ee/config/metrics/counts_all/20211130085433_g_manage_compliance_audit_event_destinations.yml
/ee/lib/api/audit_events.rb
/ee/lib/audit/
/ee/lib/ee/api/entities/audit_event.rb
@@ -1485,6 +1429,7 @@ ee/lib/ee/api/entities/project.rb
[Manage::Foundations] @gitlab-org/manage/foundations/engineering
/lib/sidebars/
/ee/lib/sidebars/
+/ee/lib/ee/sidebars/
[Global Search] @gitlab-org/search-team/migration-maintainers
/ee/elastic/migrate/
diff --git a/.gitlab/ci/cng/main.gitlab-ci.yml b/.gitlab/ci/cng/main.gitlab-ci.yml
index e7593b8f208..1ecbbbd47ad 100644
--- a/.gitlab/ci/cng/main.gitlab-ci.yml
+++ b/.gitlab/ci/cng/main.gitlab-ci.yml
@@ -9,7 +9,7 @@ stages:
include:
- local: .gitlab/ci/global.gitlab-ci.yml
-.review-build-cng-env:
+.build-cng-env:
image: ${GITLAB_DEPENDENCY_PROXY_ADDRESS}ruby:${RUBY_VERSION}-alpine3.16
stage: prepare
needs:
@@ -34,7 +34,7 @@ include:
expire_in: 7 days
when: always
-.review-build-cng:
+.build-cng:
stage: prepare
inherit:
variables: false
@@ -54,6 +54,8 @@ include:
GITLAB_WORKHORSE_VERSION: "${GITLAB_WORKHORSE_VERSION}"
GITALY_SERVER_VERSION: "${GITALY_SERVER_VERSION}"
RUBY_VERSION: "${FULL_RUBY_VERSION}"
+ NEXT_RUBY_CACHE_KEY: "${RUBY_VERSION}"
+ NEXT_RUBY_VERSION: "${FULL_RUBY_VERSION}"
trigger:
project: ${CI_PROJECT_NAMESPACE}/build/CNG-mirror
branch: $TRIGGER_BRANCH
diff --git a/.gitlab/ci/database.gitlab-ci.yml b/.gitlab/ci/database.gitlab-ci.yml
index 082d44633f8..285c99d2cbe 100644
--- a/.gitlab/ci/database.gitlab-ci.yml
+++ b/.gitlab/ci/database.gitlab-ci.yml
@@ -72,7 +72,7 @@ db:check-schema-single-db:
db:check-migrations:
extends:
- .db-job-base
- - .use-pg14 # Should match the db same version used by GDK
+ - .use-pg14 # Should match the db same version used by GDK
- .rails:rules:ee-and-foss-mr-with-migration
script:
- git fetch origin $CI_MERGE_REQUEST_TARGET_BRANCH_NAME:$CI_MERGE_REQUEST_TARGET_BRANCH_NAME --depth 20
diff --git a/.gitlab/ci/docs.gitlab-ci.yml b/.gitlab/ci/docs.gitlab-ci.yml
index 25d974b1580..6d2616557d4 100644
--- a/.gitlab/ci/docs.gitlab-ci.yml
+++ b/.gitlab/ci/docs.gitlab-ci.yml
@@ -42,7 +42,7 @@ review-docs-cleanup:
docs-lint links:
extends:
- .docs:rules:docs-lint
- image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-docs/lint-html:alpine-3.18-ruby-3.2.2-6a53d93b
+ image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-docs/lint-html:alpine-3.18-ruby-3.2.2-08fa6df8
stage: lint
needs: []
script:
@@ -58,7 +58,7 @@ docs-lint links:
.docs-markdown-lint-image:
# When updating the image version here, update it in /scripts/lint-doc.sh too.
- image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-docs/lint-markdown:alpine-3.18-vale-2.27.0-markdownlint-0.35.0-markdownlint2-0.8.1
+ image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-docs/lint-markdown:alpine-3.18-vale-2.29.6-markdownlint-0.37.0-markdownlint2-0.10.0
docs-lint markdown:
extends:
@@ -71,6 +71,7 @@ docs-lint markdown:
script:
- source ./scripts/utils.sh
- yarn_install_script
+ - install_gitlab_gem
- scripts/lint-doc.sh
docs-lint blueprint:
diff --git a/.gitlab/ci/frontend.gitlab-ci.yml b/.gitlab/ci/frontend.gitlab-ci.yml
index a1c209abd98..2afa69bbff8 100644
--- a/.gitlab/ci/frontend.gitlab-ci.yml
+++ b/.gitlab/ci/frontend.gitlab-ci.yml
@@ -67,7 +67,7 @@ compile-test-assets:
paths:
- public/assets/
- node_modules/@gitlab/svgs/dist/icons.json # app/helpers/icons_helper.rb uses this file
- - node_modules/@gitlab/svgs/dist/file_icons/file_icons.json # app/helpers/icons_helper.rb uses this file
+ - node_modules/@gitlab/svgs/dist/file_icons/file_icons.json # app/helpers/icons_helper.rb uses this file
- "${WEBPACK_COMPILE_LOG_PATH}"
when: always
diff --git a/.gitlab/ci/gitlab-com/danger-review.gitlab-ci.yml b/.gitlab/ci/gitlab-com/danger-review.gitlab-ci.yml
new file mode 100644
index 00000000000..8328051f1a0
--- /dev/null
+++ b/.gitlab/ci/gitlab-com/danger-review.gitlab-ci.yml
@@ -0,0 +1,23 @@
+include:
+ - project: gitlab-org/quality/pipeline-common
+ ref: 7.10.3
+ file:
+ - /ci/danger-review.yml
+
+danger-review:
+ extends:
+ - .default-retry
+ - .ruby-node-cache
+ - .review:rules:danger
+ image: "${DEFAULT_CI_IMAGE}"
+ before_script:
+ - source scripts/utils.sh
+ - bundle_install_script "--with danger"
+ - yarn_install_script
+
+danger-review-local:
+ extends: danger-review
+ before_script:
+ - !reference ["danger-review", "before_script"]
+ # We unset DANGER_GITLAB_API_TOKEN so that Danger will run as local from `danger-review:script`
+ - unset DANGER_GITLAB_API_TOKEN
diff --git a/.gitlab/ci/gitlab-gems.gitlab-ci.yml b/.gitlab/ci/gitlab-gems.gitlab-ci.yml
index a773e9c7f90..cc8a058d354 100644
--- a/.gitlab/ci/gitlab-gems.gitlab-ci.yml
+++ b/.gitlab/ci/gitlab-gems.gitlab-ci.yml
@@ -29,3 +29,6 @@ include:
- local: .gitlab/ci/templates/gem.gitlab-ci.yml
inputs:
gem_name: "gitlab-http"
+ - local: .gitlab/ci/templates/gem.gitlab-ci.yml
+ inputs:
+ gem_name: "gitlab-backup-cli"
diff --git a/.gitlab/ci/global.gitlab-ci.yml b/.gitlab/ci/global.gitlab-ci.yml
index 51e23dce320..37d91fae595 100644
--- a/.gitlab/ci/global.gitlab-ci.yml
+++ b/.gitlab/ci/global.gitlab-ci.yml
@@ -36,7 +36,7 @@
.ruby-gems-cache-push: &ruby-gems-cache-push
<<: *ruby-gems-cache
- policy: push # We want to rebuild the cache from scratch to ensure stale dependencies are cleaned up.
+ policy: push # We want to rebuild the cache from scratch to ensure stale dependencies are cleaned up.
.ruby-coverage-gems-cache: &ruby-coverage-gems-cache
key: "ruby-coverage-gems-debian-${DEBIAN_VERSION}-ruby-${RUBY_VERSION}"
@@ -46,7 +46,7 @@
.ruby-coverage-gems-cache-push: &ruby-coverage-gems-cache-push
<<: *ruby-coverage-gems-cache
- policy: push # We want to rebuild the cache from scratch to ensure stale dependencies are cleaned up.
+ policy: push # We want to rebuild the cache from scratch to ensure stale dependencies are cleaned up.
.gitaly-binaries-cache: &gitaly-binaries-cache
key:
@@ -229,7 +229,7 @@
.redis-services:
services:
- name: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images:redis-cluster-6.2.12
- alias: rediscluster # configure connections in config/redis.yml
+ alias: rediscluster # configure connections in config/redis.yml
- name: redis:${REDIS_VERSION}-alpine
.pg-base-variables:
@@ -281,15 +281,6 @@
- name: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images:zoekt-ci-image-1.2
alias: zoekt-ci-image
-.use-pg12:
- extends:
- - .pg-base-variables
- services:
- - !reference [.db-services, services]
- variables:
- PG_VERSION: "12"
- REDIS_VERSION: "6.2"
-
.use-pg13:
extends:
- .pg-base-variables
@@ -323,14 +314,6 @@
- name: elasticsearch:7.17.6
command: ["elasticsearch", "-E", "discovery.type=single-node", "-E", "xpack.security.enabled=false", "-E", "cluster.routing.allocation.disk.threshold_enabled=false"]
-.use-pg12-es7-ee:
- extends:
- - .use-pg12
- - .zoekt-variables
- services:
- - !reference [.db-services, services]
- - !reference [.es7-services, services]
-
.use-pg13-es7-ee:
extends:
- .use-pg13
@@ -516,7 +499,7 @@
.fast-no-clone-job:
variables:
- GIT_STRATEGY: none # We will download the required files for the job from the API
+ GIT_STRATEGY: none # We will download the required files for the job from the API
before_script:
# Logic taken from scripts/utils.sh in download_files function
- |
diff --git a/.gitlab/ci/package-and-test-nightly/main.gitlab-ci.yml b/.gitlab/ci/package-and-test-nightly/main.gitlab-ci.yml
index bb0de4a79e2..019bfde9379 100644
--- a/.gitlab/ci/package-and-test-nightly/main.gitlab-ci.yml
+++ b/.gitlab/ci/package-and-test-nightly/main.gitlab-ci.yml
@@ -2,16 +2,6 @@ include:
- local: .gitlab/ci/qa-common/main.gitlab-ci.yml
- local: .gitlab/ci/qa-common/rules.gitlab-ci.yml
- local: .gitlab/ci/qa-common/variables.gitlab-ci.yml
- - component: "gitlab.com/gitlab-org/quality/pipeline-common/allure-report@7.3.0"
- inputs:
- job_name: "e2e-test-report"
- job_stage: "report"
- aws_access_key_id_variable_name: "QA_ALLURE_AWS_ACCESS_KEY_ID"
- aws_secret_access_key_variable_name: "QA_ALLURE_AWS_SECRET_ACCESS_KEY"
- gitlab_auth_token_variable_name: "PROJECT_TOKEN_FOR_CI_SCRIPTS_API_USAGE"
- allure_results_glob: "gitlab-qa-run-*/**/allure-results"
- allure_job_name: "${QA_RUN_TYPE}"
- allure_ref_slug: "${CI_COMMIT_REF_SLUG}"
workflow:
rules:
@@ -123,6 +113,8 @@ relative-url:
# ==========================================
e2e-test-report:
extends: .rules:report:allure-report
+ variables:
+ ALLURE_RESULTS_GLOB: "gitlab-qa-run-*/**/allure-results"
upload-knapsack-report:
extends:
diff --git a/.gitlab/ci/package-and-test/main.gitlab-ci.yml b/.gitlab/ci/package-and-test/main.gitlab-ci.yml
index c616fe3de82..21dd8f957d4 100644
--- a/.gitlab/ci/package-and-test/main.gitlab-ci.yml
+++ b/.gitlab/ci/package-and-test/main.gitlab-ci.yml
@@ -4,16 +4,6 @@ include:
- local: .gitlab/ci/qa-common/main.gitlab-ci.yml
- local: .gitlab/ci/qa-common/rules.gitlab-ci.yml
- local: .gitlab/ci/qa-common/variables.gitlab-ci.yml
- - component: "gitlab.com/gitlab-org/quality/pipeline-common/allure-report@7.3.1"
- inputs:
- job_name: "e2e-test-report"
- job_stage: "report"
- aws_access_key_id_variable_name: "QA_ALLURE_AWS_ACCESS_KEY_ID"
- aws_secret_access_key_variable_name: "QA_ALLURE_AWS_SECRET_ACCESS_KEY"
- gitlab_auth_token_variable_name: "PROJECT_TOKEN_FOR_CI_SCRIPTS_API_USAGE"
- allure_results_glob: "gitlab-qa-run-*/**/allure-results"
- allure_job_name: "${QA_RUN_TYPE}"
- allure_ref_slug: "${CI_COMMIT_REF_SLUG}"
# ==========================================
# Prepare stage
@@ -417,9 +407,7 @@ integrations:
- !reference [.rules:test:manual, rules]
ldap-no-server:
- extends:
- - .qa
- - .failure-videos
+ extends: .qa
variables:
QA_SCENARIO: Test::Integration::LDAPNoServer
rules:
@@ -428,9 +416,7 @@ ldap-no-server:
- !reference [.rules:test:manual, rules]
ldap-tls:
- extends:
- - .qa
- - .failure-videos
+ extends: .qa
variables:
QA_SCENARIO: Test::Integration::LDAPTLS
rules:
@@ -439,9 +425,7 @@ ldap-tls:
- !reference [.rules:test:manual, rules]
ldap-no-tls:
- extends:
- - .qa
- - .failure-videos
+ extends: .qa
variables:
QA_SCENARIO: Test::Integration::LDAPNoTLS
rules:
@@ -643,6 +627,8 @@ update-ee-to-ce:
# ==========================================
e2e-test-report:
extends: .rules:report:allure-report
+ variables:
+ ALLURE_RESULTS_GLOB: "gitlab-qa-run-*/**/allure-results"
upload-knapsack-report:
extends:
diff --git a/.gitlab/ci/qa-common/main.gitlab-ci.yml b/.gitlab/ci/qa-common/main.gitlab-ci.yml
index bdb5e776808..94236730f6a 100644
--- a/.gitlab/ci/qa-common/main.gitlab-ci.yml
+++ b/.gitlab/ci/qa-common/main.gitlab-ci.yml
@@ -5,8 +5,16 @@ workflow:
name: $PIPELINE_NAME
include:
+ - component: "gitlab.com/gitlab-org/quality/pipeline-common/allure-report@7.10.0"
+ inputs:
+ job_name: "e2e-test-report"
+ job_stage: "report"
+ aws_access_key_id_variable_name: "QA_ALLURE_AWS_ACCESS_KEY_ID"
+ aws_secret_access_key_variable_name: "QA_ALLURE_AWS_SECRET_ACCESS_KEY"
+ gitlab_auth_token_variable_name: "PROJECT_TOKEN_FOR_CI_SCRIPTS_API_USAGE"
+ allure_job_name: "${QA_RUN_TYPE}"
- project: gitlab-org/quality/pipeline-common
- ref: 7.5.1
+ ref: 7.10.2
file:
- /ci/base.gitlab-ci.yml
- /ci/knapsack-report.yml
diff --git a/.gitlab/ci/qa-common/rules.gitlab-ci.yml b/.gitlab/ci/qa-common/rules.gitlab-ci.yml
index c593ec4ccfb..7fa66fa4384 100644
--- a/.gitlab/ci/qa-common/rules.gitlab-ci.yml
+++ b/.gitlab/ci/qa-common/rules.gitlab-ci.yml
@@ -14,6 +14,10 @@
.spec-file-specified: &spec-file-specified
if: $QA_TESTS =~ /_spec\.rb/
+# code pattern changes
+.code-pattern-changes: &code-pattern-changes
+ if: $MR_CODE_PATTERNS == "true"
+
# Specs directory specified
.spec-directory-specified: &spec-directory-specified
if: $QA_TESTS != "" && $QA_TESTS !~ /_spec\.rb/
diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml
index ef2056f164c..385b0e8b68b 100644
--- a/.gitlab/ci/rails.gitlab-ci.yml
+++ b/.gitlab/ci/rails.gitlab-ci.yml
@@ -61,7 +61,7 @@ update-ruby-gems-coverage-cache-push:
- .ruby-gems-coverage-cache-push
- .shared:rules:update-cache
variables:
- BUNDLE_WITHOUT: "" # This is to override the variable defined in .gitlab-ci.yml
+ BUNDLE_WITHOUT: "" # This is to override the variable defined in .gitlab-ci.yml
BUNDLE_ONLY: "coverage"
script:
- source scripts/utils.sh
@@ -75,7 +75,7 @@ update-ruby-gems-coverage-cache-push:
- .default-retry
- .ruby-gems-coverage-cache
variables:
- BUNDLE_WITHOUT: "" # This is to override the variable defined in .gitlab-ci.yml
+ BUNDLE_WITHOUT: "" # This is to override the variable defined in .gitlab-ci.yml
BUNDLE_ONLY: "coverage"
before_script:
- source scripts/utils.sh
@@ -276,16 +276,6 @@ rspec system pg14 praefect:
- .rspec-system-parallel
- .rails:rules:praefect-with-db
-# Dedicated job to test DB library code against PG12.
-# Note that these are already tested against PG12 in the `rspec unit pg12` / `rspec-ee unit pg12` jobs.
-rspec db-library-code pg12:
- extends:
- - .rspec-base-pg12
- - .rails:rules:ee-and-foss-db-library-code
- script:
- - !reference [.base-script, script]
- - rspec_db_library_code
-
# Dedicated job to test DB library code against PG13.
# Note that these are already tested against PG13 in the `rspec unit pg13` / `rspec-ee unit pg13` jobs.
rspec db-library-code pg13:
@@ -358,8 +348,8 @@ rspec:artifact-collector unit:
- .artifact-collector
- .rails:rules:ee-and-foss-unit
needs:
- - rspec unit pg14 # 24 jobs
- - job: rspec unit clickhouse # 1 job
+ - rspec unit pg14 # 24 jobs
+ - job: rspec unit clickhouse # 1 job
optional: true
rspec:artifact-collector system:
@@ -367,17 +357,17 @@ rspec:artifact-collector system:
- .artifact-collector
- .rails:rules:ee-and-foss-system
needs:
- - rspec system pg14 # 26 jobs
+ - rspec system pg14 # 26 jobs
rspec:artifact-collector remainder:
extends:
- .artifact-collector
needs:
- - job: rspec integration pg14 # 13 jobs
+ - job: rspec integration pg14 # 13 jobs
optional: true
- - job: rspec migration pg14 # 12 jobs
+ - job: rspec migration pg14 # 12 jobs
optional: true
- - job: rspec background_migration pg14 # 4 jobs
+ - job: rspec background_migration pg14 # 4 jobs
optional: true
rules:
- !reference ['.rails:rules:ee-and-foss-integration', rules]
@@ -389,7 +379,7 @@ rspec:artifact-collector as-if-foss unit:
- .artifact-collector
- .rails:rules:as-if-foss-unit
needs:
- - rspec unit pg14-as-if-foss # 28 jobs
+ - rspec unit pg14-as-if-foss # 28 jobs
rspec:artifact-collector as-if-foss system:
extends:
@@ -402,11 +392,11 @@ rspec:artifact-collector as-if-foss remainder:
extends:
- .artifact-collector
needs:
- - job: rspec integration pg14-as-if-foss # 12 jobs
+ - job: rspec integration pg14-as-if-foss # 12 jobs
optional: true
- - job: rspec migration pg14-as-if-foss # 8 jobs
+ - job: rspec migration pg14-as-if-foss # 8 jobs
optional: true
- - job: rspec background_migration pg14-as-if-foss # 4 jobs
+ - job: rspec background_migration pg14-as-if-foss # 4 jobs
optional: true
rules:
- !reference ['.rails:rules:as-if-foss-integration', rules]
@@ -418,43 +408,43 @@ rspec:artifact-collector single-redis:
- .artifact-collector
- .rails:rules:single-redis
needs:
- - rspec unit pg14 single-redis # 28 jobs
- - rspec integration pg14 single-redis # 12 jobs
+ - rspec unit pg14 single-redis # 28 jobs
+ - rspec integration pg14 single-redis # 12 jobs
rspec:artifact-collector system single-redis:
extends:
- .artifact-collector
- .rails:rules:single-redis
needs:
- - rspec system pg14 single-redis # 28 jobs
+ - rspec system pg14 single-redis # 28 jobs
rspec:artifact-collector ee single-redis:
extends:
- .artifact-collector
- .rails:rules:single-redis
needs:
- - job: rspec-ee unit pg14 single-redis # 18 jobs
+ - job: rspec-ee unit pg14 single-redis # 18 jobs
optional: true
- - job: rspec-ee integration pg14 single-redis # 6 jobs
+ - job: rspec-ee integration pg14 single-redis # 6 jobs
optional: true
- - job: rspec-ee system pg14 single-redis # 10 jobs
+ - job: rspec-ee system pg14 single-redis # 10 jobs
optional: true
rspec:artifact-collector ee:
extends:
- .artifact-collector
needs:
- - job: rspec-ee migration pg14 # 2 jobs
+ - job: rspec-ee migration pg14 # 2 jobs
optional: true
- - job: rspec-ee background_migration pg14 # 2 jobs
+ - job: rspec-ee background_migration pg14 # 2 jobs
optional: true
- - job: rspec-ee unit pg14 # 22 jobs
+ - job: rspec-ee unit pg14 # 22 jobs
optional: true
- - job: rspec-ee unit clickhouse # 1 job
+ - job: rspec-ee unit clickhouse # 1 job
optional: true
- - job: rspec-ee integration pg14 # 5 jobs
+ - job: rspec-ee integration pg14 # 5 jobs
optional: true
- - job: rspec-ee system pg14 # 12 jobs
+ - job: rspec-ee system pg14 # 12 jobs
optional: true
rules:
- !reference ['.rails:rules:ee-only-migration', rules]
@@ -573,11 +563,8 @@ rspec:merge-auto-explain-logs:
- .rails:rules:rspec-merge-auto-explain-logs
stage: post-test
needs: !reference ["rspec:coverage", "needs"]
- before_script:
- - source scripts/utils.sh
- - source scripts/rspec_helpers.sh
script:
- - merge_auto_explain_logs
+ - scripts/merge-auto-explain-logs
artifacts:
name: auto-explain-logs
expire_in: 31d
@@ -764,16 +751,41 @@ rspec system pg14-as-if-foss clusterwide-db:
- .clusterwide-db
- .rails:rules:clusterwide-db
-rspec-ee unit gitlab_duo_chat pg14:
+rspec-ee unit gitlab-duo-chat pg14:
variables:
REAL_AI_REQUEST: "true"
- OPENAI_EMBEDDINGS: "true"
+ RSPEC_RETRY_RETRY_COUNT: 0
extends:
- .rspec-ee-base-pg14
- - .rails:rules:ee-gitlab-duo-chat
+ - .rails:rules:ee-gitlab-duo-chat-base
+ parallel:
+ matrix:
+ - DUO_RSPEC: ["lib/gitlab/llm/chain/agents/zero_shot/executor_real_requests_spec.rb", "support_specs/helpers/chat_qa_evaluation_helpers_spec.rb"]
script:
- !reference [.base-script, script]
- - rspec_paralellized_job "--fail-fast=${RSPEC_FAIL_FAST_THRESHOLD} --tag real_ai_request"
+ - bundle exec rspec -Ispec -rspec_helper --failure-exit-code 0 --tag real_ai_request --color -- ee/spec/${DUO_RSPEC}
+
+rspec-ee unit gitlab-duo-chat-qa pg14:
+ variables:
+ REAL_AI_REQUEST: "true"
+ RSPEC_RETRY_RETRY_COUNT: 0
+ extends:
+ - .rspec-ee-base-pg14
+ - .rails:rules:ee-gitlab-duo-chat-base
+ parallel:
+ matrix:
+ - DUO_RSPEC: ["qa_epic_spec.rb", "qa_issue_spec.rb"]
+ script:
+ - !reference [.base-script, script]
+ - source ./scripts/utils.sh
+ - install_gitlab_gem
+ - bundle exec rspec -Ispec -rspec_helper --failure-exit-code 0 --tag real_ai_request --color -- ee/spec/lib/gitlab/llm/chain/agents/zero_shot/${DUO_RSPEC}
+ - ./scripts/duo_chat/reporter.rb
+ artifacts:
+ expire_in: 5d
+ paths:
+ - tmp/duo_chat/qa*.json
+ - "${DUO_RSPEC}.md"
rspec-ee migration pg14:
extends:
@@ -961,39 +973,6 @@ rspec-ee system pg14 clusterwide-db:
##########################################
# EE/FOSS: default branch nightly scheduled jobs #
-# PG12
-rspec migration pg12:
- extends:
- - .rspec-base-pg12
- - .rspec-base-migration
- - .rails:rules:rspec-on-pg12
- - .rspec-migration-parallel
-
-rspec background_migration pg12:
- extends:
- - .rspec-base-pg12
- - .rspec-base-migration
- - .rails:rules:rspec-on-pg12
- - .rspec-background-migration-parallel
-
-rspec unit pg12:
- extends:
- - .rspec-base-pg12
- - .rails:rules:rspec-on-pg12
- - .rspec-unit-parallel
-
-rspec integration pg12:
- extends:
- - .rspec-base-pg12
- - .rails:rules:rspec-on-pg12
- - .rspec-integration-parallel
-
-rspec system pg12:
- extends:
- - .rspec-base-pg12
- - .rails:rules:rspec-on-pg12
- - .rspec-system-parallel
-
# PG13
rspec migration pg13:
extends:
@@ -1065,39 +1044,6 @@ rspec system pg15:
#####################################
# EE: default branch nightly scheduled jobs #
-# PG12
-rspec-ee migration pg12:
- extends:
- - .rspec-ee-base-pg12
- - .rspec-base-migration
- - .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
- - .rspec-ee-migration-parallel
-
-rspec-ee background_migration pg12:
- extends:
- - .rspec-ee-base-pg12
- - .rspec-base-migration
- - .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
- - .rspec-ee-background-migration-parallel
-
-rspec-ee unit pg12:
- extends:
- - .rspec-ee-base-pg12
- - .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
- - .rspec-ee-unit-parallel
-
-rspec-ee integration pg12:
- extends:
- - .rspec-ee-base-pg12
- - .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
- - .rspec-ee-integration-parallel
-
-rspec-ee system pg12:
- extends:
- - .rspec-ee-base-pg12
- - .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
- - .rspec-ee-system-parallel
-
# PG13
rspec-ee unit pg13 opensearch1:
extends:
diff --git a/.gitlab/ci/rails/shared.gitlab-ci.yml b/.gitlab/ci/rails/shared.gitlab-ci.yml
index e9041e197cc..6046c672f7b 100644
--- a/.gitlab/ci/rails/shared.gitlab-ci.yml
+++ b/.gitlab/ci/rails/shared.gitlab-ci.yml
@@ -84,10 +84,10 @@ include:
- echo -e "\e[0Ksection_start:`date +%s`:report_results_section[collapsed=true]\r\e[0KReport results"
- |
if [ "$CREATE_RAILS_TEST_FAILURE_ISSUES" == "true" ]; then
- bundle exec relate-failure-issue --input-files "rspec/rspec-*.json" --system-log-files "log" --project "gitlab-org/gitlab" --token "${TEST_FAILURES_PROJECT_TOKEN}";
+ bundle exec relate-failure-issue --input-files "rspec/rspec-*.json" --system-log-files "log" --project "gitlab-org/gitlab" --token "${TEST_FAILURES_PROJECT_TOKEN}" --related-issues-file "rspec/${CI_JOB_ID}-failed-test-issues.json";
fi
if [ "$CREATE_RAILS_SLOW_TEST_ISSUES" == "true" ]; then
- bundle exec slow-test-issues --input-files "rspec/rspec-*.json" --project "gitlab-org/gitlab" --token "${TEST_FAILURES_PROJECT_TOKEN}";
+ bundle exec slow-test-issues --input-files "rspec/rspec-*.json" --project "gitlab-org/gitlab" --token "${TEST_FAILURES_PROJECT_TOKEN}" --related-issues-file "rspec/${CI_JOB_ID}-slow-test-issues.json";
fi
if [ "$ADD_SLOW_TEST_NOTE_TO_MERGE_REQUEST" == "true" ]; then
bundle exec slow-test-merge-request-report-note --input-files "rspec/rspec-*.json" --project "gitlab-org/gitlab" --merge_request_iid "$CI_MERGE_REQUEST_IID" --token "${TEST_SLOW_NOTE_PROJECT_TOKEN}";
@@ -121,11 +121,6 @@ include:
after_script:
- !reference [.rspec-base, after_script]
-.rspec-base-pg12:
- extends:
- - .rspec-base
- - .use-pg12
-
.rspec-base-pg13:
extends:
- .rspec-base
@@ -163,11 +158,6 @@ include:
- .rspec-base
- .use-pg15
-.rspec-ee-base-pg12:
- extends:
- - .rspec-base
- - .use-pg12-es7-ee
-
.rspec-ee-base-pg13:
extends:
- .rspec-base
diff --git a/.gitlab/ci/release-environments/main.gitlab-ci.yml b/.gitlab/ci/release-environments/main.gitlab-ci.yml
index ff15673d48d..9cda6d588fb 100644
--- a/.gitlab/ci/release-environments/main.gitlab-ci.yml
+++ b/.gitlab/ci/release-environments/main.gitlab-ci.yml
@@ -2,23 +2,21 @@
include:
- local: .gitlab/ci/cng/main.gitlab-ci.yml
-review-build-cng-env:
- extends:
- - .review-build-cng-env
+release-environments-build-cng-env:
+ extends: .build-cng-env
allow_failure: true
-review-build-cng:
- extends:
- - .review-build-cng
- needs: ["review-build-cng-env"]
+release-environments-build-cng:
+ extends: .build-cng
+ needs: ["release-environments-build-cng-env"]
variables:
IMAGE_TAG_EXT: "-${CI_COMMIT_SHORT_SHA}"
allow_failure: true
-review-deploy-env:
+release-environments-deploy-env:
allow_failure: true
stage: deploy
- needs: ["review-build-cng"]
+ needs: ["release-environments-build-cng"]
variables:
DEPLOY_ENV: deploy.env
script:
@@ -31,10 +29,10 @@ review-deploy-env:
expire_in: 7 days
when: always
-review-deploy:
+release-environments-deploy:
allow_failure: true
stage: deploy
- needs: ["review-deploy-env"]
+ needs: ["release-environments-deploy-env"]
inherit:
variables: false
variables:
diff --git a/.gitlab/ci/review-apps/main.gitlab-ci.yml b/.gitlab/ci/review-apps/main.gitlab-ci.yml
index 782d0261cc3..dfcd65238ec 100644
--- a/.gitlab/ci/review-apps/main.gitlab-ci.yml
+++ b/.gitlab/ci/review-apps/main.gitlab-ci.yml
@@ -6,7 +6,7 @@ stages:
- deploy
- post-deploy
- qa
- - post-qa
+ - report
- dast
include:
@@ -30,7 +30,7 @@ dont-interrupt-me:
review-build-cng-env:
extends:
- - .review-build-cng-env
+ - .build-cng-env
- .default-retry
- .review:rules:review-build-cng
- .fast-no-clone-job
@@ -45,14 +45,14 @@ review-build-cng-env:
scripts/trigger-build.rb
VERSION
before_script:
- - apk add --no-cache --update curl # Not present in ruby-alpine, so we add it manually
+ - apk add --no-cache --update curl # Not present in ruby-alpine, so we add it manually
- !reference [".fast-no-clone-job", before_script]
- - !reference [".review-build-cng-env", before_script]
- - mv VERSION GITLAB_WORKHORSE_VERSION # GITLAB_WORKHORSE_VERSION is a symlink to VERSION
+ - !reference [".build-cng-env", before_script]
+ - mv VERSION GITLAB_WORKHORSE_VERSION # GITLAB_WORKHORSE_VERSION is a symlink to VERSION
review-build-cng:
extends:
- - .review-build-cng
+ - .build-cng
- .review:rules:review-build-cng
needs: ["review-build-cng-env"]
@@ -67,7 +67,7 @@ review-build-cng:
GITLAB_IMAGE_REPOSITORY: "registry.gitlab.com/gitlab-org/build/cng-mirror"
GITLAB_IMAGE_SUFFIX: "ee"
GITLAB_REVIEW_APP_BASE_CONFIG_FILE: "scripts/review_apps/base-config.yaml"
- GITLAB_HELM_CHART_REF: "75b1486a9aec212d0f49ef1251526d8e51004bbc" # 7.0.1: https://gitlab.com/gitlab-org/charts/gitlab/-/commit/75b1486a9aec212d0f49ef1251526d8e51004bbc
+ GITLAB_HELM_CHART_REF: "db886740f66e8dfacd7b9f0f79f640c8c2e0318a" # 7.5.1: https://gitlab.com/gitlab-org/charts/gitlab/-/commit/db886740f66e8dfacd7b9f0f79f640c8c2e0318a
environment:
name: review/${CI_COMMIT_REF_SLUG}${SCHEDULE_TYPE} # No separator for SCHEDULE_TYPE so it's compatible as before and looks nice without it
url: https://gitlab-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}
@@ -98,9 +98,9 @@ review-deploy:
scripts/review_apps/seed-dast-test-data.sh
VERSION
before_script:
- - apk add --no-cache --update curl # Not present in ruby-alpine, so we add it manually
+ - apk add --no-cache --update curl # Not present in ruby-alpine, so we add it manually
- !reference [".fast-no-clone-job", before_script]
- - mv VERSION GITLAB_WORKHORSE_VERSION # GITLAB_WORKHORSE_VERSION is a symlink to VERSION
+ - mv VERSION GITLAB_WORKHORSE_VERSION # GITLAB_WORKHORSE_VERSION is a symlink to VERSION
- export GITLAB_SHELL_VERSION=$(<GITLAB_SHELL_VERSION)
- export GITALY_VERSION=$(<GITALY_SERVER_VERSION)
- export GITLAB_WORKHORSE_VERSION=$(<GITLAB_WORKHORSE_VERSION)
diff --git a/.gitlab/ci/review-apps/qa.gitlab-ci.yml b/.gitlab/ci/review-apps/qa.gitlab-ci.yml
index a9ab031e115..264763c882f 100644
--- a/.gitlab/ci/review-apps/qa.gitlab-ci.yml
+++ b/.gitlab/ci/review-apps/qa.gitlab-ci.yml
@@ -1,18 +1,6 @@
include:
- local: .gitlab/ci/qa-common/main.gitlab-ci.yml
- template: Verify/Browser-Performance.gitlab-ci.yml
- - component: "gitlab.com/gitlab-org/quality/pipeline-common/allure-report@7.3.0"
- inputs:
- job_name: "e2e-test-report"
- job_stage: "post-qa"
- aws_access_key_id_variable_name: "QA_ALLURE_AWS_ACCESS_KEY_ID"
- aws_secret_access_key_variable_name: "QA_ALLURE_AWS_SECRET_ACCESS_KEY"
- gitlab_auth_token_variable_name: "PROJECT_TOKEN_FOR_CI_SCRIPTS_API_USAGE"
- allure_results_glob: "qa/tmp/allure-results"
- allure_ref_slug: "${CI_COMMIT_REF_SLUG}"
- allure_project_path: "${CI_PROJECT_PATH}"
- allure_merge_request_iid: "${CI_MERGE_REQUEST_IID}"
- allure_job_name: "${QA_RUN_TYPE}"
.test-variables:
variables:
@@ -121,12 +109,15 @@ browser_performance:
e2e-test-report:
extends: .rules:prepare-report
+ stage: report
+ variables:
+ ALLURE_RESULTS_GLOB: "qa/tmp/allure-results"
upload-knapsack-report:
extends:
- .generate-knapsack-report-base
- .bundle-base
- stage: post-qa
+ stage: report
variables:
QA_KNAPSACK_REPORT_FILE_PATTERN: $CI_PROJECT_DIR/qa/tmp/knapsack/*/*.json
@@ -134,7 +125,7 @@ delete-test-resources:
extends:
- .bundle-base
- .rules:prepare-report
- stage: post-qa
+ stage: report
variables:
GITLAB_QA_ACCESS_TOKEN: $REVIEW_APPS_ROOT_TOKEN
script:
@@ -146,7 +137,7 @@ notify-slack:
extends:
- .notify-slack
- .rules:main-run
- stage: post-qa
+ stage: report
variables:
QA_RSPEC_XML_FILE_PATTERN: ${CI_PROJECT_DIR}/qa/tmp/rspec-*.xml
RUN_WITH_BUNDLE: "true"
@@ -157,7 +148,7 @@ export-test-metrics:
- .export-test-metrics
- .bundle-base
- .rules:main-run
- stage: post-qa
+ stage: report
variables:
QA_METRICS_REPORT_FILE_PATTERN: tmp/test-metrics-*.json
when: always
diff --git a/.gitlab/ci/review.gitlab-ci.yml b/.gitlab/ci/review.gitlab-ci.yml
index d4b199a9a81..1159bccb114 100644
--- a/.gitlab/ci/review.gitlab-ci.yml
+++ b/.gitlab/ci/review.gitlab-ci.yml
@@ -112,24 +112,3 @@ start-review-app-pipeline:
include:
- artifact: review-app-pipeline.yml
job: e2e-test-pipeline-generate
-
-include:
- - remote: 'https://gitlab.com/gitlab-org/quality/pipeline-common/-/raw/6.4.0/ci/danger-review.yml'
-
-danger-review:
- extends:
- - .default-retry
- - .ruby-node-cache
- - .review:rules:danger
- image: "${DEFAULT_CI_IMAGE}"
- before_script:
- - source scripts/utils.sh
- - bundle_install_script "--with danger"
- - yarn_install_script
-
-danger-review-local:
- extends: danger-review
- before_script:
- - !reference ["danger-review", "before_script"]
- # We unset DANGER_GITLAB_API_TOKEN so that Danger will run as local from `danger-review:script`
- - unset DANGER_GITLAB_API_TOKEN
diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml
index 97def7091c4..7b160a0efb5 100644
--- a/.gitlab/ci/rules.gitlab-ci.yml
+++ b/.gitlab/ci/rules.gitlab-ci.yml
@@ -45,10 +45,10 @@
if: '($CI_MERGE_REQUEST_EVENT_TYPE == "merged_result" || $CI_MERGE_REQUEST_EVENT_TYPE == "detached") && $CI_MERGE_REQUEST_LABELS =~ /pipeline:mr-approved/'
.if-merge-request-approved-and-specific-devops-stage: &if-merge-request-approved-and-specific-devops-stage
- if: '($CI_MERGE_REQUEST_EVENT_TYPE == "merged_result" || $CI_MERGE_REQUEST_EVENT_TYPE == "detached") && ($CI_MERGE_REQUEST_LABELS =~ /pipeline:mr-approved/ && $CI_MERGE_REQUEST_LABELS =~ /devops::create/)'
+ if: '($CI_MERGE_REQUEST_EVENT_TYPE == "merged_result" || $CI_MERGE_REQUEST_EVENT_TYPE == "detached") && ($CI_MERGE_REQUEST_LABELS =~ /pipeline:mr-approved/ && $CI_MERGE_REQUEST_LABELS =~ /devops::(create|govern|manage)/)'
.if-merge-request-and-specific-devops-stage: &if-merge-request-and-specific-devops-stage
- if: '($CI_MERGE_REQUEST_EVENT_TYPE == "merged_result" || $CI_MERGE_REQUEST_EVENT_TYPE == "detached") && $CI_MERGE_REQUEST_LABELS =~ /devops::create/'
+ if: '($CI_MERGE_REQUEST_EVENT_TYPE == "merged_result" || $CI_MERGE_REQUEST_EVENT_TYPE == "detached") && $CI_MERGE_REQUEST_LABELS =~ /devops::(create|govern|manage)/'
.if-merge-request-not-approved: &if-merge-request-not-approved
if: '($CI_MERGE_REQUEST_EVENT_TYPE == "merged_result" || $CI_MERGE_REQUEST_EVENT_TYPE == "detached") && $CI_MERGE_REQUEST_LABELS !~ /pipeline:mr-approved/'
@@ -59,9 +59,15 @@
.if-merge-request-targeting-stable-branch: &if-merge-request-targeting-stable-branch
if: '($CI_MERGE_REQUEST_EVENT_TYPE == "merged_result" || $CI_MERGE_REQUEST_EVENT_TYPE == "detached") && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME =~ /^[\d-]+-stable(-ee|-jh)?$/'
+.if-merge-request-labels-run-in-ruby3_0: &if-merge-request-labels-run-in-ruby3_0
+ if: '$CI_MERGE_REQUEST_LABELS =~ /pipeline:run-in-ruby3_0/'
+
.if-merge-request-labels-run-in-ruby3_1: &if-merge-request-labels-run-in-ruby3_1
if: '$CI_MERGE_REQUEST_LABELS =~ /pipeline:run-in-ruby3_1/'
+.if-merge-request-labels-run-in-ruby3_2: &if-merge-request-labels-run-in-ruby3_2
+ if: '$CI_MERGE_REQUEST_LABELS =~ /pipeline:run-in-ruby3_2/'
+
.if-merge-request-labels-as-if-foss: &if-merge-request-labels-as-if-foss
if: '$CI_MERGE_REQUEST_LABELS =~ /pipeline:run-as-if-foss/'
@@ -89,15 +95,9 @@
.if-merge-request-labels-run-review-app: &if-merge-request-labels-run-review-app
if: '$CI_MERGE_REQUEST_LABELS =~ /pipeline:run-review-app/'
-.if-merge-request-labels-run-on-pg12: &if-merge-request-labels-run-on-pg12
- if: '$CI_MERGE_REQUEST_LABELS =~ /pipeline:run-on-pg12/'
-
.if-merge-request-labels-skip-undercoverage: &if-merge-request-labels-skip-undercoverage
if: '$CI_MERGE_REQUEST_LABELS =~ /pipeline:skip-undercoverage/'
-.if-merge-request-labels-record-queries: &if-merge-request-labels-record-queries
- if: '$CI_MERGE_REQUEST_LABELS =~ /pipeline:record-queries/'
-
.if-merge-request-labels-jh-contribution: &if-merge-request-labels-jh-contribution
if: '$CI_MERGE_REQUEST_LABELS =~ /JiHu contribution/'
@@ -371,6 +371,7 @@
# AI patterns:
.ai-patterns: &ai-patterns
- "{,ee/,jh/}lib/gitlab/llm/**/*"
+ - "{,ee/,jh/}{,spec/}lib/gitlab/llm/**/*"
# DB patterns + .ci-patterns
.db-patterns: &db-patterns
@@ -930,7 +931,7 @@
variables:
BUILD_GDK_BASE: "true"
- !reference [".qa:rules:package-and-test-never-run", rules]
- - <<: *if-default-branch-schedule-nightly # already executed in the 2-hourly schedule
+ - <<: *if-default-branch-schedule-nightly # already executed in the 2-hourly schedule
when: never
- <<: *if-default-branch-refs
- <<: *if-merge-request
@@ -1588,9 +1589,9 @@
- <<: *if-merge-request-approved-and-specific-devops-stage
changes: *code-patterns
allow_failure: true
- # We used to have a rule at the end here that would catch any remaining code MRs and allow the job to be run
- # manually. That rule is now in ".qa:rules:code-merge-request-manual" so it can be included when needed and we can
- # still use ".qa:rules:package-and-test-common" in jobs we don't want to be manual.
+ # We used to have a rule at the end here that would catch any remaining code MRs and allow the job to be run
+ # manually. That rule is now in ".qa:rules:code-merge-request-manual" so it can be included when needed and we can
+ # still use ".qa:rules:package-and-test-common" in jobs we don't want to be manual.
# Like .qa:rules:package-and-test-common but not allowed to fail.
# It's named `e2e` instead of `package-and-test` because it's used for e2e tests on GDK (and could be used
@@ -1623,7 +1624,11 @@
- <<: *if-dot-com-gitlab-org-and-security-merge-request-and-qa-tests-specified
changes: *code-patterns
- <<: *if-merge-request
- changes: *code-qa-patterns # Includes all CI changes
+ changes: *code-patterns
+ variables:
+ MR_CODE_PATTERNS: "true"
+ - <<: *if-merge-request
+ changes: *code-qa-patterns # Includes all CI changes
- <<: *if-force-ci
when: manual
@@ -1679,7 +1684,7 @@
rules:
- if: '$QA_RUN_TESTS_ON_GDK !~ /true|yes|1/i'
when: never
- - <<: *if-default-branch-schedule-nightly # already executed in the 2-hourly schedule
+ - <<: *if-default-branch-schedule-nightly # already executed in the 2-hourly schedule
when: never
- !reference [".qa:rules:e2e-blocking", rules]
- !reference [".qa:rules:e2e-schedule-blocking", rules]
@@ -2109,14 +2114,16 @@
- <<: *if-default-refs
changes: *code-backstage-patterns
-.rails:rules:ee-gitlab-duo-chat:
+.rails:rules:ee-gitlab-duo-chat-base:
rules:
- !reference [".strict-ee-only-rules", rules]
- if: '$REAL_AI_REQUEST == null'
when: never
- if: '$ANTHROPIC_API_KEY == null'
when: never
- - if: '$OPENAI_EMBEDDINGS == null'
+ - if: '$VERTEX_AI_PROJECT == null'
+ when: never
+ - if: '$VERTEX_AI_CREDENTIALS == null'
when: never
- <<: *if-merge-request
changes: *ai-patterns
@@ -2188,7 +2195,6 @@
- <<: *if-default-refs
changes: *db-library-patterns
- <<: *if-merge-request-labels-run-all-rspec
- - <<: *if-merge-request-labels-run-on-pg12
.rails:rules:ee-mr-and-default-branch-only:
rules:
@@ -2278,11 +2284,6 @@
- <<: *if-merge-request
changes: *backend-patterns
-.rails:rules:rspec-on-pg12:
- rules:
- - <<: *if-merge-request-labels-run-on-pg12
- - !reference [".rails:rules:default-branch-schedule-nightly--code-backstage-default-rules", rules]
-
.rails:rules:rspec-merge-auto-explain-logs:
rules:
- <<: *if-not-ee
@@ -2290,7 +2291,8 @@
- <<: *if-merge-request-labels-pipeline-expedite
when: never
- <<: *if-merge-request-labels-run-all-rspec
- - <<: *if-merge-request-labels-record-queries
+ - <<: *if-merge-request
+ changes: *code-backstage-patterns
- <<: *if-default-branch-refs
changes: *code-patterns
@@ -2323,13 +2325,10 @@
rules:
- <<: *if-not-ee
when: never
- - <<: *if-merge-request-labels-pipeline-expedite
+ - <<: *if-merge-request
when: never
- if: '$FAST_QUARANTINE == "false" && $RETRY_FAILED_TESTS_IN_NEW_PROCESS != "true"'
when: never
- - <<: *if-merge-request
- changes: *code-backstage-patterns
- when: always
- <<: *if-default-branch-refs
changes: *code-backstage-patterns
when: always
@@ -2740,9 +2739,9 @@
- <<: *if-default-refs
changes: *code-backstage-patterns
-.setup:rules:verify-ruby-3.0:
+.setup:rules:verify-default-ruby:
rules:
- - <<: *if-merge-request-labels-run-in-ruby3_1
+ - <<: *if-merge-request-labels-run-in-ruby3_2
.setup:rules:verify-tests-yml:
rules:
diff --git a/.gitlab/ci/setup.gitlab-ci.yml b/.gitlab/ci/setup.gitlab-ci.yml
index b652ac5e30b..cc5e9bd2985 100644
--- a/.gitlab/ci/setup.gitlab-ci.yml
+++ b/.gitlab/ci/setup.gitlab-ci.yml
@@ -51,13 +51,14 @@ gitlab_git_test:
script:
- spec/support/prepare-gitlab-git-test-for-commit --check-for-changes
-verify-ruby-3.0:
+verify-default-ruby:
extends:
- .absolutely-predictive-job
- - .setup:rules:verify-ruby-3.0
+ - .setup:rules:verify-default-ruby
stage: prepare
script:
- - echo 'Please remove label ~"pipeline:run-in-ruby3_1" so we do test against Ruby 3.0 (default version) before merging the merge request'
+ - echo 'Please remove label ~"pipeline:run-in-ruby3_2" so we do test against default Ruby version before merging the merge request'
+ - echo 'This does not work yet. See https://gitlab.com/gitlab-org/gitlab/-/issues/428537'
- exit 1
verify-tests-yml:
diff --git a/.gitlab/ci/templates/gem.gitlab-ci.yml b/.gitlab/ci/templates/gem.gitlab-ci.yml
index f17e168c1af..449150bde6c 100644
--- a/.gitlab/ci/templates/gem.gitlab-ci.yml
+++ b/.gitlab/ci/templates/gem.gitlab-ci.yml
@@ -18,8 +18,10 @@ spec:
- ".gitlab/ci/gitlab-gems.gitlab-ci.yml"
- ".gitlab/ci/vendored-gems.gitlab-ci.yml"
- ".gitlab/ci/templates/gem.gitlab-ci.yml"
+ # Ensure dependency updates don't fail child pipelines: https://gitlab.com/gitlab-org/gitlab/-/issues/417428
+ - "Gemfile.lock"
- "gems/gem.gitlab-ci.yml"
- # Ensure new cop in the monolith don't break internal gems Rubocop checks: https://gitlab.com/gitlab-org/gitlab/-/issues/419915
+ # Ensure new cop in the monolith don't break internal gems Rubocop checks: https://gitlab.com/gitlab-org/gitlab/-/issues/419915
- ".rubocop.yml"
- "rubocop/**/*"
- ".rubocop_todo/**/*"
diff --git a/.gitlab/ci/test-on-gdk/main.gitlab-ci.yml b/.gitlab/ci/test-on-gdk/main.gitlab-ci.yml
index a64dd450c82..f0a5ea5090f 100644
--- a/.gitlab/ci/test-on-gdk/main.gitlab-ci.yml
+++ b/.gitlab/ci/test-on-gdk/main.gitlab-ci.yml
@@ -16,12 +16,43 @@ include:
allure_merge_request_iid: "${CI_MERGE_REQUEST_IID}"
allure_job_name: "${QA_RUN_TYPE}"
+# code pattern changes
+.code-pattern-changes: &code-pattern-changes
+ if: $MR_CODE_PATTERNS == "true"
+
+# Run all tests when QA framework changes present, full suite execution is explicitly enabled or a feature flag file is removed
+.qa-run-all-tests: &qa-run-all-tests
+ if: $QA_FRAMEWORK_CHANGES == "true" || $QA_RUN_ALL_TESTS == "true" || $QA_RUN_ALL_E2E_LABEL == "true" || $QA_FEATURE_FLAGS =~ /deleted/
+
variables:
COLORIZED_LOGS: "true"
GIT_DEPTH: "20"
- GIT_STRATEGY: "clone" # 'GIT_STRATEGY: clone' optimizes the pack-objects cache hit ratio
+ GIT_STRATEGY: "clone" # 'GIT_STRATEGY: clone' optimizes the pack-objects cache hit ratio
GIT_SUBMODULE_STRATEGY: "none"
+.rules:gdk:qa-selective:
+ rules:
+ - <<: *code-pattern-changes
+ when: never
+ - !reference [.rules:test:qa-selective, rules]
+ - if: $QA_SUITES =~ /Test::Instance::Blocking/
+
+.rules:gdk:qa-parallel:
+ rules:
+ - *code-pattern-changes
+ - !reference [.rules:test:qa-parallel, rules]
+ - if: $QA_SUITES =~ /Test::Instance::Blocking/
+
+.rules:gdk:qa-smoke:
+ rules:
+ - <<: *code-pattern-changes
+ variables:
+ QA_TESTS: ""
+ - <<: *qa-run-all-tests
+ variables:
+ QA_TESTS: ""
+ - if: $QA_SUITES =~ /Test::Instance::Smoke/
+
.gdk-qa-base:
image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/debian-${DEBIAN_VERSION}-ruby-${RUBY_VERSION}:bundler-2.3-git-2.36-lfs-2.9-chrome-${CHROME_VERSION}-docker-${DOCKER_VERSION}-gcloud-383-kubectl-1.23
extends:
@@ -66,7 +97,7 @@ variables:
- echo -e "\e[0Ksection_end:`date +%s`:launch_gdk\r\e[0K"
- echo -e "\e[0Ksection_start:`date +%s`:install_gems[collapsed=true]\r\e[0KInstall gems"
- source scripts/utils.sh
- - cd qa && bundle install
+ - cd qa && bundle config set --local without 'development' && bundle install
- echo -e "\e[0Ksection_end:`date +%s`:install_gems\r\e[0K"
script:
- echo -e "\e[0Ksection_start:`date +%s`:healthcheck[collapsed=true]\r\e[0KWait for gdk to start"
@@ -127,11 +158,10 @@ gdk-qa-smoke:
extends:
- .gdk-qa-base
- .gitlab-qa-report
+ - .rules:gdk:qa-smoke
variables:
QA_SCENARIO: Test::Instance::Smoke
QA_RUN_TYPE: gdk-qa-smoke
- rules:
- - when: always
gdk-qa-smoke-with-load-balancer:
extends:
@@ -155,12 +185,20 @@ gdk-qa-reliable:
- .gdk-qa-base
- .gitlab-qa-report
- .parallel
+ - .rules:gdk:qa-parallel
variables:
QA_SCENARIO: Test::Instance::Blocking
QA_RUN_TYPE: gdk-qa-blocking
parallel: 10
- rules:
- - when: always
+
+gdk-qa-reliable-selective:
+ extends:
+ - .gdk-qa-base
+ - .gitlab-qa-report
+ - .rules:gdk:qa-selective
+ variables:
+ QA_SCENARIO: Test::Instance::Blocking
+ QA_RUN_TYPE: gdk-qa-blocking
gdk-qa-reliable-with-load-balancer:
extends:
diff --git a/.gitlab/issue_templates/AI Project Proposal.md b/.gitlab/issue_templates/AI Project Proposal.md
index 9ec22c18b4a..412d8d138fe 100644
--- a/.gitlab/issue_templates/AI Project Proposal.md
+++ b/.gitlab/issue_templates/AI Project Proposal.md
@@ -1,8 +1,8 @@
<!--
HOW TO USE THIS TEMPLATE
-To propose an AI experiment, focus on completing the “Experiment” section first. As you refine the idea and gather feedback on your experiment, use the “Feature release” section to define how it will evolve as a Beta or GA capability. It's important that we link experiment to feature release. Feel free to add sections, but keep the existing ones.
+To propose an AI experiment, focus on completing the “Experiment” section first. As you refine the idea and gather feedback on your experiment, progress to the Beta section to define how it will evolve, when ready, progress to the “Generally Available release” section to define how it will evolve GA capability. It's important that we link Experiment to Beta to GA release. Feel free to add sections, but the existing ones must be kept and completed.
-You can choose how to get started with this template. For example, the proposal can start as an issue, and then be promoted to an epic to house all the work related to the experiment/prototype and feature release. If you prefer to start with an epic, you have to manually apply the proposal template. Regardless, if the experiment is eventually prioritized for development, the template content will need to appear in a top-level epic so it can be tracked alongside other prioritized AI experiments.
+You can choose how to get started with this template. For example, the proposal can start as an issue, and then be promoted to an epic to house all the work related to the Experiment, Beta, and GA release. If you prefer to start with an epic, you have to manually apply the proposal template. Regardless, if the experiment is eventually prioritized for development, the template content will need to appear in a top-level epic so it can be tracked alongside other prioritized AI experiments.
TITLE FORMAT
🤖 [AI Proposal] {Need/outcome} {Beneficiary} {Job/Small Job}
@@ -10,14 +10,10 @@ TITLE FORMAT
The title should be something that is easily understood that quickly communicates the intent of the project allowing team members to easily understand and recognize the expected work that will be done. A proposal title should combine the beneficiary of the feature/UI, the job it will allow them to accomplish (see https://about.gitlab.com/handbook/product/ux/jobs-to-be-done/#how-to-write-a-jtbd), and their expected outcome when the work is delivered. Well-defined statements are concise without sacrificing the substance of the proposal so that anyone can understand it at a glance. (e.g. {Reduce the effort} {for security teams} {when prioritizing business-critical risks in their assets}).
-->
-# Experiment
-
-This section should be completed prior to work on the Experiment beginning.
-
-# [Experiment](https://docs.gitlab.com/ee/policy/experiment-beta-support.html#experiment)
-
-## Problem to be solved
+# [Experiment](https://docs.gitlab.com/ee/policy/alpha-beta-support.html#experiment)
+_This section should be completed prior to beginning work on the Experiment._
+## Problem to be solved
### User problem
_What user problem will this solve?_
@@ -36,33 +32,76 @@ _What [personas](https://about.gitlab.com/handbook/product/personas/#list-of-use
### Success
_How will you measure whether this experiment is a success?_
+**UX maturity requirements** _[Experiment to Beta](https://about.gitlab.com/handbook/product/ai/ux-maturity/#criteria-and-requirements)_
+| Criteria | Minimum Requirement | Assessment for Beta |
+| -------- | ------------------- | ------------------- |
+| [Problem validation](https://about.gitlab.com/handbook/product/ai/ux-maturity/#validation-problem-validation)<br>How well do we understand the problem? | [Mix of evidence and assumptions](https://about.gitlab.com/handbook/product/ai/ux-maturity/#questions-to-ask) | <!-- Acceptable answers: Yes, Somewhat or Somewhat, Somewhat --> |
+| [Solution validation](https://about.gitlab.com/handbook/product/ai/ux-maturity/#validation-solution-validation)<br>How usable is the solution? | [Usability testing](https://about.gitlab.com/handbook/product/ux/ux-scorecards/#option-b-perform-a-formative-evaluation), Grade C | <!-- Acceptable: >80% and grade C --> |
+| [Improve](https://about.gitlab.com/handbook/product/ai/ux-maturity/#build-improve)<br>How successful is the solution? | Quality goals set by the team are reached. | <!-- Acceptable answers: :white_check_mark: Reached all quality goals for this phase. --> |
+| [Design standards](https://about.gitlab.com/handbook/product/ai/ux-maturity/#design-standards) adherence<br>How compliant is the solution with our design standards? | Should adhere to ([Pajamas](https://design.gitlab.com/), [checklist](https://docs.gitlab.com/ee/development/contributing/design.html#checklist)) | <!-- Acceptable: Mostly adheres to design standards --> |
-# Feature release
+# [Beta](https://docs.gitlab.com/ee/policy/alpha-beta-support.html#beta)
+_This section should be completed prior to beginning work on the Beta experience._
<!-- DO NOT REMOVE THIS SECTION
-Although the initial focus is on the “Experiment” section, do not remove this “Feature release” section. It's important that we link experiment to feature release. Fill this section as you progress.
+Although the initial focus is on the “Experiment” section, do not remove this “Beta” section. It's important that we link Experiment to Beta release. Fill this section in as you progress.
-->
-### Main Job story
+
+### [Main Job story](https://about.gitlab.com/handbook/product/ux/jobs-to-be-done/#how-to-write-a-jtbd)
_What job to be done will this solve?_
<!-- What is the [Main Job story](https://about.gitlab.com/handbook/product/ux/jobs-to-be-done/#how-to-write-a-jtbd) that this proposal was derived from? (e.g. When I am on triage rotation, I want to address all the business-critical risks in my assets, So I can minimize the likelihood of my organization being compromised by a security breach.) -->
-## Proposal updates/additions
-<!-- Explain any changes or updates to the original proposal from the experiment, including details around usage, business drivers, and reasonings that drove the updates/additions. -->
+##### [Small Jobs](https://about.gitlab.com/handbook/product/ux/jobs-to-be-done/#small-jobs)
+_What are the small jobs this feature is solving for?_
+
+### Assumption
+_What assumptions are you making about this problem and the solution?_
+
+### Proposal updates/additions
+<!-- Explain any changes or updates to the original proposal from the Experiment, including details around usage, business drivers, and reasonings that drove the updates/additions. -->
### Problem validation
_What validation exists that customers have this problem?_
-<!-- Refer to https://about.gitlab.com/handbook/product/ux/ux-research/research-in-the-AI-space/#guideline-1-problem-validation --- to help identify and understand user needs -->
+<!-- Refer to https://about.gitlab.com/handbook/product/ux/ux-research/research-in-the-AI-space/#guideline-1-problem-validation---identify-and-understand-user-needs --- to help identify and understand user needs -->
### Business objective
_What business objective will be achieved with this proposal?_
<!-- Objectives (from a business point of view) that will be achieved upon completion. (For instance, Increase engagement by making the experience efficient while reducing the chances of users overlooking high-priority items. -->
-### Confidence
-_Has this proposal been derived from research?_
-<!-- How well do we understand the user's problem and their need? Refer to https://about.gitlab.com/handbook/product/ux/product-design/ux-roadmaps/#confidence to assess confidence -->
+### Requirements
+_What tasks or actions should the user be capable of performing with this feature?_
+<!-- Requirements can be taken from existing features or design issues used to build this proposal. Any related issues should be linked with this issue in the Feature/solution issues section below. They are more granular validated needs, goals, and additional details that the proposal encompasses. -->
+
+
+### The user needs to be able to:
+- ...
+- ...
+
+#### Success
+_How will you measure whether this Beta is a success?_
+<!-- Consider how successful the solution is by looking beyond feature usage as the success metric. Instead consider how useful, efficient, effective, satisfying, and learnable was the feature. The Product Development Flow recommends outcomes and potential activities to create a combined and ongoing quantitative and qualitative feedback loop to evaluate feature success. -->
+
+**UX maturity requirements** _[Beta to GA](https://about.gitlab.com/handbook/product/ai/ux-maturity/#criteria-and-requirements)_
+| Criteria | Minimum Requirement | Assessment for GA |
+| -------- | ------------------- | ------------------- |
+| [Problem validation](https://about.gitlab.com/handbook/product/ai/ux-maturity/#validation-problem-validation)<br>How well do we understand the problem? | [Mix of evidence and assumptions](https://about.gitlab.com/handbook/product/ai/ux-maturity/#questions-to-ask) | <!-- Acceptable answers: Yes, Yes --> |
+| [Solution validation](https://about.gitlab.com/handbook/product/ai/ux-maturity/#validation-solution-validation)<br>How usable is the solution? | [Usability testing](https://about.gitlab.com/handbook/product/ux/ux-scorecards/#option-b-perform-a-formative-evaluation) and [Heuristic evaluation](https://about.gitlab.com/handbook/product/ux/ux-scorecards/#option-a-conduct-a-heuristic-evaluation), Avg. task pass rate >80%, Grade B | <!-- Acceptable: >80% and grade B --> |
+| [Improve](https://about.gitlab.com/handbook/product/ai/ux-maturity/#build-improve)<br>How successful is the solution? | Quality goals set by the team are reached. | <!-- Acceptable answers: :white_check_mark: Reached all quality goals for this phase. --> |
+| [Design standards](https://about.gitlab.com/handbook/product/ai/ux-maturity/#design-standards) adherence<br>How compliant is the solution with our design standards? | Should adhere to ([Pajamas](https://design.gitlab.com/), [checklist](https://docs.gitlab.com/ee/development/contributing/design.html#checklist)) | <!-- Acceptable: Completely adheres to design standards --> |
-| Confidence | Research |
-| ----------------- | ------------------------------ |
-| [High/Medium/Low] | [research/insight issue](Link) |
+# [Generally Available](https://docs.gitlab.com/ee/policy/alpha-beta-support.html#generally-available-ga)
+<!-- DO NOT REMOVE THIS SECTION
+Although the initial focus is on the “Experiment” section, do not remove this “Generally Available” section. It's important that we link Beta to GA release. Fill this section in as you progress.
+-->
+
+### Assumption
+_What assumptions are you making about this problem and the solution?_
+
+### Proposal updates/additions
+<!-- Explain any changes or updates to the original proposal from the experiment, including details around usage, business drivers, and reasonings that drove the updates/additions. -->
+
+### Problem validation
+_What validation exists that customers have this problem?_
+<!-- Refer to https://about.gitlab.com/handbook/product/ux/ux-research/research-in-the-AI-space/#guideline-1-problem-validation --- to help identify and understand user needs -->
### Requirements
_What tasks or actions should the user be capable of performing with this feature?_
@@ -75,32 +114,57 @@ _What tasks or actions should the user be capable of performing with this featur
- ...
## Checklist
-
### Experiment
<details> <summary> Issue information </summary>
- [ ] Add information to the issue body about:
- [ ] The user problem being solved
- - [ ] Your assumptions
+ - [ ] Why the solution hypothesis solves this problem
+ - [ ] Your assumptions have been defined
- [ ] Who it's for, list of personas impacted
- - [ ] Your proposal
+ - [ ] Your proposal has been defined
+ - [ ] Your success metrics have been defined
+ - [ ] UX maturity requirements have been measured
- [ ] Add relevant designs to the Design Management area of the issue if available
- [ ] Confirm that an unexpected outage of this feature will not negatively impact the application or other features
- [ ] Add a feature flag so that this feature can be quickly disabled if/when needed
- [ ] If this experiment introduces a new service or data store, ensure it is not processing or storing [red data](https://about.gitlab.com/handbook/security/data-classification-standard.html#data-classification-levels) without a security and if needed legal review
- *NOTE*: We recommend using one of the already adopted models or data stores. If you need to use something else, be aware that using other models or data stores will require additional review during the feature stage for operational fitness and compliance.
+- [ ] Completed the necessary steps to move from Experiment to Beta
- [ ] Ensure this issue has the ~wg-ai-integration label to ensure visibility to various teams working on this
</details>
-### Feature release
+### Beta
<details> <summary> Issue information </summary>
- [ ] Add information to the issue body about:
- - [ ] Your proposal
- - [ ] The Job Statement it's expected to satisfy
- - [ ] Details about the user problem and provide any research or problem validation
- - [ ] List the personas impacted by the proposal.
+ - [ ] The Main Job story and Small Jobs it's expected to satisfy have been stated
+ - [ ] Your assumptions have been defined
+ - [ ] Proposal has been updated as necessary
+ - [ ] Problem validation inforamtion has been added
+ - [ ] Business objective has been defined
+ - [ ] Requirements have been defined
+ - [ ] Success metrics have been defined
+ - [ ] UX maturity requirements have been measured
+- [ ] Add all related feature issues to the Linked items section
+- [ ] Add all relevant solution validation issues to the Linked items section that shows this proposal will solve the customer problem, or details explaining why it's not possible to provide that validation.
+- [ ] Add relevant designs to the Design Management area of the issue.
+- [ ] You have adhered to our [Definition of Done](https://docs.gitlab.com/ee/development/contributing/merge_request_workflow.html#definition-of-done) standards
+- [ ] Completed the necessary steps to move from Beta to GA
+
+</details>
+
+#### Generally available
+<details> <summary> Issue information </summary>
+
+- [ ] Add information to the issue body about:
+ - [ ] Your assumptions have been defined
+ - [ ] Your proposal has been defined
+ - [ ] Problem validation inforamtion has been added
+ - [ ] Business objective has been defined
+ - [ ] Confidence about this feature has been assessed and defined
+ - [ ] Requirements have been defined
- [ ] Add all relevant solution validation issues to the Linked items section that shows this proposal will solve the customer problem, or details explaining why it's not possible to provide that validation.
- [ ] Add relevant designs to the Design Management area of the issue.
- [ ] You have adhered to our [Definition of Done](https://docs.gitlab.com/ee/development/contributing/merge_request_workflow.html#definition-of-done) standards
@@ -113,11 +177,11 @@ _What tasks or actions should the user be capable of performing with this featur
- [ ] Please consider the operational aspects of the feature you are creating. A list of things to think about is in: https://gitlab.com/gitlab-org/gitlab/-/issues/403859. We will be improving this process in the future: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/117637#note_1353253349.
- [ ] @ mention your [AppSec Stable Counterpart](https://about.gitlab.com/handbook/product/categories/) and read the [AI secure coding guidelines](https://docs.gitlab.com/ee/development/secure_coding_guidelines.html#artificial-intelligence-ai-features)
-1. Work estimate and skills needs to build an ML viable feature: To build any ML feature depending on the work, there are many personas that contribute including, Data Scientist, NLP engineer, ML Engineer, MLOps Engineer, ML Infra engineers, and Fullstack engineer to integrate the ML Services with Gitlab. Post-prototype we would assess the skills needed to build a production-grade ML feature for the prototype.
+1. Work estimate and skills needs to build an ML viable feature: To build any ML feature depending on the work, there are many personas that contribute including Data Scientist, NLP engineer, ML Engineer, MLOps Engineer, ML Infra engineers, Fullstack engineer to integrate the ML Services with Gitlab. Post-prototype we would assess the skills needed to build a production-grade ML feature for the prototype.
2. Data Limitation: We would like to upfront validate if we have viable data for the feature including whether we can use the DataOps pipeline of ModelOps or create a custom one. We would want to understand the training data, test data, and feedback data to dial up the accuracy and the limitations of the data.
3. Model Limitation: We would want to understand if we can use an open-source pre-trained model, tune and customize it or start a model from scratch as well. Further, we would assess based on the ModelOps model evaluation framework which would be the right model to use based on the use case.
4. Cost, Scalability, Reliability: We would want to estimate the cost of hosting, serving, inference of the model, and the full end-to-end infrastructure including monitoring and observability.
-5. Legal and Ethical Framework: We would want to align with legal and ethical framework like any other ModelOps features to cover across the nine principles of responsible ML and any legal support needed.
+5. Legal and Ethical Framework: We would want to align with legal and ethical framework like any other ModelOps features to cover the nine principles of responsible ML and any legal support needed.
</details>
@@ -136,12 +200,14 @@ _What tasks or actions should the user be capable of performing with this featur
## Additional resources
- If you'd like help with technical validation, or would like to discuss UX considerations for AI mention the AI Assisted group using `@gitlab-org/modelops/applied-ml`.
- Read about our [AI Integration strategy](https://internal-handbook.gitlab.io/handbook/product/ai-strategy/ai-integration-effort/)
-- Slack channels
- - `#wg_ai_integration` - Slack channel for the working group and the high level alignment on getting AI ready for Production (Development, Product, UX, Legal, etc.) But from the other channels fell free to reach out and post progress here
- - `#ai_integration_dev_lobby` - Channel for all implementation related topics and discussions of actual AI features (e.g. explain the code)
+- [AI-human interaction guidelines](https://design.gitlab.com/usability/ai-human-interaction)
+- [Highlighting feature versions guidelines](https://design.gitlab.com/usability/feature-management#highlighting-feature-versions)
+- [UX maturity requirements](https://about.gitlab.com/handbook/product/ai/ux-maturity/)
+- **Slack channels**
+ - `#wg_ai_integration` - Slack channel for the working group and the high-level alignment on getting AI ready for Production (Development, Product, UX, Legal, etc.) But from the other channels feel free to reach out and post progress here
+ - `#ai_integration_dev_lobby` - Channel for all implementation-related topics and discussions of actual AI features (e.g. explain the code)
- `#ai_enablement_team` - Channel for the AI Enablement Team which is building the base for all features (experimentation API, Abstraction Layer, Embeddings, etc.)
-
-/label ~wg-ai-integration
-/cc @tmccaslin @hbenson @wayne @pedroms @jmandell
-/confidential
+/label ~"AI Feature Proposal" ~"AI-Seeking community feedback"
+/cc @tmccaslin @hbenson @pedroms @jmandell
+/parent_epic &9997
diff --git a/.gitlab/issue_templates/Feature Flag Cleanup.md b/.gitlab/issue_templates/Feature Flag Cleanup.md
index da664cb4c1e..466c5c878c7 100644
--- a/.gitlab/issue_templates/Feature Flag Cleanup.md
+++ b/.gitlab/issue_templates/Feature Flag Cleanup.md
@@ -42,7 +42,7 @@ Are there any other stages or teams involved that need to be kept in the loop?
the feature can be officially announced in a release blog post.
- [ ] `/chatops run auto_deploy status <merge-commit-of-cleanup-mr>`
- [ ] Close [the feature issue](ISSUE LINK) to indicate the feature will be released in the current milestone.
-- [ ] If not already done, clean up the feature flag from all environments by running these chatops command in `#production` channel: `/chatops run feature delete <feature-flag-name> --dev --staging --staging-ref --production`
+- [ ] If not already done, clean up the feature flag from all environments by running these chatops command in `#production` channel: `/chatops run feature delete <feature-flag-name> --dev --ops --pre --staging --staging-ref --production`
- [ ] Close this rollout issue.
diff --git a/.gitlab/issue_templates/Feature Flag Roll Out.md b/.gitlab/issue_templates/Feature Flag Roll Out.md
index 7ed9a0b4d47..3bd39a73904 100644
--- a/.gitlab/issue_templates/Feature Flag Roll Out.md
+++ b/.gitlab/issue_templates/Feature Flag Roll Out.md
@@ -63,7 +63,7 @@ and cross-posted (with the command results) to the responsible team's Slack chan
Cross link the issue here if it does.
- [ ] Ensure that you or a representative in development can be available for at least 2 hours after feature flag updates in production.
If a different developer will be covering, or an exception is needed, please inform the oncall SRE by using the `@sre-oncall` Slack alias.
-- [ ] Ensure that [documentation has been updated](https://docs.gitlab.com/ee/development/documentation/feature_flags.html).
+- [ ] Ensure that documentation exists for the feature, and the [version history text](https://docs.gitlab.com/ee/development/documentation/feature_flags.html#add-version-history-text) has been updated.
- [ ] Leave a comment on [the feature issue][main-issue] announcing estimated time when this feature flag will be enabled on GitLab.com.
- [ ] Ensure that any breaking changes have been announced following the [release post process](https://about.gitlab.com/handbook/marketing/blog/release-posts/#deprecations-removals-and-breaking-changes) to ensure GitLab customers are aware.
- [ ] Notify the [`#support_gitlab-com` Slack channel](https://gitlab.slack.com/archives/C4XFU81LG) and your team channel ([more guidance when this is necessary in the dev docs](https://docs.gitlab.com/ee/development/feature_flags/controls.html#communicate-the-change)).
@@ -132,7 +132,7 @@ You can either [create a follow-up issue for Feature Flag Cleanup](https://gitla
If the merge request was deployed before [the monthly release was tagged](https://about.gitlab.com/handbook/engineering/releases/#self-managed-releases-1),
the feature can be officially announced in a release blog post: `/chatops run release check <merge-request-url> <milestone>`
- [ ] Close [the feature issue][main-issue] to indicate the feature will be released in the current milestone.
-- [ ] Clean up the feature flag from all environments by running these chatops command in `#production` channel: `/chatops run feature delete <feature-flag-name> --dev --staging --staging-ref --production`
+- [ ] Clean up the feature flag from all environments by running these chatops command in `#production` channel: `/chatops run feature delete <feature-flag-name> --dev --ops --pre --staging --staging-ref --production`
- [ ] Close this rollout issue.
## Rollback Steps
diff --git a/.gitlab/issue_templates/Geo Replicate a new Git repository type.md b/.gitlab/issue_templates/Geo Replicate a new Git repository type.md
index 9e7d2634f3a..61fbf1aadda 100644
--- a/.gitlab/issue_templates/Geo Replicate a new Git repository type.md
+++ b/.gitlab/issue_templates/Geo Replicate a new Git repository type.md
@@ -485,10 +485,19 @@ That's all of the required database changes.
end
trait :verification_succeeded do
+ synced
verification_checksum { 'e079a831cab27bcda7d81cd9b48296d0c3dd92ef' }
verification_state { Geo::CoolWidgetRegistry.verification_state_value(:verification_succeeded) }
verified_at { 5.days.ago }
end
+
+ trait :verification_failed do
+ synced
+ verification_failure { 'Could not calculate the checksum' }
+ verification_state { Geo::CoolWidgetRegistry.verification_state_value(:verification_failed) }
+ verification_retry_count { 1 }
+ verification_retry_at { 2.hours.from_now }
+ end
end
end
```
@@ -519,15 +528,15 @@ That's all of the required database changes.
FactoryBot.modify do
factory :cool_widget do
trait :verification_succeeded do
- repository
- verification_checksum { 'abc' }
- verification_state { CoolWidget.verification_state_value(:verification_succeeded) }
+ repository
+ verification_checksum { 'abc' }
+ verification_state { CoolWidget.verification_state_value(:verification_succeeded) }
end
trait :verification_failed do
- repository
- verification_failure { 'Could not calculate the checksum' }
- verification_state { CoolWidget.verification_state_value(:verification_failed) }
+ repository
+ verification_failure { 'Could not calculate the checksum' }
+ verification_state { CoolWidget.verification_state_value(:verification_failed) }
end
end
end
@@ -687,7 +696,7 @@ The GraphQL API is used by `Admin > Geo > Replication Details` views, and is dir
module Types
module Geo
- # rubocop:disable Graphql/AuthorizeTypes because it is included
+ # rubocop:disable Graphql/AuthorizeTypes -- because it is included
class CoolWidgetRegistryType < BaseObject
graphql_name 'CoolWidgetRegistry'
diff --git a/.gitlab/issue_templates/Geo Replicate a new blob type.md b/.gitlab/issue_templates/Geo Replicate a new blob type.md
index 11ad6614bc2..cc5b764f7a2 100644
--- a/.gitlab/issue_templates/Geo Replicate a new blob type.md
+++ b/.gitlab/issue_templates/Geo Replicate a new blob type.md
@@ -442,10 +442,19 @@ That's all of the required database changes.
end
trait :verification_succeeded do
+ synced
verification_checksum { 'e079a831cab27bcda7d81cd9b48296d0c3dd92ef' }
verification_state { Geo::CoolWidgetRegistry.verification_state_value(:verification_succeeded) }
verified_at { 5.days.ago }
end
+
+ trait :verification_failed do
+ synced
+ verification_failure { 'Could not calculate the checksum' }
+ verification_state { Geo::CoolWidgetRegistry.verification_state_value(:verification_failed) }
+ verification_retry_count { 1 }
+ verification_retry_at { 2.hours.from_now }
+ end
end
end
```
@@ -468,7 +477,7 @@ That's all of the required database changes.
end
```
-- [ ] Add the following to `spec/factories/cool_widgets.rb`:
+- [ ] Add the following to `ee/spec/factories/cool_widgets.rb`:
```ruby
# frozen_string_literal: true
@@ -476,15 +485,24 @@ That's all of the required database changes.
FactoryBot.modify do
factory :cool_widget do
trait :verification_succeeded do
- with_file
- verification_checksum { 'abc' }
- verification_state { CoolWidget.verification_state_value(:verification_succeeded) }
+ with_file
+ verification_checksum { 'abc' }
+ verification_state { CoolWidget.verification_state_value(:verification_succeeded) }
end
trait :verification_failed do
- with_file
- verification_failure { 'Could not calculate the checksum' }
- verification_state { CoolWidget.verification_state_value(:verification_failed) }
+ with_file
+ verification_failure { 'Could not calculate the checksum' }
+ verification_state { CoolWidget.verification_state_value(:verification_failed) }
+
+ #
+ # Geo::VerifiableReplicator#after_verifiable_update tries to verify
+ # the replicable async and marks it as verification started when the
+ # model record is created/updated.
+ #
+ after(:create) do |instance, _|
+ instance.verification_failed!
+ end
end
end
end
@@ -653,7 +671,7 @@ The GraphQL API is used by `Admin > Geo > Replication Details` views, and is dir
module Types
module Geo
- # rubocop:disable Graphql/AuthorizeTypes because it is included
+ # rubocop:disable Graphql/AuthorizeTypes -- because it is included
class CoolWidgetRegistryType < BaseObject
graphql_name 'CoolWidgetRegistry'
diff --git a/.haml-lint.yml b/.haml-lint.yml
index 097f72e7cce..64eb1ff0a09 100644
--- a/.haml-lint.yml
+++ b/.haml-lint.yml
@@ -139,6 +139,7 @@ linters:
- Style/HashSyntax
- Style/IdenticalConditionalBranches
- Style/IfInsideElse
+ - Style/InlineDisableAnnotation
- Style/NegatedIf
- Style/NestedTernaryOperator
- Style/RedundantInterpolation
diff --git a/.haml-lint_todo.yml b/.haml-lint_todo.yml
deleted file mode 100644
index a08ab491470..00000000000
--- a/.haml-lint_todo.yml
+++ /dev/null
@@ -1,161 +0,0 @@
-# This configuration was generated by
-# `haml-lint --auto-gen-config`
-# on 2023-10-11 14:54:51 +0200 using Haml-Lint version 0.40.1.
-# The point is for the user to remove these configuration records
-# one by one as the lints are removed from the code base.
-# Note that changes in the inspected code, or installation of new
-# versions of Haml-Lint, may require this file to be generated again.
-
-linters:
-
- # Offense count: 201
- DocumentationLinks:
- exclude:
- - "app/views/admin/application_settings/_account_and_limit.html.haml"
- - "app/views/admin/application_settings/_ci_cd.html.haml"
- - "app/views/admin/application_settings/_diagramsnet.html.haml"
- - "app/views/admin/application_settings/_email.html.haml"
- - "app/views/admin/application_settings/_error_tracking.html.haml"
- - "app/views/admin/application_settings/_floc.html.haml"
- - "app/views/admin/application_settings/_gitlab_shell_operation_limits.html.haml"
- - "app/views/admin/application_settings/_gitpod.html.haml"
- - "app/views/admin/application_settings/_kroki.html.haml"
- - "app/views/admin/application_settings/_localization.html.haml"
- - "app/views/admin/application_settings/_outbound.html.haml"
- - "app/views/admin/application_settings/_plantuml.html.haml"
- - "app/views/admin/application_settings/_projects_api_limits.html.haml"
- - "app/views/admin/application_settings/_repository_check.html.haml"
- - "app/views/admin/application_settings/_repository_storage.html.haml"
- - "app/views/admin/application_settings/_runner_registrars_form.html.haml"
- - "app/views/admin/application_settings/_signin.html.haml"
- - "app/views/admin/application_settings/_sourcegraph.html.haml"
- - "app/views/admin/application_settings/_spam.html.haml"
- - "app/views/admin/application_settings/_terms.html.haml"
- - "app/views/admin/application_settings/general.html.haml"
- - "app/views/admin/application_settings/metrics_and_profiling.html.haml"
- - "app/views/admin/application_settings/network.html.haml"
- - "app/views/admin/application_settings/preferences.html.haml"
- - "app/views/admin/application_settings/reporting.html.haml"
- - "app/views/admin/application_settings/repository.html.haml"
- - "app/views/admin/dashboard/index.html.haml"
- - "app/views/admin/dev_ops_report/_score.html.haml"
- - "app/views/clusters/clusters/_advanced_settings.html.haml"
- - "app/views/clusters/clusters/_deprecation_alert.html.haml"
- - "app/views/clusters/clusters/_multiple_clusters_message.html.haml"
- - "app/views/clusters/clusters/_namespace.html.haml"
- - "app/views/clusters/clusters/_provider_details_form.html.haml"
- - "app/views/clusters/clusters/cloud_providers/_cloud_provider_selector.html.haml"
- - "app/views/clusters/clusters/show.html.haml"
- - "app/views/clusters/clusters/user/_form.html.haml"
- - "app/views/groups/_import_group_from_another_instance_panel.html.haml"
- - "app/views/groups/_import_group_from_file_panel.html.haml"
- - "app/views/groups/settings/ci_cd/_auto_devops_form.html.haml"
- - "app/views/notify/github_gists_import_errors_email.html.haml"
- - "app/views/notify/pages_domain_auto_ssl_failed_email.html.haml"
- - "app/views/notify/pages_domain_auto_ssl_failed_email.text.haml"
- - "app/views/notify/pages_domain_disabled_email.html.haml"
- - "app/views/notify/pages_domain_enabled_email.html.haml"
- - "app/views/notify/pages_domain_verification_failed_email.html.haml"
- - "app/views/notify/pages_domain_verification_succeeded_email.html.haml"
- - "app/views/profiles/gpg_keys/index.html.haml"
- - "app/views/profiles/keys/_key.html.haml"
- - "app/views/profiles/keys/index.html.haml"
- - "app/views/profiles/personal_access_tokens/index.html.haml"
- - "app/views/profiles/show.html.haml"
- - "app/views/profiles/two_factor_auths/show.html.haml"
- - "app/views/projects/blob/_pipeline_tour_success.html.haml"
- - "app/views/projects/blob/viewers/_route_map.html.haml"
- - "app/views/projects/blob/viewers/_route_map_loading.html.haml"
- - "app/views/projects/branch_defaults/_branch_names_fields.html.haml"
- - "app/views/projects/branch_defaults/_default_branch_fields.html.haml"
- - "app/views/projects/cleanup/_show.html.haml"
- - "app/views/projects/commit/_signature_badge.html.haml"
- - "app/views/projects/environments/index.html.haml"
- - "app/views/projects/feature_flags/new.html.haml"
- - "app/views/projects/feature_flags_user_lists/edit.html.haml"
- - "app/views/projects/feature_flags_user_lists/new.html.haml"
- - "app/views/projects/issues/_new_branch.html.haml"
- - "app/views/projects/merge_requests/_page.html.haml"
- - "app/views/projects/mirrors/_branch_filter.html.haml"
- - "app/views/projects/mirrors/_mirror_repos.html.haml"
- - "app/views/projects/mirrors/_mirror_repos_push.html.haml"
- - "app/views/projects/pages_domains/_certificate.html.haml"
- - "app/views/projects/pages_domains/_dns.html.haml"
- - "app/views/projects/pages_domains/_helper_text.html.haml"
- - "app/views/projects/runners/_group_runners.html.haml"
- - "app/views/projects/settings/ci_cd/_autodevops_form.html.haml"
- - "app/views/projects/settings/merge_requests/_merge_request_merge_commit_template.html.haml"
- - "app/views/projects/settings/merge_requests/_merge_request_merge_method_settings.html.haml"
- - "app/views/projects/settings/merge_requests/_merge_request_merge_suggestions_settings.html.haml"
- - "app/views/projects/settings/merge_requests/_merge_request_squash_commit_template.html.haml"
- - "app/views/projects/settings/merge_requests/_merge_request_squash_options_settings.html.haml"
- - "app/views/projects/settings/operations/_alert_management.html.haml"
- - "app/views/projects/usage_quotas/index.html.haml"
- - "app/views/shared/_auto_devops_callout.html.haml"
- - "app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml"
- - "app/views/shared/_custom_attributes.html.haml"
- - "app/views/shared/_registration_features_discovery_message.html.haml"
- - "app/views/shared/_service_ping_consent.html.haml"
- - "app/views/shared/deploy_tokens/_form.html.haml"
- - "app/views/shared/deploy_tokens/_new_deploy_token.html.haml"
- - "app/views/shared/deploy_tokens/_table.html.haml"
- - "app/views/shared/empty_states/_snippets.html.haml"
- - "app/views/shared/integrations/gitlab_slack_application/_help.html.haml"
- - "app/views/shared/integrations/gitlab_slack_application/_slack_integration_form.html.haml"
- - "app/views/shared/integrations/mattermost_slash_commands/_help.html.haml"
- - "app/views/shared/integrations/slack_slash_commands/_help.html.haml"
- - "app/views/shared/issuable/form/_type_selector.html.haml"
- - "app/views/shared/runners/_shared_runners_description.html.haml"
- - "app/views/shared/web_hooks/_form.html.haml"
- - "ee/app/views/admin/application_settings/_custom_templates_form.html.haml"
- - "ee/app/views/admin/application_settings/_ee_network_settings.haml"
- - "ee/app/views/admin/application_settings/_elasticsearch_form.html.haml"
- - "ee/app/views/admin/application_settings/_ldap_access_setting.html.haml"
- - "ee/app/views/admin/application_settings/_microsoft_application.haml"
- - "ee/app/views/admin/application_settings/_saml_group_locks_setting.html.haml"
- - "ee/app/views/admin/application_settings/_templates.html.haml"
- - "ee/app/views/admin/dashboard/_elastic_and_geo.html.haml"
- - "ee/app/views/admin/geo/shared/_hashed_storage_alerts.html.haml"
- - "ee/app/views/admin/push_rules/_merge_request_approvals.html.haml"
- - "ee/app/views/admin/push_rules/_merge_request_approvals_fields.html.haml"
- - "ee/app/views/compliance_management/compliance_framework/_project_settings.html.haml"
- - "ee/app/views/groups/_analytics_dashboards.html.haml"
- - "ee/app/views/groups/_compliance_frameworks.html.haml"
- - "ee/app/views/groups/_custom_project_templates_setting.html.haml"
- - "ee/app/views/groups/_insights.html.haml"
- - "ee/app/views/groups/_templates_setting.html.haml"
- - "ee/app/views/groups/saml_providers/_info.html.haml"
- - "ee/app/views/groups/security/policies/index.html.haml"
- - "ee/app/views/groups/settings/_ip_restriction.html.haml"
- - "ee/app/views/groups/settings/domain_verification/_certificate.html.haml"
- - "ee/app/views/groups/settings/domain_verification/_dns.html.haml"
- - "ee/app/views/groups/settings/domain_verification/_helper_text.html.haml"
- - "ee/app/views/groups/settings/domain_verification/index.html.haml"
- - "ee/app/views/notify/import_requirements_csv_email.html.haml"
- - "ee/app/views/profiles/preferences/_code_suggestions_settings_self_assignment.html.haml"
- - "ee/app/views/projects/merge_requests/_code_owner_approval_rules.html.haml"
- - "ee/app/views/projects/mirrors/_branch_filter.html.haml"
- - "ee/app/views/projects/mirrors/_mirror_repos_form.html.haml"
- - "ee/app/views/projects/protected_environments/_group_environments_list.html.haml"
- - "ee/app/views/projects/security/policies/index.html.haml"
- - "ee/app/views/projects/settings/ci_cd/_auto_rollback.html.haml"
- - "ee/app/views/projects/settings/ci_cd/_pipeline_subscriptions.html.haml"
- - "ee/app/views/projects/settings/ci_cd/_protected_environments.html.haml"
- - "ee/app/views/projects/settings/merge_requests/_merge_pipelines_settings.html.haml"
- - "ee/app/views/projects/settings/merge_requests/_merge_request_approvals_settings.html.haml"
- - "ee/app/views/projects/settings/merge_requests/_merge_trains_settings.html.haml"
- - "ee/app/views/projects/settings/merge_requests/_suggested_reviewers_settings.html.haml"
- - "ee/app/views/projects/settings/merge_requests/_target_branch_rules_settings.html.haml"
- - "ee/app/views/search/results/_error.html.haml"
- - "ee/app/views/shared/_ci_cd_only_link.html.haml"
- - "ee/app/views/shared/_mirror_trigger_builds_setting.html.haml"
- - "ee/app/views/shared/_new_user_signups_cap_reached_alert.html.haml"
- - "ee/app/views/shared/empty_states/_geo_replication.html.haml"
- - "ee/app/views/shared/issuable/form/_merge_request_blocks.html.haml"
- - "ee/app/views/shared/labels/_create_label_help_text.html.haml"
- - "ee/app/views/shared/promotions/_promote_advanced_search.html.haml"
- - "ee/app/views/shared/promotions/_promote_burndown_charts.html.haml"
- - "ee/app/views/shared/promotions/_promote_group_webhooks.html.haml"
- - "ee/app/views/shared/promotions/_promote_mobile_devops.html.haml"
- - "ee/app/views/shared/promotions/_promote_mr_features.html.haml"
- - "ee/app/views/shared/promotions/_promote_repository_features.html.haml"
diff --git a/.projections.json.example b/.projections.json.example
index 973a7c56d8c..bc0c8790fd7 100644
--- a/.projections.json.example
+++ b/.projections.json.example
@@ -136,6 +136,14 @@
"alternate": "rubocop/cop/{}.rb",
"type": "test"
},
+ "tooling/*.rb": {
+ "alternate": "spec/tooling/{}_spec.rb",
+ "type": "source"
+ },
+ "spec/tooling/*_spec.rb": {
+ "alternate": "tooling/{}.rb",
+ "type": "test"
+ },
"ee/lib/api/*.rb": {
"alternate": "ee/spec/requests/api/{}_spec.rb",
"type": "source"
diff --git a/.rubocop.yml b/.rubocop.yml
index 544ef66fba6..adc796f1332 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -153,6 +153,12 @@ Style/FrozenStringLiteralComment:
Style/SpecialGlobalVars:
EnforcedStyle: use_builtin_english_names
+Style/SignalException:
+ Exclude:
+ # Danger defines its own `fail` method
+ - '**/*/Dangerfile'
+ - 'tooling/danger/**/*.rb'
+
RSpec/FilePath:
Exclude:
- 'qa/**/*'
@@ -310,6 +316,11 @@ Rails/ActiveRecordCallbacksOrder:
- app/models/**/*.rb
- ee/app/models/**/*.rb
+# We disable this since network latency isn't an issue and schema changes execute in a few milliseconds.
+# See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/136251#note_1638949892.
+Rails/BulkChangeTable:
+ Enabled: false
+
Cop/DefaultScope:
Enabled: true
@@ -477,9 +488,9 @@ BackgroundMigration/FeatureCategory:
Include:
- 'lib/gitlab/background_migration/*.rb'
-BackgroundMigration/MissingDictionaryFile:
+BackgroundMigration/DictionaryFile:
Enabled: true
- EnforcedSince: 20230307160251
+ EnforcedSince: 20231018100907
Include:
- 'db/post_migrate/*.rb'
@@ -919,6 +930,9 @@ Cop/UserAdmin:
- 'spec/**/*.rb'
- 'ee/spec/**/*.rb'
+Style/InlineDisableAnnotation:
+ Enabled: true
+
# See https://gitlab.com/gitlab-org/gitlab/-/issues/327495
Style/RegexpLiteral:
Enabled: false
@@ -1083,3 +1097,8 @@ Cop/ExperimentsTestCoverage:
- 'lib/**/*'
- 'ee/app/**/*'
- 'ee/lib/**/*'
+
+RSpec/UselessDynamicDefinition:
+ Exclude:
+ - 'spec/factories/**/*'
+ - 'ee/spec/factories/**/*'
diff --git a/.rubocop_todo/background_migration/dictionary_file.yml b/.rubocop_todo/background_migration/dictionary_file.yml
new file mode 100644
index 00000000000..d333a9f53a0
--- /dev/null
+++ b/.rubocop_todo/background_migration/dictionary_file.yml
@@ -0,0 +1,4 @@
+---
+# Grace period will be removed in https://gitlab.com/gitlab-org/gitlab/-/issues/428931
+BackgroundMigration/DictionaryFile:
+ Details: grace period
diff --git a/.rubocop_todo/capybara/testid_finders.yml b/.rubocop_todo/capybara/testid_finders.yml
index 414a8568e80..ddaf1e27beb 100644
--- a/.rubocop_todo/capybara/testid_finders.yml
+++ b/.rubocop_todo/capybara/testid_finders.yml
@@ -1,40 +1,6 @@
---
Capybara/TestidFinders:
Exclude:
- - 'ee/spec/features/protected_branches_spec.rb'
- - 'ee/spec/features/registrations/combined_registration_spec.rb'
- - 'ee/spec/features/registrations/identity_verification_spec.rb'
- - 'ee/spec/features/remote_development/workspaces_dropdown_group_spec.rb'
- - 'ee/spec/features/remote_development/workspaces_spec.rb'
- - 'ee/spec/features/search/elastic/group_search_spec.rb'
- - 'ee/spec/features/search/zoekt/search_spec.rb'
- - 'ee/spec/features/tanuki_bot_chat_spec.rb'
- - 'ee/spec/features/trials/saas/creation_with_multiple_existing_namespace_flow_spec.rb'
- - 'ee/spec/features/trials/show_trial_banner_spec.rb'
- - 'spec/features/admin/admin_deploy_keys_spec.rb'
- - 'spec/features/admin/admin_dev_ops_reports_spec.rb'
- - 'spec/features/admin/admin_groups_spec.rb'
- - 'spec/features/admin/admin_projects_spec.rb'
- - 'spec/features/admin/admin_runners_spec.rb'
- - 'spec/features/admin/admin_settings_spec.rb'
- - 'spec/features/admin/admin_uses_repository_checks_spec.rb'
- - 'spec/features/admin/broadcast_messages_spec.rb'
- - 'spec/features/admin/users/user_spec.rb'
- - 'spec/features/admin/users/users_spec.rb'
- - 'spec/features/alert_management/alert_details_spec.rb'
- - 'spec/features/boards/board_filters_spec.rb'
- - 'spec/features/boards/boards_spec.rb'
- - 'spec/features/boards/issue_ordering_spec.rb'
- - 'spec/features/boards/new_issue_spec.rb'
- - 'spec/features/boards/sidebar_assignee_spec.rb'
- - 'spec/features/broadcast_messages_spec.rb'
- - 'spec/features/callouts/registration_enabled_spec.rb'
- - 'spec/features/clusters/create_agent_spec.rb'
- - 'spec/features/commits_spec.rb'
- - 'spec/features/dashboard/group_spec.rb'
- - 'spec/features/dashboard/issues_spec.rb'
- - 'spec/features/dashboard/merge_requests_spec.rb'
- - 'spec/features/dashboard/milestones_spec.rb'
- 'spec/features/dashboard/projects_spec.rb'
- 'spec/features/dashboard/todos/todos_spec.rb'
- 'spec/features/groups/board_sidebar_spec.rb'
@@ -94,10 +60,7 @@ Capybara/TestidFinders:
- '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/new_nav_toggle_spec.rb'
- 'spec/features/nav/pinned_nav_items_spec.rb'
- - 'spec/features/nav/top_nav_responsive_spec.rb'
- - 'spec/features/nav/top_nav_spec.rb'
- 'spec/features/populate_new_pipeline_vars_with_params_spec.rb'
- 'spec/features/profile_spec.rb'
- 'spec/features/profiles/account_spec.rb'
diff --git a/.rubocop_todo/capybara/visibility_matcher.yml b/.rubocop_todo/capybara/visibility_matcher.yml
index d303f0d5332..996ff594622 100644
--- a/.rubocop_todo/capybara/visibility_matcher.yml
+++ b/.rubocop_todo/capybara/visibility_matcher.yml
@@ -70,4 +70,3 @@ Capybara/VisibilityMatcher:
- 'spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb'
- 'spec/views/profiles/preferences/show.html.haml_spec.rb'
- 'spec/views/projects/merge_requests/edit.html.haml_spec.rb'
- - 'spec/views/projects/merge_requests/show.html.haml_spec.rb'
diff --git a/.rubocop_todo/factory_bot/create_list.yml b/.rubocop_todo/factory_bot/create_list.yml
index 9f6d3b19735..5d1f9e3e333 100644
--- a/.rubocop_todo/factory_bot/create_list.yml
+++ b/.rubocop_todo/factory_bot/create_list.yml
@@ -17,7 +17,6 @@ FactoryBot/CreateList:
- 'ee/spec/lib/ee/gitlab/usage_data_spec.rb'
- 'ee/spec/lib/gitlab/ci/reports/security/vulnerability_reports_comparer_spec.rb'
- 'ee/spec/lib/gitlab/license_scanning/sbom_scanner_spec.rb'
- - 'ee/spec/lib/gitlab/vulnerabilities/findings_preloader_spec.rb'
- 'ee/spec/models/approval_project_rule_spec.rb'
- 'ee/spec/models/ci/build_spec.rb'
- 'ee/spec/models/ee/project_spec.rb'
diff --git a/.rubocop_todo/fips/sha1.yml b/.rubocop_todo/fips/sha1.yml
index bfb51250295..f011b6e0ed7 100644
--- a/.rubocop_todo/fips/sha1.yml
+++ b/.rubocop_todo/fips/sha1.yml
@@ -48,7 +48,6 @@ Fips/SHA1:
- 'lib/gitlab/git/branch.rb'
- 'lib/gitlab/git/tag.rb'
- 'qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb'
- - 'qa/qa/specs/features/ee/browser_ui/1_manage/group/group_saml_non_enforced_sso_spec.rb'
- 'spec/components/diffs/stats_component_spec.rb'
- 'spec/controllers/projects/blob_controller_spec.rb'
- 'spec/factories/ci/reports/security/finding_keys.rb'
@@ -78,7 +77,6 @@ Fips/SHA1:
- 'spec/lib/gitlab/diff/position_spec.rb'
- 'spec/lib/gitlab/git/branch_spec.rb'
- 'spec/lib/gitlab/git/tag_spec.rb'
- - 'spec/migrations/20220524074947_finalize_backfill_null_note_discussion_ids_spec.rb'
- 'spec/models/ci/artifact_blob_spec.rb'
- 'spec/models/ci/job_artifact_spec.rb'
- 'spec/models/ci/pipeline_spec.rb'
@@ -89,7 +87,6 @@ Fips/SHA1:
- 'spec/models/merge_request_spec.rb'
- 'spec/models/note_spec.rb'
- 'spec/models/repository_spec.rb'
- - 'spec/support/migrations_helpers/vulnerabilities_findings_helper.rb'
- 'spec/support/shared_examples/lib/gitlab/position_formatters_shared_examples.rb'
- 'spec/support/shared_examples/services/alert_management/alert_processing/alert_firing_shared_examples.rb'
- 'spec/support/shared_examples/services/alert_management/alert_processing/alert_recovery_shared_examples.rb'
diff --git a/.rubocop_todo/gemspec/deprecated_attribute_assignment.yml b/.rubocop_todo/gemspec/deprecated_attribute_assignment.yml
new file mode 100644
index 00000000000..32d493c3741
--- /dev/null
+++ b/.rubocop_todo/gemspec/deprecated_attribute_assignment.yml
@@ -0,0 +1,6 @@
+---
+# Cop supports --autocorrect.
+Gemspec/DeprecatedAttributeAssignment:
+ Details: grace period
+ Exclude:
+ - 'spec/fixtures/packages/rubygems/package.gemspec'
diff --git a/.rubocop_todo/gitlab/doc_url.yml b/.rubocop_todo/gitlab/doc_url.yml
index 190bda22721..fbc58c436e8 100644
--- a/.rubocop_todo/gitlab/doc_url.yml
+++ b/.rubocop_todo/gitlab/doc_url.yml
@@ -27,7 +27,6 @@ Gitlab/DocUrl:
- 'lib/gitlab/audit/auditor.rb'
- 'lib/gitlab/ci/config/entry/processable.rb'
- 'lib/gitlab/config_checker/external_database_checker.rb'
- - 'lib/gitlab/config_checker/puma_rugged_checker.rb'
- 'lib/gitlab/database_warnings.rb'
- 'lib/gitlab/database/migration_helpers/automatic_lock_writes_on_tables.rb'
- 'lib/gitlab/database/migration_helpers/v2.rb'
diff --git a/.rubocop_todo/gitlab/namespaced_class.yml b/.rubocop_todo/gitlab/namespaced_class.yml
index 6268aa26248..8b59571249c 100644
--- a/.rubocop_todo/gitlab/namespaced_class.yml
+++ b/.rubocop_todo/gitlab/namespaced_class.yml
@@ -121,7 +121,6 @@ Gitlab/NamespacedClass:
- 'app/models/board.rb'
- 'app/models/board_group_recent_visit.rb'
- 'app/models/board_project_recent_visit.rb'
- - 'app/models/broadcast_message.rb'
- 'app/models/bulk_import.rb'
- 'app/models/chat_name.rb'
- 'app/models/chat_team.rb'
@@ -161,7 +160,6 @@ Gitlab/NamespacedClass:
- 'app/models/event_collection.rb'
- 'app/models/exported_protected_branch.rb'
- 'app/models/external_issue.rb'
- - 'app/models/external_pull_request.rb'
- 'app/models/fork_network.rb'
- 'app/models/fork_network_member.rb'
- 'app/models/generic_commit_status.rb'
@@ -268,7 +266,6 @@ Gitlab/NamespacedClass:
- 'app/models/project_import_data.rb'
- 'app/models/project_import_state.rb'
- 'app/models/project_label.rb'
- - 'app/models/project_metrics_setting.rb'
- 'app/models/project_pages_metadatum.rb'
- 'app/models/project_repository.rb'
- 'app/models/project_setting.rb'
@@ -299,7 +296,6 @@ Gitlab/NamespacedClass:
- 'app/models/resource_timebox_event.rb'
- 'app/models/review.rb'
- 'app/models/route.rb'
- - 'app/models/self_managed_prometheus_alert_event.rb'
- 'app/models/sent_notification.rb'
- 'app/models/sentry_issue.rb'
- 'app/models/service_desk_setting.rb'
@@ -324,7 +320,6 @@ Gitlab/NamespacedClass:
- 'app/models/todo.rb'
- 'app/models/tree.rb'
- 'app/models/trending_project.rb'
- - 'app/models/u2f_registration.rb'
- 'app/models/upload.rb'
- 'app/models/user.rb'
- 'app/models/user_agent_detail.rb'
@@ -591,8 +586,6 @@ Gitlab/NamespacedClass:
- 'app/serializers/project_note_entity.rb'
- 'app/serializers/project_note_serializer.rb'
- 'app/serializers/project_serializer.rb'
- - 'app/serializers/prometheus_alert_entity.rb'
- - 'app/serializers/prometheus_alert_serializer.rb'
- 'app/serializers/prometheus_metric_entity.rb'
- 'app/serializers/prometheus_metric_serializer.rb'
- 'app/serializers/release_entity.rb'
@@ -744,7 +737,6 @@ Gitlab/NamespacedClass:
- 'app/workers/create_commit_signature_worker.rb'
- 'app/workers/create_note_diff_file_worker.rb'
- 'app/workers/create_pipeline_worker.rb'
- - 'app/workers/delete_container_repository_worker.rb'
- 'app/workers/delete_diff_files_worker.rb'
- 'app/workers/delete_merged_branches_worker.rb'
- 'app/workers/delete_stored_files_worker.rb'
@@ -763,7 +755,6 @@ Gitlab/NamespacedClass:
- 'app/workers/flush_counter_increments_worker.rb'
- 'app/workers/gitlab_performance_bar_stats_worker.rb'
- 'app/workers/gitlab_service_ping_worker.rb'
- - 'app/workers/gitlab_shell_worker.rb'
- 'app/workers/group_destroy_worker.rb'
- 'app/workers/group_export_worker.rb'
- 'app/workers/group_import_worker.rb'
@@ -846,7 +837,6 @@ Gitlab/NamespacedClass:
- 'ee/app/controllers/smartcard_controller.rb'
- 'ee/app/controllers/subscriptions_controller.rb'
- 'ee/app/controllers/trial_registrations_controller.rb'
- - 'ee/app/controllers/trials_controller.rb'
- 'ee/app/finders/audit_event_finder.rb'
- 'ee/app/finders/billed_users_finder.rb'
- 'ee/app/finders/custom_project_templates_finder.rb'
@@ -862,7 +852,6 @@ Gitlab/NamespacedClass:
- 'ee/app/finders/licenses_finder.rb'
- 'ee/app/finders/productivity_analytics_finder.rb'
- 'ee/app/finders/scim_finder.rb'
- - 'ee/app/finders/software_license_policies_finder.rb'
- 'ee/app/mailers/ci_minutes_usage_mailer.rb'
- 'ee/app/mailers/credentials_inventory_mailer.rb'
- 'ee/app/mailers/license_mailer.rb'
@@ -1079,7 +1068,6 @@ Gitlab/NamespacedClass:
- 'lib/event_filter.rb'
- 'lib/file_size_validator.rb'
- 'lib/forever.rb'
- - 'lib/generators/gitlab/snowplow_event_definition_generator.rb'
- 'lib/generators/gitlab/usage_metric_definition_generator.rb'
- 'lib/generators/gitlab/usage_metric_generator.rb'
- 'lib/gitlab/anonymous_session.rb'
@@ -1121,6 +1109,7 @@ Gitlab/NamespacedClass:
- 'lib/gitlab/encrypted_configuration.rb'
- 'lib/gitlab/encrypted_incoming_email_command.rb'
- 'lib/gitlab/encrypted_ldap_command.rb'
+ - 'lib/gitlab/encrypted_redis_command.rb'
- 'lib/gitlab/encrypted_service_desk_email_command.rb'
- 'lib/gitlab/encrypted_smtp_command.rb'
- 'lib/gitlab/environment_logger.rb'
@@ -1256,12 +1245,10 @@ Gitlab/NamespacedClass:
- 'spec/lib/marginalia_spec.rb'
- 'spec/models/concerns/batch_destroy_dependent_associations_spec.rb'
- 'spec/support/helpers/ci_artifact_metadata_generator.rb'
- - 'spec/support/helpers/fake_migration_classes.rb'
- 'spec/support/helpers/fake_webauthn_device.rb'
- 'spec/support/helpers/markdown_feature.rb'
- 'spec/support/helpers/redis_without_keys.rb'
- 'spec/support/helpers/require_migration.rb'
- - 'spec/support/models/merge_request_without_merge_request_diff.rb'
- 'spec/support/renameable_upload.rb'
- 'spec/lib/gitlab/task_helpers_spec.rb'
- 'spec/uploaders/object_storage_spec.rb'
diff --git a/.rubocop_todo/gitlab/no_code_coverage_comment.yml b/.rubocop_todo/gitlab/no_code_coverage_comment.yml
index e97974a4738..a32035315f4 100644
--- a/.rubocop_todo/gitlab/no_code_coverage_comment.yml
+++ b/.rubocop_todo/gitlab/no_code_coverage_comment.yml
@@ -6,10 +6,8 @@ Gitlab/NoCodeCoverageComment:
- 'app/workers/database/batched_background_migration/single_database_worker.rb'
- 'ee/app/models/concerns/geo/replicable_model.rb'
- 'ee/lib/gitlab/geo/replicator.rb'
- - 'lib/gitlab/auth/o_auth/session.rb'
- 'lib/gitlab/cleanup/personal_access_tokens.rb'
- 'lib/gitlab/cycle_analytics/summary/defaults.rb'
- - 'lib/gitlab/database/background_migration/health_status/signals.rb'
- 'lib/gitlab/seeder.rb'
- 'lib/gitlab/webpack/dev_server_middleware.rb'
- 'lib/tasks/gems.rake'
diff --git a/.rubocop_todo/gitlab/service_response.yml b/.rubocop_todo/gitlab/service_response.yml
index eef625fed0c..fa65cade197 100644
--- a/.rubocop_todo/gitlab/service_response.yml
+++ b/.rubocop_todo/gitlab/service_response.yml
@@ -67,7 +67,6 @@ Gitlab/ServiceResponse:
- 'spec/controllers/projects/alerting/notifications_controller_spec.rb'
- 'spec/controllers/projects/issues_controller_spec.rb'
- 'spec/controllers/projects/pipelines_controller_spec.rb'
- - 'spec/controllers/projects/prometheus/alerts_controller_spec.rb'
- 'spec/lib/gitlab/import_export/snippet_repo_restorer_spec.rb'
- 'spec/requests/api/ci/pipelines_spec.rb'
- 'spec/requests/api/ci/runner/runners_post_spec.rb'
diff --git a/.rubocop_todo/gitlab/strong_memoize_attr.yml b/.rubocop_todo/gitlab/strong_memoize_attr.yml
index 33dcb37b15a..c40f51cf04e 100644
--- a/.rubocop_todo/gitlab/strong_memoize_attr.yml
+++ b/.rubocop_todo/gitlab/strong_memoize_attr.yml
@@ -205,10 +205,6 @@ Gitlab/StrongMemoizeAttr:
- 'app/services/merge_requests/outdated_discussion_diff_lines_service.rb'
- 'app/services/merge_requests/pushed_branches_service.rb'
- 'app/services/merge_requests/refresh_service.rb'
- - 'app/services/metrics/dashboard/clone_dashboard_service.rb'
- - 'app/services/metrics/dashboard/custom_metric_embed_service.rb'
- - 'app/services/metrics/dashboard/dynamic_embed_service.rb'
- - 'app/services/metrics/dashboard/gitlab_alert_embed_service.rb'
- 'app/services/namespaces/package_settings/update_service.rb'
- 'app/services/projects/container_repository/cleanup_tags_base_service.rb'
- 'app/services/projects/container_repository/third_party/cleanup_tags_service.rb'
@@ -218,11 +214,9 @@ Gitlab/StrongMemoizeAttr:
- 'app/services/projects/open_issues_count_service.rb'
- 'app/services/projects/record_target_platforms_service.rb'
- 'app/services/projects/update_statistics_service.rb'
- - 'app/services/prometheus/proxy_service.rb'
- 'app/services/quick_actions/interpret_service.rb'
- 'app/services/releases/base_service.rb'
- 'app/services/resource_access_tokens/revoke_service.rb'
- - 'app/services/resource_events/base_synthetic_notes_builder_service.rb'
- 'app/services/search/global_service.rb'
- 'app/services/search/project_service.rb'
- 'app/services/search_service.rb'
@@ -268,7 +262,6 @@ Gitlab/StrongMemoizeAttr:
- 'ee/app/finders/epics_finder.rb'
- 'ee/app/finders/incident_management/oncall_users_finder.rb'
- 'ee/app/finders/security/pipeline_vulnerabilities_finder.rb'
- - 'ee/app/finders/security/training_providers/base_url_finder.rb'
- 'ee/app/graphql/resolvers/epics_resolver.rb'
- 'ee/app/graphql/resolvers/vulnerabilities_base_resolver.rb'
- 'ee/app/helpers/admin/emails_helper.rb'
@@ -357,7 +350,6 @@ Gitlab/StrongMemoizeAttr:
- 'ee/app/services/geo/container_repository_sync.rb'
- 'ee/app/services/geo/event_service.rb'
- 'ee/app/services/geo/file_registry_removal_service.rb'
- - 'ee/app/services/geo/repository_destroy_service.rb'
- 'ee/app/services/gitlab_subscriptions/activate_service.rb'
- 'ee/app/services/gitlab_subscriptions/create_service.rb'
- 'ee/app/services/gitlab_subscriptions/fetch_purchase_eligible_namespaces_service.rb'
@@ -478,7 +470,6 @@ Gitlab/StrongMemoizeAttr:
- 'lib/container_registry/tag.rb'
- 'lib/gitlab/alert_management/alert_status_counts.rb'
- 'lib/gitlab/alert_management/payload/base.rb'
- - 'lib/gitlab/alert_management/payload/managed_prometheus.rb'
- 'lib/gitlab/analytics/cycle_analytics/aggregated/data_collector.rb'
- 'lib/gitlab/analytics/cycle_analytics/aggregated/records_fetcher.rb'
- 'lib/gitlab/analytics/cycle_analytics/average.rb'
@@ -609,10 +600,7 @@ Gitlab/StrongMemoizeAttr:
- 'lib/gitlab/kubernetes/rollout_instances.rb'
- 'lib/gitlab/language_data.rb'
- 'lib/gitlab/lets_encrypt/client.rb'
- - 'lib/gitlab/metrics/dashboard/stages/grafana_formatter.rb'
- - 'lib/gitlab/metrics/dashboard/url.rb'
- 'lib/gitlab/metrics/prometheus.rb'
- - 'lib/gitlab/pages/cache_control.rb'
- 'lib/gitlab/prometheus_client.rb'
- 'lib/gitlab/rack_attack/request.rb'
- 'lib/gitlab/redis/multi_store.rb'
diff --git a/.rubocop_todo/graphql/descriptions.yml b/.rubocop_todo/graphql/descriptions.yml
index 728c49ca30f..73e8fa8777d 100644
--- a/.rubocop_todo/graphql/descriptions.yml
+++ b/.rubocop_todo/graphql/descriptions.yml
@@ -70,7 +70,6 @@ Graphql/Descriptions:
- 'ee/app/graphql/ee/types/projects/branch_rule_type.rb'
- 'ee/app/graphql/ee/types/user_merge_request_interaction_type.rb'
- 'ee/app/graphql/resolvers/epics_resolver.rb'
- - 'ee/app/graphql/types/access_levels/user_type.rb'
- 'ee/app/graphql/types/boards/epic_list_type.rb'
- 'ee/app/graphql/types/branch_rules/approval_project_rule_type.rb'
- 'ee/app/graphql/types/burnup_chart_daily_totals_type.rb'
diff --git a/.rubocop_todo/graphql/resource_not_available_error.yml b/.rubocop_todo/graphql/resource_not_available_error.yml
index c52cdfff6b4..afe976e0f69 100644
--- a/.rubocop_todo/graphql/resource_not_available_error.yml
+++ b/.rubocop_todo/graphql/resource_not_available_error.yml
@@ -4,7 +4,6 @@ Graphql/ResourceNotAvailableError:
Exclude:
- 'app/graphql/mutations/achievements/create.rb'
- 'app/graphql/mutations/admin/sidekiq_queues/delete_jobs.rb'
- - 'app/graphql/mutations/ci/ci_cd_settings_update.rb'
- 'app/graphql/mutations/ci/job_artifact/bulk_destroy.rb'
- 'app/graphql/mutations/ci/runner/create.rb'
- 'app/graphql/mutations/custom_emoji/create.rb'
@@ -31,7 +30,6 @@ Graphql/ResourceNotAvailableError:
- 'app/graphql/resolvers/projects/snippets_resolver.rb'
- 'app/graphql/types/container_repository_details_type.rb'
- 'app/graphql/types/container_repository_type.rb'
- - 'ee/app/graphql/ee/types/query_type.rb'
- 'ee/app/graphql/mutations/ai/action.rb'
- 'ee/app/graphql/mutations/audit_events/instance_external_audit_event_destinations/base.rb'
- 'ee/app/graphql/mutations/ci/ai/generate_config.rb'
diff --git a/.rubocop_todo/layout/argument_alignment.yml b/.rubocop_todo/layout/argument_alignment.yml
index 7b4b3e68b78..b2dc0b1f570 100644
--- a/.rubocop_todo/layout/argument_alignment.yml
+++ b/.rubocop_todo/layout/argument_alignment.yml
@@ -212,14 +212,12 @@ Layout/ArgumentAlignment:
- 'app/graphql/resolvers/group_releases_resolver.rb'
- 'app/graphql/resolvers/groups_resolver.rb'
- 'app/graphql/resolvers/incident_management/timeline_events_resolver.rb'
- - 'app/graphql/resolvers/issues/base_parent_resolver.rb'
- 'app/graphql/resolvers/issues/base_resolver.rb'
- 'app/graphql/resolvers/issues_resolver.rb'
- 'app/graphql/resolvers/labels_resolver.rb'
- 'app/graphql/resolvers/members_resolver.rb'
- 'app/graphql/resolvers/merge_request_resolver.rb'
- 'app/graphql/resolvers/merge_requests_resolver.rb'
- - 'app/graphql/resolvers/metrics/dashboard_resolver.rb'
- 'app/graphql/resolvers/metrics/dashboards/annotation_resolver.rb'
- 'app/graphql/resolvers/milestones_resolver.rb'
- 'app/graphql/resolvers/namespace_projects_resolver.rb'
@@ -402,7 +400,6 @@ Layout/ArgumentAlignment:
- 'app/graphql/types/merge_requests/merge_status_enum.rb'
- 'app/graphql/types/metadata/kas_type.rb'
- 'app/graphql/types/metadata_type.rb'
- - 'app/graphql/types/metrics/dashboard_type.rb'
- 'app/graphql/types/metrics/dashboards/annotation_type.rb'
- 'app/graphql/types/milestone_stats_type.rb'
- 'app/graphql/types/milestone_type.rb'
@@ -494,11 +491,6 @@ Layout/ArgumentAlignment:
- 'app/services/lfs/lock_file_service.rb'
- 'app/services/markdown_content_rewriter_service.rb'
- 'app/services/members/base_service.rb'
- - 'app/services/members/create_service.rb'
- - 'app/services/metrics/dashboard/annotations/create_service.rb'
- - 'app/services/metrics/dashboard/annotations/delete_service.rb'
- - 'app/services/metrics/dashboard/clone_dashboard_service.rb'
- - 'app/services/metrics/users_starred_dashboards/create_service.rb'
- 'app/services/ml/experiment_tracking/experiment_repository.rb'
- 'app/services/post_receive_service.rb'
- 'app/services/preview_markdown_service.rb'
@@ -518,10 +510,6 @@ Layout/ArgumentAlignment:
- 'config/initializers/rack_timeout.rb'
- 'config/initializers/rest-client-hostname_override.rb'
- 'config/initializers/zz_metrics.rb'
- - 'db/migrate/20220401113123_add_check_constraint_to_vsa_aggregation_runtime_data_columns.rb'
- - 'db/migrate/20220405125459_add_non_migrated_index_to_container_repositories.rb'
- - 'db/migrate/20220408001450_add_work_item_type_name_unique_index_null_namespaces.rb'
- - 'db/migrate/20220413075921_update_index_on_packages_build_infos.rb'
- 'db/migrate/20220413164146_remove_max_seats_used_indices.rb'
- 'db/migrate/20220419223906_add_arkose_namespace_to_application_settings.rb'
- 'db/migrate/20220420173247_add_group_inheritance_type_to_pe_authorizable.rb'
@@ -530,7 +518,6 @@ Layout/ArgumentAlignment:
- 'db/migrate/20220511191502_add_registry_migration_guard_thresholds_to_application_settings.rb'
- 'db/migrate/20220513093615_add_ding_talk_tracker_data.rb'
- 'db/migrate/20220513095545_create_timelog_categories.rb'
- - 'db/migrate/20220520120637_add_installable_conan_packages_index_to_packages.rb'
- 'db/migrate/20220520144821_add_registry_migration_pre_import_tags_rate_to_application_settings.rb'
- 'db/migrate/20220524141800_create_audit_events_streaming_headers.rb'
- 'db/migrate/20220601101800_add_index_on_runner_id_and_semver_columns.rb'
@@ -612,8 +599,6 @@ Layout/ArgumentAlignment:
- 'db/post_migrate/20221003192827_add_index_resolved_on_default_branch_to_vulnerabilities_read.rb'
- 'db/post_migrate/20221004092038_tmp_index_members_on_id_where_namespace_id_null.rb'
- 'db/post_migrate/20221010123040_add_compliance_framework_fk_to_namespace_settings.rb'
- - 'db/post_migrate/20221021082255_add_unique_index_on_ci_runners_token.rb'
- - 'db/post_migrate/20221021082312_add_unique_index_on_ci_runners_token_encrypted.rb'
- 'db/post_migrate/20221021082720_drop_index_on_ci_runners_token.rb'
- 'db/post_migrate/20221021082734_drop_index_on_ci_runners_token_encrypted.rb'
- 'db/post_migrate/20221021160735_add_index_for_common_finder_query_desc_with_namespace_id.rb'
@@ -653,7 +638,6 @@ Layout/ArgumentAlignment:
- 'ee/app/graphql/ee/types/branch_protections/base_access_level_type.rb'
- 'ee/app/graphql/ee/types/branch_rules/branch_protection_type.rb'
- 'ee/app/graphql/ee/types/ci/runner_countable_connection_type.rb'
- - 'ee/app/graphql/ee/types/clusters/agent_type.rb'
- 'ee/app/graphql/ee/types/deployment_type.rb'
- 'ee/app/graphql/ee/types/environment_type.rb'
- 'ee/app/graphql/ee/types/group_type.rb'
@@ -662,7 +646,6 @@ Layout/ArgumentAlignment:
- 'ee/app/graphql/ee/types/permission_types/deployment.rb'
- 'ee/app/graphql/ee/types/project_type.rb'
- 'ee/app/graphql/ee/types/projects/branch_rule_type.rb'
- - 'ee/app/graphql/ee/types/query_type.rb'
- 'ee/app/graphql/ee/types/subscription_type.rb'
- 'ee/app/graphql/ee/types/user_merge_request_interaction_type.rb'
- 'ee/app/graphql/mutations/analytics/devops_adoption/enabled_namespaces/bulk_enable.rb'
@@ -926,7 +909,6 @@ Layout/ArgumentAlignment:
- 'ee/app/services/ee/issues/clone_service.rb'
- 'ee/app/services/ee/issues/move_service.rb'
- 'ee/app/services/ee/keys/create_service.rb'
- - 'ee/app/services/ee/members/create_service.rb'
- 'ee/app/services/ee/merge_requests/create_pipeline_service.rb'
- 'ee/app/services/ee/projects/create_from_template_service.rb'
- 'ee/app/services/ee/projects/gitlab_projects_import_service.rb'
@@ -939,7 +921,6 @@ Layout/ArgumentAlignment:
- 'ee/app/services/external_status_checks/update_service.rb'
- 'ee/app/services/geo/framework_repository_sync_service.rb'
- 'ee/app/services/geo/prune_event_log_service.rb'
- - 'ee/app/services/geo/repository_base_sync_service.rb'
- 'ee/app/services/group_saml/saml_group_links/create_service.rb'
- 'ee/app/services/iterations/create_service.rb'
- 'ee/app/services/merge_trains/create_pipeline_service.rb'
@@ -956,11 +937,9 @@ Layout/ArgumentAlignment:
- 'ee/db/geo/migrate/20180405074130_add_partial_index_project_repository_verification.rb'
- 'ee/db/geo/post_migrate/20210217020154_add_unique_index_on_container_repository_registry.rb'
- 'ee/db/geo/post_migrate/20210217020156_add_unique_index_on_terraform_state_version_registry.rb'
- - 'ee/db/seeds/awesome_co/awesome_co.rb'
- 'ee/lib/audit/compliance_framework_changes_auditor.rb'
- 'ee/lib/audit/external_status_check_changes_auditor.rb'
- 'ee/lib/audit/group_changes_auditor.rb'
- - 'ee/lib/audit/group_push_rules_changes_auditor.rb'
- 'ee/lib/audit/project_changes_auditor.rb'
- 'ee/lib/ee/gitlab/background_migration/backfill_epic_cache_counts.rb'
- 'ee/lib/ee/gitlab/background_migration/backfill_project_statistics_container_repository_size.rb'
@@ -1016,13 +995,10 @@ Layout/ArgumentAlignment:
- 'ee/spec/lib/ee/gitlab/background_migration/migrate_vulnerabilities_feedback_to_vulnerabilities_state_transition_spec.rb'
- 'ee/spec/lib/ee/gitlab/background_migration/populate_approval_merge_request_rules_with_security_orchestration_spec.rb'
- 'ee/spec/lib/ee/gitlab/background_migration/populate_approval_project_rules_with_security_orchestration_spec.rb'
- - 'ee/spec/lib/ee/gitlab/background_migration/populate_latest_pipeline_ids_spec.rb'
- - 'ee/spec/lib/ee/gitlab/background_migration/populate_resolved_on_default_branch_column_spec.rb'
- 'ee/spec/lib/ee/gitlab/background_migration/purge_stale_security_scans_spec.rb'
- 'ee/spec/lib/ee/gitlab/checks/push_rule_check_spec.rb'
- 'ee/spec/lib/ee/gitlab/checks/push_rules/commit_check_spec.rb'
- 'ee/spec/lib/ee/gitlab/ci/config/entry/bridge_spec.rb'
- - 'ee/spec/lib/ee/gitlab/ci/reports/security/reports_spec.rb'
- 'ee/spec/lib/ee/gitlab/git_access_project_spec.rb'
- 'ee/spec/lib/ee/gitlab/import_export/project/tree_restorer_spec.rb'
- 'ee/spec/lib/ee/gitlab/import_export/repo_restorer_spec.rb'
@@ -1059,7 +1035,6 @@ Layout/ArgumentAlignment:
- 'ee/spec/lib/gitlab/insights/loader_spec.rb'
- 'ee/spec/lib/gitlab/license_scanning/branch_components_spec.rb'
- 'ee/spec/lib/gitlab/license_scanning/package_licenses_spec.rb'
- - 'ee/spec/lib/gitlab/prometheus/queries/additional_metrics_deployment_query_spec.rb'
- 'ee/spec/lib/gitlab/status_page_spec.rb'
- 'ee/spec/lib/gitlab/zoekt/search_results_spec.rb'
- 'ee/spec/lib/incident_management/oncall_shift_generator_spec.rb'
@@ -1069,8 +1044,6 @@ Layout/ArgumentAlignment:
- 'ee/spec/requests/api/analytics/project_deployment_frequency_spec.rb'
- 'ee/spec/requests/api/api_spec.rb'
- 'ee/spec/requests/api/branches_spec.rb'
- - 'ee/spec/requests/api/ci/jobs_spec.rb'
- - 'ee/spec/requests/api/ci/pipelines_spec.rb'
- 'ee/spec/requests/api/composer_packages_spec.rb'
- 'ee/spec/requests/api/deployments_spec.rb'
- 'ee/spec/requests/api/dora/metrics_spec.rb'
@@ -1148,7 +1121,6 @@ Layout/ArgumentAlignment:
- 'ee/spec/services/security/auto_fix_service_spec.rb'
- 'ee/spec/services/security/findings/dismiss_service_spec.rb'
- 'ee/spec/services/security/ingestion/finding_map_spec.rb'
- - 'ee/spec/services/security/ingestion/tasks/ingest_issue_links_spec.rb'
- 'ee/spec/services/security/ingestion/tasks/ingest_vulnerabilities/create_spec.rb'
- 'ee/spec/services/security/ingestion/tasks/ingest_vulnerabilities_spec.rb'
- 'ee/spec/services/security/ingestion/tasks/update_vulnerability_uuids_spec.rb'
@@ -1192,7 +1164,6 @@ Layout/ArgumentAlignment:
- 'lib/api/commits.rb'
- 'lib/api/concerns/packages/debian_distribution_endpoints.rb'
- 'lib/api/concerns/packages/npm_endpoints.rb'
- - 'lib/api/concerns/packages/nuget_endpoints.rb'
- 'lib/api/container_repositories.rb'
- 'lib/api/dependency_proxy.rb'
- 'lib/api/deploy_keys.rb'
@@ -1257,7 +1228,6 @@ Layout/ArgumentAlignment:
- 'lib/api/metrics/dashboard/annotations.rb'
- 'lib/api/metrics/user_starred_dashboards.rb'
- 'lib/api/milestone_responses.rb'
- - 'lib/api/ml/mlflow.rb'
- 'lib/api/notes.rb'
- 'lib/api/nuget_project_packages.rb'
- 'lib/api/pages.rb'
@@ -1285,15 +1255,12 @@ Layout/ArgumentAlignment:
- 'lib/api/topics.rb'
- 'lib/api/usage_data.rb'
- 'lib/api/users.rb'
- - 'lib/api/v3/github.rb'
- 'lib/backup/manager.rb'
- 'lib/bitbucket_server/connection.rb'
- 'lib/gem_extensions/active_record/disable_joins/associations/association_scope.rb'
- 'lib/generators/gitlab/partitioning/foreign_keys_generator.rb'
- - 'lib/gitlab/alert_management/payload/managed_prometheus.rb'
- 'lib/gitlab/alert_management/payload/prometheus.rb'
- 'lib/gitlab/auth/ldap/adapter.rb'
- - 'lib/gitlab/bitbucket_server_import/importer.rb'
- 'lib/gitlab/chat/command.rb'
- 'lib/gitlab/ci/ansi2json/line.rb'
- 'lib/gitlab/ci/config/entry/environment.rb'
@@ -1315,7 +1282,6 @@ Layout/ArgumentAlignment:
- 'lib/gitlab/conflict/file.rb'
- 'lib/gitlab/cross_project_access.rb'
- 'lib/gitlab/data_builder/push.rb'
- - 'lib/gitlab/database/background_migration/health_status.rb'
- 'lib/gitlab/database/consistency_checker.rb'
- 'lib/gitlab/database/count/reltuples_count_strategy.rb'
- 'lib/gitlab/database/load_balancing/configuration.rb'
@@ -1326,9 +1292,6 @@ Layout/ArgumentAlignment:
- 'lib/gitlab/database/partitioning/partition_manager.rb'
- 'lib/gitlab/database/partitioning/replace_table.rb'
- 'lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb'
- - 'lib/gitlab/database/rename_reserved_paths_migration/v1/migration_classes.rb'
- - 'lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base.rb'
- - 'lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects.rb'
- 'lib/gitlab/diff/diff_refs.rb'
- 'lib/gitlab/diff/file_collection/base.rb'
- 'lib/gitlab/diff/file_collection/paginated_merge_request_diff.rb'
@@ -1354,7 +1317,6 @@ Layout/ArgumentAlignment:
- 'lib/gitlab/github_gists_import/importer/gist_importer.rb'
- 'lib/gitlab/github_import/importer/issue_importer.rb'
- 'lib/gitlab/github_import/importer/pull_request_importer.rb'
- - 'lib/gitlab/github_import/parallel_scheduling.rb'
- 'lib/gitlab/github_import/representation/issue.rb'
- 'lib/gitlab/github_import/representation/issue_event.rb'
- 'lib/gitlab/github_import/representation/note.rb'
@@ -1380,7 +1342,6 @@ Layout/ArgumentAlignment:
- 'lib/gitlab/markdown_cache/redis/store.rb'
- 'lib/gitlab/memory/reports_uploader.rb'
- 'lib/gitlab/memory/watchdog/configurator.rb'
- - 'lib/gitlab/metrics/dashboard/importers/prometheus_metrics.rb'
- 'lib/gitlab/metrics/requests_rack_middleware.rb'
- 'lib/gitlab/other_markup.rb'
- 'lib/gitlab/project_authorizations.rb'
@@ -1416,7 +1377,6 @@ Layout/ArgumentAlignment:
- 'spec/components/previews/pajamas/banner_component_preview.rb'
- 'spec/components/previews/pajamas/button_component_preview.rb'
- 'spec/graphql/features/authorization_spec.rb'
- - 'spec/initializers/00_rails_disable_joins_spec.rb'
- 'spec/initializers/secret_token_spec.rb'
- 'spec/lib/api/every_api_endpoint_spec.rb'
- 'spec/lib/atlassian/jira_connect/client_spec.rb'
@@ -1431,7 +1391,6 @@ Layout/ArgumentAlignment:
- 'spec/lib/feature/gitaly_spec.rb'
- 'spec/lib/feature_spec.rb'
- 'spec/lib/generators/gitlab/partitioning/foreign_keys_generator_spec.rb'
- - 'spec/lib/gitlab/alert_management/payload/managed_prometheus_spec.rb'
- 'spec/lib/gitlab/alert_management/payload/prometheus_spec.rb'
- 'spec/lib/gitlab/analytics/cycle_analytics/aggregated/base_query_builder_spec.rb'
- 'spec/lib/gitlab/analytics/date_filler_spec.rb'
@@ -1442,46 +1401,13 @@ Layout/ArgumentAlignment:
- 'spec/lib/gitlab/auth/otp/strategies/forti_token_cloud_spec.rb'
- 'spec/lib/gitlab/auth/saml/auth_hash_spec.rb'
- 'spec/lib/gitlab/auth/saml/user_spec.rb'
- - 'spec/lib/gitlab/background_migration/batched_migration_job_spec.rb'
- - 'spec/lib/gitlab/background_migration/copy_column_using_background_migration_job_spec.rb'
- - 'spec/lib/gitlab/background_migration/delete_orphaned_operational_vulnerabilities_spec.rb'
- - 'spec/lib/gitlab/background_migration/delete_orphans_approval_merge_request_rules_spec.rb'
- - 'spec/lib/gitlab/background_migration/delete_orphans_approval_project_rules_spec.rb'
- - 'spec/lib/gitlab/background_migration/destroy_invalid_group_members_spec.rb'
- - 'spec/lib/gitlab/background_migration/destroy_invalid_members_spec.rb'
- - 'spec/lib/gitlab/background_migration/destroy_invalid_project_members_spec.rb'
- - 'spec/lib/gitlab/background_migration/disable_legacy_open_source_licence_for_recent_public_projects_spec.rb'
- - 'spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_inactive_public_projects_spec.rb'
- - 'spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_no_issues_no_repo_projects_spec.rb'
- - 'spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_one_member_no_repo_projects_spec.rb'
- - 'spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_projects_less_than_five_mb_spec.rb'
- - 'spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_projects_less_than_one_mb_spec.rb'
- - 'spec/lib/gitlab/background_migration/expire_o_auth_tokens_spec.rb'
- - 'spec/lib/gitlab/background_migration/fix_incoherent_packages_size_on_project_statistics_spec.rb'
- - 'spec/lib/gitlab/background_migration/legacy_upload_mover_spec.rb'
- - 'spec/lib/gitlab/background_migration/migrate_shimo_confluence_integration_category_spec.rb'
- 'spec/lib/gitlab/bitbucket_import/importer_spec.rb'
- 'spec/lib/gitlab/bitbucket_import/project_creator_spec.rb'
- 'spec/lib/gitlab/bitbucket_import/wiki_formatter_spec.rb'
- - 'spec/lib/gitlab/bitbucket_server_import/importer_spec.rb'
- 'spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb'
- 'spec/lib/gitlab/changelog/config_spec.rb'
- 'spec/lib/gitlab/checks/changes_access_spec.rb'
- 'spec/lib/gitlab/checks/single_change_access_spec.rb'
- - 'spec/lib/gitlab/ci/badge/pipeline/status_spec.rb'
- - 'spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb'
- - 'spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb'
- - 'spec/lib/gitlab/ci/build/hook_spec.rb'
- - 'spec/lib/gitlab/ci/build/policy/changes_spec.rb'
- - 'spec/lib/gitlab/ci/build/policy/variables_spec.rb'
- - 'spec/lib/gitlab/ci/build/rules/rule/clause/changes_spec.rb'
- - 'spec/lib/gitlab/ci/config/entry/bridge_spec.rb'
- - 'spec/lib/gitlab/ci/config/entry/job_spec.rb'
- - 'spec/lib/gitlab/ci/config/entry/policy_spec.rb'
- - 'spec/lib/gitlab/ci/config/extendable/entry_spec.rb'
- - 'spec/lib/gitlab/ci/config/external/mapper_spec.rb'
- - 'spec/lib/gitlab/ci/config/external/rules_spec.rb'
- - 'spec/lib/gitlab/ci/parsers/security/common_spec.rb'
- 'spec/lib/gitlab/ci/pipeline/chain/command_spec.rb'
- 'spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb'
- 'spec/lib/gitlab/ci/pipeline/chain/skip_spec.rb'
@@ -1502,35 +1428,8 @@ Layout/ArgumentAlignment:
- 'spec/lib/gitlab/cross_project_access/class_methods_spec.rb'
- 'spec/lib/gitlab/cross_project_access_spec.rb'
- 'spec/lib/gitlab/data_builder/push_spec.rb'
- - 'spec/lib/gitlab/database/background_migration/batched_migration_spec.rb'
- - 'spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb'
- - 'spec/lib/gitlab/database/background_migration/prometheus_metrics_spec.rb'
- - 'spec/lib/gitlab/database/bulk_update_spec.rb'
- - 'spec/lib/gitlab/database/loose_foreign_keys_spec.rb'
- 'spec/lib/gitlab/database/migration_helpers_spec.rb'
- - 'spec/lib/gitlab/database/migrations/constraints_helpers_spec.rb'
- - 'spec/lib/gitlab/database/migrations/instrumentation_spec.rb'
- - 'spec/lib/gitlab/database/migrations/test_batched_background_runner_spec.rb'
- - 'spec/lib/gitlab/database/partitioning/detached_partition_dropper_spec.rb'
- - 'spec/lib/gitlab/database/partitioning/monthly_strategy_spec.rb'
- - 'spec/lib/gitlab/database/partitioning/sliding_list_strategy_spec.rb'
- - 'spec/lib/gitlab/database/partitioning_migration_helpers/index_helpers_spec.rb'
- - 'spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb'
- - 'spec/lib/gitlab/database/postgres_constraint_spec.rb'
- - 'spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb'
- - 'spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb'
- - 'spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb'
- - 'spec/lib/gitlab/database/tables_truncate_spec.rb'
- 'spec/lib/gitlab/dependency_linker/parser/gemfile_spec.rb'
- - 'spec/lib/gitlab/diff/file_collection/compare_spec.rb'
- - 'spec/lib/gitlab/diff/file_collection/merge_request_diff_batch_spec.rb'
- - 'spec/lib/gitlab/diff/file_collection/paginated_merge_request_diff_spec.rb'
- - 'spec/lib/gitlab/diff/file_spec.rb'
- - 'spec/lib/gitlab/diff/highlight_cache_spec.rb'
- - 'spec/lib/gitlab/diff/line_spec.rb'
- - 'spec/lib/gitlab/diff/suggestion_diff_spec.rb'
- - 'spec/lib/gitlab/diff/suggestion_spec.rb'
- - 'spec/lib/gitlab/diff/suggestions_parser_spec.rb'
- 'spec/lib/gitlab/email/hook/delivery_metrics_observer_spec.rb'
- 'spec/lib/gitlab/email/hook/smime_signature_interceptor_spec.rb'
- 'spec/lib/gitlab/error_tracking/processor/context_payload_processor_spec.rb'
@@ -1552,7 +1451,6 @@ Layout/ArgumentAlignment:
- 'spec/lib/gitlab/github_import/importer/issue_event_importer_spec.rb'
- 'spec/lib/gitlab/github_import/importer/labels_importer_spec.rb'
- 'spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb'
- - 'spec/lib/gitlab/github_import/importer/pull_request_review_importer_spec.rb'
- 'spec/lib/gitlab/graphql/query_analyzers/ast/logger_analyzer_spec.rb'
- 'spec/lib/gitlab/health_checks/redis_spec.rb'
- 'spec/lib/gitlab/i18n/po_linter_spec.rb'
@@ -1569,8 +1467,6 @@ Layout/ArgumentAlignment:
- 'spec/lib/gitlab/pagination_delegate_spec.rb'
- 'spec/lib/gitlab/path_regex_spec.rb'
- 'spec/lib/gitlab/profiler_spec.rb'
- - 'spec/lib/gitlab/prometheus/queries/additional_metrics_deployment_query_spec.rb'
- - 'spec/lib/gitlab/prometheus/queries/deployment_query_spec.rb'
- 'spec/lib/gitlab/redis/sidekiq_status_spec.rb'
- 'spec/lib/gitlab/repository_cache/preloader_spec.rb'
- 'spec/lib/gitlab/repository_cache_spec.rb'
@@ -1592,7 +1488,6 @@ Layout/ArgumentAlignment:
- 'spec/lib/gitlab/workhorse_spec.rb'
- 'spec/lib/google_api/cloud_platform/client_spec.rb'
- 'spec/lib/peek/views/detailed_view_spec.rb'
- - 'spec/lib/peek/views/rugged_spec.rb'
- 'spec/lib/security/weak_passwords_spec.rb'
- 'spec/lib/sidebars/projects/menus/repository_menu_spec.rb'
- 'spec/lib/uploaded_file_spec.rb'
@@ -1604,15 +1499,6 @@ Layout/ArgumentAlignment:
- 'spec/requests/api/api_spec.rb'
- 'spec/requests/api/badges_spec.rb'
- 'spec/requests/api/branches_spec.rb'
- - 'spec/requests/api/ci/job_artifacts_spec.rb'
- - 'spec/requests/api/ci/jobs_spec.rb'
- - 'spec/requests/api/ci/pipelines_spec.rb'
- - 'spec/requests/api/ci/resource_groups_spec.rb'
- - 'spec/requests/api/ci/runner/jobs_artifacts_spec.rb'
- - 'spec/requests/api/ci/runner/jobs_request_post_spec.rb'
- - 'spec/requests/api/ci/runner/jobs_trace_spec.rb'
- - 'spec/requests/api/ci/runner/runners_post_spec.rb'
- - 'spec/requests/api/ci/runners_spec.rb'
- 'spec/requests/api/clusters/agent_tokens_spec.rb'
- 'spec/requests/api/clusters/agents_spec.rb'
- 'spec/requests/api/commit_statuses_spec.rb'
@@ -1631,43 +1517,10 @@ Layout/ArgumentAlignment:
- 'spec/requests/api/graphql/gitlab_schema_spec.rb'
- 'spec/requests/api/graphql/group/group_members_spec.rb'
- 'spec/requests/api/graphql/milestone_spec.rb'
- - 'spec/requests/api/graphql/mutations/admin/sidekiq_queues/delete_jobs_spec.rb'
- - 'spec/requests/api/graphql/mutations/alert_management/alerts/create_alert_issue_spec.rb'
- - 'spec/requests/api/graphql/mutations/boards/issues/issue_move_list_spec.rb'
- - 'spec/requests/api/graphql/mutations/ci/pipeline_retry_spec.rb'
- - 'spec/requests/api/graphql/mutations/clusters/agent_tokens/agent_tokens/create_spec.rb'
- - 'spec/requests/api/graphql/mutations/clusters/agents/delete_spec.rb'
- 'spec/requests/api/graphql/mutations/container_expiration_policy/update_spec.rb'
- - 'spec/requests/api/graphql/mutations/design_management/upload_spec.rb'
- - 'spec/requests/api/graphql/mutations/issues/link_alerts_spec.rb'
- - 'spec/requests/api/graphql/mutations/issues/move_spec.rb'
- - 'spec/requests/api/graphql/mutations/issues/set_confidential_spec.rb'
- - 'spec/requests/api/graphql/mutations/issues/set_crm_contacts_spec.rb'
- - 'spec/requests/api/graphql/mutations/issues/set_due_date_spec.rb'
- - 'spec/requests/api/graphql/mutations/issues/set_locked_spec.rb'
- - 'spec/requests/api/graphql/mutations/issues/set_severity_spec.rb'
- - 'spec/requests/api/graphql/mutations/issues/unlink_alerts_spec.rb'
- 'spec/requests/api/graphql/mutations/jira_import/import_users_spec.rb'
- 'spec/requests/api/graphql/mutations/jira_import/start_spec.rb'
- - 'spec/requests/api/graphql/mutations/merge_requests/reviewer_rereview_spec.rb'
- - 'spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb'
- - 'spec/requests/api/graphql/mutations/merge_requests/set_draft_spec.rb'
- - 'spec/requests/api/graphql/mutations/merge_requests/set_labels_spec.rb'
- - 'spec/requests/api/graphql/mutations/merge_requests/set_locked_spec.rb'
- - 'spec/requests/api/graphql/mutations/merge_requests/set_milestone_spec.rb'
- - 'spec/requests/api/graphql/mutations/merge_requests/set_reviewers_spec.rb'
- 'spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb'
- - 'spec/requests/api/graphql/mutations/notes/update/image_diff_note_spec.rb'
- - 'spec/requests/api/graphql/mutations/packages/cleanup/policy/update_spec.rb'
- - 'spec/requests/api/graphql/mutations/release_asset_links/update_spec.rb'
- - 'spec/requests/api/graphql/mutations/snippets/create_spec.rb'
- - 'spec/requests/api/graphql/mutations/snippets/destroy_spec.rb'
- - 'spec/requests/api/graphql/mutations/snippets/mark_as_spam_spec.rb'
- - 'spec/requests/api/graphql/mutations/snippets/update_spec.rb'
- - 'spec/requests/api/graphql/mutations/todos/mark_all_done_spec.rb'
- - 'spec/requests/api/graphql/mutations/todos/mark_done_spec.rb'
- - 'spec/requests/api/graphql/mutations/todos/restore_many_spec.rb'
- - 'spec/requests/api/graphql/mutations/todos/restore_spec.rb'
- 'spec/requests/api/graphql/packages/conan_spec.rb'
- 'spec/requests/api/graphql/tasks/task_completion_status_spec.rb'
- 'spec/requests/api/graphql/user_query_spec.rb'
@@ -1686,7 +1539,6 @@ Layout/ArgumentAlignment:
- 'spec/requests/api/labels_spec.rb'
- 'spec/requests/api/members_spec.rb'
- 'spec/requests/api/merge_requests_spec.rb'
- - 'spec/requests/api/ml/mlflow_spec.rb'
- 'spec/requests/api/notes_spec.rb'
- 'spec/requests/api/nuget_group_packages_spec.rb'
- 'spec/requests/api/oauth_tokens_spec.rb'
diff --git a/.rubocop_todo/layout/array_alignment.yml b/.rubocop_todo/layout/array_alignment.yml
index 8d040f5738e..cb35ad520f0 100644
--- a/.rubocop_todo/layout/array_alignment.yml
+++ b/.rubocop_todo/layout/array_alignment.yml
@@ -54,7 +54,6 @@ Layout/ArrayAlignment:
- 'ee/app/controllers/projects/push_rules_controller.rb'
- 'ee/app/finders/autocomplete/project_invited_groups_finder.rb'
- 'ee/app/finders/ee/issues_finder/params.rb'
- - 'ee/app/finders/geo/project_registry_finder.rb'
- 'ee/app/graphql/ee/resolvers/project_pipelines_resolver.rb'
- 'ee/app/models/concerns/geo/verification_state.rb'
- 'ee/app/models/dast_site_profile.rb'
@@ -80,7 +79,6 @@ Layout/ArrayAlignment:
- 'ee/spec/features/boards/boards_licensed_features_spec.rb'
- 'ee/spec/features/groups/analytics/cycle_analytics/charts_spec.rb'
- 'ee/spec/features/groups/group_roadmap_spec.rb'
- - 'ee/spec/finders/namespaces/billed_users_finder_spec.rb'
- 'ee/spec/finders/security/pipeline_vulnerabilities_finder_spec.rb'
- 'ee/spec/frontend/fixtures/search.rb'
- 'ee/spec/graphql/resolvers/analytics/contribution_analytics/contributions_resolver_spec.rb'
@@ -110,7 +108,6 @@ Layout/ArrayAlignment:
- 'ee/spec/models/dora/daily_metrics_spec.rb'
- 'ee/spec/models/ee/group_spec.rb'
- 'ee/spec/models/ee/project_spec.rb'
- - 'ee/spec/models/ee/protected_ref_access_spec.rb'
- 'ee/spec/models/issue_spec.rb'
- 'ee/spec/models/repository_spec.rb'
- 'ee/spec/models/security/orchestration_policy_rule_schedule_spec.rb'
@@ -183,14 +180,12 @@ Layout/ArrayAlignment:
- 'lib/gitlab/visibility_level.rb'
- 'lib/kramdown/parser/atlassian_document_format.rb'
- 'lib/tasks/cache.rake'
- - 'qa/qa/specs/features/browser_ui/3_create/pages/new_static_page_spec.rb'
- 'qa/qa/specs/features/browser_ui/5_package/package_registry/composer_registry_spec.rb'
- 'qa/qa/specs/features/browser_ui/5_package/package_registry/generic_repository_spec.rb'
- 'qa/qa/specs/features/browser_ui/5_package/package_registry/npm/npm_instance_level_spec.rb'
- 'qa/qa/specs/features/browser_ui/5_package/package_registry/npm/npm_project_level_spec.rb'
- 'qa/qa/specs/features/browser_ui/5_package/package_registry/pypi_repository_spec.rb'
- 'qa/qa/specs/features/ee/browser_ui/10_govern/group/group_audit_logs_2_spec.rb'
- - 'qa/qa/specs/features/ee/browser_ui/1_manage/group/group_ldap_sync_spec.rb'
- 'qa/spec/specs/runner_spec.rb'
- 'rubocop/cop/gitlab/rspec/avoid_setup.rb'
- 'rubocop/cop/graphql/authorize_types.rb'
@@ -283,7 +278,6 @@ Layout/ArrayAlignment:
- 'spec/lib/gitlab/markup_helper_spec.rb'
- 'spec/lib/gitlab/pagination/keyset/in_operator_optimization/query_builder_spec.rb'
- 'spec/lib/gitlab/patch/prependable_spec.rb'
- - 'spec/lib/gitlab/prometheus/queries/matched_metric_query_spec.rb'
- 'spec/lib/gitlab/reference_extractor_spec.rb'
- 'spec/lib/gitlab/serializer/ci/variables_spec.rb'
- 'spec/lib/gitlab/sidekiq_config/worker_spec.rb'
@@ -303,7 +297,6 @@ Layout/ArrayAlignment:
- 'spec/models/concerns/issuable_spec.rb'
- 'spec/models/design_management/version_spec.rb'
- 'spec/models/discussion_spec.rb'
- - 'spec/models/external_pull_request_spec.rb'
- 'spec/models/group_group_link_spec.rb'
- 'spec/models/incident_management/timeline_event_tag_spec.rb'
- 'spec/models/integrations/irker_spec.rb'
diff --git a/.rubocop_todo/layout/empty_line_after_magic_comment.yml b/.rubocop_todo/layout/empty_line_after_magic_comment.yml
index 2fbb94bf41b..0bfdf721a2b 100644
--- a/.rubocop_todo/layout/empty_line_after_magic_comment.yml
+++ b/.rubocop_todo/layout/empty_line_after_magic_comment.yml
@@ -4,9 +4,7 @@ Layout/EmptyLineAfterMagicComment:
Exclude:
- 'app/controllers/admin/instance_review_controller.rb'
- 'app/controllers/concerns/render_access_tokens.rb'
- - 'app/controllers/groups/observability_controller.rb'
- 'app/controllers/groups/registry/repositories_controller.rb'
- - 'app/controllers/projects/metrics_dashboard_controller.rb'
- 'app/finders/ci/auth_job_finder.rb'
- 'app/finders/clusters/knative_services_finder.rb'
- 'app/finders/keys_finder.rb'
@@ -19,13 +17,8 @@ Layout/EmptyLineAfterMagicComment:
- 'app/graphql/resolvers/commit_pipelines_resolver.rb'
- 'app/graphql/resolvers/group_packages_resolver.rb'
- 'app/graphql/resolvers/merge_request_pipelines_resolver.rb'
- - 'app/graphql/resolvers/project_members_resolver.rb'
- - 'app/graphql/resolvers/project_milestones_resolver.rb'
- 'app/graphql/resolvers/project_packages_resolver.rb'
- 'app/graphql/resolvers/project_pipelines_resolver.rb'
- - 'app/graphql/resolvers/projects/snippets_resolver.rb'
- - 'app/graphql/resolvers/snippets_resolver.rb'
- - 'app/graphql/resolvers/users/snippets_resolver.rb'
- 'app/graphql/types/access_level_type.rb'
- 'app/graphql/types/ci/detailed_status_type.rb'
- 'app/graphql/types/ci/status_action_type.rb'
@@ -102,7 +95,6 @@ Layout/EmptyLineAfterMagicComment:
- 'app/services/merge_requests/mergeability/check_ci_status_service.rb'
- 'app/services/merge_requests/mergeability/check_discussions_status_service.rb'
- 'app/services/merge_requests/mergeability/run_checks_service.rb'
- - 'app/services/metrics/dashboard/cluster_metrics_embed_service.rb'
- 'app/services/packages/create_dependency_service.rb'
- 'app/services/packages/create_package_file_service.rb'
- 'app/services/packages/maven/create_package_service.rb'
@@ -148,8 +140,6 @@ Layout/EmptyLineAfterMagicComment:
- 'db/migrate/20221219103007_add_name_to_ml_candidates.rb'
- 'db/migrate/20221219122320_copy_clickhouse_connection_string_to_encrypted_var.rb'
- 'db/migrate/20230111124512_remove_tmp_index_vulns_on_report_type.rb'
- - 'db/post_migrate/20220412143551_add_partial_index_on_unencrypted_integrations.rb'
- - 'db/post_migrate/20220413011328_remove_partial_index_on_unencrypted_integrations.rb'
- 'db/post_migrate/20220901071355_cleanup_attention_request_user_callouts.rb'
- 'db/post_migrate/20220929091500_add_tmp_index_vulns_on_report_type.rb'
- 'db/post_migrate/20221004094814_schedule_destroy_invalid_members.rb'
@@ -171,11 +161,9 @@ Layout/EmptyLineAfterMagicComment:
- 'ee/app/controllers/projects/protected_environments_controller.rb'
- 'ee/app/finders/groups_with_templates_finder.rb'
- 'ee/app/finders/status_page/incidents_finder.rb'
- - 'ee/app/graphql/ee/types/clusters/agent_type.rb'
- 'ee/app/graphql/ee/types/repository/blob_type.rb'
- 'ee/app/graphql/types/analytics/devops_adoption/enabled_namespace_type.rb'
- 'ee/app/graphql/types/analytics/devops_adoption/snapshot_type.rb'
- - 'ee/app/graphql/types/compliance_management/compliance_framework_type.rb'
- 'ee/app/graphql/types/path_lock_type.rb'
- 'ee/app/graphql/types/product_analytics/panel_type.rb'
- 'ee/app/graphql/types/timebox_error_type.rb'
@@ -201,9 +189,7 @@ Layout/EmptyLineAfterMagicComment:
- 'ee/app/models/geo/ci_secure_file_state.rb'
- 'ee/app/models/project_security_setting.rb'
- 'ee/app/models/protected_environment.rb'
- - 'ee/app/models/sbom/vulnerable_component_version.rb'
- 'ee/app/models/vulnerabilities/merge_request_link.rb'
- - 'ee/app/policies/ee/ci/build_policy.rb'
- 'ee/app/policies/ee/environment_policy.rb'
- 'ee/app/policies/security/finding_policy.rb'
- 'ee/app/policies/vulnerabilities/finding_policy.rb'
@@ -239,12 +225,10 @@ Layout/EmptyLineAfterMagicComment:
- 'ee/app/services/protected_environments/environment_dropdown_service.rb'
- 'ee/app/services/protected_environments/search_service.rb'
- 'ee/app/services/protected_environments/update_service.rb'
- - 'ee/app/services/users/captcha_challenge_service.rb'
- 'ee/app/services/vulnerabilities/manually_create_service.rb'
- 'ee/db/fixtures/development/25_downstream_pipelines.rb'
- 'ee/db/geo/migrate/20220617125507_create_ci_secure_file_registry.rb'
- 'ee/lib/compliance_management/merge_request_approval_settings/resolver.rb'
- - 'ee/lib/ee/api/internal/kubernetes.rb'
- 'ee/lib/ee/gitlab/ci/parsers/security/validators/schema_validator.rb'
- 'ee/lib/ee/gitlab/hook_data/group_member_builder.rb'
- 'ee/lib/ee/gitlab/hook_data/issue_builder.rb'
@@ -281,7 +265,6 @@ Layout/EmptyLineAfterMagicComment:
- 'ee/spec/features/projects/settings/merge_request_approvals_settings_spec.rb'
- 'ee/spec/features/projects/settings/merge_requests_settings_spec.rb'
- 'ee/spec/finders/auth/group_saml_identity_finder_spec.rb'
- - 'ee/spec/finders/geo/project_registry_status_finder_spec.rb'
- 'ee/spec/frontend/fixtures/analytics/charts.rb'
- 'ee/spec/frontend/fixtures/analytics/metrics.rb'
- 'ee/spec/frontend/fixtures/analytics/value_streams.rb'
@@ -303,7 +286,6 @@ Layout/EmptyLineAfterMagicComment:
- 'ee/spec/graphql/mutations/vulnerabilities/create_spec.rb'
- 'ee/spec/graphql/mutations/vulnerabilities/destroy_external_issue_link_spec.rb'
- 'ee/spec/graphql/mutations/vulnerabilities/dismiss_spec.rb'
- - 'ee/spec/graphql/mutations/vulnerabilities/finding/dismiss_spec.rb'
- 'ee/spec/graphql/mutations/vulnerabilities/resolve_spec.rb'
- 'ee/spec/graphql/mutations/vulnerabilities/revert_to_detected_spec.rb'
- 'ee/spec/helpers/ee/auth_helper_spec.rb'
@@ -316,7 +298,6 @@ Layout/EmptyLineAfterMagicComment:
- 'ee/spec/lib/ee/api/helpers/members_helpers_spec.rb'
- 'ee/spec/lib/ee/gitlab/background_migration/backfill_epic_cache_counts_spec.rb'
- 'ee/spec/lib/ee/gitlab/background_migration/migrate_shared_vulnerability_scanners_spec.rb'
- - 'ee/spec/lib/ee/gitlab/database/gitlab_schema_spec.rb'
- 'ee/spec/lib/ee/gitlab/git_access_design_spec.rb'
- 'ee/spec/lib/ee/gitlab/git_access_snippet_spec.rb'
- 'ee/spec/lib/ee/gitlab/hook_data/group_member_builder_spec.rb'
@@ -349,15 +330,12 @@ Layout/EmptyLineAfterMagicComment:
- 'ee/spec/models/protected_environment_spec.rb'
- 'ee/spec/models/protected_environments/approval_rule_spec.rb'
- 'ee/spec/models/protected_environments/deploy_access_level_spec.rb'
- - 'ee/spec/models/sbom/vulnerable_component_version_spec.rb'
- - 'ee/spec/models/vulnerabilities/advisory_spec.rb'
- 'ee/spec/models/vulnerabilities/finding_spec.rb'
- 'ee/spec/models/work_items/progress_spec.rb'
- 'ee/spec/policies/app_sec/fuzzing/coverage/corpus_policy_spec.rb'
- 'ee/spec/policies/ci/build_policy_spec.rb'
- 'ee/spec/policies/deployment_policy_spec.rb'
- 'ee/spec/policies/environment_policy_spec.rb'
- - 'ee/spec/policies/identity_provider_policy_spec.rb'
- 'ee/spec/policies/path_lock_policy_spec.rb'
- 'ee/spec/policies/saml_provider_policy_spec.rb'
- 'ee/spec/presenters/ci/build_presenter_spec.rb'
@@ -403,7 +381,6 @@ Layout/EmptyLineAfterMagicComment:
- 'ee/spec/services/wikis/create_attachment_service_spec.rb'
- 'ee/spec/support/helpers/board_helpers.rb'
- 'lib/api/commits.rb'
- - 'lib/api/concerns/packages/nuget_endpoints.rb'
- 'lib/api/go_proxy.rb'
- 'lib/api/integrations.rb'
- 'lib/api/maven_packages.rb'
@@ -453,7 +430,6 @@ Layout/EmptyLineAfterMagicComment:
- 'lib/security/report_schema_version_matcher.rb'
- 'lib/security/weak_passwords.rb'
- 'lib/tasks/gitlab/docs/redirect.rake'
- - 'lib/tasks/gitlab/metrics_exporter.rake'
- 'lib/tasks/gitlab/password.rake'
- 'lib/tasks/gitlab/security/update_banned_ssh_keys.rake'
- 'qa/qa/ee/runtime/saml.rb'
@@ -463,7 +439,6 @@ Layout/EmptyLineAfterMagicComment:
- 'qa/qa/runtime/mail_hog.rb'
- 'qa/qa/specs/features/browser_ui/3_create/source_editor/source_editor_toolbar_spec.rb'
- 'qa/qa/specs/features/ee/api/7_configure/kubernetes/kubernetes_agent_spec.rb'
- - 'qa/qa/specs/features/ee/browser_ui/1_manage/ldap/admin_ldap_sync_spec.rb'
- 'qa/qa/specs/features/ee/browser_ui/4_verify/job_trace_archival_spec.rb'
- 'qa/qa/support/otp.rb'
- 'qa/qa/support/repeater.rb'
@@ -539,7 +514,6 @@ Layout/EmptyLineAfterMagicComment:
- 'spec/lib/gitlab/analytics/date_filler_spec.rb'
- 'spec/lib/gitlab/background_migration/legacy_upload_mover_spec.rb'
- 'spec/lib/gitlab/background_migration/legacy_uploads_migrator_spec.rb'
- - 'spec/lib/gitlab/background_migration/remove_occurrence_pipelines_and_duplicate_vulnerabilities_findings_spec.rb'
- 'spec/lib/gitlab/class_attributes_spec.rb'
- 'spec/lib/gitlab/cleanup/remote_uploads_spec.rb'
- 'spec/lib/gitlab/conan_token_spec.rb'
@@ -573,7 +547,6 @@ Layout/EmptyLineAfterMagicComment:
- 'spec/lib/gitlab/markdown_cache/active_record/extension_spec.rb'
- 'spec/lib/gitlab/markdown_cache/redis/extension_spec.rb'
- 'spec/lib/gitlab/markdown_cache/redis/store_spec.rb'
- - 'spec/lib/gitlab/metrics/dashboard/cache_spec.rb'
- 'spec/lib/gitlab/metrics/environment_spec.rb'
- 'spec/lib/gitlab/metrics/rails_slis_spec.rb'
- 'spec/lib/gitlab/middleware/handle_malformed_strings_spec.rb'
@@ -587,8 +560,6 @@ Layout/EmptyLineAfterMagicComment:
- 'spec/lib/gitlab/x509/commit_spec.rb'
- 'spec/lib/gitlab/x509/tag_spec.rb'
- 'spec/lib/security/report_schema_version_matcher_spec.rb'
- - 'spec/migrations/20220322132242_update_pages_onboarding_state_spec.rb'
- - 'spec/migrations/20220329175119_remove_leftover_ci_job_artifact_deletions_spec.rb'
- 'spec/models/analytics/cycle_analytics/aggregation_spec.rb'
- 'spec/models/concerns/loose_index_scan_spec.rb'
- 'spec/models/dependency_proxy/blob_spec.rb'
diff --git a/.rubocop_todo/layout/first_array_element_indentation.yml b/.rubocop_todo/layout/first_array_element_indentation.yml
index 05f12783fc6..7d233e83e5e 100644
--- a/.rubocop_todo/layout/first_array_element_indentation.yml
+++ b/.rubocop_todo/layout/first_array_element_indentation.yml
@@ -10,7 +10,6 @@ Layout/FirstArrayElementIndentation:
- 'app/models/user.rb'
- 'app/services/labels/transfer_service.rb'
- 'ee/app/finders/autocomplete/project_invited_groups_finder.rb'
- - 'ee/app/finders/geo/project_registry_finder.rb'
- 'ee/app/models/ee/application_setting.rb'
- 'ee/app/models/protected_environment.rb'
- 'ee/app/services/vulnerabilities/create_service_base.rb'
@@ -20,7 +19,6 @@ Layout/FirstArrayElementIndentation:
- 'ee/spec/features/boards/boards_licensed_features_spec.rb'
- 'ee/spec/features/groups/analytics/cycle_analytics/charts_spec.rb'
- 'ee/spec/features/groups/group_roadmap_spec.rb'
- - 'ee/spec/finders/namespaces/billed_users_finder_spec.rb'
- 'ee/spec/frontend/fixtures/dast_profiles.rb'
- 'ee/spec/frontend/fixtures/search.rb'
- 'ee/spec/graphql/resolvers/analytics/contribution_analytics/contributions_resolver_spec.rb'
@@ -48,7 +46,6 @@ Layout/FirstArrayElementIndentation:
- 'lib/gitlab/project_authorizations.rb'
- 'qa/qa/specs/features/api/12_systems/gitaly/automatic_failover_and_recovery_spec.rb'
- 'qa/qa/specs/features/api/12_systems/gitaly/changing_repository_storage_spec.rb'
- - 'qa/qa/specs/features/api/12_systems/gitaly/praefect_dataloss_spec.rb'
- 'qa/qa/specs/features/browser_ui/5_package/infrastructure_registry/terraform_module_registry_spec.rb'
- 'qa/qa/specs/features/browser_ui/5_package/package_registry/npm/npm_instance_level_spec.rb'
- 'qa/qa/specs/features/browser_ui/5_package/package_registry/npm/npm_project_level_spec.rb'
@@ -69,7 +66,6 @@ Layout/FirstArrayElementIndentation:
- 'spec/lib/gitlab/diff/inline_diff_spec.rb'
- 'spec/lib/gitlab/github_import/parallel_scheduling_spec.rb'
- 'spec/lib/gitlab/pagination/keyset/in_operator_optimization/query_builder_spec.rb'
- - 'spec/lib/gitlab/prometheus/queries/matched_metric_query_spec.rb'
- 'spec/lib/gitlab/usage_data/topology_spec.rb'
- 'spec/models/group_group_link_spec.rb'
- 'spec/models/project_group_link_spec.rb'
diff --git a/.rubocop_todo/layout/first_hash_element_indentation.yml b/.rubocop_todo/layout/first_hash_element_indentation.yml
index 893a6d2242b..14ec88a2afe 100644
--- a/.rubocop_todo/layout/first_hash_element_indentation.yml
+++ b/.rubocop_todo/layout/first_hash_element_indentation.yml
@@ -33,7 +33,6 @@ Layout/FirstHashElementIndentation:
- 'app/serializers/detailed_status_entity.rb'
- 'app/services/audit_events/build_service.rb'
- 'app/services/spam/ham_service.rb'
- - 'app/services/work_items/widgets/milestone_service/base_service.rb'
- 'app/validators/bytesize_validator.rb'
- 'ee/app/components/namespaces/free_user_cap/enforcement_alert_component.rb'
- 'ee/app/graphql/mutations/boards/epic_lists/destroy.rb'
@@ -99,7 +98,6 @@ Layout/FirstHashElementIndentation:
- 'ee/spec/lib/gitlab/graphql/aggregations/epics/lazy_epic_aggregate_spec.rb'
- 'ee/spec/mailers/credentials_inventory_mailer_spec.rb'
- 'ee/spec/mailers/emails/requirements_spec.rb'
- - 'ee/spec/models/concerns/elastic/issue_spec.rb'
- 'ee/spec/models/concerns/elastic/note_spec.rb'
- 'ee/spec/requests/api/analytics/project_deployment_frequency_spec.rb'
- 'ee/spec/requests/api/graphql/mutations/iterations/create_spec.rb'
@@ -143,10 +141,7 @@ Layout/FirstHashElementIndentation:
- 'qa/qa/resource/snippet.rb'
- 'qa/qa/specs/features/api/1_manage/migration/gitlab_migration_release_spec.rb'
- 'qa/qa/specs/features/api/3_create/repository/commit_to_templated_project_spec.rb'
- - 'qa/qa/specs/features/api/5_package/container_registry_spec.rb'
- 'qa/qa/specs/features/browser_ui/3_create/web_ide_old/open_web_ide_from_diff_tab_spec.rb'
- - 'qa/qa/specs/features/browser_ui/5_package/container_registry/container_registry_spec.rb'
- - 'qa/qa/specs/features/browser_ui/5_package/container_registry/online_garbage_collection_spec.rb'
- 'qa/qa/specs/features/browser_ui/5_package/dependency_proxy/dependency_proxy_spec.rb'
- 'qa/qa/specs/features/browser_ui/5_package/package_registry/composer_registry_spec.rb'
- 'qa/qa/specs/features/browser_ui/5_package/package_registry/conan_repository_spec.rb'
@@ -154,7 +149,6 @@ Layout/FirstHashElementIndentation:
- 'qa/qa/specs/features/browser_ui/5_package/package_registry/nuget/nuget_group_level_spec.rb'
- 'qa/qa/specs/features/browser_ui/5_package/package_registry/nuget/nuget_project_level_spec.rb'
- 'qa/qa/specs/features/browser_ui/5_package/package_registry/pypi_repository_spec.rb'
- - 'qa/qa/specs/features/ee/browser_ui/4_verify/new_discussion_not_dropping_merge_trains_mr_spec.rb'
- 'qa/qa/specs/features/ee/browser_ui/5_package/dependency_proxy_sso_spec.rb'
- 'qa/spec/support/formatters/test_metrics_formatter_spec.rb'
- 'spec/controllers/concerns/issuable_collections_spec.rb'
@@ -181,7 +175,6 @@ Layout/FirstHashElementIndentation:
- 'spec/factories/ci/builds.rb'
- 'spec/frontend/fixtures/autocomplete_sources.rb'
- 'spec/graphql/types/ci/detailed_status_type_spec.rb'
- - 'spec/helpers/groups/observability_helper_spec.rb'
- 'spec/helpers/projects/pages_helper_spec.rb'
- 'spec/helpers/routing/pseudonymization_helper_spec.rb'
- 'spec/initializers/rack_multipart_patch_spec.rb'
@@ -201,7 +194,6 @@ Layout/FirstHashElementIndentation:
- 'spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb'
- 'spec/lib/gitlab/ci/pipeline/seed/build/cache_spec.rb'
- 'spec/lib/gitlab/ci/yaml_processor_spec.rb'
- - 'spec/lib/gitlab/config_checker/puma_rugged_checker_spec.rb'
- 'spec/lib/gitlab/data_builder/build_spec.rb'
- 'spec/lib/gitlab/data_builder/issuable_spec.rb'
- 'spec/lib/gitlab/data_builder/pipeline_spec.rb'
@@ -224,9 +216,7 @@ Layout/FirstHashElementIndentation:
- 'spec/requests/api/ci/runner/runners_post_spec.rb'
- 'spec/requests/api/commit_statuses_spec.rb'
- 'spec/requests/api/graphql/ci/config_spec.rb'
- - 'spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb'
- 'spec/requests/api/graphql/project/fork_details_spec.rb'
- - 'spec/requests/api/ml/mlflow_spec.rb'
- 'spec/requests/api/releases_spec.rb'
- 'spec/requests/api/task_completion_status_spec.rb'
- 'spec/requests/pwa_controller_spec.rb'
@@ -238,7 +228,6 @@ Layout/FirstHashElementIndentation:
- 'spec/services/clusters/update_service_spec.rb'
- 'spec/services/google_cloud/get_cloudsql_instances_service_spec.rb'
- 'spec/services/import/github_service_spec.rb'
- - 'spec/services/metrics/dashboard/transient_embed_service_spec.rb'
- 'spec/services/notes/render_service_spec.rb'
- 'spec/services/packages/debian/parse_debian822_service_spec.rb'
- 'spec/services/projects/container_repository/delete_tags_service_spec.rb'
@@ -249,8 +238,6 @@ Layout/FirstHashElementIndentation:
- 'spec/spam/concerns/has_spam_action_response_fields_spec.rb'
- 'spec/support/helpers/kubernetes_helpers.rb'
- 'spec/support/helpers/wiki_helpers.rb'
- - 'spec/support/migrations_helpers/namespaces_helper.rb'
- - 'spec/support/migrations_helpers/vulnerabilities_findings_helper.rb'
- 'spec/support/shared_contexts/lib/container_registry/client_shared_context.rb'
- 'spec/support/shared_examples/harbor/artifacts_controller_shared_examples.rb'
- 'spec/support/shared_examples/harbor/repositories_controller_shared_examples.rb'
diff --git a/.rubocop_todo/layout/hash_alignment.yml b/.rubocop_todo/layout/hash_alignment.yml
index d07a936bb7d..cf32e66d3e0 100644
--- a/.rubocop_todo/layout/hash_alignment.yml
+++ b/.rubocop_todo/layout/hash_alignment.yml
@@ -9,5 +9,3 @@ Layout/HashAlignment:
- 'spec/helpers/projects/ml/experiments_helper_spec.rb'
- 'spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb'
- 'spec/requests/projects/merge_requests/creations_spec.rb'
- - 'spec/support/redis/redis_new_instance_shared_examples.rb'
- - 'spec/support/redis/redis_shared_examples.rb'
diff --git a/.rubocop_todo/layout/line_continuation_leading_space.yml b/.rubocop_todo/layout/line_continuation_leading_space.yml
index c1384bf3663..63858848cf0 100644
--- a/.rubocop_todo/layout/line_continuation_leading_space.yml
+++ b/.rubocop_todo/layout/line_continuation_leading_space.yml
@@ -8,7 +8,6 @@ Layout/LineContinuationLeadingSpace:
- 'app/helpers/application_settings_helper.rb'
- 'app/helpers/preferences_helper.rb'
- 'app/models/environment.rb'
- - 'config/initializers_before_autoloader/003_gc_compact.rb'
- 'ee/app/graphql/ee/mutations/issues/create.rb'
- 'ee/app/graphql/ee/types/merge_request_type.rb'
- 'ee/app/graphql/mutations/requirements_management/export_requirements.rb'
@@ -53,7 +52,6 @@ Layout/LineContinuationLeadingSpace:
- 'spec/lib/gitlab/ci/ansi2html_spec.rb'
- 'spec/lib/gitlab/ci/parsers/sbom/validators/cyclonedx_schema_validator_spec.rb'
- 'spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb'
- - 'spec/lib/gitlab/prometheus/queries/validate_query_spec.rb'
- 'spec/lib/gitlab/reference_counter_spec.rb'
- 'spec/lib/gitlab/usage/metrics/instrumentations/count_bulk_imports_entities_metric_spec.rb'
- 'spec/lib/gitlab/usage/metrics/instrumentations/count_imported_projects_metric_spec.rb'
diff --git a/.rubocop_todo/layout/line_continuation_spacing.yml b/.rubocop_todo/layout/line_continuation_spacing.yml
index 721d7c56942..c406fd1cae9 100644
--- a/.rubocop_todo/layout/line_continuation_spacing.yml
+++ b/.rubocop_todo/layout/line_continuation_spacing.yml
@@ -21,9 +21,7 @@ Layout/LineContinuationSpacing:
- 'app/services/merge_requests/merge_service.rb'
- 'app/services/uploads/destroy_service.rb'
- 'app/services/users/email_verification/validate_token_service.rb'
- - 'config/initializers_before_autoloader/003_gc_compact.rb'
- 'ee/app/controllers/ee/ldap/omniauth_callbacks_controller.rb'
- - 'ee/app/graphql/mutations/app_sec/fuzzing/api/ci_configuration/create.rb'
- 'ee/app/graphql/mutations/requirements_management/export_requirements.rb'
- 'ee/app/graphql/mutations/security_policy/assign_security_policy_project.rb'
- 'ee/app/graphql/resolvers/security_orchestration/scan_execution_policy_resolver.rb'
@@ -48,7 +46,6 @@ Layout/LineContinuationSpacing:
- 'ee/lib/api/ldap_group_links.rb'
- 'ee/lib/api/vulnerability_findings.rb'
- 'ee/lib/ee/gitlab/auth/ldap/access.rb'
- - 'ee/lib/ee/gitlab/ci/pipeline/quota/activity.rb'
- 'ee/lib/ee/gitlab/ci/pipeline/quota/size.rb'
- 'ee/lib/ee/gitlab/git_access.rb'
- 'ee/lib/tasks/gitlab/geo.rake'
@@ -63,8 +60,6 @@ Layout/LineContinuationSpacing:
- 'ee/spec/features/protected_branches_spec.rb'
- 'ee/spec/features/protected_tags_spec.rb'
- 'ee/spec/features/registrations/email_confirmation_spec.rb'
- - 'ee/spec/features/users/identity_verification_spec.rb'
- - 'ee/spec/graphql/mutations/app_sec/fuzzing/api/ci_configuration/create_spec.rb'
- 'ee/spec/graphql/mutations/audit_events/streaming/event_type_filters/destroy_spec.rb'
- 'ee/spec/graphql/mutations/audit_events/streaming/headers/destroy_spec.rb'
- 'ee/spec/graphql/mutations/requirements_management/export_requirements_spec.rb'
@@ -93,7 +88,6 @@ Layout/LineContinuationSpacing:
- 'ee/spec/requests/api/graphql/mutations/users/abuse/namespace_bans/destroy_spec.rb'
- 'ee/spec/requests/api/graphql/mutations/vulnerabilities/create_external_issue_link_spec.rb'
- 'ee/spec/requests/api/graphql/mutations/vulnerabilities/destroy_external_issue_link_spec.rb'
- - 'ee/spec/requests/api/graphql/mutations/vulnerabilities/finding_dismiss_spec.rb'
- 'ee/spec/requests/users/identity_verification_controller_spec.rb'
- 'ee/spec/services/boards/epic_lists/destroy_service_spec.rb'
- 'ee/spec/services/epic_issues/create_service_spec.rb'
@@ -111,19 +105,14 @@ Layout/LineContinuationSpacing:
- 'lib/api/groups.rb'
- 'lib/api/issue_links.rb'
- 'lib/api/metrics/dashboard/annotations.rb'
- - 'lib/api/ml/mlflow.rb'
- 'lib/gitlab/auth/user_access_denied_reason.rb'
- 'lib/gitlab/background_migration/populate_operation_visibility_permissions_from_operations.rb'
- 'lib/gitlab/checks/tag_check.rb'
- 'lib/gitlab/ci/parsers/security/validators/schema_validator.rb'
- - 'lib/gitlab/config_checker/puma_rugged_checker.rb'
- 'lib/gitlab/database/background_migration/batched_migration_runner.rb'
- 'lib/gitlab/database/load_balancing/sidekiq_server_middleware.rb'
- 'lib/gitlab/database/migration_helpers.rb'
- 'lib/gitlab/database/partitioning_migration_helpers/foreign_key_helpers.rb'
- - 'lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base.rb'
- - 'lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces.rb'
- - 'lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects.rb'
- 'lib/gitlab/database/shared_model.rb'
- 'lib/gitlab/i18n/po_linter.rb'
- 'qa/qa/specs/features/ee/api/9_data_stores/elasticsearch/nightly_elasticsearch_test_spec.rb'
@@ -154,12 +143,10 @@ Layout/LineContinuationSpacing:
- 'spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb'
- 'spec/lib/gitlab/ci/trace/stream_spec.rb'
- 'spec/lib/gitlab/closing_issue_extractor_spec.rb'
- - 'spec/lib/gitlab/config_checker/puma_rugged_checker_spec.rb'
- 'spec/lib/gitlab/database/background_migration/batched_migration_runner_spec.rb'
- 'spec/lib/gitlab/git_access_spec.rb'
- 'spec/lib/gitlab/github_import/markdown_text_spec.rb'
- 'spec/lib/gitlab/github_import/representation/issue_event_spec.rb'
- - 'spec/lib/gitlab/metrics/dashboard/stages/metric_endpoint_inserter_spec.rb'
- 'spec/lib/gitlab/usage/metrics/instrumentations/count_bulk_imports_entities_metric_spec.rb'
- 'spec/lib/gitlab/usage/metrics/instrumentations/count_imported_projects_metric_spec.rb'
- 'spec/lib/gitlab/usage/metrics/instrumentations/count_imported_projects_total_metric_spec.rb'
diff --git a/.rubocop_todo/layout/line_end_string_concatenation_indentation.yml b/.rubocop_todo/layout/line_end_string_concatenation_indentation.yml
index 9f826f4428b..d4653bab68d 100644
--- a/.rubocop_todo/layout/line_end_string_concatenation_indentation.yml
+++ b/.rubocop_todo/layout/line_end_string_concatenation_indentation.yml
@@ -27,7 +27,6 @@ Layout/LineEndStringConcatenationIndentation:
- 'app/models/integrations/pivotaltracker.rb'
- 'app/models/merge_request_diff_commit.rb'
- 'app/models/postgresql/replication_slot.rb'
- - 'app/presenters/packages/npm/package_presenter.rb'
- 'app/services/commits/change_service.rb'
- 'app/services/concerns/ci/job_token_scope/edit_scope_validations.rb'
- 'app/services/feature_flags/update_service.rb'
@@ -39,7 +38,6 @@ Layout/LineEndStringConcatenationIndentation:
- 'config/initializers/wikicloth_disable_lua_patch.rb'
- 'config/initializers/wikicloth_redos_patch.rb'
- 'config/initializers/wikicloth_ruby_3_patch.rb'
- - 'config/initializers_before_autoloader/003_gc_compact.rb'
- 'danger/database/Dangerfile'
- 'db/post_migrate/20220425121410_add_temporary_index_for_backfill_integrations_enable_ssl_verification.rb'
- 'db/post_migrate/20220525131624_drop_temporary_index_for_backfill_integrations_enable_ssl_verification.rb'
@@ -49,7 +47,6 @@ Layout/LineEndStringConcatenationIndentation:
- 'ee/app/controllers/concerns/insights_actions.rb'
- 'ee/app/controllers/ee/ldap/omniauth_callbacks_controller.rb'
- 'ee/app/finders/geo/framework_registry_finder.rb'
- - 'ee/app/graphql/mutations/app_sec/fuzzing/api/ci_configuration/create.rb'
- 'ee/app/graphql/mutations/dast_scanner_profiles/create.rb'
- 'ee/app/graphql/mutations/issues/set_epic.rb'
- 'ee/app/graphql/mutations/issues/set_escalation_policy.rb'
@@ -92,7 +89,6 @@ Layout/LineEndStringConcatenationIndentation:
- 'ee/spec/features/pending_group_memberships_spec.rb'
- 'ee/spec/features/projects/members/manage_groups_spec.rb'
- 'ee/spec/features/registrations/email_confirmation_spec.rb'
- - 'ee/spec/features/users/identity_verification_spec.rb'
- 'ee/spec/graphql/mutations/audit_events/streaming/event_type_filters/destroy_spec.rb'
- 'ee/spec/graphql/mutations/audit_events/streaming/headers/destroy_spec.rb'
- 'ee/spec/graphql/mutations/boards/lists/update_limit_metrics_spec.rb'
@@ -168,7 +164,6 @@ Layout/LineEndStringConcatenationIndentation:
- 'lib/gitlab/ci/parsers/security/validators/schema_validator.rb'
- 'lib/gitlab/ci/pipeline/chain/populate.rb'
- 'lib/gitlab/ci/pipeline/seed/build.rb'
- - 'lib/gitlab/config_checker/puma_rugged_checker.rb'
- 'lib/gitlab/database/load_balancing/sidekiq_server_middleware.rb'
- 'lib/gitlab/database/migration_helpers.rb'
- 'lib/gitlab/database/migration_helpers/restrict_gitlab_schema.rb'
@@ -189,7 +184,6 @@ Layout/LineEndStringConcatenationIndentation:
- 'lib/gitlab/path_regex.rb'
- 'lib/gitlab/reference_counter.rb'
- 'lib/gitlab/regex.rb'
- - 'lib/gitlab/regex/bulk_imports.rb'
- 'lib/gitlab/seeders/ci/runner/runner_fleet_seeder.rb'
- 'lib/gitlab/slash_commands/presenters/run.rb'
- 'lib/gitlab/tracking/standard_context.rb'
@@ -249,7 +243,6 @@ Layout/LineEndStringConcatenationIndentation:
- 'spec/features/projects/pipelines/pipeline_spec.rb'
- 'spec/finders/groups/projects_requiring_authorizations_refresh/on_direct_membership_finder_spec.rb'
- 'spec/helpers/markup_helper_spec.rb'
- - 'spec/lib/banzai/filter/inline_grafana_metrics_filter_spec.rb'
- 'spec/lib/banzai/filter/truncate_visible_filter_spec.rb'
- 'spec/lib/gitlab/background_migration/batched_migration_job_spec.rb'
- 'spec/lib/gitlab/ci/ansi2html_spec.rb'
@@ -268,8 +261,6 @@ Layout/LineEndStringConcatenationIndentation:
- 'spec/lib/gitlab/github_import/markdown_text_spec.rb'
- 'spec/lib/gitlab/github_import/representation/issue_event_spec.rb'
- 'spec/lib/gitlab/insecure_key_fingerprint_spec.rb'
- - 'spec/lib/gitlab/metrics/dashboard/stages/metric_endpoint_inserter_spec.rb'
- - 'spec/lib/gitlab/prometheus/queries/validate_query_spec.rb'
- 'spec/lib/gitlab/redis/multi_store_spec.rb'
- 'spec/lib/gitlab/reference_counter_spec.rb'
- 'spec/lib/gitlab/usage/metrics/instrumentations/count_bulk_imports_entities_metric_spec.rb'
@@ -292,8 +283,6 @@ Layout/LineEndStringConcatenationIndentation:
- 'spec/presenters/deploy_key_presenter_spec.rb'
- 'spec/presenters/key_presenter_spec.rb'
- 'spec/presenters/releases/link_presenter_spec.rb'
- - 'spec/requests/api/graphql/mutations/ci/pipeline_schedule_delete_spec.rb'
- - 'spec/requests/api/graphql/mutations/ci/pipeline_schedule_play_spec.rb'
- 'spec/requests/api/graphql/mutations/clusters/agents/delete_spec.rb'
- 'spec/requests/api/releases_spec.rb'
- 'spec/requests/api/users_spec.rb'
@@ -308,7 +297,6 @@ Layout/LineEndStringConcatenationIndentation:
- 'spec/services/ci/create_pipeline_service_spec.rb'
- 'spec/services/ci/job_artifacts/delete_service_spec.rb'
- 'spec/services/preview_markdown_service_spec.rb'
- - 'spec/services/prometheus/proxy_variable_substitution_service_spec.rb'
- 'spec/services/snippets/create_service_spec.rb'
- 'spec/services/users/email_verification/validate_token_service_spec.rb'
- 'spec/services/work_items/parent_links/create_service_spec.rb'
diff --git a/.rubocop_todo/layout/line_length.yml b/.rubocop_todo/layout/line_length.yml
index 4b7c1f75daa..1173c41c188 100644
--- a/.rubocop_todo/layout/line_length.yml
+++ b/.rubocop_todo/layout/line_length.yml
@@ -20,7 +20,6 @@ Layout/LineLength:
- 'app/controllers/concerns/issuable_actions.rb'
- 'app/controllers/concerns/issuable_collections.rb'
- 'app/controllers/concerns/membership_actions.rb'
- - 'app/controllers/concerns/metrics_dashboard.rb'
- 'app/controllers/concerns/notes_actions.rb'
- 'app/controllers/concerns/product_analytics_tracking.rb'
- 'app/controllers/concerns/routable_actions.rb'
@@ -66,7 +65,6 @@ Layout/LineLength:
- 'app/controllers/projects/cycle_analytics/events_controller.rb'
- 'app/controllers/projects/cycle_analytics_controller.rb'
- 'app/controllers/projects/discussions_controller.rb'
- - 'app/controllers/projects/environments/sample_metrics_controller.rb'
- 'app/controllers/projects/environments_controller.rb'
- 'app/controllers/projects/error_tracking/stack_traces_controller.rb'
- 'app/controllers/projects/forks_controller.rb'
@@ -77,10 +75,8 @@ Layout/LineLength:
- 'app/controllers/projects/labels_controller.rb'
- 'app/controllers/projects/milestones_controller.rb'
- 'app/controllers/projects/notes_controller.rb'
- - 'app/controllers/projects/performance_monitoring/dashboards_controller.rb'
- 'app/controllers/projects/pipeline_schedules_controller.rb'
- 'app/controllers/projects/pipelines_controller.rb'
- - 'app/controllers/projects/prometheus/metrics_controller.rb'
- 'app/controllers/projects/settings/ci_cd_controller.rb'
- 'app/controllers/projects/settings/repository_controller.rb'
- 'app/controllers/projects/templates_controller.rb'
@@ -105,7 +101,6 @@ Layout/LineLength:
- 'app/finders/issuables/label_filter.rb'
- 'app/finders/issues_finder.rb'
- 'app/finders/members_finder.rb'
- - 'app/finders/metrics/users_starred_dashboards_finder.rb'
- 'app/finders/packages/group_packages_finder.rb'
- 'app/finders/personal_access_tokens_finder.rb'
- 'app/finders/projects/export_job_finder.rb'
@@ -320,7 +315,6 @@ Layout/LineLength:
- 'app/models/concerns/restricted_signup.rb'
- 'app/models/concerns/shardable.rb'
- 'app/models/concerns/sortable.rb'
- - 'app/models/concerns/storage/legacy_namespace.rb'
- 'app/models/concerns/subscribable.rb'
- 'app/models/concerns/token_authenticatable_strategies/base.rb'
- 'app/models/concerns/token_authenticatable_strategies/encrypted.rb'
@@ -386,7 +380,6 @@ Layout/LineLength:
- 'app/models/merge_request_assignee.rb'
- 'app/models/merge_request_diff.rb'
- 'app/models/merge_requests_closing_issues.rb'
- - 'app/models/metrics/dashboard/annotation.rb'
- 'app/models/milestone.rb'
- 'app/models/namespace.rb'
- 'app/models/namespace/package_setting.rb'
@@ -403,7 +396,6 @@ Layout/LineLength:
- 'app/models/packages/package.rb'
- 'app/models/packages/package_file.rb'
- 'app/models/pages_domain.rb'
- - 'app/models/performance_monitoring/prometheus_dashboard.rb'
- 'app/models/personal_access_token.rb'
- 'app/models/preloaders/environments/deployment_preloader.rb'
- 'app/models/preloaders/single_hierarchy_project_group_plans_preloader.rb'
@@ -551,10 +543,6 @@ Layout/LineLength:
- 'app/services/merge_requests/refresh_service.rb'
- 'app/services/merge_requests/squash_service.rb'
- 'app/services/merge_requests/update_service.rb'
- - 'app/services/metrics/dashboard/annotations/create_service.rb'
- - 'app/services/metrics/dashboard/clone_dashboard_service.rb'
- - 'app/services/metrics/dashboard/panel_preview_service.rb'
- - 'app/services/metrics/dashboard/update_dashboard_service.rb'
- 'app/services/milestones/destroy_service.rb'
- 'app/services/namespace_settings/update_service.rb'
- 'app/services/notes/build_service.rb'
@@ -590,7 +578,6 @@ Layout/LineLength:
- 'app/services/projects/fork_service.rb'
- 'app/services/projects/hashed_storage/base_attachment_service.rb'
- 'app/services/projects/hashed_storage/migrate_attachments_service.rb'
- - 'app/services/projects/hashed_storage/migrate_repository_service.rb'
- 'app/services/projects/lfs_pointers/lfs_download_service.rb'
- 'app/services/projects/operations/update_service.rb'
- 'app/services/projects/overwrite_project_service.rb'
@@ -661,7 +648,6 @@ Layout/LineLength:
- 'app/workers/repository_fork_worker.rb'
- 'app/workers/ssh_keys/expired_notification_worker.rb'
- 'config/application.rb'
- - 'config/initializers/00_rails_disable_joins.rb'
- 'config/initializers/01_secret_token.rb'
- 'config/initializers/1_settings.rb'
- 'config/initializers/5_backend.rb'
@@ -700,7 +686,6 @@ Layout/LineLength:
- 'danger/pajamas/Dangerfile'
- 'danger/roulette/Dangerfile'
- 'danger/vue_shared_documentation/Dangerfile'
- - 'danger/z_metadata/Dangerfile'
- 'ee/app/controllers/admin/elasticsearch_controller.rb'
- 'ee/app/controllers/admin/geo/application_controller.rb'
- 'ee/app/controllers/admin/licenses_controller.rb'
@@ -722,7 +707,6 @@ Layout/LineLength:
- 'ee/app/controllers/groups/analytics/cycle_analytics/summary_controller.rb'
- 'ee/app/controllers/groups/analytics/productivity_analytics_controller.rb'
- 'ee/app/controllers/groups/hooks_controller.rb'
- - 'ee/app/controllers/groups/ldap_settings_controller.rb'
- 'ee/app/controllers/groups/omniauth_callbacks_controller.rb'
- 'ee/app/controllers/groups/saml_group_links_controller.rb'
- 'ee/app/controllers/groups/sso_controller.rb'
@@ -857,7 +841,6 @@ Layout/LineLength:
- 'ee/app/helpers/vulnerabilities_helper.rb'
- 'ee/app/mailers/ee/emails/profile.rb'
- 'ee/app/mailers/ee/preview/notify_preview.rb'
- - 'ee/app/mailers/emails/namespace_storage_usage_mailer.rb'
- 'ee/app/models/approval_merge_request_rule.rb'
- 'ee/app/models/approval_project_rule.rb'
- 'ee/app/models/approval_state.rb'
@@ -900,7 +883,6 @@ Layout/LineLength:
- 'ee/app/models/ee/packages/package_file.rb'
- 'ee/app/models/ee/pages_deployment.rb'
- 'ee/app/models/ee/project.rb'
- - 'ee/app/models/ee/project_feature.rb'
- 'ee/app/models/ee/resource_label_event.rb'
- 'ee/app/models/ee/snippet_repository.rb'
- 'ee/app/models/ee/terraform/state_version.rb'
@@ -909,7 +891,6 @@ Layout/LineLength:
- 'ee/app/models/ee/vulnerability.rb'
- 'ee/app/models/elastic/reindexing_slice.rb'
- 'ee/app/models/epic_issue.rb'
- - 'ee/app/models/geo/project_registry.rb'
- 'ee/app/models/geo/secondary_usage_data.rb'
- 'ee/app/models/geo_node.rb'
- 'ee/app/models/geo_node_status.rb'
@@ -947,7 +928,6 @@ Layout/LineLength:
- 'ee/app/models/vulnerabilities/remediation.rb'
- 'ee/app/models/vulnerabilities/scanner.rb'
- 'ee/app/policies/ee/group_policy.rb'
- - 'ee/app/policies/ee/identity_provider_policy.rb'
- 'ee/app/policies/ee/project_policy.rb'
- 'ee/app/presenters/dast/site_profile_presenter.rb'
- 'ee/app/presenters/ee/merge_request_presenter.rb'
@@ -1033,8 +1013,6 @@ Layout/LineLength:
- 'ee/app/services/geo/file_registry_removal_service.rb'
- 'ee/app/services/geo/framework_repository_sync_service.rb'
- 'ee/app/services/geo/hashed_storage_attachments_migration_service.rb'
- - 'ee/app/services/geo/hashed_storage_migration_service.rb'
- - 'ee/app/services/geo/repository_base_sync_service.rb'
- 'ee/app/services/geo/request_service.rb'
- 'ee/app/services/geo/verification_state_backfill_service.rb'
- 'ee/app/services/gitlab_subscriptions/plan_upgrade_service.rb'
@@ -1214,7 +1192,6 @@ Layout/LineLength:
- 'ee/lib/ee/gitlab/import_export/after_export_strategies/custom_template_export_import_strategy.rb'
- 'ee/lib/ee/gitlab/middleware/read_only/controller.rb'
- 'ee/lib/ee/gitlab/project_template.rb'
- - 'ee/lib/ee/gitlab/prometheus/queries/query_additional_metrics.rb'
- 'ee/lib/ee/gitlab/quick_actions/epic_actions.rb'
- 'ee/lib/ee/gitlab/quick_actions/issue_actions.rb'
- 'ee/lib/ee/gitlab/rack_attack.rb'
@@ -1286,7 +1263,6 @@ Layout/LineLength:
- 'ee/lib/system_check/geo/current_node_check.rb'
- 'ee/lib/system_check/geo/geo_database_configured_check.rb'
- 'ee/lib/tasks/geo.rake'
- - 'ee/lib/tasks/geo/git.rake'
- 'ee/lib/tasks/gitlab/elastic/test.rake'
- 'ee/lib/tasks/gitlab/seed/metrics.rake'
- 'ee/lib/world.rb'
@@ -1405,7 +1381,6 @@ Layout/LineLength:
- 'ee/spec/features/projects/audit_events_spec.rb'
- 'ee/spec/features/projects/feature_flags/user_sees_feature_flag_list_spec.rb'
- 'ee/spec/features/projects/feature_flags/user_updates_feature_flag_spec.rb'
- - 'ee/spec/features/projects/integrations/prometheus_custom_metrics_spec.rb'
- 'ee/spec/features/projects/integrations/user_activates_jira_spec.rb'
- 'ee/spec/features/projects/iterations/iteration_cadences_list_spec.rb'
- 'ee/spec/features/projects/iterations/user_views_iteration_spec.rb'
@@ -1439,8 +1414,6 @@ Layout/LineLength:
- 'ee/spec/finders/ee/group_members_finder_spec.rb'
- 'ee/spec/finders/ee/projects_finder_spec.rb'
- 'ee/spec/finders/epics_finder_spec.rb'
- - 'ee/spec/finders/geo/project_registry_finder_spec.rb'
- - 'ee/spec/finders/geo/project_registry_status_finder_spec.rb'
- 'ee/spec/finders/group_projects_finder_spec.rb'
- 'ee/spec/finders/incident_management/escalation_policies_finder_spec.rb'
- 'ee/spec/finders/incident_management/escalation_rules_finder_spec.rb'
@@ -1459,9 +1432,6 @@ Layout/LineLength:
- 'ee/spec/finders/projects/integrations/jira/issues_finder_spec.rb'
- 'ee/spec/finders/security/findings_finder_spec.rb'
- 'ee/spec/finders/security/pipeline_vulnerabilities_finder_spec.rb'
- - 'ee/spec/finders/security/training_providers/base_url_finder_spec.rb'
- - 'ee/spec/finders/security/training_providers/kontra_url_finder_spec.rb'
- - 'ee/spec/finders/security/training_providers/secure_code_warrior_url_finder_spec.rb'
- 'ee/spec/finders/security/vulnerabilities_finder_spec.rb'
- 'ee/spec/finders/security/vulnerability_reads_finder_spec.rb'
- 'ee/spec/finders/snippets_finder_spec.rb'
@@ -1500,7 +1470,6 @@ Layout/LineLength:
- 'ee/spec/graphql/mutations/issues/set_escalation_policy_spec.rb'
- 'ee/spec/graphql/mutations/projects/set_compliance_framework_spec.rb'
- 'ee/spec/graphql/mutations/releases/update_spec.rb'
- - 'ee/spec/graphql/mutations/security/training_provider_update_spec.rb'
- 'ee/spec/graphql/mutations/security_policy/assign_security_policy_project_spec.rb'
- 'ee/spec/graphql/mutations/security_policy/commit_scan_execution_policy_spec.rb'
- 'ee/spec/graphql/mutations/security_policy/unassign_security_policy_project_spec.rb'
@@ -1627,7 +1596,6 @@ Layout/LineLength:
- 'ee/spec/lib/ee/gitlab/repository_size_checker_spec.rb'
- 'ee/spec/lib/ee/gitlab/url_builder_spec.rb'
- 'ee/spec/lib/ee/gitlab/usage/service_ping/payload_keys_processor_spec.rb'
- - 'ee/spec/lib/ee/gitlab/usage_data_counters/hll_redis_counter_spec.rb'
- 'ee/spec/lib/ee/gitlab/usage_data_non_sql_metrics_spec.rb'
- 'ee/spec/lib/ee/gitlab/usage_data_spec.rb'
- 'ee/spec/lib/ee/sidebars/groups/menus/issues_menu_spec.rb'
@@ -1702,8 +1670,6 @@ Layout/LineLength:
- 'ee/spec/lib/gitlab/geo/geo_tasks_spec.rb'
- 'ee/spec/lib/gitlab/geo/git_ssh_proxy_spec.rb'
- 'ee/spec/lib/gitlab/geo/health_check_spec.rb'
- - 'ee/spec/lib/gitlab/geo/log_cursor/events/repository_created_event_spec.rb'
- - 'ee/spec/lib/gitlab/geo/log_cursor/events/repository_updated_event_spec.rb'
- 'ee/spec/lib/gitlab/geo/log_cursor/lease_spec.rb'
- 'ee/spec/lib/gitlab/geo/oauth/login_state_spec.rb'
- 'ee/spec/lib/gitlab/geo/oauth/logout_token_spec.rb'
@@ -1731,7 +1697,6 @@ Layout/LineLength:
- 'ee/spec/lib/gitlab/instrumentation/elasticsearch_transport_spec.rb'
- 'ee/spec/lib/gitlab/mirror_spec.rb'
- 'ee/spec/lib/gitlab/patch/database_config_spec.rb'
- - 'ee/spec/lib/gitlab/prometheus/queries/cluster_query_spec.rb'
- 'ee/spec/lib/gitlab/reference_extractor_spec.rb'
- 'ee/spec/lib/gitlab/search/aggregation_spec.rb'
- 'ee/spec/lib/gitlab/sitemaps/generator_spec.rb'
@@ -1838,7 +1803,6 @@ Layout/LineLength:
- 'ee/spec/models/geo/container_repository_registry_spec.rb'
- 'ee/spec/models/geo/event_log_spec.rb'
- 'ee/spec/models/geo/package_file_registry_spec.rb'
- - 'ee/spec/models/geo/project_registry_spec.rb'
- 'ee/spec/models/geo/secondary_usage_data_spec.rb'
- 'ee/spec/models/geo_node_spec.rb'
- 'ee/spec/models/geo_node_status_spec.rb'
@@ -1989,7 +1953,6 @@ Layout/LineLength:
- 'ee/spec/requests/api/graphql/project/requirements_management/requirements_spec.rb'
- 'ee/spec/requests/api/graphql/project/vulnerability_severities_count_spec.rb'
- 'ee/spec/requests/api/graphql/projects/compliance_frameworks_spec.rb'
- - 'ee/spec/requests/api/graphql/vulnerabilities/description_spec.rb'
- 'ee/spec/requests/api/graphql/vulnerabilities/external_issue_links_spec.rb'
- 'ee/spec/requests/api/graphql/vulnerabilities/issue_links_spec.rb'
- 'ee/spec/requests/api/group_milestones_spec.rb'
@@ -2166,14 +2129,6 @@ Layout/LineLength:
- 'ee/spec/services/geo/container_repository_sync_spec.rb'
- 'ee/spec/services/geo/framework_repository_sync_service_spec.rb'
- 'ee/spec/services/geo/hashed_storage_attachments_event_store_spec.rb'
- - 'ee/spec/services/geo/hashed_storage_migration_service_spec.rb'
- - 'ee/spec/services/geo/project_housekeeping_service_spec.rb'
- - 'ee/spec/services/geo/rename_repository_service_spec.rb'
- - 'ee/spec/services/geo/repository_destroy_service_spec.rb'
- - 'ee/spec/services/geo/repository_sync_service_spec.rb'
- - 'ee/spec/services/geo/repository_updated_service_spec.rb'
- - 'ee/spec/services/geo/repository_verification_secondary_service_spec.rb'
- - 'ee/spec/services/geo/wiki_sync_service_spec.rb'
- 'ee/spec/services/gitlab_subscriptions/check_future_renewal_service_spec.rb'
- 'ee/spec/services/gitlab_subscriptions/create_service_spec.rb'
- 'ee/spec/services/groups/create_service_spec.rb'
@@ -2215,7 +2170,6 @@ Layout/LineLength:
- 'ee/spec/services/projects/alerting/notify_service_spec.rb'
- 'ee/spec/services/projects/cleanup_service_spec.rb'
- 'ee/spec/services/projects/gitlab_projects_import_service_spec.rb'
- - 'ee/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb'
- 'ee/spec/services/projects/import_export/export_service_spec.rb'
- 'ee/spec/services/projects/mark_for_deletion_service_spec.rb'
- 'ee/spec/services/projects/transfer_service_spec.rb'
@@ -2289,7 +2243,6 @@ Layout/LineLength:
- 'ee/spec/support/license_scanning_reports/license_scanning_report_helper.rb'
- 'ee/spec/support/matchers/ee/epic_aggregate_matchers.rb'
- 'ee/spec/support/matchers/locked_schema.rb'
- - 'ee/spec/support/prometheus/additional_metrics_shared_examples.rb'
- 'ee/spec/support/protected_tags/access_control_shared_examples.rb'
- 'ee/spec/support/shared_contexts/lib/gitlab/insights/reducers/reducers_shared_contexts.rb'
- 'ee/spec/support/shared_contexts/push_rules_checks_shared_context.rb'
@@ -2298,7 +2251,6 @@ Layout/LineLength:
- 'ee/spec/support/shared_examples/controllers/concerns/description_diff_actions_shared_examples.rb'
- 'ee/spec/support/shared_examples/features/epics_filtered_search_shared_examples.rb'
- 'ee/spec/support/shared_examples/features/sidebar_shared_examples.rb'
- - 'ee/spec/support/shared_examples/finders/geo/file_registry_finder_shared_examples.rb'
- 'ee/spec/support/shared_examples/finders/geo/registry_finder_shared_examples.rb'
- 'ee/spec/support/shared_examples/graphql/mutations/set_multiple_assignees_shared_examples.rb'
- 'ee/spec/support/shared_examples/graphql/resolvers/security_orchestration/resolves_orchestration_policy_shared_examples.rb'
@@ -2330,7 +2282,6 @@ Layout/LineLength:
- 'ee/spec/support/shared_examples/services/search_notes_shared_examples.rb'
- 'ee/spec/support/shared_examples/services/vulnerabilities/calls_vulnerability_statistics_utility_services_in_order.rb'
- 'ee/spec/support/shared_examples/views/subscription_shared_examples.rb'
- - 'ee/spec/tasks/geo/git_rake_spec.rb'
- 'ee/spec/tasks/geo_rake_spec.rb'
- 'ee/spec/tasks/gitlab/geo_rake_spec.rb'
- 'ee/spec/validators/json_schema_validator_spec.rb'
@@ -2364,15 +2315,8 @@ Layout/LineLength:
- 'ee/spec/workers/concerns/elastic/indexing_control_spec.rb'
- 'ee/spec/workers/elastic/migration_worker_spec.rb'
- 'ee/spec/workers/elastic_association_indexer_worker_spec.rb'
- - 'ee/spec/workers/geo/batch/project_registry_scheduler_worker_spec.rb'
- 'ee/spec/workers/geo/destroy_worker_spec.rb'
- - 'ee/spec/workers/geo/project_sync_worker_spec.rb'
- 'ee/spec/workers/geo/prune_event_log_worker_spec.rb'
- - 'ee/spec/workers/geo/repositories_clean_up_worker_spec.rb'
- - 'ee/spec/workers/geo/repository_shard_sync_worker_spec.rb'
- - 'ee/spec/workers/geo/repository_verification/primary/shard_worker_spec.rb'
- - 'ee/spec/workers/geo/repository_verification/secondary/shard_worker_spec.rb'
- - 'ee/spec/workers/geo/scheduler/per_shard_scheduler_worker_spec.rb'
- 'ee/spec/workers/geo/secondary/registry_consistency_worker_spec.rb'
- 'ee/spec/workers/geo/verification_batch_worker_spec.rb'
- 'ee/spec/workers/geo/verification_timeout_worker_spec.rb'
@@ -2578,7 +2522,6 @@ Layout/LineLength:
- 'lib/gitlab/background_migration/populate_resolved_on_default_branch_column.rb'
- 'lib/gitlab/background_migration/project_namespaces/backfill_project_namespaces.rb'
- 'lib/gitlab/bitbucket_import/importer.rb'
- - 'lib/gitlab/bitbucket_server_import/importer.rb'
- 'lib/gitlab/buffered_io.rb'
- 'lib/gitlab/bullet/exclusions.rb'
- 'lib/gitlab/cache/helpers.rb'
@@ -2696,7 +2639,6 @@ Layout/LineLength:
- 'lib/gitlab/git/conflict/resolver.rb'
- 'lib/gitlab/git/remote_mirror.rb'
- 'lib/gitlab/git/repository.rb'
- - 'lib/gitlab/git/rugged_impl/repository.rb'
- 'lib/gitlab/git/user.rb'
- 'lib/gitlab/git_access.rb'
- 'lib/gitlab/git_access_project.rb'
@@ -2714,7 +2656,6 @@ Layout/LineLength:
- 'lib/gitlab/gitaly_client/server_service.rb'
- 'lib/gitlab/github_import.rb'
- 'lib/gitlab/github_import/importer/pull_request_importer.rb'
- - 'lib/gitlab/github_import/parallel_scheduling.rb'
- 'lib/gitlab/gl_repository.rb'
- 'lib/gitlab/global_id/deprecations.rb'
- 'lib/gitlab/golang.rb'
@@ -2755,14 +2696,6 @@ Layout/LineLength:
- 'lib/gitlab/lograge/custom_options.rb'
- 'lib/gitlab/mail_room/authenticator.rb'
- 'lib/gitlab/markdown_cache/active_record/extension.rb'
- - 'lib/gitlab/metrics/dashboard/importer.rb'
- - 'lib/gitlab/metrics/dashboard/importers/prometheus_metrics.rb'
- - 'lib/gitlab/metrics/dashboard/stages/cluster_endpoint_inserter.rb'
- - 'lib/gitlab/metrics/dashboard/stages/custom_metrics_details_inserter.rb'
- - 'lib/gitlab/metrics/dashboard/stages/metric_endpoint_inserter.rb'
- - 'lib/gitlab/metrics/dashboard/stages/variable_endpoint_inserter.rb'
- - 'lib/gitlab/metrics/dashboard/validator.rb'
- - 'lib/gitlab/metrics/dashboard/validator/errors.rb'
- 'lib/gitlab/metrics/samplers/action_cable_sampler.rb'
- 'lib/gitlab/metrics/samplers/puma_sampler.rb'
- 'lib/gitlab/metrics/samplers/ruby_sampler.rb'
@@ -2786,9 +2719,6 @@ Layout/LineLength:
- 'lib/gitlab/path_regex.rb'
- 'lib/gitlab/project_search_results.rb'
- 'lib/gitlab/project_template.rb'
- - 'lib/gitlab/prometheus/queries/base_query.rb'
- - 'lib/gitlab/prometheus/queries/deployment_query.rb'
- - 'lib/gitlab/prometheus/queries/query_additional_metrics.rb'
- 'lib/gitlab/prometheus_client.rb'
- 'lib/gitlab/query_limiting/active_support_subscriber.rb'
- 'lib/gitlab/quick_actions/issuable_actions.rb'
@@ -2797,7 +2727,6 @@ Layout/LineLength:
- 'lib/gitlab/quick_actions/merge_request_actions.rb'
- 'lib/gitlab/rack_attack.rb'
- 'lib/gitlab/regex.rb'
- - 'lib/gitlab/regex/bulk_imports.rb'
- 'lib/gitlab/regex/packages.rb'
- 'lib/gitlab/relative_positioning/item_context.rb'
- 'lib/gitlab/repository_size_error_message.rb'
@@ -2859,10 +2788,8 @@ Layout/LineLength:
- 'lib/tasks/gitlab/dependency_proxy/migrate.rake'
- 'lib/tasks/gitlab/docs/redirect.rake'
- 'lib/tasks/gitlab/external_diffs.rake'
- - 'lib/tasks/gitlab/generate_sample_prometheus_data.rake'
- 'lib/tasks/gitlab/graphql.rake'
- 'lib/tasks/gitlab/info.rake'
- - 'lib/tasks/gitlab/packages/events.rake'
- 'lib/tasks/gitlab/packages/migrate.rake'
- 'lib/tasks/gitlab/seed/group_seed.rake'
- 'lib/tasks/gitlab/shell.rake'
@@ -2912,9 +2839,7 @@ Layout/LineLength:
- 'qa/qa/service/cluster_provider/gcloud.rb'
- 'qa/qa/service/cluster_provider/k3s.rb'
- 'qa/qa/service/praefect_manager.rb'
- - 'qa/qa/specs/features/api/1_manage/project_access_token_spec.rb'
- 'qa/qa/specs/features/api/1_manage/rate_limits_spec.rb'
- - 'qa/qa/specs/features/api/1_manage/user_access_termination_spec.rb'
- 'qa/qa/specs/features/api/3_create/merge_request/push_options_labels_spec.rb'
- 'qa/qa/specs/features/api/3_create/merge_request/push_options_mwps_spec.rb'
- 'qa/qa/specs/features/api/3_create/repository/default_branch_name_setting_spec.rb'
@@ -2923,13 +2848,6 @@ Layout/LineLength:
- 'qa/qa/specs/features/api/3_create/repository/push_postreceive_idempotent_spec.rb'
- 'qa/qa/specs/features/api/3_create/snippet/snippet_repository_storage_move_spec.rb'
- 'qa/qa/specs/features/api/4_verify/cancel_pipeline_when_block_user_spec.rb'
- - 'qa/qa/specs/features/browser_ui/1_manage/group/create_group_with_mattermost_team_spec.rb'
- - 'qa/qa/specs/features/browser_ui/1_manage/login/2fa_ssh_recovery_spec.rb'
- - 'qa/qa/specs/features/browser_ui/1_manage/login/log_in_spec.rb'
- - 'qa/qa/specs/features/browser_ui/1_manage/login/log_in_with_2fa_spec.rb'
- - 'qa/qa/specs/features/browser_ui/1_manage/login/log_into_gitlab_via_ldap_spec.rb'
- - 'qa/qa/specs/features/browser_ui/1_manage/login/log_into_mattermost_via_gitlab_spec.rb'
- - 'qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb'
- 'qa/qa/specs/features/browser_ui/2_plan/email/trigger_email_notification_spec.rb'
- 'qa/qa/specs/features/browser_ui/2_plan/issue/collapse_comments_in_discussions_spec.rb'
- 'qa/qa/specs/features/browser_ui/2_plan/issue/comment_issue_spec.rb'
@@ -2947,7 +2865,6 @@ Layout/LineLength:
- 'qa/qa/specs/features/browser_ui/3_create/merge_request/revert/reverting_merge_request_spec.rb'
- 'qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb'
- 'qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb'
- - 'qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb'
- 'qa/qa/specs/features/browser_ui/3_create/repository/branch_with_unusual_name_spec.rb'
- 'qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb'
- 'qa/qa/specs/features/browser_ui/3_create/repository/file/create_file_via_web_spec.rb'
@@ -2982,7 +2899,6 @@ Layout/LineLength:
- 'qa/qa/specs/features/browser_ui/4_verify/pipeline/trigger_matrix_spec.rb'
- 'qa/qa/specs/features/browser_ui/4_verify/pipeline/update_ci_file_with_pipeline_editor_spec.rb'
- 'qa/qa/specs/features/browser_ui/4_verify/runner/register_runner_spec.rb'
- - 'qa/qa/specs/features/browser_ui/4_verify/testing/view_code_coverage_spec.rb'
- 'qa/qa/specs/features/browser_ui/5_package/package_registry/composer_registry_spec.rb'
- 'qa/qa/specs/features/browser_ui/5_package/package_registry/conan_repository_spec.rb'
- 'qa/qa/specs/features/browser_ui/5_package/package_registry/generic_repository_spec.rb'
@@ -2994,19 +2910,12 @@ Layout/LineLength:
- 'qa/qa/specs/features/browser_ui/5_package/package_registry/nuget/nuget_group_level_spec.rb'
- 'qa/qa/specs/features/browser_ui/5_package/package_registry/pypi_repository_spec.rb'
- 'qa/qa/specs/features/browser_ui/5_package/package_registry/rubygems_registry_spec.rb'
- - 'qa/qa/specs/features/ee/api/1_manage/user/minimal_access_user_spec.rb'
- 'qa/qa/specs/features/ee/api/2_plan/epics_milestone_dates_spec.rb'
- 'qa/qa/specs/features/ee/api/3_create/wiki/group_wiki_repository_storage_move_spec.rb'
- 'qa/qa/specs/features/ee/browser_ui/11_fulfillment/license/cloud_activation_spec.rb'
- 'qa/qa/specs/features/ee/browser_ui/11_fulfillment/license/license_spec.rb'
- 'qa/qa/specs/features/ee/browser_ui/11_fulfillment/purchase/user_registration_billing_spec.rb'
- 'qa/qa/specs/features/ee/browser_ui/13_secure/enable_scanning_from_configuration_spec.rb'
- - 'qa/qa/specs/features/ee/browser_ui/13_secure/license_compliance_spec.rb'
- - 'qa/qa/specs/features/ee/browser_ui/1_manage/group/group_saml_enforced_sso_git_access_spec.rb'
- - 'qa/qa/specs/features/ee/browser_ui/1_manage/group/group_saml_enforced_sso_new_account_spec.rb'
- - 'qa/qa/specs/features/ee/browser_ui/1_manage/group/group_saml_non_enforced_sso_spec.rb'
- - 'qa/qa/specs/features/ee/browser_ui/1_manage/ldap/admin_ldap_sync_spec.rb'
- - 'qa/qa/specs/features/ee/browser_ui/1_manage/user/minimal_access_user_spec.rb'
- 'qa/qa/specs/features/ee/browser_ui/2_plan/burndown_chart/burndown_chart_spec.rb'
- 'qa/qa/specs/features/ee/browser_ui/2_plan/custom_email/custom_email_spec.rb'
- 'qa/qa/specs/features/ee/browser_ui/2_plan/epic/epics_management_spec.rb'
@@ -3070,7 +2979,6 @@ Layout/LineLength:
- 'rubocop/cop/usage_data/large_table.rb'
- 'scripts/api/cancel_pipeline.rb'
- 'scripts/api/get_job_id.rb'
- - 'scripts/changed-feature-flags'
- 'scripts/lint_templates_bash.rb'
- 'scripts/no-dir-check'
- 'scripts/perf/query_limiting_report.rb'
@@ -3091,7 +2999,6 @@ Layout/LineLength:
- 'spec/controllers/application_controller_spec.rb'
- 'spec/controllers/concerns/check_rate_limit_spec.rb'
- 'spec/controllers/concerns/confirm_email_warning_spec.rb'
- - 'spec/controllers/concerns/metrics_dashboard_spec.rb'
- 'spec/controllers/concerns/send_file_upload_spec.rb'
- 'spec/controllers/concerns/sourcegraph_decorator_spec.rb'
- 'spec/controllers/concerns/spammable_actions/akismet_mark_as_spam_action_spec.rb'
@@ -3150,12 +3057,10 @@ Layout/LineLength:
- 'spec/controllers/projects/milestones_controller_spec.rb'
- 'spec/controllers/projects/mirrors_controller_spec.rb'
- 'spec/controllers/projects/notes_controller_spec.rb'
- - 'spec/controllers/projects/performance_monitoring/dashboards_controller_spec.rb'
- 'spec/controllers/projects/pipeline_schedules_controller_spec.rb'
- 'spec/controllers/projects/pipelines/tests_controller_spec.rb'
- 'spec/controllers/projects/pipelines_controller_spec.rb'
- 'spec/controllers/projects/project_members_controller_spec.rb'
- - 'spec/controllers/projects/prometheus/metrics_controller_spec.rb'
- 'spec/controllers/projects/registry/tags_controller_spec.rb'
- 'spec/controllers/projects/repositories_controller_spec.rb'
- 'spec/controllers/projects/settings/ci_cd_controller_spec.rb'
@@ -3278,7 +3183,6 @@ Layout/LineLength:
- 'spec/features/merge_request/user_expands_diff_spec.rb'
- 'spec/features/merge_request/user_interacts_with_batched_mr_diffs_spec.rb'
- 'spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb'
- - 'spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb'
- 'spec/features/merge_request/user_posts_diff_notes_spec.rb'
- 'spec/features/merge_request/user_posts_notes_spec.rb'
- 'spec/features/merge_request/user_resolves_conflicts_spec.rb'
@@ -3419,7 +3323,6 @@ Layout/LineLength:
- 'spec/finders/members_finder_spec.rb'
- 'spec/finders/merge_requests/by_approvals_finder_spec.rb'
- 'spec/finders/merge_requests_finder_spec.rb'
- - 'spec/finders/metrics/users_starred_dashboards_finder_spec.rb'
- 'spec/finders/milestones_finder_spec.rb'
- 'spec/finders/namespaces/projects_finder_spec.rb'
- 'spec/finders/notes_finder_spec.rb'
@@ -3488,7 +3391,6 @@ Layout/LineLength:
- 'spec/graphql/resolvers/group_labels_resolver_spec.rb'
- 'spec/graphql/resolvers/issue_status_counts_resolver_spec.rb'
- 'spec/graphql/resolvers/merge_requests_resolver_spec.rb'
- - 'spec/graphql/resolvers/metrics/dashboard_resolver_spec.rb'
- 'spec/graphql/resolvers/metrics/dashboards/annotation_resolver_spec.rb'
- 'spec/graphql/resolvers/namespace_projects_resolver_spec.rb'
- 'spec/graphql/resolvers/project_issues_resolver_spec.rb'
@@ -3569,7 +3471,6 @@ Layout/LineLength:
- 'spec/helpers/projects_helper_spec.rb'
- 'spec/helpers/registrations_helper_spec.rb'
- 'spec/helpers/search_helper_spec.rb'
- - 'spec/helpers/sidekiq_helper_spec.rb'
- 'spec/helpers/snippets_helper_spec.rb'
- 'spec/helpers/sorting_helper_spec.rb'
- 'spec/helpers/sourcegraph_helper_spec.rb'
@@ -3582,7 +3483,6 @@ Layout/LineLength:
- 'spec/helpers/visibility_level_helper_spec.rb'
- 'spec/helpers/webpack_helper_spec.rb'
- 'spec/helpers/wiki_page_version_helper_spec.rb'
- - 'spec/initializers/00_rails_disable_joins_spec.rb'
- 'spec/initializers/6_validations_spec.rb'
- 'spec/initializers/direct_upload_support_spec.rb'
- 'spec/initializers/global_id_spec.rb'
@@ -3711,7 +3611,6 @@ Layout/LineLength:
- 'spec/lib/gitlab/background_migration/legacy_upload_mover_spec.rb'
- 'spec/lib/gitlab/background_migration/update_jira_tracker_data_deployment_type_based_on_url_spec.rb'
- 'spec/lib/gitlab/bitbucket_import/importer_spec.rb'
- - 'spec/lib/gitlab/bitbucket_server_import/importer_spec.rb'
- 'spec/lib/gitlab/buffered_io_spec.rb'
- 'spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb'
- 'spec/lib/gitlab/chat/output_spec.rb'
@@ -3773,7 +3672,6 @@ Layout/LineLength:
- 'spec/lib/gitlab/code_navigation_path_spec.rb'
- 'spec/lib/gitlab/composer/cache_spec.rb'
- 'spec/lib/gitlab/composer/version_index_spec.rb'
- - 'spec/lib/gitlab/config_checker/puma_rugged_checker_spec.rb'
- 'spec/lib/gitlab/conflict/file_spec.rb'
- 'spec/lib/gitlab/consul/internal_spec.rb'
- 'spec/lib/gitlab/content_security_policy/config_loader_spec.rb'
@@ -3825,7 +3723,6 @@ Layout/LineLength:
- 'spec/lib/gitlab/database/query_analyzers/restrict_allowed_schemas_spec.rb'
- 'spec/lib/gitlab/database/reindexing/reindex_concurrently_spec.rb'
- 'spec/lib/gitlab/database/reindexing_spec.rb'
- - 'spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb'
- 'spec/lib/gitlab/database/transaction/observer_spec.rb'
- 'spec/lib/gitlab/database/with_lock_retries_outside_transaction_spec.rb'
- 'spec/lib/gitlab/database/with_lock_retries_spec.rb'
@@ -3896,7 +3793,6 @@ Layout/LineLength:
- 'spec/lib/gitlab/graphql/present/field_extension_spec.rb'
- 'spec/lib/gitlab/graphql/tracers/application_context_tracer_spec.rb'
- 'spec/lib/gitlab/graphs/commits_spec.rb'
- - 'spec/lib/gitlab/hashed_storage/migrator_spec.rb'
- 'spec/lib/gitlab/health_checks/gitaly_check_spec.rb'
- 'spec/lib/gitlab/health_checks/simple_check_shared.rb'
- 'spec/lib/gitlab/highlight_spec.rb'
@@ -3956,10 +3852,6 @@ Layout/LineLength:
- 'spec/lib/gitlab/mail_room/authenticator_spec.rb'
- 'spec/lib/gitlab/metrics/background_transaction_spec.rb'
- 'spec/lib/gitlab/metrics/boot_time_tracker_spec.rb'
- - 'spec/lib/gitlab/metrics/dashboard/finder_spec.rb'
- - 'spec/lib/gitlab/metrics/dashboard/stages/grafana_formatter_spec.rb'
- - 'spec/lib/gitlab/metrics/dashboard/url_spec.rb'
- - 'spec/lib/gitlab/metrics/dashboard/validator_spec.rb'
- 'spec/lib/gitlab/metrics/elasticsearch_rack_middleware_spec.rb'
- 'spec/lib/gitlab/metrics/method_call_spec.rb'
- 'spec/lib/gitlab/metrics/rails_slis_spec.rb'
@@ -3992,9 +3884,6 @@ Layout/LineLength:
- 'spec/lib/gitlab/path_regex_spec.rb'
- 'spec/lib/gitlab/performance_bar/redis_adapter_when_peek_enabled_spec.rb'
- 'spec/lib/gitlab/project_search_results_spec.rb'
- - 'spec/lib/gitlab/prometheus/queries/deployment_query_spec.rb'
- - 'spec/lib/gitlab/prometheus/queries/matched_metric_query_spec.rb'
- - 'spec/lib/gitlab/prometheus/query_variables_spec.rb'
- 'spec/lib/gitlab/prometheus_client_spec.rb'
- 'spec/lib/gitlab/puma_logging/json_formatter_spec.rb'
- 'spec/lib/gitlab/quick_actions/dsl_spec.rb'
@@ -4206,8 +4095,6 @@ Layout/LineLength:
- 'spec/models/merge_request_diff_commit_spec.rb'
- 'spec/models/merge_request_diff_spec.rb'
- 'spec/models/merge_request_spec.rb'
- - 'spec/models/metrics/dashboard/annotation_spec.rb'
- - 'spec/models/metrics/users_starred_dashboard_spec.rb'
- 'spec/models/milestone_spec.rb'
- 'spec/models/namespace/package_setting_spec.rb'
- 'spec/models/namespace_setting_spec.rb'
@@ -4227,8 +4114,6 @@ Layout/LineLength:
- 'spec/models/packages/package_file_spec.rb'
- 'spec/models/packages/package_spec.rb'
- 'spec/models/pages/virtual_domain_spec.rb'
- - 'spec/models/performance_monitoring/prometheus_dashboard_spec.rb'
- - 'spec/models/performance_monitoring/prometheus_panel_spec.rb'
- 'spec/models/personal_access_token_spec.rb'
- 'spec/models/postgresql/detached_partition_spec.rb'
- 'spec/models/postgresql/replication_slot_spec.rb'
@@ -4356,8 +4241,6 @@ Layout/LineLength:
- 'spec/requests/api/graphql/group/container_repositories_spec.rb'
- 'spec/requests/api/graphql/group/dependency_proxy_image_ttl_policy_spec.rb'
- 'spec/requests/api/graphql/group/milestones_spec.rb'
- - 'spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb'
- - 'spec/requests/api/graphql/metrics/dashboard_query_spec.rb'
- 'spec/requests/api/graphql/milestone_spec.rb'
- 'spec/requests/api/graphql/mutations/ci/job_token_scope/add_project_spec.rb'
- 'spec/requests/api/graphql/mutations/ci/job_token_scope/remove_project_spec.rb'
@@ -4379,10 +4262,8 @@ Layout/LineLength:
- 'spec/requests/api/graphql/mutations/snippets/create_spec.rb'
- 'spec/requests/api/graphql/mutations/work_items/create_from_task_spec.rb'
- 'spec/requests/api/graphql/mutations/work_items/delete_spec.rb'
- - 'spec/requests/api/graphql/namespace/root_storage_statistics_spec.rb'
- 'spec/requests/api/graphql/namespace_query_spec.rb'
- 'spec/requests/api/graphql/packages/package_spec.rb'
- - 'spec/requests/api/graphql/project/alert_management/alert/metrics_dashboard_url_spec.rb'
- 'spec/requests/api/graphql/project/alert_management/alert/notes_spec.rb'
- 'spec/requests/api/graphql/project/alert_management/alerts_spec.rb'
- 'spec/requests/api/graphql/project/alert_management/integrations_spec.rb'
@@ -4463,7 +4344,6 @@ Layout/LineLength:
- 'spec/requests/api/unleash_spec.rb'
- 'spec/requests/api/user_counts_spec.rb'
- 'spec/requests/api/users_spec.rb'
- - 'spec/requests/api/v3/github_spec.rb'
- 'spec/requests/dashboard_controller_spec.rb'
- 'spec/requests/git_http_spec.rb'
- 'spec/requests/groups/milestones_controller_spec.rb'
@@ -4485,8 +4365,6 @@ Layout/LineLength:
- 'spec/requests/projects/merge_requests/context_commit_diffs_spec.rb'
- 'spec/requests/projects/merge_requests_discussions_spec.rb'
- 'spec/requests/projects/merge_requests_spec.rb'
- - 'spec/requests/projects/metrics/dashboards/builder_spec.rb'
- - 'spec/requests/projects/noteable_notes_spec.rb'
- 'spec/requests/projects/settings/access_tokens_controller_spec.rb'
- 'spec/requests/projects/tags_controller_spec.rb'
- 'spec/requests/projects_controller_spec.rb'
@@ -4501,7 +4379,6 @@ Layout/LineLength:
- 'spec/routing/projects/security/configuration_controller_routing_spec.rb'
- 'spec/routing/routing_spec.rb'
- 'spec/rubocop/cop/gitlab/mark_used_feature_flags_spec.rb'
- - 'spec/rubocop/cop/lint/last_keyword_argument_spec.rb'
- 'spec/rubocop/cop/migration/safer_boolean_column_spec.rb'
- 'spec/rubocop/cop/performance/readlines_each_spec.rb'
- 'spec/rubocop/cop/rspec/env_assignment_spec.rb'
@@ -4667,7 +4544,6 @@ Layout/LineLength:
- 'spec/services/merge_requests/create_from_issue_service_spec.rb'
- 'spec/services/merge_requests/create_pipeline_service_spec.rb'
- 'spec/services/merge_requests/create_service_spec.rb'
- - 'spec/services/merge_requests/ff_merge_service_spec.rb'
- 'spec/services/merge_requests/get_urls_service_spec.rb'
- 'spec/services/merge_requests/handle_assignees_change_service_spec.rb'
- 'spec/services/merge_requests/link_lfs_objects_service_spec.rb'
@@ -4681,11 +4557,6 @@ Layout/LineLength:
- 'spec/services/merge_requests/request_review_service_spec.rb'
- 'spec/services/merge_requests/squash_service_spec.rb'
- 'spec/services/merge_requests/update_service_spec.rb'
- - 'spec/services/metrics/dashboard/annotations/create_service_spec.rb'
- - 'spec/services/metrics/dashboard/clone_dashboard_service_spec.rb'
- - 'spec/services/metrics/dashboard/update_dashboard_service_spec.rb'
- - 'spec/services/metrics/users_starred_dashboards/create_service_spec.rb'
- - 'spec/services/metrics/users_starred_dashboards/delete_service_spec.rb'
- 'spec/services/milestones/transfer_service_spec.rb'
- 'spec/services/namespace_settings/update_service_spec.rb'
- 'spec/services/notes/build_service_spec.rb'
@@ -4704,7 +4575,6 @@ Layout/LineLength:
- 'spec/services/packages/debian/extract_changes_metadata_service_spec.rb'
- 'spec/services/packages/debian/extract_metadata_service_spec.rb'
- 'spec/services/packages/debian/parse_debian822_service_spec.rb'
- - 'spec/services/packages/debian/process_changes_service_spec.rb'
- 'spec/services/packages/debian/sign_distribution_service_spec.rb'
- 'spec/services/packages/debian/update_distribution_service_spec.rb'
- 'spec/services/packages/generic/create_package_file_service_spec.rb'
@@ -4738,7 +4608,6 @@ Layout/LineLength:
- 'spec/services/projects/fork_service_spec.rb'
- 'spec/services/projects/git_deduplication_service_spec.rb'
- 'spec/services/projects/group_links/destroy_service_spec.rb'
- - 'spec/services/projects/hashed_storage/migrate_repository_service_spec.rb'
- 'spec/services/projects/hashed_storage/migration_service_spec.rb'
- 'spec/services/projects/import_error_filter_spec.rb'
- 'spec/services/projects/import_export/export_service_spec.rb'
@@ -4889,7 +4758,6 @@ Layout/LineLength:
- 'spec/support/shared_examples/lib/gitlab/sidekiq_middleware/metrics_middleware_with_worker_attribution_shared_examples.rb'
- 'spec/support/shared_examples/lib/gitlab/sidekiq_middleware/strategy_shared_examples.rb'
- 'spec/support/shared_examples/lib/gitlab/usage_data_counters/code_review_extension_request_examples.rb'
- - 'spec/support/shared_examples/lib/gitlab/usage_data_counters/issuable_activity_shared_examples.rb'
- 'spec/support/shared_examples/loose_foreign_keys/have_loose_foreign_key.rb'
- 'spec/support/shared_examples/mailers/notify_shared_examples.rb'
- 'spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb'
@@ -4924,7 +4792,6 @@ Layout/LineLength:
- 'spec/support/shared_examples/models/wiki_shared_examples.rb'
- 'spec/support/shared_examples/namespaces/traversal_examples.rb'
- 'spec/support/shared_examples/namespaces/traversal_scope_examples.rb'
- - 'spec/support/shared_examples/nav_sidebar_shared_examples.rb'
- 'spec/support/shared_examples/policies/project_policy_shared_examples.rb'
- 'spec/support/shared_examples/quick_actions/issue/clone_quick_action_shared_examples.rb'
- 'spec/support/shared_examples/quick_actions/issue/move_quick_action_shared_examples.rb'
@@ -4967,7 +4834,6 @@ Layout/LineLength:
- 'spec/support/shared_examples/services/jira/requests/base_shared_examples.rb'
- 'spec/support/shared_examples/services/jira_import/user_mapper_services_shared_examples.rb'
- 'spec/support/shared_examples/services/merge_request_shared_examples.rb'
- - 'spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb'
- 'spec/support/shared_examples/services/namespace_package_settings_shared_examples.rb'
- 'spec/support/shared_examples/services/packages/debian/generate_distribution_shared_examples.rb'
- 'spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb'
@@ -4990,7 +4856,6 @@ Layout/LineLength:
- 'spec/tasks/gitlab/db/validate_config_rake_spec.rb'
- 'spec/tasks/gitlab/db_rake_spec.rb'
- 'spec/tasks/gitlab/external_diffs_rake_spec.rb'
- - 'spec/tasks/gitlab/generate_sample_prometheus_data_rake_spec.rb'
- 'spec/tasks/gitlab/gitaly_rake_spec.rb'
- 'spec/tasks/gitlab/ldap_rake_spec.rb'
- 'spec/tasks/gitlab/lfs/check_rake_spec.rb'
@@ -5051,10 +4916,8 @@ Layout/LineLength:
- 'spec/workers/authorized_project_update/project_recalculate_per_user_worker_spec.rb'
- 'spec/workers/authorized_project_update/user_refresh_from_replica_worker_spec.rb'
- 'spec/workers/auto_devops/disable_worker_spec.rb'
- - 'spec/workers/build_success_worker_spec.rb'
- 'spec/workers/bulk_import_worker_spec.rb'
- 'spec/workers/bulk_imports/export_request_worker_spec.rb'
- - 'spec/workers/bulk_imports/stuck_import_worker_spec.rb'
- 'spec/workers/ci/job_artifacts/expire_project_build_artifacts_worker_spec.rb'
- 'spec/workers/ci/merge_requests/add_todo_when_build_fails_worker_spec.rb'
- 'spec/workers/ci/pending_builds/update_group_worker_spec.rb'
diff --git a/.rubocop_todo/layout/parameter_alignment.yml b/.rubocop_todo/layout/parameter_alignment.yml
index 8a20a207d4f..15ff07dfe09 100644
--- a/.rubocop_todo/layout/parameter_alignment.yml
+++ b/.rubocop_todo/layout/parameter_alignment.yml
@@ -4,7 +4,6 @@ Layout/ParameterAlignment:
Exclude:
- 'lib/gitlab/cross_project_access.rb'
- 'lib/gitlab/data_builder/push.rb'
- - 'spec/lib/gitlab/background_migration/remove_occurrence_pipelines_and_duplicate_vulnerabilities_findings_spec.rb'
- 'spec/support/helpers/content_security_policy_helpers.rb'
- 'spec/support/helpers/migrations_helpers/vulnerabilities_helper.rb'
- 'spec/support/helpers/repo_helpers.rb'
diff --git a/.rubocop_todo/layout/space_in_lambda_literal.yml b/.rubocop_todo/layout/space_in_lambda_literal.yml
index 11e5649b57a..7e3af689b5f 100644
--- a/.rubocop_todo/layout/space_in_lambda_literal.yml
+++ b/.rubocop_todo/layout/space_in_lambda_literal.yml
@@ -28,7 +28,6 @@ Layout/SpaceInLambdaLiteral:
- 'app/models/concerns/ci/has_status.rb'
- 'app/models/concerns/ci/has_variable.rb'
- 'app/models/concerns/has_environment_scope.rb'
- - 'app/models/concerns/has_unique_internal_users.rb'
- 'app/models/concerns/id_in_ordered.rb'
- 'app/models/concerns/incident_management/escalatable.rb'
- 'app/models/concerns/issuable.rb'
@@ -272,7 +271,6 @@ Layout/SpaceInLambdaLiteral:
- 'ee/app/services/vulnerability_exports/exporters/csv_service.rb'
- 'ee/app/workers/update_all_mirrors_worker.rb'
- 'ee/lib/api/entities/pending_member.rb'
- - 'ee/lib/api/ml/ai_assist.rb'
- 'ee/lib/ee/api/entities/ci/job_request/response.rb'
- 'ee/lib/ee/api/entities/epic.rb'
- 'ee/lib/ee/api/entities/issue.rb'
@@ -323,7 +321,6 @@ Layout/SpaceInLambdaLiteral:
- 'lib/api/merge_requests.rb'
- 'lib/api/metadata.rb'
- 'lib/api/metrics/dashboard/annotations.rb'
- - 'lib/api/ml/mlflow.rb'
- 'lib/api/releases.rb'
- 'lib/api/settings.rb'
- 'lib/api/tags.rb'
@@ -359,7 +356,6 @@ Layout/SpaceInLambdaLiteral:
- 'lib/gitlab/health_checks/server.rb'
- 'lib/gitlab/import_export/import_failure_service.rb'
- 'lib/gitlab/merge_requests/message_generator.rb'
- - 'lib/gitlab/metrics/dashboard/transformers/yml/v1/prometheus_metrics.rb'
- 'lib/gitlab/metrics/exporter/base_exporter.rb'
- 'lib/gitlab/visibility_level.rb'
- 'spec/deprecation_toolkit_env.rb'
diff --git a/.rubocop_todo/layout/space_inside_parens.yml b/.rubocop_todo/layout/space_inside_parens.yml
index 4d067749bd0..34f13f780fb 100644
--- a/.rubocop_todo/layout/space_inside_parens.yml
+++ b/.rubocop_todo/layout/space_inside_parens.yml
@@ -138,7 +138,6 @@ Layout/SpaceInsideParens:
- 'spec/lib/gitlab/ci/templates/terraform_latest_gitlab_ci_yaml_spec.rb'
- 'spec/lib/gitlab/database/migrations/runner_spec.rb'
- 'spec/lib/gitlab/database/reindexing/reindex_concurrently_spec.rb'
- - 'spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb'
- 'spec/lib/gitlab/database_spec.rb'
- 'spec/lib/gitlab/diff/highlight_cache_spec.rb'
- 'spec/lib/gitlab/exclusive_lease_helpers_spec.rb'
@@ -146,7 +145,6 @@ Layout/SpaceInsideParens:
- 'spec/lib/gitlab/git/commit_spec.rb'
- 'spec/lib/gitlab/git/diff_spec.rb'
- 'spec/lib/gitlab/git/repository_spec.rb'
- - 'spec/lib/gitlab/github_import/importer/pull_request_merged_by_importer_spec.rb'
- 'spec/lib/gitlab/grape_logging/loggers/queue_duration_logger_spec.rb'
- 'spec/lib/gitlab/grape_logging/loggers/token_logger_spec.rb'
- 'spec/lib/gitlab/graphql/lazy_spec.rb'
@@ -160,7 +158,6 @@ Layout/SpaceInsideParens:
- 'spec/lib/gitlab/import_export/recursive_merge_folders_spec.rb'
- 'spec/lib/gitlab/issuables_count_for_state_spec.rb'
- 'spec/lib/gitlab/kubernetes/rollout_status_spec.rb'
- - 'spec/lib/gitlab/metrics/dashboard/processor_spec.rb'
- 'spec/lib/gitlab/middleware/same_site_cookies_spec.rb'
- 'spec/lib/gitlab/redis/cache_spec.rb'
- 'spec/lib/gitlab/redis/queues_spec.rb'
diff --git a/.rubocop_todo/layout/trailing_whitespace.yml b/.rubocop_todo/layout/trailing_whitespace.yml
index 35db371a055..26d0b8f5f13 100644
--- a/.rubocop_todo/layout/trailing_whitespace.yml
+++ b/.rubocop_todo/layout/trailing_whitespace.yml
@@ -5,7 +5,6 @@ Layout/TrailingWhitespace:
- 'app/models/concerns/analytics/cycle_analytics/stage_event_model.rb'
- 'db/migrate/20220913082728_drop_index_cadence_create_iterations_automation.rb'
- 'db/post_migrate/20220816163444_update_start_date_for_iterations_cadences.rb'
- - 'lib/gitlab/background_migration/fix_duplicate_project_name_and_path.rb'
- 'lib/gitlab/pagination/keyset/sql_type_missing_error.rb'
- 'spec/services/suggestions/apply_service_spec.rb'
- 'spec/support/helpers/x509_helpers.rb'
diff --git a/.rubocop_todo/lint/ambiguous_operator_precedence.yml b/.rubocop_todo/lint/ambiguous_operator_precedence.yml
index fc049cb68f0..5b352842a87 100644
--- a/.rubocop_todo/lint/ambiguous_operator_precedence.yml
+++ b/.rubocop_todo/lint/ambiguous_operator_precedence.yml
@@ -45,7 +45,6 @@ Lint/AmbiguousOperatorPrecedence:
- 'ee/spec/models/ee/audit_event_spec.rb'
- 'ee/spec/models/ee/iterations/cadence_spec.rb'
- 'ee/spec/models/ee/project_statistics_spec.rb'
- - 'ee/spec/models/geo/project_registry_spec.rb'
- 'ee/spec/models/license_spec.rb'
- 'ee/spec/models/security/finding_spec.rb'
- 'ee/spec/models/status_page/project_setting_spec.rb'
@@ -127,7 +126,6 @@ Lint/AmbiguousOperatorPrecedence:
- 'spec/models/integrations/chat_message/push_message_spec.rb'
- 'spec/models/merge_request_diff_spec.rb'
- 'spec/models/packages/package_file_spec.rb'
- - 'spec/models/project_metrics_setting_spec.rb'
- 'spec/models/prometheus_alert_spec.rb'
- 'spec/requests/api/pypi_packages_spec.rb'
- 'spec/requests/lfs_http_spec.rb'
@@ -135,7 +133,6 @@ Lint/AmbiguousOperatorPrecedence:
- 'spec/services/issues/relative_position_rebalancing_service_spec.rb'
- 'spec/services/web_hook_service_spec.rb'
- 'spec/support/helpers/dependency_proxy_helpers.rb'
- - 'spec/support/models/ci/partitioning_testing/cascade_check.rb'
- 'spec/support/shared_examples/features/sidebar_shared_examples.rb'
- 'spec/support/shared_examples/models/relative_positioning_shared_examples.rb'
- 'spec/support/shared_examples/services/packages/debian/generate_distribution_shared_examples.rb'
diff --git a/.rubocop_todo/lint/ambiguous_regexp_literal.yml b/.rubocop_todo/lint/ambiguous_regexp_literal.yml
index 4754e381780..4af24464e8e 100644
--- a/.rubocop_todo/lint/ambiguous_regexp_literal.yml
+++ b/.rubocop_todo/lint/ambiguous_regexp_literal.yml
@@ -26,7 +26,6 @@ Lint/AmbiguousRegexpLiteral:
- 'spec/features/atom/users_spec.rb'
- 'spec/features/issues/user_creates_branch_and_merge_request_spec.rb'
- 'spec/features/issues/user_creates_issue_spec.rb'
- - 'spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb'
- 'spec/helpers/labels_helper_spec.rb'
- 'spec/helpers/users_helper_spec.rb'
- 'spec/helpers/visibility_level_helper_spec.rb'
diff --git a/.rubocop_todo/lint/assignment_in_condition.yml b/.rubocop_todo/lint/assignment_in_condition.yml
index ae2d672b4bc..4d2158b7a9d 100644
--- a/.rubocop_todo/lint/assignment_in_condition.yml
+++ b/.rubocop_todo/lint/assignment_in_condition.yml
@@ -32,7 +32,6 @@ Lint/AssignmentInCondition:
- 'app/models/concerns/after_commit_queue.rb'
- 'app/models/concerns/atomic_internal_id.rb'
- 'app/models/concerns/bulk_insert_safe.rb'
- - 'app/models/concerns/has_unique_internal_users.rb'
- 'app/models/concerns/subscribable.rb'
- 'app/models/design_management/design_collection.rb'
- 'app/models/diff_note.rb'
@@ -50,7 +49,6 @@ Lint/AssignmentInCondition:
- 'app/services/ci/find_exposed_artifacts_service.rb'
- 'app/services/ci/runners/register_runner_service.rb'
- 'app/services/clusters/agents/authorize_proxy_user_service.rb'
- - 'app/services/deployments/create_for_build_service.rb'
- 'app/services/deployments/create_service.rb'
- 'app/services/deployments/link_merge_requests_service.rb'
- 'app/services/deployments/update_environment_service.rb'
@@ -62,7 +60,6 @@ Lint/AssignmentInCondition:
- 'app/services/lfs/file_transformer.rb'
- 'app/services/merge_requests/base_service.rb'
- 'app/services/merge_requests/mergeability_check_service.rb'
- - 'app/services/metrics/dashboard/dynamic_embed_service.rb'
- 'app/services/packages/debian/parse_debian822_service.rb'
- 'app/services/projects/operations/update_service.rb'
- 'app/services/projects/prometheus/alerts/notify_service.rb'
@@ -145,7 +142,6 @@ Lint/AssignmentInCondition:
- 'lib/banzai/filter/broadcast_message_placeholders_filter.rb'
- 'lib/banzai/filter/footnote_filter.rb'
- 'lib/banzai/filter/gollum_tags_filter.rb'
- - 'lib/banzai/filter/inline_observability_filter.rb'
- 'lib/banzai/filter/references/abstract_reference_filter.rb'
- 'lib/banzai/filter/references/merge_request_reference_filter.rb'
- 'lib/banzai/filter/references/project_reference_filter.rb'
@@ -182,7 +178,6 @@ Lint/AssignmentInCondition:
- 'lib/gitlab/database/load_balancing/wal_tracking_sender.rb'
- 'lib/gitlab/database/partitioning/monthly_strategy.rb'
- 'lib/gitlab/database/partitioning/partition_manager.rb'
- - 'lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base.rb'
- 'lib/gitlab/database/shared_model.rb'
- 'lib/gitlab/diff/formatters/base_formatter.rb'
- 'lib/gitlab/diff/lines_unfolder.rb'
@@ -225,7 +220,6 @@ Lint/AssignmentInCondition:
- 'lib/gitlab/usage_data/topology.rb'
- 'lib/gitlab/usage_data_counters/ci_template_unique_counter.rb'
- 'lib/gitlab/utils/merge_hash.rb'
- - 'lib/gitlab/version_info.rb'
- 'lib/gitlab/webpack/dev_server_middleware.rb'
- 'lib/gitlab/wiki_pages/front_matter_parser.rb'
- 'lib/prometheus/pid_provider.rb'
diff --git a/.rubocop_todo/lint/constant_definition_in_block.yml b/.rubocop_todo/lint/constant_definition_in_block.yml
index 98cda7a7f66..eacc6e2f937 100644
--- a/.rubocop_todo/lint/constant_definition_in_block.yml
+++ b/.rubocop_todo/lint/constant_definition_in_block.yml
@@ -36,7 +36,6 @@ Lint/ConstantDefinitionInBlock:
- 'lib/tasks/gitlab/db/validate_config.rake'
- 'lib/tasks/gitlab/docs/compile_deprecations.rake'
- 'lib/tasks/gitlab/graphql.rake'
- - 'lib/tasks/gitlab/metrics_exporter.rake'
- 'lib/tasks/gitlab/refresh_project_statistics_build_artifacts_size.rake'
- 'lib/tasks/gitlab/snippets.rake'
- 'lib/tasks/gitlab/tw/codeowners.rake'
diff --git a/.rubocop_todo/lint/empty_block.yml b/.rubocop_todo/lint/empty_block.yml
index dbcef42eab4..bc538062538 100644
--- a/.rubocop_todo/lint/empty_block.yml
+++ b/.rubocop_todo/lint/empty_block.yml
@@ -109,7 +109,6 @@ Lint/EmptyBlock:
- 'spec/lib/gitlab/database/shared_model_spec.rb'
- 'spec/lib/gitlab/database/with_lock_retries_outside_transaction_spec.rb'
- 'spec/lib/gitlab/database/with_lock_retries_spec.rb'
- - 'spec/lib/gitlab/database_importers/common_metrics/importer_spec.rb'
- 'spec/lib/gitlab/database_spec.rb'
- 'spec/lib/gitlab/etag_caching/router/graphql_spec.rb'
- 'spec/lib/gitlab/exclusive_lease_helpers_spec.rb'
@@ -120,8 +119,6 @@ Lint/EmptyBlock:
- 'spec/lib/gitlab/github_import/client_spec.rb'
- 'spec/lib/gitlab/github_import/importer/protected_branches_importer_spec.rb'
- 'spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb'
- - 'spec/lib/gitlab/github_import/importer/pull_requests_merged_by_importer_spec.rb'
- - 'spec/lib/gitlab/github_import/importer/pull_requests_reviews_importer_spec.rb'
- 'spec/lib/gitlab/github_import/importer/single_endpoint_diff_notes_importer_spec.rb'
- 'spec/lib/gitlab/github_import/importer/single_endpoint_issue_notes_importer_spec.rb'
- 'spec/lib/gitlab/github_import/importer/single_endpoint_merge_request_notes_importer_spec.rb'
@@ -188,8 +185,6 @@ Lint/EmptyBlock:
- 'spec/services/ci/runners/register_runner_service_spec.rb'
- 'spec/services/ci/stuck_builds/drop_pending_service_spec.rb'
- 'spec/services/ci/stuck_builds/drop_scheduled_service_spec.rb'
- - 'spec/services/deployments/create_for_build_service_spec.rb'
- - 'spec/services/environments/create_for_build_service_spec.rb'
- 'spec/services/projects/lfs_pointers/lfs_object_download_list_service_spec.rb'
- 'spec/services/releases/destroy_service_spec.rb'
- 'spec/services/releases/update_service_spec.rb'
diff --git a/.rubocop_todo/lint/missing_cop_enable_directive.yml b/.rubocop_todo/lint/missing_cop_enable_directive.yml
index 1522d935008..00484af3b5d 100644
--- a/.rubocop_todo/lint/missing_cop_enable_directive.yml
+++ b/.rubocop_todo/lint/missing_cop_enable_directive.yml
@@ -3,11 +3,6 @@ Lint/MissingCopEnableDirective:
Exclude:
- 'app/controllers/admin/users_controller.rb'
- 'app/controllers/projects/forks_controller.rb'
- - 'app/graphql/resolvers/project_members_resolver.rb'
- - 'app/graphql/resolvers/project_milestones_resolver.rb'
- - 'app/graphql/resolvers/projects/snippets_resolver.rb'
- - 'app/graphql/resolvers/snippets_resolver.rb'
- - 'app/graphql/resolvers/users/snippets_resolver.rb'
- 'app/graphql/types/access_level_type.rb'
- 'app/graphql/types/base_enum.rb'
- 'app/graphql/types/boards/board_issue_input_base_type.rb'
@@ -63,10 +58,8 @@ Lint/MissingCopEnableDirective:
- 'app/services/notification_service.rb'
- 'app/services/projects/container_repository/third_party/delete_tags_service.rb'
- 'app/services/search/global_service.rb'
- - 'config/initializers_before_autoloader/003_gc_compact.rb'
- 'danger/feature_flag/Dangerfile'
- 'danger/pajamas/Dangerfile'
- - 'danger/z_metadata/Dangerfile'
- 'db/migrate/20220531024905_add_operations_access_levels_to_project_feature.rb'
- 'ee/app/controllers/ee/admin/dashboard_controller.rb'
- 'ee/app/controllers/ee/admin/groups_controller.rb'
@@ -84,19 +77,7 @@ Lint/MissingCopEnableDirective:
- 'ee/app/graphql/types/ci/code_quality_degradation_type.rb'
- 'ee/app/graphql/types/ci/minutes/namespace_monthly_usage_type.rb'
- 'ee/app/graphql/types/ci/minutes/project_monthly_usage_type.rb'
- - 'ee/app/graphql/types/compliance_management/compliance_framework_type.rb'
- 'ee/app/graphql/types/dast/profile_cadence_type.rb'
- - 'ee/app/graphql/types/geo/ci_secure_file_registry_type.rb'
- - 'ee/app/graphql/types/geo/group_wiki_repository_registry_type.rb'
- - 'ee/app/graphql/types/geo/job_artifact_registry_type.rb'
- - 'ee/app/graphql/types/geo/lfs_object_registry_type.rb'
- - 'ee/app/graphql/types/geo/merge_request_diff_registry_type.rb'
- - 'ee/app/graphql/types/geo/package_file_registry_type.rb'
- - 'ee/app/graphql/types/geo/pages_deployment_registry_type.rb'
- - 'ee/app/graphql/types/geo/pipeline_artifact_registry_type.rb'
- - 'ee/app/graphql/types/geo/snippet_repository_registry_type.rb'
- - 'ee/app/graphql/types/geo/terraform_state_version_registry_type.rb'
- - 'ee/app/graphql/types/geo/upload_registry_type.rb'
- 'ee/app/graphql/types/network_policy_type.rb'
- 'ee/app/graphql/types/scan_type.rb'
- 'ee/app/graphql/types/scanned_resource_type.rb'
@@ -169,17 +150,14 @@ Lint/MissingCopEnableDirective:
- 'lib/gitlab/gon_helper.rb'
- 'lib/gitlab/graphql/standard_graphql_error.rb'
- 'lib/gitlab/metrics/methods.rb'
- - 'lib/gitlab/patch/action_cable_redis_listener.rb'
- 'lib/gitlab/patch/prependable.rb'
- 'lib/gitlab/project_search_results.rb'
- 'lib/gitlab/testing/request_blocker_middleware.rb'
- 'lib/gitlab/testing/request_inspector_middleware.rb'
- 'lib/gitlab/testing/robots_blocker_middleware.rb'
- 'lib/unnested_in_filters/dsl.rb'
- - 'lib/unnested_in_filters/rewriter.rb'
- 'qa/qa/scenario/test/integration/registry_with_cdn.rb'
- 'spec/benchmarks/banzai_benchmark.rb'
- 'spec/lib/gitlab/sidekiq_middleware/size_limiter/server_spec.rb'
- 'spec/support/capybara.rb'
- - 'spec/support/google_api/cloud_platform_helpers.rb'
- 'tooling/danger/analytics_instrumentation.rb'
diff --git a/.rubocop_todo/lint/no_return_in_begin_end_blocks.yml b/.rubocop_todo/lint/no_return_in_begin_end_blocks.yml
index 8a1dad40af0..6fec56cc4f4 100644
--- a/.rubocop_todo/lint/no_return_in_begin_end_blocks.yml
+++ b/.rubocop_todo/lint/no_return_in_begin_end_blocks.yml
@@ -11,7 +11,6 @@ Lint/NoReturnInBeginEndBlocks:
- 'ee/lib/ee/gitlab/scim/filter_parser.rb'
- 'lib/api/internal/base.rb'
- 'lib/gitlab/git/repository.rb'
- - 'lib/gitlab/metrics/dashboard/importer.rb'
- 'lib/object_storage/config.rb'
- 'qa/qa/support/formatters/test_metrics_formatter.rb'
- 'qa/qa/support/influxdb_tools.rb'
diff --git a/.rubocop_todo/lint/non_atomic_file_operation.yml b/.rubocop_todo/lint/non_atomic_file_operation.yml
index d9d415de923..d5df1f9cc66 100644
--- a/.rubocop_todo/lint/non_atomic_file_operation.yml
+++ b/.rubocop_todo/lint/non_atomic_file_operation.yml
@@ -8,7 +8,6 @@ Lint/NonAtomicFileOperation:
- 'app/services/projects/import_export/parallel_export_service.rb'
- 'app/services/projects/import_export/relation_export_service.rb'
- 'app/services/projects/lfs_pointers/lfs_download_service.rb'
- - 'ee/spec/lib/ee/gitlab/usage_data_counters/hll_redis_counter_spec.rb'
- 'lib/bulk_imports/common/extractors/json_extractor.rb'
- 'lib/bulk_imports/common/extractors/ndjson_extractor.rb'
- 'lib/bulk_imports/common/pipelines/lfs_objects_pipeline.rb'
@@ -17,7 +16,6 @@ Lint/NonAtomicFileOperation:
- 'lib/bulk_imports/projects/pipelines/repository_bundle_pipeline.rb'
- 'lib/gitlab/ci/trace.rb'
- 'lib/gitlab/database/migrations/test_batched_background_runner.rb'
- - 'lib/gitlab/database/query_analyzers/query_recorder.rb'
- 'lib/gitlab/gpg.rb'
- 'lib/gitlab/import_export/after_export_strategies/base_after_export_strategy.rb'
- 'lib/gitlab/import_export/recursive_merge_folders.rb'
@@ -40,4 +38,3 @@ Lint/NonAtomicFileOperation:
- 'spec/services/bulk_imports/lfs_objects_export_service_spec.rb'
- 'spec/services/bulk_imports/repository_bundle_export_service_spec.rb'
- 'spec/services/bulk_imports/uploads_export_service_spec.rb'
- - 'spec/support/database/query_recorder.rb'
diff --git a/.rubocop_todo/lint/or_assignment_to_constant.yml b/.rubocop_todo/lint/or_assignment_to_constant.yml
index 505e7ba2935..64ebe9e49b9 100644
--- a/.rubocop_todo/lint/or_assignment_to_constant.yml
+++ b/.rubocop_todo/lint/or_assignment_to_constant.yml
@@ -3,5 +3,4 @@
Lint/OrAssignmentToConstant:
Exclude:
- 'lib/gitlab/email/handler/base_handler.rb'
- - 'lib/gitlab/utils.rb'
- 'tooling/danger/project_helper.rb'
diff --git a/.rubocop_todo/lint/redundant_cop_disable_directive.yml b/.rubocop_todo/lint/redundant_cop_disable_directive.yml
index 38ea0ea12e6..6a9b68a832a 100644
--- a/.rubocop_todo/lint/redundant_cop_disable_directive.yml
+++ b/.rubocop_todo/lint/redundant_cop_disable_directive.yml
@@ -49,7 +49,6 @@ Lint/RedundantCopDisableDirective:
- 'app/services/groups/import_export/import_service.rb'
- 'app/services/issues/export_csv_service.rb'
- 'app/services/labels/transfer_service.rb'
- - 'app/services/members/create_service.rb'
- 'app/services/members/projects/creator_service.rb'
- 'app/services/members/standard_member_builder.rb'
- 'app/services/projects/auto_devops/disable_service.rb'
@@ -92,8 +91,6 @@ Lint/RedundantCopDisableDirective:
- 'ee/app/controllers/ee/projects/settings/ci_cd_controller.rb'
- 'ee/app/controllers/groups/todos_controller.rb'
- 'ee/app/finders/epics/with_issues_finder.rb'
- - 'ee/app/finders/geo/file_registry_finder.rb'
- - 'ee/app/finders/geo/project_registry_finder.rb'
- 'ee/app/finders/geo/registry_finder.rb'
- 'ee/app/finders/status_page/incident_comments_finder.rb'
- 'ee/app/finders/status_page/incidents_finder.rb'
@@ -123,7 +120,6 @@ Lint/RedundantCopDisableDirective:
- 'ee/app/services/ee/search_service.rb'
- 'ee/app/services/security/token_revocation_service.rb'
- 'ee/app/workers/ee/issuable_export_csv_worker.rb'
- - 'ee/app/workers/geo/design_repository_shard_sync_worker.rb'
- 'ee/app/workers/geo/repository_shard_sync_worker.rb'
- 'ee/app/workers/geo/repository_verification/secondary/shard_worker.rb'
- 'ee/app/workers/scan_security_report_secrets_worker.rb'
@@ -167,7 +163,6 @@ Lint/RedundantCopDisableDirective:
- 'lib/api/helpers.rb'
- 'lib/api/issue_links.rb'
- 'lib/backup/manager.rb'
- - 'lib/bulk_imports/common/transformers/user_reference_transformer.rb'
- 'lib/bulk_imports/pipeline/runner.rb'
- 'lib/container_registry/tag.rb'
- 'lib/event_filter.rb'
@@ -201,13 +196,13 @@ Lint/RedundantCopDisableDirective:
- 'lib/gitlab/database/migration.rb'
- 'lib/gitlab/database/migrations/observation.rb'
- 'lib/gitlab/database/migrations/observers/query_log.rb'
- - 'lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces.rb'
- 'lib/gitlab/diff/file.rb'
- 'lib/gitlab/diff/file_collection/paginated_diffs.rb'
- 'lib/gitlab/diff/pair_selector.rb'
- 'lib/gitlab/diff/parser.rb'
- 'lib/gitlab/encrypted_incoming_email_command.rb'
- 'lib/gitlab/encrypted_ldap_command.rb'
+ - 'lib/gitlab/encrypted_redis_command.rb'
- 'lib/gitlab/encrypted_service_desk_email_command.rb'
- 'lib/gitlab/encrypted_smtp_command.rb'
- 'lib/gitlab/git/commit.rb'
@@ -247,7 +242,6 @@ Lint/RedundantCopDisableDirective:
- 'spec/components/previews/pajamas/banner_component_preview.rb'
- 'spec/controllers/concerns/preferred_language_switcher_spec.rb'
- 'spec/controllers/profiles/two_factor_auths_controller_spec.rb'
- - 'spec/finders/personal_access_tokens_finder_spec.rb'
- 'spec/frontend/fixtures/merge_requests.rb'
- 'spec/graphql/mutations/clusters/agent_tokens/create_spec.rb'
- 'spec/graphql/mutations/clusters/agents/create_spec.rb'
diff --git a/.rubocop_todo/lint/redundant_dir_glob_sort.yml b/.rubocop_todo/lint/redundant_dir_glob_sort.yml
index 1e4afd562f0..c992f5647a7 100644
--- a/.rubocop_todo/lint/redundant_dir_glob_sort.yml
+++ b/.rubocop_todo/lint/redundant_dir_glob_sort.yml
@@ -6,5 +6,4 @@ Lint/RedundantDirGlobSort:
- 'config/application.rb'
- 'ee/spec/spec_helper.rb'
- 'qa/qa/specs/spec_helper.rb'
- - 'rubocop/rubocop.rb'
- 'spec/spec_helper.rb'
diff --git a/.rubocop_todo/lint/symbol_conversion.yml b/.rubocop_todo/lint/symbol_conversion.yml
index 0fb6641d2a7..1fcb4eecf8b 100644
--- a/.rubocop_todo/lint/symbol_conversion.yml
+++ b/.rubocop_todo/lint/symbol_conversion.yml
@@ -2,7 +2,6 @@
# Cop supports --autocorrect.
Lint/SymbolConversion:
Exclude:
- - 'app/controllers/projects/environments/sample_metrics_controller.rb'
- 'app/helpers/breadcrumbs_helper.rb'
- 'app/helpers/environments_helper.rb'
- 'app/helpers/tooling/visual_review_helper.rb'
@@ -47,7 +46,6 @@ Lint/SymbolConversion:
- 'ee/spec/services/elastic/data_migration_service_spec.rb'
- 'ee/spec/services/security/token_revocation_service_spec.rb'
- 'ee/spec/support/helpers/subscription_portal_helpers.rb'
- - 'ee/spec/support/prometheus/additional_metrics_shared_examples.rb'
- 'ee/spec/workers/deployments/approval_worker_spec.rb'
- 'lib/gitlab/auth/otp/strategies/forti_token_cloud.rb'
- 'lib/gitlab/data_builder/repository.rb'
@@ -64,7 +62,6 @@ Lint/SymbolConversion:
- 'qa/qa/specs/features/ee/browser_ui/10_govern/project_security_dashboard_spec.rb'
- 'qa/spec/resource/events/project_spec.rb'
- 'qa/spec/specs/allure_report_spec.rb'
- - 'spec/controllers/import/gitlab_controller_spec.rb'
- 'spec/controllers/jira_connect/branches_controller_spec.rb'
- 'spec/factories/ci/reports/codequality_degradations.rb'
- 'spec/factories/evidences.rb'
@@ -93,7 +90,6 @@ Lint/SymbolConversion:
- 'spec/lib/gitlab/ci/config/entry/job_spec.rb'
- 'spec/lib/gitlab/ci/config/entry/jobs_spec.rb'
- 'spec/lib/gitlab/ci/config/entry/processable_spec.rb'
- - 'spec/lib/gitlab/ci/interpolation/template_spec.rb'
- 'spec/lib/gitlab/ci/parsers/accessibility/pa11y_spec.rb'
- 'spec/lib/gitlab/ci/parsers/codequality/code_climate_spec.rb'
- 'spec/lib/gitlab/ci/reports/accessibility_reports_spec.rb'
@@ -138,13 +134,10 @@ Lint/SymbolConversion:
- 'spec/services/incident_management/timeline_event_tags/create_service_spec.rb'
- 'spec/services/jira_connect/sync_service_spec.rb'
- 'spec/services/ml/experiment_tracking/candidate_repository_spec.rb'
- - 'spec/support/google_api/cloud_platform_helpers.rb'
- 'spec/support/helpers/kubernetes_helpers.rb'
- 'spec/support/helpers/prometheus_helpers.rb'
- - 'spec/support/prometheus/additional_metrics_shared_examples.rb'
- 'spec/support/shared_examples/harbor/artifacts_controller_shared_examples.rb'
- 'spec/support/shared_examples/harbor/repositories_controller_shared_examples.rb'
- 'spec/support/shared_examples/harbor/tags_controller_shared_examples.rb'
- 'spec/support/shared_examples/models/diff_positionable_note_shared_examples.rb'
- - 'spec/views/admin/application_settings/_repository_storage.html.haml_spec.rb'
- 'spec/workers/gitlab/github_gists_import/import_gist_worker_spec.rb'
diff --git a/.rubocop_todo/lint/unused_block_argument.yml b/.rubocop_todo/lint/unused_block_argument.yml
index a6578a63918..70c836e28ac 100644
--- a/.rubocop_todo/lint/unused_block_argument.yml
+++ b/.rubocop_todo/lint/unused_block_argument.yml
@@ -68,7 +68,6 @@ Lint/UnusedBlockArgument:
- 'app/services/authorized_project_update/find_records_due_for_refresh_service.rb'
- 'app/services/design_management/copy_design_collection/copy_service.rb'
- 'app/services/environments/stop_stale_service.rb'
- - 'app/services/prometheus/proxy_service.rb'
- 'app/services/resource_events/synthetic_label_notes_builder_service.rb'
- 'app/services/web_hooks/log_execution_service.rb'
- 'app/uploaders/object_storage.rb'
@@ -83,8 +82,6 @@ Lint/UnusedBlockArgument:
- 'config/initializers/warden.rb'
- 'config/routes/project.rb'
- 'config/routes/wiki.rb'
- - 'ee/app/finders/security/training_providers/kontra_url_finder.rb'
- - 'ee/app/finders/security/training_providers/secure_code_warrior_url_finder.rb'
- 'ee/app/graphql/resolvers/incident_management/escalation_policies_resolver.rb'
- 'ee/app/graphql/resolvers/incident_management/oncall_rotations_resolver.rb'
- 'ee/app/graphql/types/health_status_enum.rb'
@@ -96,7 +93,6 @@ Lint/UnusedBlockArgument:
- 'ee/app/serializers/ee/build_detail_entity.rb'
- 'ee/app/serializers/ee/merge_request_widget_entity.rb'
- 'ee/app/serializers/ee/note_entity.rb'
- - 'ee/app/serializers/ee/note_user_entity.rb'
- 'ee/app/serializers/epic_entity.rb'
- 'ee/app/serializers/integrations/zentao_serializers/issue_entity.rb'
- 'ee/app/serializers/security/vulnerability_report_data_entity.rb'
@@ -186,7 +182,6 @@ Lint/UnusedBlockArgument:
- 'lib/api/entities/todo.rb'
- 'lib/api/entities/tree_object.rb'
- 'lib/api/entities/user_basic.rb'
- - 'lib/api/github/entities.rb'
- 'lib/api/group_variables.rb'
- 'lib/api/helpers/authentication.rb'
- 'lib/api/helpers/container_registry_helpers.rb'
@@ -195,7 +190,6 @@ Lint/UnusedBlockArgument:
- 'lib/atlassian/jira_connect/serializers/repository_entity.rb'
- 'lib/banzai/filter/autolink_filter.rb'
- 'lib/banzai/filter/emoji_filter.rb'
- - 'lib/banzai/filter/inline_metrics_redactor_filter.rb'
- 'lib/banzai/filter/markdown_post_escape_filter.rb'
- 'lib/banzai/filter/references/abstract_reference_filter.rb'
- 'lib/banzai/filter/spaced_link_filter.rb'
@@ -229,9 +223,6 @@ Lint/UnusedBlockArgument:
- 'lib/gitlab/health_checks/probes/collection.rb'
- 'lib/gitlab/health_checks/server.rb'
- 'lib/gitlab/import_export/members_mapper.rb'
- - 'lib/gitlab/metrics/dashboard/stages/variable_endpoint_inserter.rb'
- - 'lib/gitlab/metrics/dashboard/transformers/yml/v1/prometheus_metrics.rb'
- - 'lib/gitlab/metrics/dashboard/validator/custom_formats.rb'
- 'lib/gitlab/metrics/exporter/base_exporter.rb'
- 'lib/gitlab/pagination/gitaly_keyset_pager.rb'
- 'lib/gitlab/sidekiq_config.rb'
@@ -254,7 +245,6 @@ Lint/UnusedBlockArgument:
- 'lib/tasks/gitlab/external_diffs.rake'
- 'lib/tasks/gitlab/gitaly.rake'
- 'lib/tasks/gitlab/graphql.rake'
- - 'lib/tasks/gitlab/metrics_exporter.rake'
- 'lib/tasks/gitlab/praefect.rake'
- 'lib/tasks/gitlab/seed.rake'
- 'lib/tasks/gitlab/seed/group_seed.rake'
@@ -268,7 +258,6 @@ Lint/UnusedBlockArgument:
- 'qa/qa/runtime/allure_report.rb'
- 'qa/qa/service/praefect_manager.rb'
- 'qa/qa/specs/features/api/12_systems/gitaly/distributed_reads_spec.rb'
- - 'qa/qa/specs/features/api/12_systems/gitaly/praefect_dataloss_spec.rb'
- 'qa/qa/support/knapsack_report.rb'
- 'qa/qa/support/matchers/eventually_matcher.rb'
- 'qa/qa/tools/generate_perf_testdata.rb'
@@ -323,7 +312,6 @@ Lint/UnusedBlockArgument:
- 'spec/lib/banzai/filter/video_link_filter_spec.rb'
- 'spec/lib/feature_spec.rb'
- 'spec/lib/gitlab/auth/saml/user_spec.rb'
- - 'spec/lib/gitlab/background_migration/cleanup_draft_data_from_faulty_regex_spec.rb'
- 'spec/lib/gitlab/ci/parsers/terraform/tfplan_spec.rb'
- 'spec/lib/gitlab/ci/parsers/test/junit_spec.rb'
- 'spec/lib/gitlab/ci/pipeline/logger_spec.rb'
@@ -332,7 +320,6 @@ Lint/UnusedBlockArgument:
- 'spec/lib/gitlab/database/migration_helpers/restrict_gitlab_schema_spec.rb'
- 'spec/lib/gitlab/database/migrations/test_background_runner_spec.rb'
- 'spec/lib/gitlab/database/partitioning/detached_partition_dropper_spec.rb'
- - 'spec/lib/gitlab/database/rename_reserved_paths_migration/v1_spec.rb'
- 'spec/lib/gitlab/git_access_wiki_spec.rb'
- 'spec/lib/gitlab/graphql/tracers/application_context_tracer_spec.rb'
- 'spec/lib/gitlab/health_checks/simple_check_shared.rb'
@@ -377,7 +364,6 @@ Lint/UnusedBlockArgument:
- 'spec/services/snippets/update_service_spec.rb'
- 'spec/spec_helper.rb'
- 'spec/support/atlassian/jira_connect/schemata.rb'
- - 'spec/support/graphql/subscriptions/action_cable/mock_action_cable.rb'
- 'spec/support/helpers/cycle_analytics_helpers.rb'
- 'spec/support/helpers/docs_screenshot_helpers.rb'
- 'spec/support/helpers/graphql_helpers.rb'
diff --git a/.rubocop_todo/lint/unused_method_argument.yml b/.rubocop_todo/lint/unused_method_argument.yml
index a309a041c38..e23fca97b15 100644
--- a/.rubocop_todo/lint/unused_method_argument.yml
+++ b/.rubocop_todo/lint/unused_method_argument.yml
@@ -5,7 +5,6 @@ Lint/UnusedMethodArgument:
- 'app/controllers/application_controller.rb'
- 'app/controllers/concerns/authenticates_with_two_factor.rb'
- 'app/controllers/concerns/creates_commit.rb'
- - 'app/controllers/concerns/metrics_dashboard.rb'
- 'app/controllers/concerns/renders_notes.rb'
- 'app/controllers/concerns/send_file_upload.rb'
- 'app/controllers/concerns/spammable_actions/captcha_check/common.rb'
@@ -181,16 +180,13 @@ Lint/UnusedMethodArgument:
- 'app/services/jira/requests/base.rb'
- 'app/services/keys/destroy_service.rb'
- 'app/services/merge_requests/close_service.rb'
- - 'app/services/merge_requests/ff_merge_service.rb'
- 'app/services/merge_requests/merge_base_service.rb'
- - 'app/services/metrics/dashboard/grafana_metric_embed_service.rb'
- 'app/services/notification_service.rb'
- 'app/services/packages/nuget/metadata_extraction_service.rb'
- 'app/services/projects/base_move_relations_service.rb'
- 'app/services/projects/hashed_storage/base_attachment_service.rb'
- 'app/services/projects/lfs_pointers/lfs_download_service.rb'
- 'app/services/projects/open_issues_count_service.rb'
- - 'app/services/prometheus/proxy_service.rb'
- 'app/services/protected_refs/access_level_params.rb'
- 'app/services/repositories/base_service.rb'
- 'app/services/system_notes/incidents_service.rb'
@@ -282,7 +278,6 @@ Lint/UnusedMethodArgument:
- 'ee/app/validators/vulnerabilities/cvss_vector_validator.rb'
- 'ee/app/workers/automation/execute_rule_worker.rb'
- 'ee/app/workers/gitlab_subscriptions/refresh_seats_worker.rb'
- - 'ee/app/workers/namespaces/free_user_cap/over_limit_notification_worker.rb'
- 'ee/db/fixtures/development/20_burndown.rb'
- 'ee/lib/audit/compliance_framework_changes_auditor.rb'
- 'ee/lib/compliance_management/compliance_report/commit_loader.rb'
@@ -322,13 +317,10 @@ Lint/UnusedMethodArgument:
- 'ee/spec/lib/ee/api/helpers/notes_helpers_spec.rb'
- 'ee/spec/lib/ee/gitlab/background_migration/migrate_vulnerabilities_feedback_to_vulnerabilities_state_transition_spec.rb'
- 'ee/spec/lib/gitlab/analytics/cycle_analytics/data_collector_spec.rb'
- - 'ee/spec/lib/gitlab/patch/geo_database_tasks_spec.rb'
- 'ee/spec/requests/api/project_import_spec.rb'
- 'ee/spec/services/ee/resource_events/merge_into_notes_service_spec.rb'
- - 'ee/spec/services/geo/repository_verification_primary_service_spec.rb'
- 'ee/spec/support/helpers/ee/geo_helpers.rb'
- 'ee/spec/support/helpers/ee/migrations_helpers.rb'
- - 'lib/api/concerns/packages/nuget_endpoints.rb'
- 'lib/api/entities/basic_project_details.rb'
- 'lib/api/entities/entity_helpers.rb'
- 'lib/api/entities/project.rb'
@@ -382,7 +374,6 @@ Lint/UnusedMethodArgument:
- 'lib/gitlab/background_migration/batching_strategies/loose_index_scan_batching_strategy.rb'
- 'lib/gitlab/background_migration/cleanup_orphaned_routes.rb'
- 'lib/gitlab/background_migration/job_coordinator.rb'
- - 'lib/gitlab/background_migration/migrate_personal_namespace_project_maintainer_to_owner.rb'
- 'lib/gitlab/background_migration/project_namespaces/backfill_project_namespaces.rb'
- 'lib/gitlab/bitbucket_import/importer.rb'
- 'lib/gitlab/cache/helpers.rb'
@@ -448,7 +439,6 @@ Lint/UnusedMethodArgument:
- 'lib/gitlab/database/query_analyzers/base.rb'
- 'lib/gitlab/database/query_analyzers/prevent_cross_database_modification.rb'
- 'lib/gitlab/database/rename_table_helpers.rb'
- - 'lib/gitlab/database_importers/common_metrics/importer.rb'
- 'lib/gitlab/dependency_linker/base_linker.rb'
- 'lib/gitlab/diff/file_collection/merge_request_diff_base.rb'
- 'lib/gitlab/diff/line.rb'
@@ -463,7 +453,6 @@ Lint/UnusedMethodArgument:
- 'lib/gitlab/gitaly_client/operation_service.rb'
- 'lib/gitlab/github_gists_import/representation/gist.rb'
- 'lib/gitlab/github_import/importer/pull_requests/review_requests_importer.rb'
- - 'lib/gitlab/github_import/importer/pull_requests_reviews_importer.rb'
- 'lib/gitlab/github_import/representation/diff_note.rb'
- 'lib/gitlab/github_import/representation/issue_event.rb'
- 'lib/gitlab/github_import/representation/lfs_object.rb'
@@ -494,7 +483,6 @@ Lint/UnusedMethodArgument:
- 'lib/gitlab/optimistic_locking.rb'
- 'lib/gitlab/otp_key_rotator.rb'
- 'lib/gitlab/project_search_results.rb'
- - 'lib/gitlab/prometheus/queries/query_additional_metrics.rb'
- 'lib/gitlab/quick_actions/issue_and_merge_request_actions.rb'
- 'lib/gitlab/redis/multi_store.rb'
- 'lib/gitlab/repository_cache.rb'
@@ -528,7 +516,6 @@ Lint/UnusedMethodArgument:
- 'lib/gitlab/template/gitlab_ci_yml_template.rb'
- 'lib/gitlab/template/issue_template.rb'
- 'lib/gitlab/template/merge_request_template.rb'
- - 'lib/gitlab/template/metrics_dashboard_template.rb'
- 'lib/gitlab/template_parser/ast.rb'
- 'lib/gitlab/testing/request_blocker_middleware.rb'
- 'lib/gitlab/testing/robots_blocker_middleware.rb'
@@ -582,7 +569,6 @@ Lint/UnusedMethodArgument:
- 'spec/lib/banzai/reference_parser/base_parser_spec.rb'
- 'spec/lib/gitlab/auth_spec.rb'
- 'spec/lib/gitlab/background_migration/batched_migration_job_spec.rb'
- - 'spec/lib/gitlab/background_migration/remove_occurrence_pipelines_and_duplicate_vulnerabilities_findings_spec.rb'
- 'spec/lib/gitlab/background_migration/set_correct_vulnerability_state_spec.rb'
- 'spec/lib/gitlab/background_migration/truncate_overlong_vulnerability_html_titles_spec.rb'
- 'spec/lib/gitlab/cache/request_cache_spec.rb'
@@ -607,7 +593,6 @@ Lint/UnusedMethodArgument:
- 'spec/services/lfs/push_service_spec.rb'
- 'spec/services/projects/transfer_service_spec.rb'
- 'spec/support/database/prevent_cross_joins.rb'
- - 'spec/support/graphql/subscriptions/action_cable/mock_action_cable.rb'
- 'spec/support/helpers/cycle_analytics_helpers.rb'
- 'spec/support/helpers/database/multiple_databases_helpers.rb'
- 'spec/support/helpers/database/table_schema_helpers.rb'
@@ -635,4 +620,3 @@ Lint/UnusedMethodArgument:
- 'spec/tooling/graphql/docs/renderer_spec.rb'
- 'spec/workers/concerns/cronjob_queue_spec.rb'
- 'spec/workers/container_expiration_policies/cleanup_container_repository_worker_spec.rb'
- - 'tooling/lib/tooling/mappings/base.rb'
diff --git a/.rubocop_todo/metrics/abc_size.yml b/.rubocop_todo/metrics/abc_size.yml
index 01e7a5c9688..babf39d6630 100644
--- a/.rubocop_todo/metrics/abc_size.yml
+++ b/.rubocop_todo/metrics/abc_size.yml
@@ -5,7 +5,6 @@ Metrics/AbcSize:
- 'app/helpers/nav/top_nav_helper.rb'
- 'app/models/instance_configuration.rb'
- 'app/services/projects/create_service.rb'
- - 'ee/db/seeds/awesome_co/awesome_co.rb'
- 'lib/gitlab/analytics/cycle_analytics/request_params.rb'
- 'lib/gitlab/sidekiq_middleware/server_metrics.rb'
- 'qa/qa/resource/repository/push.rb'
diff --git a/.rubocop_todo/metrics/parameter_lists.yml b/.rubocop_todo/metrics/parameter_lists.yml
index 14cd46d31fb..06d1ed48904 100644
--- a/.rubocop_todo/metrics/parameter_lists.yml
+++ b/.rubocop_todo/metrics/parameter_lists.yml
@@ -6,4 +6,3 @@ Metrics/ParameterLists:
- 'app/models/packages/sem_ver.rb'
- 'app/models/repository.rb'
- 'lib/gitlab/git/tree.rb'
- - 'lib/gitlab/version_info.rb'
diff --git a/.rubocop_todo/migration/background_migration_base_class.yml b/.rubocop_todo/migration/background_migration_base_class.yml
index c9ff0a41a2b..1a0de4e5821 100644
--- a/.rubocop_todo/migration/background_migration_base_class.yml
+++ b/.rubocop_todo/migration/background_migration_base_class.yml
@@ -6,21 +6,16 @@ Migration/BackgroundMigrationBaseClass:
- 'lib/gitlab/background_migration/backfill_issue_search_data.rb'
- 'lib/gitlab/background_migration/backfill_iteration_cadence_id_for_boards.rb'
- 'lib/gitlab/background_migration/backfill_member_namespace_for_group_members.rb'
- - 'lib/gitlab/background_migration/backfill_namespace_id_for_namespace_route.rb'
- 'lib/gitlab/background_migration/backfill_namespace_id_for_project_route.rb'
- 'lib/gitlab/background_migration/backfill_note_discussion_id.rb'
- 'lib/gitlab/background_migration/backfill_project_repositories.rb'
- 'lib/gitlab/background_migration/backfill_project_settings.rb'
- 'lib/gitlab/background_migration/backfill_snippet_repositories.rb'
- 'lib/gitlab/background_migration/backfill_topics_title.rb'
- - 'lib/gitlab/background_migration/cleanup_draft_data_from_faulty_regex.rb'
- 'lib/gitlab/background_migration/create_security_setting.rb'
- - 'lib/gitlab/background_migration/encrypt_integration_properties.rb'
- - 'lib/gitlab/background_migration/fix_duplicate_project_name_and_path.rb'
- 'lib/gitlab/background_migration/fix_first_mentioned_in_commit_at.rb'
- 'lib/gitlab/background_migration/fix_projects_without_project_feature.rb'
- 'lib/gitlab/background_migration/fix_projects_without_prometheus_service.rb'
- - 'lib/gitlab/background_migration/fix_vulnerability_occurrences_with_hashes_as_raw_metadata.rb'
- 'lib/gitlab/background_migration/legacy_upload_mover.rb'
- 'lib/gitlab/background_migration/legacy_uploads_migrator.rb'
- 'lib/gitlab/background_migration/mailers/unconfirm_mailer.rb'
@@ -29,16 +24,9 @@ Migration/BackgroundMigrationBaseClass:
- 'lib/gitlab/background_migration/migrate_approver_to_approval_rules_in_batch.rb'
- 'lib/gitlab/background_migration/migrate_job_artifact_registry_to_ssf.rb'
- 'lib/gitlab/background_migration/migrate_null_private_profile_to_false.rb'
- - 'lib/gitlab/background_migration/migrate_personal_namespace_project_maintainer_to_owner.rb'
- 'lib/gitlab/background_migration/migrate_requirements_to_work_items.rb'
- - 'lib/gitlab/background_migration/migrate_shimo_confluence_integration_category.rb'
- - 'lib/gitlab/background_migration/nullify_orphan_runner_id_on_ci_builds.rb'
- - 'lib/gitlab/background_migration/populate_container_repository_migration_plan.rb'
- 'lib/gitlab/background_migration/populate_latest_pipeline_ids.rb'
- 'lib/gitlab/background_migration/populate_resolved_on_default_branch_column.rb'
- 'lib/gitlab/background_migration/project_namespaces/backfill_project_namespaces.rb'
- 'lib/gitlab/background_migration/project_namespaces/models/namespace.rb'
- 'lib/gitlab/background_migration/project_namespaces/models/project.rb'
- - 'lib/gitlab/background_migration/remove_vulnerability_finding_links.rb'
- - 'lib/gitlab/background_migration/reset_duplicate_ci_runners_token_encrypted_values_on_projects.rb'
- - 'lib/gitlab/background_migration/reset_duplicate_ci_runners_token_values_on_projects.rb'
diff --git a/.rubocop_todo/migration/background_migration_record.yml b/.rubocop_todo/migration/background_migration_record.yml
index c803790525f..8b951caf875 100644
--- a/.rubocop_todo/migration/background_migration_record.yml
+++ b/.rubocop_todo/migration/background_migration_record.yml
@@ -11,17 +11,11 @@ Migration/BackgroundMigrationRecord:
- 'lib/gitlab/background_migration/backfill_draft_status_on_merge_requests.rb'
- 'lib/gitlab/background_migration/backfill_project_repositories.rb'
- 'lib/gitlab/background_migration/backfill_topics_title.rb'
- - 'lib/gitlab/background_migration/cleanup_draft_data_from_faulty_regex.rb'
- - 'lib/gitlab/background_migration/encrypt_integration_properties.rb'
- - 'lib/gitlab/background_migration/fix_duplicate_project_name_and_path.rb'
- 'lib/gitlab/background_migration/fix_first_mentioned_in_commit_at.rb'
- 'lib/gitlab/background_migration/fix_projects_without_prometheus_service.rb'
- - 'lib/gitlab/background_migration/fix_vulnerability_occurrences_with_hashes_as_raw_metadata.rb'
- 'lib/gitlab/background_migration/migrate_null_private_profile_to_false.rb'
- 'lib/gitlab/background_migration/populate_latest_pipeline_ids.rb'
- 'lib/gitlab/background_migration/project_namespaces/models/namespace.rb'
- 'lib/gitlab/background_migration/project_namespaces/models/project.rb'
- 'lib/gitlab/background_migration/remove_occurrence_pipelines_and_duplicate_vulnerabilities_findings.rb'
- - 'lib/gitlab/background_migration/reset_duplicate_ci_runners_token_encrypted_values_on_projects.rb'
- - 'lib/gitlab/background_migration/reset_duplicate_ci_runners_token_values_on_projects.rb'
- 'lib/gitlab/background_migration/update_jira_tracker_data_deployment_type_based_on_url.rb'
diff --git a/.rubocop_todo/migration/background_migrations.yml b/.rubocop_todo/migration/background_migrations.yml
index fee1a7a0a6e..9071e3b2c87 100644
--- a/.rubocop_todo/migration/background_migrations.yml
+++ b/.rubocop_todo/migration/background_migrations.yml
@@ -1,10 +1,6 @@
---
Migration/BackgroundMigrations:
Exclude:
- - 'db/post_migrate/20220315171129_cleanup_draft_data_from_faulty_regex.rb'
- - 'db/post_migrate/20220316202640_populate_container_repositories_migration_plan.rb'
- - 'db/post_migrate/20220324032250_migrate_shimo_confluence_service_category.rb'
- - 'db/post_migrate/20220324081709_fix_and_backfill_project_namespaces_for_projects_with_duplicate_name.rb'
- 'db/post_migrate/20220328100456_schedule20220328_reset_duplicate_ci_runners_token_encrypted_values_on_projects.rb'
- 'db/post_migrate/20220328100457_schedule20220328_reset_duplicate_ci_runners_token_values_on_projects.rb'
- 'db/post_migrate/20220331133802_schedule_backfill_topics_title.rb'
diff --git a/.rubocop_todo/naming/heredoc_delimiter_naming.yml b/.rubocop_todo/naming/heredoc_delimiter_naming.yml
index fa60792bbdc..92f5994092b 100644
--- a/.rubocop_todo/naming/heredoc_delimiter_naming.yml
+++ b/.rubocop_todo/naming/heredoc_delimiter_naming.yml
@@ -37,7 +37,6 @@ Naming/HeredocDelimiterNaming:
- 'lib/gitlab/utils/delegator_override/validator.rb'
- 'lib/tasks/gitlab/docs/compile_deprecations.rake'
- 'lib/tasks/gitlab/password.rake'
- - 'qa/qa/specs/features/browser_ui/4_verify/testing/view_code_coverage_spec.rb'
- 'qa/qa/specs/features/browser_ui/5_package/package_registry/generic_repository_spec.rb'
- 'qa/qa/specs/features/browser_ui/5_package/package_registry/nuget/nuget_group_level_spec.rb'
- 'qa/qa/specs/features/browser_ui/5_package/package_registry/nuget/nuget_project_level_spec.rb'
@@ -82,7 +81,6 @@ Naming/HeredocDelimiterNaming:
- 'spec/lib/gitlab/import_export/config_spec.rb'
- 'spec/lib/gitlab/metrics/samplers/puma_sampler_spec.rb'
- 'spec/lib/gitlab/patch/database_config_spec.rb'
- - 'spec/lib/gitlab/prometheus/additional_metrics_parser_spec.rb'
- 'spec/lib/gitlab/quick_actions/substitution_definition_spec.rb'
- 'spec/lib/gitlab/web_ide/config_spec.rb'
- 'spec/lib/gitlab/webpack/file_loader_spec.rb'
diff --git a/.rubocop_todo/performance/map_compact.yml b/.rubocop_todo/performance/map_compact.yml
index 1ed8ab1e2ee..8f831bf8f59 100644
--- a/.rubocop_todo/performance/map_compact.yml
+++ b/.rubocop_todo/performance/map_compact.yml
@@ -102,7 +102,6 @@ Performance/MapCompact:
- 'lib/gitlab/database/load_balancing/service_discovery.rb'
- 'lib/gitlab/git/commit.rb'
- 'lib/gitlab/git/conflict/file.rb'
- - 'lib/gitlab/git/rugged_impl/commit.rb'
- 'lib/gitlab/sql/pattern.rb'
- 'lib/gitlab/url_blocker.rb'
- 'qa/qa/page/component/issuable/sidebar.rb'
diff --git a/.rubocop_todo/performance/method_object_as_block.yml b/.rubocop_todo/performance/method_object_as_block.yml
index d214d61a76b..0c3914a4c1b 100644
--- a/.rubocop_todo/performance/method_object_as_block.yml
+++ b/.rubocop_todo/performance/method_object_as_block.yml
@@ -30,13 +30,9 @@ Performance/MethodObjectAsBlock:
- 'lib/gitlab/import_export/fast_hash_serializer.rb'
- 'lib/gitlab/import_export/group/tree_restorer.rb'
- 'lib/gitlab/middleware/basic_health_check.rb'
- - 'lib/gitlab/prometheus/additional_metrics_parser.rb'
- - 'lib/gitlab/prometheus/queries/matched_metric_query.rb'
- - 'lib/gitlab/prometheus/queries/query_additional_metrics.rb'
- 'lib/gitlab/search_context.rb'
- 'lib/gitlab/sidekiq_queue.rb'
- 'lib/gitlab/uploads/migration_helper.rb'
- - 'lib/gitlab/utils.rb'
- 'spec/graphql/resolvers/concerns/caching_array_resolver_spec.rb'
- 'spec/lib/api/entities/merge_request_basic_spec.rb'
- 'spec/lib/gitlab/import_export/import_test_coverage_spec.rb'
diff --git a/.rubocop_todo/performance/regexp_match.yml b/.rubocop_todo/performance/regexp_match.yml
index b4a21bfe40a..0aec7895a63 100644
--- a/.rubocop_todo/performance/regexp_match.yml
+++ b/.rubocop_todo/performance/regexp_match.yml
@@ -7,7 +7,6 @@ Performance/RegexpMatch:
- 'ee/lib/ee/banzai/filter/references/vulnerability_reference_filter.rb'
- 'ee/lib/elastic/latest/git_class_proxy.rb'
- 'ee/lib/gitlab/llm/chain/utils/text_processing.rb'
- - 'ee/lib/gitlab/llm/open_ai/response_modifiers/tanuki_bot.rb'
- 'ee/lib/gitlab/middleware/ip_restrictor.rb'
- 'ee/spec/spec_helper.rb'
- 'lib/api/helpers.rb'
@@ -28,7 +27,6 @@ Performance/RegexpMatch:
- 'qa/qa/service/cluster_provider/k3d.rb'
- 'qa/qa/specs/spec_helper.rb'
- 'qa/qa/tools/ci/ff_changes.rb'
- - 'scripts/changed-feature-flags'
- 'scripts/failed_tests.rb'
- 'scripts/lib/glfm/parse_examples.rb'
- 'scripts/lib/glfm/update_specification.rb'
diff --git a/.rubocop_todo/performance/string_include.yml b/.rubocop_todo/performance/string_include.yml
new file mode 100644
index 00000000000..c84976d55bf
--- /dev/null
+++ b/.rubocop_todo/performance/string_include.yml
@@ -0,0 +1,6 @@
+---
+# Cop supports --autocorrect.
+Performance/StringInclude:
+ Details: grace period
+ Exclude:
+ - 'spec/support/helpers/query_recorder.rb'
diff --git a/.rubocop_todo/performance/string_replacement.yml b/.rubocop_todo/performance/string_replacement.yml
new file mode 100644
index 00000000000..db22bba0897
--- /dev/null
+++ b/.rubocop_todo/performance/string_replacement.yml
@@ -0,0 +1,8 @@
+---
+# Cop supports --autocorrect.
+Performance/StringReplacement:
+ Details: grace period
+ Exclude:
+ - 'ee/app/models/saml_provider.rb'
+ - 'lib/gitlab/uploads/migration_helper.rb'
+ - 'lib/kramdown/parser/atlassian_document_format.rb'
diff --git a/.rubocop_todo/rails/file_path.yml b/.rubocop_todo/rails/file_path.yml
index 9fefe1d291d..b7291734253 100644
--- a/.rubocop_todo/rails/file_path.yml
+++ b/.rubocop_todo/rails/file_path.yml
@@ -7,7 +7,6 @@ Rails/FilePath:
- 'app/models/clusters/concerns/application_data.rb'
- 'app/models/concerns/cross_database_modification.rb'
- 'app/serializers/review_app_setup_entity.rb'
- - 'app/services/metrics/sample_metrics_service.rb'
- 'app/services/projects/readme_renderer_service.rb'
- 'config/environments/development.rb'
- 'config/initializers/1_settings.rb'
@@ -57,7 +56,6 @@ Rails/FilePath:
- 'lib/system_check/app/systemd_unit_files_or_init_script_up_to_date_check.rb'
- 'lib/system_check/app/uploads_directory_exists_check.rb'
- 'lib/system_check/incoming_email/imap_authentication_check.rb'
- - 'lib/tasks/gitlab/metrics_exporter.rake'
- 'lib/tasks/gitlab/usage_data.rake'
- 'lib/tasks/tanuki_emoji.rake'
- 'metrics_server/metrics_server.rb'
@@ -97,14 +95,12 @@ Rails/FilePath:
- 'spec/helpers/startupjs_helper_spec.rb'
- 'spec/lib/backup/database_spec.rb'
- 'spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb'
- - 'spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb'
- 'spec/lib/gitlab/database/schema_migrations/context_spec.rb'
- 'spec/lib/gitlab/feature_categories_spec.rb'
- 'spec/lib/gitlab/file_hook_spec.rb'
- 'spec/lib/gitlab/jwt_authenticatable_spec.rb'
- 'spec/lib/gitlab/legacy_http_spec.rb'
- 'spec/lib/gitlab/mail_room/mail_room_spec.rb'
- - 'spec/lib/gitlab/metrics/dashboard/finder_spec.rb'
- 'spec/lib/gitlab/middleware/multipart/handler_spec.rb'
- 'spec/lib/gitlab/multi_destination_logger_spec.rb'
- 'spec/lib/gitlab/project_transfer_spec.rb'
@@ -114,7 +110,6 @@ Rails/FilePath:
- 'spec/requests/api/internal/mail_room_spec.rb'
- 'spec/requests/api/usage_data_queries_spec.rb'
- 'spec/serializers/review_app_setup_entity_spec.rb'
- - 'spec/services/metrics/sample_metrics_service_spec.rb'
- 'spec/support/helpers/doc_url_helper.rb'
- 'spec/support/helpers/test_env.rb'
- 'spec/support/helpers/upload_helpers.rb'
@@ -123,5 +118,4 @@ Rails/FilePath:
- 'spec/support/shared_examples/models/application_setting_shared_examples.rb'
- 'spec/support/shared_examples/models/wiki_shared_examples.rb'
- 'spec/tasks/gitlab/db_rake_spec.rb'
- - 'spec/tasks/gitlab/generate_sample_prometheus_data_rake_spec.rb'
- 'spec/tasks/gitlab/usage_data_rake_spec.rb'
diff --git a/.rubocop_todo/rails/find_each.yml b/.rubocop_todo/rails/find_each.yml
new file mode 100644
index 00000000000..4e2bbb62439
--- /dev/null
+++ b/.rubocop_todo/rails/find_each.yml
@@ -0,0 +1,48 @@
+---
+# Cop supports --autocorrect.
+Rails/FindEach:
+ Details: grace period
+ Exclude:
+ - 'app/finders/projects/members/effective_access_level_finder.rb'
+ - 'app/services/ci/expire_pipeline_cache_service.rb'
+ - 'app/services/design_management/generate_image_versions_service.rb'
+ - 'app/services/groups/destroy_service.rb'
+ - 'app/services/merge_requests/refresh_service.rb'
+ - 'app/views/issues/_issues_calendar.ics.ruby'
+ - 'db/post_migrate/20220411173544_cleanup_orphans_approval_project_rules.rb'
+ - 'db/post_migrate/20231003045342_migrate_sidekiq_namespaced_jobs.rb'
+ - 'ee/app/workers/compliance_management/merge_requests/compliance_violations_consistency_worker.rb'
+ - 'ee/lib/ee/gitlab/background_migration/backfill_compliance_violations.rb'
+ - 'ee/lib/ee/gitlab/background_migration/create_compliance_standards_adherence.rb'
+ - 'ee/spec/elastic/migrate/20230901120542_force_reindex_commits_from_main_index_spec.rb'
+ - 'ee/spec/elastic/migrate/20231004124852_reindex_and_remove_leftover_notes_from_main_index_spec.rb'
+ - 'ee/spec/elastic/migrate/20231005103449_reindex_and_remove_leftover_merge_request_in_main_index_spec.rb'
+ - 'ee/spec/frontend/fixtures/runner.rb'
+ - 'ee/spec/lib/ee/gitlab/usage_data_spec.rb'
+ - 'ee/spec/requests/api/graphql/ci/jobs_spec.rb'
+ - 'ee/spec/services/package_metadata/ingestion/advisory/advisory_ingestion_task_spec.rb'
+ - 'ee/spec/services/vulnerabilities/starboard_vulnerability_resolve_service_spec.rb'
+ - 'ee/spec/workers/concerns/update_orchestration_policy_configuration_spec.rb'
+ - 'lib/banzai/reference_parser/base_parser.rb'
+ - 'lib/gitlab/background_migration/populate_vulnerability_dismissal_fields.rb'
+ - 'lib/gitlab/internal_events/event_definitions.rb'
+ - 'lib/tasks/tanuki_emoji.rake'
+ - 'qa/qa/ee/page/group/settings/general.rb'
+ - 'spec/features/admin/admin_labels_spec.rb'
+ - 'spec/features/issues/user_interacts_with_awards_spec.rb'
+ - 'spec/features/merge_request/user_creates_image_diff_notes_spec.rb'
+ - 'spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb'
+ - 'spec/features/projects/show/download_buttons_spec.rb'
+ - 'spec/features/projects/show/user_sees_git_instructions_spec.rb'
+ - 'spec/features/search/user_searches_for_merge_requests_spec.rb'
+ - 'spec/frontend/fixtures/analytics.rb'
+ - 'spec/lib/gitlab/background_migration/backfill_missing_ci_cd_settings_spec.rb'
+ - 'spec/lib/gitlab/background_migration/backfill_note_discussion_id_spec.rb'
+ - 'spec/lib/gitlab/database/reindexing/index_selection_spec.rb'
+ - 'spec/lib/gitlab/import_export/project/tree_restorer_spec.rb'
+ - 'spec/lib/gitlab/project_template_spec.rb'
+ - 'spec/lib/gitlab/sample_data_template_spec.rb'
+ - 'spec/lib/gitlab/usage/metric_definition_spec.rb'
+ - 'spec/migrations/20231003045342_migrate_sidekiq_namespaced_jobs_spec.rb'
+ - 'spec/models/ci_platform_metric_spec.rb'
+ - 'spec/workers/authorized_project_update/user_refresh_over_user_range_worker_spec.rb'
diff --git a/.rubocop_todo/rails/inverse_of.yml b/.rubocop_todo/rails/inverse_of.yml
index 30f5aaeff61..a696af73ae2 100644
--- a/.rubocop_todo/rails/inverse_of.yml
+++ b/.rubocop_todo/rails/inverse_of.yml
@@ -75,7 +75,6 @@ Rails/InverseOf:
- 'ee/app/models/iteration.rb'
- 'ee/app/models/requirements_management/requirement.rb'
- 'ee/app/models/requirements_management/test_report.rb'
- - 'ee/app/models/sbom/vulnerable_component_version.rb'
- 'ee/app/models/security/orchestration_policy_configuration.rb'
- 'ee/app/models/security/orchestration_policy_rule_schedule.rb'
- 'ee/app/models/software_license_policy.rb'
diff --git a/.rubocop_todo/rails/lexically_scoped_action_filter.yml b/.rubocop_todo/rails/lexically_scoped_action_filter.yml
index 8a1591062ab..63c403a672d 100644
--- a/.rubocop_todo/rails/lexically_scoped_action_filter.yml
+++ b/.rubocop_todo/rails/lexically_scoped_action_filter.yml
@@ -32,7 +32,6 @@ Rails/LexicallyScopedActionFilter:
- 'app/controllers/projects/notes_controller.rb'
- 'app/controllers/projects/pipelines_controller.rb'
- 'app/controllers/projects/project_members_controller.rb'
- - 'app/controllers/projects/prometheus/alerts_controller.rb'
- 'app/controllers/projects/releases_controller.rb'
- 'app/controllers/projects/settings/integration_hook_logs_controller.rb'
- 'app/controllers/projects/settings/merge_requests_controller.rb'
diff --git a/.rubocop_todo/rails/negate_include.yml b/.rubocop_todo/rails/negate_include.yml
index 8879146de89..e49a282798e 100644
--- a/.rubocop_todo/rails/negate_include.yml
+++ b/.rubocop_todo/rails/negate_include.yml
@@ -33,7 +33,6 @@ Rails/NegateInclude:
- 'qa/qa/tools/delete_test_users.rb'
- 'spec/lib/container_registry/blob_spec.rb'
- 'spec/lib/container_registry/client_spec.rb'
- - 'spec/lib/gitlab/metrics/dashboard/stages/metric_endpoint_inserter_spec.rb'
- 'spec/lib/gitlab/metrics/subscribers/active_record_spec.rb'
- 'spec/support/matchers/pushed_frontend_feature_flags_matcher.rb'
- 'spec/uploaders/object_storage_spec.rb'
diff --git a/.rubocop_todo/rails/output_safety.yml b/.rubocop_todo/rails/output_safety.yml
index 36628384477..ee7b2ec07e0 100644
--- a/.rubocop_todo/rails/output_safety.yml
+++ b/.rubocop_todo/rails/output_safety.yml
@@ -6,7 +6,6 @@ Rails/OutputSafety:
- 'app/controllers/groups_controller.rb'
- 'app/controllers/profiles/two_factor_auths_controller.rb'
- 'app/controllers/projects/mirrors_controller.rb'
- - 'app/controllers/projects/performance_monitoring/dashboards_controller.rb'
- 'app/controllers/projects/pipeline_schedules_controller.rb'
- 'app/controllers/projects/protected_refs_controller.rb'
- 'app/controllers/search_controller.rb'
@@ -91,8 +90,6 @@ Rails/OutputSafety:
- 'ee/app/components/namespaces/free_user_cap/enforcement_alert_component.rb'
- 'ee/app/components/namespaces/free_user_cap/enforcement_at_limit_alert_component.rb'
- 'ee/app/components/namespaces/free_user_cap/non_owner_enforcement_alert_component.rb'
- - 'ee/app/components/namespaces/free_user_cap/non_owner_notification_alert_component.rb'
- - 'ee/app/components/namespaces/free_user_cap/notification_alert_component.rb'
- 'ee/app/components/namespaces/free_user_cap/usage_quota_alert_component.rb'
- 'ee/app/components/namespaces/free_user_cap/usage_quota_trial_alert_component.rb'
- 'ee/app/components/namespaces/storage/limit_alert_component.rb'
diff --git a/.rubocop_todo/rails/pluck.yml b/.rubocop_todo/rails/pluck.yml
index 150e3aff6cc..6ed8c935f82 100644
--- a/.rubocop_todo/rails/pluck.yml
+++ b/.rubocop_todo/rails/pluck.yml
@@ -117,8 +117,6 @@ Rails/Pluck:
- 'lib/gitlab/github_import/representation/issue.rb'
- 'lib/gitlab/jira_import/metadata_collector.rb'
- 'lib/gitlab/merge_requests/message_generator.rb'
- - 'lib/gitlab/metrics/dashboard/importers/prometheus_metrics.rb'
- - 'lib/gitlab/metrics/dashboard/stages/custom_metrics_details_inserter.rb'
- 'lib/gitlab/sidekiq_config/cli_methods.rb'
- 'lib/gitlab/sql/pattern.rb'
- 'lib/gitlab/usage_data_counters/hll_redis_counter.rb'
@@ -167,20 +165,17 @@ Rails/Pluck:
- 'spec/lib/gitlab/ci/yaml_processor_spec.rb'
- 'spec/lib/gitlab/conflict/file_spec.rb'
- 'spec/lib/gitlab/database/similarity_score_spec.rb'
- - 'spec/lib/gitlab/database_importers/common_metrics/importer_spec.rb'
- 'spec/lib/gitlab/git/blame_spec.rb'
- 'spec/lib/gitlab/git/conflict/parser_spec.rb'
- 'spec/lib/gitlab/import_export/project/sample/relation_tree_restorer_spec.rb'
- 'spec/lib/gitlab/import_export/project/tree_saver_spec.rb'
- 'spec/lib/gitlab/language_detection_spec.rb'
- 'spec/lib/gitlab/lograge/custom_options_spec.rb'
- - 'spec/lib/gitlab/metrics/dashboard/processor_spec.rb'
- 'spec/lib/gitlab/relative_positioning/item_context_spec.rb'
- 'spec/lib/gitlab/search/query_spec.rb'
- 'spec/lib/gitlab/sidekiq_config/worker_matcher_spec.rb'
- 'spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/client_spec.rb'
- 'spec/lib/gitlab/tree_summary_spec.rb'
- - 'spec/lib/peek/views/rugged_spec.rb'
- 'spec/models/bulk_imports/entity_spec.rb'
- 'spec/models/ci/bridge_spec.rb'
- 'spec/models/ci/build_spec.rb'
@@ -253,7 +248,6 @@ Rails/Pluck:
- 'spec/requests/api/topics_spec.rb'
- 'spec/requests/api/unleash_spec.rb'
- 'spec/requests/api/users_spec.rb'
- - 'spec/requests/api/v3/github_spec.rb'
- 'spec/requests/groups/autocomplete_sources_spec.rb'
- 'spec/requests/groups/milestones_controller_spec.rb'
- 'spec/requests/jwks_controller_spec.rb'
@@ -265,7 +259,6 @@ Rails/Pluck:
- 'spec/services/ci/compare_test_reports_service_spec.rb'
- 'spec/services/ci/pipeline_processing/atomic_processing_service/status_collection_spec.rb'
- 'spec/services/issues/export_csv_service_spec.rb'
- - 'spec/services/metrics/dashboard/custom_metric_embed_service_spec.rb'
- 'spec/services/projects/participants_service_spec.rb'
- 'spec/support/helpers/api_helpers.rb'
- 'spec/support/helpers/graphql_helpers.rb'
diff --git a/.rubocop_todo/rails/require_dependency.yml b/.rubocop_todo/rails/require_dependency.yml
new file mode 100644
index 00000000000..345fe997744
--- /dev/null
+++ b/.rubocop_todo/rails/require_dependency.yml
@@ -0,0 +1,40 @@
+---
+Rails/RequireDependency:
+ Details: grace period
+ Exclude:
+ - 'app/models/alert_management/alert.rb'
+ - 'app/models/design_management/action.rb'
+ - 'config/application.rb'
+ - 'config/initializers/8_devise.rb'
+ - 'config/initializers/query_limiting.rb'
+ - 'config/initializers/zz_metrics.rb'
+ - 'ee/app/finders/status_page/incident_comments_finder.rb'
+ - 'ee/app/models/compliance_management/compliance_framework/project_settings.rb'
+ - 'ee/app/services/group_saml/saml_provider/create_service.rb'
+ - 'ee/app/services/group_saml/saml_provider/update_service.rb'
+ - 'ee/app/services/vulnerabilities/confirm_service.rb'
+ - 'ee/app/services/vulnerabilities/destroy_dismissal_feedback_service.rb'
+ - 'ee/app/services/vulnerabilities/dismiss_service.rb'
+ - 'ee/app/services/vulnerabilities/resolve_service.rb'
+ - 'ee/app/services/vulnerabilities/revert_to_detected_service.rb'
+ - 'ee/spec/lib/gitlab/insights/validators/params_validator_spec.rb'
+ - 'lib/api/terraform/state.rb'
+ - 'lib/gitlab/ci/pipeline/expression/lexeme/pattern/regular_expression.rb'
+ - 'lib/gitlab/email/receiver.rb'
+ - 'lib/gitlab/omniauth_initializer.rb'
+ - 'lib/gitlab/patch/prependable.rb'
+ - 'lib/gitlab/untrusted_regexp.rb'
+ - 'lib/gitlab/utils/merge_hash.rb'
+ - 'lib/gitlab/utils/sanitize_node_link.rb'
+ - 'lib/tasks/gitlab/assets.rake'
+ - 'spec/lib/gitlab/ci/config/entry/include/rules/rule_spec.rb'
+ - 'spec/lib/gitlab/ci/config/entry/include/rules_spec.rb'
+ - 'spec/lib/gitlab/ci/config/entry/include_spec.rb'
+ - 'spec/lib/gitlab/ci/config/entry/includes_spec.rb'
+ - 'spec/lib/gitlab/ci/config/entry/product/matrix_spec.rb'
+ - 'spec/lib/gitlab/ci/config/entry/product/parallel_spec.rb'
+ - 'spec/lib/gitlab/ci/config/entry/product/variables_spec.rb'
+ - 'spec/lib/gitlab/ci/config/entry/rules_spec.rb'
+ - 'spec/lib/gitlab/memory/watchdog/configuration_spec.rb'
+ - 'spec/lib/gitlab/memory/watchdog/monitor/unique_memory_growth_spec.rb'
+ - 'spec/support/helpers/stub_configuration.rb'
diff --git a/.rubocop_todo/rails/time_zone.yml b/.rubocop_todo/rails/time_zone.yml
index 01dcb78bdce..faa3cb5ecfb 100644
--- a/.rubocop_todo/rails/time_zone.yml
+++ b/.rubocop_todo/rails/time_zone.yml
@@ -1,27 +1,18 @@
---
# Cop supports --autocorrect.
Rails/TimeZone:
+ Details: grace period
Exclude:
- 'ee/lib/delay.rb'
- 'ee/lib/gitlab/elastic/indexer.rb'
- 'ee/lib/gitlab/geo/event_gap_tracking.rb'
- - 'ee/lib/gitlab/geo/log_cursor/events/repository_updated_event.rb'
- 'ee/lib/gitlab/geo/log_cursor/logger.rb'
- 'ee/lib/gitlab/geo/oauth/login_state.rb'
- 'ee/spec/lib/gitlab/geo/base_request_spec.rb'
- 'ee/spec/lib/gitlab/geo/log_cursor/events/cache_invalidation_event_spec.rb'
- 'ee/spec/lib/gitlab/geo/log_cursor/events/event_spec.rb'
- 'ee/spec/lib/gitlab/geo/log_cursor/events/hashed_storage_attachments_event_spec.rb'
- - 'ee/spec/lib/gitlab/geo/log_cursor/events/hashed_storage_migrated_event_spec.rb'
- - 'ee/spec/lib/gitlab/geo/log_cursor/events/repositories_changed_event_spec.rb'
- - 'ee/spec/lib/gitlab/geo/log_cursor/events/repository_created_event_spec.rb'
- - 'ee/spec/lib/gitlab/geo/log_cursor/events/repository_deleted_event_spec.rb'
- - 'ee/spec/lib/gitlab/geo/log_cursor/events/repository_renamed_event_spec.rb'
- - 'ee/spec/lib/gitlab/geo/log_cursor/events/repository_updated_event_spec.rb'
- - 'ee/spec/lib/gitlab/geo/log_cursor/events/reset_checksum_event_spec.rb'
- 'ee/spec/lib/gitlab/geo/log_cursor/logger_spec.rb'
- - 'ee/spec/lib/gitlab/prometheus/queries/additional_metrics_deployment_query_spec.rb'
- - 'ee/spec/lib/gitlab/prometheus/queries/cluster_query_spec.rb'
- 'lib/api/helpers.rb'
- 'lib/api/sidekiq_metrics.rb'
- 'lib/bitbucket_server/representation/base.rb'
@@ -43,17 +34,13 @@ Rails/TimeZone:
- 'lib/gitlab/lfs_token.rb'
- 'lib/gitlab/loop_helpers.rb'
- 'lib/gitlab/popen.rb'
- - 'lib/gitlab/prometheus/queries/additional_metrics_environment_query.rb'
- - 'lib/gitlab/prometheus/queries/matched_metric_query.rb'
- 'lib/gitlab/prometheus_client.rb'
- 'lib/gitlab/task_helpers.rb'
- - 'lib/gitlab/x509/tag.rb'
- 'lib/grafana/time_window.rb'
- 'lib/json_web_token/token.rb'
- 'lib/object_storage/direct_upload.rb'
- 'lib/quality/seeders/issues.rb'
- 'lib/tasks/gitlab/assets.rake'
- - 'lib/tasks/gitlab/backup.rake'
- 'lib/tasks/gitlab/cleanup.rake'
- 'lib/tasks/gitlab/list_repos.rake'
- 'spec/lib/api/helpers_spec.rb'
@@ -62,7 +49,6 @@ Rails/TimeZone:
- 'spec/lib/gitlab/app_text_logger_spec.rb'
- 'spec/lib/gitlab/auth/current_user_mode_spec.rb'
- 'spec/lib/gitlab/bitbucket_import/importer_spec.rb'
- - 'spec/lib/gitlab/bitbucket_server_import/importer_spec.rb'
- 'spec/lib/gitlab/checks/timed_logger_spec.rb'
- 'spec/lib/gitlab/ci/cron_parser_spec.rb'
- 'spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb'
@@ -90,15 +76,12 @@ Rails/TimeZone:
- 'spec/lib/gitlab/graphql_logger_spec.rb'
- 'spec/lib/gitlab/graphs/commits_spec.rb'
- 'spec/lib/gitlab/import_export/project/relation_factory_spec.rb'
- - 'spec/lib/gitlab/json_logger_spec.rb'
- 'spec/lib/gitlab/lfs_token_spec.rb'
- 'spec/lib/gitlab/log_timestamp_formatter_spec.rb'
- 'spec/lib/gitlab/middleware/rails_queue_duration_spec.rb'
- - 'spec/lib/gitlab/prometheus/queries/additional_metrics_deployment_query_spec.rb'
- - 'spec/lib/gitlab/prometheus/queries/deployment_query_spec.rb'
- - 'spec/lib/gitlab/prometheus/queries/validate_query_spec.rb'
- 'spec/lib/gitlab/sidekiq_logging/json_formatter_spec.rb'
- 'spec/lib/gitlab/utils/json_size_estimator_spec.rb'
- 'spec/lib/gitlab/x509/signature_spec.rb'
- 'spec/lib/grafana/time_window_spec.rb'
- 'spec/lib/json_web_token/hmac_token_spec.rb'
+ - 'spec/models/merge_request_diff_commit_spec.rb'
diff --git a/.rubocop_todo/rspec/any_instance_of.yml b/.rubocop_todo/rspec/any_instance_of.yml
index 18c225fb9ec..25229fde8de 100644
--- a/.rubocop_todo/rspec/any_instance_of.yml
+++ b/.rubocop_todo/rspec/any_instance_of.yml
@@ -40,15 +40,8 @@ RSpec/AnyInstanceOf:
- 'ee/spec/services/ee/merge_requests/refresh_service_spec.rb'
- 'ee/spec/services/ee/users/create_service_spec.rb'
- 'ee/spec/services/geo/container_repository_sync_service_spec.rb'
- - 'ee/spec/services/geo/design_repository_sync_service_spec.rb'
- 'ee/spec/services/geo/framework_repository_sync_service_spec.rb'
- - 'ee/spec/services/geo/hashed_storage_migration_service_spec.rb'
- 'ee/spec/services/geo/metrics_update_service_spec.rb'
- - 'ee/spec/services/geo/move_repository_service_spec.rb'
- - 'ee/spec/services/geo/project_housekeeping_service_spec.rb'
- - 'ee/spec/services/geo/rename_repository_service_spec.rb'
- - 'ee/spec/services/geo/repository_destroy_service_spec.rb'
- - 'ee/spec/services/geo/repository_sync_service_spec.rb'
- 'ee/spec/services/groups/destroy_service_spec.rb'
- 'ee/spec/services/groups/update_service_spec.rb'
- 'ee/spec/services/merge_trains/check_status_service_spec.rb'
@@ -62,10 +55,7 @@ RSpec/AnyInstanceOf:
- 'ee/spec/support/shared_examples/models/member_shared_examples.rb'
- 'ee/spec/support/shared_examples/services/base_sync_service_shared_examples.rb'
- 'ee/spec/workers/concerns/elastic/indexing_control_spec.rb'
- - 'ee/spec/workers/geo/design_repository_shard_sync_worker_spec.rb'
- 'ee/spec/workers/geo/registry_sync_worker_spec.rb'
- - 'ee/spec/workers/geo/repository_cleanup_worker_spec.rb'
- - 'ee/spec/workers/geo/repository_shard_sync_worker_spec.rb'
- 'ee/spec/workers/project_cache_worker_spec.rb'
- 'ee/spec/workers/repository_import_worker_spec.rb'
- 'ee/spec/workers/security/auto_fix_worker_spec.rb'
@@ -79,7 +69,6 @@ RSpec/AnyInstanceOf:
- 'spec/controllers/groups/settings/ci_cd_controller_spec.rb'
- 'spec/controllers/groups_controller_spec.rb'
- 'spec/controllers/import/bitbucket_controller_spec.rb'
- - 'spec/controllers/oauth/jira_dvcs/authorizations_controller_spec.rb'
- 'spec/controllers/omniauth_callbacks_controller_spec.rb'
- 'spec/controllers/projects/artifacts_controller_spec.rb'
- 'spec/controllers/projects/branches_controller_spec.rb'
@@ -180,7 +169,6 @@ RSpec/AnyInstanceOf:
- 'spec/lib/gitlab/gitaly_client/repository_service_spec.rb'
- 'spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb'
- 'spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb'
- - 'spec/lib/gitlab/hashed_storage/migrator_spec.rb'
- 'spec/lib/gitlab/import_export/config_spec.rb'
- 'spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb'
- 'spec/lib/gitlab/import_export/importer_spec.rb'
@@ -258,7 +246,6 @@ RSpec/AnyInstanceOf:
- 'spec/services/ci/retry_pipeline_service_spec.rb'
- 'spec/services/clusters/cleanup/project_namespace_service_spec.rb'
- 'spec/services/clusters/cleanup/service_account_service_spec.rb'
- - 'spec/services/deployments/older_deployments_drop_service_spec.rb'
- 'spec/services/deployments/update_environment_service_spec.rb'
- 'spec/services/draft_notes/destroy_service_spec.rb'
- 'spec/services/events/render_service_spec.rb'
@@ -277,8 +264,6 @@ RSpec/AnyInstanceOf:
- 'spec/services/merge_requests/refresh_service_spec.rb'
- 'spec/services/merge_requests/reload_diffs_service_spec.rb'
- 'spec/services/merge_requests/resolved_discussion_notification_service_spec.rb'
- - 'spec/services/metrics/dashboard/custom_dashboard_service_spec.rb'
- - 'spec/services/metrics/dashboard/transient_embed_service_spec.rb'
- 'spec/services/notes/create_service_spec.rb'
- 'spec/services/notes/render_service_spec.rb'
- 'spec/services/packages/conan/create_package_file_service_spec.rb'
@@ -308,12 +293,10 @@ RSpec/AnyInstanceOf:
- 'spec/support/helpers/graphql_helpers.rb'
- 'spec/support/helpers/ldap_helpers.rb'
- 'spec/support/helpers/login_helpers.rb'
- - 'spec/support/helpers/metrics_dashboard_url_helpers.rb'
- 'spec/support/helpers/rake_helpers.rb'
- 'spec/support/helpers/stub_configuration.rb'
- 'spec/support/helpers/stub_gitlab_calls.rb'
- 'spec/support/import_export/common_util.rb'
- - 'spec/support/services/migrate_to_ghost_user_service_shared_examples.rb'
- 'spec/support/shared_contexts/email_shared_context.rb'
- 'spec/support/shared_contexts/services/projects/container_repository/delete_tags_service_shared_context.rb'
- 'spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb'
diff --git a/.rubocop_todo/rspec/avoid_conditional_statements.yml b/.rubocop_todo/rspec/avoid_conditional_statements.yml
index 1e5388718ab..8013dddcd1a 100644
--- a/.rubocop_todo/rspec/avoid_conditional_statements.yml
+++ b/.rubocop_todo/rspec/avoid_conditional_statements.yml
@@ -26,7 +26,6 @@ RSpec/AvoidConditionalStatements:
- 'ee/spec/features/registrations/identity_verification_spec.rb'
- 'ee/spec/features/search/elastic/snippet_search_spec.rb'
- 'ee/spec/features/subscriptions/expiring_subscription_message_spec.rb'
- - 'ee/spec/features/users/identity_verification_spec.rb'
- 'spec/features/admin/dashboard_spec.rb'
- 'spec/features/calendar_spec.rb'
- 'spec/features/groups/dependency_proxy_for_containers_spec.rb'
diff --git a/.rubocop_todo/rspec/before_all_role_assignment.yml b/.rubocop_todo/rspec/before_all_role_assignment.yml
index 0905a79beca..9f85e031775 100644
--- a/.rubocop_todo/rspec/before_all_role_assignment.yml
+++ b/.rubocop_todo/rspec/before_all_role_assignment.yml
@@ -1,8 +1,6 @@
---
RSpec/BeforeAllRoleAssignment:
Exclude:
- - 'ee/spec/components/namespaces/free_user_cap/non_owner_notification_alert_component_spec.rb'
- - 'ee/spec/components/namespaces/free_user_cap/notification_alert_component_spec.rb'
- 'ee/spec/components/namespaces/storage/pre_enforcement_alert_component_spec.rb'
- 'ee/spec/components/namespaces/storage/subgroup_pre_enforcement_alert_component_spec.rb'
- 'ee/spec/controllers/autocomplete_controller_spec.rb'
@@ -111,7 +109,6 @@ RSpec/BeforeAllRoleAssignment:
- 'ee/spec/features/issues/user_bulk_edits_issues_spec.rb'
- 'ee/spec/features/issues/user_sees_empty_state_spec.rb'
- 'ee/spec/features/issues/user_uses_quick_actions_spec.rb'
- - 'ee/spec/features/markdown/observability_spec.rb'
- 'ee/spec/features/merge_request/user_creates_merge_request_spec.rb'
- 'ee/spec/features/merge_request/user_edits_approval_rules_mr_spec.rb'
- 'ee/spec/features/merge_request/user_sees_mr_approvals_promo_spec.rb'
@@ -171,12 +168,10 @@ RSpec/BeforeAllRoleAssignment:
- 'ee/spec/frontend/fixtures/analytics/contributions_spec.rb'
- 'ee/spec/frontend/fixtures/dast_profiles.rb'
- 'ee/spec/frontend/fixtures/on_demand_dast_scans.rb'
- - 'ee/spec/frontend/fixtures/users.rb'
- 'ee/spec/graphql/ee/mutations/boards/issues/issue_move_list_spec.rb'
- 'ee/spec/graphql/ee/mutations/ci/job_token_scope/add_project_spec.rb'
- 'ee/spec/graphql/ee/mutations/ci/job_token_scope/remove_project_spec.rb'
- 'ee/spec/graphql/ee/resolvers/project_issues_resolver_spec.rb'
- - 'ee/spec/graphql/ee/types/clusters/agent_type_spec.rb'
- 'ee/spec/graphql/ee/types/group_type_spec.rb'
- 'ee/spec/graphql/mutations/boards/epic_boards/create_spec.rb'
- 'ee/spec/graphql/mutations/boards/epic_boards/destroy_spec.rb'
@@ -218,7 +213,6 @@ RSpec/BeforeAllRoleAssignment:
- 'ee/spec/graphql/mutations/requirements_management/create_requirement_spec.rb'
- 'ee/spec/graphql/mutations/requirements_management/export_requirements_spec.rb'
- 'ee/spec/graphql/mutations/requirements_management/update_requirement_spec.rb'
- - 'ee/spec/graphql/mutations/security/training_provider_update_spec.rb'
- 'ee/spec/graphql/mutations/security_policy/create_security_policy_project_spec.rb'
- 'ee/spec/graphql/mutations/security_policy/unassign_security_policy_project_spec.rb'
- 'ee/spec/graphql/resolvers/analytics/devops_adoption/enabled_namespaces_resolver_spec.rb'
@@ -249,7 +243,6 @@ RSpec/BeforeAllRoleAssignment:
- 'ee/spec/graphql/resolvers/requirements_management/requirements_resolver_spec.rb'
- 'ee/spec/graphql/resolvers/requirements_management/test_reports_resolver_spec.rb'
- 'ee/spec/graphql/resolvers/security_orchestration/scan_execution_policy_resolver_spec.rb'
- - 'ee/spec/graphql/resolvers/security_training_urls_resolver_spec.rb'
- 'ee/spec/graphql/resolvers/vulnerabilities/container_images_resolver_spec.rb'
- 'ee/spec/graphql/resolvers/vulnerabilities/scanners_resolver_spec.rb'
- 'ee/spec/graphql/resolvers/vulnerabilities_count_per_day_resolver_spec.rb'
@@ -290,7 +283,6 @@ RSpec/BeforeAllRoleAssignment:
- 'ee/spec/helpers/vulnerabilities_helper_spec.rb'
- 'ee/spec/helpers/web_hooks/web_hooks_helper_spec.rb'
- 'ee/spec/lib/analytics/group_activity_calculator_spec.rb'
- - 'ee/spec/lib/audit/group_push_rules_changes_auditor_spec.rb'
- 'ee/spec/lib/audit/project_feature_changes_auditor_spec.rb'
- 'ee/spec/lib/bulk_imports/common/pipelines/boards_pipeline_spec.rb'
- 'ee/spec/lib/bulk_imports/groups/pipelines/epics_pipeline_spec.rb'
@@ -346,7 +338,6 @@ RSpec/BeforeAllRoleAssignment:
- 'ee/spec/policies/event_policy_spec.rb'
- 'ee/spec/policies/global_policy_spec.rb'
- 'ee/spec/policies/group_policy_spec.rb'
- - 'ee/spec/policies/identity_provider_policy_spec.rb'
- 'ee/spec/policies/incident_management/oncall_rotation_policy_spec.rb'
- 'ee/spec/policies/incident_management/oncall_schedule_policy_spec.rb'
- 'ee/spec/policies/incident_management/oncall_shift_policy_spec.rb'
@@ -473,7 +464,6 @@ RSpec/BeforeAllRoleAssignment:
- 'ee/spec/requests/api/graphql/namespace/projects_spec.rb'
- 'ee/spec/requests/api/graphql/pipeline_security_report_finding_spec.rb'
- 'ee/spec/requests/api/graphql/product_analytics/dashboards_spec.rb'
- - 'ee/spec/requests/api/graphql/product_analytics/project_visualizations_spec.rb'
- 'ee/spec/requests/api/graphql/project/alert_management/integrations_spec.rb'
- 'ee/spec/requests/api/graphql/project/branch_rules/approval_project_rules_spec.rb'
- 'ee/spec/requests/api/graphql/project/branch_rules/branch_protection_spec.rb'
@@ -498,14 +488,12 @@ RSpec/BeforeAllRoleAssignment:
- 'ee/spec/requests/api/graphql/project/pipeline/security_report_findings_spec.rb'
- 'ee/spec/requests/api/graphql/project/pipeline/security_report_summary_spec.rb'
- 'ee/spec/requests/api/graphql/project/pipelines/dast_profile_spec.rb'
- - 'ee/spec/requests/api/graphql/project/product_analytics_spec.rb'
- 'ee/spec/requests/api/graphql/project/requirements_management/requirement_counts_spec.rb'
- 'ee/spec/requests/api/graphql/project/requirements_management/requirements_spec.rb'
- 'ee/spec/requests/api/graphql/project/requirements_management/test_reports_spec.rb'
- 'ee/spec/requests/api/graphql/project/security_orchestration/scan_result_policy_spec.rb'
- 'ee/spec/requests/api/graphql/project/vulnerability_severities_count_spec.rb'
- 'ee/spec/requests/api/graphql/subscriptions/ai_completion_response_spec.rb'
- - 'ee/spec/requests/api/graphql/vulnerabilities/description_spec.rb'
- 'ee/spec/requests/api/graphql/vulnerabilities/details_spec.rb'
- 'ee/spec/requests/api/graphql/vulnerabilities/external_issue_links_spec.rb'
- 'ee/spec/requests/api/graphql/vulnerabilities/identifiers_spec.rb'
@@ -544,7 +532,6 @@ RSpec/BeforeAllRoleAssignment:
- 'ee/spec/requests/api/saml_group_links_spec.rb'
- 'ee/spec/requests/api/search_spec.rb'
- 'ee/spec/requests/api/todos_spec.rb'
- - 'ee/spec/requests/api/v3/github_spec.rb'
- 'ee/spec/requests/api/vulnerabilities_spec.rb'
- 'ee/spec/requests/api/vulnerability_exports_spec.rb'
- 'ee/spec/requests/api/vulnerability_findings_spec.rb'
@@ -633,7 +620,6 @@ RSpec/BeforeAllRoleAssignment:
- 'ee/spec/services/ee/alert_management/http_integrations/create_service_spec.rb'
- 'ee/spec/services/ee/alert_management/http_integrations/update_service_spec.rb'
- 'ee/spec/services/ee/auth/container_registry_authentication_service_spec.rb'
- - 'ee/spec/services/ee/authorized_project_update/project_recalculate_service_spec.rb'
- 'ee/spec/services/ee/design_management/delete_designs_service_spec.rb'
- 'ee/spec/services/ee/design_management/save_designs_service_spec.rb'
- 'ee/spec/services/ee/groups/autocomplete_service_spec.rb'
@@ -764,7 +750,6 @@ RSpec/BeforeAllRoleAssignment:
- 'ee/spec/workers/merge_requests/llm/summarize_merge_request_worker_spec.rb'
- 'ee/spec/workers/security/orchestration_configuration_create_bot_worker_spec.rb'
- 'spec/controllers/autocomplete_controller_spec.rb'
- - 'spec/controllers/concerns/metrics_dashboard_spec.rb'
- 'spec/controllers/dashboard_controller_spec.rb'
- 'spec/controllers/explore/projects_controller_spec.rb'
- 'spec/controllers/graphql_controller_spec.rb'
@@ -793,7 +778,6 @@ RSpec/BeforeAllRoleAssignment:
- 'spec/controllers/projects/commit_controller_spec.rb'
- 'spec/controllers/projects/commits_controller_spec.rb'
- 'spec/controllers/projects/compare_controller_spec.rb'
- - 'spec/controllers/projects/environments/sample_metrics_controller_spec.rb'
- 'spec/controllers/projects/error_tracking_controller_spec.rb'
- 'spec/controllers/projects/feature_flags_clients_controller_spec.rb'
- 'spec/controllers/projects/group_links_controller_spec.rb'
@@ -804,12 +788,9 @@ RSpec/BeforeAllRoleAssignment:
- 'spec/controllers/projects/labels_controller_spec.rb'
- 'spec/controllers/projects/merge_requests_controller_spec.rb'
- 'spec/controllers/projects/packages/infrastructure_registry_controller_spec.rb'
- - 'spec/controllers/projects/performance_monitoring/dashboards_controller_spec.rb'
- 'spec/controllers/projects/pipeline_schedules_controller_spec.rb'
- 'spec/controllers/projects/pipelines_controller_spec.rb'
- 'spec/controllers/projects/project_members_controller_spec.rb'
- - 'spec/controllers/projects/prometheus/alerts_controller_spec.rb'
- - 'spec/controllers/projects/prometheus/metrics_controller_spec.rb'
- 'spec/controllers/projects/raw_controller_spec.rb'
- 'spec/controllers/projects/refs_controller_spec.rb'
- 'spec/controllers/projects/registry/repositories_controller_spec.rb'
@@ -826,7 +807,6 @@ RSpec/BeforeAllRoleAssignment:
- 'spec/controllers/projects/work_items_controller_spec.rb'
- 'spec/controllers/projects_controller_spec.rb'
- 'spec/controllers/repositories/lfs_storage_controller_spec.rb'
- - 'spec/experiments/ios_specific_templates_experiment_spec.rb'
- 'spec/features/admin/admin_projects_spec.rb'
- 'spec/features/admin/users/user_spec.rb'
- 'spec/features/admin/users/users_spec.rb'
@@ -879,7 +859,6 @@ RSpec/BeforeAllRoleAssignment:
- 'spec/features/issues/user_edits_issue_spec.rb'
- 'spec/features/issues/user_resets_their_incoming_email_token_spec.rb'
- 'spec/features/markdown/keyboard_shortcuts_spec.rb'
- - 'spec/features/markdown/observability_spec.rb'
- 'spec/features/merge_request/user_can_see_draft_toggle_spec.rb'
- 'spec/features/merge_request/user_closes_reopens_merge_request_state_spec.rb'
- 'spec/features/merge_request/user_comments_on_whitespace_hidden_diff_spec.rb'
@@ -1000,8 +979,6 @@ RSpec/BeforeAllRoleAssignment:
- 'spec/frontend/fixtures/autocomplete.rb'
- 'spec/frontend/fixtures/autocomplete_sources.rb'
- 'spec/frontend/fixtures/groups.rb'
- - 'spec/frontend/fixtures/metrics_dashboard.rb'
- - 'spec/frontend/fixtures/milestones.rb'
- 'spec/frontend/fixtures/pipelines.rb'
- 'spec/frontend/fixtures/project.rb'
- 'spec/frontend/fixtures/releases.rb'
@@ -1115,7 +1092,6 @@ RSpec/BeforeAllRoleAssignment:
- 'spec/lib/banzai/reference_parser/work_item_parser_spec.rb'
- 'spec/lib/bulk_imports/common/pipelines/boards_pipeline_spec.rb'
- 'spec/lib/bulk_imports/common/pipelines/labels_pipeline_spec.rb'
- - 'spec/lib/bulk_imports/common/transformers/user_reference_transformer_spec.rb'
- 'spec/lib/bulk_imports/groups/pipelines/group_pipeline_spec.rb'
- 'spec/lib/bulk_imports/groups/pipelines/namespace_settings_pipeline_spec.rb'
- 'spec/lib/bulk_imports/groups/pipelines/project_entities_pipeline_spec.rb'
@@ -1142,14 +1118,12 @@ RSpec/BeforeAllRoleAssignment:
- 'spec/lib/gitlab/ci/status/build/play_spec.rb'
- 'spec/lib/gitlab/ci/variables/builder/pipeline_spec.rb'
- 'spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb'
- - 'spec/lib/gitlab/database/schema_validation/track_inconsistency_spec.rb'
- 'spec/lib/gitlab/email/handler/service_desk_handler_spec.rb'
- 'spec/lib/gitlab/git/repository_spec.rb'
- 'spec/lib/gitlab/git_access_wiki_spec.rb'
- 'spec/lib/gitlab/group_search_results_spec.rb'
- 'spec/lib/gitlab/import_export/project/tree_saver_spec.rb'
- 'spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb'
- - 'spec/lib/gitlab/metrics/dashboard/finder_spec.rb'
- 'spec/lib/gitlab/pipeline_scope_counts_spec.rb'
- 'spec/lib/gitlab/project_authorizations_spec.rb'
- 'spec/lib/gitlab/project_search_results_spec.rb'
@@ -1237,7 +1211,6 @@ RSpec/BeforeAllRoleAssignment:
- 'spec/requests/api/graphql/issue/issue_spec.rb'
- 'spec/requests/api/graphql/issue_status_counts_spec.rb'
- 'spec/requests/api/graphql/merge_request/merge_request_spec.rb'
- - 'spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb'
- 'spec/requests/api/graphql/mutations/alert_management/alerts/create_alert_issue_spec.rb'
- 'spec/requests/api/graphql/mutations/alert_management/alerts/todo/create_spec.rb'
- 'spec/requests/api/graphql/mutations/alert_management/alerts/update_alert_status_spec.rb'
@@ -1252,10 +1225,6 @@ RSpec/BeforeAllRoleAssignment:
- 'spec/requests/api/graphql/mutations/branches/create_spec.rb'
- 'spec/requests/api/graphql/mutations/ci/job_token_scope/add_project_spec.rb'
- 'spec/requests/api/graphql/mutations/ci/job_token_scope/remove_project_spec.rb'
- - 'spec/requests/api/graphql/mutations/ci/pipeline_schedule_create_spec.rb'
- - 'spec/requests/api/graphql/mutations/ci/pipeline_schedule_delete_spec.rb'
- - 'spec/requests/api/graphql/mutations/ci/pipeline_schedule_play_spec.rb'
- - 'spec/requests/api/graphql/mutations/ci/pipeline_schedule_update_spec.rb'
- 'spec/requests/api/graphql/mutations/ci/project_ci_cd_settings_update_spec.rb'
- 'spec/requests/api/graphql/mutations/ci/runner/create_spec.rb'
- 'spec/requests/api/graphql/mutations/commits/create_spec.rb'
@@ -1294,7 +1263,6 @@ RSpec/BeforeAllRoleAssignment:
- 'spec/requests/api/graphql/packages/package_spec.rb'
- 'spec/requests/api/graphql/project/alert_management/alert/assignees_spec.rb'
- 'spec/requests/api/graphql/project/alert_management/alert/issue_spec.rb'
- - 'spec/requests/api/graphql/project/alert_management/alert/metrics_dashboard_url_spec.rb'
- 'spec/requests/api/graphql/project/alert_management/alert/notes_spec.rb'
- 'spec/requests/api/graphql/project/alert_management/alert/todos_spec.rb'
- 'spec/requests/api/graphql/project/alert_management/alert_status_counts_spec.rb'
@@ -1371,7 +1339,6 @@ RSpec/BeforeAllRoleAssignment:
- 'spec/requests/api/rubygem_packages_spec.rb'
- 'spec/requests/api/search_spec.rb'
- 'spec/requests/api/terraform/modules/v1/packages_spec.rb'
- - 'spec/requests/api/v3/github_spec.rb'
- 'spec/requests/api/wikis_spec.rb'
- 'spec/requests/concerns/planning_hierarchy_spec.rb'
- 'spec/requests/groups/deploy_tokens_controller_spec.rb'
@@ -1396,7 +1363,6 @@ RSpec/BeforeAllRoleAssignment:
- 'spec/requests/projects/merge_requests/diffs_spec.rb'
- 'spec/requests/projects/merge_requests_controller_spec.rb'
- 'spec/requests/projects/merge_requests_spec.rb'
- - 'spec/requests/projects/metrics/dashboards/builder_spec.rb'
- 'spec/requests/projects/releases_controller_spec.rb'
- 'spec/requests/projects/settings/access_tokens_controller_spec.rb'
- 'spec/requests/projects/tags_controller_spec.rb'
@@ -1489,16 +1455,6 @@ RSpec/BeforeAllRoleAssignment:
- 'spec/services/merge_requests/retarget_chain_service_spec.rb'
- 'spec/services/merge_requests/update_assignees_service_spec.rb'
- 'spec/services/merge_requests/update_reviewers_service_spec.rb'
- - 'spec/services/metrics/dashboard/clone_dashboard_service_spec.rb'
- - 'spec/services/metrics/dashboard/cluster_dashboard_service_spec.rb'
- - 'spec/services/metrics/dashboard/cluster_metrics_embed_service_spec.rb'
- - 'spec/services/metrics/dashboard/custom_dashboard_service_spec.rb'
- - 'spec/services/metrics/dashboard/default_embed_service_spec.rb'
- - 'spec/services/metrics/dashboard/dynamic_embed_service_spec.rb'
- - 'spec/services/metrics/dashboard/pod_dashboard_service_spec.rb'
- - 'spec/services/metrics/dashboard/system_dashboard_service_spec.rb'
- - 'spec/services/metrics/dashboard/transient_embed_service_spec.rb'
- - 'spec/services/metrics/dashboard/update_dashboard_service_spec.rb'
- 'spec/services/notes/build_service_spec.rb'
- 'spec/services/notes/create_service_spec.rb'
- 'spec/services/notes/quick_actions_service_spec.rb'
@@ -1548,7 +1504,6 @@ RSpec/BeforeAllRoleAssignment:
- 'spec/services/work_items/widgets/description_service/update_service_spec.rb'
- 'spec/services/work_items/widgets/hierarchy_service/create_service_spec.rb'
- 'spec/services/work_items/widgets/hierarchy_service/update_service_spec.rb'
- - 'spec/support/helpers/cycle_analytics_helpers/test_generation.rb'
- 'spec/support/shared_contexts/changes_access_checks_shared_context.rb'
- 'spec/support/shared_contexts/design_management_shared_contexts.rb'
- 'spec/support/shared_contexts/features/integrations/project_integrations_shared_context.rb'
diff --git a/.rubocop_todo/rspec/context_wording.yml b/.rubocop_todo/rspec/context_wording.yml
index f95f6424593..9412efcc702 100644
--- a/.rubocop_todo/rspec/context_wording.yml
+++ b/.rubocop_todo/rspec/context_wording.yml
@@ -192,8 +192,6 @@ RSpec/ContextWording:
- 'ee/spec/finders/productivity_analytics_finder_spec.rb'
- 'ee/spec/finders/scim_finder_spec.rb'
- 'ee/spec/finders/security/pipeline_vulnerabilities_finder_spec.rb'
- - 'ee/spec/finders/security/training_providers/base_url_finder_spec.rb'
- - 'ee/spec/finders/security/training_urls_finder_spec.rb'
- 'ee/spec/finders/security/vulnerabilities_finder_spec.rb'
- 'ee/spec/finders/security/vulnerability_reads_finder_spec.rb'
- 'ee/spec/finders/snippets_finder_spec.rb'
@@ -368,7 +366,6 @@ RSpec/ContextWording:
- 'ee/spec/lib/gitlab/geo/git_ssh_proxy_spec.rb'
- 'ee/spec/lib/gitlab/geo/health_check_spec.rb'
- 'ee/spec/lib/gitlab/geo/jwt_request_decoder_spec.rb'
- - 'ee/spec/lib/gitlab/geo/log_cursor/events/repository_updated_event_spec.rb'
- 'ee/spec/lib/gitlab/geo/log_helpers_spec.rb'
- 'ee/spec/lib/gitlab/geo/oauth/session_spec.rb'
- 'ee/spec/lib/gitlab/geo/replication/blob_downloader_spec.rb'
@@ -422,7 +419,6 @@ RSpec/ContextWording:
- 'ee/spec/models/concerns/deprecated_approvals_before_merge_spec.rb'
- 'ee/spec/models/concerns/ee/issuable_spec.rb'
- 'ee/spec/models/concerns/ee/participable_spec.rb'
- - 'ee/spec/models/concerns/elastic/issue_spec.rb'
- 'ee/spec/models/concerns/elastic/note_spec.rb'
- 'ee/spec/models/concerns/elastic/project_spec.rb'
- 'ee/spec/models/concerns/elastic/repository_spec.rb'
@@ -454,7 +450,6 @@ RSpec/ContextWording:
- 'ee/spec/models/epic_issue_spec.rb'
- 'ee/spec/models/epic_spec.rb'
- 'ee/spec/models/geo/container_repository_registry_spec.rb'
- - 'ee/spec/models/geo/project_registry_spec.rb'
- 'ee/spec/models/geo/secondary_usage_data_spec.rb'
- 'ee/spec/models/geo_node_spec.rb'
- 'ee/spec/models/geo_node_status_spec.rb'
@@ -483,7 +478,6 @@ RSpec/ContextWording:
- 'ee/spec/models/project_import_data_spec.rb'
- 'ee/spec/models/project_import_state_spec.rb'
- 'ee/spec/models/project_member_spec.rb'
- - 'ee/spec/models/project_team_spec.rb'
- 'ee/spec/models/protected_environment_spec.rb'
- 'ee/spec/models/push_rule_spec.rb'
- 'ee/spec/models/release_highlight_spec.rb'
@@ -501,7 +495,6 @@ RSpec/ContextWording:
- 'ee/spec/policies/epic_policy_spec.rb'
- 'ee/spec/policies/global_policy_spec.rb'
- 'ee/spec/policies/group_policy_spec.rb'
- - 'ee/spec/policies/identity_provider_policy_spec.rb'
- 'ee/spec/policies/incident_management/oncall_rotation_policy_spec.rb'
- 'ee/spec/policies/incident_management/oncall_schedule_policy_spec.rb'
- 'ee/spec/policies/incident_management/oncall_shift_policy_spec.rb'
@@ -600,7 +593,6 @@ RSpec/ContextWording:
- 'ee/spec/requests/api/settings_spec.rb'
- 'ee/spec/requests/api/status_checks_spec.rb'
- 'ee/spec/requests/api/users_spec.rb'
- - 'ee/spec/requests/api/v3/github_spec.rb'
- 'ee/spec/requests/api/vulnerability_findings_spec.rb'
- 'ee/spec/requests/git_http_geo_spec.rb'
- 'ee/spec/requests/groups/roadmap_controller_spec.rb'
@@ -718,18 +710,9 @@ RSpec/ContextWording:
- 'ee/spec/services/external_status_checks/dispatch_service_spec.rb'
- 'ee/spec/services/geo/container_repository_sync_service_spec.rb'
- 'ee/spec/services/geo/container_repository_sync_spec.rb'
- - 'ee/spec/services/geo/design_repository_sync_service_spec.rb'
- 'ee/spec/services/geo/framework_repository_sync_service_spec.rb'
- - 'ee/spec/services/geo/hashed_storage_migration_service_spec.rb'
- - 'ee/spec/services/geo/move_repository_service_spec.rb'
- 'ee/spec/services/geo/node_update_service_spec.rb'
- - 'ee/spec/services/geo/project_housekeeping_service_spec.rb'
- 'ee/spec/services/geo/prune_event_log_service_spec.rb'
- - 'ee/spec/services/geo/rename_repository_service_spec.rb'
- - 'ee/spec/services/geo/repository_created_event_store_spec.rb'
- - 'ee/spec/services/geo/repository_sync_service_spec.rb'
- - 'ee/spec/services/geo/repository_verification_reset_spec.rb'
- - 'ee/spec/services/geo/wiki_sync_service_spec.rb'
- 'ee/spec/services/gitlab_subscriptions/create_service_spec.rb'
- 'ee/spec/services/gitlab_subscriptions/preview_billable_user_change_service_spec.rb'
- 'ee/spec/services/group_saml/group_managed_accounts/transfer_membership_service_spec.rb'
@@ -896,10 +879,6 @@ RSpec/ContextWording:
- 'ee/spec/workers/elastic_index_bulk_cron_worker_spec.rb'
- 'ee/spec/workers/elastic_indexing_control_worker_spec.rb'
- 'ee/spec/workers/geo/prune_event_log_worker_spec.rb'
- - 'ee/spec/workers/geo/repository_shard_sync_worker_spec.rb'
- - 'ee/spec/workers/geo/repository_sync_worker_spec.rb'
- - 'ee/spec/workers/geo/repository_verification/primary/shard_worker_spec.rb'
- - 'ee/spec/workers/geo/repository_verification/secondary/shard_worker_spec.rb'
- 'ee/spec/workers/geo/verification_timeout_worker_spec.rb'
- 'ee/spec/workers/group_saml_group_sync_worker_spec.rb'
- 'ee/spec/workers/incident_management/apply_incident_sla_exceeded_label_worker_spec.rb'
@@ -914,8 +893,6 @@ RSpec/ContextWording:
- 'ee/spec/workers/security/store_scans_worker_spec.rb'
- 'ee/spec/workers/security/track_secure_scans_worker_spec.rb'
- 'ee/spec/workers/sync_seat_link_worker_spec.rb'
- - 'qa/qa/specs/features/api/1_manage/user_access_termination_spec.rb'
- - 'qa/qa/specs/features/browser_ui/1_manage/login/2fa_ssh_recovery_spec.rb'
- 'qa/qa/specs/features/browser_ui/2_plan/milestone/assign_milestone_spec.rb'
- 'qa/qa/specs/features/browser_ui/3_create/repository/ssh_key_support_spec.rb'
- 'qa/qa/specs/features/browser_ui/4_verify/testing/endpoint_coverage_spec.rb'
@@ -924,8 +901,6 @@ RSpec/ContextWording:
- 'qa/qa/specs/features/ee/browser_ui/11_fulfillment/license/cloud_activation_spec.rb'
- 'qa/qa/specs/features/ee/browser_ui/11_fulfillment/license/license_spec.rb'
- 'qa/qa/specs/features/ee/browser_ui/13_secure/enable_scanning_from_configuration_spec.rb'
- - 'qa/qa/specs/features/ee/browser_ui/13_secure/license_compliance_spec.rb'
- - 'qa/qa/specs/features/ee/browser_ui/1_manage/group/group_ldap_sync_spec.rb'
- 'qa/qa/specs/features/ee/browser_ui/2_plan/epic/epics_management_spec.rb'
- 'qa/qa/specs/features/ee/browser_ui/2_plan/issue_boards/project_issue_boards_spec.rb'
- 'qa/qa/specs/features/ee/browser_ui/3_create/repository/push_rules_spec.rb'
@@ -959,7 +934,6 @@ RSpec/ContextWording:
- 'spec/controllers/concerns/group_tree_spec.rb'
- 'spec/controllers/concerns/import_url_params_spec.rb'
- 'spec/controllers/concerns/issuable_collections_spec.rb'
- - 'spec/controllers/concerns/metrics_dashboard_spec.rb'
- 'spec/controllers/concerns/renders_commits_spec.rb'
- 'spec/controllers/confirmations_controller_spec.rb'
- 'spec/controllers/dashboard/milestones_controller_spec.rb'
@@ -1033,11 +1007,8 @@ RSpec/ContextWording:
- 'spec/controllers/projects/packages/packages_controller_spec.rb'
- 'spec/controllers/projects/pages_controller_spec.rb'
- 'spec/controllers/projects/pages_domains_controller_spec.rb'
- - 'spec/controllers/projects/performance_monitoring/dashboards_controller_spec.rb'
- 'spec/controllers/projects/pipelines_controller_spec.rb'
- 'spec/controllers/projects/project_members_controller_spec.rb'
- - 'spec/controllers/projects/prometheus/alerts_controller_spec.rb'
- - 'spec/controllers/projects/prometheus/metrics_controller_spec.rb'
- 'spec/controllers/projects/raw_controller_spec.rb'
- 'spec/controllers/projects/releases_controller_spec.rb'
- 'spec/controllers/projects/settings/ci_cd_controller_spec.rb'
@@ -1063,7 +1034,6 @@ RSpec/ContextWording:
- 'spec/docs_screenshots/container_registry_docs.rb'
- 'spec/docs_screenshots/wiki_docs.rb'
- 'spec/experiments/application_experiment_spec.rb'
- - 'spec/experiments/ios_specific_templates_experiment_spec.rb'
- 'spec/features/admin/admin_appearance_spec.rb'
- 'spec/features/admin/admin_disables_git_access_protocol_spec.rb'
- 'spec/features/admin/admin_hook_logs_spec.rb'
@@ -1323,7 +1293,6 @@ RSpec/ContextWording:
- 'spec/finders/merge_request_target_project_finder_spec.rb'
- 'spec/finders/merge_requests/by_approvals_finder_spec.rb'
- 'spec/finders/merge_requests_finder_spec.rb'
- - 'spec/finders/metrics/users_starred_dashboards_finder_spec.rb'
- 'spec/finders/milestones_finder_spec.rb'
- 'spec/finders/notes_finder_spec.rb'
- 'spec/finders/packages/group_packages_finder_spec.rb'
@@ -1611,7 +1580,6 @@ RSpec/ContextWording:
- 'spec/lib/gitlab/background_migration/backfill_imported_issue_search_data_spec.rb'
- 'spec/lib/gitlab/background_migration/copy_column_using_background_migration_job_spec.rb'
- 'spec/lib/gitlab/bitbucket_import/importer_spec.rb'
- - 'spec/lib/gitlab/bitbucket_server_import/importer_spec.rb'
- 'spec/lib/gitlab/blame_spec.rb'
- 'spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb'
- 'spec/lib/gitlab/cache/helpers_spec.rb'
@@ -1701,7 +1669,6 @@ RSpec/ContextWording:
- 'spec/lib/gitlab/composer/cache_spec.rb'
- 'spec/lib/gitlab/config/entry/composable_array_spec.rb'
- 'spec/lib/gitlab/config/entry/composable_hash_spec.rb'
- - 'spec/lib/gitlab/config_checker/puma_rugged_checker_spec.rb'
- 'spec/lib/gitlab/consul/internal_spec.rb'
- 'spec/lib/gitlab/content_security_policy/config_loader_spec.rb'
- 'spec/lib/gitlab/cycle_analytics/permissions_spec.rb'
@@ -1753,8 +1720,6 @@ RSpec/ContextWording:
- 'spec/lib/gitlab/database/reindexing/grafana_notifier_spec.rb'
- 'spec/lib/gitlab/database/reindexing/reindex_action_spec.rb'
- 'spec/lib/gitlab/database/reindexing_spec.rb'
- - 'spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb'
- - 'spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb'
- 'spec/lib/gitlab/database/schema_migrations/context_spec.rb'
- 'spec/lib/gitlab/database/with_lock_retries_outside_transaction_spec.rb'
- 'spec/lib/gitlab/database/with_lock_retries_spec.rb'
@@ -1804,7 +1769,6 @@ RSpec/ContextWording:
- 'spec/lib/gitlab/git/pre_receive_error_spec.rb'
- 'spec/lib/gitlab/git/raw_diff_change_spec.rb'
- 'spec/lib/gitlab/git/repository_spec.rb'
- - 'spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb'
- 'spec/lib/gitlab/git/tag_spec.rb'
- 'spec/lib/gitlab/git/wiki_page_version_spec.rb'
- 'spec/lib/gitlab/git_access_snippet_spec.rb'
@@ -1897,14 +1861,6 @@ RSpec/ContextWording:
- 'spec/lib/gitlab/memory/reports_daemon_spec.rb'
- 'spec/lib/gitlab/memory/watchdog_spec.rb'
- 'spec/lib/gitlab/merge_requests/message_generator_spec.rb'
- - 'spec/lib/gitlab/metrics/dashboard/cache_spec.rb'
- - 'spec/lib/gitlab/metrics/dashboard/importer_spec.rb'
- - 'spec/lib/gitlab/metrics/dashboard/importers/prometheus_metrics_spec.rb'
- - 'spec/lib/gitlab/metrics/dashboard/stages/url_validator_spec.rb'
- - 'spec/lib/gitlab/metrics/dashboard/transformers/yml/v1/prometheus_metrics_spec.rb'
- - 'spec/lib/gitlab/metrics/dashboard/validator/errors_spec.rb'
- - 'spec/lib/gitlab/metrics/dashboard/validator/post_schema_validator_spec.rb'
- - 'spec/lib/gitlab/metrics/dashboard/validator_spec.rb'
- 'spec/lib/gitlab/metrics/exporter/base_exporter_spec.rb'
- 'spec/lib/gitlab/metrics/methods_spec.rb'
- 'spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb'
@@ -1937,9 +1893,6 @@ RSpec/ContextWording:
- 'spec/lib/gitlab/project_authorizations_spec.rb'
- 'spec/lib/gitlab/project_search_results_spec.rb'
- 'spec/lib/gitlab/prometheus/adapter_spec.rb'
- - 'spec/lib/gitlab/prometheus/queries/matched_metric_query_spec.rb'
- - 'spec/lib/gitlab/prometheus/queries/validate_query_spec.rb'
- - 'spec/lib/gitlab/prometheus/query_variables_spec.rb'
- 'spec/lib/gitlab/prometheus_client_spec.rb'
- 'spec/lib/gitlab/query_limiting/middleware_spec.rb'
- 'spec/lib/gitlab/query_limiting_spec.rb'
@@ -2203,8 +2156,6 @@ RSpec/ContextWording:
- 'spec/models/merge_request_diff_file_spec.rb'
- 'spec/models/merge_request_diff_spec.rb'
- 'spec/models/merge_request_spec.rb'
- - 'spec/models/metrics/dashboard/annotation_spec.rb'
- - 'spec/models/metrics/users_starred_dashboard_spec.rb'
- 'spec/models/milestone_spec.rb'
- 'spec/models/namespace/package_setting_spec.rb'
- 'spec/models/namespace/root_storage_statistics_spec.rb'
@@ -2221,19 +2172,13 @@ RSpec/ContextWording:
- 'spec/models/packages/package_file_spec.rb'
- 'spec/models/packages/package_spec.rb'
- 'spec/models/pages_domain_spec.rb'
- - 'spec/models/performance_monitoring/prometheus_dashboard_spec.rb'
- - 'spec/models/performance_monitoring/prometheus_metric_spec.rb'
- - 'spec/models/performance_monitoring/prometheus_panel_group_spec.rb'
- - 'spec/models/performance_monitoring/prometheus_panel_spec.rb'
- 'spec/models/personal_access_token_spec.rb'
- 'spec/models/plan_limits_spec.rb'
- 'spec/models/preloaders/labels_preloader_spec.rb'
- 'spec/models/project_authorization_spec.rb'
- 'spec/models/project_feature_spec.rb'
- - 'spec/models/project_feature_usage_spec.rb'
- 'spec/models/project_import_state_spec.rb'
- 'spec/models/project_label_spec.rb'
- - 'spec/models/project_metrics_setting_spec.rb'
- 'spec/models/project_spec.rb'
- 'spec/models/project_team_spec.rb'
- 'spec/models/prometheus_alert_event_spec.rb'
@@ -2277,7 +2222,6 @@ RSpec/ContextWording:
- 'spec/policies/group_policy_spec.rb'
- 'spec/policies/issuable_policy_spec.rb'
- 'spec/policies/issue_policy_spec.rb'
- - 'spec/policies/metrics/dashboard/annotation_policy_spec.rb'
- 'spec/policies/namespaces/user_namespace_policy_spec.rb'
- 'spec/policies/personal_access_token_policy_spec.rb'
- 'spec/policies/personal_snippet_policy_spec.rb'
@@ -2353,8 +2297,6 @@ RSpec/ContextWording:
- 'spec/requests/api/graphql/group/milestones_spec.rb'
- 'spec/requests/api/graphql/issue/issue_spec.rb'
- 'spec/requests/api/graphql/metadata_query_spec.rb'
- - 'spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb'
- - 'spec/requests/api/graphql/metrics/dashboard_query_spec.rb'
- 'spec/requests/api/graphql/mutations/admin/sidekiq_queues/delete_jobs_spec.rb'
- 'spec/requests/api/graphql/mutations/alert_management/alerts/todo/create_spec.rb'
- 'spec/requests/api/graphql/mutations/boards/issues/issue_move_list_spec.rb'
@@ -2465,7 +2407,6 @@ RSpec/ContextWording:
- 'spec/requests/api/usage_data_spec.rb'
- 'spec/requests/api/users_preferences_spec.rb'
- 'spec/requests/api/users_spec.rb'
- - 'spec/requests/api/v3/github_spec.rb'
- 'spec/requests/content_security_policy_spec.rb'
- 'spec/requests/dashboard/projects_controller_spec.rb'
- 'spec/requests/dashboard_controller_spec.rb'
@@ -2496,7 +2437,6 @@ RSpec/ContextWording:
- 'spec/requests/projects/issues_controller_spec.rb'
- 'spec/requests/projects/merge_requests_controller_spec.rb'
- 'spec/requests/projects/merge_requests_discussions_spec.rb'
- - 'spec/requests/projects/metrics/dashboards/builder_spec.rb'
- 'spec/requests/projects/releases_controller_spec.rb'
- 'spec/requests/projects/settings/access_tokens_controller_spec.rb'
- 'spec/requests/projects/tags_controller_spec.rb'
@@ -2515,7 +2455,6 @@ RSpec/ContextWording:
- 'spec/rubocop/cop/graphql/id_type_spec.rb'
- 'spec/rubocop/cop/graphql/json_type_spec.rb'
- 'spec/rubocop/cop/graphql/old_types_spec.rb'
- - 'spec/rubocop/cop/lint/last_keyword_argument_spec.rb'
- 'spec/rubocop/cop/migration/add_index_spec.rb'
- 'spec/rubocop/cop/migration/background_migration_record_spec.rb'
- 'spec/rubocop/cop/migration/create_table_with_foreign_keys_spec.rb'
@@ -2637,7 +2576,6 @@ RSpec/ContextWording:
- 'spec/services/git/wiki_push_service_spec.rb'
- 'spec/services/google_cloud/generate_pipeline_service_spec.rb'
- 'spec/services/gpg_keys/create_service_spec.rb'
- - 'spec/services/grafana/proxy_service_spec.rb'
- 'spec/services/groups/create_service_spec.rb'
- 'spec/services/groups/deploy_tokens/revoke_service_spec.rb'
- 'spec/services/groups/destroy_service_spec.rb'
@@ -2692,7 +2630,6 @@ RSpec/ContextWording:
- 'spec/services/merge_requests/create_from_issue_service_spec.rb'
- 'spec/services/merge_requests/create_service_spec.rb'
- 'spec/services/merge_requests/export_csv_service_spec.rb'
- - 'spec/services/merge_requests/ff_merge_service_spec.rb'
- 'spec/services/merge_requests/get_urls_service_spec.rb'
- 'spec/services/merge_requests/link_lfs_objects_service_spec.rb'
- 'spec/services/merge_requests/merge_orchestration_service_spec.rb'
@@ -2706,23 +2643,6 @@ RSpec/ContextWording:
- 'spec/services/merge_requests/reopen_service_spec.rb'
- 'spec/services/merge_requests/squash_service_spec.rb'
- 'spec/services/merge_requests/update_service_spec.rb'
- - 'spec/services/metrics/dashboard/annotations/create_service_spec.rb'
- - 'spec/services/metrics/dashboard/annotations/delete_service_spec.rb'
- - 'spec/services/metrics/dashboard/clone_dashboard_service_spec.rb'
- - 'spec/services/metrics/dashboard/cluster_dashboard_service_spec.rb'
- - 'spec/services/metrics/dashboard/cluster_metrics_embed_service_spec.rb'
- - 'spec/services/metrics/dashboard/custom_dashboard_service_spec.rb'
- - 'spec/services/metrics/dashboard/custom_metric_embed_service_spec.rb'
- - 'spec/services/metrics/dashboard/default_embed_service_spec.rb'
- - 'spec/services/metrics/dashboard/dynamic_embed_service_spec.rb'
- - 'spec/services/metrics/dashboard/gitlab_alert_embed_service_spec.rb'
- - 'spec/services/metrics/dashboard/grafana_metric_embed_service_spec.rb'
- - 'spec/services/metrics/dashboard/panel_preview_service_spec.rb'
- - 'spec/services/metrics/dashboard/pod_dashboard_service_spec.rb'
- - 'spec/services/metrics/dashboard/system_dashboard_service_spec.rb'
- - 'spec/services/metrics/dashboard/transient_embed_service_spec.rb'
- - 'spec/services/metrics/dashboard/update_dashboard_service_spec.rb'
- - 'spec/services/metrics/users_starred_dashboards/create_service_spec.rb'
- 'spec/services/milestones/create_service_spec.rb'
- 'spec/services/milestones/destroy_service_spec.rb'
- 'spec/services/milestones/promote_service_spec.rb'
@@ -2775,9 +2695,7 @@ RSpec/ContextWording:
- 'spec/services/projects/git_deduplication_service_spec.rb'
- 'spec/services/projects/group_links/destroy_service_spec.rb'
- 'spec/services/projects/group_links/update_service_spec.rb'
- - 'spec/services/projects/hashed_storage/migrate_repository_service_spec.rb'
- 'spec/services/projects/hashed_storage/migration_service_spec.rb'
- - 'spec/services/projects/in_product_marketing_campaign_emails_service_spec.rb'
- 'spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb'
- 'spec/services/projects/lfs_pointers/lfs_download_service_spec.rb'
- 'spec/services/projects/operations/update_service_spec.rb'
@@ -2790,8 +2708,6 @@ RSpec/ContextWording:
- 'spec/services/projects/update_remote_mirror_service_spec.rb'
- 'spec/services/projects/update_repository_storage_service_spec.rb'
- 'spec/services/projects/update_service_spec.rb'
- - 'spec/services/prometheus/proxy_service_spec.rb'
- - 'spec/services/prometheus/proxy_variable_substitution_service_spec.rb'
- 'spec/services/protected_tags/create_service_spec.rb'
- 'spec/services/quick_actions/interpret_service_spec.rb'
- 'spec/services/releases/create_service_spec.rb'
@@ -2847,7 +2763,6 @@ RSpec/ContextWording:
- 'spec/services/wikis/create_attachment_service_spec.rb'
- 'spec/services/work_items/create_service_spec.rb'
- 'spec/services/work_items/parent_links/create_service_spec.rb'
- - 'spec/support/helpers/cycle_analytics_helpers/test_generation.rb'
- 'spec/support/shared_contexts/bulk_imports_requests_shared_context.rb'
- 'spec/support/shared_contexts/changes_access_checks_shared_context.rb'
- 'spec/support/shared_contexts/container_repositories_shared_context.rb'
@@ -3028,7 +2943,6 @@ RSpec/ContextWording:
- 'spec/support/shared_examples/services/issuable_shared_examples.rb'
- 'spec/support/shared_examples/services/jira_import/user_mapper_services_shared_examples.rb'
- 'spec/support/shared_examples/services/merge_request_shared_examples.rb'
- - 'spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb'
- 'spec/support/shared_examples/services/notification_service_shared_examples.rb'
- 'spec/support/shared_examples/services/packages_shared_examples.rb'
- 'spec/support/shared_examples/services/repositories/housekeeping_shared_examples.rb'
@@ -3110,7 +3024,6 @@ RSpec/ContextWording:
- 'spec/views/projects/empty.html.haml_spec.rb'
- 'spec/views/projects/hooks/edit.html.haml_spec.rb'
- 'spec/views/projects/hooks/index.html.haml_spec.rb'
- - 'spec/views/projects/pipeline_schedules/_pipeline_schedule.html.haml_spec.rb'
- 'spec/views/projects/settings/integrations/edit.html.haml_spec.rb'
- 'spec/views/projects/settings/operations/show.html.haml_spec.rb'
- 'spec/views/projects/tags/index.html.haml_spec.rb'
diff --git a/.rubocop_todo/rspec/empty_line_after_hook.yml b/.rubocop_todo/rspec/empty_line_after_hook.yml
index 7dc3fd235a4..299d32f2af2 100644
--- a/.rubocop_todo/rspec/empty_line_after_hook.yml
+++ b/.rubocop_todo/rspec/empty_line_after_hook.yml
@@ -29,7 +29,6 @@ RSpec/EmptyLineAfterHook:
- 'spec/features/users/overview_spec.rb'
- 'spec/lib/gitlab/auth/ldap/person_spec.rb'
- 'spec/lib/gitlab/database/migrations/instrumentation_spec.rb'
- - 'spec/lib/gitlab/prometheus/queries/matched_metric_query_spec.rb'
- 'spec/lib/gitlab/sidekiq_middleware_spec.rb'
- 'spec/mailers/emails/pages_domains_spec.rb'
- 'spec/models/application_record_spec.rb'
@@ -45,6 +44,5 @@ RSpec/EmptyLineAfterHook:
- 'spec/services/notes/create_service_spec.rb'
- 'spec/services/notes/quick_actions_service_spec.rb'
- 'spec/services/projects/fork_service_spec.rb'
- - 'spec/support/redis/redis_shared_examples.rb'
- 'spec/support/shared_examples/requests/api/milestones_shared_examples.rb'
- 'spec/support/shared_examples/sends_git_audit_streaming_event_shared_examples.rb'
diff --git a/.rubocop_todo/rspec/expect_change.yml b/.rubocop_todo/rspec/expect_change.yml
index f625c948e79..ba2068d8117 100644
--- a/.rubocop_todo/rspec/expect_change.yml
+++ b/.rubocop_todo/rspec/expect_change.yml
@@ -23,12 +23,6 @@ RSpec/ExpectChange:
- 'ee/spec/lib/gitlab/compliance_management/violations/approved_by_insufficient_users_spec.rb'
- 'ee/spec/lib/gitlab/compliance_management/violations/approved_by_merge_request_author_spec.rb'
- 'ee/spec/lib/gitlab/geo/log_cursor/events/hashed_storage_attachments_event_spec.rb'
- - 'ee/spec/lib/gitlab/geo/log_cursor/events/hashed_storage_migrated_event_spec.rb'
- - 'ee/spec/lib/gitlab/geo/log_cursor/events/repository_created_event_spec.rb'
- - 'ee/spec/lib/gitlab/geo/log_cursor/events/repository_deleted_event_spec.rb'
- - 'ee/spec/lib/gitlab/geo/log_cursor/events/repository_renamed_event_spec.rb'
- - 'ee/spec/lib/gitlab/geo/log_cursor/events/repository_updated_event_spec.rb'
- - 'ee/spec/lib/gitlab/geo/log_cursor/events/reset_checksum_event_spec.rb'
- 'ee/spec/lib/gitlab/instrumentation/elasticsearch_transport_spec.rb'
- 'ee/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb'
- 'ee/spec/lib/quality/seeders/vulnerabilities_spec.rb'
@@ -65,7 +59,6 @@ RSpec/ExpectChange:
- 'ee/spec/requests/api/graphql/mutations/security/finding/create_issue_spec.rb'
- 'ee/spec/requests/api/graphql/mutations/vulnerabilities/create_external_issue_link_spec.rb'
- 'ee/spec/requests/api/graphql/mutations/vulnerabilities/destroy_external_issue_link_spec.rb'
- - 'ee/spec/requests/api/graphql/mutations/vulnerabilities/finding_dismiss_spec.rb'
- 'ee/spec/requests/api/graphql/mutations/work_items/update_spec.rb'
- 'ee/spec/requests/api/graphql/vulnerabilities/external_issue_links_spec.rb'
- 'ee/spec/requests/api/groups_spec.rb'
@@ -83,7 +76,6 @@ RSpec/ExpectChange:
- 'ee/spec/services/applications/create_service_spec.rb'
- 'ee/spec/services/approval_rules/create_service_spec.rb'
- 'ee/spec/services/audit_event_service_spec.rb'
- - 'ee/spec/services/audit_events/impersonation_audit_event_service_spec.rb'
- 'ee/spec/services/audit_events/protected_branch_audit_event_service_spec.rb'
- 'ee/spec/services/audit_events/runner_custom_audit_event_service_spec.rb'
- 'ee/spec/services/audit_events/runners_token_audit_event_service_spec.rb'
@@ -127,11 +119,6 @@ RSpec/ExpectChange:
- 'ee/spec/services/geo/file_registry_removal_service_spec.rb'
- 'ee/spec/services/geo/node_create_service_spec.rb'
- 'ee/spec/services/geo/node_update_service_spec.rb'
- - 'ee/spec/services/geo/project_housekeeping_service_spec.rb'
- - 'ee/spec/services/geo/repository_sync_service_spec.rb'
- - 'ee/spec/services/geo/repository_updated_service_spec.rb'
- - 'ee/spec/services/geo/repository_verification_primary_service_spec.rb'
- - 'ee/spec/services/geo/wiki_sync_service_spec.rb'
- 'ee/spec/services/gitlab_subscriptions/activate_service_spec.rb'
- 'ee/spec/services/gitlab_subscriptions/create_service_spec.rb'
- 'ee/spec/services/group_saml/identity/destroy_service_spec.rb'
@@ -146,12 +133,10 @@ RSpec/ExpectChange:
- 'ee/spec/services/iterations/cadences/update_service_spec.rb'
- 'ee/spec/services/iterations/delete_service_spec.rb'
- 'ee/spec/services/iterations/roll_over_issues_service_spec.rb'
- - 'ee/spec/services/projects/after_rename_service_spec.rb'
- 'ee/spec/services/projects/alerting/notify_service_spec.rb'
- 'ee/spec/services/projects/create_service_spec.rb'
- 'ee/spec/services/projects/destroy_service_spec.rb'
- 'ee/spec/services/projects/hashed_storage/migrate_attachments_service_spec.rb'
- - 'ee/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb'
- 'ee/spec/services/projects/transfer_service_spec.rb'
- 'ee/spec/services/protected_environments/create_service_spec.rb'
- 'ee/spec/services/quality_management/test_cases/create_service_spec.rb'
@@ -168,7 +153,6 @@ RSpec/ExpectChange:
- 'ee/spec/services/users/email_verification/send_custom_confirmation_instructions_service_spec.rb'
- 'ee/spec/services/vulnerabilities/dismiss_service_spec.rb'
- 'ee/spec/services/vulnerabilities/find_or_create_from_security_finding_service_spec.rb'
- - 'ee/spec/services/vulnerabilities/finding_dismiss_service_spec.rb'
- 'ee/spec/services/vulnerabilities/manually_create_service_spec.rb'
- 'ee/spec/services/vulnerabilities/security_finding/create_issue_service_spec.rb'
- 'ee/spec/services/vulnerabilities/security_finding/create_merge_request_service_spec.rb'
@@ -187,9 +171,7 @@ RSpec/ExpectChange:
- 'ee/spec/support/shared_examples/services/vulnerabilities/does_not_create_state_transition_for_same_state.rb'
- 'ee/spec/workers/concerns/update_orchestration_policy_configuration_spec.rb'
- 'ee/spec/workers/ee/issuable_export_csv_worker_spec.rb'
- - 'ee/spec/workers/geo/batch/project_registry_scheduler_worker_spec.rb'
- 'ee/spec/workers/geo/destroy_worker_spec.rb'
- - 'ee/spec/workers/geo/file_registry_removal_worker_spec.rb'
- 'ee/spec/workers/groups/create_event_worker_spec.rb'
- 'ee/spec/workers/import_software_licenses_worker_spec.rb'
- 'ee/spec/workers/sync_seat_link_request_worker_spec.rb'
@@ -200,7 +182,6 @@ RSpec/ExpectChange:
- 'spec/controllers/groups/group_links_controller_spec.rb'
- 'spec/controllers/groups_controller_spec.rb'
- 'spec/controllers/import/bitbucket_controller_spec.rb'
- - 'spec/controllers/import/gitlab_controller_spec.rb'
- 'spec/controllers/projects/boards_controller_spec.rb'
- 'spec/controllers/projects/deploy_keys_controller_spec.rb'
- 'spec/controllers/projects/hooks_controller_spec.rb'
@@ -237,8 +218,6 @@ RSpec/ExpectChange:
- 'spec/lib/gitlab/auth_spec.rb'
- 'spec/lib/gitlab/background_migration/backfill_project_repositories_spec.rb'
- 'spec/lib/gitlab/background_migration/delete_orphaned_operational_vulnerabilities_spec.rb'
- - 'spec/lib/gitlab/background_migration/project_namespaces/backfill_project_namespaces_spec.rb'
- - 'spec/lib/gitlab/background_migration/remove_occurrence_pipelines_and_duplicate_vulnerabilities_findings_spec.rb'
- 'spec/lib/gitlab/background_migration/remove_self_managed_wiki_notes_spec.rb'
- 'spec/lib/gitlab/background_migration/rename_task_system_note_to_checklist_item_spec.rb'
- 'spec/lib/gitlab/ci/pipeline/chain/assign_partition_spec.rb'
@@ -253,27 +232,16 @@ RSpec/ExpectChange:
- 'spec/lib/gitlab/github_import/importer/events/changed_reviewer_spec.rb'
- 'spec/lib/gitlab/github_import/importer/note_importer_spec.rb'
- 'spec/lib/gitlab/github_import/importer/protected_branch_importer_spec.rb'
- - 'spec/lib/gitlab/github_import/importer/pull_request_merged_by_importer_spec.rb'
- - 'spec/lib/gitlab/github_import/importer/pull_request_review_importer_spec.rb'
- - 'spec/lib/gitlab/hashed_storage/migrator_spec.rb'
- 'spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb'
- 'spec/lib/gitlab/import_export/base/relation_object_saver_spec.rb'
- 'spec/lib/gitlab/import_export/importer_spec.rb'
- 'spec/lib/gitlab/legacy_github_import/label_formatter_spec.rb'
- 'spec/lib/gitlab/legacy_github_import/project_creator_spec.rb'
- - 'spec/lib/gitlab/pages/cache_control_spec.rb'
- 'spec/lib/gitlab/query_limiting/transaction_spec.rb'
- 'spec/lib/gitlab/usage_data_counters/kubernetes_agent_counter_spec.rb'
- - 'spec/migrations/20220321234317_remove_all_issuable_escalation_statuses_spec.rb'
- - 'spec/migrations/20220506154054_create_sync_namespace_details_trigger_spec.rb'
- - 'spec/migrations/20220512190659_remove_web_hooks_web_hook_logs_web_hook_id_fk_spec.rb'
- - 'spec/migrations/20220524184149_create_sync_project_namespace_details_trigger_spec.rb'
- - 'spec/migrations/20220802204737_remove_deactivated_user_highest_role_stats_spec.rb'
- - 'spec/migrations/20220913030624_cleanup_attention_request_related_system_notes_spec.rb'
- 'spec/migrations/20221018050323_add_objective_and_keyresult_to_work_item_types_spec.rb'
- 'spec/migrations/20221018193635_ensure_task_note_renaming_background_migration_finished_spec.rb'
- 'spec/migrations/20221102090940_create_next_ci_partitions_record_spec.rb'
- - 'spec/migrations/cleanup_mr_attention_request_todos_spec.rb'
- 'spec/migrations/cleanup_vulnerability_state_transitions_with_same_from_state_to_state_spec.rb'
- 'spec/migrations/delete_migrate_shared_vulnerability_scanners_spec.rb'
- 'spec/models/ci/build_metadata_spec.rb'
diff --git a/.rubocop_todo/rspec/expect_in_hook.yml b/.rubocop_todo/rspec/expect_in_hook.yml
index 58ddd11c934..aa150006d08 100644
--- a/.rubocop_todo/rspec/expect_in_hook.yml
+++ b/.rubocop_todo/rspec/expect_in_hook.yml
@@ -65,7 +65,6 @@ RSpec/ExpectInHook:
- 'ee/spec/services/ee/issues/update_service_spec.rb'
- 'ee/spec/services/ee/protected_branches/destroy_service_spec.rb'
- 'ee/spec/services/geo/blob_download_service_spec.rb'
- - 'ee/spec/services/geo/project_housekeeping_service_spec.rb'
- 'ee/spec/services/geo/registry_consistency_service_spec.rb'
- 'ee/spec/services/gitlab_subscriptions/fetch_subscription_plans_service_spec.rb'
- 'ee/spec/services/gitlab_subscriptions/plan_upgrade_service_spec.rb'
@@ -118,7 +117,6 @@ RSpec/ExpectInHook:
- 'spec/controllers/projects/merge_requests/content_controller_spec.rb'
- 'spec/controllers/projects/merge_requests_controller_spec.rb'
- 'spec/controllers/projects/notes_controller_spec.rb'
- - 'spec/controllers/projects/prometheus/alerts_controller_spec.rb'
- 'spec/controllers/projects/settings/ci_cd_controller_spec.rb'
- 'spec/controllers/projects/settings/operations_controller_spec.rb'
- 'spec/controllers/projects/tree_controller_spec.rb'
@@ -136,7 +134,6 @@ RSpec/ExpectInHook:
- 'spec/features/groups/container_registry_spec.rb'
- 'spec/features/groups/group_settings_spec.rb'
- 'spec/features/markdown/markdown_spec.rb'
- - 'spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb'
- 'spec/features/merge_request/user_sees_versions_spec.rb'
- 'spec/features/oauth_login_spec.rb'
- 'spec/features/profiles/password_spec.rb'
@@ -172,7 +169,6 @@ RSpec/ExpectInHook:
- 'spec/lib/gitlab/auth_spec.rb'
- 'spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb'
- 'spec/lib/gitlab/background_migration/legacy_upload_mover_spec.rb'
- - 'spec/lib/gitlab/bitbucket_server_import/importer_spec.rb'
- 'spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb'
- 'spec/lib/gitlab/checks/changes_access_spec.rb'
- 'spec/lib/gitlab/checks/matching_merge_request_spec.rb'
@@ -196,7 +192,6 @@ RSpec/ExpectInHook:
- 'spec/lib/gitlab/daemon_spec.rb'
- 'spec/lib/gitlab/database/background_migration/batched_migration_runner_spec.rb'
- 'spec/lib/gitlab/database/background_migration/batched_migration_spec.rb'
- - 'spec/lib/gitlab/database/background_migration/health_status_spec.rb'
- 'spec/lib/gitlab/database/load_balancing/host_spec.rb'
- 'spec/lib/gitlab/database/load_balancing/load_balancer_spec.rb'
- 'spec/lib/gitlab/database/migration_helpers_spec.rb'
@@ -209,8 +204,6 @@ RSpec/ExpectInHook:
- 'spec/lib/gitlab/database/query_analyzer_spec.rb'
- 'spec/lib/gitlab/database/with_lock_retries_outside_transaction_spec.rb'
- 'spec/lib/gitlab/database/with_lock_retries_spec.rb'
- - 'spec/lib/gitlab/database_importers/common_metrics/importer_spec.rb'
- - 'spec/lib/gitlab/database_importers/instance_administrators/create_group_spec.rb'
- 'spec/lib/gitlab/diff/highlight_cache_spec.rb'
- 'spec/lib/gitlab/email/service_desk_receiver_spec.rb'
- 'spec/lib/gitlab/faraday/error_callback_spec.rb'
@@ -342,7 +335,6 @@ RSpec/ExpectInHook:
- 'spec/requests/api/project_debian_distributions_spec.rb'
- 'spec/requests/api/project_packages_spec.rb'
- 'spec/requests/api/projects_spec.rb'
- - 'spec/requests/api/v3/github_spec.rb'
- 'spec/requests/health_controller_spec.rb'
- 'spec/requests/import/gitlab_groups_controller_spec.rb'
- 'spec/requests/openid_connect_spec.rb'
@@ -399,7 +391,6 @@ RSpec/ExpectInHook:
- 'spec/services/notification_recipients/builder/default_spec.rb'
- 'spec/services/notification_recipients/builder/new_note_spec.rb'
- 'spec/services/packages/cleanup/execute_policy_service_spec.rb'
- - 'spec/services/packages/debian/process_changes_service_spec.rb'
- 'spec/services/packages/generic/create_package_file_service_spec.rb'
- 'spec/services/packages/helm/extract_file_metadata_service_spec.rb'
- 'spec/services/packages/helm/process_file_service_spec.rb'
diff --git a/.rubocop_todo/rspec/factory_bot/avoid_create.yml b/.rubocop_todo/rspec/factory_bot/avoid_create.yml
index 79e7b6027e1..2243f7e61a8 100644
--- a/.rubocop_todo/rspec/factory_bot/avoid_create.yml
+++ b/.rubocop_todo/rspec/factory_bot/avoid_create.yml
@@ -3,7 +3,6 @@ RSpec/FactoryBot/AvoidCreate:
Exclude:
- 'ee/spec/components/namespaces/free_user_cap/enforcement_alert_component_spec.rb'
- 'ee/spec/components/namespaces/free_user_cap/non_owner_enforcement_alert_component_spec.rb'
- - 'ee/spec/components/namespaces/free_user_cap/notification_alert_component_spec.rb'
- 'ee/spec/components/namespaces/free_user_cap/usage_quota_alert_component_spec.rb'
- 'ee/spec/components/namespaces/free_user_cap/usage_quota_trial_alert_component_spec.rb'
- 'ee/spec/components/namespaces/storage/pre_enforcement_alert_component_spec.rb'
@@ -92,7 +91,6 @@ RSpec/FactoryBot/AvoidCreate:
- 'ee/spec/mailers/devise_mailer_spec.rb'
- 'ee/spec/mailers/ee/emails/admin_notification_spec.rb'
- 'ee/spec/mailers/ee/emails/issues_spec.rb'
- - 'ee/spec/mailers/ee/emails/merge_requests_spec.rb'
- 'ee/spec/mailers/ee/emails/profile_spec.rb'
- 'ee/spec/mailers/ee/emails/projects_spec.rb'
- 'ee/spec/mailers/emails/group_memberships_spec.rb'
@@ -158,7 +156,6 @@ RSpec/FactoryBot/AvoidCreate:
- 'ee/spec/serializers/integrations/jira_serializers/issue_entity_spec.rb'
- 'ee/spec/serializers/integrations/jira_serializers/issue_serializer_spec.rb'
- 'ee/spec/serializers/integrations/zentao_serializers/issue_entity_spec.rb'
- - 'ee/spec/serializers/issuable_sidebar_extras_entity_spec.rb'
- 'ee/spec/serializers/issue_serializer_spec.rb'
- 'ee/spec/serializers/issues/linked_issue_feature_flag_entity_spec.rb'
- 'ee/spec/serializers/license_compliance/collapsed_comparer_entity_spec.rb'
@@ -169,7 +166,6 @@ RSpec/FactoryBot/AvoidCreate:
- 'ee/spec/serializers/member_entity_spec.rb'
- 'ee/spec/serializers/member_user_entity_spec.rb'
- 'ee/spec/serializers/merge_request_poll_widget_entity_spec.rb'
- - 'ee/spec/serializers/merge_request_sidebar_basic_entity_spec.rb'
- 'ee/spec/serializers/merge_request_widget_entity_spec.rb'
- 'ee/spec/serializers/pipeline_serializer_spec.rb'
- 'ee/spec/serializers/productivity_analytics_merge_request_entity_spec.rb'
@@ -215,7 +211,6 @@ RSpec/FactoryBot/AvoidCreate:
- 'ee/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb'
- 'ee/spec/views/layouts/project.html.haml_spec.rb'
- 'ee/spec/views/projects/edit.html.haml_spec.rb'
- - 'ee/spec/views/projects/issues/show.html.haml_spec.rb'
- 'ee/spec/views/projects/on_demand_scans/index.html.haml_spec.rb'
- 'ee/spec/views/projects/security/corpus_management/show.html.haml_spec.rb'
- 'ee/spec/views/projects/security/dast_profiles/show.html.haml_spec.rb'
@@ -615,7 +610,6 @@ RSpec/FactoryBot/AvoidCreate:
- 'spec/views/projects/jobs/_build.html.haml_spec.rb'
- 'spec/views/projects/jobs/_generic_commit_status.html.haml_spec.rb'
- 'spec/views/projects/jobs/show.html.haml_spec.rb'
- - 'spec/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml_spec.rb'
- 'spec/views/projects/merge_requests/_commits.html.haml_spec.rb'
- 'spec/views/projects/merge_requests/creations/_new_submit.html.haml_spec.rb'
- 'spec/views/projects/merge_requests/edit.html.haml_spec.rb'
@@ -623,7 +617,6 @@ RSpec/FactoryBot/AvoidCreate:
- 'spec/views/projects/pages/new.html.haml_spec.rb'
- 'spec/views/projects/pages/show.html.haml_spec.rb'
- 'spec/views/projects/pages_domains/show.html.haml_spec.rb'
- - 'spec/views/projects/pipeline_schedules/_pipeline_schedule.html.haml_spec.rb'
- 'spec/views/projects/pipelines/show.html.haml_spec.rb'
- 'spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb'
- 'spec/views/projects/settings/integrations/edit.html.haml_spec.rb'
@@ -631,14 +624,12 @@ RSpec/FactoryBot/AvoidCreate:
- 'spec/views/projects/settings/operations/show.html.haml_spec.rb'
- 'spec/views/projects/tags/index.html.haml_spec.rb'
- 'spec/views/projects/tree/show.html.haml_spec.rb'
- - 'spec/views/registrations/welcome/show.html.haml_spec.rb'
- 'spec/views/search/_results.html.haml_spec.rb'
- 'spec/views/shared/_label_row.html.haml_spec.rb'
- 'spec/views/shared/issuable/_sidebar.html.haml_spec.rb'
- 'spec/views/shared/milestones/_issuable.html.haml_spec.rb'
- 'spec/views/shared/milestones/_top.html.haml_spec.rb'
- 'spec/views/shared/nav/_sidebar.html.haml_spec.rb'
- - 'spec/views/shared/notes/_form.html.haml_spec.rb'
- 'spec/views/shared/projects/_inactive_project_deletion_alert.html.haml_spec.rb'
- 'spec/views/shared/projects/_list.html.haml_spec.rb'
- 'spec/views/shared/projects/_project.html.haml_spec.rb'
diff --git a/.rubocop_todo/rspec/factory_bot/excessive_create_list.yml b/.rubocop_todo/rspec/factory_bot/excessive_create_list.yml
index 386c7317f3c..0bbf59cc244 100644
--- a/.rubocop_todo/rspec/factory_bot/excessive_create_list.yml
+++ b/.rubocop_todo/rspec/factory_bot/excessive_create_list.yml
@@ -7,11 +7,8 @@ RSpec/FactoryBot/ExcessiveCreateList:
- 'ee/spec/models/audit_events/instance_external_audit_event_destination_spec.rb'
- 'ee/spec/models/license_spec.rb'
- 'ee/spec/models/package_metadata/advisory_spec.rb'
- - 'ee/spec/models/package_metadata/checkpoint_spec.rb'
- 'ee/spec/requests/projects/merge_requests_controller_spec.rb'
- 'ee/spec/services/ci/llm/generate_config_service_spec.rb'
- - 'ee/spec/support/protected_tags/access_control_shared_examples.rb'
- - 'ee/spec/support/shared_examples/features/protected_branches_access_control_shared_examples.rb'
- 'ee/spec/views/admin/application_settings/_elasticsearch_form.html.haml_spec.rb'
- 'spec/controllers/autocomplete_controller_spec.rb'
- 'spec/controllers/explore/projects_controller_spec.rb'
@@ -26,7 +23,6 @@ RSpec/FactoryBot/ExcessiveCreateList:
- 'spec/features/projects/work_items/work_item_spec.rb'
- 'spec/features/users/overview_spec.rb'
- 'spec/frontend/fixtures/timelogs.rb'
- - 'spec/helpers/issuables_helper_spec.rb'
- 'spec/lib/gitlab/database/background_migration/batched_migration_spec.rb'
- 'spec/lib/gitlab/database/consistency_checker_spec.rb'
- 'spec/models/project_spec.rb'
diff --git a/.rubocop_todo/rspec/feature_category.yml b/.rubocop_todo/rspec/feature_category.yml
index a8db7b1fa09..48e0329054a 100644
--- a/.rubocop_todo/rspec/feature_category.yml
+++ b/.rubocop_todo/rspec/feature_category.yml
@@ -30,7 +30,6 @@ RSpec/FeatureCategory:
- 'ee/spec/elastic/migrate/20210825110300_backfill_namespace_ancestry_for_issues_spec.rb'
- 'ee/spec/elastic/migrate/20210910094600_add_namespace_ancestry_ids_to_issues_mapping_spec.rb'
- 'ee/spec/elastic/migrate/20210910100000_redo_backfill_namespace_ancestry_ids_for_issues_spec.rb'
- - 'ee/spec/elastic_integration/repository_index_spec.rb'
- 'ee/spec/features/admin/admin_emails_spec.rb'
- 'ee/spec/features/admin/admin_settings_spec.rb'
- 'ee/spec/features/promotion_spec.rb'
@@ -57,7 +56,6 @@ RSpec/FeatureCategory:
- 'ee/spec/finders/dast_site_profiles_finder_spec.rb'
- 'ee/spec/finders/dast_site_validations_finder_spec.rb'
- 'ee/spec/finders/ee/alert_management/http_integrations_finder_spec.rb'
- - 'ee/spec/finders/ee/autocomplete/users_finder_spec.rb'
- 'ee/spec/finders/ee/ci/daily_build_group_report_results_finder_spec.rb'
- 'ee/spec/finders/ee/clusters/agents_finder_spec.rb'
- 'ee/spec/finders/ee/fork_targets_finder_spec.rb'
@@ -88,10 +86,6 @@ RSpec/FeatureCategory:
- 'ee/spec/finders/projects/integrations/jira/by_ids_finder_spec.rb'
- 'ee/spec/finders/projects/integrations/jira/issues_finder_spec.rb'
- 'ee/spec/finders/scim_finder_spec.rb'
- - 'ee/spec/finders/security/training_providers/base_url_finder_spec.rb'
- - 'ee/spec/finders/security/training_providers/kontra_url_finder_spec.rb'
- - 'ee/spec/finders/security/training_providers/secure_code_warrior_url_finder_spec.rb'
- - 'ee/spec/finders/security/training_urls_finder_spec.rb'
- 'ee/spec/finders/security/vulnerabilities_finder_spec.rb'
- 'ee/spec/finders/security/vulnerability_feedbacks_finder_spec.rb'
- 'ee/spec/finders/security/vulnerability_reads_finder_spec.rb'
@@ -140,7 +134,6 @@ RSpec/FeatureCategory:
- 'ee/spec/graphql/ee/types/board_type_spec.rb'
- 'ee/spec/graphql/ee/types/boards/board_issue_input_type_spec.rb'
- 'ee/spec/graphql/ee/types/ci/pipeline_merge_request_type_enum_spec.rb'
- - 'ee/spec/graphql/ee/types/clusters/agent_type_spec.rb'
- 'ee/spec/graphql/ee/types/compliance_management/compliance_framework_type_spec.rb'
- 'ee/spec/graphql/ee/types/environment_type_spec.rb'
- 'ee/spec/graphql/ee/types/group_type_spec.rb'
@@ -150,7 +143,6 @@ RSpec/FeatureCategory:
- 'ee/spec/graphql/ee/types/milestone_type_spec.rb'
- 'ee/spec/graphql/ee/types/mutation_type_spec.rb'
- 'ee/spec/graphql/ee/types/notes/noteable_interface_spec.rb'
- - 'ee/spec/graphql/ee/types/projects/service_type_enum_spec.rb'
- 'ee/spec/graphql/ee/types/repository/blob_type_spec.rb'
- 'ee/spec/graphql/ee/types/todoable_interface_spec.rb'
- 'ee/spec/graphql/ee/types/user_merge_request_interaction_type_spec.rb'
@@ -216,7 +208,6 @@ RSpec/FeatureCategory:
- 'ee/spec/graphql/mutations/security/ci_configuration/configure_container_scanning_spec.rb'
- 'ee/spec/graphql/mutations/security/ci_configuration/configure_dependency_scanning_spec.rb'
- 'ee/spec/graphql/mutations/security/finding/dismiss_spec.rb'
- - 'ee/spec/graphql/mutations/security/training_provider_update_spec.rb'
- 'ee/spec/graphql/mutations/security_policy/assign_security_policy_project_spec.rb'
- 'ee/spec/graphql/mutations/security_policy/commit_scan_execution_policy_spec.rb'
- 'ee/spec/graphql/mutations/security_policy/create_security_policy_project_spec.rb'
@@ -264,7 +255,6 @@ RSpec/FeatureCategory:
- 'ee/spec/graphql/resolvers/security_orchestration/scan_execution_policy_resolver_spec.rb'
- 'ee/spec/graphql/resolvers/security_orchestration/scan_result_policy_resolver_spec.rb'
- 'ee/spec/graphql/resolvers/security_report_summary_resolver_spec.rb'
- - 'ee/spec/graphql/resolvers/security_training_urls_resolver_spec.rb'
- 'ee/spec/graphql/resolvers/timebox_report_resolver_spec.rb'
- 'ee/spec/graphql/resolvers/user_discussions_count_resolver_spec.rb'
- 'ee/spec/graphql/resolvers/user_notes_count_resolver_spec.rb'
@@ -542,7 +532,6 @@ RSpec/FeatureCategory:
- 'ee/spec/lib/audit/external_status_check_changes_auditor_spec.rb'
- 'ee/spec/lib/audit/group_changes_auditor_spec.rb'
- 'ee/spec/lib/audit/group_merge_request_approval_setting_changes_auditor_spec.rb'
- - 'ee/spec/lib/audit/group_push_rules_changes_auditor_spec.rb'
- 'ee/spec/lib/banzai/filter/cross_project_issuable_information_filter_spec.rb'
- 'ee/spec/lib/banzai/filter/jira_private_image_link_filter_spec.rb'
- 'ee/spec/lib/banzai/filter/references/epic_reference_filter_spec.rb'
@@ -617,7 +606,6 @@ RSpec/FeatureCategory:
- 'ee/spec/lib/ee/gitlab/background_migration/backfill_epic_cache_counts_spec.rb'
- 'ee/spec/lib/ee/gitlab/background_migration/backfill_iteration_cadence_id_for_boards_spec.rb'
- 'ee/spec/lib/ee/gitlab/background_migration/backfill_project_statistics_container_repository_size_spec.rb'
- - 'ee/spec/lib/ee/gitlab/background_migration/create_security_setting_spec.rb'
- 'ee/spec/lib/ee/gitlab/background_migration/delete_approval_rules_with_vulnerability_spec.rb'
- 'ee/spec/lib/ee/gitlab/background_migration/delete_invalid_epic_issues_spec.rb'
- 'ee/spec/lib/ee/gitlab/background_migration/migrate_approver_to_approval_rules_check_progress_spec.rb'
@@ -626,8 +614,6 @@ RSpec/FeatureCategory:
- 'ee/spec/lib/ee/gitlab/background_migration/migrate_shared_vulnerability_scanners_spec.rb'
- 'ee/spec/lib/ee/gitlab/background_migration/populate_approval_merge_request_rules_with_security_orchestration_spec.rb'
- 'ee/spec/lib/ee/gitlab/background_migration/populate_approval_project_rules_with_security_orchestration_spec.rb'
- - 'ee/spec/lib/ee/gitlab/background_migration/populate_latest_pipeline_ids_spec.rb'
- - 'ee/spec/lib/ee/gitlab/background_migration/populate_resolved_on_default_branch_column_spec.rb'
- 'ee/spec/lib/ee/gitlab/background_migration/purge_stale_security_scans_spec.rb'
- 'ee/spec/lib/ee/gitlab/checks/push_rules/branch_check_spec.rb'
- 'ee/spec/lib/ee/gitlab/checks/push_rules/commit_check_spec.rb'
@@ -647,7 +633,6 @@ RSpec/FeatureCategory:
- 'ee/spec/lib/ee/gitlab/ci/status/build/manual_spec.rb'
- 'ee/spec/lib/ee/gitlab/ci/templates/templates_spec.rb'
- 'ee/spec/lib/ee/gitlab/cleanup/orphan_job_artifact_files_spec.rb'
- - 'ee/spec/lib/ee/gitlab/database/gitlab_schema_spec.rb'
- 'ee/spec/lib/ee/gitlab/database_spec.rb'
- 'ee/spec/lib/ee/gitlab/email/handler/service_desk_handler_spec.rb'
- 'ee/spec/lib/ee/gitlab/event_store_spec.rb'
@@ -671,7 +656,6 @@ RSpec/FeatureCategory:
- 'ee/spec/lib/ee/gitlab/namespace_storage_size_error_message_spec.rb'
- 'ee/spec/lib/ee/gitlab/omniauth_initializer_spec.rb'
- 'ee/spec/lib/ee/gitlab/pages/deployment_update_spec.rb'
- - 'ee/spec/lib/ee/gitlab/prometheus/metric_group_spec.rb'
- 'ee/spec/lib/ee/gitlab/rack_attack/request_spec.rb'
- 'ee/spec/lib/ee/gitlab/repo_path_spec.rb'
- 'ee/spec/lib/ee/gitlab/repository_size_checker_spec.rb'
@@ -878,9 +862,6 @@ RSpec/FeatureCategory:
- 'ee/spec/lib/gitlab/patch/draw_route_spec.rb'
- 'ee/spec/lib/gitlab/path_locks_finder_spec.rb'
- 'ee/spec/lib/gitlab/project_template_spec.rb'
- - 'ee/spec/lib/gitlab/prometheus/queries/additional_metrics_deployment_query_spec.rb'
- - 'ee/spec/lib/gitlab/prometheus/queries/additional_metrics_environment_query_spec.rb'
- - 'ee/spec/lib/gitlab/prometheus/queries/cluster_query_spec.rb'
- 'ee/spec/lib/gitlab/proxy_spec.rb'
- 'ee/spec/lib/gitlab/quick_actions/users_extractor_spec.rb'
- 'ee/spec/lib/gitlab/rack_attack_spec.rb'
@@ -966,7 +947,6 @@ RSpec/FeatureCategory:
- 'ee/spec/mailers/devise_mailer_spec.rb'
- 'ee/spec/mailers/ee/emails/identity_verification_spec.rb'
- 'ee/spec/mailers/ee/emails/issues_spec.rb'
- - 'ee/spec/mailers/ee/emails/merge_requests_spec.rb'
- 'ee/spec/mailers/ee/emails/profile_spec.rb'
- 'ee/spec/mailers/ee/emails/projects_spec.rb'
- 'ee/spec/mailers/emails/epics_spec.rb'
@@ -977,7 +957,6 @@ RSpec/FeatureCategory:
- 'ee/spec/mailers/license_mailer_spec.rb'
- 'ee/spec/models/alert_management/alert_payload_field_spec.rb'
- 'ee/spec/models/analytics/cycle_analytics/aggregation_context_spec.rb'
- - 'ee/spec/models/analytics/cycle_analytics/runtime_limiter_spec.rb'
- 'ee/spec/models/analytics/devops_adoption/enabled_namespace_spec.rb'
- 'ee/spec/models/analytics/devops_adoption/snapshot_spec.rb'
- 'ee/spec/models/analytics/issues_analytics_spec.rb'
@@ -1002,7 +981,6 @@ RSpec/FeatureCategory:
- 'ee/spec/models/boards/epic_list_spec.rb'
- 'ee/spec/models/boards/epic_list_user_preference_spec.rb'
- 'ee/spec/models/boards/epic_user_preference_spec.rb'
- - 'ee/spec/models/broadcast_message_spec.rb'
- 'ee/spec/models/ci/daily_build_group_report_result_spec.rb'
- 'ee/spec/models/ci/sources/project_spec.rb'
- 'ee/spec/models/ci/subscriptions/project_spec.rb'
@@ -1169,7 +1147,6 @@ RSpec/FeatureCategory:
- 'ee/spec/models/project_member_spec.rb'
- 'ee/spec/models/project_repository_state_spec.rb'
- 'ee/spec/models/project_security_setting_spec.rb'
- - 'ee/spec/models/project_team_spec.rb'
- 'ee/spec/models/protected_branch/required_code_owners_section_spec.rb'
- 'ee/spec/models/protected_environment_spec.rb'
- 'ee/spec/models/protected_environments/approval_rule_spec.rb'
@@ -1220,7 +1197,6 @@ RSpec/FeatureCategory:
- 'ee/spec/policies/environment_policy_spec.rb'
- 'ee/spec/policies/event_policy_spec.rb'
- 'ee/spec/policies/group_hook_policy_spec.rb'
- - 'ee/spec/policies/identity_provider_policy_spec.rb'
- 'ee/spec/policies/incident_management/oncall_rotation_policy_spec.rb'
- 'ee/spec/policies/incident_management/oncall_schedule_policy_spec.rb'
- 'ee/spec/policies/incident_management/oncall_shift_policy_spec.rb'
@@ -1322,7 +1298,6 @@ RSpec/FeatureCategory:
- 'ee/spec/serializers/integrations/jira_serializers/issue_entity_spec.rb'
- 'ee/spec/serializers/integrations/jira_serializers/issue_serializer_spec.rb'
- 'ee/spec/serializers/integrations/zentao_serializers/issue_entity_spec.rb'
- - 'ee/spec/serializers/issuable_sidebar_extras_entity_spec.rb'
- 'ee/spec/serializers/issue_serializer_spec.rb'
- 'ee/spec/serializers/issues/linked_issue_feature_flag_entity_spec.rb'
- 'ee/spec/serializers/license_compliance/collapsed_comparer_entity_spec.rb'
@@ -1332,7 +1307,6 @@ RSpec/FeatureCategory:
- 'ee/spec/serializers/licenses_list_serializer_spec.rb'
- 'ee/spec/serializers/linked_feature_flag_issue_entity_spec.rb'
- 'ee/spec/serializers/member_user_entity_spec.rb'
- - 'ee/spec/serializers/merge_request_sidebar_basic_entity_spec.rb'
- 'ee/spec/serializers/metrics_report_metric_entity_spec.rb'
- 'ee/spec/serializers/metrics_reports_comparer_entity_spec.rb'
- 'ee/spec/serializers/pipeline_serializer_spec.rb'
@@ -1403,8 +1377,6 @@ RSpec/FeatureCategory:
- 'ee/spec/services/arkose/blocked_users_report_service_spec.rb'
- 'ee/spec/services/arkose/token_verification_service_spec.rb'
- 'ee/spec/services/audit_events/build_service_spec.rb'
- - 'ee/spec/services/audit_events/custom_audit_event_service_spec.rb'
- - 'ee/spec/services/audit_events/impersonation_audit_event_service_spec.rb'
- 'ee/spec/services/audit_events/protected_branch_audit_event_service_spec.rb'
- 'ee/spec/services/audit_events/register_runner_audit_event_service_spec.rb'
- 'ee/spec/services/audit_events/release_artifacts_downloaded_audit_event_service_spec.rb'
@@ -1446,7 +1418,6 @@ RSpec/FeatureCategory:
- 'ee/spec/services/ee/notes/quick_actions_service_spec.rb'
- 'ee/spec/services/ee/notes/update_service_spec.rb'
- 'ee/spec/services/external_status_checks/create_service_spec.rb'
- - 'ee/spec/services/projects/after_rename_service_spec.rb'
- 'ee/spec/services/projects/alerting/notify_service_spec.rb'
- 'ee/spec/services/projects/cleanup_service_spec.rb'
- 'ee/spec/services/projects/disable_deploy_key_service_spec.rb'
@@ -1457,13 +1428,11 @@ RSpec/FeatureCategory:
- 'ee/spec/services/projects/group_links/destroy_service_spec.rb'
- 'ee/spec/services/projects/group_links/update_service_spec.rb'
- 'ee/spec/services/projects/hashed_storage/migrate_attachments_service_spec.rb'
- - 'ee/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb'
- 'ee/spec/services/projects/import_export/export_service_spec.rb'
- 'ee/spec/services/projects/import_service_spec.rb'
- 'ee/spec/services/projects/mark_for_deletion_service_spec.rb'
- 'ee/spec/services/projects/open_issues_count_service_spec.rb'
- 'ee/spec/services/projects/operations/update_service_spec.rb'
- - 'ee/spec/services/projects/prometheus/alerts/notify_service_spec.rb'
- 'ee/spec/services/projects/protect_default_branch_service_spec.rb'
- 'ee/spec/services/projects/restore_service_spec.rb'
- 'ee/spec/services/projects/setup_ci_cd_spec.rb'
@@ -1519,7 +1488,6 @@ RSpec/FeatureCategory:
- 'ee/spec/views/operations/index.html.haml_spec.rb'
- 'ee/spec/views/profiles/preferences/show.html.haml_spec.rb'
- 'ee/spec/views/projects/edit.html.haml_spec.rb'
- - 'ee/spec/views/projects/issues/show.html.haml_spec.rb'
- 'ee/spec/views/projects/security/corpus_management/show.html.haml_spec.rb'
- 'ee/spec/views/projects/security/dast_profiles/show.html.haml_spec.rb'
- 'ee/spec/views/projects/security/dast_scanner_profiles/edit.html.haml_spec.rb'
@@ -1620,7 +1588,6 @@ RSpec/FeatureCategory:
- 'spec/controllers/concerns/internal_redirect_spec.rb'
- 'spec/controllers/concerns/issuable_actions_spec.rb'
- 'spec/controllers/concerns/issuable_collections_spec.rb'
- - 'spec/controllers/concerns/metrics_dashboard_spec.rb'
- 'spec/controllers/concerns/page_limiter_spec.rb'
- 'spec/controllers/concerns/preferred_language_switcher_spec.rb'
- 'spec/controllers/concerns/project_unauthorized_spec.rb'
@@ -1702,8 +1669,6 @@ RSpec/FeatureCategory:
- 'spec/controllers/projects/deployments_controller_spec.rb'
- 'spec/controllers/projects/design_management/designs/raw_images_controller_spec.rb'
- 'spec/controllers/projects/discussions_controller_spec.rb'
- - 'spec/controllers/projects/environments/prometheus_api_controller_spec.rb'
- - 'spec/controllers/projects/environments/sample_metrics_controller_spec.rb'
- 'spec/controllers/projects/error_tracking/projects_controller_spec.rb'
- 'spec/controllers/projects/error_tracking/stack_traces_controller_spec.rb'
- 'spec/controllers/projects/error_tracking_controller_spec.rb'
@@ -1722,13 +1687,10 @@ RSpec/FeatureCategory:
- 'spec/controllers/projects/mirrors_controller_spec.rb'
- 'spec/controllers/projects/packages/infrastructure_registry_controller_spec.rb'
- 'spec/controllers/projects/packages/packages_controller_spec.rb'
- - 'spec/controllers/projects/performance_monitoring/dashboards_controller_spec.rb'
- 'spec/controllers/projects/pipelines/stages_controller_spec.rb'
- 'spec/controllers/projects/pipelines/tests_controller_spec.rb'
- 'spec/controllers/projects/pipelines_settings_controller_spec.rb'
- 'spec/controllers/projects/project_members_controller_spec.rb'
- - 'spec/controllers/projects/prometheus/alerts_controller_spec.rb'
- - 'spec/controllers/projects/prometheus/metrics_controller_spec.rb'
- 'spec/controllers/projects/protected_branches_controller_spec.rb'
- 'spec/controllers/projects/protected_tags_controller_spec.rb'
- 'spec/controllers/projects/registry/tags_controller_spec.rb'
@@ -1763,15 +1725,12 @@ RSpec/FeatureCategory:
- 'spec/controllers/users/terms_controller_spec.rb'
- 'spec/controllers/users/unsubscribes_controller_spec.rb'
- 'spec/db/development/add_security_training_providers_spec.rb'
- - 'spec/db/development/import_common_metrics_spec.rb'
- 'spec/db/production/add_security_training_providers_spec.rb'
- - 'spec/db/production/import_common_metrics_spec.rb'
- 'spec/db/production/settings_spec.rb'
- 'spec/dependencies/omniauth_saml_spec.rb'
- 'spec/docs_screenshots/container_registry_docs.rb'
- 'spec/docs_screenshots/wiki_docs.rb'
- 'spec/experiments/in_product_guidance_environments_webide_experiment_spec.rb'
- - 'spec/experiments/ios_specific_templates_experiment_spec.rb'
- 'spec/features/admin/dashboard_spec.rb'
- 'spec/features/groups/integrations/group_integrations_spec.rb'
- 'spec/features/milestones/user_views_milestones_spec.rb'
@@ -1857,7 +1816,6 @@ RSpec/FeatureCategory:
- 'spec/finders/merge_request_target_project_finder_spec.rb'
- 'spec/finders/merge_requests/by_approvals_finder_spec.rb'
- 'spec/finders/merge_requests/oldest_per_commit_finder_spec.rb'
- - 'spec/finders/metrics/users_starred_dashboards_finder_spec.rb'
- 'spec/finders/milestones_finder_spec.rb'
- 'spec/finders/namespaces/projects_finder_spec.rb'
- 'spec/finders/notes_finder_spec.rb'
@@ -1883,7 +1841,6 @@ RSpec/FeatureCategory:
- 'spec/finders/packages/pypi/packages_finder_spec.rb'
- 'spec/finders/packages/tags_finder_spec.rb'
- 'spec/finders/pending_todos_finder_spec.rb'
- - 'spec/finders/personal_access_tokens_finder_spec.rb'
- 'spec/finders/personal_projects_finder_spec.rb'
- 'spec/finders/projects/export_job_finder_spec.rb'
- 'spec/finders/projects/groups_finder_spec.rb'
@@ -1920,7 +1877,6 @@ RSpec/FeatureCategory:
- 'spec/finders/users_finder_spec.rb'
- 'spec/finders/users_star_projects_finder_spec.rb'
- 'spec/fixtures/lib/generators/gitlab/usage_metric_generator/sample_metric_test.rb'
- - 'spec/frontend/fixtures/abuse_reports.rb'
- 'spec/frontend/fixtures/admin_users.rb'
- 'spec/frontend/fixtures/analytics.rb'
- 'spec/frontend/fixtures/api_deploy_keys.rb'
@@ -1944,7 +1900,6 @@ RSpec/FeatureCategory:
- 'spec/frontend/fixtures/listbox.rb'
- 'spec/frontend/fixtures/merge_requests.rb'
- 'spec/frontend/fixtures/merge_requests_diffs.rb'
- - 'spec/frontend/fixtures/metrics_dashboard.rb'
- 'spec/frontend/fixtures/namespaces.rb'
- 'spec/frontend/fixtures/pipeline_schedules.rb'
- 'spec/frontend/fixtures/pipelines.rb'
@@ -1956,7 +1911,6 @@ RSpec/FeatureCategory:
- 'spec/frontend/fixtures/search.rb'
- 'spec/frontend/fixtures/sessions.rb'
- 'spec/frontend/fixtures/snippet.rb'
- - 'spec/frontend/fixtures/startup_css.rb'
- 'spec/frontend/fixtures/tabs.rb'
- 'spec/frontend/fixtures/tags.rb'
- 'spec/frontend/fixtures/timezones.rb'
@@ -1975,7 +1929,6 @@ RSpec/FeatureCategory:
- 'spec/graphql/mutations/alert_management/prometheus_integration/reset_token_spec.rb'
- 'spec/graphql/mutations/alert_management/prometheus_integration/update_spec.rb'
- 'spec/graphql/mutations/alert_management/update_alert_status_spec.rb'
- - 'spec/graphql/mutations/base_mutation_spec.rb'
- 'spec/graphql/mutations/boards/issues/issue_move_list_spec.rb'
- 'spec/graphql/mutations/boards/lists/create_spec.rb'
- 'spec/graphql/mutations/boards/lists/update_spec.rb'
@@ -2122,7 +2075,6 @@ RSpec/FeatureCategory:
- 'spec/graphql/resolvers/merge_requests_count_resolver_spec.rb'
- 'spec/graphql/resolvers/merge_requests_resolver_spec.rb'
- 'spec/graphql/resolvers/metadata_resolver_spec.rb'
- - 'spec/graphql/resolvers/metrics/dashboard_resolver_spec.rb'
- 'spec/graphql/resolvers/metrics/dashboards/annotation_resolver_spec.rb'
- 'spec/graphql/resolvers/package_details_resolver_spec.rb'
- 'spec/graphql/resolvers/package_pipelines_resolver_spec.rb'
@@ -2284,7 +2236,6 @@ RSpec/FeatureCategory:
- 'spec/graphql/types/design_management/design_version_event_enum_spec.rb'
- 'spec/graphql/types/design_management/version_type_spec.rb'
- 'spec/graphql/types/design_management_type_spec.rb'
- - 'spec/graphql/types/detployment_tag_type_spec.rb'
- 'spec/graphql/types/diff_refs_type_spec.rb'
- 'spec/graphql/types/duration_type_spec.rb'
- 'spec/graphql/types/environment_type_spec.rb'
@@ -2334,7 +2285,6 @@ RSpec/FeatureCategory:
- 'spec/graphql/types/merge_requests/reviewer_type_spec.rb'
- 'spec/graphql/types/metadata/kas_type_spec.rb'
- 'spec/graphql/types/metadata_type_spec.rb'
- - 'spec/graphql/types/metrics/dashboard_type_spec.rb'
- 'spec/graphql/types/metrics/dashboards/annotation_type_spec.rb'
- 'spec/graphql/types/milestone_stats_type_spec.rb'
- 'spec/graphql/types/milestone_type_spec.rb'
@@ -2501,7 +2451,6 @@ RSpec/FeatureCategory:
- 'spec/helpers/gitlab_script_tag_helper_spec.rb'
- 'spec/helpers/graph_helper_spec.rb'
- 'spec/helpers/groups/group_members_helper_spec.rb'
- - 'spec/helpers/groups/observability_helper_spec.rb'
- 'spec/helpers/groups/settings_helper_spec.rb'
- 'spec/helpers/groups_helper_spec.rb'
- 'spec/helpers/hooks_helper_spec.rb'
@@ -2549,7 +2498,6 @@ RSpec/FeatureCategory:
- 'spec/helpers/routing/pseudonymization_helper_spec.rb'
- 'spec/helpers/rss_helper_spec.rb'
- 'spec/helpers/sessions_helper_spec.rb'
- - 'spec/helpers/sidekiq_helper_spec.rb'
- 'spec/helpers/snippets_helper_spec.rb'
- 'spec/helpers/sorting_helper_spec.rb'
- 'spec/helpers/sourcegraph_helper_spec.rb'
@@ -2576,7 +2524,6 @@ RSpec/FeatureCategory:
- 'spec/helpers/wiki_helper_spec.rb'
- 'spec/helpers/wiki_page_version_helper_spec.rb'
- 'spec/helpers/x509_helper_spec.rb'
- - 'spec/initializers/00_rails_disable_joins_spec.rb'
- 'spec/initializers/0_postgresql_types_spec.rb'
- 'spec/initializers/100_patch_omniauth_oauth2_spec.rb'
- 'spec/initializers/100_patch_omniauth_saml_spec.rb'
@@ -2596,7 +2543,6 @@ RSpec/FeatureCategory:
- 'spec/initializers/forbid_sidekiq_in_transactions_spec.rb'
- 'spec/initializers/global_id_spec.rb'
- 'spec/initializers/google_api_client_spec.rb'
- - 'spec/initializers/hangouts_chat_http_override_spec.rb'
- 'spec/initializers/hashie_mash_permitted_patch_spec.rb'
- 'spec/initializers/lograge_spec.rb'
- 'spec/initializers/mail_encoding_patch_spec.rb'
@@ -2626,7 +2572,6 @@ RSpec/FeatureCategory:
- 'spec/lib/api/entities/application_setting_spec.rb'
- 'spec/lib/api/entities/branch_spec.rb'
- 'spec/lib/api/entities/bulk_import_spec.rb'
- - 'spec/lib/api/entities/bulk_imports/entity_failure_spec.rb'
- 'spec/lib/api/entities/bulk_imports/entity_spec.rb'
- 'spec/lib/api/entities/bulk_imports/export_status_spec.rb'
- 'spec/lib/api/entities/changelog_spec.rb'
@@ -2667,7 +2612,6 @@ RSpec/FeatureCategory:
- 'spec/lib/api/entities/user_spec.rb'
- 'spec/lib/api/entities/wiki_page_spec.rb'
- 'spec/lib/api/every_api_endpoint_spec.rb'
- - 'spec/lib/api/github/entities_spec.rb'
- 'spec/lib/api/helpers/authentication_spec.rb'
- 'spec/lib/api/helpers/caching_spec.rb'
- 'spec/lib/api/helpers/common_helpers_spec.rb'
@@ -2700,12 +2644,7 @@ RSpec/FeatureCategory:
- 'spec/lib/backup/file_backup_error_spec.rb'
- 'spec/lib/backup/files_spec.rb'
- 'spec/lib/backup/task_spec.rb'
- - 'spec/lib/banzai/filter/inline_alert_metrics_filter_spec.rb'
- - 'spec/lib/banzai/filter/inline_cluster_metrics_filter_spec.rb'
- 'spec/lib/banzai/filter/inline_diff_filter_spec.rb'
- - 'spec/lib/banzai/filter/inline_grafana_metrics_filter_spec.rb'
- - 'spec/lib/banzai/filter/inline_metrics_filter_spec.rb'
- - 'spec/lib/banzai/filter/inline_metrics_redactor_filter_spec.rb'
- 'spec/lib/banzai/pipeline/incident_management/timeline_event_pipeline_spec.rb'
- 'spec/lib/bitbucket/collection_spec.rb'
- 'spec/lib/bitbucket/connection_spec.rb'
@@ -2715,7 +2654,6 @@ RSpec/FeatureCategory:
- 'spec/lib/bitbucket/representation/issue_spec.rb'
- 'spec/lib/bitbucket/representation/pull_request_comment_spec.rb'
- 'spec/lib/bitbucket/representation/pull_request_spec.rb'
- - 'spec/lib/bitbucket/representation/repo_spec.rb'
- 'spec/lib/bitbucket/representation/user_spec.rb'
- 'spec/lib/bitbucket_server/client_spec.rb'
- 'spec/lib/bitbucket_server/collection_spec.rb'
@@ -2736,7 +2674,6 @@ RSpec/FeatureCategory:
- 'spec/lib/bulk_imports/common/pipelines/members_pipeline_spec.rb'
- 'spec/lib/bulk_imports/common/rest/get_badges_query_spec.rb'
- 'spec/lib/bulk_imports/common/transformers/prohibited_attributes_transformer_spec.rb'
- - 'spec/lib/bulk_imports/common/transformers/user_reference_transformer_spec.rb'
- 'spec/lib/bulk_imports/file_downloads/filename_fetch_spec.rb'
- 'spec/lib/bulk_imports/file_downloads/validations_spec.rb'
- 'spec/lib/bulk_imports/groups/extractors/subgroups_extractor_spec.rb'
@@ -2755,9 +2692,7 @@ RSpec/FeatureCategory:
- 'spec/lib/bulk_imports/projects/graphql/get_snippet_repository_query_spec.rb'
- 'spec/lib/bulk_imports/projects/pipelines/auto_devops_pipeline_spec.rb'
- 'spec/lib/bulk_imports/projects/pipelines/ci_pipelines_pipeline_spec.rb'
- - 'spec/lib/bulk_imports/projects/pipelines/container_expiration_policy_pipeline_spec.rb'
- 'spec/lib/bulk_imports/projects/pipelines/design_bundle_pipeline_spec.rb'
- - 'spec/lib/bulk_imports/projects/pipelines/external_pull_requests_pipeline_spec.rb'
- 'spec/lib/bulk_imports/projects/pipelines/issues_pipeline_spec.rb'
- 'spec/lib/bulk_imports/projects/pipelines/merge_requests_pipeline_spec.rb'
- 'spec/lib/bulk_imports/projects/pipelines/project_attributes_pipeline_spec.rb'
@@ -2812,7 +2747,6 @@ RSpec/FeatureCategory:
- 'spec/lib/gitlab/alert_management/fingerprint_spec.rb'
- 'spec/lib/gitlab/alert_management/payload/base_spec.rb'
- 'spec/lib/gitlab/alert_management/payload/generic_spec.rb'
- - 'spec/lib/gitlab/alert_management/payload/managed_prometheus_spec.rb'
- 'spec/lib/gitlab/alert_management/payload/prometheus_spec.rb'
- 'spec/lib/gitlab/alert_management/payload_spec.rb'
- 'spec/lib/gitlab/allowable_spec.rb'
@@ -2895,12 +2829,9 @@ RSpec/FeatureCategory:
- 'spec/lib/gitlab/authorized_keys_spec.rb'
- 'spec/lib/gitlab/avatar_cache_spec.rb'
- 'spec/lib/gitlab/background_migration/backfill_cluster_agents_has_vulnerabilities_spec.rb'
- - 'spec/lib/gitlab/background_migration/backfill_draft_status_on_merge_requests_spec.rb'
- - 'spec/lib/gitlab/background_migration/backfill_draft_status_on_merge_requests_with_corrected_regex_spec.rb'
- 'spec/lib/gitlab/background_migration/backfill_imported_issue_search_data_spec.rb'
- 'spec/lib/gitlab/background_migration/backfill_integrations_enable_ssl_verification_spec.rb'
- 'spec/lib/gitlab/background_migration/backfill_internal_on_notes_spec.rb'
- - 'spec/lib/gitlab/background_migration/backfill_issue_search_data_spec.rb'
- 'spec/lib/gitlab/background_migration/backfill_namespace_details_spec.rb'
- 'spec/lib/gitlab/background_migration/backfill_namespace_id_for_project_route_spec.rb'
- 'spec/lib/gitlab/background_migration/backfill_namespace_id_of_vulnerability_reads_spec.rb'
@@ -2910,20 +2841,16 @@ RSpec/FeatureCategory:
- 'spec/lib/gitlab/background_migration/backfill_project_member_namespace_id_spec.rb'
- 'spec/lib/gitlab/background_migration/backfill_project_namespace_details_spec.rb'
- 'spec/lib/gitlab/background_migration/backfill_project_repositories_spec.rb'
- - 'spec/lib/gitlab/background_migration/backfill_project_settings_spec.rb'
- 'spec/lib/gitlab/background_migration/backfill_topics_title_spec.rb'
- 'spec/lib/gitlab/background_migration/backfill_user_details_fields_spec.rb'
- 'spec/lib/gitlab/background_migration/backfill_vulnerability_reads_cluster_agent_spec.rb'
- 'spec/lib/gitlab/background_migration/base_job_spec.rb'
- - 'spec/lib/gitlab/background_migration/batched_migration_job_spec.rb'
- - 'spec/lib/gitlab/background_migration/batching_strategies/backfill_project_namespace_per_group_batching_strategy_spec.rb'
- 'spec/lib/gitlab/background_migration/batching_strategies/backfill_project_statistics_with_container_registry_size_batching_strategy_spec.rb'
- 'spec/lib/gitlab/background_migration/batching_strategies/base_strategy_spec.rb'
- 'spec/lib/gitlab/background_migration/batching_strategies/dismissed_vulnerabilities_strategy_spec.rb'
- 'spec/lib/gitlab/background_migration/batching_strategies/loose_index_scan_batching_strategy_spec.rb'
- 'spec/lib/gitlab/background_migration/batching_strategies/primary_key_batching_strategy_spec.rb'
- 'spec/lib/gitlab/background_migration/batching_strategies/remove_backfilled_job_artifacts_expire_at_batching_strategy_spec.rb'
- - 'spec/lib/gitlab/background_migration/cleanup_draft_data_from_faulty_regex_spec.rb'
- 'spec/lib/gitlab/background_migration/cleanup_orphaned_routes_spec.rb'
- 'spec/lib/gitlab/background_migration/copy_column_using_background_migration_job_spec.rb'
- 'spec/lib/gitlab/background_migration/delete_orphaned_operational_vulnerabilities_spec.rb'
@@ -2938,22 +2865,15 @@ RSpec/FeatureCategory:
- 'spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_one_member_no_repo_projects_spec.rb'
- 'spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_projects_less_than_one_mb_spec.rb'
- 'spec/lib/gitlab/background_migration/expire_o_auth_tokens_spec.rb'
- - 'spec/lib/gitlab/background_migration/fix_duplicate_project_name_and_path_spec.rb'
- 'spec/lib/gitlab/background_migration/job_coordinator_spec.rb'
- 'spec/lib/gitlab/background_migration/legacy_upload_mover_spec.rb'
- 'spec/lib/gitlab/background_migration/legacy_uploads_migrator_spec.rb'
- 'spec/lib/gitlab/background_migration/mailers/unconfirm_mailer_spec.rb'
- - 'spec/lib/gitlab/background_migration/migrate_shimo_confluence_integration_category_spec.rb'
- - 'spec/lib/gitlab/background_migration/populate_container_repository_migration_plan_spec.rb'
- 'spec/lib/gitlab/background_migration/populate_operation_visibility_permissions_from_operations_spec.rb'
- 'spec/lib/gitlab/background_migration/populate_projects_star_count_spec.rb'
- - 'spec/lib/gitlab/background_migration/project_namespaces/backfill_project_namespaces_spec.rb'
- 'spec/lib/gitlab/background_migration/remove_backfilled_job_artifacts_expire_at_spec.rb'
- - 'spec/lib/gitlab/background_migration/remove_occurrence_pipelines_and_duplicate_vulnerabilities_findings_spec.rb'
- 'spec/lib/gitlab/background_migration/remove_self_managed_wiki_notes_spec.rb'
- 'spec/lib/gitlab/background_migration/rename_task_system_note_to_checklist_item_spec.rb'
- - 'spec/lib/gitlab/background_migration/reset_duplicate_ci_runners_token_encrypted_values_on_projects_spec.rb'
- - 'spec/lib/gitlab/background_migration/reset_duplicate_ci_runners_token_values_on_projects_spec.rb'
- 'spec/lib/gitlab/background_migration/reset_too_many_tags_skipped_registry_imports_spec.rb'
- 'spec/lib/gitlab/background_migration/set_correct_vulnerability_state_spec.rb'
- 'spec/lib/gitlab/background_migration/set_legacy_open_source_license_available_for_non_public_projects_spec.rb'
@@ -2986,7 +2906,6 @@ RSpec/FeatureCategory:
- 'spec/lib/gitlab/chat/responder/mattermost_spec.rb'
- 'spec/lib/gitlab/chat/responder/slack_spec.rb'
- 'spec/lib/gitlab/chat_name_token_spec.rb'
- - 'spec/lib/gitlab/chat_spec.rb'
- 'spec/lib/gitlab/checks/branch_check_spec.rb'
- 'spec/lib/gitlab/checks/container_moved_spec.rb'
- 'spec/lib/gitlab/checks/force_push_spec.rb'
@@ -3214,7 +3133,6 @@ RSpec/FeatureCategory:
- 'spec/lib/gitlab/ci/status/build/skipped_spec.rb'
- 'spec/lib/gitlab/ci/status/build/stop_spec.rb'
- 'spec/lib/gitlab/ci/status/build/unschedule_spec.rb'
- - 'spec/lib/gitlab/ci/status/build/waiting_for_approval_spec.rb'
- 'spec/lib/gitlab/ci/status/build/waiting_for_resource_spec.rb'
- 'spec/lib/gitlab/ci/status/canceled_spec.rb'
- 'spec/lib/gitlab/ci/status/core_spec.rb'
@@ -3268,7 +3186,6 @@ RSpec/FeatureCategory:
- 'spec/lib/gitlab/ci/variables/collection/item_spec.rb'
- 'spec/lib/gitlab/ci/variables/collection/sort_spec.rb'
- 'spec/lib/gitlab/ci/variables/helpers_spec.rb'
- - 'spec/lib/gitlab/ci/yaml_processor/dag_spec.rb'
- 'spec/lib/gitlab/ci/yaml_processor/feature_flags_spec.rb'
- 'spec/lib/gitlab/ci_access_spec.rb'
- 'spec/lib/gitlab/class_attributes_spec.rb'
@@ -3301,7 +3218,6 @@ RSpec/FeatureCategory:
- 'spec/lib/gitlab/config/entry/validator_spec.rb'
- 'spec/lib/gitlab/config/entry/validators/nested_array_helpers_spec.rb'
- 'spec/lib/gitlab/config_checker/external_database_checker_spec.rb'
- - 'spec/lib/gitlab/config_checker/puma_rugged_checker_spec.rb'
- 'spec/lib/gitlab/conflict/file_collection_spec.rb'
- 'spec/lib/gitlab/conflict/file_spec.rb'
- 'spec/lib/gitlab/consul/internal_spec.rb'
@@ -3333,7 +3249,6 @@ RSpec/FeatureCategory:
- 'spec/lib/gitlab/database/background_migration/batched_job_transition_log_spec.rb'
- 'spec/lib/gitlab/database/background_migration/batched_migration_runner_spec.rb'
- 'spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb'
- - 'spec/lib/gitlab/database/background_migration/health_status/indicators/write_ahead_log_spec.rb'
- 'spec/lib/gitlab/database/background_migration/prometheus_metrics_spec.rb'
- 'spec/lib/gitlab/database/background_migration_job_spec.rb'
- 'spec/lib/gitlab/database/batch_average_counter_spec.rb'
@@ -3345,7 +3260,6 @@ RSpec/FeatureCategory:
- 'spec/lib/gitlab/database/count/reltuples_count_strategy_spec.rb'
- 'spec/lib/gitlab/database/count/tablesample_count_strategy_spec.rb'
- 'spec/lib/gitlab/database/count_spec.rb'
- - 'spec/lib/gitlab/database/dynamic_model_helpers_spec.rb'
- 'spec/lib/gitlab/database/each_database_spec.rb'
- 'spec/lib/gitlab/database/grant_spec.rb'
- 'spec/lib/gitlab/database/load_balancing/configuration_spec.rb'
@@ -3368,7 +3282,6 @@ RSpec/FeatureCategory:
- 'spec/lib/gitlab/database/migration_spec.rb'
- 'spec/lib/gitlab/database/migrations/background_migration_helpers_spec.rb'
- 'spec/lib/gitlab/database/migrations/base_background_runner_spec.rb'
- - 'spec/lib/gitlab/database/migrations/batched_background_migration_helpers_spec.rb'
- 'spec/lib/gitlab/database/migrations/constraints_helpers_spec.rb'
- 'spec/lib/gitlab/database/migrations/extension_helpers_spec.rb'
- 'spec/lib/gitlab/database/migrations/instrumentation_spec.rb'
@@ -3399,7 +3312,6 @@ RSpec/FeatureCategory:
- 'spec/lib/gitlab/database/postgres_index_spec.rb'
- 'spec/lib/gitlab/database/postgres_partitioned_table_spec.rb'
- 'spec/lib/gitlab/database/postgresql_adapter/dump_schema_versions_mixin_spec.rb'
- - 'spec/lib/gitlab/database/postgresql_adapter/empty_query_ping_spec.rb'
- 'spec/lib/gitlab/database/postgresql_adapter/force_disconnectable_mixin_spec.rb'
- 'spec/lib/gitlab/database/postgresql_adapter/type_map_cache_spec.rb'
- 'spec/lib/gitlab/database/postgresql_database_tasks/load_schema_versions_mixin_spec.rb'
@@ -3410,7 +3322,6 @@ RSpec/FeatureCategory:
- 'spec/lib/gitlab/database/query_analyzers/restrict_allowed_schemas_spec.rb'
- 'spec/lib/gitlab/database/reindexing/grafana_notifier_spec.rb'
- 'spec/lib/gitlab/database/reindexing/reindex_concurrently_spec.rb'
- - 'spec/lib/gitlab/database/rename_reserved_paths_migration/v1_spec.rb'
- 'spec/lib/gitlab/database/schema_cache_with_renamed_table_spec.rb'
- 'spec/lib/gitlab/database/schema_cleaner_spec.rb'
- 'spec/lib/gitlab/database/schema_migrations/context_spec.rb'
@@ -3423,8 +3334,6 @@ RSpec/FeatureCategory:
- 'spec/lib/gitlab/database/type/json_pg_safe_spec.rb'
- 'spec/lib/gitlab/database/type/symbolized_jsonb_spec.rb'
- 'spec/lib/gitlab/database/unidirectional_copy_trigger_spec.rb'
- - 'spec/lib/gitlab/database_importers/common_metrics/importer_spec.rb'
- - 'spec/lib/gitlab/database_importers/common_metrics/prometheus_metric_spec.rb'
- 'spec/lib/gitlab/default_branch_spec.rb'
- 'spec/lib/gitlab/dependency_linker/base_linker_spec.rb'
- 'spec/lib/gitlab/dependency_linker/cargo_toml_linker_spec.rb'
@@ -3626,15 +3535,12 @@ RSpec/FeatureCategory:
- 'spec/lib/gitlab/github_import/importer/note_importer_spec.rb'
- 'spec/lib/gitlab/github_import/importer/protected_branch_importer_spec.rb'
- 'spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb'
- - 'spec/lib/gitlab/github_import/importer/pull_request_merged_by_importer_spec.rb'
- 'spec/lib/gitlab/github_import/importer/pull_requests/review_request_importer_spec.rb'
- - 'spec/lib/gitlab/github_import/importer/pull_requests_merged_by_importer_spec.rb'
- 'spec/lib/gitlab/github_import/importer/repository_importer_spec.rb'
- 'spec/lib/gitlab/github_import/importer/single_endpoint_diff_notes_importer_spec.rb'
- 'spec/lib/gitlab/github_import/importer/single_endpoint_issue_events_importer_spec.rb'
- 'spec/lib/gitlab/github_import/importer/single_endpoint_issue_notes_importer_spec.rb'
- 'spec/lib/gitlab/github_import/importer/single_endpoint_merge_request_notes_importer_spec.rb'
- - 'spec/lib/gitlab/github_import/issuable_finder_spec.rb'
- 'spec/lib/gitlab/github_import/label_finder_spec.rb'
- 'spec/lib/gitlab/github_import/logger_spec.rb'
- 'spec/lib/gitlab/github_import/markdown_text_spec.rb'
@@ -3719,7 +3625,6 @@ RSpec/FeatureCategory:
- 'spec/lib/gitlab/harbor/client_spec.rb'
- 'spec/lib/gitlab/harbor/query_spec.rb'
- 'spec/lib/gitlab/hashed_path_spec.rb'
- - 'spec/lib/gitlab/hashed_storage/migrator_spec.rb'
- 'spec/lib/gitlab/health_checks/db_check_spec.rb'
- 'spec/lib/gitlab/health_checks/gitaly_check_spec.rb'
- 'spec/lib/gitlab/health_checks/master_check_spec.rb'
@@ -3766,7 +3671,6 @@ RSpec/FeatureCategory:
- 'spec/lib/gitlab/import_export/design_repo_restorer_spec.rb'
- 'spec/lib/gitlab/import_export/design_repo_saver_spec.rb'
- 'spec/lib/gitlab/import_export/duration_measuring_spec.rb'
- - 'spec/lib/gitlab/import_export/error_spec.rb'
- 'spec/lib/gitlab/import_export/file_importer_spec.rb'
- 'spec/lib/gitlab/import_export/group/object_builder_spec.rb'
- 'spec/lib/gitlab/import_export/group/relation_factory_spec.rb'
@@ -3776,7 +3680,6 @@ RSpec/FeatureCategory:
- 'spec/lib/gitlab/import_export/import_test_coverage_spec.rb'
- 'spec/lib/gitlab/import_export/importer_spec.rb'
- 'spec/lib/gitlab/import_export/json/ndjson_writer_spec.rb'
- - 'spec/lib/gitlab/import_export/legacy_relation_tree_saver_spec.rb'
- 'spec/lib/gitlab/import_export/lfs_restorer_spec.rb'
- 'spec/lib/gitlab/import_export/lfs_saver_spec.rb'
- 'spec/lib/gitlab/import_export/log_util_spec.rb'
@@ -3820,7 +3723,6 @@ RSpec/FeatureCategory:
- 'spec/lib/gitlab/issuables_count_for_state_spec.rb'
- 'spec/lib/gitlab/issues/rebalancing/state_spec.rb'
- 'spec/lib/gitlab/jira/dvcs_spec.rb'
- - 'spec/lib/gitlab/jira/middleware_spec.rb'
- 'spec/lib/gitlab/jira_import/base_importer_spec.rb'
- 'spec/lib/gitlab/jira_import/handle_labels_service_spec.rb'
- 'spec/lib/gitlab/jira_import/issue_serializer_spec.rb'
@@ -3911,27 +3813,6 @@ RSpec/FeatureCategory:
- 'spec/lib/gitlab/merge_requests/mergeability/redis_interface_spec.rb'
- 'spec/lib/gitlab/merge_requests/mergeability/results_store_spec.rb'
- 'spec/lib/gitlab/metrics/background_transaction_spec.rb'
- - 'spec/lib/gitlab/metrics/dashboard/cache_spec.rb'
- - 'spec/lib/gitlab/metrics/dashboard/defaults_spec.rb'
- - 'spec/lib/gitlab/metrics/dashboard/finder_spec.rb'
- - 'spec/lib/gitlab/metrics/dashboard/importer_spec.rb'
- - 'spec/lib/gitlab/metrics/dashboard/importers/prometheus_metrics_spec.rb'
- - 'spec/lib/gitlab/metrics/dashboard/processor_spec.rb'
- - 'spec/lib/gitlab/metrics/dashboard/repo_dashboard_finder_spec.rb'
- - 'spec/lib/gitlab/metrics/dashboard/service_selector_spec.rb'
- - 'spec/lib/gitlab/metrics/dashboard/stages/grafana_formatter_spec.rb'
- - 'spec/lib/gitlab/metrics/dashboard/stages/metric_endpoint_inserter_spec.rb'
- - 'spec/lib/gitlab/metrics/dashboard/stages/panel_ids_inserter_spec.rb'
- - 'spec/lib/gitlab/metrics/dashboard/stages/track_panel_type_spec.rb'
- - 'spec/lib/gitlab/metrics/dashboard/stages/url_validator_spec.rb'
- - 'spec/lib/gitlab/metrics/dashboard/stages/variable_endpoint_inserter_spec.rb'
- - 'spec/lib/gitlab/metrics/dashboard/transformers/yml/v1/prometheus_metrics_spec.rb'
- - 'spec/lib/gitlab/metrics/dashboard/url_spec.rb'
- - 'spec/lib/gitlab/metrics/dashboard/validator/client_spec.rb'
- - 'spec/lib/gitlab/metrics/dashboard/validator/custom_formats_spec.rb'
- - 'spec/lib/gitlab/metrics/dashboard/validator/errors_spec.rb'
- - 'spec/lib/gitlab/metrics/dashboard/validator/post_schema_validator_spec.rb'
- - 'spec/lib/gitlab/metrics/dashboard/validator_spec.rb'
- 'spec/lib/gitlab/metrics/delta_spec.rb'
- 'spec/lib/gitlab/metrics/elasticsearch_rack_middleware_spec.rb'
- 'spec/lib/gitlab/metrics/exporter/gc_request_middleware_spec.rb'
@@ -4012,7 +3893,6 @@ RSpec/FeatureCategory:
- 'spec/lib/gitlab/pagination/offset_header_builder_with_controller_spec.rb'
- 'spec/lib/gitlab/pagination/offset_pagination_spec.rb'
- 'spec/lib/gitlab/pagination_delegate_spec.rb'
- - 'spec/lib/gitlab/patch/action_cable_redis_listener_spec.rb'
- 'spec/lib/gitlab/patch/database_config_spec.rb'
- 'spec/lib/gitlab/patch/draw_route_spec.rb'
- 'spec/lib/gitlab/patch/prependable_spec.rb'
@@ -4036,15 +3916,7 @@ RSpec/FeatureCategory:
- 'spec/lib/gitlab/project_template_spec.rb'
- 'spec/lib/gitlab/project_transfer_spec.rb'
- 'spec/lib/gitlab/prometheus/adapter_spec.rb'
- - 'spec/lib/gitlab/prometheus/additional_metrics_parser_spec.rb'
- 'spec/lib/gitlab/prometheus/internal_spec.rb'
- - 'spec/lib/gitlab/prometheus/metric_group_spec.rb'
- - 'spec/lib/gitlab/prometheus/queries/additional_metrics_deployment_query_spec.rb'
- - 'spec/lib/gitlab/prometheus/queries/additional_metrics_environment_query_spec.rb'
- - 'spec/lib/gitlab/prometheus/queries/deployment_query_spec.rb'
- - 'spec/lib/gitlab/prometheus/queries/matched_metric_query_spec.rb'
- - 'spec/lib/gitlab/prometheus/queries/validate_query_spec.rb'
- - 'spec/lib/gitlab/prometheus/query_variables_spec.rb'
- 'spec/lib/gitlab/prometheus_client_spec.rb'
- 'spec/lib/gitlab/protocol_access_spec.rb'
- 'spec/lib/gitlab/puma_logging/json_formatter_spec.rb'
@@ -4095,7 +3967,6 @@ RSpec/FeatureCategory:
- 'spec/lib/gitlab/robots_txt/parser_spec.rb'
- 'spec/lib/gitlab/route_map_spec.rb'
- 'spec/lib/gitlab/routing_spec.rb'
- - 'spec/lib/gitlab/rugged_instrumentation_spec.rb'
- 'spec/lib/gitlab/safe_request_loader_spec.rb'
- 'spec/lib/gitlab/safe_request_purger_spec.rb'
- 'spec/lib/gitlab/sample_data_template_spec.rb'
@@ -4210,7 +4081,6 @@ RSpec/FeatureCategory:
- 'spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb'
- 'spec/lib/gitlab/template/issue_template_spec.rb'
- 'spec/lib/gitlab/template/merge_request_template_spec.rb'
- - 'spec/lib/gitlab/template/metrics_dashboard_template_spec.rb'
- 'spec/lib/gitlab/template_parser/ast_spec.rb'
- 'spec/lib/gitlab/template_parser/parser_spec.rb'
- 'spec/lib/gitlab/terraform/state_migration_helper_spec.rb'
@@ -4270,7 +4140,6 @@ RSpec/FeatureCategory:
- 'spec/lib/gitlab/usage/metrics/instrumentations/snowplow_configured_to_gitlab_collector_metric_spec.rb'
- 'spec/lib/gitlab/usage/metrics/instrumentations/snowplow_enabled_metric_spec.rb'
- 'spec/lib/gitlab/usage/metrics/instrumentations/uuid_metric_spec.rb'
- - 'spec/lib/gitlab/usage/metrics/instrumentations/work_items_activity_aggregated_metric_spec.rb'
- 'spec/lib/gitlab/usage/metrics/key_path_processor_spec.rb'
- 'spec/lib/gitlab/usage/metrics/query_spec.rb'
- 'spec/lib/gitlab/usage/service_ping/instrumented_payload_spec.rb'
@@ -4333,7 +4202,6 @@ RSpec/FeatureCategory:
- 'spec/lib/gitlab/verify/job_artifacts_spec.rb'
- 'spec/lib/gitlab/verify/lfs_objects_spec.rb'
- 'spec/lib/gitlab/verify/uploads_spec.rb'
- - 'spec/lib/gitlab/version_info_spec.rb'
- 'spec/lib/gitlab/view/presenter/base_spec.rb'
- 'spec/lib/gitlab/view/presenter/delegated_spec.rb'
- 'spec/lib/gitlab/view/presenter/factory_spec.rb'
@@ -4393,7 +4261,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/peek/views/rugged_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'
@@ -4413,7 +4280,6 @@ RSpec/FeatureCategory:
- 'spec/lib/sidebars/concerns/container_with_html_options_spec.rb'
- 'spec/lib/sidebars/concerns/link_with_html_options_spec.rb'
- 'spec/lib/sidebars/groups/menus/ci_cd_menu_spec.rb'
- - 'spec/lib/sidebars/groups/menus/observability_menu_spec.rb'
- 'spec/lib/sidebars/groups/menus/settings_menu_spec.rb'
- 'spec/lib/sidebars/menu_item_spec.rb'
- 'spec/lib/sidebars/projects/context_spec.rb'
@@ -4492,7 +4358,6 @@ RSpec/FeatureCategory:
- 'spec/models/blob_viewer/go_mod_spec.rb'
- 'spec/models/blob_viewer/license_spec.rb'
- 'spec/models/blob_viewer/markup_spec.rb'
- - 'spec/models/blob_viewer/metrics_dashboard_yml_spec.rb'
- 'spec/models/blob_viewer/podspec_json_spec.rb'
- 'spec/models/blob_viewer/podspec_spec.rb'
- 'spec/models/blob_viewer/readme_spec.rb'
@@ -4501,11 +4366,9 @@ RSpec/FeatureCategory:
- 'spec/models/board_group_recent_visit_spec.rb'
- 'spec/models/board_project_recent_visit_spec.rb'
- 'spec/models/board_spec.rb'
- - 'spec/models/broadcast_message_spec.rb'
- 'spec/models/bulk_imports/configuration_spec.rb'
- 'spec/models/bulk_imports/export_status_spec.rb'
- 'spec/models/bulk_imports/export_upload_spec.rb'
- - 'spec/models/bulk_imports/failure_spec.rb'
- 'spec/models/bulk_imports/file_transfer_spec.rb'
- 'spec/models/bulk_imports/tracker_spec.rb'
- 'spec/models/chat_team_spec.rb'
@@ -4815,8 +4678,6 @@ RSpec/FeatureCategory:
- 'spec/models/merge_request_context_commit_diff_file_spec.rb'
- 'spec/models/merge_request_context_commit_spec.rb'
- 'spec/models/merge_request_reviewer_spec.rb'
- - 'spec/models/metrics/dashboard/annotation_spec.rb'
- - 'spec/models/metrics/users_starred_dashboard_spec.rb'
- 'spec/models/milestone_note_spec.rb'
- 'spec/models/milestone_release_spec.rb'
- 'spec/models/milestone_spec.rb'
@@ -4875,10 +4736,6 @@ RSpec/FeatureCategory:
- 'spec/models/pages/virtual_domain_spec.rb'
- 'spec/models/pages_domain_acme_order_spec.rb'
- 'spec/models/pages_domain_spec.rb'
- - 'spec/models/performance_monitoring/prometheus_dashboard_spec.rb'
- - 'spec/models/performance_monitoring/prometheus_metric_spec.rb'
- - 'spec/models/performance_monitoring/prometheus_panel_group_spec.rb'
- - 'spec/models/performance_monitoring/prometheus_panel_spec.rb'
- 'spec/models/personal_snippet_spec.rb'
- 'spec/models/plan_limits_spec.rb'
- 'spec/models/plan_spec.rb'
@@ -4903,11 +4760,9 @@ RSpec/FeatureCategory:
- 'spec/models/project_custom_attribute_spec.rb'
- 'spec/models/project_daily_statistic_spec.rb'
- 'spec/models/project_deploy_token_spec.rb'
- - 'spec/models/project_feature_usage_spec.rb'
- 'spec/models/project_group_link_spec.rb'
- 'spec/models/project_import_data_spec.rb'
- 'spec/models/project_label_spec.rb'
- - 'spec/models/project_metrics_setting_spec.rb'
- 'spec/models/project_repository_spec.rb'
- 'spec/models/project_snippet_spec.rb'
- 'spec/models/project_statistics_spec.rb'
@@ -5038,7 +4893,6 @@ RSpec/FeatureCategory:
- 'spec/policies/integration_policy_spec.rb'
- 'spec/policies/issuable_policy_spec.rb'
- 'spec/policies/merge_request_policy_spec.rb'
- - 'spec/policies/metrics/dashboard/annotation_policy_spec.rb'
- 'spec/policies/namespace/root_storage_statistics_policy_spec.rb'
- 'spec/policies/namespaces/project_namespace_policy_spec.rb'
- 'spec/policies/packages/package_policy_spec.rb'
@@ -5120,7 +4974,6 @@ RSpec/FeatureCategory:
- 'spec/presenters/web_hook_log_presenter_spec.rb'
- 'spec/rack_servers/puma_spec.rb'
- 'spec/requests/api/graphql/ci/runners_spec.rb'
- - 'spec/requests/api/graphql/mutations/ci/pipeline_schedule_create_spec.rb'
- 'spec/requests/api/graphql/mutations/merge_requests/accept_spec.rb'
- 'spec/requests/api/graphql/mutations/merge_requests/set_labels_spec.rb'
- 'spec/requests/api/resource_state_events_spec.rb'
@@ -5231,7 +5084,6 @@ RSpec/FeatureCategory:
- 'spec/rubocop/cop/migration/schema_addition_methods_no_post_spec.rb'
- 'spec/rubocop/cop/migration/sidekiq_queue_migrate_spec.rb'
- 'spec/rubocop/cop/migration/timestamps_spec.rb'
- - 'spec/rubocop/cop/migration/with_lock_retries_disallowed_method_spec.rb'
- 'spec/rubocop/cop/migration/with_lock_retries_with_change_spec.rb'
- 'spec/rubocop/cop/performance/active_record_subtransaction_methods_spec.rb'
- 'spec/rubocop/cop/performance/active_record_subtransactions_spec.rb'
@@ -5278,7 +5130,6 @@ RSpec/FeatureCategory:
- 'spec/rubocop/migration_helpers_spec.rb'
- 'spec/rubocop/qa_helpers_spec.rb'
- 'spec/rubocop/todo_dir_spec.rb'
- - 'spec/scripts/changed-feature-flags_spec.rb'
- 'spec/scripts/failed_tests_spec.rb'
- 'spec/scripts/lib/glfm/parse_examples_spec.rb'
- 'spec/scripts/lib/glfm/verify_all_generated_files_are_up_to_date_spec.rb'
@@ -5440,7 +5291,6 @@ RSpec/FeatureCategory:
- 'spec/serializers/project_mirror_serializer_spec.rb'
- 'spec/serializers/project_note_entity_spec.rb'
- 'spec/serializers/project_serializer_spec.rb'
- - 'spec/serializers/prometheus_alert_entity_spec.rb'
- 'spec/serializers/release_serializer_spec.rb'
- 'spec/serializers/remote_mirror_entity_spec.rb'
- 'spec/serializers/request_aware_entity_spec.rb'
@@ -5467,7 +5317,6 @@ RSpec/FeatureCategory:
- 'spec/serializers/web_ide_terminal_serializer_spec.rb'
- 'spec/services/applications/create_service_spec.rb'
- 'spec/services/gpg_keys/destroy_service_spec.rb'
- - 'spec/services/metrics/dashboard/grafana_metric_embed_service_spec.rb'
- 'spec/sidekiq/cron/job_gem_dependency_spec.rb'
- 'spec/sidekiq_cluster/sidekiq_cluster_spec.rb'
- 'spec/spam/concerns/has_spam_action_response_fields_spec.rb'
@@ -5495,13 +5344,11 @@ RSpec/FeatureCategory:
- 'spec/tasks/gitlab/container_registry_rake_spec.rb'
- 'spec/tasks/gitlab/dependency_proxy/migrate_rake_spec.rb'
- 'spec/tasks/gitlab/external_diffs_rake_spec.rb'
- - 'spec/tasks/gitlab/generate_sample_prometheus_data_rake_spec.rb'
- 'spec/tasks/gitlab/git_rake_spec.rb'
- 'spec/tasks/gitlab/gitaly_rake_spec.rb'
- 'spec/tasks/gitlab/ldap_rake_spec.rb'
- 'spec/tasks/gitlab/lfs/check_rake_spec.rb'
- 'spec/tasks/gitlab/lfs/migrate_rake_spec.rb'
- - 'spec/tasks/gitlab/packages/events_rake_spec.rb'
- 'spec/tasks/gitlab/packages/migrate_rake_spec.rb'
- 'spec/tasks/gitlab/pages_rake_spec.rb'
- 'spec/tasks/gitlab/password_rake_spec.rb'
@@ -5623,7 +5470,6 @@ RSpec/FeatureCategory:
- 'spec/views/groups/_home_panel.html.haml_spec.rb'
- 'spec/views/groups/milestones/index.html.haml_spec.rb'
- 'spec/views/groups/new.html.haml_spec.rb'
- - 'spec/views/groups/observability/observability.html.haml_spec.rb'
- 'spec/views/groups/settings/_remove.html.haml_spec.rb'
- 'spec/views/help/drawers.html.haml_spec.rb'
- 'spec/views/help/index.html.haml_spec.rb'
@@ -5677,30 +5523,25 @@ RSpec/FeatureCategory:
- 'spec/views/projects/hooks/edit.html.haml_spec.rb'
- 'spec/views/projects/hooks/index.html.haml_spec.rb'
- 'spec/views/projects/imports/new.html.haml_spec.rb'
- - 'spec/views/projects/issues/_issue.html.haml_spec.rb'
- 'spec/views/projects/issues/_related_branches.html.haml_spec.rb'
- 'spec/views/projects/issues/_service_desk_info_content.html.haml_spec.rb'
- 'spec/views/projects/issues/show.html.haml_spec.rb'
- 'spec/views/projects/jobs/_build.html.haml_spec.rb'
- 'spec/views/projects/jobs/_generic_commit_status.html.haml_spec.rb'
- 'spec/views/projects/jobs/show.html.haml_spec.rb'
- - 'spec/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml_spec.rb'
- 'spec/views/projects/merge_requests/_commits.html.haml_spec.rb'
- 'spec/views/projects/merge_requests/edit.html.haml_spec.rb'
- - 'spec/views/projects/merge_requests/show.html.haml_spec.rb'
- 'spec/views/projects/milestones/index.html.haml_spec.rb'
- 'spec/views/projects/notes/_more_actions_dropdown.html.haml_spec.rb'
- 'spec/views/projects/pages/new.html.haml_spec.rb'
- 'spec/views/projects/pages/show.html.haml_spec.rb'
- 'spec/views/projects/pages_domains/show.html.haml_spec.rb'
- - 'spec/views/projects/pipeline_schedules/_pipeline_schedule.html.haml_spec.rb'
- 'spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb'
- 'spec/views/projects/settings/integrations/edit.html.haml_spec.rb'
- 'spec/views/projects/settings/merge_requests/show.html.haml_spec.rb'
- 'spec/views/projects/settings/operations/show.html.haml_spec.rb'
- 'spec/views/projects/tags/index.html.haml_spec.rb'
- 'spec/views/projects/tree/show.html.haml_spec.rb'
- - 'spec/views/registrations/welcome/show.html.haml_spec.rb'
- 'spec/views/shared/_label_row.html.haml_spec.rb'
- 'spec/views/shared/_milestones_sort_dropdown.html.haml_spec.rb'
- 'spec/views/shared/gitlab_version/_security_patch_upgrade_alert.html.haml_spec.rb'
@@ -5710,7 +5551,6 @@ RSpec/FeatureCategory:
- 'spec/views/shared/milestones/_issuables.html.haml_spec.rb'
- 'spec/views/shared/milestones/_top.html.haml_spec.rb'
- 'spec/views/shared/nav/_sidebar.html.haml_spec.rb'
- - 'spec/views/shared/notes/_form.html.haml_spec.rb'
- 'spec/views/shared/projects/_inactive_project_deletion_alert.html.haml_spec.rb'
- 'spec/views/shared/projects/_list.html.haml_spec.rb'
- 'spec/views/shared/projects/_project.html.haml_spec.rb'
diff --git a/.rubocop_todo/rspec/hooks_before_examples.yml b/.rubocop_todo/rspec/hooks_before_examples.yml
index 66b2fc48874..5755001fcf4 100644
--- a/.rubocop_todo/rspec/hooks_before_examples.yml
+++ b/.rubocop_todo/rspec/hooks_before_examples.yml
@@ -14,7 +14,6 @@ RSpec/HooksBeforeExamples:
- 'ee/spec/graphql/types/dast/profile_schedule_type_spec.rb'
- 'ee/spec/graphql/types/dast/profile_type_spec.rb'
- 'ee/spec/lib/ee/gitlab/checks/push_rule_check_spec.rb'
- - 'ee/spec/lib/ee/gitlab/usage_data_counters/hll_redis_counter_spec.rb'
- 'ee/spec/lib/gitlab/analytics/cycle_analytics/summary/group/stage_summary_spec.rb'
- 'ee/spec/models/ee/merge_request_diff_spec.rb'
- 'ee/spec/requests/api/boards_spec.rb'
@@ -24,16 +23,5 @@ RSpec/HooksBeforeExamples:
- 'ee/spec/services/ee/groups/deploy_tokens/revoke_service_spec.rb'
- 'ee/spec/services/ee/projects/deploy_tokens/destroy_service_spec.rb'
- 'ee/spec/services/merge_trains/create_pipeline_service_spec.rb'
- - 'qa/qa/specs/features/api/1_manage/project_access_token_spec.rb'
- - 'qa/qa/specs/features/api/1_manage/user_access_termination_spec.rb'
- - 'qa/qa/specs/features/browser_ui/1_manage/login/log_in_with_2fa_spec.rb'
- - 'qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb'
- - 'qa/qa/specs/features/browser_ui/1_manage/project/project_access_token_spec.rb'
- 'qa/qa/specs/features/browser_ui/3_create/merge_request/revert/revert_commit_spec.rb'
- - 'qa/qa/specs/features/ee/api/1_manage/user/minimal_access_user_spec.rb'
- - 'qa/qa/specs/features/ee/browser_ui/1_manage/group/group_saml_enforced_sso_git_access_spec.rb'
- - 'qa/qa/specs/features/ee/browser_ui/1_manage/group/group_saml_enforced_sso_new_account_spec.rb'
- - 'qa/qa/specs/features/ee/browser_ui/1_manage/group/group_saml_non_enforced_sso_spec.rb'
- - 'qa/qa/specs/features/ee/browser_ui/1_manage/ldap/admin_ldap_sync_spec.rb'
- - 'qa/qa/specs/features/ee/browser_ui/1_manage/user/minimal_access_user_spec.rb'
- 'qa/spec/specs/runner_spec.rb'
diff --git a/.rubocop_todo/rspec/instance_variable.yml b/.rubocop_todo/rspec/instance_variable.yml
index 603d025fa2f..b2aa21d5121 100644
--- a/.rubocop_todo/rspec/instance_variable.yml
+++ b/.rubocop_todo/rspec/instance_variable.yml
@@ -34,20 +34,10 @@ RSpec/InstanceVariable:
- 'ee/spec/views/projects/security/dast_profiles/show.html.haml_spec.rb'
- 'ee/spec/views/projects/security/dast_scanner_profiles/new.html.haml_spec.rb'
- 'ee/spec/views/projects/security/dast_site_profiles/new.html.haml_spec.rb'
- - 'qa/qa/specs/features/api/1_manage/project_access_token_spec.rb'
- - 'qa/qa/specs/features/api/1_manage/user_access_termination_spec.rb'
- - 'qa/qa/specs/features/browser_ui/1_manage/login/log_in_with_2fa_spec.rb'
- - 'qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb'
- 'qa/qa/specs/features/browser_ui/3_create/repository/push_over_ssh_spec.rb'
- 'qa/qa/specs/features/browser_ui/3_create/repository/user_views_commit_diff_patch_spec.rb'
- 'qa/qa/specs/features/browser_ui/3_create/web_ide_old/add_file_template_spec.rb'
- 'qa/qa/specs/features/browser_ui/3_create/web_ide_old/link_to_line_in_web_ide_spec.rb'
- - 'qa/qa/specs/features/ee/api/1_manage/user/minimal_access_user_spec.rb'
- - 'qa/qa/specs/features/ee/browser_ui/13_secure/license_compliance_spec.rb'
- - 'qa/qa/specs/features/ee/browser_ui/13_secure/merge_request_license_widget_spec.rb'
- - 'qa/qa/specs/features/ee/browser_ui/1_manage/group/group_ldap_sync_spec.rb'
- - 'qa/qa/specs/features/ee/browser_ui/1_manage/group/group_saml_non_enforced_sso_spec.rb'
- - 'qa/qa/specs/features/ee/browser_ui/1_manage/group/restrict_by_ip_address_spec.rb'
- 'qa/qa/specs/features/ee/browser_ui/2_plan/issue_boards/project_issue_boards_spec.rb'
- 'qa/qa/specs/features/ee/browser_ui/2_plan/multiple_assignees_for_issues/more_than_four_assignees_spec.rb'
- 'qa/qa/specs/features/ee/browser_ui/3_create/repository/code_owners_spec.rb'
@@ -116,7 +106,6 @@ RSpec/InstanceVariable:
- 'spec/lib/gitlab/reference_extractor_spec.rb'
- 'spec/lib/gitlab/tcp_checker_spec.rb'
- 'spec/lib/gitlab/user_access_spec.rb'
- - 'spec/lib/gitlab/version_info_spec.rb'
- 'spec/lib/gitlab/x509/certificate_spec.rb'
- 'spec/mailers/emails/issues_spec.rb'
- 'spec/models/group_spec.rb'
diff --git a/.rubocop_todo/rspec/multiple_memoized_helpers.yml b/.rubocop_todo/rspec/multiple_memoized_helpers.yml
index ddb9f70c3b4..cb473fcd243 100644
--- a/.rubocop_todo/rspec/multiple_memoized_helpers.yml
+++ b/.rubocop_todo/rspec/multiple_memoized_helpers.yml
@@ -4,7 +4,6 @@ RSpec/MultipleMemoizedHelpers:
- 'ee/spec/features/boards/swimlanes/epics_swimlanes_filtering_spec.rb'
- 'ee/spec/finders/epics_finder_spec.rb'
- 'ee/spec/finders/incident_management/oncall_users_finder_spec.rb'
- - 'ee/spec/lib/ee/gitlab/background_migration/populate_latest_pipeline_ids_spec.rb'
- 'ee/spec/lib/gitlab/graphql/loaders/bulk_epic_aggregate_loader_spec.rb'
- 'ee/spec/requests/api/analytics/project_deployment_frequency_spec.rb'
- 'ee/spec/services/ee/boards/issues/list_service_spec.rb'
diff --git a/.rubocop_todo/rspec/named_subject.yml b/.rubocop_todo/rspec/named_subject.yml
new file mode 100644
index 00000000000..40e6ed52b02
--- /dev/null
+++ b/.rubocop_todo/rspec/named_subject.yml
@@ -0,0 +1,3858 @@
+---
+RSpec/NamedSubject:
+ Details: grace period
+ Exclude:
+ - 'ee/spec/controllers/admin/audit_log_reports_controller_spec.rb'
+ - 'ee/spec/controllers/admin/emails_controller_spec.rb'
+ - 'ee/spec/controllers/admin/geo/settings_controller_spec.rb'
+ - 'ee/spec/controllers/admin/groups_controller_spec.rb'
+ - 'ee/spec/controllers/admin/identities_controller_spec.rb'
+ - 'ee/spec/controllers/admin/projects_controller_spec.rb'
+ - 'ee/spec/controllers/admin/users_controller_spec.rb'
+ - 'ee/spec/controllers/autocomplete_controller_spec.rb'
+ - 'ee/spec/controllers/concerns/ee/routable_actions/sso_enforcement_redirect_spec.rb'
+ - 'ee/spec/controllers/ee/dashboard/projects_controller_spec.rb'
+ - 'ee/spec/controllers/ee/groups/settings/ci_cd_controller_spec.rb'
+ - 'ee/spec/controllers/ee/groups/variables_controller_spec.rb'
+ - 'ee/spec/controllers/ee/groups_controller_spec.rb'
+ - 'ee/spec/controllers/ee/omniauth_callbacks_controller_spec.rb'
+ - 'ee/spec/controllers/ee/profiles/preferences_controller_spec.rb'
+ - 'ee/spec/controllers/ee/registrations_controller_spec.rb'
+ - 'ee/spec/controllers/ee/root_controller_spec.rb'
+ - 'ee/spec/controllers/groups/analytics/cycle_analytics/summary_controller_spec.rb'
+ - 'ee/spec/controllers/groups/analytics/productivity_analytics_controller_spec.rb'
+ - 'ee/spec/controllers/groups/analytics/repository_analytics_controller_spec.rb'
+ - 'ee/spec/controllers/groups/analytics/tasks_by_type_controller_spec.rb'
+ - 'ee/spec/controllers/groups/epic_issues_controller_spec.rb'
+ - 'ee/spec/controllers/groups/epics_controller_spec.rb'
+ - 'ee/spec/controllers/groups/group_members_controller_spec.rb'
+ - 'ee/spec/controllers/groups/groups_controller_spec.rb'
+ - 'ee/spec/controllers/groups/insights_controller_spec.rb'
+ - 'ee/spec/controllers/groups/issues_controller_spec.rb'
+ - 'ee/spec/controllers/groups/iterations_controller_spec.rb'
+ - 'ee/spec/controllers/groups/ldaps_controller_spec.rb'
+ - 'ee/spec/controllers/groups/merge_requests_controller_spec.rb'
+ - 'ee/spec/controllers/groups/saml_providers_controller_spec.rb'
+ - 'ee/spec/controllers/groups/scim_oauth_controller_spec.rb'
+ - 'ee/spec/controllers/groups/security/merge_commit_reports_controller_spec.rb'
+ - 'ee/spec/controllers/groups/security/policies_controller_spec.rb'
+ - 'ee/spec/controllers/groups/sso_controller_spec.rb'
+ - 'ee/spec/controllers/operations_controller_spec.rb'
+ - 'ee/spec/controllers/passwords_controller_spec.rb'
+ - 'ee/spec/controllers/profiles/usage_quotas_controller_spec.rb'
+ - 'ee/spec/controllers/profiles_controller_spec.rb'
+ - 'ee/spec/controllers/projects/analytics/cycle_analytics/summary_controller_spec.rb'
+ - 'ee/spec/controllers/projects/environments_controller_spec.rb'
+ - 'ee/spec/controllers/projects/issues_controller_spec.rb'
+ - 'ee/spec/controllers/projects/iterations_controller_spec.rb'
+ - 'ee/spec/controllers/projects/merge_requests/creations_controller_spec.rb'
+ - 'ee/spec/controllers/projects/merge_requests_controller_spec.rb'
+ - 'ee/spec/controllers/projects/protected_environments_controller_spec.rb'
+ - 'ee/spec/controllers/projects/quality/test_cases_controller_spec.rb'
+ - 'ee/spec/controllers/projects/requirements_management/requirements_controller_spec.rb'
+ - 'ee/spec/controllers/projects/security/scanned_resources_controller_spec.rb'
+ - 'ee/spec/controllers/projects/settings/ci_cd_controller_spec.rb'
+ - 'ee/spec/controllers/projects/settings/repository_controller_spec.rb'
+ - 'ee/spec/controllers/projects/vulnerability_feedback_controller_spec.rb'
+ - 'ee/spec/controllers/projects_controller_spec.rb'
+ - 'ee/spec/controllers/registrations/groups_controller_spec.rb'
+ - 'ee/spec/controllers/registrations/welcome_controller_spec.rb'
+ - 'ee/spec/controllers/security/projects_controller_spec.rb'
+ - 'ee/spec/controllers/sitemap_controller_spec.rb'
+ - 'ee/spec/controllers/subscriptions_controller_spec.rb'
+ - 'ee/spec/db/production/license_spec.rb'
+ - 'ee/spec/elastic/migrate/20230724070100_backfill_epics_spec.rb'
+ - 'ee/spec/features/projects/pipelines/pipeline_spec.rb'
+ - 'ee/spec/features/user_sees_marketing_header_spec.rb'
+ - 'ee/spec/features/users/login_spec.rb'
+ - 'ee/spec/finders/analytics/cycle_analytics/stage_finder_spec.rb'
+ - 'ee/spec/finders/app_sec/fuzzing/coverage/corpuses_finder_spec.rb'
+ - 'ee/spec/finders/approval_rules/group_finder_spec.rb'
+ - 'ee/spec/finders/audit_event_finder_spec.rb'
+ - 'ee/spec/finders/auth/group_saml_identity_finder_spec.rb'
+ - 'ee/spec/finders/autocomplete/group_subgroups_finder_spec.rb'
+ - 'ee/spec/finders/boards/users_finder_spec.rb'
+ - 'ee/spec/finders/compliance_management/merge_requests/compliance_violations_finder_spec.rb'
+ - 'ee/spec/finders/custom_project_templates_finder_spec.rb'
+ - 'ee/spec/finders/dast/profiles_finder_spec.rb'
+ - 'ee/spec/finders/dast_scanner_profiles_finder_spec.rb'
+ - 'ee/spec/finders/dast_site_profiles_finder_spec.rb'
+ - 'ee/spec/finders/dast_site_validations_finder_spec.rb'
+ - 'ee/spec/finders/gpg_keys_finder_spec.rb'
+ - 'ee/spec/finders/group_saml_identity_finder_spec.rb'
+ - 'ee/spec/finders/groups_finder_spec.rb'
+ - 'ee/spec/finders/issues_finder_spec.rb'
+ - 'ee/spec/finders/iterations/cadences_finder_spec.rb'
+ - 'ee/spec/finders/iterations_finder_spec.rb'
+ - 'ee/spec/finders/notes_finder_spec.rb'
+ - 'ee/spec/finders/okrs/checkin_reminder_key_result_finder_spec.rb'
+ - 'ee/spec/finders/productivity_analytics_finder_spec.rb'
+ - 'ee/spec/finders/projects/integrations/jira/issues_finder_spec.rb'
+ - 'ee/spec/finders/remote_development/workspaces_finder_spec.rb'
+ - 'ee/spec/finders/security/approval_groups_finder_spec.rb'
+ - 'ee/spec/finders/security/pipeline_vulnerabilities_finder_spec.rb'
+ - 'ee/spec/finders/security/vulnerabilities_finder_spec.rb'
+ - 'ee/spec/finders/security/vulnerability_feedbacks_finder_spec.rb'
+ - 'ee/spec/finders/security/vulnerability_reads_finder_spec.rb'
+ - 'ee/spec/finders/snippets_finder_spec.rb'
+ - 'ee/spec/graphql/ee/mutations/boards/issues/issue_move_list_spec.rb'
+ - 'ee/spec/graphql/ee/mutations/boards/lists/create_spec.rb'
+ - 'ee/spec/graphql/ee/mutations/ci/job_token_scope/add_project_spec.rb'
+ - 'ee/spec/graphql/ee/mutations/ci/job_token_scope/remove_project_spec.rb'
+ - 'ee/spec/graphql/ee/mutations/ci/project_ci_cd_settings_update_spec.rb'
+ - 'ee/spec/graphql/ee/types/group_type_spec.rb'
+ - 'ee/spec/graphql/ee/types/namespace_type_spec.rb'
+ - 'ee/spec/graphql/graphql_triggers_spec.rb'
+ - 'ee/spec/graphql/mutations/ai/action_spec.rb'
+ - 'ee/spec/graphql/mutations/audit_events/streaming/event_type_filters/create_spec.rb'
+ - 'ee/spec/graphql/mutations/audit_events/streaming/event_type_filters/destroy_spec.rb'
+ - 'ee/spec/graphql/mutations/audit_events/streaming/headers/create_spec.rb'
+ - 'ee/spec/graphql/mutations/audit_events/streaming/headers/destroy_spec.rb'
+ - 'ee/spec/graphql/mutations/boards/epic_boards/destroy_spec.rb'
+ - 'ee/spec/graphql/mutations/boards/epic_boards/epic_move_list_spec.rb'
+ - 'ee/spec/graphql/mutations/boards/epics/create_spec.rb'
+ - 'ee/spec/graphql/mutations/boards/lists/update_limit_metrics_spec.rb'
+ - 'ee/spec/graphql/mutations/boards/update_spec.rb'
+ - 'ee/spec/graphql/mutations/compliance_management/frameworks/create_spec.rb'
+ - 'ee/spec/graphql/mutations/compliance_management/frameworks/update_spec.rb'
+ - 'ee/spec/graphql/mutations/dast/profiles/create_spec.rb'
+ - 'ee/spec/graphql/mutations/dast/profiles/delete_spec.rb'
+ - 'ee/spec/graphql/mutations/dast/profiles/run_spec.rb'
+ - 'ee/spec/graphql/mutations/dast/profiles/update_spec.rb'
+ - 'ee/spec/graphql/mutations/dast_on_demand_scans/create_spec.rb'
+ - 'ee/spec/graphql/mutations/dast_scanner_profiles/create_spec.rb'
+ - 'ee/spec/graphql/mutations/dast_scanner_profiles/delete_spec.rb'
+ - 'ee/spec/graphql/mutations/dast_scanner_profiles/update_spec.rb'
+ - 'ee/spec/graphql/mutations/dast_site_profiles/create_spec.rb'
+ - 'ee/spec/graphql/mutations/dast_site_profiles/delete_spec.rb'
+ - 'ee/spec/graphql/mutations/dast_site_profiles/update_spec.rb'
+ - 'ee/spec/graphql/mutations/dast_site_tokens/create_spec.rb'
+ - 'ee/spec/graphql/mutations/dast_site_validations/create_spec.rb'
+ - 'ee/spec/graphql/mutations/dast_site_validations/revoke_spec.rb'
+ - 'ee/spec/graphql/mutations/deployments/deployment_approve_spec.rb'
+ - 'ee/spec/graphql/mutations/epics/add_issue_spec.rb'
+ - 'ee/spec/graphql/mutations/epics/create_spec.rb'
+ - 'ee/spec/graphql/mutations/epics/update_spec.rb'
+ - 'ee/spec/graphql/mutations/forecasting/build_forecast_spec.rb'
+ - 'ee/spec/graphql/mutations/instance_security_dashboard/add_project_spec.rb'
+ - 'ee/spec/graphql/mutations/instance_security_dashboard/remove_project_spec.rb'
+ - 'ee/spec/graphql/mutations/issues/promote_to_epic_spec.rb'
+ - 'ee/spec/graphql/mutations/issues/set_epic_spec.rb'
+ - 'ee/spec/graphql/mutations/issues/set_iteration_spec.rb'
+ - 'ee/spec/graphql/mutations/issues/set_weight_spec.rb'
+ - 'ee/spec/graphql/mutations/issues/update_spec.rb'
+ - 'ee/spec/graphql/mutations/merge_requests/set_reviewers_spec.rb'
+ - 'ee/spec/graphql/mutations/merge_requests/update_approval_rules_spec.rb'
+ - 'ee/spec/graphql/mutations/namespaces/increase_storage_temporarily_spec.rb'
+ - 'ee/spec/graphql/mutations/projects/set_locked_spec.rb'
+ - 'ee/spec/graphql/mutations/releases/update_spec.rb'
+ - 'ee/spec/graphql/mutations/requirements_management/create_requirement_spec.rb'
+ - 'ee/spec/graphql/mutations/requirements_management/export_requirements_spec.rb'
+ - 'ee/spec/graphql/mutations/requirements_management/update_requirement_spec.rb'
+ - 'ee/spec/graphql/mutations/security/finding/dismiss_spec.rb'
+ - 'ee/spec/graphql/mutations/security_policy/assign_security_policy_project_spec.rb'
+ - 'ee/spec/graphql/mutations/security_policy/commit_scan_execution_policy_spec.rb'
+ - 'ee/spec/graphql/mutations/security_policy/create_security_policy_project_spec.rb'
+ - 'ee/spec/graphql/mutations/security_policy/unassign_security_policy_project_spec.rb'
+ - 'ee/spec/graphql/mutations/vulnerabilities/bulk_dismiss_spec.rb'
+ - 'ee/spec/graphql/mutations/vulnerabilities/create_external_issue_link_spec.rb'
+ - 'ee/spec/graphql/mutations/vulnerabilities/create_spec.rb'
+ - 'ee/spec/graphql/mutations/vulnerabilities/destroy_external_issue_link_spec.rb'
+ - 'ee/spec/graphql/mutations/vulnerabilities/dismiss_spec.rb'
+ - 'ee/spec/graphql/mutations/vulnerabilities/revert_to_detected_spec.rb'
+ - 'ee/spec/graphql/resolvers/app_sec/fuzzing/coverage/corpuses_resolver_spec.rb'
+ - 'ee/spec/graphql/resolvers/boards/epic_list_resolver_spec.rb'
+ - 'ee/spec/graphql/resolvers/clusters/agents_resolver_spec.rb'
+ - 'ee/spec/graphql/resolvers/compliance_management/merge_requests/compliance_violation_resolver_spec.rb'
+ - 'ee/spec/graphql/resolvers/incident_management/oncall_shifts_resolver_spec.rb'
+ - 'ee/spec/graphql/resolvers/incident_management/oncall_users_resolver_spec.rb'
+ - 'ee/spec/graphql/resolvers/product_analytics/dashboards_resolver_spec.rb'
+ - 'ee/spec/graphql/resolvers/product_analytics/state_resolver_spec.rb'
+ - 'ee/spec/graphql/resolvers/product_analytics/visualization_resolver_spec.rb'
+ - 'ee/spec/graphql/resolvers/requirements_management/requirements_resolver_spec.rb'
+ - 'ee/spec/graphql/resolvers/user_discussions_count_resolver_spec.rb'
+ - 'ee/spec/graphql/resolvers/user_notes_count_resolver_spec.rb'
+ - 'ee/spec/graphql/resolvers/vulnerabilities/issue_links_resolver_spec.rb'
+ - 'ee/spec/graphql/resolvers/vulnerabilities_grade_resolver_spec.rb'
+ - 'ee/spec/graphql/resolvers/vulnerabilities_resolver_spec.rb'
+ - 'ee/spec/graphql/types/asset_type_spec.rb'
+ - 'ee/spec/graphql/types/ci/pipeline_type_spec.rb'
+ - 'ee/spec/graphql/types/incident_management/escalation_rule_input_type_spec.rb'
+ - 'ee/spec/graphql/types/pipeline_security_report_finding_type_spec.rb'
+ - 'ee/spec/graphql/types/project_type_spec.rb'
+ - 'ee/spec/graphql/types/vulnerability_evidence_source_type_spec.rb'
+ - 'ee/spec/graphql/types/vulnerability_evidence_supporting_message_type_spec.rb'
+ - 'ee/spec/graphql/types/vulnerability_evidence_type_spec.rb'
+ - 'ee/spec/graphql/types/vulnerability_location/coverage_fuzzing_type_spec.rb'
+ - 'ee/spec/graphql/types/vulnerability_location_type_spec.rb'
+ - 'ee/spec/graphql/types/vulnerability_request_response_header_type_spec.rb'
+ - 'ee/spec/graphql/types/vulnerability_request_type_spec.rb'
+ - 'ee/spec/graphql/types/vulnerability_response_type_spec.rb'
+ - 'ee/spec/graphql/types/vulnerability_scanner_type_spec.rb'
+ - 'ee/spec/graphql/types/vulnerability_type_spec.rb'
+ - 'ee/spec/helpers/admin/emails_helper_spec.rb'
+ - 'ee/spec/helpers/billing_plans_helper_spec.rb'
+ - 'ee/spec/helpers/compliance_management/compliance_framework/group_settings_helper_spec.rb'
+ - 'ee/spec/helpers/ee/auth_helper_spec.rb'
+ - 'ee/spec/helpers/ee/branches_helper_spec.rb'
+ - 'ee/spec/helpers/ee/ci/catalog/resources_helper_spec.rb'
+ - 'ee/spec/helpers/ee/ci/runners_helper_spec.rb'
+ - 'ee/spec/helpers/ee/emails_helper_spec.rb'
+ - 'ee/spec/helpers/ee/environments_helper_spec.rb'
+ - 'ee/spec/helpers/ee/events_helper_spec.rb'
+ - 'ee/spec/helpers/ee/gitlab_routing_helper_spec.rb'
+ - 'ee/spec/helpers/ee/groups/group_members_helper_spec.rb'
+ - 'ee/spec/helpers/ee/groups_helper_spec.rb'
+ - 'ee/spec/helpers/ee/lock_helper_spec.rb'
+ - 'ee/spec/helpers/ee/operations_helper_spec.rb'
+ - 'ee/spec/helpers/ee/projects/incidents_helper_spec.rb'
+ - 'ee/spec/helpers/ee/projects/pipeline_helper_spec.rb'
+ - 'ee/spec/helpers/ee/subscribable_banner_helper_spec.rb'
+ - 'ee/spec/helpers/groups/ldap_sync_helper_spec.rb'
+ - 'ee/spec/helpers/groups/sso_helper_spec.rb'
+ - 'ee/spec/helpers/kerberos_helper_spec.rb'
+ - 'ee/spec/helpers/license_helper_spec.rb'
+ - 'ee/spec/helpers/merge_requests_helper_spec.rb'
+ - 'ee/spec/helpers/nav/new_dropdown_helper_spec.rb'
+ - 'ee/spec/helpers/projects/security/dast_profiles_helper_spec.rb'
+ - 'ee/spec/helpers/projects_helper_spec.rb'
+ - 'ee/spec/helpers/search_helper_spec.rb'
+ - 'ee/spec/helpers/secrets_helper_spec.rb'
+ - 'ee/spec/helpers/sidebars_helper_spec.rb'
+ - 'ee/spec/helpers/trials_helper_spec.rb'
+ - 'ee/spec/helpers/users/group_callouts_helper_spec.rb'
+ - 'ee/spec/helpers/users_helper_spec.rb'
+ - 'ee/spec/helpers/vulnerabilities_helper_spec.rb'
+ - 'ee/spec/initializers/database_config_spec.rb'
+ - 'ee/spec/initializers/session_store_spec.rb'
+ - 'ee/spec/lib/analytics/devops_adoption/snapshot_calculator_spec.rb'
+ - 'ee/spec/lib/analytics/dora_metrics_aggregator_spec.rb'
+ - 'ee/spec/lib/analytics/forecasting/holt_winters_optimizer_spec.rb'
+ - 'ee/spec/lib/analytics/forecasting/holt_winters_spec.rb'
+ - 'ee/spec/lib/analytics/group_activity_calculator_spec.rb'
+ - 'ee/spec/lib/analytics/merge_request_metrics_calculator_spec.rb'
+ - 'ee/spec/lib/analytics/merge_request_metrics_refresh_spec.rb'
+ - 'ee/spec/lib/analytics/productivity_analytics_request_params_spec.rb'
+ - 'ee/spec/lib/analytics/refresh_comments_data_spec.rb'
+ - 'ee/spec/lib/api/entities/code_suggestions_access_token_spec.rb'
+ - 'ee/spec/lib/api/entities/deployments/approval_spec.rb'
+ - 'ee/spec/lib/api/entities/deployments/approval_summary_spec.rb'
+ - 'ee/spec/lib/api/entities/epic_board_spec.rb'
+ - 'ee/spec/lib/api/entities/epic_boards/list_details_spec.rb'
+ - 'ee/spec/lib/api/entities/epic_boards/list_spec.rb'
+ - 'ee/spec/lib/api/entities/merge_request_approval_setting_spec.rb'
+ - 'ee/spec/lib/api/entities/protected_environments/approval_rule_for_summary_spec.rb'
+ - 'ee/spec/lib/api/entities/protected_environments/approval_rule_spec.rb'
+ - 'ee/spec/lib/api/entities/protected_environments/deploy_access_level_spec.rb'
+ - 'ee/spec/lib/audit/base_changes_auditor_spec.rb'
+ - 'ee/spec/lib/audit/compliance_framework_changes_auditor_spec.rb'
+ - 'ee/spec/lib/audit/group_merge_request_approval_setting_changes_auditor_spec.rb'
+ - 'ee/spec/lib/audit/merge_request_before_destroy_auditor_spec.rb'
+ - 'ee/spec/lib/audit/merge_request_destroy_auditor_spec.rb'
+ - 'ee/spec/lib/audit/project_changes_auditor_spec.rb'
+ - 'ee/spec/lib/audit/protected_environment_authorization_rule_changes_auditor_spec.rb'
+ - 'ee/spec/lib/audit/push_rules/group_push_rules_changes_auditor_spec.rb'
+ - 'ee/spec/lib/audit/push_rules/project_push_rules_changes_auditor_spec.rb'
+ - 'ee/spec/lib/audit_events/external_destination_streamer_spec.rb'
+ - 'ee/spec/lib/audit_events/strategies/amazon_s3_destination_strategy_spec.rb'
+ - 'ee/spec/lib/audit_events/strategies/external_destination_strategy_spec.rb'
+ - 'ee/spec/lib/audit_events/strategies/google_cloud_logging_destination_strategy_spec.rb'
+ - 'ee/spec/lib/audit_events/strategies/group_external_destination_strategy_spec.rb'
+ - 'ee/spec/lib/audit_events/strategies/instance/google_cloud_logging_destination_strategy_spec.rb'
+ - 'ee/spec/lib/audit_events/strategies/instance_external_destination_strategy_spec.rb'
+ - 'ee/spec/lib/banzai/reference_parser/epic_parser_spec.rb'
+ - 'ee/spec/lib/banzai/reference_parser/vulnerability_parser_spec.rb'
+ - 'ee/spec/lib/bulk_imports/common/pipelines/boards_pipeline_spec.rb'
+ - 'ee/spec/lib/bulk_imports/groups/pipelines/epics_pipeline_spec.rb'
+ - 'ee/spec/lib/bulk_imports/groups/pipelines/iterations_cadences_pipeline_spec.rb'
+ - 'ee/spec/lib/bulk_imports/groups/pipelines/iterations_pipeline_spec.rb'
+ - 'ee/spec/lib/code_suggestions/prompts/code_completion/vertex_ai_spec.rb'
+ - 'ee/spec/lib/code_suggestions/prompts/code_generation/anthropic_spec.rb'
+ - 'ee/spec/lib/code_suggestions/task_factory_spec.rb'
+ - 'ee/spec/lib/code_suggestions/tasks/base_spec.rb'
+ - 'ee/spec/lib/ee/api/entities/dependency_list_export_spec.rb'
+ - 'ee/spec/lib/ee/api/entities/deployment_extended_spec.rb'
+ - 'ee/spec/lib/ee/api/entities/experiment_spec.rb'
+ - 'ee/spec/lib/ee/api/entities/geo_node_status_spec.rb'
+ - 'ee/spec/lib/ee/api/entities/geo_site_spec.rb'
+ - 'ee/spec/lib/ee/api/entities/geo_site_status_spec.rb'
+ - 'ee/spec/lib/ee/api/entities/group_detail_spec.rb'
+ - 'ee/spec/lib/ee/api/entities/member_role_spec.rb'
+ - 'ee/spec/lib/ee/api/entities/project_spec.rb'
+ - 'ee/spec/lib/ee/api/entities/scim/conflict_spec.rb'
+ - 'ee/spec/lib/ee/api/entities/scim/emails_spec.rb'
+ - 'ee/spec/lib/ee/api/entities/scim/error_spec.rb'
+ - 'ee/spec/lib/ee/api/entities/scim/not_found_spec.rb'
+ - 'ee/spec/lib/ee/api/entities/scim/user_name_spec.rb'
+ - 'ee/spec/lib/ee/api/entities/scim/user_spec.rb'
+ - 'ee/spec/lib/ee/api/entities/scim/users_spec.rb'
+ - 'ee/spec/lib/ee/api/entities/user_with_admin_spec.rb'
+ - 'ee/spec/lib/ee/api/entities/vulnerability_export_spec.rb'
+ - 'ee/spec/lib/ee/api/entities/vulnerability_spec.rb'
+ - 'ee/spec/lib/ee/api/helpers/notes_helpers_spec.rb'
+ - 'ee/spec/lib/ee/api/helpers/scim_pagination_spec.rb'
+ - 'ee/spec/lib/ee/api/helpers_spec.rb'
+ - 'ee/spec/lib/ee/backup/repositories_spec.rb'
+ - 'ee/spec/lib/ee/bulk_imports/groups/stage_spec.rb'
+ - 'ee/spec/lib/ee/bulk_imports/projects/stage_spec.rb'
+ - 'ee/spec/lib/ee/feature_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/auth/auth_finders_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/auth/current_user_mode_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/auth/request_authenticator_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/background_migration/backfill_dismissal_reason_in_vulnerability_reads_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/background_migration/backfill_epic_cache_counts_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/background_migration/backfill_iteration_cadence_id_for_boards_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/background_migration/delete_approval_rules_with_vulnerability_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/background_migration/delete_invalid_epic_issues_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/background_migration/delete_orphaned_transferred_project_approval_rules_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/background_migration/fix_approval_project_rules_without_protected_branches_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/background_migration/migrate_shared_vulnerability_identifiers_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/background_migration/migrate_shared_vulnerability_scanners_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/background_migration/migrate_vulnerabilities_feedback_to_vulnerabilities_state_transition_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/background_migration/populate_approval_merge_request_rules_with_security_orchestration_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/background_migration/populate_approval_project_rules_with_security_orchestration_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/checks/push_rule_check_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/checks/push_rules/branch_check_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/checks/push_rules/commit_check_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/checks/push_rules/file_size_check_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/checks/push_rules/secrets_check_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/checks/push_rules/tag_check_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/ci/config/entry/bridge_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/ci/config/entry/need_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/ci/config/entry/needs_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/ci/parsers/security/validators/schema_validator_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/ci/pipeline/quota/size_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/ci/templates/templates_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/elastic/helper_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/group_search_results_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/import_export/after_export_strategies/custom_template_export_import_strategy_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/import_export/wiki_repo_saver_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/issuable/clone/copy_resource_events_service_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/metrics/samplers/database_sampler_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/pages/deployment_update_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/repository_size_checker_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/snippet_search_results_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/url_builder_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/usage_data_spec.rb'
+ - 'ee/spec/lib/ee/sidebars/groups/menus/issues_menu_spec.rb'
+ - 'ee/spec/lib/ee/sidebars/projects/menus/issues_menu_spec.rb'
+ - 'ee/spec/lib/ee/sidebars/projects/menus/security_compliance_menu_spec.rb'
+ - 'ee/spec/lib/ee/sidebars/projects/menus/settings_menu_spec.rb'
+ - 'ee/spec/lib/elastic/latest/application_instance_proxy_spec.rb'
+ - 'ee/spec/lib/elastic/latest/epic_class_proxy_spec.rb'
+ - 'ee/spec/lib/elastic/latest/epic_instance_proxy_spec.rb'
+ - 'ee/spec/lib/elastic/latest/git_class_proxy_add_suffix_project_in_wiki_rid_running_wiki_search_spec.rb'
+ - 'ee/spec/lib/elastic/latest/git_class_proxy_spec.rb'
+ - 'ee/spec/lib/elastic/latest/git_instance_proxy_spec.rb'
+ - 'ee/spec/lib/elastic/latest/issue_class_proxy_spec.rb'
+ - 'ee/spec/lib/elastic/latest/note_class_proxy_spec.rb'
+ - 'ee/spec/lib/elastic/latest/project_instance_proxy_spec.rb'
+ - 'ee/spec/lib/elastic/latest/routing_spec.rb'
+ - 'ee/spec/lib/elastic/latest/snippet_instance_proxy_spec.rb'
+ - 'ee/spec/lib/elastic/latest/user_class_proxy_spec.rb'
+ - 'ee/spec/lib/elastic/latest/user_instance_proxy_spec.rb'
+ - 'ee/spec/lib/elastic/latest/wiki_class_proxy_spec.rb'
+ - 'ee/spec/lib/elastic/latest/wiki_instance_proxy_spec.rb'
+ - 'ee/spec/lib/elastic/multi_version_class_proxy_spec.rb'
+ - 'ee/spec/lib/elastic/multi_version_instance_proxy_spec.rb'
+ - 'ee/spec/lib/gitlab/analytics/cycle_analytics/distinct_stage_loader_spec.rb'
+ - 'ee/spec/lib/gitlab/analytics/cycle_analytics/request_params_spec.rb'
+ - 'ee/spec/lib/gitlab/analytics/cycle_analytics/summary/change_failure_rate_spec.rb'
+ - 'ee/spec/lib/gitlab/analytics/cycle_analytics/summary/group/stage_summary_spec.rb'
+ - 'ee/spec/lib/gitlab/analytics/cycle_analytics/summary/lead_time_for_changes_spec.rb'
+ - 'ee/spec/lib/gitlab/analytics/cycle_analytics/summary/lead_time_spec.rb'
+ - 'ee/spec/lib/gitlab/analytics/cycle_analytics/summary/stage_time_summary_spec.rb'
+ - 'ee/spec/lib/gitlab/analytics/cycle_analytics/summary/time_to_restore_service_spec.rb'
+ - 'ee/spec/lib/gitlab/analytics/type_of_work/tasks_by_type_spec.rb'
+ - 'ee/spec/lib/gitlab/applied_ml/suggested_reviewers/client_spec.rb'
+ - 'ee/spec/lib/gitlab/audit/events/preloader_spec.rb'
+ - 'ee/spec/lib/gitlab/audit/levels/group_spec.rb'
+ - 'ee/spec/lib/gitlab/audit/levels/instance_spec.rb'
+ - 'ee/spec/lib/gitlab/audit/levels/project_spec.rb'
+ - 'ee/spec/lib/gitlab/auth/group_saml/dynamic_settings_spec.rb'
+ - 'ee/spec/lib/gitlab/auth/group_saml/failure_handler_spec.rb'
+ - 'ee/spec/lib/gitlab/auth/group_saml/gma_membership_enforcer_spec.rb'
+ - 'ee/spec/lib/gitlab/auth/group_saml/group_lookup_spec.rb'
+ - 'ee/spec/lib/gitlab/auth/group_saml/identity_linker_spec.rb'
+ - 'ee/spec/lib/gitlab/auth/group_saml/membership_updater_spec.rb'
+ - 'ee/spec/lib/gitlab/auth/group_saml/response_check_spec.rb'
+ - 'ee/spec/lib/gitlab/auth/group_saml/response_store_spec.rb'
+ - 'ee/spec/lib/gitlab/auth/group_saml/sso_enforcer_spec.rb'
+ - 'ee/spec/lib/gitlab/auth/group_saml/sso_state_spec.rb'
+ - 'ee/spec/lib/gitlab/auth/group_saml/token_actor_spec.rb'
+ - 'ee/spec/lib/gitlab/auth/group_saml/user_spec.rb'
+ - 'ee/spec/lib/gitlab/auth/group_saml/xml_response_spec.rb'
+ - 'ee/spec/lib/gitlab/auth/ldap/adapter_spec.rb'
+ - 'ee/spec/lib/gitlab/auth/ldap/person_spec.rb'
+ - 'ee/spec/lib/gitlab/auth/o_auth/user_spec.rb'
+ - 'ee/spec/lib/gitlab/auth/smartcard/certificate_spec.rb'
+ - 'ee/spec/lib/gitlab/auth/smartcard/ldap_certificate_spec.rb'
+ - 'ee/spec/lib/gitlab/auth/smartcard/session_enforcer_spec.rb'
+ - 'ee/spec/lib/gitlab/auth/smartcard/session_spec.rb'
+ - 'ee/spec/lib/gitlab/auth_spec.rb'
+ - 'ee/spec/lib/gitlab/background_migration/create_vulnerability_links_spec.rb'
+ - 'ee/spec/lib/gitlab/checks/changes_access_spec.rb'
+ - 'ee/spec/lib/gitlab/checks/diff_check_spec.rb'
+ - 'ee/spec/lib/gitlab/ci/config/required/processor_spec.rb'
+ - 'ee/spec/lib/gitlab/ci/config/security_orchestration_policies/processor_spec.rb'
+ - 'ee/spec/lib/gitlab/ci/minutes/cached_quota_spec.rb'
+ - 'ee/spec/lib/gitlab/ci/minutes/consumption_spec.rb'
+ - 'ee/spec/lib/gitlab/ci/minutes/cost_factor_spec.rb'
+ - 'ee/spec/lib/gitlab/ci/minutes/pipeline_consumption_spec.rb'
+ - 'ee/spec/lib/gitlab/ci/parsers/license_compliance/license_scanning_spec.rb'
+ - 'ee/spec/lib/gitlab/ci/parsers/metrics/generic_spec.rb'
+ - 'ee/spec/lib/gitlab/ci/pipeline/chain/create_cross_database_associations_spec.rb'
+ - 'ee/spec/lib/gitlab/ci/pipeline/chain/limit/size_spec.rb'
+ - 'ee/spec/lib/gitlab/ci/reports/coverage_fuzzing/report_spec.rb'
+ - 'ee/spec/lib/gitlab/ci/reports/dependency_list/report_spec.rb'
+ - 'ee/spec/lib/gitlab/ci/reports/license_scanning/dependency_spec.rb'
+ - 'ee/spec/lib/gitlab/ci/reports/license_scanning/license_spec.rb'
+ - 'ee/spec/lib/gitlab/ci/reports/license_scanning/report_spec.rb'
+ - 'ee/spec/lib/gitlab/ci/reports/license_scanning/reports_comparer_spec.rb'
+ - 'ee/spec/lib/gitlab/ci/reports/metrics/report_spec.rb'
+ - 'ee/spec/lib/gitlab/ci/reports/metrics/reports_comparer_spec.rb'
+ - 'ee/spec/lib/gitlab/ci/reports/security/finding_spec.rb'
+ - 'ee/spec/lib/gitlab/ci/reports/security/locations/container_scanning_spec.rb'
+ - 'ee/spec/lib/gitlab/ci/reports/security/vulnerability_reports_comparer_spec.rb'
+ - 'ee/spec/lib/gitlab/ci/yaml_processor_spec.rb'
+ - 'ee/spec/lib/gitlab/circuit_breaker/notifier_spec.rb'
+ - 'ee/spec/lib/gitlab/code_owners/file_spec.rb'
+ - 'ee/spec/lib/gitlab/code_owners/loader_spec.rb'
+ - 'ee/spec/lib/gitlab/code_owners/validator_spec.rb'
+ - 'ee/spec/lib/gitlab/code_owners_spec.rb'
+ - 'ee/spec/lib/gitlab/elastic/group_search_results_spec.rb'
+ - 'ee/spec/lib/gitlab/elastic/indexer_spec.rb'
+ - 'ee/spec/lib/gitlab/elastic/project_search_results_spec.rb'
+ - 'ee/spec/lib/gitlab/email/feature_flag_wrapper_spec.rb'
+ - 'ee/spec/lib/gitlab/expiring_subscription_message_spec.rb'
+ - 'ee/spec/lib/gitlab/geo/geo_node_status_check_spec.rb'
+ - 'ee/spec/lib/gitlab/geo/geo_tasks_spec.rb'
+ - 'ee/spec/lib/gitlab/geo/git_push_http_spec.rb'
+ - 'ee/spec/lib/gitlab/geo/git_ssh_proxy_spec.rb'
+ - 'ee/spec/lib/gitlab/geo/health_check_spec.rb'
+ - 'ee/spec/lib/gitlab/geo/jwt_request_decoder_spec.rb'
+ - 'ee/spec/lib/gitlab/geo/log_cursor/event_logs_spec.rb'
+ - 'ee/spec/lib/gitlab/geo/log_cursor/events/cache_invalidation_event_spec.rb'
+ - 'ee/spec/lib/gitlab/geo/log_cursor/events/event_spec.rb'
+ - 'ee/spec/lib/gitlab/geo/log_cursor/events/hashed_storage_attachments_event_spec.rb'
+ - 'ee/spec/lib/gitlab/geo/oauth/session_spec.rb'
+ - 'ee/spec/lib/gitlab/geo/registry_batcher_spec.rb'
+ - 'ee/spec/lib/gitlab/geo/replication/blob_downloader_spec.rb'
+ - 'ee/spec/lib/gitlab/geo/replication/blob_retriever_spec.rb'
+ - 'ee/spec/lib/gitlab/geo/replicator_spec.rb'
+ - 'ee/spec/lib/gitlab/geo/signed_data_spec.rb'
+ - 'ee/spec/lib/gitlab/git_access_wiki_spec.rb'
+ - 'ee/spec/lib/gitlab/git_audit_event_spec.rb'
+ - 'ee/spec/lib/gitlab/graphql/aggregations/epics/epic_node_spec.rb'
+ - 'ee/spec/lib/gitlab/graphql/aggregations/epics/lazy_epic_aggregate_spec.rb'
+ - 'ee/spec/lib/gitlab/graphql/aggregations/security_orchestration_policies/lazy_dast_profile_aggregate_spec.rb'
+ - 'ee/spec/lib/gitlab/graphql/aggregations/vulnerabilities/lazy_user_notes_count_aggregate_spec.rb'
+ - 'ee/spec/lib/gitlab/graphql/aggregations/vulnerability_statistics/lazy_aggregate_spec.rb'
+ - 'ee/spec/lib/gitlab/graphql/loaders/bulk_epic_aggregate_loader_spec.rb'
+ - 'ee/spec/lib/gitlab/import_export/group/group_and_descendants_repo_restorer_spec.rb'
+ - 'ee/spec/lib/gitlab/import_export/group/group_and_descendants_repo_saver_spec.rb'
+ - 'ee/spec/lib/gitlab/import_export/project/deploy_keys_restorer_spec.rb'
+ - 'ee/spec/lib/gitlab/import_export/project/project_hooks_restorer_spec.rb'
+ - 'ee/spec/lib/gitlab/insights/configuration_filter_spec.rb'
+ - 'ee/spec/lib/gitlab/insights/finders/issuable_finder_spec.rb'
+ - 'ee/spec/lib/gitlab/insights/project_insights_config_spec.rb'
+ - 'ee/spec/lib/gitlab/insights/reducers/count_per_label_reducer_spec.rb'
+ - 'ee/spec/lib/gitlab/insights/reducers/count_per_period_reducer_spec.rb'
+ - 'ee/spec/lib/gitlab/insights/reducers/label_count_per_period_reducer_spec.rb'
+ - 'ee/spec/lib/gitlab/insights/serializers/chartjs/line_serializer_spec.rb'
+ - 'ee/spec/lib/gitlab/insights/serializers/chartjs/multi_series_serializer_spec.rb'
+ - 'ee/spec/lib/gitlab/insights/validators/params_validator_spec.rb'
+ - 'ee/spec/lib/gitlab/instrumentation_helper_spec.rb'
+ - 'ee/spec/lib/gitlab/items_collection_spec.rb'
+ - 'ee/spec/lib/gitlab/llm/ai_message_spec.rb'
+ - 'ee/spec/lib/gitlab/llm/anthropic/response_modifiers/tanuki_bot_spec.rb'
+ - 'ee/spec/lib/gitlab/llm/base_response_modifier_spec.rb'
+ - 'ee/spec/lib/gitlab/llm/chain/agents/zero_shot/prompts/anthropic_spec.rb'
+ - 'ee/spec/lib/gitlab/llm/chain/parsers/output_parser_spec.rb'
+ - 'ee/spec/lib/gitlab/llm/chain/response_modifier_spec.rb'
+ - 'ee/spec/lib/gitlab/llm/chain/tools/tool_spec.rb'
+ - '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/open_ai/completions/generate_commit_message_spec.rb'
+ - 'ee/spec/lib/gitlab/llm/templates/categorize_question_spec.rb'
+ - 'ee/spec/lib/gitlab/llm/templates/explain_vulnerability_spec.rb'
+ - 'ee/spec/lib/gitlab/llm/templates/fill_in_merge_request_template_spec.rb'
+ - 'ee/spec/lib/gitlab/llm/templates/generate_commit_message_spec.rb'
+ - 'ee/spec/lib/gitlab/llm/templates/generate_test_file_spec.rb'
+ - 'ee/spec/lib/gitlab/llm/templates/summarize_merge_request_spec.rb'
+ - 'ee/spec/lib/gitlab/llm/templates/summarize_review_spec.rb'
+ - 'ee/spec/lib/gitlab/llm/templates/summarize_submitted_review_spec.rb'
+ - 'ee/spec/lib/gitlab/llm/vertex_ai/completions/analyze_ci_job_failure_spec.rb'
+ - 'ee/spec/lib/gitlab/llm/vertex_ai/completions/fill_in_merge_request_template_spec.rb'
+ - 'ee/spec/lib/gitlab/llm/vertex_ai/completions/generate_commit_message_spec.rb'
+ - 'ee/spec/lib/gitlab/llm/vertex_ai/completions/generate_test_file_spec.rb'
+ - 'ee/spec/lib/gitlab/llm/vertex_ai/completions/summarize_merge_request_spec.rb'
+ - 'ee/spec/lib/gitlab/llm/vertex_ai/completions/summarize_review_spec.rb'
+ - 'ee/spec/lib/gitlab/llm/vertex_ai/completions/summarize_submitted_review_spec.rb'
+ - 'ee/spec/lib/gitlab/llm/vertex_ai/model_configurations/base_spec.rb'
+ - 'ee/spec/lib/gitlab/llm/vertex_ai/model_configurations/chat_spec.rb'
+ - 'ee/spec/lib/gitlab/llm/vertex_ai/model_configurations/code_chat_spec.rb'
+ - 'ee/spec/lib/gitlab/llm/vertex_ai/model_configurations/code_completion_spec.rb'
+ - 'ee/spec/lib/gitlab/llm/vertex_ai/model_configurations/code_spec.rb'
+ - 'ee/spec/lib/gitlab/llm/vertex_ai/model_configurations/text_embeddings_spec.rb'
+ - 'ee/spec/lib/gitlab/llm/vertex_ai/model_configurations/text_spec.rb'
+ - 'ee/spec/lib/gitlab/metrics/samplers/global_search_sampler_spec.rb'
+ - 'ee/spec/lib/gitlab/mirror_spec.rb'
+ - 'ee/spec/lib/gitlab/package_metadata/connector/base_data_file_spec.rb'
+ - 'ee/spec/lib/gitlab/patch/additional_database_tasks_spec.rb'
+ - 'ee/spec/lib/gitlab/patch/draw_route_spec.rb'
+ - 'ee/spec/lib/gitlab/proxy_spec.rb'
+ - 'ee/spec/lib/gitlab/reference_extractor_spec.rb'
+ - 'ee/spec/lib/gitlab/search/aggregation_parser_spec.rb'
+ - 'ee/spec/lib/gitlab/search/aggregation_spec.rb'
+ - 'ee/spec/lib/gitlab/search/client_spec.rb'
+ - 'ee/spec/lib/gitlab/search/zoekt/client_spec.rb'
+ - 'ee/spec/lib/gitlab/search_context/builder_spec.rb'
+ - 'ee/spec/lib/gitlab/sitemaps/generator_spec.rb'
+ - 'ee/spec/lib/gitlab/sitemaps/sitemap_file_spec.rb'
+ - 'ee/spec/lib/gitlab/sitemaps/url_extractor_spec.rb'
+ - 'ee/spec/lib/gitlab/spdx/catalogue_spec.rb'
+ - 'ee/spec/lib/gitlab/status_page/filter/image_filter_spec.rb'
+ - 'ee/spec/lib/gitlab/status_page/pipeline/post_process_pipeline_spec.rb'
+ - 'ee/spec/lib/gitlab/status_page_spec.rb'
+ - 'ee/spec/lib/gitlab/subscription_portal/clients/graphql_spec.rb'
+ - 'ee/spec/lib/gitlab/tree_summary_spec.rb'
+ - 'ee/spec/lib/gitlab/usage_data_metrics_spec.rb'
+ - 'ee/spec/lib/gitlab/vulnerabilities/parser_spec.rb'
+ - 'ee/spec/lib/gitlab/vulnerability_scanning/track_cvs_service_spec.rb'
+ - 'ee/spec/lib/gitlab_subscriptions/upcoming_reconciliation_entity_spec.rb'
+ - 'ee/spec/lib/omni_auth/strategies/kerberos_spec.rb'
+ - 'ee/spec/lib/product_analytics/settings_spec.rb'
+ - 'ee/spec/lib/remote_development/agent_config/main_integration_spec.rb'
+ - 'ee/spec/lib/remote_development/agent_config/updater_spec.rb'
+ - 'ee/spec/lib/remote_development/workspaces/create/creator_spec.rb'
+ - 'ee/spec/lib/remote_development/workspaces/create/devfile_flattener_spec.rb'
+ - 'ee/spec/lib/remote_development/workspaces/create/editor_component_injector_spec.rb'
+ - 'ee/spec/lib/remote_development/workspaces/create/main_integration_spec.rb'
+ - 'ee/spec/lib/remote_development/workspaces/create/personal_access_token_creator_spec.rb'
+ - 'ee/spec/lib/remote_development/workspaces/create/project_cloner_component_injector_spec.rb'
+ - 'ee/spec/lib/remote_development/workspaces/create/volume_component_injector_spec.rb'
+ - 'ee/spec/lib/remote_development/workspaces/create/volume_definer_spec.rb'
+ - 'ee/spec/lib/remote_development/workspaces/create/workspace_creator_spec.rb'
+ - 'ee/spec/lib/remote_development/workspaces/create/workspace_variables_creator_spec.rb'
+ - 'ee/spec/lib/remote_development/workspaces/reconcile/input/actual_state_calculator_spec.rb'
+ - 'ee/spec/lib/remote_development/workspaces/reconcile/input/agent_info_spec.rb'
+ - 'ee/spec/lib/remote_development/workspaces/reconcile/input/agent_infos_observer_spec.rb'
+ - 'ee/spec/lib/remote_development/workspaces/reconcile/input/factory_spec.rb'
+ - 'ee/spec/lib/remote_development/workspaces/reconcile/input/params_extractor_spec.rb'
+ - 'ee/spec/lib/remote_development/workspaces/reconcile/input/params_to_infos_converter_spec.rb'
+ - 'ee/spec/lib/remote_development/workspaces/reconcile/main_integration_spec.rb'
+ - 'ee/spec/lib/remote_development/workspaces/reconcile/output/desired_config_generator_prev1_spec.rb'
+ - 'ee/spec/lib/remote_development/workspaces/reconcile/output/desired_config_generator_spec.rb'
+ - 'ee/spec/lib/remote_development/workspaces/reconcile/output/devfile_parser_prev1_spec.rb'
+ - 'ee/spec/lib/remote_development/workspaces/reconcile/output/devfile_parser_spec.rb'
+ - 'ee/spec/lib/remote_development/workspaces/reconcile/output/rails_infos_observer_spec.rb'
+ - 'ee/spec/lib/remote_development/workspaces/reconcile/persistence/orphaned_workspaces_observer_spec.rb'
+ - 'ee/spec/lib/remote_development/workspaces/reconcile/persistence/workspaces_from_agent_infos_updater_spec.rb'
+ - 'ee/spec/lib/remote_development/workspaces/reconcile/persistence/workspaces_to_be_returned_finder_spec.rb'
+ - 'ee/spec/lib/remote_development/workspaces/reconcile/persistence/workspaces_to_be_returned_updater_spec.rb'
+ - 'ee/spec/lib/remote_development/workspaces/update/main_integration_spec.rb'
+ - 'ee/spec/lib/remote_development/workspaces/update/updater_spec.rb'
+ - 'ee/spec/lib/sidebars/groups/menus/epics_menu_spec.rb'
+ - 'ee/spec/lib/sidebars/groups/menus/security_compliance_menu_spec.rb'
+ - 'ee/spec/lib/sidebars/projects/menus/learn_gitlab_menu_spec.rb'
+ - 'ee/spec/lib/sidebars/user_settings/menus/profile_billing_menu_spec.rb'
+ - 'ee/spec/lib/system_check/geo/authorized_keys_check_spec.rb'
+ - 'ee/spec/lib/system_check/geo/authorized_keys_flag_check_spec.rb'
+ - 'ee/spec/lib/system_check/geo/clocks_synchronization_check_spec.rb'
+ - 'ee/spec/lib/system_check/geo/current_node_check_spec.rb'
+ - 'ee/spec/lib/system_check/geo/geo_database_configured_check_spec.rb'
+ - 'ee/spec/lib/system_check/geo/http_connection_check_spec.rb'
+ - 'ee/spec/lib/system_check/geo/license_check_spec.rb'
+ - 'ee/spec/lib/system_check/geo/ssh_port_check_spec.rb'
+ - 'ee/spec/lib/telesign/transaction_callback_spec.rb'
+ - 'ee/spec/mailers/ee/emails/issues_spec.rb'
+ - 'ee/spec/mailers/emails/enterprise_users_spec.rb'
+ - 'ee/spec/mailers/emails/epics_spec.rb'
+ - 'ee/spec/mailers/emails/group_memberships_spec.rb'
+ - 'ee/spec/mailers/emails/in_product_marketing_spec.rb'
+ - 'ee/spec/mailers/emails/merge_commits_spec.rb'
+ - 'ee/spec/mailers/emails/merge_requests_spec.rb'
+ - 'ee/spec/mailers/emails/requirements_spec.rb'
+ - 'ee/spec/mailers/license_mailer_spec.rb'
+ - 'ee/spec/mailers/notify_spec.rb'
+ - 'ee/spec/models/ai/job_failure_analysis_spec.rb'
+ - 'ee/spec/models/ai/project/conversations_spec.rb'
+ - 'ee/spec/models/allowed_email_domain_spec.rb'
+ - 'ee/spec/models/analytics/cycle_analytics/group_level_spec.rb'
+ - 'ee/spec/models/analytics/forecasting/deployment_frequency_forecast_spec.rb'
+ - 'ee/spec/models/analytics/forecasting/forecast_spec.rb'
+ - 'ee/spec/models/analytics/issues_analytics_spec.rb'
+ - 'ee/spec/models/application_setting_spec.rb'
+ - 'ee/spec/models/approval_merge_request_rule_spec.rb'
+ - 'ee/spec/models/approval_project_rule_spec.rb'
+ - 'ee/spec/models/approval_state_spec.rb'
+ - 'ee/spec/models/approval_wrapped_any_approver_rule_spec.rb'
+ - 'ee/spec/models/approval_wrapped_code_owner_rule_spec.rb'
+ - 'ee/spec/models/approval_wrapped_rule_spec.rb'
+ - 'ee/spec/models/approvals/scan_finding_wrapped_rule_set_spec.rb'
+ - 'ee/spec/models/approvals/wrapped_rule_set_spec.rb'
+ - 'ee/spec/models/audit_events/external_audit_event_destination_spec.rb'
+ - 'ee/spec/models/audit_events/instance_external_audit_event_destination_spec.rb'
+ - 'ee/spec/models/audit_events/streaming/event_type_filter_spec.rb'
+ - 'ee/spec/models/audit_events/streaming/instance_event_type_filter_spec.rb'
+ - 'ee/spec/models/boards/epic_board_position_spec.rb'
+ - 'ee/spec/models/burndown_spec.rb'
+ - 'ee/spec/models/ci/bridge_spec.rb'
+ - 'ee/spec/models/ci/build_spec.rb'
+ - 'ee/spec/models/ci/daily_build_group_report_result_spec.rb'
+ - 'ee/spec/models/ci/editor/ai_conversation/message_spec.rb'
+ - 'ee/spec/models/ci/minutes/cost_setting_spec.rb'
+ - 'ee/spec/models/ci/minutes/namespace_monthly_usage_spec.rb'
+ - 'ee/spec/models/ci/minutes/project_monthly_usage_spec.rb'
+ - 'ee/spec/models/ci/minutes/quota_spec.rb'
+ - 'ee/spec/models/ci/pipeline_spec.rb'
+ - 'ee/spec/models/compliance_management/compliance_framework/project_settings_spec.rb'
+ - 'ee/spec/models/compliance_management/compliance_framework/security_policy_spec.rb'
+ - 'ee/spec/models/compliance_management/framework_spec.rb'
+ - 'ee/spec/models/concerns/approval_rule_like_spec.rb'
+ - 'ee/spec/models/concerns/ee/packages/downloadable_spec.rb'
+ - 'ee/spec/models/concerns/ee/weight_eventable_spec.rb'
+ - 'ee/spec/models/concerns/elastic/projects_search_spec.rb'
+ - 'ee/spec/models/concerns/geo/has_replicator_spec.rb'
+ - 'ee/spec/models/concerns/geo/replicable_model_spec.rb'
+ - 'ee/spec/models/concerns/geo/verifiable_model_spec.rb'
+ - 'ee/spec/models/concerns/geo/verification_state_spec.rb'
+ - 'ee/spec/models/concerns/identity_verifiable_spec.rb'
+ - 'ee/spec/models/concerns/mirror_configuration_spec.rb'
+ - 'ee/spec/models/concerns/projects/custom_branch_rule_spec.rb'
+ - 'ee/spec/models/concerns/timebox_spec.rb'
+ - 'ee/spec/models/dast/branch_spec.rb'
+ - 'ee/spec/models/dast/pre_scan_verification_spec.rb'
+ - 'ee/spec/models/dast/pre_scan_verification_step_spec.rb'
+ - 'ee/spec/models/dast/profile_schedule_spec.rb'
+ - 'ee/spec/models/dast/profile_spec.rb'
+ - 'ee/spec/models/dast/scanner_profiles_build_spec.rb'
+ - 'ee/spec/models/dast/site_profile_secret_variable_spec.rb'
+ - 'ee/spec/models/dast/site_profiles_build_spec.rb'
+ - 'ee/spec/models/dast_scanner_profile_spec.rb'
+ - 'ee/spec/models/dast_site_profile_spec.rb'
+ - 'ee/spec/models/dast_site_spec.rb'
+ - 'ee/spec/models/dast_site_token_spec.rb'
+ - 'ee/spec/models/dast_site_validation_spec.rb'
+ - 'ee/spec/models/dora/base_metric_spec.rb'
+ - 'ee/spec/models/dora/change_failure_rate_metric_spec.rb'
+ - 'ee/spec/models/dora/daily_metrics_spec.rb'
+ - 'ee/spec/models/dora/deployment_frequency_metric_spec.rb'
+ - 'ee/spec/models/dora/time_to_restore_service_metric_spec.rb'
+ - 'ee/spec/models/dora/watchers/deployment_watcher_spec.rb'
+ - 'ee/spec/models/dora/watchers/issue_watcher_spec.rb'
+ - 'ee/spec/models/ee/alert_management/alert_spec.rb'
+ - 'ee/spec/models/ee/approvable_spec.rb'
+ - 'ee/spec/models/ee/audit_event_spec.rb'
+ - 'ee/spec/models/ee/ci/build_dependencies_spec.rb'
+ - 'ee/spec/models/ee/event_collection_spec.rb'
+ - 'ee/spec/models/ee/group_spec.rb'
+ - 'ee/spec/models/ee/groups/feature_setting_spec.rb'
+ - 'ee/spec/models/ee/list_spec.rb'
+ - 'ee/spec/models/ee/merge_request/metrics_spec.rb'
+ - 'ee/spec/models/ee/namespace/root_storage_statistics_spec.rb'
+ - 'ee/spec/models/ee/namespace_spec.rb'
+ - 'ee/spec/models/ee/namespaces/namespace_ban_spec.rb'
+ - 'ee/spec/models/ee/notification_setting_spec.rb'
+ - 'ee/spec/models/ee/pages_domain_spec.rb'
+ - 'ee/spec/models/ee/personal_access_token_spec.rb'
+ - 'ee/spec/models/ee/project_spec.rb'
+ - 'ee/spec/models/ee/project_wiki_spec.rb'
+ - 'ee/spec/models/ee/projects/branch_rule_spec.rb'
+ - 'ee/spec/models/ee/protected_branch_spec.rb'
+ - 'ee/spec/models/ee/resource_label_event_spec.rb'
+ - 'ee/spec/models/ee/resource_state_event_spec.rb'
+ - 'ee/spec/models/ee/user_detail_spec.rb'
+ - 'ee/spec/models/ee/user_spec.rb'
+ - 'ee/spec/models/ee/vulnerability_spec.rb'
+ - 'ee/spec/models/ee/work_items/parent_link_spec.rb'
+ - 'ee/spec/models/embedding/application_record_spec.rb'
+ - 'ee/spec/models/embedding/schema_migration_spec.rb'
+ - 'ee/spec/models/environment_spec.rb'
+ - 'ee/spec/models/epic_issue_spec.rb'
+ - 'ee/spec/models/epic_spec.rb'
+ - 'ee/spec/models/geo/deleted_project_spec.rb'
+ - 'ee/spec/models/geo/event_log_spec.rb'
+ - 'ee/spec/models/geo/every_geo_event_spec.rb'
+ - 'ee/spec/models/geo/package_file_registry_spec.rb'
+ - 'ee/spec/models/geo/push_user_spec.rb'
+ - 'ee/spec/models/geo/repository_updated_event_spec.rb'
+ - 'ee/spec/models/geo/secondary_usage_data_spec.rb'
+ - 'ee/spec/models/geo_node_spec.rb'
+ - 'ee/spec/models/geo_node_status_spec.rb'
+ - 'ee/spec/models/gitlab/seat_link_data_spec.rb'
+ - 'ee/spec/models/gitlab_subscription_spec.rb'
+ - 'ee/spec/models/gitlab_subscriptions/features_spec.rb'
+ - 'ee/spec/models/group_wiki_spec.rb'
+ - 'ee/spec/models/identity_spec.rb'
+ - 'ee/spec/models/incident_management/escalation_policy_spec.rb'
+ - 'ee/spec/models/incident_management/escalation_rule_spec.rb'
+ - 'ee/spec/models/incident_management/issuable_escalation_status_spec.rb'
+ - 'ee/spec/models/incident_management/oncall_participant_spec.rb'
+ - 'ee/spec/models/incident_management/oncall_rotation_spec.rb'
+ - 'ee/spec/models/incident_management/oncall_schedule_spec.rb'
+ - 'ee/spec/models/incident_management/oncall_shift_spec.rb'
+ - 'ee/spec/models/instance_security_dashboard_spec.rb'
+ - 'ee/spec/models/integrations/chat_message/vulnerability_message_spec.rb'
+ - 'ee/spec/models/integrations/github/remote_project_spec.rb'
+ - 'ee/spec/models/integrations/github/status_message_spec.rb'
+ - 'ee/spec/models/integrations/github/status_notifier_spec.rb'
+ - 'ee/spec/models/integrations/github_spec.rb'
+ - 'ee/spec/models/ip_restriction_spec.rb'
+ - 'ee/spec/models/issuable_metric_image_spec.rb'
+ - 'ee/spec/models/issue_spec.rb'
+ - 'ee/spec/models/iteration_note_spec.rb'
+ - 'ee/spec/models/iteration_spec.rb'
+ - 'ee/spec/models/label_note_spec.rb'
+ - 'ee/spec/models/license_spec.rb'
+ - 'ee/spec/models/merge_request_spec.rb'
+ - 'ee/spec/models/merge_requests/external_status_check_spec.rb'
+ - 'ee/spec/models/merge_trains/car_spec.rb'
+ - 'ee/spec/models/merge_trains/train_spec.rb'
+ - 'ee/spec/models/namespace_limit_spec.rb'
+ - 'ee/spec/models/namespace_setting_spec.rb'
+ - 'ee/spec/models/namespaces/free_user_cap/root_size_spec.rb'
+ - 'ee/spec/models/namespaces/storage/root_size_spec.rb'
+ - 'ee/spec/models/packages/package_file_spec.rb'
+ - 'ee/spec/models/product_analytics/dashboard_spec.rb'
+ - 'ee/spec/models/product_analytics/panel_spec.rb'
+ - 'ee/spec/models/product_analytics/visualization_spec.rb'
+ - 'ee/spec/models/project_feature_spec.rb'
+ - 'ee/spec/models/projects/all_branches_rule_spec.rb'
+ - 'ee/spec/models/projects/all_protected_branches_rule_spec.rb'
+ - 'ee/spec/models/projects/target_branch_rule_spec.rb'
+ - 'ee/spec/models/protected_environment_spec.rb'
+ - 'ee/spec/models/push_rule_spec.rb'
+ - 'ee/spec/models/release_highlight_spec.rb'
+ - 'ee/spec/models/remote_development/remote_development_agent_config_spec.rb'
+ - 'ee/spec/models/remote_development/workspace_spec.rb'
+ - 'ee/spec/models/remote_development/workspace_variable_spec.rb'
+ - 'ee/spec/models/requirements_management/requirement_spec.rb'
+ - 'ee/spec/models/requirements_management/test_report_spec.rb'
+ - 'ee/spec/models/saml_provider_spec.rb'
+ - 'ee/spec/models/sbom/occurrence_spec.rb'
+ - 'ee/spec/models/sca/license_policy_spec.rb'
+ - 'ee/spec/models/security/orchestration_policy_configuration_spec.rb'
+ - 'ee/spec/models/security/training_provider_spec.rb'
+ - 'ee/spec/models/security/training_spec.rb'
+ - 'ee/spec/models/snippet_spec.rb'
+ - 'ee/spec/models/software_license_policy_spec.rb'
+ - 'ee/spec/models/software_license_spec.rb'
+ - 'ee/spec/models/status_page/published_incident_spec.rb'
+ - 'ee/spec/models/upload_spec.rb'
+ - 'ee/spec/models/vulnerabilities/finding_spec.rb'
+ - 'ee/spec/models/vulnerabilities/state_transition_spec.rb'
+ - 'ee/spec/models/weight_note_spec.rb'
+ - 'ee/spec/models/zoekt/indexed_namespace_spec.rb'
+ - 'ee/spec/policies/epic_policy_spec.rb'
+ - 'ee/spec/policies/group_policy_spec.rb'
+ - 'ee/spec/policies/merge_request_policy_spec.rb'
+ - 'ee/spec/policies/project_policy_spec.rb'
+ - 'ee/spec/presenters/approval_rule_presenter_spec.rb'
+ - 'ee/spec/presenters/ci/build_runner_presenter_spec.rb'
+ - 'ee/spec/presenters/ci/pipeline_presenter_spec.rb'
+ - 'ee/spec/presenters/merge_request_approver_presenter_spec.rb'
+ - 'ee/spec/presenters/merge_request_presenter_spec.rb'
+ - 'ee/spec/presenters/subscription_presenter_spec.rb'
+ - 'ee/spec/presenters/vulnerabilities/scanner_presenter_spec.rb'
+ - 'ee/spec/presenters/vulnerability_presenter_spec.rb'
+ - 'ee/spec/replicators/geo/pipeline_replicator_spec.rb'
+ - 'ee/spec/requests/admin/user_permission_exports_controller_spec.rb'
+ - 'ee/spec/requests/api/audit_events_spec.rb'
+ - 'ee/spec/requests/api/ci/jobs_spec.rb'
+ - 'ee/spec/requests/api/ci/triggers_spec.rb'
+ - 'ee/spec/requests/api/epic_links_spec.rb'
+ - 'ee/spec/requests/api/geo_spec.rb'
+ - 'ee/spec/requests/api/graphql/ai_messages_spec.rb'
+ - 'ee/spec/requests/api/graphql/audit_events/streaming/headers/create_spec.rb'
+ - 'ee/spec/requests/api/graphql/audit_events/streaming/headers/destroy_spec.rb'
+ - 'ee/spec/requests/api/graphql/audit_events/streaming/headers/update_spec.rb'
+ - 'ee/spec/requests/api/graphql/audit_events/streaming/instance_headers/create_spec.rb'
+ - 'ee/spec/requests/api/graphql/audit_events/streaming/instance_headers/destroy_spec.rb'
+ - 'ee/spec/requests/api/graphql/audit_events/streaming/instance_headers/update_spec.rb'
+ - 'ee/spec/requests/api/graphql/boards/epic_list_query_spec.rb'
+ - 'ee/spec/requests/api/graphql/ci/minutes/usage_spec.rb'
+ - 'ee/spec/requests/api/graphql/environments/deployments_spec.rb'
+ - 'ee/spec/requests/api/graphql/explain_vulnerability_prompt_spec.rb'
+ - 'ee/spec/requests/api/graphql/member_role/group_member_role_spec.rb'
+ - 'ee/spec/requests/api/graphql/member_role/permissions_list_spec.rb'
+ - 'ee/spec/requests/api/graphql/member_role/project_member_role_spec.rb'
+ - 'ee/spec/requests/api/graphql/member_role/single_member_role_spec.rb'
+ - 'ee/spec/requests/api/graphql/milestone_spec.rb'
+ - 'ee/spec/requests/api/graphql/mutations/audit_events/amazon_s3_configurations/update_spec.rb'
+ - 'ee/spec/requests/api/graphql/mutations/audit_events/external_audit_event_destinations/create_spec.rb'
+ - 'ee/spec/requests/api/graphql/mutations/audit_events/google_cloud_logging_configurations/destroy_spec.rb'
+ - 'ee/spec/requests/api/graphql/mutations/audit_events/instance/google_cloud_logging_configurations/destroy_spec.rb'
+ - 'ee/spec/requests/api/graphql/mutations/audit_events/instance_external_audit_event_destinations/create_spec.rb'
+ - 'ee/spec/requests/api/graphql/mutations/boards/create_spec.rb'
+ - 'ee/spec/requests/api/graphql/mutations/boards/epic_boards/destroy_spec.rb'
+ - 'ee/spec/requests/api/graphql/mutations/boards/epic_boards/epic_move_list_spec.rb'
+ - 'ee/spec/requests/api/graphql/mutations/boards/epics/create_spec.rb'
+ - 'ee/spec/requests/api/graphql/mutations/ci/project_subscriptions/delete_spec.rb'
+ - 'ee/spec/requests/api/graphql/mutations/compliance_management/frameworks/create_spec.rb'
+ - 'ee/spec/requests/api/graphql/mutations/compliance_management/frameworks/destroy_spec.rb'
+ - 'ee/spec/requests/api/graphql/mutations/compliance_management/frameworks/update_spec.rb'
+ - 'ee/spec/requests/api/graphql/mutations/dast/profiles/create_spec.rb'
+ - 'ee/spec/requests/api/graphql/mutations/dast/profiles/delete_spec.rb'
+ - 'ee/spec/requests/api/graphql/mutations/dast/profiles/update_spec.rb'
+ - 'ee/spec/requests/api/graphql/mutations/dast_on_demand_scans/create_spec.rb'
+ - 'ee/spec/requests/api/graphql/mutations/dast_scanner_profiles/delete_spec.rb'
+ - 'ee/spec/requests/api/graphql/mutations/dast_scanner_profiles/update_spec.rb'
+ - 'ee/spec/requests/api/graphql/mutations/dast_site_profiles/create_spec.rb'
+ - 'ee/spec/requests/api/graphql/mutations/dast_site_profiles/delete_spec.rb'
+ - 'ee/spec/requests/api/graphql/mutations/dast_site_profiles/update_spec.rb'
+ - 'ee/spec/requests/api/graphql/mutations/dast_site_tokens/create_spec.rb'
+ - 'ee/spec/requests/api/graphql/mutations/dast_site_validations/create_spec.rb'
+ - 'ee/spec/requests/api/graphql/mutations/dast_site_validations/revoke_spec.rb'
+ - 'ee/spec/requests/api/graphql/mutations/dependency_proxy/packages/settings/update_spec.rb'
+ - 'ee/spec/requests/api/graphql/mutations/security_policy/assign_security_policy_project_spec.rb'
+ - 'ee/spec/requests/api/graphql/pipeline_security_report_finding_spec.rb'
+ - 'ee/spec/requests/api/graphql/project/dast_profile_schedule_spec.rb'
+ - 'ee/spec/requests/api/graphql/project/dast_profile_spec.rb'
+ - 'ee/spec/requests/api/graphql/project/dast_profiles_spec.rb'
+ - 'ee/spec/requests/api/graphql/project/dast_scanner_profiles_spec.rb'
+ - 'ee/spec/requests/api/graphql/project/dast_site_profiles_spec.rb'
+ - 'ee/spec/requests/api/graphql/project/dast_site_validations_spec.rb'
+ - 'ee/spec/requests/api/graphql/project/deployment_spec.rb'
+ - 'ee/spec/requests/api/graphql/project/environments_spec.rb'
+ - 'ee/spec/requests/api/graphql/project/pipeline/dast_profile_spec.rb'
+ - 'ee/spec/requests/api/graphql/project/pipelines/dast_profile_spec.rb'
+ - 'ee/spec/requests/api/graphql/project/product_analytics/events_stored_spec.rb'
+ - 'ee/spec/requests/api/graphql/project/product_analytics/product_analytics_spec.rb'
+ - 'ee/spec/requests/api/graphql/project/sbom/dependencies_spec.rb'
+ - 'ee/spec/requests/api/graphql/project/security_orchestration/scan_result_policy_spec.rb'
+ - 'ee/spec/requests/api/graphql/project/vulnerability_severities_count_spec.rb'
+ - 'ee/spec/requests/api/graphql/projects/compliance_frameworks_spec.rb'
+ - 'ee/spec/requests/api/graphql/vulnerabilities/details_spec.rb'
+ - 'ee/spec/requests/api/graphql/vulnerabilities/fields_spec.rb'
+ - 'ee/spec/requests/api/graphql/vulnerabilities/identifiers_spec.rb'
+ - 'ee/spec/requests/api/graphql/vulnerabilities/location_spec.rb'
+ - 'ee/spec/requests/api/graphql/vulnerabilities/primary_identifier_spec.rb'
+ - 'ee/spec/requests/api/graphql/vulnerabilities/scanner_spec.rb'
+ - 'ee/spec/requests/api/group_push_rule_spec.rb'
+ - 'ee/spec/requests/api/group_variables_spec.rb'
+ - 'ee/spec/requests/api/groups_spec.rb'
+ - 'ee/spec/requests/api/internal/app_sec/dast/site_validations_spec.rb'
+ - 'ee/spec/requests/api/internal/base_spec.rb'
+ - 'ee/spec/requests/api/internal/kubernetes_spec.rb'
+ - 'ee/spec/requests/api/internal/suggested_reviewers_spec.rb'
+ - 'ee/spec/requests/api/issues_spec.rb'
+ - 'ee/spec/requests/api/member_roles_spec.rb'
+ - 'ee/spec/requests/api/members_spec.rb'
+ - 'ee/spec/requests/api/merge_trains_spec.rb'
+ - 'ee/spec/requests/api/namespaces_spec.rb'
+ - 'ee/spec/requests/api/project_import_spec.rb'
+ - 'ee/spec/requests/api/projects_spec.rb'
+ - 'ee/spec/requests/api/releases_spec.rb'
+ - 'ee/spec/requests/api/remote_mirrors_spec.rb'
+ - 'ee/spec/requests/api/saml_group_links_spec.rb'
+ - 'ee/spec/requests/api/status_checks_spec.rb'
+ - 'ee/spec/requests/api/todos_spec.rb'
+ - 'ee/spec/requests/api/vulnerabilities_spec.rb'
+ - 'ee/spec/requests/git_http_geo_spec.rb'
+ - 'ee/spec/requests/groups/dependencies_controller_spec.rb'
+ - 'ee/spec/requests/groups/epics/epic_links_controller_spec.rb'
+ - 'ee/spec/requests/groups/group_members_controller_spec.rb'
+ - 'ee/spec/requests/groups/protected_branches_controller_spec.rb'
+ - 'ee/spec/requests/groups/protected_environments_controller_spec.rb'
+ - 'ee/spec/requests/groups/security/credentials_controller_spec.rb'
+ - 'ee/spec/requests/groups/settings/domain_verification_controller_spec.rb'
+ - 'ee/spec/requests/groups/settings/merge_requests_controller_spec.rb'
+ - 'ee/spec/requests/groups_controller_spec.rb'
+ - 'ee/spec/requests/projects/analytics/cycle_analytics/stages_controller_spec.rb'
+ - 'ee/spec/requests/projects/metrics_controller_spec.rb'
+ - 'ee/spec/requests/projects/pipelines_controller_spec.rb'
+ - 'ee/spec/requests/projects/requirements_management/requirements_controller_spec.rb'
+ - 'ee/spec/requests/projects/security/policies_controller_spec.rb'
+ - 'ee/spec/requests/projects/security/scanned_resources_controller_spec.rb'
+ - 'ee/spec/requests/projects/settings/analytics_controller_spec.rb'
+ - 'ee/spec/requests/projects/tracing_controller_spec.rb'
+ - 'ee/spec/requests/repositories/git_http_controller_spec.rb'
+ - 'ee/spec/requests/smartcard_controller_spec.rb'
+ - 'ee/spec/serializers/analytics/cycle_analytics/value_stream_errors_serializer_spec.rb'
+ - 'ee/spec/serializers/audit_event_entity_spec.rb'
+ - 'ee/spec/serializers/autocomplete/group_entity_spec.rb'
+ - 'ee/spec/serializers/autocomplete/iteration_entity_spec.rb'
+ - 'ee/spec/serializers/clusters/deployment_entity_spec.rb'
+ - 'ee/spec/serializers/clusters/environment_entity_spec.rb'
+ - 'ee/spec/serializers/dashboard_operations_project_entity_spec.rb'
+ - 'ee/spec/serializers/dependency_entity_spec.rb'
+ - 'ee/spec/serializers/ee/blob_entity_spec.rb'
+ - 'ee/spec/serializers/ee/build_details_entity_spec.rb'
+ - 'ee/spec/serializers/ee/ci/pipeline_entity_spec.rb'
+ - 'ee/spec/serializers/ee/deployment_entity_spec.rb'
+ - 'ee/spec/serializers/ee/evidences/release_entity_spec.rb'
+ - 'ee/spec/serializers/ee/issue_board_entity_spec.rb'
+ - 'ee/spec/serializers/ee/issue_entity_spec.rb'
+ - 'ee/spec/serializers/ee/issue_sidebar_extras_entity_spec.rb'
+ - 'ee/spec/serializers/ee/merge_request_poll_cached_widget_entity_spec.rb'
+ - 'ee/spec/serializers/ee/note_entity_spec.rb'
+ - 'ee/spec/serializers/environment_entity_spec.rb'
+ - 'ee/spec/serializers/epic_entity_spec.rb'
+ - 'ee/spec/serializers/epic_note_entity_spec.rb'
+ - 'ee/spec/serializers/evidences/build_artifact_entity_spec.rb'
+ - 'ee/spec/serializers/group_vulnerability_autocomplete_entity_spec.rb'
+ - 'ee/spec/serializers/incident_management/escalation_policy_entity_spec.rb'
+ - 'ee/spec/serializers/incident_management/oncall_schedule_entity_spec.rb'
+ - 'ee/spec/serializers/integrations/jira_serializers/issue_detail_entity_spec.rb'
+ - 'ee/spec/serializers/integrations/jira_serializers/issue_entity_spec.rb'
+ - 'ee/spec/serializers/integrations/jira_serializers/issue_serializer_spec.rb'
+ - 'ee/spec/serializers/integrations/zentao_serializers/issue_entity_spec.rb'
+ - 'ee/spec/serializers/license_compliance/collapsed_comparer_entity_spec.rb'
+ - 'ee/spec/serializers/license_compliance/comparer_entity_spec.rb'
+ - 'ee/spec/serializers/license_entity_spec.rb'
+ - 'ee/spec/serializers/merge_request_widget_entity_spec.rb'
+ - 'ee/spec/serializers/metrics_report_metric_entity_spec.rb'
+ - 'ee/spec/serializers/metrics_reports_comparer_entity_spec.rb'
+ - 'ee/spec/serializers/pipeline_serializer_spec.rb'
+ - 'ee/spec/serializers/productivity_analytics_merge_request_entity_spec.rb'
+ - 'ee/spec/serializers/project_mirror_entity_spec.rb'
+ - 'ee/spec/serializers/sbom/dependency_license_list_entity_spec.rb'
+ - 'ee/spec/serializers/sbom/sbom_entity_spec.rb'
+ - 'ee/spec/serializers/security/license_policy_entity_spec.rb'
+ - 'ee/spec/serializers/security/vulnerability_report_data_entity_spec.rb'
+ - 'ee/spec/serializers/test_reports_comparer_entity_spec.rb'
+ - 'ee/spec/serializers/test_reports_comparer_serializer_spec.rb'
+ - 'ee/spec/serializers/test_suite_comparer_entity_spec.rb'
+ - 'ee/spec/serializers/vulnerabilities/feedback_entity_spec.rb'
+ - 'ee/spec/serializers/vulnerabilities/finding_entity_spec.rb'
+ - 'ee/spec/serializers/vulnerabilities/finding_reports_comparer_entity_spec.rb'
+ - 'ee/spec/serializers/vulnerabilities/identifier_entity_spec.rb'
+ - 'ee/spec/serializers/vulnerabilities/request_entity_spec.rb'
+ - 'ee/spec/serializers/vulnerabilities/response_entity_spec.rb'
+ - 'ee/spec/serializers/vulnerabilities/scanner_entity_spec.rb'
+ - 'ee/spec/serializers/vulnerability_entity_spec.rb'
+ - 'ee/spec/serializers/vulnerability_note_entity_spec.rb'
+ - 'ee/spec/services/admin/email_service_spec.rb'
+ - 'ee/spec/services/ai/service_access_tokens_storage_service_spec.rb'
+ - 'ee/spec/services/analytics/cycle_analytics/stages/list_service_spec.rb'
+ - 'ee/spec/services/analytics/cycle_analytics/value_streams/update_service_spec.rb'
+ - 'ee/spec/services/analytics/devops_adoption/enabled_namespaces/find_or_create_service_spec.rb'
+ - 'ee/spec/services/analytics/devops_adoption/snapshots/calculate_and_save_service_spec.rb'
+ - 'ee/spec/services/analytics/devops_adoption/snapshots/create_service_spec.rb'
+ - 'ee/spec/services/analytics/devops_adoption/snapshots/update_service_spec.rb'
+ - 'ee/spec/services/analytics/forecasting/build_forecast_service_spec.rb'
+ - 'ee/spec/services/app_sec/dast/pipelines/find_latest_service_spec.rb'
+ - 'ee/spec/services/app_sec/dast/pre_scan_verification_steps/create_or_update_service_spec.rb'
+ - 'ee/spec/services/app_sec/dast/pre_scan_verification_steps/find_or_create_service_spec.rb'
+ - 'ee/spec/services/app_sec/dast/profiles/build_config_service_spec.rb'
+ - 'ee/spec/services/app_sec/dast/profiles/create_associations_service_spec.rb'
+ - 'ee/spec/services/app_sec/dast/profiles/create_service_spec.rb'
+ - 'ee/spec/services/app_sec/dast/profiles/destroy_service_spec.rb'
+ - 'ee/spec/services/app_sec/dast/profiles/update_service_spec.rb'
+ - 'ee/spec/services/app_sec/dast/scan_configs/fetch_service_spec.rb'
+ - 'ee/spec/services/app_sec/dast/scanner_profiles/create_service_spec.rb'
+ - 'ee/spec/services/app_sec/dast/scanner_profiles/destroy_service_spec.rb'
+ - 'ee/spec/services/app_sec/dast/scanner_profiles/update_service_spec.rb'
+ - 'ee/spec/services/app_sec/dast/scans/associate_service_spec.rb'
+ - 'ee/spec/services/app_sec/dast/scans/create_service_spec.rb'
+ - 'ee/spec/services/app_sec/dast/scans/run_service_spec.rb'
+ - 'ee/spec/services/app_sec/dast/site_profile_secret_variables/create_or_update_service_spec.rb'
+ - 'ee/spec/services/app_sec/dast/site_profile_secret_variables/destroy_service_spec.rb'
+ - 'ee/spec/services/app_sec/dast/site_profiles/create_service_spec.rb'
+ - 'ee/spec/services/app_sec/dast/site_profiles/destroy_service_spec.rb'
+ - 'ee/spec/services/app_sec/dast/site_profiles/update_service_spec.rb'
+ - 'ee/spec/services/app_sec/dast/site_tokens/find_or_create_service_spec.rb'
+ - 'ee/spec/services/app_sec/dast/site_validations/find_or_create_service_spec.rb'
+ - 'ee/spec/services/app_sec/dast/site_validations/revoke_service_spec.rb'
+ - 'ee/spec/services/app_sec/dast/site_validations/runner_service_spec.rb'
+ - 'ee/spec/services/app_sec/dast/sites/find_or_create_service_spec.rb'
+ - 'ee/spec/services/applications/create_service_spec.rb'
+ - 'ee/spec/services/approval_rules/create_service_spec.rb'
+ - 'ee/spec/services/approval_rules/finalize_service_spec.rb'
+ - 'ee/spec/services/approval_rules/project_rule_destroy_service_spec.rb'
+ - 'ee/spec/services/approval_rules/update_service_spec.rb'
+ - 'ee/spec/services/arkose/blocked_users_report_service_spec.rb'
+ - 'ee/spec/services/arkose/status_service_spec.rb'
+ - 'ee/spec/services/audit_events/register_runner_audit_event_service_spec.rb'
+ - 'ee/spec/services/audit_events/streaming/event_type_filters/create_service_spec.rb'
+ - 'ee/spec/services/audit_events/streaming/event_type_filters/destroy_service_spec.rb'
+ - 'ee/spec/services/audit_events/streaming/headers/update_service_spec.rb'
+ - 'ee/spec/services/auto_merge/add_to_merge_train_when_pipeline_succeeds_service_spec.rb'
+ - 'ee/spec/services/auto_merge/merge_train_service_spec.rb'
+ - 'ee/spec/services/boards/epics/list_service_spec.rb'
+ - 'ee/spec/services/boards/epics/move_service_spec.rb'
+ - 'ee/spec/services/boards/epics/position_create_service_spec.rb'
+ - 'ee/spec/services/ci/compare_license_scanning_reports_collapsed_service_spec.rb'
+ - 'ee/spec/services/ci/compare_license_scanning_reports_service_spec.rb'
+ - 'ee/spec/services/ci/compare_metrics_reports_service_spec.rb'
+ - 'ee/spec/services/ci/compare_security_reports_service_spec.rb'
+ - 'ee/spec/services/ci/create_pipeline_service/dast_configuration_spec.rb'
+ - 'ee/spec/services/ci/delete_project_subscription_service_spec.rb'
+ - 'ee/spec/services/ci/external_pull_requests/process_github_event_service_spec.rb'
+ - 'ee/spec/services/ci/llm/async_generate_config_service_spec.rb'
+ - 'ee/spec/services/ci/llm/generate_config_service_spec.rb'
+ - 'ee/spec/services/ci/minutes/refresh_cached_data_service_spec.rb'
+ - 'ee/spec/services/ci/minutes/reset_usage_service_spec.rb'
+ - 'ee/spec/services/ci/minutes/track_live_consumption_service_spec.rb'
+ - 'ee/spec/services/ci/minutes/update_build_minutes_service_spec.rb'
+ - 'ee/spec/services/ci/minutes/update_project_and_namespace_usage_service_spec.rb'
+ - 'ee/spec/services/ci/pipeline_bridge_status_service_spec.rb'
+ - 'ee/spec/services/ci/process_build_service_spec.rb'
+ - 'ee/spec/services/ci/runners/unregister_runner_service_spec.rb'
+ - 'ee/spec/services/ci/subscribe_bridge_service_spec.rb'
+ - 'ee/spec/services/ci/sync_reports_to_approval_rules_service_spec.rb'
+ - 'ee/spec/services/ci_cd/github_integration_setup_service_spec.rb'
+ - 'ee/spec/services/ci_cd/github_setup_service_spec.rb'
+ - 'ee/spec/services/ci_cd/setup_project_spec.rb'
+ - 'ee/spec/services/compliance_management/frameworks/create_service_spec.rb'
+ - 'ee/spec/services/compliance_management/frameworks/destroy_service_spec.rb'
+ - 'ee/spec/services/compliance_management/frameworks/update_service_spec.rb'
+ - 'ee/spec/services/compliance_management/merge_requests/create_compliance_violations_service_spec.rb'
+ - 'ee/spec/services/dashboard/operations/list_service_spec.rb'
+ - 'ee/spec/services/deploy_keys/create_service_spec.rb'
+ - 'ee/spec/services/deployments/approval_service_spec.rb'
+ - 'ee/spec/services/deployments/auto_rollback_service_spec.rb'
+ - 'ee/spec/services/dora/aggregate_metrics_service_spec.rb'
+ - 'ee/spec/services/dora/aggregate_scores_service_spec.rb'
+ - 'ee/spec/services/ee/admin/set_feature_flag_service_spec.rb'
+ - 'ee/spec/services/ee/alert_management/alerts/update_service_spec.rb'
+ - 'ee/spec/services/ee/allowed_email_domains/update_service_spec.rb'
+ - 'ee/spec/services/ee/auth/container_registry_authentication_service_spec.rb'
+ - 'ee/spec/services/ee/ci/job_artifacts/create_service_spec.rb'
+ - 'ee/spec/services/ee/ci/job_artifacts/destroy_all_expired_service_spec.rb'
+ - 'ee/spec/services/ee/ci/job_artifacts/destroy_batch_service_spec.rb'
+ - 'ee/spec/services/ee/deployments/update_environment_service_spec.rb'
+ - 'ee/spec/services/ee/git/branch_push_service_spec.rb'
+ - 'ee/spec/services/ee/gpg_keys/create_service_spec.rb'
+ - 'ee/spec/services/ee/gpg_keys/destroy_service_spec.rb'
+ - 'ee/spec/services/ee/groups/autocomplete_service_spec.rb'
+ - 'ee/spec/services/ee/groups/deploy_tokens/create_service_spec.rb'
+ - 'ee/spec/services/ee/groups/deploy_tokens/destroy_service_spec.rb'
+ - 'ee/spec/services/ee/groups/deploy_tokens/revoke_service_spec.rb'
+ - 'ee/spec/services/ee/groups/group_links/create_service_spec.rb'
+ - 'ee/spec/services/ee/groups/group_links/destroy_service_spec.rb'
+ - 'ee/spec/services/ee/groups/group_links/update_service_spec.rb'
+ - 'ee/spec/services/ee/ip_restrictions/update_service_spec.rb'
+ - 'ee/spec/services/ee/issuable/bulk_update_service_spec.rb'
+ - 'ee/spec/services/ee/issuable/common_system_notes_service_spec.rb'
+ - 'ee/spec/services/ee/issuable/destroy_service_spec.rb'
+ - 'ee/spec/services/ee/issue_links/create_service_spec.rb'
+ - 'ee/spec/services/ee/issues/clone_service_spec.rb'
+ - 'ee/spec/services/ee/issues/move_service_spec.rb'
+ - 'ee/spec/services/ee/issues/update_service_spec.rb'
+ - 'ee/spec/services/ee/keys/destroy_service_spec.rb'
+ - 'ee/spec/services/ee/members/create_service_spec.rb'
+ - 'ee/spec/services/ee/members/creator_service_spec.rb'
+ - 'ee/spec/services/ee/members/destroy_service_spec.rb'
+ - 'ee/spec/services/ee/merge_request_metrics_service_spec.rb'
+ - 'ee/spec/services/ee/merge_requests/base_service_spec.rb'
+ - 'ee/spec/services/ee/merge_requests/create_pipeline_service_spec.rb'
+ - 'ee/spec/services/ee/merge_requests/post_merge_service_spec.rb'
+ - 'ee/spec/services/ee/merge_requests/refresh_service_spec.rb'
+ - 'ee/spec/services/ee/notes/post_process_service_spec.rb'
+ - 'ee/spec/services/ee/notification_service_spec.rb'
+ - 'ee/spec/services/ee/null_notification_service_spec.rb'
+ - 'ee/spec/services/ee/personal_access_tokens/revoke_service_spec.rb'
+ - 'ee/spec/services/ee/post_receive_service_spec.rb'
+ - 'ee/spec/services/ee/projects/autocomplete_service_spec.rb'
+ - 'ee/spec/services/ee/projects/deploy_tokens/create_service_spec.rb'
+ - 'ee/spec/services/ee/projects/deploy_tokens/destroy_service_spec.rb'
+ - 'ee/spec/services/ee/projects/unlink_fork_service_spec.rb'
+ - 'ee/spec/services/ee/resource_events/change_labels_service_spec.rb'
+ - 'ee/spec/services/ee/system_notes/issuables_service_spec.rb'
+ - 'ee/spec/services/ee/terraform/states/destroy_service_spec.rb'
+ - 'ee/spec/services/ee/todos/destroy/entity_leave_service_spec.rb'
+ - 'ee/spec/services/ee/users/build_service_spec.rb'
+ - 'ee/spec/services/ee/work_items/import_csv_service_spec.rb'
+ - 'ee/spec/services/elastic/data_migration_service_spec.rb'
+ - 'ee/spec/services/elastic/index_projects_service_spec.rb'
+ - 'ee/spec/services/elastic/indexing_control_service_spec.rb'
+ - 'ee/spec/services/elastic/metrics_update_service_spec.rb'
+ - 'ee/spec/services/epic_issues/create_service_spec.rb'
+ - 'ee/spec/services/epic_issues/destroy_service_spec.rb'
+ - 'ee/spec/services/epic_issues/list_service_spec.rb'
+ - 'ee/spec/services/epic_issues/update_service_spec.rb'
+ - 'ee/spec/services/epics/close_service_spec.rb'
+ - 'ee/spec/services/epics/create_service_spec.rb'
+ - 'ee/spec/services/epics/epic_links/create_service_spec.rb'
+ - 'ee/spec/services/epics/epic_links/list_service_spec.rb'
+ - 'ee/spec/services/epics/epic_links/update_service_spec.rb'
+ - 'ee/spec/services/epics/issue_promote_service_spec.rb'
+ - 'ee/spec/services/epics/related_epic_links/destroy_service_spec.rb'
+ - 'ee/spec/services/epics/related_epic_links/list_service_spec.rb'
+ - 'ee/spec/services/epics/reopen_service_spec.rb'
+ - 'ee/spec/services/epics/tree_reorder_service_spec.rb'
+ - 'ee/spec/services/epics/update_service_spec.rb'
+ - 'ee/spec/services/external_status_checks/create_service_spec.rb'
+ - 'ee/spec/services/external_status_checks/destroy_service_spec.rb'
+ - 'ee/spec/services/external_status_checks/dispatch_service_spec.rb'
+ - 'ee/spec/services/external_status_checks/retry_service_spec.rb'
+ - 'ee/spec/services/external_status_checks/update_service_spec.rb'
+ - 'ee/spec/services/geo/base_file_service_spec.rb'
+ - 'ee/spec/services/geo/blob_download_service_spec.rb'
+ - 'ee/spec/services/geo/blob_upload_service_spec.rb'
+ - 'ee/spec/services/geo/cache_invalidation_event_store_spec.rb'
+ - 'ee/spec/services/geo/container_repository_sync_service_spec.rb'
+ - 'ee/spec/services/geo/container_repository_sync_spec.rb'
+ - 'ee/spec/services/geo/event_service_spec.rb'
+ - 'ee/spec/services/geo/file_registry_removal_service_spec.rb'
+ - 'ee/spec/services/geo/framework_repository_sync_service_spec.rb'
+ - 'ee/spec/services/geo/graphql_request_service_spec.rb'
+ - 'ee/spec/services/geo/hashed_storage_attachments_event_store_spec.rb'
+ - 'ee/spec/services/geo/metrics_update_service_spec.rb'
+ - 'ee/spec/services/geo/replication_toggle_request_service_spec.rb'
+ - 'ee/spec/services/gitlab_subscriptions/activate_service_spec.rb'
+ - 'ee/spec/services/gitlab_subscriptions/reconciliations/calculate_seat_count_data_service_spec.rb'
+ - 'ee/spec/services/gitlab_subscriptions/user_add_on_assignments/create_service_spec.rb'
+ - 'ee/spec/services/group_saml/identity/destroy_service_spec.rb'
+ - 'ee/spec/services/groups/compliance_report_csv_service_spec.rb'
+ - 'ee/spec/services/groups/destroy_service_spec.rb'
+ - 'ee/spec/services/groups/epics_count_service_spec.rb'
+ - 'ee/spec/services/groups/mark_for_deletion_service_spec.rb'
+ - 'ee/spec/services/groups/restore_service_spec.rb'
+ - 'ee/spec/services/groups/transfer_service_spec.rb'
+ - 'ee/spec/services/groups/update_repository_storage_service_spec.rb'
+ - 'ee/spec/services/groups/update_service_spec.rb'
+ - 'ee/spec/services/ide/schemas_config_service_spec.rb'
+ - 'ee/spec/services/incident_management/incidents/create_sla_service_spec.rb'
+ - 'ee/spec/services/incident_management/oncall_rotations/edit_service_spec.rb'
+ - 'ee/spec/services/issue_feature_flags/list_service_spec.rb'
+ - 'ee/spec/services/issues/duplicate_service_spec.rb'
+ - 'ee/spec/services/iterations/cadences/create_iterations_in_advance_service_spec.rb'
+ - 'ee/spec/services/iterations/roll_over_issues_service_spec.rb'
+ - 'ee/spec/services/iterations/update_service_spec.rb'
+ - 'ee/spec/services/jira/jql_builder_service_spec.rb'
+ - 'ee/spec/services/jira/requests/issues/list_service_spec.rb'
+ - 'ee/spec/services/keys/create_service_spec.rb'
+ - 'ee/spec/services/lfs/lock_file_service_spec.rb'
+ - 'ee/spec/services/lfs/unlock_file_service_spec.rb'
+ - 'ee/spec/services/llm/analyze_ci_job_failure_service_spec.rb'
+ - 'ee/spec/services/llm/chat_service_spec.rb'
+ - 'ee/spec/services/llm/execute_method_service_spec.rb'
+ - 'ee/spec/services/llm/explain_code_service_spec.rb'
+ - 'ee/spec/services/llm/explain_vulnerability_service_spec.rb'
+ - 'ee/spec/services/llm/generate_commit_message_service_spec.rb'
+ - 'ee/spec/services/llm/generate_test_file_service_spec.rb'
+ - 'ee/spec/services/llm/git_command_service_spec.rb'
+ - 'ee/spec/services/llm/resolve_vulnerability_service_spec.rb'
+ - 'ee/spec/services/llm/tanuki_bot_service_spec.rb'
+ - 'ee/spec/services/member_roles/create_service_spec.rb'
+ - 'ee/spec/services/merge_request_approval_settings/update_service_spec.rb'
+ - 'ee/spec/services/merge_requests/merge_service_spec.rb'
+ - 'ee/spec/services/merge_requests/mergeability/check_external_status_checks_passed_service_spec.rb'
+ - 'ee/spec/services/merge_requests/reload_merge_head_diff_service_spec.rb'
+ - 'ee/spec/services/merge_trains/add_merge_request_service_spec.rb'
+ - 'ee/spec/services/merge_trains/check_status_service_spec.rb'
+ - 'ee/spec/services/merge_trains/refresh_merge_request_service_spec.rb'
+ - 'ee/spec/services/merge_trains/refresh_service_spec.rb'
+ - 'ee/spec/services/notes/create_visual_review_service_spec.rb'
+ - 'ee/spec/services/onboarding/create_iterable_trigger_service_spec.rb'
+ - 'ee/spec/services/personal_access_tokens/groups/update_lifetime_service_spec.rb'
+ - 'ee/spec/services/personal_access_tokens/instance/update_lifetime_service_spec.rb'
+ - 'ee/spec/services/personal_access_tokens/revoke_service_audit_log_spec.rb'
+ - 'ee/spec/services/product_analytics/initialize_stack_service_spec.rb'
+ - 'ee/spec/services/projects/alerting/notify_service_spec.rb'
+ - 'ee/spec/services/projects/create_from_template_service_spec.rb'
+ - 'ee/spec/services/projects/create_service_spec.rb'
+ - 'ee/spec/services/projects/destroy_service_spec.rb'
+ - 'ee/spec/services/projects/disable_legacy_inactive_projects_service_spec.rb'
+ - 'ee/spec/services/projects/fork_service_spec.rb'
+ - 'ee/spec/services/projects/gitlab_projects_import_service_spec.rb'
+ - 'ee/spec/services/projects/group_links/create_service_spec.rb'
+ - 'ee/spec/services/projects/group_links/destroy_service_spec.rb'
+ - 'ee/spec/services/projects/hashed_storage/migrate_attachments_service_spec.rb'
+ - 'ee/spec/services/projects/import_export/export_service_spec.rb'
+ - 'ee/spec/services/projects/import_service_spec.rb'
+ - 'ee/spec/services/projects/mark_for_deletion_service_spec.rb'
+ - 'ee/spec/services/projects/restore_service_spec.rb'
+ - 'ee/spec/services/projects/setup_ci_cd_spec.rb'
+ - 'ee/spec/services/projects/transfer_service_spec.rb'
+ - 'ee/spec/services/projects/update_mirror_service_spec.rb'
+ - 'ee/spec/services/protected_environments/create_service_spec.rb'
+ - 'ee/spec/services/protected_environments/destroy_service_spec.rb'
+ - 'ee/spec/services/protected_environments/environment_dropdown_service_spec.rb'
+ - 'ee/spec/services/protected_environments/search_service_spec.rb'
+ - 'ee/spec/services/protected_environments/update_service_spec.rb'
+ - 'ee/spec/services/push_rules/create_or_update_service_spec.rb'
+ - 'ee/spec/services/quality_management/test_cases/create_service_spec.rb'
+ - 'ee/spec/services/requirements_management/export_csv_service_spec.rb'
+ - 'ee/spec/services/requirements_management/prepare_import_csv_service_spec.rb'
+ - 'ee/spec/services/requirements_management/process_test_reports_service_spec.rb'
+ - 'ee/spec/services/resource_access_tokens/revoke_service_spec.rb'
+ - 'ee/spec/services/resource_events/change_weight_service_spec.rb'
+ - 'ee/spec/services/search/reindexing_service_spec.rb'
+ - 'ee/spec/services/security/dependency_list_service_spec.rb'
+ - 'ee/spec/services/security/ingestion/tasks/ingest_vulnerabilities/create_spec.rb'
+ - 'ee/spec/services/security/scanned_resources_service_spec.rb'
+ - 'ee/spec/services/security/security_orchestration_policies/ci_configuration_service_spec.rb'
+ - 'ee/spec/services/security/security_orchestration_policies/create_pipeline_service_spec.rb'
+ - 'ee/spec/services/security/security_orchestration_policies/fetch_policy_approvers_service_spec.rb'
+ - 'ee/spec/services/security/security_orchestration_policies/process_scan_result_policy_service_spec.rb'
+ - 'ee/spec/services/security/security_orchestration_policies/scan_pipeline_service_spec.rb'
+ - 'ee/spec/services/security/security_orchestration_policies/sync_opened_merge_requests_service_spec.rb'
+ - 'ee/spec/services/security/security_orchestration_policies/sync_scan_result_policies_service_spec.rb'
+ - 'ee/spec/services/security/token_revocation_service_spec.rb'
+ - 'ee/spec/services/security/track_scan_service_spec.rb'
+ - 'ee/spec/services/security/training_providers/base_url_service_spec.rb'
+ - 'ee/spec/services/security/training_urls_service_spec.rb'
+ - 'ee/spec/services/security/vulnerability_scanning/finding_map_spec.rb'
+ - 'ee/spec/services/sitemap/create_service_spec.rb'
+ - 'ee/spec/services/software_license_policies/create_service_spec.rb'
+ - 'ee/spec/services/status_page/mark_for_publication_service_spec.rb'
+ - 'ee/spec/services/status_page/publish_attachments_service_spec.rb'
+ - 'ee/spec/services/status_page/publish_details_service_spec.rb'
+ - 'ee/spec/services/status_page/trigger_publish_service_spec.rb'
+ - 'ee/spec/services/system_notes/epics_service_spec.rb'
+ - 'ee/spec/services/system_notes/escalations_service_spec.rb'
+ - 'ee/spec/services/system_notes/merge_requests_service_spec.rb'
+ - 'ee/spec/services/system_notes/merge_train_service_spec.rb'
+ - 'ee/spec/services/system_notes/vulnerabilities_service_spec.rb'
+ - 'ee/spec/services/todos/destroy/confidential_epic_service_spec.rb'
+ - 'ee/spec/services/users/email_verification/send_custom_confirmation_instructions_service_spec.rb'
+ - 'ee/spec/services/vulnerabilities/create_service_spec.rb'
+ - 'ee/spec/services/vulnerabilities/find_or_create_from_security_finding_service_spec.rb'
+ - 'ee/spec/services/vulnerabilities/findings/find_or_create_from_security_finding_service_spec.rb'
+ - 'ee/spec/services/vulnerabilities/manually_create_service_spec.rb'
+ - 'ee/spec/services/vulnerabilities/security_finding/create_issue_service_spec.rb'
+ - 'ee/spec/services/vulnerabilities/security_finding/create_merge_request_service_spec.rb'
+ - 'ee/spec/services/vulnerabilities/starboard_vulnerability_create_service_spec.rb'
+ - 'ee/spec/services/vulnerabilities/update_service_spec.rb'
+ - 'ee/spec/services/vulnerability_external_issue_links/create_service_spec.rb'
+ - 'ee/spec/services/vulnerability_feedback/create_service_spec.rb'
+ - 'ee/spec/services/work_items/update_service_spec.rb'
+ - 'ee/spec/services/work_items/widgets/health_status_service/update_service_spec.rb'
+ - 'ee/spec/services/work_items/widgets/iteration_service/update_service_spec.rb'
+ - 'ee/spec/services/work_items/widgets/progress_service/update_service_spec.rb'
+ - 'ee/spec/services/work_items/widgets/status_service/update_service_spec.rb'
+ - 'ee/spec/services/work_items/widgets/weight_service/update_service_spec.rb'
+ - 'ee/spec/support/shared_contexts/audit_event_not_licensed_shared_context.rb'
+ - 'ee/spec/support/shared_examples/controllers/analytics/cycle_analytics/shared_stage_shared_examples.rb'
+ - 'ee/spec/tasks/gitlab/elastic_rake_spec.rb'
+ - 'ee/spec/tasks/gitlab/license_rake_spec.rb'
+ - 'ee/spec/tasks/gitlab/seed/group_seed_rake_spec.rb'
+ - 'ee/spec/tasks/gitlab/spdx_rake_spec.rb'
+ - 'ee/spec/validators/user_existence_validator_spec.rb'
+ - 'ee/spec/validators/user_id_existence_validator_spec.rb'
+ - 'ee/spec/views/devise/registrations/new.html.haml_spec.rb'
+ - 'ee/spec/views/layouts/header/_current_user_dropdown.html.haml_spec.rb'
+ - 'ee/spec/workers/active_user_count_threshold_worker_spec.rb'
+ - 'ee/spec/workers/admin_emails_worker_spec.rb'
+ - 'ee/spec/workers/app_sec/dast/profile_schedule_worker_spec.rb'
+ - 'ee/spec/workers/approval_rules/external_approval_rule_payload_worker_spec.rb'
+ - 'ee/spec/workers/audit_events/audit_event_streaming_worker_spec.rb'
+ - 'ee/spec/workers/audit_events/user_impersonation_event_create_worker_spec.rb'
+ - 'ee/spec/workers/ci/initial_pipeline_process_worker_spec.rb'
+ - 'ee/spec/workers/ci/llm/generate_config_worker_spec.rb'
+ - 'ee/spec/workers/ci/minutes/refresh_cached_data_worker_spec.rb'
+ - 'ee/spec/workers/ci/minutes/update_project_and_namespace_usage_worker_spec.rb'
+ - 'ee/spec/workers/ci/runners/stale_group_runners_prune_cron_worker_spec.rb'
+ - 'ee/spec/workers/compliance_management/chain_of_custody_report_worker_spec.rb'
+ - 'ee/spec/workers/concerns/elastic/migration_helper_spec.rb'
+ - 'ee/spec/workers/concerns/elastic/migration_obsolete_spec.rb'
+ - 'ee/spec/workers/concerns/elastic/migration_options_spec.rb'
+ - 'ee/spec/workers/concerns/elastic/migration_remove_fields_helper_spec.rb'
+ - 'ee/spec/workers/create_github_webhook_worker_spec.rb'
+ - 'ee/spec/workers/deployments/auto_rollback_worker_spec.rb'
+ - 'ee/spec/workers/dora/daily_metrics/refresh_worker_spec.rb'
+ - 'ee/spec/workers/ee/arkose/blocked_users_report_worker_spec.rb'
+ - 'ee/spec/workers/ee/issuable/related_links_create_worker_spec.rb'
+ - 'ee/spec/workers/ee/issuable_export_csv_worker_spec.rb'
+ - 'ee/spec/workers/ee/repository_check/batch_worker_spec.rb'
+ - 'ee/spec/workers/elastic/migration_worker_spec.rb'
+ - 'ee/spec/workers/elastic/namespace_update_worker_spec.rb'
+ - 'ee/spec/workers/elastic_association_indexer_worker_spec.rb'
+ - 'ee/spec/workers/elastic_cluster_reindexing_cron_worker_spec.rb'
+ - 'ee/spec/workers/elastic_commit_indexer_worker_spec.rb'
+ - 'ee/spec/workers/elastic_delete_project_worker_spec.rb'
+ - 'ee/spec/workers/elastic_full_index_worker_spec.rb'
+ - 'ee/spec/workers/elastic_indexing_control_worker_spec.rb'
+ - 'ee/spec/workers/elastic_namespace_indexer_worker_spec.rb'
+ - 'ee/spec/workers/elastic_namespace_rollout_worker_spec.rb'
+ - 'ee/spec/workers/elastic_remove_expired_namespace_subscriptions_from_index_cron_worker_spec.rb'
+ - 'ee/spec/workers/geo/create_repository_updated_event_worker_spec.rb'
+ - 'ee/spec/workers/geo/destroy_worker_spec.rb'
+ - 'ee/spec/workers/geo/registry_sync_worker_spec.rb'
+ - 'ee/spec/workers/geo/scheduler/scheduler_worker_spec.rb'
+ - 'ee/spec/workers/geo/secondary/registry_consistency_worker_spec.rb'
+ - 'ee/spec/workers/geo/secondary_usage_data_cron_worker_spec.rb'
+ - 'ee/spec/workers/geo/sync_timeout_cron_worker_spec.rb'
+ - 'ee/spec/workers/geo/verification_cron_worker_spec.rb'
+ - 'ee/spec/workers/geo/verification_state_backfill_worker_spec.rb'
+ - 'ee/spec/workers/geo/verification_timeout_worker_spec.rb'
+ - 'ee/spec/workers/gitlab_subscriptions/add_on_purchases/bulk_refresh_user_assignments_worker_spec.rb'
+ - 'ee/spec/workers/gitlab_subscriptions/add_on_purchases/cleanup_user_add_on_assignment_worker_spec.rb'
+ - 'ee/spec/workers/gitlab_subscriptions/add_on_purchases/refresh_user_assignments_worker_spec.rb'
+ - 'ee/spec/workers/gitlab_subscriptions/add_on_purchases/schedule_bulk_refresh_user_assignments_worker_spec.rb'
+ - 'ee/spec/workers/gitlab_subscriptions/schedule_refresh_seats_worker_spec.rb'
+ - 'ee/spec/workers/historical_data_worker_spec.rb'
+ - 'ee/spec/workers/import_software_licenses_worker_spec.rb'
+ - 'ee/spec/workers/incident_management/apply_incident_sla_exceeded_label_worker_spec.rb'
+ - 'ee/spec/workers/incident_management/pending_escalations/alert_check_worker_spec.rb'
+ - 'ee/spec/workers/incident_management/pending_escalations/alert_create_worker_spec.rb'
+ - 'ee/spec/workers/incident_management/pending_escalations/issue_check_worker_spec.rb'
+ - 'ee/spec/workers/incident_management/pending_escalations/issue_create_worker_spec.rb'
+ - 'ee/spec/workers/incident_management/pending_escalations/schedule_check_cron_worker_spec.rb'
+ - 'ee/spec/workers/ldap_all_groups_sync_worker_spec.rb'
+ - 'ee/spec/workers/ldap_group_sync_worker_spec.rb'
+ - 'ee/spec/workers/ldap_sync_worker_spec.rb'
+ - 'ee/spec/workers/llm/completion_worker_spec.rb'
+ - 'ee/spec/workers/members_destroyer/clean_up_group_protected_branch_rules_worker_spec.rb'
+ - 'ee/spec/workers/merge_requests/capture_suggested_reviewers_accepted_worker_spec.rb'
+ - 'ee/spec/workers/merge_requests/fetch_suggested_reviewers_worker_spec.rb'
+ - 'ee/spec/workers/merge_requests/sync_code_owner_approval_rules_worker_spec.rb'
+ - 'ee/spec/workers/personal_access_tokens/groups/policy_worker_spec.rb'
+ - 'ee/spec/workers/personal_access_tokens/instance/policy_worker_spec.rb'
+ - 'ee/spec/workers/product_analytics/initialize_snowplow_product_analytics_worker_spec.rb'
+ - 'ee/spec/workers/project_import_schedule_worker_spec.rb'
+ - 'ee/spec/workers/projects/deregister_suggested_reviewers_project_worker_spec.rb'
+ - 'ee/spec/workers/projects/disable_legacy_open_source_license_for_inactive_projects_worker_spec.rb'
+ - 'ee/spec/workers/projects/register_suggested_reviewers_project_worker_spec.rb'
+ - 'ee/spec/workers/repository_import_worker_spec.rb'
+ - 'ee/spec/workers/repository_update_mirror_worker_spec.rb'
+ - 'ee/spec/workers/requirements_management/import_requirements_csv_worker_spec.rb'
+ - 'ee/spec/workers/requirements_management/process_requirements_reports_worker_spec.rb'
+ - 'ee/spec/workers/search/index_curation_worker_spec.rb'
+ - 'ee/spec/workers/search/zoekt/delete_project_worker_spec.rb'
+ - 'ee/spec/workers/search/zoekt/namespace_indexer_worker_spec.rb'
+ - 'ee/spec/workers/security/scan_execution_policies/rule_schedule_worker_spec.rb'
+ - 'ee/spec/workers/security/scans/purge_by_job_id_worker_spec.rb'
+ - 'ee/spec/workers/security/track_secure_scans_worker_spec.rb'
+ - 'ee/spec/workers/set_user_status_based_on_user_cap_setting_worker_spec.rb'
+ - 'ee/spec/workers/status_page/publish_worker_spec.rb'
+ - 'ee/spec/workers/sync_seat_link_worker_spec.rb'
+ - 'ee/spec/workers/update_all_mirrors_worker_spec.rb'
+ - 'ee/spec/workers/vulnerabilities/mark_dropped_as_resolved_worker_spec.rb'
+ - 'ee/spec/workers/vulnerabilities/update_namespace_ids_of_vulnerability_reads_worker_spec.rb'
+ - 'ee/spec/workers/zoekt/indexer_worker_spec.rb'
+ - 'qa/spec/ee/runtime/geo_spec.rb'
+ - 'qa/spec/factory/resource/user_spec.rb'
+ - 'qa/spec/page/base_spec.rb'
+ - 'qa/spec/page/element_spec.rb'
+ - 'qa/spec/page/logging_spec.rb'
+ - 'qa/spec/page/validator_spec.rb'
+ - 'qa/spec/page/view_spec.rb'
+ - 'qa/spec/resource/api_fabricator_spec.rb'
+ - 'qa/spec/resource/base_spec.rb'
+ - 'qa/spec/resource/events/base_spec.rb'
+ - 'qa/spec/resource/events/project_spec.rb'
+ - 'qa/spec/resource/repository/push_spec.rb'
+ - 'qa/spec/resource/ssh_key_spec.rb'
+ - 'qa/spec/resource/user_spec.rb'
+ - 'qa/spec/runtime/api/client_spec.rb'
+ - 'qa/spec/runtime/key/ed25519_spec.rb'
+ - 'qa/spec/runtime/key/rsa_spec.rb'
+ - 'qa/spec/runtime/release_spec.rb'
+ - 'qa/spec/runtime/scenario_spec.rb'
+ - 'qa/spec/scenario/actable_spec.rb'
+ - 'qa/spec/scenario/bootable_spec.rb'
+ - 'qa/spec/scenario/template_spec.rb'
+ - 'qa/spec/service/docker_run/gitlab_runner_spec.rb'
+ - 'qa/spec/service/docker_run/k3s_spec.rb'
+ - 'qa/spec/service/shellout_spec.rb'
+ - 'qa/spec/specs/parallel_runner_spec.rb'
+ - 'qa/spec/specs/runner_spec.rb'
+ - 'qa/spec/support/helpers/masker_spec.rb'
+ - 'qa/spec/support/repeater_spec.rb'
+ - 'qa/spec/support/retrier_spec.rb'
+ - 'qa/spec/support/system_logs/sentry_spec.rb'
+ - 'qa/spec/support/wait_for_requests_spec.rb'
+ - 'qa/spec/support/waiter_spec.rb'
+ - 'qa/spec/vendor/smocker_api_spec.rb'
+ - 'spec/bin/feature_flag_spec.rb'
+ - 'spec/components/diffs/overflow_warning_component_spec.rb'
+ - 'spec/components/diffs/stats_component_spec.rb'
+ - 'spec/components/pajamas/banner_component_spec.rb'
+ - 'spec/components/pajamas/component_spec.rb'
+ - 'spec/config/object_store_settings_spec.rb'
+ - 'spec/contracts/provider_specs/helpers/provider/contract_source_helper_spec.rb'
+ - 'spec/controllers/admin/application_settings_controller_spec.rb'
+ - 'spec/controllers/admin/applications_controller_spec.rb'
+ - 'spec/controllers/admin/ci/variables_controller_spec.rb'
+ - 'spec/controllers/admin/instance_review_controller_spec.rb'
+ - 'spec/controllers/admin/integrations_controller_spec.rb'
+ - 'spec/controllers/admin/users_controller_spec.rb'
+ - 'spec/controllers/application_controller_spec.rb'
+ - 'spec/controllers/concerns/check_rate_limit_spec.rb'
+ - 'spec/controllers/concerns/graceful_timeout_handling_spec.rb'
+ - 'spec/controllers/concerns/page_limiter_spec.rb'
+ - 'spec/controllers/concerns/preferred_language_switcher_spec.rb'
+ - 'spec/controllers/concerns/product_analytics_tracking_spec.rb'
+ - 'spec/controllers/concerns/renders_commits_spec.rb'
+ - 'spec/controllers/concerns/routable_actions_spec.rb'
+ - 'spec/controllers/concerns/send_file_upload_spec.rb'
+ - 'spec/controllers/concerns/sorting_preference_spec.rb'
+ - 'spec/controllers/concerns/spammable_actions/akismet_mark_as_spam_action_spec.rb'
+ - 'spec/controllers/concerns/spammable_actions/captcha_check/html_format_actions_support_spec.rb'
+ - 'spec/controllers/concerns/spammable_actions/captcha_check/json_format_actions_support_spec.rb'
+ - 'spec/controllers/dashboard/labels_controller_spec.rb'
+ - 'spec/controllers/dashboard/projects_controller_spec.rb'
+ - 'spec/controllers/google_api/authorizations_controller_spec.rb'
+ - 'spec/controllers/graphql_controller_spec.rb'
+ - 'spec/controllers/groups/dependency_proxies_controller_spec.rb'
+ - 'spec/controllers/groups/dependency_proxy_auth_controller_spec.rb'
+ - 'spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb'
+ - 'spec/controllers/groups/group_links_controller_spec.rb'
+ - 'spec/controllers/groups/group_members_controller_spec.rb'
+ - 'spec/controllers/groups/milestones_controller_spec.rb'
+ - 'spec/controllers/groups/releases_controller_spec.rb'
+ - 'spec/controllers/groups/settings/applications_controller_spec.rb'
+ - 'spec/controllers/groups/settings/ci_cd_controller_spec.rb'
+ - 'spec/controllers/groups/settings/integrations_controller_spec.rb'
+ - 'spec/controllers/groups/settings/repository_controller_spec.rb'
+ - 'spec/controllers/groups/variables_controller_spec.rb'
+ - 'spec/controllers/groups_controller_spec.rb'
+ - 'spec/controllers/help_controller_spec.rb'
+ - 'spec/controllers/import/bitbucket_controller_spec.rb'
+ - 'spec/controllers/jira_connect/events_controller_spec.rb'
+ - 'spec/controllers/jira_connect/subscriptions_controller_spec.rb'
+ - 'spec/controllers/ldap/omniauth_callbacks_controller_spec.rb'
+ - 'spec/controllers/oauth/applications_controller_spec.rb'
+ - 'spec/controllers/oauth/authorizations_controller_spec.rb'
+ - 'spec/controllers/omniauth_callbacks_controller_spec.rb'
+ - 'spec/controllers/passwords_controller_spec.rb'
+ - 'spec/controllers/profiles/emails_controller_spec.rb'
+ - 'spec/controllers/profiles/preferences_controller_spec.rb'
+ - 'spec/controllers/profiles/slacks_controller_spec.rb'
+ - 'spec/controllers/profiles/two_factor_auths_controller_spec.rb'
+ - 'spec/controllers/profiles/webauthn_registrations_controller_spec.rb'
+ - 'spec/controllers/projects/analytics/cycle_analytics/summary_controller_spec.rb'
+ - 'spec/controllers/projects/artifacts_controller_spec.rb'
+ - 'spec/controllers/projects/avatars_controller_spec.rb'
+ - 'spec/controllers/projects/blob_controller_spec.rb'
+ - 'spec/controllers/projects/branches_controller_spec.rb'
+ - 'spec/controllers/projects/ci/lints_controller_spec.rb'
+ - 'spec/controllers/projects/deploy_keys_controller_spec.rb'
+ - 'spec/controllers/projects/design_management/designs/raw_images_controller_spec.rb'
+ - 'spec/controllers/projects/environments_controller_spec.rb'
+ - 'spec/controllers/projects/feature_flags_controller_spec.rb'
+ - 'spec/controllers/projects/forks_controller_spec.rb'
+ - 'spec/controllers/projects/graphs_controller_spec.rb'
+ - 'spec/controllers/projects/issues_controller_spec.rb'
+ - 'spec/controllers/projects/jobs_controller_spec.rb'
+ - 'spec/controllers/projects/mattermosts_controller_spec.rb'
+ - 'spec/controllers/projects/merge_requests/diffs_controller_spec.rb'
+ - 'spec/controllers/projects/merge_requests_controller_spec.rb'
+ - 'spec/controllers/projects/milestones_controller_spec.rb'
+ - 'spec/controllers/projects/notes_controller_spec.rb'
+ - 'spec/controllers/projects/pages_controller_spec.rb'
+ - 'spec/controllers/projects/pages_domains_controller_spec.rb'
+ - 'spec/controllers/projects/pipelines_controller_spec.rb'
+ - 'spec/controllers/projects/protected_branches_controller_spec.rb'
+ - 'spec/controllers/projects/raw_controller_spec.rb'
+ - 'spec/controllers/projects/refs_controller_spec.rb'
+ - 'spec/controllers/projects/releases/evidences_controller_spec.rb'
+ - 'spec/controllers/projects/releases_controller_spec.rb'
+ - 'spec/controllers/projects/settings/ci_cd_controller_spec.rb'
+ - 'spec/controllers/projects/settings/integration_hook_logs_controller_spec.rb'
+ - 'spec/controllers/projects/settings/repository_controller_spec.rb'
+ - 'spec/controllers/projects/snippets/blobs_controller_spec.rb'
+ - 'spec/controllers/projects/snippets_controller_spec.rb'
+ - 'spec/controllers/projects/tags_controller_spec.rb'
+ - 'spec/controllers/projects/terraform_controller_spec.rb'
+ - 'spec/controllers/projects/tree_controller_spec.rb'
+ - 'spec/controllers/projects/usage_quotas_controller_spec.rb'
+ - 'spec/controllers/projects/variables_controller_spec.rb'
+ - 'spec/controllers/projects/web_ide_schemas_controller_spec.rb'
+ - 'spec/controllers/projects/web_ide_terminals_controller_spec.rb'
+ - 'spec/controllers/projects/work_items_controller_spec.rb'
+ - 'spec/controllers/projects_controller_spec.rb'
+ - 'spec/controllers/registrations_controller_spec.rb'
+ - 'spec/controllers/repositories/lfs_storage_controller_spec.rb'
+ - 'spec/controllers/root_controller_spec.rb'
+ - 'spec/controllers/search_controller_spec.rb'
+ - 'spec/controllers/sessions_controller_spec.rb'
+ - 'spec/controllers/snippets/blobs_controller_spec.rb'
+ - 'spec/controllers/snippets/notes_controller_spec.rb'
+ - 'spec/controllers/snippets_controller_spec.rb'
+ - 'spec/controllers/uploads_controller_spec.rb'
+ - 'spec/controllers/users/callouts_controller_spec.rb'
+ - 'spec/experiments/application_experiment_spec.rb'
+ - 'spec/experiments/in_product_guidance_environments_webide_experiment_spec.rb'
+ - 'spec/features/admin/users/user_spec.rb'
+ - 'spec/features/groups/clusters/user_spec.rb'
+ - 'spec/features/merge_request/user_sees_merge_widget_spec.rb'
+ - 'spec/features/profiles/password_spec.rb'
+ - 'spec/features/projects/clusters/user_spec.rb'
+ - 'spec/features/projects/pipelines/pipeline_spec.rb'
+ - 'spec/features/projects/settings/registry_settings_cleanup_tags_spec.rb'
+ - 'spec/features/projects/settings/registry_settings_spec.rb'
+ - 'spec/features/users/show_spec.rb'
+ - 'spec/finders/abuse_reports_finder_spec.rb'
+ - 'spec/finders/achievements/achievements_finder_spec.rb'
+ - 'spec/finders/analytics/cycle_analytics/stage_finder_spec.rb'
+ - 'spec/finders/autocomplete/users_finder_spec.rb'
+ - 'spec/finders/branches_finder_spec.rb'
+ - 'spec/finders/bulk_imports/entities_finder_spec.rb'
+ - 'spec/finders/bulk_imports/imports_finder_spec.rb'
+ - 'spec/finders/ci/auth_job_finder_spec.rb'
+ - 'spec/finders/ci/commit_statuses_finder_spec.rb'
+ - 'spec/finders/ci/job_artifacts_finder_spec.rb'
+ - 'spec/finders/ci/jobs_finder_spec.rb'
+ - 'spec/finders/ci/pipeline_schedules_finder_spec.rb'
+ - 'spec/finders/ci/pipelines_finder_spec.rb'
+ - 'spec/finders/ci/pipelines_for_merge_request_finder_spec.rb'
+ - 'spec/finders/ci/runners_finder_spec.rb'
+ - 'spec/finders/ci/triggers_finder_spec.rb'
+ - 'spec/finders/clusters/agents/authorizations/ci_access/finder_spec.rb'
+ - 'spec/finders/clusters/agents/authorizations/user_access/finder_spec.rb'
+ - 'spec/finders/clusters/knative_services_finder_spec.rb'
+ - 'spec/finders/container_repositories_finder_spec.rb'
+ - 'spec/finders/crm/contacts_finder_spec.rb'
+ - 'spec/finders/crm/organizations_finder_spec.rb'
+ - 'spec/finders/data_transfer/group_data_transfer_finder_spec.rb'
+ - 'spec/finders/data_transfer/project_data_transfer_finder_spec.rb'
+ - 'spec/finders/deploy_tokens/tokens_finder_spec.rb'
+ - 'spec/finders/deployments_finder_spec.rb'
+ - 'spec/finders/feature_flags_finder_spec.rb'
+ - 'spec/finders/group_descendants_finder_spec.rb'
+ - 'spec/finders/group_members_finder_spec.rb'
+ - 'spec/finders/group_projects_finder_spec.rb'
+ - 'spec/finders/groups/environment_scopes_finder_spec.rb'
+ - 'spec/finders/keys_finder_spec.rb'
+ - 'spec/finders/notes_finder_spec.rb'
+ - 'spec/finders/packages/conan/package_file_finder_spec.rb'
+ - 'spec/finders/packages/conan/package_finder_spec.rb'
+ - 'spec/finders/packages/debian/distributions_finder_spec.rb'
+ - 'spec/finders/packages/go/package_finder_spec.rb'
+ - 'spec/finders/packages/group_packages_finder_spec.rb'
+ - 'spec/finders/packages/maven/package_finder_spec.rb'
+ - 'spec/finders/packages/package_file_finder_spec.rb'
+ - 'spec/finders/packages/package_finder_spec.rb'
+ - 'spec/finders/packages/packages_finder_spec.rb'
+ - 'spec/finders/packages/pipelines_finder_spec.rb'
+ - 'spec/finders/packages/pypi/packages_finder_spec.rb'
+ - 'spec/finders/personal_access_tokens_finder_spec.rb'
+ - 'spec/finders/projects/export_job_finder_spec.rb'
+ - 'spec/finders/projects/members/effective_access_level_finder_spec.rb'
+ - 'spec/finders/projects/members/effective_access_level_per_user_finder_spec.rb'
+ - 'spec/finders/projects/ml/candidate_finder_spec.rb'
+ - 'spec/finders/projects/ml/model_finder_spec.rb'
+ - 'spec/finders/projects/prometheus/alerts_finder_spec.rb'
+ - 'spec/finders/prometheus_metrics_finder_spec.rb'
+ - 'spec/finders/releases/evidence_pipeline_finder_spec.rb'
+ - 'spec/finders/releases_finder_spec.rb'
+ - 'spec/finders/repositories/tree_finder_spec.rb'
+ - 'spec/finders/resource_milestone_event_finder_spec.rb'
+ - 'spec/finders/resource_state_event_finder_spec.rb'
+ - 'spec/finders/tags_finder_spec.rb'
+ - 'spec/finders/uploader_finder_spec.rb'
+ - 'spec/finders/user_group_notification_settings_finder_spec.rb'
+ - 'spec/finders/user_groups_counter_spec.rb'
+ - 'spec/finders/vs_code/settings/settings_finder_spec.rb'
+ - 'spec/frontend/fixtures/project.rb'
+ - 'spec/graphql/features/authorization_spec.rb'
+ - 'spec/graphql/mutations/alert_management/alerts/set_assignees_spec.rb'
+ - 'spec/graphql/mutations/alert_management/create_alert_issue_spec.rb'
+ - 'spec/graphql/mutations/alert_management/http_integration/create_spec.rb'
+ - 'spec/graphql/mutations/alert_management/http_integration/destroy_spec.rb'
+ - 'spec/graphql/mutations/alert_management/http_integration/reset_token_spec.rb'
+ - 'spec/graphql/mutations/alert_management/http_integration/update_spec.rb'
+ - 'spec/graphql/mutations/alert_management/prometheus_integration/create_spec.rb'
+ - 'spec/graphql/mutations/alert_management/prometheus_integration/reset_token_spec.rb'
+ - 'spec/graphql/mutations/alert_management/prometheus_integration/update_spec.rb'
+ - 'spec/graphql/mutations/alert_management/update_alert_status_spec.rb'
+ - 'spec/graphql/mutations/boards/issues/issue_move_list_spec.rb'
+ - 'spec/graphql/mutations/boards/update_spec.rb'
+ - 'spec/graphql/mutations/branches/create_spec.rb'
+ - 'spec/graphql/mutations/ci/job_token_scope/add_project_spec.rb'
+ - 'spec/graphql/mutations/ci/job_token_scope/remove_project_spec.rb'
+ - 'spec/graphql/mutations/ci/runner/delete_spec.rb'
+ - 'spec/graphql/mutations/clusters/agent_tokens/create_spec.rb'
+ - 'spec/graphql/mutations/clusters/agent_tokens/revoke_spec.rb'
+ - 'spec/graphql/mutations/clusters/agents/create_spec.rb'
+ - 'spec/graphql/mutations/clusters/agents/delete_spec.rb'
+ - 'spec/graphql/mutations/commits/create_spec.rb'
+ - 'spec/graphql/mutations/container_repositories/destroy_tags_spec.rb'
+ - 'spec/graphql/mutations/environments/canary_ingress/update_spec.rb'
+ - 'spec/graphql/mutations/environments/create_spec.rb'
+ - 'spec/graphql/mutations/environments/delete_spec.rb'
+ - 'spec/graphql/mutations/environments/stop_spec.rb'
+ - 'spec/graphql/mutations/environments/update_spec.rb'
+ - 'spec/graphql/mutations/issues/create_spec.rb'
+ - 'spec/graphql/mutations/issues/set_confidential_spec.rb'
+ - 'spec/graphql/mutations/issues/set_due_date_spec.rb'
+ - 'spec/graphql/mutations/issues/set_locked_spec.rb'
+ - 'spec/graphql/mutations/issues/set_severity_spec.rb'
+ - 'spec/graphql/mutations/issues/update_spec.rb'
+ - 'spec/graphql/mutations/labels/create_spec.rb'
+ - 'spec/graphql/mutations/merge_requests/create_spec.rb'
+ - 'spec/graphql/mutations/merge_requests/set_draft_spec.rb'
+ - 'spec/graphql/mutations/merge_requests/set_labels_spec.rb'
+ - 'spec/graphql/mutations/merge_requests/set_locked_spec.rb'
+ - 'spec/graphql/mutations/merge_requests/set_milestone_spec.rb'
+ - 'spec/graphql/mutations/merge_requests/set_reviewers_spec.rb'
+ - 'spec/graphql/mutations/merge_requests/update_spec.rb'
+ - 'spec/graphql/mutations/notes/reposition_image_diff_note_spec.rb'
+ - 'spec/graphql/mutations/pages/mark_onboarding_complete_spec.rb'
+ - 'spec/graphql/mutations/release_asset_links/create_spec.rb'
+ - 'spec/graphql/mutations/release_asset_links/delete_spec.rb'
+ - 'spec/graphql/mutations/release_asset_links/update_spec.rb'
+ - 'spec/graphql/mutations/releases/create_spec.rb'
+ - 'spec/graphql/mutations/releases/delete_spec.rb'
+ - 'spec/graphql/mutations/releases/update_spec.rb'
+ - 'spec/graphql/mutations/saved_replies/create_spec.rb'
+ - 'spec/graphql/mutations/saved_replies/destroy_spec.rb'
+ - 'spec/graphql/mutations/saved_replies/update_spec.rb'
+ - 'spec/graphql/mutations/terraform/state/delete_spec.rb'
+ - 'spec/graphql/mutations/terraform/state/lock_spec.rb'
+ - 'spec/graphql/mutations/terraform/state/unlock_spec.rb'
+ - 'spec/graphql/mutations/timelogs/delete_spec.rb'
+ - 'spec/graphql/resolvers/admin/analytics/usage_trends/measurements_resolver_spec.rb'
+ - 'spec/graphql/resolvers/alert_management/alert_status_counts_resolver_spec.rb'
+ - 'spec/graphql/resolvers/blame_resolver_spec.rb'
+ - 'spec/graphql/resolvers/ci/runner_projects_resolver_spec.rb'
+ - 'spec/graphql/resolvers/clusters/agent_activity_events_resolver_spec.rb'
+ - 'spec/graphql/resolvers/clusters/agent_tokens_resolver_spec.rb'
+ - 'spec/graphql/resolvers/clusters/agents/authorizations/ci_access_resolver_spec.rb'
+ - 'spec/graphql/resolvers/clusters/agents/authorizations/user_access_resolver_spec.rb'
+ - 'spec/graphql/resolvers/clusters/agents_resolver_spec.rb'
+ - 'spec/graphql/resolvers/container_repositories_resolver_spec.rb'
+ - 'spec/graphql/resolvers/group_members/notification_email_resolver_spec.rb'
+ - 'spec/graphql/resolvers/groups_resolver_spec.rb'
+ - 'spec/graphql/resolvers/issue_status_counts_resolver_spec.rb'
+ - 'spec/graphql/resolvers/kas/agent_configurations_resolver_spec.rb'
+ - 'spec/graphql/resolvers/kas/agent_connections_resolver_spec.rb'
+ - 'spec/graphql/resolvers/labels_resolver_spec.rb'
+ - 'spec/graphql/resolvers/merge_requests_count_resolver_spec.rb'
+ - 'spec/graphql/resolvers/packages_base_resolver_spec.rb'
+ - 'spec/graphql/resolvers/paginated_tree_resolver_spec.rb'
+ - 'spec/graphql/resolvers/terraform/states_resolver_spec.rb'
+ - 'spec/graphql/resolvers/user_discussions_count_resolver_spec.rb'
+ - 'spec/graphql/resolvers/user_notes_count_resolver_spec.rb'
+ - 'spec/graphql/subscriptions/issuable_updated_spec.rb'
+ - 'spec/graphql/types/admin/analytics/usage_trends/measurement_type_spec.rb'
+ - 'spec/graphql/types/ci/job_token_scope_type_spec.rb'
+ - 'spec/graphql/types/environment_type_spec.rb'
+ - 'spec/graphql/types/invitation_interface_spec.rb'
+ - 'spec/graphql/types/issue_type_spec.rb'
+ - 'spec/graphql/types/member_interface_spec.rb'
+ - 'spec/graphql/types/project_type_spec.rb'
+ - 'spec/graphql/types/todo_type_spec.rb'
+ - 'spec/graphql/types/untrusted_regexp_spec.rb'
+ - 'spec/graphql/types/user_type_spec.rb'
+ - 'spec/graphql/types/users/autocompleted_user_type_spec.rb'
+ - 'spec/helpers/admin/background_migrations_helper_spec.rb'
+ - 'spec/helpers/admin/components_helper_spec.rb'
+ - 'spec/helpers/appearances_helper_spec.rb'
+ - 'spec/helpers/application_helper_spec.rb'
+ - 'spec/helpers/artifacts_helper_spec.rb'
+ - 'spec/helpers/avatars_helper_spec.rb'
+ - 'spec/helpers/award_emoji_helper_spec.rb'
+ - 'spec/helpers/badges_helper_spec.rb'
+ - 'spec/helpers/branches_helper_spec.rb'
+ - 'spec/helpers/breadcrumbs_helper_spec.rb'
+ - 'spec/helpers/ci/builds_helper_spec.rb'
+ - 'spec/helpers/ci/catalog/resources_helper_spec.rb'
+ - 'spec/helpers/ci/pipeline_editor_helper_spec.rb'
+ - 'spec/helpers/ci/pipelines_helper_spec.rb'
+ - 'spec/helpers/ci/triggers_helper_spec.rb'
+ - 'spec/helpers/clusters_helper_spec.rb'
+ - 'spec/helpers/commits_helper_spec.rb'
+ - 'spec/helpers/diff_helper_spec.rb'
+ - 'spec/helpers/environment_helper_spec.rb'
+ - 'spec/helpers/environments_helper_spec.rb'
+ - 'spec/helpers/events_helper_spec.rb'
+ - 'spec/helpers/gitlab_routing_helper_spec.rb'
+ - 'spec/helpers/groups/group_members_helper_spec.rb'
+ - 'spec/helpers/groups_helper_spec.rb'
+ - 'spec/helpers/hooks_helper_spec.rb'
+ - 'spec/helpers/integrations_helper_spec.rb'
+ - 'spec/helpers/jira_connect_helper_spec.rb'
+ - 'spec/helpers/json_helper_spec.rb'
+ - 'spec/helpers/labels_helper_spec.rb'
+ - 'spec/helpers/listbox_helper_spec.rb'
+ - 'spec/helpers/markup_helper_spec.rb'
+ - 'spec/helpers/merge_requests_helper_spec.rb'
+ - 'spec/helpers/namespaces_helper_spec.rb'
+ - 'spec/helpers/nav/top_nav_helper_spec.rb'
+ - 'spec/helpers/nav_helper_spec.rb'
+ - 'spec/helpers/operations_helper_spec.rb'
+ - 'spec/helpers/page_layout_helper_spec.rb'
+ - 'spec/helpers/projects/cluster_agents_helper_spec.rb'
+ - 'spec/helpers/projects/ml/experiments_helper_spec.rb'
+ - 'spec/helpers/projects/project_members_helper_spec.rb'
+ - 'spec/helpers/projects/terraform_helper_spec.rb'
+ - 'spec/helpers/projects_helper_spec.rb'
+ - 'spec/helpers/routing/pseudonymization_helper_spec.rb'
+ - 'spec/helpers/search_helper_spec.rb'
+ - 'spec/helpers/sessions_helper_spec.rb'
+ - 'spec/helpers/sidebars_helper_spec.rb'
+ - 'spec/helpers/snippets_helper_spec.rb'
+ - 'spec/helpers/stat_anchors_helper_spec.rb'
+ - 'spec/helpers/tree_helper_spec.rb'
+ - 'spec/helpers/whats_new_helper_spec.rb'
+ - 'spec/helpers/wiki_helper_spec.rb'
+ - 'spec/helpers/wiki_page_version_helper_spec.rb'
+ - 'spec/initializers/00_deprecations_spec.rb'
+ - 'spec/initializers/carrierwave_s3_encryption_headers_patch_spec.rb'
+ - 'spec/initializers/cookies_serializer_spec.rb'
+ - 'spec/initializers/direct_upload_support_spec.rb'
+ - 'spec/initializers/doorkeeper_spec.rb'
+ - 'spec/initializers/enumerator_next_patch_spec.rb'
+ - 'spec/initializers/lograge_spec.rb'
+ - 'spec/initializers/pages_storage_check_spec.rb'
+ - 'spec/initializers/rails_asset_host_spec.rb'
+ - 'spec/initializers/session_store_spec.rb'
+ - 'spec/initializers/settings_spec.rb'
+ - 'spec/initializers/sidekiq_spec.rb'
+ - 'spec/initializers/validate_database_config_spec.rb'
+ - 'spec/initializers/validate_puma_spec.rb'
+ - 'spec/lib/api/ci/helpers/runner_spec.rb'
+ - 'spec/lib/api/entities/application_setting_spec.rb'
+ - 'spec/lib/api/entities/bulk_import_spec.rb'
+ - 'spec/lib/api/entities/bulk_imports/entity_failure_spec.rb'
+ - 'spec/lib/api/entities/bulk_imports/entity_spec.rb'
+ - 'spec/lib/api/entities/bulk_imports/export_batch_status_spec.rb'
+ - 'spec/lib/api/entities/bulk_imports/export_status_spec.rb'
+ - 'spec/lib/api/entities/changelog_spec.rb'
+ - 'spec/lib/api/entities/ci/job_artifact_file_spec.rb'
+ - 'spec/lib/api/entities/ci/job_request/dependency_spec.rb'
+ - 'spec/lib/api/entities/ci/job_request/image_spec.rb'
+ - 'spec/lib/api/entities/ci/job_request/port_spec.rb'
+ - 'spec/lib/api/entities/ci/job_request/service_spec.rb'
+ - 'spec/lib/api/entities/ci/pipeline_spec.rb'
+ - 'spec/lib/api/entities/clusters/agent_spec.rb'
+ - 'spec/lib/api/entities/design_management/design_spec.rb'
+ - 'spec/lib/api/entities/ml/mlflow/get_run_spec.rb'
+ - 'spec/lib/api/entities/ml/mlflow/run_info_spec.rb'
+ - 'spec/lib/api/entities/ml/mlflow/run_spec.rb'
+ - 'spec/lib/api/entities/ml/mlflow/search_runs_spec.rb'
+ - 'spec/lib/api/entities/package_spec.rb'
+ - 'spec/lib/api/entities/plan_limit_spec.rb'
+ - 'spec/lib/api/entities/project_import_failed_relation_spec.rb'
+ - 'spec/lib/api/entities/project_import_status_spec.rb'
+ - 'spec/lib/api/entities/project_job_token_scope_spec.rb'
+ - 'spec/lib/api/entities/projects/topic_spec.rb'
+ - 'spec/lib/api/entities/snippet_spec.rb'
+ - 'spec/lib/api/entities/user_spec.rb'
+ - 'spec/lib/api/entities/wiki_page_spec.rb'
+ - 'spec/lib/api/helpers/authentication_spec.rb'
+ - 'spec/lib/api/helpers/caching_spec.rb'
+ - 'spec/lib/api/helpers/import_github_helpers_spec.rb'
+ - 'spec/lib/api/helpers/packages/dependency_proxy_helpers_spec.rb'
+ - 'spec/lib/api/helpers/packages/npm_spec.rb'
+ - 'spec/lib/api/helpers/packages_helpers_spec.rb'
+ - 'spec/lib/api/helpers/pagination_spec.rb'
+ - 'spec/lib/api/helpers/pagination_strategies_spec.rb'
+ - 'spec/lib/api/helpers/rate_limiter_spec.rb'
+ - 'spec/lib/api/helpers/variables_helpers_spec.rb'
+ - 'spec/lib/api/helpers_spec.rb'
+ - 'spec/lib/api/support/git_access_actor_spec.rb'
+ - 'spec/lib/atlassian/jira_connect/client_spec.rb'
+ - 'spec/lib/atlassian/jira_connect/serializers/author_entity_spec.rb'
+ - 'spec/lib/atlassian/jira_connect/serializers/base_entity_spec.rb'
+ - 'spec/lib/atlassian/jira_connect/serializers/branch_entity_spec.rb'
+ - 'spec/lib/atlassian/jira_connect/serializers/build_entity_spec.rb'
+ - 'spec/lib/atlassian/jira_connect/serializers/deployment_entity_spec.rb'
+ - 'spec/lib/atlassian/jira_connect/serializers/feature_flag_entity_spec.rb'
+ - 'spec/lib/atlassian/jira_connect/serializers/pull_request_entity_spec.rb'
+ - 'spec/lib/atlassian/jira_connect/serializers/repository_entity_spec.rb'
+ - 'spec/lib/atlassian/jira_connect/serializers/reviewer_entity_spec.rb'
+ - 'spec/lib/backup/database_backup_error_spec.rb'
+ - 'spec/lib/backup/database_model_spec.rb'
+ - 'spec/lib/backup/database_spec.rb'
+ - 'spec/lib/backup/dump/postgres_spec.rb'
+ - 'spec/lib/backup/files_spec.rb'
+ - 'spec/lib/backup/gitaly_backup_spec.rb'
+ - 'spec/lib/backup/manager_spec.rb'
+ - 'spec/lib/backup/repositories_spec.rb'
+ - 'spec/lib/backup/task_spec.rb'
+ - 'spec/lib/banzai/color_parser_spec.rb'
+ - 'spec/lib/banzai/filter/broadcast_message_placeholders_filter_spec.rb'
+ - 'spec/lib/banzai/filter/math_filter_spec.rb'
+ - 'spec/lib/banzai/filter/output_safety_spec.rb'
+ - 'spec/lib/banzai/filter/references/project_reference_filter_spec.rb'
+ - 'spec/lib/banzai/filter/references/reference_cache_spec.rb'
+ - 'spec/lib/banzai/pipeline/post_process_pipeline_spec.rb'
+ - 'spec/lib/banzai/reference_parser/alert_parser_spec.rb'
+ - 'spec/lib/banzai/reference_parser/base_parser_spec.rb'
+ - 'spec/lib/banzai/reference_parser/commit_parser_spec.rb'
+ - 'spec/lib/banzai/reference_parser/commit_range_parser_spec.rb'
+ - 'spec/lib/banzai/reference_parser/design_parser_spec.rb'
+ - 'spec/lib/banzai/reference_parser/external_issue_parser_spec.rb'
+ - 'spec/lib/banzai/reference_parser/feature_flag_parser_spec.rb'
+ - 'spec/lib/banzai/reference_parser/issue_parser_spec.rb'
+ - 'spec/lib/banzai/reference_parser/label_parser_spec.rb'
+ - 'spec/lib/banzai/reference_parser/mentioned_group_parser_spec.rb'
+ - 'spec/lib/banzai/reference_parser/mentioned_project_parser_spec.rb'
+ - 'spec/lib/banzai/reference_parser/mentioned_user_parser_spec.rb'
+ - 'spec/lib/banzai/reference_parser/merge_request_parser_spec.rb'
+ - 'spec/lib/banzai/reference_parser/milestone_parser_spec.rb'
+ - 'spec/lib/banzai/reference_parser/project_parser_spec.rb'
+ - 'spec/lib/banzai/reference_parser/snippet_parser_spec.rb'
+ - 'spec/lib/banzai/reference_parser/user_parser_spec.rb'
+ - 'spec/lib/banzai/reference_parser/work_item_parser_spec.rb'
+ - 'spec/lib/banzai/renderer_spec.rb'
+ - 'spec/lib/bitbucket_server/client_spec.rb'
+ - 'spec/lib/bitbucket_server/collection_spec.rb'
+ - 'spec/lib/bitbucket_server/connection_spec.rb'
+ - 'spec/lib/bitbucket_server/representation/activity_spec.rb'
+ - 'spec/lib/bitbucket_server/representation/comment_spec.rb'
+ - 'spec/lib/bitbucket_server/representation/pull_request_comment_spec.rb'
+ - 'spec/lib/bitbucket_server/representation/pull_request_spec.rb'
+ - 'spec/lib/bitbucket_server/representation/repo_spec.rb'
+ - 'spec/lib/bulk_imports/clients/graphql_spec.rb'
+ - 'spec/lib/bulk_imports/clients/http_spec.rb'
+ - 'spec/lib/bulk_imports/common/extractors/graphql_extractor_spec.rb'
+ - 'spec/lib/bulk_imports/common/extractors/json_extractor_spec.rb'
+ - 'spec/lib/bulk_imports/common/extractors/ndjson_extractor_spec.rb'
+ - 'spec/lib/bulk_imports/common/extractors/rest_extractor_spec.rb'
+ - 'spec/lib/bulk_imports/common/pipelines/badges_pipeline_spec.rb'
+ - 'spec/lib/bulk_imports/common/pipelines/boards_pipeline_spec.rb'
+ - 'spec/lib/bulk_imports/common/pipelines/labels_pipeline_spec.rb'
+ - 'spec/lib/bulk_imports/common/pipelines/milestones_pipeline_spec.rb'
+ - 'spec/lib/bulk_imports/common/transformers/prohibited_attributes_transformer_spec.rb'
+ - 'spec/lib/bulk_imports/groups/extractors/subgroups_extractor_spec.rb'
+ - 'spec/lib/bulk_imports/groups/graphql/get_group_query_spec.rb'
+ - 'spec/lib/bulk_imports/groups/graphql/get_projects_query_spec.rb'
+ - 'spec/lib/bulk_imports/groups/loaders/group_loader_spec.rb'
+ - 'spec/lib/bulk_imports/groups/pipelines/group_pipeline_spec.rb'
+ - 'spec/lib/bulk_imports/groups/pipelines/project_entities_pipeline_spec.rb'
+ - 'spec/lib/bulk_imports/groups/pipelines/subgroup_entities_pipeline_spec.rb'
+ - 'spec/lib/bulk_imports/groups/transformers/subgroup_to_entity_transformer_spec.rb'
+ - 'spec/lib/bulk_imports/ndjson_pipeline_spec.rb'
+ - 'spec/lib/bulk_imports/pipeline/context_spec.rb'
+ - 'spec/lib/bulk_imports/pipeline/extracted_data_spec.rb'
+ - 'spec/lib/bulk_imports/pipeline/runner_spec.rb'
+ - 'spec/lib/bulk_imports/pipeline_schema_info_spec.rb'
+ - 'spec/lib/bulk_imports/projects/graphql/get_project_query_spec.rb'
+ - 'spec/lib/bulk_imports/projects/graphql/get_repository_query_spec.rb'
+ - 'spec/lib/bulk_imports/projects/graphql/get_snippet_repository_query_spec.rb'
+ - 'spec/lib/bulk_imports/projects/pipelines/references_pipeline_spec.rb'
+ - 'spec/lib/bulk_imports/projects/stage_spec.rb'
+ - 'spec/lib/bulk_imports/source_url_builder_spec.rb'
+ - 'spec/lib/bulk_imports/users_mapper_spec.rb'
+ - 'spec/lib/constraints/admin_constrainer_spec.rb'
+ - 'spec/lib/constraints/group_url_constrainer_spec.rb'
+ - 'spec/lib/constraints/project_url_constrainer_spec.rb'
+ - 'spec/lib/constraints/user_url_constrainer_spec.rb'
+ - 'spec/lib/container_registry/client_spec.rb'
+ - 'spec/lib/container_registry/gitlab_api_client_spec.rb'
+ - 'spec/lib/container_registry/migration_spec.rb'
+ - 'spec/lib/container_registry/path_spec.rb'
+ - 'spec/lib/container_registry/registry_spec.rb'
+ - 'spec/lib/container_registry/tag_spec.rb'
+ - 'spec/lib/error_tracking/sentry_client/event_spec.rb'
+ - 'spec/lib/error_tracking/sentry_client/issue_spec.rb'
+ - 'spec/lib/error_tracking/sentry_client/projects_spec.rb'
+ - 'spec/lib/error_tracking/sentry_client/repo_spec.rb'
+ - 'spec/lib/extracts_ref/requested_ref_spec.rb'
+ - 'spec/lib/feature/definition_spec.rb'
+ - 'spec/lib/feature/gitaly_spec.rb'
+ - 'spec/lib/feature_spec.rb'
+ - 'spec/lib/generators/gitlab/usage_metric_definition_generator_spec.rb'
+ - 'spec/lib/generators/model/model_generator_spec.rb'
+ - 'spec/lib/gitaly/server_spec.rb'
+ - 'spec/lib/gitlab/alert_management/fingerprint_spec.rb'
+ - 'spec/lib/gitlab/alert_management/payload/base_spec.rb'
+ - 'spec/lib/gitlab/alert_management/payload_spec.rb'
+ - 'spec/lib/gitlab/allowable_spec.rb'
+ - 'spec/lib/gitlab/analytics/cycle_analytics/median_spec.rb'
+ - 'spec/lib/gitlab/analytics/cycle_analytics/records_fetcher_spec.rb'
+ - 'spec/lib/gitlab/analytics/cycle_analytics/stage_events/code_stage_start_spec.rb'
+ - 'spec/lib/gitlab/analytics/cycle_analytics/stage_events/plan_stage_start_spec.rb'
+ - 'spec/lib/gitlab/analytics/usage_trends/workers_argument_builder_spec.rb'
+ - 'spec/lib/gitlab/anonymous_session_spec.rb'
+ - 'spec/lib/gitlab/api_authentication/token_locator_spec.rb'
+ - 'spec/lib/gitlab/app_json_logger_spec.rb'
+ - 'spec/lib/gitlab/app_logger_spec.rb'
+ - 'spec/lib/gitlab/app_text_logger_spec.rb'
+ - 'spec/lib/gitlab/application_rate_limiter/base_strategy_spec.rb'
+ - 'spec/lib/gitlab/application_rate_limiter_spec.rb'
+ - 'spec/lib/gitlab/audit/ci_runner_token_author_spec.rb'
+ - 'spec/lib/gitlab/audit/null_author_spec.rb'
+ - 'spec/lib/gitlab/audit/null_target_spec.rb'
+ - 'spec/lib/gitlab/audit/target_spec.rb'
+ - 'spec/lib/gitlab/audit/type/definition_spec.rb'
+ - 'spec/lib/gitlab/auth/atlassian/identity_linker_spec.rb'
+ - 'spec/lib/gitlab/auth/auth_finders_spec.rb'
+ - 'spec/lib/gitlab/auth/current_user_mode_spec.rb'
+ - 'spec/lib/gitlab/auth/ip_rate_limiter_spec.rb'
+ - 'spec/lib/gitlab/auth/ldap/dn_spec.rb'
+ - 'spec/lib/gitlab/auth/ldap/person_spec.rb'
+ - 'spec/lib/gitlab/auth/o_auth/identity_linker_spec.rb'
+ - 'spec/lib/gitlab/auth/o_auth/provider_spec.rb'
+ - 'spec/lib/gitlab/auth/request_authenticator_spec.rb'
+ - 'spec/lib/gitlab/auth/result_spec.rb'
+ - 'spec/lib/gitlab/auth/saml/identity_linker_spec.rb'
+ - 'spec/lib/gitlab/auth/saml/origin_validator_spec.rb'
+ - 'spec/lib/gitlab/auth/two_factor_auth_verifier_spec.rb'
+ - 'spec/lib/gitlab/auth_spec.rb'
+ - 'spec/lib/gitlab/authorized_keys_spec.rb'
+ - 'spec/lib/gitlab/avatar_cache_spec.rb'
+ - 'spec/lib/gitlab/background_migration/backfill_admin_mode_scope_for_personal_access_tokens_spec.rb'
+ - 'spec/lib/gitlab/background_migration/backfill_missing_ci_cd_settings_spec.rb'
+ - 'spec/lib/gitlab/background_migration/backfill_project_repositories_spec.rb'
+ - 'spec/lib/gitlab/background_migration/backfill_topics_title_spec.rb'
+ - 'spec/lib/gitlab/background_migration/backfill_user_details_fields_spec.rb'
+ - 'spec/lib/gitlab/background_migration/delete_invalid_protected_branch_merge_access_levels_spec.rb'
+ - 'spec/lib/gitlab/background_migration/delete_invalid_protected_branch_push_access_levels_spec.rb'
+ - 'spec/lib/gitlab/background_migration/delete_invalid_protected_tag_create_access_levels_spec.rb'
+ - 'spec/lib/gitlab/background_migration/delete_orphans_approval_merge_request_rules2_spec.rb'
+ - 'spec/lib/gitlab/background_migration/delete_orphans_approval_merge_request_rules_spec.rb'
+ - 'spec/lib/gitlab/background_migration/delete_orphans_approval_project_rules2_spec.rb'
+ - 'spec/lib/gitlab/background_migration/delete_orphans_approval_project_rules_spec.rb'
+ - 'spec/lib/gitlab/background_migration/legacy_uploads_migrator_spec.rb'
+ - 'spec/lib/gitlab/background_migration/mailers/unconfirm_mailer_spec.rb'
+ - 'spec/lib/gitlab/background_migration/nullify_creator_id_column_of_orphaned_projects_spec.rb'
+ - 'spec/lib/gitlab/background_migration/populate_vulnerability_dismissal_fields_spec.rb'
+ - 'spec/lib/gitlab/background_migration/redis/backfill_project_pipeline_status_ttl_spec.rb'
+ - 'spec/lib/gitlab/background_migration/remove_backfilled_job_artifacts_expire_at_spec.rb'
+ - 'spec/lib/gitlab/background_migration/remove_project_group_link_with_missing_groups_spec.rb'
+ - 'spec/lib/gitlab/background_migration/truncate_overlong_vulnerability_html_titles_spec.rb'
+ - 'spec/lib/gitlab/background_migration/update_ci_pipeline_artifacts_unknown_locked_status_spec.rb'
+ - 'spec/lib/gitlab/background_task_spec.rb'
+ - 'spec/lib/gitlab/bitbucket_import/importer_spec.rb'
+ - 'spec/lib/gitlab/bitbucket_import/parallel_importer_spec.rb'
+ - 'spec/lib/gitlab/bitbucket_server_import/importers/pull_request_notes_importer_spec.rb'
+ - 'spec/lib/gitlab/blame_spec.rb'
+ - 'spec/lib/gitlab/branch_push_merge_commit_analyzer_spec.rb'
+ - 'spec/lib/gitlab/bullet/exclusions_spec.rb'
+ - 'spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb'
+ - 'spec/lib/gitlab/cache/helpers_spec.rb'
+ - 'spec/lib/gitlab/cache/metrics_spec.rb'
+ - 'spec/lib/gitlab/cache_spec.rb'
+ - 'spec/lib/gitlab/chat_name_token_spec.rb'
+ - 'spec/lib/gitlab/checks/branch_check_spec.rb'
+ - 'spec/lib/gitlab/checks/changes_access_spec.rb'
+ - 'spec/lib/gitlab/checks/container_moved_spec.rb'
+ - 'spec/lib/gitlab/checks/diff_check_spec.rb'
+ - 'spec/lib/gitlab/checks/file_size_check/any_oversized_blobs_spec.rb'
+ - 'spec/lib/gitlab/checks/file_size_check/hook_environment_aware_any_oversized_blobs_spec.rb'
+ - 'spec/lib/gitlab/checks/global_file_size_check_spec.rb'
+ - 'spec/lib/gitlab/checks/lfs_check_spec.rb'
+ - 'spec/lib/gitlab/checks/lfs_integrity_spec.rb'
+ - 'spec/lib/gitlab/checks/matching_merge_request_spec.rb'
+ - 'spec/lib/gitlab/checks/project_created_spec.rb'
+ - 'spec/lib/gitlab/checks/push_check_spec.rb'
+ - 'spec/lib/gitlab/checks/push_file_count_check_spec.rb'
+ - 'spec/lib/gitlab/checks/single_change_access_spec.rb'
+ - 'spec/lib/gitlab/checks/snippet_check_spec.rb'
+ - 'spec/lib/gitlab/checks/tag_check_spec.rb'
+ - 'spec/lib/gitlab/ci/ansi2html_spec.rb'
+ - 'spec/lib/gitlab/ci/ansi2json/line_spec.rb'
+ - 'spec/lib/gitlab/ci/ansi2json/parser_spec.rb'
+ - 'spec/lib/gitlab/ci/ansi2json/result_spec.rb'
+ - 'spec/lib/gitlab/ci/ansi2json/style_spec.rb'
+ - 'spec/lib/gitlab/ci/ansi2json_spec.rb'
+ - 'spec/lib/gitlab/ci/artifact_file_reader_spec.rb'
+ - 'spec/lib/gitlab/ci/artifacts/decompressed_artifact_size_validator_spec.rb'
+ - 'spec/lib/gitlab/ci/artifacts/metrics_spec.rb'
+ - 'spec/lib/gitlab/ci/badge/release/latest_release_spec.rb'
+ - 'spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb'
+ - 'spec/lib/gitlab/ci/build/auto_retry_spec.rb'
+ - 'spec/lib/gitlab/ci/build/credentials/factory_spec.rb'
+ - 'spec/lib/gitlab/ci/build/credentials/registry/dependency_proxy_spec.rb'
+ - 'spec/lib/gitlab/ci/build/credentials/registry/gitlab_registry_spec.rb'
+ - 'spec/lib/gitlab/ci/build/image_spec.rb'
+ - 'spec/lib/gitlab/ci/build/policy/refs_spec.rb'
+ - 'spec/lib/gitlab/ci/build/port_spec.rb'
+ - 'spec/lib/gitlab/ci/build/prerequisite/kubernetes_namespace_spec.rb'
+ - 'spec/lib/gitlab/ci/build/releaser_spec.rb'
+ - 'spec/lib/gitlab/ci/build/rules_spec.rb'
+ - 'spec/lib/gitlab/ci/build/step_spec.rb'
+ - 'spec/lib/gitlab/ci/config/entry/bridge_spec.rb'
+ - 'spec/lib/gitlab/ci/config/entry/inherit/default_spec.rb'
+ - 'spec/lib/gitlab/ci/config/entry/inherit/variables_spec.rb'
+ - 'spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb'
+ - 'spec/lib/gitlab/ci/config/entry/trigger_spec.rb'
+ - 'spec/lib/gitlab/ci/config/extendable/entry_spec.rb'
+ - 'spec/lib/gitlab/ci/config/extendable_spec.rb'
+ - 'spec/lib/gitlab/ci/config/external/context_spec.rb'
+ - 'spec/lib/gitlab/ci/config/external/file/component_spec.rb'
+ - 'spec/lib/gitlab/ci/config/external/file/remote_spec.rb'
+ - 'spec/lib/gitlab/ci/config/external/mapper_spec.rb'
+ - 'spec/lib/gitlab/ci/config/external/processor_spec.rb'
+ - 'spec/lib/gitlab/ci/config/interpolation/access_spec.rb'
+ - 'spec/lib/gitlab/ci/config/interpolation/block_spec.rb'
+ - 'spec/lib/gitlab/ci/config/interpolation/config_spec.rb'
+ - 'spec/lib/gitlab/ci/config/interpolation/context_spec.rb'
+ - 'spec/lib/gitlab/ci/config/interpolation/functions_stack_spec.rb'
+ - 'spec/lib/gitlab/ci/config/interpolation/interpolator_spec.rb'
+ - 'spec/lib/gitlab/ci/config/interpolation/template_spec.rb'
+ - 'spec/lib/gitlab/ci/config/normalizer/matrix_strategy_spec.rb'
+ - 'spec/lib/gitlab/ci/config/normalizer_spec.rb'
+ - 'spec/lib/gitlab/ci/config/yaml/tags/reference_spec.rb'
+ - 'spec/lib/gitlab/ci/cron_parser_spec.rb'
+ - 'spec/lib/gitlab/ci/decompressed_gzip_size_validator_spec.rb'
+ - 'spec/lib/gitlab/ci/environment_matcher_spec.rb'
+ - 'spec/lib/gitlab/ci/lint_spec.rb'
+ - 'spec/lib/gitlab/ci/matching/runner_matcher_spec.rb'
+ - 'spec/lib/gitlab/ci/parsers/accessibility/pa11y_spec.rb'
+ - 'spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb'
+ - 'spec/lib/gitlab/ci/parsers/terraform/tfplan_spec.rb'
+ - 'spec/lib/gitlab/ci/parsers/test/junit_spec.rb'
+ - 'spec/lib/gitlab/ci/parsers_spec.rb'
+ - 'spec/lib/gitlab/ci/pipeline/chain/assign_partition_spec.rb'
+ - 'spec/lib/gitlab/ci/pipeline/chain/cancel_pending_pipelines_spec.rb'
+ - 'spec/lib/gitlab/ci/pipeline/chain/command_spec.rb'
+ - 'spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb'
+ - 'spec/lib/gitlab/ci/pipeline/chain/ensure_environments_spec.rb'
+ - 'spec/lib/gitlab/ci/pipeline/chain/ensure_resource_groups_spec.rb'
+ - 'spec/lib/gitlab/ci/pipeline/chain/helpers_spec.rb'
+ - 'spec/lib/gitlab/ci/pipeline/chain/limit/active_jobs_spec.rb'
+ - 'spec/lib/gitlab/ci/pipeline/chain/remove_unwanted_chat_jobs_spec.rb'
+ - 'spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb'
+ - 'spec/lib/gitlab/ci/pipeline/expression/lexeme/pattern/regular_expression_spec.rb'
+ - 'spec/lib/gitlab/ci/pipeline/expression/parser_spec.rb'
+ - 'spec/lib/gitlab/ci/pipeline/expression/token_spec.rb'
+ - 'spec/lib/gitlab/ci/pipeline/seed/build/cache_spec.rb'
+ - 'spec/lib/gitlab/ci/pipeline/seed/build_spec.rb'
+ - 'spec/lib/gitlab/ci/pipeline/seed/processable/resource_group_spec.rb'
+ - 'spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb'
+ - 'spec/lib/gitlab/ci/queue/metrics_spec.rb'
+ - 'spec/lib/gitlab/ci/reports/accessibility_reports_spec.rb'
+ - 'spec/lib/gitlab/ci/reports/coverage_report_generator_spec.rb'
+ - 'spec/lib/gitlab/ci/reports/sbom/source_spec.rb'
+ - 'spec/lib/gitlab/ci/reports/security/aggregated_report_spec.rb'
+ - 'spec/lib/gitlab/ci/reports/security/finding_signature_spec.rb'
+ - 'spec/lib/gitlab/ci/reports/security/flag_spec.rb'
+ - 'spec/lib/gitlab/ci/reports/security/identifier_spec.rb'
+ - 'spec/lib/gitlab/ci/reports/security/link_spec.rb'
+ - 'spec/lib/gitlab/ci/reports/security/report_spec.rb'
+ - 'spec/lib/gitlab/ci/reports/security/reports_spec.rb'
+ - 'spec/lib/gitlab/ci/reports/security/scan_spec.rb'
+ - 'spec/lib/gitlab/ci/reports/security/scanned_resource_spec.rb'
+ - 'spec/lib/gitlab/ci/reports/security/scanner_spec.rb'
+ - 'spec/lib/gitlab/ci/reports/terraform_reports_spec.rb'
+ - 'spec/lib/gitlab/ci/reports/test_report_spec.rb'
+ - 'spec/lib/gitlab/ci/reports/test_report_summary_spec.rb'
+ - 'spec/lib/gitlab/ci/reports/test_reports_comparer_spec.rb'
+ - 'spec/lib/gitlab/ci/reports/test_suite_spec.rb'
+ - 'spec/lib/gitlab/ci/reports/test_suite_summary_spec.rb'
+ - 'spec/lib/gitlab/ci/runner/metrics_spec.rb'
+ - 'spec/lib/gitlab/ci/runner_instructions_spec.rb'
+ - 'spec/lib/gitlab/ci/runner_releases_spec.rb'
+ - 'spec/lib/gitlab/ci/secure_files/cer_spec.rb'
+ - 'spec/lib/gitlab/ci/secure_files/migration_helper_spec.rb'
+ - 'spec/lib/gitlab/ci/secure_files/mobile_provision_spec.rb'
+ - 'spec/lib/gitlab/ci/secure_files/p12_spec.rb'
+ - 'spec/lib/gitlab/ci/status/bridge/common_spec.rb'
+ - 'spec/lib/gitlab/ci/status/build/action_spec.rb'
+ - 'spec/lib/gitlab/ci/status/build/cancelable_spec.rb'
+ - 'spec/lib/gitlab/ci/status/build/canceled_spec.rb'
+ - 'spec/lib/gitlab/ci/status/build/common_spec.rb'
+ - 'spec/lib/gitlab/ci/status/build/created_spec.rb'
+ - 'spec/lib/gitlab/ci/status/build/erased_spec.rb'
+ - 'spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb'
+ - 'spec/lib/gitlab/ci/status/build/failed_spec.rb'
+ - 'spec/lib/gitlab/ci/status/build/manual_spec.rb'
+ - 'spec/lib/gitlab/ci/status/build/pending_spec.rb'
+ - 'spec/lib/gitlab/ci/status/build/play_spec.rb'
+ - 'spec/lib/gitlab/ci/status/build/preparing_spec.rb'
+ - 'spec/lib/gitlab/ci/status/build/retried_spec.rb'
+ - 'spec/lib/gitlab/ci/status/build/retryable_spec.rb'
+ - 'spec/lib/gitlab/ci/status/build/scheduled_spec.rb'
+ - 'spec/lib/gitlab/ci/status/build/skipped_spec.rb'
+ - 'spec/lib/gitlab/ci/status/build/stop_spec.rb'
+ - 'spec/lib/gitlab/ci/status/build/unschedule_spec.rb'
+ - 'spec/lib/gitlab/ci/status/canceled_spec.rb'
+ - 'spec/lib/gitlab/ci/status/created_spec.rb'
+ - 'spec/lib/gitlab/ci/status/external/common_spec.rb'
+ - 'spec/lib/gitlab/ci/status/failed_spec.rb'
+ - 'spec/lib/gitlab/ci/status/group/common_spec.rb'
+ - 'spec/lib/gitlab/ci/status/manual_spec.rb'
+ - 'spec/lib/gitlab/ci/status/pending_spec.rb'
+ - 'spec/lib/gitlab/ci/status/pipeline/blocked_spec.rb'
+ - 'spec/lib/gitlab/ci/status/pipeline/common_spec.rb'
+ - 'spec/lib/gitlab/ci/status/pipeline/delayed_spec.rb'
+ - 'spec/lib/gitlab/ci/status/preparing_spec.rb'
+ - 'spec/lib/gitlab/ci/status/processable/waiting_for_resource_spec.rb'
+ - 'spec/lib/gitlab/ci/status/running_spec.rb'
+ - 'spec/lib/gitlab/ci/status/scheduled_spec.rb'
+ - 'spec/lib/gitlab/ci/status/skipped_spec.rb'
+ - 'spec/lib/gitlab/ci/status/stage/common_spec.rb'
+ - 'spec/lib/gitlab/ci/status/success_spec.rb'
+ - 'spec/lib/gitlab/ci/status/success_warning_spec.rb'
+ - 'spec/lib/gitlab/ci/status/waiting_for_callback_spec.rb'
+ - 'spec/lib/gitlab/ci/status/waiting_for_resource_spec.rb'
+ - 'spec/lib/gitlab/ci/trace/archive_spec.rb'
+ - 'spec/lib/gitlab/ci/trace/checksum_spec.rb'
+ - 'spec/lib/gitlab/ci/trace/chunked_io_spec.rb'
+ - 'spec/lib/gitlab/ci/trace/metrics_spec.rb'
+ - 'spec/lib/gitlab/ci/trace/remote_checksum_spec.rb'
+ - 'spec/lib/gitlab/ci/trace/section_parser_spec.rb'
+ - 'spec/lib/gitlab/ci/variables/builder/group_spec.rb'
+ - 'spec/lib/gitlab/ci/variables/builder/pipeline_spec.rb'
+ - 'spec/lib/gitlab/ci/variables/builder/project_spec.rb'
+ - 'spec/lib/gitlab/ci/variables/builder/release_spec.rb'
+ - 'spec/lib/gitlab/ci/variables/builder_spec.rb'
+ - 'spec/lib/gitlab/ci/variables/collection/sort_spec.rb'
+ - 'spec/lib/gitlab/ci/variables/collection_spec.rb'
+ - 'spec/lib/gitlab/ci/yaml_processor_spec.rb'
+ - 'spec/lib/gitlab/cleanup/personal_access_tokens_spec.rb'
+ - 'spec/lib/gitlab/cleanup/project_uploads_spec.rb'
+ - 'spec/lib/gitlab/cleanup/remote_uploads_spec.rb'
+ - 'spec/lib/gitlab/closing_issue_extractor_spec.rb'
+ - 'spec/lib/gitlab/cluster/lifecycle_events_spec.rb'
+ - 'spec/lib/gitlab/cluster/rack_timeout_observer_spec.rb'
+ - 'spec/lib/gitlab/code_navigation_path_spec.rb'
+ - 'spec/lib/gitlab/composer/cache_spec.rb'
+ - 'spec/lib/gitlab/container_repository/tags/cache_spec.rb'
+ - 'spec/lib/gitlab/content_security_policy/config_loader_spec.rb'
+ - 'spec/lib/gitlab/cycle_analytics/permissions_spec.rb'
+ - 'spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb'
+ - 'spec/lib/gitlab/daemon_spec.rb'
+ - 'spec/lib/gitlab/data_builder/push_spec.rb'
+ - 'spec/lib/gitlab/database/async_constraints/postgres_async_constraint_validation_spec.rb'
+ - 'spec/lib/gitlab/database/async_constraints/validators/foreign_key_spec.rb'
+ - 'spec/lib/gitlab/database/async_constraints_spec.rb'
+ - 'spec/lib/gitlab/database/async_indexes/index_base_spec.rb'
+ - 'spec/lib/gitlab/database/async_indexes/index_creator_spec.rb'
+ - 'spec/lib/gitlab/database/async_indexes/index_destructor_spec.rb'
+ - 'spec/lib/gitlab/database/async_indexes/postgres_async_index_spec.rb'
+ - 'spec/lib/gitlab/database/async_indexes_spec.rb'
+ - 'spec/lib/gitlab/database/background_migration/batch_optimizer_spec.rb'
+ - 'spec/lib/gitlab/database/background_migration/batched_job_spec.rb'
+ - 'spec/lib/gitlab/database/background_migration/batched_migration_spec.rb'
+ - 'spec/lib/gitlab/database/batch_average_counter_spec.rb'
+ - 'spec/lib/gitlab/database/bump_sequences_spec.rb'
+ - 'spec/lib/gitlab/database/count/exact_count_strategy_spec.rb'
+ - 'spec/lib/gitlab/database/count/reltuples_count_strategy_spec.rb'
+ - 'spec/lib/gitlab/database/count/tablesample_count_strategy_spec.rb'
+ - 'spec/lib/gitlab/database/count_spec.rb'
+ - 'spec/lib/gitlab/database/database_connection_info_spec.rb'
+ - 'spec/lib/gitlab/database/dynamic_model_helpers_spec.rb'
+ - 'spec/lib/gitlab/database/gitlab_schema_spec.rb'
+ - 'spec/lib/gitlab/database/health_status/indicators/autovacuum_active_on_table_spec.rb'
+ - 'spec/lib/gitlab/database/load_balancing/load_balancer_spec.rb'
+ - 'spec/lib/gitlab/database/load_balancing/rack_middleware_spec.rb'
+ - 'spec/lib/gitlab/database/load_balancing/resolver_spec.rb'
+ - 'spec/lib/gitlab/database/load_balancing/service_discovery_spec.rb'
+ - 'spec/lib/gitlab/database/load_balancing/srv_resolver_spec.rb'
+ - 'spec/lib/gitlab/database/lock_writes_manager_spec.rb'
+ - 'spec/lib/gitlab/database/loose_foreign_keys_spec.rb'
+ - 'spec/lib/gitlab/database/migration_helpers/convert_to_bigint_spec.rb'
+ - 'spec/lib/gitlab/database/migration_helpers/wraparound_vacuum_helpers_spec.rb'
+ - 'spec/lib/gitlab/database/migration_helpers_spec.rb'
+ - 'spec/lib/gitlab/database/migration_spec.rb'
+ - 'spec/lib/gitlab/database/migrations/base_background_runner_spec.rb'
+ - 'spec/lib/gitlab/database/migrations/extension_helpers_spec.rb'
+ - 'spec/lib/gitlab/database/migrations/instrumentation_spec.rb'
+ - 'spec/lib/gitlab/database/migrations/lock_retry_mixin_spec.rb'
+ - 'spec/lib/gitlab/database/migrations/observers/query_details_spec.rb'
+ - 'spec/lib/gitlab/database/migrations/observers/query_statistics_spec.rb'
+ - 'spec/lib/gitlab/database/migrations/observers/total_database_size_change_spec.rb'
+ - 'spec/lib/gitlab/database/migrations/redis_helpers_spec.rb'
+ - 'spec/lib/gitlab/database/migrations/runner_backoff/active_record_mixin_spec.rb'
+ - 'spec/lib/gitlab/database/migrations/runner_backoff/communicator_spec.rb'
+ - 'spec/lib/gitlab/database/migrations/test_batched_background_runner_spec.rb'
+ - 'spec/lib/gitlab/database/migrations/version_spec.rb'
+ - 'spec/lib/gitlab/database/obsolete_ignored_columns_spec.rb'
+ - 'spec/lib/gitlab/database/partitioning/list/locking_configuration_spec.rb'
+ - 'spec/lib/gitlab/database/partitioning/monthly_strategy_spec.rb'
+ - 'spec/lib/gitlab/database/partitioning/partition_manager_spec.rb'
+ - 'spec/lib/gitlab/database/partitioning/partition_monitoring_spec.rb'
+ - 'spec/lib/gitlab/database/partitioning/time_partition_spec.rb'
+ - 'spec/lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table_spec.rb'
+ - 'spec/lib/gitlab/database/partitioning_migration_helpers/index_helpers_spec.rb'
+ - 'spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb'
+ - 'spec/lib/gitlab/database/pg_class_spec.rb'
+ - 'spec/lib/gitlab/database/pg_depend_spec.rb'
+ - 'spec/lib/gitlab/database/postgres_autovacuum_activity_spec.rb'
+ - 'spec/lib/gitlab/database/postgres_index_bloat_estimate_spec.rb'
+ - 'spec/lib/gitlab/database/postgres_index_spec.rb'
+ - 'spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/columns_spec.rb'
+ - 'spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/common_table_expressions_spec.rb'
+ - 'spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/targets_spec.rb'
+ - 'spec/lib/gitlab/database/query_analyzers/restrict_allowed_schemas_spec.rb'
+ - 'spec/lib/gitlab/database/reindexing/coordinator_spec.rb'
+ - 'spec/lib/gitlab/database/reindexing/index_selection_spec.rb'
+ - 'spec/lib/gitlab/database/reindexing/reindex_action_spec.rb'
+ - 'spec/lib/gitlab/database/reindexing/reindex_concurrently_spec.rb'
+ - 'spec/lib/gitlab/database/reindexing_spec.rb'
+ - 'spec/lib/gitlab/database/schema_cleaner_spec.rb'
+ - 'spec/lib/gitlab/database/similarity_score_spec.rb'
+ - 'spec/lib/gitlab/database/tables_locker_spec.rb'
+ - 'spec/lib/gitlab/database/tables_sorted_by_foreign_keys_spec.rb'
+ - 'spec/lib/gitlab/database/tables_truncate_spec.rb'
+ - 'spec/lib/gitlab/database/transaction/context_spec.rb'
+ - 'spec/lib/gitlab/database/transaction_timeout_settings_spec.rb'
+ - 'spec/lib/gitlab/database/with_lock_retries_outside_transaction_spec.rb'
+ - 'spec/lib/gitlab/database/with_lock_retries_spec.rb'
+ - 'spec/lib/gitlab/database_importers/default_organization_importer_spec.rb'
+ - 'spec/lib/gitlab/database_spec.rb'
+ - 'spec/lib/gitlab/database_warnings_spec.rb'
+ - 'spec/lib/gitlab/dependency_linker/base_linker_spec.rb'
+ - 'spec/lib/gitlab/dependency_linker/cargo_toml_linker_spec.rb'
+ - 'spec/lib/gitlab/dependency_linker/cartfile_linker_spec.rb'
+ - 'spec/lib/gitlab/dependency_linker/composer_json_linker_spec.rb'
+ - 'spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb'
+ - 'spec/lib/gitlab/dependency_linker/gemspec_linker_spec.rb'
+ - 'spec/lib/gitlab/dependency_linker/go_mod_linker_spec.rb'
+ - 'spec/lib/gitlab/dependency_linker/go_sum_linker_spec.rb'
+ - 'spec/lib/gitlab/dependency_linker/godeps_json_linker_spec.rb'
+ - 'spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb'
+ - 'spec/lib/gitlab/dependency_linker/parser/gemfile_spec.rb'
+ - 'spec/lib/gitlab/dependency_linker/podfile_linker_spec.rb'
+ - 'spec/lib/gitlab/dependency_linker/podspec_json_linker_spec.rb'
+ - 'spec/lib/gitlab/dependency_linker/podspec_linker_spec.rb'
+ - 'spec/lib/gitlab/dependency_linker/requirements_txt_linker_spec.rb'
+ - 'spec/lib/gitlab/diff/char_diff_spec.rb'
+ - 'spec/lib/gitlab/diff/diff_refs_spec.rb'
+ - 'spec/lib/gitlab/diff/file_collection/merge_request_diff_batch_spec.rb'
+ - 'spec/lib/gitlab/diff/formatters/file_formatter_spec.rb'
+ - 'spec/lib/gitlab/diff/formatters/image_formatter_spec.rb'
+ - 'spec/lib/gitlab/diff/highlight_spec.rb'
+ - 'spec/lib/gitlab/diff/inline_diff_markdown_marker_spec.rb'
+ - 'spec/lib/gitlab/diff/inline_diff_marker_spec.rb'
+ - 'spec/lib/gitlab/diff/inline_diff_spec.rb'
+ - 'spec/lib/gitlab/diff/line_mapper_spec.rb'
+ - 'spec/lib/gitlab/diff/lines_unfolder_spec.rb'
+ - 'spec/lib/gitlab/diff/pair_selector_spec.rb'
+ - 'spec/lib/gitlab/diff/parallel_diff_spec.rb'
+ - 'spec/lib/gitlab/diff/position_spec.rb'
+ - 'spec/lib/gitlab/diff/position_tracer_spec.rb'
+ - 'spec/lib/gitlab/diff/rendered/notebook/diff_file_helper_spec.rb'
+ - 'spec/lib/gitlab/diff/suggestion_diff_spec.rb'
+ - 'spec/lib/gitlab/diff/suggestions_parser_spec.rb'
+ - 'spec/lib/gitlab/discussions_diff/file_collection_spec.rb'
+ - 'spec/lib/gitlab/doctor/secrets_spec.rb'
+ - 'spec/lib/gitlab/email/handler/service_desk_handler_spec.rb'
+ - 'spec/lib/gitlab/email/html_to_markdown_parser_spec.rb'
+ - 'spec/lib/gitlab/empty_search_results_spec.rb'
+ - 'spec/lib/gitlab/encoding_helper_spec.rb'
+ - 'spec/lib/gitlab/error_tracking/error_repository/open_api_strategy_spec.rb'
+ - 'spec/lib/gitlab/etag_caching/store_spec.rb'
+ - 'spec/lib/gitlab/event_store/store_spec.rb'
+ - 'spec/lib/gitlab/exclusive_lease_helpers/sleeping_lock_spec.rb'
+ - 'spec/lib/gitlab/exclusive_lease_helpers_spec.rb'
+ - 'spec/lib/gitlab/experiment/rollout/feature_spec.rb'
+ - 'spec/lib/gitlab/faraday/error_callback_spec.rb'
+ - 'spec/lib/gitlab/favicon_spec.rb'
+ - 'spec/lib/gitlab/feature_categories_spec.rb'
+ - 'spec/lib/gitlab/file_finder_spec.rb'
+ - 'spec/lib/gitlab/fogbugz_import/importer_spec.rb'
+ - 'spec/lib/gitlab/fogbugz_import/project_creator_spec.rb'
+ - 'spec/lib/gitlab/gfm/reference_rewriter_spec.rb'
+ - 'spec/lib/gitlab/git/attributes_at_ref_parser_spec.rb'
+ - 'spec/lib/gitlab/git/attributes_parser_spec.rb'
+ - 'spec/lib/gitlab/git/blame_pagination_spec.rb'
+ - 'spec/lib/gitlab/git/blob_spec.rb'
+ - 'spec/lib/gitlab/git/branch_spec.rb'
+ - 'spec/lib/gitlab/git/changes_spec.rb'
+ - 'spec/lib/gitlab/git/commit_spec.rb'
+ - 'spec/lib/gitlab/git/compare_spec.rb'
+ - 'spec/lib/gitlab/git/diff_collection_spec.rb'
+ - 'spec/lib/gitlab/git/finders/refs_finder_spec.rb'
+ - 'spec/lib/gitlab/git/hook_env_spec.rb'
+ - 'spec/lib/gitlab/git/lfs_changes_spec.rb'
+ - 'spec/lib/gitlab/git/lfs_pointer_file_spec.rb'
+ - 'spec/lib/gitlab/git/object_pool_spec.rb'
+ - 'spec/lib/gitlab/git/push_spec.rb'
+ - 'spec/lib/gitlab/git/repository_spec.rb'
+ - 'spec/lib/gitlab/git/tag_spec.rb'
+ - 'spec/lib/gitlab/git/user_spec.rb'
+ - 'spec/lib/gitlab/git_access_design_spec.rb'
+ - 'spec/lib/gitlab/git_access_wiki_spec.rb'
+ - 'spec/lib/gitlab/git_post_receive_spec.rb'
+ - 'spec/lib/gitlab/gitaly_client/blob_service_spec.rb'
+ - 'spec/lib/gitlab/gitaly_client/call_spec.rb'
+ - 'spec/lib/gitlab/gitaly_client/commit_service_spec.rb'
+ - 'spec/lib/gitlab/gitaly_client/conflicts_service_spec.rb'
+ - 'spec/lib/gitlab/gitaly_client/diff_spec.rb'
+ - 'spec/lib/gitlab/gitaly_client/health_check_service_spec.rb'
+ - 'spec/lib/gitlab/gitaly_client/object_pool_service_spec.rb'
+ - 'spec/lib/gitlab/gitaly_client/operation_service_spec.rb'
+ - 'spec/lib/gitlab/gitaly_client/ref_service_spec.rb'
+ - 'spec/lib/gitlab/gitaly_client/util_spec.rb'
+ - 'spec/lib/gitlab/gitaly_client_spec.rb'
+ - 'spec/lib/gitlab/github_gists_import/importer/gist_importer_spec.rb'
+ - 'spec/lib/gitlab/github_import/importer/diff_note_importer_spec.rb'
+ - 'spec/lib/gitlab/github_import/importer/protected_branches_importer_spec.rb'
+ - 'spec/lib/gitlab/github_import/importer/pull_requests/all_merged_by_importer_spec.rb'
+ - 'spec/lib/gitlab/github_import/importer/pull_requests/merged_by_importer_spec.rb'
+ - 'spec/lib/gitlab/github_import/importer/pull_requests/review_importer_spec.rb'
+ - 'spec/lib/gitlab/github_import/importer/pull_requests/reviews_importer_spec.rb'
+ - 'spec/lib/gitlab/github_import/importer/single_endpoint_diff_notes_importer_spec.rb'
+ - 'spec/lib/gitlab/github_import/importer/single_endpoint_issue_events_importer_spec.rb'
+ - 'spec/lib/gitlab/github_import/importer/single_endpoint_issue_notes_importer_spec.rb'
+ - 'spec/lib/gitlab/github_import/importer/single_endpoint_merge_request_notes_importer_spec.rb'
+ - 'spec/lib/gitlab/github_import/representation/note_text_spec.rb'
+ - 'spec/lib/gitlab/gl_repository/repo_type_spec.rb'
+ - 'spec/lib/gitlab/grape_logging/loggers/cloudflare_logger_spec.rb'
+ - 'spec/lib/gitlab/grape_logging/loggers/exception_logger_spec.rb'
+ - 'spec/lib/gitlab/grape_logging/loggers/filter_parameters_spec.rb'
+ - 'spec/lib/gitlab/grape_logging/loggers/queue_duration_logger_spec.rb'
+ - 'spec/lib/gitlab/grape_logging/loggers/response_logger_spec.rb'
+ - 'spec/lib/gitlab/grape_logging/loggers/token_logger_spec.rb'
+ - 'spec/lib/gitlab/graphql/batch_key_spec.rb'
+ - 'spec/lib/gitlab/graphql/copy_field_description_spec.rb'
+ - 'spec/lib/gitlab/graphql/known_operations_spec.rb'
+ - 'spec/lib/gitlab/graphql/lazy_spec.rb'
+ - 'spec/lib/gitlab/graphql/loaders/issuable_loader_spec.rb'
+ - 'spec/lib/gitlab/graphql/pagination/active_record_array_connection_spec.rb'
+ - 'spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb'
+ - 'spec/lib/gitlab/graphql/queries_spec.rb'
+ - 'spec/lib/gitlab/graphql_logger_spec.rb'
+ - 'spec/lib/gitlab/graphs/commits_spec.rb'
+ - 'spec/lib/gitlab/group_search_results_spec.rb'
+ - 'spec/lib/gitlab/hashed_path_spec.rb'
+ - 'spec/lib/gitlab/health_checks/db_check_spec.rb'
+ - 'spec/lib/gitlab/health_checks/gitaly_check_spec.rb'
+ - 'spec/lib/gitlab/health_checks/probes/collection_spec.rb'
+ - 'spec/lib/gitlab/health_checks/redis_spec.rb'
+ - 'spec/lib/gitlab/hook_data/base_builder_spec.rb'
+ - 'spec/lib/gitlab/http_connection_adapter_spec.rb'
+ - 'spec/lib/gitlab/http_io_spec.rb'
+ - 'spec/lib/gitlab/import/database_helpers_spec.rb'
+ - 'spec/lib/gitlab/import/merge_request_creator_spec.rb'
+ - 'spec/lib/gitlab/import/merge_request_helpers_spec.rb'
+ - 'spec/lib/gitlab/import/metrics_spec.rb'
+ - 'spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb'
+ - 'spec/lib/gitlab/import_export/attribute_cleaner_spec.rb'
+ - 'spec/lib/gitlab/import_export/attributes_permitter_spec.rb'
+ - 'spec/lib/gitlab/import_export/base/object_builder_spec.rb'
+ - 'spec/lib/gitlab/import_export/base/relation_factory_spec.rb'
+ - 'spec/lib/gitlab/import_export/command_line_util_spec.rb'
+ - 'spec/lib/gitlab/import_export/config_spec.rb'
+ - 'spec/lib/gitlab/import_export/decompressed_archive_size_validator_spec.rb'
+ - 'spec/lib/gitlab/import_export/duration_measuring_spec.rb'
+ - 'spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb'
+ - 'spec/lib/gitlab/import_export/file_importer_spec.rb'
+ - 'spec/lib/gitlab/import_export/group/relation_tree_restorer_spec.rb'
+ - 'spec/lib/gitlab/import_export/import_failure_service_spec.rb'
+ - 'spec/lib/gitlab/import_export/importer_spec.rb'
+ - 'spec/lib/gitlab/import_export/json/ndjson_reader_spec.rb'
+ - 'spec/lib/gitlab/import_export/json/ndjson_writer_spec.rb'
+ - 'spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb'
+ - 'spec/lib/gitlab/import_export/log_util_spec.rb'
+ - 'spec/lib/gitlab/import_export/project/export_task_spec.rb'
+ - 'spec/lib/gitlab/import_export/project/import_task_spec.rb'
+ - 'spec/lib/gitlab/import_export/project/object_builder_spec.rb'
+ - 'spec/lib/gitlab/import_export/project/sample/relation_tree_restorer_spec.rb'
+ - 'spec/lib/gitlab/import_export/reader_spec.rb'
+ - 'spec/lib/gitlab/import_export/remote_stream_upload_spec.rb'
+ - 'spec/lib/gitlab/import_export/repo_restorer_spec.rb'
+ - 'spec/lib/gitlab/import_export/saver_spec.rb'
+ - 'spec/lib/gitlab/import_export/shared_spec.rb'
+ - 'spec/lib/gitlab/import_export/uploads_restorer_spec.rb'
+ - 'spec/lib/gitlab/instrumentation_helper_spec.rb'
+ - 'spec/lib/gitlab/internal_post_receive/response_spec.rb'
+ - 'spec/lib/gitlab/issuable/clone/copy_resource_events_service_spec.rb'
+ - 'spec/lib/gitlab/issuable_sorter_spec.rb'
+ - 'spec/lib/gitlab/issuables_count_for_state_spec.rb'
+ - 'spec/lib/gitlab/jira_import/base_importer_spec.rb'
+ - 'spec/lib/gitlab/jira_import/handle_labels_service_spec.rb'
+ - 'spec/lib/gitlab/jira_import/issue_serializer_spec.rb'
+ - 'spec/lib/gitlab/jira_import/issues_importer_spec.rb'
+ - 'spec/lib/gitlab/jira_import/labels_importer_spec.rb'
+ - 'spec/lib/gitlab/jira_import/metadata_collector_spec.rb'
+ - 'spec/lib/gitlab/jira_import_spec.rb'
+ - 'spec/lib/gitlab/json_spec.rb'
+ - 'spec/lib/gitlab/kas/client_spec.rb'
+ - 'spec/lib/gitlab/kroki_spec.rb'
+ - 'spec/lib/gitlab/kubernetes/config_maps/aws_node_auth_spec.rb'
+ - 'spec/lib/gitlab/kubernetes/default_namespace_spec.rb'
+ - 'spec/lib/gitlab/kubernetes/kube_client_spec.rb'
+ - 'spec/lib/gitlab/kubernetes/namespace_spec.rb'
+ - 'spec/lib/gitlab/kubernetes/node_spec.rb'
+ - 'spec/lib/gitlab/kubernetes_spec.rb'
+ - 'spec/lib/gitlab/language_detection_spec.rb'
+ - 'spec/lib/gitlab/legacy_github_import/importer_spec.rb'
+ - 'spec/lib/gitlab/legacy_github_import/label_formatter_spec.rb'
+ - 'spec/lib/gitlab/legacy_github_import/user_formatter_spec.rb'
+ - 'spec/lib/gitlab/log_timestamp_formatter_spec.rb'
+ - 'spec/lib/gitlab/logger_spec.rb'
+ - 'spec/lib/gitlab/lograge/custom_options_spec.rb'
+ - 'spec/lib/gitlab/loop_helpers_spec.rb'
+ - 'spec/lib/gitlab/manifest_import/metadata_spec.rb'
+ - 'spec/lib/gitlab/manifest_import/project_creator_spec.rb'
+ - 'spec/lib/gitlab/memory/diagnostic_reports_logger_spec.rb'
+ - 'spec/lib/gitlab/memory/instrumentation_spec.rb'
+ - 'spec/lib/gitlab/memory/watchdog/event_reporter_spec.rb'
+ - 'spec/lib/gitlab/metrics/methods_spec.rb'
+ - 'spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb'
+ - 'spec/lib/gitlab/metrics/samplers/action_cable_sampler_spec.rb'
+ - 'spec/lib/gitlab/metrics/samplers/database_sampler_spec.rb'
+ - 'spec/lib/gitlab/metrics/samplers/puma_sampler_spec.rb'
+ - 'spec/lib/gitlab/metrics/samplers/threads_sampler_spec.rb'
+ - 'spec/lib/gitlab/metrics/subscribers/load_balancing_spec.rb'
+ - 'spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb'
+ - 'spec/lib/gitlab/metrics_spec.rb'
+ - 'spec/lib/gitlab/middleware/handle_malformed_strings_spec.rb'
+ - 'spec/lib/gitlab/middleware/multipart_spec.rb'
+ - 'spec/lib/gitlab/middleware/path_traversal_check_spec.rb'
+ - 'spec/lib/gitlab/middleware/query_analyzer_spec.rb'
+ - 'spec/lib/gitlab/middleware/rack_multipart_tempfile_factory_spec.rb'
+ - 'spec/lib/gitlab/middleware/request_context_spec.rb'
+ - 'spec/lib/gitlab/monitor/demo_projects_spec.rb'
+ - 'spec/lib/gitlab/multi_destination_logger_spec.rb'
+ - 'spec/lib/gitlab/namespaced_session_store_spec.rb'
+ - 'spec/lib/gitlab/no_cache_headers_spec.rb'
+ - 'spec/lib/gitlab/noteable_metadata_spec.rb'
+ - 'spec/lib/gitlab/object_hierarchy_spec.rb'
+ - 'spec/lib/gitlab/omniauth_initializer_spec.rb'
+ - 'spec/lib/gitlab/optimistic_locking_spec.rb'
+ - 'spec/lib/gitlab/pages/settings_spec.rb'
+ - 'spec/lib/gitlab/pagination/cursor_based_keyset_spec.rb'
+ - 'spec/lib/gitlab/pagination/keyset/order_spec.rb'
+ - 'spec/lib/gitlab/pagination/keyset/page_spec.rb'
+ - 'spec/lib/gitlab/pagination/keyset/pager_spec.rb'
+ - 'spec/lib/gitlab/pagination/keyset/request_context_spec.rb'
+ - 'spec/lib/gitlab/pagination/keyset_spec.rb'
+ - 'spec/lib/gitlab/pagination/offset_header_builder_spec.rb'
+ - 'spec/lib/gitlab/pagination/offset_pagination_spec.rb'
+ - 'spec/lib/gitlab/patch/draw_route_spec.rb'
+ - 'spec/lib/gitlab/patch/prependable_spec.rb'
+ - 'spec/lib/gitlab/patch/sidekiq_scheduled_enq_spec.rb'
+ - 'spec/lib/gitlab/path_regex_spec.rb'
+ - 'spec/lib/gitlab/performance_bar/stats_spec.rb'
+ - 'spec/lib/gitlab/performance_bar/with_top_level_warnings_spec.rb'
+ - 'spec/lib/gitlab/plantuml_spec.rb'
+ - 'spec/lib/gitlab/popen/runner_spec.rb'
+ - 'spec/lib/gitlab/popen_spec.rb'
+ - 'spec/lib/gitlab/process_memory_cache/helper_spec.rb'
+ - 'spec/lib/gitlab/prometheus/adapter_spec.rb'
+ - 'spec/lib/gitlab/prometheus_client_spec.rb'
+ - 'spec/lib/gitlab/puma/error_handler_spec.rb'
+ - 'spec/lib/gitlab/puma_logging/json_formatter_spec.rb'
+ - 'spec/lib/gitlab/quick_actions/command_definition_spec.rb'
+ - 'spec/lib/gitlab/quick_actions/spend_time_and_date_separator_spec.rb'
+ - 'spec/lib/gitlab/quick_actions/substitution_definition_spec.rb'
+ - 'spec/lib/gitlab/rack_attack/store_spec.rb'
+ - 'spec/lib/gitlab/rack_attack/user_allowlist_spec.rb'
+ - 'spec/lib/gitlab/rack_attack_spec.rb'
+ - 'spec/lib/gitlab/reactive_cache_set_cache_spec.rb'
+ - 'spec/lib/gitlab/redis/boolean_spec.rb'
+ - 'spec/lib/gitlab/redis/cross_slot_spec.rb'
+ - 'spec/lib/gitlab/redis/db_load_balancing_spec.rb'
+ - 'spec/lib/gitlab/redis/multi_store_spec.rb'
+ - 'spec/lib/gitlab/redis/queues_spec.rb'
+ - 'spec/lib/gitlab/redis/sidekiq_status_spec.rb'
+ - 'spec/lib/gitlab/reference_extractor_spec.rb'
+ - 'spec/lib/gitlab/regex_spec.rb'
+ - 'spec/lib/gitlab/relative_positioning/item_context_spec.rb'
+ - 'spec/lib/gitlab/relative_positioning/mover_spec.rb'
+ - 'spec/lib/gitlab/repository_archive_rate_limiter_spec.rb'
+ - 'spec/lib/gitlab/repository_cache_adapter_spec.rb'
+ - 'spec/lib/gitlab/repository_hash_cache_spec.rb'
+ - 'spec/lib/gitlab/repository_set_cache_spec.rb'
+ - 'spec/lib/gitlab/repository_size_checker_spec.rb'
+ - 'spec/lib/gitlab/request_context_spec.rb'
+ - 'spec/lib/gitlab/route_map_spec.rb'
+ - 'spec/lib/gitlab/routing_spec.rb'
+ - 'spec/lib/gitlab/runtime_spec.rb'
+ - 'spec/lib/gitlab/sanitizers/exif_spec.rb'
+ - 'spec/lib/gitlab/search/found_blob_spec.rb'
+ - 'spec/lib/gitlab/search/found_wiki_page_spec.rb'
+ - 'spec/lib/gitlab/search/params_spec.rb'
+ - 'spec/lib/gitlab/search/query_spec.rb'
+ - 'spec/lib/gitlab/search_context/builder_spec.rb'
+ - 'spec/lib/gitlab/serializer/pagination_spec.rb'
+ - 'spec/lib/gitlab/setup_helper/workhorse_spec.rb'
+ - 'spec/lib/gitlab/shell_spec.rb'
+ - 'spec/lib/gitlab/sidekiq_daemon/monitor_spec.rb'
+ - 'spec/lib/gitlab/sidekiq_logging/json_formatter_spec.rb'
+ - 'spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb'
+ - 'spec/lib/gitlab/sidekiq_middleware/admin_mode/client_spec.rb'
+ - 'spec/lib/gitlab/sidekiq_middleware/admin_mode/server_spec.rb'
+ - 'spec/lib/gitlab/sidekiq_middleware/client_metrics_spec.rb'
+ - 'spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb'
+ - 'spec/lib/gitlab/sidekiq_middleware/extra_done_log_metadata_spec.rb'
+ - 'spec/lib/gitlab/sidekiq_middleware/instrumentation_logger_spec.rb'
+ - 'spec/lib/gitlab/sidekiq_middleware/monitor_spec.rb'
+ - 'spec/lib/gitlab/sidekiq_middleware/pause_control/pause_control_service_spec.rb'
+ - 'spec/lib/gitlab/sidekiq_middleware/query_analyzer_spec.rb'
+ - 'spec/lib/gitlab/sidekiq_middleware/size_limiter/server_spec.rb'
+ - 'spec/lib/gitlab/sidekiq_middleware/skip_jobs_spec.rb'
+ - 'spec/lib/gitlab/slash_commands/application_help_spec.rb'
+ - 'spec/lib/gitlab/slash_commands/command_spec.rb'
+ - 'spec/lib/gitlab/slash_commands/deploy_spec.rb'
+ - 'spec/lib/gitlab/slash_commands/incident_management/incident_new_spec.rb'
+ - 'spec/lib/gitlab/slash_commands/issue_close_spec.rb'
+ - 'spec/lib/gitlab/slash_commands/issue_comment_spec.rb'
+ - 'spec/lib/gitlab/slash_commands/issue_new_spec.rb'
+ - 'spec/lib/gitlab/slash_commands/issue_search_spec.rb'
+ - 'spec/lib/gitlab/slash_commands/issue_show_spec.rb'
+ - 'spec/lib/gitlab/slash_commands/presenters/access_spec.rb'
+ - 'spec/lib/gitlab/slash_commands/presenters/deploy_spec.rb'
+ - 'spec/lib/gitlab/slash_commands/presenters/error_spec.rb'
+ - 'spec/lib/gitlab/slash_commands/presenters/incident_management/incident_new_spec.rb'
+ - 'spec/lib/gitlab/slash_commands/presenters/issue_close_spec.rb'
+ - 'spec/lib/gitlab/slash_commands/presenters/issue_comment_spec.rb'
+ - 'spec/lib/gitlab/slash_commands/presenters/issue_move_spec.rb'
+ - 'spec/lib/gitlab/slash_commands/presenters/issue_new_spec.rb'
+ - 'spec/lib/gitlab/slash_commands/presenters/issue_search_spec.rb'
+ - 'spec/lib/gitlab/slash_commands/presenters/issue_show_spec.rb'
+ - 'spec/lib/gitlab/spamcheck/client_spec.rb'
+ - 'spec/lib/gitlab/spamcheck/result_spec.rb'
+ - 'spec/lib/gitlab/string_regex_marker_spec.rb'
+ - 'spec/lib/gitlab/submodule_links_spec.rb'
+ - 'spec/lib/gitlab/task_helpers_spec.rb'
+ - 'spec/lib/gitlab/template/gitignore_template_spec.rb'
+ - 'spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb'
+ - 'spec/lib/gitlab/terraform/state_migration_helper_spec.rb'
+ - 'spec/lib/gitlab/terraform_registry_token_spec.rb'
+ - 'spec/lib/gitlab/throttle_spec.rb'
+ - 'spec/lib/gitlab/time_tracking_formatter_spec.rb'
+ - 'spec/lib/gitlab/tracking/destinations/database_events_snowplow_spec.rb'
+ - 'spec/lib/gitlab/tracking/destinations/snowplow_micro_spec.rb'
+ - 'spec/lib/gitlab/tracking/destinations/snowplow_spec.rb'
+ - 'spec/lib/gitlab/tracking_spec.rb'
+ - 'spec/lib/gitlab/tree_summary_spec.rb'
+ - 'spec/lib/gitlab/unicode_spec.rb'
+ - 'spec/lib/gitlab/untrusted_regexp_spec.rb'
+ - 'spec/lib/gitlab/url_blocker_spec.rb'
+ - 'spec/lib/gitlab/url_builder_spec.rb'
+ - 'spec/lib/gitlab/usage/metric_definition_spec.rb'
+ - 'spec/lib/gitlab/usage/metrics/instrumentations/database_metric_spec.rb'
+ - 'spec/lib/gitlab/usage/metrics/instrumentations/generic_metric_spec.rb'
+ - 'spec/lib/gitlab/usage/metrics/instrumentations/numbers_metric_spec.rb'
+ - 'spec/lib/gitlab/usage/metrics/instrumentations/redis_hll_metric_spec.rb'
+ - 'spec/lib/gitlab/usage/metrics/instrumentations/redis_metric_spec.rb'
+ - 'spec/lib/gitlab/usage_data/topology_spec.rb'
+ - 'spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb'
+ - 'spec/lib/gitlab/usage_data_counters/kubernetes_agent_counter_spec.rb'
+ - 'spec/lib/gitlab/usage_data_counters/redis_counter_spec.rb'
+ - 'spec/lib/gitlab/usage_data_counters_spec.rb'
+ - 'spec/lib/gitlab/usage_data_metrics_spec.rb'
+ - 'spec/lib/gitlab/usage_data_spec.rb'
+ - 'spec/lib/gitlab/utils/deep_size_spec.rb'
+ - 'spec/lib/gitlab/utils/delegator_override_spec.rb'
+ - 'spec/lib/gitlab/utils/file_info_spec.rb'
+ - 'spec/lib/gitlab/utils/gzip_spec.rb'
+ - 'spec/lib/gitlab/utils/inline_hash_spec.rb'
+ - 'spec/lib/gitlab/utils/measuring_spec.rb'
+ - 'spec/lib/gitlab/utils/mime_type_spec.rb'
+ - 'spec/lib/gitlab/utils/override_spec.rb'
+ - 'spec/lib/gitlab/utils/safe_inline_hash_spec.rb'
+ - 'spec/lib/gitlab/utils/sanitize_node_link_spec.rb'
+ - 'spec/lib/gitlab/utils/usage_data_spec.rb'
+ - 'spec/lib/gitlab/utils/username_and_email_generator_spec.rb'
+ - 'spec/lib/gitlab/word_diff/parser_spec.rb'
+ - 'spec/lib/gitlab/work_items/work_item_hierarchy_spec.rb'
+ - 'spec/lib/gitlab/workhorse_spec.rb'
+ - 'spec/lib/gitlab/x509/certificate_spec.rb'
+ - 'spec/lib/gitlab_spec.rb'
+ - 'spec/lib/google_api/auth_spec.rb'
+ - 'spec/lib/google_api/cloud_platform/client_spec.rb'
+ - 'spec/lib/grafana/time_window_spec.rb'
+ - 'spec/lib/grafana/validator_spec.rb'
+ - 'spec/lib/json_web_token/rsa_token_spec.rb'
+ - 'spec/lib/kramdown/kramdown_spec.rb'
+ - 'spec/lib/mattermost/client_spec.rb'
+ - 'spec/lib/mattermost/command_spec.rb'
+ - 'spec/lib/mattermost/session_spec.rb'
+ - 'spec/lib/mattermost/team_spec.rb'
+ - 'spec/lib/microsoft_teams/activity_spec.rb'
+ - 'spec/lib/microsoft_teams/notifier_spec.rb'
+ - 'spec/lib/object_storage/config_spec.rb'
+ - 'spec/lib/object_storage/direct_upload_spec.rb'
+ - 'spec/lib/object_storage/fog_helpers_spec.rb'
+ - 'spec/lib/omni_auth/strategies/bitbucket_spec.rb'
+ - 'spec/lib/omni_auth/strategies/jwt_spec.rb'
+ - 'spec/lib/peek/views/active_record_spec.rb'
+ - 'spec/lib/peek/views/bullet_detailed_spec.rb'
+ - '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'
+ - 'spec/lib/safe_zip/entry_spec.rb'
+ - 'spec/lib/sbom/package_url_spec.rb'
+ - 'spec/lib/security/report_schema_version_matcher_spec.rb'
+ - 'spec/lib/security/weak_passwords_spec.rb'
+ - 'spec/lib/service_ping/devops_report_spec.rb'
+ - 'spec/lib/sidebars/admin/menus/abuse_reports_menu_spec.rb'
+ - 'spec/lib/sidebars/concerns/container_with_html_options_spec.rb'
+ - 'spec/lib/sidebars/concerns/has_avatar_spec.rb'
+ - 'spec/lib/sidebars/concerns/link_with_html_options_spec.rb'
+ - 'spec/lib/sidebars/concerns/super_sidebar_panel_spec.rb'
+ - 'spec/lib/sidebars/explore/menus/catalog_menu_spec.rb'
+ - 'spec/lib/sidebars/explore/panel_spec.rb'
+ - 'spec/lib/sidebars/groups/menus/packages_registries_menu_spec.rb'
+ - 'spec/lib/sidebars/groups/super_sidebar_menus/analyze_menu_spec.rb'
+ - 'spec/lib/sidebars/groups/super_sidebar_menus/build_menu_spec.rb'
+ - 'spec/lib/sidebars/groups/super_sidebar_menus/deploy_menu_spec.rb'
+ - 'spec/lib/sidebars/groups/super_sidebar_menus/manage_menu_spec.rb'
+ - 'spec/lib/sidebars/groups/super_sidebar_menus/operations_menu_spec.rb'
+ - 'spec/lib/sidebars/groups/super_sidebar_menus/plan_menu_spec.rb'
+ - 'spec/lib/sidebars/groups/super_sidebar_menus/secure_menu_spec.rb'
+ - 'spec/lib/sidebars/groups/super_sidebar_panel_spec.rb'
+ - 'spec/lib/sidebars/menu_item_spec.rb'
+ - 'spec/lib/sidebars/menu_spec.rb'
+ - 'spec/lib/sidebars/organizations/menus/manage_menu_spec.rb'
+ - 'spec/lib/sidebars/organizations/menus/settings_menu_spec.rb'
+ - 'spec/lib/sidebars/organizations/panel_spec.rb'
+ - 'spec/lib/sidebars/organizations/super_sidebar_panel_spec.rb'
+ - 'spec/lib/sidebars/projects/context_spec.rb'
+ - 'spec/lib/sidebars/projects/menus/analytics_menu_spec.rb'
+ - 'spec/lib/sidebars/projects/menus/ci_cd_menu_spec.rb'
+ - 'spec/lib/sidebars/projects/menus/confluence_menu_spec.rb'
+ - 'spec/lib/sidebars/projects/menus/deployments_menu_spec.rb'
+ - 'spec/lib/sidebars/projects/menus/external_issue_tracker_menu_spec.rb'
+ - 'spec/lib/sidebars/projects/menus/external_wiki_menu_spec.rb'
+ - 'spec/lib/sidebars/projects/menus/hidden_menu_spec.rb'
+ - 'spec/lib/sidebars/projects/menus/infrastructure_menu_spec.rb'
+ - 'spec/lib/sidebars/projects/menus/issues_menu_spec.rb'
+ - 'spec/lib/sidebars/projects/menus/merge_requests_menu_spec.rb'
+ - 'spec/lib/sidebars/projects/menus/monitor_menu_spec.rb'
+ - 'spec/lib/sidebars/projects/menus/packages_registries_menu_spec.rb'
+ - 'spec/lib/sidebars/projects/menus/repository_menu_spec.rb'
+ - 'spec/lib/sidebars/projects/menus/settings_menu_spec.rb'
+ - 'spec/lib/sidebars/projects/menus/snippets_menu_spec.rb'
+ - 'spec/lib/sidebars/projects/menus/wiki_menu_spec.rb'
+ - 'spec/lib/sidebars/projects/panel_spec.rb'
+ - 'spec/lib/sidebars/projects/super_sidebar_menus/analyze_menu_spec.rb'
+ - 'spec/lib/sidebars/projects/super_sidebar_menus/build_menu_spec.rb'
+ - 'spec/lib/sidebars/projects/super_sidebar_menus/code_menu_spec.rb'
+ - 'spec/lib/sidebars/projects/super_sidebar_menus/deploy_menu_spec.rb'
+ - 'spec/lib/sidebars/projects/super_sidebar_menus/manage_menu_spec.rb'
+ - 'spec/lib/sidebars/projects/super_sidebar_menus/monitor_menu_spec.rb'
+ - 'spec/lib/sidebars/projects/super_sidebar_menus/operations_menu_spec.rb'
+ - 'spec/lib/sidebars/projects/super_sidebar_menus/plan_menu_spec.rb'
+ - 'spec/lib/sidebars/projects/super_sidebar_menus/secure_menu_spec.rb'
+ - 'spec/lib/sidebars/projects/super_sidebar_panel_spec.rb'
+ - 'spec/lib/sidebars/static_menu_spec.rb'
+ - 'spec/lib/sidebars/uncategorized_menu_spec.rb'
+ - 'spec/lib/sidebars/user_profile/panel_spec.rb'
+ - 'spec/lib/sidebars/user_settings/menus/access_tokens_menu_spec.rb'
+ - 'spec/lib/sidebars/user_settings/menus/comment_templates_menu_spec.rb'
+ - 'spec/lib/sidebars/user_settings/menus/password_menu_spec.rb'
+ - 'spec/lib/sidebars/user_settings/panel_spec.rb'
+ - 'spec/lib/sidebars/your_work/menus/organizations_menu_spec.rb'
+ - 'spec/lib/sidebars/your_work/panel_spec.rb'
+ - 'spec/lib/system_check/app/hashed_storage_all_projects_check_spec.rb'
+ - 'spec/lib/system_check/app/hashed_storage_enabled_check_spec.rb'
+ - 'spec/lib/system_check/base_check_spec.rb'
+ - 'spec/lib/system_check/incoming_email_check_spec.rb'
+ - 'spec/lib/system_check/orphans/namespace_check_spec.rb'
+ - 'spec/lib/system_check/orphans/repository_check_spec.rb'
+ - 'spec/lib/system_check/sidekiq_check_spec.rb'
+ - 'spec/lib/system_check/simple_executor_spec.rb'
+ - 'spec/lib/system_check_spec.rb'
+ - 'spec/lib/uploaded_file_spec.rb'
+ - 'spec/mailers/devise_mailer_spec.rb'
+ - 'spec/mailers/emails/admin_notification_spec.rb'
+ - 'spec/mailers/emails/auto_devops_spec.rb'
+ - 'spec/mailers/emails/groups_spec.rb'
+ - 'spec/mailers/emails/imports_spec.rb'
+ - 'spec/mailers/emails/issues_spec.rb'
+ - 'spec/mailers/emails/merge_requests_spec.rb'
+ - 'spec/mailers/emails/service_desk_spec.rb'
+ - 'spec/mailers/notify_spec.rb'
+ - 'spec/metrics_server/metrics_server_spec.rb'
+ - 'spec/migrations/20221028022627_add_index_on_password_last_changed_at_to_user_details_spec.rb'
+ - 'spec/migrations/20221101032600_add_text_limit_to_default_preferred_language_on_application_settings_spec.rb'
+ - 'spec/migrations/20221210154044_update_active_billable_users_index_spec.rb'
+ - 'spec/migrations/20221221110733_remove_temp_index_for_project_statistics_upload_size_migration_spec.rb'
+ - 'spec/migrations/20230714015909_add_index_for_member_expiring_query_spec.rb'
+ - 'spec/migrations/drop_packages_events_table_spec.rb'
+ - 'spec/models/ability_spec.rb'
+ - 'spec/models/abuse/trust_score_spec.rb'
+ - 'spec/models/abuse_report_spec.rb'
+ - 'spec/models/active_session_spec.rb'
+ - 'spec/models/ai/service_access_token_spec.rb'
+ - 'spec/models/alert_management/alert_spec.rb'
+ - 'spec/models/alerting/project_alerting_setting_spec.rb'
+ - 'spec/models/analytics/usage_trends/measurement_spec.rb'
+ - 'spec/models/application_setting_spec.rb'
+ - 'spec/models/audit_event_spec.rb'
+ - 'spec/models/aws/role_spec.rb'
+ - 'spec/models/blob_spec.rb'
+ - 'spec/models/blob_viewer/changelog_spec.rb'
+ - 'spec/models/blob_viewer/composer_json_spec.rb'
+ - 'spec/models/blob_viewer/gemspec_spec.rb'
+ - 'spec/models/blob_viewer/go_mod_spec.rb'
+ - 'spec/models/blob_viewer/license_spec.rb'
+ - 'spec/models/blob_viewer/markup_spec.rb'
+ - 'spec/models/blob_viewer/package_json_spec.rb'
+ - 'spec/models/blob_viewer/podspec_json_spec.rb'
+ - 'spec/models/blob_viewer/podspec_spec.rb'
+ - 'spec/models/blob_viewer/readme_spec.rb'
+ - 'spec/models/blob_viewer/route_map_spec.rb'
+ - 'spec/models/blob_viewer/server_side_spec.rb'
+ - 'spec/models/bulk_imports/entity_spec.rb'
+ - 'spec/models/bulk_imports/export_status_spec.rb'
+ - 'spec/models/bulk_imports/export_upload_spec.rb'
+ - 'spec/models/bulk_imports/file_transfer/group_config_spec.rb'
+ - 'spec/models/bulk_imports/file_transfer/project_config_spec.rb'
+ - 'spec/models/chat_name_spec.rb'
+ - 'spec/models/ci/bridge_spec.rb'
+ - 'spec/models/ci/build_dependencies_spec.rb'
+ - 'spec/models/ci/build_metadata_spec.rb'
+ - 'spec/models/ci/build_runner_session_spec.rb'
+ - 'spec/models/ci/build_spec.rb'
+ - 'spec/models/ci/build_trace_chunk_spec.rb'
+ - 'spec/models/ci/build_trace_chunks/database_spec.rb'
+ - 'spec/models/ci/build_trace_chunks/fog_spec.rb'
+ - 'spec/models/ci/build_trace_spec.rb'
+ - 'spec/models/ci/daily_build_group_report_result_spec.rb'
+ - 'spec/models/ci/external_pull_request_spec.rb'
+ - 'spec/models/ci/group_spec.rb'
+ - 'spec/models/ci/group_variable_spec.rb'
+ - 'spec/models/ci/job_annotation_spec.rb'
+ - 'spec/models/ci/job_artifact_spec.rb'
+ - 'spec/models/ci/job_token/allowlist_spec.rb'
+ - 'spec/models/ci/job_token/project_scope_link_spec.rb'
+ - 'spec/models/ci/job_token/scope_spec.rb'
+ - 'spec/models/ci/pending_build_spec.rb'
+ - 'spec/models/ci/persistent_ref_spec.rb'
+ - 'spec/models/ci/pipeline_artifact_spec.rb'
+ - 'spec/models/ci/pipeline_message_spec.rb'
+ - 'spec/models/ci/pipeline_schedule_spec.rb'
+ - 'spec/models/ci/pipeline_spec.rb'
+ - 'spec/models/ci/processable_spec.rb'
+ - 'spec/models/ci/ref_spec.rb'
+ - 'spec/models/ci/resource_group_spec.rb'
+ - 'spec/models/ci/runner_spec.rb'
+ - 'spec/models/ci/secure_file_spec.rb'
+ - 'spec/models/ci/stage_spec.rb'
+ - 'spec/models/ci/variable_spec.rb'
+ - 'spec/models/clusters/agent_spec.rb'
+ - 'spec/models/clusters/agents/authorizations/ci_access/implicit_authorization_spec.rb'
+ - 'spec/models/clusters/agents/authorizations/user_access/group_authorization_spec.rb'
+ - 'spec/models/clusters/agents/authorizations/user_access/project_authorization_spec.rb'
+ - 'spec/models/clusters/cluster_spec.rb'
+ - 'spec/models/clusters/platforms/kubernetes_spec.rb'
+ - 'spec/models/clusters/providers/aws_spec.rb'
+ - 'spec/models/commit_collection_spec.rb'
+ - 'spec/models/commit_status_spec.rb'
+ - 'spec/models/compare_spec.rb'
+ - 'spec/models/concerns/approvable_spec.rb'
+ - 'spec/models/concerns/as_cte_spec.rb'
+ - 'spec/models/concerns/atomic_internal_id_spec.rb'
+ - 'spec/models/concerns/checksummable_spec.rb'
+ - 'spec/models/concerns/chronic_duration_attribute_spec.rb'
+ - 'spec/models/concerns/ci/has_variable_spec.rb'
+ - 'spec/models/concerns/ci/maskable_spec.rb'
+ - 'spec/models/concerns/ci/partitionable_spec.rb'
+ - 'spec/models/concerns/clusters/agents/authorizations/user_access/scopes_spec.rb'
+ - 'spec/models/concerns/deployment_platform_spec.rb'
+ - 'spec/models/concerns/discussion_on_diff_spec.rb'
+ - 'spec/models/concerns/exportable_spec.rb'
+ - 'spec/models/concerns/featurable_spec.rb'
+ - 'spec/models/concerns/from_set_operator_spec.rb'
+ - 'spec/models/concerns/has_environment_scope_spec.rb'
+ - 'spec/models/concerns/ignorable_columns_spec.rb'
+ - 'spec/models/concerns/issuable_spec.rb'
+ - 'spec/models/concerns/mentionable_spec.rb'
+ - 'spec/models/concerns/noteable_spec.rb'
+ - 'spec/models/concerns/packages/downloadable_spec.rb'
+ - 'spec/models/concerns/partitioned_table_spec.rb'
+ - 'spec/models/concerns/reactive_caching_spec.rb'
+ - 'spec/models/concerns/recoverable_by_any_email_spec.rb'
+ - 'spec/models/concerns/redis_cacheable_spec.rb'
+ - 'spec/models/concerns/require_email_verification_spec.rb'
+ - 'spec/models/concerns/resolvable_discussion_spec.rb'
+ - 'spec/models/concerns/resolvable_note_spec.rb'
+ - 'spec/models/concerns/runners_token_prefixable_spec.rb'
+ - 'spec/models/concerns/spammable_spec.rb'
+ - 'spec/models/concerns/stepable_spec.rb'
+ - 'spec/models/concerns/subquery_spec.rb'
+ - 'spec/models/concerns/token_authenticatable_spec.rb'
+ - 'spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb'
+ - 'spec/models/container_expiration_policy_spec.rb'
+ - 'spec/models/container_registry/event_spec.rb'
+ - 'spec/models/container_repository_spec.rb'
+ - 'spec/models/context_commits_diff_spec.rb'
+ - 'spec/models/customer_relations/issue_contact_spec.rb'
+ - 'spec/models/cycle_analytics/project_level_stage_adapter_spec.rb'
+ - 'spec/models/deploy_key_spec.rb'
+ - 'spec/models/deploy_keys_project_spec.rb'
+ - 'spec/models/deploy_token_spec.rb'
+ - 'spec/models/deployment_spec.rb'
+ - 'spec/models/design_management/action_spec.rb'
+ - 'spec/models/design_management/design_action_spec.rb'
+ - 'spec/models/design_management/design_at_version_spec.rb'
+ - 'spec/models/design_management/repository_spec.rb'
+ - 'spec/models/design_management/version_spec.rb'
+ - 'spec/models/diff_discussion_spec.rb'
+ - 'spec/models/diff_note_spec.rb'
+ - 'spec/models/diff_viewer/server_side_spec.rb'
+ - 'spec/models/discussion_spec.rb'
+ - 'spec/models/environment_spec.rb'
+ - 'spec/models/environment_status_spec.rb'
+ - 'spec/models/error_tracking/client_key_spec.rb'
+ - 'spec/models/error_tracking/error_spec.rb'
+ - 'spec/models/error_tracking/project_error_tracking_setting_spec.rb'
+ - 'spec/models/event_collection_spec.rb'
+ - 'spec/models/group_deploy_key_spec.rb'
+ - 'spec/models/group_spec.rb'
+ - 'spec/models/hooks/web_hook_log_spec.rb'
+ - 'spec/models/hooks/web_hook_spec.rb'
+ - 'spec/models/import_export_upload_spec.rb'
+ - 'spec/models/import_failure_spec.rb'
+ - 'spec/models/incident_management/project_incident_management_setting_spec.rb'
+ - 'spec/models/instance_configuration_spec.rb'
+ - 'spec/models/instance_metadata/kas_spec.rb'
+ - 'spec/models/instance_metadata_spec.rb'
+ - 'spec/models/integration_spec.rb'
+ - 'spec/models/integrations/apple_app_store_spec.rb'
+ - 'spec/models/integrations/asana_spec.rb'
+ - 'spec/models/integrations/assembla_spec.rb'
+ - 'spec/models/integrations/bamboo_spec.rb'
+ - 'spec/models/integrations/base_chat_notification_spec.rb'
+ - 'spec/models/integrations/base_issue_tracker_spec.rb'
+ - 'spec/models/integrations/base_third_party_wiki_spec.rb'
+ - 'spec/models/integrations/bugzilla_spec.rb'
+ - 'spec/models/integrations/buildkite_spec.rb'
+ - 'spec/models/integrations/campfire_spec.rb'
+ - 'spec/models/integrations/chat_message/alert_message_spec.rb'
+ - 'spec/models/integrations/chat_message/deployment_message_spec.rb'
+ - 'spec/models/integrations/chat_message/group_mention_message_spec.rb'
+ - 'spec/models/integrations/chat_message/issue_message_spec.rb'
+ - 'spec/models/integrations/chat_message/merge_message_spec.rb'
+ - 'spec/models/integrations/chat_message/note_message_spec.rb'
+ - 'spec/models/integrations/chat_message/pipeline_message_spec.rb'
+ - 'spec/models/integrations/chat_message/push_message_spec.rb'
+ - 'spec/models/integrations/chat_message/wiki_page_message_spec.rb'
+ - 'spec/models/integrations/clickup_spec.rb'
+ - 'spec/models/integrations/confluence_spec.rb'
+ - 'spec/models/integrations/custom_issue_tracker_spec.rb'
+ - 'spec/models/integrations/discord_spec.rb'
+ - 'spec/models/integrations/drone_ci_spec.rb'
+ - 'spec/models/integrations/emails_on_push_spec.rb'
+ - 'spec/models/integrations/ewm_spec.rb'
+ - 'spec/models/integrations/external_wiki_spec.rb'
+ - 'spec/models/integrations/gitlab_slack_application_spec.rb'
+ - 'spec/models/integrations/google_play_spec.rb'
+ - 'spec/models/integrations/integration_list_spec.rb'
+ - 'spec/models/integrations/irker_spec.rb'
+ - 'spec/models/integrations/jenkins_spec.rb'
+ - 'spec/models/integrations/jira_spec.rb'
+ - 'spec/models/integrations/mattermost_slash_commands_spec.rb'
+ - 'spec/models/integrations/pipelines_email_spec.rb'
+ - 'spec/models/integrations/pivotaltracker_spec.rb'
+ - 'spec/models/integrations/pushover_spec.rb'
+ - 'spec/models/integrations/redmine_spec.rb'
+ - 'spec/models/integrations/shimo_spec.rb'
+ - 'spec/models/integrations/squash_tm_spec.rb'
+ - 'spec/models/integrations/teamcity_spec.rb'
+ - 'spec/models/integrations/telegram_spec.rb'
+ - 'spec/models/integrations/youtrack_spec.rb'
+ - 'spec/models/integrations/zentao_spec.rb'
+ - 'spec/models/internal_id_spec.rb'
+ - 'spec/models/issue/metrics_spec.rb'
+ - 'spec/models/issue_email_participant_spec.rb'
+ - 'spec/models/issue_spec.rb'
+ - 'spec/models/jira_connect_installation_spec.rb'
+ - 'spec/models/jira_import_state_spec.rb'
+ - 'spec/models/label_priority_spec.rb'
+ - 'spec/models/legacy_diff_discussion_spec.rb'
+ - 'spec/models/lfs_download_object_spec.rb'
+ - 'spec/models/lfs_object_spec.rb'
+ - 'spec/models/lfs_objects_project_spec.rb'
+ - 'spec/models/loose_foreign_keys/modification_tracker_spec.rb'
+ - 'spec/models/member_spec.rb'
+ - 'spec/models/merge_request_diff_commit_spec.rb'
+ - 'spec/models/merge_request_diff_spec.rb'
+ - 'spec/models/merge_request_spec.rb'
+ - 'spec/models/milestone_note_spec.rb'
+ - 'spec/models/milestone_release_spec.rb'
+ - 'spec/models/milestone_spec.rb'
+ - 'spec/models/ml/candidate_metric_spec.rb'
+ - 'spec/models/ml/candidate_spec.rb'
+ - 'spec/models/ml/experiment_spec.rb'
+ - 'spec/models/ml/model_spec.rb'
+ - 'spec/models/namespace/package_setting_spec.rb'
+ - 'spec/models/namespace/traversal_hierarchy_spec.rb'
+ - 'spec/models/namespace_setting_spec.rb'
+ - 'spec/models/namespace_spec.rb'
+ - 'spec/models/note_spec.rb'
+ - 'spec/models/notification_recipient_spec.rb'
+ - 'spec/models/notification_setting_spec.rb'
+ - 'spec/models/operations/feature_flag_spec.rb'
+ - 'spec/models/operations/feature_flags_client_spec.rb'
+ - 'spec/models/packages/dependency_link_spec.rb'
+ - 'spec/models/packages/dependency_spec.rb'
+ - 'spec/models/packages/npm/metadata_cache_spec.rb'
+ - 'spec/models/packages/nuget/metadatum_spec.rb'
+ - 'spec/models/packages/package_file_spec.rb'
+ - 'spec/models/packages/package_spec.rb'
+ - 'spec/models/packages/rpm/repository_file_spec.rb'
+ - 'spec/models/pages_domain_spec.rb'
+ - 'spec/models/personal_access_token_spec.rb'
+ - 'spec/models/plan_limits_spec.rb'
+ - 'spec/models/project_authorization_spec.rb'
+ - 'spec/models/project_ci_cd_setting_spec.rb'
+ - 'spec/models/project_feature_spec.rb'
+ - 'spec/models/project_import_state_spec.rb'
+ - 'spec/models/project_label_spec.rb'
+ - 'spec/models/project_setting_spec.rb'
+ - 'spec/models/project_spec.rb'
+ - 'spec/models/project_wiki_spec.rb'
+ - 'spec/models/projects/branch_rule_spec.rb'
+ - 'spec/models/projects/data_transfer_spec.rb'
+ - 'spec/models/projects/import_export/relation_export_spec.rb'
+ - 'spec/models/projects/import_export/relation_export_upload_spec.rb'
+ - 'spec/models/projects/project_topic_spec.rb'
+ - 'spec/models/projects/topic_spec.rb'
+ - 'spec/models/prometheus_alert_event_spec.rb'
+ - 'spec/models/prometheus_alert_spec.rb'
+ - 'spec/models/prometheus_metric_spec.rb'
+ - 'spec/models/protected_branch_spec.rb'
+ - 'spec/models/release_highlight_spec.rb'
+ - 'spec/models/releases/source_spec.rb'
+ - 'spec/models/remote_mirror_spec.rb'
+ - 'spec/models/repository_spec.rb'
+ - 'spec/models/resource_label_event_spec.rb'
+ - 'spec/models/resource_state_event_spec.rb'
+ - 'spec/models/sent_notification_spec.rb'
+ - 'spec/models/service_desk/custom_email_credential_spec.rb'
+ - 'spec/models/service_desk/custom_email_verification_spec.rb'
+ - 'spec/models/snippet_blob_spec.rb'
+ - 'spec/models/snippet_input_action_collection_spec.rb'
+ - 'spec/models/snippet_input_action_spec.rb'
+ - 'spec/models/snippet_spec.rb'
+ - 'spec/models/snippet_statistics_spec.rb'
+ - 'spec/models/state_note_spec.rb'
+ - 'spec/models/subscription_spec.rb'
+ - 'spec/models/suggestion_spec.rb'
+ - 'spec/models/system/broadcast_message_spec.rb'
+ - 'spec/models/terraform/state_spec.rb'
+ - 'spec/models/terraform/state_version_spec.rb'
+ - 'spec/models/timelog_spec.rb'
+ - 'spec/models/todo_spec.rb'
+ - 'spec/models/tree_spec.rb'
+ - 'spec/models/upload_spec.rb'
+ - 'spec/models/uploads/fog_spec.rb'
+ - 'spec/models/uploads/local_spec.rb'
+ - 'spec/models/user_custom_attribute_spec.rb'
+ - 'spec/models/user_interacted_project_spec.rb'
+ - 'spec/models/user_spec.rb'
+ - 'spec/models/user_status_spec.rb'
+ - 'spec/models/users/credit_card_validation_spec.rb'
+ - 'spec/models/users/merge_request_interaction_spec.rb'
+ - 'spec/models/users/namespace_commit_email_spec.rb'
+ - 'spec/models/web_ide_terminal_spec.rb'
+ - 'spec/models/wiki_page/meta_spec.rb'
+ - 'spec/models/wiki_page_spec.rb'
+ - 'spec/models/work_items/type_spec.rb'
+ - 'spec/models/x509_certificate_spec.rb'
+ - 'spec/models/zoom_meeting_spec.rb'
+ - 'spec/policies/concerns/policy_actor_spec.rb'
+ - 'spec/policies/group_deploy_keys_group_policy_spec.rb'
+ - 'spec/policies/project_policy_spec.rb'
+ - 'spec/presenters/blobs/unfold_presenter_spec.rb'
+ - 'spec/presenters/ci/build_presenter_spec.rb'
+ - 'spec/presenters/ci/pipeline_artifacts/code_coverage_presenter_spec.rb'
+ - 'spec/presenters/ci/pipeline_presenter_spec.rb'
+ - 'spec/presenters/ci/trigger_presenter_spec.rb'
+ - 'spec/presenters/clusterable_presenter_spec.rb'
+ - 'spec/presenters/commit_presenter_spec.rb'
+ - 'spec/presenters/commit_status_presenter_spec.rb'
+ - 'spec/presenters/deploy_key_presenter_spec.rb'
+ - 'spec/presenters/dev_ops_report/metric_presenter_spec.rb'
+ - 'spec/presenters/gitlab/blame_presenter_spec.rb'
+ - 'spec/presenters/issue_presenter_spec.rb'
+ - 'spec/presenters/key_presenter_spec.rb'
+ - 'spec/presenters/merge_request_presenter_spec.rb'
+ - 'spec/presenters/ml/candidate_details_presenter_spec.rb'
+ - 'spec/presenters/ml/candidates_csv_presenter_spec.rb'
+ - 'spec/presenters/packages/composer/packages_presenter_spec.rb'
+ - 'spec/presenters/packages/helm/index_presenter_spec.rb'
+ - 'spec/presenters/packages/nuget/package_metadata_presenter_spec.rb'
+ - 'spec/presenters/packages/nuget/packages_metadata_presenter_spec.rb'
+ - 'spec/presenters/projects/import_export/project_export_presenter_spec.rb'
+ - 'spec/presenters/projects/security/configuration_presenter_spec.rb'
+ - 'spec/presenters/sentry_error_presenter_spec.rb'
+ - 'spec/presenters/snippet_blob_presenter_spec.rb'
+ - 'spec/presenters/snippet_presenter_spec.rb'
+ - 'spec/presenters/terraform/modules_presenter_spec.rb'
+ - 'spec/requests/abuse_reports_controller_spec.rb'
+ - 'spec/requests/admin/abuse_reports_controller_spec.rb'
+ - 'spec/requests/admin/projects_controller_spec.rb'
+ - 'spec/requests/api/alert_management_alerts_spec.rb'
+ - 'spec/requests/api/api_spec.rb'
+ - 'spec/requests/api/ci/job_artifacts_spec.rb'
+ - 'spec/requests/api/ci/jobs_spec.rb'
+ - 'spec/requests/api/ci/pipelines_spec.rb'
+ - 'spec/requests/api/ci/resource_groups_spec.rb'
+ - 'spec/requests/api/ci/runner/jobs_artifacts_spec.rb'
+ - 'spec/requests/api/ci/runner/runners_delete_spec.rb'
+ - 'spec/requests/api/ci/runners_reset_registration_token_spec.rb'
+ - 'spec/requests/api/commit_statuses_spec.rb'
+ - 'spec/requests/api/commits_spec.rb'
+ - 'spec/requests/api/composer_packages_spec.rb'
+ - 'spec/requests/api/container_repositories_spec.rb'
+ - 'spec/requests/api/deploy_keys_spec.rb'
+ - 'spec/requests/api/deploy_tokens_spec.rb'
+ - 'spec/requests/api/deployments_spec.rb'
+ - 'spec/requests/api/feature_flags_spec.rb'
+ - 'spec/requests/api/freeze_periods_spec.rb'
+ - 'spec/requests/api/geo_spec.rb'
+ - 'spec/requests/api/graphql/container_repository/container_repository_details_spec.rb'
+ - 'spec/requests/api/graphql/environments/deployments_spec.rb'
+ - 'spec/requests/api/graphql/gitlab_schema_spec.rb'
+ - 'spec/requests/api/graphql/group/container_repositories_spec.rb'
+ - 'spec/requests/api/graphql/group/data_transfer_spec.rb'
+ - 'spec/requests/api/graphql/group/dependency_proxy_blobs_spec.rb'
+ - 'spec/requests/api/graphql/group/dependency_proxy_image_ttl_policy_spec.rb'
+ - 'spec/requests/api/graphql/group/dependency_proxy_manifests_spec.rb'
+ - 'spec/requests/api/graphql/group/timelogs_spec.rb'
+ - 'spec/requests/api/graphql/groups_query_spec.rb'
+ - 'spec/requests/api/graphql/mutations/achievements/award_spec.rb'
+ - 'spec/requests/api/graphql/mutations/achievements/create_spec.rb'
+ - 'spec/requests/api/graphql/mutations/achievements/delete_spec.rb'
+ - 'spec/requests/api/graphql/mutations/achievements/delete_user_achievement_spec.rb'
+ - 'spec/requests/api/graphql/mutations/achievements/revoke_spec.rb'
+ - 'spec/requests/api/graphql/mutations/achievements/update_spec.rb'
+ - 'spec/requests/api/graphql/mutations/achievements/update_user_achievement_priorities_spec.rb'
+ - 'spec/requests/api/graphql/mutations/admin/abuse_report_labels/create_spec.rb'
+ - 'spec/requests/api/graphql/mutations/boards/destroy_spec.rb'
+ - 'spec/requests/api/graphql/mutations/ci/pipeline_trigger/create_spec.rb'
+ - 'spec/requests/api/graphql/mutations/ci/pipeline_trigger/delete_spec.rb'
+ - 'spec/requests/api/graphql/mutations/ci/pipeline_trigger/update_spec.rb'
+ - 'spec/requests/api/graphql/mutations/ci/runners_registration_token/reset_spec.rb'
+ - 'spec/requests/api/graphql/mutations/container_registry/protection/rule/create_spec.rb'
+ - 'spec/requests/api/graphql/mutations/container_repository/destroy_tags_spec.rb'
+ - 'spec/requests/api/graphql/mutations/dependency_proxy/group_settings/update_spec.rb'
+ - 'spec/requests/api/graphql/mutations/dependency_proxy/image_ttl_group_policy/update_spec.rb'
+ - 'spec/requests/api/graphql/mutations/labels/create_spec.rb'
+ - 'spec/requests/api/graphql/mutations/packages/protection/rule/create_spec.rb'
+ - 'spec/requests/api/graphql/mutations/packages/protection/rule/delete_spec.rb'
+ - 'spec/requests/api/graphql/mutations/snippets/create_spec.rb'
+ - 'spec/requests/api/graphql/mutations/snippets/update_spec.rb'
+ - 'spec/requests/api/graphql/namespace/package_settings_spec.rb'
+ - 'spec/requests/api/graphql/namespace_query_spec.rb'
+ - 'spec/requests/api/graphql/packages/composer_spec.rb'
+ - 'spec/requests/api/graphql/packages/conan_spec.rb'
+ - 'spec/requests/api/graphql/packages/helm_spec.rb'
+ - 'spec/requests/api/graphql/packages/maven_spec.rb'
+ - 'spec/requests/api/graphql/packages/nuget_spec.rb'
+ - 'spec/requests/api/graphql/packages/package_spec.rb'
+ - 'spec/requests/api/graphql/packages/pypi_spec.rb'
+ - 'spec/requests/api/graphql/project/ci_access_authorized_agents_spec.rb'
+ - 'spec/requests/api/graphql/project/container_repositories_spec.rb'
+ - 'spec/requests/api/graphql/project/data_transfer_spec.rb'
+ - 'spec/requests/api/graphql/project/deployment_spec.rb'
+ - 'spec/requests/api/graphql/project/environments_spec.rb'
+ - 'spec/requests/api/graphql/project/packages_cleanup_policy_spec.rb'
+ - 'spec/requests/api/graphql/project/packages_protection_rules_spec.rb'
+ - 'spec/requests/api/graphql/project/user_access_authorized_agents_spec.rb'
+ - 'spec/requests/api/graphql_spec.rb'
+ - 'spec/requests/api/group_container_repositories_spec.rb'
+ - 'spec/requests/api/group_import_spec.rb'
+ - 'spec/requests/api/group_packages_spec.rb'
+ - 'spec/requests/api/groups_spec.rb'
+ - 'spec/requests/api/helpers_spec.rb'
+ - 'spec/requests/api/integrations/slack/events_spec.rb'
+ - 'spec/requests/api/integrations/slack/interactions_spec.rb'
+ - 'spec/requests/api/internal/base_spec.rb'
+ - 'spec/requests/api/internal/container_registry/migration_spec.rb'
+ - 'spec/requests/api/internal/workhorse_spec.rb'
+ - 'spec/requests/api/maven_packages_spec.rb'
+ - 'spec/requests/api/notes_spec.rb'
+ - 'spec/requests/api/npm_project_packages_spec.rb'
+ - 'spec/requests/api/project_container_repositories_spec.rb'
+ - 'spec/requests/api/project_import_spec.rb'
+ - 'spec/requests/api/project_job_token_scope_spec.rb'
+ - 'spec/requests/api/project_packages_spec.rb'
+ - 'spec/requests/api/pypi_packages_spec.rb'
+ - 'spec/requests/api/releases_spec.rb'
+ - 'spec/requests/api/rubygem_packages_spec.rb'
+ - 'spec/requests/api/snippets_spec.rb'
+ - 'spec/requests/api/terraform/modules/v1/packages_spec.rb'
+ - 'spec/requests/groups/settings/access_tokens_controller_spec.rb'
+ - 'spec/requests/health_controller_spec.rb'
+ - 'spec/requests/ide_controller_spec.rb'
+ - 'spec/requests/import/gitlab_projects_controller_spec.rb'
+ - 'spec/requests/lfs_http_spec.rb'
+ - 'spec/requests/projects/cluster_agents_controller_spec.rb'
+ - 'spec/requests/projects/google_cloud/databases_controller_spec.rb'
+ - 'spec/requests/projects/incidents_controller_spec.rb'
+ - 'spec/requests/projects/issue_links_controller_spec.rb'
+ - 'spec/requests/projects/merge_requests/diffs_spec.rb'
+ - 'spec/requests/projects/network_controller_spec.rb'
+ - 'spec/requests/projects/packages/package_files_controller_spec.rb'
+ - 'spec/requests/projects/redirect_controller_spec.rb'
+ - 'spec/requests/projects/releases_controller_spec.rb'
+ - 'spec/requests/projects/settings/access_tokens_controller_spec.rb'
+ - 'spec/requests/projects/settings/packages_and_registries_controller_spec.rb'
+ - 'spec/requests/terraform/services_controller_spec.rb'
+ - 'spec/requests/time_tracking/timelogs_controller_spec.rb'
+ - 'spec/requests/users/group_callouts_spec.rb'
+ - 'spec/requests/users/namespace_visits_controller_spec.rb'
+ - 'spec/requests/users/project_callouts_spec.rb'
+ - 'spec/requests/users_controller_spec.rb'
+ - 'spec/rubocop/check_graceful_task_spec.rb'
+ - 'spec/rubocop/cop/rake/require_spec.rb'
+ - 'spec/rubocop/todo_dir_spec.rb'
+ - 'spec/scripts/api/commit_merge_requests_spec.rb'
+ - 'spec/scripts/api/create_merge_request_discussion_spec.rb'
+ - 'spec/scripts/api/create_merge_request_note_spec.rb'
+ - 'spec/scripts/api/get_package_and_test_job_spec.rb'
+ - 'spec/scripts/failed_tests_spec.rb'
+ - 'spec/scripts/generate_failed_package_and_test_mr_message_spec.rb'
+ - 'spec/scripts/generate_message_to_run_e2e_pipeline_spec.rb'
+ - 'spec/scripts/generate_rspec_pipeline_spec.rb'
+ - 'spec/scripts/lib/glfm/parse_examples_spec.rb'
+ - 'spec/scripts/lib/glfm/update_example_snapshots_spec.rb'
+ - 'spec/scripts/lib/glfm/update_specification_spec.rb'
+ - 'spec/scripts/lib/glfm/verify_all_generated_files_are_up_to_date_spec.rb'
+ - 'spec/scripts/pipeline/average_reports_spec.rb'
+ - 'spec/scripts/pipeline_test_report_builder_spec.rb'
+ - 'spec/scripts/review_apps/automated_cleanup_spec.rb'
+ - 'spec/scripts/setup/find_jh_branch_spec.rb'
+ - 'spec/scripts/trigger-build_spec.rb'
+ - 'spec/serializers/accessibility_error_entity_spec.rb'
+ - 'spec/serializers/accessibility_reports_comparer_entity_spec.rb'
+ - 'spec/serializers/accessibility_reports_comparer_serializer_spec.rb'
+ - 'spec/serializers/activity_pub/activity_streams_serializer_spec.rb'
+ - 'spec/serializers/activity_pub/project_entity_spec.rb'
+ - 'spec/serializers/activity_pub/release_entity_spec.rb'
+ - 'spec/serializers/activity_pub/releases_actor_entity_spec.rb'
+ - 'spec/serializers/activity_pub/releases_actor_serializer_spec.rb'
+ - 'spec/serializers/activity_pub/releases_outbox_serializer_spec.rb'
+ - 'spec/serializers/activity_pub/user_entity_spec.rb'
+ - 'spec/serializers/admin/abuse_report_serializer_spec.rb'
+ - 'spec/serializers/analytics_build_entity_spec.rb'
+ - 'spec/serializers/analytics_build_serializer_spec.rb'
+ - 'spec/serializers/analytics_issue_entity_spec.rb'
+ - 'spec/serializers/analytics_issue_serializer_spec.rb'
+ - 'spec/serializers/analytics_merge_request_serializer_spec.rb'
+ - 'spec/serializers/analytics_summary_serializer_spec.rb'
+ - 'spec/serializers/base_discussion_entity_spec.rb'
+ - 'spec/serializers/blob_entity_spec.rb'
+ - 'spec/serializers/build_action_entity_spec.rb'
+ - 'spec/serializers/build_artifact_entity_spec.rb'
+ - 'spec/serializers/build_details_entity_spec.rb'
+ - 'spec/serializers/build_trace_entity_spec.rb'
+ - 'spec/serializers/ci/codequality_mr_diff_report_serializer_spec.rb'
+ - 'spec/serializers/ci/dag_job_entity_spec.rb'
+ - 'spec/serializers/ci/dag_job_group_entity_spec.rb'
+ - 'spec/serializers/ci/dag_pipeline_entity_spec.rb'
+ - 'spec/serializers/ci/dag_pipeline_serializer_spec.rb'
+ - 'spec/serializers/ci/dag_stage_entity_spec.rb'
+ - 'spec/serializers/ci/downloadable_artifact_entity_spec.rb'
+ - 'spec/serializers/ci/downloadable_artifact_serializer_spec.rb'
+ - 'spec/serializers/ci/group_variable_entity_spec.rb'
+ - 'spec/serializers/ci/job_annotation_entity_spec.rb'
+ - 'spec/serializers/ci/job_entity_spec.rb'
+ - 'spec/serializers/ci/job_serializer_spec.rb'
+ - 'spec/serializers/ci/pipeline_entity_spec.rb'
+ - 'spec/serializers/ci/variable_entity_spec.rb'
+ - 'spec/serializers/cluster_entity_spec.rb'
+ - 'spec/serializers/codequality_degradation_entity_spec.rb'
+ - 'spec/serializers/codequality_reports_comparer_entity_spec.rb'
+ - 'spec/serializers/codequality_reports_comparer_serializer_spec.rb'
+ - 'spec/serializers/commit_entity_spec.rb'
+ - 'spec/serializers/container_repositories_serializer_spec.rb'
+ - 'spec/serializers/container_repository_entity_spec.rb'
+ - 'spec/serializers/container_tag_entity_spec.rb'
+ - 'spec/serializers/context_commits_diff_entity_spec.rb'
+ - 'spec/serializers/deployment_cluster_entity_spec.rb'
+ - 'spec/serializers/deployment_entity_spec.rb'
+ - 'spec/serializers/detailed_status_entity_spec.rb'
+ - 'spec/serializers/diff_file_entity_spec.rb'
+ - 'spec/serializers/diff_file_metadata_entity_spec.rb'
+ - 'spec/serializers/diff_line_entity_spec.rb'
+ - 'spec/serializers/diff_line_serializer_spec.rb'
+ - 'spec/serializers/diff_viewer_entity_spec.rb'
+ - 'spec/serializers/diffs_entity_spec.rb'
+ - 'spec/serializers/diffs_metadata_entity_spec.rb'
+ - 'spec/serializers/discussion_diff_file_entity_spec.rb'
+ - 'spec/serializers/discussion_entity_spec.rb'
+ - 'spec/serializers/entity_request_spec.rb'
+ - 'spec/serializers/environment_entity_spec.rb'
+ - 'spec/serializers/environment_serializer_spec.rb'
+ - 'spec/serializers/environment_status_entity_spec.rb'
+ - 'spec/serializers/evidences/evidence_entity_spec.rb'
+ - 'spec/serializers/evidences/issue_entity_spec.rb'
+ - 'spec/serializers/evidences/milestone_entity_spec.rb'
+ - 'spec/serializers/evidences/project_entity_spec.rb'
+ - 'spec/serializers/evidences/release_entity_spec.rb'
+ - 'spec/serializers/feature_flag_entity_spec.rb'
+ - 'spec/serializers/feature_flag_summary_entity_spec.rb'
+ - 'spec/serializers/feature_flag_summary_serializer_spec.rb'
+ - 'spec/serializers/feature_flags_client_serializer_spec.rb'
+ - 'spec/serializers/group_issuable_autocomplete_entity_spec.rb'
+ - 'spec/serializers/import/bulk_import_entity_spec.rb'
+ - 'spec/serializers/import/github_org_entity_spec.rb'
+ - 'spec/serializers/import/github_org_serializer_spec.rb'
+ - 'spec/serializers/integrations/event_entity_spec.rb'
+ - 'spec/serializers/integrations/field_entity_spec.rb'
+ - 'spec/serializers/integrations/harbor_serializers/artifact_entity_spec.rb'
+ - 'spec/serializers/integrations/harbor_serializers/repository_entity_spec.rb'
+ - 'spec/serializers/integrations/harbor_serializers/tag_entity_spec.rb'
+ - 'spec/serializers/integrations/project_entity_spec.rb'
+ - 'spec/serializers/issuable_sidebar_extras_entity_spec.rb'
+ - 'spec/serializers/issue_board_entity_spec.rb'
+ - 'spec/serializers/issue_entity_spec.rb'
+ - 'spec/serializers/jira_connect/group_entity_spec.rb'
+ - 'spec/serializers/jira_connect/subscription_entity_spec.rb'
+ - 'spec/serializers/job_artifact_report_entity_spec.rb'
+ - 'spec/serializers/label_serializer_spec.rb'
+ - 'spec/serializers/lfs_file_lock_entity_spec.rb'
+ - 'spec/serializers/merge_request_basic_entity_spec.rb'
+ - 'spec/serializers/merge_request_current_user_entity_spec.rb'
+ - 'spec/serializers/merge_request_diff_entity_spec.rb'
+ - 'spec/serializers/merge_request_for_pipeline_entity_spec.rb'
+ - 'spec/serializers/merge_request_metrics_helper_spec.rb'
+ - 'spec/serializers/merge_request_poll_cached_widget_entity_spec.rb'
+ - 'spec/serializers/merge_request_poll_widget_entity_spec.rb'
+ - 'spec/serializers/merge_request_user_entity_spec.rb'
+ - 'spec/serializers/merge_request_widget_commit_entity_spec.rb'
+ - 'spec/serializers/merge_request_widget_entity_spec.rb'
+ - 'spec/serializers/merge_requests/pipeline_entity_spec.rb'
+ - 'spec/serializers/move_to_project_entity_spec.rb'
+ - 'spec/serializers/namespace_basic_entity_spec.rb'
+ - 'spec/serializers/paginated_diff_entity_spec.rb'
+ - 'spec/serializers/pipeline_details_entity_spec.rb'
+ - 'spec/serializers/pipeline_serializer_spec.rb'
+ - 'spec/serializers/profile/event_entity_spec.rb'
+ - 'spec/serializers/project_import_entity_spec.rb'
+ - 'spec/serializers/project_note_entity_spec.rb'
+ - 'spec/serializers/project_serializer_spec.rb'
+ - 'spec/serializers/release_serializer_spec.rb'
+ - 'spec/serializers/remote_mirror_entity_spec.rb'
+ - 'spec/serializers/request_aware_entity_spec.rb'
+ - 'spec/serializers/review_app_setup_entity_spec.rb'
+ - 'spec/serializers/runner_entity_spec.rb'
+ - 'spec/serializers/serverless/domain_entity_spec.rb'
+ - 'spec/serializers/stage_entity_spec.rb'
+ - 'spec/serializers/stage_serializer_spec.rb'
+ - 'spec/serializers/suggestion_entity_spec.rb'
+ - 'spec/serializers/test_case_entity_spec.rb'
+ - 'spec/serializers/test_reports_comparer_entity_spec.rb'
+ - 'spec/serializers/test_reports_comparer_serializer_spec.rb'
+ - 'spec/serializers/test_suite_comparer_entity_spec.rb'
+ - 'spec/serializers/test_suite_entity_spec.rb'
+ - 'spec/serializers/trigger_variable_entity_spec.rb'
+ - 'spec/serializers/user_entity_spec.rb'
+ - 'spec/serializers/web_ide_terminal_serializer_spec.rb'
+ - 'spec/services/admin/abuse_reports/moderate_user_service_spec.rb'
+ - 'spec/services/admin/abuse_reports/update_service_spec.rb'
+ - 'spec/services/admin/set_feature_flag_service_spec.rb'
+ - 'spec/services/application_settings/update_service_spec.rb'
+ - 'spec/services/applications/create_service_spec.rb'
+ - 'spec/services/auto_merge/base_service_spec.rb'
+ - 'spec/services/auto_merge_service_spec.rb'
+ - 'spec/services/award_emojis/base_service_spec.rb'
+ - 'spec/services/branches/create_service_spec.rb'
+ - 'spec/services/bulk_imports/archive_extraction_service_spec.rb'
+ - 'spec/services/bulk_imports/create_service_spec.rb'
+ - 'spec/services/bulk_imports/export_service_spec.rb'
+ - 'spec/services/bulk_imports/file_decompression_service_spec.rb'
+ - 'spec/services/bulk_imports/file_download_service_spec.rb'
+ - 'spec/services/bulk_imports/get_importable_data_service_spec.rb'
+ - 'spec/services/bulk_imports/lfs_objects_export_service_spec.rb'
+ - 'spec/services/bulk_imports/process_service_spec.rb'
+ - 'spec/services/bulk_imports/relation_batch_export_service_spec.rb'
+ - 'spec/services/bulk_imports/relation_export_service_spec.rb'
+ - 'spec/services/bulk_imports/tree_export_service_spec.rb'
+ - 'spec/services/bulk_push_event_payload_service_spec.rb'
+ - 'spec/services/captcha/captcha_verification_service_spec.rb'
+ - 'spec/services/chat_names/authorize_user_service_spec.rb'
+ - 'spec/services/chat_names/find_user_service_spec.rb'
+ - 'spec/services/ci/archive_trace_service_spec.rb'
+ - 'spec/services/ci/build_report_result_service_spec.rb'
+ - 'spec/services/ci/compare_accessibility_reports_service_spec.rb'
+ - 'spec/services/ci/compare_codequality_reports_service_spec.rb'
+ - 'spec/services/ci/create_downstream_pipeline_service_spec.rb'
+ - 'spec/services/ci/create_pipeline_service/custom_config_content_spec.rb'
+ - 'spec/services/ci/create_pipeline_service/dry_run_spec.rb'
+ - 'spec/services/ci/create_pipeline_service/environment_spec.rb'
+ - 'spec/services/ci/create_pipeline_service/parameter_content_spec.rb'
+ - 'spec/services/ci/create_web_ide_terminal_service_spec.rb'
+ - 'spec/services/ci/deployments/destroy_service_spec.rb'
+ - 'spec/services/ci/destroy_pipeline_service_spec.rb'
+ - 'spec/services/ci/destroy_secure_file_service_spec.rb'
+ - 'spec/services/ci/drop_pipeline_service_spec.rb'
+ - 'spec/services/ci/expire_pipeline_cache_service_spec.rb'
+ - 'spec/services/ci/find_exposed_artifacts_service_spec.rb'
+ - 'spec/services/ci/generate_codequality_mr_diff_report_service_spec.rb'
+ - 'spec/services/ci/generate_coverage_reports_service_spec.rb'
+ - 'spec/services/ci/generate_terraform_reports_service_spec.rb'
+ - 'spec/services/ci/job_artifacts/delete_project_artifacts_service_spec.rb'
+ - 'spec/services/ci/job_artifacts/destroy_all_expired_service_spec.rb'
+ - 'spec/services/ci/job_artifacts/destroy_batch_service_spec.rb'
+ - 'spec/services/ci/job_artifacts/update_unknown_locked_status_service_spec.rb'
+ - 'spec/services/ci/parse_annotations_artifact_service_spec.rb'
+ - 'spec/services/ci/parse_dotenv_artifact_service_spec.rb'
+ - 'spec/services/ci/pipeline_artifacts/coverage_report_service_spec.rb'
+ - 'spec/services/ci/pipeline_artifacts/create_code_quality_mr_diff_report_service_spec.rb'
+ - 'spec/services/ci/pipeline_artifacts/destroy_all_expired_service_spec.rb'
+ - 'spec/services/ci/pipeline_bridge_status_service_spec.rb'
+ - 'spec/services/ci/pipeline_creation/cancel_redundant_pipelines_service_spec.rb'
+ - 'spec/services/ci/pipeline_schedule_service_spec.rb'
+ - 'spec/services/ci/play_build_service_spec.rb'
+ - 'spec/services/ci/prepare_build_service_spec.rb'
+ - 'spec/services/ci/process_build_service_spec.rb'
+ - 'spec/services/ci/process_pipeline_service_spec.rb'
+ - 'spec/services/ci/prometheus_metrics/observe_histograms_service_spec.rb'
+ - 'spec/services/ci/register_job_service_spec.rb'
+ - 'spec/services/ci/resource_groups/assign_resource_from_resource_group_service_spec.rb'
+ - 'spec/services/ci/retry_job_service_spec.rb'
+ - 'spec/services/ci/run_scheduled_build_service_spec.rb'
+ - 'spec/services/ci/track_failed_build_service_spec.rb'
+ - 'spec/services/ci/unlock_artifacts_service_spec.rb'
+ - 'spec/services/ci/update_build_queue_service_spec.rb'
+ - 'spec/services/ci/update_build_state_service_spec.rb'
+ - 'spec/services/ci/update_instance_variables_service_spec.rb'
+ - 'spec/services/clusters/agent_tokens/create_service_spec.rb'
+ - 'spec/services/clusters/agent_tokens/revoke_service_spec.rb'
+ - 'spec/services/clusters/agent_tokens/track_usage_service_spec.rb'
+ - 'spec/services/clusters/agents/authorizations/ci_access/refresh_service_spec.rb'
+ - 'spec/services/clusters/agents/authorizations/user_access/refresh_service_spec.rb'
+ - 'spec/services/clusters/agents/create_activity_event_service_spec.rb'
+ - 'spec/services/clusters/agents/delete_expired_events_service_spec.rb'
+ - 'spec/services/clusters/build_kubernetes_namespace_service_spec.rb'
+ - 'spec/services/clusters/cleanup/project_namespace_service_spec.rb'
+ - 'spec/services/clusters/cleanup/service_account_service_spec.rb'
+ - 'spec/services/clusters/create_service_spec.rb'
+ - 'spec/services/clusters/destroy_service_spec.rb'
+ - 'spec/services/clusters/kubernetes/create_or_update_namespace_service_spec.rb'
+ - 'spec/services/clusters/kubernetes/create_or_update_service_account_service_spec.rb'
+ - 'spec/services/clusters/kubernetes/fetch_kubernetes_token_service_spec.rb'
+ - 'spec/services/compare_service_spec.rb'
+ - 'spec/services/concerns/exclusive_lease_guard_spec.rb'
+ - 'spec/services/concerns/rate_limited_service_spec.rb'
+ - 'spec/services/container_expiration_policies/cleanup_service_spec.rb'
+ - 'spec/services/container_registry/protection/create_rule_service_spec.rb'
+ - 'spec/services/database/consistency_check_service_spec.rb'
+ - 'spec/services/dependency_proxy/auth_token_service_spec.rb'
+ - 'spec/services/dependency_proxy/head_manifest_service_spec.rb'
+ - 'spec/services/dependency_proxy/request_token_service_spec.rb'
+ - 'spec/services/deploy_keys/create_service_spec.rb'
+ - 'spec/services/deployments/archive_in_project_service_spec.rb'
+ - 'spec/services/deployments/update_environment_service_spec.rb'
+ - 'spec/services/design_management/copy_design_collection/copy_service_spec.rb'
+ - 'spec/services/design_management/copy_design_collection/queue_service_spec.rb'
+ - 'spec/services/design_management/design_user_notes_count_service_spec.rb'
+ - 'spec/services/design_management/move_designs_service_spec.rb'
+ - 'spec/services/discussions/capture_diff_note_position_service_spec.rb'
+ - 'spec/services/discussions/update_diff_position_service_spec.rb'
+ - 'spec/services/environments/auto_recover_service_spec.rb'
+ - 'spec/services/environments/auto_stop_service_spec.rb'
+ - 'spec/services/environments/canary_ingress/update_service_spec.rb'
+ - 'spec/services/environments/create_service_spec.rb'
+ - 'spec/services/environments/destroy_service_spec.rb'
+ - 'spec/services/environments/reset_auto_stop_service_spec.rb'
+ - 'spec/services/environments/schedule_to_delete_review_apps_service_spec.rb'
+ - 'spec/services/environments/stop_service_spec.rb'
+ - 'spec/services/environments/stop_stale_service_spec.rb'
+ - 'spec/services/environments/update_service_spec.rb'
+ - 'spec/services/error_tracking/list_issues_service_spec.rb'
+ - 'spec/services/error_tracking/list_projects_service_spec.rb'
+ - 'spec/services/event_create_service_spec.rb'
+ - 'spec/services/events/destroy_service_spec.rb'
+ - 'spec/services/export_csv/base_service_spec.rb'
+ - 'spec/services/feature_flags/create_service_spec.rb'
+ - 'spec/services/feature_flags/destroy_service_spec.rb'
+ - 'spec/services/feature_flags/update_service_spec.rb'
+ - 'spec/services/files/create_service_spec.rb'
+ - 'spec/services/files/delete_service_spec.rb'
+ - 'spec/services/files/multi_service_spec.rb'
+ - 'spec/services/files/update_service_spec.rb'
+ - 'spec/services/git/base_hooks_service_spec.rb'
+ - 'spec/services/git/branch_hooks_service_spec.rb'
+ - 'spec/services/git/branch_push_service_spec.rb'
+ - 'spec/services/git/process_ref_changes_service_spec.rb'
+ - 'spec/services/git/tag_push_service_spec.rb'
+ - 'spec/services/git/wiki_push_service_spec.rb'
+ - 'spec/services/google_cloud/fetch_google_ip_list_service_spec.rb'
+ - 'spec/services/google_cloud/setup_cloudsql_instance_service_spec.rb'
+ - 'spec/services/gpg_keys/create_service_spec.rb'
+ - 'spec/services/gpg_keys/destroy_service_spec.rb'
+ - 'spec/services/groups/autocomplete_service_spec.rb'
+ - 'spec/services/groups/create_service_spec.rb'
+ - 'spec/services/groups/deploy_tokens/revoke_service_spec.rb'
+ - 'spec/services/groups/group_links/create_service_spec.rb'
+ - 'spec/services/groups/group_links/destroy_service_spec.rb'
+ - 'spec/services/groups/group_links/update_service_spec.rb'
+ - 'spec/services/groups/merge_requests_count_service_spec.rb'
+ - 'spec/services/groups/open_issues_count_service_spec.rb'
+ - 'spec/services/groups/transfer_service_spec.rb'
+ - 'spec/services/groups/update_service_spec.rb'
+ - 'spec/services/groups/update_shared_runners_service_spec.rb'
+ - 'spec/services/ide/schemas_config_service_spec.rb'
+ - 'spec/services/import/bitbucket_server_service_spec.rb'
+ - 'spec/services/import/fogbugz_service_spec.rb'
+ - 'spec/services/import/github_service_spec.rb'
+ - 'spec/services/import/gitlab_projects/create_project_service_spec.rb'
+ - 'spec/services/import/gitlab_projects/file_acquisition_strategies/remote_file_s3_spec.rb'
+ - 'spec/services/import/gitlab_projects/file_acquisition_strategies/remote_file_spec.rb'
+ - 'spec/services/import/prepare_service_spec.rb'
+ - 'spec/services/import/validate_remote_git_endpoint_service_spec.rb'
+ - 'spec/services/integrations/slack_interactions/incident_management/incident_modal_opened_service_spec.rb'
+ - 'spec/services/integrations/test/project_service_spec.rb'
+ - 'spec/services/issuable/common_system_notes_service_spec.rb'
+ - 'spec/services/issuable/import_csv/base_service_spec.rb'
+ - 'spec/services/issue_links/list_service_spec.rb'
+ - 'spec/services/issues/build_service_spec.rb'
+ - 'spec/services/issues/clone_service_spec.rb'
+ - 'spec/services/issues/create_service_spec.rb'
+ - 'spec/services/issues/duplicate_service_spec.rb'
+ - 'spec/services/issues/export_csv_service_spec.rb'
+ - 'spec/services/issues/import_csv_service_spec.rb'
+ - 'spec/services/issues/move_service_spec.rb'
+ - 'spec/services/issues/prepare_import_csv_service_spec.rb'
+ - 'spec/services/issues/relative_position_rebalancing_service_spec.rb'
+ - 'spec/services/issues/reorder_service_spec.rb'
+ - 'spec/services/issues/update_service_spec.rb'
+ - 'spec/services/jira/requests/projects/list_service_spec.rb'
+ - 'spec/services/jira_connect/sync_service_spec.rb'
+ - 'spec/services/jira_connect_installations/destroy_service_spec.rb'
+ - 'spec/services/jira_connect_subscriptions/create_service_spec.rb'
+ - 'spec/services/jira_import/start_import_service_spec.rb'
+ - 'spec/services/jira_import/users_importer_spec.rb'
+ - 'spec/services/keys/create_service_spec.rb'
+ - 'spec/services/keys/destroy_service_spec.rb'
+ - 'spec/services/keys/expiry_notification_service_spec.rb'
+ - 'spec/services/lfs/file_transformer_spec.rb'
+ - 'spec/services/lfs/lock_file_service_spec.rb'
+ - 'spec/services/lfs/locks_finder_service_spec.rb'
+ - 'spec/services/lfs/unlock_file_service_spec.rb'
+ - 'spec/services/markdown_content_rewriter_service_spec.rb'
+ - 'spec/services/mattermost/create_team_service_spec.rb'
+ - 'spec/services/members/invitation_reminder_email_service_spec.rb'
+ - 'spec/services/members/update_service_spec.rb'
+ - 'spec/services/merge_requests/add_context_service_spec.rb'
+ - 'spec/services/merge_requests/base_service_spec.rb'
+ - 'spec/services/merge_requests/build_service_spec.rb'
+ - 'spec/services/merge_requests/conflicts/resolve_service_spec.rb'
+ - 'spec/services/merge_requests/delete_non_latest_diffs_service_spec.rb'
+ - 'spec/services/merge_requests/export_csv_service_spec.rb'
+ - 'spec/services/merge_requests/merge_orchestration_service_spec.rb'
+ - 'spec/services/merge_requests/mergeability_check_batch_service_spec.rb'
+ - 'spec/services/merge_requests/mergeability_check_service_spec.rb'
+ - 'spec/services/merge_requests/post_merge_service_spec.rb'
+ - 'spec/services/merge_requests/rebase_service_spec.rb'
+ - 'spec/services/merge_requests/refresh_service_spec.rb'
+ - 'spec/services/merge_requests/reload_diffs_service_spec.rb'
+ - 'spec/services/merge_requests/reload_merge_head_diff_service_spec.rb'
+ - 'spec/services/merge_requests/resolved_discussion_notification_service_spec.rb'
+ - 'spec/services/merge_requests/retarget_chain_service_spec.rb'
+ - 'spec/services/merge_requests/update_service_spec.rb'
+ - 'spec/services/milestones/closed_issues_count_service_spec.rb'
+ - 'spec/services/milestones/create_service_spec.rb'
+ - 'spec/services/milestones/issues_count_service_spec.rb'
+ - 'spec/services/milestones/merge_requests_count_service_spec.rb'
+ - 'spec/services/ml/experiment_tracking/candidate_repository_spec.rb'
+ - 'spec/services/ml/experiment_tracking/experiment_repository_spec.rb'
+ - 'spec/services/ml/experiment_tracking/handle_candidate_gitlab_metadata_service_spec.rb'
+ - 'spec/services/note_summary_spec.rb'
+ - 'spec/services/notes/create_service_spec.rb'
+ - 'spec/services/notes/post_process_service_spec.rb'
+ - 'spec/services/notification_recipients/builder/default_spec.rb'
+ - 'spec/services/notification_recipients/builder/new_note_spec.rb'
+ - 'spec/services/notification_service_spec.rb'
+ - 'spec/services/packages/composer/composer_json_service_spec.rb'
+ - 'spec/services/packages/composer/create_package_service_spec.rb'
+ - 'spec/services/packages/conan/create_package_file_service_spec.rb'
+ - 'spec/services/packages/conan/search_service_spec.rb'
+ - 'spec/services/packages/create_dependency_service_spec.rb'
+ - 'spec/services/packages/create_package_file_service_spec.rb'
+ - 'spec/services/packages/create_temporary_package_service_spec.rb'
+ - 'spec/services/packages/debian/extract_changes_metadata_service_spec.rb'
+ - 'spec/services/packages/debian/extract_deb_metadata_service_spec.rb'
+ - 'spec/services/packages/debian/extract_metadata_service_spec.rb'
+ - 'spec/services/packages/debian/parse_debian822_service_spec.rb'
+ - 'spec/services/packages/debian/process_package_file_service_spec.rb'
+ - 'spec/services/packages/go/create_package_service_spec.rb'
+ - 'spec/services/packages/helm/extract_file_metadata_service_spec.rb'
+ - 'spec/services/packages/mark_package_files_for_destruction_service_spec.rb'
+ - 'spec/services/packages/mark_packages_for_destruction_service_spec.rb'
+ - 'spec/services/packages/maven/find_or_create_package_service_spec.rb'
+ - 'spec/services/packages/maven/metadata/append_package_file_service_spec.rb'
+ - 'spec/services/packages/maven/metadata/create_plugins_xml_service_spec.rb'
+ - 'spec/services/packages/maven/metadata/create_versions_xml_service_spec.rb'
+ - 'spec/services/packages/ml_model/find_or_create_package_service_spec.rb'
+ - 'spec/services/packages/npm/create_metadata_cache_service_spec.rb'
+ - 'spec/services/packages/npm/create_package_service_spec.rb'
+ - 'spec/services/packages/npm/create_tag_service_spec.rb'
+ - 'spec/services/packages/npm/generate_metadata_service_spec.rb'
+ - 'spec/services/packages/nuget/create_dependency_service_spec.rb'
+ - 'spec/services/packages/nuget/extract_metadata_content_service_spec.rb'
+ - 'spec/services/packages/nuget/extract_metadata_file_service_spec.rb'
+ - 'spec/services/packages/nuget/metadata_extraction_service_spec.rb'
+ - 'spec/services/packages/nuget/odata_package_entry_service_spec.rb'
+ - 'spec/services/packages/nuget/process_package_file_service_spec.rb'
+ - 'spec/services/packages/nuget/symbols/create_symbol_files_service_spec.rb'
+ - 'spec/services/packages/nuget/symbols/extract_signature_and_checksum_service_spec.rb'
+ - 'spec/services/packages/nuget/sync_metadatum_service_spec.rb'
+ - 'spec/services/packages/nuget/update_package_from_metadata_service_spec.rb'
+ - 'spec/services/packages/protection/create_rule_service_spec.rb'
+ - 'spec/services/packages/protection/delete_rule_service_spec.rb'
+ - 'spec/services/packages/pypi/create_package_service_spec.rb'
+ - 'spec/services/packages/remove_tag_service_spec.rb'
+ - 'spec/services/packages/rpm/parse_package_service_spec.rb'
+ - 'spec/services/packages/rpm/repository_metadata/build_filelist_xml_service_spec.rb'
+ - 'spec/services/packages/rpm/repository_metadata/build_other_xml_service_spec.rb'
+ - 'spec/services/packages/rpm/repository_metadata/build_primary_xml_service_spec.rb'
+ - 'spec/services/packages/rpm/repository_metadata/build_repomd_xml_service_spec.rb'
+ - 'spec/services/packages/rubygems/create_dependencies_service_spec.rb'
+ - 'spec/services/packages/rubygems/create_gemspec_service_spec.rb'
+ - 'spec/services/packages/rubygems/dependency_resolver_service_spec.rb'
+ - 'spec/services/packages/rubygems/metadata_extraction_service_spec.rb'
+ - 'spec/services/packages/rubygems/process_gem_service_spec.rb'
+ - 'spec/services/packages/terraform_module/create_package_service_spec.rb'
+ - 'spec/services/packages/update_tags_service_spec.rb'
+ - 'spec/services/personal_access_tokens/create_service_spec.rb'
+ - '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'
+ - 'spec/services/projects/auto_devops/disable_service_spec.rb'
+ - 'spec/services/projects/autocomplete_service_spec.rb'
+ - 'spec/services/projects/batch_open_issues_count_service_spec.rb'
+ - 'spec/services/projects/batch_open_merge_requests_count_service_spec.rb'
+ - 'spec/services/projects/branches_by_mode_service_spec.rb'
+ - 'spec/services/projects/container_repository/delete_tags_service_spec.rb'
+ - 'spec/services/projects/container_repository/destroy_service_spec.rb'
+ - 'spec/services/projects/container_repository/gitlab/delete_tags_service_spec.rb'
+ - 'spec/services/projects/container_repository/third_party/cleanup_tags_service_spec.rb'
+ - 'spec/services/projects/container_repository/third_party/delete_tags_service_spec.rb'
+ - 'spec/services/projects/create_from_template_service_spec.rb'
+ - 'spec/services/projects/detect_repository_languages_service_spec.rb'
+ - 'spec/services/projects/fetch_statistics_increment_service_spec.rb'
+ - 'spec/services/projects/fork_service_spec.rb'
+ - 'spec/services/projects/forks_count_service_spec.rb'
+ - 'spec/services/projects/group_links/create_service_spec.rb'
+ - 'spec/services/projects/group_links/destroy_service_spec.rb'
+ - 'spec/services/projects/group_links/update_service_spec.rb'
+ - 'spec/services/projects/hashed_storage/base_attachment_service_spec.rb'
+ - 'spec/services/projects/import_service_spec.rb'
+ - 'spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb'
+ - 'spec/services/projects/lfs_pointers/lfs_download_service_spec.rb'
+ - 'spec/services/projects/lfs_pointers/lfs_import_service_spec.rb'
+ - 'spec/services/projects/lfs_pointers/lfs_link_service_spec.rb'
+ - 'spec/services/projects/lfs_pointers/lfs_object_download_list_service_spec.rb'
+ - 'spec/services/projects/move_access_service_spec.rb'
+ - 'spec/services/projects/move_deploy_keys_projects_service_spec.rb'
+ - 'spec/services/projects/move_forks_service_spec.rb'
+ - 'spec/services/projects/move_lfs_objects_projects_service_spec.rb'
+ - 'spec/services/projects/move_notification_settings_service_spec.rb'
+ - 'spec/services/projects/move_project_authorizations_service_spec.rb'
+ - 'spec/services/projects/move_project_group_links_service_spec.rb'
+ - 'spec/services/projects/move_project_members_service_spec.rb'
+ - 'spec/services/projects/move_users_star_projects_service_spec.rb'
+ - 'spec/services/projects/open_issues_count_service_spec.rb'
+ - 'spec/services/projects/open_merge_requests_count_service_spec.rb'
+ - 'spec/services/projects/operations/update_service_spec.rb'
+ - 'spec/services/projects/overwrite_project_service_spec.rb'
+ - 'spec/services/projects/prometheus/alerts/notify_service_spec.rb'
+ - 'spec/services/projects/prometheus/metrics/destroy_service_spec.rb'
+ - 'spec/services/projects/readme_renderer_service_spec.rb'
+ - 'spec/services/projects/transfer_service_spec.rb'
+ - 'spec/services/projects/unlink_fork_service_spec.rb'
+ - 'spec/services/projects/update_pages_service_spec.rb'
+ - 'spec/services/projects/update_repository_storage_service_spec.rb'
+ - 'spec/services/releases/create_service_spec.rb'
+ - 'spec/services/releases/destroy_service_spec.rb'
+ - 'spec/services/repositories/destroy_service_spec.rb'
+ - 'spec/services/repositories/replicate_service_spec.rb'
+ - 'spec/services/reset_project_cache_service_spec.rb'
+ - 'spec/services/resource_access_tokens/create_service_spec.rb'
+ - 'spec/services/search_service_spec.rb'
+ - 'spec/services/service_ping/submit_service_ping_service_spec.rb'
+ - 'spec/services/snippets/bulk_destroy_service_spec.rb'
+ - 'spec/services/snippets/count_service_spec.rb'
+ - 'spec/services/snippets/create_service_spec.rb'
+ - 'spec/services/snippets/destroy_service_spec.rb'
+ - 'spec/services/snippets/repository_validation_service_spec.rb'
+ - 'spec/services/snippets/update_repository_storage_service_spec.rb'
+ - 'spec/services/snippets/update_statistics_service_spec.rb'
+ - 'spec/services/spam/akismet_mark_as_spam_service_spec.rb'
+ - 'spec/services/spam/akismet_service_spec.rb'
+ - 'spec/services/spam/ham_service_spec.rb'
+ - 'spec/services/spam/spam_action_service_spec.rb'
+ - 'spec/services/submodules/update_service_spec.rb'
+ - 'spec/services/suggestions/create_service_spec.rb'
+ - 'spec/services/suggestions/outdate_service_spec.rb'
+ - 'spec/services/system_hooks_service_spec.rb'
+ - 'spec/services/system_note_service_spec.rb'
+ - 'spec/services/system_notes/alert_management_service_spec.rb'
+ - 'spec/services/system_notes/commit_service_spec.rb'
+ - 'spec/services/system_notes/design_management_service_spec.rb'
+ - 'spec/services/system_notes/incidents_service_spec.rb'
+ - 'spec/services/system_notes/issuables_service_spec.rb'
+ - 'spec/services/system_notes/merge_requests_service_spec.rb'
+ - 'spec/services/system_notes/time_tracking_service_spec.rb'
+ - 'spec/services/system_notes/zoom_service_spec.rb'
+ - 'spec/services/tags/destroy_service_spec.rb'
+ - 'spec/services/terraform/remote_state_handler_spec.rb'
+ - 'spec/services/terraform/states/destroy_service_spec.rb'
+ - 'spec/services/terraform/states/trigger_destroy_service_spec.rb'
+ - 'spec/services/timelogs/delete_service_spec.rb'
+ - 'spec/services/todos/destroy/confidential_issue_service_spec.rb'
+ - 'spec/services/todos/destroy/design_service_spec.rb'
+ - 'spec/services/todos/destroy/destroyed_issuable_service_spec.rb'
+ - 'spec/services/todos/destroy/entity_leave_service_spec.rb'
+ - 'spec/services/todos/destroy/group_private_service_spec.rb'
+ - 'spec/services/todos/destroy/project_private_service_spec.rb'
+ - 'spec/services/todos/destroy/unauthorized_features_service_spec.rb'
+ - 'spec/services/topics/merge_service_spec.rb'
+ - 'spec/services/two_factor/destroy_service_spec.rb'
+ - 'spec/services/update_container_registry_info_service_spec.rb'
+ - 'spec/services/upload_service_spec.rb'
+ - 'spec/services/uploads/destroy_service_spec.rb'
+ - 'spec/services/user_agent_detail_service_spec.rb'
+ - 'spec/services/users/activity_service_spec.rb'
+ - 'spec/services/users/approve_service_spec.rb'
+ - 'spec/services/users/assigned_issues_count_service_spec.rb'
+ - 'spec/services/users/in_product_marketing_email_records_spec.rb'
+ - 'spec/services/users/keys_count_service_spec.rb'
+ - 'spec/services/users/reject_service_spec.rb'
+ - 'spec/services/users/saved_replies/create_service_spec.rb'
+ - 'spec/services/users/saved_replies/destroy_service_spec.rb'
+ - 'spec/services/users/saved_replies/update_service_spec.rb'
+ - 'spec/services/users/update_canonical_email_service_spec.rb'
+ - 'spec/services/users/update_service_spec.rb'
+ - 'spec/services/vs_code/settings/create_or_update_service_spec.rb'
+ - 'spec/services/vs_code/settings/delete_service_spec.rb'
+ - 'spec/services/web_hooks/destroy_service_spec.rb'
+ - 'spec/services/webauthn/destroy_service_spec.rb'
+ - 'spec/services/wiki_pages/base_service_spec.rb'
+ - 'spec/services/wiki_pages/event_create_service_spec.rb'
+ - 'spec/services/work_items/callbacks/award_emoji_spec.rb'
+ - 'spec/services/work_items/export_csv_service_spec.rb'
+ - 'spec/services/work_items/import_csv_service_spec.rb'
+ - 'spec/services/work_items/parent_links/base_service_spec.rb'
+ - 'spec/services/work_items/parent_links/create_service_spec.rb'
+ - 'spec/services/work_items/parent_links/destroy_service_spec.rb'
+ - 'spec/services/work_items/prepare_import_csv_service_spec.rb'
+ - 'spec/services/work_items/update_service_spec.rb'
+ - 'spec/services/work_items/widgets/assignees_service/update_service_spec.rb'
+ - 'spec/services/work_items/widgets/current_user_todos_service/update_service_spec.rb'
+ - 'spec/services/work_items/widgets/hierarchy_service/update_service_spec.rb'
+ - 'spec/spam/concerns/has_spam_action_response_fields_spec.rb'
+ - 'spec/support/shared_contexts/lib/gitlab/sidekiq_logging/structured_logger_shared_context.rb'
+ - 'spec/support/shared_contexts/lib/gitlab/sidekiq_middleware/server_metrics_shared_context.rb'
+ - 'spec/support/shared_examples/uploaders/object_storage_shared_examples.rb'
+ - 'spec/support_specs/database/without_check_constraint_spec.rb'
+ - 'spec/support_specs/helpers/graphql_helpers_spec.rb'
+ - 'spec/support_specs/helpers/html_escaped_helpers_spec.rb'
+ - 'spec/support_specs/helpers/stub_feature_flags_spec.rb'
+ - 'spec/tasks/gitlab/artifacts/migrate_rake_spec.rb'
+ - 'spec/tasks/gitlab/audit_event_types/check_docs_task_spec.rb'
+ - 'spec/tasks/gitlab/audit_event_types/compile_docs_task_spec.rb'
+ - 'spec/tasks/gitlab/check_rake_spec.rb'
+ - 'spec/tasks/gitlab/ci_secure_files/migrate_rake_spec.rb'
+ - 'spec/tasks/gitlab/cleanup_rake_spec.rb'
+ - 'spec/tasks/gitlab/container_registry_rake_spec.rb'
+ - 'spec/tasks/gitlab/db/decomposition/connection_status_rake_spec.rb'
+ - 'spec/tasks/gitlab/db_rake_spec.rb'
+ - 'spec/tasks/gitlab/gitaly_rake_spec.rb'
+ - 'spec/tasks/gitlab/lfs/migrate_rake_spec.rb'
+ - 'spec/tasks/gitlab/pages_rake_spec.rb'
+ - 'spec/tasks/gitlab/seed/group_seed_rake_spec.rb'
+ - 'spec/tasks/gitlab/snippets_rake_spec.rb'
+ - 'spec/tasks/gitlab/terraform/migrate_rake_spec.rb'
+ - 'spec/tasks/gitlab/x509/update_rake_spec.rb'
+ - 'spec/tooling/danger/analytics_instrumentation_spec.rb'
+ - 'spec/tooling/danger/stable_branch_spec.rb'
+ - 'spec/tooling/docs/deprecation_handling_spec.rb'
+ - 'spec/tooling/graphql/docs/renderer_spec.rb'
+ - 'spec/tooling/lib/tooling/crystalball/coverage_lines_strategy_spec.rb'
+ - 'spec/tooling/lib/tooling/find_changes_spec.rb'
+ - 'spec/tooling/lib/tooling/find_codeowners_spec.rb'
+ - 'spec/tooling/lib/tooling/find_files_using_feature_flags_spec.rb'
+ - 'spec/tooling/lib/tooling/find_tests_spec.rb'
+ - 'spec/tooling/lib/tooling/gettext_extractor_spec.rb'
+ - 'spec/tooling/lib/tooling/helm3_client_spec.rb'
+ - 'spec/tooling/lib/tooling/helpers/file_handler_spec.rb'
+ - 'spec/tooling/lib/tooling/helpers/predictive_tests_helper_spec.rb'
+ - 'spec/tooling/lib/tooling/job_metrics_spec.rb'
+ - 'spec/tooling/lib/tooling/kubernetes_client_spec.rb'
+ - 'spec/tooling/lib/tooling/mappings/graphql_base_type_mappings_spec.rb'
+ - 'spec/tooling/lib/tooling/mappings/js_to_system_specs_mappings_spec.rb'
+ - 'spec/tooling/lib/tooling/mappings/partial_to_views_mappings_spec.rb'
+ - 'spec/tooling/lib/tooling/mappings/view_to_js_mappings_spec.rb'
+ - 'spec/tooling/lib/tooling/mappings/view_to_system_specs_mappings_spec.rb'
+ - 'spec/tooling/lib/tooling/predictive_tests_spec.rb'
+ - 'spec/tooling/lib/tooling/test_map_generator_spec.rb'
+ - 'spec/tooling/lib/tooling/test_map_packer_spec.rb'
+ - 'spec/tooling/quality/test_level_spec.rb'
+ - 'spec/uploaders/ci/secure_file_uploader_spec.rb'
+ - 'spec/uploaders/file_mover_spec.rb'
+ - 'spec/uploaders/file_uploader_spec.rb'
+ - 'spec/uploaders/gitlab_uploader_spec.rb'
+ - 'spec/uploaders/import_export_uploader_spec.rb'
+ - 'spec/uploaders/namespace_file_uploader_spec.rb'
+ - 'spec/uploaders/object_storage/cdn/google_cdn_spec.rb'
+ - 'spec/uploaders/object_storage/cdn_spec.rb'
+ - 'spec/uploaders/object_storage/s3_spec.rb'
+ - 'spec/uploaders/object_storage_spec.rb'
+ - 'spec/uploaders/packages/debian/distribution_release_file_uploader_spec.rb'
+ - 'spec/uploaders/packages/npm/metadata_cache_uploader_spec.rb'
+ - 'spec/uploaders/packages/nuget/symbol_uploader_spec.rb'
+ - 'spec/uploaders/terraform/state_uploader_spec.rb'
+ - 'spec/validators/addressable_url_validator_spec.rb'
+ - 'spec/validators/color_validator_spec.rb'
+ - 'spec/validators/cron_freeze_period_timezone_validator_spec.rb'
+ - 'spec/validators/cron_validator_spec.rb'
+ - 'spec/validators/devise_email_validator_spec.rb'
+ - 'spec/validators/future_date_validator_spec.rb'
+ - 'spec/validators/import/gitlab_projects/remote_file_validator_spec.rb'
+ - 'spec/validators/iso8601_date_validator_spec.rb'
+ - 'spec/validators/json_schema_validator_spec.rb'
+ - 'spec/validators/named_ecdsa_key_validator_spec.rb'
+ - 'spec/validators/nested_attributes_duplicates_validator_spec.rb'
+ - 'spec/validators/qualified_domain_array_validator_spec.rb'
+ - 'spec/validators/sha_validator_spec.rb'
+ - 'spec/views/admin/application_settings/_ci_cd.html.haml_spec.rb'
+ - 'spec/views/admin/application_settings/_package_registry.html.haml_spec.rb'
+ - 'spec/views/devise/confirmations/almost_there.html.haml_spec.rb'
+ - 'spec/views/devise/sessions/new.html.haml_spec.rb'
+ - 'spec/views/layouts/fullscreen.html.haml_spec.rb'
+ - 'spec/views/layouts/organization.html.haml_spec.rb'
+ - 'spec/workers/analytics/usage_trends/count_job_trigger_worker_spec.rb'
+ - 'spec/workers/analytics/usage_trends/counter_job_worker_spec.rb'
+ - 'spec/workers/approve_blocked_pending_approval_users_worker_spec.rb'
+ - 'spec/workers/authorized_project_update/periodic_recalculate_worker_spec.rb'
+ - 'spec/workers/auto_devops/disable_worker_spec.rb'
+ - 'spec/workers/auto_merge_process_worker_spec.rb'
+ - 'spec/workers/bulk_imports/entity_worker_spec.rb'
+ - 'spec/workers/bulk_imports/finish_batched_pipeline_worker_spec.rb'
+ - 'spec/workers/bulk_imports/stuck_import_worker_spec.rb'
+ - 'spec/workers/ci/archive_trace_worker_spec.rb'
+ - 'spec/workers/ci/archive_traces_cron_worker_spec.rb'
+ - 'spec/workers/ci/build_finished_worker_spec.rb'
+ - 'spec/workers/ci/build_prepare_worker_spec.rb'
+ - 'spec/workers/ci/build_schedule_worker_spec.rb'
+ - 'spec/workers/ci/build_trace_chunk_flush_worker_spec.rb'
+ - 'spec/workers/ci/daily_build_group_report_results_worker_spec.rb'
+ - 'spec/workers/ci/delete_unit_tests_worker_spec.rb'
+ - 'spec/workers/ci/drop_pipeline_worker_spec.rb'
+ - 'spec/workers/ci/initial_pipeline_process_worker_spec.rb'
+ - 'spec/workers/ci/merge_requests/cleanup_ref_worker_spec.rb'
+ - 'spec/workers/ci/parse_secure_file_metadata_worker_spec.rb'
+ - 'spec/workers/ci/pending_builds/update_group_worker_spec.rb'
+ - 'spec/workers/ci/pending_builds/update_project_worker_spec.rb'
+ - 'spec/workers/ci/pipeline_artifacts/coverage_report_worker_spec.rb'
+ - 'spec/workers/ci/pipeline_artifacts/create_quality_report_worker_spec.rb'
+ - 'spec/workers/ci/pipeline_artifacts/expire_artifacts_worker_spec.rb'
+ - 'spec/workers/ci/pipeline_bridge_status_worker_spec.rb'
+ - 'spec/workers/ci/pipeline_cleanup_ref_worker_spec.rb'
+ - 'spec/workers/ci/ref_delete_unlock_artifacts_worker_spec.rb'
+ - 'spec/workers/ci/refs/unlock_previous_pipelines_worker_spec.rb'
+ - 'spec/workers/ci/resource_groups/assign_resource_from_resource_group_worker_spec.rb'
+ - 'spec/workers/ci/stuck_builds/drop_running_worker_spec.rb'
+ - 'spec/workers/ci/stuck_builds/drop_scheduled_worker_spec.rb'
+ - 'spec/workers/ci/test_failure_history_worker_spec.rb'
+ - 'spec/workers/ci/track_failed_build_worker_spec.rb'
+ - 'spec/workers/ci_platform_metrics_update_cron_worker_spec.rb'
+ - 'spec/workers/cleanup_container_repository_worker_spec.rb'
+ - 'spec/workers/clusters/agents/delete_expired_events_worker_spec.rb'
+ - 'spec/workers/clusters/agents/notify_git_push_worker_spec.rb'
+ - 'spec/workers/clusters/cleanup/project_namespace_worker_spec.rb'
+ - 'spec/workers/clusters/cleanup/service_account_worker_spec.rb'
+ - 'spec/workers/concerns/packages/cleanup_artifact_worker_spec.rb'
+ - 'spec/workers/concerns/packages/error_handling_spec.rb'
+ - 'spec/workers/concerns/worker_context_spec.rb'
+ - 'spec/workers/container_expiration_policies/cleanup_container_repository_worker_spec.rb'
+ - 'spec/workers/container_expiration_policy_worker_spec.rb'
+ - 'spec/workers/container_registry/migration/enqueuer_worker_spec.rb'
+ - 'spec/workers/container_registry/migration/guard_worker_spec.rb'
+ - 'spec/workers/container_registry/migration/observer_worker_spec.rb'
+ - 'spec/workers/counters/cleanup_refresh_worker_spec.rb'
+ - 'spec/workers/create_commit_signature_worker_spec.rb'
+ - 'spec/workers/database/drop_detached_partitions_worker_spec.rb'
+ - 'spec/workers/database/partition_management_worker_spec.rb'
+ - 'spec/workers/dependency_proxy/cleanup_dependency_proxy_worker_spec.rb'
+ - 'spec/workers/dependency_proxy/image_ttl_group_policy_worker_spec.rb'
+ - 'spec/workers/deployments/archive_in_project_worker_spec.rb'
+ - 'spec/workers/deployments/link_merge_request_worker_spec.rb'
+ - 'spec/workers/deployments/update_environment_worker_spec.rb'
+ - 'spec/workers/design_management/copy_design_collection_worker_spec.rb'
+ - 'spec/workers/detect_repository_languages_worker_spec.rb'
+ - 'spec/workers/emails_on_push_worker_spec.rb'
+ - 'spec/workers/environments/auto_delete_cron_worker_spec.rb'
+ - 'spec/workers/environments/auto_recover_worker_spec.rb'
+ - 'spec/workers/environments/auto_stop_cron_worker_spec.rb'
+ - 'spec/workers/environments/auto_stop_worker_spec.rb'
+ - 'spec/workers/environments/canary_ingress/update_worker_spec.rb'
+ - 'spec/workers/error_tracking_issue_link_worker_spec.rb'
+ - 'spec/workers/file_hook_worker_spec.rb'
+ - 'spec/workers/flush_counter_increments_worker_spec.rb'
+ - 'spec/workers/gitlab/github_gists_import/import_gist_worker_spec.rb'
+ - 'spec/workers/gitlab/github_import/pull_requests/import_merged_by_worker_spec.rb'
+ - 'spec/workers/gitlab/github_import/pull_requests/import_review_worker_spec.rb'
+ - 'spec/workers/gitlab/jira_import/import_issue_worker_spec.rb'
+ - 'spec/workers/gitlab_service_ping_worker_spec.rb'
+ - 'spec/workers/google_cloud/create_cloudsql_instance_worker_spec.rb'
+ - 'spec/workers/group_export_worker_spec.rb'
+ - 'spec/workers/group_import_worker_spec.rb'
+ - 'spec/workers/groups/update_two_factor_requirement_for_members_worker_spec.rb'
+ - 'spec/workers/integrations/create_external_cross_reference_worker_spec.rb'
+ - 'spec/workers/integrations/irker_worker_spec.rb'
+ - 'spec/workers/issuable/label_links_destroy_worker_spec.rb'
+ - 'spec/workers/issuable/related_links_create_worker_spec.rb'
+ - 'spec/workers/issuable_export_csv_worker_spec.rb'
+ - 'spec/workers/issuables/clear_groups_issue_counter_worker_spec.rb'
+ - 'spec/workers/jira_connect/send_uninstalled_hook_worker_spec.rb'
+ - 'spec/workers/jira_connect/sync_builds_worker_spec.rb'
+ - 'spec/workers/jira_connect/sync_deployments_worker_spec.rb'
+ - 'spec/workers/jira_connect/sync_feature_flags_worker_spec.rb'
+ - 'spec/workers/member_invitation_reminder_emails_worker_spec.rb'
+ - 'spec/workers/merge_request_mergeability_check_worker_spec.rb'
+ - 'spec/workers/merge_requests/close_issue_worker_spec.rb'
+ - 'spec/workers/merge_requests/create_pipeline_worker_spec.rb'
+ - 'spec/workers/merge_requests/ensure_prepared_worker_spec.rb'
+ - 'spec/workers/merge_requests/mergeability_check_batch_worker_spec.rb'
+ - 'spec/workers/merge_requests/update_head_pipeline_worker_spec.rb'
+ - 'spec/workers/merge_worker_spec.rb'
+ - 'spec/workers/namespaces/update_root_statistics_worker_spec.rb'
+ - 'spec/workers/new_note_worker_spec.rb'
+ - 'spec/workers/object_pool/create_worker_spec.rb'
+ - 'spec/workers/object_pool/destroy_worker_spec.rb'
+ - 'spec/workers/object_pool/join_worker_spec.rb'
+ - 'spec/workers/object_storage/delete_stale_direct_uploads_worker_spec.rb'
+ - 'spec/workers/onboarding/issue_created_worker_spec.rb'
+ - 'spec/workers/packages/cleanup/delete_orphaned_dependencies_worker_spec.rb'
+ - 'spec/workers/packages/cleanup_package_file_worker_spec.rb'
+ - 'spec/workers/packages/composer/cache_cleanup_worker_spec.rb'
+ - 'spec/workers/packages/composer/cache_update_worker_spec.rb'
+ - 'spec/workers/packages/debian/cleanup_dangling_package_files_worker_spec.rb'
+ - 'spec/workers/packages/debian/generate_distribution_worker_spec.rb'
+ - 'spec/workers/packages/debian/process_package_file_worker_spec.rb'
+ - 'spec/workers/packages/go/sync_packages_worker_spec.rb'
+ - 'spec/workers/packages/helm/extraction_worker_spec.rb'
+ - 'spec/workers/packages/mark_package_files_for_destruction_worker_spec.rb'
+ - 'spec/workers/packages/maven/metadata/sync_worker_spec.rb'
+ - 'spec/workers/packages/npm/cleanup_stale_metadata_cache_worker_spec.rb'
+ - 'spec/workers/packages/npm/create_metadata_cache_worker_spec.rb'
+ - 'spec/workers/packages/nuget/extraction_worker_spec.rb'
+ - 'spec/workers/packages/rubygems/extraction_worker_spec.rb'
+ - 'spec/workers/partition_creation_worker_spec.rb'
+ - 'spec/workers/pipeline_notification_worker_spec.rb'
+ - 'spec/workers/pipeline_process_worker_spec.rb'
+ - 'spec/workers/pipeline_schedule_worker_spec.rb'
+ - 'spec/workers/process_commit_worker_spec.rb'
+ - 'spec/workers/project_cache_worker_spec.rb'
+ - 'spec/workers/project_export_worker_spec.rb'
+ - 'spec/workers/projects/after_import_worker_spec.rb'
+ - 'spec/workers/projects/finalize_project_statistics_refresh_worker_spec.rb'
+ - 'spec/workers/projects/git_garbage_collect_worker_spec.rb'
+ - 'spec/workers/projects/import_export/parallel_project_export_worker_spec.rb'
+ - 'spec/workers/projects/post_creation_worker_spec.rb'
+ - 'spec/workers/projects/schedule_refresh_build_artifacts_size_statistics_worker_spec.rb'
+ - 'spec/workers/propagate_integration_group_worker_spec.rb'
+ - 'spec/workers/propagate_integration_inherit_descendant_worker_spec.rb'
+ - 'spec/workers/propagate_integration_inherit_worker_spec.rb'
+ - 'spec/workers/propagate_integration_project_worker_spec.rb'
+ - 'spec/workers/propagate_integration_worker_spec.rb'
+ - 'spec/workers/prune_old_events_worker_spec.rb'
+ - 'spec/workers/purge_dependency_proxy_cache_worker_spec.rb'
+ - 'spec/workers/rebase_worker_spec.rb'
+ - 'spec/workers/redis_migration_worker_spec.rb'
+ - 'spec/workers/remote_mirror_notification_worker_spec.rb'
+ - 'spec/workers/remove_expired_group_links_worker_spec.rb'
+ - 'spec/workers/repository_check/batch_worker_spec.rb'
+ - 'spec/workers/repository_check/dispatch_worker_spec.rb'
+ - 'spec/workers/repository_check/single_repository_worker_spec.rb'
+ - 'spec/workers/repository_import_worker_spec.rb'
+ - 'spec/workers/repository_update_remote_mirror_worker_spec.rb'
+ - 'spec/workers/schedule_merge_request_cleanup_refs_worker_spec.rb'
+ - 'spec/workers/stage_update_worker_spec.rb'
+ - 'spec/workers/stuck_ci_jobs_worker_spec.rb'
+ - 'spec/workers/system_hook_push_worker_spec.rb'
+ - 'spec/workers/terraform/states/destroy_worker_spec.rb'
+ - 'spec/workers/update_container_registry_info_worker_spec.rb'
+ - 'spec/workers/update_external_pull_requests_worker_spec.rb'
+ - 'spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb'
+ - 'spec/workers/update_highest_role_worker_spec.rb'
+ - 'spec/workers/update_merge_requests_worker_spec.rb'
+ - 'spec/workers/upload_checksum_worker_spec.rb'
+ - 'spec/workers/users/create_statistics_worker_spec.rb'
+ - 'spec/workers/users/migrate_records_to_ghost_user_in_batches_worker_spec.rb'
+ - 'spec/workers/web_hook_worker_spec.rb'
+ - 'spec/workers/web_hooks/log_destroy_worker_spec.rb'
+ - 'spec/workers/work_items/import_work_items_csv_worker_spec.rb'
+ - 'spec/workers/x509_certificate_revoke_worker_spec.rb'
diff --git a/.rubocop_todo/rspec/repeated_example_group_description.yml b/.rubocop_todo/rspec/repeated_example_group_description.yml
index d5b3ad81d01..f0e57b264e7 100644
--- a/.rubocop_todo/rspec/repeated_example_group_description.yml
+++ b/.rubocop_todo/rspec/repeated_example_group_description.yml
@@ -30,13 +30,11 @@ RSpec/RepeatedExampleGroupDescription:
- 'spec/controllers/projects/issues_controller_spec.rb'
- 'spec/controllers/projects/merge_requests/drafts_controller_spec.rb'
- 'spec/controllers/projects/pages_domains_controller_spec.rb'
- - 'spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb'
- 'spec/features/merge_request/user_sees_merge_widget_spec.rb'
- 'spec/features/projects/jobs_spec.rb'
- 'spec/features/projects/new_project_spec.rb'
- 'spec/features/security/project/private_access_spec.rb'
- 'spec/finders/ci/pipelines_for_merge_request_finder_spec.rb'
- - 'spec/frontend/fixtures/startup_css.rb'
- 'spec/helpers/admin/user_actions_helper_spec.rb'
- 'spec/helpers/dropdowns_helper_spec.rb'
- 'spec/helpers/gitlab_routing_helper_spec.rb'
@@ -56,13 +54,11 @@ RSpec/RepeatedExampleGroupDescription:
- 'spec/lib/gitlab/ci/yaml_processor_spec.rb'
- 'spec/lib/gitlab/data_builder/push_spec.rb'
- 'spec/lib/gitlab/database/migrations/batched_background_migration_helpers_spec.rb'
- - 'spec/lib/gitlab/database_importers/common_metrics/importer_spec.rb'
- 'spec/lib/gitlab/git/diff_spec.rb'
- 'spec/lib/gitlab/git/push_spec.rb'
- 'spec/lib/gitlab/git/repository_spec.rb'
- 'spec/lib/gitlab/import_export/project/sample/relation_factory_spec.rb'
- 'spec/lib/gitlab/kubernetes/rollout_status_spec.rb'
- - 'spec/lib/gitlab/metrics/dashboard/validator/errors_spec.rb'
- 'spec/lib/gitlab/sanitizers/exif_spec.rb'
- 'spec/lib/gitlab/template/finders/global_template_finder_spec.rb'
- 'spec/lib/gitlab/usage_data_spec.rb'
@@ -87,7 +83,6 @@ RSpec/RepeatedExampleGroupDescription:
- 'spec/routing/project_routing_spec.rb'
- 'spec/services/ci/create_pipeline_service/parent_child_pipeline_spec.rb'
- 'spec/services/merge_requests/refresh_service_spec.rb'
- - 'spec/services/metrics/dashboard/gitlab_alert_embed_service_spec.rb'
- 'spec/services/verify_pages_domain_service_spec.rb'
- 'spec/support/shared_examples/models/application_setting_shared_examples.rb'
- 'spec/support/shared_examples/models/concerns/limitable_shared_examples.rb'
diff --git a/.rubocop_todo/rspec/return_from_stub.yml b/.rubocop_todo/rspec/return_from_stub.yml
index 1d07be7d6dc..3604a2de612 100644
--- a/.rubocop_todo/rspec/return_from_stub.yml
+++ b/.rubocop_todo/rspec/return_from_stub.yml
@@ -38,19 +38,12 @@ RSpec/ReturnFromStub:
- 'ee/spec/services/auto_merge/add_to_merge_train_when_pipeline_succeeds_service_spec.rb'
- 'ee/spec/services/auto_merge/merge_train_service_spec.rb'
- 'ee/spec/services/deployments/auto_rollback_service_spec.rb'
- - 'ee/spec/services/geo/design_repository_sync_service_spec.rb'
- - 'ee/spec/services/geo/files_expire_service_spec.rb'
- 'ee/spec/services/geo/framework_repository_sync_service_spec.rb'
- - 'ee/spec/services/geo/project_housekeeping_service_spec.rb'
- - 'ee/spec/services/geo/repository_base_sync_service_spec.rb'
- - 'ee/spec/services/geo/repository_updated_service_spec.rb'
- - 'ee/spec/services/geo/repository_verification_primary_service_spec.rb'
- 'ee/spec/services/groups/destroy_service_spec.rb'
- 'ee/spec/services/ide/schemas_config_service_spec.rb'
- 'ee/spec/services/merge_requests/build_service_spec.rb'
- 'ee/spec/services/merge_trains/create_pipeline_service_spec.rb'
- 'ee/spec/services/merge_trains/refresh_merge_request_service_spec.rb'
- - 'ee/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb'
- 'ee/spec/services/security/token_revocation_service_spec.rb'
- 'ee/spec/services/system_notes/merge_train_service_spec.rb'
- 'ee/spec/services/wiki_pages/create_service_spec.rb'
@@ -66,12 +59,6 @@ RSpec/ReturnFromStub:
- 'ee/spec/views/layouts/application.html.haml_spec.rb'
- 'ee/spec/views/shared/_mirror_update_button.html.haml_spec.rb'
- 'ee/spec/workers/ee/ci/build_finished_worker_spec.rb'
- - 'ee/spec/workers/geo/design_repository_shard_sync_worker_spec.rb'
- - 'ee/spec/workers/geo/repository_shard_sync_worker_spec.rb'
- - 'ee/spec/workers/geo/repository_verification/primary/shard_worker_spec.rb'
- - 'ee/spec/workers/geo/repository_verification/primary/single_worker_spec.rb'
- - 'ee/spec/workers/geo/repository_verification/secondary/shard_worker_spec.rb'
- - 'ee/spec/workers/geo/repository_verification/secondary/single_worker_spec.rb'
- 'ee/spec/workers/post_receive_spec.rb'
- 'ee/spec/workers/store_security_reports_worker_spec.rb'
- 'qa/spec/specs/runner_spec.rb'
@@ -135,7 +122,6 @@ RSpec/ReturnFromStub:
- 'spec/lib/gitlab/ci/trace/remote_checksum_spec.rb'
- 'spec/lib/gitlab/contributions_calendar_spec.rb'
- 'spec/lib/gitlab/daemon_spec.rb'
- - 'spec/lib/gitlab/database_importers/instance_administrators/create_group_spec.rb'
- 'spec/lib/gitlab/diff/file_spec.rb'
- 'spec/lib/gitlab/exclusive_lease_helpers/sleeping_lock_spec.rb'
- 'spec/lib/gitlab/exclusive_lease_helpers_spec.rb'
@@ -229,13 +215,10 @@ RSpec/ReturnFromStub:
- 'spec/services/projects/apple_target_platform_detector_service_spec.rb'
- 'spec/services/projects/create_service_spec.rb'
- 'spec/services/projects/hashed_storage/migrate_attachments_service_spec.rb'
- - 'spec/services/projects/hashed_storage/migrate_repository_service_spec.rb'
- - 'spec/services/projects/in_product_marketing_campaign_emails_service_spec.rb'
- 'spec/services/projects/update_remote_mirror_service_spec.rb'
- 'spec/services/projects/update_service_spec.rb'
- 'spec/services/suggestions/create_service_spec.rb'
- 'spec/services/verify_pages_domain_service_spec.rb'
- - 'spec/support/redis/redis_shared_examples.rb'
- 'spec/support/shared_contexts/services/projects/container_repository/delete_tags_service_shared_context.rb'
- 'spec/support/shared_examples/finders/snippet_visibility_shared_examples.rb'
- 'spec/support/shared_examples/lib/gitlab/middleware/read_only_gitlab_instance_shared_examples.rb'
diff --git a/.rubocop_todo/rspec/scattered_let.yml b/.rubocop_todo/rspec/scattered_let.yml
index 4d93c6816cc..627ab6870b8 100644
--- a/.rubocop_todo/rspec/scattered_let.yml
+++ b/.rubocop_todo/rspec/scattered_let.yml
@@ -73,7 +73,6 @@ RSpec/ScatteredLet:
- 'ee/spec/services/security/report_summary_service_spec.rb'
- 'ee/spec/services/vulnerabilities/security_finding/create_issue_service_spec.rb'
- 'ee/spec/workers/concerns/update_orchestration_policy_configuration_spec.rb'
- - 'spec/controllers/concerns/metrics_dashboard_spec.rb'
- 'spec/controllers/import/bitbucket_server_controller_spec.rb'
- 'spec/controllers/projects/deploy_keys_controller_spec.rb'
- 'spec/controllers/projects/environments_controller_spec.rb'
@@ -125,7 +124,6 @@ RSpec/ScatteredLet:
- 'spec/lib/gitlab/background_migration/backfill_imported_issue_search_data_spec.rb'
- 'spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_inactive_public_projects_spec.rb'
- 'spec/lib/gitlab/background_migration/legacy_uploads_migrator_spec.rb'
- - 'spec/lib/gitlab/background_migration/nullify_orphan_runner_id_on_ci_builds_spec.rb'
- 'spec/lib/gitlab/background_migration/reset_too_many_tags_skipped_registry_imports_spec.rb'
- 'spec/lib/gitlab/background_migration/update_jira_tracker_data_deployment_type_based_on_url_spec.rb'
- 'spec/lib/gitlab/checks/matching_merge_request_spec.rb'
@@ -137,7 +135,6 @@ RSpec/ScatteredLet:
- 'spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb'
- 'spec/lib/gitlab/database/background_migration/batched_migration_spec.rb'
- 'spec/lib/gitlab/database/partitioning/partition_manager_spec.rb'
- - 'spec/lib/gitlab/database/postgresql_adapter/empty_query_ping_spec.rb'
- 'spec/lib/gitlab/database/reindexing/grafana_notifier_spec.rb'
- 'spec/lib/gitlab/diff/file_collection/merge_request_diff_batch_spec.rb'
- 'spec/lib/gitlab/diff/formatters/text_formatter_spec.rb'
@@ -156,14 +153,12 @@ RSpec/ScatteredLet:
- 'spec/lib/gitlab/import_export/project/sample/relation_tree_restorer_spec.rb'
- 'spec/lib/gitlab/jira_import/issue_serializer_spec.rb'
- 'spec/lib/gitlab/lets_encrypt/client_spec.rb'
- - 'spec/lib/gitlab/metrics/dashboard/stages/grafana_formatter_spec.rb'
- 'spec/lib/gitlab/metrics/subscribers/external_http_spec.rb'
- 'spec/lib/gitlab/middleware/memory_report_spec.rb'
- 'spec/lib/gitlab/pagination/keyset/page_spec.rb'
- 'spec/lib/gitlab/pagination/offset_pagination_spec.rb'
- 'spec/lib/gitlab/patch/database_config_spec.rb'
- 'spec/lib/gitlab/path_regex_spec.rb'
- - 'spec/lib/gitlab/prometheus/queries/matched_metric_query_spec.rb'
- 'spec/lib/gitlab/relative_positioning/item_context_spec.rb'
- 'spec/lib/gitlab/relative_positioning/mover_spec.rb'
- 'spec/lib/gitlab/serializer/pagination_spec.rb'
@@ -178,7 +173,6 @@ RSpec/ScatteredLet:
- 'spec/lib/peek/views/external_http_spec.rb'
- 'spec/mailers/notify_spec.rb'
- 'spec/mailers/previews_spec.rb'
- - 'spec/migrations/20220329175119_remove_leftover_ci_job_artifact_deletions_spec.rb'
- 'spec/models/ci/bridge_spec.rb'
- 'spec/models/ci/build_dependencies_spec.rb'
- 'spec/models/ci/pipeline_spec.rb'
@@ -237,10 +231,6 @@ RSpec/ScatteredLet:
- 'spec/services/merge_requests/mergeability/logger_spec.rb'
- 'spec/services/merge_requests/update_assignees_service_spec.rb'
- 'spec/services/merge_requests/update_reviewers_service_spec.rb'
- - 'spec/services/metrics/dashboard/custom_metric_embed_service_spec.rb'
- - 'spec/services/metrics/dashboard/dynamic_embed_service_spec.rb'
- - 'spec/services/metrics/dashboard/gitlab_alert_embed_service_spec.rb'
- - 'spec/services/metrics/dashboard/grafana_metric_embed_service_spec.rb'
- 'spec/services/notification_service_spec.rb'
- 'spec/services/packages/composer/create_package_service_spec.rb'
- 'spec/services/packages/conan/create_package_file_service_spec.rb'
diff --git a/.rubocop_todo/rspec/specify_expected.yml b/.rubocop_todo/rspec/specify_expected.yml
new file mode 100644
index 00000000000..9a4c7a769d7
--- /dev/null
+++ b/.rubocop_todo/rspec/specify_expected.yml
@@ -0,0 +1,54 @@
+---
+# Cop supports --autocorrect.
+RSpec/SpecifyExpected:
+ Details: grace period
+ Exclude:
+ - 'ee/spec/controllers/groups/analytics/repository_analytics_controller_spec.rb'
+ - 'ee/spec/graphql/ee/types/branch_protection_type_spec.rb'
+ - 'ee/spec/graphql/ee/types/branch_protections/merge_access_level_type_spec.rb'
+ - 'ee/spec/graphql/ee/types/branch_protections/push_access_level_type_spec.rb'
+ - 'ee/spec/graphql/ee/types/branch_protections/unprotect_access_level_type_spec.rb'
+ - 'ee/spec/graphql/mutations/boards/epic_boards/create_spec.rb'
+ - 'ee/spec/graphql/mutations/boards/epic_boards/update_spec.rb'
+ - 'ee/spec/lib/code_suggestions/instructions_extractor_spec.rb'
+ - 'ee/spec/lib/ee/sidebars/projects/menus/analytics_menu_spec.rb'
+ - 'ee/spec/lib/ee/sidebars/projects/menus/settings_menu_spec.rb'
+ - 'ee/spec/lib/sidebars/groups/menus/analytics_menu_spec.rb'
+ - 'ee/spec/lib/sidebars/groups/menus/epics_menu_spec.rb'
+ - 'ee/spec/lib/sidebars/groups/menus/security_compliance_menu_spec.rb'
+ - 'ee/spec/lib/sidebars/groups/menus/wiki_menu_spec.rb'
+ - 'ee/spec/models/ee/users/merge_request_interaction_spec.rb'
+ - 'ee/spec/services/security/security_orchestration_policies/update_violations_service_spec.rb'
+ - 'spec/finders/ci/freeze_periods_finder_spec.rb'
+ - 'spec/finders/groups/user_groups_finder_spec.rb'
+ - 'spec/graphql/resolvers/users/groups_resolver_spec.rb'
+ - 'spec/graphql/types/access_levels/deploy_key_type_spec.rb'
+ - 'spec/graphql/types/branch_protections/merge_access_level_type_spec.rb'
+ - 'spec/graphql/types/branch_protections/push_access_level_type_spec.rb'
+ - 'spec/graphql/types/branch_rules/branch_protection_type_spec.rb'
+ - 'spec/lib/banzai/filter/references/design_reference_filter_spec.rb'
+ - 'spec/lib/gitlab/color_spec.rb'
+ - 'spec/lib/sidebars/admin/menus/monitoring_menu_spec.rb'
+ - 'spec/lib/sidebars/groups/menus/ci_cd_menu_spec.rb'
+ - 'spec/lib/sidebars/groups/menus/group_information_menu_spec.rb'
+ - 'spec/lib/sidebars/groups/menus/issues_menu_spec.rb'
+ - 'spec/lib/sidebars/groups/menus/scope_menu_spec.rb'
+ - 'spec/lib/sidebars/groups/menus/settings_menu_spec.rb'
+ - 'spec/lib/sidebars/projects/menus/analytics_menu_spec.rb'
+ - 'spec/lib/sidebars/projects/menus/deployments_menu_spec.rb'
+ - 'spec/lib/sidebars/projects/menus/hidden_menu_spec.rb'
+ - 'spec/lib/sidebars/projects/menus/infrastructure_menu_spec.rb'
+ - 'spec/lib/sidebars/projects/menus/monitor_menu_spec.rb'
+ - 'spec/lib/sidebars/projects/menus/project_information_menu_spec.rb'
+ - 'spec/lib/sidebars/projects/menus/scope_menu_spec.rb'
+ - 'spec/lib/sidebars/projects/menus/settings_menu_spec.rb'
+ - 'spec/models/application_setting_spec.rb'
+ - 'spec/models/concerns/subquery_spec.rb'
+ - 'spec/models/integrations/jira_tracker_data_spec.rb'
+ - 'spec/models/issue_spec.rb'
+ - 'spec/models/project_spec.rb'
+ - 'spec/models/user_spec.rb'
+ - 'spec/policies/group_policy_spec.rb'
+ - 'spec/policies/merge_request_policy_spec.rb'
+ - 'spec/policies/project_policy_spec.rb'
+ - 'spec/requests/api/graphql/current_user/groups_query_spec.rb'
diff --git a/.rubocop_todo/rspec/useless_dynamic_definition.yml b/.rubocop_todo/rspec/useless_dynamic_definition.yml
deleted file mode 100644
index 75bea5601ae..00000000000
--- a/.rubocop_todo/rspec/useless_dynamic_definition.yml
+++ /dev/null
@@ -1,10 +0,0 @@
----
-RSpec/UselessDynamicDefinition:
- Exclude:
- - 'ee/spec/factories/ci/builds.rb'
- - 'ee/spec/factories/ci/job_artifacts.rb'
- - 'ee/spec/factories/ci/pipelines.rb'
- - 'ee/spec/lib/gitlab/usage/metrics/instrumentations/count_security_scans_metric_spec.rb'
- - 'spec/models/ci/resource_group_spec.rb'
- - 'spec/services/packages/nuget/update_package_from_metadata_service_spec.rb'
- - 'spec/support/helpers/cycle_analytics_helpers/test_generation.rb'
diff --git a/.rubocop_todo/rspec/variable_definition.yml b/.rubocop_todo/rspec/variable_definition.yml
index 187064b9bb4..8cff98aaf64 100644
--- a/.rubocop_todo/rspec/variable_definition.yml
+++ b/.rubocop_todo/rspec/variable_definition.yml
@@ -2,4 +2,3 @@
# Cop supports --autocorrect.
RSpec/VariableDefinition:
Exclude:
- - 'spec/presenters/packages/npm/package_presenter_spec.rb'
diff --git a/.rubocop_todo/rspec/verified_doubles.yml b/.rubocop_todo/rspec/verified_doubles.yml
index 6e3e3abf87e..d26067d2783 100644
--- a/.rubocop_todo/rspec/verified_doubles.yml
+++ b/.rubocop_todo/rspec/verified_doubles.yml
@@ -55,7 +55,6 @@ RSpec/VerifiedDoubles:
- 'ee/spec/lib/gitlab/auth/otp/session_enforcer_spec.rb'
- 'ee/spec/lib/gitlab/authority_analyzer_spec.rb'
- 'ee/spec/lib/gitlab/cache_spec.rb'
- - 'ee/spec/lib/gitlab/ci/pipeline/chain/limit/activity_spec.rb'
- 'ee/spec/lib/gitlab/ci/pipeline/chain/limit/size_spec.rb'
- 'ee/spec/lib/gitlab/code_owners/groups_loader_spec.rb'
- 'ee/spec/lib/gitlab/code_owners/users_loader_spec.rb'
@@ -72,7 +71,6 @@ RSpec/VerifiedDoubles:
- 'ee/spec/lib/gitlab/git_access_spec.rb'
- 'ee/spec/lib/gitlab/import_export/group/relation_factory_spec.rb'
- 'ee/spec/lib/gitlab/middleware/ip_restrictor_spec.rb'
- - 'ee/spec/lib/gitlab/prometheus/queries/cluster_query_spec.rb'
- 'ee/spec/lib/sidebars/groups/menus/analytics_menu_spec.rb'
- 'ee/spec/lib/system_check/geo/geo_database_configured_check_spec.rb'
- 'ee/spec/models/app_sec/fuzzing/api/ci_configuration_spec.rb'
@@ -81,7 +79,6 @@ RSpec/VerifiedDoubles:
- 'ee/spec/models/ee/project_spec.rb'
- 'ee/spec/models/ee/user_spec.rb'
- 'ee/spec/models/elastic/index_setting_spec.rb'
- - 'ee/spec/models/geo/project_registry_spec.rb'
- 'ee/spec/models/geo/secondary_usage_data_spec.rb'
- 'ee/spec/models/geo_node_status_spec.rb'
- 'ee/spec/models/integrations/github/status_message_spec.rb'
@@ -116,11 +113,9 @@ RSpec/VerifiedDoubles:
- 'ee/spec/serializers/epic_note_entity_spec.rb'
- 'ee/spec/serializers/integrations/jira_serializers/issue_detail_entity_spec.rb'
- 'ee/spec/serializers/integrations/jira_serializers/issue_entity_spec.rb'
- - 'ee/spec/serializers/issuable_sidebar_extras_entity_spec.rb'
- 'ee/spec/serializers/issues/linked_issue_feature_flag_entity_spec.rb'
- 'ee/spec/serializers/linked_feature_flag_issue_entity_spec.rb'
- 'ee/spec/serializers/merge_request_poll_widget_entity_spec.rb'
- - 'ee/spec/serializers/merge_request_sidebar_basic_entity_spec.rb'
- 'ee/spec/serializers/merge_request_widget_entity_spec.rb'
- 'ee/spec/serializers/test_reports_comparer_serializer_spec.rb'
- 'ee/spec/serializers/user_analytics_entity_spec.rb'
@@ -151,7 +146,6 @@ RSpec/VerifiedDoubles:
- 'ee/spec/services/geo/graphql_request_service_spec.rb'
- 'ee/spec/services/geo/node_status_request_service_spec.rb'
- 'ee/spec/services/geo/replication_toggle_request_service_spec.rb'
- - 'ee/spec/services/geo/repository_verification_primary_service_spec.rb'
- 'ee/spec/services/gitlab_subscriptions/fetch_subscription_plans_service_spec.rb'
- 'ee/spec/services/group_saml/group_managed_accounts/clean_up_members_service_spec.rb'
- 'ee/spec/services/group_saml/sign_up_service_spec.rb'
@@ -173,7 +167,6 @@ RSpec/VerifiedDoubles:
- 'ee/spec/services/vulnerability_exports/export_service_spec.rb'
- 'ee/spec/services/vulnerability_external_issue_links/create_service_spec.rb'
- 'ee/spec/support/helpers/ee/ldap_helpers.rb'
- - 'ee/spec/support/prometheus/additional_metrics_shared_examples.rb'
- 'ee/spec/support/shared_examples/controllers/analytics/cycle_analytics/shared_stage_shared_examples.rb'
- 'ee/spec/support/shared_examples/controllers/cluster_metrics_shared_examples.rb'
- 'ee/spec/support/shared_examples/models/concerns/blob_replicator_strategy_shared_examples.rb'
@@ -188,7 +181,6 @@ RSpec/VerifiedDoubles:
- 'ee/spec/workers/ci/sync_reports_to_report_approval_rules_worker_spec.rb'
- 'ee/spec/workers/compliance_management/chain_of_custody_report_worker_spec.rb'
- 'ee/spec/workers/geo/container_repository_sync_worker_spec.rb'
- - 'ee/spec/workers/geo/design_repository_sync_worker_spec.rb'
- 'ee/spec/workers/geo/destroy_worker_spec.rb'
- 'ee/spec/workers/geo/event_worker_spec.rb'
- 'ee/spec/workers/geo/metrics_update_worker_spec.rb'
@@ -240,8 +232,6 @@ RSpec/VerifiedDoubles:
- 'spec/controllers/import/fogbugz_controller_spec.rb'
- 'spec/controllers/import/gitea_controller_spec.rb'
- 'spec/controllers/import/github_controller_spec.rb'
- - 'spec/controllers/import/gitlab_controller_spec.rb'
- - 'spec/controllers/oauth/jira_dvcs/authorizations_controller_spec.rb'
- 'spec/controllers/omniauth_callbacks_controller_spec.rb'
- 'spec/controllers/profiles/two_factor_auths_controller_spec.rb'
- 'spec/controllers/projects/blob_controller_spec.rb'
@@ -251,9 +241,7 @@ RSpec/VerifiedDoubles:
- 'spec/controllers/projects/merge_requests_controller_spec.rb'
- 'spec/controllers/projects/notes_controller_spec.rb'
- 'spec/controllers/projects/pages_controller_spec.rb'
- - 'spec/controllers/projects/performance_monitoring/dashboards_controller_spec.rb'
- 'spec/controllers/projects/pipelines_controller_spec.rb'
- - 'spec/controllers/projects/prometheus/metrics_controller_spec.rb'
- 'spec/controllers/projects/registry/tags_controller_spec.rb'
- 'spec/controllers/projects/settings/operations_controller_spec.rb'
- 'spec/controllers/projects/snippets_controller_spec.rb'
@@ -313,7 +301,6 @@ RSpec/VerifiedDoubles:
- 'spec/helpers/version_check_helper_spec.rb'
- 'spec/initializers/doorkeeper_spec.rb'
- 'spec/initializers/global_id_spec.rb'
- - 'spec/initializers/hangouts_chat_http_override_spec.rb'
- 'spec/lib/api/base_spec.rb'
- 'spec/lib/api/entities/ci/job_request/image_spec.rb'
- 'spec/lib/api/entities/ci/job_request/port_spec.rb'
@@ -371,7 +358,6 @@ RSpec/VerifiedDoubles:
- 'spec/lib/gitlab/background_migration_spec.rb'
- 'spec/lib/gitlab/bitbucket_import/importer_spec.rb'
- 'spec/lib/gitlab/bitbucket_import/project_creator_spec.rb'
- - 'spec/lib/gitlab/bitbucket_server_import/importer_spec.rb'
- 'spec/lib/gitlab/cache/import/caching_spec.rb'
- 'spec/lib/gitlab/changelog/committer_spec.rb'
- 'spec/lib/gitlab/chat/responder/base_spec.rb'
@@ -490,7 +476,6 @@ RSpec/VerifiedDoubles:
- 'spec/lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table_spec.rb'
- 'spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb'
- 'spec/lib/gitlab/database/partitioning_spec.rb'
- - 'spec/lib/gitlab/database/postgresql_adapter/empty_query_ping_spec.rb'
- 'spec/lib/gitlab/database/postgresql_database_tasks/load_schema_versions_mixin_spec.rb'
- 'spec/lib/gitlab/database/query_analyzer_spec.rb'
- 'spec/lib/gitlab/database/reindexing/grafana_notifier_spec.rb'
@@ -519,7 +504,6 @@ RSpec/VerifiedDoubles:
- 'spec/lib/gitlab/git/blob_spec.rb'
- 'spec/lib/gitlab/git/commit_spec.rb'
- 'spec/lib/gitlab/git/repository_spec.rb'
- - 'spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb'
- 'spec/lib/gitlab/git/tag_spec.rb'
- 'spec/lib/gitlab/git_access_snippet_spec.rb'
- 'spec/lib/gitlab/gitaly_client/commit_service_spec.rb'
@@ -544,10 +528,7 @@ RSpec/VerifiedDoubles:
- 'spec/lib/gitlab/github_import/importer/note_importer_spec.rb'
- 'spec/lib/gitlab/github_import/importer/notes_importer_spec.rb'
- 'spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb'
- - 'spec/lib/gitlab/github_import/importer/pull_request_merged_by_importer_spec.rb'
- 'spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb'
- - 'spec/lib/gitlab/github_import/importer/pull_requests_merged_by_importer_spec.rb'
- - 'spec/lib/gitlab/github_import/importer/pull_requests_reviews_importer_spec.rb'
- 'spec/lib/gitlab/github_import/importer/releases_importer_spec.rb'
- 'spec/lib/gitlab/github_import/importer/repository_importer_spec.rb'
- 'spec/lib/gitlab/github_import/importer/single_endpoint_diff_notes_importer_spec.rb'
@@ -596,7 +577,6 @@ RSpec/VerifiedDoubles:
- 'spec/lib/gitlab/import_export/project/tree_saver_spec.rb'
- 'spec/lib/gitlab/issuables_count_for_state_spec.rb'
- 'spec/lib/gitlab/issues/rebalancing/state_spec.rb'
- - 'spec/lib/gitlab/jira/middleware_spec.rb'
- 'spec/lib/gitlab/jira_import/issue_serializer_spec.rb'
- 'spec/lib/gitlab/jira_import/labels_importer_spec.rb'
- 'spec/lib/gitlab/jira_import/metadata_collector_spec.rb'
@@ -616,7 +596,6 @@ RSpec/VerifiedDoubles:
- 'spec/lib/gitlab/merge_requests/mergeability/redis_interface_spec.rb'
- 'spec/lib/gitlab/merge_requests/message_generator_spec.rb'
- 'spec/lib/gitlab/metrics/boot_time_tracker_spec.rb'
- - 'spec/lib/gitlab/metrics/dashboard/importers/prometheus_metrics_spec.rb'
- 'spec/lib/gitlab/metrics/elasticsearch_rack_middleware_spec.rb'
- 'spec/lib/gitlab/metrics/exporter/base_exporter_spec.rb'
- 'spec/lib/gitlab/metrics/exporter/gc_request_middleware_spec.rb'
@@ -664,8 +643,6 @@ RSpec/VerifiedDoubles:
- 'spec/lib/gitlab/process_management_spec.rb'
- 'spec/lib/gitlab/profiler_spec.rb'
- 'spec/lib/gitlab/prometheus/adapter_spec.rb'
- - 'spec/lib/gitlab/prometheus/queries/deployment_query_spec.rb'
- - 'spec/lib/gitlab/prometheus/queries/matched_metric_query_spec.rb'
- 'spec/lib/gitlab/query_limiting/middleware_spec.rb'
- 'spec/lib/gitlab/quick_actions/dsl_spec.rb'
- 'spec/lib/gitlab/repository_cache_spec.rb'
@@ -841,7 +818,6 @@ RSpec/VerifiedDoubles:
- 'spec/serializers/pipeline_details_entity_spec.rb'
- 'spec/serializers/pipeline_serializer_spec.rb'
- 'spec/serializers/project_note_entity_spec.rb'
- - 'spec/serializers/prometheus_alert_entity_spec.rb'
- 'spec/serializers/review_app_setup_entity_spec.rb'
- 'spec/serializers/runner_entity_spec.rb'
- 'spec/serializers/stage_entity_spec.rb'
@@ -886,9 +862,6 @@ RSpec/VerifiedDoubles:
- 'spec/services/merge_requests/refresh_service_spec.rb'
- 'spec/services/merge_requests/reopen_service_spec.rb'
- 'spec/services/merge_requests/request_review_service_spec.rb'
- - 'spec/services/metrics/dashboard/clone_dashboard_service_spec.rb'
- - 'spec/services/metrics/dashboard/update_dashboard_service_spec.rb'
- - 'spec/services/metrics/users_starred_dashboards/create_service_spec.rb'
- 'spec/services/milestones/update_service_spec.rb'
- 'spec/services/notes/create_service_spec.rb'
- 'spec/services/notes/render_service_spec.rb'
@@ -933,7 +906,6 @@ RSpec/VerifiedDoubles:
- 'spec/support/helpers/project_forks_helper.rb'
- 'spec/support/helpers/stub_metrics.rb'
- 'spec/support/import_export/common_util.rb'
- - 'spec/support/prometheus/additional_metrics_shared_examples.rb'
- 'spec/support/shared_contexts/lib/gitlab/sidekiq_middleware/server_metrics_shared_context.rb'
- 'spec/support/shared_contexts/services/projects/container_repository/delete_tags_service_shared_context.rb'
- 'spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb'
@@ -963,7 +935,6 @@ RSpec/VerifiedDoubles:
- 'spec/support/shared_examples/services/boards/issues_move_service_shared_examples.rb'
- 'spec/support/shared_examples/services/check_ingress_ip_address_service_shared_examples.rb'
- 'spec/support/shared_examples/services/jira/requests/base_shared_examples.rb'
- - 'spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb'
- 'spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb'
- 'spec/support/shared_examples/services/resource_events/synthetic_notes_builder_shared_examples.rb'
- 'spec/support/shared_examples/workers/background_migration_worker_shared_examples.rb'
@@ -973,7 +944,6 @@ RSpec/VerifiedDoubles:
- 'spec/tasks/gitlab/check_rake_spec.rb'
- 'spec/tasks/gitlab/cleanup_rake_spec.rb'
- 'spec/tasks/gitlab/db_rake_spec.rb'
- - 'spec/tasks/gitlab/packages/events_rake_spec.rb'
- 'spec/tasks/gitlab/setup_rake_spec.rb'
- 'spec/tooling/danger/project_helper_spec.rb'
- 'spec/tooling/lib/tooling/helm3_client_spec.rb'
diff --git a/.rubocop_todo/search/namespaced_class.yml b/.rubocop_todo/search/namespaced_class.yml
index 53dae651959..aea0ecac2a4 100644
--- a/.rubocop_todo/search/namespaced_class.yml
+++ b/.rubocop_todo/search/namespaced_class.yml
@@ -39,7 +39,6 @@ Search/NamespacedClass:
- 'ee/app/models/elasticsearch_indexed_namespace.rb'
- 'ee/app/models/elasticsearch_indexed_project.rb'
- 'ee/app/models/zoekt/indexed_namespace.rb'
- - 'ee/app/models/zoekt/shard.rb'
- 'ee/app/presenters/ee/search_service_presenter.rb'
- 'ee/app/services/ee/search_service.rb'
- 'ee/app/services/elastic/bookkeeping_shard_service.rb'
diff --git a/.rubocop_todo/sidekiq_load_balancing/worker_data_consistency.yml b/.rubocop_todo/sidekiq_load_balancing/worker_data_consistency.yml
index 1c61aa893a2..0c8affcd964 100644
--- a/.rubocop_todo/sidekiq_load_balancing/worker_data_consistency.yml
+++ b/.rubocop_todo/sidekiq_load_balancing/worker_data_consistency.yml
@@ -69,7 +69,6 @@ SidekiqLoadBalancing/WorkerDataConsistency:
- 'app/workers/clusters/applications/deactivate_integration_worker.rb'
- 'app/workers/clusters/applications/uninstall_worker.rb'
- 'app/workers/clusters/applications/wait_for_uninstall_app_worker.rb'
- - 'app/workers/clusters/integrations/check_prometheus_health_worker.rb'
- 'app/workers/container_expiration_policies/cleanup_container_repository_worker.rb'
- 'app/workers/container_expiration_policy_worker.rb'
- 'app/workers/container_registry/cleanup_worker.rb'
@@ -82,7 +81,6 @@ SidekiqLoadBalancing/WorkerDataConsistency:
- 'app/workers/create_pipeline_worker.rb'
- 'app/workers/database/drop_detached_partitions_worker.rb'
- 'app/workers/database/partition_management_worker.rb'
- - 'app/workers/delete_container_repository_worker.rb'
- 'app/workers/delete_diff_files_worker.rb'
- 'app/workers/delete_merged_branches_worker.rb'
- 'app/workers/delete_stored_files_worker.rb'
@@ -135,7 +133,6 @@ SidekiqLoadBalancing/WorkerDataConsistency:
- 'app/workers/gitlab/jira_import/stage/start_import_worker.rb'
- 'app/workers/gitlab_performance_bar_stats_worker.rb'
- 'app/workers/gitlab_service_ping_worker.rb'
- - 'app/workers/gitlab_shell_worker.rb'
- 'app/workers/google_cloud/create_cloudsql_instance_worker.rb'
- 'app/workers/group_destroy_worker.rb'
- 'app/workers/group_export_worker.rb'
@@ -177,9 +174,6 @@ SidekiqLoadBalancing/WorkerDataConsistency:
- 'app/workers/merge_requests/handle_assignees_change_worker.rb'
- 'app/workers/merge_requests/resolve_todos_worker.rb'
- 'app/workers/merge_worker.rb'
- - 'app/workers/metrics/dashboard/prune_old_annotations_worker.rb'
- - 'app/workers/metrics/dashboard/schedule_annotations_prune_worker.rb'
- - 'app/workers/metrics/dashboard/sync_dashboards_worker.rb'
- 'app/workers/migrate_external_diffs_worker.rb'
- 'app/workers/namespaces/process_sync_events_worker.rb'
- 'app/workers/namespaces/prune_aggregation_schedules_worker.rb'
@@ -203,7 +197,6 @@ SidekiqLoadBalancing/WorkerDataConsistency:
- 'app/workers/packages/composer/cache_update_worker.rb'
- 'app/workers/packages/debian/cleanup_dangling_package_files_worker.rb'
- 'app/workers/packages/debian/generate_distribution_worker.rb'
- - 'app/workers/packages/debian/process_changes_worker.rb'
- 'app/workers/packages/debian/process_package_file_worker.rb'
- 'app/workers/packages/go/sync_packages_worker.rb'
- 'app/workers/packages/helm/extraction_worker.rb'
@@ -356,10 +349,8 @@ SidekiqLoadBalancing/WorkerDataConsistency:
- 'ee/app/workers/geo/batch_event_create_worker.rb'
- 'ee/app/workers/geo/container_repository_sync_worker.rb'
- 'ee/app/workers/geo/create_repository_updated_event_worker.rb'
- - 'ee/app/workers/geo/design_repository_sync_worker.rb'
- 'ee/app/workers/geo/destroy_worker.rb'
- 'ee/app/workers/geo/event_worker.rb'
- - 'ee/app/workers/geo/file_registry_removal_worker.rb'
- 'ee/app/workers/geo/file_removal_worker.rb'
- 'ee/app/workers/geo/hashed_storage_attachments_migration_worker.rb'
- 'ee/app/workers/geo/hashed_storage_migration_worker.rb'
@@ -412,14 +403,8 @@ SidekiqLoadBalancing/WorkerDataConsistency:
- 'ee/app/workers/merge_requests/llm/summarize_merge_request_worker.rb'
- 'ee/app/workers/merge_requests/sync_code_owner_approval_rules_worker.rb'
- 'ee/app/workers/merge_trains/refresh_worker.rb'
- - 'ee/app/workers/namespaces/free_user_cap/backfill_notification_clearing_jobs_worker.rb'
- - 'ee/app/workers/namespaces/free_user_cap/backfill_notification_jobs_worker.rb'
- - 'ee/app/workers/namespaces/free_user_cap/notification_clearing_worker.rb'
- - 'ee/app/workers/namespaces/free_user_cap/over_limit_notification_worker.rb'
- 'ee/app/workers/namespaces/sync_namespace_name_worker.rb'
- 'ee/app/workers/new_epic_worker.rb'
- - 'ee/app/workers/onboarding/create_learn_gitlab_worker.rb'
- - 'ee/app/workers/package_metadata/sync_worker.rb'
- 'ee/app/workers/personal_access_tokens/groups/policy_worker.rb'
- 'ee/app/workers/personal_access_tokens/instance/policy_worker.rb'
- 'ee/app/workers/projects/register_suggested_reviewers_project_worker.rb'
diff --git a/.rubocop_todo/style/accessor_grouping.yml b/.rubocop_todo/style/accessor_grouping.yml
index 5ada1a48ccd..4587df5d52d 100644
--- a/.rubocop_todo/style/accessor_grouping.yml
+++ b/.rubocop_todo/style/accessor_grouping.yml
@@ -1,6 +1,7 @@
---
# Cop supports --autocorrect.
Style/AccessorGrouping:
+ Details: grace period
Exclude:
- 'app/finders/template_finder.rb'
- 'app/models/commit.rb'
@@ -26,7 +27,6 @@ Style/AccessorGrouping:
- 'ee/app/models/approval_wrapped_rule.rb'
- 'ee/app/models/integrations/chat_message/vulnerability_message.rb'
- 'ee/app/services/ci/pipeline_creation/drop_not_runnable_builds_service.rb'
- - 'ee/app/services/geo/project_housekeeping_service.rb'
- 'ee/lib/gitlab/ci/reports/coverage_fuzzing/crash.rb'
- 'ee/lib/gitlab/ci/reports/coverage_fuzzing/report.rb'
- 'ee/lib/gitlab/ci/reports/security/locations/container_scanning.rb'
@@ -35,7 +35,6 @@ Style/AccessorGrouping:
- 'ee/lib/gitlab/ci/reports/security/locations/dependency_scanning.rb'
- 'lib/feature/definition.rb'
- 'lib/gitlab/audit/type/definition.rb'
- - 'lib/gitlab/bitbucket_server_import/importer.rb'
- 'lib/gitlab/ci/config/external/context.rb'
- 'lib/gitlab/ci/reports/security/finding.rb'
- 'lib/gitlab/ci/reports/security/identifier.rb'
@@ -56,6 +55,7 @@ Style/AccessorGrouping:
- 'lib/gitlab/graphql/connection_redaction.rb'
- 'lib/gitlab/http_io.rb'
- 'lib/gitlab/import_export/project/tree_restorer.rb'
+ - 'lib/gitlab/search/abuse_detection.rb'
- 'lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb'
- 'lib/gitlab/suggestions/file_suggestion.rb'
- 'lib/gitlab/tracking/event_definition.rb'
@@ -66,6 +66,5 @@ Style/AccessorGrouping:
- 'lib/object_storage/direct_upload.rb'
- 'lib/safe_zip/entry.rb'
- 'lib/system_check/simple_executor.rb'
- - 'lib/uploaded_file.rb'
- 'qa/qa/ee/resource/geo/node.rb'
- 'qa/qa/ee/resource/settings/elasticsearch.rb'
diff --git a/.rubocop_todo/style/arguments_forwarding.yml b/.rubocop_todo/style/arguments_forwarding.yml
new file mode 100644
index 00000000000..f51e582b5a0
--- /dev/null
+++ b/.rubocop_todo/style/arguments_forwarding.yml
@@ -0,0 +1,173 @@
+---
+# Cop supports --autocorrect.
+Style/ArgumentsForwarding:
+ Details: grace period
+ Exclude:
+ - 'app/controllers/chaos_controller.rb'
+ - 'app/finders/clusters/knative_services_finder.rb'
+ - 'app/finders/group_finder.rb'
+ - 'app/graphql/mutations/ci/runner/create.rb'
+ - 'app/graphql/mutations/design_management/move.rb'
+ - 'app/graphql/mutations/todos/mark_all_done.rb'
+ - 'app/graphql/resolvers/concerns/caching_array_resolver.rb'
+ - 'app/helpers/emoji_helper.rb'
+ - 'app/helpers/issuables_helper.rb'
+ - 'app/helpers/namespaces_helper.rb'
+ - 'app/helpers/notify_helper.rb'
+ - 'app/helpers/routing/groups/members_helper.rb'
+ - 'app/helpers/routing/pipeline_schedules_helper.rb'
+ - 'app/helpers/routing/projects/members_helper.rb'
+ - 'app/helpers/routing/projects_helper.rb'
+ - 'app/helpers/routing/snippets_helper.rb'
+ - 'app/helpers/wiki_helper.rb'
+ - 'app/helpers/workhorse_helper.rb'
+ - 'app/models/application_record.rb'
+ - 'app/models/ci/build.rb'
+ - 'app/models/commit.rb'
+ - 'app/models/commit_collection.rb'
+ - 'app/models/concerns/as_cte.rb'
+ - 'app/models/concerns/async_devise_email.rb'
+ - 'app/models/concerns/optionally_search.rb'
+ - 'app/models/concerns/presentable.rb'
+ - 'app/models/concerns/prometheus_adapter.rb'
+ - 'app/models/concerns/reactive_caching.rb'
+ - 'app/models/merge_request.rb'
+ - 'app/models/merge_request_context_commit.rb'
+ - 'app/models/merge_request_context_commit_diff_file.rb'
+ - 'app/models/network/commit.rb'
+ - 'app/presenters/gitlab/blame_presenter.rb'
+ - 'app/services/ci/prometheus_metrics/observe_histograms_service.rb'
+ - 'app/services/concerns/rate_limited_service.rb'
+ - 'app/services/issuable_base_service.rb'
+ - 'app/services/members/creator_service.rb'
+ - 'app/services/notification_recipients/build_service.rb'
+ - 'app/services/notification_service.rb'
+ - 'app/services/users/update_service.rb'
+ - 'app/workers/authorized_keys_worker.rb'
+ - 'app/workers/concerns/limited_capacity/worker.rb'
+ - 'app/workers/concerns/reactive_cacheable_worker.rb'
+ - 'app/workers/concerns/reenqueuer.rb'
+ - 'app/workers/gitlab/github_import/refresh_import_jid_worker.rb'
+ - 'app/workers/pages_worker.rb'
+ - 'config/initializers/6_labkit_middleware.rb'
+ - 'config/initializers/active_record_table_definition.rb'
+ - 'config/initializers/postgresql_cte.rb'
+ - 'ee/app/controllers/groups/analytics/application_controller.rb'
+ - 'ee/app/helpers/ee/gitlab_routing_helper.rb'
+ - 'ee/app/helpers/ee/saml_providers_helper.rb'
+ - 'ee/app/models/ee/group.rb'
+ - 'ee/app/models/elastic/migration_record.rb'
+ - 'ee/app/models/license.rb'
+ - 'ee/app/services/search/reindexing_service.rb'
+ - 'ee/db/geo/migrate/20210504143244_add_verification_to_merge_request_diff_registry.rb'
+ - 'ee/lib/analytics/forecasting/holt_winters_optimizer.rb'
+ - 'ee/lib/analytics/merge_request_metrics_refresh.rb'
+ - 'ee/lib/ee/gitlab/url_builder.rb'
+ - 'ee/lib/elastic/latest/application_class_proxy.rb'
+ - 'ee/lib/elastic/latest/query_context.rb'
+ - 'ee/lib/elastic/latest/wiki_class_proxy.rb'
+ - 'ee/lib/gitlab/elastic/expr_name.rb'
+ - 'ee/lib/gitlab/geo/replicator.rb'
+ - 'ee/lib/gitlab/insights/reducers/base_reducer.rb'
+ - 'ee/spec/lib/ee/gitlab/background_migration/migrate_approver_to_approval_rules_spec.rb'
+ - 'ee/spec/lib/gitlab/auth/saml/membership_updater_spec.rb'
+ - 'ee/spec/lib/gitlab/status_page/storage/s3_client_spec.rb'
+ - 'ee/spec/models/protected_environment_spec.rb'
+ - 'ee/spec/requests/api/graphql/boards/epic_lists_query_spec.rb'
+ - 'ee/spec/services/status_page/publish_service_spec.rb'
+ - 'lib/api/helpers.rb'
+ - 'lib/api/helpers/caching.rb'
+ - 'lib/api/helpers/pagination.rb'
+ - 'lib/atlassian/jira_connect/jwt/asymmetric.rb'
+ - 'lib/error_tracking/sentry_client/issue.rb'
+ - 'lib/gitlab/auth/ldap/adapter.rb'
+ - 'lib/gitlab/auth/ldap/dn.rb'
+ - 'lib/gitlab/cache.rb'
+ - 'lib/gitlab/ci/parsers.rb'
+ - 'lib/gitlab/ci/pipeline/expression/token.rb'
+ - 'lib/gitlab/config/entry/configurable.rb'
+ - 'lib/gitlab/config/entry/simplifiable.rb'
+ - 'lib/gitlab/current_settings.rb'
+ - 'lib/gitlab/database/load_balancing/connection_proxy.rb'
+ - 'lib/gitlab/database/migration_helpers.rb'
+ - 'lib/gitlab/git/diff.rb'
+ - 'lib/gitlab/git/repository.rb'
+ - 'lib/gitlab/gitaly_client/storage_settings.rb'
+ - 'lib/gitlab/gitaly_client/with_feature_flag_actors.rb'
+ - 'lib/gitlab/github_import/client.rb'
+ - 'lib/gitlab/gon_helper.rb'
+ - 'lib/gitlab/graphql/authorize/authorize_resource.rb'
+ - 'lib/gitlab/graphql/mount_mutation.rb'
+ - 'lib/gitlab/import_export/attribute_cleaner.rb'
+ - 'lib/gitlab/import_export/base/relation_factory.rb'
+ - 'lib/gitlab/import_export/file_importer.rb'
+ - 'lib/gitlab/import_export/json/ndjson_reader.rb'
+ - 'lib/gitlab/import_export/json/ndjson_writer.rb'
+ - 'lib/gitlab/import_export/saver.rb'
+ - 'lib/gitlab/import_export/version_checker.rb'
+ - 'lib/gitlab/jira/http_client.rb'
+ - 'lib/gitlab/kubernetes/kubeconfig/template.rb'
+ - 'lib/gitlab/legacy_github_import/client.rb'
+ - 'lib/gitlab/memory/watchdog/configuration.rb'
+ - 'lib/gitlab/metrics/prometheus.rb'
+ - 'lib/gitlab/nav/top_nav_view_model_builder.rb'
+ - 'lib/gitlab/quick_actions/dsl.rb'
+ - 'lib/gitlab/rack_attack.rb'
+ - 'lib/gitlab/redis/multi_store.rb'
+ - 'lib/gitlab/repository_cache.rb'
+ - 'lib/gitlab/tracking.rb'
+ - 'lib/gitlab/url_blocker.rb'
+ - 'lib/gitlab/url_builder.rb'
+ - 'lib/gitlab/usage/metrics/query.rb'
+ - 'lib/gitlab_settings/settings.rb'
+ - 'lib/kramdown/parser/atlassian_document_format.rb'
+ - 'lib/uploaded_file.rb'
+ - 'metrics_server/metrics_server.rb'
+ - 'qa/qa/ee/resource/audit_events.rb'
+ - 'qa/qa/ee/runtime/path.rb'
+ - 'qa/qa/page/base.rb'
+ - 'qa/qa/page/view.rb'
+ - 'qa/qa/resource/api_fabricator.rb'
+ - 'qa/qa/resource/base.rb'
+ - 'qa/qa/resource/group_base.rb'
+ - 'qa/qa/resource/group_runner.rb'
+ - 'qa/qa/resource/project.rb'
+ - 'qa/qa/resource/project_runner.rb'
+ - 'qa/qa/resource/sandbox.rb'
+ - 'qa/qa/runtime/feature.rb'
+ - 'qa/qa/runtime/path.rb'
+ - 'qa/qa/runtime/release.rb'
+ - 'qa/qa/scenario/actable.rb'
+ - 'qa/qa/scenario/template.rb'
+ - 'qa/qa/specs/helpers/rspec.rb'
+ - 'qa/qa/support/matchers/have_text.rb'
+ - 'qa/qa/support/page/logging.rb'
+ - 'qa/qa/tools/delete_subgroups.rb'
+ - 'spec/features/projects/environments/environments_spec.rb'
+ - 'spec/graphql/features/authorization_spec.rb'
+ - 'spec/helpers/application_helper_spec.rb'
+ - 'spec/helpers/timeboxes_helper_spec.rb'
+ - 'spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb'
+ - 'spec/lib/gitlab/ci/status/bridge/factory_spec.rb'
+ - 'spec/lib/gitlab/database/load_balancing/load_balancer_spec.rb'
+ - 'spec/lib/gitlab/graphql/markdown_field_spec.rb'
+ - 'spec/lib/gitlab/import_export/group/tree_saver_spec.rb'
+ - 'spec/lib/gitlab/pagination/offset_pagination_spec.rb'
+ - 'spec/models/concerns/cache_markdown_field_spec.rb'
+ - 'spec/requests/api/go_proxy_spec.rb'
+ - 'spec/requests/api/graphql/project/error_tracking/sentry_errors_request_spec.rb'
+ - 'spec/services/ci/expire_pipeline_cache_service_spec.rb'
+ - 'spec/services/ci/retry_pipeline_service_spec.rb'
+ - 'spec/support/factory_bot.rb'
+ - 'spec/support/helpers/api_helpers.rb'
+ - 'spec/support/helpers/database/trigger_helpers.rb'
+ - 'spec/support/helpers/fast_rails_root.rb'
+ - 'spec/support/helpers/features/dom_helpers.rb'
+ - 'spec/support/helpers/git_http_helpers.rb'
+ - 'spec/support/helpers/graphql_helpers.rb'
+ - 'spec/support/helpers/harbor_helper.rb'
+ - 'spec/support/helpers/login_helpers.rb'
+ - 'spec/support/helpers/merge_request_diff_helpers.rb'
+ - 'spec/support/helpers/next_instance_of.rb'
+ - 'spec/support/helpers/reactive_caching_helpers.rb'
+ - 'spec/support/shared_examples/features/2fa_shared_examples.rb'
diff --git a/.rubocop_todo/style/block_delimiters.yml b/.rubocop_todo/style/block_delimiters.yml
new file mode 100644
index 00000000000..8d7d98ec014
--- /dev/null
+++ b/.rubocop_todo/style/block_delimiters.yml
@@ -0,0 +1,69 @@
+---
+# Cop supports --autocorrect.
+Style/BlockDelimiters:
+ Details: grace period
+ Exclude:
+ - 'ee/spec/finders/security/related_pipelines_finder_spec.rb'
+ - 'ee/spec/finders/security/vulnerability_reads_finder_spec.rb'
+ - 'ee/spec/helpers/compliance_management/compliance_framework/group_settings_helper_spec.rb'
+ - 'ee/spec/helpers/ee/labels_helper_spec.rb'
+ - 'ee/spec/helpers/ee/projects/security/api_fuzzing_configuration_helper_spec.rb'
+ - 'ee/spec/helpers/ee/projects/security/dast_configuration_helper_spec.rb'
+ - 'ee/spec/helpers/ee/projects/security/sast_configuration_helper_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/observability_spec.rb'
+ - 'ee/spec/lib/gitlab/auth/smartcard/san_extension_spec.rb'
+ - 'ee/spec/lib/gitlab/package_metadata/connector/base_data_file_spec.rb'
+ - 'ee/spec/lib/gitlab/package_metadata/connector/gcp_spec.rb'
+ - 'ee/spec/lib/gitlab/package_metadata/connector/offline_spec.rb'
+ - 'ee/spec/models/analytics/cycle_analytics/value_stream_setting_spec.rb'
+ - 'ee/spec/models/audit_events/streaming/instance_event_type_filter_spec.rb'
+ - 'ee/spec/models/compliance_management/framework_spec.rb'
+ - 'ee/spec/models/ee/design_management/repository_spec.rb'
+ - 'ee/spec/models/ee/projects/wiki_repository_spec.rb'
+ - 'ee/spec/models/geo/design_management_repository_state_spec.rb'
+ - 'ee/spec/models/geo/wiki_repository_state_spec.rb'
+ - 'ee/spec/models/merge_request_spec.rb'
+ - 'ee/spec/models/namespace_setting_spec.rb'
+ - 'ee/spec/models/protected_environments/deploy_access_level_spec.rb'
+ - 'ee/spec/models/security/finding_spec.rb'
+ - 'ee/spec/models/vulnerabilities/feedback_spec.rb'
+ - 'ee/spec/services/ee/personal_access_tokens/create_service_spec.rb'
+ - 'ee/spec/services/gitlab_subscriptions/reconciliations/calculate_seat_count_data_service_spec.rb'
+ - 'ee/spec/services/package_metadata/advisory_data_object_spec.rb'
+ - 'ee/spec/services/package_metadata/compressed_package_data_object_spec.rb'
+ - 'ee/spec/services/package_metadata/data_object_fabricator_spec.rb'
+ - 'ee/spec/services/security/scanned_resources_counting_service_spec.rb'
+ - 'ee/spec/services/security/scanned_resources_service_spec.rb'
+ - 'ee/spec/services/security/security_orchestration_policies/ci_configuration_service_spec.rb'
+ - 'ee/spec/services/security/vulnerability_counting_service_spec.rb'
+ - 'spec/helpers/abuse_reports_helper_spec.rb'
+ - 'spec/helpers/admin/user_actions_helper_spec.rb'
+ - 'spec/helpers/merge_requests_helper_spec.rb'
+ - 'spec/helpers/projects/cluster_agents_helper_spec.rb'
+ - 'spec/lib/gitlab/ci/build/rules_spec.rb'
+ - 'spec/lib/gitlab/ci/config/external/file/artifact_spec.rb'
+ - 'spec/lib/gitlab/ci/config/external/file/base_spec.rb'
+ - 'spec/lib/gitlab/ci/config/external/file/local_spec.rb'
+ - 'spec/lib/gitlab/ci/config/external/file/project_spec.rb'
+ - 'spec/lib/gitlab/ci/config/external/file/remote_spec.rb'
+ - 'spec/lib/gitlab/ci/config/external/file/template_spec.rb'
+ - 'spec/lib/gitlab/data_builder/build_spec.rb'
+ - 'spec/lib/gitlab/git/wraps_gitaly_errors_spec.rb'
+ - 'spec/lib/release_highlights/validator/entry_spec.rb'
+ - 'spec/models/abuse_report_spec.rb'
+ - 'spec/models/ci/group_variable_spec.rb'
+ - 'spec/models/container_registry/protection/rule_spec.rb'
+ - 'spec/models/environment_status_spec.rb'
+ - 'spec/models/hooks/web_hook_spec.rb'
+ - 'spec/models/incident_management/timeline_event_tag_spec.rb'
+ - 'spec/models/packages/npm/metadatum_spec.rb'
+ - 'spec/models/packages/protection/rule_spec.rb'
+ - 'spec/models/packages/pypi/metadatum_spec.rb'
+ - 'spec/models/users/in_product_marketing_email_spec.rb'
+ - 'spec/models/users/phone_number_validation_spec.rb'
+ - 'spec/models/users/project_callout_spec.rb'
+ - 'spec/presenters/tree_entry_presenter_spec.rb'
+ - 'spec/requests/api/graphql/mutations/container_registry/protection/rule/create_spec.rb'
+ - 'spec/requests/jira_connect/subscriptions_controller_spec.rb'
+ - 'spec/services/packages/nuget/extract_remote_metadata_file_service_spec.rb'
+ - 'spec/services/packages/protection/delete_rule_service_spec.rb'
diff --git a/.rubocop_todo/style/class_and_module_children.yml b/.rubocop_todo/style/class_and_module_children.yml
index 80e5b613fac..89b20f231f9 100644
--- a/.rubocop_todo/style/class_and_module_children.yml
+++ b/.rubocop_todo/style/class_and_module_children.yml
@@ -41,7 +41,6 @@ Style/ClassAndModuleChildren:
- 'app/controllers/clusters/base_controller.rb'
- 'app/controllers/clusters/clusters_controller.rb'
- 'app/controllers/concerns/integrations/actions.rb'
- - 'app/controllers/concerns/metrics/dashboard/prometheus_api_proxy.rb'
- 'app/controllers/concerns/snippets/blobs_actions.rb'
- 'app/controllers/concerns/snippets/send_blob.rb'
- 'app/controllers/concerns/spammable_actions/akismet_mark_as_spam_action.rb'
@@ -84,7 +83,6 @@ Style/ClassAndModuleChildren:
- 'app/controllers/import/fogbugz_controller.rb'
- 'app/controllers/import/gitea_controller.rb'
- 'app/controllers/import/github_controller.rb'
- - 'app/controllers/import/gitlab_controller.rb'
- 'app/controllers/import/gitlab_groups_controller.rb'
- 'app/controllers/import/gitlab_projects_controller.rb'
- 'app/controllers/import/history_controller.rb'
@@ -101,7 +99,6 @@ Style/ClassAndModuleChildren:
- 'app/controllers/oauth/applications_controller.rb'
- 'app/controllers/oauth/authorizations_controller.rb'
- 'app/controllers/oauth/authorized_applications_controller.rb'
- - 'app/controllers/oauth/jira_dvcs/authorizations_controller.rb'
- 'app/controllers/oauth/token_info_controller.rb'
- 'app/controllers/oauth/tokens_controller.rb'
- 'app/controllers/profiles/accounts_controller.rb'
@@ -150,8 +147,6 @@ Style/ClassAndModuleChildren:
- 'app/controllers/projects/deployments_controller.rb'
- 'app/controllers/projects/design_management/designs_controller.rb'
- 'app/controllers/projects/discussions_controller.rb'
- - 'app/controllers/projects/environments/prometheus_api_controller.rb'
- - 'app/controllers/projects/environments/sample_metrics_controller.rb'
- 'app/controllers/projects/environments_controller.rb'
- 'app/controllers/projects/error_tracking/base_controller.rb'
- 'app/controllers/projects/error_tracking_controller.rb'
@@ -165,7 +160,6 @@ Style/ClassAndModuleChildren:
- 'app/controllers/projects/google_cloud/gcp_regions_controller.rb'
- 'app/controllers/projects/google_cloud/revoke_oauth_controller.rb'
- 'app/controllers/projects/google_cloud/service_accounts_controller.rb'
- - 'app/controllers/projects/grafana_api_controller.rb'
- 'app/controllers/projects/graphs_controller.rb'
- 'app/controllers/projects/group_links_controller.rb'
- 'app/controllers/projects/hook_logs_controller.rb'
@@ -342,7 +336,6 @@ Style/ClassAndModuleChildren:
- 'app/workers/merge_requests/delete_source_branch_worker.rb'
- 'app/workers/merge_requests/handle_assignees_change_worker.rb'
- 'app/workers/merge_requests/resolve_todos_worker.rb'
- - 'config/initializers/http_hostname_override.rb'
- 'config/initializers/httpclient_patch.rb'
- 'config/initializers/omniauth.rb'
- 'config/initializers/postgres_cte_as_materialized.rb'
@@ -379,7 +372,6 @@ Style/ClassAndModuleChildren:
- 'ee/app/controllers/groups/audit_events_controller.rb'
- 'ee/app/controllers/groups/billings_controller.rb'
- 'ee/app/controllers/groups/bulk_update_controller.rb'
- - 'ee/app/controllers/groups/compliance_frameworks_controller.rb'
- 'ee/app/controllers/groups/contribution_analytics_controller.rb'
- 'ee/app/controllers/groups/epic_boards_controller.rb'
- 'ee/app/controllers/groups/epic_issues_controller.rb'
@@ -392,7 +384,6 @@ Style/ClassAndModuleChildren:
- 'ee/app/controllers/groups/iteration_cadences_controller.rb'
- 'ee/app/controllers/groups/iterations_controller.rb'
- 'ee/app/controllers/groups/ldap_group_links_controller.rb'
- - 'ee/app/controllers/groups/ldap_settings_controller.rb'
- 'ee/app/controllers/groups/ldaps_controller.rb'
- 'ee/app/controllers/groups/merge_requests_controller.rb'
- 'ee/app/controllers/groups/omniauth_callbacks_controller.rb'
@@ -446,7 +437,6 @@ Style/ClassAndModuleChildren:
- 'ee/app/models/analytics/language_trend/repository_language.rb'
- 'ee/app/models/concerns/geo/replicable_registry.rb'
- 'ee/app/models/concerns/geo/selective_sync.rb'
- - 'ee/app/models/concerns/geo/syncable.rb'
- 'ee/app/models/dast/profile_schedule.rb'
- 'ee/app/models/ee/ci/job_artifact.rb'
- 'ee/app/models/elastic/reindexing_slice.rb'
@@ -464,7 +454,6 @@ Style/ClassAndModuleChildren:
- 'ee/app/models/geo/merge_request_diff_registry.rb'
- 'ee/app/models/geo/package_file_registry.rb'
- 'ee/app/models/geo/pages_deployment_registry.rb'
- - 'ee/app/models/geo/project_registry.rb'
- 'ee/app/models/geo/push_user.rb'
- 'ee/app/models/geo/secondary_usage_data.rb'
- 'ee/app/models/geo/snippet_repository_registry.rb'
@@ -483,7 +472,6 @@ Style/ClassAndModuleChildren:
- 'ee/app/serializers/vulnerabilities/response_entity.rb'
- 'ee/app/serializers/vulnerabilities/scanner_entity.rb'
- 'ee/app/services/concerns/epics/related_epic_links/usage_data_helper.rb'
- - 'ee/app/services/ee/projects/after_rename_service.rb'
- 'ee/app/services/ee/projects/disable_deploy_key_service.rb'
- 'ee/app/services/ee/projects/enable_deploy_key_service.rb'
- 'ee/db/fixtures/development/20_burndown.rb'
diff --git a/.rubocop_todo/style/empty_else.yml b/.rubocop_todo/style/empty_else.yml
index e2074f4f2ef..c0debdb2625 100644
--- a/.rubocop_todo/style/empty_else.yml
+++ b/.rubocop_todo/style/empty_else.yml
@@ -8,7 +8,6 @@ Style/EmptyElse:
- 'app/graphql/resolvers/group_milestones_resolver.rb'
- 'app/graphql/types/ci/detailed_status_type.rb'
- 'app/models/legacy_diff_discussion.rb'
- - 'app/models/performance_monitoring/prometheus_dashboard.rb'
- 'app/models/resource_state_event.rb'
- 'app/models/resource_timebox_event.rb'
- 'app/services/award_emojis/add_service.rb'
diff --git a/.rubocop_todo/style/empty_method.yml b/.rubocop_todo/style/empty_method.yml
index 094abdded79..ff246e48364 100644
--- a/.rubocop_todo/style/empty_method.yml
+++ b/.rubocop_todo/style/empty_method.yml
@@ -65,8 +65,6 @@ Style/EmptyMethod:
- 'app/services/issuable_base_service.rb'
- 'app/services/projects/transfer_service.rb'
- 'app/workers/namespaces/root_statistics_worker.rb'
- - 'db/post_migrate/20220324032250_migrate_shimo_confluence_service_category.rb'
- - 'db/post_migrate/20220412143552_consume_remaining_encrypt_integration_property_jobs.rb'
- 'db/post_migrate/20220425121435_backfill_integrations_enable_ssl_verification.rb'
- 'db/post_migrate/20220524074947_finalize_backfill_null_note_discussion_ids.rb'
- 'ee/app/controllers/admin/emails_controller.rb'
@@ -76,7 +74,6 @@ Style/EmptyMethod:
- 'ee/app/controllers/groups/analytics/ci_cd_analytics_controller.rb'
- 'ee/app/controllers/groups/analytics/cycle_analytics_controller.rb'
- 'ee/app/controllers/groups/analytics/devops_adoption_controller.rb'
- - 'ee/app/controllers/groups/compliance_frameworks_controller.rb'
- 'ee/app/controllers/groups/ldap_group_links_controller.rb'
- 'ee/app/controllers/groups/settings/reporting_controller.rb'
- 'ee/app/controllers/projects/analytics/code_reviews_controller.rb'
@@ -98,7 +95,6 @@ Style/EmptyMethod:
- 'lib/api/helpers/projects_helpers.rb'
- 'lib/api/projects_relation_builder.rb'
- 'lib/backup/task.rb'
- - 'lib/banzai/filter/inline_embeds_filter.rb'
- 'lib/gitlab/alert_management/payload/base.rb'
- 'lib/gitlab/background_migration/backfill_iteration_cadence_id_for_boards.rb'
- 'lib/gitlab/background_migration/create_security_setting.rb'
diff --git a/.rubocop_todo/style/explicit_block_argument.yml b/.rubocop_todo/style/explicit_block_argument.yml
index 44d28967e66..48be875ab5a 100644
--- a/.rubocop_todo/style/explicit_block_argument.yml
+++ b/.rubocop_todo/style/explicit_block_argument.yml
@@ -45,7 +45,6 @@ Style/ExplicitBlockArgument:
- 'lib/gitlab/database/reindexing/reindex_concurrently.rb'
- 'lib/gitlab/git/changes.rb'
- 'lib/gitlab/gitaly_client/list_blobs_adapter.rb'
- - 'lib/gitlab/gitaly_client/namespace_service.rb'
- 'lib/gitlab/gitaly_client/ref_service.rb'
- 'lib/gitlab/gitaly_client/storage_settings.rb'
- 'lib/gitlab/github_import/client.rb'
@@ -58,8 +57,6 @@ Style/ExplicitBlockArgument:
- 'lib/gitlab/import_export/project/import_task.rb'
- 'lib/gitlab/import_export/remote_stream_upload.rb'
- 'lib/gitlab/issuable/clone/copy_resource_events_service.rb'
- - 'lib/gitlab/metrics/dashboard/cache.rb'
- - 'lib/gitlab/metrics/dashboard/stages/base_stage.rb'
- 'lib/gitlab/profiler.rb'
- 'lib/gitlab/redis/wrapper.rb'
- 'lib/gitlab/reference_counter.rb'
diff --git a/.rubocop_todo/style/format_string.yml b/.rubocop_todo/style/format_string.yml
index 7579229871a..3e89b2ca7ea 100644
--- a/.rubocop_todo/style/format_string.yml
+++ b/.rubocop_todo/style/format_string.yml
@@ -21,7 +21,6 @@ Style/FormatString:
- 'app/controllers/projects/google_cloud/service_accounts_controller.rb'
- 'app/controllers/projects/issues_controller.rb'
- 'app/controllers/projects/merge_requests_controller.rb'
- - 'app/controllers/projects/performance_monitoring/dashboards_controller.rb'
- 'app/controllers/projects/pipeline_schedules_controller.rb'
- 'app/controllers/projects/settings/ci_cd_controller.rb'
- 'app/controllers/projects_controller.rb'
@@ -130,9 +129,6 @@ Style/FormatString:
- 'app/services/issues/set_crm_contacts_service.rb'
- 'app/services/jira/requests/base.rb'
- 'app/services/lfs/unlock_file_service.rb'
- - 'app/services/metrics/dashboard/clone_dashboard_service.rb'
- - 'app/services/metrics/dashboard/transient_embed_service.rb'
- - 'app/services/metrics/dashboard/update_dashboard_service.rb'
- 'app/services/milestones/promote_service.rb'
- 'app/services/personal_access_tokens/revoke_service.rb'
- 'app/services/projects/cleanup_service.rb'
@@ -240,7 +236,6 @@ Style/FormatString:
- 'lib/api/helpers/packages/conan/api_helpers.rb'
- 'lib/bulk_imports/network_error.rb'
- 'lib/bulk_imports/users_mapper.rb'
- - 'lib/gitlab/bitbucket_server_import/importer.rb'
- 'lib/gitlab/checks/push_file_count_check.rb'
- 'lib/gitlab/ci/ansi2json/line.rb'
- 'lib/gitlab/ci/badge/coverage/template.rb'
@@ -248,7 +243,6 @@ Style/FormatString:
- 'lib/gitlab/ci/parsers/sbom/cyclonedx.rb'
- 'lib/gitlab/ci/status/build/waiting_for_approval.rb'
- 'lib/gitlab/config_checker/external_database_checker.rb'
- - 'lib/gitlab/config_checker/puma_rugged_checker.rb'
- 'lib/gitlab/console.rb'
- 'lib/gitlab/database/async_indexes/index_creator.rb'
- 'lib/gitlab/database/background_migration/batched_migration.rb'
@@ -257,24 +251,16 @@ Style/FormatString:
- 'lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb'
- 'lib/gitlab/database/postgres_hll/batch_distinct_counter.rb'
- 'lib/gitlab/database/reindexing/reindex_concurrently.rb'
- - 'lib/gitlab/database_importers/instance_administrators/create_group.rb'
- 'lib/gitlab/exceptions_app.rb'
- - 'lib/gitlab/github_import/importer/pull_request_merged_by_importer.rb'
- 'lib/gitlab/github_import/importer/single_endpoint_issue_events_importer.rb'
- 'lib/gitlab/github_import/issuable_finder.rb'
- - 'lib/gitlab/github_import/label_finder.rb'
- - 'lib/gitlab/github_import/milestone_finder.rb'
- 'lib/gitlab/github_import/object_counter.rb'
- 'lib/gitlab/github_import/page_counter.rb'
- - 'lib/gitlab/github_import/parallel_scheduling.rb'
- 'lib/gitlab/import_export/base/relation_factory.rb'
- 'lib/gitlab/import_export/error.rb'
- 'lib/gitlab/import_export/snippet_repo_restorer.rb'
- 'lib/gitlab/jira_import.rb'
- 'lib/gitlab/log_timestamp_formatter.rb'
- - 'lib/gitlab/metrics/dashboard/errors.rb'
- - 'lib/gitlab/metrics/dashboard/validator/errors.rb'
- - 'lib/gitlab/pages/cache_control.rb'
- 'lib/gitlab/quick_actions/command_definition.rb'
- 'lib/gitlab/quick_actions/commit_actions.rb'
- 'lib/gitlab/quick_actions/issuable_actions.rb'
@@ -282,7 +268,6 @@ Style/FormatString:
- 'lib/gitlab/quick_actions/issue_and_merge_request_actions.rb'
- 'lib/gitlab/quick_actions/merge_request_actions.rb'
- 'lib/gitlab/quick_actions/relate_actions.rb'
- - 'lib/gitlab/version_info.rb'
- 'lib/peek/views/detailed_view.rb'
- 'lib/tasks/test.rake'
- 'spec/controllers/graphql_controller_spec.rb'
diff --git a/.rubocop_todo/style/guard_clause.yml b/.rubocop_todo/style/guard_clause.yml
index 737d3d6f5a1..74051fa5944 100644
--- a/.rubocop_todo/style/guard_clause.yml
+++ b/.rubocop_todo/style/guard_clause.yml
@@ -1,6 +1,7 @@
---
# Cop supports --autocorrect.
Style/GuardClause:
+ Details: grace period
Exclude:
- 'app/controllers/admin/users_controller.rb'
- 'app/controllers/application_controller.rb'
@@ -13,11 +14,9 @@ Style/GuardClause:
- 'app/controllers/concerns/enforces_admin_authentication.rb'
- 'app/controllers/concerns/enforces_two_factor_authentication.rb'
- 'app/controllers/concerns/impersonation.rb'
- - 'app/controllers/concerns/issuable_collections.rb'
- 'app/controllers/groups/application_controller.rb'
- 'app/controllers/groups_controller.rb'
- 'app/controllers/import/gitea_controller.rb'
- - 'app/controllers/import/gitlab_controller.rb'
- 'app/controllers/import/manifest_controller.rb'
- 'app/controllers/omniauth_callbacks_controller.rb'
- 'app/controllers/passwords_controller.rb'
@@ -56,7 +55,6 @@ Style/GuardClause:
- 'app/graphql/resolvers/blobs_resolver.rb'
- 'app/graphql/resolvers/board_list_issues_resolver.rb'
- 'app/graphql/resolvers/concerns/board_item_filterable.rb'
- - 'app/graphql/resolvers/concerns/time_frame_arguments.rb'
- 'app/graphql/resolvers/projects/jira_projects_resolver.rb'
- 'app/graphql/types/ci/job_type.rb'
- 'app/graphql/types/permission_types/base_permission_type.rb'
@@ -75,7 +73,6 @@ Style/GuardClause:
- 'app/models/appearance.rb'
- 'app/models/application_setting.rb'
- 'app/models/bulk_imports/entity.rb'
- - 'app/models/ci/build.rb'
- 'app/models/ci/build_trace.rb'
- 'app/models/ci/job_artifact.rb'
- 'app/models/ci/job_token/project_scope_link.rb'
@@ -84,10 +81,12 @@ Style/GuardClause:
- 'app/models/clusters/cluster.rb'
- 'app/models/clusters/platforms/kubernetes.rb'
- 'app/models/commit_range.rb'
+ - 'app/models/concerns/atomic_internal_id.rb'
- 'app/models/concerns/avatarable.rb'
- 'app/models/concerns/bulk_insert_safe.rb'
- 'app/models/concerns/cache_markdown_field.rb'
- 'app/models/concerns/cacheable_attributes.rb'
+ - 'app/models/concerns/cascading_namespace_setting_attribute.rb'
- 'app/models/concerns/deprecated_assignee.rb'
- 'app/models/concerns/has_wiki_page_meta_attributes.rb'
- 'app/models/concerns/issuable_link.rb'
@@ -135,14 +134,12 @@ Style/GuardClause:
- 'app/models/project_import_state.rb'
- 'app/models/project_label.rb'
- 'app/models/project_setting.rb'
- - 'app/models/protected_branch/push_access_level.rb'
- 'app/models/repository.rb'
- 'app/models/sent_notification.rb'
- 'app/models/sentry_issue.rb'
- 'app/models/service_desk_setting.rb'
- 'app/models/snippet_input_action.rb'
- 'app/models/user.rb'
- - 'app/models/users/in_product_marketing_email.rb'
- 'app/models/work_item.rb'
- 'app/models/work_items/parent_link.rb'
- 'app/presenters/ci/pipeline_presenter.rb'
@@ -172,8 +169,6 @@ Style/GuardClause:
- 'app/services/files/delete_service.rb'
- 'app/services/files/multi_service.rb'
- 'app/services/files/update_service.rb'
- - 'app/services/git/branch_hooks_service.rb'
- - 'app/services/groups/group_links/update_service.rb'
- 'app/services/groups/import_export/export_service.rb'
- 'app/services/groups/transfer_service.rb'
- 'app/services/groups/update_service.rb'
@@ -190,18 +185,15 @@ Style/GuardClause:
- 'app/services/merge_requests/add_spent_time_service.rb'
- 'app/services/merge_requests/base_service.rb'
- 'app/services/merge_requests/build_service.rb'
- - 'app/services/merge_requests/merge_base_service.rb'
- 'app/services/merge_requests/merge_service.rb'
- 'app/services/merge_requests/mergeability_check_service.rb'
- 'app/services/merge_requests/push_options_handler_service.rb'
- 'app/services/merge_requests/refresh_service.rb'
- - 'app/services/metrics/dashboard/base_service.rb'
- 'app/services/namespace_settings/update_service.rb'
- 'app/services/notes/create_service.rb'
- 'app/services/notes/post_process_service.rb'
- 'app/services/notification_recipients/builder/default.rb'
- 'app/services/notification_service.rb'
- - 'app/services/packages/create_event_service.rb'
- 'app/services/packages/create_package_service.rb'
- 'app/services/packages/nuget/search_service.rb'
- 'app/services/post_receive_service.rb'
@@ -311,8 +303,6 @@ Style/GuardClause:
- 'ee/app/models/ee/project_group_link.rb'
- 'ee/app/models/ee/project_member.rb'
- 'ee/app/models/ee/user.rb'
- - 'ee/app/models/elasticsearch_indexed_project.rb'
- - 'ee/app/models/epic/related_epic_link.rb'
- 'ee/app/models/epic_issue.rb'
- 'ee/app/models/geo_node.rb'
- 'ee/app/models/geo_node_status.rb'
@@ -323,7 +313,6 @@ Style/GuardClause:
- 'ee/app/models/namespace_limit.rb'
- 'ee/app/models/preloaders/environments/protected_environment_preloader.rb'
- 'ee/app/models/protected_environment.rb'
- - 'ee/app/models/protected_environments/deploy_access_level.rb'
- 'ee/app/models/users_security_dashboard_project.rb'
- 'ee/app/models/vulnerabilities/feedback.rb'
- 'ee/app/presenters/ee/merge_request_presenter.rb'
@@ -355,20 +344,16 @@ Style/GuardClause:
- 'ee/app/services/ee/projects/gitlab_projects_import_service.rb'
- 'ee/app/services/ee/projects/update_service.rb'
- 'ee/app/services/ee/protected_branches/loggable.rb'
- - 'ee/app/services/ee/wiki_pages/base_service.rb'
- 'ee/app/services/epics/close_service.rb'
- 'ee/app/services/epics/create_service.rb'
- 'ee/app/services/epics/reopen_service.rb'
- 'ee/app/services/epics/tree_reorder_service.rb'
- 'ee/app/services/epics/update_service.rb'
- - 'ee/app/services/geo/framework_repository_sync_service.rb'
- 'ee/app/services/geo/metrics_update_service.rb'
- - 'ee/app/services/geo/repository_base_sync_service.rb'
- 'ee/app/services/group_saml/group_managed_accounts/transfer_membership_service.rb'
- 'ee/app/services/groups/update_repository_storage_service.rb'
- 'ee/app/services/incident_management/oncall_rotations/remove_participant_service.rb'
- 'ee/app/services/iterations/delete_service.rb'
- - 'ee/app/services/merge_trains/check_status_service.rb'
- 'ee/app/services/merge_trains/refresh_merge_request_service.rb'
- 'ee/app/services/projects/update_mirror_service.rb'
- 'ee/app/services/security/override_uuids_service.rb'
@@ -422,7 +407,6 @@ Style/GuardClause:
- 'ee/spec/features/billings/billing_plans_spec.rb'
- 'ee/spec/lib/ee/gitlab/background_migration/migrate_approver_to_approval_rules_spec.rb'
- 'ee/spec/support/ci/minutes_helpers.rb'
- - 'haml_lint/linter/documentation_links.rb'
- 'lib/api/commits.rb'
- 'lib/api/feature_flags.rb'
- 'lib/api/helpers.rb'
@@ -436,8 +420,6 @@ Style/GuardClause:
- 'lib/banzai/filter/gollum_tags_filter.rb'
- 'lib/banzai/filter/references/merge_request_reference_filter.rb'
- 'lib/banzai/filter/wiki_link_filter/rewriter.rb'
- - 'lib/bulk_imports/clients/graphql.rb'
- - 'lib/bulk_imports/pipeline/runner.rb'
- 'lib/bulk_imports/projects/pipelines/project_pipeline.rb'
- 'lib/container_registry/client.rb'
- 'lib/feature/definition.rb'
@@ -449,7 +431,6 @@ Style/GuardClause:
- 'lib/gitlab/auth/unique_ips_limiter.rb'
- 'lib/gitlab/background_migration/fix_projects_without_project_feature.rb'
- 'lib/gitlab/bitbucket_import/importer.rb'
- - 'lib/gitlab/bitbucket_server_import/importer.rb'
- 'lib/gitlab/blob_helper.rb'
- 'lib/gitlab/cache/ci/project_pipeline_status.rb'
- 'lib/gitlab/changelog/config.rb'
@@ -476,7 +457,6 @@ Style/GuardClause:
- 'lib/gitlab/ci/pipeline/chain/validate/abilities.rb'
- 'lib/gitlab/ci/pipeline/chain/validate/repository.rb'
- 'lib/gitlab/ci/pipeline/expression/lexeme/base.rb'
- - 'lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb'
- 'lib/gitlab/ci/reports/codequality_reports_comparer.rb'
- 'lib/gitlab/ci/runner/backoff.rb'
- 'lib/gitlab/ci/runner_upgrade_check.rb'
@@ -486,7 +466,6 @@ Style/GuardClause:
- 'lib/gitlab/ci/yaml_processor.rb'
- 'lib/gitlab/config/entry/validators.rb'
- 'lib/gitlab/daemon.rb'
- - 'lib/gitlab/database/background_migration/batch_optimizer.rb'
- 'lib/gitlab/database/background_migration/batched_migration_wrapper.rb'
- 'lib/gitlab/database/consistency_checker.rb'
- 'lib/gitlab/database/load_balancing/load_balancer.rb'
@@ -519,7 +498,6 @@ Style/GuardClause:
- 'lib/gitlab/github_import.rb'
- 'lib/gitlab/github_import/client.rb'
- 'lib/gitlab/github_import/importer/pull_request_importer.rb'
- - 'lib/gitlab/github_import/importer/pull_request_review_importer.rb'
- 'lib/gitlab/github_import/object_counter.rb'
- 'lib/gitlab/graphql/query_analyzers/ast/recursion_analyzer.rb'
- 'lib/gitlab/i18n/po_linter.rb'
@@ -541,10 +519,8 @@ Style/GuardClause:
- 'lib/gitlab/legacy_github_import/issuable_formatter.rb'
- 'lib/gitlab/marginalia.rb'
- 'lib/gitlab/metrics/samplers/ruby_sampler.rb'
- - 'lib/gitlab/metrics/subscribers/action_cable.rb'
- 'lib/gitlab/metrics/subscribers/active_record.rb'
- 'lib/gitlab/metrics/subscribers/external_http.rb'
- - 'lib/gitlab/metrics/subscribers/rails_cache.rb'
- 'lib/gitlab/metrics/web_transaction.rb'
- 'lib/gitlab/middleware/read_only/controller.rb'
- 'lib/gitlab/pages/deployment_update.rb'
@@ -557,7 +533,6 @@ Style/GuardClause:
- 'lib/gitlab/patch/global_id.rb'
- 'lib/gitlab/patch/sprockets_base_file_digest_key.rb'
- 'lib/gitlab/process_supervisor.rb'
- - 'lib/gitlab/prometheus/query_variables.rb'
- 'lib/gitlab/prometheus_client.rb'
- 'lib/gitlab/recaptcha.rb'
- 'lib/gitlab/sanitizers/exif.rb'
@@ -567,13 +542,10 @@ Style/GuardClause:
- 'lib/gitlab/serializer/pagination.rb'
- 'lib/gitlab/shell.rb'
- 'lib/gitlab/sidekiq_config/cli_methods.rb'
- - 'lib/gitlab/sidekiq_middleware/extra_done_log_metadata.rb'
- 'lib/gitlab/sidekiq_middleware/size_limiter/compressor.rb'
- 'lib/gitlab/sql/set_operator.rb'
- 'lib/gitlab/url_blocker.rb'
- - 'lib/gitlab/usage/metric_definition.rb'
- 'lib/gitlab/usage/metrics/instrumentations/count_bulk_imports_entities_metric.rb'
- - 'lib/gitlab/utils.rb'
- 'lib/gitlab/utils/override.rb'
- 'lib/gitlab/webpack/manifest.rb'
- 'lib/mattermost/session.rb'
@@ -596,7 +568,6 @@ Style/GuardClause:
- 'qa/qa/runtime/feature.rb'
- 'qa/qa/runtime/search.rb'
- 'qa/qa/service/cluster_provider/gcloud.rb'
- - 'qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb'
- 'qa/qa/specs/features/ee/browser_ui/3_create/repository/group_file_template_spec.rb'
- 'qa/qa/specs/helpers/feature_flag.rb'
- 'qa/qa/vendor/jenkins/job.rb'
@@ -616,7 +587,6 @@ Style/GuardClause:
- 'spec/lib/gitlab/database/migrations/background_migration_helpers_spec.rb'
- 'spec/services/issues/relative_position_rebalancing_service_spec.rb'
- 'spec/services/packages/maven/metadata/append_package_file_service_spec.rb'
- - 'spec/support/capybara.rb'
- 'spec/support/database/prevent_cross_joins.rb'
- 'spec/support/helpers/access_matchers_helpers.rb'
- 'spec/support/helpers/capybara_helpers.rb'
@@ -631,7 +601,6 @@ Style/GuardClause:
- 'spec/support/helpers/wait_helpers.rb'
- 'spec/support/import_export/export_file_helper.rb'
- 'spec/support/shared_examples/features/packages_shared_examples.rb'
- - 'spec/support/shared_examples/services/packages/debian/generate_distribution_shared_examples.rb'
- 'spec/tooling/lib/tooling/find_codeowners_spec.rb'
- 'spec/workers/container_expiration_policies/cleanup_container_repository_worker_spec.rb'
- 'tooling/lib/tooling/helm3_client.rb'
diff --git a/.rubocop_todo/style/hash_as_last_array_item.yml b/.rubocop_todo/style/hash_as_last_array_item.yml
index b2dceb48c1b..21399692bbe 100644
--- a/.rubocop_todo/style/hash_as_last_array_item.yml
+++ b/.rubocop_todo/style/hash_as_last_array_item.yml
@@ -10,7 +10,6 @@ Style/HashAsLastArrayItem:
- 'app/controllers/profiles_controller.rb'
- 'app/controllers/projects/feature_flags_controller.rb'
- 'app/controllers/projects/merge_requests/application_controller.rb'
- - 'app/controllers/projects/performance_monitoring/dashboards_controller.rb'
- 'app/controllers/projects/protected_branches_controller.rb'
- 'app/controllers/projects/settings/ci_cd_controller.rb'
- 'app/controllers/projects/settings/operations_controller.rb'
@@ -46,7 +45,6 @@ Style/HashAsLastArrayItem:
- 'spec/lib/gitlab/database/migration_helpers/v2_spec.rb'
- 'spec/requests/rack_attack_global_spec.rb'
- 'spec/services/git/branch_hooks_service_spec.rb'
- - 'spec/services/metrics/dashboard/panel_preview_service_spec.rb'
- 'spec/support/helpers/rack_attack_spec_helpers.rb'
- 'spec/workers/concerns/worker_attributes_spec.rb'
- 'spec/workers/merge_worker_spec.rb'
diff --git a/.rubocop_todo/style/hash_each_methods.yml b/.rubocop_todo/style/hash_each_methods.yml
index 53bde6fac69..97e2cb8645a 100644
--- a/.rubocop_todo/style/hash_each_methods.yml
+++ b/.rubocop_todo/style/hash_each_methods.yml
@@ -65,20 +65,16 @@ Style/HashEachMethods:
- 'spec/lib/gitlab/ci/status/build/failed_spec.rb'
- 'spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb'
- 'spec/models/ci/job_artifact_spec.rb'
- - 'spec/models/ci/resource_group_spec.rb'
- 'spec/models/clusters/cluster_spec.rb'
- - 'spec/models/concerns/has_user_type_spec.rb'
- 'spec/models/packages/package_spec.rb'
- 'spec/models/project_spec.rb'
- 'spec/models/user_spec.rb'
- 'spec/presenters/ci/pipeline_presenter_spec.rb'
- 'spec/presenters/commit_status_presenter_spec.rb'
- - 'spec/presenters/packages/npm/package_presenter_spec.rb'
- 'spec/services/system_notes/incident_service_spec.rb'
- 'spec/support/helpers/multipart_helpers.rb'
- 'spec/support/helpers/reactive_caching_helpers.rb'
- 'spec/support/import_export/project_tree_expectations.rb'
- 'spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb'
- 'spec/support/shared_examples/services/packages_shared_examples.rb'
- - 'spec/tasks/gitlab/packages/events_rake_spec.rb'
- 'tooling/graphql/docs/helper.rb'
diff --git a/.rubocop_todo/style/if_unless_modifier.yml b/.rubocop_todo/style/if_unless_modifier.yml
index 79dd4a7680f..c6a44782153 100644
--- a/.rubocop_todo/style/if_unless_modifier.yml
+++ b/.rubocop_todo/style/if_unless_modifier.yml
@@ -11,7 +11,6 @@ Style/IfUnlessModifier:
- 'app/controllers/groups_controller.rb'
- 'app/controllers/import/fogbugz_controller.rb'
- 'app/controllers/import/gitea_controller.rb'
- - 'app/controllers/import/gitlab_controller.rb'
- 'app/controllers/import/manifest_controller.rb'
- 'app/controllers/omniauth_callbacks_controller.rb'
- 'app/controllers/profiles/emails_controller.rb'
@@ -50,7 +49,6 @@ Style/IfUnlessModifier:
- 'app/finders/group_projects_finder.rb'
- 'app/finders/labels_finder.rb'
- 'app/finders/members_finder.rb'
- - 'app/finders/metrics/users_starred_dashboards_finder.rb'
- 'app/finders/notes_finder.rb'
- 'app/finders/packages/helm/packages_finder.rb'
- 'app/finders/personal_access_tokens_finder.rb'
@@ -272,8 +270,6 @@ Style/IfUnlessModifier:
- 'app/services/merge_requests/refresh_service.rb'
- 'app/services/merge_requests/squash_service.rb'
- 'app/services/merge_requests/update_service.rb'
- - 'app/services/metrics/dashboard/clone_dashboard_service.rb'
- - 'app/services/metrics/dashboard/update_dashboard_service.rb'
- 'app/services/milestones/close_service.rb'
- 'app/services/milestones/create_service.rb'
- 'app/services/milestones/promote_service.rb'
@@ -357,7 +353,6 @@ Style/IfUnlessModifier:
- 'app/workers/repository_fork_worker.rb'
- 'app/workers/repository_update_remote_mirror_worker.rb'
- 'config/application.rb'
- - 'config/initializers/01_active_record_database_tasks_configuration_flag.rb'
- 'config/initializers/01_secret_token.rb'
- 'config/initializers/0_inject_enterprise_edition_module.rb'
- 'config/initializers/1_settings.rb'
@@ -379,8 +374,6 @@ Style/IfUnlessModifier:
- 'config/routes.rb'
- 'danger/database/Dangerfile'
- 'danger/pipeline/Dangerfile'
- - 'danger/z_metadata/Dangerfile'
- - 'db/migrate/20220324175325_add_key_data_to_secure_files.rb'
- 'db/post_migrate/20220523171107_drop_deploy_tokens_token_column.rb'
- 'ee/app/components/namespaces/storage/limit_alert_component.rb'
- 'ee/app/controllers/admin/elasticsearch_controller.rb'
@@ -443,10 +436,8 @@ Style/IfUnlessModifier:
- 'ee/app/models/ee/milestone_release.rb'
- 'ee/app/models/ee/namespace.rb'
- 'ee/app/models/ee/project.rb'
- - 'ee/app/models/ee/project_feature.rb'
- 'ee/app/models/ee/project_team.rb'
- 'ee/app/models/ee/user.rb'
- - 'ee/app/models/geo/project_registry.rb'
- 'ee/app/models/geo/tracking_base.rb'
- 'ee/app/models/incident_management/escalation_rule.rb'
- 'ee/app/models/ip_restriction.rb'
@@ -518,7 +509,6 @@ Style/IfUnlessModifier:
- 'ee/app/services/external_status_checks/create_service.rb'
- 'ee/app/services/geo/file_registry_removal_service.rb'
- 'ee/app/services/geo/metrics_update_service.rb'
- - 'ee/app/services/geo/move_repository_service.rb'
- 'ee/app/services/geo/prune_event_log_service.rb'
- 'ee/app/services/gitlab_subscriptions/plan_upgrade_service.rb'
- 'ee/app/services/groups/memberships/export_service.rb'
@@ -544,7 +534,6 @@ Style/IfUnlessModifier:
- 'ee/app/services/start_pull_mirroring_service.rb'
- 'ee/app/services/system_notes/epics_service.rb'
- 'ee/app/services/timebox_report_service.rb'
- - 'ee/app/services/users/captcha_challenge_service.rb'
- 'ee/app/services/vulnerabilities/base_service.rb'
- 'ee/app/services/vulnerabilities/create_service.rb'
- 'ee/app/services/vulnerabilities/historical_statistics/adjustment_service.rb'
@@ -734,9 +723,7 @@ Style/IfUnlessModifier:
- 'lib/gitlab/auth/ldap/adapter.rb'
- 'lib/gitlab/auth/ldap/authentication.rb'
- 'lib/gitlab/authorized_keys.rb'
- - 'lib/gitlab/background_migration/fix_vulnerability_occurrences_with_hashes_as_raw_metadata.rb'
- 'lib/gitlab/background_migration/project_namespaces/backfill_project_namespaces.rb'
- - 'lib/gitlab/background_migration/reset_duplicate_ci_runners_token_encrypted_values_on_projects.rb'
- 'lib/gitlab/bullet/exclusions.rb'
- 'lib/gitlab/cache/ci/project_pipeline_status.rb'
- 'lib/gitlab/changelog/config.rb'
@@ -830,7 +817,6 @@ Style/IfUnlessModifier:
- 'lib/gitlab/git/merge_base.rb'
- 'lib/gitlab/git/push.rb'
- 'lib/gitlab/git/repository.rb'
- - 'lib/gitlab/git/rugged_impl/tree.rb'
- 'lib/gitlab/git_access.rb'
- 'lib/gitlab/git_access_project.rb'
- 'lib/gitlab/git_access_snippet.rb'
@@ -839,7 +825,6 @@ Style/IfUnlessModifier:
- 'lib/gitlab/gitaly_client/operation_service.rb'
- 'lib/gitlab/gitaly_client/repository_service.rb'
- 'lib/gitlab/github_import/client.rb'
- - 'lib/gitlab/github_import/importer/pull_request_review_importer.rb'
- 'lib/gitlab/github_import/representation/issue.rb'
- 'lib/gitlab/golang.rb'
- 'lib/gitlab/graphql/pagination/keyset/connection.rb'
@@ -870,11 +855,6 @@ Style/IfUnlessModifier:
- 'lib/gitlab/manifest_import/manifest.rb'
- 'lib/gitlab/marginalia.rb'
- 'lib/gitlab/markdown_cache/field_data.rb'
- - 'lib/gitlab/metrics/dashboard/finder.rb'
- - 'lib/gitlab/metrics/dashboard/importer.rb'
- - 'lib/gitlab/metrics/dashboard/stages/cluster_endpoint_inserter.rb'
- - 'lib/gitlab/metrics/dashboard/stages/metric_endpoint_inserter.rb'
- - 'lib/gitlab/metrics/dashboard/stages/variable_endpoint_inserter.rb'
- 'lib/gitlab/metrics/methods.rb'
- 'lib/gitlab/metrics/subscribers/rack_attack.rb'
- 'lib/gitlab/metrics/web_transaction.rb'
@@ -912,7 +892,6 @@ Style/IfUnlessModifier:
- 'lib/gitlab/url_blocker.rb'
- 'lib/gitlab/usage_data_counters/base_counter.rb'
- 'lib/gitlab/usage_data_counters/hll_redis_counter.rb'
- - 'lib/gitlab/utils.rb'
- 'lib/gitlab/utils/delegator_override.rb'
- 'lib/gitlab/utils/override.rb'
- 'lib/gitlab/view/presenter/delegated.rb'
@@ -955,7 +934,6 @@ Style/IfUnlessModifier:
- 'qa/qa/service/praefect_manager.rb'
- 'qa/qa/specs/features/browser_ui/3_create/merge_request/merge_when_pipeline_succeeds_spec.rb'
- 'qa/qa/specs/features/browser_ui/5_package/package_registry/nuget/nuget_group_level_spec.rb'
- - 'qa/qa/specs/features/ee/browser_ui/1_manage/group/group_ldap_sync_spec.rb'
- 'qa/qa/specs/helpers/context_selector.rb'
- 'qa/qa/specs/parallel_runner.rb'
- 'qa/qa/tools/delete_projects.rb'
@@ -1042,10 +1020,8 @@ Style/IfUnlessModifier:
- 'spec/support/helpers/stub_gitlab_calls.rb'
- 'spec/support/helpers/stubbed_feature.rb'
- 'spec/support/helpers/test_env.rb'
- - 'spec/support/http_io/http_io_helpers.rb'
- 'spec/support/import_export/project_tree_expectations.rb'
- 'spec/support/matchers/abort_matcher.rb'
- - 'spec/support/services/service_response_shared_examples.rb'
- 'spec/support/shared_contexts/requests/api/debian_repository_shared_context.rb'
- 'spec/support/shared_examples/controllers/issuables_list_metadata_shared_examples.rb'
- 'spec/support/shared_examples/features/discussion_comments_shared_example.rb'
diff --git a/.rubocop_todo/style/inline_disable_annotation.yml b/.rubocop_todo/style/inline_disable_annotation.yml
new file mode 100644
index 00000000000..136c5fe9a0c
--- /dev/null
+++ b/.rubocop_todo/style/inline_disable_annotation.yml
@@ -0,0 +1,3403 @@
+---
+Style/InlineDisableAnnotation:
+ Details: grace period
+ Exclude:
+ - 'Gemfile'
+ - 'Guardfile'
+ - 'app/channels/graphql_channel.rb'
+ - 'app/controllers/abuse_reports_controller.rb'
+ - 'app/controllers/acme_challenges_controller.rb'
+ - 'app/controllers/admin/application_settings_controller.rb'
+ - 'app/controllers/admin/background_jobs_controller.rb'
+ - 'app/controllers/admin/broadcast_messages_controller.rb'
+ - 'app/controllers/admin/dashboard_controller.rb'
+ - 'app/controllers/admin/dev_ops_report_controller.rb'
+ - 'app/controllers/admin/groups_controller.rb'
+ - 'app/controllers/admin/health_check_controller.rb'
+ - 'app/controllers/admin/identities_controller.rb'
+ - 'app/controllers/admin/impersonation_tokens_controller.rb'
+ - 'app/controllers/admin/keys_controller.rb'
+ - 'app/controllers/admin/plan_limits_controller.rb'
+ - 'app/controllers/admin/projects_controller.rb'
+ - 'app/controllers/admin/runners_controller.rb'
+ - 'app/controllers/admin/spam_logs_controller.rb'
+ - 'app/controllers/admin/system_info_controller.rb'
+ - 'app/controllers/admin/users_controller.rb'
+ - 'app/controllers/admin/version_check_controller.rb'
+ - 'app/controllers/chaos_controller.rb'
+ - 'app/controllers/concerns/access_tokens_actions.rb'
+ - 'app/controllers/concerns/analytics/cycle_analytics/value_stream_actions.rb'
+ - 'app/controllers/concerns/authenticates_with_two_factor.rb'
+ - 'app/controllers/concerns/boards_actions.rb'
+ - 'app/controllers/concerns/checks_collaboration.rb'
+ - 'app/controllers/concerns/creates_commit.rb'
+ - 'app/controllers/concerns/enforces_two_factor_authentication.rb'
+ - 'app/controllers/concerns/find_snippet.rb'
+ - 'app/controllers/concerns/group_tree.rb'
+ - 'app/controllers/concerns/import/github_oauth.rb'
+ - 'app/controllers/concerns/integrations/actions.rb'
+ - 'app/controllers/concerns/issuable_actions.rb'
+ - 'app/controllers/concerns/issuable_collections.rb'
+ - 'app/controllers/concerns/issuable_collections_action.rb'
+ - 'app/controllers/concerns/issues_calendar.rb'
+ - 'app/controllers/concerns/membership_actions.rb'
+ - 'app/controllers/concerns/milestone_actions.rb'
+ - 'app/controllers/concerns/notes_actions.rb'
+ - 'app/controllers/concerns/planning_hierarchy.rb'
+ - 'app/controllers/concerns/preferred_language_switcher.rb'
+ - 'app/controllers/concerns/preview_markdown.rb'
+ - 'app/controllers/concerns/registry/connection_errors_handler.rb'
+ - 'app/controllers/concerns/renders_commits.rb'
+ - 'app/controllers/concerns/renders_member_access.rb'
+ - 'app/controllers/concerns/renders_notes.rb'
+ - 'app/controllers/concerns/sends_blob.rb'
+ - 'app/controllers/concerns/skips_already_signed_in_message.rb'
+ - 'app/controllers/concerns/snippets_actions.rb'
+ - 'app/controllers/concerns/uploads_actions.rb'
+ - 'app/controllers/concerns/verifies_with_email.rb'
+ - 'app/controllers/concerns/web_hooks/hook_log_actions.rb'
+ - 'app/controllers/concerns/wiki_actions.rb'
+ - 'app/controllers/dashboard/projects_controller.rb'
+ - 'app/controllers/explore/projects_controller.rb'
+ - 'app/controllers/graphql_controller.rb'
+ - 'app/controllers/groups/autocomplete_sources_controller.rb'
+ - 'app/controllers/groups/labels_controller.rb'
+ - 'app/controllers/groups/milestones_controller.rb'
+ - 'app/controllers/groups_controller.rb'
+ - 'app/controllers/health_controller.rb'
+ - 'app/controllers/help_controller.rb'
+ - 'app/controllers/import/base_controller.rb'
+ - 'app/controllers/import/bitbucket_controller.rb'
+ - 'app/controllers/import/bitbucket_server_controller.rb'
+ - 'app/controllers/import/github_controller.rb'
+ - 'app/controllers/import/manifest_controller.rb'
+ - 'app/controllers/jira_connect/subscriptions_controller.rb'
+ - 'app/controllers/metrics_controller.rb'
+ - 'app/controllers/oauth/authorizations_controller.rb'
+ - 'app/controllers/passwords_controller.rb'
+ - 'app/controllers/profiles/accounts_controller.rb'
+ - 'app/controllers/profiles/notifications_controller.rb'
+ - 'app/controllers/profiles_controller.rb'
+ - 'app/controllers/projects/analytics/cycle_analytics/stages_controller.rb'
+ - 'app/controllers/projects/blob_controller.rb'
+ - 'app/controllers/projects/branches_controller.rb'
+ - 'app/controllers/projects/commit_controller.rb'
+ - 'app/controllers/projects/commits_controller.rb'
+ - 'app/controllers/projects/compare_controller.rb'
+ - 'app/controllers/projects/deployments_controller.rb'
+ - 'app/controllers/projects/design_management/designs/resized_image_controller.rb'
+ - 'app/controllers/projects/discussions_controller.rb'
+ - 'app/controllers/projects/environments_controller.rb'
+ - 'app/controllers/projects/forks_controller.rb'
+ - 'app/controllers/projects/incidents_controller.rb'
+ - 'app/controllers/projects/issue_links_controller.rb'
+ - 'app/controllers/projects/issues_controller.rb'
+ - 'app/controllers/projects/labels_controller.rb'
+ - 'app/controllers/projects/merge_requests/application_controller.rb'
+ - 'app/controllers/projects/merge_requests/creations_controller.rb'
+ - 'app/controllers/projects/merge_requests/diffs_controller.rb'
+ - 'app/controllers/projects/merge_requests/drafts_controller.rb'
+ - 'app/controllers/projects/merge_requests_controller.rb'
+ - 'app/controllers/projects/milestones_controller.rb'
+ - 'app/controllers/projects/pages_controller.rb'
+ - 'app/controllers/projects/pipeline_schedules_controller.rb'
+ - 'app/controllers/projects/pipelines_controller.rb'
+ - 'app/controllers/projects/settings/ci_cd_controller.rb'
+ - 'app/controllers/projects/settings/repository_controller.rb'
+ - 'app/controllers/projects/tags_controller.rb'
+ - 'app/controllers/projects/work_items_controller.rb'
+ - 'app/controllers/projects_controller.rb'
+ - 'app/controllers/pwa_controller.rb'
+ - 'app/controllers/registrations_controller.rb'
+ - 'app/controllers/repositories/lfs_storage_controller.rb'
+ - 'app/controllers/sandbox_controller.rb'
+ - 'app/controllers/snippets/notes_controller.rb'
+ - 'app/controllers/users/namespace_visits_controller.rb'
+ - 'app/controllers/users/unsubscribes_controller.rb'
+ - 'app/finders/admin/abuse_report_labels_finder.rb'
+ - 'app/finders/admin/plans_finder.rb'
+ - 'app/finders/admin/projects_finder.rb'
+ - 'app/finders/applications_finder.rb'
+ - 'app/finders/autocomplete/acts_as_taggable_on/tags_finder.rb'
+ - 'app/finders/autocomplete/group_users_finder.rb'
+ - 'app/finders/autocomplete/move_to_project_finder.rb'
+ - 'app/finders/autocomplete/routes_finder.rb'
+ - 'app/finders/autocomplete/users_finder.rb'
+ - 'app/finders/bulk_imports/entities_finder.rb'
+ - 'app/finders/ci/daily_build_group_report_results_finder.rb'
+ - 'app/finders/ci/pipeline_schedules_finder.rb'
+ - 'app/finders/ci/pipelines_finder.rb'
+ - 'app/finders/ci/pipelines_for_merge_request_finder.rb'
+ - 'app/finders/ci/runner_jobs_finder.rb'
+ - 'app/finders/ci/runners_finder.rb'
+ - 'app/finders/clusters/agents/authorizations/ci_access/finder.rb'
+ - 'app/finders/concerns/custom_attributes_filter.rb'
+ - 'app/finders/concerns/finder_methods.rb'
+ - 'app/finders/deployments_finder.rb'
+ - 'app/finders/environments/environments_by_deployments_finder.rb'
+ - 'app/finders/events_finder.rb'
+ - 'app/finders/fork_projects_finder.rb'
+ - 'app/finders/fork_targets_finder.rb'
+ - 'app/finders/group_descendants_finder.rb'
+ - 'app/finders/group_finder.rb'
+ - 'app/finders/group_members_finder.rb'
+ - 'app/finders/groups/accepting_group_transfers_finder.rb'
+ - 'app/finders/groups/accepting_project_creations_finder.rb'
+ - 'app/finders/groups/accepting_project_shares_finder.rb'
+ - 'app/finders/groups/base.rb'
+ - 'app/finders/groups/projects_requiring_authorizations_refresh/base.rb'
+ - 'app/finders/groups/user_groups_finder.rb'
+ - 'app/finders/groups_finder.rb'
+ - 'app/finders/issuable_finder.rb'
+ - 'app/finders/issuable_finder/params.rb'
+ - 'app/finders/issuables/crm_contact_filter.rb'
+ - 'app/finders/issuables/crm_organization_filter.rb'
+ - 'app/finders/issuables/label_filter.rb'
+ - 'app/finders/issues_finder.rb'
+ - 'app/finders/keys_finder.rb'
+ - 'app/finders/labels_finder.rb'
+ - 'app/finders/members_finder.rb'
+ - 'app/finders/merge_request_target_project_finder.rb'
+ - 'app/finders/merge_requests/by_approvals_finder.rb'
+ - 'app/finders/merge_requests_finder.rb'
+ - 'app/finders/milestones_finder.rb'
+ - 'app/finders/notes_finder.rb'
+ - 'app/finders/organizations/groups_finder.rb'
+ - 'app/finders/packages/build_infos_finder.rb'
+ - 'app/finders/projects/groups_finder.rb'
+ - 'app/finders/projects/members/effective_access_level_finder.rb'
+ - 'app/finders/projects/members/effective_access_level_per_user_finder.rb'
+ - 'app/finders/projects_finder.rb'
+ - 'app/finders/releases/group_releases_finder.rb'
+ - 'app/finders/releases_finder.rb'
+ - 'app/finders/repositories/branch_names_finder.rb'
+ - 'app/finders/resource_milestone_event_finder.rb'
+ - 'app/finders/resource_state_event_finder.rb'
+ - 'app/finders/template_finder.rb'
+ - 'app/finders/user_groups_counter.rb'
+ - 'app/finders/user_recent_events_finder.rb'
+ - 'app/finders/users_finder.rb'
+ - 'app/graphql/gitlab_schema.rb'
+ - 'app/graphql/graphql_triggers.rb'
+ - 'app/graphql/mutations/issues/update.rb'
+ - 'app/graphql/mutations/metrics/dashboard/annotations/delete.rb'
+ - 'app/graphql/mutations/projects/sync_fork.rb'
+ - 'app/graphql/mutations/releases/create.rb'
+ - 'app/graphql/mutations/work_items/export.rb'
+ - 'app/graphql/resolvers/base_resolver.rb'
+ - 'app/graphql/resolvers/ci/pipeline_job_artifacts_resolver.rb'
+ - 'app/graphql/resolvers/ci/runner_groups_resolver.rb'
+ - 'app/graphql/resolvers/ci/runner_job_count_resolver.rb'
+ - 'app/graphql/resolvers/ci/runner_owner_project_resolver.rb'
+ - 'app/graphql/resolvers/ci/runner_projects_resolver.rb'
+ - 'app/graphql/resolvers/commit_pipelines_resolver.rb'
+ - 'app/graphql/resolvers/concerns/caching_array_resolver.rb'
+ - 'app/graphql/resolvers/concerns/looks_ahead.rb'
+ - 'app/graphql/resolvers/environments/last_deployment_resolver.rb'
+ - 'app/graphql/resolvers/group_packages_resolver.rb'
+ - 'app/graphql/resolvers/groups_resolver.rb'
+ - 'app/graphql/resolvers/issues/base_resolver.rb'
+ - 'app/graphql/resolvers/merge_request_pipelines_resolver.rb'
+ - 'app/graphql/resolvers/nested_groups_resolver.rb'
+ - 'app/graphql/resolvers/project_packages_resolver.rb'
+ - 'app/graphql/resolvers/project_pipelines_resolver.rb'
+ - 'app/graphql/types/access_level_type.rb'
+ - 'app/graphql/types/admin/sidekiq_queues/delete_jobs_response_type.rb'
+ - 'app/graphql/types/alert_management/alert_type.rb'
+ - 'app/graphql/types/analytics/cycle_analytics/link_type.rb'
+ - 'app/graphql/types/analytics/cycle_analytics/metric_type.rb'
+ - 'app/graphql/types/base_enum.rb'
+ - 'app/graphql/types/blame/blame_type.rb'
+ - 'app/graphql/types/blame/commit_data_type.rb'
+ - 'app/graphql/types/blame/groups_type.rb'
+ - 'app/graphql/types/blob_viewer_type.rb'
+ - 'app/graphql/types/board_list_type.rb'
+ - 'app/graphql/types/boards/board_issue_input_base_type.rb'
+ - 'app/graphql/types/branch_protections/merge_access_level_type.rb'
+ - 'app/graphql/types/branch_protections/push_access_level_type.rb'
+ - 'app/graphql/types/branch_type.rb'
+ - 'app/graphql/types/ci/analytics_type.rb'
+ - 'app/graphql/types/ci/build_need_type.rb'
+ - 'app/graphql/types/ci/catalog/resource_type.rb'
+ - 'app/graphql/types/ci/code_quality_report_summary_type.rb'
+ - 'app/graphql/types/ci/config/config_type.rb'
+ - 'app/graphql/types/ci/config/group_type.rb'
+ - 'app/graphql/types/ci/config/include_type.rb'
+ - 'app/graphql/types/ci/config/job_restriction_type.rb'
+ - 'app/graphql/types/ci/config/job_type.rb'
+ - 'app/graphql/types/ci/config/need_type.rb'
+ - 'app/graphql/types/ci/config/stage_type.rb'
+ - 'app/graphql/types/ci/config_variable_type.rb'
+ - 'app/graphql/types/ci/detailed_status_type.rb'
+ - 'app/graphql/types/ci/group_environment_scope_connection_type.rb'
+ - 'app/graphql/types/ci/group_environment_scope_type.rb'
+ - 'app/graphql/types/ci/group_type.rb'
+ - 'app/graphql/types/ci/group_variable_connection_type.rb'
+ - 'app/graphql/types/ci/group_variable_type.rb'
+ - 'app/graphql/types/ci/inherited_ci_variable_type.rb'
+ - 'app/graphql/types/ci/instance_variable_type.rb'
+ - 'app/graphql/types/ci/job_artifact_type.rb'
+ - 'app/graphql/types/ci/job_base_field.rb'
+ - 'app/graphql/types/ci/job_token_scope_type.rb'
+ - 'app/graphql/types/ci/job_trace_type.rb'
+ - 'app/graphql/types/ci/job_type.rb'
+ - 'app/graphql/types/ci/manual_variable_type.rb'
+ - 'app/graphql/types/ci/pipeline_message_type.rb'
+ - 'app/graphql/types/ci/pipeline_type.rb'
+ - 'app/graphql/types/ci/project_variable_connection_type.rb'
+ - 'app/graphql/types/ci/project_variable_type.rb'
+ - 'app/graphql/types/ci/recent_failures_type.rb'
+ - 'app/graphql/types/ci/runner_architecture_type.rb'
+ - 'app/graphql/types/ci/runner_countable_connection_type.rb'
+ - 'app/graphql/types/ci/runner_platform_type.rb'
+ - 'app/graphql/types/ci/runner_setup_type.rb'
+ - 'app/graphql/types/ci/runner_type.rb'
+ - 'app/graphql/types/ci/runner_web_url_edge.rb'
+ - 'app/graphql/types/ci/stage_type.rb'
+ - 'app/graphql/types/ci/status_action_type.rb'
+ - 'app/graphql/types/ci/template_type.rb'
+ - 'app/graphql/types/ci/test_case_type.rb'
+ - 'app/graphql/types/ci/test_report_summary_type.rb'
+ - 'app/graphql/types/ci/test_report_total_type.rb'
+ - 'app/graphql/types/ci/test_suite_summary_type.rb'
+ - 'app/graphql/types/ci/test_suite_type.rb'
+ - 'app/graphql/types/ci_configuration/sast/analyzers_entity_type.rb'
+ - 'app/graphql/types/ci_configuration/sast/entity_type.rb'
+ - 'app/graphql/types/ci_configuration/sast/options_entity_type.rb'
+ - 'app/graphql/types/ci_configuration/sast/type.rb'
+ - 'app/graphql/types/clusters/agents/authorizations/ci_access_type.rb'
+ - 'app/graphql/types/clusters/agents/authorizations/user_access_type.rb'
+ - 'app/graphql/types/commit_references_type.rb'
+ - 'app/graphql/types/commit_signatures/verification_status_enum.rb'
+ - 'app/graphql/types/countable_connection_type.rb'
+ - 'app/graphql/types/customer_relations/organization_state_counts_type.rb'
+ - 'app/graphql/types/deployment_tag_type.rb'
+ - 'app/graphql/types/design_management_type.rb'
+ - 'app/graphql/types/diff_refs_type.rb'
+ - 'app/graphql/types/diff_stats_summary_type.rb'
+ - 'app/graphql/types/diff_stats_type.rb'
+ - 'app/graphql/types/diff_type.rb'
+ - 'app/graphql/types/error_tracking/sentry_error_frequency_type.rb'
+ - 'app/graphql/types/error_tracking/sentry_error_stack_trace_context_type.rb'
+ - 'app/graphql/types/error_tracking/sentry_error_stack_trace_entry_type.rb'
+ - 'app/graphql/types/error_tracking/sentry_error_tags_type.rb'
+ - 'app/graphql/types/error_tracking/sentry_error_type.rb'
+ - 'app/graphql/types/issue_connection_type.rb'
+ - 'app/graphql/types/issue_type.rb'
+ - 'app/graphql/types/jira_import_type.rb'
+ - 'app/graphql/types/jira_user_type.rb'
+ - 'app/graphql/types/kas/agent_configuration_type.rb'
+ - 'app/graphql/types/kas/agent_connection_type.rb'
+ - 'app/graphql/types/kas/agent_metadata_type.rb'
+ - 'app/graphql/types/key_type.rb'
+ - 'app/graphql/types/limited_countable_connection_type.rb'
+ - 'app/graphql/types/merge_request_connection_type.rb'
+ - 'app/graphql/types/merge_requests/mergeability_check_type.rb'
+ - 'app/graphql/types/nested_environment_type.rb'
+ - 'app/graphql/types/notes/deleted_note_type.rb'
+ - 'app/graphql/types/notes/diff_position_type.rb'
+ - 'app/graphql/types/packages/composer/json_type.rb'
+ - 'app/graphql/types/packages/helm/dependency_type.rb'
+ - 'app/graphql/types/packages/helm/maintainer_type.rb'
+ - 'app/graphql/types/packages/helm/metadata_type.rb'
+ - 'app/graphql/types/packages/package_base_type.rb'
+ - 'app/graphql/types/packages/package_dependency_link_type.rb'
+ - 'app/graphql/types/packages/package_dependency_type.rb'
+ - 'app/graphql/types/permission_types/base_permission_type.rb'
+ - 'app/graphql/types/project_statistics_redirect_type.rb'
+ - 'app/graphql/types/project_type.rb'
+ - 'app/graphql/types/projects/commit_parent_names_type.rb'
+ - 'app/graphql/types/projects/fork_details_type.rb'
+ - 'app/graphql/types/projects/repository_language_type.rb'
+ - 'app/graphql/types/projects/service_type_enum.rb'
+ - 'app/graphql/types/projects/services/jira_project_type.rb'
+ - 'app/graphql/types/projects/topic_type.rb'
+ - 'app/graphql/types/query_complexity_type.rb'
+ - 'app/graphql/types/repository/blob_type.rb'
+ - 'app/graphql/types/snippets/blob_connection_type.rb'
+ - 'app/graphql/types/snippets/blob_type.rb'
+ - 'app/graphql/types/snippets/blob_viewer_type.rb'
+ - 'app/graphql/types/task_completion_status.rb'
+ - 'app/graphql/types/time_tracking/timelog_connection_type.rb'
+ - 'app/graphql/types/tree/blob_type.rb'
+ - 'app/graphql/types/tree/submodule_type.rb'
+ - 'app/graphql/types/tree/tree_entry_type.rb'
+ - 'app/graphql/types/tree/tree_type.rb'
+ - 'app/graphql/types/user_callout_type.rb'
+ - 'app/graphql/types/user_preferences_type.rb'
+ - 'app/graphql/types/user_status_type.rb'
+ - 'app/graphql/types/work_item_id_type.rb'
+ - 'app/graphql/types/work_items/linked_item_type.rb'
+ - 'app/graphql/types/work_items/widgets/assignees_type.rb'
+ - 'app/graphql/types/work_items/widgets/award_emoji_type.rb'
+ - 'app/graphql/types/work_items/widgets/current_user_todos_type.rb'
+ - 'app/graphql/types/work_items/widgets/description_type.rb'
+ - 'app/graphql/types/work_items/widgets/hierarchy_type.rb'
+ - 'app/graphql/types/work_items/widgets/labels_type.rb'
+ - 'app/graphql/types/work_items/widgets/linked_items_type.rb'
+ - 'app/graphql/types/work_items/widgets/milestone_type.rb'
+ - 'app/graphql/types/work_items/widgets/notes_type.rb'
+ - 'app/graphql/types/work_items/widgets/notifications_type.rb'
+ - 'app/graphql/types/work_items/widgets/start_and_due_date_type.rb'
+ - 'app/graphql/types/x509_certificate_type.rb'
+ - 'app/graphql/types/x509_issuer_type.rb'
+ - 'app/helpers/application_helper.rb'
+ - 'app/helpers/auth_helper.rb'
+ - 'app/helpers/ci/status_helper.rb'
+ - 'app/helpers/commits_helper.rb'
+ - 'app/helpers/diff_helper.rb'
+ - 'app/helpers/dropdowns_helper.rb'
+ - 'app/helpers/environment_helper.rb'
+ - 'app/helpers/integrations_helper.rb'
+ - 'app/helpers/issuables_helper.rb'
+ - 'app/helpers/lazy_image_tag_helper.rb'
+ - 'app/helpers/namespaces_helper.rb'
+ - 'app/helpers/nav/top_nav_helper.rb'
+ - 'app/helpers/page_layout_helper.rb'
+ - 'app/helpers/routing/projects_helper.rb'
+ - 'app/helpers/routing/pseudonymization_helper.rb'
+ - 'app/helpers/search_helper.rb'
+ - 'app/helpers/sidebars_helper.rb'
+ - 'app/helpers/sorting_helper.rb'
+ - 'app/helpers/users_helper.rb'
+ - 'app/helpers/visibility_level_helper.rb'
+ - 'app/mailers/emails/issues.rb'
+ - 'app/mailers/emails/members.rb'
+ - 'app/mailers/emails/merge_requests.rb'
+ - 'app/mailers/emails/profile.rb'
+ - 'app/mailers/previews/notify_preview.rb'
+ - 'app/mailers/repository_check_mailer.rb'
+ - 'app/models/active_session.rb'
+ - 'app/models/alert_management/alert.rb'
+ - 'app/models/application_record.rb'
+ - 'app/models/application_setting.rb'
+ - 'app/models/application_setting_implementation.rb'
+ - 'app/models/audit_event.rb'
+ - 'app/models/award_emoji.rb'
+ - 'app/models/badge.rb'
+ - 'app/models/board.rb'
+ - 'app/models/bulk_import.rb'
+ - 'app/models/bulk_imports/entity.rb'
+ - 'app/models/ci/bridge.rb'
+ - 'app/models/ci/build.rb'
+ - 'app/models/ci/build_trace_chunk.rb'
+ - 'app/models/ci/freeze_period.rb'
+ - 'app/models/ci/job_artifact.rb'
+ - 'app/models/ci/namespace_mirror.rb'
+ - 'app/models/ci/pipeline.rb'
+ - 'app/models/ci/pipeline_schedule.rb'
+ - 'app/models/ci/processable.rb'
+ - 'app/models/ci/runner.rb'
+ - 'app/models/ci/trigger_request.rb'
+ - 'app/models/clusters/cluster.rb'
+ - 'app/models/commit.rb'
+ - 'app/models/commit_collection.rb'
+ - 'app/models/commit_status.rb'
+ - 'app/models/concerns/analytics/cycle_analytics/parentable.rb'
+ - 'app/models/concerns/approvable.rb'
+ - 'app/models/concerns/async_devise_email.rb'
+ - 'app/models/concerns/atomic_internal_id.rb'
+ - 'app/models/concerns/awardable.rb'
+ - 'app/models/concerns/batch_destroy_dependent_associations.rb'
+ - 'app/models/concerns/batch_nullify_dependent_associations.rb'
+ - 'app/models/concerns/boards/listable.rb'
+ - 'app/models/concerns/cache_markdown_field.rb'
+ - 'app/models/concerns/cached_commit.rb'
+ - 'app/models/concerns/cascading_namespace_setting_attribute.rb'
+ - 'app/models/concerns/ci/deployable.rb'
+ - 'app/models/concerns/ci/partitionable/switch.rb'
+ - 'app/models/concerns/deployment_platform.rb'
+ - 'app/models/concerns/diff_positionable_note.rb'
+ - 'app/models/concerns/encrypted_user_password.rb'
+ - 'app/models/concerns/fast_destroy_all.rb'
+ - 'app/models/concerns/featurable.rb'
+ - 'app/models/concerns/file_store_mounter.rb'
+ - 'app/models/concerns/from_except.rb'
+ - 'app/models/concerns/from_intersect.rb'
+ - 'app/models/concerns/from_set_operator.rb'
+ - 'app/models/concerns/from_union.rb'
+ - 'app/models/concerns/has_repository.rb'
+ - 'app/models/concerns/has_wiki_page_meta_attributes.rb'
+ - 'app/models/concerns/ignorable_columns.rb'
+ - 'app/models/concerns/integrations/reset_secret_fields.rb'
+ - 'app/models/concerns/issuable.rb'
+ - 'app/models/concerns/limitable.rb'
+ - 'app/models/concerns/mentionable.rb'
+ - 'app/models/concerns/noteable.rb'
+ - 'app/models/concerns/packages/debian/architecture.rb'
+ - 'app/models/concerns/packages/debian/component.rb'
+ - 'app/models/concerns/packages/debian/distribution.rb'
+ - 'app/models/concerns/packages/fips.rb'
+ - 'app/models/concerns/participable.rb'
+ - 'app/models/concerns/project_features_compatibility.rb'
+ - 'app/models/concerns/redactable.rb'
+ - 'app/models/concerns/redis_cacheable.rb'
+ - 'app/models/concerns/resolvable_discussion.rb'
+ - 'app/models/concerns/routable.rb'
+ - 'app/models/concerns/sanitizable.rb'
+ - 'app/models/concerns/sortable.rb'
+ - 'app/models/concerns/spammable.rb'
+ - 'app/models/concerns/subscribable.rb'
+ - 'app/models/concerns/taggable_queries.rb'
+ - 'app/models/concerns/taskable.rb'
+ - 'app/models/concerns/time_trackable.rb'
+ - 'app/models/concerns/token_authenticatable_strategies/base.rb'
+ - 'app/models/concerns/triggerable_hooks.rb'
+ - 'app/models/concerns/with_uploads.rb'
+ - 'app/models/container_repository.rb'
+ - 'app/models/cycle_analytics/project_level_stage_adapter.rb'
+ - 'app/models/deploy_key.rb'
+ - 'app/models/deployment.rb'
+ - 'app/models/design_management/design.rb'
+ - 'app/models/design_management/version.rb'
+ - 'app/models/discussion_note.rb'
+ - 'app/models/event.rb'
+ - 'app/models/group.rb'
+ - 'app/models/group_deploy_key.rb'
+ - 'app/models/hooks/web_hook.rb'
+ - 'app/models/hooks/web_hook_log.rb'
+ - 'app/models/integrations/apple_app_store.rb'
+ - 'app/models/integrations/base_chat_notification.rb'
+ - 'app/models/integrations/base_slash_commands.rb'
+ - 'app/models/integrations/base_third_party_wiki.rb'
+ - 'app/models/integrations/google_play.rb'
+ - 'app/models/integrations/pumble.rb'
+ - 'app/models/issue.rb'
+ - 'app/models/key.rb'
+ - 'app/models/label.rb'
+ - 'app/models/label_link.rb'
+ - 'app/models/legacy_diff_note.rb'
+ - 'app/models/lfs_objects_project.rb'
+ - 'app/models/member.rb'
+ - 'app/models/members/group_member.rb'
+ - 'app/models/members/project_member.rb'
+ - 'app/models/members/project_namespace_member.rb'
+ - 'app/models/merge_request.rb'
+ - 'app/models/merge_request/approval_removal_settings.rb'
+ - 'app/models/merge_request_context_commit.rb'
+ - 'app/models/merge_request_context_commit_diff_file.rb'
+ - 'app/models/merge_request_diff.rb'
+ - 'app/models/merge_request_diff_commit.rb'
+ - 'app/models/milestone.rb'
+ - 'app/models/ml/candidate.rb'
+ - 'app/models/ml/experiment.rb'
+ - 'app/models/ml/model.rb'
+ - 'app/models/namespace.rb'
+ - 'app/models/namespace_ci_cd_setting.rb'
+ - 'app/models/namespace_statistics.rb'
+ - 'app/models/namespaces/sync_event.rb'
+ - 'app/models/network/commit.rb'
+ - 'app/models/note.rb'
+ - 'app/models/notification_setting.rb'
+ - 'app/models/operations/feature_flags/user_list.rb'
+ - 'app/models/packages/debian/file_entry.rb'
+ - 'app/models/packages/package.rb'
+ - 'app/models/personal_access_token.rb'
+ - 'app/models/project.rb'
+ - 'app/models/project_export_job.rb'
+ - 'app/models/project_feature.rb'
+ - 'app/models/project_import_data.rb'
+ - 'app/models/project_statistics.rb'
+ - 'app/models/project_team.rb'
+ - 'app/models/projects/sync_event.rb'
+ - 'app/models/prometheus_alert_event.rb'
+ - 'app/models/protectable_dropdown.rb'
+ - 'app/models/protected_branch.rb'
+ - 'app/models/redirect_route.rb'
+ - 'app/models/releases/evidence.rb'
+ - 'app/models/repository.rb'
+ - 'app/models/repository_language.rb'
+ - 'app/models/route.rb'
+ - 'app/models/sent_notification.rb'
+ - 'app/models/slack_integration.rb'
+ - 'app/models/snippet.rb'
+ - 'app/models/subscription.rb'
+ - 'app/models/todo.rb'
+ - 'app/models/token_with_iv.rb'
+ - 'app/models/upload.rb'
+ - 'app/models/user.rb'
+ - 'app/models/user_agent_detail.rb'
+ - 'app/models/wiki.rb'
+ - 'app/models/wiki_page.rb'
+ - 'app/policies/application_setting/term_policy.rb'
+ - 'app/policies/application_setting_policy.rb'
+ - 'app/policies/ci/deployable_policy.rb'
+ - 'app/policies/concerns/member_policy_helpers.rb'
+ - 'app/policies/email_policy.rb'
+ - 'app/policies/event_policy.rb'
+ - 'app/policies/group_group_link_policy.rb'
+ - 'app/policies/issue_policy.rb'
+ - 'app/policies/list_policy.rb'
+ - 'app/policies/namespace_ci_cd_setting_policy.rb'
+ - 'app/policies/project_group_link_policy.rb'
+ - 'app/policies/project_policy.rb'
+ - 'app/policies/upload_policy.rb'
+ - 'app/presenters/ci/build_runner_presenter.rb'
+ - 'app/presenters/deploy_key_presenter.rb'
+ - 'app/presenters/dev_ops_report/metric_presenter.rb'
+ - 'app/presenters/key_presenter.rb'
+ - 'app/presenters/label_presenter.rb'
+ - 'app/presenters/merge_request_presenter.rb'
+ - 'app/presenters/project_presenter.rb'
+ - 'app/presenters/projects/import_export/project_export_presenter.rb'
+ - 'app/presenters/projects/settings/deploy_keys_presenter.rb'
+ - 'app/presenters/search_service_presenter.rb'
+ - 'app/presenters/work_item_presenter.rb'
+ - 'app/serializers/access_token_entity_base.rb'
+ - 'app/serializers/analytics/cycle_analytics/value_stream_entity.rb'
+ - 'app/serializers/analytics_build_entity.rb'
+ - 'app/serializers/analytics_issue_entity.rb'
+ - 'app/serializers/ci/dag_job_entity.rb'
+ - 'app/serializers/ci/dag_pipeline_entity.rb'
+ - 'app/serializers/ci/job_entity.rb'
+ - 'app/serializers/cluster_entity.rb'
+ - 'app/serializers/diffs_entity.rb'
+ - 'app/serializers/diffs_metadata_entity.rb'
+ - 'app/serializers/environment_serializer.rb'
+ - 'app/serializers/fork_namespace_entity.rb'
+ - 'app/serializers/group_access_token_entity.rb'
+ - 'app/serializers/group_access_token_serializer.rb'
+ - 'app/serializers/group_child_entity.rb'
+ - 'app/serializers/group_entity.rb'
+ - 'app/serializers/impersonation_access_token_entity.rb'
+ - 'app/serializers/impersonation_access_token_serializer.rb'
+ - 'app/serializers/import/github_failure_entity.rb'
+ - 'app/serializers/integrations/field_entity.rb'
+ - 'app/serializers/merge_request_noteable_entity.rb'
+ - 'app/serializers/merge_request_poll_cached_widget_entity.rb'
+ - 'app/serializers/merge_request_poll_widget_entity.rb'
+ - 'app/serializers/merge_request_widget_entity.rb'
+ - 'app/serializers/personal_access_token_entity.rb'
+ - 'app/serializers/personal_access_token_serializer.rb'
+ - 'app/serializers/pipeline_serializer.rb'
+ - 'app/serializers/profile/event_entity.rb'
+ - 'app/serializers/project_access_token_entity.rb'
+ - 'app/serializers/project_access_token_serializer.rb'
+ - 'app/services/admin/set_feature_flag_service.rb'
+ - 'app/services/authorized_project_update/project_access_changed_service.rb'
+ - 'app/services/authorized_project_update/project_recalculate_per_user_service.rb'
+ - 'app/services/authorized_project_update/project_recalculate_service.rb'
+ - 'app/services/award_emojis/destroy_service.rb'
+ - 'app/services/base_group_service.rb'
+ - 'app/services/boards/base_item_move_service.rb'
+ - 'app/services/boards/base_items_list_service.rb'
+ - 'app/services/boards/lists/base_create_service.rb'
+ - 'app/services/boards/lists/base_destroy_service.rb'
+ - 'app/services/boards/lists/move_service.rb'
+ - 'app/services/branches/delete_merged_service.rb'
+ - 'app/services/bulk_imports/batched_relation_export_service.rb'
+ - 'app/services/bulk_imports/create_service.rb'
+ - 'app/services/bulk_imports/lfs_objects_export_service.rb'
+ - 'app/services/bulk_imports/relation_batch_export_service.rb'
+ - 'app/services/bulk_imports/relation_export_service.rb'
+ - 'app/services/bulk_imports/tree_export_service.rb'
+ - 'app/services/bulk_imports/uploads_export_service.rb'
+ - 'app/services/bulk_update_integration_service.rb'
+ - 'app/services/chat_names/find_user_service.rb'
+ - 'app/services/ci/abort_pipelines_service.rb'
+ - 'app/services/ci/archive_trace_service.rb'
+ - 'app/services/ci/change_variable_service.rb'
+ - 'app/services/ci/create_commit_status_service.rb'
+ - 'app/services/ci/create_pipeline_service.rb'
+ - 'app/services/ci/delete_objects_service.rb'
+ - 'app/services/ci/delete_unit_tests_service.rb'
+ - 'app/services/ci/ensure_stage_service.rb'
+ - 'app/services/ci/expire_pipeline_cache_service.rb'
+ - 'app/services/ci/job_artifacts/bulk_delete_by_project_service.rb'
+ - 'app/services/ci/job_artifacts/destroy_batch_service.rb'
+ - 'app/services/ci/job_artifacts/expire_project_build_artifacts_service.rb'
+ - 'app/services/ci/pipeline_artifacts/create_code_quality_mr_diff_report_service.rb'
+ - 'app/services/ci/pipeline_creation/cancel_redundant_pipelines_service.rb'
+ - 'app/services/ci/pipeline_processing/atomic_processing_service.rb'
+ - 'app/services/ci/pipeline_processing/atomic_processing_service/status_collection.rb'
+ - 'app/services/ci/queue/build_queue_service.rb'
+ - 'app/services/ci/queue/pending_builds_strategy.rb'
+ - 'app/services/ci/refs/enqueue_pipelines_to_unlock_service.rb'
+ - 'app/services/ci/register_job_service.rb'
+ - 'app/services/ci/reset_skipped_jobs_service.rb'
+ - 'app/services/ci/resource_groups/assign_resource_from_resource_group_service.rb'
+ - 'app/services/ci/retry_job_service.rb'
+ - 'app/services/ci/runners/bulk_delete_runners_service.rb'
+ - 'app/services/ci/runners/reconcile_existing_runner_versions_service.rb'
+ - 'app/services/ci/runners/set_runner_associated_projects_service.rb'
+ - 'app/services/ci/stuck_builds/drop_helpers.rb'
+ - 'app/services/ci/stuck_builds/drop_pending_service.rb'
+ - 'app/services/ci/stuck_builds/drop_running_service.rb'
+ - 'app/services/ci/track_failed_build_service.rb'
+ - 'app/services/ci/unlock_artifacts_service.rb'
+ - 'app/services/ci/unlock_pipeline_service.rb'
+ - 'app/services/ci/update_build_state_service.rb'
+ - 'app/services/clusters/agents/authorizations/ci_access/refresh_service.rb'
+ - 'app/services/cohorts_service.rb'
+ - 'app/services/commits/change_service.rb'
+ - 'app/services/concerns/issues/resolve_discussions.rb'
+ - 'app/services/concerns/rate_limited_service.rb'
+ - 'app/services/concerns/work_items/widgetable_service.rb'
+ - 'app/services/database/consistency_check_service.rb'
+ - 'app/services/database/consistency_fix_service.rb'
+ - 'app/services/database/mark_migration_service.rb'
+ - 'app/services/deployments/create_for_job_service.rb'
+ - 'app/services/design_management/copy_design_collection/copy_service.rb'
+ - 'app/services/design_management/generate_image_versions_service.rb'
+ - 'app/services/environments/create_for_job_service.rb'
+ - 'app/services/environments/stop_stale_service.rb'
+ - 'app/services/export_csv/base_service.rb'
+ - 'app/services/git/wiki_push_service.rb'
+ - 'app/services/groups/autocomplete_service.rb'
+ - 'app/services/groups/destroy_service.rb'
+ - 'app/services/groups/import_export/import_service.rb'
+ - 'app/services/groups/transfer_service.rb'
+ - 'app/services/integrations/slack_options/label_search_handler.rb'
+ - 'app/services/integrations/slack_options/user_search_handler.rb'
+ - 'app/services/issuable_base_service.rb'
+ - 'app/services/issuable_links/create_service.rb'
+ - 'app/services/issues/build_service.rb'
+ - 'app/services/issues/export_csv_service.rb'
+ - 'app/services/issues/referenced_merge_requests_service.rb'
+ - 'app/services/issues/relative_position_rebalancing_service.rb'
+ - 'app/services/issues/set_crm_contacts_service.rb'
+ - 'app/services/issues/update_service.rb'
+ - 'app/services/labels/available_labels_service.rb'
+ - 'app/services/labels/find_or_create_service.rb'
+ - 'app/services/labels/promote_service.rb'
+ - 'app/services/labels/transfer_service.rb'
+ - 'app/services/lfs/file_transformer.rb'
+ - 'app/services/lfs/lock_file_service.rb'
+ - 'app/services/lfs/locks_finder_service.rb'
+ - 'app/services/lfs/unlock_file_service.rb'
+ - 'app/services/loose_foreign_keys/cleaner_service.rb'
+ - 'app/services/members/invite_member_builder.rb'
+ - 'app/services/members/projects/creator_service.rb'
+ - 'app/services/members/standard_member_builder.rb'
+ - 'app/services/merge_requests/base_service.rb'
+ - 'app/services/merge_requests/create_from_issue_service.rb'
+ - 'app/services/merge_requests/delete_non_latest_diffs_service.rb'
+ - 'app/services/merge_requests/migrate_external_diffs_service.rb'
+ - 'app/services/merge_requests/push_options_handler_service.rb'
+ - 'app/services/merge_requests/pushed_branches_service.rb'
+ - 'app/services/merge_requests/refresh_service.rb'
+ - 'app/services/merge_requests/reload_diffs_service.rb'
+ - 'app/services/merge_requests/remove_approval_service.rb'
+ - 'app/services/merge_requests/update_service.rb'
+ - 'app/services/milestones/find_or_create_service.rb'
+ - 'app/services/milestones/promote_service.rb'
+ - 'app/services/milestones/transfer_service.rb'
+ - 'app/services/notification_recipients/builder/base.rb'
+ - 'app/services/notification_service.rb'
+ - 'app/services/packages/cleanup/execute_policy_service.rb'
+ - 'app/services/packages/conan/single_package_search_service.rb'
+ - 'app/services/packages/create_dependency_service.rb'
+ - 'app/services/packages/create_package_service.rb'
+ - 'app/services/packages/debian/extract_metadata_service.rb'
+ - 'app/services/packages/debian/generate_distribution_key_service.rb'
+ - 'app/services/packages/debian/generate_distribution_service.rb'
+ - 'app/services/packages/helm/extract_file_metadata_service.rb'
+ - 'app/services/packages/npm/create_package_service.rb'
+ - 'app/services/packages/nuget/create_dependency_service.rb'
+ - 'app/services/packages/nuget/extract_remote_metadata_file_service.rb'
+ - 'app/services/packages/nuget/process_package_file_service.rb'
+ - 'app/services/packages/nuget/search_service.rb'
+ - 'app/services/packages/rpm/repository_metadata/build_xml_base_service.rb'
+ - 'app/services/packages/rubygems/metadata_extraction_service.rb'
+ - 'app/services/packages/update_tags_service.rb'
+ - 'app/services/pages/destroy_deployments_service.rb'
+ - 'app/services/personal_access_tokens/revoke_token_family_service.rb'
+ - 'app/services/projects/auto_devops/disable_service.rb'
+ - 'app/services/projects/autocomplete_service.rb'
+ - 'app/services/projects/batch_forks_count_service.rb'
+ - 'app/services/projects/batch_open_issues_count_service.rb'
+ - 'app/services/projects/batch_open_merge_requests_count_service.rb'
+ - 'app/services/projects/branches_by_mode_service.rb'
+ - 'app/services/projects/cleanup_service.rb'
+ - 'app/services/projects/container_repository/third_party/delete_tags_service.rb'
+ - 'app/services/projects/destroy_service.rb'
+ - 'app/services/projects/detect_repository_languages_service.rb'
+ - 'app/services/projects/disable_deploy_key_service.rb'
+ - 'app/services/projects/forks_count_service.rb'
+ - 'app/services/projects/gitlab_projects_import_service.rb'
+ - 'app/services/projects/lfs_pointers/lfs_link_service.rb'
+ - 'app/services/projects/move_deploy_keys_projects_service.rb'
+ - 'app/services/projects/move_forks_service.rb'
+ - 'app/services/projects/move_lfs_objects_projects_service.rb'
+ - 'app/services/projects/move_notification_settings_service.rb'
+ - 'app/services/projects/move_project_authorizations_service.rb'
+ - 'app/services/projects/move_project_group_links_service.rb'
+ - 'app/services/projects/move_project_members_service.rb'
+ - 'app/services/projects/open_issues_count_service.rb'
+ - 'app/services/projects/overwrite_project_service.rb'
+ - 'app/services/projects/record_target_platforms_service.rb'
+ - 'app/services/projects/slack_application_install_service.rb'
+ - 'app/services/projects/transfer_service.rb'
+ - 'app/services/projects/unlink_fork_service.rb'
+ - 'app/services/projects/update_pages_service.rb'
+ - 'app/services/protected_branches/cache_service.rb'
+ - 'app/services/protected_branches/legacy_api_update_service.rb'
+ - 'app/services/quick_actions/interpret_service.rb'
+ - 'app/services/quick_actions/target_service.rb'
+ - 'app/services/releases/create_evidence_service.rb'
+ - 'app/services/releases/create_service.rb'
+ - 'app/services/releases/update_service.rb'
+ - 'app/services/repositories/changelog_service.rb'
+ - 'app/services/resource_events/change_labels_service.rb'
+ - 'app/services/resource_events/synthetic_label_notes_builder_service.rb'
+ - 'app/services/resource_events/synthetic_milestone_notes_builder_service.rb'
+ - 'app/services/resource_events/synthetic_state_notes_builder_service.rb'
+ - 'app/services/search/global_service.rb'
+ - 'app/services/search_service.rb'
+ - 'app/services/service_ping/submit_service.rb'
+ - 'app/services/snippets/bulk_destroy_service.rb'
+ - 'app/services/snippets/count_service.rb'
+ - 'app/services/spam/akismet_service.rb'
+ - 'app/services/suggestions/create_service.rb'
+ - 'app/services/suggestions/outdate_service.rb'
+ - 'app/services/system_notes/commit_service.rb'
+ - 'app/services/system_notes/issuables_service.rb'
+ - 'app/services/terraform/remote_state_handler.rb'
+ - 'app/services/todos/destroy/confidential_issue_service.rb'
+ - 'app/services/todos/destroy/entity_leave_service.rb'
+ - 'app/services/todos/destroy/unauthorized_features_service.rb'
+ - 'app/services/topics/merge_service.rb'
+ - 'app/services/uploads/destroy_service.rb'
+ - 'app/services/user_project_access_changed_service.rb'
+ - 'app/services/users/activate_service.rb'
+ - 'app/services/users/assigned_issues_count_service.rb'
+ - 'app/services/users/batch_status_cleaner_service.rb'
+ - 'app/services/users/destroy_service.rb'
+ - 'app/services/users/last_push_event_service.rb'
+ - 'app/services/users/migrate_records_to_ghost_user_service.rb'
+ - 'app/services/users/respond_to_terms_service.rb'
+ - 'app/services/users/set_namespace_commit_email_service.rb'
+ - 'app/services/users/update_service.rb'
+ - 'app/services/verify_pages_domain_service.rb'
+ - 'app/services/web_hook_service.rb'
+ - 'app/services/wiki_pages/update_service.rb'
+ - 'app/services/work_items/widgets/hierarchy_service/base_service.rb'
+ - 'app/uploaders/gitlab_uploader.rb'
+ - 'app/uploaders/metric_image_uploader.rb'
+ - 'app/uploaders/object_storage.rb'
+ - 'app/uploaders/object_storage/cdn.rb'
+ - 'app/uploaders/object_storage/cdn/google_cdn.rb'
+ - 'app/uploaders/object_storage/cdn/google_ip_cache.rb'
+ - 'app/uploaders/records_uploads.rb'
+ - 'app/validators/addressable_url_validator.rb'
+ - 'app/validators/cron_validator.rb'
+ - 'app/validators/ip_address_validator.rb'
+ - 'app/validators/nested_attributes_duplicates_validator.rb'
+ - 'app/validators/x509_certificate_credentials_validator.rb'
+ - 'app/views/dashboard/issues.atom.builder'
+ - 'app/views/groups/issues.atom.builder'
+ - 'app/views/issues/_issues_calendar.ics.ruby'
+ - 'app/views/projects/issues/index.atom.builder'
+ - 'app/views/projects/merge_requests/index.atom.builder'
+ - 'app/workers/admin_email_worker.rb'
+ - 'app/workers/analytics/usage_trends/count_job_trigger_worker.rb'
+ - 'app/workers/authorized_project_update/periodic_recalculate_worker.rb'
+ - 'app/workers/authorized_project_update/user_refresh_over_user_range_worker.rb'
+ - 'app/workers/auto_devops/disable_worker.rb'
+ - 'app/workers/auto_merge_process_worker.rb'
+ - 'app/workers/background_migration/ci_database_worker.rb'
+ - 'app/workers/background_migration_worker.rb'
+ - 'app/workers/batched_git_ref_updates/cleanup_scheduler_worker.rb'
+ - 'app/workers/build_hooks_worker.rb'
+ - 'app/workers/build_queue_worker.rb'
+ - 'app/workers/build_success_worker.rb'
+ - 'app/workers/bulk_imports/finish_batched_pipeline_worker.rb'
+ - 'app/workers/bulk_imports/pipeline_batch_worker.rb'
+ - 'app/workers/bulk_imports/pipeline_worker.rb'
+ - 'app/workers/bulk_imports/relation_batch_export_worker.rb'
+ - 'app/workers/bulk_imports/stuck_import_worker.rb'
+ - 'app/workers/chaos/cpu_spin_worker.rb'
+ - 'app/workers/chaos/db_spin_worker.rb'
+ - 'app/workers/chaos/kill_worker.rb'
+ - 'app/workers/chaos/leak_mem_worker.rb'
+ - 'app/workers/chaos/sleep_worker.rb'
+ - 'app/workers/chat_notification_worker.rb'
+ - 'app/workers/ci/archive_trace_worker.rb'
+ - 'app/workers/ci/archive_traces_cron_worker.rb'
+ - 'app/workers/ci/build_finished_worker.rb'
+ - 'app/workers/ci/build_prepare_worker.rb'
+ - 'app/workers/ci/build_schedule_worker.rb'
+ - 'app/workers/ci/create_downstream_pipeline_worker.rb'
+ - 'app/workers/ci/delete_unit_tests_worker.rb'
+ - 'app/workers/ci/external_pull_requests/create_pipeline_worker.rb'
+ - 'app/workers/ci/pipeline_artifacts/expire_artifacts_worker.rb'
+ - 'app/workers/ci/pipeline_bridge_status_worker.rb'
+ - 'app/workers/ci/pipeline_cleanup_ref_worker.rb'
+ - 'app/workers/ci/refs/unlock_previous_pipelines_worker.rb'
+ - 'app/workers/ci/retry_pipeline_worker.rb'
+ - 'app/workers/ci/runners/reconcile_existing_runner_versions_cron_worker.rb'
+ - 'app/workers/ci/runners/stale_machines_cleanup_cron_worker.rb'
+ - 'app/workers/ci/schedule_delete_objects_cron_worker.rb'
+ - 'app/workers/ci/schedule_unlock_pipelines_in_queue_cron_worker.rb'
+ - 'app/workers/ci/stuck_builds/drop_running_worker.rb'
+ - 'app/workers/ci/stuck_builds/drop_scheduled_worker.rb'
+ - 'app/workers/ci/track_failed_build_worker.rb'
+ - 'app/workers/ci/unlock_pipelines_in_queue_worker.rb'
+ - 'app/workers/ci/update_locked_unknown_artifacts_worker.rb'
+ - 'app/workers/ci_platform_metrics_update_cron_worker.rb'
+ - 'app/workers/click_house/events_sync_worker.rb'
+ - 'app/workers/cluster_configure_istio_worker.rb'
+ - 'app/workers/cluster_install_app_worker.rb'
+ - 'app/workers/cluster_patch_app_worker.rb'
+ - 'app/workers/cluster_update_app_worker.rb'
+ - 'app/workers/cluster_upgrade_app_worker.rb'
+ - 'app/workers/cluster_wait_for_app_installation_worker.rb'
+ - 'app/workers/cluster_wait_for_app_update_worker.rb'
+ - 'app/workers/cluster_wait_for_ingress_ip_address_worker.rb'
+ - 'app/workers/clusters/applications/activate_integration_worker.rb'
+ - 'app/workers/clusters/applications/deactivate_integration_worker.rb'
+ - 'app/workers/clusters/applications/uninstall_worker.rb'
+ - 'app/workers/clusters/applications/wait_for_uninstall_app_worker.rb'
+ - 'app/workers/clusters/cleanup/project_namespace_worker.rb'
+ - 'app/workers/clusters/cleanup/service_account_worker.rb'
+ - 'app/workers/concerns/application_worker.rb'
+ - 'app/workers/concerns/chaos_queue.rb'
+ - 'app/workers/concerns/gitlab/bitbucket_import/stage_methods.rb'
+ - 'app/workers/concerns/gitlab/bitbucket_server_import/stage_methods.rb'
+ - 'app/workers/concerns/gitlab/github_import/stage_methods.rb'
+ - 'app/workers/concerns/limited_capacity/job_tracker.rb'
+ - 'app/workers/concerns/limited_capacity/worker.rb'
+ - 'app/workers/concerns/new_issuable.rb'
+ - 'app/workers/concerns/reactive_cacheable_worker.rb'
+ - 'app/workers/container_expiration_policies/cleanup_container_repository_worker.rb'
+ - 'app/workers/container_expiration_policy_worker.rb'
+ - 'app/workers/container_registry/cleanup_worker.rb'
+ - 'app/workers/container_registry/migration/enqueuer_worker.rb'
+ - 'app/workers/container_registry/migration/guard_worker.rb'
+ - 'app/workers/container_registry/migration/observer_worker.rb'
+ - 'app/workers/container_registry/record_data_repair_detail_worker.rb'
+ - 'app/workers/counters/cleanup_refresh_worker.rb'
+ - 'app/workers/create_note_diff_file_worker.rb'
+ - 'app/workers/create_pipeline_worker.rb'
+ - 'app/workers/database/batched_background_migration/ci_database_worker.rb'
+ - 'app/workers/database/batched_background_migration/ci_execution_worker.rb'
+ - 'app/workers/database/batched_background_migration/execution_worker.rb'
+ - 'app/workers/database/batched_background_migration/main_execution_worker.rb'
+ - 'app/workers/database/batched_background_migration/single_database_worker.rb'
+ - 'app/workers/database/batched_background_migration_worker.rb'
+ - 'app/workers/database/ci_namespace_mirrors_consistency_check_worker.rb'
+ - 'app/workers/database/ci_project_mirrors_consistency_check_worker.rb'
+ - 'app/workers/database/drop_detached_partitions_worker.rb'
+ - 'app/workers/database/lock_tables_worker.rb'
+ - 'app/workers/database/monitor_locked_tables_worker.rb'
+ - 'app/workers/database/partition_management_worker.rb'
+ - 'app/workers/delete_diff_files_worker.rb'
+ - 'app/workers/delete_merged_branches_worker.rb'
+ - 'app/workers/delete_stored_files_worker.rb'
+ - 'app/workers/delete_user_worker.rb'
+ - 'app/workers/dependency_proxy/cleanup_dependency_proxy_worker.rb'
+ - 'app/workers/dependency_proxy/image_ttl_group_policy_worker.rb'
+ - 'app/workers/deployments/hooks_worker.rb'
+ - 'app/workers/design_management/new_version_worker.rb'
+ - 'app/workers/detect_repository_languages_worker.rb'
+ - 'app/workers/disallow_two_factor_for_subgroups_worker.rb'
+ - 'app/workers/email_receiver_worker.rb'
+ - 'app/workers/emails_on_push_worker.rb'
+ - 'app/workers/environments/auto_delete_cron_worker.rb'
+ - 'app/workers/environments/auto_stop_cron_worker.rb'
+ - 'app/workers/error_tracking_issue_link_worker.rb'
+ - 'app/workers/expire_build_artifacts_worker.rb'
+ - 'app/workers/export_csv_worker.rb'
+ - 'app/workers/external_service_reactive_caching_worker.rb'
+ - 'app/workers/file_hook_worker.rb'
+ - 'app/workers/flush_counter_increments_worker.rb'
+ - 'app/workers/gitlab/bitbucket_import/advance_stage_worker.rb'
+ - 'app/workers/gitlab/bitbucket_import/import_issue_notes_worker.rb'
+ - 'app/workers/gitlab/bitbucket_import/import_issue_worker.rb'
+ - 'app/workers/gitlab/bitbucket_import/import_lfs_object_worker.rb'
+ - 'app/workers/gitlab/bitbucket_import/import_pull_request_notes_worker.rb'
+ - 'app/workers/gitlab/bitbucket_import/import_pull_request_worker.rb'
+ - 'app/workers/gitlab/bitbucket_import/stage/finish_import_worker.rb'
+ - 'app/workers/gitlab/bitbucket_import/stage/import_issues_notes_worker.rb'
+ - 'app/workers/gitlab/bitbucket_import/stage/import_issues_worker.rb'
+ - 'app/workers/gitlab/bitbucket_import/stage/import_lfs_objects_worker.rb'
+ - 'app/workers/gitlab/bitbucket_import/stage/import_pull_requests_notes_worker.rb'
+ - 'app/workers/gitlab/bitbucket_import/stage/import_pull_requests_worker.rb'
+ - 'app/workers/gitlab/bitbucket_import/stage/import_repository_worker.rb'
+ - 'app/workers/gitlab/bitbucket_server_import/advance_stage_worker.rb'
+ - 'app/workers/gitlab/bitbucket_server_import/import_lfs_object_worker.rb'
+ - 'app/workers/gitlab/bitbucket_server_import/import_pull_request_notes_worker.rb'
+ - 'app/workers/gitlab/bitbucket_server_import/import_pull_request_worker.rb'
+ - 'app/workers/gitlab/bitbucket_server_import/stage/finish_import_worker.rb'
+ - 'app/workers/gitlab/bitbucket_server_import/stage/import_lfs_objects_worker.rb'
+ - 'app/workers/gitlab/bitbucket_server_import/stage/import_notes_worker.rb'
+ - 'app/workers/gitlab/bitbucket_server_import/stage/import_pull_requests_worker.rb'
+ - 'app/workers/gitlab/bitbucket_server_import/stage/import_repository_worker.rb'
+ - 'app/workers/gitlab/export/prune_project_export_jobs_worker.rb'
+ - 'app/workers/gitlab/github_gists_import/import_gist_worker.rb'
+ - 'app/workers/gitlab/github_gists_import/start_import_worker.rb'
+ - 'app/workers/gitlab/github_import/advance_stage_worker.rb'
+ - 'app/workers/gitlab/github_import/attachments/import_issue_worker.rb'
+ - 'app/workers/gitlab/github_import/attachments/import_merge_request_worker.rb'
+ - 'app/workers/gitlab/github_import/attachments/import_note_worker.rb'
+ - 'app/workers/gitlab/github_import/attachments/import_release_worker.rb'
+ - 'app/workers/gitlab/github_import/import_collaborator_worker.rb'
+ - 'app/workers/gitlab/github_import/import_diff_note_worker.rb'
+ - 'app/workers/gitlab/github_import/import_issue_event_worker.rb'
+ - 'app/workers/gitlab/github_import/import_issue_worker.rb'
+ - 'app/workers/gitlab/github_import/import_lfs_object_worker.rb'
+ - 'app/workers/gitlab/github_import/import_note_worker.rb'
+ - 'app/workers/gitlab/github_import/import_protected_branch_worker.rb'
+ - 'app/workers/gitlab/github_import/import_pull_request_worker.rb'
+ - 'app/workers/gitlab/github_import/pull_requests/import_merged_by_worker.rb'
+ - 'app/workers/gitlab/github_import/pull_requests/import_review_request_worker.rb'
+ - 'app/workers/gitlab/github_import/pull_requests/import_review_worker.rb'
+ - 'app/workers/gitlab/github_import/refresh_import_jid_worker.rb'
+ - 'app/workers/gitlab/github_import/stage/finish_import_worker.rb'
+ - 'app/workers/gitlab/github_import/stage/import_attachments_worker.rb'
+ - 'app/workers/gitlab/github_import/stage/import_base_data_worker.rb'
+ - 'app/workers/gitlab/github_import/stage/import_collaborators_worker.rb'
+ - 'app/workers/gitlab/github_import/stage/import_issue_events_worker.rb'
+ - 'app/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker.rb'
+ - 'app/workers/gitlab/github_import/stage/import_lfs_objects_worker.rb'
+ - 'app/workers/gitlab/github_import/stage/import_notes_worker.rb'
+ - 'app/workers/gitlab/github_import/stage/import_protected_branches_worker.rb'
+ - 'app/workers/gitlab/github_import/stage/import_pull_requests_merged_by_worker.rb'
+ - 'app/workers/gitlab/github_import/stage/import_pull_requests_review_requests_worker.rb'
+ - 'app/workers/gitlab/github_import/stage/import_pull_requests_reviews_worker.rb'
+ - 'app/workers/gitlab/github_import/stage/import_pull_requests_worker.rb'
+ - 'app/workers/gitlab/github_import/stage/import_repository_worker.rb'
+ - 'app/workers/gitlab/import/stuck_import_job.rb'
+ - 'app/workers/gitlab/import/stuck_project_import_jobs_worker.rb'
+ - 'app/workers/gitlab/jira_import/advance_stage_worker.rb'
+ - 'app/workers/gitlab/jira_import/import_issue_worker.rb'
+ - 'app/workers/gitlab/jira_import/stage/finish_import_worker.rb'
+ - 'app/workers/gitlab/jira_import/stage/import_attachments_worker.rb'
+ - 'app/workers/gitlab/jira_import/stage/import_issues_worker.rb'
+ - 'app/workers/gitlab/jira_import/stage/import_labels_worker.rb'
+ - 'app/workers/gitlab/jira_import/stage/import_notes_worker.rb'
+ - 'app/workers/gitlab/jira_import/stage/start_import_worker.rb'
+ - 'app/workers/gitlab/jira_import/stuck_jira_import_jobs_worker.rb'
+ - 'app/workers/gitlab_performance_bar_stats_worker.rb'
+ - 'app/workers/gitlab_service_ping_worker.rb'
+ - 'app/workers/google_cloud/create_cloudsql_instance_worker.rb'
+ - 'app/workers/group_export_worker.rb'
+ - 'app/workers/group_import_worker.rb'
+ - 'app/workers/import_export_project_cleanup_worker.rb'
+ - 'app/workers/import_issues_csv_worker.rb'
+ - 'app/workers/incident_management/add_severity_system_note_worker.rb'
+ - 'app/workers/incident_management/pager_duty/process_incident_worker.rb'
+ - 'app/workers/incident_management/process_alert_worker_v2.rb'
+ - 'app/workers/integrations/execute_worker.rb'
+ - 'app/workers/integrations/irker_worker.rb'
+ - 'app/workers/invalid_gpg_signature_update_worker.rb'
+ - 'app/workers/issuable_export_csv_worker.rb'
+ - 'app/workers/issue_due_scheduler_worker.rb'
+ - 'app/workers/issues/placement_worker.rb'
+ - 'app/workers/jira_connect/forward_event_worker.rb'
+ - 'app/workers/jira_connect/retry_request_worker.rb'
+ - 'app/workers/jira_connect/sync_branch_worker.rb'
+ - 'app/workers/jira_connect/sync_builds_worker.rb'
+ - 'app/workers/jira_connect/sync_deployments_worker.rb'
+ - 'app/workers/jira_connect/sync_feature_flags_worker.rb'
+ - 'app/workers/jira_connect/sync_merge_request_worker.rb'
+ - 'app/workers/jira_connect/sync_project_worker.rb'
+ - 'app/workers/loose_foreign_keys/cleanup_worker.rb'
+ - 'app/workers/mail_scheduler/issue_due_worker.rb'
+ - 'app/workers/mail_scheduler/notification_service_worker.rb'
+ - 'app/workers/member_invitation_reminder_emails_worker.rb'
+ - 'app/workers/members/expiring_email_notification_worker.rb'
+ - 'app/workers/members/expiring_worker.rb'
+ - 'app/workers/merge_worker.rb'
+ - 'app/workers/metrics/global_metrics_update_worker.rb'
+ - 'app/workers/migrate_external_diffs_worker.rb'
+ - 'app/workers/namespaces/prune_aggregation_schedules_worker.rb'
+ - 'app/workers/new_issue_worker.rb'
+ - 'app/workers/new_merge_request_worker.rb'
+ - 'app/workers/new_note_worker.rb'
+ - 'app/workers/object_pool/create_worker.rb'
+ - 'app/workers/object_pool/destroy_worker.rb'
+ - 'app/workers/object_pool/join_worker.rb'
+ - 'app/workers/object_pool/schedule_join_worker.rb'
+ - 'app/workers/object_storage/delete_stale_direct_uploads_worker.rb'
+ - 'app/workers/object_storage/migrate_uploads_worker.rb'
+ - 'app/workers/packages/cleanup/delete_orphaned_dependencies_worker.rb'
+ - 'app/workers/packages/cleanup/execute_policy_worker.rb'
+ - 'app/workers/packages/cleanup_package_registry_worker.rb'
+ - 'app/workers/packages/composer/cache_cleanup_worker.rb'
+ - 'app/workers/packages/debian/cleanup_dangling_package_files_worker.rb'
+ - 'app/workers/packages/nuget/extraction_worker.rb'
+ - 'app/workers/packages/rubygems/extraction_worker.rb'
+ - 'app/workers/pages/deactivated_deployments_delete_cron_worker.rb'
+ - 'app/workers/pages_domain_removal_cron_worker.rb'
+ - 'app/workers/pages_domain_ssl_renewal_cron_worker.rb'
+ - 'app/workers/pages_domain_ssl_renewal_worker.rb'
+ - 'app/workers/pages_domain_verification_cron_worker.rb'
+ - 'app/workers/pages_domain_verification_worker.rb'
+ - 'app/workers/pages_worker.rb'
+ - 'app/workers/partition_creation_worker.rb'
+ - 'app/workers/pause_control/resume_worker.rb'
+ - 'app/workers/personal_access_tokens/expired_notification_worker.rb'
+ - 'app/workers/personal_access_tokens/expiring_worker.rb'
+ - 'app/workers/pipeline_hooks_worker.rb'
+ - 'app/workers/pipeline_metrics_worker.rb'
+ - 'app/workers/pipeline_notification_worker.rb'
+ - 'app/workers/pipeline_schedule_worker.rb'
+ - 'app/workers/project_export_worker.rb'
+ - 'app/workers/projects/finalize_project_statistics_refresh_worker.rb'
+ - 'app/workers/projects/git_garbage_collect_worker.rb'
+ - 'app/workers/projects/import_export/create_relation_exports_worker.rb'
+ - 'app/workers/projects/inactive_projects_deletion_cron_worker.rb'
+ - 'app/workers/projects/schedule_refresh_build_artifacts_size_statistics_worker.rb'
+ - 'app/workers/projects/update_repository_storage_worker.rb'
+ - 'app/workers/propagate_integration_group_worker.rb'
+ - 'app/workers/propagate_integration_inherit_descendant_worker.rb'
+ - 'app/workers/propagate_integration_inherit_worker.rb'
+ - 'app/workers/propagate_integration_project_worker.rb'
+ - 'app/workers/prune_old_events_worker.rb'
+ - 'app/workers/reactive_caching_worker.rb'
+ - 'app/workers/rebase_worker.rb'
+ - 'app/workers/releases/create_evidence_worker.rb'
+ - 'app/workers/releases/manage_evidence_worker.rb'
+ - 'app/workers/remote_mirror_notification_worker.rb'
+ - 'app/workers/remove_expired_group_links_worker.rb'
+ - 'app/workers/remove_expired_members_worker.rb'
+ - 'app/workers/remove_unaccepted_member_invites_worker.rb'
+ - 'app/workers/remove_unreferenced_lfs_objects_worker.rb'
+ - 'app/workers/repository_archive_cache_worker.rb'
+ - 'app/workers/repository_check/batch_worker.rb'
+ - 'app/workers/repository_check/clear_worker.rb'
+ - 'app/workers/repository_check/dispatch_worker.rb'
+ - 'app/workers/repository_check/single_repository_worker.rb'
+ - 'app/workers/repository_cleanup_worker.rb'
+ - 'app/workers/repository_fork_worker.rb'
+ - 'app/workers/repository_import_worker.rb'
+ - 'app/workers/run_pipeline_schedule_worker.rb'
+ - 'app/workers/schedule_merge_request_cleanup_refs_worker.rb'
+ - 'app/workers/schedule_migrate_external_diffs_worker.rb'
+ - 'app/workers/service_desk_email_receiver_worker.rb'
+ - 'app/workers/snippets/update_repository_storage_worker.rb'
+ - 'app/workers/ssh_keys/expired_notification_worker.rb'
+ - 'app/workers/ssh_keys/expiring_soon_notification_worker.rb'
+ - 'app/workers/stuck_ci_jobs_worker.rb'
+ - 'app/workers/stuck_export_jobs_worker.rb'
+ - 'app/workers/stuck_merge_jobs_worker.rb'
+ - 'app/workers/system_hook_push_worker.rb'
+ - 'app/workers/todos_destroyer/confidential_issue_worker.rb'
+ - 'app/workers/todos_destroyer/entity_leave_worker.rb'
+ - 'app/workers/todos_destroyer/group_private_worker.rb'
+ - 'app/workers/todos_destroyer/private_features_worker.rb'
+ - 'app/workers/todos_destroyer/project_private_worker.rb'
+ - 'app/workers/trending_projects_worker.rb'
+ - 'app/workers/update_container_registry_info_worker.rb'
+ - 'app/workers/update_external_pull_requests_worker.rb'
+ - 'app/workers/update_highest_role_worker.rb'
+ - 'app/workers/update_merge_requests_worker.rb'
+ - 'app/workers/update_project_statistics_worker.rb'
+ - 'app/workers/upload_checksum_worker.rb'
+ - 'app/workers/user_status_cleanup/batch_worker.rb'
+ - 'app/workers/users/create_statistics_worker.rb'
+ - 'app/workers/users/deactivate_dormant_users_worker.rb'
+ - 'app/workers/users/migrate_records_to_ghost_user_in_batches_worker.rb'
+ - 'app/workers/web_hook_worker.rb'
+ - 'app/workers/wikis/git_garbage_collect_worker.rb'
+ - 'app/workers/x509_issuer_crl_check_worker.rb'
+ - 'config/application.rb'
+ - 'config/initializers/00_deprecations.rb'
+ - 'config/initializers/01_secret_token.rb'
+ - 'config/initializers/0_inject_enterprise_edition_module.rb'
+ - 'config/initializers/0_postgresql_types.rb'
+ - 'config/initializers/1_active_record_data_types.rb'
+ - 'config/initializers/7_redis.rb'
+ - 'config/initializers/active_record_lifecycle.rb'
+ - 'config/initializers/active_record_transaction_observer.rb'
+ - 'config/initializers/carrierwave_performance_patch.rb'
+ - 'config/initializers/database_config.rb'
+ - 'config/initializers/enumerator_next_patch.rb'
+ - 'config/initializers/fix_local_cache_middleware.rb'
+ - 'config/initializers/fog_core_patch.rb'
+ - 'config/initializers/grape_validators.rb'
+ - 'config/initializers/grpc_patch.rb'
+ - 'config/initializers/kaminari_active_record_relation_methods_with_limit.rb'
+ - 'config/initializers/mail_starttls_patch.rb'
+ - 'config/initializers/postgres_cte_as_materialized.rb'
+ - 'config/initializers/rack_VULNDB-255039_patch.rb'
+ - 'config/initializers/rspec_profiling.rb'
+ - 'config/initializers/safe_session_store_patch.rb'
+ - 'config/initializers/sidekiq.rb'
+ - 'config/initializers/warden.rb'
+ - 'config/initializers/wikicloth_redos_patch.rb'
+ - 'config/initializers/wikicloth_ruby_3_patch.rb'
+ - 'config/routes/api.rb'
+ - 'config/routes/group.rb'
+ - 'config/routes/project.rb'
+ - 'danger/ce_ee_vue_templates/Dangerfile'
+ - 'danger/roulette/Dangerfile'
+ - 'db/migrate/20220316022505_create_namespace_details.rb'
+ - 'db/migrate/20220331125725_add_title_to_topic.rb'
+ - 'db/migrate/20220401071609_add_campaign_to_in_product_marketing_email.rb'
+ - 'db/migrate/20220421141342_add_allowed_plans_to_ci_runners.rb'
+ - 'db/migrate/20220503035221_add_gitlab_schema_to_batched_background_migrations.rb'
+ - 'db/migrate/20220513114706_add_jira_connect_application_id_application_setting.rb'
+ - 'db/migrate/20220516092207_add_globally_allowed_ips_to_application_setting.rb'
+ - 'db/migrate/20220531024905_add_operations_access_levels_to_project_feature.rb'
+ - 'db/migrate/20220601091804_add_semver_column_to_ci_runners.rb'
+ - 'db/migrate/20220602130306_add_namespace_type_index.rb'
+ - 'db/migrate/20220617141347_create_ci_secure_file_states.rb'
+ - 'db/migrate/20220726154013_add_component_id_to_sbom_occurrences.rb'
+ - 'db/migrate/20220802200719_add_user_details_profile_fields.rb'
+ - 'db/migrate/20220818125332_add_jitsu_tracking_columns_to_application_settings.rb'
+ - 'db/migrate/20220825105631_add_cube_api_key_to_application_settings.rb'
+ - 'db/migrate/20220902065317_add_partition_id_to_ci_builds.rb'
+ - 'db/migrate/20220914130800_add_jitsu_key_to_projects.rb'
+ - 'db/migrate/20220920135632_add_jira_connect_proxy_url_setting.rb'
+ - 'db/migrate/20220926023734_add_mirror_branch_regex_to_project_settings.rb'
+ - 'db/migrate/20221014034338_populate_releases_access_level_from_repository.rb'
+ - 'db/migrate/20221025043930_change_default_value_on_password_last_changed_at_to_user_details.rb'
+ - 'db/migrate/20221101032521_add_default_preferred_language_to_application_settings.rb'
+ - 'db/migrate/20221111123146_add_onboarding_in_progress_to_users.rb'
+ - 'db/migrate/20221111123147_add_onboarding_step_url_to_user_details.rb'
+ - 'db/migrate/20221114131943_add_short_title_to_appearances.rb'
+ - 'db/migrate/20221128155738_add_discord_to_user_details.rb'
+ - 'db/migrate/20221212192452_add_uuid_column_to_sbom_occurrences.rb'
+ - 'db/migrate/20221219103007_add_name_to_ml_candidates.rb'
+ - 'db/migrate/20221223114543_add_pwa_icon_to_appearances.rb'
+ - 'db/migrate/20221228072549_add_pwa_attributes_to_appearances.rb'
+ - 'db/migrate/20230102131000_add_smtp_credentials_to_service_desk_settings.rb'
+ - 'db/migrate/20230102180341_add_merge_request_meta_to_merge_requests_compliance_violations.rb'
+ - 'db/migrate/20230111132621_unpartition_pm_package_metadata_tables.rb'
+ - 'db/migrate/20230116143310_add_pages_unique_domain_columns_to_project_settings.rb'
+ - 'db/migrate/20230119151636_add_url_hash_to_web_hook_logs.rb'
+ - 'db/migrate/20230119214643_add_deactivation_email_additional_text_to_application_settings.rb'
+ - 'db/migrate/20230127155217_add_id_column_to_package_metadata_join_table.rb'
+ - 'db/migrate/20230130125541_add_attempts_and_last_error_to_postgres_async_indexes.rb'
+ - 'db/migrate/20230214142813_remove_ci_job_artifacts_original_filename.rb'
+ - 'db/migrate/20230216144719_drop_table_airflow_dags.rb'
+ - 'db/migrate/20230222161226_add_custom_jira_regex_to_jira_tracker_data.rb'
+ - 'db/migrate/20230228133011_add_design_description.rb'
+ - 'db/migrate/20230306145230_add_product_analytics_data_collector_host_to_application_settings.rb'
+ - 'db/migrate/20230313181536_create_packages_npm_metadata_caches.rb'
+ - 'db/migrate/20230315053635_add_screenshot_to_abuse_reports.rb'
+ - 'db/migrate/20230323140745_add_root_directory_to_pages_deployment.rb'
+ - 'db/migrate/20230329235300_add_diagramsnet_to_application_settings.rb'
+ - 'db/migrate/20230405071033_add_object_storage_key_to_packages_npm_metadata_caches.rb'
+ - 'db/migrate/20230406060452_create_instance_external_audit_event_destinations.rb'
+ - 'db/migrate/20230410092450_add_product_analytics_instrumentation_key_to_project_settings.rb'
+ - 'db/migrate/20230412151659_add_ci_job_artifacts_file_final_path.rb'
+ - 'db/migrate/20230503100753_add_version_format_and_data_type_to_checkpoints.rb'
+ - 'db/migrate/20230507192028_create_audit_events_google_cloud_logging_configurations.rb'
+ - 'db/migrate/20230509072635_drop_unused_sequence_by_recreating_vsa_table.rb'
+ - 'db/migrate/20230509115525_add_name_to_organization.rb'
+ - 'db/migrate/20230519112106_add_diff_column_to_schema_inconsistencies.rb'
+ - 'db/migrate/20230522162742_cleanup_bigint_conversion_for_merge_request_metrics_for_self_hosts.rb'
+ - 'db/migrate/20230529173607_add_id_column_to_pm_checkpoints.rb'
+ - 'db/migrate/20230529182720_recreate_billable_index.rb'
+ - 'db/migrate/20230529184716_recreated_activity_index.rb'
+ - 'db/migrate/20230530112122_add_path_to_organizations.rb'
+ - 'db/migrate/20230601090722_add_status_message_to_packages.rb'
+ - 'db/migrate/20230605043258_add_unconfirmed_created_at_index_to_users.rb'
+ - 'db/migrate/20230612074428_add_name_to_external_audit_event_destination.rb'
+ - 'db/migrate/20230612091747_add_name_to_instance_audit_event_destination.rb'
+ - 'db/migrate/20230614180651_add_organization_id_to_namespaces.rb'
+ - 'db/migrate/20230621072726_add_description_to_ci_variable.rb'
+ - 'db/migrate/20230621083004_add_description_to_ci_group_variable.rb'
+ - 'db/migrate/20230706130217_add_column_model_id_to_ml_experiments.rb'
+ - 'db/migrate/20230710160232_add_expires_at_to_service_access_tokens.rb'
+ - 'db/migrate/20230718145747_create_target_branch_rules.rb'
+ - 'db/migrate/20230726104022_add_name_to_google_cloud_logging_configuration.rb'
+ - 'db/migrate/20230809165212_add_path_prefix_and_build_ref_to_pages_deployments.rb'
+ - 'db/migrate/20230822064649_add_organization_id_to_project.rb'
+ - 'db/migrate/20230906185552_add_markdown_fields_to_review_llm_summary.rb'
+ - 'db/migrate/20230906204935_restart_self_hosted_sent_notifications_backfill.rb'
+ - 'db/migrate/20230915103259_create_ci_finished_build_ch_sync_events.rb'
+ - 'db/migrate/20230917144717_add_package_name_pattern_query_to_packages_protection_rule.rb'
+ - 'db/migrate/20230921081527_add_queued_migration_version_to_batched_background_migrations.rb'
+ - 'db/migrate/20231017135207_add_fields_to_ml_model.rb'
+ - 'db/migrate/20231019180421_add_name_description_to_catalog_resources.rb'
+ - 'db/migrate/20231024142236_add_fields_to_bulk_import_failures.rb'
+ - 'db/post_migrate/20220328100456_schedule20220328_reset_duplicate_ci_runners_token_encrypted_values_on_projects.rb'
+ - 'db/post_migrate/20220328100457_schedule20220328_reset_duplicate_ci_runners_token_values_on_projects.rb'
+ - 'db/post_migrate/20220504083836_cleanup_after_fixing_regression_with_new_users_emails.rb'
+ - 'db/post_migrate/20220530082653_add_traversal_id_type_group_index.rb'
+ - 'db/post_migrate/20220617123022_add_unique_index_on_projects_on_runners_token.rb'
+ - 'db/post_migrate/20220617123034_add_unique_index_on_projects_on_runners_token_encrypted.rb'
+ - 'db/post_migrate/20220628111752_drop_token_index_from_ci_builds.rb'
+ - 'db/post_migrate/20220630085003_drop_project_successfull_pages_deploy_index_from_ci_builds.rb'
+ - 'db/post_migrate/20220719081542_drop_queued_at_index_from_ci_builds.rb'
+ - 'db/post_migrate/20220720090354_remove_pending_builds_covering_index_from_ci_builds.rb'
+ - 'db/post_migrate/20220804235614_add_comment_to_vulnerability_state_transitions.rb'
+ - 'db/post_migrate/20220822090656_drop_build_coverage_regex_from_project.rb'
+ - 'db/post_migrate/20220830061704_orphaned_invited_members_cleanup.rb'
+ - 'db/post_migrate/20220901071355_cleanup_attention_request_user_callouts.rb'
+ - 'db/post_migrate/20220902204048_move_security_findings_table_to_gitlab_partitions_dynamic_schema.rb'
+ - 'db/post_migrate/20220919023208_drop_unused_fields_from_merge_request_assignees.rb'
+ - 'db/post_migrate/20220919080303_delete_migrate_shared_vulnerability_scanners.rb'
+ - 'db/post_migrate/20220920135356_tiebreak_user_type_index.rb'
+ - 'db/post_migrate/20221006172302_adjust_task_note_rename_background_migration_values.rb'
+ - 'db/post_migrate/20221013154159_update_invalid_dormant_user_setting.rb'
+ - 'db/post_migrate/20221018232820_add_temp_index_for_user_details_fields.rb'
+ - 'db/post_migrate/20221102231131_remove_temp_index_for_user_details_fields.rb'
+ - 'db/post_migrate/20221115184525_remove_namespaces_tmp_project_id_column.rb'
+ - 'db/post_migrate/20221220131020_bump_default_partition_id_value_for_ci_tables.rb'
+ - 'db/post_migrate/20221221150123_update_billable_users_index.rb'
+ - 'db/post_migrate/20230104103748_remove_new_amount_used_column.rb'
+ - 'db/post_migrate/20230105180002_remove_new_amount_used_column_on_ci_namespace_monthly_usages.rb'
+ - 'db/post_migrate/20230110172751_add_partial_index_on_group_path_id.rb'
+ - 'db/post_migrate/20230117114739_clear_duplicate_jobs_cookies.rb'
+ - 'db/post_migrate/20230123095023_add_scan_result_policy_id_to_software_license_policies.rb'
+ - 'db/post_migrate/20230131184319_update_billable_users_index_for_service_accounts.rb'
+ - 'db/post_migrate/20230201082038_drop_web_hook_calls_high_column.rb'
+ - 'db/post_migrate/20230303154314_add_user_type_migration_indexes.rb'
+ - 'db/post_migrate/20230310111859_recreate_user_type_migration_indexes.rb'
+ - 'db/post_migrate/20230313150531_reschedule_migration_for_remediation.rb'
+ - 'db/post_migrate/20230314094215_drop_u2f_registrations_table.rb'
+ - 'db/post_migrate/20230322151635_cleanup_bigint_conversion_for_merge_request_metrics.rb'
+ - 'db/post_migrate/20230405200858_requeue_backfill_project_wiki_repositories.rb'
+ - 'db/post_migrate/20230420120431_create_namespaces_by_top_level_namespace_index.rb'
+ - 'db/post_migrate/20230427194552_drop_cycle_analytics_unused_tables.rb'
+ - 'db/post_migrate/20230502134532_drop_clusters_applications_cilium.rb'
+ - 'db/post_migrate/20230502193525_drop_clusters_applications_helm.rb'
+ - 'db/post_migrate/20230502201251_drop_clusters_applications_ingress.rb'
+ - 'db/post_migrate/20230508150219_reschedule_evidences_handling_unicode.rb'
+ - 'db/post_migrate/20230516123202_create_routing_table_for_ci_builds.rb'
+ - 'db/post_migrate/20230522111534_reschedule_migration_for_links_from_metadata.rb'
+ - 'db/post_migrate/20230528203340_drop_message_from_vulnerability_occurrences.rb'
+ - 'db/post_migrate/20230530015535_swap_notes_id_to_bigint_for_gitlab_dot_com.rb'
+ - 'db/post_migrate/20230607165718_drop_project_wiki_repository_states.rb'
+ - 'db/post_migrate/20230614182049_add_index_to_namespaces_organization_id.rb'
+ - 'db/post_migrate/20230619005223_change_unconfirmed_created_at_index_on_users.rb'
+ - 'db/post_migrate/20230711093010_drop_default_partition_id_value_for_ci_tables.rb'
+ - 'db/post_migrate/20230724123547_cleanup_conversion_big_int_ci_build_needs_self_managed.rb'
+ - 'db/post_migrate/20230724150939_index_projects_on_namespace_id_and_repository_size_limit.rb'
+ - 'db/post_migrate/20230728151058_add_auditor_index_to_users_table.rb'
+ - 'db/post_migrate/20230823145126_swap_notes_id_to_bigint_for_self_managed.rb'
+ - 'db/post_migrate/20230908033511_swap_columns_for_ci_pipeline_chat_data_pipeline_id_bigint.rb'
+ - 'db/post_migrate/20230913130629_index_org_id_on_projects.rb'
+ - 'db/post_migrate/20230924134453_cleanup_uuid_type_migration_on_vulnerability_occurrences.rb'
+ - 'db/post_migrate/20231003045342_migrate_sidekiq_namespaced_jobs.rb'
+ - 'db/post_migrate/20231003142534_add_build_timeout_index.rb'
+ - 'db/post_migrate/20231005131445_add_work_items_related_link_restrictions.rb'
+ - 'db/post_migrate/20231009105056_index_users_on_email_domain_and_id.rb'
+ - 'db/post_migrate/20231017064317_swap_columns_for_ci_pipeline_variables_pipeline_id_bigint.rb'
+ - 'db/post_migrate/20231017172156_add_index_on_projects_for_adjourned_deletion.rb'
+ - 'db/post_migrate/20231018083247_remove_users_email_opted_in_columns.rb'
+ - 'db/post_migrate/20231019003052_swap_columns_for_ci_pipelines_pipeline_id_bigint_v2.rb'
+ - 'db/post_migrate/20231019084731_swap_columns_for_ci_stages_pipeline_id_bigint_v2.rb'
+ - 'db/post_migrate/20231024025629_cleanup_ci_pipeline_chat_data_pipeline_id_bigint.rb'
+ - 'db/post_migrate/20231024080150_cleanup_ci_sources_pipelines_pipeline_id_bigint.rb'
+ - 'db/post_migrate/20231025031337_cleanup_ci_pipeline_messages_pipeline_id_bigint.rb'
+ - 'ee/app/controllers/admin/application_settings/scim_oauth_controller.rb'
+ - 'ee/app/controllers/admin/emails_controller.rb'
+ - 'ee/app/controllers/admin/namespace_limits_controller.rb'
+ - 'ee/app/controllers/admin/push_rules_controller.rb'
+ - 'ee/app/controllers/concerns/credentials_inventory_actions.rb'
+ - 'ee/app/controllers/concerns/iteration_cadences_actions.rb'
+ - 'ee/app/controllers/concerns/microsoft_application_actions.rb'
+ - 'ee/app/controllers/ee/admin/application_controller.rb'
+ - 'ee/app/controllers/ee/admin/application_settings_controller.rb'
+ - 'ee/app/controllers/ee/admin/dashboard_controller.rb'
+ - 'ee/app/controllers/ee/admin/groups_controller.rb'
+ - 'ee/app/controllers/ee/admin/users_controller.rb'
+ - 'ee/app/controllers/ee/application_controller.rb'
+ - 'ee/app/controllers/ee/dashboard/projects_controller.rb'
+ - 'ee/app/controllers/ee/groups/group_members_controller.rb'
+ - 'ee/app/controllers/ee/groups/settings/ci_cd_controller.rb'
+ - 'ee/app/controllers/ee/groups/settings/repository_controller.rb'
+ - 'ee/app/controllers/ee/projects/deploy_tokens_controller.rb'
+ - 'ee/app/controllers/ee/projects/merge_requests/application_controller.rb'
+ - 'ee/app/controllers/ee/projects/security/configuration_controller.rb'
+ - 'ee/app/controllers/ee/projects/settings/ci_cd_controller.rb'
+ - 'ee/app/controllers/ee/projects/settings/repository_controller.rb'
+ - 'ee/app/controllers/ee/projects_controller.rb'
+ - 'ee/app/controllers/ee/repositories/git_http_client_controller.rb'
+ - 'ee/app/controllers/ee/repositories/git_http_controller.rb'
+ - 'ee/app/controllers/ee/search_controller.rb'
+ - 'ee/app/controllers/ee/sessions_controller.rb'
+ - 'ee/app/controllers/ee/users_controller.rb'
+ - 'ee/app/controllers/groups/analytics/dashboards_controller.rb'
+ - 'ee/app/controllers/groups/analytics/productivity_analytics_controller.rb'
+ - 'ee/app/controllers/groups/epics/notes_controller.rb'
+ - 'ee/app/controllers/groups/epics_controller.rb'
+ - 'ee/app/controllers/groups/ldap_group_links_controller.rb'
+ - 'ee/app/controllers/groups/ldaps_controller.rb'
+ - 'ee/app/controllers/groups/scim_oauth_controller.rb'
+ - 'ee/app/controllers/groups/todos_controller.rb'
+ - 'ee/app/controllers/oauth/geo_auth_controller.rb'
+ - 'ee/app/controllers/projects/approver_groups_controller.rb'
+ - 'ee/app/controllers/projects/approvers_controller.rb'
+ - 'ee/app/controllers/projects/path_locks_controller.rb'
+ - 'ee/app/controllers/projects/quality/test_cases_controller.rb'
+ - 'ee/app/controllers/registrations/welcome_controller.rb'
+ - 'ee/app/controllers/sitemap_controller.rb'
+ - 'ee/app/finders/approval_rules/group_finder.rb'
+ - 'ee/app/finders/autocomplete/group_subgroups_finder.rb'
+ - 'ee/app/finders/autocomplete/project_invited_groups_finder.rb'
+ - 'ee/app/finders/autocomplete/vulnerabilities_autocomplete_finder.rb'
+ - 'ee/app/finders/boards/milestones_finder.rb'
+ - 'ee/app/finders/boards/users_finder.rb'
+ - 'ee/app/finders/concerns/ee/finder_with_group_hierarchy.rb'
+ - 'ee/app/finders/concerns/epics/findable.rb'
+ - 'ee/app/finders/concerns/epics/with_access_check.rb'
+ - 'ee/app/finders/dast/profiles_finder.rb'
+ - 'ee/app/finders/dast_site_validations_finder.rb'
+ - 'ee/app/finders/ee/fork_targets_finder.rb'
+ - 'ee/app/finders/ee/group_members_finder.rb'
+ - 'ee/app/finders/ee/issuables/label_filter.rb'
+ - 'ee/app/finders/ee/issues_finder.rb'
+ - 'ee/app/finders/ee/merge_requests_finder.rb'
+ - 'ee/app/finders/ee/notes_finder.rb'
+ - 'ee/app/finders/epics/with_issues_finder.rb'
+ - 'ee/app/finders/geo/framework_registry_finder.rb'
+ - 'ee/app/finders/geo/registry_finder.rb'
+ - 'ee/app/finders/gitlab_subscriptions/add_on_eligible_users_finder.rb'
+ - 'ee/app/finders/group_saml_identity_finder.rb'
+ - 'ee/app/finders/groups_with_templates_finder.rb'
+ - 'ee/app/finders/iterations_finder.rb'
+ - 'ee/app/finders/licenses_finder.rb'
+ - 'ee/app/finders/merge_requests/by_approvers_finder.rb'
+ - 'ee/app/finders/namespaces/billed_users_finder.rb'
+ - 'ee/app/finders/namespaces/free_user_cap/users_finder.rb'
+ - 'ee/app/finders/okrs/checkin_reminder_key_result_finder.rb'
+ - 'ee/app/finders/productivity_analytics_finder.rb'
+ - 'ee/app/finders/projects/integrations/jira/by_ids_finder.rb'
+ - 'ee/app/finders/projects/integrations/jira/issues_finder.rb'
+ - 'ee/app/finders/security/approval_groups_finder.rb'
+ - 'ee/app/finders/security/findings_finder.rb'
+ - 'ee/app/finders/security/pure_findings_finder.rb'
+ - 'ee/app/finders/security/scan_policy_base_finder.rb'
+ - 'ee/app/finders/status_page/incident_comments_finder.rb'
+ - 'ee/app/finders/status_page/incidents_finder.rb'
+ - 'ee/app/graphql/ee/types/issue_connection_type.rb'
+ - 'ee/app/graphql/ee/types/merge_request_type.rb'
+ - 'ee/app/graphql/ee/types/repository/code_owner_error_type.rb'
+ - 'ee/app/graphql/ee/types/repository/code_owner_validation_type.rb'
+ - 'ee/app/graphql/mutations/ci/namespace_ci_cd_settings_update.rb'
+ - 'ee/app/graphql/mutations/dast_on_demand_scans/create.rb'
+ - 'ee/app/graphql/mutations/geo/registries/update.rb'
+ - 'ee/app/graphql/mutations/iterations/update.rb'
+ - 'ee/app/graphql/mutations/projects/set_locked.rb'
+ - 'ee/app/graphql/mutations/requirements_management/export_requirements.rb'
+ - 'ee/app/graphql/resolvers/analytics/contribution_analytics/contributions_resolver.rb'
+ - 'ee/app/graphql/resolvers/board_groupings/epics_resolver.rb'
+ - 'ee/app/graphql/resolvers/ci/runners_jobs_statistics_resolver.rb'
+ - 'ee/app/graphql/resolvers/iterations_resolver.rb'
+ - 'ee/app/graphql/resolvers/requirements_management/requirements_resolver.rb'
+ - 'ee/app/graphql/resolvers/vulnerabilities_base_resolver.rb'
+ - 'ee/app/graphql/types/admin/cloud_licenses/current_license_type.rb'
+ - 'ee/app/graphql/types/admin/cloud_licenses/license_history_entry_type.rb'
+ - 'ee/app/graphql/types/admin/cloud_licenses/subscription_future_entry_type.rb'
+ - 'ee/app/graphql/types/ai/message_extras_type.rb'
+ - 'ee/app/graphql/types/ai/message_type.rb'
+ - 'ee/app/graphql/types/ai/project_conversations_type.rb'
+ - 'ee/app/graphql/types/ai/prompt/ai_prompt_type.rb'
+ - 'ee/app/graphql/types/ai/prompt/explain_vulnerability_prompt_type.rb'
+ - 'ee/app/graphql/types/ai/prompt/explain_vulnerability_prompt_type/presubmission_check_results_type.rb'
+ - 'ee/app/graphql/types/analytics/contribution_analytics/contribution_metadata_type.rb'
+ - 'ee/app/graphql/types/analytics/devops_adoption/enabled_namespace_type.rb'
+ - 'ee/app/graphql/types/analytics/devops_adoption/snapshot_type.rb'
+ - 'ee/app/graphql/types/analytics/value_stream_dashboard/count_type.rb'
+ - 'ee/app/graphql/types/app_sec/fuzzing/api/ci_configuration_type.rb'
+ - 'ee/app/graphql/types/app_sec/fuzzing/api/scan_profile_type.rb'
+ - 'ee/app/graphql/types/applied_ml/suggested_reviewers_type.rb'
+ - 'ee/app/graphql/types/boards/board_epic_type.rb'
+ - 'ee/app/graphql/types/boards/epic_list_metadata_type.rb'
+ - 'ee/app/graphql/types/boards/epic_list_type.rb'
+ - 'ee/app/graphql/types/boards/epic_user_preferences_type.rb'
+ - 'ee/app/graphql/types/branch_protections/unprotect_access_level_type.rb'
+ - 'ee/app/graphql/types/burnup_chart_daily_totals_type.rb'
+ - 'ee/app/graphql/types/ci/code_coverage_activity_type.rb'
+ - 'ee/app/graphql/types/ci/code_coverage_summary_type.rb'
+ - 'ee/app/graphql/types/ci/code_quality_degradation_type.rb'
+ - 'ee/app/graphql/types/ci/jobs_duration_statistics_type.rb'
+ - 'ee/app/graphql/types/ci/jobs_statistics_type.rb'
+ - 'ee/app/graphql/types/ci/minutes/namespace_monthly_usage_type.rb'
+ - 'ee/app/graphql/types/ci/minutes/project_monthly_usage_type.rb'
+ - 'ee/app/graphql/types/ci/queueing_history_time_series_type.rb'
+ - 'ee/app/graphql/types/dast/profile_cadence_type.rb'
+ - 'ee/app/graphql/types/deployments/approval_summary_type.rb'
+ - 'ee/app/graphql/types/deployments/approval_type.rb'
+ - 'ee/app/graphql/types/dora/performance_score_connection_type.rb'
+ - 'ee/app/graphql/types/dora/performance_score_count_type.rb'
+ - 'ee/app/graphql/types/dora_metric_type.rb'
+ - 'ee/app/graphql/types/dora_type.rb'
+ - 'ee/app/graphql/types/epic_descendant_count_type.rb'
+ - 'ee/app/graphql/types/epic_descendant_weight_sum_type.rb'
+ - 'ee/app/graphql/types/epic_health_status_type.rb'
+ - 'ee/app/graphql/types/epic_issue_type.rb'
+ - 'ee/app/graphql/types/external_issue_type.rb'
+ - 'ee/app/graphql/types/forecasting/datapoint_type.rb'
+ - 'ee/app/graphql/types/forecasting/forecast_type.rb'
+ - 'ee/app/graphql/types/geo/registry_class_enum.rb'
+ - 'ee/app/graphql/types/gitlab_subscriptions/preview_billable_user_change_type.rb'
+ - 'ee/app/graphql/types/incident_management/escalation_rule_input_type.rb'
+ - 'ee/app/graphql/types/incident_management/escalation_rule_type.rb'
+ - 'ee/app/graphql/types/incident_management/oncall_participant_type.rb'
+ - 'ee/app/graphql/types/incident_management/oncall_rotation_active_period_input_type.rb'
+ - 'ee/app/graphql/types/incident_management/oncall_rotation_active_period_type.rb'
+ - 'ee/app/graphql/types/incident_management/oncall_shift_type.rb'
+ - 'ee/app/graphql/types/member_roles/customizable_permission_type.rb'
+ - 'ee/app/graphql/types/namespaces/namespace_ban_type.rb'
+ - 'ee/app/graphql/types/network_policy_type.rb'
+ - 'ee/app/graphql/types/path_lock_type.rb'
+ - 'ee/app/graphql/types/product_analytics/category_enum.rb'
+ - 'ee/app/graphql/types/product_analytics/panel_type.rb'
+ - 'ee/app/graphql/types/product_analytics/state_enum.rb'
+ - 'ee/app/graphql/types/product_analytics/visualization_type.rb'
+ - 'ee/app/graphql/types/protected_environment_type.rb'
+ - 'ee/app/graphql/types/protected_environments/approval_rule_for_summary_type.rb'
+ - 'ee/app/graphql/types/protected_environments/approval_rule_type.rb'
+ - 'ee/app/graphql/types/protected_environments/authorizable_type.rb'
+ - 'ee/app/graphql/types/protected_environments/deploy_access_level_type.rb'
+ - 'ee/app/graphql/types/requirements_management/requirement_states_count_type.rb'
+ - 'ee/app/graphql/types/sbom/license_type.rb'
+ - 'ee/app/graphql/types/sbom/location_type.rb'
+ - 'ee/app/graphql/types/scan_type.rb'
+ - 'ee/app/graphql/types/scanned_resource_type.rb'
+ - 'ee/app/graphql/types/security/training_type.rb'
+ - 'ee/app/graphql/types/security_orchestration/approval_group_type.rb'
+ - 'ee/app/graphql/types/security_orchestration/approval_scan_result_policy_type.rb'
+ - 'ee/app/graphql/types/security_orchestration/group_security_policy_source_type.rb'
+ - 'ee/app/graphql/types/security_orchestration/project_security_policy_source_type.rb'
+ - 'ee/app/graphql/types/security_orchestration/scan_execution_policy_type.rb'
+ - 'ee/app/graphql/types/security_orchestration/scan_result_policy_type.rb'
+ - 'ee/app/graphql/types/security_policy_validation_error.rb'
+ - 'ee/app/graphql/types/security_report_summary_section_type.rb'
+ - 'ee/app/graphql/types/security_report_summary_type.rb'
+ - 'ee/app/graphql/types/security_scanners.rb'
+ - 'ee/app/graphql/types/time_report_stats_type.rb'
+ - 'ee/app/graphql/types/timebox_error_type.rb'
+ - 'ee/app/graphql/types/timebox_metrics_type.rb'
+ - 'ee/app/graphql/types/timebox_report_type.rb'
+ - 'ee/app/graphql/types/vulnerabilities/asset_type.rb'
+ - 'ee/app/graphql/types/vulnerabilities/container_image_type.rb'
+ - 'ee/app/graphql/types/vulnerabilities/link_type.rb'
+ - 'ee/app/graphql/types/vulnerabilities/remediation_type.rb'
+ - 'ee/app/graphql/types/vulnerabilities_count_by_day_type.rb'
+ - 'ee/app/graphql/types/vulnerability_details/base_type.rb'
+ - 'ee/app/graphql/types/vulnerability_details/boolean_type.rb'
+ - 'ee/app/graphql/types/vulnerability_details/code_type.rb'
+ - 'ee/app/graphql/types/vulnerability_details/commit_type.rb'
+ - 'ee/app/graphql/types/vulnerability_details/diff_type.rb'
+ - 'ee/app/graphql/types/vulnerability_details/file_location_type.rb'
+ - 'ee/app/graphql/types/vulnerability_details/int_type.rb'
+ - 'ee/app/graphql/types/vulnerability_details/list_type.rb'
+ - 'ee/app/graphql/types/vulnerability_details/markdown_type.rb'
+ - 'ee/app/graphql/types/vulnerability_details/module_location_type.rb'
+ - 'ee/app/graphql/types/vulnerability_details/named_list_item_type.rb'
+ - 'ee/app/graphql/types/vulnerability_details/named_list_type.rb'
+ - 'ee/app/graphql/types/vulnerability_details/row_type.rb'
+ - 'ee/app/graphql/types/vulnerability_details/table_type.rb'
+ - 'ee/app/graphql/types/vulnerability_details/text_type.rb'
+ - 'ee/app/graphql/types/vulnerability_details/url_type.rb'
+ - 'ee/app/graphql/types/vulnerability_evidence_source_type.rb'
+ - 'ee/app/graphql/types/vulnerability_evidence_supporting_message_type.rb'
+ - 'ee/app/graphql/types/vulnerability_evidence_type.rb'
+ - 'ee/app/graphql/types/vulnerability_identifier_type.rb'
+ - 'ee/app/graphql/types/vulnerability_location/cluster_image_scanning_type.rb'
+ - 'ee/app/graphql/types/vulnerability_location/container_scanning_type.rb'
+ - 'ee/app/graphql/types/vulnerability_location/coverage_fuzzing_type.rb'
+ - 'ee/app/graphql/types/vulnerability_location/dast_type.rb'
+ - 'ee/app/graphql/types/vulnerability_location/dependency_scanning_type.rb'
+ - 'ee/app/graphql/types/vulnerability_location/generic_type.rb'
+ - 'ee/app/graphql/types/vulnerability_location/sast_type.rb'
+ - 'ee/app/graphql/types/vulnerability_location/secret_detection_type.rb'
+ - 'ee/app/graphql/types/vulnerability_request_response_header_type.rb'
+ - 'ee/app/graphql/types/vulnerability_request_type.rb'
+ - 'ee/app/graphql/types/vulnerability_response_type.rb'
+ - 'ee/app/graphql/types/vulnerability_severities_count_type.rb'
+ - 'ee/app/graphql/types/vulnerable_dependency_type.rb'
+ - 'ee/app/graphql/types/vulnerable_kubernetes_resource_type.rb'
+ - 'ee/app/graphql/types/vulnerable_package_type.rb'
+ - 'ee/app/graphql/types/work_items/widgets/health_status_type.rb'
+ - 'ee/app/graphql/types/work_items/widgets/iteration_type.rb'
+ - 'ee/app/graphql/types/work_items/widgets/progress_type.rb'
+ - 'ee/app/graphql/types/work_items/widgets/requirement_legacy_type.rb'
+ - 'ee/app/graphql/types/work_items/widgets/status_type.rb'
+ - 'ee/app/graphql/types/work_items/widgets/test_reports_type.rb'
+ - 'ee/app/graphql/types/work_items/widgets/weight_type.rb'
+ - 'ee/app/helpers/admin/application_settings_helper.rb'
+ - 'ee/app/helpers/ee/application_helper.rb'
+ - 'ee/app/helpers/ee/boards_helper.rb'
+ - 'ee/app/helpers/ee/gitlab_routing_helper.rb'
+ - 'ee/app/helpers/ee/kerberos_helper.rb'
+ - 'ee/app/helpers/projects/on_demand_scans_helper.rb'
+ - 'ee/app/helpers/push_rules_helper.rb'
+ - 'ee/app/models/ai/ai_resource/concerns/noteable.rb'
+ - 'ee/app/models/ai/ai_resource/epic.rb'
+ - 'ee/app/models/ai/ai_resource/issue.rb'
+ - 'ee/app/models/ai/job_failure_analysis.rb'
+ - 'ee/app/models/approval_project_rules_user.rb'
+ - 'ee/app/models/approver.rb'
+ - 'ee/app/models/approver_group.rb'
+ - 'ee/app/models/ci/finished_build_ch_sync_event.rb'
+ - 'ee/app/models/ci/minutes/namespace_monthly_usage.rb'
+ - 'ee/app/models/concerns/elastic/application_versioned_search.rb'
+ - 'ee/app/models/concerns/epics/metadata_cache_update.rb'
+ - 'ee/app/models/concerns/geo/blob_replicator_strategy.rb'
+ - 'ee/app/models/dast/branch.rb'
+ - 'ee/app/models/ee/analytics/cycle_analytics/issue_stage_event.rb'
+ - 'ee/app/models/ee/epic.rb'
+ - 'ee/app/models/ee/group.rb'
+ - 'ee/app/models/ee/member.rb'
+ - 'ee/app/models/ee/merge_request.rb'
+ - 'ee/app/models/ee/namespace.rb'
+ - 'ee/app/models/ee/namespace_setting.rb'
+ - 'ee/app/models/ee/project.rb'
+ - 'ee/app/models/ee/project_group_link.rb'
+ - 'ee/app/models/ee/project_member.rb'
+ - 'ee/app/models/ee/user.rb'
+ - 'ee/app/models/ee/vulnerability.rb'
+ - 'ee/app/models/elastic/migration_record.rb'
+ - 'ee/app/models/elasticsearch_indexed_namespace.rb'
+ - 'ee/app/models/elasticsearch_indexed_project.rb'
+ - 'ee/app/models/geo/container_repository_registry.rb'
+ - 'ee/app/models/geo/event_log.rb'
+ - 'ee/app/models/geo/upload_registry.rb'
+ - 'ee/app/models/geo_node.rb'
+ - 'ee/app/models/geo_node_status.rb'
+ - 'ee/app/models/incident_management/oncall_rotation.rb'
+ - 'ee/app/models/iteration.rb'
+ - 'ee/app/models/license.rb'
+ - 'ee/app/models/members/member_role.rb'
+ - 'ee/app/models/merge_request/diff_llm_summary.rb'
+ - 'ee/app/models/merge_request/predictions.rb'
+ - 'ee/app/models/merge_request/review_llm_summary.rb'
+ - 'ee/app/models/merge_requests/compliance_violation.rb'
+ - 'ee/app/models/merge_trains/car.rb'
+ - 'ee/app/models/package_metadata/package.rb'
+ - 'ee/app/models/project_security_setting.rb'
+ - 'ee/app/models/protected_environments/approval_rules/summarizable.rb'
+ - 'ee/app/models/protected_environments/authorizable.rb'
+ - 'ee/app/models/requirements_management/requirement.rb'
+ - 'ee/app/models/sbom/occurrence.rb'
+ - 'ee/app/models/search/namespace_index_assignment.rb'
+ - 'ee/app/models/security/training.rb'
+ - 'ee/app/models/vulnerabilities/finding.rb'
+ - 'ee/app/policies/merge_request/diff_llm_summary_policy.rb'
+ - 'ee/app/policies/merge_request/review_llm_summary_policy.rb'
+ - 'ee/app/policies/merge_request_diff_policy.rb'
+ - 'ee/app/policies/path_lock_policy.rb'
+ - 'ee/app/replicators/geo/container_repository_replicator.rb'
+ - 'ee/app/serializers/dashboard_environments_serializer.rb'
+ - 'ee/app/serializers/ee/discussion_serializer.rb'
+ - 'ee/app/serializers/epic_ai_entity.rb'
+ - 'ee/app/services/analytics/cycle_analytics/consistency_check_service.rb'
+ - 'ee/app/services/analytics/cycle_analytics/data_loader_service.rb'
+ - 'ee/app/services/analytics/devops_adoption/enabled_namespaces/find_or_create_service.rb'
+ - 'ee/app/services/analytics/value_stream_dashboard/top_level_group_counter_service.rb'
+ - 'ee/app/services/app_sec/dast/pre_scan_verification_steps/find_or_create_service.rb'
+ - 'ee/app/services/app_sec/dast/site_profile_secret_variables/create_or_update_service.rb'
+ - 'ee/app/services/app_sec/dast/site_profiles/update_service.rb'
+ - 'ee/app/services/app_sec/dast/site_tokens/find_or_create_service.rb'
+ - 'ee/app/services/app_sec/dast/sites/find_or_create_service.rb'
+ - 'ee/app/services/approval_rules/params_filtering_service.rb'
+ - 'ee/app/services/billable_members/destroy_service.rb'
+ - 'ee/app/services/ci/minutes/additional_packs/base_service.rb'
+ - 'ee/app/services/ci/minutes/additional_packs/create_service.rb'
+ - 'ee/app/services/ci/minutes/refresh_cached_data_service.rb'
+ - 'ee/app/services/ci/minutes/reset_usage_service.rb'
+ - 'ee/app/services/ci/minutes/update_project_and_namespace_usage_service.rb'
+ - 'ee/app/services/ci/runners/stale_group_runners_prune_service.rb'
+ - 'ee/app/services/click_house/data_ingestion/ci_finished_builds_sync_service.rb'
+ - 'ee/app/services/compliance_management/violations/export_service.rb'
+ - 'ee/app/services/dependencies/export_serializers/group_dependencies_service.rb'
+ - 'ee/app/services/deployments/approval_service.rb'
+ - 'ee/app/services/dora/aggregate_scores_service.rb'
+ - 'ee/app/services/ee/audit_event_service.rb'
+ - 'ee/app/services/ee/audit_events/build_service.rb'
+ - 'ee/app/services/ee/auth/container_registry_authentication_service.rb'
+ - 'ee/app/services/ee/boards/base_service.rb'
+ - 'ee/app/services/ee/boards/issues/list_service.rb'
+ - 'ee/app/services/ee/boards/lists/create_service.rb'
+ - 'ee/app/services/ee/ci/queue/build_queue_service.rb'
+ - 'ee/app/services/ee/groups/autocomplete_service.rb'
+ - 'ee/app/services/ee/groups/destroy_service.rb'
+ - 'ee/app/services/ee/groups/update_service.rb'
+ - 'ee/app/services/ee/issuable_base_service.rb'
+ - 'ee/app/services/ee/keys/create_service.rb'
+ - 'ee/app/services/ee/labels/promote_service.rb'
+ - 'ee/app/services/ee/lfs/unlock_file_service.rb'
+ - 'ee/app/services/ee/members/create_service.rb'
+ - 'ee/app/services/ee/members/creator_service.rb'
+ - 'ee/app/services/ee/merge_requests/merge_base_service.rb'
+ - 'ee/app/services/ee/merge_requests/refresh_service.rb'
+ - 'ee/app/services/ee/milestones/promote_service.rb'
+ - 'ee/app/services/ee/notification_recipients/builder/base.rb'
+ - 'ee/app/services/ee/projects/create_from_template_service.rb'
+ - 'ee/app/services/ee/projects/create_service.rb'
+ - 'ee/app/services/ee/projects/destroy_service.rb'
+ - 'ee/app/services/ee/projects/fork_service.rb'
+ - 'ee/app/services/ee/protected_branches/legacy_api_update_service.rb'
+ - 'ee/app/services/ee/quick_actions/interpret_service.rb'
+ - 'ee/app/services/ee/quick_actions/target_service.rb'
+ - 'ee/app/services/ee/resource_events/synthetic_iteration_notes_builder_service.rb'
+ - 'ee/app/services/ee/resource_events/synthetic_weight_notes_builder_service.rb'
+ - 'ee/app/services/ee/search/global_service.rb'
+ - 'ee/app/services/ee/search_service.rb'
+ - 'ee/app/services/ee/system_note_service.rb'
+ - 'ee/app/services/ee/users/build_service.rb'
+ - 'ee/app/services/ee/users/destroy_service.rb'
+ - 'ee/app/services/ee/users/update_service.rb'
+ - 'ee/app/services/elastic/index_projects_by_range_service.rb'
+ - 'ee/app/services/elastic/indexing_control_service.rb'
+ - 'ee/app/services/elastic/process_bookkeeping_service.rb'
+ - 'ee/app/services/epic_issues/create_service.rb'
+ - 'ee/app/services/epics/strategies/base_dates_strategy.rb'
+ - 'ee/app/services/epics/strategies/due_date_inherited_strategy.rb'
+ - 'ee/app/services/epics/strategies/start_date_inherited_strategy.rb'
+ - 'ee/app/services/epics/transfer_service.rb'
+ - 'ee/app/services/epics/update_dates_service.rb'
+ - 'ee/app/services/epics/update_service.rb'
+ - 'ee/app/services/geo/container_repository_sync_service.rb'
+ - 'ee/app/services/gitlab_subscriptions/add_on_purchases/self_managed/provision_code_suggestions_service.rb'
+ - 'ee/app/services/gitlab_subscriptions/add_on_purchases/update_service.rb'
+ - 'ee/app/services/gitlab_subscriptions/notify_seats_exceeded_batch_service.rb'
+ - 'ee/app/services/gitlab_subscriptions/preview_billable_user_change_service.rb'
+ - 'ee/app/services/gitlab_subscriptions/trials/apply_trial_service.rb'
+ - 'ee/app/services/incident_management/escalation_rules/destroy_service.rb'
+ - 'ee/app/services/incident_management/pending_escalations/create_service.rb'
+ - 'ee/app/services/incident_management/pending_escalations/process_service.rb'
+ - 'ee/app/services/iterations/cadences/create_iterations_in_advance_service.rb'
+ - 'ee/app/services/iterations/roll_over_issues_service.rb'
+ - 'ee/app/services/members/activate_service.rb'
+ - 'ee/app/services/members/await_service.rb'
+ - 'ee/app/services/merge_requests/reset_approvals_service.rb'
+ - 'ee/app/services/package_metadata/advisory_data_object.rb'
+ - 'ee/app/services/protected_environments/base_service.rb'
+ - 'ee/app/services/protected_environments/search_service.rb'
+ - 'ee/app/services/resource_events/change_weight_service.rb'
+ - 'ee/app/services/security/ingestion/schedule_mark_dropped_as_resolved_service.rb'
+ - 'ee/app/services/security/ingestion/tasks/ingest_vulnerabilities/mark_resolved_as_detected.rb'
+ - 'ee/app/services/security/ingestion/tasks/ingest_vulnerability_flags.rb'
+ - 'ee/app/services/security/merge_request_security_report_generation_service.rb'
+ - 'ee/app/services/security/scan_result_policies/sync_any_merge_request_rules_service.rb'
+ - 'ee/app/services/security/scan_result_policies/update_approvals_service.rb'
+ - 'ee/app/services/security/security_orchestration_policies/fetch_policy_approvers_service.rb'
+ - 'ee/app/services/security/security_orchestration_policies/policy_branches_service.rb'
+ - 'ee/app/services/security/security_orchestration_policies/process_scan_result_policy_service.rb'
+ - 'ee/app/services/security/security_orchestration_policies/rule_schedule_service.rb'
+ - 'ee/app/services/security/security_orchestration_policies/update_violations_service.rb'
+ - 'ee/app/services/security/security_orchestration_policies/validate_policy_service.rb'
+ - 'ee/app/services/security/sync_license_scanning_rules_service.rb'
+ - 'ee/app/services/security/token_revocation_service.rb'
+ - 'ee/app/services/security/update_training_service.rb'
+ - 'ee/app/services/status_page/publish_base_service.rb'
+ - 'ee/app/services/timebox/event_aggregation_service.rb'
+ - 'ee/app/services/timebox/rollup_report_service.rb'
+ - 'ee/app/services/timebox_report_service.rb'
+ - 'ee/app/services/vulnerabilities/bulk_dismiss_service.rb'
+ - 'ee/app/services/vulnerabilities/create_service_base.rb'
+ - 'ee/app/services/vulnerabilities/manually_create_service.rb'
+ - 'ee/app/services/vulnerability_exports/export_service.rb'
+ - 'ee/app/services/work_items/widgets/iteration_service/base_service.rb'
+ - 'ee/app/validators/user_existence_validator.rb'
+ - 'ee/app/validators/user_id_existence_validator.rb'
+ - 'ee/app/workers/active_user_count_threshold_worker.rb'
+ - 'ee/app/workers/adjourned_group_deletion_worker.rb'
+ - 'ee/app/workers/adjourned_project_deletion_worker.rb'
+ - 'ee/app/workers/adjourned_projects_deletion_cron_worker.rb'
+ - 'ee/app/workers/admin_emails_worker.rb'
+ - 'ee/app/workers/analytics/cycle_analytics/consistency_worker.rb'
+ - 'ee/app/workers/analytics/cycle_analytics/incremental_worker.rb'
+ - 'ee/app/workers/analytics/cycle_analytics/reaggregation_worker.rb'
+ - 'ee/app/workers/analytics/devops_adoption/create_all_snapshots_worker.rb'
+ - 'ee/app/workers/analytics/devops_adoption/create_snapshot_worker.rb'
+ - 'ee/app/workers/analytics/value_stream_dashboard/count_worker.rb'
+ - 'ee/app/workers/arkose/blocked_users_report_worker.rb'
+ - 'ee/app/workers/audit_events/user_impersonation_event_create_worker.rb'
+ - 'ee/app/workers/ci/minutes/update_project_and_namespace_usage_worker.rb'
+ - 'ee/app/workers/ci/runners/stale_group_runners_prune_cron_worker.rb'
+ - 'ee/app/workers/ci/sync_reports_to_report_approval_rules_worker.rb'
+ - 'ee/app/workers/ci/trigger_downstream_subscriptions_worker.rb'
+ - 'ee/app/workers/ci/upstream_projects_subscriptions_cleanup_worker.rb'
+ - 'ee/app/workers/compliance_management/chain_of_custody_report_worker.rb'
+ - 'ee/app/workers/compliance_management/merge_requests/compliance_violations_consistency_worker.rb'
+ - 'ee/app/workers/compliance_management/timeout_pending_status_check_responses_worker.rb'
+ - 'ee/app/workers/concerns/elastic/bulk_cron_worker.rb'
+ - 'ee/app/workers/concerns/elastic/indexing_control.rb'
+ - 'ee/app/workers/concerns/geo/base_registry_sync_worker.rb'
+ - 'ee/app/workers/create_github_webhook_worker.rb'
+ - 'ee/app/workers/ee/issuable_export_csv_worker.rb'
+ - 'ee/app/workers/ee/post_receive.rb'
+ - 'ee/app/workers/elastic/migration_worker.rb'
+ - 'ee/app/workers/elastic/namespace_update_worker.rb'
+ - 'ee/app/workers/elastic/project_transfer_worker.rb'
+ - 'ee/app/workers/elastic_association_indexer_worker.rb'
+ - 'ee/app/workers/elastic_cluster_reindexing_cron_worker.rb'
+ - 'ee/app/workers/elastic_full_index_worker.rb'
+ - 'ee/app/workers/elastic_index_bulk_cron_worker.rb'
+ - 'ee/app/workers/elastic_index_initial_bulk_cron_worker.rb'
+ - 'ee/app/workers/elastic_namespace_indexer_worker.rb'
+ - 'ee/app/workers/elastic_namespace_rollout_worker.rb'
+ - 'ee/app/workers/emails/abandoned_trial_emails_cron_worker.rb'
+ - 'ee/app/workers/epics/new_epic_issue_worker.rb'
+ - 'ee/app/workers/epics/update_epics_dates_worker.rb'
+ - 'ee/app/workers/geo/batch/project_registry_scheduler_worker.rb'
+ - 'ee/app/workers/geo/batch/project_registry_worker.rb'
+ - 'ee/app/workers/geo/bulk_mark_pending_batch_worker.rb'
+ - 'ee/app/workers/geo/bulk_mark_verification_pending_batch_worker.rb'
+ - 'ee/app/workers/geo/container_repository_sync_worker.rb'
+ - 'ee/app/workers/geo/event_worker.rb'
+ - 'ee/app/workers/geo/file_removal_worker.rb'
+ - 'ee/app/workers/geo/hashed_storage_attachments_migration_worker.rb'
+ - 'ee/app/workers/geo/hashed_storage_migration_worker.rb'
+ - 'ee/app/workers/geo/metrics_update_worker.rb'
+ - 'ee/app/workers/geo/prune_event_log_worker.rb'
+ - 'ee/app/workers/geo/rename_repository_worker.rb'
+ - 'ee/app/workers/geo/repositories_clean_up_worker.rb'
+ - 'ee/app/workers/geo/repository_cleanup_worker.rb'
+ - 'ee/app/workers/geo/repository_shard_sync_worker.rb'
+ - 'ee/app/workers/geo/repository_sync_worker.rb'
+ - 'ee/app/workers/geo/repository_verification/primary/batch_worker.rb'
+ - 'ee/app/workers/geo/repository_verification/primary/shard_worker.rb'
+ - 'ee/app/workers/geo/repository_verification/primary/single_worker.rb'
+ - 'ee/app/workers/geo/repository_verification/secondary/scheduler_worker.rb'
+ - 'ee/app/workers/geo/repository_verification/secondary/shard_worker.rb'
+ - 'ee/app/workers/geo/repository_verification/secondary/single_worker.rb'
+ - 'ee/app/workers/geo/scheduler/per_shard_scheduler_worker.rb'
+ - 'ee/app/workers/geo/scheduler/primary/per_shard_scheduler_worker.rb'
+ - 'ee/app/workers/geo/scheduler/primary/scheduler_worker.rb'
+ - 'ee/app/workers/geo/scheduler/scheduler_worker.rb'
+ - 'ee/app/workers/geo/scheduler/secondary/per_shard_scheduler_worker.rb'
+ - 'ee/app/workers/geo/scheduler/secondary/scheduler_worker.rb'
+ - 'ee/app/workers/geo/secondary/registry_consistency_worker.rb'
+ - 'ee/app/workers/geo/secondary_usage_data_cron_worker.rb'
+ - 'ee/app/workers/geo/sidekiq_cron_config_worker.rb'
+ - 'ee/app/workers/geo/sync_timeout_cron_worker.rb'
+ - 'ee/app/workers/geo/verification_cron_worker.rb'
+ - 'ee/app/workers/geo/verification_state_backfill_worker.rb'
+ - 'ee/app/workers/geo_repository_destroy_worker.rb'
+ - 'ee/app/workers/gitlab_subscriptions/add_on_purchases/schedule_bulk_refresh_user_assignments_worker.rb'
+ - 'ee/app/workers/gitlab_subscriptions/notify_seats_exceeded_batch_worker.rb'
+ - 'ee/app/workers/gitlab_subscriptions/refresh_seats_worker.rb'
+ - 'ee/app/workers/gitlab_subscriptions/schedule_refresh_seats_worker.rb'
+ - 'ee/app/workers/group_wikis/git_garbage_collect_worker.rb'
+ - 'ee/app/workers/groups/enterprise_users/associate_worker.rb'
+ - 'ee/app/workers/groups/enterprise_users/bulk_associate_by_domain_worker.rb'
+ - 'ee/app/workers/groups/enterprise_users/disassociate_worker.rb'
+ - 'ee/app/workers/groups/export_memberships_worker.rb'
+ - 'ee/app/workers/groups/update_repository_storage_worker.rb'
+ - 'ee/app/workers/historical_data_worker.rb'
+ - 'ee/app/workers/import_software_licenses_worker.rb'
+ - 'ee/app/workers/incident_management/incident_sla_exceeded_check_worker.rb'
+ - 'ee/app/workers/incident_management/oncall_rotations/persist_all_rotations_shifts_job.rb'
+ - 'ee/app/workers/incident_management/pending_escalations/schedule_check_cron_worker.rb'
+ - 'ee/app/workers/ldap_all_groups_sync_worker.rb'
+ - 'ee/app/workers/ldap_group_sync_worker.rb'
+ - 'ee/app/workers/ldap_sync_worker.rb'
+ - 'ee/app/workers/licenses/reset_submit_license_usage_data_banner_worker.rb'
+ - 'ee/app/workers/llm/embedding/gitlab_documentation/cleanup_previous_versions_records_worker.rb'
+ - 'ee/app/workers/llm/embedding/gitlab_documentation/create_db_embeddings_per_doc_file_worker.rb'
+ - 'ee/app/workers/llm/embedding/gitlab_documentation/create_empty_embeddings_records_worker.rb'
+ - 'ee/app/workers/llm/embedding/gitlab_documentation/set_embeddings_on_the_record_worker.rb'
+ - 'ee/app/workers/members_destroyer/clean_up_group_protected_branch_rules_worker.rb'
+ - 'ee/app/workers/merge_request_reset_approvals_worker.rb'
+ - 'ee/app/workers/new_epic_worker.rb'
+ - 'ee/app/workers/okrs/checkin_reminder_emails_cron_worker.rb'
+ - 'ee/app/workers/open_ai/clear_conversations_worker.rb'
+ - 'ee/app/workers/package_metadata/advisories_sync_worker.rb'
+ - 'ee/app/workers/package_metadata/licenses_sync_worker.rb'
+ - 'ee/app/workers/personal_access_tokens/instance/policy_worker.rb'
+ - 'ee/app/workers/product_analytics/initialize_snowplow_product_analytics_worker.rb'
+ - 'ee/app/workers/project_template_export_worker.rb'
+ - 'ee/app/workers/projects/disable_legacy_open_source_license_for_inactive_projects_worker.rb'
+ - 'ee/app/workers/pull_mirrors/reenable_configuration_worker.rb'
+ - 'ee/app/workers/refresh_license_compliance_checks_worker.rb'
+ - 'ee/app/workers/scan_security_report_secrets_worker.rb'
+ - 'ee/app/workers/search/index_curation_worker.rb'
+ - 'ee/app/workers/search/zoekt/namespace_indexer_worker.rb'
+ - 'ee/app/workers/security/create_orchestration_policy_worker.rb'
+ - 'ee/app/workers/security/orchestration_policy_rule_schedule_worker.rb'
+ - 'ee/app/workers/security/scan_execution_policies/rule_schedule_worker.rb'
+ - 'ee/app/workers/security/scan_result_policies/sync_any_merge_request_approval_rules_worker.rb'
+ - 'ee/app/workers/security/scan_result_policies/sync_findings_to_approval_rules_worker.rb'
+ - 'ee/app/workers/security/scans/purge_worker.rb'
+ - 'ee/app/workers/security/store_scans_worker.rb'
+ - 'ee/app/workers/security/track_secure_scans_worker.rb'
+ - 'ee/app/workers/set_user_status_based_on_user_cap_setting_worker.rb'
+ - 'ee/app/workers/store_security_reports_worker.rb'
+ - 'ee/app/workers/sync_seat_link_worker.rb'
+ - 'ee/app/workers/system_access/base_global_group_sync_worker.rb'
+ - 'ee/app/workers/system_access/base_saas_group_sync_worker.rb'
+ - 'ee/app/workers/todos_destroyer/confidential_epic_worker.rb'
+ - 'ee/app/workers/update_all_mirrors_worker.rb'
+ - 'ee/app/workers/users/unconfirmed_users_deletion_cron_worker.rb'
+ - 'ee/app/workers/vulnerabilities/historical_statistics/deletion_worker.rb'
+ - 'ee/app/workers/vulnerabilities/mark_dropped_as_resolved_worker.rb'
+ - 'ee/app/workers/vulnerabilities/orphaned_remediations_cleanup_worker.rb'
+ - 'ee/app/workers/vulnerabilities/statistics/adjustment_worker.rb'
+ - 'ee/app/workers/vulnerabilities/statistics/schedule_worker.rb'
+ - 'ee/config/routes/project.rb'
+ - 'ee/db/fixtures/development/31_devops_adoption.rb'
+ - 'ee/db/fixtures/development/92_dora_metrics.rb'
+ - 'ee/db/geo/migrate/20170206203234_create_project_registry.rb'
+ - 'ee/db/geo/migrate/20170223033541_create_file_registry.rb'
+ - 'ee/db/geo/migrate/20170614201943_add_last_wiki_synced_at_to_project_registry.rb'
+ - 'ee/db/geo/migrate/20171101105200_add_retry_count_fields_to_registries.rb'
+ - 'ee/db/geo/migrate/20171115143841_add_last_sync_failure_to_project_registry.rb'
+ - 'ee/db/geo/migrate/20180201154345_add_repository_verification_to_project_registry.rb'
+ - 'ee/db/geo/migrate/20180322062741_migrate_ci_job_artifacts_to_separate_registry.rb'
+ - 'ee/db/geo/migrate/20190612211021_add_container_repository_registry.rb'
+ - 'ee/db/geo/migrate/20190802200655_add_created_at_to_event_log_states.rb'
+ - 'ee/db/geo/migrate/20190923111102_add_design_registry.rb'
+ - 'ee/db/geo/migrate/20191007122326_add_unique_constraint_on_design_registry_project_id.rb'
+ - 'ee/db/geo/migrate/20200121194300_create_package_file_registry.rb'
+ - 'ee/db/geo/migrate/20200407120740_add_verification_fields_to_package_file_on_secondary.rb'
+ - 'ee/db/geo/migrate/20210325150435_create_pipeline_artifact_registry.rb'
+ - 'ee/db/geo/migrate/20210624160455_fix_state_column_in_lfs_object_registry.rb'
+ - 'ee/db/geo/migrate/20210818111211_fix_state_column_in_file_registry.rb'
+ - 'ee/db/geo/migrate/20211101113611_prepare_file_registry_for_verification.rb'
+ - 'ee/db/geo/migrate/20211119152539_add_verification_to_pages_deployment_registry.rb'
+ - 'ee/db/geo/migrate/20211124000000_add_verification_to_lfs_object_registry.rb'
+ - 'ee/db/geo/migrate/20220617125507_create_ci_secure_file_registry.rb'
+ - 'ee/db/geo/migrate/20230201110601_prepare_container_repository_registry_for_verification.rb'
+ - 'ee/db/geo/migrate/20230717195110_prepare_group_wiki_repository_registry_for_verification.rb'
+ - 'ee/db/geo/post_migrate/20210120225014_migrate_lfs_object_registry.rb'
+ - 'ee/db/geo/post_migrate/20231023230850_drop_project_registry.rb'
+ - 'ee/db/seeds/data_seeder/data_seeder.rb'
+ - 'ee/db/seeds/shared/dora_metrics.rb'
+ - 'ee/elastic/migrate/20230325200700_backfill_hashed_root_namespace_id_to_commits.rb'
+ - 'ee/elastic/migrate/20230405500000_backfill_wiki_permissions_in_main_index.rb'
+ - 'ee/elastic/migrate/20230518064300_backfill_project_permissions_in_blobs.rb'
+ - 'ee/elastic/migrate/20230519500012_reindex_wikis_to_fix_permissions_and_traversal_ids.rb'
+ - 'ee/elastic/migrate/20230702000000_backfill_existing_group_wiki.rb'
+ - 'ee/elastic/migrate/20230703112233_reindex_commits_to_fix_permissions.rb'
+ - 'ee/elastic/migrate/20230720000000_reindex_wikis_to_fix_routing.rb'
+ - 'ee/elastic/migrate/20230724070100_backfill_epics.rb'
+ - 'ee/elastic/migrate/20230724151612_backfill_archived_field_in_commits.rb'
+ - 'ee/elastic/migrate/20230724221548_remove_wikis_from_main_index.rb'
+ - 'ee/elastic/migrate/20230821123542_backfill_archived_field_in_blob.rb'
+ - 'ee/elastic/migrate/20230901120542_force_reindex_commits_from_main_index.rb'
+ - 'ee/elastic/migrate/20231019223356_reindex_wikis_to_fix_routing_and_backfill_archived.rb'
+ - 'ee/lib/analytics/devops_adoption/snapshot_calculator.rb'
+ - 'ee/lib/analytics/group_activity_calculator.rb'
+ - 'ee/lib/analytics/merge_request_metrics_calculator.rb'
+ - 'ee/lib/analytics/product_analytics/project_usage_data.rb'
+ - 'ee/lib/analytics/refresh_approvals_data.rb'
+ - 'ee/lib/analytics/refresh_reassign_data.rb'
+ - 'ee/lib/api/admin/search/migrations.rb'
+ - 'ee/lib/api/audit_events.rb'
+ - 'ee/lib/api/dora/metrics.rb'
+ - 'ee/lib/api/elasticsearch_indexed_namespaces.rb'
+ - 'ee/lib/api/entities/search/migration.rb'
+ - 'ee/lib/api/epic_issues.rb'
+ - 'ee/lib/api/helpers/epics_helpers.rb'
+ - 'ee/lib/api/ldap_group_links.rb'
+ - 'ee/lib/api/license.rb'
+ - 'ee/lib/api/saml_group_links.rb'
+ - 'ee/lib/audit/changes.rb'
+ - 'ee/lib/banzai/filter/references/iteration_reference_filter.rb'
+ - 'ee/lib/code_suggestions/instructions_extractor.rb'
+ - 'ee/lib/ee/api/entities/analytics/code_review/merge_request.rb'
+ - 'ee/lib/ee/api/entities/experiment.rb'
+ - 'ee/lib/ee/api/group_boards.rb'
+ - 'ee/lib/ee/api/group_milestones.rb'
+ - 'ee/lib/ee/api/groups.rb'
+ - 'ee/lib/ee/api/helpers.rb'
+ - 'ee/lib/ee/api/helpers/award_emoji.rb'
+ - 'ee/lib/ee/api/helpers/common_helpers.rb'
+ - 'ee/lib/ee/api/helpers/internal_helpers.rb'
+ - 'ee/lib/ee/api/helpers/members_helpers.rb'
+ - 'ee/lib/ee/api/internal/base.rb'
+ - 'ee/lib/ee/api/project_milestones.rb'
+ - 'ee/lib/ee/api/projects.rb'
+ - 'ee/lib/ee/api/settings.rb'
+ - 'ee/lib/ee/backup/repositories.rb'
+ - 'ee/lib/ee/banzai/filter/references/epic_reference_filter.rb'
+ - 'ee/lib/ee/banzai/reference_parser/epic_parser.rb'
+ - 'ee/lib/ee/gitlab/analytics/cycle_analytics/aggregated/base_query_builder.rb'
+ - 'ee/lib/ee/gitlab/analytics/cycle_analytics/base_query_builder.rb'
+ - 'ee/lib/ee/gitlab/auth/ldap/access.rb'
+ - 'ee/lib/ee/gitlab/auth/ldap/group.rb'
+ - 'ee/lib/ee/gitlab/auth/ldap/sync/group.rb'
+ - 'ee/lib/ee/gitlab/auth/ldap/sync/groups.rb'
+ - 'ee/lib/ee/gitlab/auth/ldap/sync/proxy.rb'
+ - 'ee/lib/ee/gitlab/auth/ldap/sync/users.rb'
+ - 'ee/lib/ee/gitlab/background_migration/backfill_dismissal_reason_in_vulnerability_reads.rb'
+ - 'ee/lib/ee/gitlab/background_migration/backfill_epic_cache_counts.rb'
+ - 'ee/lib/ee/gitlab/background_migration/backfill_missing_vulnerability_dismissal_details.rb'
+ - 'ee/lib/ee/gitlab/background_migration/backfill_project_statistics_container_repository_size.rb'
+ - 'ee/lib/ee/gitlab/background_migration/backfill_project_statistics_storage_size_without_uploads_size.rb'
+ - 'ee/lib/ee/gitlab/background_migration/create_compliance_standards_adherence.rb'
+ - 'ee/lib/ee/gitlab/background_migration/create_vulnerability_links.rb'
+ - 'ee/lib/ee/gitlab/background_migration/delete_approval_rules_with_vulnerability.rb'
+ - 'ee/lib/ee/gitlab/background_migration/delete_invalid_epic_issues.rb'
+ - 'ee/lib/ee/gitlab/background_migration/fix_security_scan_statuses.rb'
+ - 'ee/lib/ee/gitlab/background_migration/migrate_approver_to_approval_rules.rb'
+ - 'ee/lib/ee/gitlab/background_migration/migrate_shared_vulnerability_identifiers.rb'
+ - 'ee/lib/ee/gitlab/background_migration/migrate_shared_vulnerability_scanners.rb'
+ - 'ee/lib/ee/gitlab/background_migration/migrate_vulnerabilities_feedback_to_vulnerabilities_state_transition.rb'
+ - 'ee/lib/ee/gitlab/background_migration/populate_denormalized_columns_for_sbom_occurrences.rb'
+ - 'ee/lib/ee/gitlab/background_migration/populate_latest_pipeline_ids.rb'
+ - 'ee/lib/ee/gitlab/background_migration/purge_stale_security_scans.rb'
+ - 'ee/lib/ee/gitlab/ci/status/bridge/waiting_for_approval.rb'
+ - 'ee/lib/ee/gitlab/ci/status/build/waiting_for_approval.rb'
+ - 'ee/lib/ee/gitlab/exclusive_lease.rb'
+ - 'ee/lib/ee/gitlab/geo_git_access.rb'
+ - 'ee/lib/ee/gitlab/import_export/after_export_strategies/custom_template_export_import_strategy.rb'
+ - 'ee/lib/ee/gitlab/import_export/project/object_builder.rb'
+ - 'ee/lib/ee/gitlab/object_hierarchy.rb'
+ - 'ee/lib/ee/gitlab/quick_actions/users_extractor.rb'
+ - 'ee/lib/ee/gitlab/saas.rb'
+ - 'ee/lib/ee/gitlab/tracking.rb'
+ - 'ee/lib/ee/gitlab/usage_data.rb'
+ - 'ee/lib/ee/users/internal.rb'
+ - 'ee/lib/elastic/instance_proxy_util.rb'
+ - 'ee/lib/elastic/latest/git_class_proxy.rb'
+ - 'ee/lib/elastic/latest/issue_class_proxy.rb'
+ - 'ee/lib/elastic/latest/merge_request_class_proxy.rb'
+ - 'ee/lib/elastic/latest/note_class_proxy.rb'
+ - 'ee/lib/elastic/latest/project_class_proxy.rb'
+ - 'ee/lib/elastic/latest/project_instance_proxy.rb'
+ - 'ee/lib/elastic/latest/user_class_proxy.rb'
+ - 'ee/lib/elastic/latest/user_config.rb'
+ - 'ee/lib/elastic/multi_version_util.rb'
+ - 'ee/lib/elastic/v12p1/application_class_proxy.rb'
+ - 'ee/lib/elastic/v12p1/application_instance_proxy.rb'
+ - 'ee/lib/elastic/v12p1/config.rb'
+ - 'ee/lib/elastic/v12p1/epic_class_proxy.rb'
+ - 'ee/lib/elastic/v12p1/epic_instance_proxy.rb'
+ - 'ee/lib/elastic/v12p1/issue_class_proxy.rb'
+ - 'ee/lib/elastic/v12p1/issue_instance_proxy.rb'
+ - 'ee/lib/elastic/v12p1/merge_request_class_proxy.rb'
+ - 'ee/lib/elastic/v12p1/merge_request_instance_proxy.rb'
+ - 'ee/lib/elastic/v12p1/milestone_class_proxy.rb'
+ - 'ee/lib/elastic/v12p1/milestone_instance_proxy.rb'
+ - 'ee/lib/elastic/v12p1/note_class_proxy.rb'
+ - 'ee/lib/elastic/v12p1/note_instance_proxy.rb'
+ - 'ee/lib/elastic/v12p1/project_class_proxy.rb'
+ - 'ee/lib/elastic/v12p1/project_instance_proxy.rb'
+ - 'ee/lib/elastic/v12p1/repository_class_proxy.rb'
+ - 'ee/lib/elastic/v12p1/repository_instance_proxy.rb'
+ - 'ee/lib/elastic/v12p1/routing.rb'
+ - 'ee/lib/elastic/v12p1/snippet_class_proxy.rb'
+ - 'ee/lib/elastic/v12p1/snippet_instance_proxy.rb'
+ - 'ee/lib/elastic/v12p1/user_class_proxy.rb'
+ - 'ee/lib/elastic/v12p1/user_instance_proxy.rb'
+ - 'ee/lib/elastic/v12p1/wiki_class_proxy.rb'
+ - 'ee/lib/elastic/v12p1/wiki_instance_proxy.rb'
+ - 'ee/lib/gem_extensions/elasticsearch/api/utils.rb'
+ - 'ee/lib/gem_extensions/elasticsearch/model/adapter/multiple/records.rb'
+ - 'ee/lib/gem_extensions/elasticsearch/model/indexing/instance_methods.rb'
+ - 'ee/lib/gitlab/analytics/cycle_analytics/aggregated/data_for_duration_chart.rb'
+ - 'ee/lib/gitlab/analytics/cycle_analytics/data_for_duration_chart.rb'
+ - 'ee/lib/gitlab/analytics/cycle_analytics/distinct_stage_loader.rb'
+ - 'ee/lib/gitlab/analytics/cycle_analytics/stage_events/first_assigned_at.rb'
+ - 'ee/lib/gitlab/analytics/cycle_analytics/stage_events/label_based_stage_event.rb'
+ - 'ee/lib/gitlab/analytics/cycle_analytics/summary/base_time.rb'
+ - 'ee/lib/gitlab/analytics/type_of_work/tasks_by_type.rb'
+ - 'ee/lib/gitlab/analytics/value_stream_dashboard/namespace_cursor.rb'
+ - 'ee/lib/gitlab/applied_ml/suggested_reviewers/recommender_pb.rb'
+ - 'ee/lib/gitlab/auth/group_saml/identity_linker.rb'
+ - 'ee/lib/gitlab/auth/group_saml/membership_updater.rb'
+ - 'ee/lib/gitlab/auth/saml/membership_updater.rb'
+ - 'ee/lib/gitlab/ci/minutes/cached_quota.rb'
+ - 'ee/lib/gitlab/ci/minutes/pipeline_consumption.rb'
+ - 'ee/lib/gitlab/ci/reports/security/vulnerability_reports_comparer.rb'
+ - 'ee/lib/gitlab/circuit_breaker/store.rb'
+ - 'ee/lib/gitlab/code_owners/section_parser.rb'
+ - 'ee/lib/gitlab/com.rb'
+ - 'ee/lib/gitlab/compliance_management/violations/approved_by_committer.rb'
+ - 'ee/lib/gitlab/contribution_analytics/data_formatter.rb'
+ - 'ee/lib/gitlab/contribution_analytics/postgresql_data_collector.rb'
+ - 'ee/lib/gitlab/cube_js/data_transformer.rb'
+ - 'ee/lib/gitlab/elastic/bool_expr.rb'
+ - 'ee/lib/gitlab/elastic/elasticsearch_enabled_cache.rb'
+ - 'ee/lib/gitlab/elastic/group_search_results.rb'
+ - 'ee/lib/gitlab/elastic/helper.rb'
+ - 'ee/lib/gitlab/elastic/indexer.rb'
+ - 'ee/lib/gitlab/elastic/search_results.rb'
+ - 'ee/lib/gitlab/geo/event_gap_tracking.rb'
+ - 'ee/lib/gitlab/geo/geo_node_status_check.rb'
+ - 'ee/lib/gitlab/geo/log_cursor/daemon.rb'
+ - 'ee/lib/gitlab/geo/log_cursor/event_logs.rb'
+ - 'ee/lib/gitlab/geo/replication/blob_downloader.rb'
+ - 'ee/lib/gitlab/geo/replicator.rb'
+ - 'ee/lib/gitlab/git_audit_event.rb'
+ - 'ee/lib/gitlab/graphql/loaders/oncall_participant_loader.rb'
+ - 'ee/lib/gitlab/group_plans_preloader.rb'
+ - 'ee/lib/gitlab/insights/executors/dora_executor.rb'
+ - 'ee/lib/gitlab/insights/executors/issuable_executor.rb'
+ - 'ee/lib/gitlab/insights/finders/issuable_finder.rb'
+ - 'ee/lib/gitlab/insights/reducers/count_per_period_reducer.rb'
+ - 'ee/lib/gitlab/llm/ai_message.rb'
+ - 'ee/lib/gitlab/llm/chain/tools/identifier.rb'
+ - 'ee/lib/gitlab/llm/chain/tools/json_reader/executor.rb'
+ - 'ee/lib/gitlab/llm/chain/tools/summarize_comments/executor.rb'
+ - 'ee/lib/gitlab/llm/chat_storage.rb'
+ - 'ee/lib/gitlab/llm/open_ai/client.rb'
+ - 'ee/lib/gitlab/llm/templates/explain_vulnerability.rb'
+ - 'ee/lib/gitlab/llm/vertex_ai/completions/summarize_merge_request.rb'
+ - 'ee/lib/gitlab/llm/vertex_ai/completions/summarize_submitted_review.rb'
+ - 'ee/lib/gitlab/middleware/ip_restrictor.rb'
+ - 'ee/lib/gitlab/path_locks_finder.rb'
+ - 'ee/lib/gitlab/root_excess_size_error_message.rb'
+ - 'ee/lib/gitlab/search/recent_epics.rb'
+ - 'ee/lib/gitlab/search/zoekt/client.rb'
+ - 'ee/lib/gitlab/spdx/license.rb'
+ - 'ee/lib/gitlab/status_page/storage/object.rb'
+ - 'ee/lib/gitlab/status_page/storage/s3_multipart_upload.rb'
+ - 'ee/lib/gitlab/usage/metrics/instrumentations/license_metric.rb'
+ - 'ee/lib/gitlab/vulnerability_scanning/advisory.rb'
+ - 'ee/lib/gitlab/vulnerability_scanning/security_scanner.rb'
+ - 'ee/lib/gitlab/zoekt/search_results.rb'
+ - 'ee/lib/product_analytics/settings.rb'
+ - 'ee/lib/quality/seeders/vulnerabilities.rb'
+ - 'ee/lib/remote_development/agent_config/updater.rb'
+ - 'ee/lib/remote_development/workspaces/reconcile/input/actual_state_calculator.rb'
+ - 'ee/lib/remote_development/workspaces/reconcile/output/desired_config_generator.rb'
+ - 'ee/lib/remote_development/workspaces/reconcile/output/desired_config_generator_prev1.rb'
+ - 'ee/lib/remote_development/workspaces/reconcile/output/devfile_parser.rb'
+ - 'ee/lib/remote_development/workspaces/reconcile/output/devfile_parser_prev1.rb'
+ - 'ee/lib/remote_development/workspaces/reconcile/persistence/workspaces_from_agent_infos_updater.rb'
+ - 'ee/lib/remote_development/workspaces/reconcile/persistence/workspaces_to_be_returned_finder.rb'
+ - 'ee/lib/remote_development/workspaces/reconcile/persistence/workspaces_to_be_returned_updater.rb'
+ - 'ee/lib/system_check/geo/authorized_keys_check.rb'
+ - 'ee/lib/system_check/geo/geo_database_configured_check.rb'
+ - 'ee/lib/tasks/geo.rake'
+ - 'ee/lib/tasks/gitlab/elastic.rake'
+ - 'ee/locale/unfound_translations.rb'
+ - 'ee/spec/controllers/concerns/gitlab_subscriptions/seat_count_alert_spec.rb'
+ - 'ee/spec/controllers/concerns/routable_actions_spec.rb'
+ - 'ee/spec/controllers/projects/settings/merge_requests_controller_spec.rb'
+ - 'ee/spec/elastic/migrate/20230503064300_backfill_project_permissions_in_blobs_using_permutations_spec.rb'
+ - 'ee/spec/factories/package_metadata/pm_licenses.rb'
+ - 'ee/spec/factories/security_scans.rb'
+ - 'ee/spec/features/dashboards/todos_spec.rb'
+ - 'ee/spec/features/dependency_proxy/packages/maven_spec.rb'
+ - 'ee/spec/features/groups/group_settings_spec.rb'
+ - 'ee/spec/features/groups/settings/domain_verification_spec.rb'
+ - 'ee/spec/features/merge_request/draft_comments_spec.rb'
+ - 'ee/spec/features/merge_trains/user_adds_to_merge_train_when_pipeline_succeeds_spec.rb'
+ - 'ee/spec/features/projects/mirror_spec.rb'
+ - 'ee/spec/features/registrations/combined_registration_spec.rb'
+ - 'ee/spec/features/registrations/saas/standard_flow_with_2fa_spec.rb'
+ - 'ee/spec/features/subscriptions/subscription_flow_for_existing_user_with_eligible_group_spec.rb'
+ - 'ee/spec/features/trials/lead_creation_form_validation_spec.rb'
+ - 'ee/spec/features/trials/saas/creation_with_multiple_existing_namespace_flow_spec.rb'
+ - 'ee/spec/features/trials/saas/creation_with_no_existing_namespace_flow_spec.rb'
+ - 'ee/spec/features/trials/saas/creation_with_one_existing_namespace_flow_spec.rb'
+ - 'ee/spec/finders/audit_event_finder_spec.rb'
+ - 'ee/spec/finders/ee/group_members_finder_spec.rb'
+ - 'ee/spec/frontend/fixtures/ci_catalog_resources.rb'
+ - 'ee/spec/helpers/analytics/analytics_dashboards_helper_spec.rb'
+ - 'ee/spec/helpers/ee/dashboard_helper_spec.rb'
+ - 'ee/spec/helpers/ee/releases_helper_spec.rb'
+ - 'ee/spec/helpers/groups/security_features_helper_spec.rb'
+ - 'ee/spec/helpers/projects/security/discover_helper_spec.rb'
+ - 'ee/spec/helpers/web_hooks/web_hooks_helper_spec.rb'
+ - 'ee/spec/initializers/fog_google_https_private_urls_spec.rb'
+ - 'ee/spec/lib/analytics/devops_adoption/snapshot_calculator_spec.rb'
+ - 'ee/spec/lib/audit/push_rules/group_push_rules_changes_auditor_spec.rb'
+ - 'ee/spec/lib/audit/push_rules/project_push_rules_changes_auditor_spec.rb'
+ - 'ee/spec/lib/audit_events/strategies/group_external_destination_strategy_spec.rb'
+ - 'ee/spec/lib/audit_events/strategies/instance_external_destination_strategy_spec.rb'
+ - 'ee/spec/lib/code_suggestions/instructions_extractor_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/background_migration/backfill_dismissal_reason_in_vulnerability_reads_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/background_migration/backfill_epic_cache_counts_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/background_migration/backfill_iteration_cadence_id_for_boards_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/background_migration/backfill_missing_vulnerability_dismissal_details_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/background_migration/backfill_project_statistics_container_repository_size_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/background_migration/backfill_project_statistics_storage_size_without_uploads_size_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/background_migration/delete_invalid_epic_issues_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/background_migration/delete_orphaned_transferred_project_approval_rules_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/background_migration/fix_namespace_ids_of_vulnerability_reads_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/background_migration/migrate_approver_to_approval_rules_in_batch_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/background_migration/migrate_approver_to_approval_rules_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/background_migration/migrate_shared_vulnerability_identifiers_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/background_migration/migrate_shared_vulnerability_scanners_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/background_migration/migrate_vulnerabilities_feedback_to_vulnerabilities_state_transition_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/background_migration/populate_denormalized_columns_for_sbom_occurrences_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/database/docs/docs_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/import_export/repo_restorer_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/issuable_metadata_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/pages/deployment_update_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/saas_spec.rb'
+ - 'ee/spec/lib/elastic/latest/project_instance_proxy_spec.rb'
+ - 'ee/spec/lib/gitlab/analytics/value_stream_dashboard/namespace_cursor_spec.rb'
+ - 'ee/spec/lib/gitlab/auth/o_auth/user_spec.rb'
+ - 'ee/spec/lib/gitlab/auth/oidc/user_spec.rb'
+ - 'ee/spec/lib/gitlab/auth/saml/user_spec.rb'
+ - 'ee/spec/lib/gitlab/background_migration/create_vulnerability_links_spec.rb'
+ - 'ee/spec/lib/gitlab/ci/templates/dependency_scanning_latest_gitlab_ci_yaml_spec.rb'
+ - 'ee/spec/lib/gitlab/email/handler/create_note_handler_spec.rb'
+ - 'ee/spec/lib/gitlab/geo/cron_manager_spec.rb'
+ - 'ee/spec/lib/gitlab/geo/every_repository_type_replicated_spec.rb'
+ - 'ee/spec/lib/gitlab/geo/log_helpers_spec.rb'
+ - 'ee/spec/lib/gitlab/import_export/group/relation_factory_spec.rb'
+ - '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'
+ - 'ee/spec/lib/gitlab/timebox/snapshot_builder_spec.rb'
+ - 'ee/spec/lib/gitlab/usage/metrics/instrumentations/count_secure_pipelines_metric_spec.rb'
+ - 'ee/spec/lib/gitlab/usage/metrics/instrumentations/count_security_scans_metric_spec.rb'
+ - 'ee/spec/lib/gitlab/usage/metrics/instrumentations/count_user_merge_requests_for_projects_with_applied_scan_result_policies_metric_spec.rb'
+ - 'ee/spec/lib/gitlab/usage/metrics/instrumentations/protected_environment_approval_rules_required_approvals_average_metric_spec.rb'
+ - 'ee/spec/lib/remote_development/workspaces/create/creator_spec.rb'
+ - 'ee/spec/lib/remote_development/workspaces/create/personal_access_token_creator_spec.rb'
+ - 'ee/spec/lib/remote_development/workspaces/create/post_flatten_devfile_validator_spec.rb'
+ - 'ee/spec/lib/remote_development/workspaces/create/workspace_creator_spec.rb'
+ - 'ee/spec/lib/remote_development/workspaces/create/workspace_variables_creator_spec.rb'
+ - 'ee/spec/lib/remote_development/workspaces/reconcile/input/factory_spec.rb'
+ - 'ee/spec/lib/remote_development/workspaces/reconcile/main_integration_spec.rb'
+ - 'ee/spec/lib/remote_development/workspaces/reconcile/main_reconcile_scenarios_spec.rb'
+ - 'ee/spec/lib/remote_development/workspaces/reconcile/persistence/workspaces_from_agent_infos_updater_spec.rb'
+ - 'ee/spec/lib/remote_development/workspaces/update/updater_spec.rb'
+ - 'ee/spec/mailers/emails/enterprise_users_spec.rb'
+ - 'ee/spec/mailers/emails/merge_requests_spec.rb'
+ - 'ee/spec/mailers/emails/okr_spec.rb'
+ - 'ee/spec/models/approval_project_rule_spec.rb'
+ - 'ee/spec/models/burndown_spec.rb'
+ - 'ee/spec/models/ci/pipeline_spec.rb'
+ - 'ee/spec/models/concerns/approval_rule_like_spec.rb'
+ - 'ee/spec/models/concerns/elastic/note_spec.rb'
+ - 'ee/spec/models/ee/ci/job_artifact_spec.rb'
+ - 'ee/spec/models/ee/ci/pipeline_artifact_spec.rb'
+ - 'ee/spec/models/ee/ci/secure_file_spec.rb'
+ - 'ee/spec/models/ee/groups/feature_setting_spec.rb'
+ - 'ee/spec/models/elasticsearch_indexed_namespace_spec.rb'
+ - 'ee/spec/models/factories_spec.rb'
+ - 'ee/spec/models/gitlab_subscription_spec.rb'
+ - 'ee/spec/models/license_spec.rb'
+ - 'ee/spec/models/members/member_role_spec.rb'
+ - 'ee/spec/models/package_metadata/affected_package_spec.rb'
+ - 'ee/spec/models/package_metadata/package_spec.rb'
+ - 'ee/spec/models/product_analytics/dashboard_spec.rb'
+ - 'ee/spec/models/security/training_spec.rb'
+ - 'ee/spec/models/upload_spec.rb'
+ - 'ee/spec/models/vulnerabilities/read_spec.rb'
+ - 'ee/spec/policies/group_policy_spec.rb'
+ - 'ee/spec/presenters/approval_rule_presenter_spec.rb'
+ - 'ee/spec/presenters/ee/project_presenter_spec.rb'
+ - 'ee/spec/presenters/ee/projects/import_export/project_export_presenter_spec.rb'
+ - 'ee/spec/presenters/member_presenter_spec.rb'
+ - 'ee/spec/requests/api/conan_project_packages_spec.rb'
+ - 'ee/spec/requests/api/debian_group_packages_spec.rb'
+ - 'ee/spec/requests/api/debian_project_packages_spec.rb'
+ - 'ee/spec/requests/api/geo_sites_spec.rb'
+ - 'ee/spec/requests/api/graphql/boards/board_lists_query_spec.rb'
+ - 'ee/spec/requests/api/graphql/dora/dora_scores_spec.rb'
+ - 'ee/spec/requests/api/graphql/gitlab_subscriptions/user_add_on_assignments/create_spec.rb'
+ - 'ee/spec/requests/api/graphql/mutations/boards/epic_boards/epic_move_list_spec.rb'
+ - 'ee/spec/requests/api/graphql/mutations/geo/registries/update_spec.rb'
+ - 'ee/spec/requests/api/graphql/project/merge_request_spec.rb'
+ - 'ee/spec/requests/api/graphql/project/product_analytics/product_analytics_spec.rb'
+ - 'ee/spec/requests/api/group_service_accounts_spec.rb'
+ - 'ee/spec/requests/api/groups_spec.rb'
+ - 'ee/spec/requests/api/internal/base_spec.rb'
+ - 'ee/spec/requests/api/pypi_packages_spec.rb'
+ - 'ee/spec/requests/api/vulnerability_findings_spec.rb'
+ - 'ee/spec/requests/git_http_geo_spec.rb'
+ - 'ee/spec/requests/groups/issues_controller_spec.rb'
+ - 'ee/spec/serializers/vulnerabilities/issue_link_entity_spec.rb'
+ - 'ee/spec/serializers/vulnerabilities/merge_request_link_entity_spec.rb'
+ - 'ee/spec/services/app_sec/fuzzing/api/ci_configuration_create_service_spec.rb'
+ - 'ee/spec/services/branches/delete_service_spec.rb'
+ - 'ee/spec/services/ee/branches/delete_service_spec.rb'
+ - 'ee/spec/services/ee/resource_events/change_iteration_service_spec.rb'
+ - 'ee/spec/services/geo/file_registry_removal_service_spec.rb'
+ - 'ee/spec/services/geo/framework_repository_sync_service_spec.rb'
+ - 'ee/spec/services/geo/registry_consistency_service_spec.rb'
+ - 'ee/spec/services/geo/registry_update_service_spec.rb'
+ - 'ee/spec/services/gitlab_subscriptions/add_on_purchases/self_managed/provision_code_suggestions_service_spec.rb'
+ - 'ee/spec/services/gitlab_subscriptions/preview_billable_user_change_service_spec.rb'
+ - 'ee/spec/services/merge_requests/update_blocks_service_spec.rb'
+ - 'ee/spec/services/package_metadata/sync_service_spec.rb'
+ - 'ee/spec/services/security/merge_reports_service_spec.rb'
+ - 'ee/spec/services/security/security_orchestration_policies/policy_branches_service_spec.rb'
+ - 'ee/spec/services/security/token_revocation_service_spec.rb'
+ - 'ee/spec/spec_helper.rb'
+ - 'ee/spec/support/helpers/duo_chat_fixture_helpers.rb'
+ - 'ee/spec/support/matchers/locked_schema.rb'
+ - 'ee/spec/support/shared_contexts/graphql/geo/registries_shared_context.rb'
+ - 'ee/spec/support/shared_contexts/remote_development/remote_development_shared_contexts.rb'
+ - 'ee/spec/support/shared_contexts/saas_registration_settings_context.rb'
+ - 'ee/spec/support/shared_contexts/saas_trial_settings_context.rb'
+ - 'ee/spec/support/shared_contexts/user_contribution_events_shared_context.rb'
+ - 'ee/spec/support/shared_examples/auth/access_protocol_examples.rb'
+ - 'ee/spec/support/shared_examples/features/protected_branches_access_control_shared_examples.rb'
+ - 'ee/spec/support/shared_examples/finders/geo/framework_registry_finder_shared_examples.rb'
+ - 'ee/spec/support/shared_examples/graphql/geo/geo_registries_resolver_shared_examples.rb'
+ - 'ee/spec/support/shared_examples/models/concerns/blob_replicator_strategy_shared_examples.rb'
+ - 'ee/spec/support/shared_examples/models/concerns/linkable_items_shared_examples.rb'
+ - 'ee/spec/support/shared_examples/models/concerns/protected_ref_access_examples.rb'
+ - 'ee/spec/support/shared_examples/models/concerns/replicator_shared_examples.rb'
+ - 'ee/spec/support/shared_examples/models/elasticsearch_indexed_container_shared_examples.rb'
+ - 'ee/spec/support/shared_examples/models/geo_framework_registry_shared_examples.rb'
+ - 'ee/spec/support/shared_examples/models/geo_searchable_registry_shared_examples.rb'
+ - 'ee/spec/support/shared_examples/models/geo_verifiable_registry_shared_examples.rb'
+ - 'ee/spec/support/shared_examples/requests/api/graphql/geo/registries_shared_examples.rb'
+ - 'ee/spec/support/shared_examples/services/boards/base_service_shared_examples.rb'
+ - 'ee/spec/support/shared_examples/workers/geo/framework_registry_sync_worker_shared_examples.rb'
+ - 'ee/spec/uploaders/every_gitlab_uploader_spec.rb'
+ - 'ee/spec/views/groups/group_members/index.html.haml_spec.rb'
+ - 'ee/spec/views/layouts/header/_new_dropdown.haml_spec.rb'
+ - 'ee/spec/views/projects/project_members/index.html.haml_spec.rb'
+ - 'ee/spec/views/shared/_tier_badge.html.haml_spec.rb'
+ - 'ee/spec/workers/audit_events/audit_event_streaming_worker_spec.rb'
+ - 'ee/spec/workers/ee/ci/build_finished_worker_spec.rb'
+ - 'ee/spec/workers/ee/new_issue_worker_spec.rb'
+ - 'ee/spec/workers/elastic_namespace_rollout_worker_spec.rb'
+ - 'ee/spec/workers/geo/repository_registry_sync_worker_spec.rb'
+ - 'ee/spec/workers/historical_data_worker_spec.rb'
+ - 'ee/spec/workers/ldap_all_groups_sync_worker_spec.rb'
+ - 'ee/spec/workers/ldap_group_sync_worker_spec.rb'
+ - 'ee/spec/workers/ldap_sync_worker_spec.rb'
+ - 'ee/spec/workers/new_epic_worker_spec.rb'
+ - 'ee/spec/workers/projects/register_suggested_reviewers_project_worker_spec.rb'
+ - 'ee/spec/workers/update_all_mirrors_worker_spec.rb'
+ - 'lib/api/access_requests.rb'
+ - 'lib/api/admin/plan_limits.rb'
+ - 'lib/api/admin/sidekiq.rb'
+ - 'lib/api/api.rb'
+ - 'lib/api/api_guard.rb'
+ - 'lib/api/appearance.rb'
+ - 'lib/api/base.rb'
+ - 'lib/api/boards.rb'
+ - 'lib/api/branches.rb'
+ - 'lib/api/ci/helpers/runner.rb'
+ - 'lib/api/ci/jobs.rb'
+ - 'lib/api/ci/pipeline_schedules.rb'
+ - 'lib/api/ci/resource_groups.rb'
+ - 'lib/api/ci/runners.rb'
+ - 'lib/api/ci/secure_files.rb'
+ - 'lib/api/ci/triggers.rb'
+ - 'lib/api/ci/variables.rb'
+ - 'lib/api/commit_statuses.rb'
+ - 'lib/api/custom_attributes_endpoints.rb'
+ - 'lib/api/dependency_proxy.rb'
+ - 'lib/api/deploy_keys.rb'
+ - 'lib/api/discussions.rb'
+ - 'lib/api/entities/basic_project_details.rb'
+ - 'lib/api/entities/ci/runner_details.rb'
+ - 'lib/api/entities/feature.rb'
+ - 'lib/api/entities/issuable_time_stats.rb'
+ - 'lib/api/entities/ml/mlflow/search_runs.rb'
+ - 'lib/api/entities/project.rb'
+ - 'lib/api/entities/project_integration.rb'
+ - 'lib/api/entities/project_with_access.rb'
+ - 'lib/api/entities/tag.rb'
+ - 'lib/api/entities/todo.rb'
+ - 'lib/api/features.rb'
+ - 'lib/api/group_boards.rb'
+ - 'lib/api/groups.rb'
+ - 'lib/api/helm_packages.rb'
+ - 'lib/api/helpers.rb'
+ - 'lib/api/helpers/award_emoji.rb'
+ - 'lib/api/helpers/custom_attributes.rb'
+ - 'lib/api/helpers/internal_helpers.rb'
+ - 'lib/api/helpers/members_helpers.rb'
+ - 'lib/api/helpers/notes_helpers.rb'
+ - 'lib/api/helpers/packages/conan/api_helpers.rb'
+ - 'lib/api/helpers/packages/dependency_proxy_helpers.rb'
+ - 'lib/api/helpers/project_snapshots_helpers.rb'
+ - 'lib/api/helpers/snippets_helpers.rb'
+ - 'lib/api/helpers/users_helpers.rb'
+ - 'lib/api/helpers/variables_helpers.rb'
+ - 'lib/api/hooks/test.rb'
+ - 'lib/api/hooks/url_variables.rb'
+ - 'lib/api/internal/base.rb'
+ - 'lib/api/internal/workhorse.rb'
+ - 'lib/api/issue_links.rb'
+ - 'lib/api/issues.rb'
+ - 'lib/api/members.rb'
+ - 'lib/api/merge_requests.rb'
+ - 'lib/api/metadata.rb'
+ - 'lib/api/namespaces.rb'
+ - 'lib/api/notes.rb'
+ - 'lib/api/nuget_project_packages.rb'
+ - 'lib/api/pages_domains.rb'
+ - 'lib/api/project_container_repositories.rb'
+ - 'lib/api/project_packages.rb'
+ - 'lib/api/project_snippets.rb'
+ - 'lib/api/projects.rb'
+ - 'lib/api/projects_relation_builder.rb'
+ - 'lib/api/protected_branches.rb'
+ - 'lib/api/protected_tags.rb'
+ - 'lib/api/resource_access_tokens.rb'
+ - 'lib/api/rubygem_packages.rb'
+ - 'lib/api/settings.rb'
+ - 'lib/api/sidekiq_metrics.rb'
+ - 'lib/api/snippet_repository_storage_moves.rb'
+ - 'lib/api/users.rb'
+ - 'lib/atlassian/jira_connect/serializers/build_entity.rb'
+ - 'lib/backup/manager.rb'
+ - 'lib/banzai/filter/issuable_reference_expansion_filter.rb'
+ - 'lib/banzai/filter/references/external_issue_reference_filter.rb'
+ - 'lib/banzai/filter/references/label_reference_filter.rb'
+ - 'lib/banzai/filter/task_list_filter.rb'
+ - 'lib/banzai/object_renderer.rb'
+ - 'lib/banzai/pipeline/base_pipeline.rb'
+ - 'lib/banzai/renderer.rb'
+ - 'lib/bulk_imports/clients/http.rb'
+ - 'lib/bulk_imports/common/pipelines/entity_finisher.rb'
+ - 'lib/bulk_imports/common/pipelines/lfs_objects_pipeline.rb'
+ - 'lib/bulk_imports/file_downloads/filename_fetch.rb'
+ - 'lib/bulk_imports/groups/transformers/group_attributes_transformer.rb'
+ - 'lib/bulk_imports/pipeline/runner.rb'
+ - 'lib/bulk_imports/projects/pipelines/references_pipeline.rb'
+ - 'lib/bulk_imports/uniquify.rb'
+ - 'lib/click_house/models/base_model.rb'
+ - 'lib/click_house/query_builder.rb'
+ - 'lib/click_house/redactor.rb'
+ - 'lib/container_registry/path.rb'
+ - 'lib/container_registry/tag.rb'
+ - 'lib/declarative_enum.rb'
+ - 'lib/event_filter.rb'
+ - 'lib/extracts_path.rb'
+ - 'lib/extracts_ref.rb'
+ - 'lib/feature.rb'
+ - 'lib/file_size_validator.rb'
+ - 'lib/gem_extensions/active_record/association.rb'
+ - 'lib/gem_extensions/active_record/configurable_disable_joins.rb'
+ - 'lib/gem_extensions/active_record/delegate_cache.rb'
+ - 'lib/gem_extensions/active_record/disable_joins/associations/association_scope.rb'
+ - 'lib/gem_extensions/active_record/disable_joins/relation.rb'
+ - 'lib/gitlab/analytics/cycle_analytics/aggregated/base_query_builder.rb'
+ - 'lib/gitlab/analytics/cycle_analytics/aggregated/median.rb'
+ - 'lib/gitlab/analytics/cycle_analytics/aggregated/records_fetcher.rb'
+ - 'lib/gitlab/analytics/cycle_analytics/average.rb'
+ - 'lib/gitlab/analytics/cycle_analytics/base_query_builder.rb'
+ - 'lib/gitlab/analytics/cycle_analytics/median.rb'
+ - 'lib/gitlab/analytics/cycle_analytics/records_fetcher.rb'
+ - 'lib/gitlab/analytics/cycle_analytics/sorting.rb'
+ - 'lib/gitlab/analytics/cycle_analytics/stage_events/code_stage_start.rb'
+ - 'lib/gitlab/analytics/cycle_analytics/stage_events/issue_deployed_to_production.rb'
+ - 'lib/gitlab/analytics/cycle_analytics/stage_events/issue_stage_end.rb'
+ - 'lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_first_deployed_to_production.rb'
+ - 'lib/gitlab/analytics/cycle_analytics/stage_events/metrics_based_stage_event.rb'
+ - 'lib/gitlab/analytics/cycle_analytics/stage_events/plan_stage_start.rb'
+ - 'lib/gitlab/analytics/cycle_analytics/stage_events/stage_event.rb'
+ - 'lib/gitlab/analytics/cycle_analytics/stage_query_helpers.rb'
+ - 'lib/gitlab/application_context.rb'
+ - 'lib/gitlab/application_rate_limiter.rb'
+ - 'lib/gitlab/application_rate_limiter/base_strategy.rb'
+ - 'lib/gitlab/audit/null_author.rb'
+ - 'lib/gitlab/audit/type/definition.rb'
+ - 'lib/gitlab/auth.rb'
+ - 'lib/gitlab/auth/activity.rb'
+ - 'lib/gitlab/auth/auth_finders.rb'
+ - 'lib/gitlab/auth/devise/strategies/combined_two_factor_authenticatable.rb'
+ - 'lib/gitlab/auth/ldap/dn.rb'
+ - 'lib/gitlab/auth/ldap/person.rb'
+ - 'lib/gitlab/auth/o_auth/user.rb'
+ - 'lib/gitlab/auth/omniauth_identity_linker_base.rb'
+ - 'lib/gitlab/avatar_cache.rb'
+ - 'lib/gitlab/background_migration.rb'
+ - 'lib/gitlab/background_migration/backfill_compliance_violations.rb'
+ - 'lib/gitlab/background_migration/backfill_draft_status_on_merge_requests_with_corrected_regex.rb'
+ - 'lib/gitlab/background_migration/backfill_environment_tiers.rb'
+ - 'lib/gitlab/background_migration/backfill_epic_cache_counts.rb'
+ - 'lib/gitlab/background_migration/backfill_finding_id_in_vulnerabilities.rb'
+ - 'lib/gitlab/background_migration/backfill_imported_issue_search_data.rb'
+ - 'lib/gitlab/background_migration/backfill_issue_search_data.rb'
+ - 'lib/gitlab/background_migration/backfill_iteration_cadence_id_for_boards.rb'
+ - 'lib/gitlab/background_migration/backfill_missing_vulnerability_dismissal_details.rb'
+ - 'lib/gitlab/background_migration/backfill_nuget_normalized_version.rb'
+ - 'lib/gitlab/background_migration/backfill_partitioned_table.rb'
+ - 'lib/gitlab/background_migration/backfill_project_import_level.rb'
+ - 'lib/gitlab/background_migration/backfill_project_member_namespace_id.rb'
+ - 'lib/gitlab/background_migration/backfill_project_namespace_on_issues.rb'
+ - 'lib/gitlab/background_migration/backfill_project_repositories.rb'
+ - 'lib/gitlab/background_migration/backfill_project_statistics_container_repository_size.rb'
+ - 'lib/gitlab/background_migration/backfill_project_statistics_storage_size_with_recent_size.rb'
+ - 'lib/gitlab/background_migration/backfill_project_statistics_storage_size_without_pipeline_artifacts_size_job.rb'
+ - 'lib/gitlab/background_migration/backfill_project_statistics_storage_size_without_uploads_size.rb'
+ - 'lib/gitlab/background_migration/backfill_workspace_personal_access_token.rb'
+ - 'lib/gitlab/background_migration/batched_migration_job.rb'
+ - 'lib/gitlab/background_migration/batching_strategies/backfill_project_namespace_per_group_batching_strategy.rb'
+ - 'lib/gitlab/background_migration/batching_strategies/loose_index_scan_batching_strategy.rb'
+ - 'lib/gitlab/background_migration/batching_strategies/primary_key_batching_strategy.rb'
+ - 'lib/gitlab/background_migration/convert_credit_card_validation_data_to_hashes.rb'
+ - 'lib/gitlab/background_migration/create_compliance_standards_adherence.rb'
+ - 'lib/gitlab/background_migration/create_vulnerability_links.rb'
+ - 'lib/gitlab/background_migration/delete_approval_rules_with_vulnerability.rb'
+ - 'lib/gitlab/background_migration/delete_invalid_epic_issues.rb'
+ - 'lib/gitlab/background_migration/delete_orphans_approval_merge_request_rules2.rb'
+ - 'lib/gitlab/background_migration/delete_orphans_approval_project_rules2.rb'
+ - 'lib/gitlab/background_migration/destroy_invalid_group_members.rb'
+ - 'lib/gitlab/background_migration/destroy_invalid_members.rb'
+ - 'lib/gitlab/background_migration/destroy_invalid_project_members.rb'
+ - 'lib/gitlab/background_migration/fix_approval_project_rules_without_protected_branches.rb'
+ - 'lib/gitlab/background_migration/fix_first_mentioned_in_commit_at.rb'
+ - 'lib/gitlab/background_migration/fix_incoherent_packages_size_on_project_statistics.rb'
+ - 'lib/gitlab/background_migration/fix_namespace_ids_of_vulnerability_reads.rb'
+ - 'lib/gitlab/background_migration/fix_projects_without_prometheus_service.rb'
+ - 'lib/gitlab/background_migration/fix_vulnerability_reads_has_issues.rb'
+ - 'lib/gitlab/background_migration/job_coordinator.rb'
+ - 'lib/gitlab/background_migration/legacy_upload_mover.rb'
+ - 'lib/gitlab/background_migration/mailers/unconfirm_mailer.rb'
+ - 'lib/gitlab/background_migration/migrate_approver_to_approval_rules.rb'
+ - 'lib/gitlab/background_migration/migrate_approver_to_approval_rules_check_progress.rb'
+ - 'lib/gitlab/background_migration/migrate_approver_to_approval_rules_in_batch.rb'
+ - 'lib/gitlab/background_migration/migrate_job_artifact_registry_to_ssf.rb'
+ - 'lib/gitlab/background_migration/migrate_shared_vulnerability_identifiers.rb'
+ - 'lib/gitlab/background_migration/migrate_shared_vulnerability_scanners.rb'
+ - 'lib/gitlab/background_migration/migrate_vulnerabilities_feedback_to_vulnerabilities_state_transition.rb'
+ - 'lib/gitlab/background_migration/populate_approval_merge_request_rules_with_security_orchestration.rb'
+ - 'lib/gitlab/background_migration/populate_approval_project_rules_with_security_orchestration.rb'
+ - 'lib/gitlab/background_migration/populate_denormalized_columns_for_sbom_occurrences.rb'
+ - 'lib/gitlab/background_migration/populate_latest_pipeline_ids.rb'
+ - 'lib/gitlab/background_migration/populate_projects_star_count.rb'
+ - 'lib/gitlab/background_migration/populate_resolved_on_default_branch_column.rb'
+ - 'lib/gitlab/background_migration/populate_vulnerability_dismissal_fields.rb'
+ - 'lib/gitlab/background_migration/project_namespaces/backfill_project_namespaces.rb'
+ - 'lib/gitlab/background_migration/purge_stale_security_scans.rb'
+ - 'lib/gitlab/background_migration/re_expire_o_auth_tokens.rb'
+ - 'lib/gitlab/background_migration/recount_epic_cache_counts.rb'
+ - 'lib/gitlab/background_migration/redis/backfill_project_pipeline_status_ttl.rb'
+ - 'lib/gitlab/background_migration/remove_occurrence_pipelines_and_duplicate_vulnerabilities_findings.rb'
+ - 'lib/gitlab/background_migration/reset_status_on_container_repositories.rb'
+ - 'lib/gitlab/background_migration/reset_too_many_tags_skipped_registry_imports.rb'
+ - 'lib/gitlab/background_migration/second_recount_epic_cache_counts.rb'
+ - 'lib/gitlab/background_migration/third_recount_epic_cache_counts.rb'
+ - 'lib/gitlab/background_migration/truncate_overlong_vulnerability_html_titles.rb'
+ - 'lib/gitlab/background_migration/update_jira_tracker_data_deployment_type_based_on_url.rb'
+ - 'lib/gitlab/background_migration/update_workspaces_config_version.rb'
+ - 'lib/gitlab/background_task.rb'
+ - 'lib/gitlab/base_doorkeeper_controller.rb'
+ - 'lib/gitlab/bitbucket_import/importer.rb'
+ - 'lib/gitlab/bitbucket_import/importers/issue_importer.rb'
+ - 'lib/gitlab/bitbucket_import/importers/issue_notes_importer.rb'
+ - 'lib/gitlab/bitbucket_server_import/importers/pull_request_notes_importer.rb'
+ - 'lib/gitlab/blob_helper.rb'
+ - 'lib/gitlab/buffered_io.rb'
+ - 'lib/gitlab/cache/ci/project_pipeline_status.rb'
+ - 'lib/gitlab/cache/import/caching.rb'
+ - 'lib/gitlab/cache/request_cache.rb'
+ - 'lib/gitlab/checks/branch_check.rb'
+ - 'lib/gitlab/checks/diff_check.rb'
+ - 'lib/gitlab/checks/matching_merge_request.rb'
+ - 'lib/gitlab/checks/tag_check.rb'
+ - 'lib/gitlab/ci/ansi2html.rb'
+ - 'lib/gitlab/ci/ansi2json/parser.rb'
+ - 'lib/gitlab/ci/badge/pipeline/status.rb'
+ - 'lib/gitlab/ci/build/artifacts/metadata/entry.rb'
+ - 'lib/gitlab/ci/charts.rb'
+ - 'lib/gitlab/ci/components/instance_path.rb'
+ - 'lib/gitlab/ci/config.rb'
+ - 'lib/gitlab/ci/config/entry/processable.rb'
+ - 'lib/gitlab/ci/config/entry/product/matrix.rb'
+ - 'lib/gitlab/ci/config/entry/root.rb'
+ - 'lib/gitlab/ci/config/extendable/entry.rb'
+ - 'lib/gitlab/ci/config/external/mapper/verifier.rb'
+ - 'lib/gitlab/ci/config/interpolation/access.rb'
+ - 'lib/gitlab/ci/config/normalizer/matrix_strategy.rb'
+ - 'lib/gitlab/ci/lint.rb'
+ - 'lib/gitlab/ci/pipeline/chain/command.rb'
+ - 'lib/gitlab/ci/pipeline/duration.rb'
+ - 'lib/gitlab/ci/reports/accessibility_reports.rb'
+ - 'lib/gitlab/ci/reports/security/finding.rb'
+ - 'lib/gitlab/ci/reports/security/identifier.rb'
+ - 'lib/gitlab/ci/reports/security/report.rb'
+ - 'lib/gitlab/ci/reports/test_report.rb'
+ - 'lib/gitlab/ci/reports/test_reports_comparer.rb'
+ - 'lib/gitlab/ci/reports/test_suite.rb'
+ - 'lib/gitlab/ci/reports/test_suite_comparer.rb'
+ - 'lib/gitlab/ci/reports/test_suite_summary.rb'
+ - 'lib/gitlab/ci/secure_files/migration_helper.rb'
+ - 'lib/gitlab/ci/status/composite.rb'
+ - 'lib/gitlab/ci/tags/bulk_insert.rb'
+ - 'lib/gitlab/ci/trace.rb'
+ - 'lib/gitlab/ci/trace/chunked_io.rb'
+ - 'lib/gitlab/ci/trace/stream.rb'
+ - 'lib/gitlab/cleanup/personal_access_tokens.rb'
+ - 'lib/gitlab/cleanup/project_uploads.rb'
+ - 'lib/gitlab/cleanup/remote_uploads.rb'
+ - 'lib/gitlab/cluster/lifecycle_events.rb'
+ - 'lib/gitlab/cluster/mixins/puma_cluster.rb'
+ - 'lib/gitlab/composer/cache.rb'
+ - 'lib/gitlab/config/entry/composable_array.rb'
+ - 'lib/gitlab/config/entry/composable_hash.rb'
+ - 'lib/gitlab/config/entry/configurable.rb'
+ - 'lib/gitlab/config/entry/validators.rb'
+ - 'lib/gitlab/config/entry/validators/nested_array_helpers.rb'
+ - 'lib/gitlab/console.rb'
+ - 'lib/gitlab/container_repository/tags/cache.rb'
+ - 'lib/gitlab/content_security_policy/config_loader.rb'
+ - 'lib/gitlab/contributions_calendar.rb'
+ - 'lib/gitlab/current_settings.rb'
+ - 'lib/gitlab/daemon.rb'
+ - 'lib/gitlab/data_builder/pipeline.rb'
+ - 'lib/gitlab/data_builder/push.rb'
+ - 'lib/gitlab/database.rb'
+ - 'lib/gitlab/database/background_migration/batched_migration.rb'
+ - 'lib/gitlab/database/background_migration/batched_migration_wrapper.rb'
+ - 'lib/gitlab/database/background_migration_job.rb'
+ - 'lib/gitlab/database/batch_average_counter.rb'
+ - 'lib/gitlab/database/batch_counter.rb'
+ - 'lib/gitlab/database/consistency_checker.rb'
+ - 'lib/gitlab/database/gitlab_schema.rb'
+ - 'lib/gitlab/database/load_balancing/connection_proxy.rb'
+ - 'lib/gitlab/database/load_balancing/load_balancer.rb'
+ - 'lib/gitlab/database/load_balancing/service_discovery.rb'
+ - 'lib/gitlab/database/load_balancing/setup.rb'
+ - 'lib/gitlab/database/lock_writes_manager.rb'
+ - 'lib/gitlab/database/migration.rb'
+ - 'lib/gitlab/database/migration_helpers.rb'
+ - 'lib/gitlab/database/migration_helpers/v2.rb'
+ - 'lib/gitlab/database/migrations/background_migration_helpers.rb'
+ - 'lib/gitlab/database/migrations/batched_background_migration_helpers.rb'
+ - 'lib/gitlab/database/migrations/milestone_mixin.rb'
+ - 'lib/gitlab/database/migrations/observation.rb'
+ - 'lib/gitlab/database/migrations/observers/query_log.rb'
+ - 'lib/gitlab/database/migrations/pg_backend_pid.rb'
+ - 'lib/gitlab/database/migrations/reestablished_connection_stack.rb'
+ - 'lib/gitlab/database/migrations/runner.rb'
+ - 'lib/gitlab/database/migrations/runner_backoff/migration_helpers.rb'
+ - 'lib/gitlab/database/migrations/sidekiq_helpers.rb'
+ - 'lib/gitlab/database/postgres_index.rb'
+ - 'lib/gitlab/database/postgresql_adapter/type_map_cache.rb'
+ - 'lib/gitlab/database/query_analyzers/base.rb'
+ - 'lib/gitlab/database/query_analyzers/prevent_cross_database_modification.rb'
+ - 'lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/node.rb'
+ - 'lib/gitlab/database/reindexing/reindex_action.rb'
+ - 'lib/gitlab/database/schema_cache_with_renamed_table.rb'
+ - 'lib/gitlab/database/schema_cache_with_renamed_table_legacy.rb'
+ - 'lib/gitlab/database/with_lock_retries.rb'
+ - 'lib/gitlab/database_importers/work_items/base_type_importer.rb'
+ - 'lib/gitlab/database_importers/work_items/related_links_restrictions_importer.rb'
+ - 'lib/gitlab/dependency_linker/go_mod_linker.rb'
+ - 'lib/gitlab/dependency_linker/go_sum_linker.rb'
+ - 'lib/gitlab/diff/file.rb'
+ - 'lib/gitlab/diff/file_collection/merge_request_diff_batch.rb'
+ - 'lib/gitlab/diff/file_collection/paginated_diffs.rb'
+ - 'lib/gitlab/diff/highlight_cache.rb'
+ - 'lib/gitlab/diff/line.rb'
+ - 'lib/gitlab/diff/line_mapper.rb'
+ - 'lib/gitlab/diff/pair_selector.rb'
+ - 'lib/gitlab/diff/parser.rb'
+ - 'lib/gitlab/discussions_diff/highlight_cache.rb'
+ - 'lib/gitlab/doctor/reset_tokens.rb'
+ - 'lib/gitlab/doctor/secrets.rb'
+ - 'lib/gitlab/email/handler/create_issue_handler.rb'
+ - 'lib/gitlab/email/handler/create_merge_request_handler.rb'
+ - 'lib/gitlab/email/handler/create_note_on_issuable_handler.rb'
+ - 'lib/gitlab/email/handler/reply_processing.rb'
+ - 'lib/gitlab/encoding_helper.rb'
+ - 'lib/gitlab/encrypted_command_base.rb'
+ - 'lib/gitlab/encrypted_incoming_email_command.rb'
+ - 'lib/gitlab/encrypted_ldap_command.rb'
+ - 'lib/gitlab/encrypted_redis_command.rb'
+ - 'lib/gitlab/encrypted_service_desk_email_command.rb'
+ - 'lib/gitlab/encrypted_smtp_command.rb'
+ - 'lib/gitlab/error_tracking/processor/context_payload_processor.rb'
+ - 'lib/gitlab/error_tracking/processor/sanitizer_processor.rb'
+ - 'lib/gitlab/etag_caching/store.rb'
+ - 'lib/gitlab/exclusive_lease.rb'
+ - 'lib/gitlab/experiment/rollout/feature.rb'
+ - 'lib/gitlab/external_authorization/cache.rb'
+ - 'lib/gitlab/faraday/error_callback.rb'
+ - 'lib/gitlab/file_hook.rb'
+ - 'lib/gitlab/fips.rb'
+ - 'lib/gitlab/fogbugz_import/importer.rb'
+ - 'lib/gitlab/front_matter.rb'
+ - 'lib/gitlab/git/blob.rb'
+ - 'lib/gitlab/git/commit.rb'
+ - 'lib/gitlab/git/diff.rb'
+ - 'lib/gitlab/git/patches/collection.rb'
+ - 'lib/gitlab/git/repository.rb'
+ - 'lib/gitlab/git/tag.rb'
+ - 'lib/gitlab/git/tree.rb'
+ - 'lib/gitlab/gitaly_client.rb'
+ - 'lib/gitlab/gitaly_client/operation_service.rb'
+ - 'lib/gitlab/gitaly_client/ref_service.rb'
+ - 'lib/gitlab/gitaly_client/repository_service.rb'
+ - 'lib/gitlab/gitaly_client/storage_settings.rb'
+ - 'lib/gitlab/github_gists_import/importer/gists_importer.rb'
+ - 'lib/gitlab/github_import/bulk_importing.rb'
+ - 'lib/gitlab/github_import/client.rb'
+ - 'lib/gitlab/github_import/importer/diff_note_importer.rb'
+ - 'lib/gitlab/github_import/importer/issue_importer.rb'
+ - 'lib/gitlab/github_import/importer/labels_importer.rb'
+ - 'lib/gitlab/github_import/importer/milestones_importer.rb'
+ - 'lib/gitlab/github_import/importer/note_importer.rb'
+ - 'lib/gitlab/github_import/importer/pull_requests/review_importer.rb'
+ - 'lib/gitlab/github_import/importer/pull_requests/review_requests_importer.rb'
+ - 'lib/gitlab/github_import/importer/releases_importer.rb'
+ - 'lib/gitlab/github_import/importer/repository_importer.rb'
+ - 'lib/gitlab/github_import/importer/single_endpoint_diff_notes_importer.rb'
+ - 'lib/gitlab/github_import/importer/single_endpoint_issue_events_importer.rb'
+ - 'lib/gitlab/github_import/importer/single_endpoint_issue_notes_importer.rb'
+ - 'lib/gitlab/github_import/importer/single_endpoint_merge_request_notes_importer.rb'
+ - 'lib/gitlab/github_import/label_finder.rb'
+ - 'lib/gitlab/github_import/markdown_text.rb'
+ - 'lib/gitlab/github_import/milestone_finder.rb'
+ - 'lib/gitlab/github_import/representation/pull_requests/review_requests.rb'
+ - 'lib/gitlab/github_import/user_finder.rb'
+ - 'lib/gitlab/gon_helper.rb'
+ - 'lib/gitlab/gpg/invalid_gpg_signature_updater.rb'
+ - 'lib/gitlab/graphql/authorize/authorize_resource.rb'
+ - 'lib/gitlab/graphql/batch_key.rb'
+ - 'lib/gitlab/graphql/deprecations/deprecation.rb'
+ - 'lib/gitlab/graphql/loaders/batch_commit_loader.rb'
+ - 'lib/gitlab/graphql/loaders/batch_model_loader.rb'
+ - 'lib/gitlab/graphql/loaders/lazy_relation_loader/registry.rb'
+ - 'lib/gitlab/graphql/loaders/lazy_relation_loader/relation_proxy.rb'
+ - 'lib/gitlab/graphql/loaders/lazy_relation_loader/top_n_loader.rb'
+ - 'lib/gitlab/graphql/pagination/keyset/connection.rb'
+ - 'lib/gitlab/graphql/present.rb'
+ - 'lib/gitlab/graphql/standard_graphql_error.rb'
+ - 'lib/gitlab/group_search_results.rb'
+ - 'lib/gitlab/health_checks/metric.rb'
+ - 'lib/gitlab/health_checks/probes/status.rb'
+ - 'lib/gitlab/health_checks/redis/redis_abstract_check.rb'
+ - 'lib/gitlab/health_checks/result.rb'
+ - 'lib/gitlab/http.rb'
+ - 'lib/gitlab/identifier.rb'
+ - 'lib/gitlab/import/database_helpers.rb'
+ - 'lib/gitlab/import/errors.rb'
+ - 'lib/gitlab/import/merge_request_helpers.rb'
+ - 'lib/gitlab/import_export/after_export_strategies/web_upload_strategy.rb'
+ - 'lib/gitlab/import_export/base/relation_object_saver.rb'
+ - 'lib/gitlab/import_export/command_line_util.rb'
+ - 'lib/gitlab/import_export/fast_hash_serializer.rb'
+ - 'lib/gitlab/import_export/group/relation_tree_restorer.rb'
+ - 'lib/gitlab/import_export/json/streaming_serializer.rb'
+ - 'lib/gitlab/import_export/members_mapper.rb'
+ - 'lib/gitlab/import_export/project/import_task.rb'
+ - 'lib/gitlab/import_export/project/relation_factory.rb'
+ - 'lib/gitlab/import_sources.rb'
+ - 'lib/gitlab/instrumentation/redis_cluster_validator.rb'
+ - 'lib/gitlab/instrumentation/redis_interceptor.rb'
+ - 'lib/gitlab/internal_events.rb'
+ - 'lib/gitlab/issuable/clone/copy_resource_events_service.rb'
+ - 'lib/gitlab/issues/rebalancing/state.rb'
+ - 'lib/gitlab/jira/http_client.rb'
+ - 'lib/gitlab/kas/user_access.rb'
+ - 'lib/gitlab/lazy.rb'
+ - 'lib/gitlab/legacy_github_import/base_formatter.rb'
+ - 'lib/gitlab/legacy_github_import/client.rb'
+ - 'lib/gitlab/legacy_github_import/importer.rb'
+ - 'lib/gitlab/legacy_github_import/issuable_formatter.rb'
+ - 'lib/gitlab/legacy_github_import/user_formatter.rb'
+ - 'lib/gitlab/legacy_http.rb'
+ - 'lib/gitlab/lets_encrypt/client.rb'
+ - 'lib/gitlab/lfs_token.rb'
+ - 'lib/gitlab/local_and_remote_storage_migration/base_migrater.rb'
+ - 'lib/gitlab/marginalia/comment.rb'
+ - 'lib/gitlab/markdown_cache/redis/store.rb'
+ - 'lib/gitlab/memory/diagnostic_reports_logger.rb'
+ - 'lib/gitlab/merge_requests/mergeability/redis_interface.rb'
+ - 'lib/gitlab/metrics/methods.rb'
+ - 'lib/gitlab/metrics/rack_middleware.rb'
+ - 'lib/gitlab/metrics/sli.rb'
+ - 'lib/gitlab/metrics/subscribers/action_cable.rb'
+ - 'lib/gitlab/metrics/subscribers/rack_attack.rb'
+ - 'lib/gitlab/middleware/basic_health_check.rb'
+ - 'lib/gitlab/middleware/release_env.rb'
+ - 'lib/gitlab/monitor/demo_projects.rb'
+ - 'lib/gitlab/nav/top_nav_menu_item.rb'
+ - 'lib/gitlab/object_hierarchy.rb'
+ - 'lib/gitlab/otp_key_rotator.rb'
+ - 'lib/gitlab/pagination/keyset/column_order_definition.rb'
+ - 'lib/gitlab/pagination/keyset/in_operator_optimization/query_builder.rb'
+ - 'lib/gitlab/pagination/keyset/iterator.rb'
+ - 'lib/gitlab/pagination/keyset/order.rb'
+ - 'lib/gitlab/pagination/keyset/pager.rb'
+ - 'lib/gitlab/pagination/keyset/paginator.rb'
+ - 'lib/gitlab/pagination/offset_pagination.rb'
+ - 'lib/gitlab/pagination_delegate.rb'
+ - 'lib/gitlab/patch/action_cable_subscription_adapter_identifier.rb'
+ - 'lib/gitlab/patch/node_loader.rb'
+ - 'lib/gitlab/patch/prependable.rb'
+ - 'lib/gitlab/patch/redis_cache_store.rb'
+ - 'lib/gitlab/patch/sidekiq_cron_poller.rb'
+ - 'lib/gitlab/patch/sidekiq_scheduled_enq.rb'
+ - 'lib/gitlab/performance_bar.rb'
+ - 'lib/gitlab/performance_bar/redis_adapter_when_peek_enabled.rb'
+ - 'lib/gitlab/popen/runner.rb'
+ - 'lib/gitlab/profiler.rb'
+ - 'lib/gitlab/project_search_results.rb'
+ - 'lib/gitlab/project_stats_refresh_conflicts_logger.rb'
+ - 'lib/gitlab/project_template.rb'
+ - 'lib/gitlab/prometheus_client.rb'
+ - 'lib/gitlab/quick_actions/issue_actions.rb'
+ - 'lib/gitlab/quick_actions/work_item_actions.rb'
+ - 'lib/gitlab/rack_attack.rb'
+ - 'lib/gitlab/rack_attack/request.rb'
+ - 'lib/gitlab/rack_attack/store.rb'
+ - 'lib/gitlab/redis/cross_slot.rb'
+ - 'lib/gitlab/redis/hll.rb'
+ - 'lib/gitlab/redis/multi_store.rb'
+ - 'lib/gitlab/redis/sidekiq_status.rb'
+ - 'lib/gitlab/reference_extractor.rb'
+ - 'lib/gitlab/relative_positioning/item_context.rb'
+ - 'lib/gitlab/repository_cache_adapter.rb'
+ - 'lib/gitlab/repository_hash_cache.rb'
+ - 'lib/gitlab/repository_set_cache.rb'
+ - 'lib/gitlab/request_forgery_protection.rb'
+ - 'lib/gitlab/runtime.rb'
+ - 'lib/gitlab/safe_device_detector.rb'
+ - 'lib/gitlab/sanitizers/exif.rb'
+ - 'lib/gitlab/search/params.rb'
+ - 'lib/gitlab/search/recent_items.rb'
+ - 'lib/gitlab/search/sort_options.rb'
+ - 'lib/gitlab/search_context.rb'
+ - 'lib/gitlab/search_results.rb'
+ - 'lib/gitlab/seeders/ci/runner/runner_fleet_pipeline_seeder.rb'
+ - 'lib/gitlab/seeders/ci/runner/runner_fleet_seeder.rb'
+ - 'lib/gitlab/set_cache.rb'
+ - 'lib/gitlab/setup_helper.rb'
+ - 'lib/gitlab/shard_health_cache.rb'
+ - 'lib/gitlab/sidekiq_config/cli_methods.rb'
+ - 'lib/gitlab/sidekiq_daemon/monitor.rb'
+ - 'lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb'
+ - 'lib/gitlab/sidekiq_middleware/pause_control/pause_control_service.rb'
+ - 'lib/gitlab/sidekiq_status.rb'
+ - 'lib/gitlab/slash_commands/base_command.rb'
+ - 'lib/gitlab/slash_commands/deploy.rb'
+ - 'lib/gitlab/slash_commands/global_slack_handler.rb'
+ - 'lib/gitlab/slash_commands/issue_search.rb'
+ - 'lib/gitlab/slash_commands/presenters/run.rb'
+ - 'lib/gitlab/slash_commands/result.rb'
+ - 'lib/gitlab/snippet_search_results.rb'
+ - 'lib/gitlab/source.rb'
+ - 'lib/gitlab/sourcegraph.rb'
+ - 'lib/gitlab/task_helpers.rb'
+ - 'lib/gitlab/template_parser/ast.rb'
+ - 'lib/gitlab/terraform/state_migration_helper.rb'
+ - 'lib/gitlab/testing/action_cable_blocker.rb'
+ - 'lib/gitlab/testing/request_blocker_middleware.rb'
+ - 'lib/gitlab/testing/request_inspector_middleware.rb'
+ - 'lib/gitlab/testing/robots_blocker_middleware.rb'
+ - 'lib/gitlab/throttle.rb'
+ - 'lib/gitlab/tracking.rb'
+ - 'lib/gitlab/uploads/migration_helper.rb'
+ - 'lib/gitlab/url_blocker.rb'
+ - 'lib/gitlab/url_builder.rb'
+ - 'lib/gitlab/usage/metrics/instrumentations/count_imported_projects_metric.rb'
+ - 'lib/gitlab/usage/metrics/instrumentations/database_metric.rb'
+ - 'lib/gitlab/usage/metrics/instrumentations/numbers_metric.rb'
+ - 'lib/gitlab/usage/metrics/query.rb'
+ - 'lib/gitlab/usage_data.rb'
+ - 'lib/gitlab/usage_data_counters/merge_request_activity_unique_counter.rb'
+ - 'lib/gitlab/usage_data_counters/work_item_activity_unique_counter.rb'
+ - 'lib/gitlab/usage_data_queries.rb'
+ - 'lib/gitlab/utils/usage_data.rb'
+ - 'lib/gitlab/verify/batch_verifier.rb'
+ - 'lib/gitlab/verify/ci_secure_files.rb'
+ - 'lib/gitlab/verify/rake_task.rb'
+ - 'lib/gitlab/verify/uploads.rb'
+ - 'lib/gitlab/webpack/file_loader.rb'
+ - 'lib/gitlab/workhorse.rb'
+ - 'lib/gitlab/x509/signature.rb'
+ - 'lib/gitlab_edition.rb'
+ - 'lib/gitlab_settings/options.rb'
+ - 'lib/gitlab_settings/settings.rb'
+ - 'lib/kramdown/parser/atlassian_document_format.rb'
+ - 'lib/object_storage/pending_direct_upload.rb'
+ - 'lib/quality/seeders/issues.rb'
+ - 'lib/result.rb'
+ - 'lib/safe_zip/extract.rb'
+ - 'lib/sidebars/context.rb'
+ - 'lib/sidebars/menu_item.rb'
+ - 'lib/static_model.rb'
+ - 'lib/tasks/dev.rake'
+ - 'lib/tasks/gems.rake'
+ - 'lib/tasks/gitlab/cleanup.rake'
+ - 'lib/tasks/gitlab/db.rake'
+ - 'lib/tasks/gitlab/db/validate_config.rake'
+ - 'lib/tasks/gitlab/external_diffs.rake'
+ - 'lib/tasks/gitlab/graphql.rake'
+ - 'lib/tasks/gitlab/refresh_project_statistics_build_artifacts_size.rake'
+ - 'lib/tasks/gitlab/snippets.rake'
+ - 'lib/tasks/gitlab/usage_data.rake'
+ - 'lib/tasks/tokens.rake'
+ - 'lib/unnested_in_filters/dsl.rb'
+ - 'lib/uploaded_file.rb'
+ - 'lib/users/internal.rb'
+ - 'locale/unfound_translations.rb'
+ - 'metrics_server/dependencies.rb'
+ - 'metrics_server/metrics_server.rb'
+ - 'metrics_server/override_gitlab_current_settings.rb'
+ - 'metrics_server/override_rails_constants.rb'
+ - 'metrics_server/settings_overrides.rb'
+ - 'qa/chemlab-library-gitlab.gemspec'
+ - 'qa/qa/ee/page/admin/subscription.rb'
+ - 'qa/qa/ee/page/component/secure_report.rb'
+ - 'qa/qa/ee/page/main/banner.rb'
+ - 'qa/qa/ee/page/project/monitor/on_call_schedule/index.rb'
+ - 'qa/qa/ee/page/project/settings/merge_request.rb'
+ - 'qa/qa/ee/page/workspace/action.rb'
+ - 'qa/qa/ee/page/workspace/list.rb'
+ - 'qa/qa/page/admin/applications.rb'
+ - 'qa/qa/page/component/access_tokens.rb'
+ - 'qa/qa/page/component/ci_icon.rb'
+ - 'qa/qa/page/component/issuable/sidebar.rb'
+ - 'qa/qa/page/component/new_snippet.rb'
+ - 'qa/qa/page/component/visibility_setting.rb'
+ - 'qa/qa/page/component/web_ide/web_terminal_panel.rb'
+ - 'qa/qa/page/file/show.rb'
+ - 'qa/qa/page/layout/banner.rb'
+ - 'qa/qa/page/merge_request/show.rb'
+ - 'qa/qa/page/project/activity.rb'
+ - 'qa/qa/page/project/issue/index.rb'
+ - 'qa/qa/page/project/secure/configuration_form.rb'
+ - 'qa/qa/page/project/settings/branch_rules_details.rb'
+ - 'qa/qa/page/project/settings/integrations.rb'
+ - 'qa/qa/page/project/settings/mirroring_repositories.rb'
+ - 'qa/qa/page/project/settings/runners.rb'
+ - 'qa/qa/page/project/settings/services/jenkins.rb'
+ - 'qa/qa/page/project/settings/services/jira.rb'
+ - 'qa/qa/page/project/settings/services/pipeline_status_emails.rb'
+ - 'qa/qa/page/search/results.rb'
+ - 'qa/qa/page/sub_menus/super_sidebar/global_search_modal.rb'
+ - 'qa/qa/resource/api_fabricator.rb'
+ - 'qa/qa/resource/base.rb'
+ - 'qa/qa/resource/ci_cd_settings.rb'
+ - 'qa/qa/resource/graphql.rb'
+ - 'qa/qa/resource/registry_repository.rb'
+ - 'qa/qa/resource/runner_base.rb'
+ - 'qa/qa/runtime/api/client.rb'
+ - 'qa/qa/runtime/browser.rb'
+ - 'qa/qa/runtime/env.rb'
+ - 'qa/qa/runtime/ip_address.rb'
+ - 'qa/qa/runtime/namespace.rb'
+ - 'qa/qa/scenario/bootable.rb'
+ - 'qa/qa/scenario/test/integration/ldap_no_tls.rb'
+ - 'qa/qa/scenario/test/integration/ldap_tls.rb'
+ - 'qa/qa/scenario/test/integration/registry_with_cdn.rb'
+ - 'qa/qa/service/docker_run/gitlab_runner.rb'
+ - 'qa/qa/service/shellout.rb'
+ - 'qa/qa/specs/features/api/1_manage/import/import_large_github_repo_spec.rb'
+ - 'qa/qa/specs/features/api/1_manage/migration/gitlab_migration_large_project_spec.rb'
+ - 'qa/qa/specs/features/browser_ui/10_govern/user/user_access_termination_spec.rb'
+ - 'qa/qa/specs/features/browser_ui/1_manage/integrations/jira/jira_basic_integration_spec.rb'
+ - 'qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_lfs_over_http_spec.rb'
+ - 'qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_over_http_spec.rb'
+ - 'qa/qa/specs/features/ee/api/10_govern/instance_audit_event_streaming_spec.rb'
+ - 'qa/qa/specs/features/ee/browser_ui/10_govern/create_merge_request_with_secure_spec.rb'
+ - 'qa/qa/specs/features/ee/browser_ui/10_govern/group/group_audit_event_streaming_spec.rb'
+ - 'qa/qa/specs/features/ee/browser_ui/10_govern/group/group_audit_logs_1_spec.rb'
+ - 'qa/qa/specs/features/ee/browser_ui/10_govern/instance/instance_audit_logs_spec.rb'
+ - 'qa/qa/specs/features/ee/browser_ui/10_govern/project/project_audit_logs_spec.rb'
+ - 'qa/qa/specs/features/ee/browser_ui/1_manage/integrations/jira_issues_list_spec.rb'
+ - 'qa/qa/specs/features/ee/browser_ui/3_create/repository/pull_mirroring_over_http_spec.rb'
+ - 'qa/qa/specs/features/ee/browser_ui/3_create/repository/pull_mirroring_over_ssh_with_key_spec.rb'
+ - 'qa/qa/specs/features/shared_contexts/import/gitlab_group_migration_common.rb'
+ - 'qa/qa/specs/qa_deprecation_toolkit_env.rb'
+ - 'qa/qa/support/fips.rb'
+ - 'qa/qa/support/formatters/test_metrics_formatter.rb'
+ - 'qa/qa/support/matchers/have_text.rb'
+ - 'qa/qa/support/page_error_checker.rb'
+ - 'qa/qa/support/run.rb'
+ - 'qa/qa/support/wait_for_requests.rb'
+ - 'qa/qa/tools/reliable_report.rb'
+ - 'qa/qa/vendor/one_password/cli.rb'
+ - 'qa/spec/ee/resource/mixins/group_base_spec.rb'
+ - 'qa/spec/page/base_spec.rb'
+ - 'qa/spec/resource/project_web_hook_spec.rb'
+ - 'qa/spec/scenario/template_spec.rb'
+ - 'qa/spec/support/formatters/allure_metadata_formatter_spec.rb'
+ - 'qa/spec/support/formatters/test_metrics_formatter_spec.rb'
+ - 'rubocop/cop/experiments_test_coverage.rb'
+ - 'rubocop/cop/gettext/static_identifier.rb'
+ - 'rubocop/cop/gitlab/feature_available_usage.rb'
+ - 'rubocop/cop/graphql/id_type.rb'
+ - 'rubocop/cop/migration/add_reference.rb'
+ - 'rubocop/cop/rspec/factory_bot/inline_association.rb'
+ - 'rubocop/cop/static_translation_definition.rb'
+ - 'rubocop/feature_categories.rb'
+ - 'rubocop/migration_helpers.rb'
+ - 'rubocop/rubocop.rb'
+ - 'scripts/api/get_job_id.rb'
+ - 'scripts/failed_tests.rb'
+ - 'scripts/feature_flags/used-feature-flags'
+ - 'scripts/generate-message-to-run-e2e-pipeline.rb'
+ - 'scripts/generate_rspec_pipeline.rb'
+ - 'scripts/insert-rspec-profiling-data'
+ - 'scripts/internal_events/monitor.rb'
+ - 'scripts/lib/gitlab.rb'
+ - 'scripts/lib/glfm/parse_examples.rb'
+ - 'scripts/lib/glfm/update_example_snapshots.rb'
+ - 'scripts/perf/gc/print_gc_stats.rb'
+ - 'scripts/rubocop-parse'
+ - 'scripts/security-harness'
+ - 'scripts/trigger-build.rb'
+ - 'scripts/verify-tff-mapping'
+ - 'sidekiq_cluster/cli.rb'
+ - 'sidekiq_cluster/sidekiq_cluster.rb'
+ - 'spec/benchmarks/banzai_benchmark.rb'
+ - 'spec/commands/diagnostic_reports/uploader_smoke_spec.rb'
+ - 'spec/commands/sidekiq_cluster/cli_spec.rb'
+ - 'spec/components/previews/pajamas/banner_component_preview.rb'
+ - 'spec/components/previews/pajamas/button_component_preview.rb'
+ - 'spec/config/application_spec.rb'
+ - 'spec/controllers/concerns/content_security_policy_patch_spec.rb'
+ - 'spec/controllers/concerns/continue_params_spec.rb'
+ - 'spec/controllers/concerns/preferred_language_switcher_spec.rb'
+ - 'spec/controllers/groups/milestones_controller_spec.rb'
+ - 'spec/controllers/omniauth_callbacks_controller_spec.rb'
+ - 'spec/controllers/profiles/two_factor_auths_controller_spec.rb'
+ - 'spec/controllers/projects/milestones_controller_spec.rb'
+ - 'spec/controllers/projects/releases_controller_spec.rb'
+ - 'spec/controllers/projects/runners_controller_spec.rb'
+ - 'spec/db/docs_spec.rb'
+ - 'spec/deprecation_warnings.rb'
+ - 'spec/experiments/application_experiment_spec.rb'
+ - 'spec/factories/design_management/designs.rb'
+ - 'spec/factories/events.rb'
+ - 'spec/factories/go_module_commits.rb'
+ - 'spec/factories/ml/experiments.rb'
+ - 'spec/factories/ml/models.rb'
+ - 'spec/factories/packages/packages.rb'
+ - 'spec/factories/packages/rpm/rpm_repository_files.rb'
+ - 'spec/factories/projects.rb'
+ - 'spec/factories/projects/ci_feature_usages.rb'
+ - 'spec/factories/wiki_pages.rb'
+ - 'spec/features/issues/user_edits_issue_spec.rb'
+ - 'spec/features/markdown/math_spec.rb'
+ - 'spec/features/merge_request/user_edits_mr_spec.rb'
+ - 'spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb'
+ - 'spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb'
+ - 'spec/features/projects/members/import_project_members_spec.rb'
+ - 'spec/features/projects/navbar_spec.rb'
+ - 'spec/finders/concerns/finder_methods_spec.rb'
+ - 'spec/finders/groups/projects_requiring_authorizations_refresh/on_direct_membership_finder_spec.rb'
+ - 'spec/finders/groups/projects_requiring_authorizations_refresh/on_transfer_finder_spec.rb'
+ - 'spec/finders/packages/build_infos_finder_spec.rb'
+ - 'spec/fixtures/packages/rubygems/package.gemspec'
+ - 'spec/frontend/fixtures/merge_requests.rb'
+ - 'spec/graphql/mutations/clusters/agent_tokens/create_spec.rb'
+ - 'spec/graphql/mutations/clusters/agents/create_spec.rb'
+ - 'spec/graphql/mutations/clusters/agents/delete_spec.rb'
+ - 'spec/graphql/mutations/commits/create_spec.rb'
+ - 'spec/graphql/mutations/design_management/delete_spec.rb'
+ - 'spec/graphql/resolvers/board_resolver_spec.rb'
+ - 'spec/graphql/resolvers/boards_resolver_spec.rb'
+ - 'spec/graphql/resolvers/ci/catalog/versions_resolver_spec.rb'
+ - 'spec/haml_lint/linter/inline_javascript_spec.rb'
+ - 'spec/haml_lint/linter/no_plain_nodes_spec.rb'
+ - 'spec/helpers/admin/abuse_reports_helper_spec.rb'
+ - 'spec/helpers/events_helper_spec.rb'
+ - 'spec/helpers/groups_helper_spec.rb'
+ - 'spec/helpers/keyset_helper_spec.rb'
+ - 'spec/helpers/releases_helper_spec.rb'
+ - 'spec/helpers/sidebars_helper_spec.rb'
+ - 'spec/initializers/active_record_transaction_observer_spec.rb'
+ - 'spec/initializers/carrierwave_s3_encryption_headers_patch_spec.rb'
+ - 'spec/initializers/fog_google_https_private_urls_spec.rb'
+ - 'spec/initializers/google_api_client_spec.rb'
+ - 'spec/initializers/mail_encoding_patch_spec.rb'
+ - 'spec/initializers/mail_starttls_patch_spec.rb'
+ - 'spec/initializers/rack_multipart_patch_spec.rb'
+ - 'spec/initializers/validate_database_config_spec.rb'
+ - 'spec/lib/api/base_spec.rb'
+ - 'spec/lib/api/entities/wiki_page_spec.rb'
+ - 'spec/lib/api/helpers/packages/npm_spec.rb'
+ - 'spec/lib/backup/database_model_spec.rb'
+ - 'spec/lib/backup/database_spec.rb'
+ - 'spec/lib/backup/manager_spec.rb'
+ - 'spec/lib/banzai/filter/footnote_filter_spec.rb'
+ - 'spec/lib/banzai/filter/image_link_filter_spec.rb'
+ - 'spec/lib/banzai/pipeline/incident_management/timeline_event_pipeline_spec.rb'
+ - 'spec/lib/container_registry/gitlab_api_client_spec.rb'
+ - 'spec/lib/generators/batched_background_migration/batched_background_migration_generator_spec.rb'
+ - 'spec/lib/gitlab/alert_management/payload/base_spec.rb'
+ - 'spec/lib/gitlab/audit/target_spec.rb'
+ - 'spec/lib/gitlab/audit/type/definition_spec.rb'
+ - 'spec/lib/gitlab/auth/atlassian/user_spec.rb'
+ - 'spec/lib/gitlab/auth/ldap/user_spec.rb'
+ - 'spec/lib/gitlab/auth/o_auth/user_spec.rb'
+ - 'spec/lib/gitlab/auth/saml/user_spec.rb'
+ - 'spec/lib/gitlab/authorized_keys_spec.rb'
+ - 'spec/lib/gitlab/avatar_cache_spec.rb'
+ - 'spec/lib/gitlab/background_migration/backfill_cluster_agents_has_vulnerabilities_spec.rb'
+ - 'spec/lib/gitlab/background_migration/backfill_code_suggestions_namespace_settings_spec.rb'
+ - 'spec/lib/gitlab/background_migration/backfill_finding_id_in_vulnerabilities_spec.rb'
+ - 'spec/lib/gitlab/background_migration/backfill_has_merge_request_of_vulnerability_reads_spec.rb'
+ - 'spec/lib/gitlab/background_migration/backfill_missing_ci_cd_settings_spec.rb'
+ - 'spec/lib/gitlab/background_migration/backfill_namespace_id_for_project_route_spec.rb'
+ - 'spec/lib/gitlab/background_migration/backfill_project_import_level_spec.rb'
+ - 'spec/lib/gitlab/background_migration/backfill_project_member_namespace_id_spec.rb'
+ - 'spec/lib/gitlab/background_migration/backfill_project_namespace_on_issues_spec.rb'
+ - 'spec/lib/gitlab/background_migration/backfill_project_repositories_spec.rb'
+ - 'spec/lib/gitlab/background_migration/backfill_resource_link_events_spec.rb'
+ - 'spec/lib/gitlab/background_migration/backfill_root_storage_statistics_fork_storage_sizes_spec.rb'
+ - 'spec/lib/gitlab/background_migration/backfill_uuid_conversion_column_in_vulnerability_occurrences_spec.rb'
+ - 'spec/lib/gitlab/background_migration/backfill_vulnerability_reads_cluster_agent_spec.rb'
+ - 'spec/lib/gitlab/background_migration/batched_migration_job_spec.rb'
+ - 'spec/lib/gitlab/background_migration/batching_strategies/backfill_project_statistics_with_container_registry_size_batching_strategy_spec.rb'
+ - 'spec/lib/gitlab/background_migration/batching_strategies/loose_index_scan_batching_strategy_spec.rb'
+ - 'spec/lib/gitlab/background_migration/batching_strategies/remove_backfilled_job_artifacts_expire_at_batching_strategy_spec.rb'
+ - 'spec/lib/gitlab/background_migration/cleanup_orphaned_routes_spec.rb'
+ - 'spec/lib/gitlab/background_migration/cleanup_personal_access_tokens_with_nil_expires_at_spec.rb'
+ - 'spec/lib/gitlab/background_migration/convert_credit_card_validation_data_to_hashes_spec.rb'
+ - 'spec/lib/gitlab/background_migration/delete_orphans_approval_merge_request_rules2_spec.rb'
+ - 'spec/lib/gitlab/background_migration/delete_orphans_approval_project_rules2_spec.rb'
+ - 'spec/lib/gitlab/background_migration/destroy_invalid_group_members_spec.rb'
+ - 'spec/lib/gitlab/background_migration/destroy_invalid_members_spec.rb'
+ - 'spec/lib/gitlab/background_migration/destroy_invalid_project_members_spec.rb'
+ - 'spec/lib/gitlab/background_migration/fix_allow_descendants_override_disabled_shared_runners_spec.rb'
+ - 'spec/lib/gitlab/background_migration/fix_incoherent_packages_size_on_project_statistics_spec.rb'
+ - 'spec/lib/gitlab/background_migration/fix_vulnerability_reads_has_issues_spec.rb'
+ - 'spec/lib/gitlab/background_migration/issues_internal_id_scope_updater_spec.rb'
+ - 'spec/lib/gitlab/background_migration/legacy_upload_mover_spec.rb'
+ - 'spec/lib/gitlab/background_migration/legacy_uploads_migrator_spec.rb'
+ - 'spec/lib/gitlab/background_migration/mark_duplicate_npm_packages_for_destruction_spec.rb'
+ - 'spec/lib/gitlab/background_migration/migrate_human_user_type_spec.rb'
+ - 'spec/lib/gitlab/background_migration/nullify_last_error_from_project_mirror_data_spec.rb'
+ - 'spec/lib/gitlab/background_migration/populate_vulnerability_dismissal_fields_spec.rb'
+ - 'spec/lib/gitlab/background_migration/truncate_overlong_vulnerability_html_titles_spec.rb'
+ - 'spec/lib/gitlab/background_migration/update_ci_pipeline_artifacts_unknown_locked_status_spec.rb'
+ - 'spec/lib/gitlab/background_task_spec.rb'
+ - 'spec/lib/gitlab/cache/client_spec.rb'
+ - 'spec/lib/gitlab/ci/ansi2json/style_spec.rb'
+ - 'spec/lib/gitlab/ci/build/rules/rule/clause/changes_spec.rb'
+ - 'spec/lib/gitlab/ci/config/normalizer/factory_spec.rb'
+ - 'spec/lib/gitlab/ci/parsers/security/sast_spec.rb'
+ - 'spec/lib/gitlab/ci/pipeline/duration_spec.rb'
+ - 'spec/lib/gitlab/config/entry/validators_spec.rb'
+ - 'spec/lib/gitlab/config/loader/multi_doc_yaml_spec.rb'
+ - 'spec/lib/gitlab/content_security_policy/config_loader_spec.rb'
+ - 'spec/lib/gitlab/daemon_spec.rb'
+ - 'spec/lib/gitlab/database/bulk_update_spec.rb'
+ - 'spec/lib/gitlab/database/health_status/indicators/patroni_apdex_spec.rb'
+ - 'spec/lib/gitlab/database/health_status/indicators/prometheus_alert_indicator_spec.rb'
+ - 'spec/lib/gitlab/database/health_status/indicators/wal_rate_spec.rb'
+ - 'spec/lib/gitlab/database/load_balancing/load_balancer_spec.rb'
+ - 'spec/lib/gitlab/database/load_balancing/transaction_leaking_spec.rb'
+ - 'spec/lib/gitlab/database/migration_helpers/restrict_gitlab_schema_spec.rb'
+ - 'spec/lib/gitlab/database/migrations/instrumentation_spec.rb'
+ - 'spec/lib/gitlab/database_spec.rb'
+ - 'spec/lib/gitlab/doorkeeper_secret_storing/secret/pbkdf2_sha512_spec.rb'
+ - 'spec/lib/gitlab/doorkeeper_secret_storing/token/pbkdf2_sha512_spec.rb'
+ - 'spec/lib/gitlab/encoding_helper_spec.rb'
+ - 'spec/lib/gitlab/experiment/rollout/feature_spec.rb'
+ - 'spec/lib/gitlab/gfm/uploads_rewriter_spec.rb'
+ - 'spec/lib/gitlab/git/object_pool_spec.rb'
+ - 'spec/lib/gitlab/git/remote_mirror_spec.rb'
+ - 'spec/lib/gitlab/git/repository_spec.rb'
+ - 'spec/lib/gitlab/git/tree_spec.rb'
+ - 'spec/lib/gitlab/gitaly_client/object_pool_service_spec.rb'
+ - 'spec/lib/gitlab/github_import/importer/issue_event_importer_spec.rb'
+ - 'spec/lib/gitlab/grape_logging/loggers/urgency_logger_spec.rb'
+ - 'spec/lib/gitlab/health_checks/master_check_spec.rb'
+ - 'spec/lib/gitlab/i18n/po_linter_spec.rb'
+ - 'spec/lib/gitlab/import/merge_request_creator_spec.rb'
+ - 'spec/lib/gitlab/import_export/avatar_saver_spec.rb'
+ - 'spec/lib/gitlab/import_export/base/relation_factory_spec.rb'
+ - 'spec/lib/gitlab/import_export/design_repo_restorer_spec.rb'
+ - 'spec/lib/gitlab/import_export/group/relation_factory_spec.rb'
+ - 'spec/lib/gitlab/import_export/group/tree_saver_spec.rb'
+ - 'spec/lib/gitlab/import_export/lfs_restorer_spec.rb'
+ - 'spec/lib/gitlab/import_export/lfs_saver_spec.rb'
+ - 'spec/lib/gitlab/import_export/project/relation_factory_spec.rb'
+ - 'spec/lib/gitlab/import_export/project/relation_saver_spec.rb'
+ - 'spec/lib/gitlab/import_export/project/sample/relation_factory_spec.rb'
+ - 'spec/lib/gitlab/import_export/project/tree_saver_spec.rb'
+ - 'spec/lib/gitlab/import_export/repo_restorer_spec.rb'
+ - 'spec/lib/gitlab/import_export/repo_saver_spec.rb'
+ - 'spec/lib/gitlab/import_export/saver_spec.rb'
+ - 'spec/lib/gitlab/import_export/snippet_repo_restorer_spec.rb'
+ - 'spec/lib/gitlab/import_export/snippet_repo_saver_spec.rb'
+ - 'spec/lib/gitlab/import_export/snippets_repo_restorer_spec.rb'
+ - 'spec/lib/gitlab/import_export/snippets_repo_saver_spec.rb'
+ - 'spec/lib/gitlab/import_export/uploads_manager_spec.rb'
+ - 'spec/lib/gitlab/import_export/uploads_saver_spec.rb'
+ - 'spec/lib/gitlab/instrumentation_helper_spec.rb'
+ - 'spec/lib/gitlab/json_spec.rb'
+ - 'spec/lib/gitlab/markdown_cache/redis/store_spec.rb'
+ - 'spec/lib/gitlab/memory/diagnostic_reports_logger_spec.rb'
+ - 'spec/lib/gitlab/memory/reports_daemon_spec.rb'
+ - 'spec/lib/gitlab/memory/reports_uploader_spec.rb'
+ - 'spec/lib/gitlab/memory/watchdog/configurator_spec.rb'
+ - 'spec/lib/gitlab/memory/watchdog/handlers/puma_handler_spec.rb'
+ - 'spec/lib/gitlab/merge_requests/message_generator_spec.rb'
+ - 'spec/lib/gitlab/pages/deployment_update_spec.rb'
+ - 'spec/lib/gitlab/pagination/keyset/iterator_spec.rb'
+ - 'spec/lib/gitlab/pagination/keyset/order_spec.rb'
+ - 'spec/lib/gitlab/pagination/keyset/simple_order_builder_spec.rb'
+ - 'spec/lib/gitlab/patch/database_config_spec.rb'
+ - 'spec/lib/gitlab/patch/node_loader_spec.rb'
+ - 'spec/lib/gitlab/quick_actions/dsl_spec.rb'
+ - 'spec/lib/gitlab/redis/cross_slot_spec.rb'
+ - 'spec/lib/gitlab/redis/multi_store_spec.rb'
+ - 'spec/lib/gitlab/search/abuse_detection_spec.rb'
+ - 'spec/lib/gitlab/shard_health_cache_spec.rb'
+ - 'spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb'
+ - 'spec/lib/gitlab/sidekiq_middleware/size_limiter/server_spec.rb'
+ - 'spec/lib/gitlab/usage/metrics/instrumentations/count_deployments_metric_spec.rb'
+ - 'spec/lib/gitlab/usage/metrics/instrumentations/gitaly_apdex_metric_spec.rb'
+ - 'spec/lib/gitlab/usage/service_ping/legacy_metric_metadata_decorator_spec.rb'
+ - 'spec/lib/gitlab/usage_data_counters/gitlab_cli_activity_unique_counter_spec.rb'
+ - 'spec/lib/gitlab/usage_data_counters/jetbrains_bundled_plugin_activity_unique_counter_spec.rb'
+ - 'spec/lib/gitlab/usage_data_counters/jetbrains_plugin_activity_unique_counter_spec.rb'
+ - 'spec/lib/gitlab/usage_data_counters/vscode_extension_activity_unique_counter_spec.rb'
+ - 'spec/lib/gitlab/utils/batch_loader_spec.rb'
+ - 'spec/lib/gitlab_settings/options_spec.rb'
+ - 'spec/lib/mattermost/command_spec.rb'
+ - 'spec/lib/mattermost/team_spec.rb'
+ - 'spec/lib/object_storage/pending_direct_upload_spec.rb'
+ - 'spec/lib/omni_auth/strategies/jwt_spec.rb'
+ - 'spec/lib/result_spec.rb'
+ - 'spec/mailers/notify_spec.rb'
+ - 'spec/migrations/20230403085957_add_tmp_partial_index_on_vulnerability_report_types2_spec.rb'
+ - 'spec/migrations/20230426085615_queue_backfill_resource_link_events_spec.rb'
+ - 'spec/migrations/20230613192703_swap_ci_build_needs_to_big_int_for_self_hosts_spec.rb'
+ - 'spec/migrations/20230726144458_swap_notes_id_to_bigint_for_self_managed_spec.rb'
+ - 'spec/migrations/20230803125434_add_has_merge_request_on_vulnerability_reads_trigger_spec.rb'
+ - 'spec/migrations/20230809104753_swap_epic_user_mentions_note_id_to_bigint_for_self_hosts_spec.rb'
+ - 'spec/migrations/20230809174702_swap_system_note_metadata_note_id_to_bigint_for_self_managed_spec.rb'
+ - 'spec/migrations/20230809210550_swap_issue_user_mentions_note_id_to_bigint_for_self_managed_spec.rb'
+ - 'spec/migrations/20230810103534_swap_suggestions_note_id_to_bigint_for_self_hosts_spec.rb'
+ - 'spec/migrations/20230810113227_swap_note_diff_files_note_id_to_bigint_for_self_hosts_spec.rb'
+ - 'spec/migrations/20230810123044_swap_snippet_user_mentions_note_id_to_bigint_for_self_hosts_spec.rb'
+ - 'spec/migrations/20230811103941_swap_vulnerability_user_mentions_note_id_to_bigint_for_self_hosts_spec.rb'
+ - 'spec/migrations/20230814144045_swap_timelogs_note_id_to_bigint_for_self_hosts_spec.rb'
+ - 'spec/migrations/20230816152639_swap_design_user_mentions_note_id_to_big_int_for_self_managed_spec.rb'
+ - 'spec/migrations/20230817111938_swap_events_target_id_to_bigint_for_self_hosts_spec.rb'
+ - 'spec/migrations/20230817143637_swap_award_emoji_note_id_to_bigint_for_self_hosts_spec.rb'
+ - 'spec/migrations/20230906204934_restart_self_hosted_sent_notifications_bigint_conversion_spec.rb'
+ - 'spec/migrations/20230906204935_restart_self_hosted_sent_notifications_backfill_spec.rb'
+ - 'spec/migrations/20231016001000_fix_design_user_mentions_design_id_note_id_index_for_self_managed_spec.rb'
+ - 'spec/migrations/add_namespaces_emails_enabled_column_data_spec.rb'
+ - 'spec/migrations/add_projects_emails_enabled_column_data_spec.rb'
+ - 'spec/migrations/cleanup_bigint_conversion_for_merge_request_metrics_for_self_hosts_spec.rb'
+ - 'spec/migrations/cleanup_conversion_big_int_ci_build_needs_self_managed_spec.rb'
+ - 'spec/migrations/swap_award_emoji_note_id_to_bigint_for_gitlab_dot_com_spec.rb'
+ - 'spec/migrations/swap_commit_user_mentions_note_id_to_bigint_for_gitlab_dot_com_spec.rb'
+ - 'spec/migrations/swap_commit_user_mentions_note_id_to_bigint_for_self_managed_spec.rb'
+ - 'spec/migrations/swap_design_user_mentions_note_id_to_bigint_for_gitlab_dot_com_spec.rb'
+ - 'spec/migrations/swap_epic_user_mentions_note_id_to_bigint_for_gitlab_dot_com_spec.rb'
+ - 'spec/migrations/swap_events_target_id_to_bigint_for_gitlab_dot_com_spec.rb'
+ - 'spec/migrations/swap_issue_user_mentions_note_id_to_bigint_for_gitlab_dot_com_2_spec.rb'
+ - 'spec/migrations/swap_merge_request_metrics_id_to_bigint_for_gitlab_dot_com_spec.rb'
+ - 'spec/migrations/swap_merge_request_metrics_id_to_bigint_for_self_hosts_spec.rb'
+ - 'spec/migrations/swap_merge_request_user_mentions_note_id_to_bigint_2_spec.rb'
+ - 'spec/migrations/swap_merge_request_user_mentions_note_id_to_bigint_for_self_managed_spec.rb'
+ - 'spec/migrations/swap_note_diff_files_note_id_to_bigint_for_gitlab_dot_com_spec.rb'
+ - 'spec/migrations/swap_notes_id_to_bigint_for_gitlab_dot_com_spec.rb'
+ - 'spec/migrations/swap_sent_notifications_id_columns_spec.rb'
+ - 'spec/migrations/swap_snippet_user_mentions_note_id_to_bigint_for_gitlab_dot_com_spec.rb'
+ - 'spec/migrations/swap_suggestions_note_id_to_bigint_for_gitlab_dot_com_spec.rb'
+ - 'spec/migrations/swap_system_note_metadata_note_id_to_bigint_for_gitlab_dot_com_spec.rb'
+ - 'spec/migrations/swap_timelogs_note_id_to_bigint_for_gitlab_dot_com_spec.rb'
+ - 'spec/migrations/swap_todos_note_id_to_bigint_for_gitlab_dot_com_spec.rb'
+ - 'spec/migrations/swap_todos_note_id_to_bigint_for_self_managed_spec.rb'
+ - 'spec/migrations/swap_vulnerability_user_mentions_note_id_to_bigint_for_gitlab_dot_com_spec.rb'
+ - 'spec/models/board_spec.rb'
+ - 'spec/models/ci/build_trace_chunk_spec.rb'
+ - 'spec/models/ci/job_token/project_scope_link_spec.rb'
+ - 'spec/models/ci/persistent_ref_spec.rb'
+ - 'spec/models/ci/pipeline_spec.rb'
+ - 'spec/models/ci/runner_manager_build_spec.rb'
+ - 'spec/models/concerns/bulk_insertable_associations_spec.rb'
+ - 'spec/models/concerns/database_event_tracking_spec.rb'
+ - 'spec/models/concerns/encrypted_user_password_spec.rb'
+ - 'spec/models/concerns/legacy_bulk_insert_spec.rb'
+ - 'spec/models/concerns/manual_inverse_association_spec.rb'
+ - 'spec/models/concerns/noteable_spec.rb'
+ - 'spec/models/concerns/triggerable_hooks_spec.rb'
+ - 'spec/models/deployment_spec.rb'
+ - 'spec/models/environment_spec.rb'
+ - 'spec/models/fork_network_member_spec.rb'
+ - 'spec/models/hooks/system_hook_spec.rb'
+ - 'spec/models/incident_management/timeline_event_spec.rb'
+ - 'spec/models/key_spec.rb'
+ - 'spec/models/label_spec.rb'
+ - 'spec/models/member_spec.rb'
+ - 'spec/models/merge_request_spec.rb'
+ - 'spec/models/namespace/package_setting_spec.rb'
+ - 'spec/models/operations/feature_flags/user_list_spec.rb'
+ - 'spec/models/packages/protection/rule_spec.rb'
+ - 'spec/models/project_feature_spec.rb'
+ - 'spec/models/project_spec.rb'
+ - 'spec/models/user_spec.rb'
+ - 'spec/policies/group_policy_spec.rb'
+ - 'spec/presenters/ci/build_runner_presenter_spec.rb'
+ - 'spec/presenters/member_presenter_spec.rb'
+ - 'spec/presenters/ml/candidate_details_presenter_spec.rb'
+ - 'spec/presenters/ml/candidates_csv_presenter_spec.rb'
+ - 'spec/presenters/packages/nuget/search_results_presenter_spec.rb'
+ - 'spec/presenters/packages/pypi/simple_index_presenter_spec.rb'
+ - 'spec/presenters/packages/pypi/simple_package_versions_presenter_spec.rb'
+ - 'spec/requests/api/alert_management_alerts_spec.rb'
+ - 'spec/requests/api/ci/runner/jobs_artifacts_spec.rb'
+ - 'spec/requests/api/graphql/ci/config_spec.rb'
+ - 'spec/requests/api/graphql/groups_query_spec.rb'
+ - 'spec/requests/api/graphql/issues_spec.rb'
+ - 'spec/requests/api/graphql/mutations/issues/bulk_update_spec.rb'
+ - 'spec/requests/api/group_import_spec.rb'
+ - 'spec/requests/api/internal/base_spec.rb'
+ - 'spec/requests/api/ml_model_packages_spec.rb'
+ - 'spec/requests/api/project_import_spec.rb'
+ - 'spec/requests/lfs_http_spec.rb'
+ - 'spec/requests/projects/merge_requests_discussions_spec.rb'
+ - 'spec/rubocop/cop/gitlab/httparty_spec.rb'
+ - 'spec/rubocop/cop/rake/require_spec.rb'
+ - 'spec/rubocop/cop_todo_spec.rb'
+ - 'spec/rubocop/formatter/graceful_formatter_spec.rb'
+ - 'spec/rubocop/formatter/todo_formatter_spec.rb'
+ - 'spec/rubocop/migration_helpers_spec.rb'
+ - 'spec/rubocop/todo_dir_spec.rb'
+ - 'spec/scripts/api/commit_merge_requests_spec.rb'
+ - 'spec/scripts/api/create_merge_request_discussion_spec.rb'
+ - 'spec/scripts/api/create_merge_request_note_spec.rb'
+ - 'spec/scripts/api/get_package_and_test_job_spec.rb'
+ - 'spec/scripts/generate_message_to_run_e2e_pipeline_spec.rb'
+ - 'spec/scripts/lib/glfm/update_example_snapshots_spec.rb'
+ - 'spec/scripts/lib/glfm/update_specification_spec.rb'
+ - 'spec/scripts/pipeline/average_reports_spec.rb'
+ - 'spec/scripts/review_apps/automated_cleanup_spec.rb'
+ - 'spec/scripts/trigger-build_spec.rb'
+ - 'spec/serializers/admin/abuse_report_details_entity_spec.rb'
+ - 'spec/serializers/admin/abuse_report_serializer_spec.rb'
+ - 'spec/serializers/profile/event_entity_spec.rb'
+ - 'spec/services/admin/set_feature_flag_service_spec.rb'
+ - 'spec/services/alert_management/metric_images/upload_service_spec.rb'
+ - 'spec/services/auto_merge/base_service_spec.rb'
+ - 'spec/services/auto_merge_service_spec.rb'
+ - 'spec/services/batched_git_ref_updates/cleanup_scheduler_service_spec.rb'
+ - 'spec/services/boards/lists/list_service_spec.rb'
+ - 'spec/services/bulk_imports/create_service_spec.rb'
+ - 'spec/services/ci/create_pipeline_service/environment_spec.rb'
+ - 'spec/services/ci/create_pipeline_service/logger_spec.rb'
+ - 'spec/services/ci/create_pipeline_service_spec.rb'
+ - 'spec/services/ci/generate_coverage_reports_service_spec.rb'
+ - 'spec/services/ci/job_artifacts/create_service_spec.rb'
+ - 'spec/services/ci/pipeline_schedules/calculate_next_run_service_spec.rb'
+ - 'spec/services/draft_notes/destroy_service_spec.rb'
+ - 'spec/services/event_create_service_spec.rb'
+ - 'spec/services/google_cloud/enable_cloudsql_service_spec.rb'
+ - 'spec/services/import/gitlab_projects/file_acquisition_strategies/remote_file_spec.rb'
+ - 'spec/services/merge_requests/create_service_spec.rb'
+ - 'spec/services/merge_requests/delete_non_latest_diffs_service_spec.rb'
+ - 'spec/services/object_storage/delete_stale_direct_uploads_service_spec.rb'
+ - 'spec/services/packages/conan/create_package_file_service_spec.rb'
+ - 'spec/services/packages/debian/create_distribution_service_spec.rb'
+ - 'spec/services/packages/debian/create_package_file_service_spec.rb'
+ - 'spec/services/packages/debian/update_distribution_service_spec.rb'
+ - 'spec/services/packages/rpm/repository_metadata/build_filelist_xml_service_spec.rb'
+ - 'spec/services/projects/hashed_storage/base_attachment_service_spec.rb'
+ - 'spec/services/protected_branches/cache_service_spec.rb'
+ - 'spec/services/resource_events/change_milestone_service_spec.rb'
+ - 'spec/services/security/merge_reports_service_spec.rb'
+ - 'spec/services/system_notes/issuables_service_spec.rb'
+ - 'spec/services/todos/destroy/destroyed_issuable_service_spec.rb'
+ - 'spec/services/user_agent_detail_service_spec.rb'
+ - 'spec/services/users/migrate_records_to_ghost_user_service_spec.rb'
+ - 'spec/services/users/set_namespace_commit_email_service_spec.rb'
+ - 'spec/services/webauthn/authenticate_service_spec.rb'
+ - 'spec/services/webauthn/register_service_spec.rb'
+ - 'spec/sidekiq/cron/job_gem_dependency_spec.rb'
+ - 'spec/sidekiq_cluster/sidekiq_cluster_spec.rb'
+ - 'spec/spec_helper.rb'
+ - 'spec/support/before_all_adapter.rb'
+ - 'spec/support/capybara.rb'
+ - 'spec/support/database/click_house/hooks.rb'
+ - 'spec/support/db_cleaner.rb'
+ - 'spec/support/fips.rb'
+ - 'spec/support/forgery_protection.rb'
+ - 'spec/support/frontend_fixtures.rb'
+ - 'spec/support/helpers/batch_destroy_dependent_associations_helper.rb'
+ - 'spec/support/helpers/database/multiple_databases_helpers.rb'
+ - 'spec/support/helpers/database/table_schema_helpers.rb'
+ - 'spec/support/helpers/drag_to_helper.rb'
+ - 'spec/support/helpers/fake_webauthn_device.rb'
+ - 'spec/support/helpers/features/two_factor_helpers.rb'
+ - 'spec/support/helpers/gitaly_setup.rb'
+ - 'spec/support/helpers/google_api/cloud_platform_helpers.rb'
+ - 'spec/support/helpers/graphql/subscriptions/action_cable/mock_action_cable.rb'
+ - 'spec/support/helpers/graphql_helpers.rb'
+ - 'spec/support/helpers/javascript_fixtures_helpers.rb'
+ - 'spec/support/helpers/jira_integration_helpers.rb'
+ - 'spec/support/helpers/login_helpers.rb'
+ - 'spec/support/helpers/migrations_helpers/vulnerabilities_findings_helper.rb'
+ - 'spec/support/helpers/migrations_helpers/vulnerabilities_helper.rb'
+ - 'spec/support/helpers/rendered_helpers.rb'
+ - 'spec/support/helpers/snowplow_helpers.rb'
+ - 'spec/support/helpers/stub_feature_flags.rb'
+ - 'spec/support/helpers/stub_object_storage.rb'
+ - 'spec/support/helpers/stub_snowplow.rb'
+ - 'spec/support/helpers/wait_for_requests.rb'
+ - 'spec/support/matchers/event_store.rb'
+ - 'spec/support/rspec_order.rb'
+ - 'spec/support/shared_contexts/controllers/ambiguous_ref_controller_shared_context.rb'
+ - 'spec/support/shared_contexts/disable_user_tracking.rb'
+ - 'spec/support/shared_contexts/policies/project_policy_table_shared_context.rb'
+ - 'spec/support/shared_contexts/requests/api/npm_packages_metadata_shared_examples.rb'
+ - 'spec/support/shared_contexts/user_contribution_events_shared_context.rb'
+ - 'spec/support/shared_examples/ci/deployable_shared_examples.rb'
+ - 'spec/support/shared_examples/db/seeds/data_seeder_shared_examples.rb'
+ - 'spec/support/shared_examples/deployments/create_for_job_shared_examples.rb'
+ - 'spec/support/shared_examples/environments/create_for_job_shared_examples.rb'
+ - 'spec/support/shared_examples/features/editable_merge_request_shared_examples.rb'
+ - 'spec/support/shared_examples/features/milestone_editing_shared_examples.rb'
+ - 'spec/support/shared_examples/features/wiki/user_previews_wiki_changes_shared_examples.rb'
+ - 'spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb'
+ - 'spec/support/shared_examples/features/wiki/user_views_asciidoc_page_with_includes_shared_examples.rb'
+ - 'spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb'
+ - 'spec/support/shared_examples/lib/gitlab/cache/json_cache_shared_examples.rb'
+ - 'spec/support/shared_examples/lib/gitlab/cycle_analytics/event_shared_examples.rb'
+ - 'spec/support/shared_examples/lib/gitlab/database/reestablished_connection_stack_shared_examples.rb'
+ - 'spec/support/shared_examples/loose_foreign_keys/have_loose_foreign_key.rb'
+ - 'spec/support/shared_examples/models/boards/listable_shared_examples.rb'
+ - 'spec/support/shared_examples/models/boards/user_preferences_shared_examples.rb'
+ - 'spec/support/shared_examples/models/cluster_application_helm_cert_shared_examples.rb'
+ - 'spec/support/shared_examples/models/concerns/analytics/cycle_analytics/stage_event_model_examples.rb'
+ - 'spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb'
+ - 'spec/support/shared_examples/models/concerns/protected_ref_access_examples.rb'
+ - 'spec/support/shared_examples/models/concerns/protected_ref_deploy_key_access_examples.rb'
+ - 'spec/support/shared_examples/models/concerns/ttl_expirable_shared_examples.rb'
+ - 'spec/support/shared_examples/models/cycle_analytics_stage_shared_examples.rb'
+ - 'spec/support/shared_examples/models/issuable_link_shared_examples.rb'
+ - 'spec/support/shared_examples/models/member_shared_examples.rb'
+ - 'spec/support/shared_examples/models/packages/debian/component_file_shared_example.rb'
+ - 'spec/support/shared_examples/models/packages/debian/distribution_key_shared_examples.rb'
+ - 'spec/support/shared_examples/requests/api/award_emoji_todo_shared_examples.rb'
+ - 'spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb'
+ - 'spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb'
+ - 'spec/support/shared_examples/requests/api/helm_packages_shared_examples.rb'
+ - 'spec/support/shared_examples/requests/api/ml_model_packages_shared_examples.rb'
+ - 'spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb'
+ - 'spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb'
+ - 'spec/support/shared_examples/requests/api/rubygems_packages_shared_examples.rb'
+ - 'spec/support/shared_examples/requests/api/terraform/modules/v1/packages_shared_examples.rb'
+ - 'spec/support/shared_examples/services/boards/boards_list_service_shared_examples.rb'
+ - 'spec/support/shared_examples/services/boards/items_list_service_shared_examples.rb'
+ - 'spec/support/shared_examples/services/wiki_pages/destroy_service_shared_examples.rb'
+ - 'spec/support/shared_examples/services/wikis/create_attachment_service_shared_examples.rb'
+ - 'spec/support/sidekiq_middleware.rb'
+ - 'spec/support_specs/ability_check_spec.rb'
+ - 'spec/support_specs/capybara_slow_finder_spec.rb'
+ - 'spec/support_specs/capybara_wait_for_all_requests_spec.rb'
+ - 'spec/support_specs/database/multiple_databases_helpers_spec.rb'
+ - 'spec/support_specs/helpers/stub_feature_flags_spec.rb'
+ - 'spec/support_specs/matchers/event_store_spec.rb'
+ - 'spec/tasks/dev_rake_spec.rb'
+ - 'spec/tasks/gitlab/db/validate_config_rake_spec.rb'
+ - 'spec/tasks/rubocop_rake_spec.rb'
+ - 'spec/tooling/danger/saas_feature_spec.rb'
+ - 'spec/tooling/danger/stable_branch_spec.rb'
+ - 'spec/tooling/lib/tooling/find_changes_spec.rb'
+ - 'spec/tooling/lib/tooling/helpers/file_handler_spec.rb'
+ - 'spec/tooling/lib/tooling/helpers/predictive_tests_helper_spec.rb'
+ - 'spec/tooling/lib/tooling/job_metrics_spec.rb'
+ - 'spec/tooling/lib/tooling/parallel_rspec_runner_spec.rb'
+ - 'spec/tooling/lib/tooling/predictive_tests_spec.rb'
+ - 'spec/uploaders/file_uploader_spec.rb'
+ - 'spec/uploaders/object_storage_spec.rb'
+ - 'spec/uploaders/packages/composer/cache_uploader_spec.rb'
+ - 'spec/uploaders/packages/debian/component_file_uploader_spec.rb'
+ - 'spec/views/groups/group_members/index.html.haml_spec.rb'
+ - 'spec/views/layouts/group.html.haml_spec.rb'
+ - 'spec/views/layouts/header/_new_dropdown.haml_spec.rb'
+ - 'spec/views/notify/import_work_items_csv_email.html.haml_spec.rb'
+ - 'spec/views/projects/project_members/index.html.haml_spec.rb'
+ - 'spec/workers/concerns/cronjob_queue_spec.rb'
+ - 'spec/workers/concerns/worker_attributes_spec.rb'
+ - 'spec/workers/container_registry/migration/observer_worker_spec.rb'
+ - 'spec/workers/object_storage/delete_stale_direct_uploads_worker_spec.rb'
+ - 'spec/workers/projects/delete_branch_worker_spec.rb'
+ - 'spec/workers/redis_migration_worker_spec.rb'
+ - 'spec/workers/repository_check/single_repository_worker_spec.rb'
+ - 'tooling/danger/database_dictionary.rb'
+ - 'tooling/danger/feature_flag.rb'
+ - 'tooling/danger/ignored_model_columns.rb'
+ - 'tooling/danger/saas_feature.rb'
+ - 'tooling/danger/sidekiq_queues.rb'
+ - 'tooling/danger/stable_branch.rb'
+ - 'tooling/danger/suggestor.rb'
+ - 'tooling/lib/tooling/fast_quarantine.rb'
+ - 'tooling/lib/tooling/find_changes.rb'
+ - 'tooling/lib/tooling/gettext_extractor.rb'
+ - 'tooling/lib/tooling/helm3_client.rb'
+ - 'tooling/lib/tooling/job_metrics.rb'
+ - 'tooling/lib/tooling/kubernetes_client.rb'
+ - 'tooling/quality/test_level.rb'
diff --git a/.rubocop_todo/style/lambda.yml b/.rubocop_todo/style/lambda.yml
index d874a388e08..8c6bc59080c 100644
--- a/.rubocop_todo/style/lambda.yml
+++ b/.rubocop_todo/style/lambda.yml
@@ -48,7 +48,6 @@ Style/Lambda:
- 'lib/gitlab/database/load_balancing/action_cable_callbacks.rb'
- 'lib/gitlab/middleware/rack_multipart_tempfile_factory.rb'
- 'lib/gitlab/omniauth_initializer.rb'
- - 'lib/gitlab/prometheus/queries/query_additional_metrics.rb'
- 'lib/gitlab/rack_attack.rb'
- 'lib/gitlab/sidekiq_config/worker_matcher.rb'
- 'lib/gitlab/sidekiq_middleware.rb'
diff --git a/.rubocop_todo/style/numeric_literal_prefix.yml b/.rubocop_todo/style/numeric_literal_prefix.yml
index 441e4389aab..35c05e19837 100644
--- a/.rubocop_todo/style/numeric_literal_prefix.yml
+++ b/.rubocop_todo/style/numeric_literal_prefix.yml
@@ -41,8 +41,6 @@ Style/NumericLiteralPrefix:
- 'spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb'
- 'spec/lib/gitlab/github_import/importer/note_importer_spec.rb'
- 'spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb'
- - 'spec/lib/gitlab/github_import/importer/pull_request_merged_by_importer_spec.rb'
- - 'spec/lib/gitlab/github_import/importer/pull_request_review_importer_spec.rb'
- 'spec/lib/gitlab/github_import/importer/releases_importer_spec.rb'
- 'spec/lib/gitlab/github_import/representation/diff_note_spec.rb'
- 'spec/lib/gitlab/github_import/representation/issue_spec.rb'
@@ -57,8 +55,6 @@ Style/NumericLiteralPrefix:
- 'spec/lib/gitlab/puma_logging/json_formatter_spec.rb'
- 'spec/lib/gitlab/relative_positioning/range_spec.rb'
- 'spec/metrics_server/metrics_server_spec.rb'
- - 'spec/migrations/20220506154054_create_sync_namespace_details_trigger_spec.rb'
- - 'spec/migrations/20220524184149_create_sync_project_namespace_details_trigger_spec.rb'
- 'spec/models/issue_spec.rb'
- 'spec/models/personal_access_token_spec.rb'
- 'spec/requests/api/personal_access_tokens_spec.rb'
diff --git a/.rubocop_todo/style/percent_literal_delimiters.yml b/.rubocop_todo/style/percent_literal_delimiters.yml
deleted file mode 100644
index bcaf610bf08..00000000000
--- a/.rubocop_todo/style/percent_literal_delimiters.yml
+++ /dev/null
@@ -1,399 +0,0 @@
----
-# Cop supports --autocorrect.
-Style/PercentLiteralDelimiters:
- Exclude:
- - 'metrics_server/metrics_server.rb'
- - 'spec/lib/gitlab/alert_management/payload/base_spec.rb'
- - 'spec/lib/gitlab/asset_proxy_spec.rb'
- - 'spec/lib/gitlab/auth/ldap/auth_hash_spec.rb'
- - 'spec/lib/gitlab/auth/ldap/config_spec.rb'
- - 'spec/lib/gitlab/auth/ldap/person_spec.rb'
- - 'spec/lib/gitlab/auth/o_auth/user_spec.rb'
- - 'spec/lib/gitlab/auth/saml/auth_hash_spec.rb'
- - 'spec/lib/gitlab/auth/saml/user_spec.rb'
- - 'spec/lib/gitlab/background_migration/batched_migration_job_spec.rb'
- - 'spec/lib/gitlab/background_migration/copy_column_using_background_migration_job_spec.rb'
- - 'spec/lib/gitlab/batch_worker_context_spec.rb'
- - 'spec/lib/gitlab/bitbucket_import/importer_spec.rb'
- - 'spec/lib/gitlab/cache_spec.rb'
- - 'spec/lib/gitlab/ci/ansi2html_spec.rb'
- - 'spec/lib/gitlab/ci/config/entry/bridge_spec.rb'
- - 'spec/lib/gitlab/ci/config/entry/commands_spec.rb'
- - 'spec/lib/gitlab/ci/config/entry/environment_spec.rb'
- - 'spec/lib/gitlab/ci/config/entry/image_spec.rb'
- - 'spec/lib/gitlab/ci/config/entry/root_spec.rb'
- - 'spec/lib/gitlab/ci/config/entry/service_spec.rb'
- - 'spec/lib/gitlab/ci/config/extendable/entry_spec.rb'
- - 'spec/lib/gitlab/ci/config/external/file/base_spec.rb'
- - 'spec/lib/gitlab/ci/parsers/security/secret_detection_spec.rb'
- - 'spec/lib/gitlab/ci/pipeline/chain/template_usage_spec.rb'
- - 'spec/lib/gitlab/ci/pipeline/expression/lexeme/equals_spec.rb'
- - 'spec/lib/gitlab/ci/pipeline/seed/build_spec.rb'
- - 'spec/lib/gitlab/ci/reports/accessibility_reports_comparer_spec.rb'
- - 'spec/lib/gitlab/ci/reports/accessibility_reports_spec.rb'
- - 'spec/lib/gitlab/ci/reports/test_suite_spec.rb'
- - 'spec/lib/gitlab/ci/status/composite_spec.rb'
- - 'spec/lib/gitlab/ci/status/stage/factory_spec.rb'
- - 'spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb'
- - 'spec/lib/gitlab/ci/templates/Jobs/sast_iac_latest_gitlab_ci_yaml_spec.rb'
- - 'spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb'
- - 'spec/lib/gitlab/ci/variables/collection/item_spec.rb'
- - 'spec/lib/gitlab/ci/yaml_processor/dag_spec.rb'
- - 'spec/lib/gitlab/ci/yaml_processor_spec.rb'
- - 'spec/lib/gitlab/config/entry/factory_spec.rb'
- - 'spec/lib/gitlab/conflict/file_spec.rb'
- - 'spec/lib/gitlab/data_builder/build_spec.rb'
- - 'spec/lib/gitlab/data_builder/pipeline_spec.rb'
- - 'spec/lib/gitlab/data_builder/push_spec.rb'
- - 'spec/lib/gitlab/database/background_migration/batched_job_spec.rb'
- - 'spec/lib/gitlab/database/background_migration/batched_job_transition_log_spec.rb'
- - 'spec/lib/gitlab/database/migration_helpers/cascading_namespace_settings_spec.rb'
- - 'spec/lib/gitlab/database/migration_helpers_spec.rb'
- - 'spec/lib/gitlab/database/postgres_index_spec.rb'
- - 'spec/lib/gitlab/database/reindexing_spec.rb'
- - 'spec/lib/gitlab/database/transaction/observer_spec.rb'
- - 'spec/lib/gitlab/dependency_linker/base_linker_spec.rb'
- - 'spec/lib/gitlab/dependency_linker/cargo_toml_linker_spec.rb'
- - 'spec/lib/gitlab/dependency_linker/cartfile_linker_spec.rb'
- - 'spec/lib/gitlab/dependency_linker/composer_json_linker_spec.rb'
- - 'spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb'
- - 'spec/lib/gitlab/dependency_linker/gemspec_linker_spec.rb'
- - 'spec/lib/gitlab/dependency_linker/go_mod_linker_spec.rb'
- - 'spec/lib/gitlab/dependency_linker/go_sum_linker_spec.rb'
- - 'spec/lib/gitlab/dependency_linker/godeps_json_linker_spec.rb'
- - 'spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb'
- - 'spec/lib/gitlab/dependency_linker/podfile_linker_spec.rb'
- - 'spec/lib/gitlab/dependency_linker/podspec_json_linker_spec.rb'
- - 'spec/lib/gitlab/dependency_linker/podspec_linker_spec.rb'
- - 'spec/lib/gitlab/dependency_linker/requirements_txt_linker_spec.rb'
- - 'spec/lib/gitlab/diff/highlight_spec.rb'
- - 'spec/lib/gitlab/diff/inline_diff_marker_spec.rb'
- - 'spec/lib/gitlab/email/handler/service_desk_handler_spec.rb'
- - 'spec/lib/gitlab/email/handler_spec.rb'
- - 'spec/lib/gitlab/email/receiver_spec.rb'
- - 'spec/lib/gitlab/encoding_helper_spec.rb'
- - 'spec/lib/gitlab/endpoint_attributes_spec.rb'
- - 'spec/lib/gitlab/error_tracking/stack_trace_highlight_decorator_spec.rb'
- - 'spec/lib/gitlab/external_authorization/client_spec.rb'
- - 'spec/lib/gitlab/favicon_spec.rb'
- - 'spec/lib/gitlab/feature_categories_spec.rb'
- - 'spec/lib/gitlab/file_detector_spec.rb'
- - 'spec/lib/gitlab/gfm/reference_rewriter_spec.rb'
- - 'spec/lib/gitlab/git/merge_base_spec.rb'
- - 'spec/lib/gitlab/git/repository_spec.rb'
- - 'spec/lib/gitlab/git_access_spec.rb'
- - 'spec/lib/gitlab/gitaly_client/operation_service_spec.rb'
- - 'spec/lib/gitlab/gitaly_client/ref_service_spec.rb'
- - 'spec/lib/gitlab/github_import/importer/note_importer_spec.rb'
- - 'spec/lib/gitlab/graphql/known_operations_spec.rb'
- - 'spec/lib/gitlab/graphql/tracers/metrics_tracer_spec.rb'
- - 'spec/lib/gitlab/hashed_path_spec.rb'
- - 'spec/lib/gitlab/highlight_spec.rb'
- - 'spec/lib/gitlab/i18n/translation_entry_spec.rb'
- - 'spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_spec.rb'
- - 'spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb'
- - 'spec/lib/gitlab/import_export/attribute_cleaner_spec.rb'
- - 'spec/lib/gitlab/import_export/attributes_permitter_spec.rb'
- - 'spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb'
- - 'spec/lib/gitlab/import_export/json/ndjson_writer_spec.rb'
- - 'spec/lib/gitlab/import_export/lfs_restorer_spec.rb'
- - 'spec/lib/gitlab/import_export/lfs_saver_spec.rb'
- - 'spec/lib/gitlab/import_export/project/tree_saver_spec.rb'
- - 'spec/lib/gitlab/import_export/saver_spec.rb'
- - 'spec/lib/gitlab/import_export/snippet_repo_restorer_spec.rb'
- - 'spec/lib/gitlab/import_sources_spec.rb'
- - 'spec/lib/gitlab/instrumentation/redis_cluster_validator_spec.rb'
- - 'spec/lib/gitlab/issues/rebalancing/state_spec.rb'
- - 'spec/lib/gitlab/jira_import/handle_labels_service_spec.rb'
- - 'spec/lib/gitlab/jira_import/issue_serializer_spec.rb'
- - 'spec/lib/gitlab/jira_import/labels_importer_spec.rb'
- - 'spec/lib/gitlab/kubernetes/kubectl_cmd_spec.rb'
- - 'spec/lib/gitlab/kubernetes/role_spec.rb'
- - 'spec/lib/gitlab/language_data_spec.rb'
- - 'spec/lib/gitlab/markup_helper_spec.rb'
- - 'spec/lib/gitlab/metrics/rails_slis_spec.rb'
- - 'spec/lib/gitlab/metrics/samplers/threads_sampler_spec.rb'
- - 'spec/lib/gitlab/middleware/go_spec.rb'
- - 'spec/lib/gitlab/middleware/multipart_spec.rb'
- - 'spec/lib/gitlab/omniauth_initializer_spec.rb'
- - 'spec/lib/gitlab/pagination/keyset/order_spec.rb'
- - 'spec/lib/gitlab/pagination/offset_header_builder_spec.rb'
- - 'spec/lib/gitlab/path_regex_spec.rb'
- - 'spec/lib/gitlab/popen_spec.rb'
- - 'spec/lib/gitlab/process_management_spec.rb'
- - 'spec/lib/gitlab/process_supervisor_spec.rb'
- - 'spec/lib/gitlab/quick_actions/extractor_spec.rb'
- - 'spec/lib/gitlab/reference_extractor_spec.rb'
- - 'spec/lib/gitlab/repository_cache_adapter_spec.rb'
- - 'spec/lib/gitlab/repository_hash_cache_spec.rb'
- - 'spec/lib/gitlab/repository_set_cache_spec.rb'
- - 'spec/lib/gitlab/search/abuse_detection_spec.rb'
- - 'spec/lib/gitlab/search_results_spec.rb'
- - 'spec/lib/gitlab/security/scan_configuration_spec.rb'
- - 'spec/lib/gitlab/shard_health_cache_spec.rb'
- - 'spec/lib/gitlab/sidekiq_config/cli_methods_spec.rb'
- - 'spec/lib/gitlab/sidekiq_config/worker_matcher_spec.rb'
- - 'spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb'
- - 'spec/lib/gitlab/sidekiq_middleware/worker_context/client_spec.rb'
- - 'spec/lib/gitlab/sidekiq_status_spec.rb'
- - 'spec/lib/gitlab/ssh_public_key_spec.rb'
- - 'spec/lib/gitlab/string_range_marker_spec.rb'
- - 'spec/lib/gitlab/string_regex_marker_spec.rb'
- - 'spec/lib/gitlab/suggestions/suggestion_set_spec.rb'
- - 'spec/lib/gitlab/task_helpers_spec.rb'
- - 'spec/lib/gitlab/tracking/event_definition_spec.rb'
- - 'spec/lib/gitlab/url_sanitizer_spec.rb'
- - 'spec/lib/gitlab/usage/metric_definition_spec.rb'
- - 'spec/lib/gitlab/usage/metric_spec.rb'
- - 'spec/lib/gitlab/usage/service_ping/instrumented_payload_spec.rb'
- - 'spec/lib/gitlab/usage_data_spec.rb'
- - 'spec/lib/gitlab/utils/log_limited_array_spec.rb'
- - 'spec/lib/gitlab/webpack/graphql_known_operations_spec.rb'
- - 'spec/lib/gitlab/wiki_pages/front_matter_parser_spec.rb'
- - 'spec/lib/object_storage/config_spec.rb'
- - 'spec/lib/object_storage/direct_upload_spec.rb'
- - 'spec/lib/rouge/formatters/html_gitlab_spec.rb'
- - 'spec/lib/safe_zip/entry_spec.rb'
- - 'spec/lib/safe_zip/extract_params_spec.rb'
- - 'spec/lib/safe_zip/extract_spec.rb'
- - 'spec/lib/security/ci_configuration/container_scanning_build_action_spec.rb'
- - 'spec/lib/security/ci_configuration/sast_build_action_spec.rb'
- - 'spec/lib/security/ci_configuration/sast_iac_build_action_spec.rb'
- - 'spec/lib/security/ci_configuration/secret_detection_build_action_spec.rb'
- - 'spec/lib/sidebars/menu_spec.rb'
- - 'spec/lib/system_check/orphans/namespace_check_spec.rb'
- - 'spec/lib/system_check/orphans/repository_check_spec.rb'
- - 'spec/lib/system_check/sidekiq_check_spec.rb'
- - 'spec/lib/unnested_in_filters/dsl_spec.rb'
- - 'spec/lib/unnested_in_filters/rewriter_spec.rb'
- - 'spec/metrics_server/metrics_server_spec.rb'
- - 'spec/models/alert_management/http_integration_spec.rb'
- - 'spec/models/appearance_spec.rb'
- - 'spec/models/application_setting_spec.rb'
- - 'spec/models/authentication_event_spec.rb'
- - 'spec/models/blob_viewer/base_spec.rb'
- - 'spec/models/ci/build_dependencies_spec.rb'
- - 'spec/models/ci/build_spec.rb'
- - 'spec/models/ci/job_artifact_spec.rb'
- - 'spec/models/ci/pipeline_spec.rb'
- - 'spec/models/ci/runner_spec.rb'
- - 'spec/models/clusters/agent_spec.rb'
- - 'spec/models/clusters/platforms/kubernetes_spec.rb'
- - 'spec/models/commit_range_spec.rb'
- - 'spec/models/commit_spec.rb'
- - 'spec/models/commit_status_spec.rb'
- - 'spec/models/compare_spec.rb'
- - 'spec/models/concerns/awardable_spec.rb'
- - 'spec/models/concerns/case_sensitivity_spec.rb'
- - 'spec/models/concerns/featurable_spec.rb'
- - 'spec/models/concerns/ignorable_columns_spec.rb'
- - 'spec/models/concerns/issuable_spec.rb'
- - 'spec/models/concerns/pg_full_text_searchable_spec.rb'
- - 'spec/models/concerns/project_features_compatibility_spec.rb'
- - 'spec/models/concerns/reactive_caching_spec.rb'
- - 'spec/models/concerns/sortable_spec.rb'
- - 'spec/models/deployment_spec.rb'
- - 'spec/models/diff_viewer/base_spec.rb'
- - 'spec/models/environment_spec.rb'
- - 'spec/models/group_label_spec.rb'
- - 'spec/models/instance_configuration_spec.rb'
- - 'spec/models/integration_spec.rb'
- - 'spec/models/integrations/buildkite_spec.rb'
- - 'spec/models/integrations/campfire_spec.rb'
- - 'spec/models/integrations/jira_spec.rb'
- - 'spec/models/integrations/teamcity_spec.rb'
- - 'spec/models/issue_spec.rb'
- - 'spec/models/merge_request_diff_spec.rb'
- - 'spec/models/namespace_statistics_spec.rb'
- - 'spec/models/packages/package_spec.rb'
- - 'spec/models/packages/tag_spec.rb'
- - 'spec/models/pages_domain_spec.rb'
- - 'spec/models/personal_access_token_spec.rb'
- - 'spec/models/project_feature_spec.rb'
- - 'spec/models/project_label_spec.rb'
- - 'spec/models/project_spec.rb'
- - 'spec/models/projects/topic_spec.rb'
- - 'spec/models/prometheus_metric_spec.rb'
- - 'spec/models/releases/link_spec.rb'
- - 'spec/models/repository_spec.rb'
- - 'spec/models/snippet_spec.rb'
- - 'spec/models/terraform/state_spec.rb'
- - 'spec/models/user_spec.rb'
- - 'spec/models/web_ide_terminal_spec.rb'
- - 'spec/models/zoom_meeting_spec.rb'
- - 'spec/policies/project_policy_spec.rb'
- - 'spec/presenters/ci/pipeline_artifacts/code_coverage_presenter_spec.rb'
- - 'spec/presenters/ci/pipeline_artifacts/code_quality_mr_diff_presenter_spec.rb'
- - 'spec/presenters/packages/nuget/packages_metadata_presenter_spec.rb'
- - 'spec/presenters/packages/nuget/search_results_presenter_spec.rb'
- - 'spec/requests/api/badges_spec.rb'
- - 'spec/requests/api/ci/jobs_spec.rb'
- - 'spec/requests/api/ci/pipelines_spec.rb'
- - 'spec/requests/api/ci/runner/jobs_request_post_spec.rb'
- - 'spec/requests/api/ci/runner/runners_post_spec.rb'
- - 'spec/requests/api/ci/triggers_spec.rb'
- - 'spec/requests/api/container_repositories_spec.rb'
- - 'spec/requests/api/deployments_spec.rb'
- - 'spec/requests/api/geo_spec.rb'
- - 'spec/requests/api/graphql/ci/manual_variables_spec.rb'
- - 'spec/requests/api/graphql/gitlab_schema_spec.rb'
- - 'spec/requests/api/graphql/group/container_repositories_spec.rb'
- - 'spec/requests/api/graphql/group/milestones_spec.rb'
- - 'spec/requests/api/graphql/mutations/design_management/delete_spec.rb'
- - 'spec/requests/api/graphql/mutations/snippets/create_spec.rb'
- - 'spec/requests/api/graphql/mutations/snippets/destroy_spec.rb'
- - 'spec/requests/api/graphql/project/base_service_spec.rb'
- - 'spec/requests/api/graphql/project/container_repositories_spec.rb'
- - 'spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb'
- - 'spec/requests/api/graphql/project/issue_spec.rb'
- - 'spec/requests/api/graphql/project/jira_import_spec.rb'
- - 'spec/requests/api/graphql/project/jira_projects_spec.rb'
- - 'spec/requests/api/graphql/project/release_spec.rb'
- - 'spec/requests/api/graphql/project/terraform/state_spec.rb'
- - 'spec/requests/api/graphql/project/terraform/states_spec.rb'
- - 'spec/requests/api/internal/base_spec.rb'
- - 'spec/requests/api/issues/get_group_issues_spec.rb'
- - 'spec/requests/api/issues/get_project_issues_spec.rb'
- - 'spec/requests/api/issues/issues_spec.rb'
- - 'spec/requests/api/issues/post_projects_issues_spec.rb'
- - 'spec/requests/api/issues/put_projects_issues_spec.rb'
- - 'spec/requests/api/merge_requests_spec.rb'
- - 'spec/requests/api/metadata_spec.rb'
- - 'spec/requests/api/project_container_repositories_spec.rb'
- - 'spec/requests/api/project_templates_spec.rb'
- - 'spec/requests/api/projects_spec.rb'
- - 'spec/requests/api/releases_spec.rb'
- - 'spec/requests/api/repositories_spec.rb'
- - 'spec/requests/api/search_spec.rb'
- - 'spec/requests/api/settings_spec.rb'
- - 'spec/requests/api/tags_spec.rb'
- - 'spec/requests/api/task_completion_status_spec.rb'
- - 'spec/requests/api/unleash_spec.rb'
- - 'spec/requests/api/users_spec.rb'
- - 'spec/requests/api/wikis_spec.rb'
- - 'spec/requests/jwt_controller_spec.rb'
- - 'spec/requests/lfs_locks_api_spec.rb'
- - 'spec/requests/users_controller_spec.rb'
- - 'spec/routing/uploads_routing_spec.rb'
- - 'spec/rubocop/cop/migration/migration_record_spec.rb'
- - 'spec/rubocop/cop/migration/prevent_index_creation_spec.rb'
- - 'spec/rubocop/cop/migration/sidekiq_queue_migrate_spec.rb'
- - 'spec/rubocop/cop/performance/readlines_each_spec.rb'
- - 'spec/serializers/build_details_entity_spec.rb'
- - 'spec/serializers/container_repositories_serializer_spec.rb'
- - 'spec/serializers/diff_file_entity_spec.rb'
- - 'spec/serializers/group_child_entity_spec.rb'
- - 'spec/services/award_emojis/copy_service_spec.rb'
- - 'spec/services/bulk_imports/file_download_service_spec.rb'
- - 'spec/services/bulk_imports/lfs_objects_export_service_spec.rb'
- - 'spec/services/ci/create_pipeline_service/pre_post_stages_spec.rb'
- - 'spec/services/ci/create_pipeline_service/rules_spec.rb'
- - 'spec/services/ci/create_pipeline_service_spec.rb'
- - 'spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb'
- - 'spec/services/ci/register_job_service_spec.rb'
- - 'spec/services/ci/retry_pipeline_service_spec.rb'
- - 'spec/services/ci/runners/register_runner_service_spec.rb'
- - 'spec/services/ci/stuck_builds/drop_pending_service_spec.rb'
- - 'spec/services/ci/stuck_builds/drop_running_service_spec.rb'
- - 'spec/services/ci/stuck_builds/drop_scheduled_service_spec.rb'
- - 'spec/services/clusters/kubernetes/create_or_update_service_account_service_spec.rb'
- - 'spec/services/deployments/update_environment_service_spec.rb'
- - 'spec/services/design_management/copy_design_collection/copy_service_spec.rb'
- - 'spec/services/git/branch_push_service_spec.rb'
- - 'spec/services/git/process_ref_changes_service_spec.rb'
- - 'spec/services/groups/update_statistics_service_spec.rb'
- - 'spec/services/import/gitlab_projects/create_project_service_spec.rb'
- - 'spec/services/issuable/process_assignees_spec.rb'
- - 'spec/services/issues/export_csv_service_spec.rb'
- - 'spec/services/jira/requests/projects/list_service_spec.rb'
- - 'spec/services/lfs/file_transformer_spec.rb'
- - 'spec/services/merge_requests/conflicts/resolve_service_spec.rb'
- - 'spec/services/merge_requests/merge_service_spec.rb'
- - 'spec/services/merge_requests/pushed_branches_service_spec.rb'
- - 'spec/services/merge_requests/refresh_service_spec.rb'
- - 'spec/services/packages/create_dependency_service_spec.rb'
- - 'spec/services/packages/nuget/create_dependency_service_spec.rb'
- - 'spec/services/packages/nuget/update_package_from_metadata_service_spec.rb'
- - 'spec/services/packages/update_tags_service_spec.rb'
- - 'spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb'
- - 'spec/services/product_analytics/build_graph_service_spec.rb'
- - 'spec/services/projects/branches_by_mode_service_spec.rb'
- - 'spec/services/projects/lfs_pointers/lfs_link_service_spec.rb'
- - 'spec/services/projects/operations/update_service_spec.rb'
- - 'spec/services/projects/record_target_platforms_service_spec.rb'
- - 'spec/services/projects/update_statistics_service_spec.rb'
- - 'spec/services/quick_actions/interpret_service_spec.rb'
- - 'spec/services/upload_service_spec.rb'
- - 'spec/sidekiq_cluster/sidekiq_cluster_spec.rb'
- - 'spec/support/atlassian/jira_connect/schemata.rb'
- - 'spec/support/capybara.rb'
- - 'spec/support/helpers/gpg_helpers.rb'
- - 'spec/support/helpers/login_helpers.rb'
- - 'spec/support/helpers/prometheus_helpers.rb'
- - 'spec/support/helpers/repo_helpers.rb'
- - 'spec/support/helpers/seed_repo.rb'
- - 'spec/support/helpers/test_env.rb'
- - 'spec/support/helpers/usage_data_helpers.rb'
- - 'spec/support/import_export/configuration_helper.rb'
- - 'spec/support/import_export/export_file_helper.rb'
- - 'spec/support/matchers/markdown_matchers.rb'
- - 'spec/support/shared_contexts/graphql/resolvers/runners_resolver_shared_context.rb'
- - 'spec/support/shared_contexts/services/projects/container_repository/delete_tags_service_shared_context.rb'
- - 'spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb'
- - 'spec/support/shared_examples/features/page_description_shared_examples.rb'
- - 'spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb'
- - 'spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb'
- - 'spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb'
- - 'spec/support/shared_examples/finders/issues_finder_shared_examples.rb'
- - 'spec/support/shared_examples/lib/banzai/filters/sanitization_filter_shared_examples.rb'
- - 'spec/support/shared_examples/lib/gitlab/middleware/multipart_shared_examples.rb'
- - 'spec/support/shared_examples/metrics/transaction_metrics_with_labels_shared_examples.rb'
- - 'spec/support/shared_examples/models/application_setting_shared_examples.rb'
- - 'spec/support/shared_examples/models/diff_positionable_note_shared_examples.rb'
- - 'spec/support/shared_examples/models/project_ci_cd_settings_shared_examples.rb'
- - 'spec/support/shared_examples/models/wiki_shared_examples.rb'
- - 'spec/support/shared_examples/path_extraction_shared_examples.rb'
- - 'spec/support/shared_examples/quick_actions/commit/tag_quick_action_shared_examples.rb'
- - 'spec/support/shared_examples/quick_actions/issue/create_merge_request_quick_action_shared_examples.rb'
- - 'spec/support/shared_examples/requests/api/container_repositories_shared_examples.rb'
- - 'spec/support/shared_examples/requests/api/rubygems_packages_shared_examples.rb'
- - 'spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb'
- - 'spec/support/shared_examples/validators/url_validator_shared_examples.rb'
- - 'spec/support_specs/graphql/arguments_spec.rb'
- - 'spec/support_specs/helpers/active_record/query_recorder_spec.rb'
- - 'spec/support_specs/matchers/exceed_query_limit_helpers_spec.rb'
- - 'spec/tasks/gitlab/db_rake_spec.rb'
- - 'spec/tooling/danger/customer_success_spec.rb'
- - 'spec/tooling/danger/datateam_spec.rb'
- - 'spec/tooling/danger/sidekiq_queues_spec.rb'
- - 'spec/tooling/lib/tooling/test_map_generator_spec.rb'
- - 'spec/uploaders/attachment_uploader_spec.rb'
- - 'spec/uploaders/avatar_uploader_spec.rb'
- - 'spec/uploaders/ci/pipeline_artifact_uploader_spec.rb'
- - 'spec/uploaders/dependency_proxy/file_uploader_spec.rb'
- - 'spec/uploaders/design_management/design_v432x230_uploader_spec.rb'
- - 'spec/uploaders/external_diff_uploader_spec.rb'
- - 'spec/uploaders/import_export_uploader_spec.rb'
- - 'spec/uploaders/job_artifact_uploader_spec.rb'
- - 'spec/uploaders/lfs_object_uploader_spec.rb'
- - 'spec/uploaders/namespace_file_uploader_spec.rb'
- - 'spec/uploaders/object_storage_spec.rb'
- - 'spec/uploaders/packages/composer/cache_uploader_spec.rb'
- - 'spec/uploaders/packages/debian/component_file_uploader_spec.rb'
- - 'spec/uploaders/packages/debian/distribution_release_file_uploader_spec.rb'
- - 'spec/uploaders/packages/package_file_uploader_spec.rb'
- - 'spec/uploaders/pages/deployment_uploader_spec.rb'
- - 'spec/uploaders/personal_file_uploader_spec.rb'
- - 'spec/validators/any_field_validator_spec.rb'
- - 'spec/views/layouts/_head.html.haml_spec.rb'
- - 'spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb'
- - 'spec/views/projects/commit/branches.html.haml_spec.rb'
- - 'spec/workers/concerns/worker_context_spec.rb'
- - 'spec/workers/container_registry/migration/enqueuer_worker_spec.rb'
- - 'spec/workers/groups/update_statistics_worker_spec.rb'
- - 'spec/workers/jira_connect/sync_branch_worker_spec.rb'
- - 'spec/workers/post_receive_spec.rb'
- - 'spec/workers/project_cache_worker_spec.rb'
- - 'spec/workers/projects/record_target_platforms_worker_spec.rb'
- - 'spec/workers/stuck_merge_jobs_worker_spec.rb'
- - 'spec/workers/update_project_statistics_worker_spec.rb'
diff --git a/.rubocop_todo/style/redundant_interpolation.yml b/.rubocop_todo/style/redundant_interpolation.yml
index aa3efe18d50..d50db1cbbbd 100644
--- a/.rubocop_todo/style/redundant_interpolation.yml
+++ b/.rubocop_todo/style/redundant_interpolation.yml
@@ -18,7 +18,6 @@ Style/RedundantInterpolation:
- 'lib/gitlab/repository_hash_cache.rb'
- 'lib/gitlab/repository_set_cache.rb'
- 'lib/gitlab/usage_data_counters/search_counter.rb'
- - 'lib/gitlab/utils.rb'
- 'lib/kramdown/converter/commonmark.rb'
- 'lib/tasks/gettext.rake'
- 'lib/tasks/gitlab/seed/group_seed.rake'
@@ -28,7 +27,6 @@ Style/RedundantInterpolation:
- 'qa/qa/resource/events/base.rb'
- 'qa/qa/service/praefect_manager.rb'
- 'qa/qa/specs/features/browser_ui/4_verify/pipeline/include_multiple_files_from_a_project_spec.rb'
- - 'qa/qa/specs/features/browser_ui/5_package/container_registry/container_registry_spec.rb'
- 'qa/qa/specs/features/ee/browser_ui/3_create/repository/project_templates_spec.rb'
- 'qa/qa/specs/features/ee/browser_ui/4_verify/parent_child_pipelines_dependent_relationship_spec.rb'
- 'qa/qa/tools/generate_perf_testdata.rb'
diff --git a/.rubocop_todo/style/redundant_regexp_escape.yml b/.rubocop_todo/style/redundant_regexp_escape.yml
index 93aab692379..318a9b5320b 100644
--- a/.rubocop_todo/style/redundant_regexp_escape.yml
+++ b/.rubocop_todo/style/redundant_regexp_escape.yml
@@ -6,7 +6,6 @@ Style/RedundantRegexpEscape:
- 'app/controllers/import/bitbucket_server_controller.rb'
- 'app/helpers/emails_helper.rb'
- 'app/helpers/import_helper.rb'
- - 'app/helpers/sidekiq_helper.rb'
- 'app/models/commit_status.rb'
- 'app/models/concerns/referable.rb'
- 'app/models/deploy_token.rb'
@@ -20,7 +19,6 @@ Style/RedundantRegexpEscape:
- 'app/models/operations/feature_flag.rb'
- 'app/models/releases/link.rb'
- 'app/models/snippet.rb'
- - 'app/services/metrics/dashboard/grafana_metric_embed_service.rb'
- 'app/uploaders/file_uploader.rb'
- 'config/routes/admin.rb'
- 'config/routes/group.rb'
@@ -37,7 +35,6 @@ Style/RedundantRegexpEscape:
- 'ee/lib/gitlab/return_to_location.rb'
- 'ee/spec/features/read_only_spec.rb'
- 'ee/spec/helpers/vulnerabilities_helper_spec.rb'
- - 'ee/spec/lib/ee/gitlab/usage_data_counters/hll_redis_counter_spec.rb'
- 'ee/spec/mailers/ee/emails/admin_notification_spec.rb'
- 'ee/spec/mailers/ee/emails/profile_spec.rb'
- 'ee/spec/mailers/notify_spec.rb'
@@ -73,10 +70,8 @@ Style/RedundantRegexpEscape:
- 'lib/gitlab/search/abuse_detection.rb'
- 'lib/gitlab/task_helpers.rb'
- 'lib/gitlab/url_sanitizer.rb'
- - 'lib/gitlab/utils.rb'
- 'lib/gitlab/utils/sanitize_node_link.rb'
- 'lib/gitlab/word_diff/segments/diff_hunk.rb'
- - 'lib/product_analytics/tracker.rb'
- 'lib/tasks/gitlab/info.rake'
- 'qa/spec/runtime/key/ecdsa_spec.rb'
- 'qa/spec/runtime/key/ed25519_spec.rb'
diff --git a/.rubocop_todo/style/redundant_return.yml b/.rubocop_todo/style/redundant_return.yml
new file mode 100644
index 00000000000..344dbbbc4cd
--- /dev/null
+++ b/.rubocop_todo/style/redundant_return.yml
@@ -0,0 +1,102 @@
+---
+# Cop supports --autocorrect.
+Style/RedundantReturn:
+ Details: grace period
+ Exclude:
+ - 'app/controllers/concerns/hotlink_interceptor.rb'
+ - 'app/controllers/concerns/issuable_collections.rb'
+ - 'app/controllers/concerns/notes_actions.rb'
+ - 'app/controllers/concerns/snippet_authorizations.rb'
+ - 'app/controllers/groups/labels_controller.rb'
+ - 'app/controllers/groups/milestones_controller.rb'
+ - 'app/controllers/groups/registry/repositories_controller.rb'
+ - 'app/controllers/groups/settings/ci_cd_controller.rb'
+ - 'app/controllers/groups/variables_controller.rb'
+ - 'app/controllers/import/bitbucket_server_controller.rb'
+ - 'app/controllers/import/github_controller.rb'
+ - 'app/controllers/profiles_controller.rb'
+ - 'app/controllers/projects/application_controller.rb'
+ - 'app/controllers/projects/artifacts_controller.rb'
+ - 'app/controllers/projects/blob_controller.rb'
+ - 'app/controllers/projects/jobs_controller.rb'
+ - 'app/controllers/projects/labels_controller.rb'
+ - 'app/controllers/projects/merge_requests/conflicts_controller.rb'
+ - 'app/controllers/projects/merge_requests/diffs_controller.rb'
+ - 'app/controllers/projects/merge_requests_controller.rb'
+ - 'app/controllers/projects/milestones_controller.rb'
+ - 'app/controllers/projects/notes_controller.rb'
+ - 'app/controllers/projects/pipeline_schedules_controller.rb'
+ - 'app/controllers/projects/pipelines_controller.rb'
+ - 'app/controllers/projects/refs_controller.rb'
+ - 'app/controllers/projects/snippets/application_controller.rb'
+ - 'app/controllers/projects/web_ide_terminals_controller.rb'
+ - 'app/controllers/sent_notifications_controller.rb'
+ - 'app/controllers/snippets/notes_controller.rb'
+ - 'app/helpers/profiles_helper.rb'
+ - 'app/models/clusters/cluster.rb'
+ - 'app/models/concerns/cascading_namespace_setting_attribute.rb'
+ - 'app/models/namespace.rb'
+ - 'app/models/namespaces/randomized_suffix_path.rb'
+ - 'app/models/notification_recipient.rb'
+ - 'app/models/packages/debian/publication.rb'
+ - 'app/models/releases/link.rb'
+ - 'app/models/work_items/widgets/hierarchy.rb'
+ - 'app/presenters/packages/nuget/service_index_presenter.rb'
+ - 'app/presenters/packages/nuget/version_helpers.rb'
+ - 'app/services/boards/base_item_move_service.rb'
+ - 'app/services/error_tracking/base_service.rb'
+ - 'app/services/error_tracking/issue_update_service.rb'
+ - 'app/services/jira_import/start_import_service.rb'
+ - 'app/services/releases/update_service.rb'
+ - 'ee/app/controllers/concerns/audit_events/enforces_valid_date_params.rb'
+ - 'ee/app/controllers/concerns/description_diff_actions.rb'
+ - 'ee/app/controllers/ee/dashboard/projects_controller.rb'
+ - 'ee/app/controllers/ee/projects/environments_controller.rb'
+ - 'ee/app/controllers/ee/projects/merge_requests_controller.rb'
+ - 'ee/app/controllers/groups/analytics/application_controller.rb'
+ - 'ee/app/controllers/groups/analytics/cycle_analytics/stages_controller.rb'
+ - 'ee/app/controllers/groups/analytics/cycle_analytics/summary_controller.rb'
+ - 'ee/app/controllers/groups/analytics/tasks_by_type_controller.rb'
+ - 'ee/app/controllers/groups/epics_controller.rb'
+ - 'ee/app/controllers/projects/integrations/jira/issues_controller.rb'
+ - 'ee/app/controllers/projects/integrations/zentao/issues_controller.rb'
+ - 'ee/app/controllers/projects/on_demand_scans_controller.rb'
+ - 'ee/app/controllers/projects/security/dast_site_profiles_controller.rb'
+ - 'ee/app/controllers/projects/vulnerability_feedback_controller.rb'
+ - 'ee/app/helpers/ee/application_helper.rb'
+ - 'ee/app/helpers/ee/boards_helper.rb'
+ - 'ee/app/helpers/ee/personal_access_tokens_helper.rb'
+ - 'ee/app/models/geo/upload_registry.rb'
+ - 'ee/app/serializers/vulnerabilities/feedback_entity.rb'
+ - 'ee/app/serializers/vulnerabilities/finding_entity.rb'
+ - 'ee/app/serializers/vulnerabilities/issue_link_entity.rb'
+ - 'ee/app/serializers/vulnerabilities/merge_request_link_entity.rb'
+ - 'ee/app/services/audit_events/streaming/headers/base.rb'
+ - 'ee/app/services/ee/post_receive_service.rb'
+ - 'ee/app/services/gitlab_subscriptions/user_add_on_assignments/create_service.rb'
+ - 'ee/app/services/security/orchestration/assign_service.rb'
+ - 'ee/app/services/vulnerabilities/manually_create_service.rb'
+ - 'ee/app/workers/ee/repository_check/batch_worker.rb'
+ - 'ee/app/workers/ee/repository_check/single_repository_worker.rb'
+ - 'ee/lib/api/dependency_proxy/packages/maven.rb'
+ - 'ee/lib/ee/api/entities/billable_member.rb'
+ - 'ee/lib/ee/gitlab/checks/push_rules/secrets_check.rb'
+ - 'ee/lib/gitlab/llm/chain/tools/epic_identifier/executor.rb'
+ - 'ee/lib/gitlab/llm/chain/tools/issue_identifier/executor.rb'
+ - 'lib/api/nuget_project_packages.rb'
+ - 'lib/api/pagination_params.rb'
+ - 'lib/feature/gitaly.rb'
+ - 'lib/gitlab/auth/database/authentication.rb'
+ - 'lib/gitlab/ci/parsers/coverage/sax_document.rb'
+ - 'lib/gitlab/database/health_status/indicators/prometheus_alert_indicator.rb'
+ - 'lib/gitlab/graphql/queries.rb'
+ - 'lib/gitlab/hotlinking_detector.rb'
+ - 'lib/gitlab/instrumentation_helper.rb'
+ - 'lib/gitlab/redis/wrapper.rb'
+ - 'lib/gitlab/signed_tag.rb'
+ - 'qa/qa/specs/helpers/context_selector.rb'
+ - 'qa/qa/support/formatters/allure_metadata_formatter.rb'
+ - 'qa/qa/support/formatters/context_formatter.rb'
+ - 'rubocop/cop/background_migration/dictionary_file.rb'
+ - 'scripts/api/get_package_and_test_job.rb'
+ - 'tooling/lib/tooling/mappings/graphql_base_type_mappings.rb'
diff --git a/.rubocop_todo/style/redundant_self.yml b/.rubocop_todo/style/redundant_self.yml
index 49132170448..701ce4db7df 100644
--- a/.rubocop_todo/style/redundant_self.yml
+++ b/.rubocop_todo/style/redundant_self.yml
@@ -110,7 +110,6 @@ Style/RedundantSelf:
- 'app/models/packages/dependency.rb'
- 'app/models/packages/sem_ver.rb'
- 'app/models/pages_domain.rb'
- - 'app/models/performance_monitoring/prometheus_dashboard.rb'
- 'app/models/personal_access_token.rb'
- 'app/models/plan.rb'
- 'app/models/project.rb'
@@ -132,7 +131,6 @@ Style/RedundantSelf:
- 'app/models/snippet.rb'
- 'app/models/terraform/state.rb'
- 'app/models/todo.rb'
- - 'app/models/u2f_registration.rb'
- 'app/models/upload.rb'
- 'app/models/user.rb'
- 'app/models/user_highest_role.rb'
@@ -174,7 +172,6 @@ Style/RedundantSelf:
- 'ee/app/models/concerns/ee/protected_ref.rb'
- 'ee/app/models/concerns/ee/protected_ref_access.rb'
- 'ee/app/models/concerns/elastic/application_versioned_search.rb'
- - 'ee/app/models/concerns/elastic/projects_search.rb'
- 'ee/app/models/concerns/elasticsearch_indexed_container.rb'
- 'ee/app/models/concerns/geo/replicable_model.rb'
- 'ee/app/models/concerns/geo/repository_replicator_strategy.rb'
@@ -196,13 +193,11 @@ Style/RedundantSelf:
- 'ee/app/models/ee/namespace.rb'
- 'ee/app/models/ee/packages/package_file.rb'
- 'ee/app/models/ee/project.rb'
- - 'ee/app/models/ee/project_feature.rb'
- 'ee/app/models/ee/project_import_state.rb'
- 'ee/app/models/ee/snippet_repository.rb'
- 'ee/app/models/ee/user.rb'
- 'ee/app/models/epic/metrics.rb'
- 'ee/app/models/geo/base_registry.rb'
- - 'ee/app/models/geo/project_registry.rb'
- 'ee/app/models/geo/upload_registry.rb'
- 'ee/app/models/geo_node.rb'
- 'ee/app/models/geo_node_status.rb'
@@ -306,7 +301,6 @@ Style/RedundantSelf:
- 'lib/gitlab/database/query_analyzers/base.rb'
- 'lib/gitlab/database/query_analyzers/prevent_cross_database_modification.rb'
- 'lib/gitlab/database/query_analyzers/restrict_allowed_schemas.rb'
- - 'lib/gitlab/database/rename_reserved_paths_migration/v1/migration_classes.rb'
- 'lib/gitlab/database/shared_model.rb'
- 'lib/gitlab/database/similarity_score.rb'
- 'lib/gitlab/database/transaction/observer.rb'
@@ -345,7 +339,6 @@ Style/RedundantSelf:
- 'lib/gitlab/quick_actions/dsl.rb'
- 'lib/gitlab/redis/hll.rb'
- 'lib/gitlab/routing.rb'
- - 'lib/gitlab/rugged_instrumentation.rb'
- 'lib/gitlab/search/query.rb'
- 'lib/gitlab/session.rb'
- 'lib/gitlab/sidekiq_config/cli_methods.rb'
@@ -359,7 +352,6 @@ Style/RedundantSelf:
- 'lib/gitlab/template/gitlab_ci_yml_template.rb'
- 'lib/gitlab/template/issue_template.rb'
- 'lib/gitlab/template/merge_request_template.rb'
- - 'lib/gitlab/template/metrics_dashboard_template.rb'
- 'lib/gitlab/template/service_desk_template.rb'
- 'lib/gitlab/throttle.rb'
- 'lib/gitlab/tracking/event_definition.rb'
diff --git a/.rubocop_todo/style/sole_nested_conditional.yml b/.rubocop_todo/style/sole_nested_conditional.yml
index 0417039da20..f6bda8add14 100644
--- a/.rubocop_todo/style/sole_nested_conditional.yml
+++ b/.rubocop_todo/style/sole_nested_conditional.yml
@@ -26,7 +26,6 @@ Style/SoleNestedConditional:
- 'ee/app/services/ee/merge_requests/create_pipeline_service.rb'
- 'ee/app/services/epics/tree_reorder_service.rb'
- 'ee/app/services/geo/framework_repository_sync_service.rb'
- - 'ee/app/services/geo/repository_base_sync_service.rb'
- 'ee/app/services/vulnerability_feedback/create_service.rb'
- 'ee/app/workers/ee/post_receive.rb'
- 'ee/lib/ee/gitlab/auth/o_auth/auth_hash.rb'
@@ -48,7 +47,6 @@ Style/SoleNestedConditional:
- 'lib/gitlab/email/handler/reply_processing.rb'
- 'lib/gitlab/patch/database_config.rb'
- 'lib/gitlab/user_access.rb'
- - 'lib/gitlab/utils.rb'
- 'lib/gitlab/x509/signature.rb'
- 'lib/kramdown/converter/commonmark.rb'
- 'lib/mattermost/session.rb'
diff --git a/.rubocop_todo/style/string_concatenation.yml b/.rubocop_todo/style/string_concatenation.yml
index b8afe0c6148..b14f8236aac 100644
--- a/.rubocop_todo/style/string_concatenation.yml
+++ b/.rubocop_todo/style/string_concatenation.yml
@@ -42,7 +42,6 @@ Style/StringConcatenation:
- 'ee/app/services/analytics/cycle_analytics/data_loader_service.rb'
- 'ee/app/services/ee/issues/build_service.rb'
- 'ee/app/services/geo/framework_repository_sync_service.rb'
- - 'ee/app/services/geo/repository_base_sync_service.rb'
- 'ee/app/services/merge_requests/update_blocks_service.rb'
- 'ee/app/workers/scan_security_report_secrets_worker.rb'
- 'ee/lib/api/project_mirror.rb'
@@ -71,7 +70,6 @@ Style/StringConcatenation:
- 'ee/spec/models/incident_management/issuable_resource_link_spec.rb'
- 'ee/spec/models/status_page/project_setting_spec.rb'
- 'ee/spec/services/jira/jql_builder_service_spec.rb'
- - 'ee/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb'
- 'ee/spec/services/todo_service_spec.rb'
- 'ee/spec/support/shared_examples/models/geo_framework_registry_shared_examples.rb'
- 'ee/spec/tasks/gitlab/license_rake_spec.rb'
@@ -92,7 +90,6 @@ Style/StringConcatenation:
- 'lib/gitlab/config/entry/validators.rb'
- 'lib/gitlab/console.rb'
- 'lib/gitlab/database/migration_helpers/v2.rb'
- - 'lib/gitlab/database/rename_reserved_paths_migration/v1/migration_classes.rb'
- 'lib/gitlab/database/unidirectional_copy_trigger.rb'
- 'lib/gitlab/email/handler/service_desk_handler.rb'
- 'lib/gitlab/git.rb'
@@ -126,7 +123,6 @@ Style/StringConcatenation:
- 'lib/google_api/cloud_platform/client.rb'
- 'lib/kramdown/converter/commonmark.rb'
- 'lib/mattermost/session.rb'
- - 'lib/product_analytics/tracker.rb'
- 'lib/tasks/gitlab/sidekiq.rake'
- 'lib/tasks/tanuki_emoji.rake'
- 'qa/qa/page/component/snippet.rb'
@@ -235,7 +231,6 @@ Style/StringConcatenation:
- 'spec/models/packages/package_file_spec.rb'
- 'spec/models/packages/sem_ver_spec.rb'
- 'spec/models/pages/lookup_path_spec.rb'
- - 'spec/models/project_metrics_setting_spec.rb'
- 'spec/models/project_spec.rb'
- 'spec/models/prometheus_alert_spec.rb'
- 'spec/models/releases/link_spec.rb'
@@ -258,7 +253,6 @@ Style/StringConcatenation:
- 'spec/services/error_tracking/list_projects_service_spec.rb'
- 'spec/services/groups/update_service_spec.rb'
- 'spec/services/merge_requests/build_service_spec.rb'
- - 'spec/services/metrics/dashboard/grafana_metric_embed_service_spec.rb'
- 'spec/services/packages/conan/create_package_file_service_spec.rb'
- 'spec/services/packages/debian/create_package_file_service_spec.rb'
- 'spec/services/packages/helm/extract_file_metadata_service_spec.rb'
diff --git a/.rubocop_todo/style/string_literals_in_interpolation.yml b/.rubocop_todo/style/string_literals_in_interpolation.yml
index daa9b90b177..4eced04e261 100644
--- a/.rubocop_todo/style/string_literals_in_interpolation.yml
+++ b/.rubocop_todo/style/string_literals_in_interpolation.yml
@@ -1,15 +1,13 @@
---
# Cop supports --autocorrect.
Style/StringLiteralsInInterpolation:
+ Details: grace period
Exclude:
- - 'app/graphql/mutations/base_mutation.rb'
- - 'app/helpers/colors_helper.rb'
- 'app/models/application_setting_implementation.rb'
- 'app/models/ci/namespace_mirror.rb'
- 'app/services/draft_notes/publish_service.rb'
- 'app/services/projects/create_service.rb'
- 'app/validators/nested_attributes_duplicates_validator.rb'
- - 'app/views/events/_event.atom.builder'
- 'app/workers/concerns/application_worker.rb'
- 'config/initializers/validate_database_config.rb'
- 'ee/app/helpers/ee/merge_requests_helper.rb'
@@ -17,8 +15,7 @@ Style/StringLiteralsInInterpolation:
- 'ee/app/services/epics/tree_reorder_service.rb'
- 'ee/lib/ee/api/helpers/issues_helpers.rb'
- 'ee/spec/features/admin/admin_settings_spec.rb'
- - 'ee/spec/features/subscriptions/expiring_subscription_message_spec.rb'
- - 'ee/spec/lib/gitlab/expiring_subscription_message_spec.rb'
+ - 'ee/spec/lib/gitlab/llm/chain/agents/zero_shot/executor_real_requests_spec.rb'
- 'lib/api/helpers/snippets_helpers.rb'
- 'lib/api/validations/validators/check_assignees_count.rb'
- 'lib/banzai/filter/references/abstract_reference_filter.rb'
@@ -39,15 +36,12 @@ Style/StringLiteralsInInterpolation:
- 'lib/tasks/gitlab/info.rake'
- 'lib/tasks/gitlab/sidekiq.rake'
- 'qa/qa/ee/page/component/secure_report.rb'
- - 'qa/qa/ee/page/group/secure/show.rb'
- 'qa/qa/resource/events/base.rb'
- 'qa/qa/service/cluster_provider/base.rb'
- 'qa/qa/service/cluster_provider/gcloud.rb'
- - 'qa/qa/specs/helpers/context_selector.rb'
- 'qa/qa/tools/generate_perf_testdata.rb'
- 'rubocop/cop/migration/prevent_index_creation.rb'
- 'spec/features/commits_spec.rb'
- - 'spec/features/dashboard/merge_requests_spec.rb'
- 'spec/features/users/login_spec.rb'
- 'spec/lib/banzai/filter/references/commit_reference_filter_spec.rb'
- 'spec/lib/banzai/filter/references/issue_reference_filter_spec.rb'
diff --git a/.rubocop_todo/style/symbol_proc.yml b/.rubocop_todo/style/symbol_proc.yml
index fa6cf5c4a7e..cae9839060b 100644
--- a/.rubocop_todo/style/symbol_proc.yml
+++ b/.rubocop_todo/style/symbol_proc.yml
@@ -38,7 +38,6 @@ Style/SymbolProc:
- 'app/serializers/project_entity.rb'
- 'app/serializers/project_mirror_entity.rb'
- 'app/serializers/project_note_entity.rb'
- - 'app/serializers/prometheus_alert_entity.rb'
- 'app/serializers/review_app_setup_entity.rb'
- 'app/serializers/test_suite_summary_entity.rb'
- 'app/services/badges/create_service.rb'
@@ -61,7 +60,6 @@ Style/SymbolProc:
- 'app/workers/namespaces/prune_aggregation_schedules_worker.rb'
- 'app/workers/stuck_export_jobs_worker.rb'
- 'app/workers/update_head_pipeline_for_merge_request_worker.rb'
- - 'config/initializers/01_active_record_database_tasks_configuration_flag.rb'
- 'config/initializers/doorkeeper_openid_connect.rb'
- 'config/initializers/mail_encoding_patch.rb'
- 'config/settings.rb'
@@ -113,13 +111,10 @@ Style/SymbolProc:
- 'lib/api/entities/merge_request_approvals.rb'
- 'lib/api/entities/package.rb'
- 'lib/api/entities/protected_ref_access.rb'
- - 'lib/api/github/entities.rb'
- 'lib/api/go_proxy.rb'
- 'lib/api/helpers/internal_helpers.rb'
- 'lib/api/package_files.rb'
- 'lib/atlassian/jira_connect/serializers/base_entity.rb'
- - 'lib/banzai/filter/inline_cluster_metrics_filter.rb'
- - 'lib/banzai/filter/inline_embeds_filter.rb'
- 'lib/bulk_imports/common/pipelines/entity_finisher.rb'
- 'lib/bulk_imports/ndjson_pipeline.rb'
- 'lib/container_registry/client.rb'
@@ -145,8 +140,6 @@ Style/SymbolProc:
- 'lib/gitlab/import_export/fast_hash_serializer.rb'
- 'lib/gitlab/import_export/group/relation_tree_restorer.rb'
- 'lib/gitlab/manifest_import/manifest.rb'
- - 'lib/gitlab/metrics/dashboard/importers/prometheus_metrics.rb'
- - 'lib/gitlab/metrics/dashboard/url.rb'
- 'lib/gitlab/quick_actions/extractor.rb'
- 'lib/gitlab/quick_actions/merge_request_actions.rb'
- 'lib/gitlab/search/found_blob.rb'
@@ -163,10 +156,8 @@ Style/SymbolProc:
- 'qa/qa/page/profile/two_factor_auth.rb'
- 'qa/qa/resource/project_snippet.rb'
- 'qa/qa/runtime/ip_address.rb'
- - 'qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb'
- 'qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb'
- 'qa/qa/specs/features/browser_ui/3_create/web_ide_old/review_merge_request_spec.rb'
- - 'qa/qa/specs/features/browser_ui/5_package/container_registry/online_garbage_collection_spec.rb'
- 'qa/qa/specs/features/ee/browser_ui/13_secure/enable_scanning_from_configuration_spec.rb'
- 'qa/qa/specs/features/ee/browser_ui/3_create/merge_request/approval_rules_spec.rb'
- 'qa/qa/specs/features/ee/browser_ui/3_create/repository/file_locking_spec.rb'
@@ -176,7 +167,6 @@ Style/SymbolProc:
- 'scripts/qa/testcases-check'
- 'scripts/static-analysis'
- 'spec/controllers/concerns/product_analytics_tracking_spec.rb'
- - 'spec/controllers/concerns/redis_tracking_spec.rb'
- 'spec/controllers/projects/merge_requests/conflicts_controller_spec.rb'
- 'spec/factories/application_settings.rb'
- 'spec/factories/ci/builds.rb'
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 49a48bc27e7..7d29ef1a9d0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -688,6 +688,31 @@ entry.
- [Alias read_namespace to access_namespace and move usages to new ability](gitlab-org/gitlab@61cdb4127143162a9bf9182f9c3c2d8421ee447f) by @Taucher2003 ([merge request](gitlab-org/gitlab!126625))
- [Remove `custom_roles_on_groups` feature flag](gitlab-org/gitlab@ddb4b4399b8bb82793410005c5778a002ae409b9) ([merge request](gitlab-org/gitlab!132187)) **GitLab Enterprise Edition**
+## 16.4.2 (2023-10-30)
+
+### Fixed (4 changes)
+
+- [Fix pipeline schedules view when owner is nil](gitlab-org/security/gitlab@663b1328b6e05e472f60ebdcec9866220b88d066)
+- [Update dependency prometheus-client-mmap to '>= 0.28.1'](gitlab-org/security/gitlab@a478482f3616bfe205e10fcca997b0e6f133d692)
+- [Fix failing migration when commit_message_negative_regex is missing](gitlab-org/security/gitlab@1488c3ed6568d44aa7c8b7d0551fa8160b59c1dc)
+- [Backport fix flaky epic tests](gitlab-org/security/gitlab@155cb51939d5b3c1f4b847219a5cb62c9f2ae1b0) **GitLab Enterprise Edition**
+
+### Security (9 changes)
+
+- [Fix infinite loop when finding component project](gitlab-org/security/gitlab@b38efc987c1081fcd092c96e69c7ebb539324679) ([merge request](gitlab-org/security/gitlab!3666))
+- [Update gitlab-chronic-duration to 0.12](gitlab-org/security/gitlab@1c8dd2e890c1121b1c1ad947f701a39ec6ac5310) ([merge request](gitlab-org/security/gitlab!3628))
+- [Guard gitlab_version_check helper](gitlab-org/security/gitlab@e6c833ee0da8a801f08d5d7411b4ff683d0cde31) ([merge request](gitlab-org/security/gitlab!3653))
+- [Add the environment action to the CI JWT token fields](gitlab-org/security/gitlab@10fe34349f2e1d8230b805316f559b2dde8e6240) ([merge request](gitlab-org/security/gitlab!3616))
+- [Remove FIFO files from tarball extract](gitlab-org/security/gitlab@5d5acf918d68ffcf193a7c477c637788aadd882e) ([merge request](gitlab-org/security/gitlab!3633))
+- [Backport add abuse detection for pipes](gitlab-org/security/gitlab@2ccde2dbbcc647c8fc34ebb71c5472e2b70560ab) ([merge request](gitlab-org/security/gitlab!3618))
+- [Prevent unprivileged user assignment in templated projects](gitlab-org/security/gitlab@977371d7af40caa2a9b8fb18fe093be12d2e8443) ([merge request](gitlab-org/security/gitlab!3636))
+- [Fixes Service Desk email template issue description privileges](gitlab-org/security/gitlab@6e8e58e222937232397502828fa0985dee1bf786) ([merge request](gitlab-org/security/gitlab!3640))
+- [Update mermaid version for DOS fixes](gitlab-org/security/gitlab@5047299db60c7ab27fb521812d04dee7e70e319b) ([merge request](gitlab-org/security/gitlab!3626))
+
+### Other (1 change)
+
+- [Create Geo event when project is created](gitlab-org/security/gitlab@1f743fba3af02ab30e65c03da7f088610880a90a) **GitLab Enterprise Edition**
+
## 16.4.1 (2023-09-28)
### Security (15 changes)
@@ -1434,6 +1459,20 @@ entry.
- [Convert design_user_mentions.note_id to bigint for self-managed](gitlab-org/gitlab@08219da99fc356fecc4e9965fe1891baca4d10ff) ([merge request](gitlab-org/gitlab!129111))
- [Migrate etag cache store from SharedState to Cache](gitlab-org/gitlab@6476298fcdcf77206fa768bcca6bd1e3c7994936) ([merge request](gitlab-org/gitlab!129050))
+## 16.3.6 (2023-10-30)
+
+### Security (9 changes)
+
+- [Fix infinite loop when finding component project](gitlab-org/security/gitlab@a1c1255f8f767f1b9a26aee1008ef6a286988a1d) ([merge request](gitlab-org/security/gitlab!3667))
+- [Update gitlab-chronic-duration to 0.12](gitlab-org/security/gitlab@89ed5a67a26c362d197eae4f3228755a5e3a1c03) ([merge request](gitlab-org/security/gitlab!3630))
+- [Guard gitlab_version_check helper](gitlab-org/security/gitlab@b8f490fc3cfe465d46666380b17c065669c216e1) ([merge request](gitlab-org/security/gitlab!3654))
+- [Add the environment action to the CI JWT token fields](gitlab-org/security/gitlab@0563e1a02c2b6886cc21c4dfbedd975c102f0fbb) ([merge request](gitlab-org/security/gitlab!3615))
+- [Remove FIFO files from tarball extract](gitlab-org/security/gitlab@d794f0c972e2e081c0ed78ed5001bdd111688641) ([merge request](gitlab-org/security/gitlab!3634))
+- [Backport add abuse detection for pipes](gitlab-org/security/gitlab@84a3debec3ce0473598d4681850ccca74a892b30) ([merge request](gitlab-org/security/gitlab!3619))
+- [Prevent unprivileged user assignment in templated projects](gitlab-org/security/gitlab@b4ba31c793317dee41382f7a41af4637f38cddaa) ([merge request](gitlab-org/security/gitlab!3637))
+- [Fixes Service Desk email template issue description privileges](gitlab-org/security/gitlab@223765ae04031afda38f10e8487a3785ab53032b) ([merge request](gitlab-org/security/gitlab!3639))
+- [Update mermaid version for DOS fixes](gitlab-org/security/gitlab@602b89ced4ccad048819fc1603d6e978fd58c882) ([merge request](gitlab-org/security/gitlab!3627))
+
## 16.3.5 (2023-09-28)
### Security (16 changes)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index ac3fbaddf8f..5b9cce59508 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,12 +1,12 @@
## Contributor License Agreement and Developer Certificate of Origin
-Contributions to this repository are subject to the [Developer Certificate of Origin](https://docs.gitlab.com/ee/legal/developer_certificate_of_origin.html#developer-certificate-of-origin-version-11), or the [Individual](https://docs.gitlab.com/ee/legal/individual_contributor_license_agreement.html) or [Corporate](https://docs.gitlab.com/ee/legal/corporate_contributor_license_agreement.html) Contributor License Agreement, depending on where the contribution is made and on whose behalf:
+Contributions to this repository are subject to the [Developer Certificate of Origin](doc/legal/developer_certificate_of_origin.md#developer-certificate-of-origin-version-11), or the [Individual](doc/legal/individual_contributor_license_agreement.md) or [Corporate](doc/legal/corporate_contributor_license_agreement.md) Contributor License Agreement, depending on where the contribution is made and on whose behalf:
-- By submitting code contributions as an individual to the [`/ee` subdirectory](/ee) of this repository, you agree to the [Individual Contributor License Agreement](https://docs.gitlab.com/ee/legal/individual_contributor_license_agreement.html).
+- By submitting code contributions as an individual to the [`/ee` subdirectory](/ee) of this repository, you agree to the [Individual Contributor License Agreement](doc/legal/individual_contributor_license_agreement.md).
-- By submitting code contributions on behalf of a corporation to the [`/ee` subdirectory](/ee) of this repository, you agree to the [Corporate Contributor License Agreement](https://docs.gitlab.com/ee/legal/corporate_contributor_license_agreement.html).
+- By submitting code contributions on behalf of a corporation to the [`/ee` subdirectory](/ee) of this repository, you agree to the [Corporate Contributor License Agreement](doc/legal/corporate_contributor_license_agreement.md).
-- By submitting code contributions as an individual or on behalf of a corporation to any directory in this repository outside of the [`/ee` subdirectory](/ee), you agree to the [Developer Certificate of Origin](https://docs.gitlab.com/ee/legal/developer_certificate_of_origin.html#developer-certificate-of-origin-version-11).
+- By submitting code contributions as an individual or on behalf of a corporation to any directory in this repository outside of the [`/ee` subdirectory](/ee), you agree to the [Developer Certificate of Origin](doc/legal/developer_certificate_of_origin.md#developer-certificate-of-origin-version-11).
All Documentation content that resides under the [`doc/` directory](/doc) of this
repository is licensed under Creative Commons:
@@ -17,124 +17,76 @@ _This notice should stay as the first item in the `CONTRIBUTING.md` file._
## Contributing Documentation has been moved
As of July 2018, all the documentation for contributing to the GitLab project has been moved to a new location.
-[View the new documentation](https://about.gitlab.com/community/contribute/) to find the latest information.
+[View the documentation](https://about.gitlab.com/community/contribute/) to find the latest information.
## Contribute to GitLab
-[View the new documentation](https://about.gitlab.com/community/contribute/) to find the latest information.
+[View the documentation](https://about.gitlab.com/community/contribute/) to find the latest information.
## Security vulnerability disclosure
-This [documentation](doc/development/contributing/index.md#security-vulnerability-disclosure) has been moved.
+[View the documentation](doc/development/contributing/index.md#security-vulnerability-disclosure) to find the latest information.
## Code of Conduct
-This [documentation](https://about.gitlab.com/contributing/code-of-conduct/) has been moved.
+[View the documentation](https://about.gitlab.com/community/contribute/code-of-conduct/) to find the latest information.
## Closing policy for issues and merge requests
-This [documentation](doc/development/contributing/index.md#closing-policy-for-issues-and-merge-requests) has been moved.
+[View the documentation](doc/development/contributing/index.md#closing-policy-for-issues-and-merge-requests) to find the latest information.
## Helping others
-This [documentation](doc/development/contributing/index.md#helping-others) has been moved.
+[View the documentation](doc/development/contributing/index.md#helping-others) to find the latest information.
## I want to contribute!
-[View the new documentation](https://about.gitlab.com/community/contribute/) to find the latest information.
+[View the documentation](https://about.gitlab.com/community/contribute/) to find the latest information.
## Contribution Flow
-This [documentation](doc/development/contributing/index.md) has been moved.
+[View the documentation](doc/development/contributing/index.md) to find the latest information.
## Workflow labels
-This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
+View the [issue workflow](doc/development/contributing/issue_workflow.md) documentation for these subjects:
-### Type labels
-
-This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
-
-### Subject labels
-
-This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
-
-### Team labels
-
-This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
-
-### Release Scoping labels
-
-This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
-
-### Priority labels
-
-This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
-
-### Severity labels
-
-This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
-
-#### Severity impact guidance
-
-This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
-
-### Label for community contributors
-
-This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
+- Type labels
+- Subject labels
+- Team labels
+- Release Scoping labels
+- Priority labels
+- Severity labels
+ - Severity impact guidance
+- Labels for community contributors
## Implement design & UI elements
-This [documentation](doc/development/contributing/design.md) has been moved.
+[View the documentation](doc/development/contributing/design.md) to find the latest information.
## Issue tracker
-This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
-
-### Issue triaging
+View the [issue workflow](doc/development/contributing/issue_workflow.md) documentation for the following subjects.
-This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
-
-### Feature proposals
-
-This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
-
-### Issue tracker guidelines
-
-This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
-
-### Issue weight
-
-This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
-
-### Regression issues
-
-This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
-
-### Technical and UX debt
-
-This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
-
-### Stewardship
-
-This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
+- Issue triaging
+- Feature proposals
+- Issue tracker guidelines
+- Issue weight
+- Regression issues
+- Technical and UX debt
+- Stewardship
## Merge requests
-This [documentation](doc/development/contributing/merge_request_workflow.md) has been moved.
-
-### Merge request guidelines
-
-This [documentation](doc/development/contributing/merge_request_workflow.md) has been moved.
-
-### Contribution acceptance criteria
+View the [merge request workflow](doc/development/contributing/merge_request_workflow.md) documentation for the following subjects.
-This [documentation](doc/development/contributing/merge_request_workflow.md) has been moved.
+- Merge request guidelines
+- Contribution acceptance criteria
## Definition of done
-This [documentation](doc/development/contributing/merge_request_workflow.md) has been moved.
+[View the documentation](doc/development/contributing/merge_request_workflow.md) to find the latest information.
## Style guides
-This [documentation](doc/development/contributing/style_guides.md) has been moved.
+[View the documentation](doc/development/contributing/style_guides.md) to find the latest information.
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 075be6e2959..fb33000d1c1 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-16.5.1 \ No newline at end of file
+abb22ce3654f045c0c8b6fec1ba8b7a09eaca5f5
diff --git a/GITLAB_ELASTICSEARCH_INDEXER_VERSION b/GITLAB_ELASTICSEARCH_INDEXER_VERSION
index fdc6698807a..a84947d6ffe 100644
--- a/GITLAB_ELASTICSEARCH_INDEXER_VERSION
+++ b/GITLAB_ELASTICSEARCH_INDEXER_VERSION
@@ -1 +1 @@
-4.4.0
+4.5.0
diff --git a/GITLAB_KAS_VERSION b/GITLAB_KAS_VERSION
index 97dcb79e026..7776cb22262 100644
--- a/GITLAB_KAS_VERSION
+++ b/GITLAB_KAS_VERSION
@@ -1 +1 @@
-v16.5.0
+v16.6.0
diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION
index 075be6e2959..4a7fd6cd0fd 100644
--- a/GITLAB_PAGES_VERSION
+++ b/GITLAB_PAGES_VERSION
@@ -1 +1 @@
-16.5.1 \ No newline at end of file
+885055526d498a171120e69d7fe1c769814b73ec
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index 2da0a2a4e91..f269bd38a5b 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-14.29.0
+14.30.0
diff --git a/Gemfile b/Gemfile
index 2107186fe15..c1e9e34c3a5 100644
--- a/Gemfile
+++ b/Gemfile
@@ -30,7 +30,7 @@ gem 'activerecord-gitlab', path: 'gems/activerecord-gitlab' # rubocop:todo Gemfi
gem 'vite_rails' # rubocop:todo Gemfile/MissingFeatureCategory
-gem 'bootsnap', '~> 1.16.0', require: false # rubocop:todo Gemfile/MissingFeatureCategory
+gem 'bootsnap', '~> 1.17.0', require: false # rubocop:todo Gemfile/MissingFeatureCategory
gem 'openssl', '~> 3.0' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'ipaddr', '~> 1.2.5' # rubocop:todo Gemfile/MissingFeatureCategory
@@ -40,6 +40,7 @@ gem 'gitlab-safe_request_store', path: 'gems/gitlab-safe_request_store' # ruboco
# GitLab Monorepo Gems
group :monorepo do
gem 'gitlab-utils', path: 'gems/gitlab-utils' # rubocop:todo Gemfile/MissingFeatureCategory
+ gem 'gitlab-backup-cli', path: 'gems/gitlab-backup-cli', feature_category: :backup_restore
end
# Responders respond_to and respond_with
@@ -47,7 +48,7 @@ gem 'responders', '~> 3.0' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'sprockets', '~> 3.7.0' # rubocop:todo Gemfile/MissingFeatureCategory
-gem 'view_component', '~> 3.6.0' # rubocop:todo Gemfile/MissingFeatureCategory
+gem 'view_component', '~> 3.7.0' # rubocop:todo Gemfile/MissingFeatureCategory
# Supported DBs
gem 'pg', '~> 1.5.4' # rubocop:todo Gemfile/MissingFeatureCategory
@@ -63,7 +64,7 @@ gem 'marginalia', '~> 1.11.1' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'declarative_policy', '~> 1.1.0' # rubocop:todo Gemfile/MissingFeatureCategory
# Authentication libraries
-gem 'devise', '~> 4.8.1' # rubocop:todo Gemfile/MissingFeatureCategory
+gem 'devise', '~> 4.9.3', feature_category: :system_access
gem 'devise-pbkdf2-encryptable', '~> 0.0.0', path: 'vendor/gems/devise-pbkdf2-encryptable' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'bcrypt', '~> 3.1', '>= 3.1.14' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'doorkeeper', '~> 5.6', '>= 5.6.6' # rubocop:todo Gemfile/MissingFeatureCategory
@@ -119,7 +120,7 @@ gem 'acme-client', '~> 2.0' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'browser', '~> 5.3.1' # rubocop:todo Gemfile/MissingFeatureCategory
# OS detection for usage ping
-gem 'ohai', '~> 17.9' # rubocop:todo Gemfile/MissingFeatureCategory
+gem 'ohai', '~> 18.1' # rubocop:todo Gemfile/MissingFeatureCategory
# GPG
gem 'gpgme', '~> 2.0.23' # rubocop:todo Gemfile/MissingFeatureCategory
@@ -142,7 +143,7 @@ gem 'rack-cors', '~> 2.0.1', require: 'rack/cors' # rubocop:todo Gemfile/Missing
gem 'graphql', '~> 2.0.27', feature_category: :api
gem 'graphql-docs', '~> 4.0.0', group: [:development, :test], feature_category: :api
gem 'graphiql-rails', '~> 1.8.0', feature_category: :api
-gem 'apollo_upload_server', '~> 2.1.0', feature_category: :api
+gem 'apollo_upload_server', '~> 2.1.5', feature_category: :api
gem 'graphlient', '~> 0.5.0', feature_category: :importers # Used by BulkImport feature (group::import)
# Generate Fake data
@@ -196,7 +197,7 @@ gem 'seed-fu', '~> 2.3.7' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'elasticsearch-model', '~> 7.2' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'elasticsearch-rails', '~> 7.2', require: 'elasticsearch/rails/instrumentation' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'elasticsearch-api', '7.13.3' # rubocop:todo Gemfile/MissingFeatureCategory
-gem 'aws-sdk-core', '~> 3.185.1' # rubocop:todo Gemfile/MissingFeatureCategory
+gem 'aws-sdk-core', '~> 3.186.0' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'aws-sdk-cloudformation', '~> 1' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'aws-sdk-s3', '~> 1.136.0' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'faraday_middleware-aws-sigv4', '~>0.3.0' # rubocop:todo Gemfile/MissingFeatureCategory
@@ -208,7 +209,7 @@ gem 'deckar01-task_list', '2.3.3' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'gitlab-markup', '~> 1.9.0', require: 'github/markup' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'commonmarker', '~> 0.23.10' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'kramdown', '~> 2.3.1' # rubocop:todo Gemfile/MissingFeatureCategory
-gem 'RedCloth', '~> 4.3.2' # rubocop:todo Gemfile/MissingFeatureCategory
+gem 'RedCloth', '~> 4.3.3' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'org-ruby', '~> 0.9.12' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'creole', '~> 0.5.0' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'wikicloth', '0.8.1' # rubocop:todo Gemfile/MissingFeatureCategory
@@ -216,7 +217,7 @@ gem 'asciidoctor', '~> 2.0.18' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'asciidoctor-include-ext', '~> 0.4.0', require: false # rubocop:todo Gemfile/MissingFeatureCategory
gem 'asciidoctor-plantuml', '~> 0.0.16' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'asciidoctor-kroki', '~> 0.8.0', require: false # rubocop:todo Gemfile/MissingFeatureCategory
-gem 'rouge', '~> 4.1.3' # rubocop:todo Gemfile/MissingFeatureCategory
+gem 'rouge', '~> 4.2.0' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'truncato', '~> 0.7.12' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'nokogiri', '~> 1.15', '>= 1.15.4' # rubocop:todo Gemfile/MissingFeatureCategory
@@ -241,12 +242,11 @@ end
gem 'state_machines-activerecord', '~> 0.8.0' # rubocop:todo Gemfile/MissingFeatureCategory
# CI domain tags
-gem 'acts-as-taggable-on', '~> 9.0' # rubocop:todo Gemfile/MissingFeatureCategory
+gem 'acts-as-taggable-on', '~> 10.0' # rubocop:todo Gemfile/MissingFeatureCategory
# Background jobs
-gem 'sidekiq', '~> 6.5.7' # rubocop:todo Gemfile/MissingFeatureCategory
+gem 'sidekiq', '~> 6.5.10' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'sidekiq-cron', '~> 1.8.0' # rubocop:todo Gemfile/MissingFeatureCategory
-gem 'redis-namespace', '~> 1.9.0' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'gitlab-sidekiq-fetcher', path: 'vendor/gems/sidekiq-reliable-fetch', require: 'sidekiq-reliable-fetch' # rubocop:todo Gemfile/MissingFeatureCategory
# Cron Parser
@@ -262,11 +262,11 @@ gem 'rainbow', '~> 3.0' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'ruby-progressbar', '~> 1.10' # rubocop:todo Gemfile/MissingFeatureCategory
# Linear-time regex library for untrusted regular expressions
-gem 're2', '2.1.3' # rubocop:todo Gemfile/MissingFeatureCategory
+gem 're2', '2.3.0' # rubocop:todo Gemfile/MissingFeatureCategory
# Misc
-gem 'semver_dialects', '~> 1.2.1' # rubocop:todo Gemfile/MissingFeatureCategory
+gem 'semver_dialects', '~> 1.5', feature_category: :static_application_security_testing
gem 'version_sorter', '~> 2.3' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'csv_builder', path: 'gems/csv_builder' # rubocop:todo Gemfile/MissingFeatureCategory
@@ -284,17 +284,17 @@ gem 'connection_pool', '~> 2.4' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'redis-actionpack', '~> 5.3.0' # rubocop:todo Gemfile/MissingFeatureCategory
# Discord integration
-gem 'discordrb-webhooks', '~> 3.4', require: false # rubocop:todo Gemfile/MissingFeatureCategory
+gem 'discordrb-webhooks', '~> 3.4', require: false, feature_category: :integrations
# Jira integration
-gem 'jira-ruby', '~> 2.1.4' # rubocop:todo Gemfile/MissingFeatureCategory
-gem 'atlassian-jwt', '~> 0.2.0' # rubocop:todo Gemfile/MissingFeatureCategory
+gem 'jira-ruby', '~> 2.1.4', feature_category: :integrations
+gem 'atlassian-jwt', '~> 0.2.0', feature_category: :integrations
# Slack integration
-gem 'slack-messenger', '~> 2.3.4' # rubocop:todo Gemfile/MissingFeatureCategory
+gem 'slack-messenger', '~> 2.3.4', feature_category: :integrations
# FogBugz integration
-gem 'ruby-fogbugz', '~> 0.3.0' # rubocop:todo Gemfile/MissingFeatureCategory
+gem 'ruby-fogbugz', '~> 0.3.0', feature_category: :importers
# Kubernetes integration
gem 'kubeclient', '~> 4.11.0' # rubocop:todo Gemfile/MissingFeatureCategory
@@ -334,7 +334,7 @@ gem 'terser', '1.0.2' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'click_house-client', path: 'gems/click_house-client', require: 'click_house/client' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'addressable', '~> 2.8' # rubocop:todo Gemfile/MissingFeatureCategory
-gem 'tanuki_emoji', '~> 0.7' # rubocop:todo Gemfile/MissingFeatureCategory
+gem 'tanuki_emoji', '~> 0.9' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'gon', '~> 6.4.0' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'request_store', '~> 1.5.1' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'base32', '~> 0.3.0' # rubocop:todo Gemfile/MissingFeatureCategory
@@ -363,10 +363,10 @@ gem 'gitlab-labkit', '~> 0.34.0' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'thrift', '>= 0.16.0' # rubocop:todo Gemfile/MissingFeatureCategory
# I18n
-gem 'rails-i18n', '~> 7.0' # rubocop:todo Gemfile/MissingFeatureCategory
-gem 'gettext_i18n_rails', '~> 1.11.0' # rubocop:todo Gemfile/MissingFeatureCategory
-gem 'gettext_i18n_rails_js', '~> 1.3' # rubocop:todo Gemfile/MissingFeatureCategory
-gem 'gettext', '~> 3.3', require: false, group: :development # rubocop:todo Gemfile/MissingFeatureCategory
+gem 'rails-i18n', '~> 7.0', feature_category: :internationalization
+gem 'gettext_i18n_rails', '~> 1.11.0', feature_category: :internationalization
+gem 'gettext_i18n_rails_js', '~> 2.0.0', feature_category: :internationalization
+gem 'gettext', '~> 3.3', require: false, group: :development, feature_category: :internationalization
gem 'batch-loader', '~> 2.0.1' # rubocop:todo Gemfile/MissingFeatureCategory
@@ -399,11 +399,17 @@ group :development do
gem 'sprite-factory', '~> 1.7' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'listen', '~> 3.7' # rubocop:todo Gemfile/MissingFeatureCategory
+
+ gem 'ruby-lsp', "~> 0.12.3", feature_category: :tooling
+
+ gem 'ruby-lsp-rails', "~> 0.2.7", feature_category: :tooling
+
+ gem 'ruby-lsp-rspec', "~> 0.1.5", feature_category: :tooling
end
group :development, :test do
gem 'deprecation_toolkit', '~> 1.5.1', require: false # rubocop:todo Gemfile/MissingFeatureCategory
- gem 'bullet', '~> 7.1.1' # rubocop:todo Gemfile/MissingFeatureCategory
+ gem 'bullet', '~> 7.1.2' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'parser', '~> 3.2', '>= 3.2.2.4' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'pry-byebug' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'pry-rails', '~> 0.3.9' # rubocop:todo Gemfile/MissingFeatureCategory
@@ -411,7 +417,7 @@ group :development, :test do
gem 'awesome_print', require: false # rubocop:todo Gemfile/MissingFeatureCategory
- gem 'database_cleaner', '~> 1.7.0' # rubocop:todo Gemfile/MissingFeatureCategory
+ gem 'database_cleaner-active_record', '~> 2.1.0', feature_category: :database
gem 'factory_bot_rails', '~> 6.2.0' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'rspec-rails', '~> 6.0.3' # rubocop:todo Gemfile/MissingFeatureCategory
@@ -421,7 +427,7 @@ group :development, :test do
gem 'spring', '~> 4.1.0' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'spring-commands-rspec', '~> 1.0.4' # rubocop:todo Gemfile/MissingFeatureCategory
- gem 'gitlab-styles', '~> 10.1.0', require: false # rubocop:todo Gemfile/MissingFeatureCategory
+ gem 'gitlab-styles', '~> 11.0.0', require: false # rubocop:todo Gemfile/MissingFeatureCategory
gem 'haml_lint', '~> 0.40.0', require: false # rubocop:todo Gemfile/MissingFeatureCategory
gem 'bundler-audit', '~> 0.9.1', require: false # rubocop:todo Gemfile/MissingFeatureCategory
@@ -435,7 +441,7 @@ group :development, :test do
gem 'knapsack', '~> 1.21.1', feature_category: :tooling
gem 'crystalball', '~> 0.7.0', require: false, feature_category: :tooling
- gem 'test_file_finder', '~> 0.1.3', feature_category: :tooling
+ gem 'test_file_finder', '~> 0.2.1', feature_category: :tooling
gem 'simple_po_parser', '~> 1.1.6', require: false # rubocop:todo Gemfile/MissingFeatureCategory
@@ -449,7 +455,7 @@ group :development, :test do
end
group :development, :test, :danger do
- gem 'gitlab-dangerfiles', '~> 4.3.2', require: false, feature_category: :tooling
+ gem 'gitlab-dangerfiles', '~> 4.6.0', require: false, feature_category: :tooling
end
group :development, :test, :coverage do
@@ -479,7 +485,7 @@ group :test do
gem 'capybara', '~> 3.39', '>= 3.39.2' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'capybara-screenshot', '~> 1.0.26' # rubocop:todo Gemfile/MissingFeatureCategory
- gem 'selenium-webdriver', '~> 4.14' # rubocop:todo Gemfile/MissingFeatureCategory
+ gem 'selenium-webdriver', '~> 4.15' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'graphlyte', '~> 1.0.0' # rubocop:todo Gemfile/MissingFeatureCategory
@@ -496,7 +502,7 @@ group :test do
# Moved in `test` because https://gitlab.com/gitlab-org/gitlab/-/issues/217527
gem 'derailed_benchmarks', require: false # rubocop:todo Gemfile/MissingFeatureCategory
- gem 'gitlab_quality-test_tooling', '~> 1.3.0', require: false # rubocop:todo Gemfile/MissingFeatureCategory
+ gem 'gitlab_quality-test_tooling', '~> 1.5.0', require: false # rubocop:todo Gemfile/MissingFeatureCategory
end
gem 'octokit', '~> 6.0' # rubocop:todo Gemfile/MissingFeatureCategory
@@ -538,7 +544,7 @@ gem 'kas-grpc', '~> 0.2.0' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'grpc', '~> 1.58.0' # rubocop:todo Gemfile/MissingFeatureCategory
-gem 'google-protobuf', '~> 3.24', '>= 3.24.4' # rubocop:todo Gemfile/MissingFeatureCategory
+gem 'google-protobuf', '~> 3.25' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'toml-rb', '~> 2.2.0' # rubocop:todo Gemfile/MissingFeatureCategory
@@ -613,7 +619,7 @@ gem 'cvss-suite', '~> 3.0.1', require: 'cvss_suite' # rubocop:todo Gemfile/Missi
gem 'arr-pm', '~> 0.0.12' # rubocop:todo Gemfile/MissingFeatureCategory
# Remote Development
-gem 'devfile', '~> 0.0.23.pre.alpha1' # rubocop:todo Gemfile/MissingFeatureCategory
+gem 'devfile', '~> 0.0.24.pre.alpha1', feature_category: :remote_development
# Apple plist parsing
gem 'CFPropertyList', '~> 3.0.0' # rubocop:todo Gemfile/MissingFeatureCategory
diff --git a/Gemfile.checksum b/Gemfile.checksum
index 01ffbfa91bd..553e0131dda 100644
--- a/Gemfile.checksum
+++ b/Gemfile.checksum
@@ -1,6 +1,6 @@
[
{"name":"CFPropertyList","version":"3.0.5","platform":"ruby","checksum":"a78551cd4768d78ebca98488c27e33652ef818be64697a54676d34e6434674a4"},
-{"name":"RedCloth","version":"4.3.2","platform":"ruby","checksum":"1ee7bc55c8dcec92cf7741a2132a9a6cd19e4b884fbc1b3aca23e1a4fcd92d55"},
+{"name":"RedCloth","version":"4.3.3","platform":"ruby","checksum":"d941b8ac96e2730d2d9326d97dda9fcf64cb73532b3f902d91c18970c5f4632d"},
{"name":"acme-client","version":"2.0.11","platform":"ruby","checksum":"edf6da9f3c5dbe3ab0c6738eb3b97978b7a60e3500445480d2a72fcc610089de"},
{"name":"actioncable","version":"7.0.8","platform":"ruby","checksum":"1f504ddb4ab6a34f7c52e9df924441a403e9f358bace330c36dcca6358ecfb84"},
{"name":"actionmailbox","version":"7.0.8","platform":"ruby","checksum":"9420037b801e44aa4e36cf113f4bd6eb25c17eb1b84d9c8865e8abf8846c14e5"},
@@ -14,13 +14,14 @@
{"name":"activerecord-explain-analyze","version":"0.1.0","platform":"ruby","checksum":"5debb11fe23f35b91953a80677d80ba9284ee737fd9d148c1d7603ce45217f7b"},
{"name":"activestorage","version":"7.0.8","platform":"ruby","checksum":"8c2cae8de321ec899c7e7c4655331714fdd57f0966215286330f5c4d95a9db34"},
{"name":"activesupport","version":"7.0.8","platform":"ruby","checksum":"458316bb5098211ba9436d3c64d883177f09c49d1e29aa00f970d160275f13a1"},
-{"name":"acts-as-taggable-on","version":"9.0.1","platform":"ruby","checksum":"a3d8e0091bd1b323e0d61e07b79bd10043814800137216c747bf9a1d9bdb1eda"},
+{"name":"acts-as-taggable-on","version":"10.0.0","platform":"ruby","checksum":"d360e96f1622010a2f8fe5c6f480f8cf95ba0bc2072e0b9974574e5f336edb83"},
{"name":"addressable","version":"2.8.1","platform":"ruby","checksum":"bc724a176ef02118c8a3ed6b5c04c39cf59209607ffcce77b91d0261dbadedfa"},
{"name":"aes_key_wrap","version":"1.1.0","platform":"ruby","checksum":"b935f4756b37375895db45669e79dfcdc0f7901e12d4e08974d5540c8e0776a5"},
{"name":"akismet","version":"3.0.0","platform":"ruby","checksum":"74991b8e3d3257eeea996b47069abb8da2006c84a144255123e8dffd1c86b230"},
{"name":"aliyun-sdk","version":"0.8.0","platform":"ruby","checksum":"65915d3f9b528082253d1f9ad0e4d13d6b552933fe49251c68c6915cd4d75b9d"},
+{"name":"amatch","version":"0.4.1","platform":"ruby","checksum":"d3ff15226a2e627c72802e94579db829e5e10c96cf89d329494caec5889145f7"},
{"name":"android_key_attestation","version":"0.3.0","platform":"ruby","checksum":"467eb01a99d2bb48ef9cf24cc13712669d7056cba5a52d009554ff037560570b"},
-{"name":"apollo_upload_server","version":"2.1.0","platform":"ruby","checksum":"e5f3c9dda0c2ca775d007072742b98d517dfd91a667111fedbcdc94dfabd904e"},
+{"name":"apollo_upload_server","version":"2.1.5","platform":"ruby","checksum":"0f66bea96bdf7ce8b7278712ebafc8a26b82864ea6541213b58d9b3f673413a5"},
{"name":"app_store_connect","version":"0.29.0","platform":"ruby","checksum":"01d7a923825a4221892099acb5a72f86f6ee7d8aa95815d3c459ba6816ea430f"},
{"name":"arr-pm","version":"0.0.12","platform":"ruby","checksum":"fdff482f75239239201f4d667d93424412639aad0b3b0ad4d827e7c637e0ad39"},
{"name":"asciidoctor","version":"2.0.18","platform":"ruby","checksum":"bbd1e1d16deed8db94bf9624b9f4474fac32d9ca7225d377f076c08d9adde387"},
@@ -36,7 +37,7 @@
{"name":"aws-eventstream","version":"1.2.0","platform":"ruby","checksum":"ffa53482c92880b001ff2fb06919b9bb82fd847cbb0fa244985d2ebb6dd0d1df"},
{"name":"aws-partitions","version":"1.761.0","platform":"ruby","checksum":"291e444e1edfc92c5521a6dbdd1236ccc3f122b3520163b2be6ec5b6ef350ef2"},
{"name":"aws-sdk-cloudformation","version":"1.41.0","platform":"ruby","checksum":"31e47539719734413671edf9b1a31f8673fbf9688549f50c41affabbcb1c6b26"},
-{"name":"aws-sdk-core","version":"3.185.1","platform":"ruby","checksum":"572ada4eaf8393a9999d9a50adc2dcb78cc742c26a5727248c27f02cdaf97973"},
+{"name":"aws-sdk-core","version":"3.186.0","platform":"ruby","checksum":"5ed564f83f334010c532d55f215068cc833aad40be41fe3dc851b08f1321f4a7"},
{"name":"aws-sdk-kms","version":"1.64.0","platform":"ruby","checksum":"40de596c95047bfc6e1aacea24f3df6241aa716b6f7ce08ac4c5f7e3120395ad"},
{"name":"aws-sdk-s3","version":"1.136.0","platform":"ruby","checksum":"3547302a85d51de6cc75b48fb37d328f65f6526e7fc73a27a5b1b871f99a8d63"},
{"name":"aws-sigv4","version":"1.6.0","platform":"ruby","checksum":"ca9e6a15cd424f1f32b524b9760995331459bc22e67d3daad4fcf0c0084b087d"},
@@ -60,10 +61,10 @@
{"name":"better_errors","version":"2.10.1","platform":"ruby","checksum":"f798f1bac93f3e775925b7fcb24cffbcf0bb62ee2210f5350f161a6b75fc0a73"},
{"name":"bindata","version":"2.4.11","platform":"ruby","checksum":"c38e0c99ffcd80c10a0a7ae6c8586d2fe26bf245cbefac90bec8764523220f6a"},
{"name":"binding_of_caller","version":"1.0.0","platform":"ruby","checksum":"3aad25d1d538fc6e7972978f9bf512ccd992784009947c81633bea776713161d"},
-{"name":"bootsnap","version":"1.16.0","platform":"ruby","checksum":"f87410c00f69cd84a6e72a6c4bdba733f800d80d934f4315849d18ca9f288fed"},
+{"name":"bootsnap","version":"1.17.0","platform":"ruby","checksum":"6b0ea4dd68f0d424968dcd13953c3f04b13a19a8761c540d3af13507fcfa1347"},
{"name":"browser","version":"5.3.1","platform":"ruby","checksum":"62745301701ff2c6c5d32d077bb12532b20be261929dcb52c6781ed0d5658b3c"},
{"name":"builder","version":"3.2.4","platform":"ruby","checksum":"99caf08af60c8d7f3a6b004029c4c3c0bdaebced6c949165fe98f1db27fbbc10"},
-{"name":"bullet","version":"7.1.1","platform":"ruby","checksum":"ad7789d9ad2bfe772f96620ba8f927e756c74525f2c03e7843d3518ce50e5b9c"},
+{"name":"bullet","version":"7.1.2","platform":"ruby","checksum":"429725c174cb74ca0ae99b9720bf22cab80be59ee9401805f7ecc9ac62cbb3bb"},
{"name":"bundler-audit","version":"0.9.1","platform":"ruby","checksum":"bdc716fc21cd8652a6507b137e5bc51f5e0e4f6f106a114ab004c89d0200bd3d"},
{"name":"byebug","version":"11.1.3","platform":"ruby","checksum":"2485944d2bb21283c593d562f9ae1019bf80002143cc3a255aaffd4e9cf4a35b"},
{"name":"capybara","version":"3.39.2","platform":"ruby","checksum":"d6f0ca5f30897e64789428d4b047a0df105815a302069913578ac35d5ca99884"},
@@ -73,8 +74,8 @@
{"name":"character_set","version":"1.4.1","platform":"java","checksum":"38b632136b40e02fecba2898497b07ac640cc121f17ac536eaf19873d50053d0"},
{"name":"character_set","version":"1.4.1","platform":"ruby","checksum":"f71b1ac35b21c4c6f9f26b8a67c7eec8e10bdf0da17488ac7f8fae756d9f8062"},
{"name":"charlock_holmes","version":"0.7.7","platform":"ruby","checksum":"1790eca3f661ffa6bbf5866c53c7191e4b8472626fc4997ff9dbe7c425e2cb43"},
-{"name":"chef-config","version":"16.10.17","platform":"ruby","checksum":"1f4961e4d6aa4df374f739c6f62ae1d2be03dcff1bd93e56d9c963b8a156747c"},
-{"name":"chef-utils","version":"16.10.17","platform":"ruby","checksum":"a74253da6aab8ff92c955549536bdecbc4d1ce8032c8201576f2a8ef4e8ed7b3"},
+{"name":"chef-config","version":"18.3.0","platform":"ruby","checksum":"c183a2ff41da8d63b1e4a60853c9c701a053ab9afe13df767a578db5f07072df"},
+{"name":"chef-utils","version":"18.3.0","platform":"ruby","checksum":"827f7aace26ba9f5f8aca45059644205cc715baded80229f1fd5518d21970701"},
{"name":"chunky_png","version":"1.4.0","platform":"ruby","checksum":"89d5b31b55c0cf4da3cf89a2b4ebc3178d8abe8cbaf116a1dba95668502fdcfe"},
{"name":"circuitbox","version":"2.0.0","platform":"ruby","checksum":"496e9c1e76496e1e141490085f6cdcc4a8dedc72da8361bef69d8c5423b4da14"},
{"name":"citrus","version":"3.0.2","platform":"ruby","checksum":"4ec2412fc389ad186735f4baee1460f7900a8e130ffe3f216b30d4f9c684f650"},
@@ -98,10 +99,12 @@
{"name":"danger","version":"9.3.1","platform":"ruby","checksum":"9070fbac181eb45fb9b69ea25e6ea4faa86796ef33bf8d00346cab4385e51df5"},
{"name":"danger-gitlab","version":"8.0.0","platform":"ruby","checksum":"497dd7d0f6513913de651019223d8058cf494df10acbd17de92b175dfa04a3a8"},
{"name":"dartsass","version":"1.49.8","platform":"ruby","checksum":"267e7262a5655c8f0baa1ef663e976252bdbfa8bbf40c175153544a2dc8e1345"},
-{"name":"database_cleaner","version":"1.7.0","platform":"ruby","checksum":"bdf833c197afac7054015bcde2567c3834c366bbfe6a377c30151ca984b32016"},
+{"name":"database_cleaner-active_record","version":"2.1.0","platform":"ruby","checksum":"7384b973d67bcc1b5a850b876a4638aa83cca3bc88f9d87562fe25cd2dd60d8a"},
+{"name":"database_cleaner-core","version":"2.0.1","platform":"ruby","checksum":"8646574c32162e59ed7b5258a97a208d3c44551b854e510994f24683865d846c"},
{"name":"date","version":"3.3.3","platform":"java","checksum":"584e0a582d1eb2207b4eaac089d8a43f2ca10bea02682f286099642f15c56cce"},
{"name":"date","version":"3.3.3","platform":"ruby","checksum":"819792019d5712b748fb15f6dfaaedef14b0328723ef23583ea35f186774530f"},
{"name":"dead_end","version":"3.1.1","platform":"ruby","checksum":"1011df7f7c0149be004e11cbbc37747760227c55305cd902fd3c06e1394b2f5b"},
+{"name":"deb_version","version":"1.0.2","platform":"ruby","checksum":"c21f911d7f2fd1d61219caae254fc078e6598e477fdff8a05a18bec6c72ee713"},
{"name":"debug_inspector","version":"1.1.0","platform":"ruby","checksum":"eaa5a2d0195e1d65fb4164e8e7e466cca2e7eb53bc5e608cf12b8bf02c3a8606"},
{"name":"deckar01-task_list","version":"2.3.3","platform":"ruby","checksum":"918abaf3f81e6c0d224c2b7bef593d7f84ee5847a0692726d24e3fb272c2c758"},
{"name":"declarative","version":"0.0.20","platform":"ruby","checksum":"8021dd6cb17ab2b61233c56903d3f5a259c5cf43c80ff332d447d395b17d9ff9"},
@@ -109,11 +112,11 @@
{"name":"deprecation_toolkit","version":"1.5.1","platform":"ruby","checksum":"a8a1ab1a19ae40ea12560b65010e099f3459ebde390b76621ef0c21c516a04ba"},
{"name":"derailed_benchmarks","version":"2.1.2","platform":"ruby","checksum":"eaadc6206ceeb5538ff8f5e04a0023d54ebdd95d04f33e8960fb95a5f189a14f"},
{"name":"descendants_tracker","version":"0.0.4","platform":"ruby","checksum":"e9c41dd4cfbb85829a9301ea7e7c48c2a03b26f09319db230e6479ccdc780897"},
-{"name":"devfile","version":"0.0.23.pre.alpha1","platform":"arm64-darwin","checksum":"eec9ed97436cd5e9d456e270da979faeecbdeef42ee75ef9b39b45001c2399fb"},
-{"name":"devfile","version":"0.0.23.pre.alpha1","platform":"ruby","checksum":"fba2c679cbafb03da153f73f55a346ae01f4921383575e1f7cda269e7e67e40a"},
-{"name":"devfile","version":"0.0.23.pre.alpha1","platform":"x86_64-linux","checksum":"30e31b39599b7823673f5386f8bf19b7cb2b959c7f34a16704893db437d42094"},
+{"name":"devfile","version":"0.0.24.pre.alpha1","platform":"arm64-darwin","checksum":"4954bf498772dbf534da0638bc59023234fed7423c72c85f21b6504ee4c65482"},
+{"name":"devfile","version":"0.0.24.pre.alpha1","platform":"ruby","checksum":"72bbfc26edb519902d5c68e07188e0a3d699a1866392fa1497e5b7f3abb36600"},
+{"name":"devfile","version":"0.0.24.pre.alpha1","platform":"x86_64-linux","checksum":"d121b1094aa3a24c29592a83c629ee640920e0196711dd06f27b6fa9b1ced609"},
{"name":"device_detector","version":"1.0.0","platform":"ruby","checksum":"b800fb3150b00c23e87b6768011808ac1771fffaae74c3238ebaf2b782947a7d"},
-{"name":"devise","version":"4.8.1","platform":"ruby","checksum":"fdd48bbe79a89e7c1152236a70479842ede48bea4fa7f4f2d8da1f872559803e"},
+{"name":"devise","version":"4.9.3","platform":"ruby","checksum":"480638d6c51b97f56da6e28d4f3e2a1b8e606681b316aa594b87c6ab94923488"},
{"name":"devise-two-factor","version":"4.1.1","platform":"ruby","checksum":"c95f5b07533e62217aaed3c386874d94e2d472fb5f2b6598afe8600fc17a8b95"},
{"name":"diff-lcs","version":"1.5.0","platform":"ruby","checksum":"49b934001c8c6aedb37ba19daec5c634da27b318a7a3c654ae979d6ba1929b67"},
{"name":"diff_match_patch","version":"0.1.0","platform":"ruby","checksum":"b36057bfcfeaedf19dcb7b2c28c19ee625bd6ec6d0d182717d3ef22b3879c40e"},
@@ -194,20 +197,21 @@
{"name":"fog-local","version":"0.8.0","platform":"ruby","checksum":"263b2d09e54c69d1b87ad7f235a1a1e53c8a674edcedf7512c1715765ad7ef79"},
{"name":"fog-xml","version":"0.1.3","platform":"ruby","checksum":"5604c42649ebb0d8a31bd973aa000c2dd0127f1c1c4c174b69266a2e78e37410"},
{"name":"formatador","version":"0.2.5","platform":"ruby","checksum":"80821869ddacb79e72870ff4bb1531efacd278c04f2df26bc6b4529ee13582bd"},
+{"name":"forwardable","version":"1.3.3","platform":"ruby","checksum":"f17df4bd6afa6f46a003217023fe5716ef88ce261f5c4cf0edbdeed6470cafac"},
{"name":"fugit","version":"1.8.1","platform":"ruby","checksum":"18ffb26813869610f71bb0b7d568c3624d2b3025aeebb6600a18df0c77a6a2b2"},
{"name":"fuubar","version":"2.2.0","platform":"ruby","checksum":"9b0263c4074f39c68b37f1e4e69a7d3cfc7523c41bea43601235daa723179b4a"},
{"name":"fuzzyurl","version":"0.9.0","platform":"ruby","checksum":"542efa80f2bcaadbdc402c2f0b572f2e335a1d53e375aecad68bbb3d86860c0f"},
{"name":"gapic-common","version":"0.18.0","platform":"ruby","checksum":"6fd55a538ce2d63026fa05f379b1aec00788cc060f76903739516ab1ca1496ab"},
{"name":"gemoji","version":"3.0.1","platform":"ruby","checksum":"80553f2f4932a7a95fb1b3c7c63f7dd937e7c8c610164bbdea28fd06eba5f36d"},
{"name":"get_process_mem","version":"0.2.7","platform":"ruby","checksum":"4afd3c3641dd6a817c09806c7d6d509d8a9984512ac38dea8b917426bbf77eba"},
-{"name":"gettext","version":"3.3.6","platform":"ruby","checksum":"ee6bbd1b2f833ee52d7797fa68acbfecc4726aec6b6280fd7eab92aa0190b413"},
+{"name":"gettext","version":"3.4.9","platform":"ruby","checksum":"292864fe6a15c224cee4125a4a72fab426fdbb280e4cff3cfe44935f549b009a"},
{"name":"gettext_i18n_rails","version":"1.11.0","platform":"ruby","checksum":"e19c7e4a256c500f7f38396dca44a282b9838ae278f57c362993a54964b22bbe"},
-{"name":"gettext_i18n_rails_js","version":"1.3.0","platform":"ruby","checksum":"5d10afe4be3639bff78c50a56768c20f39aecdabc580c08aa45573911c2bd687"},
+{"name":"gettext_i18n_rails_js","version":"2.0.0","platform":"ruby","checksum":"7bfb72699e3cdf9a2d892cc816e70442a08d0f4e340b92731249ad38b9205b51"},
{"name":"git","version":"1.18.0","platform":"ruby","checksum":"c9b80462e4565cd3d7a9ba8440c41d2c52244b17b0dad0bfddb46de70630c465"},
{"name":"gitaly","version":"16.5.0.pre.rc1","platform":"ruby","checksum":"ed17515ad04d4663a0efc15c8f2887b705f006133e8b10cc9321460eb0a38353"},
{"name":"gitlab","version":"4.19.0","platform":"ruby","checksum":"3f645e3e195dbc24f0834fbf83e8ccfb2056d8e9712b01a640aad418a6949679"},
{"name":"gitlab-chronic","version":"0.10.5","platform":"ruby","checksum":"f80f18dc699b708870a80685243331290bc10cfeedb6b99c92219722f729c875"},
-{"name":"gitlab-dangerfiles","version":"4.3.2","platform":"ruby","checksum":"978bd81e30faccc629f2cdbc6f320a1d225188fbc18f072b8a60abdb53e80a96"},
+{"name":"gitlab-dangerfiles","version":"4.6.0","platform":"ruby","checksum":"441b37b17d1dad36268517490a30aaf57e43dffb2e9ebc1da38d3bc9fa20741e"},
{"name":"gitlab-experiment","version":"0.8.0","platform":"ruby","checksum":"b4e2f73e0af19cdd899a745f5a846c1318d44054e068a8f4ac887f6b1017d3f9"},
{"name":"gitlab-fog-azure-rm","version":"1.8.0","platform":"ruby","checksum":"e4f24b174b273b88849d12fbcfecb79ae1c09f56cbd614998714c7f0a81e6c28"},
{"name":"gitlab-labkit","version":"0.34.0","platform":"ruby","checksum":"ca5c504201390cd07ba1029e6ca3059f4e2e6005eb121ba8a103af1e166a3ecd"},
@@ -215,10 +219,10 @@
{"name":"gitlab-mail_room","version":"0.0.23","platform":"ruby","checksum":"23564fa4dab24ec5011d4c64a801fc0228301d5b0f046a26a1d8e96e36c19997"},
{"name":"gitlab-markup","version":"1.9.0","platform":"ruby","checksum":"7eda045a08ec2d110084252fa13a8c9eac8bdac0e302035ca7db4b82bcbd7ed4"},
{"name":"gitlab-net-dns","version":"0.9.2","platform":"ruby","checksum":"f726d978479d43810819f12a45c0906d775a07e34df111bbe693fffbbef3059d"},
-{"name":"gitlab-styles","version":"10.1.0","platform":"ruby","checksum":"f42745f5397d042fe24cf2d0eb56c995b37f9f43d8fb79b834d197a1cafdc84a"},
+{"name":"gitlab-styles","version":"11.0.0","platform":"ruby","checksum":"0dd8ec066ce9955ac51d3616c6bfded30f75bb526f39ff392ece6f43d5b9406b"},
{"name":"gitlab_chronic_duration","version":"0.12.0","platform":"ruby","checksum":"0d766944d415b5c831f176871ee8625783fc0c5bfbef2d79a3a616f207ffc16d"},
{"name":"gitlab_omniauth-ldap","version":"2.2.0","platform":"ruby","checksum":"bb4d20acb3b123ed654a8f6a47d3fac673ece7ed0b6992edb92dca14bad2838c"},
-{"name":"gitlab_quality-test_tooling","version":"1.3.0","platform":"ruby","checksum":"0c932e0a98839c219ef21e2da336edb59ff48cc43cd06e22d780738715a4652e"},
+{"name":"gitlab_quality-test_tooling","version":"1.5.0","platform":"ruby","checksum":"7ce31d48462290f39c2c9bf8ae99b39b31e3a5eba0546bac058cdb6f7f88afd3"},
{"name":"globalid","version":"1.1.0","platform":"ruby","checksum":"b337e1746f0c8cb0a6c918234b03a1ddeb4966206ce288fbb57779f59b2d154f"},
{"name":"gon","version":"6.4.0","platform":"ruby","checksum":"e3a618d659392890f1aa7db420f17c75fd7d35aeb5f8fe003697d02c4b88d2f0"},
{"name":"google-apis-androidpublisher_v3","version":"0.34.0","platform":"ruby","checksum":"d7e1d7dd92f79c498fe2082222a1740d788e022e660c135564b3fd299cab5425"},
@@ -241,16 +245,16 @@
{"name":"google-cloud-errors","version":"1.3.0","platform":"ruby","checksum":"450b681e24c089a20721a01acc4408bb4a7b0df28c175aaab488da917480d64b"},
{"name":"google-cloud-profiler-v2","version":"0.4.0","platform":"ruby","checksum":"53fc2ab175d08f54233c644310d47798feac996220916815c4fb44c937b5d3e3"},
{"name":"google-cloud-storage","version":"1.44.0","platform":"ruby","checksum":"299a1e055c9277c8120f7c10d21d37e4d8c17c7b963350c0e0bff7e9d9a570ea"},
-{"name":"google-protobuf","version":"3.24.4","platform":"aarch64-linux","checksum":"d3e824753a9511e4c08439586069a636c23d9ca16a509f316a895353c11a1ac8"},
-{"name":"google-protobuf","version":"3.24.4","platform":"arm64-darwin","checksum":"e13b12a648668d99d8b71ffcf378bfd744885af11e983460677073b2c8e2a979"},
-{"name":"google-protobuf","version":"3.24.4","platform":"java","checksum":"657d67b5425afa0beb94e54df7d0a15da3daa45a500fc252e7550806669b47f1"},
-{"name":"google-protobuf","version":"3.24.4","platform":"ruby","checksum":"38a403ca2fd905d3ed7c20f8d2e4718af1be3eb99093d35d7021383f6e72f2ca"},
-{"name":"google-protobuf","version":"3.24.4","platform":"x64-mingw-ucrt","checksum":"fc0396dd9f45ea54d494097e0077ee8c0cc002f1c825f06ed40f4e3b4de6948c"},
-{"name":"google-protobuf","version":"3.24.4","platform":"x64-mingw32","checksum":"ba1b5cd5effa6c6a738eb2d2d0701e3d83d95b81842564b5feb9c42579722fc6"},
-{"name":"google-protobuf","version":"3.24.4","platform":"x86-linux","checksum":"f9cae6c878381da082eab1d3eceb84525c7d7413401e1a4a5ee179b66fdbebe0"},
-{"name":"google-protobuf","version":"3.24.4","platform":"x86-mingw32","checksum":"4cf31ca7d447a86200dfcb86f64ddd046c1f9c96dc537c9d74ab19e0c36a8f0b"},
-{"name":"google-protobuf","version":"3.24.4","platform":"x86_64-darwin","checksum":"44d541e980989f1aa007a3c5481ec93932b2e50cfa63c22427dd42460a5d2865"},
-{"name":"google-protobuf","version":"3.24.4","platform":"x86_64-linux","checksum":"68f65302fad9f47c88d38136fda0dec6078f3a2a79fb5bcccf62121fd8fcca50"},
+{"name":"google-protobuf","version":"3.25.0","platform":"aarch64-linux","checksum":"4455602758a60bd698a57c7210efc440523523fbe0c0c712624e57bb02c6c9d4"},
+{"name":"google-protobuf","version":"3.25.0","platform":"arm64-darwin","checksum":"c1ba0bb5504155f5bd0d11d649316ff52cef5b2a1e7ce876497815f98be3c5a6"},
+{"name":"google-protobuf","version":"3.25.0","platform":"java","checksum":"7006d8485d6c729c081a7eb8592d8c494fd8716863a7fb7ad7c76188eafc41a5"},
+{"name":"google-protobuf","version":"3.25.0","platform":"ruby","checksum":"b51632d900b633fbd6164784351bee93001dfd3f32bd18f6505fc97d64e1a1a1"},
+{"name":"google-protobuf","version":"3.25.0","platform":"x64-mingw-ucrt","checksum":"e4935e41e0f3c32fe96e496803de61d36273474ebb72ac7ee9db3a4ecb4b5cd6"},
+{"name":"google-protobuf","version":"3.25.0","platform":"x64-mingw32","checksum":"42b13346a1be8346e4d62a41ac7150f374ca5d254b3bb4bf3bc817de522ce969"},
+{"name":"google-protobuf","version":"3.25.0","platform":"x86-linux","checksum":"7f391788f013778ffae197a184481ff24265a977d1cb2270b13a5af1ba2f53d5"},
+{"name":"google-protobuf","version":"3.25.0","platform":"x86-mingw32","checksum":"2f42a5738af0a874b35b228a2df8de21a58fa265627cfd0fa57edaea160c1087"},
+{"name":"google-protobuf","version":"3.25.0","platform":"x86_64-darwin","checksum":"c3d4a144d8f4d61193ab1a4c5d52e2d40562ba13e07eeca1fca34bc59212c352"},
+{"name":"google-protobuf","version":"3.25.0","platform":"x86_64-linux","checksum":"c6a76175c921b300ee62b21d36e8a9c07f0a4967a17be0671a83c57d7bf9bd0f"},
{"name":"googleapis-common-protos","version":"1.4.0","platform":"ruby","checksum":"da2380fb5ab1563580816c74e8d684ac17512c3654c829a3ee84f6d6139de382"},
{"name":"googleapis-common-protos-types","version":"1.5.0","platform":"ruby","checksum":"5769cf7376abc86ef7f5897a4aaca1d5c5a3c49ddabeddd2c251fcf8155f858b"},
{"name":"googleauth","version":"1.3.0","platform":"ruby","checksum":"51dd7362353cf1e90a2d01e1fb94321ae3926c776d4dc4a79db65230217ffcc2"},
@@ -330,11 +334,12 @@
{"name":"kramdown","version":"2.3.2","platform":"ruby","checksum":"cb4530c2e9d16481591df2c9336723683c354e5416a5dd3e447fa48215a6a71c"},
{"name":"kramdown-parser-gfm","version":"1.1.0","platform":"ruby","checksum":"fb39745516427d2988543bf01fc4cf0ab1149476382393e0e9c48592f6581729"},
{"name":"kubeclient","version":"4.11.0","platform":"ruby","checksum":"4985fcd749fb8c364a668a8350a49821647f03aa52d9ee6cbc582beb8e883fcc"},
+{"name":"language_server-protocol","version":"3.17.0.3","platform":"ruby","checksum":"3d5c58c02f44a20d972957a9febe386d7e7468ab3900ce6bd2b563dd910c6b3f"},
{"name":"launchy","version":"2.5.0","platform":"ruby","checksum":"954243c4255920982ce682f89a42e76372dba94770bf09c23a523e204bdebef5"},
{"name":"lefthook","version":"1.5.2","platform":"ruby","checksum":"37d78cbf39169c4cbd82bce2e83dc06851e408512fe5fee427b1bd53487e670a"},
{"name":"letter_opener","version":"1.7.0","platform":"ruby","checksum":"095bc0d58e006e5b43ea7d219e64ecf2de8d1f7d9dafc432040a845cf59b4725"},
{"name":"letter_opener_web","version":"2.0.0","platform":"ruby","checksum":"33860ad41e1785d75456500e8ca8bba8ed71ee6eaf08a98d06bbab67c5577b6f"},
-{"name":"libyajl2","version":"1.2.0","platform":"ruby","checksum":"1117cd1e48db013b626e36269bbf1cef210538ca6d2e62d3fa3db9ded005b258"},
+{"name":"libyajl2","version":"2.1.0","platform":"ruby","checksum":"aa5df6c725776fc050c8418450de0f7c129cb7200b811907c4c0b3b5c0aea0ef"},
{"name":"license_finder","version":"7.0.1","platform":"ruby","checksum":"0b22c9567e2a8b102c7245da49ebeddaec60f66d237d2bb91b9feddf5d242f6a"},
{"name":"licensee","version":"9.16.0","platform":"ruby","checksum":"7b1693639019dbb1d3e020d72c4470ca84da3cfc67e4d6da1d1cdcb736d09044"},
{"name":"listen","version":"3.7.1","platform":"ruby","checksum":"3b80caa7aa77fae836916c2f9e3fbcafbd15f5d695dd487c1f5b5e7e465efe29"},
@@ -358,13 +363,15 @@
{"name":"mini_histogram","version":"0.3.1","platform":"ruby","checksum":"6a114b504e4618b0e076cc672996036870f7cc6f16b8e5c25c0c637726d2dd94"},
{"name":"mini_magick","version":"4.10.1","platform":"ruby","checksum":"e939d2c70c8002233fc6b1eecfe762f38a156d69ad31a87160205870be08f852"},
{"name":"mini_mime","version":"1.1.2","platform":"ruby","checksum":"a54aec0cc7438a03a850adb00daca2bdb60747f839e28186994df057cea87151"},
-{"name":"mini_portile2","version":"2.8.4","platform":"ruby","checksum":"180bc4193701bbeb9b6c02df5a6b8185bff7f32abd466dd97d6532d36e45b20a"},
+{"name":"mini_portile2","version":"2.8.5","platform":"ruby","checksum":"7a37db8ae758086c3c3ac3a59c036704d331e965d5e106635e4a42d6e66089ce"},
{"name":"minitest","version":"5.11.3","platform":"ruby","checksum":"78e18aa2c49c58e9bc53c54a0b900e87ad0a96394e92fbbfa58d3ff860a68f45"},
{"name":"mixlib-cli","version":"2.1.8","platform":"ruby","checksum":"e6f27be34d580f6ed71731ca46b967e57793a627131c1f6e1ed2dad39ea3bdf9"},
-{"name":"mixlib-config","version":"3.0.9","platform":"ruby","checksum":"9867adab3ab547eb74a8efdc9dfab6bcc83d2802a571ff8af8d6e981ca8d53ab"},
+{"name":"mixlib-config","version":"3.0.27","platform":"ruby","checksum":"d7748b1898e4f16502afec1de00b5ad65c6de405114b1b0c65ec61b1a9100148"},
{"name":"mixlib-log","version":"3.0.9","platform":"ruby","checksum":"fd6ca2c8075f8085065dffcee0805c5b3f88d643d5c954acdc3282f463a9ad58"},
-{"name":"mixlib-shellout","version":"3.2.5","platform":"ruby","checksum":"121a54005e52b6596a945f7bfc95bbcbd7d8ee7685cb3736dd3cef5ff46029bd"},
-{"name":"mixlib-shellout","version":"3.2.5","platform":"universal-mingw32","checksum":"c40ef5f34a68eec5e0cad13482497f6c3898a30cff1747517f2169d4fa4055e0"},
+{"name":"mixlib-shellout","version":"3.2.7","platform":"ruby","checksum":"46f6d1f9c77e689a443081c5cac336203343f0f2224db06b80d39ae4cd797c7e"},
+{"name":"mixlib-shellout","version":"3.2.7","platform":"universal-mingw32","checksum":"4d7bea07e347cc8de2b4bc22f4d8f84d7bb8165cf900d26b532d0d9fa4928a19"},
+{"name":"mixlib-shellout","version":"3.2.7","platform":"x64-mingw-ucrt","checksum":"de01743f678b66c275ea5f40749cde6c056651d1bb6d320711779394d2eec654"},
+{"name":"mize","version":"0.4.1","platform":"ruby","checksum":"55bcba0cf001cbff5a647a18172c4a885061ceec586395fb08ecbb98d039f627"},
{"name":"msgpack","version":"1.5.4","platform":"java","checksum":"05b3bd16a65dddc64c878634b7ecb9cd613569ca3dd6e480d7295626a0a3f562"},
{"name":"msgpack","version":"1.5.4","platform":"ruby","checksum":"a53db320fba40f58c07c5b66ed9fd4d73cbe8eba4cb28fe9e3218444341a4e09"},
{"name":"multi_json","version":"1.14.1","platform":"ruby","checksum":"d971296c0eacea289d31e4a7ab7ac5eda97262c62bbc8c110de4f5e36425c577"},
@@ -383,9 +390,9 @@
{"name":"net-ntp","version":"2.1.3","platform":"ruby","checksum":"5bc73f4102bde0d1872bd3b293608ae99d9f5007d744f21919c6a565eda9267d"},
{"name":"net-pop","version":"0.1.2","platform":"ruby","checksum":"848b4e982013c15b2f0382792268763b748cce91c9e91e36b0f27ed26420dff3"},
{"name":"net-protocol","version":"0.1.3","platform":"ruby","checksum":"ad43e2be965ede676683c047b2c3d76762aa49a764779d98312a10da04622c14"},
-{"name":"net-scp","version":"3.0.0","platform":"ruby","checksum":"8fc6c80365b95230c6bfc529dbea3893d2d81724855bfb01cbf385866e1c902c"},
+{"name":"net-scp","version":"4.0.0","platform":"ruby","checksum":"b32ded0d48c88ce70844a063e4e14efb44a95e51a9e0c0bfb0c54b4313b622ea"},
{"name":"net-smtp","version":"0.3.3","platform":"ruby","checksum":"3d51dcaa981b74aff2d89cbe89de4503bc2d682365ea5176366e950a0d68d5b0"},
-{"name":"net-ssh","version":"6.0.0","platform":"ruby","checksum":"6290ddcb232380cae79b772af924e12f57fe1dcd0f71254411dd21c04f7b13d0"},
+{"name":"net-ssh","version":"7.2.0","platform":"ruby","checksum":"2a28f177173d1f6bef77471fa927c73959cda36cd03772e117f2fec48f34d2cb"},
{"name":"netrc","version":"0.11.0","platform":"ruby","checksum":"de1ce33da8c99ab1d97871726cba75151113f117146becbe45aa85cb3dabee3f"},
{"name":"nio4r","version":"2.5.8","platform":"java","checksum":"b2b1800f6bf7ce4b797ca8b639ad278a99c9c904fb087a91d944f38e4bd71401"},
{"name":"nio4r","version":"2.5.8","platform":"ruby","checksum":"3becb4ad95ab8ac0a9bd2e1b16466869402be62848082bf6329ae9091f276676"},
@@ -406,7 +413,7 @@
{"name":"oauth","version":"0.5.6","platform":"ruby","checksum":"4085fe28e0c5e2434135e00a6555294fd2a4ff96a98d1bdecdcd619fc6368dff"},
{"name":"oauth2","version":"2.0.9","platform":"ruby","checksum":"b21f9defcf52dc1610e0dfab4c868342173dcd707fd15c777d9f4f04e153f7fb"},
{"name":"octokit","version":"6.1.1","platform":"ruby","checksum":"920e4a9d820205f70738f58de6a7e6ef0e2f25b27db954b5806a63105207b0bf"},
-{"name":"ohai","version":"17.9.0","platform":"ruby","checksum":"c59cf16124c0a6481fb85013ec7ec5b398651b6abed782d3e06ab058ce9a5406"},
+{"name":"ohai","version":"18.1.3","platform":"ruby","checksum":"980cfd6a6597f897e157532ba2168d29afb83a8f5e125f682ec3248c3407df95"},
{"name":"oj","version":"3.13.23","platform":"ruby","checksum":"206dfdc4020ad9974705037f269cfba211d61b7662a58c717cce771829ccef51"},
{"name":"oj-introspect","version":"0.7.2","platform":"ruby","checksum":"c415a44567ed2870d8e963a69421d9322128e194fab7867e37e54d5a25d5333d"},
{"name":"omniauth","version":"2.1.0","platform":"ruby","checksum":"bff7234f5ec9323622b217c7f26d52f850de0b0e2b8c807c3358fc79fe572300"},
@@ -444,17 +451,20 @@
{"name":"peek","version":"1.1.0","platform":"ruby","checksum":"d6501ead8cde46d8d8ed0d59eb6f0ba713d0a41c11a2c4a81447b2dce37b3ecc"},
{"name":"pg","version":"1.5.4","platform":"ruby","checksum":"04f7b247151c639a0b955d8e5a9a41541343f4640aa3c2bdf749a872c339d25d"},
{"name":"pg_query","version":"4.2.3","platform":"ruby","checksum":"1cc9955c7bce8e51e1abc11f1952e3d9d0f1cd4c16c58c56ec75d5aaf1cfd697"},
-{"name":"plist","version":"3.6.0","platform":"ruby","checksum":"f468bcf6b72ec6d1585ed6744eb4817c1932a5bf91895ed056e69b7f12ca10f2"},
+{"name":"plist","version":"3.7.0","platform":"ruby","checksum":"703ca90a7cb00e8263edd03da2266627f6741d280c910abbbac07c95ffb2f073"},
{"name":"png_quantizator","version":"0.2.1","platform":"ruby","checksum":"6023d4d064125c3a7e02929c95b7320ed6ac0d7341f9e8de0c9ea6576ef3106b"},
-{"name":"po_to_json","version":"1.0.1","platform":"ruby","checksum":"6a7188aa6c42a22c9718f9b39062862ef7f3d8f6a7b4177cae058c3308b56af7"},
+{"name":"po_to_json","version":"2.0.0","platform":"ruby","checksum":"9e59b2904c015d2fcad3ec02022970ad0fb6622f6eb5ba82b47dff99d2fd6b2a"},
{"name":"premailer","version":"1.16.0","platform":"ruby","checksum":"03e4402c448e6bae13fb5f6301a8bde4f3508e1bff90ae7c0972c7be94694786"},
{"name":"premailer-rails","version":"1.10.3","platform":"ruby","checksum":"7cdcb97027866f7a81c490c6d15ada7f39666b5f6375f0821b7e97e0483b112f"},
+{"name":"prime","version":"0.1.2","platform":"ruby","checksum":"d4e956cadfaf04de036dc7dc74f95bf6a285a62cc509b28b7a66b245d19fe3a4"},
+{"name":"prism","version":"0.17.1","platform":"ruby","checksum":"e63f86df2c36aecd578431ee0c9d1f66cdef98a406f0a11e7da949514212cbcd"},
{"name":"proc_to_ast","version":"0.1.0","platform":"ruby","checksum":"92a73fa66e2250a83f8589f818b0751bcf227c68f85916202df7af85082f8691"},
{"name":"prometheus-client-mmap","version":"0.28.1","platform":"aarch64-linux","checksum":"b190045625ee8f8b3ef90e583ef7fadeac745810c8a243f1ed5e9b47c18146f0"},
{"name":"prometheus-client-mmap","version":"0.28.1","platform":"arm64-darwin","checksum":"9e7022848493b882d1de9f42d7784f9821e83b2c3b4b2dc9a12c2c8269209a6e"},
{"name":"prometheus-client-mmap","version":"0.28.1","platform":"ruby","checksum":"92fb3989a16927fb0cacfcb3ebc6c8ea5e4abf82e4aef22ab62c3c4b8f17e52a"},
{"name":"prometheus-client-mmap","version":"0.28.1","platform":"x86_64-darwin","checksum":"66e7cad96ad581174edf4f1f52da141e5a15389ce3283fba7b4e3e5968dd46b7"},
{"name":"prometheus-client-mmap","version":"0.28.1","platform":"x86_64-linux","checksum":"4d3e92a249b16e41ef3e55078537bca599659578c0f86e31d195429c6e5e1f3a"},
+{"name":"protocol","version":"2.0.0","platform":"ruby","checksum":"dcd7c509e53b8cd6284e965a2e2e71d5291ca9e2d50acfa3d7ee0561c0df16b9"},
{"name":"pry","version":"0.14.2","platform":"java","checksum":"fd780670977ba04ff7ee32dabd4d02fe4bf02e977afe8809832d5dca1412862e"},
{"name":"pry","version":"0.14.2","platform":"ruby","checksum":"c4fe54efedaca1d351280b45b8849af363184696fcac1c72e0415f9bdac4334d"},
{"name":"pry-byebug","version":"3.10.1","platform":"ruby","checksum":"c8f975c32255bfdb29e151f5532130be64ff3d0042dc858d0907e849125581f8"},
@@ -490,22 +500,21 @@
{"name":"rbtrace","version":"0.4.14","platform":"ruby","checksum":"162bbf89cecabfc4f09c869b655f6f3a679c4870ebb7cbdcadf7393a81cc1769"},
{"name":"rbtree","version":"0.4.6","platform":"ruby","checksum":"14eea4469b24fd2472542e5f3eb105d6344c8ccf36f0b56d55fdcfeb4e0f10fc"},
{"name":"rchardet","version":"1.8.0","platform":"ruby","checksum":"693acd5253d5ade81a51940697955f6dd4bb2f0d245bda76a8e23deec70a52c7"},
-{"name":"re2","version":"2.1.3","platform":"aarch64-linux","checksum":"27316bb47cfc0f28cfd1626426120e1c55ca8420a64c9e966f8feb1c911eae2a"},
-{"name":"re2","version":"2.1.3","platform":"arm-linux","checksum":"81ffdd76b202f24461b4868abed96c994e2106e57970004b841499da983f688c"},
-{"name":"re2","version":"2.1.3","platform":"arm64-darwin","checksum":"86d553e85779943a353865cbfdd89156c0411b92a1c7fe6abf1024135d53190e"},
-{"name":"re2","version":"2.1.3","platform":"ruby","checksum":"03a30b53002ab66b66fa2d4500c82ec0866020c22e11c23516f660ce43cfae8f"},
-{"name":"re2","version":"2.1.3","platform":"x64-mingw-ucrt","checksum":"be0277c15bef6f38a2f9805aca798de4a31f6319cb1790ff6683112cb89721da"},
-{"name":"re2","version":"2.1.3","platform":"x64-mingw32","checksum":"cadba41d90f2186507c97593084b8f951c9c3ee7ecb2be02f3497aa9c5cdaadb"},
-{"name":"re2","version":"2.1.3","platform":"x86-linux","checksum":"ad54cafdaf40310cf3aab485697b997718c573d6a780f802c3faab7a38119623"},
-{"name":"re2","version":"2.1.3","platform":"x86-mingw32","checksum":"6bfa3c1c119b485375688a9c90c0b8cfc03991495c2e4d50accb6bbcd406c186"},
-{"name":"re2","version":"2.1.3","platform":"x86_64-darwin","checksum":"513b12c5b7536c65e80ddb2a7eee0dbbefea534d6352e9470040016c547f90a5"},
-{"name":"re2","version":"2.1.3","platform":"x86_64-linux","checksum":"73a2e20fc1dc7b2773d2862ec061e545f6820643486c0d69e3ad40de19ce5c0b"},
+{"name":"re2","version":"2.3.0","platform":"aarch64-linux","checksum":"a5481148af570cac64196de1c2b87409e731181d8688885cf0e9633e1b19cccf"},
+{"name":"re2","version":"2.3.0","platform":"arm-linux","checksum":"93a973fa68ceee1ba34b12fe45131e3a3dde33b034b3b38f68dbcfac4da9ab9e"},
+{"name":"re2","version":"2.3.0","platform":"arm64-darwin","checksum":"bce955006e6efd9d6c45d98b00a7aa7af2d2465e983c1794e82eef0356fc7331"},
+{"name":"re2","version":"2.3.0","platform":"ruby","checksum":"15021a0e80f3b8934d6709c3ae410570c187625d46e24d4d8d472d5ef400a274"},
+{"name":"re2","version":"2.3.0","platform":"x64-mingw-ucrt","checksum":"b26623ed48eb9b5463658cd7fcac7a65fbe2be95731d5b6dcdf9c50dce4c3071"},
+{"name":"re2","version":"2.3.0","platform":"x64-mingw32","checksum":"95555a31d418ae54e77a35bf9cee248ba1633e4c076445ee3bbde31c692a9973"},
+{"name":"re2","version":"2.3.0","platform":"x86-linux","checksum":"2e693b321dada711e97f492172ad7dabb3ffa687758979ba08f2c68f170b1a55"},
+{"name":"re2","version":"2.3.0","platform":"x86-mingw32","checksum":"be81627fc0b2360ba14638b421d89f14410cd3c56924b9f6b5114170726b077d"},
+{"name":"re2","version":"2.3.0","platform":"x86_64-darwin","checksum":"b87a3920333363dc18fd898737a8e892ead62a81805fe829d9804e36d05f350b"},
+{"name":"re2","version":"2.3.0","platform":"x86_64-linux","checksum":"b0953007ffc5683c587db62fd5f2e4d332d8577b23d7ea611b5ea5c1f521337e"},
{"name":"recaptcha","version":"5.12.3","platform":"ruby","checksum":"37d1894add9e70a54d0c6c7f0ecbeedffbfa7d075acfbd4c509818dfdebdb7ee"},
{"name":"recursive-open-struct","version":"1.1.3","platform":"ruby","checksum":"a3538a72552fcebcd0ada657bdff313641a4a5fbc482c08cfb9a65acb1c9de5a"},
{"name":"redcarpet","version":"3.6.0","platform":"ruby","checksum":"8ad1889c0355ff4c47174af14edd06d62f45a326da1da6e8a121d59bdcd2e9e9"},
{"name":"redis","version":"4.8.0","platform":"ruby","checksum":"2000cf5014669c9dc821704b6d322a35a9a33852a95208911d9175d63b448a44"},
{"name":"redis-actionpack","version":"5.3.0","platform":"ruby","checksum":"3fb1ad0a8fd9d26a289c9399bb609dcaef38bf37711e6f677a53ca728fc19140"},
-{"name":"redis-namespace","version":"1.9.0","platform":"ruby","checksum":"0923961f38cf15b86cb57d92507e0a3b32480729eb5033249f5de8b12e0d8612"},
{"name":"redis-rack","version":"2.1.4","platform":"ruby","checksum":"0872eecb303e483c3863d6bd0d47323d230640d41c1a4ac4a2c7596ec0b1774c"},
{"name":"redis-store","version":"1.9.1","platform":"ruby","checksum":"7b4c7438d46f7b7ce8f67fc0eda3a04fc67d32d28cf606cc98a5df4d2b77071d"},
{"name":"regexp_parser","version":"2.6.0","platform":"ruby","checksum":"f163ba463a45ca2f2730e0902f2475bb0eefcd536dfc2f900a86d1e5a7d7a556"},
@@ -523,7 +532,7 @@
{"name":"rexml","version":"3.2.6","platform":"ruby","checksum":"e0669a2d4e9f109951cb1fde723d8acd285425d81594a2ea929304af50282816"},
{"name":"rinku","version":"2.0.0","platform":"ruby","checksum":"3e695aaf9f24baba3af45823b5c427b58a624582132f18482320e2737f9f8a85"},
{"name":"rotp","version":"6.3.0","platform":"ruby","checksum":"75d40087e65ed0d8022c33055a6306c1c400d1c12261932533b5d6cbcd868854"},
-{"name":"rouge","version":"4.1.3","platform":"ruby","checksum":"9c8663db26e05e52b3b0286daacae73ebb361c1bd31d7febd8c57087faa0b9a5"},
+{"name":"rouge","version":"4.2.0","platform":"ruby","checksum":"60dd666b3a223467dc72f5b7384764dfd7ad4e50b0df9eff072be58123506eba"},
{"name":"rqrcode","version":"2.2.0","platform":"ruby","checksum":"23eea88bb44c7ee6d6cab9354d08c287f7ebcdc6112e1fe7bcc2d010d1ffefc1"},
{"name":"rqrcode_core","version":"1.2.0","platform":"ruby","checksum":"cf4989dc82d24e2877984738c4ee569308625fed2a810960f1b02d68d0308d1a"},
{"name":"rspec","version":"3.12.0","platform":"ruby","checksum":"ccc41799a43509dc0be84070e3f0410ac95cbd480ae7b6c245543eb64162399c"},
@@ -539,21 +548,25 @@
{"name":"rspec-support","version":"3.12.0","platform":"ruby","checksum":"dd4d44b247ff679b95b5607ac5641d197a5f9b1d33f916123cb98fc5f917c58b"},
{"name":"rspec_junit_formatter","version":"0.6.0","platform":"ruby","checksum":"40dde674e6ae4e6cc0ff560da25497677e34fefd2338cc467a8972f602b62b15"},
{"name":"rspec_profiling","version":"0.0.6","platform":"ruby","checksum":"7a45697f79dcec9a174a0e26703465f6bd52ee78e8d798741240bfcef38f6e6e"},
-{"name":"rubocop","version":"1.50.2","platform":"ruby","checksum":"7cfeb0616f686ac61d049beae89f31446792d7e9f5728152657548f70aa78650"},
+{"name":"rubocop","version":"1.57.2","platform":"ruby","checksum":"8f679dfe42d7821dc61dafb17d14b1294343157a197b9f8a23720ca17fb9161b"},
{"name":"rubocop-ast","version":"1.29.0","platform":"ruby","checksum":"d1da2ab279a074baefc81758ac430c5768a8da8c7438dd4e5819ce5984d00ba1"},
-{"name":"rubocop-capybara","version":"2.18.0","platform":"ruby","checksum":"66b256755101f76dc455ba9694e2414bc957db5200401d204b00bc835401d605"},
-{"name":"rubocop-factory_bot","version":"2.23.1","platform":"ruby","checksum":"c19ee30c02e591f4293c07e943e22b7999c545d5010aac4d79621ee310850c4f"},
+{"name":"rubocop-capybara","version":"2.19.0","platform":"ruby","checksum":"fa329e0f185be313fa5dabd6056f83a718db7f4a259aa97fc287a40254899ccb"},
+{"name":"rubocop-factory_bot","version":"2.24.0","platform":"ruby","checksum":"3018d350315277200c31c98a5297c9d19463536c04bdeba0a75a512e3975e9f8"},
{"name":"rubocop-graphql","version":"0.19.0","platform":"ruby","checksum":"ba4b2fc91c9f0fda47e0870a6ae15a1e5525d6caffcb150dc88b00caaacc3e43"},
-{"name":"rubocop-performance","version":"1.18.0","platform":"ruby","checksum":"4c9d74f1b5bfaffb5b1cdb843279364198ac804e2644ae194615834dd011e02e"},
-{"name":"rubocop-rails","version":"2.20.2","platform":"ruby","checksum":"d20cbd613900fa22bcf85a7fba78ab68b21fc4f90b1e73c97284d40674332417"},
-{"name":"rubocop-rspec","version":"2.22.0","platform":"ruby","checksum":"2d7493222c81c78ad304ddd81aaf64b3543bcfac6d3d8706c220331921753a03"},
+{"name":"rubocop-performance","version":"1.19.1","platform":"ruby","checksum":"52664172d944eb45d478ed6d04c8b02c36cf0ee15726fabb6c90a95ca5cdfadf"},
+{"name":"rubocop-rails","version":"2.22.1","platform":"ruby","checksum":"db673cdb6321d8bb7627cd6cfb2cb36114acaa0e89581e4694b7304ce2acbd46"},
+{"name":"rubocop-rspec","version":"2.25.0","platform":"ruby","checksum":"083f8a0481dbb9969b2a9eae85670a454fe91d46812e6ec97b34e7f6227b99f3"},
{"name":"ruby-fogbugz","version":"0.3.0","platform":"ruby","checksum":"5e04cde474648f498a71cf1e1a7ab42c66b953862fbe224f793ec0a7a1d5f657"},
+{"name":"ruby-lsp","version":"0.12.3","platform":"ruby","checksum":"e49d82cdcb20c16f3b78556e3107af813f785c05d2d02658f810d03852db4567"},
+{"name":"ruby-lsp-rails","version":"0.2.7","platform":"ruby","checksum":"722c4613d212aa136733b36674e5773e2352de9b3c1a05cafec86dc589a47811"},
+{"name":"ruby-lsp-rspec","version":"0.1.5","platform":"ruby","checksum":"d26dcfcc0ad3e9690f22354a8b1c12e0eb5cc03949c7afa846af805f4fc842e5"},
{"name":"ruby-magic","version":"0.6.0","platform":"ruby","checksum":"7b2138877b7d23aff812c95564eba6473b74b815ef85beb0eb792e729a2b6101"},
{"name":"ruby-openai","version":"3.7.0","platform":"ruby","checksum":"fb735d4c055e282ade264cab9864944c05a8a10e0cddd45a0551e8a9851b1850"},
{"name":"ruby-progressbar","version":"1.11.0","platform":"ruby","checksum":"cc127db3866dc414ffccbf92928a241e585b3aa2b758a5563e74a6ee0f57d50a"},
{"name":"ruby-saml","version":"1.15.0","platform":"ruby","checksum":"3a9dda2b448310f4f90d5cf0967d4b668530fa7994d2a4d9cbfdfa62e35f76a3"},
{"name":"ruby-statistics","version":"3.0.0","platform":"ruby","checksum":"610301370346931cb701e3a8d3d3e28eb65681162cae6066c0c11abf20efdc81"},
{"name":"ruby2_keywords","version":"0.0.5","platform":"ruby","checksum":"ffd13740c573b7301cf7a2e61fc857b2a8e3d3aff32545d6f8300d8bae10e3ef"},
+{"name":"ruby_parser","version":"3.20.3","platform":"ruby","checksum":"8d2289a695dc81ffddcdd5a56e80c9a109806bc0d0b1239a1c852b0c71251c49"},
{"name":"rubyntlm","version":"0.6.3","platform":"ruby","checksum":"5b321456dba3130351f7451f8669f1afa83a0d26fd63cdec285b7b88e667102d"},
{"name":"rubypants","version":"0.2.0","platform":"ruby","checksum":"f07e38eac793655a0323fe91946081052341b9e69807026fcf102346589eedee"},
{"name":"rubyzip","version":"2.3.2","platform":"ruby","checksum":"3f57e3935dc2255c414484fbf8d673b4909d8a6a57007ed754dde39342d2373f"},
@@ -567,16 +580,17 @@
{"name":"sawyer","version":"0.9.2","platform":"ruby","checksum":"fa3a72d62a4525517b18857ddb78926aab3424de0129be6772a8e2ba240e7aca"},
{"name":"sd_notify","version":"0.1.1","platform":"ruby","checksum":"cbc7ac6caa7cedd26b30a72b5eeb6f36050dc0752df263452ea24fb5a4ad3131"},
{"name":"seed-fu","version":"2.3.7","platform":"ruby","checksum":"f19673443e9af799b730e3d4eca6a89b39e5a36825015dffd00d02ea3365cf74"},
-{"name":"selenium-webdriver","version":"4.14.0","platform":"ruby","checksum":"55726f81021d3f085ed9fcd318486b7ba90155598bb9e1fde7e4deeefe139d24"},
-{"name":"semver_dialects","version":"1.2.1","platform":"ruby","checksum":"60a1f67659f79c51a667e8858ec9b089c1e4ce4f6d2a0f0b4ac101916946eb23"},
+{"name":"selenium-webdriver","version":"4.15.0","platform":"ruby","checksum":"36134e883c4df98f1b7e8519a3753c77427b74621147f8245aa6cac306d52297"},
+{"name":"semver_dialects","version":"1.5.0","platform":"ruby","checksum":"0080f1abafc9c1af82d34e890d7c317b9eacb56b9e03040107ef5d1a51ca49ae"},
{"name":"sentry-rails","version":"5.8.0","platform":"ruby","checksum":"c11b2d909de2c2bfda793c45f64180fd784d54c46886338b683ee3f8efa7731b"},
{"name":"sentry-raven","version":"3.1.2","platform":"ruby","checksum":"103d3b122958810d34898ce2e705bcf549ddb9d855a70ce9a3970ee2484f364a"},
{"name":"sentry-ruby","version":"5.8.0","platform":"ruby","checksum":"caeb121433be379fb94e991a45265a287b13a9a9083e7264f539752369d37110"},
{"name":"sentry-sidekiq","version":"5.8.0","platform":"ruby","checksum":"90d1123d16a9fc5fd99dbad190b766dd189eaf9e2baddad641f1334e1877c779"},
{"name":"set","version":"1.0.2","platform":"ruby","checksum":"02ffa4de1f2621495e05b72326040dd014d7abbcb02fea698bc600a389992c02"},
+{"name":"sexp_processor","version":"4.17.0","platform":"ruby","checksum":"4daa4874ce1838cd801c65e66ed5d4f140024404a3de7482c36d4ef2604dff6f"},
{"name":"shellany","version":"0.0.1","platform":"ruby","checksum":"0e127a9132698766d7e752e82cdac8250b6adbd09e6c0a7fbbb6f61964fedee7"},
{"name":"shoulda-matchers","version":"5.1.0","platform":"ruby","checksum":"a01d20589989e9653ab4a28c67d9db2b82bcf0a2496cf01d5e1a95a4aaaf5b07"},
-{"name":"sidekiq","version":"6.5.7","platform":"ruby","checksum":"7d966fd84d42a942615d6874be31e40f8bece841fdd9b96fc53cad22a590555c"},
+{"name":"sidekiq","version":"6.5.12","platform":"ruby","checksum":"b4f93b2204c42220d0b526a7b8e0c49b5f9da82c1ce1a05d2baf1e8f744c197f"},
{"name":"sidekiq-cron","version":"1.8.0","platform":"ruby","checksum":"47da72ca73ce5b71896aaf7e7c4391386ec517dd003f184c50c0b727d82eb0ca"},
{"name":"sigdump","version":"0.2.4","platform":"ruby","checksum":"0bf2176e55c1a262788623fe5ea57caddd6ba2abebe5e349d9d5e7c3a3010ed7"},
{"name":"signet","version":"0.17.0","platform":"ruby","checksum":"1d2831930dc28da32e34bec68cf7ded97ee2867b208f97c500ee293829cb0004"},
@@ -586,11 +600,13 @@
{"name":"simplecov-html","version":"0.12.3","platform":"ruby","checksum":"4b1aad33259ffba8b29c6876c12db70e5750cb9df829486e4c6e5da4fa0aa07b"},
{"name":"simplecov-lcov","version":"0.8.0","platform":"ruby","checksum":"0115f31cb7ef5ec4334f5d9382c67fd43de2e5270e21b65bfc693da82dd713c1"},
{"name":"simplecov_json_formatter","version":"0.1.4","platform":"ruby","checksum":"529418fbe8de1713ac2b2d612aa3daa56d316975d307244399fa4838c601b428"},
+{"name":"singleton","version":"0.1.1","platform":"ruby","checksum":"b410b0417fcbb17bdfbc2d478ddba4c91e873d6e51c9d2d16b345c5ee5491c54"},
{"name":"sixarm_ruby_unaccent","version":"1.2.0","platform":"ruby","checksum":"0043a6077bdf2c4b03040152676a07f8bf77144f9b007b1960ee5c94d13a4384"},
{"name":"slack-messenger","version":"2.3.4","platform":"ruby","checksum":"49c611d2be5b0f9c250a3a957b9cc09b9c07b81dacb9843642d87b6fa35609c1"},
{"name":"snaky_hash","version":"2.0.0","platform":"ruby","checksum":"fe8b2e39e8ff69320f7812af73ea06401579e29ff1734a7009567391600687de"},
{"name":"snowplow-tracker","version":"0.8.0","platform":"ruby","checksum":"7ba6f4f1443a829845fd28e63eda72d9d3d247f485310ddcccaebbc52b734a38"},
{"name":"solargraph","version":"0.47.2","platform":"ruby","checksum":"87ca4b799b9155c2c31c15954c483e952fdacd800f52d6709b901dd447bcac6a"},
+{"name":"sorbet-runtime","version":"0.5.11120","platform":"ruby","checksum":"73112246db6c28ac93befb7335dfbf1ec96e583ee8724f2c1c177dc027586bd2"},
{"name":"sorted_set","version":"1.0.3","platform":"java","checksum":"996283f2e5c6e838825bcdcee31d6306515ae5f24bcb0ee4ce09dfff32919b8c"},
{"name":"sorted_set","version":"1.0.3","platform":"ruby","checksum":"4f2b8bee6e8c59cbd296228c0f1f81679357177a8b6859dcc2a99e86cce6372f"},
{"name":"spamcheck","version":"1.3.0","platform":"ruby","checksum":"a46082752257838d8484c844736e309ec499f85dcc51283a5f973b33f1c994f5"},
@@ -621,7 +637,7 @@
{"name":"sys-filesystem","version":"1.4.3","platform":"ruby","checksum":"390919de89822ad6d3ba3daf694d720be9d83ed95cdf7adf54d4573c98b17421"},
{"name":"sysexits","version":"1.2.0","platform":"ruby","checksum":"598241c4ae57baa403c125182dfdcc0d1ac4c0fb606dd47fbed57e4aaf795662"},
{"name":"table_print","version":"1.5.7","platform":"ruby","checksum":"436664281f93387b882335795e16cfeeb839ad0c785ff7f9110fc0f17c68b5cb"},
-{"name":"tanuki_emoji","version":"0.7.0","platform":"ruby","checksum":"d10df452d8087b2c6a0eecb888609315d47bb30bb9e17c11441869cf24aae987"},
+{"name":"tanuki_emoji","version":"0.9.0","platform":"ruby","checksum":"009f0b283f61b7aed5f57d7d1f050225f2a5df8eec121550a67bdd7b95c74056"},
{"name":"telesign","version":"2.2.4","platform":"ruby","checksum":"dcc6e96ea7bcb4da1e2ae786bfe7a4d670a4b5f94ae95dfcdde77d547c544c42"},
{"name":"telesignenterprise","version":"2.2.2","platform":"ruby","checksum":"f147a03263a8c2fe0a0db1a7a9454a6ee37d9e8abd58eaca305bdd8081f9f1b3"},
{"name":"temple","version":"0.8.2","platform":"ruby","checksum":"c12071214346c606dbd219b4117276d04a9f2c20d65e66a66b2c4ec18efc1f18"},
@@ -629,9 +645,9 @@
{"name":"terminal-table","version":"3.0.2","platform":"ruby","checksum":"f951b6af5f3e00203fb290a669e0a85c5dd5b051b3b023392ccfd67ba5abae91"},
{"name":"terser","version":"1.0.2","platform":"ruby","checksum":"80c2e0bc7e2db4e12e8529658f9e0820e13d685ae67d745bf981f269743bb28e"},
{"name":"test-prof","version":"1.2.3","platform":"ruby","checksum":"c52a40194cb30f399ed3eb6beb4c45b5daad8b8eb418e8ef69089e4dc7e01fd6"},
-{"name":"test_file_finder","version":"0.1.4","platform":"ruby","checksum":"bc36d8339eac4fb9dc36514a7c5f4d389ac2fb6d010716fc715c5c8fbb98eacd"},
+{"name":"test_file_finder","version":"0.2.1","platform":"ruby","checksum":"a5e9b369d80c76aefbb609acf5e11d89a048f35e565de3cc261c20112f0fcdb3"},
{"name":"text","version":"1.3.1","platform":"ruby","checksum":"2fbbbc82c1ce79c4195b13018a87cbb00d762bda39241bb3cdc32792759dd3f4"},
-{"name":"thor","version":"1.2.2","platform":"ruby","checksum":"2f93c652828cba9fcf4f65f5dc8c306f1a7317e05aad5835a13740122c17f24c"},
+{"name":"thor","version":"1.3.0","platform":"ruby","checksum":"1adc7f9e5b3655a68c71393fee8bd0ad088d14ee8e83a0b73726f23cbb3ca7c3"},
{"name":"thread_safe","version":"0.3.6","platform":"java","checksum":"bb28394cd0924c068981adee71f36a81c85c92e7d74d3f62372bd51489a0e0c2"},
{"name":"thread_safe","version":"0.3.6","platform":"ruby","checksum":"9ed7072821b51c57e8d6b7011a8e282e25aeea3a4065eab326e43f66f063b05a"},
{"name":"thrift","version":"0.16.0","platform":"ruby","checksum":"d023286ea89e30444c9f1c28dd76107f87d8aaf85fe1742da1d8cd3b5417dcce"},
@@ -643,7 +659,7 @@
{"name":"tomlrb","version":"1.3.0","platform":"ruby","checksum":"68666bf53fa70ba686a48a7435ce7e086f5227c58c4c993bd9792f4760f2a503"},
{"name":"tpm-key_attestation","version":"0.12.0","platform":"ruby","checksum":"e133d80cf24fef0e7a7dfad00fd6aeff01fc79875fbfc66cd8537bbd622b1e6d"},
{"name":"trailblazer-option","version":"0.1.2","platform":"ruby","checksum":"20e4f12ea4e1f718c8007e7944ca21a329eee4eed9e0fa5dde6e8ad8ac4344a3"},
-{"name":"train-core","version":"3.4.9","platform":"ruby","checksum":"d7ad8fa9a379c43a30baaaf1141af1cb28349d386c054f7fc81d169a625d6edd"},
+{"name":"train-core","version":"3.10.8","platform":"ruby","checksum":"8493da02015fbe9b11840d22ba879ef18a0aa2633cb0c04eac3f07dd9b87223b"},
{"name":"truncato","version":"0.7.12","platform":"ruby","checksum":"fed9e8a04fa35fd1a64506cd2089761bae4adfe47e756c3ce98a5c43856c9c4c"},
{"name":"tty-color","version":"0.6.0","platform":"ruby","checksum":"6f9c37ca3a4e2367fb2e6d09722762647d6f455c111f05b59f35730eeb24332a"},
{"name":"tty-command","version":"0.10.1","platform":"ruby","checksum":"0c6c471fcb932d55518734eb4e2e07e9efdd2918713cc39bb7393ba862471192"},
@@ -675,7 +691,7 @@
{"name":"validates_hostname","version":"1.0.13","platform":"ruby","checksum":"eac40178cc0b4f727df9cc6a5cb5bc2550718ad8d9bb3728df9aba6354bdda19"},
{"name":"version_gem","version":"1.1.0","platform":"ruby","checksum":"6b009518020db57f51ec7b410213fae2bf692baea9f1b51770db97fbc93d9a80"},
{"name":"version_sorter","version":"2.3.0","platform":"ruby","checksum":"2147f2a1a3804fbb8f60d268b7d7c1ec717e6dd727ffe2c165b4e05e82efe1da"},
-{"name":"view_component","version":"3.6.0","platform":"ruby","checksum":"7aa45c11b4fd51583bd63b10fbc6b1a87f088182e4f026e5f4f6a9211e5a42a3"},
+{"name":"view_component","version":"3.7.0","platform":"ruby","checksum":"648909bde6c188621d607732d64a82515b03e69761c8098a0fe4336166d7e403"},
{"name":"virtus","version":"2.0.0","platform":"ruby","checksum":"8841dae4eb7fcc097320ba5ea516bf1839e5d056c61ee27138aa4bddd6e3d1c2"},
{"name":"vite_rails","version":"3.0.15","platform":"ruby","checksum":"b8ec528aedf7e24b54f222b449cd9250810ea2456d5f8dd4ef87f06b475cf860"},
{"name":"vite_ruby","version":"3.3.4","platform":"ruby","checksum":"025e438385a6dc2320c8c148dff453f5bb1d4f056ce69c3386f47d4c388ad80c"},
@@ -693,7 +709,7 @@
{"name":"wikicloth","version":"0.8.1","platform":"ruby","checksum":"7ac8a9ca0a948cf472851e521afc6c2a6b04a8f91ef1d824ba6a61ffbd60e6ca"},
{"name":"wisper","version":"2.0.1","platform":"ruby","checksum":"ce17bc5c3a166f241a2e6613848b025c8146fce2defba505920c1d1f3f88fae6"},
{"name":"with_env","version":"1.1.0","platform":"ruby","checksum":"50b3e4f0a6cda8f90d8a6bd87a6261f6c381429abafb161c4c69ad4a0cd0b6e4"},
-{"name":"wmi-lite","version":"1.0.5","platform":"ruby","checksum":"14efa710be3226e281a66ab93f7ebc92f5e0807029e02b9cf1d3f39d15d90d84"},
+{"name":"wmi-lite","version":"1.0.7","platform":"ruby","checksum":"116ef5bb470dbe60f58c2db9047af3064c16245d6562c646bc0d90877e27ddda"},
{"name":"xml-simple","version":"1.1.9","platform":"ruby","checksum":"d21131e519c86f1a5bc2b6d2d57d46e6998e47f18ed249b25cad86433dbd695d"},
{"name":"xpath","version":"3.2.0","platform":"ruby","checksum":"6dfda79d91bb3b949b947ecc5919f042ef2f399b904013eb3ef6d20dd3a4082e"},
{"name":"yajl-ruby","version":"1.4.3","platform":"ruby","checksum":"8c974d9c11ae07b0a3b6d26efea8407269b02e4138118fbe3ef0d2ec9724d1d2"},
diff --git a/Gemfile.lock b/Gemfile.lock
index 052a59d6b7f..e2ebb913813 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -24,10 +24,17 @@ PATH
typhoeus (~> 1.0, >= 1.0.1)
PATH
+ remote: gems/gitlab-backup-cli
+ specs:
+ gitlab-backup-cli (0.0.1)
+ thor (~> 1.3)
+
+PATH
remote: gems/gitlab-http
specs:
gitlab-http (0.1.0)
activesupport (~> 7)
+ concurrent-ruby (~> 1.2)
httparty (~> 0.21.0)
ipaddress (~> 0.8.3)
nokogiri (~> 1.15.4)
@@ -37,7 +44,7 @@ PATH
remote: gems/gitlab-rspec
specs:
gitlab-rspec (0.1.0)
- activesupport (>= 6.1, < 7.1)
+ activesupport (>= 6.1, < 8)
rspec (~> 3.0)
PATH
@@ -103,7 +110,7 @@ PATH
specs:
devise-pbkdf2-encryptable (0.0.0)
devise (~> 4.0)
- devise-two-factor (~> 4.0)
+ devise-two-factor (~> 4.1.1)
PATH
remote: vendor/gems/mail-smtp_pool
@@ -152,7 +159,7 @@ PATH
PATH
remote: vendor/gems/sidekiq-reliable-fetch
specs:
- gitlab-sidekiq-fetcher (0.9.0)
+ gitlab-sidekiq-fetcher (0.10.0)
json (>= 2.5)
sidekiq (~> 6.1)
@@ -161,7 +168,7 @@ GEM
specs:
CFPropertyList (3.0.5)
rexml
- RedCloth (4.3.2)
+ RedCloth (4.3.3)
acme-client (2.0.11)
faraday (>= 1.0, < 3.0.0)
faraday-retry (~> 1.0)
@@ -233,8 +240,8 @@ GEM
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
- acts-as-taggable-on (9.0.1)
- activerecord (>= 6.0, < 7.1)
+ acts-as-taggable-on (10.0.0)
+ activerecord (>= 6.1, < 7.2)
addressable (2.8.1)
public_suffix (>= 2.0.2, < 6.0)
aes_key_wrap (1.1.0)
@@ -242,9 +249,12 @@ GEM
aliyun-sdk (0.8.0)
nokogiri (~> 1.6)
rest-client (~> 2.0)
+ amatch (0.4.1)
+ mize
+ tins (~> 1.0)
android_key_attestation (0.3.0)
- apollo_upload_server (2.1.0)
- actionpack (>= 4.2)
+ apollo_upload_server (2.1.5)
+ actionpack (>= 6.1.6)
graphql (>= 1.8)
app_store_connect (0.29.0)
activesupport (>= 6.0.0)
@@ -270,7 +280,7 @@ GEM
aws-sdk-cloudformation (1.41.0)
aws-sdk-core (~> 3, >= 3.99.0)
aws-sigv4 (~> 1.1)
- aws-sdk-core (3.185.1)
+ aws-sdk-core (3.186.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.5)
@@ -322,11 +332,11 @@ GEM
bindata (2.4.11)
binding_of_caller (1.0.0)
debug_inspector (>= 0.0.1)
- bootsnap (1.16.0)
+ bootsnap (1.17.0)
msgpack (~> 1.2)
browser (5.3.1)
builder (3.2.4)
- bullet (7.1.1)
+ bullet (7.1.2)
activesupport (>= 3.0.0)
uniform_notifier (~> 1.11)
bundler-audit (0.9.1)
@@ -354,14 +364,15 @@ GEM
character_set (1.4.1)
sorted_set (~> 1.0)
charlock_holmes (0.7.7)
- chef-config (16.10.17)
+ chef-config (18.3.0)
addressable
- chef-utils (= 16.10.17)
+ chef-utils (= 18.3.0)
fuzzyurl
mixlib-config (>= 2.2.12, < 4.0)
mixlib-shellout (>= 2.0, < 4.0)
tomlrb (~> 1.2)
- chef-utils (16.10.17)
+ chef-utils (18.3.0)
+ concurrent-ruby
chunky_png (1.4.0)
circuitbox (2.0.0)
citrus (3.0.2)
@@ -411,9 +422,13 @@ GEM
danger
gitlab (~> 4.2, >= 4.2.0)
dartsass (1.49.8)
- database_cleaner (1.7.0)
+ database_cleaner-active_record (2.1.0)
+ activerecord (>= 5.a)
+ database_cleaner-core (~> 2.0.0)
+ database_cleaner-core (2.0.1)
date (3.3.3)
dead_end (3.1.1)
+ deb_version (1.0.2)
debug_inspector (1.1.0)
deckar01-task_list (2.3.3)
html-pipeline
@@ -435,9 +450,9 @@ GEM
thor (>= 0.19, < 2)
descendants_tracker (0.0.4)
thread_safe (~> 0.3, >= 0.3.1)
- devfile (0.0.23.pre.alpha1)
+ devfile (0.0.24.pre.alpha1)
device_detector (1.0.0)
- devise (4.8.1)
+ devise (4.9.3)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
railties (>= 4.1.0)
@@ -609,6 +624,7 @@ GEM
fog-core
nokogiri (>= 1.5.11, < 2.0.0)
formatador (0.2.5)
+ forwardable (1.3.3)
fugit (1.8.1)
et-orbi (~> 1, >= 1.2.7)
raabro (~> 1.4)
@@ -627,15 +643,18 @@ GEM
gemoji (3.0.1)
get_process_mem (0.2.7)
ffi (~> 1.0)
- gettext (3.3.6)
+ gettext (3.4.9)
+ erubi
locale (>= 2.0.5)
+ prime
+ racc
text (>= 1.3.0)
gettext_i18n_rails (1.11.0)
fast_gettext (>= 0.9.0)
- gettext_i18n_rails_js (1.3.0)
+ gettext_i18n_rails_js (2.0.0)
gettext (>= 3.0.2)
gettext_i18n_rails (>= 0.7.1)
- po_to_json (>= 1.0.0)
+ po_to_json (>= 2.0.0)
rails (>= 3.2.0)
git (1.18.0)
addressable (~> 2.8)
@@ -647,7 +666,7 @@ GEM
terminal-table (>= 1.5.1)
gitlab-chronic (0.10.5)
numerizer (~> 0.2)
- gitlab-dangerfiles (4.3.2)
+ gitlab-dangerfiles (4.6.0)
danger (>= 9.3.0)
danger-gitlab (>= 8.0.0)
rake (~> 13.0)
@@ -675,8 +694,8 @@ GEM
oauth2 (>= 1.4.4, < 3)
gitlab-markup (1.9.0)
gitlab-net-dns (0.9.2)
- gitlab-styles (10.1.0)
- rubocop (~> 1.50.2)
+ gitlab-styles (11.0.0)
+ rubocop (~> 1.57.1)
rubocop-graphql (~> 0.18)
rubocop-performance (~> 1.15)
rubocop-rails (~> 2.17)
@@ -688,8 +707,9 @@ GEM
omniauth (>= 1.3, < 3)
pyu-ruby-sasl (>= 0.0.3.3, < 0.1)
rubyntlm (~> 0.5)
- gitlab_quality-test_tooling (1.3.0)
+ gitlab_quality-test_tooling (1.5.0)
activesupport (>= 6.1, < 7.2)
+ amatch (~> 0.4.1)
gitlab (~> 4.19)
http (~> 5.0)
nokogiri (~> 1.10)
@@ -758,7 +778,7 @@ GEM
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
- google-protobuf (3.24.4)
+ google-protobuf (3.25.0)
googleapis-common-protos (1.4.0)
google-protobuf (~> 3.14)
googleapis-common-protos-types (~> 1.2)
@@ -947,6 +967,7 @@ GEM
jsonpath (~> 1.0)
recursive-open-struct (~> 1.1, >= 1.1.1)
rest-client (~> 2.0)
+ language_server-protocol (3.17.0.3)
launchy (2.5.0)
addressable (~> 2.7)
lefthook (1.5.2)
@@ -957,7 +978,7 @@ GEM
letter_opener (~> 1.7)
railties (>= 5.2)
rexml
- libyajl2 (1.2.0)
+ libyajl2 (2.1.0)
license_finder (7.0.1)
bundler
rubyzip (>= 1, < 3)
@@ -1020,14 +1041,16 @@ GEM
mini_histogram (0.3.1)
mini_magick (4.10.1)
mini_mime (1.1.2)
- mini_portile2 (2.8.4)
+ mini_portile2 (2.8.5)
minitest (5.11.3)
mixlib-cli (2.1.8)
- mixlib-config (3.0.9)
+ mixlib-config (3.0.27)
tomlrb
mixlib-log (3.0.9)
- mixlib-shellout (3.2.5)
+ mixlib-shellout (3.2.7)
chef-utils
+ mize (0.4.1)
+ protocol (~> 2.0)
msgpack (1.5.4)
multi_json (1.14.1)
multi_xml (0.6.0)
@@ -1055,11 +1078,11 @@ GEM
net-protocol
net-protocol (0.1.3)
timeout
- net-scp (3.0.0)
- net-ssh (>= 2.6.5, < 7.0.0)
+ net-scp (4.0.0)
+ net-ssh (>= 2.6.5, < 8.0.0)
net-smtp (0.3.3)
net-protocol
- net-ssh (6.0.0)
+ net-ssh (7.2.0)
netrc (0.11.0)
nio4r (2.5.8)
no_proxy_fix (0.1.2)
@@ -1081,9 +1104,9 @@ GEM
octokit (6.1.1)
faraday (>= 1, < 3)
sawyer (~> 0.9)
- ohai (17.9.0)
- chef-config (>= 14.12, < 18)
- chef-utils (>= 16.0, < 18)
+ ohai (18.1.3)
+ chef-config (>= 14.12, < 19)
+ chef-utils (>= 16.0, < 19)
ffi (~> 1.9)
ffi-yajl (~> 2.2)
ipaddress
@@ -1198,9 +1221,9 @@ GEM
pg (1.5.4)
pg_query (4.2.3)
google-protobuf (>= 3.22.3)
- plist (3.6.0)
+ plist (3.7.0)
png_quantizator (0.2.1)
- po_to_json (1.0.1)
+ po_to_json (2.0.0)
json (>= 1.6.0)
premailer (1.16.0)
addressable
@@ -1209,12 +1232,18 @@ GEM
premailer-rails (1.10.3)
actionmailer (>= 3)
premailer (~> 1.7, >= 1.7.9)
+ prime (0.1.2)
+ forwardable
+ singleton
+ prism (0.17.1)
proc_to_ast (0.1.0)
coderay
parser
unparser
prometheus-client-mmap (0.28.1)
rb_sys (~> 0.9)
+ protocol (2.0.0)
+ ruby_parser (~> 3.0)
pry (0.14.2)
coderay (~> 1.1)
method_source (~> 1.0)
@@ -1298,8 +1327,8 @@ GEM
optimist (>= 3.0.0)
rbtree (0.4.6)
rchardet (1.8.0)
- re2 (2.1.3)
- mini_portile2 (~> 2.8.4)
+ re2 (2.3.0)
+ mini_portile2 (~> 2.8.5)
recaptcha (5.12.3)
json
recursive-open-struct (1.1.3)
@@ -1309,8 +1338,6 @@ GEM
actionpack (>= 5, < 8)
redis-rack (>= 2.1.0, < 3)
redis-store (>= 1.1.0, < 2)
- redis-namespace (1.9.0)
- redis (>= 4)
redis-rack (2.1.4)
rack (>= 2.0.8, < 3)
redis-store (>= 1.2, < 2)
@@ -1338,7 +1365,7 @@ GEM
rexml (3.2.6)
rinku (2.0.0)
rotp (6.3.0)
- rouge (4.1.3)
+ rouge (4.2.0)
rqrcode (2.2.0)
chunky_png (~> 1.0)
rqrcode_core (~> 1.0)
@@ -1389,38 +1416,49 @@ GEM
pg
rails
sqlite3
- rubocop (1.50.2)
+ rubocop (1.57.2)
json (~> 2.3)
+ language_server-protocol (>= 3.17.0)
parallel (~> 1.10)
- parser (>= 3.2.0.0)
+ parser (>= 3.2.2.4)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0)
- rubocop-ast (>= 1.28.0, < 2.0)
+ rubocop-ast (>= 1.28.1, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.29.0)
parser (>= 3.2.1.0)
- rubocop-capybara (2.18.0)
+ rubocop-capybara (2.19.0)
rubocop (~> 1.41)
- rubocop-factory_bot (2.23.1)
+ rubocop-factory_bot (2.24.0)
rubocop (~> 1.33)
rubocop-graphql (0.19.0)
rubocop (>= 0.87, < 2)
- rubocop-performance (1.18.0)
+ rubocop-performance (1.19.1)
rubocop (>= 1.7.0, < 2.0)
rubocop-ast (>= 0.4.0)
- rubocop-rails (2.20.2)
+ rubocop-rails (2.22.1)
activesupport (>= 4.2.0)
rack (>= 1.1)
rubocop (>= 1.33.0, < 2.0)
- rubocop-rspec (2.22.0)
- rubocop (~> 1.33)
+ rubocop-rspec (2.25.0)
+ rubocop (~> 1.40)
rubocop-capybara (~> 2.17)
rubocop-factory_bot (~> 2.22)
ruby-fogbugz (0.3.0)
crack (~> 0.4)
multipart-post (~> 2.0)
+ ruby-lsp (0.12.3)
+ language_server-protocol (~> 3.17.0)
+ prism (>= 0.17.1, < 0.18)
+ sorbet-runtime (>= 0.5.5685)
+ ruby-lsp-rails (0.2.7)
+ rails (>= 6.0)
+ ruby-lsp (>= 0.12.0, < 0.13.0)
+ sorbet-runtime (>= 0.5.9897)
+ ruby-lsp-rspec (0.1.5)
+ ruby-lsp (~> 0.12.0)
ruby-magic (0.6.0)
mini_portile2 (~> 2.8)
ruby-openai (3.7.0)
@@ -1431,6 +1469,8 @@ GEM
rexml
ruby-statistics (3.0.0)
ruby2_keywords (0.0.5)
+ ruby_parser (3.20.3)
+ sexp_processor (~> 4.16)
rubyntlm (0.6.3)
rubypants (0.2.0)
rubyzip (2.3.2)
@@ -1456,13 +1496,14 @@ GEM
seed-fu (2.3.7)
activerecord (>= 3.1)
activesupport (>= 3.1)
- selenium-webdriver (4.14.0)
+ selenium-webdriver (4.15.0)
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0)
websocket (~> 1.0)
- semver_dialects (1.2.1)
+ semver_dialects (1.5.0)
+ deb_version (~> 1.0.1)
pastel (~> 0.8.0)
- thor (~> 1.2.0)
+ thor (~> 1.3)
tty-command (~> 0.10.1)
sentry-rails (5.8.0)
railties (>= 5.0)
@@ -1475,11 +1516,12 @@ GEM
sentry-ruby (~> 5.8.0)
sidekiq (>= 3.0)
set (1.0.2)
+ sexp_processor (4.17.0)
shellany (0.0.1)
shoulda-matchers (5.1.0)
activesupport (>= 5.2.0)
- sidekiq (6.5.7)
- connection_pool (>= 2.2.5)
+ sidekiq (6.5.12)
+ connection_pool (>= 2.2.5, < 3)
rack (~> 2.0)
redis (>= 4.5.0, < 5)
sidekiq-cron (1.8.0)
@@ -1502,6 +1544,7 @@ GEM
simplecov-html (0.12.3)
simplecov-lcov (0.8.0)
simplecov_json_formatter (0.1.4)
+ singleton (0.1.1)
sixarm_ruby_unaccent (1.2.0)
slack-messenger (2.3.4)
snaky_hash (2.0.0)
@@ -1523,6 +1566,7 @@ GEM
thor (~> 1.0)
tilt (~> 2.0)
yard (~> 0.9, >= 0.9.24)
+ sorbet-runtime (0.5.11120)
sorted_set (1.0.3)
rbtree
set (~> 1.0)
@@ -1565,7 +1609,7 @@ GEM
ffi (~> 1.1)
sysexits (1.2.0)
table_print (1.5.7)
- tanuki_emoji (0.7.0)
+ tanuki_emoji (0.9.0)
telesign (2.2.4)
net-http-persistent (>= 3.0.0, < 5.0)
telesignenterprise (2.2.2)
@@ -1578,10 +1622,10 @@ GEM
terser (1.0.2)
execjs (>= 0.3.0, < 3)
test-prof (1.2.3)
- test_file_finder (0.1.4)
- faraday (~> 1.0)
+ test_file_finder (0.2.1)
+ faraday (>= 1.0, < 3.0, != 2.0.0)
text (1.3.1)
- thor (1.2.2)
+ thor (1.3.0)
thread_safe (0.3.6)
thrift (0.16.0)
tilt (2.0.11)
@@ -1597,13 +1641,13 @@ GEM
openssl (> 2.0)
openssl-signature_algorithm (~> 1.0)
trailblazer-option (0.1.2)
- train-core (3.4.9)
+ train-core (3.10.8)
addressable (~> 2.5)
ffi (!= 1.13.0)
json (>= 1.8, < 3.0)
mixlib-shellout (>= 2.0, < 4.0)
- net-scp (>= 1.2, < 4.0)
- net-ssh (>= 2.9, < 7.0)
+ net-scp (>= 1.2, < 5.0)
+ net-ssh (>= 2.9, < 8.0)
truncato (0.7.12)
htmlentities (~> 4.3.1)
nokogiri (>= 1.7.0, <= 2.0)
@@ -1662,7 +1706,7 @@ GEM
activesupport (>= 3.0)
version_gem (1.1.0)
version_sorter (2.3.0)
- view_component (3.6.0)
+ view_component (3.7.0)
activesupport (>= 5.2.0, < 8.0)
concurrent-ruby (~> 1.0)
method_source (~> 1.0)
@@ -1708,7 +1752,7 @@ GEM
rinku
wisper (2.0.1)
with_env (1.1.0)
- wmi-lite (1.0.5)
+ wmi-lite (1.0.7)
xml-simple (1.1.9)
rexml
xpath (3.2.0)
@@ -1722,14 +1766,14 @@ PLATFORMS
DEPENDENCIES
CFPropertyList (~> 3.0.0)
- RedCloth (~> 4.3.2)
+ RedCloth (~> 4.3.3)
acme-client (~> 2.0)
activerecord-explain-analyze (~> 0.1)
activerecord-gitlab!
- acts-as-taggable-on (~> 9.0)
+ acts-as-taggable-on (~> 10.0)
addressable (~> 2.8)
akismet (~> 3.0)
- apollo_upload_server (~> 2.1.0)
+ apollo_upload_server (~> 2.1.5)
app_store_connect
arr-pm (~> 0.0.12)
asciidoctor (~> 2.0.18)
@@ -1741,7 +1785,7 @@ DEPENDENCIES
autoprefixer-rails (= 10.2.5.1)
awesome_print
aws-sdk-cloudformation (~> 1)
- aws-sdk-core (~> 3.185.1)
+ aws-sdk-core (~> 3.186.0)
aws-sdk-s3 (~> 1.136.0)
axe-core-rspec
babosa (~> 2.0)
@@ -1751,9 +1795,9 @@ DEPENDENCIES
benchmark-ips (~> 2.11.0)
benchmark-memory (~> 0.1)
better_errors (~> 2.10.1)
- bootsnap (~> 1.16.0)
+ bootsnap (~> 1.17.0)
browser (~> 5.3.1)
- bullet (~> 7.1.1)
+ bullet (~> 7.1.2)
bundler-audit (~> 0.9.1)
bundler-checksum (~> 0.1.0)!
capybara (~> 3.39, >= 3.39.2)
@@ -1771,14 +1815,14 @@ DEPENDENCIES
crystalball (~> 0.7.0)
csv_builder!
cvss-suite (~> 3.0.1)
- database_cleaner (~> 1.7.0)
+ database_cleaner-active_record (~> 2.1.0)
deckar01-task_list (= 2.3.3)
declarative_policy (~> 1.1.0)
deprecation_toolkit (~> 1.5.1)
derailed_benchmarks
- devfile (~> 0.0.23.pre.alpha1)
+ devfile (~> 0.0.24.pre.alpha1)
device_detector
- devise (~> 4.8.1)
+ devise (~> 4.9.3)
devise-pbkdf2-encryptable (~> 0.0.0)!
devise-two-factor (~> 4.1.1)
diff_match_patch (~> 0.1.0)
@@ -1811,10 +1855,11 @@ DEPENDENCIES
fuubar (~> 2.2.0)
gettext (~> 3.3)
gettext_i18n_rails (~> 1.11.0)
- gettext_i18n_rails_js (~> 1.3)
+ gettext_i18n_rails_js (~> 2.0.0)
gitaly (~> 16.5.0.pre.rc1)
+ gitlab-backup-cli!
gitlab-chronic (~> 0.10.5)
- gitlab-dangerfiles (~> 4.3.2)
+ gitlab-dangerfiles (~> 4.6.0)
gitlab-experiment (~> 0.8.0)
gitlab-fog-azure-rm (~> 1.8.0)
gitlab-http!
@@ -1827,11 +1872,11 @@ DEPENDENCIES
gitlab-safe_request_store!
gitlab-schema-validation!
gitlab-sidekiq-fetcher!
- gitlab-styles (~> 10.1.0)
+ gitlab-styles (~> 11.0.0)
gitlab-utils!
gitlab_chronic_duration (~> 0.12)
gitlab_omniauth-ldap (~> 2.2.0)
- gitlab_quality-test_tooling (~> 1.3.0)
+ gitlab_quality-test_tooling (~> 1.5.0)
gon (~> 6.4.0)
google-apis-androidpublisher_v3 (~> 0.34.0)
google-apis-cloudbilling_v1 (~> 0.21.0)
@@ -1844,7 +1889,7 @@ DEPENDENCIES
google-apis-serviceusage_v1 (~> 0.28.0)
google-apis-sqladmin_v1beta4 (~> 0.41.0)
google-cloud-storage (~> 1.44.0)
- google-protobuf (~> 3.24, >= 3.24.4)
+ google-protobuf (~> 3.25)
gpgme (~> 2.0.23)
grape (~> 1.7.1)
grape-entity (~> 0.10.0)
@@ -1910,7 +1955,7 @@ DEPENDENCIES
nokogiri (~> 1.15, >= 1.15.4)
oauth2 (~> 2.0)
octokit (~> 6.0)
- ohai (~> 17.9)
+ ohai (~> 18.1)
oj (~> 3.13.21)
oj-introspect (~> 0.7)
omniauth (~> 2.1.0)
@@ -1959,16 +2004,15 @@ DEPENDENCIES
rails-i18n (~> 7.0)
rainbow (~> 3.0)
rbtrace (~> 0.4)
- re2 (= 2.1.3)
+ re2 (= 2.3.0)
recaptcha (~> 5.12)
redis (~> 4.8.0)
redis-actionpack (~> 5.3.0)
- redis-namespace (~> 1.9.0)
request_store (~> 1.5.1)
responders (~> 3.0)
retriable (~> 3.1.2)
rexml (~> 3.2.6)
- rouge (~> 4.1.3)
+ rouge (~> 4.2.0)
rqrcode (~> 2.0)
rspec-benchmark (~> 0.6.0)
rspec-parameterized (~> 1.0)
@@ -1979,6 +2023,9 @@ DEPENDENCIES
rspec_profiling (~> 0.0.6)
rubocop
ruby-fogbugz (~> 0.3.0)
+ ruby-lsp (~> 0.12.3)
+ ruby-lsp-rails (~> 0.2.7)
+ ruby-lsp-rspec (~> 0.1.5)
ruby-magic (~> 0.6)
ruby-openai (~> 3.7)
ruby-progressbar (~> 1.10)
@@ -1989,14 +2036,14 @@ DEPENDENCIES
sassc-rails (~> 2.1.0)
sd_notify (~> 0.1.0)
seed-fu (~> 2.3.7)
- selenium-webdriver (~> 4.14)
- semver_dialects (~> 1.2.1)
+ selenium-webdriver (~> 4.15)
+ semver_dialects (~> 1.5)
sentry-rails (~> 5.8.0)
sentry-raven (~> 3.1)
sentry-ruby (~> 5.8.0)
sentry-sidekiq (~> 5.8.0)
shoulda-matchers (~> 5.1.0)
- sidekiq (~> 6.5.7)
+ sidekiq (~> 6.5.10)
sidekiq-cron (~> 1.8.0)
sigdump (~> 0.2.4)
simple_po_parser (~> 1.1.6)
@@ -2015,11 +2062,11 @@ DEPENDENCIES
stackprof (~> 0.2.25)
state_machines-activerecord (~> 0.8.0)
sys-filesystem (~> 1.4.3)
- tanuki_emoji (~> 0.7)
+ tanuki_emoji (~> 0.9)
telesignenterprise (~> 2.2)
terser (= 1.0.2)
test-prof (~> 1.2.3)
- test_file_finder (~> 0.1.3)
+ test_file_finder (~> 0.2.1)
thrift (>= 0.16.0)
timfel-krb5-auth (~> 0.8)
toml-rb (~> 2.2.0)
@@ -2030,7 +2077,7 @@ DEPENDENCIES
valid_email (~> 0.1)
validates_hostname (~> 1.0.13)
version_sorter (~> 2.3)
- view_component (~> 3.6.0)
+ view_component (~> 3.7.0)
vite_rails
vmstat (~> 2.3.0)
warning (~> 1.3.0)
@@ -2041,4 +2088,4 @@ DEPENDENCIES
yajl-ruby (~> 1.4.3)
BUNDLED WITH
- 2.4.20
+ 2.4.21
diff --git a/app/assets/javascripts/add_context_commits_modal/store/actions.js b/app/assets/javascripts/add_context_commits_modal/store/actions.js
index f085b0d0e5e..890db374160 100644
--- a/app/assets/javascripts/add_context_commits_modal/store/actions.js
+++ b/app/assets/javascripts/add_context_commits_modal/store/actions.js
@@ -1,5 +1,5 @@
import _ from 'lodash';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import Api from '~/api';
import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
diff --git a/app/assets/javascripts/admin/abuse_report/components/abuse_report_app.vue b/app/assets/javascripts/admin/abuse_report/components/abuse_report_app.vue
index 3c46de7c2be..f0540ffa71e 100644
--- a/app/assets/javascripts/admin/abuse_report/components/abuse_report_app.vue
+++ b/app/assets/javascripts/admin/abuse_report/components/abuse_report_app.vue
@@ -7,6 +7,7 @@ import ReportDetails from './report_details.vue';
import ReportedContent from './reported_content.vue';
import ActivityEventsList from './activity_events_list.vue';
import ActivityHistoryItem from './activity_history_item.vue';
+import AbuseReportNotes from './abuse_report_notes.vue';
const alertDefaults = {
visible: false,
@@ -24,6 +25,7 @@ export default {
ReportedContent,
ActivityEventsList,
ActivityHistoryItem,
+ AbuseReportNotes,
},
mixins: [glFeatureFlagsMixin()],
props: {
@@ -96,5 +98,10 @@ export default {
/>
</template>
</activity-events-list>
+
+ <abuse-report-notes
+ v-if="glFeatures.abuseReportNotes"
+ :abuse-report-id="abuseReport.report.globalId"
+ />
</section>
</template>
diff --git a/app/assets/javascripts/admin/abuse_report/components/abuse_report_notes.vue b/app/assets/javascripts/admin/abuse_report/components/abuse_report_notes.vue
new file mode 100644
index 00000000000..80af7d7400a
--- /dev/null
+++ b/app/assets/javascripts/admin/abuse_report/components/abuse_report_notes.vue
@@ -0,0 +1,92 @@
+<script>
+import { uniqueId } from 'lodash';
+import { __ } from '~/locale';
+import { createAlert } from '~/alert';
+import SkeletonLoadingContainer from '~/vue_shared/components/notes/skeleton_note.vue';
+import { SKELETON_NOTES_COUNT } from '~/admin/abuse_report/constants';
+import abuseReportNotesQuery from '../graphql/notes/abuse_report_notes.query.graphql';
+import AbuseReportDiscussion from './notes/abuse_report_discussion.vue';
+
+export default {
+ name: 'AbuseReportNotes',
+ SKELETON_NOTES_COUNT,
+ i18n: {
+ fetchError: __('An error occurred while fetching comments, please try again.'),
+ },
+ components: {
+ SkeletonLoadingContainer,
+ AbuseReportDiscussion,
+ },
+ props: {
+ abuseReportId: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ addNoteKey: uniqueId(`abuse-report-add-note-${this.abuseReportId}`),
+ };
+ },
+ apollo: {
+ abuseReportNotes: {
+ query: abuseReportNotesQuery,
+ variables() {
+ return {
+ id: this.abuseReportId,
+ };
+ },
+ update(data) {
+ return data.abuseReport?.discussions || [];
+ },
+ skip() {
+ return !this.abuseReportId;
+ },
+ error() {
+ createAlert({ message: this.$options.i18n.fetchError });
+ },
+ },
+ },
+ computed: {
+ initialLoading() {
+ return this.$apollo.queries.abuseReportNotes.loading;
+ },
+ notesArray() {
+ return this.abuseReportNotes?.nodes || [];
+ },
+ },
+ methods: {
+ getDiscussionKey(discussion) {
+ const discussionId = discussion.notes.nodes[0].id;
+ return discussionId.split('/')[discussionId.split('/').length - 1];
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div class="issuable-discussion gl-mb-5 gl-clearfix!">
+ <template v-if="initialLoading">
+ <ul class="notes main-notes-list timeline">
+ <skeleton-loading-container
+ v-for="index in $options.SKELETON_NOTES_COUNT"
+ :key="index"
+ class="note-skeleton"
+ />
+ </ul>
+ </template>
+
+ <template v-else>
+ <ul class="notes main-notes-list timeline">
+ <abuse-report-discussion
+ v-for="discussion in notesArray"
+ :key="getDiscussionKey(discussion)"
+ :discussion="discussion.notes.nodes"
+ :abuse-report-id="abuseReportId"
+ />
+ </ul>
+ </template>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/admin/abuse_report/components/activity_events_list.vue b/app/assets/javascripts/admin/abuse_report/components/activity_events_list.vue
index 8c4c1da28b8..2206e600543 100644
--- a/app/assets/javascripts/admin/abuse_report/components/activity_events_list.vue
+++ b/app/assets/javascripts/admin/abuse_report/components/activity_events_list.vue
@@ -11,7 +11,7 @@ export default {
<!-- The styles `issuable-discussion`, `timeline`, `main-notes-list` and `notes` used below
are declared in app/assets/stylesheets/pages/notes.scss -->
<section class="gl-pt-6 issuable-discussion">
- <h2 class="gl-font-lg gl-mt-0 gl-mb-2">{{ $options.i18n.activity }}</h2>
+ <h2 class="gl-font-size-h1 gl-mt-0 gl-mb-4">{{ $options.i18n.activity }}</h2>
<ul class="timeline main-notes-list notes">
<slot name="history-items"></slot>
</ul>
diff --git a/app/assets/javascripts/admin/abuse_report/components/graphql/abuse_report.query.graphql b/app/assets/javascripts/admin/abuse_report/components/graphql/abuse_report.query.graphql
deleted file mode 100644
index f5b075cb9af..00000000000
--- a/app/assets/javascripts/admin/abuse_report/components/graphql/abuse_report.query.graphql
+++ /dev/null
@@ -1,13 +0,0 @@
-query abuseReportQuery($id: AbuseReportID!) {
- abuseReport(id: $id) {
- labels {
- nodes {
- id
- title
- description
- color
- textColor
- }
- }
- }
-}
diff --git a/app/assets/javascripts/admin/abuse_report/components/labels_select.vue b/app/assets/javascripts/admin/abuse_report/components/labels_select.vue
index 747c9a1a947..d2d143f0460 100644
--- a/app/assets/javascripts/admin/abuse_report/components/labels_select.vue
+++ b/app/assets/javascripts/admin/abuse_report/components/labels_select.vue
@@ -11,7 +11,7 @@ import DropdownContentsCreateView from '~/sidebar/components/labels/labels_selec
import DropdownHeader from '~/sidebar/components/labels/labels_select_widget/dropdown_header.vue';
import DropdownFooter from '~/sidebar/components/labels/labels_select_widget/dropdown_footer.vue';
import DropdownWidget from '~/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue';
-import abuseReportLabelsQuery from './graphql/abuse_report_labels.query.graphql';
+import abuseReportLabelsQuery from '../graphql/abuse_report_labels.query.graphql';
export default {
components: {
diff --git a/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_discussion.vue b/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_discussion.vue
new file mode 100644
index 00000000000..4d24471fa43
--- /dev/null
+++ b/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_discussion.vue
@@ -0,0 +1,104 @@
+<script>
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
+import DiscussionNotesRepliesWrapper from '~/notes/components/discussion_notes_replies_wrapper.vue';
+import ToggleRepliesWidget from '~/notes/components/toggle_replies_widget.vue';
+import AbuseReportNote from './abuse_report_note.vue';
+
+export default {
+ name: 'AbuseReportDiscussion',
+ components: {
+ TimelineEntryItem,
+ DiscussionNotesRepliesWrapper,
+ ToggleRepliesWidget,
+ AbuseReportNote,
+ },
+ props: {
+ abuseReportId: {
+ type: String,
+ required: true,
+ },
+ discussion: {
+ type: Array,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ isExpanded: true,
+ };
+ },
+ computed: {
+ note() {
+ return this.discussion[0];
+ },
+ noteId() {
+ return getIdFromGraphQLId(this.note.id);
+ },
+ replies() {
+ if (this.discussion?.length > 1) {
+ return this.discussion.slice(1);
+ }
+ return null;
+ },
+ hasReplies() {
+ return Boolean(this.replies?.length);
+ },
+ discussionId() {
+ return this.discussion[0]?.discussion?.id || '';
+ },
+ },
+ methods: {
+ toggleDiscussion() {
+ this.isExpanded = !this.isExpanded;
+ },
+ },
+};
+</script>
+
+<template>
+ <abuse-report-note
+ v-if="!hasReplies"
+ :note="note"
+ :abuse-report-id="abuseReportId"
+ class="gl-mb-4"
+ />
+ <timeline-entry-item v-else :data-note-id="noteId" class="note note-discussion gl-px-0">
+ <div class="timeline-content">
+ <div class="discussion">
+ <div class="discussion-body">
+ <div class="discussion-wrapper">
+ <div class="discussion-notes">
+ <ul class="notes">
+ <abuse-report-note
+ :note="note"
+ :discussion-id="discussionId"
+ :abuse-report-id="abuseReportId"
+ class="gl-mb-4"
+ />
+ <discussion-notes-replies-wrapper>
+ <toggle-replies-widget
+ v-if="hasReplies"
+ :collapsed="!isExpanded"
+ :replies="replies"
+ @toggle="toggleDiscussion({ discussionId })"
+ />
+ <template v-if="isExpanded">
+ <template v-for="reply in replies">
+ <abuse-report-note
+ :key="reply.id"
+ :discussion-id="discussionId"
+ :note="reply"
+ :abuse-report-id="abuseReportId"
+ />
+ </template>
+ </template>
+ </discussion-notes-replies-wrapper>
+ </ul>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </timeline-entry-item>
+</template>
diff --git a/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_note.vue b/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_note.vue
new file mode 100644
index 00000000000..6da3017e11e
--- /dev/null
+++ b/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_note.vue
@@ -0,0 +1,81 @@
+<script>
+import { GlAvatarLink, GlAvatar } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
+import NoteHeader from '~/notes/components/note_header.vue';
+import NoteBody from './abuse_report_note_body.vue';
+
+export default {
+ name: 'AbuseReportNote',
+ directives: {
+ SafeHtml,
+ },
+ components: {
+ GlAvatarLink,
+ GlAvatar,
+ TimelineEntryItem,
+ NoteHeader,
+ NoteBody,
+ },
+ props: {
+ abuseReportId: {
+ type: String,
+ required: true,
+ },
+ note: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ noteAnchorId() {
+ return `note_${getIdFromGraphQLId(this.note.id)}`;
+ },
+ author() {
+ return this.note.author;
+ },
+ authorId() {
+ return getIdFromGraphQLId(this.author.id);
+ },
+ },
+};
+</script>
+
+<template>
+ <timeline-entry-item :id="noteAnchorId" class="note note-wrapper note-comment">
+ <div :key="note.id" class="timeline-avatar gl-float-left">
+ <gl-avatar-link
+ :href="author.webUrl"
+ :data-user-id="authorId"
+ :data-username="author.username"
+ class="js-user-link"
+ >
+ <gl-avatar
+ :src="author.avatarUrl"
+ :entity-name="author.username"
+ :alt="author.name"
+ :size="32"
+ />
+ </gl-avatar-link>
+ </div>
+ <div class="timeline-content">
+ <div data-testid="note-wrapper">
+ <div class="note-header">
+ <note-header
+ :author="author"
+ :created-at="note.createdAt"
+ :note-id="note.id"
+ :note-url="note.url"
+ >
+ <span v-if="note.createdAt" class="d-none d-sm-inline">&middot;</span>
+ </note-header>
+ </div>
+
+ <div class="timeline-discussion-body">
+ <note-body ref="noteBody" :note="note" />
+ </div>
+ </div>
+ </div>
+ </timeline-entry-item>
+</template>
diff --git a/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_note_body.vue b/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_note_body.vue
new file mode 100644
index 00000000000..ab3d7f5fa6c
--- /dev/null
+++ b/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_note_body.vue
@@ -0,0 +1,48 @@
+<script>
+import SafeHtml from '~/vue_shared/directives/safe_html';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
+
+export default {
+ name: 'AbuseReportNoteBody',
+ directives: {
+ SafeHtml,
+ },
+ props: {
+ note: {
+ type: Object,
+ required: true,
+ },
+ },
+ watch: {
+ 'note.bodyHtml': {
+ immediate: true,
+ async handler(newVal, oldVal) {
+ if (newVal === oldVal) {
+ return;
+ }
+ await this.$nextTick();
+ this.renderGFM();
+ },
+ },
+ },
+ methods: {
+ renderGFM() {
+ renderGFM(this.$refs['note-body']);
+ gl?.lazyLoader?.searchLazyImages();
+ },
+ },
+ safeHtmlConfig: {
+ ADD_TAGS: ['use', 'gl-emoji', 'copy-code'],
+ },
+};
+</script>
+
+<template>
+ <div ref="note-body" class="note-body">
+ <div
+ v-safe-html:[$options.safeHtmlConfig]="note.bodyHtml"
+ class="note-text md"
+ data-testid="abuse-report-note-body"
+ ></div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/admin/abuse_report/components/report_details.vue b/app/assets/javascripts/admin/abuse_report/components/report_details.vue
index 10e1dca7f91..89017e6cbd4 100644
--- a/app/assets/javascripts/admin/abuse_report/components/report_details.vue
+++ b/app/assets/javascripts/admin/abuse_report/components/report_details.vue
@@ -1,8 +1,8 @@
<script>
import { __ } from '~/locale';
import { createAlert } from '~/alert';
+import abuseReportQuery from '../graphql/abuse_report.query.graphql';
import LabelsSelect from './labels_select.vue';
-import abuseReportQuery from './graphql/abuse_report.query.graphql';
export default {
name: 'ReportDetails',
diff --git a/app/assets/javascripts/admin/abuse_report/components/reported_content.vue b/app/assets/javascripts/admin/abuse_report/components/reported_content.vue
index 84d6f25ac05..99c8b3ece10 100644
--- a/app/assets/javascripts/admin/abuse_report/components/reported_content.vue
+++ b/app/assets/javascripts/admin/abuse_report/components/reported_content.vue
@@ -67,7 +67,7 @@ export default {
<div
class="gl-pb-3 gl-display-flex gl-justify-content-space-between gl-xs-flex-direction-column gl-align-items-center"
>
- <h2 class="gl-font-lg gl-mt-2 gl-mb-2">
+ <h2 class="gl-font-size-h1 gl-mt-2 gl-mb-2">
{{ $options.i18n.reportTypes[reportType] }}
</h2>
@@ -128,7 +128,7 @@ export default {
</gl-link>
<time-ago-tooltip
:time="report.reportedAt"
- class="gl-ml-3 gl-text-secondary gl-xs-w-full"
+ class="gl-ml-3 gl-text-secondary gl-w-full gl-sm-w-auto"
/>
</div>
</div>
diff --git a/app/assets/javascripts/admin/abuse_report/constants.js b/app/assets/javascripts/admin/abuse_report/constants.js
index f028408bed7..c56ea678b1d 100644
--- a/app/assets/javascripts/admin/abuse_report/constants.js
+++ b/app/assets/javascripts/admin/abuse_report/constants.js
@@ -111,3 +111,5 @@ export const HISTORY_ITEMS_I18N = {
reportedByForCategory: s__('AbuseReport|Reported by %{name} for %{category}.'),
deletedReporter: s__('AbuseReport|No user found'),
};
+
+export const SKELETON_NOTES_COUNT = 5;
diff --git a/app/assets/javascripts/admin/abuse_report/graphql/abuse_report.query.graphql b/app/assets/javascripts/admin/abuse_report/graphql/abuse_report.query.graphql
new file mode 100644
index 00000000000..640eec718f8
--- /dev/null
+++ b/app/assets/javascripts/admin/abuse_report/graphql/abuse_report.query.graphql
@@ -0,0 +1,14 @@
+query abuseReportQuery($id: AbuseReportID!) {
+ abuseReport(id: $id) {
+ id
+ labels {
+ nodes {
+ id
+ title
+ description
+ color
+ textColor
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/admin/abuse_report/components/graphql/abuse_report_labels.query.graphql b/app/assets/javascripts/admin/abuse_report/graphql/abuse_report_labels.query.graphql
index 4e724b4db2c..4e724b4db2c 100644
--- a/app/assets/javascripts/admin/abuse_report/components/graphql/abuse_report_labels.query.graphql
+++ b/app/assets/javascripts/admin/abuse_report/graphql/abuse_report_labels.query.graphql
diff --git a/app/assets/javascripts/admin/abuse_report/components/graphql/create_abuse_report_label.mutation.graphql b/app/assets/javascripts/admin/abuse_report/graphql/create_abuse_report_label.mutation.graphql
index 0781b8e634b..0781b8e634b 100644
--- a/app/assets/javascripts/admin/abuse_report/components/graphql/create_abuse_report_label.mutation.graphql
+++ b/app/assets/javascripts/admin/abuse_report/graphql/create_abuse_report_label.mutation.graphql
diff --git a/app/assets/javascripts/admin/abuse_report/graphql/notes/abuse_report_note.fragment.graphql b/app/assets/javascripts/admin/abuse_report/graphql/notes/abuse_report_note.fragment.graphql
new file mode 100644
index 00000000000..84b57b4ed79
--- /dev/null
+++ b/app/assets/javascripts/admin/abuse_report/graphql/notes/abuse_report_note.fragment.graphql
@@ -0,0 +1,30 @@
+#import "~/graphql_shared/fragments/author.fragment.graphql"
+#import "./abuse_report_note_permissions.fragment.graphql"
+
+fragment AbuseReportNote on Note {
+ id
+ body
+ bodyHtml
+ createdAt
+ lastEditedAt
+ url
+ resolved
+ author {
+ ...Author
+ }
+ lastEditedBy {
+ ...Author
+ webPath
+ }
+ userPermissions {
+ ...AbuseReportNotePermissions
+ }
+ discussion {
+ id
+ notes {
+ nodes {
+ id
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/admin/abuse_report/graphql/notes/abuse_report_note_permissions.fragment.graphql b/app/assets/javascripts/admin/abuse_report/graphql/notes/abuse_report_note_permissions.fragment.graphql
new file mode 100644
index 00000000000..01436436b93
--- /dev/null
+++ b/app/assets/javascripts/admin/abuse_report/graphql/notes/abuse_report_note_permissions.fragment.graphql
@@ -0,0 +1,3 @@
+fragment AbuseReportNotePermissions on NotePermissions {
+ adminNote
+}
diff --git a/app/assets/javascripts/admin/abuse_report/graphql/notes/abuse_report_notes.query.graphql b/app/assets/javascripts/admin/abuse_report/graphql/notes/abuse_report_notes.query.graphql
new file mode 100644
index 00000000000..3a13ac1f37a
--- /dev/null
+++ b/app/assets/javascripts/admin/abuse_report/graphql/notes/abuse_report_notes.query.graphql
@@ -0,0 +1,18 @@
+#import "./abuse_report_note.fragment.graphql"
+
+query abuseReportNotes($id: AbuseReportID!) {
+ abuseReport(id: $id) {
+ id
+ discussions {
+ nodes {
+ id
+ replyId
+ notes {
+ nodes {
+ ...AbuseReportNote
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/admin/abuse_report/graphql/notes/create_abuse_report_note.mutation.graphql b/app/assets/javascripts/admin/abuse_report/graphql/notes/create_abuse_report_note.mutation.graphql
new file mode 100644
index 00000000000..53ac9468e08
--- /dev/null
+++ b/app/assets/javascripts/admin/abuse_report/graphql/notes/create_abuse_report_note.mutation.graphql
@@ -0,0 +1,18 @@
+#import "./abuse_report_note.fragment.graphql"
+
+mutation createAbuseReportNote($input: CreateNoteInput!) {
+ createNote(input: $input) {
+ note {
+ id
+ discussion {
+ id
+ notes {
+ nodes {
+ ...AbuseReportNote
+ }
+ }
+ }
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/admin/abuse_report/graphql/notes/delete_abuse_report_note.fragment.graphql b/app/assets/javascripts/admin/abuse_report/graphql/notes/delete_abuse_report_note.fragment.graphql
new file mode 100644
index 00000000000..e8ff2933159
--- /dev/null
+++ b/app/assets/javascripts/admin/abuse_report/graphql/notes/delete_abuse_report_note.fragment.graphql
@@ -0,0 +1,8 @@
+mutation deleteAbuseReportNote($input: DestroyNoteInput!) {
+ destroyNote(input: $input) {
+ errors
+ note {
+ id
+ }
+ }
+}
diff --git a/app/assets/javascripts/admin/abuse_report/graphql/notes/update_abuse_report_note.mutation.graphql b/app/assets/javascripts/admin/abuse_report/graphql/notes/update_abuse_report_note.mutation.graphql
new file mode 100644
index 00000000000..e11165074c9
--- /dev/null
+++ b/app/assets/javascripts/admin/abuse_report/graphql/notes/update_abuse_report_note.mutation.graphql
@@ -0,0 +1,10 @@
+#import "./abuse_report_note.fragment.graphql"
+
+mutation updateAbuseReportNote($input: UpdateNoteInput!) {
+ updateNote(input: $input) {
+ note {
+ ...AbuseReportNote
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/admin/background_migrations/index.js b/app/assets/javascripts/admin/background_migrations/index.js
index 4ddd8f17c9a..890df17080d 100644
--- a/app/assets/javascripts/admin/background_migrations/index.js
+++ b/app/assets/javascripts/admin/background_migrations/index.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import Translate from '~/vue_shared/translate';
import BackgroundMigrationsDatabaseListbox from './components/database_listbox.vue';
diff --git a/app/assets/javascripts/admin/broadcast_messages/components/message_form.vue b/app/assets/javascripts/admin/broadcast_messages/components/message_form.vue
index 2c555aca3c0..753b1fb1819 100644
--- a/app/assets/javascripts/admin/broadcast_messages/components/message_form.vue
+++ b/app/assets/javascripts/admin/broadcast_messages/components/message_form.vue
@@ -107,7 +107,7 @@ export default {
targetSelected: '',
targetPath: this.broadcastMessage.targetPath,
targetAccessLevels: this.broadcastMessage.targetAccessLevels,
- targetAccessLevelOptions: this.targetAccessLevelOptions.map(([text, value]) => ({
+ targetAccessLevelCheckBoxGroupOptions: this.targetAccessLevelOptions.map(([text, value]) => ({
text,
value,
})),
@@ -324,7 +324,10 @@ export default {
:state="!isValidated || targetRolesValid"
data-testid="target-roles-checkboxes"
>
- <gl-form-checkbox-group v-model="targetAccessLevels" :options="targetAccessLevelOptions" />
+ <gl-form-checkbox-group
+ v-model="targetAccessLevels"
+ :options="targetAccessLevelCheckBoxGroupOptions"
+ />
</gl-form-group>
<gl-form-group
diff --git a/app/assets/javascripts/admin/users/components/actions/index.js b/app/assets/javascripts/admin/users/components/actions/index.js
index 4e63a85df89..633bc4d8b15 100644
--- a/app/assets/javascripts/admin/users/components/actions/index.js
+++ b/app/assets/javascripts/admin/users/components/actions/index.js
@@ -9,6 +9,8 @@ import Reject from './reject.vue';
import Unban from './unban.vue';
import Unblock from './unblock.vue';
import Unlock from './unlock.vue';
+import Trust from './trust_user.vue';
+import Untrust from './untrust_user.vue';
export default {
Activate,
@@ -22,4 +24,6 @@ export default {
Unblock,
Unlock,
Reject,
+ Trust,
+ Untrust,
};
diff --git a/app/assets/javascripts/admin/users/components/actions/trust_user.vue b/app/assets/javascripts/admin/users/components/actions/trust_user.vue
new file mode 100644
index 00000000000..41ff8d4120d
--- /dev/null
+++ b/app/assets/javascripts/admin/users/components/actions/trust_user.vue
@@ -0,0 +1,62 @@
+<script>
+import { GlDisclosureDropdownItem } from '@gitlab/ui';
+import { sprintf, s__, __ } from '~/locale';
+import eventHub, { EVENT_OPEN_CONFIRM_MODAL } from '~/vue_shared/components/confirm_modal_eventhub';
+import { I18N_USER_ACTIONS } from '../../constants';
+
+// TODO: To be replaced with <template> content in https://gitlab.com/gitlab-org/gitlab/-/issues/320922
+const messageHtml = `
+ <p>${s__('AdminUsers|When not being monitored for spam:')}</p>
+ <ul>
+ <li>${s__(
+ 'AdminUsers|The user can create issues, notes, snippets, and merge requests that appear to be spam without being blocked.',
+ )}</li>
+ </ul>
+ <p>${s__('AdminUsers|You can untrust this user in the future.')}</p>
+`;
+
+export default {
+ components: {
+ GlDisclosureDropdownItem,
+ },
+ props: {
+ username: {
+ type: String,
+ required: true,
+ },
+ path: {
+ type: String,
+ required: true,
+ },
+ },
+ methods: {
+ onClick() {
+ eventHub.$emit(EVENT_OPEN_CONFIRM_MODAL, {
+ path: this.path,
+ method: 'put',
+ modalAttributes: {
+ title: sprintf(s__('AdminUsers|Stop monitoring %{username} for possible spam?'), {
+ username: this.username,
+ }),
+ actionCancel: {
+ text: __('Cancel'),
+ },
+ actionPrimary: {
+ text: I18N_USER_ACTIONS.trust,
+ attributes: { variant: 'confirm' },
+ },
+ messageHtml,
+ },
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-disclosure-dropdown-item @action="onClick">
+ <template #list-item>
+ <slot></slot>
+ </template>
+ </gl-disclosure-dropdown-item>
+</template>
diff --git a/app/assets/javascripts/admin/users/components/actions/untrust_user.vue b/app/assets/javascripts/admin/users/components/actions/untrust_user.vue
new file mode 100644
index 00000000000..da59833af07
--- /dev/null
+++ b/app/assets/javascripts/admin/users/components/actions/untrust_user.vue
@@ -0,0 +1,56 @@
+<script>
+import { GlDisclosureDropdownItem } from '@gitlab/ui';
+import { sprintf, s__, __ } from '~/locale';
+import eventHub, { EVENT_OPEN_CONFIRM_MODAL } from '~/vue_shared/components/confirm_modal_eventhub';
+import { I18N_USER_ACTIONS } from '../../constants';
+
+// TODO: To be replaced with <template> content in https://gitlab.com/gitlab-org/gitlab/-/issues/320922
+const messageHtml = `<p>${s__(
+ 'AdminUsers|You can trust this user in the future if necessary.',
+)}</p>`;
+
+export default {
+ components: {
+ GlDisclosureDropdownItem,
+ },
+ props: {
+ username: {
+ type: String,
+ required: true,
+ },
+ path: {
+ type: String,
+ required: true,
+ },
+ },
+ methods: {
+ onClick() {
+ eventHub.$emit(EVENT_OPEN_CONFIRM_MODAL, {
+ path: this.path,
+ method: 'put',
+ modalAttributes: {
+ title: sprintf(s__('AdminUsers|Re-enable spam monitoring for %{username}?'), {
+ username: this.username,
+ }),
+ actionCancel: {
+ text: __('Cancel'),
+ },
+ actionPrimary: {
+ text: I18N_USER_ACTIONS.untrust,
+ attributes: { variant: 'confirm' },
+ },
+ messageHtml,
+ },
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-disclosure-dropdown-item @action="onClick">
+ <template #list-item>
+ <slot></slot>
+ </template>
+ </gl-disclosure-dropdown-item>
+</template>
diff --git a/app/assets/javascripts/admin/users/components/app.vue b/app/assets/javascripts/admin/users/components/app.vue
index a3abd904a6b..b0caffb6ca6 100644
--- a/app/assets/javascripts/admin/users/components/app.vue
+++ b/app/assets/javascripts/admin/users/components/app.vue
@@ -1,9 +1,15 @@
<script>
-import UsersTable from './users_table.vue';
+import { createAlert } from '~/alert';
+import { s__ } from '~/locale';
+import { convertNodeIdsFromGraphQLIds } from '~/graphql_shared/utils';
+import UsersTable from '~/vue_shared/components/users_table/users_table.vue';
+import getUsersGroupCountsQuery from '../graphql/queries/get_users_group_counts.query.graphql';
+import UserActions from './user_actions.vue';
export default {
components: {
UsersTable,
+ UserActions,
},
props: {
users: {
@@ -16,11 +22,64 @@ export default {
required: true,
},
},
+ data() {
+ return {
+ groupCounts: {},
+ };
+ },
+ apollo: {
+ groupCounts: {
+ query: getUsersGroupCountsQuery,
+ variables() {
+ return {
+ usernames: this.users.map((user) => user.username),
+ };
+ },
+ update(data) {
+ const nodes = data?.users?.nodes || [];
+ const parsedIds = convertNodeIdsFromGraphQLIds(nodes);
+
+ return parsedIds.reduce((acc, { id, groupCount }) => {
+ acc[id] = groupCount || 0;
+ return acc;
+ }, {});
+ },
+ error(error) {
+ createAlert({
+ message: this.$options.i18n.groupCountFetchError,
+ captureError: true,
+ error,
+ });
+ },
+ skip() {
+ return !this.users.length;
+ },
+ },
+ },
+ computed: {
+ groupCountsLoading() {
+ return this.$apollo.queries.groupCounts.loading;
+ },
+ },
+ i18n: {
+ groupCountFetchError: s__(
+ 'AdminUsers|Could not load user group counts. Please refresh the page to try again.',
+ ),
+ },
};
</script>
<template>
<div>
- <users-table :users="users" :paths="paths" />
+ <users-table
+ :users="users"
+ :admin-user-path="paths.adminUser"
+ :group-counts="groupCounts"
+ :group-counts-loading="groupCountsLoading"
+ >
+ <template #user-actions="{ user }">
+ <user-actions :user="user" :paths="paths" :show-button-labels="true" />
+ </template>
+ </users-table>
</div>
</template>
diff --git a/app/assets/javascripts/admin/users/components/user_avatar.vue b/app/assets/javascripts/admin/users/components/user_avatar.vue
deleted file mode 100644
index dd354794cf3..00000000000
--- a/app/assets/javascripts/admin/users/components/user_avatar.vue
+++ /dev/null
@@ -1,67 +0,0 @@
-<script>
-import { GlAvatarLabeled, GlBadge, GlIcon, GlTooltipDirective } from '@gitlab/ui';
-import { truncate } from '~/lib/utils/text_utility';
-import { USER_AVATAR_SIZE, LENGTH_OF_USER_NOTE_TOOLTIP } from '../constants';
-
-export default {
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- components: {
- GlAvatarLabeled,
- GlBadge,
- GlIcon,
- },
- props: {
- user: {
- type: Object,
- required: true,
- },
- adminUserPath: {
- type: String,
- required: true,
- },
- },
- computed: {
- adminUserHref() {
- return this.adminUserPath.replace('id', this.user.username);
- },
- adminUserMailto() {
- return `mailto:${this.user.email}`;
- },
- userNoteShort() {
- return truncate(this.user.note, LENGTH_OF_USER_NOTE_TOOLTIP);
- },
- },
- USER_AVATAR_SIZE,
-};
-</script>
-
-<template>
- <div
- v-if="user"
- class="js-user-link gl-display-inline-block"
- :data-user-id="user.id"
- :data-username="user.username"
- >
- <gl-avatar-labeled
- :size="$options.USER_AVATAR_SIZE"
- :src="user.avatarUrl"
- :label="user.name"
- :sub-label="user.email"
- :label-link="adminUserHref"
- :sub-label-link="adminUserMailto"
- >
- <template #meta>
- <div v-if="user.note" class="gl-text-gray-500 gl-p-1">
- <gl-icon v-gl-tooltip="userNoteShort" name="document" />
- </div>
- <div v-for="(badge, idx) in user.badges" :key="idx" class="gl-p-1">
- <gl-badge class="gl-display-flex!" size="sm" :variant="badge.variant">{{
- badge.text
- }}</gl-badge>
- </div>
- </template>
- </gl-avatar-labeled>
- </div>
-</template>
diff --git a/app/assets/javascripts/admin/users/components/users_table.vue b/app/assets/javascripts/admin/users/components/users_table.vue
deleted file mode 100644
index 65737be1e67..00000000000
--- a/app/assets/javascripts/admin/users/components/users_table.vue
+++ /dev/null
@@ -1,142 +0,0 @@
-<script>
-import { GlSkeletonLoader, GlTable } from '@gitlab/ui';
-import { createAlert } from '~/alert';
-import { convertNodeIdsFromGraphQLIds } from '~/graphql_shared/utils';
-import { thWidthPercent } from '~/lib/utils/table_utility';
-import { s__, __ } from '~/locale';
-import UserDate from '~/vue_shared/components/user_date.vue';
-import getUsersGroupCountsQuery from '../graphql/queries/get_users_group_counts.query.graphql';
-import UserActions from './user_actions.vue';
-import UserAvatar from './user_avatar.vue';
-
-export default {
- components: {
- GlSkeletonLoader,
- GlTable,
- UserAvatar,
- UserActions,
- UserDate,
- },
- props: {
- users: {
- type: Array,
- required: true,
- },
- paths: {
- type: Object,
- required: true,
- },
- },
- data() {
- return {
- groupCounts: [],
- };
- },
- apollo: {
- groupCounts: {
- query: getUsersGroupCountsQuery,
- variables() {
- return {
- usernames: this.users.map((user) => user.username),
- };
- },
- update(data) {
- const nodes = data?.users?.nodes || [];
- const parsedIds = convertNodeIdsFromGraphQLIds(nodes);
-
- return parsedIds.reduce((acc, { id, groupCount }) => {
- acc[id] = groupCount || 0;
- return acc;
- }, {});
- },
- error(error) {
- createAlert({
- message: this.$options.i18n.groupCountFetchError,
- captureError: true,
- error,
- });
- },
- skip() {
- return !this.users.length;
- },
- },
- },
- i18n: {
- groupCountFetchError: s__(
- 'AdminUsers|Could not load user group counts. Please refresh the page to try again.',
- ),
- },
- fields: [
- {
- key: 'name',
- label: __('Name'),
- thClass: thWidthPercent(40),
- },
- {
- key: 'projectsCount',
- label: __('Projects'),
- thClass: thWidthPercent(10),
- },
- {
- key: 'groupCount',
- label: __('Groups'),
- thClass: thWidthPercent(10),
- },
- {
- key: 'createdAt',
- label: __('Created on'),
- thClass: thWidthPercent(15),
- },
- {
- key: 'lastActivityOn',
- label: __('Last activity'),
- thClass: thWidthPercent(15),
- },
- {
- key: 'settings',
- label: '',
- thClass: thWidthPercent(10),
- },
- ],
-};
-</script>
-
-<template>
- <div>
- <gl-table
- :items="users"
- :fields="$options.fields"
- :empty-text="s__('AdminUsers|No users found')"
- show-empty
- stacked="md"
- :tbody-tr-attr="{ 'data-testid': 'user-row-content' }"
- >
- <template #cell(name)="{ item: user }">
- <user-avatar :user="user" :admin-user-path="paths.adminUser" />
- </template>
-
- <template #cell(createdAt)="{ item: { createdAt } }">
- <user-date :date="createdAt" />
- </template>
-
- <template #cell(lastActivityOn)="{ item: { lastActivityOn } }">
- <user-date :date="lastActivityOn" show-never />
- </template>
-
- <template #cell(groupCount)="{ item: { id } }">
- <div :data-testid="`user-group-count-${id}`">
- <gl-skeleton-loader v-if="$apollo.loading" :width="40" :lines="1" />
- <span v-else>{{ groupCounts[id] }}</span>
- </div>
- </template>
-
- <template #cell(projectsCount)="{ item: { id, projectsCount } }">
- <div :data-testid="`user-project-count-${id}`">{{ projectsCount }}</div>
- </template>
-
- <template #cell(settings)="{ item: user }">
- <user-actions :user="user" :paths="paths" :show-button-labels="true" />
- </template>
- </gl-table>
- </div>
-</template>
diff --git a/app/assets/javascripts/admin/users/constants.js b/app/assets/javascripts/admin/users/constants.js
index 9cd61d6b1db..73383623aa2 100644
--- a/app/assets/javascripts/admin/users/constants.js
+++ b/app/assets/javascripts/admin/users/constants.js
@@ -1,9 +1,5 @@
import { s__, __ } from '~/locale';
-export const USER_AVATAR_SIZE = 32;
-
-export const LENGTH_OF_USER_NOTE_TOOLTIP = 100;
-
export const I18N_USER_ACTIONS = {
edit: __('Edit'),
userAdministration: s__('AdminUsers|User administration'),
@@ -19,4 +15,6 @@ export const I18N_USER_ACTIONS = {
deleteWithContributions: s__('AdminUsers|Delete user and contributions'),
ban: s__('AdminUsers|Ban user'),
unban: s__('AdminUsers|Unban user'),
+ trust: s__('AdminUsers|Trust user'),
+ untrust: s__('AdminUsers|Untrust user'),
};
diff --git a/app/assets/javascripts/alert.js b/app/assets/javascripts/alert.js
index 4d724b17723..fd20d216385 100644
--- a/app/assets/javascripts/alert.js
+++ b/app/assets/javascripts/alert.js
@@ -1,7 +1,7 @@
-import * as Sentry from '@sentry/browser';
import Vue from 'vue';
import isEmpty from 'lodash/isEmpty';
import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { __ } from '~/locale';
export const VARIANT_SUCCESS = 'success';
diff --git a/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue b/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue
index fb872243e5e..29156a624fd 100644
--- a/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue
+++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue
@@ -13,8 +13,8 @@ import {
GlTabs,
GlTab,
} from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
import { isEqual, isEmpty, omit } from 'lodash';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import { PROMO_URL, DOCS_URL_IN_EE_DIR } from 'jh_else_ce/lib/utils/url_utility';
import {
diff --git a/app/assets/javascripts/analytics/cycle_analytics/store/mutations.js b/app/assets/javascripts/analytics/cycle_analytics/store/mutations.js
index 4fa88279fe0..d1c8d2c24e7 100644
--- a/app/assets/javascripts/analytics/cycle_analytics/store/mutations.js
+++ b/app/assets/javascripts/analytics/cycle_analytics/store/mutations.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { formatMedianValues } from '../utils';
-import { PAGINATION_SORT_FIELD_END_EVENT, PAGINATION_SORT_DIRECTION_DESC } from '../constants';
+import { PAGINATION_SORT_DIRECTION_DESC, PAGINATION_SORT_FIELD_DURATION } from '../constants';
import * as types from './mutation_types';
export default {
@@ -41,7 +41,7 @@ export default {
Vue.set(state, 'pagination', {
page,
hasNextPage,
- sort: sort || PAGINATION_SORT_FIELD_END_EVENT,
+ sort: sort || PAGINATION_SORT_FIELD_DURATION,
direction: direction || PAGINATION_SORT_DIRECTION_DESC,
});
},
diff --git a/app/assets/javascripts/analytics/cycle_analytics/store/state.js b/app/assets/javascripts/analytics/cycle_analytics/store/state.js
index 3d9b56b043d..f387bf65093 100644
--- a/app/assets/javascripts/analytics/cycle_analytics/store/state.js
+++ b/app/assets/javascripts/analytics/cycle_analytics/store/state.js
@@ -1,5 +1,5 @@
import {
- PAGINATION_SORT_FIELD_END_EVENT,
+ PAGINATION_SORT_FIELD_DURATION,
PAGINATION_SORT_DIRECTION_DESC,
} from '~/analytics/cycle_analytics/constants';
@@ -29,7 +29,7 @@ export default () => ({
pagination: {
page: null,
hasNextPage: false,
- sort: PAGINATION_SORT_FIELD_END_EVENT,
+ sort: PAGINATION_SORT_FIELD_DURATION,
direction: PAGINATION_SORT_DIRECTION_DESC,
},
predefinedDateRange: null,
diff --git a/app/assets/javascripts/analytics/product_analytics/activity_charts_bundle.js b/app/assets/javascripts/analytics/product_analytics/activity_charts_bundle.js
deleted file mode 100644
index 91cb48e181b..00000000000
--- a/app/assets/javascripts/analytics/product_analytics/activity_charts_bundle.js
+++ /dev/null
@@ -1,28 +0,0 @@
-import Vue from 'vue';
-import ActivityChart from './components/activity_chart.vue';
-
-export default () => {
- const containers = document.querySelectorAll('.js-project-analytics-chart');
-
- if (!containers) {
- return false;
- }
-
- return containers.forEach((container) => {
- const { chartData } = container.dataset;
- const formattedData = JSON.parse(chartData);
-
- return new Vue({
- el: container,
- components: {
- ActivityChart,
- },
- provide: {
- formattedData,
- },
- render(createElement) {
- return createElement('activity-chart');
- },
- });
- });
-};
diff --git a/app/assets/javascripts/analytics/product_analytics/components/activity_chart.vue b/app/assets/javascripts/analytics/product_analytics/components/activity_chart.vue
deleted file mode 100644
index 2be9ebda87a..00000000000
--- a/app/assets/javascripts/analytics/product_analytics/components/activity_chart.vue
+++ /dev/null
@@ -1,45 +0,0 @@
-<script>
-import { GlColumnChart } from '@gitlab/ui/dist/charts';
-import { s__ } from '~/locale';
-
-export default {
- i18n: {
- noDataMsg: s__(
- 'ProductAnalytics|There is no data for this type of chart currently. Please see the Setup tab if you have not configured the product analytics tool already.',
- ),
- },
- components: {
- GlColumnChart,
- },
- inject: {
- formattedData: {
- default: {},
- },
- },
- computed: {
- barSeriesData() {
- return [
- {
- name: 'full',
- data: this.formattedData.keys.map((val, idx) => [val, this.formattedData.values[idx]]),
- },
- ];
- },
- },
-};
-</script>
-
-<template>
- <div class="gl-xs-w-full">
- <gl-column-chart
- v-if="formattedData.keys"
- :bars="barSeriesData"
- :x-axis-title="__('Value')"
- :y-axis-title="__('Number of events')"
- :x-axis-type="'category'"
- />
- <p v-else data-testid="noActivityChartData">
- {{ $options.i18n.noDataMsg }}
- </p>
- </div>
-</template>
diff --git a/app/assets/javascripts/analytics/shared/components/metric_tile.vue b/app/assets/javascripts/analytics/shared/components/metric_tile.vue
index 54dbe329c7a..9e0262b5175 100644
--- a/app/assets/javascripts/analytics/shared/components/metric_tile.vue
+++ b/app/assets/javascripts/analytics/shared/components/metric_tile.vue
@@ -44,6 +44,7 @@ export default {
:animation-decimal-places="decimalPlaces"
:class="{ 'gl-hover-cursor-pointer': hasLinks }"
tabindex="0"
+ use-delimiters
@click="clickHandler(metric)"
/>
<metric-popover :metric="metric" :target="metric.identifier" />
diff --git a/app/assets/javascripts/analytics/usage_trends/components/usage_trends_count_chart.vue b/app/assets/javascripts/analytics/usage_trends/components/usage_trends_count_chart.vue
index 8d7761694d1..247c147609b 100644
--- a/app/assets/javascripts/analytics/usage_trends/components/usage_trends_count_chart.vue
+++ b/app/assets/javascripts/analytics/usage_trends/components/usage_trends_count_chart.vue
@@ -1,8 +1,8 @@
<script>
import { GlAlert } from '@gitlab/ui';
import { GlLineChart } from '@gitlab/ui/dist/charts';
-import * as Sentry from '@sentry/browser';
import { some, every } from 'lodash';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import {
differenceInMonths,
formatDateAsMonth,
diff --git a/app/assets/javascripts/analytics/usage_trends/components/users_chart.vue b/app/assets/javascripts/analytics/usage_trends/components/users_chart.vue
index 06b83c87985..47a34ec8b4d 100644
--- a/app/assets/javascripts/analytics/usage_trends/components/users_chart.vue
+++ b/app/assets/javascripts/analytics/usage_trends/components/users_chart.vue
@@ -1,9 +1,9 @@
<script>
import { GlAlert } from '@gitlab/ui';
import { GlAreaChart } from '@gitlab/ui/dist/charts';
-import * as Sentry from '@sentry/browser';
import produce from 'immer';
import { sortBy } from 'lodash';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { formatDateAsMonth } from '~/lib/utils/datetime_utility';
import { __ } from '~/locale';
import ChartSkeletonLoader from '~/vue_shared/components/resizable_chart/skeleton_loader.vue';
diff --git a/app/assets/javascripts/api/bulk_imports_api.js b/app/assets/javascripts/api/bulk_imports_api.js
index d636cfdff0b..248f5601705 100644
--- a/app/assets/javascripts/api/bulk_imports_api.js
+++ b/app/assets/javascripts/api/bulk_imports_api.js
@@ -2,6 +2,21 @@ import { buildApiUrl } from '~/api/api_utils';
import axios from '~/lib/utils/axios_utils';
const BULK_IMPORT_ENTITIES_PATH = '/api/:version/bulk_imports/entities';
+const BULK_IMPORT_ENTITIES_FAILURES_PATH =
+ '/api/:version/bulk_imports/:id/entities/:entity_id/failures';
export const getBulkImportsHistory = (params) =>
axios.get(buildApiUrl(BULK_IMPORT_ENTITIES_PATH), { params });
+
+export const getBulkImportFailures = (id, entityId, { page, perPage }) => {
+ const failuresPath = buildApiUrl(BULK_IMPORT_ENTITIES_FAILURES_PATH)
+ .replace(':id', encodeURIComponent(id))
+ .replace(':entity_id', encodeURIComponent(entityId));
+
+ return axios.get(failuresPath, {
+ params: {
+ page,
+ per_page: perPage,
+ },
+ });
+};
diff --git a/app/assets/javascripts/badges/components/badge_form.vue b/app/assets/javascripts/badges/components/badge_form.vue
index 2be59f00773..19da1253a17 100644
--- a/app/assets/javascripts/badges/components/badge_form.vue
+++ b/app/assets/javascripts/badges/components/badge_form.vue
@@ -277,11 +277,7 @@ export default {
>
{{ saveText }}
</gl-button>
- <gl-button
- :type="cancelButtonType"
- data-qa-selector="cancel_badge_button"
- @click="handleCancel"
- >
+ <gl-button :type="cancelButtonType" @click="handleCancel">
{{ __('Cancel') }}
</gl-button>
</div>
diff --git a/app/assets/javascripts/batch_comments/components/submit_dropdown.vue b/app/assets/javascripts/batch_comments/components/submit_dropdown.vue
index fac45f32464..b5cb1862b45 100644
--- a/app/assets/javascripts/batch_comments/components/submit_dropdown.vue
+++ b/app/assets/javascripts/batch_comments/components/submit_dropdown.vue
@@ -1,39 +1,69 @@
<script>
-import { GlDropdown, GlButton, GlIcon, GlForm, GlFormCheckbox } from '@gitlab/ui';
+import {
+ GlDisclosureDropdown,
+ GlButton,
+ GlIcon,
+ GlForm,
+ GlFormCheckbox,
+ GlFormRadioGroup,
+} from '@gitlab/ui';
// eslint-disable-next-line no-restricted-imports
import { mapGetters, mapActions, mapState } from 'vuex';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { __ } from '~/locale';
import { createAlert } from '~/alert';
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
import { scrollToElement } from '~/lib/utils/common_utils';
+import { fetchPolicies } from '~/lib/graphql';
import { CLEAR_AUTOSAVE_ENTRY_EVENT } from '~/vue_shared/constants';
import markdownEditorEventHub from '~/vue_shared/components/markdown/eventhub';
import { trackSavedUsingEditor } from '~/vue_shared/components/markdown/tracking';
+import userCanApproveQuery from '../queries/can_approve.query.graphql';
export default {
+ apollo: {
+ userPermissions: {
+ fetchPolicy: fetchPolicies.NETWORK_ONLY,
+ query: userCanApproveQuery,
+ variables() {
+ return {
+ projectPath: this.projectPath.replace(/^\//, ''),
+ iid: `${this.getNoteableData.iid}`,
+ };
+ },
+ update: (data) => data.project?.mergeRequest?.userPermissions,
+ skip() {
+ return !this.dropdownVisible;
+ },
+ },
+ },
components: {
- GlDropdown,
+ GlDisclosureDropdown,
GlButton,
GlIcon,
GlForm,
+ GlFormRadioGroup,
GlFormCheckbox,
MarkdownEditor,
ApprovalPassword: () => import('ee_component/batch_comments/components/approval_password.vue'),
SummarizeMyReview: () =>
import('ee_component/batch_comments/components/summarize_my_review.vue'),
},
+ mixins: [glFeatureFlagsMixin()],
inject: {
canSummarize: { default: false },
},
data() {
return {
isSubmitting: false,
+ dropdownVisible: false,
noteData: {
noteable_type: '',
noteable_id: '',
note: '',
approve: false,
approval_password: '',
+ reviewer_state: 'reviewed',
},
formFieldProps: {
id: 'review-note-body',
@@ -42,17 +72,51 @@ export default {
'aria-label': __('Comment'),
'data-testid': 'comment-textarea',
},
+ userPermissions: {},
};
},
computed: {
...mapGetters(['getNotesData', 'getNoteableData', 'noteableType', 'getCurrentUserLastNote']),
...mapState('batchComments', ['shouldAnimateReviewButton']),
+ ...mapState('diffs', ['projectPath']),
autocompleteDataSources() {
return gl.GfmAutoComplete?.dataSources;
},
autosaveKey() {
return `submit_review_dropdown/${this.getNoteableData.id}`;
},
+ radioGroupOptions() {
+ return [
+ {
+ html: [
+ __('Comment'),
+ `<p class="help-text">
+ ${__('Submit general feedback without explicit approval.')}
+ </p>`,
+ ].join('<br />'),
+ value: 'reviewed',
+ },
+ {
+ html: [
+ __('Approve'),
+ `<p class="help-text">
+ ${__('Submit feedback and approve these changes.')}
+ </p>`,
+ ].join('<br />'),
+ value: 'approved',
+ disabled: !this.userPermissions.canApprove,
+ },
+ {
+ html: [
+ __('Request changes'),
+ `<p class="help-text">
+ ${__('Submit feedback that should be addressed before merging.')}
+ </p>`,
+ ].join('<br />'),
+ value: 'requested_changes',
+ },
+ ];
+ },
},
watch: {
'noteData.approve': function noteDataApproveWatch() {
@@ -60,21 +124,21 @@ export default {
this.repositionDropdown();
});
},
+ dropdownVisible(val) {
+ if (!val) {
+ this.userPermissions = {};
+ }
+ },
+ userPermissions: {
+ handler() {
+ this.repositionDropdown();
+ },
+ deep: true,
+ },
},
mounted() {
this.noteData.noteable_type = this.noteableType;
this.noteData.noteable_id = this.getNoteableData.id;
-
- // We override the Bootstrap Vue click outside behaviour
- // to allow for clicking in the autocomplete dropdowns
- // without this override the submit dropdown will close
- // whenever a item in the autocomplete dropdown is clicked
- const originalClickOutHandler = this.$refs.submitDropdown.$refs.dropdown.clickOutHandler;
- this.$refs.submitDropdown.$refs.dropdown.clickOutHandler = (e) => {
- if (!e.target.closest('.atwho-container')) {
- originalClickOutHandler(e);
- }
- };
},
methods: {
...mapActions('batchComments', ['publishReview']),
@@ -113,86 +177,115 @@ export default {
updateNote(note) {
this.noteData.note = note;
},
+ onBeforeClose({ originalEvent: { target }, preventDefault }) {
+ if (
+ target &&
+ [document.querySelector('.atwho-container'), document.querySelector('.dz-hidden-input')]
+ .filter(Boolean)
+ .some((el) => el.contains(target))
+ ) {
+ preventDefault();
+ }
+ },
+ setDropdownVisible(val) {
+ this.dropdownVisible = val;
+ },
},
restrictedToolbarItems: ['full-screen'],
};
</script>
<template>
- <gl-dropdown
+ <gl-disclosure-dropdown
ref="submitDropdown"
- right
- dropup
+ placement="right"
class="submit-review-dropdown"
:class="{ 'submit-review-dropdown-animated': shouldAnimateReviewButton }"
data-testid="submit-review-dropdown"
- variant="info"
- category="primary"
+ fluid-width
+ @beforeClose="onBeforeClose"
+ @shown="setDropdownVisible(true)"
+ @hidden="setDropdownVisible(false)"
>
- <template #button-content>
- {{ __('Finish review') }}
- <gl-icon class="dropdown-chevron" name="chevron-up" />
+ <template #toggle>
+ <gl-button variant="info" category="primary">
+ {{ __('Finish review') }}
+ <gl-icon class="dropdown-chevron" name="chevron-up" />
+ </gl-button>
</template>
- <gl-form data-testid="submit-gl-form" @submit.prevent="submitReview">
- <div class="gl-display-flex gl-mb-4 gl-align-items-center">
- <label for="review-note-body" class="gl-mb-0">
- {{ __('Summary comment (optional)') }}
- </label>
- <summarize-my-review
- v-if="canSummarize"
- :id="getNoteableData.id"
- class="gl-ml-auto"
- @input="updateNote"
- />
- </div>
- <div class="common-note-form gfm-form">
- <markdown-editor
- ref="markdownEditor"
- v-model="noteData.note"
- class="js-no-autosize"
- :is-submitting="isSubmitting"
- :render-markdown-path="getNoteableData.preview_note_path"
- :markdown-docs-path="getNotesData.markdownDocsPath"
- :form-field-props="formFieldProps"
- enable-autocomplete
- :autocomplete-data-sources="autocompleteDataSources"
- :disabled="isSubmitting"
- :restricted-tool-bar-items="$options.restrictedToolbarItems"
- :force-autosize="false"
- :autosave-key="autosaveKey"
- supports-quick-actions
- @input="$emit('input', $event)"
- @keydown.meta.enter="submitReview"
- @keydown.ctrl.enter="submitReview"
- />
- </div>
- <template v-if="getNoteableData.current_user.can_approve">
- <gl-form-checkbox
- v-model="noteData.approve"
- data-testid="approve_merge_request"
+ <template #default>
+ <gl-form
+ class="submit-review-dropdown-form gl-p-4"
+ data-testid="submit-gl-form"
+ @submit.prevent="submitReview"
+ >
+ <div class="gl-display-flex gl-mb-4 gl-align-items-center">
+ <label for="review-note-body" class="gl-mb-0">
+ {{ __('Summary comment (optional)') }}
+ </label>
+ <summarize-my-review
+ v-if="canSummarize"
+ :id="getNoteableData.id"
+ class="gl-ml-auto"
+ @input="updateNote"
+ />
+ </div>
+ <div class="common-note-form gfm-form">
+ <markdown-editor
+ ref="markdownEditor"
+ v-model="noteData.note"
+ class="js-no-autosize"
+ :is-submitting="isSubmitting"
+ :render-markdown-path="getNoteableData.preview_note_path"
+ :markdown-docs-path="getNotesData.markdownDocsPath"
+ :form-field-props="formFieldProps"
+ enable-autocomplete
+ :autocomplete-data-sources="autocompleteDataSources"
+ :disabled="isSubmitting"
+ :restricted-tool-bar-items="$options.restrictedToolbarItems"
+ :force-autosize="false"
+ :autosave-key="autosaveKey"
+ supports-quick-actions
+ @input="$emit('input', $event)"
+ @keydown.meta.enter="submitReview"
+ @keydown.ctrl.enter="submitReview"
+ />
+ </div>
+ <gl-form-radio-group
+ v-if="glFeatures.mrRequestChanges"
+ v-model="noteData.reviewer_state"
+ :options="radioGroupOptions"
class="gl-mt-4"
- >
- {{ __('Approve merge request') }}
- </gl-form-checkbox>
+ data-testid="reviewer_states"
+ />
+ <template v-else-if="userPermissions.canApprove">
+ <gl-form-checkbox
+ v-model="noteData.approve"
+ data-testid="approve_merge_request"
+ class="gl-mt-4"
+ >
+ {{ __('Approve merge request') }}
+ </gl-form-checkbox>
+ </template>
<approval-password
- v-if="getNoteableData.require_password_to_approve"
- v-show="noteData.approve"
+ v-if="userPermissions.canApprove && getNoteableData.require_password_to_approve"
+ v-show="noteData.approve || noteData.reviewer_state === 'approved'"
v-model="noteData.approval_password"
class="gl-mt-3"
data-testid="approve_password"
/>
- </template>
- <div class="gl-display-flex gl-justify-content-start gl-mt-4">
- <gl-button
- :loading="isSubmitting"
- variant="confirm"
- type="submit"
- class="js-no-auto-disable"
- data-testid="submit-review-button"
- >
- {{ __('Submit review') }}
- </gl-button>
- </div>
- </gl-form>
- </gl-dropdown>
+ <div class="gl-display-flex gl-justify-content-start gl-mt-4">
+ <gl-button
+ :loading="isSubmitting"
+ variant="confirm"
+ type="submit"
+ class="js-no-auto-disable"
+ data-testid="submit-review-button"
+ >
+ {{ __('Submit review') }}
+ </gl-button>
+ </div>
+ </gl-form>
+ </template>
+ </gl-disclosure-dropdown>
</template>
diff --git a/app/assets/javascripts/batch_comments/queries/can_approve.query.graphql b/app/assets/javascripts/batch_comments/queries/can_approve.query.graphql
new file mode 100644
index 00000000000..f0c9ef7b3c8
--- /dev/null
+++ b/app/assets/javascripts/batch_comments/queries/can_approve.query.graphql
@@ -0,0 +1,11 @@
+query userCanApprove($projectPath: ID!, $iid: String!) {
+ project(fullPath: $projectPath) {
+ id
+ mergeRequest(iid: $iid) {
+ id
+ userPermissions {
+ canApprove
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js b/app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js
index 070ce38c8aa..d97f11a0acd 100644
--- a/app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js
+++ b/app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js
@@ -72,22 +72,20 @@ export const fetchDrafts = ({ commit, getters, state, dispatch }) =>
}),
);
-export const publishSingleDraft = ({ commit, dispatch, getters }, draftId) => {
+export const publishSingleDraft = ({ commit, getters }, draftId) => {
commit(types.REQUEST_PUBLISH_DRAFT, draftId);
service
.publishDraft(getters.getNotesData.draftsPublishPath, draftId)
- .then(() => dispatch('updateDiscussionsAfterPublish'))
.then(() => commit(types.RECEIVE_PUBLISH_DRAFT_SUCCESS, draftId))
.catch(() => commit(types.RECEIVE_PUBLISH_DRAFT_ERROR, draftId));
};
-export const publishReview = ({ commit, dispatch, getters }, noteData = {}) => {
+export const publishReview = ({ commit, getters }, noteData = {}) => {
commit(types.REQUEST_PUBLISH_REVIEW);
return service
.publish(getters.getNotesData.draftsPublishPath, noteData)
- .then(() => dispatch('updateDiscussionsAfterPublish'))
.then(() => commit(types.RECEIVE_PUBLISH_REVIEW_SUCCESS))
.catch((e) => {
commit(types.RECEIVE_PUBLISH_REVIEW_ERROR);
@@ -96,18 +94,6 @@ export const publishReview = ({ commit, dispatch, getters }, noteData = {}) => {
});
};
-export const updateDiscussionsAfterPublish = async ({ dispatch, getters, rootGetters }) => {
- await dispatch(
- 'fetchDiscussions',
- { path: getters.getNotesData.discussionsPath },
- { root: true },
- );
-
- dispatch('diffs/assignDiscussionsToDiff', rootGetters.discussionsStructuredByLineCode, {
- root: true,
- });
-};
-
export const updateDraft = (
{ commit, getters },
{ note, noteText, resolveDiscussion, position, flashContainer, callback, errorCallback },
diff --git a/app/assets/javascripts/behaviors/index.js b/app/assets/javascripts/behaviors/index.js
index dc9153e61f7..84ff8fa7f33 100644
--- a/app/assets/javascripts/behaviors/index.js
+++ b/app/assets/javascripts/behaviors/index.js
@@ -3,7 +3,6 @@ import './autosize';
import initCollapseSidebarOnWindowResize from './collapse_sidebar_on_window_resize';
import initCopyToClipboard from './copy_to_clipboard';
import installGlEmojiElement from './gl_emoji';
-import { loadStartupCSS } from './load_startup_css';
import initCopyAsGFM from './markdown/copy_as_gfm';
import './quick_submit';
import './requires_input';
@@ -13,8 +12,6 @@ import { initGlobalAlerts } from './global_alerts';
import './toggler_behavior';
import './preview_markdown';
-loadStartupCSS();
-
installGlEmojiElement();
initCopyAsGFM();
diff --git a/app/assets/javascripts/behaviors/load_startup_css.js b/app/assets/javascripts/behaviors/load_startup_css.js
deleted file mode 100644
index dbe9ff8b6e7..00000000000
--- a/app/assets/javascripts/behaviors/load_startup_css.js
+++ /dev/null
@@ -1,15 +0,0 @@
-export const loadStartupCSS = () => {
- // We need to fallback to dispatching `load` in case our event listener was added too late
- // or the browser environment doesn't load media=print.
- // Do this on `window.load` so that the default deferred behavior takes precedence.
- // https://gitlab.com/gitlab-org/gitlab/-/issues/239357
- window.addEventListener(
- 'load',
- () => {
- document
- .querySelectorAll('link[media=print]')
- .forEach((x) => x.dispatchEvent(new Event('load')));
- },
- { once: true },
- );
-};
diff --git a/app/assets/javascripts/behaviors/shortcuts/keybindings.js b/app/assets/javascripts/behaviors/shortcuts/keybindings.js
index e8c486f6e74..941662635ea 100644
--- a/app/assets/javascripts/behaviors/shortcuts/keybindings.js
+++ b/app/assets/javascripts/behaviors/shortcuts/keybindings.js
@@ -381,6 +381,12 @@ export const PROJECT_FILES_GO_TO_PERMALINK = {
defaultKeys: ['y'],
};
+export const PROJECT_FILES_GO_TO_COMPARE = {
+ id: 'projectFiles.goToCompare',
+ description: __('Compare Branches'),
+ defaultKeys: ['shift+c'],
+};
+
export const ISSUABLE_COMMENT_OR_REPLY = {
id: 'issuables.commentReply',
description: __('Comment/Reply (quoting selected text)'),
@@ -606,6 +612,7 @@ const PROJECT_FILES_SHORTCUTS_GROUP = {
PROJECT_FILES_OPEN_SELECTION,
PROJECT_FILES_GO_BACK,
PROJECT_FILES_GO_TO_PERMALINK,
+ PROJECT_FILES_GO_TO_COMPARE,
],
};
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js
index d9dc3aae808..4691a4228e6 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js
@@ -18,6 +18,7 @@ import {
GO_TO_PROJECT_KUBERNETES,
GO_TO_PROJECT_ENVIRONMENTS,
GO_TO_PROJECT_WEBIDE,
+ PROJECT_FILES_GO_TO_COMPARE,
NEW_ISSUE,
} from './keybindings';
import Shortcuts from './shortcuts';
@@ -43,6 +44,7 @@ export default class ShortcutsNavigation extends Shortcuts {
[GO_TO_PROJECT_SNIPPETS, () => findAndFollowLink('.shortcuts-snippets')],
[GO_TO_PROJECT_KUBERNETES, () => findAndFollowLink('.shortcuts-kubernetes')],
[GO_TO_PROJECT_ENVIRONMENTS, () => findAndFollowLink('.shortcuts-environments')],
+ [PROJECT_FILES_GO_TO_COMPARE, () => findAndFollowLink('.shortcuts-compare')],
[GO_TO_PROJECT_WEBIDE, ShortcutsNavigation.navigateToWebIDE],
[NEW_ISSUE, () => findAndFollowLink('.shortcuts-new-issue')],
]);
diff --git a/app/assets/javascripts/blob/components/blob_header.vue b/app/assets/javascripts/blob/components/blob_header.vue
index 699a0491183..5411881a8d2 100644
--- a/app/assets/javascripts/blob/components/blob_header.vue
+++ b/app/assets/javascripts/blob/components/blob_header.vue
@@ -5,7 +5,7 @@ import userInfoQuery from '../queries/user_info.query.graphql';
import applicationInfoQuery from '../queries/application_info.query.graphql';
import BlobFilepath from './blob_header_filepath.vue';
import ViewerSwitcher from './blob_header_viewer_switcher.vue';
-import { SIMPLE_BLOB_VIEWER } from './constants';
+import { SIMPLE_BLOB_VIEWER, BLAME_VIEWER } from './constants';
import TableOfContents from './table_contents.vue';
export default {
@@ -85,6 +85,11 @@ export default {
required: false,
default: '',
},
+ showBlameToggle: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -93,9 +98,6 @@ export default {
};
},
computed: {
- showViewerSwitcher() {
- return !this.hideViewerSwitcher && Boolean(this.blob.simpleViewer && this.blob.richViewer);
- },
showDefaultActions() {
return !this.hideDefaultActions;
},
@@ -114,7 +116,7 @@ export default {
},
watch: {
viewer(newVal, oldVal) {
- if (!this.hideViewerSwitcher && newVal !== oldVal) {
+ if (newVal !== BLAME_VIEWER && newVal !== oldVal) {
this.$emit('viewer-changed', newVal);
}
},
@@ -138,7 +140,14 @@ export default {
</div>
<div class="gl-display-flex gl-flex-wrap file-actions">
- <viewer-switcher v-if="showViewerSwitcher" v-model="viewer" :doc-icon="blobSwitcherDocIcon" />
+ <viewer-switcher
+ v-if="!hideViewerSwitcher"
+ v-model="viewer"
+ :doc-icon="blobSwitcherDocIcon"
+ :show-blame-toggle="showBlameToggle"
+ :show-viewer-toggles="Boolean(blob.simpleViewer && blob.richViewer)"
+ v-on="$listeners"
+ />
<web-ide-link
v-if="showWebIdeLink"
diff --git a/app/assets/javascripts/blob/components/blob_header_viewer_switcher.vue b/app/assets/javascripts/blob/components/blob_header_viewer_switcher.vue
index 7351df0f93b..9b5b77ebebe 100644
--- a/app/assets/javascripts/blob/components/blob_header_viewer_switcher.vue
+++ b/app/assets/javascripts/blob/components/blob_header_viewer_switcher.vue
@@ -5,6 +5,8 @@ import {
RICH_BLOB_VIEWER_TITLE,
SIMPLE_BLOB_VIEWER,
SIMPLE_BLOB_VIEWER_TITLE,
+ BLAME_VIEWER,
+ BLAME_TITLE,
} from './constants';
export default {
@@ -26,6 +28,16 @@ export default {
default: 'document',
required: false,
},
+ showViewerToggles: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ showBlameToggle: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
isSimpleViewer() {
@@ -34,9 +46,16 @@ export default {
isRichViewer() {
return this.value === RICH_BLOB_VIEWER;
},
+ isBlameViewer() {
+ return this.value === BLAME_VIEWER;
+ },
},
methods: {
switchToViewer(viewer) {
+ if (viewer === BLAME_VIEWER) {
+ this.$emit('blame');
+ }
+
if (viewer !== this.value) {
this.$emit('input', viewer);
}
@@ -46,11 +65,14 @@ export default {
RICH_BLOB_VIEWER,
SIMPLE_BLOB_VIEWER_TITLE,
RICH_BLOB_VIEWER_TITLE,
+ BLAME_TITLE,
+ BLAME_VIEWER,
};
</script>
<template>
<gl-button-group class="js-blob-viewer-switcher mx-2">
<gl-button
+ v-if="showViewerToggles"
v-gl-tooltip.hover
:aria-label="$options.SIMPLE_BLOB_VIEWER_TITLE"
:title="$options.SIMPLE_BLOB_VIEWER_TITLE"
@@ -63,6 +85,7 @@ export default {
@click="switchToViewer($options.SIMPLE_BLOB_VIEWER)"
/>
<gl-button
+ v-if="showViewerToggles"
v-gl-tooltip.hover
:aria-label="$options.RICH_BLOB_VIEWER_TITLE"
:title="$options.RICH_BLOB_VIEWER_TITLE"
@@ -74,5 +97,16 @@ export default {
data-viewer="rich"
@click="switchToViewer($options.RICH_BLOB_VIEWER)"
/>
+ <gl-button
+ v-if="showBlameToggle"
+ v-gl-tooltip.hover
+ :title="$options.BLAME_TITLE"
+ :selected="isBlameViewer"
+ category="primary"
+ variant="default"
+ data-test-id="blame-toggle"
+ @click="switchToViewer($options.BLAME_VIEWER)"
+ >{{ __('Blame') }}</gl-button
+ >
</gl-button-group>
</template>
diff --git a/app/assets/javascripts/blob/components/constants.js b/app/assets/javascripts/blob/components/constants.js
index adac4d6408d..bccab09c7a2 100644
--- a/app/assets/javascripts/blob/components/constants.js
+++ b/app/assets/javascripts/blob/components/constants.js
@@ -11,6 +11,9 @@ export const SIMPLE_BLOB_VIEWER_TITLE = __('Display source');
export const RICH_BLOB_VIEWER = 'rich';
export const RICH_BLOB_VIEWER_TITLE = __('Display rendered file');
+export const BLAME_VIEWER = 'blame';
+export const BLAME_TITLE = __('Display blame info');
+
export const BLOB_RENDER_EVENT_LOAD = 'force-content-fetch';
export const BLOB_RENDER_EVENT_SHOW_SOURCE = 'force-switch-viewer';
diff --git a/app/assets/javascripts/blob/filepath_form/components/template_selector.vue b/app/assets/javascripts/blob/filepath_form/components/template_selector.vue
index 51c69590796..379d5e38197 100644
--- a/app/assets/javascripts/blob/filepath_form/components/template_selector.vue
+++ b/app/assets/javascripts/blob/filepath_form/components/template_selector.vue
@@ -2,6 +2,7 @@
import { GlCollapsibleListbox } from '@gitlab/ui';
import SuggestGitlabCiYml from '~/blob/suggest_gitlab_ci_yml/components/popover.vue';
import { __ } from '~/locale';
+import { DEFAULT_CI_CONFIG_PATH, CI_CONFIG_PATH_EXTENSION } from '~/lib/utils/constants';
const templateSelectors = [
{
@@ -12,8 +13,8 @@ const templateSelectors = [
},
{
key: 'gitlab_ci_ymls',
- name: '.gitlab-ci.yml',
- pattern: /(.gitlab-ci.yml)/,
+ name: DEFAULT_CI_CONFIG_PATH,
+ pattern: CI_CONFIG_PATH_EXTENSION,
type: 'gitlab_ci_ymls',
},
{
diff --git a/app/assets/javascripts/blob/pipeline_tour_success_modal.vue b/app/assets/javascripts/blob/pipeline_tour_success_modal.vue
index f3c542c467a..0cc75d28e0b 100644
--- a/app/assets/javascripts/blob/pipeline_tour_success_modal.vue
+++ b/app/assets/javascripts/blob/pipeline_tour_success_modal.vue
@@ -94,14 +94,12 @@ export default {
<gl-modal visible size="sm" modal-id="success-pipeline-modal-id-not-used">
<template #modal-title>
{{ $options.i18n.modalTitle }}
- <gl-emoji class="gl-vertical-align-baseline font-size-inherit gl-mr-1" data-name="tada" />
+ <gl-emoji class="gl-vertical-align-baseline gl-reset-font-size gl-mr-1" data-name="tada" />
</template>
<p>
<gl-sprintf :message="$options.i18n.bodyMessage">
<template #codeQualityLink="{ content }">
- <gl-link :href="codeQualityLink" target="_blank" class="font-size-inherit">{{
- content
- }}</gl-link>
+ <gl-link :href="codeQualityLink" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</p>
diff --git a/app/assets/javascripts/boards/boards_util.js b/app/assets/javascripts/boards/boards_util.js
index bf77aa4996c..fd36eea95eb 100644
--- a/app/assets/javascripts/boards/boards_util.js
+++ b/app/assets/javascripts/boards/boards_util.js
@@ -204,7 +204,8 @@ export function moveItemListHelper(item, fromList, toList) {
export function moveItemVariables({
iid,
- epicId,
+ itemId,
+ epicId = null,
fromListId,
toListId,
moveBeforeId,
@@ -225,6 +226,7 @@ export function moveItemVariables({
};
}
return {
+ itemId,
epicId,
boardId,
moveBeforeId,
diff --git a/app/assets/javascripts/boards/components/board_add_new_column_form.vue b/app/assets/javascripts/boards/components/board_add_new_column_form.vue
index 419d0b41d69..a3c0553d17c 100644
--- a/app/assets/javascripts/boards/components/board_add_new_column_form.vue
+++ b/app/assets/javascripts/boards/components/board_add_new_column_form.vue
@@ -43,7 +43,6 @@ export default {
<div
class="board-add-new-list board gl-display-inline-block gl-h-full gl-vertical-align-top gl-white-space-normal gl-flex-shrink-0 gl-rounded-base gl-px-3"
data-testid="board-add-new-column"
- data-qa-selector="board_add_new_list"
>
<div
class="board-inner gl-display-flex gl-flex-direction-column gl-relative gl-h-full gl-rounded-base gl-bg-gray-50"
diff --git a/app/assets/javascripts/boards/components/board_card_inner.vue b/app/assets/javascripts/boards/components/board_card_inner.vue
index c10ff2e08da..a7f46dc9325 100644
--- a/app/assets/javascripts/boards/components/board_card_inner.vue
+++ b/app/assets/javascripts/boards/components/board_card_inner.vue
@@ -139,8 +139,11 @@ export default {
}
return false;
},
+ hasChildren() {
+ return this.totalIssuesCount + this.totalEpicsCount > 0;
+ },
shouldRenderEpicCountables() {
- return this.isEpicBoard && this.item.hasIssues;
+ return this.isEpicBoard && this.hasChildren;
},
shouldRenderEpicProgress() {
return this.totalWeight > 0;
@@ -396,7 +399,7 @@ export default {
<issue-due-date
v-if="item.dueDate"
:date="item.dueDate"
- :closed="item.closed || Boolean(item.closedAt)"
+ :closed="Boolean(item.closedAt)"
/>
<issue-time-estimate v-if="item.timeEstimate" :estimate="item.timeEstimate" />
<issue-card-weight
diff --git a/app/assets/javascripts/boards/components/board_content.vue b/app/assets/javascripts/boards/components/board_content.vue
index 554f3bfa416..a6ff1653c17 100644
--- a/app/assets/javascripts/boards/components/board_content.vue
+++ b/app/assets/javascripts/boards/components/board_content.vue
@@ -249,7 +249,6 @@ export default {
<transition name="slide" @after-enter="afterFormEnters">
<board-add-new-column
v-if="addColumnFormVisible"
- class="gl-xs-w-full!"
:board-id="boardId"
:list-query-variables="listQueryVariables"
:lists="boardListsById"
diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue
index 2693a6bb5ea..ca10cbbad5e 100644
--- a/app/assets/javascripts/boards/components/board_list.vue
+++ b/app/assets/javascripts/boards/components/board_list.vue
@@ -100,9 +100,6 @@ export default {
filters: this.filterParams,
};
},
- context: {
- isSingleRequest: true,
- },
skip() {
return this.isEpicBoard;
},
@@ -123,9 +120,6 @@ export default {
update(data) {
return data[this.boardType].board.lists.nodes[0];
},
- context: {
- isSingleRequest: true,
- },
error(error) {
setError({
error,
@@ -149,9 +143,6 @@ export default {
update(data) {
return data[this.boardType].board.lists.nodes[0];
},
- context: {
- isSingleRequest: true,
- },
error(error) {
setError({
error,
@@ -400,7 +391,7 @@ export default {
this.updateIssueOrderInProgress = true;
await this.moveBoardItem(
{
- epicId: itemId,
+ itemId,
iid: itemIid,
fromListId: from.dataset.listId,
toListId: to.dataset.listId,
@@ -428,11 +419,11 @@ export default {
return items.some((item) => item.iid === itemIid);
},
async moveBoardItem(variables, newIndex) {
- const { fromListId, toListId, iid } = variables;
+ const { fromListId, toListId, iid, itemId } = variables;
this.toListId = toListId;
await this.$nextTick(); // we need this next tick to retrieve `toList` from Apollo cache
- const itemToMove = this.boardListItems.find((item) => item.iid === iid);
+ const itemToMove = this.boardListItems.find((item) => item.id === itemId);
if (this.shouldCloneCard && this.isItemInTheList(iid)) {
return;
@@ -445,6 +436,7 @@ export default {
...moveItemVariables({
...variables,
isIssue: !this.isEpicBoard,
+ epicId: itemId, // for Epic Boards
boardId: this.boardId,
itemToMove,
}),
@@ -532,7 +524,8 @@ export default {
variables: {
...moveItemVariables({
iid: item.iid,
- epicId: item.id,
+ itemId: item.id,
+ epicId: item.id, // for Epic Boards
fromListId: this.currentList.id,
toListId: this.currentList.id,
isIssue: !this.isEpicBoard,
diff --git a/app/assets/javascripts/boards/components/board_list_header.vue b/app/assets/javascripts/boards/components/board_list_header.vue
index 0235edd69ac..bedb3a75a70 100644
--- a/app/assets/javascripts/boards/components/board_list_header.vue
+++ b/app/assets/javascripts/boards/components/board_list_header.vue
@@ -223,9 +223,6 @@ export default {
variables() {
return this.countQueryVariables;
},
- context: {
- isSingleRequest: true,
- },
error(error) {
setError({
error,
diff --git a/app/assets/javascripts/boards/components/new_board_button.vue b/app/assets/javascripts/boards/components/new_board_button.vue
index f7914c636cc..96cf0fadd6a 100644
--- a/app/assets/javascripts/boards/components/new_board_button.vue
+++ b/app/assets/javascripts/boards/components/new_board_button.vue
@@ -38,7 +38,7 @@ export default {
<template #control> </template>
<template #candidate>
<div v-if="canShowCreateButton" class="gl-ml-1 gl-mr-3 gl-display-flex gl-align-items-center">
- <gl-button data-qa-selector="new_board_button" @click.prevent="showDialog">
+ <gl-button @click.prevent="showDialog">
{{ createButtonText }}
</gl-button>
</div>
diff --git a/app/assets/javascripts/boards/constants.js b/app/assets/javascripts/boards/constants.js
index cb607e5220e..acf01a8c528 100644
--- a/app/assets/javascripts/boards/constants.js
+++ b/app/assets/javascripts/boards/constants.js
@@ -132,6 +132,7 @@ export const listIssuablesQueries = {
optimisticResponse: {
assignees: { nodes: [], __typename: 'UserCoreConnection' },
confidential: false,
+ closedAt: null,
dueDate: null,
emailsDisabled: false,
hidden: false,
diff --git a/app/assets/javascripts/boards/graphql/cache_updates.js b/app/assets/javascripts/boards/graphql/cache_updates.js
index ea099e02181..bd58f445493 100644
--- a/app/assets/javascripts/boards/graphql/cache_updates.js
+++ b/app/assets/javascripts/boards/graphql/cache_updates.js
@@ -1,5 +1,6 @@
-import * as Sentry from '@sentry/browser';
import produce from 'immer';
+import { toNumber } from 'lodash';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { defaultClient } from '~/graphql_shared/issuable_client';
import listQuery from 'ee_else_ce/boards/graphql/board_lists_deferred.query.graphql';
import { listsDeferredQuery } from 'ee_else_ce/boards/constants';
@@ -83,7 +84,9 @@ export function updateIssueCountAndWeight({
boardList: {
...boardList,
issuesCount: boardList.issuesCount + 1,
- ...(issue.weight ? { totalIssueWeight: boardList.totalIssueWeight + issue.weight } : {}),
+ ...(issue.weight
+ ? { totalIssueWeight: toNumber(boardList.totalIssueWeight) + issue.weight }
+ : {}),
},
}),
);
diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js
index 3e7d7a7a8d3..97e40c8cc39 100644
--- a/app/assets/javascripts/boards/stores/actions.js
+++ b/app/assets/javascripts/boards/stores/actions.js
@@ -1,5 +1,5 @@
-import * as Sentry from '@sentry/browser';
import { sortBy } from 'lodash';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import {
ListType,
inactiveId,
@@ -148,9 +148,6 @@ export default {
query: listsQuery[issuableType].query,
variables,
...(resetLists ? { fetchPolicy: fetchPolicies.NO_CACHE } : {}),
- context: {
- isSingleRequest: true,
- },
})
.then(({ data }) => {
const { lists, hideBacklogList } = data[boardType].board;
@@ -439,9 +436,6 @@ export default {
return gqlClient
.query({
query: listsIssuesQuery,
- context: {
- isSingleRequest: true,
- },
variables,
...(!fetchNext ? { fetchPolicy: fetchPolicies.NO_CACHE } : {}),
})
diff --git a/app/assets/javascripts/boards/stores/index.js b/app/assets/javascripts/boards/stores/index.js
index ee0a5e27d9a..fd562df1df7 100644
--- a/app/assets/javascripts/boards/stores/index.js
+++ b/app/assets/javascripts/boards/stores/index.js
@@ -8,12 +8,13 @@ import state from 'ee_else_ce/boards/stores/state';
Vue.use(Vuex);
-export const createStore = () =>
- new Vuex.Store({
- state,
- getters,
- actions,
- mutations,
- });
+export const storeOptions = {
+ state,
+ getters,
+ actions,
+ mutations,
+};
+
+export const createStore = (options = storeOptions) => new Vuex.Store(options);
export default createStore();
diff --git a/app/assets/javascripts/branches/components/branch_more_actions.vue b/app/assets/javascripts/branches/components/branch_more_actions.vue
index c646dab2760..ee47f6af2f8 100644
--- a/app/assets/javascripts/branches/components/branch_more_actions.vue
+++ b/app/assets/javascripts/branches/components/branch_more_actions.vue
@@ -74,7 +74,6 @@ export default {
class: 'js-delete-branch-button gl-text-red-500!',
'aria-label': this.deleteBranchText,
'data-testid': 'delete-branch-button',
- 'data-qa-selector': 'delete_branch_button',
},
});
}
diff --git a/app/assets/javascripts/branches/components/delete_branch_modal.vue b/app/assets/javascripts/branches/components/delete_branch_modal.vue
index d5631337cec..0200a30cbdf 100644
--- a/app/assets/javascripts/branches/components/delete_branch_modal.vue
+++ b/app/assets/javascripts/branches/components/delete_branch_modal.vue
@@ -182,7 +182,6 @@ export default {
ref="deleteBranchButton"
:disabled="deleteButtonDisabled"
variant="danger"
- data-qa-selector="delete_branch_confirmation_button"
data-testid="delete-branch-confirmation-button"
@click="submitForm"
>{{ buttonText }}</gl-button
diff --git a/app/assets/javascripts/ci/admin/jobs_table/admin_jobs_table_app.vue b/app/assets/javascripts/ci/admin/jobs_table/admin_jobs_table_app.vue
index 89582e64f3a..55ff647e25f 100644
--- a/app/assets/javascripts/ci/admin/jobs_table/admin_jobs_table_app.vue
+++ b/app/assets/javascripts/ci/admin/jobs_table/admin_jobs_table_app.vue
@@ -84,9 +84,6 @@ export default {
update(data) {
return data?.jobs?.count || 0;
},
- context: {
- isSingleRequest: true,
- },
error() {
this.error = this.$options.i18n.jobsCountErrorMsg;
},
diff --git a/app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue b/app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue
index d8f9eb65236..de37aa431e6 100644
--- a/app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue
+++ b/app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue
@@ -10,7 +10,7 @@ import {
GlFormCheckbox,
GlTooltipDirective,
} from '@gitlab/ui';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { createAlert } from '~/alert';
import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
@@ -68,7 +68,7 @@ export default {
GlPagination,
GlFormCheckbox,
TimeAgo,
- CiBadgeLink,
+ CiIcon,
JobCheckbox,
ArtifactsBulkDelete,
BulkDeleteModal,
@@ -442,7 +442,7 @@ export default {
<template #cell(job)="{ item }">
<div class="gl-display-inline-flex gl-align-items-center gl-mb-3 gl-gap-3">
<span data-testid="job-artifacts-job-status">
- <ci-badge-link :status="item.detailedStatus" size="sm" :show-text="false" />
+ <ci-icon :status="item.detailedStatus" />
</span>
<gl-link :href="item.webPath">
{{ item.name }}
diff --git a/app/assets/javascripts/ci/catalog/components/details/ci_resource_components.vue b/app/assets/javascripts/ci/catalog/components/details/ci_resource_components.vue
index 85dfa12c756..fbc7ddf5c91 100644
--- a/app/assets/javascripts/ci/catalog/components/details/ci_resource_components.vue
+++ b/app/assets/javascripts/ci/catalog/components/details/ci_resource_components.vue
@@ -1,11 +1,13 @@
<script>
-import { GlLoadingIcon, GlTableLite } from '@gitlab/ui';
+import { GlButton, GlEmptyState, GlLoadingIcon, GlTableLite } from '@gitlab/ui';
import { createAlert } from '~/alert';
import { __, s__ } from '~/locale';
import getCiCatalogResourceComponents from '../../graphql/queries/get_ci_catalog_resource_components.query.graphql';
export default {
components: {
+ GlButton,
+ GlEmptyState,
GlLoadingIcon,
GlTableLite,
},
@@ -37,6 +39,9 @@ export default {
},
},
computed: {
+ isMetadataMissing() {
+ return !this.components || this.components?.length === 0;
+ },
isLoading() {
return this.$apollo.queries.components.loading;
},
@@ -70,6 +75,12 @@ export default {
},
],
i18n: {
+ copyText: __('Copy value'),
+ copyAriaText: __('Copy to clipboard'),
+ emptyStateTitle: s__('CiCatalogComponent|Component details not available'),
+ emptyStateDesc: s__(
+ 'CiCatalogComponent|This tab displays auto-collected information about the components in the repository, but no information was found.',
+ ),
inputTitle: s__('CiCatalogComponent|Inputs'),
fetchError: s__("CiCatalogComponent|There was an error fetching this resource's components"),
},
@@ -79,6 +90,11 @@ export default {
<template>
<div>
<gl-loading-icon v-if="isLoading" size="lg" />
+ <gl-empty-state
+ v-else-if="isMetadataMissing"
+ :title="$options.i18n.emptyStateTitle"
+ :description="$options.i18n.emptyStateDesc"
+ />
<template v-else>
<div
v-for="component in components"
@@ -88,7 +104,24 @@ export default {
>
<h3 class="gl-font-size-h2" data-testid="component-name">{{ component.name }}</h3>
<p class="gl-mt-5">{{ component.description }}</p>
- <pre class="gl-w-85p gl-py-4">{{ generateSnippet(component.path) }}</pre>
+ <div class="gl-display-flex">
+ <pre
+ class="gl-w-85p gl-py-4 gl-display-flex gl-justify-content-space-between gl-m-0 gl-border-r-none"
+ ><span>{{ generateSnippet(component.path) }}</span>
+ </pre>
+ <div class="gl--flex-center gl-bg-gray-10 gl-border gl-border-l-none">
+ <gl-button
+ class="gl-p-4! gl-mr-3!"
+ category="tertiary"
+ icon="copy-to-clipboard"
+ size="small"
+ :title="$options.i18n.copyText"
+ :data-clipboard-text="generateSnippet(component.path)"
+ data-testid="copy-to-clipboard"
+ :aria-label="$options.i18n.copyAriaText"
+ />
+ </div>
+ </div>
<div class="gl-mt-5">
<b class="gl-display-block gl-mb-4"> {{ $options.i18n.inputTitle }}</b>
<gl-table-lite :items="component.inputs.nodes" :fields="$options.fields">
diff --git a/app/assets/javascripts/ci/catalog/components/details/ci_resource_details.vue b/app/assets/javascripts/ci/catalog/components/details/ci_resource_details.vue
index c0feb52c185..026a30988fd 100644
--- a/app/assets/javascripts/ci/catalog/components/details/ci_resource_details.vue
+++ b/app/assets/javascripts/ci/catalog/components/details/ci_resource_details.vue
@@ -30,12 +30,12 @@ export default {
<template>
<gl-tabs>
- <gl-tab v-if="glFeatures.ciCatalogComponentsTab" :title="$options.i18n.tabs.components" lazy>
- <ci-resource-components :resource-id="resourceId"
- /></gl-tab>
<gl-tab :title="$options.i18n.tabs.readme" lazy>
<ci-resource-readme :resource-id="resourceId" />
</gl-tab>
+ <gl-tab v-if="glFeatures.ciCatalogComponentsTab" :title="$options.i18n.tabs.components" lazy>
+ <ci-resource-components :resource-id="resourceId"
+ /></gl-tab>
</gl-tabs>
</template>
<style></style>
diff --git a/app/assets/javascripts/ci/catalog/components/details/ci_resource_header.vue b/app/assets/javascripts/ci/catalog/components/details/ci_resource_header.vue
index 6673785ffd2..29009c14e1b 100644
--- a/app/assets/javascripts/ci/catalog/components/details/ci_resource_header.vue
+++ b/app/assets/javascripts/ci/catalog/components/details/ci_resource_header.vue
@@ -2,13 +2,13 @@
import { GlAvatar, GlAvatarLink, GlBadge } from '@gitlab/ui';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { isNumeric } from '~/lib/utils/number_utils';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import CiResourceAbout from './ci_resource_about.vue';
import CiResourceHeaderSkeletonLoader from './ci_resource_header_skeleton_loader.vue';
export default {
components: {
- CiBadgeLink,
+ CiIcon,
CiResourceAbout,
CiResourceHeaderSkeletonLoader,
GlAvatar,
@@ -102,12 +102,11 @@ export default {
{{ versionBadgeText }}
</gl-badge>
</span>
- <ci-badge-link
+ <ci-icon
v-if="hasPipelineStatus"
- class="gl-mt-2"
:status="pipelineStatus"
- size="sm"
- show-text
+ show-status-text
+ class="gl-mt-2"
/>
</div>
</div>
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 487215875c0..db84eaa82c2 100644
--- a/app/assets/javascripts/ci/catalog/components/list/catalog_header.vue
+++ b/app/assets/javascripts/ci/catalog/components/list/catalog_header.vue
@@ -4,12 +4,22 @@ import { __, s__ } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
import { CATALOG_FEEDBACK_DISMISSED_KEY } from '../../constants';
+const defaultTitle = __('CI/CD Catalog');
+const defaultDescription = s__(
+ 'CiCatalog|Discover CI configuration resources for a seamless CI/CD experience.',
+);
+
export default {
components: {
GlBanner,
GlLink,
},
- inject: ['pageTitle', 'pageDescription'],
+ inject: {
+ pageTitle: { default: defaultTitle },
+ pageDescription: {
+ default: defaultDescription,
+ },
+ },
data() {
return {
isFeedbackBannerDismissed: localStorage.getItem(CATALOG_FEEDBACK_DISMISSED_KEY) === 'true',
@@ -50,7 +60,7 @@ export default {
</gl-banner>
<h1 class="gl-font-size-h-display">{{ pageTitle }}</h1>
<p>
- <span>{{ pageDescription }}</span>
+ <span data-testid="description">{{ pageDescription }}</span>
<gl-link :href="$options.learnMorePath" target="_blank">{{
$options.i18n.learnMore
}}</gl-link>
diff --git a/app/assets/javascripts/ci/catalog/components/list/ci_resources_list_item.vue b/app/assets/javascripts/ci/catalog/components/list/ci_resources_list_item.vue
index 63243539575..080955b4322 100644
--- a/app/assets/javascripts/ci/catalog/components/list/ci_resources_list_item.vue
+++ b/app/assets/javascripts/ci/catalog/components/list/ci_resources_list_item.vue
@@ -48,9 +48,6 @@ export default {
starCount() {
return this.resource?.starCount || 0;
},
- forksCount() {
- return this.resource?.forksCount || 0;
- },
hasReleasedVersion() {
return Boolean(this.latestVersion?.releasedAt);
},
@@ -111,14 +108,12 @@ export default {
<gl-icon name="star" :size="14" class="gl-mr-1" />
<span class="gl-mr-3">{{ starCount }}</span>
</span>
- <span class="gl--flex-center" data-testid="stats-forks">
- <gl-icon name="fork" :size="14" class="gl-mr-1" />
- <span>{{ forksCount }}</span>
- </span>
</span>
</div>
</div>
- <div class="gl-display-flex gl-sm-flex-direction-column gl-justify-content-space-between">
+ <div
+ class="gl-display-flex gl-flex-direction-column gl-md-flex-direction-row gl-justify-content-space-between"
+ >
<span class="gl-display-flex gl-flex-basis-two-thirds gl-font-sm">{{
resource.description
}}</span>
diff --git a/app/assets/javascripts/ci/catalog/components/pages/ci_resources_page.vue b/app/assets/javascripts/ci/catalog/components/pages/ci_resources_page.vue
new file mode 100644
index 00000000000..5e8727a3ed0
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/components/pages/ci_resources_page.vue
@@ -0,0 +1,112 @@
+<script>
+import { createAlert } from '~/alert';
+import { s__ } from '~/locale';
+import CatalogHeader from '~/ci/catalog/components/list/catalog_header.vue';
+import CatalogListSkeletonLoader from '~/ci/catalog/components/list/catalog_list_skeleton_loader.vue';
+import CiResourcesList from '~/ci/catalog/components/list/ci_resources_list.vue';
+import EmptyState from '~/ci/catalog/components/list/empty_state.vue';
+import { ciCatalogResourcesItemsCount } from '~/ci/catalog/graphql/settings';
+import getCatalogResources from '../../graphql/queries/get_ci_catalog_resources.query.graphql';
+
+export default {
+ components: {
+ CatalogHeader,
+ CatalogListSkeletonLoader,
+ CiResourcesList,
+ EmptyState,
+ },
+ data() {
+ return {
+ catalogResources: [],
+ currentPage: 1,
+ totalCount: 0,
+ pageInfo: {},
+ };
+ },
+ apollo: {
+ catalogResources: {
+ query: getCatalogResources,
+ variables() {
+ return {
+ first: ciCatalogResourcesItemsCount,
+ };
+ },
+ update(data) {
+ return data?.ciCatalogResources?.nodes || [];
+ },
+ result({ data }) {
+ const { pageInfo } = data?.ciCatalogResources || {};
+ this.pageInfo = pageInfo;
+ this.totalCount = data?.ciCatalogResources?.count || 0;
+ },
+ error(e) {
+ createAlert({ message: e.message || this.$options.i18n.fetchError, variant: 'danger' });
+ },
+ },
+ },
+ computed: {
+ hasResources() {
+ return this.catalogResources.length > 0;
+ },
+ isLoading() {
+ return this.$apollo.queries.catalogResources.loading;
+ },
+ },
+ methods: {
+ async handlePrevPage() {
+ try {
+ await this.$apollo.queries.catalogResources.fetchMore({
+ variables: {
+ before: this.pageInfo.startCursor,
+ last: ciCatalogResourcesItemsCount,
+ first: null,
+ },
+ });
+
+ this.currentPage -= 1;
+ } catch (e) {
+ // Ensure that the current query is properly stoped if an error occurs.
+ this.$apollo.queries.catalogResources.stop();
+ createAlert({ message: e?.message || this.$options.i18n.fetchError, variant: 'danger' });
+ }
+ },
+ async handleNextPage() {
+ try {
+ await this.$apollo.queries.catalogResources.fetchMore({
+ variables: {
+ after: this.pageInfo.endCursor,
+ },
+ });
+
+ this.currentPage += 1;
+ } catch (e) {
+ // Ensure that the current query is properly stoped if an error occurs.
+ this.$apollo.queries.catalogResources.stop();
+
+ createAlert({ message: e?.message || this.$options.i18n.fetchError, variant: 'danger' });
+ }
+ },
+ },
+ i18n: {
+ fetchError: s__('CiCatalog|There was an error fetching CI/CD Catalog resources.'),
+ },
+};
+</script>
+<template>
+ <div>
+ <catalog-header />
+ <catalog-list-skeleton-loader v-if="isLoading" class="gl-w-full gl-mt-3" />
+ <empty-state v-else-if="!hasResources" />
+ <ci-resources-list
+ v-else
+ :current-page="currentPage"
+ :page-info="pageInfo"
+ :prev-text="__('Prev')"
+ :next-text="__('Next')"
+ :resources="catalogResources"
+ :total-count="totalCount"
+ @onPrevPage="handlePrevPage"
+ @onNextPage="handleNextPage"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/ci/catalog/global_catalog.vue b/app/assets/javascripts/ci/catalog/global_catalog.vue
new file mode 100644
index 00000000000..76eac11a122
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/global_catalog.vue
@@ -0,0 +1,10 @@
+<script>
+import CiCatalogHome from './components/ci_catalog_home.vue';
+
+export default {
+ components: { CiCatalogHome },
+};
+</script>
+<template>
+ <ci-catalog-home />
+</template>
diff --git a/app/assets/javascripts/ci/catalog/graphql/fragments/catalog_resource.fragment.graphql b/app/assets/javascripts/ci/catalog/graphql/fragments/catalog_resource.fragment.graphql
index f4d1bb0eaaf..a86db4c1b03 100644
--- a/app/assets/javascripts/ci/catalog/graphql/fragments/catalog_resource.fragment.graphql
+++ b/app/assets/javascripts/ci/catalog/graphql/fragments/catalog_resource.fragment.graphql
@@ -4,7 +4,6 @@ fragment CatalogResourceFields on CiCatalogResource {
name
description
starCount
- forksCount
latestVersion {
id
tagName
diff --git a/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resources.query.graphql b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resources.query.graphql
new file mode 100644
index 00000000000..aae29edef5e
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resources.query.graphql
@@ -0,0 +1,16 @@
+#import "~/ci/catalog/graphql/fragments/catalog_resource.fragment.graphql"
+
+query getCatalogResources($after: String, $before: String, $first: Int = 20, $last: Int) {
+ ciCatalogResources(after: $after, before: $before, first: $first, last: $last) {
+ pageInfo {
+ startCursor
+ endCursor
+ hasNextPage
+ hasPreviousPage
+ }
+ count
+ nodes {
+ ...CatalogResourceFields
+ }
+ }
+}
diff --git a/app/assets/javascripts/ci/catalog/index.js b/app/assets/javascripts/ci/catalog/index.js
new file mode 100644
index 00000000000..5815245506c
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/index.js
@@ -0,0 +1,37 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import { cacheConfig, resolvers } from '~/ci/catalog/graphql/settings';
+
+import GlobalCatalog from './global_catalog.vue';
+import CiResourcesPage from './components/pages/ci_resources_page.vue';
+import { createRouter } from './router';
+
+export const initCatalog = (selector = '#js-ci-cd-catalog') => {
+ const el = document.querySelector(selector);
+ if (!el) {
+ return null;
+ }
+
+ const { dataset } = el;
+ const { ciCatalogPath } = dataset;
+
+ Vue.use(VueApollo);
+
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(resolvers, cacheConfig),
+ });
+
+ return new Vue({
+ el,
+ name: 'GlobalCatalog',
+ router: createRouter(ciCatalogPath, CiResourcesPage),
+ apolloProvider,
+ provide: {
+ ciCatalogPath,
+ },
+ render(h) {
+ return h(GlobalCatalog);
+ },
+ });
+};
diff --git a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_drawer.vue b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_drawer.vue
index a32c5f476fb..ccfe773b01f 100644
--- a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_drawer.vue
+++ b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_drawer.vue
@@ -190,6 +190,11 @@ export default {
deep: true,
},
},
+ beforeMount() {
+ // reset to default environments list every time we open the drawer
+ // and re-render the environments scope dropdown
+ this.$emit('search-environment-scope', '');
+ },
mounted() {
if (this.isProtectedByDefault && !this.isEditing) {
this.variable = { ...this.variable, protected: true };
@@ -371,7 +376,6 @@ export default {
:label-text="$options.i18n.key"
class="gl-border-none gl-pb-0! gl-mb-n5"
data-testid="ci-variable-key"
- data-qa-selector="ci_variable_key_field"
/>
<gl-form-group
:label="$options.i18n.value"
@@ -388,7 +392,6 @@ export default {
rows="3"
max-rows="10"
data-testid="ci-variable-value"
- data-qa-selector="ci_variable_value_field"
spellcheck="false"
/>
<p
@@ -419,15 +422,14 @@ export default {
variant="danger"
category="secondary"
class="gl-mr-3"
- data-testid="ci-variable-delete-btn"
+ data-testid="ci-variable-delete-button"
>{{ $options.i18n.deleteVariable }}</gl-button
>
<gl-button
category="primary"
variant="confirm"
:disabled="!canSubmit"
- data-testid="ci-variable-confirm-btn"
- data-qa-selector="ci_variable_save_button"
+ data-testid="ci-variable-confirm-button"
@click="submit"
>{{ modalActionText }}
</gl-button>
diff --git a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_modal.vue b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_modal.vue
deleted file mode 100644
index cc664d76267..00000000000
--- a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_modal.vue
+++ /dev/null
@@ -1,511 +0,0 @@
-<script>
-import {
- GlAlert,
- GlButton,
- GlCollapse,
- GlFormCheckbox,
- GlFormCombobox,
- GlFormGroup,
- GlFormSelect,
- GlFormInput,
- GlFormTextarea,
- GlIcon,
- GlLink,
- GlModal,
- GlSprintf,
-} from '@gitlab/ui';
-import { helpPagePath } from '~/helpers/help_page_helper';
-import { getCookie, setCookie } from '~/lib/utils/common_utils';
-import { __ } from '~/locale';
-import Tracking from '~/tracking';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-
-import {
- allEnvironments,
- AWS_TOKEN_CONSTANTS,
- ADD_CI_VARIABLE_MODAL_ID,
- AWS_TIP_DISMISSED_COOKIE_NAME,
- AWS_TIP_TITLE,
- AWS_TIP_MESSAGE,
- CONTAINS_VARIABLE_REFERENCE_MESSAGE,
- defaultVariableState,
- ENVIRONMENT_SCOPE_LINK_TITLE,
- EVENT_LABEL,
- EVENT_ACTION,
- EXPANDED_VARIABLES_NOTE,
- EDIT_VARIABLE_ACTION,
- FLAG_LINK_TITLE,
- VARIABLE_ACTIONS,
- variableOptions,
-} from '../constants';
-import CiEnvironmentsDropdown from './ci_environments_dropdown.vue';
-import { awsTokens, awsTokenList } from './ci_variable_autocomplete_tokens';
-
-const trackingMixin = Tracking.mixin({ label: EVENT_LABEL });
-
-export default {
- components: {
- CiEnvironmentsDropdown,
- GlAlert,
- GlButton,
- GlCollapse,
- GlFormCheckbox,
- GlFormCombobox,
- GlFormGroup,
- GlFormSelect,
- GlFormInput,
- GlFormTextarea,
- GlIcon,
- GlLink,
- GlModal,
- GlSprintf,
- },
- mixins: [glFeatureFlagsMixin(), trackingMixin],
- inject: [
- 'containsVariableReferenceLink',
- 'environmentScopeLink',
- 'isProtectedByDefault',
- 'maskedEnvironmentVariablesLink',
- 'maskableRawRegex',
- 'maskableRegex',
- ],
- props: {
- areEnvironmentsLoading: {
- type: Boolean,
- required: true,
- },
- areScopedVariablesAvailable: {
- type: Boolean,
- required: false,
- default: false,
- },
- environments: {
- type: Array,
- required: false,
- default: () => [],
- },
- hideEnvironmentScope: {
- type: Boolean,
- required: false,
- default: false,
- },
- mode: {
- type: String,
- required: true,
- validator(val) {
- return VARIABLE_ACTIONS.includes(val);
- },
- },
- selectedVariable: {
- type: Object,
- required: false,
- default: () => {},
- },
- variables: {
- type: Array,
- required: false,
- default: () => [],
- },
- },
- data() {
- return {
- newEnvironments: [],
- isTipDismissed: getCookie(AWS_TIP_DISMISSED_COOKIE_NAME) === 'true',
- validationErrorEventProperty: '',
- variable: { ...defaultVariableState, ...this.selectedVariable },
- };
- },
- computed: {
- canMask() {
- const regex = RegExp(this.useRawMaskableRegexp ? this.maskableRawRegex : this.maskableRegex);
- return regex.test(this.variable.value);
- },
- canSubmit() {
- return this.variableValidationState && this.variable.key !== '';
- },
- containsVariableReference() {
- const regex = /\$/;
- return regex.test(this.variable.value) && this.isExpanded;
- },
- displayMaskedError() {
- return !this.canMask && this.variable.masked;
- },
- isEditing() {
- return this.mode === EDIT_VARIABLE_ACTION;
- },
- isExpanded() {
- return !this.isRaw;
- },
- isRaw() {
- return this.variable.raw;
- },
- isTipVisible() {
- return !this.isTipDismissed && AWS_TOKEN_CONSTANTS.includes(this.variable.key);
- },
- maskedFeedback() {
- return this.displayMaskedError
- ? __('This variable value does not meet the masking requirements.')
- : '';
- },
- maskedState() {
- if (this.displayMaskedError) {
- return false;
- }
- return true;
- },
- modalActionText() {
- return this.isEditing ? __('Update variable') : __('Add variable');
- },
- tokenValidationFeedback() {
- const tokenSpecificFeedback = this.$options.tokens?.[this.variable.key]?.invalidMessage;
- if (!this.tokenValidationState && tokenSpecificFeedback) {
- return tokenSpecificFeedback;
- }
- return '';
- },
- tokenValidationState() {
- const validator = this.$options.tokens?.[this.variable.key]?.validation;
-
- if (validator) {
- return validator(this.variable.value);
- }
-
- return true;
- },
- useRawMaskableRegexp() {
- return this.isRaw;
- },
- variableValidationFeedback() {
- return `${this.tokenValidationFeedback} ${this.maskedFeedback}`;
- },
- variableValidationState() {
- return this.variable.value === '' || (this.tokenValidationState && this.maskedState);
- },
- variableValueHelpText() {
- return this.variable.masked
- ? __('Value must meet regular expression requirements to be masked.')
- : '';
- },
- },
- watch: {
- variable: {
- handler() {
- this.trackVariableValidationErrors();
- },
- deep: true,
- },
- },
- methods: {
- addVariable() {
- this.$emit('add-variable', this.variable);
- },
- deleteVariable() {
- this.$emit('delete-variable', this.variable);
- },
- updateVariable() {
- this.$emit('update-variable', this.variable);
- },
- dismissTip() {
- setCookie(AWS_TIP_DISMISSED_COOKIE_NAME, 'true', { expires: 90 });
- this.isTipDismissed = true;
- },
- deleteVarAndClose() {
- this.deleteVariable();
- this.hideModal();
- },
- hideModal() {
- this.$refs.modal.hide();
- },
- onShow() {
- this.setVariableProtectedByDefault();
- },
- resetModalHandler() {
- this.resetVariableData();
- this.resetValidationErrorEvents();
-
- this.$emit('close-form');
- },
- resetVariableData() {
- this.variable = { ...defaultVariableState };
- },
- setEnvironmentScope(scope) {
- this.variable = { ...this.variable, environmentScope: scope };
- },
- setVariableRaw(expanded) {
- this.variable = { ...this.variable, raw: !expanded };
- },
- setVariableProtected() {
- this.variable = { ...this.variable, protected: true };
- },
- updateOrAddVariable() {
- if (this.isEditing) {
- this.updateVariable();
- } else {
- this.addVariable();
- }
- this.hideModal();
- },
- setVariableProtectedByDefault() {
- if (this.isProtectedByDefault && !this.isEditing) {
- this.setVariableProtected();
- }
- },
- trackVariableValidationErrors() {
- const property = this.getTrackingErrorProperty();
- if (!this.validationErrorEventProperty && property) {
- this.track(EVENT_ACTION, { property });
- this.validationErrorEventProperty = property;
- }
- },
- getTrackingErrorProperty() {
- let property;
- if (this.variable.value?.length && !property) {
- if (this.displayMaskedError && this.maskableRegex?.length) {
- const supportedChars = this.maskableRegex.replace('^', '').replace(/{(\d,)}\$/, '');
- const regex = new RegExp(supportedChars, 'g');
- property = this.variable.value.replace(regex, '');
- }
- if (this.containsVariableReference) {
- property = '$';
- }
- }
-
- return property;
- },
- resetValidationErrorEvents() {
- this.validationErrorEventProperty = '';
- },
- },
- i18n: {
- awsTipTitle: AWS_TIP_TITLE,
- awsTipMessage: AWS_TIP_MESSAGE,
- containsVariableReferenceMessage: CONTAINS_VARIABLE_REFERENCE_MESSAGE,
- defaultScope: allEnvironments.text,
- environmentScopeLinkTitle: ENVIRONMENT_SCOPE_LINK_TITLE,
- expandedVariablesNote: EXPANDED_VARIABLES_NOTE,
- flagsLinkTitle: FLAG_LINK_TITLE,
- },
- flagLink: helpPagePath('ci/variables/index', {
- anchor: 'define-a-cicd-variable-in-the-ui',
- }),
- oidcLink: helpPagePath('ci/cloud_services/index', {
- anchor: 'oidc-authorization-with-your-cloud-provider',
- }),
- modalId: ADD_CI_VARIABLE_MODAL_ID,
- tokens: awsTokens,
- tokenList: awsTokenList,
- variableOptions,
-};
-</script>
-
-<template>
- <gl-modal
- ref="modal"
- :modal-id="$options.modalId"
- :title="modalActionText"
- static
- lazy
- @hidden="resetModalHandler"
- @shown="onShow"
- >
- <gl-collapse :visible="isTipVisible">
- <gl-alert
- :title="$options.i18n.awsTipTitle"
- variant="warning"
- class="gl-mb-5"
- data-testid="aws-guidance-tip"
- @dismiss="dismissTip"
- >
- <gl-sprintf :message="$options.i18n.awsTipMessage">
- <template #link="{ content }">
- <gl-link :href="$options.oidcLink">
- {{ content }}
- </gl-link>
- </template>
- </gl-sprintf>
- </gl-alert>
- </gl-collapse>
- <form>
- <gl-form-combobox
- v-model="variable.key"
- :token-list="$options.tokenList"
- :label-text="__('Key')"
- data-testid="pipeline-form-ci-variable-key"
- data-qa-selector="ci_variable_key_field"
- />
-
- <gl-form-group
- :label="__('Value')"
- label-for="ci-variable-value"
- :state="variableValidationState"
- :description="variableValueHelpText"
- :invalid-feedback="variableValidationFeedback"
- >
- <gl-form-textarea
- id="ci-variable-value"
- ref="valueField"
- v-model="variable.value"
- :state="variableValidationState"
- rows="3"
- max-rows="10"
- data-testid="pipeline-form-ci-variable-value"
- data-qa-selector="ci_variable_value_field"
- class="gl-font-monospace!"
- spellcheck="false"
- />
- <p v-if="isRaw" class="gl-mt-2 gl-mb-0 text-secondary" data-testid="raw-variable-tip">
- {{ __('Variable value will be evaluated as raw string.') }}
- </p>
- </gl-form-group>
-
- <div class="gl-display-flex">
- <gl-form-group :label="__('Type')" label-for="ci-variable-type" class="gl-w-half gl-mr-5">
- <gl-form-select
- id="ci-variable-type"
- v-model="variable.variableType"
- :options="$options.variableOptions"
- />
- </gl-form-group>
-
- <template v-if="!hideEnvironmentScope">
- <gl-form-group
- label-for="ci-variable-env"
- class="gl-w-half"
- data-testid="environment-scope"
- >
- <template #label>
- <div class="gl-display-flex gl-align-items-center">
- <span class="gl-mr-2">
- {{ __('Environment scope') }}
- </span>
- <gl-link
- class="gl-display-flex"
- :title="$options.i18n.environmentScopeLinkTitle"
- :href="environmentScopeLink"
- target="_blank"
- data-testid="environment-scope-link"
- >
- <gl-icon name="question-o" :size="14" />
- </gl-link>
- </div>
- </template>
- <ci-environments-dropdown
- v-if="areScopedVariablesAvailable"
- :are-environments-loading="areEnvironmentsLoading"
- :selected-environment-scope="variable.environmentScope"
- :environments="environments"
- @select-environment="setEnvironmentScope"
- @search-environment-scope="$emit('search-environment-scope', $event)"
- />
-
- <gl-form-input v-else :value="$options.i18n.defaultScope" class="gl-w-full" readonly />
- </gl-form-group>
- </template>
- </div>
-
- <gl-form-group>
- <template #label>
- <div class="gl-display-flex gl-align-items-center">
- <span class="gl-mr-2">
- {{ __('Flags') }}
- </span>
- <gl-link
- class="gl-display-flex"
- :title="$options.i18n.flagsLinkTitle"
- :href="$options.flagLink"
- target="_blank"
- >
- <gl-icon name="question-o" :size="14" />
- </gl-link>
- </div>
- </template>
- <gl-form-checkbox
- v-model="variable.protected"
- class="gl-mb-0"
- data-testid="ci-variable-protected-checkbox"
- :data-is-protected-checked="variable.protected"
- >
- {{ __('Protect variable') }}
- <p class="gl-mt-2 text-secondary">
- {{ __('Export variable to pipelines running on protected branches and tags only.') }}
- </p>
- </gl-form-checkbox>
- <gl-form-checkbox
- ref="masked-ci-variable"
- v-model="variable.masked"
- data-testid="ci-variable-masked-checkbox"
- >
- {{ __('Mask variable') }}
- <p class="gl-mt-2 text-secondary">
- <gl-sprintf
- :message="
- __(
- 'Mask this variable in job logs if it meets %{linkStart}regular expression requirements%{linkEnd}.',
- )
- "
- >
- <template #link="{ content }"
- ><gl-link target="_blank" :href="maskedEnvironmentVariablesLink">{{
- content
- }}</gl-link>
- </template>
- </gl-sprintf>
- </p>
- </gl-form-checkbox>
- <gl-form-checkbox
- ref="expanded-ci-variable"
- :checked="isExpanded"
- data-testid="ci-variable-expanded-checkbox"
- @change="setVariableRaw"
- >
- {{ __('Expand variable reference') }}
- <p class="gl-mt-2 gl-mb-0 gl-text-secondary">
- <gl-sprintf :message="$options.i18n.expandedVariablesNote">
- <template #code="{ content }">
- <code>{{ content }}</code>
- </template>
- </gl-sprintf>
- </p>
- </gl-form-checkbox>
- </gl-form-group>
- </form>
-
- <gl-alert
- v-if="containsVariableReference"
- :title="__('Value might contain a variable reference')"
- :dismissible="false"
- variant="warning"
- data-testid="contains-variable-reference"
- >
- <gl-sprintf :message="$options.i18n.containsVariableReferenceMessage">
- <template #code="{ content }">
- <code>{{ content }}</code>
- </template>
- <template #docsLink="{ content }">
- <gl-link :href="containsVariableReferenceLink" target="_blank">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
- </gl-alert>
- <template #modal-footer>
- <gl-button @click="hideModal">{{ __('Cancel') }}</gl-button>
- <gl-button
- v-if="isEditing"
- ref="deleteCiVariable"
- variant="danger"
- category="secondary"
- @click="deleteVarAndClose"
- >{{ __('Delete variable') }}</gl-button
- >
- <gl-button
- ref="updateOrAddVariable"
- :disabled="!canSubmit"
- variant="confirm"
- category="primary"
- data-testid="ciUpdateOrAddVariableBtn"
- data-qa-selector="ci_variable_save_button"
- @click="updateOrAddVariable"
- >{{ modalActionText }}
- </gl-button>
- </template>
- </gl-modal>
-</template>
diff --git a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_settings.vue b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_settings.vue
index f2d81b3f271..99270d36df7 100644
--- a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_settings.vue
+++ b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_settings.vue
@@ -3,13 +3,11 @@ import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { ADD_VARIABLE_ACTION, EDIT_VARIABLE_ACTION, VARIABLE_ACTIONS } from '../constants';
import CiVariableDrawer from './ci_variable_drawer.vue';
import CiVariableTable from './ci_variable_table.vue';
-import CiVariableModal from './ci_variable_modal.vue';
export default {
components: {
CiVariableDrawer,
CiVariableTable,
- CiVariableModal,
},
mixins: [glFeatureFlagsMixin()],
props: {
@@ -65,15 +63,6 @@ export default {
showForm() {
return VARIABLE_ACTIONS.includes(this.mode);
},
- useDrawerForm() {
- return this.glFeatures?.ciVariableDrawer;
- },
- showDrawer() {
- return this.showForm && this.useDrawerForm;
- },
- showModal() {
- return this.showForm && !this.useDrawerForm;
- },
},
methods: {
addVariable(variable) {
@@ -116,23 +105,8 @@ export default {
@delete-variable="deleteVariable"
@sort-changed="(val) => $emit('sort-changed', val)"
/>
- <ci-variable-modal
- v-if="showModal"
- :are-environments-loading="areEnvironmentsLoading"
- :are-scoped-variables-available="areScopedVariablesAvailable"
- :environments="environments"
- :hide-environment-scope="hideEnvironmentScope"
- :variables="variables"
- :mode="mode"
- :selected-variable="selectedVariable"
- @add-variable="addVariable"
- @delete-variable="deleteVariable"
- @close-form="closeForm"
- @update-variable="updateVariable"
- @search-environment-scope="$emit('search-environment-scope', $event)"
- />
<ci-variable-drawer
- v-if="showDrawer"
+ v-if="showForm"
:are-environments-loading="areEnvironmentsLoading"
:are-scoped-variables-available="areScopedVariablesAvailable"
:environments="environments"
diff --git a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_table.vue b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_table.vue
index 3d62313815c..86287d586ec 100644
--- a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_table.vue
+++ b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_table.vue
@@ -16,7 +16,6 @@ import {
import { __, s__, sprintf } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import {
- ADD_CI_VARIABLE_MODAL_ID,
DEFAULT_EXCEEDS_VARIABLE_LIMIT_TEXT,
EXCEEDS_VARIABLE_LIMIT_TEXT,
MAXIMUM_VARIABLE_LIMIT_REACHED,
@@ -25,7 +24,6 @@ import {
import { convertEnvironmentScope } from '../utils';
export default {
- modalId: ADD_CI_VARIABLE_MODAL_ID,
defaultFields: [
{
key: 'key',
@@ -243,10 +241,8 @@ export default {
>{{ valuesButtonText }}</gl-button
>
<gl-button
- v-gl-modal-directive="$options.modalId"
size="small"
:disabled="exceedsVariableLimit"
- data-qa-selector="add_ci_variable_button"
data-testid="add-ci-variable-button"
@click="setSelectedVariable()"
>{{ $options.i18n.addButton }}</gl-button
@@ -375,12 +371,11 @@ export default {
<template v-if="!isInheritedGroupVars" #cell(actions)="{ item }">
<div class="gl-display-flex gl-justify-content-end gl-mt-n2 gl-mb-n2">
<gl-button
- v-gl-modal-directive="$options.modalId"
icon="pencil"
size="small"
class="gl-mr-3"
:aria-label="$options.i18n.editButton"
- data-qa-selector="edit_ci_variable_button"
+ data-testid="edit-ci-variable-button"
@click="setSelectedVariable(item.index)"
/>
<gl-button
@@ -390,7 +385,6 @@ export default {
icon="remove"
size="small"
:aria-label="$options.i18n.deleteButton"
- data-qa-selector="delete_ci_variable_button"
/>
<gl-modal
ref="modal"
diff --git a/app/assets/javascripts/ci/ci_variable_list/constants.js b/app/assets/javascripts/ci/ci_variable_list/constants.js
index fc37b62299d..d85827b8220 100644
--- a/app/assets/javascripts/ci/ci_variable_list/constants.js
+++ b/app/assets/javascripts/ci/ci_variable_list/constants.js
@@ -1,6 +1,5 @@
import { __, s__, sprintf } from '~/locale';
-export const ADD_CI_VARIABLE_MODAL_ID = 'add-ci-variable';
export const ENVIRONMENT_QUERY_LIMIT = 30;
export const SORT_DIRECTIONS = {
@@ -45,7 +44,6 @@ export const AWS_TIP_MESSAGE = s__(
'CiVariable|GitLab CI/CD supports OpenID Connect (OIDC) to give your build and deployment jobs access to cloud credentials and services. %{linkStart}How do I configure OIDC for my cloud provider?%{linkEnd}',
);
-export const EVENT_LABEL = 'ci_variable_modal';
export const DRAWER_EVENT_LABEL = 'ci_variable_drawer';
export const EVENT_ACTION = 'validation_error';
diff --git a/app/assets/javascripts/ci/common/pipelines_table.vue b/app/assets/javascripts/ci/common/pipelines_table.vue
index 13b5120654a..d63d2d1713e 100644
--- a/app/assets/javascripts/ci/common/pipelines_table.vue
+++ b/app/assets/javascripts/ci/common/pipelines_table.vue
@@ -13,8 +13,6 @@ import PipelineUrl from '../pipelines_page/components/pipeline_url.vue';
import PipelineStatusBadge from '../pipelines_page/components/pipeline_status_badge.vue';
const HIDE_TD_ON_MOBILE = 'gl-display-none! gl-lg-display-table-cell!';
-const DEFAULT_TH_CLASSES =
- 'gl-bg-transparent! gl-border-b-solid! gl-border-b-gray-100! gl-p-5! gl-border-b-1!';
/**
* Pipelines Table
@@ -77,7 +75,6 @@ export default {
{
key: 'status',
label: s__('Pipeline|Status'),
- thClass: DEFAULT_TH_CLASSES,
columnClass: 'gl-w-15p',
tdClass: this.tdClasses,
thAttr: { 'data-testid': 'status-th' },
@@ -85,7 +82,6 @@ export default {
{
key: 'pipeline',
label: __('Pipeline'),
- thClass: DEFAULT_TH_CLASSES,
tdClass: `${this.tdClasses}`,
columnClass: 'gl-w-30p',
thAttr: { 'data-testid': 'pipeline-th' },
@@ -93,7 +89,6 @@ export default {
{
key: 'triggerer',
label: s__('Pipeline|Created by'),
- thClass: DEFAULT_TH_CLASSES,
tdClass: `${this.tdClasses} ${HIDE_TD_ON_MOBILE}`,
columnClass: 'gl-w-15p',
thAttr: { 'data-testid': 'triggerer-th' },
@@ -101,14 +96,12 @@ export default {
{
key: 'stages',
label: s__('Pipeline|Stages'),
- thClass: DEFAULT_TH_CLASSES,
tdClass: this.tdClasses,
columnClass: 'gl-w-quarter',
thAttr: { 'data-testid': 'stages-th' },
},
{
key: 'actions',
- thClass: DEFAULT_TH_CLASSES,
tdClass: this.tdClasses,
columnClass: 'gl-w-20p',
thAttr: { 'data-testid': 'actions-th' },
@@ -137,8 +130,7 @@ export default {
return cleanLeadingSeparator(item.project.full_path);
},
failedJobsCount(pipeline) {
- // Remove `pipeline?.failed_builds?.length` when we remove `ci_fix_performance_pipelines_json_endpoint`.
- return pipeline?.failed_builds_count || pipeline?.failed_builds?.length || 0;
+ return pipeline?.failed_builds_count || 0;
},
onRefreshPipelinesTable() {
this.$emit('refresh-pipelines-table');
diff --git a/app/assets/javascripts/ci/common/private/job_action_component.vue b/app/assets/javascripts/ci/common/private/job_action_component.vue
index b0fa724d450..c266e061513 100644
--- a/app/assets/javascripts/ci/common/private/job_action_component.vue
+++ b/app/assets/javascripts/ci/common/private/job_action_component.vue
@@ -119,6 +119,7 @@ export default {
ref="button"
:class="cssClass"
:disabled="isDisabled"
+ size="small"
class="js-ci-action gl-ci-action-icon-container ci-action-icon-container ci-action-icon-wrapper gl-display-flex gl-align-items-center gl-justify-content-center"
data-testid="ci-action-button"
@click.stop="onClickAction"
@@ -129,8 +130,17 @@ export default {
class="gl-display-flex gl-align-items-center gl-justify-content-center gl-h-full"
data-testid="ci-action-icon-tooltip-wrapper"
>
- <gl-loading-icon v-if="isLoading" size="sm" class="js-action-icon-loading" />
- <gl-icon v-else :name="actionIcon" class="gl-mr-0!" :aria-label="actionIcon" />
+ <gl-loading-icon
+ v-if="isLoading"
+ size="sm"
+ class="gl-button-icon gl-m-2 js-action-icon-loading"
+ />
+ <gl-icon
+ v-else
+ :name="actionIcon"
+ class="gl-button-icon gl-p-1 gl-mr-0!"
+ :aria-label="actionIcon"
+ />
</div>
</gl-button>
</template>
diff --git a/app/assets/javascripts/ci/common/private/job_links_layer.vue b/app/assets/javascripts/ci/common/private/job_links_layer.vue
index 59260ca3f81..9b3647e9c55 100644
--- a/app/assets/javascripts/ci/common/private/job_links_layer.vue
+++ b/app/assets/javascripts/ci/common/private/job_links_layer.vue
@@ -1,5 +1,6 @@
<script>
import { memoize } from 'lodash';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { reportToSentry } from '~/ci/utils';
import { parseData } from '~/ci/pipeline_details/utils/parsing_utils';
import LinksInner from '~/ci/pipeline_details/graph/components/links_inner.vue';
@@ -16,6 +17,7 @@ export default {
components: {
LinksInner,
},
+ mixins: [glFeatureFlagMixin()],
props: {
containerMeasurements: {
type: Object,
@@ -50,6 +52,9 @@ export default {
showLinkedLayers() {
return this.showLinks && !this.containerZero;
},
+ isNewPipelineGraph() {
+ return this.glFeatures.newPipelineGraph;
+ },
},
errorCaptured(err, _vm, info) {
reportToSentry(this.$options.name, `error: ${err}, info: ${info}`);
@@ -68,7 +73,10 @@ export default {
<slot></slot>
</links-inner>
<div v-else>
- <div class="gl-display-flex gl-relative">
+ <div
+ class="gl-display-flex gl-relative"
+ :class="{ 'gl-flex-wrap gl-sm-flex-nowrap': isNewPipelineGraph }"
+ >
<slot></slot>
</div>
</div>
diff --git a/app/assets/javascripts/ci/common/private/job_name_component.vue b/app/assets/javascripts/ci/common/private/job_name_component.vue
index 1c7f5a7476d..b4e831d69d4 100644
--- a/app/assets/javascripts/ci/common/private/job_name_component.vue
+++ b/app/assets/javascripts/ci/common/private/job_name_component.vue
@@ -30,7 +30,7 @@ export default {
</script>
<template>
<span class="mw-100 gl-display-flex gl-align-items-center gl-flex-grow-1">
- <ci-icon :size="iconSize" :status="status" class="gl-line-height-0" />
+ <ci-icon :size="iconSize" :status="status" :show-tooltip="false" class="gl-line-height-0" />
<span class="gl-text-truncate mw-70p gl-pl-3 gl-display-inline-block">
{{ name }}
</span>
diff --git a/app/assets/javascripts/ci/constants.js b/app/assets/javascripts/ci/constants.js
index 5b60528f521..138a44a8dd0 100644
--- a/app/assets/javascripts/ci/constants.js
+++ b/app/assets/javascripts/ci/constants.js
@@ -37,4 +37,5 @@ export const TRACKING_CATEGORIES = {
search: 'pipelines_filtered_search',
failed: 'pipeline_failed_jobs_tab',
tests: 'pipeline_tests_tab',
+ listbox: 'pipeline_id_iid_listbox',
};
diff --git a/app/assets/javascripts/ci/job_details/components/job_header.vue b/app/assets/javascripts/ci/job_details/components/job_header.vue
index 00d15f87064..1aa83a94bc5 100644
--- a/app/assets/javascripts/ci/job_details/components/job_header.vue
+++ b/app/assets/javascripts/ci/job_details/components/job_header.vue
@@ -4,12 +4,12 @@ import SafeHtml from '~/vue_shared/directives/safe_html';
import { isGid, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { glEmojiTag } from '~/emoji';
import { __, sprintf } from '~/locale';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
export default {
components: {
- CiBadgeLink,
+ CiIcon,
TimeagoTooltip,
GlButton,
GlAvatarLink,
@@ -113,7 +113,7 @@ export default {
</div>
</div>
<section class="header-main-content gl-display-flex gl-align-items-center gl-mr-3">
- <ci-badge-link class="gl-mr-3" :status="status" />
+ <ci-icon class="gl-mr-3" :status="status" show-status-text />
<template v-if="shouldRenderTriggeredLabel">{{ __('Started') }}</template>
<template v-else>{{ __('Created') }}</template>
diff --git a/app/assets/javascripts/ci/job_details/components/log/line_header.vue b/app/assets/javascripts/ci/job_details/components/log/line_header.vue
index 658a94e6af4..d36701323da 100644
--- a/app/assets/javascripts/ci/job_details/components/log/line_header.vue
+++ b/app/assets/javascripts/ci/job_details/components/log/line_header.vue
@@ -17,7 +17,8 @@ export default {
},
isClosed: {
type: Boolean,
- required: true,
+ required: false,
+ default: false,
},
path: {
type: String,
diff --git a/app/assets/javascripts/ci/job_details/components/sidebar/job_container_item.vue b/app/assets/javascripts/ci/job_details/components/sidebar/job_container_item.vue
index 8e87f118fa4..4ec9044a21c 100644
--- a/app/assets/javascripts/ci/job_details/components/sidebar/job_container_item.vue
+++ b/app/assets/javascripts/ci/job_details/components/sidebar/job_container_item.vue
@@ -63,11 +63,11 @@ export default {
<gl-icon
v-if="isActive"
name="arrow-right"
+ :show-tooltip="false"
class="icon-arrow-right gl-absolute gl-display-block"
- :size="14"
/>
- <ci-icon :status="job.status" class="gl-mr-3" :size="14" />
+ <ci-icon :status="job.status" :show-tooltip="false" class="gl-mr-3" />
<span class="gl-text-truncate gl-w-full">{{ jobName }}</span>
diff --git a/app/assets/javascripts/ci/job_details/components/sidebar/stages_dropdown.vue b/app/assets/javascripts/ci/job_details/components/sidebar/stages_dropdown.vue
index 7744395734f..e229abcbe12 100644
--- a/app/assets/javascripts/ci/job_details/components/sidebar/stages_dropdown.vue
+++ b/app/assets/javascripts/ci/job_details/components/sidebar/stages_dropdown.vue
@@ -1,7 +1,7 @@
<script>
import { GlLink, GlDisclosureDropdown, GlSprintf } from '@gitlab/ui';
import { isEmpty } from 'lodash';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { Mousetrap } from '~/lib/mousetrap';
import { s__ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
@@ -14,7 +14,7 @@ export default {
GlDisclosureDropdown,
GlLink,
GlSprintf,
- CiBadgeLink,
+ CiIcon,
},
props: {
pipeline: {
@@ -94,7 +94,10 @@ export default {
</script>
<template>
<div class="dropdown">
- <div class="gl-display-flex gl-flex-wrap gl-gap-2 js-pipeline-info" data-testid="pipeline-info">
+ <div
+ class="gl-display-flex gl-flex-wrap gl-align-items-center gl-gap-2 js-pipeline-info"
+ data-testid="pipeline-info"
+ >
<gl-sprintf :message="pipelineInfo">
<template #bold="{ content }">
<span class="gl-display-flex gl-font-weight-bold">{{ content }}</span>
@@ -108,9 +111,9 @@ export default {
>
</template>
<template #status>
- <ci-badge-link
+ <ci-icon
:status="pipeline.details.status"
- size="sm"
+ show-status-text
data-testid="pipeline-status-link"
/>
</template>
@@ -125,7 +128,7 @@ export default {
<template #ref>
<gl-link
:href="pipeline.ref.path"
- class="link-commit ref-name gl-mt-1"
+ class="link-commit ref-name"
data-testid="source-ref-link"
>{{ pipeline.ref.name }}</gl-link
><clipboard-button
diff --git a/app/assets/javascripts/ci/job_details/job_app.vue b/app/assets/javascripts/ci/job_details/job_app.vue
index 119f8259be7..e0708289b43 100644
--- a/app/assets/javascripts/ci/job_details/job_app.vue
+++ b/app/assets/javascripts/ci/job_details/job_app.vue
@@ -307,7 +307,7 @@ export default {
@scrollJobLogBottom="scrollBottom"
@searchResults="setSearchResults"
/>
- <log :job-log="jobLog" :is-complete="isJobLogComplete" :search-results="searchResults" />
+ <log :search-results="searchResults" />
</div>
<!-- EO job log -->
diff --git a/app/assets/javascripts/ci/job_details/store/actions.js b/app/assets/javascripts/ci/job_details/store/actions.js
index fa23589f7d6..6f538e3b3d4 100644
--- a/app/assets/javascripts/ci/job_details/store/actions.js
+++ b/app/assets/javascripts/ci/job_details/store/actions.js
@@ -175,7 +175,7 @@ export const fetchJobLog = ({ dispatch, state }) =>
}
})
.catch((e) => {
- if (e.response.status === HTTP_STATUS_FORBIDDEN) {
+ if (e.response?.status === HTTP_STATUS_FORBIDDEN) {
dispatch('receiveJobLogUnauthorizedError');
} else {
reportToSentry('job_actions', e);
diff --git a/app/assets/javascripts/ci/job_details/store/utils.js b/app/assets/javascripts/ci/job_details/store/utils.js
index b18a3fa162d..c8b33638821 100644
--- a/app/assets/javascripts/ci/job_details/store/utils.js
+++ b/app/assets/javascripts/ci/job_details/store/utils.js
@@ -117,28 +117,31 @@ export const getNextLineNumber = (acc) => {
* @returns Array parsed log lines
*/
export const logLinesParser = (lines = [], prevLogLines = [], hash = '') =>
- lines.reduce((acc, line) => {
- const lineNumber = getNextLineNumber(acc);
-
- const last = acc[acc.length - 1];
-
- // If the object is an header, we parse it into another structure
- if (line.section_header) {
- acc.push(parseHeaderLine(line, lineNumber, hash));
- } else if (isCollapsibleSection(acc, last, line)) {
- // if the object belongs to a nested section, we append it to the new `lines` array of the
- // previously formatted header
- last.lines.push(parseLine(line, lineNumber));
- } else if (line.section_duration) {
- // if the line has section_duration, we look for the correct header to add it
- addDurationToHeader(acc, line);
- } else {
- // otherwise it's a regular line
- acc.push(parseLine(line, lineNumber));
- }
+ lines.reduce(
+ (acc, line) => {
+ const lineNumber = getNextLineNumber(acc);
+
+ const last = acc[acc.length - 1];
+
+ // If the object is an header, we parse it into another structure
+ if (line.section_header) {
+ acc.push(parseHeaderLine(line, lineNumber, hash));
+ } else if (isCollapsibleSection(acc, last, line)) {
+ // if the object belongs to a nested section, we append it to the new `lines` array of the
+ // previously formatted header
+ last.lines.push(parseLine(line, lineNumber));
+ } else if (line.section_duration) {
+ // if the line has section_duration, we look for the correct header to add it
+ addDurationToHeader(acc, line);
+ } else {
+ // otherwise it's a regular line
+ acc.push(parseLine(line, lineNumber));
+ }
- return acc;
- }, prevLogLines);
+ return acc;
+ },
+ [...prevLogLines],
+ );
/**
* Finds the repeated offset, removes the old one
diff --git a/app/assets/javascripts/ci/jobs_page/components/job_cells/job_cell.vue b/app/assets/javascripts/ci/jobs_page/components/job_cells/job_cell.vue
index fbdfc7c9c6a..b97243cf2ca 100644
--- a/app/assets/javascripts/ci/jobs_page/components/job_cells/job_cell.vue
+++ b/app/assets/javascripts/ci/jobs_page/components/job_cells/job_cell.vue
@@ -136,8 +136,8 @@ export default {
v-if="triggered"
variant="info"
:size="$options.badgeSize"
- data-testid="triggered-job-badge"
- >{{ s__('Job|triggered') }}
+ data-testid="trigger-token-job-badge"
+ >{{ s__('Job|trigger token') }}
</gl-badge>
<gl-badge
v-if="showAllowedToFailBadge"
diff --git a/app/assets/javascripts/ci/jobs_page/components/job_cells/status_cell.vue b/app/assets/javascripts/ci/jobs_page/components/job_cells/status_cell.vue
index a2b6a430138..efa74d86bd6 100644
--- a/app/assets/javascripts/ci/jobs_page/components/job_cells/status_cell.vue
+++ b/app/assets/javascripts/ci/jobs_page/components/job_cells/status_cell.vue
@@ -1,14 +1,14 @@
<script>
import { GlIcon } from '@gitlab/ui';
import { formatTime } from '~/lib/utils/datetime_utility';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
export default {
iconSize: 12,
components: {
- CiBadgeLink,
+ CiIcon,
GlIcon,
TimeAgoTooltip,
},
@@ -38,7 +38,7 @@ export default {
<template>
<div>
- <ci-badge-link :status="job.detailedStatus" />
+ <ci-icon :status="job.detailedStatus" show-status-text />
<div class="gl-font-sm gl-text-secondary gl-mt-2 gl-ml-3">
<div v-if="duration" data-testid="job-duration">
<gl-icon name="timer" :size="$options.iconSize" data-testid="duration-icon" />
diff --git a/app/assets/javascripts/ci/jobs_page/jobs_page_app.vue b/app/assets/javascripts/ci/jobs_page/jobs_page_app.vue
index 03e0f2dadc8..09bbb7afbca 100644
--- a/app/assets/javascripts/ci/jobs_page/jobs_page_app.vue
+++ b/app/assets/javascripts/ci/jobs_page/jobs_page_app.vue
@@ -58,9 +58,6 @@ export default {
},
jobsCount: {
query: GetJobsCount,
- context: {
- isSingleRequest: true,
- },
variables() {
return {
fullPath: this.fullPath,
diff --git a/app/assets/javascripts/ci/pipeline_details/constants.js b/app/assets/javascripts/ci/pipeline_details/constants.js
index 70b758ae6b0..51d0e980e78 100644
--- a/app/assets/javascripts/ci/pipeline_details/constants.js
+++ b/app/assets/javascripts/ci/pipeline_details/constants.js
@@ -2,7 +2,6 @@ import { __, s__ } from '~/locale';
export const CANCEL_REQUEST = 'CANCEL_REQUEST';
export const SUPPORTED_FILTER_PARAMETERS = ['username', 'ref', 'status', 'source'];
-export const SCHEDULE_ORIGIN = 'schedule';
export const NEEDS_PROPERTY = 'needs';
export const EXPLICIT_NEEDS_PROPERTY = 'previousStageJobsOrNeeds';
diff --git a/app/assets/javascripts/ci/pipeline_details/graph/components/graph_component.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/graph_component.vue
index f098d790736..3da2f27c1b9 100644
--- a/app/assets/javascripts/ci/pipeline_details/graph/components/graph_component.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/graph_component.vue
@@ -4,6 +4,7 @@ import {
generateColumnsFromLayersListMemoized,
keepLatestDownstreamPipelines,
} from '~/ci/pipeline_details/utils/parsing_utils';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import LinksLayer from '../../../common/private/job_links_layer.vue';
import { DOWNSTREAM, MAIN, UPSTREAM, ONE_COL_WIDTH, STAGE_VIEW } from '../constants';
import { validateConfigPaths } from '../utils';
@@ -19,6 +20,7 @@ export default {
LinkedPipelinesColumn,
StageColumnComponent,
},
+ mixins: [glFeatureFlagMixin()],
props: {
configPaths: {
type: Object,
@@ -132,6 +134,9 @@ export default {
upstreamPipelines() {
return this.hasUpstreamPipelines ? this.pipeline.upstream : [];
},
+ isNewPipelineGraph() {
+ return this.glFeatures.newPipelineGraph;
+ },
},
errorCaptured(err, _vm, info) {
reportToSentry(this.$options.name, `error: ${err}, info: ${info}`);
@@ -178,10 +183,15 @@ export default {
<div class="js-pipeline-graph">
<div
ref="mainPipelineContainer"
- class="gl-display-flex gl-position-relative gl-bg-gray-10 gl-white-space-nowrap"
+ class="pipeline-graph gl-display-flex gl-position-relative gl-white-space-nowrap gl-rounded-lg"
:class="{
- 'gl-pipeline-min-h gl-py-5 gl-overflow-auto': !isLinkedPipeline,
+ 'gl-bg-gray-10': !isNewPipelineGraph,
+ 'gl-pipeline-min-h gl-py-5 gl-overflow-auto': !isNewPipelineGraph && !isLinkedPipeline,
+ 'pipeline-graph-container gl-bg-gray-10 gl-pipeline-min-h gl-align-items-flex-start gl-pt-3 gl-pb-8 gl-mt-3 gl-overflow-auto':
+ isNewPipelineGraph && !isLinkedPipeline,
+ 'gl-bg-gray-50 gl-sm-ml-5': isNewPipelineGraph && isLinkedPipeline,
}"
+ data-testid="pipeline-container"
>
<linked-graph-wrapper>
<template #upstream>
@@ -199,7 +209,7 @@ export default {
/>
</template>
<template #main>
- <div :id="containerId" :ref="containerId">
+ <div :id="containerId" :ref="containerId" class="pipeline-links-container">
<links-layer
:pipeline-data="layout"
:pipeline-id="pipeline.id"
@@ -238,7 +248,7 @@ export default {
<template #downstream>
<linked-pipelines-column
v-if="showDownstreamPipelines"
- class="gl-mr-6"
+ :class="{ 'gl-sm-ml-3': isNewPipelineGraph }"
:config-paths="configPaths"
:linked-pipelines="downstreamPipelines"
:column-title="__('Downstream')"
diff --git a/app/assets/javascripts/ci/pipeline_details/graph/components/graph_view_selector.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/graph_view_selector.vue
index fb7dcb300f1..114b224fbe7 100644
--- a/app/assets/javascripts/ci/pipeline_details/graph/components/graph_view_selector.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/graph_view_selector.vue
@@ -1,11 +1,11 @@
<script>
import { GlAlert, GlButton, GlButtonGroup, GlLoadingIcon, GlToggle } from '@gitlab/ui';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { __, s__ } from '~/locale';
import { STAGE_VIEW, LAYER_VIEW } from '../constants';
export default {
name: 'GraphViewSelector',
-
components: {
GlAlert,
GlButton,
@@ -13,7 +13,7 @@ export default {
GlLoadingIcon,
GlToggle,
},
-
+ mixins: [glFeatureFlagMixin()],
props: {
showLinks: {
type: Boolean,
@@ -77,6 +77,9 @@ export default {
};
});
},
+ isNewPipelineGraph() {
+ return this.glFeatures.newPipelineGraph;
+ },
},
watch: {
/*
@@ -138,7 +141,13 @@ export default {
<template>
<div>
- <div class="gl-relative gl-display-flex gl-align-items-center gl-w-max-content gl-my-4">
+ <div
+ class="gl-relative gl-display-flex gl-align-items-center gl-my-4"
+ :class="{
+ 'gl-w-max-content': !isNewPipelineGraph,
+ 'gl-flex-wrap gl-sm-flex-nowrap': isNewPipelineGraph,
+ }"
+ >
<gl-loading-icon
v-if="isSwitcherLoading"
data-testid="switcher-loading-state"
@@ -161,7 +170,10 @@ export default {
<gl-toggle
v-model="showLinksActive"
data-testid="show-links-toggle"
- class="gl-mx-4"
+ :class="{
+ 'gl-mx-4': !isNewPipelineGraph,
+ 'gl-sm-ml-4 gl-mt-4 gl-sm-mt-0': isNewPipelineGraph,
+ }"
:label="$options.i18n.linksLabelText"
:is-loading="isToggleLoading"
label-position="left"
diff --git a/app/assets/javascripts/ci/pipeline_details/graph/components/job_item.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/job_item.vue
index bb36ac8b6ab..c6340e6787a 100644
--- a/app/assets/javascripts/ci/pipeline_details/graph/components/job_item.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/job_item.vue
@@ -5,7 +5,7 @@ import delayedJobMixin from '~/ci/mixins/delayed_job_mixin';
import { helpPagePath } from '~/helpers/help_page_helper';
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { __, s__, sprintf } from '~/locale';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import ActionComponent from '../../../common/private/job_action_component.vue';
import JobNameComponent from '../../../common/private/job_name_component.vue';
import { BRIDGE_KIND, RETRY_ACTION_TITLE, SINGLE_JOB, SKIP_RETRY_MODAL_KEY } from '../constants';
@@ -58,7 +58,7 @@ export default {
hoverClass: 'gl-shadow-x0-y0-b3-s1-blue-500',
components: {
ActionComponent,
- CiBadgeLink,
+ CiIcon,
GlBadge,
GlForm,
GlFormCheckbox,
@@ -329,7 +329,7 @@ export default {
@mouseout="hideTooltips"
>
<div class="gl-display-flex gl-align-items-center gl-flex-grow-1">
- <ci-badge-link :status="job.status" size="md" :show-text="false" :use-link="false" />
+ <ci-icon :status="job.status" :use-link="false" />
<div class="gl-pl-3 gl-pr-3 gl-display-flex gl-flex-direction-column gl-pipeline-job-width">
<div class="gl-text-truncate gl-pr-9 gl-line-height-normal">{{ job.name }}</div>
<div
diff --git a/app/assets/javascripts/ci/pipeline_details/graph/components/linked_graph_wrapper.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/linked_graph_wrapper.vue
index fb2280d971a..0d72373a0f5 100644
--- a/app/assets/javascripts/ci/pipeline_details/graph/components/linked_graph_wrapper.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/linked_graph_wrapper.vue
@@ -1,5 +1,20 @@
+<script>
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+
+export default {
+ mixins: [glFeatureFlagMixin()],
+ computed: {
+ isNewPipelineGraph() {
+ return this.glFeatures.newPipelineGraph;
+ },
+ },
+};
+</script>
<template>
- <div class="gl-display-flex">
+ <div
+ class="gl-display-flex"
+ :class="{ 'gl-flex-wrap gl-sm-flex-nowrap gl-w-full': isNewPipelineGraph }"
+ >
<slot name="upstream"></slot>
<slot name="main"></slot>
<slot name="downstream"></slot>
diff --git a/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipeline.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipeline.vue
index 5960eea5b4f..26521f87426 100644
--- a/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipeline.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipeline.vue
@@ -7,13 +7,14 @@ import {
GlTooltip,
GlTooltipDirective,
} from '@gitlab/ui';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { TYPENAME_CI_PIPELINE } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { __, sprintf } from '~/locale';
import CancelPipelineMutation from '~/ci/pipeline_details/graphql/mutations/cancel_pipeline.mutation.graphql';
import RetryPipelineMutation from '~/ci/pipeline_details/graphql/mutations/retry_pipeline.mutation.graphql';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { reportToSentry } from '~/ci/utils';
import { ACTION_FAILURE, DOWNSTREAM, UPSTREAM } from '../constants';
@@ -22,13 +23,14 @@ export default {
GlTooltip: GlTooltipDirective,
},
components: {
- CiBadgeLink,
+ CiIcon,
GlBadge,
GlButton,
GlLink,
GlLoadingIcon,
GlTooltip,
},
+ mixins: [glFeatureFlagMixin()],
styles: {
actionSizeClasses: ['gl-h-7 gl-w-7'],
flatLeftBorder: ['gl-rounded-bottom-left-none!', 'gl-rounded-top-left-none!'],
@@ -115,9 +117,6 @@ export default {
downstreamTitle() {
return this.childPipeline ? this.sourceJobName : this.pipeline.project.name;
},
- flexDirection() {
- return this.isUpstream ? 'gl-flex-direction-row-reverse' : 'gl-flex-direction-row';
- },
graphqlPipelineId() {
return convertToGraphQLId(TYPENAME_CI_PIPELINE, this.pipeline.id);
},
@@ -176,6 +175,9 @@ export default {
return `${this.downstreamTitle} #${this.pipeline.id} - ${this.pipelineStatus.label} -
${this.sourceJobInfo}`;
},
+ isNewPipelineGraph() {
+ return this.glFeatures.newPipelineGraph;
+ },
},
errorCaptured(err, _vm, info) {
reportToSentry('linked_pipeline', `error: ${err}, info: ${info}`);
@@ -231,9 +233,15 @@ export default {
<template>
<div
ref="linkedPipeline"
- class="gl-h-full gl-display-flex! gl-px-2"
- :class="flexDirection"
+ class="linked-pipeline-container gl-h-full gl-display-flex!"
+ :class="{
+ 'gl-flex-direction-row-reverse': isUpstream,
+ 'gl-flex-direction-row': !isUpstream,
+ 'gl-px-2': !isNewPipelineGraph,
+ 'gl-w-full gl-sm-w-auto': isNewPipelineGraph,
+ }"
data-testid="linked-pipeline-container"
+ :aria-expanded="expanded"
@mouseover="onDownstreamHovered"
@mouseleave="onDownstreamHoverLeave"
>
@@ -242,17 +250,15 @@ export default {
</gl-tooltip>
<div class="gl-bg-white gl-border gl-p-3 gl-rounded-lg gl-w-full" :class="cardClasses">
<div class="gl-display-flex gl-gap-x-3">
- <ci-badge-link
+ <ci-icon
v-if="!pipelineIsLoading"
:status="pipelineStatus"
- size="md"
- :show-text="false"
:use-link="false"
class="gl-align-self-start"
/>
<div v-else class="gl-pr-3"><gl-loading-icon size="sm" inline /></div>
<div
- class="gl-display-flex gl-downstream-pipeline-job-width gl-flex-direction-column gl-line-height-normal"
+ class="gl-display-flex gl-flex-direction-column gl-line-height-normal gl-downstream-pipeline-job-width"
>
<span class="gl-text-truncate" data-testid="downstream-title-content">
{{ downstreamTitle }}
diff --git a/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipelines_column.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipelines_column.vue
index 2de7e43c9b1..395770826d8 100644
--- a/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipelines_column.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipelines_column.vue
@@ -1,4 +1,5 @@
<script>
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql';
import { reportToSentry } from '~/ci/utils';
import { LOAD_FAILURE } from '../../constants';
@@ -18,6 +19,7 @@ export default {
LinkedPipeline,
PipelineGraph: () => import('./graph_component.vue'),
},
+ mixins: [glFeatureFlagMixin()],
props: {
columnTitle: {
type: String,
@@ -63,23 +65,30 @@ export default {
'gl-pipeline-job-width',
'gl-text-truncate',
'gl-line-height-36',
- 'gl-pl-3',
- 'gl-mb-5',
],
minWidth: `${ONE_COL_WIDTH}px`,
computed: {
columnClass() {
- const positionValues = {
+ const positionValuesOld = {
right: 'gl-ml-6',
left: 'gl-mx-6',
};
+ const positionValues = {
+ right: 'gl-mx-5',
+ left: 'gl-mx-4 gl-flex-basis-full',
+ };
+ const usePositionValues = this.isNewPipelineGraph ? positionValues : positionValuesOld;
- return `graph-position-${this.graphPosition} ${positionValues[this.graphPosition]}`;
+ return `graph-position-${this.graphPosition} ${usePositionValues[this.graphPosition]}`;
},
computedTitleClasses() {
const positionalClasses = this.isUpstream ? ['gl-w-full', 'gl-linked-pipeline-padding'] : [];
- return [...this.$options.titleClasses, ...positionalClasses];
+ return [
+ ...this.$options.titleClasses,
+ !this.isNewPipelineGraph ?? ['gl-pl-3', 'gl-mb-5'],
+ ...positionalClasses,
+ ];
},
graphPosition() {
return this.isUpstream ? 'left' : 'right';
@@ -93,6 +102,9 @@ export default {
minWidth() {
return this.isUpstream ? 0 : this.$options.minWidth;
},
+ isNewPipelineGraph() {
+ return this.glFeatures.newPipelineGraph;
+ },
},
methods: {
getPipelineData(pipeline) {
@@ -197,7 +209,7 @@ export default {
</script>
<template>
- <div class="gl-display-flex">
+ <div class="gl-display-flex" :class="{ 'gl-w-full gl-sm-w-auto': isNewPipelineGraph }">
<div :class="columnClass" class="linked-pipelines-column">
<div data-testid="linked-column-title" :class="computedTitleClasses">
{{ columnTitle }}
@@ -206,8 +218,12 @@ export default {
<li
v-for="pipeline in linkedPipelines"
:key="pipeline.id"
- class="gl-display-flex gl-mb-3"
- :class="{ 'gl-flex-direction-row-reverse': isUpstream }"
+ class="gl-display-flex"
+ :class="{
+ 'gl-mb-3': !isNewPipelineGraph,
+ 'gl-flex-wrap gl-sm-flex-nowrap gl-mb-6': isNewPipelineGraph,
+ 'gl-flex-direction-row-reverse': !isNewPipelineGraph && isUpstream,
+ }"
>
<linked-pipeline
class="gl-display-inline-block"
@@ -224,12 +240,15 @@ export default {
<div
v-if="showContainer(pipeline.id)"
:style="{ minWidth }"
- class="gl-display-inline-block"
+ class="gl-display-inline-block pipeline-show-container"
>
<pipeline-graph
v-if="isExpanded(pipeline.id)"
:type="type"
- class="gl-inline-block gl-mt-n2"
+ class="gl-inline-block"
+ :class="{
+ 'gl-mt-n2': !isNewPipelineGraph,
+ }"
:config-paths="configPaths"
:pipeline="currentPipeline"
:computed-pipeline-info="getPipelineLayers(pipeline.id)"
diff --git a/app/assets/javascripts/ci/pipeline_details/graph/components/root_graph_layout.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/root_graph_layout.vue
index bcd7705669e..7c07591d0de 100644
--- a/app/assets/javascripts/ci/pipeline_details/graph/components/root_graph_layout.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/root_graph_layout.vue
@@ -1,5 +1,12 @@
<script>
+import { GlCard } from '@gitlab/ui';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+
export default {
+ components: {
+ GlCard,
+ },
+ mixins: [glFeatureFlagMixin()],
props: {
stageClasses: {
type: String,
@@ -12,18 +19,37 @@ export default {
default: '',
},
},
+ computed: {
+ isNewPipelineGraph() {
+ return this.glFeatures.newPipelineGraph;
+ },
+ },
};
</script>
<template>
<div>
- <div class="gl-display-flex gl-align-items-center gl-w-full gl-mb-5" :class="stageClasses">
- <slot name="stages"> </slot>
- </div>
- <div
- class="gl-display-flex gl-flex-direction-column gl-align-items-center gl-w-full"
- :class="jobClasses"
+ <gl-card
+ v-if="isNewPipelineGraph"
+ class="gl-rounded-lg"
+ header-class="gl-rounded-lg gl-px-0 gl-py-0 gl-bg-white gl-border-b-0"
+ body-class="gl-pt-2 gl-pb-0 gl-px-2"
>
- <slot name="jobs"> </slot>
- </div>
+ <template #header>
+ <slot name="stages"></slot>
+ </template>
+
+ <slot name="jobs"></slot>
+ </gl-card>
+ <template v-else>
+ <div class="gl-display-flex gl-align-items-center gl-w-full" :class="stageClasses">
+ <slot name="stages"> </slot>
+ </div>
+ <div
+ class="gl-display-flex gl-flex-direction-column gl-align-items-center gl-w-full"
+ :class="jobClasses"
+ >
+ <slot name="jobs"> </slot>
+ </div>
+ </template>
</div>
</template>
diff --git a/app/assets/javascripts/ci/pipeline_details/graph/components/stage_column_component.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/stage_column_component.vue
index 6030adc96ad..01a9c6d030d 100644
--- a/app/assets/javascripts/ci/pipeline_details/graph/components/stage_column_component.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/stage_column_component.vue
@@ -68,7 +68,7 @@ export default {
required: true,
},
},
- jobClasses: [
+ legacyJobClasses: [
'gl-p-3',
'gl-border-gray-100',
'gl-border-solid',
@@ -82,18 +82,43 @@ export default {
'gl-hover-border-gray-200',
'gl-focus-border-gray-200',
],
- titleClasses: [
+ jobClasses: [
+ 'gl-p-3',
+ 'gl-border-0',
+ 'gl-bg-transparent',
+ 'gl-rounded-base',
+ 'gl-hover-bg-gray-50',
+ 'gl-focus-bg-gray-50',
+ 'gl-hover-text-gray-900',
+ 'gl-focus-text-gray-900',
+ ],
+ legacyTitleClasses: [
'gl-font-weight-bold',
'gl-pipeline-job-width',
'gl-text-truncate',
'gl-line-height-36',
'gl-pl-3',
],
+ titleClasses: [
+ 'gl-font-weight-bold',
+ 'gl-pipeline-job-width',
+ 'gl-text-truncate',
+ 'gl-line-height-36',
+ 'gl-pl-4',
+ 'gl-mb-n2',
+ ],
computed: {
canUpdatePipeline() {
return this.userPermissions.updatePipeline;
},
columnSpacingClass() {
+ if (this.isNewPipelineGraph) {
+ const baseClasses = 'stage-column gl-relative gl-flex-basis-full';
+ return this.isStageView
+ ? `${baseClasses} is-stage-view gl-m-5`
+ : `${baseClasses} gl-my-5 gl-mx-7`;
+ }
+
return this.isStageView ? 'gl-px-6' : 'gl-px-9';
},
hasAction() {
@@ -102,6 +127,17 @@ export default {
showStageName() {
return !this.isStageView;
},
+ isNewPipelineGraph() {
+ return this.glFeatures.newPipelineGraph;
+ },
+ jobClasses() {
+ return this.isNewPipelineGraph ? this.$options.jobClasses : this.$options.legacyJobClasses;
+ },
+ titleClasses() {
+ return this.isNewPipelineGraph
+ ? this.$options.titleClasses
+ : this.$options.legacyTitleClasses;
+ },
},
errorCaptured(err, _vm, info) {
reportToSentry('stage_column_component', `error: ${err}, info: ${info}`);
@@ -135,12 +171,16 @@ export default {
};
</script>
<template>
- <root-graph-layout :class="columnSpacingClass" data-testid="stage-column">
+ <root-graph-layout
+ :class="columnSpacingClass"
+ class="stage-column gl-relative gl-flex-basis-full"
+ data-testid="stage-column"
+ >
<template #stages>
<div
data-testid="stage-column-title"
- class="gl-display-flex gl-justify-content-space-between gl-relative"
- :class="$options.titleClasses"
+ class="stage-column-title gl-display-flex gl-justify-content-space-between gl-relative"
+ :class="titleClasses"
>
<span :title="name" class="gl-text-truncate gl-pr-3 gl-w-85p">
{{ name }}
@@ -161,7 +201,11 @@ export default {
:id="groupId(group)"
:key="getGroupId(group)"
data-testid="stage-column-group"
- class="gl-relative gl-mb-3 gl-white-space-normal gl-pipeline-job-width"
+ class="gl-relative gl-white-space-normal gl-pipeline-job-width"
+ :class="{
+ 'gl-mb-3': !isNewPipelineGraph,
+ 'gl-mb-2': isNewPipelineGraph,
+ }"
@mouseenter="$emit('jobHover', group.name)"
@mouseleave="$emit('jobHover', '')"
>
@@ -174,7 +218,7 @@ export default {
:pipeline-expanded="pipelineExpanded"
:pipeline-id="pipelineId"
:stage-name="showStageName ? group.stageName : ''"
- :css-class-job-name="$options.jobClasses"
+ :css-class-job-name="jobClasses"
:class="[
{ 'gl-opacity-3': isFadedOut(group.name) },
'gl-transition-duration-slow gl-transition-timing-function-ease',
@@ -188,7 +232,7 @@ export default {
:group="group"
:stage-name="showStageName ? group.stageName : ''"
:pipeline-id="pipelineId"
- :css-class-job-name="$options.jobClasses"
+ :css-class-job-name="jobClasses"
/>
</div>
</div>
diff --git a/app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue b/app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue
index 51a68f6619a..651662d6395 100644
--- a/app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue
+++ b/app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue
@@ -17,7 +17,7 @@ import { setUrlFragment, redirectTo } from '~/lib/utils/url_utility'; // eslint-
import { __, s__, sprintf, formatNumber } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import SafeHtml from '~/vue_shared/directives/safe_html';
import { LOAD_FAILURE, POST_FAILURE, DELETE_FAILURE, DEFAULT } from '../constants';
@@ -38,7 +38,7 @@ export default {
pipelineRetry: 'pipelineRetry',
finishedStatuses: ['FAILED', 'SUCCESS', 'CANCELED'],
components: {
- CiBadgeLink,
+ CiIcon,
ClipboardButton,
GlAlert,
GlBadge,
@@ -58,13 +58,17 @@ export default {
i18n: {
scheduleBadgeText: s__('Pipelines|Scheduled'),
scheduleBadgeTooltip: __('This pipeline was created by a schedule'),
+ triggerBadgeText: __('trigger token'),
+ triggerBadgeTooltip: __(
+ 'This pipeline was created by an API call authenticated with a trigger token',
+ ),
childBadgeText: s__('Pipelines|Child pipeline (%{linkStart}parent%{linkEnd})'),
childBadgeTooltip: __('This is a child pipeline within the parent pipeline'),
latestBadgeText: s__('Pipelines|latest'),
latestBadgeTooltip: __('Latest pipeline for the most recent commit on this branch'),
mergeTrainBadgeText: s__('Pipelines|merge train'),
mergeTrainBadgeTooltip: s__(
- 'Pipelines|This pipeline ran on the contents of this merge request combined with the contents of all other merge requests queued for merging into the target branch.',
+ 'Pipelines|This pipeline ran on the contents of the merge request combined with the contents of all other merge requests queued for merging into the target branch.',
),
invalidBadgeText: s__('Pipelines|yaml invalid'),
failedBadgeText: s__('Pipelines|error'),
@@ -74,7 +78,11 @@ export default {
),
detachedBadgeText: s__('Pipelines|merge request'),
detachedBadgeTooltip: s__(
- "Pipelines|This pipeline ran on the contents of this merge request's source branch, not the target branch.",
+ "Pipelines|This pipeline ran on the contents of the merge request's source branch, not the target branch.",
+ ),
+ mergedResultsBadgeText: s__('Pipelines|merged results'),
+ mergedResultsBadgeTooltip: s__(
+ 'Pipelines|This pipeline ran on the contents of the merge request combined with the contents of the target branch.',
),
stuckBadgeText: s__('Pipelines|stuck'),
stuckBadgeTooltip: s__('Pipelines|This pipeline is stuck'),
@@ -403,7 +411,7 @@ export default {
{{ commitTitle }}
</h3>
<div>
- <ci-badge-link :status="detailedStatus" class="gl-display-inline-block gl-mb-3" />
+ <ci-icon :status="detailedStatus" show-status-text :show-link="false" class="gl-mb-3" />
<div class="gl-ml-2 gl-mb-3 gl-display-inline-block gl-h-6">
<gl-link
v-if="user"
@@ -458,6 +466,15 @@ export default {
{{ $options.i18n.scheduleBadgeText }}
</gl-badge>
<gl-badge
+ v-if="badges.trigger"
+ v-gl-tooltip
+ :title="$options.i18n.triggerBadgeTooltip"
+ variant="info"
+ size="sm"
+ >
+ {{ $options.i18n.triggerBadgeText }}
+ </gl-badge>
+ <gl-badge
v-if="badges.child"
v-gl-tooltip
:title="$options.i18n.childBadgeTooltip"
@@ -527,6 +544,15 @@ export default {
{{ $options.i18n.detachedBadgeText }}
</gl-badge>
<gl-badge
+ v-if="badges.mergedResultsPipeline"
+ v-gl-tooltip
+ :title="$options.i18n.mergedResultsBadgeTooltip"
+ variant="info"
+ size="sm"
+ >
+ {{ $options.i18n.mergedResultsBadgeText }}
+ </gl-badge>
+ <gl-badge
v-if="badges.stuck"
v-gl-tooltip
:title="$options.i18n.stuckBadgeTooltip"
diff --git a/app/assets/javascripts/ci/pipeline_details/jobs/components/failed_jobs_table.vue b/app/assets/javascripts/ci/pipeline_details/jobs/components/failed_jobs_table.vue
index 4752fbb3e96..287f6e045c6 100644
--- a/app/assets/javascripts/ci/pipeline_details/jobs/components/failed_jobs_table.vue
+++ b/app/assets/javascripts/ci/pipeline_details/jobs/components/failed_jobs_table.vue
@@ -5,7 +5,7 @@ import { __, s__ } from '~/locale';
import { createAlert } from '~/alert';
import Tracking from '~/tracking';
import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { TRACKING_CATEGORIES } from '~/ci/constants';
import RetryFailedJobMutation from '../graphql/mutations/retry_failed_job.mutation.graphql';
import { DEFAULT_FIELDS } from '../../constants';
@@ -14,7 +14,7 @@ export default {
fields: DEFAULT_FIELDS,
retry: __('Retry'),
components: {
- CiBadgeLink,
+ CiIcon,
GlButton,
GlLink,
GlTableLite,
@@ -80,7 +80,7 @@ export default {
<div
class="gl-display-flex gl-align-items-center gl-lg-justify-content-start gl-justify-content-end"
>
- <ci-badge-link :status="item.detailedStatus" :show-text="false" class="gl-mr-3" />
+ <ci-icon :status="item.detailedStatus" class="gl-mr-3" />
<div class="gl-text-truncate">
<gl-link
:href="item.detailedStatus.detailsPath"
diff --git a/app/assets/javascripts/ci/pipeline_details/pipeline_details_header.js b/app/assets/javascripts/ci/pipeline_details/pipeline_details_header.js
index 067ec3f305e..4966b657887 100644
--- a/app/assets/javascripts/ci/pipeline_details/pipeline_details_header.js
+++ b/app/assets/javascripts/ci/pipeline_details/pipeline_details_header.js
@@ -23,9 +23,11 @@ export const createPipelineDetailsHeaderApp = (elSelector, apolloProvider, graph
failureReason,
triggeredByPath,
schedule,
+ trigger,
child,
latest,
mergeTrainPipeline,
+ mergedResultsPipeline,
invalid,
failed,
autoDevops,
@@ -59,9 +61,11 @@ export const createPipelineDetailsHeaderApp = (elSelector, apolloProvider, graph
refText,
badges: {
schedule: parseBoolean(schedule),
+ trigger: parseBoolean(trigger),
child: parseBoolean(child),
latest: parseBoolean(latest),
mergeTrainPipeline: parseBoolean(mergeTrainPipeline),
+ mergedResultsPipeline: parseBoolean(mergedResultsPipeline),
invalid: parseBoolean(invalid),
failed: parseBoolean(failed),
autoDevops: parseBoolean(autoDevops),
diff --git a/app/assets/javascripts/ci/pipeline_details/pipeline_shared_client.js b/app/assets/javascripts/ci/pipeline_details/pipeline_shared_client.js
index c3be487caae..63a46d81dd5 100644
--- a/app/assets/javascripts/ci/pipeline_details/pipeline_shared_client.js
+++ b/app/assets/javascripts/ci/pipeline_details/pipeline_shared_client.js
@@ -2,10 +2,5 @@ import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
export const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(
- {},
- {
- useGet: true,
- },
- ),
+ defaultClient: createDefaultClient(),
});
diff --git a/app/assets/javascripts/ci/pipeline_details/pipelines_index.js b/app/assets/javascripts/ci/pipeline_details/pipelines_index.js
index 8a7c3367fc1..ea2875713a9 100644
--- a/app/assets/javascripts/ci/pipeline_details/pipelines_index.js
+++ b/app/assets/javascripts/ci/pipeline_details/pipelines_index.js
@@ -42,8 +42,6 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => {
projectId,
defaultBranchName,
params,
- iosRunnersAvailable,
- registrationToken,
fullPath,
visibilityPipelineIdType,
} = el.dataset;
@@ -55,7 +53,6 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => {
artifactsEndpoint,
artifactsEndpointPlaceholder,
fullPath,
- iosRunnersAvailable: parseBoolean(iosRunnersAvailable),
manualActionsLimit: 50,
pipelineEditorPath,
pipelineSchedulesPath,
@@ -84,7 +81,6 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => {
newPipelinePath,
params: JSON.parse(params),
projectId,
- registrationToken,
resetCachePath,
store: this.store,
},
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/editor/ci_editor_header.vue b/app/assets/javascripts/ci/pipeline_editor/components/editor/ci_editor_header.vue
index 8f4d566e7e6..204eaf20664 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/editor/ci_editor_header.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/editor/ci_editor_header.vue
@@ -80,7 +80,7 @@ export default {
<template>
<div
- class="gl-display-flex gl-p-3 gl-gap-3 gl-border-solid gl-border-gray-100 gl-border-1 gl-sm-flex-direction-column"
+ class="gl-display-flex gl-p-3 gl-gap-3 gl-border-solid gl-border-gray-100 gl-border-1 gl-flex-direction-column gl-md-flex-direction-row"
>
<slot></slot>
<gl-button
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/file_nav/branch_switcher.vue b/app/assets/javascripts/ci/pipeline_editor/components/file_nav/branch_switcher.vue
index 221a45d4d9a..21e21d54758 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/file_nav/branch_switcher.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/file_nav/branch_switcher.vue
@@ -1,13 +1,5 @@
<script>
-import {
- GlDropdown,
- GlDropdownItem,
- GlDropdownSectionHeader,
- GlInfiniteScroll,
- GlLoadingIcon,
- GlSearchBoxByType,
- GlTooltipDirective,
-} from '@gitlab/ui';
+import { GlCollapsibleListbox, GlTooltipDirective } from '@gitlab/ui';
import { produce } from 'immer';
import { historyPushState } from '~/lib/utils/common_utils';
import { setUrlParams } from '~/lib/utils/url_utility';
@@ -25,17 +17,11 @@ import getLastCommitBranch from '~/ci/pipeline_editor/graphql/queries/client/las
export default {
i18n: {
dropdownHeader: __('Switch branch'),
- title: __('Branches'),
fetchError: __('Unable to fetch branch list for this project.'),
},
inputDebounce: BRANCH_SEARCH_DEBOUNCE,
components: {
- GlDropdown,
- GlDropdownItem,
- GlDropdownSectionHeader,
- GlInfiniteScroll,
- GlLoadingIcon,
- GlSearchBoxByType,
+ GlCollapsibleListbox,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -66,6 +52,7 @@ export default {
pageCounter: 0,
searchTerm: '',
lastCommitBranch: '',
+ infiniteScrollLoading: false,
};
},
apollo: {
@@ -112,6 +99,18 @@ export default {
},
},
computed: {
+ infiniteScrollEnabled() {
+ return this.availableBranches.length > 0;
+ },
+ branchesData() {
+ return this.availableBranches.map((branch) => ({
+ text: branch,
+ extraAttrs: {
+ 'data-qa-selector': 'branch_menu_item_button',
+ },
+ value: branch,
+ }));
+ },
availableBranchesVariables() {
if (this.searchTerm.length > 0) {
return {
@@ -128,7 +127,7 @@ export default {
enableBranchSwitcher() {
return this.availableBranches.length > 0 || this.searchTerm.length > 0;
},
- isBranchesLoading() {
+ areBranchesLoading() {
return this.$apollo.queries.availableBranches.loading;
},
},
@@ -143,7 +142,7 @@ export default {
// if there is no searchPattern, paginate by {paginationLimit} branches
fetchNextBranches() {
if (
- this.isBranchesLoading ||
+ this.areBranchesLoading ||
this.searchTerm.length > 0 ||
this.availableBranches.length >= this.totalBranches
) {
@@ -178,16 +177,14 @@ export default {
this.$emit('refetchContent');
},
selectBranch(newBranch) {
- if (newBranch !== this.currentBranch) {
- // If there are unsaved changes, we want to show the user
- // a modal to confirm what to do with these before changing
- // branches.
- if (this.hasUnsavedChanges) {
- this.branchSelected = newBranch;
- this.$emit('select-branch', newBranch);
- } else {
- this.changeBranch(newBranch);
- }
+ // If there are unsaved changes, we want to show the user
+ // a modal to confirm what to do with these before changing
+ // branches.
+ if (this.hasUnsavedChanges) {
+ this.branchSelected = newBranch;
+ this.$emit('select-branch', newBranch);
+ } else {
+ this.changeBranch(newBranch);
}
},
async setSearchTerm(newSearchTerm) {
@@ -211,41 +208,23 @@ export default {
</script>
<template>
- <gl-dropdown
+ <gl-collapsible-listbox
+ v-model="currentBranch"
v-gl-tooltip.hover
+ data-qa-selector="branch_selector_button"
+ searchable
+ :items="branchesData"
:title="$options.i18n.dropdownHeader"
:header-text="$options.i18n.dropdownHeader"
- :text="currentBranch"
+ :toggle-text="currentBranch"
:disabled="!enableBranchSwitcher"
icon="branch"
data-testid="branch-selector"
- >
- <gl-search-box-by-type :debounce="$options.inputDebounce" @input="setSearchTerm" />
- <gl-dropdown-section-header>
- {{ $options.i18n.title }}
- </gl-dropdown-section-header>
-
- <gl-infinite-scroll
- :fetched-items="availableBranches.length"
- :max-list-height="250"
- @bottomReached="fetchNextBranches"
- >
- <template #items>
- <gl-dropdown-item
- v-for="branch in availableBranches"
- :key="branch"
- :is-checked="currentBranch === branch"
- is-check-item
- @click="selectBranch(branch)"
- >
- {{ branch }}
- </gl-dropdown-item>
- </template>
- <template #default>
- <gl-dropdown-item v-if="isBranchesLoading" key="loading">
- <gl-loading-icon size="lg" />
- </gl-dropdown-item>
- </template>
- </gl-infinite-scroll>
- </gl-dropdown>
+ :no-results-text="$options.i18n.fetchError"
+ :infinite-scroll-loading="areBranchesLoading"
+ :infinite-scroll="infiniteScrollEnabled"
+ @select="selectBranch"
+ @search="setSearchTerm"
+ @bottom-reached="fetchNextBranches"
+ />
</template>
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_status.vue b/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_status.vue
index 44cf11acfe2..7c4a07e3f83 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_status.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_status.vue
@@ -7,7 +7,7 @@ import getPipelineQuery from '~/ci/pipeline_editor/graphql/queries/pipeline.quer
import getPipelineEtag from '~/ci/pipeline_editor/graphql/queries/client/pipeline_etag.query.graphql';
import { getQueryHeaders, toggleQueryPollingByVisibility } from '~/ci/pipeline_details/graph/utils';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import PipelineMiniGraph from '~/ci/pipeline_mini_graph/pipeline_mini_graph.vue';
import PipelineEditorMiniGraph from './pipeline_editor_mini_graph.vue';
@@ -25,7 +25,7 @@ export const i18n = {
export default {
i18n,
components: {
- CiBadgeLink,
+ CiIcon,
GlButton,
GlIcon,
GlLink,
@@ -155,14 +155,7 @@ export default {
</template>
<template v-else>
<div class="gl-text-truncate gl-md-max-w-50p gl-mr-1">
- <a :href="status.detailsPath" class="gl-mr-auto">
- <ci-badge-link
- :status="status"
- size="md"
- :show-text="false"
- data-testid="pipeline-status-icon"
- />
- </a>
+ <ci-icon :status="status" data-testid="pipeline-status-icon" />
<span class="gl-font-weight-bold">
<gl-sprintf :message="$options.i18n.pipelineInfo">
<template #id="{ content }">
diff --git a/app/assets/javascripts/ci/pipeline_editor/options.js b/app/assets/javascripts/ci/pipeline_editor/options.js
index 922c8eee8fc..340cb6ab979 100644
--- a/app/assets/javascripts/ci/pipeline_editor/options.js
+++ b/app/assets/javascripts/ci/pipeline_editor/options.js
@@ -55,7 +55,6 @@ export const createAppOptions = (el) => {
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(resolvers, {
typeDefs,
- useGet: true,
}),
});
const { cache } = apolloProvider.clients.defaultClient;
diff --git a/app/assets/javascripts/ci/pipeline_editor/pipeline_editor_home.vue b/app/assets/javascripts/ci/pipeline_editor/pipeline_editor_home.vue
index 41e5199e204..09ba6292e13 100644
--- a/app/assets/javascripts/ci/pipeline_editor/pipeline_editor_home.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/pipeline_editor_home.vue
@@ -168,7 +168,7 @@ export default {
@toggle-file-tree="toggleFileTree"
v-on="$listeners"
/>
- <div class="gl-display-flex gl-w-full gl-sm-flex-direction-column">
+ <div class="gl-display-flex gl-w-full gl-flex-direction-column gl-md-flex-direction-row">
<pipeline-editor-file-tree
v-if="showFileTree"
class="gl-flex-shrink-0"
diff --git a/app/assets/javascripts/ci/pipeline_mini_graph/legacy_job_item.vue b/app/assets/javascripts/ci/pipeline_mini_graph/legacy_job_item.vue
index d20d4aec59d..4fded3aec60 100644
--- a/app/assets/javascripts/ci/pipeline_mini_graph/legacy_job_item.vue
+++ b/app/assets/javascripts/ci/pipeline_mini_graph/legacy_job_item.vue
@@ -132,7 +132,6 @@ export default {
<template>
<div
class="ci-job-component gl-display-flex gl-align-items-center gl-justify-content-space-between"
- data-qa-selector="job_item_container"
>
<gl-link
v-if="hasDetails"
diff --git a/app/assets/javascripts/ci/pipeline_mini_graph/legacy_pipeline_stage.vue b/app/assets/javascripts/ci/pipeline_mini_graph/legacy_pipeline_stage.vue
index 34640d49b80..ed78a335453 100644
--- a/app/assets/javascripts/ci/pipeline_mini_graph/legacy_pipeline_stage.vue
+++ b/app/assets/javascripts/ci/pipeline_mini_graph/legacy_pipeline_stage.vue
@@ -13,7 +13,7 @@
*/
import { GlDropdown, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { createAlert } from '~/alert';
import eventHub from '~/ci/event_hub';
import axios from '~/lib/utils/axios_utils';
@@ -33,7 +33,7 @@ export default {
positionFixed: true,
},
components: {
- CiBadgeLink,
+ CiIcon,
GlLoadingIcon,
GlDropdown,
LegacyJobItem,
@@ -126,14 +126,7 @@ export default {
@show="onShowDropdown"
>
<template #button-content>
- <ci-badge-link
- :status="stage.status"
- size="md"
- :show-text="false"
- :show-tooltip="false"
- :use-link="false"
- class="gl-mb-0!"
- />
+ <ci-icon :status="stage.status" :show-tooltip="false" :use-link="false" class="gl-mb-0!" />
</template>
<div v-if="isLoading" class="gl--flex-center gl-p-2" data-testid="pipeline-stage-loading-state">
<gl-loading-icon size="sm" class="gl-mr-3" />
diff --git a/app/assets/javascripts/ci/pipeline_mini_graph/linked_pipelines_mini_list.vue b/app/assets/javascripts/ci/pipeline_mini_graph/linked_pipelines_mini_list.vue
index cc703d29e23..f6a375ab94c 100644
--- a/app/assets/javascripts/ci/pipeline_mini_graph/linked_pipelines_mini_list.vue
+++ b/app/assets/javascripts/ci/pipeline_mini_graph/linked_pipelines_mini_list.vue
@@ -1,7 +1,7 @@
<script>
import { GlTooltipDirective } from '@gitlab/ui';
import { sprintf, s__ } from '~/locale';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { accessValue } from './accessors/linked_pipelines_accessors';
/**
* Renders the upstream/downstream portions of the pipeline mini graph.
@@ -11,7 +11,7 @@ export default {
GlTooltip: GlTooltipDirective,
},
components: {
- CiBadgeLink,
+ CiIcon,
},
inject: {
dataMethod: {
@@ -81,11 +81,6 @@ export default {
// detailedStatus is graphQL, details.status is REST
return pipeline?.detailedStatus || pipeline?.details?.status;
},
- triggerButtonClass(pipeline) {
- const { group } = accessValue(pipeline, this.dataMethod, 'detailedStatus');
-
- return `ci-status-icon-${group}`;
- },
},
};
</script>
@@ -99,15 +94,12 @@ export default {
}"
class="linked-pipeline-mini-list gl-display-inline gl-vertical-align-middle"
>
- <ci-badge-link
+ <ci-icon
v-for="pipeline in linkedPipelinesTrimmed"
:key="pipeline.id"
v-gl-tooltip="{ title: pipelineTooltipText(pipeline) }"
:status="pipelineStatus(pipeline)"
- size="md"
- :show-text="false"
:show-tooltip="false"
- :class="triggerButtonClass(pipeline)"
class="linked-pipeline-mini-item gl-mb-0!"
data-testid="linked-pipeline-mini-item"
/>
diff --git a/app/assets/javascripts/ci/pipeline_new/components/pipeline_new_form.vue b/app/assets/javascripts/ci/pipeline_new/components/pipeline_new_form.vue
index 2f06b82bac0..722dc29d746 100644
--- a/app/assets/javascripts/ci/pipeline_new/components/pipeline_new_form.vue
+++ b/app/assets/javascripts/ci/pipeline_new/components/pipeline_new_form.vue
@@ -13,9 +13,9 @@ import {
GlSprintf,
GlLoadingIcon,
} from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
import { uniqueId } from 'lodash';
import Vue from 'vue';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { fetchPolicies } from '~/lib/graphql';
import SafeHtml from '~/vue_shared/directives/safe_html';
import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
diff --git a/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue
index cd1d9a97ef3..5444e66cbdf 100644
--- a/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue
+++ b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue
@@ -371,11 +371,7 @@ export default {
</gl-form-group>
<!--Variable List-->
<gl-form-group class="gl-mb-0" :label="$options.i18n.variables">
- <div
- v-for="(variable, index) in variables"
- :key="`var-${index}`"
- data-qa-selector="ci_variable_row_container"
- >
+ <div v-for="(variable, index) in variables" :key="`var-${index}`">
<div
v-if="!variable.destroy"
class="gl-display-flex gl-align-items-stretch gl-flex-direction-column gl-md-flex-direction-row gl-mb-3 gl-pb-2"
diff --git a/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions.vue b/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions.vue
index ed7c2bbeb73..78df7298f4f 100644
--- a/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions.vue
+++ b/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions.vue
@@ -61,6 +61,7 @@ export default {
v-if="canPlay"
v-gl-tooltip
:title="$options.i18n.playTooltip"
+ :aria-label="$options.i18n.playTooltip"
icon="play"
data-testid="play-pipeline-schedule-btn"
@click="$emit('playPipelineSchedule', schedule.id)"
@@ -78,6 +79,7 @@ export default {
v-gl-tooltip
:href="editPathWithIdParam"
:title="$options.i18n.editTooltip"
+ :aria-label="$options.i18n.editTooltip"
icon="pencil"
data-testid="edit-pipeline-schedule-btn"
/>
@@ -85,6 +87,7 @@ export default {
v-if="canRemove"
v-gl-tooltip
:title="$options.i18n.deleteTooltip"
+ :aria-label="$options.i18n.deleteTooltip"
icon="remove"
variant="danger"
data-testid="delete-pipeline-schedule-btn"
diff --git a/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue b/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue
index 92f461c72d7..d979c0efaf2 100644
--- a/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue
+++ b/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue
@@ -1,9 +1,9 @@
<script>
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
export default {
components: {
- CiBadgeLink,
+ CiIcon,
},
props: {
schedule: {
@@ -24,9 +24,10 @@ export default {
<template>
<div data-testid="last-pipeline-status">
- <ci-badge-link
+ <ci-icon
v-if="hasPipeline"
:status="lastPipelineStatus"
+ show-status-text
class="gl-vertical-align-middle"
/>
<span v-else data-testid="pipeline-schedule-status-text">
diff --git a/app/assets/javascripts/ci/pipelines_page/components/empty_state/ios_templates.vue b/app/assets/javascripts/ci/pipelines_page/components/empty_state/ios_templates.vue
deleted file mode 100644
index 1a2021df9c8..00000000000
--- a/app/assets/javascripts/ci/pipelines_page/components/empty_state/ios_templates.vue
+++ /dev/null
@@ -1,220 +0,0 @@
-<script>
-import { GlButton, GlCard, GlSprintf, GlLink, GlPopover, GlModalDirective } from '@gitlab/ui';
-import { s__ } from '~/locale';
-import { helpPagePath } from '~/helpers/help_page_helper';
-import { mergeUrlParams, DOCS_URL } from '~/lib/utils/url_utility';
-import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue';
-import apolloProvider from '~/ci/pipeline_details/graphql/provider';
-import CiTemplates from './ci_templates.vue';
-
-export default {
- components: {
- GlButton,
- GlCard,
- GlSprintf,
- GlLink,
- GlPopover,
- RunnerInstructionsModal,
- CiTemplates,
- },
- directives: {
- GlModalDirective,
- },
- inject: ['pipelineEditorPath', 'iosRunnersAvailable'],
- props: {
- registrationToken: {
- type: String,
- required: false,
- default: null,
- },
- },
- apolloProvider,
- iOSTemplateName: 'iOS-Fastlane',
- modalId: 'runner-instructions-modal',
- runnerDocsLink: `${DOCS_URL}/runner/install/osx`,
- whatElseLink: helpPagePath('ci/index.md'),
- i18n: {
- title: s__('Pipelines|Get started with GitLab CI/CD'),
- subtitle: s__('Pipelines|Building for iOS?'),
- explanation: s__("Pipelines|We'll walk you through how to deploy to iOS in two easy steps."),
- runnerSetupTitle: s__('Pipelines|1. Set up a runner'),
- runnerSetupButton: s__('Pipelines|Set up a runner'),
- runnerSetupBodyUnfinished: s__(
- 'Pipelines|GitLab Runner is an application that works with GitLab CI/CD to run jobs in a pipeline.',
- ),
- runnerSetupBodyFinished: s__(
- 'Pipelines|You have runners available to run your job now. No need to do anything else.',
- ),
- runnerSetupPopoverTitle: s__(
- "Pipelines|Let's get that runner set up! %{emojiStart}tada%{emojiEnd}",
- ),
- runnerSetupPopoverBodyLine1: s__(
- 'Pipelines|Follow these instructions to install GitLab Runner on macOS.',
- ),
- runnerSetupPopoverBodyLine2: s__(
- 'Pipelines|Need more information to set up your runner? %{linkStart}Check out our documentation%{linkEnd}.',
- ),
- configurePipelineTitle: s__('Pipelines|2. Configure deployment pipeline'),
- configurePipelineBody: s__("Pipelines|We'll guide you through a simple pipeline set-up."),
- configurePipelineButton: s__('Pipelines|Configure pipeline'),
- noWalkthroughTitle: s__("Pipelines|Don't need a guide? Jump in right away with a template."),
- noWalkthroughExplanation: s__('Pipelines|Based on your project, we recommend this template:'),
- notBuildingForIos: s__(
- "Pipelines|Not building for iOS or not what you're looking for? %{linkStart}See what else%{linkEnd} GitLab CI/CD has to offer.",
- ),
- },
- data() {
- return {
- isModalShown: false,
- isPopoverShown: false,
- isRunnerSetupFinished: this.iosRunnersAvailable,
- popoverTarget: `${this.$options.modalId}___BV_modal_content_`,
- configurePipelineLink: mergeUrlParams(
- { template: this.$options.iOSTemplateName },
- this.pipelineEditorPath,
- ),
- };
- },
- computed: {
- runnerSetupBodyText() {
- return this.iosRunnersAvailable
- ? this.$options.i18n.runnerSetupBodyFinished
- : this.$options.i18n.runnerSetupBodyUnfinished;
- },
- },
- methods: {
- showModal() {
- this.isModalShown = true;
- },
- hideModal() {
- this.togglePopover();
- this.isRunnerSetupFinished = true;
- },
- togglePopover() {
- this.isPopoverShown = !this.isPopoverShown;
- },
- },
-};
-</script>
-
-<template>
- <div>
- <h2 class="gl-font-size-h2 gl-text-gray-900">{{ $options.i18n.title }}</h2>
- <h3 class="gl-font-lg gl-text-gray-900 gl-mt-1">{{ $options.i18n.subtitle }}</h3>
- <p>{{ $options.i18n.explanation }}</p>
-
- <div class="gl-lg-display-flex">
- <div class="gl-lg-display-flex gl-lg-w-25p gl-lg-pr-4 gl-mb-4">
- <gl-card body-class="gl-display-flex gl-flex-grow-1">
- <div
- class="gl-display-flex gl-flex-grow-1 gl-flex-direction-column gl-justify-content-space-between gl-align-items-flex-start"
- >
- <div>
- <div class="gl-py-5">
- <gl-emoji
- v-show="isRunnerSetupFinished"
- class="gl-font-size-h2-xl"
- data-name="white_check_mark"
- data-testid="runner-setup-marked-completed"
- />
- <gl-emoji
- v-show="!isRunnerSetupFinished"
- class="gl-font-size-h2-xl"
- data-name="tools"
- data-testid="runner-setup-marked-todo"
- />
- </div>
- <span class="gl-text-gray-800 gl-font-weight-bold">
- {{ $options.i18n.runnerSetupTitle }}
- </span>
- <p class="gl-font-sm gl-mt-3">{{ runnerSetupBodyText }}</p>
- </div>
-
- <gl-button
- v-if="!iosRunnersAvailable"
- v-gl-modal-directive="$options.modalId"
- category="primary"
- variant="confirm"
- @click="showModal"
- >
- {{ $options.i18n.runnerSetupButton }}
- </gl-button>
- <runner-instructions-modal
- v-if="isModalShown"
- :modal-id="$options.modalId"
- :registration-token="registrationToken"
- default-platform-name="osx"
- @shown="togglePopover"
- @hide="hideModal"
- />
- <gl-popover
- v-if="isPopoverShown"
- :show="true"
- :show-close-button="true"
- :target="popoverTarget"
- triggers="manual"
- placement="left"
- fallback-placement="clockwise"
- >
- <template #title>
- <gl-sprintf :message="$options.i18n.runnerSetupPopoverTitle">
- <template #emoji="{ content }">
- <gl-emoji class="gl-ml-2" :data-name="content" />
- </template>
- </gl-sprintf>
- </template>
- <div class="gl-mb-5">
- {{ $options.i18n.runnerSetupPopoverBodyLine1 }}
- </div>
- <gl-sprintf :message="$options.i18n.runnerSetupPopoverBodyLine2">
- <template #link="{ content }">
- <gl-link :href="$options.runnerDocsLink" target="_blank">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
- </gl-popover>
- </div>
- </gl-card>
- </div>
- <div class="gl-lg-display-flex gl-lg-w-25p gl-lg-pr-4 gl-mb-4">
- <gl-card body-class="gl-display-flex gl-flex-grow-1">
- <div
- class="gl-display-flex gl-flex-grow-1 gl-flex-direction-column gl-justify-content-space-between gl-align-items-flex-start"
- >
- <div>
- <div class="gl-py-5"><gl-emoji class="gl-font-size-h2-xl" data-name="tools" /></div>
- <span class="gl-text-gray-800 gl-font-weight-bold">
- {{ $options.i18n.configurePipelineTitle }}
- </span>
- <p class="gl-font-sm gl-mt-3">{{ $options.i18n.configurePipelineBody }}</p>
- </div>
-
- <gl-button
- :disabled="!isRunnerSetupFinished"
- category="primary"
- variant="confirm"
- data-testid="configure-pipeline-link"
- :href="configurePipelineLink"
- >
- {{ $options.i18n.configurePipelineButton }}
- </gl-button>
- </div>
- </gl-card>
- </div>
- </div>
- <h3 class="gl-font-lg gl-text-gray-900 gl-mt-5">{{ $options.i18n.noWalkthroughTitle }}</h3>
- <p>{{ $options.i18n.noWalkthroughExplanation }}</p>
- <ci-templates
- :filter-templates="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ [
- $options.iOSTemplateName,
- ] /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
- :disabled="!isRunnerSetupFinished"
- />
- <p>
- <gl-sprintf :message="$options.i18n.notBuildingForIos">
- <template #link="{ content }">
- <gl-link :href="$options.whatElseLink">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
- </p>
- </div>
-</template>
diff --git a/app/assets/javascripts/ci/pipelines_page/components/empty_state/no_ci_empty_state.vue b/app/assets/javascripts/ci/pipelines_page/components/empty_state/no_ci_empty_state.vue
index 728e8541ae3..aed5f1d235d 100644
--- a/app/assets/javascripts/ci/pipelines_page/components/empty_state/no_ci_empty_state.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/empty_state/no_ci_empty_state.vue
@@ -1,9 +1,7 @@
<script>
import { GlEmptyState } from '@gitlab/ui';
import { s__ } from '~/locale';
-import GitlabExperiment from '~/experimentation/components/gitlab_experiment.vue';
import PipelinesCiTemplates from './pipelines_ci_templates.vue';
-import IosTemplates from './ios_templates.vue';
export default {
i18n: {
@@ -12,9 +10,7 @@ export default {
name: 'PipelinesEmptyState',
components: {
GlEmptyState,
- GitlabExperiment,
PipelinesCiTemplates,
- IosTemplates,
},
props: {
emptyStateSvgPath: {
@@ -25,30 +21,15 @@ export default {
type: Boolean,
required: true,
},
- registrationToken: {
- type: String,
- required: false,
- default: null,
- },
},
};
</script>
<template>
- <div>
- <gitlab-experiment v-if="canSetCi" name="ios_specific_templates">
- <template #control>
- <pipelines-ci-templates />
- </template>
- <template #candidate>
- <ios-templates :registration-token="registrationToken" />
- </template>
- </gitlab-experiment>
- <gl-empty-state
- v-else
- title=""
- :svg-path="emptyStateSvgPath"
- :svg-height="null"
- :description="$options.i18n.noCiDescription"
- />
- </div>
+ <pipelines-ci-templates v-if="canSetCi" />
+ <gl-empty-state
+ v-else
+ :svg-path="emptyStateSvgPath"
+ :svg-height="null"
+ :description="$options.i18n.noCiDescription"
+ />
</template>
diff --git a/app/assets/javascripts/ci/pipelines_page/components/pipeline_labels.vue b/app/assets/javascripts/ci/pipelines_page/components/pipeline_labels.vue
index 8f45094eb74..31d8f207a63 100644
--- a/app/assets/javascripts/ci/pipelines_page/components/pipeline_labels.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/pipeline_labels.vue
@@ -1,7 +1,7 @@
<script>
import { GlLink, GlPopover, GlSprintf, GlTooltipDirective, GlBadge } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
-import { SCHEDULE_ORIGIN } from '~/ci/pipeline_details/constants';
+import { SCHEDULE_ORIGIN, API_ORIGIN, TRIGGER_ORIGIN } from '../constants';
export default {
components: {
@@ -31,6 +31,9 @@ export default {
isScheduled() {
return this.pipeline.source === SCHEDULE_ORIGIN;
},
+ isTriggered() {
+ return this.pipeline.source === TRIGGER_ORIGIN;
+ },
isInFork() {
return Boolean(
this.targetProjectFullPath &&
@@ -50,6 +53,9 @@ export default {
autoDevopsHelpPath() {
return helpPagePath('topics/autodevops/index.md');
},
+ isApi() {
+ return this.pipeline.source === API_ORIGIN;
+ },
},
};
</script>
@@ -64,7 +70,16 @@ export default {
variant="info"
size="sm"
data-testid="pipeline-url-scheduled"
- >{{ __('Scheduled') }}</gl-badge
+ >{{ __('scheduled') }}</gl-badge
+ >
+ <gl-badge
+ v-if="isTriggered"
+ v-gl-tooltip
+ :title="__('This pipeline was created by an API call authenticated with a trigger token')"
+ variant="info"
+ size="sm"
+ data-testid="pipeline-url-triggered"
+ >{{ __('trigger token') }}</gl-badge
>
<gl-badge
v-if="pipeline.flags.latest"
@@ -185,5 +200,14 @@ export default {
data-testid="pipeline-url-fork"
>{{ __('fork') }}</gl-badge
>
+ <gl-badge
+ v-if="isApi"
+ v-gl-tooltip
+ :title="__('This pipeline was triggered using the api')"
+ variant="info"
+ size="sm"
+ data-testid="pipeline-api-badge"
+ >{{ s__('Pipeline|api') }}</gl-badge
+ >
</div>
</template>
diff --git a/app/assets/javascripts/ci/pipelines_page/components/pipeline_status_badge.vue b/app/assets/javascripts/ci/pipelines_page/components/pipeline_status_badge.vue
index 20e2c7e9dce..380f8ce172f 100644
--- a/app/assets/javascripts/ci/pipelines_page/components/pipeline_status_badge.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/pipeline_status_badge.vue
@@ -1,12 +1,12 @@
<script>
import { TRACKING_CATEGORIES } from '~/ci/constants';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import Tracking from '~/tracking';
import PipelinesTimeago from './time_ago.vue';
export default {
components: {
- CiBadgeLink,
+ CiIcon,
PipelinesTimeago,
},
mixins: [Tracking.mixin()],
@@ -31,7 +31,12 @@ export default {
<template>
<div>
- <ci-badge-link class="gl-mb-3" :status="pipelineStatus" @ciStatusBadgeClick="trackClick" />
+ <ci-icon
+ class="gl-mb-2"
+ :status="pipelineStatus"
+ show-status-text
+ @ciStatusBadgeClick="trackClick"
+ />
<pipelines-timeago :pipeline="pipeline" />
</div>
</template>
diff --git a/app/assets/javascripts/ci/pipelines_page/components/pipeline_triggerer.vue b/app/assets/javascripts/ci/pipelines_page/components/pipeline_triggerer.vue
index 2a73795db0a..a53c7cacae2 100644
--- a/app/assets/javascripts/ci/pipelines_page/components/pipeline_triggerer.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/pipeline_triggerer.vue
@@ -29,9 +29,5 @@ export default {
<gl-avatar-link v-if="user" v-gl-tooltip :href="user.path" :title="user.name" class="gl-ml-3">
<gl-avatar :size="32" :src="user.avatar_url" />
</gl-avatar-link>
-
- <span v-else class="gl-ml-3">
- {{ s__('Pipelines|API') }}
- </span>
</div>
</template>
diff --git a/app/assets/javascripts/ci/pipelines_page/constants.js b/app/assets/javascripts/ci/pipelines_page/constants.js
index aa6ef8a25ee..438eda44afe 100644
--- a/app/assets/javascripts/ci/pipelines_page/constants.js
+++ b/app/assets/javascripts/ci/pipelines_page/constants.js
@@ -1,2 +1,5 @@
export const ANY_TRIGGER_AUTHOR = 'Any';
export const FILTER_PIPELINES_SEARCH_DELAY = 200;
+export const SCHEDULE_ORIGIN = 'schedule';
+export const API_ORIGIN = 'api';
+export const TRIGGER_ORIGIN = 'trigger';
diff --git a/app/assets/javascripts/ci/pipelines_page/pipelines.vue b/app/assets/javascripts/ci/pipelines_page/pipelines.vue
index faa013079be..98e005a162f 100644
--- a/app/assets/javascripts/ci/pipelines_page/pipelines.vue
+++ b/app/assets/javascripts/ci/pipelines_page/pipelines.vue
@@ -4,7 +4,7 @@ import NO_PIPELINES_SVG from '@gitlab/svgs/dist/illustrations/empty-state/empty-
import ERROR_STATE_SVG from '@gitlab/svgs/dist/illustrations/pipelines_failed.svg?url';
import { GlEmptyState, GlIcon, GlLoadingIcon, GlCollapsibleListbox } from '@gitlab/ui';
import { isEqual } from 'lodash';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { createAlert, VARIANT_INFO, VARIANT_WARNING } from '~/alert';
import { getParameterByName } from '~/lib/utils/url_utility';
import { __, s__ } from '~/locale';
@@ -88,11 +88,6 @@ export default {
type: Object,
required: true,
},
- registrationToken: {
- type: String,
- required: false,
- default: null,
- },
defaultVisibilityPipelineIdType: {
type: String,
required: false,
@@ -311,6 +306,12 @@ export default {
},
changeVisibilityPipelineIDType(idType) {
this.visibilityPipelineIdType = idType;
+ if (idType === PIPELINE_IID_KEY) {
+ this.track('pipelines_display_options', {
+ label: TRACKING_CATEGORIES.listbox,
+ property: idType,
+ });
+ }
if (isLoggedIn()) {
this.saveVisibilityPipelineIDType(idType);
@@ -404,7 +405,6 @@ export default {
v-else-if="stateToRender === $options.stateMap.emptyState"
:empty-state-svg-path="$options.noPipelinesSvgPath"
:can-set-ci="canCreatePipeline"
- :registration-token="registrationToken"
/>
<gl-empty-state
diff --git a/app/assets/javascripts/ci/runner/admin_new_runner/admin_new_runner_app.vue b/app/assets/javascripts/ci/runner/admin_new_runner/admin_new_runner_app.vue
index f0a41a5949e..97163c1f55c 100644
--- a/app/assets/javascripts/ci/runner/admin_new_runner/admin_new_runner_app.vue
+++ b/app/assets/javascripts/ci/runner/admin_new_runner/admin_new_runner_app.vue
@@ -4,7 +4,6 @@ import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import RegistrationCompatibilityAlert from '~/ci/runner/components/registration/registration_compatibility_alert.vue';
-import RegistrationFeedbackBanner from '~/ci/runner/components/registration/registration_feedback_banner.vue';
import RunnerPlatformsRadioGroup from '~/ci/runner/components/runner_platforms_radio_group.vue';
import RunnerCreateForm from '~/ci/runner/components/runner_create_form.vue';
import { DEFAULT_PLATFORM, PARAM_KEY_PLATFORM, INSTANCE_TYPE } from '../constants';
@@ -14,7 +13,6 @@ export default {
name: 'AdminNewRunnerApp',
components: {
RegistrationCompatibilityAlert,
- RegistrationFeedbackBanner,
RunnerPlatformsRadioGroup,
RunnerCreateForm,
},
@@ -44,8 +42,6 @@ export default {
<template>
<div>
- <registration-feedback-banner />
-
<h1 class="gl-font-size-h2">{{ s__('Runners|New instance runner') }}</h1>
<registration-compatibility-alert :alert-key="$options.INSTANCE_TYPE" />
diff --git a/app/assets/javascripts/ci/runner/admin_runners/admin_runners_app.vue b/app/assets/javascripts/ci/runner/admin_runners/admin_runners_app.vue
index 0ec94dc865f..1431f156c0e 100644
--- a/app/assets/javascripts/ci/runner/admin_runners/admin_runners_app.vue
+++ b/app/assets/javascripts/ci/runner/admin_runners/admin_runners_app.vue
@@ -14,6 +14,7 @@ import {
import allRunnersQuery from 'ee_else_ce/ci/runner/graphql/list/all_runners.query.graphql';
import allRunnersCountQuery from 'ee_else_ce/ci/runner/graphql/list/all_runners_count.query.graphql';
+import RunnerListHeader from '../components/runner_list_header.vue';
import RegistrationDropdown from '../components/registration/registration_dropdown.vue';
import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue';
import RunnerList from '../components/runner_list.vue';
@@ -28,6 +29,7 @@ import RunnerJobStatusBadge from '../components/runner_job_status_badge.vue';
import { pausedTokenConfig } from '../components/search_tokens/paused_token_config';
import { statusTokenConfig } from '../components/search_tokens/status_token_config';
import { tagTokenConfig } from '../components/search_tokens/tag_token_config';
+import { versionTokenConfig } from '../components/search_tokens/version_token_config';
import {
ADMIN_FILTERED_SEARCH_NAMESPACE,
INSTANCE_TYPE,
@@ -42,6 +44,7 @@ export default {
components: {
GlButton,
GlLink,
+ RunnerListHeader,
RegistrationDropdown,
RunnerFilteredSearchBar,
RunnerList,
@@ -78,9 +81,6 @@ export default {
apollo: {
runners: {
query: allRunnersQuery,
- context: {
- isSingleRequest: true,
- },
fetchPolicy: fetchPolicies.NETWORK_ONLY,
variables() {
return this.variables;
@@ -118,6 +118,7 @@ export default {
return [
pausedTokenConfig,
statusTokenConfig,
+ versionTokenConfig,
{
...tagTokenConfig,
recentSuggestionsStorageKey: `${this.$options.filteredSearchNamespace}-recent-tags`,
@@ -178,11 +179,9 @@ export default {
</script>
<template>
<div>
- <header class="gl-my-5 gl-display-flex gl-justify-content-space-between">
- <h2 class="gl-my-0 header-title">
- {{ s__('Runners|Runners') }}
- </h2>
- <div class="gl-display-flex gl-gap-3">
+ <runner-list-header>
+ <template #title>{{ s__('Runners|Runners') }}</template>
+ <template #actions>
<runner-dashboard-link />
<gl-button :href="newRunnerPath" variant="confirm">
{{ s__('Runners|New instance runner') }}
@@ -192,8 +191,9 @@ export default {
:type="$options.INSTANCE_TYPE"
placement="right"
/>
- </div>
- </header>
+ </template>
+ </runner-list-header>
+
<div
class="gl-display-flex gl-align-items-center gl-flex-direction-column-reverse gl-md-flex-direction-row gl-mt-3 gl-md-mt-0"
>
diff --git a/app/assets/javascripts/ci/runner/components/cells/runner_summary_cell.vue b/app/assets/javascripts/ci/runner/components/cells/runner_summary_cell.vue
index a80d6207be8..8a920c85e06 100644
--- a/app/assets/javascripts/ci/runner/components/cells/runner_summary_cell.vue
+++ b/app/assets/javascripts/ci/runner/components/cells/runner_summary_cell.vue
@@ -2,9 +2,9 @@
import { GlIcon, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
import { sprintf, __, formatNumber } from '~/locale';
-import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
+import RunnerCreatedAt from '../runner_created_at.vue';
import RunnerName from '../runner_name.vue';
import RunnerTags from '../runner_tags.vue';
import RunnerTypeBadge from '../runner_type_badge.vue';
@@ -15,8 +15,6 @@ import {
I18N_LOCKED_RUNNER_DESCRIPTION,
I18N_VERSION_LABEL,
I18N_LAST_CONTACT_LABEL,
- I18N_CREATED_AT_LABEL,
- I18N_CREATED_AT_BY_LABEL,
} from '../../constants';
import RunnerSummaryField from './runner_summary_field.vue';
@@ -26,13 +24,13 @@ export default {
GlSprintf,
TimeAgo,
RunnerSummaryField,
+ RunnerCreatedAt,
RunnerName,
RunnerTags,
RunnerTypeBadge,
RunnerManagersBadge,
RunnerUpgradeStatusIcon: () =>
import('ee_component/ci/runner/components/runner_upgrade_status_icon.vue'),
- UserAvatarLink,
TooltipOnTruncate,
},
directives: {
@@ -75,8 +73,6 @@ export default {
I18N_LOCKED_RUNNER_DESCRIPTION,
I18N_VERSION_LABEL,
I18N_LAST_CONTACT_LABEL,
- I18N_CREATED_AT_LABEL,
- I18N_CREATED_AT_BY_LABEL,
},
};
</script>
@@ -143,30 +139,7 @@ export default {
</runner-summary-field>
<runner-summary-field icon="calendar">
- <template v-if="createdBy">
- <gl-sprintf :message="$options.i18n.I18N_CREATED_AT_BY_LABEL">
- <template #timeAgo>
- <time-ago v-if="runner.createdAt" :time="runner.createdAt" />
- </template>
- <template #avatar>
- <user-avatar-link
- :link-href="createdBy.webUrl"
- :img-src="createdBy.avatarUrl"
- img-css-classes="gl-vertical-align-top"
- :img-size="16"
- :img-alt="createdByImgAlt"
- :tooltip-text="createdBy.username"
- />
- </template>
- </gl-sprintf>
- </template>
- <template v-else>
- <gl-sprintf :message="$options.i18n.I18N_CREATED_AT_LABEL">
- <template #timeAgo>
- <time-ago v-if="runner.createdAt" :time="runner.createdAt" />
- </template>
- </gl-sprintf>
- </template>
+ <runner-created-at :runner="runner" />
</runner-summary-field>
</div>
diff --git a/app/assets/javascripts/ci/runner/components/registration/registration_feedback_banner.vue b/app/assets/javascripts/ci/runner/components/registration/registration_feedback_banner.vue
deleted file mode 100644
index 6fd4edf5847..00000000000
--- a/app/assets/javascripts/ci/runner/components/registration/registration_feedback_banner.vue
+++ /dev/null
@@ -1,41 +0,0 @@
-<script>
-import ILLUSTRATION_URL from '@gitlab/svgs/dist/illustrations/rocket-launch-md.svg?url';
-import { GlBanner } from '@gitlab/ui';
-import { s__ } from '~/locale';
-import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
-
-const FEEDBACK_ISSUE_URL = 'https://gitlab.com/gitlab-org/gitlab/-/issues/387993';
-
-export default {
- components: {
- GlBanner,
- UserCalloutDismisser,
- },
- i18n: {
- title: s__("Runners|We've made some changes and want your feedback"),
- body: s__(
- "Runners|We've been making improvements to how you register runners so that it's more secure and efficient. Tell us how we're doing.",
- ),
- button: s__('Runners|Add your feedback to this issue'),
- },
- ILLUSTRATION_URL,
- FEEDBACK_ISSUE_URL,
-};
-</script>
-<template>
- <user-callout-dismisser feature-name="create_runner_workflow_banner">
- <template #default="{ dismiss, shouldShowCallout }">
- <gl-banner
- v-if="shouldShowCallout"
- class="gl-my-6"
- :title="$options.i18n.title"
- :svg-path="$options.ILLUSTRATION_URL"
- :button-text="$options.i18n.button"
- :button-link="$options.FEEDBACK_ISSUE_URL"
- @close="dismiss"
- >
- <p>{{ $options.i18n.body }}</p>
- </gl-banner>
- </template>
- </user-callout-dismisser>
-</template>
diff --git a/app/assets/javascripts/ci/runner/components/registration/registration_instructions.vue b/app/assets/javascripts/ci/runner/components/registration/registration_instructions.vue
index 771ecb1a0d4..a4dec8199a3 100644
--- a/app/assets/javascripts/ci/runner/components/registration/registration_instructions.vue
+++ b/app/assets/javascripts/ci/runner/components/registration/registration_instructions.vue
@@ -100,11 +100,11 @@ export default {
tokenMessage() {
if (this.token) {
return s__(
- 'Runners|The %{boldStart}runner token%{boldEnd} %{token} displays %{boldStart}only for a short time%{boldEnd}, and is stored in the %{codeStart}config.toml%{codeEnd} after you register the runner. It will not be visible once the runner is registered.',
+ 'Runners|The %{boldStart}runner authentication token%{boldEnd} %{token} displays here %{boldStart}for a short time only%{boldEnd}. After you register the runner, this token is stored in the %{codeStart}config.toml%{codeEnd} and cannot be accessed again from the UI.',
);
}
return s__(
- 'Runners|The %{boldStart}runner token%{boldEnd} is no longer visible, it is stored in the %{codeStart}config.toml%{codeEnd} if you have registered the runner.',
+ 'Runners|The %{boldStart}runner authentication token%{boldEnd} is no longer visible, it is stored in the %{codeStart}config.toml%{codeEnd} if you have registered the runner.',
);
},
commandPrompt() {
diff --git a/app/assets/javascripts/ci/runner/components/runner_created_at.vue b/app/assets/javascripts/ci/runner/components/runner_created_at.vue
new file mode 100644
index 00000000000..410142a0eb5
--- /dev/null
+++ b/app/assets/javascripts/ci/runner/components/runner_created_at.vue
@@ -0,0 +1,72 @@
+<script>
+import { GlSprintf, GlLink } from '@gitlab/ui';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
+
+import {
+ I18N_CREATED_AT_LABEL,
+ I18N_CREATED_BY_LABEL,
+ I18N_CREATED_BY_AT_LABEL,
+} from '../constants';
+
+export default {
+ components: {
+ GlSprintf,
+ GlLink,
+ TimeAgo,
+ },
+ props: {
+ runner: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ createdAt() {
+ return this.runner?.createdAt;
+ },
+ createdBy() {
+ return this.runner?.createdBy;
+ },
+ createdById() {
+ if (this.createdBy?.id) {
+ return getIdFromGraphQLId(this.createdBy.id);
+ }
+ return null;
+ },
+ message() {
+ if (this.createdBy && this.createdAt) {
+ return I18N_CREATED_BY_AT_LABEL;
+ }
+ if (this.createdBy) {
+ return I18N_CREATED_BY_LABEL;
+ }
+ if (this.createdAt) {
+ return I18N_CREATED_AT_LABEL;
+ }
+
+ return null;
+ },
+ },
+};
+</script>
+<template>
+ <span v-if="message">
+ <gl-sprintf :message="message">
+ <template #timeAgo>
+ <time-ago v-if="createdAt" :time="createdAt" />
+ </template>
+ <template #user>
+ <gl-link
+ class="js-user-link gl-reset-color gl-font-weight-bold"
+ :href="createdBy.webUrl"
+ :data-user-id="createdById"
+ :data-username="createdBy.username"
+ :data-name="createdBy.name"
+ :data-avatar-url="createdBy.avatarUrl"
+ >{{ createdBy.name }}</gl-link
+ >
+ </template>
+ </gl-sprintf>
+ </span>
+</template>
diff --git a/app/assets/javascripts/ci/runner/components/runner_details.vue b/app/assets/javascripts/ci/runner/components/runner_details.vue
index 0ec2ef30c20..477d28c6c28 100644
--- a/app/assets/javascripts/ci/runner/components/runner_details.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_details.vue
@@ -120,12 +120,9 @@ export default {
}}
</p>
<p class="gl-mb-0">
- <gl-link
- :href="tokenExpirationHelpUrl"
- target="_blank"
- class="gl-reset-font-size"
- >{{ __('Learn more') }}</gl-link
- >
+ <gl-link :href="tokenExpirationHelpUrl" target="_blank">{{
+ __('Learn more')
+ }}</gl-link>
</p>
</help-popover>
</template>
@@ -156,12 +153,9 @@ export default {
"
>
<template #link="{ content }"
- ><gl-link
- :href="$options.RUNNER_MANAGERS_HELP_URL"
- target="_blank"
- class="gl-reset-font-size"
- >{{ content }}</gl-link
- ></template
+ ><gl-link :href="$options.RUNNER_MANAGERS_HELP_URL" target="_blank">{{
+ content
+ }}</gl-link></template
>
</gl-sprintf>
</help-popover>
diff --git a/app/assets/javascripts/ci/runner/components/runner_header.vue b/app/assets/javascripts/ci/runner/components/runner_header.vue
index 0fa06537ed6..f8d0352e532 100644
--- a/app/assets/javascripts/ci/runner/components/runner_header.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_header.vue
@@ -1,16 +1,15 @@
<script>
-import { GlIcon, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
-import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
+import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { I18N_LOCKED_RUNNER_DESCRIPTION } from '../constants';
import { formatRunnerName } from '../utils';
+import RunnerCreatedAt from './runner_created_at.vue';
import RunnerTypeBadge from './runner_type_badge.vue';
import RunnerStatusBadge from './runner_status_badge.vue';
export default {
components: {
GlIcon,
- GlSprintf,
- TimeAgo,
+ RunnerCreatedAt,
RunnerTypeBadge,
RunnerStatusBadge,
RunnerUpgradeStatusBadge: () =>
@@ -43,21 +42,13 @@ export default {
<runner-status-badge :contacted-at="runner.contactedAt" :status="runner.status" />
<runner-type-badge :type="runner.runnerType" />
<runner-upgrade-status-badge :runner="runner" />
- <span v-if="runner.createdAt">
- <gl-sprintf :message="__('%{locked} created %{timeago}')">
- <template #locked>
- <gl-icon
- v-if="runner.locked"
- v-gl-tooltip="$options.I18N_LOCKED_RUNNER_DESCRIPTION"
- name="lock"
- :aria-label="$options.I18N_LOCKED_RUNNER_DESCRIPTION"
- />
- </template>
- <template #timeago>
- <time-ago :time="runner.createdAt" />
- </template>
- </gl-sprintf>
- </span>
+ <gl-icon
+ v-if="runner.locked"
+ v-gl-tooltip="$options.I18N_LOCKED_RUNNER_DESCRIPTION"
+ name="lock"
+ :aria-label="$options.I18N_LOCKED_RUNNER_DESCRIPTION"
+ />
+ <runner-created-at :runner="runner" />
</div>
</div>
</template>
diff --git a/app/assets/javascripts/ci/runner/components/runner_jobs_table.vue b/app/assets/javascripts/ci/runner/components/runner_jobs_table.vue
index 5d8e9dcdee2..653d9b05330 100644
--- a/app/assets/javascripts/ci/runner/components/runner_jobs_table.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_jobs_table.vue
@@ -3,7 +3,7 @@ import { GlTableLite } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { formatTime } from '~/lib/utils/datetime_utility';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import RunnerTags from '~/ci/runner/components/runner_tags.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import { tableField } from '../utils';
@@ -11,7 +11,7 @@ import LinkCell from './cells/link_cell.vue';
export default {
components: {
- CiBadgeLink,
+ CiIcon,
GlTableLite,
LinkCell,
RunnerTags,
@@ -80,7 +80,7 @@ export default {
fixed
>
<template #cell(status)="{ item = {} }">
- <ci-badge-link v-if="item.detailedStatus" :status="item.detailedStatus" />
+ <ci-icon v-if="item.detailedStatus" :status="item.detailedStatus" show-status-text />
</template>
<template #cell(job)="{ item = {} }">
diff --git a/app/assets/javascripts/ci/runner/components/runner_list_header.vue b/app/assets/javascripts/ci/runner/components/runner_list_header.vue
new file mode 100644
index 00000000000..e4367db035e
--- /dev/null
+++ b/app/assets/javascripts/ci/runner/components/runner_list_header.vue
@@ -0,0 +1,17 @@
+<script>
+export default {
+ name: 'RunnerListHeader',
+};
+</script>
+<template>
+ <header
+ class="gl-my-5 gl-display-flex gl-align-items-flex-start gl-flex-wrap gl-justify-content-space-between"
+ >
+ <h1 v-if="$scopedSlots.title" class="gl-my-0 gl-font-size-h1 header-title">
+ <slot name="title"></slot>
+ </h1>
+ <div v-if="$scopedSlots.actions" class="gl-display-flex gl-gap-3">
+ <slot name="actions"></slot>
+ </div>
+ </header>
+</template>
diff --git a/app/assets/javascripts/ci/runner/components/search_tokens/tag_token.vue b/app/assets/javascripts/ci/runner/components/search_tokens/tag_token.vue
index dd1cca0a05c..1f61e878eb0 100644
--- a/app/assets/javascripts/ci/runner/components/search_tokens/tag_token.vue
+++ b/app/assets/javascripts/ci/runner/components/search_tokens/tag_token.vue
@@ -70,7 +70,7 @@ export default {
@fetch-suggestions="fetchTags"
v-on="$listeners"
>
- <template #view-token="{ viewTokenProps: { listeners, inputValue, activeTokenValue } }">
+ <template #view-token="{ viewTokenProps: { listeners = {}, inputValue, activeTokenValue } }">
<gl-token variant="search-value" :class="$options.RUNNER_TAG_BG_CLASS" v-on="listeners">
{{ activeTokenValue ? activeTokenValue.text : inputValue }}
</gl-token>
diff --git a/app/assets/javascripts/ci/runner/components/search_tokens/version_token_config.js b/app/assets/javascripts/ci/runner/components/search_tokens/version_token_config.js
new file mode 100644
index 00000000000..23f82d06f6d
--- /dev/null
+++ b/app/assets/javascripts/ci/runner/components/search_tokens/version_token_config.js
@@ -0,0 +1,12 @@
+import { OPERATORS_IS } from '~/vue_shared/components/filtered_search_bar/constants';
+import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
+import { PARAM_KEY_VERSION, I18N_VERSION } from '../../constants';
+
+export const versionTokenConfig = {
+ icon: 'doc-versions',
+ title: I18N_VERSION,
+ type: PARAM_KEY_VERSION,
+ token: BaseToken,
+ operators: OPERATORS_IS,
+ suggestionsDisabled: true,
+};
diff --git a/app/assets/javascripts/ci/runner/constants.js b/app/assets/javascripts/ci/runner/constants.js
index b3cc295f8e4..d04d75b6e75 100644
--- a/app/assets/javascripts/ci/runner/constants.js
+++ b/app/assets/javascripts/ci/runner/constants.js
@@ -99,10 +99,14 @@ export const I18N_DELETED_TOAST = s__('Runners|Runner %{name} was deleted');
export const I18N_LOCKED_RUNNER_DESCRIPTION = s__(
'Runners|Runner is locked and available for currently assigned projects only. Only administrators can change the assigned projects.',
);
+export const I18N_VERSION = s__('Runners|Version starts with');
export const I18N_VERSION_LABEL = s__('Runners|Version %{version}');
export const I18N_LAST_CONTACT_LABEL = s__('Runners|Last contact: %{timeAgo}');
+
export const I18N_CREATED_AT_LABEL = s__('Runners|Created %{timeAgo}');
-export const I18N_CREATED_AT_BY_LABEL = s__('Runners|Created %{timeAgo} by %{avatar}');
+export const I18N_CREATED_BY_LABEL = s__('Runners|Created by %{user}');
+export const I18N_CREATED_BY_AT_LABEL = s__('Runners|Created by %{user} %{timeAgo}');
+
export const I18N_SHOW_ONLY_INHERITED = s__('Runners|Show only inherited');
export const I18N_ADMIN = s__('Runners|Administrator');
@@ -154,6 +158,7 @@ export const PARAM_KEY_STATUS = 'status';
export const PARAM_KEY_PAUSED = 'paused';
export const PARAM_KEY_RUNNER_TYPE = 'runner_type';
export const PARAM_KEY_TAG = 'tag';
+export const PARAM_KEY_VERSION = 'version_prefix';
export const PARAM_KEY_SEARCH = 'search';
export const PARAM_KEY_MEMBERSHIP = 'membership';
diff --git a/app/assets/javascripts/ci/runner/graphql/edit/runner_fields_shared.fragment.graphql b/app/assets/javascripts/ci/runner/graphql/edit/runner_fields_shared.fragment.graphql
index 41ec9967d90..5aa96f42b04 100644
--- a/app/assets/javascripts/ci/runner/graphql/edit/runner_fields_shared.fragment.graphql
+++ b/app/assets/javascripts/ci/runner/graphql/edit/runner_fields_shared.fragment.graphql
@@ -1,3 +1,5 @@
+#import "~/graphql_shared/fragments/user.fragment.graphql"
+
fragment RunnerFieldsShared on CiRunner {
id
shortSha
@@ -10,5 +12,8 @@ fragment RunnerFieldsShared on CiRunner {
maximumTimeout
tagList
createdAt
+ createdBy {
+ ...User
+ }
status
}
diff --git a/app/assets/javascripts/ci/runner/graphql/list/all_runners.query.graphql b/app/assets/javascripts/ci/runner/graphql/list/all_runners.query.graphql
index 15401c25c64..628ebfd2029 100644
--- a/app/assets/javascripts/ci/runner/graphql/list/all_runners.query.graphql
+++ b/app/assets/javascripts/ci/runner/graphql/list/all_runners.query.graphql
@@ -10,6 +10,7 @@ query getAllRunners(
$type: CiRunnerType
$tagList: [String!]
$search: String
+ $versionPrefix: String
$sort: CiRunnerSort
) {
runners(
@@ -22,6 +23,7 @@ query getAllRunners(
type: $type
tagList: $tagList
search: $search
+ versionPrefix: $versionPrefix
sort: $sort
) {
...AllRunnersConnection
diff --git a/app/assets/javascripts/ci/runner/graphql/list/all_runners_count.query.graphql b/app/assets/javascripts/ci/runner/graphql/list/all_runners_count.query.graphql
index 82591b88d3e..18f587495b0 100644
--- a/app/assets/javascripts/ci/runner/graphql/list/all_runners_count.query.graphql
+++ b/app/assets/javascripts/ci/runner/graphql/list/all_runners_count.query.graphql
@@ -4,8 +4,16 @@ query getAllRunnersCount(
$type: CiRunnerType
$tagList: [String!]
$search: String
+ $versionPrefix: String
) {
- runners(paused: $paused, status: $status, type: $type, tagList: $tagList, search: $search) {
+ runners(
+ paused: $paused
+ status: $status
+ type: $type
+ tagList: $tagList
+ search: $search
+ versionPrefix: $versionPrefix
+ ) {
count
}
}
diff --git a/app/assets/javascripts/ci/runner/graphql/show/runner_details_shared.fragment.graphql b/app/assets/javascripts/ci/runner/graphql/show/runner_details_shared.fragment.graphql
index e2c890b3834..8f998ab42fa 100644
--- a/app/assets/javascripts/ci/runner/graphql/show/runner_details_shared.fragment.graphql
+++ b/app/assets/javascripts/ci/runner/graphql/show/runner_details_shared.fragment.graphql
@@ -1,3 +1,5 @@
+#import "~/graphql_shared/fragments/user.fragment.graphql"
+
fragment RunnerDetailsShared on CiRunner {
id
shortSha
@@ -11,6 +13,9 @@ fragment RunnerDetailsShared on CiRunner {
jobCount
tagList
createdAt
+ createdBy {
+ ...User
+ }
status
contactedAt
tokenExpiresAt
diff --git a/app/assets/javascripts/ci/runner/graphql/show/runner_jobs.query.graphql b/app/assets/javascripts/ci/runner/graphql/show/runner_jobs.query.graphql
index b6d6996a857..611de43b995 100644
--- a/app/assets/javascripts/ci/runner/graphql/show/runner_jobs.query.graphql
+++ b/app/assets/javascripts/ci/runner/graphql/show/runner_jobs.query.graphql
@@ -8,7 +8,7 @@ query getRunnerJobs($id: CiRunnerID!, $first: Int, $last: Int, $before: String,
nodes {
id
detailedStatus {
- # fields for `<ci-badge-link>`
+ # fields for `<ci-icon>`
id
detailsPath
group
diff --git a/app/assets/javascripts/ci/runner/group_new_runner/group_new_runner_app.vue b/app/assets/javascripts/ci/runner/group_new_runner/group_new_runner_app.vue
index 2e1706ddae9..c907f9c8982 100644
--- a/app/assets/javascripts/ci/runner/group_new_runner/group_new_runner_app.vue
+++ b/app/assets/javascripts/ci/runner/group_new_runner/group_new_runner_app.vue
@@ -4,7 +4,6 @@ import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import RegistrationCompatibilityAlert from '~/ci/runner/components/registration/registration_compatibility_alert.vue';
-import RegistrationFeedbackBanner from '~/ci/runner/components/registration/registration_feedback_banner.vue';
import RunnerPlatformsRadioGroup from '~/ci/runner/components/runner_platforms_radio_group.vue';
import RunnerCreateForm from '~/ci/runner/components/runner_create_form.vue';
import { DEFAULT_PLATFORM, GROUP_TYPE, PARAM_KEY_PLATFORM } from '../constants';
@@ -14,7 +13,6 @@ export default {
name: 'GroupNewRunnerApp',
components: {
RegistrationCompatibilityAlert,
- RegistrationFeedbackBanner,
RunnerPlatformsRadioGroup,
RunnerCreateForm,
},
@@ -50,8 +48,6 @@ export default {
<template>
<div>
- <registration-feedback-banner />
-
<h1 class="gl-font-size-h2">{{ s__('Runners|New group runner') }}</h1>
<registration-compatibility-alert :alert-key="groupId" />
diff --git a/app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue b/app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue
index dcaf8635f5c..b5042936b1e 100644
--- a/app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue
+++ b/app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue
@@ -14,6 +14,7 @@ import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import groupRunnersCountQuery from 'ee_else_ce/ci/runner/graphql/list/group_runners_count.query.graphql';
import groupRunnersQuery from 'ee_else_ce/ci/runner/graphql/list/group_runners.query.graphql';
+import RunnerListHeader from '../components/runner_list_header.vue';
import RegistrationDropdown from '../components/registration/registration_dropdown.vue';
import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue';
import RunnerList from '../components/runner_list.vue';
@@ -44,6 +45,7 @@ export default {
components: {
GlButton,
GlLink,
+ RunnerListHeader,
RegistrationDropdown,
RunnerFilteredSearchBar,
RunnerList,
@@ -86,9 +88,6 @@ export default {
apollo: {
runners: {
query: groupRunnersQuery,
- context: {
- isSingleRequest: true,
- },
fetchPolicy: fetchPolicies.NETWORK_ONLY,
variables() {
return this.variables;
@@ -212,11 +211,9 @@ export default {
<template>
<div>
- <header class="gl-my-5 gl-display-flex gl-justify-content-space-between">
- <h2 class="gl-my-0 header-title">
- {{ s__('Runners|Runners') }}
- </h2>
- <div class="gl-display-flex gl-gap-3">
+ <runner-list-header>
+ <template #title>{{ s__('Runners|Runners') }}</template>
+ <template #actions>
<gl-button
v-if="newRunnerPath"
:href="newRunnerPath"
@@ -231,8 +228,9 @@ export default {
:type="$options.GROUP_TYPE"
placement="right"
/>
- </div>
- </header>
+ </template>
+ </runner-list-header>
+
<div
class="gl-display-flex gl-align-items-center gl-flex-direction-column-reverse gl-md-flex-direction-row gl-mt-3 gl-md-mt-0"
>
diff --git a/app/assets/javascripts/ci/runner/project_new_runner/project_new_runner_app.vue b/app/assets/javascripts/ci/runner/project_new_runner/project_new_runner_app.vue
index 51f5a9ce8d9..241479a8c98 100644
--- a/app/assets/javascripts/ci/runner/project_new_runner/project_new_runner_app.vue
+++ b/app/assets/javascripts/ci/runner/project_new_runner/project_new_runner_app.vue
@@ -4,7 +4,6 @@ import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import RegistrationCompatibilityAlert from '~/ci/runner/components/registration/registration_compatibility_alert.vue';
-import RegistrationFeedbackBanner from '~/ci/runner/components/registration/registration_feedback_banner.vue';
import RunnerPlatformsRadioGroup from '~/ci/runner/components/runner_platforms_radio_group.vue';
import RunnerCreateForm from '~/ci/runner/components/runner_create_form.vue';
import { DEFAULT_PLATFORM, PARAM_KEY_PLATFORM, PROJECT_TYPE } from '../constants';
@@ -14,7 +13,6 @@ export default {
name: 'ProjectNewRunnerApp',
components: {
RegistrationCompatibilityAlert,
- RegistrationFeedbackBanner,
RunnerPlatformsRadioGroup,
RunnerCreateForm,
},
@@ -50,8 +48,6 @@ export default {
<template>
<div>
- <registration-feedback-banner />
-
<h1 class="gl-font-size-h2">{{ s__('Runners|New project runner') }}</h1>
<registration-compatibility-alert :alert-key="projectId" />
diff --git a/app/assets/javascripts/ci/runner/runner_search_utils.js b/app/assets/javascripts/ci/runner/runner_search_utils.js
index 8915198350f..e3aee15f42c 100644
--- a/app/assets/javascripts/ci/runner/runner_search_utils.js
+++ b/app/assets/javascripts/ci/runner/runner_search_utils.js
@@ -12,6 +12,7 @@ import {
PARAM_KEY_STATUS,
PARAM_KEY_RUNNER_TYPE,
PARAM_KEY_TAG,
+ PARAM_KEY_VERSION,
PARAM_KEY_SEARCH,
PARAM_KEY_MEMBERSHIP,
PARAM_KEY_SORT,
@@ -151,7 +152,12 @@ export const fromUrlQueryToSearch = (query = window.location.search) => {
membership: membership || DEFAULT_MEMBERSHIP,
filters: prepareTokens(
urlQueryToFilter(query, {
- filterNamesAllowList: [PARAM_KEY_PAUSED, PARAM_KEY_STATUS, PARAM_KEY_TAG],
+ filterNamesAllowList: [
+ PARAM_KEY_PAUSED,
+ PARAM_KEY_STATUS,
+ PARAM_KEY_TAG,
+ PARAM_KEY_VERSION,
+ ],
filteredSearchTermKey: PARAM_KEY_SEARCH,
}),
),
@@ -178,6 +184,7 @@ export const fromSearchToUrl = (
[PARAM_KEY_MEMBERSHIP]: [],
[PARAM_KEY_TAG]: [],
[PARAM_KEY_PAUSED]: [],
+ [PARAM_KEY_VERSION]: [],
// Current filters
...filterToQueryObject(processFilters(filters), {
filteredSearchTermKey: PARAM_KEY_SEARCH,
@@ -229,6 +236,7 @@ export const fromSearchToVariables = ({
[filterVariables.status] = queryObj[PARAM_KEY_STATUS] || [];
filterVariables.search = queryObj[PARAM_KEY_SEARCH];
filterVariables.tagList = queryObj[PARAM_KEY_TAG];
+ [filterVariables.versionPrefix] = queryObj[PARAM_KEY_VERSION] || [];
if (queryObj[PARAM_KEY_PAUSED]) {
filterVariables.paused = parseBoolean(queryObj[PARAM_KEY_PAUSED]);
diff --git a/app/assets/javascripts/ci/runner/sentry_utils.js b/app/assets/javascripts/ci/runner/sentry_utils.js
index 25fecdcfa7d..01a20880e0a 100644
--- a/app/assets/javascripts/ci/runner/sentry_utils.js
+++ b/app/assets/javascripts/ci/runner/sentry_utils.js
@@ -1,4 +1,4 @@
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
const COMPONENT_TAG = 'vue_component';
diff --git a/app/assets/javascripts/ci/utils.js b/app/assets/javascripts/ci/utils.js
index 8a4f28404c6..21361aedb9d 100644
--- a/app/assets/javascripts/ci/utils.js
+++ b/app/assets/javascripts/ci/utils.js
@@ -1,4 +1,4 @@
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
export const reportToSentry = (component, failureType) => {
Sentry.captureException(failureType, {
diff --git a/app/assets/javascripts/ci_secure_files/components/secure_files_list.vue b/app/assets/javascripts/ci_secure_files/components/secure_files_list.vue
index 509bdabdd9e..fb2e24e15f6 100644
--- a/app/assets/javascripts/ci_secure_files/components/secure_files_list.vue
+++ b/app/assets/javascripts/ci_secure_files/components/secure_files_list.vue
@@ -12,7 +12,7 @@ import {
GlTable,
GlTooltipDirective,
} from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import Api, { DEFAULT_PER_PAGE } from '~/api';
import { HTTP_STATUS_PAYLOAD_TOO_LARGE } from '~/lib/utils/http_status';
import { __, s__, sprintf } from '~/locale';
diff --git a/app/assets/javascripts/ci_settings_pipeline_triggers/components/triggers_list.vue b/app/assets/javascripts/ci_settings_pipeline_triggers/components/triggers_list.vue
index 0871d543d46..e1f6006fedf 100644
--- a/app/assets/javascripts/ci_settings_pipeline_triggers/components/triggers_list.vue
+++ b/app/assets/javascripts/ci_settings_pipeline_triggers/components/triggers_list.vue
@@ -180,17 +180,11 @@ export default {
data-confirm-btn-variant="danger"
rel="nofollow"
data-testid="trigger_revoke_button"
- data-qa-selector="trigger_revoke_button"
:href="item.projectTriggerPath"
/>
</template>
</gl-table>
- <div
- v-else
- class="gl-new-card-empty gl-px-5 gl-py-4"
- data-testid="no_triggers_content"
- data-qa-selector="no_triggers_content"
- >
+ <div v-else class="gl-new-card-empty gl-px-5 gl-py-4" data-testid="no_triggers_content">
{{ s__('Pipelines|No triggers have been created yet. Add one using the form above.') }}
</div>
</div>
diff --git a/app/assets/javascripts/clusters_list/store/actions.js b/app/assets/javascripts/clusters_list/store/actions.js
index 4537fd51fcf..f474e51622a 100644
--- a/app/assets/javascripts/clusters_list/store/actions.js
+++ b/app/assets/javascripts/clusters_list/store/actions.js
@@ -1,4 +1,4 @@
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
diff --git a/app/assets/javascripts/commons/gitlab_ui.js b/app/assets/javascripts/commons/gitlab_ui.js
index 4a9f79460da..fe6142ae145 100644
--- a/app/assets/javascripts/commons/gitlab_ui.js
+++ b/app/assets/javascripts/commons/gitlab_ui.js
@@ -5,6 +5,8 @@ applyGitLabUIConfig({
translations: {
'GlSearchBoxByType.input.placeholder': __('Search'),
'GlSearchBoxByType.clearButtonTitle': __('Clear'),
+ 'GlSorting.sortAscending': __('Sort direction: Ascending'),
+ 'GlSorting.sortDescending': __('Sort direction: Descending'),
'ClearIconButton.title': __('Clear'),
},
});
diff --git a/app/assets/javascripts/content_editor/components/suggestions_dropdown.vue b/app/assets/javascripts/content_editor/components/suggestions_dropdown.vue
index 6535d9eaa5d..b34ebe85eb4 100644
--- a/app/assets/javascripts/content_editor/components/suggestions_dropdown.vue
+++ b/app/assets/javascripts/content_editor/components/suggestions_dropdown.vue
@@ -161,6 +161,8 @@ export default {
},
onKeyDown({ event }) {
+ if (!this.items.length) return false;
+
if (event.key === 'ArrowUp') {
this.upHandler();
return true;
diff --git a/app/assets/javascripts/content_editor/content_editor.stories.js b/app/assets/javascripts/content_editor/content_editor.stories.js
index 1aa6568848f..6f7e9653e6e 100644
--- a/app/assets/javascripts/content_editor/content_editor.stories.js
+++ b/app/assets/javascripts/content_editor/content_editor.stories.js
@@ -30,4 +30,5 @@ Default.args = {
serializerConfig: {},
extensions: [],
enableAutocomplete: false,
+ markdownDocsPath: 'fake/path',
};
diff --git a/app/assets/javascripts/content_editor/extensions/code_block_highlight.js b/app/assets/javascripts/content_editor/extensions/code_block_highlight.js
index 8917417e55e..da5ac7eb158 100644
--- a/app/assets/javascripts/content_editor/extensions/code_block_highlight.js
+++ b/app/assets/javascripts/content_editor/extensions/code_block_highlight.js
@@ -81,4 +81,13 @@ export default CodeBlockLowlight.extend({
addNodeView() {
return new VueNodeViewRenderer(CodeBlockWrapper);
},
+
+ addProseMirrorPlugins() {
+ const parentPlugins = this.parent?.() ?? [];
+ // We don't want TipTap's VSCode paste plugin to be loaded since
+ // it conflicts with our CopyPaste plugin.
+ const i = parentPlugins.findIndex((plugin) => plugin.key.includes('VSCode'));
+ if (i >= 0) parentPlugins.splice(i, 1);
+ return parentPlugins;
+ },
}).configure({ lowlight });
diff --git a/app/assets/javascripts/content_editor/extensions/copy_paste.js b/app/assets/javascripts/content_editor/extensions/copy_paste.js
index ab9e5619600..d29a407c5ca 100644
--- a/app/assets/javascripts/content_editor/extensions/copy_paste.js
+++ b/app/assets/javascripts/content_editor/extensions/copy_paste.js
@@ -11,6 +11,7 @@ import CodeBlockHighlight from './code_block_highlight';
import CodeSuggestion from './code_suggestion';
import Diagram from './diagram';
import Frontmatter from './frontmatter';
+import { loadingPlugin, findLoader } from './loading';
const TEXT_FORMAT = 'text/plain';
const GFM_FORMAT = 'text/x-gfm';
@@ -31,21 +32,6 @@ function parseHTML(schema, html) {
return { document: ProseMirrorDOMParser.fromSchema(schema).parse(body) };
}
-const findLoader = (editor, loaderId) => {
- let position;
-
- editor.view.state.doc.descendants((descendant, pos) => {
- if (descendant.type.name === 'loading' && descendant.attrs.id === loaderId) {
- position = pos;
- return false;
- }
-
- return true;
- });
-
- return position;
-};
-
export default Extension.create({
name: 'copyPaste',
priority: EXTENSION_PRIORITY_HIGHEST,
@@ -74,13 +60,20 @@ export default Extension.create({
Promise.resolve()
.then(() => {
- editor.commands.insertContent({ type: 'loading', attrs: { id: loaderId } });
+ editor
+ .chain()
+ .deleteSelection()
+ .setMeta(loadingPlugin, {
+ add: { loaderId, pos: editor.state.selection.from },
+ })
+ .run();
+
return promise;
})
.then(async ({ document }) => {
if (!document) return;
- const pos = findLoader(editor, loaderId);
+ const pos = findLoader(editor.state, loaderId);
if (!pos) return;
const { firstChild, childCount } = document.content;
@@ -91,7 +84,7 @@ export default Extension.create({
editor
.chain()
- .deleteRange({ from: pos, to: pos + 1 })
+ .setMeta(loadingPlugin, { remove: { loaderId } })
.insertContentAt(pos, toPaste.toJSON(), {
updateSelection: false,
})
@@ -113,7 +106,16 @@ export default Extension.create({
const handleCutAndCopy = (view, event) => {
const slice = view.state.selection.content();
- const gfmContent = this.options.serializer.serialize({ doc: slice.content });
+ let gfmContent = this.options.serializer.serialize({ doc: slice.content });
+ const gfmContentWithoutSingleTableCell = gfmContent.replace(
+ /^<table>[\s\n]*<tr>[\s\n]*<t[hd]>|<\/t[hd]>[\s\n]*<\/tr>[\s\n]*<\/table>[\s\n]*$/gim,
+ '',
+ );
+ const containsSingleTableCell = !/<t[hd]>/.test(gfmContentWithoutSingleTableCell);
+
+ if (containsSingleTableCell) {
+ gfmContent = gfmContentWithoutSingleTableCell;
+ }
const documentFragment = DOMSerializer.fromSchema(view.state.schema).serializeFragment(
slice.content,
);
diff --git a/app/assets/javascripts/content_editor/extensions/emoji.js b/app/assets/javascripts/content_editor/extensions/emoji.js
index 7f8b5da5f46..be6ecb6cafd 100644
--- a/app/assets/javascripts/content_editor/extensions/emoji.js
+++ b/app/assets/javascripts/content_editor/extensions/emoji.js
@@ -1,5 +1,5 @@
import { Node, InputRule } from '@tiptap/core';
-import { initEmojiMap, getAllEmoji } from '~/emoji';
+import { initEmojiMap, getEmojiMap } from '~/emoji';
export default Node.create({
name: 'emoji',
@@ -58,7 +58,7 @@ export default Node.create({
find: emojiInputRegex,
handler: ({ state, range: { from, to }, match }) => {
const [, , name] = match;
- const emojis = getAllEmoji();
+ const emojis = getEmojiMap();
const emoji = emojis[name];
const { tr } = state;
diff --git a/app/assets/javascripts/content_editor/extensions/html_marks.js b/app/assets/javascripts/content_editor/extensions/html_marks.js
index 79fc0eea2c7..58fa2655e25 100644
--- a/app/assets/javascripts/content_editor/extensions/html_marks.js
+++ b/app/assets/javascripts/content_editor/extensions/html_marks.js
@@ -50,7 +50,8 @@ export default marks.map((name) =>
},
parseHTML() {
- return [{ tag: name, priority: PARSE_HTML_PRIORITY_LOWEST }];
+ const tag = name === 'span' ? `${name}:not([data-escaped-char])` : name;
+ return [{ tag, priority: PARSE_HTML_PRIORITY_LOWEST }];
},
renderHTML({ HTMLAttributes }) {
diff --git a/app/assets/javascripts/content_editor/extensions/loading.js b/app/assets/javascripts/content_editor/extensions/loading.js
index 0115fb10d5d..942ac650925 100644
--- a/app/assets/javascripts/content_editor/extensions/loading.js
+++ b/app/assets/javascripts/content_editor/extensions/loading.js
@@ -1,4 +1,52 @@
import { Node } from '@tiptap/core';
+import { Decoration, DecorationSet } from '@tiptap/pm/view';
+import { Plugin } from '@tiptap/pm/state';
+
+const createDotsLoader = () => {
+ const root = document.createElement('span');
+ root.classList.add('gl-display-inline-flex', 'gl-align-items-center');
+ root.innerHTML = '<span class="gl-dots-loader gl-mx-2"><span></span></span>';
+ return root;
+};
+
+export const loadingPlugin = new Plugin({
+ state: {
+ init() {
+ return DecorationSet.empty;
+ },
+ apply(tr, set) {
+ let transformedSet = set.map(tr.mapping, tr.doc);
+ const action = tr.getMeta(this);
+
+ if (action?.add) {
+ const deco = Decoration.widget(action.add.pos, createDotsLoader(), {
+ id: action.add.loaderId,
+ side: -1,
+ });
+ transformedSet = transformedSet.add(tr.doc, [deco]);
+ } else if (action?.remove) {
+ transformedSet = transformedSet.remove(
+ transformedSet.find(null, null, (spec) => spec.id === action.remove.loaderId),
+ );
+ }
+ return transformedSet;
+ },
+ },
+ props: {
+ decorations(state) {
+ return this.getState(state);
+ },
+ },
+});
+
+export const findLoader = (state, loaderId) => {
+ const decos = loadingPlugin.getState(state);
+ const found = decos.find(null, null, (spec) => spec.id === loaderId);
+
+ return found.length ? found[0].from : null;
+};
+
+export const findAllLoaders = (state) => loadingPlugin.getState(state).find();
export default Node.create({
name: 'loading',
@@ -13,11 +61,7 @@ export default Node.create({
};
},
- renderHTML() {
- return [
- 'span',
- { class: 'gl-display-inline-flex gl-align-items-center' },
- ['span', { class: 'gl-dots-loader gl-mx-2' }, ['span']],
- ];
+ addProseMirrorPlugins() {
+ return [loadingPlugin];
},
});
diff --git a/app/assets/javascripts/content_editor/extensions/suggestions.js b/app/assets/javascripts/content_editor/extensions/suggestions.js
index f29222a5289..f7ff2fd6647 100644
--- a/app/assets/javascripts/content_editor/extensions/suggestions.js
+++ b/app/assets/javascripts/content_editor/extensions/suggestions.js
@@ -20,6 +20,7 @@ function createSuggestionPlugin({
limit = 15,
nodeType,
nodeProps = {},
+ insertionMap = {},
}) {
const fetchData = memoize(
isFunction(dataSource) ? dataSource : async () => (await axios.get(dataSource)).data,
@@ -36,7 +37,7 @@ function createSuggestionPlugin({
.focus()
.insertContentAt(range, [
{ type: nodeType, attrs: props },
- { type: 'text', text: ' ' },
+ { type: 'text', text: ` ${insertionMap[props.text] || ''}` },
])
.run();
},
@@ -56,6 +57,7 @@ function createSuggestionPlugin({
render: () => {
let component;
let popup;
+ let isHidden = false;
const onUpdate = (props) => {
component?.updateProps({ ...props, loading: false });
@@ -87,6 +89,12 @@ function createSuggestionPlugin({
popup = tippy('body', {
getReferenceClientRect: props.clientRect,
appendTo: () => document.body,
+ onHide: () => {
+ isHidden = true;
+ },
+ onShow: () => {
+ isHidden = false;
+ },
content: component.element,
showOnCreate: true,
interactive: true,
@@ -99,6 +107,8 @@ function createSuggestionPlugin({
onUpdate,
onKeyDown(props) {
+ if (isHidden) return false;
+
if (props.event.key === 'Escape') {
popup?.[0].hide();
@@ -217,11 +227,24 @@ export default Node.create({
referenceType: 'command',
},
search: (query) => ({ name }) => find(name, query),
+ insertionMap: {
+ '/label': '~',
+ '/unlabel': '~',
+ '/relabel': '~',
+ '/assign': '@',
+ '/unassign': '@',
+ '/reassign': '@',
+ '/cc': '@',
+ '/assign_reviewer': '@',
+ '/unassign_reviewer': '@',
+ '/reassign_reviewer': '@',
+ '/milestone': '%',
+ },
}),
createSuggestionPlugin({
editor: this.editor,
char: ':',
- dataSource: () => Object.values(getAllEmoji()),
+ dataSource: () => getAllEmoji(),
nodeType: 'emoji',
search: (query) => ({ d, name }) => find(d, query) || find(name, query),
limit: 10,
diff --git a/app/assets/javascripts/content_editor/extensions/word_break.js b/app/assets/javascripts/content_editor/extensions/word_break.js
index 457b7c36564..01b19cbbd13 100644
--- a/app/assets/javascripts/content_editor/extensions/word_break.js
+++ b/app/assets/javascripts/content_editor/extensions/word_break.js
@@ -24,7 +24,7 @@ export default Node.create({
},
addInputRules() {
- const inputRegex = /^<wbr>$/;
+ const inputRegex = /<wbr>$/;
return [nodeInputRule({ find: inputRegex, type: this.type })];
},
diff --git a/app/assets/javascripts/content_editor/services/content_editor.js b/app/assets/javascripts/content_editor/services/content_editor.js
index bc1ee696323..d3d2d76e481 100644
--- a/app/assets/javascripts/content_editor/services/content_editor.js
+++ b/app/assets/javascripts/content_editor/services/content_editor.js
@@ -84,9 +84,9 @@ export class ContentEditor {
async setSerializedContent(serializedContent) {
const { _tiptapEditor: editor } = this;
- const { doc, tr } = editor.state;
const { document } = await this.deserialize(serializedContent);
+ const { doc, tr } = editor.state;
if (document) {
this._pristineDoc = document;
diff --git a/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_base.vue b/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_base.vue
index e3d3360cd0c..3b9f14a218f 100644
--- a/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_base.vue
+++ b/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_base.vue
@@ -47,16 +47,20 @@ export default {
<template>
<li class="gl-mt-5 gl-pb-5 gl-border-b gl-relative">
- <time-ago-tooltip :time="event.created_at" class="gl-float-right gl-text-secondary" />
+ <time-ago-tooltip
+ :time="event.created_at"
+ class="gl-float-right gl-font-sm gl-text-secondary gl-mt-2"
+ />
<gl-avatar-link :href="author.web_url">
<gl-avatar-labeled
:label="author.name"
:sub-label="authorUsername"
+ inline-labels
:src="author.avatar_url"
- :size="32"
+ :size="24"
/>
</gl-avatar-link>
- <div class="gl-pl-8 gl-mt-2" data-testid="event-body">
+ <div class="gl-pl-7" data-testid="event-body">
<div class="gl-text-secondary">
<gl-icon :class="iconClass" :name="iconName" />
<gl-sprintf v-if="message" :message="message">
diff --git a/app/assets/javascripts/crm/organizations/components/graphql/update_customer_relations_organization.mutation.graphql b/app/assets/javascripts/crm/organizations/components/graphql/update_customer_relations_organization.mutation.graphql
new file mode 100644
index 00000000000..5ee3da2dfad
--- /dev/null
+++ b/app/assets/javascripts/crm/organizations/components/graphql/update_customer_relations_organization.mutation.graphql
@@ -0,0 +1,10 @@
+#import "./crm_organization_fields.fragment.graphql"
+
+mutation updateCustomerRelationsOrganization($input: CustomerRelationsOrganizationUpdateInput!) {
+ customerRelationsOrganizationUpdate(input: $input) {
+ organization {
+ ...OrganizationFragment
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/crm/organizations/components/graphql/update_organization.mutation.graphql b/app/assets/javascripts/crm/organizations/components/graphql/update_organization.mutation.graphql
deleted file mode 100644
index a4c46d1f0fa..00000000000
--- a/app/assets/javascripts/crm/organizations/components/graphql/update_organization.mutation.graphql
+++ /dev/null
@@ -1,10 +0,0 @@
-#import "./crm_organization_fields.fragment.graphql"
-
-mutation updateOrganization($input: CustomerRelationsOrganizationUpdateInput!) {
- customerRelationsOrganizationUpdate(input: $input) {
- organization {
- ...OrganizationFragment
- }
- errors
- }
-}
diff --git a/app/assets/javascripts/crm/organizations/components/organization_form_wrapper.vue b/app/assets/javascripts/crm/organizations/components/organization_form_wrapper.vue
index fb056e4fa2c..7dd65205b90 100644
--- a/app/assets/javascripts/crm/organizations/components/organization_form_wrapper.vue
+++ b/app/assets/javascripts/crm/organizations/components/organization_form_wrapper.vue
@@ -5,7 +5,7 @@ import { TYPENAME_CRM_ORGANIZATION, TYPENAME_GROUP } from '~/graphql_shared/cons
import CrmForm from '../../components/crm_form.vue';
import getGroupOrganizationsQuery from './graphql/get_group_organizations.query.graphql';
import createCustomerRelationsOrganizationMutation from './graphql/create_customer_relations_organization.mutation.graphql';
-import updateOrganizationMutation from './graphql/update_organization.mutation.graphql';
+import updateCustomerRelationsOrganizationMutation from './graphql/update_customer_relations_organization.mutation.graphql';
export default {
components: {
@@ -29,7 +29,7 @@ export default {
return convertToGraphQLId(TYPENAME_GROUP, this.groupId);
},
mutation() {
- if (this.isEditMode) return updateOrganizationMutation;
+ if (this.isEditMode) return updateCustomerRelationsOrganizationMutation;
return createCustomerRelationsOrganizationMutation;
},
diff --git a/app/assets/javascripts/custom_emoji/components/delete_item.vue b/app/assets/javascripts/custom_emoji/components/delete_item.vue
index 9d13d40dc47..91bd90c3682 100644
--- a/app/assets/javascripts/custom_emoji/components/delete_item.vue
+++ b/app/assets/javascripts/custom_emoji/components/delete_item.vue
@@ -1,7 +1,7 @@
<script>
-import * as Sentry from '@sentry/browser';
import { uniqueId } from 'lodash';
import { GlButton, GlTooltipDirective, GlModal, GlModalDirective, GlSprintf } from '@gitlab/ui';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { createAlert } from '~/alert';
import { __ } from '~/locale';
import deleteCustomEmojiMutation from '../queries/delete_custom_emoji.mutation.graphql';
diff --git a/app/assets/javascripts/deploy_freeze/components/deploy_freeze_modal.vue b/app/assets/javascripts/deploy_freeze/components/deploy_freeze_modal.vue
index 72d1ce9768a..6210e82119f 100644
--- a/app/assets/javascripts/deploy_freeze/components/deploy_freeze_modal.vue
+++ b/app/assets/javascripts/deploy_freeze/components/deploy_freeze_modal.vue
@@ -142,7 +142,6 @@ export default {
ref="freezeStartCron"
v-model="freezeStartCron"
class="gl-font-monospace!"
- data-qa-selector="deploy_freeze_start_field"
:placeholder="$options.i18n.cronPlaceholder"
:state="freezeStartCronState"
autofocus
@@ -160,7 +159,6 @@ export default {
id="deploy-freeze-end"
v-model="freezeEndCron"
class="gl-font-monospace!"
- data-qa-selector="deploy_freeze_end_field"
:placeholder="$options.i18n.cronPlaceholder"
:state="freezeEndCronState"
trim
diff --git a/app/assets/javascripts/deprecated_notes.js b/app/assets/javascripts/deprecated_notes.js
index 008e12abbcd..9b5b4cef1b9 100644
--- a/app/assets/javascripts/deprecated_notes.js
+++ b/app/assets/javascripts/deprecated_notes.js
@@ -152,7 +152,7 @@ export default class Notes {
// update the file name when an attachment is selected
this.$wrapperEl.on('change', '.js-note-attachment-input', this.updateFormAttachment);
// reply to diff/discussion notes
- this.$wrapperEl.on('click', '.js-discussion-reply-button', this.onReplyToDiscussionNote);
+ this.$wrapperEl.on('focus', '.js-discussion-reply-button', this.onReplyToDiscussionNote);
// add diff note
this.$wrapperEl.on('click', '.js-add-diff-note-button', this.onAddDiffNote);
// add diff note for images
diff --git a/app/assets/javascripts/design_management/components/delete_button.vue b/app/assets/javascripts/design_management/components/delete_button.vue
index dec1038d2e3..88c1b444b31 100644
--- a/app/assets/javascripts/design_management/components/delete_button.vue
+++ b/app/assets/javascripts/design_management/components/delete_button.vue
@@ -63,7 +63,7 @@ export default {
title: s__('DesignManagement|Are you sure you want to archive the selected designs?'),
actionPrimary: {
text: s__('DesignManagement|Archive designs'),
- attributes: { variant: 'confirm', 'data-qa-selector': 'confirm_archiving_button' },
+ attributes: { variant: 'confirm', 'data-testid': 'confirm-archiving-button' },
},
actionCancel: {
text: __('Cancel'),
diff --git a/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue b/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue
index 45f33967476..2a099b6f22d 100644
--- a/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue
+++ b/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue
@@ -1,6 +1,6 @@
<script>
import { GlButton, GlLink, GlTooltipDirective, GlFormCheckbox } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { createAlert } from '~/alert';
import { __, s__ } from '~/locale';
import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
@@ -293,7 +293,6 @@ export default {
<ul
class="design-discussion bordered-box gl-relative gl-p-0 gl-list-style-none"
:class="{ 'gl-bg-blue-50': isDiscussionActive }"
- data-qa-selector="design_discussion_content"
data-testid="design-discussion-content"
>
<design-note
diff --git a/app/assets/javascripts/design_management/components/design_notes/design_note.vue b/app/assets/javascripts/design_management/components/design_notes/design_note.vue
index a5b6d6276f8..b247f17fd97 100644
--- a/app/assets/javascripts/design_management/components/design_notes/design_note.vue
+++ b/app/assets/javascripts/design_management/components/design_notes/design_note.vue
@@ -7,12 +7,13 @@ import {
GlLink,
GlTooltipDirective,
} from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
import { produce } from 'immer';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import SafeHtml from '~/vue_shared/directives/safe_html';
import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils';
import { TYPENAME_USER } from '~/graphql_shared/constants';
import { __ } from '~/locale';
+import { setUrlFragment } from '~/lib/utils/url_utility';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import EmojiPicker from '~/emoji/components/picker.vue';
@@ -29,6 +30,7 @@ export default {
editCommentLabel: __('Edit comment'),
moreActionsLabel: __('More actions'),
deleteCommentText: __('Delete comment'),
+ copyCommentLink: __('Copy link'),
},
components: {
DesignNoteAwardsList,
@@ -129,19 +131,27 @@ export default {
this.isEditing = true;
},
extraAttrs: {
- 'data-testid': 'delete-note-button',
- 'data-qa-selector': 'delete_design_note_button',
class: 'gl-sm-display-none!',
},
},
{
+ text: this.$options.i18n.copyCommentLink,
+ action: () => {
+ this.$toast.show(__('Link copied to clipboard.'));
+ },
+ extraAttrs: {
+ 'data-clipboard-text': setUrlFragment(
+ window.location.href,
+ `note_${this.noteAnchorId}`,
+ ),
+ },
+ },
+ {
text: this.$options.i18n.deleteCommentText,
action: () => {
this.$emit('delete-note', this.note);
},
extraAttrs: {
- 'data-testid': 'delete-note-button',
- 'data-qa-selector': 'delete_design_note_button',
class: 'gl-text-red-500!',
},
},
@@ -311,7 +321,6 @@ export default {
v-gl-tooltip.hover
icon="ellipsis_v"
category="tertiary"
- data-qa-selector="design_discussion_actions_ellipsis_dropdown"
text-sr-only
:title="$options.i18n.moreActionsLabel"
:aria-label="$options.i18n.moreActionsLabel"
@@ -322,12 +331,7 @@ export default {
</div>
</div>
<template v-if="!isEditing">
- <div
- v-safe-html="note.bodyHtml"
- class="note-text md"
- data-qa-selector="note_content"
- data-testid="note-text"
- ></div>
+ <div v-safe-html="note.bodyHtml" class="note-text md" data-testid="note-text"></div>
<slot name="resolved-status"></slot>
</template>
<design-note-awards-list
diff --git a/app/assets/javascripts/design_management/components/design_notes/design_note_signed_out.vue b/app/assets/javascripts/design_management/components/design_notes/design_note_signed_out.vue
index f0812e62bba..de3e71c0e9c 100644
--- a/app/assets/javascripts/design_management/components/design_notes/design_note_signed_out.vue
+++ b/app/assets/javascripts/design_management/components/design_notes/design_note_signed_out.vue
@@ -37,7 +37,7 @@ export default {
</script>
<template>
- <div class="disabled-comment text-center">
+ <div class="disabled-comment gl-text-center gl-text-secondary">
<gl-sprintf :message="signedOutText">
<template #registerLink="{ content }">
<gl-link :href="registerPath">{{ content }}</gl-link>
diff --git a/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue b/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue
index 764c78ff581..b6a303ddde8 100644
--- a/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue
+++ b/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue
@@ -221,7 +221,7 @@ export default {
class="note-textarea js-gfm-input js-autosize markdown-area"
dir="auto"
data-supports-quick-actions="false"
- data-qa-selector="note_textarea"
+ data-testid="note-textarea"
:aria-label="__('Description')"
:placeholder="__('Write a comment…')"
@input="handleInput"
@@ -243,7 +243,7 @@ export default {
variant="confirm"
type="submit"
data-track-action="click_button"
- data-qa-selector="save_comment_button"
+ data-testid="save-comment-button"
@click="submitForm"
>
{{ buttonText }}
diff --git a/app/assets/javascripts/design_management/components/design_overlay.vue b/app/assets/javascripts/design_management/components/design_overlay.vue
index 4ce6395140e..e4361f94026 100644
--- a/app/assets/javascripts/design_management/components/design_overlay.vue
+++ b/app/assets/javascripts/design_management/components/design_overlay.vue
@@ -272,7 +272,7 @@ export default {
role="button"
:aria-label="$options.i18n.newCommentButtonLabel"
class="gl-absolute gl-w-full gl-h-full gl-p-0 gl-top-0 gl-left-0 gl-outline-0! btn-transparent gl-hover-cursor-crosshair"
- data-qa-selector="design_image_button"
+ data-testid="design-image-button"
@mouseup="onAddCommentMouseup"
></button>
diff --git a/app/assets/javascripts/design_management/components/list/item.vue b/app/assets/javascripts/design_management/components/list/item.vue
index 7b98557f4f0..6400f939244 100644
--- a/app/assets/javascripts/design_management/components/list/item.vue
+++ b/app/assets/javascripts/design_management/components/list/item.vue
@@ -144,12 +144,17 @@ export default {
:name="icon.name"
:size="16"
:class="icon.classes"
- data-qa-selector="design_status_icon"
+ data-testid="design-status-icon"
:data-qa-status="icon.name"
/>
</span>
</div>
- <gl-intersection-observer class="gl-flex-grow-1" @appear="onAppear">
+ <gl-intersection-observer
+ class="gl-flex-grow-1"
+ data-testid="design-image"
+ :data-qa-filename="filename"
+ @appear="onAppear"
+ >
<gl-loading-icon v-if="showLoadingSpinner" size="lg" />
<gl-icon
v-else-if="showImageErrorIcon"
@@ -162,8 +167,6 @@ export default {
:src="imageLink"
:alt="filename"
class="gl-display-block gl-mx-auto gl-max-w-full gl-max-h-full gl-w-auto design-img"
- data-qa-selector="design_image"
- :data-qa-filename="filename"
:data-testid="`design-img-${id}`"
@load="onImageLoad"
@error="onImageError"
@@ -171,11 +174,13 @@ export default {
</gl-intersection-observer>
</div>
<div class="card-footer gl-display-flex gl-w-full gl-bg-white gl-py-3 gl-px-4">
- <div class="gl-display-flex gl-flex-direction-column str-truncated-100">
+ <div
+ class="gl-display-flex gl-flex-direction-column str-truncated-100"
+ data-testid="design-file-name"
+ >
<span
v-gl-tooltip
class="gl-font-weight-semibold str-truncated-100"
- data-qa-selector="design_file_name"
:data-testid="`design-img-filename-${id}`"
:title="filename"
>{{ filename }}</span
diff --git a/app/assets/javascripts/design_management/pages/index.vue b/app/assets/javascripts/design_management/pages/index.vue
index 09f99f0927f..a1fd3520982 100644
--- a/app/assets/javascripts/design_management/pages/index.vue
+++ b/app/assets/javascripts/design_management/pages/index.vue
@@ -402,7 +402,7 @@ export default {
button-variant="default"
button-class="gl-mr-3"
button-size="small"
- data-qa-selector="archive_button"
+ data-testid="archive-button"
:loading="loading"
:has-selected-designs="hasSelectedDesigns"
@delete-selected-designs="mutate()"
@@ -490,7 +490,7 @@ export default {
:checked="isDesignSelected(design.filename)"
type="checkbox"
class="design-checkbox gl-absolute gl-top-4 gl-left-6 gl-ml-2"
- data-qa-selector="design_checkbox"
+ data-testid="design-checkbox"
:data-qa-design="design.filename"
@change="changeSelectedDesigns(design.filename)"
/>
@@ -506,7 +506,7 @@ export default {
:class="{ 'design-list-item': !isDesignListEmpty }"
:display-as-card="hasDesigns"
v-bind="$options.dropzoneProps"
- data-qa-selector="design_dropzone_content"
+ data-testid="design-dropzone-content"
@change="onUploadDesign"
@error="onDesignDropzoneError"
>
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index 924c515ee2d..54c276c36b1 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -1,6 +1,7 @@
<script>
import { GlLoadingIcon, GlPagination, GlSprintf, GlAlert } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
+import { debounce } from 'lodash';
// eslint-disable-next-line no-restricted-imports
import { mapState, mapGetters, mapActions } from 'vuex';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@@ -16,7 +17,8 @@ import {
import { createAlert } from '~/alert';
import { isSingleViewStyle } from '~/helpers/diffs_helper';
import { helpPagePath } from '~/helpers/help_page_helper';
-import { parseBoolean } from '~/lib/utils/common_utils';
+import { parseBoolean, handleLocationHash } from '~/lib/utils/common_utils';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { Mousetrap } from '~/lib/mousetrap';
import { updateHistory } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
@@ -39,8 +41,10 @@ import {
TRACKING_SINGLE_FILE_MODE,
TRACKING_MULTIPLE_FILES_MODE,
EVT_MR_PREPARED,
+ EVT_DISCUSSIONS_ASSIGNED,
} from '../constants';
+import { isCollapsed } from '../utils/diff_file';
import diffsEventHub from '../event_hub';
import { reviewStatuses } from '../utils/file_reviews';
import { diffsApp } from '../utils/performance';
@@ -55,10 +59,16 @@ import HiddenFilesWarning from './hidden_files_warning.vue';
import NoChanges from './no_changes.vue';
import VirtualScrollerScrollSync from './virtual_scroller_scroll_sync';
import DiffsFileTree from './diffs_file_tree.vue';
-import getMRCodequalityReports from './graphql/get_mr_codequality_reports.query.graphql';
+import getMRCodequalityAndSecurityReports from './graphql/get_mr_codequality_and_security_reports.query.graphql';
+
+export const FINDINGS_STATUS_PARSED = 'PARSED';
+export const FINDINGS_STATUS_ERROR = 'ERROR';
+export const FINDINGS_POLL_INTERVAL = 1000;
export default {
name: 'DiffsApp',
+ FINDINGS_STATUS_PARSED,
+ FINDINGS_STATUS_ERROR,
components: {
DiffsFileTree,
FindingsDrawer,
@@ -100,10 +110,10 @@ export default {
required: false,
default: '',
},
- endpointSast: {
- type: String,
+ sastReportAvailable: {
+ type: Boolean,
required: false,
- default: '',
+ default: false,
},
endpointCodequality: {
type: String,
@@ -135,31 +145,53 @@ export default {
diffFilesLength: 0,
virtualScrollCurrentIndex: -1,
subscribedToVirtualScrollingEvents: false,
+ autoScrolled: false,
+ activeProject: undefined,
};
},
apollo: {
- getMRCodequalityReports: {
- query: getMRCodequalityReports,
+ getMRCodequalityAndSecurityReports: {
+ query: getMRCodequalityAndSecurityReports,
+ pollInterval: FINDINGS_POLL_INTERVAL,
variables() {
return { fullPath: this.projectPath, iid: this.iid };
},
skip() {
- return !this.endpointCodequality || !this.sastReportsInInlineDiff;
+ const codeQualityBoolean = Boolean(this.endpointCodequality);
+
+ return !this.sastReportsInInlineDiff || (!codeQualityBoolean && !this.sastReportAvailable);
},
update(data) {
- if (data?.project?.mergeRequest?.codequalityReportsComparer?.report?.newErrors) {
+ const codeQualityBoolean = Boolean(this.endpointCodequality);
+ const { codequalityReportsComparer, sastReport } = data?.project?.mergeRequest || {};
+
+ this.activeProject = data?.project?.mergeRequest?.project;
+ if (
+ (sastReport?.status === FINDINGS_STATUS_PARSED || !this.sastReportAvailable) &&
+ (!codeQualityBoolean || codequalityReportsComparer.status === FINDINGS_STATUS_PARSED)
+ ) {
+ this.getMRCodequalityAndSecurityReportStopPolling(
+ this.$apollo.queries.getMRCodequalityAndSecurityReports,
+ );
+ }
+
+ if (sastReport?.status === FINDINGS_STATUS_ERROR && this.sastReportAvailable) {
+ this.fetchScannerFindingsError();
+ }
+
+ if (codequalityReportsComparer?.report?.newErrors) {
this.$store.commit(
'diffs/SET_CODEQUALITY_DATA',
- sortFindingsByFile(
- data.project.mergeRequest.codequalityReportsComparer.report.newErrors,
- ),
+ sortFindingsByFile(codequalityReportsComparer.report.newErrors),
);
}
+
+ if (sastReport?.report) {
+ this.$store.commit('diffs/SET_SAST_DATA', sastReport.report);
+ }
},
error() {
- createAlert({
- message: __('Something went wrong fetching the CodeQuality Findings. Please try again!'),
- });
+ this.fetchScannerFindingsError();
},
},
},
@@ -304,10 +336,6 @@ export default {
this.setCodequalityEndpoint(this.endpointCodequality);
}
- if (this.endpointSast) {
- this.setSastEndpoint(this.endpointSast);
- }
-
if (this.shouldShow) {
this.fetchData();
}
@@ -355,22 +383,15 @@ export default {
this.adjustView();
this.subscribeToEvents();
- this.unwatchDiscussions = this.$watch(
- () => `${this.flatBlobsList.length}:${this.$store.state.notes.discussions.length}`,
- () => {
- this.setDiscussions();
-
- if (this.$store.state.notes.doneFetchingBatchDiscussions) {
- this.unwatchDiscussions();
- }
- },
- );
-
- this.unwatchRetrievingBatches = this.$watch(
- () => `${this.retrievingBatches}:${this.$store.state.notes.discussions.length}`,
- () => {
- if (!this.retrievingBatches && this.$store.state.notes.discussions.length) {
- this.unwatchRetrievingBatches();
+ this.slowHashHandler = debounce(() => {
+ handleLocationHash();
+ this.autoScrolled = true;
+ }, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
+ this.$watch(
+ () => this.$store.state.notes.discussions.length,
+ (newVal, prevVal) => {
+ if (newVal > prevVal) {
+ this.setDiscussions();
}
},
);
@@ -388,7 +409,6 @@ export default {
...mapActions('diffs', [
'moveToNeighboringCommit',
'setCodequalityEndpoint',
- 'setSastEndpoint',
'fetchDiffFilesMeta',
'fetchDiffFilesBatch',
'fetchFileByFile',
@@ -396,7 +416,6 @@ export default {
'setFileForcedOpen',
'fetchCoverageFiles',
'fetchCodequality',
- 'fetchSast',
'rereadNoteHash',
'startRenderDiffsQueue',
'assignDiscussionsToDiff',
@@ -412,6 +431,11 @@ export default {
closeDrawer() {
this.setDrawer({});
},
+ fetchScannerFindingsError() {
+ createAlert({
+ message: __('Something went wrong fetching the Scanner Findings. Please try again.'),
+ });
+ },
subscribeToEvents() {
notesEventHub.$once('fetchDiffData', this.fetchData);
notesEventHub.$on('refetchDiffData', this.refetchDiffData);
@@ -419,8 +443,13 @@ export default {
diffsEventHub.$on('diffFilesModified', this.setDiscussions);
diffsEventHub.$on('doneLoadingBatches', this.autoScroll);
diffsEventHub.$on(EVT_MR_PREPARED, this.fetchData);
+ diffsEventHub.$on(EVT_DISCUSSIONS_ASSIGNED, this.handleHash);
+ },
+ getMRCodequalityAndSecurityReportStopPolling(query) {
+ query.stopPolling();
},
unsubscribeFromEvents() {
+ diffsEventHub.$off(EVT_DISCUSSIONS_ASSIGNED, this.handleHash);
diffsEventHub.$off(EVT_MR_PREPARED, this.fetchData);
diffsEventHub.$off('doneLoadingBatches', this.autoScroll);
diffsEventHub.$off('diffFilesModified', this.setDiscussions);
@@ -436,15 +465,27 @@ export default {
const idx = this.diffs.findIndex((diffFile) => diffFile.file_hash === sha1InHash);
const file = this.diffs[idx];
+ if (!isCollapsed(file)) return;
+
this.loadCollapsedDiff({ file })
.then(() => {
this.setDiscussions();
- this.scrollVirtualScrollerToIndex(idx);
this.setFileForcedOpen({ filePath: file.new_path });
+
+ this.$nextTick(() => this.scrollVirtualScrollerToIndex(idx));
})
.catch(() => {});
}
},
+ handleHash() {
+ if (this.viewDiffsFileByFile && !this.autoScrolled) {
+ const file = this.diffs[0];
+
+ if (file && !file.isLoadingFullFile) {
+ requestIdleCallback(() => this.slowHashHandler());
+ }
+ }
+ },
navigateToDiffFileNumber(number) {
this.navigateToDiffFileIndex(number - 1);
},
@@ -482,7 +523,7 @@ export default {
})
.catch(() => {
createAlert({
- message: __('Something went wrong on our end. Please try again!'),
+ message: __('Something went wrong on our end. Please try again.'),
});
});
}
@@ -499,7 +540,7 @@ export default {
})
.catch(() => {
createAlert({
- message: __('Something went wrong on our end. Please try again!'),
+ message: __('Something went wrong on our end. Please try again.'),
});
});
}
@@ -512,10 +553,6 @@ export default {
this.fetchCodequality();
}
- if (this.endpointSast) {
- this.fetchSast();
- }
-
if (!this.isNotesFetched) {
notesEventHub.$emit('fetchNotesData');
}
@@ -641,7 +678,7 @@ export default {
<template>
<div v-show="shouldShow">
- <findings-drawer :drawer="activeDrawer" @close="closeDrawer" />
+ <findings-drawer :project="activeProject" :drawer="activeDrawer" @close="closeDrawer" />
<div v-if="isLoading || !isTreeLoaded" class="loading"><gl-loading-icon size="lg" /></div>
<div v-else id="diffs" :class="{ active: shouldShow }" class="diffs tab-pane">
<compare-versions :diff-files-count-text="numTotalFiles" />
diff --git a/app/assets/javascripts/diffs/components/commit_item.vue b/app/assets/javascripts/diffs/components/commit_item.vue
index 3746ab9427f..7493bd5fdf7 100644
--- a/app/assets/javascripts/diffs/components/commit_item.vue
+++ b/app/assets/javascripts/diffs/components/commit_item.vue
@@ -135,7 +135,7 @@ export default {
<div
class="commit-detail flex-list gl-display-flex gl-justify-content-space-between gl-align-items-center gl-flex-grow-1 gl-min-w-0"
>
- <div class="commit-content" data-qa-selector="commit_content">
+ <div class="commit-content" data-testid="commit-content">
<a
v-safe-html:[$options.safeHtmlConfig]="commit.title_html"
:href="commit.commit_url"
diff --git a/app/assets/javascripts/diffs/components/diff_discussions.vue b/app/assets/javascripts/diffs/components/diff_discussions.vue
index 8915f32eadf..556f72059c2 100644
--- a/app/assets/javascripts/diffs/components/diff_discussions.vue
+++ b/app/assets/javascripts/diffs/components/diff_discussions.vue
@@ -39,12 +39,6 @@ export default {
},
methods: {
...mapActions(['toggleDiscussion']),
- ...mapActions('diffs', ['removeDiscussionsFromDiff']),
- deleteNoteHandler(discussion) {
- if (discussion.notes.length <= 1) {
- this.removeDiscussionsFromDiff(discussion);
- }
- },
isExpanded(discussion) {
return this.shouldCollapseDiscussions ? discussion.expanded : true;
},
@@ -90,7 +84,6 @@ export default {
:line="line"
:help-page-path="helpPagePath"
:should-scroll-to-note="false"
- @noteDeleted="deleteNoteHandler"
>
<template v-if="renderAvatarBadge" #avatar-badge>
<design-note-pin
diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue
index c74a4b47fcb..8c1cab20ece 100644
--- a/app/assets/javascripts/diffs/components/diff_file.vue
+++ b/app/assets/javascripts/diffs/components/diff_file.vue
@@ -208,11 +208,6 @@ export default {
this.manageViewedEffects();
},
},
- 'file.viewer.forceOpen': {
- handler: function fileForcedOpenHandler() {
- this.handleToggle();
- },
- },
'file.file_hash': {
handler: function hashChangeWatch(newHash, oldHash) {
if (
diff --git a/app/assets/javascripts/diffs/components/graphql/get_mr_codequality_and_security_reports.query.graphql b/app/assets/javascripts/diffs/components/graphql/get_mr_codequality_and_security_reports.query.graphql
new file mode 100644
index 00000000000..bd8f408f5a1
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/graphql/get_mr_codequality_and_security_reports.query.graphql
@@ -0,0 +1,82 @@
+query getMRCodequalityAndSecurityReports($fullPath: ID!, $iid: String!) {
+ project(fullPath: $fullPath) {
+ id
+ mergeRequest(iid: $iid) {
+ id
+ title
+ project {
+ id
+ nameWithNamespace
+ fullPath
+ }
+ hasSecurityReports
+ codequalityReportsComparer {
+ status
+ report {
+ status
+ newErrors {
+ description
+ fingerprint
+ severity
+ filePath
+ line
+ webUrl
+ engineName
+ }
+ resolvedErrors {
+ description
+ fingerprint
+ severity
+ filePath
+ line
+ webUrl
+ engineName
+ }
+ existingErrors {
+ description
+ fingerprint
+ severity
+ filePath
+ line
+ webUrl
+ engineName
+ }
+ summary {
+ errored
+ resolved
+ total
+ }
+ }
+ }
+ sastReport: findingReportsComparer(reportType: SAST) {
+ status
+ report {
+ added {
+ identifiers {
+ externalId
+ externalType
+ name
+ url
+ }
+ uuid
+ title
+ description
+ state
+ severity
+ foundByPipelineIid
+ location {
+ ... on VulnerabilityLocationSast {
+ file
+ startLine
+ endLine
+ vulnerableClass
+ vulnerableMethod
+ blobPath
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/diffs/components/graphql/get_mr_codequality_reports.query.graphql b/app/assets/javascripts/diffs/components/graphql/get_mr_codequality_reports.query.graphql
deleted file mode 100644
index b6920d0f6ec..00000000000
--- a/app/assets/javascripts/diffs/components/graphql/get_mr_codequality_reports.query.graphql
+++ /dev/null
@@ -1,46 +0,0 @@
-query getMRCodequalityReports($fullPath: ID!, $iid: String!) {
- project(fullPath: $fullPath) {
- id
- mergeRequest(iid: $iid) {
- id
- title
- codequalityReportsComparer {
- report {
- status
- newErrors {
- description
- fingerprint
- severity
- filePath
- line
- webUrl
- engineName
- }
- resolvedErrors {
- description
- fingerprint
- severity
- filePath
- line
- webUrl
- engineName
- }
- existingErrors {
- description
- fingerprint
- severity
- filePath
- line
- webUrl
- engineName
- }
- summary {
- errored
- resolved
- total
- }
- }
- }
- }
- }
-}
diff --git a/app/assets/javascripts/diffs/components/shared/findings_drawer.vue b/app/assets/javascripts/diffs/components/shared/findings_drawer.vue
index fddd455b17e..2c1a8305935 100644
--- a/app/assets/javascripts/diffs/components/shared/findings_drawer.vue
+++ b/app/assets/javascripts/diffs/components/shared/findings_drawer.vue
@@ -1,46 +1,56 @@
<script>
-import { GlDrawer, GlIcon, GlLink } from '@gitlab/ui';
-import SafeHtml from '~/vue_shared/directives/safe_html';
-import { s__ } from '~/locale';
+import { GlBadge, GlDrawer, GlIcon, GlLink } from '@gitlab/ui';
+import { __, s__ } from '~/locale';
import { DRAWER_Z_INDEX } from '~/lib/utils/constants';
-import { SEVERITY_CLASSES, SEVERITY_ICONS } from '~/ci/reports/codequality_report/constants';
+import { getSeverity } from '~/ci/reports/utils';
import { getContentWrapperHeight } from '~/lib/utils/dom_utils';
+import DrawerItem from './findings_drawer_item.vue';
export const i18n = {
- severity: s__('FindingsDrawer|Severity:'),
- engine: s__('FindingsDrawer|Engine:'),
- category: s__('FindingsDrawer|Category:'),
- otherLocations: s__('FindingsDrawer|Other locations:'),
+ name: __('Name'),
+ description: __('Description'),
+ status: __('Status'),
+ sast: __('SAST'),
+ engine: __('Engine'),
+ identifiers: __('Identifiers'),
+ project: __('Project'),
+ file: __('File'),
+ tool: __('Tool'),
+ codeQualityFinding: s__('FindingsDrawer|Code Quality Finding'),
+ sastFinding: s__('FindingsDrawer|SAST Finding'),
+ codeQuality: s__('FindingsDrawer|Code Quality'),
+ detected: s__('FindingsDrawer|Detected in pipeline'),
};
+export const codeQuality = 'codeQuality';
export default {
i18n,
- components: { GlDrawer, GlIcon, GlLink },
- directives: {
- SafeHtml,
- },
+ codeQuality,
+ components: { GlBadge, GlDrawer, GlIcon, GlLink, DrawerItem },
props: {
drawer: {
type: Object,
required: true,
},
- },
- safeHtmlConfig: {
- ALLOWED_TAGS: ['a', 'h1', 'h2', 'p'],
- ALLOWED_ATTR: ['href', 'rel'],
+ project: {
+ type: Object,
+ required: false,
+ default: () => {},
+ },
},
computed: {
getDrawerHeaderHeight() {
return getContentWrapperHeight();
},
+ isCodeQuality() {
+ return this.drawer.scale === this.$options.codeQuality;
+ },
},
DRAWER_Z_INDEX,
methods: {
- severityClass(severity) {
- return SEVERITY_CLASSES[severity] || SEVERITY_CLASSES.unknown;
- },
- severityIcon(severity) {
- return SEVERITY_ICONS[severity] || SEVERITY_ICONS.unknown;
+ getSeverity,
+ concatIdentifierName(name, index) {
+ return name + (index !== this.drawer.identifiers.length - 1 ? ', ' : '');
},
},
};
@@ -54,57 +64,82 @@ export default {
@close="$emit('close')"
>
<template #title>
- <h2 data-testid="findings-drawer-heading" class="gl-font-size-h2 gl-mt-0 gl-mb-0">
- {{ drawer.description }}
+ <h2 class="drawer-heading gl-font-base gl-mt-0 gl-mb-0">
+ <gl-icon
+ :size="12"
+ :name="getSeverity(drawer).name"
+ :class="getSeverity(drawer).class"
+ class="inline-findings-severity-icon gl-vertical-align-baseline!"
+ />
+ <span class="drawer-heading-severity">{{ drawer.severity }}</span>
+ {{ isCodeQuality ? $options.i18n.codeQualityFinding : $options.i18n.sastFinding }}
</h2>
</template>
<template #default>
<ul class="gl-list-style-none gl-border-b-initial gl-mb-0 gl-pb-0!">
- <li data-testid="findings-drawer-severity" class="gl-mb-4">
- <span class="gl-font-weight-bold">{{ $options.i18n.severity }}</span>
- <gl-icon
- data-testid="findings-drawer-severity-icon"
- :size="12"
- :name="severityIcon(drawer.severity)"
- :class="severityClass(drawer.severity)"
- class="inline-findings-severity-icon"
- />
+ <drawer-item v-if="drawer.title" :description="$options.i18n.name" :value="drawer.title" />
+
+ <drawer-item v-if="drawer.state" :description="$options.i18n.status">
+ <template #value>
+ <gl-badge variant="warning" class="text-capitalize">{{ drawer.state }}</gl-badge>
+ </template>
+ </drawer-item>
+
+ <drawer-item
+ v-if="drawer.description"
+ :description="$options.i18n.description"
+ :value="drawer.description"
+ />
+
+ <drawer-item
+ v-if="project && drawer.scale !== $options.codeQuality"
+ :description="$options.i18n.project"
+ >
+ <template #value>
+ <gl-link :href="`/${project.fullPath}`">{{ project.nameWithNamespace }}</gl-link>
+ </template>
+ </drawer-item>
+
+ <drawer-item v-if="drawer.location || drawer.webUrl" :description="$options.i18n.file">
+ <template #value>
+ <span v-if="drawer.webUrl && drawer.filePath && drawer.line">
+ <gl-link :href="drawer.webUrl">{{ drawer.filePath }}:{{ drawer.line }}</gl-link>
+ </span>
+ <span v-else-if="drawer.location">
+ {{ drawer.location.file }}:{{ drawer.location.startLine }}
+ </span>
+ </template>
+ </drawer-item>
+
+ <drawer-item
+ v-if="drawer.identifiers && drawer.identifiers.length"
+ :description="$options.i18n.identifiers"
+ >
+ <template #value>
+ <span v-for="(identifier, index) in drawer.identifiers" :key="identifier.externalId">
+ <gl-link v-if="identifier.url" :href="identifier.url">
+ {{ concatIdentifierName(identifier.name, index) }}
+ </gl-link>
+ <span v-else>
+ {{ concatIdentifierName(identifier.name, index) }}
+ </span>
+ </span>
+ </template>
+ </drawer-item>
+
+ <drawer-item
+ v-if="drawer.scale"
+ :description="$options.i18n.tool"
+ :value="isCodeQuality ? $options.i18n.codeQuality : $options.i18n.sast"
+ />
- {{ drawer.severity }}
- </li>
- <li data-testid="findings-drawer-engine" class="gl-mb-4">
- <span class="gl-font-weight-bold">{{ $options.i18n.engine }}</span>
- {{ drawer.engineName }}
- </li>
- <li data-testid="findings-drawer-category" class="gl-mb-4">
- <span class="gl-font-weight-bold">{{ $options.i18n.category }}</span>
- {{ drawer.categories ? drawer.categories[0] : '' }}
- </li>
- <li data-testid="findings-drawer-other-locations" class="gl-mb-4">
- <span class="gl-font-weight-bold gl-mb-3 gl-display-block">{{
- $options.i18n.otherLocations
- }}</span>
- <ul class="gl-pl-6">
- <li
- v-for="otherLocation in drawer.otherLocations"
- :key="otherLocation.path"
- class="gl-mb-1"
- >
- <gl-link
- data-testid="findings-drawer-other-locations-link"
- :href="otherLocation.href"
- >{{ otherLocation.path }}</gl-link
- >
- </li>
- </ul>
- </li>
+ <drawer-item
+ v-if="drawer.engineName"
+ :description="$options.i18n.engine"
+ :value="drawer.engineName"
+ />
</ul>
- <span
- v-safe-html:[$options.safeHtmlConfig]="drawer.content ? drawer.content.body : ''"
- data-testid="findings-drawer-body"
- class="drawer-body gl-display-block gl-px-3 gl-py-0!"
- ></span>
</template>
</gl-drawer>
</template>
diff --git a/app/assets/javascripts/diffs/components/shared/findings_drawer_item.vue b/app/assets/javascripts/diffs/components/shared/findings_drawer_item.vue
new file mode 100644
index 00000000000..f488e8e3bb1
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/shared/findings_drawer_item.vue
@@ -0,0 +1,30 @@
+<script>
+export default {
+ props: {
+ description: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ value: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+};
+</script>
+<template>
+ <li class="gl-mb-4">
+ <p class="gl-line-height-20">
+ <span
+ data-testid="findings-drawer-item-description"
+ class="gl-font-weight-bold gl-display-block gl-mb-1"
+ >{{ description }}</span
+ >
+ <slot name="value">
+ <span data-testid="findings-drawer-item-value-prop">{{ value }}</span>
+ </slot>
+ </p>
+ </li>
+</template>
diff --git a/app/assets/javascripts/diffs/components/tree_list.vue b/app/assets/javascripts/diffs/components/tree_list.vue
index f4715c591b2..07984beb709 100644
--- a/app/assets/javascripts/diffs/components/tree_list.vue
+++ b/app/assets/javascripts/diffs/components/tree_list.vue
@@ -3,22 +3,20 @@ import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
// eslint-disable-next-line no-restricted-imports
import { mapActions, mapGetters, mapState } from 'vuex';
import micromatch from 'micromatch';
-import { debounce } from 'lodash';
import { getModifierKey } from '~/constants';
import { s__, sprintf } from '~/locale';
import { RecycleScroller } from 'vendor/vue-virtual-scroller';
-import { contentTop } from '~/lib/utils/common_utils';
import DiffFileRow from './diff_file_row.vue';
+import TreeListHeight from './tree_list_height.vue';
const MODIFIER_KEY = getModifierKey();
-const MAX_ITEMS_ON_NARROW_SCREEN = 8;
-const BOTTOM_MARGIN = 16;
export default {
directives: {
GlTooltip: GlTooltipDirective,
},
components: {
+ TreeListHeight,
GlIcon,
DiffFileRow,
RecycleScroller,
@@ -32,17 +30,10 @@ export default {
data() {
return {
search: '',
- scrollerHeight: 0,
- rowHeight: 0,
- debouncedHeightCalc: null,
- reviewBarHeight: 0,
- largeBreakpointSize: 0,
};
},
computed: {
...mapState('diffs', ['tree', 'renderTreeList', 'currentDiffFileId', 'viewedDiffFileIds']),
- ...mapState('batchComments', ['reviewBarRendered']),
- ...mapGetters('batchComments', ['draftsCount']),
...mapGetters('diffs', ['allBlobs']),
filteredTreeList() {
let search = this.search.toLowerCase().trim();
@@ -95,76 +86,21 @@ export default {
return result;
},
- reviewBarEnabled() {
- return this.draftsCount > 0;
- },
- },
- watch: {
- reviewBarEnabled() {
- this.debouncedHeightCalc();
- },
- calculateReviewBarHeight() {
- this.debouncedHeightCalc();
- },
- },
- created() {
- this.debouncedHeightCalc = debounce(this.calculateScrollerHeight, 50);
- },
- mounted() {
- const heightProp = getComputedStyle(this.$refs.wrapper).getPropertyValue('--file-row-height');
- const breakpointProp = getComputedStyle(window.document.body).getPropertyValue(
- '--breakpoint-lg',
- );
- this.largeBreakpointSize = parseInt(breakpointProp, 10);
- this.rowHeight = parseInt(heightProp, 10);
- this.calculateScrollerHeight();
- let stop;
- // eslint-disable-next-line prefer-const
- stop = this.$watch(
- () => this.reviewBarRendered,
- (enabled) => {
- if (!enabled) return;
- this.calculateReviewBarHeight();
- stop();
- },
- { immediate: true },
- );
- window.addEventListener('resize', this.debouncedHeightCalc, { passive: true });
- },
- beforeDestroy() {
- window.removeEventListener('resize', this.debouncedHeightCalc, { passive: true });
},
methods: {
...mapActions('diffs', ['toggleTreeOpen', 'goToFile']),
clearSearch() {
this.search = '';
},
- calculateScrollerHeight() {
- if (window.matchMedia(`(max-width: ${this.largeBreakpointSize - 1}px)`).matches) {
- this.calculateMobileScrollerHeight();
- } else {
- let clipping = BOTTOM_MARGIN;
- if (this.reviewBarEnabled) clipping += this.reviewBarHeight;
- this.scrollerHeight = this.$refs.scrollRoot.clientHeight - clipping;
- }
- },
- calculateMobileScrollerHeight() {
- const maxItems = Math.min(MAX_ITEMS_ON_NARROW_SCREEN, this.flatFilteredTreeList.length);
- this.scrollerHeight = Math.min(maxItems * this.rowHeight, window.innerHeight - contentTop());
- },
- calculateReviewBarHeight() {
- this.reviewBarHeight = document.querySelector('.js-review-bar')?.offsetHeight || 0;
- },
},
searchPlaceholder: sprintf(s__('MergeRequest|Search (e.g. *.vue) (%{MODIFIER_KEY}P)'), {
MODIFIER_KEY,
}),
- DiffFileRow,
};
</script>
<template>
- <div ref="wrapper" class="tree-list-holder d-flex flex-column" data-testid="file-tree-container">
+ <div class="tree-list-holder d-flex flex-column" data-testid="file-tree-container">
<div class="gl-pb-3 position-relative tree-list-search d-flex">
<div class="flex-fill d-flex">
<gl-icon name="search" class="gl-absolute gl-top-3 gl-left-3 tree-list-icon" />
@@ -189,41 +125,41 @@ export default {
</button>
</div>
</div>
- <div
- ref="scrollRoot"
- :class="{ 'tree-list-blobs': !renderTreeList || search }"
- class="gl-flex-grow-1 mr-tree-list"
- >
- <recycle-scroller
- v-if="flatFilteredTreeList.length"
- :style="{ height: `${scrollerHeight}px` }"
- :items="flatFilteredTreeList"
- :item-size="rowHeight"
- :buffer="100"
- key-field="key"
- >
- <template #default="{ item }">
- <diff-file-row
- :file="item"
- :level="item.level"
- :viewed-files="viewedDiffFileIds"
- :hide-file-stats="hideFileStats"
- :current-diff-file-id="currentDiffFileId"
- :style="{ '--level': item.level }"
- :class="{ 'tree-list-parent': item.level > 0 }"
- class="gl-relative"
- @toggleTreeOpen="toggleTreeOpen"
- @clickFile="(path) => goToFile({ path })"
- />
- </template>
- <template #after>
- <div class="tree-list-gutter"></div>
- </template>
- </recycle-scroller>
- <p v-else class="prepend-top-20 append-bottom-20 text-center">
- {{ s__('MergeRequest|No files found') }}
- </p>
- </div>
+ <tree-list-height class="gl-flex-grow-1 gl-min-h-0" :items-count="flatFilteredTreeList.length">
+ <template #default="{ scrollerHeight, rowHeight }">
+ <div :class="{ 'tree-list-blobs': !renderTreeList || search }" class="mr-tree-list">
+ <recycle-scroller
+ v-if="flatFilteredTreeList.length"
+ :style="{ height: `${scrollerHeight}px` }"
+ :items="flatFilteredTreeList"
+ :item-size="rowHeight"
+ :buffer="100"
+ key-field="key"
+ >
+ <template #default="{ item }">
+ <diff-file-row
+ :file="item"
+ :level="item.level"
+ :viewed-files="viewedDiffFileIds"
+ :hide-file-stats="hideFileStats"
+ :current-diff-file-id="currentDiffFileId"
+ :style="{ '--level': item.level }"
+ :class="{ 'tree-list-parent': item.level > 0 }"
+ class="gl-relative"
+ @toggleTreeOpen="toggleTreeOpen"
+ @clickFile="(path) => goToFile({ path })"
+ />
+ </template>
+ <template #after>
+ <div class="tree-list-gutter"></div>
+ </template>
+ </recycle-scroller>
+ <p v-else class="prepend-top-20 append-bottom-20 text-center">
+ {{ s__('MergeRequest|No files found') }}
+ </p>
+ </div>
+ </template>
+ </tree-list-height>
</div>
</template>
diff --git a/app/assets/javascripts/diffs/components/tree_list_height.vue b/app/assets/javascripts/diffs/components/tree_list_height.vue
new file mode 100644
index 00000000000..4da94cacd75
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/tree_list_height.vue
@@ -0,0 +1,108 @@
+<script>
+import { debounce } from 'lodash';
+// eslint-disable-next-line no-restricted-imports
+import { mapState, mapGetters } from 'vuex';
+import { contentTop } from '~/lib/utils/common_utils';
+
+const MAX_ITEMS_ON_NARROW_SCREEN = 8;
+// Should be enough for the very long titles (10+ lines) on the max smallest screen
+const MAX_SCROLL_Y = 600;
+const BOTTOM_OFFSET = 16;
+
+export default {
+ name: 'TreeListHeight',
+ props: {
+ itemsCount: {
+ type: Number,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ scrollerHeight: 0,
+ rowHeight: 0,
+ reviewBarHeight: 0,
+ scrollY: 0,
+ isNarrowScreen: false,
+ mediaQueryMatch: null,
+ };
+ },
+ computed: {
+ ...mapState('batchComments', ['reviewBarRendered']),
+ ...mapGetters('batchComments', ['draftsCount']),
+ reviewBarEnabled() {
+ return this.draftsCount > 0;
+ },
+ debouncedHeightCalc() {
+ return debounce(this.calculateScrollerHeight, 100);
+ },
+ debouncedRecordScroll() {
+ return debounce(this.recordScroll, 50);
+ },
+ },
+ watch: {
+ reviewBarRendered: {
+ handler(rendered) {
+ if (!rendered || this.reviewBarHeight) return;
+ this.reviewBarHeight = document.querySelector('.js-review-bar').offsetHeight;
+ this.debouncedHeightCalc();
+ },
+ immediate: true,
+ },
+ reviewBarEnabled: 'debouncedHeightCalc',
+ scrollY: 'debouncedHeightCalc',
+ isNarrowScreen: 'recordScroll',
+ },
+ mounted() {
+ const computedStyles = getComputedStyle(this.$refs.scrollRoot);
+ this.rowHeight = parseInt(computedStyles.getPropertyValue('--file-row-height'), 10);
+
+ const largeBreakpointSize = parseInt(computedStyles.getPropertyValue('--breakpoint-lg'), 10);
+ this.mediaQueryMatch = window.matchMedia(`(max-width: ${largeBreakpointSize - 1}px)`);
+ this.isNarrowScreen = this.mediaQueryMatch.matches;
+ this.mediaQueryMatch.addEventListener('change', this.handleMediaMatch);
+
+ window.addEventListener('resize', this.debouncedHeightCalc, { passive: true });
+ window.addEventListener('scroll', this.debouncedRecordScroll, { passive: true });
+
+ this.calculateScrollerHeight();
+ },
+ beforeDestroy() {
+ this.mediaQueryMatch.removeEventListener('change', this.handleMediaMatch);
+ this.mediaQueryMatch = null;
+ window.removeEventListener('resize', this.debouncedHeightCalc, { passive: true });
+ window.removeEventListener('scroll', this.debouncedRecordScroll, { passive: true });
+ },
+ methods: {
+ recordScroll() {
+ const { scrollY } = window;
+ if (scrollY > MAX_SCROLL_Y || this.isNarrowScreen) {
+ this.scrollY = MAX_SCROLL_Y;
+ } else {
+ this.scrollY = window.scrollY;
+ }
+ },
+ handleMediaMatch({ matches }) {
+ this.isNarrowScreen = matches;
+ },
+ calculateScrollerHeight() {
+ if (this.isNarrowScreen) {
+ const maxItems = Math.min(MAX_ITEMS_ON_NARROW_SCREEN, this.itemsCount);
+ const maxHeight = maxItems * this.rowHeight;
+ this.scrollerHeight = Math.min(maxHeight, window.innerHeight - contentTop());
+ } else {
+ const { y } = this.$refs.scrollRoot.getBoundingClientRect();
+ const reviewBarOffset = this.reviewBarEnabled ? this.reviewBarHeight : 0;
+ // distance from element's top vertical position in the viewport to the bottom of the viewport minus offsets
+ this.scrollerHeight = window.innerHeight - y - reviewBarOffset - BOTTOM_OFFSET;
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <div ref="scrollRoot">
+ <slot :scroller-height="scrollerHeight" :row-height="rowHeight"></slot>
+ </div>
+</template>
diff --git a/app/assets/javascripts/diffs/constants.js b/app/assets/javascripts/diffs/constants.js
index 575cd05ceb8..e48eb10753c 100644
--- a/app/assets/javascripts/diffs/constants.js
+++ b/app/assets/javascripts/diffs/constants.js
@@ -82,6 +82,7 @@ export const RENAMED_DIFF_TRANSITIONS = {
// MR Diffs known events
export const EVT_MR_PREPARED = 'mr:asyncPreparationFinished';
export const EVT_EXPAND_ALL_FILES = 'mr:diffs:expandAllFiles';
+export const EVT_DISCUSSIONS_ASSIGNED = 'mr:diffs:discussionsAssigned';
export const EVT_PERF_MARK_FILE_TREE_START = 'mr:diffs:perf:fileTreeStart';
export const EVT_PERF_MARK_FILE_TREE_END = 'mr:diffs:perf:fileTreeEnd';
export const EVT_PERF_MARK_DIFF_FILES_START = 'mr:diffs:perf:filesStart';
diff --git a/app/assets/javascripts/diffs/index.js b/app/assets/javascripts/diffs/index.js
index c0b6c8159dc..034dd4cf6d2 100644
--- a/app/assets/javascripts/diffs/index.js
+++ b/app/assets/javascripts/diffs/index.js
@@ -36,7 +36,7 @@ export default function initDiffsApp(store = notesStore) {
iid: dataset.iid || '',
endpointCoverage: dataset.endpointCoverage || '',
endpointCodequality: dataset.endpointCodequality || '',
- endpointSast: dataset.endpointSast || '',
+ sastReportAvailable: dataset.endpointSast,
helpPagePath: dataset.helpPagePath,
currentUser: JSON.parse(dataset.currentUserData) || {},
changesEmptyStateIllustration: dataset.changesEmptyStateIllustration,
@@ -86,7 +86,7 @@ export default function initDiffsApp(store = notesStore) {
iid: this.iid,
endpointCoverage: this.endpointCoverage,
endpointCodequality: this.endpointCodequality,
- endpointSast: this.endpointSast,
+ sastReportAvailable: this.sastReportAvailable,
currentUser: this.currentUser,
helpPagePath: this.helpPagePath,
shouldShow: this.activeTab === 'diffs',
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index ed8ae795bda..fcaf8e99b2d 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -49,6 +49,7 @@ import {
TRACKING_MULTIPLE_FILES_MODE,
EVT_MR_PREPARED,
FILE_DIFF_POSITION_TYPE,
+ EVT_DISCUSSIONS_ASSIGNED,
} from '../constants';
import {
DISCUSSION_SINGLE_DIFF_FAILED,
@@ -89,6 +90,7 @@ export const setBaseConfig = ({ commit }, options) => {
viewDiffsFileByFile,
mrReviews,
diffViewType,
+ perPage,
} = options;
commit(types.SET_BASE_CONFIG, {
endpoint,
@@ -104,6 +106,7 @@ export const setBaseConfig = ({ commit }, options) => {
viewDiffsFileByFile,
mrReviews,
diffViewType,
+ perPage,
});
Array.from(new Set(Object.values(mrReviews).flat())).forEach((id) => {
@@ -206,7 +209,7 @@ export const fetchFileByFile = async ({ state, getters, commit }) => {
};
export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
- let perPage = state.viewDiffsFileByFile ? 1 : 5;
+ let perPage = state.viewDiffsFileByFile ? 1 : state.perPage;
let increaseAmount = 1.4;
const startPage = 0;
const id = window?.location?.hash;
@@ -413,12 +416,16 @@ export const assignDiscussionsToDiff = (
}
Vue.nextTick(() => {
- notesEventHub.$emit('scrollToDiscussion');
+ eventHub.$emit(EVT_DISCUSSIONS_ASSIGNED);
});
};
export const removeDiscussionsFromDiff = ({ commit }, removeDiscussion) => {
- const { file_hash: fileHash, line_code: lineCode, id } = removeDiscussion;
+ const {
+ diff_file: { file_hash: fileHash },
+ line_code: lineCode,
+ id,
+ } = removeDiscussion;
commit(types.REMOVE_LINE_DISCUSSIONS_FOR_FILE, { fileHash, lineCode, id });
};
diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js
index 31369b169f5..08c195469e3 100644
--- a/app/assets/javascripts/diffs/store/mutations.js
+++ b/app/assets/javascripts/diffs/store/mutations.js
@@ -39,6 +39,7 @@ export default {
viewDiffsFileByFile,
mrReviews,
diffViewType,
+ perPage,
} = options;
Object.assign(state, {
endpoint,
@@ -54,6 +55,7 @@ export default {
viewDiffsFileByFile,
mrReviews,
diffViewType,
+ perPage,
});
},
@@ -198,9 +200,10 @@ export default {
return {
...line,
discussionsExpanded:
- line.discussions && line.discussions.length
+ line.discussionsExpanded ||
+ (line.discussions && line.discussions.length
? line.discussions.some((disc) => !disc.resolved) || isLineNoteTargeted
- : false,
+ : false),
};
};
diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js
index 15d2ab71bc8..fb467a606b9 100644
--- a/app/assets/javascripts/diffs/store/utils.js
+++ b/app/assets/javascripts/diffs/store/utils.js
@@ -338,7 +338,7 @@ function prepareLine(line, file) {
problems.brokenSymlink || problems.fileOnlyMoved || problems.brokenLineCode,
),
rich_text: cleanRichText(line.rich_text),
- discussionsExpanded: true,
+ discussionsExpanded: false,
discussions: [],
hasForm: false,
text: undefined,
diff --git a/app/assets/javascripts/diffs/utils/file_reviews.js b/app/assets/javascripts/diffs/utils/file_reviews.js
index 227be4e4a6c..581d0b6055b 100644
--- a/app/assets/javascripts/diffs/utils/file_reviews.js
+++ b/app/assets/javascripts/diffs/utils/file_reviews.js
@@ -43,7 +43,7 @@ export function reviewable(file) {
}
export function markFileReview(reviews, file, reviewed = true) {
- const usableReviews = { ...(reviews || {}) };
+ const usableReviews = { ...reviews };
const updatedReviews = usableReviews;
let fileReviews;
diff --git a/app/assets/javascripts/diffs/utils/sort_findings_by_file.js b/app/assets/javascripts/diffs/utils/sort_findings_by_file.js
index 3a285e80ace..3cf6dc169e4 100644
--- a/app/assets/javascripts/diffs/utils/sort_findings_by_file.js
+++ b/app/assets/javascripts/diffs/utils/sort_findings_by_file.js
@@ -1,10 +1,17 @@
export function sortFindingsByFile(newErrors = []) {
const files = {};
- newErrors.forEach(({ filePath, line, description, severity }) => {
+ newErrors.forEach(({ line, description, severity, filePath, webUrl, engineName }) => {
if (!files[filePath]) {
files[filePath] = [];
}
- files[filePath].push({ line, description, severity: severity.toLowerCase() });
+ files[filePath].push({
+ line,
+ description,
+ severity: severity.toLowerCase(),
+ filePath,
+ webUrl,
+ engineName,
+ });
});
const sortedFiles = Object.keys(files)
diff --git a/app/assets/javascripts/editor/constants.js b/app/assets/javascripts/editor/constants.js
index 2be671ec7d8..2d50b7e4319 100644
--- a/app/assets/javascripts/editor/constants.js
+++ b/app/assets/javascripts/editor/constants.js
@@ -53,11 +53,6 @@ export const EDITOR_EXTENSION_STORE_IS_MISSING_ERROR = s__(
export const EXTENSION_BASE_LINE_LINK_ANCHOR_CLASS = 'link-anchor';
export const EXTENSION_BASE_LINE_NUMBERS_CLASS = 'line-numbers';
-// For CI config schemas the filename must match
-// '*.gitlab-ci.yml' regardless of project configuration.
-// https://gitlab.com/gitlab-org/gitlab/-/issues/293641
-export const EXTENSION_CI_SCHEMA_FILE_NAME_MATCH = '.gitlab-ci.yml';
-
export const EXTENSION_MARKDOWN_PREVIEW_PANEL_CLASS = 'md';
export const EXTENSION_MARKDOWN_PREVIEW_PANEL_PARENT_CLASS = 'source-editor-preview';
export const EXTENSION_MARKDOWN_PREVIEW_ACTION_ID = 'markdown-preview';
diff --git a/app/assets/javascripts/editor/schema/ci.json b/app/assets/javascripts/editor/schema/ci.json
index 0420ffb82f5..308a68544bc 100644
--- a/app/assets/javascripts/editor/schema/ci.json
+++ b/app/assets/javascripts/editor/schema/ci.json
@@ -2093,9 +2093,15 @@
"description": "A path to a directory that contains the files to be published with Pages",
"type": "string"
},
- "pages_path_prefix": {
- "description": "The path prefix identifier for this version of pages. Allows creation of multiple versions of the same site with different path prefixes",
- "type": "string"
+ "pages": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "path_prefix": {
+ "type": "string",
+ "markdownDescription": "The GitLab Pages URL path prefix used in this version of pages."
+ }
+ }
}
},
"oneOf": [
diff --git a/app/assets/javascripts/emoji/awards_app/store/actions.js b/app/assets/javascripts/emoji/awards_app/store/actions.js
index 677c11277a3..54d98687684 100644
--- a/app/assets/javascripts/emoji/awards_app/store/actions.js
+++ b/app/assets/javascripts/emoji/awards_app/store/actions.js
@@ -1,4 +1,4 @@
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import axios from '~/lib/utils/axios_utils';
import { normalizeHeaders } from '~/lib/utils/common_utils';
import { joinPaths } from '~/lib/utils/url_utility';
diff --git a/app/assets/javascripts/emoji/components/picker.vue b/app/assets/javascripts/emoji/components/picker.vue
index 238f0d81b22..462420ba4e5 100644
--- a/app/assets/javascripts/emoji/components/picker.vue
+++ b/app/assets/javascripts/emoji/components/picker.vue
@@ -3,8 +3,8 @@
import { GlIcon, GlDropdown, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
import { findLastIndex } from 'lodash';
import VirtualList from 'vue-virtual-scroll-list';
-import { CATEGORY_NAMES, getEmojiCategoryMap, state } from '~/emoji';
-import { CATEGORY_ICON_MAP, FREQUENTLY_USED_KEY } from '../constants';
+import { getEmojiCategoryMap, state } from '~/emoji';
+import { CATEGORY_NAMES, CATEGORY_ICON_MAP, FREQUENTLY_USED_KEY } from '../constants';
import Category from './category.vue';
import EmojiList from './emoji_list.vue';
import { addToFrequentlyUsed, getEmojiCategories, hasFrequentlyUsedEmojis } from './utils';
diff --git a/app/assets/javascripts/emoji/constants.js b/app/assets/javascripts/emoji/constants.js
index 215ecbfe605..c8bcb79ad15 100644
--- a/app/assets/javascripts/emoji/constants.js
+++ b/app/assets/javascripts/emoji/constants.js
@@ -1,6 +1,18 @@
export const FREQUENTLY_USED_KEY = 'frequently_used';
export const FREQUENTLY_USED_COOKIE_KEY = 'frequently_used_emojis';
+export const CATEGORY_NAMES = [
+ FREQUENTLY_USED_KEY,
+ 'custom',
+ 'people',
+ 'activity',
+ 'nature',
+ 'food',
+ 'travel',
+ 'objects',
+ 'symbols',
+ 'flags',
+];
export const CATEGORY_ICON_MAP = {
[FREQUENTLY_USED_KEY]: 'history',
custom: 'tanuki',
diff --git a/app/assets/javascripts/emoji/index.js b/app/assets/javascripts/emoji/index.js
index 1fa81a000a5..f98369c2fde 100644
--- a/app/assets/javascripts/emoji/index.js
+++ b/app/assets/javascripts/emoji/index.js
@@ -8,7 +8,7 @@ import { getEmojiScoreWithIntent } from '~/emoji/utils';
import AccessorUtilities from '../lib/utils/accessor';
import axios from '../lib/utils/axios_utils';
import customEmojiQuery from './queries/custom_emoji.query.graphql';
-import { CACHE_KEY, CACHE_VERSION_KEY, CATEGORY_ICON_MAP, FREQUENTLY_USED_KEY } from './constants';
+import { CACHE_KEY, CACHE_VERSION_KEY, CATEGORY_NAMES, FREQUENTLY_USED_KEY } from './constants';
let emojiMap = null;
let validEmojiNames = null;
@@ -20,22 +20,27 @@ export const state = Vue.observable({
export const FALLBACK_EMOJI_KEY = 'grey_question';
// Keep the version in sync with `lib/gitlab/emoji.rb`
-export const EMOJI_VERSION = '2';
+export const EMOJI_VERSION = '3';
const isLocalStorageAvailable = AccessorUtilities.canUseLocalStorage();
async function loadEmoji() {
- if (
- isLocalStorageAvailable &&
- window.localStorage.getItem(CACHE_VERSION_KEY) === EMOJI_VERSION &&
- window.localStorage.getItem(CACHE_KEY)
- ) {
- const emojis = JSON.parse(window.localStorage.getItem(CACHE_KEY));
- // Workaround because the pride flag is broken in EMOJI_VERSION = '1'
- if (emojis.gay_pride_flag) {
- emojis.gay_pride_flag.e = '🏳️‍🌈';
+ try {
+ window.localStorage.removeItem(CACHE_VERSION_KEY);
+ } catch {
+ // Cleanup after us and remove the old EMOJI_VERSION_KEY
+ }
+
+ try {
+ if (isLocalStorageAvailable) {
+ const parsed = JSON.parse(window.localStorage.getItem(CACHE_KEY));
+ if (parsed?.EMOJI_VERSION === EMOJI_VERSION && parsed.data) {
+ return parsed.data;
+ }
}
- return emojis;
+ } catch {
+ // Maybe the stored data was corrupted or the version didn't match.
+ // Let's not error out.
}
// We load the JSON file direct from the server
@@ -44,21 +49,31 @@ async function loadEmoji() {
const { data } = await axios.get(
`${gon.relative_url_root || ''}/-/emojis/${EMOJI_VERSION}/emojis.json`,
);
- window.localStorage.setItem(CACHE_VERSION_KEY, EMOJI_VERSION);
- window.localStorage.setItem(CACHE_KEY, JSON.stringify(data));
+
+ try {
+ window.localStorage.setItem(CACHE_KEY, JSON.stringify({ data, EMOJI_VERSION }));
+ } catch {
+ // Setting data in localstorage may fail when storage quota is exceeded.
+ // We should continue even when this fails.
+ }
+
return data;
}
async function loadEmojiWithNames() {
const emojiRegex = emojiRegexFactory();
- return Object.entries(await loadEmoji()).reduce((acc, [key, value]) => {
- // Filter out entries which aren't emojis
- if (value.e.match(emojiRegex)?.[0] === value.e) {
- acc[key] = { ...value, name: key };
- }
- return acc;
- }, {});
+ return (await loadEmoji()).reduce(
+ (acc, emoji) => {
+ // Filter out entries which aren't emojis
+ if (emoji.e.match(emojiRegex)?.[0] === emoji.e) {
+ acc.emojis[emoji.n] = { ...emoji, name: emoji.n };
+ acc.names.push(emoji.n);
+ }
+ return acc;
+ },
+ { emojis: {}, names: [] },
+ );
}
export async function loadCustomEmojiWithNames() {
@@ -71,31 +86,35 @@ export async function loadCustomEmojiWithNames() {
},
});
- return data?.group?.customEmoji?.nodes?.reduce((acc, e) => {
- // Map the custom emoji into the format of the normal emojis
- acc[e.name] = {
- c: 'custom',
- d: e.name,
- e: undefined,
- name: e.name,
- src: e.url,
- u: 'custom',
- };
+ return data?.group?.customEmoji?.nodes?.reduce(
+ (acc, e) => {
+ // Map the custom emoji into the format of the normal emojis
+ acc.emojis[e.name] = {
+ c: 'custom',
+ d: e.name,
+ e: undefined,
+ name: e.name,
+ src: e.url,
+ u: 'custom',
+ };
+ acc.names.push(e.name);
- return acc;
- }, {});
+ return acc;
+ },
+ { emojis: {}, names: [] },
+ );
}
- return {};
+ return { emojis: {}, names: [] };
}
async function prepareEmojiMap() {
return Promise.all([loadEmojiWithNames(), loadCustomEmojiWithNames()]).then((values) => {
emojiMap = {
- ...values[0],
- ...values[1],
+ ...values[0].emojis,
+ ...values[1].emojis,
};
- validEmojiNames = [...Object.keys(emojiMap), ...Object.keys(emojiAliases)];
+ validEmojiNames = [...values[0].names, ...values[1].names];
state.loading = false;
});
}
@@ -109,10 +128,6 @@ export function normalizeEmojiName(name) {
return Object.prototype.hasOwnProperty.call(emojiAliases, name) ? emojiAliases[name] : name;
}
-export function getValidEmojiNames() {
- return validEmojiNames;
-}
-
export function isEmojiNameValid(name) {
if (!emojiMap) {
// eslint-disable-next-line @gitlab/require-i18n-strings
@@ -122,10 +137,14 @@ export function isEmojiNameValid(name) {
return name in emojiMap || name in emojiAliases;
}
-export function getAllEmoji() {
+export function getEmojiMap() {
return emojiMap;
}
+export function getAllEmoji() {
+ return validEmojiNames.map((n) => emojiMap[n]);
+}
+
export function findCustomEmoji(name) {
return emojiMap[name];
}
@@ -218,8 +237,6 @@ export function searchEmoji(query) {
.sort(sortEmoji);
}
-export const CATEGORY_NAMES = Object.keys(CATEGORY_ICON_MAP);
-
let emojiCategoryMap;
export function getEmojiCategoryMap() {
if (!emojiCategoryMap && emojiMap) {
@@ -229,7 +246,7 @@ export function getEmojiCategoryMap() {
}
return { ...acc, [category]: [] };
}, {});
- Object.keys(emojiMap).forEach((name) => {
+ validEmojiNames.forEach((name) => {
const emoji = emojiMap[name];
if (emojiCategoryMap[emoji.c]) {
emojiCategoryMap[emoji.c].push(name);
diff --git a/app/assets/javascripts/ensure_data.js b/app/assets/javascripts/ensure_data.js
index 4566ab20258..ddb34e59144 100644
--- a/app/assets/javascripts/ensure_data.js
+++ b/app/assets/javascripts/ensure_data.js
@@ -1,6 +1,6 @@
import emptySvg from '@gitlab/svgs/dist/illustrations/security-dashboard-empty-state.svg?raw';
import { GlEmptyState } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { __ } from '~/locale';
export const ERROR_FETCHING_DATA_HEADER = __('Could not get the data properly');
diff --git a/app/assets/javascripts/entrypoints/analytics.js b/app/assets/javascripts/entrypoints/analytics.js
index 8eb265cb1e8..e18c4bc8742 100644
--- a/app/assets/javascripts/entrypoints/analytics.js
+++ b/app/assets/javascripts/entrypoints/analytics.js
@@ -14,4 +14,10 @@ if (appId && host) {
errorTracking: false,
},
});
+
+ const userId = window.gl?.snowplowStandardContext?.data?.user_id;
+
+ if (userId) {
+ window.glClient?.identify(userId);
+ }
}
diff --git a/app/assets/javascripts/environments/components/confirm_rollback_modal.vue b/app/assets/javascripts/environments/components/confirm_rollback_modal.vue
index 2cf71de7ea2..2bc65e4ad04 100644
--- a/app/assets/javascripts/environments/components/confirm_rollback_modal.vue
+++ b/app/assets/javascripts/environments/components/confirm_rollback_modal.vue
@@ -3,8 +3,8 @@
* Render modal to confirm rollback/redeploy.
*/
import { GlModal, GlSprintf, GlLink } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
import { escape } from 'lodash';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import csrf from '~/lib/utils/csrf';
import { __, s__, sprintf } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
diff --git a/app/assets/javascripts/environments/components/environment_form.vue b/app/assets/javascripts/environments/components/environment_form.vue
index 8ebba0e27bb..c6cf6b7e24b 100644
--- a/app/assets/javascripts/environments/components/environment_form.vue
+++ b/app/assets/javascripts/environments/components/environment_form.vue
@@ -193,6 +193,7 @@ export default {
headers: {
'GitLab-Agent-Id': getIdFromGraphQLId(this.selectedAgentId),
'Content-Type': 'application/json',
+ Accept: 'application/json',
...csrf.headers,
},
credentials: 'include',
diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue
index 08a1eacec7a..47edec8dcb0 100644
--- a/app/assets/javascripts/environments/components/environment_item.vue
+++ b/app/assets/javascripts/environments/components/environment_item.vue
@@ -752,7 +752,7 @@ export default {
:title="upcomingDeploymentTooltipText"
data-testid="upcoming-deployment-status-link"
>
- <ci-icon class="gl-mr-2" :status="upcomingDeployment.deployable.status" />
+ <ci-icon :status="upcomingDeployment.deployable.status" class="gl-mr-2" />
</gl-link>
</div>
<span
diff --git a/app/assets/javascripts/environments/components/environments_app.vue b/app/assets/javascripts/environments/components/environments_app.vue
index 795cbf5327a..4e8b75536a4 100644
--- a/app/assets/javascripts/environments/components/environments_app.vue
+++ b/app/assets/javascripts/environments/components/environments_app.vue
@@ -78,7 +78,7 @@ export default {
newEnvironmentButtonLabel: s__('Environments|New environment'),
reviewAppButtonLabel: s__('Environments|Enable review apps'),
cleanUpEnvsButtonLabel: s__('Environments|Clean up environments'),
- available: __('Available'),
+ active: __('Active'),
stopped: __('Stopped'),
prevPage: __('Go to previous page'),
nextPage: __('Go to next page'),
@@ -97,9 +97,7 @@ export default {
isStopStaleEnvModalVisible: false,
page: parseInt(page, 10),
pageInfo: {},
- scope: Object.values(ENVIRONMENTS_SCOPE).includes(scope)
- ? scope
- : ENVIRONMENTS_SCOPE.AVAILABLE,
+ scope: Object.values(ENVIRONMENTS_SCOPE).includes(scope) ? scope : ENVIRONMENTS_SCOPE.ACTIVE,
environmentToDelete: {},
environmentToRollback: {},
environmentToStop: {},
@@ -112,6 +110,9 @@ export default {
canSetupReviewApp() {
return this.environmentApp?.reviewApp?.canSetupReviewApp;
},
+ hasReviewApp() {
+ return this.environmentApp?.reviewApp?.hasReviewApp;
+ },
canCleanUpEnvs() {
return this.environmentApp?.canStopStaleEnvironments;
},
@@ -130,14 +131,14 @@ export default {
hasSearch() {
return Boolean(this.search);
},
- availableCount() {
- return this.environmentApp?.availableCount;
+ activeCount() {
+ return this.environmentApp?.activeCount ?? 0;
},
stoppedCount() {
- return this.environmentApp?.stoppedCount;
+ return this.environmentApp?.stoppedCount ?? 0;
},
hasAnyEnvironment() {
- return this.availableCount > 0 || this.stoppedCount > 0;
+ return this.activeCount > 0 || this.stoppedCount > 0;
},
showContent() {
return this.hasAnyEnvironment || this.hasSearch;
@@ -157,7 +158,10 @@ export default {
};
},
openReviewAppModal() {
- if (!this.canSetupReviewApp) {
+ // we don't show the Enable review apps button
+ // if a user cannot setup a review app or review
+ // apps are already configured
+ if (!this.canSetupReviewApp || this.hasReviewApp) {
return null;
}
@@ -272,13 +276,13 @@ export default {
@primary="showCleanUpEnvsModal"
>
<gl-tab
- :query-param-value="$options.ENVIRONMENTS_SCOPE.AVAILABLE"
- @click="setScope($options.ENVIRONMENTS_SCOPE.AVAILABLE)"
+ :query-param-value="$options.ENVIRONMENTS_SCOPE.ACTIVE"
+ @click="setScope($options.ENVIRONMENTS_SCOPE.ACTIVE)"
>
<template #title>
- <span>{{ $options.i18n.available }}</span>
+ <span>{{ $options.i18n.active }}</span>
<gl-badge size="sm" class="gl-tab-counter-badge">
- {{ availableCount }}
+ {{ activeCount }}
</gl-badge>
</template>
</gl-tab>
diff --git a/app/assets/javascripts/environments/components/environments_table.vue b/app/assets/javascripts/environments/components/environments_table.vue
index b47086a19da..2f49ed847bf 100644
--- a/app/assets/javascripts/environments/components/environments_table.vue
+++ b/app/assets/javascripts/environments/components/environments_table.vue
@@ -147,12 +147,7 @@ export default {
</div>
</div>
<template v-for="(model, i) in sortedEnvironments">
- <environment-item
- :key="`environment-item-${i}`"
- :model="model"
- :table-data="tableData"
- data-qa-selector="environment_item"
- />
+ <environment-item :key="`environment-item-${i}`" :model="model" :table-data="tableData" />
<div
v-if="shouldRenderDeployBoard(model)"
@@ -185,7 +180,6 @@ export default {
:key="`environment-row-${i}-${index}`"
:model="child"
:table-data="tableData"
- data-qa-selector="environment_item"
/>
<div
diff --git a/app/assets/javascripts/environments/components/kubernetes_overview.vue b/app/assets/javascripts/environments/components/kubernetes_overview.vue
index 252ced6391d..36cce29d624 100644
--- a/app/assets/javascripts/environments/components/kubernetes_overview.vue
+++ b/app/assets/javascripts/environments/components/kubernetes_overview.vue
@@ -64,6 +64,7 @@ export default {
headers: {
'GitLab-Agent-Id': this.gitlabAgentId,
'Content-Type': 'application/json',
+ Accept: 'application/json',
...csrf.headers,
},
credentials: 'include',
@@ -110,7 +111,6 @@ export default {
<kubernetes-status-bar
:cluster-health-status="clusterHealthStatus"
:configuration="k8sAccessConfiguration"
- :namespace="namespace"
:environment-name="environmentName"
:flux-resource-path="fluxResourcePath"
class="gl-mb-3" />
diff --git a/app/assets/javascripts/environments/components/kubernetes_status_bar.vue b/app/assets/javascripts/environments/components/kubernetes_status_bar.vue
index c603d83db9c..8ecb61711ce 100644
--- a/app/assets/javascripts/environments/components/kubernetes_status_bar.vue
+++ b/app/assets/javascripts/environments/components/kubernetes_status_bar.vue
@@ -37,11 +37,6 @@ export default {
required: true,
type: String,
},
- namespace: {
- required: false,
- type: String,
- default: '',
- },
fluxResourcePath: {
required: false,
type: String,
@@ -54,14 +49,12 @@ export default {
variables() {
return {
configuration: this.configuration,
- namespace: this.namespace,
- environmentName: this.environmentName.toLowerCase(),
fluxResourcePath: this.fluxResourcePath,
};
},
skip() {
return Boolean(
- !this.namespace || this.fluxResourcePath?.includes(HELM_RELEASES_RESOURCE_TYPE),
+ !this.fluxResourcePath || this.fluxResourcePath?.includes(HELM_RELEASES_RESOURCE_TYPE),
);
},
error(err) {
@@ -73,17 +66,12 @@ export default {
variables() {
return {
configuration: this.configuration,
- namespace: this.namespace,
- environmentName: this.environmentName.toLowerCase(),
fluxResourcePath: this.fluxResourcePath,
};
},
skip() {
return Boolean(
- !this.namespace ||
- this.$apollo.queries.fluxKustomizationStatus.loading ||
- this.hasKustomizations ||
- this.fluxResourcePath?.includes(KUSTOMIZATIONS_RESOURCE_TYPE),
+ !this.fluxResourcePath || this.fluxResourcePath?.includes(KUSTOMIZATIONS_RESOURCE_TYPE),
);
},
error(err) {
diff --git a/app/assets/javascripts/environments/constants.js b/app/assets/javascripts/environments/constants.js
index 7214454c45c..e97720312b0 100644
--- a/app/assets/javascripts/environments/constants.js
+++ b/app/assets/javascripts/environments/constants.js
@@ -42,12 +42,12 @@ export const CANARY_STATUS = {
export const CANARY_UPDATE_MODAL = 'confirm-canary-change';
export const ENVIRONMENTS_SCOPE = {
- AVAILABLE: 'available',
+ ACTIVE: 'active',
STOPPED: 'stopped',
};
export const ENVIRONMENT_COUNT_BY_SCOPE = {
- [ENVIRONMENTS_SCOPE.AVAILABLE]: 'availableCount',
+ [ENVIRONMENTS_SCOPE.ACTIVE]: 'activeCount',
[ENVIRONMENTS_SCOPE.STOPPED]: 'stoppedCount',
};
diff --git a/app/assets/javascripts/environments/environment_details/index.vue b/app/assets/javascripts/environments/environment_details/index.vue
index 6e3ec04ba3b..bc535eb73aa 100644
--- a/app/assets/javascripts/environments/environment_details/index.vue
+++ b/app/assets/javascripts/environments/environment_details/index.vue
@@ -1,7 +1,7 @@
<!-- eslint-disable vue/multi-word-component-names -->
<script>
import { GlLoadingIcon } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { logError } from '~/lib/logger';
import { toggleQueryPollingByVisibility, etagQueryHeaders } from '~/graphql_shared/utils';
import ConfirmRollbackModal from '~/environments/components/confirm_rollback_modal.vue';
diff --git a/app/assets/javascripts/environments/graphql/client.js b/app/assets/javascripts/environments/graphql/client.js
index 8faed710402..8f57069d89d 100644
--- a/app/assets/javascripts/environments/graphql/client.js
+++ b/app/assets/javascripts/environments/graphql/client.js
@@ -17,7 +17,6 @@ import typeDefs from './typedefs.graphql';
export const apolloProvider = (endpoint) => {
const defaultClient = createDefaultClient(resolvers(endpoint), {
typeDefs,
- useGet: true,
});
const { cache } = defaultClient;
diff --git a/app/assets/javascripts/environments/graphql/queries/environment_app.query.graphql b/app/assets/javascripts/environments/graphql/queries/environment_app.query.graphql
index 7a50ded7d6c..ef5a8194dca 100644
--- a/app/assets/javascripts/environments/graphql/queries/environment_app.query.graphql
+++ b/app/assets/javascripts/environments/graphql/queries/environment_app.query.graphql
@@ -1,6 +1,6 @@
query getEnvironmentApp($page: Int, $scope: String, $search: String) {
environmentApp(page: $page, scope: $scope, search: $search) @client {
- availableCount
+ activeCount
stoppedCount
environments
reviewApp
diff --git a/app/assets/javascripts/environments/graphql/queries/flux_helm_release_status.query.graphql b/app/assets/javascripts/environments/graphql/queries/flux_helm_release_status.query.graphql
index 544232dafd7..042bdc1992d 100644
--- a/app/assets/javascripts/environments/graphql/queries/flux_helm_release_status.query.graphql
+++ b/app/assets/javascripts/environments/graphql/queries/flux_helm_release_status.query.graphql
@@ -1,15 +1,6 @@
-query getFluxHelmReleaseStatusQuery(
- $configuration: LocalConfiguration
- $namespace: String
- $environmentName: String
- $fluxResourcePath: String
-) {
- fluxHelmReleaseStatus(
- configuration: $configuration
- namespace: $namespace
- environmentName: $environmentName
- fluxResourcePath: $fluxResourcePath
- ) @client {
+query getFluxHelmReleaseStatusQuery($configuration: LocalConfiguration, $fluxResourcePath: String) {
+ fluxHelmReleaseStatus(configuration: $configuration, fluxResourcePath: $fluxResourcePath)
+ @client {
message
status
type
diff --git a/app/assets/javascripts/environments/graphql/queries/flux_kustomization_status.query.graphql b/app/assets/javascripts/environments/graphql/queries/flux_kustomization_status.query.graphql
index 2884f95355e..458b8a4d9db 100644
--- a/app/assets/javascripts/environments/graphql/queries/flux_kustomization_status.query.graphql
+++ b/app/assets/javascripts/environments/graphql/queries/flux_kustomization_status.query.graphql
@@ -1,15 +1,9 @@
query getFluxHelmKustomizationStatusQuery(
$configuration: LocalConfiguration
- $namespace: String
- $environmentName: String
$fluxResourcePath: String
) {
- fluxKustomizationStatus(
- configuration: $configuration
- namespace: $namespace
- environmentName: $environmentName
- fluxResourcePath: $fluxResourcePath
- ) @client {
+ fluxKustomizationStatus(configuration: $configuration, fluxResourcePath: $fluxResourcePath)
+ @client {
message
status
type
diff --git a/app/assets/javascripts/environments/graphql/queries/folder.query.graphql b/app/assets/javascripts/environments/graphql/queries/folder.query.graphql
index c662acb8f93..ac6a68e450c 100644
--- a/app/assets/javascripts/environments/graphql/queries/folder.query.graphql
+++ b/app/assets/javascripts/environments/graphql/queries/folder.query.graphql
@@ -1,6 +1,6 @@
query getEnvironmentFolder($environment: NestedLocalEnvironment, $scope: String, $search: String) {
folder(environment: $environment, scope: $scope, search: $search) @client {
- availableCount
+ activeCount
environments
stoppedCount
}
diff --git a/app/assets/javascripts/environments/graphql/resolvers/base.js b/app/assets/javascripts/environments/graphql/resolvers/base.js
index 9752a3a6634..4427b8ff2ef 100644
--- a/app/assets/javascripts/environments/graphql/resolvers/base.js
+++ b/app/assets/javascripts/environments/graphql/resolvers/base.js
@@ -47,7 +47,7 @@ export const baseQueries = (endpoint) => ({
});
return {
- availableCount: res.data.available_count,
+ activeCount: res.data.active_count,
environments: res.data.environments.map(mapNestedEnvironment),
reviewApp: {
...convertObjectPropsToCamelCase(res.data.review_app),
@@ -61,7 +61,7 @@ export const baseQueries = (endpoint) => ({
},
folder(_, { environment: { folderPath }, scope, search }) {
return axios.get(folderPath, { params: { scope, search, per_page: 3 } }).then((res) => ({
- availableCount: res.data.available_count,
+ activeCount: res.data.active_count,
environments: res.data.environments.map(mapEnvironment),
stoppedCount: res.data.stopped_count,
__typename: 'LocalEnvironmentFolder',
diff --git a/app/assets/javascripts/environments/graphql/resolvers/flux.js b/app/assets/javascripts/environments/graphql/resolvers/flux.js
index d39b1bed7b6..5cb5db5d752 100644
--- a/app/assets/javascripts/environments/graphql/resolvers/flux.js
+++ b/app/assets/javascripts/environments/graphql/resolvers/flux.js
@@ -1,35 +1,83 @@
+import { Configuration, WatchApi, EVENT_DATA } from '@gitlab/cluster-client';
import axios from '~/lib/utils/axios_utils';
import {
HELM_RELEASES_RESOURCE_TYPE,
KUSTOMIZATIONS_RESOURCE_TYPE,
} from '~/environments/constants';
+import fluxKustomizationStatusQuery from '../queries/flux_kustomization_status.query.graphql';
+import fluxHelmReleaseStatusQuery from '../queries/flux_helm_release_status.query.graphql';
const helmReleasesApiVersion = 'helm.toolkit.fluxcd.io/v2beta1';
const kustomizationsApiVersion = 'kustomize.toolkit.fluxcd.io/v1beta1';
+const helmReleaseField = 'fluxHelmReleaseStatus';
+const kustomizationField = 'fluxKustomizationStatus';
+
const handleClusterError = (err) => {
const error = err?.response?.data?.message ? new Error(err.response.data.message) : err;
throw error;
};
-const buildFluxResourceUrl = ({
- basePath,
- namespace,
- apiVersion,
- resourceType,
- environmentName = '',
-}) => {
- return `${basePath}/apis/${apiVersion}/namespaces/${namespace}/${resourceType}/${environmentName}`;
+const buildFluxResourceUrl = ({ basePath, namespace, apiVersion, resourceType }) => {
+ return `${basePath}/apis/${apiVersion}/namespaces/${namespace}/${resourceType}`;
};
-const getFluxResourceStatus = (configuration, url) => {
- const { headers } = configuration;
+const buildFluxResourceWatchPath = ({ namespace, apiVersion, resourceType }) => {
+ return `/apis/${apiVersion}/namespaces/${namespace}/${resourceType}`;
+};
+
+const watchFluxResource = ({ watchPath, resourceName, query, variables, field, client }) => {
+ const config = new Configuration(variables.configuration);
+ const watcherApi = new WatchApi(config);
+ const fieldSelector = `metadata.name=${decodeURIComponent(resourceName)}`;
+
+ watcherApi
+ .subscribeToStream(watchPath, { watch: true, fieldSelector })
+ .then((watcher) => {
+ let result = [];
+
+ watcher.on(EVENT_DATA, (data) => {
+ result = data[0]?.status?.conditions;
+
+ client.writeQuery({
+ query,
+ variables,
+ data: { [field]: result },
+ });
+ });
+ })
+ .catch((err) => {
+ handleClusterError(err);
+ });
+};
+
+const getFluxResourceStatus = ({ query, variables, field, resourceType, client }) => {
+ const { headers } = variables.configuration;
const withCredentials = true;
+ const url = `${variables.configuration.basePath}/apis/${variables.fluxResourcePath}`;
return axios
.get(url, { withCredentials, headers })
.then((res) => {
- return res?.data?.status?.conditions || [];
+ const fluxData = res?.data;
+ const resourceName = fluxData?.metadata?.name;
+ const namespace = fluxData?.metadata?.namespace;
+ const apiVersion = fluxData?.apiVersion;
+
+ if (gon.features?.k8sWatchApi && resourceName) {
+ const watchPath = buildFluxResourceWatchPath({ namespace, apiVersion, resourceType });
+
+ watchFluxResource({
+ watchPath,
+ resourceName,
+ query,
+ variables,
+ field,
+ client,
+ });
+ }
+
+ return fluxData?.status?.conditions || [];
})
.catch((err) => {
handleClusterError(err);
@@ -62,37 +110,23 @@ const getFluxResources = (configuration, url) => {
};
export default {
- fluxKustomizationStatus(_, { configuration, namespace, environmentName, fluxResourcePath = '' }) {
- let url;
-
- if (fluxResourcePath) {
- url = `${configuration.basePath}/apis/${fluxResourcePath}`;
- } else {
- url = buildFluxResourceUrl({
- basePath: configuration.basePath,
- resourceType: KUSTOMIZATIONS_RESOURCE_TYPE,
- apiVersion: kustomizationsApiVersion,
- namespace,
- environmentName,
- });
- }
- return getFluxResourceStatus(configuration, url);
+ fluxKustomizationStatus(_, { configuration, fluxResourcePath }, { client }) {
+ return getFluxResourceStatus({
+ query: fluxKustomizationStatusQuery,
+ variables: { configuration, fluxResourcePath },
+ field: kustomizationField,
+ resourceType: KUSTOMIZATIONS_RESOURCE_TYPE,
+ client,
+ });
},
- fluxHelmReleaseStatus(_, { configuration, namespace, environmentName, fluxResourcePath }) {
- let url;
-
- if (fluxResourcePath) {
- url = `${configuration.basePath}/apis/${fluxResourcePath}`;
- } else {
- url = buildFluxResourceUrl({
- basePath: configuration.basePath,
- resourceType: HELM_RELEASES_RESOURCE_TYPE,
- apiVersion: helmReleasesApiVersion,
- namespace,
- environmentName,
- });
- }
- return getFluxResourceStatus(configuration, url);
+ fluxHelmReleaseStatus(_, { configuration, fluxResourcePath }, { client }) {
+ return getFluxResourceStatus({
+ query: fluxHelmReleaseStatusQuery,
+ variables: { configuration, fluxResourcePath },
+ field: helmReleaseField,
+ resourceType: HELM_RELEASES_RESOURCE_TYPE,
+ client,
+ });
},
fluxKustomizations(_, { configuration, namespace }) {
const url = buildFluxResourceUrl({
diff --git a/app/assets/javascripts/environments/graphql/resolvers/kubernetes.js b/app/assets/javascripts/environments/graphql/resolvers/kubernetes.js
index 67a472dac93..8375b8793d9 100644
--- a/app/assets/javascripts/environments/graphql/resolvers/kubernetes.js
+++ b/app/assets/javascripts/environments/graphql/resolvers/kubernetes.js
@@ -1,5 +1,13 @@
-import { CoreV1Api, Configuration, AppsV1Api, BatchV1Api } from '@gitlab/cluster-client';
+import {
+ CoreV1Api,
+ Configuration,
+ AppsV1Api,
+ BatchV1Api,
+ WatchApi,
+ EVENT_DATA,
+} from '@gitlab/cluster-client';
import { humanizeClusterErrors } from '../../helpers/k8s_integration_helper';
+import k8sPodsQuery from '../queries/k8s_pods.query.graphql';
const mapWorkloadItems = (items, kind) => {
return items.map((item) => {
@@ -53,15 +61,50 @@ const handleClusterError = async (err) => {
throw errorData;
};
+const watchPods = ({ configuration, namespace, client }) => {
+ const path = namespace ? `/api/v1/namespaces/${namespace}/pods` : '/api/v1/pods';
+ const config = new Configuration(configuration);
+ const watcherApi = new WatchApi(config);
+
+ watcherApi
+ .subscribeToStream(path, { watch: true })
+ .then((watcher) => {
+ let result = [];
+
+ watcher.on(EVENT_DATA, (data) => {
+ result = data.map((item) => {
+ return { status: { phase: item.status.phase } };
+ });
+
+ client.writeQuery({
+ query: k8sPodsQuery,
+ variables: { configuration, namespace },
+ data: { k8sPods: result },
+ });
+ });
+ })
+ .catch((err) => {
+ handleClusterError(err);
+ });
+};
+
export default {
- k8sPods(_, { configuration, namespace }) {
- const coreV1Api = new CoreV1Api(new Configuration(configuration));
+ k8sPods(_, { configuration, namespace }, { client }) {
+ const config = new Configuration(configuration);
+
+ const coreV1Api = new CoreV1Api(config);
const podsApi = namespace
? coreV1Api.listCoreV1NamespacedPod({ namespace })
: coreV1Api.listCoreV1PodForAllNamespaces();
return podsApi
- .then((res) => res?.items || [])
+ .then((res) => {
+ if (gon.features?.k8sWatchApi) {
+ watchPods({ configuration, namespace, client });
+ }
+
+ return res?.items || [];
+ })
.catch(async (err) => {
try {
await handleClusterError(err);
diff --git a/app/assets/javascripts/error_tracking/components/error_details.vue b/app/assets/javascripts/error_tracking/components/error_details.vue
index 1821aa073bc..01879a092ed 100644
--- a/app/assets/javascripts/error_tracking/components/error_details.vue
+++ b/app/assets/javascripts/error_tracking/components/error_details.vue
@@ -196,7 +196,7 @@ export default {
text: __('Create issue'),
action: this.createIssue,
extraAttrs: {
- 'data-qa-selector': 'create_issue_button',
+ 'data-testid': 'create-issue-button',
},
};
},
@@ -309,7 +309,7 @@ export default {
<div
v-if="!loadingStacktrace && stacktrace"
class="gl-my-auto gl-text-truncate"
- data-qa-selector="reported_text"
+ data-testid="reported-text"
>
<gl-sprintf :message="__('Reported %{timeAgo} by %{reportedBy}')">
<template #reportedBy>
@@ -367,7 +367,7 @@ export default {
category="primary"
variant="confirm"
:loading="issueCreationInProgress"
- data-qa-selector="create_issue_button"
+ data-testid="create-issue-button"
@click="createIssue"
>
{{ __('Create issue') }}
diff --git a/app/assets/javascripts/feature_flags/components/feature_flags.vue b/app/assets/javascripts/feature_flags/components/feature_flags.vue
index daaeb5f8e85..e0a5e92564e 100644
--- a/app/assets/javascripts/feature_flags/components/feature_flags.vue
+++ b/app/assets/javascripts/feature_flags/components/feature_flags.vue
@@ -206,7 +206,6 @@ export default {
v-gl-modal="'configure-feature-flags'"
variant="confirm"
category="secondary"
- data-qa-selector="configure_feature_flags_button"
data-testid="ff-configure-button"
class="gl-mb-0 gl-mr-3"
>
diff --git a/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js b/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js
index 4d3647cdf5c..74d91734630 100644
--- a/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js
+++ b/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js
@@ -5,9 +5,10 @@ import {
TOKEN_TYPE_APPROVED_BY,
TOKEN_TYPE_REVIEWER,
TOKEN_TYPE_TARGET_BRANCH,
+ TOKEN_TYPE_SOURCE_BRANCH,
} from '~/vue_shared/components/filtered_search_bar/constants';
-export default (IssuableTokenKeys, disableTargetBranchFilter = false) => {
+export default (IssuableTokenKeys, disableBranchFilter = false) => {
const reviewerToken = {
formattedKey: TOKEN_TITLE_REVIEWER,
key: TOKEN_TYPE_REVIEWER,
@@ -57,7 +58,7 @@ export default (IssuableTokenKeys, disableTargetBranchFilter = false) => {
IssuableTokenKeys.tokenKeysWithAlternative.push(draftToken.token);
IssuableTokenKeys.conditions.push(...draftToken.conditions);
- if (!disableTargetBranchFilter) {
+ if (!disableBranchFilter) {
const targetBranchToken = {
formattedKey: __('Target-Branch'),
key: TOKEN_TYPE_TARGET_BRANCH,
@@ -68,8 +69,18 @@ export default (IssuableTokenKeys, disableTargetBranchFilter = false) => {
tag: 'branch',
};
- IssuableTokenKeys.tokenKeys.push(targetBranchToken);
- IssuableTokenKeys.tokenKeysWithAlternative.push(targetBranchToken);
+ const sourceBranchToken = {
+ formattedKey: __('Source-Branch'),
+ key: TOKEN_TYPE_SOURCE_BRANCH,
+ type: 'string',
+ param: '',
+ symbol: '',
+ icon: 'branch',
+ tag: 'branch',
+ };
+
+ IssuableTokenKeys.tokenKeys.push(targetBranchToken, sourceBranchToken);
+ IssuableTokenKeys.tokenKeysWithAlternative.push(targetBranchToken, sourceBranchToken);
}
const approvedToken = {
diff --git a/app/assets/javascripts/filtered_search/available_dropdown_mappings.js b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
index 892e9130fe8..a1782c549d6 100644
--- a/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
+++ b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
@@ -11,6 +11,7 @@ import {
TOKEN_TYPE_RELEASE,
TOKEN_TYPE_REVIEWER,
TOKEN_TYPE_TARGET_BRANCH,
+ TOKEN_TYPE_SOURCE_BRANCH,
} from '~/vue_shared/components/filtered_search_bar/constants';
import DropdownEmoji from './dropdown_emoji';
import DropdownHint from './dropdown_hint';
@@ -157,6 +158,15 @@ export default class AvailableDropdownMappings {
},
element: this.container.querySelector('#js-dropdown-target-branch'),
},
+ [TOKEN_TYPE_SOURCE_BRANCH]: {
+ reference: null,
+ gl: DropdownNonUser,
+ extraArguments: {
+ endpoint: this.getMergeRequestSourceBranchesEndpoint(),
+ symbol: '',
+ },
+ element: this.container.querySelector('#js-dropdown-source-branch'),
+ },
environment: {
reference: null,
gl: DropdownNonUser,
@@ -197,10 +207,17 @@ export default class AvailableDropdownMappings {
}
getMergeRequestTargetBranchesEndpoint() {
- const endpoint = `${
- gon.relative_url_root || ''
- }/-/autocomplete/merge_request_target_branches.json`;
+ const targetBranchEndpointPath = '/-/autocomplete/merge_request_target_branches.json';
+ return this.getMergeRequestBranchesEndpoint(targetBranchEndpointPath);
+ }
+
+ getMergeRequestSourceBranchesEndpoint() {
+ const sourceBranchEndpointPath = '/-/autocomplete/merge_request_source_branches.json';
+ return this.getMergeRequestBranchesEndpoint(sourceBranchEndpointPath);
+ }
+ getMergeRequestBranchesEndpoint(endpointPath = '') {
+ const endpoint = `${gon.relative_url_root || ''}${endpointPath}`;
const params = {
group_id: this.getGroupId(),
project_id: this.getProjectId(),
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index 99d22b1330b..39a8b1d0a9c 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -979,7 +979,7 @@ GfmAutoComplete.Emoji = {
},
filter(query) {
if (query.length === 0) {
- return Object.values(Emoji.getAllEmoji())
+ return Emoji.getAllEmoji()
.map((emoji) => ({
emoji,
fieldValue: emoji.name,
diff --git a/app/assets/javascripts/gitlab_pages/components/pages_pipeline_wizard.vue b/app/assets/javascripts/gitlab_pages/components/pages_pipeline_wizard.vue
index f19e047061f..dcf6c90f7fa 100644
--- a/app/assets/javascripts/gitlab_pages/components/pages_pipeline_wizard.vue
+++ b/app/assets/javascripts/gitlab_pages/components/pages_pipeline_wizard.vue
@@ -1,6 +1,6 @@
<script>
import { GlLoadingIcon } from '@gitlab/ui';
-import { captureException } from '@sentry/browser';
+import { captureException } from '~/sentry/sentry_browser_wrapper';
import PipelineWizard from '~/pipeline_wizard/pipeline_wizard.vue';
import PagesWizardTemplate from '~/pipeline_wizard/templates/pages.yml?raw';
import { logError } from '~/lib/logger';
diff --git a/app/assets/javascripts/graphql_shared/constants.js b/app/assets/javascripts/graphql_shared/constants.js
index 5ba46697496..2863f52bea9 100644
--- a/app/assets/javascripts/graphql_shared/constants.js
+++ b/app/assets/javascripts/graphql_shared/constants.js
@@ -27,5 +27,6 @@ export const TYPENAME_USER = 'User';
export const TYPENAME_VULNERABILITIES_SCANNER = 'Vulnerabilities::Scanner';
export const TYPENAME_VULNERABILITY = 'Vulnerability';
export const TYPENAME_WORK_ITEM = 'WorkItem';
+export const TYPENAME_ORGANIZATION = 'Organization';
export const TYPE_USERS_SAVED_REPLY = 'Users::SavedReply';
export const TYPE_WORKSPACE = 'RemoteDevelopment::Workspace';
diff --git a/app/assets/javascripts/graphql_shared/fragments/issue.fragment.graphql b/app/assets/javascripts/graphql_shared/fragments/issue.fragment.graphql
index 85a28fe1f71..458fdb24e6d 100644
--- a/app/assets/javascripts/graphql_shared/fragments/issue.fragment.graphql
+++ b/app/assets/javascripts/graphql_shared/fragments/issue.fragment.graphql
@@ -6,6 +6,7 @@ fragment IssueNode on Issue {
iid
title
referencePath: reference(full: true)
+ closedAt
dueDate
timeEstimate
totalTimeSpent
diff --git a/app/assets/javascripts/graphql_shared/possible_types.json b/app/assets/javascripts/graphql_shared/possible_types.json
index 4e0b1413f71..1439a3181b0 100644
--- a/app/assets/javascripts/graphql_shared/possible_types.json
+++ b/app/assets/javascripts/graphql_shared/possible_types.json
@@ -67,9 +67,11 @@
],
"MemberInterface": [
"GroupMember",
+ "PendingGroupMember",
"ProjectMember"
],
"NoteableInterface": [
+ "AbuseReport",
"AlertManagementAlert",
"BoardEpic",
"Design",
diff --git a/app/assets/javascripts/graphql_shared/queries/groups_autocomplete.query.graphql b/app/assets/javascripts/graphql_shared/queries/groups_autocomplete.query.graphql
new file mode 100644
index 00000000000..74da46e5a60
--- /dev/null
+++ b/app/assets/javascripts/graphql_shared/queries/groups_autocomplete.query.graphql
@@ -0,0 +1,10 @@
+query groupsAutocomplete($search: String) {
+ groups(search: $search) {
+ nodes {
+ id
+ name
+ fullName
+ avatarUrl
+ }
+ }
+}
diff --git a/app/assets/javascripts/groups/components/item_actions.vue b/app/assets/javascripts/groups/components/item_actions.vue
index fc7cfffc22c..43689e6677b 100644
--- a/app/assets/javascripts/groups/components/item_actions.vue
+++ b/app/assets/javascripts/groups/components/item_actions.vue
@@ -57,7 +57,6 @@ export default {
icon="ellipsis_v"
no-caret
:data-testid="`group-${group.id}-dropdown-button`"
- data-qa-selector="group_dropdown_button"
:data-qa-group-id="group.id"
>
<gl-dropdown-item
diff --git a/app/assets/javascripts/groups/settings/init_access_dropdown.js b/app/assets/javascripts/groups/settings/init_access_dropdown.js
index 4da38e0e641..f18f260097b 100644
--- a/app/assets/javascripts/groups/settings/init_access_dropdown.js
+++ b/app/assets/javascripts/groups/settings/init_access_dropdown.js
@@ -1,5 +1,5 @@
-import * as Sentry from '@sentry/browser';
import Vue from 'vue';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import AccessDropdown from './components/access_dropdown.vue';
export const initAccessDropdown = (el) => {
diff --git a/app/assets/javascripts/groups_projects/components/transfer_locations.vue b/app/assets/javascripts/groups_projects/components/transfer_locations.vue
index 997e2bc3138..5774065bff9 100644
--- a/app/assets/javascripts/groups_projects/components/transfer_locations.vue
+++ b/app/assets/javascripts/groups_projects/components/transfer_locations.vue
@@ -260,11 +260,7 @@ export default {
>
<gl-dropdown-divider />
</template>
- <div
- v-if="hasUserTransferLocations"
- data-qa-selector="namespaces_list_users"
- data-testid="user-transfer-locations"
- >
+ <div v-if="hasUserTransferLocations" data-testid="user-transfer-locations">
<gl-dropdown-section-header>{{ $options.i18n.USERS }}</gl-dropdown-section-header>
<gl-dropdown-item
v-for="item in userTransferLocations"
diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js
index 25a84d17379..095a2dc1324 100644
--- a/app/assets/javascripts/header.js
+++ b/app/assets/javascripts/header.js
@@ -1,3 +1,6 @@
+// TODO: Remove this with the removal of the old navigation.
+// See https://gitlab.com/groups/gitlab-org/-/epics/11875.
+
import Vue from 'vue';
import NewNavToggle from '~/nav/components/new_nav_toggle.vue';
import { highCountTrim } from '~/lib/utils/text_utility';
diff --git a/app/assets/javascripts/header_search/index.js b/app/assets/javascripts/header_search/index.js
index 2bbad5f3f98..7b26dd183ad 100644
--- a/app/assets/javascripts/header_search/index.js
+++ b/app/assets/javascripts/header_search/index.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import Translate from '~/vue_shared/translate';
import HeaderSearchApp from './components/app.vue';
import createStore from './store';
diff --git a/app/assets/javascripts/header_search/init.js b/app/assets/javascripts/header_search/init.js
index 64502d13ee2..1c582ace480 100644
--- a/app/assets/javascripts/header_search/init.js
+++ b/app/assets/javascripts/header_search/init.js
@@ -1,4 +1,4 @@
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { HEADER_INIT_EVENTS } from './constants';
async function eventHandler(callback = () => {}) {
diff --git a/app/assets/javascripts/helpers/help_page_helper.js b/app/assets/javascripts/helpers/help_page_helper.js
index 21d27b5fea9..fab0f17cd3b 100644
--- a/app/assets/javascripts/helpers/help_page_helper.js
+++ b/app/assets/javascripts/helpers/help_page_helper.js
@@ -1,6 +1,6 @@
import { joinPaths, setUrlFragment } from '~/lib/utils/url_utility';
-const HELP_PAGE_URL_ROOT = '/help/';
+const HELP_PAGE_URL_ROOT = '/help';
/**
* Generate link to a GitLab documentation page.
diff --git a/app/assets/javascripts/helpers/init_simple_app_helper.js b/app/assets/javascripts/helpers/init_simple_app_helper.js
index 695fc455f13..f7bef8c563e 100644
--- a/app/assets/javascripts/helpers/init_simple_app_helper.js
+++ b/app/assets/javascripts/helpers/init_simple_app_helper.js
@@ -1,4 +1,26 @@
import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+
+/**
+ * @param {boolean|VueApollo} apolloProviderOption
+ * @returns {undefined | VueApollo}
+ */
+const getApolloProvider = (apolloProviderOption) => {
+ if (apolloProviderOption === true) {
+ Vue.use(VueApollo);
+
+ return new VueApollo({
+ defaultClient: createDefaultClient(),
+ });
+ }
+
+ if (apolloProviderOption instanceof VueApollo) {
+ return apolloProviderOption;
+ }
+
+ return undefined;
+};
/**
* Initializes a component as a simple vue app, passing the necessary props. If the element
@@ -8,6 +30,8 @@ import Vue from 'vue';
*
* @param {string} selector css selector for where to build
* @param {Vue.component} component The Vue compoment to be built as the root of the app
+ * @param {{withApolloProvider: boolean|VueApollo}} options. extra options to be passed to the vue app
+ * withApolloProvider: if true, instantiates a default apolloProvider. Also accepts and instance of VueApollo
*
* @example
* ```html
@@ -15,13 +39,13 @@ import Vue from 'vue';
* ```
*
* ```javascript
- * initSimpleApp('#mount-here', MyApp)
+ * initSimpleApp('#mount-here', MyApp, { withApolloProvider: true })
* ```
*
* This will mount MyApp as root on '#mount-here'. It will receive {'some': 'object'} as it's
* view model prop.
*/
-export const initSimpleApp = (selector, component) => {
+export const initSimpleApp = (selector, component, { withApolloProvider } = {}) => {
const element = document.querySelector(selector);
if (!element) {
@@ -32,6 +56,7 @@ export const initSimpleApp = (selector, component) => {
return new Vue({
el: element,
+ apolloProvider: getApolloProvider(withApolloProvider),
render(h) {
return h(component, { props });
},
diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue
index 76b284b6185..984dc9edaf1 100644
--- a/app/assets/javascripts/ide/components/ide_status_bar.vue
+++ b/app/assets/javascripts/ide/components/ide_status_bar.vue
@@ -88,7 +88,6 @@ export default {
@click="openRightPane($options.rightSidebarViews.pipelines)"
>
<ci-icon
- v-gl-tooltip
:status="latestPipeline.details.status"
:title="latestPipeline.details.status.text"
/>
diff --git a/app/assets/javascripts/ide/components/jobs/detail/description.vue b/app/assets/javascripts/ide/components/jobs/detail/description.vue
index f0c5b29e210..f5840661c17 100644
--- a/app/assets/javascripts/ide/components/jobs/detail/description.vue
+++ b/app/assets/javascripts/ide/components/jobs/detail/description.vue
@@ -25,9 +25,7 @@ export default {
<template>
<div class="d-flex align-items-center">
<ci-icon
- is-borderless
:status="job.status"
- :size="24"
class="gl-align-items-center gl-border gl-display-inline-flex gl-z-index-1"
/>
<span class="gl-ml-3">
diff --git a/app/assets/javascripts/ide/components/pipelines/list.vue b/app/assets/javascripts/ide/components/pipelines/list.vue
index 6bf51ed06a6..0e07cc34dd8 100644
--- a/app/assets/javascripts/ide/components/pipelines/list.vue
+++ b/app/assets/javascripts/ide/components/pipelines/list.vue
@@ -63,7 +63,7 @@ export default {
</div>
<template v-else-if="hasLoadedPipeline">
<header v-if="latestPipeline" class="ide-tree-header ide-pipeline-header">
- <ci-icon :status="latestPipeline.details.status" :size="24" class="d-flex" />
+ <ci-icon :status="latestPipeline.details.status" />
<span class="gl-ml-3">
<strong> {{ __('Pipeline') }} </strong>
<a
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index 137df9aa102..3b59fe86764 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -8,7 +8,6 @@ import {
EDITOR_TYPE_CODE,
EDITOR_CODE_INSTANCE_FN,
EDITOR_DIFF_INSTANCE_FN,
- EXTENSION_CI_SCHEMA_FILE_NAME_MATCH,
} from '~/editor/constants';
import { SourceEditorExtension } from '~/editor/extensions/source_editor_extension_base';
import { EditorWebIdeExtension } from '~/editor/extensions/source_editor_webide_ext';
@@ -30,6 +29,7 @@ import { viewerInformationForPath } from '~/vue_shared/components/content_viewer
import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
import { markRaw } from '~/lib/utils/vue3compat/mark_raw';
import { readFileAsDataURL } from '~/lib/utils/file_utility';
+import { isDefaultCiConfig, hasCiConfigExtension } from '~/lib/utils/common_utils';
import {
leftSidebarViews,
@@ -152,8 +152,9 @@ export default {
},
isCiConfigFile() {
return (
- this.file.path === EXTENSION_CI_SCHEMA_FILE_NAME_MATCH &&
- this.editor?.getEditorType() === EDITOR_TYPE_CODE
+ // For CI config schemas the filename must match '*.gitlab-ci.yml' regardless of project configuration.
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/293641
+ hasCiConfigExtension(this.file.path) && this.editor?.getEditorType() === EDITOR_TYPE_CODE
);
},
},
@@ -162,7 +163,7 @@ export default {
handler() {
this.stopWatchingCiYaml();
- if (this.file.name === '.gitlab-ci.yml') {
+ if (isDefaultCiConfig(this.file.name)) {
this.startWatchingCiYaml();
}
},
diff --git a/app/assets/javascripts/ide/lib/alerts/index.js b/app/assets/javascripts/ide/lib/alerts/index.js
index c9db9779b1f..ac4eeb0386f 100644
--- a/app/assets/javascripts/ide/lib/alerts/index.js
+++ b/app/assets/javascripts/ide/lib/alerts/index.js
@@ -1,3 +1,4 @@
+import { isDefaultCiConfig } from '~/lib/utils/common_utils';
import { leftSidebarViews } from '../../constants';
import EnvironmentsMessage from './environments.vue';
@@ -6,7 +7,7 @@ const alerts = [
key: Symbol('ALERT_ENVIRONMENT'),
show: (state, file) =>
state.currentActivityView === leftSidebarViews.commit.name &&
- file.path === '.gitlab-ci.yml' &&
+ isDefaultCiConfig(file.path) &&
state.environmentsGuidanceAlertDetected &&
!state.environmentsGuidanceAlertDismissed,
props: { variant: 'tip' },
diff --git a/app/assets/javascripts/ide/stores/modules/file_templates/getters.js b/app/assets/javascripts/ide/stores/modules/file_templates/getters.js
index bf0d3ed337c..5681f6cdec5 100644
--- a/app/assets/javascripts/ide/stores/modules/file_templates/getters.js
+++ b/app/assets/javascripts/ide/stores/modules/file_templates/getters.js
@@ -1,9 +1,10 @@
import { __ } from '~/locale';
+import { DEFAULT_CI_CONFIG_PATH } from '~/lib/utils/constants';
import { leftSidebarViews } from '../../../constants';
export const templateTypes = () => [
{
- name: '.gitlab-ci.yml',
+ name: DEFAULT_CI_CONFIG_PATH,
key: 'gitlab_ci_ymls',
},
{
diff --git a/app/assets/javascripts/ide/stores/modules/terminal/mutations.js b/app/assets/javascripts/ide/stores/modules/terminal/mutations.js
index 37f40af9c2e..8adde8f6b4e 100644
--- a/app/assets/javascripts/ide/stores/modules/terminal/mutations.js
+++ b/app/assets/javascripts/ide/stores/modules/terminal/mutations.js
@@ -48,7 +48,7 @@ export default {
},
[types.SET_SESSION_STATUS](state, status) {
const session = {
- ...(state.session || {}),
+ ...state.session,
status,
};
diff --git a/app/assets/javascripts/import/constants.js b/app/assets/javascripts/import/constants.js
index ddf69a8fcdf..b02eb3c4307 100644
--- a/app/assets/javascripts/import/constants.js
+++ b/app/assets/javascripts/import/constants.js
@@ -1,6 +1,18 @@
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { __, s__ } from '~/locale';
+export const BULK_IMPORT_STATIC_ITEMS = {
+ badges: __('Badge'),
+ boards: s__('IssueBoards|Board'),
+ epics: __('Epic'),
+ issues: __('Issue'),
+ labels: __('Label'),
+ members: __('Member'),
+ merge_requests: __('Merge request'),
+ milestones: __('Milestone'),
+ project: __('Project'),
+};
+
const STATISTIC_ITEMS = {
diff_note: __('Diff notes'),
issue: __('Issues'),
diff --git a/app/assets/javascripts/import/details/components/bulk_import_details_app.vue b/app/assets/javascripts/import/details/components/bulk_import_details_app.vue
new file mode 100644
index 00000000000..5da16454032
--- /dev/null
+++ b/app/assets/javascripts/import/details/components/bulk_import_details_app.vue
@@ -0,0 +1,44 @@
+<script>
+import { __ } from '~/locale';
+import ImportDetailsTable from '~/import/details/components/import_details_table.vue';
+
+export default {
+ name: 'BulkImportDetailsApp',
+ components: { ImportDetailsTable },
+
+ fields: [
+ {
+ key: 'relation',
+ label: __('Type'),
+ tdClass: 'gl-white-space-nowrap',
+ },
+ {
+ key: 'source_title',
+ label: __('Title'),
+ tdClass: 'gl-md-w-30 gl-word-break-word',
+ },
+ {
+ key: 'error',
+ label: __('Error'),
+ },
+ {
+ key: 'correlation_id_value',
+ label: __('Correlation ID'),
+ },
+ ],
+
+ LOCAL_STORAGE_KEY: 'gl-bulk-import-details-page-size',
+};
+</script>
+
+<template>
+ <div>
+ <h1>{{ s__('Import|GitLab Migration details') }}</h1>
+
+ <import-details-table
+ bulk-import
+ :fields="$options.fields"
+ :local-storage-key="$options.LOCAL_STORAGE_KEY"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/import/details/components/import_details_app.vue b/app/assets/javascripts/import/details/components/import_details_app.vue
index 13483fa8ba2..f654dc61e07 100644
--- a/app/assets/javascripts/import/details/components/import_details_app.vue
+++ b/app/assets/javascripts/import/details/components/import_details_app.vue
@@ -1,18 +1,44 @@
<script>
-import { s__ } from '~/locale';
+import { __ } from '~/locale';
import ImportDetailsTable from './import_details_table.vue';
export default {
+ name: 'ImportDetailsApp',
components: { ImportDetailsTable },
- i18n: {
- pageTitle: s__('Import|GitHub import details'),
- },
+
+ fields: [
+ {
+ key: 'type',
+ label: __('Type'),
+ tdClass: 'gl-white-space-nowrap',
+ },
+ {
+ key: 'title',
+ label: __('Title'),
+ tdClass: 'gl-md-w-30 gl-word-break-word',
+ },
+ {
+ key: 'provider_url',
+ label: __('URL'),
+ tdClass: 'gl-white-space-nowrap',
+ },
+ {
+ key: 'details',
+ label: __('Details'),
+ },
+ ],
+
+ LOCAL_STORAGE_KEY: 'gl-import-details-page-size',
};
</script>
<template>
<div>
- <h1>{{ $options.i18n.pageTitle }}</h1>
- <import-details-table />
+ <h1>{{ s__('Import|GitHub import details') }}</h1>
+
+ <import-details-table
+ :fields="$options.fields"
+ :local-storage-key="$options.LOCAL_STORAGE_KEY"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/import/details/components/import_details_table.vue b/app/assets/javascripts/import/details/components/import_details_table.vue
index 813dc1f2645..535ccb525ac 100644
--- a/app/assets/javascripts/import/details/components/import_details_table.vue
+++ b/app/assets/javascripts/import/details/components/import_details_table.vue
@@ -1,12 +1,13 @@
<script>
import { GlEmptyState, GlIcon, GlLink, GlLoadingIcon, GlTable } from '@gitlab/ui';
-import { __, s__ } from '~/locale';
+import { s__ } from '~/locale';
import { createAlert } from '~/alert';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
import { getParameterValues } from '~/lib/utils/url_utility';
import PaginationBar from '~/vue_shared/components/pagination_bar/pagination_bar.vue';
-import { STATISTIC_ITEMS } from '../../constants';
+import { getBulkImportFailures } from '~/rest_api';
+import { BULK_IMPORT_STATIC_ITEMS, STATISTIC_ITEMS } from '../../constants';
import { fetchImportFailures } from '../api';
const DEFAULT_PAGE_SIZE = 20;
@@ -21,28 +22,6 @@ export default {
PaginationBar,
},
STATISTIC_ITEMS,
- LOCAL_STORAGE_KEY: 'gl-import-details-page-size',
- fields: [
- {
- key: 'type',
- label: __('Type'),
- tdClass: 'gl-white-space-nowrap',
- },
- {
- key: 'title',
- label: __('Title'),
- tdClass: 'gl-md-w-30 gl-word-break-word',
- },
- {
- key: 'provider_url',
- label: __('URL'),
- tdClass: 'gl-white-space-nowrap',
- },
- {
- key: 'details',
- label: __('Details'),
- },
- ],
i18n: {
fetchErrorMessage: s__('Import|An error occurred while fetching import details.'),
@@ -55,6 +34,25 @@ export default {
},
},
+ props: {
+ bulkImport: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+
+ fields: {
+ type: Array,
+ required: true,
+ },
+
+ localStorageKey: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+
data() {
return {
items: [],
@@ -97,18 +95,28 @@ export default {
this.loadImportFailures();
},
+ fetchFn(params) {
+ return this.bulkImport
+ ? getBulkImportFailures(
+ getParameterValues('id')[0],
+ getParameterValues('entity_id')[0],
+ params,
+ )
+ : fetchImportFailures(this.failuresPath, {
+ projectId: getParameterValues('project_id')[0],
+ ...params,
+ });
+ },
+
async loadImportFailures() {
- if (!this.failuresPath) {
+ if (!this.bulkImport && !this.failuresPath) {
return;
}
this.loading = true;
+
try {
- const response = await fetchImportFailures(this.failuresPath, {
- projectId: getParameterValues('project_id')[0],
- page: this.page,
- perPage: this.perPage,
- });
+ const response = await this.fetchFn({ page: this.page, perPage: this.perPage });
const { page, perPage, totalPages, total } = parseIntPagination(
normalizeHeaders(response.headers),
@@ -123,13 +131,17 @@ export default {
}
this.loading = false;
},
+
+ itemTypeText(type) {
+ return (this.bulkImport ? BULK_IMPORT_STATIC_ITEMS[type] : STATISTIC_ITEMS[type]) || type;
+ },
},
};
</script>
<template>
<div>
- <gl-table :fields="$options.fields" :items="items" class="gl-mt-5" :busy="loading" show-empty>
+ <gl-table :fields="fields" :items="items" class="gl-mt-5" :busy="loading" show-empty>
<template #table-busy>
<gl-loading-icon size="lg" class="gl-my-5" />
</template>
@@ -139,7 +151,7 @@ export default {
</template>
<template #cell(type)="{ item: { type } }">
- {{ $options.STATISTIC_ITEMS[type] }}
+ {{ itemTypeText(type) }}
</template>
<template #cell(provider_url)="{ item: { provider_url } }">
<gl-link v-if="provider_url" :href="provider_url" target="_blank">
@@ -147,12 +159,30 @@ export default {
<gl-icon name="external-link" />
</gl-link>
</template>
+
+ <template #cell(relation)="{ item: { relation } }">
+ {{ itemTypeText(relation) }}
+ </template>
+ <template #cell(source_title)="{ item: { source_title, source_url } }">
+ <gl-link v-if="source_url" :href="source_url" target="_blank">
+ {{ source_title }}
+ <gl-icon name="external-link" />
+ </gl-link>
+ <span v-else>
+ {{ source_title }}
+ </span>
+ </template>
+ <template #cell(error)="{ item: { exception_class, exception_message } }">
+ <strong>{{ exception_class }}</strong>
+ <p>{{ exception_message }}</p>
+ </template>
</gl-table>
+
<pagination-bar
v-if="hasItems"
:page-info="pageInfo"
class="gl-mt-5"
- :storage-key="$options.LOCAL_STORAGE_KEY"
+ :storage-key="localStorageKey"
@set-page="setPage"
@set-page-size="setPageSize"
/>
diff --git a/app/assets/javascripts/import_entities/components/import_status.vue b/app/assets/javascripts/import_entities/components/import_status.vue
index 91436457b03..3cde3a8df3c 100644
--- a/app/assets/javascripts/import_entities/components/import_status.vue
+++ b/app/assets/javascripts/import_entities/components/import_status.vue
@@ -1,46 +1,9 @@
<script>
import { GlAccordion, GlAccordionItem, GlBadge, GlIcon, GlLink } from '@gitlab/ui';
-import { __, s__ } from '~/locale';
+import { s__ } from '~/locale';
import { STATISTIC_ITEMS } from '~/import/constants';
-import { STATUSES } from '../constants';
-
-const SCHEDULED_STATUS = {
- icon: 'status-scheduled',
- text: __('Pending'),
- variant: 'muted',
-};
-
-const STATUS_MAP = {
- [STATUSES.NONE]: {
- icon: 'status-waiting',
- text: __('Not started'),
- variant: 'muted',
- },
- [STATUSES.SCHEDULING]: SCHEDULED_STATUS,
- [STATUSES.SCHEDULED]: SCHEDULED_STATUS,
- [STATUSES.CREATED]: SCHEDULED_STATUS,
- [STATUSES.STARTED]: {
- icon: 'status-running',
- text: __('Importing...'),
- variant: 'info',
- },
- [STATUSES.FAILED]: {
- icon: 'status-failed',
- text: __('Failed'),
- variant: 'danger',
- },
- [STATUSES.TIMEOUT]: {
- icon: 'status-failed',
- text: __('Timeout'),
- variant: 'danger',
- },
- [STATUSES.CANCELED]: {
- icon: 'status-stopped',
- text: __('Cancelled'),
- variant: 'neutral',
- },
-};
+import { STATUSES, STATUS_ICON_MAP } from '../constants';
function isIncompleteImport(stats) {
return Object.keys(stats?.fetched ?? []).some(
@@ -96,21 +59,11 @@ export default {
},
mappedStatus() {
- if (this.status === STATUSES.FINISHED) {
- return this.isIncomplete
- ? {
- icon: 'status-alert',
- text: s__('Import|Partially completed'),
- variant: 'warning',
- }
- : {
- icon: 'status-success',
- text: __('Complete'),
- variant: 'success',
- };
+ if (this.isIncomplete) {
+ return STATUS_ICON_MAP[STATUSES.PARTIAL];
}
- return STATUS_MAP[this.status];
+ return STATUS_ICON_MAP[this.status];
},
showDetails() {
diff --git a/app/assets/javascripts/import_entities/constants.js b/app/assets/javascripts/import_entities/constants.js
index 48b7febca4b..23604c7fb44 100644
--- a/app/assets/javascripts/import_entities/constants.js
+++ b/app/assets/javascripts/import_entities/constants.js
@@ -1,18 +1,65 @@
-// The `scheduling` status is only present on the client-side,
-// it is used as the status when we are requesting to start an import.
+import { __, s__ } from '~/locale';
export const STATUSES = {
FINISHED: 'finished',
FAILED: 'failed',
SCHEDULED: 'scheduled',
+ SCHEDULING: 'scheduling', // only present client-side, used when user is requesting to start an import
CREATED: 'created',
STARTED: 'started',
NONE: 'none',
- SCHEDULING: 'scheduling',
CANCELED: 'canceled',
TIMEOUT: 'timeout',
+ PARTIAL: 'partial', // only present client-side, finished but with failures
};
export const PROVIDERS = {
GITHUB: 'github',
};
+
+const SCHEDULED_STATUS_ICON = {
+ icon: 'status-scheduled',
+ text: __('Pending'),
+ variant: 'muted',
+};
+
+export const STATUS_ICON_MAP = {
+ [STATUSES.NONE]: {
+ icon: 'status-waiting',
+ text: __('Not started'),
+ variant: 'muted',
+ },
+ [STATUSES.SCHEDULING]: SCHEDULED_STATUS_ICON,
+ [STATUSES.SCHEDULED]: SCHEDULED_STATUS_ICON,
+ [STATUSES.CREATED]: SCHEDULED_STATUS_ICON,
+ [STATUSES.STARTED]: {
+ icon: 'status-running',
+ text: __('Importing...'),
+ variant: 'info',
+ },
+ [STATUSES.FAILED]: {
+ icon: 'status-failed',
+ text: __('Failed'),
+ variant: 'danger',
+ },
+ [STATUSES.TIMEOUT]: {
+ icon: 'status-failed',
+ text: __('Timeout'),
+ variant: 'danger',
+ },
+ [STATUSES.CANCELED]: {
+ icon: 'status-stopped',
+ text: __('Cancelled'),
+ variant: 'neutral',
+ },
+ [STATUSES.FINISHED]: {
+ icon: 'status-success',
+ text: __('Complete'),
+ variant: 'success',
+ },
+ [STATUSES.PARTIAL]: {
+ icon: 'status-alert',
+ text: s__('Import|Partially completed'),
+ variant: 'warning',
+ },
+};
diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_status.vue b/app/assets/javascripts/import_entities/import_groups/components/import_status.vue
new file mode 100644
index 00000000000..cdb38cdf7f1
--- /dev/null
+++ b/app/assets/javascripts/import_entities/import_groups/components/import_status.vue
@@ -0,0 +1,83 @@
+<script>
+import { GlBadge, GlLink } from '@gitlab/ui';
+import { mergeUrlParams } from '~/lib/utils/url_utility';
+import { STATUSES, STATUS_ICON_MAP } from '~/import_entities/constants';
+
+export default {
+ components: {
+ GlBadge,
+ GlLink,
+ },
+
+ inject: {
+ detailsPath: {
+ default: undefined,
+ },
+ },
+
+ props: {
+ id: {
+ type: Number,
+ required: false,
+ default: null,
+ },
+ entityId: {
+ type: Number,
+ required: false,
+ default: null,
+ },
+ hasFailures: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ showDetailsLink: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ status: {
+ type: String,
+ required: true,
+ },
+ },
+
+ computed: {
+ isPartial() {
+ return this.status === STATUSES.FINISHED && this.hasFailures;
+ },
+
+ mappedStatus() {
+ if (this.isPartial) {
+ return STATUS_ICON_MAP[STATUSES.PARTIAL];
+ }
+
+ return STATUS_ICON_MAP[this.status];
+ },
+
+ showDetails() {
+ return this.showDetailsLink && Boolean(this.detailsPathWithId) && this.hasFailures;
+ },
+
+ detailsPathWithId() {
+ if (!this.id || !this.entityId || !this.detailsPath) {
+ return null;
+ }
+
+ return mergeUrlParams({ id: this.id, entity_id: this.entityId }, this.detailsPath);
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-badge :icon="mappedStatus.icon" :variant="mappedStatus.variant" size="md" icon-size="sm">
+ {{ mappedStatus.text }}
+ </gl-badge>
+
+ <div v-if="showDetails" class="gl-mt-2">
+ <gl-link :href="detailsPathWithId">{{ s__('Import|See failures') }}</gl-link>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_table.vue b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue
index 24197c680eb..df1e50cb433 100644
--- a/app/assets/javascripts/import_entities/import_groups/components/import_table.vue
+++ b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue
@@ -42,9 +42,6 @@ import ImportTargetCell from './import_target_cell.vue';
const VALIDATION_DEBOUNCE_TIME = DEFAULT_DEBOUNCE_AND_THROTTLE_MS;
const PAGE_SIZES = [20, 50, 100];
const DEFAULT_PAGE_SIZE = PAGE_SIZES[0];
-const DEFAULT_TH_CLASSES =
- 'gl-bg-transparent! gl-border-b-solid! gl-border-b-gray-200! gl-border-b-1! gl-p-5!';
-const DEFAULT_TD_CLASSES = 'gl-vertical-align-top!';
export default {
components: {
@@ -129,36 +126,28 @@ export default {
{
key: 'selected',
label: '',
- // eslint-disable-next-line @gitlab/require-i18n-strings
- thClass: `${DEFAULT_TH_CLASSES} gl-w-3 gl-pr-3!`,
- // eslint-disable-next-line @gitlab/require-i18n-strings
- tdClass: `${DEFAULT_TD_CLASSES} gl-pr-3!`,
+ thClass: 'gl-w-3 gl-pr-3!',
+ tdClass: 'gl-pr-3!',
},
{
key: 'webUrl',
label: s__('BulkImport|Source group'),
- thClass: `${DEFAULT_TH_CLASSES} gl-pl-0! gl-w-half`,
- // eslint-disable-next-line @gitlab/require-i18n-strings
- tdClass: `${DEFAULT_TD_CLASSES} gl-pl-0!`,
+ thClass: 'gl-pl-0! gl-w-half',
+ tdClass: 'gl-pl-0!',
},
{
key: 'importTarget',
label: s__('BulkImport|New group'),
- thClass: `${DEFAULT_TH_CLASSES} gl-w-half`,
- tdClass: DEFAULT_TD_CLASSES,
+ thClass: `gl-w-half`,
},
{
key: 'progress',
label: __('Status'),
- thClass: `${DEFAULT_TH_CLASSES}`,
- tdClass: DEFAULT_TD_CLASSES,
tdAttr: { 'data-qa-selector': 'import_status_indicator' },
},
{
key: 'actions',
label: '',
- thClass: `${DEFAULT_TH_CLASSES}`,
- tdClass: DEFAULT_TD_CLASSES,
},
],
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/services/local_storage_cache.js b/app/assets/javascripts/import_entities/import_groups/graphql/services/local_storage_cache.js
index 1aad22f0f3f..c2e35ce8270 100644
--- a/app/assets/javascripts/import_entities/import_groups/graphql/services/local_storage_cache.js
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/services/local_storage_cache.js
@@ -60,7 +60,7 @@ export class LocalStorageCache {
updateStatusByJobId(jobId, status) {
this.getCacheKeysByJobId(jobId).forEach((webUrl) =>
this.set(webUrl, {
- ...(this.get(webUrl) ?? {}),
+ ...this.get(webUrl),
progress: {
id: jobId,
status,
diff --git a/app/assets/javascripts/import_entities/import_projects/components/advanced_settings.vue b/app/assets/javascripts/import_entities/import_projects/components/advanced_settings.vue
index cf1a4de68ed..d22a52df326 100644
--- a/app/assets/javascripts/import_entities/import_projects/components/advanced_settings.vue
+++ b/app/assets/javascripts/import_entities/import_projects/components/advanced_settings.vue
@@ -41,7 +41,7 @@ export default {
:key="name"
:checked="value[name]"
:data-qa-option-name="name"
- data-qa-selector="advanced_settings_checkbox"
+ data-testid="advanced-settings-checkbox"
@change="$emit('input', { ...value, [name]: $event })"
>
{{ label }}
diff --git a/app/assets/javascripts/import_entities/import_projects/components/github_organizations_box.vue b/app/assets/javascripts/import_entities/import_projects/components/github_organizations_box.vue
index 5d5965e33da..72c6f45cdc9 100644
--- a/app/assets/javascripts/import_entities/import_projects/components/github_organizations_box.vue
+++ b/app/assets/javascripts/import_entities/import_projects/components/github_organizations_box.vue
@@ -1,6 +1,6 @@
<script>
-import * as Sentry from '@sentry/browser';
import { GlCollapsibleListbox } from '@gitlab/ui';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { createAlert } from '~/alert';
import { __, s__ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
diff --git a/app/assets/javascripts/import_entities/import_projects/components/github_status_table.vue b/app/assets/javascripts/import_entities/import_projects/components/github_status_table.vue
index cb3476c48db..5931e0d307a 100644
--- a/app/assets/javascripts/import_entities/import_projects/components/github_status_table.vue
+++ b/app/assets/javascripts/import_entities/import_projects/components/github_status_table.vue
@@ -78,7 +78,6 @@ export default {
@input="setFilter({ organization_login: $event })"
/>
<gl-search-box-by-click
- data-qa-selector="githubish_import_filter_field"
name="filter"
:disabled="isNameFilterDisabled"
:value="nameFilter"
diff --git a/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue b/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue
index 009945f8b9b..d98132382c6 100644
--- a/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue
+++ b/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue
@@ -155,7 +155,6 @@ export default {
<slot name="actions"></slot>
<form v-if="filterable" class="gl-ml-auto" novalidate @submit.prevent>
<gl-search-box-by-click
- data-qa-selector="githubish_import_filter_field"
name="filter"
:placeholder="__('Filter by name')"
autofocus
diff --git a/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue b/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue
index d75ba53d727..9b5aff45375 100644
--- a/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue
+++ b/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue
@@ -159,7 +159,7 @@ export default {
<template>
<tr
class="gl-h-11"
- data-qa-selector="project_import_row"
+ data-testid="project-import-row"
:data-qa-source-project="repo.importSource.fullName"
>
<td>
@@ -174,7 +174,7 @@ export default {
:href="repo.importedProject.fullPath"
class="gl-font-sm"
target="_blank"
- data-qa-selector="go_to_project_link"
+ data-testid="go-to-project-link"
>
{{ displayFullPath }}
</gl-link>
@@ -182,7 +182,7 @@ export default {
</gl-sprintf>
</div>
</td>
- <td data-testid="fullPath" data-qa-selector="project_path_content">
+ <td data-testid="fullPath">
<div class="gl-display-flex gl-sm-flex-wrap">
<template v-if="repo.importSource.target">{{ repo.importSource.target }}</template>
<template v-else-if="isImportNotStarted || isSelectedForReimport">
@@ -201,14 +201,14 @@ export default {
ref="newNameInput"
v-model="newNameInput"
class="gl-rounded-top-left-none gl-rounded-bottom-left-none"
- data-qa-selector="project_path_field"
+ data-testid="project-path-field"
/>
</div>
</template>
<template v-else-if="repo.importedProject">{{ displayFullPath }}</template>
</div>
</td>
- <td data-qa-selector="import_status_indicator">
+ <td data-testid="import-status-indicator">
<import-status :project-id="importedProjectId" :status="importStatus" :stats="stats" />
</td>
<td data-testid="actions" class="gl-white-space-nowrap">
@@ -235,7 +235,7 @@ export default {
<gl-button
v-if="isImportNotStarted || isFinished"
type="button"
- data-qa-selector="import_button"
+ data-testid="import-button"
@click="handleImportRepo()"
>
{{ importButtonText }}
diff --git a/app/assets/javascripts/import_entities/import_projects/store/actions.js b/app/assets/javascripts/import_entities/import_projects/store/actions.js
index 4305f8d4db5..e5cbac71ce0 100644
--- a/app/assets/javascripts/import_entities/import_projects/store/actions.js
+++ b/app/assets/javascripts/import_entities/import_projects/store/actions.js
@@ -83,7 +83,7 @@ const fetchReposFactory = ({ reposPath = isRequired() }) => ({ state, commit })
.get(
pathWithParams({
path: reposPath,
- ...(filter ?? {}),
+ ...filter,
...paginationParams({ state }),
}),
)
diff --git a/app/assets/javascripts/incidents/components/incidents_list.vue b/app/assets/javascripts/incidents/components/incidents_list.vue
index 0e1afebbe2b..727ab43435d 100644
--- a/app/assets/javascripts/incidents/components/incidents_list.vue
+++ b/app/assets/javascripts/incidents/components/incidents_list.vue
@@ -87,12 +87,11 @@ export default {
{
key: 'incidentSla',
label: s__('IncidentManagement|Time to SLA'),
- thClass: `gl-text-right gl-w-10p`,
+ thClass: `${thClass} gl-text-right gl-w-10p`,
tdClass: `${tdClass} gl-text-right`,
thAttr: TH_INCIDENT_SLA_TEST_ID,
actualSortKey: 'SLA_DUE_AT',
sortable: true,
- sortDirection: 'asc',
},
{
key: 'assignees',
diff --git a/app/assets/javascripts/integrations/edit/components/integration_form.vue b/app/assets/javascripts/integrations/edit/components/integration_form.vue
index fa9a59212eb..281666a021d 100644
--- a/app/assets/javascripts/integrations/edit/components/integration_form.vue
+++ b/app/assets/javascripts/integrations/edit/components/integration_form.vue
@@ -1,9 +1,9 @@
<script>
import { GlAlert, GlForm } from '@gitlab/ui';
import axios from 'axios';
-import * as Sentry from '@sentry/browser';
// eslint-disable-next-line no-restricted-imports
import { mapState, mapActions, mapGetters } from 'vuex';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { s__ } from '~/locale';
import SafeHtml from '~/vue_shared/directives/safe_html';
import {
diff --git a/app/assets/javascripts/integrations/overrides/components/integration_overrides.vue b/app/assets/javascripts/integrations/overrides/components/integration_overrides.vue
index a8389e32b40..356557442db 100644
--- a/app/assets/javascripts/integrations/overrides/components/integration_overrides.vue
+++ b/app/assets/javascripts/integrations/overrides/components/integration_overrides.vue
@@ -1,6 +1,6 @@
<script>
import { GlLink, GlLoadingIcon, GlPagination, GlTable, GlAlert } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { DEFAULT_PER_PAGE } from '~/api';
import { fetchOverrides } from '~/integrations/overrides/api';
diff --git a/app/assets/javascripts/invite_members/components/invite_groups_modal.vue b/app/assets/javascripts/invite_members/components/invite_groups_modal.vue
index 4b492e48095..ceb9200dfad 100644
--- a/app/assets/javascripts/invite_members/components/invite_groups_modal.vue
+++ b/app/assets/javascripts/invite_members/components/invite_groups_modal.vue
@@ -1,7 +1,7 @@
<script>
-import * as Sentry from '@sentry/browser';
import { GlAlert } from '@gitlab/ui';
import { uniqueId } from 'lodash';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import Api from '~/api';
import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants';
import InviteModalBase from 'ee_else_ce/invite_members/components/invite_modal_base.vue';
diff --git a/app/assets/javascripts/invite_members/components/invite_members_modal.vue b/app/assets/javascripts/invite_members/components/invite_members_modal.vue
index 509efd31dcd..1a10130e969 100644
--- a/app/assets/javascripts/invite_members/components/invite_members_modal.vue
+++ b/app/assets/javascripts/invite_members/components/invite_members_modal.vue
@@ -1,13 +1,17 @@
<script>
-import { GlAlert, GlButton, GlCollapse, GlIcon } from '@gitlab/ui';
+import { GlAlert, GlButton, GlCollapse, GlLink, GlIcon, GlSprintf } from '@gitlab/ui';
import { partition, isString, uniqueId, isEmpty } from 'lodash';
import SafeHtml from '~/vue_shared/directives/safe_html';
import InviteModalBase from 'ee_else_ce/invite_members/components/invite_modal_base.vue';
import Api from '~/api';
import Tracking from '~/tracking';
import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants';
-import { n__, sprintf } from '~/locale';
-import { memberName, triggerExternalAlert } from 'ee_else_ce/invite_members/utils/member_utils';
+import { n__, s__, sprintf } from '~/locale';
+import {
+ memberName,
+ triggerExternalAlert,
+ inviteMembersTrackingOptions,
+} from 'ee_else_ce/invite_members/utils/member_utils';
import { captureException } from '~/ci/runner/sentry_utils';
import {
USERS_FILTER_ALL,
@@ -31,7 +35,9 @@ export default {
GlAlert,
GlButton,
GlCollapse,
+ GlLink,
GlIcon,
+ GlSprintf,
InviteModalBase,
MembersTokenSelect,
ModalConfetti,
@@ -43,6 +49,17 @@ export default {
SafeHtml,
},
mixins: [Tracking.mixin({ category: INVITE_MEMBER_MODAL_TRACKING_CATEGORY })],
+ inject: {
+ isCurrentUserAdmin: {
+ default: false,
+ },
+ isEmailSignupEnabled: {
+ default: true,
+ },
+ newUsersUrl: {
+ default: '',
+ },
+ },
props: {
id: {
type: String,
@@ -122,6 +139,12 @@ export default {
isCelebration() {
return this.mode === 'celebrate';
},
+ baseTrackingDetails() {
+ return { label: this.source, celebrate: this.isCelebration };
+ },
+ isTextForAdmin() {
+ return this.isCurrentUserAdmin && Boolean(this.newUsersUrl);
+ },
modalTitle() {
return this.$options.labels.modal[this.mode].title;
},
@@ -131,6 +154,11 @@ export default {
labelIntroText() {
return this.$options.labels[this.inviteTo][this.mode].introText;
},
+ labelSearchField() {
+ return this.isEmailSignupEnabled
+ ? this.$options.labels.searchField
+ : s__('InviteMembersModal|Username');
+ },
isEmptyInvites() {
return Boolean(this.newUsersToInvite.length);
},
@@ -144,6 +172,14 @@ export default {
this.errorList.length,
);
},
+ signupDisabledText() {
+ return s__(
+ "InviteMembersModal|Administrators can %{linkStart}add new users by email manually%{linkEnd}. After they've been added, you can invite them to this group with their username.",
+ );
+ },
+ signupDisabledTitle() {
+ return s__('InviteMembersModal|Inviting users by email is disabled');
+ },
showUserLimitNotification() {
return !isEmpty(this.usersLimitDataset.alertVariant);
},
@@ -173,8 +209,13 @@ export default {
count: this.errorsExpanded.length,
});
},
+ formGroupDescriptionText() {
+ return this.isEmailSignupEnabled
+ ? this.$options.labels.placeHolder
+ : s__('InviteMembersModal|Select members');
+ },
formGroupDescription() {
- return this.invalidFeedbackMessage ? null : this.$options.labels.placeHolder;
+ return this.invalidFeedbackMessage ? null : this.formGroupDescriptionText;
},
},
watch: {
@@ -218,13 +259,13 @@ export default {
this.source = source;
this.$root.$emit(BV_SHOW_MODAL, this.modalId);
- this.track('render', { label: this.source });
+ this.track('render', inviteMembersTrackingOptions(this.baseTrackingDetails));
},
closeModal() {
this.$root.$emit(BV_HIDE_MODAL, this.modalId);
},
showEmptyInvitesAlert() {
- this.invalidFeedbackMessage = this.$options.labels.placeHolder;
+ this.invalidFeedbackMessage = this.formGroupDescriptionText;
this.shouldShowEmptyInvitesAlert = true;
this.$refs.alerts.focus();
},
@@ -287,10 +328,10 @@ export default {
return this.newUsersToInvite.find((member) => memberName(member) === username)?.name;
},
onCancel() {
- this.track('click_cancel', { label: this.source });
+ this.track('click_cancel', inviteMembersTrackingOptions(this.baseTrackingDetails));
},
onClose() {
- this.track('click_x', { label: this.source });
+ this.track('click_x', inviteMembersTrackingOptions(this.baseTrackingDetails));
},
resetFields() {
this.clearValidation();
@@ -299,7 +340,7 @@ export default {
this.newUsersToInvite = [];
},
onInviteSuccess() {
- this.track('invite_successful', { label: this.source });
+ this.track('invite_successful', inviteMembersTrackingOptions(this.baseTrackingDetails));
if (this.reloadPageOnSubmit) {
reloadOnInvitationSuccess();
@@ -345,7 +386,7 @@ export default {
:default-access-level="defaultAccessLevel"
:help-link="helpLink"
:label-intro-text="labelIntroText"
- :label-search-field="$options.labels.searchField"
+ :label-search-field="labelSearchField"
:form-group-description="formGroupDescription"
:invalid-feedback-message="invalidFeedbackMessage"
:is-loading="isLoading"
@@ -429,6 +470,24 @@ export default {
</gl-button>
</template>
</gl-alert>
+ <gl-alert
+ v-if="!isEmailSignupEnabled"
+ id="signup-disabled-alert"
+ :dismissible="false"
+ :title="signupDisabledTitle"
+ class="gl-mb-4"
+ variant="warning"
+ data-testid="email-signup-disabled-alert"
+ >
+ <gl-sprintf :message="signupDisabledText">
+ <template #link="{ content }">
+ <gl-link v-if="isTextForAdmin" :href="newUsersUrl" target="_blank">{{
+ content
+ }}</gl-link>
+ <span v-else>{{ content }}</span>
+ </template>
+ </gl-sprintf>
+ </gl-alert>
<user-limit-notification
v-else-if="showUserLimitNotification"
class="gl-mb-5"
@@ -447,6 +506,7 @@ export default {
v-model="newUsersToInvite"
class="gl-mb-2"
aria-labelledby="empty-invites-alert"
+ :can-use-email-token="isEmailSignupEnabled"
:input-id="inputId"
:exception-state="exceptionState"
:users-filter="usersFilter"
diff --git a/app/assets/javascripts/invite_members/components/invite_modal_base.vue b/app/assets/javascripts/invite_members/components/invite_modal_base.vue
index 18d22395104..a14dcd38aa7 100644
--- a/app/assets/javascripts/invite_members/components/invite_modal_base.vue
+++ b/app/assets/javascripts/invite_members/components/invite_modal_base.vue
@@ -297,7 +297,7 @@ export default {
</gl-form-group>
<gl-form-group
- class="gl-w-half gl-xs-w-full"
+ class="gl-sm-w-half gl-w-full"
:label="$options.ACCESS_LEVEL"
:label-for="dropdownId"
>
@@ -317,7 +317,7 @@ export default {
</gl-form-group>
<gl-form-group
- class="gl-w-half gl-xs-w-full"
+ class="gl-sm-w-half gl-w-full"
:label="$options.ACCESS_EXPIRE_DATE"
:label-for="datepickerId"
>
@@ -338,10 +338,10 @@ export default {
<template #modal-footer>
<div
- class="gl-m-0 gl-xs-w-full gl-display-flex gl-xs-flex-direction-column! gl-flex-direction-row-reverse"
+ class="gl-m-0 gl-w-full gl-display-flex gl-xs-flex-direction-column! gl-flex-direction-row-reverse"
>
<gl-button
- class="gl-xs-w-full gl-xs-mb-3! gl-sm-ml-3!"
+ class="gl-w-full gl-sm-w-auto gl-xs-mb-3! gl-sm-ml-3!"
data-testid="invite-modal-submit"
v-bind="actionPrimary.attributes"
@click="onSubmit"
@@ -350,7 +350,7 @@ export default {
</gl-button>
<gl-button
- class="gl-xs-w-full"
+ class="gl-w-full gl-sm-w-auto"
data-testid="invite-modal-cancel"
v-bind="actionCancel.attributes"
@click="onCancel"
diff --git a/app/assets/javascripts/invite_members/components/members_token_select.vue b/app/assets/javascripts/invite_members/components/members_token_select.vue
index 8493787f075..0be04b7af35 100644
--- a/app/assets/javascripts/invite_members/components/members_token_select.vue
+++ b/app/assets/javascripts/invite_members/components/members_token_select.vue
@@ -21,6 +21,11 @@ export default {
GlSprintf,
},
props: {
+ canUseEmailToken: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
placeholder: {
type: String,
required: false,
@@ -68,6 +73,10 @@ export default {
},
computed: {
emailIsValid() {
+ if (!this.canUseEmailToken) {
+ return false;
+ }
+
const regex = /^\S+@\S+$/;
return this.originalInput.match(regex) !== null;
@@ -137,9 +146,8 @@ export default {
username: token.username,
avatar_url: token.avatar_url,
}));
- this.loading = false;
})
- .catch(() => {
+ .finally(() => {
this.loading = false;
});
}, SEARCH_DELAY),
diff --git a/app/assets/javascripts/invite_members/components/project_select.vue b/app/assets/javascripts/invite_members/components/project_select.vue
index 640df5cdb88..6c2f53afe3c 100644
--- a/app/assets/javascripts/invite_members/components/project_select.vue
+++ b/app/assets/javascripts/invite_members/components/project_select.vue
@@ -115,7 +115,6 @@ export default {
:search-placeholder="$options.i18n.searchPlaceholder"
:no-results-text="$options.i18n.emptySearchResult"
data-testid="project-select-dropdown"
- data-qa-selector="project_select_dropdown"
class="gl-collapsible-listbox-w-full"
@search="searchTerm = $event"
@select="selectProject"
diff --git a/app/assets/javascripts/invite_members/init_invite_members_modal.js b/app/assets/javascripts/invite_members/init_invite_members_modal.js
index 41ed0179364..8dfe697e2cb 100644
--- a/app/assets/javascripts/invite_members/init_invite_members_modal.js
+++ b/app/assets/javascripts/invite_members/init_invite_members_modal.js
@@ -25,6 +25,9 @@ export default (function initInviteMembersModal() {
name: 'InviteMembersModalRoot',
provide: {
name: el.dataset.name,
+ newUsersUrl: el.dataset.newUsersUrl,
+ isCurrentUserAdmin: parseBoolean(el.dataset.isCurrentUserAdmin),
+ isEmailSignupEnabled: parseBoolean(el.dataset.isSignupEnabled),
},
render: (createElement) =>
createElement(InviteMembersModal, {
diff --git a/app/assets/javascripts/invite_members/utils/member_utils.js b/app/assets/javascripts/invite_members/utils/member_utils.js
index 7998cb69445..52fb5e98f27 100644
--- a/app/assets/javascripts/invite_members/utils/member_utils.js
+++ b/app/assets/javascripts/invite_members/utils/member_utils.js
@@ -6,3 +6,7 @@ export function memberName(member) {
export function triggerExternalAlert() {
return false;
}
+
+export function inviteMembersTrackingOptions(options) {
+ return { label: options.label };
+}
diff --git a/app/assets/javascripts/issuable/components/locked_badge.vue b/app/assets/javascripts/issuable/components/locked_badge.vue
index f97ac888417..652d02e8f9d 100644
--- a/app/assets/javascripts/issuable/components/locked_badge.vue
+++ b/app/assets/javascripts/issuable/components/locked_badge.vue
@@ -20,9 +20,12 @@ export default {
},
computed: {
title() {
- return sprintf(__('This %{issuable} is locked. Only project members can comment.'), {
- issuable: issuableTypeText[this.issuableType],
- });
+ return sprintf(
+ __('The discussion in this %{issuable} is locked. Only project members can comment.'),
+ {
+ issuable: issuableTypeText[this.issuableType],
+ },
+ );
},
},
};
diff --git a/app/assets/javascripts/issuable/components/related_issuable_item.vue b/app/assets/javascripts/issuable/components/related_issuable_item.vue
index 71bd301162e..126a3a84d66 100644
--- a/app/assets/javascripts/issuable/components/related_issuable_item.vue
+++ b/app/assets/javascripts/issuable/components/related_issuable_item.vue
@@ -88,6 +88,9 @@ export default {
workItemIid() {
return String(this.iid);
},
+ pipelinePath() {
+ return this.pipelineStatus?.details_path || this.pipelineStatus?.detailsPath;
+ },
},
methods: {
handleTitleClick(event) {
@@ -191,16 +194,16 @@ export default {
<div
class="item-attributes-area gl-display-flex gl-align-items-center gl-flex-wrap gl-gap-3"
>
- <span v-if="hasPipeline" class="mr-ci-status order-md-last">
- <a :href="pipelineStatus.details_path">
- <ci-icon v-gl-tooltip :status="pipelineStatus" :title="pipelineStatusTooltip" />
+ <span v-if="hasPipeline" class="mr-ci-status order-md-last gl-md-ml-3 gl-mr-n2">
+ <a :href="pipelinePath">
+ <ci-icon :status="pipelineStatus" :title="pipelineStatusTooltip" />
</a>
</span>
<issue-milestone
v-if="hasMilestone"
:milestone="milestone"
- class="item-milestone gl-font-sm gl-display-flex gl-align-items-center order-md-first"
+ class="item-milestone gl-font-sm gl-display-flex gl-align-items-center order-md-first gl-ml-2"
/>
<!-- Flex order for slots is defined in the parent component: e.g. related_issues_block.vue -->
diff --git a/app/assets/javascripts/issuable/components/status_badge.vue b/app/assets/javascripts/issuable/components/status_badge.vue
index 949fb3c1ce5..35f6446d582 100644
--- a/app/assets/javascripts/issuable/components/status_badge.vue
+++ b/app/assets/javascripts/issuable/components/status_badge.vue
@@ -14,29 +14,29 @@ import {
const badgePropertiesMap = {
[TYPE_EPIC]: {
[STATUS_OPEN]: {
- icon: 'epic',
+ icon: 'issue-open-m',
text: __('Open'),
variant: 'success',
},
[STATUS_CLOSED]: {
- icon: 'epic-closed',
+ icon: 'issue-close',
text: __('Closed'),
variant: 'info',
},
},
[TYPE_ISSUE]: {
[STATUS_OPEN]: {
- icon: 'issues',
+ icon: 'issue-open-m',
text: __('Open'),
variant: 'success',
},
[STATUS_CLOSED]: {
- icon: 'issue-closed',
+ icon: 'issue-close',
text: __('Closed'),
variant: 'info',
},
[STATUS_LOCKED]: {
- icon: 'issues',
+ icon: 'issue-open-m',
text: __('Open'),
variant: 'success',
},
diff --git a/app/assets/javascripts/issuable/popover/components/mr_popover.vue b/app/assets/javascripts/issuable/popover/components/mr_popover.vue
index e2c2181684f..80ae8ed8cf6 100644
--- a/app/assets/javascripts/issuable/popover/components/mr_popover.vue
+++ b/app/assets/javascripts/issuable/popover/components/mr_popover.vue
@@ -96,14 +96,14 @@ export default {
</gl-skeleton-loader>
<div v-else-if="showDetails" class="d-flex align-items-center justify-content-between">
<div class="d-inline-flex align-items-center">
- <gl-badge class="gl-mr-3" :variant="badgeVariant">
+ <gl-badge class="gl-mr-2" :variant="badgeVariant">
{{ stateHumanName }}
</gl-badge>
<span class="gl-text-secondary">
{{ __('Opened') }} <time v-text="formattedTime"></time
></span>
</div>
- <ci-icon v-if="detailedStatus" :status="detailedStatus" />
+ <ci-icon v-if="detailedStatus" :status="detailedStatus" class="gl-ml-2" />
</div>
<h5 v-if="!$apollo.queries.mergeRequest.loading" class="my-2">{{ title }}</h5>
<!-- eslint-disable @gitlab/vue-require-i18n-strings -->
diff --git a/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue b/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue
index a756229e6ca..b6465cf6c68 100644
--- a/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue
+++ b/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue
@@ -5,7 +5,7 @@ import {
GlFilteredSearchToken,
GlTooltipDirective,
} from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import getIssuesQuery from 'ee_else_ce/issues/dashboard/queries/get_issues.query.graphql';
import IssueCardStatistics from 'ee_else_ce/issues/list/components/issue_card_statistics.vue';
import IssueCardTimeInfo from 'ee_else_ce/issues/list/components/issue_card_time_info.vue';
@@ -165,9 +165,6 @@ export default {
skip() {
return !this.hasSearch;
},
- context: {
- isSingleRequest: true,
- },
},
},
computed: {
diff --git a/app/assets/javascripts/issues/issue.js b/app/assets/javascripts/issues/issue.js
index 06bbcdc12ea..b83db65caa6 100644
--- a/app/assets/javascripts/issues/issue.js
+++ b/app/assets/javascripts/issues/issue.js
@@ -53,6 +53,8 @@ export default class Issue {
$(document).trigger('issuable:change', isClosed);
+ // TODO: Remove this with the removal of the old navigation.
+ // See https://gitlab.com/groups/gitlab-org/-/epics/11875.
let numProjectIssues = Number(
projectIssuesCounter.first().text().trim().replace(/[^\d]/, ''),
);
diff --git a/app/assets/javascripts/issues/list/components/issues_list_app.vue b/app/assets/javascripts/issues/list/components/issues_list_app.vue
index 16e687cff10..72bb88ef1d5 100644
--- a/app/assets/javascripts/issues/list/components/issues_list_app.vue
+++ b/app/assets/javascripts/issues/list/components/issues_list_app.vue
@@ -9,11 +9,11 @@ import {
GlDrawer,
GlLink,
} from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
import produce from 'immer';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { isEmpty } from 'lodash';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import IssueCardStatistics from 'ee_else_ce/issues/list/components/issue_card_statistics.vue';
import IssueCardTimeInfo from 'ee_else_ce/issues/list/components/issue_card_time_info.vue';
import getIssuesQuery from 'ee_else_ce/issues/list/queries/get_issues.query.graphql';
@@ -277,9 +277,6 @@ export default {
skip() {
return !this.hasAnyIssues || isEmpty(this.pageParams);
},
- context: {
- isSingleRequest: true,
- },
},
},
computed: {
@@ -910,7 +907,7 @@ export default {
v-if="issuesDrawerEnabled"
:open="isIssuableSelected"
header-height="calc(var(--top-bar-height) + var(--performance-bar-height))"
- class="gl-w-40p gl-xs-w-full"
+ class="gl-w-full gl-sm-w-40p"
@close="activeIssuable = null"
>
<template #title>
@@ -1030,7 +1027,10 @@ export default {
:export-csv-path="exportCsvPathWithQuery"
:issuable-count="currentTabCount"
/>
- <gl-disclosure-dropdown-group :bordered="true" :group="subscribeDropdownOptions" />
+ <gl-disclosure-dropdown-group
+ :bordered="showCsvButtons"
+ :group="subscribeDropdownOptions"
+ />
</gl-disclosure-dropdown>
</template>
diff --git a/app/assets/javascripts/issues/service_desk/components/service_desk_list_app.vue b/app/assets/javascripts/issues/service_desk/components/service_desk_list_app.vue
index 4b59672428b..eb7bcf70563 100644
--- a/app/assets/javascripts/issues/service_desk/components/service_desk_list_app.vue
+++ b/app/assets/javascripts/issues/service_desk/components/service_desk_list_app.vue
@@ -1,7 +1,7 @@
<script>
-import * as Sentry from '@sentry/browser';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { isEmpty } from 'lodash';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { fetchPolicies } from '~/lib/graphql';
import { isPositiveInteger } from '~/lib/utils/number_utils';
import axios from '~/lib/utils/axios_utils';
@@ -166,9 +166,6 @@ export default {
skip() {
return this.shouldSkipQuery;
},
- context: {
- isSingleRequest: true,
- },
},
},
computed: {
diff --git a/app/assets/javascripts/issues/show/components/fields/description.vue b/app/assets/javascripts/issues/show/components/fields/description.vue
index 10323b99665..1f159e71da9 100644
--- a/app/assets/javascripts/issues/show/components/fields/description.vue
+++ b/app/assets/javascripts/issues/show/components/fields/description.vue
@@ -79,7 +79,6 @@ export default {
:autocomplete-data-sources="autocompleteDataSources"
supports-quick-actions
autofocus
- data-qa-selector="description_field"
@input="$emit('input', $event)"
@keydown.meta.enter="saveIssuable"
@keydown.ctrl.enter="saveIssuable"
diff --git a/app/assets/javascripts/issues/show/components/header_actions.vue b/app/assets/javascripts/issues/show/components/header_actions.vue
index dee4c536afa..32df19dfe44 100644
--- a/app/assets/javascripts/issues/show/components/header_actions.vue
+++ b/app/assets/javascripts/issues/show/components/header_actions.vue
@@ -1,17 +1,17 @@
<script>
import {
GlButton,
- GlDropdown,
+ GlDisclosureDropdown,
GlDropdownDivider,
- GlDropdownItem,
+ GlDisclosureDropdownItem,
GlLink,
GlModal,
GlModalDirective,
GlTooltipDirective,
} from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
// eslint-disable-next-line no-restricted-imports
import { mapActions, mapGetters, mapState } from 'vuex';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { createAlert, VARIANT_SUCCESS } from '~/alert';
import { EVENT_ISSUABLE_VUE_APP_CHANGE } from '~/issuable/constants';
import { STATUS_CLOSED, TYPE_ISSUE, issuableTypeText } from '~/issues/constants';
@@ -59,9 +59,9 @@ export default {
components: {
DeleteIssueModal,
GlButton,
- GlDropdown,
+ GlDisclosureDropdown,
GlDropdownDivider,
- GlDropdownItem,
+ GlDisclosureDropdownItem,
GlLink,
GlModal,
AbuseCategorySelector,
@@ -184,6 +184,18 @@ export default {
showMovedSidebarOptions() {
return this.isMrSidebarMoved && this.isUserSignedIn;
},
+ newIssueItem() {
+ return {
+ text: this.newIssueTypeText,
+ href: this.newIssuePath,
+ };
+ },
+ submitSpamItem() {
+ return {
+ text: __('Submit as spam'),
+ href: this.submitAsSpamPath,
+ };
+ },
},
created() {
eventHub.$on('toggle.issuable.state', this.toggleIssueState);
@@ -197,6 +209,7 @@ export default {
toggleIssueState() {
if (!this.isClosed && this.getBlockedByIssues?.length) {
this.$refs.blockedByIssuesModal.show();
+ this.closeActionsDropdown();
return;
}
@@ -204,6 +217,7 @@ export default {
},
toggleReportAbuseDrawer(isOpen) {
this.isReportAbuseDrawerOpen = isOpen;
+ this.closeActionsDropdown();
},
invokeUpdateIssueMutation() {
this.toggleStateButtonLoading(true);
@@ -237,6 +251,7 @@ export default {
.catch(() => createAlert({ message: __('Error occurred while updating the issue status') }))
.finally(() => {
this.toggleStateButtonLoading(false);
+ this.closeActionsDropdown();
});
},
promoteToEpic() {
@@ -267,16 +282,24 @@ export default {
.catch(() => createAlert({ message: this.$options.i18n.promoteErrorMessage }))
.finally(() => {
this.toggleStateButtonLoading(false);
+ this.closeActionsDropdown();
});
},
edit() {
issuesEventHub.$emit('open.form');
+ this.closeActionsDropdown();
},
copyReference() {
toast(__('Reference copied'));
+ this.closeActionsDropdown();
},
copyEmailAddress() {
toast(__('Email address copied'));
+ this.closeActionsDropdown();
+ },
+ closeActionsDropdown() {
+ this.$refs.issuableActionsDropdownMobile?.close();
+ this.$refs.issuableActionsDropdownDesktop?.close();
},
},
TYPE_ISSUE,
@@ -285,87 +308,90 @@ export default {
<template>
<div class="detail-page-header-actions gl-display-flex gl-align-self-start gl-sm-gap-3">
- <gl-dropdown
- v-if="hasMobileDropdown"
- class="gl-sm-display-none! w-100"
- block
- :text="dropdownText"
- data-testid="mobile-dropdown"
- :loading="isToggleStateButtonLoading"
- >
- <template v-if="showMovedSidebarOptions">
- <sidebar-subscriptions-widget
- :iid="String(iid)"
- :full-path="fullPath"
- :issuable-type="$options.TYPE_ISSUE"
- data-testid="notification-toggle"
- />
+ <div class="gl-sm-display-none! w-100">
+ <gl-disclosure-dropdown
+ v-if="hasMobileDropdown"
+ ref="issuableActionsDropdownMobile"
+ toggle-class="gl-w-full"
+ block
+ :toggle-text="dropdownText"
+ :auto-close="false"
+ data-testid="mobile-dropdown"
+ :loading="isToggleStateButtonLoading"
+ placement="right"
+ >
+ <template v-if="showMovedSidebarOptions">
+ <sidebar-subscriptions-widget
+ :iid="String(iid)"
+ :full-path="fullPath"
+ :issuable-type="$options.TYPE_ISSUE"
+ data-testid="notification-toggle"
+ />
- <gl-dropdown-divider />
- </template>
+ <gl-dropdown-divider />
+ </template>
- <template v-if="showLockIssueOption">
- <issuable-lock-form :is-editable="false" data-testid="lock-issue-toggle" />
- </template>
+ <template v-if="showLockIssueOption">
+ <issuable-lock-form :is-editable="false" data-testid="lock-issue-toggle" />
+ </template>
- <gl-dropdown-item v-if="canUpdateIssue" @click="edit">
- {{ $options.i18n.edit }}
- </gl-dropdown-item>
- <gl-dropdown-item
- v-if="showToggleIssueStateButton"
- :data-testid="`mobile_${qaSelector}`"
- @click="toggleIssueState"
- >
- {{ buttonText }}
- </gl-dropdown-item>
- <gl-dropdown-item v-if="canCreateIssue" :href="newIssuePath">
- {{ newIssueTypeText }}
- </gl-dropdown-item>
- <gl-dropdown-item v-if="canPromoteToEpic" @click="promoteToEpic">
- {{ __('Promote to epic') }}
- </gl-dropdown-item>
- <template v-if="isMrSidebarMoved">
- <gl-dropdown-item
- :data-clipboard-text="issuableReference"
- button-class="js-copy-reference"
- data-testid="copy-reference"
- @click="copyReference"
- >{{ $options.i18n.copyReferenceText }}</gl-dropdown-item
- >
- <gl-dropdown-item
- v-if="issuableEmailAddress && showMovedSidebarOptions"
- :data-clipboard-text="issuableEmailAddress"
- data-testid="copy-email"
- @click="copyEmailAddress"
- >{{ copyMailAddressText }}</gl-dropdown-item
+ <gl-disclosure-dropdown-item v-if="canUpdateIssue" @action="edit">
+ <template #list-item>{{ $options.i18n.edit }}</template>
+ </gl-disclosure-dropdown-item>
+ <gl-disclosure-dropdown-item
+ v-if="showToggleIssueStateButton"
+ :data-testid="`mobile_${qaSelector}`"
+ @action="toggleIssueState"
>
- </template>
- <gl-dropdown-item
- v-if="canReportSpam"
- :href="submitAsSpamPath"
- data-method="post"
- rel="nofollow"
- >
- {{ __('Submit as spam') }}
- </gl-dropdown-item>
- <template v-if="canDestroyIssue">
- <gl-dropdown-divider />
- <gl-dropdown-item
- v-gl-modal="$options.deleteModalId"
- variant="danger"
- @click="track('click_dropdown')"
+ <template #list-item>{{ buttonText }}</template>
+ </gl-disclosure-dropdown-item>
+ <gl-disclosure-dropdown-item v-if="canCreateIssue" :item="newIssueItem" />
+ <gl-disclosure-dropdown-item v-if="canPromoteToEpic" @action="promoteToEpic">
+ <template #list-item>{{ __('Promote to epic') }}</template>
+ </gl-disclosure-dropdown-item>
+ <template v-if="isMrSidebarMoved">
+ <gl-disclosure-dropdown-item
+ :data-clipboard-text="issuableReference"
+ button-class="js-copy-reference"
+ data-testid="copy-reference"
+ @action="copyReference"
+ ><template #list-item>{{
+ $options.i18n.copyReferenceText
+ }}</template></gl-disclosure-dropdown-item
+ >
+ <gl-disclosure-dropdown-item
+ v-if="issuableEmailAddress && showMovedSidebarOptions"
+ :data-clipboard-text="issuableEmailAddress"
+ data-testid="copy-email"
+ @action="copyEmailAddress"
+ >{{ copyMailAddressText }}</gl-disclosure-dropdown-item
+ >
+ </template>
+ <gl-disclosure-dropdown-item
+ v-if="canReportSpam"
+ :item="submitSpamItem"
+ data-method="post"
+ rel="nofollow"
+ />
+ <template v-if="canDestroyIssue">
+ <gl-dropdown-divider />
+ <gl-disclosure-dropdown-item
+ v-gl-modal="$options.deleteModalId"
+ variant="danger"
+ @action="track('click_dropdown')"
+ >
+ <template #list-item>{{ deleteButtonText }}</template>
+ </gl-disclosure-dropdown-item>
+ </template>
+ <gl-disclosure-dropdown-item
+ v-if="!isIssueAuthor && isUserSignedIn"
+ data-testid="report-abuse-item"
+ @action="toggleReportAbuseDrawer(true)"
>
- {{ deleteButtonText }}
- </gl-dropdown-item>
- </template>
- <gl-dropdown-item
- v-if="!isIssueAuthor && isUserSignedIn"
- data-testid="report-abuse-item"
- @click="toggleReportAbuseDrawer(true)"
- >
- {{ $options.i18n.reportAbuse }}
- </gl-dropdown-item>
- </gl-dropdown>
+ <template #list-item>{{ $options.i18n.reportAbuse }}</template>
+ </gl-disclosure-dropdown-item>
+ </gl-disclosure-dropdown>
+ </div>
<gl-button
v-if="canUpdateIssue"
@@ -379,20 +405,22 @@ export default {
{{ $options.i18n.edit }}
</gl-button>
- <gl-dropdown
+ <gl-disclosure-dropdown
v-if="hasDesktopDropdown"
id="new-actions-header-dropdown"
+ ref="issuableActionsDropdownDesktop"
v-gl-tooltip.hover
class="gl-display-none gl-sm-display-inline-flex!"
icon="ellipsis_v"
category="tertiary"
- :text="dropdownText"
- :text-sr-only="true"
+ placement="left"
+ :toggle-text="dropdownText"
+ text-sr-only
:title="dropdownText"
:aria-label="dropdownText"
+ :auto-close="false"
data-testid="desktop-dropdown"
no-caret
- right
>
<template v-if="showMovedSidebarOptions && !glFeatures.notificationsTodosButtons">
<sidebar-subscriptions-widget
@@ -401,73 +429,70 @@ export default {
:issuable-type="$options.TYPE_ISSUE"
data-testid="notification-toggle"
/>
-
<gl-dropdown-divider />
</template>
- <gl-dropdown-item
+ <gl-disclosure-dropdown-item
v-if="showToggleIssueStateButton"
data-testid="toggle-issue-state-button"
- @click="toggleIssueState"
+ @action="toggleIssueState"
>
- {{ buttonText }}
- </gl-dropdown-item>
- <gl-dropdown-item v-if="canCreateIssue && isUserSignedIn" :href="newIssuePath">
- {{ newIssueTypeText }}
- </gl-dropdown-item>
- <gl-dropdown-item
+ <template #list-item>{{ buttonText }}</template>
+ </gl-disclosure-dropdown-item>
+ <gl-disclosure-dropdown-item v-if="canCreateIssue && isUserSignedIn" :item="newIssueItem" />
+ <gl-disclosure-dropdown-item
v-if="canPromoteToEpic"
:disabled="isToggleStateButtonLoading"
data-testid="promote-button"
- @click="promoteToEpic"
+ @action="promoteToEpic"
>
- {{ __('Promote to epic') }}
- </gl-dropdown-item>
+ <template #list-item>{{ __('Promote to epic') }}</template>
+ </gl-disclosure-dropdown-item>
<template v-if="showLockIssueOption">
<issuable-lock-form :is-editable="false" data-testid="lock-issue-toggle" />
</template>
<template v-if="isMrSidebarMoved">
- <gl-dropdown-item
+ <gl-disclosure-dropdown-item
:data-clipboard-text="issuableReference"
button-class="js-copy-reference"
data-testid="copy-reference"
- @click="copyReference"
- >{{ $options.i18n.copyReferenceText }}</gl-dropdown-item
+ @action="copyReference"
+ ><template #list-item>{{
+ $options.i18n.copyReferenceText
+ }}</template></gl-disclosure-dropdown-item
>
- <gl-dropdown-item
+ <gl-disclosure-dropdown-item
v-if="issuableEmailAddress && showMovedSidebarOptions"
:data-clipboard-text="issuableEmailAddress"
data-testid="copy-email"
- @click="copyEmailAddress"
- >{{ copyMailAddressText }}</gl-dropdown-item
+ @action="copyEmailAddress"
+ ><template #list-item>{{ copyMailAddressText }}</template></gl-disclosure-dropdown-item
>
</template>
<gl-dropdown-divider v-if="canDestroyIssue || canReportSpam || !isIssueAuthor" />
- <gl-dropdown-item
+ <gl-disclosure-dropdown-item
v-if="canReportSpam"
- :href="submitAsSpamPath"
+ :item="submitSpamItem"
data-method="post"
rel="nofollow"
- >
- {{ __('Submit as spam') }}
- </gl-dropdown-item>
- <gl-dropdown-item
+ />
+ <gl-disclosure-dropdown-item
v-if="!isIssueAuthor && isUserSignedIn"
data-testid="report-abuse-item"
- @click="toggleReportAbuseDrawer(true)"
+ @action="toggleReportAbuseDrawer(true)"
>
- {{ $options.i18n.reportAbuse }}
- </gl-dropdown-item>
+ <template #list-item>{{ $options.i18n.reportAbuse }}</template>
+ </gl-disclosure-dropdown-item>
<template v-if="canDestroyIssue">
- <gl-dropdown-item
+ <gl-disclosure-dropdown-item
v-gl-modal="$options.deleteModalId"
variant="danger"
data-testid="delete-issue-button"
- @click="track('click_dropdown')"
+ @action="track('click_dropdown')"
>
- {{ deleteButtonText }}
- </gl-dropdown-item>
+ <template #list-item>{{ deleteButtonText }}</template>
+ </gl-disclosure-dropdown-item>
</template>
- </gl-dropdown>
+ </gl-disclosure-dropdown>
<gl-modal
ref="blockedByIssuesModal"
diff --git a/app/assets/javascripts/issues/show/components/issue_header.vue b/app/assets/javascripts/issues/show/components/issue_header.vue
index 211f3217ddc..96eb8fbb3c7 100644
--- a/app/assets/javascripts/issues/show/components/issue_header.vue
+++ b/app/assets/javascripts/issues/show/components/issue_header.vue
@@ -82,7 +82,7 @@ export default {
return this.issuableState === STATUS_OPEN || this.issuableState === STATUS_REOPENED;
},
statusIcon() {
- return this.isOpen ? 'issues' : 'issue-closed';
+ return this.isOpen ? 'issue-open-m' : 'issue-close';
},
statusText() {
if (this.isOpen) {
@@ -115,11 +115,9 @@ export default {
<template #status-badge>
<gl-sprintf v-if="closedStatusLink" :message="statusText">
<template #link>
- <gl-link
- class="gl-reset-color! gl-reset-font-size gl-text-decoration-underline"
- :href="closedStatusLink"
- >{{ closedStatusText }}</gl-link
- >
+ <gl-link class="gl-reset-color! gl-text-decoration-underline" :href="closedStatusLink">{{
+ closedStatusText
+ }}</gl-link>
</template>
</gl-sprintf>
<template v-else>{{ statusText }}</template>
diff --git a/app/assets/javascripts/issues/show/components/sticky_header.vue b/app/assets/javascripts/issues/show/components/sticky_header.vue
index 738bb2c2aa0..18e37c4216c 100644
--- a/app/assets/javascripts/issues/show/components/sticky_header.vue
+++ b/app/assets/javascripts/issues/show/components/sticky_header.vue
@@ -2,12 +2,7 @@
import { GlBadge, GlIcon, GlIntersectionObserver, GlLink } from '@gitlab/ui';
import HiddenBadge from '~/issuable/components/hidden_badge.vue';
import LockedBadge from '~/issuable/components/locked_badge.vue';
-import {
- issuableStatusText,
- STATUS_CLOSED,
- TYPE_EPIC,
- WORKSPACE_PROJECT,
-} from '~/issues/constants';
+import { issuableStatusText, STATUS_CLOSED, WORKSPACE_PROJECT } from '~/issues/constants';
import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
export default {
@@ -60,10 +55,7 @@ export default {
return this.issuableStatus === STATUS_CLOSED;
},
statusIcon() {
- if (this.issuableType === TYPE_EPIC) {
- return this.isClosed ? 'epic-closed' : 'epic';
- }
- return this.isClosed ? 'issue-closed' : 'issues';
+ return this.isClosed ? 'issue-close' : 'issue-open-m';
},
statusText() {
return issuableStatusText[this.issuableStatus];
@@ -84,7 +76,7 @@ export default {
data-testid="issue-sticky-header"
>
<div
- class="issue-sticky-header-text gl-display-flex gl-align-items-center gl-gap-2 gl-mx-auto gl-px-5"
+ class="issue-sticky-header-text gl-display-flex gl-align-items-center gl-gap-2 gl-mx-auto"
>
<gl-badge :variant="statusVariant">
<gl-icon :name="statusIcon" />
diff --git a/app/assets/javascripts/issues/show/utils/parse_data.js b/app/assets/javascripts/issues/show/utils/parse_data.js
index f1e6bd2419a..23d5292da00 100644
--- a/app/assets/javascripts/issues/show/utils/parse_data.js
+++ b/app/assets/javascripts/issues/show/utils/parse_data.js
@@ -1,4 +1,4 @@
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { sanitize } from '~/lib/dompurify';
// We currently load + parse the data from the issue app and related merge request
diff --git a/app/assets/javascripts/jira_connect/subscriptions/constants.js b/app/assets/javascripts/jira_connect/subscriptions/constants.js
index 1a10360ed30..85e250b14a0 100644
--- a/app/assets/javascripts/jira_connect/subscriptions/constants.js
+++ b/app/assets/javascripts/jira_connect/subscriptions/constants.js
@@ -37,9 +37,15 @@ export const I18N_OAUTH_FAILED_MESSAGE = s__(
export const INTEGRATIONS_DOC_LINK = helpPagePath('integration/jira/development_panel', {
anchor: 'use-the-integration',
});
+export const PREREQUISITES_DOC_LINK = helpPagePath('administration/settings/jira_cloud_app', {
+ anchor: 'prerequisites',
+});
export const OAUTH_SELF_MANAGED_DOC_LINK = helpPagePath('administration/settings/jira_cloud_app', {
anchor: 'set-up-oauth-authentication',
});
+export const SET_UP_INSTANCE_DOC_LINK = helpPagePath('administration/settings/jira_cloud_app', {
+ anchor: 'set-up-your-instance',
+});
export const FAILED_TO_UPDATE_DOC_LINK = helpPagePath('administration/settings/jira_cloud_app', {
anchor: 'failed-to-update-the-gitlab-instance',
});
diff --git a/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions.vue b/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions.vue
index d8d2db18d9f..9f8fae5b476 100644
--- a/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions.vue
+++ b/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions.vue
@@ -1,13 +1,44 @@
<script>
-import { GlButton, GlLink } from '@gitlab/ui';
-import { OAUTH_SELF_MANAGED_DOC_LINK } from '~/jira_connect/subscriptions/constants';
+import { GlButton, GlFormCheckbox, GlLink } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import {
+ PREREQUISITES_DOC_LINK,
+ OAUTH_SELF_MANAGED_DOC_LINK,
+ SET_UP_INSTANCE_DOC_LINK,
+} from '~/jira_connect/subscriptions/constants';
export default {
components: {
GlButton,
+ GlFormCheckbox,
GlLink,
},
- OAUTH_SELF_MANAGED_DOC_LINK,
+ data() {
+ return {
+ requiredSteps: [
+ {
+ name: s__('JiraConnect|Prerequisites'),
+ link: PREREQUISITES_DOC_LINK,
+ checked: false,
+ },
+ {
+ name: s__('JiraConnect|Set up OAuth authentication'),
+ link: OAUTH_SELF_MANAGED_DOC_LINK,
+ checked: false,
+ },
+ {
+ name: s__('JiraConnect|Set up your instance'),
+ link: SET_UP_INSTANCE_DOC_LINK,
+ checked: false,
+ },
+ ],
+ };
+ },
+ computed: {
+ nextDisabled() {
+ return !this.requiredSteps.every((step) => step.checked);
+ },
+ },
};
</script>
@@ -17,20 +48,25 @@ export default {
<p>
{{
s__(
- 'JiraConnect|In order to complete the set up, you’ll need to complete a few steps in GitLab.',
+ 'JiraConnect|In order to complete the set up, you’ll need to complete a few steps in GitLab:',
)
}}
- <gl-link
- class="gl-reset-font-size!"
- :href="$options.OAUTH_SELF_MANAGED_DOC_LINK"
- target="_blank"
- >{{ __('Learn more') }}</gl-link
- >
</p>
+ <div class="gl-mb-5">
+ <div v-for="step in requiredSteps" :key="step.name" class="gl-mb-2">
+ <gl-form-checkbox v-model="step.checked">
+ <gl-link :href="step.link" target="_blank">
+ {{ step.name }}
+ </gl-link>
+ </gl-form-checkbox>
+ </div>
+ </div>
<div class="gl-display-flex gl-justify-content-space-between">
<gl-button @click="$emit('back')">{{ __('Back') }}</gl-button>
- <gl-button variant="confirm" @click="$emit('next')">{{ __('Next') }}</gl-button>
+ <gl-button variant="confirm" :disabled="nextDisabled" @click="$emit('next')"
+ >{{ __('Next') }}
+ </gl-button>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/lib/graphql.js b/app/assets/javascripts/lib/graphql.js
index 6ab530576fc..5285fa363a5 100644
--- a/app/assets/javascripts/lib/graphql.js
+++ b/app/assets/javascripts/lib/graphql.js
@@ -1,5 +1,4 @@
import { ApolloClient, InMemoryCache, ApolloLink, HttpLink } from '@apollo/client/core';
-import { BatchHttpLink } from '@apollo/client/link/batch-http';
import { createUploadLink } from 'apollo-upload-client';
import { persistCache } from 'apollo3-cache-persist';
import ActionCableLink from '~/actioncable_link';
@@ -116,18 +115,14 @@ Object.defineProperty(window, 'pendingApolloRequests', {
function createApolloClient(resolvers = {}, config = {}) {
const {
baseUrl,
- batchMax = 10,
cacheConfig = { typePolicies: {}, possibleTypes: {} },
fetchPolicy = fetchPolicies.CACHE_FIRST,
typeDefs,
httpHeaders = {},
fetchCredentials = 'same-origin',
path = '/api/graphql',
- useGet = false,
} = config;
- const shouldUnbatch = gon.features?.unbatchGraphqlQueries;
-
let ac = null;
let uri = `${gon.relative_url_root || ''}${path}`;
@@ -146,7 +141,6 @@ function createApolloClient(resolvers = {}, config = {}) {
// We set to `same-origin` which is default value in modern browsers.
// See https://github.com/whatwg/fetch/pull/585 for more information.
credentials: fetchCredentials,
- batchMax,
};
/*
@@ -165,14 +159,10 @@ function createApolloClient(resolvers = {}, config = {}) {
return fetch(stripWhitespaceFromQuery(url, uri), options);
};
- const requestLink = ApolloLink.split(
- () => useGet || shouldUnbatch,
- new HttpLink({ ...httpOptions, fetch: fetchIntervention }),
- new BatchHttpLink(httpOptions),
- );
+ const requestLink = new HttpLink({ ...httpOptions, fetch: fetchIntervention });
const uploadsLink = ApolloLink.split(
- (operation) => operation.getContext().hasUpload || operation.getContext().isSingleRequest,
+ (operation) => operation.getContext().hasUpload,
createUploadLink(httpOptions),
);
diff --git a/app/assets/javascripts/lib/utils/color_utils.js b/app/assets/javascripts/lib/utils/color_utils.js
index a9f4257e28b..74c9f7de8c1 100644
--- a/app/assets/javascripts/lib/utils/color_utils.js
+++ b/app/assets/javascripts/lib/utils/color_utils.js
@@ -46,5 +46,5 @@ export function darkModeEnabled() {
if (isWebIde) {
return ideDarkThemes.includes(window.gon?.user_color_scheme);
}
- return document.body.classList.contains('gl-dark');
+ return document.documentElement.classList.contains('gl-dark');
}
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 7d16af003e4..27da2ac6ce1 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -7,6 +7,7 @@ import $ from 'jquery';
import { isFunction, defer, escape, partial, toLower } from 'lodash';
import Cookies from '~/lib/utils/cookies';
import { SCOPED_LABEL_DELIMITER } from '~/sidebar/components/labels/labels_select_widget/constants';
+import { DEFAULT_CI_CONFIG_PATH, CI_CONFIG_PATH_EXTENSION } from '~/lib/utils/constants';
import { convertToCamelCase, convertToSnakeCase } from './text_utility';
import { isObject } from './type_utility';
import { getLocationHash } from './url_utility';
@@ -737,3 +738,17 @@ export const isCurrentUser = (userId) => {
export const cloneWithoutReferences = (obj) => {
return JSON.parse(JSON.stringify(obj));
};
+
+/**
+ * Returns true if the given path is the default CI config path.
+ */
+export const isDefaultCiConfig = (path) => {
+ return path === DEFAULT_CI_CONFIG_PATH;
+};
+
+/**
+ * Returns true if the given path has the CI config path extension.
+ */
+export const hasCiConfigExtension = (path) => {
+ return CI_CONFIG_PATH_EXTENSION.test(path);
+};
diff --git a/app/assets/javascripts/lib/utils/constants.js b/app/assets/javascripts/lib/utils/constants.js
index da5fb831ae5..d9ac0abf7b3 100644
--- a/app/assets/javascripts/lib/utils/constants.js
+++ b/app/assets/javascripts/lib/utils/constants.js
@@ -23,3 +23,6 @@ export const BYTES_FORMAT_BYTES = 'B';
export const BYTES_FORMAT_KIB = 'KiB';
export const BYTES_FORMAT_MIB = 'MiB';
export const BYTES_FORMAT_GIB = 'GiB';
+
+export const DEFAULT_CI_CONFIG_PATH = '.gitlab-ci.yml';
+export const CI_CONFIG_PATH_EXTENSION = /(\.gitlab-ci\.yml)/;
diff --git a/app/assets/javascripts/lib/utils/datetime/timeago_utility.js b/app/assets/javascripts/lib/utils/datetime/timeago_utility.js
index a973cd890ba..89170ecc55d 100644
--- a/app/assets/javascripts/lib/utils/datetime/timeago_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime/timeago_utility.js
@@ -108,15 +108,27 @@ timeago.register(`${timeagoLanguageCode}-remaining`, memoizedLocaleRemaining());
timeago.register(`${timeagoLanguageCode}-duration`, memoizedLocaleDuration());
const setupAbsoluteFormatters = () => {
- const cache = {};
+ let cache = {};
// Intl.DateTimeFormat options (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat#using_options)
+ // For hourCycle please check https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/hourCycle
+ const hourCycle = [undefined, 'h12', 'h23'];
const formats = {
- [DATE_WITH_TIME_FORMAT]: () => ({ dateStyle: 'medium', timeStyle: 'short' }),
+ [DATE_WITH_TIME_FORMAT]: () => ({
+ dateStyle: 'medium',
+ timeStyle: 'short',
+ hourCycle: hourCycle[window.gon?.time_display_format || 0],
+ }),
[DATE_ONLY_FORMAT]: () => ({ dateStyle: 'medium' }),
};
return (formatName = DEFAULT_DATE_TIME_FORMAT) => {
+ if (cache.time_display_format !== window.gon?.time_display_format) {
+ cache = {
+ time_display_format: window.gon?.time_display_format,
+ };
+ }
+
if (cache[formatName]) {
return cache[formatName];
}
diff --git a/app/assets/javascripts/lib/utils/forms.js b/app/assets/javascripts/lib/utils/forms.js
index 652ae337506..6713a18cbf3 100644
--- a/app/assets/javascripts/lib/utils/forms.js
+++ b/app/assets/javascripts/lib/utils/forms.js
@@ -69,18 +69,32 @@ export const isIntegerGreaterThan = (value, greaterThan) =>
isParseableAsInteger(value) && parseInt(value, 10) > greaterThan;
/**
- * Regexp that matches email structure.
+ * Regexp that matches service desk setting email structure.
* Taken from app/models/service_desk_setting.rb custom_email
*/
-export const EMAIL_REGEXP = /^[\w\-._]+@[\w\-.]+\.[a-zA-Z]{2,}$/;
+const SERVICE_DESK_SETTING_EMAIL_REGEXP = /^[\w\-._]+@[\w\-.]+\.[a-zA-Z]{2,}$/;
/**
- * Checks if the input is a valid email address
+ * Checks if the input is a valid service desk setting email address
*
* @param {String} - value
* @returns {Boolean}
*/
-export const isEmail = (value) => EMAIL_REGEXP.test(value);
+export const isServiceDeskSettingEmail = (value) => SERVICE_DESK_SETTING_EMAIL_REGEXP.test(value);
+
+/**
+ * Regexp that matches user email structure.
+ * Taken from DeviseEmailValidator
+ */
+const USER_EMAIL_REGEXP = /^[^@\s]+@[^@\s]+$/;
+
+/**
+ * Checks if the input is a valid user email address
+ *
+ * @param {String} - value
+ * @returns {Boolean}
+ */
+export const isUserEmail = (value) => USER_EMAIL_REGEXP.test(value);
/**
* A form object serializer
diff --git a/app/assets/javascripts/lib/utils/keys.js b/app/assets/javascripts/lib/utils/keys.js
index 7cfcd11ece9..e5022551b97 100644
--- a/app/assets/javascripts/lib/utils/keys.js
+++ b/app/assets/javascripts/lib/utils/keys.js
@@ -1,5 +1,6 @@
export const ESC_KEY = 'Escape';
export const ENTER_KEY = 'Enter';
+export const NUMPAD_ENTER_KEY = 'NumpadEnter';
export const BACKSPACE_KEY = 'Backspace';
export const ARROW_DOWN_KEY = 'ArrowDown';
export const ARROW_UP_KEY = 'ArrowUp';
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 5bfdd174694..29189e3ac2f 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -29,7 +29,6 @@ import initBreadcrumbs from './breadcrumb';
import initPersistentUserCallouts from './persistent_user_callouts';
import { initUserTracking, initDefaultTrackers } from './tracking';
import { initSidebarTracking } from './pages/shared/nav/sidebar_tracking';
-import initServicePingConsent from './service_ping_consent';
import GlFieldErrors from './gl_field_errors';
import initUserPopovers from './user_popovers';
import initBroadcastNotifications from './broadcast_notification';
@@ -93,7 +92,6 @@ function deferredInitialisation() {
initBreadcrumbs();
initPrefetchLinks('.js-prefetch-document');
initLogoAnimation();
- initServicePingConsent();
initUserPopovers();
initBroadcastNotifications();
initPersistentUserCallouts();
diff --git a/app/assets/javascripts/members/components/avatars/group_avatar.vue b/app/assets/javascripts/members/components/avatars/group_avatar.vue
index 3b176bf2b43..83b5855492b 100644
--- a/app/assets/javascripts/members/components/avatars/group_avatar.vue
+++ b/app/assets/javascripts/members/components/avatars/group_avatar.vue
@@ -1,11 +1,18 @@
<script>
-import { GlAvatarLink, GlAvatarLabeled } from '@gitlab/ui';
+import { GlAvatarLink, GlAvatarLabeled, GlTooltipDirective } from '@gitlab/ui';
+import { __ } from '~/locale';
+import PrivateIcon from '../icons/private_icon.vue';
import { AVATAR_SIZE } from '../../constants';
export default {
name: 'GroupAvatar',
- avatarSize: AVATAR_SIZE,
- components: { GlAvatarLink, GlAvatarLabeled },
+ components: { GlAvatarLink, GlAvatarLabeled, PrivateIcon },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ i18n: {
+ private: __('Private'),
+ },
props: {
member: {
type: Object,
@@ -16,19 +23,36 @@ export default {
group() {
return this.member.sharedWithGroup;
},
+ isPrivate() {
+ return this.member.isSharedWithGroupPrivate;
+ },
+ avatarLabeledProps() {
+ const label = this.isPrivate ? this.$options.i18n.private : this.group.fullName;
+
+ return {
+ label,
+ src: this.group.avatarUrl,
+ alt: label,
+ size: AVATAR_SIZE,
+ entityName: this.isPrivate ? this.$options.i18n.private : this.group.name,
+ entityId: this.group.id,
+ };
+ },
},
};
</script>
<template>
- <gl-avatar-link :href="group.webUrl">
- <gl-avatar-labeled
- :label="group.fullName"
- :src="group.avatarUrl"
- :alt="group.fullName"
- :size="$options.avatarSize"
- :entity-name="group.name"
- :entity-id="group.id"
- />
+ <div v-if="isPrivate">
+ <gl-avatar-labeled v-bind="avatarLabeledProps">
+ <template #meta>
+ <div class="gl-p-1">
+ <private-icon />
+ </div>
+ </template>
+ </gl-avatar-labeled>
+ </div>
+ <gl-avatar-link v-else :href="group.webUrl">
+ <gl-avatar-labeled v-bind="avatarLabeledProps" />
</gl-avatar-link>
</template>
diff --git a/app/assets/javascripts/members/components/icons/private_icon.vue b/app/assets/javascripts/members/components/icons/private_icon.vue
new file mode 100644
index 00000000000..6168ea955f3
--- /dev/null
+++ b/app/assets/javascripts/members/components/icons/private_icon.vue
@@ -0,0 +1,19 @@
+<script>
+import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { s__ } from '~/locale';
+
+export default {
+ name: 'GroupAvatar',
+ components: { GlIcon },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ i18n: {
+ tooltip: s__('Members|Private group information is only accessible to its members.'),
+ },
+};
+</script>
+
+<template>
+ <gl-icon v-gl-tooltip="$options.i18n.tooltip" name="eye-slash" />
+</template>
diff --git a/app/assets/javascripts/members/components/table/member_source.vue b/app/assets/javascripts/members/components/table/member_source.vue
index ed1971d020b..f1a1c4cecaa 100644
--- a/app/assets/javascripts/members/components/table/member_source.vue
+++ b/app/assets/javascripts/members/components/table/member_source.vue
@@ -1,10 +1,12 @@
<script>
import { GlSprintf, GlTooltipDirective } from '@gitlab/ui';
import { s__, __ } from '~/locale';
+import PrivateIcon from '../icons/private_icon.vue';
export default {
name: 'MemberSource',
i18n: {
+ private: __('Private'),
inherited: __('Inherited'),
directMember: __('Direct member'),
directMemberWithCreatedBy: s__('Members|Direct member by %{createdBy}'),
@@ -13,16 +15,24 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
- components: { GlSprintf },
+ components: { GlSprintf, PrivateIcon },
props: {
memberSource: {
type: Object,
- required: true,
+ required: false,
+ default() {
+ return {};
+ },
},
isDirectMember: {
type: Boolean,
required: true,
},
+ isSharedWithGroupPrivate: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
createdBy: {
type: Object,
required: false,
@@ -43,7 +53,11 @@ export default {
</script>
<template>
- <span v-if="showCreatedBy">
+ <div v-if="isSharedWithGroupPrivate" class="gl-display-flex gl-column-gap-2">
+ <span>{{ $options.i18n.private }}</span>
+ <private-icon />
+ </div>
+ <span v-else-if="showCreatedBy">
<gl-sprintf :message="messageWithCreatedBy">
<template #group>
<a v-gl-tooltip.hover="$options.i18n.inherited" :href="memberSource.webUrl">{{
diff --git a/app/assets/javascripts/members/components/table/members_table.vue b/app/assets/javascripts/members/components/table/members_table.vue
index 68f624e9a3d..2b3294c1c79 100644
--- a/app/assets/javascripts/members/components/table/members_table.vue
+++ b/app/assets/javascripts/members/components/table/members_table.vue
@@ -270,6 +270,7 @@ export default {
:is-direct-member="isDirectMember"
:member-source="member.source"
:created-by="member.createdBy"
+ :is-shared-with-group-private="member.isSharedWithGroupPrivate"
/>
</members-table-cell>
</template>
diff --git a/app/assets/javascripts/members/components/table/role_dropdown.vue b/app/assets/javascripts/members/components/table/role_dropdown.vue
index 4b39c000b8f..2b72a3fe6e8 100644
--- a/app/assets/javascripts/members/components/table/role_dropdown.vue
+++ b/app/assets/javascripts/members/components/table/role_dropdown.vue
@@ -3,9 +3,10 @@ import { GlCollapsibleListbox } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
// eslint-disable-next-line no-restricted-imports
import { mapActions } from 'vuex';
-import * as Sentry from '@sentry/browser';
-import { s__ } from '~/locale';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { guestOverageConfirmAction } from 'ee_else_ce/members/guest_overage_confirm_action';
+import { roleDropdownItems, initialSelectedRole } from 'ee_else_ce/members/utils';
+import { s__ } from '~/locale';
export default {
name: 'RoleDropdown',
@@ -29,7 +30,7 @@ export default {
return {
isDesktop: false,
busy: false,
- selectedRoleValue: this.member.accessLevel.integerValue,
+ selectedRole: null,
};
},
computed: {
@@ -37,12 +38,12 @@ export default {
return this.permissions.canOverride && !this.member.isOverridden;
},
dropdownItems() {
- return Object.entries(this.member.validRoles).map(([name, value]) => ({
- value,
- text: name,
- }));
+ return roleDropdownItems(this.member);
},
},
+ created() {
+ this.selectedRole = initialSelectedRole(this.dropdownItems.flatten, this.member);
+ },
mounted() {
this.isDesktop = bp.isDesktop();
},
@@ -52,44 +53,39 @@ export default {
return dispatch(`${this.namespace}/updateMemberRole`, payload);
},
}),
- async handleOverageConfirm(currentRoleValue, newRoleValue, newRoleName) {
- return guestOverageConfirmAction({
- currentRoleValue,
- newRoleValue,
- newRoleName,
- group: this.group,
- memberId: this.member.id,
- memberType: this.namespace,
- });
- },
- async handleSelect(newRoleValue) {
- const currentRoleValue = this.member.accessLevel.integerValue;
- if (newRoleValue === currentRoleValue) {
- return;
- }
-
+ async handleSelect(value) {
this.busy = true;
- const { text: newRoleName } = this.dropdownItems.find((item) => item.value === newRoleValue);
- const confirmed = await this.handleOverageConfirm(
- currentRoleValue,
- newRoleValue,
- newRoleName,
- );
- if (!confirmed) {
- this.selectedRoleValue = currentRoleValue;
- this.busy = false;
- return;
- }
+ const newRole = this.dropdownItems.flatten.find((item) => item.value === value);
+ const previousRole = this.selectedRole;
try {
+ const confirmed = await guestOverageConfirmAction({
+ currentRoleValue: this.member.accessLevel.integerValue,
+ newRoleValue: newRole.accessLevel,
+ newRoleName: newRole.text,
+ newMemberRoleId: newRole.memberRoleId,
+ group: this.group,
+ memberId: this.member.id,
+ memberType: this.namespace,
+ });
+ if (!confirmed) {
+ return;
+ }
+
+ this.selectedRole = value;
+
await this.updateMemberRole({
memberId: this.member.id,
- accessLevel: { integerValue: newRoleValue, stringValue: newRoleName },
+ accessLevel: {
+ integerValue: newRole.accessLevel,
+ memberRoleId: newRole.memberRoleId,
+ },
});
this.$toast.show(s__('Members|Role updated successfully.'));
} catch (error) {
+ this.selectedRole = previousRole;
Sentry.captureException(error);
} finally {
this.busy = false;
@@ -101,14 +97,14 @@ export default {
<template>
<gl-collapsible-listbox
- v-model="selectedRoleValue"
:placement="isDesktop ? 'left' : 'right'"
:toggle-text="member.accessLevel.stringValue"
:header-text="__('Change role')"
:disabled="disabled"
:loading="busy"
data-qa-selector="access_level_dropdown"
- :items="dropdownItems"
+ :items="dropdownItems.formatted"
+ :selected="selectedRole"
@select="handleSelect"
>
<template #list-item="{ item }">
diff --git a/app/assets/javascripts/members/store/actions.js b/app/assets/javascripts/members/store/actions.js
index 712f0d6caa7..d696f618a3c 100644
--- a/app/assets/javascripts/members/store/actions.js
+++ b/app/assets/javascripts/members/store/actions.js
@@ -6,7 +6,10 @@ export const updateMemberRole = async ({ state, commit }, { memberId, accessLeve
try {
await axios.put(
state.memberPath.replace(/:id$/, memberId),
- state.requestFormatter({ accessLevel: accessLevel.integerValue }),
+ state.requestFormatter({
+ accessLevel: accessLevel.integerValue,
+ memberRoleId: accessLevel.memberRoleId,
+ }),
);
commit(types.RECEIVE_MEMBER_ROLE_SUCCESS, { memberId, accessLevel });
diff --git a/app/assets/javascripts/members/utils.js b/app/assets/javascripts/members/utils.js
index 09e4b5e8a6f..1304fb0fee1 100644
--- a/app/assets/javascripts/members/utils.js
+++ b/app/assets/javascripts/members/utils.js
@@ -1,4 +1,4 @@
-import { isUndefined } from 'lodash';
+import { isUndefined, uniqueId } from 'lodash';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { getParameterByName, setUrlParams } from '~/lib/utils/url_utility';
import {
@@ -35,6 +35,36 @@ export const generateBadges = ({ member, isCurrentUser, canManageMembers }) => [
},
];
+/**
+ * Creates the dropdowns options for static roles
+ *
+ * @param {object} member
+ * @param {Map<string, number>} member.validRoles
+ */
+export const roleDropdownItems = ({ validRoles }) => {
+ const staticRoleDropdownItems = Object.entries(validRoles).map(([name, value]) => ({
+ accessLevel: value,
+ memberRoleId: null, // The value `null` is need to downgrade from custom role to static role. See: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/133430#note_1595153555
+ text: name,
+ value: uniqueId('role-static-'),
+ }));
+
+ return { flatten: staticRoleDropdownItems, formatted: staticRoleDropdownItems };
+};
+
+/**
+ * Finds and returns unique value
+ *
+ * @param {Array<{accessLevel: number, memberRoleId: null, text: string, value: string}>} flattenDropdownItems
+ * @param {object} member
+ * @param {{integerValue: number}} member.accessLevel
+ */
+export const initialSelectedRole = (flattenDropdownItems, member) => {
+ return flattenDropdownItems.find(
+ ({ accessLevel }) => accessLevel === member.accessLevel.integerValue,
+ )?.value;
+};
+
export const isGroup = (member) => {
return Boolean(member.sharedWithGroup);
};
@@ -128,6 +158,7 @@ export const parseDataAttributes = (el) => {
export const baseRequestFormatter = (basePropertyName, accessLevelPropertyName) => ({
accessLevel,
+ memberRoleId,
...otherProperties
}) => {
const accessLevelProperty = !isUndefined(accessLevel)
@@ -137,6 +168,7 @@ export const baseRequestFormatter = (basePropertyName, accessLevelPropertyName)
return {
[basePropertyName]: {
...accessLevelProperty,
+ member_role_id: memberRoleId ?? null,
...otherProperties,
},
};
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 2095f24eb84..8ea995b8b4e 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -21,12 +21,7 @@ import syntaxHighlight from './syntax_highlight';
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(
- {},
- {
- useGet: true,
- },
- ),
+ defaultClient: createDefaultClient(),
});
// MergeRequestTabs
diff --git a/app/assets/javascripts/merge_requests/components/sticky_header.vue b/app/assets/javascripts/merge_requests/components/sticky_header.vue
index e8bdb854334..877e6142bae 100644
--- a/app/assets/javascripts/merge_requests/components/sticky_header.vue
+++ b/app/assets/javascripts/merge_requests/components/sticky_header.vue
@@ -1,5 +1,12 @@
<script>
-import { GlIntersectionObserver, GlLink, GlSprintf, GlBadge } from '@gitlab/ui';
+import {
+ GlIntersectionObserver,
+ GlLink,
+ GlSprintf,
+ GlBadge,
+ GlIcon,
+ GlTooltipDirective,
+} from '@gitlab/ui';
// eslint-disable-next-line no-restricted-imports
import { mapGetters, mapState } from 'vuex';
import SafeHtml from '~/vue_shared/directives/safe_html';
@@ -45,6 +52,7 @@ export default {
GlLink,
GlSprintf,
GlBadge,
+ GlIcon,
DiscussionCounter,
StatusBadge,
TodoWidget,
@@ -53,10 +61,12 @@ export default {
},
directives: {
SafeHtml,
+ GlTooltip: GlTooltipDirective,
},
mixins: [glFeatureFlagsMixin()],
inject: {
projectPath: { default: null },
+ sourceProjectPath: { default: null },
title: { default: '' },
tabs: { default: () => [] },
isFluidLayout: { default: false },
@@ -89,6 +99,16 @@ export default {
isNotificationsTodosButtons() {
return this.glFeatures.notificationsTodosButtons && this.glFeatures.movedMrSidebar;
},
+ isForked() {
+ return this.projectPath !== this.sourceProjectPath;
+ },
+ sourceBranch() {
+ if (this.isForked) {
+ return `${this.sourceProjectPath}:${this.getNoteableData.source_branch}`;
+ }
+
+ return this.getNoteableData.source_branch;
+ },
},
watch: {
discussionTabCounter(val) {
@@ -122,8 +142,8 @@ export default {
:class="{ 'gl-visibility-hidden': !isStickyHeaderVisible }"
>
<div
- class="issue-sticky-header-text gl-display-flex gl-flex-direction-column gl-align-items-center gl-mx-auto gl-px-5 gl-w-full"
- :class="{ 'gl-max-w-container-xl': !isFluidLayout }"
+ class="issue-sticky-header-text gl-display-flex gl-flex-direction-column gl-align-items-center gl-mx-auto gl-w-full"
+ :class="{ 'container-limited': !isFluidLayout }"
>
<div class="gl-w-full gl-display-flex gl-align-items-baseline">
<status-badge
@@ -153,8 +173,17 @@ export default {
:title="getNoteableData.source_branch"
:href="getNoteableData.source_branch_path"
class="gl-text-blue-500! gl-font-monospace gl-bg-blue-50 gl-rounded-base gl-font-sm gl-px-2 gl-text-truncate gl-max-w-26"
+ data-testid="source-branch"
>
- {{ getNoteableData.source_branch }}
+ <span
+ v-if="isForked"
+ v-gl-tooltip
+ class="gl-vertical-align-middle gl-mr-n2"
+ :title="__('The source project is a fork')"
+ >
+ <gl-icon name="fork" :size="12" class="gl-ml-1" />
+ </span>
+ {{ sourceBranch }}
</gl-link>
</template>
<template #target>
diff --git a/app/assets/javascripts/milestones/components/milestone_combobox.vue b/app/assets/javascripts/milestones/components/milestone_combobox.vue
index 2f7fb542d0e..a5e306b5372 100644
--- a/app/assets/javascripts/milestones/components/milestone_combobox.vue
+++ b/app/assets/javascripts/milestones/components/milestone_combobox.vue
@@ -1,19 +1,10 @@
<script>
-import {
- GlDropdown,
- GlDropdownDivider,
- GlDropdownSectionHeader,
- GlDropdownItem,
- GlLoadingIcon,
- GlSearchBoxByType,
- GlIcon,
-} from '@gitlab/ui';
+import { GlBadge, GlButton, GlCollapsibleListbox } from '@gitlab/ui';
import { debounce, isEqual } from 'lodash';
// eslint-disable-next-line no-restricted-imports
import { mapActions, mapGetters, mapState } from 'vuex';
import { s__, __, sprintf } from '~/locale';
import createStore from '../stores';
-import MilestoneResultsSection from './milestone_results_section.vue';
const SEARCH_DEBOUNCE_MS = 250;
@@ -21,14 +12,9 @@ export default {
name: 'MilestoneCombobox',
store: createStore(),
components: {
- GlDropdown,
- GlDropdownDivider,
- GlDropdownSectionHeader,
- GlDropdownItem,
- GlLoadingIcon,
- GlSearchBoxByType,
- GlIcon,
- MilestoneResultsSection,
+ GlCollapsibleListbox,
+ GlBadge,
+ GlButton,
},
props: {
value: {
@@ -56,27 +42,43 @@ export default {
required: false,
},
},
- data() {
- return {
- searchQuery: '',
- };
- },
translations: {
- milestone: s__('MilestoneCombobox|Milestone'),
selectMilestone: s__('MilestoneCombobox|Select milestone'),
noMilestone: s__('MilestoneCombobox|No milestone'),
- noResultsLabel: s__('MilestoneCombobox|No matching results'),
- searchMilestones: s__('MilestoneCombobox|Search Milestones'),
- searchErrorMessage: s__('MilestoneCombobox|An error occurred while searching for milestones'),
projectMilestones: s__('MilestoneCombobox|Project milestones'),
groupMilestones: s__('MilestoneCombobox|Group milestones'),
+ unselect: __('Unselect'),
},
computed: {
...mapState(['matches', 'selectedMilestones']),
- ...mapGetters(['isLoading', 'groupMilestonesEnabled']),
+ ...mapGetters(['isLoading']),
+ allMilestones() {
+ const { groupMilestones, projectMilestones } = this.matches || {};
+ const milestones = [];
+
+ if (projectMilestones?.totalCount) {
+ milestones.push({
+ id: 'project-milestones',
+ text: this.$options.translations.projectMilestones,
+ options: projectMilestones.list,
+ totalCount: projectMilestones.totalCount,
+ });
+ }
+
+ if (groupMilestones?.totalCount) {
+ milestones.push({
+ id: 'group-milestones',
+ text: this.$options.translations.groupMilestones,
+ options: groupMilestones.list,
+ totalCount: groupMilestones.totalCount,
+ });
+ }
+
+ return milestones;
+ },
selectedMilestonesLabel() {
const { selectedMilestones } = this;
- const firstMilestoneName = selectedMilestones[0];
+ const [firstMilestoneName] = selectedMilestones;
if (selectedMilestones.length === 0) {
return this.$options.translations.noMilestone;
@@ -92,20 +94,6 @@ export default {
numberOfOtherMilestones,
});
},
- showProjectMilestoneSection() {
- return Boolean(
- this.matches.projectMilestones.totalCount > 0 || this.matches.projectMilestones.error,
- );
- },
- showGroupMilestoneSection() {
- return (
- this.groupMilestonesEnabled &&
- Boolean(this.matches.groupMilestones.totalCount > 0 || this.matches.groupMilestones.error)
- );
- },
- showNoResults() {
- return !this.showProjectMilestoneSection && !this.showGroupMilestoneSection;
- },
},
watch: {
// Keep the Vuex store synchronized if the parent
@@ -127,8 +115,8 @@ export default {
// because we need to access the .cancel() method
// lodash attaches to the function, which is
// made inaccessible by Vue.
- this.debouncedSearch = debounce(function search() {
- this.search(this.searchQuery);
+ this.debouncedSearch = debounce(function search(q) {
+ this.search(q);
}, SEARCH_DEBOUNCE_MS);
this.setProjectId(this.projectId);
@@ -143,22 +131,14 @@ export default {
'setGroupMilestonesAvailable',
'setSelectedMilestones',
'clearSelectedMilestones',
- 'toggleMilestones',
'search',
'fetchMilestones',
]),
- focusSearchBox() {
- this.$refs.searchBox.$el.querySelector('input').focus();
- },
- onSearchBoxEnter() {
- this.debouncedSearch.cancel();
- this.search(this.searchQuery);
+ onSearchBoxInput(q) {
+ this.debouncedSearch(q);
},
- onSearchBoxInput() {
- this.debouncedSearch();
- },
- selectMilestone(milestone) {
- this.toggleMilestones(milestone);
+ selectMilestone(milestones) {
+ this.setSelectedMilestones(milestones);
this.$emit('input', this.selectedMilestones);
},
selectNoMilestone() {
@@ -170,84 +150,42 @@ export default {
</script>
<template>
- <gl-dropdown v-bind="$attrs" class="milestone-combobox" @shown="focusSearchBox">
- <template #button-content>
- <span data-testid="milestone-combobox-button-content" class="gl-flex-grow-1 text-muted">{{
- selectedMilestonesLabel
- }}</span>
- <gl-icon name="chevron-down" />
- </template>
-
- <gl-dropdown-section-header>
- <span class="text-center d-block">{{ $options.translations.selectMilestone }}</span>
- </gl-dropdown-section-header>
-
- <gl-dropdown-divider />
-
- <gl-search-box-by-type
- ref="searchBox"
- v-model.trim="searchQuery"
- class="gl-m-3"
- :placeholder="$options.translations.searchMilestones"
- @input="onSearchBoxInput"
- @keydown.enter.prevent="onSearchBoxEnter"
- />
-
- <gl-dropdown-item
- :is-checked="selectedMilestones.length === 0"
- is-check-item
- @click="selectNoMilestone()"
- >
- {{ $options.translations.noMilestone }}
- </gl-dropdown-item>
-
- <gl-dropdown-divider />
-
- <template v-if="isLoading">
- <gl-loading-icon size="sm" />
- <gl-dropdown-divider />
+ <gl-collapsible-listbox
+ :header-text="$options.translations.selectMilestone"
+ :items="allMilestones"
+ :reset-button-label="$options.translations.unselect"
+ :searching="isLoading"
+ :selected="selectedMilestones"
+ :toggle-text="selectedMilestonesLabel"
+ block
+ multiple
+ searchable
+ @reset="selectNoMilestone"
+ @search="onSearchBoxInput"
+ @select="selectMilestone"
+ >
+ <template #group-label="{ group }">
+ <span :data-testid="`${group.id}-section`"
+ >{{ group.text }}<gl-badge size="sm" class="gl-ml-2">{{ group.totalCount }}</gl-badge></span
+ >
</template>
- <template v-else-if="showNoResults">
- <div class="dropdown-item-space">
- <span data-testid="milestone-combobox-no-results" class="gl-pl-6">{{
- $options.translations.noResultsLabel
- }}</span>
+ <template #footer>
+ <div
+ class="gl-border-t-solid gl-border-t-1 gl-border-t-gray-200 gl-display-flex gl-flex-direction-column gl-p-2! gl-pt-0!"
+ >
+ <gl-button
+ v-for="(item, idx) in extraLinks"
+ :key="idx"
+ :href="item.url"
+ is-check-item
+ data-testid="milestone-combobox-extra-links"
+ category="tertiary"
+ block
+ class="gl-justify-content-start! gl-mt-2!"
+ >
+ {{ item.text }}
+ </gl-button>
</div>
- <gl-dropdown-divider />
- </template>
- <template v-else>
- <milestone-results-section
- v-if="showProjectMilestoneSection"
- :section-title="$options.translations.projectMilestones"
- :total-count="matches.projectMilestones.totalCount"
- :items="matches.projectMilestones.list"
- :selected-milestones="selectedMilestones"
- :error="matches.projectMilestones.error"
- :error-message="$options.translations.searchErrorMessage"
- data-testid="project-milestones-section"
- @selected="selectMilestone($event)"
- />
-
- <milestone-results-section
- v-if="showGroupMilestoneSection"
- :section-title="$options.translations.groupMilestones"
- :total-count="matches.groupMilestones.totalCount"
- :items="matches.groupMilestones.list"
- :selected-milestones="selectedMilestones"
- :error="matches.groupMilestones.error"
- :error-message="$options.translations.searchErrorMessage"
- data-testid="group-milestones-section"
- @selected="selectMilestone($event)"
- />
</template>
- <gl-dropdown-item
- v-for="(item, idx) in extraLinks"
- :key="idx"
- :href="item.url"
- is-check-item
- data-testid="milestone-combobox-extra-links"
- >
- {{ item.text }}
- </gl-dropdown-item>
- </gl-dropdown>
+ </gl-collapsible-listbox>
</template>
diff --git a/app/assets/javascripts/milestones/stores/mutations.js b/app/assets/javascripts/milestones/stores/mutations.js
index 1f88c0a1ea6..c0cd58cc5d2 100644
--- a/app/assets/javascripts/milestones/stores/mutations.js
+++ b/app/assets/javascripts/milestones/stores/mutations.js
@@ -37,7 +37,7 @@ export default {
},
[types.RECEIVE_PROJECT_MILESTONES_SUCCESS](state, response) {
state.matches.projectMilestones = {
- list: response.data.map(({ title }) => ({ title })),
+ list: response.data.map(({ title }) => ({ text: title, value: title })),
totalCount: parseInt(response.headers['x-total'], 10) || response.data.length,
error: null,
};
@@ -51,7 +51,7 @@ export default {
},
[types.RECEIVE_GROUP_MILESTONES_SUCCESS](state, response) {
state.matches.groupMilestones = {
- list: response.data.map(({ title }) => ({ title })),
+ list: response.data.map(({ title }) => ({ text: title, value: title })),
totalCount: parseInt(response.headers['x-total'], 10) || response.data.length,
error: null,
};
diff --git a/app/assets/javascripts/ml/experiment_tracking/routes/experiments/index/constants.js b/app/assets/javascripts/ml/experiment_tracking/routes/experiments/index/constants.js
index 3026bce0972..c94e7648d1d 100644
--- a/app/assets/javascripts/ml/experiment_tracking/routes/experiments/index/constants.js
+++ b/app/assets/javascripts/ml/experiment_tracking/routes/experiments/index/constants.js
@@ -2,9 +2,9 @@ import { s__ } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
export const CREATE_EXPERIMENT_HELP_PATH = helpPagePath(
- 'user/project/ml/experiment_tracking/index.md',
+ 'user/project/ml/experiment_tracking/index',
{
- anchor: 'tracking-new-experiments-and-trials',
+ anchor: 'track-new-experiments-and-candidates',
},
);
diff --git a/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/constants.js b/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/constants.js
index 4d34555ac2f..346c2453715 100644
--- a/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/constants.js
+++ b/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/constants.js
@@ -1,5 +1,4 @@
import { s__ } from '~/locale';
-import { helpPagePath } from '~/helpers/help_page_helper';
export const METRIC_KEY_PREFIX = 'metric.';
export const LIST_KEY_CREATED_AT = 'created_at';
@@ -13,9 +12,3 @@ export const BASE_SORT_FIELDS = Object.freeze([
label: s__('MlExperimentTracking|Created at'),
},
]);
-export const CREATE_CANDIDATE_HELP_PATH = helpPagePath(
- 'user/project/ml/experiment_tracking/index.md',
- {
- anchor: 'tracking-new-experiments-and-trials',
- },
-);
diff --git a/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/ml_experiments_show.vue b/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/ml_experiments_show.vue
index 28a27059b17..afd48df93e4 100644
--- a/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/ml_experiments_show.vue
+++ b/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/ml_experiments_show.vue
@@ -10,12 +10,8 @@ import KeysetPagination from '~/vue_shared/components/incubation/pagination.vue'
import ModelExperimentsHeader from '~/ml/experiment_tracking/components/model_experiments_header.vue';
import DeleteButton from '~/ml/experiment_tracking/components/delete_button.vue';
-import {
- LIST_KEY_CREATED_AT,
- BASE_SORT_FIELDS,
- METRIC_KEY_PREFIX,
- CREATE_CANDIDATE_HELP_PATH,
-} from './constants';
+import { CREATE_EXPERIMENT_HELP_PATH as CREATE_CANDIDATE_HELP_PATH } from '../index/constants';
+import { LIST_KEY_CREATED_AT, BASE_SORT_FIELDS, METRIC_KEY_PREFIX } from './constants';
import * as translations from './translations';
export default {
diff --git a/app/assets/javascripts/ml/model_registry/apps/index.js b/app/assets/javascripts/ml/model_registry/apps/index.js
index f9e5f82e708..92d159f68be 100644
--- a/app/assets/javascripts/ml/model_registry/apps/index.js
+++ b/app/assets/javascripts/ml/model_registry/apps/index.js
@@ -1,3 +1,5 @@
import ShowMlModel from './show_ml_model.vue';
+import ShowMlModelVersion from './show_ml_model_version.vue';
+import IndexMlModels from './index_ml_models.vue';
-export { ShowMlModel };
+export { ShowMlModel, ShowMlModelVersion, IndexMlModels };
diff --git a/app/assets/javascripts/ml/model_registry/apps/index_ml_models.vue b/app/assets/javascripts/ml/model_registry/apps/index_ml_models.vue
new file mode 100644
index 00000000000..5a55d5669a8
--- /dev/null
+++ b/app/assets/javascripts/ml/model_registry/apps/index_ml_models.vue
@@ -0,0 +1,61 @@
+<script>
+import { isEmpty } from 'lodash';
+import Pagination from '~/vue_shared/components/incubation/pagination.vue';
+import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
+import TitleArea from '~/vue_shared/components/registry/title_area.vue';
+import * as i18n from '../translations';
+import { BASE_SORT_FIELDS } from '../constants';
+import SearchBar from '../components/search_bar.vue';
+import ModelRow from '../components/model_row.vue';
+
+export default {
+ name: 'IndexMlModels',
+ components: {
+ Pagination,
+ ModelRow,
+ SearchBar,
+ MetadataItem,
+ TitleArea,
+ },
+ props: {
+ models: {
+ type: Array,
+ required: true,
+ },
+ pageInfo: {
+ type: Object,
+ required: true,
+ },
+ modelCount: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
+ },
+ computed: {
+ hasModels() {
+ return !isEmpty(this.models);
+ },
+ },
+ i18n,
+ sortableFields: BASE_SORT_FIELDS,
+};
+</script>
+
+<template>
+ <div>
+ <title-area :title="$options.i18n.TITLE_LABEL">
+ <template #metadata-models-count>
+ <metadata-item icon="machine-learning" :text="$options.i18n.modelsCountLabel(modelCount)" />
+ </template>
+ </title-area>
+
+ <template v-if="hasModels">
+ <search-bar :sortable-fields="$options.sortableFields" />
+ <model-row v-for="model in models" :key="model.name" :model="model" />
+ <pagination v-bind="pageInfo" />
+ </template>
+
+ <p v-else class="gl-text-secondary">{{ $options.i18n.NO_MODELS_LABEL }}</p>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ml/model_registry/apps/show_ml_model.vue b/app/assets/javascripts/ml/model_registry/apps/show_ml_model.vue
index d4f17c840d7..e8ec8f157ef 100644
--- a/app/assets/javascripts/ml/model_registry/apps/show_ml_model.vue
+++ b/app/assets/javascripts/ml/model_registry/apps/show_ml_model.vue
@@ -1,16 +1,71 @@
<script>
+import { GlTab, GlTabs, GlBadge } from '@gitlab/ui';
+import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
+import TitleArea from '~/vue_shared/components/registry/title_area.vue';
+import * as i18n from '../translations';
+
export default {
name: 'ShowMlModelApp',
- components: {},
+ components: {
+ TitleArea,
+ GlTabs,
+ GlTab,
+ GlBadge,
+ MetadataItem,
+ },
props: {
model: {
type: Object,
required: true,
},
},
+ computed: {
+ versionCount() {
+ return this.model.versionCount || 0;
+ },
+ candidateCount() {
+ return this.model.candidateCount || 0;
+ },
+ },
+ i18n,
};
</script>
<template>
- <div>{{ model.name }}</div>
+ <div>
+ <title-area :title="model.name">
+ <template #metadata-versions-count>
+ <metadata-item
+ icon="machine-learning"
+ :text="$options.i18n.versionsCountLabel(model.versionCount)"
+ />
+ </template>
+
+ <template #sub-header>
+ {{ model.description }}
+ </template>
+ </title-area>
+
+ <gl-tabs class="gl-mt-4">
+ <gl-tab :title="$options.i18n.MODEL_DETAILS_TAB_LABEL">
+ <h3 class="gl-font-lg">{{ $options.i18n.LATEST_VERSION_LABEL }}</h3>
+ <template v-if="model.latestVersion">
+ {{ model.latestVersion.version }}
+ </template>
+ <div v-else class="gl-text-secondary">{{ $options.i18n.NO_VERSIONS_LABEL }}</div>
+ </gl-tab>
+ <gl-tab>
+ <template #title>
+ {{ $options.i18n.MODEL_OTHER_VERSIONS_TAB_LABEL }}
+ <gl-badge size="sm" class="gl-tab-counter-badge">{{ versionCount }}</gl-badge>
+ </template>
+ </gl-tab>
+ <gl-tab>
+ <template #title>
+ {{ $options.i18n.MODEL_CANDIDATES_TAB_LABEL }}
+ <gl-badge size="sm" class="gl-tab-counter-badge">{{ candidateCount }}</gl-badge>
+ </template>
+ </gl-tab>
+ </gl-tabs>
+ </div>
</template>
diff --git a/app/assets/javascripts/ml/model_registry/apps/show_ml_model_version.vue b/app/assets/javascripts/ml/model_registry/apps/show_ml_model_version.vue
new file mode 100644
index 00000000000..a9440aff1ce
--- /dev/null
+++ b/app/assets/javascripts/ml/model_registry/apps/show_ml_model_version.vue
@@ -0,0 +1,16 @@
+<script>
+export default {
+ name: 'ShowMlModelVersionApp',
+ components: {},
+ props: {
+ modelVersion: {
+ type: Object,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div>{{ modelVersion.model.name }} - {{ modelVersion.version }}</div>
+</template>
diff --git a/app/assets/javascripts/ml/model_registry/components/model_row.vue b/app/assets/javascripts/ml/model_registry/components/model_row.vue
new file mode 100644
index 00000000000..ffae7e83099
--- /dev/null
+++ b/app/assets/javascripts/ml/model_registry/components/model_row.vue
@@ -0,0 +1,45 @@
+<script>
+import { GlLink } from '@gitlab/ui';
+import { s__, n__ } from '~/locale';
+
+export default {
+ name: 'MlModelRow',
+ components: {
+ GlLink,
+ },
+ props: {
+ model: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ hasVersions() {
+ return this.model.version != null;
+ },
+ modelVersionCountMessage() {
+ if (!this.model.versionCount) return s__('MlModelRegistry|No registered versions');
+
+ return n__(
+ 'MlModelRegistry|· No other versions',
+ 'MlModelRegistry|· %d versions',
+ this.model.versionCount,
+ );
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-border-b-solid gl-border-b-1 gl-border-b-gray-100 gl-py-3">
+ <gl-link :href="model.path" class="gl-text-body gl-font-weight-bold gl-line-height-24">
+ {{ model.name }}
+ </gl-link>
+
+ <div class="gl-text-secondary">
+ <gl-link v-if="hasVersions" :href="model.versionPath">{{ model.version }}</gl-link>
+
+ {{ modelVersionCountMessage }}
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ml/model_registry/components/search_bar.vue b/app/assets/javascripts/ml/model_registry/components/search_bar.vue
new file mode 100644
index 00000000000..2bcdabc403f
--- /dev/null
+++ b/app/assets/javascripts/ml/model_registry/components/search_bar.vue
@@ -0,0 +1,71 @@
+<script>
+import { queryToObject, setUrlParams, visitUrl } from '~/lib/utils/url_utility';
+import { LIST_KEY_CREATED_AT } from '~/ml/experiment_tracking/routes/experiments/show/constants';
+import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
+import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
+
+export default {
+ name: 'SearchBar',
+ components: {
+ RegistrySearch,
+ },
+ props: {
+ sortableFields: {
+ type: Array,
+ required: true,
+ },
+ },
+ data() {
+ const query = queryToObject(window.location.search);
+
+ const filter = query.name ? [{ value: { data: query.name }, type: FILTERED_SEARCH_TERM }] : [];
+
+ const orderBy = query.orderBy || LIST_KEY_CREATED_AT;
+
+ return {
+ filters: filter,
+ sorting: {
+ orderBy,
+ sort: (query.sort || 'desc').toLowerCase(),
+ },
+ };
+ },
+ methods: {
+ submitFilters() {
+ return visitUrl(setUrlParams(this.parsedQuery()));
+ },
+ parsedQuery() {
+ const name = this.filters
+ .map((f) => f.value.data)
+ .join(' ')
+ .trim();
+
+ const filterByQuery = name === '' ? {} : { name };
+
+ return { ...filterByQuery, ...this.sorting };
+ },
+ updateFilters(newValue) {
+ this.filters = newValue;
+ },
+ updateSorting(newValue) {
+ this.sorting = { ...this.sorting, ...newValue };
+ },
+ updateSortingAndEmitUpdate(newValue) {
+ this.updateSorting(newValue);
+ this.submitFilters();
+ },
+ },
+};
+</script>
+
+<template>
+ <registry-search
+ :filters="filters"
+ :sorting="sorting"
+ :sortable-fields="sortableFields"
+ @sorting:changed="updateSortingAndEmitUpdate"
+ @filter:changed="updateFilters"
+ @filter:submit="submitFilters"
+ @filter:clear="filters = []"
+ />
+</template>
diff --git a/app/assets/javascripts/ml/model_registry/constants.js b/app/assets/javascripts/ml/model_registry/constants.js
new file mode 100644
index 00000000000..10c21ec4f12
--- /dev/null
+++ b/app/assets/javascripts/ml/model_registry/constants.js
@@ -0,0 +1,13 @@
+import { s__ } from '~/locale';
+
+export const LIST_KEY_CREATED_AT = 'created_at';
+export const BASE_SORT_FIELDS = Object.freeze([
+ {
+ orderBy: 'name',
+ label: s__('MlExperimentTracking|Name'),
+ },
+ {
+ orderBy: LIST_KEY_CREATED_AT,
+ label: s__('MlExperimentTracking|Created at'),
+ },
+]);
diff --git a/app/assets/javascripts/ml/model_registry/routes/models/index/components/ml_models_index.vue b/app/assets/javascripts/ml/model_registry/routes/models/index/components/ml_models_index.vue
deleted file mode 100644
index 3770b4ec3ac..00000000000
--- a/app/assets/javascripts/ml/model_registry/routes/models/index/components/ml_models_index.vue
+++ /dev/null
@@ -1,49 +0,0 @@
-<script>
-import { isEmpty } from 'lodash';
-import * as translations from '~/ml/model_registry/routes/models/index/translations';
-import Pagination from '~/vue_shared/components/incubation/pagination.vue';
-import ModelRow from './model_row.vue';
-
-export default {
- name: 'MlModelRegistryApp',
- components: {
- Pagination,
- ModelRow,
- },
- props: {
- models: {
- type: Array,
- required: true,
- },
- pageInfo: {
- type: Object,
- required: true,
- },
- },
- computed: {
- hasModels() {
- return !isEmpty(this.models);
- },
- },
- i18n: translations,
-};
-</script>
-
-<template>
- <div>
- <div class="detail-page-header gl-flex-wrap">
- <div class="detail-page-header-body">
- <div class="page-title gl-flex-grow-1 gl-display-flex gl-align-items-center">
- <h2 class="gl-font-size-h-display gl-my-0">{{ $options.i18n.TITLE_LABEL }}</h2>
- </div>
- </div>
- </div>
-
- <template v-if="hasModels">
- <model-row v-for="model in models" :key="model.name" :model="model" />
- <pagination v-bind="pageInfo" />
- </template>
-
- <p v-else class="gl-text-secondary">{{ $options.i18n.NO_MODELS_LABEL }}</p>
- </div>
-</template>
diff --git a/app/assets/javascripts/ml/model_registry/routes/models/index/components/model_row.vue b/app/assets/javascripts/ml/model_registry/routes/models/index/components/model_row.vue
deleted file mode 100644
index 4f91f0939a8..00000000000
--- a/app/assets/javascripts/ml/model_registry/routes/models/index/components/model_row.vue
+++ /dev/null
@@ -1,35 +0,0 @@
-<script>
-import { GlLink } from '@gitlab/ui';
-import { modelVersionCountMessage } from '../translations';
-
-export default {
- name: 'MlModelRow',
- components: {
- GlLink,
- },
- props: {
- model: {
- type: Object,
- required: true,
- },
- },
- computed: {
- hasVersions() {
- return this.model.version != null;
- },
- },
- modelVersionCountMessage,
-};
-</script>
-
-<template>
- <div class="gl-border-b-solid gl-border-b-1 gl-border-b-gray-100 gl-py-3">
- <gl-link :href="model.path" class="gl-text-body gl-font-weight-bold gl-line-height-24">
- {{ model.name }}
- </gl-link>
-
- <div class="gl-text-secondary">
- {{ $options.modelVersionCountMessage(model.version, model.versionCount) }}
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/ml/model_registry/routes/models/index/index.js b/app/assets/javascripts/ml/model_registry/routes/models/index/index.js
deleted file mode 100644
index d303d9716af..00000000000
--- a/app/assets/javascripts/ml/model_registry/routes/models/index/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import MlModelsIndex from './components/ml_models_index.vue';
-
-export default MlModelsIndex;
diff --git a/app/assets/javascripts/ml/model_registry/routes/models/index/translations.js b/app/assets/javascripts/ml/model_registry/routes/models/index/translations.js
deleted file mode 100644
index 9210d816373..00000000000
--- a/app/assets/javascripts/ml/model_registry/routes/models/index/translations.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import { s__, n__, sprintf } from '~/locale';
-
-export const TITLE_LABEL = s__('MlModelRegistry|Model registry');
-export const NO_MODELS_LABEL = s__('MlModelRegistry|No models registered in this project');
-
-export const modelVersionCountMessage = (version, versionCount) => {
- if (!versionCount) return s__('MlModelRegistry|No registered versions');
-
- const message = n__(
- 'MlModelRegistry|%{version} · No other versions',
- 'MlModelRegistry|%{version} · %{versionCount} versions',
- versionCount,
- );
-
- return sprintf(message, { version, versionCount });
-};
diff --git a/app/assets/javascripts/ml/model_registry/translations.js b/app/assets/javascripts/ml/model_registry/translations.js
new file mode 100644
index 00000000000..89b3f45ed94
--- /dev/null
+++ b/app/assets/javascripts/ml/model_registry/translations.js
@@ -0,0 +1,16 @@
+import { s__, n__ } from '~/locale';
+
+export const MODEL_DETAILS_TAB_LABEL = s__('MlModelRegistry|Details');
+export const MODEL_OTHER_VERSIONS_TAB_LABEL = s__('MlModelRegistry|Versions');
+export const MODEL_CANDIDATES_TAB_LABEL = s__('MlModelRegistry|Version candidates');
+export const LATEST_VERSION_LABEL = s__('MlModelRegistry|Latest version');
+export const NO_VERSIONS_LABEL = s__('MlModelRegistry|This model has no versions');
+
+export const versionsCountLabel = (versionCount) =>
+ n__('MlModelRegistry|%d version', 'MlModelRegistry|%d versions', versionCount);
+
+export const TITLE_LABEL = s__('MlModelRegistry|Model registry');
+export const NO_MODELS_LABEL = s__('MlModelRegistry|No models registered in this project');
+
+export const modelsCountLabel = (modelCount) =>
+ n__('MlModelRegistry|%d model', 'MlModelRegistry|%d models', modelCount);
diff --git a/app/assets/javascripts/mr_notes/init.js b/app/assets/javascripts/mr_notes/init.js
index 28f294589ae..5594e71641b 100644
--- a/app/assets/javascripts/mr_notes/init.js
+++ b/app/assets/javascripts/mr_notes/init.js
@@ -40,6 +40,7 @@ function setupMrNotesState(store, notesDataset, diffsDataset) {
mrReviews: getReviewsForMergeRequest(mrPath),
diffViewType:
getParameterValues('view')[0] || getCookie(DIFF_VIEW_COOKIE_NAME) || INLINE_DIFF_VIEW_TYPE,
+ perPage: Number(diffsDataset.perPage),
});
}
diff --git a/app/assets/javascripts/notes/components/comment_field_layout.vue b/app/assets/javascripts/notes/components/comment_field_layout.vue
index cefcc1b0c98..7673bd61631 100644
--- a/app/assets/javascripts/notes/components/comment_field_layout.vue
+++ b/app/assets/javascripts/notes/components/comment_field_layout.vue
@@ -1,5 +1,4 @@
<script>
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import NoteableWarning from '~/vue_shared/components/notes/noteable_warning.vue';
import EmailParticipantsWarning from './email_participants_warning.vue';
import AttachmentsWarning from './attachments_warning.vue';
@@ -12,7 +11,6 @@ export default {
EmailParticipantsWarning,
NoteableWarning,
},
- mixins: [glFeatureFlagsMixin()],
props: {
noteableData: {
type: Object,
@@ -56,11 +54,7 @@ export default {
return this.emailParticipants.length && !this.isInternalNote;
},
showAttachmentWarning() {
- return (
- this.glFeatures.serviceDeskNewNoteEmailNativeAttachments &&
- this.showEmailParticipantsWarning &&
- this.containsLink
- );
+ return this.showEmailParticipantsWarning && this.containsLink;
},
},
};
diff --git a/app/assets/javascripts/notes/components/discussion_locked_widget.vue b/app/assets/javascripts/notes/components/discussion_locked_widget.vue
index bcf9b4cf893..a999b633f64 100644
--- a/app/assets/javascripts/notes/components/discussion_locked_widget.vue
+++ b/app/assets/javascripts/notes/components/discussion_locked_widget.vue
@@ -24,7 +24,9 @@ export default {
},
lockedIssueWarning() {
return sprintf(
- __('This %{issuableDisplayName} is locked. Only project members can comment.'),
+ __(
+ 'The discussion in this %{issuableDisplayName} is locked. Only project members can comment.',
+ ),
{ issuableDisplayName: this.issuableDisplayName },
);
},
diff --git a/app/assets/javascripts/notes/components/discussion_resolve_button.vue b/app/assets/javascripts/notes/components/discussion_resolve_button.vue
index b1aee19d5b2..cc4f360a694 100644
--- a/app/assets/javascripts/notes/components/discussion_resolve_button.vue
+++ b/app/assets/javascripts/notes/components/discussion_resolve_button.vue
@@ -21,7 +21,11 @@ export default {
</script>
<template>
- <gl-button :loading="isResolving" class="gl-xs-w-full ml-sm-2" @click="$emit('onClick')">
+ <gl-button
+ :loading="isResolving"
+ class="gl-w-full gl-sm-w-auto ml-sm-2"
+ @click="$emit('onClick')"
+ >
{{ buttonTitle }}
</gl-button>
</template>
diff --git a/app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue b/app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue
index 4ccba011014..34cbba8ce43 100644
--- a/app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue
+++ b/app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue
@@ -29,7 +29,7 @@ export default {
:href="url"
:title="$options.i18n.buttonLabel"
:aria-label="$options.i18n.buttonLabel"
- class="new-issue-for-discussion discussion-create-issue-btn gl-xs-w-full"
+ class="new-issue-for-discussion discussion-create-issue-btn gl-w-full gl-sm-w-auto"
icon="issue-new"
/>
</div>
diff --git a/app/assets/javascripts/notes/components/note_signed_out_widget.vue b/app/assets/javascripts/notes/components/note_signed_out_widget.vue
index 30d3bfcb989..738af4f6064 100644
--- a/app/assets/javascripts/notes/components/note_signed_out_widget.vue
+++ b/app/assets/javascripts/notes/components/note_signed_out_widget.vue
@@ -35,5 +35,5 @@ export default {
</script>
<template>
- <div v-safe-html="signedOutText" class="disabled-comment text-center"></div>
+ <div v-safe-html="signedOutText" class="disabled-comment gl-text-center gl-text-secondary"></div>
</template>
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index e0b1f7a8c6a..493beb8cea9 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -290,9 +290,6 @@ export default {
parent: this.$el,
});
},
- deleteNoteHandler(note) {
- this.$emit('noteDeleted', this.discussion, note);
- },
onStartReplying(discussionId) {
if (this.discussion.id === discussionId) {
this.showReplyForm();
@@ -329,7 +326,6 @@ export default {
:is-overview-tab="isOverviewTab"
:should-scroll-to-note="shouldScrollToNote"
@startReplying="showReplyForm"
- @deleteNote="deleteNoteHandler"
>
<template #avatar-badge>
<slot name="avatar-badge"></slot>
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index 809b1716b91..c817655b649 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -105,6 +105,11 @@ export default {
required: false,
default: true,
},
+ discussion: {
+ type: Object,
+ required: false,
+ default: null,
+ },
},
data() {
return {
diff --git a/app/assets/javascripts/notes/components/notes_activity_header.vue b/app/assets/javascripts/notes/components/notes_activity_header.vue
index ce642733396..b4eeea8db02 100644
--- a/app/assets/javascripts/notes/components/notes_activity_header.vue
+++ b/app/assets/javascripts/notes/components/notes_activity_header.vue
@@ -40,7 +40,7 @@ export default {
showAiActions() {
return (
this.resourceGlobalId &&
- this.glFeatures.openaiExperimentation &&
+ (this.glFeatures.openaiExperimentation || this.glFeatures.aiGlobalSwitch) &&
this.glFeatures.summarizeNotes
);
},
diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js
index 966f4184780..a995b9fa214 100644
--- a/app/assets/javascripts/notes/stores/mutations.js
+++ b/app/assets/javascripts/notes/stores/mutations.js
@@ -318,11 +318,6 @@ export default {
const note = noteData;
const selectedDiscussion = state.discussions.find((disc) => disc.id === note.id);
note.expanded = true; // override expand flag to prevent collapse
- if (note.diff_file) {
- Object.assign(note, {
- file_hash: note.diff_file.file_hash,
- });
- }
Object.assign(selectedDiscussion, { ...note });
},
diff --git a/app/assets/javascripts/observability/client.js b/app/assets/javascripts/observability/client.js
index 2e976cd6230..32ff7fff128 100644
--- a/app/assets/javascripts/observability/client.js
+++ b/app/assets/javascripts/observability/client.js
@@ -1,12 +1,15 @@
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import axios from '~/lib/utils/axios_utils';
+import { logError } from '~/lib/logger';
+import { DEFAULT_SORTING_OPTION, SORTING_OPTIONS } from './constants';
function reportErrorAndThrow(e) {
+ logError(e);
Sentry.captureException(e);
throw e;
}
// Provisioning API spec: https://gitlab.com/gitlab-org/opstrace/opstrace/-/blob/main/provisioning-api/pkg/provisioningapi/routes.go#L59
-async function enableTraces(provisioningUrl) {
+async function enableObservability(provisioningUrl) {
try {
// Note: axios.put(url, undefined, {withCredentials: true}) does not send cookies properly, so need to use the API below for the correct behaviour
return await axios(provisioningUrl, {
@@ -19,7 +22,7 @@ async function enableTraces(provisioningUrl) {
}
// Provisioning API spec: https://gitlab.com/gitlab-org/opstrace/opstrace/-/blob/main/provisioning-api/pkg/provisioningapi/routes.go#L37
-async function isTracingEnabled(provisioningUrl) {
+async function isObservabilityEnabled(provisioningUrl) {
try {
const { data } = await axios.get(provisioningUrl, { withCredentials: true });
if (data && data.status) {
@@ -42,18 +45,11 @@ async function fetchTrace(tracingUrl, traceId) {
throw new Error('traceId is required.');
}
- const { data } = await axios.get(tracingUrl, {
+ const { data } = await axios.get(`${tracingUrl}/${traceId}`, {
withCredentials: true,
- params: {
- trace_id: traceId,
- },
});
- if (!Array.isArray(data.traces) || data.traces.length === 0) {
- throw new Error('traces are missing/invalid in the response'); // eslint-disable-line @gitlab/require-i18n-strings
- }
-
- return data.traces[0];
+ return data;
} catch (e) {
return reportErrorAndThrow(e);
}
@@ -65,9 +61,10 @@ async function fetchTrace(tracingUrl, traceId) {
const SUPPORTED_FILTERS = {
durationMs: ['>', '<'],
operation: ['=', '!='],
- serviceName: ['=', '!='],
+ service: ['=', '!='],
period: ['='],
traceId: ['=', '!='],
+ attribute: ['='],
// free-text 'search' temporarily ignored https://gitlab.com/gitlab-org/opstrace/opstrace/-/issues/2309
};
@@ -77,9 +74,10 @@ const SUPPORTED_FILTERS = {
const FILTER_TO_QUERY_PARAM = {
durationMs: 'duration_nano',
operation: 'operation',
- serviceName: 'service_name',
+ service: 'service_name',
period: 'period',
traceId: 'trace_id',
+ attribute: 'attribute',
};
const FILTER_OPERATORS_PREFIX = {
@@ -112,13 +110,32 @@ function getFilterParamName(filterName, operator) {
}
/**
+ * Process `filterValue` and append the proper query params to the `searchParams` arg
+ *
+ * It mutates `searchParams`
+ *
+ * @param {String} filterValue The filter value, in the format `attribute_name=attribute_value`
+ * @param {String} filterOperator The filter operator
+ * @param {URLSearchParams} searchParams The URLSearchParams object where to append the proper query params
+ */
+function handleAttributeFilter(filterValue, filterOperator, searchParams) {
+ const [attrName, attrValue] = filterValue.split('=');
+ if (attrName && attrValue) {
+ if (filterOperator === '=') {
+ searchParams.append('attr_name', attrName);
+ searchParams.append('attr_value', attrValue);
+ }
+ }
+}
+
+/**
* Builds URLSearchParams from a filter object of type { [filterName]: undefined | null | Array<{operator: String, value: any} }
* e.g:
*
* filterObj = {
* durationMs: [{operator: '>', value: '100'}, {operator: '<', value: '1000' }],
* operation: [{operator: '=', value: 'someOp' }],
- * serviceName: [{operator: '!=', value: 'foo' }]
+ * service: [{operator: '!=', value: 'foo' }]
* }
*
* It handles converting the filter to the proper supported query params
@@ -131,20 +148,22 @@ function filterObjToQueryParams(filterObj) {
Object.keys(SUPPORTED_FILTERS).forEach((filterName) => {
const filterValues = filterObj[filterName] || [];
- const supportedFilters = filterValues.filter((f) =>
+ const validFilters = filterValues.filter((f) =>
SUPPORTED_FILTERS[filterName].includes(f.operator),
);
- supportedFilters.forEach(({ operator, value: rawValue }) => {
- const paramName = getFilterParamName(filterName, operator);
-
- let value = rawValue;
- if (filterName === 'durationMs') {
- // converting durationMs to duration_nano
- value *= 1000000;
- }
-
- if (paramName && value) {
- filterParams.append(paramName, value);
+ validFilters.forEach(({ operator, value: rawValue }) => {
+ if (filterName === 'attribute') {
+ handleAttributeFilter(rawValue, operator, filterParams);
+ } else {
+ const paramName = getFilterParamName(filterName, operator);
+ let value = rawValue;
+ if (filterName === 'durationMs') {
+ // converting durationMs to duration_nano
+ value *= 1000000;
+ }
+ if (paramName && value) {
+ filterParams.append(paramName, value);
+ }
}
});
});
@@ -161,12 +180,12 @@ function filterObjToQueryParams(filterObj) {
* {
* durationMs: [ {operator: '>', value: '100'}, {operator: '<', value: '1000'}],
* operation: [ {operator: '=', value: 'someOp}],
- * serviceName: [ {operator: '!=', value: 'foo}]
+ * service: [ {operator: '!=', value: 'foo}]
* }
*
* @returns Array<Trace> : A list of traces
*/
-async function fetchTraces(tracingUrl, { filters = {}, pageToken, pageSize } = {}) {
+async function fetchTraces(tracingUrl, { filters = {}, pageToken, pageSize, sortBy } = {}) {
const params = filterObjToQueryParams(filters);
if (pageToken) {
params.append('page_token', pageToken);
@@ -174,6 +193,10 @@ async function fetchTraces(tracingUrl, { filters = {}, pageToken, pageSize } = {
if (pageSize) {
params.append('page_size', pageSize);
}
+ const sortOrder = Object.values(SORTING_OPTIONS).includes(sortBy)
+ ? sortBy
+ : DEFAULT_SORTING_OPTION;
+ params.append('sort', sortOrder);
try {
const { data } = await axios.get(tracingUrl, {
@@ -228,18 +251,54 @@ async function fetchOperations(operationsUrl, serviceName) {
}
}
-export function buildClient({ provisioningUrl, tracingUrl, servicesUrl, operationsUrl } = {}) {
- if (!provisioningUrl || !tracingUrl || !servicesUrl || !operationsUrl) {
- throw new Error(
- 'missing required params. provisioningUrl, tracingUrl, servicesUrl, operationsUrl are required',
- );
+async function fetchMetrics(metricsUrl) {
+ try {
+ const { data } = await axios.get(metricsUrl, {
+ withCredentials: true,
+ });
+ if (!Array.isArray(data.metrics)) {
+ throw new Error('metrics are missing/invalid in the response'); // eslint-disable-line @gitlab/require-i18n-strings
+ }
+ return data;
+ } catch (e) {
+ return reportErrorAndThrow(e);
+ }
+}
+
+export function buildClient(config) {
+ if (!config) {
+ throw new Error('No options object provided'); // eslint-disable-line @gitlab/require-i18n-strings
+ }
+
+ const { provisioningUrl, tracingUrl, servicesUrl, operationsUrl, metricsUrl } = config;
+
+ if (typeof provisioningUrl !== 'string') {
+ throw new Error('provisioningUrl param must be a string');
}
+
+ if (typeof tracingUrl !== 'string') {
+ throw new Error('tracingUrl param must be a string');
+ }
+
+ if (typeof servicesUrl !== 'string') {
+ throw new Error('servicesUrl param must be a string');
+ }
+
+ if (typeof operationsUrl !== 'string') {
+ throw new Error('operationsUrl param must be a string');
+ }
+
+ if (typeof metricsUrl !== 'string') {
+ throw new Error('metricsUrl param must be a string');
+ }
+
return {
- enableTraces: () => enableTraces(provisioningUrl),
- isTracingEnabled: () => isTracingEnabled(provisioningUrl),
- fetchTraces: (filters) => fetchTraces(tracingUrl, filters),
+ enableObservability: () => enableObservability(provisioningUrl),
+ isObservabilityEnabled: () => isObservabilityEnabled(provisioningUrl),
+ fetchTraces: (options) => fetchTraces(tracingUrl, options),
fetchTrace: (traceId) => fetchTrace(tracingUrl, traceId),
fetchServices: () => fetchServices(servicesUrl),
fetchOperations: (serviceName) => fetchOperations(operationsUrl, serviceName),
+ fetchMetrics: () => fetchMetrics(metricsUrl),
};
}
diff --git a/app/assets/javascripts/observability/components/loader/constants.js b/app/assets/javascripts/observability/components/loader/constants.js
new file mode 100644
index 00000000000..5c2d8ad0d1b
--- /dev/null
+++ b/app/assets/javascripts/observability/components/loader/constants.js
@@ -0,0 +1,20 @@
+import { __ } from '~/locale';
+
+export const CONTENT_STATE = Object.freeze({
+ ERROR: 'error',
+ LOADED: 'loaded',
+});
+
+export const LOADER_STATE = Object.freeze({
+ ERROR: 'error',
+ VISIBLE: 'visible',
+ HIDDEN: 'hidden',
+});
+
+export const DEFAULT_TIMERS = Object.freeze({
+ TIMEOUT_MS: 20000,
+ CONTENT_WAIT_MS: 500,
+});
+
+export const TIMEOUT_ERROR_LABEL = __('Unable to load the page');
+export const TIMEOUT_ERROR_MESSAGE = __('Reload the page to try again.');
diff --git a/app/assets/javascripts/observability/components/loader/index.vue b/app/assets/javascripts/observability/components/loader/index.vue
new file mode 100644
index 00000000000..6b92dc428d2
--- /dev/null
+++ b/app/assets/javascripts/observability/components/loader/index.vue
@@ -0,0 +1,139 @@
+<!-- eslint-disable vue/multi-word-component-names -->
+<script>
+import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
+
+import {
+ LOADER_STATE,
+ CONTENT_STATE,
+ DEFAULT_TIMERS,
+ TIMEOUT_ERROR_LABEL,
+ TIMEOUT_ERROR_MESSAGE,
+} from './constants';
+
+export default {
+ components: {
+ GlAlert,
+ GlLoadingIcon,
+ },
+ LOADER_STATE,
+ i18n: {
+ TIMEOUT_ERROR_LABEL,
+ TIMEOUT_ERROR_MESSAGE,
+ },
+ props: {
+ contentState: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+ data() {
+ return {
+ state: null,
+ loadingTimeout: null,
+ errorTimeout: null,
+ };
+ },
+
+ computed: {
+ loaderVisible() {
+ return this.state === LOADER_STATE.VISIBLE;
+ },
+ loaderHidden() {
+ return this.state === LOADER_STATE.HIDDEN;
+ },
+ errorVisible() {
+ return this.state === LOADER_STATE.ERROR;
+ },
+ },
+ watch: {
+ contentState(newValue) {
+ if (newValue === CONTENT_STATE.LOADED) {
+ this.onContentLoaded();
+ } else if (newValue === CONTENT_STATE.ERROR) {
+ this.onError();
+ }
+ },
+ },
+ mounted() {
+ this.setLoadingTimeout();
+ this.setErrorTimeout();
+ },
+ destroyed() {
+ clearTimeout(this.loadingTimeout);
+ clearTimeout(this.errorTimeout);
+ },
+ methods: {
+ onContentLoaded() {
+ clearTimeout(this.errorTimeout);
+ clearTimeout(this.loadingTimeout);
+
+ this.hideLoader();
+ },
+ onError() {
+ clearTimeout(this.errorTimeout);
+ clearTimeout(this.loadingTimeout);
+
+ this.showError();
+ },
+ setLoadingTimeout() {
+ this.loadingTimeout = setTimeout(() => {
+ /**
+ * If content is not loaded within CONTENT_WAIT_MS,
+ * show the loader
+ */
+ if (this.state !== LOADER_STATE.HIDDEN) {
+ this.showLoader();
+ }
+ }, DEFAULT_TIMERS.CONTENT_WAIT_MS);
+ },
+ setErrorTimeout() {
+ this.errorTimeout = setTimeout(() => {
+ /**
+ * If content is not loaded within TIMEOUT_MS,
+ * show the error dialog
+ */
+ if (this.state !== LOADER_STATE.HIDDEN) {
+ this.showError();
+ }
+ }, DEFAULT_TIMERS.TIMEOUT_MS);
+ },
+ hideLoader() {
+ this.state = LOADER_STATE.HIDDEN;
+ },
+ showLoader() {
+ this.state = LOADER_STATE.VISIBLE;
+ },
+ showError() {
+ this.state = LOADER_STATE.ERROR;
+ },
+ },
+};
+</script>
+<template>
+ <div class="gl-flex-grow-1 gl-display-flex gl-flex-direction-column gl-flex-align-items-stretch">
+ <transition name="fade">
+ <div v-if="loaderVisible" class="gl-px-5 gl-my-5">
+ <gl-loading-icon size="lg" />
+ </div>
+
+ <div
+ v-else-if="loaderHidden"
+ data-testid="content-wrapper"
+ class="gl-flex-grow-1 gl-display-flex gl-flex-direction-column gl-flex-align-items-stretch"
+ >
+ <slot></slot>
+ </div>
+ </transition>
+
+ <gl-alert
+ v-if="errorVisible"
+ :title="$options.i18n.TIMEOUT_ERROR_LABEL"
+ variant="danger"
+ :dismissible="false"
+ class="gl-m-5"
+ >
+ {{ $options.i18n.TIMEOUT_ERROR_MESSAGE }}
+ </gl-alert>
+ </div>
+</template>
diff --git a/app/assets/javascripts/observability/components/observability_container.vue b/app/assets/javascripts/observability/components/observability_container.vue
index 1518c132560..b89c2624f81 100644
--- a/app/assets/javascripts/observability/components/observability_container.vue
+++ b/app/assets/javascripts/observability/components/observability_container.vue
@@ -1,32 +1,17 @@
<script>
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
+import { logError } from '~/lib/logger';
import { buildClient } from '../client';
-import { SKELETON_SPINNER_VARIANT } from '../constants';
-import ObservabilitySkeleton from './skeleton/index.vue';
+import ObservabilityLoader from './loader/index.vue';
+import { CONTENT_STATE } from './loader/constants';
export default {
- SKELETON_SPINNER_VARIANT,
components: {
- ObservabilitySkeleton,
+ ObservabilityLoader,
},
props: {
- oauthUrl: {
- type: String,
- required: true,
- },
- provisioningUrl: {
- type: String,
- required: true,
- },
- tracingUrl: {
- type: String,
- required: true,
- },
- servicesUrl: {
- type: String,
- required: true,
- },
- operationsUrl: {
- type: String,
+ apiConfig: {
+ type: Object,
required: true,
},
},
@@ -34,6 +19,7 @@ export default {
return {
observabilityClient: null,
authCompleted: false,
+ loaderContentState: null,
};
},
mounted() {
@@ -53,7 +39,7 @@ export default {
},
methods: {
messageHandler(e) {
- const isExpectedOrigin = e.origin === new URL(this.oauthUrl).origin;
+ const isExpectedOrigin = e.origin === new URL(this.apiConfig.oauthUrl).origin;
if (!isExpectedOrigin) return;
const { data } = e;
@@ -63,17 +49,14 @@ export default {
const { status, message, statusCode } = data;
if (status === 'success') {
- this.observabilityClient = buildClient({
- provisioningUrl: this.provisioningUrl,
- tracingUrl: this.tracingUrl,
- servicesUrl: this.servicesUrl,
- operationsUrl: this.operationsUrl,
- });
- this.$refs.observabilitySkeleton?.onContentLoaded();
+ this.observabilityClient = buildClient(this.apiConfig);
+ this.$emit('observability-client-ready', this.observabilityClient);
+ this.loaderContentState = CONTENT_STATE.LOADED;
} else if (status === 'error') {
- // eslint-disable-next-line @gitlab/require-i18n-strings,no-console
- console.error('GOB auth failed with error:', message, statusCode);
- this.$refs.observabilitySkeleton?.onError();
+ const error = new Error(`GOB auth failed with error: ${message} - status: ${statusCode}`);
+ Sentry.captureException(error);
+ logError(error);
+ this.loaderContentState = CONTENT_STATE.ERROR;
}
this.authCompleted = true;
}
@@ -88,15 +71,12 @@ export default {
v-if="!authCompleted"
sandbox="allow-same-origin allow-forms allow-scripts"
hidden
- :src="oauthUrl"
+ :src="apiConfig.oauthUrl"
data-testid="observability-oauth-iframe"
></iframe>
- <observability-skeleton
- ref="observabilitySkeleton"
- :variant="$options.SKELETON_SPINNER_VARIANT"
- >
+ <observability-loader :content-state="loaderContentState">
<slot v-if="observabilityClient" :observability-client="observabilityClient"></slot>
- </observability-skeleton>
+ </observability-loader>
</div>
</template>
diff --git a/app/assets/javascripts/observability/components/observability_empty_state.vue b/app/assets/javascripts/observability/components/observability_empty_state.vue
new file mode 100644
index 00000000000..d4d8b887934
--- /dev/null
+++ b/app/assets/javascripts/observability/components/observability_empty_state.vue
@@ -0,0 +1,36 @@
+<script>
+import EMPTY_TRACING_SVG from '@gitlab/svgs/dist/illustrations/monitoring/tracing.svg?url';
+import { GlEmptyState, GlButton } from '@gitlab/ui';
+import { s__ } from '~/locale';
+
+export default {
+ EMPTY_TRACING_SVG,
+ i18n: {
+ title: s__('Observability|Get started with GitLab Observability'),
+ description: s__('Observability|Monitor your applications with GitLab Observability.'),
+ enableButtonText: s__('Observability|Enable'),
+ },
+ components: {
+ GlEmptyState,
+ GlButton,
+ },
+};
+</script>
+
+<template>
+ <gl-empty-state
+ :title="$options.i18n.title"
+ :svg-path="$options.EMPTY_TRACING_SVG"
+ :svg-height="null"
+ >
+ <template #description>
+ <span>{{ $options.i18n.description }}</span>
+ </template>
+
+ <template #actions>
+ <gl-button variant="confirm" class="gl-mx-2 gl-mb-3" @click="$emit('enable-observability')">
+ {{ $options.i18n.enableButtonText }}
+ </gl-button>
+ </template>
+ </gl-empty-state>
+</template>
diff --git a/app/assets/javascripts/observability/components/provisioned_observability_container.vue b/app/assets/javascripts/observability/components/provisioned_observability_container.vue
new file mode 100644
index 00000000000..95ffd54fd1d
--- /dev/null
+++ b/app/assets/javascripts/observability/components/provisioned_observability_container.vue
@@ -0,0 +1,95 @@
+<script>
+import { GlLoadingIcon } from '@gitlab/ui';
+import ObservabilityContainer from '~/observability/components/observability_container.vue';
+import { s__ } from '~/locale';
+import { createAlert } from '~/alert';
+import ObservabilityEmptyState from './observability_empty_state.vue';
+
+export default {
+ components: {
+ ObservabilityContainer,
+ ObservabilityEmptyState,
+ GlLoadingIcon,
+ },
+ props: {
+ apiConfig: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ loading: false,
+ /**
+ * observabilityEnabled: boolean | null.
+ * null identifies a state where we don't know if observability is enabled or not (e.g. when fetching the status from the API fails)
+ */
+ observabilityEnabled: null,
+ observabilityClient: null,
+ };
+ },
+ computed: {
+ isObservabilityStatusKnown() {
+ return this.observabilityEnabled !== null;
+ },
+ isObservabilityDisabled() {
+ return this.observabilityEnabled === false;
+ },
+ isObservabilityEnabled() {
+ return this.observabilityEnabled;
+ },
+ },
+ methods: {
+ onObservabilityClientReady(client) {
+ this.observabilityClient = client;
+ this.checkEnabled();
+ },
+ async checkEnabled() {
+ this.loading = true;
+ try {
+ this.observabilityEnabled = await this.observabilityClient.isObservabilityEnabled();
+ } catch (e) {
+ createAlert({
+ message: s__('Observability|Error: Failed to load page. Try reloading the page.'),
+ });
+ } finally {
+ this.loading = false;
+ }
+ },
+ async onEnableObservability() {
+ this.loading = true;
+ try {
+ await this.observabilityClient.enableObservability();
+ this.observabilityEnabled = true;
+ } catch (e) {
+ createAlert({
+ message: s__(
+ 'Observability|Error: Failed to enable GitLab Observability. Please retry later.',
+ ),
+ });
+ } finally {
+ this.loading = false;
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <observability-container
+ :api-config="apiConfig"
+ @observability-client-ready="onObservabilityClientReady"
+ >
+ <div v-if="loading" class="gl-py-5">
+ <gl-loading-icon size="lg" />
+ </div>
+
+ <template v-else-if="isObservabilityStatusKnown">
+ <observability-empty-state
+ v-if="isObservabilityDisabled"
+ @enable-observability="onEnableObservability"
+ />
+ <slot v-if="isObservabilityEnabled" :observability-client="observabilityClient"></slot>
+ </template>
+ </observability-container>
+</template>
diff --git a/app/assets/javascripts/observability/components/skeleton/index.vue b/app/assets/javascripts/observability/components/skeleton/index.vue
deleted file mode 100644
index c3d0a7c90b1..00000000000
--- a/app/assets/javascripts/observability/components/skeleton/index.vue
+++ /dev/null
@@ -1,151 +0,0 @@
-<!-- eslint-disable vue/multi-word-component-names -->
-<script>
-import { GlSkeletonLoader, GlAlert, GlLoadingIcon } from '@gitlab/ui';
-
-import {
- SKELETON_STATE,
- DEFAULT_TIMERS,
- TIMEOUT_ERROR_LABEL,
- TIMEOUT_ERROR_MESSAGE,
- SKELETON_SPINNER_VARIANT,
-} from '../../constants';
-
-export default {
- components: {
- GlSkeletonLoader,
- GlAlert,
- GlLoadingIcon,
- },
- SKELETON_STATE,
- i18n: {
- TIMEOUT_ERROR_LABEL,
- TIMEOUT_ERROR_MESSAGE,
- },
- props: {
- variant: {
- type: String,
- required: false,
- default: '',
- },
- },
- data() {
- return {
- state: null,
- loadingTimeout: null,
- errorTimeout: null,
- };
- },
- computed: {
- skeletonVisible() {
- return this.state === SKELETON_STATE.VISIBLE;
- },
- skeletonHidden() {
- return this.state === SKELETON_STATE.HIDDEN;
- },
- errorVisible() {
- return this.state === SKELETON_STATE.ERROR;
- },
- spinnerVariant() {
- return this.variant === SKELETON_SPINNER_VARIANT;
- },
- },
- mounted() {
- this.setLoadingTimeout();
- this.setErrorTimeout();
- },
- destroyed() {
- clearTimeout(this.loadingTimeout);
- clearTimeout(this.errorTimeout);
- },
- methods: {
- onContentLoaded() {
- clearTimeout(this.errorTimeout);
- clearTimeout(this.loadingTimeout);
-
- this.hideSkeleton();
- },
- onError() {
- clearTimeout(this.errorTimeout);
- clearTimeout(this.loadingTimeout);
-
- this.showError();
- },
- setLoadingTimeout() {
- this.loadingTimeout = setTimeout(() => {
- /**
- * If content is not loaded within CONTENT_WAIT_MS,
- * show the skeleton
- */
- if (this.state !== SKELETON_STATE.HIDDEN) {
- this.showSkeleton();
- }
- }, DEFAULT_TIMERS.CONTENT_WAIT_MS);
- },
- setErrorTimeout() {
- this.errorTimeout = setTimeout(() => {
- /**
- * If content is not loaded within TIMEOUT_MS,
- * show the error dialog
- */
- if (this.state !== SKELETON_STATE.HIDDEN) {
- this.showError();
- }
- }, DEFAULT_TIMERS.TIMEOUT_MS);
- },
- hideSkeleton() {
- this.state = SKELETON_STATE.HIDDEN;
- },
- showSkeleton() {
- this.state = SKELETON_STATE.VISIBLE;
- },
- showError() {
- this.state = SKELETON_STATE.ERROR;
- },
- },
-};
-</script>
-<template>
- <div class="gl-flex-grow-1 gl-display-flex gl-flex-direction-column gl-flex-align-items-stretch">
- <transition name="fade">
- <div v-if="skeletonVisible" class="gl-px-5 gl-my-5">
- <gl-loading-icon v-if="spinnerVariant" size="lg" />
- <gl-skeleton-loader v-else>
- <rect y="2" width="10" height="8" />
- <rect y="2" x="15" width="15" height="8" />
- <rect y="2" x="35" width="15" height="8" />
- <rect y="15" width="400" height="30" />
- </gl-skeleton-loader>
- </div>
-
- <!-- The double condition is only here temporarily for back-compatibility reasons. Will be removed in next iteration https://gitlab.com/gitlab-org/opstrace/opstrace/-/issues/2275 -->
- <div
- v-else-if="spinnerVariant && skeletonHidden"
- data-testid="content-wrapper"
- class="gl-flex-grow-1 gl-display-flex gl-flex-direction-column gl-flex-align-items-stretch"
- >
- <slot></slot>
- </div>
- </transition>
-
- <gl-alert
- v-if="errorVisible"
- :title="$options.i18n.TIMEOUT_ERROR_LABEL"
- variant="danger"
- :dismissible="false"
- class="gl-m-5"
- >
- {{ $options.i18n.TIMEOUT_ERROR_MESSAGE }}
- </gl-alert>
-
- <!-- This is only kept temporarily for back-compatibility reasons. Will be removed in next iteration https://gitlab.com/gitlab-org/opstrace/opstrace/-/issues/2275 -->
- <transition v-if="!spinnerVariant">
- <div
- v-show="skeletonHidden"
- data-testid="content-wrapper"
- class="gl-flex-grow-1 gl-display-flex gl-flex-direction-column gl-flex-align-items-stretch"
- >
- <slot></slot>
- </div>
- </transition>
- </div>
-</template>
diff --git a/app/assets/javascripts/observability/constants.js b/app/assets/javascripts/observability/constants.js
index 83eaea185e5..34c43a10fc0 100644
--- a/app/assets/javascripts/observability/constants.js
+++ b/app/assets/javascripts/observability/constants.js
@@ -1,17 +1,7 @@
-import { __ } from '~/locale';
-
-export const SKELETON_SPINNER_VARIANT = 'spinner';
-
-export const SKELETON_STATE = Object.freeze({
- ERROR: 'error',
- VISIBLE: 'visible',
- HIDDEN: 'hidden',
-});
-
-export const DEFAULT_TIMERS = Object.freeze({
- TIMEOUT_MS: 20000,
- CONTENT_WAIT_MS: 500,
-});
-
-export const TIMEOUT_ERROR_LABEL = __('Unable to load the page');
-export const TIMEOUT_ERROR_MESSAGE = __('Reload the page to try again.');
+export const SORTING_OPTIONS = {
+ TIMESTAMP_DESC: 'timestamp_desc',
+ TIMESTAMP_ASC: 'timestamp_asc',
+ DURATION_DESC: 'duration_desc',
+ DURATION_ASC: 'duration_asc',
+};
+export const DEFAULT_SORTING_OPTION = SORTING_OPTIONS.TIMESTAMP_DESC;
diff --git a/app/assets/javascripts/organizations/mock_data.js b/app/assets/javascripts/organizations/mock_data.js
index d281a0d8a1c..725b6ac1ad8 100644
--- a/app/assets/javascripts/organizations/mock_data.js
+++ b/app/assets/javascripts/organizations/mock_data.js
@@ -281,10 +281,31 @@ export const organizationGroups = {
],
};
-export const createOrganizationResponse = {
+export const organizationCreateResponse = {
+ data: {
+ organizationCreate: {
+ organization: {
+ id: 'gid://gitlab/Organizations::Organization/1',
+ webUrl: 'http://127.0.0.1:3000/-/organizations/default',
+ },
+ errors: [],
+ },
+ },
+};
+
+export const organizationCreateResponseWithErrors = {
+ data: {
+ organizationCreate: {
+ organization: null,
+ errors: ['Path is too short (minimum is 2 characters)'],
+ },
+ },
+};
+
+export const updateOrganizationResponse = {
organization: {
- name: 'Default',
- path: '/-/organizations/default',
+ id: 'gid://gitlab/Organizations/1',
+ name: 'Default updated',
},
errors: [],
};
diff --git a/app/assets/javascripts/organizations/new/components/app.vue b/app/assets/javascripts/organizations/new/components/app.vue
index 8f71fdfe68b..f7f7b79d52b 100644
--- a/app/assets/javascripts/organizations/new/components/app.vue
+++ b/app/assets/javascripts/organizations/new/components/app.vue
@@ -4,12 +4,13 @@ import { s__ } from '~/locale';
import { visitUrlWithAlerts } from '~/lib/utils/url_utility';
import { createAlert } from '~/alert';
import { helpPagePath } from '~/helpers/help_page_helper';
-import createOrganizationMutation from '../graphql/mutations/create_organization.mutation.graphql';
+import FormErrorsAlert from '~/vue_shared/components/form/errors_alert.vue';
+import organizationCreateMutation from '../graphql/mutations/organization_create.mutation.graphql';
import NewEditForm from '../../shared/components/new_edit_form.vue';
export default {
name: 'OrganizationNewApp',
- components: { NewEditForm, GlSprintf, GlLink },
+ components: { NewEditForm, GlSprintf, GlLink, FormErrorsAlert },
i18n: {
pageTitle: s__('Organization|New organization'),
pageDescription: s__(
@@ -22,6 +23,7 @@ export default {
data() {
return {
loading: false,
+ errors: [],
};
},
computed: {
@@ -35,21 +37,22 @@ export default {
try {
const {
data: {
- createOrganization: { organization, errors },
+ organizationCreate: { organization, errors },
},
} = await this.$apollo.mutate({
- mutation: createOrganizationMutation,
+ mutation: organizationCreateMutation,
variables: {
- ...formValues,
+ input: { name: formValues.name, path: formValues.path },
},
});
if (errors.length) {
- // TODO: handle errors when using real API after https://gitlab.com/gitlab-org/gitlab/-/issues/417891 is complete.
+ this.errors = errors;
+
return;
}
- visitUrlWithAlerts(organization.path, [
+ visitUrlWithAlerts(organization.webUrl, [
{
id: 'organization-successfully-created',
title: this.$options.i18n.successAlertTitle,
@@ -69,6 +72,7 @@ export default {
<template>
<div class="gl-py-6">
+ <form-errors-alert v-model="errors" />
<h1 class="gl-mt-0 gl-font-size-h-display">{{ $options.i18n.pageTitle }}</h1>
<p>
<gl-sprintf :message="$options.i18n.pageDescription">
diff --git a/app/assets/javascripts/organizations/new/graphql/mutations/create_organization.mutation.graphql b/app/assets/javascripts/organizations/new/graphql/mutations/create_organization.mutation.graphql
deleted file mode 100644
index 766c7e96d14..00000000000
--- a/app/assets/javascripts/organizations/new/graphql/mutations/create_organization.mutation.graphql
+++ /dev/null
@@ -1,9 +0,0 @@
-mutation createOrganization($input: LocalCreateOrganizationInput!) {
- createOrganization(input: $input) @client {
- organization {
- name
- path
- }
- errors
- }
-}
diff --git a/app/assets/javascripts/organizations/new/graphql/mutations/organization_create.mutation.graphql b/app/assets/javascripts/organizations/new/graphql/mutations/organization_create.mutation.graphql
new file mode 100644
index 00000000000..81fbfddd1e4
--- /dev/null
+++ b/app/assets/javascripts/organizations/new/graphql/mutations/organization_create.mutation.graphql
@@ -0,0 +1,9 @@
+mutation organizationCreate($input: OrganizationCreateInput!) {
+ organizationCreate(input: $input) {
+ organization {
+ id
+ webUrl
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/organizations/new/graphql/typedefs.graphql b/app/assets/javascripts/organizations/new/graphql/typedefs.graphql
deleted file mode 100644
index f708c4ad162..00000000000
--- a/app/assets/javascripts/organizations/new/graphql/typedefs.graphql
+++ /dev/null
@@ -1,5 +0,0 @@
-# TODO: Use real input type when https://gitlab.com/gitlab-org/gitlab/-/issues/417891 is complete.
-input LocalCreateOrganizationInput {
- name: String
- path: String
-}
diff --git a/app/assets/javascripts/organizations/new/index.js b/app/assets/javascripts/organizations/new/index.js
index a65603227f6..9c7e5344800 100644
--- a/app/assets/javascripts/organizations/new/index.js
+++ b/app/assets/javascripts/organizations/new/index.js
@@ -3,7 +3,6 @@ import VueApollo from 'vue-apollo';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import createDefaultClient from '~/lib/graphql';
-import resolvers from '../shared/graphql/resolvers';
import App from './components/app.vue';
export const initOrganizationsNew = () => {
@@ -17,7 +16,7 @@ export const initOrganizationsNew = () => {
const { organizationsPath, rootUrl } = convertObjectPropsToCamelCase(JSON.parse(appData));
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(resolvers),
+ defaultClient: createDefaultClient(),
});
return new Vue({
diff --git a/app/assets/javascripts/organizations/profile/preferences/index.js b/app/assets/javascripts/organizations/profile/preferences/index.js
new file mode 100644
index 00000000000..0b0dd313cd8
--- /dev/null
+++ b/app/assets/javascripts/organizations/profile/preferences/index.js
@@ -0,0 +1,41 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import { s__ } from '~/locale';
+import OrganizationSelect from '~/vue_shared/components/entity_select/organization_select.vue';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import resolvers from '../../shared/graphql/resolvers';
+
+export const initHomeOrganizationSetting = () => {
+ const el = document.getElementById('js-home-organization-setting');
+
+ if (!el) return false;
+
+ const {
+ dataset: { appData },
+ } = el;
+ const { initialSelection } = convertObjectPropsToCamelCase(JSON.parse(appData));
+
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(resolvers),
+ });
+
+ return new Vue({
+ el,
+ name: 'HomeOrganizationSetting',
+ apolloProvider,
+ render(createElement) {
+ return createElement(OrganizationSelect, {
+ props: {
+ block: true,
+ label: s__('Organization|Home organization'),
+ description: s__('Organization|Choose what organization you want to see by default.'),
+ inputName: 'home_organization',
+ inputId: 'home_organization',
+ initialSelection,
+ toggleClass: 'gl-form-input-xl',
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/organizations/settings/general/components/app.vue b/app/assets/javascripts/organizations/settings/general/components/app.vue
new file mode 100644
index 00000000000..134fcc17b54
--- /dev/null
+++ b/app/assets/javascripts/organizations/settings/general/components/app.vue
@@ -0,0 +1,14 @@
+<script>
+import OrganizationSettings from './organization_settings.vue';
+
+export default {
+ name: 'OrganizationSettingsGeneralApp',
+ components: { OrganizationSettings },
+};
+</script>
+
+<template>
+ <div>
+ <organization-settings />
+ </div>
+</template>
diff --git a/app/assets/javascripts/organizations/settings/general/components/organization_settings.vue b/app/assets/javascripts/organizations/settings/general/components/organization_settings.vue
new file mode 100644
index 00000000000..14826825cd6
--- /dev/null
+++ b/app/assets/javascripts/organizations/settings/general/components/organization_settings.vue
@@ -0,0 +1,77 @@
+<script>
+import { s__, __ } from '~/locale';
+import { createAlert, VARIANT_INFO } from '~/alert';
+import NewEditForm from '~/organizations/shared/components/new_edit_form.vue';
+import { FORM_FIELD_NAME, FORM_FIELD_ID } from '~/organizations/shared/constants';
+import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue';
+import updateOrganizationMutation from '../graphql/mutations/update_organization.mutation.graphql';
+
+export default {
+ name: 'OrganizationSettings',
+ components: { NewEditForm, SettingsBlock },
+ inject: ['organization'],
+ i18n: {
+ submitButtonText: __('Save changes'),
+ settingsBlock: {
+ title: s__('Organization|Organization settings'),
+ description: s__('Organization|Update your organization name, description, and avatar.'),
+ },
+ errorMessage: s__(
+ 'Organization|An error occurred updating your organization. Please try again.',
+ ),
+ successMessage: s__('Organization|Organization was successfully updated.'),
+ },
+ fieldsToRender: [FORM_FIELD_NAME, FORM_FIELD_ID],
+ data() {
+ return {
+ loading: false,
+ };
+ },
+ methods: {
+ async onSubmit(formValues) {
+ this.loading = true;
+ try {
+ const {
+ data: {
+ updateOrganization: { errors },
+ },
+ } = await this.$apollo.mutate({
+ mutation: updateOrganizationMutation,
+ variables: {
+ id: this.organization.id,
+ name: formValues.name,
+ },
+ });
+
+ if (errors.length) {
+ // TODO: handle errors when using real API after https://gitlab.com/gitlab-org/gitlab/-/issues/419608 is complete.
+ return;
+ }
+
+ createAlert({ message: this.$options.i18n.successMessage, variant: VARIANT_INFO });
+ } catch (error) {
+ createAlert({ message: this.$options.i18n.errorMessage, error, captureError: true });
+ } finally {
+ this.loading = false;
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <settings-block default-expanded slide-animated>
+ <template #title>{{ $options.i18n.settingsBlock.title }}</template>
+ <template #description>{{ $options.i18n.settingsBlock.description }}</template>
+ <template #default>
+ <new-edit-form
+ :loading="loading"
+ :initial-form-values="organization"
+ :fields-to-render="$options.fieldsToRender"
+ :submit-button-text="$options.i18n.submitButtonText"
+ :show-cancel-button="false"
+ @submit="onSubmit"
+ />
+ </template>
+ </settings-block>
+</template>
diff --git a/app/assets/javascripts/organizations/settings/general/graphql/mutations/update_organization.mutation.graphql b/app/assets/javascripts/organizations/settings/general/graphql/mutations/update_organization.mutation.graphql
new file mode 100644
index 00000000000..b571a523260
--- /dev/null
+++ b/app/assets/javascripts/organizations/settings/general/graphql/mutations/update_organization.mutation.graphql
@@ -0,0 +1,9 @@
+mutation updateOrganization($input: LocalUpdateOrganizationInput!) {
+ updateOrganization(input: $input) @client {
+ organization {
+ id
+ name
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/organizations/settings/general/graphql/typedefs.graphql b/app/assets/javascripts/organizations/settings/general/graphql/typedefs.graphql
new file mode 100644
index 00000000000..eb81a7b0321
--- /dev/null
+++ b/app/assets/javascripts/organizations/settings/general/graphql/typedefs.graphql
@@ -0,0 +1,5 @@
+# TODO: Use real input type when https://gitlab.com/gitlab-org/gitlab/-/issues/419608 is complete.
+input LocalUpdateOrganizationInput {
+ id: ID!
+ name: String
+}
diff --git a/app/assets/javascripts/organizations/settings/general/index.js b/app/assets/javascripts/organizations/settings/general/index.js
new file mode 100644
index 00000000000..36303c32b94
--- /dev/null
+++ b/app/assets/javascripts/organizations/settings/general/index.js
@@ -0,0 +1,38 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import createDefaultClient from '~/lib/graphql';
+import resolvers from '../../shared/graphql/resolvers';
+import App from './components/app.vue';
+
+export const initOrganizationsSettingsGeneral = () => {
+ const el = document.getElementById('js-organizations-settings-general');
+
+ if (!el) return false;
+
+ const {
+ dataset: { appData },
+ } = el;
+ const { organization, organizationsPath, rootUrl } = convertObjectPropsToCamelCase(
+ JSON.parse(appData),
+ );
+
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(resolvers),
+ });
+
+ return new Vue({
+ el,
+ name: 'OrganizationSettingsGeneralRoot',
+ apolloProvider,
+ provide: {
+ organization,
+ organizationsPath,
+ rootUrl,
+ },
+ render(createElement) {
+ return createElement(App);
+ },
+ });
+};
diff --git a/app/assets/javascripts/organizations/shared/components/new_edit_form.vue b/app/assets/javascripts/organizations/shared/components/new_edit_form.vue
index db33f240966..8aaa680036f 100644
--- a/app/assets/javascripts/organizations/shared/components/new_edit_form.vue
+++ b/app/assets/javascripts/organizations/shared/components/new_edit_form.vue
@@ -12,6 +12,7 @@ import { formValidators } from '@gitlab/ui/dist/utils';
import { s__, __ } from '~/locale';
import { slugify } from '~/lib/utils/text_utility';
import { joinPaths } from '~/lib/utils/url_utility';
+import { FORM_FIELD_NAME, FORM_FIELD_ID, FORM_FIELD_PATH } from '../constants';
export default {
name: 'NewEditForm',
@@ -25,43 +26,47 @@ export default {
GlTruncate,
},
i18n: {
- createOrganization: s__('Organization|Create organization'),
cancel: __('Cancel'),
pathPlaceholder: s__('Organization|my-organization'),
},
formId: 'new-organization-form',
- fields: {
- name: {
- label: s__('Organization|Organization name'),
- validators: [formValidators.required(s__('Organization|Organization name is required.'))],
- groupAttrs: {
- description: s__(
- 'Organization|Must start with a letter, digit, emoji, or underscore. Can also contain periods, dashes, spaces, and parentheses.',
- ),
- },
- inputAttrs: {
- class: 'gl-md-form-input-lg',
- placeholder: s__('Organization|My organization'),
- },
- },
- path: {
- label: s__('Organization|Organization URL'),
- validators: [formValidators.required(s__('Organization|Organization URL is required.'))],
- },
- },
inject: ['organizationsPath', 'rootUrl'],
props: {
loading: {
type: Boolean,
required: true,
},
+ initialFormValues: {
+ type: Object,
+ required: false,
+ default() {
+ return {
+ [FORM_FIELD_NAME]: '',
+ [FORM_FIELD_PATH]: '',
+ };
+ },
+ },
+ fieldsToRender: {
+ type: Array,
+ required: false,
+ default() {
+ return [FORM_FIELD_NAME, FORM_FIELD_PATH];
+ },
+ },
+ submitButtonText: {
+ type: String,
+ required: false,
+ default: s__('Organization|Create organization'),
+ },
+ showCancelButton: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
data() {
return {
- formValues: {
- name: '',
- path: '',
- },
+ formValues: this.initialFormValues,
hasPathBeenManuallySet: false,
};
},
@@ -69,10 +74,63 @@ export default {
baseUrl() {
return joinPaths(this.rootUrl, this.organizationsPath, '/');
},
+ fields() {
+ const fields = {
+ [FORM_FIELD_NAME]: {
+ label: s__('Organization|Organization name'),
+ validators: [formValidators.required(s__('Organization|Organization name is required.'))],
+ groupAttrs: {
+ class: this.fieldsToRender.includes(FORM_FIELD_ID)
+ ? 'gl-flex-grow-1 gl-md-form-input-lg'
+ : 'gl-flex-grow-1',
+ description: s__(
+ 'Organization|Must start with a letter, digit, emoji, or underscore. Can also contain periods, dashes, spaces, and parentheses.',
+ ),
+ },
+ inputAttrs: {
+ class: !this.fieldsToRender.includes(FORM_FIELD_ID) ? 'gl-md-form-input-lg' : null,
+ placeholder: s__('Organization|My organization'),
+ },
+ },
+ [FORM_FIELD_ID]: {
+ label: s__('Organization|Organization ID'),
+ groupAttrs: {
+ class: 'gl-md-form-input-lg gl-flex-grow-1',
+ },
+ inputAttrs: {
+ disabled: true,
+ },
+ },
+ [FORM_FIELD_PATH]: {
+ label: s__('Organization|Organization URL'),
+ validators: [
+ formValidators.required(s__('Organization|Organization URL is required.')),
+ formValidators.factory(
+ s__('Organization|Organization URL must be a minimum of two characters.'),
+ (val) => val.length >= 2,
+ ),
+ ],
+ groupAttrs: {
+ class: 'gl-w-full',
+ },
+ },
+ };
+
+ return Object.entries(fields).reduce((accumulator, [fieldKey, fieldDefinition]) => {
+ if (!this.fieldsToRender.includes(fieldKey)) {
+ return accumulator;
+ }
+
+ return {
+ ...accumulator,
+ [fieldKey]: fieldDefinition,
+ };
+ }, {});
+ },
},
watch: {
'formValues.name': function watchName(value) {
- if (this.hasPathBeenManuallySet) {
+ if (this.hasPathBeenManuallySet || !this.fieldsToRender.includes(FORM_FIELD_PATH)) {
return;
}
@@ -93,7 +151,8 @@ export default {
<gl-form-fields
v-model="formValues"
:form-id="$options.formId"
- :fields="$options.fields"
+ :fields="fields"
+ class="gl-display-flex gl-column-gap-5 gl-flex-wrap"
@submit="$emit('submit', formValues)"
>
<template #input(path)="{ id, value, validation, input, blur }">
@@ -117,9 +176,11 @@ export default {
</gl-form-fields>
<div class="gl-display-flex gl-gap-3">
<gl-button type="submit" variant="confirm" class="js-no-auto-disable" :loading="loading">{{
- $options.i18n.createOrganization
+ submitButtonText
+ }}</gl-button>
+ <gl-button v-if="showCancelButton" :href="organizationsPath">{{
+ $options.i18n.cancel
}}</gl-button>
- <gl-button :href="organizationsPath">{{ $options.i18n.cancel }}</gl-button>
</div>
</gl-form>
</template>
diff --git a/app/assets/javascripts/organizations/shared/constants.js b/app/assets/javascripts/organizations/shared/constants.js
new file mode 100644
index 00000000000..010613bc9fd
--- /dev/null
+++ b/app/assets/javascripts/organizations/shared/constants.js
@@ -0,0 +1,3 @@
+export const FORM_FIELD_NAME = 'name';
+export const FORM_FIELD_ID = 'id';
+export const FORM_FIELD_PATH = 'path';
diff --git a/app/assets/javascripts/organizations/shared/graphql/queries/organization.query.graphql b/app/assets/javascripts/organizations/shared/graphql/queries/organization.query.graphql
new file mode 100644
index 00000000000..1d95786fcb0
--- /dev/null
+++ b/app/assets/javascripts/organizations/shared/graphql/queries/organization.query.graphql
@@ -0,0 +1,9 @@
+query getOrganization($id: ID!) {
+ organization(id: $id) @client {
+ id
+ name
+ descriptionHtml
+ avatarUrl
+ webUrl
+ }
+}
diff --git a/app/assets/javascripts/organizations/shared/graphql/resolvers.js b/app/assets/javascripts/organizations/shared/graphql/resolvers.js
index 9f7e9b22e1d..9ed1be62352 100644
--- a/app/assets/javascripts/organizations/shared/graphql/resolvers.js
+++ b/app/assets/javascripts/organizations/shared/graphql/resolvers.js
@@ -2,7 +2,7 @@ import {
organizations,
organizationProjects,
organizationGroups,
- createOrganizationResponse,
+ updateOrganizationResponse,
} from '../../mock_data';
const simulateLoading = () => {
@@ -34,11 +34,11 @@ export default {
},
},
Mutation: {
- createOrganization: async () => {
+ updateOrganization: async () => {
// Simulate API loading
await simulateLoading();
- return createOrganizationResponse;
+ return updateOrganizationResponse;
},
},
};
diff --git a/app/assets/javascripts/organizations/users/components/app.vue b/app/assets/javascripts/organizations/users/components/app.vue
new file mode 100644
index 00000000000..ae22bedd69a
--- /dev/null
+++ b/app/assets/javascripts/organizations/users/components/app.vue
@@ -0,0 +1,51 @@
+<script>
+import { __, s__ } from '~/locale';
+import { createAlert } from '~/alert';
+import organizationUsersQuery from '../graphql/organization_users.query.graphql';
+
+export default {
+ name: 'OrganizationsUsersApp',
+ i18n: {
+ users: __('Users'),
+ loadingPlaceholder: __('Loading'),
+ errorMessage: s__(
+ 'Organization|An error occurred loading the organization users. Please refresh the page to try again.',
+ ),
+ },
+ inject: ['organizationGid'],
+ data() {
+ return {
+ users: [],
+ };
+ },
+ apollo: {
+ users: {
+ query: organizationUsersQuery,
+ variables() {
+ return { id: this.organizationGid };
+ },
+ update(data) {
+ return data.organization.organizationUsers.nodes;
+ },
+ error(error) {
+ createAlert({ message: this.$options.i18n.errorMessage, error, captureError: true });
+ },
+ },
+ },
+ computed: {
+ loading() {
+ return this.$apollo.queries.users.loading;
+ },
+ },
+};
+</script>
+
+<template>
+ <section>
+ <h1 class="gl-my-4 gl-font-size-h-display">{{ $options.i18n.users }}</h1>
+ <template v-if="loading">
+ {{ $options.i18n.loadingPlaceholder }}
+ </template>
+ <div data-testid="organization-users">{{ users }}</div>
+ </section>
+</template>
diff --git a/app/assets/javascripts/organizations/users/graphql/organization_users.query.graphql b/app/assets/javascripts/organizations/users/graphql/organization_users.query.graphql
new file mode 100644
index 00000000000..a0b2a639401
--- /dev/null
+++ b/app/assets/javascripts/organizations/users/graphql/organization_users.query.graphql
@@ -0,0 +1,17 @@
+query getOrganizationUsers($id: OrganizationsOrganizationID!) {
+ organization(id: $id) {
+ id
+ organizationUsers {
+ nodes {
+ badges {
+ text
+ variant
+ }
+ id
+ user {
+ id
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/organizations/users/index.js b/app/assets/javascripts/organizations/users/index.js
new file mode 100644
index 00000000000..76656243075
--- /dev/null
+++ b/app/assets/javascripts/organizations/users/index.js
@@ -0,0 +1,29 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import OrganizationsUsersApp from './components/app.vue';
+
+export const initOrganizationsUsers = () => {
+ const el = document.getElementById('js-organizations-users');
+
+ if (!el) return false;
+
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+ });
+
+ const { organizationGid } = convertObjectPropsToCamelCase(el.dataset);
+
+ return new Vue({
+ el,
+ name: 'OrganizationsUsersRoot',
+ apolloProvider,
+ provide: {
+ organizationGid,
+ },
+ render(createElement) {
+ return createElement(OrganizationsUsersApp);
+ },
+ });
+};
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue
index 7c594a6c091..934bb206cc4 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue
@@ -219,7 +219,6 @@ export default {
<template>
<div>
<persisted-search
- class="gl-mb-5"
:sortable-fields="$options.sortableFields"
:default-order="$options.sortableFields[0].orderBy"
default-sort="asc"
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue
index a1c4d7ea1f2..89a8c4c2a2f 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue
@@ -293,7 +293,6 @@ export default {
</template>
</registry-header>
<persisted-search
- class="gl-mb-5"
:sortable-fields="$options.searchConfig"
:default-order="$options.searchConfig[0].orderBy"
default-sort="desc"
diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/components/list/harbor_list_row.vue b/app/assets/javascripts/packages_and_registries/harbor_registry/components/list/harbor_list_row.vue
index bfe0c250dd9..cf1ee44b82e 100644
--- a/app/assets/javascripts/packages_and_registries/harbor_registry/components/list/harbor_list_row.vue
+++ b/app/assets/javascripts/packages_and_registries/harbor_registry/components/list/harbor_list_row.vue
@@ -44,12 +44,7 @@ export default {
<template>
<list-item v-bind="$attrs">
<template #left-primary>
- <router-link
- class="gl-text-body gl-font-weight-bold"
- data-testid="details-link"
- data-qa-selector="registry_image_content"
- :to="linkTo"
- >
+ <router-link class="gl-text-body gl-font-weight-bold" data-testid="details-link" :to="linkTo">
{{ item.name }}
</router-link>
<clipboard-button
diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/pages/details.vue b/app/assets/javascripts/packages_and_registries/harbor_registry/pages/details.vue
index bff32a124bc..bf0cdd5db10 100644
--- a/app/assets/javascripts/packages_and_registries/harbor_registry/pages/details.vue
+++ b/app/assets/javascripts/packages_and_registries/harbor_registry/pages/details.vue
@@ -135,7 +135,6 @@ export default {
<div class="gl-my-3">
<details-header :images-detail="imagesDetail" />
<persisted-search
- class="gl-mb-5"
:sortable-fields="$options.searchConfig.nameSortFields"
:default-order="$options.searchConfig.nameSortFields[0].orderBy"
default-sort="asc"
diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/app.vue b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/app.vue
index b49c448c478..a821a2483cd 100644
--- a/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/app.vue
+++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/app.vue
@@ -167,7 +167,7 @@ export default {
<gl-tabs>
<gl-tab :title="__('Detail')">
- <div data-qa-selector="package_information_content">
+ <div>
<package-history :package-entity="packageEntity" :project-name="projectName" />
<terraform-installation />
</div>
diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/details_title.vue b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/details_title.vue
index cd5f9f5a676..9d70391a8dd 100644
--- a/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/details_title.vue
+++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/details_title.vue
@@ -34,7 +34,7 @@ export default {
</script>
<template>
- <title-area :title="packageEntity.name" data-qa-selector="package_title">
+ <title-area :title="packageEntity.name">
<template #sub-header>
<gl-icon name="eye" class="gl-mr-3" />
<gl-sprintf :message="$options.i18n.packageInfo">
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/additional_metadata.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/additional_metadata.vue
index a3bbd569f41..937553e25cc 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/additional_metadata.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/additional_metadata.vue
@@ -1,6 +1,6 @@
<script>
import { GlAlert } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { s__ } from '~/locale';
import Composer from '~/packages_and_registries/package_registry/components/details/metadata/composer.vue';
import Conan from '~/packages_and_registries/package_registry/components/details/metadata/conan.vue';
@@ -41,9 +41,6 @@ export default {
apollo: {
packageMetadata: {
query: getPackageMetadataQuery,
- context: {
- isSingleRequest: true,
- },
variables() {
return {
id: this.packageId,
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_files.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_files.vue
index c8924e6548b..7b3acaf2ab6 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_files.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_files.vue
@@ -12,7 +12,7 @@ import {
GlSprintf,
GlKeysetPagination,
} from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { createAlert, VARIANT_SUCCESS, VARIANT_WARNING } from '~/alert';
import { NEXT, PREV } from '~/vue_shared/components/pagination/constants';
import { numberToHumanSize } from '~/lib/utils/number_utils';
@@ -92,9 +92,6 @@ export default {
apollo: {
packageFiles: {
query: getPackageFilesQuery,
- context: {
- isSingleRequest: true,
- },
variables() {
return this.queryVariables;
},
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_history.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_history.vue
index 663c361819e..32f94b82fa3 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_history.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_history.vue
@@ -1,7 +1,7 @@
<script>
import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
import { first } from 'lodash';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { truncateSha } from '~/lib/utils/text_utility';
import { s__, n__ } from '~/locale';
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_title.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_title.vue
index cdf03d64b27..db5e007b81f 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_title.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_title.vue
@@ -77,7 +77,6 @@ export default {
v-gl-resize-observer="checkBreakpoints"
:title="packageEntity.name"
:avatar="packageIcon"
- data-qa-selector="package_title"
>
<template #sub-header>
<div data-testid="sub-header" class="gl-display-flex gl-flex-wrap gl-gap-3">
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_versions_list.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_versions_list.vue
index 482249bc252..0c0001ba6d6 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_versions_list.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_versions_list.vue
@@ -1,6 +1,6 @@
<script>
import { GlAlert } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { n__ } from '~/locale';
import VersionRow from '~/packages_and_registries/package_registry/components/details/version_row.vue';
import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue';
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue
index a545ad1d09c..674683aa02f 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue
@@ -183,7 +183,12 @@ export default {
<span data-testid="right-secondary">
<gl-sprintf :message="publishedMessage">
<template v-if="isGroupPage" #projectName>
- <gl-link data-testid="root-link" :href="projectLink">{{ projectName }}</gl-link>
+ <gl-link
+ data-testid="root-link"
+ class="gl-text-decoration-underline"
+ :href="projectLink"
+ >{{ projectName }}</gl-link
+ >
</template>
<template #date>
<timeago-tooltip :time="packageEntity.createdAt" />
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/list/publish_method.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/list/publish_method.vue
index 8ecf433f3ab..2f74de9a615 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/list/publish_method.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/list/publish_method.vue
@@ -39,9 +39,12 @@ export default {
<span data-testid="pipeline-ref" class="gl-mr-2">{{ pipeline.ref }}</span>
<gl-icon name="commit" class="gl-mr-2" />
- <gl-link data-testid="pipeline-sha" :href="pipeline.commitPath" class="gl-mr-2">{{
- packageShaShort
- }}</gl-link>
+ <gl-link
+ data-testid="pipeline-sha"
+ :href="pipeline.commitPath"
+ class="gl-mr-2 gl-text-decoration-underline"
+ >{{ packageShaShort }}</gl-link
+ >
<clipboard-button
:text="pipeline.sha"
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/pages/list.vue b/app/assets/javascripts/packages_and_registries/package_registry/pages/list.vue
index a187c7a70d2..294c6baad1b 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/pages/list.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/pages/list.vue
@@ -171,7 +171,7 @@ export default {
/>
</template>
</package-title>
- <package-search class="gl-mb-5" @update="handleSearchUpdate" />
+ <package-search @update="handleSearchUpdate" />
<delete-packages
:refetch-queries="refetchQueriesData"
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/packages_settings.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/packages_settings.vue
index de087a8fcc5..e15f204dc6e 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/components/packages_settings.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/group/components/packages_settings.vue
@@ -3,6 +3,7 @@ import { GlTableLite, GlToggle } from '@gitlab/ui';
import {
GENERIC_PACKAGE_FORMAT,
MAVEN_PACKAGE_FORMAT,
+ NUGET_PACKAGE_FORMAT,
PACKAGE_FORMATS_TABLE_HEADER,
PACKAGE_SETTINGS_HEADER,
PACKAGE_SETTINGS_DESCRIPTION,
@@ -91,6 +92,18 @@ export default {
},
testid: 'generic-settings',
},
+ {
+ id: 'nuget-duplicated-settings-regex-input',
+ format: NUGET_PACKAGE_FORMAT,
+ duplicatesAllowed: this.packageSettings.nugetDuplicatesAllowed,
+ duplicateExceptionRegex: this.packageSettings.nugetDuplicateExceptionRegex,
+ duplicateExceptionRegexError: this.errors.nugetDuplicateExceptionRegex,
+ modelNames: {
+ allowed: 'nugetDuplicatesAllowed',
+ exception: 'nugetDuplicateExceptionRegex',
+ },
+ testid: 'nuget-settings',
+ },
];
},
},
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/constants.js b/app/assets/javascripts/packages_and_registries/settings/group/constants.js
index bfb57e3ac1c..54b337a4296 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/constants.js
+++ b/app/assets/javascripts/packages_and_registries/settings/group/constants.js
@@ -10,6 +10,7 @@ export const MAVEN_PACKAGE_FORMAT = s__('PackageRegistry|Maven');
export const NPM_PACKAGE_FORMAT = s__('PackageRegistry|npm');
export const PYPI_PACKAGE_FORMAT = s__('PackageRegistry|PyPI');
export const GENERIC_PACKAGE_FORMAT = s__('PackageRegistry|Generic');
+export const NUGET_PACKAGE_FORMAT = s__('PackageRegistry|NuGet');
export const DUPLICATES_TOGGLE_LABEL = s__('PackageRegistry|Allow duplicates');
export const DUPLICATES_SETTING_EXCEPTION_TITLE = __('Exceptions');
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/graphql/fragments/package_settings_fields.fragment.graphql b/app/assets/javascripts/packages_and_registries/settings/group/graphql/fragments/package_settings_fields.fragment.graphql
index 267e40263f2..0e36f48e9a6 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/graphql/fragments/package_settings_fields.fragment.graphql
+++ b/app/assets/javascripts/packages_and_registries/settings/group/graphql/fragments/package_settings_fields.fragment.graphql
@@ -3,6 +3,8 @@ fragment PackageSettingsFields on PackageSettings {
mavenDuplicateExceptionRegex
genericDuplicatesAllowed
genericDuplicateExceptionRegex
+ nugetDuplicatesAllowed
+ nugetDuplicateExceptionRegex
mavenPackageRequestsForwarding
lockMavenPackageRequestsForwarding
mavenPackageRequestsForwardingLocked
diff --git a/app/assets/javascripts/packages_and_registries/shared/components/package_path.vue b/app/assets/javascripts/packages_and_registries/shared/components/package_path.vue
index 0a94f67ea5e..18e95ee313e 100644
--- a/app/assets/javascripts/packages_and_registries/shared/components/package_path.vue
+++ b/app/assets/javascripts/packages_and_registries/shared/components/package_path.vue
@@ -47,7 +47,7 @@ export default {
</script>
<template>
- <div data-qa-selector="package_path" class="gl-display-flex gl-align-items-center">
+ <div class="gl-display-flex gl-align-items-center">
<gl-icon data-testid="base-icon" name="project" class="gl-mx-3 gl-min-w-0" />
<gl-link
diff --git a/app/assets/javascripts/packages_and_registries/shared/components/registry_list.vue b/app/assets/javascripts/packages_and_registries/shared/components/registry_list.vue
index f67bee77eb6..ac83f5fc1ad 100644
--- a/app/assets/javascripts/packages_and_registries/shared/components/registry_list.vue
+++ b/app/assets/javascripts/packages_and_registries/shared/components/registry_list.vue
@@ -92,7 +92,7 @@ export default {
<div>
<div
v-if="!hiddenDelete"
- class="gl-display-flex gl-justify-content-space-between gl-mb-3 gl-align-items-center"
+ class="gl-display-flex gl-justify-content-space-between gl-mb-3 gl-mt-5 gl-align-items-center"
>
<div class="gl-display-flex gl-align-items-center">
<gl-form-checkbox
diff --git a/app/assets/javascripts/pages/admin/application_settings/appearances/preview_sign_in/index.js b/app/assets/javascripts/pages/admin/application_settings/appearances/preview_sign_in/index.js
new file mode 100644
index 00000000000..e3524bfbee3
--- /dev/null
+++ b/app/assets/javascripts/pages/admin/application_settings/appearances/preview_sign_in/index.js
@@ -0,0 +1,3 @@
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
+
+renderGFM(document.body);
diff --git a/app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/index.js b/app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/index.js
index a5305777dd5..6ca9f39842a 100644
--- a/app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/index.js
+++ b/app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/index.js
@@ -1,3 +1,5 @@
import setup from '~/admin/application_settings/setup_metrics_and_profiling';
+import initServiceUsageData from '~/admin/application_settings/setup_service_usage_data';
setup();
+initServiceUsageData();
diff --git a/app/assets/javascripts/pages/admin/application_settings/service_usage_data/index.js b/app/assets/javascripts/pages/admin/application_settings/service_usage_data/index.js
deleted file mode 100644
index 8a12e753847..00000000000
--- a/app/assets/javascripts/pages/admin/application_settings/service_usage_data/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import initServiceUsageData from '~/admin/application_settings/setup_service_usage_data';
-
-initServiceUsageData();
diff --git a/app/assets/javascripts/pages/explore/catalog/index.js b/app/assets/javascripts/pages/explore/catalog/index.js
new file mode 100644
index 00000000000..fec738a93a6
--- /dev/null
+++ b/app/assets/javascripts/pages/explore/catalog/index.js
@@ -0,0 +1,3 @@
+import { initCatalog } from '~/ci/catalog/';
+
+initCatalog();
diff --git a/app/assets/javascripts/pages/import/bulk_imports/details/index.js b/app/assets/javascripts/pages/import/bulk_imports/details/index.js
new file mode 100644
index 00000000000..5c2571af60f
--- /dev/null
+++ b/app/assets/javascripts/pages/import/bulk_imports/details/index.js
@@ -0,0 +1,20 @@
+import Vue from 'vue';
+import BulkImportDetailsApp from '~/import/details/components/bulk_import_details_app.vue';
+
+export const initBulkImportDetails = () => {
+ const el = document.querySelector('.js-bulk-import-details');
+
+ if (!el) {
+ return null;
+ }
+
+ return new Vue({
+ el,
+ name: 'BulkImportDetailsRoot',
+ render(createElement) {
+ return createElement(BulkImportDetailsApp);
+ },
+ });
+};
+
+initBulkImportDetails();
diff --git a/app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue b/app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue
index 459546a5562..e912bfa4f92 100644
--- a/app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue
+++ b/app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue
@@ -14,13 +14,14 @@ import { createAlert } from '~/alert';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
import { joinPaths } from '~/lib/utils/url_utility';
import { getBulkImportsHistory } from '~/rest_api';
-import ImportStatus from '~/import_entities/components/import_status.vue';
+import ImportStatus from '~/import_entities/import_groups/components/import_status.vue';
import { StatusPoller } from '~/import_entities/import_groups/services/status_poller';
import { WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants';
import PaginationBar from '~/vue_shared/components/pagination_bar/pagination_bar.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { isImporting } from '../utils';
import { DEFAULT_ERROR } from '../utils/error_messages';
@@ -57,6 +58,8 @@ export default {
GlTooltip,
},
+ mixins: [glFeatureFlagMixin()],
+
inject: ['realtimeChangesPath'],
data() {
@@ -103,6 +106,10 @@ export default {
.filter((item) => isImporting(item.status))
.map((item) => item.bulk_import_id);
},
+
+ showDetailsLink() {
+ return this.glFeatures.bulkImportDetailsPage;
+ },
},
watch: {
@@ -225,12 +232,7 @@ export default {
:description="s__('BulkImport|Your imported groups and projects will appear here.')"
/>
<template v-else>
- <gl-table-lite
- :fields="$options.fields"
- :items="historyItems"
- data-qa-selector="import_history_table"
- class="gl-w-full"
- >
+ <gl-table-lite :fields="$options.fields" :items="historyItems" class="gl-w-full">
<template #cell(destination_name)="{ item }">
<gl-icon
v-gl-tooltip
@@ -252,14 +254,23 @@ export default {
<time-ago :time="value" />
</template>
<template #cell(status)="{ value, item, toggleDetails, detailsShowing }">
- <import-status :status="value" class="gl-display-inline-block gl-w-13" />
- <gl-button
- v-if="item.failures.length"
- class="gl-ml-3"
- :selected="detailsShowing"
- @click="toggleDetails"
- >{{ __('Details') }}</gl-button
+ <div
+ class="gl-display-flex gl-flex-direction-column gl-lg-flex-direction-row gl-align-items-flex-start gl-justify-content-space-between gl-gap-3"
>
+ <import-status
+ :id="item.bulk_import_id"
+ :entity-id="item.id"
+ :has-failures="item.has_failures"
+ :show-details-link="showDetailsLink"
+ :status="value"
+ />
+ <gl-button
+ v-if="!showDetailsLink && item.failures.length"
+ :selected="detailsShowing"
+ @click="toggleDetails"
+ >{{ __('Details') }}</gl-button
+ >
+ </div>
</template>
<template #row-details="{ item }">
<pre><code>{{ item.failures }}</code></pre>
diff --git a/app/assets/javascripts/pages/import/bulk_imports/history/index.js b/app/assets/javascripts/pages/import/bulk_imports/history/index.js
index cc12723572d..ac975db3667 100644
--- a/app/assets/javascripts/pages/import/bulk_imports/history/index.js
+++ b/app/assets/javascripts/pages/import/bulk_imports/history/index.js
@@ -4,13 +4,14 @@ import BulkImportHistoryApp from './components/bulk_imports_history_app.vue';
function mountImportHistoryApp(mountElement) {
if (!mountElement) return undefined;
- const { realtimeChangesPath } = mountElement.dataset;
+ const { realtimeChangesPath, detailsPath } = mountElement.dataset;
return new Vue({
el: mountElement,
name: 'BulkImportHistoryRoot',
provide: {
realtimeChangesPath,
+ detailsPath,
},
render(createElement) {
return createElement(BulkImportHistoryApp);
diff --git a/app/assets/javascripts/pages/import/history/components/import_history_app.vue b/app/assets/javascripts/pages/import/history/components/import_history_app.vue
index 938c2be89c5..9c0f937fe0e 100644
--- a/app/assets/javascripts/pages/import/history/components/import_history_app.vue
+++ b/app/assets/javascripts/pages/import/history/components/import_history_app.vue
@@ -11,11 +11,8 @@ import { DEFAULT_ERROR } from '../utils/error_messages';
import ImportErrorDetails from './import_error_details.vue';
const DEFAULT_PER_PAGE = 20;
-const DEFAULT_TH_CLASSES =
- 'gl-bg-transparent! gl-border-b-solid! gl-border-b-gray-200! gl-border-b-1! gl-p-5!';
const tableCell = (config) => ({
- thClass: DEFAULT_TH_CLASSES,
tdClass: (value, key, item) => {
return {
// eslint-disable-next-line no-underscore-dangle
@@ -57,12 +54,12 @@ export default {
tableCell({
key: 'source',
label: s__('BulkImport|Source'),
- thClass: `${DEFAULT_TH_CLASSES} gl-w-30p`,
+ thClass: 'gl-w-30p',
}),
tableCell({
key: 'destination',
label: s__('BulkImport|Destination'),
- thClass: `${DEFAULT_TH_CLASSES} gl-w-40p`,
+ thClass: 'gl-w-40p',
}),
tableCell({
key: 'created_at',
@@ -144,12 +141,7 @@ export default {
:description="s__('BulkImport|Your imported projects will appear here.')"
/>
<template v-else>
- <gl-table
- :fields="$options.fields"
- :items="historyItems"
- data-qa-selector="import_history_table"
- class="gl-w-full"
- >
+ <gl-table :fields="$options.fields" :items="historyItems" class="gl-w-full">
<template #cell(source)="{ item }">
<template v-if="item.import_url">
<gl-link
diff --git a/app/assets/javascripts/pages/organizations/organizations/users/index.js b/app/assets/javascripts/pages/organizations/organizations/users/index.js
new file mode 100644
index 00000000000..12d53207b22
--- /dev/null
+++ b/app/assets/javascripts/pages/organizations/organizations/users/index.js
@@ -0,0 +1,3 @@
+import { initOrganizationsUsers } from '~/organizations/users';
+
+initOrganizationsUsers();
diff --git a/app/assets/javascripts/pages/organizations/settings/general/index.js b/app/assets/javascripts/pages/organizations/settings/general/index.js
new file mode 100644
index 00000000000..5b74af6206e
--- /dev/null
+++ b/app/assets/javascripts/pages/organizations/settings/general/index.js
@@ -0,0 +1,3 @@
+import { initOrganizationsSettingsGeneral } from '~/organizations/settings/general';
+
+initOrganizationsSettingsGeneral();
diff --git a/app/assets/javascripts/pages/passwords/new/index.js b/app/assets/javascripts/pages/passwords/new/index.js
new file mode 100644
index 00000000000..e3524bfbee3
--- /dev/null
+++ b/app/assets/javascripts/pages/passwords/new/index.js
@@ -0,0 +1,3 @@
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
+
+renderGFM(document.body);
diff --git a/app/assets/javascripts/pages/profiles/preferences/show/index.js b/app/assets/javascripts/pages/profiles/preferences/show/index.js
index 76939434680..3668811bec7 100644
--- a/app/assets/javascripts/pages/profiles/preferences/show/index.js
+++ b/app/assets/javascripts/pages/profiles/preferences/show/index.js
@@ -1,5 +1,7 @@
import initProfilePreferences from '~/profile/preferences/profile_preferences_bundle';
import initProfilePreferencesDiffsColors from '~/profile/preferences/profile_preferences_diffs_colors';
+import { initHomeOrganizationSetting } from '~/organizations/profile/preferences';
initProfilePreferences();
initProfilePreferencesDiffsColors();
+initHomeOrganizationSetting();
diff --git a/app/assets/javascripts/pages/projects/blob/show/index.js b/app/assets/javascripts/pages/projects/blob/show/index.js
index 07662e4411e..d42fb10063e 100644
--- a/app/assets/javascripts/pages/projects/blob/show/index.js
+++ b/app/assets/javascripts/pages/projects/blob/show/index.js
@@ -153,9 +153,10 @@ const initForkInfo = () => {
initForkInfo();
const CommitPipelineStatusEl = document.querySelector('.js-commit-pipeline-status');
-const statusLink = document.querySelector('.commit-actions .ci-status-link');
-if (statusLink) {
- statusLink.remove();
+const legacyStatusBadge = document.querySelector('.js-ci-status-badge-legacy');
+
+if (legacyStatusBadge) {
+ legacyStatusBadge.remove();
// eslint-disable-next-line no-new
new Vue({
el: CommitPipelineStatusEl,
diff --git a/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue b/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue
index 9659c927fbf..e3d50e900ca 100644
--- a/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue
+++ b/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue
@@ -7,6 +7,7 @@ import {
GlFormGroup,
GlFormTextarea,
GlButton,
+ GlSprintf,
GlFormRadio,
GlFormRadioGroup,
} from '@gitlab/ui';
@@ -56,6 +57,7 @@ export default {
GlIcon,
GlLink,
GlButton,
+ GlSprintf,
GlFormInput,
GlFormTextarea,
GlFormGroup,
@@ -91,6 +93,9 @@ export default {
projectDescription: {
default: '',
},
+ projectDefaultBranch: {
+ default: '',
+ },
projectVisibility: {
default: '',
},
@@ -116,6 +121,7 @@ export default {
required: false,
skipValidation: true,
}),
+ branches: initFormField({ value: '', required: true, skipValidation: true }),
visibility: initFormField({ value: null }),
},
};
@@ -168,6 +174,18 @@ export default {
return allowedLevels;
},
+ branchesOptions() {
+ return [
+ {
+ text: s__('ForkProject|All branches'),
+ value: '',
+ },
+ {
+ text: s__(`ForkProject|Only the default branch %{defaultBranch}`),
+ value: this.projectDefaultBranch,
+ },
+ ];
+ },
visibilityLevels() {
return [
{
@@ -245,7 +263,7 @@ export default {
this.form.showValidation = false;
const { projectId } = this;
- const { name, slug, description, visibility, namespace } = this.form.fields;
+ const { name, slug, description, branches, visibility, namespace } = this.form.fields;
const postParams = {
id: projectId,
@@ -253,6 +271,7 @@ export default {
namespace_id: namespace.value.id,
path: slug.value,
description: description.value,
+ branches: branches.value,
visibility: visibility.value,
};
@@ -263,6 +282,7 @@ export default {
const { data } = await axios.post(url, postParams);
redirectTo(data.web_url); // eslint-disable-line import/no-deprecated
} catch (error) {
+ this.isSaving = false;
createAlert({
message: s__(
'ForkProject|An error occurred while forking the project. Please try again.',
@@ -348,6 +368,34 @@ export default {
/>
</gl-form-group>
+ <gl-form-group>
+ <label>
+ {{ s__('ForkProject|Branches to include') }}
+ </label>
+ <gl-form-radio-group
+ v-model="form.fields.branches.value"
+ data-testid="fork-branches-radio-group"
+ name="branches"
+ :aria-label="__('branches')"
+ required
+ >
+ <gl-form-radio
+ v-for="{ text, value } in branchesOptions"
+ :key="value"
+ :value="value"
+ :data-testid="`radio-${value}`"
+ >
+ <div>
+ <gl-sprintf :message="text">
+ <template #defaultBranch>
+ <code class="gl-ml-2">{{ projectDefaultBranch }}</code>
+ </template>
+ </gl-sprintf>
+ </div>
+ </gl-form-radio>
+ </gl-form-radio-group>
+ </gl-form-group>
+
<gl-form-group
v-validation:[form.showValidation]
:invalid-feedback="s__('ForkProject|Please select a visibility level')"
diff --git a/app/assets/javascripts/pages/projects/forks/new/index.js b/app/assets/javascripts/pages/projects/forks/new/index.js
index a31b8b1a1f4..694914e9154 100644
--- a/app/assets/javascripts/pages/projects/forks/new/index.js
+++ b/app/assets/javascripts/pages/projects/forks/new/index.js
@@ -15,6 +15,7 @@ const {
projectId,
projectName,
projectPath,
+ projectDefaultBranch,
projectDescription,
projectVisibility,
restrictedVisibilityLevels,
@@ -38,6 +39,7 @@ new Vue({
projectName,
projectPath,
projectDescription,
+ projectDefaultBranch,
projectVisibility,
restrictedVisibilityLevels: JSON.parse(restrictedVisibilityLevels),
},
diff --git a/app/assets/javascripts/pages/projects/merge_requests/page.js b/app/assets/javascripts/pages/projects/merge_requests/page.js
index fb243d01dc6..a9d281fc899 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/page.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/page.js
@@ -28,7 +28,7 @@ requestIdleCallback(() => {
if (el) {
const { data } = el.dataset;
- const { iid, projectPath, title, tabs, isFluidLayout } = JSON.parse(data);
+ const { iid, projectPath, title, tabs, isFluidLayout, sourceProjectPath } = JSON.parse(data);
// eslint-disable-next-line no-new
new Vue({
@@ -42,6 +42,7 @@ requestIdleCallback(() => {
title,
tabs,
isFluidLayout: parseBoolean(isFluidLayout),
+ sourceProjectPath,
},
render(h) {
return h(StickyHeader);
diff --git a/app/assets/javascripts/pages/projects/ml/model_versions/show/index.js b/app/assets/javascripts/pages/projects/ml/model_versions/show/index.js
new file mode 100644
index 00000000000..1a2b85d7e16
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/ml/model_versions/show/index.js
@@ -0,0 +1,4 @@
+import { initSimpleApp } from '~/helpers/init_simple_app_helper';
+import { ShowMlModelVersion } from '~/ml/model_registry/apps';
+
+initSimpleApp('#js-mount-show-ml-model-version', ShowMlModelVersion);
diff --git a/app/assets/javascripts/pages/projects/ml/models/index/index.js b/app/assets/javascripts/pages/projects/ml/models/index/index.js
index 62d326f43a5..3f8ef4910a7 100644
--- a/app/assets/javascripts/pages/projects/ml/models/index/index.js
+++ b/app/assets/javascripts/pages/projects/ml/models/index/index.js
@@ -1,4 +1,4 @@
import { initSimpleApp } from '~/helpers/init_simple_app_helper';
-import MlModelsIndex from '~/ml/model_registry/routes/models/index';
+import { IndexMlModels } from '~/ml/model_registry/apps';
-initSimpleApp('#js-index-ml-models', MlModelsIndex);
+initSimpleApp('#js-index-ml-models', IndexMlModels);
diff --git a/app/assets/javascripts/pages/projects/ml/models/show/index.js b/app/assets/javascripts/pages/projects/ml/models/show/index.js
index 87ee5c851f6..c8e25e0f0e8 100644
--- a/app/assets/javascripts/pages/projects/ml/models/show/index.js
+++ b/app/assets/javascripts/pages/projects/ml/models/show/index.js
@@ -1,4 +1,4 @@
import { initSimpleApp } from '~/helpers/init_simple_app_helper';
import { ShowMlModel } from '~/ml/model_registry/apps';
-initSimpleApp('#js-mount-show-ml-model', ShowMlModel);
+initSimpleApp('#js-mount-show-ml-model', ShowMlModel, { withApolloProvider: true });
diff --git a/app/assets/javascripts/pages/projects/product_analytics/graphs/index.js b/app/assets/javascripts/pages/projects/product_analytics/graphs/index.js
deleted file mode 100644
index ba03fccdb03..00000000000
--- a/app/assets/javascripts/pages/projects/product_analytics/graphs/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import initActivityCharts from '~/analytics/product_analytics/activity_charts_bundle';
-
-initActivityCharts();
diff --git a/app/assets/javascripts/pages/registrations/new/index.js b/app/assets/javascripts/pages/registrations/new/index.js
index 90a9c9e7279..54974e878c3 100644
--- a/app/assets/javascripts/pages/registrations/new/index.js
+++ b/app/assets/javascripts/pages/registrations/new/index.js
@@ -5,6 +5,7 @@ import EmailFormatValidator from '~/pages/sessions/new/email_format_validator';
import { initLanguageSwitcher } from '~/language_switcher';
import { initPasswordInput } from '~/authentication/password';
import Tracking from '~/tracking';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
new UsernameValidator(); // eslint-disable-line no-new
new LengthValidator(); // eslint-disable-line no-new
@@ -17,3 +18,4 @@ Tracking.enableFormTracking({
initLanguageSwitcher();
initPasswordInput();
+renderGFM(document.body);
diff --git a/app/assets/javascripts/pages/sessions/new/index.js b/app/assets/javascripts/pages/sessions/new/index.js
index 1d5d885753c..32df2911a48 100644
--- a/app/assets/javascripts/pages/sessions/new/index.js
+++ b/app/assets/javascripts/pages/sessions/new/index.js
@@ -4,6 +4,7 @@ import NoEmojiValidator from '~/emoji/no_emoji_validator';
import { initLanguageSwitcher } from '~/language_switcher';
import LengthValidator from '~/validators/length_validator';
import mountEmailVerificationApplication from '~/sessions/new';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
import OAuthRememberMe from './oauth_remember_me';
import preserveUrlFragment from './preserve_url_fragment';
import SigninTabsMemoizer from './signin_tabs_memoizer';
@@ -24,3 +25,4 @@ preserveUrlFragment(window.location.hash);
initVueAlerts();
initLanguageSwitcher();
mountEmailVerificationApplication();
+renderGFM(document.body);
diff --git a/app/assets/javascripts/pages/users/index.js b/app/assets/javascripts/pages/users/index.js
index af55a5dc01a..d2c31314bba 100644
--- a/app/assets/javascripts/pages/users/index.js
+++ b/app/assets/javascripts/pages/users/index.js
@@ -1,11 +1,15 @@
import $ from 'jquery';
import { setCookie } from '~/lib/utils/common_utils';
import UserCallout from '~/user_callout';
-import { initReportAbuse } from '~/users/profile';
-import { initProfileTabs } from '~/profile';
+import { initProfileTabs, initUserAchievements } from '~/profile';
+import { initUserActionsApp } from '~/users/profile/actions';
import UserTabs from './user_tabs';
function initUserProfile(action) {
+ // TODO: Remove both Vue and legacy JS tabs code/feature flag uses with the
+ // removal of the old navigation.
+ // See https://gitlab.com/groups/gitlab-org/-/epics/11875.
+
if (gon.features?.profileTabsVue) {
initProfileTabs();
} else {
@@ -24,5 +28,6 @@ function initUserProfile(action) {
const page = $('body').attr('data-page');
const action = page.split(':')[1];
initUserProfile(action);
+initUserAchievements();
+initUserActionsApp();
new UserCallout(); // eslint-disable-line no-new
-initReportAbuse();
diff --git a/app/assets/javascripts/pages/users/show/index.js b/app/assets/javascripts/pages/users/show/index.js
deleted file mode 100644
index 7d612d6cc4e..00000000000
--- a/app/assets/javascripts/pages/users/show/index.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import { initUserAchievements } from '~/profile';
-import { initUserActionsApp } from '~/users/profile/actions';
-
-initUserAchievements();
-initUserActionsApp();
diff --git a/app/assets/javascripts/pages/users/user_tabs.js b/app/assets/javascripts/pages/users/user_tabs.js
index 430022f9a9b..79eb3902116 100644
--- a/app/assets/javascripts/pages/users/user_tabs.js
+++ b/app/assets/javascripts/pages/users/user_tabs.js
@@ -1,3 +1,6 @@
+// TODO: Remove this with the removal of the old navigation.
+// See https://gitlab.com/groups/gitlab-org/-/epics/11875.
+
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import $ from 'jquery';
import Activities from '~/activities';
@@ -194,7 +197,7 @@ export default class UserTabs {
this.loadActivityCalendar();
UserTabs.renderMostRecentBlocks('#js-overview .activities-block', {
- requestParams: { limit: 10 },
+ requestParams: { limit: 15 },
});
UserTabs.renderMostRecentBlocks('#js-overview .projects-block', {
requestParams: { limit: 10, skip_pagination: true, skip_namespace: true, compact_mode: true },
diff --git a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
index 720c1e0d7f2..c5f8fd1904f 100644
--- a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
+++ b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
@@ -61,11 +61,6 @@ export default {
keys: ['feature', 'request'],
},
{
- metric: 'rugged',
- header: s__('PerformanceBar|Rugged calls'),
- keys: ['feature', 'args'],
- },
- {
metric: 'redis',
header: s__('PerformanceBar|Redis calls'),
keys: ['cmd', 'instance'],
diff --git a/app/assets/javascripts/persistent_user_callouts.js b/app/assets/javascripts/persistent_user_callouts.js
index cea01852630..bba8e1f7ba5 100644
--- a/app/assets/javascripts/persistent_user_callouts.js
+++ b/app/assets/javascripts/persistent_user_callouts.js
@@ -23,7 +23,7 @@ const PERSISTENT_USER_CALLOUTS = [
'.js-geo-migrate-hashed-storage-callout',
'.js-unlimited-members-during-trial-alert',
'.js-branch-rules-info-callout',
- '.js-new-navigation-callout',
+ '.js-new-nav-for-everyone-callout',
'.js-namespace-over-storage-users-combined-alert',
];
diff --git a/app/assets/javascripts/pipeline_wizard/components/wrapper.vue b/app/assets/javascripts/pipeline_wizard/components/wrapper.vue
index ab837d04d9a..43e70046cfb 100644
--- a/app/assets/javascripts/pipeline_wizard/components/wrapper.vue
+++ b/app/assets/javascripts/pipeline_wizard/components/wrapper.vue
@@ -14,8 +14,7 @@ import CommitStep from './commit.vue';
export const i18n = {
stepNofN: __('Step %{currentStep} of %{stepCount}'),
draft: __('Draft: %{filename}'),
- overlayMessage: __(`Start inputting changes and we will generate a
- YAML-file for you to add to your repository`),
+ overlayMessage: __(`Enter values to populate the .gitlab-ci.yml configuration file.`),
};
const trackingMixin = Tracking.mixin();
diff --git a/app/assets/javascripts/pipeline_wizard/pipeline_wizard.vue b/app/assets/javascripts/pipeline_wizard/pipeline_wizard.vue
index 5a93de3b1be..3676ba96254 100644
--- a/app/assets/javascripts/pipeline_wizard/pipeline_wizard.vue
+++ b/app/assets/javascripts/pipeline_wizard/pipeline_wizard.vue
@@ -1,5 +1,6 @@
<script>
import { parseDocument } from 'yaml';
+import { DEFAULT_CI_CONFIG_PATH } from '~/lib/utils/constants';
import WizardWrapper from './components/wrapper.vue';
export default {
@@ -23,7 +24,7 @@ export default {
defaultFilename: {
type: String,
required: false,
- default: '.gitlab-ci.yml',
+ default: DEFAULT_CI_CONFIG_PATH,
},
},
computed: {
diff --git a/app/assets/javascripts/pipeline_wizard/templates/pages.yml b/app/assets/javascripts/pipeline_wizard/templates/pages.yml
index 9d7936f2f5a..8eecd51fe27 100644
--- a/app/assets/javascripts/pipeline_wizard/templates/pages.yml
+++ b/app/assets/javascripts/pipeline_wizard/templates/pages.yml
@@ -1,12 +1,10 @@
id: gitlab/pages
-title: Get started with Pages
-description: "GitLab Pages lets you deploy static websites in minutes. All you
- need is a .gitlab-ci.yml file. Follow the below steps to
- create one for your app now."
+title: Get started with GitLab Pages
+description: "Use GitLab Pages to deploy your static website. Follow these steps to create the configuration file, .gitlab-ci.yml, and start a pipeline to deploy the site."
steps:
- inputs:
- label: Select your build image
- description: A Docker image that we can use to build your image
+ description: A Docker image, used to create an instance where your job runs.
placeholder: node:lts
widget: text
target: $BUILD_IMAGE
@@ -14,18 +12,15 @@ steps:
pattern: "(?:[a-z]+/)?([a-z]+)(?::[0-9]+)?"
invalid-feedback: Please enter a valid docker image
- widget: checklist
- title: "Before we begin, please check:"
items:
- - text: The app's built output files are in a folder named "public"
- help: GitLab Pages will only publish files in that folder.
- You may need to adjust your build engine's config.
+ - text: The application files are in the `public` folder
+ help: GitLab Pages publishes files in the public folder only. If needed, change your jobs to send output to this folder.
template:
# The Docker image that will be used to build your app
image: $BUILD_IMAGE
- inputs:
- label: Installation Steps
- description: "Enter the steps that need to run to set up a local build
- environment, for example installing dependencies."
+ description: "Enter steps to set up a local build environment, like installing dependencies."
placeholder: npm ci
widget: list
target: $INSTALLATION_STEPS
@@ -34,8 +29,7 @@ steps:
before_script: $INSTALLATION_STEPS
- inputs:
- label: Build Steps
- description: "Enter the steps necessary to build a production version of
- your application."
+ description: "Enter steps to build a production version of your application."
widget: list
target: $BUILD_STEPS
template:
diff --git a/app/assets/javascripts/profile/edit/components/profile_edit_app.vue b/app/assets/javascripts/profile/edit/components/profile_edit_app.vue
index 6b39f137880..815b8742500 100644
--- a/app/assets/javascripts/profile/edit/components/profile_edit_app.vue
+++ b/app/assets/javascripts/profile/edit/components/profile_edit_app.vue
@@ -109,8 +109,11 @@ export default {
async syncHeaderAvatars() {
const dataURL = await readFileAsDataURL(this.avatarBlob);
- // TODO: implement sync for super sidebar
- ['.header-user-avatar', '.js-sidebar-user-avatar'].forEach((selector) => {
+ const elements = gon?.use_new_navigation
+ ? ['[data-testid="user-dropdown"] .gl-avatar']
+ : ['.header-user-avatar', '.js-sidebar-user-avatar'];
+
+ elements.forEach((selector) => {
const node = document.querySelector(selector);
if (!node) return;
diff --git a/app/assets/javascripts/profile/preferences/components/profile_preferences.vue b/app/assets/javascripts/profile/preferences/components/profile_preferences.vue
index aa30192b74b..2fc1f99c183 100644
--- a/app/assets/javascripts/profile/preferences/components/profile_preferences.vue
+++ b/app/assets/javascripts/profile/preferences/components/profile_preferences.vue
@@ -5,9 +5,9 @@ import { INTEGRATION_VIEW_CONFIGS, i18n } from '../constants';
import IntegrationView from './integration_view.vue';
function updateClasses(bodyClasses = '', applicationTheme, layout) {
- // Remove body class for any previous theme, re-add current one
- document.body.classList.remove(...bodyClasses.split(' '));
- document.body.classList.add(applicationTheme);
+ // Remove documentElement class for any previous theme, re-add current one
+ document.documentElement.classList.remove(...bodyClasses.split(' '));
+ document.documentElement.classList.add(applicationTheme);
// Toggle container-fluid class
if (layout === 'fluid') {
diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js
index 947bf7acd5c..2ccb360c7c1 100644
--- a/app/assets/javascripts/profile/profile.js
+++ b/app/assets/javascripts/profile/profile.js
@@ -89,8 +89,12 @@ export default class Profile {
}
updateHeaderAvatar() {
- $('.header-user-avatar').attr('src', this.avatarGlCrop.dataURL);
- $('.js-sidebar-user-avatar').attr('src', this.avatarGlCrop.dataURL);
+ if (gon?.use_new_navigation) {
+ $('[data-testid="user-dropdown"] .gl-avatar').attr('src', this.avatarGlCrop.dataURL);
+ } else {
+ $('.header-user-avatar').attr('src', this.avatarGlCrop.dataURL);
+ $('.js-sidebar-user-avatar').attr('src', this.avatarGlCrop.dataURL);
+ }
}
setRepoRadio() {
diff --git a/app/assets/javascripts/projects/commit/components/commit_options_dropdown.vue b/app/assets/javascripts/projects/commit/components/commit_options_dropdown.vue
index 7c00ce45b3a..377310b087e 100644
--- a/app/assets/javascripts/projects/commit/components/commit_options_dropdown.vue
+++ b/app/assets/javascripts/projects/commit/components/commit_options_dropdown.vue
@@ -51,7 +51,6 @@ export default {
text: s__('ChangeTypeAction|Cherry-pick'),
extraAttrs: {
'data-testid': 'cherry-pick-link',
- 'data-qa-selector': 'cherry_pick_button',
},
action: () => this.showModal(OPEN_CHERRY_PICK_MODAL),
};
@@ -62,7 +61,6 @@ export default {
text: s__('ChangeTypeAction|Revert'),
extraAttrs: {
'data-testid': 'revert-link',
- 'data-qa-selector': 'revert_button',
},
action: () => this.showModal(OPEN_REVERT_MODAL),
};
@@ -85,7 +83,6 @@ export default {
download: '',
rel: 'nofollow',
'data-testid': 'plain-diff-link',
- 'data-qa-selector': 'plain_diff',
},
};
},
@@ -97,7 +94,6 @@ export default {
download: '',
rel: 'nofollow',
'data-testid': 'email-patches-link',
- 'data-qa-selector': 'email_patches',
},
};
},
@@ -148,8 +144,7 @@ export default {
:toggle-text="__('Options')"
right
data-testid="commit-options-dropdown"
- data-qa-selector="options_button"
- class="gl-xs-w-full gl-line-height-20"
+ class="gl-line-height-20"
>
<gl-disclosure-dropdown-group :group="optionsGroup" @action="closeDropdown" />
diff --git a/app/assets/javascripts/projects/commit/components/form_modal.vue b/app/assets/javascripts/projects/commit/components/form_modal.vue
index 44b8ccb57ca..d1e78084b9f 100644
--- a/app/assets/javascripts/projects/commit/components/form_modal.vue
+++ b/app/assets/javascripts/projects/commit/components/form_modal.vue
@@ -57,7 +57,6 @@ export default {
variant: 'confirm',
category: 'primary',
'data-testid': 'submit-commit',
- 'data-qa-selector': 'submit_commit_button',
},
},
actionCancel: {
@@ -74,7 +73,6 @@ export default {
'branchCollaboration',
'modalTitle',
'existingBranch',
- 'prependedText',
'targetProjectId',
'targetProjectName',
'branchesEndpoint',
diff --git a/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_status.vue b/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_status.vue
index 0feaf8db82b..a4851b4fe4b 100644
--- a/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_status.vue
+++ b/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_status.vue
@@ -1,6 +1,6 @@
<script>
import { GlLoadingIcon } from '@gitlab/ui';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { createAlert } from '~/alert';
import { getQueryHeaders, toggleQueryPollingByVisibility } from '~/ci/pipeline_details/graph/utils';
import getLatestPipelineStatusQuery from '../graphql/queries/get_latest_pipeline_status.query.graphql';
@@ -9,7 +9,7 @@ import { COMMIT_BOX_POLL_INTERVAL, PIPELINE_STATUS_FETCH_ERROR } from '../consta
export default {
PIPELINE_STATUS_FETCH_ERROR,
components: {
- CiBadgeLink,
+ CiIcon,
GlLoadingIcon,
},
inject: {
@@ -63,12 +63,6 @@ export default {
<template>
<div class="gl-display-inline-block gl-vertical-align-middle gl-mr-2">
<gl-loading-icon v-if="loading" />
- <ci-badge-link
- v-else
- :status="pipelineStatus"
- :details-path="pipelineStatus.detailsPath"
- size="md"
- :show-text="false"
- />
+ <ci-icon v-else :status="pipelineStatus" />
</div>
</template>
diff --git a/app/assets/javascripts/projects/commit_box/info/init_commit_pipeline_mini_graph.js b/app/assets/javascripts/projects/commit_box/info/init_commit_pipeline_mini_graph.js
index c206e648561..24b7130e765 100644
--- a/app/assets/javascripts/projects/commit_box/info/init_commit_pipeline_mini_graph.js
+++ b/app/assets/javascripts/projects/commit_box/info/init_commit_pipeline_mini_graph.js
@@ -5,7 +5,7 @@ import createDefaultClient from '~/lib/graphql';
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient({}, { useGet: true }),
+ defaultClient: createDefaultClient(),
});
export const initCommitPipelineMiniGraph = async (selector = '.js-commit-pipeline-mini-graph') => {
diff --git a/app/assets/javascripts/projects/commit_box/info/init_commit_pipeline_status.js b/app/assets/javascripts/projects/commit_box/info/init_commit_pipeline_status.js
index d5e62531283..079f74dc8a2 100644
--- a/app/assets/javascripts/projects/commit_box/info/init_commit_pipeline_status.js
+++ b/app/assets/javascripts/projects/commit_box/info/init_commit_pipeline_status.js
@@ -6,7 +6,7 @@ import CommitBoxPipelineStatus from './components/commit_box_pipeline_status.vue
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient({}, { useGet: true }),
+ defaultClient: createDefaultClient(),
});
export default (selector = '.js-commit-pipeline-status') => {
diff --git a/app/assets/javascripts/projects/commits/store/actions.js b/app/assets/javascripts/projects/commits/store/actions.js
index 5175f7f9151..7d04e9a15a3 100644
--- a/app/assets/javascripts/projects/commits/store/actions.js
+++ b/app/assets/javascripts/projects/commits/store/actions.js
@@ -1,4 +1,4 @@
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { joinPaths } from '~/lib/utils/url_utility';
diff --git a/app/assets/javascripts/projects/default_project_templates.js b/app/assets/javascripts/projects/default_project_templates.js
index 5bbc881952f..e3599c87616 100644
--- a/app/assets/javascripts/projects/default_project_templates.js
+++ b/app/assets/javascripts/projects/default_project_templates.js
@@ -121,4 +121,8 @@ export default {
text: s__('ProjectTemplates|Laravel Framework'),
icon: '.template-option .icon-laravel',
},
+ astro_tailwind: {
+ text: s__('ProjectTemplates|Astro Tailwind'),
+ icon: '.template-option .icon-gitlab_logo',
+ },
};
diff --git a/app/assets/javascripts/projects/new/components/new_project_url_select.vue b/app/assets/javascripts/projects/new/components/new_project_url_select.vue
index ef2a2aa5526..84a2ddfce07 100644
--- a/app/assets/javascripts/projects/new/components/new_project_url_select.vue
+++ b/app/assets/javascripts/projects/new/components/new_project_url_select.vue
@@ -67,7 +67,7 @@ export default {
}
: this.$options.emptyNameSpace,
shouldSkipQuery: true,
- userNamespaceId: this.userNamespaceId,
+ userNamespaceUniqueId: this.userNamespaceId,
};
},
computed: {
@@ -186,7 +186,7 @@ export default {
{{ group.fullPath }}
</gl-dropdown-item>
</template>
- <template v-if="hasNamespaceMatches && userNamespaceId">
+ <template v-if="hasNamespaceMatches && userNamespaceUniqueId">
<gl-dropdown-section-header>{{ __('Users') }}</gl-dropdown-section-header>
<gl-dropdown-item @click="handleDropdownItemClick(userNamespace)">
{{ userNamespace.fullPath }}
@@ -202,7 +202,7 @@ export default {
:id="inputId"
type="hidden"
:name="inputName"
- :value="selectedNamespace.id || userNamespaceId"
+ :value="selectedNamespace.id || userNamespaceUniqueId"
/>
</gl-button-group>
</template>
diff --git a/app/assets/javascripts/projects/pipelines/charts/components/statistics_list.vue b/app/assets/javascripts/projects/pipelines/charts/components/statistics_list.vue
index 5383a6cdddf..f921b2dfdd6 100644
--- a/app/assets/javascripts/projects/pipelines/charts/components/statistics_list.vue
+++ b/app/assets/javascripts/projects/pipelines/charts/components/statistics_list.vue
@@ -1,7 +1,7 @@
<script>
import { GlLink } from '@gitlab/ui';
import { SUPPORTED_FORMATS, getFormatter } from '~/lib/utils/unit_format';
-import { s__, n__ } from '~/locale';
+import { s__, n__, formatNumber } from '~/locale';
const defaultPrecision = 2;
@@ -22,25 +22,25 @@ export default {
},
computed: {
statistics() {
- const formatter = getFormatter(SUPPORTED_FORMATS.percentHundred);
+ const formatPercent = getFormatter(SUPPORTED_FORMATS.percentHundred);
return [
{
title: s__('PipelineCharts|Total:'),
- value: n__('1 pipeline', '%d pipelines', this.counts.total),
+ value: n__('1 pipeline', '%d pipelines', formatNumber(this.counts.total)),
},
{
title: s__('PipelineCharts|Successful:'),
- value: n__('1 pipeline', '%d pipelines', this.counts.success),
+ value: n__('1 pipeline', '%d pipelines', formatNumber(this.counts.success)),
},
{
title: s__('PipelineCharts|Failed:'),
- value: n__('1 pipeline', '%d pipelines', this.counts.failed),
+ value: n__('1 pipeline', '%d pipelines', formatNumber(this.counts.failed)),
link: this.failedPipelinesLink,
},
{
title: s__('PipelineCharts|Success ratio:'),
- value: formatter(this.counts.successRatio, defaultPrecision),
+ value: formatPercent(this.counts.successRatio, defaultPrecision),
},
];
},
diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue b/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue
index dbcb77b67f3..becd373c5f1 100644
--- a/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue
+++ b/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue
@@ -187,7 +187,7 @@ export default {
:roles="pushAccessLevels.roles"
:users="pushAccessLevels.users"
:groups="pushAccessLevels.groups"
- data-qa-selector="allowed_to_push_content"
+ data-testid="allowed-to-push-content"
/>
<!-- Allowed to merge -->
@@ -198,7 +198,7 @@ export default {
:roles="mergeAccessLevels.roles"
:users="mergeAccessLevels.users"
:groups="mergeAccessLevels.groups"
- data-qa-selector="allowed_to_merge_content"
+ data-testid="allowed-to-merge-content"
/>
<!-- Force push -->
diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/view/protection_row.vue b/app/assets/javascripts/projects/settings/branch_rules/components/view/protection_row.vue
index 3a5b3409596..366c69556f2 100644
--- a/app/assets/javascripts/projects/settings/branch_rules/components/view/protection_row.vue
+++ b/app/assets/javascripts/projects/settings/branch_rules/components/view/protection_row.vue
@@ -105,7 +105,6 @@ export default {
v-for="(item, index) in accessLevels"
:key="index"
data-testid="access-level"
- data-qa-selector="access_level_content"
:data-qa-role="item.accessLevelDescription"
>
<span v-if="commaSeparateList && index > 0" data-testid="comma-separator">,</span>
diff --git a/app/assets/javascripts/projects/settings/components/default_branch_selector.vue b/app/assets/javascripts/projects/settings/components/default_branch_selector.vue
index fee2f591216..f5fb72e84bc 100644
--- a/app/assets/javascripts/projects/settings/components/default_branch_selector.vue
+++ b/app/assets/javascripts/projects/settings/components/default_branch_selector.vue
@@ -33,6 +33,5 @@ export default {
:translations="$options.i18n"
name="project[default_branch]"
data-testid="default-branch-dropdown"
- data-qa-selector="default_branch_dropdown"
/>
</template>
diff --git a/app/assets/javascripts/projects/settings/init_access_dropdown.js b/app/assets/javascripts/projects/settings/init_access_dropdown.js
index 67afbee3854..b02a33675ee 100644
--- a/app/assets/javascripts/projects/settings/init_access_dropdown.js
+++ b/app/assets/javascripts/projects/settings/init_access_dropdown.js
@@ -1,5 +1,5 @@
-import * as Sentry from '@sentry/browser';
import Vue from 'vue';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import AccessDropdown from './components/access_dropdown.vue';
export const initAccessDropdown = (el, options) => {
@@ -22,6 +22,7 @@ export const initAccessDropdown = (el, options) => {
data() {
return { preselected };
},
+ disabled,
methods: {
setPreselectedItems(items) {
this.preselected = items;
diff --git a/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue b/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue
index 7753b850744..7d9ad83a1c6 100644
--- a/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue
+++ b/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue
@@ -76,7 +76,7 @@ export default {
v-gl-modal="$options.modalId"
size="small"
class="gl-ml-3"
- data-qa-selector="add_branch_rule_button"
+ data-testid="add-branch-rule-button"
>{{ $options.i18n.addBranchRule }}</gl-button
>
</template>
diff --git a/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue b/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue
index f45a5b12db6..0a5fa288828 100644
--- a/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue
+++ b/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue
@@ -156,7 +156,7 @@ export default {
<li>
<div
class="gl-display-flex gl-justify-content-space-between"
- data-qa-selector="branch_content"
+ data-testid="branch-content"
:data-qa-branch-name="name"
>
<div>
@@ -178,7 +178,7 @@ export default {
class="gl-align-self-start"
category="tertiary"
size="small"
- data-qa-selector="details_button"
+ data-testid="details-button"
:href="detailsPath"
>
{{ $options.i18n.detailsButtonLabel }}</gl-button
diff --git a/app/assets/javascripts/projects/settings_service_desk/components/custom_email_form.vue b/app/assets/javascripts/projects/settings_service_desk/components/custom_email_form.vue
index 09bc275cbd4..6f22af4bd26 100644
--- a/app/assets/javascripts/projects/settings_service_desk/components/custom_email_form.vue
+++ b/app/assets/javascripts/projects/settings_service_desk/components/custom_email_form.vue
@@ -6,10 +6,16 @@ import {
GlFormInputGroup,
GlFormInput,
GlLink,
+ GlFormSelect,
GlSprintf,
} from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
-import { isEmptyValue, hasMinimumLength, isIntegerGreaterThan, isEmail } from '~/lib/utils/forms';
+import {
+ isEmptyValue,
+ hasMinimumLength,
+ isIntegerGreaterThan,
+ isServiceDeskSettingEmail,
+} from '~/lib/utils/forms';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import {
I18N_FORM_INTRODUCTION_PARAGRAPH,
@@ -23,6 +29,11 @@ import {
I18N_FORM_SMTP_USERNAME_LABEL,
I18N_FORM_SMTP_PASSWORD_LABEL,
I18N_FORM_SMTP_PASSWORD_DESCRIPTION,
+ I18N_FORM_SMTP_AUTHENTICATION_LABEL,
+ I18N_FORM_SMTP_AUTHENTICATION_NONE,
+ I18N_FORM_SMTP_AUTHENTICATION_PLAIN,
+ I18N_FORM_SMTP_AUTHENTICATION_LOGIN,
+ I18N_FORM_SMTP_AUTHENTICATION_CRAM_MD5,
I18N_FORM_SUBMIT_LABEL,
I18N_FORM_INVALID_FEEDBACK_CUSTOM_EMAIL,
I18N_FORM_INVALID_FEEDBACK_SMTP_ADDRESS,
@@ -42,6 +53,7 @@ export default {
GlFormGroup,
GlFormInputGroup,
GlFormInput,
+ GlFormSelect,
GlLink,
GlSprintf,
},
@@ -56,6 +68,11 @@ export default {
I18N_FORM_SMTP_USERNAME_LABEL,
I18N_FORM_SMTP_PASSWORD_LABEL,
I18N_FORM_SMTP_PASSWORD_DESCRIPTION,
+ I18N_FORM_SMTP_AUTHENTICATION_LABEL,
+ I18N_FORM_SMTP_AUTHENTICATION_NONE,
+ I18N_FORM_SMTP_AUTHENTICATION_PLAIN,
+ I18N_FORM_SMTP_AUTHENTICATION_LOGIN,
+ I18N_FORM_SMTP_AUTHENTICATION_CRAM_MD5,
I18N_FORM_SUBMIT_LABEL,
I18N_FORM_INVALID_FEEDBACK_CUSTOM_EMAIL,
I18N_FORM_INVALID_FEEDBACK_SMTP_ADDRESS,
@@ -82,6 +99,7 @@ export default {
smtpPort: '587',
smtpUsername: '',
smtpPassword: '',
+ smtpAuthentication: null,
validationState: {
customEmail: null,
smtpAddress: null,
@@ -113,6 +131,7 @@ export default {
smtp_port: this.smtpPort,
smtp_username: this.smtpUsername,
smtp_password: this.smtpPassword,
+ smtp_authentication: this.smtpAuthentication,
};
},
onCustomEmailChange() {
@@ -124,7 +143,7 @@ export default {
}
},
validateCustomEmail() {
- this.validationState.customEmail = isEmail(this.customEmail);
+ this.validationState.customEmail = isServiceDeskSettingEmail(this.customEmail);
},
validateSmtpAddress() {
this.validationState.smtpAddress = !isEmptyValue(this.smtpAddress);
@@ -145,6 +164,26 @@ export default {
this.validateSmtpUsername();
this.validateSmtpPassword();
},
+ getSmtpAuthenticationOptions() {
+ return [
+ {
+ text: this.$options.I18N_FORM_SMTP_AUTHENTICATION_NONE,
+ value: null,
+ },
+ {
+ text: this.$options.I18N_FORM_SMTP_AUTHENTICATION_PLAIN,
+ value: 'plain',
+ },
+ {
+ text: this.$options.I18N_FORM_SMTP_AUTHENTICATION_LOGIN,
+ value: 'login',
+ },
+ {
+ text: this.$options.I18N_FORM_SMTP_AUTHENTICATION_CRAM_MD5,
+ value: 'cram_md5',
+ },
+ ];
+ },
},
};
</script>
@@ -298,6 +337,20 @@ export default {
/>
</gl-form-group>
+ <gl-form-group
+ :label="$options.I18N_FORM_SMTP_AUTHENTICATION_LABEL"
+ label-for="custom-email-form-smtp-password"
+ class="gl-mt-3"
+ >
+ <gl-form-select
+ id="custom-email-form-smtp-authentication"
+ v-model.trim="smtpAuthentication"
+ :options="getSmtpAuthenticationOptions()"
+ :aria-label="$options.I18N_FORM_SMTP_AUTHENTICATION_LABEL"
+ :disabled="isSubmitting"
+ />
+ </gl-form-group>
+
<gl-button
type="submit"
variant="confirm"
diff --git a/app/assets/javascripts/projects/settings_service_desk/components/custom_email_wrapper.vue b/app/assets/javascripts/projects/settings_service_desk/components/custom_email_wrapper.vue
index 03ba99bcf71..f72aa19bdf2 100644
--- a/app/assets/javascripts/projects/settings_service_desk/components/custom_email_wrapper.vue
+++ b/app/assets/javascripts/projects/settings_service_desk/components/custom_email_wrapper.vue
@@ -234,7 +234,6 @@ export default {
:href="$options.FEEDBACK_ISSUE_URL"
target="_blank"
data-testid="feedback-link"
- class="gl-text-blue-600 font-size-inherit"
>{{ content }}
</gl-link>
</template>
diff --git a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue
index 2b2722ab329..6674937be67 100644
--- a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue
+++ b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue
@@ -55,6 +55,9 @@ export default {
projectKey: {
default: '',
},
+ addExternalParticipantsFromCc: {
+ default: false,
+ },
templates: {
default: [],
},
@@ -109,13 +112,20 @@ export default {
});
},
- onSaveTemplate({ selectedTemplate, fileTemplateProjectId, outgoingName, projectKey }) {
+ onSaveTemplate({
+ selectedTemplate,
+ fileTemplateProjectId,
+ outgoingName,
+ projectKey,
+ addExternalParticipantsFromCc,
+ }) {
this.isTemplateSaving = true;
const body = {
issue_template_key: selectedTemplate,
outgoing_name: outgoingName,
project_key: projectKey,
+ add_external_participants_from_cc: addExternalParticipantsFromCc,
service_desk_enabled: this.isEnabled,
file_template_project_id: fileTemplateProjectId,
};
@@ -187,6 +197,7 @@ export default {
:initial-selected-file-template-project-id="selectedFileTemplateProjectId"
:initial-outgoing-name="outgoingName"
:initial-project-key="projectKey"
+ :initial-add-external-participants-from-cc="addExternalParticipantsFromCc"
:templates="templates"
:is-template-saving="isTemplateSaving"
@save="onSaveTemplate"
diff --git a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue
index 5078cbbdf59..5febb6ff0aa 100644
--- a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue
+++ b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue
@@ -4,6 +4,7 @@ import {
GlToggle,
GlLoadingIcon,
GlSprintf,
+ GlFormCheckbox,
GlFormInputGroup,
GlFormGroup,
GlFormInput,
@@ -11,7 +12,8 @@ import {
GlAlert,
} from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
-import { __ } from '~/locale';
+import { __, s__ } from '~/locale';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import ServiceDeskTemplateDropdown from './service_desk_template_dropdown.vue';
@@ -21,12 +23,22 @@ export default {
issueTrackerEnableMessage: __(
'To use Service Desk in this project, you must %{linkStart}activate the issue tracker%{linkEnd}.',
),
+ addExternalParticipantsFromCc: {
+ label: s__('ServiceDesk|Add external participants from the %{codeStart}Cc%{codeEnd} header'),
+ help: s__(
+ 'ServiceDesk|Add email addresses in the %{codeStart}Cc%{codeEnd} header of Service Desk emails to the issue.',
+ ),
+ helpNotificationExtra: s__(
+ 'ServiceDesk|Like the author, external participants receive Service Desk emails and can participate in the discussion.',
+ ),
+ },
},
components: {
ClipboardButton,
GlButton,
GlToggle,
GlLoadingIcon,
+ GlFormCheckbox,
GlSprintf,
GlFormInput,
GlFormGroup,
@@ -35,6 +47,7 @@ export default {
GlAlert,
ServiceDeskTemplateDropdown,
},
+ mixins: [glFeatureFlagsMixin()],
props: {
isEnabled: {
type: Boolean,
@@ -78,6 +91,11 @@ export default {
required: false,
default: '',
},
+ initialAddExternalParticipantsFromCc: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
templates: {
type: Array,
required: false,
@@ -95,11 +113,15 @@ export default {
selectedFileTemplateProjectId: this.initialSelectedFileTemplateProjectId,
outgoingName: this.initialOutgoingName || __('GitLab Support Bot'),
projectKey: this.initialProjectKey,
+ addExternalParticipantsFromCc: this.initialAddExternalParticipantsFromCc,
searchTerm: '',
projectKeyError: null,
};
},
computed: {
+ showAddExternalParticipantsFromCC() {
+ return this.glFeatures.issueEmailParticipants;
+ },
hasProjectKeySupport() {
return Boolean(this.serviceDeskEmailEnabled);
},
@@ -134,6 +156,7 @@ export default {
selectedTemplate: this.selectedTemplate,
outgoingName: this.outgoingName,
projectKey: this.projectKey,
+ addExternalParticipantsFromCc: this.addExternalParticipantsFromCc,
fileTemplateProjectId: this.selectedFileTemplateProjectId,
});
},
@@ -240,12 +263,7 @@ export default {
"
>
<template #link="{ content }">
- <gl-link
- :href="emailSuffixHelpUrl"
- target="_blank"
- class="gl-text-blue-600 font-size-inherit"
- >{{ content }}
- </gl-link>
+ <gl-link :href="emailSuffixHelpUrl" target="_blank">{{ content }} </gl-link>
</template>
</gl-sprintf>
</template>
@@ -259,10 +277,7 @@ export default {
"
>
<template #link="{ content }">
- <gl-link
- :href="serviceDeskEmailAddressHelpUrl"
- target="_blank"
- class="gl-text-blue-600 font-size-inherit"
+ <gl-link :href="serviceDeskEmailAddressHelpUrl" target="_blank"
>{{ content }}
</gl-link>
</template>
@@ -307,11 +322,31 @@ export default {
</template>
</gl-form-group>
+ <gl-form-checkbox
+ v-if="showAddExternalParticipantsFromCC"
+ v-model="addExternalParticipantsFromCc"
+ :disabled="!isIssueTrackerEnabled"
+ >
+ <gl-sprintf :message="$options.i18n.addExternalParticipantsFromCc.label">
+ <template #code="{ content }">
+ <code>{{ content }}</code>
+ </template>
+ </gl-sprintf>
+
+ <template #help>
+ <gl-sprintf :message="$options.i18n.addExternalParticipantsFromCc.help">
+ <template #code="{ content }">
+ <code>{{ content }}</code>
+ </template>
+ </gl-sprintf>
+ {{ $options.i18n.addExternalParticipantsFromCc.helpNotificationExtra }}
+ </template>
+ </gl-form-checkbox>
+
<gl-button
variant="confirm"
class="gl-mt-5"
data-testid="save_service_desk_settings_button"
- data-qa-selector="save_service_desk_settings_button"
:disabled="isTemplateSaving || !isIssueTrackerEnabled"
@click="onSaveTemplate"
>
diff --git a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_template_dropdown.vue b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_template_dropdown.vue
index 315f0743b53..86c4fdcc30a 100644
--- a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_template_dropdown.vue
+++ b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_template_dropdown.vue
@@ -84,7 +84,6 @@ export default {
id="service-desk-template-select"
:text="selectedTemplate || $options.i18n.defaultDropdownText"
:header-text="$options.i18n.defaultDropdownText"
- data-qa-selector="service_desk_template_dropdown"
:block="true"
class="service-desk-template-select"
toggle-class="gl-m-0"
diff --git a/app/assets/javascripts/projects/settings_service_desk/custom_email_constants.js b/app/assets/javascripts/projects/settings_service_desk/custom_email_constants.js
index aafd77bd25e..8ac186e292c 100644
--- a/app/assets/javascripts/projects/settings_service_desk/custom_email_constants.js
+++ b/app/assets/javascripts/projects/settings_service_desk/custom_email_constants.js
@@ -37,6 +37,13 @@ export const I18N_FORM_SMTP_PORT_DESCRIPTION = s__(
export const I18N_FORM_SMTP_USERNAME_LABEL = s__('ServiceDesk|SMTP username');
export const I18N_FORM_SMTP_PASSWORD_LABEL = s__('ServiceDesk|SMTP password');
export const I18N_FORM_SMTP_PASSWORD_DESCRIPTION = s__('ServiceDesk|Minimum 8 characters long.');
+export const I18N_FORM_SMTP_AUTHENTICATION_LABEL = s__('ServiceDesk|SMTP authentication method');
+export const I18N_FORM_SMTP_AUTHENTICATION_NONE = s__(
+ 'ServiceDesk|Let GitLab select a server-supported method (recommended)',
+);
+export const I18N_FORM_SMTP_AUTHENTICATION_PLAIN = s__('ServiceDesk|Plain');
+export const I18N_FORM_SMTP_AUTHENTICATION_LOGIN = s__('ServiceDesk|Login');
+export const I18N_FORM_SMTP_AUTHENTICATION_CRAM_MD5 = s__('ServiceDesk|CRAM-MD5');
export const I18N_FORM_SUBMIT_LABEL = s__('ServiceDesk|Save and test connection');
export const I18N_FORM_INVALID_FEEDBACK_CUSTOM_EMAIL = s__(
diff --git a/app/assets/javascripts/projects/settings_service_desk/index.js b/app/assets/javascripts/projects/settings_service_desk/index.js
index c4d4f42576f..ce223b349bf 100644
--- a/app/assets/javascripts/projects/settings_service_desk/index.js
+++ b/app/assets/javascripts/projects/settings_service_desk/index.js
@@ -21,6 +21,7 @@ export default () => {
incomingEmail,
outgoingName,
projectKey,
+ addExternalParticipantsFromCc,
selectedTemplate,
selectedFileTemplateProjectId,
templates,
@@ -39,6 +40,7 @@ export default () => {
isIssueTrackerEnabled: parseBoolean(issueTrackerEnabled),
outgoingName,
projectKey,
+ addExternalParticipantsFromCc: parseBoolean(addExternalParticipantsFromCc),
selectedTemplate,
selectedFileTemplateProjectId: parseInt(selectedFileTemplateProjectId, 10) || null,
templates: JSON.parse(templates),
diff --git a/app/assets/javascripts/projects/tree/components/commit_pipeline_status.vue b/app/assets/javascripts/projects/tree/components/commit_pipeline_status.vue
index 5b620aa2300..074cddac422 100644
--- a/app/assets/javascripts/projects/tree/components/commit_pipeline_status.vue
+++ b/app/assets/javascripts/projects/tree/components/commit_pipeline_status.vue
@@ -91,17 +91,10 @@ export default {
};
</script>
<template>
- <div class="ci-status-link">
+ <div class="gl-ml-5">
<gl-loading-icon v-if="isLoading" size="lg" label="Loading pipeline status" />
<a v-else :href="ciStatus.details_path">
- <ci-icon
- v-gl-tooltip
- :title="statusTitle"
- :aria-label="statusTitle"
- :status="ciStatus"
- :size="24"
- data-container="body"
- />
+ <ci-icon :status="ciStatus" :title="statusTitle" :aria-label="statusTitle" />
</a>
</div>
</template>
diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit.js b/app/assets/javascripts/protected_branches/protected_branch_edit.js
index 29034b3bc0e..66da3de516a 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_edit.js
+++ b/app/assets/javascripts/protected_branches/protected_branch_edit.js
@@ -6,6 +6,10 @@ import { initToggle } from '~/toggles';
import { initAccessDropdown } from '~/projects/settings/init_access_dropdown';
import { ACCESS_LEVELS, LEVEL_TYPES } from './constants';
+const isDropdownDisabled = (dropdown) => {
+ return dropdown?.$options.disabled === '';
+};
+
export default class ProtectedBranchEdit {
constructor(options) {
this.hasLicense = options.hasLicense;
@@ -104,6 +108,9 @@ export default class ProtectedBranchEdit {
}
initSelectedItems(dropdown, accessLevel) {
+ if (isDropdownDisabled(dropdown)) {
+ return;
+ }
this.selectedItems[accessLevel] = dropdown.preselected.map((item) => {
if (item.type === LEVEL_TYPES.USER) return { id: item.id, user_id: item.user_id };
if (item.type === LEVEL_TYPES.ROLE) return { id: item.id, access_level: item.access_level };
@@ -183,7 +190,10 @@ export default class ProtectedBranchEdit {
};
});
- this.selectedItems[accessLevel] = itemsToAdd;
- this[`${accessLevel}_dropdown`]?.setPreselectedItems(itemsToAdd);
+ const dropdown = this[`${accessLevel}_dropdown`];
+ if (!isDropdownDisabled(dropdown)) {
+ this.selectedItems[accessLevel] = itemsToAdd;
+ dropdown?.setPreselectedItems(itemsToAdd);
+ }
}
}
diff --git a/app/assets/javascripts/protected_tags/protected_tag_create.js b/app/assets/javascripts/protected_tags/protected_tag_create.js
index b5661af352c..b3754cecce4 100644
--- a/app/assets/javascripts/protected_tags/protected_tag_create.js
+++ b/app/assets/javascripts/protected_tags/protected_tag_create.js
@@ -41,7 +41,7 @@ export default class ProtectedTagCreate {
accessLevel: ACCESS_LEVELS.CREATE,
accessLevelsData: gon.create_access_levels,
searchEnabled: dropdownEl.dataset.filter !== undefined,
- testId: 'allowed_to_create_dropdown',
+ testId: 'allowed-to-create-dropdown',
});
this.protectedTagAccessDropdown.$on('select', (selected) => {
diff --git a/app/assets/javascripts/protected_tags/protected_tag_edit.vue b/app/assets/javascripts/protected_tags/protected_tag_edit.vue
index 82b2ecc5f5c..7fe1dc9c01a 100644
--- a/app/assets/javascripts/protected_tags/protected_tag_edit.vue
+++ b/app/assets/javascripts/protected_tags/protected_tag_edit.vue
@@ -101,7 +101,7 @@ export default {
<template>
<access-dropdown
toggle-class="js-allowed-to-create gl-max-w-34"
- test-id="allowed_to_create_dropdown"
+ test-id="allowed-to-create-dropdown"
:has-license="hasLicense"
:access-level="$options.ACCESS_LEVELS.CREATE"
:access-levels-data="accessLevelsData"
diff --git a/app/assets/javascripts/protected_tags/protected_tag_edit_list.js b/app/assets/javascripts/protected_tags/protected_tag_edit_list.js
index 444d6e9cf76..fad15a5d89e 100644
--- a/app/assets/javascripts/protected_tags/protected_tag_edit_list.js
+++ b/app/assets/javascripts/protected_tags/protected_tag_edit_list.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import ProtectedTagEdit from './protected_tag_edit.vue';
export default class ProtectedTagEditList {
diff --git a/app/assets/javascripts/releases/components/app_edit_new.vue b/app/assets/javascripts/releases/components/app_edit_new.vue
index c68fbceb4f6..df9f333afe5 100644
--- a/app/assets/javascripts/releases/components/app_edit_new.vue
+++ b/app/assets/javascripts/releases/components/app_edit_new.vue
@@ -176,8 +176,7 @@ export default {
</p>
<form v-if="showForm" class="js-quick-submit" @submit.prevent="submitForm">
<tag-field />
- <gl-form-group>
- <label for="release-title">{{ __('Release title') }}</label>
+ <gl-form-group :label="__('Release title')">
<gl-form-input
id="release-title"
ref="releaseTitleInput"
@@ -186,17 +185,14 @@ export default {
class="form-control"
/>
</gl-form-group>
- <gl-form-group class="w-50" data-testid="milestones-field">
- <label>{{ __('Milestones') }}</label>
- <div class="d-flex flex-column col-md-6 col-sm-10 pl-0">
- <milestone-combobox
- v-model="releaseMilestones"
- :project-id="projectId"
- :group-id="groupId"
- :group-milestones-available="groupMilestonesAvailable"
- :extra-links="milestoneComboboxExtraLinks"
- />
- </div>
+ <gl-form-group :label="__('Milestones')" class="gl-w-30" data-testid="milestones-field">
+ <milestone-combobox
+ v-model="releaseMilestones"
+ :project-id="projectId"
+ :group-id="groupId"
+ :group-milestones-available="groupMilestonesAvailable"
+ :extra-links="milestoneComboboxExtraLinks"
+ />
</gl-form-group>
<gl-form-group :label="__('Release date')" label-for="release-released-at">
<template #label-description>
@@ -214,8 +210,7 @@ export default {
</template>
<gl-datepicker id="release-released-at" v-model="releasedAt" :default-date="releasedAt" />
</gl-form-group>
- <gl-form-group data-testid="release-notes">
- <label for="release-notes">{{ __('Release notes') }}</label>
+ <gl-form-group :label="__('Release notes')" data-testid="release-notes">
<div class="common-note-form">
<markdown-field
:can-attach-file="true"
diff --git a/app/assets/javascripts/releases/components/release_block_header.vue b/app/assets/javascripts/releases/components/release_block_header.vue
index 070865cf84b..b4c897a8236 100644
--- a/app/assets/javascripts/releases/components/release_block_header.vue
+++ b/app/assets/javascripts/releases/components/release_block_header.vue
@@ -50,7 +50,7 @@ export default {
<template>
<div class="card-header d-flex align-items-center bg-white pr-0">
<h2 class="card-title my-2 mr-auto">
- <gl-link v-if="selfLink" :href="selfLink" class="font-size-inherit">
+ <gl-link v-if="selfLink" :href="selfLink">
{{ release.name }}
</gl-link>
<template v-else>
diff --git a/app/assets/javascripts/repository/components/blob_button_group.vue b/app/assets/javascripts/repository/components/blob_button_group.vue
index 99b861ca104..ed7212eb9a6 100644
--- a/app/assets/javascripts/repository/components/blob_button_group.vue
+++ b/app/assets/javascripts/repository/components/blob_button_group.vue
@@ -92,8 +92,8 @@ export default {
deleteModalTitle() {
return sprintf(__('Delete %{name}'), { name: this.name });
},
- lockBtnQASelector() {
- return this.canLock ? 'lock_button' : 'disabled_lock_button';
+ lockBtnTestId() {
+ return this.canLock ? 'lock-button' : 'disabled-lock-button';
},
},
methods: {
@@ -120,8 +120,7 @@ export default {
:project-path="projectPath"
:is-locked="isLocked"
:can-lock="canLock"
- data-testid="lock"
- :data-qa-selector="lockBtnQASelector"
+ :data-testid="lockBtnTestId"
/>
<gl-button data-testid="replace" @click="showModal($options.replaceBlobModalId)">
{{ $options.i18n.replace }}
diff --git a/app/assets/javascripts/repository/components/blob_content_viewer.vue b/app/assets/javascripts/repository/components/blob_content_viewer.vue
index 6565c84fa11..97a1cbda5d0 100644
--- a/app/assets/javascripts/repository/components/blob_content_viewer.vue
+++ b/app/assets/javascripts/repository/components/blob_content_viewer.vue
@@ -122,6 +122,7 @@ export default {
blobInfo: {},
isEmptyRepository: false,
projectId: null,
+ showBlame: this.$route?.query?.blame === '1',
};
},
computed: {
@@ -202,6 +203,9 @@ export default {
isUsingLfs() {
return this.blobInfo.storedExternally && this.blobInfo.externalStorage === LFS_STORAGE;
},
+ isBlameEnabled() {
+ return this.glFeatures.blobBlameInfo && this.blobInfo.language === 'json'; // This feature is currently scoped to JSON files
+ },
},
watch: {
// Watch the URL 'plain' query value to know if the viewer needs changing.
@@ -289,6 +293,14 @@ export default {
onCopy() {
navigator.clipboard.writeText(this.blobInfo.rawTextBlob);
},
+ handleToggleBlame() {
+ this.switchViewer(SIMPLE_BLOB_VIEWER);
+ this.showBlame = !this.showBlame;
+
+ const blame = this.showBlame === true ? '1' : '0';
+ if (this.$route?.query?.blame === blame) return;
+ this.$router.push({ path: this.$route.path, query: { ...this.$route.query, blame } });
+ },
},
};
</script>
@@ -299,19 +311,21 @@ export default {
<div v-if="blobInfo && !isLoading" id="fileHolder" class="file-holder">
<blob-header
:blob="blobInfo"
- :hide-viewer-switcher="!hasRichViewer || isBinaryFileType || isUsingLfs"
+ :hide-viewer-switcher="isBinaryFileType || isUsingLfs"
:is-binary="isBinaryFileType"
:active-viewer-type="viewer.type"
:has-render-error="hasRenderError"
:show-path="false"
:override-copy="true"
:show-fork-suggestion="showForkSuggestion"
+ :show-blame-toggle="isBlameEnabled"
:project-path="projectPath"
:project-id="projectId"
@viewer-changed="handleViewerChanged"
@copy="onCopy"
@edit="editBlob"
@error="displayError"
+ @blame="handleToggleBlame"
>
<template #actions>
<blob-button-group
@@ -354,6 +368,7 @@ export default {
v-else
:blob="blobInfo"
:chunks="chunks"
+ :show-blame="showBlame"
:project-path="projectPath"
:current-ref="currentRef"
class="blob-viewer"
diff --git a/app/assets/javascripts/repository/components/commit_info.vue b/app/assets/javascripts/repository/components/commit_info.vue
index b6e3cdbb7a3..b6674114a20 100644
--- a/app/assets/javascripts/repository/components/commit_info.vue
+++ b/app/assets/javascripts/repository/components/commit_info.vue
@@ -1,6 +1,6 @@
<script>
import { GlTooltipDirective, GlLink, GlButton } from '@gitlab/ui';
-import { __ } from '~/locale';
+import { __, sprintf } from '~/locale';
import SafeHtml from '~/vue_shared/directives/safe_html';
import defaultAvatarUrl from 'images/no_avatar.png';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
@@ -26,6 +26,11 @@ export default {
type: Object,
required: true,
},
+ prevBlameLink: {
+ type: String,
+ required: false,
+ default: null,
+ },
},
data() {
return { showDescription: false };
@@ -35,6 +40,9 @@ export default {
// Strip the newline at the beginning
return this.commit?.descriptionHtml?.replace(/^&#x000A;/, '');
},
+ avatarLinkAltText() {
+ return sprintf(__(`%{username}'s avatar`), { username: this.commit.authorName });
+ },
},
methods: {
toggleShowDescription() {
@@ -58,6 +66,7 @@ export default {
v-if="commit.author"
:link-href="commit.author.webPath"
:img-src="commit.author.avatarUrl"
+ :img-alt="avatarLinkAltText"
:img-size="32"
class="gl-my-2 gl-mr-4"
/>
@@ -67,10 +76,8 @@ export default {
:img-src="commit.authorGravatar || $options.defaultAvatarUrl"
:size="32"
/>
- <div
- class="commit-detail flex-list gl-display-flex gl-justify-content-space-between gl-align-items-center gl-flex-grow-1 gl-min-w-0"
- >
- <div class="commit-content" data-qa-selector="commit_content">
+ <div class="commit-detail flex-list gl-display-flex gl-flex-grow-1 gl-min-w-0">
+ <div class="commit-content gl-w-full gl-text-truncate" data-testid="commit-content">
<gl-link
v-safe-html:[$options.safeHtmlConfig]="commit.titleHtml"
:href="commit.webPath"
@@ -112,5 +119,6 @@ export default {
<div class="gl-flex-grow-1"></div>
<slot></slot>
</div>
+ <div v-if="prevBlameLink" v-safe-html:[$options.safeHtmlConfig]="prevBlameLink"></div>
</div>
</template>
diff --git a/app/assets/javascripts/repository/components/delete_blob_modal.vue b/app/assets/javascripts/repository/components/delete_blob_modal.vue
index 97171a3282b..079d4c522a8 100644
--- a/app/assets/javascripts/repository/components/delete_blob_modal.vue
+++ b/app/assets/javascripts/repository/components/delete_blob_modal.vue
@@ -273,7 +273,7 @@ export default {
v-model="form.fields['commit_message'].value"
v-validation:[form.showValidation]
name="commit_message"
- data-qa-selector="commit_message_field"
+ data-testid="commit-message-field"
:state="form.fields['commit_message'].state"
:disabled="loading"
required
diff --git a/app/assets/javascripts/repository/components/last_commit.vue b/app/assets/javascripts/repository/components/last_commit.vue
index 05d4d9e1f81..7f7a76cd4aa 100644
--- a/app/assets/javascripts/repository/components/last_commit.vue
+++ b/app/assets/javascripts/repository/components/last_commit.vue
@@ -3,7 +3,7 @@ import { GlTooltipDirective, GlButton, GlButtonGroup, GlLoadingIcon } from '@git
import SafeHtml from '~/vue_shared/directives/safe_html';
import pathLastCommitQuery from 'shared_queries/repository/path_last_commit.query.graphql';
import { sprintf, s__ } from '~/locale';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import SignatureBadge from '~/commit/components/signature_badge.vue';
import getRefMixin from '../mixins/get_ref';
@@ -17,7 +17,7 @@ export default {
CommitInfo,
ClipboardButton,
SignatureBadge,
- CiBadgeLink,
+ CiIcon,
GlButtonGroup,
GlButton,
GlLoadingIcon,
@@ -50,9 +50,6 @@ export default {
pipeline: pipelines?.length && pipelines[0].node,
};
},
- context: {
- isSingleRequest: true,
- },
error(error) {
throw error;
},
@@ -115,12 +112,10 @@ export default {
class="commit-actions gl-display-flex gl-flex-align gl-align-items-center gl-flex-direction-row"
>
<signature-badge v-if="commit.signature" :signature="commit.signature" />
- <div v-if="commit.pipeline" class="ci-status-link">
- <ci-badge-link
+ <div v-if="commit.pipeline" class="gl-ml-5">
+ <ci-icon
:status="commit.pipeline.detailedStatus"
- :details-path="commit.pipeline.detailedStatus.detailsPath"
:aria-label="statusTitle"
- :show-text="false"
class="js-commit-pipeline"
/>
</div>
diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue
index 526757e6147..6a81f11eb51 100644
--- a/app/assets/javascripts/repository/components/table/row.vue
+++ b/app/assets/javascripts/repository/components/table/row.vue
@@ -248,19 +248,19 @@ export default {
class="ml-1"
/>
</td>
- <td class="d-none d-sm-table-cell tree-commit cursor-default gl-text-secondary">
+ <td class="d-none d-sm-table-cell tree-commit cursor-default">
<gl-link
v-if="commitData"
v-safe-html:[$options.safeHtmlConfig]="commitData.titleHtml"
:href="commitData.commitPath"
:title="commitData.message"
- class="str-truncated-100 tree-commit-link gl-text-secondary"
+ class="str-truncated-100 tree-commit-link gl-text-gray-600"
/>
<gl-intersection-observer @appear="rowAppeared" @disappear="rowDisappeared">
<gl-skeleton-loader v-if="showSkeletonLoader" :lines="1" />
</gl-intersection-observer>
</td>
- <td class="tree-time-ago text-right cursor-default gl-text-secondary">
+ <td class="tree-time-ago text-right cursor-default gl-text-gray-600">
<gl-intersection-observer @appear="rowAppeared" @disappear="rowDisappeared">
<timeago-tooltip v-if="commitData" :time="commitData.committedDate" />
</gl-intersection-observer>
diff --git a/app/assets/javascripts/search/sidebar/components/app.vue b/app/assets/javascripts/search/sidebar/components/app.vue
index 2ff138cabe5..86a5f5107f8 100644
--- a/app/assets/javascripts/search/sidebar/components/app.vue
+++ b/app/assets/javascripts/search/sidebar/components/app.vue
@@ -1,12 +1,12 @@
<script>
// eslint-disable-next-line no-restricted-imports
import { mapState, mapGetters } from 'vuex';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ScopeLegacyNavigation from '~/search/sidebar/components/scope_legacy_navigation.vue';
import ScopeSidebarNavigation from '~/search/sidebar/components/scope_sidebar_navigation.vue';
import SmallScreenDrawerNavigation from '~/search/sidebar/components/small_screen_drawer_navigation.vue';
import SidebarPortal from '~/super_sidebar/components/sidebar_portal.vue';
import { toggleSuperSidebarCollapsed } from '~/super_sidebar/super_sidebar_collapsed_state_manager';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import DomElementListener from '~/vue_shared/components/dom_element_listener.vue';
import {
SCOPE_ISSUES,
@@ -16,6 +16,7 @@ import {
SCOPE_NOTES,
SCOPE_COMMITS,
SCOPE_MILESTONES,
+ SCOPE_WIKI_BLOBS,
SEARCH_TYPE_ADVANCED,
} from '../constants';
import IssuesFilters from './issues_filters.vue';
@@ -25,6 +26,7 @@ import ProjectsFilters from './projects_filters.vue';
import NotesFilters from './notes_filters.vue';
import CommitsFilters from './commits_filters.vue';
import MilestonesFilters from './milestones_filters.vue';
+import WikiBlobsFilters from './wiki_blobs_filters.vue';
export default {
name: 'GlobalSearchSidebar',
@@ -34,6 +36,7 @@ export default {
BlobsFilters,
ProjectsFilters,
NotesFilters,
+ WikiBlobsFilters,
ScopeLegacyNavigation,
ScopeSidebarNavigation,
SidebarPortal,
@@ -60,20 +63,18 @@ export default {
return this.currentScope === SCOPE_PROJECTS;
},
showNotesFilters() {
- // for now, the feature flag is placed here. Since we have only one filter in notes scope
- return this.currentScope === SCOPE_NOTES && this.glFeatures.searchNotesHideArchivedProjects;
+ return this.currentScope === SCOPE_NOTES;
},
showCommitsFilters() {
- // for now, the feature flag is placed here. Since we have only one filter in commits scope
- return (
- this.currentScope === SCOPE_COMMITS && this.glFeatures.searchCommitsHideArchivedProjects
- );
+ return this.currentScope === SCOPE_COMMITS;
},
showMilestonesFilters() {
- // for now, the feature flag is placed here. Since we have only one filter in milestones scope
+ return this.currentScope === SCOPE_MILESTONES;
+ },
+ showWikiBlobsFilters() {
return (
- this.currentScope === SCOPE_MILESTONES &&
- this.glFeatures.searchMilestonesHideArchivedProjects
+ this.currentScope === SCOPE_WIKI_BLOBS &&
+ this.glFeatures?.searchProjectWikisHideArchivedProjects
);
},
showScopeNavigation() {
@@ -103,6 +104,7 @@ export default {
<notes-filters v-if="showNotesFilters" />
<commits-filters v-if="showCommitsFilters" />
<milestones-filters v-if="showMilestonesFilters" />
+ <wiki-blobs-filters v-if="showWikiBlobsFilters" />
</sidebar-portal>
</section>
@@ -119,6 +121,7 @@ export default {
<notes-filters v-if="showNotesFilters" />
<commits-filters v-if="showCommitsFilters" />
<milestones-filters v-if="showMilestonesFilters" />
+ <wiki-blobs-filters v-if="showWikiBlobsFilters" />
</div>
<small-screen-drawer-navigation class="gl-lg-display-none">
<scope-legacy-navigation />
@@ -129,6 +132,7 @@ export default {
<notes-filters v-if="showNotesFilters" />
<commits-filters v-if="showCommitsFilters" />
<milestones-filters v-if="showMilestonesFilters" />
+ <wiki-blobs-filters v-if="showWikiBlobsFilters" />
</small-screen-drawer-navigation>
</section>
</template>
diff --git a/app/assets/javascripts/search/sidebar/components/archived_filter/data.js b/app/assets/javascripts/search/sidebar/components/archived_filter/data.js
index ed90e2aaded..96a6f119da2 100644
--- a/app/assets/javascripts/search/sidebar/components/archived_filter/data.js
+++ b/app/assets/javascripts/search/sidebar/components/archived_filter/data.js
@@ -5,7 +5,16 @@ const checkboxLabel = s__('GlobalSearch|Include archived');
export const TRACKING_NAMESPACE = 'search:archived:select';
export const TRACKING_LABEL_CHECKBOX = 'checkbox';
-const scopes = ['projects', 'issues', 'merge_requests', 'notes', 'blobs', 'commits', 'milestones'];
+const scopes = [
+ 'projects',
+ 'issues',
+ 'merge_requests',
+ 'notes',
+ 'blobs',
+ 'commits',
+ 'milestones',
+ 'wiki_blobs',
+];
const filterParam = 'include_archived';
diff --git a/app/assets/javascripts/search/sidebar/components/blobs_filters.vue b/app/assets/javascripts/search/sidebar/components/blobs_filters.vue
index ac36ae6b366..0ed2c24efba 100644
--- a/app/assets/javascripts/search/sidebar/components/blobs_filters.vue
+++ b/app/assets/javascripts/search/sidebar/components/blobs_filters.vue
@@ -18,11 +18,8 @@ export default {
computed: {
...mapGetters(['currentScope']),
...mapState(['useSidebarNavigation', 'searchType']),
- showArchivedFilter() {
- return this.glFeatures.searchBlobsHideArchivedProjects;
- },
showDivider() {
- return !this.useSidebarNavigation && this.showArchivedFilter;
+ return !this.useSidebarNavigation;
},
hrClasses() {
return [...HR_DEFAULT_CLASSES, 'gl-display-none', 'gl-md-display-block'];
@@ -35,6 +32,6 @@ export default {
<filters-template>
<language-filter class="gl-mb-5" />
<hr v-if="showDivider" :class="hrClasses" />
- <archived-filter v-if="showArchivedFilter" class="gl-mb-5" />
+ <archived-filter class="gl-mb-5" />
</filters-template>
</template>
diff --git a/app/assets/javascripts/search/sidebar/components/issues_filters.vue b/app/assets/javascripts/search/sidebar/components/issues_filters.vue
index 4a2d3df6921..a77fb34cdba 100644
--- a/app/assets/javascripts/search/sidebar/components/issues_filters.vue
+++ b/app/assets/javascripts/search/sidebar/components/issues_filters.vue
@@ -41,10 +41,7 @@ export default {
);
},
showArchivedFilter() {
- return (
- archivedFilterData.scopes.includes(this.currentScope) &&
- this.glFeatures.searchIssuesHideArchivedProjects
- );
+ return archivedFilterData.scopes.includes(this.currentScope);
},
showDivider() {
return !this.useSidebarNavigation;
diff --git a/app/assets/javascripts/search/sidebar/components/label_filter/index.vue b/app/assets/javascripts/search/sidebar/components/label_filter/index.vue
index ebd0406bcec..97583730958 100644
--- a/app/assets/javascripts/search/sidebar/components/label_filter/index.vue
+++ b/app/assets/javascripts/search/sidebar/components/label_filter/index.vue
@@ -55,12 +55,15 @@ export default {
},
i18n: I18N,
computed: {
- ...mapState(['useSidebarNavigation', 'searchLabelString', 'query', 'aggregations']),
+ ...mapState(['useSidebarNavigation', 'searchLabelString', 'query', 'urlQuery', 'aggregations']),
...mapGetters([
'filteredLabels',
'filteredUnselectedLabels',
'filteredAppliedSelectedLabels',
'appliedSelectedLabels',
+ 'unselectedLabels',
+ 'unappliedNewLabels',
+ 'labelAggregationBuckets',
]),
searchInputDescribeBy() {
if (this.isLoggedIn) {
@@ -100,10 +103,10 @@ export default {
return FIRST_DROPDOWN_INDEX;
},
hasSelectedLabels() {
- return this.filteredAppliedSelectedLabels.length > 0;
+ return this.filteredAppliedSelectedLabels?.length > 0;
},
hasUnselectedLabels() {
- return this.filteredUnselectedLabels.length > 0;
+ return this.filteredUnselectedLabels?.length > 0;
},
labelSearchBox() {
return this.$refs.searchLabelInputBox?.$el.querySelector('[role=searchbox]');
@@ -122,25 +125,30 @@ export default {
this.setLabelFilterSearch({ value });
},
},
- selectedFilters: {
+ selectedLabels: {
get() {
return this.combinedSelectedFilters;
},
set(value) {
this.setQuery({ key: this.$options.labelFilterData?.filterParam, value });
-
trackSelectCheckbox(value);
},
},
},
async created() {
- await this.fetchAllAggregation();
+ if (this.urlQuery?.[labelFilterData.filterParam]?.length > 0) {
+ await this.fetchAllAggregation();
+ }
},
methods: {
...mapActions(['fetchAllAggregation', 'setQuery', 'closeLabel', 'setLabelFilterSearch']),
- openDropdown() {
+ async openDropdown() {
this.isFocused = true;
+ if (!this.aggregations.error && this.filteredLabels?.length === 0) {
+ await this.fetchAllAggregation();
+ }
+
trackOpenDropdown();
},
closeDropdown(event) {
@@ -158,16 +166,8 @@ export default {
const { key } = event.target.closest('.gl-label').dataset;
this.closeLabel({ key });
},
- reactiveLabelColor(label) {
- const { color, key } = label;
-
- return this.query?.labels?.some((labelKey) => labelKey === key)
- ? color
- : `rgba(${rgbFromHex(color)}, 0.3)`;
- },
- isLabelClosable(label) {
- const { key } = label;
- return this.query?.labels?.some((labelKey) => labelKey === key);
+ inactiveLabelColor(label) {
+ return `rgba(${rgbFromHex(label.color)}, 0.3)`;
},
},
FIRST_DROPDOWN_INDEX,
@@ -188,13 +188,34 @@ export default {
</h5>
<div class="gl-my-5">
<gl-label
+ v-for="label in unappliedNewLabels"
+ :key="label.key"
+ class="gl-mr-2 gl-mb-2 gl-bg-gray-10"
+ :data-key="label.key"
+ :background-color="inactiveLabelColor(label)"
+ :title="label.title"
+ :show-close-button="false"
+ data-testid="unapplied-label"
+ />
+ <gl-label
+ v-for="label in unselectedLabels"
+ :key="label.key"
+ class="gl-mr-2 gl-mb-2 gl-bg-gray-10"
+ :data-key="label.key"
+ :background-color="inactiveLabelColor(label)"
+ :title="label.title"
+ :show-close-button="false"
+ data-testid="unselected-label"
+ />
+ <gl-label
v-for="label in appliedSelectedLabels"
:key="label.key"
class="gl-mr-2 gl-mb-2 gl-bg-gray-10"
:data-key="label.key"
- :background-color="reactiveLabelColor(label)"
+ :background-color="label.color"
:title="label.title"
- :show-close-button="isLabelClosable(label)"
+ :show-close-button="true"
+ data-testid="label"
@close="onLabelClose"
/>
</div>
@@ -245,7 +266,7 @@ export default {
$options.i18n.DROPDOWN_HEADER
}}</gl-dropdown-section-header>
<gl-dropdown-form>
- <gl-form-checkbox-group v-model="selectedFilters">
+ <gl-form-checkbox-group v-model="selectedLabels">
<label-dropdown-items
v-if="hasSelectedLabels"
:labels="filteredAppliedSelectedLabels"
diff --git a/app/assets/javascripts/search/sidebar/components/merge_requests_filters.vue b/app/assets/javascripts/search/sidebar/components/merge_requests_filters.vue
index 6e476ef7935..f86906ebd26 100644
--- a/app/assets/javascripts/search/sidebar/components/merge_requests_filters.vue
+++ b/app/assets/javascripts/search/sidebar/components/merge_requests_filters.vue
@@ -1,7 +1,6 @@
<script>
// eslint-disable-next-line no-restricted-imports
import { mapGetters, mapState } from 'vuex';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { HR_DEFAULT_CLASSES } from '../constants';
import { statusFilterData } from './status_filter/data';
import StatusFilter from './status_filter/index.vue';
@@ -16,15 +15,11 @@ export default {
FiltersTemplate,
ArchivedFilter,
},
- mixins: [glFeatureFlagsMixin()],
computed: {
...mapGetters(['currentScope']),
...mapState(['useSidebarNavigation', 'searchType']),
showArchivedFilter() {
- return (
- archivedFilterData.scopes.includes(this.currentScope) &&
- this.glFeatures.searchMergeRequestsHideArchivedProjects
- );
+ return archivedFilterData.scopes.includes(this.currentScope);
},
showStatusFilter() {
return Object.values(statusFilterData.scopes).includes(this.currentScope);
diff --git a/app/assets/javascripts/search/sidebar/components/wiki_blobs_filters.vue b/app/assets/javascripts/search/sidebar/components/wiki_blobs_filters.vue
new file mode 100644
index 00000000000..b1f386d9f4f
--- /dev/null
+++ b/app/assets/javascripts/search/sidebar/components/wiki_blobs_filters.vue
@@ -0,0 +1,18 @@
+<script>
+import ArchivedFilter from './archived_filter/index.vue';
+import FiltersTemplate from './filters_template.vue';
+
+export default {
+ name: 'WikiBlobsFilters',
+ components: {
+ ArchivedFilter,
+ FiltersTemplate,
+ },
+};
+</script>
+
+<template>
+ <filters-template>
+ <archived-filter class="gl-mb-5" />
+ </filters-template>
+</template>
diff --git a/app/assets/javascripts/search/sidebar/constants/index.js b/app/assets/javascripts/search/sidebar/constants/index.js
index b5446ecbb42..1559155a941 100644
--- a/app/assets/javascripts/search/sidebar/constants/index.js
+++ b/app/assets/javascripts/search/sidebar/constants/index.js
@@ -5,6 +5,8 @@ export const SCOPE_PROJECTS = 'projects';
export const SCOPE_NOTES = 'notes';
export const SCOPE_COMMITS = 'commits';
export const SCOPE_MILESTONES = 'milestones';
+export const SCOPE_WIKI_BLOBS = 'wiki_blobs';
+
export const LABEL_DEFAULT_CLASSES = [
'gl-display-flex',
'gl-flex-direction-row',
diff --git a/app/assets/javascripts/search/store/getters.js b/app/assets/javascripts/search/store/getters.js
index d01fd884bad..de05e9b80b2 100644
--- a/app/assets/javascripts/search/store/getters.js
+++ b/app/assets/javascripts/search/store/getters.js
@@ -1,10 +1,24 @@
-import { findKey } from 'lodash';
+import { findKey, intersection } from 'lodash';
import { languageFilterData } from '~/search/sidebar/components/language_filter/data';
import { labelFilterData } from '~/search/sidebar/components/label_filter/data';
import { formatSearchResultCount, addCountOverLimit } from '~/search/store/utils';
import { GROUPS_LOCAL_STORAGE_KEY, PROJECTS_LOCAL_STORAGE_KEY, ICON_MAP } from './constants';
+const queryLabelFilters = (state) => state?.query?.[labelFilterData.filterParam] || [];
+const urlQueryLabelFilters = (state) => state?.urlQuery?.[labelFilterData.filterParam] || [];
+
+const appliedSelectedLabelsKeys = (state) =>
+ intersection(urlQueryLabelFilters(state), queryLabelFilters(state));
+
+const unselectedLabelsKeys = (state) =>
+ urlQueryLabelFilters(state)?.filter((label) => !queryLabelFilters(state)?.includes(label));
+
+const unappliedNewLabelKeys = (state) =>
+ state?.query?.labels?.filter((label) => !urlQueryLabelFilters(state)?.includes(label));
+
+export const queryLanguageFilters = (state) => state.query[languageFilterData.filterParam] || [];
+
export const frequentGroups = (state) => {
return state.frequentItems[GROUPS_LOCAL_STORAGE_KEY];
};
@@ -39,25 +53,28 @@ export const filteredLabels = (state) => {
};
export const filteredAppliedSelectedLabels = (state) =>
- filteredLabels(state)?.filter((label) => state?.urlQuery?.labels?.includes(label.key));
+ filteredLabels(state)?.filter((label) => urlQueryLabelFilters(state)?.includes(label.key));
export const appliedSelectedLabels = (state) => {
return labelAggregationBuckets(state)?.filter((label) =>
- state?.urlQuery?.labels?.includes(label.key),
+ appliedSelectedLabelsKeys(state)?.includes(label.key),
);
};
-export const filteredUnselectedLabels = (state) => {
- if (!state?.urlQuery?.labels) {
- return filteredLabels(state);
- }
+export const filteredUnselectedLabels = (state) =>
+ filteredLabels(state)?.filter((label) => !urlQueryLabelFilters(state)?.includes(label.key));
- return filteredLabels(state)?.filter((label) => !state?.urlQuery?.labels?.includes(label.key));
-};
+export const unselectedLabels = (state) =>
+ labelAggregationBuckets(state).filter((label) =>
+ unselectedLabelsKeys(state)?.includes(label.key),
+ );
-export const currentScope = (state) => findKey(state.navigation, { active: true });
+export const unappliedNewLabels = (state) =>
+ labelAggregationBuckets(state).filter((label) =>
+ unappliedNewLabelKeys(state)?.includes(label.key),
+ );
-export const queryLanguageFilters = (state) => state.query[languageFilterData.filterParam] || [];
+export const currentScope = (state) => findKey(state.navigation, { active: true });
export const navigationItems = (state) =>
Object.values(state.navigation).map((item) => ({
diff --git a/app/assets/javascripts/security_configuration/components/training_provider_list.vue b/app/assets/javascripts/security_configuration/components/training_provider_list.vue
index f5f88e12163..d424ec6dfeb 100644
--- a/app/assets/javascripts/security_configuration/components/training_provider_list.vue
+++ b/app/assets/javascripts/security_configuration/components/training_provider_list.vue
@@ -9,7 +9,7 @@ import {
GlSkeletonLoader,
GlIcon,
} from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import SafeHtml from '~/vue_shared/directives/safe_html';
import Tracking from '~/tracking';
import { __, s__ } from '~/locale';
diff --git a/app/assets/javascripts/sentry/init_sentry.js b/app/assets/javascripts/sentry/init_sentry.js
index 6f32c8c4165..722741b50e4 100644
--- a/app/assets/javascripts/sentry/init_sentry.js
+++ b/app/assets/javascripts/sentry/init_sentry.js
@@ -23,7 +23,7 @@ const initSentry = () => {
const client = new BrowserClient({
// Sentry.init(...) options
dsn: gon.sentry_dsn,
- release: gon.version,
+ release: gon.revision,
allowUrls:
process.env.NODE_ENV === 'production'
? [gon.gitlab_url]
@@ -56,7 +56,7 @@ const initSentry = () => {
hub.bindClient(client);
hub.setTags({
- revision: gon.revision,
+ version: gon.version,
feature_category: gon.feature_category,
page,
});
@@ -75,7 +75,7 @@ const initSentry = () => {
// The _Sentry object is globally exported so it can be used by
// ./sentry_browser_wrapper.js
- // This hack allows us to load a single version of `@sentry/browser`
+ // This hack allows us to load a single version of `~/sentry/sentry_browser_wrapper`
// in the browser, see app/views/layouts/_head.html.haml to find how it is imported.
// eslint-disable-next-line no-underscore-dangle
diff --git a/app/assets/javascripts/sentry/legacy_index.js b/app/assets/javascripts/sentry/legacy_index.js
index 604b982e128..688f8eb0a44 100644
--- a/app/assets/javascripts/sentry/legacy_index.js
+++ b/app/assets/javascripts/sentry/legacy_index.js
@@ -25,7 +25,7 @@ index();
// The _Sentry object is globally exported so it can be used by
// ./sentry_browser_wrapper.js
-// This hack allows us to load a single version of `@sentry/browser`
+// This hack allows us to load a single version of `~/sentry/sentry_browser_wrapper`
// in the browser, see app/views/layouts/_head.html.haml to find how it is imported.
// eslint-disable-next-line no-underscore-dangle
diff --git a/app/assets/javascripts/sentry/sentry_browser_wrapper.js b/app/assets/javascripts/sentry/sentry_browser_wrapper.js
index 03cf53fabef..99f5adf8e89 100644
--- a/app/assets/javascripts/sentry/sentry_browser_wrapper.js
+++ b/app/assets/javascripts/sentry/sentry_browser_wrapper.js
@@ -1,15 +1,23 @@
+/* eslint-disable no-console */
+
// The _Sentry object is globally exported so it can be used here
// This hack allows us to load a single version of `@sentry/browser`
-// in the browser (or none). See app/views/layouts/_head.html.haml
-// to find how it is imported.
+// in the browser (or none).
+
+// See app/views/layouts/_head.html.haml to find how it is imported.
-// This module wraps methods used by our production code.
-// Each export is names as we cannot export the entire namespace from *.
+// This module exports Sentry methods used by our production code.
/** @type {import('@sentry/core').captureException} */
export const captureException = (...args) => {
// eslint-disable-next-line no-underscore-dangle
const Sentry = window._Sentry;
+ // When Sentry is not configured during development, show console error
+ if (process.env.NODE_ENV === 'development' && !Sentry) {
+ console.error('[Sentry stub]', 'captureException(...) called with:', { ...args });
+ return;
+ }
+
Sentry?.captureException(...args);
};
diff --git a/app/assets/javascripts/service_ping_consent.js b/app/assets/javascripts/service_ping_consent.js
deleted file mode 100644
index 7d6e7e81f3b..00000000000
--- a/app/assets/javascripts/service_ping_consent.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import $ from 'jquery';
-import { createAlert } from '~/alert';
-import axios from './lib/utils/axios_utils';
-import { parseBoolean } from './lib/utils/common_utils';
-import { __ } from './locale';
-
-export default () => {
- $('body').on('click', '.js-service-ping-consent-action', (e) => {
- e.preventDefault();
- e.stopImmediatePropagation(); // overwrite rails listener
-
- const { url, checkEnabled, servicePingEnabled } = e.target.dataset;
- const data = {
- application_setting: {
- version_check_enabled: parseBoolean(checkEnabled),
- service_ping_enabled: parseBoolean(servicePingEnabled),
- },
- };
-
- const hideConsentMessage = () =>
- document.querySelector('.service-ping-consent-message .js-close')?.click();
-
- axios
- .put(url, data)
- .then(() => {
- hideConsentMessage();
- })
- .catch(() => {
- hideConsentMessage();
- createAlert({
- message: __('Something went wrong. Try again later.'),
- });
- });
- });
-};
diff --git a/app/assets/javascripts/sessions/new/components/update_email.vue b/app/assets/javascripts/sessions/new/components/update_email.vue
index 124cd671169..f9b9a063808 100644
--- a/app/assets/javascripts/sessions/new/components/update_email.vue
+++ b/app/assets/javascripts/sessions/new/components/update_email.vue
@@ -1,6 +1,7 @@
<script>
import { GlForm, GlFormGroup, GlFormInput, GlButton } from '@gitlab/ui';
import { createAlert, VARIANT_SUCCESS } from '~/alert';
+import { isUserEmail } from '~/lib/utils/forms';
import axios from '~/lib/utils/axios_utils';
import {
I18N_EMAIL,
@@ -10,7 +11,6 @@ import {
I18N_EMAIL_INVALID,
I18N_UPDATE_EMAIL_SUCCESS,
I18N_GENERIC_ERROR,
- EMAIL_REGEXP,
SUCCESS_RESPONSE,
FAILURE_RESPONSE,
} from '../constants';
@@ -48,7 +48,7 @@ export default {
return '';
}
- if (!EMAIL_REGEXP.test(this.email)) {
+ if (!isUserEmail(this.email)) {
return I18N_EMAIL_INVALID;
}
diff --git a/app/assets/javascripts/sessions/new/constants.js b/app/assets/javascripts/sessions/new/constants.js
index e9bd26099aa..eb2bc25d958 100644
--- a/app/assets/javascripts/sessions/new/constants.js
+++ b/app/assets/javascripts/sessions/new/constants.js
@@ -25,6 +25,5 @@ export const I18N_UPDATE_EMAIL_SUCCESS = s__(
);
export const VERIFICATION_CODE_REGEX = /^\d{6}$/;
-export const EMAIL_REGEXP = /^[^@\s]+@[^@\s]+$/; // Taken from DeviseEmailValidator
export const SUCCESS_RESPONSE = 'success';
export const FAILURE_RESPONSE = 'failure';
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue
index 609a9355d20..745122afb4a 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue
@@ -47,7 +47,6 @@ export default {
class="js-sidebar-dropdown-toggle edit-link btn gl-text-gray-900! gl-ml-auto hide-collapsed btn-default btn-sm gl-button btn-default-tertiary float-right"
href="#"
data-test-id="edit-link"
- data-qa-selector="edit_link"
data-track-action="click_edit_button"
data-track-label="right_sidebar"
data-track-property="assignee"
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees.vue b/app/assets/javascripts/sidebar/components/assignees/assignees.vue
index 9d6a8bf47e0..55c5b04dbe3 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignees.vue
@@ -58,7 +58,6 @@ export default {
type="button"
class="gl-button btn-link gl-reset-color!"
data-testid="assign-yourself"
- data-qa-selector="assign_yourself_button"
@click="assignSelf"
>
{{ __('assign yourself') }}
diff --git a/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_value.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_value.vue
index 5ca18969f0b..06ac2cb715d 100644
--- a/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_value.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_value.vue
@@ -58,7 +58,6 @@ export default {
<template v-for="label in sortedSelectedLabels" v-else>
<gl-label
:key="label.id"
- data-qa-selector="selected_label_content"
:data-qa-label-name="label.title"
:title="label.title"
:description="label.description"
diff --git a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_header.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_header.vue
index 377200ab804..3d9a5893c67 100644
--- a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_header.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_header.vue
@@ -81,7 +81,6 @@ export default {
:value="searchKey"
:placeholder="__('Search labels')"
:disabled="labelsFetchInProgress"
- data-qa-selector="dropdown_input_field"
data-testid="dropdown-input-field"
@input="$emit('input', $event)"
@keydown.enter="$emit('searchEnter', $event)"
diff --git a/app/assets/javascripts/sidebar/components/lock/edit_form.vue b/app/assets/javascripts/sidebar/components/lock/edit_form.vue
index c9e651370f9..1497b229a59 100644
--- a/app/assets/javascripts/sidebar/components/lock/edit_form.vue
+++ b/app/assets/javascripts/sidebar/components/lock/edit_form.vue
@@ -27,11 +27,10 @@ export default {
<gl-sprintf
:message="
__(
- 'Unlock this %{issuableDisplayName}? %{strongStart}Everyone%{strongEnd} will be able to comment.',
+ 'Unlock this discussion? %{strongStart}Everyone%{strongEnd} will be able to comment.',
)
"
>
- <template #issuableDisplayName>{{ issuableDisplayName }}</template>
<template #strong="{ content }"
><strong>{{ content }}</strong></template
>
@@ -42,11 +41,10 @@ export default {
<gl-sprintf
:message="
__(
- 'Lock this %{issuableDisplayName}? Only %{strongStart}project members%{strongEnd} will be able to comment.',
+ 'Lock this discussion? Only %{strongStart}project members%{strongEnd} will be able to comment.',
)
"
>
- <template #issuableDisplayName>{{ issuableDisplayName }}</template>
<template #strong="{ content }"
><strong>{{ content }}</strong></template
>
diff --git a/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue b/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue
index 165499696de..977d1d6f668 100644
--- a/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue
+++ b/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue
@@ -50,12 +50,12 @@ export default {
issueCapitalized: __('Issue'),
mergeRequest: __('merge request'),
mergeRequestCapitalized: __('Merge request'),
- lockingMergeRequest: __('Locking %{issuableDisplayName}'),
- unlockingMergeRequest: __('Unlocking %{issuableDisplayName}'),
- lockMergeRequest: __('Lock %{issuableDisplayName}'),
- unlockMergeRequest: __('Unlock %{issuableDisplayName}'),
- lockedMessage: __('%{issuableDisplayName} locked.'),
- unlockedMessage: __('%{issuableDisplayName} unlocked.'),
+ lockingMergeRequest: __('Locking discussion'),
+ unlockingMergeRequest: __('Unlocking discussion'),
+ lockMergeRequest: __('Lock discussion'),
+ unlockMergeRequest: __('Unlock discussion'),
+ lockedMessage: __('Discussion locked.'),
+ unlockedMessage: __('Discussion unlocked.'),
},
data() {
return {
@@ -152,7 +152,7 @@ export default {
})
.catch(() => {
const alertMessage = __(
- 'Something went wrong trying to change the locked state of this %{issuableDisplayName}',
+ 'Something went wrong trying to change the locked state of the discussion',
);
createAlert({
message: sprintf(alertMessage, { issuableDisplayName: this.issuableDisplayName }),
@@ -170,9 +170,14 @@ export default {
</script>
<template>
- <li v-if="isMovedMrSidebar && isIssuable" class="gl-dropdown-item">
- <button type="button" class="dropdown-item" data-testid="issuable-lock" @click="toggleLocked">
- <span class="gl-dropdown-item-text-wrapper">
+ <li v-if="isMovedMrSidebar && isIssuable" class="gl-new-dropdown-item">
+ <button
+ type="button"
+ class="gl-new-dropdown-item-content"
+ data-testid="issuable-lock"
+ @click="toggleLocked"
+ >
+ <span class="gl-new-dropdown-item-text-wrapper">
<template v-if="isLoading">
<gl-loading-icon inline size="sm" /> {{ lockToggleInProgressText }}
</template>
diff --git a/app/assets/javascripts/sidebar/components/move/issuable_move_dropdown.vue b/app/assets/javascripts/sidebar/components/move/issuable_move_dropdown.vue
index 34a4da946d6..ea8e0c4b950 100644
--- a/app/assets/javascripts/sidebar/components/move/issuable_move_dropdown.vue
+++ b/app/assets/javascripts/sidebar/components/move/issuable_move_dropdown.vue
@@ -1,26 +1,20 @@
<script>
import {
GlIcon,
- GlLoadingIcon,
- GlDropdown,
- GlDropdownForm,
- GlDropdownItem,
- GlSearchBoxByType,
GlButton,
+ GlCollapsibleListbox,
GlTooltipDirective as GlTooltip,
} from '@gitlab/ui';
-
+import { debounce } from 'lodash';
+import { __ } from '~/locale';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import axios from '~/lib/utils/axios_utils';
export default {
components: {
GlIcon,
- GlLoadingIcon,
- GlDropdown,
- GlDropdownForm,
- GlDropdownItem,
- GlSearchBoxByType,
GlButton,
+ GlCollapsibleListbox,
},
directives: {
GlTooltip,
@@ -51,82 +45,58 @@ export default {
},
data() {
return {
- projectsListLoading: false,
- projectsListLoadFailed: false,
- searchKey: '',
projects: [],
- selectedProject: null,
- projectItemClick: false,
+ projectsList: [],
+ selectedProjects: [],
+ noResultsText: '',
+ isSearching: false,
};
},
- computed: {
- hasNoSearchResults() {
- return Boolean(
- !this.projectsListLoading &&
- !this.projectsListLoadFailed &&
- this.searchKey &&
- !this.projects.length,
- );
- },
- failedToLoadResults() {
- return !this.projectsListLoading && this.projectsListLoadFailed;
- },
- },
- watch: {
- searchKey(value = '') {
- this.fetchProjects(value);
- },
+ mounted() {
+ this.fetchProjects = debounce(this.fetchProjects, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
},
methods: {
- fetchProjects(search = '') {
- this.projectsListLoading = true;
- this.projectsListLoadFailed = false;
- return axios
- .get(this.projectsFetchPath, {
+ triggerSearch() {
+ this.$refs.dropdown.search();
+ },
+ async fetchProjects(search = '') {
+ this.isSearching = true;
+
+ try {
+ const { data } = await axios.get(this.projectsFetchPath, {
params: {
search,
},
- })
- .then(({ data }) => {
- this.projects = data;
- this.$refs.searchInput.focusInput();
- })
- .catch(() => {
- this.projectsListLoadFailed = true;
- })
- .finally(() => {
- this.projectsListLoading = false;
});
- },
- isSelectedProject(project) {
- if (this.selectedProject) {
- return this.selectedProject.id === project.id;
- }
- return false;
- },
- /**
- * This handler is to prevent dropdown
- * from closing when an item is selected
- * and emit an event only when dropdown closes.
- */
- handleDropdownHide(e) {
- if (this.projectItemClick) {
- e.preventDefault();
- this.projectItemClick = false;
- } else {
- this.$emit('dropdown-close');
+ this.projects = data;
+ this.projectsList = data.map((item) => ({
+ value: item.id,
+ text: item.name_with_namespace,
+ }));
+
+ if (!this.projectsList.length) {
+ this.noResultsText = __('No matching results');
+ }
+ } catch (e) {
+ this.noResultsText = __('Failed to load projects');
+ } finally {
+ this.isSearching = false;
}
},
- handleDropdownCloseClick() {
- this.$refs.dropdown.hide();
- },
- handleProjectSelect(project) {
- this.selectedProject = project.id === this.selectedProject?.id ? null : project;
- this.projectItemClick = true;
+ handleProjectSelect(items) {
+ // hack: simulate a single select to prevent the dropdown from closing
+ // todo: switch back to single select when https://gitlab.com/gitlab-org/gitlab-ui/-/issues/2363 is fixed
+ this.selectedProjects = [items[items.length - 1]];
},
handleMoveClick() {
- this.$refs.dropdown.hide();
- this.$emit('move-issuable', this.selectedProject);
+ this.$refs.dropdown.close();
+ this.$emit(
+ 'move-issuable',
+ this.projects.find((item) => item.id === this.selectedProjects[0]),
+ );
+ },
+ handleDropdownHide() {
+ this.$emit('dropdown-close');
},
},
};
@@ -143,79 +113,45 @@ export default {
>
<gl-icon name="arrow-right" />
</div>
- <gl-dropdown
+ <gl-collapsible-listbox
ref="dropdown"
+ v-model="selectedProjects"
+ :items="projectsList"
:block="true"
- :disabled="moveInProgress || disabled"
- class="hide-collapsed"
- toggle-class="js-sidebar-dropdown-toggle"
- @shown="fetchProjects"
- @hide="handleDropdownHide"
+ :multiple="true"
+ :searchable="true"
+ :searching="isSearching"
+ :search-placeholder="__('Search project')"
+ :no-results-text="noResultsText"
+ :header-text="dropdownButtonTitle"
+ @hidden="handleDropdownHide"
+ @shown="triggerSearch"
+ @search="fetchProjects"
+ @select="handleProjectSelect"
>
- <template #button-content
- ><gl-loading-icon v-if="moveInProgress" size="sm" class="gl-mr-3" />{{
- dropdownButtonTitle
- }}</template
- >
- <gl-dropdown-form class="gl-pt-0">
- <div
- data-testid="header"
- class="gl-display-flex gl-pb-3 gl-border-1 gl-border-b-solid gl-border-gray-100"
- >
- <span class="gl-flex-grow-1 gl-text-center gl-font-weight-bold gl-py-1">{{
- dropdownHeaderTitle
- }}</span>
- <gl-button
- variant="link"
- icon="close"
- class="gl-mr-2 gl-w-auto! gl-p-2!"
- :aria-label="__('Close')"
- @click.prevent="handleDropdownCloseClick"
- />
- </div>
- <gl-search-box-by-type
- ref="searchInput"
- v-model.trim="searchKey"
- :placeholder="__('Search project')"
- :debounce="300"
- />
- <div data-testid="content" class="dropdown-content">
- <gl-loading-icon v-if="projectsListLoading" size="lg" class="gl-p-5" />
- <ul v-else>
- <gl-dropdown-item
- v-for="project in projects"
- :key="project.id"
- is-check-item
- :is-checked="isSelectedProject(project)"
- @click.stop.prevent="handleProjectSelect(project)"
- >{{ project.name_with_namespace }}</gl-dropdown-item
- >
- </ul>
- <div v-if="hasNoSearchResults" class="gl-text-center gl-p-3">
- {{ __('No matching results') }}
- </div>
- <div
- v-if="failedToLoadResults"
- data-testid="failed-load-results"
- class="gl-text-center gl-p-3"
- >
- {{ __('Failed to load projects') }}
- </div>
- </div>
- <div
- data-testid="footer"
- class="gl-pt-3 gl-px-3 gl-border-1 gl-border-t-solid gl-border-gray-100"
+ <template #toggle>
+ <gl-button
+ :loading="moveInProgress"
+ size="medium"
+ class="gl-w-full js-sidebar-dropdown-toggle hide-collapsed"
+ data-testid="dropdown-button"
+ :disabled="moveInProgress || disabled"
+ >{{ dropdownButtonTitle }}</gl-button
>
+ </template>
+ <template #footer>
+ <div data-testid="footer" class="gl-p-3">
<gl-button
category="primary"
variant="confirm"
- :disabled="!Boolean(selectedProject)"
- class="gl-w-full issuable-move-button"
+ :disabled="!Boolean(selectedProjects.length)"
+ class="gl-w-full"
+ data-testid="dropdown-move-button"
@click="handleMoveClick"
>{{ __('Move') }}</gl-button
>
</div>
- </gl-dropdown-form>
- </gl-dropdown>
+ </template>
+ </gl-collapsible-listbox>
</div>
</template>
diff --git a/app/assets/javascripts/sidebar/components/participants/sidebar_participants_widget.vue b/app/assets/javascripts/sidebar/components/participants/sidebar_participants_widget.vue
index b764d660d63..40893f10109 100644
--- a/app/assets/javascripts/sidebar/components/participants/sidebar_participants_widget.vue
+++ b/app/assets/javascripts/sidebar/components/participants/sidebar_participants_widget.vue
@@ -49,9 +49,6 @@ export default {
error,
});
},
- context: {
- isSingleRequest: true,
- },
},
},
computed: {
diff --git a/app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar.vue b/app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar.vue
index a7db3b3d09f..d8e61c135e7 100644
--- a/app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar.vue
+++ b/app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar.vue
@@ -40,7 +40,6 @@ export default {
:width="imgSize"
:class="`s${imgSize}`"
class="avatar avatar-inline m-0"
- data-qa-selector="avatar_image"
/>
<gl-icon v-if="hasMergeIcon" name="warning-solid" aria-hidden="true" class="merge-icon" />
</span>
diff --git a/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue b/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue
index ee9edd6a022..1bcbf2167e9 100644
--- a/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue
+++ b/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue
@@ -62,7 +62,11 @@ export default {
<collapsed-reviewer-list :users="sortedReviewers" :issuable-type="issuableType" />
<div class="value hide-collapsed">
- <span v-if="hasNoUsers" class="no-value" data-testid="no-value">
+ <span
+ v-if="hasNoUsers"
+ class="no-value gl-display-flex gl-font-base gl-line-height-normal"
+ data-testid="no-value"
+ >
{{ __('None') }}
<template v-if="editable">
-
@@ -71,7 +75,6 @@ export default {
variant="link"
class="gl-ml-2"
data-testid="assign-yourself"
- data-qa-selector="assign_yourself_button"
@click="assignSelf"
>
<span class="gl-text-gray-500 gl-hover-text-blue-800">{{ __('assign yourself') }}</span>
diff --git a/app/assets/javascripts/sidebar/components/severity/sidebar_severity_widget.vue b/app/assets/javascripts/sidebar/components/severity/sidebar_severity_widget.vue
index e2a3efa096f..e14fee5bfb8 100644
--- a/app/assets/javascripts/sidebar/components/severity/sidebar_severity_widget.vue
+++ b/app/assets/javascripts/sidebar/components/severity/sidebar_severity_widget.vue
@@ -112,7 +112,7 @@ export default {
</script>
<template>
- <div ref="sidebarSeverity" class="block">
+ <div ref="sidebarSeverity" class="block" data-testid="severity-block-container">
<sidebar-editable-item
ref="toggle"
:loading="isUpdating"
@@ -131,7 +131,7 @@ export default {
</gl-sprintf>
</gl-tooltip>
</div>
- <div class="hide-collapsed">
+ <div class="hide-collapsed" data-testid="incident-severity">
<severity-token :severity="selectedItem" />
</div>
</template>
diff --git a/app/assets/javascripts/sidebar/components/toggle/toggle_sidebar.vue b/app/assets/javascripts/sidebar/components/toggle/toggle_sidebar.vue
index ba0bf783315..7ce1ceb4bb8 100644
--- a/app/assets/javascripts/sidebar/components/toggle/toggle_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/toggle/toggle_sidebar.vue
@@ -1,6 +1,7 @@
<script>
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
name: 'ToggleSidebar',
@@ -10,6 +11,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
+ mixins: [glFeatureFlagsMixin()],
props: {
collapsed: {
type: Boolean,
@@ -29,7 +31,13 @@ export default {
return this.collapsed ? 'chevron-double-lg-left' : 'chevron-double-lg-right';
},
allCssClasses() {
- return [this.cssClasses, { 'js-sidebar-collapsed': this.collapsed }];
+ return [
+ this.cssClasses,
+ {
+ 'js-sidebar-collapsed': this.collapsed,
+ 'gl-mt-2': this.glFeatures.notificationsTodosButtons,
+ },
+ ];
},
},
watch: {
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
index 4b6dbdcc2c9..12e60a9ed4e 100644
--- a/app/assets/javascripts/sidebar/mount_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -799,8 +799,7 @@ export function mountAssigneesDropdown() {
});
}
-const isAssigneesWidgetShown =
- (isInIssuePage() || isInDesignPage() || isInMRPage()) && gon.features.issueAssigneesWidget;
+const isAssigneesWidgetShown = isInIssuePage() || isInDesignPage() || isInMRPage();
export function mountSidebar(mediator, store) {
mountSidebarTodoWidget();
diff --git a/app/assets/javascripts/sidebar/queries/constants.js b/app/assets/javascripts/sidebar/queries/constants.js
index 0844abc4599..6bcdc01a003 100644
--- a/app/assets/javascripts/sidebar/queries/constants.js
+++ b/app/assets/javascripts/sidebar/queries/constants.js
@@ -12,8 +12,8 @@ import {
WORKSPACE_PROJECT,
} from '~/issues/constants';
import updateAlertAssigneesMutation from '~/vue_shared/alert_details/graphql/mutations/alert_set_assignees.mutation.graphql';
-import abuseReportLabelsQuery from '~/admin/abuse_report/components/graphql/abuse_report_labels.query.graphql';
-import createAbuseReportLabelMutation from '~/admin/abuse_report/components/graphql/create_abuse_report_label.mutation.graphql';
+import abuseReportLabelsQuery from '~/admin/abuse_report/graphql/abuse_report_labels.query.graphql';
+import createAbuseReportLabelMutation from '~/admin/abuse_report/graphql/create_abuse_report_label.mutation.graphql';
import createGroupOrProjectLabelMutation from '../components/labels/labels_select_widget/graphql/create_label.mutation.graphql';
import updateTestCaseLabelsMutation from '../components/labels/labels_select_widget/graphql/update_test_case_labels.mutation.graphql';
import epicLabelsQuery from '../components/labels/labels_select_widget/graphql/epic_labels.query.graphql';
diff --git a/app/assets/javascripts/silent_mode_settings/components/app.vue b/app/assets/javascripts/silent_mode_settings/components/app.vue
index 2dd0449448c..a151492c75c 100644
--- a/app/assets/javascripts/silent_mode_settings/components/app.vue
+++ b/app/assets/javascripts/silent_mode_settings/components/app.vue
@@ -1,5 +1,5 @@
<script>
-import { GlToggle, GlBadge } from '@gitlab/ui';
+import { GlToggle } from '@gitlab/ui';
import { updateApplicationSettings } from '~/rest_api';
import { createAlert } from '~/alert';
import toast from '~/vue_shared/plugins/global_toast';
@@ -13,11 +13,9 @@ export default {
saveError: s__('SilentMode|There was an error updating the Silent Mode Settings.'),
enabled: __('enabled'),
disabled: __('disabled'),
- experiment: __('Experiment'),
},
components: {
GlToggle,
- GlBadge,
},
props: {
isSilentModeEnabled: {
@@ -62,9 +60,5 @@ export default {
:label="$options.i18n.toggleLabel"
:is-loading="isLoading"
@change="updateSilentModeSettings"
- >
- <template #label
- >{{ $options.i18n.toggleLabel }} <gl-badge>{{ $options.i18n.experiment }}</gl-badge></template
- >
- </gl-toggle>
+ />
</template>
diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js
index 11896a75798..1e01da795e8 100644
--- a/app/assets/javascripts/single_file_diff.js
+++ b/app/assets/javascripts/single_file_diff.js
@@ -40,9 +40,12 @@ export default class SingleFileDiff {
this.$chevronDownIcon.removeClass('gl-display-none');
}
- $('.js-file-title, .click-to-expand', this.file).on('click', (e) => {
+ $('.js-file-title', this.file).on('click', (e) => {
this.toggleDiff($(e.target));
});
+ $('.click-to-expand', this.file).on('click', (e) => {
+ this.toggleDiff($(e.currentTarget));
+ });
}
toggleDiff($target, cb) {
diff --git a/app/assets/javascripts/snippets/components/snippet_header.vue b/app/assets/javascripts/snippets/components/snippet_header.vue
index 56ea931fc8c..573b8777ade 100644
--- a/app/assets/javascripts/snippets/components/snippet_header.vue
+++ b/app/assets/javascripts/snippets/components/snippet_header.vue
@@ -213,7 +213,7 @@ export default {
</script>
<template>
<div class="detail-page-header">
- <div class="detail-page-header-body">
+ <div class="detail-page-header-body gl-align-items-baseline">
<div
class="snippet-box has-tooltip d-flex align-items-center gl-mr-2 mb-1"
data-testid="snippet-container"
@@ -235,12 +235,20 @@ export default {
<template #author>
<a :href="snippet.author.webUrl" class="d-inline">
<gl-avatar :size="24" :src="snippet.author.avatarUrl" />
- <span class="bold">{{ snippet.author.name }}</span>
+ <span class="bold gl-display-none gl-sm-display-inline">{{
+ snippet.author.name
+ }}</span>
+ <strong
+ v-if="snippet.author.username"
+ data-testid="authored-username"
+ class="gl-display-inline gl-sm-display-none!"
+ >@{{ snippet.author.username }}</strong
+ >
</a>
<gl-emoji
v-if="snippet.author.status"
v-gl-tooltip
- class="gl-vertical-align-baseline font-size-inherit gl-mr-1"
+ class="gl-vertical-align-baseline gl-reset-font-size gl-mr-1"
:title="snippet.author.status.message"
:data-name="snippet.author.status.emoji"
/>
@@ -249,7 +257,7 @@ export default {
</div>
</div>
- <div v-if="hasPersonalSnippetActions" class="detail-page-header-actions">
+ <div v-if="hasPersonalSnippetActions" class="detail-page-header-actions gl-align-self-start">
<div class="d-none d-sm-flex">
<template v-for="(action, index) in personalSnippetActions">
<div
diff --git a/app/assets/javascripts/super_sidebar/components/create_menu.vue b/app/assets/javascripts/super_sidebar/components/create_menu.vue
index 279e689bd8d..e8410a51905 100644
--- a/app/assets/javascripts/super_sidebar/components/create_menu.vue
+++ b/app/assets/javascripts/super_sidebar/components/create_menu.vue
@@ -70,7 +70,6 @@ export default {
:toggle-text="$options.i18n.createNew"
:toggle-id="$options.toggleId"
:dropdown-offset="dropdownOffset"
- data-qa-selector="new_menu_toggle"
data-testid="new-menu-toggle"
@shown="dropdownOpen = true"
@hidden="dropdownOpen = false"
diff --git a/app/assets/javascripts/super_sidebar/components/flyout_menu.vue b/app/assets/javascripts/super_sidebar/components/flyout_menu.vue
index e73b9b275ee..414e4a54a8e 100644
--- a/app/assets/javascripts/super_sidebar/components/flyout_menu.vue
+++ b/app/assets/javascripts/super_sidebar/components/flyout_menu.vue
@@ -139,8 +139,8 @@ export default {
:key="item.id"
:item="item"
:is-flyout="true"
- @pin-add="(itemId) => $emit('pin-add', itemId)"
- @pin-remove="(itemId) => $emit('pin-remove', itemId)"
+ @pin-add="(itemId, itemTitle) => $emit('pin-add', itemId, itemTitle)"
+ @pin-remove="(itemId, itemTitle) => $emit('pin-remove', itemId, itemTitle)"
/>
</ul>
<svg
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/command_palette_items.vue b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/command_palette_items.vue
index b85b163cea9..1a681d6e9bd 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/command_palette_items.vue
+++ b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/command_palette_items.vue
@@ -2,7 +2,7 @@
import { debounce } from 'lodash';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { GlDisclosureDropdownGroup, GlLoadingIcon } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import axios from '~/lib/utils/axios_utils';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import Tracking from '~/tracking';
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue b/app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue
index 61fa360c41f..e6137bda401 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue
+++ b/app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue
@@ -15,7 +15,14 @@ import { truncate } from '~/lib/utils/text_utility';
import { visitUrl } from '~/lib/utils/url_utility';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { sprintf } from '~/locale';
-import { ARROW_DOWN_KEY, ARROW_UP_KEY, END_KEY, HOME_KEY, ESC_KEY } from '~/lib/utils/keys';
+import {
+ ARROW_DOWN_KEY,
+ ARROW_UP_KEY,
+ END_KEY,
+ HOME_KEY,
+ ESC_KEY,
+ NUMPAD_ENTER_KEY,
+} from '~/lib/utils/keys';
import {
COMMAND_PALETTE,
MIN_SEARCH_TERM,
@@ -215,6 +222,8 @@ export default {
this.focusNextItem(event, elements, 1);
} else if (code === ESC_KEY) {
this.$refs.searchModal.close();
+ } else if (code === NUMPAD_ENTER_KEY) {
+ event.target?.firstChild.click();
} else {
stop = false;
}
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/components/global_search_default_places.vue b/app/assets/javascripts/super_sidebar/components/global_search/components/global_search_default_places.vue
index 9167be5c1cc..914d3c393f5 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/components/global_search_default_places.vue
+++ b/app/assets/javascripts/super_sidebar/components/global_search/components/global_search_default_places.vue
@@ -1,5 +1,6 @@
<script>
import { GlDisclosureDropdownGroup } from '@gitlab/ui';
+import { kebabCase } from 'lodash';
import { PLACES } from '~/vue_shared/global_search/constants';
import { TRACKING_UNKNOWN_ID, TRACKING_UNKNOWN_PANEL } from '~/super_sidebar/constants';
import { TRACKING_CLICK_COMMAND_PALETTE_ITEM } from '../command_palette/constants';
@@ -20,7 +21,7 @@ export default {
group() {
return {
name: this.$options.i18n.PLACES,
- items: this.contextSwitcherLinks.map(({ title, link }) => ({
+ items: this.contextSwitcherLinks.map(({ title, link, ...rest }) => ({
text: title,
href: link,
extraAttrs: {
@@ -35,6 +36,12 @@ export default {
// QA attributes
'data-testid': 'places-item-link',
'data-qa-places-item': title,
+
+ // Any other data- attributes (e.g., for @rails/ujs)
+ ...Object.entries(rest).reduce((acc, [name, value]) => {
+ if (name.startsWith('data')) acc[kebabCase(name)] = value;
+ return acc;
+ }, {}),
},
})),
};
diff --git a/app/assets/javascripts/super_sidebar/components/menu_section.vue b/app/assets/javascripts/super_sidebar/components/menu_section.vue
index 91b781b8235..a672e254004 100644
--- a/app/assets/javascripts/super_sidebar/components/menu_section.vue
+++ b/app/assets/javascripts/super_sidebar/components/menu_section.vue
@@ -145,8 +145,8 @@ export default {
:items="item.items"
@mouseover="isMouseOverFlyout = true"
@mouseleave="isMouseOverFlyout = false"
- @pin-add="(itemId) => $emit('pin-add', itemId)"
- @pin-remove="(itemId) => $emit('pin-remove', itemId)"
+ @pin-add="(itemId, itemTitle) => $emit('pin-add', itemId, itemTitle)"
+ @pin-remove="(itemId, itemTitle) => $emit('pin-remove', itemId, itemTitle)"
/>
<gl-collapse
@@ -162,8 +162,8 @@ export default {
v-for="subItem of item.items"
:key="`${item.title}-${subItem.title}`"
:item="subItem"
- @pin-add="(itemId) => $emit('pin-add', itemId)"
- @pin-remove="(itemId) => $emit('pin-remove', itemId)"
+ @pin-add="(itemId, itemTitle) => $emit('pin-add', itemId, itemTitle)"
+ @pin-remove="(itemId, itemTitle) => $emit('pin-remove', itemId, itemTitle)"
/>
</ul>
</slot>
diff --git a/app/assets/javascripts/super_sidebar/components/nav_item.vue b/app/assets/javascripts/super_sidebar/components/nav_item.vue
index 5416f86abeb..3ae33bf8b37 100644
--- a/app/assets/javascripts/super_sidebar/components/nav_item.vue
+++ b/app/assets/javascripts/super_sidebar/components/nav_item.vue
@@ -70,14 +70,16 @@ export default {
return {
isMouseIn: false,
canClickPinButton: false,
- pillCount: this.item.pill_count,
};
},
computed: {
+ pillData() {
+ return this.item.pill_count;
+ },
hasPill() {
return (
- Number.isFinite(this.pillCount) ||
- (typeof this.pillCount === 'string' && this.pillCount !== '')
+ Number.isFinite(this.pillData) ||
+ (typeof this.pillData === 'string' && this.pillData !== '')
);
},
isPinnable() {
@@ -188,12 +190,22 @@ export default {
eventHub.$off('updatePillValue', this.updatePillValue);
},
methods: {
+ pinAdd() {
+ this.$emit('pin-add', this.item.id, this.item.title);
+ },
+ pinRemove() {
+ this.$emit('pin-remove', this.item.id, this.item.title);
+ },
togglePointerEvents() {
this.canClickPinButton = this.isMouseIn;
},
updatePillValue({ value, itemId }) {
if (this.item.id === itemId) {
- this.pillCount = value;
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/428246
+ // fixing this linting issue is causing the pills not to async update
+ //
+ // eslint-disable-next-line vue/no-mutating-props
+ this.item.pill_count = value;
}
},
},
@@ -214,7 +226,6 @@ export default {
class="gl-relative gl-display-flex gl-align-items-center gl-min-h-7 gl-gap-3 gl-mb-1 gl-py-2 gl-text-black-normal! gl-hover-bg-t-gray-a-08 gl-focus-bg-t-gray-a-08 gl-text-decoration-none! gl-focus--focus show-on-focus-or-hover--control hide-on-focus-or-hover--control"
:class="computedLinkClasses"
data-testid="nav-item-link"
- data-qa-selector="nav_item_link"
>
<div
:class="[isActive ? 'gl-opacity-10' : 'gl-opacity-0']"
@@ -258,7 +269,7 @@ export default {
'hide-on-focus-or-hover--target transition-opacity-on-hover--target': isPinnable,
}"
>
- {{ pillCount }}
+ {{ pillData }}
</gl-badge>
</span>
</component>
@@ -273,7 +284,7 @@ export default {
data-testid="nav-item-unpin"
icon="thumbtack-solid"
size="small"
- @click="$emit('pin-remove', item.id)"
+ @click="pinRemove"
@transitionend="togglePointerEvents"
/>
<gl-button
@@ -286,7 +297,7 @@ export default {
data-testid="nav-item-pin"
icon="thumbtack"
size="small"
- @click="$emit('pin-add', item.id)"
+ @click="pinAdd"
@transitionend="togglePointerEvents"
/>
</template>
diff --git a/app/assets/javascripts/super_sidebar/components/pinned_section.vue b/app/assets/javascripts/super_sidebar/components/pinned_section.vue
index ea3e9e9df1f..05040218164 100644
--- a/app/assets/javascripts/super_sidebar/components/pinned_section.vue
+++ b/app/assets/javascripts/super_sidebar/components/pinned_section.vue
@@ -84,8 +84,8 @@ export default {
return { ...i, title };
});
},
- onPinRemove(itemId) {
- this.$emit('pin-remove', itemId);
+ onPinRemove(itemId, itemTitle) {
+ this.$emit('pin-remove', itemId, itemTitle);
},
},
};
@@ -113,7 +113,7 @@ export default {
:key="item.id"
:item="item"
is-in-pinned-section
- @pin-remove="onPinRemove"
+ @pin-remove="onPinRemove(item.id, item.title)"
/>
</draggable>
<li v-else class="gl-text-secondary gl-font-sm gl-py-3" style="margin-left: 2.5rem">
diff --git a/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue b/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue
index 772072c0996..c04addf5262 100644
--- a/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue
+++ b/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue
@@ -1,6 +1,7 @@
<script>
-import * as Sentry from '@sentry/browser';
import { GlBreakpointInstance, breakpoints } from '@gitlab/ui/dist/utils';
+import { s__, sprintf } from '~/locale';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import axios from '~/lib/utils/axios_utils';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { PANELS_WITH_PINS } from '../constants';
@@ -16,7 +17,10 @@ export default {
PinnedSection,
},
mixins: [glFeatureFlagsMixin()],
-
+ i18n: {
+ pinAdded: s__('Navigation|%{title} added to pinned items'),
+ pinRemoved: s__('Navigation|%{title} removed from pinned items'),
+ },
provide() {
return {
pinnedItemIds: this.changedPinnedItemIds,
@@ -111,12 +115,22 @@ export default {
window.removeEventListener('resize', this.decideFlyoutState);
},
methods: {
- createPin(itemId) {
+ createPin(itemId, itemTitle) {
this.changedPinnedItemIds.ids.push(itemId);
+ this.$toast.show(
+ sprintf(this.$options.i18n.pinAdded, {
+ title: itemTitle,
+ }),
+ );
this.updatePins();
},
- destroyPin(itemId) {
+ destroyPin(itemId, itemTitle) {
this.changedPinnedItemIds.ids = this.changedPinnedItemIds.ids.filter((id) => id !== itemId);
+ this.$toast.show(
+ sprintf(this.$options.i18n.pinRemoved, {
+ title: itemTitle,
+ }),
+ );
this.updatePins();
},
movePin(fromId, toId, isDownwards) {
diff --git a/app/assets/javascripts/super_sidebar/components/user_bar.vue b/app/assets/javascripts/super_sidebar/components/user_bar.vue
index 88ea4d828b7..3c47245a1a6 100644
--- a/app/assets/javascripts/super_sidebar/components/user_bar.vue
+++ b/app/assets/javascripts/super_sidebar/components/user_bar.vue
@@ -115,6 +115,7 @@ export default {
<gl-badge
v-if="sidebarData.gitlab_com_and_canary"
variant="success"
+ data-testid="canary-badge-link"
:href="sidebarData.canary_toggle_com_url"
size="sm"
>
diff --git a/app/assets/javascripts/super_sidebar/components/user_menu.vue b/app/assets/javascripts/super_sidebar/components/user_menu.vue
index 891e883b6c0..5712b716f48 100644
--- a/app/assets/javascripts/super_sidebar/components/user_menu.vue
+++ b/app/assets/javascripts/super_sidebar/components/user_menu.vue
@@ -8,7 +8,6 @@ import {
} from '@gitlab/ui';
import SafeHtml from '~/vue_shared/directives/safe_html';
import { s__, __, sprintf } from '~/locale';
-import NewNavToggle from '~/nav/components/new_nav_toggle.vue';
import Tracking from '~/tracking';
import PersistentUserCallout from '~/persistent_user_callout';
import { USER_MENU_TRACKING_DEFAULTS, DROPDOWN_Y_OFFSET, IMPERSONATING_OFFSET } from '../constants';
@@ -39,14 +38,13 @@ export default {
GlDisclosureDropdownGroup,
GlDisclosureDropdownItem,
GlButton,
- NewNavToggle,
UserMenuProfileItem,
},
directives: {
SafeHtml,
},
mixins: [Tracking.mixin()],
- inject: ['toggleNewNavEndpoint', 'isImpersonating'],
+ inject: ['isImpersonating'],
props: {
data: {
required: true,
@@ -301,13 +299,6 @@ export default {
/>
</gl-disclosure-dropdown-group>
- <gl-disclosure-dropdown-group bordered>
- <template #group-label>
- <span class="gl-font-sm">{{ $options.i18n.newNavigation.sectionTitle }}</span>
- </template>
- <new-nav-toggle :endpoint="toggleNewNavEndpoint" enabled new-navigation />
- </gl-disclosure-dropdown-group>
-
<gl-disclosure-dropdown-group
v-if="data.can_sign_out"
bordered
diff --git a/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js b/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js
index f9e488ea5ee..9e540175b48 100644
--- a/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js
+++ b/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import { GlToast } from '@gitlab/ui';
import { convertObjectPropsToCamelCase, parseBoolean } from '~/lib/utils/common_utils';
import { initStatusTriggers } from '../header';
import { JS_TOGGLE_EXPAND_CLASS } from './constants';
@@ -10,6 +11,8 @@ import {
import SuperSidebar from './components/super_sidebar.vue';
import SuperSidebarToggle from './components/super_sidebar_toggle.vue';
+Vue.use(GlToast);
+
const getTrialStatusWidgetData = (sidebarData) => {
if (sidebarData.trial_status_widget_data_attrs && sidebarData.trial_status_popover_data_attrs) {
const {
@@ -63,13 +66,7 @@ export const initSuperSidebar = () => {
if (!el) return false;
- const {
- rootPath,
- sidebar,
- toggleNewNavEndpoint,
- forceDesktopExpandedSidebar,
- commandPalette,
- } = el.dataset;
+ const { rootPath, sidebar, forceDesktopExpandedSidebar, commandPalette } = el.dataset;
bindSuperSidebarCollapsedEvents(forceDesktopExpandedSidebar);
initSuperSidebarCollapsedState(parseBoolean(forceDesktopExpandedSidebar));
@@ -95,7 +92,6 @@ export const initSuperSidebar = () => {
name: 'SuperSidebarRoot',
provide: {
rootPath,
- toggleNewNavEndpoint,
isImpersonating,
...getTrialStatusWidgetData(sidebarData),
commandPaletteCommands,
diff --git a/app/assets/javascripts/super_sidebar/utils.js b/app/assets/javascripts/super_sidebar/utils.js
index d2fb72adb85..3d6eef62ad2 100644
--- a/app/assets/javascripts/super_sidebar/utils.js
+++ b/app/assets/javascripts/super_sidebar/utils.js
@@ -1,4 +1,4 @@
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import AccessorUtilities from '~/lib/utils/accessor';
import { FREQUENT_ITEMS, FIFTEEN_MINUTES_IN_MS } from '~/frequent_items/constants';
import axios from '~/lib/utils/axios_utils';
diff --git a/app/assets/javascripts/tags/components/delete_tag_modal.vue b/app/assets/javascripts/tags/components/delete_tag_modal.vue
index c4f9db70d2a..9a0cc026223 100644
--- a/app/assets/javascripts/tags/components/delete_tag_modal.vue
+++ b/app/assets/javascripts/tags/components/delete_tag_modal.vue
@@ -151,7 +151,6 @@ export default {
ref="deleteTagButton"
:disabled="deleteButtonDisabled"
variant="danger"
- data-qa-selector="delete_tag_confirmation_button"
data-testid="delete-tag-confirmation-button"
@click="submitForm"
>{{ buttonText }}</gl-button
diff --git a/app/assets/javascripts/terraform/components/init_command_modal.vue b/app/assets/javascripts/terraform/components/init_command_modal.vue
index 74c41700f43..7962c8573df 100644
--- a/app/assets/javascripts/terraform/components/init_command_modal.vue
+++ b/app/assets/javascripts/terraform/components/init_command_modal.vue
@@ -40,15 +40,14 @@ export default {
},
methods: {
getModalInfoCopyStr() {
- const stateNameEncoded = this.stateName
- ? encodeURIComponent(this.stateName)
- : '<YOUR-STATE-NAME>';
+ const stateNameEncoded = this.stateName ? encodeURIComponent(this.stateName) : 'default';
return `export GITLAB_ACCESS_TOKEN=<YOUR-ACCESS-TOKEN>
+export TF_STATE_NAME=${stateNameEncoded}
terraform init \\
- -backend-config="address=${this.terraformApiUrl}/${stateNameEncoded}" \\
- -backend-config="lock_address=${this.terraformApiUrl}/${stateNameEncoded}/lock" \\
- -backend-config="unlock_address=${this.terraformApiUrl}/${stateNameEncoded}/lock" \\
+ -backend-config="address=${this.terraformApiUrl}/$TF_STATE_NAME" \\
+ -backend-config="lock_address=${this.terraformApiUrl}/$TF_STATE_NAME/lock" \\
+ -backend-config="unlock_address=${this.terraformApiUrl}/$TF_STATE_NAME/lock" \\
-backend-config="username=${this.username}" \\
-backend-config="password=$GITLAB_ACCESS_TOKEN" \\
-backend-config="lock_method=POST" \\
diff --git a/app/assets/javascripts/terraform/components/states_table.vue b/app/assets/javascripts/terraform/components/states_table.vue
index c88c528a632..273cd599308 100644
--- a/app/assets/javascripts/terraform/components/states_table.vue
+++ b/app/assets/javascripts/terraform/components/states_table.vue
@@ -11,14 +11,14 @@ import {
} from '@gitlab/ui';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { s__, sprintf } from '~/locale';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import StateActions from './states_table_actions.vue';
export default {
components: {
- CiBadgeLink,
+ CiIcon,
GlAlert,
GlBadge,
GlLink,
@@ -198,10 +198,10 @@ export default {
:id="`terraformJobStatusContainer${item.name}`"
class="gl-my-2"
>
- <ci-badge-link
+ <ci-icon
:id="`terraformJobStatus${item.name}`"
:status="pipelineDetailedStatus(item)"
- class="gl-py-1"
+ show-status-text
/>
<gl-tooltip
diff --git a/app/assets/javascripts/time_tracking/components/timelogs_app.vue b/app/assets/javascripts/time_tracking/components/timelogs_app.vue
index 7bb9b6c52a5..8464384ac7c 100644
--- a/app/assets/javascripts/time_tracking/components/timelogs_app.vue
+++ b/app/assets/javascripts/time_tracking/components/timelogs_app.vue
@@ -1,5 +1,4 @@
<script>
-import * as Sentry from '@sentry/browser';
import {
GlButton,
GlFormGroup,
@@ -8,6 +7,7 @@ import {
GlKeysetPagination,
GlDatepicker,
} from '@gitlab/ui';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { createAlert } from '~/alert';
import { formatTimeSpent } from '~/lib/utils/datetime_utility';
import { s__ } from '~/locale';
diff --git a/app/assets/javascripts/token_access/components/inbound_token_access.vue b/app/assets/javascripts/token_access/components/inbound_token_access.vue
index 7e55f56279e..345db1752f6 100644
--- a/app/assets/javascripts/token_access/components/inbound_token_access.vue
+++ b/app/assets/javascripts/token_access/components/inbound_token_access.vue
@@ -46,12 +46,6 @@ export default {
columnClass: 'gl-w-40p',
},
{
- key: 'namespace',
- label: __('Namespace'),
- thClass: 'gl-border-t-none!',
- columnClass: 'gl-w-40p',
- },
- {
key: 'actions',
label: '',
tdClass: 'gl-text-right',
diff --git a/app/assets/javascripts/token_access/components/outbound_token_access.vue b/app/assets/javascripts/token_access/components/outbound_token_access.vue
index 43aa9b94b3a..846b0d1791f 100644
--- a/app/assets/javascripts/token_access/components/outbound_token_access.vue
+++ b/app/assets/javascripts/token_access/components/outbound_token_access.vue
@@ -54,12 +54,6 @@ export default {
columnClass: 'gl-w-40p',
},
{
- key: 'namespace',
- label: __('Namespace'),
- thClass: 'gl-border-t-none!',
- columnClass: 'gl-w-40p',
- },
- {
key: 'actions',
label: '',
tdClass: 'gl-text-right',
diff --git a/app/assets/javascripts/token_access/components/token_projects_table.vue b/app/assets/javascripts/token_access/components/token_projects_table.vue
index ee88b4ec339..4245b39dec1 100644
--- a/app/assets/javascripts/token_access/components/token_projects_table.vue
+++ b/app/assets/javascripts/token_access/components/token_projects_table.vue
@@ -29,9 +29,6 @@ export default {
removeProject(project) {
this.$emit('removeProject', project);
},
- namespaceFallback(namespace) {
- return namespace?.fullPath || '';
- },
},
};
</script>
@@ -50,13 +47,7 @@ export default {
</template>
<template #cell(project)="{ item }">
- <span data-testid="token-access-project-name">{{ item.name }}</span>
- </template>
-
- <template #cell(namespace)="{ item }">
- <span data-testid="token-access-project-namespace">
- {{ namespaceFallback(item.namespace) }}
- </span>
+ <span data-testid="token-access-project-name">{{ item.fullPath }}</span>
</template>
<template #cell(actions)="{ item }">
diff --git a/app/assets/javascripts/token_access/graphql/cache_config.js b/app/assets/javascripts/token_access/graphql/cache_config.js
new file mode 100644
index 00000000000..2db534b7eb5
--- /dev/null
+++ b/app/assets/javascripts/token_access/graphql/cache_config.js
@@ -0,0 +1,14 @@
+export default {
+ typePolicies: {
+ Project: {
+ fields: {
+ ciCdSettings: {
+ merge: true,
+ },
+ ciJobTokenScope: {
+ merge: true,
+ },
+ },
+ },
+ },
+};
diff --git a/app/assets/javascripts/token_access/index.js b/app/assets/javascripts/token_access/index.js
index 9258d5eba45..45bd1921dbd 100644
--- a/app/assets/javascripts/token_access/index.js
+++ b/app/assets/javascripts/token_access/index.js
@@ -2,11 +2,12 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import TokenAccessApp from './components/token_access_app.vue';
+import cacheConfig from './graphql/cache_config';
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(),
+ defaultClient: createDefaultClient({}, { cacheConfig }),
});
export const initTokenAccess = (containerId = 'js-ci-token-access-app') => {
diff --git a/app/assets/javascripts/tracking/constants.js b/app/assets/javascripts/tracking/constants.js
index 46278152879..bc416b20e80 100644
--- a/app/assets/javascripts/tracking/constants.js
+++ b/app/assets/javascripts/tracking/constants.js
@@ -1,5 +1,7 @@
export const SNOWPLOW_JS_SOURCE = 'gitlab-javascript';
+export const MAX_LOCAL_STORAGE_QUEUE_SIZE = 100;
+
export const DEFAULT_SNOWPLOW_OPTIONS = {
namespace: 'gl',
hostname: window.location.hostname,
@@ -15,6 +17,7 @@ export const DEFAULT_SNOWPLOW_OPTIONS = {
forms: { allow: [] },
fields: { allow: [] },
},
+ maxLocalStorageQueueSize: MAX_LOCAL_STORAGE_QUEUE_SIZE,
};
export const ACTION_ATTR_SELECTOR = '[data-track-action]';
diff --git a/app/assets/javascripts/tracking/dispatch_snowplow_event.js b/app/assets/javascripts/tracking/dispatch_snowplow_event.js
index 99e4a6aa3c7..91512292eb6 100644
--- a/app/assets/javascripts/tracking/dispatch_snowplow_event.js
+++ b/app/assets/javascripts/tracking/dispatch_snowplow_event.js
@@ -1,4 +1,4 @@
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import getStandardContext from './get_standard_context';
export function dispatchSnowplowEvent(
diff --git a/app/assets/javascripts/users/profile/actions/components/user_actions_app.vue b/app/assets/javascripts/users/profile/actions/components/user_actions_app.vue
index 5dfa9c67852..f994cad6881 100644
--- a/app/assets/javascripts/users/profile/actions/components/user_actions_app.vue
+++ b/app/assets/javascripts/users/profile/actions/components/user_actions_app.vue
@@ -83,7 +83,13 @@ export default {
<template>
<span>
- <gl-disclosure-dropdown icon="ellipsis_v" category="tertiary" no-caret :items="dropdownItems" />
+ <gl-disclosure-dropdown
+ data-testid="user-profile-actions"
+ icon="ellipsis_v"
+ category="tertiary"
+ no-caret
+ :items="dropdownItems"
+ />
<abuse-category-selector
v-if="reportedUserId"
:reported-user-id="reportedUserId"
diff --git a/app/assets/javascripts/users/profile/components/report_abuse_button.vue b/app/assets/javascripts/users/profile/components/report_abuse_button.vue
deleted file mode 100644
index 0e41a214888..00000000000
--- a/app/assets/javascripts/users/profile/components/report_abuse_button.vue
+++ /dev/null
@@ -1,58 +0,0 @@
-<script>
-import { GlButton, GlTooltipDirective } from '@gitlab/ui';
-import { s__ } from '~/locale';
-import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
-
-import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
-
-export default {
- name: 'ReportAbuseButton',
- components: {
- GlButton,
- AbuseCategorySelector,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- inject: ['reportedUserId', 'reportedFromUrl'],
- i18n: {
- reportAbuse: s__('ReportAbuse|Report abuse to administrator'),
- },
- data() {
- return {
- open: false,
- };
- },
- computed: {
- buttonTooltipText() {
- return this.$options.i18n.reportAbuse;
- },
- },
- methods: {
- toggleDrawer(open) {
- this.open = open;
- },
- hideTooltips() {
- this.$root.$emit(BV_HIDE_TOOLTIP);
- },
- },
-};
-</script>
-<template>
- <span>
- <gl-button
- v-gl-tooltip="buttonTooltipText"
- category="primary"
- :aria-label="buttonTooltipText"
- icon="error"
- @click="toggleDrawer(true)"
- @mouseout="hideTooltips"
- />
- <abuse-category-selector
- :reported-user-id="reportedUserId"
- :reported-from-url="reportedFromUrl"
- :show-drawer="open"
- @close-drawer="toggleDrawer(false)"
- />
- </span>
-</template>
diff --git a/app/assets/javascripts/users/profile/index.js b/app/assets/javascripts/users/profile/index.js
deleted file mode 100644
index 3ae3cc2de98..00000000000
--- a/app/assets/javascripts/users/profile/index.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import Vue from 'vue';
-import ReportAbuseButton from './components/report_abuse_button.vue';
-
-export const initReportAbuse = () => {
- const el = document.getElementById('js-report-abuse');
-
- if (!el) return false;
-
- const { reportAbusePath, reportedUserId, reportedFromUrl } = el.dataset;
-
- return new Vue({
- el,
- name: 'ReportAbuseButtonRoot',
- provide: {
- reportAbusePath,
- reportedUserId: reportedUserId ? parseInt(reportedUserId, 10) : null,
- reportedFromUrl,
- },
- render(createElement) {
- return createElement(ReportAbuseButton);
- },
- });
-};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue
index 974b53caa15..524f2c045e6 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue
@@ -1,6 +1,7 @@
<script>
import { GlButton, GlSprintf } from '@gitlab/ui';
import { createAlert } from '~/alert';
+import { visitUrl } from '~/lib/utils/url_utility';
import { STATUS_MERGED } from '~/issues/constants';
import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import { HTTP_STATUS_UNAUTHORIZED } from '~/lib/utils/http_status';
@@ -114,6 +115,13 @@ export default {
return this.userHasApproved && !this.userCanApprove && this.mr.state !== STATUS_MERGED;
},
approvalText() {
+ // Repeating a text of this to keep i18n easier to do (vs, construcing a compound string)
+ if (this.requireSamlAuthToApprove) {
+ return this.isApproved && this.approvedBy.length > 0
+ ? s__('mrWidget|Approve additionally with SAML')
+ : s__('mrWidget|Approve with SAML');
+ }
+
return this.isApproved && this.approvedBy.length > 0
? s__('mrWidget|Approve additionally')
: s__('mrWidget|Approve');
@@ -161,14 +169,20 @@ export default {
.join(', ')
.concat('.');
},
+ requireSamlAuthToApprove() {
+ return this.mr.requireSamlAuthToApprove;
+ },
},
methods: {
approve() {
+ if (this.requireSamlAuthToApprove) {
+ this.approveWithSamlAuth();
+ return;
+ }
if (this.requirePasswordToApprove) {
this.$root.$emit(BV_SHOW_MODAL, this.modalId);
return;
}
-
this.updateApproval(
() => this.service.approveMergeRequest(),
() =>
@@ -179,6 +193,10 @@ export default {
),
);
},
+ approveWithSamlAuth() {
+ // Intentionally direct to SAML Identity Provider for renewed authorization even if SSO session exists
+ visitUrl(this.mr.samlApprovalPath);
+ },
approveWithAuth(data) {
this.updateApproval(
() => this.service.approveMergeRequestWithAuth(data),
@@ -236,7 +254,7 @@ export default {
};
</script>
<template>
- <div class="js-mr-approvals mr-section-container mr-widget-workflow">
+ <div v-if="approvals" class="js-mr-approvals mr-section-container mr-widget-workflow">
<state-container
:is-loading="$apollo.queries.approvals.loading"
:mr="mr"
@@ -258,7 +276,7 @@ export default {
:category="action.category"
:loading="isApproving"
class="gl-mr-3"
- data-qa-selector="approve_button"
+ data-testid="approve-button"
@click="action.action"
>
{{ action.text }}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary.vue b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary.vue
index 367395f4446..b2c44dee230 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary.vue
@@ -137,7 +137,7 @@ export default {
</script>
<template>
- <div data-qa-selector="approvals_summary_content">
+ <div data-testid="approvals-summary-content">
<span class="gl-font-weight-bold">{{ approvalLeftMessage }}</span>
<template v-if="hasApprovers">
<span v-if="approvalLeftMessage">{{ message }}</span>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/checks/conflicts.vue b/app/assets/javascripts/vue_merge_request_widget/components/checks/conflicts.vue
index 303952c787e..32c3f19014b 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/checks/conflicts.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/checks/conflicts.vue
@@ -72,6 +72,8 @@ export default {
<template>
<merge-checks-message :check="check">
- <action-buttons v-if="!isLoading" :tertiary-buttons="tertiaryActionsButtons" />
+ <template #failed>
+ <action-buttons v-if="!isLoading" :tertiary-buttons="tertiaryActionsButtons" />
+ </template>
</merge-checks-message>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/checks/constants.js b/app/assets/javascripts/vue_merge_request_widget/components/checks/constants.js
new file mode 100644
index 00000000000..431348e1d57
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/checks/constants.js
@@ -0,0 +1,6 @@
+export const COMPONENTS = {
+ conflict: () => import('./conflicts.vue'),
+ unresolved_discussions: () => import('./unresolved_discussions.vue'),
+ need_rebase: () => import('./rebase.vue'),
+ default: () => import('./message.vue'),
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/checks/message.vue b/app/assets/javascripts/vue_merge_request_widget/components/checks/message.vue
index d0d749aa441..058b9e1fe99 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/checks/message.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/checks/message.vue
@@ -1,10 +1,25 @@
<script>
+import { __ } from '~/locale';
import StatusIcon from '../widget/status_icon.vue';
const ICON_NAMES = {
failed: 'failed',
- allowed_to_fail: 'neutral',
- passed: 'success',
+ inactive: 'neutral',
+ success: 'success',
+};
+
+const FAILURE_REASONS = {
+ broken_status: __('Cannot merge the source into the target branch, due to a conflict.'),
+ ci_must_pass: __('Pipeline must succeed.'),
+ conflict: __('Merge conflicts must be resolved.'),
+ discussions_not_resolved: __('Unresolved discussions must be resolved.'),
+ draft_status: __('Merge request must not be draft.'),
+ not_open: __('Merge request must be open.'),
+ need_rebase: __('Merge request must be rebased, because a fast-forward merge is not possible.'),
+ not_approved: __('All required approvals must be given.'),
+ policies_denied: __('Denied licenses must be removed or approved.'),
+ merge_request_blocked: __('Merge request is blocked by another merge request.'),
+ status_checks_must_pass: __('Status checks must pass.'),
};
export default {
@@ -25,7 +40,10 @@ export default {
},
computed: {
iconName() {
- return ICON_NAMES[this.check.result];
+ return ICON_NAMES[this.check.status.toLowerCase()];
+ },
+ failureReason() {
+ return FAILURE_REASONS[this.check.identifier.toLowerCase()];
},
},
};
@@ -36,9 +54,10 @@ export default {
<div class="gl-display-flex">
<status-icon :icon-name="iconName" :level="2" />
<div class="gl-w-full gl-min-w-0">
- <div class="gl-display-flex">{{ check.failureReason }}</div>
+ <div class="gl-display-flex">{{ failureReason }}</div>
</div>
<slot></slot>
+ <slot v-if="check.status === 'FAILED'" name="failed"></slot>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/checks/rebase.stories.js b/app/assets/javascripts/vue_merge_request_widget/components/checks/rebase.stories.js
new file mode 100644
index 00000000000..c0ac1818ffa
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/checks/rebase.stories.js
@@ -0,0 +1,85 @@
+import createMockApollo from 'helpers/mock_apollo_helper';
+import rebaseStateQuery from '../../queries/states/rebase.query.graphql';
+import Rebase from './rebase.vue';
+
+const service = {
+ rebase: () => new Promise(() => {}),
+};
+
+const defaultRender = ({ apolloProvider, check, mr, canCreatePipelineInTargetProject }) => ({
+ components: { Rebase },
+ apolloProvider,
+ provide: {
+ canCreatePipelineInTargetProject,
+ },
+ data() {
+ return { service, mr: { ...mr, targetProjectFullPath: 'gitlab-org/gitlab' }, check };
+ },
+ template: '<rebase :mr="mr" :service="service" :check="check" />',
+});
+
+const Template = ({
+ failed,
+ pushToSourceBranch,
+ rebaseInProgress,
+ onlyAllowMergeIfPipelineSucceeds,
+ canCreatePipelineInTargetProject,
+}) => {
+ const requestHandlers = [
+ [
+ rebaseStateQuery,
+ () =>
+ Promise.resolve({
+ data: {
+ project: {
+ id: '1',
+ mergeRequest: {
+ id: '2',
+ rebaseInProgress,
+ targetBranch: 'main',
+ userPermissions: {
+ pushToSourceBranch,
+ },
+ pipelines: {
+ nodes: [
+ {
+ id: '1',
+ project: {
+ id: '2',
+ fullPath: 'gitlab/gitlab',
+ },
+ },
+ ],
+ },
+ },
+ },
+ },
+ }),
+ ],
+ ];
+ const apolloProvider = createMockApollo(requestHandlers);
+
+ return defaultRender({
+ apolloProvider,
+ check: {
+ identifier: 'need_rebase',
+ status: failed ? 'failed' : 'passed',
+ },
+ mr: { onlyAllowMergeIfPipelineSucceeds },
+ canCreatePipelineInTargetProject,
+ });
+};
+
+export const Default = Template.bind({});
+Default.args = {
+ failed: true,
+ pushToSourceBranch: true,
+ rebaseInProgress: false,
+ onlyAllowMergeIfPipelineSucceeds: false,
+ canCreatePipelineInTargetProject: false,
+};
+
+export default {
+ title: 'vue_merge_request_widget/merge_checks/rebase',
+ component: Rebase,
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/checks/rebase.vue b/app/assets/javascripts/vue_merge_request_widget/components/checks/rebase.vue
new file mode 100644
index 00000000000..72140c22a89
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/checks/rebase.vue
@@ -0,0 +1,220 @@
+<script>
+import { GlModal, GlLink } from '@gitlab/ui';
+import { s__, __ } from '~/locale';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import { createAlert } from '~/alert';
+import toast from '~/vue_shared/plugins/global_toast';
+import simplePoll from '~/lib/utils/simple_poll';
+import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
+import rebaseQuery from '../../queries/states/rebase.query.graphql';
+import eventHub from '../../event_hub';
+import ActionButtons from '../action_buttons.vue';
+import MergeChecksMessage from './message.vue';
+
+export default {
+ name: 'MergeChecksRebase',
+ components: {
+ GlModal,
+ GlLink,
+ MergeChecksMessage,
+ ActionButtons,
+ },
+ mixins: [mergeRequestQueryVariablesMixin],
+ apollo: {
+ state: {
+ query: rebaseQuery,
+ variables() {
+ return this.mergeRequestQueryVariables;
+ },
+ update: (data) => data.project.mergeRequest,
+ },
+ },
+ inject: {
+ canCreatePipelineInTargetProject: {
+ default: false,
+ },
+ },
+ props: {
+ check: {
+ type: Object,
+ required: true,
+ },
+ mr: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ service: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ },
+ data() {
+ return {
+ state: {},
+ isMakingRequest: false,
+ };
+ },
+ computed: {
+ isLoading() {
+ return this.$apollo.queries.state.loading;
+ },
+ rebaseInProgress() {
+ return this.state.rebaseInProgress;
+ },
+ showRebaseWithoutPipeline() {
+ return (
+ !this.mr.onlyAllowMergeIfPipelineSucceeds ||
+ (this.mr.onlyAllowMergeIfPipelineSucceeds && this.mr.allowMergeOnSkippedPipeline)
+ );
+ },
+ isForkMergeRequest() {
+ return this.mr.sourceProjectFullPath !== this.mr.targetProjectFullPath;
+ },
+ isLatestPipelineCreatedInTargetProject() {
+ const latestPipeline = this.state.pipelines.nodes[0];
+
+ return latestPipeline?.project?.fullPath === this.mr.targetProjectFullPath;
+ },
+ shouldShowSecurityWarning() {
+ return (
+ this.canCreatePipelineInTargetProject &&
+ this.isForkMergeRequest &&
+ !this.isLatestPipelineCreatedInTargetProject
+ );
+ },
+ tertiaryActionsButtons() {
+ if (this.check.result === 'success') return [];
+
+ return [
+ {
+ text: s__('mrWidget|Rebase'),
+ loading: this.isMakingRequest || this.rebaseInProgress,
+ testId: 'standard-rebase-button',
+ onClick: () => this.tryRebase(),
+ },
+ this.showRebaseWithoutPipeline && {
+ text: s__('mrWidget|Rebase without pipeline'),
+ loading: this.isMakingRequest || this.rebaseInProgress,
+ testId: 'rebase-without-ci-button',
+ onClick: () => this.rebaseWithoutCi(),
+ },
+ ].filter((b) => b);
+ },
+ },
+ methods: {
+ rebase({ skipCi = false } = {}) {
+ this.isMakingRequest = true;
+
+ this.service
+ .rebase({ skipCi })
+ .then(() => simplePoll(this.checkRebaseStatus))
+ .catch((error) => {
+ this.isMakingRequest = false;
+
+ if (!error.response?.data?.merge_error) {
+ createAlert({
+ message: __('Something went wrong. Please try again.'),
+ });
+ }
+ });
+ },
+ rebaseWithoutCi() {
+ return this.rebase({ skipCi: true });
+ },
+ tryRebase() {
+ if (this.shouldShowSecurityWarning) {
+ this.$refs.modal.show();
+ } else {
+ this.rebase();
+ }
+ },
+ checkRebaseStatus(continuePolling, stopPolling) {
+ this.service
+ .poll()
+ .then((res) => res.data)
+ .then((res) => {
+ if (res.rebase_in_progress || res.should_be_rebased) {
+ continuePolling();
+ } else {
+ this.isMakingRequest = false;
+
+ if (!res.merge_error?.length) {
+ toast(__('Rebase completed'));
+ }
+
+ eventHub.$emit('MRWidgetRebaseSuccess');
+ stopPolling();
+ }
+ })
+ .catch(() => {
+ this.isMakingRequest = false;
+ createAlert({
+ message: __('Something went wrong. Please try again.'),
+ });
+ stopPolling();
+ });
+ },
+ },
+ modal: {
+ id: 'rebase-security-risk-modal',
+ title: s__('mrWidget|Are you sure you want to rebase?'),
+ actionPrimary: {
+ text: s__('mrWidget|Rebase'),
+ attributes: {
+ variant: 'danger',
+ },
+ },
+ actionCancel: {
+ text: __('Cancel'),
+ attributes: {
+ variant: 'default',
+ },
+ },
+ },
+ runPipelinesInTheParentProjectHelpPath: helpPagePath(
+ '/ci/pipelines/merge_request_pipelines.html',
+ {
+ anchor: 'run-pipelines-in-the-parent-project',
+ },
+ ),
+};
+</script>
+
+<template>
+ <merge-checks-message :check="check">
+ <template #failed>
+ <action-buttons v-if="!isLoading" :tertiary-buttons="tertiaryActionsButtons" />
+ </template>
+ <gl-modal
+ ref="modal"
+ :modal-id="$options.modal.id"
+ :title="$options.modal.title"
+ :action-primary="$options.modal.actionPrimary"
+ :action-cancel="$options.modal.actionCancel"
+ @primary="rebase"
+ >
+ <p>
+ {{
+ s__(
+ 'Pipelines|Rebasing creates a pipeline that runs code originating from a forked project merge request. Consequently there are potential security implications, such as the exposure of CI variables.',
+ )
+ }}
+ </p>
+ <p>
+ {{
+ s__(
+ "Pipelines|You should review the code thoroughly before running this pipeline with the parent project's CI/CD resources.",
+ )
+ }}
+ </p>
+ <p>
+ {{ s__('Pipelines|If you are unsure, ask a project maintainer to review it for you.') }}
+ </p>
+ <gl-link :href="$options.runPipelinesInTheParentProjectHelpPath" target="_blank">
+ {{ s__('Pipelines|More Information') }}
+ </gl-link>
+ </gl-modal>
+ </merge-checks-message>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/checks/unresolved_discussions.vue b/app/assets/javascripts/vue_merge_request_widget/components/checks/unresolved_discussions.vue
new file mode 100644
index 00000000000..a6970d9c795
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/checks/unresolved_discussions.vue
@@ -0,0 +1,39 @@
+<script>
+import { s__ } from '~/locale';
+import notesEventHub from '~/notes/event_hub';
+import ActionButtons from '../action_buttons.vue';
+import MergeChecksMessage from './message.vue';
+
+export default {
+ name: 'MergeChecksUnresolvedDiscussions',
+ components: {
+ MergeChecksMessage,
+ ActionButtons,
+ },
+ props: {
+ check: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ tertiaryActionsButtons() {
+ return [
+ {
+ text: s__('mrWidget|Go to first unresolved thread'),
+ category: 'default',
+ onClick: () => notesEventHub.$emit('jumpToFirstUnresolvedDiscussion'),
+ },
+ ];
+ },
+ },
+};
+</script>
+
+<template>
+ <merge-checks-message :check="check">
+ <template #failed>
+ <action-buttons :tertiary-buttons="tertiaryActionsButtons" />
+ </template>
+ </merge-checks-message>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue
index 3e2f3ab4103..0f692f23142 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue
@@ -1,7 +1,7 @@
<!-- eslint-disable vue/multi-word-component-names -->
<script>
import { GlButton, GlLoadingIcon, GlTooltipDirective, GlIntersectionObserver } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import SafeHtml from '~/vue_shared/directives/safe_html';
import { DynamicScroller, DynamicScrollerItem } from 'vendor/vue-virtual-scroller';
import { sprintf, s__, __ } from '~/locale';
@@ -102,7 +102,7 @@ export default {
return this.statusIcon(this.collapsedData);
},
tertiaryActionsButtons() {
- return this.tertiaryButtons ? this.tertiaryButtons() : undefined;
+ return 'tertiaryButtons' in this ? this.tertiaryButtons() : undefined;
},
hydratedSummary() {
const structuredOutput = this.summary(this.collapsedData);
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.stories.js b/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.stories.js
index 1c57226f887..77dc5b1d0da 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.stories.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.stories.js
@@ -15,9 +15,9 @@ const defaultRender = (apolloProvider) => ({
components: { MergeChecks },
apolloProvider,
data() {
- return { mr: { conflictResolutionPath: 'https://gitlab.com' } };
+ return { service: {}, mr: { conflictResolutionPath: 'https://gitlab.com' } };
},
- template: '<merge-checks :mr="mr" />',
+ template: '<merge-checks :mr="mr" :service="service" />',
});
const Template = ({ canMerge, failed, pushToSourceBranch }) => {
@@ -32,16 +32,14 @@ const Template = ({ canMerge, failed, pushToSourceBranch }) => {
mergeRequest: {
id: 1,
userPermissions: { canMerge },
- mergeChecks: [
+ mergeabilityChecks: [
{
- failureReason: 'Unresolved discussions',
- identifier: 'unresolved_discussions',
- result: failed ? 'failed' : 'passed',
+ identifier: 'DISCUSSIONS_NOT_RESOLVED',
+ status: failed ? 'FAILED' : 'SUCCESS',
},
{
- failureReason: 'Resolve conflicts',
- identifier: 'conflicts',
- result: failed ? 'failed' : 'passed',
+ identifier: 'CONFLICT',
+ status: failed ? 'FAILED' : 'SUCCESS',
},
],
},
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.vue b/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.vue
index fa84c0a4a6f..ac403c2c6f2 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.vue
@@ -1,16 +1,12 @@
<script>
import { GlSkeletonLoader } from '@gitlab/ui';
-import { n__, __, sprintf } from '~/locale';
+import { __, n__, sprintf } from '~/locale';
+import { COMPONENTS } from '~/vue_merge_request_widget/components/checks/constants';
import mergeRequestQueryVariablesMixin from '../mixins/merge_request_query_variables';
import mergeChecksQuery from '../queries/merge_checks.query.graphql';
import StateContainer from './state_container.vue';
import BoldText from './bold_text.vue';
-const COMPONENTS = {
- conflicts: () => import('./checks/conflicts.vue'),
- default: () => import('./checks/message.vue'),
-};
-
export default {
apollo: {
state: {
@@ -35,6 +31,10 @@ export default {
type: Object,
required: true,
},
+ service: {
+ type: Object,
+ required: true,
+ },
},
data() {
return {
@@ -68,10 +68,10 @@ export default {
);
},
checks() {
- return this.state.mergeChecks || [];
+ return this.state.mergeabilityChecks || [];
},
failedChecks() {
- return this.checks.filter((c) => c.result === 'failed');
+ return this.checks.filter((c) => c.status.toLowerCase() === 'failed');
},
},
methods: {
@@ -79,7 +79,7 @@ export default {
this.collapsed = !this.collapsed;
},
checkComponent(check) {
- return COMPONENTS[check.identifier] || COMPONENTS.default;
+ return COMPONENTS[check.identifier.toLowerCase()] || COMPONENTS.default;
},
},
};
@@ -122,6 +122,7 @@ export default {
}"
:check="check"
:mr="mr"
+ :service="service"
/>
</div>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
index 2e104f2b93b..efc74241941 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
@@ -10,7 +10,7 @@ import {
} from '@gitlab/ui';
import SafeHtml from '~/vue_shared/directives/safe_html';
import { s__, n__ } from '~/locale';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { keepLatestDownstreamPipelines } from '~/ci/pipeline_details/utils/parsing_utils';
import PipelineArtifacts from '~/ci/pipelines_page/components/pipelines_artifacts.vue';
import LegacyPipelineMiniGraph from '~/ci/pipeline_mini_graph/legacy_pipeline_mini_graph.vue';
@@ -21,7 +21,7 @@ import { MT_MERGE_STRATEGY } from '../constants';
export default {
name: 'MRWidgetPipeline',
components: {
- CiBadgeLink,
+ CiIcon,
GlLink,
GlLoadingIcon,
GlIcon,
@@ -194,13 +194,7 @@ export default {
</p>
</template>
<template v-else-if="hasPipeline">
- <ci-badge-link
- :status="status"
- :href="status.details_path"
- size="md"
- :show-text="false"
- class="gl-align-self-start gl-mt-2 gl-mr-3"
- />
+ <ci-icon :status="status" class="gl-align-self-start gl-mt-2 gl-mr-3" />
<div class="ci-widget-container d-flex">
<div class="ci-widget-content">
<div class="media-body">
@@ -208,7 +202,9 @@ export default {
data-testid="pipeline-info-container"
class="gl-display-flex gl-flex-wrap gl-align-items-center gl-justify-content-space-between"
>
- <p class="mr-pipeline-title gl-m-0! gl-mr-3! gl-font-weight-bold gl-text-gray-900">
+ <p
+ class="mr-pipeline-title gl-align-self-start gl-m-0! gl-mr-3! gl-font-weight-bold gl-text-gray-900"
+ >
{{ pipeline.details.event_type_name }}
<gl-link :href="pipeline.path" class="pipeline-id" data-testid="pipeline-id"
>#{{ pipeline.id }}</gl-link
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue
index ea3f324b8f2..370e07b397c 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue
@@ -28,7 +28,7 @@ export default {
};
</script>
<template>
- <div class="gl-w-6 gl-h-6 gl-display-flex gl-align-self-center gl-mr-3">
+ <div class="gl-w-6 gl-h-6 gl-display-flex gl-align-self-start gl-mr-3">
<div class="gl-display-flex gl-m-auto">
<gl-icon v-if="isMerged" name="merge" :size="16" class="gl-text-blue-500" />
<gl-icon v-else-if="isClosed" name="merge-request-close" :size="16" class="gl-text-red-500" />
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue
index 45958d7fb8d..c70213ad8a2 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue
@@ -99,11 +99,7 @@ export default {
<p class="gl-mt-2">
<gl-sprintf :message="$options.SP_HELP_CONTENT">
<template #link="{ content }">
- <gl-link
- data-testid="help"
- :href="$options.SP_HELP_URL"
- target="_blank"
- class="font-size-inherit"
+ <gl-link data-testid="help" :href="$options.SP_HELP_URL" target="_blank"
>{{ content }}
</gl-link>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
index ac434c5be4e..3c2d8efaffc 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
@@ -1,10 +1,9 @@
<script>
import {
- GlIcon,
GlButton,
GlButtonGroup,
- GlDropdown,
- GlDropdownItem,
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
GlFormCheckbox,
GlSprintf,
GlLink,
@@ -15,6 +14,7 @@ import { isEmpty, isNil } from 'lodash';
import readyToMergeMixin from 'ee_else_ce/vue_merge_request_widget/mixins/ready_to_merge';
import readyToMergeQuery from 'ee_else_ce/vue_merge_request_widget/queries/states/ready_to_merge.query.graphql';
import { createAlert } from '~/alert';
+import { fetchPolicies } from '~/lib/graphql';
import { TYPENAME_MERGE_REQUEST } from '~/graphql_shared/constants';
import { STATUS_CLOSED, STATUS_MERGED } from '~/issues/constants';
import { secondsToMilliseconds } from '~/lib/utils/datetime_utility';
@@ -25,11 +25,14 @@ import { helpPagePath } from '~/helpers/help_page_helper';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import readyToMergeSubscription from '~/vue_merge_request_widget/queries/states/ready_to_merge.subscription.graphql';
import HelpPopover from '~/vue_shared/components/help_popover.vue';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import {
AUTO_MERGE_STRATEGIES,
MT_MERGE_STRATEGY,
PIPELINE_FAILED_STATE,
STATE_MACHINE,
+ MT_SKIP_TRAIN,
+ MT_RESTART_TRAIN,
} from '../../constants';
import eventHub from '../../event_hub';
import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
@@ -61,6 +64,10 @@ export default {
},
manual: true,
result({ data }) {
+ if (!data.project) {
+ return;
+ }
+
if (Object.keys(this.state).length === 0) {
this.removeSourceBranch =
data.project.mergeRequest.shouldRemoveSourceBranch ||
@@ -121,13 +128,12 @@ export default {
SquashBeforeMerge,
CommitEdit,
CommitMessageDropdown,
- GlIcon,
GlSprintf,
GlLink,
GlButton,
GlButtonGroup,
- GlDropdown,
- GlDropdownItem,
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
GlFormCheckbox,
GlSkeletonLoader,
MergeFailedPipelineConfirmationDialog,
@@ -139,6 +145,10 @@ export default {
import(
'ee_component/vue_merge_request_widget/components/merge_train_failed_pipeline_confirmation_dialog.vue'
),
+ MergeTrainRestartTrainConfirmationDialog: () =>
+ import(
+ 'ee_component/vue_merge_request_widget/components/merge_train_restart_train_confirmation_dialog.vue'
+ ),
AddedCommitMessage,
RelatedLinks,
HelpPopover,
@@ -148,7 +158,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
- mixins: [readyToMergeMixin, mergeRequestQueryVariablesMixin],
+ mixins: [readyToMergeMixin, mergeRequestQueryVariablesMixin, glFeatureFlagsMixin()],
props: {
mr: { type: Object, required: true },
service: { type: Object, required: true },
@@ -168,6 +178,10 @@ export default {
squashCommitMessageIsTouched: false,
isPipelineFailedModalVisibleMergeTrain: false,
isPipelineFailedModalVisibleNormalMerge: false,
+ isMergeTrainBeingForceMerged: false,
+ mergeTrainMergeType: MT_RESTART_TRAIN,
+ skipMergeTrain: false,
+ mergeTrainsSkipAllowed: this.mr.mergeTrainsSkipAllowed,
editCommitMessage: false,
};
},
@@ -319,6 +333,12 @@ export default {
title: this.autoMergePopoverSettings.title,
};
},
+ isSkipMergeTrainAvailable() {
+ return this.mergeTrainsSkipAllowed && this.glFeatures.mergeTrainsSkipTrain;
+ },
+ displaySkipMergeTrainOptions() {
+ return this.shouldDisplayMergeImmediatelyDropdownOptions && this.isSkipMergeTrainAvailable;
+ },
},
watch: {
'mr.state': function mrStateWatcher() {
@@ -329,6 +349,12 @@ export default {
eventHub.$on('ApprovalUpdated', this.updateGraphqlState);
eventHub.$on('MRWidgetUpdateRequested', this.updateGraphqlState);
eventHub.$on('mr.discussion.updated', this.updateGraphqlState);
+
+ if (this.glFeatures.widgetPipelinePassSubscriptionUpdate) {
+ this.$apollo.queries.state.setOptions({
+ fetchPolicy: fetchPolicies.NO_CACHE,
+ });
+ }
},
beforeDestroy() {
eventHub.$off('ApprovalUpdated', this.updateGraphqlState);
@@ -377,6 +403,7 @@ export default {
auto_merge_strategy: useAutoMerge ? this.preferredAutoMergeStrategy : undefined,
should_remove_source_branch: this.removeSourceBranch === true,
squash: this.squashBeforeMerge,
+ skip_merge_train: this.skipMergeTrain,
};
// If users can't alter the squash message (e.g. for 1-commit merge requests),
@@ -428,6 +455,17 @@ export default {
this.handleMergeButtonClick(false, true);
}
},
+ handleMergeTrainMergeImmediatelyButtonClick(type) {
+ this.mergeTrainMergeType = type;
+ this.isMergeTrainBeingForceMerged = true;
+ },
+ processMergeTrain() {
+ if (this.mergeTrainMergeType === MT_SKIP_TRAIN) {
+ this.skipMergeTrain = true;
+ }
+
+ this.handleMergeButtonClick(false, true, true);
+ },
onMergeImmediatelyConfirmation() {
this.handleMergeButtonClick(false, true, true);
},
@@ -491,6 +529,8 @@ export default {
sourceDivergedFromTargetText: s__('mrWidget|The source branch is %{link} the target branch.'),
divergedCommits: (count) => n__('%d commit behind', '%d commits behind', count),
},
+ MT_SKIP_TRAIN,
+ MT_RESTART_TRAIN,
};
</script>
@@ -520,7 +560,7 @@ export default {
<div class="mr-widget-body-controls gl-display-flex gl-align-items-center gl-flex-wrap">
<template v-if="shouldShowMergeControls">
<div
- class="gl-display-flex gl-sm-flex-direction-column gl-md-align-items-center gl-flex-wrap gl-w-full"
+ class="gl-display-flex gl-flex-direction-column gl-md-flex-direction-row gl-md-align-items-center gl-flex-wrap gl-w-full"
>
<gl-form-checkbox
v-if="canRemoveSourceBranch"
@@ -637,32 +677,57 @@ export default {
@click="handleMergeButtonClick(isAutoMergeAvailable)"
>{{ mergeButtonText }}</gl-button
>
- <gl-dropdown
+ <gl-disclosure-dropdown
v-if="shouldShowMergeImmediatelyDropdown"
v-gl-tooltip.hover.focus="__('Select merge moment')"
:disabled="isMergeButtonDisabled"
variant="confirm"
+ class="gl-mr-0"
data-testid="merge-immediately-dropdown"
+ icon="chevron-down"
toggle-class="btn-icon js-merge-moment"
+ :toggle-text="__('Select a merge moment')"
+ text-sr-only
+ no-caret
>
- <template #button-content>
- <gl-icon name="chevron-down" class="mr-0" />
- <span class="sr-only">{{ __('Select merge moment') }}</span>
- </template>
- <gl-dropdown-item
- icon-name="warning"
- button-class="accept-merge-request"
+ <gl-disclosure-dropdown-item
+ v-if="
+ !shouldDisplayMergeImmediatelyDropdownOptions || !isSkipMergeTrainAvailable
+ "
data-testid="merge-immediately-button"
- @click="handleMergeImmediatelyButtonClick"
+ @action="handleMergeImmediatelyButtonClick"
+ >
+ <template #list-item> {{ __('Merge immediately') }} </template>
+ </gl-disclosure-dropdown-item>
+ <gl-disclosure-dropdown-item
+ v-if="displaySkipMergeTrainOptions"
+ data-testid="mt-merge-now-restart-button"
+ @action="handleMergeTrainMergeImmediatelyButtonClick($options.MT_RESTART_TRAIN)"
>
- {{ __('Merge immediately') }}
- </gl-dropdown-item>
- </gl-dropdown>
+ <template #list-item>
+ <strong>{{ __(`Merge now and restart train`) }}</strong>
+ <p class="gl-text-gray-400 gl-font-sm gl-mb-0">
+ {{ __('Restart merge train pipelines with the merged changes.') }}
+ </p>
+ </template>
+ </gl-disclosure-dropdown-item>
+ <gl-disclosure-dropdown-item
+ v-if="displaySkipMergeTrainOptions"
+ data-testid="mt-merge-now-skip-restart-button"
+ @action="handleMergeTrainMergeImmediatelyButtonClick($options.MT_SKIP_TRAIN)"
+ >
+ <template #list-item>
+ <strong>{{ __(`Merge now and don't restart train`) }}</strong>
+ <p class="gl-text-gray-400 gl-font-sm gl-mb-0">
+ {{ __('Merge train pipelines continue without the merged changes.') }}
+ </p>
+ </template>
+ </gl-disclosure-dropdown-item>
+ </gl-disclosure-dropdown>
</gl-button-group>
<template v-if="showAutoMergeHelperText">
<div
class="gl-ml-4 gl-text-gray-500 gl-font-sm"
- data-qa-selector="auto_merge_helper_text"
data-testid="auto-merge-helper-text"
>
{{ autoMergeHelperText }}
@@ -730,12 +795,16 @@ export default {
class="mr-ready-merge-related-links gl-display-inline"
/>
</li>
+ <li v-if="state.autoMergeEnabled" class="gl-line-height-normal">
+ {{ s__('mrWidget|Auto-merge enabled') }}
+ </li>
</ul>
</div>
</div>
</div>
</div>
<merge-immediately-confirmation-dialog
+ v-if="mr.mergeImmediatelyDocsPath"
ref="confirmationDialog"
:docs-url="mr.mergeImmediatelyDocsPath"
@mergeImmediately="onMergeImmediatelyConfirmation"
@@ -745,6 +814,13 @@ export default {
@startMergeTrain="onStartMergeTrainConfirmation"
@cancel="isPipelineFailedModalVisibleMergeTrain = false"
/>
+ <merge-train-restart-train-confirmation-dialog
+ v-if="isSkipMergeTrainAvailable"
+ :visible="isMergeTrainBeingForceMerged"
+ :merge-train-type="mergeTrainMergeType"
+ @processMergeTrainMerge="processMergeTrain"
+ @cancel="isMergeTrainBeingForceMerged = false"
+ />
<merge-failed-pipeline-confirmation-dialog
:visible="isPipelineFailedModalVisibleNormalMerge"
@mergeWithFailedPipeline="onMergeWithFailedPipelineConfirmation"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue
index 7fc4a06cbae..267facb0a50 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue
@@ -143,7 +143,9 @@ export default {
:collapsed="mr.mergeDetailsCollapsed"
@toggle="() => mr.toggleMergeDetails()"
>
- <span class="gl-ml-0! gl-text-body! gl-flex-grow-1">
+ <span
+ class="gl-display-inline-flex gl-align-self-start gl-pt-2 gl-ml-0! gl-text-body! gl-flex-grow-1"
+ >
<bold-text :message="$options.i18n.removeDraftStatus" />
</span>
<template #actions>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/app.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/app.vue
index 8249dffcc27..08e803bffc9 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/widget/app.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/app.vue
@@ -9,6 +9,8 @@ export default {
MrTerraformWidget: () => import('~/vue_merge_request_widget/extensions/terraform/index.vue'),
MrCodeQualityWidget: () =>
import('~/vue_merge_request_widget/extensions/code_quality/index.vue'),
+ MrAccessibilityWidget: () =>
+ import('~/vue_merge_request_widget/extensions/accessibility/index.vue'),
},
props: {
@@ -31,12 +33,17 @@ export default {
return this.mr.codequalityReportsPath ? 'MrCodeQualityWidget' : undefined;
},
+ accessibilityWidget() {
+ return this.mr.accessibilityReportPath ? 'MrAccessibilityWidget' : undefined;
+ },
+
widgets() {
return [
this.codeQualityWidget,
this.testReportWidget,
this.terraformPlansWidget,
'MrSecurityWidget',
+ this.accessibilityWidget,
].filter((w) => w);
},
},
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue
index 72c041759d9..d4375690ad1 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue
@@ -1,5 +1,5 @@
<script>
-import { GlBadge, GlLink } from '@gitlab/ui';
+import { GlBadge, GlLink, GlTooltipDirective } from '@gitlab/ui';
import SafeHtml from '~/vue_shared/directives/safe_html';
import { generateText } from '../extensions/utils';
import ContentRow from './widget_content_row.vue';
@@ -15,6 +15,7 @@ export default {
},
directives: {
SafeHtml,
+ GlTooltip: GlTooltipDirective,
},
props: {
data: {
@@ -78,7 +79,11 @@ export default {
<div class="gl-display-flex gl-flex-grow-1">
<div class="gl-display-flex gl-flex-grow-1 gl-align-items-baseline">
<div>
- <p v-safe-html="generatedText" class="gl-mb-0 gl-mr-1"></p>
+ <p
+ v-gl-tooltip="{ title: data.tooltipText, boundary: 'viewport' }"
+ v-safe-html="generatedText"
+ class="gl-mb-0 gl-mr-1"
+ ></p>
<gl-link v-if="data.link" :href="data.link.href">{{ data.link.text }}</gl-link>
<p
v-if="data.supportingText"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue
index d17be3e4037..0eb50b9ff4f 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue
@@ -1,7 +1,7 @@
<!-- eslint-disable vue/multi-word-component-names -->
<script>
import { GlButton, GlLink, GlTooltipDirective, GlLoadingIcon } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { normalizeHeaders } from '~/lib/utils/common_utils';
import { logError } from '~/lib/logger';
import SafeHtml from '~/vue_shared/directives/safe_html';
diff --git a/app/assets/javascripts/vue_merge_request_widget/constants.js b/app/assets/javascripts/vue_merge_request_widget/constants.js
index 1a469f9b7bb..071f95a28fa 100644
--- a/app/assets/javascripts/vue_merge_request_widget/constants.js
+++ b/app/assets/javascripts/vue_merge_request_widget/constants.js
@@ -6,7 +6,7 @@ import { stateToComponentMap as classStateMap, stateKey } from './stores/state_m
export const FOUR_MINUTES_IN_MS = 1000 * 60 * 4;
export const STATE_QUERY_POLLING_INTERVAL_DEFAULT = 5000;
-export const STATE_QUERY_POLLING_INTERVAL_BACKOFF = 2;
+export const STATE_QUERY_POLLING_INTERVAL_BACKOFF = 1.2;
export const SUCCESS = 'success';
export const WARNING = 'warning';
@@ -202,3 +202,6 @@ export const DETAILED_MERGE_STATUS = {
CI_STILL_RUNNING: 'CI_STILL_RUNNING',
EXTERNAL_STATUS_CHECKS: 'EXTERNAL_STATUS_CHECKS',
};
+
+export const MT_SKIP_TRAIN = 'skip';
+export const MT_RESTART_TRAIN = 'restart';
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/accessibility/index.js b/app/assets/javascripts/vue_merge_request_widget/extensions/accessibility/index.js
deleted file mode 100644
index 0fb5e13ad82..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/extensions/accessibility/index.js
+++ /dev/null
@@ -1,123 +0,0 @@
-import { uniqueId } from 'lodash';
-import { __, n__, s__, sprintf } from '~/locale';
-import axios from '~/lib/utils/axios_utils';
-import { EXTENSION_ICONS } from '../../constants';
-
-export default {
- name: 'WidgetAccessibility',
- enablePolling: true,
- i18n: {
- loading: s__('Reports|Accessibility scanning results are being parsed'),
- error: s__('Reports|Accessibility scanning failed loading results'),
- },
- props: ['accessibilityReportPath'],
- computed: {
- statusIcon() {
- return this.collapsedData.status === 'failed'
- ? EXTENSION_ICONS.warning
- : EXTENSION_ICONS.success;
- },
- },
- methods: {
- summary() {
- const numOfResults = this.collapsedData?.summary?.errored || 0;
-
- const successText = s__(
- 'Reports|Accessibility scanning detected no issues for the source branch only',
- );
- const warningText = sprintf(
- n__(
- 'Reports|Accessibility scanning detected %{strong_start}%{number}%{strong_end} issue for the source branch only',
- 'Reports|Accessibility scanning detected %{strong_start}%{number}%{strong_end} issues for the source branch only',
- numOfResults,
- ),
- {
- number: numOfResults,
- },
- false,
- );
-
- return numOfResults === 0 ? successText : warningText;
- },
- shouldCollapse() {
- return this.collapsedData?.summary?.errored > 0;
- },
- fetchCollapsedData() {
- return axios.get(this.accessibilityReportPath);
- },
- fetchFullData() {
- return Promise.resolve(this.prepareReports());
- },
- parsedTECHSCode(code) {
- /*
- * In issue code looks like "WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail"
- * or "WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent"
- *
- * The TECHS code is the "G18", "G168", "H91", etc. from the code which is used for the documentation.
- * Here we simply split the string on `.` and get the code in the 5th position
- */
- return code?.split('.')[4];
- },
- formatLearnMoreUrl(code) {
- const parsed = this.parsedTECHSCode(code);
- // eslint-disable-next-line @gitlab/require-i18n-strings
- return `https://www.w3.org/TR/WCAG20-TECHS/${parsed || 'Overview'}.html`;
- },
- formatText(code) {
- return sprintf(
- s__(
- 'AccessibilityReport|The accessibility scanning found an error of the following type: %{code}',
- ),
- { code },
- );
- },
- formatMessage(message) {
- return sprintf(s__('AccessibilityReport|Message: %{message}'), { message });
- },
- prepareReports() {
- const { collapsedData } = this;
-
- const newErrors = collapsedData.new_errors.map((error) => {
- return {
- header: __('New'),
- id: uniqueId('new-error-'),
- text: this.formatText(error.code),
- icon: { name: EXTENSION_ICONS.failed },
- link: {
- href: this.formatLearnMoreUrl(error.code),
- text: __('Learn more'),
- },
- supportingText: this.formatMessage(error.message),
- };
- });
-
- const existingErrors = collapsedData.existing_errors.map((error) => {
- return {
- id: uniqueId('existing-error-'),
- text: this.formatText(error.code),
- icon: { name: EXTENSION_ICONS.failed },
- link: {
- href: this.formatLearnMoreUrl(error.code),
- text: __('Learn more'),
- },
- supportingText: this.formatMessage(error.message),
- };
- });
-
- const resolvedErrors = collapsedData.resolved_errors.map((error) => {
- return {
- id: uniqueId('resolved-error-'),
- text: this.formatText(error.code),
- icon: { name: EXTENSION_ICONS.success },
- link: {
- href: this.formatLearnMoreUrl(error.code),
- text: __('Learn more'),
- },
- supportingText: this.formatMessage(error.message),
- };
- });
-
- return [...newErrors, ...existingErrors, ...resolvedErrors];
- },
- },
-};
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/accessibility/index.vue b/app/assets/javascripts/vue_merge_request_widget/extensions/accessibility/index.vue
new file mode 100644
index 00000000000..2ae16eef410
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/extensions/accessibility/index.vue
@@ -0,0 +1,154 @@
+<script>
+import { uniqueId } from 'lodash';
+import { __, n__, s__, sprintf } from '~/locale';
+import axios from '~/lib/utils/axios_utils';
+import MrWidget from '~/vue_merge_request_widget/components/widget/widget.vue';
+import { EXTENSION_ICONS } from '../../constants';
+
+export default {
+ name: 'WidgetAccessibility',
+ i18n: {
+ loading: s__('Reports|Accessibility scanning results are being parsed'),
+ error: s__('Reports|Accessibility scanning failed loading results'),
+ },
+ components: {
+ MrWidget,
+ },
+ props: {
+ mr: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ collapsedData: {},
+ content: [],
+ };
+ },
+ computed: {
+ statusIcon() {
+ return this.collapsedData?.status === 'failed'
+ ? EXTENSION_ICONS.warning
+ : EXTENSION_ICONS.success;
+ },
+ summary() {
+ const numOfResults = this.collapsedData?.summary?.errored || 0;
+
+ const successText = s__(
+ 'Reports|Accessibility scanning detected no issues for the source branch only',
+ );
+ const warningText = sprintf(
+ n__(
+ 'Reports|Accessibility scanning detected %{strong_start}%{number}%{strong_end} issue for the source branch only',
+ 'Reports|Accessibility scanning detected %{strong_start}%{number}%{strong_end} issues for the source branch only',
+ numOfResults,
+ ),
+ {
+ number: numOfResults,
+ },
+ false,
+ );
+
+ return numOfResults === 0 ? { title: successText } : { title: warningText };
+ },
+ shouldCollapse() {
+ return this.collapsedData?.summary?.errored > 0;
+ },
+ },
+ methods: {
+ fetchCollapsedData() {
+ return axios.get(this.mr.accessibilityReportPath).then((response) => {
+ this.collapsedData = response.data;
+ this.content = this.getContent(response.data);
+
+ return response;
+ });
+ },
+ fetchFullData() {
+ return Promise.resolve(this.prepareReports());
+ },
+ parsedTECHSCode(code) {
+ /*
+ * In issue code looks like "WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail"
+ * or "WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent"
+ *
+ * The TECHS code is the "G18", "G168", "H91", etc. from the code which is used for the documentation.
+ * Here we simply split the string on `.` and get the code in the 5th position
+ */
+ return code?.split('.')[4];
+ },
+ formatLearnMoreUrl(code) {
+ const parsed = this.parsedTECHSCode(code);
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ return `https://www.w3.org/TR/WCAG20-TECHS/${parsed || 'Overview'}.html`;
+ },
+ formatText(code) {
+ return sprintf(
+ s__(
+ 'AccessibilityReport|The accessibility scanning found an error of the following type: %{code}',
+ ),
+ { code },
+ );
+ },
+ formatMessage(message) {
+ return sprintf(s__('AccessibilityReport|Message: %{message}'), { message });
+ },
+ getContent(collapsedData) {
+ const newErrors = collapsedData.new_errors.map((error) => {
+ return {
+ header: __('New'),
+ id: uniqueId('new-error-'),
+ text: this.formatText(error.code),
+ icon: { name: EXTENSION_ICONS.failed },
+ link: {
+ href: this.formatLearnMoreUrl(error.code),
+ text: __('Learn more'),
+ },
+ supportingText: this.formatMessage(error.message),
+ };
+ });
+
+ const existingErrors = collapsedData.existing_errors.map((error) => {
+ return {
+ id: uniqueId('existing-error-'),
+ text: this.formatText(error.code),
+ icon: { name: EXTENSION_ICONS.failed },
+ link: {
+ href: this.formatLearnMoreUrl(error.code),
+ text: __('Learn more'),
+ },
+ supportingText: this.formatMessage(error.message),
+ };
+ });
+
+ const resolvedErrors = collapsedData.resolved_errors.map((error) => {
+ return {
+ id: uniqueId('resolved-error-'),
+ text: this.formatText(error.code),
+ icon: { name: EXTENSION_ICONS.success },
+ link: {
+ href: this.formatLearnMoreUrl(error.code),
+ text: __('Learn more'),
+ },
+ supportingText: this.formatMessage(error.message),
+ };
+ });
+
+ return [...newErrors, ...existingErrors, ...resolvedErrors];
+ },
+ },
+};
+</script>
+<template>
+ <mr-widget
+ :error-text="$options.i18n.error"
+ :status-icon-name="statusIcon"
+ :loading-text="$options.i18n.loading"
+ :widget-name="$options.name"
+ :summary="summary"
+ :content="content"
+ :is-collapsible="shouldCollapse"
+ :fetch-collapsed-data="fetchCollapsedData"
+ />
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.vue b/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.vue
index cd3a98effa3..e87b5d20ca0 100644
--- a/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.vue
@@ -1,5 +1,5 @@
<script>
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { SEVERITY_ICONS_MR_WIDGET } from '~/ci/reports/codequality_report/constants';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports.vue b/app/assets/javascripts/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports.vue
index e7d8de97f20..a36a58c68de 100644
--- a/app/assets/javascripts/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports.vue
@@ -1,5 +1,5 @@
<script>
-import { GlDropdown, GlDropdownItem, GlTooltipDirective as GlTooltip } from '@gitlab/ui';
+import { GlDisclosureDropdown } from '@gitlab/ui';
import MrWidget from '~/vue_merge_request_widget/components/widget/widget.vue';
import { helpPagePath } from '~/helpers/help_page_helper';
import { s__, sprintf } from '~/locale';
@@ -10,11 +10,7 @@ export default {
name: 'WidgetSecurityReportsCE',
components: {
MrWidget,
- GlDropdown,
- GlDropdownItem,
- },
- directives: {
- GlTooltip,
+ GlDisclosureDropdown,
},
i18n: {
apiError: s__(
@@ -76,17 +72,23 @@ export default {
summary() {
return { title: this.$options.i18n.scansHaveRun };
},
+ listboxOptions() {
+ return this.artifacts.map(({ name, path }) => ({
+ text: sprintf(s__('SecurityReports|Download %{artifactName}'), {
+ artifactName: name,
+ }),
+ href: path,
+ extraAttrs: {
+ download: '',
+ rel: 'nofollow',
+ },
+ }));
+ },
},
methods: {
handleIsLoading(value) {
this.isLoading = value;
},
-
- artifactText({ name }) {
- return sprintf(s__('SecurityReports|Download %{artifactName}'), {
- artifactName: name,
- });
- },
},
widgetHelpPopover: {
options: { title: s__('ciReport|Security scan results') },
@@ -116,26 +118,12 @@ export default {
@is-loading="handleIsLoading"
>
<template #action-buttons>
- <div class="gl-ml-3">
- <gl-dropdown
- v-gl-tooltip
- icon="download"
- size="small"
- category="tertiary"
- variant="confirm"
- right
- >
- <gl-dropdown-item
- v-for="artifact in artifacts"
- :key="artifact.path"
- :href="artifact.path"
- :data-testid="`download-${artifact.name}`"
- download
- >
- {{ artifactText(artifact) }}
- </gl-dropdown-item>
- </gl-dropdown>
- </div>
+ <gl-disclosure-dropdown
+ class="gl-ml-3"
+ size="small"
+ icon="download"
+ :items="listboxOptions"
+ />
</template>
</mr-widget>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/index.vue b/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/index.vue
index 1b03b9c04e1..c12bc6456a5 100644
--- a/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/index.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/index.vue
@@ -19,7 +19,9 @@ import {
import { i18n, TESTS_FAILED_STATUS, ERROR_STATUS } from './constants';
export default {
- name: 'WidgetTestReport',
+ // widget name does not match file path because widget name must match telemetry event names
+ // see https://gitlab.com/gitlab-org/gitlab/-/issues/427061
+ name: 'WidgetTestSummary',
components: {
MrWidget,
MrWidgetRow,
diff --git a/app/assets/javascripts/vue_merge_request_widget/mixins/approvals.js b/app/assets/javascripts/vue_merge_request_widget/mixins/approvals.js
index 564e9321d54..8bb2f2898eb 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mixins/approvals.js
+++ b/app/assets/javascripts/vue_merge_request_widget/mixins/approvals.js
@@ -18,8 +18,16 @@ export default {
iid: `${this.mr.iid}`,
};
},
- update: (data) => data.project.mergeRequest,
+ update: (data) => data.project?.mergeRequest,
result({ data }) {
+ // This case can occur when backend returns an empty project due to expired session.
+ // See https://gitlab.com/gitlab-org/gitlab/-/issues/413627 for more information.
+ if (!data.project) {
+ // Needed to suppress several errors.
+ this.mr.setApprovals({});
+ return;
+ }
+
const { mergeRequest } = data.project;
this.disableCommittersApproval = data.project.mergeRequestsDisableCommittersApproval;
diff --git a/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js
index 2f49252a06b..623b504fcc1 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js
+++ b/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js
@@ -21,13 +21,6 @@ export default {
this.mr.preventMerge,
);
},
- mergeDisabledText() {
- if (this.pipeline?.status === PIPELINE_SKIPPED_STATUS) {
- return MERGE_DISABLED_SKIPPED_PIPELINE_TEXT;
- }
-
- return MERGE_DISABLED_TEXT;
- },
pipelineMustSucceedConflictText() {
return PIPELINE_MUST_SUCCEED_CONFLICT_TEXT;
},
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
index 02d73cf9cbd..cc116b42f1e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
@@ -1,9 +1,6 @@
<script>
import { isEmpty, clamp } from 'lodash';
-import {
- registerExtension,
- registeredExtensions,
-} from '~/vue_merge_request_widget/components/extensions';
+import { registeredExtensions } from '~/vue_merge_request_widget/components/extensions';
import SafeHtml from '~/vue_shared/directives/safe_html';
import MrWidgetApprovals from 'ee_else_ce/vue_merge_request_widget/components/approvals/approvals.vue';
import MRWidgetService from 'ee_else_ce/vue_merge_request_widget/services/mr_widget_service';
@@ -55,7 +52,6 @@ import eventHub from './event_hub';
import mergeRequestQueryVariablesMixin from './mixins/merge_request_query_variables';
import getStateQuery from './queries/get_state.query.graphql';
import getStateSubscription from './queries/get_state.subscription.graphql';
-import accessibilityExtension from './extensions/accessibility';
import ReportWidgetContainer from './components/report_widget_container.vue';
import MrWidgetReadyToMerge from './components/states/new_ready_to_merge.vue';
@@ -235,9 +231,6 @@ export default {
false,
);
},
- shouldShowAccessibilityReport() {
- return Boolean(this.mr?.accessibilityReportPath);
- },
formattedHumanAccess() {
return (this.mr.humanAccess || '').toLowerCase();
},
@@ -268,11 +261,6 @@ export default {
this.initPostMergeDeploymentsPolling();
}
},
- shouldShowAccessibilityReport(newVal) {
- if (newVal) {
- this.registerAccessibilityExtension();
- }
- },
},
mounted() {
MRWidgetService.fetchInitialData()
@@ -507,11 +495,6 @@ export default {
dismissSuggestPipelines() {
this.mr.isDismissedSuggestPipeline = true;
},
- registerAccessibilityExtension() {
- if (this.shouldShowAccessibilityReport) {
- registerExtension(accessibilityExtension);
- }
- },
},
};
</script>
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/merge_checks.query.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/merge_checks.query.graphql
index 6b602a0095c..fcaddcc2a42 100644
--- a/app/assets/javascripts/vue_merge_request_widget/queries/merge_checks.query.graphql
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/merge_checks.query.graphql
@@ -6,7 +6,10 @@ query mergeChecks($projectPath: ID!, $iid: String!) {
userPermissions {
canMerge
}
- mergeChecks @client
+ mergeabilityChecks {
+ identifier
+ status
+ }
}
}
}
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue b/app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue
index 6803d609dbc..e84b3f53b53 100644
--- a/app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue
@@ -10,7 +10,7 @@ import {
GlTab,
GlButton,
} from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import SafeHtml from '~/vue_shared/directives/safe_html';
import highlightCurrentUser from '~/behaviors/markdown/highlight_current_user';
import { fetchPolicies } from '~/lib/graphql';
@@ -30,7 +30,7 @@ import AlertSidebar from './alert_sidebar.vue';
import AlertSummaryRow from './alert_summary_row.vue';
import SystemNote from './system_notes/system_note.vue';
-const containerEl = document.querySelector('.page-with-contextual-sidebar');
+const containerEl = document.querySelector('.layout-page');
export default {
i18n: {
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue
index 2d3815439a6..056388f690d 100644
--- a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue
@@ -284,13 +284,7 @@ export default {
>
<div v-if="userName" class="gl-display-inline-flex gl-mt-2" data-testid="assigned-users">
<span class="gl-relative gl-mr-4">
- <img
- :alt="userName"
- :src="userImg"
- :width="32"
- class="avatar avatar-inline gl-m-0 s32"
- data-qa-selector="avatar_image"
- />
+ <img :alt="userName" :src="userImg" :width="32" class="avatar avatar-inline gl-m-0 s32" />
</span>
<span class="gl-display-flex gl-flex-direction-column gl-overflow-hidden">
<strong class="dropdown-menu-user-full-name">
diff --git a/app/assets/javascripts/vue_shared/components/chronic_duration_input.vue b/app/assets/javascripts/vue_shared/components/chronic_duration_input.vue
index ffbcdefc924..93e1fc4a0c2 100644
--- a/app/assets/javascripts/vue_shared/components/chronic_duration_input.vue
+++ b/app/assets/javascripts/vue_shared/components/chronic_duration_input.vue
@@ -1,6 +1,6 @@
<script>
-import * as Sentry from '@sentry/browser';
import { GlFormInput } from '@gitlab/ui';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import {
DurationParseError,
outputChronicDuration,
diff --git a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
deleted file mode 100644
index abbeac0e098..00000000000
--- a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
+++ /dev/null
@@ -1,157 +0,0 @@
-<script>
-import { GlBadge, GlTooltipDirective } from '@gitlab/ui';
-import CiIcon from './ci_icon.vue';
-
-/**
- * Renders CI Badge link with CI icon and status text based on
- * API response shared between all places where it is used.
- *
- * Receives status object containing:
- * status: {
- * details_path or detailsPath: "/gitlab-org/gitlab-foss/pipelines/8150156" // url
- * group:"running" // used for CSS class
- * icon: "icon_status_running" // used to render the icon
- * label:"running" // used for potential tooltip
- * text:"running" // text rendered
- * }
- *
- * Used in:
- * - Pipelines table - first column
- * - Jobs table - first column
- * - Pipeline show view - header
- * - Job show view - header
- * - MR widget
- * - Terraform table
- * - On-demand scans list
- */
-
-const badgeSizeOptions = {
- sm: 'sm',
- md: 'md',
- lg: 'lg',
-};
-
-export default {
- components: {
- CiIcon,
- GlBadge,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- props: {
- status: {
- type: Object,
- required: true,
- },
- showText: {
- type: Boolean,
- required: false,
- default: true,
- },
- size: {
- type: String,
- required: false,
- default: badgeSizeOptions.md,
- validator(value) {
- return badgeSizeOptions[value] !== undefined;
- },
- },
- showTooltip: {
- type: Boolean,
- required: false,
- default: true,
- },
- useLink: {
- type: Boolean,
- default: true,
- required: false,
- },
- },
- computed: {
- isNotLargeBadgeSize() {
- return this.size !== badgeSizeOptions.lg;
- },
- title() {
- return this.showTooltip && !this.showText ? this.status?.text : '';
- },
- detailsPath() {
- // For now, this can either come from graphQL with camelCase or REST API in snake_case
- if (!this.useLink) {
- return null;
- }
- return this.status.detailsPath || this.status.details_path;
- },
- badgeStyles() {
- switch (this.status.icon) {
- case 'status_success':
- return {
- textColor: 'gl-text-green-700',
- variant: 'success',
- };
- case 'status_warning':
- return {
- textColor: 'gl-text-orange-700',
- variant: 'warning',
- };
- case 'status_failed':
- return {
- textColor: 'gl-text-red-700',
- variant: 'danger',
- };
- case 'status_running':
- return {
- textColor: 'gl-text-blue-700',
- variant: 'info',
- };
- case 'status_pending':
- return {
- textColor: 'gl-text-orange-700',
- variant: 'warning',
- };
- case 'status_canceled':
- return {
- textColor: 'gl-text-gray-700',
- variant: 'neutral',
- };
- case 'status_manual':
- return {
- textColor: 'gl-text-gray-700',
- variant: 'neutral',
- };
- // default covers the styles for the remainder of CI
- // statuses that are not explicitly stated here
- default:
- return {
- textColor: 'gl-text-gray-600',
- variant: 'muted',
- };
- }
- },
- },
-};
-</script>
-<template>
- <gl-badge
- v-gl-tooltip
- :class="{ 'gl-px-2': !showText && isNotLargeBadgeSize }"
- :title="title"
- :href="detailsPath"
- :size="size"
- :variant="badgeStyles.variant"
- data-testid="ci-badge-link"
- @click="$emit('ciStatusBadgeClick')"
- >
- <ci-icon :status="status" />
-
- <template v-if="showText">
- <span
- class="gl-ml-2 gl-white-space-nowrap"
- :class="badgeStyles.textColor"
- data-testid="ci-badge-text"
- >
- {{ status.text }}
- </span>
- </template>
- </gl-badge>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/ci_icon.vue b/app/assets/javascripts/vue_shared/components/ci_icon.vue
index 6670b931416..a2b6b4642c9 100644
--- a/app/assets/javascripts/vue_shared/components/ci_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_icon.vue
@@ -1,99 +1,115 @@
<script>
-import { GlIcon } from '@gitlab/ui';
+import { GlBadge, GlTooltipDirective, GlIcon } from '@gitlab/ui';
/**
* Renders CI icon based on API response shared between all places where it is used.
*
* Receives status object containing:
* status: {
- * group:"running" // used for CSS class
- * icon: "icon_status_running" // used to render the icon
+ * icon: "status_running" // used to render the icon and CSS class
+ * text: "Running",
+ * detailsPath: '/project1/jobs/1' // can also be details_path
* }
*
- * Used in:
- * - Extended MR Popover
- * - Jobs show view header
- * - Jobs show view sidebar
- * - Jobs table
- * - Linked pipelines
- * - Pipeline graph
- * - Pipeline mini graph
- * - Pipeline show view badge
- * - Pipelines table Badge
*/
-/*
- * These sizes are defined in gitlab-ui/src/scss/variables.scss
- * under '$gl-icon-sizes'
- */
-const validSizes = [8, 12, 14, 16, 24, 32, 48, 72];
-
export default {
components: {
+ GlBadge,
GlIcon,
},
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
props: {
status: {
type: Object,
required: true,
validator(status) {
- const { group, icon } = status;
- return (
- typeof group === 'string' &&
- group.length &&
- typeof icon === 'string' &&
- icon.startsWith('status_')
- );
+ const { icon } = status;
+ return typeof icon === 'string' && icon.startsWith('status_');
},
},
- size: {
- type: Number,
- required: false,
- default: 16,
- validator(value) {
- return validSizes.includes(value);
- },
- },
- isActive: {
+ showStatusText: {
type: Boolean,
required: false,
default: false,
},
- isBorderless: {
+ showTooltip: {
type: Boolean,
required: false,
- default: false,
+ default: true,
},
- isInteractive: {
+ useLink: {
type: Boolean,
+ default: true,
required: false,
- default: false,
- },
- cssClasses: {
- type: String,
- required: false,
- default: '',
},
},
computed: {
- wrapperStyleClasses() {
- const status = this.status.group;
- return `ci-status-icon ci-status-icon-${status} gl-rounded-full gl-justify-content-center gl-line-height-0`;
+ title() {
+ if (this.showTooltip) {
+ // show tooltip only when not showing text already
+ return !this.showStatusText ? this.status?.text : null;
+ }
+ return null;
+ },
+ ariaLabel() {
+ // show aria-label only when text is not rendered
+ if (!this.showStatusText) {
+ return this.status?.text;
+ }
+ return null;
+ },
+ href() {
+ // href can come from GraphQL (camelCase) or REST API (snake_case)
+ if (this.useLink) {
+ return this.status.detailsPath || this.status.details_path;
+ }
+ return null;
},
icon() {
- return this.isBorderless ? `${this.status.icon}_borderless` : this.status.icon;
+ if (this.status.icon) {
+ return `${this.status.icon}_borderless`;
+ }
+ return null;
+ },
+ variant() {
+ switch (this.status.icon) {
+ case 'status_success':
+ return 'success';
+ case 'status_warning':
+ case 'status_pending':
+ return 'warning';
+ case 'status_failed':
+ return 'danger';
+ case 'status_running':
+ return 'info';
+ // default covers the styles for the remainder of CI
+ // statuses that are not explicitly stated here
+ default:
+ return 'neutral';
+ }
},
},
};
</script>
<template>
- <span
- :class="[
- wrapperStyleClasses,
- { interactive: isInteractive, active: isActive, borderless: isBorderless },
- ]"
- :style="{ height: `${size}px`, width: `${size}px` }"
+ <gl-badge
+ v-gl-tooltip
+ class="ci-icon gl-p-2"
+ :class="`ci-icon-variant-${variant}`"
+ :variant="variant"
+ :title="title"
+ :aria-label="ariaLabel"
+ :href="href"
+ size="md"
+ data-testid="ci-icon"
+ @click="$emit('ciStatusBadgeClick')"
>
- <gl-icon :name="icon" :size="size" :class="cssClasses" :aria-label="status.icon" />
- </span>
+ <span class="ci-icon-gl-icon-wrapper"><gl-icon :name="icon" /></span
+ ><span v-if="showStatusText" class="gl-mx-2 gl-white-space-nowrap" data-testid="ci-icon-text">{{
+ status.text
+ }}</span>
+ </gl-badge>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/diff_stats_dropdown.vue b/app/assets/javascripts/vue_shared/components/diff_stats_dropdown.vue
index f62bfb551df..55767c5f4bc 100644
--- a/app/assets/javascripts/vue_shared/components/diff_stats_dropdown.vue
+++ b/app/assets/javascripts/vue_shared/components/diff_stats_dropdown.vue
@@ -1,11 +1,5 @@
<script>
-import {
- GlDropdown,
- GlDropdownItem,
- GlDropdownText,
- GlSearchBoxByType,
- GlSprintf,
-} from '@gitlab/ui';
+import { GlDisclosureDropdown, GlIcon, GlSearchBoxByType, GlSprintf } from '@gitlab/ui';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { __, n__, s__, sprintf } from '~/locale';
@@ -16,12 +10,16 @@ export const i18n = {
searchFiles: __('Search files'),
};
+const variantCssColorMap = {
+ success: 'gl-text-green-500',
+ danger: 'gl-text-red-500',
+};
+
export default {
i18n,
components: {
- GlDropdown,
- GlDropdownItem,
- GlDropdownText,
+ GlDisclosureDropdown,
+ GlIcon,
GlSearchBoxByType,
GlSprintf,
},
@@ -54,6 +52,15 @@ export default {
? fuzzaldrinPlus.filter(this.files, this.search, { key: 'name' })
: this.files;
},
+ dropdownItems() {
+ return this.filteredFiles.map((file) => {
+ return {
+ ...file,
+ text: file.name || this.$options.i18n.noFileNameAvailable,
+ iconColor: variantCssColorMap[file.iconColor],
+ };
+ });
+ },
messageChanged() {
return sprintf(
n__(
@@ -64,21 +71,21 @@ export default {
{ count: this.changed },
);
},
-
- additionsText() {
- return n__('Diffs|%d addition', 'Diffs|%d additions', this.added);
- },
- deletionsText() {
- return n__('Diffs|%d deletion', 'Diffs|%d deletions', this.deleted);
- },
},
methods: {
- jumpToFile(fileHash) {
- window.location.hash = fileHash;
- },
focusInput() {
this.$refs.search.focusInput();
},
+ focusFirstItem() {
+ if (!this.filteredFiles.length) return;
+ this.$el.querySelector('.gl-new-dropdown-item:first-child').focus();
+ },
+ additionsText(numberOfChanges = this.added) {
+ return n__('Diffs|%d addition', 'Diffs|%d additions', numberOfChanges);
+ },
+ deletionsText(numberOfChanges = this.deleted) {
+ return n__('Diffs|%d deletion', 'Diffs|%d deletions', numberOfChanges);
+ },
},
};
</script>
@@ -87,15 +94,15 @@ export default {
<div>
<gl-sprintf :message="messageChanged">
<template #dropdown="{ content: dropdownText }">
- <gl-dropdown
+ <gl-disclosure-dropdown
+ :toggle-text="dropdownText"
+ :items="dropdownItems"
category="tertiary"
variant="confirm"
- :text="dropdownText"
data-testid="diff-stats-dropdown"
class="gl-vertical-align-baseline"
toggle-class="gl-px-0! gl-font-weight-bold!"
- menu-class="gl-w-auto!"
- no-flip
+ fluid-width
@shown="focusInput"
>
<template #header>
@@ -103,35 +110,38 @@ export default {
ref="search"
v-model.trim="search"
:placeholder="$options.i18n.searchFiles"
+ class="gl-mx-3 gl-my-4"
+ @keydown.down="focusFirstItem"
/>
+ <span v-if="!filteredFiles.length" class="gl-mx-3">
+ {{ $options.i18n.noFilesFound }}
+ </span>
</template>
- <gl-dropdown-item
- v-for="file in filteredFiles"
- :key="file.href"
- :icon-name="file.icon"
- :icon-color="file.iconColor"
- @click="jumpToFile(file.href)"
- >
- <div class="gl-display-flex">
- <span v-if="file.name" class="gl-font-weight-bold gl-mr-3 gl-text-truncate">{{
- file.name
- }}</span>
- <span v-else class="gl-mr-3 gl-font-weight-bold gl-font-style-italic gl-gray-400">{{
- $options.i18n.noFileNameAvailable
- }}</span>
- <span class="gl-ml-auto gl-white-space-nowrap">
- <span class="gl-text-green-600">+{{ file.added }}</span>
- <span class="gl-text-red-500">-{{ file.removed }}</span>
- </span>
+ <template #list-item="{ item }">
+ <div class="gl-display-flex gl-gap-3 gl-align-items-center gl-overflow-hidden">
+ <gl-icon :name="item.icon" :class="item.iconColor" class="gl-flex-shrink-0" />
+ <div class="gl-flex-grow-1 gl-overflow-hidden">
+ <div class="gl-display-flex">
+ <span
+ class="gl-font-weight-bold gl-mr-3 gl-flex-grow-1"
+ :class="item.name ? 'gl-text-truncate' : 'gl-font-style-italic gl-gray-400'"
+ >{{ item.text }}</span
+ >
+ <span class="gl-ml-auto gl-white-space-nowrap" aria-hidden="true">
+ <span class="gl-text-green-600">+{{ item.added }}</span>
+ <span class="gl-text-red-500">-{{ item.removed }}</span>
+ </span>
+ <span class="gl-sr-only"
+ >{{ additionsText(item.added) }}, {{ deletionsText(item.removed) }}</span
+ >
+ </div>
+ <div class="gl-text-gray-700 gl-overflow-hidden gl-text-overflow-ellipsis">
+ {{ item.path }}
+ </div>
+ </div>
</div>
- <div class="gl-text-gray-700 gl-overflow-hidden gl-text-overflow-ellipsis">
- {{ file.path }}
- </div>
- </gl-dropdown-item>
- <gl-dropdown-text v-if="!filteredFiles.length">
- {{ $options.i18n.noFilesFound }}
- </gl-dropdown-text>
- </gl-dropdown>
+ </template>
+ </gl-disclosure-dropdown>
</template>
</gl-sprintf>
<span
@@ -140,12 +150,20 @@ export default {
>
<gl-sprintf :message="$options.i18n.messageAdditionsDeletions">
<template #additions>
- <span class="gl-text-green-600 gl-font-weight-bold">{{ additionsText }}</span>
+ <span class="gl-text-green-600 gl-font-weight-bold">{{ additionsText() }}</span>
</template>
<template #deletions>
- <span class="gl-text-red-500 gl-font-weight-bold">{{ deletionsText }}</span>
+ <span class="gl-text-red-500 gl-font-weight-bold">{{ deletionsText() }}</span>
</template>
</gl-sprintf>
</span>
</div>
</template>
+
+<style scoped>
+/* TODO: Use max-height prop when gitlab-ui got updated.
+See https://gitlab.com/gitlab-org/gitlab-ui/-/issues/2374 */
+::v-deep .gl-new-dropdown-inner {
+ max-height: 310px;
+}
+</style>
diff --git a/app/assets/javascripts/vue_shared/components/entity_select/constants.js b/app/assets/javascripts/vue_shared/components/entity_select/constants.js
index 0fb5a2d5534..5bad907c9f9 100644
--- a/app/assets/javascripts/vue_shared/components/entity_select/constants.js
+++ b/app/assets/javascripts/vue_shared/components/entity_select/constants.js
@@ -14,3 +14,13 @@ export const PROJECT_TOGGLE_TEXT = s__('ProjectSelect|Search for project');
export const PROJECT_HEADER_TEXT = s__('ProjectSelect|Select a project');
export const FETCH_PROJECTS_ERROR = __('Unable to fetch projects. Reload the page to try again.');
export const FETCH_PROJECT_ERROR = __('Unable to fetch project. Reload the page to try again.');
+
+// Organizations
+export const ORGANIZATION_TOGGLE_TEXT = s__('Organization|Search for an organization');
+export const ORGANIZATION_HEADER_TEXT = s__('Organization|Select an organization');
+export const FETCH_ORGANIZATIONS_ERROR = s__(
+ 'Organization|Unable to fetch organizations. Reload the page to try again.',
+);
+export const FETCH_ORGANIZATION_ERROR = s__(
+ 'Organization|Unable to fetch organizations. Reload the page to try again.',
+);
diff --git a/app/assets/javascripts/vue_shared/components/entity_select/entity_select.vue b/app/assets/javascripts/vue_shared/components/entity_select/entity_select.vue
index 970c24c6e87..1a215454ab6 100644
--- a/app/assets/javascripts/vue_shared/components/entity_select/entity_select.vue
+++ b/app/assets/javascripts/vue_shared/components/entity_select/entity_select.vue
@@ -22,6 +22,11 @@ export default {
type: String,
required: true,
},
+ description: {
+ type: String,
+ required: false,
+ default: '',
+ },
inputName: {
type: String,
required: true,
@@ -31,7 +36,7 @@ export default {
required: true,
},
initialSelection: {
- type: String,
+ type: [String, Number],
required: false,
default: null,
},
@@ -57,6 +62,11 @@ export default {
required: false,
default: null,
},
+ toggleClass: {
+ type: [String, Array, Object],
+ required: false,
+ default: '',
+ },
},
data() {
return {
@@ -152,6 +162,7 @@ export default {
this.searching = true;
const name = await this.fetchInitialSelectionText(this.initialSelection);
+
this.selectedValue = this.initialSelection;
this.selectedText = name;
this.pristine = false;
@@ -178,7 +189,7 @@ export default {
</script>
<template>
- <gl-form-group :label="label">
+ <gl-form-group :label="label" :description="description">
<slot name="error"></slot>
<template v-if="Boolean($scopedSlots.label)" #label>
<slot name="label"></slot>
@@ -196,6 +207,7 @@ export default {
:no-results-text="noResultsText"
:infinite-scroll="hasMoreItems"
:infinite-scroll-loading="infiniteScrollLoading"
+ :toggle-class="toggleClass"
searchable
@shown="onShown"
@search="search"
diff --git a/app/assets/javascripts/vue_shared/components/entity_select/group_select.vue b/app/assets/javascripts/vue_shared/components/entity_select/group_select.vue
index eb7b20fa4c1..8a338551fbe 100644
--- a/app/assets/javascripts/vue_shared/components/entity_select/group_select.vue
+++ b/app/assets/javascripts/vue_shared/components/entity_select/group_select.vue
@@ -1,6 +1,6 @@
<script>
import { GlAlert } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import axios from '~/lib/utils/axios_utils';
import { normalizeHeaders, parseIntPagination } from '~/lib/utils/common_utils';
import Api, { DEFAULT_PER_PAGE } from '~/api';
diff --git a/app/assets/javascripts/vue_shared/components/entity_select/organization_select.vue b/app/assets/javascripts/vue_shared/components/entity_select/organization_select.vue
new file mode 100644
index 00000000000..d068d86d95b
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/entity_select/organization_select.vue
@@ -0,0 +1,150 @@
+<script>
+import { GlAlert } from '@gitlab/ui';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
+import getCurrentUserOrganizationsQuery from '~/organizations/index/graphql/organizations.query.graphql';
+import getOrganizationQuery from '~/organizations/shared/graphql/queries/organization.query.graphql';
+import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils';
+import { TYPENAME_ORGANIZATION } from '~/graphql_shared/constants';
+import {
+ ORGANIZATION_TOGGLE_TEXT,
+ ORGANIZATION_HEADER_TEXT,
+ FETCH_ORGANIZATIONS_ERROR,
+ FETCH_ORGANIZATION_ERROR,
+} from './constants';
+import EntitySelect from './entity_select.vue';
+
+export default {
+ name: 'OrganizationSelect',
+ components: {
+ GlAlert,
+ EntitySelect,
+ },
+ props: {
+ block: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ label: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ description: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ inputName: {
+ type: String,
+ required: true,
+ },
+ inputId: {
+ type: String,
+ required: true,
+ },
+ initialSelection: {
+ type: [String, Number],
+ required: false,
+ default: null,
+ },
+ clearable: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ toggleClass: {
+ type: [String, Array, Object],
+ required: false,
+ default: '',
+ },
+ },
+ data() {
+ return {
+ errorMessage: '',
+ };
+ },
+ methods: {
+ async fetchOrganizations() {
+ try {
+ const {
+ data: {
+ currentUser: {
+ organizations: { nodes },
+ },
+ },
+ } = await this.$apollo.query({
+ query: getCurrentUserOrganizationsQuery,
+ // TODO: implement search support - https://gitlab.com/gitlab-org/gitlab/-/issues/429999.
+ });
+
+ return {
+ items: nodes.map((organization) => ({
+ text: organization.name,
+ value: getIdFromGraphQLId(organization.id),
+ })),
+ // TODO: implement pagination - https://gitlab.com/gitlab-org/gitlab/-/issues/429999.
+ totalPages: 1,
+ };
+ } catch (error) {
+ this.handleError({ message: FETCH_ORGANIZATIONS_ERROR, error });
+
+ return { items: [], totalPages: 0 };
+ }
+ },
+ async fetchOrganizationName(id) {
+ try {
+ const {
+ data: {
+ organization: { name },
+ },
+ } = await this.$apollo.query({
+ query: getOrganizationQuery,
+ variables: { id: convertToGraphQLId(TYPENAME_ORGANIZATION, id) },
+ });
+
+ return name;
+ } catch (error) {
+ this.handleError({ message: FETCH_ORGANIZATION_ERROR, error });
+
+ return '';
+ }
+ },
+ handleError({ message, error }) {
+ Sentry.captureException(error);
+ this.errorMessage = message;
+ },
+ dismissError() {
+ this.errorMessage = '';
+ },
+ },
+ i18n: {
+ toggleText: ORGANIZATION_TOGGLE_TEXT,
+ selectGroup: ORGANIZATION_HEADER_TEXT,
+ },
+};
+</script>
+
+<template>
+ <entity-select
+ :block="block"
+ :label="label"
+ :description="description"
+ :input-name="inputName"
+ :input-id="inputId"
+ :initial-selection="initialSelection"
+ :clearable="clearable"
+ :header-text="$options.i18n.selectGroup"
+ :default-toggle-text="$options.i18n.toggleText"
+ :fetch-items="fetchOrganizations"
+ :fetch-initial-selection-text="fetchOrganizationName"
+ :toggle-class="toggleClass"
+ v-on="$listeners"
+ >
+ <template #error>
+ <gl-alert v-if="errorMessage" class="gl-mb-3" variant="danger" @dismiss="dismissError">{{
+ errorMessage
+ }}</gl-alert>
+ </template>
+ </entity-select>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/entity_select/project_select.vue b/app/assets/javascripts/vue_shared/components/entity_select/project_select.vue
index 13a825a68f6..8c371e3d4ce 100644
--- a/app/assets/javascripts/vue_shared/components/entity_select/project_select.vue
+++ b/app/assets/javascripts/vue_shared/components/entity_select/project_select.vue
@@ -1,6 +1,6 @@
<script>
import { GlAlert } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import Api from '~/api';
import SafeHtml from '~/vue_shared/directives/safe_html';
import {
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
index 346384e3023..d39e4d2ee42 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
@@ -292,7 +292,9 @@ export default {
this.recentSearchesService.save(resultantSearches);
this.recentSearches = [];
},
- handleFilterSubmit() {
+ async handleFilterSubmit() {
+ this.blurSearchInput();
+ await this.$nextTick();
const filterTokens = uniqueTokens(this.filterValue);
this.filterValue = filterTokens;
@@ -309,7 +311,6 @@ export default {
// https://gitlab.com/gitlab-org/gitlab-foss/issues/30821
});
}
- this.blurSearchInput();
this.$emit('onFilter', this.removeQuotesEnclosure(filterTokens));
},
historyTokenOptionTitle(historyToken) {
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue
index 23de8dd5596..3857dd9c55d 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue
@@ -7,9 +7,10 @@ import {
GlDropdownText,
GlLoadingIcon,
} from '@gitlab/ui';
-import { debounce } from 'lodash';
+import { debounce, last } from 'lodash';
import { stripQuotes } from '~/lib/utils/text_utility';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { DEBOUNCE_DELAY, FILTERS_NONE_ANY, OPERATOR_NOT, OPERATOR_OR } from '../constants';
import { getRecentlyUsedSuggestions, setTokenValueToRecentlyUsed } from '../filtered_search_utils';
@@ -22,6 +23,7 @@ export default {
GlDropdownText,
GlLoadingIcon,
},
+ mixins: [glFeatureFlagMixin()],
props: {
config: {
type: Object,
@@ -70,6 +72,11 @@ export default {
required: false,
default: undefined,
},
+ multiSelectValues: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
},
data() {
return {
@@ -94,7 +101,11 @@ export default {
return this.preloadedSuggestions.map((tokenValue) => tokenValue[this.valueIdentifier]);
},
activeTokenValue() {
- return this.getActiveTokenValue(this.suggestions, this.value.data);
+ const data =
+ this.glFeatures.groupMultiSelectTokens && Array.isArray(this.value.data)
+ ? last(this.value.data)
+ : this.value.data;
+ return this.getActiveTokenValue(this.suggestions, data);
},
availableDefaultSuggestions() {
if ([OPERATOR_NOT, OPERATOR_OR].includes(this.value.operator)) {
@@ -146,10 +157,14 @@ export default {
watch: {
active: {
immediate: true,
- handler(newValue) {
- if (!newValue && !this.suggestions.length) {
- const search = this.searchTerm ? this.searchTerm : this.value.data;
- this.$emit('fetch-suggestions', search);
+ handler(active) {
+ if (!active && !this.suggestions.length) {
+ // data could be a string or an array of strings
+ const selectedItems = [this.value.data].flat();
+ selectedItems.forEach((item) => {
+ const search = this.searchTerm ? this.searchTerm : item;
+ this.$emit('fetch-suggestions', search);
+ });
}
},
},
@@ -163,6 +178,9 @@ export default {
},
methods: {
handleInput: debounce(function debouncedSearch({ data, operator }) {
+ // in multiSelect mode, data could be an array
+ if (Array.isArray(data)) return;
+
// Prevent fetching suggestions when data or operator is not present
if (data || operator) {
this.searchKey = data;
@@ -181,8 +199,11 @@ export default {
}
}, DEBOUNCE_DELAY),
handleTokenValueSelected(selectedValue) {
- const activeTokenValue = this.getActiveTokenValue(this.suggestions, selectedValue);
+ if (this.glFeatures.groupMultiSelectTokens) {
+ this.$emit('token-selected', selectedValue);
+ }
+ const activeTokenValue = this.getActiveTokenValue(this.suggestions, selectedValue);
// Make sure that;
// 1. Recently used values feature is enabled
// 2. User has actually selected a value
@@ -210,6 +231,7 @@ export default {
:config="config"
:value="value"
:active="active"
+ :multi-select-values="multiSelectValues"
v-bind="$attrs"
v-on="$listeners"
@input="handleInput"
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/user_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/user_token.vue
index 4601287b417..c5326ead60d 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/user_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/user_token.vue
@@ -1,11 +1,12 @@
<script>
-import { GlAvatar, GlFilteredSearchSuggestion } from '@gitlab/ui';
+import { GlAvatar, GlIcon, GlIntersperse, GlFilteredSearchSuggestion } from '@gitlab/ui';
import { compact } from 'lodash';
import { createAlert } from '~/alert';
import { __ } from '~/locale';
import { WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants';
import usersAutocompleteQuery from '~/graphql_shared/queries/users_autocomplete.query.graphql';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { OPTIONS_NONE_ANY } from '../constants';
import BaseToken from './base_token.vue';
@@ -14,8 +15,11 @@ export default {
components: {
BaseToken,
GlAvatar,
+ GlIcon,
+ GlIntersperse,
GlFilteredSearchSuggestion,
},
+ mixins: [glFeatureFlagMixin()],
props: {
config: {
type: Object,
@@ -32,8 +36,11 @@ export default {
},
data() {
return {
+ // current users visible in list
users: this.config.initialUsers || [],
+ allUsers: this.config.initialUsers || [],
loading: false,
+ selectedUsernames: [],
};
},
computed: {
@@ -49,13 +56,69 @@ export default {
fetchUsersQuery() {
return this.config.fetchUsers ? this.config.fetchUsers : this.fetchUsersBySearchTerm;
},
+ multiSelectEnabled() {
+ return this.config.multiSelect && this.glFeatures.groupMultiSelectTokens;
+ },
+ },
+ watch: {
+ value: {
+ deep: true,
+ immediate: true,
+ handler(newValue) {
+ const { data } = newValue;
+
+ if (!this.multiSelectEnabled) {
+ return;
+ }
+
+ // don't add empty values to selectedUsernames
+ if (!data) {
+ return;
+ }
+
+ if (Array.isArray(data)) {
+ this.selectedUsernames = data;
+ // !active so we don't add strings while searching, e.g. r, ro, roo
+ // !includes so we don't add the same usernames (if @input is emitted twice)
+ } else if (!this.active && !this.selectedUsernames.includes(data)) {
+ this.selectedUsernames = this.selectedUsernames.concat(data);
+ }
+ },
+ },
},
methods: {
getActiveUser(users, data) {
return users.find((user) => user.username.toLowerCase() === data.toLowerCase());
},
getAvatarUrl(user) {
- return user.avatarUrl || user.avatar_url;
+ return user?.avatarUrl || user?.avatar_url;
+ },
+ displayNameFor(username) {
+ return this.getActiveUser(this.allUsers, username)?.name || `@${username}`;
+ },
+ avatarFor(username) {
+ const user = this.getActiveUser(this.allUsers, username);
+ return this.getAvatarUrl(user);
+ },
+ addCheckIcon(username) {
+ return this.multiSelectEnabled && this.selectedUsernames.includes(username);
+ },
+ addPadding(username) {
+ return this.multiSelectEnabled && !this.selectedUsernames.includes(username);
+ },
+ handleSelected(username) {
+ if (!this.multiSelectEnabled) {
+ return;
+ }
+
+ const index = this.selectedUsernames.indexOf(username);
+ if (index > -1) {
+ this.selectedUsernames.splice(index, 1);
+ } else {
+ this.selectedUsernames.push(username);
+ }
+
+ this.$emit('input', { ...this.value, data: '' });
},
fetchUsersBySearchTerm(search) {
return this.$apollo
@@ -79,6 +142,7 @@ export default {
// TODO: rm when completed https://gitlab.com/gitlab-org/gitlab/-/issues/345756
this.users = Array.isArray(res) ? compact(res) : compact(res.data);
+ this.allUsers = this.allUsers.concat(this.users);
})
.catch(() =>
createAlert({
@@ -103,18 +167,32 @@ export default {
:get-active-token-value="getActiveUser"
:default-suggestions="defaultUsers"
:preloaded-suggestions="preloadedUsers"
+ :multi-select-values="selectedUsernames"
v-bind="$attrs"
@fetch-suggestions="fetchUsers"
+ @token-selected="handleSelected"
v-on="$listeners"
>
<template #view="{ viewTokenProps: { inputValue, activeTokenValue } }">
- <gl-avatar
- v-if="activeTokenValue"
- :size="16"
- :src="getAvatarUrl(activeTokenValue)"
- class="gl-mr-2"
- />
- {{ activeTokenValue ? activeTokenValue.name : inputValue }}
+ <gl-intersperse v-if="multiSelectEnabled" separator=",">
+ <span
+ v-for="(username, index) in selectedUsernames"
+ :key="username"
+ :class="{ 'gl-ml-2': index > 0 }"
+ ><gl-avatar :size="16" :src="avatarFor(username)" class="gl-mr-1" />{{
+ displayNameFor(username)
+ }}</span
+ >
+ </gl-intersperse>
+ <template v-else>
+ <gl-avatar
+ v-if="activeTokenValue"
+ :size="16"
+ :src="getAvatarUrl(activeTokenValue)"
+ class="gl-mr-2"
+ />
+ {{ activeTokenValue ? activeTokenValue.name : inputValue }}
+ </template>
</template>
<template #suggestions-list="{ suggestions }">
<gl-filtered-search-suggestion
@@ -122,7 +200,15 @@ export default {
:key="user.username"
:value="user.username"
>
- <div class="gl-display-flex">
+ <div
+ class="gl-display-flex gl-align-items-center"
+ :class="{ 'gl-pl-6': addPadding(user.username) }"
+ >
+ <gl-icon
+ v-if="addCheckIcon(user.username)"
+ name="check"
+ class="gl-mr-3 gl-text-secondary gl-flex-shrink-0"
+ />
<gl-avatar :size="32" :src="getAvatarUrl(user)" />
<div>
<div>{{ user.name }}</div>
diff --git a/app/assets/javascripts/vue_shared/components/form/errors_alert.stories.js b/app/assets/javascripts/vue_shared/components/form/errors_alert.stories.js
new file mode 100644
index 00000000000..7c32e38a299
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/form/errors_alert.stories.js
@@ -0,0 +1,21 @@
+import ErrorsAlert from './errors_alert.vue';
+
+export default {
+ component: ErrorsAlert,
+ title: 'vue_shared/form/errors_alert',
+};
+
+const defaultProps = {
+ errors: ['Name must be at least 5 characters.', 'Name cannot contain special characters.'],
+};
+
+const Template = (args) => ({
+ components: { ErrorsAlert },
+ data() {
+ return { errors: args.errors };
+ },
+ template: `<errors-alert v-model="errors" />`,
+});
+
+export const Default = Template.bind({});
+Default.args = defaultProps;
diff --git a/app/assets/javascripts/vue_shared/components/form/errors_alert.vue b/app/assets/javascripts/vue_shared/components/form/errors_alert.vue
new file mode 100644
index 00000000000..3e33168781b
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/form/errors_alert.vue
@@ -0,0 +1,42 @@
+<script>
+import { GlAlert } from '@gitlab/ui';
+import { n__ } from '~/locale';
+
+export default {
+ components: { GlAlert },
+ model: {
+ prop: 'errors',
+ },
+ props: {
+ errors: {
+ type: Array,
+ required: true,
+ },
+ },
+ computed: {
+ title() {
+ return n__(
+ 'The form contains the following error:',
+ 'The form contains the following errors:',
+ this.errors.length,
+ );
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-alert
+ v-if="errors.length"
+ class="gl-mb-5"
+ :title="title"
+ variant="danger"
+ @dismiss="$emit('input', [])"
+ >
+ <ul class="gl-pl-5 gl-mb-0">
+ <li v-for="error in errors" :key="error">
+ {{ error }}
+ </li>
+ </ul>
+ </gl-alert>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue b/app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue
index d97f1ae6135..0455685627d 100644
--- a/app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue
+++ b/app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue
@@ -94,8 +94,12 @@ export default {
computedValueIsVisible() {
return !this.showToggleVisibilityButton || this.valueIsVisible;
},
- inputType() {
- return this.computedValueIsVisible ? 'text' : 'password';
+ formInputClass() {
+ return [
+ 'gl-font-monospace! gl-cursor-default!',
+ { 'input-copy-show-disc': !this.computedValueIsVisible },
+ this.formInputGroupProps.class,
+ ];
},
},
mounted() {
@@ -157,10 +161,9 @@ export default {
ref="input"
:readonly="readonly"
:width="size"
- class="gl-font-monospace! gl-cursor-default!"
+ :class="formInputClass"
v-bind="formInputGroupProps"
:value="value"
- :type="inputType"
@input="handleInput"
@click="handleClick"
/>
@@ -194,3 +197,8 @@ export default {
</template>
</gl-form-group>
</template>
+<style>
+.input-copy-show-disc {
+ -webkit-text-security: disc;
+}
+</style>
diff --git a/app/assets/javascripts/vue_shared/components/list_selector/constants.js b/app/assets/javascripts/vue_shared/components/list_selector/constants.js
new file mode 100644
index 00000000000..cff9c56a1c0
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/list_selector/constants.js
@@ -0,0 +1,6 @@
+import { __ } from '~/locale';
+
+export const CONFIG = {
+ users: { title: __('Users'), icon: 'user', filterKey: 'username', showNamespaceDropdown: true },
+ groups: { title: __('Groups'), icon: 'group', filterKey: 'name' },
+};
diff --git a/app/assets/javascripts/vue_shared/components/list_selector/group_item.vue b/app/assets/javascripts/vue_shared/components/list_selector/group_item.vue
new file mode 100644
index 00000000000..2d24cc5553b
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/list_selector/group_item.vue
@@ -0,0 +1,55 @@
+<script>
+import { GlAvatar, GlButton } from '@gitlab/ui';
+import { sprintf, __ } from '~/locale';
+
+export default {
+ name: 'GroupItem',
+ components: {
+ GlAvatar,
+ GlButton,
+ },
+ props: {
+ data: {
+ type: Object,
+ required: true,
+ },
+ canDelete: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ deleteButtonLabel() {
+ return sprintf(__('Delete %{name}'), { name: this.name });
+ },
+ fullName() {
+ return this.data.fullName;
+ },
+ name() {
+ return this.data.name;
+ },
+ avatarUrl() {
+ return this.data.avatarUrl;
+ },
+ },
+};
+</script>
+
+<template>
+ <span class="gl-display-flex gl-align-items-center gl-gap-3" @click="$emit('select', name)">
+ <gl-avatar :alt="fullName" :size="32" :src="avatarUrl" />
+ <span class="gl-display-flex gl-flex-direction-column gl-flex-grow-1">
+ <span class="gl-font-weight-bold">{{ fullName }}</span>
+ <span class="gl-text-gray-600">@{{ name }}</span>
+ </span>
+
+ <gl-button
+ v-if="canDelete"
+ icon="remove"
+ :aria-label="deleteButtonLabel"
+ category="tertiary"
+ @click="$emit('delete', name)"
+ />
+ </span>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/list_selector/index.vue b/app/assets/javascripts/vue_shared/components/list_selector/index.vue
new file mode 100644
index 00000000000..b8480a0c496
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/list_selector/index.vue
@@ -0,0 +1,193 @@
+<script>
+import { GlCard, GlIcon, GlCollapsibleListbox, GlSearchBoxByType } from '@gitlab/ui';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import { createAlert } from '~/alert';
+import { __ } from '~/locale';
+import groupsAutocompleteQuery from '~/graphql_shared/queries/groups_autocomplete.query.graphql';
+import Api from '~/api';
+import UserItem from './user_item.vue';
+import GroupItem from './group_item.vue';
+import { CONFIG } from './constants';
+
+const I18N = {
+ allGroups: __('All groups'),
+ projectGroups: __('Project groups'),
+ apiErrorMessage: __('An error occurred while fetching. Please try again.'),
+};
+
+export default {
+ name: 'ListSelector',
+ i18n: I18N,
+ components: {
+ GlCard,
+ GlIcon,
+ GlSearchBoxByType,
+ GlCollapsibleListbox,
+ },
+ props: {
+ title: {
+ type: String,
+ required: true,
+ },
+ type: {
+ type: String,
+ required: true,
+ },
+ selectedItems: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ projectPath: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ groupPath: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+ data() {
+ return {
+ searchValue: '',
+ isProjectNamespace: 'true',
+ selected: [],
+ items: [],
+ };
+ },
+ computed: {
+ config() {
+ return CONFIG[this.type];
+ },
+ isUserVariant() {
+ return this.type === 'users';
+ },
+ component() {
+ return this.isUserVariant ? UserItem : GroupItem;
+ },
+ namespaceDropdownText() {
+ return parseBoolean(this.isProjectNamespace)
+ ? this.$options.i18n.projectGroups
+ : this.$options.i18n.allGroups;
+ },
+ },
+ methods: {
+ async handleSearchInput(search) {
+ this.$refs.results.open();
+
+ try {
+ if (this.isUserVariant) {
+ this.items = await this.fetchUsersBySearchTerm(search);
+ } else {
+ this.items = await this.fetchGroupsBySearchTerm(search);
+ }
+ } catch (e) {
+ createAlert({
+ message: this.$options.i18n.apiErrorMessage,
+ });
+ }
+ },
+ async fetchUsersBySearchTerm(search) {
+ let users = [];
+ if (parseBoolean(this.isProjectNamespace)) {
+ users = await Api.projectUsers(this.projectPath, search);
+ } else {
+ const groupMembers = await Api.groupMembers(this.groupPath, { query: search });
+ users = groupMembers?.data || [];
+ }
+
+ return users?.map((user) => ({ text: user.name, value: user.username, ...user }));
+ },
+ fetchGroupsBySearchTerm(search) {
+ return this.$apollo
+ .query({
+ query: groupsAutocompleteQuery,
+ variables: { search },
+ })
+ .then(({ data }) =>
+ data?.groups.nodes.map((group) => ({
+ text: group.fullName,
+ value: group.name,
+ ...group,
+ })),
+ );
+ },
+ getItemByKey(key) {
+ return this.items.find((item) => item[this.config.filterKey] === key);
+ },
+ handleSelectItem(key) {
+ this.$emit('select', this.getItemByKey(key));
+ },
+ handleDeleteItem(key) {
+ this.$emit('delete', key);
+ },
+ handleSelectNamespace() {
+ this.items = [];
+ this.searchValue = '';
+ },
+ },
+ namespaceOptions: [
+ { text: I18N.projectGroups, value: 'true' },
+ { text: I18N.allGroups, value: 'false' },
+ ],
+};
+</script>
+
+<template>
+ <gl-card header-class="gl-new-card-header gl-border-none" body-class="gl-card-footer">
+ <template #header
+ ><strong data-testid="list-selector-title"
+ >{{ title }}
+ <span class="gl-text-gray-700 gl-ml-3"
+ ><gl-icon :name="config.icon" /> {{ selectedItems.length }}</span
+ ></strong
+ ></template
+ >
+
+ <div class="gl-display-flex gl-gap-3" :class="{ 'gl-mb-4': selectedItems.length }">
+ <gl-collapsible-listbox
+ ref="results"
+ v-model="selected"
+ class="list-selector gl-display-block gl-flex-grow-1"
+ :items="items"
+ multiple
+ @shown="$refs.search.focusInput()"
+ >
+ <template #toggle>
+ <gl-search-box-by-type
+ ref="search"
+ v-model="searchValue"
+ autofocus
+ debounce="500"
+ @input="handleSearchInput"
+ />
+ </template>
+
+ <template #list-item="{ item }">
+ <component :is="component" :data="item" @select="handleSelectItem" />
+ </template>
+ </gl-collapsible-listbox>
+
+ <gl-collapsible-listbox
+ v-if="config.showNamespaceDropdown"
+ v-model="isProjectNamespace"
+ :toggle-text="namespaceDropdownText"
+ :items="$options.namespaceOptions"
+ @select="handleSelectNamespace"
+ />
+ </div>
+
+ <component
+ :is="component"
+ v-for="(item, index) of selectedItems"
+ :key="index"
+ :class="{ 'gl-border-t': index > 0 }"
+ class="gl-p-3"
+ :data="item"
+ can-delete
+ @delete="handleDeleteItem"
+ />
+ </gl-card>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/list_selector/user_item.vue b/app/assets/javascripts/vue_shared/components/list_selector/user_item.vue
new file mode 100644
index 00000000000..fdbc767db81
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/list_selector/user_item.vue
@@ -0,0 +1,55 @@
+<script>
+import { GlAvatar, GlButton } from '@gitlab/ui';
+import { sprintf, __ } from '~/locale';
+
+export default {
+ name: 'UserItem',
+ components: {
+ GlAvatar,
+ GlButton,
+ },
+ props: {
+ data: {
+ type: Object,
+ required: true,
+ },
+ canDelete: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ deleteButtonLabel() {
+ return sprintf(__('Delete %{name}'), { name: this.name });
+ },
+ name() {
+ return this.data.name;
+ },
+ username() {
+ return this.data.username;
+ },
+ avatarUrl() {
+ return this.data.avatarUrl;
+ },
+ },
+};
+</script>
+
+<template>
+ <span class="gl-display-flex gl-align-items-center gl-gap-3" @click="$emit('select', username)">
+ <gl-avatar :alt="name" :size="32" :src="avatarUrl" />
+ <span class="gl-display-flex gl-flex-direction-column gl-flex-grow-1">
+ <span class="gl-font-weight-bold">{{ name }}</span>
+ <span class="gl-text-gray-600">@{{ username }}</span>
+ </span>
+
+ <gl-button
+ v-if="canDelete"
+ icon="remove"
+ :aria-label="deleteButtonLabel"
+ category="tertiary"
+ @click="$emit('delete', username)"
+ />
+ </span>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue
index 741bdfd211b..cc3c95a047b 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -492,7 +492,7 @@ export default {
tracking-property="quickAction"
/>
<comment-templates-dropdown
- v-if="!previewMarkdown && newCommentTemplatePath && glFeatures.savedReplies"
+ v-if="!previewMarkdown && newCommentTemplatePath"
:new-comment-template-path="newCommentTemplatePath"
@select="insertSavedReply"
/>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
index 4a3c3cf0053..73c030b23dc 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
@@ -190,7 +190,7 @@ export default {
renderMarkdown(markdown) {
const url = setUrlParams(
{ render_quick_actions: this.supportsQuickActions },
- joinPaths(gon.relative_url_root || window.location.origin, this.renderMarkdownPath),
+ joinPaths(window.location.origin, gon.relative_url_root, this.renderMarkdownPath),
);
return axios.post(url, { text: markdown }).then(({ data }) => data.body);
},
diff --git a/app/assets/javascripts/vue_shared/components/markdown_drawer/utils/fetch.js b/app/assets/javascripts/vue_shared/components/markdown_drawer/utils/fetch.js
index 27237f2f16b..6d74c1d083a 100644
--- a/app/assets/javascripts/vue_shared/components/markdown_drawer/utils/fetch.js
+++ b/app/assets/javascripts/vue_shared/components/markdown_drawer/utils/fetch.js
@@ -1,4 +1,4 @@
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { helpPagePath } from '~/helpers/help_page_helper';
import axios from '~/lib/utils/axios_utils';
diff --git a/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue b/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue
index 0ec8b6e2a0a..3bee539688b 100644
--- a/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue
@@ -64,7 +64,7 @@ export default {
});
},
lockedContextText() {
- return sprintf(__('This %{noteableTypeText} is locked.'), {
+ return sprintf(__('The discussion in this %{noteableTypeText} is locked.'), {
noteableTypeText: this.noteableTypeText,
});
},
@@ -80,7 +80,7 @@ export default {
<gl-sprintf
:message="
__(
- 'This %{noteableTypeText} is %{confidentialLinkStart}confidential%{confidentialLinkEnd} and %{lockedLinkStart}locked%{lockedLinkEnd}.',
+ 'This %{noteableTypeText} is %{confidentialLinkStart}confidential%{confidentialLinkEnd} and its %{lockedLinkStart}discussion is locked%{lockedLinkEnd}.',
)
"
>
diff --git a/app/assets/javascripts/vue_shared/components/notes/system_note.vue b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
index 81cbbf951ad..6a5884e4857 100644
--- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
@@ -30,12 +30,10 @@ import { renderGFM } from '~/behaviors/markdown/render_gfm';
import TimelineEntryItem from './timeline_entry_item.vue';
const MAX_VISIBLE_COMMIT_LIST_COUNT = 3;
-const MR_ICON_COLORS = {
+const ICON_COLORS = {
check: 'gl-bg-green-100 gl-text-green-700',
'merge-request-close': 'gl-bg-red-100 gl-text-red-700',
merge: 'gl-bg-blue-100 gl-text-blue-700',
-};
-const ICON_COLORS = {
'issue-close': 'gl-bg-blue-100 gl-text-blue-700',
};
@@ -76,6 +74,9 @@ export default {
noteAnchorId() {
return `note_${this.note.id}`;
},
+ isAllowedIcon() {
+ return Object.keys(ICON_COLORS).includes(this.note.system_note_icon_name);
+ },
isTargetNote() {
return this.targetNoteHash === this.noteAnchorId;
},
@@ -95,15 +96,8 @@ export default {
isMergeRequest() {
return this.getNoteableData.noteableType === 'MergeRequest';
},
- hasIconColors() {
- if (!this.isMergeRequest) return true;
-
- return this.isMergeRequest && MR_ICON_COLORS[this.note.system_note_icon_name];
- },
iconBgClass() {
- const colors = this.isMergeRequest ? MR_ICON_COLORS : ICON_COLORS;
-
- return colors[this.note.system_note_icon_name] || 'gl-bg-gray-50 gl-text-gray-600';
+ return ICON_COLORS[this.note.system_note_icon_name] || 'gl-bg-gray-50 gl-text-gray-600';
},
},
mounted() {
@@ -140,17 +134,16 @@ export default {
:class="[
iconBgClass,
{
- 'mr-system-note-empty gl-bg-gray-900!': !hasIconColors,
- 'gl-w-6 gl-h-6 gl-mt-n1 gl-ml-2': !isMergeRequest,
- 'mr-system-note-icon': isMergeRequest,
+ 'system-note-icon': isAllowedIcon,
+ 'system-note-tiny-dot gl-bg-gray-900!': !isAllowedIcon,
},
]"
class="gl-float-left gl--flex-center gl-rounded-full gl-relative timeline-icon"
>
<gl-icon
- v-if="note.system_note_icon_name && hasIconColors"
+ v-if="isAllowedIcon"
:name="note.system_note_icon_name"
- :size="isMergeRequest ? 12 : 16"
+ :size="12"
data-testid="timeline-icon"
/>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/components/blame_info.vue b/app/assets/javascripts/vue_shared/components/source_viewer/components/blame_info.vue
index 9bce9402afa..e2fd4477f0a 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/components/blame_info.vue
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/components/blame_info.vue
@@ -2,7 +2,6 @@
import { GlTooltipDirective } from '@gitlab/ui';
import SafeHtml from '~/vue_shared/directives/safe_html';
import CommitInfo from '~/repository/components/commit_info.vue';
-import { calculateBlameOffset, toggleBlameClasses } from '../utils';
export default {
name: 'BlameInfo',
@@ -14,25 +13,11 @@ export default {
SafeHtml,
},
props: {
- blameData: {
+ blameInfo: {
type: Array,
required: true,
},
},
- computed: {
- blameInfo() {
- return this.blameData.map((blame, index) => ({
- ...blame,
- blameOffset: calculateBlameOffset(blame.lineno, index),
- }));
- },
- },
- mounted() {
- toggleBlameClasses(this.blameData, true);
- },
- destroyed() {
- toggleBlameClasses(this.blameData, false);
- },
};
</script>
<template>
@@ -41,10 +26,11 @@ export default {
<commit-info
v-for="(blame, index) in blameInfo"
:key="index"
- :class="{ 'gl-border-t': index !== 0 }"
+ :class="{ 'gl-border-t': blame.blameOffset !== '0px' }"
class="gl-display-flex gl-absolute gl-px-3"
:style="{ top: blame.blameOffset }"
:commit="blame.commit"
+ :prev-blame-link="blame.commitData && blame.commitData.projectBlameLink"
/>
</div>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_new.vue b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_new.vue
index 8dac6327a99..3b6dcace8fe 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_new.vue
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_new.vue
@@ -56,7 +56,6 @@ export default {
data() {
return {
hasAppeared: false,
- isLoading: true,
};
},
computed: {
@@ -68,17 +67,6 @@ export default {
return getPageSearchString(this.blamePath, page);
},
},
- created() {
- if (this.chunkIndex === 0) {
- // Display first chunk ASAP in order to improve perceived performance
- this.isLoading = false;
- return;
- }
-
- window.requestIdleCallback(() => {
- this.isLoading = false;
- });
- },
methods: {
handleChunkAppear() {
this.hasAppeared = true;
@@ -91,37 +79,37 @@ export default {
};
</script>
<template>
- <gl-intersection-observer @appear="handleChunkAppear">
- <div class="gl-display-flex">
- <div v-if="shouldHighlight" class="gl-display-flex gl-flex-direction-column">
- <div
- v-for="(n, index) in totalLines"
- :key="index"
- data-testid="line-numbers"
- class="gl-p-0! gl-z-index-3 diff-line-num gl-border-r gl-display-flex line-links line-numbers"
+ <div class="gl-display-flex">
+ <div v-if="shouldHighlight" class="gl-display-flex gl-flex-direction-column">
+ <div
+ v-for="(n, index) in totalLines"
+ :key="index"
+ data-testid="line-numbers"
+ class="gl-p-0! gl-z-index-3 diff-line-num gl-border-r gl-display-flex line-links line-numbers"
+ >
+ <a
+ class="gl-user-select-none gl-shadow-none! file-line-blame"
+ :href="`${blamePath}${pageSearchString}#L${calculateLineNumber(index)}`"
+ ></a>
+ <a
+ :id="`L${calculateLineNumber(index)}`"
+ class="gl-user-select-none gl-shadow-none! file-line-num"
+ :href="`#L${calculateLineNumber(index)}`"
+ :data-line-number="calculateLineNumber(index)"
>
- <a
- class="gl-user-select-none gl-shadow-none! file-line-blame"
- :href="`${blamePath}${pageSearchString}#L${calculateLineNumber(index)}`"
- ></a>
- <a
- :id="`L${calculateLineNumber(index)}`"
- class="gl-user-select-none gl-shadow-none! file-line-num"
- :href="`#L${calculateLineNumber(index)}`"
- :data-line-number="calculateLineNumber(index)"
- >
- {{ calculateLineNumber(index) }}
- </a>
- </div>
+ {{ calculateLineNumber(index) }}
+ </a>
</div>
+ </div>
- <div v-else-if="!isLoading" class="line-numbers gl-p-0! gl-mr-3 gl-text-transparent">
- <!-- Placeholder for line numbers while content is not highlighted -->
- </div>
+ <div v-else class="line-numbers gl-p-0! gl-mr-3 gl-text-transparent">
+ <!-- Placeholder for line numbers while content is not highlighted -->
+ </div>
+ <gl-intersection-observer class="gl-w-full" @appear="handleChunkAppear">
<pre
class="gl-m-0 gl-p-0! gl-w-full gl-overflow-visible! gl-border-none! code highlight gl-line-height-0"
- ><code v-if="shouldHighlight" v-safe-html="highlightedContent" data-testid="content"></code><code v-else-if="!isLoading" v-once class="line gl-white-space-pre-wrap! gl-ml-1" data-testid="content" v-text="rawContent"></code></pre>
- </div>
- </gl-intersection-observer>
+ ><code v-if="shouldHighlight" v-safe-html="highlightedContent" data-testid="content"></code><code v-else v-once class="line gl-white-space-pre-wrap! gl-ml-1" data-testid="content" v-text="rawContent"></code></pre>
+ </gl-intersection-observer>
+ </div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/queries/blame_data.query.graphql b/app/assets/javascripts/vue_shared/components/source_viewer/queries/blame_data.query.graphql
new file mode 100644
index 00000000000..a5f3f348cfc
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/queries/blame_data.query.graphql
@@ -0,0 +1,36 @@
+#import "~/graphql_shared/fragments/author.fragment.graphql"
+
+query getBlameData($fullPath: ID!, $filePath: String!, $fromLine: Int, $toLine: Int) {
+ project(fullPath: $fullPath) {
+ id
+ repository {
+ blobs(paths: [$filePath]) {
+ nodes {
+ id
+ blame(fromLine: $fromLine, toLine: $toLine) {
+ firstLine
+ groups {
+ lineno
+ span
+ commit {
+ id
+ titleHtml
+ message
+ authoredDate
+ authorGravatar
+ webPath
+ author {
+ ...Author
+ }
+ sha
+ }
+ commitData {
+ projectBlameLink
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer_new.vue b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer_new.vue
index c7353ed6785..dcefa66c403 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer_new.vue
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer_new.vue
@@ -1,10 +1,15 @@
<script>
+import { debounce } from 'lodash';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import SafeHtml from '~/vue_shared/directives/safe_html';
import Tracking from '~/tracking';
import addBlobLinksTracking from '~/blob/blob_links_tracking';
import LineHighlighter from '~/blob/line_highlighter';
import { EVENT_ACTION, EVENT_LABEL_VIEWER } from './constants';
import Chunk from './components/chunk_new.vue';
+import Blame from './components/blame_info.vue';
+import { calculateBlameOffset, shouldRender, toggleBlameClasses } from './utils';
+import blameDataQuery from './queries/blame_data.query.graphql';
/*
* Note, this is a new experimental version of the SourceViewer, it is not ready for production use.
@@ -15,6 +20,7 @@ export default {
name: 'SourceViewerNew',
components: {
Chunk,
+ Blame,
},
directives: {
SafeHtml,
@@ -30,13 +36,55 @@ export default {
required: false,
default: () => [],
},
+ showBlame: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ projectPath: {
+ type: String,
+ required: true,
+ },
},
data() {
return {
lineHighlighter: new LineHighlighter(),
+ blameData: [],
+ renderedChunks: [],
};
},
+ computed: {
+ blameInfo() {
+ return this.blameData.reduce((result, blame, index) => {
+ if (shouldRender(this.blameData, index)) {
+ result.push({
+ ...blame,
+ blameOffset: calculateBlameOffset(blame.lineno, index),
+ });
+ }
+
+ return result;
+ }, []);
+ },
+ },
+ watch: {
+ showBlame: {
+ handler(shouldShow) {
+ toggleBlameClasses(this.blameData, shouldShow);
+ this.requestBlameInfo(this.renderedChunks[0]);
+ },
+ immediate: true,
+ },
+ blameData: {
+ handler(blameData) {
+ if (!this.showBlame) return;
+ toggleBlameClasses(blameData, true);
+ },
+ immediate: true,
+ },
+ },
created() {
+ this.handleAppear = debounce(this.handleChunkAppear, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
this.track(EVENT_ACTION, { label: EVENT_LABEL_VIEWER, property: this.blob.language });
addBlobLinksTracking();
},
@@ -44,10 +92,39 @@ export default {
this.selectLine();
},
methods: {
+ async handleChunkAppear(chunkIndex, handleOverlappingChunk = true) {
+ if (!this.renderedChunks.includes(chunkIndex)) {
+ this.renderedChunks.push(chunkIndex);
+ await this.requestBlameInfo(chunkIndex);
+
+ if (chunkIndex > 0 && handleOverlappingChunk) {
+ // request the blame information for overlapping chunk incase it is visible in the DOM
+ this.handleChunkAppear(chunkIndex - 1, false);
+ }
+ }
+ },
+ async requestBlameInfo(chunkIndex) {
+ const chunk = this.chunks[chunkIndex];
+ if (!this.showBlame || !chunk) return;
+
+ const { data } = await this.$apollo.query({
+ query: blameDataQuery,
+ variables: {
+ fullPath: this.projectPath,
+ filePath: this.blob.path,
+ fromLine: chunk.startingFrom + 1,
+ toLine: chunk.startingFrom + chunk.totalLines,
+ },
+ });
+
+ const blob = data?.project?.repository?.blobs?.nodes[0];
+ const blameGroups = blob?.blame?.groups;
+ const isDuplicate = this.blameData.includes(blameGroups[0]);
+ if (blameGroups && !isDuplicate) this.blameData.push(...blameGroups);
+ },
async selectLine() {
await this.$nextTick();
- const scrollEnabled = false;
- this.lineHighlighter.highlightHash(this.$route.hash, scrollEnabled);
+ this.lineHighlighter.highlightHash(this.$route.hash);
},
},
userColorScheme: window.gon.user_color_scheme,
@@ -55,24 +132,27 @@ export default {
</script>
<template>
- <div
- class="file-content code js-syntax-highlight blob-content gl-display-flex gl-flex-direction-column gl-overflow-auto"
- :class="$options.userColorScheme"
- data-type="simple"
- :data-path="blob.path"
- data-qa-selector="blob_viewer_file_content"
- >
- <chunk
- v-for="(chunk, _, index) in chunks"
- :key="index"
- :chunk-index="index"
- :is-highlighted="Boolean(chunk.isHighlighted)"
- :raw-content="chunk.rawContent"
- :highlighted-content="chunk.highlightedContent"
- :total-lines="chunk.totalLines"
- :starting-from="chunk.startingFrom"
- :blame-path="blob.blamePath"
- @appear="selectLine"
- />
+ <div class="gl-display-flex">
+ <blame v-if="showBlame && blameInfo.length" :blame-info="blameInfo" />
+
+ <div
+ class="file-content code js-syntax-highlight blob-content gl-display-flex gl-flex-direction-column gl-overflow-auto gl-w-full"
+ :class="$options.userColorScheme"
+ data-type="simple"
+ :data-path="blob.path"
+ >
+ <chunk
+ v-for="(chunk, index) in chunks"
+ :key="index"
+ :chunk-index="index"
+ :is-highlighted="Boolean(chunk.isHighlighted)"
+ :raw-content="chunk.rawContent"
+ :highlighted-content="chunk.highlightedContent"
+ :total-lines="chunk.totalLines"
+ :starting-from="chunk.startingFrom"
+ :blame-path="blob.blamePath"
+ @appear="() => handleAppear(index)"
+ />
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/utils.js b/app/assets/javascripts/vue_shared/components/source_viewer/utils.js
index af01653fc0d..596829b51a4 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/utils.js
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/utils.js
@@ -1,6 +1,7 @@
const BLAME_INFO_CLASSLIST = ['gl-border-t', 'gl-border-gray-500', 'gl-pt-3!'];
const PADDING_BOTTOM_LARGE = 'gl-pb-6!';
const PADDING_BOTTOM_SMALL = 'gl-pb-3!';
+const VIEWER_SELECTOR = '.file-holder .blob-viewer';
const findLineNumberElement = (lineNumber) => document.getElementById(`L${lineNumber}`);
@@ -8,8 +9,18 @@ const findLineContentElement = (lineNumber) => document.getElementById(`LC${line
export const calculateBlameOffset = (lineNumber) => {
if (lineNumber === 1) return '0px';
- const lineContentOffset = findLineContentElement(lineNumber)?.offsetTop;
- return `${lineContentOffset}px`;
+ const blobViewerOffset = document.querySelector(VIEWER_SELECTOR)?.getBoundingClientRect().top;
+ const lineContentOffset = findLineContentElement(lineNumber)?.getBoundingClientRect().top;
+ return `${lineContentOffset - blobViewerOffset}px`;
+};
+
+export const shouldRender = (data, index) => {
+ const prevBlame = data[index - 1];
+ const currBlame = data[index];
+ const identicalSha = currBlame.commit.sha === prevBlame?.commit?.sha;
+ const lineNumberSmaller = currBlame.lineno < prevBlame?.lineno;
+
+ return !identicalSha || lineNumberSmaller;
};
export const toggleBlameClasses = (blameData, isVisible) => {
@@ -17,7 +28,9 @@ export const toggleBlameClasses = (blameData, isVisible) => {
* Adds/removes classes to line number/content elements to match the line with the blame info
* */
const method = isVisible ? 'add' : 'remove';
- blameData.forEach(({ lineno, span }) => {
+ blameData.forEach(({ lineno, span }, index) => {
+ if (!shouldRender(blameData, index)) return;
+
const lineNumberEl = findLineNumberElement(lineno)?.parentElement;
const lineContentEl = findLineContentElement(lineno);
const lineNumberSpanEl = findLineNumberElement(lineno + span - 1)?.parentElement;
diff --git a/app/assets/javascripts/vue_shared/components/toggle_labels.vue b/app/assets/javascripts/vue_shared/components/toggle_labels.vue
index 05c837e32f0..db20e1288aa 100644
--- a/app/assets/javascripts/vue_shared/components/toggle_labels.vue
+++ b/app/assets/javascripts/vue_shared/components/toggle_labels.vue
@@ -54,7 +54,6 @@ export default {
label-position="left"
aria-describedby="board-labels-toggle-text"
data-testid="show-labels-toggle"
- data-qa-selector="show_labels_toggle"
class="gl-flex-direction-row"
@change="setShowLabels"
/>
diff --git a/app/assets/javascripts/vue_shared/components/users_table/constants.js b/app/assets/javascripts/vue_shared/components/users_table/constants.js
new file mode 100644
index 00000000000..2a063a1be33
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/users_table/constants.js
@@ -0,0 +1,3 @@
+export const USER_AVATAR_SIZE = 32;
+
+export const LENGTH_OF_USER_NOTE_TOOLTIP = 100;
diff --git a/app/assets/javascripts/vue_shared/components/users_table/user_avatar.vue b/app/assets/javascripts/vue_shared/components/users_table/user_avatar.vue
new file mode 100644
index 00000000000..5d86f90880d
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/users_table/user_avatar.vue
@@ -0,0 +1,76 @@
+<script>
+import { GlAvatarLabeled, GlBadge, GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { truncate } from '~/lib/utils/text_utility';
+import { USER_AVATAR_SIZE, LENGTH_OF_USER_NOTE_TOOLTIP } from './constants';
+
+export default {
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ components: {
+ GlAvatarLabeled,
+ GlBadge,
+ GlIcon,
+ },
+ props: {
+ user: {
+ type: Object,
+ required: true,
+ },
+ adminUserPath: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ subLabel() {
+ if (this.user.email) {
+ return {
+ label: this.user.email,
+ link: `mailto:${this.user.email}`,
+ };
+ }
+
+ return {
+ label: `@${this.user.username}`,
+ };
+ },
+ adminUserHref() {
+ return this.adminUserPath.replace('id', this.user.username);
+ },
+ userNoteShort() {
+ return truncate(this.user.note, LENGTH_OF_USER_NOTE_TOOLTIP);
+ },
+ },
+ USER_AVATAR_SIZE,
+};
+</script>
+
+<template>
+ <div
+ v-if="user"
+ class="js-user-link gl-display-inline-block"
+ :data-user-id="user.id"
+ :data-username="user.username"
+ >
+ <gl-avatar-labeled
+ :size="$options.USER_AVATAR_SIZE"
+ :src="user.avatarUrl"
+ :label="user.name"
+ :sub-label="subLabel.label"
+ :label-link="adminUserHref"
+ :sub-label-link="subLabel.link"
+ >
+ <template #meta>
+ <div v-if="user.note" class="gl-text-gray-500 gl-p-1">
+ <gl-icon v-gl-tooltip="userNoteShort" name="document" />
+ </div>
+ <div v-for="(badge, idx) in user.badges" :key="idx" class="gl-p-1">
+ <gl-badge class="gl-display-flex!" size="sm" :variant="badge.variant">{{
+ badge.text
+ }}</gl-badge>
+ </div>
+ </template>
+ </gl-avatar-labeled>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/users_table/users_table.vue b/app/assets/javascripts/vue_shared/components/users_table/users_table.vue
new file mode 100644
index 00000000000..be164bb07a3
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/users_table/users_table.vue
@@ -0,0 +1,110 @@
+<script>
+import { GlSkeletonLoader, GlTable } from '@gitlab/ui';
+import { thWidthPercent } from '~/lib/utils/table_utility';
+import { __ } from '~/locale';
+import UserDate from '~/vue_shared/components/user_date.vue';
+import UserAvatar from './user_avatar.vue';
+
+export default {
+ components: {
+ GlSkeletonLoader,
+ GlTable,
+ UserAvatar,
+ UserDate,
+ },
+ props: {
+ users: {
+ type: Array,
+ required: true,
+ },
+ adminUserPath: {
+ type: String,
+ required: true,
+ },
+ groupCounts: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ groupCountsLoading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ fields: [
+ {
+ key: 'name',
+ label: __('Name'),
+ thClass: thWidthPercent(40),
+ },
+ {
+ key: 'projectsCount',
+ label: __('Projects'),
+ thClass: thWidthPercent(10),
+ },
+ {
+ key: 'groupCount',
+ label: __('Groups'),
+ thClass: thWidthPercent(10),
+ },
+ {
+ key: 'createdAt',
+ label: __('Created on'),
+ thClass: thWidthPercent(15),
+ },
+ {
+ key: 'lastActivityOn',
+ label: __('Last activity'),
+ thClass: thWidthPercent(15),
+ },
+ {
+ key: 'settings',
+ label: '',
+ thClass: thWidthPercent(10),
+ },
+ ],
+};
+</script>
+
+<template>
+ <div>
+ <gl-table
+ :items="users"
+ :fields="$options.fields"
+ :empty-text="s__('AdminUsers|No users found')"
+ show-empty
+ stacked="md"
+ :tbody-tr-attr="{ 'data-testid': 'user-row-content' }"
+ >
+ <template #cell(name)="{ item: user }">
+ <user-avatar :user="user" :admin-user-path="adminUserPath" />
+ </template>
+
+ <template #cell(createdAt)="{ item: { createdAt } }">
+ <user-date :date="createdAt" />
+ </template>
+
+ <template #cell(lastActivityOn)="{ item: { lastActivityOn } }">
+ <user-date :date="lastActivityOn" show-never />
+ </template>
+
+ <template #cell(groupCount)="{ item: { id } }">
+ <div :data-testid="`user-group-count-${id}`">
+ <gl-skeleton-loader v-if="groupCountsLoading" :width="40" :lines="1" />
+ <span v-else>{{ groupCounts[id] || 0 }}</span>
+ </div>
+ </template>
+
+ <template #cell(projectsCount)="{ item: { id, projectsCount } }">
+ <div :data-testid="`user-project-count-${id}`">
+ {{ projectsCount || 0 }}
+ </div>
+ </template>
+
+ <template #cell(settings)="{ item: user }">
+ <slot name="user-actions" :user="user"></slot>
+ </template>
+ </gl-table>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/web_ide_link.vue b/app/assets/javascripts/vue_shared/components/web_ide_link.vue
index 9fb0add5522..441b4c31b3a 100644
--- a/app/assets/javascripts/vue_shared/components/web_ide_link.vue
+++ b/app/assets/javascripts/vue_shared/components/web_ide_link.vue
@@ -335,7 +335,7 @@ export default {
:variant="isBlob ? 'confirm' : 'default'"
:category="isBlob ? 'primary' : 'secondary'"
:toggle-text="$options.i18n.toggleText"
- data-qa-selector="action_dropdown"
+ data-testid="action-dropdown"
fluid-width
block
@shown="$emit('shown')"
@@ -347,7 +347,7 @@ export default {
v-for="action in actions"
:key="action.key"
:item="action"
- :data-qa-selector="`${action.key}_menu_item`"
+ :data-testid="`${action.key}-menu-item`"
@action="executeAction(action)"
>
<template #list-item>
diff --git a/app/assets/javascripts/vue_shared/directives/safe_html.js b/app/assets/javascripts/vue_shared/directives/safe_html.js
index 450c7fc1bc5..c731f742771 100644
--- a/app/assets/javascripts/vue_shared/directives/safe_html.js
+++ b/app/assets/javascripts/vue_shared/directives/safe_html.js
@@ -11,7 +11,7 @@ const DEFAULT_CONFIG = {
const transform = (el, binding) => {
if (binding.oldValue !== binding.value) {
- const config = { ...DEFAULT_CONFIG, ...(binding.arg ?? {}) };
+ const config = { ...DEFAULT_CONFIG, ...binding.arg };
el.textContent = '';
diff --git a/app/assets/javascripts/vue_shared/gl_feature_flags_plugin.js b/app/assets/javascripts/vue_shared/gl_feature_flags_plugin.js
index 79946ebaecd..a1abb079cc2 100644
--- a/app/assets/javascripts/vue_shared/gl_feature_flags_plugin.js
+++ b/app/assets/javascripts/vue_shared/gl_feature_flags_plugin.js
@@ -2,12 +2,11 @@ export default (Vue) => {
Vue.mixin({
provide() {
return {
- glFeatures:
- {
- ...window.gon?.features,
- // TODO: extract into glLicensedFeatures https://gitlab.com/gitlab-org/gitlab/-/issues/322460
- ...window.gon?.licensed_features,
- } || {},
+ glFeatures: {
+ ...window.gon?.features,
+ // TODO: extract into glLicensedFeatures https://gitlab.com/gitlab-org/gitlab/-/issues/322460
+ ...window.gon?.licensed_features,
+ },
};
},
});
diff --git a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_body.vue b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_body.vue
index 45fde45f516..dae3ddfe016 100644
--- a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_body.vue
+++ b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_body.vue
@@ -74,6 +74,11 @@ export default {
required: false,
default: 0,
},
+ workspaceType: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
computed: {
isUpdated() {
@@ -161,6 +166,7 @@ export default {
:issuable="issuable"
:status-icon="statusIcon"
:enable-edit="enableEdit"
+ :workspace-type="workspaceType"
@edit-issuable="$emit('edit-issuable', $event)"
>
<template #status-badge>
diff --git a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue
index a9b5e3a66a8..62a2b44e660 100644
--- a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue
+++ b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue
@@ -221,7 +221,7 @@ export default {
@click="handleRightSidebarToggleClick"
/>
</div>
- <div class="detail-page-header-actions gl-display-flex">
+ <div class="detail-page-header-actions gl-align-self-center gl-display-flex">
<slot name="header-actions"></slot>
</div>
</div>
diff --git a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_show_root.vue b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_show_root.vue
index 3878c16c8d0..040f49c7c25 100644
--- a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_show_root.vue
+++ b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_show_root.vue
@@ -147,6 +147,7 @@ export default {
:description-help-path="descriptionHelpPath"
:task-list-update-path="taskListUpdatePath"
:task-list-lock-version="taskListLockVersion"
+ :workspace-type="workspaceType"
@edit-issuable="$emit('edit-issuable', $event)"
@task-list-update-success="$emit('task-list-update-success', $event)"
@task-list-update-failure="$emit('task-list-update-failure')"
diff --git a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue
index da71adc8abd..5387e39e3eb 100644
--- a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue
+++ b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue
@@ -1,5 +1,6 @@
<script>
import { GlIcon, GlBadge, GlButton, GlIntersectionObserver, GlTooltipDirective } from '@gitlab/ui';
+import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
import SafeHtml from '~/vue_shared/directives/safe_html';
import { STATUS_OPEN } from '~/issues/constants';
import { __ } from '~/locale';
@@ -13,6 +14,7 @@ export default {
GlBadge,
GlButton,
GlIntersectionObserver,
+ ConfidentialityBadge,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -31,6 +33,11 @@ export default {
type: Boolean,
required: true,
},
+ workspaceType: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
data() {
return {
@@ -79,9 +86,7 @@ export default {
class="issue-sticky-header gl-fixed gl-z-index-3 gl-bg-white gl-border-1 gl-border-b-solid gl-border-b-gray-100 gl-py-3"
data-testid="header"
>
- <div
- class="issue-sticky-header-text gl-display-flex gl-align-items-baseline gl-mx-auto gl-px-5"
- >
+ <div class="issue-sticky-header-text gl-display-flex gl-align-items-baseline gl-mx-auto">
<gl-badge
class="gl-white-space-nowrap gl-mr-3 gl-align-self-center"
:variant="badgeVariant"
@@ -91,6 +96,12 @@ export default {
<slot name="status-badge"></slot>
</span>
</gl-badge>
+ <confidentiality-badge
+ v-if="issuable.confidential"
+ class="gl-white-space-nowrap gl-mr-3 gl-align-self-center"
+ :issuable-type="issuable.type"
+ :workspace-type="workspaceType"
+ />
<p
class="gl-font-weight-bold gl-overflow-hidden gl-white-space-nowrap gl-text-overflow-ellipsis gl-my-0"
:title="issuable.title"
diff --git a/app/assets/javascripts/webhooks/components/push_events.vue b/app/assets/javascripts/webhooks/components/push_events.vue
index 91d7e21500a..b5e0a4b2348 100644
--- a/app/assets/javascripts/webhooks/components/push_events.vue
+++ b/app/assets/javascripts/webhooks/components/push_events.vue
@@ -43,7 +43,7 @@ export default {
value="all_branches"
data-testid="rule_all_branches"
>
- <div data-qa-selector="strategy_radio_all">{{ __('All branches') }}</div>
+ <div>{{ __('All branches') }}</div>
</gl-form-radio>
<!-- wildcard -->
@@ -52,7 +52,7 @@ export default {
value="wildcard"
data-testid="rule_wildcard"
>
- <div data-qa-selector="strategy_radio_wildcard">
+ <div>
{{ s__('Webhooks|Wildcard pattern') }}
</div>
</gl-form-radio>
@@ -61,7 +61,6 @@ export default {
v-if="branchFilterStrategyData === 'wildcard'"
v-model="pushEventsBranchFilterData"
name="hook[push_events_branch_filter]"
- data-qa-selector="webhook_branch_filter_field"
data-testid="webhook_branch_filter_field"
/>
</div>
@@ -85,7 +84,7 @@ export default {
value="regex"
data-testid="rule_regex"
>
- <div data-qa-selector="strategy_radio_regex">
+ <div>
{{ s__('Webhooks|Regular expression') }}
</div>
</gl-form-radio>
@@ -94,7 +93,6 @@ export default {
v-if="branchFilterStrategyData === 'regex'"
v-model="pushEventsBranchFilterData"
name="hook[push_events_branch_filter]"
- data-qa-selector="webhook_branch_filter_field"
data-testid="webhook_branch_filter_field"
/>
</div>
diff --git a/app/assets/javascripts/work_items/components/notes/system_note.vue b/app/assets/javascripts/work_items/components/notes/system_note.vue
index 7903adea9bd..31cfe387b6e 100644
--- a/app/assets/javascripts/work_items/components/notes/system_note.vue
+++ b/app/assets/javascripts/work_items/components/notes/system_note.vue
@@ -26,6 +26,11 @@ import { __ } from '~/locale';
import NoteHeader from '~/notes/components/note_header.vue';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
+const ALLOWED_ICONS = ['issue-close'];
+const ICON_COLORS = {
+ 'issue-close': 'gl-bg-blue-100! gl-text-blue-700',
+};
+
export default {
i18n: {
deleteButtonLabel: __('Remove description history'),
@@ -66,6 +71,12 @@ export default {
noteAnchorId() {
return `note_${this.noteId}`;
},
+ getIconColor() {
+ return ICON_COLORS[this.note.systemNoteIconName] || '';
+ },
+ isAllowedIcon() {
+ return ALLOWED_ICONS.includes(this.note.systemNoteIconName);
+ },
isTargetNote() {
return this.targetNoteHash === this.noteAnchorId;
},
@@ -102,9 +113,16 @@ export default {
class="note system-note note-wrapper"
>
<div
- class="gl-float-left gl--flex-center gl-rounded-full gl-mt-n1 gl-ml-2 gl-w-6 gl-h-6 gl-bg-gray-50 gl-text-gray-600"
+ :class="[
+ getIconColor,
+ {
+ 'gl-bg-gray-50 gl-text-gray-600 system-note-icon': isAllowedIcon,
+ 'system-note-tiny-dot gl-bg-gray-900!': !isAllowedIcon,
+ },
+ ]"
+ class="gl-float-left gl--flex-center gl-rounded-full gl-relative"
>
- <gl-icon :name="note.systemNoteIconName" />
+ <gl-icon v-if="isAllowedIcon" :size="12" :name="note.systemNoteIconName" />
</div>
<div class="timeline-content">
<div class="note-header">
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue b/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue
index c867e53dc30..c3b7b7a2953 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue
@@ -1,5 +1,5 @@
<script>
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import Tracking from '~/tracking';
import { ASC } from '~/notes/constants';
import { __ } from '~/locale';
@@ -105,7 +105,7 @@ export default {
};
},
update(data) {
- return data.workspace.workItems.nodes[0];
+ return data.workspace.workItems.nodes[0] ?? {};
},
skip() {
return !this.workItemIid;
@@ -150,13 +150,13 @@ export default {
};
},
isProjectArchived() {
- return this.workItem?.project?.archived;
+ return this.workItem.archived;
},
canCreateNote() {
- return this.workItem?.userPermissions?.createNote;
+ return this.workItem.userPermissions?.createNote;
},
workItemState() {
- return this.workItem?.state;
+ return this.workItem.state;
},
commentButtonText() {
return this.isNewDiscussion ? __('Comment') : __('Reply');
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue b/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue
index c7d8a50f402..1e6bd9ff1ac 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue
@@ -8,7 +8,7 @@ import { STATE_OPEN, TRACKING_CATEGORY_SHOW, TASK_TYPE_NAME } from '~/work_items
import { getDraft, clearDraft, updateDraft } from '~/lib/utils/autosave';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
-import WorkItemStateToggleButton from '~/work_items/components/work_item_state_toggle_button.vue';
+import WorkItemStateToggle from '~/work_items/components/work_item_state_toggle.vue';
import CommentFieldLayout from '~/notes/components/comment_field_layout.vue';
export default {
@@ -29,7 +29,7 @@ export default {
MarkdownEditor,
GlFormCheckbox,
GlIcon,
- WorkItemStateToggleButton,
+ WorkItemStateToggle,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -195,7 +195,6 @@ export default {
:autocomplete-data-sources="autocompleteDataSources"
:form-field-props="formFieldProps"
:add-spacing-classes="false"
- data-testid="work-item-add-comment"
use-bottom-toolbar
supports-quick-actions
:autofocus="autofocus"
@@ -230,7 +229,7 @@ export default {
@click="$emit('submitForm', { commentText, isNoteInternal })"
>{{ commentButtonTextComputed }}
</gl-button>
- <work-item-state-toggle-button
+ <work-item-state-toggle
v-if="isNewDiscussion"
class="gl-ml-3"
:work-item-id="workItemId"
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_note.vue b/app/assets/javascripts/work_items/components/notes/work_item_note.vue
index f4c654f054c..11aecc65803 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_note.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_note.vue
@@ -1,6 +1,6 @@
<script>
import { GlAvatarLink, GlAvatar } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import toast from '~/vue_shared/plugins/global_toast';
import { __ } from '~/locale';
import Tracking from '~/tracking';
@@ -96,6 +96,7 @@ export default {
data() {
return {
isEditing: false,
+ workItem: {},
};
},
computed: {
@@ -163,13 +164,13 @@ export default {
return this.authorId === this.currentUserId;
},
isWorkItemAuthor() {
- return getIdFromGraphQLId(this.workItem?.author?.id) === this.authorId;
+ return getIdFromGraphQLId(this.workItem.author?.id) === this.authorId;
},
projectName() {
- return this.workItem?.project?.name;
+ return this.workItem.namespace?.name;
},
isWorkItemConfidential() {
- return this.workItem?.confidential;
+ return this.workItem.confidential;
},
},
apollo: {
@@ -184,7 +185,7 @@ export default {
};
},
update(data) {
- return data.workspace?.workItems?.nodes[0];
+ return data.workspace?.workItems?.nodes[0] ?? {};
},
skip() {
return !this.workItemIid;
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue b/app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue
index 2cdf8b5ea9d..cb9a560f9e1 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue
@@ -5,7 +5,7 @@ import {
GlDisclosureDropdown,
GlDisclosureDropdownItem,
} from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { __, sprintf } from '~/locale';
import UserAccessRoleBadge from '~/vue_shared/components/user_access_role_badge.vue';
import ReplyButton from '~/notes/components/note_actions/reply_button.vue';
@@ -207,7 +207,6 @@ export default {
<gl-button
v-if="showEdit"
v-gl-tooltip
- data-testid="edit-work-item-note"
data-track-action="click_button"
data-track-label="edit_button"
category="tertiary"
@@ -219,7 +218,6 @@ export default {
<gl-disclosure-dropdown
ref="dropdown"
v-gl-tooltip
- data-testid="work-item-note-actions"
icon="ellipsis_v"
text-sr-only
placement="right"
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_note_awards_list.vue b/app/assets/javascripts/work_items/components/notes/work_item_note_awards_list.vue
index 17d22e66530..75a8a7b29c0 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_note_awards_list.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_note_awards_list.vue
@@ -1,5 +1,5 @@
<script>
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import AwardsList from '~/vue_shared/components/awards_list.vue';
import { getMutation, optimisticAwardUpdate } from '../../notes/award_utils';
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_note_signed_out.vue b/app/assets/javascripts/work_items/components/notes/work_item_note_signed_out.vue
index bccbec903b4..e073fddeddb 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_note_signed_out.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_note_signed_out.vue
@@ -27,5 +27,8 @@ export default {
</script>
<template>
- <div v-safe-html="signedOutText" class="disabled-comment gl-text-center gl-relative"></div>
+ <div
+ v-safe-html="signedOutText"
+ class="disabled-comment gl-text-center gl-text-secondary gl-relative"
+ ></div>
</template>
diff --git a/app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue b/app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue
index 49813edf6fc..cbe7de4abcd 100644
--- a/app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue
+++ b/app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue
@@ -1,6 +1,6 @@
<script>
-import { GlLabel, GlLink, GlIcon, GlTooltipDirective } from '@gitlab/ui';
-import { __ } from '~/locale';
+import { GlLabel, GlLink, GlIcon, GlTooltipDirective, GlButton } from '@gitlab/ui';
+import { __, s__ } from '~/locale';
import { isScopedLabel } from '~/lib/utils/common_utils';
import RichTimestampTooltip from '~/vue_shared/components/rich_timestamp_tooltip.vue';
import WorkItemLinkChildMetadata from 'ee_else_ce/work_items/components/shared/work_item_link_child_metadata.vue';
@@ -15,21 +15,21 @@ import {
WIDGET_TYPE_LABELS,
WORK_ITEM_NAME_TO_ICON_MAP,
} from '../../constants';
-import WorkItemLinksMenu from './work_item_links_menu.vue';
export default {
i18n: {
confidential: __('Confidential'),
created: __('Created'),
closed: __('Closed'),
+ remove: s__('WorkItem|Remove'),
},
components: {
GlLabel,
GlLink,
GlIcon,
+ GlButton,
RichTimestampTooltip,
WorkItemLinkChildMetadata,
- WorkItemLinksMenu,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -52,6 +52,16 @@ export default {
required: false,
default: false,
},
+ showLabels: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ },
+ data() {
+ return {
+ isFocused: false,
+ };
},
computed: {
labels() {
@@ -106,6 +116,12 @@ export default {
}
return false;
},
+ showRemove() {
+ return this.canUpdate && this.isFocused;
+ },
+ displayLabels() {
+ return this.showLabels && this.labels.length;
+ },
},
methods: {
showScopedLabel(label) {
@@ -117,8 +133,12 @@ export default {
<template>
<div
- class="item-body work-item-link-child gl-relative gl-display-flex gl-flex-grow-1 gl-overflow-break-word gl-min-w-0 gl-pl-3 gl-pr-2 gl-py-2 gl-mx-n2 gl-rounded-base"
+ class="item-body work-item-link-child gl-relative gl-display-flex gl-flex-grow-1 gl-overflow-break-word gl-min-w-0 gl-pl-3 gl-pr-2 gl-py-2 gl-mx-n2 gl-rounded-base gl-gap-3"
data-testid="links-child"
+ @mouseover="isFocused = true"
+ @mouseleave="isFocused = false"
+ @focusin="isFocused = true"
+ @focusout="isFocused = false"
>
<div class="item-contents gl-display-flex gl-flex-grow-1 gl-flex-wrap gl-min-w-0">
<div
@@ -168,7 +188,7 @@ export default {
class="gl-ml-6 ml-xl-0"
/>
</div>
- <div v-if="labels.length" class="gl-display-flex gl-flex-wrap gl-flex-basis-full gl-ml-6">
+ <div v-if="displayLabels" class="gl-display-flex gl-flex-wrap gl-flex-basis-full gl-ml-6">
<gl-label
v-for="label in labels"
:key="label.id"
@@ -181,10 +201,16 @@ export default {
/>
</div>
</div>
- <div v-if="canUpdate" class="gl-ml-0 gl-sm-ml-auto! gl-display-inline-flex">
- <work-item-links-menu
- data-testid="links-menu"
- @removeChild="$emit('removeChild', childItem)"
+ <div v-if="canUpdate">
+ <gl-button
+ :class="{ 'gl-visibility-visible': showRemove }"
+ class="gl-visibility-hidden"
+ category="tertiary"
+ size="small"
+ icon="close"
+ :aria-label="$options.i18n.remove"
+ data-testid="remove-work-item-link"
+ @click="$emit('removeChild', childItem)"
/>
</div>
</div>
diff --git a/app/assets/javascripts/work_items/components/shared/work_item_links_menu.vue b/app/assets/javascripts/work_items/components/shared/work_item_links_menu.vue
deleted file mode 100644
index 12b7bade31d..00000000000
--- a/app/assets/javascripts/work_items/components/shared/work_item_links_menu.vue
+++ /dev/null
@@ -1,28 +0,0 @@
-<script>
-import { GlDisclosureDropdown, GlDisclosureDropdownItem } from '@gitlab/ui';
-
-export default {
- components: {
- GlDisclosureDropdownItem,
- GlDisclosureDropdown,
- },
-};
-</script>
-
-<template>
- <div class="gl-ml-5">
- <gl-disclosure-dropdown
- category="tertiary"
- toggle-class="btn-icon btn-sm"
- icon="ellipsis_v"
- data-testid="work_items_links_menu"
- :aria-label="__(`More actions`)"
- text-sr-only
- no-caret
- >
- <gl-disclosure-dropdown-item @action="$emit('removeChild')">
- <template #list-item>{{ s__('WorkItem|Remove') }}</template>
- </gl-disclosure-dropdown-item>
- </gl-disclosure-dropdown>
- </div>
-</template>
diff --git a/app/assets/javascripts/work_items/components/shared/work_item_token_input.vue b/app/assets/javascripts/work_items/components/shared/work_item_token_input.vue
index 3595ab631df..c122db6c902 100644
--- a/app/assets/javascripts/work_items/components/shared/work_item_token_input.vue
+++ b/app/assets/javascripts/work_items/components/shared/work_item_token_input.vue
@@ -1,20 +1,29 @@
<script>
-import { GlTokenSelector } from '@gitlab/ui';
+import { GlTokenSelector, GlAlert } from '@gitlab/ui';
import { debounce } from 'lodash';
+
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { isNumeric } from '~/lib/utils/number_utils';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+import SafeHtml from '~/vue_shared/directives/safe_html';
+import { highlighter } from 'ee_else_ce/gfm_auto_complete';
+import groupWorkItemsQuery from '../../graphql/group_work_items.query.graphql';
import projectWorkItemsQuery from '../../graphql/project_work_items.query.graphql';
import {
WORK_ITEMS_TYPE_MAP,
I18N_WORK_ITEM_SEARCH_INPUT_PLACEHOLDER,
+ I18N_WORK_ITEM_SEARCH_ERROR,
sprintfWorkItem,
} from '../../constants';
export default {
components: {
GlTokenSelector,
+ GlAlert,
},
+ directives: { SafeHtml },
+ inject: ['isGroup'],
props: {
value: {
type: Array,
@@ -47,30 +56,37 @@ export default {
},
apollo: {
availableWorkItems: {
- query: projectWorkItemsQuery,
+ query() {
+ return this.isGroup ? groupWorkItemsQuery : projectWorkItemsQuery;
+ },
variables() {
return {
fullPath: this.fullPath,
- searchTerm: this.search?.title || this.search,
+ searchTerm: '',
types: this.childrenType ? [this.childrenType] : [],
- in: this.search ? 'TITLE' : undefined,
+ isNumber: false,
};
},
skip() {
return !this.searchStarted;
},
update(data) {
- return data.workspace.workItems.nodes.filter(
- (wi) => !this.childrenIds.includes(wi.id) && this.parentWorkItemId !== wi.id,
- );
+ return [
+ ...this.filterItems(data.workspace.workItemsByIid?.nodes),
+ ...this.filterItems(data.workspace.workItems.nodes),
+ ];
+ },
+ error() {
+ this.error = sprintfWorkItem(I18N_WORK_ITEM_SEARCH_ERROR, this.childrenTypeName);
},
},
},
data() {
return {
availableWorkItems: [],
- search: '',
+ query: '',
searchStarted: false,
+ error: '',
};
},
computed: {
@@ -101,7 +117,24 @@ export default {
methods: {
getIdFromGraphQLId,
setSearchKey(value) {
- this.search = value;
+ this.query = value;
+
+ // Query parameters for searching by text
+ const variables = {
+ searchTerm: value,
+ in: value ? 'TITLE' : undefined,
+ iid: null,
+ isNumber: false,
+ };
+
+ // Check if it is a number, add iid as query parameter
+ if (isNumeric(value) && value) {
+ variables.iid = value;
+ variables.isNumber = true;
+ }
+
+ // Fetch combined results of search by iid and search by title.
+ this.$apollo.queries.availableWorkItems.refetch(variables);
},
handleFocus() {
this.searchStarted = true;
@@ -125,33 +158,58 @@ export default {
}
});
},
+ formatResults(input) {
+ if (!this.query) {
+ return input;
+ }
+
+ return highlighter(`<span class="gl-text-black-normal">${input}</span>`, this.query);
+ },
+ unsetError() {
+ this.error = '';
+ },
+ filterItems(items) {
+ return (
+ items?.filter(
+ (wi) => !this.childrenIds.includes(wi.id) && this.parentWorkItemId !== wi.id,
+ ) || []
+ );
+ },
},
};
</script>
<template>
- <gl-token-selector
- ref="tokenSelector"
- v-model="workItemsToAdd"
- :dropdown-items="availableWorkItems"
- :loading="isLoading"
- :placeholder="addInputPlaceholder"
- menu-class="gl-dropdown-menu-wide dropdown-reduced-height gl-min-h-7!"
- :container-class="tokenSelectorContainerClass"
- data-testid="work-item-token-select-input"
- @text-input="debouncedSearchKeyUpdate"
- @focus="handleFocus"
- @mouseover.native="handleMouseOver"
- @mouseout.native="handleMouseOut"
- @token-add="focusInputText"
- @token-remove="focusInputText"
- @blur="handleBlur"
- >
- <template #token-content="{ token }"> {{ token.iid }} {{ token.title }} </template>
- <template #dropdown-item-content="{ dropdownItem }">
- <div class="gl-display-flex">
- <div class="gl-text-secondary gl-font-sm gl-mr-4">{{ dropdownItem.iid }}</div>
- <div class="gl-text-truncate">{{ dropdownItem.title }}</div>
- </div>
- </template>
- </gl-token-selector>
+ <div>
+ <gl-alert v-if="error" variant="danger" class="gl-mb-3" @dismiss="unsetError">
+ {{ error }}
+ </gl-alert>
+ <gl-token-selector
+ ref="tokenSelector"
+ v-model="workItemsToAdd"
+ :dropdown-items="availableWorkItems"
+ :loading="isLoading"
+ :placeholder="addInputPlaceholder"
+ menu-class="gl-dropdown-menu-wide dropdown-reduced-height gl-min-h-7!"
+ :container-class="tokenSelectorContainerClass"
+ data-testid="work-item-token-select-input"
+ @text-input="debouncedSearchKeyUpdate"
+ @focus="handleFocus"
+ @mouseover.native="handleMouseOver"
+ @mouseout.native="handleMouseOut"
+ @token-add="focusInputText"
+ @token-remove="focusInputText"
+ @blur="handleBlur"
+ >
+ <template #token-content="{ token }"> {{ token.iid }} {{ token.title }} </template>
+ <template #dropdown-item-content="{ dropdownItem }">
+ <div class="gl-display-flex">
+ <div
+ v-safe-html="formatResults(dropdownItem.iid)"
+ class="gl-text-secondary gl-font-sm gl-mr-4"
+ ></div>
+ <div v-safe-html="formatResults(dropdownItem.title)" class="gl-text-truncate"></div>
+ </div>
+ </template>
+ </gl-token-selector>
+ </div>
</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_actions.vue b/app/assets/javascripts/work_items/components/work_item_actions.vue
index 02d2ea24ca0..0a71fbc9a34 100644
--- a/app/assets/javascripts/work_items/components/work_item_actions.vue
+++ b/app/assets/javascripts/work_items/components/work_item_actions.vue
@@ -8,7 +8,7 @@ import {
GlToggle,
} from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { __, s__ } from '~/locale';
import Tracking from '~/tracking';
@@ -20,12 +20,12 @@ import {
I18N_WORK_ITEM_DELETE,
I18N_WORK_ITEM_ARE_YOU_SURE_DELETE,
TEST_ID_CONFIDENTIALITY_TOGGLE_ACTION,
- TEST_ID_NOTIFICATIONS_TOGGLE_ACTION,
TEST_ID_NOTIFICATIONS_TOGGLE_FORM,
TEST_ID_DELETE_ACTION,
TEST_ID_PROMOTE_ACTION,
TEST_ID_COPY_CREATE_NOTE_EMAIL_ACTION,
TEST_ID_COPY_REFERENCE_ACTION,
+ TEST_ID_TOGGLE_ACTION,
I18N_WORK_ITEM_ERROR_CONVERTING,
WORK_ITEM_TYPE_VALUE_KEY_RESULT,
WORK_ITEM_TYPE_VALUE_OBJECTIVE,
@@ -36,11 +36,12 @@ import {
import updateWorkItemNotificationsMutation from '../graphql/update_work_item_notifications.mutation.graphql';
import convertWorkItemMutation from '../graphql/work_item_convert.mutation.graphql';
import projectWorkItemTypesQuery from '../graphql/project_work_item_types.query.graphql';
+import WorkItemStateToggle from './work_item_state_toggle.vue';
export default {
i18n: {
- enableTaskConfidentiality: s__('WorkItem|Turn on confidentiality'),
- disableTaskConfidentiality: s__('WorkItem|Turn off confidentiality'),
+ enableConfidentiality: s__('WorkItem|Turn on confidentiality'),
+ disableConfidentiality: s__('WorkItem|Turn off confidentiality'),
notifications: s__('WorkItem|Notifications'),
notificationOn: s__('WorkItem|Notifications turned on.'),
notificationOff: s__('WorkItem|Notifications turned off.'),
@@ -54,25 +55,30 @@ export default {
GlDropdownDivider,
GlModal,
GlToggle,
+ WorkItemStateToggle,
},
directives: {
GlModal: GlModalDirective,
},
mixins: [Tracking.mixin({ label: 'actions_menu' })],
isLoggedIn: isLoggedIn(),
- notificationsToggleTestId: TEST_ID_NOTIFICATIONS_TOGGLE_ACTION,
notificationsToggleFormTestId: TEST_ID_NOTIFICATIONS_TOGGLE_FORM,
confidentialityTestId: TEST_ID_CONFIDENTIALITY_TOGGLE_ACTION,
copyReferenceTestId: TEST_ID_COPY_REFERENCE_ACTION,
copyCreateNoteEmailTestId: TEST_ID_COPY_CREATE_NOTE_EMAIL_ACTION,
deleteActionTestId: TEST_ID_DELETE_ACTION,
promoteActionTestId: TEST_ID_PROMOTE_ACTION,
+ stateToggleTestId: TEST_ID_TOGGLE_ACTION,
inject: ['isGroup'],
props: {
fullPath: {
type: String,
required: true,
},
+ workItemState: {
+ type: String,
+ required: true,
+ },
workItemId: {
type: String,
required: false,
@@ -128,6 +134,11 @@ export default {
required: false,
default: false,
},
+ workItemParentId: {
+ type: String,
+ required: false,
+ default: null,
+ },
},
apollo: {
workItemTypes: {
@@ -165,6 +176,11 @@ export default {
canPromoteToObjective() {
return this.canUpdate && this.workItemType === WORK_ITEM_TYPE_VALUE_KEY_RESULT;
},
+ confidentialItemText() {
+ return this.isConfidential
+ ? this.$options.i18n.disableConfidentiality
+ : this.$options.i18n.enableConfidentiality;
+ },
objectiveWorkItemTypeId() {
return this.workItemTypes.find((type) => type.name === WORK_ITEM_TYPE_VALUE_OBJECTIVE).id;
},
@@ -267,7 +283,7 @@ export default {
icon="ellipsis_v"
data-testid="work-item-actions-dropdown"
text-sr-only
- :text="__('More actions')"
+ :toggle-text="__('More actions')"
category="tertiary"
:auto-close="false"
no-caret
@@ -282,7 +298,6 @@ export default {
<gl-toggle
:value="subscribedToNotifications"
:label="$options.i18n.notifications"
- :data-testid="$options.notificationsToggleTestId"
class="work-item-notification-toggle"
label-position="left"
@change="toggleNotifications($event)"
@@ -299,49 +314,56 @@ export default {
>
<template #list-item>{{ __('Promote to objective') }}</template>
</gl-disclosure-dropdown-item>
- <template v-if="canUpdate && !isParentConfidential">
- <gl-disclosure-dropdown-item
- :data-testid="$options.confidentialityTestId"
- @action="handleToggleWorkItemConfidentiality"
- ><template #list-item>{{
- isConfidential
- ? $options.i18n.disableTaskConfidentiality
- : $options.i18n.enableTaskConfidentiality
- }}</template></gl-disclosure-dropdown-item
- >
- </template>
+
+ <gl-disclosure-dropdown-item
+ v-if="canUpdate && !isParentConfidential"
+ :data-testid="$options.confidentialityTestId"
+ @action="handleToggleWorkItemConfidentiality"
+ >
+ <template #list-item>{{ confidentialItemText }}</template>
+ </gl-disclosure-dropdown-item>
+
+ <work-item-state-toggle
+ v-if="canUpdate"
+ :data-testid="$options.stateToggleTestId"
+ :work-item-id="workItemId"
+ :work-item-state="workItemState"
+ :work-item-parent-id="workItemParentId"
+ :work-item-type="workItemType"
+ show-as-dropdown-item
+ />
+
<gl-disclosure-dropdown-item
- ref="workItemReference"
:data-testid="$options.copyReferenceTestId"
:data-clipboard-text="workItemReference"
@action="copyToClipboard(workItemReference, $options.i18n.referenceCopied)"
- ><template #list-item>{{
- $options.i18n.copyReference
- }}</template></gl-disclosure-dropdown-item
>
- <template v-if="$options.isLoggedIn && workItemCreateNoteEmail">
- <gl-disclosure-dropdown-item
- ref="workItemCreateNoteEmail"
- :data-testid="$options.copyCreateNoteEmailTestId"
- :data-clipboard-text="workItemCreateNoteEmail"
- @action="copyToClipboard(workItemCreateNoteEmail, $options.i18n.emailAddressCopied)"
- ><template #list-item>{{
- i18n.copyCreateNoteEmail
- }}</template></gl-disclosure-dropdown-item
- >
- </template>
- <gl-dropdown-divider v-if="canDelete" />
+ <template #list-item>{{ $options.i18n.copyReference }}</template>
+ </gl-disclosure-dropdown-item>
+
<gl-disclosure-dropdown-item
- v-if="canDelete"
- :data-testid="$options.deleteActionTestId"
- variant="danger"
- @action="handleDelete"
+ v-if="$options.isLoggedIn && workItemCreateNoteEmail"
+ :data-testid="$options.copyCreateNoteEmailTestId"
+ :data-clipboard-text="workItemCreateNoteEmail"
+ @action="copyToClipboard(workItemCreateNoteEmail, $options.i18n.emailAddressCopied)"
>
- <template #list-item
- ><span class="text-danger">{{ i18n.deleteWorkItem }}</span></template
- >
+ <template #list-item>{{ i18n.copyCreateNoteEmail }}</template>
</gl-disclosure-dropdown-item>
+
+ <template v-if="canDelete">
+ <gl-dropdown-divider />
+ <gl-disclosure-dropdown-item
+ :data-testid="$options.deleteActionTestId"
+ variant="danger"
+ @action="handleDelete"
+ >
+ <template #list-item>
+ <span class="text-danger">{{ i18n.deleteWorkItem }}</span>
+ </template>
+ </gl-disclosure-dropdown-item>
+ </template>
</gl-disclosure-dropdown>
+
<gl-modal
ref="modal"
modal-id="work-item-confirm-delete"
diff --git a/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue b/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue
index fd01d855782..7d09a003926 100644
--- a/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue
+++ b/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue
@@ -13,6 +13,7 @@ import {
WIDGET_TYPE_WEIGHT,
WORK_ITEM_TYPE_VALUE_KEY_RESULT,
WORK_ITEM_TYPE_VALUE_OBJECTIVE,
+ WORK_ITEM_TYPE_VALUE_TASK,
} from '../constants';
import WorkItemDueDate from './work_item_due_date.vue';
import WorkItemAssignees from './work_item_assignees.vue';
@@ -98,7 +99,8 @@ export default {
showWorkItemParent() {
return (
this.workItemType === WORK_ITEM_TYPE_VALUE_OBJECTIVE ||
- this.workItemType === WORK_ITEM_TYPE_VALUE_KEY_RESULT
+ this.workItemType === WORK_ITEM_TYPE_VALUE_KEY_RESULT ||
+ this.workItemType === WORK_ITEM_TYPE_VALUE_TASK
);
},
workItemParent() {
diff --git a/app/assets/javascripts/work_items/components/work_item_award_emoji.vue b/app/assets/javascripts/work_items/components/work_item_award_emoji.vue
index 44bd17b59a2..f806946509f 100644
--- a/app/assets/javascripts/work_items/components/work_item_award_emoji.vue
+++ b/app/assets/javascripts/work_items/components/work_item_award_emoji.vue
@@ -1,13 +1,14 @@
<script>
-import * as Sentry from '@sentry/browser';
import { produce } from 'immer';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils';
import AwardsList from '~/vue_shared/components/awards_list.vue';
import { isLoggedIn } from '~/lib/utils/common_utils';
import { TYPENAME_USER } from '~/graphql_shared/constants';
-import workItemAwardEmojiQuery from '../graphql/award_emoji.query.graphql';
+import groupWorkItemAwardEmojiQuery from '../graphql/group_award_emoji.query.graphql';
+import projectWorkItemAwardEmojiQuery from '../graphql/award_emoji.query.graphql';
import updateAwardEmojiMutation from '../graphql/update_award_emoji.mutation.graphql';
import {
EMOJI_THUMBSDOWN,
@@ -23,6 +24,7 @@ export default {
components: {
AwardsList,
},
+ inject: ['isGroup'],
props: {
workItemId: {
type: String,
@@ -75,7 +77,9 @@ export default {
},
apollo: {
awardEmoji: {
- query: workItemAwardEmojiQuery,
+ query() {
+ return this.isGroup ? groupWorkItemAwardEmojiQuery : projectWorkItemAwardEmojiQuery;
+ },
variables() {
return {
iid: this.workItemIid,
@@ -116,7 +120,7 @@ export default {
after: this.pageInfo?.endCursor,
},
});
- } catch (error) {
+ } catch {
this.$emit('error', I18N_WORK_ITEM_FETCH_AWARD_EMOJI_ERROR);
}
},
@@ -139,7 +143,7 @@ export default {
return this.awardEmoji.nodes;
}
- // else make a copy of unmutable list and return the list after adding the new emoji
+ // else make a copy of immutable list and return the list after adding the new emoji
const awardEmojiNodes = [...this.awardEmoji.nodes];
awardEmojiNodes.push({
name,
@@ -162,7 +166,7 @@ export default {
},
updateWorkItemAwardEmojiWidgetCache({ cache, name, toggledOn }) {
const query = {
- query: workItemAwardEmojiQuery,
+ query: this.isGroup ? groupWorkItemAwardEmojiQuery : projectWorkItemAwardEmojiQuery,
variables: {
fullPath: this.workItemFullpath,
iid: this.workItemIid,
@@ -234,7 +238,6 @@ export default {
<template>
<div v-if="!isLoading" class="gl-mt-3">
<awards-list
- data-testid="work-item-award-list"
:awards="awards"
:can-award-emoji="$options.isLoggedIn"
:current-user-id="currentUserId"
diff --git a/app/assets/javascripts/work_items/components/work_item_created_updated.vue b/app/assets/javascripts/work_items/components/work_item_created_updated.vue
index 460b5d35187..d352d66196a 100644
--- a/app/assets/javascripts/work_items/components/work_item_created_updated.vue
+++ b/app/assets/javascripts/work_items/components/work_item_created_updated.vue
@@ -86,7 +86,7 @@ export default {
</script>
<template>
- <div class="gl-mb-3 gl-text-gray-700">
+ <div class="gl-mb-3 gl-text-gray-700 gl-mt-3">
<work-item-state-badge v-if="workItemState" :work-item-state="workItemState" />
<gl-loading-icon v-if="updateInProgress" :inline="true" class="gl-mr-3" />
<confidentiality-badge
diff --git a/app/assets/javascripts/work_items/components/work_item_description.vue b/app/assets/javascripts/work_items/components/work_item_description.vue
index b7f3ac93cdb..77c573b47e4 100644
--- a/app/assets/javascripts/work_items/components/work_item_description.vue
+++ b/app/assets/javascripts/work_items/components/work_item_description.vue
@@ -1,6 +1,6 @@
<script>
import { GlAlert, GlButton, GlForm, GlFormGroup } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { helpPagePath } from '~/helpers/help_page_helper';
import { getDraft, clearDraft, updateDraft } from '~/lib/utils/autosave';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
@@ -244,13 +244,7 @@ export default {
@keydown.ctrl.enter="updateWorkItem"
/>
<div class="gl-display-flex">
- <gl-alert
- v-if="hasConflicts"
- :dismissible="false"
- variant="danger"
- class="gl-w-full"
- data-testid="work-item-description-conflicts"
- >
+ <gl-alert v-if="hasConflicts" :dismissible="false" variant="danger" class="gl-w-full">
<p>
{{
s__(
diff --git a/app/assets/javascripts/work_items/components/work_item_description_rendered.vue b/app/assets/javascripts/work_items/components/work_item_description_rendered.vue
index 07e03eba1d1..124e05db431 100644
--- a/app/assets/javascripts/work_items/components/work_item_description_rendered.vue
+++ b/app/assets/javascripts/work_items/components/work_item_description_rendered.vue
@@ -114,7 +114,7 @@ export default {
v-else
ref="gfm-content"
v-safe-html="descriptionHtml"
- class="md gl-mb-5 gl-min-h-8"
+ class="md gl-mb-5 gl-min-h-8 gl-clearfix"
data-testid="work-item-description"
@change="toggleCheckboxes"
></div>
diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue
index 53929775684..45d3aa564a5 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail.vue
@@ -50,7 +50,6 @@ import WorkItemDescription from './work_item_description.vue';
import WorkItemNotes from './work_item_notes.vue';
import WorkItemDetailModal from './work_item_detail_modal.vue';
import WorkItemAwardEmoji from './work_item_award_emoji.vue';
-import WorkItemStateToggleButton from './work_item_state_toggle_button.vue';
import WorkItemRelationships from './work_item_relationships/work_item_relationships.vue';
import WorkItemTypeIcon from './work_item_type_icon.vue';
@@ -61,7 +60,6 @@ export default {
},
isLoggedIn: isLoggedIn(),
components: {
- WorkItemStateToggleButton,
GlAlert,
GlButton,
GlLoadingIcon,
@@ -146,9 +144,9 @@ export default {
if (isEmpty(this.workItem)) {
this.setEmptyState();
}
- if (!this.isModal && this.workItem.project) {
- const path = this.workItem.project?.fullPath
- ? ` · ${this.workItem.project.fullPath}`
+ if (!this.isModal && this.workItem.namespace) {
+ const path = this.workItem.namespace.fullPath
+ ? ` · ${this.workItem.namespace.fullPath}`
: '';
document.title = `${this.workItem.title} · ${this.workItem?.workItemType?.name}${path}`;
@@ -181,19 +179,19 @@ export default {
return this.workItemType ? `#${this.workItem.iid}` : '';
},
canUpdate() {
- return this.workItem?.userPermissions?.updateWorkItem;
+ return this.workItem.userPermissions?.updateWorkItem;
},
canDelete() {
- return this.workItem?.userPermissions?.deleteWorkItem;
+ return this.workItem.userPermissions?.deleteWorkItem;
},
canSetWorkItemMetadata() {
- return this.workItem?.userPermissions?.setWorkItemMetadata;
+ return this.workItem.userPermissions?.setWorkItemMetadata;
},
canAssignUnassignUser() {
return this.workItemAssignees && this.canSetWorkItemMetadata;
},
projectFullPath() {
- return this.workItem?.project?.fullPath;
+ return this.workItem.namespace?.fullPath;
},
workItemsMvc2Enabled() {
return this.glFeatures.workItemsMvc2;
@@ -222,7 +220,7 @@ export default {
return this.parentWorkItem?.webUrl;
},
workItemIconName() {
- return this.workItem?.workItemType?.iconName;
+ return this.workItem.workItemType?.iconName;
},
noAccessSvgPath() {
return `data:image/svg+xml;utf8,${encodeURIComponent(noAccessSvg)}`;
@@ -274,6 +272,18 @@ export default {
showWorkItemLinkedItems() {
return this.hasLinkedWorkItems && this.workItemLinkedItems;
},
+ titleClassHeader() {
+ return {
+ 'gl-sm-display-none!': this.parentWorkItem,
+ 'gl-w-full': !this.parentWorkItem,
+ };
+ },
+ titleClassComponent() {
+ return {
+ 'gl-sm-display-block!': !this.parentWorkItem,
+ 'gl-display-none gl-sm-display-block!': this.parentWorkItem,
+ };
+ },
},
mounted() {
if (this.modalWorkItemIid) {
@@ -285,7 +295,7 @@ export default {
},
methods: {
isWidgetPresent(type) {
- return this.workItem?.widgets?.find((widget) => widget.type === type);
+ return this.workItem.widgets?.find((widget) => widget.type === type);
},
toggleConfidentiality(confidentialStatus) {
this.updateInProgress = true;
@@ -409,7 +419,20 @@ export default {
</gl-skeleton-loader>
</div>
<template v-else>
- <div class="gl-display-flex gl-align-items-center" data-testid="work-item-body">
+ <div class="gl-sm-display-none! gl-display-flex">
+ <gl-button
+ v-if="isModal"
+ class="gl-ml-auto"
+ category="tertiary"
+ data-testid="work-item-close"
+ icon="close"
+ :aria-label="__('Close')"
+ @click="$emit('close')"
+ />
+ </div>
+ <div
+ class="gl-display-block gl-sm-display-flex! gl-align-items-flex-start gl-flex-direction-column gl-sm-flex-direction-row gl-gap-3 gl-pt-3"
+ >
<ul
v-if="parentWorkItem"
class="list-unstyled gl-display-flex gl-min-w-0 gl-mr-auto gl-mb-0 gl-z-index-0"
@@ -440,53 +463,55 @@ export default {
</li>
</ul>
<div
- v-else-if="!error && !workItemLoading"
- class="gl-mr-auto"
+ v-if="!error && !workItemLoading"
+ :class="titleClassHeader"
data-testid="work-item-type"
>
- <work-item-type-icon
- :work-item-icon-name="workItemIconName"
+ <work-item-title
+ v-if="workItem.title"
+ ref="title"
+ class="gl-sm-display-block!"
+ :work-item-id="workItem.id"
+ :work-item-title="workItem.title"
:work-item-type="workItemType"
- show-text
+ :work-item-parent-id="workItemParentId"
+ :can-update="canUpdate"
+ @error="updateError = $event"
+ />
+ </div>
+ <div class="detail-page-header-actions gl-display-flex gl-align-self-start gl-gap-3">
+ <work-item-todos
+ v-if="showWorkItemCurrentUserTodos"
+ :work-item-id="workItem.id"
+ :work-item-iid="workItemIid"
+ :work-item-fullpath="projectFullPath"
+ :current-user-todos="currentUserTodos"
+ @error="updateError = $event"
+ />
+ <work-item-actions
+ :full-path="fullPath"
+ :work-item-id="workItem.id"
+ :subscribed-to-notifications="workItemNotificationsSubscribed"
+ :work-item-type="workItemType"
+ :work-item-type-id="workItemTypeId"
+ :can-delete="canDelete"
+ :can-update="canUpdate"
+ :is-confidential="workItem.confidential"
+ :is-parent-confidential="parentWorkItemConfidentiality"
+ :work-item-reference="workItem.reference"
+ :work-item-create-note-email="workItem.createNoteEmail"
+ :is-modal="isModal"
+ :work-item-state="workItem.state"
+ :work-item-parent-id="workItemParentId"
+ @deleteWorkItem="$emit('deleteWorkItem', { workItemType, workItemId: workItem.id })"
+ @toggleWorkItemConfidentiality="toggleConfidentiality"
+ @error="updateError = $event"
+ @promotedToObjective="$emit('promotedToObjective', workItemIid)"
/>
- {{ workItemBreadcrumbReference }}
</div>
- <work-item-state-toggle-button
- v-if="canUpdate"
- :work-item-id="workItem.id"
- :work-item-state="workItem.state"
- :work-item-parent-id="workItemParentId"
- :work-item-type="workItemType"
- @error="updateError = $event"
- />
- <work-item-todos
- v-if="showWorkItemCurrentUserTodos"
- :work-item-id="workItem.id"
- :work-item-iid="workItemIid"
- :work-item-fullpath="projectFullPath"
- :current-user-todos="currentUserTodos"
- @error="updateError = $event"
- />
- <work-item-actions
- :full-path="fullPath"
- :work-item-id="workItem.id"
- :subscribed-to-notifications="workItemNotificationsSubscribed"
- :work-item-type="workItemType"
- :work-item-type-id="workItemTypeId"
- :can-delete="canDelete"
- :can-update="canUpdate"
- :is-confidential="workItem.confidential"
- :is-parent-confidential="parentWorkItemConfidentiality"
- :work-item-reference="workItem.reference"
- :work-item-create-note-email="workItem.createNoteEmail"
- :is-modal="isModal"
- @deleteWorkItem="$emit('deleteWorkItem', { workItemType, workItemId: workItem.id })"
- @toggleWorkItemConfidentiality="toggleConfidentiality"
- @error="updateError = $event"
- @promotedToObjective="$emit('promotedToObjective', workItemIid)"
- />
<gl-button
v-if="isModal"
+ class="gl-display-none gl-sm-display-block!"
category="tertiary"
data-testid="work-item-close"
icon="close"
@@ -496,8 +521,9 @@ export default {
</div>
<div>
<work-item-title
- v-if="workItem.title"
+ v-if="workItem.title && parentWorkItem"
ref="title"
+ :class="titleClassComponent"
:work-item-id="workItem.id"
:work-item-title="workItem.title"
:work-item-type="workItemType"
diff --git a/app/assets/javascripts/work_items/components/work_item_due_date.vue b/app/assets/javascripts/work_items/components/work_item_due_date.vue
index 1aa62a2b906..704fe6fb11d 100644
--- a/app/assets/javascripts/work_items/components/work_item_due_date.vue
+++ b/app/assets/javascripts/work_items/components/work_item_due_date.vue
@@ -1,6 +1,6 @@
<script>
import { GlButton, GlDatepicker, GlFormGroup } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { getDateWithUTC, newDateAsLocaleTime } from '~/lib/utils/datetime/date_calculation_utility';
import { s__ } from '~/locale';
import Tracking from '~/tracking';
diff --git a/app/assets/javascripts/work_items/components/work_item_labels.vue b/app/assets/javascripts/work_items/components/work_item_labels.vue
index 3cdbf816421..7a5d3b1155f 100644
--- a/app/assets/javascripts/work_items/components/work_item_labels.vue
+++ b/app/assets/javascripts/work_items/components/work_item_labels.vue
@@ -3,7 +3,8 @@ import { GlTokenSelector, GlLabel, GlSkeletonLoader } from '@gitlab/ui';
import { debounce, uniqueId, without } from 'lodash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import Tracking from '~/tracking';
-import labelSearchQuery from '~/sidebar/components/labels/labels_select_widget/graphql/project_labels.query.graphql';
+import groupLabelsQuery from '~/sidebar/components/labels/labels_select_widget/graphql/group_labels.query.graphql';
+import projectLabelsQuery from '~/sidebar/components/labels/labels_select_widget/graphql/project_labels.query.graphql';
import LabelItem from '~/sidebar/components/labels/labels_select_widget/label_item.vue';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { isScopedLabel } from '~/lib/utils/common_utils';
@@ -90,7 +91,9 @@ export default {
},
},
searchLabels: {
- query: labelSearchQuery,
+ query() {
+ return this.isGroup ? groupLabelsQuery : projectLabelsQuery;
+ },
variables() {
return {
fullPath: this.fullPath,
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_children_wrapper.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_children_wrapper.vue
index f4de7c1dddc..b6ea09edbd4 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_children_wrapper.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_children_wrapper.vue
@@ -1,7 +1,7 @@
<script>
-import * as Sentry from '@sentry/browser';
import produce from 'immer';
import Draggable from 'vuedraggable';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { isLoggedIn } from '~/lib/utils/common_utils';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
@@ -50,6 +50,11 @@ export default {
required: false,
default: false,
},
+ showLabels: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
data() {
return {
@@ -151,9 +156,6 @@ export default {
update(data) {
return data.workspace.workItems.nodes[0];
},
- context: {
- isSingleRequest: true,
- },
});
},
prefetchWorkItem({ iid }) {
@@ -280,6 +282,7 @@ export default {
:confidential="child.confidential"
:work-item-type="workItemType"
:has-indirect-children="hasIndirectChildren"
+ :show-labels="showLabels"
@mouseover="prefetchWorkItem(child)"
@mouseout="clearPrefetching"
@removeChild="removeChild"
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue
index 847a3585ac4..49454c3d9f3 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue
@@ -1,7 +1,7 @@
<script>
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import { cloneDeep } from 'lodash';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { __, s__ } from '~/locale';
import { isScopedLabel } from '~/lib/utils/common_utils';
import { createAlert } from '~/alert';
@@ -49,6 +49,11 @@ export default {
required: false,
default: '',
},
+ showLabels: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
data() {
return {
@@ -231,6 +236,7 @@ export default {
:can-update="canUpdate"
:parent-work-item-id="issuableGid"
:work-item-type="workItemType"
+ :show-labels="showLabels"
@click="$emit('click', $event)"
@removeChild="$emit('removeChild', childItem)"
/>
@@ -241,6 +247,7 @@ export default {
:work-item-id="issuableGid"
:work-item-type="workItemType"
:children="children"
+ :show-labels="showLabels"
@removeChild="removeChild"
@click="$emit('click', $event)"
/>
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue
index 7fa6ac2c57f..dd0a26c0b9c 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue
@@ -5,6 +5,7 @@ import {
GlIcon,
GlLoadingIcon,
GlTooltipDirective,
+ GlToggle,
} from '@gitlab/ui';
import { isEmpty } from 'lodash';
import { s__ } from '~/locale';
@@ -15,7 +16,12 @@ import { isMetaKey } from '~/lib/utils/common_utils';
import { getParameterByName, setUrlParams, updateHistory } from '~/lib/utils/url_utility';
import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
-import { FORM_TYPES, WIDGET_ICONS, WORK_ITEM_STATUS_TEXT } from '../../constants';
+import {
+ FORM_TYPES,
+ WIDGET_ICONS,
+ WORK_ITEM_STATUS_TEXT,
+ I18N_WORK_ITEM_SHOW_LABELS,
+} from '../../constants';
import { findHierarchyWidgetChildren } from '../../utils';
import { removeHierarchyChild } from '../../graphql/cache_utils';
import groupWorkItemByIidQuery from '../../graphql/group_work_item_by_iid.query.graphql';
@@ -36,6 +42,7 @@ export default {
WorkItemDetailModal,
AbuseCategorySelector,
WorkItemChildrenWrapper,
+ GlToggle,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -65,9 +72,6 @@ export default {
update(data) {
return data.workspace.workItems.nodes[0] ?? {};
},
- context: {
- isSingleRequest: true,
- },
skip() {
return !this.iid;
},
@@ -107,6 +111,7 @@ export default {
reportedUserId: 0,
reportedUrl: '',
widgetName: 'tasks',
+ showLabels: true,
};
},
computed: {
@@ -204,6 +209,7 @@ export default {
addChildButtonLabel: s__('WorkItem|Add'),
addChildOptionLabel: s__('WorkItem|Existing task'),
createChildOptionLabel: s__('WorkItem|New task'),
+ showLabelsLabel: I18N_WORK_ITEM_SHOW_LABELS,
},
WIDGET_TYPE_TASK_ICON: WIDGET_ICONS.TASK,
WORK_ITEM_STATUS_TEXT,
@@ -227,6 +233,14 @@ export default {
</span>
</template>
<template #header-right>
+ <gl-toggle
+ class="gl-mr-4"
+ :value="showLabels"
+ :label="$options.i18n.showLabelsLabel"
+ label-position="left"
+ label-id="relationship-toggle-labels"
+ @change="showLabels = $event"
+ />
<gl-disclosure-dropdown
v-if="canUpdate && canAddTask"
placement="right"
@@ -282,6 +296,7 @@ export default {
:full-path="fullPath"
:work-item-id="issuableGid"
:work-item-iid="iid"
+ :show-labels="showLabels"
@error="error = $event"
@show-modal="openChild"
/>
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue
index b61b3b2e0d3..3d09a90169c 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue
@@ -1,10 +1,12 @@
<script>
+import { GlToggle } from '@gitlab/ui';
import {
FORM_TYPES,
WIDGET_TYPE_HIERARCHY,
WORK_ITEMS_TREE_TEXT_MAP,
WORK_ITEM_TYPE_ENUM_OBJECTIVE,
WORK_ITEM_TYPE_ENUM_KEY_RESULT,
+ I18N_WORK_ITEM_SHOW_LABELS,
} from '../../constants';
import WidgetWrapper from '../widget_wrapper.vue';
import OkrActionsSplitButton from './okr_actions_split_button.vue';
@@ -21,6 +23,7 @@ export default {
WidgetWrapper,
WorkItemLinksForm,
WorkItemChildrenWrapper,
+ GlToggle,
},
props: {
fullPath: {
@@ -68,6 +71,7 @@ export default {
formType: null,
childType: null,
widgetName: 'tasks',
+ showLabels: true,
};
},
computed: {
@@ -99,6 +103,9 @@ export default {
this.$emit('show-modal', { event, modalWorkItem: child });
},
},
+ i18n: {
+ showLabelsLabel: I18N_WORK_ITEM_SHOW_LABELS,
+ },
};
</script>
@@ -114,6 +121,14 @@ export default {
{{ $options.WORK_ITEMS_TREE_TEXT_MAP[workItemType].title }}
</template>
<template #header-right>
+ <gl-toggle
+ class="gl-mr-4"
+ :value="showLabels"
+ :label="$options.i18n.showLabelsLabel"
+ label-position="left"
+ label-id="relationship-toggle-labels"
+ @change="showLabels = $event"
+ />
<okr-actions-split-button
v-if="canUpdate"
@showCreateObjectiveForm="
@@ -160,6 +175,7 @@ export default {
:work-item-id="workItemId"
:work-item-iid="workItemIid"
:work-item-type="workItemType"
+ :show-labels="showLabels"
@error="error = $event"
@show-modal="showModal"
/>
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree_children.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree_children.vue
index 401223c3593..af181fa4e7e 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree_children.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree_children.vue
@@ -22,6 +22,11 @@ export default {
required: false,
default: false,
},
+ showLabels: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
};
</script>
@@ -35,6 +40,7 @@ export default {
:issuable-gid="workItemId"
:child-item="child"
:work-item-type="workItemType"
+ :show-labels="showLabels"
@removeChild="$emit('removeChild', $event)"
@click="$emit('click', Object.assign($event, { childItem: child }))"
/>
diff --git a/app/assets/javascripts/work_items/components/work_item_milestone.vue b/app/assets/javascripts/work_items/components/work_item_milestone.vue
index a2cbb7f7598..9c6fa158169 100644
--- a/app/assets/javascripts/work_items/components/work_item_milestone.vue
+++ b/app/assets/javascripts/work_items/components/work_item_milestone.vue
@@ -1,15 +1,7 @@
<script>
-import {
- GlFormGroup,
- GlDropdown,
- GlDropdownItem,
- GlDropdownDivider,
- GlSkeletonLoader,
- GlSearchBoxByType,
- GlDropdownText,
-} from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import { GlCollapsibleListbox, GlFormGroup, GlSkeletonLoader } from '@gitlab/ui';
import { debounce } from 'lodash';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import Tracking from '~/tracking';
import { s__, __ } from '~/locale';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
@@ -22,7 +14,8 @@ import {
TRACKING_CATEGORY_SHOW,
} from '../constants';
-const noMilestoneId = 'no-milestone-id';
+export const noMilestoneId = 'no-milestone-id';
+const noMilestoneItem = { text: s__('WorkItem|No milestone'), value: noMilestoneId };
export default {
i18n: {
@@ -37,13 +30,9 @@ export default {
EXPIRED_TEXT: __('(expired)'),
},
components: {
+ GlCollapsibleListbox,
GlFormGroup,
- GlDropdown,
- GlDropdownItem,
- GlDropdownDivider,
GlSkeletonLoader,
- GlSearchBoxByType,
- GlDropdownText,
},
mixins: [Tracking.mixin()],
props: {
@@ -74,11 +63,23 @@ export default {
data() {
return {
localMilestone: this.workItemMilestone,
+ localMilestoneId: this.workItemMilestone?.id,
searchTerm: '',
shouldFetch: false,
updateInProgress: false,
- isFocused: false,
milestones: [],
+ dropdownGroups: [
+ {
+ text: this.$options.i18n.NO_MILESTONE,
+ textSrOnly: true,
+ options: [noMilestoneItem],
+ },
+ {
+ text: __('Milestones'),
+ textSrOnly: true,
+ options: [],
+ },
+ ],
};
},
computed: {
@@ -103,23 +104,29 @@ export default {
isLoadingMilestones() {
return this.$apollo.queries.milestones.loading;
},
- isNoMilestone() {
- return this.localMilestone?.id === noMilestoneId || !this.localMilestone?.id;
+ milestonesList() {
+ return (
+ this.milestones.map(({ id, title, expired }) => {
+ return {
+ value: id,
+ text: title,
+ expired,
+ };
+ }) ?? []
+ );
},
- dropdownClasses() {
- return {
- 'gl-text-gray-500!': this.canUpdate && this.isNoMilestone,
- 'is-not-focused': !this.isFocused,
- 'gl-min-w-20': true,
- };
+ toggleClasses() {
+ const toggleClasses = ['gl-max-w-full'];
+
+ if (this.localMilestoneId === noMilestoneId) {
+ toggleClasses.push('gl-text-gray-500!');
+ }
+ return toggleClasses;
},
},
watch: {
- workItemMilestone: {
- handler(newVal) {
- this.localMilestone = newVal;
- },
- deep: true,
+ milestones() {
+ this.dropdownGroups[1].options = this.milestonesList;
},
},
created() {
@@ -152,15 +159,11 @@ export default {
this.localMilestone = milestone;
},
onDropdownShown() {
- this.$refs.search.focusInput();
this.shouldFetch = true;
- this.isFocused = true;
},
onDropdownHide() {
- this.isFocused = false;
this.searchTerm = '';
this.shouldFetch = false;
- this.updateMilestone();
},
setSearchKey(value) {
this.searchTerm = value;
@@ -169,6 +172,9 @@ export default {
return this.localMilestone?.id === milestone?.id;
},
updateMilestone() {
+ this.localMilestone =
+ this.milestones.find(({ id }) => id === this.localMilestoneId) ?? noMilestoneItem;
+
if (this.workItemMilestone?.id === this.localMilestone?.id) {
return;
}
@@ -182,8 +188,7 @@ export default {
input: {
id: this.workItemId,
milestoneWidget: {
- milestoneId:
- this.localMilestone?.id === 'no-milestone-id' ? null : this.localMilestone?.id,
+ milestoneId: this.localMilestoneId === noMilestoneId ? null : this.localMilestoneId,
},
},
},
@@ -222,50 +227,45 @@ export default {
>
{{ dropdownText }}
</span>
- <gl-dropdown
+
+ <gl-collapsible-listbox
v-else
id="milestone-value"
+ v-model="localMilestoneId"
+ :items="dropdownGroups"
+ category="tertiary"
data-testid="work-item-milestone-dropdown"
- class="gl-pl-0 gl-max-w-full work-item-field-value"
- :toggle-class="dropdownClasses"
- :text="dropdownText"
+ class="gl-max-w-full"
+ :toggle-text="dropdownText"
:loading="updateInProgress"
+ :toggle-class="toggleClasses"
+ searchable
+ @select="updateMilestone"
@shown="onDropdownShown"
- @hide="onDropdownHide"
+ @hidden="onDropdownHide"
+ @search="debouncedSearchKeyUpdate"
>
- <template #header>
- <gl-search-box-by-type ref="search" :value="searchTerm" @input="debouncedSearchKeyUpdate" />
+ <template #list-item="{ item }">
+ {{ item.text }}
+ <span v-if="item.expired">{{ $options.i18n.EXPIRED_TEXT }}</span>
</template>
- <gl-dropdown-item
- data-testid="no-milestone"
- is-check-item
- :is-checked="isNoMilestone"
- @click="handleMilestoneClick({ id: 'no-milestone-id' })"
- >
- {{ $options.i18n.NO_MILESTONE }}
- </gl-dropdown-item>
- <gl-dropdown-divider />
- <gl-dropdown-text v-if="isLoadingMilestones">
- <gl-skeleton-loader :height="90">
+ <template #footer>
+ <gl-skeleton-loader v-if="isLoadingMilestones" :height="90">
<rect width="380" height="10" x="10" y="15" rx="4" />
<rect width="280" height="10" x="10" y="30" rx="4" />
<rect width="380" height="10" x="10" y="50" rx="4" />
<rect width="280" height="10" x="10" y="65" rx="4" />
</gl-skeleton-loader>
- </gl-dropdown-text>
- <template v-else-if="milestones.length">
- <gl-dropdown-item
- v-for="milestone in milestones"
- :key="milestone.id"
- is-check-item
- :is-checked="isMilestoneChecked(milestone)"
- @click="handleMilestoneClick(milestone)"
+
+ <div
+ v-else-if="!milestones.length"
+ aria-live="assertive"
+ class="gl-pl-7 gl-pr-5 gl-py-3 gl-font-base gl-text-gray-600"
+ data-testid="no-results-text"
>
- {{ milestone.title }}
- <template v-if="milestone.expired">{{ $options.i18n.EXPIRED_TEXT }}</template>
- </gl-dropdown-item>
+ {{ $options.i18n.NO_MATCHING_RESULTS }}
+ </div>
</template>
- <gl-dropdown-text v-else>{{ $options.i18n.NO_MATCHING_RESULTS }}</gl-dropdown-text>
- </gl-dropdown>
+ </gl-collapsible-listbox>
</gl-form-group>
</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_notes.vue b/app/assets/javascripts/work_items/components/work_item_notes.vue
index fe8aea99f53..6756acd4495 100644
--- a/app/assets/javascripts/work_items/components/work_item_notes.vue
+++ b/app/assets/javascripts/work_items/components/work_item_notes.vue
@@ -1,7 +1,7 @@
<script>
import { GlSkeletonLoader, GlModal } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
import { uniqueId } from 'lodash';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { __ } from '~/locale';
import { scrollToTargetOnResize } from '~/lib/utils/resize_observer';
import { TYPENAME_DISCUSSION, TYPENAME_NOTE } from '~/graphql_shared/constants';
@@ -170,9 +170,6 @@ export default {
apollo: {
workItemNotes: {
query: workItemNotesByIidQuery,
- context: {
- isSingleRequest: true,
- },
variables() {
return {
fullPath: this.fullPath,
diff --git a/app/assets/javascripts/work_items/components/work_item_parent.vue b/app/assets/javascripts/work_items/components/work_item_parent.vue
index e16299f482f..ce30f7985cf 100644
--- a/app/assets/javascripts/work_items/components/work_item_parent.vue
+++ b/app/assets/javascripts/work_items/components/work_item_parent.vue
@@ -1,18 +1,20 @@
<script>
import { GlFormGroup, GlCollapsibleListbox } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
import { debounce } from 'lodash';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { s__ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
+import { removeHierarchyChild } from '../graphql/cache_utils';
+import groupWorkItemsQuery from '../graphql/group_work_items.query.graphql';
import projectWorkItemsQuery from '../graphql/project_work_items.query.graphql';
import {
I18N_WORK_ITEM_ERROR_UPDATING,
sprintfWorkItem,
- WORK_ITEM_TYPE_ENUM_OBJECTIVE,
+ SUPPORTED_PARENT_TYPE_MAP,
} from '../constants';
export default {
@@ -31,7 +33,7 @@ export default {
GlCollapsibleListbox,
},
mixins: [glFeatureFlagMixin()],
- inject: ['fullPath'],
+ inject: ['fullPath', 'isGroup'],
props: {
workItemId: {
type: String,
@@ -60,7 +62,7 @@ export default {
searchStarted: false,
availableWorkItems: [],
localSelectedItem: this.parent?.id,
- isNotFocused: true,
+ oldParent: this.parent,
};
},
computed: {
@@ -80,13 +82,8 @@ export default {
workItems() {
return this.availableWorkItems.map(({ id, title }) => ({ text: title, value: id }));
},
- listboxCategory() {
- return this.searchStarted ? 'secondary' : 'tertiary';
- },
- listboxClasses() {
- return {
- 'is-not-focused': this.isNotFocused && !this.searchStarted,
- };
+ parentType() {
+ return SUPPORTED_PARENT_TYPE_MAP[this.workItemType];
},
},
watch: {
@@ -101,13 +98,17 @@ export default {
},
apollo: {
availableWorkItems: {
- query: projectWorkItemsQuery,
+ query() {
+ return this.isGroup ? groupWorkItemsQuery : projectWorkItemsQuery;
+ },
variables() {
return {
fullPath: this.fullPath,
searchTerm: this.search,
- types: [WORK_ITEM_TYPE_ENUM_OBJECTIVE],
+ types: this.parentType,
in: this.search ? 'TITLE' : undefined,
+ iid: null,
+ isNumber: false,
};
},
skip() {
@@ -146,6 +147,14 @@ export default {
},
},
},
+ update: (cache) =>
+ removeHierarchyChild({
+ cache,
+ fullPath: this.fullPath,
+ iid: this.oldParent?.iid,
+ isGroup: this.isGroup,
+ workItem: { id: this.workItemId },
+ }),
});
if (errors.length) {
@@ -171,19 +180,10 @@ export default {
},
onListboxShown() {
this.searchStarted = true;
- this.isNotFocused = false;
},
onListboxHide() {
this.searchStarted = false;
this.search = '';
- this.isNotFocused = true;
- },
- setListboxFocused() {
- // This is to match the caret behaviour of parent listbox
- // to the other dropdown fields of work items
- if (document.activeElement.parentElement.id !== 'work-item-parent-listbox-value') {
- this.isNotFocused = true;
- }
},
},
};
@@ -206,30 +206,20 @@ export default {
>
{{ listboxText }}
</span>
- <div
- v-else
- :class="{ 'gl-max-w-max-content': !workItemsMvc2Enabled }"
- @mouseover="isNotFocused = false"
- @mouseleave="setListboxFocused"
- @focusout="isNotFocused = true"
- @focusin="isNotFocused = false"
- >
+ <div v-else :class="{ 'gl-max-w-max-content': !workItemsMvc2Enabled }">
<gl-collapsible-listbox
id="work-item-parent-listbox-value"
class="gl-max-w-max-content"
data-testid="work-item-parent-listbox"
- block
searchable
- :no-caret="isNotFocused && !searchStarted"
is-check-centered
- :category="listboxCategory"
+ category="tertiary"
:searching="isLoading"
:header-text="$options.i18n.assignParentLabel"
:no-results-text="$options.i18n.noMatchingResults"
:loading="updateInProgress"
:items="workItems"
:toggle-text="listboxText"
- :toggle-class="listboxClasses"
:selected="localSelectedItem"
:reset-button-label="$options.i18n.unAssign"
@reset="unAssignParent"
diff --git a/app/assets/javascripts/work_items/components/work_item_relationships/work_item_add_relationship_form.vue b/app/assets/javascripts/work_items/components/work_item_relationships/work_item_add_relationship_form.vue
index d242db95896..c98bd6ce1e9 100644
--- a/app/assets/javascripts/work_items/components/work_item_relationships/work_item_add_relationship_form.vue
+++ b/app/assets/javascripts/work_items/components/work_item_relationships/work_item_add_relationship_form.vue
@@ -4,6 +4,7 @@ import { GlFormGroup, GlForm, GlFormRadioGroup, GlButton, GlAlert } from '@gitla
import { __, s__ } from '~/locale';
import WorkItemTokenInput from '../shared/work_item_token_input.vue';
import addLinkedItemsMutation from '../../graphql/add_linked_items.mutation.graphql';
+import groupWorkItemByIidQuery from '../../graphql/group_work_item_by_iid.query.graphql';
import workItemByIidQuery from '../../graphql/work_item_by_iid.query.graphql';
import {
LINK_ITEM_FORM_HEADER_LABEL,
@@ -23,6 +24,7 @@ export default {
GlAlert,
WorkItemTokenInput,
},
+ inject: ['isGroup'],
props: {
workItemId: {
type: String,
@@ -121,7 +123,7 @@ export default {
},
) => {
const queryArgs = {
- query: workItemByIidQuery,
+ query: this.isGroup ? groupWorkItemByIidQuery : workItemByIidQuery,
variables: { fullPath: this.workItemFullPath, iid: this.workItemIid },
};
const sourceData = cache.readQuery(queryArgs);
diff --git a/app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationship_list.vue b/app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationship_list.vue
index 002c1786044..e70c79ea68f 100644
--- a/app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationship_list.vue
+++ b/app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationship_list.vue
@@ -19,6 +19,11 @@ export default {
type: Boolean,
required: true,
},
+ showLabels: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
};
</script>
@@ -42,6 +47,7 @@ export default {
:child-item="linkedItem.workItem"
:can-update="canUpdate"
:show-task-icon="true"
+ :show-labels="showLabels"
@click="$emit('showModal', { event: $event, child: linkedItem.workItem })"
@removeChild="$emit('removeLinkedItem', linkedItem.workItem)"
/>
diff --git a/app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationships.vue b/app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationships.vue
index 20427fe96c4..790804a8934 100644
--- a/app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationships.vue
+++ b/app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationships.vue
@@ -1,6 +1,6 @@
<script>
import { produce } from 'immer';
-import { GlLoadingIcon, GlIcon, GlButton, GlLink } from '@gitlab/ui';
+import { GlLoadingIcon, GlIcon, GlButton, GlLink, GlToggle } from '@gitlab/ui';
import { s__ } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
@@ -8,7 +8,11 @@ import { helpPagePath } from '~/helpers/help_page_helper';
import groupWorkItemByIidQuery from '../../graphql/group_work_item_by_iid.query.graphql';
import workItemByIidQuery from '../../graphql/work_item_by_iid.query.graphql';
import removeLinkedItemsMutation from '../../graphql/remove_linked_items.mutation.graphql';
-import { WIDGET_TYPE_LINKED_ITEMS, LINKED_CATEGORIES_MAP } from '../../constants';
+import {
+ WIDGET_TYPE_LINKED_ITEMS,
+ LINKED_CATEGORIES_MAP,
+ I18N_WORK_ITEM_SHOW_LABELS,
+} from '../../constants';
import WidgetWrapper from '../widget_wrapper.vue';
import WorkItemRelationshipList from './work_item_relationship_list.vue';
@@ -24,6 +28,7 @@ export default {
WidgetWrapper,
WorkItemRelationshipList,
WorkItemAddRelationshipForm,
+ GlToggle,
},
inject: ['isGroup'],
props: {
@@ -60,9 +65,6 @@ export default {
update(data) {
return data.workspace.workItems.nodes[0] ?? {};
},
- context: {
- isSingleRequest: true,
- },
skip() {
return !this.workItemIid;
},
@@ -97,6 +99,7 @@ export default {
linksBlocks: [],
isShownLinkItemForm: false,
widgetName: 'linkeditems',
+ showLabels: true,
};
},
computed: {
@@ -150,7 +153,7 @@ export default {
return;
}
const queryArgs = {
- query: workItemByIidQuery,
+ query: this.isGroup ? groupWorkItemByIidQuery : workItemByIidQuery,
variables: { fullPath: this.workItemFullPath, iid: this.workItemIid },
};
const sourceData = cache.readQuery(queryArgs);
@@ -200,6 +203,7 @@ export default {
blockingTitle: s__('WorkItem|Blocking'),
blockedByTitle: s__('WorkItem|Blocked by'),
addLinkedWorkItemButtonLabel: s__('WorkItem|Add'),
+ showLabelsLabel: I18N_WORK_ITEM_SHOW_LABELS,
},
};
</script>
@@ -222,11 +226,18 @@ export default {
</div>
</template>
<template #header-right>
+ <gl-toggle
+ :value="showLabels"
+ :label="$options.i18n.showLabelsLabel"
+ label-position="left"
+ label-id="relationship-toggle-labels"
+ @change="showLabels = $event"
+ />
<gl-button
v-if="canAdminWorkItemLink"
data-testid="link-item-add-button"
size="small"
- class="gl-ml-3"
+ class="gl-ml-4"
@click="showLinkItemForm"
>
<slot name="add-button-text">{{ $options.i18n.addLinkedWorkItemButtonLabel }}</slot>
@@ -264,6 +275,7 @@ export default {
:linked-items="linksBlocks"
:heading="$options.i18n.blockingTitle"
:can-update="canAdminWorkItemLink"
+ :show-labels="showLabels"
@showModal="$emit('showModal', { event: $event.event, modalWorkItem: $event.child })"
@removeLinkedItem="removeLinkedItem"
/>
@@ -276,6 +288,7 @@ export default {
:linked-items="linksIsBlockedBy"
:heading="$options.i18n.blockedByTitle"
:can-update="canAdminWorkItemLink"
+ :show-labels="showLabels"
@showModal="$emit('showModal', { event: $event.event, modalWorkItem: $event.child })"
@removeLinkedItem="removeLinkedItem"
/>
@@ -284,6 +297,7 @@ export default {
:linked-items="linksRelatesTo"
:heading="$options.i18n.relatedToTitle"
:can-update="canAdminWorkItemLink"
+ :show-labels="showLabels"
@showModal="$emit('showModal', { event: $event.event, modalWorkItem: $event.child })"
@removeLinkedItem="removeLinkedItem"
/>
diff --git a/app/assets/javascripts/work_items/components/work_item_state_toggle.vue b/app/assets/javascripts/work_items/components/work_item_state_toggle.vue
new file mode 100644
index 00000000000..581ef9ec945
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_state_toggle.vue
@@ -0,0 +1,131 @@
+<script>
+import { GlButton, GlDisclosureDropdownItem, GlLoadingIcon } from '@gitlab/ui';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
+import Tracking from '~/tracking';
+import { __ } from '~/locale';
+import { getUpdateWorkItemMutation } from '~/work_items/components/update_work_item';
+import {
+ sprintfWorkItem,
+ I18N_WORK_ITEM_ERROR_UPDATING,
+ STATE_OPEN,
+ STATE_EVENT_CLOSE,
+ STATE_EVENT_REOPEN,
+ TRACKING_CATEGORY_SHOW,
+} from '../constants';
+
+export default {
+ components: {
+ GlButton,
+ GlDisclosureDropdownItem,
+ GlLoadingIcon,
+ },
+ mixins: [Tracking.mixin()],
+ props: {
+ workItemState: {
+ type: String,
+ required: true,
+ },
+ workItemId: {
+ type: String,
+ required: true,
+ },
+ workItemType: {
+ type: String,
+ required: true,
+ },
+ workItemParentId: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ showAsDropdownItem: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ updateInProgress: false,
+ };
+ },
+ computed: {
+ isWorkItemOpen() {
+ return this.workItemState === STATE_OPEN;
+ },
+ toggleWorkItemStateText() {
+ const baseText = this.isWorkItemOpen
+ ? __('Close %{workItemType}')
+ : __('Reopen %{workItemType}');
+ return sprintfWorkItem(baseText, this.workItemType);
+ },
+ tracking() {
+ return {
+ category: TRACKING_CATEGORY_SHOW,
+ label: 'item_state',
+ property: `type_${this.workItemType}`,
+ };
+ },
+ toggleInProgressText() {
+ const baseText = this.isWorkItemOpen
+ ? __('Closing %{workItemType}')
+ : __('Reopening %{workItemType}');
+ return sprintfWorkItem(baseText, this.workItemType);
+ },
+ },
+ methods: {
+ async updateWorkItem() {
+ const input = {
+ id: this.workItemId,
+ stateEvent: this.isWorkItemOpen ? STATE_EVENT_CLOSE : STATE_EVENT_REOPEN,
+ };
+
+ this.updateInProgress = true;
+
+ try {
+ this.track('updated_state');
+
+ const { mutation, variables } = getUpdateWorkItemMutation({
+ workItemParentId: this.workItemParentId,
+ input,
+ });
+
+ const { data } = await this.$apollo.mutate({
+ mutation,
+ variables,
+ });
+
+ const errors = data.workItemUpdate?.errors;
+
+ if (errors?.length) {
+ throw new Error(errors[0]);
+ }
+ } catch (error) {
+ const msg = sprintfWorkItem(I18N_WORK_ITEM_ERROR_UPDATING, this.workItemType);
+
+ this.$emit('error', msg);
+ Sentry.captureException(error);
+ }
+
+ this.updateInProgress = false;
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-disclosure-dropdown-item v-if="showAsDropdownItem" @action="updateWorkItem">
+ <template #list-item>
+ <template v-if="updateInProgress">
+ <gl-loading-icon inline size="sm" />
+ {{ toggleInProgressText }}
+ </template>
+ <template v-else>
+ {{ toggleWorkItemStateText }}
+ </template>
+ </template>
+ </gl-disclosure-dropdown-item>
+ <gl-button v-else :loading="updateInProgress" @click="updateWorkItem">{{
+ toggleWorkItemStateText
+ }}</gl-button>
+</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_state_toggle_button.vue b/app/assets/javascripts/work_items/components/work_item_state_toggle_button.vue
deleted file mode 100644
index 0ea30845466..00000000000
--- a/app/assets/javascripts/work_items/components/work_item_state_toggle_button.vue
+++ /dev/null
@@ -1,113 +0,0 @@
-<script>
-import { GlButton } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
-import Tracking from '~/tracking';
-import { __, sprintf } from '~/locale';
-import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
-import { getUpdateWorkItemMutation } from '~/work_items/components/update_work_item';
-import {
- sprintfWorkItem,
- I18N_WORK_ITEM_ERROR_UPDATING,
- STATE_OPEN,
- STATE_EVENT_CLOSE,
- STATE_EVENT_REOPEN,
- TRACKING_CATEGORY_SHOW,
-} from '../constants';
-
-export default {
- components: {
- GlButton,
- },
- mixins: [Tracking.mixin()],
- props: {
- workItemState: {
- type: String,
- required: true,
- },
- workItemId: {
- type: String,
- required: true,
- },
- workItemType: {
- type: String,
- required: true,
- },
- workItemParentId: {
- type: String,
- required: false,
- default: null,
- },
- },
- data() {
- return {
- updateInProgress: false,
- };
- },
- computed: {
- isWorkItemOpen() {
- return this.workItemState === STATE_OPEN;
- },
- toggleWorkItemStateText() {
- const baseText = this.isWorkItemOpen
- ? __('Close %{workItemType}')
- : __('Reopen %{workItemType}');
- return capitalizeFirstCharacter(
- sprintf(baseText, { workItemType: this.workItemType.toLowerCase() }),
- );
- },
- tracking() {
- return {
- category: TRACKING_CATEGORY_SHOW,
- label: 'item_state',
- property: `type_${this.workItemType}`,
- };
- },
- },
- methods: {
- async updateWorkItem() {
- const input = {
- id: this.workItemId,
- stateEvent: this.isWorkItemOpen ? STATE_EVENT_CLOSE : STATE_EVENT_REOPEN,
- };
-
- this.updateInProgress = true;
-
- try {
- this.track('updated_state');
-
- const { mutation, variables } = getUpdateWorkItemMutation({
- workItemParentId: this.workItemParentId,
- input,
- });
-
- const { data } = await this.$apollo.mutate({
- mutation,
- variables,
- });
-
- const errors = data.workItemUpdate?.errors;
-
- if (errors?.length) {
- throw new Error(errors[0]);
- }
- } catch (error) {
- const msg = sprintfWorkItem(I18N_WORK_ITEM_ERROR_UPDATING, this.workItemType);
-
- this.$emit('error', msg);
- Sentry.captureException(error);
- }
-
- this.updateInProgress = false;
- },
- },
-};
-</script>
-
-<template>
- <gl-button
- :loading="updateInProgress"
- data-testid="work-item-state-toggle"
- @click="updateWorkItem"
- >{{ toggleWorkItemStateText }}</gl-button
- >
-</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_title.vue b/app/assets/javascripts/work_items/components/work_item_title.vue
index c52a6854fad..9b5803421dd 100644
--- a/app/assets/javascripts/work_items/components/work_item_title.vue
+++ b/app/assets/javascripts/work_items/components/work_item_title.vue
@@ -1,10 +1,12 @@
<script>
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import Tracking from '~/tracking';
import {
sprintfWorkItem,
I18N_WORK_ITEM_ERROR_UPDATING,
TRACKING_CATEGORY_SHOW,
+ WORK_ITEM_TITLE_MAX_LENGTH,
+ I18N_MAX_CHARS_IN_WORK_ITEM_TITLE_MESSAGE,
} from '../constants';
import { getUpdateWorkItemMutation } from './update_work_item';
import ItemTitle from './item_title.vue';
@@ -56,6 +58,11 @@ export default {
return;
}
+ if (updatedTitle.length > WORK_ITEM_TITLE_MAX_LENGTH) {
+ this.$emit('error', sprintfWorkItem(I18N_MAX_CHARS_IN_WORK_ITEM_TITLE_MESSAGE));
+ return;
+ }
+
const input = {
id: this.workItemId,
title: updatedTitle,
diff --git a/app/assets/javascripts/work_items/components/work_item_todos.vue b/app/assets/javascripts/work_items/components/work_item_todos.vue
index e6d7f2067ba..62518616398 100644
--- a/app/assets/javascripts/work_items/components/work_item_todos.vue
+++ b/app/assets/javascripts/work_items/components/work_item_todos.vue
@@ -175,17 +175,12 @@ export default {
<template>
<gl-button
v-gl-tooltip.hover
- data-testid="work-item-todos-action"
:loading="isLoading"
:title="buttonLabel"
- category="tertiary"
+ category="secondary"
:aria-label="buttonLabel"
@click="onToggle"
>
- <gl-icon
- data-testid="work-item-todos-icon"
- :class="{ 'gl-fill-blue-500': pendingTodo }"
- :name="buttonIcon"
- />
+ <gl-icon :class="{ 'gl-fill-blue-500': pendingTodo }" :name="buttonIcon" />
</gl-button>
</template>
diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js
index a64172acff4..daa72204609 100644
--- a/app/assets/javascripts/work_items/constants.js
+++ b/app/assets/javascripts/work_items/constants.js
@@ -35,6 +35,7 @@ export const WORK_ITEM_TYPE_ENUM_TEST_CASE = 'TEST_CASE';
export const WORK_ITEM_TYPE_ENUM_REQUIREMENTS = 'REQUIREMENTS';
export const WORK_ITEM_TYPE_ENUM_OBJECTIVE = 'OBJECTIVE';
export const WORK_ITEM_TYPE_ENUM_KEY_RESULT = 'KEY_RESULT';
+export const WORK_ITEM_TYPE_ENUM_EPIC = 'EPIC';
export const WORK_ITEM_TYPE_VALUE_EPIC = 'Epic';
export const WORK_ITEM_TYPE_VALUE_INCIDENT = 'Incident';
@@ -45,6 +46,8 @@ export const WORK_ITEM_TYPE_VALUE_REQUIREMENTS = 'Requirements';
export const WORK_ITEM_TYPE_VALUE_KEY_RESULT = 'Key Result';
export const WORK_ITEM_TYPE_VALUE_OBJECTIVE = 'Objective';
+export const WORK_ITEM_TITLE_MAX_LENGTH = 255;
+
export const i18n = {
fetchErrorTitle: s__('WorkItem|Work item not found'),
fetchError: s__(
@@ -91,8 +94,9 @@ export const I18N_WORK_ITEM_FETCH_AWARD_EMOJI_ERROR = s__(
export const I18N_WORK_ITEM_CREATE_BUTTON_LABEL = s__('WorkItem|Create %{workItemType}');
export const I18N_WORK_ITEM_ADD_BUTTON_LABEL = s__('WorkItem|Add %{workItemType}');
export const I18N_WORK_ITEM_ADD_MULTIPLE_BUTTON_LABEL = s__('WorkItem|Add %{workItemType}s');
-export const I18N_WORK_ITEM_SEARCH_INPUT_PLACEHOLDER = s__(
- 'WorkItem|Search existing %{workItemType}s',
+export const I18N_WORK_ITEM_SEARCH_INPUT_PLACEHOLDER = s__('WorkItem|Search existing items');
+export const I18N_WORK_ITEM_SEARCH_ERROR = s__(
+ 'WorkItem|Something went wrong while fetching the %{workItemType}. Please try again.',
);
export const I18N_WORK_ITEM_CONFIDENTIALITY_CHECKBOX_LABEL = s__(
'WorkItem|This %{workItemType} is confidential and should only be visible to team members with at least Reporter access',
@@ -108,6 +112,11 @@ export const I18N_WORK_ITEM_ERROR_COPY_EMAIL = s__(
'WorkItem|Something went wrong while copying the %{workItemType} email address. Please try again.',
);
+export const I18N_MAX_CHARS_IN_WORK_ITEM_TITLE_MESSAGE = sprintf(
+ s__('WorkItem|Title cannot have more than %{WORK_ITEM_TITLE_MAX_LENGTH} characters.'),
+ { WORK_ITEM_TITLE_MAX_LENGTH },
+);
+
export const I18N_WORK_ITEM_COPY_CREATE_NOTE_EMAIL = s__(
'WorkItem|Copy %{workItemType} email address',
);
@@ -122,6 +131,7 @@ export const I18N_MAX_WORK_ITEMS_NOTE_LABEL = sprintf(
s__('WorkItem|Add a maximum of %{MAX_WORK_ITEMS} items at a time.'),
{ MAX_WORK_ITEMS },
);
+export const I18N_WORK_ITEM_SHOW_LABELS = s__('WorkItem|Show labels');
export const sprintfWorkItem = (msg, workItemTypeArg, parentWorkItemType = '') => {
const workItemType = workItemTypeArg || s__('WorkItem|item');
@@ -178,6 +188,11 @@ export const WORK_ITEMS_TYPE_MAP = {
name: s__('WorkItem|Key result'),
value: WORK_ITEM_TYPE_VALUE_KEY_RESULT,
},
+ [WORK_ITEM_TYPE_ENUM_EPIC]: {
+ icon: `epic`,
+ name: s__('WorkItem|Epic'),
+ value: WORK_ITEM_TYPE_VALUE_EPIC,
+ },
};
export const WORK_ITEMS_TREE_TEXT_MAP = {
@@ -246,12 +261,12 @@ export const WORK_ITEM_ACTIVITY_SORT_OPTIONS = [
];
export const TEST_ID_CONFIDENTIALITY_TOGGLE_ACTION = 'confidentiality-toggle-action';
-export const TEST_ID_NOTIFICATIONS_TOGGLE_ACTION = 'notifications-toggle-action';
export const TEST_ID_NOTIFICATIONS_TOGGLE_FORM = 'notifications-toggle-form';
export const TEST_ID_DELETE_ACTION = 'delete-action';
export const TEST_ID_PROMOTE_ACTION = 'promote-action';
export const TEST_ID_COPY_REFERENCE_ACTION = 'copy-reference-action';
export const TEST_ID_COPY_CREATE_NOTE_EMAIL_ACTION = 'copy-create-note-email-action';
+export const TEST_ID_TOGGLE_ACTION = 'state-toggle-action';
export const TODO_ADD_ICON = 'todo-add';
export const TODO_DONE_ICON = 'todo-done';
@@ -288,3 +303,9 @@ export const LINK_ITEM_FORM_HEADER_LABEL = {
[WORK_ITEM_TYPE_VALUE_KEY_RESULT]: s__('WorkItem|The current key result'),
[WORK_ITEM_TYPE_VALUE_TASK]: s__('WorkItem|The current task'),
};
+
+export const SUPPORTED_PARENT_TYPE_MAP = {
+ [WORK_ITEM_TYPE_VALUE_OBJECTIVE]: [WORK_ITEM_TYPE_ENUM_OBJECTIVE],
+ [WORK_ITEM_TYPE_VALUE_KEY_RESULT]: [WORK_ITEM_TYPE_ENUM_OBJECTIVE],
+ [WORK_ITEM_TYPE_VALUE_TASK]: [WORK_ITEM_TYPE_ENUM_ISSUE],
+};
diff --git a/app/assets/javascripts/work_items/graphql/award_emoji.query.graphql b/app/assets/javascripts/work_items/graphql/award_emoji.query.graphql
index 82a532e1bea..0b9dc546df3 100644
--- a/app/assets/javascripts/work_items/graphql/award_emoji.query.graphql
+++ b/app/assets/javascripts/work_items/graphql/award_emoji.query.graphql
@@ -1,7 +1,7 @@
#import "~/graphql_shared/fragments/page_info.fragment.graphql"
#import "~/work_items/graphql/award_emoji.fragment.graphql"
-query workItemAwardEmojis($fullPath: ID!, $iid: String, $after: String, $pageSize: Int) {
+query projectWorkItemAwardEmojis($fullPath: ID!, $iid: String, $after: String, $pageSize: Int) {
workspace: project(fullPath: $fullPath) {
id
workItems(iid: $iid) {
diff --git a/app/assets/javascripts/work_items/graphql/group_award_emoji.query.graphql b/app/assets/javascripts/work_items/graphql/group_award_emoji.query.graphql
new file mode 100644
index 00000000000..cdf8c7cad04
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/group_award_emoji.query.graphql
@@ -0,0 +1,27 @@
+#import "~/graphql_shared/fragments/page_info.fragment.graphql"
+#import "~/work_items/graphql/award_emoji.fragment.graphql"
+
+query groupWorkItemAwardEmojis($fullPath: ID!, $iid: String, $after: String, $pageSize: Int) {
+ workspace: group(fullPath: $fullPath) {
+ id
+ workItems(iid: $iid) {
+ nodes {
+ id
+ iid
+ widgets {
+ ... on WorkItemWidgetAwardEmoji {
+ type
+ awardEmoji(first: $pageSize, after: $after) {
+ pageInfo {
+ ...PageInfo
+ }
+ nodes {
+ ...AwardEmojiFragment
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/work_items/graphql/group_work_items.query.graphql b/app/assets/javascripts/work_items/graphql/group_work_items.query.graphql
new file mode 100644
index 00000000000..5332e21a0cb
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/group_work_items.query.graphql
@@ -0,0 +1,17 @@
+query groupWorkItems(
+ $searchTerm: String
+ $fullPath: ID!
+ $types: [IssueType!]
+ $in: [IssuableSearchableField!]
+) {
+ workspace: group(fullPath: $fullPath) {
+ id
+ workItems(search: $searchTerm, types: $types, in: $in) {
+ nodes {
+ id
+ iid
+ title
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/work_items/graphql/project_work_items.query.graphql b/app/assets/javascripts/work_items/graphql/project_work_items.query.graphql
index 2be436aa8c2..3aeaaa1116a 100644
--- a/app/assets/javascripts/work_items/graphql/project_work_items.query.graphql
+++ b/app/assets/javascripts/work_items/graphql/project_work_items.query.graphql
@@ -3,6 +3,8 @@ query projectWorkItems(
$fullPath: ID!
$types: [IssueType!]
$in: [IssuableSearchableField!]
+ $iid: String = null
+ $isNumber: Boolean!
) {
workspace: project(fullPath: $fullPath) {
id
@@ -11,8 +13,13 @@ query projectWorkItems(
id
iid
title
- state
- confidential
+ }
+ }
+ workItemsByIid: workItems(iid: $iid, types: $types) @include(if: $isNumber) {
+ nodes {
+ id
+ iid
+ title
}
}
}
diff --git a/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql b/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql
index fac99310890..ef43b9c026d 100644
--- a/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql
+++ b/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql
@@ -4,6 +4,7 @@
fragment WorkItem on WorkItem {
id
iid
+ archived
title
state
description
@@ -13,10 +14,9 @@ fragment WorkItem on WorkItem {
closedAt
reference(full: true)
createNoteEmail
- project {
+ namespace {
id
fullPath
- archived
name
}
author {
diff --git a/app/assets/javascripts/work_items/list/components/work_items_list_app.vue b/app/assets/javascripts/work_items/list/components/work_items_list_app.vue
index a853018a931..58f74dccd4d 100644
--- a/app/assets/javascripts/work_items/list/components/work_items_list_app.vue
+++ b/app/assets/javascripts/work_items/list/components/work_items_list_app.vue
@@ -1,5 +1,5 @@
<script>
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import IssueCardStatistics from 'ee_else_ce/issues/list/components/issue_card_statistics.vue';
import IssueCardTimeInfo from 'ee_else_ce/issues/list/components/issue_card_time_info.vue';
import { STATUS_OPEN } from '~/issues/constants';
diff --git a/app/assets/javascripts/work_items/pages/create_work_item.vue b/app/assets/javascripts/work_items/pages/create_work_item.vue
index 31e790254d9..435a1233dce 100644
--- a/app/assets/javascripts/work_items/pages/create_work_item.vue
+++ b/app/assets/javascripts/work_items/pages/create_work_item.vue
@@ -103,7 +103,7 @@ export default {
data: {
workspace: {
__typename: TYPENAME_PROJECT,
- id: workItem.project.id,
+ id: workItem.namespace.id,
workItems: {
__typename: 'WorkItemConnection',
nodes: [workItem],
diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss
index 40228b93e01..ce8ccb2bc08 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -31,7 +31,3 @@
@media print {
@import 'print';
}
-
-/* Rules for overriding cloaking in startup-general.scss */
-@import 'startup/cloaking';
-@include cloak-startup-scss(block);
diff --git a/app/assets/stylesheets/application_utilities.scss b/app/assets/stylesheets/application_utilities.scss
index 817e983a0ec..8bec12784ed 100644
--- a/app/assets/stylesheets/application_utilities.scss
+++ b/app/assets/stylesheets/application_utilities.scss
@@ -10,3 +10,5 @@
// Gitlab UI util classes
@import '@gitlab/ui/src/scss/utilities';
+
+@import 'tmp_utilities'; \ No newline at end of file
diff --git a/app/assets/stylesheets/components/detail_page.scss b/app/assets/stylesheets/components/detail_page.scss
index de8142924f9..a5fd57f6c57 100644
--- a/app/assets/stylesheets/components/detail_page.scss
+++ b/app/assets/stylesheets/components/detail_page.scss
@@ -36,7 +36,6 @@
}
.detail-page-header-actions {
- align-self: center;
flex: 0 0 auto;
&:not(.is-merge-request) {
@@ -67,6 +66,8 @@
}
.description {
+ @include clearfix;
+
margin-top: 6px;
}
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index 8a64b0999b6..88509dbc4a1 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -191,25 +191,6 @@
color: $gray-700;
}
- // deprecated class
- &.btn-text-field {
- width: 100%;
- text-align: left;
- padding: 6px 16px;
- border-color: $border-color;
- color: $gray-darkest;
- background-color: $white;
-
- &:hover,
- &:active,
- &:focus {
- cursor: text;
- box-shadow: none;
- border-color: lighten($blue-300, 20%);
- color: $gray-darkest;
- }
- }
-
&.dot-highlight::after {
content: '';
background-color: $blue-500;
diff --git a/app/assets/stylesheets/framework/diffs.scss b/app/assets/stylesheets/framework/diffs.scss
index 4bf109a0bff..8f07ef73554 100644
--- a/app/assets/stylesheets/framework/diffs.scss
+++ b/app/assets/stylesheets/framework/diffs.scss
@@ -901,7 +901,6 @@ table.code {
@media (max-width: map-get($grid-breakpoints, lg)-1) {
.diffs .files {
- @include fixed-width-container;
flex-direction: column;
}
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 613e504c771..eb627b036fe 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -247,6 +247,7 @@ span.idiff {
border-bottom: 1px solid $border-color;
padding: $gl-padding-8 $gl-padding;
margin: 0;
+ min-height: px-to-rem(42px);
border-radius: $border-radius-default $border-radius-default 0 0;
@include media-breakpoint-up(md) {
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 32735679ded..e269ea68e41 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -630,11 +630,18 @@ $search-input-field-x-min-width: 200px;
header.navbar-gitlab.super-sidebar-logged-out {
background-color: $brand-charcoal !important;
+ li.nav-item > button,
li.nav-item > a {
- @include gl-text-white;
+ @include gl-text-gray-100;
@include gl-font-weight-normal;
&:hover,
+ &:focus,
+ &:active {
+ @include gl-text-white
+ }
+
+ &:hover,
&:focus {
background-color: $brand-gray-04;
text-decoration: none;
diff --git a/app/assets/stylesheets/framework/highlight.scss b/app/assets/stylesheets/framework/highlight.scss
index a63ce66e681..a93c2191016 100644
--- a/app/assets/stylesheets/framework/highlight.scss
+++ b/app/assets/stylesheets/framework/highlight.scss
@@ -33,7 +33,7 @@
padding-right: 10px;
white-space: pre;
- &:empty::before {
+ &:empty::before, span:empty::before {
content: '\200b';
}
}
diff --git a/app/assets/stylesheets/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss
index 37a2264122d..bfd55fbb53d 100644
--- a/app/assets/stylesheets/framework/icons.scss
+++ b/app/assets/stylesheets/framework/icons.scss
@@ -1,55 +1,37 @@
-@mixin icon-styles($primary-color, $svg-color) {
+@mixin icon-styles($color) {
svg,
.gl-icon {
- fill: $primary-color;
- }
-
- // For the pipeline mini graph, we pass a custom 'gl-border' so that we can enforce
- // a border of 1px instead of the thicker svg borders to adhere to design standards.
- // If we implement the component with 'isBorderless' and also pass that border,
- // this css is to dynamically apply the correct border color for those specific icons.
- &.borderless {
- border-color: $primary-color;
- }
-
- &.interactive {
- &:hover {
- background: $svg-color;
- }
-
- &:hover,
- &.active {
- box-shadow: 0 0 0 1px $primary-color;
- }
+ fill: $color;
}
}
.ci-status-icon-success,
.ci-status-icon-passed {
- @include icon-styles($green-500, $green-100);
+ @include icon-styles($green-500);
}
.ci-status-icon-error,
.ci-status-icon-failed {
- @include icon-styles($red-500, $red-100);
+ @include icon-styles($red-500);
}
.ci-status-icon-pending,
.ci-status-icon-waiting-for-resource,
+.ci-status-icon-waiting-for-callback,
.ci-status-icon-failed-with-warnings,
.ci-status-icon-success-with-warnings {
- @include icon-styles($orange-500, $orange-100);
+ @include icon-styles($orange-500);
}
.ci-status-icon-running {
- @include icon-styles($blue-500, $blue-100);
+ @include icon-styles($blue-500);
}
.ci-status-icon-canceled,
.ci-status-icon-disabled,
.ci-status-icon-scheduled,
.ci-status-icon-manual {
- @include icon-styles($gray-900, $gray-100);
+ @include icon-styles($gray-900);
}
.ci-status-icon-notification,
@@ -57,7 +39,58 @@
.ci-status-icon-created,
.ci-status-icon-skipped,
.ci-status-icon-notfound {
- @include icon-styles($gray-500, $gray-100);
+ @include icon-styles($gray-500);
+}
+
+.ci-icon {
+ // .ci-icon class is used at
+ // - app/assets/javascripts/vue_shared/components/ci_icon.vue
+ // - app/helpers/ci/status_helper.rb
+ .ci-icon-gl-icon-wrapper {
+ @include gl-rounded-full;
+ @include gl-line-height-0;
+ }
+
+ // Makes the borderless CI icons appear slightly bigger than the default 16px.
+ // Could be fixed by making the SVG fill up the canvas in a follow up issue.
+ .gl-icon {
+ // fill: currentColor;
+ width: 20px;
+ height: 20px;
+ margin: -2px;
+ }
+
+ @mixin ci-icon-style($bg-color, $color, $gl-dark-bg-color: null, $gl-dark-color: null) {
+ .ci-icon-gl-icon-wrapper {
+ background-color: $bg-color;
+ color: $color;
+
+ .gl-dark & {
+ background-color: $gl-dark-bg-color;
+ color: $gl-dark-color;
+ }
+ }
+ }
+
+ &.ci-icon-variant-success {
+ @include ci-icon-style($green-500, $white, $green-600, $green-50)
+ }
+
+ &.ci-icon-variant-warning {
+ @include ci-icon-style($orange-500, $white, $orange-600, $orange-50)
+ }
+
+ &.ci-icon-variant-danger {
+ @include ci-icon-style($red-500, $white, $red-600, $red-50)
+ }
+
+ &.ci-icon-variant-info {
+ @include ci-icon-style($white, $blue-500, $blue-600, $blue-50)
+ }
+
+ &.ci-icon-variant-neutral {
+ @include ci-icon-style($white, $gray-500)
+ }
}
.password-status-icon-success {
diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss
index 171f070d776..33c8a0254fd 100644
--- a/app/assets/stylesheets/framework/layout.scss
+++ b/app/assets/stylesheets/framework/layout.scss
@@ -4,12 +4,6 @@ html {
&.touch .tooltip {
display: none !important;
}
-
- @include media-breakpoint-up(sm) {
- &.logged-out-marketing-header {
- --header-height: 72px;
- }
- }
}
body {
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index df107798a87..0f6fdf18ea0 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -30,15 +30,6 @@
max-width: $max-width;
}
-/**
- * Mixin for fixed width container
- */
-@mixin fixed-width-container {
- max-width: $limited-layout-width - ($gl-padding * 2);
- margin-left: auto;
- margin-right: auto;
-}
-
/*
* Base mixin for lists in GitLab
*/
diff --git a/app/assets/stylesheets/framework/page_header.scss b/app/assets/stylesheets/framework/page_header.scss
index c2bd475ab90..ad183a64cc5 100644
--- a/app/assets/stylesheets/framework/page_header.scss
+++ b/app/assets/stylesheets/framework/page_header.scss
@@ -34,12 +34,4 @@
margin-left: 8px;
}
}
-
- .ci-status-link {
- svg {
- position: relative;
- top: 2px;
- margin: 0 2px 0 3px;
- }
- }
}
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 0619d5f166e..168aa704a69 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -2,7 +2,11 @@
width: 100%;
.container-fluid {
- padding: 0 $gl-padding;
+ padding: 0 $container-margin;
+
+ @include media-breakpoint-up(xl) {
+ padding: 0 $container-margin-xl;
+ }
&.container-blank {
background: none;
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index a4bb39e0764..ab8547c3fef 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -468,8 +468,10 @@ $content-wrapper-padding: 100px;
$header-zindex: 1000;
$zindex-dropdown-menu: 300;
$ide-statusbar-height: 25px;
-$fixed-layout-width: 1280px;
-$limited-layout-width: 990px;
+$limited-layout-width: 1006px;
+$fixed-layout-width: 1296px;
+$container-margin: $gl-padding;
+$container-margin-xl: $gl-padding-24;
$container-text-max-width: 540px;
$border-radius-default: 4px;
$border-radius-small: 2px;
@@ -485,7 +487,7 @@ $performance-bar-height: 2.5rem;
$system-header-height: 16px;
$system-footer-height: $system-header-height;
$mr-sticky-header-height: 72px;
-$mr-review-bar-height: calc(2rem + 13px);
+$mr-review-bar-height: calc(2rem + 16px);
$flash-height: 52px;
$context-header-height: 60px;
$top-bar-height: 48px;
@@ -655,8 +657,8 @@ $status-icon-size: 22px;
*/
$discord: #5865f2;
$linkedin: #2867b2;
+$mastodon: #6364ff;
$skype: #0078d7;
-$twitter: #1d9bf0;
/*
* Award emoji
@@ -715,10 +717,10 @@ $blame-blue: #254e77;
*/
$builds-log-bg: #111;
$job-log-highlight-height: 18px;
-$job-log-line-padding: 55px;
+$job-log-line-padding: 63px;
$job-line-number-width: 50px;
-$job-line-number-margin: 43px;
-$job-arrow-margin: 55px;
+$job-line-number-margin: 51px;
+$job-arrow-margin: 63px;
/*
* Calendar
@@ -810,7 +812,7 @@ $ci-action-icon-size: 22px;
$ci-action-icon-size-lg: 24px;
$pipeline-dropdown-line-height: 20px;
$ci-action-dropdown-button-size: 24px;
-$ci-action-dropdown-svg-size: 12px;
+$ci-action-dropdown-svg-size: 16px;
/*
CI variable lists
diff --git a/app/assets/stylesheets/page_bundles/_system_note_styles.scss b/app/assets/stylesheets/page_bundles/_system_note_styles.scss
new file mode 100644
index 00000000000..68e2b747c52
--- /dev/null
+++ b/app/assets/stylesheets/page_bundles/_system_note_styles.scss
@@ -0,0 +1,59 @@
+/**
+Shared styles for system note dot and icon styles used for MR, Issue, Work Item
+*/
+.system-note-tiny-dot {
+ width: 8px;
+ height: 8px;
+ margin-top: 6px;
+ margin-left: 12px;
+ margin-right: 8px;
+ border: 2px solid var(--gray-50, $gray-50);
+ }
+
+ .system-note-icon {
+ width: 20px;
+ height: 20px;
+ margin-left: 6px;
+
+ &.gl-bg-green-100 {
+ --bg-color: var(--green-100, #{$green-100});
+ }
+
+ &.gl-bg-red-100 {
+ --bg-color: var(--red-100, #{$red-100});
+ }
+
+ &.gl-bg-blue-100 {
+ --bg-color: var(--blue-100, #{$blue-100});
+ }
+ }
+
+ .system-note-icon:not(.mr-system-note-empty)::before {
+ content: '';
+ display: block;
+ position: absolute;
+ left: calc(50% - 1px);
+ bottom: 100%;
+ width: 2px;
+ height: 20px;
+ background: linear-gradient(to bottom, transparent, var(--bg-color));
+
+ .system-note:first-child & {
+ display: none;
+ }
+ }
+
+ .system-note-icon:not(.mr-system-note-empty)::after {
+ content: '';
+ display: block;
+ position: absolute;
+ left: calc(50% - 1px);
+ top: 100%;
+ width: 2px;
+ height: 20px;
+ background: linear-gradient(to bottom, var(--bg-color), transparent);
+
+ .system-note:last-child & {
+ display: none;
+ }
+ } \ No newline at end of file
diff --git a/app/assets/stylesheets/page_bundles/boards.scss b/app/assets/stylesheets/page_bundles/boards.scss
index 5aca697ae26..22e42d0a7f7 100644
--- a/app/assets/stylesheets/page_bundles/boards.scss
+++ b/app/assets/stylesheets/page_bundles/boards.scss
@@ -39,6 +39,12 @@
width: 400px;
}
+ &.board-add-new-list {
+ @include media-breakpoint-down(sm) {
+ width: 100%;
+ }
+ }
+
&.is-collapsed {
.board-title-text > span,
.issue-count-badge > span {
diff --git a/app/assets/stylesheets/page_bundles/branches.scss b/app/assets/stylesheets/page_bundles/branches.scss
index daf828fb559..973ba1afb17 100644
--- a/app/assets/stylesheets/page_bundles/branches.scss
+++ b/app/assets/stylesheets/page_bundles/branches.scss
@@ -42,6 +42,10 @@
.branches-list .branch-item:not(:last-of-type) {
border-bottom: 1px solid $border-color;
+
+ .gl-dark & {
+ border-bottom-color: $gray-800;
+ }
}
.branch-item {
diff --git a/app/assets/stylesheets/page_bundles/build.scss b/app/assets/stylesheets/page_bundles/build.scss
index 16fc0e7ebae..6165ee6e8b4 100644
--- a/app/assets/stylesheets/page_bundles/build.scss
+++ b/app/assets/stylesheets/page_bundles/build.scss
@@ -48,14 +48,6 @@
border: 1px solid var(--border-color, $border-color);
padding: 8px $gl-padding 12px;
border-radius: $border-radius-default;
-
- svg {
- position: relative;
- top: 3px;
- margin-right: 5px;
- width: 22px;
- height: 22px;
- }
}
.build-loader-animation {
diff --git a/app/assets/stylesheets/page_bundles/ci_status.scss b/app/assets/stylesheets/page_bundles/ci_status.scss
index 17886ab954a..f2129aa6841 100644
--- a/app/assets/stylesheets/page_bundles/ci_status.scss
+++ b/app/assets/stylesheets/page_bundles/ci_status.scss
@@ -48,6 +48,7 @@
&.ci-pending,
&.ci-waiting-for-resource,
+ &.ci-waiting-for-callback,
&.ci-failed-with-warnings,
&.ci-success-with-warnings {
@include status-color(
diff --git a/app/assets/stylesheets/page_bundles/issuable.scss b/app/assets/stylesheets/page_bundles/issuable.scss
index 07614c5271a..05563f8e314 100644
--- a/app/assets/stylesheets/page_bundles/issuable.scss
+++ b/app/assets/stylesheets/page_bundles/issuable.scss
@@ -1,33 +1,5 @@
@import 'mixins_and_variables_and_functions';
-
-.limit-container-width {
- .flash-container,
- .detail-page-header,
- .page-content-header,
- .commit-box,
- .info-well,
- .commit-ci-menu,
- .files-changed-inner,
- .limited-header-width,
- .limited-width-notes {
- @include fixed-width-container;
- }
-
- .issuable-details {
- .detail-page-description,
- .mr-source-target,
- .mr-state-widget,
- .merge-manually {
- @include fixed-width-container;
- }
- }
-
- .merge-request-details {
- .emoji-list-container {
- @include fixed-width-container;
- }
- }
-}
+@import 'system_note_styles';
.issuable-details {
section {
@@ -114,29 +86,6 @@
}
}
-/*
- * Following overrides are done to prevent
- * legacy dropdown styles from influencing
- * GitLab UI components used within GlDropdown
- */
-.issuable-move-dropdown {
- .b-dropdown-form {
- @include gl-p-0;
- }
-
- .gl-search-box-by-type button.gl-clear-icon-button:hover {
- @include gl-bg-transparent;
-
- &:focus {
- @include gl-focus($inset: true);
- }
- }
-
- .issuable-move-button:not(.disabled):hover {
- @include gl-text-white;
- }
-}
-
.suggestion-footer {
font-size: 12px;
line-height: 15px;
diff --git a/app/assets/stylesheets/page_bundles/merge_request.scss b/app/assets/stylesheets/page_bundles/merge_request.scss
index e429c0c149e..8dc4401e72c 100644
--- a/app/assets/stylesheets/page_bundles/merge_request.scss
+++ b/app/assets/stylesheets/page_bundles/merge_request.scss
@@ -88,20 +88,6 @@ $comparison-empty-state-height: 62px;
.merge-request-title {
margin-bottom: 2px;
-
- .ci-status-link {
- svg {
- height: 16px;
- width: 16px;
- position: relative;
- top: 3px;
- }
-
- &:hover,
- &:focus {
- text-decoration: none;
- }
- }
}
}
}
@@ -147,10 +133,6 @@ $comparison-empty-state-height: 62px;
padding: 0;
background: transparent;
}
-
- .ci-status-link {
- margin-right: 5px;
- }
}
.merge-request-select {
diff --git a/app/assets/stylesheets/page_bundles/merge_requests.scss b/app/assets/stylesheets/page_bundles/merge_requests.scss
index b00e1813696..847cd3f2ff4 100644
--- a/app/assets/stylesheets/page_bundles/merge_requests.scss
+++ b/app/assets/stylesheets/page_bundles/merge_requests.scss
@@ -258,15 +258,15 @@ $tabs-holder-z-index: 250;
position: sticky;
top: calc(#{$calc-application-header-height} + #{$mr-tabs-height} + #{$diff-file-header-top});
+ // height calc is fully delegated to the tree_list_height.vue component
+ height: 0;
min-height: 300px;
- height: calc(#{$calc-application-viewport-height} - (#{$mr-tabs-height} + #{$diff-file-header-top}));
.drag-handle {
bottom: 16px;
}
&.is-sidebar-moved {
- height: calc(#{$calc-application-viewport-height} - (#{$mr-sticky-header-height} + #{$diff-file-header-top}));
top: calc(#{$calc-application-header-height} + #{$mr-sticky-header-height} + #{$diff-file-header-top});
}
}
@@ -379,6 +379,10 @@ $tabs-holder-z-index: 250;
.deployment-info {
margin-bottom: $gl-padding-8;
}
+
+ .gl-button {
+ margin-left: 0;
+ }
}
> *:not(:last-child) {
@@ -645,6 +649,9 @@ $tabs-holder-z-index: 250;
// to the end of the line or to force it to a
// new line if there is not enough space.
flex-grow: 999;
+ // Avoid layout shift of title when Mini Graph
+ // moves below title
+ padding-top: 5px;
}
.label-branch {
@@ -981,7 +988,7 @@ $tabs-holder-z-index: 250;
.merge-request-tabs-container {
&.is-merge-request {
@include gl-mx-auto;
- max-width: $fixed-layout-width - ($gl-padding * 2);
+ max-width: $fixed-layout-width - ($container-margin-xl * 2);
}
}
}
@@ -994,24 +1001,13 @@ $tabs-holder-z-index: 250;
}
}
-.submit-review-dropdown {
- &.show .dropdown-menu {
- width: calc(100vw - 20px);
- max-width: 680px;
- max-height: calc(100vh - 50px);
-
- .gl-dropdown-inner {
- max-height: none !important;
- }
- }
-
- .gl-dropdown-contents {
- padding: $gl-spacing-scale-4 !important;
- }
+.submit-review-dropdown .gl-new-dropdown-panel {
+ max-width: none;
+}
- .md-preview-holder {
- max-height: 182px;
- }
+.submit-review-dropdown-form {
+ width: calc(100vw - 20px);
+ max-width: 680px;
}
.submit-review-dropdown-animated {
@@ -1112,7 +1108,7 @@ $tabs-holder-z-index: 250;
display: flex;
align-items: center;
width: 100%;
- height: $toggle-sidebar-height;
+ height: var(--mr-review-bar-height);
padding-left: $contextual-sidebar-width;
padding-right: $right-sidebar-collapsed-width;
background: var(--white, $white);
@@ -1128,14 +1124,14 @@ $tabs-holder-z-index: 250;
padding-right: 0;
}
- .dropdown {
+ .submit-review-dropdown {
margin-left: $grid-size;
}
}
.review-bar-content {
max-width: $limited-layout-width;
- padding: 0 $gl-padding;
+ padding: 0 $container-margin;
width: 100%;
margin: 0 auto;
}
@@ -1198,63 +1194,6 @@ $tabs-holder-z-index: 250;
}
}
-.mr-system-note-icon {
- width: 20px;
- height: 20px;
- margin-left: 6px;
-
- &.gl-bg-green-100 {
- --bg-color: var(--green-100, #{$green-100});
- }
-
- &.gl-bg-red-100 {
- --bg-color: var(--red-100, #{$red-100});
- }
-
- &.gl-bg-blue-100 {
- --bg-color: var(--blue-100, #{$blue-100});
- }
-}
-
-.mr-system-note-icon:not(.mr-system-note-empty)::before {
- content: '';
- display: block;
- position: absolute;
- left: calc(50% - 1px);
- bottom: 100%;
- width: 2px;
- height: 20px;
- background: linear-gradient(to bottom, transparent, var(--bg-color));
-
- .system-note:first-child & {
- display: none;
- }
-}
-
-.mr-system-note-icon:not(.mr-system-note-empty)::after {
- content: '';
- display: block;
- position: absolute;
- left: calc(50% - 1px);
- top: 100%;
- width: 2px;
- height: 20px;
- background: linear-gradient(to bottom, var(--bg-color), transparent);
-
- .system-note:last-child & {
- display: none;
- }
-}
-
-.mr-system-note-empty {
- width: 8px;
- height: 8px;
- margin-top: 6px;
- margin-left: 12px;
- margin-right: 8px;
- border: 2px solid var(--gray-50, $gray-50);
-}
-
.diff-file-discussions-wrapper {
@include gl-w-full;
diff --git a/app/assets/stylesheets/page_bundles/pipeline.scss b/app/assets/stylesheets/page_bundles/pipeline.scss
index 98e9e2b3c27..aaec277cf08 100644
--- a/app/assets/stylesheets/page_bundles/pipeline.scss
+++ b/app/assets/stylesheets/page_bundles/pipeline.scss
@@ -125,21 +125,27 @@
// They are here to still access a variable or because they use magic values.
// scoped to the graph. Do not add other styles.
.gl-pipeline-min-h {
- min-height: $dropdown-max-height-lg;
+ min-height: calc(#{$dropdown-max-height-lg} + #{$gl-spacing-scale-6});
}
.gl-pipeline-job-width {
width: 100%;
- max-width: 400px;
}
.gl-pipeline-job-width\! {
width: 100% !important;
- max-width: 400px !important;
}
.gl-downstream-pipeline-job-width {
width: 8rem;
+
+ .pipeline-graph-container & {
+ width: 100%;
+
+ @media (min-width: $breakpoint-sm) {
+ width: 8rem;
+ }
+ }
}
.gl-linked-pipeline-padding {
@@ -154,8 +160,8 @@
// Action Icons in big pipeline-graph nodes
&.ci-action-icon-wrapper {
- height: 30px;
- width: 30px;
+ height: 24px;
+ width: 24px;
border-radius: 100%;
display: block;
padding: 0;
@@ -163,6 +169,10 @@
}
}
+.stage-column-title .gl-ci-action-icon-container {
+ right: 11px;
+}
+
.split-report-section {
border-bottom: 1px solid var(--gray-50, $gray-50);
@@ -242,3 +252,69 @@
}
}
}
+
+.pipeline-graph-container {
+ .stage-column.is-stage-view:not(:last-of-type)::after {
+ content: "";
+ position: absolute;
+ top: 100%;
+ left: $gl-spacing-scale-6;
+ width: 2px;
+ height: $gl-spacing-scale-5 * 2;
+ background-color: $gray-200;
+
+ @media (min-width: $breakpoint-sm) {
+ top: 1.25rem;
+ left: 100%;
+ width: $gl-spacing-scale-5 * 2;
+ height: 2px;
+ }
+ }
+
+ .stage-column,
+ .stage-column.is-stage-view {
+ min-width: 1px;
+
+ @media (min-width: $breakpoint-sm) {
+ min-width: inherit;
+ max-width: $gl-spacing-scale-48;
+
+ &:first-of-type {
+ margin-left: $gl-spacing-scale-6;
+ }
+ }
+ }
+
+ .linked-pipeline-container[aria-expanded=true] {
+ @media (max-width: $breakpoint-sm) {
+ width: 100%;
+
+ > div {
+ border-bottom-left-radius: 0;
+ }
+
+ > div > button {
+ border-bottom-right-radius: 0 !important;
+ }
+ }
+ }
+
+ .linked-pipelines-column,
+ .pipeline-show-container,
+ .pipeline-links-container {
+ @media (max-width: $breakpoint-sm) {
+ width: 100%;
+ }
+ }
+
+ .pipeline-graph {
+ @media (max-width: $breakpoint-sm) {
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+ }
+ }
+
+ .pipeline-graph .pipeline-graph {
+ background-color: $gray-100;
+ }
+}
diff --git a/app/assets/stylesheets/page_bundles/pipelines.scss b/app/assets/stylesheets/page_bundles/pipelines.scss
index f9c49b0e6ca..bcc0ad112ac 100644
--- a/app/assets/stylesheets/page_bundles/pipelines.scss
+++ b/app/assets/stylesheets/page_bundles/pipelines.scss
@@ -14,10 +14,6 @@
// - app/assets/javascripts/commit/pipelines/pipelines_bundle.js
.pipelines {
- .badge {
- margin-bottom: 3px;
- }
-
.pipeline-actions {
min-width: 170px; //Guarantees buttons don't break in several lines.
diff --git a/app/assets/stylesheets/page_bundles/profile.scss b/app/assets/stylesheets/page_bundles/profile.scss
index dbe82f583d1..2c08db048fd 100644
--- a/app/assets/stylesheets/page_bundles/profile.scss
+++ b/app/assets/stylesheets/page_bundles/profile.scss
@@ -235,13 +235,17 @@
}
.twitter-icon {
- color: $twitter;
+ color: var(--gl-text-color, $gl-text-color);
}
.discord-icon {
color: $discord;
}
+.mastodon-icon {
+ color: $mastodon;
+}
+
.key-created-at {
line-height: 42px;
}
diff --git a/app/assets/stylesheets/page_bundles/projects.scss b/app/assets/stylesheets/page_bundles/projects.scss
index 99c84026762..d252afd0b29 100644
--- a/app/assets/stylesheets/page_bundles/projects.scss
+++ b/app/assets/stylesheets/page_bundles/projects.scss
@@ -320,10 +320,6 @@
}
}
- .ci-status-link {
- @include gl-text-decoration-none;
- }
-
&:not(.compact) {
.controls {
@include media-breakpoint-up(lg) {
@@ -369,10 +365,6 @@
}
}
}
-
- .ci-status-link {
- @include gl-display-inline-flex;
- }
}
.icon-container {
diff --git a/app/assets/stylesheets/page_bundles/wiki.scss b/app/assets/stylesheets/page_bundles/wiki.scss
index 4fb07328493..81e6b4c1191 100644
--- a/app/assets/stylesheets/page_bundles/wiki.scss
+++ b/app/assets/stylesheets/page_bundles/wiki.scss
@@ -148,7 +148,8 @@
margin: 0;
}
- ul.wiki-pages ul {
+ ul.wiki-pages ul,
+ ul.wiki-pages li:not(.wiki-directory){
padding-left: 20px;
}
@@ -161,6 +162,16 @@
}
}
+.right-sidebar.wiki-sidebar {
+ .active > .wiki-list {
+ a,
+ .wiki-list-expand-button,
+ .wiki-list-collapse-button {
+ color: $white;
+ }
+ }
+}
+
ul.wiki-pages-list.content-list {
a {
color: var(--blue-600, $blue-600);
diff --git a/app/assets/stylesheets/page_bundles/work_items.scss b/app/assets/stylesheets/page_bundles/work_items.scss
index 01c6fde80da..ec73f27ed09 100644
--- a/app/assets/stylesheets/page_bundles/work_items.scss
+++ b/app/assets/stylesheets/page_bundles/work_items.scss
@@ -1,7 +1,8 @@
@import 'mixins_and_variables_and_functions';
+@import 'system_note_styles';
$work-item-field-inset-shadow: inset 0 0 0 $gl-border-size-1 var(--gray-200, $gray-200) !important;
-$work-item-overview-right-sidebar-width: 340px;
+$work-item-overview-right-sidebar-width: 23rem;
$work-item-sticky-header-height: 52px;
.gl-token-selector-token-container {
@@ -67,6 +68,7 @@ $work-item-sticky-header-height: 52px;
}
}
+//TODO: remove all the styles related to `gl-dropdown` when all `.work-item-dropdown`s are migrated
.work-item-dropdown {
// duplicate classname because we are fighting with gl-button styles
.gl-dropdown-toggle.gl-dropdown-toggle {
@@ -95,24 +97,25 @@ $work-item-sticky-header-height: 52px;
// need to override the listbox styles to match with dropdown
// till the dropdown are converted to listbox
- .gl-new-dropdown-toggle {
+ .gl-new-dropdown-toggle.gl-new-dropdown-toggle {
&:hover,
&:focus {
- background: none !important;
box-shadow: $work-item-field-inset-shadow;
background-color: $input-bg;
- }
- .is-not-focused {
- &.gl-new-dropdown-button-text {
- margin: 0 0.25rem;
+ .gl-dark & {
+ // $input-bg is overridden in dark mode but that does not
+ // work in page bundles currently, manually override here
+ background-color: var(--gray-50, $input-bg);
}
}
- }
- .gl-new-dropdown-toggle.is-not-focused {
- .gl-new-dropdown-button-text {
- margin: 0 0.25rem;
+ &:not(:hover, :focus) {
+ box-shadow: none;
+
+ .gl-new-dropdown-chevron {
+ visibility: hidden;
+ }
}
}
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index 8b093e7bb7b..72ea586979f 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -131,7 +131,7 @@
}
.committer {
- color: $gl-text-color-tertiary;
+ color: $gl-text-color-secondary;
.commit-author-link {
color: $gl-text-color;
@@ -144,7 +144,6 @@
vertical-align: text-bottom;
}
- > .ci-status-link,
> .btn,
> .commit-sha-group {
margin-left: $gl-padding;
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index 36efe42aed1..e82a689fe5d 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -81,17 +81,6 @@ ul.related-merge-requests > li gl-emoji {
}
}
-.related-merge-requests {
- .ci-status-link {
- display: block;
- margin-right: 5px;
- }
-
- svg {
- display: block;
- }
-}
-
@include media-breakpoint-down(xs) {
.detail-page-header {
.issuable-meta {
@@ -262,6 +251,14 @@ ul.related-merge-requests > li gl-emoji {
}
}
+.issue-sticky-header-text {
+ padding: 0 $container-margin;
+
+ @include media-breakpoint-up(xl) {
+ padding: 0 $container-margin-xl;
+ }
+}
+
.issuable-header-slide-enter-active,
.issuable-header-slide-leave-active {
@include gl-transition-medium;
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 2722893d04c..8e0fab04ab2 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -10,16 +10,13 @@ $icon-size-diff: $avatar-icon-size - $system-note-icon-size;
$system-note-icon-m-top: $avatar-m-top + $icon-size-diff - 1.3rem;
$system-note-icon-m-left: $avatar-m-left + $icon-size-diff / $avatar-m-ratio;
-@mixin vertical-line($left) {
- &::before {
- content: '';
- border-left: 2px solid var(--gray-50, $gray-50);
- position: absolute;
- top: 16px;
- bottom: 0;
- left: calc(#{$left} - 1px);
- height: calc(100% + 20px);
- }
+@mixin vertical-line($top, $left) {
+ content: '';
+ position: absolute;
+ width: 2px;
+ left: $left;
+ top: $top;
+ height: calc(100% - #{$top});
}
@mixin outline-comment() {
@@ -32,12 +29,7 @@ $system-note-icon-m-left: $avatar-m-left + $icon-size-diff / $avatar-m-ratio;
.limited-width-notes {
.main-notes-list::before,
.timeline-entry:last-child::before {
- content: '';
- position: absolute;
- width: 2px;
- left: 15px;
- top: 15px;
- height: calc(100% - 15px);
+ @include vertical-line(15px, 15px);
}
.main-notes-list::before {
@@ -1143,6 +1135,24 @@ $system-note-icon-m-left: $avatar-m-left + $icon-size-diff / $avatar-m-ratio;
}
}
+.user-activity-content {
+ &::before {
+ @include vertical-line(80px, 25px);
+ background: var(--gray-50, $gray-50);
+ }
+
+ .system-note-image {
+ @include gl--flex-center;
+ top: 14px;
+ width: 22px;
+ height: 22px;
+
+ svg {
+ fill: $gray-600 !important;
+ }
+ }
+}
+
//This needs to be deleted when Snippet/Commit comments are convered to Vue
// See https://gitlab.com/gitlab-org/gitlab-foss/issues/53918#note_117038785
.unstyled-comments {
diff --git a/app/assets/stylesheets/print.scss b/app/assets/stylesheets/print.scss
index c3662c3e6ea..3015cfec34f 100644
--- a/app/assets/stylesheets/print.scss
+++ b/app/assets/stylesheets/print.scss
@@ -67,7 +67,6 @@ nav.navbar-collapse.collapse,
.nav,
.btn,
ul.notes-form,
-.ci-status-link::after,
.issuable-gutter-toggle,
.gutter-toggle,
.issuable-details .content-block-small,
diff --git a/app/assets/stylesheets/startup/_cloaking.scss b/app/assets/stylesheets/startup/_cloaking.scss
deleted file mode 100644
index f60d72a51fb..00000000000
--- a/app/assets/stylesheets/startup/_cloaking.scss
+++ /dev/null
@@ -1,15 +0,0 @@
-/**
- Prevent flashing of content when using startup.css
- */
-@mixin cloak-startup-scss($display) {
- // General selector for cloaking until ready
- .cloak-startup,
- // Breadcrumbs and alerts on the top of the page
- .content-wrapper > .alert-wrapper,
- // Content on pages
- #content-body,
- // Prevent flashing of haml generated modal contents
- .modal-dialog {
- display: $display;
- }
-}
diff --git a/app/assets/stylesheets/startup/startup-dark.scss b/app/assets/stylesheets/startup/startup-dark.scss
deleted file mode 100644
index 60cbcffd506..00000000000
--- a/app/assets/stylesheets/startup/startup-dark.scss
+++ /dev/null
@@ -1,1928 +0,0 @@
-// DO NOT EDIT! This is auto-generated from "yarn run generate:startup_css"
-// Please see the feedback issue for more details and help:
-// https://gitlab.com/gitlab-org/gitlab/-/issues/331812
-@charset "UTF-8";
-:root {
- --white: #333238;
-}
-*,
-*::before,
-*::after {
- box-sizing: border-box;
-}
-html {
- font-family: sans-serif;
- line-height: 1.15;
-}
-aside,
-header {
- display: block;
-}
-body {
- margin: 0;
- font-family: var(--default-regular-font, "GitLab Sans"), -apple-system,
- BlinkMacSystemFont, "Segoe UI", Roboto, "Noto Sans", Ubuntu, Cantarell,
- "Helvetica Neue", sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
- "Segoe UI Symbol", "Noto Color Emoji";
- font-size: 1rem;
- font-weight: 400;
- line-height: 1.5;
- color: #ececef;
- text-align: left;
- background-color: #1f1e24;
-}
-ul {
- margin-top: 0;
- margin-bottom: 1rem;
-}
-ul ul {
- margin-bottom: 0;
-}
-strong {
- font-weight: bolder;
-}
-a {
- color: #428fdc;
- text-decoration: none;
- background-color: transparent;
-}
-a:not([href]):not([class]) {
- color: inherit;
- text-decoration: none;
-}
-kbd {
- font-family: var(--default-mono-font, "GitLab Mono"), "JetBrains Mono",
- "Menlo", "DejaVu Sans Mono", "Liberation Mono", "Consolas", "Ubuntu Mono",
- "Courier New", "andale mono", "lucida console", monospace;
- font-size: 1em;
-}
-img {
- vertical-align: middle;
- border-style: none;
-}
-svg {
- overflow: hidden;
- vertical-align: middle;
-}
-button {
- border-radius: 0;
-}
-input,
-button {
- margin: 0;
- font-family: inherit;
- font-size: inherit;
- line-height: inherit;
-}
-button,
-input {
- overflow: visible;
-}
-button {
- text-transform: none;
-}
-[role="button"] {
- cursor: pointer;
-}
-button:not(:disabled),
-[type="button"]:not(:disabled) {
- cursor: pointer;
-}
-button::-moz-focus-inner,
-[type="button"]::-moz-focus-inner {
- padding: 0;
- border-style: none;
-}
-[type="search"] {
- outline-offset: -2px;
-}
-.list-unstyled {
- padding-left: 0;
- list-style: none;
-}
-kbd {
- padding: 0.2rem 0.4rem;
- font-size: 90%;
- color: #333238;
- background-color: #ececef;
- border-radius: 0.2rem;
-}
-kbd kbd {
- padding: 0;
- font-size: 100%;
- font-weight: 600;
-}
-.container-fluid {
- width: 100%;
- padding-right: 15px;
- padding-left: 15px;
- margin-right: auto;
- margin-left: auto;
-}
-.form-control {
- display: block;
- width: 100%;
- height: 32px;
- padding: 0.375rem 0.75rem;
- font-size: 0.875rem;
- font-weight: 400;
- line-height: 1.5;
- color: #ececef;
- background-color: #333238;
- background-clip: padding-box;
- border: 1px solid #737278;
- border-radius: 0.25rem;
-}
-@media (prefers-reduced-motion: reduce) {
-}
-.form-control::placeholder {
- color: #a4a3a8;
- opacity: 1;
-}
-.form-control:disabled {
- background-color: #24232a;
- opacity: 1;
-}
-.btn {
- display: inline-block;
- font-weight: 400;
- color: #ececef;
- text-align: center;
- vertical-align: middle;
- user-select: none;
- background-color: transparent;
- border: 1px solid transparent;
- padding: 0.375rem 0.75rem;
- font-size: 1rem;
- line-height: 20px;
- border-radius: 0.25rem;
-}
-@media (prefers-reduced-motion: reduce) {
-}
-.btn:disabled {
- opacity: 0.65;
-}
-.btn:not(:disabled):not(.disabled) {
- cursor: pointer;
-}
-.collapse:not(.show) {
- display: none;
-}
-.dropdown {
- position: relative;
-}
-.dropdown-menu {
- position: absolute;
- top: 100%;
- left: 0;
- z-index: 1000;
- display: none;
- float: left;
- min-width: 10rem;
- padding: 0.5rem 0;
- margin: 0.125rem 0 0;
- font-size: 1rem;
- color: #ececef;
- text-align: left;
- list-style: none;
- background-color: #333238;
- background-clip: padding-box;
- border: 1px solid rgba(255, 255, 255, 0.15);
- border-radius: 0.25rem;
-}
-.nav {
- display: flex;
- flex-wrap: wrap;
- padding-left: 0;
- margin-bottom: 0;
- list-style: none;
-}
-.navbar {
- position: relative;
- display: flex;
- flex-wrap: wrap;
- align-items: center;
- justify-content: space-between;
- padding: 0.25rem 0.5rem;
-}
-.navbar .container-fluid {
- display: flex;
- flex-wrap: wrap;
- align-items: center;
- justify-content: space-between;
-}
-.navbar-nav {
- display: flex;
- flex-direction: column;
- padding-left: 0;
- margin-bottom: 0;
- list-style: none;
-}
-.navbar-nav .dropdown-menu {
- position: static;
- float: none;
-}
-.navbar-collapse {
- flex-basis: 100%;
- flex-grow: 1;
- align-items: center;
-}
-.navbar-toggler {
- padding: 0.25rem 0.75rem;
- font-size: 1.25rem;
- line-height: 1;
- background-color: transparent;
- border: 1px solid transparent;
- border-radius: 0.25rem;
-}
-@media (max-width: 575.98px) {
- .navbar-expand-sm > .container-fluid {
- padding-right: 0;
- padding-left: 0;
- }
-}
-@media (min-width: 576px) {
- .navbar-expand-sm {
- flex-flow: row nowrap;
- justify-content: flex-start;
- }
- .navbar-expand-sm .navbar-nav {
- flex-direction: row;
- }
- .navbar-expand-sm .navbar-nav .dropdown-menu {
- position: absolute;
- }
- .navbar-expand-sm > .container-fluid {
- flex-wrap: nowrap;
- }
- .navbar-expand-sm .navbar-collapse {
- display: flex !important;
- flex-basis: auto;
- }
- .navbar-expand-sm .navbar-toggler {
- display: none;
- }
-}
-.badge {
- display: inline-block;
- padding: 0.25em 0.4em;
- font-size: 75%;
- font-weight: 600;
- line-height: 1;
- text-align: center;
- white-space: nowrap;
- vertical-align: baseline;
- border-radius: 0.25rem;
-}
-@media (prefers-reduced-motion: reduce) {
-}
-.badge:empty {
- display: none;
-}
-.btn .badge {
- position: relative;
- top: -1px;
-}
-.badge-pill {
- padding-right: 0.6em;
- padding-left: 0.6em;
- border-radius: 10rem;
-}
-.badge-success {
- color: #fbfafd;
- background-color: #2da160;
-}
-.badge-info {
- color: #fbfafd;
- background-color: #428fdc;
-}
-.badge-warning {
- color: #fbfafd;
- background-color: #c17d10;
-}
-.rounded-circle {
- border-radius: 50% !important;
-}
-.d-none {
- display: none !important;
-}
-.d-block {
- display: block !important;
-}
-@media (min-width: 576px) {
- .d-sm-none {
- display: none !important;
- }
- .d-sm-inline-block {
- display: inline-block !important;
- }
-}
-@media (min-width: 768px) {
- .d-md-block {
- display: block !important;
- }
-}
-@media (min-width: 992px) {
- .d-lg-none {
- display: none !important;
- }
-}
-.sr-only {
- position: absolute;
- width: 1px;
- height: 1px;
- padding: 0;
- margin: -1px;
- overflow: hidden;
- clip: rect(0, 0, 0, 0);
- white-space: nowrap;
- border: 0;
-}
-.gl-avatar {
- display: inline-flex;
- border-width: 1px;
- border-style: solid;
- border-color: rgba(251, 250, 253, 0.08);
- overflow: hidden;
- flex-shrink: 0;
-}
-.gl-avatar-s24 {
- width: 1.5rem;
- height: 1.5rem;
- font-size: 0.75rem;
- line-height: 1rem;
- border-radius: 0.25rem;
-}
-.gl-avatar-circle {
- border-radius: 50%;
-}
-.gl-badge {
- display: inline-flex;
- align-items: center;
- font-size: 0.75rem;
- font-weight: 400;
- line-height: 1rem;
- padding-top: 0.25rem;
- padding-bottom: 0.25rem;
- padding-left: 0.5rem;
- padding-right: 0.5rem;
-}
-.gl-badge.sm {
- padding-top: 0;
- padding-bottom: 0;
-}
-.gl-badge.badge-info {
- background-color: #064787;
- color: #9dc7f1;
-}
-a.gl-badge.badge-info.active,
-a.gl-badge.badge-info:active {
- color: #e9f3fc;
- background-color: #0b5cad;
-}
-a.gl-badge.badge-info:active {
- box-shadow: 0 0 0 1px #333238, 0 0 0 3px #1f75cb;
- outline: none;
-}
-.gl-badge.badge-success {
- background-color: #0d532a;
- color: #91d4a8;
-}
-a.gl-badge.badge-success.active,
-a.gl-badge.badge-success:active {
- color: #ecf4ee;
- background-color: #24663b;
-}
-a.gl-badge.badge-success:active {
- box-shadow: 0 0 0 1px #333238, 0 0 0 3px #1f75cb;
- outline: none;
-}
-.gl-badge.badge-warning {
- background-color: #703800;
- color: #e9be74;
-}
-a.gl-badge.badge-warning.active,
-a.gl-badge.badge-warning:active {
- color: #fdf1dd;
- background-color: #8f4700;
-}
-a.gl-badge.badge-warning:active {
- box-shadow: 0 0 0 1px #333238, 0 0 0 3px #1f75cb;
- outline: none;
-}
-.gl-button .gl-badge {
- top: 0;
-}
-.gl-form-input,
-.gl-form-input.form-control {
- background-color: #333238;
- font-family: var(--default-regular-font, "GitLab Sans"), -apple-system,
- BlinkMacSystemFont, "Segoe UI", Roboto, "Noto Sans", Ubuntu, Cantarell,
- "Helvetica Neue", sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
- "Segoe UI Symbol", "Noto Color Emoji";
- font-size: 0.875rem;
- line-height: 1rem;
- padding-top: 0.5rem;
- padding-bottom: 0.5rem;
- padding-left: 0.75rem;
- padding-right: 0.75rem;
- height: auto;
- color: #ececef;
- box-shadow: inset 0 0 0 1px #737278;
- border-style: none;
- appearance: none;
- -moz-appearance: none;
-}
-.gl-form-input:disabled,
-.gl-form-input:not(.form-control-plaintext):not([type="color"]):read-only,
-.gl-form-input.form-control:disabled,
-.gl-form-input.form-control:not(.form-control-plaintext):not([type="color"]):read-only {
- background-color: #1f1e24;
- box-shadow: inset 0 0 0 1px #434248;
-}
-.gl-form-input:disabled,
-.gl-form-input.form-control:disabled {
- cursor: not-allowed;
- color: #89888d;
-}
-.gl-form-input::placeholder,
-.gl-form-input.form-control::placeholder {
- color: #737278;
-}
-.gl-icon {
- fill: currentColor;
-}
-.gl-icon.s12 {
- width: 12px;
- height: 12px;
-}
-.gl-icon.s16 {
- width: 16px;
- height: 16px;
-}
-.gl-icon.s32 {
- width: 32px;
- height: 32px;
-}
-.gl-link {
- font-size: 0.875rem;
- color: #428fdc;
-}
-.gl-link:active {
- color: #9dc7f1;
-}
-.gl-link:active {
- text-decoration: underline;
- outline: 2px solid #1f75cb;
- outline-offset: 2px;
-}
-.gl-button {
- display: inline-flex;
-}
-.gl-button:not(.btn-link):active {
- text-decoration: none;
-}
-.gl-button.gl-button {
- border-width: 0;
- padding-top: 0.5rem;
- padding-bottom: 0.5rem;
- padding-left: 0.75rem;
- padding-right: 0.75rem;
- background-color: transparent;
- line-height: 1rem;
- color: #ececef;
- fill: currentColor;
- box-shadow: inset 0 0 0 1px #535158;
- justify-content: center;
- align-items: center;
- font-size: 0.875rem;
- border-radius: 0.25rem;
-}
-.gl-button.gl-button .gl-button-text {
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- padding-top: 1px;
- padding-bottom: 1px;
- margin-top: -1px;
- margin-bottom: -1px;
-}
-.gl-button.gl-button.btn-default {
- background-color: #333238;
-}
-.gl-button.gl-button.btn-default:active,
-.gl-button.gl-button.btn-default.active {
- box-shadow: inset 0 0 0 1px #a4a3a8, 0 0 0 1px #333238, 0 0 0 3px #1f75cb;
- outline: none;
- background-color: #434248;
-}
-.gl-button.gl-button.btn-default:active .gl-icon,
-.gl-button.gl-button.btn-default.active .gl-icon {
- color: #ececef;
-}
-.gl-button.gl-button.btn-default .gl-icon {
- color: #89888d;
-}
-.gl-search-box-by-type-search-icon {
- color: #89888d;
- width: 1rem;
- position: absolute;
- left: 0.5rem;
- top: calc(50% - 16px / 2);
-}
-.gl-search-box-by-type {
- display: flex;
- position: relative;
-}
-.gl-search-box-by-type-input,
-.gl-search-box-by-type-input.gl-form-input {
- height: 2rem;
- padding-right: 2rem;
- padding-left: 1.75rem;
-}
-body {
- font-size: 0.875rem;
-}
-button,
-html [type="button"],
-[role="button"] {
- cursor: pointer;
-}
-strong {
- font-weight: bold;
-}
-svg {
- vertical-align: baseline;
-}
-.form-control {
- font-size: 0.875rem;
-}
-.hidden {
- display: none !important;
- visibility: hidden !important;
-}
-.badge:not(.gl-badge) {
- padding: 4px 5px;
- font-size: 12px;
- font-style: normal;
- font-weight: 400;
- display: inline-block;
-}
-.divider {
- height: 0;
- margin: 4px 0;
- overflow: hidden;
- border-top: 1px solid #434248;
-}
-.toggle-sidebar-button .collapse-text,
-.toggle-sidebar-button .icon-chevron-double-lg-left {
- color: #bfbfc3;
-}
-html {
- overflow-y: scroll;
-}
-.layout-page {
- padding-top: calc(
- var(--header-height, 48px) +
- calc(var(--system-header-height) + var(--performance-bar-height))
- );
- padding-bottom: var(--system-footer-height);
-}
-@media (min-width: 576px) {
- .logged-out-marketing-header {
- --header-height: 72px;
- }
-}
-.btn {
- border-radius: 4px;
- font-size: 0.875rem;
- font-weight: 400;
- padding: 6px 10px;
- background-color: #333238;
- border-color: #434248;
- color: #ececef;
- color: #ececef;
- white-space: nowrap;
-}
-.btn:active {
- background-color: #333238;
- box-shadow: none;
-}
-.btn:active,
-.btn.active {
- background-color: #434248;
- border-color: #4f4f4f;
- color: #ececef;
-}
-.btn svg {
- height: 15px;
- width: 15px;
-}
-.btn svg:not(:last-child) {
- margin-right: 5px;
-}
-.badge.badge-pill:not(.gl-badge) {
- font-weight: 400;
- background-color: rgba(255, 255, 255, 0.07);
- color: #bfbfc3;
- vertical-align: baseline;
-}
-:root {
- --performance-bar-height: 0px;
- --system-header-height: 0px;
- --top-bar-height: 0px;
- --system-footer-height: 0px;
- --mr-review-bar-height: 0px;
- --breakpoint-xs: 0;
- --breakpoint-sm: 576px;
- --breakpoint-md: 768px;
- --breakpoint-lg: 992px;
- --breakpoint-xl: 1200px;
-}
-.with-top-bar {
- --top-bar-height: 48px;
-}
-@media (min-width: 768px) {
- .page-with-contextual-sidebar {
- --application-bar-left: 56px;
- }
-}
-@media (min-width: 1200px) {
- .page-with-contextual-sidebar {
- --application-bar-left: 256px;
- }
- .page-with-icon-sidebar {
- --application-bar-left: 56px;
- }
- .page-with-super-sidebar {
- --application-bar-left: 256px;
- }
- .page-with-super-sidebar-collapsed {
- --application-bar-left: 0px;
- }
-}
-.gl-font-sm {
- font-size: 12px;
-}
-.dropdown {
- position: relative;
-}
-.dropdown-menu {
- display: none;
- position: absolute;
- width: auto;
- top: 100%;
- z-index: 300;
- min-width: 240px;
- max-width: 500px;
- margin-top: 4px;
- margin-bottom: 24px;
- font-size: 0.875rem;
- font-weight: 400;
- padding: 8px 0;
- background-color: #333238;
- border: 1px solid #434248;
- border-radius: 0.25rem;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
-}
-.dropdown-menu ul {
- margin: 0;
- padding: 0;
-}
-.dropdown-menu li {
- display: block;
- text-align: left;
- list-style: none;
-}
-.dropdown-menu li > a,
-.dropdown-menu li > button {
- background: transparent;
- border: 0;
- border-radius: 0;
- box-shadow: none;
- display: block;
- font-weight: 400;
- position: relative;
- padding: 8px 12px;
- color: #ececef;
- line-height: 16px;
- white-space: normal;
- overflow: hidden;
- text-align: left;
- width: 100%;
-}
-.dropdown-menu li > a:active,
-.dropdown-menu li > button:active {
- background-color: #4e4c53;
- color: #ececef;
- outline: 0;
- text-decoration: none;
-}
-.dropdown-menu li > a:active,
-.dropdown-menu li > button:active {
- box-shadow: inset 0 0 0 2px #1f75cb, inset 0 0 0 3px #333238,
- inset 0 0 0 1px #333238;
- outline: none;
-}
-.dropdown-menu .divider {
- height: 1px;
- margin: 0.25rem 0;
- padding: 0;
- background-color: #434248;
-}
-.dropdown-menu .badge.badge-pill + span:not(.badge):not(.badge-pill) {
- margin-right: 40px;
-}
-@media (max-width: 575.98px) {
- .navbar-gitlab li.dropdown {
- position: static;
- }
- .navbar-gitlab li.dropdown.user-counter {
- margin-left: 8px !important;
- }
- .navbar-gitlab li.dropdown.user-counter > a {
- padding: 0 4px !important;
- }
- header.navbar-gitlab .dropdown .dropdown-menu {
- width: 100%;
- min-width: 100%;
- }
-}
-input {
- border-radius: 0.25rem;
- color: #ececef;
- background-color: #333238;
-}
-input[type="search"] {
- appearance: textfield;
-}
-.form-control {
- border-radius: 4px;
- padding: 6px 10px;
-}
-.form-control::placeholder {
- color: #737278;
-}
-kbd {
- display: inline-block;
- padding: 3px 5px;
- font-size: 0.75rem;
- line-height: 10px;
- color: var(--gray-700, #bfbfc3);
- vertical-align: unset;
- background-color: var(--gray-10, #1f1e24);
- border-width: 1px;
- border-style: solid;
- border-color: var(--gray-100, #434248) var(--gray-100, #434248)
- var(--gray-200, #535158);
- border-image: none;
- border-radius: 3px;
- box-shadow: 0 -1px 0 var(--gray-200, #535158) inset;
-}
-.navbar-gitlab {
- padding: 0 16px;
- z-index: 1000;
- margin-bottom: 0;
- min-height: var(--header-height, 48px);
- border: 0;
- position: fixed;
- top: calc(var(--system-header-height) + var(--performance-bar-height));
- left: 0;
- right: 0;
- border-radius: 0;
-}
-.navbar-gitlab .close-icon {
- display: none;
-}
-.navbar-gitlab .header-content {
- width: 100%;
- display: flex;
- justify-content: space-between;
- position: relative;
- min-height: var(--header-height, 48px);
- padding-left: 0;
-}
-.navbar-gitlab .header-content .title {
- padding-right: 0;
- color: currentColor;
- display: flex;
- position: relative;
- margin: 0;
- font-size: 18px;
- vertical-align: top;
- white-space: nowrap;
-}
-.navbar-gitlab .header-content .title img {
- height: 24px;
-}
-.navbar-gitlab .header-content .title a:not(.canary-badge) {
- display: flex;
- align-items: center;
- padding: 2px 8px;
- margin: 4px 2px 4px -8px;
- border-radius: 4px;
-}
-.navbar-gitlab .header-content .title a:not(.canary-badge):active {
- box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.6), 0 0 0 3px #1068bf;
- outline: none;
-}
-.navbar-gitlab .header-content .navbar-collapse > ul.nav > li:not(.d-none) {
- margin: 0 2px;
-}
-.navbar-gitlab .header-search-form {
- min-width: 320px;
-}
-@media (min-width: 768px) and (max-width: 1199.98px) {
- .navbar-gitlab .header-search-form {
- min-width: 200px;
- }
-}
-.navbar-gitlab .header-search-form .keyboard-shortcut-helper {
- transform: translateY(calc(50% - 2px));
- box-shadow: none;
- border-color: transparent;
-}
-.navbar-gitlab .navbar-collapse {
- flex: 0 0 auto;
- border-top: 0;
- padding: 0;
-}
-@media (max-width: 575.98px) {
- .navbar-gitlab .navbar-collapse {
- flex: 1 1 auto;
- }
-}
-.navbar-gitlab .navbar-collapse .nav {
- flex-wrap: nowrap;
-}
-@media (max-width: 575.98px) {
- .navbar-gitlab .navbar-collapse .nav > li:not(.d-none) a {
- margin-left: 0;
- }
-}
-.navbar-gitlab .container-fluid {
- padding: 0;
-}
-.navbar-gitlab .container-fluid .user-counter svg {
- margin-right: 3px;
-}
-.navbar-gitlab .container-fluid .navbar-toggler {
- position: relative;
- right: -10px;
- border-radius: 0;
- min-width: 45px;
- padding: 0;
- margin: 8px 8px 8px 0;
- font-size: 14px;
- text-align: center;
- color: currentColor;
-}
-.navbar-gitlab .container-fluid .navbar-toggler.active {
- color: currentColor;
- background-color: transparent;
-}
-@media (max-width: 575.98px) {
- .navbar-gitlab .container-fluid .navbar-nav {
- display: flex;
- padding-right: 10px;
- flex-direction: row;
- }
-}
-.navbar-gitlab
- .container-fluid
- .navbar-nav
- li
- .badge.badge-pill:not(.gl-badge) {
- box-shadow: none;
- font-weight: 600;
-}
-@media (max-width: 575.98px) {
- .navbar-gitlab .container-fluid .nav > li.header-user {
- padding-left: 10px;
- }
-}
-.navbar-gitlab .container-fluid .nav > li > a {
- will-change: color;
- margin: 4px 0;
- padding: 6px 8px;
- height: 32px;
-}
-@media (max-width: 575.98px) {
- .navbar-gitlab .container-fluid .nav > li > a {
- padding: 0;
- }
-}
-.navbar-gitlab .container-fluid .nav > li > a.header-user-dropdown-toggle {
- margin-left: 2px;
-}
-.navbar-gitlab
- .container-fluid
- .nav
- > li
- > a.header-user-dropdown-toggle
- .header-user-avatar {
- margin-right: 0;
-}
-.navbar-gitlab .container-fluid .nav > li .header-new-dropdown-toggle {
- margin-right: 0;
-}
-.navbar-sub-nav > li > a,
-.navbar-sub-nav > li > button,
-.navbar-nav > li > a,
-.navbar-nav > li > button {
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 6px 8px;
- margin: 4px 2px;
- font-size: 12px;
- color: currentColor;
- border-radius: 4px;
- height: 32px;
- font-weight: 600;
-}
-.navbar-sub-nav > li > a:active,
-.navbar-sub-nav > li > button:active,
-.navbar-nav > li > a:active,
-.navbar-nav > li > button:active {
- box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.6), 0 0 0 3px #1068bf;
- outline: none;
-}
-.navbar-sub-nav > li .top-nav-toggle,
-.navbar-sub-nav > li > button,
-.navbar-nav > li .top-nav-toggle,
-.navbar-nav > li > button {
- background: transparent;
- border: 0;
-}
-.navbar-sub-nav .dropdown-menu,
-.navbar-nav .dropdown-menu {
- position: absolute;
-}
-.navbar-sub-nav {
- display: flex;
- align-items: center;
- height: 100%;
- margin: 0 0 0 6px;
-}
-.caret-down,
-.btn .caret-down {
- top: 0;
- height: 11px;
- width: 11px;
- margin-left: 4px;
- fill: currentColor;
-}
-.header-user .dropdown-menu,
-.header-new .dropdown-menu {
- margin-top: 4px;
-}
-@media (max-width: 575.98px) {
- .navbar-gitlab .container-fluid {
- font-size: 18px;
- }
- .navbar-gitlab .container-fluid .navbar-nav {
- table-layout: fixed;
- width: 100%;
- margin: 0;
- text-align: right;
- }
- .navbar-gitlab .container-fluid .navbar-collapse {
- margin-left: -8px;
- margin-right: -10px;
- }
- .navbar-gitlab .container-fluid .navbar-collapse .nav > li:not(.d-none) {
- flex: 1;
- }
- .header-user-dropdown-toggle {
- text-align: center;
- }
- .header-user-avatar {
- float: none;
- }
-}
-.header-user-avatar {
- float: left;
- margin-right: 5px;
- border-radius: 50%;
- border: 1px solid #333238;
-}
-.notification-dot {
- background-color: #9e5400;
- height: 12px;
- width: 12px;
- pointer-events: none;
- visibility: hidden;
- top: 3px;
-}
-.tanuki-logo .tanuki {
- fill: #e24329;
-}
-.tanuki-logo .left-cheek,
-.tanuki-logo .right-cheek {
- fill: #fc6d26;
-}
-.tanuki-logo .chin {
- fill: #fca326;
-}
-.context-header {
- position: relative;
- margin-right: 2px;
- width: 256px;
-}
-.context-header > a,
-.context-header > button {
- font-weight: 600;
- display: flex;
- width: 100%;
- align-items: center;
- padding: 10px 16px 10px 10px;
- color: #ececef;
- background-color: transparent;
- border: 0;
- text-align: left;
-}
-.context-header .avatar-container {
- flex: 0 0 32px;
- background-color: #333238;
-}
-.context-header .sidebar-context-title {
- overflow: hidden;
- text-overflow: ellipsis;
- color: #ececef;
-}
-@media (min-width: 768px) {
- .page-with-contextual-sidebar {
- padding-left: 56px;
- }
-}
-@media (min-width: 1200px) {
- .page-with-contextual-sidebar {
- padding-left: 256px;
- }
-}
-@media (min-width: 768px) {
- .page-with-icon-sidebar {
- padding-left: 56px;
- }
-}
-.nav-sidebar {
- position: fixed;
- bottom: var(--system-footer-height);
- left: 0;
- z-index: 600;
- width: 256px;
- top: calc(
- var(--header-height, 48px) +
- calc(var(--system-header-height) + var(--performance-bar-height)) +
- var(--top-bar-height)
- );
- background-color: #1f1e24;
- border-right: 1px solid #e9e9e9;
- transform: translate3d(0, 0, 0);
-}
-.nav-sidebar.sidebar-collapsed-desktop {
- width: 56px;
-}
-.nav-sidebar.sidebar-collapsed-desktop .nav-sidebar-inner-scroll {
- overflow-x: hidden;
-}
-.nav-sidebar.sidebar-collapsed-desktop .badge.badge-pill:not(.fly-out-badge),
-.nav-sidebar.sidebar-collapsed-desktop .nav-item-name,
-.nav-sidebar.sidebar-collapsed-desktop .collapse-text {
- border: 0;
- clip: rect(0, 0, 0, 0);
- height: 1px;
- margin: -1px;
- overflow: hidden;
- padding: 0;
- position: absolute;
- white-space: nowrap;
- width: 1px;
-}
-.nav-sidebar.sidebar-collapsed-desktop .sidebar-top-level-items > li > a {
- min-height: unset;
-}
-.nav-sidebar.sidebar-collapsed-desktop .fly-out-top-item:not(.divider) {
- display: block !important;
-}
-.nav-sidebar.sidebar-collapsed-desktop .avatar-container {
- margin: 0 auto;
-}
-.nav-sidebar.sidebar-collapsed-desktop li.active:not(.fly-out-top-item) > a {
- background-color: rgba(41, 41, 97, 0.08);
-}
-.nav-sidebar a {
- text-decoration: none;
- color: #ececef;
-}
-.nav-sidebar li {
- white-space: nowrap;
-}
-.nav-sidebar li .nav-item-name {
- flex: 1;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-.nav-sidebar li > a,
-.nav-sidebar li > .fly-out-top-item-container {
- height: 2rem;
- padding-left: 0.75rem;
- padding-right: 0.75rem;
- display: flex;
- align-items: center;
- border-radius: 0.25rem;
- width: auto;
- margin: 1px 8px;
-}
-.nav-sidebar li.active > a {
- font-weight: 600;
-}
-.nav-sidebar li.active:not(.fly-out-top-item) > a:not(.has-sub-items) {
- background-color: rgba(251, 250, 253, 0.08);
-}
-.nav-sidebar ul {
- padding-left: 0;
- list-style: none;
-}
-@media (max-width: 767.98px) {
- .nav-sidebar {
- left: -256px;
- }
-}
-.nav-sidebar .nav-icon-container {
- display: flex;
- margin-right: 8px;
-}
-.nav-sidebar
- a:not(.has-sub-items)
- + .sidebar-sub-level-items
- .fly-out-top-item {
- display: none;
-}
-.nav-sidebar
- a:not(.has-sub-items)
- + .sidebar-sub-level-items
- .fly-out-top-item
- a,
-.nav-sidebar
- a:not(.has-sub-items)
- + .sidebar-sub-level-items
- .fly-out-top-item.active
- a,
-.nav-sidebar
- a:not(.has-sub-items)
- + .sidebar-sub-level-items
- .fly-out-top-item
- .fly-out-top-item-container {
- margin-left: 0;
- margin-right: 0;
- padding-left: 1rem;
- padding-right: 1rem;
- cursor: default;
- pointer-events: none;
- font-size: 0.75rem;
- margin-top: -0.25rem;
- margin-bottom: -0.25rem;
- margin-top: 0;
- position: relative;
- color: #333238;
- background: var(--black, #fff);
-}
-.nav-sidebar
- a:not(.has-sub-items)
- + .sidebar-sub-level-items
- .fly-out-top-item
- a
- strong,
-.nav-sidebar
- a:not(.has-sub-items)
- + .sidebar-sub-level-items
- .fly-out-top-item.active
- a
- strong,
-.nav-sidebar
- a:not(.has-sub-items)
- + .sidebar-sub-level-items
- .fly-out-top-item
- .fly-out-top-item-container
- strong {
- font-weight: 400;
-}
-.nav-sidebar
- a:not(.has-sub-items)
- + .sidebar-sub-level-items
- .fly-out-top-item
- a::before,
-.nav-sidebar
- a:not(.has-sub-items)
- + .sidebar-sub-level-items
- .fly-out-top-item.active
- a::before,
-.nav-sidebar
- a:not(.has-sub-items)
- + .sidebar-sub-level-items
- .fly-out-top-item
- .fly-out-top-item-container::before {
- position: absolute;
- content: "";
- display: block;
- top: 50%;
- left: -0.25rem;
- margin-top: -0.25rem;
- width: 0;
- height: 0;
- border-top: 0.25rem solid transparent;
- border-bottom: 0.25rem solid transparent;
- border-right: 0.25rem solid #fff;
- border-right-color: var(--black, #fff);
-}
-@media (min-width: 576px) {
- .nav-sidebar a.has-sub-items + .sidebar-sub-level-items {
- min-width: 150px;
- }
-}
-.nav-sidebar a.has-sub-items + .sidebar-sub-level-items .fly-out-top-item {
- display: none;
-}
-.nav-sidebar a.has-sub-items + .sidebar-sub-level-items .fly-out-top-item a,
-.nav-sidebar
- a.has-sub-items
- + .sidebar-sub-level-items
- .fly-out-top-item.active
- a,
-.nav-sidebar
- a.has-sub-items
- + .sidebar-sub-level-items
- .fly-out-top-item
- .fly-out-top-item-container {
- margin-left: 0;
- margin-right: 0;
- padding-left: 1rem;
- padding-right: 1rem;
- cursor: default;
- pointer-events: none;
- font-size: 0.75rem;
- margin-top: 0;
- border-bottom-left-radius: 0;
- border-bottom-right-radius: 0;
-}
-@media (min-width: 768px) and (max-width: 1199px) {
- .nav-sidebar:not(.sidebar-expanded-mobile) {
- width: 56px;
- }
- .nav-sidebar:not(.sidebar-expanded-mobile) .nav-sidebar-inner-scroll {
- overflow-x: hidden;
- }
- .nav-sidebar:not(.sidebar-expanded-mobile)
- .badge.badge-pill:not(.fly-out-badge),
- .nav-sidebar:not(.sidebar-expanded-mobile) .nav-item-name,
- .nav-sidebar:not(.sidebar-expanded-mobile) .collapse-text {
- border: 0;
- clip: rect(0, 0, 0, 0);
- height: 1px;
- margin: -1px;
- overflow: hidden;
- padding: 0;
- position: absolute;
- white-space: nowrap;
- width: 1px;
- }
- .nav-sidebar:not(.sidebar-expanded-mobile) .sidebar-top-level-items > li > a {
- min-height: unset;
- }
- .nav-sidebar:not(.sidebar-expanded-mobile) .fly-out-top-item:not(.divider) {
- display: block !important;
- }
- .nav-sidebar:not(.sidebar-expanded-mobile) .avatar-container {
- margin: 0 auto;
- }
- .nav-sidebar:not(.sidebar-expanded-mobile)
- li.active:not(.fly-out-top-item)
- > a {
- background-color: rgba(41, 41, 97, 0.08);
- }
- .nav-sidebar:not(.sidebar-expanded-mobile) .context-header {
- height: 60px;
- width: 56px;
- }
- .nav-sidebar:not(.sidebar-expanded-mobile) .context-header a {
- padding: 10px 4px;
- }
- .nav-sidebar:not(.sidebar-expanded-mobile) .sidebar-context-title {
- border: 0;
- clip: rect(0, 0, 0, 0);
- height: 1px;
- margin: -1px;
- overflow: hidden;
- padding: 0;
- position: absolute;
- white-space: nowrap;
- width: 1px;
- }
- .nav-sidebar:not(.sidebar-expanded-mobile) .context-header {
- height: auto;
- }
- .nav-sidebar:not(.sidebar-expanded-mobile) .context-header a {
- padding: 0.25rem;
- }
- .nav-sidebar:not(.sidebar-expanded-mobile)
- .sidebar-top-level-items
- > li
- .sidebar-sub-level-items:not(.flyout-list) {
- display: none;
- }
- .nav-sidebar:not(.sidebar-expanded-mobile) .nav-icon-container {
- margin-right: 0;
- }
- .nav-sidebar:not(.sidebar-expanded-mobile) .toggle-sidebar-button {
- width: 55px;
- padding: 0 21px;
- }
- .nav-sidebar:not(.sidebar-expanded-mobile)
- .toggle-sidebar-button
- .collapse-text {
- display: none;
- }
- .nav-sidebar:not(.sidebar-expanded-mobile)
- .toggle-sidebar-button
- .icon-chevron-double-lg-left {
- transform: rotate(180deg);
- margin: 0;
- }
-}
-.nav-sidebar-inner-scroll {
- height: 100%;
- width: 100%;
- overflow-x: hidden;
- overflow-y: auto;
-}
-.nav-sidebar-inner-scroll > div.context-header {
- margin-top: 0.25rem;
-}
-.nav-sidebar-inner-scroll > div.context-header a {
- height: 2rem;
- padding-left: 0.75rem;
- padding-right: 0.75rem;
- display: flex;
- align-items: center;
- border-radius: 0.25rem;
- width: auto;
- margin: 1px 8px;
- padding: 0.25rem;
- margin-bottom: 0.25rem;
- margin-top: 0.125rem;
- height: auto;
-}
-.nav-sidebar-inner-scroll > div.context-header a .avatar-container {
- font-weight: 400;
- flex: none;
-}
-.sidebar-top-level-items {
- margin-bottom: 60px;
-}
-.sidebar-top-level-items .context-header a {
- padding: 0.25rem;
- margin-bottom: 0.25rem;
- margin-top: 0.125rem;
- height: auto;
-}
-.sidebar-top-level-items .context-header a .avatar-container {
- font-weight: 400;
- flex: none;
-}
-.sidebar-top-level-items
- > li.active
- .sidebar-sub-level-items:not(.is-fly-out-only) {
- display: block;
-}
-.sidebar-top-level-items li > a.gl-link {
- color: #ececef;
-}
-.sidebar-top-level-items li > a.gl-link:active {
- text-decoration: none;
-}
-.sidebar-sub-level-items {
- padding-top: 0;
- padding-bottom: 0;
- display: none;
-}
-.sidebar-sub-level-items:not(.fly-out-list) li > a {
- padding-left: 2.25rem;
-}
-.toggle-sidebar-button,
-.close-nav-button {
- height: 48px;
- padding: 0 16px;
- background-color: #24232a;
- border: 0;
- color: #bfbfc3;
- display: flex;
- align-items: center;
- background-color: #1f1e24;
- position: fixed;
- bottom: 0;
- width: 255px;
-}
-.toggle-sidebar-button .collapse-text,
-.toggle-sidebar-button .icon-chevron-double-lg-left,
-.close-nav-button .collapse-text,
-.close-nav-button .icon-chevron-double-lg-left {
- color: inherit;
-}
-.collapse-text {
- white-space: nowrap;
- overflow: hidden;
-}
-.sidebar-collapsed-desktop .context-header {
- height: 60px;
- width: 56px;
-}
-.sidebar-collapsed-desktop .context-header a {
- padding: 10px 4px;
-}
-.sidebar-collapsed-desktop .sidebar-context-title {
- border: 0;
- clip: rect(0, 0, 0, 0);
- height: 1px;
- margin: -1px;
- overflow: hidden;
- padding: 0;
- position: absolute;
- white-space: nowrap;
- width: 1px;
-}
-.sidebar-collapsed-desktop .context-header {
- height: auto;
-}
-.sidebar-collapsed-desktop .context-header a {
- padding: 0.25rem;
-}
-.sidebar-collapsed-desktop
- .sidebar-top-level-items
- > li
- .sidebar-sub-level-items:not(.flyout-list) {
- display: none;
-}
-.sidebar-collapsed-desktop .nav-icon-container {
- margin-right: 0;
-}
-.sidebar-collapsed-desktop .toggle-sidebar-button {
- width: 55px;
- padding: 0 21px;
-}
-.sidebar-collapsed-desktop .toggle-sidebar-button .collapse-text {
- display: none;
-}
-.sidebar-collapsed-desktop .toggle-sidebar-button .icon-chevron-double-lg-left {
- transform: rotate(180deg);
- margin: 0;
-}
-.close-nav-button {
- display: none;
-}
-@media (max-width: 767.98px) {
- .close-nav-button {
- display: flex;
- }
- .toggle-sidebar-button {
- display: none;
- }
-}
-.super-sidebar {
- display: flex;
- flex-direction: column;
- position: fixed;
- top: calc(
- var(--header-height, 48px) +
- calc(var(--system-header-height) + var(--performance-bar-height))
- );
- bottom: var(--system-footer-height);
- left: 0;
- background-color: var(--gray-10, #1f1e24);
- border-right: 1px solid rgba(251, 250, 253, 0.08);
- transform: translate3d(0, 0, 0);
- width: 256px;
- z-index: 600;
-}
-.super-sidebar.super-sidebar-loading {
- transform: translate3d(-100%, 0, 0);
-}
-@media (min-width: 1200px) {
- .super-sidebar.super-sidebar-loading {
- transform: translate3d(0, 0, 0);
- }
-}
-@media (prefers-reduced-motion: no-preference) {
-}
-.page-with-super-sidebar {
- padding-left: 0;
-}
-@media (prefers-reduced-motion: no-preference) {
-}
-@media (min-width: 1200px) {
- .page-with-super-sidebar {
- padding-left: 256px;
- }
-}
-.page-with-super-sidebar-collapsed .super-sidebar {
- transform: translate3d(-100%, 0, 0);
-}
-@media (min-width: 1200px) {
- .page-with-super-sidebar-collapsed {
- padding-left: 0;
- }
-}
-input::-moz-placeholder {
- color: #737278;
- opacity: 1;
-}
-input::-ms-input-placeholder {
- color: #737278;
-}
-input:-ms-input-placeholder {
- color: #737278;
-}
-svg {
- fill: currentColor;
-}
-svg.s12 {
- width: 12px;
- height: 12px;
-}
-svg.s16 {
- width: 16px;
- height: 16px;
-}
-svg.s32 {
- width: 32px;
- height: 32px;
-}
-svg.s12 {
- vertical-align: -1px;
-}
-svg.s16 {
- vertical-align: -3px;
-}
-.avatar,
-.avatar-container {
- float: left;
- margin-right: 16px;
- border-radius: 50%;
-}
-.avatar.s16,
-.avatar-container.s16 {
- width: 16px;
- height: 16px;
- margin-right: 8px;
-}
-.avatar.s32,
-.avatar-container.s32 {
- width: 32px;
- height: 32px;
- margin-right: 8px;
-}
-.avatar {
- transition-property: none;
- width: 40px;
- height: 40px;
- padding: 0;
- background: #212027;
- overflow: hidden;
- box-shadow: inset 0 0 0 1px rgba(251, 250, 253, 0.1);
-}
-.avatar.avatar-tile {
- border-radius: 0;
- border: 0;
-}
-.identicon {
- text-align: center;
- vertical-align: top;
- color: #ececef;
- background-color: #333238;
-}
-.identicon.s16 {
- font-size: 10px;
- line-height: 16px;
-}
-.identicon.s32 {
- font-size: 14px;
- line-height: 32px;
-}
-.identicon.bg1 {
- background-color: #660e00;
-}
-.identicon.bg2 {
- background-color: #232150;
-}
-.identicon.bg3 {
- background-color: #1a1a40;
-}
-.identicon.bg4 {
- background-color: #033464;
-}
-.identicon.bg5 {
- background-color: #0a4020;
-}
-.identicon.bg6 {
- background-color: #5c2900;
-}
-.identicon.bg7 {
- background-color: #333238;
-}
-.avatar-container {
- overflow: hidden;
- display: flex;
-}
-.avatar-container a {
- width: 100%;
- height: 100%;
- display: flex;
- text-decoration: none;
-}
-.avatar-container .avatar {
- border-radius: 0;
- border: 0;
- height: auto;
- width: 100%;
- margin: 0;
- align-self: center;
-}
-.rect-avatar {
- border-radius: 2px;
-}
-.rect-avatar.s16 {
- border-radius: 2px;
-}
-.rect-avatar.s16 .avatar {
- border-radius: 2px;
-}
-.rect-avatar.s32 {
- border-radius: 4px;
-}
-.rect-avatar.s32 .avatar {
- border-radius: 4px;
-}
-:root {
- color-scheme: dark;
- --gray-10: #1f1e24;
- --gray-50: #333238;
- --gray-100: #434248;
- --gray-200: #535158;
- --gray-700: #bfbfc3;
- --gray-900: #ececef;
- --border-color: #434248;
- --white: #333238;
- --black: #fff;
-}
-body.gl-dark {
- color-scheme: dark;
- --gray-10: #1f1e24;
- --border-color: #434248;
- --white: #333238;
- --black: #fff;
-}
-.nav-sidebar,
-.toggle-sidebar-button,
-.close-nav-button {
- background-color: #29282d;
- border-right: 1px solid #333238;
-}
-.gl-avatar:not(.gl-avatar-identicon),
-.avatar-container,
-.avatar {
- background: rgba(251, 250, 253, 0.04);
-}
-.gl-avatar {
- border-style: none;
- box-shadow: inset 0 0 0 1px rgba(251, 250, 253, 0.1);
-}
-body.gl-dark {
- --gl-theme-accent: #737278;
-}
-body.gl-dark .navbar-gitlab {
- background-color: #ececef;
-}
-body.gl-dark .navbar-gitlab .navbar-collapse {
- color: #ececef;
-}
-body.gl-dark .navbar-gitlab .container-fluid .navbar-toggler {
- border-left: 1px solid #a3a2a6;
- color: #ececef;
-}
-body.gl-dark .navbar-gitlab .navbar-sub-nav > li.active > a,
-body.gl-dark .navbar-gitlab .navbar-sub-nav > li.active > button,
-body.gl-dark .navbar-gitlab .navbar-nav > li.active > a,
-body.gl-dark .navbar-gitlab .navbar-nav > li.active > button {
- color: #ececef;
- background-color: #333238;
-}
-body.gl-dark .navbar-gitlab .navbar-sub-nav {
- color: #ececef;
-}
-body.gl-dark .navbar-gitlab .nav > li {
- color: #ececef;
-}
-body.gl-dark .navbar-gitlab .nav > li.header-search {
- color: #ececef;
-}
-body.gl-dark .navbar-gitlab .nav > li > a .notification-dot {
- border: 2px solid #ececef;
-}
-body.gl-dark
- .navbar-gitlab
- .nav
- > li
- > a.header-help-dropdown-toggle
- .notification-dot {
- background-color: #ececef;
-}
-body.gl-dark
- .navbar-gitlab
- .nav
- > li
- > a.header-user-dropdown-toggle
- .header-user-avatar {
- border-color: #ececef;
-}
-body.gl-dark .navbar-gitlab .nav > li.active > a {
- color: #ececef;
- background-color: #333238;
-}
-body.gl-dark .navbar-gitlab .nav > li.active > a .notification-dot {
- border-color: #333238;
-}
-body.gl-dark
- .navbar-gitlab
- .nav
- > li.active
- > a.header-help-dropdown-toggle
- .notification-dot {
- background-color: #ececef;
-}
-body.gl-dark .header-search-form {
- background-color: rgba(236, 236, 239, 0.2) !important;
- border-radius: 4px;
-}
-body.gl-dark .header-search-form svg.gl-search-box-by-type-search-icon {
- color: rgba(236, 236, 239, 0.8);
-}
-body.gl-dark .header-search-form input {
- background-color: transparent;
- color: rgba(236, 236, 239, 0.8);
- box-shadow: inset 0 0 0 1px rgba(236, 236, 239, 0.4);
-}
-body.gl-dark .header-search-form input::placeholder {
- color: rgba(236, 236, 239, 0.8);
-}
-body.gl-dark .header-search-form input:active::placeholder {
- color: #737278;
-}
-body.gl-dark .header-search-form .keyboard-shortcut-helper {
- color: #ececef;
- background-color: rgba(236, 236, 239, 0.2);
-}
-body.gl-dark .nav-sidebar li.active > a {
- color: #ececef;
-}
-body.gl-dark .nav-sidebar .fly-out-top-item a,
-body.gl-dark .nav-sidebar .fly-out-top-item.active a,
-body.gl-dark .nav-sidebar .fly-out-top-item .fly-out-top-item-container {
- background-color: var(--gray-100, #333238);
- color: var(--gray-900, #ececef);
-}
-body.gl-dark .navbar-gitlab {
- background-color: var(--gray-50);
- box-shadow: 0 1px 0 0 var(--gray-100);
-}
-body.gl-dark .navbar-gitlab .navbar-sub-nav li.active > a,
-body.gl-dark .navbar-gitlab .navbar-sub-nav li.active > button,
-body.gl-dark .navbar-gitlab .navbar-nav li.active > a,
-body.gl-dark .navbar-gitlab .navbar-nav li.active > button {
- color: var(--gl-text-color);
- background-color: var(--gray-200);
-}
-body.gl-dark .navbar-gitlab .header-search-form {
- background-color: var(--gray-100) !important;
- box-shadow: inset 0 0 0 1px var(--border-color) !important;
-}
-body.gl-dark .navbar-gitlab .header-search-form:active {
- background-color: var(--gray-100) !important;
- box-shadow: inset 0 0 0 1px var(--blue-200) !important;
-}
-
-.tab-width-8 {
- tab-size: 8;
-}
-.gl-sr-only {
- border: 0;
- clip: rect(0, 0, 0, 0);
- height: 1px;
- margin: -1px;
- overflow: hidden;
- padding: 0;
- position: absolute;
- white-space: nowrap;
- width: 1px;
-}
-.gl-border-none\! {
- border-style: none !important;
-}
-.gl-display-none {
- display: none;
-}
-.gl-display-flex {
- display: flex;
-}
-@media (min-width: 992px) {
- .gl-lg-display-flex {
- display: flex;
- }
-}
-@media (min-width: 576px) {
- .gl-sm-display-block {
- display: block;
- }
-}
-@media (min-width: 992px) {
- .gl-lg-display-block {
- display: block;
- }
-}
-.gl-align-items-center {
- align-items: center;
-}
-.gl-align-items-stretch {
- align-items: stretch;
-}
-.gl-flex-grow-0\! {
- flex-grow: 0 !important;
-}
-.gl-flex-grow-1 {
- flex-grow: 1;
-}
-.gl-flex-basis-half\! {
- flex-basis: 50% !important;
-}
-.gl-justify-content-end {
- justify-content: flex-end;
-}
-.gl-relative {
- position: relative;
-}
-.gl-absolute {
- position: absolute;
-}
-.gl-top-0 {
- top: 0;
-}
-.gl-right-3 {
- right: 0.5rem;
-}
-.gl-w-full {
- width: 100%;
-}
-.gl-px-3 {
- padding-left: 0.5rem;
- padding-right: 0.5rem;
-}
-.gl-pr-2 {
- padding-right: 0.25rem;
-}
-.gl-pt-0 {
- padding-top: 0;
-}
-.gl-mr-auto {
- margin-right: auto;
-}
-.gl-mr-3 {
- margin-right: 0.5rem;
-}
-.gl-ml-n2 {
- margin-left: -0.25rem;
-}
-.gl-ml-3 {
- margin-left: 0.5rem;
-}
-.gl-mx-0\! {
- margin-left: 0 !important;
- margin-right: 0 !important;
-}
-.gl-text-right {
- text-align: right;
-}
-.gl-white-space-nowrap {
- white-space: nowrap;
-}
-.gl-font-sm {
- font-size: 0.75rem;
-}
-.gl-font-weight-bold {
- font-weight: 600;
-}
-.gl-z-index-1 {
- z-index: 1;
-}
-
-@import "startup/cloaking";
-@include cloak-startup-scss(none);
diff --git a/app/assets/stylesheets/startup/startup-general.scss b/app/assets/stylesheets/startup/startup-general.scss
deleted file mode 100644
index 04c44dd9603..00000000000
--- a/app/assets/stylesheets/startup/startup-general.scss
+++ /dev/null
@@ -1,1781 +0,0 @@
-// DO NOT EDIT! This is auto-generated from "yarn run generate:startup_css"
-// Please see the feedback issue for more details and help:
-// https://gitlab.com/gitlab-org/gitlab/-/issues/331812
-@charset "UTF-8";
-:root {
- --white: #fff;
-}
-*,
-*::before,
-*::after {
- box-sizing: border-box;
-}
-html {
- font-family: sans-serif;
- line-height: 1.15;
-}
-aside,
-header {
- display: block;
-}
-body {
- margin: 0;
- font-family: var(--default-regular-font, "GitLab Sans"), -apple-system,
- BlinkMacSystemFont, "Segoe UI", Roboto, "Noto Sans", Ubuntu, Cantarell,
- "Helvetica Neue", sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
- "Segoe UI Symbol", "Noto Color Emoji";
- font-size: 1rem;
- font-weight: 400;
- line-height: 1.5;
- color: #333238;
- text-align: left;
- background-color: #fff;
-}
-ul {
- margin-top: 0;
- margin-bottom: 1rem;
-}
-ul ul {
- margin-bottom: 0;
-}
-strong {
- font-weight: bolder;
-}
-a {
- color: #1f75cb;
- text-decoration: none;
- background-color: transparent;
-}
-a:not([href]):not([class]) {
- color: inherit;
- text-decoration: none;
-}
-kbd {
- font-family: var(--default-mono-font, "GitLab Mono"), "JetBrains Mono",
- "Menlo", "DejaVu Sans Mono", "Liberation Mono", "Consolas", "Ubuntu Mono",
- "Courier New", "andale mono", "lucida console", monospace;
- font-size: 1em;
-}
-img {
- vertical-align: middle;
- border-style: none;
-}
-svg {
- overflow: hidden;
- vertical-align: middle;
-}
-button {
- border-radius: 0;
-}
-input,
-button {
- margin: 0;
- font-family: inherit;
- font-size: inherit;
- line-height: inherit;
-}
-button,
-input {
- overflow: visible;
-}
-button {
- text-transform: none;
-}
-[role="button"] {
- cursor: pointer;
-}
-button:not(:disabled),
-[type="button"]:not(:disabled) {
- cursor: pointer;
-}
-button::-moz-focus-inner,
-[type="button"]::-moz-focus-inner {
- padding: 0;
- border-style: none;
-}
-[type="search"] {
- outline-offset: -2px;
-}
-.list-unstyled {
- padding-left: 0;
- list-style: none;
-}
-kbd {
- padding: 0.2rem 0.4rem;
- font-size: 90%;
- color: #fff;
- background-color: #333238;
- border-radius: 0.2rem;
-}
-kbd kbd {
- padding: 0;
- font-size: 100%;
- font-weight: 600;
-}
-.container-fluid {
- width: 100%;
- padding-right: 15px;
- padding-left: 15px;
- margin-right: auto;
- margin-left: auto;
-}
-.form-control {
- display: block;
- width: 100%;
- height: 32px;
- padding: 0.375rem 0.75rem;
- font-size: 0.875rem;
- font-weight: 400;
- line-height: 1.5;
- color: #333238;
- background-color: #fff;
- background-clip: padding-box;
- border: 1px solid #89888d;
- border-radius: 0.25rem;
-}
-@media (prefers-reduced-motion: reduce) {
-}
-.form-control::placeholder {
- color: #626168;
- opacity: 1;
-}
-.form-control:disabled {
- background-color: #fbfafd;
- opacity: 1;
-}
-.btn {
- display: inline-block;
- font-weight: 400;
- color: #333238;
- text-align: center;
- vertical-align: middle;
- user-select: none;
- background-color: transparent;
- border: 1px solid transparent;
- padding: 0.375rem 0.75rem;
- font-size: 1rem;
- line-height: 20px;
- border-radius: 0.25rem;
-}
-@media (prefers-reduced-motion: reduce) {
-}
-.btn:disabled {
- opacity: 0.65;
-}
-.btn:not(:disabled):not(.disabled) {
- cursor: pointer;
-}
-.collapse:not(.show) {
- display: none;
-}
-.dropdown {
- position: relative;
-}
-.dropdown-menu {
- position: absolute;
- top: 100%;
- left: 0;
- z-index: 1000;
- display: none;
- float: left;
- min-width: 10rem;
- padding: 0.5rem 0;
- margin: 0.125rem 0 0;
- font-size: 1rem;
- color: #333238;
- text-align: left;
- list-style: none;
- background-color: #fff;
- background-clip: padding-box;
- border: 1px solid rgba(0, 0, 0, 0.15);
- border-radius: 0.25rem;
-}
-.nav {
- display: flex;
- flex-wrap: wrap;
- padding-left: 0;
- margin-bottom: 0;
- list-style: none;
-}
-.navbar {
- position: relative;
- display: flex;
- flex-wrap: wrap;
- align-items: center;
- justify-content: space-between;
- padding: 0.25rem 0.5rem;
-}
-.navbar .container-fluid {
- display: flex;
- flex-wrap: wrap;
- align-items: center;
- justify-content: space-between;
-}
-.navbar-nav {
- display: flex;
- flex-direction: column;
- padding-left: 0;
- margin-bottom: 0;
- list-style: none;
-}
-.navbar-nav .dropdown-menu {
- position: static;
- float: none;
-}
-.navbar-collapse {
- flex-basis: 100%;
- flex-grow: 1;
- align-items: center;
-}
-.navbar-toggler {
- padding: 0.25rem 0.75rem;
- font-size: 1.25rem;
- line-height: 1;
- background-color: transparent;
- border: 1px solid transparent;
- border-radius: 0.25rem;
-}
-@media (max-width: 575.98px) {
- .navbar-expand-sm > .container-fluid {
- padding-right: 0;
- padding-left: 0;
- }
-}
-@media (min-width: 576px) {
- .navbar-expand-sm {
- flex-flow: row nowrap;
- justify-content: flex-start;
- }
- .navbar-expand-sm .navbar-nav {
- flex-direction: row;
- }
- .navbar-expand-sm .navbar-nav .dropdown-menu {
- position: absolute;
- }
- .navbar-expand-sm > .container-fluid {
- flex-wrap: nowrap;
- }
- .navbar-expand-sm .navbar-collapse {
- display: flex !important;
- flex-basis: auto;
- }
- .navbar-expand-sm .navbar-toggler {
- display: none;
- }
-}
-.badge {
- display: inline-block;
- padding: 0.25em 0.4em;
- font-size: 75%;
- font-weight: 600;
- line-height: 1;
- text-align: center;
- white-space: nowrap;
- vertical-align: baseline;
- border-radius: 0.25rem;
-}
-@media (prefers-reduced-motion: reduce) {
-}
-.badge:empty {
- display: none;
-}
-.btn .badge {
- position: relative;
- top: -1px;
-}
-.badge-pill {
- padding-right: 0.6em;
- padding-left: 0.6em;
- border-radius: 10rem;
-}
-.badge-success {
- color: #fff;
- background-color: #108548;
-}
-.badge-info {
- color: #fff;
- background-color: #1f75cb;
-}
-.badge-warning {
- color: #fff;
- background-color: #ab6100;
-}
-.rounded-circle {
- border-radius: 50% !important;
-}
-.d-none {
- display: none !important;
-}
-.d-block {
- display: block !important;
-}
-@media (min-width: 576px) {
- .d-sm-none {
- display: none !important;
- }
- .d-sm-inline-block {
- display: inline-block !important;
- }
-}
-@media (min-width: 768px) {
- .d-md-block {
- display: block !important;
- }
-}
-@media (min-width: 992px) {
- .d-lg-none {
- display: none !important;
- }
-}
-.sr-only {
- position: absolute;
- width: 1px;
- height: 1px;
- padding: 0;
- margin: -1px;
- overflow: hidden;
- clip: rect(0, 0, 0, 0);
- white-space: nowrap;
- border: 0;
-}
-.gl-avatar {
- display: inline-flex;
- border-width: 1px;
- border-style: solid;
- border-color: rgba(31, 30, 36, 0.08);
- overflow: hidden;
- flex-shrink: 0;
-}
-.gl-avatar-s24 {
- width: 1.5rem;
- height: 1.5rem;
- font-size: 0.75rem;
- line-height: 1rem;
- border-radius: 0.25rem;
-}
-.gl-avatar-circle {
- border-radius: 50%;
-}
-.gl-badge {
- display: inline-flex;
- align-items: center;
- font-size: 0.75rem;
- font-weight: 400;
- line-height: 1rem;
- padding-top: 0.25rem;
- padding-bottom: 0.25rem;
- padding-left: 0.5rem;
- padding-right: 0.5rem;
-}
-.gl-badge.sm {
- padding-top: 0;
- padding-bottom: 0;
-}
-.gl-badge.badge-info {
- background-color: #cbe2f9;
- color: #0b5cad;
-}
-a.gl-badge.badge-info.active,
-a.gl-badge.badge-info:active {
- color: #033464;
- background-color: #9dc7f1;
-}
-a.gl-badge.badge-info:active {
- box-shadow: 0 0 0 1px #fff, 0 0 0 3px #428fdc;
- outline: none;
-}
-.gl-badge.badge-success {
- background-color: #c3e6cd;
- color: #24663b;
-}
-a.gl-badge.badge-success.active,
-a.gl-badge.badge-success:active {
- color: #0a4020;
- background-color: #91d4a8;
-}
-a.gl-badge.badge-success:active {
- box-shadow: 0 0 0 1px #fff, 0 0 0 3px #428fdc;
- outline: none;
-}
-.gl-badge.badge-warning {
- background-color: #f5d9a8;
- color: #8f4700;
-}
-a.gl-badge.badge-warning.active,
-a.gl-badge.badge-warning:active {
- color: #5c2900;
- background-color: #e9be74;
-}
-a.gl-badge.badge-warning:active {
- box-shadow: 0 0 0 1px #fff, 0 0 0 3px #428fdc;
- outline: none;
-}
-.gl-button .gl-badge {
- top: 0;
-}
-.gl-form-input,
-.gl-form-input.form-control {
- background-color: #fff;
- font-family: var(--default-regular-font, "GitLab Sans"), -apple-system,
- BlinkMacSystemFont, "Segoe UI", Roboto, "Noto Sans", Ubuntu, Cantarell,
- "Helvetica Neue", sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
- "Segoe UI Symbol", "Noto Color Emoji";
- font-size: 0.875rem;
- line-height: 1rem;
- padding-top: 0.5rem;
- padding-bottom: 0.5rem;
- padding-left: 0.75rem;
- padding-right: 0.75rem;
- height: auto;
- color: #333238;
- box-shadow: inset 0 0 0 1px #89888d;
- border-style: none;
- appearance: none;
- -moz-appearance: none;
-}
-.gl-form-input:disabled,
-.gl-form-input:not(.form-control-plaintext):not([type="color"]):read-only,
-.gl-form-input.form-control:disabled,
-.gl-form-input.form-control:not(.form-control-plaintext):not([type="color"]):read-only {
- background-color: #fbfafd;
- box-shadow: inset 0 0 0 1px #dcdcde;
-}
-.gl-form-input:disabled,
-.gl-form-input.form-control:disabled {
- cursor: not-allowed;
- color: #737278;
-}
-.gl-form-input::placeholder,
-.gl-form-input.form-control::placeholder {
- color: #89888d;
-}
-.gl-icon {
- fill: currentColor;
-}
-.gl-icon.s12 {
- width: 12px;
- height: 12px;
-}
-.gl-icon.s16 {
- width: 16px;
- height: 16px;
-}
-.gl-icon.s32 {
- width: 32px;
- height: 32px;
-}
-.gl-link {
- font-size: 0.875rem;
- color: #1f75cb;
-}
-.gl-link:active {
- color: #0b5cad;
-}
-.gl-link:active {
- text-decoration: underline;
- outline: 2px solid #428fdc;
- outline-offset: 2px;
-}
-.gl-button {
- display: inline-flex;
-}
-.gl-button:not(.btn-link):active {
- text-decoration: none;
-}
-.gl-button.gl-button {
- border-width: 0;
- padding-top: 0.5rem;
- padding-bottom: 0.5rem;
- padding-left: 0.75rem;
- padding-right: 0.75rem;
- background-color: transparent;
- line-height: 1rem;
- color: #333238;
- fill: currentColor;
- box-shadow: inset 0 0 0 1px #bfbfc3;
- justify-content: center;
- align-items: center;
- font-size: 0.875rem;
- border-radius: 0.25rem;
-}
-.gl-button.gl-button .gl-button-text {
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- padding-top: 1px;
- padding-bottom: 1px;
- margin-top: -1px;
- margin-bottom: -1px;
-}
-.gl-button.gl-button.btn-default {
- background-color: #fff;
-}
-.gl-button.gl-button.btn-default:active,
-.gl-button.gl-button.btn-default.active {
- box-shadow: inset 0 0 0 1px #626168, 0 0 0 1px #fff, 0 0 0 3px #428fdc;
- outline: none;
- background-color: #dcdcde;
-}
-.gl-button.gl-button.btn-default:active .gl-icon,
-.gl-button.gl-button.btn-default.active .gl-icon {
- color: #333238;
-}
-.gl-button.gl-button.btn-default .gl-icon {
- color: #737278;
-}
-.gl-search-box-by-type-search-icon {
- color: #737278;
- width: 1rem;
- position: absolute;
- left: 0.5rem;
- top: calc(50% - 16px / 2);
-}
-.gl-search-box-by-type {
- display: flex;
- position: relative;
-}
-.gl-search-box-by-type-input,
-.gl-search-box-by-type-input.gl-form-input {
- height: 2rem;
- padding-right: 2rem;
- padding-left: 1.75rem;
-}
-body {
- font-size: 0.875rem;
-}
-button,
-html [type="button"],
-[role="button"] {
- cursor: pointer;
-}
-strong {
- font-weight: bold;
-}
-svg {
- vertical-align: baseline;
-}
-.form-control {
- font-size: 0.875rem;
-}
-.hidden {
- display: none !important;
- visibility: hidden !important;
-}
-.badge:not(.gl-badge) {
- padding: 4px 5px;
- font-size: 12px;
- font-style: normal;
- font-weight: 400;
- display: inline-block;
-}
-.divider {
- height: 0;
- margin: 4px 0;
- overflow: hidden;
- border-top: 1px solid #dcdcde;
-}
-.toggle-sidebar-button .collapse-text,
-.toggle-sidebar-button .icon-chevron-double-lg-left {
- color: #737278;
-}
-html {
- overflow-y: scroll;
-}
-.layout-page {
- padding-top: calc(
- var(--header-height, 48px) +
- calc(var(--system-header-height) + var(--performance-bar-height))
- );
- padding-bottom: var(--system-footer-height);
-}
-@media (min-width: 576px) {
- .logged-out-marketing-header {
- --header-height: 72px;
- }
-}
-.btn {
- border-radius: 4px;
- font-size: 0.875rem;
- font-weight: 400;
- padding: 6px 10px;
- background-color: #fff;
- border-color: #dcdcde;
- color: #333238;
- color: #333238;
- white-space: nowrap;
-}
-.btn:active {
- background-color: #ececef;
- box-shadow: none;
-}
-.btn:active,
-.btn.active {
- background-color: #e6e6ea;
- border-color: #dedee3;
- color: #333238;
-}
-.btn svg {
- height: 15px;
- width: 15px;
-}
-.btn svg:not(:last-child) {
- margin-right: 5px;
-}
-.badge.badge-pill:not(.gl-badge) {
- font-weight: 400;
- background-color: rgba(0, 0, 0, 0.07);
- color: #535158;
- vertical-align: baseline;
-}
-:root {
- --performance-bar-height: 0px;
- --system-header-height: 0px;
- --top-bar-height: 0px;
- --system-footer-height: 0px;
- --mr-review-bar-height: 0px;
- --breakpoint-xs: 0;
- --breakpoint-sm: 576px;
- --breakpoint-md: 768px;
- --breakpoint-lg: 992px;
- --breakpoint-xl: 1200px;
-}
-.with-top-bar {
- --top-bar-height: 48px;
-}
-@media (min-width: 768px) {
- .page-with-contextual-sidebar {
- --application-bar-left: 56px;
- }
-}
-@media (min-width: 1200px) {
- .page-with-contextual-sidebar {
- --application-bar-left: 256px;
- }
- .page-with-icon-sidebar {
- --application-bar-left: 56px;
- }
- .page-with-super-sidebar {
- --application-bar-left: 256px;
- }
- .page-with-super-sidebar-collapsed {
- --application-bar-left: 0px;
- }
-}
-.gl-font-sm {
- font-size: 12px;
-}
-.dropdown {
- position: relative;
-}
-.dropdown-menu {
- display: none;
- position: absolute;
- width: auto;
- top: 100%;
- z-index: 300;
- min-width: 240px;
- max-width: 500px;
- margin-top: 4px;
- margin-bottom: 24px;
- font-size: 0.875rem;
- font-weight: 400;
- padding: 8px 0;
- background-color: #fff;
- border: 1px solid #dcdcde;
- border-radius: 0.25rem;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
-}
-.dropdown-menu ul {
- margin: 0;
- padding: 0;
-}
-.dropdown-menu li {
- display: block;
- text-align: left;
- list-style: none;
-}
-.dropdown-menu li > a,
-.dropdown-menu li > button {
- background: transparent;
- border: 0;
- border-radius: 0;
- box-shadow: none;
- display: block;
- font-weight: 400;
- position: relative;
- padding: 8px 12px;
- color: #333238;
- line-height: 16px;
- white-space: normal;
- overflow: hidden;
- text-align: left;
- width: 100%;
-}
-.dropdown-menu li > a:active,
-.dropdown-menu li > button:active {
- background-color: #ececef;
- color: #333238;
- outline: 0;
- text-decoration: none;
-}
-.dropdown-menu li > a:active,
-.dropdown-menu li > button:active {
- box-shadow: inset 0 0 0 2px #428fdc, inset 0 0 0 3px #fff,
- inset 0 0 0 1px #fff;
- outline: none;
-}
-.dropdown-menu .divider {
- height: 1px;
- margin: 0.25rem 0;
- padding: 0;
- background-color: #dcdcde;
-}
-.dropdown-menu .badge.badge-pill + span:not(.badge):not(.badge-pill) {
- margin-right: 40px;
-}
-@media (max-width: 575.98px) {
- .navbar-gitlab li.dropdown {
- position: static;
- }
- .navbar-gitlab li.dropdown.user-counter {
- margin-left: 8px !important;
- }
- .navbar-gitlab li.dropdown.user-counter > a {
- padding: 0 4px !important;
- }
- header.navbar-gitlab .dropdown .dropdown-menu {
- width: 100%;
- min-width: 100%;
- }
-}
-input {
- border-radius: 0.25rem;
- color: #333238;
- background-color: #fff;
-}
-input[type="search"] {
- appearance: textfield;
-}
-.form-control {
- border-radius: 4px;
- padding: 6px 10px;
-}
-.form-control::placeholder {
- color: #89888d;
-}
-kbd {
- display: inline-block;
- padding: 3px 5px;
- font-size: 0.75rem;
- line-height: 10px;
- color: var(--gray-700, #535158);
- vertical-align: unset;
- background-color: var(--gray-10, #fbfafd);
- border-width: 1px;
- border-style: solid;
- border-color: var(--gray-100, #dcdcde) var(--gray-100, #dcdcde)
- var(--gray-200, #bfbfc3);
- border-image: none;
- border-radius: 3px;
- box-shadow: 0 -1px 0 var(--gray-200, #bfbfc3) inset;
-}
-.navbar-gitlab {
- padding: 0 16px;
- z-index: 1000;
- margin-bottom: 0;
- min-height: var(--header-height, 48px);
- border: 0;
- position: fixed;
- top: calc(var(--system-header-height) + var(--performance-bar-height));
- left: 0;
- right: 0;
- border-radius: 0;
-}
-.navbar-gitlab .close-icon {
- display: none;
-}
-.navbar-gitlab .header-content {
- width: 100%;
- display: flex;
- justify-content: space-between;
- position: relative;
- min-height: var(--header-height, 48px);
- padding-left: 0;
-}
-.navbar-gitlab .header-content .title {
- padding-right: 0;
- color: currentColor;
- display: flex;
- position: relative;
- margin: 0;
- font-size: 18px;
- vertical-align: top;
- white-space: nowrap;
-}
-.navbar-gitlab .header-content .title img {
- height: 24px;
-}
-.navbar-gitlab .header-content .title a:not(.canary-badge) {
- display: flex;
- align-items: center;
- padding: 2px 8px;
- margin: 4px 2px 4px -8px;
- border-radius: 4px;
-}
-.navbar-gitlab .header-content .title a:not(.canary-badge):active {
- box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.6), 0 0 0 3px #63a6e9;
- outline: none;
-}
-.navbar-gitlab .header-content .navbar-collapse > ul.nav > li:not(.d-none) {
- margin: 0 2px;
-}
-.navbar-gitlab .header-search-form {
- min-width: 320px;
-}
-@media (min-width: 768px) and (max-width: 1199.98px) {
- .navbar-gitlab .header-search-form {
- min-width: 200px;
- }
-}
-.navbar-gitlab .header-search-form .keyboard-shortcut-helper {
- transform: translateY(calc(50% - 2px));
- box-shadow: none;
- border-color: transparent;
-}
-.navbar-gitlab .navbar-collapse {
- flex: 0 0 auto;
- border-top: 0;
- padding: 0;
-}
-@media (max-width: 575.98px) {
- .navbar-gitlab .navbar-collapse {
- flex: 1 1 auto;
- }
-}
-.navbar-gitlab .navbar-collapse .nav {
- flex-wrap: nowrap;
-}
-@media (max-width: 575.98px) {
- .navbar-gitlab .navbar-collapse .nav > li:not(.d-none) a {
- margin-left: 0;
- }
-}
-.navbar-gitlab .container-fluid {
- padding: 0;
-}
-.navbar-gitlab .container-fluid .user-counter svg {
- margin-right: 3px;
-}
-.navbar-gitlab .container-fluid .navbar-toggler {
- position: relative;
- right: -10px;
- border-radius: 0;
- min-width: 45px;
- padding: 0;
- margin: 8px 8px 8px 0;
- font-size: 14px;
- text-align: center;
- color: currentColor;
-}
-.navbar-gitlab .container-fluid .navbar-toggler.active {
- color: currentColor;
- background-color: transparent;
-}
-@media (max-width: 575.98px) {
- .navbar-gitlab .container-fluid .navbar-nav {
- display: flex;
- padding-right: 10px;
- flex-direction: row;
- }
-}
-.navbar-gitlab
- .container-fluid
- .navbar-nav
- li
- .badge.badge-pill:not(.gl-badge) {
- box-shadow: none;
- font-weight: 600;
-}
-@media (max-width: 575.98px) {
- .navbar-gitlab .container-fluid .nav > li.header-user {
- padding-left: 10px;
- }
-}
-.navbar-gitlab .container-fluid .nav > li > a {
- will-change: color;
- margin: 4px 0;
- padding: 6px 8px;
- height: 32px;
-}
-@media (max-width: 575.98px) {
- .navbar-gitlab .container-fluid .nav > li > a {
- padding: 0;
- }
-}
-.navbar-gitlab .container-fluid .nav > li > a.header-user-dropdown-toggle {
- margin-left: 2px;
-}
-.navbar-gitlab
- .container-fluid
- .nav
- > li
- > a.header-user-dropdown-toggle
- .header-user-avatar {
- margin-right: 0;
-}
-.navbar-gitlab .container-fluid .nav > li .header-new-dropdown-toggle {
- margin-right: 0;
-}
-.navbar-sub-nav > li > a,
-.navbar-sub-nav > li > button,
-.navbar-nav > li > a,
-.navbar-nav > li > button {
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 6px 8px;
- margin: 4px 2px;
- font-size: 12px;
- color: currentColor;
- border-radius: 4px;
- height: 32px;
- font-weight: 600;
-}
-.navbar-sub-nav > li > a:active,
-.navbar-sub-nav > li > button:active,
-.navbar-nav > li > a:active,
-.navbar-nav > li > button:active {
- box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.6), 0 0 0 3px #63a6e9;
- outline: none;
-}
-.navbar-sub-nav > li .top-nav-toggle,
-.navbar-sub-nav > li > button,
-.navbar-nav > li .top-nav-toggle,
-.navbar-nav > li > button {
- background: transparent;
- border: 0;
-}
-.navbar-sub-nav .dropdown-menu,
-.navbar-nav .dropdown-menu {
- position: absolute;
-}
-.navbar-sub-nav {
- display: flex;
- align-items: center;
- height: 100%;
- margin: 0 0 0 6px;
-}
-.caret-down,
-.btn .caret-down {
- top: 0;
- height: 11px;
- width: 11px;
- margin-left: 4px;
- fill: currentColor;
-}
-.header-user .dropdown-menu,
-.header-new .dropdown-menu {
- margin-top: 4px;
-}
-@media (max-width: 575.98px) {
- .navbar-gitlab .container-fluid {
- font-size: 18px;
- }
- .navbar-gitlab .container-fluid .navbar-nav {
- table-layout: fixed;
- width: 100%;
- margin: 0;
- text-align: right;
- }
- .navbar-gitlab .container-fluid .navbar-collapse {
- margin-left: -8px;
- margin-right: -10px;
- }
- .navbar-gitlab .container-fluid .navbar-collapse .nav > li:not(.d-none) {
- flex: 1;
- }
- .header-user-dropdown-toggle {
- text-align: center;
- }
- .header-user-avatar {
- float: none;
- }
-}
-.header-user-avatar {
- float: left;
- margin-right: 5px;
- border-radius: 50%;
- border: 1px solid #f2f2f4;
-}
-.notification-dot {
- background-color: #d99530;
- height: 12px;
- width: 12px;
- pointer-events: none;
- visibility: hidden;
- top: 3px;
-}
-.tanuki-logo .tanuki {
- fill: #e24329;
-}
-.tanuki-logo .left-cheek,
-.tanuki-logo .right-cheek {
- fill: #fc6d26;
-}
-.tanuki-logo .chin {
- fill: #fca326;
-}
-.context-header {
- position: relative;
- margin-right: 2px;
- width: 256px;
-}
-.context-header > a,
-.context-header > button {
- font-weight: 600;
- display: flex;
- width: 100%;
- align-items: center;
- padding: 10px 16px 10px 10px;
- color: #333238;
- background-color: transparent;
- border: 0;
- text-align: left;
-}
-.context-header .avatar-container {
- flex: 0 0 32px;
- background-color: #fff;
-}
-.context-header .sidebar-context-title {
- overflow: hidden;
- text-overflow: ellipsis;
- color: #333238;
-}
-@media (min-width: 768px) {
- .page-with-contextual-sidebar {
- padding-left: 56px;
- }
-}
-@media (min-width: 1200px) {
- .page-with-contextual-sidebar {
- padding-left: 256px;
- }
-}
-@media (min-width: 768px) {
- .page-with-icon-sidebar {
- padding-left: 56px;
- }
-}
-.nav-sidebar {
- position: fixed;
- bottom: var(--system-footer-height);
- left: 0;
- z-index: 600;
- width: 256px;
- top: calc(
- var(--header-height, 48px) +
- calc(var(--system-header-height) + var(--performance-bar-height)) +
- var(--top-bar-height)
- );
- background-color: #fbfafd;
- border-right: 1px solid #e9e9e9;
- transform: translate3d(0, 0, 0);
-}
-.nav-sidebar.sidebar-collapsed-desktop {
- width: 56px;
-}
-.nav-sidebar.sidebar-collapsed-desktop .nav-sidebar-inner-scroll {
- overflow-x: hidden;
-}
-.nav-sidebar.sidebar-collapsed-desktop .badge.badge-pill:not(.fly-out-badge),
-.nav-sidebar.sidebar-collapsed-desktop .nav-item-name,
-.nav-sidebar.sidebar-collapsed-desktop .collapse-text {
- border: 0;
- clip: rect(0, 0, 0, 0);
- height: 1px;
- margin: -1px;
- overflow: hidden;
- padding: 0;
- position: absolute;
- white-space: nowrap;
- width: 1px;
-}
-.nav-sidebar.sidebar-collapsed-desktop .sidebar-top-level-items > li > a {
- min-height: unset;
-}
-.nav-sidebar.sidebar-collapsed-desktop .fly-out-top-item:not(.divider) {
- display: block !important;
-}
-.nav-sidebar.sidebar-collapsed-desktop .avatar-container {
- margin: 0 auto;
-}
-.nav-sidebar.sidebar-collapsed-desktop li.active:not(.fly-out-top-item) > a {
- background-color: rgba(41, 41, 97, 0.08);
-}
-.nav-sidebar a {
- text-decoration: none;
- color: #333238;
-}
-.nav-sidebar li {
- white-space: nowrap;
-}
-.nav-sidebar li .nav-item-name {
- flex: 1;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-.nav-sidebar li > a,
-.nav-sidebar li > .fly-out-top-item-container {
- height: 2rem;
- padding-left: 0.75rem;
- padding-right: 0.75rem;
- display: flex;
- align-items: center;
- border-radius: 0.25rem;
- width: auto;
- margin: 1px 8px;
-}
-.nav-sidebar li.active > a {
- font-weight: 600;
-}
-.nav-sidebar li.active:not(.fly-out-top-item) > a:not(.has-sub-items) {
- background-color: rgba(31, 30, 36, 0.08);
-}
-.nav-sidebar ul {
- padding-left: 0;
- list-style: none;
-}
-@media (max-width: 767.98px) {
- .nav-sidebar {
- left: -256px;
- }
-}
-.nav-sidebar .nav-icon-container {
- display: flex;
- margin-right: 8px;
-}
-.nav-sidebar
- a:not(.has-sub-items)
- + .sidebar-sub-level-items
- .fly-out-top-item {
- display: none;
-}
-.nav-sidebar
- a:not(.has-sub-items)
- + .sidebar-sub-level-items
- .fly-out-top-item
- a,
-.nav-sidebar
- a:not(.has-sub-items)
- + .sidebar-sub-level-items
- .fly-out-top-item.active
- a,
-.nav-sidebar
- a:not(.has-sub-items)
- + .sidebar-sub-level-items
- .fly-out-top-item
- .fly-out-top-item-container {
- margin-left: 0;
- margin-right: 0;
- padding-left: 1rem;
- padding-right: 1rem;
- cursor: default;
- pointer-events: none;
- font-size: 0.75rem;
- margin-top: -0.25rem;
- margin-bottom: -0.25rem;
- margin-top: 0;
- position: relative;
- color: #fff;
- background: var(--black, #000);
-}
-.nav-sidebar
- a:not(.has-sub-items)
- + .sidebar-sub-level-items
- .fly-out-top-item
- a
- strong,
-.nav-sidebar
- a:not(.has-sub-items)
- + .sidebar-sub-level-items
- .fly-out-top-item.active
- a
- strong,
-.nav-sidebar
- a:not(.has-sub-items)
- + .sidebar-sub-level-items
- .fly-out-top-item
- .fly-out-top-item-container
- strong {
- font-weight: 400;
-}
-.nav-sidebar
- a:not(.has-sub-items)
- + .sidebar-sub-level-items
- .fly-out-top-item
- a::before,
-.nav-sidebar
- a:not(.has-sub-items)
- + .sidebar-sub-level-items
- .fly-out-top-item.active
- a::before,
-.nav-sidebar
- a:not(.has-sub-items)
- + .sidebar-sub-level-items
- .fly-out-top-item
- .fly-out-top-item-container::before {
- position: absolute;
- content: "";
- display: block;
- top: 50%;
- left: -0.25rem;
- margin-top: -0.25rem;
- width: 0;
- height: 0;
- border-top: 0.25rem solid transparent;
- border-bottom: 0.25rem solid transparent;
- border-right: 0.25rem solid #000;
- border-right-color: var(--black, #000);
-}
-@media (min-width: 576px) {
- .nav-sidebar a.has-sub-items + .sidebar-sub-level-items {
- min-width: 150px;
- }
-}
-.nav-sidebar a.has-sub-items + .sidebar-sub-level-items .fly-out-top-item {
- display: none;
-}
-.nav-sidebar a.has-sub-items + .sidebar-sub-level-items .fly-out-top-item a,
-.nav-sidebar
- a.has-sub-items
- + .sidebar-sub-level-items
- .fly-out-top-item.active
- a,
-.nav-sidebar
- a.has-sub-items
- + .sidebar-sub-level-items
- .fly-out-top-item
- .fly-out-top-item-container {
- margin-left: 0;
- margin-right: 0;
- padding-left: 1rem;
- padding-right: 1rem;
- cursor: default;
- pointer-events: none;
- font-size: 0.75rem;
- margin-top: 0;
- border-bottom-left-radius: 0;
- border-bottom-right-radius: 0;
-}
-@media (min-width: 768px) and (max-width: 1199px) {
- .nav-sidebar:not(.sidebar-expanded-mobile) {
- width: 56px;
- }
- .nav-sidebar:not(.sidebar-expanded-mobile) .nav-sidebar-inner-scroll {
- overflow-x: hidden;
- }
- .nav-sidebar:not(.sidebar-expanded-mobile)
- .badge.badge-pill:not(.fly-out-badge),
- .nav-sidebar:not(.sidebar-expanded-mobile) .nav-item-name,
- .nav-sidebar:not(.sidebar-expanded-mobile) .collapse-text {
- border: 0;
- clip: rect(0, 0, 0, 0);
- height: 1px;
- margin: -1px;
- overflow: hidden;
- padding: 0;
- position: absolute;
- white-space: nowrap;
- width: 1px;
- }
- .nav-sidebar:not(.sidebar-expanded-mobile) .sidebar-top-level-items > li > a {
- min-height: unset;
- }
- .nav-sidebar:not(.sidebar-expanded-mobile) .fly-out-top-item:not(.divider) {
- display: block !important;
- }
- .nav-sidebar:not(.sidebar-expanded-mobile) .avatar-container {
- margin: 0 auto;
- }
- .nav-sidebar:not(.sidebar-expanded-mobile)
- li.active:not(.fly-out-top-item)
- > a {
- background-color: rgba(41, 41, 97, 0.08);
- }
- .nav-sidebar:not(.sidebar-expanded-mobile) .context-header {
- height: 60px;
- width: 56px;
- }
- .nav-sidebar:not(.sidebar-expanded-mobile) .context-header a {
- padding: 10px 4px;
- }
- .nav-sidebar:not(.sidebar-expanded-mobile) .sidebar-context-title {
- border: 0;
- clip: rect(0, 0, 0, 0);
- height: 1px;
- margin: -1px;
- overflow: hidden;
- padding: 0;
- position: absolute;
- white-space: nowrap;
- width: 1px;
- }
- .nav-sidebar:not(.sidebar-expanded-mobile) .context-header {
- height: auto;
- }
- .nav-sidebar:not(.sidebar-expanded-mobile) .context-header a {
- padding: 0.25rem;
- }
- .nav-sidebar:not(.sidebar-expanded-mobile)
- .sidebar-top-level-items
- > li
- .sidebar-sub-level-items:not(.flyout-list) {
- display: none;
- }
- .nav-sidebar:not(.sidebar-expanded-mobile) .nav-icon-container {
- margin-right: 0;
- }
- .nav-sidebar:not(.sidebar-expanded-mobile) .toggle-sidebar-button {
- width: 55px;
- padding: 0 21px;
- }
- .nav-sidebar:not(.sidebar-expanded-mobile)
- .toggle-sidebar-button
- .collapse-text {
- display: none;
- }
- .nav-sidebar:not(.sidebar-expanded-mobile)
- .toggle-sidebar-button
- .icon-chevron-double-lg-left {
- transform: rotate(180deg);
- margin: 0;
- }
-}
-.nav-sidebar-inner-scroll {
- height: 100%;
- width: 100%;
- overflow-x: hidden;
- overflow-y: auto;
-}
-.nav-sidebar-inner-scroll > div.context-header {
- margin-top: 0.25rem;
-}
-.nav-sidebar-inner-scroll > div.context-header a {
- height: 2rem;
- padding-left: 0.75rem;
- padding-right: 0.75rem;
- display: flex;
- align-items: center;
- border-radius: 0.25rem;
- width: auto;
- margin: 1px 8px;
- padding: 0.25rem;
- margin-bottom: 0.25rem;
- margin-top: 0.125rem;
- height: auto;
-}
-.nav-sidebar-inner-scroll > div.context-header a .avatar-container {
- font-weight: 400;
- flex: none;
-}
-.sidebar-top-level-items {
- margin-bottom: 60px;
-}
-.sidebar-top-level-items .context-header a {
- padding: 0.25rem;
- margin-bottom: 0.25rem;
- margin-top: 0.125rem;
- height: auto;
-}
-.sidebar-top-level-items .context-header a .avatar-container {
- font-weight: 400;
- flex: none;
-}
-.sidebar-top-level-items
- > li.active
- .sidebar-sub-level-items:not(.is-fly-out-only) {
- display: block;
-}
-.sidebar-top-level-items li > a.gl-link {
- color: #333238;
-}
-.sidebar-top-level-items li > a.gl-link:active {
- text-decoration: none;
-}
-.sidebar-sub-level-items {
- padding-top: 0;
- padding-bottom: 0;
- display: none;
-}
-.sidebar-sub-level-items:not(.fly-out-list) li > a {
- padding-left: 2.25rem;
-}
-.toggle-sidebar-button,
-.close-nav-button {
- height: 48px;
- padding: 0 16px;
- background-color: #fbfafd;
- border: 0;
- color: #737278;
- display: flex;
- align-items: center;
- background-color: #fbfafd;
- position: fixed;
- bottom: 0;
- width: 255px;
-}
-.toggle-sidebar-button .collapse-text,
-.toggle-sidebar-button .icon-chevron-double-lg-left,
-.close-nav-button .collapse-text,
-.close-nav-button .icon-chevron-double-lg-left {
- color: inherit;
-}
-.collapse-text {
- white-space: nowrap;
- overflow: hidden;
-}
-.sidebar-collapsed-desktop .context-header {
- height: 60px;
- width: 56px;
-}
-.sidebar-collapsed-desktop .context-header a {
- padding: 10px 4px;
-}
-.sidebar-collapsed-desktop .sidebar-context-title {
- border: 0;
- clip: rect(0, 0, 0, 0);
- height: 1px;
- margin: -1px;
- overflow: hidden;
- padding: 0;
- position: absolute;
- white-space: nowrap;
- width: 1px;
-}
-.sidebar-collapsed-desktop .context-header {
- height: auto;
-}
-.sidebar-collapsed-desktop .context-header a {
- padding: 0.25rem;
-}
-.sidebar-collapsed-desktop
- .sidebar-top-level-items
- > li
- .sidebar-sub-level-items:not(.flyout-list) {
- display: none;
-}
-.sidebar-collapsed-desktop .nav-icon-container {
- margin-right: 0;
-}
-.sidebar-collapsed-desktop .toggle-sidebar-button {
- width: 55px;
- padding: 0 21px;
-}
-.sidebar-collapsed-desktop .toggle-sidebar-button .collapse-text {
- display: none;
-}
-.sidebar-collapsed-desktop .toggle-sidebar-button .icon-chevron-double-lg-left {
- transform: rotate(180deg);
- margin: 0;
-}
-.close-nav-button {
- display: none;
-}
-@media (max-width: 767.98px) {
- .close-nav-button {
- display: flex;
- }
- .toggle-sidebar-button {
- display: none;
- }
-}
-.super-sidebar {
- display: flex;
- flex-direction: column;
- position: fixed;
- top: calc(
- var(--header-height, 48px) +
- calc(var(--system-header-height) + var(--performance-bar-height))
- );
- bottom: var(--system-footer-height);
- left: 0;
- background-color: var(--gray-10, #fbfafd);
- border-right: 1px solid rgba(31, 30, 36, 0.08);
- transform: translate3d(0, 0, 0);
- width: 256px;
- z-index: 600;
-}
-.super-sidebar.super-sidebar-loading {
- transform: translate3d(-100%, 0, 0);
-}
-@media (min-width: 1200px) {
- .super-sidebar.super-sidebar-loading {
- transform: translate3d(0, 0, 0);
- }
-}
-@media (prefers-reduced-motion: no-preference) {
-}
-.page-with-super-sidebar {
- padding-left: 0;
-}
-@media (prefers-reduced-motion: no-preference) {
-}
-@media (min-width: 1200px) {
- .page-with-super-sidebar {
- padding-left: 256px;
- }
-}
-.page-with-super-sidebar-collapsed .super-sidebar {
- transform: translate3d(-100%, 0, 0);
-}
-@media (min-width: 1200px) {
- .page-with-super-sidebar-collapsed {
- padding-left: 0;
- }
-}
-input::-moz-placeholder {
- color: #89888d;
- opacity: 1;
-}
-input::-ms-input-placeholder {
- color: #89888d;
-}
-input:-ms-input-placeholder {
- color: #89888d;
-}
-svg {
- fill: currentColor;
-}
-svg.s12 {
- width: 12px;
- height: 12px;
-}
-svg.s16 {
- width: 16px;
- height: 16px;
-}
-svg.s32 {
- width: 32px;
- height: 32px;
-}
-svg.s12 {
- vertical-align: -1px;
-}
-svg.s16 {
- vertical-align: -3px;
-}
-.avatar,
-.avatar-container {
- float: left;
- margin-right: 16px;
- border-radius: 50%;
-}
-.avatar.s16,
-.avatar-container.s16 {
- width: 16px;
- height: 16px;
- margin-right: 8px;
-}
-.avatar.s32,
-.avatar-container.s32 {
- width: 32px;
- height: 32px;
- margin-right: 8px;
-}
-.avatar {
- transition-property: none;
- width: 40px;
- height: 40px;
- padding: 0;
- background: #fefefe;
- overflow: hidden;
- box-shadow: inset 0 0 0 1px rgba(31, 30, 36, 0.1);
-}
-.avatar.avatar-tile {
- border-radius: 0;
- border: 0;
-}
-.identicon {
- text-align: center;
- vertical-align: top;
- color: #333238;
- background-color: #ececef;
-}
-.identicon.s16 {
- font-size: 10px;
- line-height: 16px;
-}
-.identicon.s32 {
- font-size: 14px;
- line-height: 32px;
-}
-.identicon.bg1 {
- background-color: #fcf1ef;
-}
-.identicon.bg2 {
- background-color: #f4f0ff;
-}
-.identicon.bg3 {
- background-color: #f1f1ff;
-}
-.identicon.bg4 {
- background-color: #e9f3fc;
-}
-.identicon.bg5 {
- background-color: #ecf4ee;
-}
-.identicon.bg6 {
- background-color: #fdf1dd;
-}
-.identicon.bg7 {
- background-color: #ececef;
-}
-.avatar-container {
- overflow: hidden;
- display: flex;
-}
-.avatar-container a {
- width: 100%;
- height: 100%;
- display: flex;
- text-decoration: none;
-}
-.avatar-container .avatar {
- border-radius: 0;
- border: 0;
- height: auto;
- width: 100%;
- margin: 0;
- align-self: center;
-}
-.rect-avatar {
- border-radius: 2px;
-}
-.rect-avatar.s16 {
- border-radius: 2px;
-}
-.rect-avatar.s16 .avatar {
- border-radius: 2px;
-}
-.rect-avatar.s32 {
- border-radius: 4px;
-}
-.rect-avatar.s32 .avatar {
- border-radius: 4px;
-}
-
-.tab-width-8 {
- tab-size: 8;
-}
-.gl-sr-only {
- border: 0;
- clip: rect(0, 0, 0, 0);
- height: 1px;
- margin: -1px;
- overflow: hidden;
- padding: 0;
- position: absolute;
- white-space: nowrap;
- width: 1px;
-}
-.gl-border-none\! {
- border-style: none !important;
-}
-.gl-display-none {
- display: none;
-}
-.gl-display-flex {
- display: flex;
-}
-@media (min-width: 992px) {
- .gl-lg-display-flex {
- display: flex;
- }
-}
-@media (min-width: 576px) {
- .gl-sm-display-block {
- display: block;
- }
-}
-@media (min-width: 992px) {
- .gl-lg-display-block {
- display: block;
- }
-}
-.gl-align-items-center {
- align-items: center;
-}
-.gl-align-items-stretch {
- align-items: stretch;
-}
-.gl-flex-grow-0\! {
- flex-grow: 0 !important;
-}
-.gl-flex-grow-1 {
- flex-grow: 1;
-}
-.gl-flex-basis-half\! {
- flex-basis: 50% !important;
-}
-.gl-justify-content-end {
- justify-content: flex-end;
-}
-.gl-relative {
- position: relative;
-}
-.gl-absolute {
- position: absolute;
-}
-.gl-top-0 {
- top: 0;
-}
-.gl-right-3 {
- right: 0.5rem;
-}
-.gl-w-full {
- width: 100%;
-}
-.gl-px-3 {
- padding-left: 0.5rem;
- padding-right: 0.5rem;
-}
-.gl-pr-2 {
- padding-right: 0.25rem;
-}
-.gl-pt-0 {
- padding-top: 0;
-}
-.gl-mr-auto {
- margin-right: auto;
-}
-.gl-mr-3 {
- margin-right: 0.5rem;
-}
-.gl-ml-n2 {
- margin-left: -0.25rem;
-}
-.gl-ml-3 {
- margin-left: 0.5rem;
-}
-.gl-mx-0\! {
- margin-left: 0 !important;
- margin-right: 0 !important;
-}
-.gl-text-right {
- text-align: right;
-}
-.gl-white-space-nowrap {
- white-space: nowrap;
-}
-.gl-font-sm {
- font-size: 0.75rem;
-}
-.gl-font-weight-bold {
- font-weight: 600;
-}
-.gl-z-index-1 {
- z-index: 1;
-}
-
-@import "startup/cloaking";
-@include cloak-startup-scss(none);
diff --git a/app/assets/stylesheets/startup/startup-signin.scss b/app/assets/stylesheets/startup/startup-signin.scss
deleted file mode 100644
index 32da8e1bb6b..00000000000
--- a/app/assets/stylesheets/startup/startup-signin.scss
+++ /dev/null
@@ -1,852 +0,0 @@
-// DO NOT EDIT! This is auto-generated from "yarn run generate:startup_css"
-// Please see the feedback issue for more details and help:
-// https://gitlab.com/gitlab-org/gitlab/-/issues/331812
-@charset "UTF-8";
-:root {
- --white: #fff;
-}
-*,
-*::before,
-*::after {
- box-sizing: border-box;
-}
-html {
- font-family: sans-serif;
- line-height: 1.15;
-}
-header {
- display: block;
-}
-body {
- margin: 0;
- font-family: var(--default-regular-font, "GitLab Sans"), -apple-system,
- BlinkMacSystemFont, "Segoe UI", Roboto, "Noto Sans", Ubuntu, Cantarell,
- "Helvetica Neue", sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
- "Segoe UI Symbol", "Noto Color Emoji";
- font-size: 1rem;
- font-weight: 400;
- line-height: 1.5;
- color: #333238;
- text-align: left;
- background-color: #fff;
-}
-hr {
- box-sizing: content-box;
- height: 0;
- overflow: visible;
-}
-h1,
-h3 {
- margin-top: 0;
- margin-bottom: 0.25rem;
-}
-p {
- margin-top: 0;
- margin-bottom: 1rem;
-}
-a {
- color: #1f75cb;
- text-decoration: none;
- background-color: transparent;
-}
-a:not([href]):not([class]) {
- color: inherit;
- text-decoration: none;
-}
-img {
- vertical-align: middle;
- border-style: none;
-}
-svg {
- overflow: hidden;
- vertical-align: middle;
-}
-label {
- display: inline-block;
- margin-bottom: 0.5rem;
-}
-button {
- border-radius: 0;
-}
-input,
-button {
- margin: 0;
- font-family: inherit;
- font-size: inherit;
- line-height: inherit;
-}
-button,
-input {
- overflow: visible;
-}
-button {
- text-transform: none;
-}
-button:not(:disabled),
-[type="submit"]:not(:disabled) {
- cursor: pointer;
-}
-button::-moz-focus-inner,
-[type="submit"]::-moz-focus-inner {
- padding: 0;
- border-style: none;
-}
-input[type="checkbox"] {
- box-sizing: border-box;
- padding: 0;
-}
-fieldset {
- min-width: 0;
- padding: 0;
- margin: 0;
- border: 0;
-}
-[hidden] {
- display: none !important;
-}
-h1,
-h3 {
- margin-bottom: 0.25rem;
- font-weight: 600;
- line-height: 1.2;
- color: #333238;
-}
-h1 {
- font-size: 2.1875rem;
-}
-h3 {
- font-size: 1.53125rem;
-}
-hr {
- margin-top: 0.5rem;
- margin-bottom: 0.5rem;
- border: 0;
- border-top: 1px solid rgba(0, 0, 0, 0.1);
-}
-.container {
- width: 100%;
- padding-right: 15px;
- padding-left: 15px;
- margin-right: auto;
- margin-left: auto;
-}
-@media (min-width: 576px) {
- .container {
- max-width: 540px;
- }
-}
-@media (min-width: 768px) {
- .container {
- max-width: 720px;
- }
-}
-@media (min-width: 992px) {
- .container {
- max-width: 960px;
- }
-}
-@media (min-width: 1200px) {
- .container {
- max-width: 1140px;
- }
-}
-.row {
- display: flex;
- flex-wrap: wrap;
- margin-right: -15px;
- margin-left: -15px;
-}
-.col-md-6,
-.col-sm-12 {
- position: relative;
- width: 100%;
- padding-right: 15px;
- padding-left: 15px;
-}
-.order-1 {
- order: 1;
-}
-.order-12 {
- order: 12;
-}
-@media (min-width: 576px) {
- .col-sm-12 {
- flex: 0 0 100%;
- max-width: 100%;
- }
- .order-sm-1 {
- order: 1;
- }
- .order-sm-12 {
- order: 12;
- }
-}
-@media (min-width: 768px) {
- .col-md-6 {
- flex: 0 0 50%;
- max-width: 50%;
- }
-}
-.form-control {
- display: block;
- width: 100%;
- height: 32px;
- padding: 0.375rem 0.75rem;
- font-size: 0.875rem;
- font-weight: 400;
- line-height: 1.5;
- color: #333238;
- background-color: #fff;
- background-clip: padding-box;
- border: 1px solid #89888d;
- border-radius: 0.25rem;
-}
-@media (prefers-reduced-motion: reduce) {
-}
-.form-control::placeholder {
- color: #626168;
- opacity: 1;
-}
-.form-control:disabled {
- background-color: #fbfafd;
- opacity: 1;
-}
-.form-group {
- margin-bottom: 1rem;
-}
-.form-text {
- display: block;
- margin-top: 0.25rem;
-}
-.btn {
- display: inline-block;
- font-weight: 400;
- color: #333238;
- text-align: center;
- vertical-align: middle;
- user-select: none;
- background-color: transparent;
- border: 1px solid transparent;
- padding: 0.375rem 0.75rem;
- font-size: 1rem;
- line-height: 20px;
- border-radius: 0.25rem;
-}
-@media (prefers-reduced-motion: reduce) {
-}
-.btn:disabled {
- opacity: 0.65;
-}
-.btn:not(:disabled):not(.disabled) {
- cursor: pointer;
-}
-fieldset:disabled a.btn {
- pointer-events: none;
-}
-.btn-block {
- display: block;
- width: 100%;
-}
-.btn-block + .btn-block {
- margin-top: 0.5rem;
-}
-input.btn-block[type="submit"] {
- width: 100%;
-}
-.custom-control {
- position: relative;
- z-index: 1;
- display: block;
- min-height: 1.5rem;
- padding-left: 1.5rem;
- print-color-adjust: exact;
-}
-.custom-control-input {
- position: absolute;
- left: 0;
- z-index: -1;
- width: 1rem;
- height: 1.25rem;
- opacity: 0;
-}
-.custom-control-input:checked ~ .custom-control-label::before {
- color: #fff;
- border-color: #007bff;
- background-color: #007bff;
-}
-.custom-control-input:not(:disabled):active ~ .custom-control-label::before {
- color: #fff;
- background-color: #b3d7ff;
- border-color: #b3d7ff;
-}
-.custom-control-input:disabled ~ .custom-control-label {
- color: #626168;
-}
-.custom-control-input:disabled ~ .custom-control-label::before {
- background-color: #fbfafd;
-}
-.custom-control-label {
- position: relative;
- margin-bottom: 0;
- vertical-align: top;
-}
-.custom-control-label::before {
- position: absolute;
- top: 0.25rem;
- left: -1.5rem;
- display: block;
- width: 1rem;
- height: 1rem;
- pointer-events: none;
- content: "";
- background-color: #fff;
- border: 1px solid #737278;
-}
-.custom-control-label::after {
- position: absolute;
- top: 0.25rem;
- left: -1.5rem;
- display: block;
- width: 1rem;
- height: 1rem;
- content: "";
- background: 50% / 50% 50% no-repeat;
-}
-.custom-checkbox .custom-control-label::before {
- border-radius: 0.25rem;
-}
-.custom-checkbox .custom-control-input:checked ~ .custom-control-label::after {
- background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26l2.974 2.99L8 2.193z'/%3e%3c/svg%3e");
-}
-.custom-checkbox
- .custom-control-input:indeterminate
- ~ .custom-control-label::before {
- border-color: #007bff;
- background-color: #007bff;
-}
-.custom-checkbox
- .custom-control-input:indeterminate
- ~ .custom-control-label::after {
- background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4'%3e%3cpath stroke='%23fff' d='M0 2h4'/%3e%3c/svg%3e");
-}
-.custom-checkbox
- .custom-control-input:disabled:checked
- ~ .custom-control-label::before {
- background-color: rgba(0, 123, 255, 0.5);
-}
-.custom-checkbox
- .custom-control-input:disabled:indeterminate
- ~ .custom-control-label::before {
- background-color: rgba(0, 123, 255, 0.5);
-}
-@media (prefers-reduced-motion: reduce) {
-}
-.tab-content > .tab-pane {
- display: none;
-}
-.tab-content > .active {
- display: block;
-}
-.navbar {
- position: relative;
- display: flex;
- flex-wrap: wrap;
- align-items: center;
- justify-content: space-between;
- padding: 0.25rem 0.5rem;
-}
-.navbar .container {
- display: flex;
- flex-wrap: wrap;
- align-items: center;
- justify-content: space-between;
-}
-.fixed-top {
- position: fixed;
- top: 0;
- right: 0;
- left: 0;
- z-index: 1030;
-}
-.mt-3 {
- margin-top: 1rem !important;
-}
-.mb-3 {
- margin-bottom: 1rem !important;
-}
-.text-nowrap {
- white-space: nowrap !important;
-}
-.font-weight-normal {
- font-weight: 400 !important;
-}
-.gl-form-input,
-.gl-form-input.form-control {
- background-color: #fff;
- font-family: var(--default-regular-font, "GitLab Sans"), -apple-system,
- BlinkMacSystemFont, "Segoe UI", Roboto, "Noto Sans", Ubuntu, Cantarell,
- "Helvetica Neue", sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
- "Segoe UI Symbol", "Noto Color Emoji";
- font-size: 0.875rem;
- line-height: 1rem;
- padding-top: 0.5rem;
- padding-bottom: 0.5rem;
- padding-left: 0.75rem;
- padding-right: 0.75rem;
- height: auto;
- color: #333238;
- box-shadow: inset 0 0 0 1px #89888d;
- border-style: none;
- appearance: none;
- -moz-appearance: none;
-}
-.gl-form-input:disabled,
-.gl-form-input:not(.form-control-plaintext):not([type="color"]):read-only,
-.gl-form-input.form-control:disabled,
-.gl-form-input.form-control:not(.form-control-plaintext):not([type="color"]):read-only {
- background-color: #fbfafd;
- box-shadow: inset 0 0 0 1px #dcdcde;
-}
-.gl-form-input:disabled,
-.gl-form-input.form-control:disabled {
- cursor: not-allowed;
- color: #737278;
-}
-.gl-form-input::placeholder,
-.gl-form-input.form-control::placeholder {
- color: #89888d;
-}
-.gl-form-checkbox {
- font-size: 0.875rem;
- line-height: 1rem;
- color: #333238;
-}
-.gl-form-checkbox .custom-control-input:disabled,
-.gl-form-checkbox .custom-control-input:disabled ~ .custom-control-label {
- cursor: not-allowed;
- color: #89888d;
-}
-.gl-form-checkbox.custom-control {
- padding-left: 1rem;
-}
-.gl-form-checkbox.custom-control .custom-control-input ~ .custom-control-label {
- cursor: pointer;
- padding-left: 0.5rem;
- margin-bottom: 0.5rem;
-}
-.gl-form-checkbox.custom-control
- .custom-control-input
- ~ .custom-control-label::before,
-.gl-form-checkbox.custom-control
- .custom-control-input
- ~ .custom-control-label::after {
- top: 0;
- left: -1rem;
-}
-.gl-form-checkbox.custom-control
- .custom-control-input
- ~ .custom-control-label::before {
- background-color: #fff;
- border-color: #89888d;
-}
-.gl-form-checkbox.custom-control
- .custom-control-input:checked
- ~ .custom-control-label::before {
- background-color: #1f75cb;
- border-color: #1f75cb;
-}
-.gl-form-checkbox.custom-control
- .custom-control-input[type="checkbox"]:checked
- ~ .custom-control-label::after,
-.gl-form-checkbox.custom-control
- .custom-control-input[type="checkbox"]:indeterminate
- ~ .custom-control-label::after {
- background: none;
- background-color: #fff;
- mask-repeat: no-repeat;
- mask-position: center center;
-}
-.gl-form-checkbox.custom-control
- .custom-control-input[type="checkbox"]:checked
- ~ .custom-control-label::after {
- mask-image: url('data:image/svg+xml,%3Csvg width="8" height="7" viewBox="0 0 8 7" fill="none" xmlns="http://www.w3.org/2000/svg"%3E%3Cpath d="M1 3.05299L2.99123 5L7 1" stroke="white" stroke-width="2"/%3E%3C/svg%3E%0A');
-}
-.gl-form-checkbox.custom-control
- .custom-control-input[type="checkbox"]:indeterminate
- ~ .custom-control-label::after {
- mask-image: url('data:image/svg+xml,%3Csvg width="8" height="2" viewBox="0 0 8 2" fill="none" xmlns="http://www.w3.org/2000/svg"%3E%3Cpath d="M0 1L8 1" stroke="white" stroke-width="2"/%3E%3C/svg%3E%0A');
-}
-.gl-form-checkbox.custom-control.custom-checkbox
- .custom-control-input:indeterminate
- ~ .custom-control-label::before {
- background-color: #1f75cb;
- border-color: #1f75cb;
-}
-.gl-form-checkbox.custom-control
- .custom-control-input:disabled
- ~ .custom-control-label {
- cursor: not-allowed;
-}
-.gl-form-checkbox.custom-control
- .custom-control-input:disabled
- ~ .custom-control-label::before {
- background-color: #ececef;
- border-color: #dcdcde;
- pointer-events: auto;
-}
-.gl-form-checkbox.custom-control
- .custom-control-input:checked:disabled
- ~ .custom-control-label::before,
-.gl-form-checkbox.custom-control
- .custom-control-input[type="checkbox"]:indeterminate:disabled
- ~ .custom-control-label::before {
- background-color: #dcdcde;
- border-color: #dcdcde;
-}
-.gl-form-checkbox.custom-control
- .custom-control-input:checked:disabled
- ~ .custom-control-label::after,
-.gl-form-checkbox.custom-control
- .custom-control-input[type="checkbox"]:indeterminate:disabled
- ~ .custom-control-label::after {
- background-color: #737278;
-}
-.gl-button {
- display: inline-flex;
-}
-.gl-button:not(.btn-link):active {
- text-decoration: none;
-}
-.gl-button.gl-button,
-.gl-button.gl-button.btn-block {
- border-width: 0;
- padding-top: 0.5rem;
- padding-bottom: 0.5rem;
- padding-left: 0.75rem;
- padding-right: 0.75rem;
- background-color: transparent;
- line-height: 1rem;
- color: #333238;
- fill: currentColor;
- box-shadow: inset 0 0 0 1px #bfbfc3;
- justify-content: center;
- align-items: center;
- font-size: 0.875rem;
- border-radius: 0.25rem;
-}
-.gl-button.gl-button .gl-button-text,
-.gl-button.gl-button.btn-block .gl-button-text {
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- padding-top: 1px;
- padding-bottom: 1px;
- margin-top: -1px;
- margin-bottom: -1px;
-}
-.gl-button.gl-button .gl-button-icon,
-.gl-button.gl-button.btn-block .gl-button-icon {
- height: 1rem;
- width: 1rem;
- flex-shrink: 0;
- margin-right: 0.25rem;
- top: auto;
-}
-.gl-button.gl-button.btn-default,
-.gl-button.gl-button.btn-block.btn-default {
- background-color: #fff;
-}
-.gl-button.gl-button.btn-default:active,
-.gl-button.gl-button.btn-default.active,
-.gl-button.gl-button.btn-block.btn-default:active,
-.gl-button.gl-button.btn-block.btn-default.active {
- box-shadow: inset 0 0 0 1px #626168, 0 0 0 1px #fff, 0 0 0 3px #428fdc;
- outline: none;
- background-color: #dcdcde;
-}
-.gl-button.gl-button.btn-confirm,
-.gl-button.gl-button.btn-block.btn-confirm {
- color: #fff;
-}
-.gl-button.gl-button.btn-confirm,
-.gl-button.gl-button.btn-block.btn-confirm {
- background-color: #1f75cb;
- box-shadow: inset 0 0 0 1px #1068bf;
-}
-.gl-button.gl-button.btn-confirm:active,
-.gl-button.gl-button.btn-confirm.active,
-.gl-button.gl-button.btn-block.btn-confirm:active,
-.gl-button.gl-button.btn-block.btn-confirm.active {
- box-shadow: inset 0 0 0 1px #033464, 0 0 0 1px #fff, 0 0 0 3px #428fdc;
- outline: none;
- background-color: #0b5cad;
-}
-body {
- font-size: 0.875rem;
-}
-button,
-[type="submit"] {
- cursor: pointer;
-}
-h1,
-h3 {
- margin-top: 20px;
- margin-bottom: 10px;
-}
-hr {
- overflow: hidden;
-}
-svg {
- vertical-align: baseline;
-}
-.form-control {
- font-size: 0.875rem;
-}
-.hidden {
- display: none !important;
- visibility: hidden !important;
-}
-html {
- overflow-y: scroll;
-}
-body.navless {
- background-color: #fff !important;
-}
-.container {
- padding-top: 0;
- z-index: 5;
-}
-.container .content {
- margin: 0;
-}
-@media (max-width: 575.98px) {
- .container .content {
- margin-top: 20px;
- }
-}
-.btn {
- border-radius: 4px;
- font-size: 0.875rem;
- font-weight: 400;
- padding: 6px 10px;
- background-color: #fff;
- border-color: #dcdcde;
- color: #333238;
- color: #333238;
- white-space: nowrap;
-}
-.btn:active {
- background-color: #ececef;
- box-shadow: none;
-}
-.btn:active,
-.btn.active {
- background-color: #e6e6ea;
- border-color: #dedee3;
- color: #333238;
-}
-.btn svg {
- height: 15px;
- width: 15px;
-}
-.btn svg:not(:last-child) {
- margin-right: 5px;
-}
-.btn-block {
- width: 100%;
- margin: 0;
-}
-.btn-block.btn {
- padding: 6px 0;
-}
-:root {
- --performance-bar-height: 0px;
- --system-header-height: 0px;
- --top-bar-height: 0px;
- --system-footer-height: 0px;
- --mr-review-bar-height: 0px;
- --breakpoint-xs: 0;
- --breakpoint-sm: 576px;
- --breakpoint-md: 768px;
- --breakpoint-lg: 992px;
- --breakpoint-xl: 1200px;
-}
-.tab-content {
- overflow: visible;
-}
-@media (max-width: 767.98px) {
- .tab-content {
- isolation: isolate;
- }
-}
-hr {
- margin: 1.5rem 0;
- border-top: 1px solid #ececef;
-}
-.flash-container {
- margin: 0;
- margin-bottom: 16px;
- font-size: 14px;
- position: relative;
- z-index: 1;
-}
-.flash-container.sticky {
- position: sticky;
- top: calc(
- var(--header-height, 48px) +
- calc(var(--system-header-height) + var(--performance-bar-height)) +
- var(--top-bar-height)
- );
- z-index: 251;
-}
-.flash-container.flash-container-page {
- margin-bottom: 0;
-}
-.flash-container:empty {
- margin: 0;
-}
-input {
- border-radius: 0.25rem;
- color: #333238;
- background-color: #fff;
-}
-label {
- font-weight: 600;
-}
-label.custom-control-label {
- font-weight: 400;
-}
-.form-control {
- border-radius: 4px;
- padding: 6px 10px;
-}
-.form-control::placeholder {
- color: #89888d;
-}
-.gl-show-field-errors .form-control:not(textarea) {
- height: 32px;
-}
-.navbar-empty {
- justify-content: center;
- height: var(--header-height, 48px);
- background: #fff;
- border-bottom: 1px solid #dcdcde;
-}
-.navbar-empty .tanuki-logo,
-.navbar-empty .brand-header-logo {
- max-height: 100%;
-}
-.tanuki-logo .tanuki {
- fill: #e24329;
-}
-.tanuki-logo .left-cheek,
-.tanuki-logo .right-cheek {
- fill: #fc6d26;
-}
-.tanuki-logo .chin {
- fill: #fca326;
-}
-input::-moz-placeholder {
- color: #89888d;
- opacity: 1;
-}
-input::-ms-input-placeholder {
- color: #89888d;
-}
-input:-ms-input-placeholder {
- color: #89888d;
-}
-svg {
- fill: currentColor;
-}
-
-.fixed-top {
- top: calc(var(--system-header-height) + var(--performance-bar-height));
-}
-.gl-display-flex {
- display: flex;
-}
-.gl-align-items-center {
- align-items: center;
-}
-.gl-flex-wrap {
- flex-wrap: wrap;
-}
-.gl-justify-content-space-between {
- justify-content: space-between;
-}
-.gl-align-self-end {
- align-self: flex-end;
-}
-.gl-w-10 {
- width: 3.5rem;
-}
-.gl-w-half {
- width: 50%;
-}
-.gl-w-full {
- width: 100%;
-}
-@media (max-width: 575.98px) {
- .gl-xs-w-full {
- width: 100%;
- }
-}
-.gl-h-full {
- height: 100%;
-}
-.gl-p-5 {
- padding: 1rem;
-}
-.gl-px-5 {
- padding-left: 1rem;
- padding-right: 1rem;
-}
-.gl-py-5 {
- padding-top: 1rem;
- padding-bottom: 1rem;
-}
-.gl-m-0 {
- margin: 0;
-}
-.gl-mt-3 {
- margin-top: 0.5rem;
-}
-.gl-mt-5 {
- margin-top: 1rem;
-}
-.gl-mr-auto {
- margin-right: auto;
-}
-.gl-mb-2 {
- margin-bottom: 0.25rem;
-}
-.gl-mb-3 {
- margin-bottom: 0.5rem;
-}
-.gl-ml-auto {
- margin-left: auto;
-}
-.gl-gap-5 {
- gap: 1rem;
-}
-@media (min-width: 576px) {
- .gl-sm-mt-0 {
- margin-top: 0;
- }
-}
-.gl-text-center {
- text-align: center;
-}
-.gl-text-right {
- text-align: right;
-}
-.gl-font-size-h2 {
- font-size: 1.1875rem;
-}
-.gl-font-weight-bold {
- font-weight: 600;
-}
-
-@import "startup/cloaking";
-@include cloak-startup-scss(none);
diff --git a/app/assets/stylesheets/themes/dark_mode_overrides.scss b/app/assets/stylesheets/themes/dark_mode_overrides.scss
index 73877c04c46..c0eced48171 100644
--- a/app/assets/stylesheets/themes/dark_mode_overrides.scss
+++ b/app/assets/stylesheets/themes/dark_mode_overrides.scss
@@ -105,7 +105,7 @@
--svg-status-bg: #{$white};
}
-body.gl-dark {
+:root.gl-dark {
// redefine some colors and values to prevent sourcegraph conflicts
color-scheme: dark;
--gray-10: #{$gray-10};
@@ -178,6 +178,10 @@ body.gl-dark {
}
}
+.gl-label-text-light .gl-label-close.gl-button:hover {
+ background-color: $gray-900;
+}
+
.gl-label-text-dark.gl-label-text-dark {
&,
.gl-label-close .gl-icon {
@@ -194,6 +198,10 @@ body.gl-dark {
}
}
+.gl-label-text-dark .gl-label-close.gl-button:hover {
+ background-color: $gray-10;
+}
+
// duplicated class as the original .atwho-view style is added later
.atwho-view.atwho-view {
background-color: $white;
@@ -231,7 +239,7 @@ aside.right-sidebar:not(.right-sidebar-merge-requests) {
border-left-color: $gray-50;
}
-body.gl-dark {
+:root.gl-dark {
@include gitlab-theme($gray-900, $gray-400, $gray-500, $gray-900, $white);
.terms {
diff --git a/app/assets/stylesheets/themes/theme_blue.scss b/app/assets/stylesheets/themes/theme_blue.scss
index 06f3e13e99e..749120a0ecb 100644
--- a/app/assets/stylesheets/themes/theme_blue.scss
+++ b/app/assets/stylesheets/themes/theme_blue.scss
@@ -1,6 +1,6 @@
@import './theme_helper';
-body {
+:root {
&.ui-blue {
@include gitlab-theme(
$theme-blue-200,
diff --git a/app/assets/stylesheets/themes/theme_gray.scss b/app/assets/stylesheets/themes/theme_gray.scss
index 3112aaef227..70611e692cd 100644
--- a/app/assets/stylesheets/themes/theme_gray.scss
+++ b/app/assets/stylesheets/themes/theme_gray.scss
@@ -1,6 +1,6 @@
@import './theme_helper';
-body {
+:root {
&.ui-gray {
@include gitlab-theme(
$gray-200,
diff --git a/app/assets/stylesheets/themes/theme_green.scss b/app/assets/stylesheets/themes/theme_green.scss
index c9ea1162206..ae969873692 100644
--- a/app/assets/stylesheets/themes/theme_green.scss
+++ b/app/assets/stylesheets/themes/theme_green.scss
@@ -1,6 +1,6 @@
@import './theme_helper';
-body {
+:root {
&.ui-green {
@include gitlab-theme(
$theme-green-200,
diff --git a/app/assets/stylesheets/themes/theme_indigo.scss b/app/assets/stylesheets/themes/theme_indigo.scss
index 78ce96667d4..d7e8ddadf46 100644
--- a/app/assets/stylesheets/themes/theme_indigo.scss
+++ b/app/assets/stylesheets/themes/theme_indigo.scss
@@ -1,6 +1,6 @@
@import './theme_helper';
-body {
+:root {
&.ui-indigo {
@include gitlab-theme(
$indigo-200,
diff --git a/app/assets/stylesheets/themes/theme_light_blue.scss b/app/assets/stylesheets/themes/theme_light_blue.scss
index 73fe072393f..430960f563f 100644
--- a/app/assets/stylesheets/themes/theme_light_blue.scss
+++ b/app/assets/stylesheets/themes/theme_light_blue.scss
@@ -1,6 +1,6 @@
@import './theme_helper';
-body {
+:root {
&.ui-light-blue {
@include gitlab-theme(
$theme-light-blue-200,
diff --git a/app/assets/stylesheets/themes/theme_light_gray.scss b/app/assets/stylesheets/themes/theme_light_gray.scss
index e8357647f48..f63da3f22f1 100644
--- a/app/assets/stylesheets/themes/theme_light_gray.scss
+++ b/app/assets/stylesheets/themes/theme_light_gray.scss
@@ -1,6 +1,6 @@
@import './theme_helper';
-body {
+:root {
&.ui-light-gray {
@include gitlab-theme(
$gray-500,
diff --git a/app/assets/stylesheets/themes/theme_light_green.scss b/app/assets/stylesheets/themes/theme_light_green.scss
index 6b058b2dd7b..05adc56c36a 100644
--- a/app/assets/stylesheets/themes/theme_light_green.scss
+++ b/app/assets/stylesheets/themes/theme_light_green.scss
@@ -1,6 +1,6 @@
@import './theme_helper';
-body {
+:root {
&.ui-light-green {
@include gitlab-theme(
$theme-green-200,
diff --git a/app/assets/stylesheets/themes/theme_light_indigo.scss b/app/assets/stylesheets/themes/theme_light_indigo.scss
index ff12366466a..04bcfaf8366 100644
--- a/app/assets/stylesheets/themes/theme_light_indigo.scss
+++ b/app/assets/stylesheets/themes/theme_light_indigo.scss
@@ -1,6 +1,6 @@
@import './theme_helper';
-body {
+:root {
&.ui-light-indigo {
@include gitlab-theme(
$indigo-200,
diff --git a/app/assets/stylesheets/themes/theme_light_red.scss b/app/assets/stylesheets/themes/theme_light_red.scss
index 3ae67309014..c4952b8e155 100644
--- a/app/assets/stylesheets/themes/theme_light_red.scss
+++ b/app/assets/stylesheets/themes/theme_light_red.scss
@@ -1,6 +1,6 @@
@import './theme_helper';
-body {
+:root {
&.ui-light-red {
@include gitlab-theme(
$theme-light-red-200,
diff --git a/app/assets/stylesheets/themes/theme_red.scss b/app/assets/stylesheets/themes/theme_red.scss
index 82de30e8b0e..536963e12ef 100644
--- a/app/assets/stylesheets/themes/theme_red.scss
+++ b/app/assets/stylesheets/themes/theme_red.scss
@@ -1,6 +1,6 @@
@import './theme_helper';
-body {
+:root {
&.ui-red {
@include gitlab-theme(
$theme-red-200,
diff --git a/app/assets/stylesheets/tmp_utilities.scss b/app/assets/stylesheets/tmp_utilities.scss
new file mode 100644
index 00000000000..96464aa5a39
--- /dev/null
+++ b/app/assets/stylesheets/tmp_utilities.scss
@@ -0,0 +1,32 @@
+/**
+ * DISCLAIMER
+ * This is a temporary stylesheet meant to assist in migrating away from desktop-first responsive
+ * CSS utilities.
+ * DO NOT add utils in here unless you are actively taking part in in the migration.
+ * We needed this new file for temporary utils to be defined _after_ the main, non-responsive
+ * GitLab UI util.
+ * This file is scheduled to be removed by the end of 2023.
+ */
+ .gl-sm-w-25p {
+ @include gl-media-breakpoint-up(sm) {
+ width: 25%;
+ }
+}
+
+.gl-sm-w-30p {
+ @include gl-media-breakpoint-up(sm) {
+ width: 30%;
+ }
+}
+
+.gl-sm-w-40p {
+ @include gl-media-breakpoint-up(sm) {
+ width: 40%;
+ }
+}
+
+.gl-sm-w-75p {
+ @include gl-media-breakpoint-up(sm) {
+ width: 75%;
+ }
+}
diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss
index 8fe45d4bb9d..347b8e20ab4 100644
--- a/app/assets/stylesheets/utilities.scss
+++ b/app/assets/stylesheets/utilities.scss
@@ -65,9 +65,6 @@
min-width: 0;
}
-// .gl-font-size-inherit will be moved to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1466
-.gl-font-size-inherit,
-.font-size-inherit { font-size: inherit; }
.gl-w-16 { width: px-to-rem($grid-size * 2); }
.gl-w-64 { width: px-to-rem($grid-size * 8); }
.gl-h-32 { height: px-to-rem($grid-size * 4); }
diff --git a/app/components/projects/ml/models_index_component.rb b/app/components/projects/ml/models_index_component.rb
index 57900165ad1..5754c2a1fa9 100644
--- a/app/components/projects/ml/models_index_component.rb
+++ b/app/components/projects/ml/models_index_component.rb
@@ -3,10 +3,11 @@
module Projects
module Ml
class ModelsIndexComponent < ViewComponent::Base
- attr_reader :paginator
+ attr_reader :paginator, :model_count
- def initialize(paginator:)
+ def initialize(paginator:, model_count:)
@paginator = paginator
+ @model_count = model_count
end
private
@@ -14,7 +15,8 @@ module Projects
def view_model
vm = {
models: models_view_model,
- page_info: page_info_view_model
+ page_info: page_info_view_model,
+ model_count: model_count
}
Gitlab::Json.generate(vm.deep_transform_keys { |k| k.to_s.camelize(:lower) })
@@ -26,7 +28,8 @@ module Projects
name: m.name,
version: m.latest_version_name,
version_count: m.version_count,
- path: m.latest_package_path
+ version_package_path: m.latest_package_path,
+ version_path: m.latest_version_path
}
end
end
diff --git a/app/components/projects/ml/show_ml_model_component.rb b/app/components/projects/ml/show_ml_model_component.rb
index 2fe2c7e7e9d..d349c0a22e9 100644
--- a/app/components/projects/ml/show_ml_model_component.rb
+++ b/app/components/projects/ml/show_ml_model_component.rb
@@ -16,11 +16,22 @@ module Projects
model: {
id: model.id,
name: model.name,
- path: model.path
+ path: model.path,
+ description: "This is a placeholder for the short description",
+ latest_version: latest_version_view_model,
+ version_count: model.version_count
}
}
- Gitlab::Json.generate(vm)
+ Gitlab::Json.generate(vm.deep_transform_keys { |k| k.to_s.camelize(:lower) })
+ end
+
+ def latest_version_view_model
+ return unless model.latest_version
+
+ {
+ version: model.latest_version.version
+ }
end
end
end
diff --git a/app/components/projects/ml/show_ml_model_version_component.html.haml b/app/components/projects/ml/show_ml_model_version_component.html.haml
new file mode 100644
index 00000000000..7410e648306
--- /dev/null
+++ b/app/components/projects/ml/show_ml_model_version_component.html.haml
@@ -0,0 +1 @@
+#js-mount-show-ml-model-version{ data: { view_model: view_model } }
diff --git a/app/components/projects/ml/show_ml_model_version_component.rb b/app/components/projects/ml/show_ml_model_version_component.rb
new file mode 100644
index 00000000000..ae81642a891
--- /dev/null
+++ b/app/components/projects/ml/show_ml_model_version_component.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Projects
+ module Ml
+ class ShowMlModelVersionComponent < ViewComponent::Base
+ attr_reader :model_version, :model
+
+ def initialize(model_version:)
+ @model_version = model_version.present
+ @model = model_version.model.present
+ end
+
+ private
+
+ def view_model
+ vm = {
+ model_version: {
+ id: model_version.id,
+ version: model_version.version,
+ path: model_version.path,
+ model: {
+ name: model.name,
+ path: model.path
+ }
+ }
+ }
+
+ Gitlab::Json.generate(vm.deep_transform_keys { |k| k.to_s.camelize(:lower) })
+ end
+ end
+ end
+end
diff --git a/app/controllers/acme_challenges_controller.rb b/app/controllers/acme_challenges_controller.rb
index a187e43b3df..4a7706db94e 100644
--- a/app/controllers/acme_challenges_controller.rb
+++ b/app/controllers/acme_challenges_controller.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
-class AcmeChallengesController < BaseActionController
+# rubocop:disable Rails/ApplicationController
+class AcmeChallengesController < ActionController::Base
def show
if acme_order
render plain: acme_order.challenge_file_content, content_type: 'text/plain'
@@ -15,3 +16,4 @@ class AcmeChallengesController < BaseActionController
@acme_order ||= PagesDomainAcmeOrder.find_by_domain_and_token(params[:domain], params[:token])
end
end
+# rubocop:enable Rails/ApplicationController
diff --git a/app/controllers/admin/abuse_reports_controller.rb b/app/controllers/admin/abuse_reports_controller.rb
index b48d6f4f7c2..d5c505ba1dd 100644
--- a/app/controllers/admin/abuse_reports_controller.rb
+++ b/app/controllers/admin/abuse_reports_controller.rb
@@ -7,6 +7,7 @@ class Admin::AbuseReportsController < Admin::ApplicationController
before_action :find_abuse_report, only: [:show, :moderate_user, :update, :destroy]
before_action only: :show do
push_frontend_feature_flag(:abuse_report_labels)
+ push_frontend_feature_flag(:abuse_report_notes)
end
def index
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index be1edeb0d37..8cf0ab60fd3 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -12,10 +12,10 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
before_action :set_application_setting, except: :integrations
before_action :disable_query_limiting, only: [:usage_data]
+ before_action :prerecorded_service_ping_data, only: [:metrics_and_profiling] # rubocop:disable Rails/LexicallyScopedActionFilter
before_action do
push_frontend_feature_flag(:ci_variables_pages, current_user)
- push_frontend_feature_flag(:ci_variable_drawer, current_user)
end
feature_category :not_owned, [ # rubocop:todo Gitlab/AvoidFeatureCategoryNotOwned
@@ -30,7 +30,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
feature_category :source_code_management, [:repository, :clear_repository_check_states]
feature_category :continuous_integration, [:ci_cd, :reset_registration_token]
urgency :low, [:ci_cd, :reset_registration_token]
- feature_category :service_ping, [:usage_data, :service_usage_data]
+ feature_category :service_ping, [:usage_data]
feature_category :integrations, [:integrations, :slack_app_manifest_share, :slack_app_manifest_download]
feature_category :pages, [:lets_encrypt_terms_of_service]
feature_category :error_tracking, [:reset_error_tracking_access_token]
@@ -56,18 +56,16 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
@integrations = Integration.find_or_initialize_all_non_project_specific(Integration.for_instance).sort_by(&:title)
end
- def service_usage_data
- @service_ping_data_present = prerecorded_service_ping_data.present?
- end
-
def update
perform_update
end
def usage_data
+ return not_found unless prerecorded_service_ping_data.present?
+
respond_to do |format|
format.html do
- usage_data_json = Gitlab::Json.pretty_generate(service_ping_data)
+ usage_data_json = Gitlab::Json.pretty_generate(prerecorded_service_ping_data)
render html: Gitlab::Highlight.highlight('payload.json', usage_data_json, language: 'json')
end
@@ -75,7 +73,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
format.json do
Gitlab::UsageDataCounters::ServiceUsageDataCounter.count(:download_payload_click)
- render json: Gitlab::Json.dump(service_ping_data)
+ render json: Gitlab::Json.dump(prerecorded_service_ping_data)
end
end
end
@@ -243,12 +241,9 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
VALID_SETTING_PANELS
end
- def service_ping_data
- prerecorded_service_ping_data || Gitlab::Usage::ServicePingReport.for(output: :all_metrics_values)
- end
-
def prerecorded_service_ping_data
- Rails.cache.fetch(Gitlab::Usage::ServicePingReport::CACHE_KEY) || ::RawUsageData.for_current_reporting_cycle.first&.payload
+ @service_ping_data ||= Rails.cache.fetch(Gitlab::Usage::ServicePingReport::CACHE_KEY) ||
+ ::RawUsageData.for_current_reporting_cycle.first&.payload
end
end
diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb
index dab0f3e870a..a03e0c0807f 100644
--- a/app/controllers/admin/dashboard_controller.rb
+++ b/app/controllers/admin/dashboard_controller.rb
@@ -13,8 +13,7 @@ class Admin::DashboardController < Admin::ApplicationController
@projects = Project.order_id_desc.without_deleted.with_route.limit(10)
@users = User.order_id_desc.limit(10)
@groups = Group.order_id_desc.with_route.limit(10)
- @notices = Gitlab::ConfigChecker::PumaRuggedChecker.check
- @notices += Gitlab::ConfigChecker::ExternalDatabaseChecker.check
+ @notices = Gitlab::ConfigChecker::ExternalDatabaseChecker.check
@redis_versions = Gitlab::Redis::ALL_CLASSES.map(&:version).uniq
end
diff --git a/app/controllers/admin/spam_logs_controller.rb b/app/controllers/admin/spam_logs_controller.rb
index b27185a6add..d7ed6aa33ef 100644
--- a/app/controllers/admin/spam_logs_controller.rb
+++ b/app/controllers/admin/spam_logs_controller.rb
@@ -5,7 +5,9 @@ class Admin::SpamLogsController < Admin::ApplicationController
# rubocop: disable CodeReuse/ActiveRecord
def index
- @spam_logs = SpamLog.includes(:user).order(id: :desc).page(params[:page]).without_count
+ @spam_logs = SpamLog.preload(user: [:trusted_with_spam_attribute])
+ .order(id: :desc)
+ .page(params[:page]).without_count
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index 1f05e4e7b21..ee78d5a8c35 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -164,6 +164,26 @@ class Admin::UsersController < Admin::ApplicationController
end
end
+ def trust
+ result = Users::TrustService.new(current_user).execute(user)
+
+ if result[:status] == :success
+ redirect_back_or_admin_user(notice: _("Successfully trusted"))
+ else
+ redirect_back_or_admin_user(alert: _("Error occurred. User was not updated"))
+ end
+ end
+
+ def untrust
+ result = Users::UntrustService.new(current_user).execute(user)
+
+ if result[:status] == :success
+ redirect_back_or_admin_user(notice: _("Successfully untrusted"))
+ else
+ redirect_back_or_admin_user(alert: _("Error occurred. User was not updated"))
+ end
+ end
+
def confirm
if update_user(&:force_confirm)
redirect_back_or_admin_user(notice: _("Successfully confirmed"))
@@ -290,7 +310,7 @@ class Admin::UsersController < Admin::ApplicationController
end
def users_with_included_associations(users)
- users.includes(:authorized_projects) # rubocop: disable CodeReuse/ActiveRecord
+ users.includes(:authorized_projects, :trusted_with_spam_attribute) # rubocop: disable CodeReuse/ActiveRecord
end
def admin_making_changes_for_another_user?
@@ -342,6 +362,7 @@ class Admin::UsersController < Admin::ApplicationController
:bio,
:can_create_group,
:color_scheme_id,
+ :discord,
:email,
:extern_uid,
:external,
@@ -350,6 +371,7 @@ class Admin::UsersController < Admin::ApplicationController
:hide_no_ssh_key,
:key_id,
:linkedin,
+ :mastodon,
:name,
:password_expires_at,
:projects_limit,
@@ -358,7 +380,6 @@ class Admin::UsersController < Admin::ApplicationController
:skype,
:theme_id,
:twitter,
- :discord,
:username,
:website_url,
:note,
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index f60da46826a..6739fc57a1f 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -3,7 +3,7 @@
require 'gon'
require 'fogbugz'
-class ApplicationController < BaseActionController
+class ApplicationController < ActionController::Base
include Gitlab::GonHelper
include Gitlab::NoCacheHeaders
include GitlabRoutingHelper
diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb
index c9cb1ca14e2..1c2bd10bc81 100644
--- a/app/controllers/autocomplete_controller.rb
+++ b/app/controllers/autocomplete_controller.rb
@@ -3,16 +3,18 @@
class AutocompleteController < ApplicationController
include SearchRateLimitable
- skip_before_action :authenticate_user!, only: [:users, :award_emojis, :merge_request_target_branches]
+ skip_before_action :authenticate_user!, only: [
+ :users, :award_emojis, :merge_request_target_branches, :merge_request_source_branches
+ ]
before_action :check_search_rate_limit!, only: [:users, :projects]
feature_category :user_profile, [:users, :user]
feature_category :groups_and_projects, [:projects]
feature_category :team_planning, [:award_emojis]
- feature_category :code_review_workflow, [:merge_request_target_branches]
+ feature_category :code_review_workflow, [:merge_request_target_branches, :merge_request_source_branches]
feature_category :continuous_delivery, [:deploy_keys_with_owners]
- urgency :low, [:merge_request_target_branches, :deploy_keys_with_owners, :users]
+ urgency :low, [:merge_request_target_branches, :merge_request_source_branches, :deploy_keys_with_owners, :users]
urgency :low, [:award_emojis]
urgency :medium, [:projects]
@@ -62,14 +64,11 @@ class AutocompleteController < ApplicationController
end
def merge_request_target_branches
- if target_branch_params.present?
- merge_requests = MergeRequestsFinder.new(current_user, target_branch_params).execute
- target_branches = merge_requests.recent_target_branches
+ merge_request_branches(target: true)
+ end
- render json: target_branches.map { |target_branch| { title: target_branch } }
- else
- render json: { error: _('At least one of group_id or project_id must be specified') }, status: :bad_request
- end
+ def merge_request_source_branches
+ merge_request_branches(source: true)
end
def deploy_keys_with_owners
@@ -90,7 +89,7 @@ class AutocompleteController < ApplicationController
.execute
end
- def target_branch_params
+ def branch_params
params.permit(:group_id, :project_id).select { |_, v| v.present? }
end
@@ -98,6 +97,21 @@ class AutocompleteController < ApplicationController
def presented_suggested_users
[]
end
+
+ def merge_request_branches(source: false, target: false)
+ if branch_params.present?
+ merge_requests = MergeRequestsFinder.new(current_user, branch_params).execute
+
+ branches = []
+
+ branches.concat(merge_requests.recent_source_branches) if source
+ branches.concat(merge_requests.recent_target_branches) if target
+
+ render json: branches.map { |branch| { title: branch } }
+ else
+ render json: { error: _('At least one of group_id or project_id must be specified') }, status: :bad_request
+ end
+ end
end
AutocompleteController.prepend_mod_with('AutocompleteController')
diff --git a/app/controllers/base_action_controller.rb b/app/controllers/base_action_controller.rb
deleted file mode 100644
index af2c9e98778..00000000000
--- a/app/controllers/base_action_controller.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-# frozen_string_literal: true
-
-# GitLab lightweight base action controller
-#
-# This class should be limited to content that
-# is desired/required for *all* controllers in
-# GitLab.
-#
-# Most controllers inherit from `ApplicationController`.
-# Some controllers don't want or need all of that
-# logic and instead inherit from `ActionController::Base`.
-# This makes it difficult to set security headers and
-# handle other critical logic across *all* controllers.
-#
-# Between this controller and `ApplicationController`
-# no controller should ever inherit directly from
-# `ActionController::Base`
-#
-# rubocop:disable Rails/ApplicationController
-# rubocop:disable Gitlab/NamespacedClass
-class BaseActionController < ActionController::Base
- before_action :security_headers
-
- private
-
- def security_headers
- headers['Cross-Origin-Opener-Policy'] = 'same-origin' if ::Feature.enabled?(:coop_header)
- end
-end
-# rubocop:enable Gitlab/NamespacedClass
-# rubocop:enable Rails/ApplicationController
diff --git a/app/controllers/chaos_controller.rb b/app/controllers/chaos_controller.rb
index b61a8c5ff12..7328b793b09 100644
--- a/app/controllers/chaos_controller.rb
+++ b/app/controllers/chaos_controller.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
-class ChaosController < BaseActionController
+# rubocop:disable Rails/ApplicationController
+class ChaosController < ActionController::Base
before_action :validate_chaos_secret, unless: :development_or_test?
def leakmem
@@ -94,3 +95,4 @@ class ChaosController < BaseActionController
Rails.env.development? || Rails.env.test?
end
end
+# rubocop:enable Rails/ApplicationController
diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb
index 27f1d1f5528..5009bf7ff0c 100644
--- a/app/controllers/concerns/creates_commit.rb
+++ b/app/controllers/concerns/creates_commit.rb
@@ -3,6 +3,7 @@
module CreatesCommit
extend ActiveSupport::Concern
include Gitlab::Utils::StrongMemoize
+ include SafeFormatHelper
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def create_commit(service, success_path:, failure_path:, failure_view: nil, success_notice: nil, target_project: nil)
@@ -31,10 +32,10 @@ module CreatesCommit
result = service.new(@project_to_commit_into, current_user, commit_params).execute
if result[:status] == :success
- update_flash_notice(success_notice)
-
success_path = final_success_path(success_path, target_project)
+ update_flash_notice(success_notice, success_path)
+
respond_to do |format|
format.html { redirect_to success_path }
format.json { render json: { message: _("success"), filePath: success_path } }
@@ -65,8 +66,13 @@ module CreatesCommit
private
- def update_flash_notice(success_notice)
- flash[:notice] = success_notice || _("Your changes have been successfully committed.")
+ def update_flash_notice(success_notice, success_path)
+ changes_link = ActionController::Base.helpers.link_to _('changes'), success_path, class: 'gl-link'
+
+ default_message = safe_format(_("Your %{changes_link} have been committed successfully."),
+ changes_link: changes_link)
+
+ flash[:notice] = success_notice || default_message
if create_merge_request?
flash[:notice] =
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index 28e1056092d..cd2372825ac 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -147,6 +147,8 @@ module IssuableActions
finder = Issuable::DiscussionsListService.new(current_user, issuable, finder_params_for_issuable)
discussion_notes = finder.execute
+ yield discussion_notes if block_given?
+
if finder.paginator.present? && finder.paginator.has_next_page?
response.headers['X-Next-Page-Cursor'] = finder.paginator.cursor_for_next_page
end
diff --git a/app/controllers/concerns/render_access_tokens.rb b/app/controllers/concerns/render_access_tokens.rb
index b0bbad7e37f..43e4686e66f 100644
--- a/app/controllers/concerns/render_access_tokens.rb
+++ b/app/controllers/concerns/render_access_tokens.rb
@@ -1,4 +1,5 @@
# frozen_string_literal: true
+
module RenderAccessTokens
extend ActiveSupport::Concern
diff --git a/app/controllers/concerns/wiki_actions.rb b/app/controllers/concerns/wiki_actions.rb
index c606ccf4a07..f8c3e125c3b 100644
--- a/app/controllers/concerns/wiki_actions.rb
+++ b/app/controllers/concerns/wiki_actions.rb
@@ -246,7 +246,7 @@ module WikiActions
@sidebar_page = wiki.find_sidebar(params[:version_id])
unless @sidebar_page # Fallback to default sidebar
- @sidebar_wiki_entries, @sidebar_limited = wiki.sidebar_entries
+ @sidebar_wiki_entries, @sidebar_limited = wiki.sidebar_entries(load_content: Feature.enabled?(:wiki_front_matter_title, container))
end
rescue ::Gitlab::Git::CommandTimedOut => e
@sidebar_error = e
@@ -326,7 +326,9 @@ module WikiActions
end
def load_content?
- return false if %w[history destroy diff show].include?(params[:action])
+ skip_actions = Feature.enabled?(:wiki_front_matter_title, container) ? %w[history destroy diff] : %w[history destroy diff show]
+
+ return false if skip_actions.include?(params[:action])
true
end
diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb
index 188a8540a58..a0997484c58 100644
--- a/app/controllers/dashboard_controller.rb
+++ b/app/controllers/dashboard_controller.rb
@@ -14,6 +14,7 @@ class DashboardController < Dashboard::ApplicationController
before_action only: :issues do
push_frontend_feature_flag(:frontend_caching)
+ push_frontend_feature_flag(:group_multi_select_tokens)
end
before_action only: :merge_requests do
diff --git a/app/controllers/explore/catalog_controller.rb b/app/controllers/explore/catalog_controller.rb
new file mode 100644
index 00000000000..3cd3771129e
--- /dev/null
+++ b/app/controllers/explore/catalog_controller.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Explore
+ class CatalogController < Explore::ApplicationController
+ feature_category :pipeline_composition
+ before_action :check_feature_flag
+
+ def show; end
+
+ def index
+ render 'show'
+ end
+
+ private
+
+ def check_feature_flag
+ render_404 unless Feature.enabled?(:global_ci_catalog, current_user)
+ end
+ end
+end
diff --git a/app/controllers/external_redirect/external_redirect_controller.rb b/app/controllers/external_redirect/external_redirect_controller.rb
new file mode 100644
index 00000000000..532196157b7
--- /dev/null
+++ b/app/controllers/external_redirect/external_redirect_controller.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module ExternalRedirect
+ class ExternalRedirectController < ApplicationController
+ feature_category :navigation
+ skip_before_action :authenticate_user!
+ before_action :check_url_param
+
+ def index
+ if known_url?
+ redirect_to url_param
+ else
+ render layout: 'fullscreen', locals: {
+ minimal: true,
+ url: url_param
+ }
+ end
+ end
+
+ private
+
+ def url_param
+ params['url']&.strip
+ end
+
+ def known_url?
+ uri_data = Addressable::URI.parse(url_param)
+
+ uri_data.site == Gitlab.config.gitlab.url
+ end
+
+ def check_url_param
+ render_404 unless ::Gitlab::UrlSanitizer.valid_web?(url_param)
+ end
+ end
+end
diff --git a/app/controllers/groups/settings/applications_controller.rb b/app/controllers/groups/settings/applications_controller.rb
index 3ae1ae824a0..5aea078db17 100644
--- a/app/controllers/groups/settings/applications_controller.rb
+++ b/app/controllers/groups/settings/applications_controller.rb
@@ -5,7 +5,7 @@ module Groups
class ApplicationsController < Groups::ApplicationController
include OauthApplications
- prepend_before_action :authorize_admin_group!
+ before_action :authorize_admin_group!
before_action :set_application, only: [:show, :edit, :update, :renew, :destroy]
before_action :load_scopes, only: [:index, :create, :edit, :update]
diff --git a/app/controllers/groups/settings/ci_cd_controller.rb b/app/controllers/groups/settings/ci_cd_controller.rb
index f50cdd2b1de..371db7b30b6 100644
--- a/app/controllers/groups/settings/ci_cd_controller.rb
+++ b/app/controllers/groups/settings/ci_cd_controller.rb
@@ -15,7 +15,6 @@ module Groups
before_action do
push_frontend_feature_flag(:ci_variables_pages, current_user)
- push_frontend_feature_flag(:ci_variable_drawer, current_user)
end
urgency :low
diff --git a/app/controllers/groups/work_items_controller.rb b/app/controllers/groups/work_items_controller.rb
index bd85f12119b..ece279da778 100644
--- a/app/controllers/groups/work_items_controller.rb
+++ b/app/controllers/groups/work_items_controller.rb
@@ -4,6 +4,13 @@ module Groups
class WorkItemsController < Groups::ApplicationController
feature_category :team_planning
+ before_action do
+ push_force_frontend_feature_flag(:work_items, group&.work_items_feature_flag_enabled?)
+ push_force_frontend_feature_flag(:work_items_mvc, group&.work_items_mvc_feature_flag_enabled?)
+ push_force_frontend_feature_flag(:work_items_mvc_2, group&.work_items_mvc_2_feature_flag_enabled?)
+ push_force_frontend_feature_flag(:linked_work_items, group&.linked_work_items_feature_flag_enabled?)
+ end
+
def index
not_found unless Feature.enabled?(:namespace_level_work_items, group)
end
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index edc590e1370..5b9b3b7de11 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -36,7 +36,11 @@ class GroupsController < Groups::ApplicationController
push_frontend_feature_flag(:or_issuable_queries, group)
push_frontend_feature_flag(:frontend_caching, group)
push_force_frontend_feature_flag(:work_items, group.work_items_feature_flag_enabled?)
+ push_force_frontend_feature_flag(:work_items_mvc, group.work_items_mvc_feature_flag_enabled?)
+ push_force_frontend_feature_flag(:work_items_mvc_2, group.work_items_mvc_2_feature_flag_enabled?)
+ push_force_frontend_feature_flag(:linked_work_items, group.linked_work_items_feature_flag_enabled?)
push_frontend_feature_flag(:issues_grid_view)
+ push_frontend_feature_flag(:group_multi_select_tokens, group)
end
before_action only: :merge_requests do
@@ -275,6 +279,7 @@ class GroupsController < Groups::ApplicationController
:avatar,
:description,
:emails_disabled,
+ :emails_enabled,
:show_diff_preview_in_email,
:mentions_disabled,
:lfs_enabled,
diff --git a/app/controllers/health_controller.rb b/app/controllers/health_controller.rb
index 2b2db2f950c..1381999ab4c 100644
--- a/app/controllers/health_controller.rb
+++ b/app/controllers/health_controller.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
-class HealthController < BaseActionController
+# rubocop:disable Rails/ApplicationController
+class HealthController < ActionController::Base
protect_from_forgery with: :exception, prepend: true
include RequiresAllowlistedMonitoringClient
@@ -39,3 +40,4 @@ class HealthController < BaseActionController
render json: result.json, status: result.http_status
end
end
+# rubocop:enable Rails/ApplicationController
diff --git a/app/controllers/import/bulk_imports_controller.rb b/app/controllers/import/bulk_imports_controller.rb
index a8ec738caf4..bc425323d6f 100644
--- a/app/controllers/import/bulk_imports_controller.rb
+++ b/app/controllers/import/bulk_imports_controller.rb
@@ -6,6 +6,10 @@ class Import::BulkImportsController < ApplicationController
before_action :ensure_bulk_import_enabled
before_action :verify_blocked_uri, only: :status
+ before_action only: [:history] do
+ push_frontend_feature_flag(:bulk_import_details_page)
+ end
+
feature_category :importers
urgency :low
@@ -49,6 +53,10 @@ class Import::BulkImportsController < ApplicationController
end
end
+ def details
+ render_404 unless Feature.enabled?(:bulk_import_details_page)
+ end
+
def create
return render json: { success: false }, status: :too_many_requests if throttled_request?
return render json: { success: false }, status: :unprocessable_entity unless valid_create_params?
diff --git a/app/controllers/jira_connect/subscriptions_controller.rb b/app/controllers/jira_connect/subscriptions_controller.rb
index 773ef2bddca..17a79f83a78 100644
--- a/app/controllers/jira_connect/subscriptions_controller.rb
+++ b/app/controllers/jira_connect/subscriptions_controller.rb
@@ -48,7 +48,7 @@ class JiraConnect::SubscriptionsController < JiraConnect::ApplicationController
def destroy
subscription = current_jira_installation.subscriptions.find(params[:id])
- if !jira_user&.site_admin?
+ if !jira_user&.jira_admin?
render json: { error: 'forbidden' }, status: :forbidden
elsif subscription.destroy
render json: { success: true }
diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb
index 84ccfbc603a..83409c7e096 100644
--- a/app/controllers/jwt_controller.rb
+++ b/app/controllers/jwt_controller.rb
@@ -33,7 +33,7 @@ class JwtController < ApplicationController
@authentication_result = Gitlab::Auth::Result.new(nil, nil, :none, Gitlab::Auth.read_only_authentication_abilities)
authenticate_with_http_basic do |login, password|
- @authentication_result = Gitlab::Auth.find_for_git_client(login, password, project: nil, ip: request.ip)
+ @authentication_result = Gitlab::Auth.find_for_git_client(login, password, project: nil, request: request)
if @authentication_result.failed?
log_authentication_failed(login, @authentication_result)
@@ -98,11 +98,7 @@ class JwtController < ApplicationController
return unless params[:scope].present?
scopes = Array(Rack::Utils.parse_query(request.query_string)['scope'])
- if Feature.enabled?(:jwt_auth_space_delimited_scopes, Feature.current_request)
- scopes.flat_map(&:split)
- else
- scopes
- end
+ scopes.flat_map(&:split)
end
def auth_user
diff --git a/app/controllers/metrics_controller.rb b/app/controllers/metrics_controller.rb
index 61851fd1c60..9f41c092fa0 100644
--- a/app/controllers/metrics_controller.rb
+++ b/app/controllers/metrics_controller.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
-class MetricsController < BaseActionController
+# rubocop:disable Rails/ApplicationController
+class MetricsController < ActionController::Base
include RequiresAllowlistedMonitoringClient
protect_from_forgery with: :exception, prepend: true
@@ -35,3 +36,4 @@ class MetricsController < BaseActionController
)
end
end
+# rubocop:enable Rails/ApplicationController
diff --git a/app/controllers/oauth/jira_dvcs/authorizations_controller.rb b/app/controllers/oauth/jira_dvcs/authorizations_controller.rb
deleted file mode 100644
index ba587944a36..00000000000
--- a/app/controllers/oauth/jira_dvcs/authorizations_controller.rb
+++ /dev/null
@@ -1,86 +0,0 @@
-# frozen_string_literal: true
-
-# This controller's role is to mimic and rewire the GitLab OAuth
-# flow routes for Jira DVCS integration.
-# See https://gitlab.com/gitlab-org/gitlab/issues/2381
-#
-class Oauth::JiraDvcs::AuthorizationsController < ApplicationController
- skip_before_action :authenticate_user!
- skip_before_action :verify_authenticity_token
-
- before_action :reversible_end_of_life!
- before_action :validate_redirect_uri, only: :new
-
- feature_category :integrations
-
- # 1. Rewire Jira OAuth initial request to our stablished OAuth authorization URL.
- def new
- session[:redirect_uri] = params['redirect_uri']
-
- redirect_to oauth_authorization_path(
- client_id: params['client_id'],
- response_type: 'code',
- scope: normalize_scope(params['scope']),
- redirect_uri: oauth_jira_dvcs_callback_url
- )
- end
-
- # 2. Handle the callback call as we were a Github Enterprise instance client.
- def callback
- # Handling URI query params concatenation.
- redirect_uri = URI.parse(session['redirect_uri'])
- new_query = URI.decode_www_form(String(redirect_uri.query)) << ['code', params[:code]]
- redirect_uri.query = URI.encode_www_form(new_query)
-
- redirect_to redirect_uri.to_s
- end
-
- # 3. Rewire and adjust access_token request accordingly.
- def access_token
- # We have to modify request.parameters because Doorkeeper::Server reads params from there
- request.parameters[:redirect_uri] = oauth_jira_dvcs_callback_url
-
- strategy = Doorkeeper::Server.new(self).token_request('authorization_code')
- response = strategy.authorize
-
- if response.status == :ok
- access_token, scope, token_type = response.body.values_at('access_token', 'scope', 'token_type')
-
- render body: "access_token=#{access_token}&scope=#{scope}&token_type=#{token_type}"
- else
- render status: response.status, body: response.body
- end
- rescue Doorkeeper::Errors::DoorkeeperError => e
- render status: :unauthorized, body: e.type
- end
-
- private
-
- # The endpoints in this controller have been deprecated since 15.1.
- #
- # Due to uncertainty about the impact of a full removal in 16.0, all endpoints return `404`
- # by default but we allow customers to toggle a flag to reverse this breaking change.
- # See https://gitlab.com/gitlab-org/gitlab/-/issues/362168#note_1347692683.
- #
- # TODO Make the breaking change irreversible https://gitlab.com/gitlab-org/gitlab/-/issues/408148.
- def reversible_end_of_life!
- render_404 unless Feature.enabled?(:jira_dvcs_end_of_life_amnesty)
- end
-
- # When using the GitHub Enterprise connector in Jira we receive the "repo" scope,
- # this doesn't exist in GitLab but we can map it to our "api" scope.
- def normalize_scope(scope)
- scope == 'repo' ? 'api' : scope
- end
-
- def validate_redirect_uri
- client = Doorkeeper::OAuth::Client.find(params[:client_id])
- return render_404 unless client
-
- return true if Doorkeeper::OAuth::Helpers::URIChecker.valid_for_authorization?(
- params['redirect_uri'], client.redirect_uri
- )
-
- render_403
- end
-end
diff --git a/app/controllers/organizations/organizations_controller.rb b/app/controllers/organizations/organizations_controller.rb
index 88c6c9b3cef..3085f0c07d1 100644
--- a/app/controllers/organizations/organizations_controller.rb
+++ b/app/controllers/organizations/organizations_controller.rb
@@ -19,5 +19,9 @@ module Organizations
def groups_and_projects
authorize_read_organization!
end
+
+ def users
+ authorize_read_organization!
+ end
end
end
diff --git a/app/controllers/profiles/comment_templates_controller.rb b/app/controllers/profiles/comment_templates_controller.rb
index d6725c27f76..f7c1f8733de 100644
--- a/app/controllers/profiles/comment_templates_controller.rb
+++ b/app/controllers/profiles/comment_templates_controller.rb
@@ -5,8 +5,6 @@ module Profiles
feature_category :user_profile
before_action do
- render_404 unless Feature.enabled?(:saved_replies, current_user)
-
@hide_search_settings = true
end
end
diff --git a/app/controllers/profiles/preferences_controller.rb b/app/controllers/profiles/preferences_controller.rb
index 931070ecdd4..7059e2a0371 100644
--- a/app/controllers/profiles/preferences_controller.rb
+++ b/app/controllers/profiles/preferences_controller.rb
@@ -48,6 +48,7 @@ class Profiles::PreferencesController < Profiles::ApplicationController
:first_day_of_week,
:preferred_language,
:time_display_relative,
+ :time_display_format,
:show_whitespace_in_diffs,
:view_diffs_file_by_file,
:tab_width,
diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb
index da15b393e6c..cb29f0f3539 100644
--- a/app/controllers/profiles_controller.rb
+++ b/app/controllers/profiles_controller.rb
@@ -111,6 +111,7 @@ class ProfilesController < Profiles::ApplicationController
[
:avatar,
:bio,
+ :discord,
:email,
:role,
:gitpod_enabled,
@@ -119,12 +120,12 @@ class ProfilesController < Profiles::ApplicationController
:hide_project_limit,
:linkedin,
:location,
+ :mastodon,
:name,
:public_email,
:commit_email,
:skype,
:twitter,
- :discord,
:username,
:website_url,
:organization,
diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb
index 30c6f4d865a..4bfee0c9c82 100644
--- a/app/controllers/projects/application_controller.rb
+++ b/app/controllers/projects/application_controller.rb
@@ -91,6 +91,19 @@ class Projects::ApplicationController < ApplicationController
def check_issues_available!
return render_404 unless @project.feature_available?(:issues, current_user)
end
+
+ def set_is_ambiguous_ref
+ return @is_ambiguous_ref if defined? @is_ambiguous_ref
+
+ @is_ambiguous_ref = if Feature.enabled?(:ambiguous_ref_modal, @project)
+ ExtractsRef::RequestedRef
+ .new(@project.repository, ref_type: ref_type, ref: @ref)
+ .find
+ .fetch(:ambiguous, false)
+ else
+ false
+ end
+ end
end
Projects::ApplicationController.prepend_mod_with('Projects::ApplicationController')
diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb
index 2828d17c36f..85bdeb07b00 100644
--- a/app/controllers/projects/artifacts_controller.rb
+++ b/app/controllers/projects/artifacts_controller.rb
@@ -62,7 +62,11 @@ class Projects::ArtifactsController < Projects::ApplicationController
conditionally_expand_blob(blob)
if blob.external_link?(build)
- redirect_to external_file_project_job_artifacts_path(@project, @build, path: params[:path])
+ if Gitlab::CurrentSettings.enable_artifact_external_redirect_warning_page
+ redirect_to external_file_project_job_artifacts_path(@project, @build, path: params[:path])
+ else
+ redirect_to blob.external_url(build)
+ end
else
respond_to do |format|
format.html do
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 015e56db012..7371902a6bd 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -31,6 +31,7 @@ class Projects::BlobController < Projects::ApplicationController
before_action :authorize_edit_tree!, only: [:new, :create, :update, :destroy]
before_action :commit, except: [:new, :create]
+ before_action :set_is_ambiguous_ref, only: [:show]
before_action :check_for_ambiguous_ref, only: [:show]
before_action :blob, except: [:new, :create]
before_action :require_branch_head, only: [:edit, :update]
@@ -48,6 +49,7 @@ class Projects::BlobController < Projects::ApplicationController
urgency :low, [:create, :show, :edit, :update, :diff]
before_action do
+ push_frontend_feature_flag(:blob_blame_info, @project)
push_frontend_feature_flag(:highlight_js_worker, @project)
push_frontend_feature_flag(:explain_code_chat, current_user)
push_licensed_feature(:file_locks) if @project.licensed_feature_available?(:file_locks)
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index aabea122fb6..4b2749dc716 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -2,12 +2,18 @@
class Projects::EnvironmentsController < Projects::ApplicationController
MIN_SEARCH_LENGTH = 3
+ ACTIVE_STATES = %i[available stopping].freeze
+ SCOPES_TO_STATES = { "active" => ACTIVE_STATES, "stopped" => %i[stopped] }.freeze
include ProductAnalyticsTracking
include KasCookie
layout 'project'
+ before_action only: [:index] do
+ push_frontend_feature_flag(:k8s_watch_api, project)
+ end
+
before_action :authorize_read_environment!
before_action :authorize_create_environment!, only: [:new, :create]
before_action :authorize_stop_environment!, only: [:stop]
@@ -31,7 +37,9 @@ class Projects::EnvironmentsController < Projects::ApplicationController
respond_to do |format|
format.html
format.json do
- @environments = search_environments.with_state(params[:scope] || :available)
+ states = SCOPES_TO_STATES.fetch(params[:scope], ACTIVE_STATES)
+ @environments = search_environments.with_state(states)
+
environments_count_by_state = search_environments.count_by_state
Gitlab::PollingInterval.set_header(response, interval: 3_000)
@@ -40,6 +48,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
review_app: serialize_review_app,
can_stop_stale_environments: can?(current_user, :stop_environment, @project),
available_count: environments_count_by_state[:available],
+ active_count: environments_count_by_state[:available] + environments_count_by_state[:stopping],
stopped_count: environments_count_by_state[:stopped]
}
end
@@ -54,14 +63,16 @@ class Projects::EnvironmentsController < Projects::ApplicationController
respond_to do |format|
format.html
format.json do
+ states = SCOPES_TO_STATES.fetch(params[:scope], ACTIVE_STATES)
folder_environments = search_environments(type: params[:id])
- @environments = folder_environments.with_state(params[:scope] || :available)
+ @environments = folder_environments.with_state(states)
.order(:name)
render json: {
environments: serialize_environments(request, response),
available_count: folder_environments.available.count,
+ active_count: folder_environments.active.count,
stopped_count: folder_environments.stopped.count
}
end
diff --git a/app/controllers/projects/group_links_controller.rb b/app/controllers/projects/group_links_controller.rb
index 60300f78bbb..5f8bf423219 100644
--- a/app/controllers/projects/group_links_controller.rb
+++ b/app/controllers/projects/group_links_controller.rb
@@ -9,30 +9,47 @@ class Projects::GroupLinksController < Projects::ApplicationController
feature_category :groups_and_projects
def update
- Projects::GroupLinks::UpdateService.new(group_link, current_user).execute(group_link_params)
+ result = Projects::GroupLinks::UpdateService.new(group_link, current_user).execute(group_link_params)
- if group_link.expires?
- render json: {
- expires_in: helpers.time_ago_with_tooltip(group_link.expires_at),
- expires_soon: group_link.expires_soon?
- }
- else
- render json: {}
+ if result.success?
+ if group_link.expires?
+ render json: {
+ expires_in: helpers.time_ago_with_tooltip(group_link.expires_at),
+ expires_soon: group_link.expires_soon?
+ }
+ else
+ render json: {}
+ end
+ elsif result.reason == :not_found
+ render json: { message: result.message }, status: :not_found
end
end
def destroy
- ::Projects::GroupLinks::DestroyService.new(project, current_user).execute(group_link)
-
- respond_to do |format|
- format.html do
- if can?(current_user, :admin_group, group_link.group)
- redirect_to group_path(group_link.group), status: :found
- elsif can?(current_user, :admin_project, group_link.project)
- redirect_to project_project_members_path(project), status: :found
+ result = ::Projects::GroupLinks::DestroyService.new(project, current_user).execute(group_link)
+
+ if result.success?
+ respond_to do |format|
+ format.html do
+ if can?(current_user, :admin_group, group_link.group)
+ redirect_to group_path(group_link.group), status: :found
+ elsif can?(current_user, :admin_project, group_link.project)
+ redirect_to project_project_members_path(project), status: :found
+ end
+ end
+ format.js { head :ok }
+ end
+ else
+ respond_to do |format|
+ format.html do
+ redirect_to project_project_members_path(project, tab: :groups), status: :found,
+ alert: _('The project-group link could not be removed.')
+ end
+
+ format.js do
+ render json: { message: result.message }, status: :not_found if result.reason == :not_found
end
end
- format.js { head :ok }
end
end
diff --git a/app/controllers/projects/incidents_controller.rb b/app/controllers/projects/incidents_controller.rb
index bacf3192ee6..a3c1fd64a9d 100644
--- a/app/controllers/projects/incidents_controller.rb
+++ b/app/controllers/projects/incidents_controller.rb
@@ -12,7 +12,7 @@ class Projects::IncidentsController < Projects::ApplicationController
push_force_frontend_feature_flag(:work_items_mvc_2, @project&.work_items_mvc_2_feature_flag_enabled?)
push_frontend_feature_flag(:moved_mr_sidebar, project)
push_force_frontend_feature_flag(:linked_work_items, @project&.linked_work_items_feature_flag_enabled?)
- push_frontend_feature_flag(:notifications_todos_buttons, project)
+ push_frontend_feature_flag(:notifications_todos_buttons, current_user)
end
feature_category :incident_management
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 4849cccac52..a6444dc038c 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -45,8 +45,6 @@ class Projects::IssuesController < Projects::ApplicationController
before_action do
push_frontend_feature_flag(:preserve_unchanged_markdown, project)
- push_frontend_feature_flag(:service_desk_new_note_email_native_attachments, project)
- push_frontend_feature_flag(:saved_replies, current_user)
push_frontend_feature_flag(:issues_grid_view)
push_frontend_feature_flag(:service_desk_ticket)
push_frontend_feature_flag(:issues_list_drawer, project)
@@ -60,17 +58,17 @@ class Projects::IssuesController < Projects::ApplicationController
before_action only: [:index, :service_desk] do
push_frontend_feature_flag(:or_issuable_queries, project)
push_frontend_feature_flag(:frontend_caching, project&.group)
+ push_frontend_feature_flag(:group_multi_select_tokens, project)
end
before_action only: :show do
- push_frontend_feature_flag(:issue_assignees_widget, project)
push_frontend_feature_flag(:work_items_mvc, project&.group)
push_force_frontend_feature_flag(:work_items_mvc, project&.work_items_mvc_feature_flag_enabled?)
push_force_frontend_feature_flag(:work_items_mvc_2, project&.work_items_mvc_2_feature_flag_enabled?)
push_frontend_feature_flag(:epic_widget_edit_confirmation, project)
push_frontend_feature_flag(:moved_mr_sidebar, project)
push_force_frontend_feature_flag(:linked_work_items, project.linked_work_items_feature_flag_enabled?)
- push_frontend_feature_flag(:notifications_todos_buttons, project)
+ push_frontend_feature_flag(:notifications_todos_buttons, current_user)
end
around_action :allow_gitaly_ref_name_caching, only: [:discussions]
diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb
index 802ffd99e41..d5a7f25d4ce 100644
--- a/app/controllers/projects/jobs_controller.rb
+++ b/app/controllers/projects/jobs_controller.rb
@@ -6,14 +6,16 @@ class Projects::JobsController < Projects::ApplicationController
include ContinueParams
include ProjectStatsRefreshConflictsGuard
- urgency :low, [:index, :show, :trace, :retry, :play, :cancel, :unschedule, :erase, :raw]
+ urgency :low, [:index, :show, :trace, :retry, :play, :cancel, :unschedule, :erase, :raw, :test_report_summary]
before_action :find_job_as_build, except: [:index, :play, :retry, :show]
before_action :find_job_as_processable, only: [:play, :retry, :show]
before_action :authorize_read_build_trace!, only: [:trace, :raw]
- before_action :authorize_read_build!
+ before_action :authorize_read_build!, except: [:test_report_summary]
+ before_action :authorize_read_build_report_results!, only: [:test_report_summary]
before_action :authorize_update_build!,
- except: [:index, :show, :raw, :trace, :erase, :cancel, :unschedule]
+ except: [:index, :show, :raw, :trace, :erase, :cancel, :unschedule, :test_report_summary]
+ before_action :authorize_cancel_build!, only: [:cancel]
before_action :authorize_erase_build!, only: [:erase]
before_action :authorize_use_build_terminal!, only: [:terminal, :terminal_websocket_authorize]
before_action :verify_api_request!, only: :terminal_websocket_authorize
@@ -153,6 +155,20 @@ class Projects::JobsController < Projects::ApplicationController
end
end
+ def test_report_summary
+ return not_found unless @build.report_results.present?
+
+ summary = Gitlab::Ci::Reports::TestReportSummary.new(@build.report_results)
+
+ respond_to do |format|
+ format.json do
+ render json: TestReportSummarySerializer
+ .new(project: project, current_user: @current_user)
+ .represent(summary)
+ end
+ end
+ end
+
def terminal
end
@@ -170,10 +186,18 @@ class Projects::JobsController < Projects::ApplicationController
attr_reader :build
+ def authorize_read_build_report_results!
+ return access_denied! unless can?(current_user, :read_build_report_results, build)
+ end
+
def authorize_update_build!
return access_denied! unless can?(current_user, :update_build, @build)
end
+ def authorize_cancel_build!
+ return access_denied! unless can?(current_user, :cancel_build, @build)
+ end
+
def authorize_erase_build!
return access_denied! unless can?(current_user, :erase_build, @build)
end
diff --git a/app/controllers/projects/merge_requests/drafts_controller.rb b/app/controllers/projects/merge_requests/drafts_controller.rb
index 74c495261a3..fb0073e0ad4 100644
--- a/app/controllers/projects/merge_requests/drafts_controller.rb
+++ b/app/controllers/projects/merge_requests/drafts_controller.rb
@@ -61,7 +61,9 @@ class Projects::MergeRequests::DraftsController < Projects::MergeRequests::Appli
merge_request_activity_counter.track_submit_review_comment(user: current_user)
end
- if Gitlab::Utils.to_boolean(approve_params[:approve])
+ if Feature.enabled?(:mr_request_changes, current_user) && reviewer_state_params[:reviewer_state]
+ update_reviewer_state
+ elsif Gitlab::Utils.to_boolean(approve_params[:approve])
unless merge_request.approved_by?(current_user)
success = ::MergeRequests::ApprovalService
.new(project: @project, current_user: current_user, params: approve_params)
@@ -144,6 +146,10 @@ class Projects::MergeRequests::DraftsController < Projects::MergeRequests::Appli
params.permit(:approve)
end
+ def reviewer_state_params
+ params.permit(:reviewer_state)
+ end
+
def prepare_notes_for_rendering(notes)
return [] unless notes
@@ -180,6 +186,18 @@ class Projects::MergeRequests::DraftsController < Projects::MergeRequests::Appli
def merge_request_activity_counter
Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter
end
+
+ def update_reviewer_state
+ if reviewer_state_params[:reviewer_state] === 'approved'
+ ::MergeRequests::ApprovalService
+ .new(project: @project, current_user: current_user, params: approve_params)
+ .execute(merge_request)
+ else
+ ::MergeRequests::UpdateReviewerStateService
+ .new(project: @project, current_user: current_user)
+ .execute(merge_request, reviewer_state_params[:reviewer_state])
+ end
+ end
end
Projects::MergeRequests::DraftsController.prepend_mod
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index ad7b7221e44..eb7505bd81f 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -11,6 +11,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
include SourcegraphDecorator
include DiffHelper
include Gitlab::Cache::Helpers
+ include MergeRequestsHelper
prepend_before_action(only: [:index]) { authenticate_sessionless_user!(:rss) }
skip_before_action :merge_request, only: [:index, :bulk_update, :export_csv]
@@ -37,15 +38,15 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
before_action only: [:show, :diffs] do
push_frontend_feature_flag(:core_security_mr_widget_counts, project)
- push_frontend_feature_flag(:issue_assignees_widget, @project)
push_frontend_feature_flag(:moved_mr_sidebar, project)
push_frontend_feature_flag(:sast_reports_in_inline_diff, project)
push_frontend_feature_flag(:mr_experience_survey, project)
- push_frontend_feature_flag(:saved_replies, current_user)
push_force_frontend_feature_flag(:summarize_my_code_review, summarize_my_code_review_enabled?)
push_frontend_feature_flag(:ci_job_failures_in_mr, project)
push_frontend_feature_flag(:mr_pipelines_graphql, project)
- push_frontend_feature_flag(:notifications_todos_buttons, project)
+ push_frontend_feature_flag(:notifications_todos_buttons, current_user)
+ push_frontend_feature_flag(:widget_pipeline_pass_subscription_update, project)
+ push_frontend_feature_flag(:mr_request_changes, current_user)
end
before_action only: [:edit] do
@@ -159,7 +160,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
.represent(
@pipelines,
preload: true,
- disable_failed_builds: ::Feature.enabled?(:ci_fix_performance_pipelines_json_endpoint, @project)
+ disable_failed_builds: true
),
count: {
all: @pipelines.count
@@ -344,9 +345,16 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
end
def discussions
- merge_request.discussions_diffs.load_highlight
+ if Feature.enabled?(:only_highlight_discussions_requested, project)
+ super do |discussion_notes|
+ note_ids = discussion_notes.flat_map { |x| x.notes.collect(&:id) }
+ merge_request.discussions_diffs.load_highlight(diff_note_ids: note_ids)
+ end
+ else
+ merge_request.discussions_diffs.load_highlight
- super
+ super
+ end
end
def export_csv
@@ -617,7 +625,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
end
def endpoint_diff_batch_url(project, merge_request)
- per_page = current_user&.view_diffs_file_by_file ? '1' : '5'
+ per_page = current_user&.view_diffs_file_by_file ? '1' : DIFF_BATCH_ENDPOINT_PER_PAGE.to_s
params = request
.query_parameters
.merge(view: 'inline', diff_head: true, w: show_whitespace, page: '0', per_page: per_page)
diff --git a/app/controllers/projects/ml/model_versions_controller.rb b/app/controllers/projects/ml/model_versions_controller.rb
new file mode 100644
index 00000000000..bc69f5bf144
--- /dev/null
+++ b/app/controllers/projects/ml/model_versions_controller.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Projects
+ module Ml
+ class ModelVersionsController < ::Projects::ApplicationController
+ before_action :authorize_read_model_registry!
+ feature_category :mlops
+
+ def show
+ @model_version = ::Ml::ModelVersion.by_project_id_and_id(@project, params[:model_version_id])
+
+ return render_404 unless @model_version
+
+ @model = @model_version.model
+ end
+
+ private
+
+ def authorize_read_model_registry!
+ render_404 unless can?(current_user, :read_model_registry, @project)
+ end
+ end
+ end
+end
diff --git a/app/controllers/projects/ml/models_controller.rb b/app/controllers/projects/ml/models_controller.rb
index 4ff7d014723..68a8b7a1686 100644
--- a/app/controllers/projects/ml/models_controller.rb
+++ b/app/controllers/projects/ml/models_controller.rb
@@ -3,26 +3,45 @@
module Projects
module Ml
class ModelsController < ::Projects::ApplicationController
- before_action :check_feature_enabled
- before_action :set_model, only: [:show]
+ before_action :authorize_read_model_registry!
+ before_action :authorize_write_model_registry!, only: [:destroy]
+ before_action :set_model, only: [:show, :destroy]
feature_category :mlops
MAX_MODELS_PER_PAGE = 20
def index
- @paginator = ::Projects::Ml::ModelFinder.new(@project)
- .execute
- .keyset_paginate(cursor: params[:cursor], per_page: MAX_MODELS_PER_PAGE)
+ find_params = params
+ .transform_keys(&:underscore)
+ .permit(:name, :order_by, :sort)
+
+ finder = ::Projects::Ml::ModelFinder.new(@project, find_params)
+
+ @paginator = finder.execute.keyset_paginate(cursor: params[:cursor], per_page: MAX_MODELS_PER_PAGE)
+
+ @model_count = finder.count
end
def show; end
+ def destroy
+ @model.destroy!
+
+ redirect_to project_ml_models_path(@project),
+ status: :found,
+ notice: s_("MlExperimentTracking|Model removed")
+ end
+
private
- def check_feature_enabled
+ def authorize_read_model_registry!
render_404 unless can?(current_user, :read_model_registry, @project)
end
+ def authorize_write_model_registry!
+ render_404 unless can?(current_user, :write_model_registry, @project)
+ end
+
def set_model
@model = ::Ml::Model.by_project_id_and_id(@project, params[:model_id])
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index 036ea45cc78..cd2db2dad2c 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -18,7 +18,8 @@ class Projects::PipelinesController < Projects::ApplicationController
before_action :authorize_read_build!, only: [:index, :show]
before_action :authorize_read_ci_cd_analytics!, only: [:charts]
before_action :authorize_create_pipeline!, only: [:new, :create]
- before_action :authorize_update_pipeline!, only: [:retry, :cancel]
+ before_action :authorize_update_pipeline!, only: [:retry]
+ before_action :authorize_cancel_pipeline!, only: [:cancel]
before_action :ensure_pipeline, only: [:show, :downloadable_artifacts]
before_action :reject_if_build_artifacts_size_refreshing!, only: [:destroy]
@@ -303,6 +304,10 @@ class Projects::PipelinesController < Projects::ApplicationController
return access_denied! unless can?(current_user, :update_pipeline, @pipeline)
end
+ def authorize_cancel_pipeline!
+ return access_denied! unless can?(current_user, :cancel_pipeline, @pipeline)
+ end
+
def limited_pipelines_count(project, scope = nil)
finder = Ci::PipelinesFinder.new(project, current_user, index_params.merge(scope: scope))
diff --git a/app/controllers/projects/raw_controller.rb b/app/controllers/projects/raw_controller.rb
index 79b5990abba..d0a80c6aa07 100644
--- a/app/controllers/projects/raw_controller.rb
+++ b/app/controllers/projects/raw_controller.rb
@@ -19,7 +19,8 @@ class Projects::RawController < Projects::ApplicationController
def show
@blob = @repository.blob_at(@ref, @path, limit: Gitlab::Git::Blob::LFS_POINTER_MAX_SIZE)
- send_blob(@repository, @blob, inline: (params[:inline] != 'false'), allow_caching: Guest.can?(:read_code, @project))
+ send_blob(@repository, @blob, inline: (params[:inline] != 'false'), allow_caching:
+::Users::Anonymous.can?(:read_code, @project))
end
private
diff --git a/app/controllers/projects/repositories_controller.rb b/app/controllers/projects/repositories_controller.rb
index 4a9282432fd..406e3bd62c2 100644
--- a/app/controllers/projects/repositories_controller.rb
+++ b/app/controllers/projects/repositories_controller.rb
@@ -48,7 +48,7 @@ class Projects::RepositoriesController < Projects::ApplicationController
expires_in(
cache_max_age(commit_id),
- public: Guest.can?(:download_code, project),
+ public: ::Users::Anonymous.can?(:download_code, project),
must_revalidate: true,
stale_if_error: 5.minutes,
stale_while_revalidate: 1.minute,
diff --git a/app/controllers/projects/service_desk_controller.rb b/app/controllers/projects/service_desk_controller.rb
index ca3cecf5949..70cb439c4f3 100644
--- a/app/controllers/projects/service_desk_controller.rb
+++ b/app/controllers/projects/service_desk_controller.rb
@@ -29,7 +29,7 @@ class Projects::ServiceDeskController < Projects::ApplicationController
end
def allowed_update_attributes
- %i[issue_template_key outgoing_name project_key]
+ %i[issue_template_key outgoing_name project_key add_external_participants_from_cc]
end
def service_desk_attributes
@@ -41,7 +41,8 @@ class Projects::ServiceDeskController < Projects::ApplicationController
issue_template_key: service_desk_settings&.issue_template_key,
template_file_missing: service_desk_settings&.issue_template_missing?,
outgoing_name: service_desk_settings&.outgoing_name,
- project_key: service_desk_settings&.project_key
+ project_key: service_desk_settings&.project_key,
+ add_external_participants_from_cc: service_desk_settings&.add_external_participants_from_cc
}
end
diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb
index 0845fbc9713..9a128adb926 100644
--- a/app/controllers/projects/settings/ci_cd_controller.rb
+++ b/app/controllers/projects/settings/ci_cd_controller.rb
@@ -14,7 +14,6 @@ module Projects
before_action do
push_frontend_feature_flag(:ci_variables_pages, current_user)
- push_frontend_feature_flag(:ci_variable_drawer, current_user)
end
helper_method :highlight_badge
diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb
index 0371fb21ac8..cfcc27edf3e 100644
--- a/app/controllers/projects/tree_controller.rb
+++ b/app/controllers/projects/tree_controller.rb
@@ -12,12 +12,14 @@ class Projects::TreeController < Projects::ApplicationController
before_action :require_non_empty_project, except: [:new, :create]
before_action :assign_ref_vars
+ before_action :set_is_ambiguous_ref, only: [:show]
before_action :find_requested_ref, only: [:show]
before_action :assign_dir_vars, only: [:create_dir]
before_action :authorize_read_code!
before_action :authorize_edit_tree!, only: [:create_dir]
before_action do
+ push_frontend_feature_flag(:blob_blame_info, @project)
push_frontend_feature_flag(:highlight_js_worker, @project)
push_frontend_feature_flag(:explain_code_chat, current_user)
push_licensed_feature(:file_locks) if @project.licensed_feature_available?(:file_locks)
diff --git a/app/controllers/projects/work_items_controller.rb b/app/controllers/projects/work_items_controller.rb
index c3986be31b0..84cc1b16136 100644
--- a/app/controllers/projects/work_items_controller.rb
+++ b/app/controllers/projects/work_items_controller.rb
@@ -11,7 +11,6 @@ class Projects::WorkItemsController < Projects::ApplicationController
push_force_frontend_feature_flag(:work_items, project&.work_items_feature_flag_enabled?)
push_force_frontend_feature_flag(:work_items_mvc, project&.work_items_mvc_feature_flag_enabled?)
push_force_frontend_feature_flag(:work_items_mvc_2, project&.work_items_mvc_2_feature_flag_enabled?)
- push_force_frontend_feature_flag(:saved_replies, current_user)
push_force_frontend_feature_flag(:linked_work_items, project&.linked_work_items_feature_flag_enabled?)
end
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index fa26601204a..cee56dca538 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -29,7 +29,8 @@ class ProjectsController < Projects::ApplicationController
before_action :authorize_read_code!, only: [:refs]
# Authorize
- before_action :authorize_admin_project!, only: [:edit, :update, :housekeeping, :download_export, :export, :remove_export, :generate_new_export]
+ before_action :authorize_admin_project_or_custom_permissions!, only: :edit
+ before_action :authorize_admin_project!, only: [:update, :housekeeping, :download_export, :export, :remove_export, :generate_new_export]
before_action :authorize_archive_project!, only: [:archive, :unarchive]
before_action :event_filter, only: [:show, :activity]
@@ -37,11 +38,14 @@ class ProjectsController < Projects::ApplicationController
before_action :check_export_rate_limit!, only: [:export, :download_export, :generate_new_export]
before_action do
+ push_frontend_feature_flag(:blob_blame_info, @project)
push_frontend_feature_flag(:highlight_js_worker, @project)
push_frontend_feature_flag(:remove_monitor_metrics, @project)
push_frontend_feature_flag(:explain_code_chat, current_user)
push_frontend_feature_flag(:service_desk_custom_email, @project)
push_frontend_feature_flag(:issue_email_participants, @project)
+ # TODO: We need to remove the FF eventually when we rollout page_specific_styles
+ push_frontend_feature_flag(:page_specific_styles, current_user)
push_licensed_feature(:file_locks) if @project.present? && @project.licensed_feature_available?(:file_locks)
push_licensed_feature(:security_orchestration_policies) if @project.present? && @project.licensed_feature_available?(:security_orchestration_policies)
push_force_frontend_feature_flag(:work_items, @project&.work_items_feature_flag_enabled?)
@@ -595,6 +599,11 @@ class ProjectsController < Projects::ApplicationController
def render_edit
render 'edit'
end
+
+ # Overridden in EE
+ def authorize_admin_project_or_custom_permissions!
+ authorize_admin_project!
+ end
end
ProjectsController.prepend_mod_with('ProjectsController')
diff --git a/app/controllers/repositories/git_http_client_controller.rb b/app/controllers/repositories/git_http_client_controller.rb
index a5ca17db113..e8da6ee986a 100644
--- a/app/controllers/repositories/git_http_client_controller.rb
+++ b/app/controllers/repositories/git_http_client_controller.rb
@@ -129,7 +129,7 @@ module Repositories
def handle_basic_authentication(login, password)
@authentication_result = Gitlab::Auth.find_for_git_client(
- login, password, project: project, ip: request.ip)
+ login, password, project: project, request: request)
@authentication_result.success?
end
@@ -142,7 +142,7 @@ module Repositories
Gitlab::ProtocolAccess.allowed?('http') &&
download_request? &&
container &&
- Guest.can?(repo_type.guest_read_ability, container)
+ ::Users::Anonymous.can?(repo_type.guest_read_ability, container)
end
def bypass_admin_mode!(&block)
diff --git a/app/controllers/repositories/git_http_controller.rb b/app/controllers/repositories/git_http_controller.rb
index 4f228ced542..48edda13904 100644
--- a/app/controllers/repositories/git_http_controller.rb
+++ b/app/controllers/repositories/git_http_controller.rb
@@ -106,7 +106,8 @@ module Repositories
def access_actor
return user if user
- return :ci if ci?
+
+ :ci if ci?
end
def access_check
@@ -124,6 +125,13 @@ module Repositories
def log_user_activity
Users::ActivityService.new(author: user, project: project, namespace: project&.namespace).execute
end
+
+ def append_info_to_payload(payload)
+ super
+
+ payload[:metadata] ||= {}
+ payload[:metadata][:repository_storage] = project&.repository_storage
+ end
end
end
diff --git a/app/controllers/repositories/lfs_api_controller.rb b/app/controllers/repositories/lfs_api_controller.rb
index d9ca216b168..d9d3753a2ff 100644
--- a/app/controllers/repositories/lfs_api_controller.rb
+++ b/app/controllers/repositories/lfs_api_controller.rb
@@ -60,7 +60,7 @@ module Repositories
.for_oids(objects_oids)
.index_by(&:oid)
- guest_can_download = Guest.can?(:download_code, project)
+ guest_can_download = ::Users::Anonymous.can?(:download_code, project)
objects.each do |object|
if lfs_object = existing_oids[object[:oid]]
@@ -87,7 +87,7 @@ module Repositories
if existing_oids.include?(object[:oid])
object[:actions] = proxy_download_actions(object)
- if Guest.can?(:download_code, project)
+ if ::Users::Anonymous.can?(:download_code, project)
object[:authenticated] = true
end
else
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb
index 7fff31c767f..b639a9dda3f 100644
--- a/app/controllers/search_controller.rb
+++ b/app/controllers/search_controller.rb
@@ -4,7 +4,6 @@ class SearchController < ApplicationController
include ControllerWithCrossProjectAccessCheck
include SearchHelper
include ProductAnalyticsTracking
- include ProductAnalyticsTracking
include SearchRateLimitable
RESCUE_FROM_TIMEOUT_ACTIONS = [:count, :show, :autocomplete, :aggregations].freeze
@@ -16,6 +15,12 @@ class SearchController < ApplicationController
action: 'executed',
destinations: [:redis_hll, :snowplow]
+ track_event :autocomplete,
+ name: 'i_search_total',
+ label: 'redis_hll_counters.search.search_total_unique_counts_monthly',
+ action: 'autocomplete',
+ destinations: [:redis_hll, :snowplow]
+
def self.search_rate_limited_endpoints
%i[show count autocomplete]
end
@@ -35,18 +40,6 @@ class SearchController < ApplicationController
update_scope_for_code_search
end
- before_action only: :show do
- push_frontend_feature_flag(:search_notes_hide_archived_projects, current_user)
- end
-
- before_action only: :show do
- push_frontend_feature_flag(:search_issues_hide_archived_projects, current_user)
- end
-
- before_action only: :show do
- push_frontend_feature_flag(:search_merge_requests_hide_archived_projects, current_user)
- end
-
rescue_from ActiveRecord::QueryCanceled, with: :render_timeout
layout 'search'
diff --git a/app/experiments/ios_specific_templates_experiment.rb b/app/experiments/ios_specific_templates_experiment.rb
deleted file mode 100644
index 5bd4a3d0287..00000000000
--- a/app/experiments/ios_specific_templates_experiment.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-# frozen_string_literal: true
-
-class IosSpecificTemplatesExperiment < ApplicationExperiment
- control
-
- before_run(if: :skip_experiment) { throw(:abort) } # rubocop:disable Cop/BanCatchThrow
-
- private
-
- def skip_experiment
- actor_not_able_to_create_pipelines? ||
- project_targets_non_ios_platforms? ||
- project_has_gitlab_ci? ||
- project_has_pipelines?
- end
-
- def actor_not_able_to_create_pipelines?
- !context.actor.is_a?(User) || !context.actor.can?(:create_pipeline, context.project)
- end
-
- def project_targets_non_ios_platforms?
- context.project.project_setting.target_platforms.exclude?('ios')
- end
-
- def project_has_gitlab_ci?
- context.project.has_ci? && context.project.builds_enabled?
- end
-
- def project_has_pipelines?
- context.project.all_pipelines.count > 0
- end
-end
diff --git a/app/finders/ci/catalog/resources/versions_finder.rb b/app/finders/ci/catalog/resources/versions_finder.rb
new file mode 100644
index 00000000000..b37d4f0377a
--- /dev/null
+++ b/app/finders/ci/catalog/resources/versions_finder.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+module Ci
+ module Catalog
+ module Resources
+ class VersionsFinder
+ include Gitlab::Utils::StrongMemoize
+
+ def initialize(catalog_resources, current_user, params = {})
+ # The catalog resources should already have their project association preloaded
+ @catalog_resources = Array.wrap(catalog_resources)
+ @current_user = current_user
+ @params = params
+ end
+
+ def execute
+ return Ci::Catalog::Resources::Version.none if authorized_catalog_resources.empty?
+
+ versions = params[:latest] ? get_latest_versions : get_versions
+ versions = versions.preloaded
+ sort(versions)
+ end
+
+ private
+
+ DEFAULT_SORT = :released_at_desc
+
+ attr_reader :catalog_resources, :current_user, :params
+
+ def get_versions
+ Ci::Catalog::Resources::Version.for_catalog_resources(authorized_catalog_resources)
+ end
+
+ def get_latest_versions
+ Ci::Catalog::Resources::Version.latest_for_catalog_resources(authorized_catalog_resources)
+ end
+
+ def authorized_catalog_resources
+ # Preload project authorizations to avoid N+1 queries
+ projects = catalog_resources.map(&:project)
+ ActiveRecord::Associations::Preloader.new(records: projects, associations: :project_feature).call
+ Preloaders::UserMaxAccessLevelInProjectsPreloader.new(projects, current_user).execute
+
+ catalog_resources.select { |resource| authorized?(resource.project) }
+ end
+ strong_memoize_attr :authorized_catalog_resources
+
+ def sort(versions)
+ versions.order_by(params[:sort] || DEFAULT_SORT)
+ end
+
+ def authorized?(project)
+ Ability.allowed?(current_user, :read_release, project)
+ end
+ end
+ end
+ end
+end
diff --git a/app/finders/ci/runners_finder.rb b/app/finders/ci/runners_finder.rb
index 331f732bff7..945d332ff47 100644
--- a/app/finders/ci/runners_finder.rb
+++ b/app/finders/ci/runners_finder.rb
@@ -20,6 +20,8 @@ module Ci
filter_by_upgrade_status!
filter_by_runner_type!
filter_by_tag_list!
+ filter_by_creator_id!
+ filter_by_version_prefix!
sort!
request_tag_list!
@@ -113,6 +115,21 @@ module Ci
end
end
+ def filter_by_creator_id!
+ creator_id = @params[:creator_id]
+ @runners = @runners.with_creator_id(creator_id) if creator_id.present?
+ end
+
+ def filter_by_version_prefix!
+ return @runners unless @params[:version_prefix]
+
+ sanitized_prefix = @params[:version_prefix][/^[\d+.]+/]
+
+ return @runners unless sanitized_prefix
+
+ @runners = @runners.with_version_prefix(sanitized_prefix)
+ end
+
def sort!
@runners = @runners.order_by(sort_key)
end
diff --git a/app/finders/data_transfer/mocked_transfer_finder.rb b/app/finders/data_transfer/mocked_transfer_finder.rb
deleted file mode 100644
index 9c5551005ea..00000000000
--- a/app/finders/data_transfer/mocked_transfer_finder.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-# frozen_string_literal: true
-
-# Mocked data for data transfer
-# Follow this epic for recent progress: https://gitlab.com/groups/gitlab-org/-/epics/9330
-module DataTransfer
- class MockedTransferFinder
- def execute
- start_date = Date.new(2023, 0o1, 0o1)
- date_for_index = ->(i) { (start_date + i.months).strftime('%Y-%m-%d') }
-
- 0.upto(11).map do |i|
- {
- date: date_for_index.call(i),
- repository_egress: rand(70000..550000),
- artifacts_egress: rand(70000..550000),
- packages_egress: rand(70000..550000),
- registry_egress: rand(70000..550000)
- }.tap do |hash|
- hash[:total_egress] = hash
- .slice(:repository_egress, :artifacts_egress, :packages_egress, :registry_egress)
- .values
- .sum
- end
- end
- end
- end
-end
diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb
index 95b5b267089..b7de1c08f86 100644
--- a/app/finders/merge_requests_finder.rb
+++ b/app/finders/merge_requests_finder.rb
@@ -46,6 +46,7 @@ class MergeRequestsFinder < IssuableFinder
:merged_before,
:reviewer_id,
:reviewer_username,
+ :source_branch,
:target_branch,
:wip
]
@@ -73,7 +74,6 @@ class MergeRequestsFinder < IssuableFinder
items = by_deployments(items)
items = by_reviewer(items)
items = by_source_project_id(items)
- items = items.allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/417462")
by_approved(items)
end
@@ -82,7 +82,8 @@ class MergeRequestsFinder < IssuableFinder
items = super(items)
items = by_negated_reviewer(items)
items = by_negated_approved_by(items)
- by_negated_target_branch(items)
+ items = by_negated_target_branch(items)
+ by_negated_source_branch(items)
end
private
@@ -133,6 +134,12 @@ class MergeRequestsFinder < IssuableFinder
items.where.not(target_branch: not_params[:target_branch])
end
+
+ def by_negated_source_branch(items)
+ return items unless not_params[:source_branch]
+
+ items.where.not(source_branch: not_params[:source_branch])
+ end
# rubocop: enable CodeReuse/ActiveRecord
def by_negated_approved_by(items)
diff --git a/app/finders/organizations/user_organizations_finder.rb b/app/finders/organizations/user_organizations_finder.rb
new file mode 100644
index 00000000000..739940c44ca
--- /dev/null
+++ b/app/finders/organizations/user_organizations_finder.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module Organizations
+ class UserOrganizationsFinder
+ def initialize(current_user, target_user, params = {})
+ @current_user = current_user
+ @target_user = target_user
+ @params = params
+ end
+
+ def execute
+ return Organizations::Organization.none unless can_read_user_organizations?
+ return Organizations::Organization.none if target_user.blank?
+
+ target_user.organizations
+ end
+
+ private
+
+ attr_reader :current_user, :target_user, :params
+
+ def can_read_user_organizations?
+ current_user&.can?(:read_user_organizations, target_user)
+ end
+ end
+end
diff --git a/app/finders/packages/packages_finder.rb b/app/finders/packages/packages_finder.rb
index 31fbbfb7937..8fe1a73a030 100644
--- a/app/finders/packages/packages_finder.rb
+++ b/app/finders/packages/packages_finder.rb
@@ -22,6 +22,7 @@ module Packages
packages = filter_by_package_type(packages)
packages = filter_by_package_name(packages)
packages = filter_by_status(packages)
+ packages = filter_by_package_version(packages)
order_packages(packages)
end
diff --git a/app/finders/packages/pypi/packages_finder.rb b/app/finders/packages/pypi/packages_finder.rb
index 17138134eb3..944824bee6e 100644
--- a/app/finders/packages/pypi/packages_finder.rb
+++ b/app/finders/packages/pypi/packages_finder.rb
@@ -3,6 +3,8 @@
module Packages
module Pypi
class PackagesFinder < ::Packages::GroupOrProjectPackageFinder
+ extend ::Gitlab::Utils::Override
+
def execute
return packages unless @params[:package_name]
@@ -14,6 +16,15 @@ module Packages
def packages
base.pypi.has_version
end
+
+ override :group_packages
+ def group_packages
+ packages_visible_to_user(
+ @current_user,
+ within_group: @project_or_group,
+ with_package_registry_enabled: true
+ )
+ end
end
end
end
diff --git a/app/finders/projects/ml/model_finder.rb b/app/finders/projects/ml/model_finder.rb
index 1e407ba4aa4..57e0620c7a7 100644
--- a/app/finders/projects/ml/model_finder.rb
+++ b/app/finders/projects/ml/model_finder.rb
@@ -3,16 +3,58 @@
module Projects
module Ml
class ModelFinder
- def initialize(project)
+ include Gitlab::Utils::StrongMemoize
+
+ VALID_ORDER_BY = %w[name created_at id].freeze
+ VALID_SORT = %w[asc desc].freeze
+
+ def initialize(project, params = {})
@project = project
+ @params = params
end
def execute
- ::Ml::Model
- .by_project(@project)
- .including_latest_version
- .with_version_count
+ relation
+ end
+
+ def count
+ relation.length
+ end
+
+ private
+
+ def relation
+ @models = ::Ml::Model
+ .by_project(project)
+ .including_latest_version
+ .including_project
+ .with_version_count
+
+ @models = by_name
+ ordered
+ end
+ strong_memoize_attr :relation
+
+ def by_name
+ return models unless params[:name].present?
+
+ models.by_name(params[:name])
+ end
+
+ def ordered
+ order_by = valid_or_default(params[:order_by]&.downcase, VALID_ORDER_BY, 'created_at')
+ sort = valid_or_default(params[:sort]&.downcase, VALID_SORT, 'desc')
+
+ models.order_by("#{order_by}_#{sort}").with_order_id_desc
end
+
+ def valid_or_default(value, valid_values, default)
+ return value if valid_values.include?(value)
+
+ default
+ end
+
+ attr_reader :params, :project, :models
end
end
end
diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb
index 87edf36d1ce..1aa5245590e 100644
--- a/app/finders/projects_finder.rb
+++ b/app/finders/projects_finder.rb
@@ -28,6 +28,7 @@
# last_activity_before: datetime
# repository_storage: string
# not_aimed_for_deletion: boolean
+# full_paths: string[]
#
class ProjectsFinder < UnionFinder
include CustomAttributesFilter
@@ -76,8 +77,9 @@ class ProjectsFinder < UnionFinder
# EE would override this to add more filters
def filter_projects(collection)
- collection = collection.without_deleted
+ collection = by_deleted_status(collection)
collection = by_ids(collection)
+ collection = by_full_paths(collection)
collection = by_personal(collection)
collection = by_starred(collection)
collection = by_trending(collection)
@@ -153,6 +155,12 @@ class ProjectsFinder < UnionFinder
params[:min_access_level].present?
end
+ def by_deleted_status(items)
+ return items.without_deleted unless current_user&.can?(:admin_all_resources)
+
+ params[:include_pending_delete].present? ? items : items.without_deleted
+ end
+
# rubocop: disable CodeReuse/ActiveRecord
def by_ids(items)
items = items.where(id: project_ids_relation) if project_ids_relation
@@ -162,6 +170,10 @@ class ProjectsFinder < UnionFinder
end
# rubocop: enable CodeReuse/ActiveRecord
+ def by_full_paths(items)
+ params[:full_paths].present? ? items.where_full_path_in(params[:full_paths], use_includes: false) : items
+ end
+
def union(items)
find_union(items, Project).with_route
end
diff --git a/app/finders/user_group_notification_settings_finder.rb b/app/finders/user_group_notification_settings_finder.rb
index c6a1a6b36d1..8d06d3d18ca 100644
--- a/app/finders/user_group_notification_settings_finder.rb
+++ b/app/finders/user_group_notification_settings_finder.rb
@@ -11,11 +11,16 @@ class UserGroupNotificationSettingsFinder
@loaded_groups_with_ancestors = groups_with_ancestors.index_by(&:id)
@loaded_notification_settings = user.notification_settings_for_groups(groups_with_ancestors).preload_source_route.index_by(&:source_id)
- preload_emails_disabled
+ preload_emails_enabled
- groups.map do |group|
+ group_notifications = groups.map do |group|
find_notification_setting_for(group)
end
+
+ group_sources = group_notifications.map(&:source)
+ ActiveRecord::Associations::Preloader.new(records: group_sources, associations: :namespace_settings).call
+
+ group_notifications
end
private
@@ -45,18 +50,18 @@ class UserGroupNotificationSettingsFinder
parent_setting.level != NotificationSetting.levels[:global] || parent_setting.notification_email.present?
end
- # This method preloads the `emails_disabled` strong memoized method for the given groups.
+ # This method preloads the `emails_enabled` strong memoized method for the given groups.
#
- # For each group, look up the ancestor hierarchy and look for any group where emails_disabled is true.
+ # For each group, look up the ancestor hierarchy and look for any group where emails_enabled is false.
# The lookup is implemented with an EXISTS subquery, so we can look up the ancestor chain for each group individually.
# The query will return groups where at least one ancestor has the `emails_disabled` set to true.
#
# After the query, we set the instance variable.
- def preload_emails_disabled
+ def preload_emails_enabled
group_ids_with_disabled_email = Group.ids_with_disabled_email(groups.to_a)
groups.each do |group|
- group.emails_disabled_memoized = group_ids_with_disabled_email.include?(group.id) if group.parent_id
+ group.emails_enabled_memoized = group_ids_with_disabled_email.exclude?(group.id) if group.parent_id
end
end
end
diff --git a/app/graphql/mutations/base_mutation.rb b/app/graphql/mutations/base_mutation.rb
index 994668b5f8f..8419f7d5eae 100644
--- a/app/graphql/mutations/base_mutation.rb
+++ b/app/graphql/mutations/base_mutation.rb
@@ -30,12 +30,6 @@ module Mutations
def ready?(**args)
raise_resource_not_available_error!(ERROR_MESSAGE) if read_only?
- missing_args = self.class.arguments.values
- .reject { |arg| arg.accepts?(args.fetch(arg.keyword, :not_given)) }
- .map(&:graphql_name)
-
- raise ArgumentError, "Arguments must be provided: #{missing_args.join(", ")}" if missing_args.any?
-
true
end
diff --git a/app/graphql/mutations/ci/catalog/resources/create.rb b/app/graphql/mutations/ci/catalog/resources/create.rb
new file mode 100644
index 00000000000..7f934e101c8
--- /dev/null
+++ b/app/graphql/mutations/ci/catalog/resources/create.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Ci
+ module Catalog
+ module Resources
+ class Create < BaseMutation
+ graphql_name 'CatalogResourcesCreate'
+
+ argument :project_path, GraphQL::Types::ID,
+ required: true,
+ description: 'Project to convert to a catalog resource.'
+
+ authorize :add_catalog_resource
+
+ def resolve(project_path:)
+ project = authorized_find!(project_path: project_path)
+ response = ::Ci::Catalog::Resources::CreateService.new(project, current_user).execute
+
+ errors = response.success? ? [] : [response.message]
+
+ {
+ errors: errors
+ }
+ end
+
+ private
+
+ def find_object(project_path:)
+ Project.find_by_full_path(project_path)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/ci/catalog/resources/unpublish.rb b/app/graphql/mutations/ci/catalog/resources/unpublish.rb
new file mode 100644
index 00000000000..e45e9646147
--- /dev/null
+++ b/app/graphql/mutations/ci/catalog/resources/unpublish.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Ci
+ module Catalog
+ module Resources
+ class Unpublish < BaseMutation
+ graphql_name 'CatalogResourceUnpublish'
+
+ authorize :add_catalog_resource
+
+ argument :id, ::Types::GlobalIDType[::Ci::Catalog::Resource],
+ required: true,
+ description: 'Global ID of the catalog resource to unpublish.'
+
+ def resolve(id:)
+ catalog_resource = ::Gitlab::Graphql::Lazy.force(GitlabSchema.find_by_gid(id))
+ authorize!(catalog_resource&.project)
+
+ catalog_resource.unpublish!
+
+ {
+ errors: []
+ }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/ci/job/cancel.rb b/app/graphql/mutations/ci/job/cancel.rb
index dc9f4d19779..44a7772019d 100644
--- a/app/graphql/mutations/ci/job/cancel.rb
+++ b/app/graphql/mutations/ci/job/cancel.rb
@@ -11,7 +11,7 @@ module Mutations
null: true,
description: 'Job after the mutation.'
- authorize :update_build
+ authorize :cancel_build
def resolve(id:)
job = authorized_find!(id: id)
diff --git a/app/graphql/mutations/ci/pipeline/cancel.rb b/app/graphql/mutations/ci/pipeline/cancel.rb
index 810f458fd75..1014462d0b1 100644
--- a/app/graphql/mutations/ci/pipeline/cancel.rb
+++ b/app/graphql/mutations/ci/pipeline/cancel.rb
@@ -6,7 +6,7 @@ module Mutations
class Cancel < Base
graphql_name 'PipelineCancel'
- authorize :update_pipeline
+ authorize :cancel_pipeline
def resolve(id:)
pipeline = authorized_find!(id: id)
diff --git a/app/graphql/mutations/commits/create.rb b/app/graphql/mutations/commits/create.rb
index 02e1e4c78bf..cbe2c49e950 100644
--- a/app/graphql/mutations/commits/create.rb
+++ b/app/graphql/mutations/commits/create.rb
@@ -64,7 +64,7 @@ module Mutations
result = ::Files::MultiService.new(project, current_user, attributes).execute
{
- content: actions.pluck(:content), # rubocop:disable CodeReuse/ActiveRecord because actions is an Array, not a Relation
+ content: actions.pluck(:content), # rubocop:disable CodeReuse/ActiveRecord -- Array#pluck
commit: (project.repository.commit(result[:result]) if result[:status] == :success),
commit_pipeline_path: UrlHelpers.new.graphql_etag_pipeline_sha_path(result[:result]),
errors: Array.wrap(result[:message])
diff --git a/app/graphql/mutations/container_registry/protection/rule/create.rb b/app/graphql/mutations/container_registry/protection/rule/create.rb
new file mode 100644
index 00000000000..cf8416480a2
--- /dev/null
+++ b/app/graphql/mutations/container_registry/protection/rule/create.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+module Mutations
+ module ContainerRegistry
+ module Protection
+ module Rule
+ class Create < ::Mutations::BaseMutation
+ graphql_name 'CreateContainerRegistryProtectionRule'
+ description 'Creates a protection rule to restrict access to a project\'s container registry. ' \
+ 'Available only when feature flag `container_registry_protected_containers` is enabled.'
+
+ include FindsProject
+
+ authorize :admin_container_image
+
+ argument :project_path,
+ GraphQL::Types::ID,
+ required: true,
+ description: 'Full path of the project where a protection rule is located.'
+
+ argument :container_path_pattern,
+ GraphQL::Types::String,
+ required: true,
+ description:
+ 'ContainerRegistryname protected by the protection rule. For example `@my-scope/my-container-*`. ' \
+ 'Wildcard character `*` allowed.'
+
+ argument :push_protected_up_to_access_level,
+ Types::ContainerRegistry::Protection::RuleAccessLevelEnum,
+ required: true,
+ description:
+ 'Max GitLab access level to prevent from pushing container images to the container registry. ' \
+ 'For example `DEVELOPER`, `MAINTAINER`, `OWNER`.'
+
+ argument :delete_protected_up_to_access_level,
+ Types::ContainerRegistry::Protection::RuleAccessLevelEnum,
+ required: true,
+ description:
+ 'Max GitLab access level to prevent from deleting container images in the container registry. ' \
+ 'For example `DEVELOPER`, `MAINTAINER`, `OWNER`.'
+
+ field :container_registry_protection_rule,
+ Types::ContainerRegistry::Protection::RuleType,
+ null: true,
+ description: 'Container registry protection rule after mutation.'
+
+ def resolve(project_path:, **kwargs)
+ project = authorized_find!(project_path)
+
+ if Feature.disabled?(:container_registry_protected_containers, project)
+ raise_resource_not_available_error!("'container_registry_protected_containers' feature flag is disabled")
+ end
+
+ response = ::ContainerRegistry::Protection::CreateRuleService.new(project, current_user, kwargs).execute
+
+ { container_registry_protection_rule: response.payload[:container_registry_protection_rule],
+ errors: response.errors }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/merge_requests/accept.rb b/app/graphql/mutations/merge_requests/accept.rb
index 220ebea22c7..604fdd49f45 100644
--- a/app/graphql/mutations/merge_requests/accept.rb
+++ b/app/graphql/mutations/merge_requests/accept.rb
@@ -9,6 +9,10 @@ module Mutations
Accepts a merge request.
When accepted, the source branch will be scheduled to merge into the target branch, either
immediately if possible, or using one of the automatic merge strategies.
+
+ [In GitLab 16.5](https://gitlab.com/gitlab-org/gitlab/-/issues/421510), the merging happens asynchronously.
+ This results in `mergeRequest` and `state` not updating after a mutation request,
+ because the merging may not have happened yet.
DESC
NOT_MERGEABLE = 'This branch cannot be merged'
diff --git a/app/graphql/mutations/namespace/package_settings/update.rb b/app/graphql/mutations/namespace/package_settings/update.rb
index 4e71bed52c6..97c16ee79fe 100644
--- a/app/graphql/mutations/namespace/package_settings/update.rb
+++ b/app/graphql/mutations/namespace/package_settings/update.rb
@@ -8,8 +8,6 @@ module Mutations
include Mutations::ResolvesNamespace
- NUGET_DUPLICATES_FF_ERROR = '`nuget_duplicates_option` feature flag is disabled.'
-
description <<~DESC
These settings can be adjusted by the group Owner or Maintainer.
[Issue 370471](https://gitlab.com/gitlab-org/gitlab/-/issues/370471) proposes limiting
@@ -91,10 +89,6 @@ module Mutations
def resolve(namespace_path:, **args)
namespace = authorized_find!(namespace_path: namespace_path)
- if nuget_duplicate_settings_present?(args) && Feature.disabled?(:nuget_duplicates_option, namespace)
- raise_resource_not_available_error! NUGET_DUPLICATES_FF_ERROR
- end
-
result = ::Namespaces::PackageSettings::UpdateService
.new(container: namespace, current_user: current_user, params: args)
.execute
@@ -110,10 +104,6 @@ module Mutations
def find_object(namespace_path:)
resolve_namespace(full_path: namespace_path)
end
-
- def nuget_duplicate_settings_present?(args)
- args.key?(:nuget_duplicates_allowed) || args.key?(:nuget_duplicate_exception_regex)
- end
end
end
end
diff --git a/app/graphql/mutations/organizations/create.rb b/app/graphql/mutations/organizations/create.rb
new file mode 100644
index 00000000000..0d1b204a4c1
--- /dev/null
+++ b/app/graphql/mutations/organizations/create.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Organizations
+ class Create < BaseMutation
+ graphql_name 'OrganizationCreate'
+
+ authorize :create_organization
+
+ field :organization,
+ ::Types::Organizations::OrganizationType,
+ null: true,
+ description: 'Organization created.'
+
+ argument :name, GraphQL::Types::String,
+ required: true,
+ description: 'Name for the organization.'
+
+ argument :path, GraphQL::Types::String,
+ required: true,
+ description: 'Path for the organization.'
+
+ def resolve(args)
+ authorize!(:global)
+
+ result = ::Organizations::CreateService.new(
+ current_user: current_user,
+ params: args
+ ).execute
+
+ { organization: result.payload, errors: result.errors }
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/packages/protection/rule/delete.rb b/app/graphql/mutations/packages/protection/rule/delete.rb
new file mode 100644
index 00000000000..bd0159d3c23
--- /dev/null
+++ b/app/graphql/mutations/packages/protection/rule/delete.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Packages
+ module Protection
+ module Rule
+ class Delete < ::Mutations::BaseMutation
+ graphql_name 'DeletePackagesProtectionRule'
+ description 'Deletes a protection rule for packages. ' \
+ 'Available only when feature flag `packages_protected_packages` is enabled.'
+
+ authorize :admin_package
+
+ argument :id,
+ ::Types::GlobalIDType[::Packages::Protection::Rule],
+ required: true,
+ description: 'Global ID of the package protection rule to delete.'
+
+ field :package_protection_rule,
+ Types::Packages::Protection::RuleType,
+ null: true,
+ description: 'Packages protection rule that was deleted successfully.'
+
+ def resolve(id:, **_kwargs)
+ if Feature.disabled?(:packages_protected_packages)
+ raise_resource_not_available_error!("'packages_protected_packages' feature flag is disabled")
+ end
+
+ package_protection_rule = authorized_find!(id: id)
+
+ response = ::Packages::Protection::DeleteRuleService.new(package_protection_rule,
+ current_user: current_user).execute
+
+ { package_protection_rule: response.payload[:package_protection_rule], errors: response.errors }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/saved_replies/base.rb b/app/graphql/mutations/saved_replies/base.rb
index 4923fcb7851..79761645eb7 100644
--- a/app/graphql/mutations/saved_replies/base.rb
+++ b/app/graphql/mutations/saved_replies/base.rb
@@ -23,10 +23,6 @@ module Mutations
end
end
- def feature_enabled?
- Feature.enabled?(:saved_replies, current_user)
- end
-
def find_object(id)
GitlabSchema.find_by_gid(id)
end
diff --git a/app/graphql/mutations/saved_replies/create.rb b/app/graphql/mutations/saved_replies/create.rb
index d97461a1c2a..25c02b79cb8 100644
--- a/app/graphql/mutations/saved_replies/create.rb
+++ b/app/graphql/mutations/saved_replies/create.rb
@@ -16,8 +16,6 @@ module Mutations
description: copy_field_description(Types::SavedReplyType, :content)
def resolve(name:, content:)
- raise Gitlab::Graphql::Errors::ResourceNotAvailable, 'Feature disabled' unless feature_enabled?
-
result = ::Users::SavedReplies::CreateService.new(current_user: current_user, name: name, content: content).execute
present_result(result)
end
diff --git a/app/graphql/mutations/saved_replies/destroy.rb b/app/graphql/mutations/saved_replies/destroy.rb
index 7cd0f21ad45..655ed9cb798 100644
--- a/app/graphql/mutations/saved_replies/destroy.rb
+++ b/app/graphql/mutations/saved_replies/destroy.rb
@@ -12,8 +12,6 @@ module Mutations
description: copy_field_description(Types::SavedReplyType, :id)
def resolve(id:)
- raise Gitlab::Graphql::Errors::ResourceNotAvailable, 'Feature disabled' unless feature_enabled?
-
saved_reply = authorized_find!(id)
result = ::Users::SavedReplies::DestroyService.new(saved_reply: saved_reply).execute
present_result(result)
diff --git a/app/graphql/mutations/saved_replies/update.rb b/app/graphql/mutations/saved_replies/update.rb
index d9368de7547..f5dc81614d2 100644
--- a/app/graphql/mutations/saved_replies/update.rb
+++ b/app/graphql/mutations/saved_replies/update.rb
@@ -20,8 +20,6 @@ module Mutations
description: copy_field_description(Types::SavedReplyType, :content)
def resolve(id:, name:, content:)
- raise Gitlab::Graphql::Errors::ResourceNotAvailable, 'Feature disabled' unless feature_enabled?
-
saved_reply = authorized_find!(id)
result = ::Users::SavedReplies::UpdateService.new(saved_reply: saved_reply, name: name, content: content).execute
present_result(result)
diff --git a/app/graphql/resolvers/analytics/cycle_analytics/deployment_count_resolver.rb b/app/graphql/resolvers/analytics/cycle_analytics/deployment_count_resolver.rb
index 51a1afdd5ab..2d722b02bf1 100644
--- a/app/graphql/resolvers/analytics/cycle_analytics/deployment_count_resolver.rb
+++ b/app/graphql/resolvers/analytics/cycle_analytics/deployment_count_resolver.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-# rubocop:disable Graphql/ResolverType (inherited from Resolvers::Analytics::CycleAnalytics::BaseCountResolver)
+# rubocop:disable Graphql/ResolverType -- inherited from Resolvers::Analytics::CycleAnalytics::BaseCountResolver
module Resolvers
module Analytics
module CycleAnalytics
diff --git a/app/graphql/resolvers/analytics/cycle_analytics/issue_count_resolver.rb b/app/graphql/resolvers/analytics/cycle_analytics/issue_count_resolver.rb
index fd20800ee16..32b884df84f 100644
--- a/app/graphql/resolvers/analytics/cycle_analytics/issue_count_resolver.rb
+++ b/app/graphql/resolvers/analytics/cycle_analytics/issue_count_resolver.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-# rubocop:disable Graphql/ResolverType (inherited from Resolvers::Analytics::CycleAnalytics::BaseIssueResolver)
+# rubocop:disable Graphql/ResolverType -- inherited from Resolvers::Analytics::CycleAnalytics::BaseIssueResolver
module Resolvers
module Analytics
module CycleAnalytics
diff --git a/app/graphql/resolvers/ci/catalog/resource_resolver.rb b/app/graphql/resolvers/ci/catalog/resource_resolver.rb
new file mode 100644
index 00000000000..4b722bd3ec7
--- /dev/null
+++ b/app/graphql/resolvers/ci/catalog/resource_resolver.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Ci
+ module Catalog
+ class ResourceResolver < BaseResolver
+ include Gitlab::Graphql::Authorize::AuthorizeResource
+
+ authorize :read_code
+
+ type ::Types::Ci::Catalog::ResourceType, null: true
+
+ argument :id, ::Types::GlobalIDType[::Ci::Catalog::Resource],
+ required: false,
+ description: 'CI/CD Catalog resource global ID.'
+
+ argument :full_path, GraphQL::Types::ID,
+ required: false,
+ description: 'CI/CD Catalog resource full path.'
+
+ def ready?(**args)
+ unless args[:id].present? ^ args[:full_path].present?
+ raise Gitlab::Graphql::Errors::ArgumentError,
+ "Exactly one of 'id' or 'full_path' arguments is required."
+ end
+
+ super
+ end
+
+ def resolve(id: nil, full_path: nil)
+ if full_path.present?
+ project = Project.find_by_full_path(full_path)
+ authorize!(project)
+
+ raise_resource_not_available_error! unless project.catalog_resource
+
+ project.catalog_resource
+ else
+ catalog_resource = ::Gitlab::Graphql::Lazy.force(GitlabSchema.find_by_gid(id))
+ authorize!(catalog_resource&.project)
+
+ catalog_resource
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/ci/catalog/resources_resolver.rb b/app/graphql/resolvers/ci/catalog/resources_resolver.rb
new file mode 100644
index 00000000000..c6904dcd7f6
--- /dev/null
+++ b/app/graphql/resolvers/ci/catalog/resources_resolver.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Ci
+ module Catalog
+ class ResourcesResolver < BaseResolver
+ include LooksAhead
+
+ type ::Types::Ci::Catalog::ResourceType.connection_type, null: true
+
+ argument :scope, ::Types::Ci::Catalog::ResourceScopeEnum,
+ required: false,
+ default_value: :all,
+ description: 'Scope of the returned catalog resources.'
+
+ argument :search, GraphQL::Types::String,
+ required: false,
+ description: 'Search term to filter the catalog resources by name or description.'
+
+ argument :sort, ::Types::Ci::Catalog::ResourceSortEnum,
+ required: false,
+ description: 'Sort catalog resources by given criteria.'
+
+ # TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/429636
+ argument :project_path, GraphQL::Types::ID,
+ required: false,
+ description: 'Project with the namespace catalog.'
+
+ def resolve_with_lookahead(scope:, project_path: nil, search: nil, sort: nil)
+ if project_path.present?
+ project = Project.find_by_full_path(project_path)
+
+ apply_lookahead(
+ ::Ci::Catalog::Listing
+ .new(context[:current_user])
+ .resources(namespace: project.root_namespace, sort: sort, search: search)
+ )
+ elsif scope == :all
+ apply_lookahead(::Ci::Catalog::Listing.new(context[:current_user]).resources(sort: sort, search: search))
+ end
+ end
+
+ private
+
+ def preloads
+ {
+ web_path: { project: { namespace: :route } },
+ readme_html: { project: :route }
+ }
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/ci/catalog/versions_resolver.rb b/app/graphql/resolvers/ci/catalog/versions_resolver.rb
new file mode 100644
index 00000000000..046adeb7a67
--- /dev/null
+++ b/app/graphql/resolvers/ci/catalog/versions_resolver.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Ci
+ module Catalog
+ class VersionsResolver < ::Resolvers::ReleasesResolver
+ type Types::ReleaseType.connection_type, null: true
+
+ # This allows a maximum of 1 call to the field that uses this resolver. If the
+ # field is evaluated on more than one node, it causes performance degradation.
+ extension ::Gitlab::Graphql::Limit::FieldCallCount, limit: 1
+
+ private
+
+ def get_project
+ object.respond_to?(:project) ? object.project : object
+ end
+
+ # Override the aliased method in ReleasesResolver
+ alias_method :project, :get_project
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/ci/runners_resolver.rb b/app/graphql/resolvers/ci/runners_resolver.rb
index 3289f1d0056..9121c413b1f 100644
--- a/app/graphql/resolvers/ci/runners_resolver.rb
+++ b/app/graphql/resolvers/ci/runners_resolver.rb
@@ -41,6 +41,17 @@ module Resolvers
required: false,
description: 'Filter by upgrade status.'
+ argument :creator_id, ::Types::GlobalIDType[::User].as('UserID'),
+ required: false,
+ description: 'Filter runners by creator ID.'
+
+ argument :version_prefix, GraphQL::Types::String,
+ required: false,
+ description: "Filter runners by version. Runners that contain runner managers with the version at " \
+ "the start of the search term are returned. For example, the search term '14.' returns " \
+ "runner managers with versions '14.11.1' and '14.2.3'.",
+ alpha: { milestone: '16.6' }
+
def resolve_with_lookahead(**args)
apply_lookahead(
::Ci::RunnersFinder
@@ -68,6 +79,9 @@ module Resolvers
upgrade_status: params[:upgrade_status],
search: params[:search],
sort: params[:sort]&.to_s,
+ creator_id:
+ params[:creator_id] ? ::GitlabSchema.parse_gid(params[:creator_id], expected_type: ::User).model_id : nil,
+ version_prefix: params[:version_prefix],
preload: false # we'll handle preloading ourselves
}.compact
.merge(parent_param)
diff --git a/app/graphql/resolvers/concerns/caching_array_resolver.rb b/app/graphql/resolvers/concerns/caching_array_resolver.rb
index 15bf9a90e46..f678e02533d 100644
--- a/app/graphql/resolvers/concerns/caching_array_resolver.rb
+++ b/app/graphql/resolvers/concerns/caching_array_resolver.rb
@@ -132,7 +132,7 @@ module CachingArrayResolver
model_class.arel_table[Arel.star]
end
- # rubocop: disable Graphql/Descriptions (false positive!)
+ # rubocop: disable Graphql/Descriptions -- false positive
def query_limit
field&.max_page_size.presence || context.schema.default_max_page_size
end
diff --git a/app/graphql/resolvers/concerns/work_items/shared_filter_arguments.rb b/app/graphql/resolvers/concerns/work_items/shared_filter_arguments.rb
index ecb105a64d0..1982b458143 100644
--- a/app/graphql/resolvers/concerns/work_items/shared_filter_arguments.rb
+++ b/app/graphql/resolvers/concerns/work_items/shared_filter_arguments.rb
@@ -17,7 +17,12 @@ module WorkItems
argument :state,
Types::IssuableStateEnum,
required: false,
- description: 'Current state of the work item.'
+ description: 'Current state of the work item.',
+ prepare: ->(state, _ctx) {
+ return state unless state == 'locked'
+
+ raise Gitlab::Graphql::Errors::ArgumentError, Types::IssuableStateEnum::INVALID_LOCKED_MESSAGE
+ }
argument :types,
[Types::IssueTypeEnum],
as: :issue_types,
diff --git a/app/graphql/resolvers/container_repository_tags_resolver.rb b/app/graphql/resolvers/container_repository_tags_resolver.rb
index 55a83dd49da..bc5006ae06c 100644
--- a/app/graphql/resolvers/container_repository_tags_resolver.rb
+++ b/app/graphql/resolvers/container_repository_tags_resolver.rb
@@ -14,21 +14,61 @@ module Resolvers
required: false,
default_value: nil
+ alias_method :container_repository, :object
+
def resolve(sort:, **filters)
- result = tags
+ if container_repository.migrated? && Feature.enabled?(:use_repository_list_tags_on_graphql, container_repository.project)
+ page_size = [filters[:first], filters[:last]].map(&:to_i).max
+
+ result = container_repository.tags_page(
+ before: filters[:before],
+ last: filters[:after],
+ sort: map_sort_field(sort),
+ name: filters[:name],
+ page_size: page_size
+ )
- if filters[:name]
- result = tags.filter do |tag|
- tag.name.include?(filters[:name])
+ Gitlab::Graphql::ExternallyPaginatedArray.new(
+ parse_pagination_cursor(result, :previous),
+ parse_pagination_cursor(result, :next),
+ *result[:tags]
+ )
+ else
+ result = tags
+
+ if filters[:name]
+ result = tags.filter do |tag|
+ tag.name.include?(filters[:name])
+ end
end
- end
- result = sort_tags(result, sort) if sort
- result
+ result = sort_tags(result, sort) if sort
+ result
+ end
end
private
+ def parse_pagination_cursor(result, direction)
+ pagination_uri = result.dig(:pagination, direction, :uri)
+
+ return unless pagination_uri
+
+ query_params = CGI.parse(pagination_uri.query)
+ key = direction == :previous ? 'before' : 'last'
+
+ query_params[key]&.first
+ end
+
+ def map_sort_field(sort)
+ return unless sort
+
+ sort_field, direction = sort.to_s.split('_')
+ return sort_field if direction == 'asc'
+
+ "-#{sort_field}"
+ end
+
def sort_tags(to_be_sorted, sort)
raise StandardError unless Types::ContainerRepositoryTagsSortEnum.enum.include?(sort)
@@ -41,7 +81,7 @@ module Resolvers
end
def tags
- object.tags
+ container_repository.tags
rescue Faraday::Error
raise ::Gitlab::Graphql::Errors::ResourceNotAvailable, "Can't connect to the Container Registry. If this error persists, please review the troubleshooting documentation."
end
diff --git a/app/graphql/resolvers/data_transfer/group_data_transfer_resolver.rb b/app/graphql/resolvers/data_transfer/group_data_transfer_resolver.rb
index 83bb144017c..133b86623f1 100644
--- a/app/graphql/resolvers/data_transfer/group_data_transfer_resolver.rb
+++ b/app/graphql/resolvers/data_transfer/group_data_transfer_resolver.rb
@@ -16,16 +16,12 @@ module Resolvers
def resolve(**args)
return { egress_nodes: [] } unless Feature.enabled?(:data_transfer_monitoring, group)
- results = if Feature.enabled?(:data_transfer_monitoring_mock_data, group)
- ::DataTransfer::MockedTransferFinder.new.execute
- else
- ::DataTransfer::GroupDataTransferFinder.new(
- group: group,
- from: args[:from],
- to: args[:to],
- user: current_user
- ).execute.map(&:attributes)
- end
+ results = ::DataTransfer::GroupDataTransferFinder.new(
+ group: group,
+ from: args[:from],
+ to: args[:to],
+ user: current_user
+ ).execute.map(&:attributes)
{ egress_nodes: results.to_a }
end
diff --git a/app/graphql/resolvers/data_transfer/project_data_transfer_resolver.rb b/app/graphql/resolvers/data_transfer/project_data_transfer_resolver.rb
index c3296f7d4c3..d711f837251 100644
--- a/app/graphql/resolvers/data_transfer/project_data_transfer_resolver.rb
+++ b/app/graphql/resolvers/data_transfer/project_data_transfer_resolver.rb
@@ -16,16 +16,12 @@ module Resolvers
def resolve(**args)
return { egress_nodes: [] } unless Feature.enabled?(:data_transfer_monitoring, project.group)
- results = if Feature.enabled?(:data_transfer_monitoring_mock_data, project.group)
- ::DataTransfer::MockedTransferFinder.new.execute
- else
- ::DataTransfer::ProjectDataTransferFinder.new(
- project: project,
- from: args[:from],
- to: args[:to],
- user: current_user
- ).execute
- end
+ results = ::DataTransfer::ProjectDataTransferFinder.new(
+ project: project,
+ from: args[:from],
+ to: args[:to],
+ user: current_user
+ ).execute
{ egress_nodes: results }
end
diff --git a/app/graphql/resolvers/group_issues_resolver.rb b/app/graphql/resolvers/group_issues_resolver.rb
index 5e0fb27bafa..5a6a3d678b9 100644
--- a/app/graphql/resolvers/group_issues_resolver.rb
+++ b/app/graphql/resolvers/group_issues_resolver.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-# rubocop:disable Graphql/ResolverType (inherited from Issues::BaseParentResolver)
+# rubocop:disable Graphql/ResolverType -- inherited from Issues::BaseParentResolver
module Resolvers
class GroupIssuesResolver < Issues::BaseParentResolver
def self.issuable_collection_name
diff --git a/app/graphql/resolvers/issues/base_parent_resolver.rb b/app/graphql/resolvers/issues/base_parent_resolver.rb
index 6308e56f049..78ef4132baf 100644
--- a/app/graphql/resolvers/issues/base_parent_resolver.rb
+++ b/app/graphql/resolvers/issues/base_parent_resolver.rb
@@ -7,8 +7,13 @@ module Resolvers
include ::Issues::SortArguments
argument :state, Types::IssuableStateEnum,
- required: false,
- description: 'Current state of this issue.'
+ required: false,
+ description: 'Current state of this issue.',
+ prepare: ->(state, _ctx) {
+ return state unless state == 'locked'
+
+ raise Gitlab::Graphql::Errors::ArgumentError, Types::IssuableStateEnum::INVALID_LOCKED_MESSAGE
+ }
# see app/graphql/types/issue_connection.rb
type 'Types::IssueConnection', null: true
diff --git a/app/graphql/resolvers/issues_resolver.rb b/app/graphql/resolvers/issues_resolver.rb
index 34f14eee0e5..bc0e7334303 100644
--- a/app/graphql/resolvers/issues_resolver.rb
+++ b/app/graphql/resolvers/issues_resolver.rb
@@ -14,7 +14,12 @@ module Resolvers
description: 'Whether to include issues from archived projects. Defaults to `false`.'
argument :state, Types::IssuableStateEnum,
required: false,
- description: 'Current state of this issue.'
+ description: 'Current state of this issue.',
+ prepare: ->(state, _ctx) {
+ return state unless state == 'locked'
+
+ raise Gitlab::Graphql::Errors::ArgumentError, Types::IssuableStateEnum::INVALID_LOCKED_MESSAGE
+ }
# see app/graphql/types/issue_connection.rb
type 'Types::IssueConnection', null: true
diff --git a/app/graphql/resolvers/namespaces/work_items_resolver.rb b/app/graphql/resolvers/namespaces/work_items_resolver.rb
index 6985a7a898a..671788668b1 100644
--- a/app/graphql/resolvers/namespaces/work_items_resolver.rb
+++ b/app/graphql/resolvers/namespaces/work_items_resolver.rb
@@ -2,7 +2,7 @@
module Resolvers
module Namespaces
- # rubocop:disable Graphql/ResolverType (inherited from Resolvers::WorkItemsResolver)
+ # rubocop:disable Graphql/ResolverType -- inherited from Resolvers::WorkItemsResolver
class WorkItemsResolver < ::Resolvers::WorkItemsResolver
def ready?(**args)
return false if Feature.disabled?(:namespace_level_work_items, resource_parent)
diff --git a/app/graphql/resolvers/packages_base_resolver.rb b/app/graphql/resolvers/packages_base_resolver.rb
index 7d153d16910..7e5d89a7897 100644
--- a/app/graphql/resolvers/packages_base_resolver.rb
+++ b/app/graphql/resolvers/packages_base_resolver.rb
@@ -19,6 +19,12 @@ module Resolvers
required: false,
default_value: nil
+ argument :package_version, GraphQL::Types::String,
+ description: 'Filter a package by version. If used in combination with `include_versionless`,
+ then no versionless packages are returned.',
+ required: false,
+ default_value: nil
+
argument :status, Types::Packages::PackageStatusEnum,
description: 'Filter a package by status.',
required: false,
diff --git a/app/graphql/resolvers/project_issues_resolver.rb b/app/graphql/resolvers/project_issues_resolver.rb
index f869d8f11c6..2bc610e8266 100644
--- a/app/graphql/resolvers/project_issues_resolver.rb
+++ b/app/graphql/resolvers/project_issues_resolver.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-# rubocop:disable Graphql/ResolverType (inherited from Issues::BaseParentResolver)
+# rubocop:disable Graphql/ResolverType -- inherited from Issues::BaseParentResolver
module Resolvers
class ProjectIssuesResolver < Issues::BaseParentResolver
accept_release_tag
diff --git a/app/graphql/resolvers/project_members_resolver.rb b/app/graphql/resolvers/project_members_resolver.rb
index e889b47c000..a27183438cd 100644
--- a/app/graphql/resolvers/project_members_resolver.rb
+++ b/app/graphql/resolvers/project_members_resolver.rb
@@ -1,5 +1,6 @@
# frozen_string_literal: true
-# rubocop:disable Graphql/ResolverType (inherited from MembersResolver)
+
+# rubocop:disable Graphql/ResolverType -- inherited from MembersResolver
module Resolvers
class ProjectMembersResolver < MembersResolver
@@ -17,3 +18,4 @@ module Resolvers
end
end
end
+# rubocop:enable Graphql/ResolverType
diff --git a/app/graphql/resolvers/project_milestones_resolver.rb b/app/graphql/resolvers/project_milestones_resolver.rb
index 567a55aa09b..cb4e9a5cdf7 100644
--- a/app/graphql/resolvers/project_milestones_resolver.rb
+++ b/app/graphql/resolvers/project_milestones_resolver.rb
@@ -1,5 +1,4 @@
# frozen_string_literal: true
-# rubocop:disable Graphql/ResolverType (inherited from MilestonesResolver)
module Resolvers
class ProjectMilestonesResolver < MilestonesResolver
diff --git a/app/graphql/resolvers/projects/snippets_resolver.rb b/app/graphql/resolvers/projects/snippets_resolver.rb
index 448918be2f5..9ab9db21e89 100644
--- a/app/graphql/resolvers/projects/snippets_resolver.rb
+++ b/app/graphql/resolvers/projects/snippets_resolver.rb
@@ -1,5 +1,6 @@
# frozen_string_literal: true
-# rubocop:disable Graphql/ResolverType (inherited from ResolvesSnippets)
+
+# rubocop:disable Graphql/ResolverType -- inherited from ResolvesSnippets
module Resolvers
module Projects
@@ -27,3 +28,4 @@ module Resolvers
end
end
end
+# rubocop:enable Graphql/ResolverType
diff --git a/app/graphql/resolvers/projects_resolver.rb b/app/graphql/resolvers/projects_resolver.rb
index 8dd409a8173..450caa9aff6 100644
--- a/app/graphql/resolvers/projects_resolver.rb
+++ b/app/graphql/resolvers/projects_resolver.rb
@@ -3,6 +3,7 @@
module Resolvers
class ProjectsResolver < BaseResolver
include ProjectSearchArguments
+ include LooksAhead
type Types::ProjectType.connection_type, null: true
@@ -10,6 +11,10 @@ module Resolvers
required: false,
description: 'Filter projects by IDs.'
+ argument :full_paths, [GraphQL::Types::String],
+ required: false,
+ description: 'Filter projects by full paths. You cannot provide more than 50 full paths.'
+
argument :sort, GraphQL::Types::String,
required: false,
description: "Sort order of results. Format: `<field_name>_<sort_direction>`, " \
@@ -23,19 +28,48 @@ module Resolvers
required: false,
description: "Return only projects with merge requests enabled."
- def resolve(**args)
- ProjectsFinder
+ def resolve_with_lookahead(**args)
+ validate_args!(args)
+
+ projects = ProjectsFinder
.new(current_user: current_user, params: finder_params(args), project_ids_relation: parse_gids(args[:ids]))
.execute
+
+ apply_lookahead(projects)
end
private
+ def validate_args!(args)
+ return unless args[:full_paths].present? && args[:full_paths].length > 50
+
+ raise Gitlab::Graphql::Errors::ArgumentError, 'You cannot provide more than 50 full_paths'
+ end
+
+ def unconditional_includes
+ [:creator, :group, :invited_groups, :project_setting]
+ end
+
+ def preloads
+ {
+ full_path: [:route],
+ topics: [:topics],
+ import_status: [:import_state],
+ service_desk_address: [:project_feature, :service_desk_setting],
+ jira_import_status: [:jira_imports],
+ container_repositories: [:container_repositories],
+ container_repositories_count: [:container_repositories],
+ web_url: { namespace: [:route] },
+ is_catalog_resource: [:catalog_resource]
+ }
+ end
+
def finder_params(args)
{
**project_finder_params(args),
with_issues_enabled: args[:with_issues_enabled],
- with_merge_requests_enabled: args[:with_merge_requests_enabled]
+ with_merge_requests_enabled: args[:with_merge_requests_enabled],
+ full_paths: args[:full_paths]
}
end
@@ -44,3 +78,5 @@ module Resolvers
end
end
end
+
+Resolvers::ProjectsResolver.prepend_mod_with('Resolvers::ProjectsResolver')
diff --git a/app/graphql/resolvers/saved_reply_resolver.rb b/app/graphql/resolvers/saved_reply_resolver.rb
index 96bbc139c96..1a5f2c9be78 100644
--- a/app/graphql/resolvers/saved_reply_resolver.rb
+++ b/app/graphql/resolvers/saved_reply_resolver.rb
@@ -11,8 +11,6 @@ module Resolvers
description: 'ID of a saved reply.'
def resolve(id:)
- return unless Feature.enabled?(:saved_replies, current_user)
-
saved_reply = ::Users::SavedReply.find_saved_reply(user_id: current_user.id, id: id.model_id)
return unless saved_reply
diff --git a/app/graphql/resolvers/snippets_resolver.rb b/app/graphql/resolvers/snippets_resolver.rb
index 90f5f2cb534..759cc61a8a7 100644
--- a/app/graphql/resolvers/snippets_resolver.rb
+++ b/app/graphql/resolvers/snippets_resolver.rb
@@ -1,5 +1,6 @@
# frozen_string_literal: true
-# rubocop:disable Graphql/ResolverType (inherited from ResolvesSnippets)
+
+# rubocop:disable Graphql/ResolverType -- inherited from ResolvesSnippets
module Resolvers
class SnippetsResolver < BaseResolver
@@ -45,3 +46,4 @@ module Resolvers
end
end
end
+# rubocop:enable Graphql/ResolverType
diff --git a/app/graphql/resolvers/users/frecent_groups_resolver.rb b/app/graphql/resolvers/users/frecent_groups_resolver.rb
new file mode 100644
index 00000000000..2fc757e31ab
--- /dev/null
+++ b/app/graphql/resolvers/users/frecent_groups_resolver.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Users
+ class FrecentGroupsResolver < BaseResolver
+ include Gitlab::Graphql::Authorize::AuthorizeResource
+
+ type [Types::GroupType], null: true
+
+ def resolve
+ return unless current_user.present?
+
+ if Feature.disabled?(:frecent_namespaces_suggestions, current_user)
+ raise_resource_not_available_error!("'frecent_namespaces_suggestions' feature flag is disabled")
+ end
+
+ return unless Feature.enabled?(:frecent_namespaces_suggestions, current_user)
+
+ ::Users::GroupVisit.frecent_groups(user_id: current_user.id)
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/users/frecent_projects_resolver.rb b/app/graphql/resolvers/users/frecent_projects_resolver.rb
new file mode 100644
index 00000000000..397d4ca0cfd
--- /dev/null
+++ b/app/graphql/resolvers/users/frecent_projects_resolver.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Users
+ class FrecentProjectsResolver < BaseResolver
+ include Gitlab::Graphql::Authorize::AuthorizeResource
+
+ type [Types::ProjectType], null: true
+
+ def resolve
+ return unless current_user.present?
+
+ if Feature.disabled?(:frecent_namespaces_suggestions, current_user)
+ raise_resource_not_available_error!("'frecent_namespaces_suggestions' feature flag is disabled")
+ end
+
+ ::Users::ProjectVisit.frecent_projects(user_id: current_user.id)
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/users/organizations_resolver.rb b/app/graphql/resolvers/users/organizations_resolver.rb
new file mode 100644
index 00000000000..ffc1a141eb6
--- /dev/null
+++ b/app/graphql/resolvers/users/organizations_resolver.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Users
+ class OrganizationsResolver < BaseResolver
+ include Gitlab::Graphql::Authorize::AuthorizeResource
+
+ type Types::Organizations::OrganizationType.connection_type, null: true
+
+ authorize :read_user_organizations
+ authorizes_object!
+
+ def resolve(**args)
+ ::Organizations::UserOrganizationsFinder.new(current_user, object, args).execute
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/users/snippets_resolver.rb b/app/graphql/resolvers/users/snippets_resolver.rb
index 75bba8debab..ea5f6b7b8c9 100644
--- a/app/graphql/resolvers/users/snippets_resolver.rb
+++ b/app/graphql/resolvers/users/snippets_resolver.rb
@@ -1,5 +1,6 @@
# frozen_string_literal: true
-# rubocop:disable Graphql/ResolverType (inherited from ResolvesSnippets)
+
+# rubocop:disable Graphql/ResolverType -- inherited from ResolvesSnippets
module Resolvers
module Users
@@ -27,3 +28,4 @@ module Resolvers
end
end
end
+# rubocop:enable Graphql/ResolverType
diff --git a/app/graphql/resolvers/work_items/linked_items_resolver.rb b/app/graphql/resolvers/work_items/linked_items_resolver.rb
index 108d5d41b62..f2ff1205d3a 100644
--- a/app/graphql/resolvers/work_items/linked_items_resolver.rb
+++ b/app/graphql/resolvers/work_items/linked_items_resolver.rb
@@ -3,6 +3,8 @@
module Resolvers
module WorkItems
class LinkedItemsResolver < BaseResolver
+ prepend ::WorkItems::LookAheadPreloads
+
alias_method :linked_items_widget, :object
argument :filter, Types::WorkItems::RelatedLinkTypeEnum,
@@ -13,30 +15,28 @@ module Resolvers
type Types::WorkItems::LinkedItemType.connection_type, null: true
- def resolve(filter: nil)
- related_work_items(filter).map do |related_work_item|
- {
- link_id: related_work_item.issue_link_id,
- link_type: related_work_item.issue_link_type,
- link_created_at: related_work_item.issue_link_created_at,
- link_updated_at: related_work_item.issue_link_updated_at,
- work_item: related_work_item
- }
- end
+ def resolve_with_lookahead(**args)
+ apply_lookahead(related_work_items(args))
end
private
- def related_work_items(type)
- return [] unless work_item.resource_parent.linked_work_items_feature_flag_enabled?
+ def related_work_items(args)
+ return WorkItem.none unless work_item.resource_parent.linked_work_items_feature_flag_enabled?
- work_item.linked_work_items(current_user, preload: { project: [:project_feature, :group] }, link_type: type)
+ offset_pagination(
+ work_item.linked_work_items(authorize: false, link_type: args[:filter])
+ )
end
def work_item
linked_items_widget.work_item
end
strong_memoize_attr :work_item
+
+ def node_selection(selection = lookahead)
+ super.selection(:work_item)
+ end
end
end
end
diff --git a/app/graphql/types/abuse_report_type.rb b/app/graphql/types/abuse_report_type.rb
index 012e709cdb5..2532530cfa9 100644
--- a/app/graphql/types/abuse_report_type.rb
+++ b/app/graphql/types/abuse_report_type.rb
@@ -3,9 +3,18 @@
module Types
class AbuseReportType < BaseObject
graphql_name 'AbuseReport'
+
+ implements Types::Notes::NoteableInterface
+
description 'An abuse report'
+
authorize :read_abuse_report
+ expose_permissions Types::PermissionTypes::AbuseReport
+
+ field :id, Types::GlobalIDType[::AbuseReport],
+ null: false, description: 'Global ID of the abuse report.'
+
field :labels, ::Types::LabelType.connection_type,
null: true, description: 'Labels of the abuse report.'
end
diff --git a/app/graphql/types/analytics/cycle_analytics/value_stream_type.rb b/app/graphql/types/analytics/cycle_analytics/value_stream_type.rb
new file mode 100644
index 00000000000..16ce9b82718
--- /dev/null
+++ b/app/graphql/types/analytics/cycle_analytics/value_stream_type.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Types
+ module Analytics
+ module CycleAnalytics
+ class ValueStreamType < BaseObject
+ graphql_name 'ValueStream'
+
+ authorize :read_cycle_analytics
+
+ field :id,
+ type: ::Types::GlobalIDType[::Analytics::CycleAnalytics::ValueStream],
+ null: false,
+ description: "ID of the value stream."
+
+ field :name,
+ GraphQL::Types::String,
+ null: false,
+ description: 'Name of the value stream.'
+
+ field :namespace, Types::NamespaceType,
+ null: false,
+ description: 'Namespace the value stream belongs to.'
+
+ field :project, Types::ProjectType,
+ null: true,
+ description: 'Project the value stream belongs to, returns empty if it belongs to a group.',
+ alpha: { milestone: '15.6' }
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/base_argument.rb b/app/graphql/types/base_argument.rb
index cda7fa4a5df..3b4223c3ba1 100644
--- a/app/graphql/types/base_argument.rb
+++ b/app/graphql/types/base_argument.rb
@@ -9,29 +9,7 @@ module Types
def initialize(*args, **kwargs, &block)
@doc_reference = kwargs.delete(:see)
- # our custom addition `nullable` which allows us to declare
- # an argument that must be provided, even if its value is null.
- # When `required: true` then required arguments must not be null.
- @gl_required = !!kwargs[:required]
- @gl_nullable = kwargs[:required] == :nullable
-
- # Only valid if an argument is also required.
- if @gl_nullable
- # Since the framework asserts that "required" means "cannot be null"
- # we have to switch off "required" but still do the check in `ready?` behind the scenes
- kwargs[:required] = false
- end
-
super(*args, **kwargs, &block)
end
-
- def accepts?(value)
- # if the argument is declared as required, it must be included
- return false if @gl_required && value == :not_given
- # if the argument is declared as required, the value can only be null IF it is also nullable.
- return false if @gl_required && value.nil? && !@gl_nullable
-
- true
- end
end
end
diff --git a/app/graphql/types/base_input_object.rb b/app/graphql/types/base_input_object.rb
index 90a29b0cfb8..d14da9ac878 100644
--- a/app/graphql/types/base_input_object.rb
+++ b/app/graphql/types/base_input_object.rb
@@ -3,5 +3,7 @@
module Types
class BaseInputObject < GraphQL::Schema::InputObject
prepend Gitlab::Graphql::CopyFieldDescription
+
+ argument_class ::Types::BaseArgument
end
end
diff --git a/app/graphql/types/ci/catalog/resource_scope_enum.rb b/app/graphql/types/ci/catalog/resource_scope_enum.rb
new file mode 100644
index 00000000000..b825c3a7925
--- /dev/null
+++ b/app/graphql/types/ci/catalog/resource_scope_enum.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ module Catalog
+ class ResourceScopeEnum < BaseEnum
+ graphql_name 'CiCatalogResourceScope'
+ description 'Values for scoping catalog resources'
+
+ value 'ALL', 'All catalog resources visible to the current user.', value: :all
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/ci/catalog/resource_sort_enum.rb b/app/graphql/types/ci/catalog/resource_sort_enum.rb
new file mode 100644
index 00000000000..bb0b5a6e0eb
--- /dev/null
+++ b/app/graphql/types/ci/catalog/resource_sort_enum.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ module Catalog
+ class ResourceSortEnum < BaseEnum
+ graphql_name 'CiCatalogResourceSort'
+ description 'Values for sorting catalog resources'
+
+ value 'NAME_ASC', 'Name by ascending order.', value: :name_asc
+ value 'NAME_DESC', 'Name by descending order.', value: :name_desc
+ value 'LATEST_RELEASED_AT_ASC', 'Latest release date by ascending order.', value: :latest_released_at_asc
+ value 'LATEST_RELEASED_AT_DESC', 'Latest release date by descending order.', value: :latest_released_at_desc
+ value 'CREATED_ASC', 'Created date by ascending order.', value: :created_at_asc
+ value 'CREATED_DESC', 'Created date by descending order.', value: :created_at_desc
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/ci/catalog/resource_type.rb b/app/graphql/types/ci/catalog/resource_type.rb
new file mode 100644
index 00000000000..119313ae52b
--- /dev/null
+++ b/app/graphql/types/ci/catalog/resource_type.rb
@@ -0,0 +1,120 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ module Catalog
+ # rubocop: disable Graphql/AuthorizeTypes
+ class ResourceType < BaseObject
+ graphql_name 'CiCatalogResource'
+
+ connection_type_class Types::CountableConnectionType
+
+ field :open_issues_count, GraphQL::Types::Int, null: false,
+ description: 'Count of open issues that belong to the the catalog resource.',
+ alpha: { milestone: '16.3' }
+
+ field :open_merge_requests_count, GraphQL::Types::Int, null: false,
+ description: 'Count of open merge requests that belong to the the catalog resource.',
+ alpha: { milestone: '16.3' }
+
+ field :id, GraphQL::Types::ID, null: false, description: 'ID of the catalog resource.',
+ alpha: { milestone: '15.11' }
+
+ field :name, GraphQL::Types::String, null: true, description: 'Name of the catalog resource.',
+ alpha: { milestone: '15.11' }
+
+ field :description, GraphQL::Types::String, null: true, description: 'Description of the catalog resource.',
+ alpha: { milestone: '15.11' }
+
+ field :icon, GraphQL::Types::String, null: true, description: 'Icon for the catalog resource.',
+ method: :avatar_path, alpha: { milestone: '15.11' }
+
+ field :web_path, GraphQL::Types::String, null: true, description: 'Web path of the catalog resource.',
+ alpha: { milestone: '16.1' }
+
+ field :versions, Types::ReleaseType.connection_type, null: true,
+ description: 'Versions of the catalog resource. This field can only be ' \
+ 'resolved for one catalog resource in any single request.',
+ resolver: Resolvers::Ci::Catalog::VersionsResolver,
+ alpha: { milestone: '16.2' }
+
+ field :latest_version, Types::ReleaseType, null: true, description: 'Latest version of the catalog resource.',
+ alpha: { milestone: '16.1' }
+
+ field :latest_released_at, Types::TimeType, null: true,
+ description: "Release date of the catalog resource's latest version.",
+ alpha: { milestone: '16.5' }
+
+ field :star_count, GraphQL::Types::Int, null: false,
+ description: 'Number of times the catalog resource has been starred.',
+ alpha: { milestone: '16.1' }
+
+ field :forks_count, GraphQL::Types::Int, null: false, calls_gitaly: true,
+ description: 'Number of times the catalog resource has been forked.',
+ alpha: { milestone: '16.1' }
+
+ field :root_namespace, Types::NamespaceType, null: true,
+ description: 'Root namespace of the catalog resource.',
+ alpha: { milestone: '16.1' }
+
+ markdown_field :readme_html, null: false,
+ alpha: { milestone: '16.1' }
+
+ def open_issues_count
+ BatchLoader::GraphQL.wrap(object.project.open_issues_count)
+ end
+
+ def open_merge_requests_count
+ BatchLoader::GraphQL.wrap(object.project.open_merge_requests_count)
+ end
+
+ def web_path
+ ::Gitlab::Routing.url_helpers.project_path(object.project)
+ end
+
+ def latest_version
+ BatchLoader::GraphQL.for(object.project).batch do |projects, loader|
+ latest_releases = ReleasesFinder.new(projects, current_user, latest: true).execute
+
+ latest_releases.index_by(&:project).each do |project, latest_release|
+ loader.call(project, latest_release)
+ end
+ end
+ end
+
+ def forks_count
+ BatchLoader::GraphQL.wrap(object.forks_count)
+ end
+
+ def root_namespace
+ BatchLoader::GraphQL.for(object.project_id).batch do |project_ids, loader|
+ projects = Project.id_in(project_ids)
+
+ # This preloader uses traversal_ids to obtain Group-type root namespaces.
+ # It also preloads each project's immediate parent namespace, which effectively
+ # preloads the User-type root namespaces since they cannot be nested (parent == root).
+ Preloaders::ProjectRootAncestorPreloader.new(projects, :group).execute
+ root_namespaces = projects.map(&:root_ancestor)
+
+ # NamespaceType requires the `:read_namespace` ability. We must preload the policy for
+ # Group-type namespaces to avoid N+1 queries caused by the authorization requests.
+ group_root_namespaces = root_namespaces.select { |n| n.type == ::Group.sti_name }
+ Preloaders::GroupPolicyPreloader.new(group_root_namespaces, current_user).execute
+
+ # For User-type namespaces, the authorization request requires preloading the owner objects.
+ user_root_namespaces = root_namespaces.select { |n| n.type == ::Namespaces::UserNamespace.sti_name }
+ ActiveRecord::Associations::Preloader.new(records: user_root_namespaces, associations: :owner).call
+
+ projects.each { |project| loader.call(project.id, project.root_ancestor) }
+ end
+ end
+
+ def readme_html_resolver
+ markdown_context = context.to_h.dup.merge(project: object.project)
+ ::MarkupHelper.markdown(object.project.repository.readme&.data, markdown_context)
+ end
+ end
+ # rubocop: enable Graphql/AuthorizeTypes
+ end
+ end
+end
diff --git a/app/graphql/types/ci/pipeline_status_enum.rb b/app/graphql/types/ci/pipeline_status_enum.rb
index c8e031e18ea..17cf48bb5cf 100644
--- a/app/graphql/types/ci/pipeline_status_enum.rb
+++ b/app/graphql/types/ci/pipeline_status_enum.rb
@@ -7,6 +7,7 @@ module Types
created: 'Pipeline has been created.',
waiting_for_resource: 'A resource (for example, a runner) that the pipeline requires to run is unavailable.',
preparing: 'Pipeline is preparing to run.',
+ waiting_for_callback: 'Pipeline is waiting for an external action.',
pending: 'Pipeline has not started running yet.',
running: 'Pipeline is running.',
failed: 'At least one stage of the pipeline failed.',
diff --git a/app/graphql/types/container_registry/protection/rule_access_level_enum.rb b/app/graphql/types/container_registry/protection/rule_access_level_enum.rb
new file mode 100644
index 00000000000..31e8cbe2e49
--- /dev/null
+++ b/app/graphql/types/container_registry/protection/rule_access_level_enum.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Types
+ module ContainerRegistry
+ module Protection
+ class RuleAccessLevelEnum < BaseEnum
+ graphql_name 'ContainerRegistryProtectionRuleAccessLevel'
+ description 'Access level of a container registry protection rule resource'
+
+ ::ContainerRegistry::Protection::Rule.push_protected_up_to_access_levels.each_key do |access_level_key|
+ value access_level_key.upcase, value: access_level_key.to_s,
+ description: "#{access_level_key.capitalize} access."
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/container_registry/protection/rule_type.rb b/app/graphql/types/container_registry/protection/rule_type.rb
new file mode 100644
index 00000000000..387f0202d2d
--- /dev/null
+++ b/app/graphql/types/container_registry/protection/rule_type.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Types
+ module ContainerRegistry
+ module Protection
+ class RuleType < ::Types::BaseObject
+ graphql_name 'ContainerRegistryProtectionRule'
+ description 'A container registry protection rule designed to prevent users with a certain ' \
+ 'access level or lower from altering the container registry.'
+
+ authorize :admin_container_image
+
+ field :id,
+ ::Types::GlobalIDType[::ContainerRegistry::Protection::Rule],
+ null: false,
+ description: 'ID of the container registry protection rule.'
+
+ field :container_path_pattern,
+ GraphQL::Types::String,
+ null: false,
+ description:
+ 'Container repository path pattern protected by the protection rule. ' \
+ 'For example `@my-scope/my-container-*`. Wildcard character `*` allowed.'
+
+ field :push_protected_up_to_access_level,
+ Types::ContainerRegistry::Protection::RuleAccessLevelEnum,
+ null: false,
+ description:
+ 'Max GitLab access level to prevent from pushing container images to the container registry. ' \
+ 'For example `DEVELOPER`, `MAINTAINER`, `OWNER`.'
+
+ field :delete_protected_up_to_access_level,
+ Types::ContainerRegistry::Protection::RuleAccessLevelEnum,
+ null: false,
+ description:
+ 'Max GitLab access level to prevent from pushing container images to the container registry. ' \
+ 'For example `DEVELOPER`, `MAINTAINER`, `OWNER`.'
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/container_repository_details_type.rb b/app/graphql/types/container_repository_details_type.rb
index 1ee9e76a1c8..b043a7c9d8d 100644
--- a/app/graphql/types/container_repository_details_type.rb
+++ b/app/graphql/types/container_repository_details_type.rb
@@ -13,7 +13,8 @@ module Types
null: true,
description: 'Tags of the container repository.',
max_page_size: 20,
- resolver: Resolvers::ContainerRepositoryTagsResolver
+ resolver: Resolvers::ContainerRepositoryTagsResolver,
+ connection_extension: Gitlab::Graphql::Extensions::ExternallyPaginatedArrayExtension
field :size,
GraphQL::Types::Float,
diff --git a/app/graphql/types/data_transfer/project_data_transfer_type.rb b/app/graphql/types/data_transfer/project_data_transfer_type.rb
index 36afa20194e..363b675209d 100644
--- a/app/graphql/types/data_transfer/project_data_transfer_type.rb
+++ b/app/graphql/types/data_transfer/project_data_transfer_type.rb
@@ -13,7 +13,6 @@ module Types
def total_egress(parent:)
return unless Feature.enabled?(:data_transfer_monitoring, parent.group)
- return 40_000_000 if Feature.enabled?(:data_transfer_monitoring_mock_data, parent.group)
object[:egress_nodes].sum('repository_egress + artifacts_egress + packages_egress + registry_egress')
end
diff --git a/app/graphql/types/group_member_type.rb b/app/graphql/types/group_member_type.rb
index 2745853c9bb..d494c55369d 100644
--- a/app/graphql/types/group_member_type.rb
+++ b/app/graphql/types/group_member_type.rb
@@ -11,11 +11,11 @@ module Types
implements MemberInterface
field :group, Types::GroupType, null: true,
- description: 'Group that a User is a member of.'
+ description: 'Group that a user is a member of.'
field :notification_email,
resolver: Resolvers::GroupMembers::NotificationEmailResolver,
- description: "Group notification email for User. Only available for admins."
+ description: "Group notification email for user. Only available for admins."
def group
Gitlab::Graphql::Loaders::BatchModelLoader.new(Group, object.source_id).find
diff --git a/app/graphql/types/issuable_state_enum.rb b/app/graphql/types/issuable_state_enum.rb
index 5a1b11b3bdc..8e3ed1d4bc8 100644
--- a/app/graphql/types/issuable_state_enum.rb
+++ b/app/graphql/types/issuable_state_enum.rb
@@ -1,10 +1,15 @@
# frozen_string_literal: true
+# DO NOT use this ENUM with issues. We need to define a new enum in places where we
+# need to filter by state. locked is not a valid state filter for issues. More info in
+# https://gitlab.com/gitlab-org/gitlab/-/issues/420667#note_1605900474
module Types
class IssuableStateEnum < BaseEnum
graphql_name 'IssuableState'
description 'State of a GitLab issue or merge request'
+ INVALID_LOCKED_MESSAGE = 'locked is not a valid state filter for issues.'
+
value 'opened', description: 'In open state.'
value 'closed', description: 'In closed state.'
value 'locked', description: 'Discussion has been locked.'
diff --git a/app/graphql/types/merge_request_review_state_enum.rb b/app/graphql/types/merge_request_review_state_enum.rb
index 45f97758425..c7c82de2906 100644
--- a/app/graphql/types/merge_request_review_state_enum.rb
+++ b/app/graphql/types/merge_request_review_state_enum.rb
@@ -5,7 +5,11 @@ module Types
graphql_name 'MergeRequestReviewState'
description 'State of a review of a GitLab merge request.'
- from_rails_enum(::MergeRequestReviewer.states,
- description: "The merge request is %{name}.")
+ value 'UNREVIEWED', value: 'unreviewed',
+ description: 'Awaiting review from merge request reviewer.'
+ value 'REVIEWED', value: 'reviewed',
+ description: 'Merge request reviewer has reviewed.'
+ value 'REQUESTED_CHANGES', value: 'requested_changes',
+ description: 'Merge request reviewer has requested changes.'
end
end
diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb
index e6625e44508..9dca82f1750 100644
--- a/app/graphql/types/merge_request_type.rb
+++ b/app/graphql/types/merge_request_type.rb
@@ -106,7 +106,8 @@ module Types
null: false,
description: 'Status of all mergeability checks of the merge request.',
method: :all_mergeability_checks_results,
- alpha: { milestone: '16.5' }
+ alpha: { milestone: '16.5' },
+ calls_gitaly: true
field :mergeable_discussions_state, GraphQL::Types::Boolean, null: true,
calls_gitaly: true,
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
index 3af7140aed3..e1bd1f603ad 100644
--- a/app/graphql/types/mutation_type.rb
+++ b/app/graphql/types/mutation_type.rb
@@ -106,6 +106,7 @@ module Types
mount_mutation Mutations::Notes::Update::ImageDiffNote
mount_mutation Mutations::Notes::RepositionImageDiffNote
mount_mutation Mutations::Notes::Destroy
+ mount_mutation Mutations::Organizations::Create, alpha: { milestone: '16.6' }
mount_mutation Mutations::Projects::SyncFork, calls_gitaly: true, alpha: { milestone: '15.9' }
mount_mutation Mutations::Releases::Create
mount_mutation Mutations::Releases::Update
@@ -134,33 +135,36 @@ module Types
mount_mutation Mutations::DesignManagement::Move
mount_mutation Mutations::DesignManagement::Update
mount_mutation Mutations::ContainerExpirationPolicies::Update
+ mount_mutation Mutations::ContainerRegistry::Protection::Rule::Create, alpha: { milestone: '16.6' }
mount_mutation Mutations::ContainerRepositories::Destroy
mount_mutation Mutations::ContainerRepositories::DestroyTags
+ mount_mutation Mutations::Ci::Catalog::Resources::Create, alpha: { milestone: '15.11' }
+ mount_mutation Mutations::Ci::Catalog::Resources::Unpublish, alpha: { milestone: '16.6' }
+ mount_mutation Mutations::Ci::Job::Cancel
+ mount_mutation Mutations::Ci::Job::Play
+ mount_mutation Mutations::Ci::Job::Retry
+ mount_mutation Mutations::Ci::Job::ArtifactsDestroy
+ mount_mutation Mutations::Ci::Job::Unschedule
+ mount_mutation Mutations::Ci::JobTokenScope::AddProject
+ mount_mutation Mutations::Ci::JobArtifact::BulkDestroy, alpha: { milestone: '15.10' }
+ mount_mutation Mutations::Ci::JobArtifact::Destroy
+ mount_mutation Mutations::Ci::JobTokenScope::RemoveProject
mount_mutation Mutations::Ci::Pipeline::Cancel
mount_mutation Mutations::Ci::Pipeline::Destroy
mount_mutation Mutations::Ci::Pipeline::Retry
+ mount_mutation Mutations::Ci::PipelineSchedule::Create
mount_mutation Mutations::Ci::PipelineSchedule::Delete
- mount_mutation Mutations::Ci::PipelineSchedule::TakeOwnership
mount_mutation Mutations::Ci::PipelineSchedule::Play
- mount_mutation Mutations::Ci::PipelineSchedule::Create
+ mount_mutation Mutations::Ci::PipelineSchedule::TakeOwnership
mount_mutation Mutations::Ci::PipelineSchedule::Update
mount_mutation Mutations::Ci::PipelineTrigger::Create, alpha: { milestone: '16.3' }
- mount_mutation Mutations::Ci::PipelineTrigger::Update, alpha: { milestone: '16.3' }
mount_mutation Mutations::Ci::PipelineTrigger::Delete, alpha: { milestone: '16.3' }
+ mount_mutation Mutations::Ci::PipelineTrigger::Update, alpha: { milestone: '16.3' }
mount_mutation Mutations::Ci::ProjectCiCdSettingsUpdate
- mount_mutation Mutations::Ci::Job::ArtifactsDestroy
- mount_mutation Mutations::Ci::Job::Play
- mount_mutation Mutations::Ci::Job::Retry
- mount_mutation Mutations::Ci::Job::Cancel
- mount_mutation Mutations::Ci::Job::Unschedule
- mount_mutation Mutations::Ci::JobArtifact::Destroy
- mount_mutation Mutations::Ci::JobArtifact::BulkDestroy, alpha: { milestone: '15.10' }
- mount_mutation Mutations::Ci::JobTokenScope::AddProject
- mount_mutation Mutations::Ci::JobTokenScope::RemoveProject
+ mount_mutation Mutations::Ci::Runner::BulkDelete, alpha: { milestone: '15.3' }
mount_mutation Mutations::Ci::Runner::Create, alpha: { milestone: '15.10' }
- mount_mutation Mutations::Ci::Runner::Update
mount_mutation Mutations::Ci::Runner::Delete
- mount_mutation Mutations::Ci::Runner::BulkDelete, alpha: { milestone: '15.3' }
+ mount_mutation Mutations::Ci::Runner::Update
mount_mutation Mutations::Ci::RunnersRegistrationToken::Reset
mount_mutation Mutations::Namespace::PackageSettings::Update
mount_mutation Mutations::Groups::Update
@@ -171,6 +175,7 @@ module Types
extensions: [::Gitlab::Graphql::Limit::FieldCallCount => { limit: 1 }]
mount_mutation Mutations::Packages::DestroyFile
mount_mutation Mutations::Packages::Protection::Rule::Create, alpha: { milestone: '16.5' }
+ mount_mutation Mutations::Packages::Protection::Rule::Delete, alpha: { milestone: '16.6' }
mount_mutation Mutations::Packages::DestroyFiles
mount_mutation Mutations::Packages::Cleanup::Policy::Update
mount_mutation Mutations::Echo
diff --git a/app/graphql/types/namespace/package_settings_type.rb b/app/graphql/types/namespace/package_settings_type.rb
index 61240243b1f..6c6144f2357 100644
--- a/app/graphql/types/namespace/package_settings_type.rb
+++ b/app/graphql/types/namespace/package_settings_type.rb
@@ -20,21 +20,18 @@ module Types
field :maven_duplicates_allowed, GraphQL::Types::Boolean,
null: false,
description: 'Indicates whether duplicate Maven packages are allowed for this namespace.'
- field :nuget_duplicate_exception_regex, Types::UntrustedRegexp,
- null: true,
- description: 'When nuget_duplicates_allowed is false, you can publish duplicate packages with names that match this regex. Otherwise, this setting has no effect. ' \
- 'Error is raised if `nuget_duplicates_option` feature flag is disabled.'
- field :nuget_duplicates_allowed, GraphQL::Types::Boolean,
- null: false,
- description: 'Indicates whether duplicate NuGet packages are allowed for this namespace. ' \
- 'Error is raised if `nuget_duplicates_option` feature flag is disabled.'
-
field :maven_package_requests_forwarding, GraphQL::Types::Boolean,
null: true,
description: 'Indicates whether Maven package forwarding is allowed for this namespace.'
field :npm_package_requests_forwarding, GraphQL::Types::Boolean,
null: true,
description: 'Indicates whether npm package forwarding is allowed for this namespace.'
+ field :nuget_duplicate_exception_regex, Types::UntrustedRegexp,
+ null: true,
+ description: 'When nuget_duplicates_allowed is false, you can publish duplicate packages with names that match this regex. Otherwise, this setting has no effect. '
+ field :nuget_duplicates_allowed, GraphQL::Types::Boolean,
+ null: false,
+ description: 'Indicates whether duplicate NuGet packages are allowed for this namespace. '
field :pypi_package_requests_forwarding, GraphQL::Types::Boolean,
null: true,
description: 'Indicates whether PyPI package forwarding is allowed for this namespace.'
diff --git a/app/graphql/types/notes/noteable_interface.rb b/app/graphql/types/notes/noteable_interface.rb
index 9971511d6ce..7c75f213e24 100644
--- a/app/graphql/types/notes/noteable_interface.rb
+++ b/app/graphql/types/notes/noteable_interface.rb
@@ -21,6 +21,8 @@ module Types
Types::DesignManagement::DesignType
when ::AlertManagement::Alert
Types::AlertManagement::AlertType
+ when AbuseReport
+ Types::AbuseReportType
else
raise "Unknown GraphQL type for #{object}"
end
diff --git a/app/graphql/types/organizations/organization_type.rb b/app/graphql/types/organizations/organization_type.rb
index cae0ef2232e..e7ba8de527c 100644
--- a/app/graphql/types/organizations/organization_type.rb
+++ b/app/graphql/types/organizations/organization_type.rb
@@ -33,6 +33,10 @@ module Types
null: false,
description: 'Path of the organization.',
alpha: { milestone: '16.4' }
+ field :web_url, GraphQL::Types::String,
+ null: false,
+ description: 'Web URL of the organization.',
+ alpha: { milestone: '16.6' }
end
end
end
diff --git a/app/graphql/types/organizations/organization_user_badge_type.rb b/app/graphql/types/organizations/organization_user_badge_type.rb
new file mode 100644
index 00000000000..f4e18676dd1
--- /dev/null
+++ b/app/graphql/types/organizations/organization_user_badge_type.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module Types
+ module Organizations
+ # rubocop: disable Graphql/AuthorizeTypes -- Already authorized in parent OrganizationUserType.
+ class OrganizationUserBadgeType < BaseObject
+ graphql_name 'OrganizationUserBadge'
+ description 'An organization user badge.'
+
+ field :text,
+ GraphQL::Types::String,
+ null: false,
+ description: 'Badge text.'
+
+ field :variant,
+ GraphQL::Types::String,
+ null: false,
+ description: 'Badge variant.'
+ end
+ # rubocop: enable Graphql/AuthorizeTypes
+ end
+end
diff --git a/app/graphql/types/organizations/organization_user_type.rb b/app/graphql/types/organizations/organization_user_type.rb
index 41924586f38..ce036c7dd4a 100644
--- a/app/graphql/types/organizations/organization_user_type.rb
+++ b/app/graphql/types/organizations/organization_user_type.rb
@@ -13,7 +13,7 @@ module Types
alias_method :organization_user, :object
field :badges,
- [GraphQL::Types::String],
+ [::Types::Organizations::OrganizationUserBadgeType],
null: true,
description: 'Badges describing the user within the organization.',
alpha: { milestone: '16.4' }
@@ -29,7 +29,7 @@ module Types
alpha: { milestone: '16.4' }
def badges
- user_badges_in_admin_section(organization_user.user).pluck(:text) # rubocop:disable CodeReuse/ActiveRecord
+ user_badges_in_admin_section(organization_user.user)
end
end
end
diff --git a/app/graphql/types/packages/package_base_type.rb b/app/graphql/types/packages/package_base_type.rb
index aa580d48709..5102e4ebcd5 100644
--- a/app/graphql/types/packages/package_base_type.rb
+++ b/app/graphql/types/packages/package_base_type.rb
@@ -10,11 +10,19 @@ module Types
authorize :read_package
+ expose_permissions Types::PermissionTypes::Package
+
field :id, ::Types::GlobalIDType[::Packages::Package], null: false, description: 'ID of the package.'
field :_links, Types::Packages::PackageLinksType, null: false, method: :itself,
description: 'Map of links to perform actions on the package.'
- field :can_destroy, GraphQL::Types::Boolean, null: false, description: 'Whether the user can destroy the package.'
+ field :can_destroy, GraphQL::Types::Boolean,
+ null: false,
+ deprecated: {
+ reason: 'Superseded by `user_permissions` field. See `Types::PermissionTypes::Package` type',
+ milestone: '16.6'
+ },
+ description: 'Whether the user can destroy the package.'
field :created_at, Types::TimeType, null: false, description: 'Date of creation.'
field :metadata, Types::Packages::MetadataType,
null: true,
diff --git a/app/graphql/types/packages/protection/rule_type.rb b/app/graphql/types/packages/protection/rule_type.rb
index 1e969d39ce2..e2ea2d89d2d 100644
--- a/app/graphql/types/packages/protection/rule_type.rb
+++ b/app/graphql/types/packages/protection/rule_type.rb
@@ -10,6 +10,11 @@ module Types
authorize :admin_package
+ field :id,
+ ::Types::GlobalIDType[::Packages::Protection::Rule],
+ null: false,
+ description: 'ID of the package protection rule.'
+
field :package_name_pattern,
GraphQL::Types::String,
null: false,
diff --git a/app/graphql/types/packages/pypi/metadatum_type.rb b/app/graphql/types/packages/pypi/metadatum_type.rb
index 63452d8ab6e..8ccdb592c52 100644
--- a/app/graphql/types/packages/pypi/metadatum_type.rb
+++ b/app/graphql/types/packages/pypi/metadatum_type.rb
@@ -9,8 +9,17 @@ module Types
authorize :read_package
+ field :author_email, GraphQL::Types::String, null: true,
+ description: 'Author email address(es) in RFC-822 format.'
+ field :description, GraphQL::Types::String, null: true,
+ description: 'Longer description that can run to several paragraphs.'
+ field :description_content_type, GraphQL::Types::String, null: true,
+ description: 'Markup syntax used in the description field.'
field :id, ::Types::GlobalIDType[::Packages::Pypi::Metadatum], null: false, description: 'ID of the metadatum.'
+ field :keywords, GraphQL::Types::String, null: true, description: 'List of keywords, separated by commas.'
+ field :metadata_version, GraphQL::Types::String, null: true, description: 'Metadata version.'
field :required_python, GraphQL::Types::String, null: true, description: 'Required Python version of the Pypi package.'
+ field :summary, GraphQL::Types::String, null: true, description: 'One-line summary of the description.'
end
end
end
diff --git a/app/graphql/types/permission_types/abuse_report.rb b/app/graphql/types/permission_types/abuse_report.rb
new file mode 100644
index 00000000000..abd5d545d02
--- /dev/null
+++ b/app/graphql/types/permission_types/abuse_report.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Types
+ module PermissionTypes
+ class AbuseReport < BasePermissionType
+ graphql_name 'AbuseReportPermissions'
+
+ abilities :read_abuse_report, :create_note
+ end
+ end
+end
diff --git a/app/graphql/types/permission_types/base_permission_type.rb b/app/graphql/types/permission_types/base_permission_type.rb
index d45c61f489b..3c0e68bdaf2 100644
--- a/app/graphql/types/permission_types/base_permission_type.rb
+++ b/app/graphql/types/permission_types/base_permission_type.rb
@@ -21,7 +21,7 @@ module Types
kword_args = kword_args.reverse_merge(
name: name,
type: GraphQL::Types::Boolean,
- description: "Indicates the user can perform `#{name}` on this resource",
+ description: "If `true`, the user can perform `#{name}` on this resource",
null: false)
field(**kword_args, &block) # rubocop:disable Graphql/Descriptions
diff --git a/app/graphql/types/permission_types/ci/job.rb b/app/graphql/types/permission_types/ci/job.rb
index c9a85317e67..35904fb1fc3 100644
--- a/app/graphql/types/permission_types/ci/job.rb
+++ b/app/graphql/types/permission_types/ci/job.rb
@@ -8,6 +8,7 @@ module Types
abilities :read_job_artifacts, :read_build
ability_field :update_build, calls_gitaly: true
+ ability_field :cancel_build, calls_gitaly: true
end
end
end
diff --git a/app/graphql/types/permission_types/ci/pipeline.rb b/app/graphql/types/permission_types/ci/pipeline.rb
index cfd68380005..94adbf7c59b 100644
--- a/app/graphql/types/permission_types/ci/pipeline.rb
+++ b/app/graphql/types/permission_types/ci/pipeline.rb
@@ -8,6 +8,7 @@ module Types
abilities :admin_pipeline, :destroy_pipeline
ability_field :update_pipeline, calls_gitaly: true
+ ability_field :cancel_pipeline, calls_gitaly: true
end
end
end
diff --git a/app/graphql/types/permission_types/package.rb b/app/graphql/types/permission_types/package.rb
new file mode 100644
index 00000000000..debde3a1a8e
--- /dev/null
+++ b/app/graphql/types/permission_types/package.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module Types
+ module PermissionTypes
+ class Package < BasePermissionType
+ graphql_name 'PackagePermissions'
+
+ ability_field :destroy_package,
+ description: 'If `true`, the user can perform `destroy_package` on this resource'
+ end
+ end
+end
diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb
index 95caefc3825..ec87f133843 100644
--- a/app/graphql/types/project_type.rb
+++ b/app/graphql/types/project_type.rb
@@ -641,6 +641,12 @@ module Types
resolver: Resolvers::AutocompleteUsersResolver,
description: 'Search users for autocompletion'
+ field :detailed_import_status,
+ ::Types::Projects::DetailedImportStatusType,
+ null: true,
+ description: 'Detailed import status of the project.',
+ method: :import_state
+
def timelog_categories
object.project_namespace.timelog_categories if Feature.enabled?(:timelog_categories)
end
diff --git a/app/graphql/types/projects/detailed_import_status_type.rb b/app/graphql/types/projects/detailed_import_status_type.rb
new file mode 100644
index 00000000000..9cba176e097
--- /dev/null
+++ b/app/graphql/types/projects/detailed_import_status_type.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+module Types
+ module Projects
+ class DetailedImportStatusType < BaseObject
+ graphql_name 'DetailedImportStatus'
+ description 'Details of the import status of a project.'
+
+ authorize :read_project
+
+ field :id, ::Types::GlobalIDType[::ProjectImportState],
+ description: 'ID of the import state.'
+
+ field :status, GraphQL::Types::String,
+ description: 'Current status of the import.'
+
+ field :url, GraphQL::Types::String,
+ description: 'Import url.'
+
+ field :last_error, GraphQL::Types::String,
+ description: 'Last error of the import.',
+ null: true,
+ authorize: :read_import_error
+
+ field :last_update_at, Types::TimeType,
+ description: 'Time of the last update.'
+
+ field :last_update_started_at, Types::TimeType,
+ description: 'Time of the start of the last update.'
+
+ field :last_successful_update_at, Types::TimeType,
+ description: 'Time of the last successful update.'
+
+ def url
+ object.project.safe_import_url
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb
index d185007f05b..173e877d86c 100644
--- a/app/graphql/types/query_type.rb
+++ b/app/graphql/types/query_type.rb
@@ -21,6 +21,20 @@ module Types
required: true, description: 'Global ID of the CI stage.'
end
+ field :ci_catalog_resources,
+ ::Types::Ci::Catalog::ResourceType.connection_type,
+ null: true,
+ alpha: { milestone: '15.11' },
+ description: 'All CI/CD Catalog resources under a common namespace, visible to an authorized user',
+ resolver: ::Resolvers::Ci::Catalog::ResourcesResolver
+
+ field :ci_catalog_resource,
+ ::Types::Ci::Catalog::ResourceType,
+ null: true,
+ alpha: { milestone: '16.1' },
+ description: 'A single CI/CD Catalog resource visible to an authorized user',
+ resolver: ::Resolvers::Ci::Catalog::ResourceResolver
+
field :ci_variables,
Types::Ci::InstanceVariableType.connection_type,
null: true,
@@ -41,6 +55,14 @@ module Types
null: false,
description: 'Fields related to design management.'
field :echo, resolver: Resolvers::EchoResolver
+ field :frecent_groups, [Types::GroupType],
+ resolver: Resolvers::Users::FrecentGroupsResolver,
+ description: "A user's frecently visited groups. Requires the `frecent_namespaces_suggestions` feature flag to be enabled.",
+ alpha: { milestone: '16.6' }
+ field :frecent_projects, [Types::ProjectType],
+ resolver: Resolvers::Users::FrecentProjectsResolver,
+ description: "A user's frecently visited projects. Requires the `frecent_namespaces_suggestions` feature flag to be enabled.",
+ alpha: { milestone: '16.6' }
field :gitpod_enabled, GraphQL::Types::Boolean,
null: true,
description: "Whether Gitpod is enabled in application settings."
diff --git a/app/graphql/types/security/codequality_reports_comparer/degradation_type.rb b/app/graphql/types/security/codequality_reports_comparer/degradation_type.rb
index fb7d722069f..7dd47611a2e 100644
--- a/app/graphql/types/security/codequality_reports_comparer/degradation_type.rb
+++ b/app/graphql/types/security/codequality_reports_comparer/degradation_type.rb
@@ -3,10 +3,9 @@
module Types
module Security
module CodequalityReportsComparer
- # rubocop: disable Graphql/AuthorizeTypes (The resolver authorizes the request)
+ # rubocop: disable Graphql/AuthorizeTypes -- The resolver authorizes the request
class DegradationType < BaseObject
graphql_name 'CodequalityReportsComparerReportDegradation'
-
description 'Represents a degradation on the compared codequality report.'
field :description, GraphQL::Types::String,
diff --git a/app/graphql/types/security/codequality_reports_comparer/report_generation_status_enum.rb b/app/graphql/types/security/codequality_reports_comparer/report_generation_status_enum.rb
new file mode 100644
index 00000000000..dace3aec97c
--- /dev/null
+++ b/app/graphql/types/security/codequality_reports_comparer/report_generation_status_enum.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Types
+ module Security
+ module CodequalityReportsComparer
+ class ReportGenerationStatusEnum < BaseEnum
+ graphql_name 'CodequalityReportsComparerReportGenerationStatus'
+ description 'Represents the generation status of the compared codequality report.'
+
+ value 'PARSED', value: :parsed, description: 'Report was generated.'
+ value 'PARSING', value: :parsing, description: 'Report is being generated.'
+ value 'ERROR', value: :error, description: 'An error happened while generating the report.'
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/security/codequality_reports_comparer/report_type.rb b/app/graphql/types/security/codequality_reports_comparer/report_type.rb
index 8a41160141a..d20c9dd9ab6 100644
--- a/app/graphql/types/security/codequality_reports_comparer/report_type.rb
+++ b/app/graphql/types/security/codequality_reports_comparer/report_type.rb
@@ -3,7 +3,7 @@
module Types
module Security
module CodequalityReportsComparer
- # rubocop: disable Graphql/AuthorizeTypes (Parent node applies authorization)
+ # rubocop: disable Graphql/AuthorizeTypes -- Parent node applies authorization
class ReportType < BaseObject
graphql_name 'CodequalityReportsComparerReport'
diff --git a/app/graphql/types/security/codequality_reports_comparer/status_enum.rb b/app/graphql/types/security/codequality_reports_comparer/status_enum.rb
index 9cab2664db8..fdccfdc7e44 100644
--- a/app/graphql/types/security/codequality_reports_comparer/status_enum.rb
+++ b/app/graphql/types/security/codequality_reports_comparer/status_enum.rb
@@ -4,11 +4,11 @@ module Types
module Security
module CodequalityReportsComparer
class StatusEnum < BaseEnum
- graphql_name 'CodequalityReportsComparerReportStatus'
- description 'Report comparison status'
+ graphql_name 'CodequalityReportsComparerStatus'
+ description 'Represents the state of the code quality report.'
- value 'SUCCESS', value: 'success', description: 'Report successfully generated.'
- value 'FAILED', value: 'failed', description: 'Report failed to generate.'
+ value 'SUCCESS', value: 'success', description: 'No degradations found in the head pipeline report.'
+ value 'FAILED', value: 'failed', description: 'Report generated and there are new code quality degradations.'
value 'NOT_FOUND', value: 'not_found', description: 'Head report or base report not found.'
end
end
diff --git a/app/graphql/types/security/codequality_reports_comparer/summary_type.rb b/app/graphql/types/security/codequality_reports_comparer/summary_type.rb
index cd4a594c193..43037be5245 100644
--- a/app/graphql/types/security/codequality_reports_comparer/summary_type.rb
+++ b/app/graphql/types/security/codequality_reports_comparer/summary_type.rb
@@ -3,7 +3,7 @@
module Types
module Security
module CodequalityReportsComparer
- # rubocop: disable Graphql/AuthorizeTypes (The resolver authorizes the request)
+ # rubocop: disable Graphql/AuthorizeTypes -- The resolver authorizes the request
class SummaryType < BaseObject
graphql_name 'CodequalityReportsComparerReportSummary'
diff --git a/app/graphql/types/security/codequality_reports_comparer_type.rb b/app/graphql/types/security/codequality_reports_comparer_type.rb
index 8088bf84627..32fe8c12330 100644
--- a/app/graphql/types/security/codequality_reports_comparer_type.rb
+++ b/app/graphql/types/security/codequality_reports_comparer_type.rb
@@ -2,12 +2,17 @@
module Types
module Security
- # rubocop: disable Graphql/AuthorizeTypes (The resolver authorizes the request)
+ # rubocop: disable Graphql/AuthorizeTypes -- The resolver authorizes the request
class CodequalityReportsComparerType < BaseObject
graphql_name 'CodequalityReportsComparer'
description 'Represents reports comparison for code quality.'
+ field :status,
+ type: CodequalityReportsComparer::ReportGenerationStatusEnum,
+ null: true,
+ description: 'Compared codequality report generation status.'
+
field :report,
type: CodequalityReportsComparer::ReportType,
null: true,
diff --git a/app/graphql/types/user_interface.rb b/app/graphql/types/user_interface.rb
index 47d486265b0..040711b5f58 100644
--- a/app/graphql/types/user_interface.rb
+++ b/app/graphql/types/user_interface.rb
@@ -71,6 +71,11 @@ module Types
type: GraphQL::Types::String,
null: false,
description: 'Web path of the user.'
+ field :organizations,
+ resolver: Resolvers::Users::OrganizationsResolver,
+ null: true,
+ alpha: { milestone: '16.6' },
+ description: 'Organizations where the user has access.'
field :group_memberships,
type: Types::GroupMemberType.connection_type,
null: true,
@@ -134,13 +139,11 @@ module Types
field :saved_replies,
Types::SavedReplyType.connection_type,
null: true,
- description: 'Saved replies authored by the user. ' \
- 'Will not return saved replies if `saved_replies` feature flag is disabled.'
+ description: 'Saved replies authored by the user.'
field :saved_reply,
resolver: Resolvers::SavedReplyResolver,
- description: 'Saved reply authored by the user. ' \
- 'Will not return saved reply if `saved_replies` feature flag is disabled.'
+ description: 'Saved reply authored by the user.'
field :gitpod_enabled, GraphQL::Types::Boolean, null: true,
description: 'Whether Gitpod is enabled at the user level.'
@@ -197,6 +200,11 @@ module Types
null: true,
description: 'Timestamp of when the user was created.'
+ field :last_activity_on,
+ type: Types::DateType,
+ null: true,
+ description: 'Date the user last performed any actions.'
+
field :pronouns,
type: ::GraphQL::Types::String,
null: true,
diff --git a/app/graphql/types/work_items/linked_item_type.rb b/app/graphql/types/work_items/linked_item_type.rb
index a4dbeed7480..1b989d78091 100644
--- a/app/graphql/types/work_items/linked_item_type.rb
+++ b/app/graphql/types/work_items/linked_item_type.rb
@@ -2,21 +2,29 @@
module Types
module WorkItems
- # rubocop:disable Graphql/AuthorizeTypes
class LinkedItemType < BaseObject
graphql_name 'LinkedWorkItemType'
+ authorize :read_work_item
+
field :link_created_at, Types::TimeType,
- description: 'Timestamp the link was created.', null: false
+ description: 'Timestamp the link was created.', null: false,
+ method: :issue_link_created_at
field :link_id, ::Types::GlobalIDType[::WorkItems::RelatedWorkItemLink],
- description: 'Global ID of the link.', null: false
+ description: 'Global ID of the link.', null: false,
+ method: :issue_link_id
field :link_type, GraphQL::Types::String,
- description: 'Type of link.', null: false
+ description: 'Type of link.', null: false,
+ method: :issue_link_type
field :link_updated_at, Types::TimeType,
- description: 'Timestamp the link was updated.', null: false
+ description: 'Timestamp the link was updated.', null: false,
+ method: :issue_link_updated_at
field :work_item, Types::WorkItemType,
- description: 'Linked work item.', null: false
+ description: 'Linked work item.', null: true
+
+ def work_item
+ object
+ end
end
- # rubocop:enable Graphql/AuthorizeTypes
end
end
diff --git a/app/graphql/types/work_items/widgets/linked_items_type.rb b/app/graphql/types/work_items/widgets/linked_items_type.rb
index 2611c2456c5..c541a12a050 100644
--- a/app/graphql/types/work_items/widgets/linked_items_type.rb
+++ b/app/graphql/types/work_items/widgets/linked_items_type.rb
@@ -13,6 +13,7 @@ module Types
field :linked_items, Types::WorkItems::LinkedItemType.connection_type,
null: true, complexity: 5,
alpha: { milestone: '16.3' },
+ extras: [:lookahead],
description: 'Linked items for the work item. Returns `null` ' \
'if `linked_work_items` feature flag is disabled.',
resolver: Resolvers::WorkItems::LinkedItemsResolver
diff --git a/app/helpers/admin/user_actions_helper.rb b/app/helpers/admin/user_actions_helper.rb
index 969c5d5a0b5..ba40b3c8a8d 100644
--- a/app/helpers/admin/user_actions_helper.rb
+++ b/app/helpers/admin/user_actions_helper.rb
@@ -16,6 +16,7 @@ module Admin
unlock_actions
delete_actions
ban_actions
+ trust_actions
@actions
end
@@ -66,5 +67,19 @@ module Admin
@actions << 'ban'
end
end
+
+ def trust_actions
+ return if @user.internal? ||
+ @user.blocked_pending_approval? ||
+ @user.banned? ||
+ @user.blocked? ||
+ @user.deactivated?
+
+ @actions << if @user.trusted?
+ 'untrust'
+ else
+ 'trust'
+ end
+ end
end
end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 57937353955..8a0a46e6b25 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -318,7 +318,6 @@ module ApplicationHelper
class_names << 'with-header' if !show_super_sidebar? || !current_user
class_names << 'with-top-bar' if show_super_sidebar? && !@hide_top_bar_padding
class_names << system_message_class
- class_names << 'logged-out-marketing-header' if !current_user && ::Gitlab.com? && !show_super_sidebar?
class_names
end
@@ -371,6 +370,14 @@ module ApplicationHelper
"https://discord.com/users/#{user.discord}"
end
+ def mastodon_url(user)
+ return '' if user.mastodon.blank?
+
+ url = user.mastodon.match UserDetail::MASTODON_VALIDATION_REGEX
+
+ external_redirect_path(url: "https://#{url[2]}/@#{url[1]}")
+ end
+
def collapsed_sidebar?
cookies["sidebar_collapsed"] == "true"
end
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 58648a82487..0c6ab41004a 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -488,6 +488,7 @@ module ApplicationSettingsHelper
:sidekiq_job_limiter_compression_threshold_bytes,
:sidekiq_job_limiter_limit_bytes,
:suggest_pipeline_enabled,
+ :enable_artifact_external_redirect_warning_page,
:search_rate_limit,
:search_rate_limit_unauthenticated,
:search_rate_limit_allowlist_raw,
diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb
index fc157df3891..e447940e2af 100644
--- a/app/helpers/auth_helper.rb
+++ b/app/helpers/auth_helper.rb
@@ -93,16 +93,11 @@ module AuthHelper
end
def saml_providers
- auth_providers.select do |provider|
- provider == :saml || auth_strategy_class(provider) == 'OmniAuth::Strategies::SAML'
+ providers = Gitlab.config.omniauth.providers.select do |provider|
+ provider.name == 'saml' || provider.dig('args', 'strategy_class') == 'OmniAuth::Strategies::SAML'
end
- end
-
- def auth_strategy_class(provider)
- config = Gitlab::Auth::OAuth::Provider.config_for(provider)
- return if config.nil? || config['args'].blank?
- config.args['strategy_class']
+ providers.map(&:name).map(&:to_sym)
end
def any_form_based_providers_enabled?
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 0d5b8755a37..8c199aefd81 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -300,7 +300,7 @@ module BlobHelper
end
def show_suggest_pipeline_creation_celebration?
- @blob.path == Gitlab::FileDetector::PATTERNS[:gitlab_ci] &&
+ Gitlab::FileDetector.type_of(@blob.path) == :gitlab_ci &&
@blob.auxiliary_viewer&.valid?(project: @project, sha: @commit.sha, user: current_user) &&
@project.uses_default_ci_config? &&
cookies[suggest_pipeline_commit_cookie_name].present?
diff --git a/app/helpers/ci/catalog/resources_helper.rb b/app/helpers/ci/catalog/resources_helper.rb
index bc77e0cd33a..8324da870d3 100644
--- a/app/helpers/ci/catalog/resources_helper.rb
+++ b/app/helpers/ci/catalog/resources_helper.rb
@@ -3,8 +3,8 @@
module Ci
module Catalog
module ResourcesHelper
- def can_add_catalog_resource?(_project)
- false
+ def can_add_catalog_resource?(project)
+ can?(current_user, :add_catalog_resource, project)
end
def can_view_namespace_catalog?(_project)
diff --git a/app/helpers/ci/pipelines_helper.rb b/app/helpers/ci/pipelines_helper.rb
index 510c7cd5fb6..9c4ceaccff1 100644
--- a/app/helpers/ci/pipelines_helper.rb
+++ b/app/helpers/ci/pipelines_helper.rb
@@ -71,7 +71,7 @@ module Ci
def pipelines_list_data(project, list_url)
artifacts_endpoint_placeholder = ':pipeline_artifacts_id'
- data = {
+ {
endpoint: list_url,
project_id: project.id,
default_branch_name: project.default_branch,
@@ -89,15 +89,6 @@ module Ci
full_path: project.full_path,
visibility_pipeline_id_type: visibility_pipeline_id_type
}
-
- experiment(:ios_specific_templates, actor: current_user, project: project, sticky_to: project) do |e|
- e.candidate do
- data[:registration_token] = project.runners_token if can?(current_user, :register_project_runners, project)
- data[:ios_runners_available] = (project.shared_runners_available? && Gitlab.com?).to_s
- end
- end
-
- data
end
def visibility_pipeline_id_type
diff --git a/app/helpers/ci/status_helper.rb b/app/helpers/ci/status_helper.rb
index 86f48b51f76..21d982d42bc 100644
--- a/app/helpers/ci/status_helper.rb
+++ b/app/helpers/ci/status_helper.rb
@@ -15,50 +15,46 @@ module Ci
end
# rubocop:disable Metrics/CyclomaticComplexity
- def ci_icon_for_status(status, size: 16)
- if detailed_status?(status)
- return sprite_icon(status.icon, size: size)
- end
-
+ def ci_icon_for_status(status, size: 24)
icon_name =
- case status
- when 'success'
- 'status_success'
- when 'success-with-warnings'
- 'status_warning'
- when 'failed'
- 'status_failed'
- when 'pending'
- 'status_pending'
- when 'waiting_for_resource'
- 'status_pending'
- when 'preparing'
- 'status_preparing'
- when 'running'
- 'status_running'
- when 'play'
- 'play'
- when 'created'
- 'status_created'
- when 'skipped'
- 'status_skipped'
- when 'manual'
- 'status_manual'
- when 'scheduled'
- 'status_scheduled'
+ if detailed_status?(status)
+ status.icon
else
- 'status_canceled'
+ case status
+ when 'success'
+ 'status_success'
+ when 'success-with-warnings'
+ 'status_warning'
+ when 'failed'
+ 'status_failed'
+ when 'pending'
+ 'status_pending'
+ when 'waiting-for-resource'
+ 'status_pending'
+ when 'preparing'
+ 'status_preparing'
+ when 'running'
+ 'status_running'
+ when 'play'
+ 'play'
+ when 'created'
+ 'status_created'
+ when 'skipped'
+ 'status_skipped'
+ when 'manual'
+ 'status_manual'
+ when 'scheduled'
+ 'status_scheduled'
+ else
+ 'status_canceled'
+ end
end
- sprite_icon(icon_name, size: size)
- end
- # rubocop:enable Metrics/CyclomaticComplexity
-
- def ci_icon_class_for_status(status)
- group = detailed_status?(status) ? status.group : status.dasherize
+ icon_name = icon_name == 'play' ? icon_name : "#{icon_name}_borderless"
- "ci-status-icon-#{group}"
+ sprite_icon(icon_name, size: size, css_class: 'gl-icon')
end
+ # rubocop:enable Metrics/CyclomaticComplexity
def pipeline_status_cache_key(pipeline_status)
"pipeline-status/#{pipeline_status.sha}-#{pipeline_status.status}"
@@ -68,23 +64,35 @@ module Ci
project = commit.project
path = pipelines_project_commit_path(project, commit, ref: ref)
- render_status_with_link(
+ render_ci_icon(
status,
path,
tooltip_placement: tooltip_placement,
- icon_size: 16)
+ option_css_classes: 'gl-ml-3'
+ )
end
- def render_status_with_link(status, path = nil, type: _('pipeline'), tooltip_placement: 'left', cssclass: '', container: 'body', icon_size: 16)
+ def render_ci_icon(
+ status,
+ path = nil,
+ tooltip_placement: 'left',
+ option_css_classes: '',
+ container: 'body',
+ show_status_text: false
+ )
variant = badge_variant(status)
- klass = "ci-status-link #{ci_icon_class_for_status(status)} d-inline-flex gl-line-height-1 #{cssclass}"
- title = "#{type.titleize}: #{ci_label_for_status(status)}"
- data = { toggle: 'tooltip', placement: tooltip_placement, container: container, testid: 'ci-status-badge-legacy' }
- badge_classes = 'gl-px-2 gl-ml-3'
+ badge_classes = "ci-icon ci-icon-variant-#{variant} gl-p-2 #{option_css_classes}"
+ title = "#{_('Pipeline')}: #{ci_label_for_status(status)}"
+ data = { toggle: 'tooltip', placement: tooltip_placement, container: container, testid: 'ci-icon' }
+
+ icon_wrapper_class = "js-ci-status-badge-legacy ci-icon-gl-icon-wrapper"
gl_badge_tag(variant: variant, size: :md, href: path, class: badge_classes, title: title, data: data) do
- content_tag :span, ci_icon_for_status(status, size: icon_size),
- class: klass
+ if show_status_text
+ content_tag(:span, ci_icon_for_status(status), { class: icon_wrapper_class }) + content_tag(:span, status.label, { class: 'gl-mx-2 gl-white-space-nowrap', data: { testid: 'ci-icon-text' } })
+ else
+ content_tag(:span, ci_icon_for_status(status), { class: icon_wrapper_class })
+ end
end
end
@@ -124,16 +132,18 @@ module Ci
case variant
when 'success'
:success
- when 'success-with-warnings', 'pending'
+ when 'success-with-warnings'
+ :warning
+ when 'pending'
+ :warning
+ when 'waiting-for-resource'
:warning
when 'failed'
:danger
when 'running'
:info
- when 'canceled', 'manual'
- :neutral
else
- :muted
+ :neutral
end
end
end
diff --git a/app/helpers/clusters_helper.rb b/app/helpers/clusters_helper.rb
index 1989d6ab3d5..319cec6f140 100644
--- a/app/helpers/clusters_helper.rb
+++ b/app/helpers/clusters_helper.rb
@@ -38,7 +38,7 @@ module ClustersHelper
environment_scope: cluster.environment_scope,
base_domain: cluster.base_domain,
auto_devops_help_path: help_page_path('topics/autodevops/index'),
- external_endpoint_help_path: help_page_path('user/project/clusters/gitlab_managed_clusters.md', anchor: 'base-domain')
+ external_endpoint_help_path: help_page_path('user/project/clusters/gitlab_managed_clusters', anchor: 'base-domain')
}
end
diff --git a/app/helpers/colors_helper.rb b/app/helpers/colors_helper.rb
index 3cd7263c39e..34b18b80be4 100644
--- a/app/helpers/colors_helper.rb
+++ b/app/helpers/colors_helper.rb
@@ -10,16 +10,4 @@ module ColorsHelper
hex_color.length == 7 ? hex_color[1, 7].scan(/.{2}/).map(&:hex) : hex_color[1, 4].scan(/./).map { |v| (v * 2).hex }
end
-
- def rgb_array_to_hex_color(rgb_array)
- raise ArgumentError, "invalid RGB array `#{rgb_array}`" unless rgb_array_valid?(rgb_array)
-
- "##{rgb_array.map{ "%02x" % _1 }.join}"
- end
-
- private
-
- def rgb_array_valid?(rgb_array)
- rgb_array.is_a?(Array) && rgb_array.length == 3 && rgb_array.all?{ _1 >= 0 && _1 <= 255 }
- end
end
diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb
index cc91b70758f..b6e0b2d6b20 100644
--- a/app/helpers/dropdowns_helper.rb
+++ b/app/helpers/dropdowns_helper.rb
@@ -110,7 +110,7 @@ module DropdownsHelper
def dropdown_filter(placeholder, search_id: nil)
content_tag :div, class: "dropdown-input" do
- filter_output = search_field_tag search_id, nil, data: { qa_selector: "dropdown_input_field" }, id: nil, class: "dropdown-input-field", placeholder: placeholder, autocomplete: 'off'
+ filter_output = search_field_tag search_id, nil, data: { testid: "dropdown-input-field" }, id: nil, class: "dropdown-input-field", placeholder: placeholder, autocomplete: 'off'
filter_output << sprite_icon('search', css_class: 'dropdown-input-search')
filter_output << sprite_icon('close', size: 16, css_class: 'dropdown-input-clear js-dropdown-input-clear')
diff --git a/app/helpers/environment_helper.rb b/app/helpers/environment_helper.rb
index 6e9379a5926..fa47a12a72c 100644
--- a/app/helpers/environment_helper.rb
+++ b/app/helpers/environment_helper.rb
@@ -9,15 +9,6 @@ module EnvironmentHelper
end
# rubocop: enable CodeReuse/ActiveRecord
- def environment_link_for_build(project, build)
- environment = environment_for_build(project, build)
- if environment
- link_to environment.name, project_environment_path(project, environment)
- else
- content_tag :span, build.expanded_environment_name
- end
- end
-
def deployment_path(deployment)
[deployment.project, deployment.deployable]
end
@@ -30,45 +21,6 @@ module EnvironmentHelper
link_to link_label, deployment_path(deployment)
end
- def last_deployment_link_for_environment_build(project, build)
- environment = environment_for_build(project, build)
- return unless environment
-
- deployment_link(environment.last_deployment)
- end
-
- def render_deployment_status(deployment)
- status = deployment.status
-
- status_text =
- case status
- when 'created'
- s_('Deployment|created')
- when 'running'
- s_('Deployment|running')
- when 'success'
- s_('Deployment|success')
- when 'failed'
- s_('Deployment|failed')
- when 'canceled'
- s_('Deployment|canceled')
- when 'skipped'
- s_('Deployment|skipped')
- when 'blocked'
- s_('Deployment|blocked')
- end
-
- ci_icon_utilities = "gl-display-inline-flex gl-align-items-center gl-line-height-0 gl-px-3 gl-py-2 gl-rounded-base"
- klass = "ci-status ci-#{status.dasherize} #{ci_icon_utilities}"
- text = "#{ci_icon_for_status(status)} <span class=\"gl-ml-2\">#{status_text}</span>".html_safe
-
- if deployment.deployable.instance_of?(::Ci::Build)
- link_to(text, deployment_path(deployment), class: klass)
- else
- content_tag(:span, text, class: klass)
- end
- end
-
def environments_detail_data(user, project, environment)
{
name: environment.name,
diff --git a/app/helpers/environments_helper.rb b/app/helpers/environments_helper.rb
index 80a56493653..28bdd3e69b6 100644
--- a/app/helpers/environments_helper.rb
+++ b/app/helpers/environments_helper.rb
@@ -33,15 +33,6 @@ module EnvironmentsHelper
metrics_data
end
- def environment_logs_data(project, environment)
- {
- "environment_name": environment.name,
- "environments_path": api_v4_projects_environments_path(id: project.id),
- "environment_id": environment.id,
- "clusters_path": project_clusters_path(project, format: :json)
- }
- end
-
def can_destroy_environment?(environment)
can?(current_user, :destroy_environment, environment)
end
@@ -85,8 +76,8 @@ module EnvironmentsHelper
def static_metrics_data
{
- 'documentation_path' => help_page_path('administration/monitoring/prometheus/index.md'),
- 'add_dashboard_documentation_path' => help_page_path('operations/metrics/dashboards/index.md', anchor: 'add-a-new-dashboard-to-your-project'),
+ 'documentation_path' => help_page_path('administration/monitoring/prometheus/index'),
+ 'add_dashboard_documentation_path' => help_page_path('operations/metrics/dashboards/index', anchor: 'add-a-new-dashboard-to-your-project'),
'empty_getting_started_svg_path' => image_path('illustrations/monitoring/getting_started.svg'),
'empty_loading_svg_path' => image_path('illustrations/monitoring/loading.svg'),
'empty_no_data_svg_path' => image_path('illustrations/monitoring/no_data.svg'),
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index 795d35ec81f..769af0d9ef9 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -13,7 +13,10 @@ module EventsHelper
'deleted' => 'remove',
'destroyed' => 'remove',
'imported' => 'import',
- 'joined' => 'users'
+ 'joined' => 'users',
+ 'approved' => 'check',
+ 'added' => 'upload',
+ 'removed' => 'remove'
}.freeze
def localized_action_name_map
@@ -70,7 +73,7 @@ module EventsHelper
if author
name = self_added ? _('You') : author.name
- link_to name, user_path(author.username), title: name
+ link_to name, user_path(author.username), title: name, data: { user_id: author.id, username: author.username }, class: 'js-user-link'
else
escape_once(event.author_name)
end
@@ -242,7 +245,7 @@ module EventsHelper
def event_wiki_title_html(event)
capture do
- concat content_tag(:span, _('wiki page'), class: "event-target-type gl-mr-2")
+ concat content_tag(:span, _('wiki page'), class: "event-target-type gl-mr-2 #{user_profile_activity_classes}")
concat link_to(
event.target_title,
event_wiki_page_target_url(event),
@@ -254,7 +257,7 @@ module EventsHelper
def event_design_title_html(event)
capture do
- concat content_tag(:span, _('design'), class: "event-target-type gl-mr-2")
+ concat content_tag(:span, _('design'), class: "event-target-type gl-mr-2 #{user_profile_activity_classes}")
concat link_to(
event.design.reference_link_text,
design_url(event.design),
@@ -271,7 +274,7 @@ module EventsHelper
def event_note_title_html(event)
if event.note_target
capture do
- concat content_tag(:span, event.note_target_type_name, class: "event-target-type gl-mr-2")
+ concat content_tag(:span, event.note_target_type_name, class: "event-target-type gl-mr-2 #{user_profile_activity_classes}")
concat link_to(event.note_target_reference, event_note_target_url(event), title: event.target_title, class: 'has-tooltip event-target-link gl-mr-2')
end
else
@@ -303,19 +306,16 @@ module EventsHelper
end
def icon_for_profile_event(event)
- if current_path?('users#show')
- content_tag :div, class: "system-note-image #{event.action_name.parameterize}-icon" do
- icon_for_event(event.action_name)
- end
- else
- content_tag :div, class: 'system-note-image user-avatar' do
- author_avatar(event, size: 32)
- end
- end
+ base_class = 'system-note-image'
+
+ classes = current_path?('users#activity') ? "#{event.action_name.parameterize}-icon gl-rounded-full gl-bg-gray-50 gl-line-height-0" : "user-avatar"
+ content = current_path?('users#activity') ? icon_for_event(event.action_name, size: 14) : author_avatar(event, size: 32)
+
+ tag.div(class: "#{base_class} #{classes}") { content }
end
def inline_event_icon(event)
- unless current_path?('users#show')
+ unless current_path?('users#activity')
content_tag :span, class: "system-note-image-inline d-none d-sm-flex gl-mr-2 #{event.action_name.parameterize}-icon align-self-center" do
next design_event_icon(event.action, size: 14) if event.design?
@@ -325,13 +325,19 @@ module EventsHelper
end
def event_user_info(event)
- content_tag(:div, class: "event-user-info") do
- concat content_tag(:span, link_to_author(event), class: "author-name")
- concat "&nbsp;".html_safe
- concat content_tag(:span, event.author.to_reference, class: "username")
+ return if current_path?('users#activity')
+
+ tag.div(class: 'event-user-info') do
+ concat tag.span(link_to_author(event), class: 'author-name')
+ concat '&nbsp;'.html_safe
+ concat tag.span(event.author.to_reference, class: 'username')
end
end
+ def user_profile_activity_classes
+ current_path?('users#activity') ? ' gl-font-weight-semibold gl-text-black-normal' : ''
+ end
+
private
def design_url(design, opts = {})
diff --git a/app/helpers/graph_helper.rb b/app/helpers/graph_helper.rb
index ab72442857b..829e72d9055 100644
--- a/app/helpers/graph_helper.rb
+++ b/app/helpers/graph_helper.rb
@@ -4,12 +4,6 @@ module GraphHelper
def refs(repo, commit)
refs = [commit.ref_names(repo).join(' ')]
- # append note count
- unless Feature.enabled?(:disable_network_graph_notes_count, @project, type: :experiment)
- notes_count = @graph.notes[commit.id]
- refs << "[#{pluralize(notes_count, 'note')}]" if notes_count > 0
- end
-
refs.join
end
@@ -18,13 +12,6 @@ module GraphHelper
ids.zip(parent_spaces)
end
- def success_ratio(counts)
- return 100 if counts[:failed] == 0
-
- ratio = (counts[:success].to_f / (counts[:success] + counts[:failed])) * 100
- ratio.to_i
- end
-
def should_render_dora_charts
false
end
diff --git a/app/helpers/ide_helper.rb b/app/helpers/ide_helper.rb
index 2582d6fcc34..f2d393f1f77 100644
--- a/app/helpers/ide_helper.rb
+++ b/app/helpers/ide_helper.rb
@@ -5,7 +5,7 @@ module IdeHelper
def ide_data(project:, fork_info:, params:)
base_data = {
'use-new-web-ide' => use_new_web_ide?.to_s,
- 'new-web-ide-help-page-path' => help_page_path('user/project/web_ide/index.md', anchor: 'vscode-reimplementation'),
+ 'new-web-ide-help-page-path' => help_page_path('user/project/web_ide/index', anchor: 'vscode-reimplementation'),
'sign-in-path' => new_session_path(current_user),
'user-preferences-path' => profile_preferences_path
}.merge(use_new_web_ide? ? new_ide_data(project: project) : legacy_ide_data(project: project))
@@ -71,16 +71,16 @@ module IdeHelper
'switch-editor-svg-path': image_path('illustrations/rocket-launch-md.svg'),
'promotion-svg-path': image_path('illustrations/web-ide_promotion.svg'),
'ci-help-page-path' => help_page_path('ci/quick_start/index'),
- 'web-ide-help-page-path' => help_page_path('user/project/web_ide/index.md'),
+ 'web-ide-help-page-path' => help_page_path('user/project/web_ide/index'),
'render-whitespace-in-code': current_user.render_whitespace_in_code.to_s,
'default-branch' => project && project.default_branch,
'project' => convert_to_project_entity_json(project),
'enable-environments-guidance' => enable_environments_guidance?(project).to_s,
'preview-markdown-path' => project && preview_markdown_path(project),
'web-terminal-svg-path' => image_path('illustrations/web-ide_promotion.svg'),
- 'web-terminal-help-path' => help_page_path('user/project/web_ide/index.md', anchor: 'interactive-web-terminals-for-the-web-ide'),
- 'web-terminal-config-help-path' => help_page_path('user/project/web_ide/index.md', anchor: 'web-ide-configuration-file'),
- 'web-terminal-runners-help-path' => help_page_path('user/project/web_ide/index.md', anchor: 'runner-configuration')
+ 'web-terminal-help-path' => help_page_path('user/project/web_ide/index', anchor: 'interactive-web-terminals-for-the-web-ide'),
+ 'web-terminal-config-help-path' => help_page_path('user/project/web_ide/index', anchor: 'web-ide-configuration-file'),
+ 'web-terminal-runners-help-path' => help_page_path('user/project/web_ide/index', anchor: 'runner-configuration')
}
end
diff --git a/app/helpers/members_helper.rb b/app/helpers/members_helper.rb
index e4c1d7932aa..600e5f06c61 100644
--- a/app/helpers/members_helper.rb
+++ b/app/helpers/members_helper.rb
@@ -30,22 +30,11 @@ module MembersHelper
"#{text} #{action} the #{member.source.human_name} #{source_text(member)}?"
end
- def remove_member_title(member)
- action = member.request? ? 'Deny access request' : 'Remove user'
-
- "#{action} from #{source_text(member)}"
- end
-
def leave_confirmation_message(member_source)
"Are you sure you want to leave the " \
"\"#{member_source.human_name}\" #{member_source.model_name.to_s.humanize(capitalize: false)}?"
end
- def filter_group_project_member_path(options = {})
- options = params.slice(:search, :sort).merge(options).permit!
- "#{request.path}?#{options.to_param}"
- end
-
def member_path(member)
if member.is_a?(GroupMember)
group_group_member_path(member.source, member)
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index 131cd7cd969..1dc4c393bf2 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -3,6 +3,7 @@
module MergeRequestsHelper
include Gitlab::Utils::StrongMemoize
include CompareHelper
+ DIFF_BATCH_ENDPOINT_PER_PAGE = 5
def create_mr_button_from_event?(event)
create_mr_button?(from: event.branch_name, source_project: event.project)
@@ -176,7 +177,7 @@ module MergeRequestsHelper
end
def notifications_todos_buttons_enabled?
- Feature.enabled?(:notifications_todos_buttons, @project)
+ Feature.enabled?(:notifications_todos_buttons, current_user)
end
def diffs_tab_pane_data(project, merge_request, params)
@@ -187,7 +188,7 @@ module MergeRequestsHelper
endpoint_batch: diffs_batch_project_json_merge_request_path(project, merge_request, 'json', params),
endpoint_coverage: @coverage_path,
endpoint_diff_for_path: diff_for_path_namespace_project_merge_request_path(format: 'json', id: merge_request.iid, namespace_id: project.namespace.to_param, project_id: project.path),
- help_page_path: help_page_path('user/project/merge_requests/reviews/suggestions.md'),
+ help_page_path: help_page_path('user/project/merge_requests/reviews/suggestions'),
current_user_data: @current_user_data,
update_current_user_path: @update_current_user_path,
project_path: project_path(merge_request.project),
@@ -202,7 +203,8 @@ module MergeRequestsHelper
source_project_full_path: merge_request.source_project&.full_path,
is_forked: project.forked?.to_s,
new_comment_template_path: profile_comment_templates_path,
- iid: merge_request.iid
+ iid: merge_request.iid,
+ per_page: DIFF_BATCH_ENDPOINT_PER_PAGE
}
end
@@ -219,7 +221,7 @@ module MergeRequestsHelper
source_project_full_path: merge_request.source_project&.full_path,
source_project_default_url: merge_request.source_project && default_url_to_repo(merge_request.source_project),
target_branch: merge_request.target_branch,
- reviewing_docs_path: help_page_path('user/project/merge_requests/reviews/index.md', anchor: "checkout-merge-requests-locally-through-the-head-ref")
+ reviewing_docs_path: help_page_path('user/project/merge_requests/reviews/index', anchor: "checkout-merge-requests-locally-through-the-head-ref")
}
end
@@ -288,6 +290,7 @@ module MergeRequestsHelper
data = {
iid: @merge_request.iid,
projectPath: @project.full_path,
+ sourceProjectPath: @merge_request.source_project_path,
title: markdown_field(@merge_request, :title),
isFluidLayout: fluid_layout.to_s,
tabs: [
diff --git a/app/helpers/nav/new_dropdown_helper.rb b/app/helpers/nav/new_dropdown_helper.rb
index 5274ace3d8a..88e834b537a 100644
--- a/app/helpers/nav/new_dropdown_helper.rb
+++ b/app/helpers/nav/new_dropdown_helper.rb
@@ -132,6 +132,17 @@ module Nav
)
end
+ if Feature.enabled?(:ui_for_organizations, current_user) && current_user.can?(:create_organization)
+ menu_items.push(
+ ::Gitlab::Nav::TopNavMenuItem.build(
+ id: 'general_new_organization',
+ title: s_('Organization|New organization'),
+ href: new_organization_path,
+ data: { track_action: 'click_link_new_organization_parent', track_label: 'plus_menu_dropdown', track_property: 'navigation_top', testid: 'global_new_organization_link' }
+ )
+ )
+ end
+
if current_user.can?(:create_snippet)
menu_items.push(
::Gitlab::Nav::TopNavMenuItem.build(
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index d3707183964..0c61749701e 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -57,10 +57,6 @@ module NavHelper
end
end
- def nav_control_class
- "nav-control" if current_user
- end
-
def user_dropdown_class
class_names = []
class_names << 'header-user-dropdown-toggle'
@@ -82,23 +78,11 @@ module NavHelper
%w[system_info background_migrations background_jobs health_check]
end
- def admin_analytics_nav_links
- %w[dev_ops_report usage_trends]
- end
-
- def show_super_sidebar?(user = current_user)
- # The new sidebar is not enabled for anonymous use
- # Once we enable the new sidebar by default, this
- # should return true
- return Feature.enabled?(:super_sidebar_logged_out) unless user
-
- # Users who got the special `super_sidebar_nav_enrolled` enabled,
- # see the new nav as long as they don't explicitly opt-out via the toggle
- if user.use_new_navigation.nil? && Feature.enabled?(:super_sidebar_nav_enrolled, user)
- true
- else
- !!user.use_new_navigation
- end
+ def show_super_sidebar?(_user = current_user)
+ # The new navigation is now enabled for everyone.
+ # We are working on cleaning up the use of this helper and other related code.
+ # See https://gitlab.com/groups/gitlab-org/-/epics/11875
+ true
end
private
diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb
index af8da86b391..75e89a7d7bc 100644
--- a/app/helpers/notes_helper.rb
+++ b/app/helpers/notes_helper.rb
@@ -71,16 +71,20 @@ module NotesHelper
def link_to_reply_discussion(discussion, line_type = nil)
return unless current_user
- data = {
- discussion_id: discussion.reply_id,
- discussion_project_id: discussion.project&.id,
- line_type: line_type
- }
-
- button_tag 'Reply...',
- class: 'btn btn-text-field js-discussion-reply-button',
- data: data,
- title: 'Add a reply'
+ content_tag(
+ :textarea,
+ rows: 1,
+ placeholder: _('Reply...'),
+ 'aria-label': _('Reply to comment'),
+ class: 'reply-placeholder-text-field js-discussion-reply-button',
+ data: {
+ discussion_id: discussion.reply_id,
+ discussion_project_id: discussion.project&.id,
+ line_type: line_type
+ }
+ ) do
+ # render empty textarea
+ end
end
def note_max_access_for_user(note)
diff --git a/app/helpers/operations_helper.rb b/app/helpers/operations_helper.rb
index 8528f5f04f7..d8b3cc3b36e 100644
--- a/app/helpers/operations_helper.rb
+++ b/app/helpers/operations_helper.rb
@@ -21,7 +21,7 @@ module OperationsHelper
'prometheus_authorization_key' => @project.alerting_setting&.token,
'prometheus_api_url' => prometheus_integration.api_url,
'prometheus_url' => notify_project_prometheus_alerts_url(@project, format: :json),
- 'alerts_setup_url' => help_page_path('operations/incident_management/integrations.md', anchor: 'configuration'),
+ 'alerts_setup_url' => help_page_path('operations/incident_management/integrations', anchor: 'configuration'),
'alerts_usage_url' => project_alert_management_index_path(@project),
'disabled' => disabled.to_s,
'project_path' => @project.full_path,
diff --git a/app/helpers/organizations/organization_helper.rb b/app/helpers/organizations/organization_helper.rb
index 5d89bb93000..61eb9b5c35f 100644
--- a/app/helpers/organizations/organization_helper.rb
+++ b/app/helpers/organizations/organization_helper.rb
@@ -23,6 +23,14 @@ module Organizations
}.to_json
end
+ def organization_settings_general_app_data(organization)
+ {
+ organization: organization.slice(:id, :name, :path),
+ organizations_path: organizations_path,
+ root_url: root_url
+ }.to_json
+ end
+
def organization_groups_and_projects_app_data
shared_groups_and_projects_app_data.to_json
end
@@ -34,6 +42,19 @@ module Organizations
}
end
+ def organization_user_app_data(organization)
+ {
+ organization_gid: organization.to_global_id
+ }
+ end
+
+ def home_organization_setting_app_data
+ {
+ # TODO: use real setting - https://gitlab.com/gitlab-org/gitlab/-/issues/428668
+ initial_selection: 1
+ }.to_json
+ end
+
private
def shared_groups_and_projects_app_data
diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb
index 656d35e927d..204e3b149b9 100644
--- a/app/helpers/preferences_helper.rb
+++ b/app/helpers/preferences_helper.rb
@@ -59,6 +59,10 @@ module PreferencesHelper
]
end
+ def time_display_format_choices
+ UserPreference.time_display_formats
+ end
+
def first_day_of_week_choices_with_default
first_day_of_week_choices.unshift([_('System default (%{default})') % { default: default_first_day_of_week }, nil])
end
@@ -122,8 +126,8 @@ module PreferencesHelper
def integration_views
[].tap do |views|
- views << { name: 'gitpod', message: gitpod_enable_description, message_url: gitpod_url_placeholder, help_link: help_page_path('integration/gitpod.md') } if Gitlab::CurrentSettings.gitpod_enabled
- views << { name: 'sourcegraph', message: sourcegraph_url_message, message_url: Gitlab::CurrentSettings.sourcegraph_url, help_link: help_page_path('user/profile/preferences.md', anchor: 'sourcegraph') } if Gitlab::Sourcegraph.feature_available? && Gitlab::CurrentSettings.sourcegraph_enabled
+ views << { name: 'gitpod', message: gitpod_enable_description, message_url: gitpod_url_placeholder, help_link: help_page_path('integration/gitpod') } if Gitlab::CurrentSettings.gitpod_enabled
+ views << { name: 'sourcegraph', message: sourcegraph_url_message, message_url: Gitlab::CurrentSettings.sourcegraph_url, help_link: help_page_path('user/profile/preferences', anchor: 'sourcegraph') } if Gitlab::Sourcegraph.feature_available? && Gitlab::CurrentSettings.sourcegraph_enabled
end
end
diff --git a/app/helpers/projects/pipeline_helper.rb b/app/helpers/projects/pipeline_helper.rb
index 0c3b7d26fe2..fc33e239451 100644
--- a/app/helpers/projects/pipeline_helper.rb
+++ b/app/helpers/projects/pipeline_helper.rb
@@ -37,9 +37,11 @@ module Projects
failure_reason: pipeline.failure_reason,
triggered_by_path: pipeline.child? ? pipeline_path(pipeline.triggered_by_pipeline) : '',
schedule: pipeline.schedule?.to_s,
+ trigger: pipeline.trigger?.to_s,
child: pipeline.child?.to_s,
latest: pipeline.latest?.to_s,
merge_train_pipeline: pipeline.merge_train_pipeline?.to_s,
+ merged_results_pipeline: (pipeline.merged_result_pipeline? && !pipeline.merge_train_pipeline?).to_s,
invalid: pipeline.has_yaml_errors?.to_s,
failed: pipeline.failure_reason?.to_s,
auto_devops: pipeline.auto_devops_source?.to_s,
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 04fe0a4450c..c3287d141f7 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -11,7 +11,7 @@ module ProjectsHelper
end
def link_to_project(project)
- link_to namespace_project_path(namespace_id: project.namespace, id: project), title: h(project.name), class: 'gl-link' do
+ link_to namespace_project_path(namespace_id: project.namespace, id: project), title: h(project.name), class: 'gl-link gl-text-truncate' do
title = content_tag(:span, project.name, class: 'project-name')
if project.namespace
@@ -187,7 +187,7 @@ module ProjectsHelper
end
def link_to_autodeploy_doc
- link_to _('About auto deploy'), help_page_path('topics/autodevops/stages.md', anchor: 'auto-deploy'), target: '_blank', rel: 'noopener'
+ link_to _('About auto deploy'), help_page_path('topics/autodevops/stages', anchor: 'auto-deploy'), target: '_blank', rel: 'noopener'
end
def autodeploy_flash_notice(branch_name)
@@ -200,6 +200,10 @@ module ProjectsHelper
.load_in_batch_for_projects(projects)
end
+ def load_catalog_resources(projects)
+ ActiveRecord::Associations::Preloader.new(records: projects, associations: :catalog_resource).call
+ end
+
def last_pipeline_from_status_cache(project)
if Feature.enabled?(:last_pipeline_from_pipeline_status, project)
pipeline_status = project.pipeline_status
diff --git a/app/helpers/sidebars_helper.rb b/app/helpers/sidebars_helper.rb
index 33ca5ad584e..f983812ad22 100644
--- a/app/helpers/sidebars_helper.rb
+++ b/app/helpers/sidebars_helper.rb
@@ -358,7 +358,9 @@ module SidebarsHelper
def context_switcher_links
links = [
({ title: s_('Navigation|Your work'), link: root_path, icon: 'work' } if current_user),
- { title: s_('Navigation|Explore'), link: explore_root_path, icon: 'compass' }
+ { title: s_('Navigation|Explore'), link: explore_root_path, icon: 'compass' },
+ ({ title: s_('Navigation|Profile'), link: profile_path, icon: 'profile' } if current_user),
+ ({ title: s_('Navigation|Preferences'), link: profile_preferences_path, icon: 'preferences' } if current_user)
]
# Usually, using current_user.admin? is discouraged because it does not
diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb
index 94445564c22..8b5c0707d08 100644
--- a/app/helpers/sorting_helper.rb
+++ b/app/helpers/sorting_helper.rb
@@ -263,36 +263,6 @@ module SortingHelper
sort_direction_button(url, reverse_sort, sort_value)
end
- def packages_sort_options_hash
- {
- sort_value_recently_created => sort_title_created_date,
- sort_value_oldest_created => sort_title_created_date,
- sort_value_name => sort_title_name,
- sort_value_name_desc => sort_title_name,
- sort_value_version_desc => sort_title_version,
- sort_value_version_asc => sort_title_version,
- sort_value_type_desc => sort_title_type,
- sort_value_type_asc => sort_title_type,
- sort_value_project_name_desc => sort_title_project_name,
- sort_value_project_name_asc => sort_title_project_name
- }
- end
-
- def packages_reverse_sort_order_hash
- {
- sort_value_recently_created => sort_value_oldest_created,
- sort_value_oldest_created => sort_value_recently_created,
- sort_value_name => sort_value_name_desc,
- sort_value_name_desc => sort_value_name,
- sort_value_version_desc => sort_value_version_asc,
- sort_value_version_asc => sort_value_version_desc,
- sort_value_type_desc => sort_value_type_asc,
- sort_value_type_asc => sort_value_type_desc,
- sort_value_project_name_desc => sort_value_project_name_asc,
- sort_value_project_name_asc => sort_value_project_name_desc
- }
- end
-
def forks_sort_direction_button(sort_value, without = [:state, :scope, :label_name, :milestone_id, :assignee_id, :author_id])
reverse_sort = forks_reverse_sort_options_hash[sort_value]
url = page_filter_path(sort: reverse_sort, without: without)
diff --git a/app/helpers/users/callouts_helper.rb b/app/helpers/users/callouts_helper.rb
index 1b5d0b276a3..6f1d4db4349 100644
--- a/app/helpers/users/callouts_helper.rb
+++ b/app/helpers/users/callouts_helper.rb
@@ -15,7 +15,7 @@ module Users
REGISTRATION_ENABLED_CALLOUT_ALLOWED_CONTROLLER_PATHS = [/^root/, /^dashboard\S*/, /^admin\S*/].freeze
WEB_HOOK_DISABLED = 'web_hook_disabled'
BRANCH_RULES_INFO_CALLOUT = 'branch_rules_info_callout'
- NEW_NAVIGATION_CALLOUT = 'new_navigation_callout'
+ NEW_NAV_FOR_EVERYONE_CALLOUT = 'new_nav_for_everyone_callout'
def show_gke_cluster_integration_callout?(project)
active_nav_link?(controller: sidebar_operations_paths) &&
@@ -71,26 +71,16 @@ module Users
!user_dismissed?(MERGE_REQUEST_SETTINGS_MOVED_CALLOUT) && project.merge_requests_enabled?
end
- def show_pages_menu_callout?
- !user_dismissed?(PAGES_MOVED_CALLOUT)
- end
-
def show_branch_rules_info?
!user_dismissed?(BRANCH_RULES_INFO_CALLOUT)
end
- def show_new_navigation_callout?
- show_super_sidebar? &&
- !user_dismissed?(NEW_NAVIGATION_CALLOUT) &&
- # GitLab.com users created after the feature flag's full rollout (June 2nd 2023) don't need to see the callout.
- # Remove the gitlab_com_user_created_after_new_nav_rollout? method when the callout isn't needed anymore.
- !gitlab_com_user_created_after_new_nav_rollout?
- end
-
- def gitlab_com_user_created_after_new_nav_rollout?
- return true unless current_user
-
- Gitlab.com? && current_user.created_at >= Date.new(2023, 6, 2)
+ def show_new_nav_for_everyone_callout?
+ # The use_new_navigation user preference was controlled by the now removed "New navigation" toggle in the UI.
+ # We want to show this banner only to signed-in users who chose to disable the new nav (`false`).
+ # We don't want to show it for users who never touched the toggle and already had the new nav by default (`nil`)
+ user_had_new_nav_off = current_user && current_user.use_new_navigation == false
+ user_had_new_nav_off && !user_dismissed?(NEW_NAV_FOR_EVERYONE_CALLOUT)
end
private
diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb
index a892b6e6ac6..84a809bc510 100644
--- a/app/helpers/users_helper.rb
+++ b/app/helpers/users_helper.rb
@@ -80,10 +80,6 @@ module UsersHelper
current_user&.max_member_access_for_project(project.id) || Gitlab::Access::NO_ACCESS
end
- def max_project_member_access_cache_key(project)
- "access:#{max_project_member_access(project)}"
- end
-
def user_status(user)
return unless user
@@ -262,7 +258,9 @@ module UsersHelper
delete_with_contributions: admin_user_path(:id, hard_delete: true),
admin_user: admin_user_path(:id),
ban: ban_admin_user_path(:id),
- unban: unban_admin_user_path(:id)
+ unban: unban_admin_user_path(:id),
+ trust: trust_admin_user_path(:id),
+ untrust: untrust_admin_user_path(:id)
}
end
@@ -334,27 +332,6 @@ module UsersHelper
end
end
- def user_table_headers
- [
- {
- section_class_name: 'section-40',
- header_text: _('Name')
- },
- {
- section_class_name: 'section-10',
- header_text: _('Projects')
- },
- {
- section_class_name: 'section-15',
- header_text: _('Created on')
- },
- {
- section_class_name: 'section-15',
- header_text: _('Last activity')
- }
- ]
- end
-
# the keys should match the user model defined roles in app/models/user.rb
def localized_user_roles
{
@@ -370,10 +347,6 @@ module UsersHelper
}.with_indifferent_access.freeze
end
- def saved_replies_enabled?
- Feature.enabled?(:saved_replies, current_user)
- end
-
def preload_project_associations(_)
# Overridden in EE
end
diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb
index 68b15f7e042..cddfc48c649 100644
--- a/app/helpers/visibility_level_helper.rb
+++ b/app/helpers/visibility_level_helper.rb
@@ -76,16 +76,6 @@ module VisibilityLevelHelper
end
end
- def visibility_level_options(form_model)
- available_visibility_levels(form_model).map do |level|
- {
- level: level,
- label: visibility_level_label(level),
- description: visibility_level_description(level, form_model)
- }
- end
- end
-
def snippets_selected_visibility_level(visibility_levels, selected)
visibility_levels.find { |level| level == selected } || visibility_levels.min
end
diff --git a/app/helpers/vite_helper.rb b/app/helpers/vite_helper.rb
index 4d1085a5169..5096d3649b7 100644
--- a/app/helpers/vite_helper.rb
+++ b/app/helpers/vite_helper.rb
@@ -1,22 +1,6 @@
# frozen_string_literal: true
module ViteHelper
- def universal_javascript_include_tag(*args)
- if vite_enabled
- vite_javascript_tag(*args)
- else
- javascript_include_tag(*args)
- end
- end
-
- def universal_asset_path(*args)
- if vite_enabled
- vite_asset_path(*args)
- else
- asset_path(*args)
- end
- end
-
private
def vite_enabled
diff --git a/app/helpers/wiki_helper.rb b/app/helpers/wiki_helper.rb
index bd63381e9d1..eda789d5e55 100644
--- a/app/helpers/wiki_helper.rb
+++ b/app/helpers/wiki_helper.rb
@@ -68,14 +68,6 @@ module WikiHelper
render Pajamas::ButtonComponent.new(href: wiki_path(wiki, **link_options), icon: "sort-#{icon_class}", button_options: { class: link_class, title: title })
end
- def wiki_sort_title(key)
- if key == Wiki::CREATED_AT_ORDER
- s_("Wiki|Created date")
- else
- s_("Wiki|Title")
- end
- end
-
def wiki_empty_state_messages(wiki)
case wiki.container
when Project
diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb
index 52a16475c07..f859294960c 100644
--- a/app/mailers/emails/issues.rb
+++ b/app/mailers/emails/issues.rb
@@ -70,7 +70,7 @@ module Emails
setup_issue_mail(issue_id, recipient_id)
@label_names = label_names
- @labels_url = project_labels_url(@project)
+ @labels_url = project_labels_url(@project, subscribed: true)
mail_answer_thread(
@issue,
issue_thread_options(
diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb
index cd7869123f3..5e82a3e8dcf 100644
--- a/app/mailers/emails/merge_requests.rb
+++ b/app/mailers/emails/merge_requests.rb
@@ -65,7 +65,7 @@ module Emails
setup_merge_request_mail(merge_request_id, recipient_id)
@label_names = label_names
- @labels_url = project_labels_url(@project)
+ @labels_url = project_labels_url(@project, subscribed: true)
mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, reason))
end
diff --git a/app/mailers/emails/service_desk.rb b/app/mailers/emails/service_desk.rb
index f6595a91bee..f67c2636fc6 100644
--- a/app/mailers/emails/service_desk.rb
+++ b/app/mailers/emails/service_desk.rb
@@ -227,8 +227,6 @@ module Emails
# Filepaths we should replace in markdown content
@uploads_as_attachments = []
- return unless Feature.enabled?(:service_desk_new_note_email_native_attachments, @note.project)
-
uploaders = find_uploaders_for(@note)
return if uploaders.nil?
return if uploaders.sum(&:size) > EMAIL_ATTACHMENTS_SIZE_LIMIT
diff --git a/app/models/abuse_report.rb b/app/models/abuse_report.rb
index 872dedf07b1..de6b644c536 100644
--- a/app/models/abuse_report.rb
+++ b/app/models/abuse_report.rb
@@ -139,11 +139,11 @@ class AbuseReport < ApplicationRecord
def reported_content
case report_type
when :issue
- project.issues.iid_in(route_hash[:id]).pick(:description_html)
+ reported_project.issues.iid_in(route_hash[:id]).pick(:description_html)
when :merge_request
- project.merge_requests.iid_in(route_hash[:id]).pick(:description_html)
+ reported_project.merge_requests.iid_in(route_hash[:id]).pick(:description_html)
when :comment
- project.notes.id_in(note_id_from_url).pick(:note_html)
+ reported_project.notes.id_in(note_id_from_url).pick(:note_html)
end
end
@@ -157,13 +157,19 @@ class AbuseReport < ApplicationRecord
user.abuse_reports.open.by_category(category).id_not_in(id).includes(:reporter)
end
+ # createNote mutation calls noteable.project,
+ # which in case of abuse reports is nil
+ def project
+ nil
+ end
+
private
- def project
+ def reported_project
Project.find_by_full_path(route_hash.values_at(:namespace_id, :project_id).join('/'))
end
- def group
+ def reported_group
Group.find_by_full_path(route_hash[:group_id])
end
diff --git a/app/models/active_session.rb b/app/models/active_session.rb
index e42f9eeef23..9756e1b7dd3 100644
--- a/app/models/active_session.rb
+++ b/app/models/active_session.rb
@@ -84,7 +84,7 @@ class ActiveSession
)
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
- redis.pipelined do |pipeline|
+ Gitlab::Redis::CrossSlot::Pipeline.new(redis).pipelined do |pipeline|
pipeline.setex(
key_name(user.id, session_private_id),
expiry,
@@ -135,9 +135,15 @@ class ActiveSession
redis.srem(lookup_key_name(user.id), session_ids)
+ session_keys = rack_session_keys(session_ids)
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
- redis.del(key_names)
- redis.del(rack_session_keys(session_ids))
+ if Gitlab::Redis::ClusterUtil.cluster?(redis)
+ Gitlab::Redis::ClusterUtil.batch_unlink(key_names, redis)
+ Gitlab::Redis::ClusterUtil.batch_unlink(session_keys, redis)
+ else
+ redis.del(key_names)
+ redis.del(session_keys)
+ end
end
end
@@ -206,7 +212,13 @@ class ActiveSession
session_keys.each_slice(SESSION_BATCH_SIZE).flat_map do |session_keys_batch|
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
- redis.mget(session_keys_batch).compact.map do |raw_session|
+ raw_sessions = if Gitlab::Redis::ClusterUtil.cluster?(redis)
+ Gitlab::Redis::ClusterUtil.batch_get(session_keys_batch, redis)
+ else
+ redis.mget(session_keys_batch)
+ end
+
+ raw_sessions.compact.map do |raw_session|
load_raw_session(raw_session)
end
end
@@ -249,7 +261,13 @@ class ActiveSession
found = Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
entry_keys = session_ids.map { |session_id| key_name(user_id, session_id) }
- session_ids.zip(redis.mget(entry_keys)).to_h
+ entries = if Gitlab::Redis::ClusterUtil.cluster?(redis)
+ Gitlab::Redis::ClusterUtil.batch_get(entry_keys, redis)
+ else
+ redis.mget(entry_keys)
+ end
+
+ session_ids.zip(entries).to_h
end
found.compact!
@@ -258,7 +276,13 @@ class ActiveSession
fallbacks = Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
entry_keys = missing.map { |session_id| key_name_v1(user_id, session_id) }
- missing.zip(redis.mget(entry_keys)).to_h
+ entries = if Gitlab::Redis::ClusterUtil.cluster?(redis)
+ Gitlab::Redis::ClusterUtil.batch_get(entry_keys, redis)
+ else
+ redis.mget(entry_keys)
+ end
+
+ missing.zip(entries).to_h
end
fallbacks.merge(found.compact)
diff --git a/app/models/activity_pub.rb b/app/models/activity_pub.rb
new file mode 100644
index 00000000000..9131d8be776
--- /dev/null
+++ b/app/models/activity_pub.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module ActivityPub
+ def self.table_name_prefix
+ "activity_pub_"
+ end
+end
diff --git a/app/models/activity_pub/releases_subscription.rb b/app/models/activity_pub/releases_subscription.rb
new file mode 100644
index 00000000000..a6304f1fc35
--- /dev/null
+++ b/app/models/activity_pub/releases_subscription.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module ActivityPub
+ class ReleasesSubscription < ApplicationRecord
+ belongs_to :project, optional: false
+
+ enum :status, [:requested, :accepted], default: :requested
+
+ attribute :payload, Gitlab::Database::Type::JsonPgSafe.new
+
+ validates :payload, json_schema: { filename: 'activity_pub_follow_payload' }, allow_blank: true
+ validates :subscriber_url, presence: true, uniqueness: { case_sensitive: false, scope: :project_id },
+ public_url: true
+ validates :subscriber_inbox_url, uniqueness: { case_sensitive: false, scope: :project_id },
+ public_url: { allow_nil: true }
+ validates :shared_inbox_url, public_url: { allow_nil: true }
+
+ def self.find_by_subscriber_url(subscriber_url)
+ find_by('LOWER(subscriber_url) = ?', subscriber_url.downcase)
+ end
+ end
+end
diff --git a/app/models/ai/service_access_token.rb b/app/models/ai/service_access_token.rb
index b8a2a271976..46dfbe9078c 100644
--- a/app/models/ai/service_access_token.rb
+++ b/app/models/ai/service_access_token.rb
@@ -2,11 +2,13 @@
module Ai
class ServiceAccessToken < ApplicationRecord
+ include IgnorableColumns
self.table_name = 'service_access_tokens'
+ ignore_column :category, remove_with: '16.8', remove_after: '2024-01-22'
+
scope :expired, -> { where('expires_at < :now', now: Time.current) }
scope :active, -> { where('expires_at > :now', now: Time.current) }
- scope :for_category, ->(category) { where(category: category) }
attr_encrypted :token,
mode: :per_attribute_iv,
@@ -16,11 +18,5 @@ module Ai
encode_iv: false
validates :token, :expires_at, presence: true
-
- enum category: {
- code_suggestions: 1
- }
-
- validates :category, presence: true
end
end
diff --git a/app/models/analytics/cycle_analytics/value_stream.rb b/app/models/analytics/cycle_analytics/value_stream.rb
index 7f8c6eef704..d884932072b 100644
--- a/app/models/analytics/cycle_analytics/value_stream.rb
+++ b/app/models/analytics/cycle_analytics/value_stream.rb
@@ -36,6 +36,12 @@ module Analytics
new(name: Analytics::CycleAnalytics::Stages::BaseService::DEFAULT_VALUE_STREAM_NAME, namespace: namespace)
end
+ def project
+ return unless namespace.is_a?(::Namespaces::ProjectNamespace)
+
+ namespace.project
+ end
+
private
def max_value_streams_count
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 824a2bd9fa4..8d4f50de75e 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -30,7 +30,9 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
jitsu_project_xid
jitsu_administrator_email
], remove_with: '16.5', remove_after: '2023-09-22'
- ignore_columns %i[encrypted_ai_access_token encrypted_ai_access_token_iv], remove_with: '16.6', remove_after: '2023-10-22'
+ ignore_columns %i[encrypted_ai_access_token encrypted_ai_access_token_iv], remove_with: '16.10', remove_after: '2024-03-22'
+
+ ignore_columns %i[repository_storages], remove_with: '16.8', remove_after: '2023-12-21'
INSTANCE_REVIEW_MIN_USERS = 50
GRAFANA_URL_ERROR_MESSAGE = 'Please check your Grafana URL setting in ' \
@@ -91,7 +93,6 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
serialize :disabled_oauth_sign_in_sources, Array # rubocop:disable Cop/ActiveRecordSerialize
serialize :domain_allowlist, Array # rubocop:disable Cop/ActiveRecordSerialize
serialize :domain_denylist, Array # rubocop:disable Cop/ActiveRecordSerialize
- serialize :repository_storages # rubocop:disable Cop/ActiveRecordSerialize
# See https://gitlab.com/gitlab-org/gitlab/-/issues/300916
serialize :asset_proxy_allowlist, Array # rubocop:disable Cop/ActiveRecordSerialize
@@ -303,8 +304,6 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
presence: true,
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
- validates :repository_storages, presence: true
- validate :check_repository_storages
validate :check_repository_storages_weighted
validates :auto_devops_domain,
@@ -488,7 +487,7 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
validates :invisible_captcha_enabled,
inclusion: { in: [true, false], message: N_('must be a boolean value') }
- validates :invitation_flow_enforcement, :can_create_group, :user_defaults_to_private_profile,
+ validates :invitation_flow_enforcement, :can_create_group, :allow_project_creation_for_guest_and_below, :user_defaults_to_private_profile,
allow_nil: false,
inclusion: { in: [true, false], message: N_('must be a boolean value') }
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index 1bd15a56de5..00b093c8ac3 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -57,6 +57,7 @@ module ApplicationSettingImplementation
default_artifacts_expire_in: '30 days',
default_branch_name: nil,
default_branch_protection: Settings.gitlab['default_branch_protection'],
+ default_branch_protection_defaults: Settings.gitlab['default_branch_protection_defaults'],
default_ci_config_path: nil,
default_group_visibility: Settings.gitlab.default_projects_features['visibility_level'],
default_project_creation: Settings.gitlab['default_project_creation'],
@@ -158,7 +159,6 @@ module ApplicationSettingImplementation
recaptcha_enabled: false,
repository_checks_enabled: true,
repository_storages_weighted: { 'default' => 100 },
- repository_storages: ['default'],
require_admin_approval_after_user_signup: true,
require_two_factor_authentication: false,
restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'],
@@ -433,10 +433,6 @@ module ApplicationSettingImplementation
read_attribute(:asset_proxy_whitelist)
end
- def repository_storages
- Array(read_attribute(:repository_storages))
- end
-
def commit_email_hostname
super.presence || self.class.default_commit_email_hostname
end
@@ -644,12 +640,6 @@ module ApplicationSettingImplementation
self.uuid = SecureRandom.uuid
end
- def check_repository_storages
- invalid = repository_storages - Gitlab.config.repositories.storages.keys
- errors.add(:repository_storages, "can't include: #{invalid.join(", ")}") unless
- invalid.empty?
- end
-
def coerce_repository_storages_weighted
repository_storages_weighted.transform_values!(&:to_i)
end
diff --git a/app/models/bulk_imports/entity.rb b/app/models/bulk_imports/entity.rb
index 437118c36e8..a075c2f7e4f 100644
--- a/app/models/bulk_imports/entity.rb
+++ b/app/models/bulk_imports/entity.rb
@@ -124,6 +124,10 @@ class BulkImports::Entity < ApplicationRecord
entity_type.pluralize
end
+ def portable_class
+ entity_type.classify.constantize
+ end
+
def base_resource_url_path
"/#{pluralized_name}/#{encoded_source_full_path}"
end
diff --git a/app/models/bulk_imports/failure.rb b/app/models/bulk_imports/failure.rb
index 44d16618c77..8a6077b523c 100644
--- a/app/models/bulk_imports/failure.rb
+++ b/app/models/bulk_imports/failure.rb
@@ -15,6 +15,10 @@ class BulkImports::Failure < ApplicationRecord
pipeline_relation || default_relation
end
+ def exception_message=(message)
+ super(::Projects::ImportErrorFilter.filter_message(message).truncate(255))
+ end
+
private
def pipeline_relation
diff --git a/app/models/ci/bridge.rb b/app/models/ci/bridge.rb
index d0ccf5c543a..cf6401dc1da 100644
--- a/app/models/ci/bridge.rb
+++ b/app/models/ci/bridge.rb
@@ -114,7 +114,7 @@ module Ci
project = options&.dig(:trigger, :project)
next unless project
- scoped_variables.to_runner_variables.yield_self do |all_variables|
+ scoped_variables.to_runner_variables.then do |all_variables|
::ExpandVariables.expand(project, all_variables)
end
end
@@ -199,7 +199,7 @@ module Ci
branch = options&.dig(:trigger, :branch)
return unless branch
- scoped_variables.to_runner_variables.yield_self do |all_variables|
+ scoped_variables.to_runner_variables.then do |all_variables|
::ExpandVariables.expand(branch, all_variables)
end
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index d2cf9058976..0bb93a68470 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -392,8 +392,8 @@ module Ci
name == 'pages'
end
- # overridden on EE
- def pages_path_prefix; end
+ # Overriden on EE
+ def pages; end
def runnable?
true
@@ -729,7 +729,7 @@ module Ci
end
def artifacts_expired?
- artifacts_expire_at && artifacts_expire_at < Time.current
+ artifacts_expire_at&.past?
end
def artifacts_expire_in
@@ -745,7 +745,7 @@ module Ci
def has_expired_locked_archive_artifacts?
locked_artifacts? &&
- artifacts_expire_at.present? && artifacts_expire_at < Time.current
+ artifacts_expire_at&.past?
end
def has_expiring_archive_artifacts?
@@ -921,13 +921,25 @@ module Ci
# Consider this object to have a structural integrity problems
def doom!
transaction do
- update_columns(status: :failed, failure_reason: :data_integrity_failure)
+ now = Time.current
+ attributes = {
+ status: :failed,
+ failure_reason: :data_integrity_failure,
+ updated_at: now
+ }
+ attributes[:finished_at] = now unless finished_at.present?
+
+ update_columns(attributes)
all_queuing_entries.delete_all
all_runtime_metadata.delete_all
end
deployment&.sync_status_with(self)
+ ::Gitlab::Ci::Pipeline::Metrics
+ .job_failure_reason_counter
+ .increment(reason: :data_integrity_failure)
+
Gitlab::AppLogger.info(
message: 'Build doomed',
class: self.class.name,
diff --git a/app/models/ci/build_trace_chunks/redis_base.rb b/app/models/ci/build_trace_chunks/redis_base.rb
index 3b7a844d122..5f6b5c30a6a 100644
--- a/app/models/ci/build_trace_chunks/redis_base.rb
+++ b/app/models/ci/build_trace_chunks/redis_base.rb
@@ -71,7 +71,11 @@ module Ci
with_redis do |redis|
# https://gitlab.com/gitlab-org/gitlab/-/issues/224171
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
- redis.del(keys)
+ if Gitlab::Redis::ClusterUtil.cluster?(redis)
+ Gitlab::Redis::ClusterUtil.batch_unlink(keys, redis)
+ else
+ redis.del(keys)
+ end
end
end
end
diff --git a/app/models/ci/build_trace_metadata.rb b/app/models/ci/build_trace_metadata.rb
index c5ad3d19425..525cb08f2ca 100644
--- a/app/models/ci/build_trace_metadata.rb
+++ b/app/models/ci/build_trace_metadata.rb
@@ -33,7 +33,7 @@ module Ci
return false unless archival_attempts_available?
return true unless last_archival_attempt_at
- last_archival_attempt_at + backoff < Time.current
+ (last_archival_attempt_at + backoff).past?
end
def archival_attempts_available?
diff --git a/app/models/ci/catalog/components_project.rb b/app/models/ci/catalog/components_project.rb
index 2bc33a6f050..02593d41bc2 100644
--- a/app/models/ci/catalog/components_project.rb
+++ b/app/models/ci/catalog/components_project.rb
@@ -9,7 +9,8 @@ module Ci
TEMPLATE_FILE = 'template.yml'
TEMPLATES_DIR = 'templates'
- TEMPLATE_PATH_REGEX = '^templates\/\w+\-?\w+(?:\/template)?\.yml$'
+ TEMPLATE_PATH_REGEX = '^templates\/[\w-]+(?:\/template)?\.yml$'
+ COMPONENTS_LIMIT = 10
ComponentData = Struct.new(:content, :path, keyword_init: true)
@@ -18,8 +19,8 @@ module Ci
@sha = sha
end
- def fetch_component_paths(sha)
- project.repository.search_files_by_regexp(TEMPLATE_PATH_REGEX, sha)
+ def fetch_component_paths(sha, limit: COMPONENTS_LIMIT)
+ project.repository.search_files_by_regexp(TEMPLATE_PATH_REGEX, sha, limit: limit)
end
def extract_component_name(path)
diff --git a/app/models/ci/catalog/listing.rb b/app/models/ci/catalog/listing.rb
index c3b18af8c3f..51bd85016a5 100644
--- a/app/models/ci/catalog/listing.rb
+++ b/app/models/ci/catalog/listing.rb
@@ -3,42 +3,53 @@
module Ci
module Catalog
class Listing
- # This class is the SSoT to displaying the list of resources in the
- # CI/CD Catalog given a namespace as a scope.
+ # This class is the SSoT to displaying the list of resources in the CI/CD Catalog.
# This model is not directly backed by a table and joins catalog resources
# with projects to return relevant data.
- def initialize(namespace, current_user)
- raise ArgumentError, 'Namespace is not a root namespace' unless namespace.root?
- @namespace = namespace
+ MIN_SEARCH_LENGTH = 3
+
+ def initialize(current_user)
@current_user = current_user
end
- def resources(sort: nil)
+ def resources(namespace: nil, sort: nil, search: nil)
+ relation = all_resources
+ relation = by_namespace(relation, namespace)
+ relation = by_search(relation, search)
+
case sort.to_s
- when 'name_desc' then all_resources.order_by_name_desc
- when 'name_asc' then all_resources.order_by_name_asc
- when 'latest_released_at_desc' then all_resources.order_by_latest_released_at_desc
- when 'latest_released_at_asc' then all_resources.order_by_latest_released_at_asc
+ when 'name_desc' then relation.order_by_name_desc
+ when 'name_asc' then relation.order_by_name_asc
+ when 'latest_released_at_desc' then relation.order_by_latest_released_at_desc
+ when 'latest_released_at_asc' then relation.order_by_latest_released_at_asc
+ when 'created_at_asc' then relation.order_by_created_at_asc
else
- all_resources.order_by_created_at_desc
+ relation.order_by_created_at_desc
end
end
private
- attr_reader :namespace, :current_user
+ attr_reader :current_user
def all_resources
- Ci::Catalog::Resource
- .joins(:project).includes(:project)
- .merge(projects_in_namespace_visible_to_user)
+ Ci::Catalog::Resource.joins(:project).includes(:project)
+ .merge(Project.public_or_visible_to_user(current_user))
+ end
+
+ def by_namespace(relation, namespace)
+ return relation unless namespace
+ raise ArgumentError, 'Namespace is not a root namespace' unless namespace.root?
+
+ relation.merge(Project.in_namespace(namespace.self_and_descendant_ids))
end
- def projects_in_namespace_visible_to_user
- Project
- .in_namespace(namespace.self_and_descendant_ids)
- .public_or_visible_to_user(current_user, ::Gitlab::Access::DEVELOPER)
+ def by_search(relation, search)
+ return relation unless search
+ return relation.none if search.length < MIN_SEARCH_LENGTH
+
+ relation.search(search)
end
end
end
diff --git a/app/models/ci/catalog/resource.rb b/app/models/ci/catalog/resource.rb
index 8ffc0292a69..f947c5158cf 100644
--- a/app/models/ci/catalog/resource.rb
+++ b/app/models/ci/catalog/resource.rb
@@ -8,29 +8,55 @@ module Ci
# dependency on the Project model and its need to join with that table
# in order to generate the CI/CD catalog.
class Resource < ::ApplicationRecord
+ include Gitlab::SQL::Pattern
+
self.table_name = 'catalog_resources'
belongs_to :project
- has_many :components, class_name: 'Ci::Catalog::Resources::Component', inverse_of: :catalog_resource
- has_many :versions, class_name: 'Ci::Catalog::Resources::Version', inverse_of: :catalog_resource
+ has_many :components, class_name: 'Ci::Catalog::Resources::Component', foreign_key: :catalog_resource_id,
+ inverse_of: :catalog_resource
+ has_many :versions, class_name: 'Ci::Catalog::Resources::Version', foreign_key: :catalog_resource_id,
+ inverse_of: :catalog_resource
scope :for_projects, ->(project_ids) { where(project_id: project_ids) }
+ scope :search, ->(query) { fuzzy_search(query, [:name, :description], use_minimum_char_limit: false) }
+
scope :order_by_created_at_desc, -> { reorder(created_at: :desc) }
- scope :order_by_name_desc, -> { joins(:project).merge(Project.sorted_by_name_desc) }
- scope :order_by_name_asc, -> { joins(:project).merge(Project.sorted_by_name_asc) }
+ scope :order_by_created_at_asc, -> { reorder(created_at: :asc) }
+ scope :order_by_name_desc, -> { reorder(arel_table[:name].desc.nulls_last) }
+ scope :order_by_name_asc, -> { reorder(arel_table[:name].asc.nulls_last) }
scope :order_by_latest_released_at_desc, -> { reorder(arel_table[:latest_released_at].desc.nulls_last) }
scope :order_by_latest_released_at_asc, -> { reorder(arel_table[:latest_released_at].asc.nulls_last) }
- delegate :avatar_path, :description, :name, :star_count, :forks_count, to: :project
+ delegate :avatar_path, :star_count, :forks_count, to: :project
enum state: { draft: 0, published: 1 }
- def versions
- project.releases.order_released_desc
+ before_create :sync_with_project
+
+ def unpublish!
+ update!(state: :draft)
+ end
+
+ def publish!
+ update!(state: :published)
+ end
+
+ def sync_with_project!
+ sync_with_project
+ save!
end
- def latest_version
- project.releases.latest
+ private
+
+ # These columns are denormalized from the `projects` table. We first sync these
+ # columns when the catalog resource record is created. Then any updates to the
+ # `projects` columns will be synced to the `catalog_resources` table by a worker
+ # (to be implemented in https://gitlab.com/gitlab-org/gitlab/-/issues/429376.)
+ def sync_with_project
+ self.name = project.name
+ self.description = project.description
+ self.visibility_level = project.visibility_level
end
end
end
diff --git a/app/models/ci/catalog/resources/component.rb b/app/models/ci/catalog/resources/component.rb
index 7b95c14ba7e..07d5404981b 100644
--- a/app/models/ci/catalog/resources/component.rb
+++ b/app/models/ci/catalog/resources/component.rb
@@ -6,6 +6,8 @@ module Ci
# This class represents a CI/CD Catalog resource component.
# The data will be used as metadata of a component.
class Component < ::ApplicationRecord
+ include BulkInsertSafe
+
self.table_name = 'catalog_resource_components'
belongs_to :project, inverse_of: :ci_components
diff --git a/app/models/ci/catalog/resources/version.rb b/app/models/ci/catalog/resources/version.rb
index 68f60e6a965..bd0ebc77a6d 100644
--- a/app/models/ci/catalog/resources/version.rb
+++ b/app/models/ci/catalog/resources/version.rb
@@ -6,6 +6,8 @@ module Ci
# This class represents a CI/CD Catalog resource version.
# Only versions which contain valid CI components are included in this table.
class Version < ::ApplicationRecord
+ include BulkInsertableAssociations
+
self.table_name = 'catalog_resource_versions'
belongs_to :release, inverse_of: :catalog_resource_version
@@ -14,6 +16,100 @@ module Ci
has_many :components, class_name: 'Ci::Catalog::Resources::Component', inverse_of: :version
validates :release, :catalog_resource, :project, presence: true
+
+ scope :for_catalog_resources, ->(catalog_resources) { where(catalog_resource_id: catalog_resources) }
+ scope :preloaded, -> { includes(:catalog_resource, project: [:route, { namespace: :route }], release: :author) }
+
+ scope :order_by_created_at_asc, -> { reorder(created_at: :asc) }
+ scope :order_by_created_at_desc, -> { reorder(created_at: :desc) }
+ # After we denormalize the `released_at` column, we won't need to use `joins(:release)` and keyset_order_*
+ scope :order_by_released_at_asc, -> { joins(:release).keyset_order_by_released_at_asc }
+ scope :order_by_released_at_desc, -> { joins(:release).keyset_order_by_released_at_desc }
+
+ delegate :name, :description, :tag, :sha, :released_at, :author_id, to: :release
+
+ class << self
+ # In the future, we should support semantic versioning.
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/427286
+ def latest
+ order_by_released_at_desc.first
+ end
+
+ # This query uses LATERAL JOIN to find the latest version for each catalog resource. To avoid
+ # joining the `catalog_resources` table, we build an in-memory table using the resource ids.
+ # Example:
+ # SELECT ...
+ # FROM (VALUES (CATALOG_RESOURCE_ID_1),(CATALOG_RESOURCE_ID_2)) catalog_resources (id)
+ # INNER JOIN LATERAL (...)
+ def latest_for_catalog_resources(catalog_resources)
+ return none if catalog_resources.empty?
+
+ catalog_resources_table = Ci::Catalog::Resource.arel_table
+ catalog_resources_id_list = catalog_resources.map { |resource| "(#{resource.id})" }.join(',')
+
+ # We need to use an alias for the `releases` table here so that it does not
+ # conflict with `joins(:release)` in the `order_by_released_at_*` scope.
+ join_query = Ci::Catalog::Resources::Version
+ .where(catalog_resources_table[:id].eq(arel_table[:catalog_resource_id]))
+ .joins("INNER JOIN releases AS rel ON rel.id = #{table_name}.release_id")
+ .order(Arel.sql('rel.released_at DESC'))
+ .limit(1)
+
+ Ci::Catalog::Resources::Version
+ .from("(VALUES #{catalog_resources_id_list}) #{catalog_resources_table.name} (id)")
+ .joins("INNER JOIN LATERAL (#{join_query.to_sql}) #{table_name} ON TRUE")
+ end
+
+ def keyset_order_by_released_at_asc
+ keyset_order = Gitlab::Pagination::Keyset::Order.build([
+ Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
+ attribute_name: :released_at,
+ column_expression: Release.arel_table[:released_at],
+ order_expression: Release.arel_table[:released_at].asc,
+ nullable: :not_nullable,
+ distinct: false
+ ),
+ Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
+ attribute_name: :id,
+ order_expression: Release.arel_table[:id].asc,
+ nullable: :not_nullable,
+ distinct: true
+ )
+ ])
+
+ reorder(keyset_order)
+ end
+
+ def keyset_order_by_released_at_desc
+ keyset_order = Gitlab::Pagination::Keyset::Order.build([
+ Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
+ attribute_name: :released_at,
+ column_expression: Release.arel_table[:released_at],
+ order_expression: Release.arel_table[:released_at].desc,
+ nullable: :not_nullable,
+ distinct: false
+ ),
+ Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
+ attribute_name: :id,
+ order_expression: Release.arel_table[:id].desc,
+ nullable: :not_nullable,
+ distinct: true
+ )
+ ])
+
+ reorder(keyset_order)
+ end
+
+ def order_by(order)
+ case order.to_s
+ when 'created_asc' then order_by_created_at_asc
+ when 'created_desc' then order_by_created_at_desc
+ when 'released_at_asc' then order_by_released_at_asc
+ else
+ order_by_released_at_desc
+ end
+ end
+ end
end
end
end
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index 2a346f97958..fe4437a4ad6 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -306,7 +306,7 @@ module Ci
end
def expired?
- expire_at.present? && expire_at < Time.current
+ expire_at.present? && expire_at.past?
end
def expiring?
diff --git a/app/models/ci/job_token/scope.rb b/app/models/ci/job_token/scope.rb
index f389c642fd8..17809ba20d3 100644
--- a/app/models/ci/job_token/scope.rb
+++ b/app/models/ci/job_token/scope.rb
@@ -54,6 +54,11 @@ module Ci
# if the setting is disabled any project is considered to be in scope.
return true unless current_project.ci_outbound_job_token_scope_enabled?
+ if !accessed_project.private? &&
+ Feature.enabled?(:restrict_ci_job_token_for_public_and_internal_projects, accessed_project)
+ return true
+ end
+
outbound_allowlist.includes?(accessed_project)
end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 0a876d26cc9..cf3efc5998f 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -30,9 +30,11 @@ module Ci
PROJECT_ROUTE_AND_NAMESPACE_ROUTE = {
project: [:project_feature, :route, { namespace: :route }]
}.freeze
- CONFIG_EXTENSION = '.gitlab-ci.yml'
- DEFAULT_CONFIG_PATH = CONFIG_EXTENSION
+
+ DEFAULT_CONFIG_PATH = '.gitlab-ci.yml'
+
CANCELABLE_STATUSES = (Ci::HasStatus::CANCELABLE_STATUSES + ['manual']).freeze
+ UNLOCKABLE_STATUSES = (Ci::Pipeline.completed_statuses + [:manual]).freeze
paginates_per 15
@@ -189,6 +191,7 @@ module Ci
# this is needed to ensure tests to be covered
transition [:running] => :running
+ transition [:waiting_for_callback] => :waiting_for_callback
end
event :request_resource do
@@ -203,6 +206,10 @@ module Ci
transition any - [:running] => :running
end
+ event :wait_for_callback do
+ transition any - [:waiting_for_callback] => :waiting_for_callback
+ end
+
event :skip do
transition any - [:skipped] => :skipped
end
@@ -266,6 +273,32 @@ module Ci
pipeline.run_after_commit { PipelineMetricsWorker.perform_async(pipeline.id) }
end
+ after_transition any => UNLOCKABLE_STATUSES do |pipeline|
+ # This is a temporary flag that we added just in case we need to totally
+ # stop unlocking pipelines due to unexpected issues during rollout.
+ next if Feature.enabled?(:ci_stop_unlock_pipelines, pipeline.project)
+
+ next unless Feature.enabled?(:ci_unlock_non_successful_pipelines, pipeline.project)
+
+ pipeline.run_after_commit do
+ Ci::Refs::UnlockPreviousPipelinesWorker.perform_async(pipeline.ci_ref_id)
+ end
+ end
+
+ # TODO: Remove this block once we've completed roll-out of ci_unlock_non_successful_pipelines
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/428408
+ after_transition any => :success do |pipeline|
+ # This is a temporary flag that we added just in case we need to totally
+ # stop unlocking pipelines due to unexpected issues during rollout.
+ next if Feature.enabled?(:ci_stop_unlock_pipelines, pipeline.project)
+
+ next unless Feature.disabled?(:ci_unlock_non_successful_pipelines, pipeline.project)
+
+ pipeline.run_after_commit do
+ Ci::Refs::UnlockPreviousPipelinesWorker.perform_async(pipeline.ci_ref_id)
+ end
+ end
+
after_transition [:created, :waiting_for_resource, :preparing, :pending, :running] => :success do |pipeline|
# We wait a little bit to ensure that all Ci::BuildFinishedWorkers finish first
# because this is where some metrics like code coverage is parsed and stored
@@ -380,7 +413,7 @@ module Ci
pipeline.run_after_commit do
next if pipeline.child?
- next unless project.only_allow_merge_if_pipeline_succeeds?(inherit_group_setting: true)
+ next unless Feature.enabled?(:widget_pipeline_pass_subscription_update, project) || project.only_allow_merge_if_pipeline_succeeds?(inherit_group_setting: true)
pipeline.all_merge_requests.opened.each do |merge_request|
GraphqlTriggers.merge_request_merge_status_updated(merge_request)
@@ -389,6 +422,7 @@ module Ci
end
end
+ scope :with_unlockable_status, -> { with_status(*UNLOCKABLE_STATUSES) }
scope :internal, -> { where(source: internal_sources) }
scope :no_child, -> { where.not(source: :parent_pipeline) }
scope :ci_sources, -> { where(source: Enums::Ci::Pipeline.ci_sources.values) }
@@ -554,7 +588,7 @@ module Ci
end
def self.bridgeable_statuses
- ::Ci::Pipeline::AVAILABLE_STATUSES - %w[created waiting_for_resource preparing pending]
+ ::Ci::Pipeline::AVAILABLE_STATUSES - %w[created waiting_for_resource waiting_for_callback preparing pending]
end
def self.auto_devops_pipelines_completed_total
@@ -850,6 +884,7 @@ module Ci
when 'created' then nil
when 'waiting_for_resource' then request_resource
when 'preparing' then prepare
+ when 'waiting_for_callback' then wait_for_callback
when 'pending' then enqueue
when 'running' then run
when 'success' then succeed
@@ -1366,11 +1401,6 @@ module Ci
merge_request.merge_request_diff_for(merge_request_diff_sha)
end
- def reduced_build_attributes_list_for_rules?
- ::Feature.enabled?(:reduced_build_attributes_list_for_rules, project)
- end
- strong_memoize_attr :reduced_build_attributes_list_for_rules?
-
private
def add_message(severity, content)
diff --git a/app/models/ci/ref.rb b/app/models/ci/ref.rb
index 8655e8eb9b8..e8ce58f2de5 100644
--- a/app/models/ci/ref.rb
+++ b/app/models/ci/ref.rb
@@ -30,15 +30,6 @@ module Ci
state :fixed, value: 3
state :broken, value: 4
state :still_failing, value: 5
-
- after_transition any => [:fixed, :success] do |ci_ref|
- # Do not try to unlock if no artifacts are locked
- next unless ci_ref.artifacts_locked?
-
- ci_ref.run_after_commit do
- Ci::Refs::UnlockPreviousPipelinesWorker.perform_async(ci_ref.id)
- end
- end
end
class << self
@@ -75,5 +66,13 @@ module Ci
self.status_name
end
end
+
+ def last_successful_ci_source_pipeline
+ pipelines.ci_sources.success.order(id: :desc).first
+ end
+
+ def last_unlockable_ci_source_pipeline
+ pipelines.ci_sources.with_unlockable_status.order(id: :desc).first
+ end
end
end
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index 91c919dc662..9c30beeeb59 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -123,6 +123,8 @@ module Ci
joins(:runner_namespaces).where(ci_runner_namespaces: { namespace_id: group_id })
}
+ scope :with_creator_id, -> (value) { where(creator_id: value) }
+
scope :belonging_to_group_or_project_descendants, -> (group_id) {
group_ids = Ci::NamespaceMirror.by_group_and_descendants(group_id).select(:namespace_id)
project_ids = Ci::ProjectMirror.by_namespace_id(group_ids).select(:project_id)
@@ -217,6 +219,8 @@ module Ci
validate :any_project, if: :project_type?
validate :exactly_one_group, if: :group_type?
+ scope :with_version_prefix, ->(value) { joins(:runner_managers).merge(RunnerManager.with_version_prefix(value)) }
+
acts_as_taggable
after_destroy :cleanup_runner_queue
diff --git a/app/models/ci/runner_manager.rb b/app/models/ci/runner_manager.rb
index 7d8fc097f51..e6576859827 100644
--- a/app/models/ci/runner_manager.rb
+++ b/app/models/ci/runner_manager.rb
@@ -62,6 +62,16 @@ module Ci
scope :order_id_desc, -> { order(id: :desc) }
+ scope :with_version_prefix, ->(value) do
+ regex = version_regex_expression_for_version(value)
+ value += '.' if regex.end_with?('\.') && !value.end_with?('.')
+ substring = Arel::Nodes::NamedFunction.new('substring', [
+ Ci::RunnerManager.arel_table[:version],
+ Arel.sql("'#{regex}'::text")
+ ])
+ where(substring.eq(sanitize_sql_like(value)))
+ end
+
scope :with_upgrade_status, ->(upgrade_status) do
joins(:runner_version).where(runner_version: { status: upgrade_status })
end
@@ -137,5 +147,16 @@ module Ci
Ci::Runners::ProcessRunnerVersionUpdateWorker.perform_async(new_version)
end
+
+ def self.version_regex_expression_for_version(version)
+ case version
+ when /\d+\.\d+\.\d+/
+ '^\d+\.\d+\.\d+'
+ when /\d+\.\d+(\.)?/
+ '^\d+\.\d+\.'
+ else
+ '^\d+\.'
+ end
+ end
end
end
diff --git a/app/models/ci/sources/pipeline.rb b/app/models/ci/sources/pipeline.rb
index 5b6946b04fd..475d57ee4c8 100644
--- a/app/models/ci/sources/pipeline.rb
+++ b/app/models/ci/sources/pipeline.rb
@@ -12,7 +12,7 @@ module Ci
:pipeline_id_convert_to_bigint, :source_pipeline_id_convert_to_bigint
], remove_with: '16.6', remove_after: '2023-10-22'
- columns_changing_default :partition_id
+ columns_changing_default :partition_id, :source_partition_id
self.table_name = "ci_sources_pipelines"
diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb
index 3a498972153..3d2df9a45ef 100644
--- a/app/models/ci/stage.rb
+++ b/app/models/ci/stage.rb
@@ -78,6 +78,10 @@ module Ci
transition any - [:running] => :running
end
+ event :wait_for_callback do
+ transition any - [:waiting_for_callback] => :waiting_for_callback
+ end
+
event :skip do
transition any - [:skipped] => :skipped
end
@@ -109,6 +113,7 @@ module Ci
when 'created' then nil
when 'waiting_for_resource' then request_resource
when 'preparing' then prepare
+ when 'waiting_for_callback' then wait_for_callback
when 'pending' then enqueue
when 'running' then run
when 'success' then succeed
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 39e12b53f21..886e6e9fbd7 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -372,9 +372,7 @@ class Commit
strong_memoize(:raw_signature_type) do
next unless @raw.instance_of?(Gitlab::Git::Commit)
- if raw_commit_from_rugged? && gpg_commit.signature_text.present?
- :PGP
- elsif defined? @raw.raw_commit.signature_type
+ if defined? @raw.raw_commit.signature_type
@raw.raw_commit.signature_type
end
end
@@ -397,10 +395,6 @@ class Commit
end
end
- def raw_commit_from_rugged?
- @raw.raw_commit.is_a?(Rugged::Commit)
- end
-
def gpg_commit
@gpg_commit ||= Gitlab::Gpg::Commit.new(self)
end
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 3761aa81bf7..9f77bd8ebe2 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -8,20 +8,24 @@ class CommitStatus < Ci::ApplicationRecord
include Presentable
include BulkInsertableAssociations
include TaggableQueries
-
- def self.switch_table_names
- if Gitlab::Utils.to_boolean(ENV['USE_CI_BUILDS_ROUTING_TABLE'])
- :p_ci_builds
- else
- :ci_builds
- end
- end
-
- self.table_name = self.switch_table_names
+ include IgnorableColumns
+
+ ignore_columns %i[
+ auto_canceled_by_id_convert_to_bigint
+ commit_id_convert_to_bigint
+ erased_by_id_convert_to_bigint
+ project_id_convert_to_bigint
+ runner_id_convert_to_bigint
+ trigger_request_id_convert_to_bigint
+ upstream_pipeline_id_convert_to_bigint
+ user_id_convert_to_bigint
+ ], remove_with: '17.0', remove_after: '2024-04-22'
+
+ self.table_name = :p_ci_builds
self.sequence_name = :ci_builds_id_seq
self.primary_key = :id
- partitionable scope: :pipeline
+ partitionable scope: :pipeline, partitioned: true
belongs_to :user
belongs_to :project
@@ -155,15 +159,15 @@ class CommitStatus < Ci::ApplicationRecord
end
event :drop do
- transition [:created, :waiting_for_resource, :preparing, :pending, :running, :manual, :scheduled] => :failed
+ transition [:created, :waiting_for_resource, :preparing, :waiting_for_callback, :pending, :running, :manual, :scheduled] => :failed
end
event :success do
- transition [:created, :waiting_for_resource, :preparing, :pending, :running] => :success
+ transition [:created, :waiting_for_resource, :preparing, :waiting_for_callback, :pending, :running] => :success
end
event :cancel do
- transition [:created, :waiting_for_resource, :preparing, :pending, :running, :manual, :scheduled] => :canceled
+ transition [:created, :waiting_for_resource, :preparing, :waiting_for_callback, :pending, :running, :manual, :scheduled] => :canceled
end
before_transition [:created, :waiting_for_resource, :preparing, :skipped, :manual, :scheduled] => :pending do |commit_status|
diff --git a/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb b/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb
index 1d9cf5729cd..dfcc905b3c3 100644
--- a/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb
+++ b/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb
@@ -1,4 +1,5 @@
# frozen_string_literal: true
+
module Analytics
module CycleAnalytics
module StageEventModel
diff --git a/app/models/concerns/can_move_repository_storage.rb b/app/models/concerns/can_move_repository_storage.rb
index 1132e4e79ac..1646ed3dc7c 100644
--- a/app/models/concerns/can_move_repository_storage.rb
+++ b/app/models/concerns/can_move_repository_storage.rb
@@ -9,6 +9,9 @@ module CanMoveRepositoryStorage
# progress beforehand. Setting a repository read-only will fail if it is
# already in that state.
#
+ # It is assumed that `with_lock` is used here to ensure that no race condition
+ # appears between reading and writing the read-only column.
+ #
# @return nil. Failures will raise an exception
def set_repository_read_only!(skip_git_transfer_check: false)
with_lock do
@@ -16,10 +19,10 @@ module CanMoveRepositoryStorage
!skip_git_transfer_check && git_transfer_in_progress?
raise RepositoryReadOnlyError, _('Repository already read-only') if
- _safe_read_repository_read_only_column
+ safe_read_repository_read_only_column
raise ActiveRecord::RecordNotSaved, _('Database update failed') unless
- _update_repository_read_only_column(true)
+ update_repository_read_only_column(true)
nil
end
@@ -28,12 +31,8 @@ module CanMoveRepositoryStorage
# Set repository as writable again. Unlike setting it read-only, this will
# succeed if the repository is already writable.
def set_repository_writable!
- with_lock do
- raise ActiveRecord::RecordNotSaved, _('Database update failed') unless
- _update_repository_read_only_column(false)
-
- nil
- end
+ raise ActiveRecord::RecordNotSaved, _('Database update failed') unless
+ update_repository_read_only_column(false)
end
def git_transfer_in_progress?
@@ -49,13 +48,13 @@ module CanMoveRepositoryStorage
# Not all resources that can move repositories have the `repository_read_only`
# in their table, for example groups. We need these methods to override the
# behavior in those classes in order to access the column.
- def _safe_read_repository_read_only_column
+ def safe_read_repository_read_only_column
# This was added originally this way because of
# https://gitlab.com/gitlab-org/gitlab/-/commit/43f9b98302d3985312c9f8b66018e2835d8293d2
self.class.where(id: id).pick(:repository_read_only)
end
- def _update_repository_read_only_column(value)
+ def update_repository_read_only_column(value)
update_column(:repository_read_only, value)
end
end
diff --git a/app/models/concerns/ci/has_status.rb b/app/models/concerns/ci/has_status.rb
index 2971ecb04b8..fb2b12e5f00 100644
--- a/app/models/concerns/ci/has_status.rb
+++ b/app/models/concerns/ci/has_status.rb
@@ -6,19 +6,20 @@ module Ci
DEFAULT_STATUS = 'created'
BLOCKED_STATUS = %w[manual scheduled].freeze
- AVAILABLE_STATUSES = %w[created waiting_for_resource preparing pending running success failed canceled skipped manual scheduled].freeze
+ AVAILABLE_STATUSES = %w[created waiting_for_resource preparing waiting_for_callback pending running success failed canceled skipped manual scheduled].freeze
STARTED_STATUSES = %w[running success failed].freeze
- ACTIVE_STATUSES = %w[waiting_for_resource preparing pending running].freeze
+ ACTIVE_STATUSES = %w[waiting_for_resource preparing waiting_for_callback pending running].freeze
COMPLETED_STATUSES = %w[success failed canceled skipped].freeze
STOPPED_STATUSES = COMPLETED_STATUSES + BLOCKED_STATUS
- ORDERED_STATUSES = %w[failed preparing pending running waiting_for_resource manual scheduled canceled success skipped created].freeze
+ ORDERED_STATUSES = %w[failed preparing pending running waiting_for_callback waiting_for_resource manual scheduled canceled success skipped created].freeze
PASSED_WITH_WARNINGS_STATUSES = %w[failed canceled].to_set.freeze
IGNORED_STATUSES = %w[manual].to_set.freeze
ALIVE_STATUSES = (ACTIVE_STATUSES + ['created']).freeze
CANCELABLE_STATUSES = (ALIVE_STATUSES + ['scheduled']).freeze
STATUSES_ENUM = { created: 0, pending: 1, running: 2, success: 3,
failed: 4, canceled: 5, skipped: 6, manual: 7,
- scheduled: 8, preparing: 9, waiting_for_resource: 10 }.freeze
+ scheduled: 8, preparing: 9, waiting_for_resource: 10,
+ waiting_for_callback: 11 }.freeze
UnknownStatusError = Class.new(StandardError)
@@ -58,6 +59,7 @@ module Ci
state :created, value: 'created'
state :waiting_for_resource, value: 'waiting_for_resource'
state :preparing, value: 'preparing'
+ state :waiting_for_callback, value: 'waiting_for_callback'
state :pending, value: 'pending'
state :running, value: 'running'
state :failed, value: 'failed'
@@ -72,6 +74,7 @@ module Ci
scope :waiting_for_resource, -> { with_status(:waiting_for_resource) }
scope :preparing, -> { with_status(:preparing) }
scope :relevant, -> { without_status(:created) }
+ scope :waiting_for_callback, -> { with_status(:waiting_for_callback) }
scope :running, -> { with_status(:running) }
scope :pending, -> { with_status(:pending) }
scope :success, -> { with_status(:success) }
diff --git a/app/models/concerns/commit_signature.rb b/app/models/concerns/commit_signature.rb
index 5bdf6bb31bf..201994cb321 100644
--- a/app/models/concerns/commit_signature.rb
+++ b/app/models/concerns/commit_signature.rb
@@ -1,4 +1,5 @@
# frozen_string_literal: true
+
module CommitSignature
extend ActiveSupport::Concern
diff --git a/app/models/concerns/diff_positionable_note.rb b/app/models/concerns/diff_positionable_note.rb
index 2f64129b65f..e799127d69a 100644
--- a/app/models/concerns/diff_positionable_note.rb
+++ b/app/models/concerns/diff_positionable_note.rb
@@ -1,4 +1,5 @@
# frozen_string_literal: true
+
module DiffPositionableNote
extend ActiveSupport::Concern
diff --git a/app/models/concerns/enums/package_metadata.rb b/app/models/concerns/enums/package_metadata.rb
index 3f107987ef6..352eb41829b 100644
--- a/app/models/concerns/enums/package_metadata.rb
+++ b/app/models/concerns/enums/package_metadata.rb
@@ -14,7 +14,8 @@ module Enums
apk: 9,
rpm: 10,
deb: 11,
- cbl_mariner: 12
+ 'cbl-mariner': 12,
+ wolfi: 13
}.with_indifferent_access.freeze
ADVISORY_SOURCES = {
diff --git a/app/models/concerns/enums/sbom.rb b/app/models/concerns/enums/sbom.rb
index 59aafc32d94..af8e37b4248 100644
--- a/app/models/concerns/enums/sbom.rb
+++ b/app/models/concerns/enums/sbom.rb
@@ -18,7 +18,8 @@ module Enums
apk: 9,
rpm: 10,
deb: 11,
- cbl_mariner: 12
+ 'cbl-mariner': 12,
+ wolfi: 13
}.with_indifferent_access.freeze
def self.component_types
diff --git a/app/models/concerns/merge_request_reviewer_state.rb b/app/models/concerns/merge_request_reviewer_state.rb
index 412b1da55da..e4ee6e7e58e 100644
--- a/app/models/concerns/merge_request_reviewer_state.rb
+++ b/app/models/concerns/merge_request_reviewer_state.rb
@@ -6,7 +6,8 @@ module MergeRequestReviewerState
included do
enum state: {
unreviewed: 0,
- reviewed: 1
+ reviewed: 1,
+ requested_changes: 2
}
validates :state,
diff --git a/app/models/concerns/repository_storage_movable.rb b/app/models/concerns/repository_storage_movable.rb
index 77edabb9706..b1dbebff4fb 100644
--- a/app/models/concerns/repository_storage_movable.rb
+++ b/app/models/concerns/repository_storage_movable.rb
@@ -6,6 +6,9 @@ module RepositoryStorageMovable
included do
scope :order_created_at_desc, -> { order(created_at: :desc) }
+ scope :scheduled_or_started, -> do
+ where(state: [state_machine.states[:scheduled].value, state_machine.states[:started].value])
+ end
validates :container, presence: true
validates :state, presence: true
@@ -43,6 +46,8 @@ module RepositoryStorageMovable
transition replicated: :cleanup_failed
end
+ # An after_transition can't affect the success of the transition.
+ # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45160#note_431071664
around_transition initial: :scheduled do |storage_move, block|
block.call
@@ -61,13 +66,9 @@ module RepositoryStorageMovable
true
end
- before_transition started: :replicated do |storage_move|
+ after_transition started: :replicated do |storage_move|
storage_move.container.set_repository_writable!
- storage_move.update_repository_storage(storage_move.destination_storage_name)
- end
-
- after_transition started: :replicated do |storage_move|
# We have several scripts in place that replicate some statistics information
# to other databases. Some of them depend on the updated_at column
# to identify the models they need to extract.
@@ -83,6 +84,13 @@ module RepositoryStorageMovable
storage_move.container.set_repository_writable!
end
+ # This callback ensures the repository is set to writable in the event of
+ # a connection error during the :started -> :replicated transition
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/427254#note_1636072125
+ before_transition replicated: :cleanup_failed do |storage_move|
+ storage_move.container.set_repository_writable!
+ end
+
state :initial, value: 1
state :scheduled, value: 2
state :started, value: 3
@@ -93,15 +101,6 @@ module RepositoryStorageMovable
end
end
- # Projects, snippets, and group wikis has different db structure. In projects,
- # we need to update some columns in this step, but we don't with the other resources.
- #
- # Therefore, we create this No-op method for snippets and wikis and let project
- # overwrite it in their implementation.
- def update_repository_storage(new_storage)
- # No-op
- end
-
def schedule_repository_storage_update_worker
raise NotImplementedError
end
diff --git a/app/models/concerns/restricted_signup.rb b/app/models/concerns/restricted_signup.rb
index 6af9ede5e8b..87b62214529 100644
--- a/app/models/concerns/restricted_signup.rb
+++ b/app/models/concerns/restricted_signup.rb
@@ -1,4 +1,5 @@
# frozen_string_literal: true
+
module RestrictedSignup
extend ActiveSupport::Concern
diff --git a/app/models/concerns/token_authenticatable_strategies/base.rb b/app/models/concerns/token_authenticatable_strategies/base.rb
index d0085b60d98..b25ee434484 100644
--- a/app/models/concerns/token_authenticatable_strategies/base.rb
+++ b/app/models/concerns/token_authenticatable_strategies/base.rb
@@ -65,7 +65,7 @@ module TokenAuthenticatableStrategies
return false unless expirable? && token_expiration_enforced?
exp = expires_at(instance)
- !!exp && Time.current > exp
+ !!exp && exp.past?
end
def expirable?
diff --git a/app/models/concerns/use_sql_function_for_primary_key_lookups.rb b/app/models/concerns/use_sql_function_for_primary_key_lookups.rb
new file mode 100644
index 00000000000..c3ca3cfc038
--- /dev/null
+++ b/app/models/concerns/use_sql_function_for_primary_key_lookups.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+module UseSqlFunctionForPrimaryKeyLookups
+ extend ActiveSupport::Concern
+
+ class_methods do
+ def find(*args)
+ return super unless Feature.enabled?(:use_sql_functions_for_primary_key_lookups, Feature.current_request)
+ return super unless args.one?
+ return super if block_given? || primary_key.nil? || scope_attributes?
+
+ return_array = false
+ id = args.first
+
+ if id.is_a?(Array)
+ return super if id.many?
+
+ return_array = true
+
+ id = id.first
+ end
+
+ return super if id.nil? || (id.is_a?(String) && !id.number?)
+
+ from_clause = "find_#{table_name}_by_id(?) #{quoted_table_name}"
+ filter_empty_row = "#{quoted_table_name}.#{connection.quote_column_name(primary_key)} IS NOT NULL"
+ query = from(from_clause).where(filter_empty_row).limit(1).to_sql
+ # Using find_by_sql so we get query cache working
+ record = find_by_sql([query, id]).first
+
+ unless record
+ message = "Couldn't find #{name} with '#{primary_key}'=#{id}"
+ raise(ActiveRecord::RecordNotFound.new(message, name, primary_key, id))
+ end
+
+ return_array ? [record] : record
+ end
+ end
+end
diff --git a/app/models/concerns/users/visitable.rb b/app/models/concerns/users/visitable.rb
index cb8e5fdc682..029d60d61ee 100644
--- a/app/models/concerns/users/visitable.rb
+++ b/app/models/concerns/users/visitable.rb
@@ -13,6 +13,45 @@ module Users
time = time.to_datetime
where(entity_id: entity_id, user_id: user_id, visited_at: (time - 15.minutes)..(time + 15.minutes))
end
+
+ scope :for_user, ->(user_id) { where(user_id: user_id) }
+
+ scope :recently_visited, -> do
+ where('visited_at > ?', 3.months.ago)
+ .where('visited_at <= ?', Time.current)
+ end
+
+ def self.grouped_by_week_start_and_entity_for_user(user_id:)
+ recently_visited
+ .for_user(user_id)
+ .group(:week_start, :entity_id)
+ .select(
+ :entity_id,
+ "COUNT(entity_id) AS week_count",
+ "DATE_TRUNC('week', visited_at)::date AS week_start",
+ "DENSE_RANK() OVER (ORDER BY DATE_TRUNC('week', visited_at)::date)"
+ )
+ end
+
+ def self.frecent_visits_scores(user_id:, limit:)
+ ranked_entity_visits_query = grouped_by_week_start_and_entity_for_user(user_id: user_id).to_sql
+ sql = <<~SQL
+ SELECT
+ entity_id,
+ SUM(week_count * dense_rank) AS score
+ FROM
+ (#{ranked_entity_visits_query}) as ranked_entity_visits
+ GROUP BY
+ entity_id
+ ORDER BY
+ score DESC
+ LIMIT #{limit}
+ SQL
+
+ ::Gitlab::Database::LoadBalancing::Session.current.fallback_to_replicas_for_ambiguous_queries do
+ connection.execute(sql).to_a
+ end
+ end
end
end
end
diff --git a/app/models/container_repository.rb b/app/models/container_repository.rb
index 6a52f6a0112..15ed517dc12 100644
--- a/app/models/container_repository.rb
+++ b/app/models/container_repository.rb
@@ -482,6 +482,24 @@ class ContainerRepository < ApplicationRecord
raise 'too many pages requested' if page_count >= MAX_TAGS_PAGES
end
+ def tags_page(before: nil, last: nil, sort: nil, name: nil, page_size: 100)
+ raise ArgumentError, 'not a migrated repository' unless migrated?
+
+ page = gitlab_api_client.tags(
+ self.path,
+ page_size: page_size,
+ before: before,
+ last: last,
+ sort: sort,
+ name: name
+ )
+
+ {
+ tags: transform_tags_page(page[:response_body]),
+ pagination: page[:pagination]
+ }
+ end
+
def tags_count
return 0 unless manifest && manifest['tags']
@@ -505,15 +523,11 @@ class ContainerRepository < ApplicationRecord
digests = tags.map { |tag| tag.digest }.compact.to_set
- digests.map { |digest| delete_tag_by_digest(digest) }.all?
- end
-
- def delete_tag_by_digest(digest)
- client.delete_repository_tag_by_digest(self.path, digest)
+ digests.map { |digest| delete_tag(digest) }.all?
end
- def delete_tag_by_name(name)
- client.delete_repository_tag_by_name(self.path, name)
+ def delete_tag(name_or_digest)
+ client.delete_repository_tag_by_digest(self.path, name_or_digest)
end
def start_expiration_policy!
@@ -640,6 +654,9 @@ class ContainerRepository < ApplicationRecord
tag = ContainerRegistry::Tag.new(self, raw_tag['name'])
tag.force_created_at_from_iso8601(raw_tag['created_at'])
tag.updated_at = raw_tag['updated_at']
+ tag.total_size = raw_tag['size_bytes']
+ tag.manifest_digest = raw_tag['digest']
+ tag.revision = raw_tag['config_digest'].to_s.split(':')[1]
tag
end
end
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index 0bdce18bab5..f0093445ba8 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -8,12 +8,15 @@ class Deployment < ApplicationRecord
include Importable
include Gitlab::Utils::StrongMemoize
include FastDestroyAll
+ include IgnorableColumns
StatusUpdateError = Class.new(StandardError)
StatusSyncError = Class.new(StandardError)
ARCHIVABLE_OFFSET = 50_000
+ ignore_column :cluster_id, remove_with: '16.8', remove_after: '2023-12-21'
+
belongs_to :project, optional: false
belongs_to :environment, optional: false
belongs_to :user
diff --git a/app/models/environment.rb b/app/models/environment.rb
index efdcf7174aa..4f76fae24eb 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -8,6 +8,8 @@ class Environment < ApplicationRecord
include NullifyIfBlank
include FromUnion
+ LONG_STOP = 1.week
+
self.reactive_cache_refresh_interval = 1.minute
self.reactive_cache_lifetime = 55.seconds
self.reactive_cache_hard_limit = 10.megabytes
@@ -89,6 +91,7 @@ class Environment < ApplicationRecord
delegate :auto_rollback_enabled?, to: :project
scope :available, -> { with_state(:available) }
+ scope :active, -> { with_state(:available, :stopping) }
scope :stopped, -> { with_state(:stopped) }
scope :order_by_last_deployed_at, -> do
@@ -104,6 +107,7 @@ class Environment < ApplicationRecord
scope :preload_project, -> { preload(:project) }
scope :auto_stoppable, -> (limit) { available.where('auto_stop_at < ?', Time.zone.now).limit(limit) }
scope :auto_deletable, -> (limit) { stopped.where('auto_delete_at < ?', Time.zone.now).limit(limit) }
+ scope :long_stopping, -> { with_state(:stopping).where('updated_at < ?', LONG_STOP.ago) }
scope :deployed_and_updated_before, -> (project_id, before) do
# this query joins deployments and filters out any environment that has recent deployments
@@ -322,6 +326,10 @@ class Environment < ApplicationRecord
last_deployment.try(:created_at)
end
+ def long_stopping?
+ stopping? && self.updated_at < LONG_STOP.ago
+ end
+
def ref_path
"refs/#{Repository::REF_ENVIRONMENTS}/#{slug}"
end
diff --git a/app/models/group.rb b/app/models/group.rb
index c83dd24e98e..51c26767569 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -300,14 +300,15 @@ class Group < Namespace
groups.drop(1).each { |group| group.root_ancestor = root }
end
- # Returns the ids of the passed group models where the `emails_disabled`
- # column is set to true anywhere in the ancestor hierarchy.
+ # Returns the ids of the passed group models where the `emails_enabled`
+ # column is set to false anywhere in the ancestor hierarchy.
def ids_with_disabled_email(groups)
inner_groups = Group.where('id = namespaces_with_emails_disabled.id')
inner_query = inner_groups
.self_and_ancestors
- .where(emails_disabled: true)
+ .joins(:namespace_settings)
+ .where(namespace_settings: { emails_enabled: false })
.select('1')
.limit(1)
@@ -593,40 +594,13 @@ class Group < Namespace
end
def authorizable_members_with_parents
- source_ids =
- if has_parent?
- self_and_ancestors.reorder(nil).select(:id)
- else
- id
- end
-
- group_hierarchy_members = GroupMember.where(source_id: source_ids).select(*GroupMember.cached_column_list)
-
- GroupMember.from_union([group_hierarchy_members,
- members_from_self_and_ancestor_group_shares]).authorizable
+ Members::MembersWithParents.new(self).all_members.authorizable
end
def members_with_parents(only_active_users: true)
- # Avoids an unnecessary SELECT when the group has no parents
- source_ids =
- if has_parent?
- self_and_ancestors.reorder(nil).select(:id)
- else
- id
- end
-
- group_hierarchy_members = GroupMember.non_minimal_access
- .where(source_id: source_ids)
- .select(*GroupMember.cached_column_list)
-
- group_hierarchy_members = if only_active_users
- group_hierarchy_members.active_without_invites_and_requests
- else
- group_hierarchy_members.without_invites_and_requests
- end
-
- GroupMember.from_union([group_hierarchy_members,
- members_from_self_and_ancestor_group_shares])
+ Members::MembersWithParents
+ .new(self)
+ .members(active_users: only_active_users)
end
def members_from_self_and_ancestors_with_effective_access_level
@@ -671,15 +645,6 @@ class Group < Namespace
members.count
end
- # Returns all users that are members of projects
- # belonging to the current group or sub-groups
- def project_users_with_descendants
- User
- .joins(projects: :group)
- .where(namespaces: { id: self_and_descendants.select(:id) })
- .allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/417455")
- end
-
# Return the highest access level for a user
#
# A special case is handled here when the user is a GitLab admin
@@ -996,48 +961,6 @@ class Group < Namespace
errors.add(:require_two_factor_authentication, _('is forbidden by a top-level group'))
end
- def members_from_self_and_ancestor_group_shares
- group_group_link_table = GroupGroupLink.arel_table
- group_member_table = GroupMember.arel_table
-
- source_ids =
- if has_parent?
- self_and_ancestors.reorder(nil).select(:id)
- else
- id
- end
-
- group_group_links_query = GroupGroupLink.where(shared_group_id: source_ids)
- cte = Gitlab::SQL::CTE.new(:group_group_links_cte, group_group_links_query)
- cte_alias = cte.table.alias(GroupGroupLink.table_name)
-
- # Instead of members.access_level, we need to maximize that access_level at
- # the respective group_group_links.group_access.
- member_columns = GroupMember.attribute_names.map do |column_name|
- if column_name == 'access_level'
- smallest_value_arel([cte_alias[:group_access], group_member_table[:access_level]], 'access_level')
- else
- group_member_table[column_name]
- end
- end
-
- GroupMember
- .with(cte.to_arel)
- .select(*member_columns)
- .from([group_member_table, cte.alias_to(group_group_link_table)])
- .where(group_member_table[:requested_at].eq(nil))
- .where(group_member_table[:source_id].eq(group_group_link_table[:shared_with_group_id]))
- .where(group_member_table[:source_type].eq('Namespace'))
- .where(group_member_table[:state].eq(::Member::STATE_ACTIVE))
- .non_minimal_access
- end
-
- def smallest_value_arel(args, column_alias)
- Arel::Nodes::As.new(
- Arel::Nodes::NamedFunction.new('LEAST', args),
- Arel::Nodes::SqlLiteral.new(column_alias))
- end
-
def runners_token_prefix
RunnersTokenPrefixable::RUNNERS_TOKEN_PREFIX
end
diff --git a/app/models/guest.rb b/app/models/guest.rb
deleted file mode 100644
index 9c8097e1ac8..00000000000
--- a/app/models/guest.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# frozen_string_literal: true
-
-class Guest
- class << self
- def can?(action, subject = :global)
- Ability.allowed?(nil, action, subject)
- end
- end
-end
diff --git a/app/models/integration.rb b/app/models/integration.rb
index b4408301c6d..7c14c1b1716 100644
--- a/app/models/integration.rb
+++ b/app/models/integration.rb
@@ -237,6 +237,18 @@ class Integration < ApplicationRecord
end
private_class_method :boolean_accessor
+ def self.title
+ raise NotImplementedError
+ end
+
+ def self.description
+ raise NotImplementedError
+ end
+
+ def self.help
+ # no-op
+ end
+
def self.to_param
raise NotImplementedError
end
@@ -447,19 +459,18 @@ class Integration < ApplicationRecord
end
def title
- # implement inside child
+ self.class.title
end
def description
- # implement inside child
+ self.class.description
end
def help
- # implement inside child
+ self.class.help
end
def to_param
- # implement inside child
self.class.to_param
end
@@ -588,7 +599,7 @@ class Integration < ApplicationRecord
return if ::Gitlab::SilentMode.enabled?
return unless supported_events.include?(data[:object_kind])
- Integrations::ExecuteWorker.perform_async(id, data)
+ Integrations::ExecuteWorker.perform_async(id, data.deep_stringify_keys)
end
# override if needed
diff --git a/app/models/integrations/apple_app_store.rb b/app/models/integrations/apple_app_store.rb
index ef12fc6bf6f..f8fddf8a457 100644
--- a/app/models/integrations/apple_app_store.rb
+++ b/app/models/integrations/apple_app_store.rb
@@ -37,15 +37,15 @@ module Integrations
title: -> { s_('AppleAppStore|Protected branches and tags only') },
checkbox_label: -> { s_('AppleAppStore|Only set variables on protected branches and tags') }
- def title
+ def self.title
'Apple App Store Connect'
end
- def description
+ def self.description
s_('AppleAppStore|Use GitLab to build and release an app in the Apple App Store.')
end
- def help
+ def self.help
variable_list = [
'<code>APP_STORE_CONNECT_API_KEY_ISSUER_ID</code>',
'<code>APP_STORE_CONNECT_API_KEY_KEY_ID</code>',
diff --git a/app/models/integrations/asana.rb b/app/models/integrations/asana.rb
index 77555996cd9..39407acd6c9 100644
--- a/app/models/integrations/asana.rb
+++ b/app/models/integrations/asana.rb
@@ -20,15 +20,15 @@ module Integrations
title: -> { s_('Integrations|Restrict to branch (optional)') },
help: -> { s_('AsanaService|Comma-separated list of branches to be automatically inspected. Leave blank to include all branches.') }
- def title
+ def self.title
'Asana'
end
- def description
+ def self.description
s_('AsanaService|Add commit messages as comments to Asana tasks.')
end
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/asana'), target: '_blank', rel: 'noopener noreferrer'
s_('Add commit messages as comments to Asana tasks. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
end
diff --git a/app/models/integrations/assembla.rb b/app/models/integrations/assembla.rb
index 1d3616b4c3b..bbdd0e183f2 100644
--- a/app/models/integrations/assembla.rb
+++ b/app/models/integrations/assembla.rb
@@ -15,11 +15,11 @@ module Integrations
exposes_secrets: true,
placeholder: ''
- def title
+ def self.title
'Assembla'
end
- def description
+ def self.description
_('Manage projects.')
end
diff --git a/app/models/integrations/bamboo.rb b/app/models/integrations/bamboo.rb
index 9f15532a0b0..9fe73f86be3 100644
--- a/app/models/integrations/bamboo.rb
+++ b/app/models/integrations/bamboo.rb
@@ -38,15 +38,15 @@ module Integrations
attr_accessor :response
- def title
+ def self.title
s_('BambooService|Atlassian Bamboo')
end
- def description
+ def self.description
s_('BambooService|Run CI/CD pipelines with Atlassian Bamboo.')
end
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to(
_('Learn more.'),
Rails.application.routes.url_helpers.help_page_url('user/project/integrations/bamboo'),
diff --git a/app/models/integrations/base_chat_notification.rb b/app/models/integrations/base_chat_notification.rb
index b75801335bd..167bc210349 100644
--- a/app/models/integrations/base_chat_notification.rb
+++ b/app/models/integrations/base_chat_notification.rb
@@ -136,10 +136,6 @@ module Integrations
raise NotImplementedError
end
- def help
- raise NotImplementedError
- end
-
# With some integrations the webhook is already tied to a specific channel,
# for others the channels are configurable for each event.
def configurable_channels?
diff --git a/app/models/integrations/base_slack_notification.rb b/app/models/integrations/base_slack_notification.rb
index 09a0c9ba361..33dd9d9d387 100644
--- a/app/models/integrations/base_slack_notification.rb
+++ b/app/models/integrations/base_slack_notification.rb
@@ -36,7 +36,7 @@ module Integrations
true
end
- def help
+ def self.help
# noop
end
diff --git a/app/models/integrations/bugzilla.rb b/app/models/integrations/bugzilla.rb
index 74e282f6848..3ca348e42a1 100644
--- a/app/models/integrations/bugzilla.rb
+++ b/app/models/integrations/bugzilla.rb
@@ -6,15 +6,15 @@ module Integrations
validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated?
- def title
+ def self.title
'Bugzilla'
end
- def description
+ def self.description
s_("IssueTracker|Use Bugzilla as this project's issue tracker.")
end
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/bugzilla'), target: '_blank', rel: 'noopener noreferrer'
s_("IssueTracker|Use Bugzilla as this project's issue tracker. %{docs_link}").html_safe % { docs_link: docs_link.html_safe }
end
diff --git a/app/models/integrations/buildkite.rb b/app/models/integrations/buildkite.rb
index 82a5142e8c2..aab0cdf2134 100644
--- a/app/models/integrations/buildkite.rb
+++ b/app/models/integrations/buildkite.rb
@@ -75,20 +75,20 @@ module Integrations
"#{project_url}/builds?commit=#{sha}"
end
- def title
+ def self.title
'Buildkite'
end
- def description
+ def self.description
'Run CI/CD pipelines with Buildkite.'
end
- def self.to_param
- 'buildkite'
+ def self.help
+ s_('ProjectService|Run CI/CD pipelines with Buildkite.')
end
- def help
- s_('ProjectService|Run CI/CD pipelines with Buildkite.')
+ def self.to_param
+ 'buildkite'
end
def calculate_reactive_cache(sha, ref)
diff --git a/app/models/integrations/campfire.rb b/app/models/integrations/campfire.rb
index 8b5797a9d24..18268ed18f4 100644
--- a/app/models/integrations/campfire.rb
+++ b/app/models/integrations/campfire.rb
@@ -36,15 +36,15 @@ module Integrations
placeholder: '123456',
help: -> { s_('CampfireService|From the end of the room URL.') }
- def title
+ def self.title
'Campfire'
end
- def description
+ def self.description
'Send notifications about push events to Campfire chat rooms.'
end
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to(
_('Learn more.'),
Rails.application.routes.url_helpers.help_page_url('api/integrations', anchor: 'campfire'),
diff --git a/app/models/integrations/clickup.rb b/app/models/integrations/clickup.rb
index 7cc05d41e14..25287b53300 100644
--- a/app/models/integrations/clickup.rb
+++ b/app/models/integrations/clickup.rb
@@ -10,15 +10,15 @@ module Integrations
@reference_pattern ||= /((#|CU-)(?<issue>[a-z0-9]+)|(?<issue>[A-Z0-9_]{2,10}-\d+))\b/
end
- def title
+ def self.title
'ClickUp'
end
- def description
+ def self.description
s_("IssueTracker|Use Clickup as this project's issue tracker.")
end
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to _('Learn more.'),
Rails.application.routes.url_helpers.help_page_url('user/project/integrations/clickup'),
target: '_blank',
diff --git a/app/models/integrations/confluence.rb b/app/models/integrations/confluence.rb
index eda8c37fc72..f97f1fd25c9 100644
--- a/app/models/integrations/confluence.rb
+++ b/app/models/integrations/confluence.rb
@@ -22,11 +22,11 @@ module Integrations
'confluence'
end
- def title
+ def self.title
s_('ConfluenceService|Confluence Workspace')
end
- def description
+ def self.description
s_('ConfluenceService|Link to a Confluence Workspace from the sidebar.')
end
diff --git a/app/models/integrations/custom_issue_tracker.rb b/app/models/integrations/custom_issue_tracker.rb
index 3770e813eaa..fe0d01d60bd 100644
--- a/app/models/integrations/custom_issue_tracker.rb
+++ b/app/models/integrations/custom_issue_tracker.rb
@@ -6,15 +6,15 @@ module Integrations
validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated?
- def title
+ def self.title
s_('IssueTracker|Custom issue tracker')
end
- def description
+ def self.description
s_("IssueTracker|Use a custom issue tracker as this project's issue tracker.")
end
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/custom_issue_tracker'), target: '_blank', rel: 'noopener noreferrer'
s_('IssueTracker|Use a custom issue tracker that is not in the integration list. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
end
diff --git a/app/models/integrations/datadog.rb b/app/models/integrations/datadog.rb
index b1f1361afcd..5682fc2b139 100644
--- a/app/models/integrations/datadog.rb
+++ b/app/models/integrations/datadog.rb
@@ -117,15 +117,15 @@ module Integrations
# archive_trace is opt-in but we handle it with a more detailed field below
end
- def title
+ def self.title
'Datadog'
end
- def description
+ def self.description
s_('DatadogIntegration|Trace your GitLab pipelines with Datadog.')
end
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to(
s_('DatadogIntegration|How do I set up this integration?'),
Rails.application.routes.url_helpers.help_page_url('integration/datadog'),
diff --git a/app/models/integrations/discord.rb b/app/models/integrations/discord.rb
index 33b2b52fa62..7ce597389f0 100644
--- a/app/models/integrations/discord.rb
+++ b/app/models/integrations/discord.rb
@@ -21,23 +21,23 @@ module Integrations
title: -> { s_('Integrations|Branches for which notifications are to be sent') },
choices: -> { branch_choices }
- def title
+ def self.title
s_("DiscordService|Discord Notifications")
end
- def description
+ def self.description
s_("DiscordService|Send notifications about project events to a Discord channel.")
end
- def self.to_param
- "discord"
- end
-
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to _('How do I set up this service?'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/discord_notifications'), target: '_blank', rel: 'noopener noreferrer'
s_('Send notifications about project events to a Discord channel. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
end
+ def self.to_param
+ "discord"
+ end
+
def default_channel_placeholder
s_('DiscordService|Override the default webhook (e.g. https://discord.com/api/webhooks/…)')
end
diff --git a/app/models/integrations/drone_ci.rb b/app/models/integrations/drone_ci.rb
index f6a12c4bb1a..b59e504c98f 100644
--- a/app/models/integrations/drone_ci.rb
+++ b/app/models/integrations/drone_ci.rb
@@ -87,20 +87,20 @@ module Integrations
"gitlab/#{project.full_path}/redirect/commits/#{sha}?branch=#{Addressable::URI.encode_component(ref.to_s)}")
end
- def title
+ def self.title
'Drone'
end
- def description
+ def self.description
s_('ProjectService|Run CI/CD pipelines with Drone.')
end
- def self.to_param
- 'drone_ci'
+ def self.help
+ s_('ProjectService|Run CI/CD pipelines with Drone.')
end
- def help
- s_('ProjectService|Run CI/CD pipelines with Drone.')
+ def self.to_param
+ 'drone_ci'
end
override :hook_url
diff --git a/app/models/integrations/emails_on_push.rb b/app/models/integrations/emails_on_push.rb
index 144d1a07b04..77be8f5db45 100644
--- a/app/models/integrations/emails_on_push.rb
+++ b/app/models/integrations/emails_on_push.rb
@@ -39,11 +39,11 @@ module Integrations
recipients.split.grep(Devise.email_regexp).uniq(&:downcase)
end
- def title
+ def self.title
s_('EmailsOnPushService|Emails on push')
end
- def description
+ def self.description
s_('EmailsOnPushService|Email the commits and diff of each push to a list of recipients.')
end
diff --git a/app/models/integrations/ewm.rb b/app/models/integrations/ewm.rb
index 003c896704a..9d6f4c2a56c 100644
--- a/app/models/integrations/ewm.rb
+++ b/app/models/integrations/ewm.rb
@@ -10,15 +10,15 @@ module Integrations
@reference_pattern ||= %r{(?<issue>\b(bug|task|work item|workitem|rtcwi|defect)\b\s+\d+)}i
end
- def title
+ def self.title
'EWM'
end
- def description
+ def self.description
s_("IssueTracker|Use IBM Engineering Workflow Management as this project's issue tracker.")
end
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/ewm'), target: '_blank', rel: 'noopener noreferrer'
s_("IssueTracker|Use IBM Engineering Workflow Management as this project's issue tracker. %{docs_link}").html_safe % { docs_link: docs_link.html_safe }
end
diff --git a/app/models/integrations/external_wiki.rb b/app/models/integrations/external_wiki.rb
index acacab2528e..7408f86d231 100644
--- a/app/models/integrations/external_wiki.rb
+++ b/app/models/integrations/external_wiki.rb
@@ -11,24 +11,24 @@ module Integrations
help: -> { s_('ExternalWikiService|Enter the URL to the external wiki.') },
required: true
- def title
+ def self.title
s_('ExternalWikiService|External wiki')
end
- def description
+ def self.description
s_('ExternalWikiService|Link to an external wiki from the sidebar.')
end
- def self.to_param
- 'external_wiki'
- end
-
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/wiki/index', anchor: 'link-an-external-wiki'), target: '_blank', rel: 'noopener noreferrer'
s_('Link an external wiki from the project\'s sidebar. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
end
+ def self.to_param
+ 'external_wiki'
+ end
+
def sections
[
{
diff --git a/app/models/integrations/gitlab_slack_application.rb b/app/models/integrations/gitlab_slack_application.rb
index 2d520eaf7e7..d008a28a226 100644
--- a/app/models/integrations/gitlab_slack_application.rb
+++ b/app/models/integrations/gitlab_slack_application.rb
@@ -26,11 +26,11 @@ module Integrations
update(active: !!slack_integration)
end
- def title
+ def self.title
s_('Integrations|GitLab for Slack app')
end
- def description
+ def self.description
s_('Integrations|Enable slash commands and notifications for a Slack workspace.')
end
diff --git a/app/models/integrations/google_play.rb b/app/models/integrations/google_play.rb
index 5389e8dfa81..746f68fdc4c 100644
--- a/app/models/integrations/google_play.rb
+++ b/app/models/integrations/google_play.rb
@@ -32,15 +32,15 @@ module Integrations
title: -> { s_('GooglePlayStore|Protected branches and tags only') },
checkbox_label: -> { s_('GooglePlayStore|Only set variables on protected branches and tags') }
- def title
+ def self.title
s_('GooglePlay|Google Play')
end
- def description
+ def self.description
s_('GooglePlay|Use GitLab to build and release an app in Google Play.')
end
- def help
+ def self.help
variable_list = [
'<code>SUPPLY_PACKAGE_NAME</code>',
'<code>SUPPLY_JSON_KEY_DATA</code>'
diff --git a/app/models/integrations/hangouts_chat.rb b/app/models/integrations/hangouts_chat.rb
index 6e4753470a3..6a9d603e6e5 100644
--- a/app/models/integrations/hangouts_chat.rb
+++ b/app/models/integrations/hangouts_chat.rb
@@ -17,11 +17,11 @@ module Integrations
title: -> { s_('Integrations|Branches for which notifications are to be sent') },
choices: -> { branch_choices }
- def title
+ def self.title
'Google Chat'
end
- def description
+ def self.description
'Send notifications from GitLab to a room in Google Chat.'
end
@@ -29,7 +29,7 @@ module Integrations
'hangouts_chat'
end
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to(_('How do I set up a Google Chat webhook?'),
Rails.application.routes.url_helpers.help_page_url('user/project/integrations/hangouts_chat'),
target: '_blank', rel: 'noopener noreferrer')
diff --git a/app/models/integrations/harbor.rb b/app/models/integrations/harbor.rb
index 559e48afd10..cc570e49e36 100644
--- a/app/models/integrations/harbor.rb
+++ b/app/models/integrations/harbor.rb
@@ -32,34 +32,32 @@ module Integrations
non_empty_password_help: -> { s_('HarborIntegration|Leave blank to use your current password.') },
required: true
- def title
+ def self.title
'Harbor'
end
- def description
+ def self.description
s_("HarborIntegration|Use Harbor as this project's container registry.")
end
- def help
+ def self.help
s_("HarborIntegration|After the Harbor integration is activated, global variables `$HARBOR_USERNAME`, `$HARBOR_HOST`, `$HARBOR_OCI`, `$HARBOR_PASSWORD`, `$HARBOR_URL` and `$HARBOR_PROJECT` will be created for CI/CD use.")
end
+ def self.to_param
+ name.demodulize.downcase
+ end
+
def hostname
Gitlab::Utils.parse_url(url).hostname
end
- class << self
- def to_param
- name.demodulize.downcase
- end
-
- def supported_events
- []
- end
+ def self.supported_events
+ []
+ end
- def supported_event_actions
- []
- end
+ def self.supported_event_actions
+ []
end
def test(*_args)
diff --git a/app/models/integrations/irker.rb b/app/models/integrations/irker.rb
index a54946f074a..a1ce0877957 100644
--- a/app/models/integrations/irker.rb
+++ b/app/models/integrations/irker.rb
@@ -53,14 +53,31 @@ module Integrations
# in the UI or API.
prop_accessor :channels
- def title
+ def self.title
s_('IrkerService|irker (IRC gateway)')
end
- def description
+ def self.description
s_('IrkerService|Send update messages to an irker server.')
end
+ def self.help
+ docs_link = ActionController::Base.helpers.link_to(
+ _('Learn more.'),
+ Rails.application.routes.url_helpers.help_page_url(
+ 'user/project/integrations/irker',
+ anchor: 'set-up-an-irker-daemon'
+ ),
+ target: '_blank',
+ rel: 'noopener noreferrer'
+ )
+
+ format(s_(
+ 'IrkerService|Send update messages to an irker server. ' \
+ 'Before you can use this, you need to set up the irker daemon. %{docs_link}'
+ ).html_safe, docs_link: docs_link.html_safe)
+ end
+
def self.to_param
'irker'
end
@@ -85,23 +102,6 @@ module Integrations
}
end
- def help
- docs_link = ActionController::Base.helpers.link_to(
- _('Learn more.'),
- Rails.application.routes.url_helpers.help_page_url(
- 'user/project/integrations/irker',
- anchor: 'set-up-an-irker-daemon'
- ),
- target: '_blank',
- rel: 'noopener noreferrer'
- )
-
- format(s_(
- 'IrkerService|Send update messages to an irker server. ' \
- 'Before you can use this, you need to set up the irker daemon. %{docs_link}'
- ).html_safe, docs_link: docs_link.html_safe)
- end
-
private
def get_channels
diff --git a/app/models/integrations/jenkins.rb b/app/models/integrations/jenkins.rb
index 0683c8408bc..a2f5667eaee 100644
--- a/app/models/integrations/jenkins.rb
+++ b/app/models/integrations/jenkins.rb
@@ -69,15 +69,15 @@ module Integrations
%w[push merge_request tag_push]
end
- def title
+ def self.title
'Jenkins'
end
- def description
+ def self.description
s_('Run CI/CD pipelines with Jenkins.')
end
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('integration/jenkins'), target: '_blank', rel: 'noopener noreferrer'
s_('Run CI/CD pipelines with Jenkins when you push to a repository, or when a merge request is created, updated, or merged. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
end
diff --git a/app/models/integrations/jira.rb b/app/models/integrations/jira.rb
index f6e99454cb1..22367ee336d 100644
--- a/app/models/integrations/jira.rb
+++ b/app/models/integrations/jira.rb
@@ -184,16 +184,24 @@ module Integrations
options
end
- def client
- @client ||= JIRA::Client.new(options).tap do |client|
+ def client(additional_options = {})
+ JIRA::Client.new(options.merge(additional_options)).tap do |client|
# Replaces JIRA default http client with our implementation
client.request_client = Gitlab::Jira::HttpClient.new(client.options)
end
end
- def help
+ def self.title
+ 'Jira'
+ end
+
+ def self.description
+ s_("JiraService|Use Jira as this project's issue tracker.")
+ end
+
+ def self.help
jira_doc_link_start = format('<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe,
- url: help_page_path('integration/jira/index'))
+ url: Gitlab::Routing.url_helpers.help_page_path('integration/jira/index'))
format(
s_("JiraService|You must configure Jira before enabling this integration. " \
"%{jira_doc_link_start}Learn more.%{link_end}"),
@@ -201,14 +209,6 @@ module Integrations
link_end: '</a>'.html_safe)
end
- def title
- 'Jira'
- end
-
- def description
- s_("JiraService|Use Jira as this project's issue tracker.")
- end
-
def self.to_param
'jira'
end
diff --git a/app/models/integrations/mattermost.rb b/app/models/integrations/mattermost.rb
index 7e391b11d82..361ff4afce8 100644
--- a/app/models/integrations/mattermost.rb
+++ b/app/models/integrations/mattermost.rb
@@ -5,11 +5,11 @@ module Integrations
include SlackMattermostNotifier
include SlackMattermostFields
- def title
+ def self.title
_('Mattermost notifications')
end
- def description
+ def self.description
s_('Send notifications about project events to Mattermost channels.')
end
@@ -17,7 +17,7 @@ module Integrations
'mattermost'
end
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/mattermost'), target: '_blank', rel: 'noopener noreferrer'
s_('Send notifications about project events to Mattermost channels. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
end
diff --git a/app/models/integrations/mattermost_slash_commands.rb b/app/models/integrations/mattermost_slash_commands.rb
index 73cddd163e0..9554dec4168 100644
--- a/app/models/integrations/mattermost_slash_commands.rb
+++ b/app/models/integrations/mattermost_slash_commands.rb
@@ -14,11 +14,11 @@ module Integrations
false
end
- def title
+ def self.title
s_('Integrations|Mattermost slash commands')
end
- def description
+ def self.description
s_('Integrations|Perform common tasks with slash commands.')
end
diff --git a/app/models/integrations/microsoft_teams.rb b/app/models/integrations/microsoft_teams.rb
index 208172d6303..3a7c848d411 100644
--- a/app/models/integrations/microsoft_teams.rb
+++ b/app/models/integrations/microsoft_teams.rb
@@ -18,11 +18,11 @@ module Integrations
title: -> { s_('Integrations|Branches for which notifications are to be sent') },
choices: -> { branch_choices }
- def title
+ def self.title
'Microsoft Teams notifications'
end
- def description
+ def self.description
'Send notifications about project events to Microsoft Teams.'
end
@@ -30,7 +30,7 @@ module Integrations
'microsoft_teams'
end
- def help
+ def self.help
'<p>Use this service to send notifications about events in GitLab projects to your Microsoft Teams channels. <a href="https://docs.gitlab.com/ee/user/project/integrations/microsoft_teams.html" target="_blank" rel="noopener noreferrer">How do I configure this integration?</a></p>'
end
diff --git a/app/models/integrations/mock_ci.rb b/app/models/integrations/mock_ci.rb
index 2d8e26d409f..9c129ca727c 100644
--- a/app/models/integrations/mock_ci.rb
+++ b/app/models/integrations/mock_ci.rb
@@ -14,11 +14,11 @@ module Integrations
validates :mock_service_url, presence: true, public_url: true, if: :activated?
- def title
+ def self.title
'MockCI'
end
- def description
+ def self.description
'Mock an external CI'
end
diff --git a/app/models/integrations/mock_monitoring.rb b/app/models/integrations/mock_monitoring.rb
index 72bb292edaa..9e474078b28 100644
--- a/app/models/integrations/mock_monitoring.rb
+++ b/app/models/integrations/mock_monitoring.rb
@@ -2,11 +2,11 @@
module Integrations
class MockMonitoring < BaseMonitoring
- def title
+ def self.title
'Mock monitoring'
end
- def description
+ def self.description
'Mock monitoring service'
end
diff --git a/app/models/integrations/packagist.rb b/app/models/integrations/packagist.rb
index c0acb6c87b4..f027afe0381 100644
--- a/app/models/integrations/packagist.rb
+++ b/app/models/integrations/packagist.rb
@@ -29,11 +29,11 @@ module Integrations
validates :username, presence: true, if: :activated?
validates :token, presence: true, if: :activated?
- def title
+ def self.title
'Packagist'
end
- def description
+ def self.description
s_('Integrations|Keep your PHP dependencies updated on Packagist.')
end
diff --git a/app/models/integrations/pipelines_email.rb b/app/models/integrations/pipelines_email.rb
index 01efbc3e4a4..c7a93d48825 100644
--- a/app/models/integrations/pipelines_email.rb
+++ b/app/models/integrations/pipelines_email.rb
@@ -44,11 +44,11 @@ module Integrations
end
end
- def title
+ def self.title
_('Pipeline status emails')
end
- def description
+ def self.description
_('Email the pipeline status to a list of recipients.')
end
diff --git a/app/models/integrations/pivotaltracker.rb b/app/models/integrations/pivotaltracker.rb
index b3cbc988dd6..97e6e3e09d1 100644
--- a/app/models/integrations/pivotaltracker.rb
+++ b/app/models/integrations/pivotaltracker.rb
@@ -20,15 +20,15 @@ module Integrations
'automatically inspect. Leave blank to include all branches.')
end
- def title
+ def self.title
'Pivotal Tracker'
end
- def description
+ def self.description
s_('PivotalTrackerService|Add commit messages as comments to Pivotal Tracker stories.')
end
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/pivotal_tracker'), target: '_blank', rel: 'noopener noreferrer'
s_('Add commit messages as comments to Pivotal Tracker stories. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
end
diff --git a/app/models/integrations/prometheus.rb b/app/models/integrations/prometheus.rb
index ff8d07a1b4c..de923bbbdd5 100644
--- a/app/models/integrations/prometheus.rb
+++ b/app/models/integrations/prometheus.rb
@@ -51,11 +51,11 @@ module Integrations
false
end
- def title
+ def self.title
'Prometheus'
end
- def description
+ def self.description
s_('PrometheusService|Monitor application health with Prometheus metrics and dashboards')
end
diff --git a/app/models/integrations/pumble.rb b/app/models/integrations/pumble.rb
index 09e011023ed..36ff5189b0f 100644
--- a/app/models/integrations/pumble.rb
+++ b/app/models/integrations/pumble.rb
@@ -18,11 +18,11 @@ module Integrations
title: -> { s_('Integrations|Branches for which notifications are to be sent') },
choices: -> { branch_choices }
- def title
+ def self.title
'Pumble'
end
- def description
+ def self.description
s_("PumbleIntegration|Send notifications about project events to Pumble.")
end
@@ -30,7 +30,7 @@ module Integrations
'pumble'
end
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to(
_('Learn more.'),
Rails.application.routes.url_helpers.help_page_url('user/project/integrations/pumble'),
diff --git a/app/models/integrations/pushover.rb b/app/models/integrations/pushover.rb
index 2feae29f627..b2c4e06e71f 100644
--- a/app/models/integrations/pushover.rb
+++ b/app/models/integrations/pushover.rb
@@ -71,11 +71,11 @@ module Integrations
]
end
- def title
+ def self.title
'Pushover'
end
- def description
+ def self.description
s_('PushoverService|Get real-time notifications on your device.')
end
diff --git a/app/models/integrations/redmine.rb b/app/models/integrations/redmine.rb
index bc2a64b0848..11eda7c69f7 100644
--- a/app/models/integrations/redmine.rb
+++ b/app/models/integrations/redmine.rb
@@ -6,15 +6,15 @@ module Integrations
validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated?
- def title
+ def self.title
'Redmine'
end
- def description
+ def self.description
s_("IssueTracker|Use Redmine as this project's issue tracker.")
end
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/redmine'), target: '_blank', rel: 'noopener noreferrer'
s_('IssueTracker|Use Redmine as the issue tracker. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
end
diff --git a/app/models/integrations/shimo.rb b/app/models/integrations/shimo.rb
index 227fdca5c91..1d004356469 100644
--- a/app/models/integrations/shimo.rb
+++ b/app/models/integrations/shimo.rb
@@ -16,11 +16,11 @@ module Integrations
valid? && activated?
end
- def title
+ def self.title
s_('Shimo|Shimo')
end
- def description
+ def self.description
s_('Shimo|Link to a Shimo Workspace from the sidebar.')
end
diff --git a/app/models/integrations/slack.rb b/app/models/integrations/slack.rb
index f70376e2f0d..9f9614a84fd 100644
--- a/app/models/integrations/slack.rb
+++ b/app/models/integrations/slack.rb
@@ -5,11 +5,11 @@ module Integrations
include SlackMattermostNotifier
include SlackMattermostFields
- def title
+ def self.title
'Slack notifications'
end
- def description
+ def self.description
'Send notifications about project events to Slack.'
end
diff --git a/app/models/integrations/slack_slash_commands.rb b/app/models/integrations/slack_slash_commands.rb
index b209f37ee7c..c5ea6f22951 100644
--- a/app/models/integrations/slack_slash_commands.rb
+++ b/app/models/integrations/slack_slash_commands.rb
@@ -10,11 +10,11 @@ module Integrations
non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current token.') },
placeholder: ''
- def title
+ def self.title
'Slack slash commands'
end
- def description
+ def self.description
"Perform common operations in Slack."
end
diff --git a/app/models/integrations/squash_tm.rb b/app/models/integrations/squash_tm.rb
index bf3f391564f..1b4ab152b1d 100644
--- a/app/models/integrations/squash_tm.rb
+++ b/app/models/integrations/squash_tm.rb
@@ -22,15 +22,15 @@ module Integrations
validates :token, length: { maximum: 255 }, allow_blank: true
end
- def title
+ def self.title
'Squash TM'
end
- def description
+ def self.description
s_("SquashTmIntegration|Update Squash TM requirements when GitLab issues are modified.")
end
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to(
_('Learn more.'),
Rails.application.routes.url_helpers.help_page_url('user/project/integrations/squash_tm'),
diff --git a/app/models/integrations/teamcity.rb b/app/models/integrations/teamcity.rb
index 575c3b8a334..913242ef9ac 100644
--- a/app/models/integrations/teamcity.rb
+++ b/app/models/integrations/teamcity.rb
@@ -47,15 +47,15 @@ module Integrations
end
end
- def title
+ def self.title
'JetBrains TeamCity'
end
- def description
+ def self.description
s_('ProjectService|Run CI/CD pipelines with JetBrains TeamCity.')
end
- def help
+ def self.help
s_('To run CI/CD pipelines with JetBrains TeamCity, input the GitLab project details in the TeamCity project Version Control Settings.')
end
diff --git a/app/models/integrations/telegram.rb b/app/models/integrations/telegram.rb
index 71fe6f8d6ef..8eb1a7ad0ea 100644
--- a/app/models/integrations/telegram.rb
+++ b/app/models/integrations/telegram.rb
@@ -38,11 +38,11 @@ module Integrations
before_validation :set_webhook
- def title
+ def self.title
'Telegram'
end
- def description
+ def self.description
s_("TelegramIntegration|Send notifications about project events to Telegram.")
end
@@ -50,7 +50,7 @@ module Integrations
'telegram'
end
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to(
_('Learn more.'),
Rails.application.routes.url_helpers.help_page_url('user/project/integrations/telegram'),
diff --git a/app/models/integrations/unify_circuit.rb b/app/models/integrations/unify_circuit.rb
index 3b4bcfa28d3..6ee95c1173b 100644
--- a/app/models/integrations/unify_circuit.rb
+++ b/app/models/integrations/unify_circuit.rb
@@ -17,11 +17,11 @@ module Integrations
title: -> { s_('Integrations|Branches for which notifications are to be sent') },
choices: -> { branch_choices }
- def title
+ def self.title
'Unify Circuit'
end
- def description
+ def self.description
s_('Integrations|Send notifications about project events to Unify Circuit.')
end
@@ -29,7 +29,7 @@ module Integrations
'unify_circuit'
end
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to _('How do I set up this service?'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/unify_circuit'), target: '_blank', rel: 'noopener noreferrer'
s_('Integrations|Send notifications about project events to a Unify Circuit conversation. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
end
diff --git a/app/models/integrations/webex_teams.rb b/app/models/integrations/webex_teams.rb
index 3ef8ab39352..5f8cc195544 100644
--- a/app/models/integrations/webex_teams.rb
+++ b/app/models/integrations/webex_teams.rb
@@ -17,11 +17,11 @@ module Integrations
title: -> { s_('Integrations|Branches for which notifications are to be sent') },
choices: -> { branch_choices }
- def title
+ def self.title
s_("WebexTeamsService|Webex Teams")
end
- def description
+ def self.description
s_("WebexTeamsService|Send notifications about project events to Webex Teams.")
end
@@ -29,7 +29,7 @@ module Integrations
'webex_teams'
end
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/webex_teams'), target: '_blank', rel: 'noopener noreferrer'
s_("WebexTeamsService|Send notifications about project events to a Webex Teams conversation. %{docs_link}") % { docs_link: docs_link.html_safe }
end
diff --git a/app/models/integrations/youtrack.rb b/app/models/integrations/youtrack.rb
index 15246a37aa7..932e588a829 100644
--- a/app/models/integrations/youtrack.rb
+++ b/app/models/integrations/youtrack.rb
@@ -14,15 +14,15 @@ module Integrations
@reference_pattern = /(?<issue>\b[A-Za-z][A-Za-z0-9_]*-\d+\b)#{regex_suffix if only_long}/
end
- def title
+ def self.title
'YouTrack'
end
- def description
+ def self.description
s_("IssueTracker|Use YouTrack as this project's issue tracker.")
end
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/youtrack'), target: '_blank', rel: 'noopener noreferrer'
s_("IssueTracker|Use YouTrack as this project's issue tracker. %{docs_link}").html_safe % { docs_link: docs_link.html_safe }
end
diff --git a/app/models/integrations/zentao.rb b/app/models/integrations/zentao.rb
index 58ec4abf30c..2aec0c1e871 100644
--- a/app/models/integrations/zentao.rb
+++ b/app/models/integrations/zentao.rb
@@ -57,18 +57,18 @@ module Integrations
data_fields.api_url ||= issues_tracker['api_url']
end
- def title
+ def self.title
'ZenTao'
end
- def description
+ def self.description
s_("ZentaoIntegration|Use ZenTao as this project's issue tracker.")
end
- def help
+ def self.help
s_("ZentaoIntegration|Before you enable this integration, you must configure ZenTao. For more details, read the %{link_start}ZenTao integration documentation%{link_end}.") % {
link_start: '<a href="%{url}" target="_blank" rel="noopener noreferrer">'
- .html_safe % { url: help_page_url('user/project/integrations/zentao') },
+ .html_safe % { url: Rails.application.routes.url_helpers.help_page_url('user/project/integrations/zentao') },
link_end: '</a>'.html_safe
}
end
diff --git a/app/models/member.rb b/app/models/member.rb
index 77e283044ea..9690e16fd7d 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -135,11 +135,12 @@ class Member < ApplicationRecord
.reorder(nil)
end
- scope :without_invites_and_requests, -> do
- active_state
- .non_request
- .non_invite
- .non_minimal_access
+ scope :without_invites_and_requests, ->(minimal_access: false) do
+ result = active_state.non_request.non_invite
+
+ result = result.non_minimal_access unless minimal_access
+
+ result
end
scope :invite, -> { where.not(invite_token: nil) }
diff --git a/app/models/members/members/members_with_parents.rb b/app/models/members/members/members_with_parents.rb
new file mode 100644
index 00000000000..61ce99e1f3e
--- /dev/null
+++ b/app/models/members/members/members_with_parents.rb
@@ -0,0 +1,105 @@
+# frozen_string_literal: true
+
+module Members
+ class MembersWithParents
+ attr_reader :group
+
+ def initialize(group)
+ @group = group
+ end
+
+ # Returns all members for group and parents, with no filters
+ def all_members
+ GroupMember.from_union([
+ members_from_self_and_ancestors,
+ members_from_self_and_ancestor_group_shares
+ ])
+ end
+
+ # Returns members based on filter options:
+ #
+ # - `active_users`. DEPRECATED. If true, returns only members for active users
+ # - `minimal_access`. Used only in EE (GitLab Premium). If true, returns
+ # members which has minimal access. If false (default), does not return
+ # members with minimal access
+ #
+ # NOTE : this method does not return pending invites, nor requests.
+ def members(active_users: false, minimal_access: false)
+ raise ArgumentError, 'active_users: is deprecated' if active_users && minimal_access
+
+ group_hierarchy_members = members_from_self_and_ancestors
+
+ group_hierarchy_members =
+ if active_users
+ group_hierarchy_members.active_without_invites_and_requests
+ else
+ filter_invites_and_requests(group_hierarchy_members, minimal_access)
+ end
+
+ GroupMember.from_union([
+ group_hierarchy_members,
+ members_from_self_and_ancestor_group_shares
+ ])
+ end
+
+ private
+
+ # NOTE: minimal access is Premium, so in FOSS we will not include minimal access member
+ def filter_invites_and_requests(members, _minimal_access)
+ members.without_invites_and_requests(minimal_access: false)
+ end
+
+ def source_ids
+ # Avoids an unnecessary SELECT when the group has no parents
+ @source_ids ||=
+ if group.has_parent?
+ group.self_and_ancestors.reorder(nil).select(:id)
+ else
+ group.id
+ end
+ end
+
+ def members_from_self_and_ancestors
+ GroupMember
+ .with_source_id(source_ids)
+ .select(*GroupMember.cached_column_list)
+ end
+
+ def members_from_self_and_ancestor_group_shares
+ group_group_link_table = GroupGroupLink.arel_table
+ group_member_table = GroupMember.arel_table
+
+ group_group_links_query = GroupGroupLink.where(shared_group_id: source_ids)
+ cte = Gitlab::SQL::CTE.new(:group_group_links_cte, group_group_links_query)
+ cte_alias = cte.table.alias(GroupGroupLink.table_name)
+
+ # Instead of members.access_level, we need to maximize that access_level at
+ # the respective group_group_links.group_access.
+ member_columns = GroupMember.attribute_names.map do |column_name|
+ if column_name == 'access_level'
+ smallest_value_arel([cte_alias[:group_access], group_member_table[:access_level]], 'access_level')
+ else
+ group_member_table[column_name]
+ end
+ end
+
+ GroupMember
+ .with(cte.to_arel)
+ .select(*member_columns)
+ .from([group_member_table, cte.alias_to(group_group_link_table)])
+ .where(group_member_table[:requested_at].eq(nil))
+ .where(group_member_table[:source_id].eq(group_group_link_table[:shared_with_group_id]))
+ .where(group_member_table[:source_type].eq('Namespace'))
+ .where(group_member_table[:state].eq(::Member::STATE_ACTIVE))
+ .non_minimal_access
+ end
+
+ def smallest_value_arel(args, column_alias)
+ Arel::Nodes::As.new(
+ Arel::Nodes::NamedFunction.new('LEAST', args),
+ Arel::Nodes::SqlLiteral.new(column_alias))
+ end
+ end
+end
+
+Members::MembersWithParents.prepend_mod
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index d07e4f9e298..5e5f9ab7385 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -48,6 +48,12 @@ class ProjectMember < Member
end
end
+ def permissible_access_level_roles_for_project_access_token(current_user, project)
+ permissible_access_level_roles(current_user, project).filter do |_, value|
+ value <= project.project_authorizations.find_by(user: current_user).access_level
+ end
+ end
+
def access_level_roles
Gitlab::Access.options
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index d9726e76c4b..524a9b8074b 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -384,7 +384,6 @@ class MergeRequest < ApplicationRecord
}
scope :with_csv_entity_associations, -> { preload(:assignees, :approved_by_users, :author, :milestone, metrics: [:merged_by]) }
- scope :with_jira_integration_associations, -> { preload_routables.preload(:metrics, :assignees, :author) }
scope :recently_unprepared, -> { where(prepared_at: nil).where(created_at: 2.hours.ago..).order(:created_at, :id) } # id is the tie-breaker
scope :by_target_branch_wildcard, ->(wildcard_branch_name) do
@@ -530,6 +529,14 @@ class MergeRequest < ApplicationRecord
.pluck(:target_branch)
end
+ def self.recent_source_branches(limit: 100)
+ group(:source_branch)
+ .select(:source_branch)
+ .reorder(arel_table[:updated_at].maximum.desc)
+ .limit(limit)
+ .pluck(:source_branch)
+ end
+
def self.sort_by_attribute(method, excluded_labels: [])
case method.to_s
when 'merged_at', 'merged_at_asc' then order_merged_at_asc
@@ -1235,17 +1242,14 @@ class MergeRequest < ApplicationRecord
}
end
- def mergeable?(
- skip_ci_check: false, skip_discussions_check: false, skip_approved_check: false, check_mergeability_retry_lease: false,
- skip_draft_check: false, skip_rebase_check: false, skip_blocked_check: false)
-
- return false unless mergeable_state?(
- skip_ci_check: skip_ci_check,
- skip_discussions_check: skip_discussions_check,
- skip_draft_check: skip_draft_check,
- skip_approved_check: skip_approved_check,
- skip_blocked_check: skip_blocked_check
- )
+ # mergeable_state_check_params allows a hash of merge checks to skip or not
+ # skip_ci_check
+ # skip_discussions_check
+ # skip_draft_check
+ # skip_approved_check
+ # skip_blocked_check
+ def mergeable?(check_mergeability_retry_lease: false, skip_rebase_check: false, **mergeable_state_check_params)
+ return false unless mergeable_state?(**mergeable_state_check_params)
check_mergeability(sync_retry_lease: check_mergeability_retry_lease)
mergeable_git_state?(skip_rebase_check: skip_rebase_check)
@@ -1275,18 +1279,16 @@ class MergeRequest < ApplicationRecord
mergeable_state_checks + mergeable_git_state_checks
end
- def mergeable_state?(
- skip_ci_check: false, skip_discussions_check: false, skip_approved_check: false,
- skip_draft_check: false, skip_blocked_check: false)
+ # mergeable_state_check_params allows a hash of merge checks to skip or not
+ # skip_ci_check
+ # skip_discussions_check
+ # skip_draft_check
+ # skip_approved_check
+ # skip_blocked_check
+ def mergeable_state?(**mergeable_state_check_params)
additional_checks = execute_merge_checks(
self.class.mergeable_state_checks,
- params: {
- skip_ci_check: skip_ci_check,
- skip_discussions_check: skip_discussions_check,
- skip_approved_check: skip_approved_check,
- skip_draft_check: skip_draft_check,
- skip_blocked_check: skip_blocked_check
- }
+ params: mergeable_state_check_params
)
additional_checks.success?
end
@@ -1386,7 +1388,7 @@ class MergeRequest < ApplicationRecord
end
def mergeable_discussions_state?
- return true unless project.only_allow_merge_if_all_discussions_are_resolved?(inherit_group_setting: true)
+ return true unless only_allow_merge_if_all_discussions_are_resolved?
unresolved_notes.none?(&:to_be_resolved?)
end
@@ -1566,8 +1568,16 @@ class MergeRequest < ApplicationRecord
access.can_push_to_branch?(target_branch)
end
+ def only_allow_merge_if_pipeline_succeeds?
+ project.only_allow_merge_if_pipeline_succeeds?(inherit_group_setting: true)
+ end
+
+ def only_allow_merge_if_all_discussions_are_resolved?
+ project.only_allow_merge_if_all_discussions_are_resolved?(inherit_group_setting: true)
+ end
+
def mergeable_ci_state?
- return true unless project.only_allow_merge_if_pipeline_succeeds?(inherit_group_setting: true)
+ return true unless only_allow_merge_if_pipeline_succeeds?
return false unless actual_head_pipeline
return true if project.allow_merge_on_skipped_pipeline?(inherit_group_setting: true) && actual_head_pipeline.skipped?
diff --git a/app/models/merge_request_context_commit_diff_file.rb b/app/models/merge_request_context_commit_diff_file.rb
index fdf57068928..2fb995ee512 100644
--- a/app/models/merge_request_context_commit_diff_file.rb
+++ b/app/models/merge_request_context_commit_diff_file.rb
@@ -10,7 +10,6 @@ class MergeRequestContextCommitDiffFile < ApplicationRecord
belongs_to :merge_request_context_commit, inverse_of: :diff_files
sha_attribute :sha
- alias_attribute :id, :sha
# create MergeRequestContextCommitDiffFile by given diff file record(s)
def self.bulk_insert(*args)
diff --git a/app/models/merge_request_diff_commit.rb b/app/models/merge_request_diff_commit.rb
index fc08dd4d9c8..790520c4123 100644
--- a/app/models/merge_request_diff_commit.rb
+++ b/app/models/merge_request_diff_commit.rb
@@ -6,13 +6,8 @@ class MergeRequestDiffCommit < ApplicationRecord
include BulkInsertSafe
include ShaAttribute
include CachedCommit
- include IgnorableColumns
include FromUnion
- ignore_column %i[author_name author_email committer_name committer_email],
- remove_with: '14.6',
- remove_after: '2021-11-22'
-
belongs_to :merge_request_diff
# This relation is called `commit_author` and not `author`, as the project
@@ -33,7 +28,6 @@ class MergeRequestDiffCommit < ApplicationRecord
belongs_to :committer, class_name: 'MergeRequest::DiffCommitUser'
sha_attribute :sha
- alias_attribute :id, :sha
attribute :trailers, :ind_jsonb
validates :trailers, json_schema: { filename: 'git_trailers' }
@@ -129,4 +123,8 @@ class MergeRequestDiffCommit < ApplicationRecord
def committer_email
committer&.email
end
+
+ def to_hash
+ super.merge({ 'id' => sha })
+ end
end
diff --git a/app/models/ml/candidate.rb b/app/models/ml/candidate.rb
index 6f4728a1d98..70eaab8c0ab 100644
--- a/app/models/ml/candidate.rb
+++ b/app/models/ml/candidate.rb
@@ -12,12 +12,14 @@ module Ml
validates :eid, :experiment, presence: true
validates :status, inclusion: { in: statuses.keys }
+ validates :model_version_id, uniqueness: { allow_nil: true }
belongs_to :experiment, class_name: 'Ml::Experiment'
belongs_to :user
belongs_to :package, class_name: 'Packages::Package'
belongs_to :project
belongs_to :ci_build, class_name: 'Ci::Build', optional: true
+ belongs_to :model_version, class_name: 'Ml::ModelVersion', optional: true, inverse_of: :candidate
has_many :metrics, class_name: 'Ml::CandidateMetric'
has_many :params, class_name: 'Ml::CandidateParam'
has_many :metadata, class_name: 'Ml::CandidateMetadata'
diff --git a/app/models/ml/model.rb b/app/models/ml/model.rb
index 27f03ed5857..b6f7e9a0639 100644
--- a/app/models/ml/model.rb
+++ b/app/models/ml/model.rb
@@ -3,6 +3,7 @@
module Ml
class Model < ApplicationRecord
include Presentable
+ include Sortable
validates :project, :default_experiment, presence: true
validates :name,
@@ -15,15 +16,19 @@ module Ml
has_one :default_experiment, class_name: 'Ml::Experiment'
belongs_to :project
+ belongs_to :user
has_many :versions, class_name: 'Ml::ModelVersion'
+ has_many :metadata, class_name: 'Ml::ModelMetadata'
has_one :latest_version, -> { latest_by_model }, class_name: 'Ml::ModelVersion', inverse_of: :model
scope :including_latest_version, -> { includes(:latest_version) }
+ scope :including_project, -> { includes(:project) }
scope :with_version_count, -> {
left_outer_joins(:versions)
.select("ml_models.*, count(ml_model_versions.id) as version_count")
.group(:id)
}
+ scope :by_name, ->(name) { where("ml_models.name LIKE ?", "%#{sanitize_sql_like(name)}%") } # rubocop:disable GitlabSecurity/SqlInjection
scope :by_project, ->(project) { where(project_id: project.id) }
def valid_default_experiment?
@@ -33,13 +38,12 @@ module Ml
errors.add(:default_experiment) unless default_experiment.project_id == project_id
end
- def self.find_or_create(project, name, experiment)
- create_with(default_experiment: experiment)
- .find_or_create_by(project: project, name: name)
- end
-
def self.by_project_id_and_id(project_id, id)
find_by(project_id: project_id, id: id)
end
+
+ def self.by_project_id_and_name(project_id, name)
+ find_by(project_id: project_id, name: name)
+ end
end
end
diff --git a/app/models/ml/model_metadata.rb b/app/models/ml/model_metadata.rb
new file mode 100644
index 00000000000..9c4273c629c
--- /dev/null
+++ b/app/models/ml/model_metadata.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Ml
+ class ModelMetadata < ApplicationRecord
+ validates :name,
+ length: { maximum: 250 },
+ presence: true,
+ uniqueness: { scope: :model, message: ->(metadata, _) { "'#{metadata.name}' already taken" } }
+ validates :value, length: { maximum: 5000 }, presence: true
+
+ belongs_to :model, class_name: 'Ml::Model', optional: false
+ end
+end
diff --git a/app/models/ml/model_version.rb b/app/models/ml/model_version.rb
index e7fcde2cb5c..58da57f27d6 100644
--- a/app/models/ml/model_version.rb
+++ b/app/models/ml/model_version.rb
@@ -2,6 +2,8 @@
module Ml
class ModelVersion < ApplicationRecord
+ include Presentable
+
validates :project, :model, presence: true
validates :version,
@@ -10,11 +12,15 @@ module Ml
presence: true,
length: { maximum: 255 }
+ validates :description,
+ length: { maximum: 500 }
+
validate :valid_model?, :valid_package?
belongs_to :model, class_name: 'Ml::Model'
belongs_to :project
belongs_to :package, class_name: 'Packages::MlModel::Package', optional: true
+ has_one :candidate, class_name: 'Ml::Candidate'
delegate :name, to: :model
@@ -22,8 +28,17 @@ module Ml
scope :latest_by_model, -> { order_by_model_id_id_desc.select('DISTINCT ON (model_id) *') }
class << self
- def find_or_create!(model, version, package)
- create_with(package: package).find_or_create_by!(project: model.project, model: model, version: version)
+ def find_or_create!(model, version, package, description)
+ create_with(package: package, description: description)
+ .find_or_create_by!(project: model.project, model: model, version: version)
+ end
+
+ def by_project_id_and_id(project_id, id)
+ find_by(project_id: project_id, id: id)
+ end
+
+ def by_project_id_name_and_version(project_id, name, version)
+ joins(:model).find_by(model: { name: name, project_id: project_id }, project_id: project_id, version: version)
end
end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 733b89fcaf2..cd54ac1b24a 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -18,6 +18,7 @@ class Namespace < ApplicationRecord
include Referable
include CrossDatabaseIgnoredTables
include IgnorableColumns
+ include UseSqlFunctionForPrimaryKeyLookups
ignore_column :unlock_membership_to_ldap, remove_with: '16.7', remove_after: '2023-11-16'
@@ -138,6 +139,8 @@ class Namespace < ApplicationRecord
to: :namespace_settings
delegate :runner_registration_enabled, :runner_registration_enabled?, :runner_registration_enabled=,
to: :namespace_settings
+ delegate :emails_enabled, :emails_enabled=,
+ to: :namespace_settings, allow_nil: true
delegate :allow_runner_registration_token,
:allow_runner_registration_token=,
to: :namespace_settings
@@ -204,7 +207,7 @@ class Namespace < ApplicationRecord
# Make sure that the name is same as strong_memoize name in root_ancestor
# method
- attr_writer :root_ancestor, :emails_disabled_memoized
+ attr_writer :root_ancestor, :emails_enabled_memoized
class << self
def sti_class_for(type_name)
@@ -299,6 +302,14 @@ class Namespace < ApplicationRecord
super || Gitlab::CurrentSettings.default_branch_protection
end
+ def default_branch_protection_settings
+ settings = default_branch_protection_defaults
+
+ return settings unless settings.blank?
+
+ Gitlab::CurrentSettings.default_branch_protection_defaults
+ end
+
def visibility_level_field
:visibility_level
end
@@ -382,17 +393,16 @@ class Namespace < ApplicationRecord
# any ancestor can disable emails for all descendants
def emails_disabled?
- strong_memoize(:emails_disabled_memoized) do
- if parent_id
- self_and_ancestors.where(emails_disabled: true).exists?
- else
- !!emails_disabled
- end
- end
+ !emails_enabled?
end
def emails_enabled?
- !emails_disabled?
+ # If no namespace_settings, we can assume it has not changed from enabled
+ return true unless namespace_settings
+
+ strong_memoize(:emails_enabled_memoized) do
+ namespace_settings.emails_enabled?
+ end
end
def lfs_enabled?
@@ -626,8 +636,7 @@ class Namespace < ApplicationRecord
:route,
:project_setting,
:project_feature,
- pages_metadatum: :pages_deployment
- )
+ :active_pages_deployments)
end
private
diff --git a/app/models/namespace_setting.rb b/app/models/namespace_setting.rb
index 3befcdeaec5..13d2c5a62e2 100644
--- a/app/models/namespace_setting.rb
+++ b/app/models/namespace_setting.rb
@@ -63,6 +63,12 @@ class NamespaceSetting < ApplicationRecord
namespace.root_ancestor.prevent_sharing_groups_outside_hierarchy
end
+ def emails_enabled?
+ return emails_enabled unless namespace.has_parent?
+
+ all_ancestors_have_emails_enabled?
+ end
+
def show_diff_preview_in_email?
return show_diff_preview_in_email unless namespace.has_parent?
@@ -89,6 +95,10 @@ class NamespaceSetting < ApplicationRecord
private
+ def all_ancestors_have_emails_enabled?
+ self.class.where(namespace_id: namespace.self_and_ancestors, emails_enabled: false).none?
+ end
+
def all_ancestors_allow_diff_preview_in_email?
!self.class.where(namespace_id: namespace.self_and_ancestors, show_diff_preview_in_email: false).exists?
end
diff --git a/app/models/network/graph.rb b/app/models/network/graph.rb
index 0f410d4810d..f60e7682418 100644
--- a/app/models/network/graph.rb
+++ b/app/models/network/graph.rb
@@ -2,7 +2,7 @@
module Network
class Graph
- attr_reader :days, :commits, :map, :notes, :repo
+ attr_reader :days, :commits, :map, :repo
def self.max_count
@max_count ||= 650
@@ -17,28 +17,10 @@ module Network
@commits = collect_commits
@days = index_commits
- @notes = collect_notes
end
protected
- def collect_notes
- return {} if Feature.enabled?(:disable_network_graph_notes_count, @project, type: :experiment)
-
- h = Hash.new(0)
-
- @project
- .notes
- .where(noteable_type: 'Commit')
- .group('notes.commit_id')
- .select('notes.commit_id, count(notes.id) as note_count')
- .each do |item|
- h[item.commit_id] = item.note_count.to_i
- end
-
- h
- end
-
# Get commits from repository
#
def collect_commits
diff --git a/app/models/note.rb b/app/models/note.rb
index eae7a40fb4e..6f4a56dd3cc 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -383,7 +383,11 @@ class Note < ApplicationRecord
end
def for_project_noteable?
- !(for_personal_snippet? || for_abuse_report?)
+ !(for_personal_snippet? || for_abuse_report? || group_level_issue?)
+ end
+
+ def group_level_issue?
+ (for_issue? || for_work_item?) && noteable&.project_id.blank?
end
def for_design?
diff --git a/app/models/organizations/organization.rb b/app/models/organizations/organization.rb
index 893b08d7872..157b851e009 100644
--- a/app/models/organizations/organization.rb
+++ b/app/models/organizations/organization.rb
@@ -42,6 +42,10 @@ module Organizations
organization_users.exists?(user: user)
end
+ def web_url(only_path: nil)
+ Gitlab::UrlBuilder.build(self, only_path: only_path)
+ end
+
private
def check_if_default_organization
diff --git a/app/models/packages/npm/metadata_cache.rb b/app/models/packages/npm/metadata_cache.rb
index 02efeda69cb..b6ab2a88a98 100644
--- a/app/models/packages/npm/metadata_cache.rb
+++ b/app/models/packages/npm/metadata_cache.rb
@@ -5,6 +5,9 @@ module Packages
class MetadataCache < ApplicationRecord
include FileStoreMounter
include Packages::Downloadable
+ include Packages::Destructible
+
+ enum status: { default: 0, processing: 1, error: 3 }
belongs_to :project, inverse_of: :npm_metadata_caches
@@ -18,6 +21,9 @@ module Packages
before_validation :set_object_storage_key
attr_readonly :object_storage_key
+ scope :stale, -> { where(project_id: nil) }
+ scope :pending_destruction, -> { stale.default }
+
def self.find_or_build(package_name:, project_id:)
find_or_initialize_by(
package_name: package_name,
diff --git a/app/models/packages/nuget/symbol.rb b/app/models/packages/nuget/symbol.rb
index 643b5552d84..3315f11b974 100644
--- a/app/models/packages/nuget/symbol.rb
+++ b/app/models/packages/nuget/symbol.rb
@@ -4,6 +4,7 @@ module Packages
module Nuget
class Symbol < ApplicationRecord
include FileStoreMounter
+ include ShaAttribute
belongs_to :package, -> { where(package_type: :nuget) }, inverse_of: :nuget_symbols
@@ -13,6 +14,8 @@ module Packages
validates :signature, uniqueness: { scope: :file_path }
validates :object_storage_key, uniqueness: true
+ sha256_attribute :file_sha256
+
mount_file_store_uploader SymbolUploader
before_validation :set_object_storage_key, on: :create
diff --git a/app/models/packages/protection/rule.rb b/app/models/packages/protection/rule.rb
index 582b51475c2..f13bcc6e32e 100644
--- a/app/models/packages/protection/rule.rb
+++ b/app/models/packages/protection/rule.rb
@@ -12,6 +12,12 @@ module Packages
validates :package_name_pattern, presence: true, uniqueness: { scope: [:project_id, :package_type] },
length: { maximum: 255 }
+ validates :package_name_pattern,
+ format: {
+ with: Gitlab::Regex.protection_rules_npm_package_name_pattern_regex,
+ message: ->(_object, _data) { _('should be a valid NPM package name with optional wildcard characters.') }
+ },
+ if: :npm?
validates :package_type, presence: true
validates :push_protected_up_to_access_level, presence: true
@@ -20,7 +26,7 @@ module Packages
scope :for_package_name, ->(package_name) {
return none if package_name.blank?
- where(":package_name ILIKE package_name_pattern_ilike_query", package_name: package_name)
+ where(':package_name ILIKE package_name_pattern_ilike_query', package_name: package_name)
}
def self.push_protected_from?(access_level:, package_name:, package_type:)
diff --git a/app/models/packages/pypi/metadatum.rb b/app/models/packages/pypi/metadatum.rb
index ff247fedb59..f7360409507 100644
--- a/app/models/packages/pypi/metadatum.rb
+++ b/app/models/packages/pypi/metadatum.rb
@@ -3,10 +3,24 @@
class Packages::Pypi::Metadatum < ApplicationRecord
self.primary_key = :package_id
+ MAX_REQUIRED_PYTHON_LENGTH = 255
+ MAX_KEYWORDS_LENGTH = 255
+ MAX_METADATA_VERSION_LENGTH = 16
+ MAX_AUTHOR_EMAIL_LENGTH = 2048
+ MAX_SUMMARY_LENGTH = 255
+ MAX_DESCRIPTION_LENGTH = 4000
+ MAX_DESCRIPTION_CONTENT_TYPE = 128
+
belongs_to :package, -> { where(package_type: :pypi) }, inverse_of: :pypi_metadatum
validates :package, presence: true
- validates :required_python, length: { maximum: 255 }, allow_nil: false
+ validates :required_python, length: { maximum: MAX_REQUIRED_PYTHON_LENGTH }, allow_nil: false
+ validates :keywords, length: { maximum: MAX_KEYWORDS_LENGTH }, allow_nil: true
+ validates :metadata_version, length: { maximum: MAX_METADATA_VERSION_LENGTH }, allow_nil: true
+ validates :author_email, length: { maximum: MAX_AUTHOR_EMAIL_LENGTH }, allow_nil: true
+ validates :summary, length: { maximum: MAX_SUMMARY_LENGTH }, allow_nil: true
+ validates :description, length: { maximum: MAX_DESCRIPTION_LENGTH }, allow_nil: true
+ validates :description_content_type, length: { maximum: MAX_DESCRIPTION_CONTENT_TYPE }, allow_nil: true
validate :pypi_package_type
diff --git a/app/models/packages/tag.rb b/app/models/packages/tag.rb
index 9c17a147bf4..0df64bfba54 100644
--- a/app/models/packages/tag.rb
+++ b/app/models/packages/tag.rb
@@ -1,9 +1,12 @@
# frozen_string_literal: true
class Packages::Tag < ApplicationRecord
belongs_to :package, inverse_of: :tags
+ belongs_to :project
validates :package, :name, presence: true
+ before_save :ensure_project_id
+
FOR_PACKAGES_TAGS_LIMIT = 200
NUGET_TAGS_SEPARATOR = ' ' # https://docs.microsoft.com/en-us/nuget/reference/nuspec#tags
@@ -15,4 +18,8 @@ class Packages::Tag < ApplicationRecord
.order(updated_at: :desc)
.limit(FOR_PACKAGES_TAGS_LIMIT)
end
+
+ def ensure_project_id
+ self.project_id ||= package.project_id
+ end
end
diff --git a/app/models/pages/lookup_path.rb b/app/models/pages/lookup_path.rb
index 8a02415aef4..e5e23c3bb84 100644
--- a/app/models/pages/lookup_path.rb
+++ b/app/models/pages/lookup_path.rb
@@ -4,8 +4,6 @@ module Pages
class LookupPath
include Gitlab::Utils::StrongMemoize
- LegacyStorageDisabledError = Class.new(::StandardError)
-
def initialize(project, trim_prefix: nil, domain: nil)
@project = project
@domain = domain
@@ -15,6 +13,7 @@ module Pages
def project_id
project.id
end
+ strong_memoize_attr :project_id
def access_control
project.private_pages?
@@ -76,8 +75,15 @@ module Pages
attr_reader :project, :trim_prefix, :domain
+ # project.active_pages_deployments is already loaded from the database,
+ # so selecting from the array to avoid N+1
+ # this will change with when serving multiple versions on
+ # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/133261
def deployment
- project.pages_metadatum.pages_deployment
+ project
+ .active_pages_deployments
+ .to_a
+ .find { |deployment| deployment.path_prefix.blank? }
end
strong_memoize_attr :deployment
diff --git a/app/models/pages_deployment.rb b/app/models/pages_deployment.rb
index f05ed2aac6e..2aa36a94171 100644
--- a/app/models/pages_deployment.rb
+++ b/app/models/pages_deployment.rb
@@ -17,7 +17,8 @@ class PagesDeployment < ApplicationRecord
scope :with_files_stored_locally, -> { where(file_store: ::ObjectStorage::Store::LOCAL) }
scope :with_files_stored_remotely, -> { where(file_store: ::ObjectStorage::Store::REMOTE) }
scope :project_id_in, ->(ids) { where(project_id: ids) }
- scope :active, -> { where(deleted_at: nil) }
+ scope :with_path_prefix, ->(prefix) { where("COALESCE(path_prefix, '') = ?", prefix.to_s) }
+ scope :active, -> { where(deleted_at: nil).order(created_at: :desc) }
scope :deactivated, -> { where('deleted_at < ?', Time.now.utc) }
validates :file, presence: true
@@ -33,11 +34,23 @@ class PagesDeployment < ApplicationRecord
skip_callback :save, :after, :store_file!
after_commit :store_file_after_commit!, on: [:create, :update]
+ def self.latest_pipeline_id
+ Ci::Build.id_in(pluck(:ci_build_id)).maximum(:commit_id)
+ end
+
+ def self.deactivate_all(project)
+ now = Time.now.utc
+ active
+ .project_id_in(project.id)
+ .update_all(updated_at: now, deleted_at: now)
+ end
+
def self.deactivate_deployments_older_than(deployment, time: nil)
now = Time.now.utc
active
.older_than(deployment.id)
- .where(project_id: deployment.project_id, path_prefix: deployment.path_prefix)
+ .project_id_in(deployment.project_id)
+ .with_path_prefix(deployment.path_prefix)
.update_all(updated_at: now, deleted_at: time || now)
end
diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb
index b86bc761cc1..cabd3924fd6 100644
--- a/app/models/pages_domain.rb
+++ b/app/models/pages_domain.rb
@@ -11,6 +11,8 @@ class PagesDomain < ApplicationRecord
MAX_CERTIFICATE_KEY_LENGTH = 8192
+ X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN = 19
+
enum certificate_source: { user_provided: 0, gitlab_provided: 1 }, _prefix: :certificate
enum scope: { instance: 0, group: 1, project: 2 }, _prefix: :scope, _default: :project
enum usage: { pages: 0, serverless: 1 }, _prefix: :usage, _default: :pages
@@ -122,15 +124,23 @@ class PagesDomain < ApplicationRecord
x509.check_private_key(pkey)
end
- def has_intermediates?
+ def has_valid_intermediates?
return false unless x509
- # self-signed certificates doesn't have the certificate chain
+ # self-signed certificates don't have the certificate chain
return true if x509.verify(x509.public_key)
store = OpenSSL::X509::Store.new
store.set_default_paths
+ store.verify_callback = ->(is_valid, store_ctx) {
+ # allow self signed certs, see https://gitlab.com/gitlab-org/gitlab/-/issues/356447
+ return true if store_ctx.error == X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN
+
+ self.errors.add(:certificate, store_ctx.error_string) unless is_valid
+ is_valid
+ }
+
store.verify(x509, untrusted_ca_certs_bundle)
rescue OpenSSL::X509::StoreError
false
@@ -230,9 +240,7 @@ class PagesDomain < ApplicationRecord
end
def pages_deployed?
- return false unless project
-
- project.pages_metadatum&.deployed?
+ project&.pages_deployed?
end
private
@@ -260,9 +268,7 @@ class PagesDomain < ApplicationRecord
end
def validate_intermediates
- unless has_intermediates?
- self.errors.add(:certificate, 'misses intermediates')
- end
+ self.errors.add(:certificate, 'misses intermediates') unless has_valid_intermediates?
end
def validate_pages_domain
diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb
index 4dfe7252a0c..f2fbb5b989e 100644
--- a/app/models/personal_access_token.rb
+++ b/app/models/personal_access_token.rb
@@ -44,8 +44,9 @@ class PersonalAccessToken < ApplicationRecord
scope :last_used_after, -> (date) { where("last_used_at >= ?", date) }
validates :scopes, presence: true
+ validates :expires_at, presence: true, on: :create, unless: :allow_expires_at_to_be_empty?
+
validate :validate_scopes
- validates :expires_at, presence: true, on: :create
validate :expires_at_before_instance_max_expiry_date, on: :create
def revoke!
@@ -97,6 +98,10 @@ class PersonalAccessToken < ApplicationRecord
self.class.token_prefix
end
+ def allow_expires_at_to_be_empty?
+ false
+ end
+
def expires_at_before_instance_max_expiry_date
return unless expires_at
diff --git a/app/models/project.rb b/app/models/project.rb
index fd226d23e77..0d103094aec 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -45,6 +45,7 @@ class Project < ApplicationRecord
include UpdatedAtFilterable
include IgnorableColumns
include CrossDatabaseIgnoredTables
+ include UseSqlFunctionForPrimaryKeyLookups
ignore_column :emails_disabled, remove_with: '16.3', remove_after: '2023-08-22'
@@ -140,8 +141,14 @@ class Project < ApplicationRecord
after_create -> { create_or_load_association(:pages_metadatum) }
after_create :set_timestamps_for_create
after_create :check_repository_absence!
+
+ # TODO: Remove this callback after background syncing is implemented. See https://gitlab.com/gitlab-org/gitlab/-/issues/429376.
+ after_update :update_catalog_resource,
+ if: -> { (saved_change_to_name? || saved_change_to_description? || saved_change_to_visibility_level?) && catalog_resource }
+
before_destroy :remove_private_deploy_keys
after_destroy :remove_exports
+
after_save :update_project_statistics, if: :saved_change_to_namespace_id?
after_save :schedule_sync_event_worker, if: -> { saved_change_to_id? || saved_change_to_namespace_id? }
@@ -457,8 +464,10 @@ class Project < ApplicationRecord
# GitLab Pages
has_many :pages_domains
has_one :pages_metadatum, class_name: 'ProjectPagesMetadatum', inverse_of: :project
- # we need to clean up files, not only remove records
- has_many :pages_deployments, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ # rubocop:disable Cop/ActiveRecordDependent -- we need to clean up files, not only remove records
+ has_many :pages_deployments, dependent: :destroy, inverse_of: :project
+ # 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
@@ -497,7 +506,7 @@ class Project < ApplicationRecord
delegate :merge_requests_access_level, :forking_access_level, :issues_access_level, :wiki_access_level, :snippets_access_level, :builds_access_level, :repository_access_level, :package_registry_access_level, :pages_access_level, :metrics_dashboard_access_level, :analytics_access_level, :operations_access_level, :security_and_compliance_access_level, :container_registry_access_level, :environments_access_level, :feature_flags_access_level, :monitor_access_level, :releases_access_level, :infrastructure_access_level, :model_experiments_access_level, to: :project_feature, allow_nil: true
delegate :name, to: :owner, allow_nil: true, prefix: true
- delegate :log_jira_dvcs_integration_usage, :jira_dvcs_server_last_sync_at, :jira_dvcs_cloud_last_sync_at, to: :feature_usage
+ delegate :jira_dvcs_server_last_sync_at, :jira_dvcs_cloud_last_sync_at, to: :feature_usage
delegate :last_pipeline, to: :commit, allow_nil: true
with_options to: :team do
@@ -620,42 +629,6 @@ class Project < ApplicationRecord
.or(arel_table[:storage_version].eq(nil)))
end
- scope :sorted_by_name_desc, -> {
- keyset_order = Gitlab::Pagination::Keyset::Order.build([
- Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
- attribute_name: :name,
- column_expression: Project.arel_table[:name],
- order_expression: Project.arel_table[:name].desc,
- distinct: false,
- nullable: :nulls_last
- ),
- Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
- attribute_name: :id,
- order_expression: Project.arel_table[:id].desc
- )
- ])
-
- reorder(keyset_order)
- }
-
- scope :sorted_by_name_asc, -> {
- keyset_order = Gitlab::Pagination::Keyset::Order.build([
- Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
- attribute_name: :name,
- column_expression: Project.arel_table[:name],
- order_expression: Project.arel_table[:name].asc,
- distinct: false,
- nullable: :nulls_last
- ),
- Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
- attribute_name: :id,
- order_expression: Project.arel_table[:id].asc
- )
- ])
-
- reorder(keyset_order)
- }
-
scope :sorted_by_updated_asc, -> { reorder(self.arel_table['updated_at'].asc) }
scope :sorted_by_updated_desc, -> { reorder(self.arel_table['updated_at'].desc) }
scope :sorted_by_stars_desc, -> { reorder(self.arel_table['star_count'].desc) }
@@ -769,7 +742,7 @@ class Project < ApplicationRecord
end
scope :with_pages_deployed, -> do
- joins(:pages_metadatum).merge(ProjectPagesMetadatum.deployed)
+ where_exists(PagesDeployment.active.where('pages_deployments.project_id = projects.id'))
end
scope :pages_metadata_not_migrated, -> do
@@ -1476,12 +1449,10 @@ class Project < ApplicationRecord
end
def build_or_assign_import_data(data: nil, credentials: nil)
- return if data.nil? && credentials.nil?
-
project_import_data = import_data || build_import_data
- project_import_data.merge_data(data.to_h)
- project_import_data.merge_credentials(credentials.to_h)
+ project_import_data.merge_data(data.to_h) if data
+ project_import_data.merge_credentials(credentials.to_h) if credentials
project_import_data
end
@@ -1564,9 +1535,9 @@ class Project < ApplicationRecord
limit = creator.projects_limit
error =
if limit == 0
- _('Personal project creation is not allowed. Please contact your administrator with questions')
+ _('You cannot create projects in your personal namespace. Contact your GitLab administrator.')
else
- _('Your project limit is %{limit} projects! Please contact your administrator to increase it')
+ _("You've reached your limit of %{limit} projects created. Contact your GitLab administrator.")
end
self.errors.add(:limit_reached, error % { limit: limit })
@@ -2236,11 +2207,11 @@ class Project < ApplicationRecord
end
def pages_deployed?
- pages_metadatum&.deployed?
+ active_pages_deployments.exists?
end
def pages_show_onboarding?
- !(pages_metadatum&.onboarding_complete || pages_metadatum&.deployed)
+ !(pages_metadatum&.onboarding_complete || pages_deployed?)
end
def remove_private_deploy_keys
@@ -2262,27 +2233,6 @@ class Project < ApplicationRecord
ensure_pages_metadatum.update!(onboarding_complete: true)
end
- def mark_pages_as_deployed
- ensure_pages_metadatum.update!(deployed: true)
- end
-
- def mark_pages_as_not_deployed
- ensure_pages_metadatum.update!(deployed: false)
- end
-
- def update_pages_deployment!(deployment)
- ensure_pages_metadatum.update!(pages_deployment: deployment)
- end
-
- def set_first_pages_deployment!(deployment)
- ensure_pages_metadatum
-
- # where().update_all to perform update in the single transaction with check for null
- ProjectPagesMetadatum
- .where(project_id: id, pages_deployment_id: nil)
- .update_all(deployed: deployment.present?, pages_deployment_id: deployment&.id)
- end
-
def set_full_path(gl_full_path: full_path)
# We'd need to keep track of project full path otherwise directory tree
# created with hashed storage enabled cannot be usefully imported using
@@ -2875,7 +2825,7 @@ class Project < ApplicationRecord
end
def uses_default_ci_config?
- ci_config_path.blank? || ci_config_path == Gitlab::FileDetector::PATTERNS[:gitlab_ci]
+ ci_config_path.blank? || Gitlab::FileDetector.type_of(ci_config_path) == :gitlab_ci
end
def limited_protected_branches(limit)
@@ -3026,7 +2976,7 @@ class Project < ApplicationRecord
end
def ci_config_for(sha)
- repository.gitlab_ci_yml_for(sha, ci_config_path_or_default)
+ repository.blob_data_at(sha, ci_config_path_or_default)
end
def enabled_group_deploy_keys
@@ -3530,6 +3480,10 @@ class Project < ApplicationRecord
pool_repository_shard == repository_storage
end
+
+ def update_catalog_resource
+ catalog_resource.sync_with_project!
+ end
end
Project.prepend_mod_with('Project')
diff --git a/app/models/project_feature_usage.rb b/app/models/project_feature_usage.rb
index dba81a6cb60..5e47ec6310d 100644
--- a/app/models/project_feature_usage.rb
+++ b/app/models/project_feature_usage.rb
@@ -19,19 +19,6 @@ class ProjectFeatureUsage < ApplicationRecord
end
end
- def log_jira_dvcs_integration_usage(cloud: true)
- ::Gitlab::Database::LoadBalancing::Session.without_sticky_writes do
- integration_field = self.class.jira_dvcs_integration_field(cloud: cloud)
-
- # The feature usage is used only once later to query the feature usage in a
- # long date range. Therefore, we just need to update the timestamp once per
- # day
- break if persisted? && updated_today?(integration_field)
-
- persist_jira_dvcs_usage(integration_field)
- end
- end
-
private
def updated_today?(integration_field)
diff --git a/app/models/project_pages_metadatum.rb b/app/models/project_pages_metadatum.rb
index eca2e5a740e..87cff4f2715 100644
--- a/app/models/project_pages_metadatum.rb
+++ b/app/models/project_pages_metadatum.rb
@@ -10,7 +10,4 @@ class ProjectPagesMetadatum < ApplicationRecord
belongs_to :project, inverse_of: :pages_metadatum
belongs_to :pages_deployment
-
- scope :deployed, -> { where(deployed: true) }
- scope :with_project_route_and_deployment, -> { preload(:pages_deployment, project: [:namespace, :route]) }
end
diff --git a/app/models/project_snippet.rb b/app/models/project_snippet.rb
index ffb08e10f1f..7a80ad33d68 100644
--- a/app/models/project_snippet.rb
+++ b/app/models/project_snippet.rb
@@ -5,4 +5,6 @@ class ProjectSnippet < Snippet
validates :project, presence: true
validates :secret, inclusion: { in: [false] }
+
+ scope :by_project, ->(project) { where(project: project) }
end
diff --git a/app/models/projects/repository_storage_move.rb b/app/models/projects/repository_storage_move.rb
index f4411e0b4fd..e2c6d1853a9 100644
--- a/app/models/projects/repository_storage_move.rb
+++ b/app/models/projects/repository_storage_move.rb
@@ -14,11 +14,6 @@ module Projects
alias_attribute :project, :container
scope :with_projects, -> { includes(container: :route) }
- override :update_repository_storage
- def update_repository_storage(new_storage)
- container.update_column(:repository_storage, new_storage)
- end
-
override :schedule_repository_storage_update_worker
def schedule_repository_storage_update_worker
Projects::UpdateRepositoryStorageWorker.perform_async(
diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb
index aebce59a040..40a1a4392dd 100644
--- a/app/models/protected_branch.rb
+++ b/app/models/protected_branch.rb
@@ -5,6 +5,7 @@ class ProtectedBranch < ApplicationRecord
include Gitlab::SQL::Pattern
include FromUnion
include EachBatch
+ include Presentable
belongs_to :group, foreign_key: :namespace_id, touch: true, inverse_of: :protected_branches
diff --git a/app/models/repository.rb b/app/models/repository.rb
index e565de9c4ba..e639a389e0a 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -1102,10 +1102,6 @@ class Repository
blob_data_at(sha, '.gitlab/route-map.yml')
end
- def gitlab_ci_yml_for(sha, path = '.gitlab-ci.yml')
- blob_data_at(sha, path)
- end
-
def lfsconfig_for(sha)
blob_data_at(sha, '.lfsconfig')
end
@@ -1245,6 +1241,10 @@ class Repository
def get_patch_id(old_revision, new_revision)
raw_repository.get_patch_id(old_revision, new_revision)
rescue Gitlab::Git::CommandError, Gitlab::Git::Repository::NoRepository => e
+ # This is expected when there are no differences between the old_revision and the new_revision.
+ # It's not ideal, but is simpler to handle this here than making breaking changes to gitaly.
+ return if e.message.match?(/no difference between old and new revision./)
+
Gitlab::ErrorTracking.track_exception(
e,
project_id: project.id,
diff --git a/app/models/resource_label_event.rb b/app/models/resource_label_event.rb
index d5c839724d4..ad1ce740c89 100644
--- a/app/models/resource_label_event.rb
+++ b/app/models/resource_label_event.rb
@@ -112,7 +112,7 @@ class ResourceLabelEvent < ResourceEvent
end
def resource_parent
- issuable.project || issuable.group
+ issuable.try(:resource_parent) || issuable.project || issuable.group
end
def discussion_id_key
diff --git a/app/models/service_desk/custom_email_credential.rb b/app/models/service_desk/custom_email_credential.rb
index 5986ac8a43f..82bda673491 100644
--- a/app/models/service_desk/custom_email_credential.rb
+++ b/app/models/service_desk/custom_email_credential.rb
@@ -2,6 +2,14 @@
module ServiceDesk
class CustomEmailCredential < ApplicationRecord
+ # Used to explicitly set the SMTP AUTH method.
+ # If nil Net::SMTP will choose one of methods listed by the SMTP server.
+ enum smtp_authentication: {
+ plain: 0,
+ login: 1,
+ cram_md5: 2
+ }
+
attr_encrypted :smtp_username,
mode: :per_attribute_iv,
algorithm: 'aes-256-gcm',
@@ -44,7 +52,8 @@ module ServiceDesk
password: smtp_password,
address: smtp_address,
domain: Mail::Address.new(service_desk_setting.custom_email).domain,
- port: smtp_port || 587
+ port: smtp_port || 587,
+ authentication: smtp_authentication
}
end
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index 78b0c0849e3..3e075fdaa9e 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -77,6 +77,7 @@ class Snippet < ApplicationRecord
scope :inc_relations_for_view, -> { includes(author: :status) }
scope :inc_statistics, -> { includes(:statistics) }
scope :with_statistics, -> { joins(:statistics) }
+ scope :with_repository_storage_moves, -> { joins(:repository_storage_moves) }
scope :inc_projects_namespace_route, -> { includes(project: [:route, :namespace]) }
scope :without_created_by_banned_user, -> do
diff --git a/app/models/snippet_repository.rb b/app/models/snippet_repository.rb
index a262802c8af..6b2fa99d547 100644
--- a/app/models/snippet_repository.rb
+++ b/app/models/snippet_repository.rb
@@ -31,6 +31,11 @@ class SnippetRepository < ApplicationRecord
options[:actions] = transform_file_entries(files)
+ # The Gitaly calls perform HTTP requests for permissions check
+ # Stick to the primary in order to make those requests aware that
+ # primary database must be used to fetch the data
+ self.class.sticking.stick(:user, user.id)
+
capture_git_error { repository.commit_files(user, **options) }
ensure
Gitlab::ExclusiveLease.cancel(lease_key, uuid)
diff --git a/app/models/system/broadcast_message.rb b/app/models/system/broadcast_message.rb
index 06f0115ade6..d959a6339a4 100644
--- a/app/models/system/broadcast_message.rb
+++ b/app/models/system/broadcast_message.rb
@@ -117,7 +117,7 @@ module System
end
def ended?
- ends_at < Time.current
+ ends_at.past?
end
def now?
diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb
index dc93decce5e..8624a1a9463 100644
--- a/app/models/system_note_metadata.rb
+++ b/app/models/system_note_metadata.rb
@@ -4,6 +4,7 @@ class SystemNoteMetadata < ApplicationRecord
include Importable
include IgnorableColumns
+ ignore_column :id_convert_to_bigint, remove_with: '16.9', remove_after: '2024-01-13'
ignore_column :note_id_convert_to_bigint, remove_with: '16.7', remove_after: '2023-11-16'
# These notes's action text might contain a reference that is external.
diff --git a/app/models/upload.rb b/app/models/upload.rb
index 59ce9a1f37a..745a6174931 100644
--- a/app/models/upload.rb
+++ b/app/models/upload.rb
@@ -174,7 +174,7 @@ class Upload < ApplicationRecord
end
def update_project_statistics
- ProjectCacheWorker.perform_async(model_id, [], [:uploads_size])
+ ProjectCacheWorker.perform_async(model_id, [], ['uploads_size'])
end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 4034677509f..25f22563136 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -32,6 +32,7 @@ class User < MainClusterwide::ApplicationRecord
include EachBatch
include CrossDatabaseIgnoredTables
include IgnorableColumns
+ include UseSqlFunctionForPrimaryKeyLookups
ignore_column %i[
email_opted_in
@@ -48,7 +49,7 @@ class User < MainClusterwide::ApplicationRecord
# Associations with dependent: option
cross_database_ignore_tables(
- %w[namespaces projects project_authorizations issues merge_requests merge_requests issues issues merge_requests],
+ %w[namespaces projects project_authorizations issues merge_requests merge_requests issues issues merge_requests events],
url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/424285',
on: :destroy
)
@@ -390,6 +391,7 @@ class User < MainClusterwide::ApplicationRecord
:first_day_of_week, :first_day_of_week=,
:timezone, :timezone=,
:time_display_relative, :time_display_relative=,
+ :time_display_format, :time_display_format=,
:show_whitespace_in_diffs, :show_whitespace_in_diffs=,
:view_diffs_file_by_file, :view_diffs_file_by_file=,
:pass_user_identities_to_ci_jwt, :pass_user_identities_to_ci_jwt=,
@@ -417,6 +419,7 @@ class User < MainClusterwide::ApplicationRecord
delegate :pronouns, :pronouns=, to: :user_detail, allow_nil: true
delegate :pronunciation, :pronunciation=, to: :user_detail, allow_nil: true
delegate :registration_objective, :registration_objective=, to: :user_detail, allow_nil: true
+ delegate :mastodon, :mastodon=, to: :user_detail, allow_nil: true
delegate :linkedin, :linkedin=, to: :user_detail, allow_nil: true
delegate :twitter, :twitter=, to: :user_detail, allow_nil: true
delegate :skype, :skype=, to: :user_detail, allow_nil: true
@@ -425,6 +428,7 @@ class User < MainClusterwide::ApplicationRecord
delegate :organization, :organization=, to: :user_detail, allow_nil: true
delegate :discord, :discord=, to: :user_detail, allow_nil: true
delegate :email_reset_offered_at, :email_reset_offered_at=, to: :user_detail, allow_nil: true
+ delegate :project_authorizations_recalculated_at, :project_authorizations_recalculated_at=, to: :user_detail, allow_nil: true
accepts_nested_attributes_for :user_preference, update_only: true
accepts_nested_attributes_for :user_detail, update_only: true
@@ -600,6 +604,12 @@ class User < MainClusterwide::ApplicationRecord
scope :by_provider_and_extern_uid, ->(provider, extern_uid) { joins(:identities).merge(Identity.with_extern_uid(provider, extern_uid)) }
scope :by_ids_or_usernames, -> (ids, usernames) { where(username: usernames).or(where(id: ids)) }
scope :without_forbidden_states, -> { where.not(state: FORBIDDEN_SEARCH_STATES) }
+ scope :trusted, -> do
+ where('EXISTS (?)', ::UserCustomAttribute
+ .select(1)
+ .where('user_id = users.id')
+ .trusted_with_spam)
+ end
strip_attributes! :name
@@ -768,6 +778,8 @@ class User < MainClusterwide::ApplicationRecord
external
when 'deactivated'
deactivated
+ when "trusted"
+ trusted
else
active_without_ghosts
end
@@ -791,9 +803,9 @@ class User < MainClusterwide::ApplicationRecord
order = <<~SQL
CASE
- WHEN LOWER(users.name) = :query THEN 0
+ WHEN LOWER(users.public_email) = :query THEN 0
WHEN LOWER(users.username) = :query THEN 1
- WHEN LOWER(users.public_email) = :query THEN 2
+ WHEN LOWER(users.name) = :query THEN 2
ELSE 3
END
SQL
@@ -1081,7 +1093,7 @@ class User < MainClusterwide::ApplicationRecord
def otp_secret_expired?
return true unless otp_secret_expires_at
- otp_secret_expires_at < Time.current
+ otp_secret_expires_at.past?
end
def update_otp_secret!
@@ -1446,7 +1458,7 @@ class User < MainClusterwide::ApplicationRecord
if !Gitlab.config.ldap.enabled
false
elsif ldap_user?
- !last_credential_check_at || (last_credential_check_at + ldap_sync_time) < Time.current
+ !last_credential_check_at || (last_credential_check_at + ldap_sync_time).past?
else
false
end
@@ -2087,7 +2099,7 @@ class User < MainClusterwide::ApplicationRecord
end
def password_expired?
- !!(password_expires_at && password_expires_at < Time.current)
+ !!(password_expires_at && password_expires_at.past?)
end
def password_expired_if_applicable?
diff --git a/app/models/user_custom_attribute.rb b/app/models/user_custom_attribute.rb
index 728c1f4844a..5a592b425df 100644
--- a/app/models/user_custom_attribute.rb
+++ b/app/models/user_custom_attribute.rb
@@ -20,6 +20,7 @@ class UserCustomAttribute < ApplicationRecord
TRUSTED_BY = 'trusted_by'
AUTO_BANNED_BY = 'auto_banned_by'
IDENTITY_VERIFICATION_PHONE_EXEMPT = 'identity_verification_phone_exempt'
+ IDENTITY_VERIFICATION_EXEMPT = 'identity_verification_exempt'
class << self
def upsert_custom_attributes(custom_attributes)
diff --git a/app/models/user_detail.rb b/app/models/user_detail.rb
index 9ac814eebda..bbb08ed5774 100644
--- a/app/models/user_detail.rb
+++ b/app/models/user_detail.rb
@@ -17,10 +17,24 @@ class UserDetail < MainClusterwide::ApplicationRecord
DEFAULT_FIELD_LENGTH = 500
+ MASTODON_VALIDATION_REGEX = /
+ \A # beginning of string
+ @?\b # optional leading at
+ ([\w\d.%+-]+) # character group to pick up words in user portion of username
+ @ # separator between user and host
+ ( # beginning of charagter group for host portion
+ [\w\d.-]+ # character group to pick up words in host portion of username
+ \.\w{2,} # pick up tld of host domain, 2 chars or more
+ )\b # end of character group to pick up words in host portion of username
+ \z # end of string
+ /x
+
validates :discord, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true
validate :discord_format
validates :linkedin, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true
validates :location, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true
+ validates :mastodon, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true
+ validate :mastodon_format
validates :organization, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true
validates :skype, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true
validates :twitter, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true
@@ -32,7 +46,7 @@ class UserDetail < MainClusterwide::ApplicationRecord
enum registration_objective: REGISTRATION_OBJECTIVE_PAIRS, _suffix: true
def sanitize_attrs
- %i[discord linkedin skype twitter website_url].each do |attr|
+ %i[discord linkedin mastodon skype twitter website_url].each do |attr|
value = self[attr]
self[attr] = Sanitize.clean(value) if value.present?
end
@@ -49,6 +63,7 @@ class UserDetail < MainClusterwide::ApplicationRecord
self.discord = '' if discord.nil?
self.linkedin = '' if linkedin.nil?
self.location = '' if location.nil?
+ self.mastodon = '' if mastodon.nil?
self.organization = '' if organization.nil?
self.skype = '' if skype.nil?
self.twitter = '' if twitter.nil?
@@ -62,4 +77,10 @@ def discord_format
errors.add(:discord, _('must contain only a discord user ID.'))
end
+def mastodon_format
+ return if mastodon.blank? || mastodon =~ UserDetail::MASTODON_VALIDATION_REGEX
+
+ errors.add(:mastodon, _('must contain only a mastodon username.'))
+end
+
UserDetail.prepend_mod_with('UserDetail')
diff --git a/app/models/user_preference.rb b/app/models/user_preference.rb
index 8fc9f4617d0..59cfe9a8426 100644
--- a/app/models/user_preference.rb
+++ b/app/models/user_preference.rb
@@ -7,6 +7,7 @@ class UserPreference < MainClusterwide::ApplicationRecord
# enum options with same name for multiple fields, also it creates
# extra methods that aren't really needed here.
NOTES_FILTERS = { all_notes: 0, only_comments: 1, only_activity: 2 }.freeze
+ TIME_DISPLAY_FORMATS = { system: 0, non_iso_format: 1, iso_format: 2 }.freeze
belongs_to :user
@@ -27,12 +28,15 @@ class UserPreference < MainClusterwide::ApplicationRecord
validates :pinned_nav_items, json_schema: { filename: 'pinned_nav_items' }
+ validates :time_display_format, inclusion: { in: TIME_DISPLAY_FORMATS.values }, presence: true
+
ignore_columns :experience_level, remove_with: '14.10', remove_after: '2021-03-22'
# 2023-06-22 is after 16.1 release and during 16.2 release https://docs.gitlab.com/ee/development/database/avoiding_downtime_in_migrations.html#ignoring-the-column-release-m
ignore_columns :use_legacy_web_ide, remove_with: '16.2', remove_after: '2023-06-22'
attribute :tab_width, default: -> { Gitlab::TabWidth::DEFAULT }
attribute :time_display_relative, default: true
+ attribute :time_display_format, default: 0
attribute :render_whitespace_in_code, default: false
attribute :project_shortcut_buttons, default: true
attribute :keyboard_shortcuts_enabled, default: true
@@ -80,6 +84,16 @@ class UserPreference < MainClusterwide::ApplicationRecord
end
end
+ class << self
+ def time_display_formats
+ {
+ s_('Time Display|System') => TIME_DISPLAY_FORMATS[:system],
+ s_('Time Display|12-hour: 2:34 PM') => TIME_DISPLAY_FORMATS[:non_iso_format],
+ s_('Time Display|24-hour: 14:34') => TIME_DISPLAY_FORMATS[:iso_format]
+ }
+ end
+ end
+
def time_display_relative
value = read_attribute(:time_display_relative)
return value unless value.nil?
diff --git a/app/models/users/anonymous.rb b/app/models/users/anonymous.rb
new file mode 100644
index 00000000000..b4a182ba203
--- /dev/null
+++ b/app/models/users/anonymous.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Users
+ class Anonymous
+ class << self
+ def can?(action, subject = :global)
+ Ability.allowed?(nil, action, subject)
+ end
+ end
+ end
+end
diff --git a/app/models/users/callout.rb b/app/models/users/callout.rb
index 60dd89c3ee7..a9880e56e8c 100644
--- a/app/models/users/callout.rb
+++ b/app/models/users/callout.rb
@@ -65,18 +65,19 @@ module Users
# 62, removed in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131314
# 63 and 64 were removed with https://gitlab.com/gitlab-org/gitlab/-/merge_requests/120233
branch_rules_info_callout: 65,
- create_runner_workflow_banner: 66,
+ # 66 removed in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/135470/
# 67 removed in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121920
project_repository_limit_alert_warning_threshold: 68, # EE-only
project_repository_limit_alert_alert_threshold: 69, # EE-only
project_repository_limit_alert_error_threshold: 70, # EE-only
- new_navigation_callout: 71,
+ # 71 removed in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134432
# 72 removed in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/129022
namespace_over_storage_users_combined_alert: 73, # EE-only
# 74 removed in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132751
vsd_feedback_banner: 75, # EE-only
security_policy_protected_branch_modification: 76, # EE-only
- vulnerability_report_grouping: 77 # EE-only
+ vulnerability_report_grouping: 77, # EE-only
+ new_nav_for_everyone_callout: 78
}
validates :feature_name,
diff --git a/app/models/users/credit_card_validation.rb b/app/models/users/credit_card_validation.rb
index 276d549006f..6d0a22c8b0a 100644
--- a/app/models/users/credit_card_validation.rb
+++ b/app/models/users/credit_card_validation.rb
@@ -2,10 +2,16 @@
module Users
class CreditCardValidation < ApplicationRecord
+ include IgnorableColumns
+
RELEASE_DAY = Date.new(2021, 5, 17)
self.table_name = 'user_credit_card_validations'
+ ignore_columns %i[last_digits network holder_name expiration_date], remove_with: '16.8', remove_after: '2023-12-22'
+
+ attr_accessor :last_digits, :network, :holder_name, :expiration_date
+
belongs_to :user
belongs_to :banned_user, class_name: '::Users::BannedUser', foreign_key: :user_id,
inverse_of: :credit_card_validation
diff --git a/app/models/users/group_visit.rb b/app/models/users/group_visit.rb
index 0bcfda049fc..d7c76e2ee2c 100644
--- a/app/models/users/group_visit.rb
+++ b/app/models/users/group_visit.rb
@@ -13,5 +13,12 @@ module Users
validates :entity_id, presence: true
validates :user_id, presence: true
validates :visited_at, presence: true
+
+ MAX_FRECENT_ITEMS = 3
+
+ def self.frecent_groups(user_id:)
+ ids = frecent_visits_scores(user_id: user_id, limit: MAX_FRECENT_ITEMS).pluck("entity_id")
+ Group.find(ids)
+ end
end
end
diff --git a/app/models/users/phone_number_validation.rb b/app/models/users/phone_number_validation.rb
index e033445d76b..2256eb8ddc4 100644
--- a/app/models/users/phone_number_validation.rb
+++ b/app/models/users/phone_number_validation.rb
@@ -41,6 +41,10 @@ module Users
).exists?
end
+ def self.by_reference_id(ref_id)
+ find_by(telesign_reference_xid: ref_id)
+ end
+
def validated?
validated_at.present?
end
diff --git a/app/models/users/project_visit.rb b/app/models/users/project_visit.rb
index 1d076e0be56..9ff3d8d2c91 100644
--- a/app/models/users/project_visit.rb
+++ b/app/models/users/project_visit.rb
@@ -13,5 +13,12 @@ module Users
validates :entity_id, presence: true
validates :user_id, presence: true
validates :visited_at, presence: true
+
+ MAX_FRECENT_ITEMS = 5
+
+ def self.frecent_projects(user_id:)
+ ids = frecent_visits_scores(user_id: user_id, limit: MAX_FRECENT_ITEMS).pluck("entity_id")
+ Project.find(ids)
+ end
end
end
diff --git a/app/models/vs_code/settings/vs_code_setting.rb b/app/models/vs_code/settings/vs_code_setting.rb
index e55d958d2b4..1401ce82045 100644
--- a/app/models/vs_code/settings/vs_code_setting.rb
+++ b/app/models/vs_code/settings/vs_code_setting.rb
@@ -5,7 +5,9 @@ module VsCode
class VsCodeSetting < ApplicationRecord
belongs_to :user, inverse_of: :vscode_settings
- validates :setting_type, presence: true
+ validates :setting_type, presence: true,
+ inclusion: { in: SETTINGS_TYPES },
+ uniqueness: { scope: :user_id }
validates :content, presence: true
scope :by_setting_type, ->(setting_type) { where(setting_type: setting_type) }
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index 2eed693ca76..3dd8f334a68 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -80,6 +80,7 @@ class WikiPage
alias_method :to_param, :slug
def human_title
+ return front_matter_title if Feature.enabled?(:wiki_front_matter_title, container) && front_matter_title.present?
return 'Home' if title == Wiki::HOMEPAGE
title
@@ -95,6 +96,10 @@ class WikiPage
attributes[:title] = new_title
end
+ def front_matter_title
+ front_matter[:title]
+ end
+
def raw_content
attributes[:content] ||= page&.text_data
end
@@ -320,7 +325,7 @@ class WikiPage
def serialize_front_matter(hash)
return '' unless hash.present?
- YAML.dump(hash.transform_keys(&:to_s)) + "---\n"
+ YAML.dump(hash.to_h.transform_keys(&:to_s)) + "---\n"
end
def update_front_matter(attrs)
diff --git a/app/models/work_item.rb b/app/models/work_item.rb
index 0761a213532..a62d77939bf 100644
--- a/app/models/work_item.rb
+++ b/app/models/work_item.rb
@@ -73,6 +73,19 @@ class WorkItem < Issue
includes(:parent_link).order(keyset_order)
end
+ def linked_items_keyset_order
+ ::Gitlab::Pagination::Keyset::Order.build(
+ [
+ ::Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
+ attribute_name: 'issue_link_id',
+ column_expression: IssueLink.arel_table[:id],
+ order_expression: IssueLink.arel_table[:id].asc,
+ nullable: :not_nullable,
+ distinct: true
+ )
+ ])
+ end
+
override :related_link_class
def related_link_class
WorkItems::RelatedWorkItemLink
@@ -150,7 +163,9 @@ class WorkItem < Issue
def linked_work_items(current_user = nil, authorize: true, preload: nil, link_type: nil)
return [] if new_record?
- linked_work_items = linked_work_items_query(link_type).preload(preload).reorder('issue_link_id')
+ linked_work_items = linked_work_items_query(link_type)
+ .preload(preload)
+ .reorder(self.class.linked_items_keyset_order)
return linked_work_items unless authorize
cross_project_filter = ->(work_items) { work_items.where(project: project) }
diff --git a/app/policies/abuse_report_policy.rb b/app/policies/abuse_report_policy.rb
index f1f994e6a42..043dbd0cb89 100644
--- a/app/policies/abuse_report_policy.rb
+++ b/app/policies/abuse_report_policy.rb
@@ -3,5 +3,6 @@
class AbuseReportPolicy < ::BasePolicy
rule { admin }.policy do
enable :read_abuse_report
+ enable :create_note
end
end
diff --git a/app/policies/analytics/cycle_analytics/value_stream_policy.rb b/app/policies/analytics/cycle_analytics/value_stream_policy.rb
new file mode 100644
index 00000000000..7e236f94e91
--- /dev/null
+++ b/app/policies/analytics/cycle_analytics/value_stream_policy.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Analytics
+ module CycleAnalytics
+ class ValueStreamPolicy < ::BasePolicy
+ delegate { subject.namespace }
+ end
+ end
+end
diff --git a/app/policies/base_policy.rb b/app/policies/base_policy.rb
index 1ec2495a661..462afbaa475 100644
--- a/app/policies/base_policy.rb
+++ b/app/policies/base_policy.rb
@@ -37,7 +37,7 @@ class BasePolicy < DeclarativePolicy::Base
desc "User is security policy bot"
with_options scope: :user, score: 0
- condition(:security_policy_bot) { @user&.security_policy_bot? }
+ condition(:security_policy_bot) { false }
desc "User is automation bot"
with_options scope: :user, score: 0
diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb
index bce7ceafe17..71ea42e1f23 100644
--- a/app/policies/ci/build_policy.rb
+++ b/app/policies/ci/build_policy.rb
@@ -81,6 +81,7 @@ module Ci
end
rule { ~can?(:jailbreak) & (archived | protected_ref) }.policy do
+ prevent :cancel_build
prevent :update_build
prevent :erase_build
end
@@ -88,6 +89,7 @@ module Ci
rule { can?(:admin_build) | (can?(:update_build) & owner_of_job & unprotected_ref) }.enable :erase_build
rule { can?(:public_access) & branch_allows_collaboration }.policy do
+ enable :cancel_build
enable :update_build
enable :update_commit_status
end
diff --git a/app/policies/ci/deployable_policy.rb b/app/policies/ci/deployable_policy.rb
index f0105b001f2..e83bdd5361a 100644
--- a/app/policies/ci/deployable_policy.rb
+++ b/app/policies/ci/deployable_policy.rb
@@ -11,7 +11,10 @@ module Ci
@subject.outdated_deployment?
end
- rule { outdated_deployment }.prevent :update_build
+ rule { outdated_deployment }.policy do
+ prevent :cancel_build
+ prevent :update_build
+ end
end
end
end
diff --git a/app/policies/ci/pipeline_policy.rb b/app/policies/ci/pipeline_policy.rb
index 1d60b1e79de..c01162a86df 100644
--- a/app/policies/ci/pipeline_policy.rb
+++ b/app/policies/ci/pipeline_policy.rb
@@ -27,10 +27,14 @@ module Ci
prevent :read_pipeline
end
- rule { protected_ref }.prevent :update_pipeline
+ rule { protected_ref }.policy do
+ prevent :update_pipeline
+ prevent :cancel_pipeline
+ end
rule { can?(:public_access) & branch_allows_collaboration }.policy do
enable :update_pipeline
+ enable :cancel_pipeline
end
rule { can?(:owner_access) }.policy do
diff --git a/app/policies/concerns/policy_actor.rb b/app/policies/concerns/policy_actor.rb
index e000f1514e5..8fa09683b06 100644
--- a/app/policies/concerns/policy_actor.rb
+++ b/app/policies/concerns/policy_actor.rb
@@ -53,10 +53,6 @@ module PolicyActor
false
end
- def security_policy_bot?
- false
- end
-
def automation_bot?
false
end
diff --git a/app/policies/container_registry/protection/rule_policy.rb b/app/policies/container_registry/protection/rule_policy.rb
new file mode 100644
index 00000000000..4dc8dba3276
--- /dev/null
+++ b/app/policies/container_registry/protection/rule_policy.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module ContainerRegistry
+ module Protection
+ class RulePolicy < BasePolicy
+ delegate { @subject.project }
+ end
+ end
+end
diff --git a/app/policies/global_policy.rb b/app/policies/global_policy.rb
index 7594360a91c..175f86c9673 100644
--- a/app/policies/global_policy.rb
+++ b/app/policies/global_policy.rb
@@ -63,10 +63,6 @@ class GlobalPolicy < BasePolicy
prevent :access_git
end
- rule { security_policy_bot }.policy do
- enable :access_git
- end
-
rule { project_bot | service_account }.policy do
prevent :log_in
prevent :receive_notifications
diff --git a/app/policies/group_group_link_policy.rb b/app/policies/group_group_link_policy.rb
new file mode 100644
index 00000000000..0108f0b7fca
--- /dev/null
+++ b/app/policies/group_group_link_policy.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+class GroupGroupLinkPolicy < ::BasePolicy # rubocop:disable Gitlab/NamespacedClass
+ condition(:can_read_shared_with_group) { can?(:read_group, @subject.shared_with_group) }
+ condition(:group_member) { @subject.shared_group.member?(@user) }
+
+ rule { can_read_shared_with_group | group_member }.enable :read_shared_with_group
+end
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index 2ab59f5a34d..ca170133105 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -121,6 +121,7 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
enable :upload_file
enable :guest_access
enable :read_release
+ enable :award_emoji
end
rule { admin }.policy do
diff --git a/app/policies/issue_policy.rb b/app/policies/issue_policy.rb
index 6114785a851..683c53d8d78 100644
--- a/app/policies/issue_policy.rb
+++ b/app/policies/issue_policy.rb
@@ -57,7 +57,10 @@ class IssuePolicy < IssuablePolicy
prevent :read_issue
end
- rule { ~can?(:read_issue) }.prevent :create_note
+ rule { ~can?(:read_issue) }.policy do
+ prevent :create_note
+ prevent :read_note
+ end
rule { locked }.policy do
prevent :reopen_issue
diff --git a/app/policies/namespaces/group_project_namespace_shared_policy.rb b/app/policies/namespaces/group_project_namespace_shared_policy.rb
index b24cb5be607..81bb5d6289e 100644
--- a/app/policies/namespaces/group_project_namespace_shared_policy.rb
+++ b/app/policies/namespaces/group_project_namespace_shared_policy.rb
@@ -22,6 +22,7 @@ module Namespaces
enable :create_work_item
enable :read_work_item
enable :read_issue
+ enable :read_note
enable :read_namespace
enable :read_namespace_via_membership
end
diff --git a/app/policies/project_group_link_policy.rb b/app/policies/project_group_link_policy.rb
index 00bb246d70b..7ad2985ecc5 100644
--- a/app/policies/project_group_link_policy.rb
+++ b/app/policies/project_group_link_policy.rb
@@ -2,9 +2,13 @@
class ProjectGroupLinkPolicy < BasePolicy # rubocop:disable Gitlab/NamespacedClass
condition(:group_owner_or_project_admin) { group_owner? || project_admin? }
+ condition(:can_read_group) { can?(:read_group, @subject.group) }
+ condition(:project_member) { @subject.project.member?(@user) }
rule { group_owner_or_project_admin }.enable :admin_project_group_link
+ rule { can_read_group | project_member }.enable :read_shared_with_group
+
private
def group_owner?
diff --git a/app/policies/project_import_state_policy.rb b/app/policies/project_import_state_policy.rb
new file mode 100644
index 00000000000..c2cd03337b7
--- /dev/null
+++ b/app/policies/project_import_state_policy.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class ProjectImportStatePolicy < ::BasePolicy # rubocop:disable Gitlab/NamespacedClass -- required by DeclarativePolicy lookup logic
+ delegate { @subject.project }
+end
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index 20f88577d67..bbb0e3df500 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -38,9 +38,6 @@ class ProjectPolicy < BasePolicy
desc "User is a project bot"
condition(:project_bot) { user.project_bot? && team_member? }
- desc "User is a security policy bot on the project"
- condition(:security_policy_bot) { user&.security_policy_bot? && team_member? }
-
desc "Project is public"
condition(:public_project, scope: :subject, score: 0) { project.public? }
@@ -136,6 +133,29 @@ class ProjectPolicy < BasePolicy
!@user&.from_ci_job_token? || @user.ci_job_token_scope.accessible?(project)
end
+ desc "If the user is via CI job token and project container registry visibility allows access"
+ condition(:job_token_container_registry) { job_token_access_allowed_to?(:container_registry) }
+
+ desc "If the user is via CI job token and project package registry visibility allows access"
+ condition(:job_token_package_registry) { job_token_access_allowed_to?(:package_registry) }
+
+ desc "If the user is via CI job token and project ci/cd visibility allows access"
+ condition(:job_token_builds) { job_token_access_allowed_to?(:builds) }
+
+ desc "If the user is via CI job token and project releases visibility allows access"
+ condition(:job_token_releases) { job_token_access_allowed_to?(:releases) }
+
+ desc "If the user is via CI job token and project environment visibility allows access"
+ condition(:job_token_environments) { job_token_access_allowed_to?(:environments) }
+
+ desc "If the project is either public or internal"
+ condition(:public_or_internal) do
+ project.public? || project.internal?
+ end
+
+ with_scope :subject
+ condition(:restrict_job_token_enabled) { Feature.enabled?(:restrict_ci_job_token_for_public_and_internal_projects, @subject) }
+
with_scope :subject
condition(:forking_allowed) do
@subject.feature_available?(:forking, @user)
@@ -303,6 +323,8 @@ class ProjectPolicy < BasePolicy
enable :set_show_diff_preview_in_email
enable :set_warn_about_potentially_unwanted_characters
enable :manage_owners
+
+ enable :add_catalog_resource
end
rule { can?(:guest_access) }.policy do
@@ -469,6 +491,7 @@ class ProjectPolicy < BasePolicy
enable :update_commit_status
enable :create_build
enable :update_build
+ enable :cancel_build
enable :read_resource_group
enable :update_resource_group
enable :create_merge_request_from
@@ -512,6 +535,7 @@ class ProjectPolicy < BasePolicy
rule { can?(:developer_access) & user_confirmed? }.policy do
enable :create_pipeline
enable :update_pipeline
+ enable :cancel_pipeline
enable :create_pipeline_schedule
end
@@ -640,6 +664,7 @@ class ProjectPolicy < BasePolicy
rule { builds_disabled | repository_disabled }.policy do
prevent(*create_read_update_admin_destroy(:build))
+ prevent :cancel_build
prevent(*create_read_update_admin_destroy(:pipeline_schedule))
prevent(*create_read_update_admin_destroy(:environment))
prevent(*create_read_update_admin_destroy(:deployment))
@@ -652,6 +677,7 @@ class ProjectPolicy < BasePolicy
# - We prevent the user from accessing Pipelines
rule { (builds_disabled & ~internal_builds_disabled) | repository_disabled }.policy do
prevent(*create_read_update_admin_destroy(:pipeline))
+ prevent :cancel_pipeline
prevent(*create_read_update_admin_destroy(:commit_status))
end
@@ -679,8 +705,42 @@ class ProjectPolicy < BasePolicy
enable :read_project_for_iids
end
+ # If the project is private
rule { ~public_project & ~internal_access & ~project_allowed_for_job_token }.prevent_all
+ # If this project is public or internal we want to prevent all aside from a few public policies
+ rule { public_or_internal & ~project_allowed_for_job_token & restrict_job_token_enabled }.policy do
+ prevent :guest_access
+ prevent :public_access
+ prevent :public_user_access
+ prevent :reporter_access
+ prevent :developer_access
+ prevent :maintainer_access
+ prevent :owner_access
+ end
+
+ rule { public_or_internal & job_token_container_registry & restrict_job_token_enabled }.policy do
+ enable :build_read_container_image
+ enable :read_container_image
+ end
+
+ rule { public_or_internal & job_token_package_registry & restrict_job_token_enabled }.policy do
+ enable :read_package
+ enable :read_project
+ end
+
+ rule { public_or_internal & job_token_builds & restrict_job_token_enabled }.policy do
+ enable :read_commit_status # this is additionally needed to download artifacts
+ end
+
+ rule { public_or_internal & job_token_releases & restrict_job_token_enabled }.policy do
+ enable :read_release
+ end
+
+ rule { public_or_internal & job_token_environments & restrict_job_token_enabled }.policy do
+ enable :read_environment
+ end
+
rule { can?(:public_access) }.policy do
enable :read_package
enable :read_project
@@ -908,14 +968,14 @@ class ProjectPolicy < BasePolicy
enable :read_namespace_catalog
end
- rule { can?(:owner_access) & namespace_catalog_available }.policy do
- enable :add_catalog_resource
- end
-
rule { model_registry_enabled }.policy do
enable :read_model_registry
end
+ rule { can?(:reporter_access) & model_registry_enabled }.policy do
+ enable :write_model_registry
+ end
+
rule { model_experiments_enabled }.policy do
enable :read_model_experiments
end
@@ -1007,6 +1067,20 @@ class ProjectPolicy < BasePolicy
end
end
+ def job_token_access_allowed_to?(feature)
+ return false unless @user&.from_ci_job_token?
+ return false unless project.project_feature
+
+ case project.project_feature.access_level(feature)
+ when ProjectFeature::DISABLED
+ false
+ when ProjectFeature::PRIVATE
+ @user.ci_job_token_scope.accessible?(project)
+ else
+ true
+ end
+ end
+
def resource_access_token_feature_available?
true
end
diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb
index 2fd198b8cf4..04fbc8467c9 100644
--- a/app/policies/user_policy.rb
+++ b/app/policies/user_policy.rb
@@ -29,6 +29,7 @@ class UserPolicy < BasePolicy
enable :read_user_personal_access_tokens
enable :read_group_count
enable :read_user_groups
+ enable :read_user_organizations
enable :read_saved_replies
enable :read_user_email_address
enable :admin_user_email_address
diff --git a/app/presenters/clusters/cluster_presenter.rb b/app/presenters/clusters/cluster_presenter.rb
index ec1dc96c2e3..5765d08dfb3 100644
--- a/app/presenters/clusters/cluster_presenter.rb
+++ b/app/presenters/clusters/cluster_presenter.rb
@@ -61,7 +61,7 @@ module Clusters
'clusters-path': clusterable.index_path,
'dashboard-endpoint': clusterable.metrics_dashboard_path(cluster),
'documentation-path': help_page_path('user/infrastructure/clusters/manage/clusters_health'),
- 'add-dashboard-documentation-path': help_page_path('operations/metrics/dashboards/index.md', anchor: 'add-a-new-dashboard-to-your-project'),
+ 'add-dashboard-documentation-path': help_page_path('operations/metrics/dashboards/index', anchor: 'add-a-new-dashboard-to-your-project'),
'empty-getting-started-svg-path': image_path('illustrations/monitoring/getting_started.svg'),
'empty-loading-svg-path': image_path('illustrations/monitoring/loading.svg'),
'empty-no-data-svg-path': image_path('illustrations/monitoring/no_data.svg'),
diff --git a/app/presenters/commit_status_presenter.rb b/app/presenters/commit_status_presenter.rb
index f6720546fab..0858fad1e1a 100644
--- a/app/presenters/commit_status_presenter.rb
+++ b/app/presenters/commit_status_presenter.rb
@@ -1,4 +1,5 @@
# frozen_string_literal: true
+
class CommitStatusPresenter < Gitlab::View::Presenter::Delegated
CALLOUT_FAILURE_MESSAGES = {
unknown_failure: 'There is an unknown failure, please try again',
diff --git a/app/presenters/member_presenter.rb b/app/presenters/member_presenter.rb
index 4cdaca3c39e..7acaa704368 100644
--- a/app/presenters/member_presenter.rb
+++ b/app/presenters/member_presenter.rb
@@ -15,6 +15,10 @@ class MemberPresenter < Gitlab::View::Presenter::Delegated
end
end
+ def valid_member_roles
+ []
+ end
+
def can_resend_invite?
invite? &&
can?(current_user, admin_member_permission, source)
@@ -37,6 +41,11 @@ class MemberPresenter < Gitlab::View::Presenter::Delegated
false
end
+ # This functionality is only available in EE.
+ def custom_permissions
+ []
+ end
+
def last_owner?
raise NotImplementedError
end
diff --git a/app/presenters/ml/model_presenter.rb b/app/presenters/ml/model_presenter.rb
index 388e2b73bc1..24d30af1d4e 100644
--- a/app/presenters/ml/model_presenter.rb
+++ b/app/presenters/ml/model_presenter.rb
@@ -5,17 +5,31 @@ module Ml
presents ::Ml::Model, as: :model
def latest_version_name
- model.latest_version&.version
+ latest_version&.version
+ end
+
+ def version_count
+ return model.version_count if model.respond_to?(:version_count)
+
+ model.versions.size
end
def latest_package_path
- return unless model.latest_version&.package_id.present?
+ latest_version&.package_path
+ end
- Gitlab::Routing.url_helpers.project_package_path(model.project, model.latest_version.package_id)
+ def latest_version_path
+ latest_version&.path
end
def path
- Gitlab::Routing.url_helpers.project_ml_model_path(model.project, model.id)
+ project_ml_model_path(model.project, model.id)
+ end
+
+ private
+
+ def latest_version
+ model.latest_version&.present
end
end
end
diff --git a/app/presenters/ml/model_version_presenter.rb b/app/presenters/ml/model_version_presenter.rb
new file mode 100644
index 00000000000..210b213ca2a
--- /dev/null
+++ b/app/presenters/ml/model_version_presenter.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Ml
+ class ModelVersionPresenter < Gitlab::View::Presenter::Delegated
+ presents ::Ml::ModelVersion, as: :model_version
+
+ def display_name
+ "#{model_version.model.name} / #{model_version.version}"
+ end
+
+ def path
+ project_ml_model_version_path(
+ model_version.model.project,
+ model_version.model,
+ model_version
+ )
+ end
+
+ def package_path
+ return unless model_version.package_id.present?
+
+ project_package_path(model_version.project, model_version.package_id)
+ end
+ end
+end
diff --git a/app/presenters/project_presenter.rb b/app/presenters/project_presenter.rb
index 4533ef3633d..c983d8623d2 100644
--- a/app/presenters/project_presenter.rb
+++ b/app/presenters/project_presenter.rb
@@ -11,6 +11,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
include ChecksCollaboration
include Gitlab::Utils::StrongMemoize
include Gitlab::Experiment::Dsl
+ include SafeFormatHelper
delegator_override_with GitlabRoutingHelper # TODO: Remove `GitlabRoutingHelper` inclusion as it's duplicate
delegator_override_with Gitlab::Utils::StrongMemoize # This module inclusion is expected. See https://gitlab.com/gitlab-org/gitlab/-/issues/352884.
@@ -163,14 +164,10 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
def storage_anchor_data
can_show_quota = can?(current_user, :admin_project, project) && !empty_repo?
+
AnchorData.new(
true,
- statistic_icon('disk') +
- _('%{strong_start}%{human_size}%{strong_end} Project Storage').html_safe % {
- human_size: storage_counter(statistics.storage_size),
- strong_start: '<strong class="project-stat-value">'.html_safe,
- strong_end: '</strong>'.html_safe
- },
+ statistic_icon('disk') + storage_anchor_text,
can_show_quota ? project_usage_quotas_path(project) : nil
)
end
@@ -439,6 +436,10 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
count_of_extra_topics_not_shown > 0
end
+ def has_review_app?
+ !project.environments_for_scope('review/*').empty?
+ end
+
def can_setup_review_app?
strong_memoize(:can_setup_review_app) do
(can_instantiate_cluster? && all_clusters_empty?) || cicd_missing?
@@ -528,6 +529,16 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
def project_create_wiki_path
"#{wiki_path(project.wiki)}?view=create"
end
+
+ def storage_anchor_text
+ safe_format(
+ _('%{strong_start}%{human_size}%{strong_end} Project Storage'), {
+ human_size: storage_counter(statistics.storage_size),
+ strong_start: '<strong class="project-stat-value">'.html_safe,
+ strong_end: '</strong>'.html_safe
+ }
+ )
+ end
end
ProjectPresenter.prepend_mod_with('ProjectPresenter')
diff --git a/app/presenters/projects/security/configuration_presenter.rb b/app/presenters/projects/security/configuration_presenter.rb
index f248652befc..a0d731f0ccf 100644
--- a/app/presenters/projects/security/configuration_presenter.rb
+++ b/app/presenters/projects/security/configuration_presenter.rb
@@ -55,8 +55,8 @@ module Projects
def gitlab_ci_history_path
return '' if project.empty_repo?
- gitlab_ci = ::Gitlab::FileDetector::PATTERNS[:gitlab_ci]
- ::Gitlab::Routing.url_helpers.project_blame_path(project, File.join(project.default_branch_or_main, gitlab_ci))
+ ::Gitlab::Routing.url_helpers.project_blame_path(
+ project, File.join(project.default_branch_or_main, project.ci_config_path_or_default))
end
def features
diff --git a/app/presenters/user_presenter.rb b/app/presenters/user_presenter.rb
index 43164cca9c9..da087ce6858 100644
--- a/app/presenters/user_presenter.rb
+++ b/app/presenters/user_presenter.rb
@@ -21,7 +21,6 @@ class UserPresenter < Gitlab::View::Presenter::Delegated
delegator_override :saved_replies
def saved_replies
- return ::Users::SavedReply.none unless Feature.enabled?(:saved_replies, current_user)
return ::Users::SavedReply.none unless current_user.can?(:read_saved_replies, user)
user.saved_replies
diff --git a/app/serializers/build_details_entity.rb b/app/serializers/build_details_entity.rb
index 9aee031328b..35063ceeb06 100644
--- a/app/serializers/build_details_entity.rb
+++ b/app/serializers/build_details_entity.rb
@@ -14,7 +14,7 @@ class BuildDetailsEntity < Ci::JobEntity
expose :deployment_status, if: -> (*) { build.deployment_job? } do
expose :deployment_status, as: :status
expose :persisted_environment, as: :environment do |build, options|
- options.merge(deployment_details: false).yield_self do |opts|
+ options.merge(deployment_details: false).then do |opts|
EnvironmentEntity.represent(build.persisted_environment, opts)
end
end
diff --git a/app/serializers/ci/job_entity.rb b/app/serializers/ci/job_entity.rb
index 813938c2a18..828a9eb33a5 100644
--- a/app/serializers/ci/job_entity.rb
+++ b/app/serializers/ci/job_entity.rb
@@ -53,7 +53,7 @@ module Ci
alias_method :job, :object
def cancelable?
- job.cancelable? && can?(request.current_user, :update_build, job)
+ job.cancelable? && can?(request.current_user, :cancel_build, job)
end
def retryable?
diff --git a/app/serializers/ci/pipeline_entity.rb b/app/serializers/ci/pipeline_entity.rb
index 832ca619edc..4ff56c67d13 100644
--- a/app/serializers/ci/pipeline_entity.rb
+++ b/app/serializers/ci/pipeline_entity.rb
@@ -106,7 +106,7 @@ class Ci::PipelineEntity < Grape::Entity
end
def can_cancel?
- can?(request.current_user, :update_pipeline, pipeline) &&
+ can?(request.current_user, :cancel_pipeline, pipeline) &&
pipeline.cancelable?
end
diff --git a/app/serializers/deployment_entity.rb b/app/serializers/deployment_entity.rb
index 7cd913d057e..851d7a95d40 100644
--- a/app/serializers/deployment_entity.rb
+++ b/app/serializers/deployment_entity.rb
@@ -28,7 +28,7 @@ class DeploymentEntity < Grape::Entity
expose :deployed_by, as: :user, using: UserEntity
expose :deployable, if: -> (deployment) { deployment.deployable.present? } do |deployment, opts|
- deployment.deployable.yield_self do |deployable|
+ deployment.deployable.then do |deployable|
if include_details?
Ci::JobEntity.represent(deployable, opts)
elsif can_read_deployables?
diff --git a/app/serializers/group_link/group_group_link_entity.rb b/app/serializers/group_link/group_group_link_entity.rb
index d5d7eea74ea..f855d89f593 100644
--- a/app/serializers/group_link/group_group_link_entity.rb
+++ b/app/serializers/group_link/group_group_link_entity.rb
@@ -4,7 +4,7 @@ module GroupLink
class GroupGroupLinkEntity < GroupLink::GroupLinkEntity
include RequestAwareEntity
- expose :source do |group_link|
+ expose :source, if: ->(group_link) { can_read_shared_group?(group_link) } do |group_link|
GroupEntity.represent(group_link.shared_from, only: [:id, :full_name, :web_url])
end
diff --git a/app/serializers/group_link/group_link_entity.rb b/app/serializers/group_link/group_link_entity.rb
index 4cc7e9f3c8c..66645e736a9 100644
--- a/app/serializers/group_link/group_link_entity.rb
+++ b/app/serializers/group_link/group_link_entity.rb
@@ -19,16 +19,28 @@ module GroupLink
group_link.class.access_options
end
+ expose :is_shared_with_group_private do |group_link|
+ !can_read_shared_group?(group_link)
+ end
+
expose :shared_with_group do
- expose :avatar_url do |group_link|
+ expose :avatar_url, if: ->(group_link) { can_read_shared_group?(group_link) } do |group_link|
group_link.shared_with_group.avatar_url(only_path: false, size: Member::AVATAR_SIZE)
end
- expose :web_url do |group_link|
+ expose :web_url, if: ->(group_link) { can_read_shared_group?(group_link) } do |group_link|
group_link.shared_with_group.web_url
end
- expose :shared_with_group, merge: true, using: GroupBasicEntity
+ # We have to expose shared_with_group.id because we use this to get distinct
+ # with ancestors
+ expose :shared_with_group, merge: true do |group_link|
+ if can_read_shared_group?(group_link)
+ GroupBasicEntity.represent(group_link.shared_with_group)
+ else
+ GroupBasicEntity.represent(group_link.shared_with_group, only: [:id])
+ end
+ end
end
expose :can_update do |group_link, options|
@@ -45,6 +57,10 @@ module GroupLink
private
+ def can_read_shared_group?(group_link)
+ can?(current_user, :read_shared_with_group, group_link)
+ end
+
def current_user
options[:current_user]
end
diff --git a/app/serializers/group_link/project_group_link_entity.rb b/app/serializers/group_link/project_group_link_entity.rb
index d246bff1c58..fbad69bf2c5 100644
--- a/app/serializers/group_link/project_group_link_entity.rb
+++ b/app/serializers/group_link/project_group_link_entity.rb
@@ -4,7 +4,7 @@ module GroupLink
class ProjectGroupLinkEntity < GroupLink::GroupLinkEntity
include RequestAwareEntity
- expose :source do |group_link|
+ expose :source, if: ->(group_link) { can_read_shared_group?(group_link) } do |group_link|
ProjectEntity.represent(group_link.shared_from, only: [:id, :full_name])
end
diff --git a/app/serializers/issue_entity.rb b/app/serializers/issue_entity.rb
index 657af578c7f..9a55e761bf0 100644
--- a/app/serializers/issue_entity.rb
+++ b/app/serializers/issue_entity.rb
@@ -73,11 +73,11 @@ class IssueEntity < IssuableEntity
end
expose :confidential_issues_docs_path, if: -> (issue) { issue.confidential? } do |issue|
- help_page_path('user/project/issues/confidential_issues.md')
+ help_page_path('user/project/issues/confidential_issues')
end
expose :locked_discussion_docs_path, if: -> (issue) { issue.discussion_locked? } do |issue|
- help_page_path('user/discussions/index.md', anchor: 'prevent-comments-by-locking-an-issue')
+ help_page_path('user/discussions/index', anchor: 'prevent-comments-by-locking-an-issue')
end
expose :is_project_archived do |issue|
@@ -85,7 +85,7 @@ class IssueEntity < IssuableEntity
end
expose :archived_project_docs_path, if: -> (issue) { issue.project.archived? } do |issue|
- help_page_path('user/project/settings/index.md', anchor: 'archive-a-project')
+ help_page_path('user/project/settings/index', anchor: 'archive-a-project')
end
expose :issue_email_participants do |issue|
diff --git a/app/serializers/member_entity.rb b/app/serializers/member_entity.rb
index 8e5d352e413..a710df9ce5b 100644
--- a/app/serializers/member_entity.rb
+++ b/app/serializers/member_entity.rb
@@ -32,8 +32,11 @@ class MemberEntity < Grape::Entity
expose :access_level do
expose :human_access, as: :string_value
expose :access_level, as: :integer_value
+ expose :member_role_id
end
+ expose :custom_permissions
+
expose :source do |member|
GroupEntity.represent(member.source, only: [:id, :full_name, :web_url])
end
@@ -42,6 +45,8 @@ class MemberEntity < Grape::Entity
expose :valid_level_roles, as: :valid_roles
+ expose :valid_member_roles, as: :custom_roles
+
expose :user, if: -> (member) { member.user.present? } do |member, options|
MemberUserEntity.represent(member.user, options)
end
diff --git a/app/serializers/merge_request_noteable_entity.rb b/app/serializers/merge_request_noteable_entity.rb
index aac90c20b53..04b801e29ad 100644
--- a/app/serializers/merge_request_noteable_entity.rb
+++ b/app/serializers/merge_request_noteable_entity.rb
@@ -49,14 +49,10 @@ class MergeRequestNoteableEntity < IssuableEntity
expose :can_update do |merge_request|
can?(current_user, :update_merge_request, merge_request)
end
-
- expose :can_approve do |merge_request|
- merge_request.eligible_for_approval_by?(current_user)
- end
end
expose :locked_discussion_docs_path, if: -> (merge_request) { merge_request.discussion_locked? } do |merge_request|
- help_page_path('user/discussions/index.md', anchor: 'prevent-comments-by-locking-an-issue')
+ help_page_path('user/discussions/index', anchor: 'prevent-comments-by-locking-an-issue')
end
expose :is_project_archived do |merge_request|
@@ -66,7 +62,7 @@ class MergeRequestNoteableEntity < IssuableEntity
expose :project_id
expose :archived_project_docs_path, if: -> (merge_request) { merge_request.project.archived? } do |merge_request|
- help_page_path('user/project/settings/index.md', anchor: 'archive-a-project')
+ help_page_path('user/project/settings/index', anchor: 'archive-a-project')
end
private
diff --git a/app/serializers/merge_request_widget_entity.rb b/app/serializers/merge_request_widget_entity.rb
index cf984207ad1..95072ae815e 100644
--- a/app/serializers/merge_request_widget_entity.rb
+++ b/app/serializers/merge_request_widget_entity.rb
@@ -48,15 +48,15 @@ class MergeRequestWidgetEntity < Grape::Entity
end
expose :conflicts_docs_path do |merge_request|
- help_page_path('user/project/merge_requests/conflicts.md')
+ help_page_path('user/project/merge_requests/conflicts')
end
expose :reviewing_and_managing_merge_requests_docs_path do |merge_request|
- help_page_path('user/project/merge_requests/reviews/index.md', anchor: "checkout-merge-requests-locally-through-the-head-ref")
+ help_page_path('user/project/merge_requests/reviews/index', anchor: "checkout-merge-requests-locally-through-the-head-ref")
end
expose :merge_request_pipelines_docs_path do |merge_request|
- help_page_path('ci/pipelines/merge_request_pipelines.md')
+ help_page_path('ci/pipelines/merge_request_pipelines')
end
expose :ci_environments_status_path do |merge_request|
@@ -129,7 +129,7 @@ class MergeRequestWidgetEntity < Grape::Entity
end
expose :security_reports_docs_path do |merge_request|
- help_page_path('user/application_security/index.md', anchor: 'view-security-scan-information-in-merge-requests')
+ help_page_path('user/application_security/index', anchor: 'view-security-scan-information-in-merge-requests')
end
expose :enabled_reports do |merge_request|
diff --git a/app/serializers/review_app_setup_entity.rb b/app/serializers/review_app_setup_entity.rb
index 3a21fe24d9e..1fde31bc847 100644
--- a/app/serializers/review_app_setup_entity.rb
+++ b/app/serializers/review_app_setup_entity.rb
@@ -13,6 +13,8 @@ class ReviewAppSetupEntity < Grape::Entity
YAML.safe_load(File.read(Rails.root.join('lib', 'gitlab', 'ci', 'snippets', 'review_app_default.yml'))).to_s
end
+ expose :has_review_app?, as: :has_review_app
+
private
def current_user
diff --git a/app/services/activity_pub/accept_follow_service.rb b/app/services/activity_pub/accept_follow_service.rb
new file mode 100644
index 00000000000..0ec440fa972
--- /dev/null
+++ b/app/services/activity_pub/accept_follow_service.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+module ActivityPub
+ class AcceptFollowService
+ MissingInboxURLError = Class.new(StandardError)
+
+ attr_reader :subscription, :actor
+
+ def initialize(subscription, actor)
+ @subscription = subscription
+ @actor = actor
+ end
+
+ def execute
+ return if subscription.accepted?
+ raise MissingInboxURLError unless subscription.subscriber_inbox_url.present?
+
+ upload_accept_activity
+ subscription.accepted!
+ end
+
+ private
+
+ def upload_accept_activity
+ body = Gitlab::Json::LimitedEncoder.encode(payload, limit: 1.megabyte)
+
+ begin
+ Gitlab::HTTP.post(subscription.subscriber_inbox_url, body: body, headers: headers)
+ rescue StandardError => e
+ raise ThirdPartyError, e.message
+ end
+ end
+
+ def payload
+ follow = subscription.payload.dup
+ follow.delete('@context')
+
+ {
+ '@context': 'https://www.w3.org/ns/activitystreams',
+ id: "#{actor}#follow/#{subscription.id}/accept",
+ type: 'Accept',
+ actor: actor,
+ object: follow
+ }
+ end
+
+ def headers
+ {
+ 'User-Agent' => "GitLab/#{Gitlab::VERSION}",
+ 'Content-Type' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
+ 'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
+ }
+ end
+ end
+end
diff --git a/app/services/activity_pub/inbox_resolver_service.rb b/app/services/activity_pub/inbox_resolver_service.rb
new file mode 100644
index 00000000000..c2bd2112b16
--- /dev/null
+++ b/app/services/activity_pub/inbox_resolver_service.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module ActivityPub
+ class InboxResolverService
+ attr_reader :subscription
+
+ def initialize(subscription)
+ @subscription = subscription
+ end
+
+ def execute
+ profile = subscriber_profile
+ unless profile.has_key?('inbox') && profile['inbox'].is_a?(String)
+ raise ThirdPartyError, 'Inbox parameter absent or invalid'
+ end
+
+ subscription.subscriber_inbox_url = profile['inbox']
+ subscription.shared_inbox_url = profile.dig('entrypoints', 'sharedInbox')
+ subscription.save!
+ end
+
+ private
+
+ def subscriber_profile
+ raw_data = download_subscriber_profile
+
+ begin
+ profile = Gitlab::Json.parse(raw_data)
+ rescue JSON::ParserError => e
+ raise ThirdPartyError, e.message
+ end
+
+ profile
+ end
+
+ def download_subscriber_profile
+ begin
+ response = Gitlab::HTTP.get(subscription.subscriber_url,
+ headers: {
+ 'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
+ }
+ )
+ rescue StandardError => e
+ raise ThirdPartyError, e.message
+ end
+
+ response.body
+ end
+ end
+end
diff --git a/app/services/activity_pub/third_party_error.rb b/app/services/activity_pub/third_party_error.rb
new file mode 100644
index 00000000000..473a67984a4
--- /dev/null
+++ b/app/services/activity_pub/third_party_error.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+module ActivityPub
+ ThirdPartyError = Class.new(StandardError)
+end
diff --git a/app/services/admin/plan_limits/update_service.rb b/app/services/admin/plan_limits/update_service.rb
index 24ce3c4095f..7412f9852d1 100644
--- a/app/services/admin/plan_limits/update_service.rb
+++ b/app/services/admin/plan_limits/update_service.rb
@@ -51,35 +51,63 @@ module Admin
def validate_notification_limit
return unless parsed_params.include?(:notification_limit)
- return if notification_limit >= storage_size_limit && notification_limit <= enforcement_limit
+ return if unlimited_value?(:notification_limit)
- plan_limits.errors.add(:notification_limit, "must be greater than or equal to " \
- "storage_size_limit (Dashboard limit): #{storage_size_limit} " \
- "and less than or equal to enforcement_limit: #{enforcement_limit}")
+ if storage_size_limit > 0 && notification_limit < storage_size_limit
+ plan_limits.errors.add(
+ :notification_limit, "must be greater than or equal to the dashboard limit (#{storage_size_limit})"
+ )
+ end
+
+ return unless enforcement_limit > 0 && notification_limit > enforcement_limit
+
+ plan_limits.errors.add(
+ :notification_limit, "must be less than or equal to the enforcement limit (#{enforcement_limit})"
+ )
end
def validate_enforcement_limit
return unless parsed_params.include?(:enforcement_limit)
- return if enforcement_limit >= storage_size_limit && enforcement_limit >= notification_limit
+ return if unlimited_value?(:enforcement_limit)
+
+ if storage_size_limit > 0 && enforcement_limit < storage_size_limit
+ plan_limits.errors.add(
+ :enforcement_limit, "must be greater than or equal to the dashboard limit (#{storage_size_limit})"
+ )
+ end
+
+ return unless notification_limit > 0 && enforcement_limit < notification_limit
- plan_limits.errors.add(:enforcement_limit, "must be greater than or equal to " \
- "storage_size_limit (Dashboard limit): #{storage_size_limit} and " \
- "greater than or equal to notification_limit: #{notification_limit}")
+ plan_limits.errors.add(
+ :enforcement_limit, "must be greater than or equal to the notification limit (#{notification_limit})"
+ )
end
def validate_storage_size_limit
return unless parsed_params.include?(:storage_size_limit)
- return if storage_size_limit <= enforcement_limit && storage_size_limit <= notification_limit
+ return if unlimited_value?(:storage_size_limit)
- plan_limits.errors.add(:storage_size_limit, "(Dashboard limit) must be less than or equal to " \
- "enforcement_limit: #{enforcement_limit} " \
- "and notification_limit: #{notification_limit}")
+ if enforcement_limit > 0 && storage_size_limit > enforcement_limit
+ plan_limits.errors.add(
+ :dashboard_limit, "must be less than or equal to the enforcement limit (#{enforcement_limit})"
+ )
+ end
+
+ return unless notification_limit > 0 && storage_size_limit > notification_limit
+
+ plan_limits.errors.add(
+ :dashboard_limit, "must be less than or equal to the notification limit (#{notification_limit})"
+ )
end
# Overridden in EE
def parsed_params
params
end
+
+ def unlimited_value?(limit)
+ parsed_params[limit] == 0
+ end
end
end
end
diff --git a/app/services/auto_merge/base_service.rb b/app/services/auto_merge/base_service.rb
index d0fde43138a..467a4ed2621 100644
--- a/app/services/auto_merge/base_service.rb
+++ b/app/services/auto_merge/base_service.rb
@@ -61,15 +61,19 @@ module AutoMerge
merge_request.can_be_merged_by?(current_user) &&
merge_request.open? &&
!merge_request.broken? &&
- (skip_draft_check(merge_request) || !merge_request.draft?) &&
- (skip_discussions_check(merge_request) || merge_request.mergeable_discussions_state?) &&
- (skip_blocked_check(merge_request) || !merge_request.merge_blocked_by_other_mrs?) &&
+ overrideable_available_for_checks(merge_request) &&
yield
end
end
private
+ def overrideable_available_for_checks(merge_request)
+ !merge_request.draft? &&
+ merge_request.mergeable_discussions_state? &&
+ !merge_request.merge_blocked_by_other_mrs?
+ end
+
# Overridden in child classes
def notify(merge_request)
end
@@ -109,20 +113,5 @@ module AutoMerge
def track_exception(error, merge_request)
Gitlab::ErrorTracking.track_exception(error, merge_request_id: merge_request&.id)
end
-
- # Will skip the draft check or not when checking if strategy is available
- def skip_draft_check(merge_request)
- false
- end
-
- # Will skip the blocked check or not when checking if strategy is available
- def skip_blocked_check(merge_request)
- false
- end
-
- # Will skip the discussions check or not when checking if strategy is available
- def skip_discussions_check(merge_request)
- false
- end
end
end
diff --git a/app/services/boards/lists/move_service.rb b/app/services/boards/lists/move_service.rb
index 4bb7b4dbc6d..4715f1276e3 100644
--- a/app/services/boards/lists/move_service.rb
+++ b/app/services/boards/lists/move_service.rb
@@ -22,8 +22,11 @@ module Boards
attr_reader :board, :old_position, :new_position
def valid_move?
- new_position.present? && new_position != old_position &&
- new_position >= 0 && new_position <= board.lists.movable.last.position
+ new_position.present? && new_position != old_position && new_position.between?(0, max_position)
+ end
+
+ def max_position
+ board.lists.movable.maximum(:position)
end
def reorder_intermediate_lists
diff --git a/app/services/bulk_imports/batched_relation_export_service.rb b/app/services/bulk_imports/batched_relation_export_service.rb
index 778510f2e35..c7c01c80fbf 100644
--- a/app/services/bulk_imports/batched_relation_export_service.rb
+++ b/app/services/bulk_imports/batched_relation_export_service.rb
@@ -26,8 +26,6 @@ module BulkImports
start_export!
export.batches.destroy_all # rubocop: disable Cop/DestroyAll
enqueue_batch_exports
- rescue StandardError => e
- fail_export!(e)
ensure
FinishBatchedRelationExportWorker.perform_async(export.id)
end
@@ -81,11 +79,5 @@ module BulkImports
def find_or_create_batch(batch_number)
export.batches.find_or_create_by!(batch_number: batch_number) # rubocop:disable CodeReuse/ActiveRecord
end
-
- def fail_export!(exception)
- Gitlab::ErrorTracking.track_exception(exception, portable_id: portable.id, portable_type: portable.class.name)
-
- export.update!(status_event: 'fail_op', error: exception.message.truncate(255))
- end
end
end
diff --git a/app/services/bulk_imports/file_download_service.rb b/app/services/bulk_imports/file_download_service.rb
index 1f2437d783d..cc2d544198b 100644
--- a/app/services/bulk_imports/file_download_service.rb
+++ b/app/services/bulk_imports/file_download_service.rb
@@ -83,7 +83,7 @@ module BulkImports
end
def raise_error(message)
- logger.warn(message: message, response_headers: response_headers, importer: 'gitlab_migration')
+ logger.warn(message: message, response_headers: response_headers)
raise ServiceError, message
end
@@ -112,7 +112,7 @@ module BulkImports
end
def logger
- @logger ||= Gitlab::Import::Logger.build
+ @logger ||= Logger.build
end
def validate_url
diff --git a/app/services/bulk_imports/process_service.rb b/app/services/bulk_imports/process_service.rb
index 14c5545cfd5..7a6a883f1a9 100644
--- a/app/services/bulk_imports/process_service.rb
+++ b/app/services/bulk_imports/process_service.rb
@@ -20,10 +20,6 @@ module BulkImports
process_bulk_import
re_enqueue
- rescue StandardError => e
- Gitlab::ErrorTracking.track_exception(e, bulk_import_id: bulk_import.id)
-
- bulk_import.fail_op
end
private
@@ -114,16 +110,15 @@ module BulkImports
bulk_import_id: entity.bulk_import_id,
bulk_import_entity_type: entity.source_type,
source_full_path: entity.source_full_path,
- pipeline_name: pipeline[:pipeline],
+ pipeline_class: pipeline[:pipeline],
minimum_source_version: minimum_version,
maximum_source_version: maximum_version,
- source_version: entity.source_version.to_s,
- importer: 'gitlab_migration'
+ source_version: entity.source_version.to_s
)
end
def logger
- @logger ||= Gitlab::Import::Logger.build
+ @logger ||= Logger.build
end
end
end
diff --git a/app/services/bulk_imports/relation_batch_export_service.rb b/app/services/bulk_imports/relation_batch_export_service.rb
index c7164d7c304..3f98d49aa1b 100644
--- a/app/services/bulk_imports/relation_batch_export_service.rb
+++ b/app/services/bulk_imports/relation_batch_export_service.rb
@@ -4,9 +4,9 @@ module BulkImports
class RelationBatchExportService
include Gitlab::ImportExport::CommandLineUtil
- def initialize(user_id, batch_id)
- @user = User.find(user_id)
- @batch = BulkImports::ExportBatch.find(batch_id)
+ def initialize(user, batch)
+ @user = user
+ @batch = batch
@config = FileTransfer.config_for(portable)
end
@@ -19,8 +19,6 @@ module BulkImports
upload_compressed_file
finish_batch!
- rescue StandardError => e
- fail_batch!(e)
ensure
FileUtils.remove_entry(export_path)
end
@@ -72,12 +70,6 @@ module BulkImports
batch.update!(status_event: 'finish', objects_count: exported_objects_count, error: nil)
end
- def fail_batch!(exception)
- Gitlab::ErrorTracking.track_exception(exception, portable_id: portable.id, portable_type: portable.class.name)
-
- batch.update!(status_event: 'fail_op', error: exception.message.truncate(255))
- end
-
def exported_filepath
File.join(export_path, exported_filename)
end
diff --git a/app/services/bulk_imports/relation_export_service.rb b/app/services/bulk_imports/relation_export_service.rb
index 91640496440..6db5ef3e9ec 100644
--- a/app/services/bulk_imports/relation_export_service.rb
+++ b/app/services/bulk_imports/relation_export_service.rb
@@ -42,8 +42,6 @@ module BulkImports
yield export
finish_export!(export)
- rescue StandardError => e
- fail_export!(export, e)
end
def export_service
@@ -87,12 +85,6 @@ module BulkImports
export.update!(status_event: 'finish', batched: false, error: nil)
end
- def fail_export!(export, exception)
- Gitlab::ErrorTracking.track_exception(exception, portable_id: portable.id, portable_type: portable.class.name)
-
- export&.update(status_event: 'fail_op', error: exception.class, batched: false)
- end
-
def exported_filepath
File.join(export_path, export_service.exported_filename)
end
diff --git a/app/services/ci/build_cancel_service.rb b/app/services/ci/build_cancel_service.rb
index a23418ed738..834d4febd10 100644
--- a/app/services/ci/build_cancel_service.rb
+++ b/app/services/ci/build_cancel_service.rb
@@ -21,7 +21,7 @@ module Ci
attr_reader :build, :user
def allowed?
- user.can?(:update_build, build)
+ user.can?(:cancel_build, build)
end
def forbidden
diff --git a/app/services/ci/cancel_pipeline_service.rb b/app/services/ci/cancel_pipeline_service.rb
index b5c8c00273e..38053b13921 100644
--- a/app/services/ci/cancel_pipeline_service.rb
+++ b/app/services/ci/cancel_pipeline_service.rb
@@ -8,27 +8,23 @@ module Ci
##
# @cascade_to_children - if true cancels all related child pipelines for parent child pipelines
- # @auto_canceled_by_pipeline_id - store the pipeline_id of the pipeline that triggered cancellation
+ # @auto_canceled_by_pipeline - store the pipeline_id of the pipeline that triggered cancellation
# @execute_async - if true cancel the children asyncronously
def initialize(
pipeline:,
current_user:,
cascade_to_children: true,
- auto_canceled_by_pipeline_id: nil,
+ auto_canceled_by_pipeline: nil,
execute_async: true)
@pipeline = pipeline
@current_user = current_user
@cascade_to_children = cascade_to_children
- @auto_canceled_by_pipeline_id = auto_canceled_by_pipeline_id
+ @auto_canceled_by_pipeline = auto_canceled_by_pipeline
@execute_async = execute_async
end
def execute
- unless can?(current_user, :update_pipeline, pipeline)
- return ServiceResponse.error(
- message: 'Insufficient permissions to cancel the pipeline',
- reason: :insufficient_permissions)
- end
+ return permission_error_response unless can?(current_user, :cancel_pipeline, pipeline)
force_execute
end
@@ -45,7 +41,7 @@ module Ci
log_pipeline_being_canceled
- pipeline.update_column(:auto_canceled_by_id, @auto_canceled_by_pipeline_id) if @auto_canceled_by_pipeline_id
+ pipeline.update_column(:auto_canceled_by_id, @auto_canceled_by_pipeline.id) if @auto_canceled_by_pipeline
cancel_jobs(pipeline.cancelable_statuses)
return ServiceResponse.success unless cascade_to_children?
@@ -65,7 +61,7 @@ module Ci
Gitlab::AppJsonLogger.info(
event: 'pipeline_cancel_running',
pipeline_id: pipeline.id,
- auto_canceled_by_pipeline_id: @auto_canceled_by_pipeline_id,
+ auto_canceled_by_pipeline_id: @auto_canceled_by_pipeline&.id,
cascade_to_children: cascade_to_children?,
execute_async: execute_async?,
**Gitlab::ApplicationContext.current
@@ -89,21 +85,34 @@ module Ci
relation = CommitStatus.id_in(batch)
Preloaders::CommitStatusPreloader.new(relation).execute(preloaded_relations)
- relation.each do |job|
- job.auto_canceled_by_id = @auto_canceled_by_pipeline_id if @auto_canceled_by_pipeline_id
- job.cancel
- end
+ relation.each { |job| cancel_job(job) }
end
end
end
+ def cancel_job(job)
+ if @auto_canceled_by_pipeline
+ job.auto_canceled_by_id = @auto_canceled_by_pipeline.id
+ job.auto_canceled_by_partition_id = @auto_canceled_by_pipeline.partition_id
+ end
+
+ job.cancel
+ end
+
+ def permission_error_response
+ ServiceResponse.error(
+ message: 'Insufficient permissions to cancel the pipeline',
+ reason: :insufficient_permissions
+ )
+ end
+
# For parent child-pipelines only (not multi-project)
def cancel_children
pipeline.all_child_pipelines.each do |child_pipeline|
if execute_async?
::Ci::CancelPipelineWorker.perform_async(
child_pipeline.id,
- @auto_canceled_by_pipeline_id
+ @auto_canceled_by_pipeline&.id
)
else
# cascade_to_children is false because we iterate through children
@@ -113,7 +122,7 @@ module Ci
current_user: nil,
cascade_to_children: false,
execute_async: execute_async?,
- auto_canceled_by_pipeline_id: @auto_canceled_by_pipeline_id
+ auto_canceled_by_pipeline: @auto_canceled_by_pipeline
).force_execute
end
end
diff --git a/app/services/ci/catalog/resources/create_service.rb b/app/services/ci/catalog/resources/create_service.rb
new file mode 100644
index 00000000000..89367c70e82
--- /dev/null
+++ b/app/services/ci/catalog/resources/create_service.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Ci
+ module Catalog
+ module Resources
+ class CreateService
+ include Gitlab::Allowable
+
+ attr_reader :project, :current_user
+
+ def initialize(project, user)
+ @current_user = user
+ @project = project
+ end
+
+ def execute
+ raise Gitlab::Access::AccessDeniedError unless can?(current_user, :add_catalog_resource, project)
+
+ catalog_resource = Ci::Catalog::Resource.new(project: project)
+
+ if catalog_resource.valid?
+ catalog_resource.save!
+ ServiceResponse.success(payload: catalog_resource)
+ else
+ ServiceResponse.error(message: catalog_resource.errors.full_messages.join(', '))
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/ci/catalog/resources/release_service.rb b/app/services/ci/catalog/resources/release_service.rb
new file mode 100644
index 00000000000..ad77bff3ef9
--- /dev/null
+++ b/app/services/ci/catalog/resources/release_service.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module Ci
+ module Catalog
+ module Resources
+ class ReleaseService
+ def initialize(release)
+ @release = release
+ @project = release.project
+ @errors = []
+ end
+
+ def execute
+ validate_catalog_resource
+ create_version
+
+ if errors.empty?
+ ServiceResponse.success
+ else
+ ServiceResponse.error(message: errors.join(', '))
+ end
+ end
+
+ private
+
+ attr_reader :project, :errors, :release
+
+ def validate_catalog_resource
+ response = Ci::Catalog::Resources::ValidateService.new(project, release.sha).execute
+ return if response.success?
+
+ errors << response.message
+ end
+
+ def create_version
+ return if errors.present?
+
+ response = Ci::Catalog::Resources::Versions::CreateService.new(release).execute
+ return if response.success?
+
+ errors << response.message
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/ci/catalog/resources/validate_service.rb b/app/services/ci/catalog/resources/validate_service.rb
index 9e8986ba6fc..0e842fb7405 100644
--- a/app/services/ci/catalog/resources/validate_service.rb
+++ b/app/services/ci/catalog/resources/validate_service.rb
@@ -4,7 +4,7 @@ module Ci
module Catalog
module Resources
class ValidateService
- attr_reader :project
+ MINIMUM_AMOUNT_OF_COMPONENTS = 1
def initialize(project, ref)
@project = project
@@ -13,30 +13,38 @@ module Ci
end
def execute
- check_project_readme
- check_project_description
+ verify_presence_project_readme
+ verify_presence_project_description
+ scan_directory_for_components
if errors.empty?
ServiceResponse.success
else
- ServiceResponse.error(message: errors.join(' , '))
+ ServiceResponse.error(message: errors.join(', '))
end
end
private
- attr_reader :ref, :errors
+ attr_reader :project, :ref, :errors
- def check_project_description
+ def verify_presence_project_readme
+ return if project_has_readme?
+
+ errors << 'Project must have a README'
+ end
+
+ def verify_presence_project_description
return if project.description.present?
errors << 'Project must have a description'
end
- def check_project_readme
- return if project_has_readme?
+ def scan_directory_for_components
+ return if Ci::Catalog::ComponentsProject.new(project).fetch_component_paths(ref,
+ limit: MINIMUM_AMOUNT_OF_COMPONENTS).any?
- errors << 'Project must have a README'
+ errors << 'Project must contain components. Ensure you are using the correct directory structure'
end
def project_has_readme?
diff --git a/app/services/ci/catalog/resources/versions/create_service.rb b/app/services/ci/catalog/resources/versions/create_service.rb
new file mode 100644
index 00000000000..863bad43271
--- /dev/null
+++ b/app/services/ci/catalog/resources/versions/create_service.rb
@@ -0,0 +1,111 @@
+# frozen_string_literal: true
+
+module Ci
+ module Catalog
+ module Resources
+ module Versions
+ class CreateService
+ def initialize(release)
+ @project = release.project
+ @release = release
+ @errors = []
+ @version = nil
+ @components_project = Ci::Catalog::ComponentsProject.new(project)
+ end
+
+ def execute
+ build_catalog_resource_version
+ fetch_and_build_components if Feature.enabled?(:ci_catalog_create_metadata, project)
+ publish_catalog_resource!
+
+ if errors.empty?
+ ServiceResponse.success
+ else
+ ServiceResponse.error(message: errors.flatten.first(10).join(', '))
+ end
+ end
+
+ private
+
+ attr_reader :project, :errors, :release, :components_project
+
+ def build_catalog_resource_version
+ return error('Project is not a catalog resource') unless project.catalog_resource
+
+ @version = Ci::Catalog::Resources::Version.new(
+ release: release,
+ catalog_resource: project.catalog_resource,
+ project: project
+ )
+ end
+
+ def fetch_and_build_components
+ return if errors.present?
+
+ max_components = Ci::Catalog::ComponentsProject::COMPONENTS_LIMIT
+ component_paths = components_project.fetch_component_paths(release.sha, limit: max_components + 1)
+
+ if component_paths.size > max_components
+ return error("Release cannot contain more than #{max_components} components")
+ end
+
+ build_components(component_paths)
+ end
+
+ def build_components(component_paths)
+ paths_with_oids = component_paths.map { |path| [release.sha, path] }
+ blobs = project.repository.blobs_at(paths_with_oids)
+
+ blobs.each do |blob|
+ metadata = extract_metadata(blob)
+ build_catalog_resource_component(metadata)
+ end
+ rescue ::Gitlab::Config::Loader::FormatError => e
+ error(e)
+ end
+
+ def extract_metadata(blob)
+ {
+ name: components_project.extract_component_name(blob.path),
+ inputs: components_project.extract_inputs(blob.data),
+ path: blob.path
+ }
+ end
+
+ def build_catalog_resource_component(metadata)
+ return if errors.present?
+
+ component = @version.components.build(
+ name: metadata[:name],
+ project: @version.project,
+ inputs: metadata[:inputs],
+ catalog_resource: @version.catalog_resource,
+ path: metadata[:path],
+ created_at: Time.current
+ )
+
+ return if component.valid?
+
+ error("Build component error: #{component.errors.full_messages.join(', ')}")
+ end
+
+ def publish_catalog_resource!
+ return if errors.present?
+
+ ::Ci::Catalog::Resources::Version.transaction do
+ BulkInsertableAssociations.with_bulk_insert do
+ @version.save!
+ end
+
+ project.catalog_resource.publish!
+ end
+ end
+
+ def error(message)
+ errors << message
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/ci/destroy_pipeline_service.rb b/app/services/ci/destroy_pipeline_service.rb
index a9d2e17657e..7adf573687a 100644
--- a/app/services/ci/destroy_pipeline_service.rb
+++ b/app/services/ci/destroy_pipeline_service.rb
@@ -28,3 +28,5 @@ module Ci
end
end
end
+
+Ci::DestroyPipelineService.prepend_mod
diff --git a/app/services/ci/enqueue_job_service.rb b/app/services/ci/enqueue_job_service.rb
index 9e3bea3fd28..db616473336 100644
--- a/app/services/ci/enqueue_job_service.rb
+++ b/app/services/ci/enqueue_job_service.rb
@@ -11,11 +11,14 @@ module Ci
end
def execute(&transition)
- job.user = current_user
- job.job_variables_attributes = variables if variables
-
transition ||= ->(job) { job.enqueue! }
- Gitlab::OptimisticLocking.retry_lock(job, name: 'ci_enqueue_job', &transition)
+
+ Gitlab::OptimisticLocking.retry_lock(job, name: 'ci_enqueue_job') do |job|
+ job.user = current_user
+ job.job_variables_attributes = variables if variables
+
+ transition.call(job)
+ end
ResetSkippedJobsService.new(job.project, current_user).execute(job)
diff --git a/app/services/ci/pipeline_creation/cancel_redundant_pipelines_service.rb b/app/services/ci/pipeline_creation/cancel_redundant_pipelines_service.rb
index c18984953a1..224b2d96205 100644
--- a/app/services/ci/pipeline_creation/cancel_redundant_pipelines_service.rb
+++ b/app/services/ci/pipeline_creation/cancel_redundant_pipelines_service.rb
@@ -88,7 +88,7 @@ module Ci
::Ci::CancelPipelineService.new(
pipeline: cancelable_pipeline,
current_user: nil,
- auto_canceled_by_pipeline_id: pipeline.id,
+ auto_canceled_by_pipeline: pipeline,
cascade_to_children: false
).force_execute
end
diff --git a/app/services/ci/pipelines/update_metadata_service.rb b/app/services/ci/pipelines/update_metadata_service.rb
new file mode 100644
index 00000000000..2f2d648c13d
--- /dev/null
+++ b/app/services/ci/pipelines/update_metadata_service.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Ci
+ module Pipelines
+ class UpdateMetadataService
+ def initialize(pipeline, params)
+ @pipeline = pipeline
+ @params = params
+ end
+
+ def execute
+ metadata = pipeline.pipeline_metadata
+
+ metadata = pipeline.build_pipeline_metadata(project: pipeline.project) if metadata.nil?
+
+ params[:name] = params[:name].strip if params.key?(:name)
+
+ if metadata.update(params)
+ ServiceResponse.success(message: 'Pipeline metadata was updated', payload: pipeline)
+ else
+ ServiceResponse.error(message: 'Failed to update pipeline', payload: metadata.errors.full_messages,
+ reason: :bad_request)
+ end
+ end
+
+ private
+
+ attr_reader :pipeline, :params
+ end
+ end
+end
diff --git a/app/services/ci/refs/enqueue_pipelines_to_unlock_service.rb b/app/services/ci/refs/enqueue_pipelines_to_unlock_service.rb
index 319186ce030..4e9e9a2effe 100644
--- a/app/services/ci/refs/enqueue_pipelines_to_unlock_service.rb
+++ b/app/services/ci/refs/enqueue_pipelines_to_unlock_service.rb
@@ -7,13 +7,12 @@ module Ci
BATCH_SIZE = 50
ENQUEUE_INTERVAL_SECONDS = 0.1
+ EXCLUDED_IDS_LIMIT = 1000
def execute(ci_ref, before_pipeline: nil)
- pipelines_scope = ci_ref.pipelines.artifacts_locked
- pipelines_scope = pipelines_scope.before_pipeline(before_pipeline) if before_pipeline
total_new_entries = 0
- pipelines_scope.each_batch(of: BATCH_SIZE) do |batch|
+ pipelines_scope(ci_ref, before_pipeline).each_batch(of: BATCH_SIZE) do |batch|
pipeline_ids = batch.pluck(:id) # rubocop: disable CodeReuse/ActiveRecord
total_added = Ci::UnlockPipelineRequest.enqueue(pipeline_ids)
total_new_entries += total_added
@@ -27,6 +26,34 @@ module Ci
total_new_entries: total_new_entries
)
end
+
+ private
+
+ def pipelines_scope(ci_ref, before_pipeline)
+ scope = ci_ref.pipelines.artifacts_locked
+
+ if before_pipeline
+ # We use `same_family_pipeline_ids.map(&:id)` to force run the query and
+ # specifically pass the array of IDs to the NOT IN condition. If not, we would
+ # end up running the subquery for same_family_pipeline_ids on each batch instead.
+ excluded_ids = before_pipeline.same_family_pipeline_ids.map(&:id)
+ scope = scope.created_before_id(before_pipeline.id)
+
+ # When unlocking previous pipelines, we still want to keep the
+ # last successful CI source pipeline locked.
+ # If before_pipeline is not provided, like in the case of deleting a ref,
+ # we want to unlock all pipelines instead.
+ ci_ref.last_successful_ci_source_pipeline.try do |pipeline|
+ excluded_ids.concat(pipeline.same_family_pipeline_ids.map(&:id))
+ end
+
+ # We add a limit to the excluded IDs just to be safe and avoid any
+ # arity issues with the NOT IN query.
+ scope = scope.where.not(id: excluded_ids.take(EXCLUDED_IDS_LIMIT)) # rubocop: disable CodeReuse/ActiveRecord
+ end
+
+ scope
+ end
end
end
end
diff --git a/app/services/ci/retry_job_service.rb b/app/services/ci/retry_job_service.rb
index d7c3e9e7f64..a8ea5ac6df0 100644
--- a/app/services/ci/retry_job_service.rb
+++ b/app/services/ci/retry_job_service.rb
@@ -39,10 +39,6 @@ module Ci
::Ci::CopyCrossDatabaseAssociationsService.new.execute(job, new_job)
- if Feature.disabled?(:create_deployment_only_for_processable_jobs, project)
- ::Deployments::CreateForJobService.new.execute(new_job)
- end
-
::MergeRequests::AddTodoWhenBuildFailsService
.new(project: project)
.close(new_job)
diff --git a/app/services/concerns/update_repository_storage_methods.rb b/app/services/concerns/update_repository_storage_methods.rb
index 50963cc58b2..aff36d6943e 100644
--- a/app/services/concerns/update_repository_storage_methods.rb
+++ b/app/services/concerns/update_repository_storage_methods.rb
@@ -32,9 +32,9 @@ module UpdateRepositoryStorageMethods
end
end
- repository_storage_move.transaction do
- repository_storage_move.finish_replication!
+ repository_storage_move.finish_replication!
+ repository_storage_move.transaction do
track_repository(destination_storage_name)
end
diff --git a/app/services/container_registry/protection/create_rule_service.rb b/app/services/container_registry/protection/create_rule_service.rb
new file mode 100644
index 00000000000..34ec6f42b19
--- /dev/null
+++ b/app/services/container_registry/protection/create_rule_service.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module ContainerRegistry
+ module Protection
+ class CreateRuleService < BaseService
+ ALLOWED_ATTRIBUTES = %i[
+ container_path_pattern
+ push_protected_up_to_access_level
+ delete_protected_up_to_access_level
+ ].freeze
+
+ def execute
+ unless can?(current_user, :admin_container_image, project)
+ error_message = _('Unauthorized to create a container registry protection rule')
+ return service_response_error(message: error_message)
+ end
+
+ container_registry_protection_rule =
+ project.container_registry_protection_rules.create(params.slice(*ALLOWED_ATTRIBUTES))
+
+ unless container_registry_protection_rule.persisted?
+ return service_response_error(message: container_registry_protection_rule.errors.full_messages.to_sentence)
+ end
+
+ ServiceResponse.success(payload: { container_registry_protection_rule: container_registry_protection_rule })
+ rescue StandardError => e
+ service_response_error(message: e.message)
+ end
+
+ private
+
+ def service_response_error(message:)
+ ServiceResponse.error(
+ message: message,
+ payload: { container_registry_protection_rule: nil }
+ )
+ end
+ end
+ end
+end
diff --git a/app/services/draft_notes/publish_service.rb b/app/services/draft_notes/publish_service.rb
index a7a2ad63c1c..5ba7f829c8e 100644
--- a/app/services/draft_notes/publish_service.rb
+++ b/app/services/draft_notes/publish_service.rb
@@ -81,7 +81,9 @@ module DraftNotes
end
def set_reviewed
- ::MergeRequests::MarkReviewerReviewedService.new(project: project, current_user: current_user).execute(merge_request)
+ return if Feature.enabled?(:mr_request_changes, current_user)
+
+ ::MergeRequests::UpdateReviewerStateService.new(project: project, current_user: current_user).execute(merge_request, "reviewed")
end
def capture_diff_note_positions(notes)
diff --git a/app/services/environments/auto_recover_service.rb b/app/services/environments/auto_recover_service.rb
new file mode 100644
index 00000000000..d52f90bbe50
--- /dev/null
+++ b/app/services/environments/auto_recover_service.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+module Environments
+ class AutoRecoverService
+ include ::Gitlab::ExclusiveLeaseHelpers
+ include ::Gitlab::LoopHelpers
+
+ BATCH_SIZE = 100
+ LOOP_TIMEOUT = 45.minutes
+ LOOP_LIMIT = 1000
+ EXCLUSIVE_LOCK_KEY = 'environments:auto_recover:lock'
+ LOCK_TIMEOUT = 50.minutes
+
+ ##
+ # Recover environments that are stuck stopping on a GitLab instance
+ #
+ # This auto stop process cannot run for more than 45 minutes. This is for
+ # preventing multiple `AutoStopCronWorker` CRON jobs run concurrently,
+ # which is scheduled at every hour.
+ def execute
+ in_lock(EXCLUSIVE_LOCK_KEY, ttl: LOCK_TIMEOUT, retries: 1) do
+ loop_until(timeout: LOOP_TIMEOUT, limit: LOOP_LIMIT) do
+ recover_in_batch
+ end
+ end
+ end
+
+ private
+
+ def recover_in_batch
+ environments = Environment.preload_project.select(:id, :project_id).long_stopping.limit(BATCH_SIZE)
+
+ return false if environments.empty?
+
+ Environments::AutoRecoverWorker.bulk_perform_async_with_contexts(
+ environments,
+ arguments_proc: ->(environment) { environment.id },
+ context_proc: ->(environment) { { project: environment.project } }
+ )
+
+ true
+ end
+ end
+end
diff --git a/app/services/git/branch_hooks_service.rb b/app/services/git/branch_hooks_service.rb
index a2eb4f1f396..c6214311692 100644
--- a/app/services/git/branch_hooks_service.rb
+++ b/app/services/git/branch_hooks_service.rb
@@ -110,7 +110,6 @@ module Git
end
def track_ci_config_change_event
- return unless ::ServicePing::ServicePingSettings.enabled?
return unless default_branch?
commits_changing_ci_config.each do |commit|
diff --git a/app/services/google_cloud/generate_pipeline_service.rb b/app/services/google_cloud/generate_pipeline_service.rb
index 30c358687aa..97d008db76b 100644
--- a/app/services/google_cloud/generate_pipeline_service.rb
+++ b/app/services/google_cloud/generate_pipeline_service.rb
@@ -67,7 +67,7 @@ module GoogleCloud
end
def default_branch_gitlab_ci_yml
- @default_branch_gitlab_ci_yml ||= project.repository.gitlab_ci_yml_for(project.default_branch)
+ @default_branch_gitlab_ci_yml ||= project.ci_config_for(project.default_branch)
end
def pipeline_content(include_path)
diff --git a/app/services/groups/ssh_certificates/create_service.rb b/app/services/groups/ssh_certificates/create_service.rb
index 6890901c306..e4570078395 100644
--- a/app/services/groups/ssh_certificates/create_service.rb
+++ b/app/services/groups/ssh_certificates/create_service.rb
@@ -3,9 +3,10 @@
module Groups
module SshCertificates
class CreateService
- def initialize(group, params)
+ def initialize(group, params, current_user)
@group = group
@params = params
+ @current_user = current_user
end
def execute
@@ -41,7 +42,7 @@ module Groups
private
- attr_reader :group, :params
+ attr_reader :group, :params, :current_user
def generate_fingerprint(key)
Gitlab::SSHPublicKey.new(key).fingerprint_sha256&.delete_prefix('SHA256:')
@@ -49,3 +50,5 @@ module Groups
end
end
end
+
+Groups::SshCertificates::CreateService.prepend_mod_with('Groups::SshCertificates::CreateService')
diff --git a/app/services/groups/ssh_certificates/destroy_service.rb b/app/services/groups/ssh_certificates/destroy_service.rb
index 7a450d5bee6..5f7bba12878 100644
--- a/app/services/groups/ssh_certificates/destroy_service.rb
+++ b/app/services/groups/ssh_certificates/destroy_service.rb
@@ -3,16 +3,17 @@
module Groups
module SshCertificates
class DestroyService
- def initialize(group, params)
+ def initialize(group, params, current_user)
@group = group
@params = params
+ @current_user = current_user
end
def execute
ssh_certificate = group.ssh_certificates.find(params[:ssh_certificates_id])
ssh_certificate.destroy!
- ServiceResponse.success
+ ServiceResponse.success(payload: { ssh_certificate: ssh_certificate })
rescue ActiveRecord::RecordNotFound
ServiceResponse.error(
@@ -29,7 +30,9 @@ module Groups
private
- attr_reader :group, :params
+ attr_reader :group, :params, :current_user
end
end
end
+
+Groups::SshCertificates::DestroyService.prepend_mod_with('Groups::SshCertificates::DestroyService')
diff --git a/app/services/import/validate_remote_git_endpoint_service.rb b/app/services/import/validate_remote_git_endpoint_service.rb
index a994072c4aa..8297757997f 100644
--- a/app/services/import/validate_remote_git_endpoint_service.rb
+++ b/app/services/import/validate_remote_git_endpoint_service.rb
@@ -13,6 +13,8 @@ module Import
GIT_PROTOCOL_PKT_LEN = 4
GIT_MINIMUM_RESPONSE_LENGTH = GIT_PROTOCOL_PKT_LEN + GIT_EXPECTED_FIRST_PACKET_LINE.length
EXPECTED_CONTENT_TYPE = "application/x-#{GIT_SERVICE_NAME}-advertisement"
+ INVALID_BODY_MESSAGE = 'Not a git repository: Invalid response body'
+ INVALID_CONTENT_TYPE_MESSAGE = 'Not a git repository: Invalid content-type'
def initialize(params)
@params = params
@@ -30,32 +32,35 @@ module Import
uri.fragment = nil
url = Gitlab::Utils.append_path(uri.to_s, "/info/refs?service=#{GIT_SERVICE_NAME}")
- response_body = ''
- result = nil
- Gitlab::HTTP.try_get(url, stream_body: true, follow_redirects: false, basic_auth: auth) do |fragment|
- response_body += fragment
- next if response_body.length < GIT_MINIMUM_RESPONSE_LENGTH
-
- result = if status_code_is_valid(fragment) && content_type_is_valid(fragment) && response_body_is_valid(response_body)
- :success
- else
- :error
- end
-
- # We are interested only in the first chunks of the response
- # So we're using stream_body: true and breaking when receive enough body
- break
- end
+ response, response_body = http_get_and_extract_first_chunks(url)
- if result == :success
- ServiceResponse.success
- else
- ServiceResponse.error(message: "#{uri} is not a valid HTTP Git repository")
- end
+ validate(uri, response, response_body)
+ rescue *Gitlab::HTTP::HTTP_ERRORS => err
+ error_result("HTTP #{err.class.name.underscore} error: #{err.message}")
+ rescue StandardError => err
+ ServiceResponse.error(
+ message: "Internal #{err.class.name.underscore} error: #{err.message}",
+ reason: 500
+ )
end
private
+ def http_get_and_extract_first_chunks(url)
+ # We are interested only in the first chunks of the response
+ # So we're using stream_body: true and breaking when receive enough body
+ response = nil
+ response_body = ''
+
+ Gitlab::HTTP.get(url, stream_body: true, follow_redirects: false, basic_auth: auth) do |response_chunk|
+ response = response_chunk
+ response_body += response_chunk
+ break if GIT_MINIMUM_RESPONSE_LENGTH <= response_body.length
+ end
+
+ [response, response_body]
+ end
+
def auth
unless @params[:user].to_s.blank?
{
@@ -65,15 +70,37 @@ module Import
end
end
- def status_code_is_valid(fragment)
- fragment.http_response.code == '200'
+ def validate(uri, response, response_body)
+ return status_code_error(uri, response) unless status_code_is_valid?(response)
+ return error_result(INVALID_CONTENT_TYPE_MESSAGE) unless content_type_is_valid?(response)
+ return error_result(INVALID_BODY_MESSAGE) unless response_body_is_valid?(response_body)
+
+ ServiceResponse.success
+ end
+
+ def status_code_error(uri, response)
+ http_code = response.http_response.code.to_i
+ message = response.http_response.message || Rack::Utils::HTTP_STATUS_CODES[http_code]
+
+ error_result(
+ "#{uri} endpoint error: #{http_code}#{message.presence&.prepend(' ')}",
+ http_code
+ )
+ end
+
+ def error_result(message, reason = nil)
+ ServiceResponse.error(message: message, reason: reason)
+ end
+
+ def status_code_is_valid?(response)
+ response.http_response.code == '200'
end
- def content_type_is_valid(fragment)
- fragment.http_response['content-type'] == EXPECTED_CONTENT_TYPE
+ def content_type_is_valid?(response)
+ response.http_response['content-type'] == EXPECTED_CONTENT_TYPE
end
- def response_body_is_valid(response_body)
+ def response_body_is_valid?(response_body)
response_body.match?(GIT_BODY_MESSAGE_REGEXP)
end
end
diff --git a/app/services/jira_connect_subscriptions/create_service.rb b/app/services/jira_connect_subscriptions/create_service.rb
index d5ab3800dcf..f537da5c091 100644
--- a/app/services/jira_connect_subscriptions/create_service.rb
+++ b/app/services/jira_connect_subscriptions/create_service.rb
@@ -11,7 +11,7 @@ module JiraConnectSubscriptions
return error(s_('JiraConnect|Could not fetch user information from Jira. ' \
'Check the permissions in Jira and try again.'), 403)
elsif !can_administer_jira?
- return error(s_('JiraConnect|The Jira user is not a site administrator. ' \
+ return error(s_('JiraConnect|The Jira user is not a site or organization administrator. ' \
'Check the permissions in Jira and try again.'), 403)
end
@@ -25,7 +25,7 @@ module JiraConnectSubscriptions
private
def can_administer_jira?
- params[:jira_user]&.site_admin?
+ params[:jira_user]&.jira_admin?
end
def create_subscription
diff --git a/app/services/members/create_service.rb b/app/services/members/create_service.rb
index 9cedc7ee3a5..b453098e27a 100644
--- a/app/services/members/create_service.rb
+++ b/app/services/members/create_service.rb
@@ -21,15 +21,16 @@ module Members
def execute
raise Gitlab::Access::AccessDeniedError unless can?(current_user, create_member_permission(source), source)
- # rubocop:disable Layout/EmptyLineAfterGuardClause
- raise Gitlab::Access::AccessDeniedError if adding_at_least_one_owner &&
- cannot_assign_owner_responsibilities_to_member_in_project?
- # rubocop:enable Layout/EmptyLineAfterGuardClause
+ if adding_at_least_one_owner && cannot_assign_owner_responsibilities_to_member_in_project?
+ raise Gitlab::Access::AccessDeniedError
+ end
validate_invite_source!
validate_invitable!
add_members
+ after_add_hooks
+
enqueue_onboarding_progress_action
publish_event!
@@ -73,8 +74,8 @@ module Members
return unless user_limit && invites.size > user_limit
- raise TooManyInvitesError,
- format(s_("AddMember|Too many users specified (limit is %{user_limit})"), user_limit: user_limit)
+ message = format(s_("AddMember|Too many users specified (limit is %{user_limit})"), user_limit: user_limit)
+ raise TooManyInvitesError, message
end
def blank_invites_message
@@ -82,16 +83,24 @@ module Members
end
def add_members
- @members = source.add_members(
- invites,
- params[:access_level],
- expires_at: params[:expires_at],
- current_user: current_user
+ @members = creator_service.add_members(
+ source, invites, params[:access_level], **create_params
)
members.each { |member| process_result(member) }
end
+ def creator_service
+ "Members::#{source.class.to_s.pluralize}::CreatorService".constantize
+ end
+
+ def create_params
+ {
+ expires_at: params[:expires_at],
+ current_user: current_user
+ }
+ end
+
def process_result(member)
existing_errors = member.errors.full_messages
@@ -116,6 +125,10 @@ module Members
existing_errors.concat(member.errors.full_messages).uniq
end
+ def after_add_hooks
+ # overridden in subclasses/ee
+ end
+
def after_execute(member:)
super
@@ -123,11 +136,13 @@ module Members
end
def track_invite_source(member)
- Gitlab::Tracking.event(self.class.name,
- 'create_member',
- label: invite_source,
- property: tracking_property(member),
- user: current_user)
+ Gitlab::Tracking.event(
+ self.class.name,
+ 'create_member',
+ label: invite_source,
+ property: tracking_property(member),
+ user: current_user
+ )
end
def invite_source
@@ -148,11 +163,15 @@ module Members
end
def enqueue_onboarding_progress_action
- return unless member_created_namespace_id
+ return unless at_least_one_member_created?
Onboarding::UserAddedWorker.perform_async(member_created_namespace_id)
end
+ def at_least_one_member_created?
+ member_created_namespace_id.present?
+ end
+
def result
if errors.any?
error(formatted_errors)
@@ -166,7 +185,7 @@ module Members
end
def publish_event!
- return unless member_created_namespace_id
+ return unless at_least_one_member_created?
Gitlab::EventStore.publish(
Members::MembersAddedEvent.new(data: {
diff --git a/app/services/merge_requests/mark_reviewer_reviewed_service.rb b/app/services/merge_requests/mark_reviewer_reviewed_service.rb
deleted file mode 100644
index 96747eabcf6..00000000000
--- a/app/services/merge_requests/mark_reviewer_reviewed_service.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# frozen_string_literal: true
-
-module MergeRequests
- class MarkReviewerReviewedService < MergeRequests::BaseService
- def execute(merge_request)
- return error("Invalid permissions") unless can?(current_user, :update_merge_request, merge_request)
-
- reviewer = merge_request.find_reviewer(current_user)
-
- if reviewer
- return error("Failed to update reviewer") unless reviewer.update(state: :reviewed)
-
- trigger_merge_request_reviewers_updated(merge_request)
-
- success
- else
- error("Reviewer not found")
- end
- end
- end
-end
diff --git a/app/services/merge_requests/mergeability/check_base_service.rb b/app/services/merge_requests/mergeability/check_base_service.rb
index e1c4d751296..b8a275b6c32 100644
--- a/app/services/merge_requests/mergeability/check_base_service.rb
+++ b/app/services/merge_requests/mergeability/check_base_service.rb
@@ -42,6 +42,11 @@ module MergeRequests
.failed(payload: default_payload(args))
end
+ def inactive(**args)
+ Gitlab::MergeRequests::Mergeability::CheckResult
+ .inactive(payload: default_payload(args))
+ end
+
def default_payload(args)
args.merge(identifier: self.class.identifier)
end
diff --git a/app/services/merge_requests/mergeability/check_ci_status_service.rb b/app/services/merge_requests/mergeability/check_ci_status_service.rb
index f7fa3259d97..b4e60e964b7 100644
--- a/app/services/merge_requests/mergeability/check_ci_status_service.rb
+++ b/app/services/merge_requests/mergeability/check_ci_status_service.rb
@@ -7,6 +7,8 @@ module MergeRequests
end
def execute
+ return inactive unless merge_request.only_allow_merge_if_pipeline_succeeds?
+
if merge_request.mergeable_ci_state?
success
else
diff --git a/app/services/merge_requests/mergeability/check_discussions_status_service.rb b/app/services/merge_requests/mergeability/check_discussions_status_service.rb
index 34db5f8a944..f9cff5d1e5f 100644
--- a/app/services/merge_requests/mergeability/check_discussions_status_service.rb
+++ b/app/services/merge_requests/mergeability/check_discussions_status_service.rb
@@ -7,6 +7,8 @@ module MergeRequests
end
def execute
+ return inactive unless merge_request.only_allow_merge_if_all_discussions_are_resolved?
+
if merge_request.mergeable_discussions_state?
success
else
diff --git a/app/services/merge_requests/mergeability/check_rebase_status_service.rb b/app/services/merge_requests/mergeability/check_rebase_status_service.rb
index 2163fec8bd6..02cd0587be0 100644
--- a/app/services/merge_requests/mergeability/check_rebase_status_service.rb
+++ b/app/services/merge_requests/mergeability/check_rebase_status_service.rb
@@ -8,6 +8,8 @@ module MergeRequests
end
def execute
+ return inactive unless merge_request.project.ff_merge_must_be_possible?
+
if merge_request.should_be_rebased?
failure(reason: failure_reason)
else
diff --git a/app/services/merge_requests/mergeability/detailed_merge_status_service.rb b/app/services/merge_requests/mergeability/detailed_merge_status_service.rb
index 86c8122604c..92f0fb0429c 100644
--- a/app/services/merge_requests/mergeability/detailed_merge_status_service.rb
+++ b/app/services/merge_requests/mergeability/detailed_merge_status_service.rb
@@ -18,10 +18,10 @@ module MergeRequests
# If everything else is mergeable, but CI is not, the frontend expects two potential states to be returned
# See discussion: gitlab.com/gitlab-org/gitlab/-/merge_requests/96778#note_1093063523
- if check_ci_results.success?
- :mergeable
- else
+ if check_ci_results.failed?
ci_check_failure_reason
+ else
+ :mergeable
end
else
check_results.payload[:failure_reason]
diff --git a/app/services/merge_requests/mergeability/run_checks_service.rb b/app/services/merge_requests/mergeability/run_checks_service.rb
index 5150c03d0a3..92f3e5e951a 100644
--- a/app/services/merge_requests/mergeability/run_checks_service.rb
+++ b/app/services/merge_requests/mergeability/run_checks_service.rb
@@ -65,7 +65,7 @@ module MergeRequests
end
def all_results_success?
- results.all?(&:success?)
+ results.none?(&:failed?)
end
def failure_reason
diff --git a/app/services/merge_requests/push_options_handler_service.rb b/app/services/merge_requests/push_options_handler_service.rb
index 1890addf692..3f972e747b9 100644
--- a/app/services/merge_requests/push_options_handler_service.rb
+++ b/app/services/merge_requests/push_options_handler_service.rb
@@ -9,7 +9,12 @@ module MergeRequests
def initialize(project:, current_user:, changes:, push_options:, params: {})
super(project: project, current_user: current_user, params: params)
- @target_project = @project.default_merge_request_target
+ @target_project = if push_options[:target_project]
+ Project.find_by_full_path(push_options[:target_project])
+ else
+ @project.default_merge_request_target
+ end
+
@changes = Gitlab::ChangesList.new(changes)
@push_options = push_options
@errors = []
@@ -63,6 +68,10 @@ module MergeRequests
return
end
+ unless project == target_project || project.in_fork_network_of?(target_project)
+ errors << "Projects #{project.full_path} and #{target_project.full_path} are not in the same network"
+ end
+
unless target_project.merge_requests_enabled?
errors << "Merge requests are not enabled for project #{target_project.full_path}"
end
diff --git a/app/services/merge_requests/update_reviewer_state_service.rb b/app/services/merge_requests/update_reviewer_state_service.rb
new file mode 100644
index 00000000000..e2252f55fd3
--- /dev/null
+++ b/app/services/merge_requests/update_reviewer_state_service.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module MergeRequests
+ class UpdateReviewerStateService < MergeRequests::BaseService
+ def execute(merge_request, state)
+ return error("Invalid permissions") unless can?(current_user, :update_merge_request, merge_request)
+
+ reviewer = merge_request.find_reviewer(current_user)
+
+ if reviewer
+ return error("Failed to update reviewer") unless reviewer.update(state: state)
+
+ trigger_merge_request_reviewers_updated(merge_request)
+
+ return success if state != 'requested_changes'
+
+ if merge_request.approved_by?(current_user) && !remove_approval(merge_request)
+ return error("Failed to remove approval")
+ end
+
+ success
+ else
+ error("Reviewer not found")
+ end
+ end
+
+ private
+
+ def remove_approval(merge_request)
+ MergeRequests::RemoveApprovalService.new(project: project, current_user: current_user)
+ .execute(merge_request)
+ end
+ end
+end
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index 37a829e3014..fb6544a910a 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -168,6 +168,7 @@ module MergeRequests
merge_request.target_branch
)
+ delete_approvals_on_target_branch_change(merge_request)
refresh_pipelines_on_merge_requests(merge_request, allow_duplicate: true)
abort_auto_merge(merge_request, 'target branch was changed')
@@ -321,6 +322,10 @@ module MergeRequests
def trigger_merge_request_status_updated(merge_request)
GraphqlTriggers.merge_request_merge_status_updated(merge_request)
end
+
+ def delete_approvals_on_target_branch_change(_merge_request)
+ # Overridden in EE. No-op since we only want to delete approvals in EE.
+ end
end
end
diff --git a/app/services/ml/create_candidate_service.rb b/app/services/ml/create_candidate_service.rb
new file mode 100644
index 00000000000..53913c3fb19
--- /dev/null
+++ b/app/services/ml/create_candidate_service.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Ml
+ class CreateCandidateService
+ def initialize(experiment, params = {})
+ @experiment = experiment
+ @name = params[:name]
+ @user = params[:user]
+ @start_time = params[:start_time]
+ @model_version = params[:model_version]
+ end
+
+ def execute
+ Ml::Candidate.create!(
+ experiment: experiment,
+ project: experiment.project,
+ name: candidate_name,
+ start_time: start_time || 0,
+ user: user,
+ model_version: model_version
+ )
+ end
+
+ private
+
+ def candidate_name
+ name.presence || random_candidate_name
+ end
+
+ def random_candidate_name
+ parts = Array.new(3).map { FFaker::Animal.common_name.downcase.delete(' ') } << rand(10000)
+ parts.join('-').truncate(255)
+ end
+
+ attr_reader :name, :user, :experiment, :start_time, :model_version
+ end
+end
diff --git a/app/services/ml/create_model_service.rb b/app/services/ml/create_model_service.rb
new file mode 100644
index 00000000000..5c179d8edf7
--- /dev/null
+++ b/app/services/ml/create_model_service.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+module Ml
+ class CreateModelService
+ def initialize(project, name, user = nil, description = nil, metadata = [])
+ @project = project
+ @name = name
+ @description = description
+ @metadata = metadata
+ @user = user
+ end
+
+ def execute
+ ApplicationRecord.transaction do
+ model = Ml::Model.create!(
+ project: @project,
+ name: @name,
+ user: (@user.is_a?(User) ? @user : nil),
+ description: @description,
+ default_experiment: default_experiment
+ )
+
+ add_metadata(model, @metadata)
+
+ model
+ end
+ end
+
+ private
+
+ def default_experiment
+ @default_experiment ||= Ml::FindOrCreateExperimentService.new(@project, @name).execute
+ end
+
+ def add_metadata(model, metadata_key_value)
+ return unless model.present? && metadata_key_value.present?
+
+ entities = metadata_key_value.map do |d|
+ {
+ model_id: model.id,
+ name: d[:key],
+ value: d[:value]
+ }
+ end
+
+ entities.each do |entry|
+ ::Ml::ModelMetadata.create!(entry)
+ end
+ end
+ end
+end
diff --git a/app/services/ml/experiment_tracking/candidate_repository.rb b/app/services/ml/experiment_tracking/candidate_repository.rb
index 436f06e3ca5..8739379912a 100644
--- a/app/services/ml/experiment_tracking/candidate_repository.rb
+++ b/app/services/ml/experiment_tracking/candidate_repository.rb
@@ -15,12 +15,13 @@ module Ml
end
def create!(experiment, start_time, tags = nil, name = nil)
- candidate = experiment.candidates.create!(
+ create_params = {
+ start_time: start_time,
user: user,
- name: candidate_name(name, tags),
- project: project,
- start_time: start_time || 0
- )
+ name: candidate_name(name, tags)
+ }
+
+ candidate = Ml::CreateCandidateService.new(experiment, create_params).execute
add_tags(candidate, tags)
@@ -103,17 +104,12 @@ module Ml
end
def candidate_name(name, tags)
- name.presence || candidate_name_from_tags(tags) || random_candidate_name
+ name.presence || candidate_name_from_tags(tags)
end
def candidate_name_from_tags(tags)
tags&.detect { |t| t[:key] == 'mlflow.runName' }&.dig(:value)
end
-
- def random_candidate_name
- parts = Array.new(3).map { FFaker::Animal.common_name.downcase.delete(' ') } << rand(10000)
- parts.join('-').truncate(255)
- end
end
end
end
diff --git a/app/services/ml/find_model_service.rb b/app/services/ml/find_model_service.rb
new file mode 100644
index 00000000000..23ca0266629
--- /dev/null
+++ b/app/services/ml/find_model_service.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Ml
+ class FindModelService
+ def initialize(project, name)
+ @project = project
+ @name = name
+ end
+
+ def execute
+ Ml::Model.by_project_id_and_name(@project.id, @name)
+ end
+ end
+end
diff --git a/app/services/ml/find_or_create_model_service.rb b/app/services/ml/find_or_create_model_service.rb
index 66dec7a6234..9199730e84b 100644
--- a/app/services/ml/find_or_create_model_service.rb
+++ b/app/services/ml/find_or_create_model_service.rb
@@ -2,21 +2,17 @@
module Ml
class FindOrCreateModelService
- def initialize(project, model_name)
+ def initialize(project, name, user = nil, description = nil, metadata = [])
@project = project
- @name = model_name
+ @name = name
+ @description = description
+ @metadata = metadata
+ @user = user
end
def execute
- Ml::Model.find_or_create(
- project,
- name,
- Ml::FindOrCreateExperimentService.new(project, name).execute
- )
+ FindModelService.new(@project, @name).execute ||
+ CreateModelService.new(@project, @name, @user, @description, @metadata).execute
end
-
- private
-
- attr_reader :name, :project
end
end
diff --git a/app/services/ml/find_or_create_model_version_service.rb b/app/services/ml/find_or_create_model_version_service.rb
index f4d3f3e72d3..a5e9bf997cc 100644
--- a/app/services/ml/find_or_create_model_version_service.rb
+++ b/app/services/ml/find_or_create_model_version_service.rb
@@ -7,15 +7,20 @@ module Ml
@name = params[:model_name]
@version = params[:version]
@package = params[:package]
+ @description = params[:description]
end
def execute
- model = Ml::FindOrCreateModelService.new(project, name).execute
- Ml::ModelVersion.find_or_create!(model, version, package)
- end
+ model = Ml::FindOrCreateModelService.new(@project, @name).execute
+
+ model_version = Ml::ModelVersion.find_or_create!(model, @version, @package, @description)
- private
+ model_version.candidate = ::Ml::CreateCandidateService.new(
+ model.default_experiment,
+ { model_version: model_version }
+ ).execute
- attr_reader :version, :name, :project, :package
+ model_version
+ end
end
end
diff --git a/app/services/ml/model_versions/get_model_version_service.rb b/app/services/ml/model_versions/get_model_version_service.rb
new file mode 100644
index 00000000000..e8794689d73
--- /dev/null
+++ b/app/services/ml/model_versions/get_model_version_service.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Ml
+ module ModelVersions
+ class GetModelVersionService
+ def initialize(project, name, version)
+ @project = project
+ @name = name
+ @version = version
+ end
+
+ def execute
+ Ml::ModelVersion.by_project_id_name_and_version(
+ @project.id,
+ @name,
+ @version
+ )
+ end
+ end
+ end
+end
diff --git a/app/services/ml/update_model_service.rb b/app/services/ml/update_model_service.rb
new file mode 100644
index 00000000000..dade6c72588
--- /dev/null
+++ b/app/services/ml/update_model_service.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Ml
+ class UpdateModelService
+ def initialize(model, description)
+ @model = model
+ @description = description
+ end
+
+ def execute
+ @model.update!(description: @description)
+
+ @model
+ end
+ end
+end
diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb
index 1af26377b71..a63b1cf375f 100644
--- a/app/services/notes/create_service.rb
+++ b/app/services/notes/create_service.rb
@@ -226,8 +226,10 @@ module Notes
end
def set_reviewed(note)
- ::MergeRequests::MarkReviewerReviewedService.new(project: project, current_user: current_user)
- .execute(note.noteable)
+ return if Feature.enabled?(:mr_request_changes, current_user)
+
+ ::MergeRequests::UpdateReviewerStateService.new(project: project, current_user: current_user)
+ .execute(note.noteable, "reviewed")
end
end
end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index f1781b3d3c5..5099272a212 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -358,7 +358,7 @@ class NotificationService
def review_requested_of_merge_request(merge_request, current_user, reviewer)
recipients = NotificationRecipients::BuildService.build_requested_review_recipients(merge_request, current_user, reviewer)
- deliver_option = review_request_deliver_options(merge_request.project, reviewer)
+ deliver_option = review_request_deliver_options(merge_request.project)
recipients.each do |recipient|
mailer
@@ -975,7 +975,7 @@ class NotificationService
{}
end
- def review_request_deliver_options(project, user)
+ def review_request_deliver_options(project)
# Overridden in EE
{}
end
diff --git a/app/services/organizations/base_service.rb b/app/services/organizations/base_service.rb
new file mode 100644
index 00000000000..19bbc64ebdd
--- /dev/null
+++ b/app/services/organizations/base_service.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Organizations
+ class BaseService
+ include BaseServiceUtility
+
+ attr_reader :current_user, :params
+
+ def initialize(current_user: nil, params: {})
+ @current_user = current_user
+ @params = params.dup
+ end
+ end
+end
diff --git a/app/services/organizations/create_service.rb b/app/services/organizations/create_service.rb
new file mode 100644
index 00000000000..89c579032d2
--- /dev/null
+++ b/app/services/organizations/create_service.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Organizations
+ class CreateService < ::Organizations::BaseService
+ def execute
+ return error_no_permissions unless current_user&.can?(:create_organization)
+
+ organization = Organization.create(params)
+
+ return error_creating(organization) unless organization.persisted?
+
+ ServiceResponse.success(payload: organization)
+ end
+
+ private
+
+ def error_no_permissions
+ ServiceResponse.error(message: [_('You have insufficient permissions to create organizations')])
+ end
+
+ def error_creating(organization)
+ message = organization.errors.full_messages || _('Failed to create organization')
+
+ ServiceResponse.error(message: Array(message))
+ end
+ end
+end
diff --git a/app/services/packages/ml_model/create_package_file_service.rb b/app/services/packages/ml_model/create_package_file_service.rb
index b1e8e814015..ff569a8eecf 100644
--- a/app/services/packages/ml_model/create_package_file_service.rb
+++ b/app/services/packages/ml_model/create_package_file_service.rb
@@ -37,7 +37,8 @@ module Packages
model_version_params = {
model_name: package.name,
version: package.version,
- package: package
+ package: package,
+ user: current_user
}
Ml::FindOrCreateModelVersionService.new(project, model_version_params).execute
diff --git a/app/services/packages/npm/create_package_service.rb b/app/services/packages/npm/create_package_service.rb
index d599cecc8da..0f0dc297e9a 100644
--- a/app/services/packages/npm/create_package_service.rb
+++ b/app/services/packages/npm/create_package_service.rb
@@ -12,6 +12,7 @@ module Packages
return error('Version is empty.', 400) if version.blank?
return error('Attachment data is empty.', 400) if attachment['data'].blank?
return error('Package already exists.', 403) if current_package_exists?
+ return error('Package protected.', 403) if current_package_protected?
return error('File is too large.', 400) if file_size_exceeded?
package = try_obtain_lease do
@@ -56,6 +57,13 @@ module Packages
.exists?
end
+ def current_package_protected?
+ return false if Feature.disabled?(:packages_protected_packages, project)
+
+ user_project_authorization_access_level = current_user.max_member_access_for_project(project.id)
+ project.package_protection_rules.push_protected_from?(access_level: user_project_authorization_access_level, package_name: name, package_type: :npm)
+ end
+
def name
params[:name]
end
diff --git a/app/services/packages/nuget/check_duplicates_service.rb b/app/services/packages/nuget/check_duplicates_service.rb
index 7ad9038d7c1..33a66c2bce1 100644
--- a/app/services/packages/nuget/check_duplicates_service.rb
+++ b/app/services/packages/nuget/check_duplicates_service.rb
@@ -49,40 +49,30 @@ module Packages
strong_memoize_attr :existing_package
def metadata
- if remote_package_file?
- ExtractMetadataContentService
+ if params[:remote_url].present?
+ ::Packages::Nuget::ExtractMetadataContentService
.new(nuspec_file_content)
.execute
.payload
else # to cover the case when package file is on disk not in object storage
- MetadataExtractionService
- .new(mock_package_file)
- .execute
- .payload
+ Zip::InputStream.open(params[:file]) do |zip|
+ ::Packages::Nuget::MetadataExtractionService
+ .new(zip)
+ .execute
+ .payload
+ end
end
end
strong_memoize_attr :metadata
- def remote_package_file?
- params[:remote_url].present?
- end
-
def nuspec_file_content
- ExtractRemoteMetadataFileService
+ ::Packages::Nuget::ExtractRemoteMetadataFileService
.new(params[:remote_url])
.execute
.payload
- rescue ExtractRemoteMetadataFileService::ExtractionError => e
+ rescue ::Packages::Nuget::ExtractRemoteMetadataFileService::ExtractionError => e
raise ExtractionError, e.message
end
-
- def mock_package_file
- ::Packages::PackageFile.new(
- params
- .slice(:file, :file_name)
- .merge(package: ::Packages::Package.nuget.build)
- )
- end
end
end
end
diff --git a/app/services/packages/nuget/extract_metadata_file_service.rb b/app/services/packages/nuget/extract_metadata_file_service.rb
index fd4f9b5d1c1..1daf0aba8d6 100644
--- a/app/services/packages/nuget/extract_metadata_file_service.rb
+++ b/app/services/packages/nuget/extract_metadata_file_service.rb
@@ -20,7 +20,7 @@ module Packages
attr_reader :package_zip_file
def nuspec_file_content
- entry = package_zip_file.glob('*.nuspec').first
+ entry = extract_nuspec_file
raise ExtractionError, 'nuspec file not found' unless entry
raise ExtractionError, 'nuspec file too big' if MAX_FILE_SIZE < entry.size
@@ -32,6 +32,16 @@ module Packages
rescue Zip::EntrySizeError => e
raise ExtractionError, "nuspec file has the wrong entry size: #{e.message}"
end
+
+ def extract_nuspec_file
+ if package_zip_file.is_a?(Zip::InputStream)
+ while (entry = package_zip_file.get_next_entry) # rubocop:disable Lint/AssignmentInCondition -- Following https://github.com/rubyzip/rubyzip#notes-on-zipinputstream and that's why the disable rubocop rule
+ break entry if entry.name.end_with?('.nuspec')
+ end
+ else
+ package_zip_file.glob('*.nuspec').first
+ end
+ end
end
end
end
diff --git a/app/services/packages/nuget/metadata_extraction_service.rb b/app/services/packages/nuget/metadata_extraction_service.rb
index 53189063c85..813cb8e0979 100644
--- a/app/services/packages/nuget/metadata_extraction_service.rb
+++ b/app/services/packages/nuget/metadata_extraction_service.rb
@@ -3,8 +3,8 @@
module Packages
module Nuget
class MetadataExtractionService
- def initialize(package_file)
- @package_file = package_file
+ def initialize(package_zip_file)
+ @package_zip_file = package_zip_file
end
def execute
@@ -13,19 +13,20 @@ module Packages
private
- attr_reader :package_file
+ attr_reader :package_zip_file
def metadata
- ExtractMetadataContentService
+ ::Packages::Nuget::ExtractMetadataContentService
.new(nuspec_file_content)
.execute
.payload
end
def nuspec_file_content
- ProcessPackageFileService
- .new(package_file)
- .execute[:nuspec_file_content]
+ ::Packages::Nuget::ExtractMetadataFileService
+ .new(package_zip_file)
+ .execute
+ .payload
end
end
end
diff --git a/app/services/packages/nuget/process_package_file_service.rb b/app/services/packages/nuget/process_package_file_service.rb
index fa7a84ee3d6..99b59bd3322 100644
--- a/app/services/packages/nuget/process_package_file_service.rb
+++ b/app/services/packages/nuget/process_package_file_service.rb
@@ -4,7 +4,6 @@ module Packages
module Nuget
class ProcessPackageFileService
ExtractionError = Class.new(StandardError)
- NUGET_SYMBOL_FILE_EXTENSION = '.snupkg'
def initialize(package_file)
@package_file = package_file
@@ -13,14 +12,9 @@ module Packages
def execute
raise ExtractionError, 'invalid package file' unless valid_package_file?
- nuspec_content = nil
-
with_zip_file do |zip_file|
- nuspec_content = nuspec_file_content(zip_file)
- create_symbol_files(zip_file) if symbol_package_file?
+ ::Packages::Nuget::UpdatePackageFromMetadataService.new(package_file, zip_file).execute
end
-
- ServiceResponse.success(payload: { nuspec_file_content: nuspec_content })
end
private
@@ -38,23 +32,6 @@ module Packages
Zip::File.open(open_file.file_path, &block) # rubocop: disable Performance/Rubyzip
end
end
-
- def nuspec_file_content(zip_file)
- ::Packages::Nuget::ExtractMetadataFileService
- .new(zip_file)
- .execute
- .payload
- end
-
- def create_symbol_files(zip_file)
- ::Packages::Nuget::Symbols::CreateSymbolFilesService
- .new(package_file.package, zip_file)
- .execute
- end
-
- def symbol_package_file?
- package_file.file_name.end_with?(NUGET_SYMBOL_FILE_EXTENSION)
- end
end
end
end
diff --git a/app/services/packages/nuget/symbols/create_symbol_files_service.rb b/app/services/packages/nuget/symbols/create_symbol_files_service.rb
index 03e14ba00e1..5f0b8762054 100644
--- a/app/services/packages/nuget/symbols/create_symbol_files_service.rb
+++ b/app/services/packages/nuget/symbols/create_symbol_files_service.rb
@@ -18,7 +18,7 @@ module Packages
process_symbol_entries
rescue ExtractionError => e
- Gitlab::ErrorTracking.log_exception(e, class: self.class.name, package_id: package.id)
+ Gitlab::ErrorTracking.track_exception(e, class: self.class.name, package_id: package.id)
end
private
@@ -31,7 +31,7 @@ module Packages
raise ExtractionError, 'too many symbol entries' if index >= SYMBOL_ENTRIES_LIMIT
entry.extract(tmp_file.path) { true }
- File.open(tmp_file.path) do |file|
+ File.open(tmp_file.path, 'rb') do |file|
create_symbol(entry.name, file)
end
end
@@ -43,25 +43,27 @@ module Packages
end
def create_symbol(path, file)
- signature = extract_signature(file.read(1.kilobyte))
- return if signature.blank?
+ signature, checksum = extract_signature_and_checksum(file)
+ return if signature.blank? || checksum.blank?
::Packages::Nuget::Symbol.create!(
package: package,
file: { tempfile: file, filename: path.downcase, content_type: CONTENT_TYPE },
file_path: path,
signature: signature,
- size: file.size
+ size: file.size,
+ file_sha256: checksum
)
rescue StandardError => e
- Gitlab::ErrorTracking.log_exception(e, class: self.class.name, package_id: package.id)
+ Gitlab::ErrorTracking.track_exception(e, class: self.class.name, package_id: package.id)
end
- def extract_signature(content_fragment)
- ExtractSymbolSignatureService
- .new(content_fragment)
+ def extract_signature_and_checksum(file)
+ ::Packages::Nuget::Symbols::ExtractSignatureAndChecksumService
+ .new(file)
.execute
.payload
+ .values_at(:signature, :checksum)
end
end
end
diff --git a/app/services/packages/nuget/symbols/extract_signature_and_checksum_service.rb b/app/services/packages/nuget/symbols/extract_signature_and_checksum_service.rb
new file mode 100644
index 00000000000..fd37d139145
--- /dev/null
+++ b/app/services/packages/nuget/symbols/extract_signature_and_checksum_service.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+module Packages
+ module Nuget
+ module Symbols
+ class ExtractSignatureAndChecksumService
+ include Gitlab::Utils::StrongMemoize
+
+ # More information about the GUID format can be found here:
+ # https://github.com/dotnet/symstore/blob/main/docs/specs/SSQP_Key_Conventions.md#key-formatting-basic-rules
+ GUID_START_INDEX = 7
+ GUID_END_INDEX = 26
+ SIGNATURE_LENGTH = 16
+ TWENTY_ZEROED_BYTES = "\u0000" * 20
+ GUID_PARTS_LENGTHS = [4, 2, 2, 8].freeze
+ GUID_AGE_PART = 'FFFFFFFF'
+ TWO_CHARACTER_HEX_REGEX = /\h{2}/
+ GUID_CHUNK_SIZE = 256.bytes
+ SHA_CHUNK_SIZE = 16.kilobytes
+
+ # The extraction of the signature in this service is based on the following documentation:
+ # https://github.com/dotnet/symstore/blob/main/docs/specs/SSQP_Key_Conventions.md#portable-pdb-signature
+
+ def initialize(file)
+ @file = file
+ end
+
+ def execute
+ return error_response unless signature
+
+ ServiceResponse.success(payload: { signature: signature, checksum: checksum })
+ end
+
+ private
+
+ attr_reader :file
+
+ def signature
+ return unless pdb_id
+
+ # Convert the GUID into an array of two-character hex strings
+ guid = pdb_id.first(SIGNATURE_LENGTH).unpack('H*').flat_map { |el| el.scan(TWO_CHARACTER_HEX_REGEX) }
+
+ # Reorder the GUID parts based on arbitrary lengths
+ guid = GUID_PARTS_LENGTHS.map { |length| guid.shift(length) }
+
+ # Concatenate the parts of the GUID back together
+ result = guid.first(3).map(&:reverse)
+ result << guid.last
+ result = result.join
+ result << GUID_AGE_PART
+ end
+ strong_memoize_attr :signature
+
+ # https://github.com/dotnet/corefx/blob/master/src/System.Reflection.Metadata/specs/PE-COFF.md#portable-pdb-checksum
+ def checksum
+ sha = OpenSSL::Digest.new('SHA256')
+ count = 0
+ chunk = (+'').force_encoding(Encoding::BINARY)
+ file.rewind
+
+ while file.read(SHA_CHUNK_SIZE, chunk)
+ count += 1
+ chunk[pdb_id] = TWENTY_ZEROED_BYTES if count == 1
+ sha.update(chunk)
+ end
+
+ sha.hexdigest
+ end
+
+ def pdb_id
+ # The ID is located in the first 256 bytes of the symbol `.pdb` file
+ chunk = file.read(GUID_CHUNK_SIZE)
+ return unless chunk
+
+ # Find the index of the first occurrence of 'Blob'
+ guid_index = chunk.index('Blob')
+ return unless guid_index
+
+ # Extract the binary GUID from the symbol content
+ chunk[(guid_index + GUID_START_INDEX)..(guid_index + GUID_END_INDEX)]
+ end
+ strong_memoize_attr :pdb_id
+
+ def error_response
+ ServiceResponse.error(message: 'Could not find the signature in the symbol file')
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/packages/nuget/symbols/extract_symbol_signature_service.rb b/app/services/packages/nuget/symbols/extract_symbol_signature_service.rb
deleted file mode 100644
index c2ccdb517b5..00000000000
--- a/app/services/packages/nuget/symbols/extract_symbol_signature_service.rb
+++ /dev/null
@@ -1,63 +0,0 @@
-# frozen_string_literal: true
-
-module Packages
- module Nuget
- module Symbols
- class ExtractSymbolSignatureService
- include Gitlab::Utils::StrongMemoize
-
- # More information about the GUID format can be found here:
- # https://github.com/dotnet/symstore/blob/main/docs/specs/SSQP_Key_Conventions.md#key-formatting-basic-rules
- GUID_START_INDEX = 7
- GUID_END_INDEX = 22
- GUID_PARTS_LENGTHS = [4, 2, 2, 8].freeze
- GUID_AGE_PART = 'FFFFFFFF'
- TWO_CHARACTER_HEX_REGEX = /\h{2}/
-
- # The extraction of the signature in this service is based on the following documentation:
- # https://github.com/dotnet/symstore/blob/main/docs/specs/SSQP_Key_Conventions.md#portable-pdb-signature
-
- def initialize(symbol_content)
- @symbol_content = symbol_content
- end
-
- def execute
- return error_response unless signature
-
- ServiceResponse.success(payload: signature)
- end
-
- private
-
- attr_reader :symbol_content
-
- def signature
- # Find the index of the first occurrence of 'Blob'
- guid_index = symbol_content.index('Blob')
- return if guid_index.nil?
-
- # Extract the binary GUID from the symbol content
- guid = symbol_content[(guid_index + GUID_START_INDEX)..(guid_index + GUID_END_INDEX)]
- return if guid.nil?
-
- # Convert the GUID into an array of two-character hex strings
- guid = guid.unpack('H*').flat_map { |el| el.scan(TWO_CHARACTER_HEX_REGEX) }
-
- # Reorder the GUID parts based on arbitrary lengths
- guid = GUID_PARTS_LENGTHS.map { |length| guid.shift(length) }
-
- # Concatenate the parts of the GUID back together
- result = guid.first(3).map(&:reverse)
- result << guid.last
- result = result.join
- result << GUID_AGE_PART
- end
- strong_memoize_attr :signature
-
- def error_response
- ServiceResponse.error(message: 'Could not find the signature in the symbol file')
- end
- end
- end
- end
-end
diff --git a/app/services/packages/nuget/update_package_from_metadata_service.rb b/app/services/packages/nuget/update_package_from_metadata_service.rb
index 4cec4ed2fae..b7411d5f8a8 100644
--- a/app/services/packages/nuget/update_package_from_metadata_service.rb
+++ b/app/services/packages/nuget/update_package_from_metadata_service.rb
@@ -13,11 +13,11 @@ module Packages
INVALID_METADATA_ERROR_SYMBOL_MESSAGE = 'package name, version and/or description not found in metadata'
MISSING_MATCHING_PACKAGE_ERROR_MESSAGE = 'symbol package is invalid, matching package does not exist'
- InvalidMetadataError = Class.new(StandardError)
- ZipError = Class.new(StandardError)
+ InvalidMetadataError = ZipError = Class.new(StandardError)
- def initialize(package_file)
+ def initialize(package_file, package_zip_file)
@package_file = package_file
+ @package_zip_file = package_zip_file
end
def execute
@@ -57,7 +57,7 @@ module Packages
build_infos = package_to_destroy&.build_infos || []
update_package(target_package, build_infos)
- update_symbol_files(target_package, package_to_destroy) if symbol_package?
+ create_symbol_files
::Packages::UpdatePackageFileService.new(@package_file, package_id: target_package.id, file_name: package_filename)
.execute
package_to_destroy&.destroy!
@@ -79,8 +79,12 @@ module Packages
raise InvalidMetadataError, e.message
end
- def update_symbol_files(package, package_to_destroy)
- package_to_destroy.nuget_symbols.update_all(package_id: package.id)
+ def create_symbol_files
+ return unless symbol_package?
+
+ ::Packages::Nuget::Symbols::CreateSymbolFilesService
+ .new(existing_package, @package_zip_file)
+ .execute
end
def valid_metadata?
@@ -145,9 +149,10 @@ module Packages
def symbol_package?
package_types.include?(SYMBOL_PACKAGE_IDENTIFIER)
end
+ strong_memoize_attr :symbol_package?
def metadata
- ::Packages::Nuget::MetadataExtractionService.new(@package_file).execute.payload
+ ::Packages::Nuget::MetadataExtractionService.new(@package_zip_file).execute.payload
end
strong_memoize_attr :metadata
diff --git a/app/services/packages/protection/delete_rule_service.rb b/app/services/packages/protection/delete_rule_service.rb
new file mode 100644
index 00000000000..a1fa111b57b
--- /dev/null
+++ b/app/services/packages/protection/delete_rule_service.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module Packages
+ module Protection
+ class DeleteRuleService
+ include Gitlab::Allowable
+
+ def initialize(package_protection_rule, current_user:)
+ if package_protection_rule.blank? || current_user.blank?
+ raise ArgumentError,
+ 'package_protection_rule and current_user must be set'
+ end
+
+ @package_protection_rule = package_protection_rule
+ @current_user = current_user
+ end
+
+ def execute
+ unless can?(current_user, :admin_package, package_protection_rule.project)
+ error_message = _('Unauthorized to delete a package protection rule')
+ return service_response_error(message: error_message)
+ end
+
+ deleted_package_protection_rule = package_protection_rule.destroy!
+
+ ServiceResponse.success(payload: { package_protection_rule: deleted_package_protection_rule })
+ rescue StandardError => e
+ service_response_error(message: e.message)
+ end
+
+ private
+
+ attr_reader :package_protection_rule, :current_user
+
+ def service_response_error(message:)
+ ServiceResponse.error(
+ message: message,
+ payload: { package_protection_rule: nil }
+ )
+ end
+ end
+ end
+end
diff --git a/app/services/packages/pypi/create_package_service.rb b/app/services/packages/pypi/create_package_service.rb
index 087a8e42a66..fca7b1bca37 100644
--- a/app/services/packages/pypi/create_package_service.rb
+++ b/app/services/packages/pypi/create_package_service.rb
@@ -9,7 +9,13 @@ module Packages
::Packages::Package.transaction do
meta = Packages::Pypi::Metadatum.new(
package: created_package,
- required_python: params[:requires_python] || ''
+ required_python: params[:requires_python] || '',
+ metadata_version: params[:metadata_version],
+ author_email: params[:author_email],
+ description: params[:description],
+ description_content_type: params[:description_content_type],
+ summary: params[:summary],
+ keywords: params[:keywords]
)
unless meta.valid?
diff --git a/app/services/packages/update_tags_service.rb b/app/services/packages/update_tags_service.rb
index cf1acc6ee19..014d5501b76 100644
--- a/app/services/packages/update_tags_service.rb
+++ b/app/services/packages/update_tags_service.rb
@@ -32,7 +32,8 @@ module Packages
package_id: @package.id,
name: tag,
created_at: now,
- updated_at: now
+ updated_at: now,
+ project_id: @package.project_id
}
end
end
diff --git a/app/services/pages/delete_service.rb b/app/services/pages/delete_service.rb
index dcee4c5b665..96b451aeba4 100644
--- a/app/services/pages/delete_service.rb
+++ b/app/services/pages/delete_service.rb
@@ -3,7 +3,7 @@
module Pages
class DeleteService < BaseService
def execute
- project.mark_pages_as_not_deployed
+ PagesDeployment.deactivate_all(project)
# project.pages_domains.delete_all will just nullify project_id:
# > If no :dependent option is given, then it will follow the default
diff --git a/app/services/personal_access_tokens/rotate_service.rb b/app/services/personal_access_tokens/rotate_service.rb
index b765aacef68..32710629caf 100644
--- a/app/services/personal_access_tokens/rotate_service.rb
+++ b/app/services/personal_access_tokens/rotate_service.rb
@@ -9,7 +9,7 @@ module PersonalAccessTokens
@token = token
end
- def execute
+ def execute(params = {})
return ServiceResponse.error(message: _('token already revoked')) if token.revoked?
response = ServiceResponse.success
@@ -21,7 +21,7 @@ module PersonalAccessTokens
end
target_user = token.user
- new_token = target_user.personal_access_tokens.create(create_token_params(token))
+ new_token = target_user.personal_access_tokens.create(create_token_params(token, params))
if new_token.persisted?
response = ServiceResponse.success(payload: { personal_access_token: new_token })
@@ -39,12 +39,13 @@ module PersonalAccessTokens
attr_reader :current_user, :token
- def create_token_params(token)
+ def create_token_params(token, params)
+ expires_at = params[:expires_at] || (Date.today + EXPIRATION_PERIOD)
{ name: token.name,
previous_personal_access_token_id: token.id,
impersonation: token.impersonation,
scopes: token.scopes,
- expires_at: Date.today + EXPIRATION_PERIOD }
+ expires_at: expires_at }
end
end
end
diff --git a/app/services/projects/container_repository/gitlab/delete_tags_service.rb b/app/services/projects/container_repository/gitlab/delete_tags_service.rb
index a5b7f4bbb6f..6dc50dac7a4 100644
--- a/app/services/projects/container_repository/gitlab/delete_tags_service.rb
+++ b/app/services/projects/container_repository/gitlab/delete_tags_service.rb
@@ -34,7 +34,7 @@ module Projects
@tag_names.each do |name|
raise TimeoutError if timeout?(start_time)
- if @container_repository.delete_tag_by_name(name)
+ if @container_repository.delete_tag(name)
@deleted_tags.append(name)
end
end
diff --git a/app/services/projects/container_repository/third_party/delete_tags_service.rb b/app/services/projects/container_repository/third_party/delete_tags_service.rb
index 942df177bea..ae3f1cc23d6 100644
--- a/app/services/projects/container_repository/third_party/delete_tags_service.rb
+++ b/app/services/projects/container_repository/third_party/delete_tags_service.rb
@@ -30,7 +30,7 @@ module Projects
# Deletes the dummy image
# All created tag digests are the same since they all have the same dummy image.
# a single delete is sufficient to remove all tags with it
- if deleted_tags.any? && @container_repository.delete_tag_by_digest(deleted_tags.each_value.first)
+ if deleted_tags.any? && @container_repository.delete_tag(deleted_tags.each_value.first)
success(deleted: deleted_tags.keys)
else
error("could not delete tags: #{@tag_names.join(', ')}".truncate(1000))
diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb
index a2a2f9d2800..8c86646ba5c 100644
--- a/app/services/projects/destroy_service.rb
+++ b/app/services/projects/destroy_service.rb
@@ -18,6 +18,15 @@ module Projects
return false unless can?(current_user, :remove_project, project)
project.update_attribute(:pending_delete, true)
+
+ # There is a possibility of active repository move processes for
+ # project and snippets. An attempt to delete the project at the same time
+ # can lead to race condition and an inconsistent state.
+ #
+ # This validation stops the project delete process if it detects active
+ # repository move schedules for it.
+ validate_active_repositories_move!
+
# Flush the cache for both repositories. This has to be done _before_
# removing the physical repositories as some expiration code depends on
# Git data (e.g. a list of branch names).
@@ -50,6 +59,16 @@ module Projects
private
+ def validate_active_repositories_move!
+ if project.repository_storage_moves.scheduled_or_started.exists?
+ raise_error(s_("DeleteProject|Couldn't remove the project. A project repository storage move is in progress. Try again when it's complete."))
+ end
+
+ if ::ProjectSnippet.by_project(project).with_repository_storage_moves.merge(::Snippets::RepositoryStorageMove.scheduled_or_started).exists?
+ raise_error(s_("DeleteProject|Couldn't remove the project. A related snippet repository storage move is in progress. Try again when it's complete."))
+ end
+ end
+
def trash_project_repositories!
unless remove_repository(project.repository)
raise_error(s_('DeleteProject|Failed to remove project repository. Please try again or contact administrator.'))
diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb
index aace8846afc..168420b17bf 100644
--- a/app/services/projects/fork_service.rb
+++ b/app/services/projects/fork_service.rb
@@ -17,6 +17,10 @@ module Projects
@valid_fork_targets ||= ForkTargetsFinder.new(@project, current_user).execute(options)
end
+ def valid_fork_branch?(branch)
+ @project.repository.branch_exists?(branch)
+ end
+
def valid_fork_target?(namespace = target_namespace)
return true if current_user.admin?
@@ -68,7 +72,8 @@ module Projects
external_authorization_classification_label: @project.external_authorization_classification_label,
suggestion_commit_message: @project.suggestion_commit_message,
merge_commit_template: @project.merge_commit_template,
- squash_commit_template: @project.squash_commit_template
+ squash_commit_template: @project.squash_commit_template,
+ import_data: { data: { fork_branch: branch } }
}
if @project.avatar.present? && @project.avatar.image?
@@ -145,6 +150,12 @@ module Projects
def stream_audit_event(forked_project)
# Defined in EE
end
+
+ def branch
+ # We extract branch name from @params[:branches] because the front end
+ # insists on sending it as 'branches'.
+ @params[:branches]
+ end
end
end
diff --git a/app/services/projects/group_links/destroy_service.rb b/app/services/projects/group_links/destroy_service.rb
index a2307bfebf0..e0218ae087e 100644
--- a/app/services/projects/group_links/destroy_service.rb
+++ b/app/services/projects/group_links/destroy_service.rb
@@ -3,8 +3,10 @@
module Projects
module GroupLinks
class DestroyService < BaseService
- def execute(group_link)
- return false unless group_link
+ def execute(group_link, skip_authorization: false)
+ unless valid_to_destroy?(group_link, skip_authorization)
+ return ServiceResponse.error(message: 'Not found', reason: :not_found)
+ end
if group_link.project.private?
TodosDestroyer::ProjectPrivateWorker.perform_in(Todo::WAIT_FOR_DELETE, project.id)
@@ -12,20 +14,29 @@ module Projects
TodosDestroyer::ConfidentialIssueWorker.perform_in(Todo::WAIT_FOR_DELETE, nil, project.id)
end
- group_link.destroy.tap do |link|
- refresh_project_authorizations_asynchronously(link.project)
+ link = group_link.destroy
- # Until we compare the inconsistency rates of the new specialized worker and
- # the old approach, we still run AuthorizedProjectsWorker
- # but with some delay and lower urgency as a safety net.
- link.group.refresh_members_authorized_projects(
- priority: UserProjectAccessChangedService::LOW_PRIORITY
- )
- end
+ refresh_project_authorizations_asynchronously(link.project)
+
+ # Until we compare the inconsistency rates of the new specialized worker and
+ # the old approach, we still run AuthorizedProjectsWorker
+ # but with some delay and lower urgency as a safety net.
+ link.group.refresh_members_authorized_projects(
+ priority: UserProjectAccessChangedService::LOW_PRIORITY
+ )
+
+ ServiceResponse.success(payload: { link: link })
end
private
+ def valid_to_destroy?(group_link, skip_authorization)
+ return false unless group_link
+ return true if skip_authorization
+
+ current_user.can?(:admin_project_group_link, group_link)
+ end
+
def refresh_project_authorizations_asynchronously(project)
AuthorizedProjectUpdate::ProjectRecalculateWorker.perform_async(project.id)
end
diff --git a/app/services/projects/group_links/update_service.rb b/app/services/projects/group_links/update_service.rb
index 9b2565adaca..04f1552d929 100644
--- a/app/services/projects/group_links/update_service.rb
+++ b/app/services/projects/group_links/update_service.rb
@@ -10,15 +10,23 @@ module Projects
end
def execute(group_link_params)
+ return ServiceResponse.error(message: 'Not found', reason: :not_found) unless allowed_to_update?
+
group_link.update!(group_link_params)
refresh_authorizations if requires_authorization_refresh?(group_link_params)
+
+ ServiceResponse.success
end
private
attr_reader :group_link
+ def allowed_to_update?
+ current_user.can?(:admin_project_member, project)
+ end
+
def refresh_authorizations
AuthorizedProjectUpdate::ProjectRecalculateWorker.perform_async(project.id)
diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb
index ab38efff7c9..83b28840d39 100644
--- a/app/services/projects/update_pages_service.rb
+++ b/app/services/projects/update_pages_service.rb
@@ -33,7 +33,7 @@ module Projects
break error('The uploaded artifact size does not match the expected value') unless deployment
break error(deployment_update.errors.first.full_message) unless deployment_update.valid?
- update_project_pages_deployment(deployment)
+ deactive_old_deployments(deployment)
success
end
rescue StandardError => e
@@ -45,7 +45,6 @@ module Projects
def success
commit_status.success
- @project.mark_pages_as_deployed
publish_deployed_event
super
end
@@ -84,11 +83,11 @@ module Projects
def create_pages_deployment(artifacts_path, build)
File.open(artifacts_path) do |file|
attributes = pages_deployment_attributes(file, build)
- deployment = project.pages_deployments.create!(**attributes)
+ deployment = project.pages_deployments.build(**attributes)
- break if deployment.size != file.size || deployment.file.size != file.size
+ break if deployment.file.size != file.size
- deployment
+ deployment.tap(&:save!)
end
end
@@ -103,9 +102,7 @@ module Projects
}
end
- def update_project_pages_deployment(deployment)
- project.update_pages_deployment!(deployment)
-
+ def deactive_old_deployments(deployment)
PagesDeployment.deactivate_deployments_older_than(
deployment,
time: OLD_DEPLOYMENTS_DESTRUCTION_DELAY.from_now)
diff --git a/app/services/projects/update_repository_storage_service.rb b/app/services/projects/update_repository_storage_service.rb
index 85fb1890fcd..a9f6afb26c9 100644
--- a/app/services/projects/update_repository_storage_service.rb
+++ b/app/services/projects/update_repository_storage_service.rb
@@ -8,7 +8,9 @@ module Projects
private
- def track_repository(_destination_storage_name)
+ def track_repository(destination_storage_name)
+ project.update!(repository_storage: destination_storage_name)
+
# Connect project to pool repository from the new shard
project.swap_pool_repository!
diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb
index e5e39247dbf..336e887c241 100644
--- a/app/services/projects/update_service.rb
+++ b/app/services/projects/update_service.rb
@@ -58,11 +58,11 @@ module Projects
def validate!
unless valid_visibility_level_change?(project, project.visibility_attribute_value(params))
- raise ValidationError, s_('UpdateProject|New visibility level not allowed!')
+ raise_validation_error(s_('UpdateProject|New visibility level not allowed!'))
end
if renaming_project_with_container_registry_tags?
- raise ValidationError, s_('UpdateProject|Cannot rename project because it contains container registry tags!')
+ raise_validation_error(s_('UpdateProject|Cannot rename project because it contains container registry tags!'))
end
validate_default_branch_change
@@ -78,21 +78,22 @@ module Projects
params[:previous_default_branch] = previous_default_branch
if !project.root_ref?(new_default_branch) && has_custom_head_branch?
- raise ValidationError,
+ raise_validation_error(
format(
s_("UpdateProject|Could not set the default branch. Do you have a branch named 'HEAD' in your repository? (%{linkStart}How do I fix this?%{linkEnd})"),
linkStart: ambiguous_head_documentation_link, linkEnd: '</a>'
).html_safe
+ )
end
after_default_branch_change(previous_default_branch)
else
- raise ValidationError, s_("UpdateProject|Could not set the default branch")
+ raise_validation_error(s_("UpdateProject|Could not set the default branch"))
end
end
def ambiguous_head_documentation_link
- url = Rails.application.routes.url_helpers.help_page_path('user/project/repository/branches/index.md', anchor: 'error-ambiguous-head-branch-exists')
+ url = Rails.application.routes.url_helpers.help_page_path('user/project/repository/branches/index', anchor: 'error-ambiguous-head-branch-exists')
format('<a href="%{url}" target="_blank" rel="noopener noreferrer">', url: url)
end
@@ -144,6 +145,10 @@ module Projects
AfterRenameService.new(project, path_before: project.path_before_last_save, full_path_before: project.full_path_before_last_save)
end
+ def raise_validation_error(message)
+ raise ValidationError, message
+ end
+
def update_failed!
model_errors = project.errors.full_messages.to_sentence
error_message = model_errors.presence || s_('UpdateProject|Project could not be updated!')
diff --git a/app/services/releases/base_service.rb b/app/services/releases/base_service.rb
index 5d6cb372653..088776b896c 100644
--- a/app/services/releases/base_service.rb
+++ b/app/services/releases/base_service.rb
@@ -111,6 +111,10 @@ module Releases
# overridden in EE
def project_group_id; end
+
+ def audit(release, action:)
+ # overridden in EE
+ end
end
end
diff --git a/app/services/releases/create_service.rb b/app/services/releases/create_service.rb
index 95e0861a37a..38c9e6d60a7 100644
--- a/app/services/releases/create_service.rb
+++ b/app/services/releases/create_service.rb
@@ -18,12 +18,6 @@ module Releases
return tag unless tag.is_a?(Gitlab::Git::Tag)
- if project.catalog_resource
- response = Ci::Catalog::Resources::ValidateService.new(project, ref).execute
-
- return error(response.message) if response.error?
- end
-
create_release(tag, evidence_pipeline)
end
@@ -56,6 +50,12 @@ module Releases
def create_release(tag, evidence_pipeline)
release = build_release(tag)
+ if project.catalog_resource && release.valid?
+ response = Ci::Catalog::Resources::ReleaseService.new(release).execute
+
+ return error(response.message, 422) if response.error?
+ end
+
release.save!
notify_create_release(release)
@@ -64,6 +64,8 @@ module Releases
create_evidence!(release, evidence_pipeline)
+ audit(release, action: :created)
+
success(tag: tag, release: release)
rescue StandardError => e
error(e.message, 400)
diff --git a/app/services/releases/destroy_service.rb b/app/services/releases/destroy_service.rb
index 78613c05ff1..1e8338651a8 100644
--- a/app/services/releases/destroy_service.rb
+++ b/app/services/releases/destroy_service.rb
@@ -11,6 +11,8 @@ module Releases
execute_hooks(release, 'delete')
+ audit(release, action: :deleted)
+
success(tag: existing_tag, release: release)
else
error(release.errors.messages || '400 Bad request', 400)
diff --git a/app/services/releases/update_service.rb b/app/services/releases/update_service.rb
index c11d9468814..13ece1c10c8 100644
--- a/app/services/releases/update_service.rb
+++ b/app/services/releases/update_service.rb
@@ -19,6 +19,8 @@ module Releases
ApplicationRecord.transaction do
if release.update(params)
execute_hooks(release, 'update')
+ audit(release, action: :updated)
+ audit(release, action: :milestones_updated) if milestones_updated?(previous_milestones)
success(tag: existing_tag, release: release, milestones_updated: milestones_updated?(previous_milestones))
else
error(release.errors.messages || '400 Bad request', 400)
diff --git a/app/services/resource_access_tokens/create_service.rb b/app/services/resource_access_tokens/create_service.rb
index 1c496aa5e77..824b1a8c377 100644
--- a/app/services/resource_access_tokens/create_service.rb
+++ b/app/services/resource_access_tokens/create_service.rb
@@ -17,6 +17,8 @@ module ResourceAccessTokens
access_level = params[:access_level] || Gitlab::Access::MAINTAINER
return error("Could not provision owner access to project access token") if do_not_allow_owner_access_level_for_project_bot?(access_level)
+ return error("Access level of the token can't be greater the access level of the user who created the token") unless validate_access_level(access_level)
+
return error(s_('AccessTokens|Access token limit reached')) if reached_access_token_limit?
user = create_user
@@ -125,6 +127,14 @@ module ResourceAccessTokens
ServiceResponse.success(payload: { access_token: access_token })
end
+ def validate_access_level(access_level)
+ return true unless resource.is_a?(Project)
+ return true if current_user.bot?
+ return true if current_user.can?(:manage_owners, resource)
+
+ current_user.authorized_project?(resource, access_level.to_i)
+ end
+
def do_not_allow_owner_access_level_for_project_bot?(access_level)
resource.is_a?(Project) &&
access_level.to_i == Gitlab::Access::OWNER &&
diff --git a/app/services/resource_events/base_synthetic_notes_builder_service.rb b/app/services/resource_events/base_synthetic_notes_builder_service.rb
index e675bb61072..9943fd4910b 100644
--- a/app/services/resource_events/base_synthetic_notes_builder_service.rb
+++ b/app/services/resource_events/base_synthetic_notes_builder_service.rb
@@ -44,10 +44,9 @@ module ResourceEvents
end
def resource_parent
- strong_memoize(:resource_parent) do
- resource.project || resource.group
- end
+ resource.try(:resource_parent) || resource.project || resource.group
end
+ strong_memoize_attr :resource_parent
def table_name
raise NotImplementedError
diff --git a/app/services/resource_events/merge_into_notes_service.rb b/app/services/resource_events/merge_into_notes_service.rb
index ea465c1e75e..eb0023937b2 100644
--- a/app/services/resource_events/merge_into_notes_service.rb
+++ b/app/services/resource_events/merge_into_notes_service.rb
@@ -37,4 +37,4 @@ module ResourceEvents
end
end
-ResourceEvents::MergeIntoNotesService.prepend_mod_with('ResourceEvents::MergeIntoNotesService')
+ResourceEvents::MergeIntoNotesService.prepend_mod
diff --git a/app/services/security/ci_configuration/sast_parser_service.rb b/app/services/security/ci_configuration/sast_parser_service.rb
index 16a9efcefdf..f466dd0b649 100644
--- a/app/services/security/ci_configuration/sast_parser_service.rb
+++ b/app/services/security/ci_configuration/sast_parser_service.rb
@@ -89,17 +89,15 @@ module Security
def gitlab_ci_yml_attributes
@gitlab_ci_yml_attributes ||= begin
- config_content = @project.repository.blob_data_at(@project.repository.root_ref_sha, ci_config_file)
+ config_content = @project.repository.blob_data_at(
+ @project.repository.root_ref_sha, @project.ci_config_path_or_default
+ )
return {} unless config_content
build_sast_attributes(config_content)
end
end
- def ci_config_file
- '.gitlab-ci.yml'
- end
-
def build_sast_attributes(content)
options = { project: @project, user: current_user, sha: @project.repository.commit.sha }
yaml_result = Gitlab::Ci::YamlProcessor.new(content, options).execute
diff --git a/app/services/service_desk/custom_email_verifications/update_service.rb b/app/services/service_desk/custom_email_verifications/update_service.rb
index 5ef36ce0576..fbd217e3a3e 100644
--- a/app/services/service_desk/custom_email_verifications/update_service.rb
+++ b/app/services/service_desk/custom_email_verifications/update_service.rb
@@ -8,7 +8,7 @@ module ServiceDesk
def execute
return error_feature_flag_disabled unless Feature.enabled?(:service_desk_custom_email, project)
return error_parameter_missing if settings.blank? || verification.blank?
- return error_already_finished if already_finished_and_no_mail?
+ return error_already_finished if verification.finished?
return error_already_failed if already_failed_and_no_mail?
verification_error = verify
@@ -39,10 +39,6 @@ module ServiceDesk
@verification ||= settings.custom_email_verification
end
- def already_finished_and_no_mail?
- verification.finished? && mail.blank?
- end
-
def already_failed_and_no_mail?
verification.failed? && mail.blank?
end
diff --git a/app/services/service_desk/custom_emails/create_service.rb b/app/services/service_desk/custom_emails/create_service.rb
index 305f5b3fa11..c06c836f0fa 100644
--- a/app/services/service_desk/custom_emails/create_service.rb
+++ b/app/services/service_desk/custom_emails/create_service.rb
@@ -42,6 +42,8 @@ module ServiceDesk
def create_credential
credential = ::ServiceDesk::CustomEmailCredential.new(create_credential_params.merge(project: project))
credential.save
+ rescue ArgumentError
+ false
end
def create_verification
@@ -53,7 +55,7 @@ module ServiceDesk
end
def create_credential_params
- ensure_params.permit(:smtp_address, :smtp_port, :smtp_username, :smtp_password)
+ ensure_params.permit(:smtp_address, :smtp_port, :smtp_username, :smtp_password, :smtp_authentication)
end
def ensure_params
diff --git a/app/services/service_desk_settings/update_service.rb b/app/services/service_desk_settings/update_service.rb
index 182022beb1d..f8b825923f3 100644
--- a/app/services/service_desk_settings/update_service.rb
+++ b/app/services/service_desk_settings/update_service.rb
@@ -9,6 +9,8 @@ module ServiceDeskSettings
params[:project_key] = nil if params[:project_key].blank?
+ apply_feature_flag_restrictions!
+
# We want to know when custom email got enabled
write_log_message = params[:custom_email_enabled].present? && !settings.custom_email_enabled?
@@ -20,5 +22,14 @@ module ServiceDeskSettings
ServiceResponse.error(message: settings.errors.full_messages.to_sentence)
end
end
+
+ private
+
+ def apply_feature_flag_restrictions!
+ return if Feature.enabled?(:issue_email_participants, project)
+ return unless params.include?(:add_external_participants_from_cc)
+
+ params.delete(:add_external_participants_from_cc)
+ end
end
end
diff --git a/app/services/spam/spam_action_service.rb b/app/services/spam/spam_action_service.rb
index 6ec8d09c37c..cca0bb709aa 100644
--- a/app/services/spam/spam_action_service.rb
+++ b/app/services/spam/spam_action_service.rb
@@ -78,14 +78,17 @@ module Spam
when BLOCK_USER
target.spam!
create_spam_log
+ create_spam_abuse_event(result)
ban_user!
when DISALLOW
target.spam!
create_spam_log
+ create_spam_abuse_event(result)
when CONDITIONAL_ALLOW
# This means "require a CAPTCHA to be solved"
target.needs_recaptcha!
create_spam_log
+ create_spam_abuse_event(result)
when OVERRIDE_VIA_ALLOW_POSSIBLE_SPAM
create_spam_log
when ALLOW
@@ -118,6 +121,22 @@ module Spam
target.spam_log = spam_log
end
+ def create_spam_abuse_event(result)
+ params = {
+ user_id: user.id,
+ title: target.spam_title,
+ description: target.spam_description,
+ source_ip: spam_params&.ip_address,
+ user_agent: spam_params&.user_agent,
+ noteable_type: noteable_type,
+ verdict: result
+ }
+
+ target.run_after_commit_or_now do
+ Abuse::SpamAbuseEventsWorker.perform_async(params)
+ end
+ end
+
def ban_user!
UserCustomAttribute.set_banned_by_spam_log(target.spam_log)
diff --git a/app/services/system_notes/issuables_service.rb b/app/services/system_notes/issuables_service.rb
index 8442ff81d41..c584d5ccca3 100644
--- a/app/services/system_notes/issuables_service.rb
+++ b/app/services/system_notes/issuables_service.rb
@@ -437,7 +437,7 @@ module SystemNotes
def discussion_lock
action = noteable.discussion_locked? ? 'locked' : 'unlocked'
- body = "#{action} this #{noteable.class.to_s.titleize.downcase}"
+ body = "#{action} the discussion in this #{noteable.class.to_s.titleize.downcase}"
if action == 'locked'
track_issue_event(:track_issue_locked_action)
diff --git a/app/services/users/refresh_authorized_projects_service.rb b/app/services/users/refresh_authorized_projects_service.rb
index 32acc3f170d..6ec87df9f76 100644
--- a/app/services/users/refresh_authorized_projects_service.rb
+++ b/app/services/users/refresh_authorized_projects_service.rb
@@ -72,6 +72,8 @@ module Users
changes.remove_projects_for_user(user, remove)
end.apply!
+ user.update!(project_authorizations_recalculated_at: Time.zone.now) if remove.any? || add.any?
+
# Since we batch insert authorization rows, Rails' associations may get
# out of sync. As such we force a reload of the User object.
user.reset
diff --git a/app/services/users/upsert_credit_card_validation_service.rb b/app/services/users/upsert_credit_card_validation_service.rb
index 62df676db25..e0f81971944 100644
--- a/app/services/users/upsert_credit_card_validation_service.rb
+++ b/app/services/users/upsert_credit_card_validation_service.rb
@@ -2,41 +2,68 @@
module Users
class UpsertCreditCardValidationService < BaseService
+ attr_reader :params
+
def initialize(params)
@params = params.to_h.with_indifferent_access
end
def execute
- user_id = params.fetch(:user_id)
-
- @params = {
- user_id: user_id,
- credit_card_validated_at: params.fetch(:credit_card_validated_at),
- expiration_date: get_expiration_date(params),
- last_digits: Integer(params.fetch(:credit_card_mask_number), 10),
- network: params.fetch(:credit_card_type),
- holder_name: params.fetch(:credit_card_holder_name)
- }
-
credit_card = Users::CreditCardValidation.find_or_initialize_by_user(user_id)
- credit_card.update(@params.except(:user_id))
+ credit_card_params = {
+ credit_card_validated_at: credit_card_validated_at,
+ last_digits: last_digits,
+ holder_name: holder_name,
+ network: network,
+ expiration_date: expiration_date
+ }
+
+ credit_card.update(credit_card_params)
- ServiceResponse.success(message: 'CreditCardValidation was set')
- rescue ActiveRecord::InvalidForeignKey, ActiveRecord::NotNullViolation => e
- ServiceResponse.error(message: "Could not set CreditCardValidation: #{e.message}")
+ success
+ rescue ActiveRecord::InvalidForeignKey, ActiveRecord::NotNullViolation
+ error
rescue StandardError => e
- Gitlab::ErrorTracking.track_exception(e, params: @params, class: self.class.to_s)
- ServiceResponse.error(message: "Could not set CreditCardValidation: #{e.message}")
+ Gitlab::ErrorTracking.track_exception(e)
+ error
end
private
- def get_expiration_date(params)
+ def user_id
+ params.fetch(:user_id)
+ end
+
+ def credit_card_validated_at
+ params.fetch(:credit_card_validated_at)
+ end
+
+ def last_digits
+ Integer(params.fetch(:credit_card_mask_number), 10)
+ end
+
+ def holder_name
+ params.fetch(:credit_card_holder_name)
+ end
+
+ def network
+ params.fetch(:credit_card_type)
+ end
+
+ def expiration_date
year = params.fetch(:credit_card_expiration_year)
month = params.fetch(:credit_card_expiration_month)
Date.new(year, month, -1) # last day of the month
end
+
+ def success
+ ServiceResponse.success(message: _('Credit card validation record saved'))
+ end
+
+ def error
+ ServiceResponse.error(message: _('Error saving credit card validation record'))
+ end
end
end
diff --git a/app/services/verify_pages_domain_service.rb b/app/services/verify_pages_domain_service.rb
index 59c73aa929c..f5dfe13539b 100644
--- a/app/services/verify_pages_domain_service.rb
+++ b/app/services/verify_pages_domain_service.rb
@@ -79,7 +79,7 @@ class VerifyPagesDomainService < BaseService
# A domain is only expired until `disable!` has been called
def expired?
- domain.enabled_until && domain.enabled_until < Time.current
+ domain.enabled_until&.past?
end
def dns_record_present?
diff --git a/app/services/vs_code/settings/delete_service.rb b/app/services/vs_code/settings/delete_service.rb
new file mode 100644
index 00000000000..a2edd734eb2
--- /dev/null
+++ b/app/services/vs_code/settings/delete_service.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module VsCode
+ module Settings
+ class DeleteService
+ def initialize(current_user:)
+ @current_user = current_user
+ end
+
+ def execute
+ VsCodeSetting.by_user(current_user).delete_all
+
+ ServiceResponse.success
+ end
+
+ private
+
+ attr_reader :current_user
+ end
+ end
+end
diff --git a/app/services/web_hook_service.rb b/app/services/web_hook_service.rb
index 27b29feed50..035f1754cbb 100644
--- a/app/services/web_hook_service.rb
+++ b/app/services/web_hook_service.rb
@@ -83,13 +83,13 @@ class WebHookService
log_execution(
response: response,
- execution_duration: Gitlab::Metrics::System.monotonic_time - start_time
+ execution_duration: ::Gitlab::Metrics::System.monotonic_time - start_time
)
ServiceResponse.success(message: response.body, payload: { http_status: response.code })
rescue *Gitlab::HTTP::HTTP_ERRORS,
Gitlab::Json::LimitedEncoder::LimitExceeded, URI::InvalidURIError => e
- execution_duration = Gitlab::Metrics::System.monotonic_time - start_time
+ execution_duration = ::Gitlab::Metrics::System.monotonic_time - start_time
error_message = e.to_s
log_execution(
@@ -110,10 +110,10 @@ class WebHookService
break log_recursion_blocked if recursion_blocked?
params = {
- recursion_detection_request_uuid: Gitlab::WebHooks::RecursionDetection::UUID.instance.request_uuid
+ "recursion_detection_request_uuid" => Gitlab::WebHooks::RecursionDetection::UUID.instance.request_uuid
}.compact
- WebHookWorker.perform_async(hook.id, data, hook_name, params)
+ WebHookWorker.perform_async(hook.id, data.deep_stringify_keys, hook_name.to_s, params)
end
end
@@ -170,7 +170,9 @@ class WebHookService
def queue_log_execution_with_retry(log_data, category)
retried = false
begin
- ::WebHooks::LogExecutionWorker.perform_async(hook.id, log_data, category, uniqueness_token)
+ ::WebHooks::LogExecutionWorker.perform_async(
+ hook.id, log_data.deep_stringify_keys, category.to_s, uniqueness_token.to_s
+ )
rescue Gitlab::SidekiqMiddleware::SizeLimiter::ExceedLimitError
raise if retried
diff --git a/app/validators/ip_cidr_array_validator.rb b/app/validators/ip_cidr_array_validator.rb
new file mode 100644
index 00000000000..fff1368508f
--- /dev/null
+++ b/app/validators/ip_cidr_array_validator.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+# IpCidrArrayValidator
+#
+# Validates that an array of IP are a valid IPv4 or IPv6 CIDR address.
+#
+# Example:
+#
+# class Group < ActiveRecord::Base
+# validates :ip_array, presence: true, ip_cidr_array: true
+# end
+
+class IpCidrArrayValidator < ActiveModel::EachValidator # rubocop:disable Gitlab/NamespacedClass -- This is a globally shareable validator, but it's unclear what namespace it should belong in
+ def validate_each(record, attribute, value)
+ unless value.is_a?(Array)
+ record.errors.add(attribute, _("must be an array of CIDR values"))
+ return
+ end
+
+ value.each do |cidr|
+ single_validator = IpCidrValidator.new(attributes: attribute)
+ single_validator.validate_each(record, attribute, cidr)
+ end
+ end
+end
diff --git a/app/validators/ip_cidr_validator.rb b/app/validators/ip_cidr_validator.rb
new file mode 100644
index 00000000000..b1760a99d6d
--- /dev/null
+++ b/app/validators/ip_cidr_validator.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+# IpCidrValidator
+#
+# Validates that an IP is a valid IPv4 or IPv6 CIDR address.
+#
+# Example:
+#
+# class Group < ActiveRecord::Base
+# validates :ip, presence: true, ip_cidr: true
+# end
+
+class IpCidrValidator < ActiveModel::EachValidator # rubocop:disable Gitlab/NamespacedClass -- This is a globally shareable validator, but it's unclear what namespace it should belong in
+ def validate_each(record, attribute, value)
+ # NOTE: We want this to be usable for nullable fields, so we don't validate presence.
+ # Use a separate `presence` validation for the field if needed.
+ return true if value.blank?
+
+ # rubocop:disable Layout/LineLength -- The error message is bigger than the line limit
+ unless valid_cidr_format?(value)
+ record.errors.add(
+ attribute,
+ format(_(
+ "IP '%{value}' is not a valid CIDR: IP should be followed by a slash followed by an integer subnet mask (for example: '192.168.1.0/24')"),
+ value: value
+ )
+ )
+ return
+ end
+ # rubocop:enable Layout/LineLength
+
+ IPAddress.parse(value)
+ rescue ArgumentError => e
+ record.errors.add(
+ attribute,
+ format(_("IP '%{value}' is not a valid CIDR: %{message}"), value: value, message: e.message)
+ )
+ end
+
+ private
+
+ def valid_cidr_format?(cidr)
+ cidr.count('/') == 1 && cidr.split('/').last =~ /^\d+$/
+ end
+end
diff --git a/app/validators/json_schemas/activity_pub_follow_payload.json b/app/validators/json_schemas/activity_pub_follow_payload.json
new file mode 100644
index 00000000000..1f453ce840f
--- /dev/null
+++ b/app/validators/json_schemas/activity_pub_follow_payload.json
@@ -0,0 +1,53 @@
+{
+ "description": "ActivityPub Follow activity payload",
+ "type": "object",
+ "required": [
+ "@context",
+ "id",
+ "type",
+ "actor",
+ "object"
+ ],
+ "properties": {
+ "@context": {
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "array"
+ }
+ ]
+ },
+ "id": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string"
+ },
+ "actor": {
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "id": {
+ "type": "string"
+ },
+ "inbox": {
+ "type": "string"
+ },
+ "additionalProperties": true
+ }
+ ]
+ },
+ "object": {
+ "type": "string"
+ },
+ "additionalProperties": true
+ }
+}
diff --git a/app/validators/json_schemas/vulnerability_cvss_vectors.json b/app/validators/json_schemas/vulnerability_cvss_vectors.json
index 7ec1339e974..0da6de0a69d 100644
--- a/app/validators/json_schemas/vulnerability_cvss_vectors.json
+++ b/app/validators/json_schemas/vulnerability_cvss_vectors.json
@@ -9,14 +9,14 @@
"type": "string",
"default": "unknown"
},
- "vector_string": {
+ "vector": {
"type": "string",
"example": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H"
}
},
"required": [
"vendor",
- "vector_string"
+ "vector"
]
}
}
diff --git a/app/views/admin/abuse_reports/show.html.haml b/app/views/admin/abuse_reports/show.html.haml
index bd7a1054b5d..ff9ac6a052c 100644
--- a/app/views/admin/abuse_reports/show.html.haml
+++ b/app/views/admin/abuse_reports/show.html.haml
@@ -1,5 +1,6 @@
- add_to_breadcrumbs _('Abuse Reports'), admin_abuse_reports_path
- breadcrumb_title @abuse_report.user&.name
+- @content_class = "limit-container-width" unless fluid_layout
- page_title @abuse_report.user&.name, _('Abuse Reports')
#js-abuse-reports-detail-view{ data: abuse_report_data(@abuse_report) }
diff --git a/app/views/admin/application_settings/_account_and_limit.html.haml b/app/views/admin/application_settings/_account_and_limit.html.haml
index 4e55c99e445..1d58b0106c4 100644
--- a/app/views/admin/application_settings/_account_and_limit.html.haml
+++ b/app/views/admin/application_settings/_account_and_limit.html.haml
@@ -24,12 +24,13 @@
%span.form-text.text-muted#session_expire_delay_help_block= _('Restart GitLab to apply changes.')
.form-group
= f.label :remember_me_enabled, _('Remember me'), class: 'label-light'
- - remember_me_help_link = help_page_path('user/profile/index.md', anchor: 'stay-signed-in-for-two-weeks')
+ - remember_me_help_link = help_page_path('user/profile/index', anchor: 'stay-signed-in-for-two-weeks')
- remember_me_help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: remember_me_help_link }
= f.gitlab_ui_checkbox_component :remember_me_enabled, _('Allow users to extend their session'), help_text: _("Users can select 'Remember me' on sign-in to keep their session active beyond the session duration. %{link_start}Learn more.%{link_end}").html_safe % { link_start: remember_me_help_link_start, link_end: '</a>'.html_safe }
= render_if_exists 'admin/application_settings/git_two_factor_session_expiry', form: f
= render_if_exists 'admin/application_settings/personal_access_token_expiration_policy', form: f
+ = render_if_exists 'admin/application_settings/service_access_tokens_expiration_enforced', form: f
= render_if_exists 'admin/application_settings/ssh_key_expiration_policy', form: f
.form-group
diff --git a/app/views/admin/application_settings/_ci_cd.html.haml b/app/views/admin/application_settings/_ci_cd.html.haml
index c08270a8522..8092299fb61 100644
--- a/app/views/admin/application_settings/_ci_cd.html.haml
+++ b/app/views/admin/application_settings/_ci_cd.html.haml
@@ -4,7 +4,7 @@
%fieldset
.form-group
- - devops_help_link_url = help_page_path('topics/autodevops/index.md')
+ - devops_help_link_url = help_page_path('topics/autodevops/index')
- devops_help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: devops_help_link_url }
= f.gitlab_ui_checkbox_component :auto_devops_enabled, s_('CICD|Default to Auto DevOps pipeline for all projects'), help_text: s_('CICD|The Auto DevOps pipeline runs by default in all projects with no CI/CD configuration file. %{link_start}What is Auto DevOps?%{link_end}').html_safe % { link_start: devops_help_link_start, link_end: '</a>'.html_safe }
.form-group
@@ -12,7 +12,7 @@
= f.text_field :auto_devops_domain, class: 'form-control gl-form-input', placeholder: 'example.com'
.form-text.text-muted
= s_("AdminSettings|The default domain to use for Auto Review Apps and Auto Deploy stages in all projects.")
- = link_to _('Learn more.'), help_page_path('topics/autodevops/stages.md', anchor: 'auto-review-apps'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('topics/autodevops/stages', anchor: 'auto-review-apps'), target: '_blank', rel: 'noopener noreferrer'
.form-group
= f.gitlab_ui_checkbox_component :shared_runners_enabled, s_("AdminSettings|Enable shared runners for new projects"), help_text: s_("AdminSettings|All new projects can use the instance's shared runners by default.")
@@ -59,6 +59,8 @@
.form-group
= f.gitlab_ui_checkbox_component :suggest_pipeline_enabled, s_('AdminSettings|Enable pipeline suggestion banner'), help_text: s_('AdminSettings|Display a banner on merge requests in projects with no pipelines to initiate steps to add a .gitlab-ci.yml file.')
#js-runner-token-expiration-intervals{ data: runner_token_expiration_interval_attributes }
+ .form-group
+ = f.gitlab_ui_checkbox_component :enable_artifact_external_redirect_warning_page, s_('AdminSettings|Enable the external redirect warning page for job artifacts'), help_text: s_('AdminSettings|Show a redirect page that warns you about user-generated content in GitLab Pages.')
= f.submit _('Save changes'), pajamas_button: true
diff --git a/app/views/admin/application_settings/_diagramsnet.html.haml b/app/views/admin/application_settings/_diagramsnet.html.haml
index 0cf44938881..0d44b38b0e0 100644
--- a/app/views/admin/application_settings/_diagramsnet.html.haml
+++ b/app/views/admin/application_settings/_diagramsnet.html.haml
@@ -7,7 +7,7 @@
= expanded ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= _('Render diagrams in your documents using diagrams.net.')
- = link_to _('Learn more.'), help_page_path('administration/integration/diagrams_net.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('administration/integration/diagrams_net'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= gitlab_ui_form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-diagramsnet-settings'), html: { class: 'fieldset-form', id: 'diagramsnet-settings' } do |f|
= form_errors(@application_setting) if expanded
diff --git a/app/views/admin/application_settings/_email.html.haml b/app/views/admin/application_settings/_email.html.haml
index 2d45391a839..a9bc8ab9d32 100644
--- a/app/views/admin/application_settings/_email.html.haml
+++ b/app/views/admin/application_settings/_email.html.haml
@@ -10,7 +10,7 @@
= f.label :commit_email_hostname, _('Custom hostname (for private commit emails)'), class: 'label-bold'
= f.text_field :commit_email_hostname, class: 'form-control gl-form-input'
.form-text.text-muted
- - commit_email_hostname_docs_link = link_to _('Learn more'), help_page_path('administration/settings/email.md', anchor: 'custom-hostname-for-private-commit-emails'), target: '_blank', rel: 'noopener noreferrer'
+ - commit_email_hostname_docs_link = link_to _('Learn more'), help_page_path('administration/settings/email', anchor: 'custom-hostname-for-private-commit-emails'), target: '_blank', rel: 'noopener noreferrer'
= _("Hostname used in private commit emails. %{learn_more}").html_safe % { learn_more: commit_email_hostname_docs_link }
= render_if_exists 'admin/application_settings/email_additional_text_setting', form: f
diff --git a/app/views/admin/application_settings/_error_tracking.html.haml b/app/views/admin/application_settings/_error_tracking.html.haml
index 6754dd99bbc..ab4ed9917a0 100644
--- a/app/views/admin/application_settings/_error_tracking.html.haml
+++ b/app/views/admin/application_settings/_error_tracking.html.haml
@@ -7,8 +7,8 @@
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded ? _('Collapse') : _('Expand')
%p.gl-text-secondary
- = _('Allows projects to track errors using an Opstrace integration.').html_safe % { link: help_page_path('operations/error_tracking.md') }
- = link_to _('Learn more.'), help_page_path('operations/error_tracking.md'), target: '_blank', rel: 'noopener noreferrer'
+ = _('Allows projects to track errors using an Opstrace integration.').html_safe % { link: help_page_path('operations/error_tracking') }
+ = link_to _('Learn more.'), help_page_path('operations/error_tracking'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
diff --git a/app/views/admin/application_settings/_floc.html.haml b/app/views/admin/application_settings/_floc.html.haml
index e1576e84e66..27df417d225 100644
--- a/app/views/admin/application_settings/_floc.html.haml
+++ b/app/views/admin/application_settings/_floc.html.haml
@@ -7,7 +7,7 @@
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded ? _('Collapse') : _('Expand')
%p.gl-text-secondary
- - floc_link_url = help_page_path('administration/settings/floc.md')
+ - floc_link_url = help_page_path('administration/settings/floc')
- floc_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: floc_link_url }
= html_escape(s_('FloC|Configure whether you want to participate in FLoC. %{floc_link_start}What is FLoC?%{floc_link_end}')) % { floc_link_start: floc_link_start, floc_link_end: '</a>'.html_safe }
diff --git a/app/views/admin/application_settings/_gitlab_shell_operation_limits.html.haml b/app/views/admin/application_settings/_gitlab_shell_operation_limits.html.haml
index 64549b97bd1..22372146ea1 100644
--- a/app/views/admin/application_settings/_gitlab_shell_operation_limits.html.haml
+++ b/app/views/admin/application_settings/_gitlab_shell_operation_limits.html.haml
@@ -6,7 +6,7 @@
= expanded_by_default? ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= s_('ShellOperations|Limit the number of Git operations a user can perform per minute, per repository.')
- = link_to _('Learn more.'), help_page_path('administration/settings/rate_limits_on_git_ssh_operations.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('administration/settings/rate_limits_on_git_ssh_operations'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= gitlab_ui_form_for @application_setting, url: network_admin_application_settings_path(anchor: 'js-gitlab-shell-operation-limits-settings'), html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
@@ -15,5 +15,5 @@
.form-group
= f.label :gitlab_shell_operation_limit, s_('ShellOperations|Maximum number of Git operations per minute'), class: 'gl-font-bold'
= f.number_field :gitlab_shell_operation_limit, class: 'form-control gl-form-input'
-
+ %span.form-text.text-muted= _('Set to 0 to disable the limit.')
= f.submit _('Save changes'), pajamas_button: true
diff --git a/app/views/admin/application_settings/_gitpod.html.haml b/app/views/admin/application_settings/_gitpod.html.haml
index 1f56487cea4..ce8c390baa5 100644
--- a/app/views/admin/application_settings/_gitpod.html.haml
+++ b/app/views/admin/application_settings/_gitpod.html.haml
@@ -8,7 +8,7 @@
= expanded ? _('Collapse') : _('Expand')
.gl-text-secondary.gl-mb-5
#js-gitpod-settings-help-text{ data: {"message" => gitpod_enable_description, "message-url" => "https://gitpod.io/" } }
- = link_to sprite_icon('question-o'), help_page_path('integration/gitpod.md'), target: '_blank', class: 'has-tooltip', title: _('More information')
+ = link_to sprite_icon('question-o'), help_page_path('integration/gitpod'), target: '_blank', class: 'has-tooltip', title: _('More information')
.settings-content
= gitlab_ui_form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-gitpod-settings'), html: { class: 'fieldset-form', id: 'gitpod-settings' } do |f|
diff --git a/app/views/admin/application_settings/_import_export_limits.html.haml b/app/views/admin/application_settings/_import_export_limits.html.haml
index 8cb7915f847..269a1497324 100644
--- a/app/views/admin/application_settings/_import_export_limits.html.haml
+++ b/app/views/admin/application_settings/_import_export_limits.html.haml
@@ -2,8 +2,7 @@
= form_errors(@application_setting)
%fieldset
- = html_escape(_("Set any rate limit to %{code_open}0%{code_close} to disable the limit.")) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
-
+ = html_escape(_("Set to 0 to disable the limits."))
%fieldset
.form-group
diff --git a/app/views/admin/application_settings/_kroki.html.haml b/app/views/admin/application_settings/_kroki.html.haml
index 4dbca235a73..0f1316996fa 100644
--- a/app/views/admin/application_settings/_kroki.html.haml
+++ b/app/views/admin/application_settings/_kroki.html.haml
@@ -7,7 +7,7 @@
= expanded ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= _('Users can render diagrams in AsciiDoc, Markdown, reStructuredText, and Textile documents using Kroki.')
- = link_to _('Learn more.'), help_page_path('administration/integration/kroki.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('administration/integration/kroki'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= gitlab_ui_form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-kroki-settings'), html: { class: 'fieldset-form', id: 'kroki-settings' } do |f|
= form_errors(@application_setting) if expanded
diff --git a/app/views/admin/application_settings/_localization.html.haml b/app/views/admin/application_settings/_localization.html.haml
index 25038e6f221..62849a81633 100644
--- a/app/views/admin/application_settings/_localization.html.haml
+++ b/app/views/admin/application_settings/_localization.html.haml
@@ -7,11 +7,11 @@
= f.select :first_day_of_week, first_day_of_week_choices, {}, class: 'form-control'
.form-text.text-muted
= _('Default first day of the week in calendars and date pickers.')
- = link_to _('Learn more.'), help_page_path('administration/settings/localization.md', anchor: 'change-the-default-first-day-of-the-week'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('administration/settings/localization', anchor: 'change-the-default-first-day-of-the-week'), target: '_blank', rel: 'noopener noreferrer'
.form-group
= f.label :time_tracking, _('Time tracking'), class: 'label-bold'
- - time_tracking_help_link = help_page_path('user/project/time_tracking.md')
+ - time_tracking_help_link = help_page_path('user/project/time_tracking')
- time_tracking_help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: time_tracking_help_link }
= f.gitlab_ui_checkbox_component :time_tracking_limit_to_hours, _('Limit display of time tracking units to hours.'), help_text: _('Display time tracking in issues in total hours only. %{link_start}What is time tracking?%{link_end}').html_safe % { link_start: time_tracking_help_link_start, link_end: '</a>'.html_safe }
diff --git a/app/views/admin/application_settings/_outbound.html.haml b/app/views/admin/application_settings/_outbound.html.haml
index f36fbd8d68c..a4ec3a31584 100644
--- a/app/views/admin/application_settings/_outbound.html.haml
+++ b/app/views/admin/application_settings/_outbound.html.haml
@@ -26,7 +26,7 @@
= f.text_area :outbound_local_requests_allowlist_raw, placeholder: "example.com, 192.168.1.1, xn--itlab-j1a.com", class: 'form-control gl-form-input', rows: 8
%span.form-text.text-muted
= s_('OutboundRequests|Requests can be made to these IP addresses and domains even when local requests are not allowed. IP ranges such as %{code_start}1:0:0:0:0:0:0:0/124%{code_end} and %{code_start}127.0.0.0/28%{code_end} are supported. Domain wildcards are not supported. To separate entries, use commas, semicolons, or newlines. The allowlist can have a maximum of 1000 entries. Domains must be IDNA-encoded.').html_safe % { code_start: '<code>'.html_safe, code_end: '</code>'.html_safe }
- = link_to _('Learn more.'), help_page_path('security/webhooks.md', anchor: 'allow-outbound-requests-to-certain-ip-addresses-and-domains'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('security/webhooks', anchor: 'allow-outbound-requests-to-certain-ip-addresses-and-domains'), target: '_blank', rel: 'noopener noreferrer'
.form-group
= f.gitlab_ui_checkbox_component :dns_rebinding_protection_enabled,
diff --git a/app/views/admin/application_settings/_performance.html.haml b/app/views/admin/application_settings/_performance.html.haml
index bfa548b70e5..14c785509bd 100644
--- a/app/views/admin/application_settings/_performance.html.haml
+++ b/app/views/admin/application_settings/_performance.html.haml
@@ -11,16 +11,16 @@
= f.label :raw_blob_request_limit, _('Raw blob request rate limit per minute'), class: 'label-bold'
= f.number_field :raw_blob_request_limit, class: 'form-control gl-form-input'
.form-text.text-muted
- = _('Maximum number of requests per minute for each raw path (default is `300`). Set to `0` to disable throttling.')
+ = _('Maximum number of requests per minute for each raw path (default is 300). Set to 0 to disable throttling.')
.form-group
= f.label :push_event_hooks_limit, class: 'label-bold'
= f.number_field :push_event_hooks_limit, class: 'form-control gl-form-input'
.form-text.text-muted
- = _('Maximum number of changes (branches or tags) in a single push above which webhooks and integrations are not triggered (default is `3`). Setting to `0` does not disable throttling.')
+ = _('Maximum number of changes (branches or tags) in a single push above which webhooks and integrations are not triggered (default is 3). Setting to 0 does not disable throttling.')
.form-group
= f.label :push_event_activities_limit, class: 'label-bold'
= f.number_field :push_event_activities_limit, class: 'form-control gl-form-input'
.form-text.text-muted
- = _('Maximum number of changes (branches or tags) in a single push above which a bulk push event is created (default is `3`). Setting to `0` does not disable throttling.')
+ = _('Maximum number of changes (branches or tags) in a single push above which a bulk push event is created (default is 3). Setting to 0 does not disable throttling.')
= f.submit _('Save changes'), pajamas_button: true
diff --git a/app/views/admin/application_settings/_plantuml.html.haml b/app/views/admin/application_settings/_plantuml.html.haml
index a8b758f7324..c673bf72397 100644
--- a/app/views/admin/application_settings/_plantuml.html.haml
+++ b/app/views/admin/application_settings/_plantuml.html.haml
@@ -7,7 +7,7 @@
= expanded ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= _('Render diagrams in your documents using PlantUML.')
- = link_to _('Learn more.'), help_page_path('administration/integration/plantuml.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('administration/integration/plantuml'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= gitlab_ui_form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-plantuml-settings'), html: { class: 'fieldset-form', id: 'plantuml-settings' } do |f|
= form_errors(@application_setting) if expanded
diff --git a/app/views/admin/application_settings/_projects_api_limits.html.haml b/app/views/admin/application_settings/_projects_api_limits.html.haml
index dde8ab07958..c9eff76916a 100644
--- a/app/views/admin/application_settings/_projects_api_limits.html.haml
+++ b/app/views/admin/application_settings/_projects_api_limits.html.haml
@@ -6,7 +6,7 @@
= expanded_by_default? ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= _('Set the per-IP address rate limit applicable to unauthenticated requests for getting a list of projects via the API.')
- = link_to _('Learn more.'), help_page_path('administration/settings/rate_limit_on_projects_api.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('administration/settings/rate_limit_on_projects_api'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= gitlab_ui_form_for @application_setting, url: network_admin_application_settings_path(anchor: 'js-projects-api-limits-settings'), html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
@@ -16,6 +16,6 @@
= f.label :projects_api_rate_limit_unauthenticated, _('Maximum requests per 10 minutes per IP address'), class: 'label-bold'
= f.number_field :projects_api_rate_limit_unauthenticated, class: 'form-control gl-form-input'
.form-text.gl-text-gray-600
- = _("Set this number to 0 to disable the limit.")
+ = _("Set to 0 to disable the limit.")
= f.submit _('Save changes'), data: { qa_selector: 'save_changes_button' }, pajamas_button: true
diff --git a/app/views/admin/application_settings/_repository_check.html.haml b/app/views/admin/application_settings/_repository_check.html.haml
index 5751ae9059a..cb1a0a40566 100644
--- a/app/views/admin/application_settings/_repository_check.html.haml
+++ b/app/views/admin/application_settings/_repository_check.html.haml
@@ -21,7 +21,7 @@
%h4= _("Housekeeping")
.form-group
- help_text = _("Run housekeeping tasks to automatically optimize Git repositories. Disabling this option will cause performance to degenerate over time.")
- - help_link = link_to _('Learn more.'), help_page_path('administration/housekeeping.md', anchor: 'heuristical-housekeeping'), target: '_blank', rel: 'noopener noreferrer'
+ - help_link = link_to _('Learn more.'), help_page_path('administration/housekeeping', anchor: 'heuristical-housekeeping'), target: '_blank', rel: 'noopener noreferrer'
= f.gitlab_ui_checkbox_component :housekeeping_enabled,
_("Enable automatic repository housekeeping"),
help_text: '%{help_text} %{help_link}'.html_safe % { help_text: help_text, help_link: help_link }
diff --git a/app/views/admin/application_settings/_repository_storage.html.haml b/app/views/admin/application_settings/_repository_storage.html.haml
index 066d77c792b..412098cfae4 100644
--- a/app/views/admin/application_settings/_repository_storage.html.haml
+++ b/app/views/admin/application_settings/_repository_storage.html.haml
@@ -5,7 +5,7 @@
.sub-section
%h4= _('Hashed repository storage paths')
.form-group
- - repository_storage_help_link_url = help_page_path('administration/repository_storage_types.md')
+ - repository_storage_help_link_url = help_page_path('administration/repository_storage_types')
- repository_storage_help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: repository_storage_help_link_url }
= f.gitlab_ui_checkbox_component :hashed_storage_enabled,
_('Use hashed storage'),
@@ -17,10 +17,10 @@
.form-group
.form-text
%p.text-secondary
- - weights_link_url = help_page_path('administration/repository_storage_paths.md', anchor: 'configure-where-new-repositories-are-stored')
+ - weights_link_url = help_page_path('administration/repository_storage_paths', anchor: 'configure-where-new-repositories-are-stored')
- weights_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: weights_link_url }
= html_escape(s_('Enter %{weights_link_start}weights%{weights_link_end} for storages for new repositories. Configured storages appear below.')) % { weights_link_start: weights_link_start, weights_link_end: '</a>'.html_safe }
- = link_to _('Learn more.'), help_page_path('administration/repository_storage_paths.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('administration/repository_storage_paths'), target: '_blank', rel: 'noopener noreferrer'
.form-check
= f.fields_for :repository_storages_weighted, storage_weights do |storage_form|
- Gitlab.config.repositories.storages.each_key do |storage|
diff --git a/app/views/admin/application_settings/_runner_registrars_form.html.haml b/app/views/admin/application_settings/_runner_registrars_form.html.haml
index b112c273aad..36bab2f6650 100644
--- a/app/views/admin/application_settings/_runner_registrars_form.html.haml
+++ b/app/views/admin/application_settings/_runner_registrars_form.html.haml
@@ -7,7 +7,7 @@
= s_('Runners|Runner version management')
%span.form-text.gl-mb-3.gl-mt-0
- help_text = s_('Runners|Official runner version data is periodically fetched from GitLab.com to determine whether the runners need upgrades.')
- - learn_more_link = link_to _('Learn more.'), help_page_path('ci/runners/runners_scope.md', anchor: 'determine-which-runners-need-to-be-upgraded'), target: '_blank', rel: 'noopener noreferrer'
+ - learn_more_link = link_to _('Learn more.'), help_page_path('ci/runners/runners_scope', anchor: 'determine-which-runners-need-to-be-upgraded'), target: '_blank', rel: 'noopener noreferrer'
= f.gitlab_ui_checkbox_component :update_runner_versions_enabled,
s_('Runners|Fetch GitLab Runner release version data from GitLab.com'),
help_text: '%{help_text} %{learn_more_link}'.html_safe % { help_text: help_text, learn_more_link: learn_more_link }
diff --git a/app/views/admin/application_settings/_signin.html.haml b/app/views/admin/application_settings/_signin.html.haml
index 5518122b5cf..0f20864fc68 100644
--- a/app/views/admin/application_settings/_signin.html.haml
+++ b/app/views/admin/application_settings/_signin.html.haml
@@ -19,7 +19,7 @@
.form-group
= f.label :two_factor_authentication, _('Two-factor authentication'), class: 'label-bold'
- help_text = _('Enforce two-factor authentication for all user sign-ins.')
- - help_link = link_to _('Learn more.'), help_page_path('security/two_factor_authentication.md'), target: '_blank', rel: 'noopener noreferrer'
+ - help_link = link_to _('Learn more.'), help_page_path('security/two_factor_authentication'), target: '_blank', rel: 'noopener noreferrer'
= f.gitlab_ui_checkbox_component :require_two_factor_authentication,
_('Enforce two-factor authentication'),
help_text: '%{help_text} %{help_link}'.html_safe % { help_text: help_text, help_link: help_link }
@@ -39,7 +39,7 @@
.form-group
= f.label :unknown_sign_in, _('Email notification for unknown sign-ins'), class: 'label-bold'
- help_text = _('Notify users by email when sign-in location is not recognized.')
- - help_link = link_to _('Learn more.'), help_page_path('user/profile/notifications.md', anchor: 'notifications-for-unknown-sign-ins'), target: '_blank', rel: 'noopener noreferrer'
+ - help_link = link_to _('Learn more.'), help_page_path('user/profile/notifications', anchor: 'notifications-for-unknown-sign-ins'), target: '_blank', rel: 'noopener noreferrer'
= f.gitlab_ui_checkbox_component :notify_on_unknown_sign_in,
_('Enable email notification'),
help_text: '%{help_text} %{help_link}'.html_safe % { help_text: help_text, help_link: help_link }
diff --git a/app/views/admin/application_settings/_sourcegraph.html.haml b/app/views/admin/application_settings/_sourcegraph.html.haml
index 61ec841bb83..e61947e3cff 100644
--- a/app/views/admin/application_settings/_sourcegraph.html.haml
+++ b/app/views/admin/application_settings/_sourcegraph.html.haml
@@ -12,7 +12,7 @@
- link_end = "#{sprite_icon('external-link', size: 12, css_class: 'ml-1 vertical-align-center')}</a>".html_safe
= s_('SourcegraphAdmin|Enable code intelligence powered by %{link_start}Sourcegraph%{link_end} on your GitLab instance\'s code views and merge requests.').html_safe % { link_start: link_start, link_end: link_end }
%span
- = link_to s_('SourcegraphAdmin|Learn more.'), help_page_path('integration/sourcegraph.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to s_('SourcegraphAdmin|Learn more.'), help_page_path('integration/sourcegraph'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
diff --git a/app/views/admin/application_settings/_spam.html.haml b/app/views/admin/application_settings/_spam.html.haml
index abc7abe92ad..4e21717a4e6 100644
--- a/app/views/admin/application_settings/_spam.html.haml
+++ b/app/views/admin/application_settings/_spam.html.haml
@@ -8,7 +8,7 @@
= _('reCAPTCHA helps prevent credential stuffing.')
= link_to _('Only reCAPTCHA v2 is supported:'), 'https://developers.google.com/recaptcha/docs/versions', target: '_blank', rel: 'noopener noreferrer'
.form-group
- - spam_help_link_url = help_page_path('integration/recaptcha.md')
+ - spam_help_link_url = help_page_path('integration/recaptcha')
- spam_help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: spam_help_link_url }
= f.gitlab_ui_checkbox_component :recaptcha_enabled, _("Enable reCAPTCHA"),
help_text: _('Helps prevent bots from creating accounts. %{link_start}How do I configure it?%{link_end}').html_safe % { link_start: spam_help_link_start, link_end: '</a>'.html_safe }
@@ -40,7 +40,7 @@
= _('Akismet')
%p
= _('Akismet helps prevent the creation of spam issues in public projects.')
- = link_to _('How do I configure Akismet?'), help_page_path('integration/akismet.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('How do I configure Akismet?'), help_page_path('integration/akismet'), target: '_blank', rel: 'noopener noreferrer'
.form-group
= f.gitlab_ui_checkbox_component :akismet_enabled, _('Enable Akismet'),
diff --git a/app/views/admin/application_settings/_terms.html.haml b/app/views/admin/application_settings/_terms.html.haml
index 8da441d5245..2afcb26b43b 100644
--- a/app/views/admin/application_settings/_terms.html.haml
+++ b/app/views/admin/application_settings/_terms.html.haml
@@ -10,5 +10,5 @@
= f.text_area :terms, class: 'form-control gl-form-input', rows: 8
.form-text.text-muted
= _("Markdown supported.")
- = link_to _('What is Markdown?'), help_page_path('user/markdown.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('What is Markdown?'), help_page_path('user/markdown'), target: '_blank', rel: 'noopener noreferrer'
= f.submit _("Save changes"), pajamas_button: true
diff --git a/app/views/admin/application_settings/_usage.html.haml b/app/views/admin/application_settings/_usage.html.haml
index 2d51dc2a6f2..dd9820d064a 100644
--- a/app/views/admin/application_settings/_usage.html.haml
+++ b/app/views/admin/application_settings/_usage.html.haml
@@ -13,18 +13,30 @@
.form-group
- can_be_configured = @application_setting.usage_ping_can_be_configured?
- service_ping_link_start = link_start % { url: help_page_path('development/internal_analytics/service_ping/index') }
- - deactivating_service_ping_link_start = link_start % { url: help_page_path('administration/settings/usage_statistics', anchor: 'disable-usage-statistics-with-the-configuration-file') }
+ - deactivating_service_ping_link_start = link_start % { url: help_page_path('administration/settings/usage_statistics', anchor: 'through-the-configuration-file') }
- usage_ping_help_text = s_('AdminSettings|To help improve GitLab and its user experience, GitLab periodically collects usage information. %{link_start}What information is shared with GitLab Inc.?%{link_end}').html_safe % { link_start: service_ping_link_start, link_end: link_end }
- disabled_help_text = s_('AdminSettings|Service ping is disabled in your configuration file, and cannot be enabled through this form. For more information, see the documentation on %{link_start}deactivating service ping%{link_end}.').html_safe % { link_start: deactivating_service_ping_link_start, link_end: link_end }
= f.gitlab_ui_checkbox_component :usage_ping_enabled, s_('AdminSettings|Enable Service Ping'),
help_text: can_be_configured ? usage_ping_help_text : disabled_help_text,
checkbox_options: { disabled: !can_be_configured, data: { testid: 'enable-usage-data-checkbox' } }
.form-text.gl-pl-6
- - if can_be_configured
- = render Pajamas::ButtonComponent.new(button_options: { class: 'js-payload-preview-trigger', data: { payload_selector: ".#{payload_class}" } }) do
- = gl_loading_icon(css_class: 'js-spinner gl-display-none gl-mr-2')
- .js-text.gl-display-inline= s_('AdminSettings|Preview payload')
- %pre.service-data-payload-container.js-syntax-highlight.code.highlight.gl-mt-2.gl-display-none{ class: payload_class, data: { endpoint: usage_data_admin_application_settings_path(format: :html) } }
+ - if @service_ping_data.present?
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-payload-preview-trigger gl-mr-2', data: { payload_selector: ".#{payload_class}" } }) do
+ = gl_loading_icon(css_class: 'js-spinner gl-display-none', inline: true)
+ %span.js-text.gl-display-inline= s_('AdminSettings|Preview payload')
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-payload-download-trigger gl-mr-2', data: { endpoint: usage_data_admin_application_settings_path(format: :json) } }) do
+ = gl_loading_icon(css_class: 'js-spinner gl-display-none', inline: true)
+ %span.js-text.gl-display-inline= s_('AdminSettings|Download payload')
+ %pre.js-syntax-highlight.code.highlight.gl-mt-2.gl-display-none{ class: payload_class, data: { endpoint: usage_data_admin_application_settings_path(format: :html) } }
+ - else
+ = render Pajamas::AlertComponent.new(variant: :warning,
+ dismissible: false,
+ title: s_('AdminSettings|Service Ping payload not found in the application cache')) do |c|
+
+ - c.with_body do
+ - generate_manually_link = link_to('', help_page_path('development/internal_analytics/service_ping/troubleshooting', anchor: 'generate-service-ping'), target: '_blank', rel: 'noopener noreferrer')
+ = safe_format(s_('AdminSettings|%{generate_manually_link_start}Generate%{generate_manually_link_end} Service Ping to preview and download service usage data payload.'), tag_pair(generate_manually_link, :generate_manually_link_start, :generate_manually_link_end))
+
.form-group
- usage_ping_enabled = @application_setting.usage_ping_enabled?
- label = s_('AdminSettings|Enable Registration Features')
diff --git a/app/views/admin/application_settings/general.html.haml b/app/views/admin/application_settings/general.html.haml
index dad0bf08bb0..d84fbe94f65 100644
--- a/app/views/admin/application_settings/general.html.haml
+++ b/app/views/admin/application_settings/general.html.haml
@@ -66,7 +66,7 @@
= expanded_by_default? ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= _('Set sign-in restrictions for all users.')
- = link_to _('Learn more.'), help_page_path('administration/settings/sign_in_restrictions.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('administration/settings/sign_in_restrictions'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'signin'
@@ -78,7 +78,7 @@
= expanded_by_default? ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= _('Add a Terms of Service agreement and Privacy Policy for users of this GitLab instance.')
- = link_to _('Learn more.'), help_page_path('administration/settings/terms.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('administration/settings/terms'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'terms'
@@ -95,7 +95,7 @@
= expanded_by_default? ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= _('Set the maximum session time for a web terminal.')
- = link_to _('How do I use a web terminal?'), help_page_path('ci/environments/index.md', anchor: 'web-terminals-deprecated'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('How do I use a web terminal?'), help_page_path('ci/environments/index', anchor: 'web-terminals-deprecated'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'terminal'
diff --git a/app/views/admin/application_settings/metrics_and_profiling.html.haml b/app/views/admin/application_settings/metrics_and_profiling.html.haml
index 188359158ef..23f536bd6d4 100644
--- a/app/views/admin/application_settings/metrics_and_profiling.html.haml
+++ b/app/views/admin/application_settings/metrics_and_profiling.html.haml
@@ -24,7 +24,7 @@
= expanded_by_default? ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= _('Link to your Grafana instance.')
- = link_to _('Learn more.'), help_page_path('administration/monitoring/performance/grafana_configuration.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('administration/monitoring/performance/grafana_configuration'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'grafana'
@@ -37,11 +37,11 @@
= expanded_by_default? ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= _('Enable access to the performance bar for non-administrators in a given group.')
- = link_to _('Learn more.'), help_page_path('administration/monitoring/performance/performance_bar.md', anchor: 'enable-the-performance-bar-for-non-administrators'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('administration/monitoring/performance/performance_bar', anchor: 'enable-the-performance-bar-for-non-administrators'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'performance_bar'
-%section.settings.as-usage.no-animate#js-usage-settings{ class: ('expanded' if expanded_by_default?), data: { qa_selector: 'usage_statistics_settings_content' } }
+%section.settings.as-usage.no-animate#js-usage-settings{ class: ('expanded' if expanded_by_default?), data: { testid: 'usage-statistics-settings-content' } }
.settings-header#usage-statistics
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _('Usage statistics')
@@ -53,7 +53,7 @@
= render 'usage'
- if Feature.enabled?(:configure_sentry_in_application_settings)
- %section.settings.as-sentry.no-animate#js-sentry-settings{ class: ('expanded' if expanded_by_default?), data: { qa_selector: 'sentry_settings_content' } }
+ %section.settings.as-sentry.no-animate#js-sentry-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _('Sentry')
diff --git a/app/views/admin/application_settings/network.html.haml b/app/views/admin/application_settings/network.html.haml
index 849c5c749e0..ae5f7a5cec3 100644
--- a/app/views/admin/application_settings/network.html.haml
+++ b/app/views/admin/application_settings/network.html.haml
@@ -22,11 +22,11 @@
= expanded_by_default? ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= _('Set limits for web and API requests.')
- = link_to _('Learn more.'), help_page_path('administration/settings/user_and_ip_rate_limits.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('administration/settings/user_and_ip_rate_limits'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'ip_limits'
-%section.settings.as-packages-limits.no-animate#js-packages-limits-settings{ class: ('expanded' if expanded_by_default?), data: { qa_selector: 'packages_limits_content' } }
+%section.settings.as-packages-limits.no-animate#js-packages-limits-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _('Package registry rate limits')
@@ -34,7 +34,7 @@
= expanded_by_default? ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= _('Set rate limits for package registry API requests that supersede the general user and IP rate limits.')
- = link_to _('Learn more.'), help_page_path('administration/settings/package_registry_rate_limits.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('administration/settings/package_registry_rate_limits'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render partial: 'network_rate_limits', locals: { anchor: 'js-packages-limits-settings', setting_fragment: 'packages_api' }
@@ -68,11 +68,11 @@
= expanded_by_default? ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= _('Configure specific limits for deprecated API requests that supersede the general user and IP rate limits.')
- = link_to _('Which API requests are affected?'), help_page_path('administration/settings/deprecated_api_rate_limits.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Which API requests are affected?'), help_page_path('administration/settings/deprecated_api_rate_limits'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render partial: 'network_rate_limits', locals: { anchor: 'js-deprecated-limits-settings', setting_fragment: 'deprecated_api' }
-%section.settings.as-git-lfs-limits.no-animate#js-git-lfs-limits-settings{ class: ('expanded' if expanded_by_default?), data: { qa_selector: 'git_lfs_limits_content' } }
+%section.settings.as-git-lfs-limits.no-animate#js-git-lfs-limits-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _('Git LFS Rate Limits')
@@ -80,7 +80,7 @@
= expanded_by_default? ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= _('Configure specific limits for Git LFS requests that supersede the general user and IP rate limits.')
- = link_to _('Learn more.'), help_page_path('administration/settings/git_lfs_rate_limits.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('administration/settings/git_lfs_rate_limits'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'git_lfs_limits'
@@ -96,7 +96,7 @@
= expanded_by_default? ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= s_('OutboundRequests|Allow requests to the local network from hooks and integrations.')
- = link_to _('Learn more.'), help_page_path('security/webhooks.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('security/webhooks'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'outbound'
@@ -108,7 +108,7 @@
= expanded_by_default? ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= _('Rate limit access to specified paths.')
- = link_to _('Learn more.'), help_page_path('administration/settings/protected_paths.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('administration/settings/protected_paths'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'protected_paths'
@@ -121,7 +121,7 @@
= expanded_by_default? ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= _('Limit the number of issues and epics per minute a user can create through web and API requests.')
- = link_to _('Learn more.'), help_page_path('administration/settings/rate_limit_on_issues_creation.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('administration/settings/rate_limit_on_issues_creation'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'issue_limits'
@@ -133,7 +133,7 @@
= expanded_by_default? ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= _('Set the per-user rate limit for notes created by web or API requests.')
- = link_to _('Learn more.'), help_page_path('administration/settings/rate_limit_on_notes_creation.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('administration/settings/rate_limit_on_notes_creation'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'note_limits'
@@ -145,7 +145,7 @@
= expanded_by_default? ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= _('Set the per-user rate limit for getting a user by ID via the API.')
- = link_to _('Learn more.'), help_page_path('administration/settings/rate_limit_on_users_api.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('administration/settings/rate_limit_on_users_api'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'users_api_limits'
@@ -159,7 +159,7 @@
= expanded_by_default? ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= _('Set per-user rate limits for imports and exports of projects and groups.')
- = link_to _('Learn more.'), help_page_path('administration/settings/import_export_rate_limits.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('administration/settings/import_export_rate_limits'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'import_export_limits'
@@ -171,7 +171,7 @@
= expanded_by_default? ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= _('Limit the number of pipeline creation requests per minute. This limit includes pipelines created through the UI, the API, and by background processing.')
- = link_to _('Learn more.'), help_page_path('administration/settings/rate_limit_on_pipelines_creation.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('administration/settings/rate_limit_on_pipelines_creation'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'pipeline_limits'
diff --git a/app/views/admin/application_settings/preferences.html.haml b/app/views/admin/application_settings/preferences.html.haml
index 4590b6f4586..3543e1d918a 100644
--- a/app/views/admin/application_settings/preferences.html.haml
+++ b/app/views/admin/application_settings/preferences.html.haml
@@ -33,7 +33,7 @@
= expanded_by_default? ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= _('Additional text for the sign-in and Help page.')
- = link_to _('Learn more.'), help_page_path('administration/settings/help_page.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('administration/settings/help_page'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'help_page'
@@ -56,7 +56,7 @@
= expanded_by_default? ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= _('Adjust how frequently the GitLab UI polls for updates.')
- = link_to _('Learn more.'), help_page_path('administration/polling.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('administration/polling'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'realtime'
@@ -69,7 +69,7 @@
%p.gl-text-secondary
= _('Configure Gitaly timeouts.')
%span
- = link_to _('Learn more.'), help_page_path('administration/settings/gitaly_timeouts.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('administration/settings/gitaly_timeouts'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'gitaly'
@@ -93,7 +93,7 @@
%p.gl-text-secondary
= _('Limit the size of Sidekiq jobs stored in Redis.')
%span
- = link_to _('Learn more.'), help_page_path('administration/settings/sidekiq_job_limits.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('administration/settings/sidekiq_job_limits'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'sidekiq_job_limits'
@@ -106,6 +106,6 @@
= expanded_by_default? ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= s_('TerraformLimits|Limits for Terraform features')
- = link_to s_('TerraformLimits|Learn more about Terraform limits.'), help_page_path('administration/settings/terraform_limits.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to s_('TerraformLimits|Learn more about Terraform limits.'), help_page_path('administration/settings/terraform_limits'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'terraform_limits'
diff --git a/app/views/admin/application_settings/reporting.html.haml b/app/views/admin/application_settings/reporting.html.haml
index 91fabb505c2..49279c4584b 100644
--- a/app/views/admin/application_settings/reporting.html.haml
+++ b/app/views/admin/application_settings/reporting.html.haml
@@ -25,7 +25,7 @@
= expanded_by_default? ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= _('Receive notification of abuse reports by email.')
- = link_to _('Learn more.'), help_page_path('administration/review_abuse_reports.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('administration/review_abuse_reports'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'abuse'
diff --git a/app/views/admin/application_settings/repository.html.haml b/app/views/admin/application_settings/repository.html.haml
index c7a2fca00ef..0b31da36804 100644
--- a/app/views/admin/application_settings/repository.html.haml
+++ b/app/views/admin/application_settings/repository.html.haml
@@ -22,11 +22,11 @@
= expanded_by_default? ? 'Collapse' : 'Expand'
%p.gl-text-secondary
= _('Configure repository mirroring.')
- = link_to _('Learn more.'), help_page_path('user/project/repository/mirror/index.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('user/project/repository/mirror/index'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render partial: 'repository_mirrors_form'
-%section.settings.as-repository-storage.no-animate#js-repository-storage-settings{ class: ('expanded' if expanded_by_default?), data: { qa_selector: 'repository_storage_settings_content' } }
+%section.settings.as-repository-storage.no-animate#js-repository-storage-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _('Repository storage')
@@ -34,7 +34,7 @@
= expanded_by_default? ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= _('Configure repository storage.')
- = link_to _('Learn more.'), help_page_path('administration/repository_storage_paths.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('administration/repository_storage_paths'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'repository_storage'
@@ -45,9 +45,9 @@
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
%p.gl-text-secondary
- - repository_checks_link_url = help_page_path('administration/repository_checks.md')
+ - repository_checks_link_url = help_page_path('administration/repository_checks')
- repository_checks_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: repository_checks_link_url }
- - housekeeping_link_url = help_page_path('administration/housekeeping.md')
+ - housekeeping_link_url = help_page_path('administration/housekeeping')
- housekeeping_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: housekeeping_link_url }
= html_escape(s_('Configure %{repository_checks_link_start}repository checks%{link_end} and %{housekeeping_link_start}housekeeping%{link_end} on repositories.')) % { repository_checks_link_start: repository_checks_link_start, housekeeping_link_start: housekeeping_link_start, link_end: '</a>'.html_safe }
.settings-content
@@ -61,6 +61,6 @@
= expanded_by_default? ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= _('Serve repository static objects (for example, archives and blobs) from external storage.')
- = link_to _('Learn more.'), help_page_path('administration/static_objects_external_storage.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('administration/static_objects_external_storage'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'repository_static_objects'
diff --git a/app/views/admin/application_settings/service_usage_data.html.haml b/app/views/admin/application_settings/service_usage_data.html.haml
deleted file mode 100644
index 9f73099465c..00000000000
--- a/app/views/admin/application_settings/service_usage_data.html.haml
+++ /dev/null
@@ -1,29 +0,0 @@
-- name = _("Service usage data")
-
-- breadcrumb_title name
-- page_title name
-- add_page_specific_style 'page_bundles/settings'
-- payload_class = 'js-service-ping-payload'
-- @force_desktop_expanded_sidebar = true
-
-%section.js-search-settings-section
- %h3= name
-
- - if @service_ping_data_present
- = render Pajamas::ButtonComponent.new(button_options: { class: 'js-payload-preview-trigger gl-mr-2', data: { payload_selector: ".#{payload_class}" } }) do
- = gl_loading_icon(css_class: 'js-spinner gl-display-none', inline: true)
- %span.js-text.gl-display-inline= _('Preview payload')
- = render Pajamas::ButtonComponent.new(button_options: { class: 'js-payload-download-trigger gl-mr-2', data: { endpoint: usage_data_admin_application_settings_path(format: :json) } }) do
- = gl_loading_icon(css_class: 'js-spinner gl-display-none', inline: true)
- %span.js-text.gl-display-inline= _('Download payload')
- %pre.js-syntax-highlight.code.highlight.gl-mt-2.gl-display-none{ class: payload_class, data: { endpoint: usage_data_admin_application_settings_path(format: :html) } }
- - else
- = render Pajamas::AlertComponent.new(variant: :warning,
- dismissible: false,
- title: _('Service Ping payload not found in the application cache')) do |c|
-
- - c.with_body do
- - enable_service_ping_link = link_to('', help_page_path('administration/settings/usage_statistics', anchor: 'enable-or-disable-usage-statistics'), target: '_blank', rel: 'noopener noreferrer')
- - generate_manually_link = link_to('', help_page_path('development/internal_analytics/service_ping/troubleshooting', anchor: 'generate-service-ping'), target: '_blank', rel: 'noopener noreferrer')
-
- = safe_format(s_('%{enable_service_ping_link_start}Enable%{enable_service_ping_link_end} or %{generate_manually_link_start}generate%{generate_manually_link_end} Service Ping to preview and download service usage data payload.'), tag_pair(enable_service_ping_link, :enable_service_ping_link_start, :enable_service_ping_link_end), tag_pair(generate_manually_link, :generate_manually_link_start, :generate_manually_link_end))
diff --git a/app/views/admin/background_migrations/index.html.haml b/app/views/admin/background_migrations/index.html.haml
index 9550ea2884e..e7212f00e5b 100644
--- a/app/views/admin/background_migrations/index.html.haml
+++ b/app/views/admin/background_migrations/index.html.haml
@@ -1,7 +1,7 @@
- page_title s_('BackgroundMigrations|Background Migrations')
- @breadcrumb_link = admin_background_migrations_path(database: params[:database])
-.gl-display-flex.gl-sm-flex-direction-column.gl-sm-align-items-flex-end.gl-pb-5.gl-border-b-1.gl-border-b-solid.gl-border-b-gray-100
+.gl-display-flex.gl-flex-direction-column.gl-md-flex-direction-row.gl-sm-align-items-flex-end.gl-pb-5.gl-border-b-1.gl-border-b-solid.gl-border-b-gray-100
.gl-flex-grow-1
%h3= s_('BackgroundMigrations|Background Migrations')
%p.light.gl-mb-0
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index 4973c0f985c..bf00fbfd81d 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -89,7 +89,7 @@
= feature_entry(_('LDAP'),
enabled: Gitlab.config.ldap.enabled,
- doc_href: help_page_path('administration/auth/ldap/index.md'))
+ doc_href: help_page_path('administration/auth/ldap/index'))
= feature_entry(_('Gravatar'),
href: general_admin_application_settings_path(anchor: 'js-account-settings'),
diff --git a/app/views/admin/dev_ops_report/_score.html.haml b/app/views/admin/dev_ops_report/_score.html.haml
index a504563ad91..59cb30e8447 100644
--- a/app/views/admin/dev_ops_report/_score.html.haml
+++ b/app/views/admin/dev_ops_report/_score.html.haml
@@ -1,6 +1,6 @@
- service_ping_enabled = Gitlab::CurrentSettings.usage_ping_enabled
- if !service_ping_enabled
- #js-devops-service-ping-disabled{ data: { is_admin: current_user&.admin.to_s, empty_state_svg_path: image_path('illustrations/convdev/convdev_no_index.svg'), enable_service_ping_path: metrics_and_profiling_admin_application_settings_path(anchor: 'js-usage-settings'), docs_link: help_page_path('development/internal_analytics/service_ping/index.md') } }
+ #js-devops-service-ping-disabled{ data: { is_admin: current_user&.admin.to_s, empty_state_svg_path: image_path('illustrations/convdev/convdev_no_index.svg'), enable_service_ping_path: metrics_and_profiling_admin_application_settings_path(anchor: 'js-usage-settings'), docs_link: help_page_path('development/internal_analytics/service_ping/index') } }
- else
#js-devops-score{ data: { devops_score_metrics: devops_score_metrics(@metric).to_json, no_data_image_path: image_path('dev_ops_report_no_data.svg'), devops_score_intro_image_path: image_path('dev_ops_report_overview.svg') } }
diff --git a/app/views/admin/spam_logs/_spam_log.html.haml b/app/views/admin/spam_logs/_spam_log.html.haml
index 6aed8508a6a..878692438d4 100644
--- a/app/views/admin/spam_logs/_spam_log.html.haml
+++ b/app/views/admin/spam_logs/_spam_log.html.haml
@@ -29,7 +29,7 @@
variant: :danger,
method: :delete,
href: admin_spam_log_path(spam_log, remove_user: true),
- button_options: { data: { confirm: _("USER %{user_name} WILL BE REMOVED! Are you sure?") % { user_name: user.name }, confirm_btn_variant: 'danger' }, aria: { label: _('Remove user') } }) do
+ button_options: { data: { confirm: _("User %{user_name} will be removed! Are you sure?") % { user_name: user.name }, confirm_btn_variant: 'danger' }, aria: { label: _('Remove user') } }) do
= _('Remove user')
%td
-# TODO: Remove conditonal once spamcheck supports this https://gitlab.com/gitlab-com/gl-security/engineering-and-research/automation-team/spam/spamcheck/-/issues/190
@@ -48,11 +48,23 @@
= render Pajamas::ButtonComponent.new(size: :small,
method: :put,
href: block_admin_user_path(user),
- button_options: { class: 'gl-mb-3', data: {confirm: _('USER WILL BE BLOCKED! Are you sure?')} }) do
+ button_options: { class: 'gl-mb-3', data: {confirm: _('User will be blocked! Are you sure?')} }) do
= _('Block user')
- else
= render Pajamas::ButtonComponent.new(size: :small, button_options: { class: 'disabled gl-mb-3'}) do
= _("Already blocked")
+ - if user && !user.trusted?
+ = render Pajamas::ButtonComponent.new(size: :small,
+ method: :put,
+ href: trust_admin_user_path(user),
+ button_options: { class: 'gl-mb-3', data: {confirm: _('User will be allowed to create possible spam! Are you sure?')} }) do
+ = _('Trust user')
+ - else
+ = render Pajamas::ButtonComponent.new(size: :small,
+ method: :put,
+ href: untrust_admin_user_path(user),
+ button_options: { class: 'gl-mb-3', data: {confirm: _('User will not be allowed to create possible spam! Are you sure?')} }) do
+ = _('Untrust user')
= render Pajamas::ButtonComponent.new(size: :small,
method: :delete,
href: [:admin, spam_log],
diff --git a/app/views/admin/topics/_form.html.haml b/app/views/admin/topics/_form.html.haml
index 2638e45c9eb..c61be1182e0 100644
--- a/app/views/admin/topics/_form.html.haml
+++ b/app/views/admin/topics/_form.html.haml
@@ -4,7 +4,7 @@
.form-group
= f.label :name do
= _("Topic slug (name)")
- = f.text_field :name, placeholder: _('my-topic'), class: 'form-control input-lg', data: { qa_selector: 'topic_name_field' },
+ = f.text_field :name, placeholder: _('my-topic'), class: 'form-control input-lg',
required: true,
title: _('Please fill in a name for your topic.'),
autofocus: true
@@ -12,7 +12,7 @@
.form-group
= f.label :title do
= _("Topic title")
- = f.text_field :title, placeholder: _('My topic'), class: 'form-control input-lg', data: { qa_selector: 'topic_title_field' },
+ = f.text_field :title, placeholder: _('My topic'), class: 'form-control input-lg',
required: true,
title: _('Please fill in a title for your topic.')
diff --git a/app/views/admin/topics/_topic.html.haml b/app/views/admin/topics/_topic.html.haml
index 3e8a023ec9f..4e8b1394e06 100644
--- a/app/views/admin/topics/_topic.html.haml
+++ b/app/views/admin/topics/_topic.html.haml
@@ -1,7 +1,7 @@
- topic = local_assigns.fetch(:topic)
- title = topic.title || topic.name
-%li.topic-row.gl-py-3.gl-align-items-center{ class: 'gl-display-flex!', data: { qa_selector: 'topic_row_content' } }
+%li.topic-row.gl-py-3.gl-align-items-center{ class: 'gl-display-flex!' }
= render Pajamas::AvatarComponent.new(topic, size: 32, alt: '')
.gl-min-w-0.gl-flex-grow-1.gl-ml-3
diff --git a/app/views/admin/topics/index.html.haml b/app/views/admin/topics/index.html.haml
index 6d64fa1983f..46c1b9ac5c4 100644
--- a/app/views/admin/topics/index.html.haml
+++ b/app/views/admin/topics/index.html.haml
@@ -6,7 +6,7 @@
= form_tag admin_topics_path, method: :get do |f|
- search = params.fetch(:search, nil)
.search-field-holder
- = search_field_tag :search, search, class: "form-control gl-form-input search-text-input js-search-input", autofocus: true, spellcheck: false, placeholder: _('Search by name'), data: { qa_selector: 'topic_search_field' }
+ = search_field_tag :search, search, class: "form-control gl-form-input search-text-input js-search-input", autofocus: true, spellcheck: false, placeholder: _('Search by name')
= sprite_icon('search', css_class: 'search-icon')
.gl-flex-grow-1
.js-merge-topics{ data: { path: merge_admin_topics_path } }
diff --git a/app/views/admin/users/_users.html.haml b/app/views/admin/users/_users.html.haml
index d4a9009a0cf..bbb068c3680 100644
--- a/app/views/admin/users/_users.html.haml
+++ b/app/views/admin/users/_users.html.haml
@@ -44,6 +44,9 @@
= gl_tab_link_to admin_users_path(filter: "wop"), { item_active: active_when(params[:filter] == 'wop'), class: 'gl-border-0!' } do
= s_('AdminUsers|Without projects')
= gl_tab_counter_badge(limited_counter_with_delimiter(User.without_projects))
+ = gl_tab_link_to admin_users_path(filter: "trusted"), { item_active: active_when(params[:filter] == 'trusted'), class: 'gl-border-0!' } do
+ = s_('AdminUsers|Trusted')
+ = gl_tab_counter_badge(limited_counter_with_delimiter(User.trusted))
.nav-controls
= render_if_exists 'admin/users/admin_email_users'
= render_if_exists 'admin/users/admin_export_user_permissions'
diff --git a/app/views/admin/users/projects.html.haml b/app/views/admin/users/projects.html.haml
index fa89c3d4b4f..bbf1e3b0b2f 100644
--- a/app/views/admin/users/projects.html.haml
+++ b/app/views/admin/users/projects.html.haml
@@ -1,3 +1,4 @@
+-# rubocop: disable CodeReuse/ActiveRecord
- add_to_breadcrumbs _("Users"), admin_users_path
- breadcrumb_title @user.name
- page_title _("Groups and projects"), @user.name, _("Users")
@@ -9,7 +10,7 @@
= _('Groups')
- c.with_body do
%ul.hover-list
- - @user.group_members.includes(:source).each do |group_member| # rubocop: disable CodeReuse/ActiveRecord
+ - @user.group_members.includes(:source).find_each do |group_member|
- group = group_member.group
%li.group_member
%strong= link_to group.name, admin_group_path(group)
@@ -50,3 +51,4 @@
- if member.respond_to? :project
= link_button_to nil, project_project_member_path(project, member), data: { confirm: remove_member_message(member), confirm_btn_variant: 'danger' }, aria: { label: _('Remove') }, remote: true, method: :delete, class: 'gl-ml-3', title: _('Remove user from project'), variant: :danger, size: :small, icon: 'remove'
+-# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/views/ci/status/_badge.html.haml b/app/views/ci/status/_badge.html.haml
deleted file mode 100644
index e3b409dea76..00000000000
--- a/app/views/ci/status/_badge.html.haml
+++ /dev/null
@@ -1,13 +0,0 @@
-- status = local_assigns.fetch(:status)
-- link = local_assigns.fetch(:link, true)
-- title = local_assigns.fetch(:title, nil)
-- css_classes = "gl-display-inline-flex gl-align-items-center gl-gap-2 gl-line-height-0 gl-px-3 gl-py-2 gl-rounded-base ci-status ci-#{status.group} #{'has-tooltip' if title.present?}"
-
-- if link && status.has_details?
- = link_to status.details_path, class: css_classes, title: title, data: { html: title.present? } do
- = sprite_icon(status.icon)
- = status.text
-- else
- %span{ class: css_classes, title: title, data: { html: title.present? } }
- = sprite_icon(status.icon)
- = status.text
diff --git a/app/views/ci/status/_icon.html.haml b/app/views/ci/status/_icon.html.haml
index 9fa5734d6b6..bcb83874bfb 100644
--- a/app/views/ci/status/_icon.html.haml
+++ b/app/views/ci/status/_icon.html.haml
@@ -1,10 +1,7 @@
- status = local_assigns.fetch(:status)
-- tooltip_placement = local_assigns.fetch(:tooltip_placement, "left")
- path = local_assigns.fetch(:path, status.has_details? ? status.details_path : nil)
-- option_css_classes = local_assigns.fetch(:option_css_classes, '')
-- css_classes = "gl-px-2 #{option_css_classes}"
-- ci_css_classes = "ci-status-link ci-status-icon ci-status-icon-#{status.group} gl-line-height-1"
-- title = s_("PipelineStatusTooltip|Pipeline: %{ci_status}") % {ci_status: status.label}
+- tooltip_placement = local_assigns.fetch(:tooltip_placement, "left")
+- option_css_classes = local_assigns.fetch(:option_css_classes, nil)
+- show_status_text = local_assigns.fetch(:show_status_text, false)
-= gl_badge_tag(variant: badge_variant(status), size: :md, href: path, class: css_classes, title: title, data: { toggle: 'tooltip', placement: tooltip_placement, testid: "ci-status-badge" }) do
- = content_tag :span, sprite_icon(status.icon, size: 16), class: ci_css_classes
+= render_ci_icon(status, path, tooltip_placement: tooltip_placement, option_css_classes: option_css_classes, show_status_text: show_status_text)
diff --git a/app/views/clusters/clusters/_advanced_settings.html.haml b/app/views/clusters/clusters/_advanced_settings.html.haml
index a818f8a5c26..57111dd6232 100644
--- a/app/views/clusters/clusters/_advanced_settings.html.haml
+++ b/app/views/clusters/clusters/_advanced_settings.html.haml
@@ -30,7 +30,7 @@
selected: @cluster.management_project_id } }
%p.text-muted.gl-mt-n5
= html_escape(s_('ClusterIntegration|A cluster management project can be used to run deployment jobs with Kubernetes %{code_open}cluster-admin%{code_close} privileges.')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
- = link_to _('More information'), help_page_path('user/clusters/management_project.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('More information'), help_page_path('user/clusters/management_project'), target: '_blank', rel: 'noopener noreferrer'
= field.submit _('Save changes'), pajamas_button: true
.sub-section.form-group
diff --git a/app/views/clusters/clusters/_deprecation_alert.html.haml b/app/views/clusters/clusters/_deprecation_alert.html.haml
index 4f35ba78cc6..cfc3418b1b5 100644
--- a/app/views/clusters/clusters/_deprecation_alert.html.haml
+++ b/app/views/clusters/clusters/_deprecation_alert.html.haml
@@ -2,6 +2,6 @@
- c.with_body do
- link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe
- issue_link_start = link_start % { url: 'https://gitlab.com/gitlab-org/configure/general/-/issues/199' }
- - docs_link_start = link_start % { url: help_page_path('user/clusters/agent/index.md') }
+ - docs_link_start = link_start % { url: help_page_path('user/clusters/agent/index') }
- link_end = '</a>'.html_safe
= s_('ClusterIntegration|This process is %{issue_link_start}deprecated%{issue_link_end}. Use the %{docs_link_start}the GitLab agent for Kubernetes%{docs_link_end} instead.').html_safe % { docs_link_start: docs_link_start, docs_link_end: link_end, issue_link_start: issue_link_start, issue_link_end: link_end }
diff --git a/app/views/clusters/clusters/_multiple_clusters_message.html.haml b/app/views/clusters/clusters/_multiple_clusters_message.html.haml
index 04c1f9b6e7a..2878bb1371c 100644
--- a/app/views/clusters/clusters/_multiple_clusters_message.html.haml
+++ b/app/views/clusters/clusters/_multiple_clusters_message.html.haml
@@ -1,4 +1,4 @@
-- autodevops_help_url = help_page_path('topics/autodevops/multiple_clusters_auto_devops.md')
+- autodevops_help_url = help_page_path('topics/autodevops/multiple_clusters_auto_devops')
- help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe
- help_link_end = '</a>'.html_safe
diff --git a/app/views/clusters/clusters/_namespace.html.haml b/app/views/clusters/clusters/_namespace.html.haml
index 34576b6e5af..9c20a409b18 100644
--- a/app/views/clusters/clusters/_namespace.html.haml
+++ b/app/views/clusters/clusters/_namespace.html.haml
@@ -1,6 +1,6 @@
- managed_namespace_help_text = s_('ClusterIntegration|Set a prefix for your namespaces. If not set, defaults to your project path. If modified, existing environments will use their current namespaces until the cluster cache is cleared.')
- non_managed_namespace_help_text = s_('ClusterIntegration|The namespace associated with your project. This will be used for deploy boards, and Web terminals.')
-- managed_namespace_help_link = link_to _('More information'), help_page_path('user/project/clusters/gitlab_managed_clusters.md'), target: '_blank', rel: 'noopener noreferrer'
+- managed_namespace_help_link = link_to _('More information'), help_page_path('user/project/clusters/gitlab_managed_clusters'), target: '_blank', rel: 'noopener noreferrer'
.js-namespace-prefixed
.form-group
diff --git a/app/views/clusters/clusters/_provider_details_form.html.haml b/app/views/clusters/clusters/_provider_details_form.html.haml
index 4b7164f9845..f675ea5865e 100644
--- a/app/views/clusters/clusters/_provider_details_form.html.haml
+++ b/app/views/clusters/clusters/_provider_details_form.html.haml
@@ -1,35 +1,35 @@
= gitlab_ui_form_for cluster, url: update_cluster_url_path, html: { class: 'js-provider-details gl-show-field-errors', role: 'form' },
as: :cluster do |field|
.form-group
- - copy_name_btn = deprecated_clipboard_button(text: cluster.name, title: s_('ClusterIntegration|Copy Kubernetes cluster name'),
- class: 'input-group-text btn-default') if cluster.read_only_kubernetes_platform_fields?
= field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-bold required'
.input-group.gl-field-error-anchor
= field.text_field :name, class: 'form-control js-select-on-focus cluster-name', required: true,
title: s_('ClusterIntegration|Cluster name is required.'),
- readonly: cluster.read_only_kubernetes_platform_fields?,
- append: copy_name_btn
+ readonly: cluster.read_only_kubernetes_platform_fields?
+ - if cluster.read_only_kubernetes_platform_fields?
+ .input-group-append
+ = clipboard_button(text: cluster.name, title: s_('ClusterIntegration|Copy Kubernetes cluster name'), variant: :default, category: :primary, size: :medium)
= field.fields_for :platform_kubernetes, platform do |platform_field|
.form-group
- - copy_api_url = deprecated_clipboard_button(text: platform.api_url, title: s_('ClusterIntegration|Copy API URL'),
- class: 'input-group-text btn-default') if cluster.read_only_kubernetes_platform_fields?
= platform_field.label :api_url, s_('ClusterIntegration|API URL'), class: 'label-bold required'
.input-group.gl-field-error-anchor
= platform_field.text_field :api_url, class: 'form-control js-select-on-focus', required: true,
title: s_('ClusterIntegration|API URL should be a valid http/https url.'),
- readonly: cluster.read_only_kubernetes_platform_fields?,
- append: copy_api_url
+ readonly: cluster.read_only_kubernetes_platform_fields?
+ - if cluster.read_only_kubernetes_platform_fields?
+ .input-group-append
+ = clipboard_button(text: platform.api_url, title: s_('ClusterIntegration|Copy API URL'), variant: :default, category: :primary, size: :medium)
.form-group
- - copy_ca_cert_btn = deprecated_clipboard_button(text: platform.ca_cert, title: s_('ClusterIntegration|Copy CA Certificate'),
- class: 'input-group-text btn-default') if cluster.read_only_kubernetes_platform_fields?
= platform_field.label :ca_cert, s_('ClusterIntegration|CA Certificate'), class: 'label-bold'
- .input-group.gl-field-error-anchor
- = platform_field.text_area :ca_cert, class: 'form-control js-select-on-focus', rows: '10',
+ .input-group.gl-field-error-anchor.markdown-code-block
+ = platform_field.text_area :ca_cert, class: 'gl-rounded-top-right-base! gl-rounded-bottom-right-base! form-control js-select-on-focus', rows: '10',
readonly: cluster.read_only_kubernetes_platform_fields?,
- placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)'),
- append: copy_ca_cert_btn
+ placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)')
+ - if cluster.read_only_kubernetes_platform_fields?
+ %copy-code
+ = clipboard_button(text: platform.ca_cert, title: s_('ClusterIntegration|Copy CA Certificate'), variant: :default, category: :primary, size: :medium, class: 'copy-code')
.form-group
= platform_field.label :token, s_('ClusterIntegration|Enter new Service Token'), class: 'label-bold required'
@@ -51,7 +51,7 @@
= field.label :managed, s_('ClusterIntegration|GitLab-managed cluster'), class: 'form-check-label label-bold'
.form-text.text-muted
= s_('ClusterIntegration|Allow GitLab to manage namespaces and service accounts for this cluster.')
- = link_to _('More information'), help_page_path('user/project/clusters/gitlab_managed_clusters.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('More information'), help_page_path('user/project/clusters/gitlab_managed_clusters'), target: '_blank', rel: 'noopener noreferrer'
.form-group
.form-check
@@ -59,7 +59,7 @@
= field.label :namespace_per_environment, s_('ClusterIntegration|Namespace per environment'), class: 'form-check-label label-bold'
.form-text.text-muted
= s_('ClusterIntegration|Deploy each environment to its own namespace. Otherwise, environments within a project share a project-wide namespace. Note that anyone who can trigger a deployment of a namespace can read its secrets. If modified, existing environments will use their current namespaces until the cluster cache is cleared.')
- = link_to _('More information'), help_page_path('user/project/clusters/deploy_to_cluster.md', anchor: 'custom-namespace'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('More information'), help_page_path('user/project/clusters/deploy_to_cluster', anchor: 'custom-namespace'), target: '_blank', rel: 'noopener noreferrer'
- if cluster.allow_user_defined_namespace?
= render('clusters/clusters/namespace', platform_field: platform_field, field: field)
diff --git a/app/views/clusters/clusters/cloud_providers/_cloud_provider_selector.html.haml b/app/views/clusters/clusters/cloud_providers/_cloud_provider_selector.html.haml
index 49dab193da8..8ac232ac7ca 100644
--- a/app/views/clusters/clusters/cloud_providers/_cloud_provider_selector.html.haml
+++ b/app/views/clusters/clusters/cloud_providers/_cloud_provider_selector.html.haml
@@ -2,9 +2,9 @@
- eks_label = s_('ClusterIntegration|Amazon EKS')
- civo_label = s_('ClusterIntegration|Civo Kubernetes')
- create_cluster_label = s_('ClusterIntegration|Where do you want to create a cluster?')
-- eks_help_path = help_page_path('user/infrastructure/clusters/connect/new_eks_cluster.md')
-- gke_help_path = help_page_path('user/infrastructure/clusters/connect/new_gke_cluster.md')
-- civo_help_path = help_page_path('user/infrastructure/clusters/connect/new_civo_cluster.md')
+- eks_help_path = help_page_path('user/infrastructure/clusters/connect/new_eks_cluster')
+- gke_help_path = help_page_path('user/infrastructure/clusters/connect/new_gke_cluster')
+- civo_help_path = help_page_path('user/infrastructure/clusters/connect/new_civo_cluster')
.gl-py-5.gl-md-pl-5.gl-md-pr-5
%h4.gl-mb-5
diff --git a/app/views/clusters/clusters/connect.html.haml b/app/views/clusters/clusters/connect.html.haml
index a6e1837badf..68e5fcb277b 100644
--- a/app/views/clusters/clusters/connect.html.haml
+++ b/app/views/clusters/clusters/connect.html.haml
@@ -5,7 +5,7 @@
= render 'deprecation_alert'
.gl-md-display-flex.gl-mt-3
- .gl-w-quarter.gl-xs-w-full.gl-flex-shrink-0.gl-md-mr-5
+ .gl-w-full.gl-sm-w-25p.gl-flex-shrink-0.gl-md-mr-5
= render 'sidebar', is_connect_page: true
.gl-w-full
#js-cluster-new
diff --git a/app/views/clusters/clusters/new_cluster_docs.html.haml b/app/views/clusters/clusters/new_cluster_docs.html.haml
index 72c70f35e22..d58c844382d 100644
--- a/app/views/clusters/clusters/new_cluster_docs.html.haml
+++ b/app/views/clusters/clusters/new_cluster_docs.html.haml
@@ -5,7 +5,7 @@
= render_gcp_signup_offer
.gl-md-display-flex.gl-mt-3
- .gl-w-quarter.gl-xs-w-full.gl-flex-shrink-0.gl-md-mr-5
+ .gl-w-full.gl-sm-w-25p.gl-flex-shrink-0.gl-md-mr-5
= render 'sidebar', is_connect_page: false
.gl-w-full
= render 'clusters/clusters/cloud_providers/cloud_provider_selector'
diff --git a/app/views/clusters/clusters/show.html.haml b/app/views/clusters/clusters/show.html.haml
index 1287f4e689f..22dee5876c2 100644
--- a/app/views/clusters/clusters/show.html.haml
+++ b/app/views/clusters/clusters/show.html.haml
@@ -12,10 +12,10 @@
cluster_status: @cluster.status_name,
cluster_status_reason: @cluster.status_reason,
provider_type: @cluster.provider_type,
- help_path: help_page_path('user/infrastructure/clusters/index.md'),
- environments_help_path: help_page_path('ci/environments/index.md', anchor: 'create-a-static-environment'),
- clusters_help_path: help_page_path('user/project/clusters/deploy_to_cluster.md'),
- deploy_boards_help_path: help_page_path('user/project/deploy_boards.md', anchor: 'enabling-deploy-boards'),
+ help_path: help_page_path('user/infrastructure/clusters/index'),
+ environments_help_path: help_page_path('ci/environments/index', anchor: 'create-a-static-environment'),
+ clusters_help_path: help_page_path('user/project/clusters/deploy_to_cluster'),
+ deploy_boards_help_path: help_page_path('user/project/deploy_boards', anchor: 'enabling-deploy-boards'),
cluster_id: @cluster.id } }
.js-cluster-application-notice
diff --git a/app/views/clusters/clusters/user/_form.html.haml b/app/views/clusters/clusters/user/_form.html.haml
index 4ecef4b76ce..6a5acf4f507 100644
--- a/app/views/clusters/clusters/user/_form.html.haml
+++ b/app/views/clusters/clusters/user/_form.html.haml
@@ -58,7 +58,7 @@
= field.label :managed, s_('ClusterIntegration|GitLab-managed cluster'), class: 'form-check-label label-bold'
.form-text.text-muted
= s_('ClusterIntegration|Allow GitLab to manage namespaces and service accounts for this cluster.')
- = link_to _('Learn more.'), help_page_path('user/project/clusters/gitlab_managed_clusters.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('user/project/clusters/gitlab_managed_clusters'), target: '_blank', rel: 'noopener noreferrer'
.form-group
.form-check
@@ -66,7 +66,7 @@
= field.label :namespace_per_environment, s_('ClusterIntegration|Namespace per environment'), class: 'form-check-label label-bold'
.form-text.text-muted
= s_('ClusterIntegration|Deploy each environment to its own namespace. Otherwise, environments within a project share a project-wide namespace. Note that anyone who can trigger a deployment of a namespace can read its secrets. If modified, existing environments will use their current namespaces until the cluster cache is cleared.')
- = link_to _('Learn more.'), help_page_path('user/project/clusters/deploy_to_cluster.md', anchor: 'custom-namespace'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('user/project/clusters/deploy_to_cluster', anchor: 'custom-namespace'), target: '_blank', rel: 'noopener noreferrer'
= field.fields_for :platform_kubernetes, @user_cluster.platform_kubernetes do |platform_kubernetes_field|
- if @user_cluster.allow_user_defined_namespace?
diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml
index 74dc2277f54..7527f32274a 100644
--- a/app/views/dashboard/_projects_head.html.haml
+++ b/app/views/dashboard/_projects_head.html.haml
@@ -1,5 +1,6 @@
-= content_for :flash_message do
- = render 'shared/project_limit'
+- if params[:personal]
+ = content_for :flash_message do
+ = render 'shared/project_limit'
.page-title-holder.gl-display-flex.gl-align-items-center
%h1.page-title.gl-font-size-h-display= _('Projects')
diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml
index 181c79e7bd0..6920ad9cd83 100644
--- a/app/views/dashboard/todos/_todo.html.haml
+++ b/app/views/dashboard/todos/_todo.html.haml
@@ -33,7 +33,7 @@
- if todo.note.present?
\:
- %span.action-name{ data: { qa_selector: "todo_action_name_content" } }<
+ %span.action-name{ data: { testid: "todo-action-name-content" } }<
- if !todo.note.present?
= todo_action_name(todo)
- unless todo.self_assigned?
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index ab97507b3c8..4f3ca9fd71b 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -39,26 +39,26 @@
.filter-item.gl-m-2
- if params[:group_id].present?
= hidden_field_tag(:group_id, params[:group_id])
- = dropdown_tag(group_dropdown_label(params[:group_id], _("Group")), options: { toggle_class: 'js-group-search js-filter-submit gl-xs-w-full!', title: s_("Todos|Filter by group"), filter: true, filterInput: 'input#group-search', dropdown_class: 'dropdown-menu-selectable dropdown-menu-group js-filter-submit', placeholder: _("Search groups"), data: { default_label: _("Group"), display: 'static', testid: 'group-dropdown' } })
+ = dropdown_tag(group_dropdown_label(params[:group_id], _("Group")), options: { toggle_class: 'js-group-search js-filter-submit gl-w-full gl-sm-w-auto', title: s_("Todos|Filter by group"), filter: true, filterInput: 'input#group-search', dropdown_class: 'dropdown-menu-selectable dropdown-menu-group js-filter-submit', placeholder: _("Search groups"), data: { default_label: _("Group"), display: 'static', testid: 'group-dropdown' } })
.filter-item.gl-m-2
- if params[:project_id].present?
= hidden_field_tag(:project_id, params[:project_id])
- = dropdown_tag(project_dropdown_label(params[:project_id], _("Project")), options: { toggle_class: 'js-project-search js-filter-submit gl-xs-w-full!', title: s_("Todos|Filter by project"), filter: true, filterInput: 'input#project-search', dropdown_class: 'dropdown-menu-selectable dropdown-menu-project js-filter-submit', placeholder: _("Search projects"), data: { default_label: _("Project"), display: 'static' } })
+ = dropdown_tag(project_dropdown_label(params[:project_id], _("Project")), options: { toggle_class: 'js-project-search js-filter-submit gl-w-full gl-sm-w-auto', title: s_("Todos|Filter by project"), filter: true, filterInput: 'input#project-search', dropdown_class: 'dropdown-menu-selectable dropdown-menu-project js-filter-submit', placeholder: _("Search projects"), data: { default_label: _("Project"), display: 'static' } })
.filter-item.gl-m-2
- if params[:author_id].present?
= hidden_field_tag(:author_id, params[:author_id])
- = dropdown_tag(user_dropdown_label(params[:author_id], _("Author")), options: { toggle_class: 'js-user-search js-filter-submit js-author-search gl-xs-w-full!', title: s_("Todos|Filter by author"), filter: true, filterInput: 'input#author-search', dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author js-filter-submit', placeholder: _("Search authors"), data: { any_user: _("Any Author"), first_user: (current_user.username if current_user), project_id: (@project.id if @project), selected: params[:author_id], field_name: 'author_id', default_label: _("Author"), todo_filter: true, todo_state_filter: params[:state] || 'pending' } })
+ = dropdown_tag(user_dropdown_label(params[:author_id], _("Author")), options: { toggle_class: 'js-user-search js-filter-submit js-author-search gl-w-full gl-sm-w-auto', title: s_("Todos|Filter by author"), filter: true, filterInput: 'input#author-search', dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author js-filter-submit', placeholder: _("Search authors"), data: { any_user: _("Any Author"), first_user: (current_user.username if current_user), project_id: (@project.id if @project), selected: params[:author_id], field_name: 'author_id', default_label: _("Author"), todo_filter: true, todo_state_filter: params[:state] || 'pending' } })
.filter-item.gl-m-2
- if params[:type].present?
= hidden_field_tag(:type, params[:type])
- = dropdown_tag(todo_types_dropdown_label(params[:type], _("Type")), options: { toggle_class: 'js-type-search js-filter-submit gl-xs-w-full!', dropdown_class: 'dropdown-menu-selectable dropdown-menu-type js-filter-submit', data: { data: todo_types_options, default_label: _("Type") } })
+ = dropdown_tag(todo_types_dropdown_label(params[:type], _("Type")), options: { toggle_class: 'js-type-search js-filter-submit gl-w-full gl-sm-w-auto', dropdown_class: 'dropdown-menu-selectable dropdown-menu-type js-filter-submit', data: { data: todo_types_options, default_label: _("Type") } })
.filter-item.actions-filter.gl-m-2
- if params[:action_id].present?
= hidden_field_tag(:action_id, params[:action_id])
- = dropdown_tag(todo_actions_dropdown_label(params[:action_id], _("Action")), options: { toggle_class: 'js-action-search js-filter-submit gl-xs-w-full!', dropdown_class: 'dropdown-menu-selectable dropdown-menu-action js-filter-submit', data: { data: todo_actions_options, default_label: _("Action") } })
+ = dropdown_tag(todo_actions_dropdown_label(params[:action_id], _("Action")), options: { toggle_class: 'js-action-search js-filter-submit gl-w-full gl-sm-w-auto', dropdown_class: 'dropdown-menu-selectable dropdown-menu-action js-filter-submit', data: { data: todo_actions_options, default_label: _("Action") } })
.filter-item.sort-filter.gl-my-2
.dropdown
- %button.dropdown-menu-toggle.dropdown-menu-toggle-sort{ type: 'button', class: 'gl-xs-w-full!', 'data-toggle' => 'dropdown' }
+ %button.dropdown-menu-toggle.dropdown-menu-toggle-sort{ type: 'button', class: 'gl-w-full gl-sm-w-auto', 'data-toggle' => 'dropdown' }
%span.light
- if @sort.present?
= sort_options_hash[@sort]
@@ -78,12 +78,12 @@
.row.js-todos-all
- if @allowed_todos.any?
- .col.js-todos-list-container{ data: { qa_selector: "todos_list_container" } }
+ .col.js-todos-list-container{ data: { testid: "todos-list-container" } }
.js-todos-options{ data: { per_page: @allowed_todos.count, current_page: @todos.current_page, total_pages: @todos.total_pages } }
%ul.content-list.todos-list
= render @allowed_todos
= paginate @todos, theme: "gitlab"
- .js-nothing-here-container.gl-empty-state.gl-text-center.hidden
+ .col.js-nothing-here-container.gl-empty-state.gl-text-center.hidden
.svg-content.svg-150
= image_tag 'illustrations/empty-todos-all-done-md.svg'
.text-content.gl-text-center
diff --git a/app/views/devise/shared/_sign_in_link.html.haml b/app/views/devise/shared/_sign_in_link.html.haml
index a1d10898c5b..a9f24e42d0b 100644
--- a/app/views/devise/shared/_sign_in_link.html.haml
+++ b/app/views/devise/shared/_sign_in_link.html.haml
@@ -1,4 +1,4 @@
-%p.text-center
+%p{ class: local_assigns.fetch(:wrapper_class, 'gl-text-center') }
%span.light
= _('Already have an account?')
- path_params = { redirect_to_referer: 'yes' }
diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml
index bf1b604465b..fb60b8c08eb 100644
--- a/app/views/devise/shared/_signup_box.html.haml
+++ b/app/views/devise/shared/_signup_box.html.haml
@@ -1,77 +1,10 @@
-- max_first_name_length = max_last_name_length = 127
- borderless ||= false
-- form_resource_name = "new_#{resource_name}"
.gl-mb-3.gl-p-4{ class: (borderless ? '' : 'gl-border-gray-100 gl-border-1 gl-border-solid gl-rounded-base') }
= yield :omniauth_providers_top if show_omniauth_providers
- = gitlab_ui_form_for(resource, as: form_resource_name, url: url, html: { class: 'gl-show-field-errors js-arkose-labs-form', aria: { live: 'assertive' }}, data: { testid: 'signup-form' }) do |f|
- .devise-errors
- = render 'devise/shared/error_messages', resource: resource
- - if Gitlab::CurrentSettings.invisible_captcha_enabled
- = invisible_captcha nonce: true, autocomplete: SecureRandom.alphanumeric(12)
- .name.form-row
- .col.form-group
- = f.label :first_name, _('First name'), for: 'new_user_first_name'
- = f.text_field :first_name,
- class: 'form-control gl-form-input top js-block-emoji js-validate-length',
- data: { max_length: max_first_name_length,
- max_length_message: s_('SignUp|First name is too long (maximum is %{max_length} characters).') % { max_length: max_first_name_length },
- testid: 'new-user-first-name-field' },
- required: true,
- title: _('This field is required.')
- .col.form-group
- = f.label :last_name, _('Last name'), for: 'new_user_last_name'
- = f.text_field :last_name,
- class: 'form-control gl-form-input top js-block-emoji js-validate-length',
- data: { max_length: max_last_name_length,
- max_length_message: s_('SignUp|Last name is too long (maximum is %{max_length} characters).') % { max_length: max_last_name_length },
- testid: 'new-user-last-name-field' },
- required: true,
- title: _('This field is required.')
- .username.form-group
- = f.label :username, _('Username')
- = f.text_field :username,
- class: 'form-control gl-form-input middle js-block-emoji js-validate-length js-validate-username',
- data: signup_username_data_attributes,
- pattern: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS,
- required: true,
- title: _('Please create a username with only alphanumeric characters.')
- %p.validation-error.gl-text-red-500.gl-field-error-ignore.gl-mt-2.field-validation.hide= _('Username is already taken.')
- %p.validation-success.gl-text-green-600.gl-field-error-ignore.gl-mt-2.field-validation.hide= _('Username is available.')
- %p.validation-pending.gl-field-error-ignore.gl-mt-2.field-validation.hide= _('Checking username availability...')
- .form-group
- = f.label :email, _('Email')
- = f.email_field :email,
- class: 'form-control gl-form-input middle js-validate-email',
- data: { testid: 'new-user-email-field' },
- required: true,
- title: _('Please provide a valid email address.')
- %p.validation-hint.gl-field-hint.text-secondary= _('We recommend a work email address.')
- %p.validation-warning.gl-field-error-ignore.text-secondary.hide= _('This email address does not look right, are you sure you typed it correctly?')
- -# This is used for providing entry to Jihu on email verification
- = render_if_exists 'devise/shared/signup_email_additional_info'
- .form-group.gl-mb-5
- = f.label :password, _('Password')
- %input.form-control.gl-form-input.js-password{ data: { id: "#{form_resource_name}_password",
- title: s_('SignUp|Minimum length is %{minimum_password_length} characters.') % { minimum_password_length: @minimum_password_length },
- minimum_password_length: @minimum_password_length,
- testid: 'new-user-password-field',
- autocomplete: 'new-password',
- name: "#{form_resource_name}[password]" } }
- %p.gl-field-hint-valid.text-secondary= s_('SignUp|Minimum length is %{minimum_password_length} characters.') % { minimum_password_length: @minimum_password_length }
- = render_if_exists 'shared/password_requirements_list'
- = render_if_exists 'devise/shared/phone_verification', form: f
+ = render 'devise/shared/signup_box_form',
+ button_text: button_text,
+ url: url,
+ show_omniauth_providers: omniauth_enabled? && button_based_providers_enabled?
- .form-group
- - if arkose_labs_enabled?
- = render_if_exists 'devise/registrations/arkose_labs'
- - elsif show_recaptcha_sign_up?
- = recaptcha_tags nonce: content_security_policy_nonce
-
- = render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm, block: true, button_options: { data: { testid: 'new-user-register-button' }}) do
- = button_text
-
- = render 'devise/shared/terms_of_service_notice', button_text: button_text
-
- = yield :omniauth_providers_bottom if show_omniauth_providers
diff --git a/app/views/devise/shared/_signup_box_form.html.haml b/app/views/devise/shared/_signup_box_form.html.haml
new file mode 100644
index 00000000000..246036b72e1
--- /dev/null
+++ b/app/views/devise/shared/_signup_box_form.html.haml
@@ -0,0 +1,73 @@
+- max_first_name_length = max_last_name_length = 127
+- form_resource_name = "new_#{resource_name}"
+
+= gitlab_ui_form_for(resource, as: form_resource_name, url: url, html: { class: 'gl-show-field-errors js-arkose-labs-form', aria: { live: 'assertive' }}, data: { testid: 'signup-form' }) do |f|
+ .devise-errors
+ = render 'devise/shared/error_messages', resource: resource
+ - if Gitlab::CurrentSettings.invisible_captcha_enabled
+ = invisible_captcha nonce: true, autocomplete: SecureRandom.alphanumeric(12)
+ .name.form-row
+ .col.form-group
+ = f.label :first_name, _('First name'), for: 'new_user_first_name'
+ = f.text_field :first_name,
+ class: 'form-control gl-form-input top js-block-emoji js-validate-length',
+ data: { max_length: max_first_name_length,
+ max_length_message: s_('SignUp|First name is too long (maximum is %{max_length} characters).') % { max_length: max_first_name_length },
+ testid: 'new-user-first-name-field' },
+ required: true,
+ title: _('This field is required.')
+ .col.form-group
+ = f.label :last_name, _('Last name'), for: 'new_user_last_name'
+ = f.text_field :last_name,
+ class: 'form-control gl-form-input top js-block-emoji js-validate-length',
+ data: { max_length: max_last_name_length,
+ max_length_message: s_('SignUp|Last name is too long (maximum is %{max_length} characters).') % { max_length: max_last_name_length },
+ testid: 'new-user-last-name-field' },
+ required: true,
+ title: _('This field is required.')
+ .username.form-group
+ = f.label :username, _('Username')
+ = f.text_field :username,
+ class: 'form-control gl-form-input middle js-block-emoji js-validate-length js-validate-username',
+ data: signup_username_data_attributes,
+ pattern: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS,
+ required: true,
+ title: _('Please create a username with only alphanumeric characters.')
+ %p.validation-error.gl-text-red-500.gl-field-error-ignore.gl-mt-2.field-validation.hide= _('Username is already taken.')
+ %p.validation-success.gl-text-green-600.gl-field-error-ignore.gl-mt-2.field-validation.hide= _('Username is available.')
+ %p.validation-pending.gl-field-error-ignore.gl-mt-2.field-validation.hide= _('Checking username availability...')
+ .form-group
+ = f.label :email, _('Email')
+ = f.email_field :email,
+ class: 'form-control gl-form-input middle js-validate-email',
+ data: { testid: 'new-user-email-field' },
+ required: true,
+ title: _('Please provide a valid email address.')
+ %p.validation-hint.gl-field-hint.text-secondary= _('We recommend a work email address.')
+ %p.validation-warning.gl-field-error-ignore.text-secondary.hide= _('This email address does not look right, are you sure you typed it correctly?')
+ -# This is used for providing entry to Jihu on email verification
+ = render_if_exists 'devise/shared/signup_email_additional_info'
+ .form-group.gl-mb-5
+ = f.label :password, _('Password')
+ %input.form-control.gl-form-input.js-password{ data: { id: "#{form_resource_name}_password",
+ title: s_('SignUp|Minimum length is %{minimum_password_length} characters.') % { minimum_password_length: @minimum_password_length },
+ minimum_password_length: @minimum_password_length,
+ testid: 'new-user-password-field',
+ autocomplete: 'new-password',
+ name: "#{form_resource_name}[password]" } }
+ %p.gl-field-hint-valid.text-secondary= s_('SignUp|Minimum length is %{minimum_password_length} characters.') % { minimum_password_length: @minimum_password_length }
+ = render_if_exists 'shared/password_requirements_list'
+ = render_if_exists 'devise/shared/phone_verification', form: f
+
+ .form-group
+ - if arkose_labs_enabled?
+ = render_if_exists 'devise/registrations/arkose_labs'
+ - elsif show_recaptcha_sign_up?
+ = recaptcha_tags nonce: content_security_policy_nonce
+
+ = render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm, block: true, button_options: { data: { testid: 'new-user-register-button' }}) do
+ = button_text
+
+ = render 'devise/shared/terms_of_service_notice', button_text: button_text
+
+= yield :omniauth_providers_bottom if show_omniauth_providers
diff --git a/app/views/devise/shared/_signup_omniauth_provider_list.haml b/app/views/devise/shared/_signup_omniauth_provider_list.haml
index e8c82e456ae..b9efcaa11b4 100644
--- a/app/views/devise/shared/_signup_omniauth_provider_list.haml
+++ b/app/views/devise/shared/_signup_omniauth_provider_list.haml
@@ -14,7 +14,10 @@
= _("Create an account using:")
.gl-display-flex.gl-justify-content-between.gl-flex-wrap
- providers.each do |provider|
- = button_to omniauth_authorize_path(:user, provider, register_omniauth_params(local_assigns)), class: "btn gl-button btn-default gl-w-full gl-mb-4 js-oauth-login #{qa_selector_for_provider(provider)}", data: { provider: provider, track_action: "#{provider}_sso", track_label: tracking_label }, id: "oauth-login-#{provider}" do
+ = button_to omniauth_authorize_path(:user, provider, register_omniauth_params(local_assigns)),
+ class: "btn gl-button btn-default gl-w-full gl-mb-4 js-oauth-login #{qa_selector_for_provider(provider)}",
+ data: { provider: provider, track_action: "#{provider}_sso", track_label: tracking_label },
+ id: "oauth-login-#{provider}" do
- if provider_has_icon?(provider)
= provider_image_tag(provider)
%span.gl-button-text
diff --git a/app/views/discussions/_notes.html.haml b/app/views/discussions/_notes.html.haml
index e34a5cebe78..5e6ebe87808 100644
--- a/app/views/discussions/_notes.html.haml
+++ b/app/views/discussions/_notes.html.haml
@@ -20,7 +20,7 @@
.discussion-with-resolve-btn
= link_to_reply_discussion(discussion)
- elsif !current_user
- .disabled-comment.text-center
+ .disabled-comment.gl-text-center.gl-text-secondary
Please
= link_to "register", new_session_path(:user, redirect_to_referer: 'yes')
or
diff --git a/app/views/events/_event.html.haml b/app/views/events/_event.html.haml
index 83f7d743755..c28fe7c8330 100644
--- a/app/views/events/_event.html.haml
+++ b/app/views/events/_event.html.haml
@@ -1,8 +1,8 @@
- event = event.present
- if event.visible_to_user?(current_user)
- .event-item
- .event-item-timestamp
+ .event-item{ class: current_path?('users#activity') ? 'user-profile-activity gl-border-bottom-0 gl-pl-7! gl-pb-3' : '' }
+ .event-item-timestamp.gl-font-sm
#{time_ago_with_tooltip(event.created_at)}
- if event.wiki_page?
diff --git a/app/views/events/_event_scope.html.haml b/app/views/events/_event_scope.html.haml
index 67e4c538b4a..f3e3a304cfd 100644
--- a/app/views/events/_event_scope.html.haml
+++ b/app/views/events/_event_scope.html.haml
@@ -1,4 +1,4 @@
-%span.event-scope
+%span.event-scope.gl-text-truncate
= event_preposition(event)
- if event.project
= link_to_project(event.project)
diff --git a/app/views/events/event/_common.html.haml b/app/views/events/event/_common.html.haml
index 7ef3461a7fb..78ce24c429a 100644
--- a/app/views/events/event/_common.html.haml
+++ b/app/views/events/event/_common.html.haml
@@ -5,9 +5,9 @@
.event-title.d-flex.flex-wrap
= inline_event_icon(event)
- if event.target
- %span.event-type.d-inline-block.gl-mr-2{ class: event.action_name }
+ %span.event-type.d-inline-block.gl-mr-2{ class: event.action_name + user_profile_activity_classes }
= localized_action_name(event)
- %span.event-target-type.gl-mr-2= event.target_type_name
+ %span.event-target-type.gl-mr-2{ class: user_profile_activity_classes }= event.target_type_name
= link_to event_target_path(event), class: 'has-tooltip event-target-link gl-mr-2', title: event.target_title do
= event.target.reference_link_text
- unless event.milestone?
diff --git a/app/views/events/event/_created_project.html.haml b/app/views/events/event/_created_project.html.haml
index f0bb07d062c..390c9ec6c89 100644
--- a/app/views/events/event/_created_project.html.haml
+++ b/app/views/events/event/_created_project.html.haml
@@ -4,7 +4,7 @@
.event-title.d-flex.flex-wrap
= inline_event_icon(event)
- %span.event-type.d-inline-block.gl-mr-2{ class: event.action_name }
+ %span.event-type.d-inline-block.gl-mr-2{ class: event.action_name + user_profile_activity_classes }
= event_action_name(event)
- if event.project
diff --git a/app/views/events/event/_design.html.haml b/app/views/events/event/_design.html.haml
index c1fa1aaca50..945c7465ea8 100644
--- a/app/views/events/event/_design.html.haml
+++ b/app/views/events/event/_design.html.haml
@@ -4,7 +4,7 @@
.event-title.d-flex.flex-wrap
= inline_event_icon(event)
- %span.event-type.d-inline-block.gl-mr-2{ class: event.action_name }
+ %span.event-type.d-inline-block.gl-mr-2{ class: event.action_name + user_profile_activity_classes }
= event.action_name
= event_design_title_html(event)
= render "events/event_scope", event: event
diff --git a/app/views/events/event/_note.html.haml b/app/views/events/event/_note.html.haml
index 53c59474d83..5bbece84e40 100644
--- a/app/views/events/event/_note.html.haml
+++ b/app/views/events/event/_note.html.haml
@@ -6,7 +6,7 @@
.event-title.d-flex.flex-wrap
= inline_event_icon(event)
- %span.event-type.d-inline-block.gl-mr-2{ class: event.action_name }
+ %span.event-type.d-inline-block.gl-mr-2{ class: event.action_name + user_profile_activity_classes }
= event.action_name
= event_note_title_html(event)
- title = note_target_title(event.target)
diff --git a/app/views/events/event/_private.html.haml b/app/views/events/event/_private.html.haml
index d91f30c07cb..5e9d6da3996 100644
--- a/app/views/events/event/_private.html.haml
+++ b/app/views/events/event/_private.html.haml
@@ -1,8 +1,8 @@
-.event-item
- .event-item-timestamp
+.event-item{ class: current_path?('users#activity') ? 'user-profile-activity gl-border-bottom-0 gl-pl-7! gl-pb-3' : '' }
+ .event-item-timestamp.gl-font-sm
= time_ago_with_tooltip(event.created_at)
- .system-note-image= sprite_icon('eye-slash', size: 24, css_class: 'icon')
+ .system-note-image.gl-rounded-full.gl-bg-gray-50.gl-line-height-0= sprite_icon('eye-slash', size: 14, css_class: 'icon')
= event_user_info(event)
diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml
index 0ad969116e0..ff7983a9ba4 100644
--- a/app/views/events/event/_push.html.haml
+++ b/app/views/events/event/_push.html.haml
@@ -6,7 +6,7 @@
.event-title.d-flex.flex-wrap
= inline_event_icon(event)
- %span.event-type.d-inline-block.gl-mr-2.pushed= event.push_activity_description
+ %span.event-type.d-inline-block.gl-mr-2.pushed{ class: user_profile_activity_classes }= event.push_activity_description
- unless event.batch_push?
%span.gl-mr-2.text-truncate
- commits_link = project_commits_path(project, event.ref_name)
diff --git a/app/views/events/event/_wiki.html.haml b/app/views/events/event/_wiki.html.haml
index cbd5ebcae12..a48c34f80d8 100644
--- a/app/views/events/event/_wiki.html.haml
+++ b/app/views/events/event/_wiki.html.haml
@@ -4,7 +4,7 @@
.event-title.d-flex.flex-wrap
= inline_event_icon(event)
- %span.event-type.d-inline-block.gl-mr-2{ class: event.action_name }
+ %span.event-type.d-inline-block.gl-mr-2{ class: event.action_name + user_profile_activity_classes }
= event.action_name
= event_wiki_title_html(event)
= render "events/event_scope", event: event
diff --git a/app/views/explore/catalog/show.html.haml b/app/views/explore/catalog/show.html.haml
new file mode 100644
index 00000000000..7c8d788f8e3
--- /dev/null
+++ b/app/views/explore/catalog/show.html.haml
@@ -0,0 +1,3 @@
+- page_title _('CI/CD Catalog')
+
+#js-ci-cd-catalog{ data: { ci_catalog_path: explore_catalog_index_path } }
diff --git a/app/views/external_redirect/external_redirect/index.html.haml b/app/views/external_redirect/external_redirect/index.html.haml
new file mode 100644
index 00000000000..36bf98cba02
--- /dev/null
+++ b/app/views/external_redirect/external_redirect/index.html.haml
@@ -0,0 +1,12 @@
+- add_page_specific_style 'page_bundles/external_redirect'
+- page_title _("You're about to leave GitLab")
+
+- url = local_assigns.fetch(:url)
+
+.gl-max-w-62.gl-h-full.gl-display-flex.gl-justify-content-center.gl-align-items-center.gl-flex-direction-column.gl-mr-auto.gl-ml-auto.gl-px-3
+ = sprite_icon('warning', size: 48, css_class: 'gl-text-orange-300')
+ %h3.gl-mt-6= _("You're about to leave GitLab")
+ %p= safe_format(_('This link will redirect you to %{url}. If this URL looks wrong, please go back or close this window. Do you want to continue?'), url: content_tag(:code, url))
+ .gl-display-flex.gl-justify-content-center.gl-w-full
+ = render Pajamas::ButtonComponent.new(variant: :default, href: url) do
+ = _("Proceed")
diff --git a/app/views/groups/_home_panel.html.haml b/app/views/groups/_home_panel.html.haml
index 544acd5ae56..269a7309ec2 100644
--- a/app/views/groups/_home_panel.html.haml
+++ b/app/views/groups/_home_panel.html.haml
@@ -3,7 +3,7 @@
- emails_disabled = @group.emails_disabled?
.group-home-panel
- .gl-display-flex.gl-justify-content-space-between.gl-flex-wrap.gl-sm-flex-direction-column.gl-gap-3.gl-my-5
+ .gl-display-flex.gl-justify-content-space-between.gl-flex-wrap.gl-flex-direction-column.gl-md-flex-direction-row.gl-gap-3.gl-my-5
.home-panel-title-row.gl-display-flex.gl-align-items-center
.avatar-container.rect-avatar.s64.home-panel-avatar.gl-flex-shrink-0.float-none{ class: 'gl-mr-3!' }
= group_icon(@group, class: 'avatar avatar-tile s64', width: 64, height: 64, itemprop: 'logo')
diff --git a/app/views/groups/_import_group_from_another_instance_panel.html.haml b/app/views/groups/_import_group_from_another_instance_panel.html.haml
index c35bbce6ba7..6c5a27e68c4 100644
--- a/app/views/groups/_import_group_from_another_instance_panel.html.haml
+++ b/app/views/groups/_import_group_from_another_instance_panel.html.haml
@@ -24,7 +24,7 @@
= render Pajamas::AlertComponent.new(dismissible: false,
variant: :warning) do |c|
- c.with_body do
- - docs_link = link_to('', help_page_path('user/group/import/index.md', anchor: 'migrated-group-items'), target: '_blank', rel: 'noopener noreferrer')
+ - docs_link = link_to('', help_page_path('user/group/import/index', anchor: 'migrated-group-items'), target: '_blank', rel: 'noopener noreferrer')
= safe_format(s_('GroupsNew|Not all group items are migrated. %{docs_link_start}What items are migrated%{docs_link_end}?'), tag_pair(docs_link, :docs_link_start, :docs_link_end))
%p.gl-mt-3
@@ -37,12 +37,12 @@
id: 'import_gitlab_url',
data: { testid: 'import-gitlab-url' }
.form-group.gl-display-flex.gl-flex-direction-column
- = f.label :bulk_import_gitlab_access_token, s_('GroupsNew|Personal access token'), for: 'import_gitlab_token'
+ = f.label :bulk_import_gitlab_access_token, s_('GroupsNew|Personal access token'), for: 'import_gitlab_token', class: 'col-form-label'
.gl-font-weight-normal
- pat_link = link_to('', help_page_path('user/profile/personal_access_tokens'), target: '_blank')
- short_living_link = link_to('', help_page_path('security/token_overview', anchor: 'security-considerations'), target: '_blank')
= safe_format(s_('GroupsNew|Create a token with %{code_start}api%{code_end} and %{code_start}read_repository%{code_end} scopes in the %{pat_link_start}user settings%{pat_link_end} of the source GitLab instance. For %{short_living_link_start}security reasons%{short_living_link_end}, set a short expiration date for the token. Keep in mind that large migrations take more time.'), tag_pair('<code></code>'.html_safe, :code_start , :code_end), tag_pair(pat_link, :pat_link_start, :pat_link_end), tag_pair(short_living_link, :short_living_link_start, :short_living_link_end))
- = f.text_field :bulk_import_gitlab_access_token, placeholder: s_('GroupsNew|e.g. h8d3f016698e...'), class: 'gl-form-input gl-mt-3 col-xs-12 col-sm-8',
+ = f.password_field :bulk_import_gitlab_access_token, placeholder: s_('GroupsNew|e.g. h8d3f016698e...'), class: 'gl-form-input gl-mt-3 col-xs-12 col-sm-8',
required: true,
disabled: bulk_imports_disabled,
autocomplete: 'off',
diff --git a/app/views/groups/_import_group_from_file_panel.html.haml b/app/views/groups/_import_group_from_file_panel.html.haml
index e3d54e52aab..c39f5cf87c7 100644
--- a/app/views/groups/_import_group_from_file_panel.html.haml
+++ b/app/views/groups/_import_group_from_file_panel.html.haml
@@ -10,7 +10,7 @@
alert_options: { class: 'gl-mb-5' },
dismissible: false) do |c|
- c.with_body do
- - docs_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/group/import/index.md', anchor: 'migrate-groups-by-direct-transfer-recommended') }
+ - docs_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/group/import/index', anchor: 'migrate-groups-by-direct-transfer-recommended') }
- link_end = '</a>'.html_safe
= s_('GroupsNew|This feature is deprecated and replaced by group migration by direct transfer. %{docs_link_start}Learn more%{docs_link_end}.').html_safe % { docs_link_start: docs_link_start, docs_link_end: link_end }
= render 'shared/groups/group_name_and_path_fields', f: f
diff --git a/app/views/groups/_invite_members_modal.html.haml b/app/views/groups/_invite_members_modal.html.haml
index cd3327ba9ec..d53190948fd 100644
--- a/app/views/groups/_invite_members_modal.html.haml
+++ b/app/views/groups/_invite_members_modal.html.haml
@@ -3,4 +3,8 @@
.js-invite-members-modal{ data: { is_project: 'false',
access_levels: group.access_level_roles.to_json,
reload_page_on_submit: current_path?('group_members#index').to_s,
- help_link: help_page_url('user/permissions') }.merge(common_invite_modal_dataset(group)).merge(users_filter_data(group)) }
+ help_link: help_page_url('user/permissions'),
+ is_signup_enabled: signup_enabled?.to_s,
+ new_users_url: new_admin_user_url,
+ is_current_user_admin: current_user&.admin?.to_s,
+ }.merge(common_invite_modal_dataset(group)).merge(users_filter_data(group)) }
diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml
index 76758769d01..2f2edec2d80 100644
--- a/app/views/groups/projects.html.haml
+++ b/app/views/groups/projects.html.haml
@@ -17,15 +17,15 @@
= _("New project")
- c.with_body do
%ul.content-list{ class: 'gl-px-3!' }
- - @projects.each_with_index do |project, idx|
- %li.project-row.gl-align-items-center{ class: 'gl-display-flex!', data: { qa_selector: 'project_row_container', qa_index: idx } }
+ - @projects.each do |project|
+ %li.project-row.gl-align-items-center{ class: 'gl-display-flex!' }
.avatar-container.rect-avatar.s40.gl-flex-shrink-0
= project_icon(project, alt: '', class: 'avatar project-avatar s40', width: 40, height: 40)
.gl-min-w-0.gl-flex-grow-1
.title
= link_to project_path(project), class: 'js-prefetch-document' do
- %span.project-full-name{ data: { qa_selector: 'project_fullname_content' } }
- %span.namespace-name{ data: { qa_selector: 'project_namespace_content' } }
+ %span.project-full-name
+ %span.namespace-name
- if project.namespace
= project.namespace.human_name
\/
@@ -43,13 +43,12 @@
.controls.gl-flex-shrink-0.gl-ml-5
= render Pajamas::ButtonComponent.new(href: project_project_members_path(project),
variant: :link,
- button_options: { class: 'gl-mr-2', data: { qa_selector: 'project_members_button' } }) do
+ button_options: { class: 'gl-mr-2' }) do
= _('View members')
= render Pajamas::ButtonComponent.new(href: edit_project_path(project),
- size: :small,
- button_options: { data: { qa_selector: 'project_edit_button' } }) do
+ size: :small) do
= _('Edit')
- = render 'delete_project_button', project: project, data: { qa_selector: 'project_delete_button' }
+ = render 'delete_project_button', project: project
- if @projects.blank?
.nothing-here-block= _("This group has no projects yet")
diff --git a/app/views/groups/settings/_export.html.haml b/app/views/groups/settings/_export.html.haml
index 8eb9f8fc5f1..059426fd596 100644
--- a/app/views/groups/settings/_export.html.haml
+++ b/app/views/groups/settings/_export.html.haml
@@ -31,8 +31,8 @@
- if group.export_file_exists?
= render Pajamas::ButtonComponent.new(href: download_export_group_path(group), button_options: { rel: 'nofollow', data: { method: :get, qa_selector: 'download_export_link' } }) do
= _('Download export')
- = render Pajamas::ButtonComponent.new(href: export_group_path(group), button_options: { data: { method: :post, qa_selector: 'regenerate_export_group_link' } }) do
+ = render Pajamas::ButtonComponent.new(href: export_group_path(group), button_options: { data: { method: :post } }) do
= _('Regenerate export')
- else
- = render Pajamas::ButtonComponent.new(href: export_group_path(group), button_options: { data: { method: :post, qa_selector: 'export_group_link' } }) do
+ = render Pajamas::ButtonComponent.new(href: export_group_path(group), button_options: { data: { method: :post } }) do
= _('Export group')
diff --git a/app/views/groups/settings/_git_access_protocols.html.haml b/app/views/groups/settings/_git_access_protocols.html.haml
index d23f72a3055..c9cbe56e6ec 100644
--- a/app/views/groups/settings/_git_access_protocols.html.haml
+++ b/app/views/groups/settings/_git_access_protocols.html.haml
@@ -1,7 +1,7 @@
- if group.root?
.form-group
= f.label _('Enabled Git access protocols'), class: 'label-bold'
- = f.select :enabled_git_access_protocol, options_for_select(enabled_git_access_protocol_options_for_group(group), group.enabled_git_access_protocol), {}, class: 'form-control', data: { qa_selector: 'enabled_git_access_protocol_dropdown' }, disabled: !::Gitlab::CurrentSettings.enabled_git_access_protocol.blank?
+ = f.select :enabled_git_access_protocol, options_for_select(enabled_git_access_protocol_options_for_group(group), group.enabled_git_access_protocol), {}, class: 'form-control', disabled: !::Gitlab::CurrentSettings.enabled_git_access_protocol.blank?
- if !::Gitlab::CurrentSettings.enabled_git_access_protocol.blank?
.form-text.text-muted
= _("This setting has been configured at the instance level and cannot be overridden per group")
diff --git a/app/views/groups/settings/_permissions.html.haml b/app/views/groups/settings/_permissions.html.haml
index 45fd98adbb9..8ea80700340 100644
--- a/app/views/groups/settings/_permissions.html.haml
+++ b/app/views/groups/settings/_permissions.html.haml
@@ -38,11 +38,12 @@
= render 'groups/settings/lfs', f: f
= render_if_exists 'groups/settings/code_suggestions', f: f, group: @group
= render_if_exists 'groups/settings/experimental_settings', f: f, group: @group
- = render_if_exists 'groups/settings/ai_third_party_settings', f: f, group: @group
+ = render_if_exists 'groups/settings/product_analytics_settings', f: f, group: @group
= render 'groups/settings/git_access_protocols', f: f, group: @group
= render 'groups/settings/project_creation_level', f: f, group: @group
= render 'groups/settings/subgroup_creation_level', f: f, group: @group
= render_if_exists 'groups/settings/prevent_forking', f: f, group: @group
+ = render_if_exists 'groups/settings/service_access_tokens_expiration_enforced', f: f, group: @group
= render 'groups/settings/two_factor_auth', f: f, group: @group
= render_if_exists 'groups/personal_access_token_expiration_policy', f: f, group: @group
= render 'groups/settings/membership', f: f, group: @group
diff --git a/app/views/groups/settings/_resource_access_token_creation.html.haml b/app/views/groups/settings/_resource_access_token_creation.html.haml
index d304dba3250..7d64ab84ad2 100644
--- a/app/views/groups/settings/_resource_access_token_creation.html.haml
+++ b/app/views/groups/settings/_resource_access_token_creation.html.haml
@@ -7,4 +7,4 @@
- link_start_group = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: group_access_tokens_link }
= f.gitlab_ui_checkbox_component :resource_access_token_creation_allowed,
s_('GroupSettings|Users can create %{link_start_project}project access tokens%{link_end} and %{link_start_group}group access tokens%{link_end} in this group').html_safe % { link_start_project: link_start_project, link_start_group: link_start_group, link_end: '</a>'.html_safe },
- checkbox_options: { checked: group.namespace_settings.resource_access_token_creation_allowed?, data: { qa_selector: 'resource_access_token_creation_allowed_checkbox' } }
+ checkbox_options: { checked: group.namespace_settings.resource_access_token_creation_allowed? }
diff --git a/app/views/groups/settings/ci_cd/_auto_devops_form.html.haml b/app/views/groups/settings/ci_cd/_auto_devops_form.html.haml
index b0a5d0bd4fa..705a9704fc2 100644
--- a/app/views/groups/settings/ci_cd/_auto_devops_form.html.haml
+++ b/app/views/groups/settings/ci_cd/_auto_devops_form.html.haml
@@ -4,7 +4,7 @@
.form-group
= render Pajamas::CardComponent.new(card_options: { class: 'gl-mb-3' }) do |c|
- c.with_body do
- - learn_more_link = link_to _('Learn more.'), help_page_path('topics/autodevops/index.md'), target: '_blank', rel: 'noopener noreferrer'
+ - learn_more_link = link_to _('Learn more.'), help_page_path('topics/autodevops/index'), target: '_blank', rel: 'noopener noreferrer'
- help_text = s_('GroupSettings|The Auto DevOps pipeline runs if no alternative CI configuration file is found.')
- badge = gl_badge_tag badge_for_auto_devops_scope(group), variant: :info
- label = s_('GroupSettings|Default to Auto DevOps pipeline for all projects within this group')
diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml
index b4b73e9e790..dc80aeb8a30 100644
--- a/app/views/import/bitbucket/status.html.haml
+++ b/app/views/import/bitbucket/status.html.haml
@@ -1,9 +1,9 @@
- page_title _('Bitbucket import')
- header_title _('Projects'), root_path
-%h1.page-title.gl-font-size-h-display.d-flex
+%h1.page-title.gl-font-size-h-display.d-flex.gl-align-items-center
.gl-display-flex.gl-align-items-center.gl-justify-content-center
- = sprite_icon('bitbucket', css_class: 'gl-mr-2')
+ = sprite_icon('bitbucket', css_class: 'gl-mr-3', size: 48)
= _('Import projects from Bitbucket')
= render 'import/githubish_status', provider: 'bitbucket', default_namespace: @namespace
diff --git a/app/views/import/bitbucket_server/new.html.haml b/app/views/import/bitbucket_server/new.html.haml
index de94f142a40..583d312154c 100644
--- a/app/views/import/bitbucket_server/new.html.haml
+++ b/app/views/import/bitbucket_server/new.html.haml
@@ -2,10 +2,11 @@
- header_title _("New project"), new_project_path
- add_to_breadcrumbs s_('ProjectsNew|Import project'), new_project_path(anchor: 'import_project')
-%h1.page-title.gl-font-size-h-display.d-flex
+%h1.page-title.gl-font-size-h-display.d-flex.gl-align-items-center
.gl-display-flex.gl-align-items-center.gl-justify-content-center
- = sprite_icon('bitbucket', css_class: 'gl-mr-2')
+ = sprite_icon('bitbucket', css_class: 'gl-mr-3', size: 48)
= _('Import repositories from Bitbucket Server')
+%hr
%p
= _('Enter in your Bitbucket Server URL and personal access token below')
diff --git a/app/views/import/bitbucket_server/status.html.haml b/app/views/import/bitbucket_server/status.html.haml
index 7e0c7b3dd74..6994404c8c9 100644
--- a/app/views/import/bitbucket_server/status.html.haml
+++ b/app/views/import/bitbucket_server/status.html.haml
@@ -1,8 +1,8 @@
- page_title _('Bitbucket Server import')
-%h1.page-title.gl-font-size-h-display.d-flex
+%h1.page-title.gl-font-size-h-display.d-flex.gl-align-items-center
.gl-display-flex.gl-align-items-center.gl-justify-content-center
- = sprite_icon('bitbucket', css_class: 'gl-mr-2')
+ = sprite_icon('bitbucket', css_class: 'gl-mr-3', size: 48)
= _('Import projects from Bitbucket Server')
= render 'import/githubish_status', provider: 'bitbucket_server', paginatable: true, default_namespace: @namespace, extra_data: { reconfigure_path: configure_import_bitbucket_server_path }
diff --git a/app/views/import/bulk_imports/details.html.haml b/app/views/import/bulk_imports/details.html.haml
new file mode 100644
index 00000000000..511bf2c38a1
--- /dev/null
+++ b/app/views/import/bulk_imports/details.html.haml
@@ -0,0 +1,5 @@
+- add_to_breadcrumbs _('New group'), new_group_path
+- add_to_breadcrumbs _('Import group'), new_group_path(anchor: 'import-group-pane')
+- page_title s_('Import|GitLab Migration details')
+
+.js-bulk-import-details
diff --git a/app/views/import/bulk_imports/history.html.haml b/app/views/import/bulk_imports/history.html.haml
index 38196f97030..57e3e60a702 100644
--- a/app/views/import/bulk_imports/history.html.haml
+++ b/app/views/import/bulk_imports/history.html.haml
@@ -3,4 +3,4 @@
- add_page_specific_style 'page_bundles/import'
- page_title _('Import history')
-#import-history-mount-element{ data: { realtime_changes_path: realtime_changes_import_bulk_imports_path(format: :json) } }
+#import-history-mount-element{ data: { details_path: details_import_bulk_imports_path, realtime_changes_path: realtime_changes_import_bulk_imports_path(format: :json) } }
diff --git a/app/views/import/fogbugz/new.html.haml b/app/views/import/fogbugz/new.html.haml
index 2edd9cd5592..001f6588405 100644
--- a/app/views/import/fogbugz/new.html.haml
+++ b/app/views/import/fogbugz/new.html.haml
@@ -2,9 +2,9 @@
- header_title _("New project"), new_project_path
- add_to_breadcrumbs s_('ProjectsNew|Import project'), new_project_path(anchor: 'import_project')
-%h1.page-title.gl-font-size-h-display.d-flex
+%h1.page-title.gl-font-size-h-display.d-flex.gl-align-items-center
.gl-display-flex.gl-align-items-center.gl-justify-content-center
- = sprite_icon('bug', css_class: 'gl-mr-2')
+ = sprite_icon('bug', css_class: 'gl-mr-3', size: 48)
= _('Import projects from FogBugz')
%hr
diff --git a/app/views/import/fogbugz/status.html.haml b/app/views/import/fogbugz/status.html.haml
index fb05e8e9724..7512e3d3935 100644
--- a/app/views/import/fogbugz/status.html.haml
+++ b/app/views/import/fogbugz/status.html.haml
@@ -1,7 +1,7 @@
- page_title _("FogBugz import")
-%h1.page-title.gl-font-size-h-display.d-flex
+%h1.page-title.gl-font-size-h-display.d-flex.gl-align-items-center
.gl-display-flex.gl-align-items-center.gl-justify-content-center
- = sprite_icon('bug', css_class: 'gl-mr-2')
+ = sprite_icon('bug', css_class: 'gl-mr-3', size: 48)
= _('Import projects from FogBugz')
%p.light
diff --git a/app/views/import/gitea/new.html.haml b/app/views/import/gitea/new.html.haml
index f76e9f3f6ed..dcee0c473a1 100644
--- a/app/views/import/gitea/new.html.haml
+++ b/app/views/import/gitea/new.html.haml
@@ -2,9 +2,11 @@
- header_title _("New project"), new_project_path
- add_to_breadcrumbs s_('ProjectsNew|Import project'), new_project_path(anchor: 'import_project')
-%h1.page-title.gl-font-size-h-display
- = custom_icon('gitea_logo')
+%h1.page-title.gl-font-size-h-display.d-flex.gl-align-items-center
+ .gl-display-flex.gl-align-items-center.gl-justify-content-center
+ = sprite_icon('gitea', css_class: 'gl-mr-3', size: 48)
= _('Import projects from Gitea')
+%hr
%p
- link_to_personal_token = link_to(_('personal access token'), 'https://docs.gitea.io/en-us/api-usage/#authentication-via-the-api')
@@ -17,9 +19,9 @@
.col-sm-4
= text_field_tag :gitea_host_url, nil, placeholder: 'https://gitea.com', class: 'form-control gl-form-input'
.form-group.row
- = label_tag :personal_access_token, _('Personal access token'), class: 'col-form-label col-sm-2'
+ = label_tag :personal_access_token, _('Personal access token'), for: :personal_access_token, class: 'col-form-label col-sm-2'
.col-sm-4
- = text_field_tag :personal_access_token, nil, class: 'form-control gl-form-input'
+ = password_field_tag :personal_access_token, nil, class: 'form-control gl-form-input'
.form-actions
= render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm) do
= _('List your Gitea repositories')
diff --git a/app/views/import/gitea/status.html.haml b/app/views/import/gitea/status.html.haml
index 2dde642d8f0..86ab3ca85c3 100644
--- a/app/views/import/gitea/status.html.haml
+++ b/app/views/import/gitea/status.html.haml
@@ -1,6 +1,7 @@
- page_title _("Gitea import")
-%h1.page-title.gl-font-size-h-display
- = custom_icon('gitea_logo')
+%h1.page-title.gl-font-size-h-display.d-flex.gl-align-items-center
+ .gl-display-flex.gl-align-items-center.gl-justify-content-center
+ = sprite_icon('gitea', css_class: 'gl-mr-3', size: 48)
= _('Import projects from Gitea')
= render 'import/githubish_status', provider: 'gitea', default_namespace: @namespace
diff --git a/app/views/import/github/new.html.haml b/app/views/import/github/new.html.haml
index 5293013b813..24369ff3d39 100644
--- a/app/views/import/github/new.html.haml
+++ b/app/views/import/github/new.html.haml
@@ -3,8 +3,11 @@
- header_title _("New project"), new_project_path
- add_to_breadcrumbs s_('ProjectsNew|Import project'), new_project_path(anchor: 'import_project')
-%h1.page-title.gl-font-size-h-display
+%h1.page-title.gl-font-size-h-display.d-flex.gl-align-items-center
+ .gl-display-flex.gl-align-items-center.gl-justify-content-center
+ = sprite_icon('github', css_class: 'gl-mr-3', size: 48)
= title
+%hr
%p
= import_github_authorize_message
@@ -23,9 +26,9 @@
= form_tag personal_access_token_import_github_path, method: :post do
.form-group
- %label.label-bold= _('Personal Access Token')
+ %label.col-form-label{ for: 'personal_access_token' }= _('Personal Access Token')
= hidden_field_tag(:namespace_id, params[:namespace_id])
- = text_field_tag :personal_access_token, '', class: 'form-control gl-form-input', placeholder: _('e.g. %{token}') % { token: '8d3f016698e...' }, data: { qa_selector: 'personal_access_token_field' }
+ = password_field_tag :personal_access_token, '', class: 'form-control gl-form-input', placeholder: _('e.g. %{token}') % { token: '8d3f016698e...' }, data: { testid: 'personal-access-token-field' }
%span.form-text.gl-text-gray-600
= import_github_personal_access_token_message
@@ -34,7 +37,5 @@
.form-actions.gl-display-flex.gl-justify-content-end
= render Pajamas::ButtonComponent.new(href: new_project_path) do
= _('Cancel')
- = render Pajamas::ButtonComponent.new(variant: :confirm,
- type: :submit,
- button_options: { class: 'gl-ml-3', data: { qa_selector: 'authenticate_button' } }) do
+ = render Pajamas::ButtonComponent.new(variant: :confirm, type: :submit, button_options: { class: 'gl-ml-3', data: { testid: 'authenticate-button' } }) do
= _('Authenticate')
diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml
index 6f25bc75ca1..f1a61d72771 100644
--- a/app/views/import/github/status.html.haml
+++ b/app/views/import/github/status.html.haml
@@ -1,8 +1,8 @@
- title = has_ci_cd_only_params? ? _('Connect repositories from GitHub') : _('GitHub import')
- page_title title
-%h1.page-title.gl-font-size-h-display.mb-0.gl-display-flex
+%h1.page-title.gl-font-size-h-display.d-flex.gl-align-items-center
.gl-display-flex.gl-align-items-center.gl-justify-content-center
- = sprite_icon('github', css_class: 'gl-mr-2')
+ = sprite_icon('github', css_class: 'gl-mr-3', size: 48)
= _('Import repositories from GitHub')
= render 'import/githubish_status',
diff --git a/app/views/import/gitlab_projects/new.html.haml b/app/views/import/gitlab_projects/new.html.haml
index 079123e989e..b90d400a843 100644
--- a/app/views/import/gitlab_projects/new.html.haml
+++ b/app/views/import/gitlab_projects/new.html.haml
@@ -2,9 +2,9 @@
- header_title _("New project"), new_project_path
- add_to_breadcrumbs s_('ProjectsNew|Import project'), new_project_path(anchor: 'import_project')
-%h1.page-title.gl-font-size-h-display.d-flex
+%h1.page-title.gl-font-size-h-display.d-flex.gl-align-items-center
.gl-display-flex.gl-align-items-center.gl-justify-content-center
- = sprite_icon('tanuki', css_class: 'gl-mr-2')
+ = sprite_icon('tanuki', css_class: 'gl-mr-3', size: 48)
= _('Import an exported GitLab project')
%hr
@@ -21,7 +21,7 @@
= file_field_tag :file, class: ''
.row
.form-actions.col-sm-12
- = render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm, button_options: { class: 'gl-mr-2', data: { qa_selector: 'import_project_button' }}) do
+ = render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm, button_options: { class: 'gl-mr-2', data: { testid: 'import-project-button' }}) do
= _('Import project')
= render Pajamas::ButtonComponent.new(href: new_project_path) do
= _('Cancel')
diff --git a/app/views/import/shared/_new_project_form.html.haml b/app/views/import/shared/_new_project_form.html.haml
index 6000612a285..042d94ad1b6 100644
--- a/app/views/import/shared/_new_project_form.html.haml
+++ b/app/views/import/shared/_new_project_form.html.haml
@@ -1,7 +1,7 @@
.row
.form-group.project-name.col-sm-12
= label_tag :name, _('Project name'), class: 'label-bold'
- = text_field_tag :name, @name, placeholder: "My awesome project", class: "js-project-name form-control gl-form-input input-lg", autofocus: true, required: true, aria: { required: true }, data: { qa_selector: 'project_name_field' }
+ = text_field_tag :name, @name, placeholder: "My awesome project", class: "js-project-name form-control gl-form-input input-lg", autofocus: true, required: true, aria: { required: true }, data: { testid: 'project-name-field' }
.form-group.col-12.col-sm-6.gl-pr-0
= label_tag :namespace_id, _('Project URL'), class: 'label-bold'
.input-group.gl-flex-nowrap
@@ -21,4 +21,4 @@
.gl-align-self-center.gl-pl-5 /
.form-group.col-12.col-sm-6.project-path
= label_tag :path, _('Project slug'), class: 'label-bold'
- = text_field_tag :path, @path, placeholder: "my-awesome-project", class: "js-path-name form-control gl-form-input", required: true, aria: { required: true }, data: { qa_selector: 'project_slug_field' }
+ = text_field_tag :path, @path, placeholder: "my-awesome-project", class: "js-path-name form-control gl-form-input", required: true, aria: { required: true }
diff --git a/app/views/invites/decline.html.haml b/app/views/invites/decline.html.haml
index 4a57d70cb6e..40e4455e565 100644
--- a/app/views/invites/decline.html.haml
+++ b/app/views/invites/decline.html.haml
@@ -1,5 +1,5 @@
- page_title _('Invitation declined')
-.decline-page.gl-display-flex.gl-flex-direction-column.gl-mx-auto{ class: 'gl-xs-w-full!' }
+.decline-page.gl-display-flex.gl-flex-direction-column.gl-mx-auto.gl-w-full.gl-sm-w-auto
.gl-align-self-center.gl-mb-4.gl-mt-7.gl-sm-mt-0= sprite_icon('check-circle', size: 48, css_class: 'gl-text-green-400')
%h2.gl-font-size-h2= _('You successfully declined the invitation')
%p
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index 37d03bde72e..41f663c7c06 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -2,10 +2,6 @@
- site_name = _('GitLab')
- omit_og = sign_in_with_redirect?
--# This is a temporary place for the page specific style migrations to be included on all pages like page_specific_files
-- if Feature.disabled?(:page_specific_styles, current_user)
- - add_page_specific_style('page_bundles/projects')
-
%head{ omit_og ? { } : { prefix: "og: http://ogp.me/ns#" } }
%meta{ charset: "utf-8" }
%meta{ 'http-equiv' => 'X-UA-Compatible', content: 'IE=edge' }
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index f52ea801eef..fe2c2e968e8 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -7,7 +7,7 @@
- sidebar_panel = super_sidebar_nav_panel(nav: nav, user: current_user, group: group, project: @project, current_ref: current_ref, ref_type: @ref_type, viewed_user: @user, organization: @organization)
- sidebar_data = super_sidebar_context(current_user, group: group, project: @project, panel: sidebar_panel, panel_type: nav).to_json
- %aside.js-super-sidebar.super-sidebar.super-sidebar-loading{ data: { root_path: root_path, sidebar: sidebar_data, toggle_new_nav_endpoint: profile_preferences_path, force_desktop_expanded_sidebar: @force_desktop_expanded_sidebar.to_s, command_palette: command_palette_data(project: @project).to_json } }
+ %aside.js-super-sidebar.super-sidebar.super-sidebar-loading{ data: { root_path: root_path, sidebar: sidebar_data, force_desktop_expanded_sidebar: @force_desktop_expanded_sidebar.to_s, command_palette: command_palette_data(project: @project).to_json } }
- if display_whats_new?
#whats-new-app{ data: { version_digest: whats_new_version_digest } }
@@ -20,7 +20,7 @@
.mobile-overlay
= dispensable_render_if_exists 'layouts/header/verification_reminder'
.alert-wrapper.gl-force-block-formatting-context
- = dispensable_render 'shared/new_nav_announcement'
+ = dispensable_render 'shared/new_nav_for_everyone_announcement'
= dispensable_render 'shared/outdated_browser'
= dispensable_render_if_exists "layouts/header/licensed_user_count_threshold"
= dispensable_render_if_exists "layouts/header/token_expiry_notification"
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index 451c66b074b..5a66cc0ddb5 100644
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -1,6 +1,6 @@
- page_classes = page_class << @html_class
-- page_classes = page_classes.flatten.compact
-- body_classes = [user_application_theme, user_tab_width, @body_class, client_class_list, *custom_diff_color_classes]
+- page_classes = [user_application_theme, page_classes.flatten.compact]
+- body_classes = [user_tab_width, @body_class, client_class_list, *custom_diff_color_classes]
!!! 5
%html{ lang: I18n.locale, class: page_classes }
diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml
index 4e9ae7c7fd8..6a65b31a002 100644
--- a/app/views/layouts/devise.html.haml
+++ b/app/views/layouts/devise.html.haml
@@ -1,9 +1,9 @@
- add_page_specific_style 'page_bundles/login'
- custom_text = custom_sign_in_description
!!! 5
-%html.html-devise-layout{ lang: I18n.locale }
+%html.html-devise-layout{ class: user_application_theme, lang: I18n.locale }
= render "layouts/head", { startup_filename: 'signin' }
- %body.gl-h-full.login-page.navless{ class: "#{system_message_class} #{user_application_theme} #{client_class_list}", data: { page: body_data_page, qa_selector: 'login_page' } }
+ %body.gl-h-full.login-page.navless{ class: "#{system_message_class} #{client_class_list}", data: { page: body_data_page, qa_selector: 'login_page', testid: 'login-page' } }
= header_message
= render "layouts/init_client_detection_flags"
- if Feature.enabled?(:restyle_login_page, @project)
@@ -31,7 +31,7 @@
%h1.mb-3.gl-font-size-h2
= brand_title
.mb-3
- .gl-w-half.gl-xs-w-full.gl-ml-auto.gl-mr-auto.bar
+ .gl-w-full.gl-sm-w-half.gl-ml-auto.gl-mr-auto.bar
= yield
= render 'devise/shared/footer'
diff --git a/app/views/layouts/devise_empty.html.haml b/app/views/layouts/devise_empty.html.haml
index 3e969b866a6..6816a64ac8f 100644
--- a/app/views/layouts/devise_empty.html.haml
+++ b/app/views/layouts/devise_empty.html.haml
@@ -1,8 +1,8 @@
- add_page_specific_style 'page_bundles/login'
!!! 5
-%html.html-devise-layout{ lang: I18n.locale }
+%html.html-devise-layout{ class: user_application_theme, lang: I18n.locale }
= render "layouts/head"
- %body.gl-h-full.login-page.navless{ class: "#{system_message_class} #{user_application_theme} #{client_class_list}" }
+ %body.gl-h-full.login-page.navless{ class: "#{system_message_class} #{client_class_list}" }
= header_message
= render "layouts/init_client_detection_flags"
= render "layouts/header/empty"
diff --git a/app/views/layouts/fullscreen.html.haml b/app/views/layouts/fullscreen.html.haml
index da192822902..f168c742085 100644
--- a/app/views/layouts/fullscreen.html.haml
+++ b/app/views/layouts/fullscreen.html.haml
@@ -1,8 +1,8 @@
- minimal = local_assigns.fetch(:minimal, false)
!!! 5
-%html{ lang: I18n.locale, class: page_class }
+%html{ class: [user_application_theme, page_class], lang: I18n.locale }
= render "layouts/head"
- %body{ class: "#{user_application_theme} #{user_tab_width} #{@body_class} fullscreen-layout", data: { page: body_data_page } }
+ %body{ class: "#{user_tab_width} #{@body_class} fullscreen-layout", data: { page: body_data_page } }
= render 'peek/bar'
= header_message
- unless minimal
diff --git a/app/views/layouts/minimal.html.haml b/app/views/layouts/minimal.html.haml
index 8b6a2a2f2a7..e499b9ae240 100644
--- a/app/views/layouts/minimal.html.haml
+++ b/app/views/layouts/minimal.html.haml
@@ -1,17 +1,18 @@
- page_classes = page_class.push(@html_class).flatten.compact
!!! 5
-%html{ lang: I18n.locale, class: page_classes }
+%html.gl-h-full{ lang: I18n.locale, class: page_classes }
= render "layouts/head"
- %body{ data: body_data, class: system_message_class }
+ %body.gl-h-full{ data: body_data, class: system_message_class }
= header_message
= render 'peek/bar'
= render 'layouts/published_experiments'
= render "layouts/header/empty"
- .layout-page
+ .layout-page.gl-h-full.borderless.gl-display-flex.gl-flex-wrap
.content-wrapper.gl-pt-6{ class: 'gl-md-pt-11!' }
%div{ class: container_class }
%main#content-body.content
= render "layouts/flash" unless @hide_flash
= yield
+ = yield :footer
= footer_message
diff --git a/app/views/layouts/nav/_ask_duo_button.html.haml b/app/views/layouts/nav/_ask_duo_button.html.haml
deleted file mode 100644
index e37ce50352c..00000000000
--- a/app/views/layouts/nav/_ask_duo_button.html.haml
+++ /dev/null
@@ -1,13 +0,0 @@
-- if Gitlab.ee? && ::Gitlab::Llm::TanukiBot.show_breadcrumbs_entry_point_for?(user: current_user)
- - label = s_('TanukiBot|GitLab Duo Chat')
- = render Pajamas::ButtonComponent.new(variant: :confirm,
- category: :secondary,
- icon: 'tanuki-ai',
- size: 'small',
- button_options: { class: 'js-tanuki-bot-chat-toggle gl-ml-3 gl-display-none gl-md-display-inline', data: { track_action: 'click_button', track_label: 'tanuki_bot_breadcrumbs_button' }, aria: { label: label }}) do
- = label
- = render Pajamas::ButtonComponent.new(variant: :confirm,
- category: :secondary,
- icon: 'tanuki-ai',
- size: 'small',
- button_options: { class: 'js-tanuki-bot-chat-toggle has-tooltip gl-ml-3 gl-md-display-none', title: label, data: { track_action: 'click_button', track_label: 'tanuki_bot_breadcrumbs_button', placement: 'left' }, aria: { label: label }})
diff --git a/app/views/layouts/nav/_top_bar.html.haml b/app/views/layouts/nav/_top_bar.html.haml
index ef783b688e0..c938cad5c42 100644
--- a/app/views/layouts/nav/_top_bar.html.haml
+++ b/app/views/layouts/nav/_top_bar.html.haml
@@ -12,4 +12,4 @@
- elsif defined?(@left_sidebar)
= render Pajamas::ButtonComponent.new(icon: 'sidebar', category: :tertiary, button_options: { class: 'toggle-mobile-nav gl-ml-n3', data: { testid: 'toggle-mobile-nav-button' }, aria: { label: _('Open sidebar') } })
= render "layouts/nav/breadcrumbs/breadcrumbs"
- = render "layouts/nav/ask_duo_button"
+ = render_if_exists "layouts/nav/ask_duo_button"
diff --git a/app/views/layouts/signup_onboarding.html.haml b/app/views/layouts/signup_onboarding.html.haml
index a5953021671..c8e15896b97 100644
--- a/app/views/layouts/signup_onboarding.html.haml
+++ b/app/views/layouts/signup_onboarding.html.haml
@@ -1,9 +1,9 @@
- add_page_specific_style 'page_bundles/signup'
- add_page_specific_style 'page_bundles/login'
!!! 5
-%html.html-devise-layout{ lang: I18n.locale }
+%html.html-devise-layout{ class: user_application_theme, lang: I18n.locale }
= render "layouts/head"
- %body.signup-page.navless{ class: "#{system_message_class} #{user_application_theme} #{client_class_list}", data: { page: body_data_page, qa_selector: 'signup_page' } }
+ %body.signup-page.navless{ class: "#{system_message_class} #{client_class_list}", data: { page: body_data_page } }
= header_message
= render "layouts/init_client_detection_flags"
= render "layouts/header/logo_with_title"
diff --git a/app/views/layouts/terms.html.haml b/app/views/layouts/terms.html.haml
index 32f00a4c0c6..09b5407ecdb 100644
--- a/app/views/layouts/terms.html.haml
+++ b/app/views/layouts/terms.html.haml
@@ -2,11 +2,10 @@
- add_page_specific_style 'page_bundles/terms'
- @hide_top_bar = true
- @hide_top_bar_padding = true
-- body_classes = [user_application_theme]
-%html{ lang: I18n.locale, class: page_class }
+%html{ lang: I18n.locale, class: [user_application_theme, page_class] }
= render "layouts/head"
- %body{ class: body_classes, data: { page: body_data_page } }
+ %body{ data: { page: body_data_page } }
.layout-page.terms{ class: page_class }
.content-wrapper.gl-pb-5
.mobile-overlay
diff --git a/app/views/notify/github_gists_import_errors_email.html.haml b/app/views/notify/github_gists_import_errors_email.html.haml
index 07b4cfca77e..903f4bf1466 100644
--- a/app/views/notify/github_gists_import_errors_email.html.haml
+++ b/app/views/notify/github_gists_import_errors_email.html.haml
@@ -11,7 +11,7 @@
%li
= s_("GithubImporter|Gist with id %{gist_id} failed due to error: %{error}.") % { gist_id: gist_id, error: error }
- if error == Gitlab::GithubGistsImport::Importer::GistImporter::FILE_COUNT_LIMIT_MESSAGE
- - import_snippets_url = help_page_url('api/import.md', anchor: 'import-github-gists-into-gitlab-snippets')
+ - import_snippets_url = help_page_url('api/import', anchor: 'import-github-gists-into-gitlab-snippets')
- import_snippets_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: import_snippets_url }
= html_escape(s_("GithubImporter|Please follow %{import_snippets_link_start}Import GitHub gists into GitLab snippets%{import_snippets_link_end} for more details.")) % { import_snippets_link_start: import_snippets_link_start, import_snippets_link_end: '</a>'.html_safe }
diff --git a/app/views/notify/pages_domain_auto_ssl_failed_email.html.haml b/app/views/notify/pages_domain_auto_ssl_failed_email.html.haml
index c0b334fba94..d053fdff624 100644
--- a/app/views/notify/pages_domain_auto_ssl_failed_email.html.haml
+++ b/app/views/notify/pages_domain_auto_ssl_failed_email.html.haml
@@ -5,7 +5,7 @@
%p
#{_('Domain')}: #{link_to @domain.domain, project_pages_domain_url(@project, @domain)}
%p
- - docs_url = help_page_url('user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration.md', anchor: 'troubleshooting')
+ - docs_url = help_page_url('user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration', anchor: 'troubleshooting')
- link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: docs_url }
- link_end = '</a>'.html_safe
= _("Please follow the %{link_start}Let's Encrypt troubleshooting instructions%{link_end} to re-obtain your Let's Encrypt certificate.").html_safe % { link_start: link_start, link_end: link_end }
diff --git a/app/views/notify/pages_domain_auto_ssl_failed_email.text.haml b/app/views/notify/pages_domain_auto_ssl_failed_email.text.haml
index feb88d2df39..ecc466d3e74 100644
--- a/app/views/notify/pages_domain_auto_ssl_failed_email.text.haml
+++ b/app/views/notify/pages_domain_auto_ssl_failed_email.text.haml
@@ -3,5 +3,5 @@
#{_('Project')}: #{project_url(@project)}
#{_('Domain')}: #{project_pages_domain_url(@project, @domain)}
-- docs_url = help_page_url('user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration.md', anchor: 'troubleshooting')
+- docs_url = help_page_url('user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration', anchor: 'troubleshooting')
= _("Please follow the Let's Encrypt troubleshooting instructions to re-obtain your Let's Encrypt certificate: %{docs_url}.").html_safe % { docs_url: docs_url }
diff --git a/app/views/notify/pages_domain_disabled_email.html.haml b/app/views/notify/pages_domain_disabled_email.html.haml
index 44f85df97b9..6b4e40780aa 100644
--- a/app/views/notify/pages_domain_disabled_email.html.haml
+++ b/app/views/notify/pages_domain_disabled_email.html.haml
@@ -8,6 +8,6 @@
Domain: #{link_to @domain.domain, project_pages_domain_url(@project, @domain)}
%p
If this domain has been disabled in error, please follow
- = link_to 'these instructions', help_page_url('user/project/pages/custom_domains_ssl_tls_certification/index.md', anchor: '4-verify-the-domains-ownership')
+ = link_to 'these instructions', help_page_url('user/project/pages/custom_domains_ssl_tls_certification/index', anchor: '4-verify-the-domains-ownership')
to verify and re-enable your domain.
= render 'removal_notification'
diff --git a/app/views/notify/pages_domain_disabled_email.text.haml b/app/views/notify/pages_domain_disabled_email.text.haml
index 5a0fcab72d4..12295f9aa18 100644
--- a/app/views/notify/pages_domain_disabled_email.text.haml
+++ b/app/views/notify/pages_domain_disabled_email.text.haml
@@ -7,7 +7,7 @@ Domain: #{@domain.domain} (#{project_pages_domain_url(@project, @domain)})
If this domain has been disabled in error, please follow these instructions
to verify and re-enable your domain:
-= help_page_url('user/project/pages/custom_domains_ssl_tls_certification/index.md', anchor: 'steps')
+= help_page_url('user/project/pages/custom_domains_ssl_tls_certification/index', anchor: 'steps')
If you no longer wish to use this domain with GitLab Pages, please remove it
from your GitLab project and delete any related DNS records.
diff --git a/app/views/notify/pages_domain_enabled_email.html.haml b/app/views/notify/pages_domain_enabled_email.html.haml
index 103b17a87df..64155e888b7 100644
--- a/app/views/notify/pages_domain_enabled_email.html.haml
+++ b/app/views/notify/pages_domain_enabled_email.html.haml
@@ -7,5 +7,5 @@
Domain: #{link_to @domain.domain, project_pages_domain_url(@project, @domain)}
%p
Please visit
- = link_to 'these instructions', help_page_url('user/project/pages/custom_domains_ssl_tls_certification/index.md', anchor: 'steps')
+ = link_to 'these instructions', help_page_url('user/project/pages/custom_domains_ssl_tls_certification/index', anchor: 'steps')
for more information about custom domain verification.
diff --git a/app/views/notify/pages_domain_enabled_email.text.haml b/app/views/notify/pages_domain_enabled_email.text.haml
index bf8d2ac767a..df56dacf52c 100644
--- a/app/views/notify/pages_domain_enabled_email.text.haml
+++ b/app/views/notify/pages_domain_enabled_email.text.haml
@@ -5,5 +5,5 @@ Project: #{@project.human_name} (#{project_url(@project)})
Domain: #{@domain.domain} (#{project_pages_domain_url(@project, @domain)})
Please visit
-= help_page_url('user/project/pages/custom_domains_ssl_tls_certification/index.md', anchor: 'steps')
+= help_page_url('user/project/pages/custom_domains_ssl_tls_certification/index', anchor: 'steps')
for more information about custom domain verification.
diff --git a/app/views/notify/pages_domain_verification_failed_email.html.haml b/app/views/notify/pages_domain_verification_failed_email.html.haml
index a819b66f18e..4d92d8d1088 100644
--- a/app/views/notify/pages_domain_verification_failed_email.html.haml
+++ b/app/views/notify/pages_domain_verification_failed_email.html.haml
@@ -10,6 +10,6 @@
Until then, you can view your content at #{link_to @domain.url, @domain.url}
%p
Please visit
- = link_to 'these instructions', help_page_url('user/project/pages/custom_domains_ssl_tls_certification/index.md', anchor: 'steps')
+ = link_to 'these instructions', help_page_url('user/project/pages/custom_domains_ssl_tls_certification/index', anchor: 'steps')
for more information about custom domain verification.
= render 'removal_notification'
diff --git a/app/views/notify/pages_domain_verification_failed_email.text.haml b/app/views/notify/pages_domain_verification_failed_email.text.haml
index 85aa2d7a503..045fd5483b2 100644
--- a/app/views/notify/pages_domain_verification_failed_email.text.haml
+++ b/app/views/notify/pages_domain_verification_failed_email.text.haml
@@ -7,7 +7,7 @@ Unless you take action, it will be disabled on *#{@domain.enabled_until.strftime
Until then, you can view your content at #{@domain.url}
Please visit
-= help_page_url('user/project/pages/custom_domains_ssl_tls_certification/index.md', anchor: 'steps')
+= help_page_url('user/project/pages/custom_domains_ssl_tls_certification/index', anchor: 'steps')
for more information about custom domain verification.
If you no longer wish to use this domain with GitLab Pages, please remove it
diff --git a/app/views/notify/pages_domain_verification_succeeded_email.html.haml b/app/views/notify/pages_domain_verification_succeeded_email.html.haml
index 808b12948f9..aaf0dae597f 100644
--- a/app/views/notify/pages_domain_verification_succeeded_email.html.haml
+++ b/app/views/notify/pages_domain_verification_succeeded_email.html.haml
@@ -9,5 +9,5 @@
content at #{link_to @domain.url, @domain.url}
%p
Please visit
- = link_to 'these instructions', help_page_url('user/project/pages/custom_domains_ssl_tls_certification/index.md', anchor: 'steps')
+ = link_to 'these instructions', help_page_url('user/project/pages/custom_domains_ssl_tls_certification/index', anchor: 'steps')
for more information about custom domain verification.
diff --git a/app/views/notify/pages_domain_verification_succeeded_email.text.haml b/app/views/notify/pages_domain_verification_succeeded_email.text.haml
index 8d0694ef613..15cf9823a08 100644
--- a/app/views/notify/pages_domain_verification_succeeded_email.text.haml
+++ b/app/views/notify/pages_domain_verification_succeeded_email.text.haml
@@ -6,5 +6,5 @@ Domain: #{@domain.domain} (#{project_pages_domain_url(@project, @domain)})
No action is required on your part. You can view your content at #{@domain.url}
Please visit
-= help_page_url('user/project/pages/custom_domains_ssl_tls_certification/index.md', anchor: 'steps')
+= help_page_url('user/project/pages/custom_domains_ssl_tls_certification/index', anchor: 'steps')
for more information about custom domain verification.
diff --git a/app/views/organizations/organizations/users.html.haml b/app/views/organizations/organizations/users.html.haml
new file mode 100644
index 00000000000..5fb9d786e0b
--- /dev/null
+++ b/app/views/organizations/organizations/users.html.haml
@@ -0,0 +1,4 @@
+- page_title _('Users')
+
+#js-organizations-users{ data: organization_user_app_data(@organization) }
+
diff --git a/app/views/organizations/settings/general.html.haml b/app/views/organizations/settings/general.html.haml
index 94892ef9fbb..663c8fceedf 100644
--- a/app/views/organizations/settings/general.html.haml
+++ b/app/views/organizations/settings/general.html.haml
@@ -1 +1,4 @@
- page_title _("General settings")
+- add_page_specific_style 'page_bundles/settings'
+
+#js-organizations-settings-general{ data: { app_data: organization_settings_general_app_data(@organization) } }
diff --git a/app/views/profiles/gpg_keys/index.html.haml b/app/views/profiles/gpg_keys/index.html.haml
index 982199d3d6f..031869cc60e 100644
--- a/app/views/profiles/gpg_keys/index.html.haml
+++ b/app/views/profiles/gpg_keys/index.html.haml
@@ -28,7 +28,7 @@
%h4.gl-mt-0
= _('Add a GPG key')
%p
- - help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/project/repository/signed_commits/gpg.md') }
+ - help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/project/repository/signed_commits/gpg') }
= _('Add a GPG key for secure access to GitLab. %{help_link_start}Learn more%{help_link_end}.').html_safe % {help_link_start: help_link_start, help_link_end: '</a>'.html_safe }
= render 'form'
diff --git a/app/views/profiles/keys/_key.html.haml b/app/views/profiles/keys/_key.html.haml
index 7ba42274f88..f80cd8cddc5 100644
--- a/app/views/profiles/keys/_key.html.haml
+++ b/app/views/profiles/keys/_key.html.haml
@@ -25,7 +25,7 @@
-# TODO: Remove this conditional when https://gitlab.com/gitlab-org/gitlab/-/issues/324764 is resolved.
- if Feature.enabled?(:disable_ssh_key_used_tracking)
= _('Unavailable')
- = link_to sprite_icon('question-o'), help_page_path('user/ssh.md', anchor: 'view-your-accounts-ssh-keys')
+ = link_to sprite_icon('question-o'), help_page_path('user/ssh', anchor: 'view-your-accounts-ssh-keys')
- else
= key.last_used_at ? time_ago_with_tooltip(key.last_used_at) : _('Never')
diff --git a/app/views/profiles/keys/index.html.haml b/app/views/profiles/keys/index.html.haml
index 0cd41788a53..8477d87a587 100644
--- a/app/views/profiles/keys/index.html.haml
+++ b/app/views/profiles/keys/index.html.haml
@@ -30,7 +30,7 @@
%h4.gl-mt-0
= _('Add an SSH key')
%p
- - help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/ssh.md') }
+ - help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/ssh') }
= _('Add an SSH key for secure access to GitLab. %{help_link_start}Learn more%{help_link_end}.').html_safe % {help_link_start: help_link_start, help_link_end: '</a>'.html_safe }
= render 'form'
diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml
index c12f6907afb..0457561b283 100644
--- a/app/views/profiles/personal_access_tokens/index.html.haml
+++ b/app/views/profiles/personal_access_tokens/index.html.haml
@@ -35,7 +35,7 @@
path: profile_personal_access_tokens_path,
token: @personal_access_token,
scopes: @scopes,
- help_path: help_page_path('user/profile/personal_access_tokens.md', anchor: 'personal-access-token-scopes')
+ help_path: help_page_path('user/profile/personal_access_tokens', anchor: 'personal-access-token-scopes')
#js-access-token-table-app{ data: { access_token_type: type, access_token_type_plural: type_plural, initial_active_access_tokens: @active_access_tokens.to_json } }
diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml
index a6534a16e86..96375412f94 100644
--- a/app/views/profiles/preferences/show.html.haml
+++ b/app/views/profiles/preferences/show.html.haml
@@ -78,6 +78,9 @@
= f.gitlab_ui_radio_component :layout, layout_choices[0][1], layout_choices[0][0], help_text: fixed_help_text
= f.gitlab_ui_radio_component :layout, layout_choices[1][1], layout_choices[1][0], help_text: fluid_help_text
+ - if Feature.enabled?(:ui_for_organizations, current_user)
+ #js-home-organization-setting{ data: { app_data: home_organization_setting_app_data } }
+
.js-listbox-input{ data: { label: s_('Preferences|Homepage'), description: s_('Preferences|Choose what content you want to see by default on your homepage.'), name: 'user[dashboard]', items: dashboard_choices.to_json, value: current_user.dashboard, block: true.to_s, toggle_class: 'gl-form-input-xl' } }
= render_if_exists 'profiles/preferences/group_overview_selector', f: f # EE-specific
@@ -152,6 +155,12 @@
= f.gitlab_ui_checkbox_component :time_display_relative,
s_('Preferences|Use relative times'),
help_text: s_('Preferences|For example: 30 minutes ago.')
+ .form-group
+ = f.label :time_display_format, class: 'label-bold' do
+ = s_('Preferences|Time format')
+ - time_display_format_choices.each_entry do |time_display_format_option|
+ .gl-mb-4
+ = f.gitlab_ui_radio_component :time_display_format, time_display_format_option[1], time_display_format_option[0]
.settings-section.js-preferences-form.js-search-settings-section#enabled_following
.settings-sticky-header
.settings-sticky-header-inner
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index 4da48771ba3..405364b6792 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -122,6 +122,10 @@
allow_empty: true}
%small.form-text.text-gl-muted
= external_accounts_docs_link
+ - if Feature.enabled?(:mastodon_social_ui, @user)
+ .form-group.gl-form-group
+ = f.label :mastodon
+ = f.text_field :mastodon, class: 'gl-form-input form-control gl-md-form-input-lg', placeholder: "@robin@example.com"
.form-group.gl-form-group
= f.label :website_url, s_('Profiles|Website url')
@@ -152,7 +156,7 @@
%legend.col-form-label
= _('Private profile')
- private_profile_label = s_("Profiles|Don't display activity-related personal information on your profile.")
- - private_profile_help_link = link_to sprite_icon('question-o'), help_page_path('user/profile/index.md', anchor: 'make-your-user-profile-page-private')
+ - private_profile_help_link = link_to sprite_icon('question-o'), help_page_path('user/profile/index', anchor: 'make-your-user-profile-page-private')
= f.gitlab_ui_checkbox_component :private_profile, '%{private_profile_label} %{private_profile_help_link}'.html_safe % { private_profile_label: private_profile_label, private_profile_help_link: private_profile_help_link.html_safe }
%fieldset.form-group.gl-form-group
%legend.col-form-label
diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml
index ff0b31da022..7c42053a376 100644
--- a/app/views/profiles/two_factor_auths/show.html.haml
+++ b/app/views/profiles/two_factor_auths/show.html.haml
@@ -41,7 +41,7 @@
alert_options: { class: 'gl-mb-3' },
dismissible: false) do |c|
- c.with_body do
- = link_to _('Try the troubleshooting steps here.'), help_page_path('user/profile/account/two_factor_authentication.md', anchor: 'troubleshooting'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Try the troubleshooting steps here.'), help_page_path('user/profile/account/two_factor_authentication', anchor: 'troubleshooting'), target: '_blank', rel: 'noopener noreferrer'
- if current_password_required?
.form-group
@@ -130,7 +130,7 @@
alert_options: { class: 'gl-mb-3' },
dismissible: false) do |c|
- c.with_body do
- = link_to _('Try the troubleshooting steps here.'), help_page_path('user/profile/account/two_factor_authentication.md', anchor: 'troubleshooting'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Try the troubleshooting steps here.'), help_page_path('user/profile/account/two_factor_authentication', anchor: 'troubleshooting'), target: '_blank', rel: 'noopener noreferrer'
.js-manage-two-factor-form{ data: { current_password_required: current_password_required?.to_s, profile_two_factor_auth_path: profile_two_factor_auth_path, profile_two_factor_auth_method: 'delete', codes_profile_two_factor_auth_path: codes_profile_two_factor_auth_path, codes_profile_two_factor_auth_method: 'post' } }
- else
%p
diff --git a/app/views/projects/_errors.html.haml b/app/views/projects/_errors.html.haml
index 2dba22d3be6..9c478f245dc 100644
--- a/app/views/projects/_errors.html.haml
+++ b/app/views/projects/_errors.html.haml
@@ -1 +1 @@
-= form_errors(@project)
+= form_errors(@project, custom_message: [:limit_reached])
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index 93f4fe62568..e41a0d3d262 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -3,15 +3,17 @@
- emails_disabled = @project.emails_disabled?
.project-home-panel.js-show-on-project-root.gl-mt-4.gl-mb-5{ class: [("empty-project" if empty_repo)] }
- .gl-display-flex.gl-justify-content-space-between.gl-flex-wrap.gl-sm-flex-direction-column.gl-mb-3.gl-gap-5
+ .gl-display-flex.gl-justify-content-space-between.gl-flex-wrap.gl-flex-direction-column.gl-md-flex-direction-row.gl-mb-3.gl-gap-5
.home-panel-title-row.gl-display-flex.gl-align-items-center
%div{ class: 'avatar-container rect-avatar s64 home-panel-avatar gl-flex-shrink-0 gl-w-11 gl-h-11 gl-mr-3! float-none' }
= project_icon(@project, alt: @project.name, class: 'avatar avatar-tile s64', width: 64, height: 64, itemprop: 'image')
%div
%h1.home-panel-title.gl-font-size-h1.gl-mt-3.gl-mb-2.gl-display-flex.gl-word-break-word{ data: { testid: 'project-name-content' }, itemprop: 'name' }
= @project.name
- = visibility_level_content(@project, css_class: 'visibility-icon gl-text-secondary gl-ml-2', icon_css_class: 'icon')
- = render_if_exists 'compliance_management/compliance_framework/compliance_framework_badge', project: @project, additional_classes: 'gl-align-self-center gl-ml-2'
+ = visibility_level_content(@project, css_class: 'visibility-icon gl-text-secondary gl-mx-2', icon_css_class: 'icon')
+ = render_if_exists 'compliance_management/compliance_framework/compliance_framework_badge', project: @project, additional_classes: 'gl-align-self-center gl-mx-2'
+ - if @project.catalog_resource
+ = render partial: 'shared/ci_catalog_badge', locals: { href: project_ci_catalog_resource_path(@project, @project.catalog_resource), css_class: 'gl-mx-2' }
- if @project.group
= render_if_exists 'shared/tier_badge', source: @project, source_type: 'Project'
.home-panel-metadata.gl-font-sm.gl-text-secondary.gl-font-base.gl-font-weight-normal.gl-line-height-normal{ data: { testid: 'project-id-content' }, itemprop: 'identifier' }
diff --git a/app/views/projects/_import_project_pane.html.haml b/app/views/projects/_import_project_pane.html.haml
index 6315c6dc52d..3e92ef25552 100644
--- a/app/views/projects/_import_project_pane.html.haml
+++ b/app/views/projects/_import_project_pane.html.haml
@@ -17,7 +17,7 @@
= html_escape(_("Importing GitLab projects? Migrating GitLab projects when migrating groups by direct transfer is in Beta. %{link_start}Learn more.%{link_end}")) % { link_start: docs_link, link_end: '</a>'.html_safe }
.import-buttons
- if gitlab_project_import_enabled?
- .import_gitlab_project.has-tooltip{ data: { container: 'body', qa_selector: 'gitlab_import_button' } }
+ .import_gitlab_project.has-tooltip{ data: { container: 'body', testid: 'gitlab-import-button' } }
= render Pajamas::ButtonComponent.new(href: '#', icon: 'tanuki', button_options: { class: 'btn_import_gitlab_project js-import-project-btn', data: { href: new_import_gitlab_project_path, platform: 'gitlab_export', **tracking_attrs_data(track_label, 'click_button', 'gitlab_export') } }) do
= _('GitLab export')
diff --git a/app/views/projects/_invite_members_empty_project.html.haml b/app/views/projects/_invite_members_empty_project.html.haml
index d6cab06f773..14b0e82e021 100644
--- a/app/views/projects/_invite_members_empty_project.html.haml
+++ b/app/views/projects/_invite_members_empty_project.html.haml
@@ -4,6 +4,6 @@
= s_('InviteMember|Invite your team')
%p= s_('InviteMember|Add members to this project and start collaborating with your team.')
.js-invite-members-trigger{ data: { variant: 'confirm',
- classes: 'gl-mb-8 gl-xs-w-full',
+ classes: 'gl-mb-8 gl-w-full gl-sm-w-auto',
display_text: s_('InviteMember|Invite members'),
trigger_source: 'project_empty_page' } }
diff --git a/app/views/projects/_invite_members_modal.html.haml b/app/views/projects/_invite_members_modal.html.haml
index a1b0bdd6c56..8713cb4990a 100644
--- a/app/views/projects/_invite_members_modal.html.haml
+++ b/app/views/projects/_invite_members_modal.html.haml
@@ -3,4 +3,8 @@
.js-invite-members-modal{ data: { is_project: 'true',
access_levels: ProjectMember.permissible_access_level_roles(current_user, project).to_json,
reload_page_on_submit: current_path?('project_members#index').to_s,
- help_link: help_page_url('user/permissions') }.merge(common_invite_modal_dataset(project)).merge(users_filter_data(project.group)) }
+ help_link: help_page_url('user/permissions'),
+ is_signup_enabled: signup_enabled?.to_s,
+ new_users_url: new_admin_user_url,
+ is_current_user_admin: current_user&.admin?.to_s,
+ }.merge(common_invite_modal_dataset(project)).merge(users_filter_data(project.group)) }
diff --git a/app/views/projects/_service_desk_settings.html.haml b/app/views/projects/_service_desk_settings.html.haml
index 3dbc4c0fad7..aee61624f69 100644
--- a/app/views/projects/_service_desk_settings.html.haml
+++ b/app/views/projects/_service_desk_settings.html.haml
@@ -1,5 +1,5 @@
- expanded = expanded_by_default?
-%section.settings.js-service-desk-setting-wrapper.no-animate#js-service-desk{ class: ('expanded' if expanded), data: { qa_selector: 'service_desk_settings_content' } }
+%section.settings.js-service-desk-setting-wrapper.no-animate#js-service-desk{ class: ('expanded' if expanded) }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Service Desk')
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
@@ -18,6 +18,7 @@
selected_file_template_project_id: "#{@project.service_desk_setting&.file_template_project_id}",
outgoing_name: "#{@project.service_desk_setting&.outgoing_name}",
project_key: "#{@project.service_desk_setting&.project_key}",
+ add_external_participants_from_cc: "#{@project.service_desk_setting&.add_external_participants_from_cc}",
templates: available_service_desk_templates_for(@project),
public_project: "#{@project.public?}",
custom_email_endpoint: project_service_desk_custom_email_path(@project) } }
diff --git a/app/views/projects/artifacts/_tree_file.html.haml b/app/views/projects/artifacts/_tree_file.html.haml
index e120975a8f9..19db01a2df1 100644
--- a/app/views/projects/artifacts/_tree_file.html.haml
+++ b/app/views/projects/artifacts/_tree_file.html.haml
@@ -1,6 +1,6 @@
- blob = file.blob
- external_link = blob.external_link?(@build)
-- if external_link
+- if external_link && Gitlab::CurrentSettings.enable_artifact_external_redirect_warning_page
- path_to_file = external_file_project_job_artifacts_path(@project, @build, path: file.path)
- else
- path_to_file = file_project_job_artifacts_path(@project, @build, path: file.path)
diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml
index 49a29e1dcb7..0753a021f1f 100644
--- a/app/views/projects/blob/_editor.html.haml
+++ b/app/views/projects/blob/_editor.html.haml
@@ -12,7 +12,7 @@
= render 'filepath_form', input_options: input_options
- if current_action?(:new) || current_action?(:create)
- - input_options = { id: 'file_name', name: 'file_name', value: params[:file_name] || (should_suggest_gitlab_ci_yml? ? '.gitlab-ci.yml' : ''), required: true, placeholder: "Filename", testid: 'file_name_field', class: 'new-file-name js-file-path-name-input' }
+ - input_options = { id: 'file_name', name: 'file_name', value: params[:file_name] || (should_suggest_gitlab_ci_yml? ? '.gitlab-ci.yml' : ''), required: true, placeholder: "Filename", testid: 'file-name-field', class: 'new-file-name js-file-path-name-input' }
= render 'filepath_form', input_options: input_options
- if should_suggest_gitlab_ci_yml?
.js-suggest-gitlab-ci-yml{ data: { track_label: 'suggest_gitlab_ci_yml',
@@ -37,7 +37,7 @@
= _("Soft wrap")
.file-editor.code
- .js-edit-mode-pane#editor{ data: { 'editor-loading': true, qa_selector: 'source_editor_preview_container' } }<
+ .js-edit-mode-pane#editor{ data: { 'editor-loading': true, testid: 'source-editor-preview-container' } }<
%pre.editor-loading-content= params[:content] || local_assigns[:blob_data]
- if local_assigns[:path]
.js-edit-mode-pane#preview.hide
diff --git a/app/views/projects/blob/_pipeline_tour_success.html.haml b/app/views/projects/blob/_pipeline_tour_success.html.haml
index f645d23aa1c..be2654c9b86 100644
--- a/app/views/projects/blob/_pipeline_tour_success.html.haml
+++ b/app/views/projects/blob/_pipeline_tour_success.html.haml
@@ -1,6 +1,6 @@
.js-success-pipeline-modal{ data: { 'commit-cookie': suggest_pipeline_commit_cookie_name,
'go-to-pipelines-path': project_pipelines_path(@project),
'project-merge-requests-path': project_merge_requests_path(@project),
- 'example-link': help_page_path('ci/examples/index.md'),
+ 'example-link': help_page_path('ci/examples/index'),
'code-quality-link': help_page_path('ci/testing/code_quality'),
'human-access': @project.team.human_max_access(current_user&.id) } }
diff --git a/app/views/projects/blob/show.html.haml b/app/views/projects/blob/show.html.haml
index 82f517e8a84..e8b0f2a6c6f 100644
--- a/app/views/projects/blob/show.html.haml
+++ b/app/views/projects/blob/show.html.haml
@@ -15,3 +15,6 @@
= render partial: 'pipeline_tour_success' if show_suggest_pipeline_creation_celebration?
= render 'shared/web_ide_path'
+
+-# https://gitlab.com/gitlab-org/gitlab/-/issues/408388#note_1578533983
+#js-ambiguous-ref-modal{ data: { ambiguous: @is_ambiguous_ref.to_s, ref: current_ref } }
diff --git a/app/views/projects/blob/viewers/_route_map.html.haml b/app/views/projects/blob/viewers/_route_map.html.haml
index 30182c100d5..64122b4dcd4 100644
--- a/app/views/projects/blob/viewers/_route_map.html.haml
+++ b/app/views/projects/blob/viewers/_route_map.html.haml
@@ -6,4 +6,4 @@
This Route Map is invalid:
= viewer.validation_message
-= link_to 'Learn more', help_page_path('ci/environments/index.md', anchor: 'go-from-source-files-to-public-pages')
+= link_to 'Learn more', help_page_path('ci/environments/index', anchor: 'go-from-source-files-to-public-pages')
diff --git a/app/views/projects/blob/viewers/_route_map_loading.html.haml b/app/views/projects/blob/viewers/_route_map_loading.html.haml
index d9e965246a8..0e5816a56af 100644
--- a/app/views/projects/blob/viewers/_route_map_loading.html.haml
+++ b/app/views/projects/blob/viewers/_route_map_loading.html.haml
@@ -1,4 +1,4 @@
= gl_loading_icon(inline: true, css_class: "gl-mr-1")
Validating Route Map…
-= link_to 'Learn more', help_page_path('ci/environments/index.md', anchor: 'go-from-source-files-to-public-pages')
+= link_to 'Learn more', help_page_path('ci/environments/index', anchor: 'go-from-source-files-to-public-pages')
diff --git a/app/views/projects/branch_defaults/_branch_names_fields.html.haml b/app/views/projects/branch_defaults/_branch_names_fields.html.haml
index 3e77cb51a85..982280120fa 100644
--- a/app/views/projects/branch_defaults/_branch_names_fields.html.haml
+++ b/app/views/projects/branch_defaults/_branch_names_fields.html.haml
@@ -10,6 +10,6 @@
%p.form-text.text-muted
= s_('ProjectSettings|Leave empty to use default template.')
= sprintf(s_('ProjectSettings|Maximum %{maxLength} characters.'), { maxLength: Issue::MAX_BRANCH_TEMPLATE })
- - branch_name_help_link = help_page_path('user/project/repository/branches/index.md', anchor: 'name-your-branch')
+ - branch_name_help_link = help_page_path('user/project/repository/branches/index', anchor: 'name-your-branch')
= link_to _('What variables can I use?'), branch_name_help_link, target: "_blank"
= render_if_exists 'projects/branch_defaults/branch_names_help'
diff --git a/app/views/projects/branch_defaults/_default_branch_fields.html.haml b/app/views/projects/branch_defaults/_default_branch_fields.html.haml
index 2c59e187d30..78ce43ca8c9 100644
--- a/app/views/projects/branch_defaults/_default_branch_fields.html.haml
+++ b/app/views/projects/branch_defaults/_default_branch_fields.html.haml
@@ -11,7 +11,7 @@
.form-group
- help_text = _("When merge requests and commits in the default branch close, any issues they reference also close.")
- - help_icon = link_to sprite_icon('question-o'), help_page_path('user/project/issues/managing_issues.md', anchor: 'closing-issues-automatically'), target: '_blank', rel: 'noopener noreferrer'
+ - help_icon = link_to sprite_icon('question-o'), help_page_path('user/project/issues/managing_issues', anchor: 'closing-issues-automatically'), target: '_blank', rel: 'noopener noreferrer'
= f.gitlab_ui_checkbox_component :autoclose_referenced_issues,
s_('ProjectSettings|Auto-close referenced issues on default branch'),
help_text: (help_text + "&nbsp;" + help_icon).html_safe
diff --git a/app/views/projects/branch_defaults/_show.html.haml b/app/views/projects/branch_defaults/_show.html.haml
index 5906cd34c17..521d5bb9890 100644
--- a/app/views/projects/branch_defaults/_show.html.haml
+++ b/app/views/projects/branch_defaults/_show.html.haml
@@ -14,4 +14,4 @@
%input{ name: 'update_section', type: 'hidden', value: 'js-issue-settings' }
= render 'projects/branch_defaults/default_branch_fields', f: f
= render 'projects/branch_defaults/branch_names_fields', f: f
- = f.submit _('Save changes'), pajamas_button: true, data: { qa_selector: 'save_changes_button' }
+ = f.submit _('Save changes'), pajamas_button: true
diff --git a/app/views/projects/branch_rules/_show.html.haml b/app/views/projects/branch_rules/_show.html.haml
index c16c03953c6..10cb91e35bd 100644
--- a/app/views/projects/branch_rules/_show.html.haml
+++ b/app/views/projects/branch_rules/_show.html.haml
@@ -3,7 +3,7 @@
- show_status_checks = @project.licensed_feature_available?(:external_status_checks)
- show_approvers = @project.licensed_feature_available?(:merge_request_approvers)
-%section.settings.no-animate#branch-rules{ class: ('expanded' if expanded), data: { qa_selector: 'branch_rules_content' } }
+%section.settings.no-animate#branch-rules{ class: ('expanded' if expanded), data: { testid: 'branch-rules-content' } }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Branch rules')
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index 61961172eb2..3b9e8e706f9 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -4,10 +4,10 @@
- mr_status = merge_request_status(related_merge_request)
- is_default_branch = branch.name == @repository.root_ref
-%li{ class: "branch-item gl-display-flex! gl-align-items-center! js-branch-item js-branch-#{branch.name} gl-pl-5! gl-pr-2!", data: { name: branch.name, qa_selector: 'branch_container', qa_name: branch.name } }
+%li{ class: "branch-item gl-display-flex! gl-align-items-center! js-branch-item js-branch-#{branch.name} gl-pl-5! gl-pr-2!", data: { name: branch.name, testid: 'branch-container', qa_name: branch.name } }
.branch-info
.gl-display-flex.gl-align-items-center
- = link_to project_tree_path(@project, branch.name, ref_type: 'heads'), class: 'item-title str-truncated-100 ref-name', data: { qa_selector: 'branch_link' } do
+ = link_to project_tree_path(@project, branch.name, ref_type: 'heads'), class: 'item-title str-truncated-100 ref-name', data: { testid: 'branch-link' } do
= branch.name
= clipboard_button(text: branch.name, title: _("Copy branch name"))
- if is_default_branch
@@ -28,7 +28,7 @@
.pipeline-status.d-none.d-md-block<
- if commit_status
- = render 'ci/status/icon', size: 16, status: commit_status
+ = render 'ci/status/icon', status: commit_status
- elsif show_commit_status
.gl-display-inline-flex.gl-vertical-align-middle.gl-mr-3
%svg.s16
diff --git a/app/views/projects/branches/_panel.html.haml b/app/views/projects/branches/_panel.html.haml
index 8ef7d435420..8952ba75568 100644
--- a/app/views/projects/branches/_panel.html.haml
+++ b/app/views/projects/branches/_panel.html.haml
@@ -12,7 +12,7 @@
%h3.gl-new-card-title.h5
= panel_title
- c.with_body do
- %ul.content-list.branches-list.all-branches{ data: { qa_selector: 'all_branches_container' } }
+ %ul.content-list.branches-list.all-branches{ data: { testid: 'all-branches-container' } }
- branches.first(overview_max_branches).each do |branch|
= render "projects/branches/branch", branch: branch, merged: project.repository.merged_to_root_ref?(branch), commit_status: @branch_pipeline_statuses[branch.name], show_commit_status: @branch_pipeline_statuses.any?
- if branches.size > overview_max_branches
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index 4017db459a9..76d6b0a042d 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -14,7 +14,7 @@
%td.status
-# Sending 'status' prevents calling the user relation inside the presenter, generating N+1,
-# see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68743
- = render "ci/status/badge", status: status, title: job.status_title(status)
+ = render "ci/status/icon", status: status, show_status_text: true
%td
- if can?(current_user, :read_build, job)
@@ -104,10 +104,10 @@
.btn-group
- if can?(current_user, :read_job_artifacts, job) && job.artifacts?
= link_button_to nil, download_project_job_artifacts_path(job.project, job), rel: 'nofollow', download: '', title: _('Download artifacts'), icon: 'download'
+ - if can?(current_user, :cancel_build, job) && job.active?
+ = link_button_to nil, cancel_project_job_path(job.project, job, continue: { to: request.fullpath }), method: :post, title: _('Cancel'), icon: 'cancel'
- if can?(current_user, :update_build, job)
- - if job.active?
- = link_button_to nil, cancel_project_job_path(job.project, job, continue: { to: request.fullpath }), method: :post, title: _('Cancel'), icon: 'cancel'
- - elsif job.scheduled?
+ - if job.scheduled?
= render Pajamas::ButtonComponent.new(disabled: true, icon: 'planning') do
%time.js-remaining-time{ datetime: job.scheduled_at.utc.iso8601 }
= duration_in_numbers(job.execute_in)
@@ -124,7 +124,7 @@
class: 'has-tooltip',
icon: 'time-out'
- elsif allow_retry
- - if job.playable? && !admin && can?(current_user, :update_build, job)
+ - if job.playable? && !admin
= link_button_to nil, play_project_job_path(job.project, job, return_to: request.original_url), method: :post, title: _('Play'), icon: 'play'
- elsif job.retryable?
= link_button_to nil, retry_project_job_path(job.project, job, return_to: request.original_url), method: :post, title: _('Retry'), icon: 'retry'
diff --git a/app/views/projects/cleanup/_show.html.haml b/app/views/projects/cleanup/_show.html.haml
index fffa1ff36b9..1d365dbceb8 100644
--- a/app/views/projects/cleanup/_show.html.haml
+++ b/app/views/projects/cleanup/_show.html.haml
@@ -11,7 +11,7 @@
- link_end = '</a>'.html_safe
= _("Clean up after running %{link_start}git filter-repo%{link_end} on the repository.").html_safe % { link_start: link_start, link_end: link_end }
= link_to sprite_icon('question-o'),
- help_page_path('user/project/repository/reducing_the_repo_size_using_git.md'),
+ help_page_path('user/project/repository/reducing_the_repo_size_using_git'),
target: '_blank', rel: 'noopener noreferrer'
.settings-content
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index e79a91eddaf..42482a773be 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -3,7 +3,7 @@
= render partial: 'signature', object: @commit.signature
%strong
#{ s_('CommitBoxTitle|Commit') }
- %span.commit-sha{ data: { qa_selector: 'commit_sha_content' } }= @commit.short_id
+ %span.commit-sha{ data: { testid: 'commit-sha-content' } }= @commit.short_id
= clipboard_button(text: @commit.id, title: _('Copy commit SHA'))
%span.d-none.d-sm-inline= _('authored')
#{time_ago_with_tooltip(@commit.authored_date)}
@@ -19,7 +19,7 @@
#{time_ago_with_tooltip(@commit.committed_date)}
#js-commit-comments-button{ data: { comments_count: @notes_count.to_i } }
- = link_button_to _('Browse files'), project_tree_path(@project, @commit), class: 'gl-mr-3 gl-xs-w-full gl-xs-mb-3'
+ = link_button_to _('Browse files'), project_tree_path(@project, @commit), class: 'gl-mr-3 gl-w-full gl-sm-w-auto gl-xs-mb-3'
#js-commit-options-dropdown{ data: commit_options_dropdown_data(@project, @commit) }
.commit-box{ data: { project_path: project_path(@project) } }
diff --git a/app/views/projects/commit/_signature_badge.html.haml b/app/views/projects/commit/_signature_badge.html.haml
index 6aefc2eaa8b..d4a775728e3 100644
--- a/app/views/projects/commit/_signature_badge.html.haml
+++ b/app/views/projects/commit/_signature_badge.html.haml
@@ -17,17 +17,17 @@
- if signature.x509?
= render partial: "projects/commit/x509/certificate_details", locals: { signature: signature }
- = link_to(_('Learn more about X.509 signed commits'), help_page_path('user/project/repository/signed_commits/x509.md'), class: 'gl-link gl-display-block')
+ = link_to(_('Learn more about X.509 signed commits'), help_page_path('user/project/repository/signed_commits/x509'), class: 'gl-link gl-display-block')
- elsif signature.ssh?
= _('SSH key fingerprint:')
%span.gl-font-monospace= signature.key_fingerprint_sha256 || _('Unknown')
- = link_to(_('Learn about signing commits with SSH keys.'), help_page_path('user/project/repository/signed_commits/ssh.md'), class: 'gl-link gl-display-block gl-mt-3')
+ = link_to(_('Learn about signing commits with SSH keys.'), help_page_path('user/project/repository/signed_commits/ssh'), class: 'gl-link gl-display-block gl-mt-3')
- else
= _('GPG Key ID:')
%span.gl-font-monospace= signature.gpg_key_primary_keyid
- = link_to(_('Learn about signing commits'), help_page_path('user/project/repository/signed_commits/index.md'), class: 'gl-link gl-display-block gl-mt-3')
+ = link_to(_('Learn about signing commits'), help_page_path('user/project/repository/signed_commits/index'), class: 'gl-link gl-display-block gl-mt-3')
%a.signature-badge.gl-display-inline-block.gl-ml-4{ role: 'button', tabindex: 0, data: { toggle: 'popover', html: 'true', placement: 'top', title: title, content: content } }
= gl_badge_tag label, variant: variant
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index c42d0fe9931..9f0c910c1c0 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -26,7 +26,7 @@
= author_avatar(commit, size: 40, has_tooltip: false)
.commit-detail.flex-list.gl-display-flex.gl-justify-content-space-between.gl-align-items-center.gl-flex-grow-1.gl-min-w-0
- .commit-content{ data: { qa_selector: 'commit_content' } }
+ .commit-content{ data: { testid: 'commit-content' } }
- if view_details && merge_request
= link_to commit.title, project_commit_path(project, commit.id, merge_request_iid: merge_request.iid), class: ["commit-row-message item-title js-onboarding-commit-item", ("font-italic" if commit.message.empty?)]
- else
@@ -36,7 +36,7 @@
= commit.short_id
- if commit.description? && collapsible
= render Pajamas::ButtonComponent.new(icon: 'ellipsis_h',
- button_options: { class: 'button-ellipsis-horizontal text-expander js-toggle-button', data: { toggle: 'tooltip', container: 'body' }, :title => _("Toggle commit description"), aria: { label: _("Toggle commit description") }})
+ button_options: { class: 'button-ellipsis-horizontal text-expander js-toggle-button', data: { toggle: 'tooltip', container: 'body', collapse_title: _("Toggle commit description"), expand_title: _("Toggle commit description") }, :title => _("Toggle commit description"), aria: { label: _("Toggle commit description") }})
.committer
- commit_author_link = commit_author_link(commit, avatar: false, size: 24)
diff --git a/app/views/projects/diffs/viewers/_collapsed.html.haml b/app/views/projects/diffs/viewers/_collapsed.html.haml
index 578b0af3241..6cffae44084 100644
--- a/app/views/projects/diffs/viewers/_collapsed.html.haml
+++ b/app/views/projects/diffs/viewers/_collapsed.html.haml
@@ -1,3 +1,4 @@
.nothing-here-block.diff-collapsed{ data: { diff_for_path: collapsed_diff_url(viewer.diff_file) } }
= _("This diff is collapsed.")
- %button.click-to-expand.gl-button.btn.btn-link= _("Click to expand it.")
+ = render Pajamas::ButtonComponent.new(variant: :link, button_options: { class: 'click-to-expand' }) do
+ = _("Click to expand it.")
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 4e84a6ef7e7..fd0dc1178f7 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -5,116 +5,119 @@
- reduce_visibility_form_id = 'reduce-visibility-form'
- @force_desktop_expanded_sidebar = true
-= render Pajamas::AlertComponent.new(title: _('GitLab Pages has moved'),
+- if can?(current_user, :admin_project, @project)
+ = render Pajamas::AlertComponent.new(title: _('GitLab Pages has moved'),
alert_options: { class: 'gl-my-5', data: { feature_id: Users::CalloutsHelper::PAGES_MOVED_CALLOUT, dismiss_endpoint: callouts_path, defer_links: 'true' } }) do |c|
- - c.with_body do
- = _('To go to GitLab Pages, on the left sidebar, select %{pages_link}.').html_safe % {pages_link: link_to('Deploy > Pages', project_pages_path(@project)).html_safe}
-
-%section.settings.general-settings.no-animate.expanded#js-general-settings
- .settings-header
- %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Naming, topics, avatar')
- = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
- = _('Collapse')
- %p.gl-text-secondary= _('Update your project name, topics, description, and avatar.')
- .settings-content= render 'projects/settings/general'
-
-%section.settings.sharing-permissions.no-animate#js-shared-permissions{ class: ('expanded' if expanded), data: { testid: 'visibility-features-permissions-content' } }
- .settings-header
- %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Visibility, project features, permissions')
- = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
- = expanded ? _('Collapse') : _('Expand')
- %p.gl-text-secondary= _('Choose visibility level, enable/disable project features and their permissions, disable email notifications, and show default emoji reactions.')
-
- .settings-content
- = form_for @project, html: { multipart: true, class: "sharing-permissions-form", id: reduce_visibility_form_id }, authenticity_token: true do |f|
- %input{ name: 'update_section', type: 'hidden', value: 'js-shared-permissions' }
- %template.js-project-permissions-form-data{ type: "application/json" }= project_permissions_panel_data(@project).to_json.html_safe
- .js-project-permissions-form{ data: visibility_confirm_modal_data(@project, reduce_visibility_form_id) }
-- if show_merge_request_settings_callout?(@project)
- %section.settings.expanded
- = render Pajamas::AlertComponent.new(variant: :info,
+ - c.with_body do
+ = _('To go to GitLab Pages, on the left sidebar, select %{pages_link}.').html_safe % {pages_link: link_to('Deploy > Pages', project_pages_path(@project)).html_safe}
+
+ %section.settings.general-settings.no-animate.expanded#js-general-settings
+ .settings-header
+ %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Naming, topics, avatar')
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
+ = _('Collapse')
+ %p.gl-text-secondary= _('Update your project name, topics, description, and avatar.')
+ .settings-content= render 'projects/settings/general'
+
+ %section.settings.sharing-permissions.no-animate#js-shared-permissions{ class: ('expanded' if expanded), data: { testid: 'visibility-features-permissions-content' } }
+ .settings-header
+ %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Visibility, project features, permissions')
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
+ = expanded ? _('Collapse') : _('Expand')
+ %p.gl-text-secondary= _('Choose visibility level, enable/disable project features and their permissions, disable email notifications, and show default emoji reactions.')
+
+ .settings-content
+ = form_for @project, html: { multipart: true, class: "sharing-permissions-form", id: reduce_visibility_form_id }, authenticity_token: true do |f|
+ %input{ name: 'update_section', type: 'hidden', value: 'js-shared-permissions' }
+ %template.js-project-permissions-form-data{ type: "application/json" }= project_permissions_panel_data(@project).to_json.html_safe
+ .js-project-permissions-form{ data: visibility_confirm_modal_data(@project, reduce_visibility_form_id) }
+ - if show_merge_request_settings_callout?(@project)
+ %section.settings.expanded
+ = render Pajamas::AlertComponent.new(variant: :info,
title: _('Merge requests and approvals settings have moved.'),
alert_options: { class: 'js-merge-request-settings-callout gl-my-5', data: { feature_id: Users::CalloutsHelper::MERGE_REQUEST_SETTINGS_MOVED_CALLOUT, dismiss_endpoint: callouts_path, defer_links: 'true' } }) do |c|
- - c.with_body do
- = _('On the left sidebar, select %{merge_requests_link} to view them.').html_safe % { merge_requests_link: link_to('Settings > Merge requests', project_settings_merge_requests_path(@project)).html_safe }
-
-%section.settings.no-animate{ class: ('expanded' if expanded), data: { testid: 'badges-settings-content' } }
- .settings-header
- %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
- = s_('ProjectSettings|Badges')
- = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
- = expanded ? _('Collapse') : _('Expand')
- %p.gl-text-secondary
- = s_('ProjectSettings|Customize this project\'s badges.')
- = link_to s_('ProjectSettings|What are badges?'), help_page_path('user/project/badges')
- .settings-content
- = render 'shared/badges/badge_settings'
-
-= render_if_exists 'compliance_management/compliance_framework/project_settings', expanded: expanded
-
-= render_if_exists 'projects/settings/default_issue_template'
-
-= render 'projects/service_desk_settings'
-
-%section.settings.advanced-settings.no-animate#js-project-advanced-settings{ class: ('expanded' if expanded), data: { testid: 'advanced-settings-content' } }
- .settings-header
- %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Advanced')
- = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
- = expanded ? _('Collapse') : _('Expand')
- %p.gl-text-secondary= s_('ProjectSettings|Housekeeping, export, archive, change path, transfer, and delete.')
-
- .settings-content
- = render_if_exists 'projects/settings/restore', project: @project
-
- = render Pajamas::CardComponent.new(card_options: { class: 'gl-new-card gl-mt-0' }, header_options: { class: 'gl-new-card-header gl-flex-direction-column' }, body_options: { class: 'gl-new-card-body gl-px-5 gl-py-4' }) do |c|
- - c.with_header do
- .gl-new-card-title-wrapper
- %h4.gl-new-card-title= _('Housekeeping')
- %p.gl-new-card-description
- = _('Runs a number of housekeeping tasks within the current repository, such as compressing file revisions and removing unreachable objects.')
- = link_to _('Learn more.'), help_page_path('administration/housekeeping'), target: '_blank', rel: 'noopener noreferrer'
-
- - c.with_body do
- .gl-display-flex.gl-flex-wrap.gl-gap-3
- = render Pajamas::ButtonComponent.new(method: :post, href: housekeeping_project_path(@project)) do
- = _('Run housekeeping')
- #js-project-prune-unreachable-objects-button{ data: { prune_objects_path: housekeeping_project_path(@project, prune: true), prune_objects_doc_path: help_page_path('administration/housekeeping', anchor: 'prune-unreachable-objects') } }
-
- = render 'export', project: @project
-
- = render_if_exists 'projects/settings/archive'
-
- = render Pajamas::CardComponent.new(card_options: { class: 'gl-new-card rename-repository' }, header_options: { class: 'gl-new-card-header gl-flex-direction-column' }, body_options: { class: 'gl-new-card-body gl-px-5 gl-py-4' }) do |c|
- - c.with_header do
- .gl-new-card-title-wrapper
- %h4.gl-new-card-title.warning-title= _('Change path')
- %p.gl-new-card-description
- - link = link_to('', help_page_path('user/project/settings/index', anchor: 'rename-a-repository'), target: '_blank', rel: 'noopener noreferrer')
- = safe_format(_("A project’s repository name defines its URL (the one you use to access the project via a browser) and its place on the file disk where GitLab is installed. %{link_start}Learn more.%{link_end}"), tag_pair(link, :link_start, :link_end))
-
- - c.with_body do
- = render 'projects/errors'
- = gitlab_ui_form_for @project do |f|
- .form-group
- %p
- %span.gl-font-weight-bold= _("Be careful. Renaming a project's repository can have unintended side effects.")
- = _('You will need to update your local repositories to point to the new location.')
- - if @project.deployment_platform.present?
- %p= _('Your deployment services will be broken, you will need to manually fix the services after renaming.')
- = f.label :path, _('Path'), class: 'label-bold'
+ - c.with_body do
+ = _('On the left sidebar, select %{merge_requests_link} to view them.').html_safe % { merge_requests_link: link_to('Settings > Merge requests', project_settings_merge_requests_path(@project)).html_safe }
+
+ %section.settings.no-animate{ class: ('expanded' if expanded), data: { testid: 'badges-settings-content' } }
+ .settings-header
+ %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
+ = s_('ProjectSettings|Badges')
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
+ = expanded ? _('Collapse') : _('Expand')
+ %p.gl-text-secondary
+ = s_('ProjectSettings|Customize this project\'s badges.')
+ = link_to s_('ProjectSettings|What are badges?'), help_page_path('user/project/badges')
+ .settings-content
+ = render 'shared/badges/badge_settings'
+
+ = render_if_exists 'compliance_management/compliance_framework/project_settings', expanded: expanded
+
+ = render_if_exists 'projects/settings/default_issue_template'
+
+ = render 'projects/service_desk_settings'
+
+ %section.settings.advanced-settings.no-animate#js-project-advanced-settings{ class: ('expanded' if expanded), data: { testid: 'advanced-settings-content' } }
+ .settings-header
+ %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Advanced')
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
+ = expanded ? _('Collapse') : _('Expand')
+ %p.gl-text-secondary= s_('ProjectSettings|Housekeeping, export, archive, change path, transfer, and delete.')
+
+ .settings-content
+ = render_if_exists 'projects/settings/restore', project: @project
+
+ = render Pajamas::CardComponent.new(card_options: { class: 'gl-new-card gl-mt-0' }, header_options: { class: 'gl-new-card-header gl-flex-direction-column' }, body_options: { class: 'gl-new-card-body gl-px-5 gl-py-4' }) do |c|
+ - c.with_header do
+ .gl-new-card-title-wrapper
+ %h4.gl-new-card-title= _('Housekeeping')
+ %p.gl-new-card-description
+ = _('Runs a number of housekeeping tasks within the current repository, such as compressing file revisions and removing unreachable objects.')
+ = link_to _('Learn more.'), help_page_path('administration/housekeeping'), target: '_blank', rel: 'noopener noreferrer'
+
+ - c.with_body do
+ .gl-display-flex.gl-flex-wrap.gl-gap-3
+ = render Pajamas::ButtonComponent.new(method: :post, href: housekeeping_project_path(@project)) do
+ = _('Run housekeeping')
+ #js-project-prune-unreachable-objects-button{ data: { prune_objects_path: housekeeping_project_path(@project, prune: true), prune_objects_doc_path: help_page_path('administration/housekeeping', anchor: 'prune-unreachable-objects') } }
+
+ = render 'export', project: @project
+
+ = render_if_exists 'projects/settings/archive'
+
+ = render Pajamas::CardComponent.new(card_options: { class: 'gl-new-card rename-repository' }, header_options: { class: 'gl-new-card-header gl-flex-direction-column' }, body_options: { class: 'gl-new-card-body gl-px-5 gl-py-4' }) do |c|
+ - c.with_header do
+ .gl-new-card-title-wrapper
+ %h4.gl-new-card-title.warning-title= _('Change path')
+ %p.gl-new-card-description
+ - link = link_to('', help_page_path('user/project/settings/index', anchor: 'rename-a-repository'), target: '_blank', rel: 'noopener noreferrer')
+ = safe_format(_("A project’s repository name defines its URL (the one you use to access the project via a browser) and its place on the file disk where GitLab is installed. %{link_start}Learn more.%{link_end}"), tag_pair(link, :link_start, :link_end))
+
+ - c.with_body do
+ = render 'projects/errors'
+ = gitlab_ui_form_for @project do |f|
.form-group
- .input-group
- .input-group-prepend
- .input-group-text
- #{Gitlab::Utils.append_path(root_url, @project.namespace.full_path)}/
- = f.text_field :path, class: 'form-control gl-form-input-xl', data: { testid: 'project-path-field' }
- = f.submit _('Change path'), class: "btn-danger", data: { testid: 'change-path-button' }, pajamas_button: true
-
- = render 'transfer', project: @project
-
- = render 'remove_fork', project: @project
-
- = render 'remove', project: @project
+ %p
+ %span.gl-font-weight-bold= _("Be careful. Renaming a project's repository can have unintended side effects.")
+ = _('You will need to update your local repositories to point to the new location.')
+ - if @project.deployment_platform.present?
+ %p= _('Your deployment services will be broken, you will need to manually fix the services after renaming.')
+ = f.label :path, _('Path'), class: 'label-bold'
+ .form-group
+ .input-group
+ .input-group-prepend
+ .input-group-text
+ #{Gitlab::Utils.append_path(root_url, @project.namespace.full_path)}/
+ = f.text_field :path, class: 'form-control gl-form-input-xl', data: { testid: 'project-path-field' }
+ = f.submit _('Change path'), class: "btn-danger", data: { testid: 'change-path-button' }, pajamas_button: true
+
+ = render 'transfer', project: @project
+
+ = render 'remove_fork', project: @project
+
+ = render 'remove', project: @project
+- elsif can?(current_user, :archive_project, @project)
+ = render_if_exists 'projects/settings/archive'
.save-project-loader.hide
.center
diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml
index 7ddaf868a35..c2bea4bf43c 100644
--- a/app/views/projects/environments/index.html.haml
+++ b/app/views/projects/environments/index.html.haml
@@ -5,7 +5,7 @@
"can-read-environment" => can?(current_user, :read_environment, @project).to_s,
"can-create-environment" => can?(current_user, :create_environment, @project).to_s,
"new-environment-path" => new_project_environment_path(@project),
- "help-page-path" => help_page_path("ci/environments/index.md"),
+ "help-page-path" => help_page_path("ci/environments/index"),
"project-path" => @project.full_path,
"project-id" => @project.id,
"default-branch-name" => @project.default_branch_or_main,
diff --git a/app/views/projects/feature_flags/new.html.haml b/app/views/projects/feature_flags/new.html.haml
index 3a32a249d1e..1f723cb96b0 100644
--- a/app/views/projects/feature_flags/new.html.haml
+++ b/app/views/projects/feature_flags/new.html.haml
@@ -10,5 +10,5 @@
user_callout_id: Users::CalloutsHelper::FEATURE_FLAGS_NEW_VERSION,
show_user_callout: show_feature_flags_new_version?.to_s,
strategy_type_docs_page_path: help_page_path('operations/feature_flags', anchor: 'feature-flag-strategies'),
- environments_scope_docs_path: help_page_path('ci/environments/index.md', anchor: 'limit-the-environment-scope-of-a-cicd-variable'),
+ environments_scope_docs_path: help_page_path('ci/environments/index', anchor: 'limit-the-environment-scope-of-a-cicd-variable'),
project_id: @project.id } }
diff --git a/app/views/projects/feature_flags_user_lists/edit.html.haml b/app/views/projects/feature_flags_user_lists/edit.html.haml
index 417b6354ec0..70d614fc327 100644
--- a/app/views/projects/feature_flags_user_lists/edit.html.haml
+++ b/app/views/projects/feature_flags_user_lists/edit.html.haml
@@ -3,6 +3,6 @@
- breadcrumb_title s_('FeatureFlags|Edit User List')
- page_title s_('FeatureFlags|Edit User List')
-#js-edit-user-list{ data: { 'user-lists-docs-path' => help_page_path('operations/feature_flags.md', anchor: 'user-list'),
+#js-edit-user-list{ data: { 'user-lists-docs-path' => help_page_path('operations/feature_flags', anchor: 'user-list'),
'user-list-iid' => @user_list.iid,
'project-id' => @project.id } }
diff --git a/app/views/projects/feature_flags_user_lists/new.html.haml b/app/views/projects/feature_flags_user_lists/new.html.haml
index cea55c0ca2a..7f20fc4a9ec 100644
--- a/app/views/projects/feature_flags_user_lists/new.html.haml
+++ b/app/views/projects/feature_flags_user_lists/new.html.haml
@@ -4,6 +4,6 @@
- breadcrumb_title s_('FeatureFlags|New User List')
- page_title s_('FeatureFlags|New User List')
-#js-new-user-list{ data: { 'user-lists-docs-path' => help_page_path('operations/feature_flags.md', anchor: 'user-list'),
+#js-new-user-list{ data: { 'user-lists-docs-path' => help_page_path('operations/feature_flags', anchor: 'user-list'),
'feature-flags-path' => project_feature_flags_path(@project),
'project-id' => @project.id } }
diff --git a/app/views/projects/find_file/show.html.haml b/app/views/projects/find_file/show.html.haml
index 0c760ab82c9..997e7b7f24d 100644
--- a/app/views/projects/find_file/show.html.haml
+++ b/app/views/projects/find_file/show.html.haml
@@ -6,7 +6,7 @@
- blob_path = project_blob_path(@project, @ref)
.file-finder-holder.tree-holder.clearfix.js-file-finder.gl-pt-4{ data: { file_find_url: "#{escape_javascript(project_files_path(@project, @ref, ref_type: @ref_type, format: :json))}", find_tree_url: escape_javascript(tree_path), blob_url_template: escape_javascript(blob_path), ref_type: @ref_type } }
.nav-block.gl-xs-mr-0
- .tree-ref-holder.gl-xs-mb-3.gl-xs-w-full.gl-max-w-26
+ .tree-ref-holder.gl-xs-mb-3.gl-max-w-26
#js-blob-ref-switcher{ data: { project_id: @project.id, ref: @ref, ref_type: @ref_type, namespace: "/-/find_file" } }
%ul.breadcrumb.repo-breadcrumb.gl-flex-nowrap
%li.breadcrumb-item.gl-white-space-nowrap
diff --git a/app/views/projects/forks/index.html.haml b/app/views/projects/forks/index.html.haml
index 49047749b71..fe7d2c9d198 100644
--- a/app/views/projects/forks/index.html.haml
+++ b/app/views/projects/forks/index.html.haml
@@ -8,7 +8,7 @@
- full_count_title = "#{@public_forks_count} public, #{@internal_forks_count} internal, and #{@private_forks_count} private"
#{pluralize(@total_forks_count, 'fork')}: #{full_count_title}
- .gl-display-flex.gl-sm-flex-direction-column.gl-md-align-items-center
+ .gl-display-flex.gl-flex-direction-column.gl-md-flex-direction-row.gl-md-align-items-center
= form_tag request.original_url, method: :get, class: 'project-filter-form gl-display-flex gl-mt-3 gl-md-mt-0', id: 'project-filter-form' do |f|
= search_field_tag :filter_projects, nil, placeholder: _('Search forks'), class: 'projects-list-filter project-filter-form-field form-control input-short gl-flex-grow-1',
spellcheck: false, data: { 'filter-selector' => 'span.namespace-name' }
diff --git a/app/views/projects/forks/new.html.haml b/app/views/projects/forks/new.html.haml
index e9c6b3fcd22..1194a361753 100644
--- a/app/views/projects/forks/new.html.haml
+++ b/app/views/projects/forks/new.html.haml
@@ -9,6 +9,7 @@
project_id: @project.id,
project_name: @project.name,
project_path: @project.path,
+ project_default_branch: @project.default_branch,
project_description: @project.description,
project_visibility: @project.visibility,
restricted_visibility_levels: Gitlab::CurrentSettings.restricted_visibility_levels.to_json } }
diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
index a3569d41714..e766536f12b 100644
--- a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
+++ b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
@@ -7,7 +7,7 @@
%tr.generic-commit-status{ class: ('retried' if retried) }
%td.status
- = render 'ci/status/badge', status: generic_commit_status.detailed_status(current_user)
+ = render 'ci/status/icon', status: generic_commit_status.detailed_status(current_user), show_status_text: true
%td
= generic_commit_status.name
diff --git a/app/views/projects/issues/_new_branch.html.haml b/app/views/projects/issues/_new_branch.html.haml
index 90d99d51d29..68de9c44e38 100644
--- a/app/views/projects/issues/_new_branch.html.haml
+++ b/app/views/projects/issues/_new_branch.html.haml
@@ -43,7 +43,7 @@
%li.droplab-item-ignore.gl-ml-3.gl-mr-3.gl-mt-5
- if can_create_confidential_merge_request?
- #js-forked-project{ data: { namespace_path: @project.namespace.full_path, project_path: @project.full_path, new_fork_path: new_project_fork_path(@project), help_page_path: help_page_path('user/project/merge_requests/index.md') } }
+ #js-forked-project{ data: { namespace_path: @project.namespace.full_path, project_path: @project.full_path, new_fork_path: new_project_fork_path(@project), help_page_path: help_page_path('user/project/merge_requests/index') } }
.form-group
%label{ for: 'new-branch-name' }
= _('Branch name')
diff --git a/app/views/projects/issues/_related_branches.html.haml b/app/views/projects/issues/_related_branches.html.haml
index 21f1a4d19fa..1a6edb288b5 100644
--- a/app/views/projects/issues/_related_branches.html.haml
+++ b/app/views/projects/issues/_related_branches.html.haml
@@ -16,9 +16,9 @@
%li.list-item{ class: "gl-py-0! gl-border-0!" }
.item-body.gl-display-flex.align-items-center.gl-px-3.gl-pr-2.gl-mx-n2
.item-contents.gl-display-flex.gl-align-items-center.gl-flex-wrap.gl-flex-grow-1.gl-min-h-7
- .item-title.gl-display-flex.mb-xl-0.gl-min-w-0
+ .item-title.gl-display-flex.mb-xl-0.gl-min-w-0.gl-align-items-center
- if branch[:pipeline_status].present?
- %span.related-branch-ci-status
+ %span.gl-mt-n2.gl-mb-n2.gl-mr-3
= render 'ci/status/icon', status: branch[:pipeline_status]
%span.related-branch-info
%strong
diff --git a/app/views/projects/issues/service_desk/_issue.html.haml b/app/views/projects/issues/service_desk/_issue.html.haml
index 66b2eabac9d..dbc6e613e8b 100644
--- a/app/views/projects/issues/service_desk/_issue.html.haml
+++ b/app/views/projects/issues/service_desk/_issue.html.haml
@@ -1,4 +1,4 @@
-%li{ id: dom_id(issue), class: issue_css_classes(issue), url: issue_path(issue), data: { labels: issue.label_ids, id: issue.id, qa_selector: 'issue_container', qa_issue_title: issue.title } }
+%li{ id: dom_id(issue), class: issue_css_classes(issue), url: issue_path(issue), data: { labels: issue.label_ids, id: issue.id } }
.issuable-info-container
.issuable-main-info
.issue-title.title
diff --git a/app/views/projects/issues/service_desk/_issue_estimate.html.haml b/app/views/projects/issues/service_desk/_issue_estimate.html.haml
index c49bf626f4e..c6fa8b64dec 100644
--- a/app/views/projects/issues/service_desk/_issue_estimate.html.haml
+++ b/app/views/projects/issues/service_desk/_issue_estimate.html.haml
@@ -1,7 +1,7 @@
- issue = local_assigns.fetch(:issue)
- if issue.time_estimate > 0
- %span.issuable-estimate.d-none.d-sm-inline-block.has-tooltip{ data: { container: 'body', qa_selector: 'issuable_estimate' }, title: _('Estimate') }
+ %span.issuable-estimate.d-none.d-sm-inline-block.has-tooltip{ data: { container: 'body' }, title: _('Estimate') }
&nbsp;
= sprite_icon('timer', css_class: 'issue-estimate-icon')
= Gitlab::TimeTrackingFormatter.output(issue.time_estimate)
diff --git a/app/views/projects/jobs/_header.html.haml b/app/views/projects/jobs/_header.html.haml
index 018ff093475..a77e8f2d0b4 100644
--- a/app/views/projects/jobs/_header.html.haml
+++ b/app/views/projects/jobs/_header.html.haml
@@ -2,7 +2,7 @@
.content-block.build-header.top-area.page-content-header
.header-content
- = render 'ci/status/badge', status: @build.detailed_status(current_user), link: false, title: @build.status_title
+ = render 'ci/status/icon', status: @build.detailed_status(current_user), show_status_text: true
%strong
Job
= link_to "##{@build.id}", project_job_path(@project, @build), class: 'js-build-id'
diff --git a/app/views/projects/merge_requests/_nav_btns.html.haml b/app/views/projects/merge_requests/_nav_btns.html.haml
index 80085cc6a34..2c0a8d831e4 100644
--- a/app/views/projects/merge_requests/_nav_btns.html.haml
+++ b/app/views/projects/merge_requests/_nav_btns.html.haml
@@ -9,12 +9,9 @@
= _("New merge request")
.dropdown.gl-dropdown
- = button_tag type: 'button', class: "btn dropdown-toggle btn-default btn-md gl-button gl-dropdown gl-dropdown-toggle btn-default-tertiary dropdown-icon-only dropdown-toggle-no-caret has-tooltip gl-display-none! gl-md-display-inline-flex!", data: { toggle: 'dropdown', title: _('Actions') } do
- = sprite_icon "ellipsis_v", size: 16, css_class: "dropdown-icon gl-icon"
- %span.gl-sr-only
- = _('Actions')
- = button_tag type: 'button', class: "btn dropdown-toggle btn-default btn-md btn-block gl-button gl-dropdown-toggle gl-md-display-none!", data: { 'toggle' => 'dropdown' } do
- %span.gl-dropdown-button-text= _('Actions')
+ = render Pajamas::ButtonComponent.new(type: :button, category: :tertiary, variant: :default, icon: 'ellipsis_v', button_options: { data: { toggle: 'dropdown' }, class: 'has-tooltip gl-display-none! gl-md-display-inline-flex!', title: _("Actions")})
+ = render Pajamas::ButtonComponent.new(type: :button, variant: :default, button_options: { data: { 'toggle' => 'dropdown' }, class: 'gl-md-display-none!'}) do
+ = _('Actions')
= sprite_icon "chevron-down", size: 16, css_class: "dropdown-icon gl-icon"
.dropdown-menu.dropdown-menu-right
.gl-dropdown-inner
diff --git a/app/views/projects/merge_requests/_page.html.haml b/app/views/projects/merge_requests/_page.html.haml
index 637980bd2f8..03a1f2f3179 100644
--- a/app/views/projects/merge_requests/_page.html.haml
+++ b/app/views/projects/merge_requests/_page.html.haml
@@ -6,7 +6,7 @@
- page_title "#{@merge_request.title} (#{@merge_request.to_reference})", _("Merge requests")
- page_description @merge_request.description_html
- page_card_attributes @merge_request.card_attributes
-- suggest_changes_help_path = help_page_path('user/project/merge_requests/reviews/suggestions.md')
+- suggest_changes_help_path = help_page_path('user/project/merge_requests/reviews/suggestions')
- mr_action = j(params[:tab].presence || 'show')
- add_page_specific_style 'page_bundles/issuable'
- add_page_specific_style 'page_bundles/design_management'
diff --git a/app/views/projects/mirrors/_authentication_method.html.haml b/app/views/projects/mirrors/_authentication_method.html.haml
index c4cf128a62a..f72b0d582b7 100644
--- a/app/views/projects/mirrors/_authentication_method.html.haml
+++ b/app/views/projects/mirrors/_authentication_method.html.haml
@@ -5,7 +5,7 @@
= f.label :auth_method, _('Authentication method'), class: 'label-bold'
= f.select :auth_method,
options_for_select(auth_options, mirror.auth_method),
- {}, { class: "custom-select gl-form-select js-mirror-auth-type gl-max-w-34 gl-display-block", data: { qa_selector: 'authentication_method_field' } }
+ {}, { class: "custom-select gl-form-select js-mirror-auth-type gl-max-w-34 gl-display-block", data: { testid: 'authentication-method-field' } }
= f.hidden_field :auth_method, value: "password", class: "js-hidden-mirror-auth-type"
.form-group
diff --git a/app/views/projects/mirrors/_branch_filter.html.haml b/app/views/projects/mirrors/_branch_filter.html.haml
index 7d90906bfe8..39e82fd5711 100644
--- a/app/views/projects/mirrors/_branch_filter.html.haml
+++ b/app/views/projects/mirrors/_branch_filter.html.haml
@@ -6,4 +6,4 @@
= _('Mirror only protected branches')
- c.with_help_text do
= _('If enabled, only protected branches will be mirrored.')
- = link_to _('Learn more.'), help_page_path('user/project/repository/mirror/index.md', anchor: 'mirror-only-protected-branches'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('user/project/repository/mirror/index', anchor: 'mirror-only-protected-branches'), target: '_blank', rel: 'noopener noreferrer'
diff --git a/app/views/projects/mirrors/_mirror_repos.html.haml b/app/views/projects/mirrors/_mirror_repos.html.haml
index 00837ce1c73..7b27062f782 100644
--- a/app/views/projects/mirrors/_mirror_repos.html.haml
+++ b/app/views/projects/mirrors/_mirror_repos.html.haml
@@ -3,14 +3,14 @@
- mirror_settings_enabled = can?(current_user, :admin_remote_mirror, @project)
- mirror_settings_class = "#{'expanded' if expanded} #{'js-mirror-settings' if mirror_settings_enabled}".strip
-%section.settings.project-mirror-settings.no-animate#js-push-remote-settings{ class: mirror_settings_class, data: { qa_selector: 'mirroring_repositories_settings_content' } }
+%section.settings.project-mirror-settings.no-animate#js-push-remote-settings{ class: mirror_settings_class, data: { testid: 'mirroring-repositories-settings-content' } }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Mirroring repositories')
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= _('Set up your project to automatically push and/or pull changes to/from another repository. Branches, tags, and commits will be synced automatically.')
- = link_to _('How do I mirror repositories?'), help_page_path('user/project/repository/mirror/index.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('How do I mirror repositories?'), help_page_path('user/project/repository/mirror/index'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
@@ -35,7 +35,7 @@
%div= form_errors(@project)
.form-group.has-feedback
= label_tag :url, _('Git repository URL'), class: 'label-light'
- = text_field_tag :url, nil, class: 'form-control gl-form-input js-mirror-url js-repo-url gl-form-input-xl', placeholder: _('Input the remote repository URL'), required: true, pattern: "(#{protocols}):\/\/.+", autocomplete: 'new-password', data: { qa_selector: 'mirror_repository_url_field' }
+ = text_field_tag :url, nil, class: 'form-control gl-form-input js-mirror-url js-repo-url gl-form-input-xl', placeholder: _('Input the remote repository URL'), required: true, pattern: "(#{protocols}):\/\/.+", autocomplete: 'new-password', data: { testid: 'mirror-repository-url-field' }
= render 'projects/mirrors/instructions'
@@ -43,7 +43,7 @@
= render 'projects/mirrors/branch_filter'
- = f.submit _('Mirror repository'), class: 'js-mirror-submit', name: :update_remote_mirror, pajamas_button: true, data: { qa_selector: 'mirror_repository_button' }
+ = f.submit _('Mirror repository'), class: 'js-mirror-submit', name: :update_remote_mirror, pajamas_button: true, data: { testid: 'mirror-repository-button' }
= render Pajamas::ButtonComponent.new(button_options: { type: 'reset', class: 'gl-ml-2 js-toggle-button' }) do
= _('Cancel')
diff --git a/app/views/projects/mirrors/_mirror_repos_form.html.haml b/app/views/projects/mirrors/_mirror_repos_form.html.haml
index 8378a74311f..24cda3445de 100644
--- a/app/views/projects/mirrors/_mirror_repos_form.html.haml
+++ b/app/views/projects/mirrors/_mirror_repos_form.html.haml
@@ -1,7 +1,7 @@
.form-group
= label_tag :mirror_direction, _('Mirror direction'), class: 'label-light'
.select-wrapper
- = select_tag :mirror_direction, options_for_select([[_('Push'), 'push']]), class: 'form-control gl-form-select select-control js-mirror-direction gl-max-w-34 gl-display-block', disabled: true, data: { qa_selector: 'mirror_direction_field' }
+ = select_tag :mirror_direction, options_for_select([[_('Push'), 'push']]), class: 'form-control gl-form-select select-control js-mirror-direction gl-max-w-34 gl-display-block', disabled: true, data: { testid: 'mirror-direction-field' }
= sprite_icon('chevron-down', css_class: "gl-icon gl-absolute gl-top-3 gl-right-3 gl-text-gray-200")
= render partial: "projects/mirrors/mirror_repos_push", locals: { f: f }
diff --git a/app/views/projects/mirrors/_mirror_repos_list.html.haml b/app/views/projects/mirrors/_mirror_repos_list.html.haml
index 59611db941f..5e3c4889d1d 100644
--- a/app/views/projects/mirrors/_mirror_repos_list.html.haml
+++ b/app/views/projects/mirrors/_mirror_repos_list.html.haml
@@ -17,24 +17,24 @@
= render_if_exists 'projects/mirrors/table_pull_row'
- @project.remote_mirrors.each_with_index do |mirror, index|
- next if mirror.new_record?
- %tr.rspec-mirrored-repository-row{ class: ('bg-secondary' if mirror.disabled?), data: { qa_selector: 'mirrored_repository_row_container' } }
- %td{ data: { qa_selector: 'mirror_repository_url_content' } }
+ %tr.rspec-mirrored-repository-row{ class: ('bg-secondary' if mirror.disabled?), data: { testid: 'mirrored-repository-row-container' } }
+ %td{ data: { testid: 'mirror-repository-url-content' } }
= mirror.safe_url || _('Invalid URL')
= render_if_exists 'projects/mirrors/mirror_branches_setting_badge', record: mirror
%td= _('Push')
%td
= mirror.last_update_started_at.present? ? time_ago_with_tooltip(mirror.last_update_started_at) : _('Never')
- %td{ data: { qa_selector: 'mirror_last_update_at_content' } }= mirror.last_update_at.present? ? time_ago_with_tooltip(mirror.last_update_at) : _('Never')
+ %td{ data: { testid: 'mirror-last-update-at-content' } }= mirror.last_update_at.present? ? time_ago_with_tooltip(mirror.last_update_at) : _('Never')
%td
- if mirror.disabled?
= render 'projects/mirrors/disabled_mirror_badge'
- if mirror.last_error.present?
- = gl_badge_tag _('Error'), { variant: :danger }, { data: { toggle: 'tooltip', html: 'true', qa_selector: 'mirror_error_badge_content' }, title: html_escape(mirror.last_error.try(:strip)) }
+ = gl_badge_tag _('Error'), { variant: :danger }, { data: { toggle: 'tooltip', html: 'true', testid: 'mirror-error-badge-content' }, title: html_escape(mirror.last_error.try(:strip)) }
%td
- if mirror_settings_enabled
.btn-group.mirror-actions-group{ role: 'group' }
- if mirror.ssh_key_auth?
- = clipboard_button(text: mirror.ssh_public_key, variant: :default, category: :primary, size: :medium, title: _('Copy SSH public key'), testid: 'copy_public_key_button')
+ = clipboard_button(text: mirror.ssh_public_key, variant: :default, category: :primary, size: :medium, title: _('Copy SSH public key'), testid: 'copy-public-key-button')
= render 'shared/remote_mirror_update_button', remote_mirror: mirror
= render Pajamas::ButtonComponent.new(variant: :danger,
icon: 'remove',
diff --git a/app/views/projects/mirrors/_mirror_repos_push.html.haml b/app/views/projects/mirrors/_mirror_repos_push.html.haml
index 5b02d650989..7f0298191cd 100644
--- a/app/views/projects/mirrors/_mirror_repos_push.html.haml
+++ b/app/views/projects/mirrors/_mirror_repos_push.html.haml
@@ -16,4 +16,4 @@
= _('Keep divergent refs')
- c.with_help_text do
- link_opening_tag = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe
- = html_escape(_('Do not force push over diverged refs. After the mirror is created, this setting can only be modified using the API. %{mirroring_docs_link_start}Learn more about this option%{link_closing_tag} and %{mirroring_api_docs_link_start}the API.%{link_closing_tag}')) % { mirroring_docs_link_start: link_opening_tag % {url: help_page_path('user/project/repository/mirror/push.md', anchor: 'keep-divergent-refs')}, mirroring_api_docs_link_start: link_opening_tag % {url: help_page_path('api/remote_mirrors')}, link_closing_tag: '</a>'.html_safe }
+ = html_escape(_('Do not force push over diverged refs. After the mirror is created, this setting can only be modified using the API. %{mirroring_docs_link_start}Learn more about this option%{link_closing_tag} and %{mirroring_api_docs_link_start}the API.%{link_closing_tag}')) % { mirroring_docs_link_start: link_opening_tag % {url: help_page_path('user/project/repository/mirror/push', anchor: 'keep-divergent-refs')}, mirroring_api_docs_link_start: link_opening_tag % {url: help_page_path('api/remote_mirrors')}, link_closing_tag: '</a>'.html_safe }
diff --git a/app/views/projects/mirrors/_ssh_host_keys.html.haml b/app/views/projects/mirrors/_ssh_host_keys.html.haml
index d367f383e5a..cd9580d15e9 100644
--- a/app/views/projects/mirrors/_ssh_host_keys.html.haml
+++ b/app/views/projects/mirrors/_ssh_host_keys.html.haml
@@ -3,13 +3,13 @@
- verified_at = mirror.ssh_known_hosts_verified_at
.form-group.js-ssh-host-keys-section{ class: ('collapse' unless mirror.ssh_mirror_url?) }
- = render Pajamas::ButtonComponent.new(button_options: { class: 'js-detect-host-keys gl-mr-3', data: { qa_selector: 'detect_host_keys' } }) do
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-detect-host-keys gl-mr-3', data: { testid: 'detect-host-keys' } }) do
= gl_loading_icon(inline: true, css_class: 'js-spinner gl-display-none gl-mr-2')
= _('Detect host keys')
.fingerprint-ssh-info.js-fingerprint-ssh-info.gl-mt-3.gl-mb-3{ class: ('collapse' unless mirror.ssh_mirror_url?) }
%label.label-bold
= _('Fingerprints')
- .fingerprints-list.js-fingerprints-list{ data: { qa_selector: 'fingerprints_list' } }
+ .fingerprints-list.js-fingerprints-list{ data: { testid: 'fingerprints-list' } }
- mirror.ssh_known_hosts_fingerprints.each do |fp|
%code= fp.fingerprint_sha256 || fp.fingerprint
- if verified_at
diff --git a/app/views/projects/ml/model_versions/show.html.haml b/app/views/projects/ml/model_versions/show.html.haml
new file mode 100644
index 00000000000..0b3d5462a89
--- /dev/null
+++ b/app/views/projects/ml/model_versions/show.html.haml
@@ -0,0 +1,6 @@
+- add_to_breadcrumbs s_('ModelRegistry|Model registry'), project_ml_models_path(@project)
+- add_to_breadcrumbs @model_version.name, project_ml_model_path(@project, @model)
+- breadcrumb_title @model_version.version
+- page_title "#{@model_version.name} / #{@model_version.version}"
+
+= render(Projects::Ml::ShowMlModelVersionComponent.new(model_version: @model_version))
diff --git a/app/views/projects/ml/models/index.html.haml b/app/views/projects/ml/models/index.html.haml
index 08f0db257ae..ffe7ee3397e 100644
--- a/app/views/projects/ml/models/index.html.haml
+++ b/app/views/projects/ml/models/index.html.haml
@@ -1,4 +1,4 @@
- breadcrumb_title s_('ModelRegistry|Model registry')
- page_title s_('ModelRegistry|Model registry')
-= render(Projects::Ml::ModelsIndexComponent.new(paginator: @paginator))
+= render(Projects::Ml::ModelsIndexComponent.new(paginator: @paginator, model_count: @model_count))
diff --git a/app/views/projects/pages/_access.html.haml b/app/views/projects/pages/_access.html.haml
index 6eab31075d4..1e18e528665 100644
--- a/app/views/projects/pages/_access.html.haml
+++ b/app/views/projects/pages/_access.html.haml
@@ -1,7 +1,7 @@
- if @project.pages_deployed?
- pages_url = build_pages_url(@project, with_unique_domain: true)
- = render Pajamas::CardComponent.new(card_options: { class: 'gl-mb-5', data: { qa_selector: 'access_page_container' } }, footer_options: { class: 'gl-alert-warning' }) do |c|
+ = render Pajamas::CardComponent.new(card_options: { class: 'gl-mb-5', data: { testid: 'access-page-container' } }, footer_options: { class: 'gl-alert-warning' }) do |c|
- c.with_header do
= s_('GitLabPages|Access pages')
- c.with_body do
diff --git a/app/views/projects/pages/_waiting.html.haml b/app/views/projects/pages/_waiting.html.haml
index 0613ffc4809..7aad6d6e0d2 100644
--- a/app/views/projects/pages/_waiting.html.haml
+++ b/app/views/projects/pages/_waiting.html.haml
@@ -5,7 +5,7 @@
.row.gl-align-items-center.gl-justify-content-center
.text-content.gl-text-center.order-md-1
%h4= s_("GitLabPages|Waiting for the Pages Pipeline to complete...")
- %p= s_("GitLabPages|Your Project has been configured for Pages. Now we have to wait for the Pipeline to succeed for the first time.")
+ %p= s_("GitLabPages|Your project is configured for GitLab Pages and the pipeline is running...")
= render Pajamas::ButtonComponent.new(variant: :confirm, href: project_pipelines_path(@project)) do
= s_("GitLabPages|Check the Pipeline Status")
= render Pajamas::ButtonComponent.new(href: new_namespace_project_pages_path) do
diff --git a/app/views/projects/pages/new.html.haml b/app/views/projects/pages/new.html.haml
index 89f8f62ea83..56dfc69d740 100644
--- a/app/views/projects/pages/new.html.haml
+++ b/app/views/projects/pages/new.html.haml
@@ -1,10 +1,5 @@
- @breadcrumb_link = project_pages_path(@project)
- page_title s_('GitLabPages|Pages')
-- if Feature.enabled?(:use_pipeline_wizard_for_pages, @project.group)
- #js-pages{ data: @pipeline_wizard_data }
-- else
- = render 'header'
-
- = render 'use'
+#js-pages{ data: @pipeline_wizard_data }
diff --git a/app/views/projects/pages_domains/_certificate.html.haml b/app/views/projects/pages_domains/_certificate.html.haml
index f80fd495695..1136abe9884 100644
--- a/app/views/projects/pages_domains/_certificate.html.haml
+++ b/app/views/projects/pages_domains/_certificate.html.haml
@@ -21,7 +21,7 @@
label_position: :hidden)
= f.hidden_field :auto_ssl_enabled?, class: "js-project-feature-toggle-input"
%p.gl-text-secondary.gl-mt-1
- - docs_link_url = help_page_path("user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration.md")
+ - docs_link_url = help_page_path("user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration")
- docs_link_start = "<a href=\"%{docs_link_url}\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"text-nowrap\">".html_safe % { docs_link_url: docs_link_url }
- docs_link_end = "</a>".html_safe
= _("Let's Encrypt is a free, automated, and open certificate authority (CA) that gives digital certificates in order to enable HTTPS (SSL/TLS) for websites. Learn more about Let's Encrypt configuration by following the %{docs_link_start}documentation on GitLab Pages%{docs_link_end}.").html_safe % { docs_link_url: docs_link_url, docs_link_start: docs_link_start, docs_link_end: docs_link_end }
diff --git a/app/views/projects/pages_domains/_dns.html.haml b/app/views/projects/pages_domains/_dns.html.haml
index 9ca9360199d..bec35dba147 100644
--- a/app/views/projects/pages_domains/_dns.html.haml
+++ b/app/views/projects/pages_domains/_dns.html.haml
@@ -27,5 +27,5 @@
.input-group-append
= deprecated_clipboard_button(target: '#domain_verification', class: 'btn-default d-none d-sm-block')
%p.form-text.text-muted
- - link_to_help = link_to(_('verify ownership'), help_page_path('user/project/pages/custom_domains_ssl_tls_certification/index.md', anchor: '4-verify-the-domains-ownership'))
+ - link_to_help = link_to(_('verify ownership'), help_page_path('user/project/pages/custom_domains_ssl_tls_certification/index', anchor: '4-verify-the-domains-ownership'))
= _("To %{link_to_help} of your domain, add the above key to a TXT record within your DNS configuration within seven days.").html_safe % { link_to_help: link_to_help }
diff --git a/app/views/projects/pages_domains/_helper_text.html.haml b/app/views/projects/pages_domains/_helper_text.html.haml
index f29cb0609e6..4ad341c1394 100644
--- a/app/views/projects/pages_domains/_helper_text.html.haml
+++ b/app/views/projects/pages_domains/_helper_text.html.haml
@@ -1,4 +1,4 @@
-- docs_link_url = help_page_path("user/project/pages/custom_domains_ssl_tls_certification/index.md", anchor: "adding-an-ssltls-certificate-to-pages")
+- docs_link_url = help_page_path("user/project/pages/custom_domains_ssl_tls_certification/index", anchor: "adding-an-ssltls-certificate-to-pages")
- docs_link_start = "<a href=\"%{docs_link_url}\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"text-nowrap\">".html_safe % { docs_link_url: docs_link_url }
- docs_link_end = "</a>".html_safe
diff --git a/app/views/projects/protected_tags/shared/_create_protected_tag.html.haml b/app/views/projects/protected_tags/shared/_create_protected_tag.html.haml
index 8dcc59a09d0..cd49f064613 100644
--- a/app/views/projects/protected_tags/shared/_create_protected_tag.html.haml
+++ b/app/views/projects/protected_tags/shared/_create_protected_tag.html.haml
@@ -14,7 +14,7 @@
.create_access_levels-container
= yield :create_access_levels
- = f.submit _('Protect'), pajamas_button: true, disabled: true, data: { qa_selector: 'protect_tag_button' }
+ = f.submit _('Protect'), pajamas_button: true, disabled: true, data: { testid: 'protect-tag-button' }
= render Pajamas::ButtonComponent.new(button_options: { type: 'reset', class: 'gl-ml-2 js-toggle-button' }) do
= _('Cancel')
diff --git a/app/views/projects/protected_tags/shared/_dropdown.html.haml b/app/views/projects/protected_tags/shared/_dropdown.html.haml
index 758df7b3c1e..b1e29768be2 100644
--- a/app/views/projects/protected_tags/shared/_dropdown.html.haml
+++ b/app/views/projects/protected_tags/shared/_dropdown.html.haml
@@ -6,7 +6,7 @@
footer_content: true,
data: { show_no: true, show_any: true, show_upcoming: true,
selected: params[:protected_tag_name],
- project_id: @project.try(:id), qa_selector: 'tags_dropdown' } }) do
+ project_id: @project.try(:id), testid: 'tags-dropdown' } }) do
%ul.dropdown-footer-list
%li
diff --git a/app/views/projects/protected_tags/shared/_index.html.haml b/app/views/projects/protected_tags/shared/_index.html.haml
index f71ecc3a7c5..5c810b55bec 100644
--- a/app/views/projects/protected_tags/shared/_index.html.haml
+++ b/app/views/projects/protected_tags/shared/_index.html.haml
@@ -1,6 +1,6 @@
- expanded = expanded_by_default?
-%section.settings.no-animate#js-protected-tags-settings{ class: ('expanded' if expanded), data: { qa_selector: 'protected_tag_settings_content' } }
+%section.settings.no-animate#js-protected-tags-settings{ class: ('expanded' if expanded), data: { testid: 'protected-tag-settings-content' } }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= s_("ProtectedTag|Protected tags")
diff --git a/app/views/projects/readme_templates/default.md.tt b/app/views/projects/readme_templates/default.md.tt
index 779b87336ea..7432918be21 100644
--- a/app/views/projects/readme_templates/default.md.tt
+++ b/app/views/projects/readme_templates/default.md.tt
@@ -38,7 +38,7 @@ git push -uf origin <%= params[:default_branch] %>
Use the built-in continuous integration in GitLab.
- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html)
-- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing(SAST)](https://docs.gitlab.com/ee/user/application_security/sast/)
+- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/ee/user/application_security/sast/)
- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html)
- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/)
- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html)
@@ -47,9 +47,10 @@ Use the built-in continuous integration in GitLab.
# Editing this README
-When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thank you to [makeareadme.com](https://www.makeareadme.com/) for this template.
+When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thanks to [makeareadme.com](https://www.makeareadme.com/) for this template.
## Suggestions for a good README
+
Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
## Name
diff --git a/app/views/projects/runners/_group_runners.html.haml b/app/views/projects/runners/_group_runners.html.haml
index 2d435a7ce9d..a79b73f6f61 100644
--- a/app/views/projects/runners/_group_runners.html.haml
+++ b/app/views/projects/runners/_group_runners.html.haml
@@ -1,4 +1,4 @@
-- link = link_to _('Runner API'), help_page_path('api/runners.md')
+- link = link_to _('Runner API'), help_page_path('api/runners')
%h4
= _('Group runners')
diff --git a/app/views/projects/runners/_runner.html.haml b/app/views/projects/runners/_runner.html.haml
index 12432cd3484..96b87767690 100644
--- a/app/views/projects/runners/_runner.html.haml
+++ b/app/views/projects/runners/_runner.html.haml
@@ -26,7 +26,8 @@
- elsif runner.project_type?
= form_for [@project, @project.runner_projects.new] do |f|
= f.hidden_field :runner_id, value: runner.id
- = f.submit _('Enable for this project'), class: 'btn gl-button'
+ = render Pajamas::ButtonComponent.new(variant: :default, size: :small, type: :submit) do
+ = _('Enable for this project')
- if runner.description.present?
%p.gl-my-2
= runner.description
diff --git a/app/views/projects/settings/access_tokens/_form.html.haml b/app/views/projects/settings/access_tokens/_form.html.haml
index 919462a0f62..ee993962c7a 100644
--- a/app/views/projects/settings/access_tokens/_form.html.haml
+++ b/app/views/projects/settings/access_tokens/_form.html.haml
@@ -7,7 +7,7 @@
resource: @project,
token: @resource_access_token,
scopes: @scopes,
- access_levels: ProjectMember.permissible_access_level_roles(current_user, @project),
+ access_levels: ProjectMember.permissible_access_level_roles_for_project_access_token(current_user, @project),
default_access_level: Gitlab::Access::GUEST,
prefix: :resource_access_token,
description_prefix: :project_access_token,
diff --git a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
index fd27b125602..7011595e075 100644
--- a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
+++ b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
@@ -9,9 +9,9 @@
- base_domain_path = help_page_path('user/project/clusters/gitlab_managed_clusters', anchor: 'base-domain')
- base_domain_link_start = link_start % { url: base_domain_path }
-- help_link_continouos = link_to sprite_icon('question-o'), help_page_path('topics/autodevops/stages.md', anchor: 'auto-deploy'), target: '_blank', rel: 'noopener noreferrer'
-- help_link_timed = link_to sprite_icon('question-o'), help_page_path('topics/autodevops/cicd_variables.md', anchor: 'timed-incremental-rollout-to-production'), target: '_blank', rel: 'noopener noreferrer'
-- help_link_incremental = link_to sprite_icon('question-o'), help_page_path('topics/autodevops/cicd_variables.md', anchor: 'incremental-rollout-to-production'), target: '_blank', rel: 'noopener noreferrer'
+- help_link_continouos = link_to sprite_icon('question-o'), help_page_path('topics/autodevops/stages', anchor: 'auto-deploy'), target: '_blank', rel: 'noopener noreferrer'
+- help_link_timed = link_to sprite_icon('question-o'), help_page_path('topics/autodevops/cicd_variables', anchor: 'timed-incremental-rollout-to-production'), target: '_blank', rel: 'noopener noreferrer'
+- help_link_incremental = link_to sprite_icon('question-o'), help_page_path('topics/autodevops/cicd_variables', anchor: 'incremental-rollout-to-production'), target: '_blank', rel: 'noopener noreferrer'
.row
.col-lg-12
@@ -22,7 +22,7 @@
= f.fields_for :auto_devops_attributes, @auto_devops do |form|
= render Pajamas::CardComponent.new(card_options: { class: 'gl-mb-3' }, footer_options: { class: "js-extra-settings #{auto_devops_enabled || 'hidden'}", data: { testid: 'extra-auto-devops-settings' } }) do |c|
- c.with_body do
- - autodevops_help_link = link_to _('Learn more.'), help_page_path('topics/autodevops/index.md'), target: '_blank', rel: 'noopener noreferrer'
+ - autodevops_help_link = link_to _('Learn more.'), help_page_path('topics/autodevops/index'), target: '_blank', rel: 'noopener noreferrer'
- auto_devops_badge = auto_devops_enabled ? (gl_badge_tag badge_for_auto_devops_scope(@project), { variant: :info }, { class: 'js-instance-default-badge gl-ml-3 gl-mt-n1'}) : ''
= form.gitlab_ui_checkbox_component :enabled,
(s_('CICD|Default to Auto DevOps pipeline') + auto_devops_badge).html_safe,
diff --git a/app/views/projects/settings/merge_requests/_merge_request_merge_commit_template.html.haml b/app/views/projects/settings/merge_requests/_merge_request_merge_commit_template.html.haml
index da1965f549c..0a6f940e41a 100644
--- a/app/views/projects/settings/merge_requests/_merge_request_merge_commit_template.html.haml
+++ b/app/views/projects/settings/merge_requests/_merge_request_merge_commit_template.html.haml
@@ -9,5 +9,5 @@
%p.form-text.text-muted
= s_('ProjectSettings|Leave empty to use default template.')
= sprintf(s_('ProjectSettings|Maximum %{maxLength} characters.'), { maxLength: Project::MAX_COMMIT_TEMPLATE_LENGTH })
- - link = link_to('', help_page_path('user/project/merge_requests/commit_templates.md'), target: '_blank', rel: 'noopener noreferrer')
+ - link = link_to('', help_page_path('user/project/merge_requests/commit_templates'), target: '_blank', rel: 'noopener noreferrer')
= safe_format(s_('ProjectSettings|%{link_start}What variables can I use?%{link_end}'), tag_pair(link, :link_start, :link_end))
diff --git a/app/views/projects/settings/merge_requests/_merge_request_merge_method_settings.html.haml b/app/views/projects/settings/merge_requests/_merge_request_merge_method_settings.html.haml
index dd32d3f9d92..891bd62c0a4 100644
--- a/app/views/projects/settings/merge_requests/_merge_request_merge_method_settings.html.haml
+++ b/app/views/projects/settings/merge_requests/_merge_request_merge_method_settings.html.haml
@@ -12,7 +12,7 @@
- ffOnly = s_('ProjectSettings|Fast-forward merges only.')
- ffConflictRebase = s_('ProjectSettings|When there is a merge conflict, the user is given the option to rebase.')
- ffTrains = s_('ProjectSettings|If merge trains are enabled, merging is only possible if the branch can be rebased without conflicts.')
-- ffTrainsHelp = link_to s_('ProjectSettings|What are merge trains?'), help_page_path('ci/pipelines/merge_trains.md', anchor: 'enable-merge-trains'), target: '_blank', rel: 'noopener noreferrer'
+- ffTrainsHelp = link_to s_('ProjectSettings|What are merge trains?'), help_page_path('ci/pipelines/merge_trains', anchor: 'enable-merge-trains'), target: '_blank', rel: 'noopener noreferrer'
- ffTrainsWithFastForward = (noMergeCommit + "<br />" + ffOnly + "<br />" + ffConflictRebase + "<br />" + ffTrains + " " + ffTrainsHelp).html_safe
- ffTrainsWithoutFastForward = (noMergeCommit + "<br />" + ffOnly + "<br />" + ffConflictRebase).html_safe
@@ -22,7 +22,7 @@
%b= s_('ProjectSettings|Merge method')
%p.text-secondary
= s_('ProjectSettings|Determine what happens to the commit history when you merge a merge request.')
- = link_to s_('ProjectSettings|How do they differ?'), help_page_path('user/project/merge_requests/methods/index.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to s_('ProjectSettings|How do they differ?'), help_page_path('user/project/merge_requests/methods/index'), target: '_blank', rel: 'noopener noreferrer'
= form.gitlab_ui_radio_component :merge_method,
:merge,
labelMerge,
@@ -35,4 +35,4 @@
:ff,
labelFastForward,
help_text: ffTrainsHelpFullHelpText,
- radio_options: { data: { qa_selector: 'merge_ff_radio' } }
+ radio_options: { data: { testid: 'merge-ff-radio' } }
diff --git a/app/views/projects/settings/merge_requests/_merge_request_merge_suggestions_settings.html.haml b/app/views/projects/settings/merge_requests/_merge_request_merge_suggestions_settings.html.haml
index 501288f727b..5aa7449c72f 100644
--- a/app/views/projects/settings/merge_requests/_merge_request_merge_suggestions_settings.html.haml
+++ b/app/views/projects/settings/merge_requests/_merge_request_merge_suggestions_settings.html.haml
@@ -9,5 +9,5 @@
%p.form-text.text-muted
= s_('ProjectSettings|Leave empty to use default template.')
= sprintf(s_('ProjectSettings|Maximum %{maxLength} characters.'), { maxLength: Project::MAX_SUGGESTIONS_TEMPLATE_LENGTH })
- - link = link_to('', help_page_path('user/project/merge_requests/reviews/suggestions.md', anchor: 'configure-the-commit-message-for-applied-suggestions'), target: '_blank', rel: 'noopener noreferrer')
+ - link = link_to('', help_page_path('user/project/merge_requests/reviews/suggestions', anchor: 'configure-the-commit-message-for-applied-suggestions'), target: '_blank', rel: 'noopener noreferrer')
= safe_format(s_('ProjectSettings|%{link_start}What variables can I use?%{link_end}'), tag_pair(link, :link_start, :link_end))
diff --git a/app/views/projects/settings/merge_requests/_merge_request_pipelines_and_threads_options.html.haml b/app/views/projects/settings/merge_requests/_merge_request_pipelines_and_threads_options.html.haml
index a9609434f15..65eb5b60cc3 100644
--- a/app/views/projects/settings/merge_requests/_merge_request_pipelines_and_threads_options.html.haml
+++ b/app/views/projects/settings/merge_requests/_merge_request_pipelines_and_threads_options.html.haml
@@ -9,5 +9,4 @@
help_text: s_('MergeChecks|Introduces the risk of merging changes that do not pass the pipeline.'),
checkbox_options: { class: 'gl-pl-6' }
= form.gitlab_ui_checkbox_component :only_allow_merge_if_all_discussions_are_resolved,
- s_('MergeChecks|All threads must be resolved'),
- checkbox_options: { data: { qa_selector: 'only_allow_merge_if_all_discussions_are_resolved_checkbox' } }
+ s_('MergeChecks|All threads must be resolved')
diff --git a/app/views/projects/settings/merge_requests/_merge_request_squash_commit_template.html.haml b/app/views/projects/settings/merge_requests/_merge_request_squash_commit_template.html.haml
index bc6530b927c..26b038f1bf7 100644
--- a/app/views/projects/settings/merge_requests/_merge_request_squash_commit_template.html.haml
+++ b/app/views/projects/settings/merge_requests/_merge_request_squash_commit_template.html.haml
@@ -9,5 +9,5 @@
%p.form-text.text-muted
= s_('ProjectSettings|Leave empty to use default template.')
= sprintf(s_('ProjectSettings|Maximum %{maxLength} characters.'), { maxLength: Project::MAX_COMMIT_TEMPLATE_LENGTH })
- - link = link_to('', help_page_path('user/project/merge_requests/commit_templates.md'), target: '_blank', rel: 'noopener noreferrer')
+ - link = link_to('', help_page_path('user/project/merge_requests/commit_templates'), target: '_blank', rel: 'noopener noreferrer')
= safe_format(s_('ProjectSettings|%{link_start}What variables can I use?%{link_end}'), tag_pair(link, :link_start, :link_end))
diff --git a/app/views/projects/settings/merge_requests/_merge_request_squash_options_settings.html.haml b/app/views/projects/settings/merge_requests/_merge_request_squash_options_settings.html.haml
index 372c0723600..120b183bf51 100644
--- a/app/views/projects/settings/merge_requests/_merge_request_squash_options_settings.html.haml
+++ b/app/views/projects/settings/merge_requests/_merge_request_squash_options_settings.html.haml
@@ -5,7 +5,7 @@
%b= s_('ProjectSettings|Squash commits when merging')
%p.text-secondary
= s_('ProjectSettings|Set the default behavior of this option in merge requests. Changes to this are also applied to existing merge requests.')
- = link_to s_('ProjectSettings|What is squashing?'), help_page_path('user/project/merge_requests/squash_and_merge.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to s_('ProjectSettings|What is squashing?'), help_page_path('user/project/merge_requests/squash_and_merge'), target: '_blank', rel: 'noopener noreferrer'
= settings.gitlab_ui_radio_component :squash_option,
:never,
diff --git a/app/views/projects/settings/merge_requests/show.html.haml b/app/views/projects/settings/merge_requests/show.html.haml
index e877be704a2..f48a4e5e42c 100644
--- a/app/views/projects/settings/merge_requests/show.html.haml
+++ b/app/views/projects/settings/merge_requests/show.html.haml
@@ -13,7 +13,7 @@
= gitlab_ui_form_for @project, url: project_settings_merge_requests_path(@project), html: { multipart: true, class: "merge-request-settings-form js-mr-settings-form" }, authenticity_token: true do |f|
%input{ name: 'update_section', type: 'hidden', value: 'js-merge-request-settings' }
= render 'projects/settings/merge_requests/merge_request_settings', form: f
- = f.submit _('Save changes'), class: "rspec-save-merge-request-changes", data: { qa_selector: 'save_merge_request_changes_button' }, pajamas_button: true
+ = f.submit _('Save changes'), class: "rspec-save-merge-request-changes", data: { testid: 'save-merge-request-changes-button' }, pajamas_button: true
= render_if_exists 'projects/settings/merge_requests/merge_request_approvals_settings', expanded: true
= render_if_exists 'projects/settings/merge_requests/suggested_reviewers_settings', expanded: true
diff --git a/app/views/projects/settings/operations/_alert_management.html.haml b/app/views/projects/settings/operations/_alert_management.html.haml
index c29cedd8250..849597f6e65 100644
--- a/app/views/projects/settings/operations/_alert_management.html.haml
+++ b/app/views/projects/settings/operations/_alert_management.html.haml
@@ -11,6 +11,6 @@
= _('Expand')
%p.gl-text-secondary
= _('Display alerts from all configured monitoring tools.')
- = link_to _('Learn more.'), help_page_path('operations/incident_management/integrations.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('operations/incident_management/integrations'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
.js-alerts-settings{ data: alerts_settings_data }
diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml
index cc49ff9e293..f04d6ab341f 100644
--- a/app/views/projects/tags/_tag.html.haml
+++ b/app/views/projects/tags/_tag.html.haml
@@ -30,7 +30,7 @@
= render partial: 'projects/commit/signature', object: tag.signature
- if commit_status
- = render 'ci/status/icon', size: 24, status: commit_status, option_css_classes: 'gl-display-inline-flex gl-vertical-align-middle gl-mr-5'
+ = render 'ci/status/icon', status: commit_status, option_css_classes: 'gl-display-inline-flex gl-vertical-align-middle gl-mr-5'
- elsif @tag_pipeline_statuses && @tag_pipeline_statuses.any?
.gl-display-inline-flex.gl-vertical-align-middle.gl-mr-5
%svg.s24
diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml
index 37f27aa7caf..bed37d9cb63 100644
--- a/app/views/projects/tree/_tree_header.html.haml
+++ b/app/views/projects/tree/_tree_header.html.haml
@@ -10,9 +10,10 @@
.tree-controls
.d-block.d-sm-flex.flex-wrap.align-items-start.gl-children-ml-sm-3.gl-first-child-ml-sm-0<
= render_if_exists 'projects/tree/lock_link'
+ = render 'projects/buttons/compare', project: @project, ref: @ref, root_ref: @repository&.root_ref
+
#js-tree-history-link{ data: { history_link: project_commits_path(@project, @ref) } }
- = render 'projects/buttons/compare', project: @project, ref: @ref, root_ref: @repository&.root_ref
= render 'projects/find_file_link'
= render 'shared/web_ide_button', blob: nil
= render 'projects/buttons/download', project: @project, ref: @ref
diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml
index 3c3f9eb7390..97b254a7b85 100644
--- a/app/views/projects/tree/show.html.haml
+++ b/app/views/projects/tree/show.html.haml
@@ -13,3 +13,6 @@
= render 'projects/last_push'
= render 'projects/files', commit: @last_commit, project: @project, ref: @ref, content_url: project_tree_path(@project, @id)
= render 'shared/web_ide_path'
+
+-# https://gitlab.com/gitlab-org/gitlab/-/issues/408388#note_1578533983
+#js-ambiguous-ref-modal{ data: { ambiguous: @is_ambiguous_ref.to_s, ref: current_ref } }
diff --git a/app/views/projects/usage_quotas/index.html.haml b/app/views/projects/usage_quotas/index.html.haml
index 6f2a2aacf66..039df9738ff 100644
--- a/app/views/projects/usage_quotas/index.html.haml
+++ b/app/views/projects/usage_quotas/index.html.haml
@@ -14,7 +14,7 @@
.col-sm-12
%p.gl-text-secondary
= s_('UsageQuota|Usage of project resources across the %{strong_start}%{project_name}%{strong_end} project').html_safe % { strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe, project_name: @project.name } + '.'
- %a{ href: help_page_path('user/usage_quotas.md'), target: '_blank', rel: 'noopener noreferrer' }
+ %a{ href: help_page_path('user/usage_quotas'), target: '_blank', rel: 'noopener noreferrer' }
= s_('UsageQuota|Learn more about usage quotas') + '.'
= gl_tabs_nav({ id: 'js-project-usage-quotas-tabs' }) do
diff --git a/app/views/protected_branches/shared/_create_protected_branch.html.haml b/app/views/protected_branches/shared/_create_protected_branch.html.haml
index 96e6990b080..bb1d56dcc61 100644
--- a/app/views/protected_branches/shared/_create_protected_branch.html.haml
+++ b/app/views/protected_branches/shared/_create_protected_branch.html.haml
@@ -14,12 +14,17 @@
= render partial: "protected_branches/shared/dropdown", locals: { f: f, toggle_classes: 'gl-w-full! gl-form-input-lg' }
.form-text.text-muted
- wildcards_url = help_page_url('user/project/protected_branches', anchor: 'protect-multiple-branches-with-wildcard-rules')
- - wildcards_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: wildcards_url }
- - placeholders = { wildcards_link_start: wildcards_link_start, wildcards_link_end: '</a>', code_tag_start: '<code>', code_tag_end: '</code>' }
+ - wildcards_link_tag_pair = tag_pair(link_to('', wildcards_url, target: '_blank', rel: 'noopener noreferrer'), :wildcards_link_start, :wildcards_link_end)
+
+ - case_sensitive_url = help_page_url('user/project/protected_branches', anchor: 'branch-names-are-case-sensitive')
+ - case_sensitive_link_tag_pair = tag_pair(link_to('', case_sensitive_url, target: '_blank', rel: 'noopener noreferrer'), :case_sensitive_link_start, :case_sensitive_link_end)
+
+ - code_tag_pair = tag_pair(tag.code, :code_tag_start, :code_tag_end)
+
- if protected_branch_entity.is_a?(Group)
- = (s_("ProtectedBranch|Only %{wildcards_link_start}Wildcards%{wildcards_link_end} such as %{code_tag_start}*-stable%{code_tag_end} or %{code_tag_start}production/*%{code_tag_end} are supported.") % placeholders).html_safe
+ = safe_format(s_('ProtectedBranch|Only %{wildcards_link_start}Wildcards%{wildcards_link_end} such as %{code_tag_start}*-stable%{code_tag_end} or %{code_tag_start}production/*%{code_tag_end} are supported. %{case_sensitive_link_start}Branch names are case-sensitive.%{case_sensitive_link_end}'), wildcards_link_tag_pair, case_sensitive_link_tag_pair, code_tag_pair)
- else
- = (s_("ProtectedBranch|%{wildcards_link_start}Wildcards%{wildcards_link_end} such as %{code_tag_start}*-stable%{code_tag_end} or %{code_tag_start}production/*%{code_tag_end} are supported.") % placeholders).html_safe
+ = safe_format(s_('ProtectedBranch|%{wildcards_link_start}Wildcards%{wildcards_link_end} such as %{code_tag_start}*-stable%{code_tag_end} or %{code_tag_start}production/*%{code_tag_end} are supported. %{case_sensitive_link_start}Branch names are case-sensitive.%{case_sensitive_link_end}'), wildcards_link_tag_pair, case_sensitive_link_tag_pair, code_tag_pair)
.form-group.row
= f.label :merge_access_levels_attributes, s_("ProtectedBranch|Allowed to merge:"), class: 'col-sm-12'
.col-sm-12
@@ -38,6 +43,6 @@
- force_push_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: force_push_docs_url }
= (s_("ProtectedBranch|Allow all users with push access to %{tag_start}force push%{tag_end}.") % { tag_start: force_push_link_start, tag_end: '</a>' }).html_safe
= render_if_exists 'protected_branches/ee/code_owner_approval_form', f: f, protected_branch_entity: protected_branch_entity
- = f.submit s_('ProtectedBranch|Protect'), disabled: true, data: { qa_selector: 'protect_button' }, pajamas_button: true
+ = f.submit s_('ProtectedBranch|Protect'), disabled: true, data: { testid: 'protect-button' }, pajamas_button: true
= render Pajamas::ButtonComponent.new(button_options: { type: 'reset', class: 'gl-ml-2 js-toggle-button' }) do
= _('Cancel')
diff --git a/app/views/protected_branches/shared/_index.html.haml b/app/views/protected_branches/shared/_index.html.haml
index 8e72563182c..ce5b58ee189 100644
--- a/app/views/protected_branches/shared/_index.html.haml
+++ b/app/views/protected_branches/shared/_index.html.haml
@@ -1,7 +1,7 @@
- can_admin_entity = protected_branch_can_admin_entity?(protected_branch_entity)
- expanded = expanded_by_default?
-%section.settings.no-animate#js-protected-branches-settings{ class: ('expanded' if expanded), data: { qa_selector: 'protected_branches_settings_content' } }
+%section.settings.no-animate#js-protected-branches-settings{ class: ('expanded' if expanded), data: { testid: 'protected-branches-settings-content' } }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= s_("ProtectedBranch|Protected branches")
diff --git a/app/views/protected_branches/shared/_protected_branch.html.haml b/app/views/protected_branches/shared/_protected_branch.html.haml
index 93c84e67d81..67c6e991a59 100644
--- a/app/views/protected_branches/shared/_protected_branch.html.haml
+++ b/app/views/protected_branches/shared/_protected_branch.html.haml
@@ -27,4 +27,14 @@
%span.has-tooltip{ data: { container: 'body' }, title: s_('ProtectedBranch|Inherited - This setting can be changed at the group level'), 'aria-hidden': 'true' }
= sprite_icon 'lock'
- else
- = link_button_to s_('ProtectedBranch|Unprotect'), [protected_branch_entity, protected_branch, { update_section: 'js-protected-branches-settings' }], disabled: local_assigns[:disabled], aria: { label: s_('ProtectedBranch|Unprotect branch') }, data: { confirm: s_('ProtectedBranch|Branch will be writable for developers. Are you sure?'), confirm_btn_variant: 'danger' }, method: :delete, variant: :danger, category: :secondary, size: :small
+ .gl-relative
+ - if local_assigns[:protected_from_deletion]
+ %span.gl-absolute.gl-display-inline-block.gl-w-full.gl-h-full{ data: { container: 'body', toggle: 'popover', placement: local_assigns[:placemet], html: 'true', triggers: 'hover', content: local_assigns[:popover_content] } }
+ = render Pajamas::ButtonComponent.new(size: :small,
+ variant: :danger,
+ href: [protected_branch_entity, protected_branch, { update_section: 'js-protected-branches-settings' }],
+ method: :delete,
+ disabled: local_assigns[:protected_from_deletion],
+ button_options: { update_section: 'js-protected-branches-settings', aria: { label: s_('ProtectedBranch|Unprotect branch') }, data: { confirm: s_('ProtectedBranch|Branch will be writable for developers. Are you sure?'), confirm_btn_variant: 'danger' } },
+ category: :secondary) do
+ = s_('ProtectedBranch|Unprotect')
diff --git a/app/views/pwa/manifest.json.erb b/app/views/pwa/manifest.json.erb
index e780b13de6e..82730105a53 100644
--- a/app/views/pwa/manifest.json.erb
+++ b/app/views/pwa/manifest.json.erb
@@ -3,7 +3,7 @@
"name": "<%= appearance_pwa_name %>",
"short_name": "<%= appearance_pwa_short_name %>",
"description": "<%= appearance_pwa_description %>",
- "start_url": "<%= explore_projects_path %>",
+ "start_url": "<%= root_path %>",
"scope": "<%= root_path %>",
"display": "browser",
"orientation": "any",
diff --git a/app/views/search/show.html.haml b/app/views/search/show.html.haml
index 9c1f4c8643f..4fda5379876 100644
--- a/app/views/search/show.html.haml
+++ b/app/views/search/show.html.haml
@@ -17,9 +17,8 @@
- page_description(_("%{count} %{scope} for term '%{term}'") % { count: @search_results.formatted_count(@scope), scope: @scope, term: @search_term })
- page_card_attributes("Namespace" => @group&.full_path, "Project" => @project&.full_path)
-.page-title-holder.gl-display-flex.gl-flex-wrap.gl-justify-content-space-between
- %h1.page-title.gl-font-size-h-display.gl-mr-5= _('Search')
- = render_if_exists 'search/form_elasticsearch', attrs: { class: 'mb-2 mb-sm-0 align-self-center' }
+.gl-display-flex.gl-flex-wrap.gl-justify-content-end.gl-pt-6.gl-pb-5
+ = render_if_exists 'search/form_elasticsearch'
#js-search-topbar{ data: { "group-initial-json": group_attributes.to_json, "project-initial-json": project_attributes.to_json, "default-branch-name": @project&.default_branch } }
.results.gl-lg-display-flex.gl-mt-0
diff --git a/app/views/shared/_auto_devops_callout.html.haml b/app/views/shared/_auto_devops_callout.html.haml
index 6d8d4f4cab9..3f613a1b383 100644
--- a/app/views/shared/_auto_devops_callout.html.haml
+++ b/app/views/shared/_auto_devops_callout.html.haml
@@ -12,5 +12,5 @@
%p= s_('AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration.')
%p
- - link = link_to(s_('AutoDevOps|Auto DevOps documentation'), help_page_path('topics/autodevops/index.md'), target: '_blank', rel: 'noopener noreferrer')
+ - link = link_to(s_('AutoDevOps|Auto DevOps documentation'), help_page_path('topics/autodevops/index'), target: '_blank', rel: 'noopener noreferrer')
= s_('AutoDevOps|Learn more in the %{link_to_documentation}').html_safe % { link_to_documentation: link }
diff --git a/app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml b/app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml
index 79a9bafc4f0..0ff2ee935cc 100644
--- a/app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml
+++ b/app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml
@@ -9,4 +9,4 @@
= _('Container registry is not enabled on this GitLab instance. Ask an administrator to enable it in order for Auto DevOps to work.')
- c.with_actions do
= link_button_to _('Settings'), project_settings_ci_cd_path(project), class: 'alert-link', variant: :confirm
- = link_button_to _('More information'), help_page_path('topics/autodevops/index.md'), target: '_blank', class: 'alert-link gl-ml-3'
+ = link_button_to _('More information'), help_page_path('topics/autodevops/index'), target: '_blank', class: 'alert-link gl-ml-3'
diff --git a/app/views/shared/_ci_catalog_badge.html.haml b/app/views/shared/_ci_catalog_badge.html.haml
new file mode 100644
index 00000000000..7f8f4f6143b
--- /dev/null
+++ b/app/views/shared/_ci_catalog_badge.html.haml
@@ -0,0 +1 @@
+= render Pajamas::BadgeComponent.new(s_('CiCatalog|CI/CD catalog resource'), variant: 'info', icon: 'catalog-checkmark', class: css_class, href: href)
diff --git a/app/views/shared/_commit_message_container.html.haml b/app/views/shared/_commit_message_container.html.haml
index 2b55d35cf1f..f420f176a11 100644
--- a/app/views/shared/_commit_message_container.html.haml
+++ b/app/views/shared/_commit_message_container.html.haml
@@ -10,7 +10,7 @@
class: 'form-control gl-form-input js-commit-message',
placeholder: local_assigns[:placeholder],
data: descriptions,
- 'data-qa-selector': 'commit_message_field',
+ 'data-testid': 'commit-message-field',
required: true, rows: (local_assigns[:rows] || 3),
id: "commit_message-#{nonce}"
- if local_assigns[:hint]
diff --git a/app/views/shared/_custom_attributes.html.haml b/app/views/shared/_custom_attributes.html.haml
index be96e77dbd4..33f3ca93b9c 100644
--- a/app/views/shared/_custom_attributes.html.haml
+++ b/app/views/shared/_custom_attributes.html.haml
@@ -2,7 +2,7 @@
= render Pajamas::CardComponent.new(body_options: { class: 'gl-py-0' }) do |c|
- c.with_header do
- = link_to(_('Custom Attributes'), help_page_path('api/custom_attributes.md'))
+ = link_to(_('Custom Attributes'), help_page_path('api/custom_attributes'))
- c.with_body do
%ul.content-list
- custom_attributes.each do |custom_attribute|
diff --git a/app/views/shared/_md_preview.html.haml b/app/views/shared/_md_preview.html.haml
index 1fd430527a1..7ac6a822420 100644
--- a/app/views/shared/_md_preview.html.haml
+++ b/app/views/shared/_md_preview.html.haml
@@ -5,7 +5,7 @@
.issuable-note-warning
= sprite_icon('lock', css_class: 'icon')
%span
- = _('This merge request is locked.')
+ = _('The discussion in this merge request is locked.')
= _('Only project members can comment.')
.md-area.position-relative
diff --git a/app/views/shared/_new_nav_announcement.html.haml b/app/views/shared/_new_nav_announcement.html.haml
deleted file mode 100644
index 8cabab09ec2..00000000000
--- a/app/views/shared/_new_nav_announcement.html.haml
+++ /dev/null
@@ -1,33 +0,0 @@
-- return unless show_new_navigation_callout?
-
-- changes_url = 'https://gitlab.com/groups/gitlab-org/-/epics/9044#whats-different'
-- vision_url = 'https://about.gitlab.com/blog/2023/05/01/gitlab-product-navigation/'
-- design_url = 'https://about.gitlab.com/blog/2023/05/15/overhauling-the-navigation-is-like-building-a-dream-home/'
-- feedback_url = 'https://gitlab.com/gitlab-org/gitlab/-/issues/409005'
-- docs_url = help_page_path('tutorials/left_sidebar/index')
-
-- changes_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: changes_url }
-- vision_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: vision_url }
-- design_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: design_url }
-- link_end = '</a>'.html_safe
-
-- welcome_text = _('For the next few releases, you can go to your avatar at any time to turn the new navigation on and off.')
-- cta_text = _('Read more about the %{changes_link_start}changes%{link_end}, the %{vision_link_start}vision%{link_end}, and the %{design_link_start}design%{link_end}.' % { changes_link_start: changes_link_start,
- vision_link_start: vision_link_start,
- design_link_start: design_link_start,
- link_end: link_end}).html_safe # rubocop:disable Gettext/StaticIdentifier
-
-= render Pajamas::AlertComponent.new(dismissible: true, title: _('Welcome to a new navigation experience'),
- alert_options: { class: 'js-new-navigation-callout', data: { feature_id: "new_navigation_callout", dismiss_endpoint: callouts_path }}) do |c|
- - c.with_body do
- %p
- = welcome_text
- = cta_text
- - c.with_actions do
- = render Pajamas::ButtonComponent.new(variant: :confirm,
- href: docs_url,
- button_options: { class: 'gl-alert-action', data: { track_action: 'click_button', track_label: 'banner_nav_learn_more' } }) do |c|
- = _('Learn more')
- = render Pajamas::ButtonComponent.new(href: feedback_url,
- button_options: { data: { track_action: 'click_button', track_label: 'banner_nav_provide_feedback' } }) do |c|
- = _('Provide feedback')
diff --git a/app/views/shared/_new_nav_for_everyone_announcement.html.haml b/app/views/shared/_new_nav_for_everyone_announcement.html.haml
new file mode 100644
index 00000000000..fa870249596
--- /dev/null
+++ b/app/views/shared/_new_nav_for_everyone_announcement.html.haml
@@ -0,0 +1,18 @@
+- return unless show_new_nav_for_everyone_callout?
+
+- blog_url = 'https://about.gitlab.com/blog/2023/08/15/navigation-research-blog-post/'
+- issues_url = 'https://about.gitlab.com/submit-feedback/#product-feedback'
+
+- blog_link_tags = tag_pair(link_to('', blog_url, rel: 'noopener noreferrer', target: '_blank'), :blog_link_start, :link_end)
+- issues_link_tags = tag_pair(link_to('', issues_url, rel: 'noopener noreferrer', target: '_blank'), :issues_link_start, :link_end)
+
+- welcome_text = safe_format(_('GitLab has redesigned the left sidebar to address customer feedback. View details in %{blog_link_start}this blog post%{link_end}. Here\'s how to %{issues_link_start}file an issue%{link_end} with the GitLab product team.'), blog_link_tags, issues_link_tags)
+
+= render Pajamas::AlertComponent.new(dismissible: true,
+ alert_options: { class: 'js-new-nav-for-everyone-callout', data: { feature_id: "new_nav_for_everyone_callout", dismiss_endpoint: callouts_path }}) do |c|
+ - c.with_body do
+ %p
+ = welcome_text
+ - c.with_actions do
+ = render Pajamas::ButtonComponent.new(variant: :confirm, href: blog_url, target: '_blank', button_options: { class: 'gl-alert-action' }) do |c|
+ = _('Learn more')
diff --git a/app/views/shared/_project_limit.html.haml b/app/views/shared/_project_limit.html.haml
index a99db32c40e..914c20fb7b0 100644
--- a/app/views/shared/_project_limit.html.haml
+++ b/app/views/shared/_project_limit.html.haml
@@ -3,7 +3,7 @@
dismissible: false,
alert_options: { class: 'project-limit-message' }) do |c|
- c.with_body do
- = _("You won't be able to create new projects because you have reached your project limit.")
+ = _("You cannot create new projects in your personal namespace because you have reached your personal project limit.")
- c.with_actions do
= link_button_to _('Remind later'), '#', class: 'alert-link hide-project-limit-message', variant: :confirm
= link_button_to _("Don't show again"), profile_path(user: {hide_project_limit: true}), method: :put, class: 'alert-link gl-ml-3'
diff --git a/app/views/shared/_registration_features_discovery_message.html.haml b/app/views/shared/_registration_features_discovery_message.html.haml
index 6e386866dfb..5fa554171aa 100644
--- a/app/views/shared/_registration_features_discovery_message.html.haml
+++ b/app/views/shared/_registration_features_discovery_message.html.haml
@@ -1,5 +1,5 @@
- feature_title = local_assigns.fetch(:feature_title, s_('RegistrationFeatures|use this feature'))
-- registration_features_docs_path = help_page_path('administration/settings/usage_statistics.md', anchor: 'registration-features-program')
+- registration_features_docs_path = help_page_path('administration/settings/usage_statistics', anchor: 'registration-features-program')
- registration_features_link_start = '<a href="%{url}" target="_blank">'.html_safe % { url: registration_features_docs_path }
%div
diff --git a/app/views/shared/_remote_mirror_update_button.html.haml b/app/views/shared/_remote_mirror_update_button.html.haml
index fa5c862b768..ec897e59d4a 100644
--- a/app/views/shared/_remote_mirror_update_button.html.haml
+++ b/app/views/shared/_remote_mirror_update_button.html.haml
@@ -3,4 +3,4 @@
button_options: { class: 'disabled', title: _('Updating'), data: { toggle: 'tooltip', container: 'body' } },
icon_classes: 'spin')
- elsif remote_mirror.enabled?
- = link_button_to nil, update_now_project_mirror_path(@project, sync_remote: true), method: :post, class: 'rspec-update-now-button', data: { toggle: 'tooltip', container: 'body', qa_selector: 'update_now_button' }, title: _('Update now'), icon: 'retry'
+ = link_button_to nil, update_now_project_mirror_path(@project, sync_remote: true), method: :post, class: 'rspec-update-now-button', data: { toggle: 'tooltip', container: 'body', testid: 'update-now-button' }, title: _('Update now'), icon: 'retry'
diff --git a/app/views/shared/_service_ping_consent.html.haml b/app/views/shared/_service_ping_consent.html.haml
index cfc0afb4646..b65808bfcd2 100644
--- a/app/views/shared/_service_ping_consent.html.haml
+++ b/app/views/shared/_service_ping_consent.html.haml
@@ -1,14 +1,14 @@
- if session[:ask_for_usage_stats_consent]
= render Pajamas::AlertComponent.new(alert_options: { class: 'service-ping-consent-message' }) do |c|
- c.with_body do
- - docs_link = link_to '', help_page_path('administration/settings/usage_statistics.md'), class: 'gl-link'
+ - docs_link = link_to '', help_page_path('administration/settings/usage_statistics'), class: 'gl-link'
- settings_link = link_to '', metrics_and_profiling_admin_application_settings_path(anchor: 'js-usage-settings'), class: 'gl-link'
= safe_format s_('ServicePing|To help improve GitLab, we would like to periodically %{link_start}collect usage information%{link_end}.'), tag_pair(docs_link, :link_start, :link_end)
= safe_format s_('ServicePing|This can be changed at any time in %{link_start}your settings%{link_end}.'), tag_pair(settings_link, :link_start, :link_end)
- c.with_actions do
- send_service_data_path = admin_application_settings_path(application_setting: { version_check_enabled: 1, usage_ping_enabled: 1 })
- not_now_path = admin_application_settings_path(application_setting: { version_check_enabled: 0, usage_ping_enabled: 0 })
- = render Pajamas::ButtonComponent.new(href: send_service_data_path, method: :put, variant: :confirm, button_options: { 'data-url' => admin_application_settings_path, 'data-check-enabled': true, 'data-service-ping-enabled': true, class: 'js-service-ping-consent-action alert-link' }) do
+ = render Pajamas::ButtonComponent.new(href: send_service_data_path, method: :put, variant: :confirm, button_options: { class: 'alert-link' }) do
= _('Send service data')
- = render Pajamas::ButtonComponent.new(href: not_now_path, method: :put, button_options: { 'data-url' => admin_application_settings_path, 'data-check-enabled': false, 'data-service-ping-enabled': false, class: 'js-service-ping-consent-action alert-link gl-ml-3' }) do
+ = render Pajamas::ButtonComponent.new(href: not_now_path, method: :put, button_options: { class: 'alert-link gl-ml-3' }) do
= _("Don't send service data")
diff --git a/app/views/shared/access_tokens/_form.html.haml b/app/views/shared/access_tokens/_form.html.haml
index e46da882e83..3bf85da83b1 100644
--- a/app/views/shared/access_tokens/_form.html.haml
+++ b/app/views/shared/access_tokens/_form.html.haml
@@ -30,7 +30,7 @@
.form-group
= label_tag :access_level, s_("AccessTokens|Select a role"), class: "label-bold"
.select-wrapper.gl-form-input-md
- = select_tag :"#{prefix}[access_level]", options_for_select(access_levels, default_access_level), class: "form-control select-control", data: { qa_selector: 'access_token_access_level' }
+ = select_tag :"#{prefix}[access_level]", options_for_select(access_levels, default_access_level), class: "form-control select-control"
= sprite_icon('chevron-down', css_class: "gl-icon gl-absolute gl-top-3 gl-right-3 gl-text-gray-200")
.form-group
diff --git a/app/views/shared/deploy_tokens/_form.html.haml b/app/views/shared/deploy_tokens/_form.html.haml
index bb7e0d774cc..109bd559762 100644
--- a/app/views/shared/deploy_tokens/_form.html.haml
+++ b/app/views/shared/deploy_tokens/_form.html.haml
@@ -1,17 +1,17 @@
%p
- - link = link_to('', help_page_path('user/project/deploy_tokens/index.md'), target: '_blank', rel: 'noopener noreferrer')
+ - link = link_to('', help_page_path('user/project/deploy_tokens/index'), target: '_blank', rel: 'noopener noreferrer')
= safe_format(s_('DeployTokens|Create a new deploy token for all projects in this group. %{link_start}What are deploy tokens?%{link_end}'), tag_pair(link, :link_start, :link_end))
= gitlab_ui_form_for token, url: create_deploy_token_path(group_or_project, anchor: 'js-deploy-tokens'), method: :post, remote: true do |f|
.form-group
= f.label :name, class: 'label-bold'
- = f.text_field :name, class: 'form-control gl-form-input', data: { qa_selector: 'deploy_token_name_field' }, required: true
+ = f.text_field :name, class: 'form-control gl-form-input', data: { testid: 'deploy-token-name-field' }, required: true
.text-secondary= s_('DeployTokens|Enter a unique name for your deploy token.')
.form-group
= f.label :expires_at, _('Expiration date (optional)'), class: 'label-bold'
- = f.gitlab_ui_datepicker :expires_at, data: { qa_selector: 'deploy_token_expires_at_field' }, value: f.object.expires_at
+ = f.gitlab_ui_datepicker :expires_at, data: { testid: 'deploy-token-expires-at-field' }, value: f.object.expires_at
.text-secondary= s_('DeployTokens|Enter an expiration date for your token. Defaults to never expire.')
.form-group
@@ -22,15 +22,15 @@
.form-group
= f.label :scopes, _('Scopes (select at least one)'), class: 'label-bold'
- = f.gitlab_ui_checkbox_component :read_repository, 'read_repository', help_text: s_('DeployTokens|Allows read-only access to the repository.'), checkbox_options: { data: { qa_selector: 'deploy_token_read_repository_checkbox' } }
+ = f.gitlab_ui_checkbox_component :read_repository, 'read_repository', help_text: s_('DeployTokens|Allows read-only access to the repository.'), checkbox_options: { data: { testid: 'deploy-token-read-repository-checkbox' } }
- if container_registry_enabled?(group_or_project)
- = f.gitlab_ui_checkbox_component :read_registry, 'read_registry', help_text: s_('DeployTokens|Allows read-only access to registry images.'), checkbox_options: { data: { qa_selector: 'deploy_token_read_registry_checkbox' } }
- = f.gitlab_ui_checkbox_component :write_registry, 'write_registry', help_text: s_('DeployTokens|Allows write access to registry images.'), checkbox_options: { data: { qa_selector: 'deploy_token_write_registry_checkbox' } }
+ = f.gitlab_ui_checkbox_component :read_registry, 'read_registry', help_text: s_('DeployTokens|Allows read-only access to registry images.'), checkbox_options: { data: { testid: 'deploy-token-read-registry-checkbox' } }
+ = f.gitlab_ui_checkbox_component :write_registry, 'write_registry', help_text: s_('DeployTokens|Allows write access to registry images.'), checkbox_options: { data: { testid: 'deploy-token-write-registry-checkbox' } }
- if packages_registry_enabled?(group_or_project)
- = f.gitlab_ui_checkbox_component :read_package_registry, 'read_package_registry', help_text: s_('DeployTokens|Allows read-only access to the package registry.'), checkbox_options: { data: { qa_selector: 'deploy_token_read_package_registry_checkbox' } }
- = f.gitlab_ui_checkbox_component :write_package_registry, 'write_package_registry', help_text: s_('DeployTokens|Allows read and write access to the package registry.'), checkbox_options: { data: { qa_selector: 'deploy_token_write_package_registry_checkbox' } }
+ = f.gitlab_ui_checkbox_component :read_package_registry, 'read_package_registry', help_text: s_('DeployTokens|Allows read-only access to the package registry.'), checkbox_options: { data: { testid: 'deploy-token-read-package-registry-checkbox' } }
+ = f.gitlab_ui_checkbox_component :write_package_registry, 'write_package_registry', help_text: s_('DeployTokens|Allows read and write access to the package registry.'), checkbox_options: { data: { testid: 'deploy-token-write-package-registry-checkbox' } }
.gl-mt-3
- = f.submit s_('DeployTokens|Create deploy token'), data: { qa_selector: 'create_deploy_token_button' }, pajamas_button: true
+ = f.submit s_('DeployTokens|Create deploy token'), data: { testid: 'create-deploy-token-button' }, pajamas_button: true
diff --git a/app/views/shared/deploy_tokens/_index.html.haml b/app/views/shared/deploy_tokens/_index.html.haml
index ccffc3ec923..74de71867b8 100644
--- a/app/views/shared/deploy_tokens/_index.html.haml
+++ b/app/views/shared/deploy_tokens/_index.html.haml
@@ -1,6 +1,6 @@
- expanded = expand_deploy_tokens_section?(@new_deploy_token, @created_deploy_token)
-%section.settings.no-animate#js-deploy-tokens{ class: ('expanded' if expanded), data: { qa_selector: 'deploy_tokens_settings_content' } }
+%section.settings.no-animate#js-deploy-tokens{ class: ('expanded' if expanded), data: { testid: 'deploy-tokens-settings-content' } }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= s_('DeployTokens|Deploy tokens')
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
diff --git a/app/views/shared/deploy_tokens/_new_deploy_token.html.haml b/app/views/shared/deploy_tokens/_new_deploy_token.html.haml
index 25c277ea0ea..2bc2e6c5b81 100644
--- a/app/views/shared/deploy_tokens/_new_deploy_token.html.haml
+++ b/app/views/shared/deploy_tokens/_new_deploy_token.html.haml
@@ -1,21 +1,21 @@
-.created-deploy-token-container.info-well{ data: { qa_selector: 'created_deploy_token_container' } }
+.created-deploy-token-container.info-well{ data: { testid: 'created-deploy-token-container' } }
.well-segment
%h5.gl-mt-0
= s_('DeployTokens|Your new Deploy Token username')
.form-group
.input-group
- = text_field_tag 'deploy-token-user', deploy_token.username, readonly: true, class: 'deploy-token-field form-control js-select-on-focus', data: { qa_selector: 'deploy_token_user_field' }
+ = text_field_tag 'deploy-token-user', deploy_token.username, readonly: true, class: 'deploy-token-field form-control js-select-on-focus', data: { testid: 'deploy-token-user-field' }
.input-group-append
= deprecated_clipboard_button(text: deploy_token.username, title: s_('DeployTokens|Copy username'), placement: 'left')
%span.deploy-token-help-block.gl-mt-2.text-success
- - link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/project/deploy_tokens/index.md') }
+ - link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/project/deploy_tokens/index') }
- link_end = "</a>".html_safe
= s_("DeployTokens|This username supports access. %{link_start}What kind of access?%{link_end}").html_safe % { link_start: link_start, link_end: link_end }
.form-group
.input-group
- = text_field_tag 'deploy-token', deploy_token.token, readonly: true, class: 'deploy-token-field form-control js-select-on-focus', data: { qa_selector: 'deploy_token_field' }
+ = text_field_tag 'deploy-token', deploy_token.token, readonly: true, class: 'deploy-token-field form-control js-select-on-focus', data: { testid: 'deploy-token-field' }
.input-group-append
= deprecated_clipboard_button(text: deploy_token.token, title: s_('DeployTokens|Copy deploy token'), placement: 'left')
%span.deploy-token-help-block.gl-mt-2.text-danger
diff --git a/app/views/shared/deploy_tokens/_table.html.haml b/app/views/shared/deploy_tokens/_table.html.haml
index 3b351387d41..0b8a97a34f2 100644
--- a/app/views/shared/deploy_tokens/_table.html.haml
+++ b/app/views/shared/deploy_tokens/_table.html.haml
@@ -16,7 +16,7 @@
packages_registry_enabled: packages_registry_enabled?(group_or_project),
create_new_token_path: create_deploy_token_path(group_or_project),
token_type: group_or_project.is_a?(Group) ? 'group' : 'project',
- deploy_tokens_help_url: help_page_path('user/project/deploy_tokens/index.md')
+ deploy_tokens_help_url: help_page_path('user/project/deploy_tokens/index')
}
}
- if active_tokens.present?
diff --git a/app/views/shared/empty_states/_snippets.html.haml b/app/views/shared/empty_states/_snippets.html.haml
index a2457fb0810..800cfe8b0d1 100644
--- a/app/views/shared/empty_states/_snippets.html.haml
+++ b/app/views/shared/empty_states/_snippets.html.haml
@@ -13,6 +13,6 @@
.gl-mt-3<
- if button_path
= link_button_to s_('SnippetsEmptyState|New snippet'), button_path, title: s_('SnippetsEmptyState|New snippet'), id: 'new_snippet_link', data: { testid: 'create-first-snippet-link' }, variant: :confirm
- = link_button_to s_('SnippetsEmptyState|Documentation'), help_page_path('user/snippets.md'), title: s_('SnippetsEmptyState|Documentation')
+ = link_button_to s_('SnippetsEmptyState|Documentation'), help_page_path('user/snippets'), title: s_('SnippetsEmptyState|Documentation')
- else
%h4.gl-text-center= s_('SnippetsEmptyState|There are no snippets to show.')
diff --git a/app/views/shared/integrations/gitlab_slack_application/_help.html.haml b/app/views/shared/integrations/gitlab_slack_application/_help.html.haml
index 0956f1183cb..2e7768e54f4 100644
--- a/app/views/shared/integrations/gitlab_slack_application/_help.html.haml
+++ b/app/views/shared/integrations/gitlab_slack_application/_help.html.haml
@@ -2,7 +2,7 @@
.well-segment
%p
= s_("SlackIntegration|This integration allows users to perform common operations on this project by entering slash commands in Slack.")
- = link_to _('Learn more'), help_page_path('user/project/integrations/gitlab_slack_application.md')
+ = link_to _('Learn more'), help_page_path('user/project/integrations/gitlab_slack_application')
%p
= s_("SlackIntegration|See the list of available commands in Slack after setting up this integration by entering")
%kbd.inline /gitlab help
diff --git a/app/views/shared/integrations/gitlab_slack_application/_slack_integration_form.html.haml b/app/views/shared/integrations/gitlab_slack_application/_slack_integration_form.html.haml
index e5d05a8a83d..57d172b41f4 100644
--- a/app/views/shared/integrations/gitlab_slack_application/_slack_integration_form.html.haml
+++ b/app/views/shared/integrations/gitlab_slack_application/_slack_integration_form.html.haml
@@ -29,7 +29,7 @@
= render Pajamas::ButtonComponent.new(href: add_to_slack_link(@project, slack_app_id)) do
= s_('SlackIntegration|Reinstall GitLab for Slack app…')
%p
- = html_escape(s_('SlackIntegration|You may need to reinstall the GitLab for Slack app when we %{linkStart}make updates or change permissions%{linkEnd}.')) % { linkStart: %(<a href="#{help_page_path('user/project/integrations/gitlab_slack_application.md', anchor: 'update-the-gitlab-for-slack-app')}">).html_safe, linkEnd: '</a>'.html_safe}
+ = html_escape(s_('SlackIntegration|You may need to reinstall the GitLab for Slack app when we %{linkStart}make updates or change permissions%{linkEnd}.')) % { linkStart: %(<a href="#{help_page_path('user/project/integrations/gitlab_slack_application', anchor: 'update-the-gitlab-for-slack-app')}">).html_safe, linkEnd: '</a>'.html_safe}
- else
= render Pajamas::ButtonComponent.new(href: add_to_slack_link(@project, slack_app_id)) do
= s_('SlackIntegration|Install GitLab for Slack app…')
diff --git a/app/views/shared/integrations/mattermost_slash_commands/_help.html.haml b/app/views/shared/integrations/mattermost_slash_commands/_help.html.haml
index 6ce1c65a8dc..e01999c2279 100644
--- a/app/views/shared/integrations/mattermost_slash_commands/_help.html.haml
+++ b/app/views/shared/integrations/mattermost_slash_commands/_help.html.haml
@@ -4,7 +4,7 @@
.well-segment
%p
= s_("MattermostService|Use this service to perform common tasks in your project by entering slash commands in Mattermost.")
- = link_to help_page_path('user/project/integrations/mattermost_slash_commands.md'), target: '_blank' do
+ = link_to help_page_path('user/project/integrations/mattermost_slash_commands'), target: '_blank' do
= _("How do I configure this integration?")
= sprite_icon('external-link')
%p.inline
diff --git a/app/views/shared/integrations/slack_slash_commands/_help.html.haml b/app/views/shared/integrations/slack_slash_commands/_help.html.haml
index fd30c5b0da3..0440bb13797 100644
--- a/app/views/shared/integrations/slack_slash_commands/_help.html.haml
+++ b/app/views/shared/integrations/slack_slash_commands/_help.html.haml
@@ -5,7 +5,7 @@
.well-segment
%p
= s_("SlackService|Perform common operations in this project by entering slash commands in Slack.")
- = link_to help_page_path('user/project/integrations/slack_slash_commands.md'), target: '_blank' do
+ = link_to help_page_path('user/project/integrations/slack_slash_commands'), target: '_blank' do
= _("Learn more.")
= sprite_icon('external-link')
%p.inline
@@ -40,7 +40,7 @@
.col-12.input-group
= text_field_tag :url, integration_trigger_url(integration), class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
- = clipboard_button(target: '#url', category: :primary, size: :medium)
+ = clipboard_button(target: '#url', category: :primary, size: :medium, title: _('Copy URL'))
.form-group
= label_tag nil, _('Method'), class: 'col-12 col-form-label label-bold'
@@ -51,7 +51,7 @@
.col-12.input-group
= text_field_tag :customize_name, 'GitLab', class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
- = clipboard_button(target: '#customize_name', category: :primary, size: :medium)
+ = clipboard_button(target: '#customize_name', category: :primary, size: :medium, title: _('Copy customize name'))
.form-group
= label_tag nil, _('Customize icon'), class: 'col-12 col-form-label label-bold'
@@ -68,21 +68,21 @@
.col-12.input-group
= text_field_tag :autocomplete_description, run_actions_text.html_safe, class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
- = clipboard_button(target: '#autocomplete_description', category: :primary, size: :medium)
+ = clipboard_button(target: '#autocomplete_description', category: :primary, size: :medium, title: _('Copy autocomplete description'))
.form-group
= label_tag :autocomplete_usage_hint, _('Autocomplete usage hint'), class: 'col-12 col-form-label label-bold'
.col-12.input-group
= text_field_tag :autocomplete_usage_hint, '[help]', class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
- = clipboard_button(target: '#autocomplete_usage_hint', category: :primary, size: :medium)
+ = clipboard_button(target: '#autocomplete_usage_hint', category: :primary, size: :medium, title: _('Copy autocomplete usage hint'))
.form-group
= label_tag :descriptive_label, _('Descriptive label'), class: 'col-12 col-form-label label-bold'
.col-12.input-group
= text_field_tag :descriptive_label, _('Perform common operations on GitLab project'), class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
- = clipboard_button(target: '#descriptive_label', category: :primary, size: :medium)
+ = clipboard_button(target: '#descriptive_label', category: :primary, size: :medium, title: _('Copy descriptive label'))
%hr
diff --git a/app/views/shared/issuable/_assignees.html.haml b/app/views/shared/issuable/_assignees.html.haml
index 5326b26d655..1ae9ce4eecd 100644
--- a/app/views/shared/issuable/_assignees.html.haml
+++ b/app/views/shared/issuable/_assignees.html.haml
@@ -7,5 +7,5 @@
= link_to_member(@project, assignee, name: false, title: s_("MrList|Assigned to %{name}") % { name: assignee.name})
- if more_assignees_count > 0
- %span{ class: 'avatar-counter has-tooltip', data: { container: 'body', placement: 'bottom', 'line-type' => 'old', qa_selector: 'avatar_counter_content' }, title: _("+%{more_assignees_count} more assignees") % { more_assignees_count: more_assignees_count} }
+ %span{ class: 'avatar-counter has-tooltip', data: { container: 'body', placement: 'bottom', 'line-type' => 'old' }, title: _("+%{more_assignees_count} more assignees") % { more_assignees_count: more_assignees_count} }
= _("+%{more_assignees_count}") % { more_assignees_count: more_assignees_count}
diff --git a/app/views/shared/issuable/_nav.html.haml b/app/views/shared/issuable/_nav.html.haml
index 4a33f625347..c48f51dc9bc 100644
--- a/app/views/shared/issuable/_nav.html.haml
+++ b/app/views/shared/issuable/_nav.html.haml
@@ -11,7 +11,7 @@
= gl_tab_link_to page_filter_path(state: 'closed'), item_active: params[:state] == 'closed', id: 'state-closed', title: _('Filter by merge requests that are currently closed and unmerged.'), data: { state: 'closed' } do
#{issuables_state_counter_text(type, :closed, display_count)}
- else
- = gl_tab_link_to page_filter_path(state: 'closed'), item_active: params[:state] == 'closed', id: 'state-closed', title: _('Filter by issues that are currently closed.'), data: { state: 'closed', qa_selector: 'closed_issues_link' } do
+ = gl_tab_link_to page_filter_path(state: 'closed'), item_active: params[:state] == 'closed', id: 'state-closed', title: _('Filter by issues that are currently closed.'), data: { state: 'closed' } do
#{issuables_state_counter_text(type, :closed, display_count)}
= render 'shared/issuable/nav_links/all', page_context_word: page_context_word, counter: issuables_state_counter_text(type, :all, display_count)
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index 86aaa5128a8..52c8a4d4123 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -185,6 +185,11 @@
%li.filter-dropdown-item
%button.btn.btn-link.js-data-value.monospace
{{title}}
+ #js-dropdown-source-branch.filtered-search-input-dropdown-menu.dropdown-menu
+ %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
+ %li.filter-dropdown-item
+ %button.btn.btn-link.js-data-value.monospace
+ {{title}}
#js-dropdown-environment.filtered-search-input-dropdown-menu.dropdown-menu
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 1392c7ab89f..f018e4f122e 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -16,7 +16,7 @@
%aside.right-sidebar.js-right-sidebar.js-issuable-sidebar{ data: { always_show_toggle: true, signed: { in: signed_in }, issuable_type: issuable_type }, class: "#{sidebar_gutter_collapsed_class(is_merge_request_with_flag)} #{'right-sidebar-merge-requests' if is_merge_request_with_flag}", 'aria-live' => 'polite', 'aria-label': issuable_type }
.issuable-sidebar{ class: "#{'is-merge-request' if is_merge_request_with_flag}" }
.issuable-sidebar-header{ class: "gl-pb-4! #{'gl-pb-2! gl-md-display-flex gl-justify-content-end gl-lg-display-none!' if is_merge_request_with_flag}" }
- = render Pajamas::ButtonComponent.new(button_options: { class: "gutter-toggle float-right js-sidebar-toggle has-tooltip gl-shadow-none! #{'gl-display-block' if moved_sidebar_enabled}", type: 'button', 'aria-label' => _('Toggle sidebar'), title: sidebar_gutter_tooltip_text, data: { container: 'body', placement: 'left', boundary: 'viewport' } }) do
+ = render Pajamas::ButtonComponent.new(button_options: { class: "gutter-toggle float-right js-sidebar-toggle has-tooltip gl-shadow-none! #{'gl-display-block' if moved_sidebar_enabled} #{'gl-mt-2' if notifications_todos_buttons_enabled?}" , type: 'button', 'aria-label' => _('Toggle sidebar'), title: sidebar_gutter_tooltip_text, data: { container: 'body', placement: 'left', boundary: 'viewport' } }) do
= sidebar_gutter_toggle_icon
- if signed_in
- if !is_merge_request_with_flag
diff --git a/app/views/shared/issuable/form/_type_selector.html.haml b/app/views/shared/issuable/form/_type_selector.html.haml
index 0bcdcb9e963..89a07444d9f 100644
--- a/app/views/shared/issuable/form/_type_selector.html.haml
+++ b/app/views/shared/issuable/form/_type_selector.html.haml
@@ -10,6 +10,6 @@
- if issuable.incident_type_issue?
%p.form-text.text-muted
- - incident_docs_url = help_page_path('operations/incident_management/incidents.md')
+ - incident_docs_url = help_page_path('operations/incident_management/incidents')
- incident_docs_start = format('<a href="%{url}" target="_blank" rel="noopener noreferrer">', url: incident_docs_url)
= format(_('A %{incident_docs_start}modified issue%{incident_docs_end} to guide the resolution of incidents.'), incident_docs_start: incident_docs_start, incident_docs_end: '</a>').html_safe
diff --git a/app/views/shared/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml
index 7d1e9c06966..2e2c0300ae1 100644
--- a/app/views/shared/milestones/_sidebar.html.haml
+++ b/app/views/shared/milestones/_sidebar.html.haml
@@ -161,11 +161,10 @@
- milestone_ref = milestone.try(:to_reference, full: true)
- if milestone_ref.present?
.block.reference
- .sidebar-collapsed-icon.js-dont-change-state
- = deprecated_clipboard_button(text: milestone_ref, title: s_('MilestoneSidebar|Copy reference'), placement: "left", boundary: 'viewport')
+ = clipboard_button(text: milestone_ref, title: s_('MilestoneSidebar|Copy reference'), placement: "left", boundary: 'viewport', class: 'sidebar-collapsed-icon js-dont-change-state')
.gl-display-flex.gl-align-items-center.gl-justify-content-space-between.gl-mb-2.hide-collapsed
%span.gl-overflow-hidden.gl-text-overflow-ellipsis.gl-white-space-nowrap
= s_('MilestoneSidebar|Reference:')
%span{ title: milestone_ref }
= milestone_ref
- = deprecated_clipboard_button(text: milestone_ref, title: s_('MilestoneSidebar|Copy reference'), placement: "left", boundary: 'viewport')
+ = clipboard_button(text: milestone_ref, title: s_('MilestoneSidebar|Copy reference'), placement: "left", boundary: 'viewport')
diff --git a/app/views/shared/notes/_notes_with_form.html.haml b/app/views/shared/notes/_notes_with_form.html.haml
index 336fdedf89b..343a8597444 100644
--- a/app/views/shared/notes/_notes_with_form.html.haml
+++ b/app/views/shared/notes/_notes_with_form.html.haml
@@ -15,12 +15,12 @@
.timeline-content.timeline-content-form
= render "shared/notes/form", view: diff_view, supports_autocomplete: autocomplete
- elsif !current_user
- .disabled-comment.text-center.gl-mt-3
+ .disabled-comment.gl-text-center.gl-text-secondary.gl-mt-3
- link_to_register = link_to(_("register"), new_user_registration_path(redirect_to_referer: 'yes'), class: 'js-register-link')
- link_to_sign_in = link_to(_("sign in"), new_session_path(:user, redirect_to_referer: 'yes'), class: 'js-sign-in-link')
= _("Please %{link_to_register} or %{link_to_sign_in} to comment").html_safe % { link_to_register: link_to_register, link_to_sign_in: link_to_sign_in }
- elsif discussion_locked
- .disabled-comment.text-center.gl-mt-3
+ .disabled-comment.gl-text-center.gl-mt-3
%span.issuable-note-warning
= sprite_icon('lock', css_class: 'icon')
%span
diff --git a/app/views/shared/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml
index 14785870dc0..74c325383a1 100644
--- a/app/views/shared/projects/_list.html.haml
+++ b/app/views/shared/projects/_list.html.haml
@@ -32,6 +32,7 @@
- if any_projects?(projects)
- load_pipeline_status(projects) if pipeline_status
- load_max_project_member_accesses(projects) # Prime cache used in shared/projects/project view rendered below
+ - load_catalog_resources(projects)
%ul.projects-list.gl-text-secondary.gl-w-full.gl-my-2{ class: css_classes }
- projects.each_with_index do |project, i|
- css_class = (i >= projects_limit) || project.pending_delete? ? 'hide' : nil
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index 2de4a9d7780..e65dcd68f66 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -35,7 +35,10 @@
%span.project-name<
= project.name
- = visibility_level_content(project, css_class: 'gl-mr-3')
+ = visibility_level_content(project, css_class: 'gl-mr-2')
+
+ - if project.catalog_resource
+ = render partial: 'shared/ci_catalog_badge', locals: { href: project_ci_catalog_resource_path(project, project.catalog_resource), css_class: 'gl-mr-2' }
- if explore_projects_tab? && project_license_name(project)
%span.gl-display-inline-flex.gl-align-items-center.gl-mr-3
diff --git a/app/views/shared/runners/_shared_runners_description.html.haml b/app/views/shared/runners/_shared_runners_description.html.haml
index c8ddb5d5176..89da1a6fa09 100644
--- a/app/views/shared/runners/_shared_runners_description.html.haml
+++ b/app/views/shared/runners/_shared_runners_description.html.haml
@@ -1,4 +1,4 @@
-- shared_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('ci/runners/runners_scope.md', anchor: 'shared-runners') }
+- shared_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('ci/runners/runners_scope', anchor: 'shared-runners') }
%h4
= _('Shared runners')
diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml
index 7c713e63cd7..a3dfc6eb042 100644
--- a/app/views/shared/web_hooks/_form.html.haml
+++ b/app/views/shared/web_hooks/_form.html.haml
@@ -66,7 +66,7 @@
help_text: s_('Webhooks|A release is created, updated, or deleted.')
- if Feature.enabled?(:emoji_webhooks, hook.parent)
%li.gl-pb-5
- - emoji_help_link = link_to s_('Which emoji events trigger webhooks'), help_page_path('user/project/integrations/webhook_events.md', anchor: 'emoji-events')
+ - emoji_help_link = link_to s_('Which emoji events trigger webhooks'), help_page_path('user/project/integrations/webhook_events', anchor: 'emoji-events')
= form.gitlab_ui_checkbox_component :emoji_events,
integration_webhook_event_human_name(:emoji_events),
help_text: s_('Webhooks|An emoji is awarded or revoked. %{help_link}?').html_safe % { help_link: emoji_help_link }
diff --git a/app/views/shared/wikis/_wiki_directory.html.haml b/app/views/shared/wikis/_wiki_directory.html.haml
index cce81257691..8b0b6dbd8f7 100644
--- a/app/views/shared/wikis/_wiki_directory.html.haml
+++ b/app/views/shared/wikis/_wiki_directory.html.haml
@@ -1,12 +1,12 @@
- wiki_path = wiki_page_path(@wiki, wiki_directory)
-%li{ class: active_when(params[:id] == wiki_directory.slug), data: { testid: 'wiki-directory-content' } }
+%li{ class: ['wiki-directory', active_when(params[:id] == wiki_directory.slug)], data: { testid: 'wiki-directory-content' } }
.gl-relative.gl-display-flex.gl-align-items-center.js-wiki-list-toggle.wiki-list{ data: { testid: 'wiki-list' } }<
= sprite_icon('chevron-right', css_class: 'js-wiki-list-expand-button wiki-list-expand-button gl-mr-2 gl-cursor-pointer')
= sprite_icon('chevron-down', css_class: 'js-wiki-list-collapse-button wiki-list-collapse-button gl-mr-2 gl-cursor-pointer')
= render Pajamas::ButtonComponent.new(icon: 'plus', href: "#{wiki_path}/{new_page_title}", button_options: { class: 'wiki-list-create-child-button gl-bg-transparent! gl-hover-bg-gray-50! gl-focus-bg-gray-50! gl-absolute gl-top-half gl-translate-y-n50 gl-cursor-pointer gl-right-3' })
= link_to wiki_path, data: { testid: 'wiki-dir-page-link', qa_page_name: wiki_directory.title } do
= wiki_directory.title
- %ul
+ %ul.gl-pl-8
- wiki_directory.entries.each do |entry|
= render partial: entry.to_partial_path, object: entry, locals: { context: context }
diff --git a/app/views/shared/wikis/show.html.haml b/app/views/shared/wikis/show.html.haml
index 9537d6fec15..2cd03c20080 100644
--- a/app/views/shared/wikis/show.html.haml
+++ b/app/views/shared/wikis/show.html.haml
@@ -12,8 +12,7 @@
.nav-controls.pb-md-3.pb-lg-0
= render 'shared/wikis/main_links'
- - if Feature.enabled?(:print_wiki, current_user)
- #js-export-actions{ data: { options: { target: '.js-wiki-page-content', title: @page.human_title, stylesheet: [stylesheet_path('application')] }.to_json } }
+ #js-export-actions{ data: { options: { target: '.js-wiki-page-content', title: @page.human_title, stylesheet: [stylesheet_path('application')] }.to_json } }
- if @page.historical?
= render Pajamas::AlertComponent.new(variant: :warning,
diff --git a/app/views/users/_cover_controls.html.haml b/app/views/users/_cover_controls.html.haml
deleted file mode 100644
index 899a08c8a17..00000000000
--- a/app/views/users/_cover_controls.html.haml
+++ /dev/null
@@ -1,2 +0,0 @@
-.cover-controls.gl-display-flex.gl-gap-3.gl-pb-4
- = yield
diff --git a/app/views/users/_overview.html.haml b/app/views/users/_overview.html.haml
index 3649f72c956..597e7c37388 100644
--- a/app/views/users/_overview.html.haml
+++ b/app/views/users/_overview.html.haml
@@ -1,4 +1,4 @@
-- activity_pane_class = Feature.enabled?(:security_auto_fix) && @user.bot? ? "col-12" : "col-md-12 col-lg-6"
+- activity_pane_class = Feature.enabled?(:security_auto_fix) && @user.bot? ? "col-12" : "col-md-12 col-lg-6 gl-align-self-start"
.row.d-none.d-sm-flex
.col-12.calendar-block.gl-my-3
@@ -33,7 +33,7 @@
%h4.gl-flex-grow-1
= Feature.enabled?(:security_auto_fix) && @user.bot? ? s_('UserProfile|Bot activity') : s_('UserProfile|Activity')
= link_to s_('UserProfile|View all'), user_activity_path, class: "hide js-view-all"
- .overview-content-list{ data: { href: user_activity_path, testid: 'user-activity-content' } }
+ .overview-content-list.user-activity-content{ data: { href: user_activity_path, testid: 'user-activity-content' } }
= gl_loading_icon(size: 'md', css_class: 'loading')
- unless Feature.enabled?(:security_auto_fix) && @user.bot?
diff --git a/app/views/users/_profile_basic_info.html.haml b/app/views/users/_profile_basic_info.html.haml
index 6de9e80008e..7dd131dbe2c 100644
--- a/app/views/users/_profile_basic_info.html.haml
+++ b/app/views/users/_profile_basic_info.html.haml
@@ -2,9 +2,5 @@
= render 'middle_dot_divider', stacking: true do
@#{@user.username}
- if can?(current_user, :read_user_profile, @user)
- - unless Feature.enabled?(:user_profile_overflow_menu_vue)
- = render 'middle_dot_divider', stacking: true do
- = s_('UserProfile|User ID: %{id}') % { id: @user.id }
- = clipboard_button(title: s_('UserProfile|Copy user ID'), text: @user.id)
= render 'middle_dot_divider', stacking: true do
= s_('Member since %{date}') % { date: l(@user.created_at.to_date, format: :long) }
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index 0881c5bba54..e23555428aa 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -17,32 +17,16 @@
.user-profile
.cover-block.user-cover-block.gl-border-t.gl-border-b.gl-mt-n1
%div{ class: container_class }
- - if Feature.enabled?(:user_profile_overflow_menu_vue)
- .cover-controls.gl-display-flex.gl-gap-3.gl-pb-4
- = render 'users/follow_user'
- -# The following edit button is mutually exclusive to the follow user button, they won't be shown together
- - if @user == current_user
- = render Pajamas::ButtonComponent.new(href: profile_path,
- button_options: { class: 'gl-flex-grow-1', title: s_('UserProfile|Edit profile') }) do
- = s_("UserProfile|Edit profile")
- = render 'users/view_gpg_keys'
- = render 'users/view_user_in_admin_area'
- .js-user-profile-actions{ data: user_profile_actions_data(@user) }
- - else
- = render layout: 'users/cover_controls' do
- - if @user == current_user
- = render Pajamas::ButtonComponent.new(href: profile_path,
- icon: 'pencil',
- button_options: { class: 'gl-flex-grow-1 has-tooltip', title: s_('UserProfile|Edit profile'), 'aria-label': 'Edit profile', data: { toggle: 'tooltip', placement: 'bottom', container: 'body' }})
- - elsif current_user
- #js-report-abuse{ data: { report_abuse_path: add_category_abuse_reports_path, reported_user_id: @user.id, reported_from_url: user_url(@user) } }
- = render 'users/view_gpg_keys'
- - if can?(current_user, :read_user_profile, @user)
- = render Pajamas::ButtonComponent.new(href: user_path(@user, rss_url_options),
- icon: 'rss',
- button_options: { class: 'gl-flex-grow-1 has-tooltip', title: s_('UserProfile|Subscribe'), data: { toggle: 'tooltip', placement: 'bottom', container: 'body' }})
- = render 'users/view_user_in_admin_area'
- = render 'users/follow_user'
+ .cover-controls.gl-display-flex.gl-gap-3.gl-pb-4
+ = render 'users/follow_user'
+ -# The following edit button is mutually exclusive to the follow user button, they won't be shown together
+ - if @user == current_user
+ = render Pajamas::ButtonComponent.new(href: profile_path,
+ button_options: { class: 'gl-flex-grow-1', title: s_('UserProfile|Edit profile') }) do
+ = s_("UserProfile|Edit profile")
+ = render 'users/view_gpg_keys'
+ = render 'users/view_user_in_admin_area'
+ .js-user-profile-actions{ data: user_profile_actions_data(@user) }
.profile-header{ class: [('with-no-profile-tabs' if profile_tabs.empty?), ('gl-mb-4!' if show_super_sidebar?)] }
.gl-display-inline-block.gl-mx-8.gl-vertical-align-top
@@ -111,6 +95,10 @@
= render 'middle_dot_divider', breakpoint: 'sm' do
= link_to discord_url(@user), class: 'gl-hover-text-decoration-none', title: "Discord", target: '_blank', rel: 'noopener noreferrer nofollow' do
= sprite_icon('discord', css_class: 'discord-icon')
+ - if Feature.enabled?(:mastodon_social_ui, @user) && @user.mastodon.present?
+ = render 'middle_dot_divider', breakpoint: 'sm' do
+ = link_to mastodon_url(@user), class: 'gl-hover-text-decoration-none', title: "Mastodon", target: '_blank', rel: 'noopener noreferrer nofollow' do
+ = sprite_icon('mastodon', css_class: 'mastodon-icon')
- if @user.website_url.present?
= render 'middle_dot_divider', stacking: true do
- if Feature.enabled?(:security_auto_fix) && @user.bot?
@@ -126,6 +114,8 @@
%p.profile-user-bio.gl-mb-3
= @user.bio
+ -# TODO: Remove this with the removal of the old navigation.
+ -# See https://gitlab.com/groups/gitlab-org/-/epics/11875.
- if !profile_tabs.empty? && !Feature.enabled?(:profile_tabs_vue, current_user)
.scrolling-tabs-container{ class: [('gl-display-none' if show_super_sidebar?)] }
%button.fade-left{ type: 'button', title: _('Scroll left'), 'aria-label': _('Scroll left') }
@@ -169,7 +159,7 @@
= gl_badge_tag @user.followers.count, size: :sm
- if profile_tab?(:following)
%li.js-following-tab
- = link_to user_following_path, data: { target: 'div#following', action: 'following', toggle: 'tab', endpoint: user_following_path(format: :json), qa_selector: 'following_tab' } do
+ = link_to user_following_path, data: { target: 'div#following', action: 'following', toggle: 'tab', endpoint: user_following_path(format: :json) } do
= s_('UserProfile|Following')
= gl_badge_tag @user.followees.count, size: :sm
- if !profile_tabs.empty? && Feature.enabled?(:profile_tabs_vue, current_user)
@@ -183,13 +173,15 @@
- if profile_tab?(:activity)
#activity.tab-pane
- .flash-container
- - if can?(current_user, :read_cross_project)
- %h4.prepend-top-20
- = s_('UserProfile|Most Recent Activity')
- .content_list{ data: { href: user_activity_path } }
- .loading
- = gl_loading_icon(size: 'md')
+ .row
+ .col-12
+ .flash-container
+ - if can?(current_user, :read_cross_project)
+ %h4.prepend-top-20
+ = s_('UserProfile|Most Recent Activity')
+ .content_list.user-activity-content{ data: { href: user_activity_path } }
+ .loading
+ = gl_loading_icon(size: 'md')
- unless @user.bot?
- if profile_tab?(:groups)
#groups.tab-pane
diff --git a/app/workers/abuse/spam_abuse_events_worker.rb b/app/workers/abuse/spam_abuse_events_worker.rb
new file mode 100644
index 00000000000..7d86e994ae4
--- /dev/null
+++ b/app/workers/abuse/spam_abuse_events_worker.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+module Abuse
+ class SpamAbuseEventsWorker
+ include ApplicationWorker
+
+ data_consistency :delayed
+
+ idempotent!
+ feature_category :instance_resiliency
+ urgency :low
+
+ def perform(params)
+ params = params.with_indifferent_access
+
+ @user = User.find_by_id(params[:user_id])
+ unless @user
+ logger.info(structured_payload(message: "User not found.", user_id: params[:user_id]))
+ return
+ end
+
+ report_user(params)
+ end
+
+ private
+
+ attr_reader :user
+
+ def report_user(params)
+ category = 'spam'
+ reporter = Users::Internal.security_bot
+ report_params = { user_id: params[:user_id],
+ reporter: reporter,
+ category: category,
+ message: 'User reported for abuse based on spam verdict' }
+
+ abuse_report = AbuseReport.by_category(category).by_reporter_id(reporter.id).by_user_id(params[:user_id]).first
+
+ abuse_report = AbuseReport.create!(report_params) if abuse_report.nil?
+
+ create_abuse_event(abuse_report.id, params)
+ end
+
+ # Associate the abuse report with an abuse event
+ def create_abuse_event(abuse_report_id, params)
+ Abuse::Event.create!(
+ abuse_report_id: abuse_report_id,
+ category: :spam,
+ metadata: { noteable_type: params[:noteable_type],
+ title: params[:title],
+ description: params[:description],
+ source_ip: params[:source_ip],
+ user_agent: params[:user_agent],
+ verdict: params[:verdict] },
+ source: :spamcheck,
+ user: user
+ )
+ end
+ end
+end
diff --git a/app/workers/activity_pub/projects/releases_subscription_worker.rb b/app/workers/activity_pub/projects/releases_subscription_worker.rb
new file mode 100644
index 00000000000..c392726a469
--- /dev/null
+++ b/app/workers/activity_pub/projects/releases_subscription_worker.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+module ActivityPub
+ module Projects
+ class ReleasesSubscriptionWorker
+ include ApplicationWorker
+ include Gitlab::Routing.url_helpers
+
+ idempotent!
+ worker_has_external_dependencies!
+ feature_category :release_orchestration
+ data_consistency :delayed
+ queue_namespace :activity_pub
+
+ sidekiq_retries_exhausted do |msg, _ex|
+ subscription_id = msg['args'].second
+ subscription = ActivityPub::ReleasesSubscription.find_by_id(subscription_id)
+ subscription&.destroy
+ end
+
+ def perform(subscription_id)
+ subscription = ActivityPub::ReleasesSubscription.find_by_id(subscription_id)
+ return if subscription.nil?
+
+ unless subscription.project.public?
+ subscription.destroy
+ return
+ end
+
+ InboxResolverService.new(subscription).execute if needs_resolving?(subscription)
+ AcceptFollowService.new(subscription, project_releases_url(subscription.project)).execute
+ end
+
+ def needs_resolving?(subscription)
+ subscription.subscriber_inbox_url.blank? || subscription.shared_inbox_url.blank?
+ end
+ end
+ end
+end
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index e5b860ba525..0bb88efe183 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -3,6 +3,15 @@
#
# Do not edit it manually!
---
+- :name: activity_pub:activity_pub_projects_releases_subscription
+ :worker_name: ActivityPub::Projects::ReleasesSubscriptionWorker
+ :feature_category: :release_orchestration
+ :has_external_dependencies: true
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: authorized_project_update:authorized_project_update_project_recalculate
:worker_name: AuthorizedProjectUpdate::ProjectRecalculateWorker
:feature_category: :system_access
@@ -1461,42 +1470,6 @@
:weight: 1
:idempotent: false
:tags: []
-- :name: hashed_storage:hashed_storage_migrator
- :worker_name: HashedStorage::MigratorWorker
- :feature_category: :source_code_management
- :has_external_dependencies: false
- :urgency: :low
- :resource_boundary: :unknown
- :weight: 1
- :idempotent: false
- :tags: []
-- :name: hashed_storage:hashed_storage_project_migrate
- :worker_name: HashedStorage::ProjectMigrateWorker
- :feature_category: :source_code_management
- :has_external_dependencies: false
- :urgency: :low
- :resource_boundary: :unknown
- :weight: 1
- :idempotent: false
- :tags: []
-- :name: hashed_storage:hashed_storage_project_rollback
- :worker_name: HashedStorage::ProjectRollbackWorker
- :feature_category: :source_code_management
- :has_external_dependencies: false
- :urgency: :low
- :resource_boundary: :unknown
- :weight: 1
- :idempotent: false
- :tags: []
-- :name: hashed_storage:hashed_storage_rollbacker
- :worker_name: HashedStorage::RollbackerWorker
- :feature_category: :source_code_management
- :has_external_dependencies: false
- :urgency: :low
- :resource_boundary: :unknown
- :weight: 1
- :idempotent: false
- :tags: []
- :name: incident_management:incident_management_add_severity_system_note
:worker_name: IncidentManagement::AddSeveritySystemNoteWorker
:feature_category: :incident_management
@@ -1767,6 +1740,15 @@
:weight: 1
:idempotent: true
:tags: []
+- :name: package_cleanup:packages_npm_cleanup_stale_metadata_cache
+ :worker_name: Packages::Npm::CleanupStaleMetadataCacheWorker
+ :feature_category: :package_registry
+ :has_external_dependencies: false
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: package_repositories:packages_debian_generate_distribution
:worker_name: Packages::Debian::GenerateDistributionWorker
:feature_category: :package_registry
@@ -2307,6 +2289,15 @@
:weight: 1
:idempotent: true
:tags: []
+- :name: abuse_spam_abuse_events
+ :worker_name: Abuse::SpamAbuseEventsWorker
+ :feature_category: :instance_resiliency
+ :has_external_dependencies: false
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: analytics_usage_trends_counter_job
:worker_name: Analytics::UsageTrends::CounterJobWorker
:feature_category: :devops_reports
@@ -2575,7 +2566,7 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent: false
+ :idempotent: true
:tags: []
- :name: bulk_imports_entity
:worker_name: BulkImports::EntityWorker
@@ -2629,7 +2620,7 @@
:urgency: :low
:resource_boundary: :memory
:weight: 1
- :idempotent: false
+ :idempotent: true
:tags: []
- :name: bulk_imports_pipeline_batch
:worker_name: BulkImports::PipelineBatchWorker
@@ -2638,7 +2629,7 @@
:urgency: :low
:resource_boundary: :memory
:weight: 1
- :idempotent: false
+ :idempotent: true
:tags: []
- :name: bulk_imports_relation_batch_export
:worker_name: BulkImports::RelationBatchExportWorker
@@ -2892,6 +2883,15 @@
:weight: 2
:idempotent: false
:tags: []
+- :name: environments_auto_recover
+ :worker_name: Environments::AutoRecoverWorker
+ :feature_category: :continuous_delivery
+ :has_external_dependencies: false
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: environments_auto_stop
:worker_name: Environments::AutoStopWorker
:feature_category: :continuous_delivery
@@ -3567,6 +3567,15 @@
:weight: 1
:idempotent: false
:tags: []
+- :name: projects_import_export_after_import_merge_requests
+ :worker_name: Projects::ImportExport::AfterImportMergeRequestsWorker
+ :feature_category: :importers
+ :has_external_dependencies: false
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: projects_import_export_create_relation_exports
:worker_name: Projects::ImportExport::CreateRelationExportsWorker
:feature_category: :importers
@@ -3837,15 +3846,6 @@
:weight: 1
:idempotent: false
:tags: []
-- :name: tasks_to_be_done_create
- :worker_name: TasksToBeDone::CreateWorker
- :feature_category: :onboarding
- :has_external_dependencies: false
- :urgency: :low
- :resource_boundary: :cpu
- :weight: 1
- :idempotent: true
- :tags: []
- :name: update_external_pull_requests
:worker_name: UpdateExternalPullRequestsWorker
:feature_category: :continuous_integration
diff --git a/app/workers/bulk_import_worker.rb b/app/workers/bulk_import_worker.rb
index 5b9b46081cc..70e7d82741f 100644
--- a/app/workers/bulk_import_worker.rb
+++ b/app/workers/bulk_import_worker.rb
@@ -1,11 +1,16 @@
# frozen_string_literal: true
-class BulkImportWorker # rubocop:disable Scalability/IdempotentWorker
+class BulkImportWorker
include ApplicationWorker
data_consistency :always
feature_category :importers
- sidekiq_options retry: false, dead: false
+ sidekiq_options retry: 3, dead: false
+ idempotent!
+
+ sidekiq_retries_exhausted do |msg, exception|
+ new.perform_failure(exception, msg['args'].first)
+ end
def perform(bulk_import_id)
bulk_import = BulkImport.find_by_id(bulk_import_id)
@@ -13,4 +18,12 @@ class BulkImportWorker # rubocop:disable Scalability/IdempotentWorker
BulkImports::ProcessService.new(bulk_import).execute
end
+
+ def perform_failure(exception, bulk_import_id)
+ bulk_import = BulkImport.find_by_id(bulk_import_id)
+
+ Gitlab::ErrorTracking.track_exception(exception, bulk_import_id: bulk_import.id)
+
+ bulk_import.fail_op
+ end
end
diff --git a/app/workers/bulk_imports/entity_worker.rb b/app/workers/bulk_imports/entity_worker.rb
index 9b60dcdeb8a..e510a8c0d06 100644
--- a/app/workers/bulk_imports/entity_worker.rb
+++ b/app/workers/bulk_imports/entity_worker.rb
@@ -5,12 +5,16 @@ module BulkImports
include ApplicationWorker
idempotent!
- deduplicate :until_executed
+ deduplicate :until_executed, if_deduplicated: :reschedule_once
data_consistency :always
feature_category :importers
- sidekiq_options retry: false, dead: false
+ sidekiq_options retry: 3, dead: false
worker_has_external_dependencies!
+ sidekiq_retries_exhausted do |msg, exception|
+ new.perform_failure(exception, msg['args'].first)
+ end
+
PERFORM_DELAY = 5.seconds
# Keep `_current_stage` parameter for backwards compatibility.
@@ -27,10 +31,17 @@ module BulkImports
end
re_enqueue
- rescue StandardError => e
- Gitlab::ErrorTracking.track_exception(e, log_params(message: 'Entity failed'))
+ end
+
+ def perform_failure(exception, entity_id)
+ @entity = ::BulkImports::Entity.find(entity_id)
+
+ Gitlab::ErrorTracking.track_exception(
+ exception,
+ log_params(message: "Request to export #{entity.source_type} failed")
+ )
- @entity.fail_op!
+ entity.fail_op!
end
private
@@ -68,7 +79,7 @@ module BulkImports
end
def logger
- @logger ||= Gitlab::Import::Logger.build
+ @logger ||= Logger.build
end
def log_exception(exception, payload)
@@ -88,7 +99,7 @@ module BulkImports
bulk_import_entity_type: entity.source_type,
source_full_path: entity.source_full_path,
source_version: source_version,
- importer: 'gitlab_migration'
+ importer: Logger::IMPORTER_NAME
}
defaults.merge(extra)
diff --git a/app/workers/bulk_imports/export_request_worker.rb b/app/workers/bulk_imports/export_request_worker.rb
index 44759916f99..f7456ddccb1 100644
--- a/app/workers/bulk_imports/export_request_worker.rb
+++ b/app/workers/bulk_imports/export_request_worker.rb
@@ -80,8 +80,7 @@ module BulkImports
bulk_import_id: entity.bulk_import_id,
bulk_import_entity_type: entity.source_type,
source_full_path: entity.source_full_path,
- source_version: entity.bulk_import.source_version_info.to_s,
- importer: 'gitlab_migration'
+ source_version: entity.bulk_import.source_version_info.to_s
}
)
@@ -97,7 +96,7 @@ module BulkImports
end
def logger
- @logger ||= Gitlab::Import::Logger.build
+ @logger ||= Logger.build
end
def log_exception(exception, payload)
@@ -114,8 +113,7 @@ module BulkImports
bulk_import_entity_type: entity.source_type,
source_full_path: entity.source_full_path,
message: "Request to export #{entity.source_type} failed",
- source_version: entity.bulk_import.source_version_info.to_s,
- importer: 'gitlab_migration'
+ source_version: entity.bulk_import.source_version_info.to_s
}
)
diff --git a/app/workers/bulk_imports/finish_batched_pipeline_worker.rb b/app/workers/bulk_imports/finish_batched_pipeline_worker.rb
index b1f3757e058..40d26e14dc1 100644
--- a/app/workers/bulk_imports/finish_batched_pipeline_worker.rb
+++ b/app/workers/bulk_imports/finish_batched_pipeline_worker.rb
@@ -16,22 +16,21 @@ module BulkImports
def perform(pipeline_tracker_id)
@tracker = Tracker.find(pipeline_tracker_id)
+ @context = ::BulkImports::Pipeline::Context.new(tracker)
return unless tracker.batched?
return unless tracker.started?
return re_enqueue if import_in_progress?
if tracker.stale?
+ logger.error(log_attributes(message: 'Tracker stale. Failing batches and tracker'))
tracker.batches.map(&:fail_op!)
tracker.fail_op!
else
+ tracker.pipeline_class.new(@context).on_finish
+ logger.info(log_attributes(message: 'Tracker finished'))
tracker.finish!
end
-
- ensure
- # This is needed for in-flight migrations.
- # It will be remove in https://gitlab.com/gitlab-org/gitlab/-/issues/426299
- ::BulkImports::EntityWorker.perform_async(tracker.entity.id) if job_version.nil?
end
private
@@ -45,5 +44,20 @@ module BulkImports
def import_in_progress?
tracker.batches.any? { |b| b.started? || b.created? }
end
+
+ def logger
+ @logger ||= Logger.build
+ end
+
+ def log_attributes(extra = {})
+ structured_payload(
+ {
+ tracker_id: tracker.id,
+ bulk_import_id: tracker.entity.id,
+ bulk_import_entity_id: tracker.entity.bulk_import_id,
+ pipeline_class: tracker.pipeline_name
+ }.merge(extra)
+ )
+ end
end
end
diff --git a/app/workers/bulk_imports/pipeline_batch_worker.rb b/app/workers/bulk_imports/pipeline_batch_worker.rb
index 6230d517641..1485275e616 100644
--- a/app/workers/bulk_imports/pipeline_batch_worker.rb
+++ b/app/workers/bulk_imports/pipeline_batch_worker.rb
@@ -1,26 +1,65 @@
# frozen_string_literal: true
module BulkImports
- class PipelineBatchWorker # rubocop:disable Scalability/IdempotentWorker
+ class PipelineBatchWorker
include ApplicationWorker
include ExclusiveLeaseGuard
+ DEFER_ON_HEALTH_DELAY = 5.minutes
+
data_consistency :always # rubocop:disable SidekiqLoadBalancing/WorkerDataConsistency
feature_category :importers
- sidekiq_options retry: false, dead: false
+ sidekiq_options dead: false, retry: 3
worker_has_external_dependencies!
worker_resource_boundary :memory
+ idempotent!
+
+ sidekiq_retries_exhausted do |msg, exception|
+ new.perform_failure(msg['args'].first, exception)
+ end
+
+ defer_on_database_health_signal(:gitlab_main, [], DEFER_ON_HEALTH_DELAY) do |job_args, schema, tables|
+ batch = ::BulkImports::BatchTracker.find(job_args.first)
+ pipeline_tracker = batch.tracker
+ pipeline_schema = ::BulkImports::PipelineSchemaInfo.new(
+ pipeline_tracker.pipeline_class,
+ pipeline_tracker.entity.portable_class
+ )
+
+ if pipeline_schema.db_schema && pipeline_schema.db_table
+ schema = pipeline_schema.db_schema
+ tables = [pipeline_schema.db_table]
+ end
+
+ [schema, tables]
+ end
+
+ def self.defer_on_database_health_signal?
+ Feature.enabled?(:bulk_import_deferred_workers)
+ end
def perform(batch_id)
@batch = ::BulkImports::BatchTracker.find(batch_id)
+
@tracker = @batch.tracker
@pending_retry = false
+ return unless process_batch?
+
+ log_extra_metadata_on_done(:pipeline_class, @tracker.pipeline_name)
+
try_obtain_lease { run }
ensure
::BulkImports::FinishBatchedPipelineWorker.perform_async(tracker.id) unless pending_retry
end
+ def perform_failure(batch_id, exception)
+ @batch = ::BulkImports::BatchTracker.find(batch_id)
+ @tracker = @batch.tracker
+
+ fail_batch(exception)
+ end
+
private
attr_reader :batch, :tracker, :pending_retry
@@ -28,35 +67,31 @@ module BulkImports
def run
return batch.skip! if tracker.failed? || tracker.finished?
+ logger.info(log_attributes(message: 'Batch tracker started'))
batch.start!
tracker.pipeline_class.new(context).run
batch.finish!
+ logger.info(log_attributes(message: 'Batch tracker finished'))
rescue BulkImports::RetryPipelineError => e
@pending_retry = true
retry_batch(e)
- rescue StandardError => e
- fail_batch(e)
end
def fail_batch(exception)
batch.fail_op!
- Gitlab::ErrorTracking.track_exception(
- exception,
- batch_id: batch.id,
- tracker_id: tracker.id,
- pipeline_class: tracker.pipeline_name,
- pipeline_step: 'pipeline_batch_worker_run'
- )
+ Gitlab::ErrorTracking.track_exception(exception, log_attributes(message: 'Batch tracker failed'))
BulkImports::Failure.create(
bulk_import_entity_id: batch.tracker.entity.id,
pipeline_class: tracker.pipeline_name,
pipeline_step: 'pipeline_batch_worker_run',
exception_class: exception.class.to_s,
- exception_message: exception.message.truncate(255),
+ exception_message: exception.message,
correlation_id_value: Labkit::Correlation::CorrelationId.current_or_new_id
)
+
+ ::BulkImports::FinishBatchedPipelineWorker.perform_async(tracker.id)
end
def context
@@ -78,7 +113,32 @@ module BulkImports
end
def re_enqueue(delay = FILE_EXTRACTION_PIPELINE_PERFORM_DELAY)
+ log_extra_metadata_on_done(:re_enqueue, true)
+
self.class.perform_in(delay, batch.id)
end
+
+ def process_batch?
+ batch.created? || batch.started?
+ end
+
+ def logger
+ @logger ||= Logger.build
+ end
+
+ def log_attributes(extra = {})
+ structured_payload(
+ {
+ batch_id: batch.id,
+ batch_number: batch.batch_number,
+ tracker_id: tracker.id,
+ bulk_import_id: tracker.entity.bulk_import_id,
+ bulk_import_entity_id: tracker.entity.id,
+ pipeline_class: tracker.pipeline_name,
+ pipeline_step: 'pipeline_batch_worker_run',
+ importer: Logger::IMPORTER_NAME
+ }.merge(extra)
+ )
+ end
end
end
diff --git a/app/workers/bulk_imports/pipeline_worker.rb b/app/workers/bulk_imports/pipeline_worker.rb
index 24185f43795..2c1d28b33c5 100644
--- a/app/workers/bulk_imports/pipeline_worker.rb
+++ b/app/workers/bulk_imports/pipeline_worker.rb
@@ -1,43 +1,68 @@
# frozen_string_literal: true
module BulkImports
- class PipelineWorker # rubocop:disable Scalability/IdempotentWorker
+ class PipelineWorker
include ApplicationWorker
include ExclusiveLeaseGuard
FILE_EXTRACTION_PIPELINE_PERFORM_DELAY = 10.seconds
+ DEFER_ON_HEALTH_DELAY = 5.minutes
+
data_consistency :always
feature_category :importers
- sidekiq_options retry: false, dead: false
+ sidekiq_options dead: false, retry: 3
worker_has_external_dependencies!
deduplicate :until_executing
worker_resource_boundary :memory
+ idempotent!
version 2
+ sidekiq_retries_exhausted do |msg, exception|
+ new.perform_failure(msg['args'][0], msg['args'][2], exception)
+ end
+
+ defer_on_database_health_signal(:gitlab_main, [], DEFER_ON_HEALTH_DELAY) do |job_args, schema, tables|
+ pipeline_tracker = ::BulkImports::Tracker.find(job_args.first)
+ pipeline_schema = ::BulkImports::PipelineSchemaInfo.new(
+ pipeline_tracker.pipeline_class,
+ pipeline_tracker.entity.portable_class
+ )
+
+ if pipeline_schema.db_schema && pipeline_schema.db_table
+ schema = pipeline_schema.db_schema
+ tables = [pipeline_schema.db_table]
+ end
+
+ [schema, tables]
+ end
+
+ def self.defer_on_database_health_signal?
+ Feature.enabled?(:bulk_import_deferred_workers)
+ end
+
# Keep _stage parameter for backwards compatibility.
def perform(pipeline_tracker_id, _stage, entity_id)
@entity = ::BulkImports::Entity.find(entity_id)
@pipeline_tracker = ::BulkImports::Tracker.find(pipeline_tracker_id)
+ log_extra_metadata_on_done(:pipeline_class, @pipeline_tracker.pipeline_name)
+
try_obtain_lease do
- if pipeline_tracker.enqueued?
+ if pipeline_tracker.enqueued? || pipeline_tracker.started?
logger.info(log_attributes(message: 'Pipeline starting'))
run
- else
- message = "Pipeline in #{pipeline_tracker.human_status_name} state instead of expected enqueued state"
-
- logger.error(log_attributes(message: message))
-
- fail_tracker(StandardError.new(message)) unless pipeline_tracker.finished? || pipeline_tracker.skipped?
end
end
- ensure
- # This is needed for in-flight migrations.
- # It will be remove in https://gitlab.com/gitlab-org/gitlab/-/issues/426299
- ::BulkImports::EntityWorker.perform_async(entity_id) if job_version.nil?
+ end
+
+ def perform_failure(pipeline_tracker_id, entity_id, exception)
+ @entity = ::BulkImports::Entity.find(entity_id)
+ @pipeline_tracker = ::BulkImports::Tracker.find(pipeline_tracker_id)
+
+ fail_tracker(exception)
end
private
@@ -53,20 +78,22 @@ module BulkImports
return re_enqueue if export_empty? || export_started?
if file_extraction_pipeline? && export_status.batched?
+ log_extra_metadata_on_done(:batched, true)
+
pipeline_tracker.update!(status_event: 'start', jid: jid, batched: true)
return pipeline_tracker.finish! if export_status.batches_count < 1
enqueue_batches
else
+ log_extra_metadata_on_done(:batched, false)
+
pipeline_tracker.update!(status_event: 'start', jid: jid)
pipeline_tracker.pipeline_class.new(context).run
pipeline_tracker.finish!
end
rescue BulkImports::RetryPipelineError => e
retry_tracker(e)
- rescue StandardError => e
- fail_tracker(e)
end
def source_version
@@ -85,16 +112,18 @@ module BulkImports
pipeline_class: pipeline_tracker.pipeline_name,
pipeline_step: 'pipeline_worker_run',
exception_class: exception.class.to_s,
- exception_message: exception.message.truncate(255),
+ exception_message: exception.message,
correlation_id_value: Labkit::Correlation::CorrelationId.current_or_new_id
)
end
def logger
- @logger ||= Gitlab::Import::Logger.build
+ @logger ||= Logger.build
end
def re_enqueue(delay = FILE_EXTRACTION_PIPELINE_PERFORM_DELAY)
+ log_extra_metadata_on_done(:re_enqueue, true)
+
self.class.perform_in(
delay,
pipeline_tracker.id,
@@ -159,10 +188,10 @@ module BulkImports
bulk_import_entity_type: entity.source_type,
source_full_path: entity.source_full_path,
pipeline_tracker_id: pipeline_tracker.id,
- pipeline_name: pipeline_tracker.pipeline_name,
+ pipeline_class: pipeline_tracker.pipeline_name,
pipeline_tracker_state: pipeline_tracker.human_status_name,
source_version: source_version,
- importer: 'gitlab_migration'
+ importer: Logger::IMPORTER_NAME
}.merge(extra)
)
end
diff --git a/app/workers/bulk_imports/relation_batch_export_worker.rb b/app/workers/bulk_imports/relation_batch_export_worker.rb
index 4ce36929e15..87ceb775075 100644
--- a/app/workers/bulk_imports/relation_batch_export_worker.rb
+++ b/app/workers/bulk_imports/relation_batch_export_worker.rb
@@ -7,10 +7,25 @@ module BulkImports
idempotent!
data_consistency :always # rubocop:disable SidekiqLoadBalancing/WorkerDataConsistency
feature_category :importers
- sidekiq_options status_expiration: StuckExportJobsWorker::EXPORT_JOBS_EXPIRATION
+ sidekiq_options status_expiration: StuckExportJobsWorker::EXPORT_JOBS_EXPIRATION, retry: 3
+
+ sidekiq_retries_exhausted do |job, exception|
+ batch = BulkImports::ExportBatch.find(job['args'][1])
+ portable = batch.export.portable
+
+ Gitlab::ErrorTracking.track_exception(exception, portable_id: portable.id, portable_type: portable.class.name)
+
+ batch.update!(status_event: 'fail_op', error: exception.message.truncate(255))
+ end
def perform(user_id, batch_id)
- RelationBatchExportService.new(user_id, batch_id).execute
+ @user = User.find(user_id)
+ @batch = BulkImports::ExportBatch.find(batch_id)
+
+ log_extra_metadata_on_done(:relation, @batch.export.relation)
+ log_extra_metadata_on_done(:objects_count, @batch.objects_count)
+
+ RelationBatchExportService.new(@user, @batch).execute
end
end
end
diff --git a/app/workers/bulk_imports/relation_export_worker.rb b/app/workers/bulk_imports/relation_export_worker.rb
index 531edc6c7a7..168626fee85 100644
--- a/app/workers/bulk_imports/relation_export_worker.rb
+++ b/app/workers/bulk_imports/relation_export_worker.rb
@@ -10,25 +10,37 @@ module BulkImports
loggable_arguments 2, 3
data_consistency :always
feature_category :importers
- sidekiq_options status_expiration: StuckExportJobsWorker::EXPORT_JOBS_EXPIRATION
+ sidekiq_options status_expiration: StuckExportJobsWorker::EXPORT_JOBS_EXPIRATION, retry: 3
worker_resource_boundary :memory
+ sidekiq_retries_exhausted do |job, exception|
+ _user_id, portable_id, portable_type, relation, batched = job['args']
+ portable = portable(portable_id, portable_type)
+
+ export = portable.bulk_import_exports.find_by_relation(relation)
+
+ Gitlab::ErrorTracking.track_exception(exception, portable_id: portable_id, portable_type: portable.class.name)
+
+ export.update!(status_event: 'fail_op', error: exception.message.truncate(255), batched: batched)
+ end
+
+ def self.portable(portable_id, portable_class)
+ portable_class.classify.constantize.find(portable_id)
+ end
+
def perform(user_id, portable_id, portable_class, relation, batched = false)
user = User.find(user_id)
- portable = portable(portable_id, portable_class)
+ portable = self.class.portable(portable_id, portable_class)
config = BulkImports::FileTransfer.config_for(portable)
+ log_extra_metadata_on_done(:relation, relation)
if Gitlab::Utils.to_boolean(batched) && config.batchable_relation?(relation)
+ log_extra_metadata_on_done(:batched, true)
BatchedRelationExportService.new(user, portable, relation, jid).execute
else
+ log_extra_metadata_on_done(:batched, false)
RelationExportService.new(user, portable, relation, jid).execute
end
end
-
- private
-
- def portable(portable_id, portable_class)
- portable_class.classify.constantize.find(portable_id)
- end
end
end
diff --git a/app/workers/bulk_imports/stuck_import_worker.rb b/app/workers/bulk_imports/stuck_import_worker.rb
index 3fa4221728b..6c8569b0aa0 100644
--- a/app/workers/bulk_imports/stuck_import_worker.rb
+++ b/app/workers/bulk_imports/stuck_import_worker.rb
@@ -14,18 +14,29 @@ module BulkImports
def perform
BulkImport.stale.find_each do |import|
+ logger.error(message: 'BulkImport stale', bulk_import_id: import.id)
import.cleanup_stale
end
- BulkImports::Entity.includes(:trackers).stale.find_each do |import| # rubocop: disable CodeReuse/ActiveRecord
+ BulkImports::Entity.includes(:trackers).stale.find_each do |entity| # rubocop: disable CodeReuse/ActiveRecord
ApplicationRecord.transaction do
- import.cleanup_stale
+ logger.error(
+ message: 'BulkImports::Entity stale',
+ bulk_import_id: entity.bulk_import_id,
+ bulk_import_entity_id: entity.id
+ )
- import.trackers.find_each do |tracker|
+ entity.cleanup_stale
+
+ entity.trackers.find_each do |tracker|
tracker.cleanup_stale
end
end
end
end
+
+ def logger
+ @logger ||= Logger.build
+ end
end
end
diff --git a/app/workers/ci/cancel_pipeline_worker.rb b/app/workers/ci/cancel_pipeline_worker.rb
index 0b2c96e7ace..f099e185629 100644
--- a/app/workers/ci/cancel_pipeline_worker.rb
+++ b/app/workers/ci/cancel_pipeline_worker.rb
@@ -20,7 +20,7 @@ module Ci
pipeline: pipeline,
current_user: nil,
cascade_to_children: false,
- auto_canceled_by_pipeline_id: auto_canceled_by_pipeline_id
+ auto_canceled_by_pipeline: ::Ci::Pipeline.find_by_id(auto_canceled_by_pipeline_id)
).force_execute
end
end
diff --git a/app/workers/ci/initial_pipeline_process_worker.rb b/app/workers/ci/initial_pipeline_process_worker.rb
index 703cae8bf88..8d7a62e5b09 100644
--- a/app/workers/ci/initial_pipeline_process_worker.rb
+++ b/app/workers/ci/initial_pipeline_process_worker.rb
@@ -17,24 +17,10 @@ module Ci
def perform(pipeline_id)
Ci::Pipeline.find_by_id(pipeline_id).try do |pipeline|
- create_deployments!(pipeline)
-
Ci::PipelineCreation::StartPipelineService
.new(pipeline)
.execute
end
end
-
- private
-
- def create_deployments!(pipeline)
- return if Feature.enabled?(:create_deployment_only_for_processable_jobs, pipeline.project)
-
- pipeline.stages.flat_map(&:statuses).each { |build| create_deployment(build) }
- end
-
- def create_deployment(build)
- ::Deployments::CreateForJobService.new.execute(build)
- end
end
end
diff --git a/app/workers/ci/refs/unlock_previous_pipelines_worker.rb b/app/workers/ci/refs/unlock_previous_pipelines_worker.rb
index bf595590cb1..588ec4ce1f0 100644
--- a/app/workers/ci/refs/unlock_previous_pipelines_worker.rb
+++ b/app/workers/ci/refs/unlock_previous_pipelines_worker.rb
@@ -14,7 +14,9 @@ module Ci
def perform(ref_id)
::Ci::Ref.find_by_id(ref_id).try do |ref|
- pipeline = ref.last_finished_pipeline
+ next unless ref.artifacts_locked?
+
+ pipeline = ref.last_unlockable_ci_source_pipeline
result = ::Ci::Refs::EnqueuePipelinesToUnlockService.new.execute(ref, before_pipeline: pipeline)
log_extra_metadata_on_done(:total_pending_entries, result[:total_pending_entries])
diff --git a/app/workers/concerns/gitlab/github_import/rescheduling_methods.rb b/app/workers/concerns/gitlab/github_import/rescheduling_methods.rb
index f6feb6d1598..316d30d94da 100644
--- a/app/workers/concerns/gitlab/github_import/rescheduling_methods.rb
+++ b/app/workers/concerns/gitlab/github_import/rescheduling_methods.rb
@@ -52,8 +52,7 @@ module Gitlab
job_delay = client.rate_limit_resets_in + calculate_job_delay(enqueued_job_counter)
- self.class
- .perform_in(job_delay, project.id, hash, notify_key)
+ self.class.perform_in(job_delay, project.id, hash.deep_stringify_keys, notify_key.to_s)
end
end
end
diff --git a/app/workers/concerns/gitlab/github_import/stage_methods.rb b/app/workers/concerns/gitlab/github_import/stage_methods.rb
index 80013ff3cd9..5c63c667a03 100644
--- a/app/workers/concerns/gitlab/github_import/stage_methods.rb
+++ b/app/workers/concerns/gitlab/github_import/stage_methods.rb
@@ -5,6 +5,8 @@ module Gitlab
module StageMethods
extend ActiveSupport::Concern
+ MAX_RETRIES_AFTER_INTERRUPTION = 20
+
included do
include ApplicationWorker
@@ -18,6 +20,29 @@ module Gitlab
end
end
+ class_methods do
+ # We can increase the number of times a GitHubImport::Stage worker is retried
+ # after being interrupted if the importer it executes can restart exactly
+ # from where it left off.
+ #
+ # It is not safe to call this method if the importer loops over its data from
+ # the beginning when restarted, even if it skips data that is already imported
+ # inside the loop, as there is a possibility the importer will never reach
+ # the end of the loop.
+ #
+ # Examples of stage workers that call this method are ones that execute services that:
+ #
+ # - Continue paging an endpoint from where it left off:
+ # https://gitlab.com/gitlab-org/gitlab/-/blob/487521cc/lib/gitlab/github_import/parallel_scheduling.rb#L114-117
+ # - Continue their loop from where it left off:
+ # https://gitlab.com/gitlab-org/gitlab/-/blob/024235ec/lib/gitlab/github_import/importer/pull_requests/review_requests_importer.rb#L15
+ def resumes_work_when_interrupted!
+ return unless Feature.enabled?(:github_importer_raise_max_interruptions)
+
+ sidekiq_options max_retries_after_interruption: MAX_RETRIES_AFTER_INTERRUPTION
+ end
+ end
+
# project_id - The ID of the GitLab project to import the data into.
def perform(project_id)
info(project_id, message: 'starting stage')
@@ -54,6 +79,8 @@ module Gitlab
# client - An instance of Gitlab::GithubImport::Client.
# project - An instance of Project.
def try_import(client, project)
+ project.import_state.refresh_jid_expiration
+
import(client, project)
rescue RateLimitError
self.class.perform_in(client.rate_limit_resets_in, project.id)
diff --git a/app/workers/concerns/worker_attributes.rb b/app/workers/concerns/worker_attributes.rb
index cb09aaf1a6a..28c82a5a38e 100644
--- a/app/workers/concerns/worker_attributes.rb
+++ b/app/workers/concerns/worker_attributes.rb
@@ -201,10 +201,10 @@ module WorkerAttributes
!!get_class_attribute(:big_payload)
end
- def defer_on_database_health_signal(gitlab_schema, tables = [], delay_by = DEFAULT_DEFER_DELAY)
+ def defer_on_database_health_signal(gitlab_schema, tables = [], delay_by = DEFAULT_DEFER_DELAY, &block)
set_class_attribute(
:database_health_check_attrs,
- { gitlab_schema: gitlab_schema, tables: tables, delay_by: delay_by }
+ { gitlab_schema: gitlab_schema, tables: tables, delay_by: delay_by, block: block }
)
end
diff --git a/app/workers/environments/auto_recover_worker.rb b/app/workers/environments/auto_recover_worker.rb
new file mode 100644
index 00000000000..75e86e38f1a
--- /dev/null
+++ b/app/workers/environments/auto_recover_worker.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module Environments
+ class AutoRecoverWorker
+ include ApplicationWorker
+
+ deduplicate :until_executed
+ data_consistency :delayed
+ idempotent!
+ feature_category :continuous_delivery
+
+ def perform(environment_id, _params = {})
+ Environment.find_by_id(environment_id).try do |environment|
+ next unless environment.long_stopping?
+
+ next unless environment.stop_actions.all?(&:complete?)
+
+ environment.recover_stuck_stopping
+ end
+ end
+ end
+end
diff --git a/app/workers/environments/auto_stop_cron_worker.rb b/app/workers/environments/auto_stop_cron_worker.rb
index 4d6453a85e7..26b18c406e5 100644
--- a/app/workers/environments/auto_stop_cron_worker.rb
+++ b/app/workers/environments/auto_stop_cron_worker.rb
@@ -13,6 +13,7 @@ module Environments
def perform
AutoStopService.new.execute
+ AutoRecoverService.new.execute
end
end
end
diff --git a/app/workers/gitlab/github_import/stage/import_attachments_worker.rb b/app/workers/gitlab/github_import/stage/import_attachments_worker.rb
index f9952f04e99..a5d085a82c0 100644
--- a/app/workers/gitlab/github_import/stage/import_attachments_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_attachments_worker.rb
@@ -11,6 +11,8 @@ module Gitlab
include GithubImport::Queue
include StageMethods
+ resumes_work_when_interrupted!
+
# client - An instance of Gitlab::GithubImport::Client.
# project - An instance of Project.
def import(client, project)
@@ -48,8 +50,8 @@ module Gitlab
def move_to_next_stage(project, waiters = {})
AdvanceStageWorker.perform_async(
project.id,
- waiters,
- :protected_branches
+ waiters.deep_stringify_keys,
+ 'protected_branches'
)
end
end
diff --git a/app/workers/gitlab/github_import/stage/import_base_data_worker.rb b/app/workers/gitlab/github_import/stage/import_base_data_worker.rb
index 94cb3cb6c71..5bbe14b6528 100644
--- a/app/workers/gitlab/github_import/stage/import_base_data_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_base_data_worker.rb
@@ -27,8 +27,6 @@ module Gitlab
klass.new(project, client).execute
end
- project.import_state.refresh_jid_expiration
-
ImportPullRequestsWorker.perform_async(project.id)
end
end
diff --git a/app/workers/gitlab/github_import/stage/import_collaborators_worker.rb b/app/workers/gitlab/github_import/stage/import_collaborators_worker.rb
index 751ca92388a..037b529b866 100644
--- a/app/workers/gitlab/github_import/stage/import_collaborators_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_collaborators_worker.rb
@@ -20,7 +20,6 @@ module Gitlab
info(project.id, message: 'starting importer', importer: 'Importer::CollaboratorsImporter')
waiter = Importer::CollaboratorsImporter.new(project, client).execute
- project.import_state.refresh_jid_expiration
move_to_next_stage(project, { waiter.key => waiter.jobs_remaining })
end
@@ -44,7 +43,7 @@ module Gitlab
def move_to_next_stage(project, waiters = {})
AdvanceStageWorker.perform_async(
- project.id, waiters, :pull_requests_merged_by
+ project.id, waiters.deep_stringify_keys, 'pull_requests_merged_by'
)
end
end
diff --git a/app/workers/gitlab/github_import/stage/import_issue_events_worker.rb b/app/workers/gitlab/github_import/stage/import_issue_events_worker.rb
index c80412d941b..35779d7bfc5 100644
--- a/app/workers/gitlab/github_import/stage/import_issue_events_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_issue_events_worker.rb
@@ -11,6 +11,8 @@ module Gitlab
include GithubImport::Queue
include StageMethods
+ resumes_work_when_interrupted!
+
# client - An instance of Gitlab::GithubImport::Client.
# project - An instance of Project.
def import(client, project)
@@ -30,7 +32,7 @@ module Gitlab
end
def move_to_next_stage(project, waiters = {})
- AdvanceStageWorker.perform_async(project.id, waiters, :notes)
+ AdvanceStageWorker.perform_async(project.id, waiters.deep_stringify_keys, 'notes')
end
end
end
diff --git a/app/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker.rb b/app/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker.rb
index 592b789cc94..58e1f637b6a 100644
--- a/app/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker.rb
@@ -11,6 +11,8 @@ module Gitlab
include GithubImport::Queue
include StageMethods
+ resumes_work_when_interrupted!
+
# client - An instance of Gitlab::GithubImport::Client.
# project - An instance of Project.
def import(client, project)
@@ -20,7 +22,7 @@ module Gitlab
hash[waiter.key] = waiter.jobs_remaining
end
- AdvanceStageWorker.perform_async(project.id, waiters, :issue_events)
+ AdvanceStageWorker.perform_async(project.id, waiters.deep_stringify_keys, 'issue_events')
end
# The importers to run in this stage. Issues can't be imported earlier
diff --git a/app/workers/gitlab/github_import/stage/import_lfs_objects_worker.rb b/app/workers/gitlab/github_import/stage/import_lfs_objects_worker.rb
index e89a850c991..8d7bd98f303 100644
--- a/app/workers/gitlab/github_import/stage/import_lfs_objects_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_lfs_objects_worker.rb
@@ -11,6 +11,11 @@ module Gitlab
include GithubImport::Queue
include StageMethods
+ # Importer::LfsObjectsImporter can resume work when interrupted as
+ # it uses Projects::LfsPointers::LfsObjectDownloadListService which excludes LFS objects that already exist.
+ # https://gitlab.com/gitlab-org/gitlab/-/blob/eabf0800/app/services/projects/lfs_pointers/lfs_object_download_list_service.rb#L69-71
+ resumes_work_when_interrupted!
+
def perform(project_id)
return unless (project = find_project(project_id))
@@ -28,7 +33,7 @@ module Gitlab
AdvanceStageWorker.perform_async(
project.id,
{ waiter.key => waiter.jobs_remaining },
- :finish
+ 'finish'
)
end
end
diff --git a/app/workers/gitlab/github_import/stage/import_notes_worker.rb b/app/workers/gitlab/github_import/stage/import_notes_worker.rb
index c1fdb76d03e..0459545d8e1 100644
--- a/app/workers/gitlab/github_import/stage/import_notes_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_notes_worker.rb
@@ -11,6 +11,8 @@ module Gitlab
include GithubImport::Queue
include StageMethods
+ resumes_work_when_interrupted!
+
# client - An instance of Gitlab::GithubImport::Client.
# project - An instance of Project.
def import(client, project)
@@ -20,7 +22,7 @@ module Gitlab
hash[waiter.key] = waiter.jobs_remaining
end
- AdvanceStageWorker.perform_async(project.id, waiters, :attachments)
+ AdvanceStageWorker.perform_async(project.id, waiters.deep_stringify_keys, 'attachments')
end
def importers(project)
diff --git a/app/workers/gitlab/github_import/stage/import_protected_branches_worker.rb b/app/workers/gitlab/github_import/stage/import_protected_branches_worker.rb
index f8448094c28..e281e965f94 100644
--- a/app/workers/gitlab/github_import/stage/import_protected_branches_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_protected_branches_worker.rb
@@ -19,12 +19,10 @@ module Gitlab
.new(project, client)
.execute
- project.import_state.refresh_jid_expiration
-
AdvanceStageWorker.perform_async(
project.id,
{ waiter.key => waiter.jobs_remaining },
- :lfs_objects
+ 'lfs_objects'
)
end
end
diff --git a/app/workers/gitlab/github_import/stage/import_pull_requests_merged_by_worker.rb b/app/workers/gitlab/github_import/stage/import_pull_requests_merged_by_worker.rb
index 2e7cd28578f..2f543951bf3 100644
--- a/app/workers/gitlab/github_import/stage/import_pull_requests_merged_by_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_pull_requests_merged_by_worker.rb
@@ -11,6 +11,8 @@ module Gitlab
include GithubImport::Queue
include StageMethods
+ resumes_work_when_interrupted!
+
# client - An instance of Gitlab::GithubImport::Client.
# project - An instance of Project.
def import(client, project)
@@ -18,12 +20,10 @@ module Gitlab
.new(project, client)
.execute
- project.import_state.refresh_jid_expiration
-
AdvanceStageWorker.perform_async(
project.id,
{ waiter.key => waiter.jobs_remaining },
- :pull_request_review_requests
+ 'pull_request_review_requests'
)
end
end
diff --git a/app/workers/gitlab/github_import/stage/import_pull_requests_review_requests_worker.rb b/app/workers/gitlab/github_import/stage/import_pull_requests_review_requests_worker.rb
index 2f860349e25..db76545ae87 100644
--- a/app/workers/gitlab/github_import/stage/import_pull_requests_review_requests_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_pull_requests_review_requests_worker.rb
@@ -11,6 +11,8 @@ module Gitlab
include GithubImport::Queue
include StageMethods
+ resumes_work_when_interrupted!
+
# client - An instance of Gitlab::GithubImport::Client.
# project - An instance of Project.
def import(client, project)
@@ -18,12 +20,10 @@ module Gitlab
.new(project, client)
.execute
- project.import_state.refresh_jid_expiration
-
AdvanceStageWorker.perform_async(
project.id,
{ waiter.key => waiter.jobs_remaining },
- :pull_request_reviews
+ 'pull_request_reviews'
)
end
end
diff --git a/app/workers/gitlab/github_import/stage/import_pull_requests_reviews_worker.rb b/app/workers/gitlab/github_import/stage/import_pull_requests_reviews_worker.rb
index 51730033133..31b7c57a524 100644
--- a/app/workers/gitlab/github_import/stage/import_pull_requests_reviews_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_pull_requests_reviews_worker.rb
@@ -11,6 +11,8 @@ module Gitlab
include GithubImport::Queue
include StageMethods
+ resumes_work_when_interrupted!
+
# client - An instance of Gitlab::GithubImport::Client.
# project - An instance of Project.
def import(client, project)
@@ -18,12 +20,10 @@ module Gitlab
.new(project, client)
.execute
- project.import_state.refresh_jid_expiration
-
AdvanceStageWorker.perform_async(
project.id,
{ waiter.key => waiter.jobs_remaining },
- :issues_and_diff_notes
+ 'issues_and_diff_notes'
)
end
end
diff --git a/app/workers/gitlab/github_import/stage/import_pull_requests_worker.rb b/app/workers/gitlab/github_import/stage/import_pull_requests_worker.rb
index 029d38d8b93..c68b95b5111 100644
--- a/app/workers/gitlab/github_import/stage/import_pull_requests_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_pull_requests_worker.rb
@@ -11,6 +11,8 @@ module Gitlab
include GithubImport::Queue
include StageMethods
+ resumes_work_when_interrupted!
+
# client - An instance of Gitlab::GithubImport::Client.
# project - An instance of Project.
def import(client, project)
@@ -25,12 +27,10 @@ module Gitlab
.new(project, client)
.execute
- project.import_state.refresh_jid_expiration
-
AdvanceStageWorker.perform_async(
project.id,
{ waiter.key => waiter.jobs_remaining },
- :collaborators
+ 'collaborators'
)
end
diff --git a/app/workers/gitlab/import/advance_stage.rb b/app/workers/gitlab/import/advance_stage.rb
index 180c08905ff..782439894c0 100644
--- a/app/workers/gitlab/import/advance_stage.rb
+++ b/app/workers/gitlab/import/advance_stage.rb
@@ -19,7 +19,7 @@ module Gitlab
# completed.
# timeout_timer - Time the sidekiq worker was first initiated with the current job_count
# previous_job_count - Number of jobs remaining on last invocation of this worker
- def perform(project_id, waiters, next_stage, timeout_timer = Time.zone.now, previous_job_count = nil)
+ def perform(project_id, waiters, next_stage, timeout_timer = Time.zone.now.to_s, previous_job_count = nil)
import_state_jid = find_import_state_jid(project_id)
# If the import state is nil the project may have been deleted or the import
@@ -45,7 +45,9 @@ module Gitlab
handle_timeout(import_state_jid, next_stage, project_id, new_waiters, new_job_count)
else
- self.class.perform_in(INTERVAL, project_id, new_waiters, next_stage, timeout_timer, previous_job_count)
+ self.class.perform_in(INTERVAL,
+ project_id, new_waiters.deep_stringify_keys, next_stage.to_s, timeout_timer.to_s, previous_job_count
+ )
end
end
diff --git a/app/workers/gitlab/jira_import/stage/import_issues_worker.rb b/app/workers/gitlab/jira_import/stage/import_issues_worker.rb
index 7a5eb6c1e3a..5d890ecfe13 100644
--- a/app/workers/gitlab/jira_import/stage/import_issues_worker.rb
+++ b/app/workers/gitlab/jira_import/stage/import_issues_worker.rb
@@ -9,7 +9,14 @@ module Gitlab
private
def import(project)
- jobs_waiter = Gitlab::JiraImport::IssuesImporter.new(project).execute
+ jira_client = if Feature.enabled?(:increase_jira_import_issues_timeout)
+ project.jira_integration.client(read_timeout: 2.minutes)
+ end
+
+ jobs_waiter = Gitlab::JiraImport::IssuesImporter.new(
+ project,
+ jira_client
+ ).execute
project.latest_jira_import.refresh_jid_expiration
diff --git a/app/workers/hashed_storage/base_worker.rb b/app/workers/hashed_storage/base_worker.rb
deleted file mode 100644
index 372440996d9..00000000000
--- a/app/workers/hashed_storage/base_worker.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-# frozen_string_literal: true
-
-module HashedStorage
- class BaseWorker # rubocop:disable Scalability/IdempotentWorker
- include ExclusiveLeaseGuard
- include WorkerAttributes
-
- feature_category :source_code_management
-
- LEASE_TIMEOUT = 30.seconds.to_i
- LEASE_KEY_SEGMENT = 'project_migrate_hashed_storage_worker'
-
- protected
-
- def lease_key
- # we share the same lease key for both migration and rollback so they don't run simultaneously
- "#{LEASE_KEY_SEGMENT}:#{project_id}"
- end
-
- def lease_timeout
- LEASE_TIMEOUT
- end
- end
-end
diff --git a/app/workers/hashed_storage/migrator_worker.rb b/app/workers/hashed_storage/migrator_worker.rb
deleted file mode 100644
index a7e7a505681..00000000000
--- a/app/workers/hashed_storage/migrator_worker.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-# frozen_string_literal: true
-
-module HashedStorage
- class MigratorWorker # rubocop:disable Scalability/IdempotentWorker
- include ApplicationWorker
-
- data_consistency :always
-
- sidekiq_options retry: 3
-
- queue_namespace :hashed_storage
- feature_category :source_code_management
-
- # @param [Integer] start initial ID of the batch
- # @param [Integer] finish last ID of the batch
- def perform(start, finish); end
- end
-end
diff --git a/app/workers/hashed_storage/project_migrate_worker.rb b/app/workers/hashed_storage/project_migrate_worker.rb
deleted file mode 100644
index e1bf71de179..00000000000
--- a/app/workers/hashed_storage/project_migrate_worker.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-# frozen_string_literal: true
-
-module HashedStorage
- class ProjectMigrateWorker < BaseWorker # rubocop:disable Scalability/IdempotentWorker
- include ApplicationWorker
-
- data_consistency :always
-
- sidekiq_options retry: 3
-
- queue_namespace :hashed_storage
- loggable_arguments 1
-
- attr_reader :project_id
-
- def perform(project_id, old_disk_path = nil); end
- end
-end
diff --git a/app/workers/hashed_storage/project_rollback_worker.rb b/app/workers/hashed_storage/project_rollback_worker.rb
deleted file mode 100644
index af4223ff354..00000000000
--- a/app/workers/hashed_storage/project_rollback_worker.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-# frozen_string_literal: true
-
-module HashedStorage
- class ProjectRollbackWorker < BaseWorker # rubocop:disable Scalability/IdempotentWorker
- include ApplicationWorker
-
- data_consistency :always
-
- sidekiq_options retry: 3
-
- queue_namespace :hashed_storage
- loggable_arguments 1
-
- attr_reader :project_id
-
- def perform(project_id, old_disk_path = nil); end
- end
-end
diff --git a/app/workers/hashed_storage/rollbacker_worker.rb b/app/workers/hashed_storage/rollbacker_worker.rb
deleted file mode 100644
index e659e65a370..00000000000
--- a/app/workers/hashed_storage/rollbacker_worker.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-# frozen_string_literal: true
-
-module HashedStorage
- class RollbackerWorker # rubocop:disable Scalability/IdempotentWorker
- include ApplicationWorker
-
- data_consistency :always
-
- sidekiq_options retry: 3
-
- queue_namespace :hashed_storage
- feature_category :source_code_management
-
- # @param [Integer] start initial ID of the batch
- # @param [Integer] finish last ID of the batch
- def perform(start, finish); end
- end
-end
diff --git a/app/workers/merge_request_cleanup_refs_worker.rb b/app/workers/merge_request_cleanup_refs_worker.rb
index 92dfe8a8cb0..db1a1e96997 100644
--- a/app/workers/merge_request_cleanup_refs_worker.rb
+++ b/app/workers/merge_request_cleanup_refs_worker.rb
@@ -18,8 +18,6 @@ class MergeRequestCleanupRefsWorker
FAILURE_THRESHOLD = 3
def perform_work
- return unless Feature.enabled?(:merge_request_refs_cleanup)
-
unless merge_request
logger.error('No existing merge request to be cleaned up.')
return
diff --git a/app/workers/merge_requests/set_reviewer_reviewed_worker.rb b/app/workers/merge_requests/set_reviewer_reviewed_worker.rb
index 2f15bf3b879..7e8bc60f6e1 100644
--- a/app/workers/merge_requests/set_reviewer_reviewed_worker.rb
+++ b/app/workers/merge_requests/set_reviewer_reviewed_worker.rb
@@ -13,18 +13,23 @@ module MergeRequests
current_user_id = event.data[:current_user_id]
merge_request_id = event.data[:merge_request_id]
current_user = User.find_by_id(current_user_id)
- merge_request = MergeRequest.find_by_id(merge_request_id)
- if !current_user
+ unless current_user
logger.info(structured_payload(message: 'Current user not found.', current_user_id: current_user_id))
- elsif !merge_request
- logger.info(structured_payload(message: 'Merge request not found.', merge_request_id: merge_request_id))
- else
- project = merge_request.source_project
+ return
+ end
+
+ merge_request = MergeRequest.find_by_id(merge_request_id)
- ::MergeRequests::MarkReviewerReviewedService.new(project: project, current_user: current_user)
- .execute(merge_request)
+ unless merge_request
+ logger.info(structured_payload(message: 'Merge request not found.', merge_request_id: merge_request_id))
+ return
end
+
+ project = merge_request.source_project
+
+ ::MergeRequests::UpdateReviewerStateService.new(project: project, current_user: current_user)
+ .execute(merge_request, "reviewed")
end
end
end
diff --git a/app/workers/packages/cleanup_package_registry_worker.rb b/app/workers/packages/cleanup_package_registry_worker.rb
index 5f14102b5a1..5b2d8bacd62 100644
--- a/app/workers/packages/cleanup_package_registry_worker.rb
+++ b/app/workers/packages/cleanup_package_registry_worker.rb
@@ -13,6 +13,7 @@ module Packages
def perform
enqueue_package_file_cleanup_job if Packages::PackageFile.pending_destruction.exists?
enqueue_cleanup_policy_jobs if Packages::Cleanup::Policy.runnable.exists?
+ enqueue_cleanup_stale_npm_metadata_cache_job if Packages::Npm::MetadataCache.pending_destruction.exists?
log_counts
end
@@ -27,6 +28,10 @@ module Packages
Packages::Cleanup::ExecutePolicyWorker.perform_with_capacity
end
+ def enqueue_cleanup_stale_npm_metadata_cache_job
+ Packages::Npm::CleanupStaleMetadataCacheWorker.perform_with_capacity
+ end
+
def log_counts
use_replica_if_available do
pending_destruction_package_files_count = Packages::PackageFile.pending_destruction.count
diff --git a/app/workers/packages/npm/cleanup_stale_metadata_cache_worker.rb b/app/workers/packages/npm/cleanup_stale_metadata_cache_worker.rb
new file mode 100644
index 00000000000..158209c28fd
--- /dev/null
+++ b/app/workers/packages/npm/cleanup_stale_metadata_cache_worker.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+module Packages
+ module Npm
+ class CleanupStaleMetadataCacheWorker
+ include ApplicationWorker
+ include ::Packages::CleanupArtifactWorker
+
+ MAX_CAPACITY = 2
+
+ data_consistency :sticky
+
+ queue_namespace :package_cleanup
+ feature_category :package_registry
+
+ deduplicate :until_executed
+ idempotent!
+
+ def max_running_jobs
+ MAX_CAPACITY
+ end
+
+ private
+
+ def model
+ Packages::Npm::MetadataCache
+ end
+
+ def log_metadata(npm_metadata_cache)
+ log_extra_metadata_on_done(:npm_metadata_cache_id, npm_metadata_cache.id)
+ end
+
+ def log_cleanup_item(npm_metadata_cache)
+ logger.info(
+ structured_payload(
+ npm_metadata_cache_id: npm_metadata_cache.id
+ )
+ )
+ end
+ end
+ end
+end
diff --git a/app/workers/packages/nuget/extraction_worker.rb b/app/workers/packages/nuget/extraction_worker.rb
index 55aca0beb03..33fc98cf95b 100644
--- a/app/workers/packages/nuget/extraction_worker.rb
+++ b/app/workers/packages/nuget/extraction_worker.rb
@@ -18,7 +18,7 @@ module Packages
return unless package_file
- ::Packages::Nuget::UpdatePackageFromMetadataService.new(package_file).execute
+ ::Packages::Nuget::ProcessPackageFileService.new(package_file).execute
rescue StandardError => exception
process_package_file_error(
package_file: package_file,
diff --git a/app/workers/projects/import_export/after_import_merge_requests_worker.rb b/app/workers/projects/import_export/after_import_merge_requests_worker.rb
new file mode 100644
index 00000000000..b40e0ca5f09
--- /dev/null
+++ b/app/workers/projects/import_export/after_import_merge_requests_worker.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Projects
+ module ImportExport
+ class AfterImportMergeRequestsWorker
+ include ApplicationWorker
+
+ idempotent!
+ data_consistency :delayed
+ urgency :low
+ feature_category :importers
+
+ def perform(project_id)
+ project = Project.find_by_id(project_id)
+ return unless project
+
+ project.merge_requests.set_latest_merge_request_diff_ids!
+ end
+ end
+ end
+end
diff --git a/app/workers/remove_expired_group_links_worker.rb b/app/workers/remove_expired_group_links_worker.rb
index f1da5f37945..0bac595f0c4 100644
--- a/app/workers/remove_expired_group_links_worker.rb
+++ b/app/workers/remove_expired_group_links_worker.rb
@@ -11,7 +11,7 @@ class RemoveExpiredGroupLinksWorker # rubocop:disable Scalability/IdempotentWork
def perform
ProjectGroupLink.expired.find_each do |link|
- Projects::GroupLinks::DestroyService.new(link.project, nil).execute(link)
+ Projects::GroupLinks::DestroyService.new(link.project, nil).execute(link, skip_authorization: true)
end
GroupGroupLink.expired.find_in_batches do |link_batch|
diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb
index 5ec9ceaf004..f4a507246ac 100644
--- a/app/workers/repository_fork_worker.rb
+++ b/app/workers/repository_fork_worker.rb
@@ -2,6 +2,7 @@
class RepositoryForkWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ include Gitlab::Utils::StrongMemoize
data_consistency :always
@@ -12,10 +13,8 @@ class RepositoryForkWorker # rubocop:disable Scalability/IdempotentWorker
feature_category :source_code_management
def perform(*args)
- target_project_id = args.shift
- target_project = Project.find(target_project_id)
+ @target_project_id = args.shift
- source_project = target_project.forked_from_project
unless source_project
return target_project.import_state.mark_as_failed(_('Source project cannot be found.'))
end
@@ -25,6 +24,21 @@ class RepositoryForkWorker # rubocop:disable Scalability/IdempotentWorker
private
+ def target_project
+ Project.find(@target_project_id)
+ end
+ strong_memoize_attr :target_project
+
+ def source_project
+ @source_project ||= target_project.forked_from_project
+ end
+
+ def branch
+ return unless target_project.import_data&.data
+
+ target_project.import_data.data['fork_branch']
+ end
+
def fork_repository(target_project, source_project)
return unless start_fork(target_project)
@@ -46,7 +60,7 @@ class RepositoryForkWorker # rubocop:disable Scalability/IdempotentWorker
source_repo = source_project.repository.raw
target_repo = target_project.repository.raw
- ::Gitlab::GitalyClient::RepositoryService.new(target_repo).fork_repository(source_repo)
+ ::Gitlab::GitalyClient::RepositoryService.new(target_repo).fork_repository(source_repo, branch)
rescue GRPC::BadStatus => e
Gitlab::ErrorTracking.track_exception(e, source_project_id: source_project.id, target_project_id: target_project.id)
diff --git a/app/workers/schedule_merge_request_cleanup_refs_worker.rb b/app/workers/schedule_merge_request_cleanup_refs_worker.rb
index ced1f443ea6..2ecc95335e2 100644
--- a/app/workers/schedule_merge_request_cleanup_refs_worker.rb
+++ b/app/workers/schedule_merge_request_cleanup_refs_worker.rb
@@ -12,7 +12,6 @@ class ScheduleMergeRequestCleanupRefsWorker
def perform
return if Gitlab::Database.read_only?
- return unless Feature.enabled?(:merge_request_refs_cleanup)
MergeRequest::CleanupSchedule.stuck_retry!
MergeRequestCleanupRefsWorker.perform_with_capacity
diff --git a/app/workers/tasks_to_be_done/create_worker.rb b/app/workers/tasks_to_be_done/create_worker.rb
deleted file mode 100644
index 91046e3cfed..00000000000
--- a/app/workers/tasks_to_be_done/create_worker.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-# frozen_string_literal: true
-
-module TasksToBeDone
- class CreateWorker
- include ApplicationWorker
-
- data_consistency :always
- idempotent!
- feature_category :onboarding
- urgency :low
- worker_resource_boundary :cpu
-
- def perform(member_task_id, current_user_id, assignee_ids = [])
- # no-op removing
- # https://docs.gitlab.com/ee/development/sidekiq/compatibility_across_updates.html#removing-worker-classes
- end
- end
-end
diff --git a/bin/gitlab-backup-cli b/bin/gitlab-backup-cli
new file mode 100755
index 00000000000..4037684be07
--- /dev/null
+++ b/bin/gitlab-backup-cli
@@ -0,0 +1,14 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+$:.unshift File.expand_path("../../lib", __FILE__)
+
+# We require APP_PATH when the rails environment is required only,
+# this allows for faster CLI execution when rails is not needed
+APP_PATH = File.expand_path('../config/application', __dir__)
+
+require_relative '../config/boot'
+
+require 'gitlab/backup/cli'
+
+Gitlab::Backup::Cli::Runner.start(ARGV)
diff --git a/config/application.rb b/config/application.rb
index 4ee2866dad2..847577f68cb 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -270,8 +270,6 @@ module Gitlab
config.assets.precompile << "application_utilities_dark.css"
config.assets.precompile << "application_dark.css"
- config.assets.precompile << "startup/*.css"
-
config.assets.precompile << "print.css"
config.assets.precompile << "mailer.css"
config.assets.precompile << "mailer_client_specific.css"
diff --git a/config/feature_categories.yml b/config/feature_categories.yml
index 5ac22ca085e..3eaf8b2b34d 100644
--- a/config/feature_categories.yml
+++ b/config/feature_categories.yml
@@ -16,17 +16,16 @@
- api
- api_security
- application_instrumentation
-- application_performance
- attack_emulation
- audit_events
- auto_devops
- backup_restore
-- billing_and_payments
- build
- build_artifacts
- capacity_planning
- cell
- ci-cd_visibility
+- cloud_connector
- cloud_native_installation
- code_quality
- code_review_workflow
@@ -94,6 +93,7 @@
- organization
- package_registry
- pages
+- permissions
- pipeline_composition
- portfolio_management
- product_analytics_data_management
diff --git a/config/feature_flags/development/abuse_report_notes.yml b/config/feature_flags/development/abuse_report_notes.yml
new file mode 100644
index 00000000000..9378b1a1d89
--- /dev/null
+++ b/config/feature_flags/development/abuse_report_notes.yml
@@ -0,0 +1,8 @@
+---
+name: abuse_report_notes
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134730
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/429027
+milestone: '16.6'
+type: development
+group: group::anti-abuse
+default_enabled: false
diff --git a/config/feature_flags/development/access_token_pagination.yml b/config/feature_flags/development/access_token_pagination.yml
index df003ed8891..9cc8cf68e08 100644
--- a/config/feature_flags/development/access_token_pagination.yml
+++ b/config/feature_flags/development/access_token_pagination.yml
@@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91372
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/366534
milestone: '15.2'
type: development
-group: group::authentication and authorization
+group: group::authentication
default_enabled: false
diff --git a/config/feature_flags/development/activity_filter_has_mr.yml b/config/feature_flags/development/activity_filter_has_mr.yml
index 235e8b559b5..b276f6b9b49 100644
--- a/config/feature_flags/development/activity_filter_has_mr.yml
+++ b/config/feature_flags/development/activity_filter_has_mr.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/426104
milestone: '16.5'
type: development
group: group::threat insights
-default_enabled: false
+default_enabled: true
diff --git a/config/feature_flags/development/activity_filter_has_remediations.yml b/config/feature_flags/development/activity_filter_has_remediations.yml
new file mode 100644
index 00000000000..7a0b5f958f3
--- /dev/null
+++ b/config/feature_flags/development/activity_filter_has_remediations.yml
@@ -0,0 +1,8 @@
+---
+name: activity_filter_has_remediations
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/135009
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/429262
+milestone: '16.6'
+type: development
+group: group::threat insights
+default_enabled: false
diff --git a/config/feature_flags/development/admin_group_member.yml b/config/feature_flags/development/admin_group_member.yml
deleted file mode 100644
index c6267dd3fe3..00000000000
--- a/config/feature_flags/development/admin_group_member.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: admin_group_member
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131914
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/426580
-milestone: '16.5'
-type: development
-group: group::authentication and authorization
-default_enabled: false
diff --git a/config/feature_flags/development/ai_assist_api.yml b/config/feature_flags/development/ai_assist_api.yml
deleted file mode 100644
index 9b7da480f62..00000000000
--- a/config/feature_flags/development/ai_assist_api.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: ai_assist_api
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/100500
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/378470
-milestone: '15.6'
-type: development
-group: group::incubation
-default_enabled: false
diff --git a/config/feature_flags/development/ai_self_discover.yml b/config/feature_flags/development/ai_self_discover.yml
deleted file mode 100644
index ef5e2bc8926..00000000000
--- a/config/feature_flags/development/ai_self_discover.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: ai_self_discover
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132267
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/425908
-milestone: '16.4'
-type: development
-group: group::ai framework
-default_enabled: false
diff --git a/config/feature_flags/development/ambiguous_ref_modal.yml b/config/feature_flags/development/ambiguous_ref_modal.yml
new file mode 100644
index 00000000000..c1cc52682e2
--- /dev/null
+++ b/config/feature_flags/development/ambiguous_ref_modal.yml
@@ -0,0 +1,8 @@
+---
+name: ambiguous_ref_modal
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/133093
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/429523
+milestone: '16.6'
+type: development
+group: group::source code
+default_enabled: false
diff --git a/config/feature_flags/development/auto_devops_banner_disabled.yml b/config/feature_flags/development/auto_devops_banner_disabled.yml
index 5e0c037bf23..e2087ad574c 100644
--- a/config/feature_flags/development/auto_devops_banner_disabled.yml
+++ b/config/feature_flags/development/auto_devops_banner_disabled.yml
@@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/14
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/350882
milestone: '10.0'
type: development
-group: group::pipeline execution
+group: group::environments
default_enabled: false
diff --git a/config/feature_flags/development/blob_blame_info.yml b/config/feature_flags/development/blob_blame_info.yml
new file mode 100644
index 00000000000..106ceb60cfe
--- /dev/null
+++ b/config/feature_flags/development/blob_blame_info.yml
@@ -0,0 +1,8 @@
+---
+name: blob_blame_info
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/133798
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/425272
+milestone: '16.5'
+type: development
+group: group::source code
+default_enabled: false
diff --git a/config/feature_flags/development/build_service_proxy.yml b/config/feature_flags/development/build_service_proxy.yml
index 8032a39e959..cefb88b7b24 100644
--- a/config/feature_flags/development/build_service_proxy.yml
+++ b/config/feature_flags/development/build_service_proxy.yml
@@ -1,8 +1,8 @@
---
name: build_service_proxy
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/9723
-rollout_issue_url:
+rollout_issue_url:
milestone: '11.11'
type: development
-group: group::editor
+group: group::ide
default_enabled: false
diff --git a/config/feature_flags/development/bulk_import_deferred_workers.yml b/config/feature_flags/development/bulk_import_deferred_workers.yml
new file mode 100644
index 00000000000..1b6a022099c
--- /dev/null
+++ b/config/feature_flags/development/bulk_import_deferred_workers.yml
@@ -0,0 +1,8 @@
+---
+name: bulk_import_deferred_workers
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/136137
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/431032
+milestone: '16.6'
+type: development
+group: group::import and integrate
+default_enabled: false
diff --git a/config/feature_flags/development/bulk_import_details_page.yml b/config/feature_flags/development/bulk_import_details_page.yml
new file mode 100644
index 00000000000..c8265161233
--- /dev/null
+++ b/config/feature_flags/development/bulk_import_details_page.yml
@@ -0,0 +1,8 @@
+---
+name: bulk_import_details_page
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/135004
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/429109
+milestone: '16.6'
+type: development
+group: group::import and integrate
+default_enabled: true
diff --git a/config/feature_flags/development/bulk_import_idempotent_workers.yml b/config/feature_flags/development/bulk_import_idempotent_workers.yml
deleted file mode 100644
index 83d5b7f65c7..00000000000
--- a/config/feature_flags/development/bulk_import_idempotent_workers.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: bulk_import_idempotent_workers
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132702
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/426480
-milestone: '16.5'
-type: development
-group: group::import and integrate
-default_enabled: false
diff --git a/config/feature_flags/development/by_pass_two_factor_for_current_session.yml b/config/feature_flags/development/by_pass_two_factor_for_current_session.yml
index d60b5bdc234..5842c1ccc61 100644
--- a/config/feature_flags/development/by_pass_two_factor_for_current_session.yml
+++ b/config/feature_flags/development/by_pass_two_factor_for_current_session.yml
@@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/122109
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/416535
milestone: '16.3'
type: development
-group: group::authentication and authorization
+group: group::authentication
default_enabled: false
diff --git a/config/feature_flags/development/ci_catalog_create_metadata.yml b/config/feature_flags/development/ci_catalog_create_metadata.yml
new file mode 100644
index 00000000000..a73f499554d
--- /dev/null
+++ b/config/feature_flags/development/ci_catalog_create_metadata.yml
@@ -0,0 +1,8 @@
+---
+name: ci_catalog_create_metadata
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134148
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/430120
+milestone: '16.6'
+type: development
+group: group::pipeline authoring
+default_enabled: false
diff --git a/config/feature_flags/development/ci_fix_performance_pipelines_json_endpoint.yml b/config/feature_flags/development/ci_fix_performance_pipelines_json_endpoint.yml
deleted file mode 100644
index 069d0349181..00000000000
--- a/config/feature_flags/development/ci_fix_performance_pipelines_json_endpoint.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: ci_fix_performance_pipelines_json_endpoint
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132990
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/427255
-milestone: '16.5'
-type: development
-group: group::pipeline authoring
-default_enabled: false
diff --git a/config/feature_flags/development/ci_job_artifacts_backlog_large_loop_limit.yml b/config/feature_flags/development/ci_job_artifacts_backlog_large_loop_limit.yml
index 1415d9e0db7..395580a4d80 100644
--- a/config/feature_flags/development/ci_job_artifacts_backlog_large_loop_limit.yml
+++ b/config/feature_flags/development/ci_job_artifacts_backlog_large_loop_limit.yml
@@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/76509
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/347151
milestone: '14.10'
type: development
-group: group::pipeline execution
+group: group::pipeline security
default_enabled: false
diff --git a/config/feature_flags/development/ci_require_credit_card_on_free_plan.yml b/config/feature_flags/development/ci_require_credit_card_on_free_plan.yml
index 7e5795de6a0..8816e0ebec4 100644
--- a/config/feature_flags/development/ci_require_credit_card_on_free_plan.yml
+++ b/config/feature_flags/development/ci_require_credit_card_on_free_plan.yml
@@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/61152
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/330104
milestone: '13.12'
type: development
-group: group::fulfillment
+group: group::anti-abuse
default_enabled: false
diff --git a/config/feature_flags/development/ci_require_credit_card_on_trial_plan.yml b/config/feature_flags/development/ci_require_credit_card_on_trial_plan.yml
index 578101a1ba4..402a2b42310 100644
--- a/config/feature_flags/development/ci_require_credit_card_on_trial_plan.yml
+++ b/config/feature_flags/development/ci_require_credit_card_on_trial_plan.yml
@@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/61152
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/330105
milestone: '13.12'
type: development
-group: group::fulfillment
+group: group::anti-abuse
default_enabled: false
diff --git a/config/feature_flags/development/ci_stop_unlock_pipelines.yml b/config/feature_flags/development/ci_stop_unlock_pipelines.yml
new file mode 100644
index 00000000000..a7ca6b73b4e
--- /dev/null
+++ b/config/feature_flags/development/ci_stop_unlock_pipelines.yml
@@ -0,0 +1,8 @@
+---
+name: ci_stop_unlock_pipelines
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134967
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/428408
+milestone: '16.6'
+type: development
+group: group::pipeline security
+default_enabled: false
diff --git a/config/feature_flags/development/ci_unlock_non_successful_pipelines.yml b/config/feature_flags/development/ci_unlock_non_successful_pipelines.yml
new file mode 100644
index 00000000000..4cba44f5de4
--- /dev/null
+++ b/config/feature_flags/development/ci_unlock_non_successful_pipelines.yml
@@ -0,0 +1,8 @@
+---
+name: ci_unlock_non_successful_pipelines
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134967
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/428408
+milestone: '16.5'
+type: development
+group: group::pipeline security
+default_enabled: false
diff --git a/config/feature_flags/development/ci_variable_drawer.yml b/config/feature_flags/development/ci_variable_drawer.yml
deleted file mode 100644
index ad451ab6414..00000000000
--- a/config/feature_flags/development/ci_variable_drawer.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: ci_variable_drawer
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/126197
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/418005
-milestone: '16.3'
-type: development
-group: group::pipeline security
-default_enabled: false
diff --git a/config/feature_flags/development/code_suggestions_for_instance_admin_enabled.yml b/config/feature_flags/development/code_suggestions_for_instance_admin_enabled.yml
index 1a7b2356f55..5df6440bd5b 100644
--- a/config/feature_flags/development/code_suggestions_for_instance_admin_enabled.yml
+++ b/config/feature_flags/development/code_suggestions_for_instance_admin_enabled.yml
@@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/122645
rollout_issue_url:
milestone: '16.1'
type: development
-group: group::application performance
+group: group::cloud connector
default_enabled: false
diff --git a/config/feature_flags/development/code_tasks.yml b/config/feature_flags/development/code_tasks.yml
new file mode 100644
index 00000000000..fec0e8326f3
--- /dev/null
+++ b/config/feature_flags/development/code_tasks.yml
@@ -0,0 +1,8 @@
+---
+name: code_tasks
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/135717
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/430962
+milestone: '16.6'
+type: development
+group: group::code creation
+default_enabled: false
diff --git a/config/feature_flags/development/compare_project_authorization_linear_cte.yml b/config/feature_flags/development/compare_project_authorization_linear_cte.yml
index 7032e6f64f4..b992cd3897a 100644
--- a/config/feature_flags/development/compare_project_authorization_linear_cte.yml
+++ b/config/feature_flags/development/compare_project_authorization_linear_cte.yml
@@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/122886
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/414310
milestone: '16.1'
type: development
-group: group::authentication and authorization
+group: group::authentication
default_enabled: false
diff --git a/config/feature_flags/development/composer_use_ssh_source_urls.yml b/config/feature_flags/development/composer_use_ssh_source_urls.yml
deleted file mode 100644
index d74dcdf9806..00000000000
--- a/config/feature_flags/development/composer_use_ssh_source_urls.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: composer_use_ssh_source_urls
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/119739
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/422171
-milestone: '16.4'
-type: development
-group: group::package registry
-default_enabled: true
diff --git a/config/feature_flags/development/container_registry_protected_containers.yml b/config/feature_flags/development/container_registry_protected_containers.yml
new file mode 100644
index 00000000000..94305b7251b
--- /dev/null
+++ b/config/feature_flags/development/container_registry_protected_containers.yml
@@ -0,0 +1,8 @@
+---
+name: container_registry_protected_containers
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/133527
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/429074
+milestone: '16.6'
+type: development
+group: group::container registry
+default_enabled: false \ No newline at end of file
diff --git a/config/feature_flags/development/coop_header.yml b/config/feature_flags/development/coop_header.yml
deleted file mode 100644
index 9166f4c6819..00000000000
--- a/config/feature_flags/development/coop_header.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: coop_header
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131571
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/425701
-milestone: '16.5'
-type: development
-group: group::authentication and authorization
-default_enabled: false
diff --git a/config/feature_flags/development/create_deployment_only_for_processable_jobs.yml b/config/feature_flags/development/create_deployment_only_for_processable_jobs.yml
deleted file mode 100644
index f721dd8265c..00000000000
--- a/config/feature_flags/development/create_deployment_only_for_processable_jobs.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: create_deployment_only_for_processable_jobs
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132835
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/427062
-milestone: '16.5'
-type: development
-group: group::environments
-default_enabled: false
diff --git a/config/feature_flags/development/create_embeddings_with_vertex_ai.yml b/config/feature_flags/development/create_embeddings_with_vertex_ai.yml
deleted file mode 100644
index 327961d971e..00000000000
--- a/config/feature_flags/development/create_embeddings_with_vertex_ai.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: create_embeddings_with_vertex_ai
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/129930
-rollout_issue_url:
-milestone: '16.4'
-type: development
-group: group::duo chat
-default_enabled: false
diff --git a/config/feature_flags/development/create_project_subscription_graphql_endpoint.yml b/config/feature_flags/development/create_project_subscription_graphql_endpoint.yml
new file mode 100644
index 00000000000..a39664a875d
--- /dev/null
+++ b/config/feature_flags/development/create_project_subscription_graphql_endpoint.yml
@@ -0,0 +1,8 @@
+---
+name: create_project_subscription_graphql_endpoint
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/133308
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/429339
+milestone: '16.6'
+type: development
+group: group::pipeline execution
+default_enabled: false
diff --git a/config/feature_flags/development/custom_roles_in_members_page.yml b/config/feature_flags/development/custom_roles_in_members_page.yml
index cb6bea5ca42..b7b7b2f6093 100644
--- a/config/feature_flags/development/custom_roles_in_members_page.yml
+++ b/config/feature_flags/development/custom_roles_in_members_page.yml
@@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/128491
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/422897
milestone: '16.3'
type: development
-group: group::authentication and authorization
+group: group::authorization
default_enabled: false
diff --git a/config/feature_flags/development/custom_roles_ui_saas.yml b/config/feature_flags/development/custom_roles_ui_saas.yml
index ea4925eb322..6ad2150f597 100644
--- a/config/feature_flags/development/custom_roles_ui_saas.yml
+++ b/config/feature_flags/development/custom_roles_ui_saas.yml
@@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/130089
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/423077
milestone: '16.4'
type: development
-group: group::authentication and authorization
+group: group::authorization
default_enabled: true
diff --git a/config/feature_flags/development/data_transfer_monitoring_mock_data.yml b/config/feature_flags/development/data_transfer_monitoring_mock_data.yml
deleted file mode 100644
index 77a43426e74..00000000000
--- a/config/feature_flags/development/data_transfer_monitoring_mock_data.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: data_transfer_monitoring_mock_data
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/113392
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/397693
-milestone: '15.11'
-type: development
-group: group::source code
-default_enabled: false
diff --git a/config/feature_flags/development/disable_unsafe_regexp.yml b/config/feature_flags/development/disable_unsafe_regexp.yml
index 196b647082e..cb00645444f 100644
--- a/config/feature_flags/development/disable_unsafe_regexp.yml
+++ b/config/feature_flags/development/disable_unsafe_regexp.yml
@@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/79611
rollout_issue_url:
milestone: '14.9'
type: development
-group: group::pipeline execution
+group: group::pipeline authoring
default_enabled: false
diff --git a/config/feature_flags/development/display_cost_factored_storage_size_on_project_pages.yml b/config/feature_flags/development/display_cost_factored_storage_size_on_project_pages.yml
new file mode 100644
index 00000000000..9f47e4bf157
--- /dev/null
+++ b/config/feature_flags/development/display_cost_factored_storage_size_on_project_pages.yml
@@ -0,0 +1,8 @@
+---
+name: display_cost_factored_storage_size_on_project_pages
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/130862
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/428743
+milestone: '16.6'
+type: development
+group: group::utilization
+default_enabled: false
diff --git a/config/feature_flags/development/do_not_run_safety_net_auth_refresh_jobs.yml b/config/feature_flags/development/do_not_run_safety_net_auth_refresh_jobs.yml
index 94784f3facb..89035f5f32b 100644
--- a/config/feature_flags/development/do_not_run_safety_net_auth_refresh_jobs.yml
+++ b/config/feature_flags/development/do_not_run_safety_net_auth_refresh_jobs.yml
@@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/110986
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/390336
milestone: '15.9'
type: development
-group: group::authentication and authorization
+group: group::authentication
default_enabled: false
diff --git a/config/feature_flags/development/explain_code_vertex_ai.yml b/config/feature_flags/development/explain_code_vertex_ai.yml
deleted file mode 100644
index 4eb4d64ed30..00000000000
--- a/config/feature_flags/development/explain_code_vertex_ai.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: explain_code_vertex_ai
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/125292
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/416907
-milestone: '16.2'
-type: development
-group: group::source code
-default_enabled: false
diff --git a/config/feature_flags/development/forti_authenticator.yml b/config/feature_flags/development/forti_authenticator.yml
index 63e780ccc64..fef86fa8bc0 100644
--- a/config/feature_flags/development/forti_authenticator.yml
+++ b/config/feature_flags/development/forti_authenticator.yml
@@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45055
rollout_issue_url:
milestone: '13.5'
type: development
-group: group::authentication and authorization
+group: group::authentication
default_enabled: false
diff --git a/config/feature_flags/development/forti_token_cloud.yml b/config/feature_flags/development/forti_token_cloud.yml
index 5bf350c9b33..e3f9941e92f 100644
--- a/config/feature_flags/development/forti_token_cloud.yml
+++ b/config/feature_flags/development/forti_token_cloud.yml
@@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49089
rollout_issue_url:
milestone: '13.7'
type: development
-group: group::authentication and authorization
+group: group::authentication
default_enabled: false
diff --git a/config/feature_flags/development/frecent_namespaces_suggestions.yml b/config/feature_flags/development/frecent_namespaces_suggestions.yml
new file mode 100644
index 00000000000..1fe0f0694e8
--- /dev/null
+++ b/config/feature_flags/development/frecent_namespaces_suggestions.yml
@@ -0,0 +1,8 @@
+---
+name: frecent_namespaces_suggestions
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132128
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/428362
+milestone: '16.6'
+type: development
+group: group::foundations
+default_enabled: false
diff --git a/config/feature_flags/development/github_importer_raise_max_interruptions.yml b/config/feature_flags/development/github_importer_raise_max_interruptions.yml
new file mode 100644
index 00000000000..3cbcc10865f
--- /dev/null
+++ b/config/feature_flags/development/github_importer_raise_max_interruptions.yml
@@ -0,0 +1,8 @@
+---
+name: github_importer_raise_max_interruptions
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134949
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/429306
+milestone: '16.6'
+type: development
+group: group::import and integrate
+default_enabled: false
diff --git a/config/feature_flags/development/global_ci_catalog.yml b/config/feature_flags/development/global_ci_catalog.yml
new file mode 100644
index 00000000000..cf61406112b
--- /dev/null
+++ b/config/feature_flags/development/global_ci_catalog.yml
@@ -0,0 +1,8 @@
+---
+name: global_ci_catalog
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/133885
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/427940
+milestone: '16.6'
+type: development
+group: group::pipeline authoring
+default_enabled: false
diff --git a/config/feature_flags/development/global_dependency_scanning_on_advisory_ingestion.yml b/config/feature_flags/development/global_dependency_scanning_on_advisory_ingestion.yml
new file mode 100644
index 00000000000..ca33869fd2a
--- /dev/null
+++ b/config/feature_flags/development/global_dependency_scanning_on_advisory_ingestion.yml
@@ -0,0 +1,8 @@
+---
+name: global_dependency_scanning_on_advisory_ingestion
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/135581
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/427424
+milestone: '16.6'
+type: development
+group: group::composition analysis
+default_enabled: false
diff --git a/config/feature_flags/development/group_multi_select_tokens.yml b/config/feature_flags/development/group_multi_select_tokens.yml
new file mode 100644
index 00000000000..485a665d1eb
--- /dev/null
+++ b/config/feature_flags/development/group_multi_select_tokens.yml
@@ -0,0 +1,8 @@
+---
+name: group_multi_select_tokens
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/133725
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/428741
+milestone: '16.6'
+type: development
+group: group::project management
+default_enabled: false
diff --git a/config/feature_flags/development/import_fallback_to_db_empty_cache.yml b/config/feature_flags/development/import_fallback_to_db_empty_cache.yml
new file mode 100644
index 00000000000..d97adc841fc
--- /dev/null
+++ b/config/feature_flags/development/import_fallback_to_db_empty_cache.yml
@@ -0,0 +1,8 @@
+---
+name: import_fallback_to_db_empty_cache
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/133914
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/428700
+milestone: '16.6'
+type: development
+group: group::import and integrate
+default_enabled: false
diff --git a/config/feature_flags/development/increase_jira_import_issues_timeout.yml b/config/feature_flags/development/increase_jira_import_issues_timeout.yml
new file mode 100644
index 00000000000..709522b098b
--- /dev/null
+++ b/config/feature_flags/development/increase_jira_import_issues_timeout.yml
@@ -0,0 +1,8 @@
+---
+name: increase_jira_import_issues_timeout
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/135050
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/429293
+milestone: '16.6'
+type: development
+group: group::project management
+default_enabled: true
diff --git a/config/feature_flags/development/inherit_higher_access_levels_no_cross_join.yml b/config/feature_flags/development/inherit_higher_access_levels_no_cross_join.yml
index b4cef5219ba..299a43ccb9e 100644
--- a/config/feature_flags/development/inherit_higher_access_levels_no_cross_join.yml
+++ b/config/feature_flags/development/inherit_higher_access_levels_no_cross_join.yml
@@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132947
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/427238
milestone: '16.5'
type: development
-group: group::authentication and authorization
+group: group::authentication
default_enabled: true
diff --git a/config/feature_flags/development/invert_omniauth_args_merging.yml b/config/feature_flags/development/invert_omniauth_args_merging.yml
new file mode 100644
index 00000000000..1a5d0a08541
--- /dev/null
+++ b/config/feature_flags/development/invert_omniauth_args_merging.yml
@@ -0,0 +1,8 @@
+---
+name: invert_omniauth_args_merging
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/135770
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/430348
+milestone: '16.6'
+type: development
+group: group::authentication
+default_enabled: false
diff --git a/config/feature_flags/development/issue_assignees_widget.yml b/config/feature_flags/development/issue_assignees_widget.yml
deleted file mode 100644
index 5163a345a3b..00000000000
--- a/config/feature_flags/development/issue_assignees_widget.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: issue_assignees_widget
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/59620/
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/328185
-milestone: '13.11'
-type: development
-group: group::project management
-default_enabled: true
diff --git a/config/feature_flags/development/jira_dvcs_end_of_life_amnesty.yml b/config/feature_flags/development/jira_dvcs_end_of_life_amnesty.yml
deleted file mode 100644
index dd72f9e13dd..00000000000
--- a/config/feature_flags/development/jira_dvcs_end_of_life_amnesty.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: jira_dvcs_end_of_life_amnesty
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/118126
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/408148
-milestone: '16.0'
-type: development
-group: group::import and integrate
-default_enabled: false
diff --git a/config/feature_flags/development/jwt_auth_space_delimited_scopes.yml b/config/feature_flags/development/jwt_auth_space_delimited_scopes.yml
deleted file mode 100644
index cddeb60c4ab..00000000000
--- a/config/feature_flags/development/jwt_auth_space_delimited_scopes.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: jwt_auth_space_delimited_scopes
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/133841
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/427881
-milestone: '16.5'
-type: development
-group: group::container registry
-default_enabled: false
diff --git a/config/feature_flags/development/k8s_watch_api.yml b/config/feature_flags/development/k8s_watch_api.yml
new file mode 100644
index 00000000000..c8aa176538e
--- /dev/null
+++ b/config/feature_flags/development/k8s_watch_api.yml
@@ -0,0 +1,8 @@
+---
+name: k8s_watch_api
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/133734
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/427762
+milestone: '16.5'
+type: development
+group: group::environments
+default_enabled: false
diff --git a/config/feature_flags/development/linear_project_authorization.yml b/config/feature_flags/development/linear_project_authorization.yml
index f3fcb968b8e..e1f639a4a16 100644
--- a/config/feature_flags/development/linear_project_authorization.yml
+++ b/config/feature_flags/development/linear_project_authorization.yml
@@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/117988
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/410459
milestone: '16.0'
type: development
-group: group::authentication and authorization
+group: group::authentication
default_enabled: false
diff --git a/config/feature_flags/development/log_git_streaming_audit_events.yml b/config/feature_flags/development/log_git_streaming_audit_events.yml
new file mode 100644
index 00000000000..1c3cace7aa5
--- /dev/null
+++ b/config/feature_flags/development/log_git_streaming_audit_events.yml
@@ -0,0 +1,8 @@
+---
+name: log_git_streaming_audit_events
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/123486
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/415138
+milestone: "16.5"
+type: development
+group: group::source code
+default_enabled: false
diff --git a/config/feature_flags/development/manage_project_access_tokens.yml b/config/feature_flags/development/manage_project_access_tokens.yml
index 6a91e1fc140..a6cf2cf4f9f 100644
--- a/config/feature_flags/development/manage_project_access_tokens.yml
+++ b/config/feature_flags/development/manage_project_access_tokens.yml
@@ -1,8 +1,8 @@
---
name: manage_project_access_tokens
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132342
-rollout_issue_url:
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/430353
milestone: '16.5'
type: development
-group: group::authentication and authorization
+group: group::authorization
default_enabled: false
diff --git a/config/feature_flags/development/mastodon_social_ui.yml b/config/feature_flags/development/mastodon_social_ui.yml
new file mode 100644
index 00000000000..5e04d8176e4
--- /dev/null
+++ b/config/feature_flags/development/mastodon_social_ui.yml
@@ -0,0 +1,8 @@
+---
+name: mastodon_social_ui
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132892
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/428163
+milestone: '16.5'
+type: development
+group: group::tenant scale
+default_enabled: false
diff --git a/config/feature_flags/development/member_expiring_email_notification.yml b/config/feature_flags/development/member_expiring_email_notification.yml
index 36a15c27daf..c37f9667b58 100644
--- a/config/feature_flags/development/member_expiring_email_notification.yml
+++ b/config/feature_flags/development/member_expiring_email_notification.yml
@@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/124577
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/416581
milestone: '16.3'
type: development
-group: group::authentication and authorization
+group: group::authentication
default_enabled: true
diff --git a/config/feature_flags/development/merge_request_refs_cleanup.yml b/config/feature_flags/development/merge_request_refs_cleanup.yml
deleted file mode 100644
index e306dd89c93..00000000000
--- a/config/feature_flags/development/merge_request_refs_cleanup.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: merge_request_refs_cleanup
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/51558
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/336070
-milestone: '13.8'
-type: development
-group: group::code review
-default_enabled: true
diff --git a/config/feature_flags/development/mr_request_changes.yml b/config/feature_flags/development/mr_request_changes.yml
new file mode 100644
index 00000000000..f55e410190a
--- /dev/null
+++ b/config/feature_flags/development/mr_request_changes.yml
@@ -0,0 +1,8 @@
+---
+name: mr_request_changes
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134766
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/429557
+milestone: '16.6'
+type: development
+group: group::code review
+default_enabled: false
diff --git a/config/feature_flags/development/new_pipeline_graph.yml b/config/feature_flags/development/new_pipeline_graph.yml
new file mode 100644
index 00000000000..d3570980f63
--- /dev/null
+++ b/config/feature_flags/development/new_pipeline_graph.yml
@@ -0,0 +1,8 @@
+---
+name: new_pipeline_graph
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132462
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/426902
+milestone: '16.5'
+type: development
+group: group::ux paper cuts
+default_enabled: false
diff --git a/config/feature_flags/development/nuget_duplicates_option.yml b/config/feature_flags/development/nuget_duplicates_option.yml
deleted file mode 100644
index 5b386063f26..00000000000
--- a/config/feature_flags/development/nuget_duplicates_option.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: nuget_duplicates_option
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/123783
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/419078
-milestone: '16.3'
-type: development
-group: group::package registry
-default_enabled: false
diff --git a/config/feature_flags/development/observability_metrics.yml b/config/feature_flags/development/observability_metrics.yml
new file mode 100644
index 00000000000..c8bb1d3c2e2
--- /dev/null
+++ b/config/feature_flags/development/observability_metrics.yml
@@ -0,0 +1,8 @@
+---
+name: observability_metrics
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134393
+rollout_issue_url: https://gitlab.com/gitlab-org/opstrace/opstrace/-/issues/2444
+milestone: '16.6'
+type: development
+group: group::observability
+default_enabled: false
diff --git a/config/feature_flags/development/oidc_issuer_url.yml b/config/feature_flags/development/oidc_issuer_url.yml
new file mode 100644
index 00000000000..e919d1095d1
--- /dev/null
+++ b/config/feature_flags/development/oidc_issuer_url.yml
@@ -0,0 +1,8 @@
+---
+name: oidc_issuer_url
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/135049
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/429855
+milestone: '16.6'
+type: development
+group: group::pipeline security
+default_enabled: false
diff --git a/config/feature_flags/development/only_highlight_discussions_requested.yml b/config/feature_flags/development/only_highlight_discussions_requested.yml
new file mode 100644
index 00000000000..8dfb93c33e0
--- /dev/null
+++ b/config/feature_flags/development/only_highlight_discussions_requested.yml
@@ -0,0 +1,8 @@
+---
+name: only_highlight_discussions_requested
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/135096
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/429489
+milestone: '16.6'
+type: development
+group: group::code review
+default_enabled: false
diff --git a/config/feature_flags/development/openai_experimentation.yml b/config/feature_flags/development/openai_experimentation.yml
deleted file mode 100644
index 054e6442445..00000000000
--- a/config/feature_flags/development/openai_experimentation.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: openai_experimentation
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/116364
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/403855
-milestone: '15.11'
-type: development
-group: group::ai framework
-default_enabled: false
diff --git a/config/feature_flags/development/order_builds_for_group_runner.yml b/config/feature_flags/development/order_builds_for_group_runner.yml
index 50f9a301ad6..3ca461e8033 100644
--- a/config/feature_flags/development/order_builds_for_group_runner.yml
+++ b/config/feature_flags/development/order_builds_for_group_runner.yml
@@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/94815
rollout_issue_url:
milestone: '15.4'
type: development
-group: group::pipeline execution
+group: group::runner
default_enabled: true
diff --git a/config/feature_flags/development/personal_snippet_reference_filters.yml b/config/feature_flags/development/personal_snippet_reference_filters.yml
index eb97a2caf0d..099378f7da0 100644
--- a/config/feature_flags/development/personal_snippet_reference_filters.yml
+++ b/config/feature_flags/development/personal_snippet_reference_filters.yml
@@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/38571
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/235155
milestone: '13.3'
type: development
-group: group::editor
+group: group::source code
default_enabled: false
diff --git a/config/feature_flags/development/preserve_unchanged_markdown.yml b/config/feature_flags/development/preserve_unchanged_markdown.yml
index 55e5d913389..8d0dc879bdb 100644
--- a/config/feature_flags/development/preserve_unchanged_markdown.yml
+++ b/config/feature_flags/development/preserve_unchanged_markdown.yml
@@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/86060
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/360713
milestone: '15.0'
type: development
-group: group::editor
+group: group::knowledge
default_enabled: false
diff --git a/config/feature_flags/development/print_wiki.yml b/config/feature_flags/development/print_wiki.yml
deleted file mode 100644
index 75305425deb..00000000000
--- a/config/feature_flags/development/print_wiki.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: print_wiki
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/125260
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/414691
-milestone: '16.3'
-type: development
-group: group::knowledge
-default_enabled: true
diff --git a/config/feature_flags/development/product_analytics_usage_quota.yml b/config/feature_flags/development/product_analytics_usage_quota.yml
new file mode 100644
index 00000000000..d5807c5b507
--- /dev/null
+++ b/config/feature_flags/development/product_analytics_usage_quota.yml
@@ -0,0 +1,8 @@
+---
+name: product_analytics_usage_quota
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/133781
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/427838
+milestone: '16.6'
+type: development
+group: group::product analytics
+default_enabled: false
diff --git a/config/feature_flags/development/project_overwrite_service_tracking.yml b/config/feature_flags/development/project_overwrite_service_tracking.yml
index 1a0c4fed4cd..c2e2349e066 100644
--- a/config/feature_flags/development/project_overwrite_service_tracking.yml
+++ b/config/feature_flags/development/project_overwrite_service_tracking.yml
@@ -4,5 +4,5 @@ introduced_by_url:
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/350889
milestone: '14.1'
type: development
-group: group::pipeline execution
+group: group::import and integrate
default_enabled: false
diff --git a/config/feature_flags/development/project_tool_filter_with_scanner_name.yml b/config/feature_flags/development/project_tool_filter_with_scanner_name.yml
new file mode 100644
index 00000000000..c778edeba52
--- /dev/null
+++ b/config/feature_flags/development/project_tool_filter_with_scanner_name.yml
@@ -0,0 +1,8 @@
+---
+name: project_tool_filter_with_scanner_name
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131310
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/424509
+milestone: '16.5'
+type: development
+group: group::threat insights
+default_enabled: false
diff --git a/config/feature_flags/development/rate_limit_oauth_api.yml b/config/feature_flags/development/rate_limit_oauth_api.yml
index 67b333420a7..9c5a44fa6c4 100644
--- a/config/feature_flags/development/rate_limit_oauth_api.yml
+++ b/config/feature_flags/development/rate_limit_oauth_api.yml
@@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/133109
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/427874
milestone: '16.5'
type: development
-group: group::authentication and authorization
+group: group::authentication
default_enabled: false
diff --git a/config/feature_flags/development/reduce_duplicate_job_key_ttl.yml b/config/feature_flags/development/reduce_duplicate_job_key_ttl.yml
new file mode 100644
index 00000000000..a338bc93753
--- /dev/null
+++ b/config/feature_flags/development/reduce_duplicate_job_key_ttl.yml
@@ -0,0 +1,8 @@
+---
+name: reduce_duplicate_job_key_ttl
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/135910
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/430345
+milestone: '16.6'
+type: development
+group: group::scalability
+default_enabled: false
diff --git a/config/feature_flags/development/reduced_build_attributes_list_for_rules.yml b/config/feature_flags/development/reduced_build_attributes_list_for_rules.yml
deleted file mode 100644
index 85170fb02ba..00000000000
--- a/config/feature_flags/development/reduced_build_attributes_list_for_rules.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: reduced_build_attributes_list_for_rules
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132654
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/426259
-milestone: '16.5'
-type: development
-group: group::pipeline execution
-default_enabled: false
diff --git a/config/feature_flags/development/reject_unsigned_commits_by_gitlab.yml b/config/feature_flags/development/reject_unsigned_commits_by_gitlab.yml
index 93c0026d59d..d44e1e22902 100644
--- a/config/feature_flags/development/reject_unsigned_commits_by_gitlab.yml
+++ b/config/feature_flags/development/reject_unsigned_commits_by_gitlab.yml
@@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/58453
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/326775
milestone: '13.11'
type: development
-group: group::editor
+group: group::ide
default_enabled: true
diff --git a/config/feature_flags/development/remove_mr_blocking_constraints.yml b/config/feature_flags/development/remove_mr_blocking_constraints.yml
new file mode 100644
index 00000000000..df4631c711d
--- /dev/null
+++ b/config/feature_flags/development/remove_mr_blocking_constraints.yml
@@ -0,0 +1,8 @@
+---
+name: remove_mr_blocking_constraints
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/133897
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/429428
+milestone: '16.6'
+type: development
+group: group::code review
+default_enabled: false
diff --git a/config/feature_flags/development/replicate_object_pool_on_move.yml b/config/feature_flags/development/replicate_object_pool_on_move.yml
index 8f34969a02d..e413c8ee56c 100644
--- a/config/feature_flags/development/replicate_object_pool_on_move.yml
+++ b/config/feature_flags/development/replicate_object_pool_on_move.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/420720
milestone: '16.3'
type: development
group: group::source code
-default_enabled: false
+default_enabled: true
diff --git a/config/feature_flags/development/restrict_ci_job_token_for_public_and_internal_projects.yml b/config/feature_flags/development/restrict_ci_job_token_for_public_and_internal_projects.yml
new file mode 100644
index 00000000000..31216be0a5c
--- /dev/null
+++ b/config/feature_flags/development/restrict_ci_job_token_for_public_and_internal_projects.yml
@@ -0,0 +1,8 @@
+---
+name: restrict_ci_job_token_for_public_and_internal_projects
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/135263
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/417172
+milestone: '16.6'
+type: development
+group: group::pipeline security
+default_enabled: true
diff --git a/config/feature_flags/development/restrict_pipeline_cancellation_by_role.yml b/config/feature_flags/development/restrict_pipeline_cancellation_by_role.yml
new file mode 100644
index 00000000000..0ef8a5d38db
--- /dev/null
+++ b/config/feature_flags/development/restrict_pipeline_cancellation_by_role.yml
@@ -0,0 +1,8 @@
+---
+name: restrict_pipeline_cancellation_by_role
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/135047
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/429699
+milestone: '16.6'
+type: development
+group: group::pipeline execution
+default_enabled: false
diff --git a/config/feature_flags/development/restyle_login_page.yml b/config/feature_flags/development/restyle_login_page.yml
index bfe99590e6e..90b56e64c48 100644
--- a/config/feature_flags/development/restyle_login_page.yml
+++ b/config/feature_flags/development/restyle_login_page.yml
@@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91673
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/368387
milestone: '15.2'
type: development
-group: group::authentication and authorization
+group: group::authentication
default_enabled: true
diff --git a/config/feature_flags/development/rugged_commit_is_ancestor.yml b/config/feature_flags/development/rugged_commit_is_ancestor.yml
deleted file mode 100644
index 470b0b60bfe..00000000000
--- a/config/feature_flags/development/rugged_commit_is_ancestor.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: rugged_commit_is_ancestor
-introduced_by_url:
-rollout_issue_url:
-milestone:
-type: development
-group: group::gitaly
-default_enabled: false
diff --git a/config/feature_flags/development/rugged_commit_tree_entry.yml b/config/feature_flags/development/rugged_commit_tree_entry.yml
deleted file mode 100644
index c0ba656f7a6..00000000000
--- a/config/feature_flags/development/rugged_commit_tree_entry.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: rugged_commit_tree_entry
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/25896
-rollout_issue_url:
-milestone: '11.9'
-type: development
-group: group::gitaly
-default_enabled: false
diff --git a/config/feature_flags/development/rugged_find_commit.yml b/config/feature_flags/development/rugged_find_commit.yml
deleted file mode 100644
index e1f9de24abd..00000000000
--- a/config/feature_flags/development/rugged_find_commit.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: rugged_find_commit
-introduced_by_url:
-rollout_issue_url:
-milestone:
-type: development
-group: group::gitaly
-default_enabled: false
diff --git a/config/feature_flags/development/rugged_list_commits_by_oid.yml b/config/feature_flags/development/rugged_list_commits_by_oid.yml
deleted file mode 100644
index e41c717a5fe..00000000000
--- a/config/feature_flags/development/rugged_list_commits_by_oid.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: rugged_list_commits_by_oid
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/27441
-rollout_issue_url:
-milestone: '11.10'
-type: development
-group: group::gitaly
-default_enabled: false
diff --git a/config/feature_flags/development/rugged_tree_entries.yml b/config/feature_flags/development/rugged_tree_entries.yml
deleted file mode 100644
index b6ac660f291..00000000000
--- a/config/feature_flags/development/rugged_tree_entries.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: rugged_tree_entries
-introduced_by_url:
-rollout_issue_url:
-milestone:
-type: development
-group: group::gitaly
-default_enabled: false
diff --git a/config/feature_flags/development/rugged_tree_entry.yml b/config/feature_flags/development/rugged_tree_entry.yml
deleted file mode 100644
index 976cc4573f6..00000000000
--- a/config/feature_flags/development/rugged_tree_entry.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: rugged_tree_entry
-introduced_by_url:
-rollout_issue_url:
-milestone:
-type: development
-group: group::gitaly
-default_enabled: false
diff --git a/config/feature_flags/development/runners_dashboard.yml b/config/feature_flags/development/runners_dashboard.yml
deleted file mode 100644
index dd773c5e337..00000000000
--- a/config/feature_flags/development/runners_dashboard.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: runners_dashboard
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/125301
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/417002
-milestone: '16.2'
-type: development
-group: group::runner
-default_enabled: false
diff --git a/config/feature_flags/development/saved_replies.yml b/config/feature_flags/development/saved_replies.yml
deleted file mode 100644
index 0c973292ba0..00000000000
--- a/config/feature_flags/development/saved_replies.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: saved_replies
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/80811
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/352956
-milestone: '14.9'
-type: development
-group: group::project management
-default_enabled: true
diff --git a/config/feature_flags/development/search_issues_hide_archived_projects.yml b/config/feature_flags/development/search_issues_hide_archived_projects.yml
deleted file mode 100644
index 68a6d058e81..00000000000
--- a/config/feature_flags/development/search_issues_hide_archived_projects.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: search_issues_hide_archived_projects
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/124846
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/416483
-milestone: '16.2'
-type: development
-group: group::global search
-default_enabled: false
diff --git a/config/feature_flags/development/search_merge_requests_hide_archived_projects.yml b/config/feature_flags/development/search_merge_requests_hide_archived_projects.yml
deleted file mode 100644
index 565d32b7188..00000000000
--- a/config/feature_flags/development/search_merge_requests_hide_archived_projects.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: search_merge_requests_hide_archived_projects
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/126024
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/417595
-milestone: '16.3'
-type: development
-group: group::global search
-default_enabled: false
diff --git a/config/feature_flags/development/search_notes_hide_archived_projects.yml b/config/feature_flags/development/search_notes_hide_archived_projects.yml
deleted file mode 100644
index c0a922ea08b..00000000000
--- a/config/feature_flags/development/search_notes_hide_archived_projects.yml
+++ /dev/null
@@ -1,9 +0,0 @@
----
-name: search_notes_hide_archived_projects
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/127333
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/419534
-milestone: '16.3'
-type: development
-group: group::global search
-default_enabled: false
-
diff --git a/config/feature_flags/development/service_accounts_crud.yml b/config/feature_flags/development/service_accounts_crud.yml
index 48a9104398d..5c55a910b1a 100644
--- a/config/feature_flags/development/service_accounts_crud.yml
+++ b/config/feature_flags/development/service_accounts_crud.yml
@@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/113884
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/397730
milestone: '15.11'
type: development
-group: group::authentication and authorization
+group: group::authentication
default_enabled: false
diff --git a/config/feature_flags/development/service_desk_new_note_email_native_attachments.yml b/config/feature_flags/development/service_desk_new_note_email_native_attachments.yml
deleted file mode 100644
index 89f0804ad39..00000000000
--- a/config/feature_flags/development/service_desk_new_note_email_native_attachments.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: service_desk_new_note_email_native_attachments
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/107887
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/386860
-milestone: '15.8'
-type: development
-group: group::respond
-default_enabled: true
diff --git a/config/feature_flags/development/set_feature_flag_service.yml b/config/feature_flags/development/set_feature_flag_service.yml
index f25076177d6..4b49af02dfd 100644
--- a/config/feature_flags/development/set_feature_flag_service.yml
+++ b/config/feature_flags/development/set_feature_flag_service.yml
@@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/87028
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/373176
milestone: '15.4'
type: development
-group: group::pipeline execution
+group: group::import and integrate
default_enabled: false
diff --git a/config/feature_flags/development/source_editor_toolbar.yml b/config/feature_flags/development/source_editor_toolbar.yml
index 6fe2dd2d306..6d8a749b945 100644
--- a/config/feature_flags/development/source_editor_toolbar.yml
+++ b/config/feature_flags/development/source_editor_toolbar.yml
@@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82304
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/354748
milestone: '14.9'
type: development
-group: group::editor
+group: group::source code
default_enabled: false
diff --git a/config/feature_flags/development/sourcegraph.yml b/config/feature_flags/development/sourcegraph.yml
index f9aa76f6c7c..0b9e45ef4b4 100644
--- a/config/feature_flags/development/sourcegraph.yml
+++ b/config/feature_flags/development/sourcegraph.yml
@@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/16556
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/292199
milestone: '12.5'
type: development
-group: group::editor
+group: group::source code
default_enabled: true
diff --git a/config/feature_flags/development/specialized_worker_for_group_lock_update_auth_recalculation.yml b/config/feature_flags/development/specialized_worker_for_group_lock_update_auth_recalculation.yml
index aa8e243e89e..0ddaed98be5 100644
--- a/config/feature_flags/development/specialized_worker_for_group_lock_update_auth_recalculation.yml
+++ b/config/feature_flags/development/specialized_worker_for_group_lock_update_auth_recalculation.yml
@@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66525
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/336592
milestone: '14.2'
type: development
-group: group::authentication and authorization
+group: group::authentication
default_enabled: false
diff --git a/config/feature_flags/development/summarize_notes_with_anthropic.yml b/config/feature_flags/development/summarize_notes_with_anthropic.yml
new file mode 100644
index 00000000000..d9e91748d5a
--- /dev/null
+++ b/config/feature_flags/development/summarize_notes_with_anthropic.yml
@@ -0,0 +1,8 @@
+---
+name: summarize_notes_with_anthropic
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134731
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/work_items/430196
+milestone: '16.6'
+type: development
+group: group::duo chat
+default_enabled: false
diff --git a/config/feature_flags/development/super_sidebar_logged_out.yml b/config/feature_flags/development/super_sidebar_logged_out.yml
deleted file mode 100644
index 8deeb63b537..00000000000
--- a/config/feature_flags/development/super_sidebar_logged_out.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: super_sidebar_logged_out
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/127756
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/419936
-milestone: '16.3'
-type: development
-group: group::foundations
-default_enabled: false
diff --git a/config/feature_flags/development/super_sidebar_nav_enrolled.yml b/config/feature_flags/development/super_sidebar_nav_enrolled.yml
deleted file mode 100644
index 14b2e9df39f..00000000000
--- a/config/feature_flags/development/super_sidebar_nav_enrolled.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: super_sidebar_nav_enrolled
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/119506
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/410121
-milestone: '16.0'
-type: development
-group: group::foundations
-default_enabled: true
diff --git a/config/feature_flags/development/support_group_level_merge_checks_setting.yml b/config/feature_flags/development/support_group_level_merge_checks_setting.yml
index 282fa289c91..4537c5ff5b7 100644
--- a/config/feature_flags/development/support_group_level_merge_checks_setting.yml
+++ b/config/feature_flags/development/support_group_level_merge_checks_setting.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/377723
milestone: '15.8'
type: development
group: group::code review
-default_enabled: false
+default_enabled: true
diff --git a/config/feature_flags/development/two_factor_for_cli.yml b/config/feature_flags/development/two_factor_for_cli.yml
index 341f06d9ffa..0a2430146a6 100644
--- a/config/feature_flags/development/two_factor_for_cli.yml
+++ b/config/feature_flags/development/two_factor_for_cli.yml
@@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39703
rollout_issue_url:
milestone: '13.5'
type: development
-group: group::authentication and authorization
+group: group::authentication
default_enabled: false
diff --git a/config/feature_flags/development/unbatch_graphql_queries.yml b/config/feature_flags/development/unbatch_graphql_queries.yml
deleted file mode 100644
index 8a78a46c109..00000000000
--- a/config/feature_flags/development/unbatch_graphql_queries.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: unbatch_graphql_queries
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/117407
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/406765
-milestone: '16.0'
-type: development
-group: group::project management
-default_enabled: true
diff --git a/config/feature_flags/development/use_embeddings_with_vertex.yml b/config/feature_flags/development/use_embeddings_with_vertex.yml
deleted file mode 100644
index 1f37539b4ff..00000000000
--- a/config/feature_flags/development/use_embeddings_with_vertex.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: use_embeddings_with_vertex
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/130421
-rollout_issue_url:
-milestone: '16.5'
-type: development
-group: group::duo chat
-default_enabled: false
diff --git a/config/feature_flags/development/use_gitlab_http_v2.yml b/config/feature_flags/development/use_gitlab_http_v2.yml
index 92a3cdddbb9..8a840c48a4b 100644
--- a/config/feature_flags/development/use_gitlab_http_v2.yml
+++ b/config/feature_flags/development/use_gitlab_http_v2.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/426549
milestone: '16.5'
type: development
group: group::pipeline authoring
-default_enabled: false
+default_enabled: true
diff --git a/config/feature_flags/development/use_new_rule_finalize_approach.yml b/config/feature_flags/development/use_new_rule_finalize_approach.yml
new file mode 100644
index 00000000000..32de525624a
--- /dev/null
+++ b/config/feature_flags/development/use_new_rule_finalize_approach.yml
@@ -0,0 +1,8 @@
+---
+name: use_new_rule_finalize_approach
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/133633
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/427612
+milestone: '16.6'
+type: development
+group: group::code review
+default_enabled: false
diff --git a/config/feature_flags/development/use_pipeline_wizard_for_pages.yml b/config/feature_flags/development/use_pipeline_wizard_for_pages.yml
deleted file mode 100644
index 2de1b952f95..00000000000
--- a/config/feature_flags/development/use_pipeline_wizard_for_pages.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: use_pipeline_wizard_for_pages
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/78276
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/349095
-milestone: '15.4'
-type: development
-group: group::incubation
-default_enabled: true
diff --git a/config/feature_flags/development/use_primary_and_secondary_stores_for_action_cable.yml b/config/feature_flags/development/use_primary_and_secondary_stores_for_action_cable.yml
deleted file mode 100644
index 50ffddd2c0c..00000000000
--- a/config/feature_flags/development/use_primary_and_secondary_stores_for_action_cable.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: use_primary_and_secondary_stores_for_action_cable
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/126451
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/423216
-milestone: '16.4'
-type: development
-group: group::scalability
-default_enabled: false
diff --git a/config/feature_flags/development/use_primary_and_secondary_stores_for_shared_state.yml b/config/feature_flags/development/use_primary_and_secondary_stores_for_shared_state.yml
new file mode 100644
index 00000000000..3e22d84d192
--- /dev/null
+++ b/config/feature_flags/development/use_primary_and_secondary_stores_for_shared_state.yml
@@ -0,0 +1,8 @@
+---
+name: use_primary_and_secondary_stores_for_shared_state
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134483
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/429884
+milestone: '16.6'
+type: development
+group: group::scalability
+default_enabled: false
diff --git a/config/feature_flags/development/use_primary_store_as_default_for_action_cable.yml b/config/feature_flags/development/use_primary_store_as_default_for_action_cable.yml
deleted file mode 100644
index d5606516820..00000000000
--- a/config/feature_flags/development/use_primary_store_as_default_for_action_cable.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: use_primary_store_as_default_for_action_cable
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/126451
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/423216
-milestone: '16.4'
-type: development
-group: group::scalability
-default_enabled: false
diff --git a/config/feature_flags/development/use_primary_store_as_default_for_shared_state.yml b/config/feature_flags/development/use_primary_store_as_default_for_shared_state.yml
new file mode 100644
index 00000000000..4c309144342
--- /dev/null
+++ b/config/feature_flags/development/use_primary_store_as_default_for_shared_state.yml
@@ -0,0 +1,8 @@
+---
+name: use_primary_store_as_default_for_shared_state
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134483
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/429884
+milestone: '16.6'
+type: development
+group: group::scalability
+default_enabled: false
diff --git a/config/feature_flags/development/use_repository_list_tags_on_graphql.yml b/config/feature_flags/development/use_repository_list_tags_on_graphql.yml
new file mode 100644
index 00000000000..926d952e6f9
--- /dev/null
+++ b/config/feature_flags/development/use_repository_list_tags_on_graphql.yml
@@ -0,0 +1,8 @@
+---
+name: use_repository_list_tags_on_graphql
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132716
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/426358
+milestone: '16.5'
+type: development
+group: group::container registry
+default_enabled: false
diff --git a/config/feature_flags/development/use_sql_functions_for_primary_key_lookups.yml b/config/feature_flags/development/use_sql_functions_for_primary_key_lookups.yml
new file mode 100644
index 00000000000..c8ee2894aef
--- /dev/null
+++ b/config/feature_flags/development/use_sql_functions_for_primary_key_lookups.yml
@@ -0,0 +1,8 @@
+---
+name: use_sql_functions_for_primary_key_lookups
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/135196
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/429479
+milestone: '16.6'
+type: development
+group: group::optimize
+default_enabled: false
diff --git a/config/feature_flags/development/user_profile_overflow_menu_vue.yml b/config/feature_flags/development/user_profile_overflow_menu_vue.yml
deleted file mode 100644
index 42a792414cf..00000000000
--- a/config/feature_flags/development/user_profile_overflow_menu_vue.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: user_profile_overflow_menu_vue
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/122971
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/414773
-milestone: '16.1'
-type: development
-group: group::tenant scale
-default_enabled: false
diff --git a/config/feature_flags/development/value_stream_dashboard_on_off_setting.yml b/config/feature_flags/development/value_stream_dashboard_on_off_setting.yml
deleted file mode 100644
index a6023199d05..00000000000
--- a/config/feature_flags/development/value_stream_dashboard_on_off_setting.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: value_stream_dashboard_on_off_setting
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/120610
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/411223
-milestone: '16.1'
-type: development
-group: group::optimize
-default_enabled: true
diff --git a/config/feature_flags/development/verify_push_rules_for_first_commit.yml b/config/feature_flags/development/verify_push_rules_for_first_commit.yml
deleted file mode 100644
index f18f9eecfdb..00000000000
--- a/config/feature_flags/development/verify_push_rules_for_first_commit.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: verify_push_rules_for_first_commit
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/123950
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/419128
-milestone: '16.3'
-type: development
-group: group::source code
-default_enabled: true
diff --git a/config/feature_flags/development/vscode_web_ide.yml b/config/feature_flags/development/vscode_web_ide.yml
index dc9d9d5f5f1..93f4141ed97 100644
--- a/config/feature_flags/development/vscode_web_ide.yml
+++ b/config/feature_flags/development/vscode_web_ide.yml
@@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/95169
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/371084
milestone: '15.4'
type: development
-group: group::editor
+group: group::ide
default_enabled: true
diff --git a/config/feature_flags/development/vulnerability_report_grouping.yml b/config/feature_flags/development/vulnerability_report_grouping.yml
deleted file mode 100644
index fc7312cc147..00000000000
--- a/config/feature_flags/development/vulnerability_report_grouping.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: vulnerability_report_grouping
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/129709
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/422509
-milestone: '16.5'
-type: development
-group: group::threat insights
-default_enabled: true
diff --git a/config/feature_flags/development/webauthn_without_totp.yml b/config/feature_flags/development/webauthn_without_totp.yml
index 3820c5edfb9..c4174e7f7c0 100644
--- a/config/feature_flags/development/webauthn_without_totp.yml
+++ b/config/feature_flags/development/webauthn_without_totp.yml
@@ -4,5 +4,5 @@ introduced_by_url: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/107438
rollout_issue_url: "https://gitlab.com/gitlab-org/gitlab/-/issues/386270"
milestone: '15.9'
type: development
-group: group::authentication and authorization
+group: group::authentication
default_enabled: false
diff --git a/config/feature_flags/development/widget_pipeline_pass_subscription_update.yml b/config/feature_flags/development/widget_pipeline_pass_subscription_update.yml
new file mode 100644
index 00000000000..764b0a59291
--- /dev/null
+++ b/config/feature_flags/development/widget_pipeline_pass_subscription_update.yml
@@ -0,0 +1,8 @@
+---
+name: widget_pipeline_pass_subscription_update
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132353
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/428633
+milestone: '16.6'
+type: development
+group: group::code review
+default_enabled: false
diff --git a/config/feature_flags/development/wiki_front_matter.yml b/config/feature_flags/development/wiki_front_matter.yml
index 39196440d17..6e4cf0db88b 100644
--- a/config/feature_flags/development/wiki_front_matter.yml
+++ b/config/feature_flags/development/wiki_front_matter.yml
@@ -1,8 +1,8 @@
---
name: wiki_front_matter
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/27706
-rollout_issue_url:
+rollout_issue_url:
milestone: '12.10'
type: development
-group: group::editor
+group: group::knowledge
default_enabled: false
diff --git a/config/feature_flags/development/wiki_front_matter_title.yml b/config/feature_flags/development/wiki_front_matter_title.yml
new file mode 100644
index 00000000000..381bb2e1154
--- /dev/null
+++ b/config/feature_flags/development/wiki_front_matter_title.yml
@@ -0,0 +1,8 @@
+---
+name: wiki_front_matter_title
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/133521
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/428259
+milestone: '16.6'
+type: development
+group: group::knowledge
+default_enabled: false
diff --git a/config/feature_flags/experiment/disable_network_graph_notes_count.yml b/config/feature_flags/experiment/disable_network_graph_notes_count.yml
deleted file mode 100644
index fa4e5b4e104..00000000000
--- a/config/feature_flags/experiment/disable_network_graph_notes_count.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: disable_network_graph_notes_count
-introduced_by_url: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/103636"
-rollout_issue_url:
-milestone: '15.6'
-type: experiment
-group: group::source code
-default_enabled: false
diff --git a/config/feature_flags/experiment/ios_specific_templates.yml b/config/feature_flags/experiment/ios_specific_templates.yml
deleted file mode 100644
index 0af80e7a5bb..00000000000
--- a/config/feature_flags/experiment/ios_specific_templates.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: ios_specific_templates
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/84589
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/356398
-milestone: "14.10"
-type: experiment
-group: group::activation
-default_enabled: false
diff --git a/config/feature_flags/ops/automatic_lock_writes_on_partition_tables.yml b/config/feature_flags/ops/automatic_lock_writes_on_partition_tables.yml
new file mode 100644
index 00000000000..dcaa0c9b4cb
--- /dev/null
+++ b/config/feature_flags/ops/automatic_lock_writes_on_partition_tables.yml
@@ -0,0 +1,8 @@
+---
+name: automatic_lock_writes_on_partition_tables
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132517
+rollout_issue_url:
+milestone: '16.5'
+type: ops
+group: group::tenant scale
+default_enabled: true
diff --git a/config/feature_flags/ops/block_password_auth_for_saml_users.yml b/config/feature_flags/ops/block_password_auth_for_saml_users.yml
index d84d8b5133b..ffc04d14a19 100644
--- a/config/feature_flags/ops/block_password_auth_for_saml_users.yml
+++ b/config/feature_flags/ops/block_password_auth_for_saml_users.yml
@@ -4,5 +4,5 @@ introduced_by_url:
rollout_issue_url:
milestone: '13.11'
type: ops
-group: group::authentication and authorization
+group: group::authentication
default_enabled: false
diff --git a/config/feature_flags/ops/code_suggestions_tokens_api.yml b/config/feature_flags/ops/code_suggestions_tokens_api.yml
index 9fc2a5358cc..2de8afc0428 100644
--- a/config/feature_flags/ops/code_suggestions_tokens_api.yml
+++ b/config/feature_flags/ops/code_suggestions_tokens_api.yml
@@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/120892
rollout_issue_url:
milestone: '16.1'
type: ops
-group: group::ai assisted
+group: group::ai model validation
default_enabled: true
diff --git a/config/feature_flags/ops/enforce_ci_builds_pagination_limit.yml b/config/feature_flags/ops/enforce_ci_builds_pagination_limit.yml
new file mode 100644
index 00000000000..b5f44795c20
--- /dev/null
+++ b/config/feature_flags/ops/enforce_ci_builds_pagination_limit.yml
@@ -0,0 +1,8 @@
+---
+name: enforce_ci_builds_pagination_limit
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/135162
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/429453
+milestone: '16.6'
+type: ops
+group: group::pipeline execution
+default_enabled: false
diff --git a/config/feature_flags/ops/enforce_memory_watchdog.yml b/config/feature_flags/ops/enforce_memory_watchdog.yml
index ae0ab81f9f6..e87e897d223 100644
--- a/config/feature_flags/ops/enforce_memory_watchdog.yml
+++ b/config/feature_flags/ops/enforce_memory_watchdog.yml
@@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91910
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/367534
milestone: '15.2'
type: ops
-group: group::application performance
+group: group::cloud connector
default_enabled: true
diff --git a/config/feature_flags/ops/report_heap_dumps.yml b/config/feature_flags/ops/report_heap_dumps.yml
index 12b126a8f80..9583ebd9970 100644
--- a/config/feature_flags/ops/report_heap_dumps.yml
+++ b/config/feature_flags/ops/report_heap_dumps.yml
@@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/106406
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/385175
milestone: '15.7'
type: ops
-group: group::application performance
+group: group::cloud connector
default_enabled: false
diff --git a/config/feature_flags/ops/report_jemalloc_stats.yml b/config/feature_flags/ops/report_jemalloc_stats.yml
index 61fbfa26206..f2a34dae232 100644
--- a/config/feature_flags/ops/report_jemalloc_stats.yml
+++ b/config/feature_flags/ops/report_jemalloc_stats.yml
@@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91283
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/367845
milestone: '15.2'
type: ops
-group: group::application performance
+group: group::cloud connector
default_enabled: false
diff --git a/config/feature_flags/ops/suggested_reviewers_internal_api.yml b/config/feature_flags/ops/suggested_reviewers_internal_api.yml
index 44e197307a5..62cdf94a5b9 100644
--- a/config/feature_flags/ops/suggested_reviewers_internal_api.yml
+++ b/config/feature_flags/ops/suggested_reviewers_internal_api.yml
@@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/106648
rollout_issue_url:
milestone: '15.7'
type: ops
-group: group::ai assisted
+group: group::ai model validation
default_enabled: true
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index e2e5c37aa43..1e5fb17c971 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -183,6 +183,7 @@ Settings.gitlab['default_project_creation'] ||= ::Gitlab::Access::DEVELOPER_MAIN
Settings.gitlab['default_project_deletion_protection'] ||= false
Settings.gitlab['default_projects_limit'] ||= 100000
Settings.gitlab['default_branch_protection'] ||= 2
+Settings.gitlab['default_branch_protection_defaults'] ||= ::Gitlab::Access::BranchProtection.protected_fully
# `default_can_create_group` is deprecated since GitLab 15.5 in favour of the `can_create_group` column on `ApplicationSetting`.
Settings.gitlab['default_can_create_group'] = true if Settings.gitlab['default_can_create_group'].nil?
Settings.gitlab['default_theme'] = Gitlab::Themes::APPLICATION_DEFAULT if Settings.gitlab['default_theme'].nil?
@@ -799,15 +800,9 @@ Gitlab.ee do
Settings.cron_jobs['llm_embedding_gitlab_documentation_create_empty_embeddings_records_worker'] ||= {}
Settings.cron_jobs['llm_embedding_gitlab_documentation_create_empty_embeddings_records_worker']['cron'] ||= '0 5 * * 1,2,3,4,5'
Settings.cron_jobs['llm_embedding_gitlab_documentation_create_empty_embeddings_records_worker']['job_class'] ||= 'Llm::Embedding::GitlabDocumentation::CreateEmptyEmbeddingsRecordsWorker'
- Settings.cron_jobs['tanuki_bot_recreate_records_worker'] ||= {}
- Settings.cron_jobs['tanuki_bot_recreate_records_worker']['cron'] ||= '0 5 * * 1,2,3,4,5'
- Settings.cron_jobs['tanuki_bot_recreate_records_worker']['job_class'] ||= 'Llm::TanukiBot::RecreateRecordsWorker'
Settings.cron_jobs['llm_embedding_gitlab_documentation_cleanup_previous_versions_records_worker'] ||= {}
Settings.cron_jobs['llm_embedding_gitlab_documentation_cleanup_previous_versions_records_worker']['cron'] ||= '0 0 * * *'
Settings.cron_jobs['llm_embedding_gitlab_documentation_cleanup_previous_versions_records_worker']['job_class'] ||= 'Llm::Embedding::GitlabDocumentation::CleanupPreviousVersionsRecordsWorker'
- Settings.cron_jobs['tanuki_bot_remove_previous_records_worker'] ||= {}
- Settings.cron_jobs['tanuki_bot_remove_previous_records_worker']['cron'] ||= '0 0 * * *'
- Settings.cron_jobs['tanuki_bot_remove_previous_records_worker']['job_class'] ||= 'Llm::TanukiBot::RemovePreviousRecordsWorker'
Settings.cron_jobs['users_create_statistics_worker'] ||= {}
Settings.cron_jobs['users_create_statistics_worker']['cron'] ||= '2 15 * * *'
Settings.cron_jobs['users_create_statistics_worker']['job_class'] = 'Users::CreateStatisticsWorker'
@@ -874,6 +869,9 @@ Gitlab.ee do
Settings.cron_jobs['ci_schedule_unlock_pipelines_in_queue_worker'] ||= {}
Settings.cron_jobs['ci_schedule_unlock_pipelines_in_queue_worker']['cron'] ||= '*/1 * * * *'
Settings.cron_jobs['ci_schedule_unlock_pipelines_in_queue_worker']['job_class'] = 'Ci::ScheduleUnlockPipelinesInQueueCronWorker'
+ Settings.cron_jobs['timeout_pending_status_check_responses_worker'] ||= {}
+ Settings.cron_jobs['timeout_pending_status_check_responses_worker']['cron'] ||= '*/1 * * * *'
+ Settings.cron_jobs['timeout_pending_status_check_responses_worker']['job_class'] = 'ComplianceManagement::TimeoutPendingStatusCheckResponsesWorker'
Gitlab.com do
Settings.cron_jobs['disable_legacy_open_source_license_for_inactive_projects'] ||= {}
diff --git a/config/initializers/7_redis.rb b/config/initializers/7_redis.rb
index 060d0a8a67b..25c2c6aa11f 100644
--- a/config/initializers/7_redis.rb
+++ b/config/initializers/7_redis.rb
@@ -27,6 +27,10 @@ Redis::Cluster::SlotLoader.prepend(Gitlab::Patch::SlotLoader)
Redis::Cluster::CommandLoader.prepend(Gitlab::Patch::CommandLoader)
Redis::Cluster.prepend(Gitlab::Patch::RedisCluster)
+if Gitlab::Redis::Workhorse.params[:cluster].present?
+ raise "Do not configure workhorse with a Redis Cluster as pub/sub commands are not cluster-compatible."
+end
+
# Make sure we initialize a Redis connection pool before multi-threaded
# execution starts by
# 1. Sidekiq
diff --git a/config/initializers/action_cable.rb b/config/initializers/action_cable.rb
index 6d7f0497cd0..fb52ac6eb8a 100644
--- a/config/initializers/action_cable.rb
+++ b/config/initializers/action_cable.rb
@@ -11,15 +11,20 @@ end
ActionCable::SubscriptionAdapter::Base.prepend(Gitlab::Patch::ActionCableSubscriptionAdapterIdentifier)
+using_redis_cluster = begin
+ Rails.application.config_for(:cable)[:cluster].present?
+rescue RuntimeError
+ # config/cable.yml does not exist, but that is not the purpose of this check
+end
+
+raise "Do not configure cable.yml with a Redis Cluster as ActionCable only works with Redis." if using_redis_cluster
+
# https://github.com/rails/rails/blob/bb5ac1623e8de08c1b7b62b1368758f0d3bb6379/actioncable/lib/action_cable/subscription_adapter/redis.rb#L18
ActionCable::SubscriptionAdapter::Redis.redis_connector = lambda do |config|
args = config.except(:adapter, :channel_prefix)
.merge(instrumentation_class: ::Gitlab::Instrumentation::Redis::ActionCable)
- primary_store = ::Redis.new(Gitlab::Redis::Pubsub.params)
- secondary_store = ::Redis.new(args)
-
- Gitlab::Redis::MultiStore.new(primary_store, secondary_store, "ActionCable")
+ ::Redis.new(args)
end
Gitlab::ActionCable::RequestStoreCallbacks.install
diff --git a/config/initializers/active_record_renamed_table.rb b/config/initializers/active_record_renamed_table.rb
index 948ef8790c8..5a9e30d2fb5 100644
--- a/config/initializers/active_record_renamed_table.rb
+++ b/config/initializers/active_record_renamed_table.rb
@@ -1,5 +1,9 @@
# frozen_string_literal: true
ActiveSupport.on_load(:active_record) do
- ActiveRecord::ConnectionAdapters::SchemaCache.prepend(Gitlab::Database::SchemaCacheWithRenamedTable)
+ if Gem::Version.new(ActiveRecord::VERSION::STRING) >= Gem::Version.new('7.1')
+ ActiveRecord::ConnectionAdapters::SchemaCache.prepend(Gitlab::Database::SchemaCacheWithRenamedTable)
+ else
+ ActiveRecord::ConnectionAdapters::SchemaCache.prepend(Gitlab::Database::SchemaCacheWithRenamedTableLegacy)
+ end
end
diff --git a/config/initializers/database_query_analyzers.rb b/config/initializers/database_query_analyzers.rb
index 5c2f3caf89e..9facd822e5c 100644
--- a/config/initializers/database_query_analyzers.rb
+++ b/config/initializers/database_query_analyzers.rb
@@ -9,7 +9,10 @@ Gitlab::Database::QueryAnalyzer.instance.tap do |query_analyzer|
analyzers.append(::Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification)
analyzers.append(::Gitlab::Database::QueryAnalyzers::Ci::PartitioningRoutingAnalyzer)
- analyzers.append(::Gitlab::Database::QueryAnalyzers::GitlabSchemasValidateConnection) if Gitlab.dev_or_test_env?
+ if Gitlab.dev_or_test_env?
+ analyzers.append(::Gitlab::Database::QueryAnalyzers::GitlabSchemasValidateConnection)
+ analyzers.append(::Gitlab::Database::QueryAnalyzers::PreventSetOperatorMismatch)
+ end
end
end
diff --git a/config/initializers/elastic_client_setup.rb b/config/initializers/elastic_client_setup.rb
index dd68a3f7999..5fe26d7bd92 100644
--- a/config/initializers/elastic_client_setup.rb
+++ b/config/initializers/elastic_client_setup.rb
@@ -19,6 +19,9 @@ Gitlab.ee do
Elasticsearch::Model::ClassMethods.prepend GemExtensions::Elasticsearch::Model::Client
Elasticsearch::Model.singleton_class.prepend GemExtensions::Elasticsearch::Model::Client
+ require 'elasticsearch/api'
+ Elasticsearch::API::Utils.prepend GemExtensions::Elasticsearch::API::Utils
+
### Modified from elasticsearch-model/lib/elasticsearch/model/searching.rb
module Elasticsearch
diff --git a/config/initializers/peek.rb b/config/initializers/peek.rb
index 6ac116f46f5..e1c59851fb1 100644
--- a/config/initializers/peek.rb
+++ b/config/initializers/peek.rb
@@ -16,7 +16,6 @@ Peek.into Peek::Views::Gitaly
Peek.into Peek::Views::RedisDetailed
Peek.into Peek::Views::Elasticsearch
Peek.into Peek::Views::Zoekt
-Peek.into Peek::Views::Rugged
Peek.into Peek::Views::ExternalHttp
Peek.into Peek::Views::ClickHouse
Peek.into Peek::Views::BulletDetailed if defined?(Bullet)
diff --git a/config/initializers/postgresql_cte.rb b/config/initializers/postgresql_cte.rb
index 7f0196197b9..40d959c1ba0 100644
--- a/config/initializers/postgresql_cte.rb
+++ b/config/initializers/postgresql_cte.rb
@@ -40,6 +40,8 @@ module ActiveRecord::Querying
delegate :with, to: :all
end
+# Rails 7.1 defines #with method.
+# Therefore, this file can be either simplified or completely removed.
module ActiveRecord
class Relation
# WithChain objects act as placeholder for queries in which #with does not have any parameter.
@@ -51,21 +53,21 @@ module ActiveRecord
# Returns a new relation expressing WITH RECURSIVE
def recursive(*args)
- @scope.with_values += args
+ @scope.with_values_ += args
@scope.recursive_value = true
@scope.extend(Gitlab::Database::ReadOnlyRelation)
@scope
end
end
- def with_values
- @values[:with] || []
+ def with_values_
+ @values[:with_values] || []
end
- def with_values=(values)
+ def with_values_=(values)
raise ImmutableRelation if @loaded
- @values[:with] = values
+ @values[:with_values] = values
end
def recursive_value=(value)
@@ -92,7 +94,7 @@ module ActiveRecord
if opts == :chain
WithChain.new(self)
else
- self.with_values += [opts] + rest
+ self.with_values_ += [opts] + rest
self
end
end
@@ -100,13 +102,13 @@ module ActiveRecord
def build_arel(aliases = nil)
arel = super
- build_with(arel) if @values[:with]
+ build_with(arel) if @values[:with_values]
arel
end
def build_with(arel)
- with_statements = with_values.flat_map do |with_value|
+ with_statements = with_values_.flat_map do |with_value|
case with_value
when String
with_value
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
index 57850e4e35c..9b6a9b17935 100644
--- a/config/initializers/sidekiq.rb
+++ b/config/initializers/sidekiq.rb
@@ -30,10 +30,6 @@ end
# Custom Queues configuration
queues_config_hash = Gitlab::Redis::Queues.params
-unless Gitlab::Utils.to_boolean(ENV['SIDEKIQ_ENQUEUE_NON_NAMESPACED'])
- queues_config_hash[:namespace] = Gitlab::Redis::Queues::SIDEKIQ_NAMESPACE
-end
-
enable_json_logs = Gitlab.config.sidekiq.log_format != 'text'
Sidekiq.configure_server do |config|
diff --git a/config/initializers/sprockets_patch.rb b/config/initializers/sprockets_patch.rb
new file mode 100644
index 00000000000..76474b370be
--- /dev/null
+++ b/config/initializers/sprockets_patch.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+# This backports https://github.com/rails/sprockets/pull/759 to Sprockets v3.7.2 to
+# fix thread-safety issues with compiling SASS.
+#
+# This pull request has already been merged in Sprockets v4.2.0, but we
+# don't plan on upgrading: https://gitlab.com/gitlab-org/gitlab/-/issues/373997#note_1360248557
+
+require 'sprockets/utils'
+
+unless Gem::Version.new(Sprockets::VERSION) == Gem::Version.new('3.7.2')
+ raise 'New version of Sprockets detected. This patch can likely be removed.'
+end
+
+# rubocop:disable Style/CombinableLoops -- Keep the format consistent with upstream project
+# rubocop:disable Cop/LineBreakAroundConditionalBlock -- Keep the format consistent with upstream project
+# rubocop:disable Style/IfUnlessModifier -- Keep the format consistent with upstream project
+# rubocop:disable Style/SoleNestedConditional -- Keep the format consistent with upstream project
+module Sprockets
+ module Utils
+ extend self
+
+ MODULE_INCLUDE_MUTEX = Mutex.new
+ private_constant :MODULE_INCLUDE_MUTEX
+
+ # Internal: Inject into target module for the duration of the block.
+ #
+ # mod - Module
+ #
+ # Returns result of block.
+ def module_include(base, mod)
+ MODULE_INCLUDE_MUTEX.synchronize do
+ old_methods = {}
+
+ mod.instance_methods.each do |sym|
+ old_methods[sym] = base.instance_method(sym) if base.method_defined?(sym)
+ end
+
+ unless UNBOUND_METHODS_BIND_TO_ANY_OBJECT
+ base.send(:include, mod) unless base < mod
+ end
+
+ mod.instance_methods.each do |sym|
+ method = mod.instance_method(sym)
+ base.send(:define_method, sym, method)
+ end
+
+ yield
+ ensure
+ mod.instance_methods.each do |sym|
+ base.send(:undef_method, sym) if base.method_defined?(sym)
+ end
+ old_methods.each do |sym, method|
+ base.send(:define_method, sym, method)
+ end
+ end
+ end
+ end
+end
+# rubocop:enable Style/CombinableLoops
+# rubocop:enable Cop/LineBreakAroundConditionalBlock
+# rubocop:enable Style/IfUnlessModifier
+# rubocop:enable Style/SoleNestedConditional
diff --git a/config/initializers/wikicloth_redos_patch.rb b/config/initializers/wikicloth_redos_patch.rb
index 95901378891..501a4084edc 100644
--- a/config/initializers/wikicloth_redos_patch.rb
+++ b/config/initializers/wikicloth_redos_patch.rb
@@ -94,7 +94,7 @@ module WikiCloth
data << "\n" if data.last(1) != "\n"
data << "garbage"
- buffer = WikiBuffer.new("",options)
+ buffer = WikiBuffer.new(+'',options)
begin
if self.options[:fast]
diff --git a/config/mail_room.yml b/config/mail_room.yml
index b453ed8ce35..355df13fd61 100644
--- a/config/mail_room.yml
+++ b/config/mail_room.yml
@@ -31,7 +31,6 @@
:delivery_options:
:redis_url: <%= config[:redis_url].to_json %>
:redis_db: <%= config[:redis_db] %>
- :namespace: <%= Gitlab::Redis::Queues::SIDEKIQ_NAMESPACE %>
:queue: <%= config[:queue] %>
:worker: <%= config[:worker] %>
<% if config[:sentinels] %>
diff --git a/config/metrics/counts_28d/20231102160653_i_quickactions_request_changes_monthly.yml b/config/metrics/counts_28d/20231102160653_i_quickactions_request_changes_monthly.yml
new file mode 100644
index 00000000000..970c49e3962
--- /dev/null
+++ b/config/metrics/counts_28d/20231102160653_i_quickactions_request_changes_monthly.yml
@@ -0,0 +1,23 @@
+---
+key_path: redis_hll_counters.quickactions.i_quickactions_request_changes_monthly
+description: Count using the `/request_changes` quick action on Merge Requests
+product_section: dev
+product_stage: create
+product_group: code_review
+value_type: number
+status: active
+milestone: '16.6'
+time_frame: 28d
+data_source: redis_hll
+data_category: optional
+instrumentation_class: RedisHLLMetric
+options:
+ events:
+ - i_quickactions_request_changes
+distribution:
+ - ce
+ - ee
+tier:
+ - free
+ - premium
+ - ultimate
diff --git a/config/metrics/counts_7d/20231102160653_i_quickactions_request_changes_weekly.yml b/config/metrics/counts_7d/20231102160653_i_quickactions_request_changes_weekly.yml
new file mode 100644
index 00000000000..e529dfd4e51
--- /dev/null
+++ b/config/metrics/counts_7d/20231102160653_i_quickactions_request_changes_weekly.yml
@@ -0,0 +1,23 @@
+---
+key_path: redis_hll_counters.quickactions.i_quickactions_request_changes_weekly
+description: Count using the `/request_changes` quick action on Merge Requests
+product_section: dev
+product_stage: create
+product_group: code_review
+value_type: number
+status: active
+milestone: '16.6'
+time_frame: 7d
+data_source: redis_hll
+data_category: optional
+instrumentation_class: RedisHLLMetric
+options:
+ events:
+ - i_quickactions_request_changes
+distribution:
+ - ce
+ - ee
+tier:
+ - free
+ - premium
+ - ultimate
diff --git a/config/metrics/counts_all/20210216180232_projects_jira_dvcs_cloud_active.yml b/config/metrics/counts_all/20210216180232_projects_jira_dvcs_cloud_active.yml
index 5bf8e1d6e78..c9c85bca415 100644
--- a/config/metrics/counts_all/20210216180232_projects_jira_dvcs_cloud_active.yml
+++ b/config/metrics/counts_all/20210216180232_projects_jira_dvcs_cloud_active.yml
@@ -6,7 +6,7 @@ product_section: dev
product_stage: manage
product_group: integrations
value_type: number
-status: active
+status: removed
time_frame: all
data_source: database
instrumentation_class: CountProjectsWithJiraDvcsIntegrationMetric
@@ -21,3 +21,5 @@ tier:
- ultimate
performance_indicator_type: []
milestone: "<13.9"
+removed_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/135755
+milestone_removed: "<16.6"
diff --git a/config/metrics/schema.json b/config/metrics/schema.json
deleted file mode 100644
index fe53b92f7de..00000000000
--- a/config/metrics/schema.json
+++ /dev/null
@@ -1,237 +0,0 @@
-{
- "type": "object",
- "required": [
- "key_path",
- "description",
- "value_type",
- "status",
- "product_group",
- "product_section",
- "product_stage",
- "time_frame",
- "data_source",
- "distribution",
- "tier",
- "data_category",
- "milestone"
- ],
- "properties": {
- "key_path": {
- "type": "string"
- },
- "description": {
- "type": "string"
- },
- "product_section": {
- "type": [
- "string"
- ]
- },
- "product_stage": {
- "type": [
- "string"
- ]
- },
- "product_group": {
- "type": "string",
- "pattern": "^$|^([a-z]+_)*[a-z]+$"
- },
- "value_type": {
- "type": "string",
- "enum": [
- "string",
- "number",
- "boolean",
- "object"
- ]
- },
- "status": {
- "type": [
- "string"
- ],
- "enum": [
- "active",
- "removed",
- "broken"
- ]
- },
- "milestone": {
- "type": [
- "string"
- ],
- "pattern": "^<?[0-9]+\\.[0-9]+$"
- },
- "milestone_removed": {
- "type": [
- "string"
- ],
- "pattern": "^<?[0-9]+\\.[0-9]+$"
- },
- "introduced_by_url": {
- "type": [
- "string",
- "null"
- ]
- },
- "removed_by_url": {
- "type": [
- "string",
- "null"
- ]
- },
- "repair_issue_url": {
- "type": [
- "string"
- ]
- },
- "options": {
- "type": "object"
- },
- "events": {
- "type": "array",
- "items": {
- "type": "object",
- "required": [
- "name"
- ],
- "properties": {
- "name": {
- "type": "string"
- },
- "unique": {
- "type": "string",
- "enum": [
- "user.id",
- "project.id",
- "namespace.id"
- ]
- }
- }
- }
- },
- "time_frame": {
- "type": "string",
- "enum": [
- "7d",
- "28d",
- "all",
- "none"
- ]
- },
- "data_source": {
- "type": "string",
- "enum": [
- "database",
- "redis",
- "redis_hll",
- "prometheus",
- "system",
- "license",
- "internal_events"
- ]
- },
- "data_category": {
- "type": "string",
- "enum": [
- "Operational",
- "Optional",
- "Subscription",
- "Standard",
- "operational",
- "optional",
- "subscription",
- "standard"
- ]
- },
- "instrumentation_class": {
- "type": "string",
- "pattern": "^(([A-Z][a-z]+)+::)*(([A-Z]+[a-z]+)+)$"
- },
- "distribution": {
- "type": "array",
- "items": {
- "type": "string",
- "enum": [
- "ee",
- "ce"
- ]
- }
- },
- "performance_indicator_type": {
- "type": "array",
- "items": {
- "type": "string",
- "enum": [
- "gmau",
- "smau",
- "paid_gmau",
- "umau",
- "customer_health_score"
- ]
- }
- },
- "tier": {
- "type": "array",
- "items": {
- "type": "string",
- "enum": [
- "free",
- "starter",
- "premium",
- "ultimate",
- "bronze",
- "silver",
- "gold"
- ]
- }
- },
- "value_json_schema": {
- "type": "string"
- }
- },
- "allOf": [
- {
- "if": {
- "properties": {
- "status": {
- "const": "broken"
- }
- }
- },
- "then": {
- "required": [
- "repair_issue_url"
- ]
- }
- },
- {
- "if": {
- "properties": {
- "data_source": {
- "const": "internal_events"
- }
- }
- },
- "then": {
- "required": [
- "events"
- ]
- }
- },
- {
- "if": {
- "properties": {
- "status": {
- "const": "removed"
- }
- }
- },
- "then": {
- "required": [
- "removed_by_url",
- "milestone_removed"
- ]
- }
- }
- ]
-}
diff --git a/config/metrics/schema/base.json b/config/metrics/schema/base.json
new file mode 100644
index 00000000000..5f571566651
--- /dev/null
+++ b/config/metrics/schema/base.json
@@ -0,0 +1,170 @@
+{
+ "type": "object",
+ "required": [
+ "key_path",
+ "description",
+ "value_type",
+ "status",
+ "product_group",
+ "product_section",
+ "product_stage",
+ "time_frame",
+ "data_source",
+ "distribution",
+ "tier",
+ "data_category",
+ "milestone"
+ ],
+ "properties": {
+ "key_path": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "product_section": {
+ "type": [
+ "string"
+ ]
+ },
+ "product_stage": {
+ "type": [
+ "string"
+ ]
+ },
+ "product_group": {
+ "type": "string",
+ "pattern": "^$|^([a-z]+_)*[a-z]+$"
+ },
+ "value_type": {
+ "type": "string",
+ "enum": [
+ "string",
+ "number",
+ "boolean",
+ "object"
+ ]
+ },
+ "status": {
+ "type": [
+ "string"
+ ],
+ "enum": [
+ "active",
+ "removed",
+ "broken"
+ ]
+ },
+ "milestone": {
+ "type": [
+ "string"
+ ],
+ "pattern": "^<?[0-9]+\\.[0-9]+$"
+ },
+ "milestone_removed": {
+ "type": [
+ "string"
+ ],
+ "pattern": "^<?[0-9]+\\.[0-9]+$"
+ },
+ "introduced_by_url": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "removed_by_url": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "repair_issue_url": {
+ "type": [
+ "string"
+ ]
+ },
+ "options": {
+ "type": "object"
+ },
+ "time_frame": {
+ "type": "string",
+ "enum": [
+ "7d",
+ "28d",
+ "all",
+ "none"
+ ]
+ },
+ "data_source": {
+ "type": "string",
+ "enum": [
+ "database",
+ "redis",
+ "redis_hll",
+ "prometheus",
+ "system",
+ "license",
+ "internal_events"
+ ]
+ },
+ "data_category": {
+ "type": "string",
+ "enum": [
+ "Operational",
+ "Optional",
+ "Subscription",
+ "Standard",
+ "operational",
+ "optional",
+ "subscription",
+ "standard"
+ ]
+ },
+ "instrumentation_class": {
+ "type": "string",
+ "pattern": "^(([A-Z][a-z]+)+::)*(([A-Z]+[a-z]+)+)$"
+ },
+ "distribution": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": [
+ "ee",
+ "ce"
+ ]
+ }
+ },
+ "performance_indicator_type": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": [
+ "gmau",
+ "smau",
+ "paid_gmau",
+ "umau",
+ "customer_health_score"
+ ]
+ }
+ },
+ "tier": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": [
+ "free",
+ "starter",
+ "premium",
+ "ultimate",
+ "bronze",
+ "silver",
+ "gold"
+ ]
+ }
+ },
+ "value_json_schema": {
+ "type": "string"
+ }
+ }
+}
diff --git a/config/metrics/schema/internal_events.json b/config/metrics/schema/internal_events.json
new file mode 100644
index 00000000000..75378db054d
--- /dev/null
+++ b/config/metrics/schema/internal_events.json
@@ -0,0 +1,106 @@
+{
+ "if": {
+ "properties": {
+ "data_source": {
+ "const": "internal_events"
+ }
+ }
+ },
+ "then": {
+ "oneOf": [
+ {
+ "properties": {
+ "instrumentation_class": {
+ "const": "RedisHLLMetric"
+ },
+ "options": {
+ "type": "object",
+ "properties": {
+ "events": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ "required": [
+ "events"
+ ],
+ "additionalProperties": false
+ },
+ "events": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "name",
+ "unique"
+ ],
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "unique": {
+ "type": "string",
+ "enum": [
+ "user.id",
+ "project.id",
+ "namespace.id"
+ ]
+ }
+ },
+ "additionalProperties": false
+ }
+ }
+ },
+ "required": [
+ "events",
+ "options",
+ "instrumentation_class"
+ ]
+ },
+ {
+ "properties": {
+ "instrumentation_class": {
+ "const": "TotalCountMetric"
+ },
+ "options": {
+ "type": "object",
+ "properties": {
+ "events": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ "required": [
+ "events"
+ ],
+ "additionalProperties": false
+ },
+ "events": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ }
+ }
+ },
+ "required": [
+ "events",
+ "options",
+ "instrumentation_class"
+ ]
+ }
+ ]
+ }
+}
diff --git a/config/metrics/schema/redis.json b/config/metrics/schema/redis.json
new file mode 100644
index 00000000000..3fe3ba3e7ec
--- /dev/null
+++ b/config/metrics/schema/redis.json
@@ -0,0 +1,95 @@
+{
+ "if": {
+ "properties": {
+ "data_source": {
+ "const": "redis"
+ }
+ }
+ },
+ "then": {
+ "oneOf": [
+ {
+ "properties": {
+ "instrumentation_class": {
+ "const": "MergeRequestWidgetExtensionMetric"
+ },
+ "options": {
+ "type": "object",
+ "properties": {
+ "event": {
+ "type": "string"
+ },
+ "widget": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "event",
+ "widget"
+ ],
+ "additionalProperties": false
+ }
+ },
+ "required": [
+ "instrumentation_class",
+ "options"
+ ]
+ },
+ {
+ "properties": {
+ "instrumentation_class": {
+ "const": "RedisMetric"
+ },
+ "options": {
+ "type": "object",
+ "properties": {
+ "event": {
+ "type": "string"
+ },
+ "prefix": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "include_usage_prefix": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "event",
+ "prefix"
+ ],
+ "additionalProperties": false
+ }
+ },
+ "required": [
+ "instrumentation_class",
+ "options"
+ ]
+ },
+ {
+ "properties": {
+ "key_path": {
+ "description": "Legacy metrics that do not match with the schema",
+ "enum": [
+ "counts.dependency_list_usages_total",
+ "counts.network_policy_forwards",
+ "counts.network_policy_drops",
+ "counts.static_site_editor_views",
+ "counts.static_site_editor_commits",
+ "counts.static_site_editor_merge_requests",
+ "counts.package_events_i_package_container_delete_package",
+ "counts.package_events_i_package_container_pull_package",
+ "counts.package_events_i_package_container_push_package",
+ "counts.package_events_i_package_debian_push_package",
+ "counts.package_events_i_package_tag_delete_package",
+ "counts.package_events_i_package_tag_pull_package",
+ "counts.package_events_i_package_tag_push_package"
+ ]
+ }
+ }
+ }
+ ]
+ }
+}
diff --git a/config/metrics/schema/redis_hll.json b/config/metrics/schema/redis_hll.json
new file mode 100644
index 00000000000..35d520a5833
--- /dev/null
+++ b/config/metrics/schema/redis_hll.json
@@ -0,0 +1,103 @@
+{
+ "if": {
+ "properties": {
+ "data_source": {
+ "const": "redis_hll"
+ }
+ }
+ },
+ "then": {
+ "oneOf": [
+ {
+ "properties": {
+ "instrumentation_class": {
+ "const": "RedisHLLMetric"
+ },
+ "options": {
+ "type": "object",
+ "properties": {
+ "events": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ "required": [
+ "events"
+ ],
+ "additionalProperties": false
+ }
+ },
+ "required": [
+ "instrumentation_class",
+ "options"
+ ]
+ },
+ {
+ "properties": {
+ "instrumentation_class": {
+ "const": "AggregatedMetric"
+ },
+ "options": {
+ "type": "object",
+ "properties": {
+ "aggregate": {
+ "type": "object",
+ "properties": {
+ "operator": {
+ "enum": [
+ "OR",
+ "AND"
+ ]
+ },
+ "attribute": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "operator",
+ "attribute"
+ ],
+ "additionalProperties": false
+ },
+ "events": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ "required": [
+ "aggregate",
+ "events"
+ ],
+ "additionalProperties": false
+ }
+ },
+ "required": [
+ "instrumentation_class",
+ "options"
+ ]
+ },
+ {
+ "properties": {
+ "key_path": {
+ "description": "Legacy metrics that do not match with the schema",
+ "enum": [
+ "usage_activity_by_stage_monthly.create.merge_requests_users",
+ "usage_activity_by_stage_monthly.create.action_monthly_active_users_web_ide_edit",
+ "usage_activity_by_stage_monthly.create.action_monthly_active_users_sfe_edit",
+ "usage_activity_by_stage_monthly.create.action_monthly_active_users_snippet_editor_edit",
+ "usage_activity_by_stage_monthly.create.action_monthly_active_users_sse_edit",
+ "counts_monthly.aggregated_metrics.product_analytics_test_metrics_union",
+ "counts_weekly.aggregated_metrics.product_analytics_test_metrics_union",
+ "counts_monthly.aggregated_metrics.product_analytics_test_metrics_intersection",
+ "counts_weekly.aggregated_metrics.product_analytics_test_metrics_intersection"
+ ]
+ }
+ }
+ }
+ ]
+ }
+}
diff --git a/config/metrics/schema/status.json b/config/metrics/schema/status.json
new file mode 100644
index 00000000000..7b71a22b4c6
--- /dev/null
+++ b/config/metrics/schema/status.json
@@ -0,0 +1,33 @@
+{
+ "allOf": [
+ {
+ "if": {
+ "properties": {
+ "status": {
+ "const": "broken"
+ }
+ }
+ },
+ "then": {
+ "required": [
+ "repair_issue_url"
+ ]
+ }
+ },
+ {
+ "if": {
+ "properties": {
+ "status": {
+ "const": "removed"
+ }
+ }
+ },
+ "then": {
+ "required": [
+ "removed_by_url",
+ "milestone_removed"
+ ]
+ }
+ }
+ ]
+}
diff --git a/config/metrics/settings/20210204124920_web_ide_clientside_preview_enabled.yml b/config/metrics/settings/20210204124920_web_ide_clientside_preview_enabled.yml
index a206d8ecd7a..a04e8d82686 100644
--- a/config/metrics/settings/20210204124920_web_ide_clientside_preview_enabled.yml
+++ b/config/metrics/settings/20210204124920_web_ide_clientside_preview_enabled.yml
@@ -6,7 +6,7 @@ product_section: dev
product_stage: create
product_group: ide
value_type: boolean
-status: active
+status: removed
time_frame: none
data_source: database
distribution:
@@ -18,3 +18,5 @@ tier:
- ultimate
performance_indicator_type: []
milestone: "<13.9"
+removed_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/136114
+milestone_removed: "16.6"
diff --git a/config/redis.yml.example b/config/redis.yml.example
index 9d884038af7..a391ae36a65 100644
--- a/config/redis.yml.example
+++ b/config/redis.yml.example
@@ -18,6 +18,11 @@ development:
queues_metadata:
cluster:
- redis://localhost:7001
+ shared_state:
+ cluster:
+ - redis://localhost:7001
+ workhorse:
+ url: redis://localhost:6379
test:
chat:
@@ -38,3 +43,10 @@ test:
queues_metadata:
cluster:
- redis://localhost:7001
+ shared_state:
+ cluster:
+ - redis://localhost:7001
+ # pubsub and workhorse are not redis-cluster compatible
+ # even though they fall-back to shared_state
+ workhorse:
+ url: redis://localhost:6379
diff --git a/config/routes.rb b/config/routes.rb
index 82b2ef84a64..80a30372f5a 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -31,16 +31,6 @@ InitializerConnections.raise_if_new_database_connection do
end
put '/oauth/applications/:id/renew(.:format)' => 'oauth/applications#renew', as: :renew_oauth_application
- # This prefixless path is required because Jira gets confused if we set it up with a path
- # More information: https://gitlab.com/gitlab-org/gitlab/issues/6752
- scope path: '/login/oauth', controller: 'oauth/jira_dvcs/authorizations', as: :oauth_jira_dvcs do
- get :authorize, action: :new
- get :callback
- post :access_token
-
- match '*all', via: [:get, :post], to: proc { [404, {}, ['']] }
- end
-
draw :oauth
use_doorkeeper_openid_connect do
@@ -98,6 +88,7 @@ InitializerConnections.raise_if_new_database_connection do
get '/autocomplete/projects' => 'autocomplete#projects'
get '/autocomplete/award_emojis' => 'autocomplete#award_emojis'
get '/autocomplete/merge_request_target_branches' => 'autocomplete#merge_request_target_branches'
+ get '/autocomplete/merge_request_source_branches' => 'autocomplete#merge_request_source_branches'
get '/autocomplete/deploy_keys_with_owners' => 'autocomplete#deploy_keys_with_owners'
Gitlab.ee do
@@ -229,6 +220,8 @@ InitializerConnections.raise_if_new_database_connection do
get '/timelogs' => 'time_tracking/timelogs#index'
post '/track_namespace_visits' => 'users/namespace_visits#create'
+
+ get '/external_redirect' => 'external_redirect/external_redirect#index'
end
# End of the /-/ scope.
diff --git a/config/routes/admin.rb b/config/routes/admin.rb
index 5513ac1813a..bb59435729e 100644
--- a/config/routes/admin.rb
+++ b/config/routes/admin.rb
@@ -22,6 +22,8 @@ namespace :admin do
put :unlock
put :confirm
put :approve
+ put :trust
+ put :untrust
delete :reject
post :impersonate
patch :disable_two_factor
@@ -161,7 +163,6 @@ namespace :admin do
get :lets_encrypt_terms_of_service
get :slack_app_manifest_download, format: :json
get :slack_app_manifest_share
- get :service_usage_data
resource :appearances, only: [:show, :create, :update], path: 'appearance', module: 'application_settings' do
member do
diff --git a/config/routes/explore.rb b/config/routes/explore.rb
index 6777571bb68..36c2432d0cc 100644
--- a/config/routes/explore.rb
+++ b/config/routes/explore.rb
@@ -11,6 +11,7 @@ namespace :explore do
end
resources :groups, only: [:index]
+ resources :catalog, only: [:index, :show], constraints: { id: /\d+/ }
resources :snippets, only: [:index]
root to: 'projects#index'
end
diff --git a/config/routes/import.rb b/config/routes/import.rb
index 26843b4bc8d..274611283b5 100644
--- a/config/routes/import.rb
+++ b/config/routes/import.rb
@@ -74,6 +74,7 @@ namespace :import do
get :status
get :realtime_changes
get :history
+ get :details
end
resource :manifest, only: [:create, :new], controller: :manifest do
diff --git a/config/routes/organizations.rb b/config/routes/organizations.rb
index d53cfdf1a4e..62c791cdf69 100644
--- a/config/routes/organizations.rb
+++ b/config/routes/organizations.rb
@@ -8,6 +8,7 @@ resources(
) do
member do
get :groups_and_projects
+ get :users
resource :settings, only: [], as: :settings_organization do
get :general
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 8ed8574d0cc..947ed6b5413 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -69,6 +69,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
get :raw
get :terminal
get :proxy
+ get :test_report_summary
# These routes are also defined in gitlab-workhorse. Make sure to update accordingly.
get '/terminal.ws/authorize', to: 'jobs#terminal_websocket_authorize', format: false
@@ -461,7 +462,9 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
namespace :ml do
resources :experiments, only: [:index, :show, :destroy], controller: 'experiments', param: :iid
resources :candidates, only: [:show, :destroy], controller: 'candidates', param: :iid
- resources :models, only: [:index, :show], controller: 'models', param: :model_id
+ resources :models, only: [:index, :show, :destroy], controller: 'models', param: :model_id do
+ resources :versions, only: [:show], controller: 'model_versions', param: :model_version_id
+ end
end
namespace :service_desk do
diff --git a/config/settings.rb b/config/settings.rb
index 9abbbc11899..3edbcc9b5ed 100644
--- a/config/settings.rb
+++ b/config/settings.rb
@@ -3,10 +3,11 @@
require_relative '../lib/gitlab_settings'
file = ENV.fetch('GITLAB_CONFIG') { Rails.root.join('config/gitlab.yml') }
+section = ENV.fetch('GITLAB_ENV') { Rails.env }
GITLAB_INSTANCE_UUID_NOT_SET = 'uuid-not-set'
-Settings = GitlabSettings.load(file, Rails.env) do
+Settings = GitlabSettings.load(file, section) do
def gitlab_on_standard_port?
on_standard_port?(gitlab)
end
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 1f0b4840a8e..210a246978a 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -25,6 +25,10 @@
:queues:
- - abuse_new_abuse_report
- 1
+- - abuse_spam_abuse_events
+ - 1
+- - activity_pub
+ - 1
- - adjourned_project_deletion
- 1
- - admin_emails
@@ -273,6 +277,8 @@
- 2
- - emails_on_push
- 2
+- - environments_auto_recover
+ - 1
- - environments_auto_stop
- 1
- - environments_canary_ingress_update
@@ -349,8 +355,6 @@
- 1
- - groups_update_two_factor_requirement_for_members
- 1
-- - hashed_storage
- - 1
- - import_issues_csv
- 2
- - incident_management
@@ -411,8 +415,6 @@
- 1
- - llm_namespace_access_cache_reset
- 1
-- - llm_tanuki_bot_update
- - 1
- - llm_vertex_ai_access_token_refresh
- 1
- - mail_scheduler
@@ -471,6 +473,8 @@
- 1
- - ml_experiment_tracking_associate_ml_candidate_to_package
- 1
+- - namespaces_free_user_cap_group_over_limit_notification
+ - 1
- - namespaces_process_sync_events
- 1
- - namespaces_storage_usage_export
@@ -505,6 +509,8 @@
- 1
- - package_metadata_advisory_scan
- 1
+- - package_metadata_global_advisory_scan
+ - 1
- - package_repositories
- 1
- - packages_composer_cache_update
@@ -557,6 +563,8 @@
- 1
- - projects_git_garbage_collect
- 1
+- - projects_import_export_after_import_merge_requests
+ - 1
- - projects_import_export_create_relation_exports
- 1
- - projects_import_export_parallel_project_export
@@ -659,8 +667,6 @@
- 1
- - security_scan_result_policies_sync_any_merge_request_approval_rules
- 1
-- - security_scan_result_policies_sync_opened_merge_requests
- - 1
- - security_scan_result_policies_sync_project
- 1
- - security_scans
@@ -689,8 +695,6 @@
- 1
- - system_hook_push
- 1
-- - tasks_to_be_done_create
- - 1
- - terraform
- 1
- - todos_destroyer
diff --git a/config/webpack.config.js b/config/webpack.config.js
index 8b45b25a328..2977b1ce712 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -184,11 +184,6 @@ function generateEntries() {
const alias = {
// Map Apollo client to apollo/client/core to prevent react related imports from being loaded
'@apollo/client$': '@apollo/client/core',
- // Map Sentry calls to use local wrapper
- '@sentry/browser$': path.join(
- ROOT_PATH,
- 'app/assets/javascripts/sentry/sentry_browser_wrapper.js',
- ),
'~': path.join(ROOT_PATH, 'app/assets/javascripts'),
emojis: path.join(ROOT_PATH, 'fixtures/emojis'),
images: path.join(ROOT_PATH, 'app/assets/images'),
@@ -388,6 +383,15 @@ module.exports = {
loader: 'babel-loader',
},
{
+ test: /swagger-ui-dist\/.*\.js?$/,
+ include: /node_modules/,
+ loader: 'babel-loader',
+ options: {
+ plugins: ['@babel/plugin-proposal-logical-assignment-operators'],
+ ...defaultJsOptions,
+ },
+ },
+ {
test: /\.(js|cjs)$/,
exclude: shouldExcludeFromCompliling,
use: [
diff --git a/danger/analytics_instrumentation/Dangerfile b/danger/analytics_instrumentation/Dangerfile
index bbb984939dc..6b2139adab9 100644
--- a/danger/analytics_instrumentation/Dangerfile
+++ b/danger/analytics_instrumentation/Dangerfile
@@ -7,3 +7,5 @@ analytics_instrumentation.check!
analytics_instrumentation.check_affected_scopes!
analytics_instrumentation.check_usage_data_insertions!
+
+analytics_instrumentation.check_deprecated_data_sources!
diff --git a/danger/change_column_default/Dangerfile b/danger/change_column_default/Dangerfile
new file mode 100644
index 00000000000..143943dd942
--- /dev/null
+++ b/danger/change_column_default/Dangerfile
@@ -0,0 +1,3 @@
+# frozen_string_literal: true
+
+change_column_default.add_comment_for_change_column_default
diff --git a/danger/ci_tables/Dangerfile b/danger/ci_tables/Dangerfile
index 1d4601d33b2..422b77d337a 100644
--- a/danger/ci_tables/Dangerfile
+++ b/danger/ci_tables/Dangerfile
@@ -18,7 +18,7 @@ def check_database_dictionary_yaml(database_dictionary)
markdown(PARTITIONING_COMMENT, file: database_dictionary.path, line: mr_line.succ)
rescue Psych::Exception
# YAML could not be parsed, fail the build.
- fail "#{helper.html_link(database_ditionary.path)} isn't valid YAML! #{SEE_DB_DOC}" # rubocop:disable Style/SignalException
+ fail "#{helper.html_link(database_ditionary.path)} isn't valid YAML! #{SEE_DB_DOC}"
rescue StandardError => e
warn "There was a problem trying to check the database dictionary file. Exception: #{e.class.name} - #{e.message}"
end
diff --git a/danger/database/Dangerfile b/danger/database/Dangerfile
index f3bdddb0d95..b7f2151dcf9 100644
--- a/danger/database/Dangerfile
+++ b/danger/database/Dangerfile
@@ -74,7 +74,7 @@ return if helper.mr_labels.include?(DATABASE_APPROVED_LABEL)
migration_testing_has_run = helper.mr_labels.include?(DATABASE_TESTING_RUN_LABEL)
community_contribution = helper.mr_labels.include?(COMMUNITY_CONTRIBUTION_LABEL)
if non_geo_migration_created && !migration_testing_has_run && !community_contribution
- fail DB_MIGRATION_TESTING_REQUIRED_MESSAGE # rubocop:disable Style/SignalException
+ fail DB_MIGRATION_TESTING_REQUIRED_MESSAGE
end
if helper.mr_labels.include?('database') || database.changes.any?
diff --git a/danger/documentation/Dangerfile b/danger/documentation/Dangerfile
index 150109eff51..78f8c87a528 100644
--- a/danger/documentation/Dangerfile
+++ b/danger/documentation/Dangerfile
@@ -19,6 +19,13 @@ MSG
docs_paths_to_review = helper.changes_by_category[:docs]
+# Some docs do not need a review from a technical writer
+SKIP_TW_REVIEW_PATHS = ['doc/solutions'].freeze
+
+docs_paths_to_review.delete_if do |item|
+ SKIP_TW_REVIEW_PATHS.any? { |skip_path| item.start_with?(skip_path) }
+end
+
# Documentation should be updated for feature::addition and feature::enhancement
if docs_paths_to_review.empty?
warn(DOCUMENTATION_UPDATE_MISSING) if feature_mr?
diff --git a/danger/experiments/Dangerfile b/danger/experiments/Dangerfile
index 3a206bc876e..c23f94e8d94 100644
--- a/danger/experiments/Dangerfile
+++ b/danger/experiments/Dangerfile
@@ -3,5 +3,5 @@
unless experiments.class_files_removed?
msg = "This merge request removes experiment: `#{experiments.removed_experiments.join(',')}`" \
", please also remove the class file."
- fail msg # rubocop:disable Style/SignalException
+ fail msg
end
diff --git a/danger/feature_flag/Dangerfile b/danger/feature_flag/Dangerfile
index 68e6cadd04e..d44e83dbe53 100644
--- a/danger/feature_flag/Dangerfile
+++ b/danger/feature_flag/Dangerfile
@@ -1,5 +1,4 @@
# frozen_string_literal: true
-# rubocop:disable Style/SignalException
SEE_DOC = "See the [feature flag documentation](https://docs.gitlab.com/ee/development/feature_flags#feature-flag-definition-and-validation)."
FEATURE_FLAG_LABEL = "feature flag"
diff --git a/danger/gitaly/Dangerfile b/danger/gitaly/Dangerfile
index d7ff8d6446a..228ed3fc192 100644
--- a/danger/gitaly/Dangerfile
+++ b/danger/gitaly/Dangerfile
@@ -1,17 +1,28 @@
# frozen_string_literal: true
GITALY_COORDINATION_MESSAGE = <<~MSG
-This merge request requires coordination with gitaly deployments.
-Before merging this merge request we should verify that gitaly
-running in production already implements the new gRPC interface
-included here.
+## Changing Gitaly version
-Failing to do so will introduce a [non backward compatible
-change](https://docs.gitlab.com/ee/development/multi_version_compatibility.html)
-during canary depoyment that can cause an incident.
+This merge request requires coordination with Gitaly deployments. You must assert why this change is safe.
-1. Identify the gitaly MR introducing the new interface
-1. Verify that the environment widget contains a `gprd` deployment
+If these two assertions can be made, then this change is safe:
+
+1. No Gitaly definitions that have been removed in the version bump are in use on the Rails side.
+1. No Gitaly definitions that are not yet part of a released version become used without a feature flag.
+
+In general, we can ignore the first assertion because the specs will fail as needed. If a GitLab Rails spec
+exercises a definition that is removed in the new Gitaly version, then that
+spec will fail.
+
+You must confirm the second assertion. Failing to do so will introduce a [non
+backward compatible change](https://docs.gitlab.com/ee/development/multi_version_compatibility.html),
+for example during canary deployment of GitLab.com, which can cause an incident.
+This type of problem can also impact customers performing zero-downtime upgrades.
+Some options:
+
+- This change does not cause Rails to use a new definition.
+- This change causes Rails to use a new definition, but only behind a feature flag which is disabled by default.
+ This feature flag must only be removed in a subsequent release.
MSG
changed_lines = helper.changed_lines('Gemfile.lock')
diff --git a/danger/gitlab_schema_validation/Dangerfile b/danger/gitlab_schema_validation/Dangerfile
new file mode 100644
index 00000000000..3d44ad592ae
--- /dev/null
+++ b/danger/gitlab_schema_validation/Dangerfile
@@ -0,0 +1,3 @@
+# frozen_string_literal: true
+
+gitlab_schema_validation.add_suggestions_on_using_clusterwide_schema
diff --git a/danger/pajamas/Dangerfile b/danger/pajamas/Dangerfile
index 5fe9e9e8b19..cb13052c1e2 100644
--- a/danger/pajamas/Dangerfile
+++ b/danger/pajamas/Dangerfile
@@ -1,5 +1,4 @@
# frozen_string_literal: true
-# rubocop:disable Style/SignalException
PATTERNS = %w[
%a.btn.btn-
diff --git a/danger/plugins/change_column_default.rb b/danger/plugins/change_column_default.rb
new file mode 100644
index 00000000000..7f96b94f923
--- /dev/null
+++ b/danger/plugins/change_column_default.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require_relative '../../tooling/danger/change_column_default'
+
+module Danger
+ class ChangeColumnDefault < ::Danger::Plugin
+ include Tooling::Danger::ChangeColumnDefault
+ end
+end
diff --git a/danger/plugins/gitlab_schema_validation.rb b/danger/plugins/gitlab_schema_validation.rb
new file mode 100644
index 00000000000..ca4bc1a12be
--- /dev/null
+++ b/danger/plugins/gitlab_schema_validation.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require_relative '../../tooling/danger/gitlab_schema_validation_suggestion'
+
+module Danger
+ class GitlabSchemaValidation < ::Danger::Plugin
+ include Tooling::Danger::GitlabSchemaValidationSuggestion
+ end
+end
diff --git a/danger/plugins/todos.rb b/danger/plugins/todos.rb
new file mode 100644
index 00000000000..b31f147f2af
--- /dev/null
+++ b/danger/plugins/todos.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require_relative '../../tooling/danger/outdated_todo'
+
+module Danger
+ class Todos < ::Danger::Plugin
+ def check_outdated_todos(filenames)
+ Tooling::Danger::OutdatedTodo.new(filenames, context: self).check
+ end
+ end
+end
diff --git a/danger/rubocop/Dangerfile b/danger/rubocop/Dangerfile
index a53847199db..41131241691 100644
--- a/danger/rubocop/Dangerfile
+++ b/danger/rubocop/Dangerfile
@@ -1,5 +1,10 @@
# frozen_string_literal: true
+# Danger should not comment when inline disables are added in the following files.
+no_suggestions_for_extensions = %w[.md]
+
helper.all_changed_files.each do |filename|
+ next if filename.end_with?(*no_suggestions_for_extensions)
+
rubocop.add_suggestions_for(filename)
end
diff --git a/danger/saas_feature/Dangerfile b/danger/saas_feature/Dangerfile
index 5b1eed7078c..38ca87fb5fd 100644
--- a/danger/saas_feature/Dangerfile
+++ b/danger/saas_feature/Dangerfile
@@ -1,11 +1,6 @@
# frozen_string_literal: true
-# rubocop:disable Style/SignalException
-
SEE_DOC = "see the [SaaS feature documentation](https://docs.gitlab.com/ee/development/ee_features.html#saas-only-feature)."
-LABEL = "saas_feature"
-EXISTS_LABEL = "#{LABEL}::exists".freeze
-SKIPPED_LABEL = "#{LABEL}::skipped".freeze
SUGGEST_MR_COMMENT = <<~SUGGEST_COMMENT.freeze
```suggestion
@@ -15,12 +10,6 @@ SUGGEST_MR_COMMENT = <<~SUGGEST_COMMENT.freeze
#{SEE_DOC.capitalize}
SUGGEST_COMMENT
-ENFORCEMENT_WARNING = <<~WARNING_MESSAGE.freeze
- There were no new or modified SaaS feature YAML files detected in this MR.
-
- For guidance on when to use a SaaS feature, please #{SEE_DOC}
-WARNING_MESSAGE
-
def check_yaml(saas_feature)
mr_group_label = helper.group_label
@@ -63,42 +52,6 @@ def added_files
saas_feature.files(change_type: :added)
end
-def modified_files
- saas_feature.files(change_type: :modified)
-end
-
-def file_added?
- added_files.any?
-end
-
-def file_modified?
- modified_files.any?
-end
-
-def file_added_or_modified?
- file_added? || file_modified?
-end
-
-def mr_has_backend_or_frontend_changes?
- changes = helper.changes_by_category
- changes.has_key?(:backend) || changes.has_key?(:frontend)
-end
-
-def mr_missing_status_label?
- helper.mr_labels.none? { |label| label.start_with?(LABEL) }
-end
-
added_files.each do |saas_feature|
check_yaml(saas_feature)
end
-
-if !helper.security_mr? && mr_has_backend_or_frontend_changes?
- if file_added_or_modified? && !helper.mr_has_labels?(EXISTS_LABEL)
- # SaaS feature config file touched in this MR, so let's add the label to avoid the warning.
- helper.labels_to_add << EXISTS_LABEL
- end
-
- warn ENFORCEMENT_WARNING if mr_missing_status_label?
-end
-
-# rubocop:enable Style/SignalException
diff --git a/danger/todos/Dangerfile b/danger/todos/Dangerfile
new file mode 100644
index 00000000000..45494e59bac
--- /dev/null
+++ b/danger/todos/Dangerfile
@@ -0,0 +1,3 @@
+# frozen_string_literal: true
+
+todos.check_outdated_todos(git.deleted_files)
diff --git a/data/deprecations/ 16_3_runner-terminationgracepriodseconds.yml b/data/deprecations/ 16_3_runner-terminationgracepriodseconds.yml
deleted file mode 100644
index 032032b2196..00000000000
--- a/data/deprecations/ 16_3_runner-terminationgracepriodseconds.yml
+++ /dev/null
@@ -1,22 +0,0 @@
-- title: "Deprecate `terminationGracePeriodSeconds` in the GitLab Runner Kubernetes executor" # (required) Clearly explain the change, or planned change. For example, "The `confidential` field for a `Note` is deprecated" or "CI/CD job names will be limited to 250 characters."
- removal_milestone: "17.0" # (required) The milestone when this feature is planned to be removed
- announcement_milestone: "16.3" # (required) The milestone when this feature was first announced as deprecated.
- breaking_change: false # (required) Change to false if this is not a breaking change.
- reporter: deastman # (required) GitLab username of the person reporting the change
- stage: stage # (required) String value of the stage that the feature was created in. e.g., Growth
- issue_url: "https://gitlab.com/gitlab-org/gitlab-runner/-/issues/28165" # (required) Link to the deprecation issue in GitLab
- body: | # (required) Do not modify this line, instead modify the lines below.
- The GitLab Runner Kubernetes executor setting, `terminationGracePeriodSeconds`, is deprecated and will be removed in GitLab 17.0. To manage the cleanup and termination of GitLab Runner worker pods on Kubernetes, customers should instead configure `cleanupGracePeriodSeconds` and `podTerminationGracePeriodSeconds`. For information about how to use the `cleanupGracePeriodSeconds` and `podTerminationGracePeriodSeconds, see the [GitLab Runner Executor documentation](https://docs.gitlab.com/runner/executors/kubernetes.html#other-configtoml-settings).
-
-#
-# OPTIONAL END OF SUPPORT FIELDS
-#
-# If an End of Support period applies, the announcement should be shared with GitLab Support
-# in the `#spt_managers` channel in Slack, and mention `@gitlab-com/support` in this MR.
-#
- end_of_support_milestone: 17.0 # (optional) Use "XX.YY" format. The milestone when support for this feature will end.
- #
- # OTHER OPTIONAL FIELDS
- #
- tiers: [Free, Premium, Ultimate] # (optional - may be required in the future) An array of tiers that the feature is available in currently. e.g., [Free, Silver, Gold, Core, Premium, Ultimate]
- documentation_url: "https://docs.gitlab.com/runner/executors/kubernetes.html" # (optional) This is a link to the current documentation page
diff --git a/data/deprecations/15.8-kas-private-tls.yml b/data/deprecations/15-8-kas-private-tls.yml
index 3d1a6e7830f..3d1a6e7830f 100644
--- a/data/deprecations/15.8-kas-private-tls.yml
+++ b/data/deprecations/15-8-kas-private-tls.yml
diff --git a/data/deprecations/15-9-database-single-database-connection-conf.yml b/data/deprecations/15-9-database-single-database-connection-conf.yml
index de4ae51d615..c7d59e860ca 100644
--- a/data/deprecations/15-9-database-single-database-connection-conf.yml
+++ b/data/deprecations/15-9-database-single-database-connection-conf.yml
@@ -6,8 +6,6 @@
stage: Enablement
issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/387898
body: |
- This deprecation is now superseded by another [deprecation notice](#running-a-single-database-is-deprecated).
-
Previously, [GitLab's database](https://docs.gitlab.com/omnibus/settings/database.html)
configuration had a single `main:` section. This is being deprecated. The new
configuration has both a `main:` and a `ci:` section.
diff --git a/data/deprecations/16-0-deprecate-omnibus-grafana.yml b/data/deprecations/16-0-deprecate-omnibus-grafana.yml
index 61c04e9f042..4035c3ce44a 100644
--- a/data/deprecations/16-0-deprecate-omnibus-grafana.yml
+++ b/data/deprecations/16-0-deprecate-omnibus-grafana.yml
@@ -19,5 +19,5 @@
In GitLab versions 16.0 to 16.2, you can still [re-enable the bundled Grafana](https://docs.gitlab.com/ee/administration/monitoring/performance/grafana_configuration.html#temporary-workaround).
However, enabling the bundled Grafana will no longer work from GitLab 16.3.
end_of_support_milestone:
- tiers: # (optional - may be required in the future) An array of tiers that the feature is available in currently. e.g., [Free, Silver, Gold, Core, Premium, Ultimate]
+ tiers: # (optional - may be required in the future) An array of tiers that the feature is available in currently. e.g., [Free, Silver, Gold, Core, Premium, Ultimate]
documentation_url: https://docs.gitlab.com/omnibus/settings/grafana.html
diff --git a/data/deprecations/16.0-eol-windows-server-2004-and-20H2.yml b/data/deprecations/16-0-eol-windows-server-2004-and-20H2.yml
index 267304f6a13..267304f6a13 100644
--- a/data/deprecations/16.0-eol-windows-server-2004-and-20H2.yml
+++ b/data/deprecations/16-0-eol-windows-server-2004-and-20H2.yml
diff --git a/data/deprecations/16-1-non-decomposed-mode-deprecation.yml b/data/deprecations/16-1-non-decomposed-mode-deprecation.yml
index 83f99fc2dbd..d98c4f65504 100644
--- a/data/deprecations/16-1-non-decomposed-mode-deprecation.yml
+++ b/data/deprecations/16-1-non-decomposed-mode-deprecation.yml
@@ -1,12 +1,12 @@
- title: "Running a single database is deprecated"
- removal_milestone: "17.0"
+ removal_milestone: "18.0"
announcement_milestone: "16.1"
breaking_change: true
reporter: lohrc
stage: data_stores
issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/411239
body: |
- From GitLab 17.0, we will require a [separate database for CI features](https://gitlab.com/groups/gitlab-org/-/epics/7509).
+ From GitLab 18.0, we will require a [separate database for CI features](https://gitlab.com/groups/gitlab-org/-/epics/7509).
We recommend running both databases on the same Postgres instance(s) due to ease of management for most deployments.
We are providing this as an informational advance notice but we do not recommend taking action yet.
@@ -14,4 +14,4 @@
This change provides additional scalability for the largest of GitLab instances, like GitLab.com.
This change applies to all installation methods: Omnibus GitLab, GitLab Helm chart, GitLab Operator, GitLab Docker images, and installation from source.
- Before upgrading to GitLab 17.0, please ensure you have [migrated](https://docs.gitlab.com/ee/administration/postgresql/multiple_databases.html) to two databases.
+ Before upgrading to GitLab 18.0, please ensure you have [migrated](https://docs.gitlab.com/ee/administration/postgresql/multiple_databases.html) to two databases.
diff --git a/data/deprecations/16-1-unified-approval-rules.yml b/data/deprecations/16-1-unified-approval-rules.yml
index 8622ed86d5f..802daded7f7 100644
--- a/data/deprecations/16-1-unified-approval-rules.yml
+++ b/data/deprecations/16-1-unified-approval-rules.yml
@@ -24,6 +24,6 @@
# OTHER OPTIONAL FIELDS
#
tiers: [Premium, Ultimate] # (optional - may be required in the future) An array of tiers that the feature is available in currently. e.g., [Free, Silver, Gold, Core, Premium, Ultimate]
- documentation_url: https://docs.gitlab.com/ee/ci/environments/deployment_approvals.html # (optional) This is a link to the current documentation page
+ documentation_url: https://docs.gitlab.com/ee/ci/environments/deployment_approvals.html # (optional) This is a link to the current documentation page
image_url: # (optional) This is a link to a thumbnail image depicting the feature
video_url: # (optional) Use the youtube thumbnail URL with the structure of https://img.youtube.com/vi/UNIQUEID/hqdefault.jpg
diff --git a/data/deprecations/16-1-windows-cmd-runner-shell-executor.yml b/data/deprecations/16-1-windows-cmd-runner-shell-executor.yml
index c4c2435ba6f..12334f764b7 100644
--- a/data/deprecations/16-1-windows-cmd-runner-shell-executor.yml
+++ b/data/deprecations/16-1-windows-cmd-runner-shell-executor.yml
@@ -2,12 +2,12 @@
announcement_milestone: "16.1" # (required) The milestone when this feature was first announced as deprecated.
announcement_date: "2023-06-22" # (required) The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post.
removal_milestone: "17.0" # (required) The milestone when this feature is planned to be removed
- removal_date: "2024-05-22" # (required) The date of the milestone release when this feature is planned to be removed. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post.
+ removal_date: "2024-05-22" # (required) The date of the milestone release when this feature is planned to be removed. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post.
breaking_change: true # (required) If this deprecation is a breaking change, set this value to true
reporter: DarrenEastman # (required) GitLab username of the person reporting the deprecation
- stage: Verify # (required) String value of the stage that the feature was created in. e.g., Growth
- issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/414864 # (required) Link to the deprecation issue in GitLab
+ stage: Verify # (required) String value of the stage that the feature was created in. e.g., Growth
+ issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/414864 # (required) Link to the deprecation issue in GitLab
body: | # (required) Do not modify this line, instead modify the lines below.
In GitLab 11.11 the Windows Batch executor, the CMD shell was deprecated in GitLab Runner in favor of PowerShell. Since then, the CMD shell has continued to be supported in GitLab Runner. However this has resulted in additional complexity for both the engineering team and customers using the Runner on Windows. We plan to fully remove support for Windows CMD from GitLab Runner in 17.0. Customers should plan to use PowerShell when using the runner on Windows with the shell executor. Customers can provide feedback or ask questions in the removal issue, [issue 29479](https://gitlab.com/gitlab-org/gitlab-runner/-/issues/29479).
end_of_support_milestone: # (optional) Use "XX.YY" format. The milestone when support for this feature will end.
- end_of_support_date: "2024-05-22" # (optional) The date of the milestone release when support for this feature will end.
+ end_of_support_date: "2024-05-22" # (optional) The date of the milestone release when support for this feature will end.
diff --git a/data/deprecations/16-2-custom_sign_in_fields.yml b/data/deprecations/16-2-custom_sign_in_fields.yml
new file mode 100644
index 00000000000..e9931ee5dce
--- /dev/null
+++ b/data/deprecations/16-2-custom_sign_in_fields.yml
@@ -0,0 +1,11 @@
+- title: 'Deprecated parameters related to custom text in the sign-in page' # (required) The name of the feature to be deprecated
+ announcement_milestone: '16.2' # (required) The milestone when this feature was first announced as deprecated.
+ announcement_date: '2023-07-22' # (required) The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post.
+ removal_milestone: '17.0' # (required) The milestone when this feature is planned to be removed
+ removal_date: '2024-04-22' # (required) The date of the milestone release when this feature is planned to be removed. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post.
+ breaking_change: true # (required) If this deprecation is a breaking change, set this value to true
+ reporter: eduardosanz # (required) GitLab username of the person reporting the deprecation
+ stage: Manage # (required) String value of the stage that the feature was created in. e.g., Growth
+ issue_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/124461 # (required) Link to the deprecation issue in GitLab
+ body: | # (required) Do not modify this line, instead modify the lines below.
+ The parameters, `sign_in_text` and `help_text`, are deprecated in the [Settings API](https://docs.gitlab.com/ee/api/settings.html). To add a custom text to the sign-in and sign-up pages, use the `description` field in the [Appearance API](https://docs.gitlab.com/ee/api/appearance.html).
diff --git a/data/deprecations/16-3-geo-housekeeping-rake-tasks.yml b/data/deprecations/16-3-geo-housekeeping-rake-tasks.yml
new file mode 100644
index 00000000000..a791837df7a
--- /dev/null
+++ b/data/deprecations/16-3-geo-housekeeping-rake-tasks.yml
@@ -0,0 +1,22 @@
+- title: "Geo: Housekeeping Rake tasks"
+ removal_milestone: "16.5"
+ announcement_milestone: "16.3"
+ breaking_change: true
+ reporter: sranasinghe
+ stage: enablement
+ issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/416384
+ body: |
+ As part of the migration of the replication and verification to the
+ [Geo self-service framework (SSF)](https://docs.gitlab.com/ee/development/geo/framework.html),
+ the legacy replication for project repositories has been
+ [removed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/130565).
+ As a result, the following Rake tasks that relied on legacy code have also been removed. The work invoked by these Rake tasks are now triggered automatically either periodically or based on trigger events.
+
+ | Rake task | Replacement |
+ | --------- | ----------- |
+ | `geo:git:housekeeping:full_repack` | [Moved to UI](https://docs.gitlab.com/ee/administration/housekeeping.html#heuristical-housekeeping). No equivalent Rake task in the SSF. |
+ | `geo:git:housekeeping:gc` | Always executed for new repositories, and then when it's needed. No equivalent Rake task in the SSF. |
+ | `geo:git:housekeeping:incremental_repack` | Executed when needed. No equivalent Rake task in the SSF. |
+ | `geo:run_orphaned_project_registry_cleaner` | Executed regularly by a registry [consistency worker](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/app/workers/geo/secondary/registry_consistency_worker.rb) which removes orphaned registries. No equivalent Rake task in the SSF. |
+ | `geo:verification:repository:reset` | Moved to UI. No equivalent Rake task in the SSF. |
+ | `geo:verification:wiki:reset` | Moved to UI. No equivalent Rake task in the SSF. |
diff --git a/data/deprecations/16-3-runner-terminationgracepriodseconds.yml b/data/deprecations/16-3-runner-terminationgracepriodseconds.yml
new file mode 100644
index 00000000000..bdb54ab3981
--- /dev/null
+++ b/data/deprecations/16-3-runner-terminationgracepriodseconds.yml
@@ -0,0 +1,22 @@
+- title: "Deprecate `terminationGracePeriodSeconds` in the GitLab Runner Kubernetes executor" # (required) Clearly explain the change, or planned change. For example, "The `confidential` field for a `Note` is deprecated" or "CI/CD job names will be limited to 250 characters."
+ removal_milestone: "17.0" # (required) The milestone when this feature is planned to be removed
+ announcement_milestone: "16.3" # (required) The milestone when this feature was first announced as deprecated.
+ breaking_change: false # (required) Change to false if this is not a breaking change.
+ reporter: deastman # (required) GitLab username of the person reporting the change
+ stage: stage # (required) String value of the stage that the feature was created in. e.g., Growth
+ issue_url: "https://gitlab.com/gitlab-org/gitlab-runner/-/issues/28165" # (required) Link to the deprecation issue in GitLab
+ body: | # (required) Do not modify this line, instead modify the lines below.
+ The GitLab Runner Kubernetes executor setting, `terminationGracePeriodSeconds`, is deprecated and will be removed in GitLab 17.0. To manage the cleanup and termination of GitLab Runner worker pods on Kubernetes, customers should instead configure `cleanupGracePeriodSeconds` and `podTerminationGracePeriodSeconds`. For information about how to use the `cleanupGracePeriodSeconds` and `podTerminationGracePeriodSeconds, see the [GitLab Runner Executor documentation](https://docs.gitlab.com/runner/executors/kubernetes.html#other-configtoml-settings).
+
+#
+# OPTIONAL END OF SUPPORT FIELDS
+#
+# If an End of Support period applies, the announcement should be shared with GitLab Support
+# in the `#spt_managers` channel in Slack, and mention `@gitlab-com/support` in this MR.
+#
+ end_of_support_milestone: 17.0 # (optional) Use "XX.YY" format. The milestone when support for this feature will end.
+ #
+ # OTHER OPTIONAL FIELDS
+ #
+ tiers: [Free, Premium, Ultimate] # (optional - may be required in the future) An array of tiers that the feature is available in currently. e.g., [Free, Silver, Gold, Core, Premium, Ultimate]
+ documentation_url: "https://docs.gitlab.com/runner/executors/kubernetes.html" # (optional) This is a link to the current documentation page
diff --git a/data/deprecations/16-4-ci_job_token_scope_enabled-attribute-deprecation.yml b/data/deprecations/16-4-ci_job_token_scope_enabled-attribute-deprecation.yml
index 57e42f17cc8..698e5c1a7c9 100644
--- a/data/deprecations/16-4-ci_job_token_scope_enabled-attribute-deprecation.yml
+++ b/data/deprecations/16-4-ci_job_token_scope_enabled-attribute-deprecation.yml
@@ -7,7 +7,7 @@
breaking_change: true # (required) Change to false if this is not a breaking change.
reporter: jocelynjane # (required) GitLab username of the person reporting the change
stage: verify # (required) String value of the stage that the feature was created in. e.g., Growth
- issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/423091 # (required) Link to the deprecation issue in GitLab
+ issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/423091 # (required) Link to the deprecation issue in GitLab
body: | # (required) Do not modify this line, instead modify the lines below.
GitLab 16.1 introduced [API endpoints for the job token scope](https://gitlab.com/gitlab-org/gitlab/-/issues/351740). In the [projects API](https://docs.gitlab.com/ee/api/projects.html), the `ci_job_token_scope_enabled` attribute is deprecated, and will be removed in 17.0. You should use the [job token scope APIs](https://docs.gitlab.com/ee/api/project_job_token_scopes.html) instead.
#
diff --git a/data/deprecations/16-5-container-registry-support-storage-drivers-swift-oss.yml b/data/deprecations/16-5-container-registry-support-storage-drivers-swift-oss.yml
new file mode 100644
index 00000000000..0d7364d05ff
--- /dev/null
+++ b/data/deprecations/16-5-container-registry-support-storage-drivers-swift-oss.yml
@@ -0,0 +1,13 @@
+- title: "Container Registry support for the Swift and OSS storage drivers"
+ announcement_milestone: "16.6"
+ removal_milestone: "17.0"
+ breaking_change: true
+ reporter: trizzi
+ stage: Package
+ issue_url: https://gitlab.com/gitlab-org/container-registry/-/issues/1141
+ body: |
+ The container registry uses storage drivers to work with various object storage platforms. While each driver's code is relatively self-contained, there is a high maintenance burden for these drivers. Each driver implementation is unique and making changes to a driver requires a high level of domain expertise with that specific driver.
+
+ As we look to reduce maintenance costs, we are deprecating support for OSS (Object Storage Service) and OpenStack Swift. Both have already been removed from the upstream Docker Distribution. This helps align the container registry with the broader GitLab product offering with regards to [object storage support](https://docs.gitlab.com/ee/administration/object_storage.html#supported-object-storage-providers).
+
+ OSS has an [S3 compatibility mode](https://www.alibabacloud.com/help/en/oss/developer-reference/compatibility-with-amazon-s3), so consider using that if you can't migrate to a supported driver. Swift is [compatible with S3 API operations](https://docs.openstack.org/swift/latest/s3_compat.html), required by the S3 storage driver as well.
diff --git a/data/deprecations/16-6-deprecation-legacy-geo-prometheus-metrics.yml b/data/deprecations/16-6-deprecation-legacy-geo-prometheus-metrics.yml
new file mode 100644
index 00000000000..59db93293be
--- /dev/null
+++ b/data/deprecations/16-6-deprecation-legacy-geo-prometheus-metrics.yml
@@ -0,0 +1,22 @@
+- title: "Legacy Geo Prometheus metrics"
+ removal_milestone: "17.0"
+ announcement_milestone: "16.6"
+ breaking_change: true
+ reporter: sranasinghe
+ stage: enablement
+ issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/430192
+ body: |
+ Following the migration of projects to the [Geo self-service framework](https://docs.gitlab.com/ee/development/geo/framework.html) we have deprecated a number of [Prometheus](https://docs.gitlab.com/ee/administration/monitoring/prometheus/) metrics.
+ The following Geo-related Prometheus metrics are deprecated and will be removed in 17.0.
+ The table below lists the deprecated metrics and their respective replacements. The replacements are available in GitLab 16.3.0 and later.
+
+ | Deprecated metric | Replacement metric |
+ | ---------------------------------------- | ---------------------------------------------- |
+ | `geo_repositories_synced` | `geo_project_repositories_synced` |
+ | `geo_repositories_failed` | `geo_project_repositories_failed` |
+ | `geo_repositories_checksummed` | `geo_project_repositories_checksummed` |
+ | `geo_repositories_checksum_failed` | `geo_project_repositories_checksum_failed` |
+ | `geo_repositories_verified` | `geo_project_repositories_verified` |
+ | `geo_repositories_verification_failed` | `geo_project_repositories_verification_failed` |
+ | `geo_repositories_checksum_mismatch` | None available |
+ | `geo_repositories_retrying_verification` | None available |
diff --git a/data/deprecations/16-6-file-type-variable-extension-deprecation.yml b/data/deprecations/16-6-file-type-variable-extension-deprecation.yml
new file mode 100644
index 00000000000..b8fc9620a88
--- /dev/null
+++ b/data/deprecations/16-6-file-type-variable-extension-deprecation.yml
@@ -0,0 +1,13 @@
+- title: "File type variable expansion fixed in downstream pipelines"
+ removal_milestone: "17.0" # (required) The milestone when this feature is planned to be removed
+ announcement_milestone: "16.6" # (required) The milestone when this feature was first announced as deprecated.
+ breaking_change: true # (required) Change to false if this is not a breaking change.
+ reporter: jocelynjane # (required) GitLab username of the person reporting the change
+ stage: Verify # (required) String value of the stage that the feature was created in. e.g., Growth
+ issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/419445 # (required) Link to the deprecation issue in GitLab
+ body: | # (required) Do not modify this line, instead modify the lines below.
+ Previously, if you tried to reference a [file type CI/CD variable](https://docs.gitlab.com/ee/ci/variables/#use-file-type-cicd-variables) in another CI/CD variable, the CI/CD variable would expand to contain the contents of the file. This behavior was incorrect because it did not comply with typical shell variable expansion rules. The CI/CD variable reference should expand to only contain the path to the file, not the contents of the file itself. This was [fixed for most use cases in GitLab 15.7](https://gitlab.com/gitlab-org/gitlab/-/issues/29407). Unfortunately, passing CI/CD variables to downstream pipelines was an edge case not yet fixed, but which will now be fixed in GitLab 17.0.
+
+ With this change, a variable configured in the `.gitlab-ci.yml` file can reference a file variable and be passed to a downstream pipeline, and the file variable will be passed to the downstream pipeline as well. The downstream pipeline will expand the variable reference to the file path, not the file contents.
+
+ This breaking change could disrupt user workflows that depend on expanding a file variable in a downstream pipeline.
diff --git a/data/deprecations/16-6-lfs-integrity-check-feature-flag-deprecation.yml b/data/deprecations/16-6-lfs-integrity-check-feature-flag-deprecation.yml
new file mode 100644
index 00000000000..8e6088dc4e4
--- /dev/null
+++ b/data/deprecations/16-6-lfs-integrity-check-feature-flag-deprecation.yml
@@ -0,0 +1,13 @@
+- title: "Deprecation of `lfs_check` feature flag" # (required) Clearly explain the change, or planned change. For example, "The `confidential` field for a `Note` is deprecated" or "CI/CD job names will be limited to 250 characters."
+ removal_milestone: "16.9" # (required) The milestone when this feature is planned to be removed
+ announcement_milestone: "16.6" # (required) The milestone when this feature was first announced as deprecated.
+ breaking_change: false # (required) Change to false if this is not a breaking change.
+ reporter: derekferguson # (required) GitLab username of the person reporting the change
+ stage: Create # (required) String value of the stage that the feature was created in. e.g., Growth
+ issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/233550 # (required) Link to the deprecation issue in GitLab
+ body: | # (required) Do not modify this line, instead modify the lines below.
+ In GitLab 16.9, we will remove the `lfs_check` feature flag. This feature flag was [introduced 4 years ago](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/60588) and controls whether the LFS integrity check is enabled. The feature flag is enabled by default, but some customers experienced performance issues with the LFS integrity check and explicitly disabled it.
+
+ After [dramatically improving the performance](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/61355) of the LFS integrity check, we are ready to remove the feature flag. After the flag is removed, the feature will automatically be turned on for any environment in which it is currently disabled.
+
+ If this feature flag is disabled for your environment, and you are concerned about performance issues, please enable it and monitor the performance before it is removed in 16.9. If you see any performance issues after enabling it, please let us know in [this feedback issue](https://gitlab.com/gitlab-org/gitlab/-/issues/233550).
diff --git a/data/deprecations/16-6-maven-group-permissions.yml b/data/deprecations/16-6-maven-group-permissions.yml
new file mode 100644
index 00000000000..f22aff107f9
--- /dev/null
+++ b/data/deprecations/16-6-maven-group-permissions.yml
@@ -0,0 +1,15 @@
+- title: "Breaking change to the Maven repository group permissions"
+ announcement_milestone: "16.6"
+ removal_milestone: "17.0"
+ breaking_change: true
+ reporter: trizzi
+ stage: Package
+ issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/393933
+ body: |
+ The Maven repository exposes an API endpoint at the group level that allows Maven clients to download files from a specific package. The package finder first locates the package within the group, and then finds the file within the package.
+ However, there is a limitation that affects duplicate package names hosted in different projects. The Maven package finder always returns the most recent package, but the "most recent" filter depends on user permissions. It is possible for a user with different permissions in different projects to download the wrong Maven package.
+
+ In GitLab 17.0, the package finder logic will be fixed so that the "most recent" package is the last updated name and version of a package in a group. User permissions will be checked after the most recent package is found.
+ After the change, download requests for users without correct permissions will be rejected. If your workflow depends on the current bugged behavior, this fix will introduce a breaking change.
+
+ The change will be introduced in GitLab 16.6 behind a feature flag. If you are interested in enabling the feature flag for your group, leave a comment in [issue 393933](https://gitlab.com/gitlab-org/gitlab/-/issues/393933).
diff --git a/data/deprecations/16-6-package-deprecate-two-graphql-fields.yml b/data/deprecations/16-6-package-deprecate-two-graphql-fields.yml
new file mode 100644
index 00000000000..fd9498d5871
--- /dev/null
+++ b/data/deprecations/16-6-package-deprecate-two-graphql-fields.yml
@@ -0,0 +1,13 @@
+- title: "GraphQL: deprecate support for `canDestroy` and `canDelete`"
+ announcement_milestone: "16.6"
+ removal_milestone: "17.0"
+ breaking_change: true
+ reporter: trizzi
+ stage: Package
+ issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/390754
+ body: |
+ The Package Registry user interface relies on the GitLab GraphQL API. To make it easy for everyone to contribute, it's important that the frontend is coded consistently across all GitLab product areas. Before GitLab 16.6, however, the Package Registry UI handled permissions differently from other areas of the product.
+
+ In 16.6, we added a new `UserPermissions` field under the `Types::PermissionTypes::Package` type to align the Package Registry with the rest of GitLab. This new field replaces the `canDestroy` field under the `Package`, `PackageBase`, and `PackageDetailsType` types. It also replaces the field `canDelete` for `ContainerRepository`, `ContainerRepositoryDetails`, and `ContainerRepositoryTag`. In GitLab 17.0, the `canDestroy` and `canDelete` fields will be removed.
+
+ This is a breaking change that will be completed in 17.0.
diff --git a/data/deprecations/16-6-proxy-based-dast-deprecation.yml b/data/deprecations/16-6-proxy-based-dast-deprecation.yml
new file mode 100644
index 00000000000..34a47fe8ea5
--- /dev/null
+++ b/data/deprecations/16-6-proxy-based-dast-deprecation.yml
@@ -0,0 +1,9 @@
+- title: "Proxy-based DAST deprecated"
+ removal_milestone: "17.0" # (required) The milestone when this feature is planned to be removed
+ announcement_milestone: "16.6" # (required) The milestone when this feature was first announced as deprecated.
+ breaking_change: true # (required) Change to false if this is not a breaking change.
+ reporter: smeadzinger # (required) GitLab username of the person reporting the change
+ stage: Secure # (required) String value of the stage that the feature was created in. e.g., Growth
+ issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/430966 # (required) Link to the deprecation issue in GitLab
+ body: | # (required) Do not modify this line, instead modify the lines below.
+ As of GitLab 17.0, Proxy-based DAST will not be supported. Please migrate to Browser-based DAST to continue analyzing your projects for security findings via dynamic analysis.
diff --git a/data/deprecations/16_2-custom_sign_in_fields.yml b/data/deprecations/16_2-custom_sign_in_fields.yml
deleted file mode 100644
index 80184a3c096..00000000000
--- a/data/deprecations/16_2-custom_sign_in_fields.yml
+++ /dev/null
@@ -1,11 +0,0 @@
-- title: 'Deprecated parameters related to custom text in the sign-in page' # (required) The name of the feature to be deprecated
- announcement_milestone: '16.2' # (required) The milestone when this feature was first announced as deprecated.
- announcement_date: '2023-07-22' # (required) The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post.
- removal_milestone: '17.0' # (required) The milestone when this feature is planned to be removed
- removal_date: '2024-04-22' # (required) The date of the milestone release when this feature is planned to be removed. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post.
- breaking_change: true # (required) If this deprecation is a breaking change, set this value to true
- reporter: eduardosanz # (required) GitLab username of the person reporting the deprecation
- stage: Manage # (required) String value of the stage that the feature was created in. e.g., Growth
- issue_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/124461 # (required) Link to the deprecation issue in GitLab
- body: | # (required) Do not modify this line, instead modify the lines below.
- The parameters, `sign_in_text` and `help_text`, are deprecated in the [Settings API](https://docs.gitlab.com/ee/api/settings.html). To add a custom text to the sign-in and sign-up pages, use the `description` field in the [Appearance API](https://docs.gitlab.com/ee/api/appearance.html).
diff --git a/data/deprecations/17-0-github-rake-task.yml b/data/deprecations/17-0-github-rake-task.yml
new file mode 100644
index 00000000000..d1e11973993
--- /dev/null
+++ b/data/deprecations/17-0-github-rake-task.yml
@@ -0,0 +1,14 @@
+- title: "The GitHub importer Rake task"
+ removal_milestone: "17.0"
+ announcement_milestone: "16.6"
+ breaking_change: true
+ reporter: wortschi
+ stage: manage
+ issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/428225
+ body: | # (required) Do not modify this line, instead modify the lines below.
+ In GitLab 16.6 the [GitHub importer Rake task](https://docs.gitlab.com/ee/administration/raketasks/github_import.html) is deprecated. The Rake task lacks several features that are supported by the API and is not actively maintained.
+
+ In GitLab 17.0, the Rake task will be removed.
+
+ Instead, GitHub repositories can be imported by using the [API](https://docs.gitlab.com/ee/api/import.html#import-repository-from-github) or the [UI](https://docs.gitlab.com/ee/user/project/import/github.html).
+ documentation_url: https://docs.gitlab.com/ee/administration/raketasks/github_import.html
diff --git a/data/whats_new/202310220001_16_5.yml b/data/whats_new/202310220001_16_5.yml
new file mode 100644
index 00000000000..acb4118645b
--- /dev/null
+++ b/data/whats_new/202310220001_16_5.yml
@@ -0,0 +1,60 @@
+- name: Compliance standards adherence report
+ description: | # Do not modify this line, instead modify the lines below.
+ The Compliance Center now includes a new tab for the standards adherence report.
+ This report initially includes a GitLab best practices standard, showing when the
+ projects in your group are not meeting the requirements for the checks included in the standard. The
+ three checks shown initially are:
+
+ - Approval rule exists to require at least 2 approvers on MRs
+ - Approval rule exists to disallow the MR author to merge
+ - Approval rule exists to disallow committers to the MR to merge
+
+ The report contains details on the status of each check on a per project basis. It will
+ also show you when the check was last run, which standard the check applies to,
+ and how to fix any failures or problems that might be shown on the report. Future iterations
+ will add more checks and expand the scope to include more regulations and standards.
+ Additionally, we will be adding improvements to group and filter the report, so you
+ can focus on the projects or standards that matter most to your organization.
+ stage: govern
+ self-managed: true
+ gitlab-com: true
+ available_in: [Ultimate]
+ documentation_link: 'https://docs.gitlab.com/ee/user/compliance/compliance_center/#standards-adherence-dashboard'
+ image_url: 'https://about.gitlab.com/images/16_5/govern-compliance-standards-adherence-report.png'
+ published_at: 2023-10-22
+ release: 16.5
+- name: Create rules to set target branches for merge requests
+ description: | # Do not modify this line, instead modify the lines below.
+ Some projects use multiple long-term branches for development, like `develop` and `qa`. In these projects, you might want to keep `main` as the default branch since it represents the production state of the project. However, development work expects merge requests to target `develop` or `qa`. Target branch rules help ensure merge requests target the appropriate branch for your project and development workflow.
+
+ When you create a merge request, the rule checks the name of the branch. If the branch name matches the rule, the merge request pre-selects the branch you specified in the rule as the target. If the branch name does not match, the merge request targets the default branch of the project.
+ stage: create
+ self-managed: true
+ gitlab-com: true
+ available_in: [Premium, Ultimate]
+ documentation_link: 'https://docs.gitlab.com/ee/user/project/repository/branches/#configure-rules-for-target-branches'
+ image_url: 'https://about.gitlab.com/images/16_5/create-target-branch-rules.png'
+ published_at: 2023-10-22
+ release: 16.5
+- name: Resolve an issue thread
+ description: | # Do not modify this line, instead modify the lines below.
+ Long-running issues with many threads can be challenging to read and track. You can now resolve a thread on an issue when the topic of discussion has concluded.
+ stage: plan
+ self-managed: true
+ gitlab-com: true
+ available_in: [Free, Premium, Ultimate]
+ documentation_link: 'https://docs.gitlab.com/ee/user/discussions/#resolve-a-thread'
+ image_url: 'https://about.gitlab.com/images/16_5/resolve_functionality_for_issues.png'
+ published_at: 2023-10-22
+ release: 16.5
+- name: Fast-forward merge trains with semi-linear history
+ description: | # Do not modify this line, instead modify the lines below.
+ In 16.4, we released [Fast-forward merge trains](https://about.gitlab.com/releases/2023/09/22/gitlab-16-4-released/#fast-forward-merge-support-for-merge-trains), and as a continuation, we want to ensure we support all [merge methods](https://docs.gitlab.com/ee/user/project/merge_requests/methods/). Now, if you want to ensure your semi-linear commit history is maintained you can use Semi-linear fast-forward merge trains.
+ stage: verify
+ self-managed: true
+ gitlab-com: true
+ available_in: [Premium, Ultimate]
+ documentation_link: 'https://docs.gitlab.com/ee/ci/pipelines/merge_trains.html'
+ image_url: 'https://about.gitlab.com/images/16_5/ff-merge.png'
+ published_at: 2023-10-22
+ release: 16.5
diff --git a/db/click_house/main/20230705124511_create_events.sql b/db/click_house/main/20230705124511_create_events.sql
deleted file mode 100644
index 8af45443e4c..00000000000
--- a/db/click_house/main/20230705124511_create_events.sql
+++ /dev/null
@@ -1,16 +0,0 @@
-CREATE TABLE events
-(
- id UInt64 DEFAULT 0,
- path String DEFAULT '',
- author_id UInt64 DEFAULT 0,
- target_id UInt64 DEFAULT 0,
- target_type LowCardinality(String) DEFAULT '',
- action UInt8 DEFAULT 0,
- deleted UInt8 DEFAULT 0,
- created_at DateTime64(6, 'UTC') DEFAULT now(),
- updated_at DateTime64(6, 'UTC') DEFAULT now()
-)
-ENGINE = ReplacingMergeTree(updated_at, deleted)
-PRIMARY KEY (id)
-ORDER BY (id)
-PARTITION BY toYear(created_at)
diff --git a/db/click_house/main/20230707151359_create_ci_finished_builds.sql b/db/click_house/main/20230707151359_create_ci_finished_builds.sql
deleted file mode 100644
index 5c2cc0e8eb3..00000000000
--- a/db/click_house/main/20230707151359_create_ci_finished_builds.sql
+++ /dev/null
@@ -1,33 +0,0 @@
--- source table for CI analytics, almost useless on it's own, but it's a basis for creating materialized views
-CREATE TABLE ci_finished_builds
-(
- id UInt64 DEFAULT 0,
- project_id UInt64 DEFAULT 0,
- pipeline_id UInt64 DEFAULT 0,
- status LowCardinality(String) DEFAULT '',
-
- --- Fields to calculate timings
- created_at DateTime64(6, 'UTC') DEFAULT now(),
- queued_at DateTime64(6, 'UTC') DEFAULT now(),
- finished_at DateTime64(6, 'UTC') DEFAULT now(),
- started_at DateTime64(6, 'UTC') DEFAULT now(),
-
- runner_id UInt64 DEFAULT 0,
- runner_manager_system_xid String DEFAULT '',
-
- --- Runner fields
- runner_run_untagged Boolean DEFAULT FALSE,
- runner_type UInt8 DEFAULT 0,
- runner_manager_version LowCardinality(String) DEFAULT '',
- runner_manager_revision LowCardinality(String) DEFAULT '',
- runner_manager_platform LowCardinality(String) DEFAULT '',
- runner_manager_architecture LowCardinality(String) DEFAULT '',
-
- --- Materialized columns
- duration Int64 MATERIALIZED age('ms', started_at, finished_at),
- queueing_duration Int64 MATERIALIZED age('ms', queued_at, started_at)
- --- This table is incomplete, we'll add more fields before starting the data migration
-)
-ENGINE = ReplacingMergeTree -- Using ReplacingMergeTree just in case we accidentally insert the same data twice
-ORDER BY (status, runner_type, project_id, finished_at, id)
-PARTITION BY toYear(finished_at)
diff --git a/db/click_house/main/20230719101806_create_ci_finished_builds_aggregated_queueing_delay_percentiles.sql b/db/click_house/main/20230719101806_create_ci_finished_builds_aggregated_queueing_delay_percentiles.sql
deleted file mode 100644
index 56889ffc0d4..00000000000
--- a/db/click_house/main/20230719101806_create_ci_finished_builds_aggregated_queueing_delay_percentiles.sql
+++ /dev/null
@@ -1,11 +0,0 @@
-CREATE TABLE ci_finished_builds_aggregated_queueing_delay_percentiles
-(
- status LowCardinality(String) DEFAULT '',
- runner_type UInt8 DEFAULT 0,
- started_at_bucket DateTime64(6, 'UTC') DEFAULT now(),
-
- count_builds AggregateFunction(count),
- queueing_duration_quantile AggregateFunction(quantile, Int64)
-)
-ENGINE = AggregatingMergeTree()
-ORDER BY (started_at_bucket, status, runner_type)
diff --git a/db/click_house/main/20230724064832_create_contribution_analytics_events.sql b/db/click_house/main/20230724064832_create_contribution_analytics_events.sql
deleted file mode 100644
index 7867897e897..00000000000
--- a/db/click_house/main/20230724064832_create_contribution_analytics_events.sql
+++ /dev/null
@@ -1,13 +0,0 @@
-CREATE TABLE contribution_analytics_events
-(
- id UInt64 DEFAULT 0,
- path String DEFAULT '',
- author_id UInt64 DEFAULT 0,
- target_type LowCardinality(String) DEFAULT '',
- action UInt8 DEFAULT 0,
- created_at Date DEFAULT toDate(now()),
- updated_at DateTime64(6, 'UTC') DEFAULT now()
-)
- ENGINE = MergeTree
- ORDER BY (path, created_at, author_id, id)
- PARTITION BY toYear(created_at);
diff --git a/db/click_house/main/20230724064918_contribution_analytics_events_materialized_view.sql b/db/click_house/main/20230724064918_contribution_analytics_events_materialized_view.sql
deleted file mode 100644
index 669b03ce0f3..00000000000
--- a/db/click_house/main/20230724064918_contribution_analytics_events_materialized_view.sql
+++ /dev/null
@@ -1,16 +0,0 @@
-CREATE MATERIALIZED VIEW contribution_analytics_events_mv
-TO contribution_analytics_events
-AS
-SELECT
- id,
- argMax(path, events.updated_at) as path,
- argMax(author_id, events.updated_at) as author_id,
- argMax(target_type, events.updated_at) as target_type,
- argMax(action, events.updated_at) as action,
- argMax(date(created_at), events.updated_at) as created_at,
- max(events.updated_at) as updated_at
-FROM events
-where (("events"."action" = 5 AND "events"."target_type" = '')
- OR ("events"."action" IN (1, 3, 7, 12)
- AND "events"."target_type" IN ('MergeRequest', 'Issue')))
-GROUP BY id
diff --git a/db/click_house/main/20230808070520_create_events_cursor.sql b/db/click_house/main/20230808070520_create_events_cursor.sql
deleted file mode 100644
index effc3c64f60..00000000000
--- a/db/click_house/main/20230808070520_create_events_cursor.sql
+++ /dev/null
@@ -1,9 +0,0 @@
-CREATE TABLE sync_cursors
-(
- table_name LowCardinality(String) DEFAULT '',
- primary_key_value UInt64 DEFAULT 0,
- recorded_at DateTime64(6, 'UTC') DEFAULT now()
-)
-ENGINE = ReplacingMergeTree(recorded_at)
-ORDER BY (table_name)
-PRIMARY KEY (table_name)
diff --git a/db/click_house/main/20230808140217_create_ci_finished_builds_aggregated_queueing_delay_percentiles_mv.sql b/db/click_house/main/20230808140217_create_ci_finished_builds_aggregated_queueing_delay_percentiles_mv.sql
deleted file mode 100644
index 504e2d87609..00000000000
--- a/db/click_house/main/20230808140217_create_ci_finished_builds_aggregated_queueing_delay_percentiles_mv.sql
+++ /dev/null
@@ -1,12 +0,0 @@
-CREATE MATERIALIZED VIEW ci_finished_builds_aggregated_queueing_delay_percentiles_mv
-TO ci_finished_builds_aggregated_queueing_delay_percentiles
-AS
-SELECT
- status,
- runner_type,
- toStartOfInterval(started_at, INTERVAL 5 minute) AS started_at_bucket,
-
- countState(*) as count_builds,
- quantileState(queueing_duration) AS queueing_duration_quantile
-FROM ci_finished_builds
-GROUP BY status, runner_type, started_at_bucket
diff --git a/db/click_house/migrate/20230705124511_create_events.rb b/db/click_house/migrate/20230705124511_create_events.rb
new file mode 100644
index 00000000000..cd60ade5d4d
--- /dev/null
+++ b/db/click_house/migrate/20230705124511_create_events.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+class CreateEvents < ClickHouse::Migration
+ def up
+ execute <<~SQL
+ CREATE TABLE IF NOT EXISTS events
+ (
+ id UInt64 DEFAULT 0,
+ path String DEFAULT '',
+ author_id UInt64 DEFAULT 0,
+ target_id UInt64 DEFAULT 0,
+ target_type LowCardinality(String) DEFAULT '',
+ action UInt8 DEFAULT 0,
+ deleted UInt8 DEFAULT 0,
+ created_at DateTime64(6, 'UTC') DEFAULT now(),
+ updated_at DateTime64(6, 'UTC') DEFAULT now()
+ )
+ ENGINE = ReplacingMergeTree(updated_at, deleted)
+ PRIMARY KEY (id)
+ ORDER BY (id)
+ PARTITION BY toYear(created_at)
+ SQL
+ end
+
+ def down
+ execute <<~SQL
+ DROP TABLE events
+ SQL
+ end
+end
diff --git a/db/click_house/migrate/20230707151359_create_ci_finished_builds.rb b/db/click_house/migrate/20230707151359_create_ci_finished_builds.rb
new file mode 100644
index 00000000000..39521af8d99
--- /dev/null
+++ b/db/click_house/migrate/20230707151359_create_ci_finished_builds.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+class CreateCiFinishedBuilds < ClickHouse::Migration
+ def up
+ execute <<~SQL
+ -- source table for CI analytics, almost useless on it's own, but it's a basis for creating materialized views
+ CREATE TABLE IF NOT EXISTS ci_finished_builds
+ (
+ id UInt64 DEFAULT 0,
+ project_id UInt64 DEFAULT 0,
+ pipeline_id UInt64 DEFAULT 0,
+ status LowCardinality(String) DEFAULT '',
+
+ --- Fields to calculate timings
+ created_at DateTime64(6, 'UTC') DEFAULT now(),
+ queued_at DateTime64(6, 'UTC') DEFAULT now(),
+ finished_at DateTime64(6, 'UTC') DEFAULT now(),
+ started_at DateTime64(6, 'UTC') DEFAULT now(),
+
+ runner_id UInt64 DEFAULT 0,
+ runner_manager_system_xid String DEFAULT '',
+
+ --- Runner fields
+ runner_run_untagged Boolean DEFAULT FALSE,
+ runner_type UInt8 DEFAULT 0,
+ runner_manager_version LowCardinality(String) DEFAULT '',
+ runner_manager_revision LowCardinality(String) DEFAULT '',
+ runner_manager_platform LowCardinality(String) DEFAULT '',
+ runner_manager_architecture LowCardinality(String) DEFAULT '',
+
+ --- Materialized columns
+ duration Int64 MATERIALIZED age('ms', started_at, finished_at),
+ queueing_duration Int64 MATERIALIZED age('ms', queued_at, started_at)
+ --- This table is incomplete, we'll add more fields before starting the data migration
+ )
+ ENGINE = ReplacingMergeTree -- Using ReplacingMergeTree just in case we accidentally insert the same data twice
+ ORDER BY (status, runner_type, project_id, finished_at, id)
+ PARTITION BY toYear(finished_at)
+ SQL
+ end
+
+ def down
+ execute <<~SQL
+ DROP TABLE ci_finished_builds
+ SQL
+ end
+end
diff --git a/db/click_house/migrate/20230719101806_create_ci_finished_builds_aggregated_queueing_delay_percentiles.rb b/db/click_house/migrate/20230719101806_create_ci_finished_builds_aggregated_queueing_delay_percentiles.rb
new file mode 100644
index 00000000000..47934d8fe02
--- /dev/null
+++ b/db/click_house/migrate/20230719101806_create_ci_finished_builds_aggregated_queueing_delay_percentiles.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+class CreateCiFinishedBuildsAggregatedQueueingDelayPercentiles < ClickHouse::Migration
+ def up
+ execute <<~SQL
+ CREATE TABLE IF NOT EXISTS ci_finished_builds_aggregated_queueing_delay_percentiles
+ (
+ status LowCardinality(String) DEFAULT '',
+ runner_type UInt8 DEFAULT 0,
+ started_at_bucket DateTime64(6, 'UTC') DEFAULT now(),
+
+ count_builds AggregateFunction(count),
+ queueing_duration_quantile AggregateFunction(quantile, Int64)
+ )
+ ENGINE = AggregatingMergeTree()
+ ORDER BY (started_at_bucket, status, runner_type)
+ SQL
+ end
+
+ def down
+ execute <<~SQL
+ DROP TABLE ci_finished_builds_aggregated_queueing_delay_percentiles
+ SQL
+ end
+end
diff --git a/db/click_house/migrate/20230724064832_create_contribution_analytics_events.rb b/db/click_house/migrate/20230724064832_create_contribution_analytics_events.rb
new file mode 100644
index 00000000000..2606ae3adc9
--- /dev/null
+++ b/db/click_house/migrate/20230724064832_create_contribution_analytics_events.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+class CreateContributionAnalyticsEvents < ClickHouse::Migration
+ def up
+ execute <<~SQL
+ CREATE TABLE IF NOT EXISTS contribution_analytics_events
+ (
+ id UInt64 DEFAULT 0,
+ path String DEFAULT '',
+ author_id UInt64 DEFAULT 0,
+ target_type LowCardinality(String) DEFAULT '',
+ action UInt8 DEFAULT 0,
+ created_at Date DEFAULT toDate(now()),
+ updated_at DateTime64(6, 'UTC') DEFAULT now()
+ )
+ ENGINE = MergeTree
+ ORDER BY (path, created_at, author_id, id)
+ PARTITION BY toYear(created_at);
+ SQL
+ end
+
+ def down
+ execute <<~SQL
+ DROP TABLE contribution_analytics_events
+ SQL
+ end
+end
diff --git a/db/click_house/migrate/20230724064918_create_contribution_analytics_events_materialized_view.rb b/db/click_house/migrate/20230724064918_create_contribution_analytics_events_materialized_view.rb
new file mode 100644
index 00000000000..956a26d80f3
--- /dev/null
+++ b/db/click_house/migrate/20230724064918_create_contribution_analytics_events_materialized_view.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+class CreateContributionAnalyticsEventsMaterializedView < ClickHouse::Migration
+ def up
+ execute <<~SQL
+ CREATE MATERIALIZED VIEW IF NOT EXISTS contribution_analytics_events_mv
+ TO contribution_analytics_events
+ AS
+ SELECT
+ id,
+ argMax(path, events.updated_at) as path,
+ argMax(author_id, events.updated_at) as author_id,
+ argMax(target_type, events.updated_at) as target_type,
+ argMax(action, events.updated_at) as action,
+ argMax(date(created_at), events.updated_at) as created_at,
+ max(events.updated_at) as updated_at
+ FROM events
+ WHERE (("events"."action" = 5 AND "events"."target_type" = '')
+ OR ("events"."action" IN (1, 3, 7, 12)
+ AND "events"."target_type" IN ('MergeRequest', 'Issue')))
+ GROUP BY id
+ SQL
+ end
+
+ def down
+ execute <<~SQL
+ DROP VIEW contribution_analytics_events_mv
+ SQL
+ end
+end
diff --git a/db/click_house/migrate/20230808070520_create_sync_cursors.rb b/db/click_house/migrate/20230808070520_create_sync_cursors.rb
new file mode 100644
index 00000000000..7583f8ec0c5
--- /dev/null
+++ b/db/click_house/migrate/20230808070520_create_sync_cursors.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+class CreateSyncCursors < ClickHouse::Migration
+ def up
+ execute <<~SQL
+ CREATE TABLE IF NOT EXISTS sync_cursors
+ (
+ table_name LowCardinality(String) DEFAULT '',
+ primary_key_value UInt64 DEFAULT 0,
+ recorded_at DateTime64(6, 'UTC') DEFAULT now()
+ )
+ ENGINE = ReplacingMergeTree(recorded_at)
+ ORDER BY (table_name)
+ PRIMARY KEY (table_name)
+ SQL
+ end
+
+ def down
+ execute <<~SQL
+ DROP TABLE sync_cursors
+ SQL
+ end
+end
diff --git a/db/click_house/migrate/20230808140217_create_ci_finished_builds_aggregated_queueing_delay_percentiles_mv.rb b/db/click_house/migrate/20230808140217_create_ci_finished_builds_aggregated_queueing_delay_percentiles_mv.rb
new file mode 100644
index 00000000000..cc029d48436
--- /dev/null
+++ b/db/click_house/migrate/20230808140217_create_ci_finished_builds_aggregated_queueing_delay_percentiles_mv.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+class CreateCiFinishedBuildsAggregatedQueueingDelayPercentilesMv < ClickHouse::Migration
+ def up
+ execute <<~SQL
+ CREATE MATERIALIZED VIEW IF NOT EXISTS ci_finished_builds_aggregated_queueing_delay_percentiles_mv
+ TO ci_finished_builds_aggregated_queueing_delay_percentiles
+ AS
+ SELECT
+ status,
+ runner_type,
+ toStartOfInterval(started_at, INTERVAL 5 minute) AS started_at_bucket,
+
+ countState(*) as count_builds,
+ quantileState(queueing_duration) AS queueing_duration_quantile
+ FROM ci_finished_builds
+ GROUP BY status, runner_type, started_at_bucket
+ SQL
+ end
+
+ def down
+ execute <<~SQL
+ DROP VIEW ci_finished_builds_aggregated_queueing_delay_percentiles_mv
+ SQL
+ end
+end
diff --git a/db/click_house/migrate/20231106202300_modify_ci_finished_builds_settings.rb b/db/click_house/migrate/20231106202300_modify_ci_finished_builds_settings.rb
new file mode 100644
index 00000000000..d9951725c9b
--- /dev/null
+++ b/db/click_house/migrate/20231106202300_modify_ci_finished_builds_settings.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class ModifyCiFinishedBuildsSettings < ClickHouse::Migration
+ def up
+ execute <<~SQL
+ ALTER TABLE ci_finished_builds MODIFY SETTING use_async_block_ids_cache = true
+ SQL
+ end
+
+ def down
+ execute <<~SQL
+ ALTER TABLE ci_finished_builds MODIFY SETTING use_async_block_ids_cache = false
+ SQL
+ end
+end
diff --git a/db/docs/activity_pub_releases_subscriptions.yml b/db/docs/activity_pub_releases_subscriptions.yml
new file mode 100644
index 00000000000..d759aada5a9
--- /dev/null
+++ b/db/docs/activity_pub_releases_subscriptions.yml
@@ -0,0 +1,11 @@
+---
+table_name: activity_pub_releases_subscriptions
+classes:
+- ActivityPub::ReleasesSubscription
+feature_categories:
+- release_orchestration
+description: Stores subscriptions from external users through ActivityPub for project
+ releases
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132889
+milestone: '16.6'
+gitlab_schema: gitlab_main
diff --git a/db/docs/approval_group_rules.yml b/db/docs/approval_group_rules.yml
new file mode 100644
index 00000000000..b9dab08c5df
--- /dev/null
+++ b/db/docs/approval_group_rules.yml
@@ -0,0 +1,10 @@
+---
+table_name: approval_group_rules
+classes:
+- ApprovalRules::ApprovalGroupRule
+feature_categories:
+- source_code_management
+description: Keeps approval group rules
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132651
+milestone: '16.5'
+gitlab_schema: gitlab_main
diff --git a/db/docs/approval_group_rules_groups.yml b/db/docs/approval_group_rules_groups.yml
new file mode 100644
index 00000000000..0599af6ac15
--- /dev/null
+++ b/db/docs/approval_group_rules_groups.yml
@@ -0,0 +1,9 @@
+---
+table_name: approval_group_rules_groups
+classes: []
+feature_categories:
+ - source_code_management
+description: Keeps connection between group and a group approval rule
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132651
+milestone: '16.5'
+gitlab_schema: gitlab_main
diff --git a/db/docs/approval_group_rules_protected_branches.yml b/db/docs/approval_group_rules_protected_branches.yml
new file mode 100644
index 00000000000..ac55f0980be
--- /dev/null
+++ b/db/docs/approval_group_rules_protected_branches.yml
@@ -0,0 +1,9 @@
+---
+table_name: approval_group_rules_protected_branches
+classes: []
+feature_categories:
+ - source_code_management
+description: Keeps relation between approval group rules and protected branches.
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132651
+milestone: '16.5'
+gitlab_schema: gitlab_main
diff --git a/db/docs/approval_group_rules_users.yml b/db/docs/approval_group_rules_users.yml
new file mode 100644
index 00000000000..67271d2a35d
--- /dev/null
+++ b/db/docs/approval_group_rules_users.yml
@@ -0,0 +1,9 @@
+---
+table_name: approval_group_rules_users
+classes: []
+feature_categories:
+ - source_code_management
+description: Keeps connection between user and a group approval rule
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132651
+milestone: '16.5'
+gitlab_schema: gitlab_main
diff --git a/db/docs/approval_project_rules.yml b/db/docs/approval_project_rules.yml
index c970b86bb18..9208e49d49d 100644
--- a/db/docs/approval_project_rules.yml
+++ b/db/docs/approval_project_rules.yml
@@ -7,4 +7,4 @@ feature_categories:
description: Keeps approval project rules
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/8497
milestone: '11.7'
-gitlab_schema: gitlab_main
+gitlab_schema: gitlab_main_cell
diff --git a/db/docs/audit_events_external_audit_event_destinations.yml b/db/docs/audit_events_external_audit_event_destinations.yml
index 91fb1e5a17a..534d3470e7b 100644
--- a/db/docs/audit_events_external_audit_event_destinations.yml
+++ b/db/docs/audit_events_external_audit_event_destinations.yml
@@ -7,4 +7,4 @@ feature_categories:
description: TODO
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70706
milestone: '14.4'
-gitlab_schema: gitlab_main
+gitlab_schema: gitlab_main_cell
diff --git a/db/docs/audit_events_google_cloud_logging_configurations.yml b/db/docs/audit_events_google_cloud_logging_configurations.yml
index bd6c13a1fdf..e910071eaa3 100644
--- a/db/docs/audit_events_google_cloud_logging_configurations.yml
+++ b/db/docs/audit_events_google_cloud_logging_configurations.yml
@@ -7,4 +7,4 @@ feature_categories:
description: Stores Google Cloud Logging configurations associated with IAM service accounts, used for generating access tokens.
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/issues/409421
milestone: '16.0'
-gitlab_schema: gitlab_main
+gitlab_schema: gitlab_main_cell
diff --git a/db/docs/audit_events_streaming_http_group_namespace_filters.yml b/db/docs/audit_events_streaming_http_group_namespace_filters.yml
new file mode 100644
index 00000000000..df08e8b57d2
--- /dev/null
+++ b/db/docs/audit_events_streaming_http_group_namespace_filters.yml
@@ -0,0 +1,10 @@
+---
+table_name: audit_events_streaming_http_group_namespace_filters
+classes:
+ - AuditEvents::Streaming::HTTP::NamespaceFilter
+feature_categories:
+ - audit_events
+description: Represents a subgroup or project filter for audit event streaming on groups
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/135080
+milestone: '16.6'
+gitlab_schema: gitlab_main
diff --git a/db/docs/batched_background_migrations/backfill_packages_tags_project_id.yml b/db/docs/batched_background_migrations/backfill_packages_tags_project_id.yml
new file mode 100644
index 00000000000..b8caef928bb
--- /dev/null
+++ b/db/docs/batched_background_migrations/backfill_packages_tags_project_id.yml
@@ -0,0 +1,9 @@
+---
+migration_job_name: BackfillPackagesTagsProjectId
+description: Populates the new `packages_tags.project_id` column after joining with the `packages_packages` table
+feature_category: package_registry
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/135451
+milestone: 16.6
+queued_migration_version: 20231030071209
+finalize_after: '2023-12-23'
+finalized_by: # version of the migration that ensured this bbm
diff --git a/db/docs/batched_background_migrations/delete_invalid_protected_branch_merge_access_levels.yml b/db/docs/batched_background_migrations/delete_invalid_protected_branch_merge_access_levels.yml
new file mode 100644
index 00000000000..cd85f7e4ab2
--- /dev/null
+++ b/db/docs/batched_background_migrations/delete_invalid_protected_branch_merge_access_levels.yml
@@ -0,0 +1,7 @@
+---
+migration_job_name: DeleteInvalidProtectedBranchMergeAccessLevels
+description: Remove rows from protected_branch_merge_access_levels for groups that do not have project_group_links to the project for the associated protected branch
+feature_category: source_code_management
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/issues/427486
+milestone: 16.6
+queued_migration_version: 20231016173129
diff --git a/db/docs/batched_background_migrations/delete_invalid_protected_branch_push_access_levels.yml b/db/docs/batched_background_migrations/delete_invalid_protected_branch_push_access_levels.yml
new file mode 100644
index 00000000000..dd92e35f26f
--- /dev/null
+++ b/db/docs/batched_background_migrations/delete_invalid_protected_branch_push_access_levels.yml
@@ -0,0 +1,7 @@
+---
+migration_job_name: DeleteInvalidProtectedBranchPushAccessLevels
+description: Remove rows from protected_branch_push_access_levels for groups that do not have project_group_links to the project for the associated protected branch
+feature_category: source_code_management
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/issues/427486
+milestone: 16.6
+queued_migration_version: 20231016194927
diff --git a/db/docs/batched_background_migrations/delete_invalid_protected_tag_create_access_levels.yml b/db/docs/batched_background_migrations/delete_invalid_protected_tag_create_access_levels.yml
new file mode 100644
index 00000000000..0c406c7650b
--- /dev/null
+++ b/db/docs/batched_background_migrations/delete_invalid_protected_tag_create_access_levels.yml
@@ -0,0 +1,7 @@
+---
+migration_job_name: DeleteInvalidProtectedTagCreateAccessLevels
+description: Remove rows from protected_tag_create_access_levels for groups that do not have project_group_links to the project for the associated protected tag
+feature_category: source_code_management
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/issues/427486
+milestone: 16.6
+queued_migration_version: 20231016194943
diff --git a/db/docs/compliance_framework_security_policies.yml b/db/docs/compliance_framework_security_policies.yml
new file mode 100644
index 00000000000..9f16b703a9d
--- /dev/null
+++ b/db/docs/compliance_framework_security_policies.yml
@@ -0,0 +1,10 @@
+---
+table_name: compliance_framework_security_policies
+classes:
+- ComplianceManagement::ComplianceFramework::SecurityPolicy
+feature_categories:
+- security_policy_management
+description: Persists the relation between compliance_frameworks and security_orchestration_policy_configurations
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/135291
+milestone: '16.6'
+gitlab_schema: gitlab_main
diff --git a/db/docs/container_expiration_policies.yml b/db/docs/container_expiration_policies.yml
index 8cc8c675cf9..b1c203134e7 100644
--- a/db/docs/container_expiration_policies.yml
+++ b/db/docs/container_expiration_policies.yml
@@ -7,4 +7,4 @@ feature_categories:
description: Project level settings for container registry cleanup policies
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/20412
milestone: '12.6'
-gitlab_schema: gitlab_main
+gitlab_schema: gitlab_main_cell
diff --git a/db/docs/events.yml b/db/docs/events.yml
index 04d90a24ec9..4e493fefea3 100644
--- a/db/docs/events.yml
+++ b/db/docs/events.yml
@@ -14,4 +14,4 @@ feature_categories:
description: Stores events created by users interacting with various product features
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/a847501fd2ffc1c4becc7d0d352d80168d9b3568
milestone: "2.2"
-gitlab_schema: gitlab_main
+gitlab_schema: gitlab_main_cell
diff --git a/db/docs/fork_network_members.yml b/db/docs/fork_network_members.yml
index c3dd193b4aa..a164593c5e1 100644
--- a/db/docs/fork_network_members.yml
+++ b/db/docs/fork_network_members.yml
@@ -7,4 +7,4 @@ feature_categories:
description: Keeps track of fork relations between projects.
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/62186
milestone: '10.1'
-gitlab_schema: gitlab_main
+gitlab_schema: gitlab_main_cell
diff --git a/db/docs/fork_networks.yml b/db/docs/fork_networks.yml
index ca0960dd93a..65938326da7 100644
--- a/db/docs/fork_networks.yml
+++ b/db/docs/fork_networks.yml
@@ -7,4 +7,4 @@ feature_categories:
description: When a project is first forked, a row is created in this table. Also referenced by the fork_network_members table. This is used to know which projects can send merge reqeusts to each other.
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/3098
milestone: '10.1'
-gitlab_schema: gitlab_main
+gitlab_schema: gitlab_main_cell
diff --git a/db/docs/group_merge_request_approval_settings.yml b/db/docs/group_merge_request_approval_settings.yml
index c3b6bb8877c..b81d6f3c165 100644
--- a/db/docs/group_merge_request_approval_settings.yml
+++ b/db/docs/group_merge_request_approval_settings.yml
@@ -7,4 +7,4 @@ feature_categories:
description: Keeps merge request approval settings per group
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/50256
milestone: '13.8'
-gitlab_schema: gitlab_main
+gitlab_schema: gitlab_main_cell
diff --git a/db/docs/incident_management_timeline_event_tags.yml b/db/docs/incident_management_timeline_event_tags.yml
index aba8f7db152..9243616d26a 100644
--- a/db/docs/incident_management_timeline_event_tags.yml
+++ b/db/docs/incident_management_timeline_event_tags.yml
@@ -7,4 +7,4 @@ feature_categories:
description: Persists tags for timeline events in a project.
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/100271
milestone: '15.6'
-gitlab_schema: gitlab_main
+gitlab_schema: gitlab_main_cell
diff --git a/db/docs/internal_ids.yml b/db/docs/internal_ids.yml
index 5109a51802c..53e83142780 100644
--- a/db/docs/internal_ids.yml
+++ b/db/docs/internal_ids.yml
@@ -7,4 +7,4 @@ feature_categories:
description: Keeps track of counters scoped to a certain context, e.g. a project-wide counter for issues.
introduced_by_url: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/17580
milestone: '10.7'
-gitlab_schema: gitlab_main
+gitlab_schema: gitlab_main_cell
diff --git a/db/docs/ip_restrictions.yml b/db/docs/ip_restrictions.yml
index fbf90135d0a..dd7615dce24 100644
--- a/db/docs/ip_restrictions.yml
+++ b/db/docs/ip_restrictions.yml
@@ -7,4 +7,4 @@ feature_categories:
description: TODO
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/12669
milestone: '12.0'
-gitlab_schema: gitlab_main
+gitlab_schema: gitlab_main_cell
diff --git a/db/docs/labels.yml b/db/docs/labels.yml
index f43814ced30..83956783891 100644
--- a/db/docs/labels.yml
+++ b/db/docs/labels.yml
@@ -11,4 +11,4 @@ description: Information related to labels, which can be associated with groups
projects
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/03654a6abf47c88b8b980a6707874ff78080d2fe
milestone: '7.2'
-gitlab_schema: gitlab_main
+gitlab_schema: gitlab_main_cell
diff --git a/db/docs/lfs_file_locks.yml b/db/docs/lfs_file_locks.yml
index 07850aedddb..c487fa3d42a 100644
--- a/db/docs/lfs_file_locks.yml
+++ b/db/docs/lfs_file_locks.yml
@@ -7,4 +7,4 @@ feature_categories:
description: File locks for LFS objects
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/4091
milestone: '10.5'
-gitlab_schema: gitlab_main
+gitlab_schema: gitlab_main_cell
diff --git a/db/docs/ml_model_metadata.yml b/db/docs/ml_model_metadata.yml
new file mode 100644
index 00000000000..0f48f71dcbb
--- /dev/null
+++ b/db/docs/ml_model_metadata.yml
@@ -0,0 +1,10 @@
+---
+table_name: ml_model_metadata
+classes:
+ - Ml::ModelMetadata
+feature_categories:
+ - mlops
+gitlab_schema: gitlab_main
+description: A Model Metadata record holds extra information about the model
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134443
+milestone: 16.6
diff --git a/db/docs/namespace_aggregation_schedules.yml b/db/docs/namespace_aggregation_schedules.yml
index d57311fff8f..a6434e5601c 100644
--- a/db/docs/namespace_aggregation_schedules.yml
+++ b/db/docs/namespace_aggregation_schedules.yml
@@ -7,4 +7,4 @@ feature_categories:
description: Keeps update schedules for namespace_root_storage_statistics
introduced_by_url: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/29570
milestone: '12.1'
-gitlab_schema: gitlab_main
+gitlab_schema: gitlab_main_cell
diff --git a/db/docs/namespace_commit_emails.yml b/db/docs/namespace_commit_emails.yml
index c19ff1c577b..c5afcfaaebd 100644
--- a/db/docs/namespace_commit_emails.yml
+++ b/db/docs/namespace_commit_emails.yml
@@ -7,4 +7,4 @@ feature_categories:
description: User default email for commits from the GitLab UI
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/101832
milestone: '15.6'
-gitlab_schema: gitlab_main
+gitlab_schema: gitlab_main_cell
diff --git a/db/docs/namespaces.yml b/db/docs/namespaces.yml
index ba3d345d8c7..8fa7c2a3d31 100644
--- a/db/docs/namespaces.yml
+++ b/db/docs/namespaces.yml
@@ -11,3 +11,10 @@ description: Storing namespaces records for groups, users and projects
introduced_by_url: https://github.com/gitlabhq/gitlabhq/pull/2051
milestone: "<6.0"
gitlab_schema: gitlab_main_cell
+schema_inconsistencies:
+- type: missing_indexes
+ object_name: index_namespaces_on_created_at
+ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134948
+- type: missing_indexes
+ object_name: index_namespaces_on_ldap_sync_last_successful_update_at
+ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/135040
diff --git a/db/docs/namespaces_sync_events.yml b/db/docs/namespaces_sync_events.yml
index fdac8accd7f..f654ca8657e 100644
--- a/db/docs/namespaces_sync_events.yml
+++ b/db/docs/namespaces_sync_events.yml
@@ -7,4 +7,4 @@ feature_categories:
description: Used as a queue of data that needs to be synchronized between the `ci` and `main` database
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/75517
milestone: '14.6'
-gitlab_schema: gitlab_main
+gitlab_schema: gitlab_main_cell
diff --git a/db/docs/onboarding_progresses.yml b/db/docs/onboarding_progresses.yml
index 805b674d44b..ff7a80a1db5 100644
--- a/db/docs/onboarding_progresses.yml
+++ b/db/docs/onboarding_progresses.yml
@@ -7,4 +7,4 @@ feature_categories:
description: TODO
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/50711
milestone: '13.8'
-gitlab_schema: gitlab_main
+gitlab_schema: gitlab_main_cell
diff --git a/db/docs/p_ci_job_annotations.yml b/db/docs/p_ci_job_annotations.yml
index 62a1b56abad..aae2ea67295 100644
--- a/db/docs/p_ci_job_annotations.yml
+++ b/db/docs/p_ci_job_annotations.yml
@@ -6,4 +6,5 @@ feature_categories:
- build_artifacts
description: Stores user provided annotations for jobs. Currently storing extra information for a given job feed by API.
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/117319
+milestone: '16.1'
gitlab_schema: gitlab_ci
diff --git a/db/docs/path_locks.yml b/db/docs/path_locks.yml
index f27856d5dee..ba36f45ce4d 100644
--- a/db/docs/path_locks.yml
+++ b/db/docs/path_locks.yml
@@ -7,4 +7,4 @@ feature_categories:
description: Stores paths to repository blobs locked by users
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/684e9d1b5979e11d2edae11a3028a696bfcdedf8
milestone: '8.9'
-gitlab_schema: gitlab_main
+gitlab_schema: gitlab_main_cell
diff --git a/db/docs/project_ci_cd_settings.yml b/db/docs/project_ci_cd_settings.yml
index 265ec896247..0f7f59dbb15 100644
--- a/db/docs/project_ci_cd_settings.yml
+++ b/db/docs/project_ci_cd_settings.yml
@@ -9,4 +9,4 @@ feature_categories:
description: Project-scoped settings related to the CI/CD domain
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/392c411bdc16386ef42c86afaf8c4d8e4cddb955
milestone: '10.8'
-gitlab_schema: gitlab_main
+gitlab_schema: gitlab_main_cell
diff --git a/db/docs/project_compliance_standards_adherence.yml b/db/docs/project_compliance_standards_adherence.yml
index c2f08e9f82c..78fbf8a8a46 100644
--- a/db/docs/project_compliance_standards_adherence.yml
+++ b/db/docs/project_compliance_standards_adherence.yml
@@ -7,4 +7,4 @@ feature_categories:
description: Stores the details about projects and their adherence to compliance standards
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/122293
milestone: '16.1'
-gitlab_schema: gitlab_main
+gitlab_schema: gitlab_main_cell
diff --git a/db/docs/project_group_links.yml b/db/docs/project_group_links.yml
index aa981adb745..927fc05bf2a 100644
--- a/db/docs/project_group_links.yml
+++ b/db/docs/project_group_links.yml
@@ -7,4 +7,4 @@ feature_categories:
description: TODO
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/3ac5a759e93e632539438d4564582c645a9f6799
milestone: "<6.0"
-gitlab_schema: gitlab_main
+gitlab_schema: gitlab_main_cell
diff --git a/db/docs/project_import_data.yml b/db/docs/project_import_data.yml
index 283657a1dd3..d0ea6a3f2bc 100644
--- a/db/docs/project_import_data.yml
+++ b/db/docs/project_import_data.yml
@@ -7,4 +7,4 @@ feature_categories:
description: Used to store credentials and configuration of external projects when using the Import/Export feature
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/7d98c8842d6bc9b14fb410f028db7ab651961b42
milestone: '7.10'
-gitlab_schema: gitlab_main
+gitlab_schema: gitlab_main_cell
diff --git a/db/docs/project_pages_metadata.yml b/db/docs/project_pages_metadata.yml
index d9b609d7784..e0d70015784 100644
--- a/db/docs/project_pages_metadata.yml
+++ b/db/docs/project_pages_metadata.yml
@@ -7,4 +7,4 @@ feature_categories:
description: Store GitLab Pages metadata for projects.
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/17197
milestone: '12.4'
-gitlab_schema: gitlab_main
+gitlab_schema: gitlab_main_cell
diff --git a/db/docs/project_repositories.yml b/db/docs/project_repositories.yml
index 2a3e37098c7..fdad3bb3e4f 100644
--- a/db/docs/project_repositories.yml
+++ b/db/docs/project_repositories.yml
@@ -7,4 +7,4 @@ feature_categories:
description: Keeps disk path to repositories and link to the shard
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/8614
milestone: '11.6'
-gitlab_schema: gitlab_main
+gitlab_schema: gitlab_main_cell
diff --git a/db/docs/project_security_settings.yml b/db/docs/project_security_settings.yml
index af559d11164..dd098aef0bc 100644
--- a/db/docs/project_security_settings.yml
+++ b/db/docs/project_security_settings.yml
@@ -8,4 +8,4 @@ feature_categories:
description: Project settings related to security features.
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/32577
milestone: '13.1'
-gitlab_schema: gitlab_main
+gitlab_schema: gitlab_main_cell
diff --git a/db/docs/project_settings.yml b/db/docs/project_settings.yml
index 63e96e34dc5..d9b1c68a0b9 100644
--- a/db/docs/project_settings.yml
+++ b/db/docs/project_settings.yml
@@ -7,4 +7,4 @@ feature_categories:
description: Stores settings per project
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/a2a7ad291f64a5db74c1bc21fb556e6e8862d0f3
milestone: '10.8'
-gitlab_schema: gitlab_main
+gitlab_schema: gitlab_main_cell
diff --git a/db/docs/project_statistics.yml b/db/docs/project_statistics.yml
index 9bc6175b45f..a9d47be3bb4 100644
--- a/db/docs/project_statistics.yml
+++ b/db/docs/project_statistics.yml
@@ -7,4 +7,4 @@ feature_categories:
description: Records statistics about the usage of various product features
introduced_by_url: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/7754
milestone: '8.16'
-gitlab_schema: gitlab_main
+gitlab_schema: gitlab_main_cell
diff --git a/db/docs/project_wiki_repositories.yml b/db/docs/project_wiki_repositories.yml
index 7da09b7fffe..666b76aa498 100644
--- a/db/docs/project_wiki_repositories.yml
+++ b/db/docs/project_wiki_repositories.yml
@@ -7,4 +7,4 @@ feature_categories:
description: Stores information about project wiki repositories.
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/103399
milestone: '15.6'
-gitlab_schema: gitlab_main
+gitlab_schema: gitlab_main_cell
diff --git a/db/docs/projects_sync_events.yml b/db/docs/projects_sync_events.yml
index 84b99fe6080..aca4b407902 100644
--- a/db/docs/projects_sync_events.yml
+++ b/db/docs/projects_sync_events.yml
@@ -7,4 +7,4 @@ feature_categories:
description: Used as a queue of data that needs to be synchronized between the `ci` and `main` database
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/75517
milestone: '14.6'
-gitlab_schema: gitlab_main
+gitlab_schema: gitlab_main_cell
diff --git a/db/docs/protected_branch_merge_access_levels.yml b/db/docs/protected_branch_merge_access_levels.yml
index 3a348825dce..f0a11ef5489 100644
--- a/db/docs/protected_branch_merge_access_levels.yml
+++ b/db/docs/protected_branch_merge_access_levels.yml
@@ -7,4 +7,4 @@ feature_categories:
description: Stores merge access settings for protected branches
introduced_by_url: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/5081
milestone: '8.11'
-gitlab_schema: gitlab_main
+gitlab_schema: gitlab_main_cell
diff --git a/db/docs/protected_branch_push_access_levels.yml b/db/docs/protected_branch_push_access_levels.yml
index 24865372ad0..e614c3d4838 100644
--- a/db/docs/protected_branch_push_access_levels.yml
+++ b/db/docs/protected_branch_push_access_levels.yml
@@ -7,4 +7,4 @@ feature_categories:
description: Stores push access settings for protected branches
introduced_by_url: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/5081
milestone: '8.11'
-gitlab_schema: gitlab_main
+gitlab_schema: gitlab_main_cell
diff --git a/db/docs/protected_branches.yml b/db/docs/protected_branches.yml
index 7c3132336e2..dcd1fc28cd3 100644
--- a/db/docs/protected_branches.yml
+++ b/db/docs/protected_branches.yml
@@ -8,4 +8,4 @@ feature_categories:
description: Keeps a list of protected branches by project
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/37224dc9c1ee80ba9030b616e2bc87bd96919e09
milestone: "<6.0"
-gitlab_schema: gitlab_main
+gitlab_schema: gitlab_main_cell
diff --git a/db/docs/push_rules.yml b/db/docs/push_rules.yml
index 85c609719b6..1579268a9bb 100644
--- a/db/docs/push_rules.yml
+++ b/db/docs/push_rules.yml
@@ -7,4 +7,4 @@ feature_categories:
description: TODO
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/1b98b5ab97ce3e9997df542059cbf3c6ce0bf0e1
milestone: '8.10'
-gitlab_schema: gitlab_main
+gitlab_schema: gitlab_main_cell
diff --git a/db/docs/remote_mirrors.yml b/db/docs/remote_mirrors.yml
index 2ae633eb023..4d32c94f257 100644
--- a/db/docs/remote_mirrors.yml
+++ b/db/docs/remote_mirrors.yml
@@ -7,4 +7,4 @@ feature_categories:
description: Stores push mirrors and their update statuses
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/249
milestone: '8.7'
-gitlab_schema: gitlab_main
+gitlab_schema: gitlab_main_cell
diff --git a/db/docs/repository_languages.yml b/db/docs/repository_languages.yml
index 506c607cf54..92786d7ec18 100644
--- a/db/docs/repository_languages.yml
+++ b/db/docs/repository_languages.yml
@@ -7,4 +7,4 @@ feature_categories:
description: Keeps relation between projects and repository languages
introduced_by_url: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/19480
milestone: '11.2'
-gitlab_schema: gitlab_main
+gitlab_schema: gitlab_main_cell
diff --git a/db/docs/security_orchestration_policy_configurations.yml b/db/docs/security_orchestration_policy_configurations.yml
index c015de47123..388df529835 100644
--- a/db/docs/security_orchestration_policy_configurations.yml
+++ b/db/docs/security_orchestration_policy_configurations.yml
@@ -9,4 +9,4 @@ description: |
Policies are stored in the repository as a YAML file.
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/53743
milestone: '13.9'
-gitlab_schema: gitlab_main
+gitlab_schema: gitlab_main_cell
diff --git a/db/docs/service_access_tokens.yml b/db/docs/service_access_tokens.yml
index 2acd0d33c7d..c75b62883b0 100644
--- a/db/docs/service_access_tokens.yml
+++ b/db/docs/service_access_tokens.yml
@@ -3,7 +3,7 @@ table_name: service_access_tokens
classes:
- Ai::ServiceAccessToken
feature_categories:
-- application_performance
+- cloud_connector
description: Persists JWT tokens for AI features (e.g. Code Suggestions) to authenticate
the GitLab instance
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/125383
diff --git a/db/docs/topics.yml b/db/docs/topics.yml
index dcf988c58eb..42fc6a9f4e3 100644
--- a/db/docs/topics.yml
+++ b/db/docs/topics.yml
@@ -7,4 +7,4 @@ feature_categories:
description: Stores topics that can be assigned to projects
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/67574
milestone: '14.3'
-gitlab_schema: gitlab_main
+gitlab_schema: gitlab_main_cell
diff --git a/db/docs/web_hook_logs.yml b/db/docs/web_hook_logs.yml
index d342c9a9ed0..2635b94f9e6 100644
--- a/db/docs/web_hook_logs.yml
+++ b/db/docs/web_hook_logs.yml
@@ -7,4 +7,4 @@ feature_categories:
description: Webhooks logs data.
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/330789c23c777d8ca646eba7c25f39cb7342cdee
milestone: '9.3'
-gitlab_schema: gitlab_main
+gitlab_schema: gitlab_main_cell
diff --git a/db/docs/zoekt_nodes.yml b/db/docs/zoekt_nodes.yml
new file mode 100644
index 00000000000..2c0740d8b60
--- /dev/null
+++ b/db/docs/zoekt_nodes.yml
@@ -0,0 +1,10 @@
+---
+table_name: zoekt_nodes
+classes:
+- Search::Zoekt::Node
+feature_categories:
+- global_search
+description: Describes a Zoekt server that will be used for indexing and search for some configured namespaces
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134901
+milestone: '16.6'
+gitlab_schema: gitlab_main
diff --git a/db/docs/zoekt_shards.yml b/db/docs/zoekt_shards.yml
index 5fe3b469b19..31f918c2a7b 100644
--- a/db/docs/zoekt_shards.yml
+++ b/db/docs/zoekt_shards.yml
@@ -1,10 +1,11 @@
---
table_name: zoekt_shards
classes:
-- Zoekt::Shard
+- Search::Zoekt::Node
feature_categories:
- global_search
-description: Describes a Zoekt server that will be used for indexing and search for some configured namespaces
+description: Describes a Zoekt server that will be used for indexing and search for
+ some configured namespaces
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/105049
milestone: '15.9'
gitlab_schema: gitlab_main
diff --git a/db/migrate/20230529182720_recreate_billable_index.rb b/db/migrate/20230529182720_recreate_billable_index.rb
index 5e56dd7005a..a983dc5f295 100644
--- a/db/migrate/20230529182720_recreate_billable_index.rb
+++ b/db/migrate/20230529182720_recreate_billable_index.rb
@@ -8,8 +8,10 @@ class RecreateBillableIndex < Gitlab::Database::Migration[2.1]
def up
remove_concurrent_index_by_name :users, INDEX_NAME
+ # rubocop:disable Migration/PreventIndexCreation
add_concurrent_index :users, :id, name: INDEX_NAME,
where: "state = 'active' AND (user_type IN (0, 6, 4, 13)) AND (user_type IN (0, 4, 5))"
+ # rubocop:enable Migration/PreventIndexCreation
end
def down
diff --git a/db/migrate/20230529184716_recreated_activity_index.rb b/db/migrate/20230529184716_recreated_activity_index.rb
index 2b949d39de1..c5c76b8ec14 100644
--- a/db/migrate/20230529184716_recreated_activity_index.rb
+++ b/db/migrate/20230529184716_recreated_activity_index.rb
@@ -8,9 +8,11 @@ class RecreatedActivityIndex < Gitlab::Database::Migration[2.1]
def up
remove_concurrent_index_by_name :users, INDEX_NAME
+ # rubocop:disable Migration/PreventIndexCreation
add_concurrent_index :users, [:id, :last_activity_on],
name: INDEX_NAME,
where: "state = 'active' AND user_type IN (0, 4)"
+ # rubocop:enable Migration/PreventIndexCreation
end
def down
diff --git a/db/migrate/20230605043258_add_unconfirmed_created_at_index_to_users.rb b/db/migrate/20230605043258_add_unconfirmed_created_at_index_to_users.rb
index 65bd7a1266b..bd3a7006972 100644
--- a/db/migrate/20230605043258_add_unconfirmed_created_at_index_to_users.rb
+++ b/db/migrate/20230605043258_add_unconfirmed_created_at_index_to_users.rb
@@ -6,9 +6,11 @@ class AddUnconfirmedCreatedAtIndexToUsers < Gitlab::Database::Migration[2.1]
INDEX_NAME = 'index_users_on_unconfirmed_and_created_at_for_active_humans'
def up
+ # rubocop:disable Migration/PreventIndexCreation
add_concurrent_index :users, [:created_at, :id],
name: INDEX_NAME,
where: "confirmed_at IS NULL AND state = 'active' AND user_type IN (0)"
+ # rubocop:enable Migration/PreventIndexCreation
end
def down
diff --git a/db/migrate/20230926092914_add_approval_group_rules.rb b/db/migrate/20230926092914_add_approval_group_rules.rb
new file mode 100644
index 00000000000..c5f4a356df1
--- /dev/null
+++ b/db/migrate/20230926092914_add_approval_group_rules.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+class AddApprovalGroupRules < Gitlab::Database::Migration[2.1]
+ INDEX_GROUP_ID_TYPE_NAME = 'idx_on_approval_group_rules_group_id_type_name'
+ INDEX_ANY_APPROVER_TYPE = 'idx_on_approval_group_rules_any_approver_type'
+ INDEX_SECURITY_ORCHESTRATION_POLICY_CONFURATION = 'idx_on_approval_group_rules_security_orch_policy'
+ disable_ddl_transaction!
+
+ def up
+ create_table :approval_group_rules do |t|
+ t.references :group, references: :namespaces, null: false,
+ foreign_key: { to_table: :namespaces, on_delete: :cascade }, index: false
+ t.timestamps_with_timezone
+ t.integer :approvals_required, limit: 2, null: false, default: 0
+ t.integer :report_type, limit: 2, null: true, default: nil
+ t.integer :rule_type, limit: 2, null: false, default: 1
+ t.integer :security_orchestration_policy_configuration_id, limit: 5
+ t.integer :scan_result_policy_id, limit: 5, index: true
+ t.text :name, null: false, limit: 255
+
+ t.index [:group_id, :rule_type, :name], unique: true, name: INDEX_GROUP_ID_TYPE_NAME
+ t.index [:group_id, :rule_type], where: 'rule_type = 4', unique: true, name: INDEX_ANY_APPROVER_TYPE
+ t.index :security_orchestration_policy_configuration_id, name: INDEX_SECURITY_ORCHESTRATION_POLICY_CONFURATION
+ end
+
+ add_text_limit :approval_group_rules, :name, 255
+ end
+
+ def down
+ with_lock_retries do
+ drop_table :approval_group_rules
+ end
+ end
+end
diff --git a/db/migrate/20230926092944_add_approval_group_rules_groups.rb b/db/migrate/20230926092944_add_approval_group_rules_groups.rb
new file mode 100644
index 00000000000..52ac86737e6
--- /dev/null
+++ b/db/migrate/20230926092944_add_approval_group_rules_groups.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class AddApprovalGroupRulesGroups < Gitlab::Database::Migration[2.1]
+ INDEX_RULE_GROUP = 'idx_on_approval_group_rules_groups_rule_group'
+
+ def up
+ create_table :approval_group_rules_groups do |t|
+ t.bigint :approval_group_rule_id, null: false
+ t.bigint :group_id, null: false, index: true
+
+ t.index [:approval_group_rule_id, :group_id], unique: true, name: INDEX_RULE_GROUP
+ end
+ end
+
+ def down
+ drop_table :approval_group_rules_groups
+ end
+end
diff --git a/db/migrate/20230926093004_add_approval_group_rules_users.rb b/db/migrate/20230926093004_add_approval_group_rules_users.rb
new file mode 100644
index 00000000000..8c6d14ce9ac
--- /dev/null
+++ b/db/migrate/20230926093004_add_approval_group_rules_users.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class AddApprovalGroupRulesUsers < Gitlab::Database::Migration[2.1]
+ INDEX_RULE_USER = 'idx_on_approval_group_rules_users_rule_user'
+
+ def up
+ create_table :approval_group_rules_users do |t|
+ t.bigint :approval_group_rule_id, null: false
+ t.bigint :user_id, null: false, index: true
+
+ t.index [:approval_group_rule_id, :user_id], unique: true, name: INDEX_RULE_USER
+ end
+ end
+
+ def down
+ drop_table :approval_group_rules_users
+ end
+end
diff --git a/db/migrate/20230926093025_add_approval_group_rules_protected_branches.rb b/db/migrate/20230926093025_add_approval_group_rules_protected_branches.rb
new file mode 100644
index 00000000000..5f623ec9edb
--- /dev/null
+++ b/db/migrate/20230926093025_add_approval_group_rules_protected_branches.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class AddApprovalGroupRulesProtectedBranches < Gitlab::Database::Migration[2.1]
+ INDEX_RULE_PROTECTED_BRANCH = 'idx_on_approval_group_rules_protected_branch'
+ INDEX_APPROVAL_GROUP_RULE = 'idx_on_approval_group_rules'
+ INDEX_PROTECTED_BRANCH = 'idx_on_protected_branch'
+
+ def up
+ create_table :approval_group_rules_protected_branches do |t|
+ t.bigint :approval_group_rule_id, null: false
+ t.bigint :protected_branch_id, null: false
+
+ t.index :protected_branch_id, name: INDEX_PROTECTED_BRANCH
+ t.index [:approval_group_rule_id, :protected_branch_id], unique: true, name: INDEX_RULE_PROTECTED_BRANCH
+ end
+ end
+
+ def down
+ drop_table :approval_group_rules_protected_branches
+ end
+end
diff --git a/db/migrate/20230926093101_add_fk_to_approval_rule_on_approval_group_rules_users.rb b/db/migrate/20230926093101_add_fk_to_approval_rule_on_approval_group_rules_users.rb
new file mode 100644
index 00000000000..4c11542e9e6
--- /dev/null
+++ b/db/migrate/20230926093101_add_fk_to_approval_rule_on_approval_group_rules_users.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class AddFkToApprovalRuleOnApprovalGroupRulesUsers < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_foreign_key :approval_group_rules_users,
+ :approval_group_rules,
+ column: :approval_group_rule_id,
+ on_delete: :cascade
+ end
+
+ def down
+ with_lock_retries do
+ remove_foreign_key :approval_group_rules_users, column: :approval_group_rule_id
+ end
+ end
+end
diff --git a/db/migrate/20230926093144_add_fk_to_user_on_approval_group_rules_users.rb b/db/migrate/20230926093144_add_fk_to_user_on_approval_group_rules_users.rb
new file mode 100644
index 00000000000..30c08c8966d
--- /dev/null
+++ b/db/migrate/20230926093144_add_fk_to_user_on_approval_group_rules_users.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class AddFkToUserOnApprovalGroupRulesUsers < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_foreign_key :approval_group_rules_users, :users, column: :user_id, on_delete: :cascade
+ end
+
+ def down
+ with_lock_retries do
+ remove_foreign_key :approval_group_rules_users, column: :user_id
+ end
+ end
+end
diff --git a/db/migrate/20230926093211_add_fk_to_approval_rule_on_approval_group_rules_groups.rb b/db/migrate/20230926093211_add_fk_to_approval_rule_on_approval_group_rules_groups.rb
new file mode 100644
index 00000000000..44526150266
--- /dev/null
+++ b/db/migrate/20230926093211_add_fk_to_approval_rule_on_approval_group_rules_groups.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class AddFkToApprovalRuleOnApprovalGroupRulesGroups < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_foreign_key :approval_group_rules_groups, :approval_group_rules, column: :approval_group_rule_id,
+ on_delete: :cascade
+ end
+
+ def down
+ with_lock_retries do
+ remove_foreign_key :approval_group_rules_groups, column: :approval_group_rule_id
+ end
+ end
+end
diff --git a/db/migrate/20230926093251_add_fk_to_group_on_approval_group_rules_groups.rb b/db/migrate/20230926093251_add_fk_to_group_on_approval_group_rules_groups.rb
new file mode 100644
index 00000000000..2052993af05
--- /dev/null
+++ b/db/migrate/20230926093251_add_fk_to_group_on_approval_group_rules_groups.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class AddFkToGroupOnApprovalGroupRulesGroups < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_foreign_key :approval_group_rules_groups, :namespaces, column: :group_id, on_delete: :cascade
+ end
+
+ def down
+ with_lock_retries do
+ remove_foreign_key :approval_group_rules_groups, column: :group_id
+ end
+ end
+end
diff --git a/db/migrate/20230926105440_add_fk_to_approval_rule_on_approval_group_rules_protected_branches.rb b/db/migrate/20230926105440_add_fk_to_approval_rule_on_approval_group_rules_protected_branches.rb
new file mode 100644
index 00000000000..cd799656ac9
--- /dev/null
+++ b/db/migrate/20230926105440_add_fk_to_approval_rule_on_approval_group_rules_protected_branches.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class AddFkToApprovalRuleOnApprovalGroupRulesProtectedBranches < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_foreign_key :approval_group_rules_protected_branches,
+ :approval_group_rules,
+ column: :approval_group_rule_id,
+ on_delete: :cascade
+ end
+
+ def down
+ with_lock_retries do
+ remove_foreign_key :approval_group_rules_protected_branches, column: :approval_group_rule_id
+ end
+ end
+end
diff --git a/db/migrate/20230926105931_add_fk_to_protected_branch_on_approval_group_rules_protected_branches.rb b/db/migrate/20230926105931_add_fk_to_protected_branch_on_approval_group_rules_protected_branches.rb
new file mode 100644
index 00000000000..5804a8da4d8
--- /dev/null
+++ b/db/migrate/20230926105931_add_fk_to_protected_branch_on_approval_group_rules_protected_branches.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class AddFkToProtectedBranchOnApprovalGroupRulesProtectedBranches < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_foreign_key :approval_group_rules_protected_branches, :protected_branches,
+ column: :protected_branch_id, on_delete: :cascade
+ end
+
+ def down
+ with_lock_retries do
+ remove_foreign_key :approval_group_rules_protected_branches, column: :protected_branch_id
+ end
+ end
+end
diff --git a/db/migrate/20230927124202_add_mastodon_to_user_details.rb b/db/migrate/20230927124202_add_mastodon_to_user_details.rb
new file mode 100644
index 00000000000..a1aa099087b
--- /dev/null
+++ b/db/migrate/20230927124202_add_mastodon_to_user_details.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class AddMastodonToUserDetails < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ USER_DETAILS_FIELD_LIMIT = 500
+
+ def up
+ with_lock_retries do
+ add_column :user_details, :mastodon, :text, default: '', null: false, if_not_exists: true
+ end
+
+ add_text_limit :user_details, :mastodon, USER_DETAILS_FIELD_LIMIT
+ end
+
+ def down
+ with_lock_retries do
+ remove_column :user_details, :mastodon
+ end
+ end
+end
diff --git a/db/migrate/20230928145555_add_fk_to_security_orchestration_policy_configuration_on_approval_group_rules.rb b/db/migrate/20230928145555_add_fk_to_security_orchestration_policy_configuration_on_approval_group_rules.rb
new file mode 100644
index 00000000000..2630adcf81f
--- /dev/null
+++ b/db/migrate/20230928145555_add_fk_to_security_orchestration_policy_configuration_on_approval_group_rules.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddFkToSecurityOrchestrationPolicyConfigurationOnApprovalGroupRules < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_foreign_key :approval_group_rules, :security_orchestration_policy_configurations,
+ column: :security_orchestration_policy_configuration_id, on_delete: :cascade
+ end
+
+ def down
+ with_lock_retries do
+ remove_foreign_key :approval_group_rules,
+ column: :security_orchestration_policy_configuration_id
+ end
+ end
+end
diff --git a/db/migrate/20230928145637_add_fk_to_scan_result_policy_on_approval_group_rules.rb b/db/migrate/20230928145637_add_fk_to_scan_result_policy_on_approval_group_rules.rb
new file mode 100644
index 00000000000..f30d03e0f62
--- /dev/null
+++ b/db/migrate/20230928145637_add_fk_to_scan_result_policy_on_approval_group_rules.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class AddFkToScanResultPolicyOnApprovalGroupRules < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_foreign_key :approval_group_rules, :scan_result_policies,
+ column: :scan_result_policy_id, on_delete: :cascade
+ end
+
+ def down
+ with_lock_retries do
+ remove_foreign_key :approval_group_rules, column: :scan_result_policy_id
+ end
+ end
+end
diff --git a/db/migrate/20230929155123_migrate_disable_merge_trains_value.rb b/db/migrate/20230929155123_migrate_disable_merge_trains_value.rb
new file mode 100644
index 00000000000..59eadd07733
--- /dev/null
+++ b/db/migrate/20230929155123_migrate_disable_merge_trains_value.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+class MigrateDisableMergeTrainsValue < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ restrict_gitlab_migration gitlab_schema: :gitlab_main
+
+ class Gate < MigrationRecord
+ self.table_name = 'feature_gates'
+ end
+
+ UPDATE_QUERY = <<-SQL
+ UPDATE project_ci_cd_settings SET merge_trains_enabled = :merge_trains_enabled
+ WHERE project_id IN (:project_ids)
+ SQL
+
+ def update_merge_trains_enabled(project_ids, merge_trains_enabled)
+ ApplicationRecord.connection.execute(
+ ApplicationRecord.sanitize_sql([
+ UPDATE_QUERY,
+ {
+ project_ids: project_ids,
+ merge_trains_enabled: merge_trains_enabled.to_s.upcase
+ }
+ ])
+ )
+ end
+
+ def get_project_ids
+ project_ids = Gate.where(feature_key: :disable_merge_trains, key: 'actors').pluck('value')
+
+ project_ids.filter_map do |project_id|
+ # ensure actor is a project formatted correctly
+ match = project_id.match(/Project:[0-9]+/)[0]
+ # Extract the project id if there is an actor
+ match ? project_id.gsub('Project:', '').to_i : nil
+ end
+ end
+
+ def up
+ project_ids = get_project_ids
+
+ return unless project_ids
+
+ update_merge_trains_enabled(project_ids, false)
+ end
+
+ def down
+ project_ids = get_project_ids
+
+ return unless project_ids
+
+ update_merge_trains_enabled(project_ids, true)
+ end
+end
diff --git a/db/migrate/20231002162941_add_enable_artifact_external_redirect_warning_page_to_application_settings.rb b/db/migrate/20231002162941_add_enable_artifact_external_redirect_warning_page_to_application_settings.rb
new file mode 100644
index 00000000000..06fc4b6b313
--- /dev/null
+++ b/db/migrate/20231002162941_add_enable_artifact_external_redirect_warning_page_to_application_settings.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+class AddEnableArtifactExternalRedirectWarningPageToApplicationSettings < Gitlab::Database::Migration[2.1]
+ enable_lock_retries!
+
+ def change
+ add_column(:application_settings, :enable_artifact_external_redirect_warning_page, :boolean, default: true,
+ null: false)
+ end
+end
diff --git a/db/migrate/20231005151816_add_created_at_to_status_check_responses.rb b/db/migrate/20231005151816_add_created_at_to_status_check_responses.rb
new file mode 100644
index 00000000000..118586f61a8
--- /dev/null
+++ b/db/migrate/20231005151816_add_created_at_to_status_check_responses.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddCreatedAtToStatusCheckResponses < Gitlab::Database::Migration[2.1]
+ def change
+ add_column :status_check_responses, :created_at, :datetime_with_timezone, null: false, default: -> { 'NOW()' }
+ end
+end
diff --git a/db/migrate/20231009115713_remove_duplicate_index_rule_type_four.rb b/db/migrate/20231009115713_remove_duplicate_index_rule_type_four.rb
new file mode 100644
index 00000000000..7fe69c30a81
--- /dev/null
+++ b/db/migrate/20231009115713_remove_duplicate_index_rule_type_four.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class RemoveDuplicateIndexRuleTypeFour < Gitlab::Database::Migration[2.1]
+ INDEX_NAME = 'any_approver_merge_request_rule_type_unique_index'
+
+ disable_ddl_transaction!
+
+ def up
+ remove_concurrent_index_by_name :approval_merge_request_rules, INDEX_NAME
+ end
+
+ def down
+ add_concurrent_index :approval_merge_request_rules, [:merge_request_id, :rule_type], where: 'rule_type = 4',
+ name: INDEX_NAME, unique: true
+ end
+end
diff --git a/db/migrate/20231013204933_remove_tasks_to_be_done_worker.rb b/db/migrate/20231013204933_remove_tasks_to_be_done_worker.rb
new file mode 100644
index 00000000000..d5e8ecfe370
--- /dev/null
+++ b/db/migrate/20231013204933_remove_tasks_to_be_done_worker.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class RemoveTasksToBeDoneWorker < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ DEPRECATED_JOB_CLASSES = %w[TasksToBeDone::CreateWorker]
+
+ def up
+ sidekiq_remove_jobs(job_klasses: DEPRECATED_JOB_CLASSES)
+ end
+
+ def down
+ # This migration removes any instances of deprecated workers and cannot be undone.
+ end
+end
diff --git a/db/migrate/20231017095738_create_activity_pub_releases_subscriptions.rb b/db/migrate/20231017095738_create_activity_pub_releases_subscriptions.rb
new file mode 100644
index 00000000000..19693c29a33
--- /dev/null
+++ b/db/migrate/20231017095738_create_activity_pub_releases_subscriptions.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+class CreateActivityPubReleasesSubscriptions < Gitlab::Database::Migration[2.1]
+ enable_lock_retries!
+
+ def up
+ create_table :activity_pub_releases_subscriptions do |t|
+ t.references :project, index: false, foreign_key: { on_delete: :cascade }, null: false
+ t.timestamps_with_timezone null: false
+ t.integer :status, null: false, limit: 2, default: 1
+ t.text :shared_inbox_url, limit: 1024
+ t.text :subscriber_inbox_url, limit: 1024
+ t.text :subscriber_url, limit: 1024, null: false
+ t.jsonb :payload, null: true
+ t.index 'project_id, LOWER(subscriber_url)', name: :index_activity_pub_releases_sub_on_project_id_sub_url,
+ unique: true
+ t.index 'project_id, LOWER(subscriber_inbox_url)',
+ name: :index_activity_pub_releases_sub_on_project_id_inbox_url, unique: true
+ end
+ end
+
+ def down
+ drop_table :activity_pub_releases_subscriptions
+ end
+end
diff --git a/db/migrate/20231017114131_add_auto_canceled_by_partition_id_to_p_ci_builds.rb b/db/migrate/20231017114131_add_auto_canceled_by_partition_id_to_p_ci_builds.rb
index ecc606ca1a8..afd3b60a244 100644
--- a/db/migrate/20231017114131_add_auto_canceled_by_partition_id_to_p_ci_builds.rb
+++ b/db/migrate/20231017114131_add_auto_canceled_by_partition_id_to_p_ci_builds.rb
@@ -1,13 +1,13 @@
# frozen_string_literal: true
class AddAutoCanceledByPartitionIdToPCiBuilds < Gitlab::Database::Migration[2.1]
- include Gitlab::Database::MigrationHelpers::WraparoundAutovacuum
-
- enable_lock_retries!
-
- def change
- return unless can_execute_on?(:ci_builds)
+ def up
+ # no-op
+ # moved to db/migrate/20231020074227_add_auto_canceled_by_partition_id_to_p_ci_builds_self_managed.rb
+ end
- add_column :p_ci_builds, :auto_canceled_by_partition_id, :bigint, default: 100, null: false, if_not_exists: true
+ def down
+ # no-op
+ # moved to db/migrate/20231020074227_add_auto_canceled_by_partition_id_to_p_ci_builds_self_managed.rb
end
end
diff --git a/db/migrate/20231017134349_create_ml_model_metadata.rb b/db/migrate/20231017134349_create_ml_model_metadata.rb
new file mode 100644
index 00000000000..f34ba729677
--- /dev/null
+++ b/db/migrate/20231017134349_create_ml_model_metadata.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class CreateMlModelMetadata < Gitlab::Database::Migration[2.1]
+ ML_MODEL_METADATA_NAME_INDEX_NAME = "unique_index_ml_model_metadata_name"
+
+ def change
+ create_table :ml_model_metadata do |t|
+ t.timestamps_with_timezone null: false
+ t.references :model,
+ foreign_key: { to_table: :ml_models, on_delete: :cascade },
+ index: false,
+ null: false
+ t.text :name, limit: 255, null: false
+ t.text :value, limit: 5000, null: false
+
+ t.index [:model_id, :name], unique: true, name: ML_MODEL_METADATA_NAME_INDEX_NAME
+ end
+ end
+end
diff --git a/db/migrate/20231017135207_add_fields_to_ml_model.rb b/db/migrate/20231017135207_add_fields_to_ml_model.rb
new file mode 100644
index 00000000000..cb937e49491
--- /dev/null
+++ b/db/migrate/20231017135207_add_fields_to_ml_model.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+class AddFieldsToMlModel < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ def up
+ # rubocop:disable Migration/AddLimitToTextColumns
+ # limit is added in 20231018152419_add_text_limit_to_ml_models.rb
+ add_column :ml_models, :description, :text
+ # rubocop:enable Migration/AddLimitToTextColumns
+
+ add_column :ml_models, :user_id, :integer, null: true
+ add_concurrent_foreign_key :ml_models, :users, column: :user_id, on_delete: :nullify
+
+ add_concurrent_index :ml_models, :user_id
+ end
+
+ def down
+ remove_column :ml_models, :description
+ remove_column :ml_models, :user_id
+ remove_foreign_key_if_exists :ml_models, column: :user_id
+ end
+end
diff --git a/db/migrate/20231017154804_add_index_to_status_check_responses_on_id_and_status.rb b/db/migrate/20231017154804_add_index_to_status_check_responses_on_id_and_status.rb
new file mode 100644
index 00000000000..77aa1a1bb0f
--- /dev/null
+++ b/db/migrate/20231017154804_add_index_to_status_check_responses_on_id_and_status.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+class AddIndexToStatusCheckResponsesOnIdAndStatus < Gitlab::Database::Migration[2.1]
+ INDEX_NAME = 'idx_status_check_responses_on_id_and_status'
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :status_check_responses, [:id, :status], name: INDEX_NAME
+ end
+
+ def down
+ remove_concurrent_index_by_name :status_check_responses, name: INDEX_NAME
+ end
+end
diff --git a/db/migrate/20231017181403_add_generated_to_diff_files.rb b/db/migrate/20231017181403_add_generated_to_diff_files.rb
new file mode 100644
index 00000000000..f93669381ef
--- /dev/null
+++ b/db/migrate/20231017181403_add_generated_to_diff_files.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddGeneratedToDiffFiles < Gitlab::Database::Migration[2.1]
+ enable_lock_retries!
+
+ def change
+ add_column :merge_request_diff_files, :generated, :boolean
+ end
+end
diff --git a/db/migrate/20231018140154_remove_hashed_storage_migration_workers_job_instances.rb b/db/migrate/20231018140154_remove_hashed_storage_migration_workers_job_instances.rb
new file mode 100644
index 00000000000..73105a76249
--- /dev/null
+++ b/db/migrate/20231018140154_remove_hashed_storage_migration_workers_job_instances.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class RemoveHashedStorageMigrationWorkersJobInstances < Gitlab::Database::Migration[2.1]
+ DEPRECATED_JOB_CLASSES = %w[
+ HashedStorage::MigratorWorker
+ HashedStorage::ProjectMigrateWorker
+ HashedStorage::ProjectRollbackWorker
+ HashedStorage::RollbackerWorker
+ HashedStorage::BaseWorker
+ ]
+
+ disable_ddl_transaction!
+
+ def up
+ sidekiq_remove_jobs(job_klasses: DEPRECATED_JOB_CLASSES)
+ end
+
+ def down
+ # This migration removes any instances of deprecated workers and cannot be undone.
+ end
+end
diff --git a/db/migrate/20231018152419_add_text_limit_to_ml_models.rb b/db/migrate/20231018152419_add_text_limit_to_ml_models.rb
new file mode 100644
index 00000000000..179d6f20b53
--- /dev/null
+++ b/db/migrate/20231018152419_add_text_limit_to_ml_models.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class AddTextLimitToMlModels < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ def up
+ add_text_limit :ml_models, :description, 5000
+ end
+
+ def down
+ remove_text_limit :ml_models, :description
+ end
+end
diff --git a/db/migrate/20231019104211_add_file_sha256_to_packages_nuget_symbols.rb b/db/migrate/20231019104211_add_file_sha256_to_packages_nuget_symbols.rb
new file mode 100644
index 00000000000..374fa91000d
--- /dev/null
+++ b/db/migrate/20231019104211_add_file_sha256_to_packages_nuget_symbols.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class AddFileSha256ToPackagesNugetSymbols < Gitlab::Database::Migration[2.1]
+ enable_lock_retries!
+
+ def up
+ add_column :packages_nuget_symbols, :file_sha256, :binary
+ end
+
+ def down
+ remove_column :packages_nuget_symbols, :file_sha256
+ end
+end
diff --git a/db/migrate/20231019122855_add_semver_index_ci_runner_machines.rb b/db/migrate/20231019122855_add_semver_index_ci_runner_machines.rb
new file mode 100644
index 00000000000..b09f3cda60e
--- /dev/null
+++ b/db/migrate/20231019122855_add_semver_index_ci_runner_machines.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+class AddSemverIndexCiRunnerMachines < Gitlab::Database::Migration[2.1]
+ MAJOR_INDEX_NAME = 'index_ci_runner_machines_on_major_version_trigram'
+ MINOR_INDEX_NAME = 'index_ci_runner_machines_on_minor_version_trigram'
+ PATCH_INDEX_NAME = 'index_ci_runner_machines_on_patch_version_trigram'
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :ci_runner_machines, %q[((substring(version from '^\d+\.'))), version, runner_id],
+ name: MAJOR_INDEX_NAME
+ add_concurrent_index :ci_runner_machines, %q[((substring(version from '^\d+\.\d+\.'))), version, runner_id],
+ name: MINOR_INDEX_NAME
+ add_concurrent_index :ci_runner_machines, %q[((substring(version from '^\d+\.\d+\.\d+'))), version, runner_id],
+ name: PATCH_INDEX_NAME
+ end
+
+ def down
+ remove_concurrent_index_by_name :ci_runner_machines, MAJOR_INDEX_NAME
+ remove_concurrent_index_by_name :ci_runner_machines, MINOR_INDEX_NAME
+ remove_concurrent_index_by_name :ci_runner_machines, PATCH_INDEX_NAME
+ end
+end
diff --git a/db/migrate/20231019145202_add_status_to_packages_npm_metadata_caches.rb b/db/migrate/20231019145202_add_status_to_packages_npm_metadata_caches.rb
new file mode 100644
index 00000000000..f3d910e9350
--- /dev/null
+++ b/db/migrate/20231019145202_add_status_to_packages_npm_metadata_caches.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddStatusToPackagesNpmMetadataCaches < Gitlab::Database::Migration[2.1]
+ def change
+ add_column :packages_npm_metadata_caches, :status, :integer, default: 0, null: false, limit: 2
+ end
+end
diff --git a/db/migrate/20231019180421_add_name_description_to_catalog_resources.rb b/db/migrate/20231019180421_add_name_description_to_catalog_resources.rb
new file mode 100644
index 00000000000..391d56342be
--- /dev/null
+++ b/db/migrate/20231019180421_add_name_description_to_catalog_resources.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+class AddNameDescriptionToCatalogResources < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ NAME_INDEX = 'index_catalog_resources_on_name_trigram'
+ DESCRIPTION_INDEX = 'index_catalog_resources_on_description_trigram'
+
+ def up
+ # These columns must match the settings for the corresponding columns in the `projects` table
+ add_column :catalog_resources, :name, :varchar, null: true
+ add_column :catalog_resources, :description, :text, null: true # rubocop: disable Migration/AddLimitToTextColumns
+
+ add_concurrent_index :catalog_resources, :name, name: NAME_INDEX,
+ using: :gin, opclass: { name: :gin_trgm_ops }
+
+ add_concurrent_index :catalog_resources, :description, name: DESCRIPTION_INDEX,
+ using: :gin, opclass: { description: :gin_trgm_ops }
+ end
+
+ def down
+ remove_column :catalog_resources, :name
+ remove_column :catalog_resources, :description
+
+ remove_concurrent_index_by_name :catalog_resources, NAME_INDEX
+ remove_concurrent_index_by_name :catalog_resources, DESCRIPTION_INDEX
+ end
+end
diff --git a/db/migrate/20231020020732_add_user_phone_number_validation_telesign_reference_xid_index.rb b/db/migrate/20231020020732_add_user_phone_number_validation_telesign_reference_xid_index.rb
new file mode 100644
index 00000000000..4a0343f5809
--- /dev/null
+++ b/db/migrate/20231020020732_add_user_phone_number_validation_telesign_reference_xid_index.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class AddUserPhoneNumberValidationTelesignReferenceXidIndex < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ INDEX_NAME = 'index_user_phone_number_validations_on_telesign_reference_xid'
+
+ def up
+ add_concurrent_index(:user_phone_number_validations, :telesign_reference_xid, name: INDEX_NAME)
+ end
+
+ def down
+ remove_concurrent_index_by_name(:user_phone_number_validations, INDEX_NAME)
+ end
+end
diff --git a/db/migrate/20231020074227_add_auto_canceled_by_partition_id_to_p_ci_builds_self_managed.rb b/db/migrate/20231020074227_add_auto_canceled_by_partition_id_to_p_ci_builds_self_managed.rb
new file mode 100644
index 00000000000..5aa5d6c42ae
--- /dev/null
+++ b/db/migrate/20231020074227_add_auto_canceled_by_partition_id_to_p_ci_builds_self_managed.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class AddAutoCanceledByPartitionIdToPCiBuildsSelfManaged < Gitlab::Database::Migration[2.1]
+ enable_lock_retries!
+
+ def up
+ add_column :p_ci_builds, :auto_canceled_by_partition_id, :bigint, default: 100, null: false, if_not_exists: true
+ end
+
+ def down
+ remove_column :p_ci_builds, :auto_canceled_by_partition_id, if_exists: true
+ end
+end
diff --git a/db/migrate/20231020095624_create_audit_events_streaming_http_group_namespace_filters.rb b/db/migrate/20231020095624_create_audit_events_streaming_http_group_namespace_filters.rb
new file mode 100644
index 00000000000..07a580a12b2
--- /dev/null
+++ b/db/migrate/20231020095624_create_audit_events_streaming_http_group_namespace_filters.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class CreateAuditEventsStreamingHttpGroupNamespaceFilters < Gitlab::Database::Migration[2.1]
+ enable_lock_retries!
+
+ UNIQ_DESTINATION_INDEX_NAME = 'unique_audit_events_group_namespace_filters_destination_id'
+ UNIQ_NAMESPACE_INDEX_NAME = 'unique_audit_events_group_namespace_filters_namespace_id'
+
+ def change
+ create_table :audit_events_streaming_http_group_namespace_filters do |t|
+ t.timestamps_with_timezone null: false
+ t.references :external_audit_event_destination,
+ null: false,
+ index: { unique: true, name: UNIQ_DESTINATION_INDEX_NAME },
+ foreign_key: { to_table: 'audit_events_external_audit_event_destinations', on_delete: :cascade }
+ t.references :namespace,
+ null: false,
+ index: { unique: true, name: UNIQ_NAMESPACE_INDEX_NAME },
+ foreign_key: { on_delete: :cascade }
+ end
+ end
+end
diff --git a/db/migrate/20231020112541_add_column_model_version_id_to_ml_candidates.rb b/db/migrate/20231020112541_add_column_model_version_id_to_ml_candidates.rb
new file mode 100644
index 00000000000..7bfe78c4ebd
--- /dev/null
+++ b/db/migrate/20231020112541_add_column_model_version_id_to_ml_candidates.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddColumnModelVersionIdToMlCandidates < Gitlab::Database::Migration[2.1]
+ def change
+ add_column :ml_candidates, :model_version_id, :bigint, null: true
+ end
+end
diff --git a/db/migrate/20231020181652_add_index_packages_npm_metadata_caches_on_id_and_project_id_and_status.rb b/db/migrate/20231020181652_add_index_packages_npm_metadata_caches_on_id_and_project_id_and_status.rb
new file mode 100644
index 00000000000..6350ad935ca
--- /dev/null
+++ b/db/migrate/20231020181652_add_index_packages_npm_metadata_caches_on_id_and_project_id_and_status.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class AddIndexPackagesNpmMetadataCachesOnIdAndProjectIdAndStatus < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ INDEX_NAME = 'idx_pkgs_npm_metadata_caches_on_id_and_project_id_and_status'
+ NPM_METADATA_CACHES_STATUS_DEFAULT = 0
+
+ def up
+ where = "project_id IS NULL AND status = #{NPM_METADATA_CACHES_STATUS_DEFAULT}"
+
+ add_concurrent_index :packages_npm_metadata_caches, :id, name: INDEX_NAME, where: where
+ end
+
+ def down
+ remove_concurrent_index_by_name :packages_npm_metadata_caches, name: INDEX_NAME
+ end
+end
diff --git a/db/migrate/20231023073841_add_indexes_to_project_compliance_standards_adherence.rb b/db/migrate/20231023073841_add_indexes_to_project_compliance_standards_adherence.rb
new file mode 100644
index 00000000000..0a593547ddb
--- /dev/null
+++ b/db/migrate/20231023073841_add_indexes_to_project_compliance_standards_adherence.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class AddIndexesToProjectComplianceStandardsAdherence < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ INDEX_NAMESPACE_ID = 'index_project_compliance_standards_adherence_on_namespace_id'
+ INDEX_NAMESPACE_AND_PROJECT_ID_DESC = 'i_compliance_standards_adherence_on_namespace_id_and_proj_id'
+
+ def up
+ add_concurrent_index :project_compliance_standards_adherence, [:namespace_id, :project_id, :id],
+ order: { project_id: :desc, id: :desc }, using: :btree, name: INDEX_NAMESPACE_AND_PROJECT_ID_DESC
+
+ remove_concurrent_index_by_name :project_compliance_standards_adherence, INDEX_NAMESPACE_ID
+ end
+
+ def down
+ add_concurrent_index :project_compliance_standards_adherence, :namespace_id, name: INDEX_NAMESPACE_ID
+
+ remove_concurrent_index_by_name :project_compliance_standards_adherence, INDEX_NAMESPACE_AND_PROJECT_ID_DESC
+ end
+end
diff --git a/db/migrate/20231023114006_add_index_on_model_version_id_to_ml_candidates.rb b/db/migrate/20231023114006_add_index_on_model_version_id_to_ml_candidates.rb
new file mode 100644
index 00000000000..598600b8539
--- /dev/null
+++ b/db/migrate/20231023114006_add_index_on_model_version_id_to_ml_candidates.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class AddIndexOnModelVersionIdToMlCandidates < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ INDEX_NAME = 'index_ml_candidates_on_model_version_id'
+
+ def up
+ add_concurrent_index :ml_candidates, :model_version_id, name: INDEX_NAME, unique: true
+ end
+
+ def down
+ remove_concurrent_index_by_name :ml_candidates, name: INDEX_NAME
+ end
+end
diff --git a/db/migrate/20231023114551_add_fk_on_ml_candidates_to_ml_model_versions.rb b/db/migrate/20231023114551_add_fk_on_ml_candidates_to_ml_model_versions.rb
new file mode 100644
index 00000000000..0d625a54656
--- /dev/null
+++ b/db/migrate/20231023114551_add_fk_on_ml_candidates_to_ml_model_versions.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class AddFkOnMlCandidatesToMlModelVersions < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_foreign_key(:ml_candidates, :ml_model_versions, column: :model_version_id, on_delete: :cascade)
+ end
+
+ def down
+ with_lock_retries do
+ remove_foreign_key_if_exists(:ml_model_versions, column: :model_version_id, on_delete: :cascade)
+ end
+ end
+end
diff --git a/db/migrate/20231023121955_add_description_to_ml_model_versions.rb b/db/migrate/20231023121955_add_description_to_ml_model_versions.rb
new file mode 100644
index 00000000000..4361477160a
--- /dev/null
+++ b/db/migrate/20231023121955_add_description_to_ml_model_versions.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddDescriptionToMlModelVersions < Gitlab::Database::Migration[2.1]
+ def change
+ # rubocop:disable Migration/AddLimitToTextColumns -- limit being added on 20231023122508
+ add_column :ml_model_versions, :description, :text
+ # rubocop:enable Migration/AddLimitToTextColumns
+ end
+end
diff --git a/db/migrate/20231023122508_add_text_limit_to_descriptions_on_ml_model_versions.rb b/db/migrate/20231023122508_add_text_limit_to_descriptions_on_ml_model_versions.rb
new file mode 100644
index 00000000000..9df61e4c2ef
--- /dev/null
+++ b/db/migrate/20231023122508_add_text_limit_to_descriptions_on_ml_model_versions.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class AddTextLimitToDescriptionsOnMlModelVersions < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ def up
+ add_text_limit :ml_model_versions, :description, 500
+ end
+
+ def down
+ remove_text_limit :ml_model_versions, :description
+ end
+end
diff --git a/db/migrate/20231024123444_add_archive_project_to_member_roles.rb b/db/migrate/20231024123444_add_archive_project_to_member_roles.rb
new file mode 100644
index 00000000000..27ff86450e8
--- /dev/null
+++ b/db/migrate/20231024123444_add_archive_project_to_member_roles.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddArchiveProjectToMemberRoles < Gitlab::Database::Migration[2.1]
+ enable_lock_retries!
+
+ def change
+ add_column :member_roles, :archive_project, :boolean, default: false, null: false
+ end
+end
diff --git a/db/migrate/20231024133234_add_source_package_name_to_sbom_component.rb b/db/migrate/20231024133234_add_source_package_name_to_sbom_component.rb
new file mode 100644
index 00000000000..41970429ca9
--- /dev/null
+++ b/db/migrate/20231024133234_add_source_package_name_to_sbom_component.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+class AddSourcePackageNameToSbomComponent < Gitlab::Database::Migration[2.2]
+ milestone '16.6'
+ disable_ddl_transaction!
+
+ INDEX = 'index_source_package_names_on_component_and_purl'
+
+ def up
+ with_lock_retries do
+ add_column :sbom_components, :source_package_name, :text, if_not_exists: true
+ end
+
+ add_text_limit :sbom_components, :source_package_name, 255
+ add_concurrent_index :sbom_components,
+ [:component_type, :source_package_name, :purl_type],
+ name: INDEX,
+ unique: true
+ end
+
+ def down
+ with_lock_retries do
+ remove_column :sbom_components, :source_package_name, if_exists: true
+ end
+
+ remove_concurrent_index_by_name :sbom_components, name: INDEX
+ end
+end
diff --git a/db/migrate/20231024142236_add_fields_to_bulk_import_failures.rb b/db/migrate/20231024142236_add_fields_to_bulk_import_failures.rb
new file mode 100644
index 00000000000..670e42ba627
--- /dev/null
+++ b/db/migrate/20231024142236_add_fields_to_bulk_import_failures.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+class AddFieldsToBulkImportFailures < Gitlab::Database::Migration[2.2]
+ milestone '16.6'
+
+ # rubocop:disable Migration/AddLimitToTextColumns
+ def change
+ add_column :bulk_import_failures, :source_url, :text
+ add_column :bulk_import_failures, :source_title, :text
+ end
+ # rubocop:enable Migration/AddLimitToTextColumns
+end
diff --git a/db/migrate/20231024143457_add_text_limit_to_bulk_import_failures.rb b/db/migrate/20231024143457_add_text_limit_to_bulk_import_failures.rb
new file mode 100644
index 00000000000..eeca88f22c9
--- /dev/null
+++ b/db/migrate/20231024143457_add_text_limit_to_bulk_import_failures.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class AddTextLimitToBulkImportFailures < Gitlab::Database::Migration[2.2]
+ milestone '16.6'
+ disable_ddl_transaction!
+
+ def up
+ add_text_limit :bulk_import_failures, :source_url, 255
+ add_text_limit :bulk_import_failures, :source_title, 255
+ end
+
+ def down
+ remove_text_limit :bulk_import_failures, :source_url
+ remove_text_limit :bulk_import_failures, :source_title
+ end
+end
diff --git a/db/migrate/20231024151916_add_index_unique_setting_type_on_vs_code_settings.rb b/db/migrate/20231024151916_add_index_unique_setting_type_on_vs_code_settings.rb
new file mode 100644
index 00000000000..6eb34086299
--- /dev/null
+++ b/db/migrate/20231024151916_add_index_unique_setting_type_on_vs_code_settings.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class AddIndexUniqueSettingTypeOnVsCodeSettings < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ INDEX_NAME = 'unique_user_id_and_setting_type'
+ PREVIOUS_INDEX_NAME = 'index_vs_code_settings_on_user_id'
+
+ def up
+ remove_concurrent_index_by_name :vs_code_settings, name: PREVIOUS_INDEX_NAME
+ add_concurrent_index :vs_code_settings, [:user_id, :setting_type], name: INDEX_NAME, unique: true
+ end
+
+ def down
+ remove_concurrent_index_by_name :vs_code_settings, name: INDEX_NAME
+ add_concurrent_index :vs_code_settings, [:user_id], name: PREVIOUS_INDEX_NAME
+ end
+end
diff --git a/db/migrate/20231024173744_add_path_to_catalog_resource_components.rb b/db/migrate/20231024173744_add_path_to_catalog_resource_components.rb
new file mode 100644
index 00000000000..2473856faf1
--- /dev/null
+++ b/db/migrate/20231024173744_add_path_to_catalog_resource_components.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class AddPathToCatalogResourceComponents < Gitlab::Database::Migration[2.2]
+ milestone '16.6'
+ disable_ddl_transaction!
+
+ def up
+ with_lock_retries do
+ add_column :catalog_resource_components, :path, :text, if_not_exists: true
+ end
+
+ add_text_limit :catalog_resource_components, :path, 255
+ end
+
+ def down
+ with_lock_retries do
+ remove_column :catalog_resource_components, :path, :text, if_exists: true
+ end
+ end
+end
diff --git a/db/migrate/20231024212214_add_pipeline_cancel_role_restriction_enum.rb b/db/migrate/20231024212214_add_pipeline_cancel_role_restriction_enum.rb
new file mode 100644
index 00000000000..ab26a1d783b
--- /dev/null
+++ b/db/migrate/20231024212214_add_pipeline_cancel_role_restriction_enum.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+class AddPipelineCancelRoleRestrictionEnum < Gitlab::Database::Migration[2.1]
+ def up
+ add_column :project_ci_cd_settings, :restrict_pipeline_cancellation_role,
+ :integer, limit: 2, default: 0, null: false
+ end
+
+ def down
+ remove_column :project_ci_cd_settings, :restrict_pipeline_cancellation_role
+ end
+end
diff --git a/db/migrate/20231025123238_create_compliance_framework_security_policies.rb b/db/migrate/20231025123238_create_compliance_framework_security_policies.rb
new file mode 100644
index 00000000000..1cf970e0d6c
--- /dev/null
+++ b/db/migrate/20231025123238_create_compliance_framework_security_policies.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class CreateComplianceFrameworkSecurityPolicies < Gitlab::Database::Migration[2.2]
+ UNIQUE_INDEX_NAME = 'unique_compliance_framework_security_policies_framework_id'
+ POLICY_CONFIGURATION_INDEX_NAME = 'idx_compliance_security_policies_on_policy_configuration_id'
+
+ milestone '16.6'
+ enable_lock_retries!
+
+ def change
+ create_table :compliance_framework_security_policies do |t|
+ t.bigint :framework_id, null: false
+ t.bigint :policy_configuration_id, null: false
+ t.timestamps_with_timezone null: false
+ t.integer :policy_index, limit: 2, null: false
+
+ t.index :policy_configuration_id, name: POLICY_CONFIGURATION_INDEX_NAME
+ t.index [:framework_id, :policy_configuration_id, :policy_index], unique: true, name: UNIQUE_INDEX_NAME
+ end
+ end
+end
diff --git a/db/migrate/20231026050554_add_functions_for_primary_key_lookup.rb b/db/migrate/20231026050554_add_functions_for_primary_key_lookup.rb
new file mode 100644
index 00000000000..ecf32f74e4b
--- /dev/null
+++ b/db/migrate/20231026050554_add_functions_for_primary_key_lookup.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+class AddFunctionsForPrimaryKeyLookup < Gitlab::Database::Migration[2.2]
+ milestone '16.6'
+
+ TABLES = %i[users namespaces projects].freeze
+
+ def up
+ TABLES.each do |table|
+ execute <<~SQL
+ CREATE OR REPLACE FUNCTION find_#{table}_by_id(#{table}_id bigint)
+ RETURNS #{table} AS $$
+ BEGIN
+ return (SELECT #{table} FROM #{table} WHERE id = #{table}_id LIMIT 1);
+ END;
+ $$ LANGUAGE plpgsql STABLE PARALLEL SAFE COST 1;
+ SQL
+ end
+ end
+
+ def down
+ TABLES.each do |table|
+ execute "DROP FUNCTION IF EXISTS find_#{table}_by_id"
+ end
+ end
+end
diff --git a/db/migrate/20231027052949_initialize_conversion_of_system_note_metadata_to_bigint.rb b/db/migrate/20231027052949_initialize_conversion_of_system_note_metadata_to_bigint.rb
new file mode 100644
index 00000000000..6dc840e8790
--- /dev/null
+++ b/db/migrate/20231027052949_initialize_conversion_of_system_note_metadata_to_bigint.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class InitializeConversionOfSystemNoteMetadataToBigint < Gitlab::Database::Migration[2.2]
+ disable_ddl_transaction!
+
+ TABLE = :system_note_metadata
+ COLUMNS = %i[id]
+
+ milestone '16.6'
+
+ def up
+ initialize_conversion_of_integer_to_bigint(TABLE, COLUMNS)
+ end
+
+ def down
+ revert_initialize_conversion_of_integer_to_bigint(TABLE, COLUMNS)
+ end
+end
diff --git a/db/migrate/20231027064352_add_service_access_tokens_expiration_application_setting.rb b/db/migrate/20231027064352_add_service_access_tokens_expiration_application_setting.rb
new file mode 100644
index 00000000000..6ef85a353fb
--- /dev/null
+++ b/db/migrate/20231027064352_add_service_access_tokens_expiration_application_setting.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class AddServiceAccessTokensExpirationApplicationSetting < Gitlab::Database::Migration[2.2]
+ milestone '16.6'
+
+ enable_lock_retries!
+
+ def change
+ add_column :application_settings, :service_access_tokens_expiration_enforced, :boolean, default: true, null: false
+ end
+end
diff --git a/db/migrate/20231027065205_add_service_access_tokens_expiration_namespace_setting.rb b/db/migrate/20231027065205_add_service_access_tokens_expiration_namespace_setting.rb
new file mode 100644
index 00000000000..86df338c416
--- /dev/null
+++ b/db/migrate/20231027065205_add_service_access_tokens_expiration_namespace_setting.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class AddServiceAccessTokensExpirationNamespaceSetting < Gitlab::Database::Migration[2.2]
+ milestone '16.6'
+
+ enable_lock_retries!
+
+ def change
+ add_column :namespace_settings, :service_access_tokens_expiration_enforced, :boolean, default: true, null: false
+ end
+end
diff --git a/db/migrate/20231027084327_change_personal_access_tokens_remove_not_null_expires_at.rb b/db/migrate/20231027084327_change_personal_access_tokens_remove_not_null_expires_at.rb
new file mode 100644
index 00000000000..0f7e3d53707
--- /dev/null
+++ b/db/migrate/20231027084327_change_personal_access_tokens_remove_not_null_expires_at.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class ChangePersonalAccessTokensRemoveNotNullExpiresAt < Gitlab::Database::Migration[2.2]
+ milestone '16.6'
+
+ disable_ddl_transaction!
+
+ CONSTRAINT_NAME = 'check_b8d60815eb'
+
+ def up
+ remove_not_null_constraint :personal_access_tokens, :expires_at
+ end
+
+ def down
+ add_not_null_constraint :personal_access_tokens, :expires_at, validate: false, constraint_name: CONSTRAINT_NAME
+ end
+end
diff --git a/db/migrate/20231030051837_add_project_id_to_packages_tags.rb b/db/migrate/20231030051837_add_project_id_to_packages_tags.rb
new file mode 100644
index 00000000000..b27e15cb648
--- /dev/null
+++ b/db/migrate/20231030051837_add_project_id_to_packages_tags.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+class AddProjectIdToPackagesTags < Gitlab::Database::Migration[2.2]
+ milestone '16.6'
+ enable_lock_retries!
+
+ def change
+ add_column :packages_tags, :project_id, :bigint
+ end
+end
diff --git a/db/migrate/20231030051838_add_index_to_packages_tags_project_id.rb b/db/migrate/20231030051838_add_index_to_packages_tags_project_id.rb
new file mode 100644
index 00000000000..17512137fff
--- /dev/null
+++ b/db/migrate/20231030051838_add_index_to_packages_tags_project_id.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class AddIndexToPackagesTagsProjectId < Gitlab::Database::Migration[2.2]
+ milestone '16.6'
+ disable_ddl_transaction!
+ INDEX_NAME = :index_packages_tags_on_project_id
+
+ def up
+ add_concurrent_index :packages_tags, :project_id, name: INDEX_NAME
+ end
+
+ def down
+ remove_concurrent_index_by_name(:packages_tags, INDEX_NAME)
+ end
+end
diff --git a/db/migrate/20231030051839_add_foreign_key_to_packages_tags_project_id.rb b/db/migrate/20231030051839_add_foreign_key_to_packages_tags_project_id.rb
new file mode 100644
index 00000000000..6e3d6161582
--- /dev/null
+++ b/db/migrate/20231030051839_add_foreign_key_to_packages_tags_project_id.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class AddForeignKeyToPackagesTagsProjectId < Gitlab::Database::Migration[2.2]
+ milestone '16.6'
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_foreign_key :packages_tags, :projects, column: :project_id, on_delete: :cascade
+ end
+
+ def down
+ with_lock_retries do
+ remove_foreign_key :packages_tags, column: :project_id
+ end
+ end
+end
diff --git a/db/migrate/20231030205639_update_default_package_metadata_purl_types.rb b/db/migrate/20231030205639_update_default_package_metadata_purl_types.rb
new file mode 100644
index 00000000000..1e2f1ccb578
--- /dev/null
+++ b/db/migrate/20231030205639_update_default_package_metadata_purl_types.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class UpdateDefaultPackageMetadataPurlTypes < Gitlab::Database::Migration[2.2]
+ milestone '16.6'
+
+ disable_ddl_transaction!
+
+ PARTIALLY_ENABLED_SYNC = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].freeze
+ FULLY_ENABLED_SYNC = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13].freeze
+
+ def change
+ change_column_default :application_settings, :package_metadata_purl_types,
+ from: PARTIALLY_ENABLED_SYNC, to: FULLY_ENABLED_SYNC
+ end
+end
diff --git a/db/migrate/20231030205756_index_user_details_on_enterprise_group_id_and_user_id.rb b/db/migrate/20231030205756_index_user_details_on_enterprise_group_id_and_user_id.rb
new file mode 100644
index 00000000000..a993944e152
--- /dev/null
+++ b/db/migrate/20231030205756_index_user_details_on_enterprise_group_id_and_user_id.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class IndexUserDetailsOnEnterpriseGroupIdAndUserId < Gitlab::Database::Migration[2.2]
+ milestone '16.6'
+
+ disable_ddl_transaction!
+
+ INDEX_NAME = 'index_user_details_on_enterprise_group_id_and_user_id'
+ INDEX_NAME_TO_REMOVE = 'index_user_details_on_enterprise_group_id'
+
+ def up
+ add_concurrent_index(:user_details, [:enterprise_group_id, :user_id], name: INDEX_NAME)
+
+ remove_concurrent_index_by_name :user_details, INDEX_NAME_TO_REMOVE
+ end
+
+ def down
+ remove_concurrent_index_by_name :user_details, INDEX_NAME
+
+ add_concurrent_index :user_details, :enterprise_group_id, name: INDEX_NAME_TO_REMOVE
+ end
+end
diff --git a/db/migrate/20231031141439_add_smtp_authentication_to_service_desk_custom_email_credentials.rb b/db/migrate/20231031141439_add_smtp_authentication_to_service_desk_custom_email_credentials.rb
new file mode 100644
index 00000000000..e15e500af90
--- /dev/null
+++ b/db/migrate/20231031141439_add_smtp_authentication_to_service_desk_custom_email_credentials.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+class AddSmtpAuthenticationToServiceDeskCustomEmailCredentials < Gitlab::Database::Migration[2.2]
+ milestone '16.6'
+
+ def change
+ add_column :service_desk_custom_email_credentials, :smtp_authentication, :integer,
+ limit: 2, null: true, default: nil
+ end
+end
diff --git a/db/migrate/20231031200433_add_framework_fk_to_compliance_framework_security_policies.rb b/db/migrate/20231031200433_add_framework_fk_to_compliance_framework_security_policies.rb
new file mode 100644
index 00000000000..bb7fa924d15
--- /dev/null
+++ b/db/migrate/20231031200433_add_framework_fk_to_compliance_framework_security_policies.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class AddFrameworkFkToComplianceFrameworkSecurityPolicies < Gitlab::Database::Migration[2.2]
+ milestone '16.6'
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_foreign_key :compliance_framework_security_policies,
+ :compliance_management_frameworks,
+ column: :framework_id,
+ on_delete: :cascade
+ end
+
+ def down
+ with_lock_retries do
+ remove_foreign_key :compliance_framework_security_policies, column: :framework_id
+ end
+ end
+end
diff --git a/db/migrate/20231031200645_add_policy_configuration_fk_to_compliance_framework_security_policies.rb b/db/migrate/20231031200645_add_policy_configuration_fk_to_compliance_framework_security_policies.rb
new file mode 100644
index 00000000000..cf6419c5128
--- /dev/null
+++ b/db/migrate/20231031200645_add_policy_configuration_fk_to_compliance_framework_security_policies.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class AddPolicyConfigurationFkToComplianceFrameworkSecurityPolicies < Gitlab::Database::Migration[2.2]
+ milestone '16.6'
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_foreign_key :compliance_framework_security_policies,
+ :security_orchestration_policy_configurations,
+ column: :policy_configuration_id,
+ on_delete: :cascade
+ end
+
+ def down
+ with_lock_retries do
+ remove_foreign_key :compliance_framework_security_policies, column: :policy_configuration_id
+ end
+ end
+end
diff --git a/db/migrate/20231102142553_add_zoekt_nodes.rb b/db/migrate/20231102142553_add_zoekt_nodes.rb
new file mode 100644
index 00000000000..69a937ea4b0
--- /dev/null
+++ b/db/migrate/20231102142553_add_zoekt_nodes.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class AddZoektNodes < Gitlab::Database::Migration[2.2]
+ milestone '16.6'
+ enable_lock_retries!
+
+ def change
+ create_table :zoekt_nodes do |t|
+ t.uuid :uuid, index: { unique: true }, null: false
+ t.bigint :used_bytes, null: false, default: 0
+ t.bigint :total_bytes, null: false, default: 0
+ t.datetime_with_timezone :last_seen_at, index: true, null: false, default: '1970-01-01'
+ t.timestamps_with_timezone
+ t.text :index_base_url, limit: 1024, index: { unique: true }, null: false
+ t.text :search_base_url, limit: 1024, index: { unique: true }, null: false
+ t.jsonb :metadata, default: {}, null: false
+ end
+ end
+end
diff --git a/db/migrate/20231102142554_migrate_zoekt_shards_to_zoekt_nodes.rb b/db/migrate/20231102142554_migrate_zoekt_shards_to_zoekt_nodes.rb
new file mode 100644
index 00000000000..23ae1231ae0
--- /dev/null
+++ b/db/migrate/20231102142554_migrate_zoekt_shards_to_zoekt_nodes.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+class MigrateZoektShardsToZoektNodes < Gitlab::Database::Migration[2.2]
+ milestone '16.6'
+ disable_ddl_transaction!
+
+ restrict_gitlab_migration gitlab_schema: :gitlab_main
+
+ SELECTED_COLUMNS = %w[
+ index_base_url
+ search_base_url
+ uuid
+ used_bytes
+ total_bytes
+ metadata
+ last_seen_at
+ created_at
+ updated_at
+ ].join(',')
+
+ def up
+ connection.execute(<<~SQL)
+ INSERT INTO zoekt_nodes (#{SELECTED_COLUMNS})
+ SELECT #{SELECTED_COLUMNS}
+ FROM zoekt_shards
+ SQL
+ end
+
+ def down
+ connection.execute(<<~SQL)
+ DELETE FROM zoekt_nodes
+ SQL
+ end
+end
diff --git a/db/migrate/20231102142555_add_zoekt_node_id_to_indexed_namespaces.rb b/db/migrate/20231102142555_add_zoekt_node_id_to_indexed_namespaces.rb
new file mode 100644
index 00000000000..0b706cb0051
--- /dev/null
+++ b/db/migrate/20231102142555_add_zoekt_node_id_to_indexed_namespaces.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class AddZoektNodeIdToIndexedNamespaces < Gitlab::Database::Migration[2.2]
+ milestone '16.6'
+
+ def up
+ add_column :zoekt_indexed_namespaces, :zoekt_node_id, :bigint
+ end
+
+ def down
+ remove_column :zoekt_indexed_namespaces, :zoekt_node_id
+ end
+end
diff --git a/db/migrate/20231102142565_add_zoekt_node_foreign_key_to_indexed_namespaces.rb b/db/migrate/20231102142565_add_zoekt_node_foreign_key_to_indexed_namespaces.rb
new file mode 100644
index 00000000000..957a2e751fa
--- /dev/null
+++ b/db/migrate/20231102142565_add_zoekt_node_foreign_key_to_indexed_namespaces.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddZoektNodeForeignKeyToIndexedNamespaces < Gitlab::Database::Migration[2.2]
+ milestone '16.6'
+ disable_ddl_transaction!
+
+ INDEX_NAME = 'index_zoekt_node_and_namespace'
+
+ def up
+ add_concurrent_foreign_key :zoekt_indexed_namespaces, :zoekt_nodes, column: :zoekt_node_id, on_delete: :cascade
+ add_concurrent_index :zoekt_indexed_namespaces, [:zoekt_node_id, :namespace_id], unique: true, name: INDEX_NAME
+ end
+
+ def down
+ remove_concurrent_index :zoekt_indexed_namespaces, [:zoekt_node_id, :namespace_id], name: INDEX_NAME
+ end
+end
diff --git a/db/migrate/20231103162825_add_wolfi_purl_type_to_package_metadata_purl_types.rb b/db/migrate/20231103162825_add_wolfi_purl_type_to_package_metadata_purl_types.rb
new file mode 100644
index 00000000000..bdbe8aa3a63
--- /dev/null
+++ b/db/migrate/20231103162825_add_wolfi_purl_type_to_package_metadata_purl_types.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+# See https://docs.gitlab.com/ee/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddWolfiPurlTypeToPackageMetadataPurlTypes < Gitlab::Database::Migration[2.2]
+ milestone '16.6'
+
+ restrict_gitlab_migration gitlab_schema: :gitlab_main
+
+ class ApplicationSetting < MigrationRecord
+ end
+
+ WOLFI_PURL_TYPE = 13
+
+ def up
+ application_setting = ApplicationSetting.last
+ return unless application_setting
+
+ application_setting.package_metadata_purl_types |= [WOLFI_PURL_TYPE]
+ application_setting.save
+ end
+
+ def down
+ application_setting = ApplicationSetting.last
+ return unless application_setting
+
+ application_setting.package_metadata_purl_types.delete(WOLFI_PURL_TYPE)
+ application_setting.save
+ end
+end
diff --git a/db/migrate/20231103195309_remove_deprecated_package_metadata_sync_worker.rb b/db/migrate/20231103195309_remove_deprecated_package_metadata_sync_worker.rb
new file mode 100644
index 00000000000..ae461d21799
--- /dev/null
+++ b/db/migrate/20231103195309_remove_deprecated_package_metadata_sync_worker.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class RemoveDeprecatedPackageMetadataSyncWorker < Gitlab::Database::Migration[2.2]
+ milestone '16.6'
+ disable_ddl_transaction!
+
+ DEPRECATED_JOB_CLASSES = %w[PackageMetadata::SyncWorker]
+
+ def up
+ sidekiq_remove_jobs(job_klasses: DEPRECATED_JOB_CLASSES)
+ end
+
+ def down
+ # This migration removes any instances of deprecated workers and cannot be undone.
+ end
+end
diff --git a/db/migrate/20231103223224_backfill_zoekt_node_id_on_indexed_namespaces.rb b/db/migrate/20231103223224_backfill_zoekt_node_id_on_indexed_namespaces.rb
new file mode 100644
index 00000000000..7d6bb25f832
--- /dev/null
+++ b/db/migrate/20231103223224_backfill_zoekt_node_id_on_indexed_namespaces.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class BackfillZoektNodeIdOnIndexedNamespaces < Gitlab::Database::Migration[2.2]
+ milestone '16.6'
+ restrict_gitlab_migration gitlab_schema: :gitlab_main
+
+ def up
+ sql = <<-SQL
+ UPDATE zoekt_indexed_namespaces
+ SET zoekt_node_id = (SELECT id FROM zoekt_nodes ORDER BY created_at DESC LIMIT 1)
+ SQL
+
+ execute(sql)
+ end
+
+ def down
+ # no-op
+ end
+end
diff --git a/db/migrate/20231106145853_add_product_analytics_enabled_to_namespace_settings.rb b/db/migrate/20231106145853_add_product_analytics_enabled_to_namespace_settings.rb
new file mode 100644
index 00000000000..45b617be6ca
--- /dev/null
+++ b/db/migrate/20231106145853_add_product_analytics_enabled_to_namespace_settings.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddProductAnalyticsEnabledToNamespaceSettings < Gitlab::Database::Migration[2.2]
+ milestone '16.6'
+
+ def change
+ add_column :namespace_settings, :product_analytics_enabled, :boolean, default: false, null: false
+ end
+end
diff --git a/db/migrate/20231106212340_add_visibility_level_to_catalog_resources.rb b/db/migrate/20231106212340_add_visibility_level_to_catalog_resources.rb
new file mode 100644
index 00000000000..46150396c1e
--- /dev/null
+++ b/db/migrate/20231106212340_add_visibility_level_to_catalog_resources.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class AddVisibilityLevelToCatalogResources < Gitlab::Database::Migration[2.2]
+ milestone '16.6'
+
+ enable_lock_retries!
+
+ def change
+ # This column must match the settings of `visibility_level` in the `projects` table.
+ # Backfill will be done as part of https://gitlab.com/gitlab-org/gitlab/-/issues/429056.
+ add_column :catalog_resources, :visibility_level, :integer, default: 0, null: false
+ end
+end
diff --git a/db/migrate/20231107062104_add_network_policy_egress_to_agent.rb b/db/migrate/20231107062104_add_network_policy_egress_to_agent.rb
new file mode 100644
index 00000000000..c7f9da1831c
--- /dev/null
+++ b/db/migrate/20231107062104_add_network_policy_egress_to_agent.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class AddNetworkPolicyEgressToAgent < Gitlab::Database::Migration[2.2]
+ milestone '16.6'
+
+ NETWORK_POLICY_EGRESS_DEFAULT = [{
+ allow: "0.0.0.0/0",
+ except: [
+ - "10.0.0.0/8",
+ - "172.16.0.0/12",
+ - "192.168.0.0/16"
+ ]
+ }]
+
+ def change
+ add_column :remote_development_agent_configs,
+ :network_policy_egress,
+ :jsonb,
+ null: false,
+ default: NETWORK_POLICY_EGRESS_DEFAULT
+ end
+end
diff --git a/db/migrate/20231107071201_add_project_authorizations_recalculated_at_to_user_details.rb b/db/migrate/20231107071201_add_project_authorizations_recalculated_at_to_user_details.rb
new file mode 100644
index 00000000000..c7f0ca83695
--- /dev/null
+++ b/db/migrate/20231107071201_add_project_authorizations_recalculated_at_to_user_details.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class AddProjectAuthorizationsRecalculatedAtToUserDetails < Gitlab::Database::Migration[2.2]
+ milestone '16.6'
+ enable_lock_retries!
+
+ def change
+ add_column :user_details, :project_authorizations_recalculated_at, :datetime_with_timezone,
+ default: '2010-01-01', null: false
+ end
+end
diff --git a/db/migrate/20231107205734_add_update_namespace_name_to_application_settings.rb b/db/migrate/20231107205734_add_update_namespace_name_to_application_settings.rb
new file mode 100644
index 00000000000..a812166ed9d
--- /dev/null
+++ b/db/migrate/20231107205734_add_update_namespace_name_to_application_settings.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddUpdateNamespaceNameToApplicationSettings < Gitlab::Database::Migration[2.2]
+ milestone '16.6'
+
+ def change
+ add_column :application_settings, :update_namespace_name_rate_limit, :smallint, default: 120, null: false
+ end
+end
diff --git a/db/migrate/20231108072342_add_display_time_format_preference.rb b/db/migrate/20231108072342_add_display_time_format_preference.rb
new file mode 100644
index 00000000000..2f3773f73ef
--- /dev/null
+++ b/db/migrate/20231108072342_add_display_time_format_preference.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+class AddDisplayTimeFormatPreference < Gitlab::Database::Migration[2.2]
+ milestone '16.6'
+ enable_lock_retries!
+
+ def change
+ add_column :user_preferences, :time_display_format, :integer, limit: 2, default: 0, null: false
+ end
+end
diff --git a/db/migrate/20231108093031_add_allow_project_creation_for_guest_and_below_to_application_settings.rb b/db/migrate/20231108093031_add_allow_project_creation_for_guest_and_below_to_application_settings.rb
new file mode 100644
index 00000000000..06e0a7fc000
--- /dev/null
+++ b/db/migrate/20231108093031_add_allow_project_creation_for_guest_and_below_to_application_settings.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddAllowProjectCreationForGuestAndBelowToApplicationSettings < Gitlab::Database::Migration[2.2]
+ milestone '16.6'
+
+ def change
+ add_column(:application_settings, :allow_project_creation_for_guest_and_below, :boolean, default: true, null: false)
+ end
+end
diff --git a/db/migrate/20231109133153_drop_idx_namespaces_on_ldap_sync_last_successful_update_at_for_gitlab.rb b/db/migrate/20231109133153_drop_idx_namespaces_on_ldap_sync_last_successful_update_at_for_gitlab.rb
new file mode 100644
index 00000000000..1d171e3285c
--- /dev/null
+++ b/db/migrate/20231109133153_drop_idx_namespaces_on_ldap_sync_last_successful_update_at_for_gitlab.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+class DropIdxNamespacesOnLdapSyncLastSuccessfulUpdateAtForGitlab < Gitlab::Database::Migration[2.2]
+ milestone '16.6'
+
+ disable_ddl_transaction!
+
+ TABLE_NAME = :namespaces
+ INDEX_NAME = :index_namespaces_on_ldap_sync_last_successful_update_at
+
+ def up
+ return unless should_run?
+
+ remove_concurrent_index_by_name TABLE_NAME, INDEX_NAME
+ end
+
+ def down
+ return unless should_run?
+
+ add_concurrent_index TABLE_NAME, :ldap_sync_last_successful_update_at, name: INDEX_NAME
+ end
+
+ private
+
+ def should_run?
+ Gitlab.com_except_jh?
+ end
+end
diff --git a/db/post_migrate/20220531233600_remove_sse_usage_data_from_redis.rb b/db/post_migrate/20220531233600_remove_sse_usage_data_from_redis.rb
index b7b02e483df..26ae9aed5cc 100644
--- a/db/post_migrate/20220531233600_remove_sse_usage_data_from_redis.rb
+++ b/db/post_migrate/20220531233600_remove_sse_usage_data_from_redis.rb
@@ -3,6 +3,8 @@
class RemoveSseUsageDataFromRedis < Gitlab::Database::Migration[2.0]
disable_ddl_transaction!
+ restrict_gitlab_migration gitlab_schema: :gitlab_main
+
def up
Gitlab::Redis::SharedState.with { |r| r.del("USAGE_STATIC_SITE_EDITOR_VIEWS") }
Gitlab::Redis::SharedState.with { |r| r.del("USAGE_STATIC_SITE_EDITOR_COMMITS") }
diff --git a/db/post_migrate/20220617123022_add_unique_index_on_projects_on_runners_token.rb b/db/post_migrate/20220617123022_add_unique_index_on_projects_on_runners_token.rb
index 1e0409b16ea..abd730685d7 100644
--- a/db/post_migrate/20220617123022_add_unique_index_on_projects_on_runners_token.rb
+++ b/db/post_migrate/20220617123022_add_unique_index_on_projects_on_runners_token.rb
@@ -6,10 +6,12 @@ class AddUniqueIndexOnProjectsOnRunnersToken < Gitlab::Database::Migration[2.0]
INDEX_NAME = 'index_uniq_projects_on_runners_token'
def up
+ # rubocop:disable Migration/PreventIndexCreation
add_concurrent_index :projects,
:runners_token,
name: INDEX_NAME,
unique: true
+ # rubocop:enable Migration/PreventIndexCreation
end
def down
diff --git a/db/post_migrate/20220617123034_add_unique_index_on_projects_on_runners_token_encrypted.rb b/db/post_migrate/20220617123034_add_unique_index_on_projects_on_runners_token_encrypted.rb
index b9ba570606e..51b630397dc 100644
--- a/db/post_migrate/20220617123034_add_unique_index_on_projects_on_runners_token_encrypted.rb
+++ b/db/post_migrate/20220617123034_add_unique_index_on_projects_on_runners_token_encrypted.rb
@@ -6,10 +6,12 @@ class AddUniqueIndexOnProjectsOnRunnersTokenEncrypted < Gitlab::Database::Migrat
INDEX_NAME = 'index_uniq_projects_on_runners_token_encrypted'
def up
+ # rubocop:disable Migration/PreventIndexCreation
add_concurrent_index :projects,
:runners_token_encrypted,
name: INDEX_NAME,
unique: true
+ # rubocop:enable Migration/PreventIndexCreation
end
def down
diff --git a/db/post_migrate/20220902204048_move_security_findings_table_to_gitlab_partitions_dynamic_schema.rb b/db/post_migrate/20220902204048_move_security_findings_table_to_gitlab_partitions_dynamic_schema.rb
index 7b80b6a15bd..62511e0e616 100644
--- a/db/post_migrate/20220902204048_move_security_findings_table_to_gitlab_partitions_dynamic_schema.rb
+++ b/db/post_migrate/20220902204048_move_security_findings_table_to_gitlab_partitions_dynamic_schema.rb
@@ -147,7 +147,6 @@ class MoveSecurityFindingsTableToGitlabPartitionsDynamicSchema < Gitlab::Databas
latest_partition.match(/security_findings_(\d+)/).captures.first
end
- # rubocop:disable Migration/DropTable (These methods are called from the `down` method)
def create_non_partitioned_security_findings_with_data
with_lock_retries do
lock_tables
@@ -227,6 +226,5 @@ class MoveSecurityFindingsTableToGitlabPartitionsDynamicSchema < Gitlab::Databas
SQL
end
end
- # rubocop:enable Migration/DropTable
end
# rubocop:enable Migration/WithLockRetriesDisallowedMethod
diff --git a/db/post_migrate/20220920135356_tiebreak_user_type_index.rb b/db/post_migrate/20220920135356_tiebreak_user_type_index.rb
index 778a957086f..489196c8eab 100644
--- a/db/post_migrate/20220920135356_tiebreak_user_type_index.rb
+++ b/db/post_migrate/20220920135356_tiebreak_user_type_index.rb
@@ -7,7 +7,9 @@ class TiebreakUserTypeIndex < Gitlab::Database::Migration[2.0]
OLD_INDEX_NAME = 'index_users_on_user_type'
def up
+ # rubocop:disable Migration/PreventIndexCreation
add_concurrent_index :users, [:user_type, :id], name: NEW_INDEX_NAME
+ # rubocop:enable Migration/PreventIndexCreation
remove_concurrent_index_by_name :users, OLD_INDEX_NAME
end
diff --git a/db/post_migrate/20221018232820_add_temp_index_for_user_details_fields.rb b/db/post_migrate/20221018232820_add_temp_index_for_user_details_fields.rb
index b46b316981d..1cb93886ca3 100644
--- a/db/post_migrate/20221018232820_add_temp_index_for_user_details_fields.rb
+++ b/db/post_migrate/20221018232820_add_temp_index_for_user_details_fields.rb
@@ -6,6 +6,7 @@ class AddTempIndexForUserDetailsFields < Gitlab::Database::Migration[2.0]
disable_ddl_transaction!
def up
+ # rubocop:disable Migration/PreventIndexCreation
add_concurrent_index :users, :id, name: INDEX_NAME, where: <<~QUERY
(COALESCE(linkedin, '') IS DISTINCT FROM '')
OR (COALESCE(twitter, '') IS DISTINCT FROM '')
@@ -14,6 +15,7 @@ class AddTempIndexForUserDetailsFields < Gitlab::Database::Migration[2.0]
OR (COALESCE(location, '') IS DISTINCT FROM '')
OR (COALESCE(organization, '') IS DISTINCT FROM '')
QUERY
+ # rubocop:enable Migration/PreventIndexCreation
end
def down
diff --git a/db/post_migrate/20221221150123_update_billable_users_index.rb b/db/post_migrate/20221221150123_update_billable_users_index.rb
index d2f55e06b0b..d77669f6a69 100644
--- a/db/post_migrate/20221221150123_update_billable_users_index.rb
+++ b/db/post_migrate/20221221150123_update_billable_users_index.rb
@@ -16,7 +16,9 @@ class UpdateBillableUsersIndex < Gitlab::Database::Migration[2.1]
QUERY
def up
+ # rubocop:disable Migration/PreventIndexCreation
add_concurrent_index(:users, :id, where: NEW_INDEX_CONDITION, name: NEW_INDEX)
+ # rubocop:enable Migration/PreventIndexCreation
remove_concurrent_index_by_name(:users, OLD_INDEX)
end
diff --git a/db/post_migrate/20230131184319_update_billable_users_index_for_service_accounts.rb b/db/post_migrate/20230131184319_update_billable_users_index_for_service_accounts.rb
index e86a2476156..842c7295fcb 100644
--- a/db/post_migrate/20230131184319_update_billable_users_index_for_service_accounts.rb
+++ b/db/post_migrate/20230131184319_update_billable_users_index_for_service_accounts.rb
@@ -16,7 +16,9 @@ class UpdateBillableUsersIndexForServiceAccounts < Gitlab::Database::Migration[2
QUERY
def up
+ # rubocop:disable Migration/PreventIndexCreation
add_concurrent_index(:users, :id, where: NEW_INDEX_CONDITION, name: NEW_INDEX)
+ # rubocop:enable Migration/PreventIndexCreation
remove_concurrent_index_by_name(:users, OLD_INDEX)
end
diff --git a/db/post_migrate/20230303154314_add_user_type_migration_indexes.rb b/db/post_migrate/20230303154314_add_user_type_migration_indexes.rb
index 8f9e193f0eb..d4f48c1c977 100644
--- a/db/post_migrate/20230303154314_add_user_type_migration_indexes.rb
+++ b/db/post_migrate/20230303154314_add_user_type_migration_indexes.rb
@@ -6,6 +6,7 @@ class AddUserTypeMigrationIndexes < Gitlab::Database::Migration[2.1]
BILLABLE_INDEX = 'index_users_for_active_billable_users_migration'
LAST_ACTIVITY_INDEX = 'i_users_on_last_activity_for_active_human_service_migration'
+ # rubocop:disable Migration/PreventIndexCreation
def up
# Temporary indexes to migrate human user_type. See https://gitlab.com/gitlab-org/gitlab/-/issues/386474
add_concurrent_index :users, :id, name: BILLABLE_INDEX,
@@ -14,6 +15,7 @@ class AddUserTypeMigrationIndexes < Gitlab::Database::Migration[2.1]
add_concurrent_index :users, [:id, :last_activity_on], name: LAST_ACTIVITY_INDEX,
where: "((state)::text = 'active'::text) AND ((user_type IS NULL OR user_type = 0) OR (user_type = 4))"
end
+ # rubocop:enable Migration/PreventIndexCreation
def down
remove_concurrent_index_by_name :users, BILLABLE_INDEX
diff --git a/db/post_migrate/20230310111859_recreate_user_type_migration_indexes.rb b/db/post_migrate/20230310111859_recreate_user_type_migration_indexes.rb
index 539ce99a319..147409bf5f0 100644
--- a/db/post_migrate/20230310111859_recreate_user_type_migration_indexes.rb
+++ b/db/post_migrate/20230310111859_recreate_user_type_migration_indexes.rb
@@ -8,9 +8,11 @@ class RecreateUserTypeMigrationIndexes < Gitlab::Database::Migration[2.1]
def up
# Temporary index to migrate human user_type. See https://gitlab.com/gitlab-org/gitlab/-/issues/386474
+ # rubocop:disable Migration/PreventIndexCreation
add_concurrent_index :users, :id, name: BILLABLE_INDEX,
where: "state = 'active' AND ((user_type IS NULL OR user_type = 0) OR (user_type = ANY (ARRAY[0, 6, 4, 13]))) " \
"AND ((user_type IS NULL OR user_type = 0) OR (user_type = ANY (ARRAY[0, 4, 5])))"
+ # rubocop:enable Migration/PreventIndexCreation
remove_concurrent_index_by_name :users, INCORRECT_BILLABLE_INDEX
end
diff --git a/db/post_migrate/20230313150531_reschedule_migration_for_remediation.rb b/db/post_migrate/20230313150531_reschedule_migration_for_remediation.rb
index ebb6e53341f..01d5c3a79b0 100644
--- a/db/post_migrate/20230313150531_reschedule_migration_for_remediation.rb
+++ b/db/post_migrate/20230313150531_reschedule_migration_for_remediation.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-# rubocop: disable BackgroundMigration/MissingDictionaryFile
+# rubocop: disable BackgroundMigration/DictionaryFile
class RescheduleMigrationForRemediation < Gitlab::Database::Migration[2.1]
MIGRATION = 'MigrateRemediationsForVulnerabilityFindings'
@@ -29,4 +29,4 @@ class RescheduleMigrationForRemediation < Gitlab::Database::Migration[2.1]
delete_batched_background_migration(MIGRATION, :vulnerability_occurrences, :id, [])
end
end
-# rubocop: enable BackgroundMigration/MissingDictionaryFile
+# rubocop: enable BackgroundMigration/DictionaryFile
diff --git a/db/post_migrate/20230317004428_migrate_daily_redis_hll_events_to_weekly_aggregation.rb b/db/post_migrate/20230317004428_migrate_daily_redis_hll_events_to_weekly_aggregation.rb
index 59bff26f964..22ef3381c17 100644
--- a/db/post_migrate/20230317004428_migrate_daily_redis_hll_events_to_weekly_aggregation.rb
+++ b/db/post_migrate/20230317004428_migrate_daily_redis_hll_events_to_weekly_aggregation.rb
@@ -3,6 +3,8 @@
class MigrateDailyRedisHllEventsToWeeklyAggregation < Gitlab::Database::Migration[2.1]
disable_ddl_transaction!
+ restrict_gitlab_migration gitlab_schema: :gitlab_main
+
DAILY_EVENTS =
%w[g_edit_by_web_ide
g_edit_by_sfe
diff --git a/db/post_migrate/20230328111013_re_migrate_redis_slot_keys.rb b/db/post_migrate/20230328111013_re_migrate_redis_slot_keys.rb
index 17776d8e42e..a4061c3c7c6 100644
--- a/db/post_migrate/20230328111013_re_migrate_redis_slot_keys.rb
+++ b/db/post_migrate/20230328111013_re_migrate_redis_slot_keys.rb
@@ -3,6 +3,8 @@
class ReMigrateRedisSlotKeys < Gitlab::Database::Migration[2.1]
disable_ddl_transaction!
+ restrict_gitlab_migration gitlab_schema: :gitlab_main
+
KEY_EXPIRY_LENGTH = 6.weeks
DAILY_EVENTS =
diff --git a/db/post_migrate/20230405200858_requeue_backfill_project_wiki_repositories.rb b/db/post_migrate/20230405200858_requeue_backfill_project_wiki_repositories.rb
index 363a3099064..6c4792d0d6c 100644
--- a/db/post_migrate/20230405200858_requeue_backfill_project_wiki_repositories.rb
+++ b/db/post_migrate/20230405200858_requeue_backfill_project_wiki_repositories.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-# rubocop:disable BackgroundMigration/MissingDictionaryFile
+# rubocop:disable BackgroundMigration/DictionaryFile
class RequeueBackfillProjectWikiRepositories < Gitlab::Database::Migration[2.1]
MIGRATION = "BackfillProjectWikiRepositories"
DELAY_INTERVAL = 2.minutes
@@ -26,4 +26,4 @@ class RequeueBackfillProjectWikiRepositories < Gitlab::Database::Migration[2.1]
delete_batched_background_migration(MIGRATION, :projects, :id, [])
end
end
-# rubocop:enable BackgroundMigration/MissingDictionaryFile
+# rubocop:enable BackgroundMigration/DictionaryFile
diff --git a/db/post_migrate/20230508150219_reschedule_evidences_handling_unicode.rb b/db/post_migrate/20230508150219_reschedule_evidences_handling_unicode.rb
index 804db553f6f..ed23df4405e 100644
--- a/db/post_migrate/20230508150219_reschedule_evidences_handling_unicode.rb
+++ b/db/post_migrate/20230508150219_reschedule_evidences_handling_unicode.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-# rubocop:disable BackgroundMigration/MissingDictionaryFile
+# rubocop:disable BackgroundMigration/DictionaryFile
class RescheduleEvidencesHandlingUnicode < Gitlab::Database::Migration[2.1]
restrict_gitlab_migration gitlab_schema: :gitlab_main
@@ -29,4 +29,4 @@ class RescheduleEvidencesHandlingUnicode < Gitlab::Database::Migration[2.1]
delete_batched_background_migration(MIGRATION, :vulnerability_occurrences, :id, [])
end
end
-# rubocop:enable BackgroundMigration/MissingDictionaryFile
+# rubocop:enable BackgroundMigration/DictionaryFile
diff --git a/db/post_migrate/20230522111534_reschedule_migration_for_links_from_metadata.rb b/db/post_migrate/20230522111534_reschedule_migration_for_links_from_metadata.rb
index d351d795ddf..f49b158593f 100644
--- a/db/post_migrate/20230522111534_reschedule_migration_for_links_from_metadata.rb
+++ b/db/post_migrate/20230522111534_reschedule_migration_for_links_from_metadata.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-# rubocop: disable BackgroundMigration/MissingDictionaryFile
+# rubocop: disable BackgroundMigration/DictionaryFile
class RescheduleMigrationForLinksFromMetadata < Gitlab::Database::Migration[2.1]
MIGRATION = 'MigrateLinksForVulnerabilityFindings'
@@ -29,4 +29,4 @@ class RescheduleMigrationForLinksFromMetadata < Gitlab::Database::Migration[2.1]
delete_batched_background_migration(MIGRATION, :vulnerability_occurrences, :id, [])
end
end
-# rubocop: enable BackgroundMigration/MissingDictionaryFile
+# rubocop: enable BackgroundMigration/DictionaryFile
diff --git a/db/post_migrate/20230619005223_change_unconfirmed_created_at_index_on_users.rb b/db/post_migrate/20230619005223_change_unconfirmed_created_at_index_on_users.rb
index 5b9b4e36512..e299ce394a3 100644
--- a/db/post_migrate/20230619005223_change_unconfirmed_created_at_index_on_users.rb
+++ b/db/post_migrate/20230619005223_change_unconfirmed_created_at_index_on_users.rb
@@ -7,9 +7,11 @@ class ChangeUnconfirmedCreatedAtIndexOnUsers < Gitlab::Database::Migration[2.1]
NEW_INDEX_NAME = 'index_users_on_unconfirmed_created_at_active_type_sign_in_count'
def up
+ # rubocop:disable Migration/PreventIndexCreation
add_concurrent_index :users, [:created_at, :id],
name: NEW_INDEX_NAME,
where: "confirmed_at IS NULL AND state = 'active' AND user_type IN (0) AND sign_in_count = 0"
+ # rubocop:enable Migration/PreventIndexCreation
remove_concurrent_index_by_name :users, OLD_INDEX_NAME
end
diff --git a/db/post_migrate/20230724150939_index_projects_on_namespace_id_and_repository_size_limit.rb b/db/post_migrate/20230724150939_index_projects_on_namespace_id_and_repository_size_limit.rb
index b066cb248fb..fd2387e2bc4 100644
--- a/db/post_migrate/20230724150939_index_projects_on_namespace_id_and_repository_size_limit.rb
+++ b/db/post_migrate/20230724150939_index_projects_on_namespace_id_and_repository_size_limit.rb
@@ -9,7 +9,9 @@ class IndexProjectsOnNamespaceIdAndRepositorySizeLimit < Gitlab::Database::Migra
disable_ddl_transaction!
def up
+ # rubocop:disable Migration/PreventIndexCreation
add_concurrent_index :projects, [:namespace_id, :repository_size_limit], name: INDEX_NAME
+ # rubocop:enable Migration/PreventIndexCreation
end
def down
diff --git a/db/post_migrate/20230728151058_add_auditor_index_to_users_table.rb b/db/post_migrate/20230728151058_add_auditor_index_to_users_table.rb
index 1a849e7b728..055174b9e32 100644
--- a/db/post_migrate/20230728151058_add_auditor_index_to_users_table.rb
+++ b/db/post_migrate/20230728151058_add_auditor_index_to_users_table.rb
@@ -5,7 +5,9 @@ class AddAuditorIndexToUsersTable < Gitlab::Database::Migration[2.1]
disable_ddl_transaction!
def up
+ # rubocop:disable Migration/PreventIndexCreation
add_concurrent_index :users, :id, where: 'auditor IS true', name: INDEX_NAME
+ # rubocop:enable Migration/PreventIndexCreation
end
def down
diff --git a/db/post_migrate/20230913130629_index_org_id_on_projects.rb b/db/post_migrate/20230913130629_index_org_id_on_projects.rb
index 45186b900c6..c4d3de6c172 100644
--- a/db/post_migrate/20230913130629_index_org_id_on_projects.rb
+++ b/db/post_migrate/20230913130629_index_org_id_on_projects.rb
@@ -6,7 +6,9 @@ class IndexOrgIdOnProjects < Gitlab::Database::Migration[2.1]
INDEX_NAME = 'index_projects_on_organization_id'
def up
+ # rubocop:disable Migration/PreventIndexCreation
add_concurrent_index :projects, :organization_id, name: INDEX_NAME
+ # rubocop:enable Migration/PreventIndexCreation
end
def down
diff --git a/db/post_migrate/20231003045342_migrate_sidekiq_namespaced_jobs.rb b/db/post_migrate/20231003045342_migrate_sidekiq_namespaced_jobs.rb
new file mode 100644
index 00000000000..7d4d6876848
--- /dev/null
+++ b/db/post_migrate/20231003045342_migrate_sidekiq_namespaced_jobs.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+class MigrateSidekiqNamespacedJobs < Gitlab::Database::Migration[2.1]
+ BATCH_SIZE = 1000
+ SORTED_SET_NAMES = %w[schedule retry dead]
+
+ def up
+ SORTED_SET_NAMES.each do |set_name|
+ sorted_set_migrate("resque:gitlab:#{set_name}", set_name)
+ end
+
+ Sidekiq::Queue.all.each do |queue|
+ name = queue.name
+ sidekiq_queue_migrate("resque:gitlab:queue:#{name}", to: "queue:#{name}")
+ end
+ end
+
+ def down
+ SORTED_SET_NAMES.each do |set_name|
+ sorted_set_migrate(set_name, "resque:gitlab:#{set_name}")
+ end
+
+ Sidekiq::Queue.all.each do |queue|
+ name = queue.name
+ sidekiq_queue_migrate("queue:#{name}", to: "resque:gitlab:queue:#{name}")
+ end
+ end
+
+ private
+
+ def sidekiq_queue_migrate(queue_from, to:)
+ Gitlab::Redis::Queues.with do |conn| # rubocop:disable Cop/RedisQueueUsage
+ conn.rpoplpush(queue_from, to) while conn.llen(queue_from) > 0
+ end
+ end
+
+ def sorted_set_migrate(from, to)
+ cursor = '0'
+
+ loop do
+ result = []
+
+ Gitlab::Redis::Queues.with do |redis| # rubocop:disable Cop/RedisQueueUsage
+ cursor, result = redis.zscan(from, cursor, count: BATCH_SIZE)
+
+ next if result.empty?
+
+ redis.multi do |multi|
+ multi.zadd(to, result.map { |k, v| [v, k] })
+ multi.zrem(from, result.map { |k, _v| k })
+ end
+ end
+
+ sleep(0.01)
+
+ break if cursor == '0'
+ end
+ end
+end
diff --git a/db/post_migrate/20231003142534_add_build_timeout_index.rb b/db/post_migrate/20231003142534_add_build_timeout_index.rb
index 3a95c7cf748..5820a35eb8d 100644
--- a/db/post_migrate/20231003142534_add_build_timeout_index.rb
+++ b/db/post_migrate/20231003142534_add_build_timeout_index.rb
@@ -6,7 +6,9 @@ class AddBuildTimeoutIndex < Gitlab::Database::Migration[2.1]
INDEX_NAME = 'index_projects_on_id_where_build_timeout_geq_than_2629746'
def up
+ # rubocop:disable Migration/PreventIndexCreation
add_concurrent_index :projects, :id, where: 'build_timeout >= 2629746', name: INDEX_NAME
+ # rubocop:enable Migration/PreventIndexCreation
end
def down
diff --git a/db/post_migrate/20231009105056_index_users_on_email_domain_and_id.rb b/db/post_migrate/20231009105056_index_users_on_email_domain_and_id.rb
index e6b750ca38b..61aab7cc2c4 100644
--- a/db/post_migrate/20231009105056_index_users_on_email_domain_and_id.rb
+++ b/db/post_migrate/20231009105056_index_users_on_email_domain_and_id.rb
@@ -6,7 +6,9 @@ class IndexUsersOnEmailDomainAndId < Gitlab::Database::Migration[2.1]
INDEX_NAME = 'index_users_on_email_domain_and_id'
def up
+ # rubocop:disable Migration/PreventIndexCreation
add_concurrent_index(:users, "lower(split_part(email, '@', 2)), id", name: INDEX_NAME)
+ # rubocop:enable Migration/PreventIndexCreation
end
def down
diff --git a/db/post_migrate/20231016001000_fix_design_user_mentions_design_id_note_id_index_for_self_managed.rb b/db/post_migrate/20231016001000_fix_design_user_mentions_design_id_note_id_index_for_self_managed.rb
new file mode 100644
index 00000000000..454158ecbee
--- /dev/null
+++ b/db/post_migrate/20231016001000_fix_design_user_mentions_design_id_note_id_index_for_self_managed.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+class FixDesignUserMentionsDesignIdNoteIdIndexForSelfManaged < Gitlab::Database::Migration[2.1]
+ include Gitlab::Database::MigrationHelpers::ConvertToBigint
+
+ disable_ddl_transaction!
+
+ TABLE_NAME = 'design_user_mentions'
+ INDEX_NAME = 'design_user_mentions_on_design_id_and_note_id_unique_index'
+
+ def up
+ return if com_or_dev_or_test_but_not_jh?
+ return if index_exists?(TABLE_NAME, [:design_id, :note_id], unique: true, name: INDEX_NAME)
+
+ add_concurrent_index TABLE_NAME, [:design_id, :note_id], unique: true, name: "#{INDEX_NAME}_int8"
+
+ with_lock_retries(raise_on_exhaustion: true) do
+ execute "LOCK TABLE #{TABLE_NAME} IN ACCESS EXCLUSIVE MODE"
+
+ execute "DROP INDEX IF EXISTS #{INDEX_NAME}"
+ rename_index TABLE_NAME, "#{INDEX_NAME}_int8", INDEX_NAME
+ end
+ end
+
+ def down
+ # no-op
+ end
+end
diff --git a/db/post_migrate/20231016173128_add_temporary_index_to_merge_access_levels.rb b/db/post_migrate/20231016173128_add_temporary_index_to_merge_access_levels.rb
new file mode 100644
index 00000000000..0d8fbdfea00
--- /dev/null
+++ b/db/post_migrate/20231016173128_add_temporary_index_to_merge_access_levels.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+class AddTemporaryIndexToMergeAccessLevels < Gitlab::Database::Migration[2.2]
+ disable_ddl_transaction!
+ milestone '16.6'
+
+ INDEX_NAME = 'tmp_idx_protected_branch_merge_access_levels_on_id_with_group'
+
+ def up
+ # Temporary index to be removed in 16.7 https://gitlab.com/gitlab-org/gitlab/-/issues/430843
+ add_concurrent_index(
+ :protected_branch_merge_access_levels,
+ %i[id],
+ where: 'group_id IS NOT NULL',
+ name: INDEX_NAME
+ )
+ end
+
+ def down
+ remove_concurrent_index_by_name(
+ :protected_branch_merge_access_levels,
+ INDEX_NAME
+ )
+ end
+end
diff --git a/db/post_migrate/20231016173129_queue_delete_invalid_protected_branch_merge_access_levels.rb b/db/post_migrate/20231016173129_queue_delete_invalid_protected_branch_merge_access_levels.rb
new file mode 100644
index 00000000000..3f4009d783c
--- /dev/null
+++ b/db/post_migrate/20231016173129_queue_delete_invalid_protected_branch_merge_access_levels.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+class QueueDeleteInvalidProtectedBranchMergeAccessLevels < Gitlab::Database::Migration[2.1]
+ MIGRATION = "DeleteInvalidProtectedBranchMergeAccessLevels"
+ DELAY_INTERVAL = 2.minutes
+ MAX_BATCH_SIZE = 10_000
+ BATCH_SIZE = 5_000
+ SUB_BATCH_SIZE = 500
+
+ disable_ddl_transaction!
+ restrict_gitlab_migration gitlab_schema: :gitlab_main
+
+ def up
+ queue_batched_background_migration(
+ MIGRATION,
+ :protected_branch_merge_access_levels,
+ :id,
+ job_interval: DELAY_INTERVAL,
+ queued_migration_version: '20231016173129',
+ batch_size: BATCH_SIZE,
+ sub_batch_size: SUB_BATCH_SIZE
+ )
+ end
+
+ def down
+ delete_batched_background_migration(MIGRATION, :protected_branch_merge_access_levels, :id, [])
+ end
+end
diff --git a/db/post_migrate/20231016194926_add_temporary_index_to_push_access_levels.rb b/db/post_migrate/20231016194926_add_temporary_index_to_push_access_levels.rb
new file mode 100644
index 00000000000..91599051fd4
--- /dev/null
+++ b/db/post_migrate/20231016194926_add_temporary_index_to_push_access_levels.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+class AddTemporaryIndexToPushAccessLevels < Gitlab::Database::Migration[2.2]
+ disable_ddl_transaction!
+
+ milestone '16.6'
+ INDEX_NAME = 'tmp_idx_protected_branch_push_access_levels_on_id_with_group'
+
+ def up
+ # Temporary index to be removed in 16.7 https://gitlab.com/gitlab-org/gitlab/-/issues/430843
+ add_concurrent_index(
+ :protected_branch_push_access_levels,
+ %i[id],
+ where: 'group_id IS NOT NULL',
+ name: INDEX_NAME
+ )
+ end
+
+ def down
+ remove_concurrent_index_by_name(
+ :protected_branch_push_access_levels,
+ INDEX_NAME
+ )
+ end
+end
diff --git a/db/post_migrate/20231016194927_queue_delete_invalid_protected_branch_push_access_levels.rb b/db/post_migrate/20231016194927_queue_delete_invalid_protected_branch_push_access_levels.rb
new file mode 100644
index 00000000000..6accaa3296b
--- /dev/null
+++ b/db/post_migrate/20231016194927_queue_delete_invalid_protected_branch_push_access_levels.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+class QueueDeleteInvalidProtectedBranchPushAccessLevels < Gitlab::Database::Migration[2.1]
+ MIGRATION = "DeleteInvalidProtectedBranchPushAccessLevels"
+ DELAY_INTERVAL = 2.minutes
+ MAX_BATCH_SIZE = 10_000
+ BATCH_SIZE = 5_000
+ SUB_BATCH_SIZE = 500
+
+ disable_ddl_transaction!
+ restrict_gitlab_migration gitlab_schema: :gitlab_main
+
+ def up
+ queue_batched_background_migration(
+ MIGRATION,
+ :protected_branch_push_access_levels,
+ :id,
+ job_interval: DELAY_INTERVAL,
+ queued_migration_version: '20231016194927',
+ batch_size: BATCH_SIZE,
+ sub_batch_size: SUB_BATCH_SIZE
+ )
+ end
+
+ def down
+ delete_batched_background_migration(MIGRATION, :protected_branch_push_access_levels, :id, [])
+ end
+end
diff --git a/db/post_migrate/20231016194942_add_temporary_index_to_create_access_levels.rb b/db/post_migrate/20231016194942_add_temporary_index_to_create_access_levels.rb
new file mode 100644
index 00000000000..d28b664c517
--- /dev/null
+++ b/db/post_migrate/20231016194942_add_temporary_index_to_create_access_levels.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+class AddTemporaryIndexToCreateAccessLevels < Gitlab::Database::Migration[2.2]
+ disable_ddl_transaction!
+ milestone '16.6'
+
+ INDEX_NAME = 'tmp_idx_protected_tag_create_access_levels_on_id_with_group'
+
+ def up
+ # Temporary index to be removed in 16.7 https://gitlab.com/gitlab-org/gitlab/-/issues/430843
+ add_concurrent_index(
+ :protected_tag_create_access_levels,
+ %i[id],
+ where: 'group_id IS NOT NULL',
+ name: INDEX_NAME
+ )
+ end
+
+ def down
+ remove_concurrent_index_by_name(
+ :protected_tag_create_access_levels,
+ INDEX_NAME
+ )
+ end
+end
diff --git a/db/post_migrate/20231016194943_queue_delete_invalid_protected_tag_create_access_levels.rb b/db/post_migrate/20231016194943_queue_delete_invalid_protected_tag_create_access_levels.rb
new file mode 100644
index 00000000000..5880124d0a6
--- /dev/null
+++ b/db/post_migrate/20231016194943_queue_delete_invalid_protected_tag_create_access_levels.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+class QueueDeleteInvalidProtectedTagCreateAccessLevels < Gitlab::Database::Migration[2.1]
+ MIGRATION = "DeleteInvalidProtectedTagCreateAccessLevels"
+ DELAY_INTERVAL = 2.minutes
+ BATCH_SIZE = 10_000
+ SUB_BATCH_SIZE = 500
+
+ disable_ddl_transaction!
+ restrict_gitlab_migration gitlab_schema: :gitlab_main
+
+ def up
+ queue_batched_background_migration(
+ MIGRATION,
+ :protected_tag_create_access_levels,
+ :id,
+ job_interval: DELAY_INTERVAL,
+ queued_migration_version: '20231016194943',
+ batch_size: BATCH_SIZE,
+ sub_batch_size: SUB_BATCH_SIZE
+ )
+ end
+
+ def down
+ delete_batched_background_migration(MIGRATION, :protected_tag_create_access_levels, :id, [])
+ end
+end
diff --git a/db/post_migrate/20231017172156_add_index_on_projects_for_adjourned_deletion.rb b/db/post_migrate/20231017172156_add_index_on_projects_for_adjourned_deletion.rb
index 6a689a5e11a..9b73035471e 100644
--- a/db/post_migrate/20231017172156_add_index_on_projects_for_adjourned_deletion.rb
+++ b/db/post_migrate/20231017172156_add_index_on_projects_for_adjourned_deletion.rb
@@ -6,10 +6,12 @@ class AddIndexOnProjectsForAdjournedDeletion < Gitlab::Database::Migration[2.1]
INDEX_NAME = 'index_projects_id_for_aimed_for_deletion'
def up
+ # rubocop:disable Migration/PreventIndexCreation
add_concurrent_index :projects,
[:id, :marked_for_deletion_at],
where: 'marked_for_deletion_at IS NOT NULL AND pending_delete = false',
name: INDEX_NAME
+ # rubocop:enable Migration/PreventIndexCreation
end
def down
diff --git a/db/post_migrate/20231018083247_remove_users_email_opted_in_columns.rb b/db/post_migrate/20231018083247_remove_users_email_opted_in_columns.rb
new file mode 100644
index 00000000000..a77ccb599df
--- /dev/null
+++ b/db/post_migrate/20231018083247_remove_users_email_opted_in_columns.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class RemoveUsersEmailOptedInColumns < Gitlab::Database::Migration[2.1]
+ enable_lock_retries!
+
+ def up
+ remove_column :users, :email_opted_in
+ remove_column :users, :email_opted_in_ip
+ remove_column :users, :email_opted_in_source_id
+ remove_column :users, :email_opted_in_at
+ end
+
+ # This migration removes columns. Disabling rule only for rollback action
+ # rubocop:disable Migration/AddColumnsToWideTables
+ def down
+ add_column :users, :email_opted_in, :boolean
+ add_column :users, :email_opted_in_ip, :string
+ add_column :users, :email_opted_in_source_id, :integer
+ add_column :users, :email_opted_in_at, :datetime_with_timezone
+ end
+ # rubocop:enable Migration/AddColumnsToWideTables
+end
diff --git a/db/post_migrate/20231018093625_drop_index_namespaces_on_shared_and_extra_runners_minutes_limit.rb b/db/post_migrate/20231018093625_drop_index_namespaces_on_shared_and_extra_runners_minutes_limit.rb
new file mode 100644
index 00000000000..9b293b066d6
--- /dev/null
+++ b/db/post_migrate/20231018093625_drop_index_namespaces_on_shared_and_extra_runners_minutes_limit.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class DropIndexNamespacesOnSharedAndExtraRunnersMinutesLimit < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ TABLE_NAME = :namespaces
+ INDEX_NAME = :index_namespaces_on_shared_and_extra_runners_minutes_limit
+
+ def up
+ remove_concurrent_index_by_name TABLE_NAME, INDEX_NAME
+ end
+
+ def down
+ # no-op
+ # Since adding the same index will be time consuming,
+ # we have to create it asynchronously using 'prepare_async_index' helper (if needed).
+ end
+end
diff --git a/db/post_migrate/20231018105749_remove_application_settings_marketing_emails_enabled_column.rb b/db/post_migrate/20231018105749_remove_application_settings_marketing_emails_enabled_column.rb
new file mode 100644
index 00000000000..ea7ef21f110
--- /dev/null
+++ b/db/post_migrate/20231018105749_remove_application_settings_marketing_emails_enabled_column.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class RemoveApplicationSettingsMarketingEmailsEnabledColumn < Gitlab::Database::Migration[2.1]
+ def up
+ remove_column :application_settings, :in_product_marketing_emails_enabled
+ end
+
+ def down
+ add_column :application_settings, :in_product_marketing_emails_enabled, :boolean, default: true, null: false
+ end
+end
diff --git a/db/post_migrate/20231019003052_swap_columns_for_ci_pipelines_pipeline_id_bigint_v2.rb b/db/post_migrate/20231019003052_swap_columns_for_ci_pipelines_pipeline_id_bigint_v2.rb
new file mode 100644
index 00000000000..4065bad5fb5
--- /dev/null
+++ b/db/post_migrate/20231019003052_swap_columns_for_ci_pipelines_pipeline_id_bigint_v2.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+class SwapColumnsForCiPipelinesPipelineIdBigintV2 < Gitlab::Database::Migration[2.1]
+ include Gitlab::Database::MigrationHelpers::WraparoundAutovacuum
+ include Gitlab::Database::MigrationHelpers::Swapping
+
+ disable_ddl_transaction!
+
+ TABLE_NAME = :ci_pipelines
+ TRIGGER_FUNCTION_NAME = :trigger_1bd97da9c1a4
+ COLUMN_NAME = :auto_canceled_by_id
+ BIGINT_COLUMN_NAME = :auto_canceled_by_id_convert_to_bigint
+ FK_NAME = :fk_262d4c2d19
+ BIGINT_FK_NAME = :fk_67e4288f3a
+ INDEX_NAME = :index_ci_pipelines_on_auto_canceled_by_id
+ BIGINT_INDEX_NAME = :index_ci_pipelines_on_auto_canceled_by_id_bigint
+
+ def up
+ return if should_skip? || column_type_of?(:bigint)
+
+ swap
+ end
+
+ def down
+ return if should_skip? || column_type_of?(:integer)
+
+ swap
+ end
+
+ private
+
+ def should_skip?
+ !can_execute_on?(TABLE_NAME)
+ end
+
+ def column_type_of?(type)
+ column_for(TABLE_NAME, COLUMN_NAME).sql_type.to_s == type.to_s
+ end
+
+ def swap
+ # rubocop:disable Migration/WithLockRetriesDisallowedMethod
+ with_lock_retries(raise_on_exhaustion: true) do
+ # Lock the tables involved.
+ lock_tables(TABLE_NAME)
+
+ # Rename the columns to swap names
+ swap_columns(TABLE_NAME, COLUMN_NAME, BIGINT_COLUMN_NAME)
+
+ # Reset the trigger function
+ reset_trigger_function(TRIGGER_FUNCTION_NAME)
+
+ # Swap fkey constraint
+ swap_foreign_keys(TABLE_NAME, FK_NAME, BIGINT_FK_NAME)
+
+ # Swap index
+ swap_indexes(TABLE_NAME, INDEX_NAME, BIGINT_INDEX_NAME)
+ end
+ # rubocop:enable Migration/WithLockRetriesDisallowedMethod
+ end
+end
diff --git a/db/post_migrate/20231019084731_swap_columns_for_ci_stages_pipeline_id_bigint_v2.rb b/db/post_migrate/20231019084731_swap_columns_for_ci_stages_pipeline_id_bigint_v2.rb
new file mode 100644
index 00000000000..d64d2ea737d
--- /dev/null
+++ b/db/post_migrate/20231019084731_swap_columns_for_ci_stages_pipeline_id_bigint_v2.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+class SwapColumnsForCiStagesPipelineIdBigintV2 < Gitlab::Database::Migration[2.1]
+ include Gitlab::Database::MigrationHelpers::WraparoundAutovacuum
+ include Gitlab::Database::MigrationHelpers::Swapping
+
+ disable_ddl_transaction!
+
+ TABLE_NAME = :ci_stages
+ TRIGGER_FUNCTION_NAME = :trigger_07bc3c48f407
+ COLUMN_NAME = :pipeline_id
+ BIGINT_COLUMN_NAME = :pipeline_id_convert_to_bigint
+ FK_NAME = :fk_fb57e6cc56
+ BIGINT_FK_NAME = :fk_c5ddde695f
+ INDEX_NAMES = %i[
+ index_ci_stages_on_pipeline_id
+ index_ci_stages_on_pipeline_id_and_id
+ index_ci_stages_on_pipeline_id_and_name
+ index_ci_stages_on_pipeline_id_and_position
+ ]
+ BIGINT_INDEX_NAMES = %i[
+ index_ci_stages_on_pipeline_id_convert_to_bigint
+ index_ci_stages_on_pipeline_id_convert_to_bigint_and_id
+ index_ci_stages_on_pipeline_id_convert_to_bigint_and_name
+ index_ci_stages_on_pipeline_id_convert_to_bigint_and_position
+ ]
+
+ def up
+ return if should_skip? || column_type_of?(:bigint)
+
+ swap
+ end
+
+ def down
+ return if should_skip? || column_type_of?(:integer)
+
+ swap
+ end
+
+ private
+
+ def should_skip?
+ !can_execute_on?(:ci_pipelines, :ci_stages)
+ end
+
+ def column_type_of?(type)
+ column_for(TABLE_NAME, COLUMN_NAME).sql_type.to_s == type.to_s
+ end
+
+ def swap
+ # rubocop:disable Migration/WithLockRetriesDisallowedMethod
+ with_lock_retries(raise_on_exhaustion: true) do
+ # Lock the tables involved.
+ lock_tables(:ci_pipelines, :ci_stages)
+
+ # Rename the columns to swap names
+ swap_columns(TABLE_NAME, COLUMN_NAME, BIGINT_COLUMN_NAME)
+
+ # Reset the trigger function
+ reset_trigger_function(TRIGGER_FUNCTION_NAME)
+
+ # Swap fkey constraint
+ swap_foreign_keys(TABLE_NAME, FK_NAME, BIGINT_FK_NAME)
+
+ # Swap index
+ INDEX_NAMES.each_with_index do |index_name, i|
+ swap_indexes(TABLE_NAME, index_name, BIGINT_INDEX_NAMES[i])
+ end
+ end
+ # rubocop:enable Migration/WithLockRetriesDisallowedMethod
+ end
+end
diff --git a/db/post_migrate/20231019223224_backfill_catalog_resources_name_and_description.rb b/db/post_migrate/20231019223224_backfill_catalog_resources_name_and_description.rb
new file mode 100644
index 00000000000..fd5db7621e3
--- /dev/null
+++ b/db/post_migrate/20231019223224_backfill_catalog_resources_name_and_description.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+class BackfillCatalogResourcesNameAndDescription < Gitlab::Database::Migration[2.1]
+ restrict_gitlab_migration gitlab_schema: :gitlab_main
+
+ def up
+ sql = <<-SQL
+ UPDATE catalog_resources
+ SET name = projects.name,
+ description = projects.description
+ FROM projects
+ WHERE catalog_resources.project_id = projects.id
+ SQL
+
+ execute(sql)
+ end
+
+ def down
+ # no-op
+
+ # The `name` and `description` columns in `catalog_resources` are denormalized;
+ # they should always stay in sync with the corresponding data in `projects`.
+ end
+end
diff --git a/db/post_migrate/20231020082425_remove_force_full_reconciliation_from_workspaces.rb b/db/post_migrate/20231020082425_remove_force_full_reconciliation_from_workspaces.rb
new file mode 100644
index 00000000000..85283183323
--- /dev/null
+++ b/db/post_migrate/20231020082425_remove_force_full_reconciliation_from_workspaces.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class RemoveForceFullReconciliationFromWorkspaces < Gitlab::Database::Migration[2.1]
+ enable_lock_retries!
+
+ def change
+ remove_column :workspaces, :force_full_reconciliation, :boolean, default: false, null: false
+ end
+end
diff --git a/db/post_migrate/20231020150211_delete_duplicated_index_scan_result_policies_on_policy_configuration_id.rb b/db/post_migrate/20231020150211_delete_duplicated_index_scan_result_policies_on_policy_configuration_id.rb
new file mode 100644
index 00000000000..8df7a7d5194
--- /dev/null
+++ b/db/post_migrate/20231020150211_delete_duplicated_index_scan_result_policies_on_policy_configuration_id.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class DeleteDuplicatedIndexScanResultPoliciesOnPolicyConfigurationId < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ INDEX_NAME = 'index_scan_result_policies_on_policy_configuration_id'
+ COLUMNS = %i[security_orchestration_policy_configuration_id]
+
+ def up
+ remove_concurrent_index_by_name :scan_result_policies, INDEX_NAME
+ end
+
+ def down
+ add_concurrent_index :scan_result_policies, COLUMNS, name: INDEX_NAME
+ end
+end
diff --git a/db/post_migrate/20231023083349_init_conversion_for_p_ci_builds.rb b/db/post_migrate/20231023083349_init_conversion_for_p_ci_builds.rb
new file mode 100644
index 00000000000..886e8c85599
--- /dev/null
+++ b/db/post_migrate/20231023083349_init_conversion_for_p_ci_builds.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+class InitConversionForPCiBuilds < Gitlab::Database::Migration[2.1]
+ include ::Gitlab::Database::MigrationHelpers::WraparoundAutovacuum
+
+ disable_ddl_transaction!
+
+ TABLE_NAME = :p_ci_builds
+ COLUMN_NAMES = %i[
+ auto_canceled_by_id
+ commit_id
+ erased_by_id
+ project_id
+ runner_id
+ trigger_request_id
+ upstream_pipeline_id
+ user_id
+ ]
+
+ def up
+ return if should_skip?
+
+ initialize_conversion_of_integer_to_bigint(TABLE_NAME, COLUMN_NAMES)
+ end
+
+ def down
+ return if should_skip?
+
+ revert_initialize_conversion_of_integer_to_bigint(TABLE_NAME, COLUMN_NAMES)
+ end
+
+ private
+
+ def should_skip?
+ !can_execute_on?(TABLE_NAME)
+ end
+end
diff --git a/db/post_migrate/20231023113908_add_index_stopping_environments_on_updated_at.rb b/db/post_migrate/20231023113908_add_index_stopping_environments_on_updated_at.rb
new file mode 100644
index 00000000000..01fe32d6bd7
--- /dev/null
+++ b/db/post_migrate/20231023113908_add_index_stopping_environments_on_updated_at.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+# See https://docs.gitlab.com/ee/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddIndexStoppingEnvironmentsOnUpdatedAt < Gitlab::Database::Migration[2.1]
+ INDEX_NAME = 'index_environments_on_updated_at_for_stopping_state'
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :environments, :updated_at, where: "state = 'stopping'", name: INDEX_NAME
+ end
+
+ def down
+ remove_concurrent_index_by_name :environments, name: INDEX_NAME
+ end
+end
diff --git a/db/post_migrate/20231023164908_async_drop_index_users_on_accepted_term_id.rb b/db/post_migrate/20231023164908_async_drop_index_users_on_accepted_term_id.rb
new file mode 100644
index 00000000000..a5688a9b196
--- /dev/null
+++ b/db/post_migrate/20231023164908_async_drop_index_users_on_accepted_term_id.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class AsyncDropIndexUsersOnAcceptedTermId < Gitlab::Database::Migration[2.2]
+ milestone '16.6'
+ disable_ddl_transaction!
+
+ TABLE_NAME = 'users'
+ INDEX_NAME = 'index_users_on_accepted_term_id'
+ COLUMN = 'accepted_term_id'
+
+ # TODO: Index to be destroyed synchronously in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/135293
+ def up
+ prepare_async_index_removal TABLE_NAME, COLUMN, name: INDEX_NAME
+ end
+
+ def down
+ prepare_async_index_removal TABLE_NAME, COLUMN, name: INDEX_NAME
+ end
+end
diff --git a/db/post_migrate/20231024015915_drop_index_namespaces_on_created_at_for_gitlab_com.rb b/db/post_migrate/20231024015915_drop_index_namespaces_on_created_at_for_gitlab_com.rb
new file mode 100644
index 00000000000..8f2f8a4064c
--- /dev/null
+++ b/db/post_migrate/20231024015915_drop_index_namespaces_on_created_at_for_gitlab_com.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+class DropIndexNamespacesOnCreatedAtForGitlabCom < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ TABLE_NAME = :namespaces
+ INDEX_NAME = :index_namespaces_on_created_at
+
+ def up
+ return unless should_run?
+
+ remove_concurrent_index_by_name TABLE_NAME, INDEX_NAME
+ end
+
+ def down
+ return unless should_run?
+
+ add_concurrent_index TABLE_NAME, :created_at, name: INDEX_NAME
+ end
+
+ def should_run?
+ Gitlab.com_except_jh?
+ end
+end
diff --git a/db/post_migrate/20231024025457_cleanup_bigint_conversion_for_ci_project_monthly_usages_shared_runners_duration.rb b/db/post_migrate/20231024025457_cleanup_bigint_conversion_for_ci_project_monthly_usages_shared_runners_duration.rb
new file mode 100644
index 00000000000..9528c00a1fd
--- /dev/null
+++ b/db/post_migrate/20231024025457_cleanup_bigint_conversion_for_ci_project_monthly_usages_shared_runners_duration.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class CleanupBigintConversionForCiProjectMonthlyUsagesSharedRunnersDuration < Gitlab::Database::Migration[2.1]
+ enable_lock_retries!
+
+ TABLE = :ci_project_monthly_usages
+ COLUMNS = [:shared_runners_duration]
+
+ def up
+ cleanup_conversion_of_integer_to_bigint(TABLE, COLUMNS)
+ end
+
+ def down
+ restore_conversion_of_integer_to_bigint(TABLE, COLUMNS)
+ end
+end
diff --git a/db/post_migrate/20231024025533_cleanup_bigint_conversion_for_ci_namespace_monthly_usages_shared_runners_duration.rb b/db/post_migrate/20231024025533_cleanup_bigint_conversion_for_ci_namespace_monthly_usages_shared_runners_duration.rb
new file mode 100644
index 00000000000..792650130cd
--- /dev/null
+++ b/db/post_migrate/20231024025533_cleanup_bigint_conversion_for_ci_namespace_monthly_usages_shared_runners_duration.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class CleanupBigintConversionForCiNamespaceMonthlyUsagesSharedRunnersDuration < Gitlab::Database::Migration[2.1]
+ enable_lock_retries!
+
+ TABLE = :ci_namespace_monthly_usages
+ COLUMNS = [:shared_runners_duration]
+
+ def up
+ cleanup_conversion_of_integer_to_bigint(TABLE, COLUMNS)
+ end
+
+ def down
+ restore_conversion_of_integer_to_bigint(TABLE, COLUMNS)
+ end
+end
diff --git a/db/post_migrate/20231024025629_cleanup_ci_pipeline_chat_data_pipeline_id_bigint.rb b/db/post_migrate/20231024025629_cleanup_ci_pipeline_chat_data_pipeline_id_bigint.rb
new file mode 100644
index 00000000000..e79f4eb43b7
--- /dev/null
+++ b/db/post_migrate/20231024025629_cleanup_ci_pipeline_chat_data_pipeline_id_bigint.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+class CleanupCiPipelineChatDataPipelineIdBigint < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ TABLE = :ci_pipeline_chat_data
+ COLUMNS = [:pipeline_id]
+
+ def up
+ with_lock_retries(raise_on_exhaustion: true) do
+ cleanup_conversion_of_integer_to_bigint(TABLE, COLUMNS) # rubocop:disable Migration/WithLockRetriesDisallowedMethod
+ end
+ end
+
+ def down
+ restore_conversion_of_integer_to_bigint(TABLE, COLUMNS)
+
+ add_concurrent_index(
+ TABLE, :pipeline_id_convert_to_bigint,
+ name: :index_ci_pipeline_chat_data_on_pipeline_id_convert_to_bigint,
+ unique: true
+ )
+ add_concurrent_foreign_key(
+ TABLE, :ci_pipelines,
+ column: :pipeline_id_convert_to_bigint,
+ on_delete: :cascade, validate: true, reverse_lock_order: true
+ )
+ end
+end
diff --git a/db/post_migrate/20231024080150_cleanup_ci_sources_pipelines_pipeline_id_bigint.rb b/db/post_migrate/20231024080150_cleanup_ci_sources_pipelines_pipeline_id_bigint.rb
new file mode 100644
index 00000000000..6aa8019a182
--- /dev/null
+++ b/db/post_migrate/20231024080150_cleanup_ci_sources_pipelines_pipeline_id_bigint.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+class CleanupCiSourcesPipelinesPipelineIdBigint < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ TABLE = :ci_sources_pipelines
+ REFERENCING_TABLE = :ci_pipelines
+ COLUMNS = [:pipeline_id, :source_pipeline_id]
+
+ def up
+ with_lock_retries(raise_on_exhaustion: true) do
+ lock_tables(:ci_pipelines, TABLE)
+ cleanup_conversion_of_integer_to_bigint(TABLE, COLUMNS) # rubocop:disable Migration/WithLockRetriesDisallowedMethod
+ end
+ end
+
+ def down
+ restore_conversion_of_integer_to_bigint(TABLE, COLUMNS)
+
+ add_concurrent_index(TABLE, :pipeline_id_convert_to_bigint,
+ name: :index_ci_sources_pipelines_on_pipeline_id_bigint)
+ add_concurrent_index(TABLE, :source_pipeline_id_convert_to_bigint,
+ name: :index_ci_sources_pipelines_on_source_pipeline_id_bigint)
+ add_concurrent_foreign_key(
+ TABLE, REFERENCING_TABLE,
+ column: :pipeline_id_convert_to_bigint,
+ on_delete: :cascade, validate: true, reverse_lock_order: true
+ )
+ add_concurrent_foreign_key(
+ TABLE, REFERENCING_TABLE,
+ column: :source_pipeline_id_convert_to_bigint,
+ on_delete: :cascade, validate: true, reverse_lock_order: true
+ )
+ end
+end
diff --git a/db/post_migrate/20231024124856_remove_redundant_group_stages_index.rb b/db/post_migrate/20231024124856_remove_redundant_group_stages_index.rb
new file mode 100644
index 00000000000..d9546597bd9
--- /dev/null
+++ b/db/post_migrate/20231024124856_remove_redundant_group_stages_index.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class RemoveRedundantGroupStagesIndex < Gitlab::Database::Migration[2.2]
+ disable_ddl_transaction!
+
+ milestone '16.6'
+
+ INDEX_NAME = 'index_analytics_ca_group_stages_on_group_id'
+
+ def up
+ remove_concurrent_index_by_name(:analytics_cycle_analytics_group_stages, INDEX_NAME)
+ end
+
+ def down
+ add_concurrent_index(:analytics_cycle_analytics_group_stages, :group_id, name: INDEX_NAME)
+ end
+end
diff --git a/db/post_migrate/20231024125551_remove_redundant_mr_metrics_index_on_target_project_id.rb b/db/post_migrate/20231024125551_remove_redundant_mr_metrics_index_on_target_project_id.rb
new file mode 100644
index 00000000000..2186402828d
--- /dev/null
+++ b/db/post_migrate/20231024125551_remove_redundant_mr_metrics_index_on_target_project_id.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class RemoveRedundantMrMetricsIndexOnTargetProjectId < Gitlab::Database::Migration[2.2]
+ disable_ddl_transaction!
+
+ milestone '16.6'
+
+ INDEX_NAME = 'index_merge_request_metrics_on_target_project_id'
+
+ def up
+ remove_concurrent_index_by_name(:merge_request_metrics, INDEX_NAME)
+ end
+
+ def down
+ add_concurrent_index(:merge_request_metrics, :target_project_id, name: INDEX_NAME)
+ end
+end
diff --git a/db/post_migrate/20231025025733_swap_columns_for_ci_pipelines_pipeline_id_bigint_for_self_host.rb b/db/post_migrate/20231025025733_swap_columns_for_ci_pipelines_pipeline_id_bigint_for_self_host.rb
new file mode 100644
index 00000000000..a960258ff3d
--- /dev/null
+++ b/db/post_migrate/20231025025733_swap_columns_for_ci_pipelines_pipeline_id_bigint_for_self_host.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+class SwapColumnsForCiPipelinesPipelineIdBigintForSelfHost < Gitlab::Database::Migration[2.2]
+ include Gitlab::Database::MigrationHelpers::Swapping
+
+ milestone '16.6'
+ disable_ddl_transaction!
+
+ TABLE_NAME = :ci_pipelines
+ TRIGGER_FUNCTION_NAME = :trigger_1bd97da9c1a4
+ COLUMN_NAME = :auto_canceled_by_id
+ BIGINT_COLUMN_NAME = :auto_canceled_by_id_convert_to_bigint
+ FK_NAME = :fk_262d4c2d19
+ BIGINT_FK_NAME = :fk_67e4288f3a
+ INDEX_NAME = :index_ci_pipelines_on_auto_canceled_by_id
+ BIGINT_INDEX_NAME = :index_ci_pipelines_on_auto_canceled_by_id_bigint
+
+ def up
+ return if column_type_of?(:bigint)
+
+ swap
+ end
+
+ def down
+ return if column_type_of?(:integer)
+
+ swap
+ end
+
+ private
+
+ def column_type_of?(type)
+ column_for(TABLE_NAME, COLUMN_NAME).sql_type.to_s == type.to_s
+ end
+
+ def swap
+ with_lock_retries(raise_on_exhaustion: true) do
+ # Lock the tables involved.
+ lock_tables(TABLE_NAME)
+
+ # Rename the columns to swap names
+ swap_columns(TABLE_NAME, COLUMN_NAME, BIGINT_COLUMN_NAME)
+
+ # Reset the trigger function
+ reset_trigger_function(TRIGGER_FUNCTION_NAME)
+
+ # Swap fkey constraint
+ swap_foreign_keys(TABLE_NAME, FK_NAME, BIGINT_FK_NAME)
+
+ # Swap index
+ swap_indexes(TABLE_NAME, INDEX_NAME, BIGINT_INDEX_NAME)
+ end
+ end
+end
diff --git a/db/post_migrate/20231025031337_cleanup_ci_pipeline_messages_pipeline_id_bigint.rb b/db/post_migrate/20231025031337_cleanup_ci_pipeline_messages_pipeline_id_bigint.rb
new file mode 100644
index 00000000000..b9e44f8f2d0
--- /dev/null
+++ b/db/post_migrate/20231025031337_cleanup_ci_pipeline_messages_pipeline_id_bigint.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+class CleanupCiPipelineMessagesPipelineIdBigint < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ TABLE = :ci_pipeline_messages
+ COLUMNS = [:pipeline_id]
+
+ def up
+ with_lock_retries(raise_on_exhaustion: true) do
+ lock_tables(:ci_pipelines, TABLE)
+ cleanup_conversion_of_integer_to_bigint(TABLE, COLUMNS) # rubocop:disable Migration/WithLockRetriesDisallowedMethod
+ end
+ end
+
+ def down
+ restore_conversion_of_integer_to_bigint(TABLE, COLUMNS)
+
+ add_concurrent_index(
+ TABLE, :pipeline_id_convert_to_bigint,
+ name: :index_ci_pipeline_messages_on_pipeline_id_convert_to_bigint
+ )
+ add_concurrent_foreign_key(
+ TABLE, :ci_pipelines,
+ column: :pipeline_id_convert_to_bigint,
+ on_delete: :cascade, validate: true, reverse_lock_order: true
+ )
+ end
+end
diff --git a/db/post_migrate/20231025031539_swap_columns_for_ci_stages_pipeline_id_bigint_for_self_host.rb b/db/post_migrate/20231025031539_swap_columns_for_ci_stages_pipeline_id_bigint_for_self_host.rb
new file mode 100644
index 00000000000..c28f49899b6
--- /dev/null
+++ b/db/post_migrate/20231025031539_swap_columns_for_ci_stages_pipeline_id_bigint_for_self_host.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+class SwapColumnsForCiStagesPipelineIdBigintForSelfHost < Gitlab::Database::Migration[2.2]
+ include Gitlab::Database::MigrationHelpers::Swapping
+
+ milestone '16.6'
+ disable_ddl_transaction!
+
+ TABLE_NAME = :ci_stages
+ TRIGGER_FUNCTION_NAME = :trigger_07bc3c48f407
+ COLUMN_NAME = :pipeline_id
+ BIGINT_COLUMN_NAME = :pipeline_id_convert_to_bigint
+ FK_NAME = :fk_fb57e6cc56
+ BIGINT_FK_NAME = :fk_c5ddde695f
+ INDEX_NAMES = %i[
+ index_ci_stages_on_pipeline_id
+ index_ci_stages_on_pipeline_id_and_id
+ index_ci_stages_on_pipeline_id_and_name
+ index_ci_stages_on_pipeline_id_and_position
+ ]
+ BIGINT_INDEX_NAMES = %i[
+ index_ci_stages_on_pipeline_id_convert_to_bigint
+ index_ci_stages_on_pipeline_id_convert_to_bigint_and_id
+ index_ci_stages_on_pipeline_id_convert_to_bigint_and_name
+ index_ci_stages_on_pipeline_id_convert_to_bigint_and_position
+ ]
+
+ def up
+ return if column_type_of?(:bigint)
+
+ swap
+ end
+
+ def down
+ return if column_type_of?(:integer)
+
+ swap
+ end
+
+ private
+
+ def column_type_of?(type)
+ column_for(TABLE_NAME, COLUMN_NAME).sql_type.to_s == type.to_s
+ end
+
+ def swap
+ with_lock_retries(raise_on_exhaustion: true) do
+ # Lock the tables involved.
+ lock_tables(:ci_pipelines, :ci_stages)
+
+ # Rename the columns to swap names
+ swap_columns(TABLE_NAME, COLUMN_NAME, BIGINT_COLUMN_NAME)
+
+ # Reset the trigger function
+ reset_trigger_function(TRIGGER_FUNCTION_NAME)
+
+ # Swap fkey constraint
+ swap_foreign_keys(TABLE_NAME, FK_NAME, BIGINT_FK_NAME)
+
+ # Swap index
+ INDEX_NAMES.each_with_index do |index_name, i|
+ swap_indexes(TABLE_NAME, index_name, BIGINT_INDEX_NAMES[i])
+ end
+ end
+ end
+end
diff --git a/db/post_migrate/20231026103346_drop_project_settings_jitsu_key.rb b/db/post_migrate/20231026103346_drop_project_settings_jitsu_key.rb
new file mode 100644
index 00000000000..606648ca7fa
--- /dev/null
+++ b/db/post_migrate/20231026103346_drop_project_settings_jitsu_key.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class DropProjectSettingsJitsuKey < Gitlab::Database::Migration[2.2]
+ milestone '16.6'
+
+ disable_ddl_transaction!
+
+ def up
+ with_lock_retries do
+ remove_column :project_settings, :jitsu_key, if_exists: true
+ end
+ end
+
+ def down
+ with_lock_retries do
+ add_column :project_settings, :jitsu_key, :text, if_not_exists: true
+ end
+
+ add_text_limit :project_settings, :jitsu_key, 100
+ end
+end
diff --git a/db/post_migrate/20231027013210_remove_partial_index_deployments_for_legacy_successful_deployments.rb b/db/post_migrate/20231027013210_remove_partial_index_deployments_for_legacy_successful_deployments.rb
new file mode 100644
index 00000000000..2bd52fdc10a
--- /dev/null
+++ b/db/post_migrate/20231027013210_remove_partial_index_deployments_for_legacy_successful_deployments.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class RemovePartialIndexDeploymentsForLegacySuccessfulDeployments < Gitlab::Database::Migration[2.2]
+ INDEX_NAME = 'partial_index_deployments_for_legacy_successful_deployments'
+
+ milestone '16.6'
+
+ disable_ddl_transaction!
+
+ def up
+ remove_concurrent_index_by_name :deployments, name: INDEX_NAME
+ end
+
+ def down
+ # This is based on the following `CREATE INDEX` command in db/init_structure.sql:
+ # CREATE INDEX partial_index_deployments_for_legacy_successful_deployments ON deployments
+ # USING btree (id) WHERE ((finished_at IS NULL) AND (status = 2));
+ add_concurrent_index :deployments, :id, name: INDEX_NAME, where: '((finished_at IS NULL) AND (status = 2))'
+ end
+end
diff --git a/db/post_migrate/20231027060443_backfill_system_note_metadata_id_for_bigint_conversion.rb b/db/post_migrate/20231027060443_backfill_system_note_metadata_id_for_bigint_conversion.rb
new file mode 100644
index 00000000000..d3c90134102
--- /dev/null
+++ b/db/post_migrate/20231027060443_backfill_system_note_metadata_id_for_bigint_conversion.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class BackfillSystemNoteMetadataIdForBigintConversion < Gitlab::Database::Migration[2.2]
+ restrict_gitlab_migration gitlab_schema: :gitlab_main
+
+ TABLE = :system_note_metadata
+ COLUMNS = %i[id]
+
+ milestone '16.6'
+
+ def up
+ backfill_conversion_of_integer_to_bigint(TABLE, COLUMNS, sub_batch_size: 100)
+ end
+
+ def down
+ revert_backfill_conversion_of_integer_to_bigint(TABLE, COLUMNS)
+ end
+end
diff --git a/db/post_migrate/20231027083355_remove_projects_duplicated_indexes.rb b/db/post_migrate/20231027083355_remove_projects_duplicated_indexes.rb
new file mode 100644
index 00000000000..7911a60df3f
--- /dev/null
+++ b/db/post_migrate/20231027083355_remove_projects_duplicated_indexes.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class RemoveProjectsDuplicatedIndexes < Gitlab::Database::Migration[2.2]
+ disable_ddl_transaction!
+
+ milestone '16.6'
+
+ INDEX_NAME = :index_on_projects_path
+ TABLE_NAME = :projects
+
+ def up
+ remove_concurrent_index_by_name TABLE_NAME, INDEX_NAME
+ end
+
+ def down
+ add_concurrent_index TABLE_NAME, :path, name: INDEX_NAME
+ end
+end
diff --git a/db/post_migrate/20231030051840_add_not_null_to_packages_tags_project_id.rb b/db/post_migrate/20231030051840_add_not_null_to_packages_tags_project_id.rb
new file mode 100644
index 00000000000..6541861cd45
--- /dev/null
+++ b/db/post_migrate/20231030051840_add_not_null_to_packages_tags_project_id.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+class AddNotNullToPackagesTagsProjectId < Gitlab::Database::Migration[2.2]
+ milestone '16.6'
+ disable_ddl_transaction!
+
+ def up
+ add_not_null_constraint :packages_tags, :project_id, validate: false
+ end
+
+ def down
+ remove_not_null_constraint :packages_tags, :project_id
+ end
+end
diff --git a/db/post_migrate/20231030071209_queue_backfill_packages_tags_project_id.rb b/db/post_migrate/20231030071209_queue_backfill_packages_tags_project_id.rb
new file mode 100644
index 00000000000..4984eb83263
--- /dev/null
+++ b/db/post_migrate/20231030071209_queue_backfill_packages_tags_project_id.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+class QueueBackfillPackagesTagsProjectId < Gitlab::Database::Migration[2.2]
+ milestone '16.6'
+ disable_ddl_transaction!
+ restrict_gitlab_migration gitlab_schema: :gitlab_main
+
+ MIGRATION = "BackfillPackagesTagsProjectId"
+ DELAY_INTERVAL = 2.minutes
+ BATCH_SIZE = 1000
+ SUB_BATCH_SIZE = 100
+
+ def up
+ queue_batched_background_migration(
+ MIGRATION,
+ :packages_tags,
+ :id,
+ job_interval: DELAY_INTERVAL,
+ queued_migration_version: '20231030071209',
+ batch_size: BATCH_SIZE,
+ sub_batch_size: SUB_BATCH_SIZE
+ )
+ end
+
+ def down
+ delete_batched_background_migration(MIGRATION, :packages_tags, :id, [])
+ end
+end
diff --git a/db/post_migrate/20231030094755_add_index_to_catalog_resources_on_state.rb b/db/post_migrate/20231030094755_add_index_to_catalog_resources_on_state.rb
new file mode 100644
index 00000000000..b7c6c8affdb
--- /dev/null
+++ b/db/post_migrate/20231030094755_add_index_to_catalog_resources_on_state.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddIndexToCatalogResourcesOnState < Gitlab::Database::Migration[2.2]
+ milestone '16.6'
+
+ disable_ddl_transaction!
+
+ INDEX_NAME = 'index_catalog_resources_on_state'
+
+ def up
+ add_concurrent_index :catalog_resources, :state, name: INDEX_NAME
+ end
+
+ def down
+ remove_concurrent_index_by_name :catalog_resources, INDEX_NAME
+ end
+end
diff --git a/db/post_migrate/20231030095419_remove_temp_index_to_packages_on_project_id_when_npm_and_not_pending_destruction.rb b/db/post_migrate/20231030095419_remove_temp_index_to_packages_on_project_id_when_npm_and_not_pending_destruction.rb
new file mode 100644
index 00000000000..cd3fccf5f4d
--- /dev/null
+++ b/db/post_migrate/20231030095419_remove_temp_index_to_packages_on_project_id_when_npm_and_not_pending_destruction.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+class RemoveTempIndexToPackagesOnProjectIdWhenNpmAndNotPendingDestruction < Gitlab::Database::Migration[2.2]
+ disable_ddl_transaction!
+ milestone '16.7'
+
+ INDEX_NAME = 'tmp_idx_packages_on_project_id_when_npm_not_pending_destruction'
+ NPM_PACKAGE_TYPE = 2
+ PENDING_DESTRUCTION_STATUS = 4
+
+ def up
+ remove_concurrent_index_by_name :packages_packages, INDEX_NAME
+ end
+
+ def down
+ add_concurrent_index(
+ :packages_packages,
+ :project_id,
+ name: INDEX_NAME,
+ where: "package_type = #{NPM_PACKAGE_TYPE} AND status <> #{PENDING_DESTRUCTION_STATUS}"
+ )
+ end
+end
diff --git a/db/post_migrate/20231030154117_insert_new_ultimate_trial_plan_into_plans.rb b/db/post_migrate/20231030154117_insert_new_ultimate_trial_plan_into_plans.rb
new file mode 100644
index 00000000000..af589f6337a
--- /dev/null
+++ b/db/post_migrate/20231030154117_insert_new_ultimate_trial_plan_into_plans.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+class InsertNewUltimateTrialPlanIntoPlans < Gitlab::Database::Migration[2.2]
+ milestone '16.6'
+
+ disable_ddl_transaction!
+
+ restrict_gitlab_migration gitlab_schema: :gitlab_main
+
+ def up
+ execute <<~SQL
+ INSERT INTO plans (name, title, created_at, updated_at)
+ VALUES ('ultimate_trial_paid_customer', 'Ultimate Trial for Paid Customer', current_timestamp, current_timestamp)
+ SQL
+ end
+
+ def down
+ # NOTE: We have a uniqueness constraint for the 'name' column in 'plans'
+ execute <<~SQL
+ DELETE FROM plans
+ WHERE name = 'ultimate_trial_paid_customer'
+ SQL
+ end
+end
diff --git a/db/post_migrate/20231031134320_init_conversion_for_p_ci_builds_for_self_host.rb b/db/post_migrate/20231031134320_init_conversion_for_p_ci_builds_for_self_host.rb
new file mode 100644
index 00000000000..d70ce00e9df
--- /dev/null
+++ b/db/post_migrate/20231031134320_init_conversion_for_p_ci_builds_for_self_host.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+class InitConversionForPCiBuildsForSelfHost < Gitlab::Database::Migration[2.2]
+ include ::Gitlab::Database::SchemaHelpers
+
+ milestone '16.6'
+ disable_ddl_transaction!
+
+ TABLE_NAME = :p_ci_builds
+ COLUMN_NAMES = %i[
+ auto_canceled_by_id
+ commit_id
+ erased_by_id
+ project_id
+ runner_id
+ trigger_request_id
+ upstream_pipeline_id
+ user_id
+ ]
+ TRIGGER_NAME = :trigger_10ee1357e825
+
+ def up
+ return if should_skip?
+
+ initialize_conversion_of_integer_to_bigint(TABLE_NAME, COLUMN_NAMES)
+ end
+
+ def down
+ return unless should_skip?
+
+ revert_initialize_conversion_of_integer_to_bigint(TABLE_NAME, COLUMN_NAMES)
+ end
+
+ private
+
+ def should_skip?
+ trigger_exists?(TABLE_NAME, TRIGGER_NAME)
+ end
+end
diff --git a/db/post_migrate/20231101130230_remove_in_product_marketing_emails_campaign_column.rb b/db/post_migrate/20231101130230_remove_in_product_marketing_emails_campaign_column.rb
new file mode 100644
index 00000000000..8916a1e9729
--- /dev/null
+++ b/db/post_migrate/20231101130230_remove_in_product_marketing_emails_campaign_column.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+class RemoveInProductMarketingEmailsCampaignColumn < Gitlab::Database::Migration[2.2]
+ disable_ddl_transaction!
+ milestone '16.6'
+
+ TARGET_TABLE = :in_product_marketing_emails
+ UNIQUE_INDEX_NAME = :index_in_product_marketing_emails_on_user_campaign
+ CONSTRAINT_NAME = :in_product_marketing_emails_track_and_series_or_campaign
+ TRACK_AND_SERIES_NOT_NULL_CONSTRAINT = 'track IS NOT NULL AND series IS NOT NULL AND campaign IS NULL'
+ CAMPAIGN_NOT_NULL_CONSTRAINT = 'track IS NULL AND series IS NULL AND campaign IS NOT NULL'
+
+ def up
+ with_lock_retries do
+ remove_column :in_product_marketing_emails, :campaign, if_exists: true
+ end
+ end
+
+ def down
+ with_lock_retries do
+ add_column :in_product_marketing_emails, :campaign, :text, if_not_exists: true
+ end
+
+ add_text_limit :in_product_marketing_emails, :campaign, 255
+
+ add_concurrent_index TARGET_TABLE, [:user_id, :campaign], unique: true, name: UNIQUE_INDEX_NAME
+ add_check_constraint TARGET_TABLE,
+ "(#{TRACK_AND_SERIES_NOT_NULL_CONSTRAINT}) OR (#{CAMPAIGN_NOT_NULL_CONSTRAINT})",
+ CONSTRAINT_NAME
+ end
+end
diff --git a/db/post_migrate/20231102083539_backfill_p_ci_builds_pipeline_id.rb b/db/post_migrate/20231102083539_backfill_p_ci_builds_pipeline_id.rb
new file mode 100644
index 00000000000..feada383fe4
--- /dev/null
+++ b/db/post_migrate/20231102083539_backfill_p_ci_builds_pipeline_id.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+class BackfillPCiBuildsPipelineId < Gitlab::Database::Migration[2.2]
+ restrict_gitlab_migration gitlab_schema: :gitlab_ci
+ milestone '16.6'
+
+ TABLE_NAME = :ci_builds
+ COLUMN_NAMES = %i[
+ auto_canceled_by_id
+ commit_id
+ erased_by_id
+ project_id
+ runner_id
+ trigger_request_id
+ upstream_pipeline_id
+ user_id
+ ]
+ SUB_BATCH_SIZE = 750
+ BATCH_SIZE = 75_000
+ PAUSE_MS = 0
+
+ def up
+ backfill_conversion_of_integer_to_bigint(
+ TABLE_NAME, COLUMN_NAMES,
+ sub_batch_size: SUB_BATCH_SIZE,
+ batch_size: BATCH_SIZE,
+ pause_ms: PAUSE_MS
+ )
+ end
+
+ def down
+ revert_backfill_conversion_of_integer_to_bigint(TABLE_NAME, COLUMN_NAMES)
+ end
+end
diff --git a/db/post_migrate/20231102142557_remove_zoekt_shard_null_constraint_from_indexed_namespaces.rb b/db/post_migrate/20231102142557_remove_zoekt_shard_null_constraint_from_indexed_namespaces.rb
new file mode 100644
index 00000000000..08e76c749c7
--- /dev/null
+++ b/db/post_migrate/20231102142557_remove_zoekt_shard_null_constraint_from_indexed_namespaces.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+class RemoveZoektShardNullConstraintFromIndexedNamespaces < Gitlab::Database::Migration[2.2]
+ milestone '16.6'
+ disable_ddl_transaction!
+
+ def up
+ change_column_null :zoekt_indexed_namespaces, :zoekt_shard_id, true
+ end
+
+ def down
+ change_column_null :zoekt_indexed_namespaces, :zoekt_shard_id, false
+ end
+end
diff --git a/db/post_migrate/20231103132849_add_state_index_for_snippet_repository_storage_move.rb b/db/post_migrate/20231103132849_add_state_index_for_snippet_repository_storage_move.rb
new file mode 100644
index 00000000000..3270a60acd8
--- /dev/null
+++ b/db/post_migrate/20231103132849_add_state_index_for_snippet_repository_storage_move.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddStateIndexForSnippetRepositoryStorageMove < Gitlab::Database::Migration[2.2]
+ disable_ddl_transaction!
+ milestone '16.6'
+
+ INDEX_NAME = 'index_snippet_repository_storage_moves_on_state'
+
+ def up
+ # State 2 = scheduled and 3 = started
+ add_concurrent_index :snippet_repository_storage_moves, :state, where: 'state IN (2, 3)', name: INDEX_NAME
+ end
+
+ def down
+ remove_concurrent_index_by_name :snippet_repository_storage_moves, INDEX_NAME
+ end
+end
diff --git a/db/post_migrate/20231105165706_drop_repositories_columns_from_geo_node_status_table.rb b/db/post_migrate/20231105165706_drop_repositories_columns_from_geo_node_status_table.rb
new file mode 100644
index 00000000000..69c9b54dc68
--- /dev/null
+++ b/db/post_migrate/20231105165706_drop_repositories_columns_from_geo_node_status_table.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+class DropRepositoriesColumnsFromGeoNodeStatusTable < Gitlab::Database::Migration[2.2]
+ enable_lock_retries!
+ milestone '16.6'
+
+ def up
+ [
+ :repositories_synced_count,
+ :repositories_failed_count,
+ :repositories_verified_count,
+ :repositories_verification_failed_count,
+ :repositories_checksummed_count,
+ :repositories_checksum_failed_count,
+ :repositories_checksum_mismatch_count,
+ :repositories_retrying_verification_count
+ ].each do |column_name|
+ remove_column :geo_node_statuses, column_name, if_exists: true
+ end
+ end
+
+ def down
+ change_table(:geo_node_statuses) do |t|
+ t.integer :repositories_synced_count
+ t.integer :repositories_failed_count
+ t.integer :repositories_verified_count
+ t.integer :repositories_verification_failed_count
+ t.integer :repositories_checksummed_count
+ t.integer :repositories_checksum_failed_count
+ t.integer :repositories_checksum_mismatch_count
+ t.integer :repositories_retrying_verification_count
+ end
+ end
+end
diff --git a/db/post_migrate/20231109183438_drop_merge_request_assignees_on_merge_request_id_index.rb b/db/post_migrate/20231109183438_drop_merge_request_assignees_on_merge_request_id_index.rb
new file mode 100644
index 00000000000..e1f96393031
--- /dev/null
+++ b/db/post_migrate/20231109183438_drop_merge_request_assignees_on_merge_request_id_index.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class DropMergeRequestAssigneesOnMergeRequestIdIndex < Gitlab::Database::Migration[2.2]
+ disable_ddl_transaction!
+
+ milestone '16.6'
+
+ INDEX_NAME = 'index_merge_request_assignees_on_merge_request_id'
+ TABLE_NAME = :merge_request_assignees
+
+ def up
+ # Duplicated index. This index is covered by +index_merge_request_assignees_on_merge_request_id_and_user_id+
+ remove_concurrent_index_by_name TABLE_NAME, INDEX_NAME
+ end
+
+ def down
+ add_concurrent_index TABLE_NAME, :merge_request_id, name: INDEX_NAME
+ end
+end
diff --git a/db/schema_migrations/20230926092914 b/db/schema_migrations/20230926092914
new file mode 100644
index 00000000000..391ade3aab0
--- /dev/null
+++ b/db/schema_migrations/20230926092914
@@ -0,0 +1 @@
+0018bc2180eeb632d75132b6d82e959e772ff1e7d8966310858e304d07d4ec34 \ No newline at end of file
diff --git a/db/schema_migrations/20230926092944 b/db/schema_migrations/20230926092944
new file mode 100644
index 00000000000..1fe6c64634a
--- /dev/null
+++ b/db/schema_migrations/20230926092944
@@ -0,0 +1 @@
+e2b4cdafd6147740ad43c286d90f7feec9f70d66a510d58a3cc3c33b0d703b49 \ No newline at end of file
diff --git a/db/schema_migrations/20230926093004 b/db/schema_migrations/20230926093004
new file mode 100644
index 00000000000..30407142a30
--- /dev/null
+++ b/db/schema_migrations/20230926093004
@@ -0,0 +1 @@
+d3d90178100e92cffe263715cdfc3c9ddcb47ce804f3ffd92d5bc4326de0244c \ No newline at end of file
diff --git a/db/schema_migrations/20230926093025 b/db/schema_migrations/20230926093025
new file mode 100644
index 00000000000..6a5ef092c69
--- /dev/null
+++ b/db/schema_migrations/20230926093025
@@ -0,0 +1 @@
+840bc159c277271b66f4348c31d912485c04b8ee1b15227c96dcc690f6b93311 \ No newline at end of file
diff --git a/db/schema_migrations/20230926093101 b/db/schema_migrations/20230926093101
new file mode 100644
index 00000000000..2ce67dab37f
--- /dev/null
+++ b/db/schema_migrations/20230926093101
@@ -0,0 +1 @@
+9a560649866367e556cf841e20f981b6c09fe03d1054f0db37cb510fbfbaef13 \ No newline at end of file
diff --git a/db/schema_migrations/20230926093144 b/db/schema_migrations/20230926093144
new file mode 100644
index 00000000000..b383692607f
--- /dev/null
+++ b/db/schema_migrations/20230926093144
@@ -0,0 +1 @@
+eba011de5a174a93e5159c765c093d3a6519111769a1ac09b2f996322cf3973e \ No newline at end of file
diff --git a/db/schema_migrations/20230926093211 b/db/schema_migrations/20230926093211
new file mode 100644
index 00000000000..9befd202129
--- /dev/null
+++ b/db/schema_migrations/20230926093211
@@ -0,0 +1 @@
+f9659a07b4c7b2d4508f1de231e759cf4e15e684ecaa4231ff6069b4ba203e20 \ No newline at end of file
diff --git a/db/schema_migrations/20230926093251 b/db/schema_migrations/20230926093251
new file mode 100644
index 00000000000..63bb045e437
--- /dev/null
+++ b/db/schema_migrations/20230926093251
@@ -0,0 +1 @@
+9df85930f78c6fa9e02252877d136aab3167a8ac1134cbd321c26f5958899f06 \ No newline at end of file
diff --git a/db/schema_migrations/20230926105440 b/db/schema_migrations/20230926105440
new file mode 100644
index 00000000000..957b7cbbbac
--- /dev/null
+++ b/db/schema_migrations/20230926105440
@@ -0,0 +1 @@
+0be5d3565d71dc9656fd90dbd404ea0314ff29f6da9ca9ef2d100bcc9515308b \ No newline at end of file
diff --git a/db/schema_migrations/20230926105931 b/db/schema_migrations/20230926105931
new file mode 100644
index 00000000000..f158665e529
--- /dev/null
+++ b/db/schema_migrations/20230926105931
@@ -0,0 +1 @@
+021dbeb0a8c5ebecfa647344b1e99dd1698ae3fb72a8857409551070b23f9f49 \ No newline at end of file
diff --git a/db/schema_migrations/20230927124202 b/db/schema_migrations/20230927124202
new file mode 100644
index 00000000000..a4089994e97
--- /dev/null
+++ b/db/schema_migrations/20230927124202
@@ -0,0 +1 @@
+652375e6b7318fe85b4b23eac3cce88618136341cee7721522adacbe52a52c66 \ No newline at end of file
diff --git a/db/schema_migrations/20230928145555 b/db/schema_migrations/20230928145555
new file mode 100644
index 00000000000..860364e57cc
--- /dev/null
+++ b/db/schema_migrations/20230928145555
@@ -0,0 +1 @@
+71e2f63bf9a327f62d21c2407b9ccebe779e0fd881266467f180cf285edc326f \ No newline at end of file
diff --git a/db/schema_migrations/20230928145637 b/db/schema_migrations/20230928145637
new file mode 100644
index 00000000000..47d4c9b0593
--- /dev/null
+++ b/db/schema_migrations/20230928145637
@@ -0,0 +1 @@
+1e9bf34cc708dd8637e4e636894fb9b7894c6d54832b3b42c88af17c4ed87532 \ No newline at end of file
diff --git a/db/schema_migrations/20230929155123 b/db/schema_migrations/20230929155123
new file mode 100644
index 00000000000..e2332c208aa
--- /dev/null
+++ b/db/schema_migrations/20230929155123
@@ -0,0 +1 @@
+91650c6c2dd7066036be3b276331361e7e514ec65a048aebabd43110e860e8ff \ No newline at end of file
diff --git a/db/schema_migrations/20231002162941 b/db/schema_migrations/20231002162941
new file mode 100644
index 00000000000..a6842b3f677
--- /dev/null
+++ b/db/schema_migrations/20231002162941
@@ -0,0 +1 @@
+ddf75326b9bb04275bf48e9a2eb6c15af7a9ca6c00864a636d5e179c5881b20b \ No newline at end of file
diff --git a/db/schema_migrations/20231003045342 b/db/schema_migrations/20231003045342
new file mode 100644
index 00000000000..cf16a592ca6
--- /dev/null
+++ b/db/schema_migrations/20231003045342
@@ -0,0 +1 @@
+a3a577992319a628fb7b1e8c492b1cb3ef1994d3e91e2af351c7b75a3900144d \ No newline at end of file
diff --git a/db/schema_migrations/20231005151816 b/db/schema_migrations/20231005151816
new file mode 100644
index 00000000000..93bf7686fab
--- /dev/null
+++ b/db/schema_migrations/20231005151816
@@ -0,0 +1 @@
+cc9ddab54a3e120e53e214c2d5cb689fda02810031c30da26d0fdc09921c1082 \ No newline at end of file
diff --git a/db/schema_migrations/20231009115713 b/db/schema_migrations/20231009115713
new file mode 100644
index 00000000000..fc5cf7122d1
--- /dev/null
+++ b/db/schema_migrations/20231009115713
@@ -0,0 +1 @@
+4c3129e96dd84ae715999edc7f53e3a001ebbfda28c79ef7108b74ad89d8afd4 \ No newline at end of file
diff --git a/db/schema_migrations/20231013204933 b/db/schema_migrations/20231013204933
new file mode 100644
index 00000000000..d0e92a8935d
--- /dev/null
+++ b/db/schema_migrations/20231013204933
@@ -0,0 +1 @@
+1358c375db88b2d318448cc748eb233b63781fbcdfdbe18c23275b69a7cd794b \ No newline at end of file
diff --git a/db/schema_migrations/20231016001000 b/db/schema_migrations/20231016001000
new file mode 100644
index 00000000000..54c77e2a080
--- /dev/null
+++ b/db/schema_migrations/20231016001000
@@ -0,0 +1 @@
+4b40cab24870578ece8648bb0c1e7e2ba4b118cf104ddb76eb0dd7599e51b320 \ No newline at end of file
diff --git a/db/schema_migrations/20231016173128 b/db/schema_migrations/20231016173128
new file mode 100644
index 00000000000..6aa7c3f955e
--- /dev/null
+++ b/db/schema_migrations/20231016173128
@@ -0,0 +1 @@
+b720259efa4eb9fe75a3352a64b9e14ae7b048240daf34c40a66cc5ef409dcc0 \ No newline at end of file
diff --git a/db/schema_migrations/20231016173129 b/db/schema_migrations/20231016173129
new file mode 100644
index 00000000000..acbf77968e9
--- /dev/null
+++ b/db/schema_migrations/20231016173129
@@ -0,0 +1 @@
+f886678df9907d2bf60f98c0184c91604069cd613b541a0476e30789f327df15 \ No newline at end of file
diff --git a/db/schema_migrations/20231016194926 b/db/schema_migrations/20231016194926
new file mode 100644
index 00000000000..4aa858f00f0
--- /dev/null
+++ b/db/schema_migrations/20231016194926
@@ -0,0 +1 @@
+0f2e4b7fc2658b5063dbe8dea6c881fb59a9d99ed53332ae1bdb5578343c3e89 \ No newline at end of file
diff --git a/db/schema_migrations/20231016194927 b/db/schema_migrations/20231016194927
new file mode 100644
index 00000000000..6d8d7d11191
--- /dev/null
+++ b/db/schema_migrations/20231016194927
@@ -0,0 +1 @@
+44474805c7858d07d093650e43f3313746976e4c523b408d029e32829a5b7301 \ No newline at end of file
diff --git a/db/schema_migrations/20231016194942 b/db/schema_migrations/20231016194942
new file mode 100644
index 00000000000..05862999a37
--- /dev/null
+++ b/db/schema_migrations/20231016194942
@@ -0,0 +1 @@
+43de9dd5e63a80c51aa21e42b7f41d03b6d36143afa45cf45ead6ee0cc8152cc \ No newline at end of file
diff --git a/db/schema_migrations/20231016194943 b/db/schema_migrations/20231016194943
new file mode 100644
index 00000000000..df18251008c
--- /dev/null
+++ b/db/schema_migrations/20231016194943
@@ -0,0 +1 @@
+b17f7eaff454fab3e46e438d81fdebab14776322af261e0f2a12ceb69a5623a8 \ No newline at end of file
diff --git a/db/schema_migrations/20231017095738 b/db/schema_migrations/20231017095738
new file mode 100644
index 00000000000..20feb63b199
--- /dev/null
+++ b/db/schema_migrations/20231017095738
@@ -0,0 +1 @@
+730b861c660b96556969054402a7776f622d42ed98055b0f7099c940ecf03c32 \ No newline at end of file
diff --git a/db/schema_migrations/20231017134349 b/db/schema_migrations/20231017134349
new file mode 100644
index 00000000000..ef53b4c0df9
--- /dev/null
+++ b/db/schema_migrations/20231017134349
@@ -0,0 +1 @@
+9bec84c51111ba6d9fb685d043bdc002eed3d5089242b6f1ae6bb360b0b832ee \ No newline at end of file
diff --git a/db/schema_migrations/20231017135207 b/db/schema_migrations/20231017135207
new file mode 100644
index 00000000000..f27b16c2caa
--- /dev/null
+++ b/db/schema_migrations/20231017135207
@@ -0,0 +1 @@
+e32402af4e39d6e09e274c3b0bb4588f6c2f1a7bb3dce29b5ce82beda909e86b \ No newline at end of file
diff --git a/db/schema_migrations/20231017154804 b/db/schema_migrations/20231017154804
new file mode 100644
index 00000000000..61386d6ebf9
--- /dev/null
+++ b/db/schema_migrations/20231017154804
@@ -0,0 +1 @@
+999c4fefec34812883cb458fe70b89247e3808e53441739ccfec5862b687977a \ No newline at end of file
diff --git a/db/schema_migrations/20231017181403 b/db/schema_migrations/20231017181403
new file mode 100644
index 00000000000..e8e1d282897
--- /dev/null
+++ b/db/schema_migrations/20231017181403
@@ -0,0 +1 @@
+6d34316fdbe5a2c7e825e7abd9a817313c36ff7d6ef29f1bbee40f805e279ee3 \ No newline at end of file
diff --git a/db/schema_migrations/20231018083247 b/db/schema_migrations/20231018083247
new file mode 100644
index 00000000000..1807921c388
--- /dev/null
+++ b/db/schema_migrations/20231018083247
@@ -0,0 +1 @@
+ccf25454919c35e8275f48aca973fdd263e57d643640878aa776a7760b38e851 \ No newline at end of file
diff --git a/db/schema_migrations/20231018093625 b/db/schema_migrations/20231018093625
new file mode 100644
index 00000000000..3d571c1667d
--- /dev/null
+++ b/db/schema_migrations/20231018093625
@@ -0,0 +1 @@
+655ef95f055a139776f0d1873415bff48d39cee243ffb467200b3b3938b3968a \ No newline at end of file
diff --git a/db/schema_migrations/20231018105749 b/db/schema_migrations/20231018105749
new file mode 100644
index 00000000000..fb443c9eb63
--- /dev/null
+++ b/db/schema_migrations/20231018105749
@@ -0,0 +1 @@
+4289e51e278d842ec0a7344256ed5c3de2b0a3355de6437b079b75e3a607b7a8 \ No newline at end of file
diff --git a/db/schema_migrations/20231018140154 b/db/schema_migrations/20231018140154
new file mode 100644
index 00000000000..7c9cb7eb276
--- /dev/null
+++ b/db/schema_migrations/20231018140154
@@ -0,0 +1 @@
+7a80a42db1c6d44a034ea7f2cb27f919c099df2ca3e30e59d2b5b7cee3ebc610 \ No newline at end of file
diff --git a/db/schema_migrations/20231018152419 b/db/schema_migrations/20231018152419
new file mode 100644
index 00000000000..db144b1f48e
--- /dev/null
+++ b/db/schema_migrations/20231018152419
@@ -0,0 +1 @@
+1342acaf87fdcd643d7be37ade7a789d8fd57026fab07cc7b48bb5c47b4a8d00 \ No newline at end of file
diff --git a/db/schema_migrations/20231019003052 b/db/schema_migrations/20231019003052
new file mode 100644
index 00000000000..c12f6d1a9a5
--- /dev/null
+++ b/db/schema_migrations/20231019003052
@@ -0,0 +1 @@
+baa0d627f26ff5d2cb773f724bc08eca03b80f59553f6706388429194844b5dc \ No newline at end of file
diff --git a/db/schema_migrations/20231019084731 b/db/schema_migrations/20231019084731
new file mode 100644
index 00000000000..5c7275172d6
--- /dev/null
+++ b/db/schema_migrations/20231019084731
@@ -0,0 +1 @@
+dd4575590153280219bff3b7f37047c67c1a16219c2b6ba18322cb7e295665f7 \ No newline at end of file
diff --git a/db/schema_migrations/20231019104211 b/db/schema_migrations/20231019104211
new file mode 100644
index 00000000000..cce6cc892da
--- /dev/null
+++ b/db/schema_migrations/20231019104211
@@ -0,0 +1 @@
+e2ee8bcb49b470bbea1874f6a63c9b7a2fd67ef4223cf5d358de1fca4e3f36be \ No newline at end of file
diff --git a/db/schema_migrations/20231019122855 b/db/schema_migrations/20231019122855
new file mode 100644
index 00000000000..30a6774ec1b
--- /dev/null
+++ b/db/schema_migrations/20231019122855
@@ -0,0 +1 @@
+17039c530b6f85b46b014aa0c0f3d0e9340419a89b78581ca9ad4865d567929d \ No newline at end of file
diff --git a/db/schema_migrations/20231019145202 b/db/schema_migrations/20231019145202
new file mode 100644
index 00000000000..726093ee4dc
--- /dev/null
+++ b/db/schema_migrations/20231019145202
@@ -0,0 +1 @@
+c6a94dda004fccc8b3c8b5f59c7730a9243fe5d33a287997dae98748f3ad3bb4 \ No newline at end of file
diff --git a/db/schema_migrations/20231019180421 b/db/schema_migrations/20231019180421
new file mode 100644
index 00000000000..f7cd8f99b10
--- /dev/null
+++ b/db/schema_migrations/20231019180421
@@ -0,0 +1 @@
+9098a39552648a1a2b6439bc26b3e987fc604c0b3bd149d08049b376a09f5ebb \ No newline at end of file
diff --git a/db/schema_migrations/20231019223224 b/db/schema_migrations/20231019223224
new file mode 100644
index 00000000000..0a35a48222b
--- /dev/null
+++ b/db/schema_migrations/20231019223224
@@ -0,0 +1 @@
+d2bd2f99340f7653cec908c4c41c0326d7bf4765fd4e4287ae914ed3025cd690 \ No newline at end of file
diff --git a/db/schema_migrations/20231020020732 b/db/schema_migrations/20231020020732
new file mode 100644
index 00000000000..d53dd4c91c7
--- /dev/null
+++ b/db/schema_migrations/20231020020732
@@ -0,0 +1 @@
+6f28ddf4aa999419cd6a1cf05027161f002ffc28a299ea6f7281b6a7672ee180 \ No newline at end of file
diff --git a/db/schema_migrations/20231020074227 b/db/schema_migrations/20231020074227
new file mode 100644
index 00000000000..48185f79937
--- /dev/null
+++ b/db/schema_migrations/20231020074227
@@ -0,0 +1 @@
+0dcf5a04af59563c58a658cb3c99619d2b671f3e78960859f3ed6053a9e96994 \ No newline at end of file
diff --git a/db/schema_migrations/20231020082425 b/db/schema_migrations/20231020082425
new file mode 100644
index 00000000000..7e72d043d05
--- /dev/null
+++ b/db/schema_migrations/20231020082425
@@ -0,0 +1 @@
+9d808ab1739e61d1c80f8b0563191ce31e7766fdb24c993791be4850f7164041 \ No newline at end of file
diff --git a/db/schema_migrations/20231020095624 b/db/schema_migrations/20231020095624
new file mode 100644
index 00000000000..b5c91fb6f9b
--- /dev/null
+++ b/db/schema_migrations/20231020095624
@@ -0,0 +1 @@
+8a69cad1fba51cbec7e296d985dbecae214fcc98edb32c9d3da78070fbf7b47d \ No newline at end of file
diff --git a/db/schema_migrations/20231020112541 b/db/schema_migrations/20231020112541
new file mode 100644
index 00000000000..f385bb06bd6
--- /dev/null
+++ b/db/schema_migrations/20231020112541
@@ -0,0 +1 @@
+16a0b32619e6b28c49fc2e9e609970ac61582c295c2ed281c531e407de2af216 \ No newline at end of file
diff --git a/db/schema_migrations/20231020150211 b/db/schema_migrations/20231020150211
new file mode 100644
index 00000000000..47a22d6c9f9
--- /dev/null
+++ b/db/schema_migrations/20231020150211
@@ -0,0 +1 @@
+0ef5c4756854bead328fe61e51a78a86076870844edb7a8dba0941788736a12f \ No newline at end of file
diff --git a/db/schema_migrations/20231020181652 b/db/schema_migrations/20231020181652
new file mode 100644
index 00000000000..3b0faf6040f
--- /dev/null
+++ b/db/schema_migrations/20231020181652
@@ -0,0 +1 @@
+ec632fbf61f89a45cb4f011117af10c26d847f822c2edcce637cbf18cb6a2b67 \ No newline at end of file
diff --git a/db/schema_migrations/20231023073841 b/db/schema_migrations/20231023073841
new file mode 100644
index 00000000000..64b0c1cf4d0
--- /dev/null
+++ b/db/schema_migrations/20231023073841
@@ -0,0 +1 @@
+007e2a09c9d8519ea7bb4868ce20b1a57b14a7f694bd477796584fcafc7f3c58 \ No newline at end of file
diff --git a/db/schema_migrations/20231023083349 b/db/schema_migrations/20231023083349
new file mode 100644
index 00000000000..d3d7e4e45fc
--- /dev/null
+++ b/db/schema_migrations/20231023083349
@@ -0,0 +1 @@
+1b0cd52ccf99a477f39168cdb6b719d5b64f6110a7fd9df0a6f200c6ff9c0237 \ No newline at end of file
diff --git a/db/schema_migrations/20231023113908 b/db/schema_migrations/20231023113908
new file mode 100644
index 00000000000..bf05644a5ab
--- /dev/null
+++ b/db/schema_migrations/20231023113908
@@ -0,0 +1 @@
+63d7eb49469273cef193dd7c80f1bac042893f5da544f5066d00175f9e026d48 \ No newline at end of file
diff --git a/db/schema_migrations/20231023114006 b/db/schema_migrations/20231023114006
new file mode 100644
index 00000000000..bc17ae9b852
--- /dev/null
+++ b/db/schema_migrations/20231023114006
@@ -0,0 +1 @@
+030809f5519906dbdcdf3b8fd35a7181ca2c9ec1bdca745997aa14f24ee6ac6d \ No newline at end of file
diff --git a/db/schema_migrations/20231023114551 b/db/schema_migrations/20231023114551
new file mode 100644
index 00000000000..a53b51b53bd
--- /dev/null
+++ b/db/schema_migrations/20231023114551
@@ -0,0 +1 @@
+df2937c8e70fde85677ef150ed6c2445d06b8a2f7113f58a8908a81ece449d75 \ No newline at end of file
diff --git a/db/schema_migrations/20231023121955 b/db/schema_migrations/20231023121955
new file mode 100644
index 00000000000..3c559afe5ea
--- /dev/null
+++ b/db/schema_migrations/20231023121955
@@ -0,0 +1 @@
+912289edbed417e2e552e8d0c6d44d37b1066531d3dd28a6960fce47a8fcbe52 \ No newline at end of file
diff --git a/db/schema_migrations/20231023122508 b/db/schema_migrations/20231023122508
new file mode 100644
index 00000000000..2ef64fd5b26
--- /dev/null
+++ b/db/schema_migrations/20231023122508
@@ -0,0 +1 @@
+b4850d28d0000d9dd7f81df26a5b9f4b5a38c2f0d33a48037ab5c097789345d8 \ No newline at end of file
diff --git a/db/schema_migrations/20231023164908 b/db/schema_migrations/20231023164908
new file mode 100644
index 00000000000..f94a5e457bf
--- /dev/null
+++ b/db/schema_migrations/20231023164908
@@ -0,0 +1 @@
+4d742e6f54307710370453fdd72313c0a0f6928bdf2e4812bc5c16ec1043dd3f \ No newline at end of file
diff --git a/db/schema_migrations/20231024015915 b/db/schema_migrations/20231024015915
new file mode 100644
index 00000000000..7f6eac81c71
--- /dev/null
+++ b/db/schema_migrations/20231024015915
@@ -0,0 +1 @@
+8ad5065584f72084ee929e479725593330d0d13542dc4939476d62f831c6f2e8 \ No newline at end of file
diff --git a/db/schema_migrations/20231024025457 b/db/schema_migrations/20231024025457
new file mode 100644
index 00000000000..81dc3359183
--- /dev/null
+++ b/db/schema_migrations/20231024025457
@@ -0,0 +1 @@
+1bd136e7d4fb7c34030cea6c915a2eeae619ea5ae1a701cb4d5d4bb069df7113 \ No newline at end of file
diff --git a/db/schema_migrations/20231024025533 b/db/schema_migrations/20231024025533
new file mode 100644
index 00000000000..3adea905aa3
--- /dev/null
+++ b/db/schema_migrations/20231024025533
@@ -0,0 +1 @@
+bf03b09c6247d2f5c3543f4046b48763dfc7e6fb2cdaedc52d8cfc8777f70e71 \ No newline at end of file
diff --git a/db/schema_migrations/20231024025629 b/db/schema_migrations/20231024025629
new file mode 100644
index 00000000000..30d63a84636
--- /dev/null
+++ b/db/schema_migrations/20231024025629
@@ -0,0 +1 @@
+4c90d6df75ddb93f8fd8fb89131256fc97bac990f024576fa57a3a8c6b60fee9 \ No newline at end of file
diff --git a/db/schema_migrations/20231024080150 b/db/schema_migrations/20231024080150
new file mode 100644
index 00000000000..582b17c4325
--- /dev/null
+++ b/db/schema_migrations/20231024080150
@@ -0,0 +1 @@
+13b70e77df1309a1b0d93239b6deff9d34fd5e67650baa0b7495528c6521283d \ No newline at end of file
diff --git a/db/schema_migrations/20231024123444 b/db/schema_migrations/20231024123444
new file mode 100644
index 00000000000..578f1cef1bd
--- /dev/null
+++ b/db/schema_migrations/20231024123444
@@ -0,0 +1 @@
+db84d40c9afd9121aa24617167fa82b86cabc98bf274e61057eef02e1fafd7c3 \ No newline at end of file
diff --git a/db/schema_migrations/20231024124856 b/db/schema_migrations/20231024124856
new file mode 100644
index 00000000000..5305af27bb1
--- /dev/null
+++ b/db/schema_migrations/20231024124856
@@ -0,0 +1 @@
+add7ce4f9fb56221512227d5aa3697245d537cd5c975978b7dc6dab992890e4e \ No newline at end of file
diff --git a/db/schema_migrations/20231024125551 b/db/schema_migrations/20231024125551
new file mode 100644
index 00000000000..05c647f3abd
--- /dev/null
+++ b/db/schema_migrations/20231024125551
@@ -0,0 +1 @@
+08275dacbe6b1bd44cc67834fc77d6615e43ebd1b9a85edc9e7237cbecc57315 \ No newline at end of file
diff --git a/db/schema_migrations/20231024133234 b/db/schema_migrations/20231024133234
new file mode 100644
index 00000000000..fb536f574d3
--- /dev/null
+++ b/db/schema_migrations/20231024133234
@@ -0,0 +1 @@
+0a92e23317e4fc12b9de9d15c0d3895afe211b543a0449834b9459616152680a \ No newline at end of file
diff --git a/db/schema_migrations/20231024142236 b/db/schema_migrations/20231024142236
new file mode 100644
index 00000000000..283bed9db8d
--- /dev/null
+++ b/db/schema_migrations/20231024142236
@@ -0,0 +1 @@
+6103bd075183ce4196dee2b140cb960f075cc7d3f4fc4f370bb6217c3ff1e758 \ No newline at end of file
diff --git a/db/schema_migrations/20231024143457 b/db/schema_migrations/20231024143457
new file mode 100644
index 00000000000..a3033f54954
--- /dev/null
+++ b/db/schema_migrations/20231024143457
@@ -0,0 +1 @@
+9627d5af229e51bee8a5a8c47beedf5bd0b3b2ce89f4cc209fe96089e662c749 \ No newline at end of file
diff --git a/db/schema_migrations/20231024151916 b/db/schema_migrations/20231024151916
new file mode 100644
index 00000000000..1333c1f3b82
--- /dev/null
+++ b/db/schema_migrations/20231024151916
@@ -0,0 +1 @@
+b316a07e7f307aea53dd9cac257c75ac58ff2b4deeace4e454ec933bd4039761 \ No newline at end of file
diff --git a/db/schema_migrations/20231024173744 b/db/schema_migrations/20231024173744
new file mode 100644
index 00000000000..b262f2a2ebb
--- /dev/null
+++ b/db/schema_migrations/20231024173744
@@ -0,0 +1 @@
+fd51e236973eaf1d4a2719eaa34dbd7955c2d73e37adf244472c8c69fc486fdf \ No newline at end of file
diff --git a/db/schema_migrations/20231024212214 b/db/schema_migrations/20231024212214
new file mode 100644
index 00000000000..d3ad27bd4dd
--- /dev/null
+++ b/db/schema_migrations/20231024212214
@@ -0,0 +1 @@
+c5884c327b3be31122ca36302f8fbd36666ddee07229480884c8c64af825c03f \ No newline at end of file
diff --git a/db/schema_migrations/20231025025733 b/db/schema_migrations/20231025025733
new file mode 100644
index 00000000000..a488c5206e1
--- /dev/null
+++ b/db/schema_migrations/20231025025733
@@ -0,0 +1 @@
+c0129899dcea5f304661b49665a371de86dbff9df88afbb3fdbb348a411c1dd8 \ No newline at end of file
diff --git a/db/schema_migrations/20231025031337 b/db/schema_migrations/20231025031337
new file mode 100644
index 00000000000..8d28d710397
--- /dev/null
+++ b/db/schema_migrations/20231025031337
@@ -0,0 +1 @@
+7dc72ca807126bb992c22879a6d989f282e442ff2c6e15b046e6f3d0f464237f \ No newline at end of file
diff --git a/db/schema_migrations/20231025031539 b/db/schema_migrations/20231025031539
new file mode 100644
index 00000000000..4332cd2b867
--- /dev/null
+++ b/db/schema_migrations/20231025031539
@@ -0,0 +1 @@
+4de438a35ae2cbeee4cec03961cf7b5dddfcce2454a1e3ce08985e28b7065a0d \ No newline at end of file
diff --git a/db/schema_migrations/20231025123238 b/db/schema_migrations/20231025123238
new file mode 100644
index 00000000000..e93a7a4d3fb
--- /dev/null
+++ b/db/schema_migrations/20231025123238
@@ -0,0 +1 @@
+8a34911b504b3752071aa2f6f1eb8dbc6b91540cceb69881c12c89adb48dcc78 \ No newline at end of file
diff --git a/db/schema_migrations/20231026050554 b/db/schema_migrations/20231026050554
new file mode 100644
index 00000000000..d99dc675ab6
--- /dev/null
+++ b/db/schema_migrations/20231026050554
@@ -0,0 +1 @@
+e71f80b77121722c75125e59ec2e9c3df323b34a107304447948bed05804224c \ No newline at end of file
diff --git a/db/schema_migrations/20231026103346 b/db/schema_migrations/20231026103346
new file mode 100644
index 00000000000..53f5520bcc4
--- /dev/null
+++ b/db/schema_migrations/20231026103346
@@ -0,0 +1 @@
+dc0065c2caffdf5bbf79c1e94f8bdb6d415a836cc575109d25df8217423be0e1 \ No newline at end of file
diff --git a/db/schema_migrations/20231027013210 b/db/schema_migrations/20231027013210
new file mode 100644
index 00000000000..fdf26416bdc
--- /dev/null
+++ b/db/schema_migrations/20231027013210
@@ -0,0 +1 @@
+3ee898fd7593c7a300bdfc0dc6f041e2fb65f3600b85788521175449d250590f \ No newline at end of file
diff --git a/db/schema_migrations/20231027052949 b/db/schema_migrations/20231027052949
new file mode 100644
index 00000000000..dff1dd0f197
--- /dev/null
+++ b/db/schema_migrations/20231027052949
@@ -0,0 +1 @@
+399e9a19e9436dc077e9b107daf3397a6be2efe574981265758c082deb2c19ce \ No newline at end of file
diff --git a/db/schema_migrations/20231027060443 b/db/schema_migrations/20231027060443
new file mode 100644
index 00000000000..b00276b1849
--- /dev/null
+++ b/db/schema_migrations/20231027060443
@@ -0,0 +1 @@
+f3ce119c5ded9fae2f94168455379eb3a8d7d7bc1eff3e555a2a77011a6309fb \ No newline at end of file
diff --git a/db/schema_migrations/20231027064352 b/db/schema_migrations/20231027064352
new file mode 100644
index 00000000000..2a770ac96db
--- /dev/null
+++ b/db/schema_migrations/20231027064352
@@ -0,0 +1 @@
+2418c94e1e40f2765252f5c69dae7def898ed3c329fa5fc05d3b51ed812bb7c7 \ No newline at end of file
diff --git a/db/schema_migrations/20231027065205 b/db/schema_migrations/20231027065205
new file mode 100644
index 00000000000..9c013a95bba
--- /dev/null
+++ b/db/schema_migrations/20231027065205
@@ -0,0 +1 @@
+b8ecc7e8ead4cddc7dad712c46fdff0d559da1697bd5d16c1130f2c71272b890 \ No newline at end of file
diff --git a/db/schema_migrations/20231027083355 b/db/schema_migrations/20231027083355
new file mode 100644
index 00000000000..2ceb5337067
--- /dev/null
+++ b/db/schema_migrations/20231027083355
@@ -0,0 +1 @@
+ce4863f02f807498da9c3cf7b49d85a2e5a296903fe0673bfa6f40d50c8a51b5 \ No newline at end of file
diff --git a/db/schema_migrations/20231027084327 b/db/schema_migrations/20231027084327
new file mode 100644
index 00000000000..9b4a0baee6e
--- /dev/null
+++ b/db/schema_migrations/20231027084327
@@ -0,0 +1 @@
+38dcfa54fa7da63c1fbceb842e277b27bd90b1b0ef31fc82db8f80e6ba286047 \ No newline at end of file
diff --git a/db/schema_migrations/20231030051837 b/db/schema_migrations/20231030051837
new file mode 100644
index 00000000000..9c9bb912eba
--- /dev/null
+++ b/db/schema_migrations/20231030051837
@@ -0,0 +1 @@
+ebd61f5c5f74ce00b86aacc996e46c2971deac18d2a6e31bf531576fe3af090f \ No newline at end of file
diff --git a/db/schema_migrations/20231030051838 b/db/schema_migrations/20231030051838
new file mode 100644
index 00000000000..3c15f764cce
--- /dev/null
+++ b/db/schema_migrations/20231030051838
@@ -0,0 +1 @@
+09129fdab92e39c57f0db400b179eecc1b498249db7b928014eabdcb9af30052 \ No newline at end of file
diff --git a/db/schema_migrations/20231030051839 b/db/schema_migrations/20231030051839
new file mode 100644
index 00000000000..144c443f5cc
--- /dev/null
+++ b/db/schema_migrations/20231030051839
@@ -0,0 +1 @@
+90e0409db7a30b4b531cb0dbbccff7d06c2196e6afdacb88fc5d1ecdc00fcc2f \ No newline at end of file
diff --git a/db/schema_migrations/20231030051840 b/db/schema_migrations/20231030051840
new file mode 100644
index 00000000000..4926ff15f09
--- /dev/null
+++ b/db/schema_migrations/20231030051840
@@ -0,0 +1 @@
+6fc7bb7b27a5885890dac96738190bc4157cd8c3b5afd9b47809e0487a8a7b4b \ No newline at end of file
diff --git a/db/schema_migrations/20231030071209 b/db/schema_migrations/20231030071209
new file mode 100644
index 00000000000..5f7b172c22c
--- /dev/null
+++ b/db/schema_migrations/20231030071209
@@ -0,0 +1 @@
+c3614cda6677dc3afdbb69a95111f39bd4719e1fef683358855a6ff04bebfdac \ No newline at end of file
diff --git a/db/schema_migrations/20231030094755 b/db/schema_migrations/20231030094755
new file mode 100644
index 00000000000..5a18105655b
--- /dev/null
+++ b/db/schema_migrations/20231030094755
@@ -0,0 +1 @@
+981110baa181be00e7195b9f6e9773d14683b00a0de851b23b261561e7aaae27 \ No newline at end of file
diff --git a/db/schema_migrations/20231030095419 b/db/schema_migrations/20231030095419
new file mode 100644
index 00000000000..039ad039283
--- /dev/null
+++ b/db/schema_migrations/20231030095419
@@ -0,0 +1 @@
+99b845e37c091107a0540a182e4376bb0c0b0b2c46def577a96cbcf1971a8cd4 \ No newline at end of file
diff --git a/db/schema_migrations/20231030154117 b/db/schema_migrations/20231030154117
new file mode 100644
index 00000000000..5380cfa5252
--- /dev/null
+++ b/db/schema_migrations/20231030154117
@@ -0,0 +1 @@
+07c4a447b3888046333b0b8fa237411783fc031ea9943520f716ea0c00ed964f \ No newline at end of file
diff --git a/db/schema_migrations/20231030205639 b/db/schema_migrations/20231030205639
new file mode 100644
index 00000000000..4abedebbd44
--- /dev/null
+++ b/db/schema_migrations/20231030205639
@@ -0,0 +1 @@
+873fab24af680c9e33bedfe574f20a5a2242732b922bb4bd2f01d13180601de3 \ No newline at end of file
diff --git a/db/schema_migrations/20231030205756 b/db/schema_migrations/20231030205756
new file mode 100644
index 00000000000..3923ee6dbd0
--- /dev/null
+++ b/db/schema_migrations/20231030205756
@@ -0,0 +1 @@
+fd45299e8376db582461fa4b714b3718c4f589bc087d73465ac51d04437e07c3 \ No newline at end of file
diff --git a/db/schema_migrations/20231031134320 b/db/schema_migrations/20231031134320
new file mode 100644
index 00000000000..2c27b20bbc6
--- /dev/null
+++ b/db/schema_migrations/20231031134320
@@ -0,0 +1 @@
+235c903dcd43c1bf6a3e11154ff0bde3f1a7a3fc5d9129dce8bfb770fd36b75b \ No newline at end of file
diff --git a/db/schema_migrations/20231031141439 b/db/schema_migrations/20231031141439
new file mode 100644
index 00000000000..bbdae989385
--- /dev/null
+++ b/db/schema_migrations/20231031141439
@@ -0,0 +1 @@
+568e7a227911f23e4285e1bbcc9dd516ecbd2013501a2add13e99e98880effc8 \ No newline at end of file
diff --git a/db/schema_migrations/20231031200433 b/db/schema_migrations/20231031200433
new file mode 100644
index 00000000000..1093e9edabb
--- /dev/null
+++ b/db/schema_migrations/20231031200433
@@ -0,0 +1 @@
+409134f3d8980c647bd9ecd73f6f56729c7cf6f83059b3fd32d5665c36ab1a92 \ No newline at end of file
diff --git a/db/schema_migrations/20231031200645 b/db/schema_migrations/20231031200645
new file mode 100644
index 00000000000..4d29fbfd996
--- /dev/null
+++ b/db/schema_migrations/20231031200645
@@ -0,0 +1 @@
+09f38031c5ae4a88eae80d24285163b45ee6cbc96903a4f54dc0552cb11d12a4 \ No newline at end of file
diff --git a/db/schema_migrations/20231101130230 b/db/schema_migrations/20231101130230
new file mode 100644
index 00000000000..8fa382d7033
--- /dev/null
+++ b/db/schema_migrations/20231101130230
@@ -0,0 +1 @@
+c8dbdeb4ffcb7f5dc1c719a09a1f6c41188f584c80331a4482542a873d3ad12d \ No newline at end of file
diff --git a/db/schema_migrations/20231102083539 b/db/schema_migrations/20231102083539
new file mode 100644
index 00000000000..489269151bb
--- /dev/null
+++ b/db/schema_migrations/20231102083539
@@ -0,0 +1 @@
+1ac3716a5e014abe1828d648bd9f1014d770b40c4006944f341739728026fcd4 \ No newline at end of file
diff --git a/db/schema_migrations/20231102142553 b/db/schema_migrations/20231102142553
new file mode 100644
index 00000000000..ea7ab1a82ff
--- /dev/null
+++ b/db/schema_migrations/20231102142553
@@ -0,0 +1 @@
+268ae2897297990a3ee94df152cc2ca1188073841d5da81c276d62471c6a5822 \ No newline at end of file
diff --git a/db/schema_migrations/20231102142554 b/db/schema_migrations/20231102142554
new file mode 100644
index 00000000000..80d70adf962
--- /dev/null
+++ b/db/schema_migrations/20231102142554
@@ -0,0 +1 @@
+af9d1bebd6e3736735fcbb9bb08858b25e3c1c5d6479c43d3f996f63a2f9660d \ No newline at end of file
diff --git a/db/schema_migrations/20231102142555 b/db/schema_migrations/20231102142555
new file mode 100644
index 00000000000..81c33fa8d36
--- /dev/null
+++ b/db/schema_migrations/20231102142555
@@ -0,0 +1 @@
+3b683096e72455356d1ce1f115260b65ac15c5365c3c223abf3a9abed2d89b40 \ No newline at end of file
diff --git a/db/schema_migrations/20231102142557 b/db/schema_migrations/20231102142557
new file mode 100644
index 00000000000..9510ecb5bd7
--- /dev/null
+++ b/db/schema_migrations/20231102142557
@@ -0,0 +1 @@
+498535936c4d4e306ab6efa930dc77ef0684f07146c54c5e42136cfcbc45fa55 \ No newline at end of file
diff --git a/db/schema_migrations/20231102142565 b/db/schema_migrations/20231102142565
new file mode 100644
index 00000000000..f12256acc25
--- /dev/null
+++ b/db/schema_migrations/20231102142565
@@ -0,0 +1 @@
+61f9b94b89cd0edac11e56a67b99ded13b7e5761a91be08b06c24ddf9eb3ca02 \ No newline at end of file
diff --git a/db/schema_migrations/20231103132849 b/db/schema_migrations/20231103132849
new file mode 100644
index 00000000000..4ac9f938f32
--- /dev/null
+++ b/db/schema_migrations/20231103132849
@@ -0,0 +1 @@
+42c514c8e4addaa7836538741d5080c16e4db330507a037abfb149907c37a6a7 \ No newline at end of file
diff --git a/db/schema_migrations/20231103162825 b/db/schema_migrations/20231103162825
new file mode 100644
index 00000000000..6bb33354de4
--- /dev/null
+++ b/db/schema_migrations/20231103162825
@@ -0,0 +1 @@
+a6b5c59b0035f536185b94157950a2900754e07bcc2c6ea980cd9213f35b899c \ No newline at end of file
diff --git a/db/schema_migrations/20231103195309 b/db/schema_migrations/20231103195309
new file mode 100644
index 00000000000..cb5e21db50d
--- /dev/null
+++ b/db/schema_migrations/20231103195309
@@ -0,0 +1 @@
+d237c0aa5d44d58ee0a32246f3c2911d7515d18cff6b177709a95c3d064d000d \ No newline at end of file
diff --git a/db/schema_migrations/20231103223224 b/db/schema_migrations/20231103223224
new file mode 100644
index 00000000000..0ea5fea923e
--- /dev/null
+++ b/db/schema_migrations/20231103223224
@@ -0,0 +1 @@
+0c33abeb9990c6d913000de5c15c431fea7e8e68dbcd4fc1c16e42e679a9e28d \ No newline at end of file
diff --git a/db/schema_migrations/20231105165706 b/db/schema_migrations/20231105165706
new file mode 100644
index 00000000000..6b70de3ab1a
--- /dev/null
+++ b/db/schema_migrations/20231105165706
@@ -0,0 +1 @@
+050d1a1a44af5f93902c6a715434ce8144bb6644a891a890d381ae85e6cda9d7 \ No newline at end of file
diff --git a/db/schema_migrations/20231106145853 b/db/schema_migrations/20231106145853
new file mode 100644
index 00000000000..0c50f91529f
--- /dev/null
+++ b/db/schema_migrations/20231106145853
@@ -0,0 +1 @@
+daa117df4a6d8e9a39fcf12e2c64917b7c66429952343b65212fcb27ad30130a \ No newline at end of file
diff --git a/db/schema_migrations/20231106212340 b/db/schema_migrations/20231106212340
new file mode 100644
index 00000000000..1731e94d37a
--- /dev/null
+++ b/db/schema_migrations/20231106212340
@@ -0,0 +1 @@
+c049aa4242cf88bb418e3285de83cf837e6855a709d68970c2f460a7e86bbf26 \ No newline at end of file
diff --git a/db/schema_migrations/20231107062104 b/db/schema_migrations/20231107062104
new file mode 100644
index 00000000000..b58cae8fc66
--- /dev/null
+++ b/db/schema_migrations/20231107062104
@@ -0,0 +1 @@
+d31386b36b5db29deb9041febc116915f94fa7c551f1d91d5f474671dccdc709 \ No newline at end of file
diff --git a/db/schema_migrations/20231107071201 b/db/schema_migrations/20231107071201
new file mode 100644
index 00000000000..4c867fb2ad7
--- /dev/null
+++ b/db/schema_migrations/20231107071201
@@ -0,0 +1 @@
+353eb22ec8e991d6aff2a79ae7e54e5d045aac3da34769e927d137ce9fb41306 \ No newline at end of file
diff --git a/db/schema_migrations/20231107205734 b/db/schema_migrations/20231107205734
new file mode 100644
index 00000000000..8c5a02b54a8
--- /dev/null
+++ b/db/schema_migrations/20231107205734
@@ -0,0 +1 @@
+72f0dde010df3c7bd9f8e5f44510f9d9eae275d1f6c4c3a72fa5813a2d9f3992 \ No newline at end of file
diff --git a/db/schema_migrations/20231108072342 b/db/schema_migrations/20231108072342
new file mode 100644
index 00000000000..69228af4769
--- /dev/null
+++ b/db/schema_migrations/20231108072342
@@ -0,0 +1 @@
+6798b462ec86a98c9f901ba10f6c8b904295091ff9aae48b76289699534f39c4 \ No newline at end of file
diff --git a/db/schema_migrations/20231108093031 b/db/schema_migrations/20231108093031
new file mode 100644
index 00000000000..d532e469d82
--- /dev/null
+++ b/db/schema_migrations/20231108093031
@@ -0,0 +1 @@
+fea17e6126f21671a8836dea252e2bd655179aeb6c746b6bbecaed0580dd255a \ No newline at end of file
diff --git a/db/schema_migrations/20231109133153 b/db/schema_migrations/20231109133153
new file mode 100644
index 00000000000..c9cfd53a77d
--- /dev/null
+++ b/db/schema_migrations/20231109133153
@@ -0,0 +1 @@
+fb17684ac5976811bd08e4a2edb3b3c45baaf293ee3c04e986ae3c197c59c54a \ No newline at end of file
diff --git a/db/schema_migrations/20231109183438 b/db/schema_migrations/20231109183438
new file mode 100644
index 00000000000..32c590bad5a
--- /dev/null
+++ b/db/schema_migrations/20231109183438
@@ -0,0 +1 @@
+87a41f56368f4211291dc6022af91a2168c389b426a1d615321cf0f36bd2c801 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index d7d5d469d9e..1055e902056 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -35,6 +35,248 @@ RETURN NULL;
END
$$;
+CREATE TABLE namespaces (
+ id integer NOT NULL,
+ name character varying NOT NULL,
+ path character varying NOT NULL,
+ owner_id integer,
+ created_at timestamp without time zone,
+ updated_at timestamp without time zone,
+ type character varying DEFAULT 'User'::character varying NOT NULL,
+ description character varying DEFAULT ''::character varying NOT NULL,
+ avatar character varying,
+ membership_lock boolean DEFAULT false,
+ share_with_group_lock boolean DEFAULT false,
+ visibility_level integer DEFAULT 20 NOT NULL,
+ request_access_enabled boolean DEFAULT true NOT NULL,
+ ldap_sync_status character varying DEFAULT 'ready'::character varying NOT NULL,
+ ldap_sync_error character varying,
+ ldap_sync_last_update_at timestamp without time zone,
+ ldap_sync_last_successful_update_at timestamp without time zone,
+ ldap_sync_last_sync_at timestamp without time zone,
+ description_html text,
+ lfs_enabled boolean,
+ parent_id integer,
+ shared_runners_minutes_limit integer,
+ repository_size_limit bigint,
+ require_two_factor_authentication boolean DEFAULT false NOT NULL,
+ two_factor_grace_period integer DEFAULT 48 NOT NULL,
+ cached_markdown_version integer,
+ project_creation_level integer,
+ runners_token character varying,
+ file_template_project_id integer,
+ saml_discovery_token character varying,
+ runners_token_encrypted character varying,
+ custom_project_templates_group_id integer,
+ auto_devops_enabled boolean,
+ extra_shared_runners_minutes_limit integer,
+ last_ci_minutes_notification_at timestamp with time zone,
+ last_ci_minutes_usage_notification_level integer,
+ subgroup_creation_level integer DEFAULT 1,
+ emails_disabled boolean,
+ max_pages_size integer,
+ max_artifacts_size integer,
+ mentions_disabled boolean,
+ default_branch_protection smallint,
+ unlock_membership_to_ldap boolean,
+ max_personal_access_token_lifetime integer,
+ push_rule_id bigint,
+ shared_runners_enabled boolean DEFAULT true NOT NULL,
+ allow_descendants_override_disabled_shared_runners boolean DEFAULT false NOT NULL,
+ traversal_ids integer[] DEFAULT '{}'::integer[] NOT NULL,
+ organization_id bigint DEFAULT 1
+);
+
+CREATE FUNCTION find_namespaces_by_id(namespaces_id bigint) RETURNS namespaces
+ LANGUAGE plpgsql STABLE COST 1 PARALLEL SAFE
+ AS $$
+BEGIN
+ return (SELECT namespaces FROM namespaces WHERE id = namespaces_id LIMIT 1);
+END;
+$$;
+
+CREATE TABLE projects (
+ id integer NOT NULL,
+ name character varying,
+ path character varying,
+ description text,
+ created_at timestamp without time zone,
+ updated_at timestamp without time zone,
+ creator_id integer,
+ namespace_id integer NOT NULL,
+ last_activity_at timestamp without time zone,
+ import_url character varying,
+ visibility_level integer DEFAULT 0 NOT NULL,
+ archived boolean DEFAULT false NOT NULL,
+ avatar character varying,
+ merge_requests_template text,
+ star_count integer DEFAULT 0 NOT NULL,
+ merge_requests_rebase_enabled boolean DEFAULT false,
+ import_type character varying,
+ import_source character varying,
+ approvals_before_merge integer DEFAULT 0 NOT NULL,
+ reset_approvals_on_push boolean DEFAULT true,
+ merge_requests_ff_only_enabled boolean DEFAULT false,
+ issues_template text,
+ mirror boolean DEFAULT false NOT NULL,
+ mirror_last_update_at timestamp without time zone,
+ mirror_last_successful_update_at timestamp without time zone,
+ mirror_user_id integer,
+ shared_runners_enabled boolean DEFAULT true NOT NULL,
+ runners_token character varying,
+ build_allow_git_fetch boolean DEFAULT true NOT NULL,
+ build_timeout integer DEFAULT 3600 NOT NULL,
+ mirror_trigger_builds boolean DEFAULT false NOT NULL,
+ pending_delete boolean DEFAULT false,
+ public_builds boolean DEFAULT true NOT NULL,
+ last_repository_check_failed boolean,
+ last_repository_check_at timestamp without time zone,
+ only_allow_merge_if_pipeline_succeeds boolean DEFAULT false NOT NULL,
+ has_external_issue_tracker boolean,
+ repository_storage character varying DEFAULT 'default'::character varying NOT NULL,
+ repository_read_only boolean,
+ request_access_enabled boolean DEFAULT true NOT NULL,
+ has_external_wiki boolean,
+ ci_config_path character varying,
+ lfs_enabled boolean,
+ description_html text,
+ only_allow_merge_if_all_discussions_are_resolved boolean,
+ repository_size_limit bigint,
+ printing_merge_request_link_enabled boolean DEFAULT true NOT NULL,
+ auto_cancel_pending_pipelines integer DEFAULT 1 NOT NULL,
+ service_desk_enabled boolean DEFAULT true,
+ cached_markdown_version integer,
+ delete_error text,
+ last_repository_updated_at timestamp without time zone,
+ disable_overriding_approvers_per_merge_request boolean,
+ storage_version smallint,
+ resolve_outdated_diff_discussions boolean,
+ remote_mirror_available_overridden boolean,
+ only_mirror_protected_branches boolean,
+ pull_mirror_available_overridden boolean,
+ jobs_cache_index integer,
+ external_authorization_classification_label character varying,
+ mirror_overwrites_diverged_branches boolean,
+ pages_https_only boolean DEFAULT true,
+ external_webhook_token character varying,
+ packages_enabled boolean,
+ merge_requests_author_approval boolean DEFAULT false,
+ pool_repository_id bigint,
+ runners_token_encrypted character varying,
+ bfg_object_map character varying,
+ detected_repository_languages boolean,
+ merge_requests_disable_committers_approval boolean,
+ require_password_to_approve boolean,
+ emails_disabled boolean,
+ max_pages_size integer,
+ max_artifacts_size integer,
+ pull_mirror_branch_prefix character varying(50),
+ remove_source_branch_after_merge boolean,
+ marked_for_deletion_at date,
+ marked_for_deletion_by_user_id integer,
+ autoclose_referenced_issues boolean,
+ suggestion_commit_message character varying(255),
+ project_namespace_id bigint,
+ hidden boolean DEFAULT false NOT NULL,
+ organization_id bigint DEFAULT 1
+);
+
+CREATE FUNCTION find_projects_by_id(projects_id bigint) RETURNS projects
+ LANGUAGE plpgsql STABLE COST 1 PARALLEL SAFE
+ AS $$
+BEGIN
+ return (SELECT projects FROM projects WHERE id = projects_id LIMIT 1);
+END;
+$$;
+
+CREATE TABLE users (
+ id integer NOT NULL,
+ email character varying DEFAULT ''::character varying NOT NULL,
+ encrypted_password character varying DEFAULT ''::character varying NOT NULL,
+ reset_password_token character varying,
+ reset_password_sent_at timestamp without time zone,
+ remember_created_at timestamp without time zone,
+ sign_in_count integer DEFAULT 0,
+ current_sign_in_at timestamp without time zone,
+ last_sign_in_at timestamp without time zone,
+ current_sign_in_ip character varying,
+ last_sign_in_ip character varying,
+ created_at timestamp without time zone,
+ updated_at timestamp without time zone,
+ name character varying,
+ admin boolean DEFAULT false NOT NULL,
+ projects_limit integer NOT NULL,
+ failed_attempts integer DEFAULT 0,
+ locked_at timestamp without time zone,
+ username character varying,
+ can_create_group boolean DEFAULT true NOT NULL,
+ can_create_team boolean DEFAULT true NOT NULL,
+ state character varying,
+ color_scheme_id integer DEFAULT 1 NOT NULL,
+ password_expires_at timestamp without time zone,
+ created_by_id integer,
+ last_credential_check_at timestamp without time zone,
+ avatar character varying,
+ confirmation_token character varying,
+ confirmed_at timestamp without time zone,
+ confirmation_sent_at timestamp without time zone,
+ unconfirmed_email character varying,
+ hide_no_ssh_key boolean DEFAULT false,
+ admin_email_unsubscribed_at timestamp without time zone,
+ notification_email character varying,
+ hide_no_password boolean DEFAULT false,
+ password_automatically_set boolean DEFAULT false,
+ encrypted_otp_secret character varying,
+ encrypted_otp_secret_iv character varying,
+ encrypted_otp_secret_salt character varying,
+ otp_required_for_login boolean DEFAULT false NOT NULL,
+ otp_backup_codes text,
+ public_email character varying,
+ dashboard integer DEFAULT 0,
+ project_view integer DEFAULT 2,
+ consumed_timestep integer,
+ layout integer DEFAULT 0,
+ hide_project_limit boolean DEFAULT false,
+ note text,
+ unlock_token character varying,
+ otp_grace_period_started_at timestamp without time zone,
+ external boolean DEFAULT false,
+ incoming_email_token character varying,
+ auditor boolean DEFAULT false NOT NULL,
+ require_two_factor_authentication_from_group boolean DEFAULT false NOT NULL,
+ two_factor_grace_period integer DEFAULT 48 NOT NULL,
+ last_activity_on date,
+ notified_of_own_activity boolean DEFAULT false,
+ preferred_language character varying,
+ theme_id smallint,
+ accepted_term_id integer,
+ feed_token character varying,
+ private_profile boolean DEFAULT false NOT NULL,
+ roadmap_layout smallint,
+ include_private_contributions boolean,
+ commit_email character varying,
+ group_view integer,
+ managing_group_id integer,
+ first_name character varying(255),
+ last_name character varying(255),
+ static_object_token character varying(255),
+ role smallint,
+ user_type smallint DEFAULT 0,
+ static_object_token_encrypted text,
+ otp_secret_expires_at timestamp with time zone,
+ onboarding_in_progress boolean DEFAULT false NOT NULL,
+ CONSTRAINT check_0dd5948e38 CHECK ((user_type IS NOT NULL)),
+ CONSTRAINT check_7bde697e8e CHECK ((char_length(static_object_token_encrypted) <= 255))
+);
+
+CREATE FUNCTION find_users_by_id(users_id bigint) RETURNS users
+ LANGUAGE plpgsql STABLE COST 1 PARALLEL SAFE
+ AS $$
+BEGIN
+ return (SELECT users FROM users WHERE id = users_id LIMIT 1);
+END;
+$$;
+
CREATE FUNCTION gitlab_schema_prevent_write() RETURNS trigger
LANGUAGE plpgsql
AS $$
@@ -298,30 +540,27 @@ BEGIN
END;
$$;
-CREATE FUNCTION trigger_1bd97da9c1a4() RETURNS trigger
+CREATE FUNCTION trigger_10ee1357e825() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
NEW."auto_canceled_by_id_convert_to_bigint" := NEW."auto_canceled_by_id";
+ NEW."commit_id_convert_to_bigint" := NEW."commit_id";
+ NEW."erased_by_id_convert_to_bigint" := NEW."erased_by_id";
+ NEW."project_id_convert_to_bigint" := NEW."project_id";
+ NEW."runner_id_convert_to_bigint" := NEW."runner_id";
+ NEW."trigger_request_id_convert_to_bigint" := NEW."trigger_request_id";
+ NEW."upstream_pipeline_id_convert_to_bigint" := NEW."upstream_pipeline_id";
+ NEW."user_id_convert_to_bigint" := NEW."user_id";
RETURN NEW;
END;
$$;
-CREATE FUNCTION trigger_239c8032a8d6() RETURNS trigger
- LANGUAGE plpgsql
- AS $$
-BEGIN
- NEW."pipeline_id_convert_to_bigint" := NEW."pipeline_id";
- RETURN NEW;
-END;
-$$;
-
-CREATE FUNCTION trigger_68d7b6653c7d() RETURNS trigger
+CREATE FUNCTION trigger_1bd97da9c1a4() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
- NEW."pipeline_id_convert_to_bigint" := NEW."pipeline_id";
- NEW."source_pipeline_id_convert_to_bigint" := NEW."source_pipeline_id";
+ NEW."auto_canceled_by_id_convert_to_bigint" := NEW."auto_canceled_by_id";
RETURN NEW;
END;
$$;
@@ -344,29 +583,11 @@ BEGIN
END;
$$;
-CREATE FUNCTION trigger_bbb95b2d6929() RETURNS trigger
- LANGUAGE plpgsql
- AS $$
-BEGIN
- NEW."shared_runners_duration_convert_to_bigint" := NEW."shared_runners_duration";
- RETURN NEW;
-END;
-$$;
-
-CREATE FUNCTION trigger_bfad0e2b9c86() RETURNS trigger
- LANGUAGE plpgsql
- AS $$
-BEGIN
- NEW."pipeline_id_convert_to_bigint" := NEW."pipeline_id";
- RETURN NEW;
-END;
-$$;
-
-CREATE FUNCTION trigger_c0353bbb6145() RETURNS trigger
+CREATE FUNCTION trigger_eaec934fe6b2() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
- NEW."shared_runners_duration_convert_to_bigint" := NEW."shared_runners_duration";
+ NEW."id_convert_to_bigint" := NEW."id";
RETURN NEW;
END;
$$;
@@ -10880,6 +11101,30 @@ CREATE SEQUENCE achievements_id_seq
ALTER SEQUENCE achievements_id_seq OWNED BY achievements.id;
+CREATE TABLE activity_pub_releases_subscriptions (
+ id bigint NOT NULL,
+ project_id bigint NOT NULL,
+ created_at timestamp with time zone NOT NULL,
+ updated_at timestamp with time zone NOT NULL,
+ status smallint DEFAULT 1 NOT NULL,
+ shared_inbox_url text,
+ subscriber_inbox_url text,
+ subscriber_url text NOT NULL,
+ payload jsonb,
+ CONSTRAINT check_0ebf38bcaa CHECK ((char_length(subscriber_inbox_url) <= 1024)),
+ CONSTRAINT check_2afd35ba17 CHECK ((char_length(subscriber_url) <= 1024)),
+ CONSTRAINT check_61b77ced49 CHECK ((char_length(shared_inbox_url) <= 1024))
+);
+
+CREATE SEQUENCE activity_pub_releases_subscriptions_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+ALTER SEQUENCE activity_pub_releases_subscriptions_id_seq OWNED BY activity_pub_releases_subscriptions.id;
+
CREATE TABLE agent_activity_events (
id bigint NOT NULL,
agent_id bigint NOT NULL,
@@ -11639,7 +11884,6 @@ CREATE TABLE application_settings (
notes_create_limit integer DEFAULT 300 NOT NULL,
notes_create_limit_allowlist text[] DEFAULT '{}'::text[] NOT NULL,
kroki_formats jsonb DEFAULT '{}'::jsonb NOT NULL,
- in_product_marketing_emails_enabled boolean DEFAULT true NOT NULL,
asset_proxy_whitelist text,
admin_mode boolean DEFAULT false NOT NULL,
delayed_project_removal boolean DEFAULT false NOT NULL,
@@ -11831,7 +12075,7 @@ CREATE TABLE application_settings (
encrypted_product_analytics_configurator_connection_string bytea,
encrypted_product_analytics_configurator_connection_string_iv bytea,
silent_mode_enabled boolean DEFAULT false NOT NULL,
- package_metadata_purl_types smallint[] DEFAULT '{1,2,3,4,5,6,7,8,9,10,11,12}'::smallint[],
+ package_metadata_purl_types smallint[] DEFAULT '{1,2,3,4,5,6,7,8,9,10,11,12,13}'::smallint[],
ci_max_includes integer DEFAULT 150 NOT NULL,
remember_me_enabled boolean DEFAULT true NOT NULL,
encrypted_anthropic_api_key bytea,
@@ -11874,6 +12118,10 @@ CREATE TABLE application_settings (
encrypted_vertex_ai_access_token_iv bytea,
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,
+ 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,
CONSTRAINT app_settings_container_reg_cleanup_tags_max_list_size_positive CHECK ((container_registry_cleanup_tags_service_max_list_size >= 0)),
CONSTRAINT app_settings_container_registry_pre_import_tags_rate_positive CHECK ((container_registry_pre_import_tags_rate >= (0)::numeric)),
CONSTRAINT app_settings_dep_proxy_ttl_policies_worker_capacity_positive CHECK ((dependency_proxy_ttl_group_policy_worker_capacity >= 0)),
@@ -11986,6 +12234,74 @@ CREATE SEQUENCE application_settings_id_seq
ALTER SEQUENCE application_settings_id_seq OWNED BY application_settings.id;
+CREATE TABLE approval_group_rules (
+ id bigint NOT NULL,
+ group_id bigint NOT NULL,
+ created_at timestamp with time zone NOT NULL,
+ updated_at timestamp with time zone NOT NULL,
+ approvals_required smallint DEFAULT 0 NOT NULL,
+ report_type smallint,
+ rule_type smallint DEFAULT 1 NOT NULL,
+ security_orchestration_policy_configuration_id bigint,
+ scan_result_policy_id bigint,
+ name text NOT NULL,
+ CONSTRAINT check_25d42add43 CHECK ((char_length(name) <= 255))
+);
+
+CREATE TABLE approval_group_rules_groups (
+ id bigint NOT NULL,
+ approval_group_rule_id bigint NOT NULL,
+ group_id bigint NOT NULL
+);
+
+CREATE SEQUENCE approval_group_rules_groups_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+ALTER SEQUENCE approval_group_rules_groups_id_seq OWNED BY approval_group_rules_groups.id;
+
+CREATE SEQUENCE approval_group_rules_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+ALTER SEQUENCE approval_group_rules_id_seq OWNED BY approval_group_rules.id;
+
+CREATE TABLE approval_group_rules_protected_branches (
+ id bigint NOT NULL,
+ approval_group_rule_id bigint NOT NULL,
+ protected_branch_id bigint NOT NULL
+);
+
+CREATE SEQUENCE approval_group_rules_protected_branches_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+ALTER SEQUENCE approval_group_rules_protected_branches_id_seq OWNED BY approval_group_rules_protected_branches.id;
+
+CREATE TABLE approval_group_rules_users (
+ id bigint NOT NULL,
+ approval_group_rule_id bigint NOT NULL,
+ user_id bigint NOT NULL
+);
+
+CREATE SEQUENCE approval_group_rules_users_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+ALTER SEQUENCE approval_group_rules_users_id_seq OWNED BY approval_group_rules_users.id;
+
CREATE TABLE approval_merge_request_rule_sources (
id bigint NOT NULL,
approval_merge_request_rule_id bigint NOT NULL,
@@ -12395,6 +12711,23 @@ CREATE SEQUENCE audit_events_streaming_headers_id_seq
ALTER SEQUENCE audit_events_streaming_headers_id_seq OWNED BY audit_events_streaming_headers.id;
+CREATE TABLE audit_events_streaming_http_group_namespace_filters (
+ id bigint NOT NULL,
+ created_at timestamp with time zone NOT NULL,
+ updated_at timestamp with time zone NOT NULL,
+ external_audit_event_destination_id bigint NOT NULL,
+ namespace_id bigint NOT NULL
+);
+
+CREATE SEQUENCE audit_events_streaming_http_group_namespace_filters_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+ALTER SEQUENCE audit_events_streaming_http_group_namespace_filters_id_seq OWNED BY audit_events_streaming_http_group_namespace_filters.id;
+
CREATE TABLE audit_events_streaming_instance_event_type_filters (
id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
@@ -13031,10 +13364,14 @@ CREATE TABLE bulk_import_failures (
exception_message text NOT NULL,
correlation_id_value text,
pipeline_step text,
+ source_url text,
+ source_title text,
CONSTRAINT check_053d65c7a4 CHECK ((char_length(pipeline_class) <= 255)),
CONSTRAINT check_6eca8f972e CHECK ((char_length(exception_message) <= 255)),
CONSTRAINT check_721a422375 CHECK ((char_length(pipeline_step) <= 255)),
+ CONSTRAINT check_74414228d4 CHECK ((char_length(source_title) <= 255)),
CONSTRAINT check_c7dba8398e CHECK ((char_length(exception_class) <= 255)),
+ CONSTRAINT check_e035a720ad CHECK ((char_length(source_url) <= 255)),
CONSTRAINT check_e787285882 CHECK ((char_length(correlation_id_value) <= 255))
);
@@ -13105,6 +13442,8 @@ CREATE TABLE catalog_resource_components (
resource_type smallint DEFAULT 1 NOT NULL,
inputs jsonb DEFAULT '{}'::jsonb NOT NULL,
name text NOT NULL,
+ path text,
+ CONSTRAINT check_a76bfd47fe CHECK ((char_length(path) <= 255)),
CONSTRAINT check_ddca729980 CHECK ((char_length(name) <= 255))
);
@@ -13139,7 +13478,10 @@ CREATE TABLE catalog_resources (
project_id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
state smallint DEFAULT 0 NOT NULL,
- latest_released_at timestamp with time zone
+ latest_released_at timestamp with time zone,
+ name character varying,
+ description text,
+ visibility_level integer DEFAULT 0 NOT NULL
);
CREATE SEQUENCE catalog_resources_id_seq
@@ -13313,6 +13655,14 @@ CREATE TABLE p_ci_builds (
stage_id bigint,
partition_id bigint NOT NULL,
auto_canceled_by_partition_id bigint DEFAULT 100 NOT NULL,
+ auto_canceled_by_id_convert_to_bigint bigint,
+ commit_id_convert_to_bigint bigint,
+ erased_by_id_convert_to_bigint bigint,
+ project_id_convert_to_bigint bigint,
+ runner_id_convert_to_bigint bigint,
+ trigger_request_id_convert_to_bigint bigint,
+ upstream_pipeline_id_convert_to_bigint bigint,
+ user_id_convert_to_bigint bigint,
CONSTRAINT check_1e2fbd1b39 CHECK ((lock_version IS NOT NULL))
)
PARTITION BY LIST (partition_id);
@@ -13363,6 +13713,14 @@ CREATE TABLE ci_builds (
stage_id bigint,
partition_id bigint NOT NULL,
auto_canceled_by_partition_id bigint DEFAULT 100 NOT NULL,
+ auto_canceled_by_id_convert_to_bigint bigint,
+ commit_id_convert_to_bigint bigint,
+ erased_by_id_convert_to_bigint bigint,
+ project_id_convert_to_bigint bigint,
+ runner_id_convert_to_bigint bigint,
+ trigger_request_id_convert_to_bigint bigint,
+ upstream_pipeline_id_convert_to_bigint bigint,
+ user_id_convert_to_bigint bigint,
CONSTRAINT check_1e2fbd1b39 CHECK ((lock_version IS NOT NULL))
);
@@ -13704,7 +14062,6 @@ CREATE TABLE ci_namespace_monthly_usages (
namespace_id bigint NOT NULL,
date date NOT NULL,
notification_level smallint DEFAULT 100 NOT NULL,
- shared_runners_duration_convert_to_bigint integer DEFAULT 0 NOT NULL,
created_at timestamp with time zone,
amount_used numeric(18,4) DEFAULT 0.0 NOT NULL,
shared_runners_duration bigint DEFAULT 0 NOT NULL,
@@ -13794,7 +14151,6 @@ ALTER SEQUENCE ci_pipeline_artifacts_id_seq OWNED BY ci_pipeline_artifacts.id;
CREATE TABLE ci_pipeline_chat_data (
id bigint NOT NULL,
- pipeline_id_convert_to_bigint integer DEFAULT 0 NOT NULL,
chat_name_id integer NOT NULL,
response_url text NOT NULL,
pipeline_id bigint NOT NULL
@@ -13812,7 +14168,6 @@ ALTER SEQUENCE ci_pipeline_chat_data_id_seq OWNED BY ci_pipeline_chat_data.id;
CREATE TABLE ci_pipeline_messages (
id bigint NOT NULL,
severity smallint DEFAULT 0 NOT NULL,
- pipeline_id_convert_to_bigint integer DEFAULT 0 NOT NULL,
content text NOT NULL,
pipeline_id bigint NOT NULL,
CONSTRAINT check_58ca2981b2 CHECK ((char_length(content) <= 10000))
@@ -13921,7 +14276,7 @@ CREATE TABLE ci_pipelines (
duration integer,
user_id integer,
lock_version integer DEFAULT 0,
- auto_canceled_by_id integer,
+ auto_canceled_by_id_convert_to_bigint integer,
pipeline_schedule_id integer,
source integer,
config_source integer,
@@ -13936,7 +14291,7 @@ CREATE TABLE ci_pipelines (
locked smallint DEFAULT 1 NOT NULL,
partition_id bigint NOT NULL,
id_convert_to_bigint bigint DEFAULT 0 NOT NULL,
- auto_canceled_by_id_convert_to_bigint bigint,
+ auto_canceled_by_id bigint,
CONSTRAINT check_d7e99a025e CHECK ((lock_version IS NOT NULL))
);
@@ -13991,7 +14346,6 @@ CREATE TABLE ci_project_monthly_usages (
id bigint NOT NULL,
project_id bigint NOT NULL,
date date NOT NULL,
- shared_runners_duration_convert_to_bigint integer DEFAULT 0 NOT NULL,
created_at timestamp with time zone,
amount_used numeric(18,4) DEFAULT 0.0 NOT NULL,
shared_runners_duration bigint DEFAULT 0 NOT NULL,
@@ -14240,9 +14594,7 @@ ALTER SEQUENCE ci_secure_files_id_seq OWNED BY ci_secure_files.id;
CREATE TABLE ci_sources_pipelines (
id integer NOT NULL,
project_id integer,
- pipeline_id_convert_to_bigint integer,
source_project_id integer,
- source_pipeline_id_convert_to_bigint integer,
source_job_id bigint,
partition_id bigint NOT NULL,
source_partition_id bigint NOT NULL,
@@ -14276,7 +14628,7 @@ ALTER SEQUENCE ci_sources_projects_id_seq OWNED BY ci_sources_projects.id;
CREATE TABLE ci_stages (
project_id integer,
- pipeline_id integer,
+ pipeline_id_convert_to_bigint integer,
created_at timestamp without time zone,
updated_at timestamp without time zone,
name character varying,
@@ -14285,7 +14637,7 @@ CREATE TABLE ci_stages (
"position" integer,
id bigint NOT NULL,
partition_id bigint NOT NULL,
- pipeline_id_convert_to_bigint bigint,
+ pipeline_id bigint,
CONSTRAINT check_81b431e49b CHECK ((lock_version IS NOT NULL))
);
@@ -14674,6 +15026,24 @@ CREATE SEQUENCE commit_user_mentions_id_seq
ALTER SEQUENCE commit_user_mentions_id_seq OWNED BY commit_user_mentions.id;
+CREATE TABLE compliance_framework_security_policies (
+ id bigint NOT NULL,
+ framework_id bigint NOT NULL,
+ policy_configuration_id bigint NOT NULL,
+ created_at timestamp with time zone NOT NULL,
+ updated_at timestamp with time zone NOT NULL,
+ policy_index smallint NOT NULL
+);
+
+CREATE SEQUENCE compliance_framework_security_policies_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+ALTER SEQUENCE compliance_framework_security_policies_id_seq OWNED BY compliance_framework_security_policies.id;
+
CREATE TABLE compliance_management_frameworks (
id bigint NOT NULL,
name text NOT NULL,
@@ -16431,8 +16801,6 @@ CREATE TABLE geo_node_statuses (
id integer NOT NULL,
geo_node_id integer NOT NULL,
db_replication_lag_seconds integer,
- repositories_synced_count integer,
- repositories_failed_count integer,
lfs_objects_count integer,
lfs_objects_synced_count integer,
lfs_objects_failed_count integer,
@@ -16452,15 +16820,9 @@ CREATE TABLE geo_node_statuses (
job_artifacts_failed_count integer,
version character varying,
revision character varying,
- repositories_verified_count integer,
- repositories_verification_failed_count integer,
lfs_objects_synced_missing_on_primary_count integer,
job_artifacts_synced_missing_on_primary_count integer,
- repositories_checksummed_count integer,
- repositories_checksum_failed_count integer,
- repositories_checksum_mismatch_count integer,
storage_configuration_digest bytea,
- repositories_retrying_verification_count integer,
projects_count integer,
container_repositories_count integer,
container_repositories_synced_count integer,
@@ -17102,10 +17464,7 @@ CREATE TABLE in_product_marketing_emails (
track smallint,
series smallint,
created_at timestamp with time zone NOT NULL,
- updated_at timestamp with time zone NOT NULL,
- campaign text,
- CONSTRAINT check_9d8b29f74f CHECK ((char_length(campaign) <= 255)),
- CONSTRAINT in_product_marketing_emails_track_and_series_or_campaign CHECK ((((track IS NOT NULL) AND (series IS NOT NULL) AND (campaign IS NULL)) OR ((track IS NULL) AND (series IS NULL) AND (campaign IS NOT NULL))))
+ updated_at timestamp with time zone NOT NULL
);
CREATE SEQUENCE in_product_marketing_emails_id_seq
@@ -18166,6 +18525,7 @@ CREATE TABLE member_roles (
admin_merge_request boolean DEFAULT false NOT NULL,
admin_group_member boolean DEFAULT false NOT NULL,
manage_project_access_tokens boolean DEFAULT false NOT NULL,
+ archive_project boolean DEFAULT false NOT NULL,
CONSTRAINT check_4364846f58 CHECK ((char_length(description) <= 255)),
CONSTRAINT check_9907916995 CHECK ((char_length(name) <= 255))
);
@@ -18388,7 +18748,8 @@ CREATE TABLE merge_request_diff_files (
diff text,
"binary" boolean,
external_diff_offset integer,
- external_diff_size integer
+ external_diff_size integer,
+ generated boolean
);
CREATE TABLE merge_request_diff_llm_summaries (
@@ -18819,6 +19180,7 @@ CREATE TABLE ml_candidates (
project_id bigint,
internal_id bigint,
ci_build_id bigint,
+ model_version_id bigint,
CONSTRAINT check_25e6c65051 CHECK ((char_length(name) <= 255)),
CONSTRAINT check_cd160587d4 CHECK ((eid IS NOT NULL))
);
@@ -18874,6 +19236,26 @@ CREATE SEQUENCE ml_experiments_id_seq
ALTER SEQUENCE ml_experiments_id_seq OWNED BY ml_experiments.id;
+CREATE TABLE ml_model_metadata (
+ id bigint NOT NULL,
+ created_at timestamp with time zone NOT NULL,
+ updated_at timestamp with time zone NOT NULL,
+ model_id bigint NOT NULL,
+ name text NOT NULL,
+ value text NOT NULL,
+ CONSTRAINT check_26d3322153 CHECK ((char_length(value) <= 5000)),
+ CONSTRAINT check_36240c80a7 CHECK ((char_length(name) <= 255))
+);
+
+CREATE SEQUENCE ml_model_metadata_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+ALTER SEQUENCE ml_model_metadata_id_seq OWNED BY ml_model_metadata.id;
+
CREATE TABLE ml_model_versions (
id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
@@ -18882,7 +19264,9 @@ CREATE TABLE ml_model_versions (
model_id bigint NOT NULL,
package_id bigint,
version text NOT NULL,
- CONSTRAINT check_28b2d892c8 CHECK ((char_length(version) <= 255))
+ description text,
+ CONSTRAINT check_28b2d892c8 CHECK ((char_length(version) <= 255)),
+ CONSTRAINT check_caff7d000b CHECK ((char_length(description) <= 500))
);
CREATE SEQUENCE ml_model_versions_id_seq
@@ -18900,7 +19284,10 @@ CREATE TABLE ml_models (
updated_at timestamp with time zone NOT NULL,
project_id bigint NOT NULL,
name text NOT NULL,
- CONSTRAINT check_1fd2cc7d93 CHECK ((char_length(name) <= 255))
+ description text,
+ user_id integer,
+ CONSTRAINT check_1fd2cc7d93 CHECK ((char_length(name) <= 255)),
+ CONSTRAINT check_d0c47d63b5 CHECK ((char_length(description) <= 5000))
);
CREATE SEQUENCE ml_models_id_seq
@@ -19082,6 +19469,8 @@ CREATE TABLE namespace_settings (
experiment_features_enabled boolean DEFAULT false NOT NULL,
third_party_ai_features_enabled boolean DEFAULT true NOT NULL,
default_branch_protection_defaults jsonb DEFAULT '{}'::jsonb NOT NULL,
+ service_access_tokens_expiration_enforced boolean DEFAULT true NOT NULL,
+ product_analytics_enabled boolean DEFAULT false NOT NULL,
CONSTRAINT check_0ba93c78c7 CHECK ((char_length(default_branch_name) <= 255)),
CONSTRAINT namespace_settings_unique_project_download_limit_alertlist_size CHECK ((cardinality(unique_project_download_limit_alertlist) <= 100)),
CONSTRAINT namespace_settings_unique_project_download_limit_allowlist_size CHECK ((cardinality(unique_project_download_limit_allowlist) <= 100))
@@ -19106,58 +19495,6 @@ CREATE SEQUENCE namespace_statistics_id_seq
ALTER SEQUENCE namespace_statistics_id_seq OWNED BY namespace_statistics.id;
-CREATE TABLE namespaces (
- id integer NOT NULL,
- name character varying NOT NULL,
- path character varying NOT NULL,
- owner_id integer,
- created_at timestamp without time zone,
- updated_at timestamp without time zone,
- type character varying DEFAULT 'User'::character varying NOT NULL,
- description character varying DEFAULT ''::character varying NOT NULL,
- avatar character varying,
- membership_lock boolean DEFAULT false,
- share_with_group_lock boolean DEFAULT false,
- visibility_level integer DEFAULT 20 NOT NULL,
- request_access_enabled boolean DEFAULT true NOT NULL,
- ldap_sync_status character varying DEFAULT 'ready'::character varying NOT NULL,
- ldap_sync_error character varying,
- ldap_sync_last_update_at timestamp without time zone,
- ldap_sync_last_successful_update_at timestamp without time zone,
- ldap_sync_last_sync_at timestamp without time zone,
- description_html text,
- lfs_enabled boolean,
- parent_id integer,
- shared_runners_minutes_limit integer,
- repository_size_limit bigint,
- require_two_factor_authentication boolean DEFAULT false NOT NULL,
- two_factor_grace_period integer DEFAULT 48 NOT NULL,
- cached_markdown_version integer,
- project_creation_level integer,
- runners_token character varying,
- file_template_project_id integer,
- saml_discovery_token character varying,
- runners_token_encrypted character varying,
- custom_project_templates_group_id integer,
- auto_devops_enabled boolean,
- extra_shared_runners_minutes_limit integer,
- last_ci_minutes_notification_at timestamp with time zone,
- last_ci_minutes_usage_notification_level integer,
- subgroup_creation_level integer DEFAULT 1,
- emails_disabled boolean,
- max_pages_size integer,
- max_artifacts_size integer,
- mentions_disabled boolean,
- default_branch_protection smallint,
- unlock_membership_to_ldap boolean,
- max_personal_access_token_lifetime integer,
- push_rule_id bigint,
- shared_runners_enabled boolean DEFAULT true NOT NULL,
- allow_descendants_override_disabled_shared_runners boolean DEFAULT false NOT NULL,
- traversal_ids integer[] DEFAULT '{}'::integer[] NOT NULL,
- organization_id bigint DEFAULT 1
-);
-
CREATE SEQUENCE namespaces_id_seq
START WITH 1
INCREMENT BY 1
@@ -20084,6 +20421,7 @@ CREATE TABLE packages_npm_metadata_caches (
file text NOT NULL,
package_name text NOT NULL,
object_storage_key text NOT NULL,
+ status smallint DEFAULT 0 NOT NULL,
CONSTRAINT check_57aa07a4b2 CHECK ((char_length(file) <= 255)),
CONSTRAINT check_f97c15aa60 CHECK ((char_length(object_storage_key) <= 255))
);
@@ -20130,6 +20468,7 @@ CREATE TABLE packages_nuget_symbols (
file_path text NOT NULL,
signature text NOT NULL,
object_storage_key text NOT NULL,
+ file_sha256 bytea,
CONSTRAINT check_0e93ca58b7 CHECK ((char_length(file) <= 255)),
CONSTRAINT check_28b82b08fa CHECK ((char_length(object_storage_key) <= 255)),
CONSTRAINT check_30b0ef2ca2 CHECK ((char_length(file_path) <= 255)),
@@ -20356,7 +20695,8 @@ CREATE TABLE packages_tags (
package_id integer NOT NULL,
name character varying(255) NOT NULL,
created_at timestamp with time zone NOT NULL,
- updated_at timestamp with time zone NOT NULL
+ updated_at timestamp with time zone NOT NULL,
+ project_id bigint
);
CREATE SEQUENCE packages_tags_id_seq
@@ -21242,7 +21582,8 @@ CREATE TABLE project_ci_cd_settings (
allow_fork_pipelines_to_run_in_parent_project boolean DEFAULT true NOT NULL,
inbound_job_token_scope_enabled boolean DEFAULT true NOT NULL,
forward_deployment_rollback_allowed boolean DEFAULT true NOT NULL,
- merge_trains_skip_train_allowed boolean DEFAULT false NOT NULL
+ merge_trains_skip_train_allowed boolean DEFAULT false NOT NULL,
+ restrict_pipeline_cancellation_role smallint DEFAULT 0 NOT NULL
);
CREATE SEQUENCE project_ci_cd_settings_id_seq
@@ -21697,7 +22038,6 @@ CREATE TABLE project_settings (
selective_code_owner_removals boolean DEFAULT false NOT NULL,
issue_branch_template text,
show_diff_preview_in_email boolean DEFAULT true NOT NULL,
- jitsu_key text,
suggested_reviewers_enabled boolean DEFAULT false NOT NULL,
only_allow_merge_if_all_status_checks_passed boolean DEFAULT false NOT NULL,
mirror_branch_regex text,
@@ -21715,7 +22055,6 @@ CREATE TABLE project_settings (
encrypted_product_analytics_configurator_connection_string_iv bytea,
pages_multiple_versions_enabled boolean DEFAULT false NOT NULL,
CONSTRAINT check_1a30456322 CHECK ((char_length(pages_unique_domain) <= 63)),
- CONSTRAINT check_2981f15877 CHECK ((char_length(jitsu_key) <= 100)),
CONSTRAINT check_3a03e7557a CHECK ((char_length(previous_default_branch) <= 4096)),
CONSTRAINT check_3ca5cbffe6 CHECK ((char_length(issue_branch_template) <= 255)),
CONSTRAINT check_4b142e71f3 CHECK ((char_length(product_analytics_data_collector_host) <= 255)),
@@ -21813,92 +22152,6 @@ CREATE SEQUENCE project_wiki_repositories_id_seq
ALTER SEQUENCE project_wiki_repositories_id_seq OWNED BY project_wiki_repositories.id;
-CREATE TABLE projects (
- id integer NOT NULL,
- name character varying,
- path character varying,
- description text,
- created_at timestamp without time zone,
- updated_at timestamp without time zone,
- creator_id integer,
- namespace_id integer NOT NULL,
- last_activity_at timestamp without time zone,
- import_url character varying,
- visibility_level integer DEFAULT 0 NOT NULL,
- archived boolean DEFAULT false NOT NULL,
- avatar character varying,
- merge_requests_template text,
- star_count integer DEFAULT 0 NOT NULL,
- merge_requests_rebase_enabled boolean DEFAULT false,
- import_type character varying,
- import_source character varying,
- approvals_before_merge integer DEFAULT 0 NOT NULL,
- reset_approvals_on_push boolean DEFAULT true,
- merge_requests_ff_only_enabled boolean DEFAULT false,
- issues_template text,
- mirror boolean DEFAULT false NOT NULL,
- mirror_last_update_at timestamp without time zone,
- mirror_last_successful_update_at timestamp without time zone,
- mirror_user_id integer,
- shared_runners_enabled boolean DEFAULT true NOT NULL,
- runners_token character varying,
- build_allow_git_fetch boolean DEFAULT true NOT NULL,
- build_timeout integer DEFAULT 3600 NOT NULL,
- mirror_trigger_builds boolean DEFAULT false NOT NULL,
- pending_delete boolean DEFAULT false,
- public_builds boolean DEFAULT true NOT NULL,
- last_repository_check_failed boolean,
- last_repository_check_at timestamp without time zone,
- only_allow_merge_if_pipeline_succeeds boolean DEFAULT false NOT NULL,
- has_external_issue_tracker boolean,
- repository_storage character varying DEFAULT 'default'::character varying NOT NULL,
- repository_read_only boolean,
- request_access_enabled boolean DEFAULT true NOT NULL,
- has_external_wiki boolean,
- ci_config_path character varying,
- lfs_enabled boolean,
- description_html text,
- only_allow_merge_if_all_discussions_are_resolved boolean,
- repository_size_limit bigint,
- printing_merge_request_link_enabled boolean DEFAULT true NOT NULL,
- auto_cancel_pending_pipelines integer DEFAULT 1 NOT NULL,
- service_desk_enabled boolean DEFAULT true,
- cached_markdown_version integer,
- delete_error text,
- last_repository_updated_at timestamp without time zone,
- disable_overriding_approvers_per_merge_request boolean,
- storage_version smallint,
- resolve_outdated_diff_discussions boolean,
- remote_mirror_available_overridden boolean,
- only_mirror_protected_branches boolean,
- pull_mirror_available_overridden boolean,
- jobs_cache_index integer,
- external_authorization_classification_label character varying,
- mirror_overwrites_diverged_branches boolean,
- pages_https_only boolean DEFAULT true,
- external_webhook_token character varying,
- packages_enabled boolean,
- merge_requests_author_approval boolean DEFAULT false,
- pool_repository_id bigint,
- runners_token_encrypted character varying,
- bfg_object_map character varying,
- detected_repository_languages boolean,
- merge_requests_disable_committers_approval boolean,
- require_password_to_approve boolean,
- emails_disabled boolean,
- max_pages_size integer,
- max_artifacts_size integer,
- pull_mirror_branch_prefix character varying(50),
- remove_source_branch_after_merge boolean,
- marked_for_deletion_at date,
- marked_for_deletion_by_user_id integer,
- autoclose_referenced_issues boolean,
- suggestion_commit_message character varying(255),
- project_namespace_id bigint,
- hidden boolean DEFAULT false NOT NULL,
- organization_id bigint DEFAULT 1
-);
-
CREATE SEQUENCE projects_id_seq
START WITH 1
INCREMENT BY 1
@@ -22337,6 +22590,7 @@ CREATE TABLE remote_development_agent_configs (
dns_zone text NOT NULL,
network_policy_enabled boolean DEFAULT true NOT NULL,
gitlab_workspaces_proxy_namespace text DEFAULT 'gitlab-workspaces'::text NOT NULL,
+ network_policy_egress jsonb DEFAULT '[{"allow": "0.0.0.0/0", "except": ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"]}]'::jsonb NOT NULL,
CONSTRAINT check_72947a4495 CHECK ((char_length(gitlab_workspaces_proxy_namespace) <= 63)),
CONSTRAINT check_9f5cd54d1c CHECK ((char_length(dns_zone) <= 256))
);
@@ -22697,7 +22951,9 @@ CREATE TABLE sbom_components (
component_type smallint NOT NULL,
name text NOT NULL,
purl_type smallint,
- CONSTRAINT check_91a8f6ad53 CHECK ((char_length(name) <= 255))
+ source_package_name text,
+ CONSTRAINT check_91a8f6ad53 CHECK ((char_length(name) <= 255)),
+ CONSTRAINT check_e2dcb53709 CHECK ((char_length(source_package_name) <= 255))
);
CREATE SEQUENCE sbom_components_id_seq
@@ -23093,6 +23349,7 @@ CREATE TABLE service_desk_custom_email_credentials (
encrypted_smtp_username_iv bytea,
encrypted_smtp_password bytea,
encrypted_smtp_password_iv bytea,
+ smtp_authentication smallint,
CONSTRAINT check_6dd11e956a CHECK ((char_length(smtp_address) <= 255))
);
@@ -23418,7 +23675,8 @@ CREATE TABLE status_check_responses (
sha bytea NOT NULL,
external_status_check_id bigint NOT NULL,
status smallint DEFAULT 0 NOT NULL,
- retried_at timestamp with time zone
+ retried_at timestamp with time zone,
+ created_at timestamp with time zone DEFAULT now() NOT NULL
);
CREATE SEQUENCE status_check_responses_id_seq
@@ -23621,7 +23879,8 @@ CREATE TABLE system_note_metadata (
created_at timestamp without time zone NOT NULL,
updated_at timestamp without time zone NOT NULL,
description_version_id bigint,
- note_id bigint NOT NULL
+ note_id bigint NOT NULL,
+ id_convert_to_bigint bigint DEFAULT 0 NOT NULL
);
CREATE SEQUENCE system_note_metadata_id_seq
@@ -24088,6 +24347,8 @@ CREATE TABLE user_details (
enterprise_group_id bigint,
enterprise_group_associated_at timestamp with time zone,
email_reset_offered_at timestamp with time zone,
+ mastodon text DEFAULT ''::text NOT NULL,
+ project_authorizations_recalculated_at timestamp with time zone DEFAULT '2010-01-01 00:00:00+00'::timestamp with time zone NOT NULL,
CONSTRAINT check_245664af82 CHECK ((char_length(webauthn_xid) <= 100)),
CONSTRAINT check_444573ee52 CHECK ((char_length(skype) <= 500)),
CONSTRAINT check_466a25be35 CHECK ((char_length(twitter) <= 500)),
@@ -24099,6 +24360,7 @@ CREATE TABLE user_details (
CONSTRAINT check_8a7fcf8a60 CHECK ((char_length(location) <= 500)),
CONSTRAINT check_a73b398c60 CHECK ((char_length(phone) <= 50)),
CONSTRAINT check_eeeaf8d4f0 CHECK ((char_length(pronouns) <= 50)),
+ CONSTRAINT check_f1a8a05b9a CHECK ((char_length(mastodon) <= 500)),
CONSTRAINT check_f932ed37db CHECK ((char_length(pronunciation) <= 255))
);
@@ -24239,6 +24501,7 @@ CREATE TABLE user_preferences (
project_shortcut_buttons boolean DEFAULT true NOT NULL,
enabled_zoekt boolean DEFAULT true NOT NULL,
keyboard_shortcuts_enabled boolean DEFAULT true NOT NULL,
+ time_display_format smallint DEFAULT 0 NOT NULL,
CONSTRAINT check_89bf269f41 CHECK ((char_length(diffs_deletion_color) <= 7)),
CONSTRAINT check_d07ccd35f7 CHECK ((char_length(diffs_addition_color) <= 7))
);
@@ -24306,90 +24569,6 @@ CREATE SEQUENCE user_synced_attributes_metadata_id_seq
ALTER SEQUENCE user_synced_attributes_metadata_id_seq OWNED BY user_synced_attributes_metadata.id;
-CREATE TABLE users (
- id integer NOT NULL,
- email character varying DEFAULT ''::character varying NOT NULL,
- encrypted_password character varying DEFAULT ''::character varying NOT NULL,
- reset_password_token character varying,
- reset_password_sent_at timestamp without time zone,
- remember_created_at timestamp without time zone,
- sign_in_count integer DEFAULT 0,
- current_sign_in_at timestamp without time zone,
- last_sign_in_at timestamp without time zone,
- current_sign_in_ip character varying,
- last_sign_in_ip character varying,
- created_at timestamp without time zone,
- updated_at timestamp without time zone,
- name character varying,
- admin boolean DEFAULT false NOT NULL,
- projects_limit integer NOT NULL,
- failed_attempts integer DEFAULT 0,
- locked_at timestamp without time zone,
- username character varying,
- can_create_group boolean DEFAULT true NOT NULL,
- can_create_team boolean DEFAULT true NOT NULL,
- state character varying,
- color_scheme_id integer DEFAULT 1 NOT NULL,
- password_expires_at timestamp without time zone,
- created_by_id integer,
- last_credential_check_at timestamp without time zone,
- avatar character varying,
- confirmation_token character varying,
- confirmed_at timestamp without time zone,
- confirmation_sent_at timestamp without time zone,
- unconfirmed_email character varying,
- hide_no_ssh_key boolean DEFAULT false,
- admin_email_unsubscribed_at timestamp without time zone,
- notification_email character varying,
- hide_no_password boolean DEFAULT false,
- password_automatically_set boolean DEFAULT false,
- encrypted_otp_secret character varying,
- encrypted_otp_secret_iv character varying,
- encrypted_otp_secret_salt character varying,
- otp_required_for_login boolean DEFAULT false NOT NULL,
- otp_backup_codes text,
- public_email character varying,
- dashboard integer DEFAULT 0,
- project_view integer DEFAULT 2,
- consumed_timestep integer,
- layout integer DEFAULT 0,
- hide_project_limit boolean DEFAULT false,
- note text,
- unlock_token character varying,
- otp_grace_period_started_at timestamp without time zone,
- external boolean DEFAULT false,
- incoming_email_token character varying,
- auditor boolean DEFAULT false NOT NULL,
- require_two_factor_authentication_from_group boolean DEFAULT false NOT NULL,
- two_factor_grace_period integer DEFAULT 48 NOT NULL,
- last_activity_on date,
- notified_of_own_activity boolean DEFAULT false,
- preferred_language character varying,
- email_opted_in boolean,
- email_opted_in_ip character varying,
- email_opted_in_source_id integer,
- email_opted_in_at timestamp without time zone,
- theme_id smallint,
- accepted_term_id integer,
- feed_token character varying,
- private_profile boolean DEFAULT false NOT NULL,
- roadmap_layout smallint,
- include_private_contributions boolean,
- commit_email character varying,
- group_view integer,
- managing_group_id integer,
- first_name character varying(255),
- last_name character varying(255),
- static_object_token character varying(255),
- role smallint,
- user_type smallint DEFAULT 0,
- static_object_token_encrypted text,
- otp_secret_expires_at timestamp with time zone,
- onboarding_in_progress boolean DEFAULT false NOT NULL,
- CONSTRAINT check_0dd5948e38 CHECK ((user_type IS NOT NULL)),
- CONSTRAINT check_7bde697e8e CHECK ((char_length(static_object_token_encrypted) <= 255))
-);
-
CREATE SEQUENCE users_id_seq
START WITH 1
INCREMENT BY 1
@@ -25296,7 +25475,6 @@ CREATE TABLE workspaces (
deployment_resource_version text,
personal_access_token_id bigint,
config_version integer DEFAULT 1 NOT NULL,
- force_full_reconciliation boolean DEFAULT false NOT NULL,
force_include_all_resources boolean DEFAULT true NOT NULL,
CONSTRAINT check_15543fb0fa CHECK ((char_length(name) <= 64)),
CONSTRAINT check_157d5f955c CHECK ((char_length(namespace) <= 64)),
@@ -25405,11 +25583,12 @@ ALTER SEQUENCE zentao_tracker_data_id_seq OWNED BY zentao_tracker_data.id;
CREATE TABLE zoekt_indexed_namespaces (
id bigint NOT NULL,
- zoekt_shard_id bigint NOT NULL,
+ zoekt_shard_id bigint,
namespace_id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
- search boolean DEFAULT true NOT NULL
+ search boolean DEFAULT true NOT NULL,
+ zoekt_node_id bigint
);
CREATE SEQUENCE zoekt_indexed_namespaces_id_seq
@@ -25421,6 +25600,30 @@ CREATE SEQUENCE zoekt_indexed_namespaces_id_seq
ALTER SEQUENCE zoekt_indexed_namespaces_id_seq OWNED BY zoekt_indexed_namespaces.id;
+CREATE TABLE zoekt_nodes (
+ id bigint NOT NULL,
+ uuid uuid NOT NULL,
+ used_bytes bigint DEFAULT 0 NOT NULL,
+ total_bytes bigint DEFAULT 0 NOT NULL,
+ last_seen_at timestamp with time zone DEFAULT '1970-01-01 00:00:00+00'::timestamp with time zone NOT NULL,
+ created_at timestamp with time zone NOT NULL,
+ updated_at timestamp with time zone NOT NULL,
+ index_base_url text NOT NULL,
+ search_base_url text NOT NULL,
+ metadata jsonb DEFAULT '{}'::jsonb NOT NULL,
+ CONSTRAINT check_32f39efba3 CHECK ((char_length(search_base_url) <= 1024)),
+ CONSTRAINT check_38c354a3c2 CHECK ((char_length(index_base_url) <= 1024))
+);
+
+CREATE SEQUENCE zoekt_nodes_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+ALTER SEQUENCE zoekt_nodes_id_seq OWNED BY zoekt_nodes.id;
+
CREATE TABLE zoekt_shards (
id bigint NOT NULL,
index_base_url text NOT NULL,
@@ -25864,6 +26067,8 @@ ALTER TABLE ONLY abuse_trust_scores ALTER COLUMN id SET DEFAULT nextval('abuse_t
ALTER TABLE ONLY achievements ALTER COLUMN id SET DEFAULT nextval('achievements_id_seq'::regclass);
+ALTER TABLE ONLY activity_pub_releases_subscriptions ALTER COLUMN id SET DEFAULT nextval('activity_pub_releases_subscriptions_id_seq'::regclass);
+
ALTER TABLE ONLY agent_activity_events ALTER COLUMN id SET DEFAULT nextval('agent_activity_events_id_seq'::regclass);
ALTER TABLE ONLY agent_group_authorizations ALTER COLUMN id SET DEFAULT nextval('agent_group_authorizations_id_seq'::regclass);
@@ -25906,6 +26111,14 @@ ALTER TABLE ONLY application_setting_terms ALTER COLUMN id SET DEFAULT nextval('
ALTER TABLE ONLY application_settings ALTER COLUMN id SET DEFAULT nextval('application_settings_id_seq'::regclass);
+ALTER TABLE ONLY approval_group_rules ALTER COLUMN id SET DEFAULT nextval('approval_group_rules_id_seq'::regclass);
+
+ALTER TABLE ONLY approval_group_rules_groups ALTER COLUMN id SET DEFAULT nextval('approval_group_rules_groups_id_seq'::regclass);
+
+ALTER TABLE ONLY approval_group_rules_protected_branches ALTER COLUMN id SET DEFAULT nextval('approval_group_rules_protected_branches_id_seq'::regclass);
+
+ALTER TABLE ONLY approval_group_rules_users ALTER COLUMN id SET DEFAULT nextval('approval_group_rules_users_id_seq'::regclass);
+
ALTER TABLE ONLY approval_merge_request_rule_sources ALTER COLUMN id SET DEFAULT nextval('approval_merge_request_rule_sources_id_seq'::regclass);
ALTER TABLE ONLY approval_merge_request_rules ALTER COLUMN id SET DEFAULT nextval('approval_merge_request_rules_id_seq'::regclass);
@@ -25946,6 +26159,8 @@ ALTER TABLE ONLY audit_events_streaming_event_type_filters ALTER COLUMN id SET D
ALTER TABLE ONLY audit_events_streaming_headers ALTER COLUMN id SET DEFAULT nextval('audit_events_streaming_headers_id_seq'::regclass);
+ALTER TABLE ONLY audit_events_streaming_http_group_namespace_filters ALTER COLUMN id SET DEFAULT nextval('audit_events_streaming_http_group_namespace_filters_id_seq'::regclass);
+
ALTER TABLE ONLY audit_events_streaming_instance_event_type_filters ALTER COLUMN id SET DEFAULT nextval('audit_events_streaming_instance_event_type_filters_id_seq'::regclass);
ALTER TABLE ONLY authentication_events ALTER COLUMN id SET DEFAULT nextval('authentication_events_id_seq'::regclass);
@@ -26136,6 +26351,8 @@ ALTER TABLE ONLY clusters_kubernetes_namespaces ALTER COLUMN id SET DEFAULT next
ALTER TABLE ONLY commit_user_mentions ALTER COLUMN id SET DEFAULT nextval('commit_user_mentions_id_seq'::regclass);
+ALTER TABLE ONLY compliance_framework_security_policies ALTER COLUMN id SET DEFAULT nextval('compliance_framework_security_policies_id_seq'::regclass);
+
ALTER TABLE ONLY compliance_management_frameworks ALTER COLUMN id SET DEFAULT nextval('compliance_management_frameworks_id_seq'::regclass);
ALTER TABLE ONLY container_registry_protection_rules ALTER COLUMN id SET DEFAULT nextval('container_registry_protection_rules_id_seq'::regclass);
@@ -26500,6 +26717,8 @@ ALTER TABLE ONLY ml_experiment_metadata ALTER COLUMN id SET DEFAULT nextval('ml_
ALTER TABLE ONLY ml_experiments ALTER COLUMN id SET DEFAULT nextval('ml_experiments_id_seq'::regclass);
+ALTER TABLE ONLY ml_model_metadata ALTER COLUMN id SET DEFAULT nextval('ml_model_metadata_id_seq'::regclass);
+
ALTER TABLE ONLY ml_model_versions ALTER COLUMN id SET DEFAULT nextval('ml_model_versions_id_seq'::regclass);
ALTER TABLE ONLY ml_models ALTER COLUMN id SET DEFAULT nextval('ml_models_id_seq'::regclass);
@@ -27022,6 +27241,8 @@ ALTER TABLE ONLY zentao_tracker_data ALTER COLUMN id SET DEFAULT nextval('zentao
ALTER TABLE ONLY zoekt_indexed_namespaces ALTER COLUMN id SET DEFAULT nextval('zoekt_indexed_namespaces_id_seq'::regclass);
+ALTER TABLE ONLY zoekt_nodes ALTER COLUMN id SET DEFAULT nextval('zoekt_nodes_id_seq'::regclass);
+
ALTER TABLE ONLY zoekt_shards ALTER COLUMN id SET DEFAULT nextval('zoekt_shards_id_seq'::regclass);
ALTER TABLE ONLY zoom_meetings ALTER COLUMN id SET DEFAULT nextval('zoom_meetings_id_seq'::regclass);
@@ -27632,6 +27853,9 @@ ALTER TABLE ONLY abuse_trust_scores
ALTER TABLE ONLY achievements
ADD CONSTRAINT achievements_pkey PRIMARY KEY (id);
+ALTER TABLE ONLY activity_pub_releases_subscriptions
+ ADD CONSTRAINT activity_pub_releases_subscriptions_pkey PRIMARY KEY (id);
+
ALTER TABLE ONLY agent_activity_events
ADD CONSTRAINT agent_activity_events_pkey PRIMARY KEY (id);
@@ -27704,6 +27928,18 @@ ALTER TABLE ONLY application_setting_terms
ALTER TABLE ONLY application_settings
ADD CONSTRAINT application_settings_pkey PRIMARY KEY (id);
+ALTER TABLE ONLY approval_group_rules_groups
+ ADD CONSTRAINT approval_group_rules_groups_pkey PRIMARY KEY (id);
+
+ALTER TABLE ONLY approval_group_rules
+ ADD CONSTRAINT approval_group_rules_pkey PRIMARY KEY (id);
+
+ALTER TABLE ONLY approval_group_rules_protected_branches
+ ADD CONSTRAINT approval_group_rules_protected_branches_pkey PRIMARY KEY (id);
+
+ALTER TABLE ONLY approval_group_rules_users
+ ADD CONSTRAINT approval_group_rules_users_pkey PRIMARY KEY (id);
+
ALTER TABLE ONLY approval_merge_request_rule_sources
ADD CONSTRAINT approval_merge_request_rule_sources_pkey PRIMARY KEY (id);
@@ -27770,6 +28006,9 @@ ALTER TABLE ONLY audit_events_streaming_event_type_filters
ALTER TABLE ONLY audit_events_streaming_headers
ADD CONSTRAINT audit_events_streaming_headers_pkey PRIMARY KEY (id);
+ALTER TABLE ONLY audit_events_streaming_http_group_namespace_filters
+ ADD CONSTRAINT audit_events_streaming_http_group_namespace_filters_pkey PRIMARY KEY (id);
+
ALTER TABLE ONLY audit_events_streaming_instance_event_type_filters
ADD CONSTRAINT audit_events_streaming_instance_event_type_filters_pkey PRIMARY KEY (id);
@@ -27893,8 +28132,8 @@ ALTER TABLE workspaces
ALTER TABLE vulnerability_scanners
ADD CONSTRAINT check_37608c9db5 CHECK ((char_length(vendor) <= 255)) NOT VALID;
-ALTER TABLE personal_access_tokens
- ADD CONSTRAINT check_b8d60815eb CHECK ((expires_at IS NOT NULL)) NOT VALID;
+ALTER TABLE packages_tags
+ ADD CONSTRAINT check_91b8472153 CHECK ((project_id IS NOT NULL)) NOT VALID;
ALTER TABLE sprints
ADD CONSTRAINT check_ccd8a1eae0 CHECK ((start_date IS NOT NULL)) NOT VALID;
@@ -28124,6 +28363,9 @@ ALTER TABLE ONLY clusters
ALTER TABLE ONLY commit_user_mentions
ADD CONSTRAINT commit_user_mentions_pkey PRIMARY KEY (id);
+ALTER TABLE ONLY compliance_framework_security_policies
+ ADD CONSTRAINT compliance_framework_security_policies_pkey PRIMARY KEY (id);
+
ALTER TABLE ONLY compliance_management_frameworks
ADD CONSTRAINT compliance_management_frameworks_pkey PRIMARY KEY (id);
@@ -28766,6 +29008,9 @@ ALTER TABLE ONLY ml_experiment_metadata
ALTER TABLE ONLY ml_experiments
ADD CONSTRAINT ml_experiments_pkey PRIMARY KEY (id);
+ALTER TABLE ONLY ml_model_metadata
+ ADD CONSTRAINT ml_model_metadata_pkey PRIMARY KEY (id);
+
ALTER TABLE ONLY ml_model_versions
ADD CONSTRAINT ml_model_versions_pkey PRIMARY KEY (id);
@@ -29687,6 +29932,9 @@ ALTER TABLE ONLY zentao_tracker_data
ALTER TABLE ONLY zoekt_indexed_namespaces
ADD CONSTRAINT zoekt_indexed_namespaces_pkey PRIMARY KEY (id);
+ALTER TABLE ONLY zoekt_nodes
+ ADD CONSTRAINT zoekt_nodes_pkey PRIMARY KEY (id);
+
ALTER TABLE ONLY zoekt_shards
ADD CONSTRAINT zoekt_shards_pkey PRIMARY KEY (id);
@@ -30879,8 +31127,6 @@ CREATE INDEX analytics_index_events_on_created_at_and_author_id ON events USING
CREATE INDEX analytics_repository_languages_on_project_id ON analytics_language_trend_repository_languages USING btree (project_id);
-CREATE UNIQUE INDEX any_approver_merge_request_rule_type_unique_index ON approval_merge_request_rules USING btree (merge_request_id, rule_type) WHERE (rule_type = 4);
-
CREATE UNIQUE INDEX any_approver_project_rule_type_unique_index ON approval_project_rules USING btree (project_id) WHERE (rule_type = 3);
CREATE INDEX approval_mr_rule_index_merge_request_id ON approval_merge_request_rules USING btree (merge_request_id);
@@ -30941,6 +31187,8 @@ CREATE UNIQUE INDEX i_bulk_import_trackers_id_batch_number ON bulk_import_batch_
CREATE INDEX i_compliance_frameworks_on_id_and_created_at ON compliance_management_frameworks USING btree (id, created_at, pipeline_configuration_full_path);
+CREATE INDEX i_compliance_standards_adherence_on_namespace_id_and_proj_id ON project_compliance_standards_adherence USING btree (namespace_id, project_id DESC, id DESC);
+
CREATE INDEX i_compliance_violations_for_export ON merge_requests_compliance_violations USING btree (target_project_id, id);
CREATE INDEX i_compliance_violations_on_project_id_merged_at_and_id ON merge_requests_compliance_violations USING btree (target_project_id, merged_at, id);
@@ -30999,6 +31247,8 @@ CREATE INDEX idx_build_artifacts_size_refreshes_state_updated_at ON project_buil
CREATE INDEX idx_ci_pipelines_artifacts_locked ON ci_pipelines USING btree (ci_ref_id, id) WHERE (locked = 1);
+CREATE INDEX idx_compliance_security_policies_on_policy_configuration_id ON compliance_framework_security_policies USING btree (policy_configuration_id);
+
CREATE INDEX idx_container_exp_policies_on_project_id_next_run_at ON container_expiration_policies USING btree (project_id, next_run_at) WHERE (enabled = true);
CREATE INDEX idx_container_exp_policies_on_project_id_next_run_at_enabled ON container_expiration_policies USING btree (project_id, next_run_at, enabled);
@@ -31079,6 +31329,18 @@ CREATE INDEX idx_mrs_on_target_id_and_created_at_and_state_id ON merge_requests
CREATE UNIQUE INDEX idx_namespace_settings_on_default_compliance_framework_id ON namespace_settings USING btree (default_compliance_framework_id);
+CREATE UNIQUE INDEX idx_on_approval_group_rules_any_approver_type ON approval_group_rules USING btree (group_id, rule_type) WHERE (rule_type = 4);
+
+CREATE UNIQUE INDEX idx_on_approval_group_rules_group_id_type_name ON approval_group_rules USING btree (group_id, rule_type, name);
+
+CREATE UNIQUE INDEX idx_on_approval_group_rules_groups_rule_group ON approval_group_rules_groups USING btree (approval_group_rule_id, group_id);
+
+CREATE UNIQUE INDEX idx_on_approval_group_rules_protected_branch ON approval_group_rules_protected_branches USING btree (approval_group_rule_id, protected_branch_id);
+
+CREATE INDEX idx_on_approval_group_rules_security_orch_policy ON approval_group_rules USING btree (security_orchestration_policy_configuration_id);
+
+CREATE UNIQUE INDEX idx_on_approval_group_rules_users_rule_user ON approval_group_rules_users USING btree (approval_group_rule_id, user_id);
+
CREATE UNIQUE INDEX idx_on_compliance_management_frameworks_namespace_id_name ON compliance_management_frameworks USING btree (namespace_id, name);
CREATE UNIQUE INDEX idx_on_external_approval_rules_project_id_external_url ON external_approval_rules USING btree (project_id, external_url);
@@ -31089,6 +31351,8 @@ CREATE UNIQUE INDEX idx_on_external_status_checks_project_id_external_url ON ext
CREATE UNIQUE INDEX idx_on_external_status_checks_project_id_name ON external_status_checks USING btree (project_id, name);
+CREATE INDEX idx_on_protected_branch ON approval_group_rules_protected_branches USING btree (protected_branch_id);
+
CREATE INDEX idx_open_issues_on_project_and_confidential_and_author_and_id ON issues USING btree (project_id, confidential, author_id, id) WHERE (state_id = 1);
CREATE INDEX idx_packages_debian_group_component_files_on_architecture_id ON packages_debian_group_component_files USING btree (architecture_id);
@@ -31121,6 +31385,8 @@ CREATE UNIQUE INDEX idx_pkgs_dep_links_on_pkg_id_dependency_id_dependency_type O
CREATE INDEX idx_pkgs_installable_package_files_on_package_id_id_file_name ON packages_package_files USING btree (package_id, id, file_name) WHERE (status = 0);
+CREATE INDEX idx_pkgs_npm_metadata_caches_on_id_and_project_id_and_status ON packages_npm_metadata_caches USING btree (id) WHERE ((project_id IS NULL) AND (status = 0));
+
CREATE INDEX idx_proj_feat_usg_on_jira_dvcs_cloud_last_sync_at_and_proj_id ON project_feature_usages USING btree (jira_dvcs_cloud_last_sync_at, project_id) WHERE (jira_dvcs_cloud_last_sync_at IS NOT NULL);
CREATE INDEX idx_proj_feat_usg_on_jira_dvcs_server_last_sync_at_and_proj_id ON project_feature_usages USING btree (jira_dvcs_server_last_sync_at, project_id) WHERE (jira_dvcs_server_last_sync_at IS NOT NULL);
@@ -31163,6 +31429,8 @@ CREATE INDEX idx_security_scans_on_scan_type ON security_scans USING btree (scan
CREATE UNIQUE INDEX idx_software_license_policies_unique_on_project_and_scan_policy ON software_license_policies USING btree (project_id, software_license_id, scan_result_policy_id);
+CREATE INDEX idx_status_check_responses_on_id_and_status ON status_check_responses USING btree (id, status);
+
CREATE INDEX idx_streaming_headers_on_external_audit_event_destination_id ON audit_events_streaming_headers USING btree (external_audit_event_destination_id);
CREATE INDEX idx_test_reports_on_issue_id_created_at_and_id ON requirements_management_test_reports USING btree (issue_id, created_at, id);
@@ -31229,6 +31497,10 @@ CREATE INDEX index_abuse_trust_scores_on_user_id_and_source_and_created_at ON ab
CREATE UNIQUE INDEX "index_achievements_on_namespace_id_LOWER_name" ON achievements USING btree (namespace_id, lower(name));
+CREATE UNIQUE INDEX index_activity_pub_releases_sub_on_project_id_inbox_url ON activity_pub_releases_subscriptions USING btree (project_id, lower(subscriber_inbox_url));
+
+CREATE UNIQUE INDEX index_activity_pub_releases_sub_on_project_id_sub_url ON activity_pub_releases_subscriptions USING btree (project_id, lower(subscriber_url));
+
CREATE INDEX index_agent_activity_events_on_agent_id_and_recorded_at_and_id ON agent_activity_events USING btree (agent_id, recorded_at, id);
CREATE INDEX index_agent_activity_events_on_agent_token_id ON agent_activity_events USING btree (agent_token_id) WHERE (agent_token_id IS NOT NULL);
@@ -31283,8 +31555,6 @@ CREATE INDEX index_allowed_email_domains_on_group_id ON allowed_email_domains US
CREATE INDEX index_analytics_ca_group_stages_on_end_event_label_id ON analytics_cycle_analytics_group_stages USING btree (end_event_label_id);
-CREATE INDEX index_analytics_ca_group_stages_on_group_id ON analytics_cycle_analytics_group_stages USING btree (group_id);
-
CREATE INDEX index_analytics_ca_group_stages_on_relative_position ON analytics_cycle_analytics_group_stages USING btree (relative_position);
CREATE INDEX index_analytics_ca_group_stages_on_start_event_label_id ON analytics_cycle_analytics_group_stages USING btree (start_event_label_id);
@@ -31311,6 +31581,12 @@ CREATE INDEX index_application_settings_on_usage_stats_set_by_user_id ON applica
CREATE INDEX index_applicationsettings_on_instance_administration_project_id ON application_settings USING btree (instance_administration_project_id);
+CREATE INDEX index_approval_group_rules_groups_on_group_id ON approval_group_rules_groups USING btree (group_id);
+
+CREATE INDEX index_approval_group_rules_on_scan_result_policy_id ON approval_group_rules USING btree (scan_result_policy_id);
+
+CREATE INDEX index_approval_group_rules_users_on_user_id ON approval_group_rules_users USING btree (user_id);
+
CREATE UNIQUE INDEX index_approval_merge_request_rule_sources_1 ON approval_merge_request_rule_sources USING btree (approval_merge_request_rule_id);
CREATE INDEX index_approval_merge_request_rule_sources_2 ON approval_merge_request_rule_sources USING btree (approval_project_rule_id);
@@ -31519,8 +31795,14 @@ CREATE INDEX index_catalog_resource_versions_on_project_id ON catalog_resource_v
CREATE UNIQUE INDEX index_catalog_resource_versions_on_release_id ON catalog_resource_versions USING btree (release_id);
+CREATE INDEX index_catalog_resources_on_description_trigram ON catalog_resources USING gin (description gin_trgm_ops);
+
+CREATE INDEX index_catalog_resources_on_name_trigram ON catalog_resources USING gin (name gin_trgm_ops);
+
CREATE UNIQUE INDEX index_catalog_resources_on_project_id ON catalog_resources USING btree (project_id);
+CREATE INDEX index_catalog_resources_on_state ON catalog_resources USING btree (state);
+
CREATE INDEX index_chat_names_on_team_id_and_chat_id ON chat_names USING btree (team_id, chat_id);
CREATE INDEX index_chat_names_on_user_id ON chat_names USING btree (user_id);
@@ -31739,12 +32021,8 @@ CREATE INDEX index_ci_pipeline_chat_data_on_chat_name_id ON ci_pipeline_chat_dat
CREATE UNIQUE INDEX index_ci_pipeline_chat_data_on_pipeline_id ON ci_pipeline_chat_data USING btree (pipeline_id);
-CREATE UNIQUE INDEX index_ci_pipeline_chat_data_on_pipeline_id_convert_to_bigint ON ci_pipeline_chat_data USING btree (pipeline_id_convert_to_bigint);
-
CREATE INDEX index_ci_pipeline_messages_on_pipeline_id ON ci_pipeline_messages USING btree (pipeline_id);
-CREATE INDEX index_ci_pipeline_messages_on_pipeline_id_convert_to_bigint ON ci_pipeline_messages USING btree (pipeline_id_convert_to_bigint);
-
CREATE INDEX index_ci_pipeline_metadata_on_project_id ON ci_pipeline_metadata USING btree (project_id);
CREATE UNIQUE INDEX index_ci_pipeline_schedule_variables_on_schedule_id_and_key ON ci_pipeline_schedule_variables USING btree (pipeline_schedule_id, key);
@@ -31831,6 +32109,12 @@ CREATE INDEX index_ci_runner_machines_on_contacted_at_desc_and_id_desc ON ci_run
CREATE INDEX index_ci_runner_machines_on_created_at_and_id_desc ON ci_runner_machines USING btree (created_at, id DESC);
+CREATE INDEX index_ci_runner_machines_on_major_version_trigram ON ci_runner_machines USING btree ("substring"(version, '^\d+\.'::text), version, runner_id);
+
+CREATE INDEX index_ci_runner_machines_on_minor_version_trigram ON ci_runner_machines USING btree ("substring"(version, '^\d+\.\d+\.'::text), version, runner_id);
+
+CREATE INDEX index_ci_runner_machines_on_patch_version_trigram ON ci_runner_machines USING btree ("substring"(version, '^\d+\.\d+\.\d+'::text), version, runner_id);
+
CREATE UNIQUE INDEX index_ci_runner_machines_on_runner_id_and_system_xid ON ci_runner_machines USING btree (runner_id, system_xid);
CREATE INDEX index_ci_runner_machines_on_version ON ci_runner_machines USING btree (version);
@@ -31893,8 +32177,6 @@ CREATE INDEX index_ci_secure_files_on_project_id ON ci_secure_files USING btree
CREATE INDEX index_ci_sources_pipelines_on_pipeline_id ON ci_sources_pipelines USING btree (pipeline_id);
-CREATE INDEX index_ci_sources_pipelines_on_pipeline_id_bigint ON ci_sources_pipelines USING btree (pipeline_id_convert_to_bigint);
-
CREATE INDEX index_ci_sources_pipelines_on_project_id ON ci_sources_pipelines USING btree (project_id);
CREATE INDEX index_ci_sources_pipelines_on_source_job_id ON ci_sources_pipelines USING btree (source_job_id);
@@ -31903,8 +32185,6 @@ CREATE INDEX index_ci_sources_pipelines_on_source_partition_id_source_job_id ON
CREATE INDEX index_ci_sources_pipelines_on_source_pipeline_id ON ci_sources_pipelines USING btree (source_pipeline_id);
-CREATE INDEX index_ci_sources_pipelines_on_source_pipeline_id_bigint ON ci_sources_pipelines USING btree (source_pipeline_id_convert_to_bigint);
-
CREATE INDEX index_ci_sources_pipelines_on_source_project_id ON ci_sources_pipelines USING btree (source_project_id);
CREATE INDEX index_ci_sources_projects_on_pipeline_id ON ci_sources_projects USING btree (pipeline_id);
@@ -32293,6 +32573,8 @@ CREATE INDEX index_environments_on_state_and_auto_delete_at ON environments USIN
CREATE INDEX index_environments_on_state_and_auto_stop_at ON environments USING btree (state, auto_stop_at) WHERE ((auto_stop_at IS NOT NULL) AND ((state)::text = 'available'::text));
+CREATE INDEX index_environments_on_updated_at_for_stopping_state ON environments USING btree (updated_at) WHERE ((state)::text = 'stopping'::text);
+
CREATE UNIQUE INDEX index_epic_board_list_preferences_on_user_and_list ON boards_epic_list_user_preferences USING btree (user_id, epic_list_id);
CREATE UNIQUE INDEX index_epic_board_recent_visits_on_user_group_and_board ON boards_epic_board_recent_visits USING btree (user_id, group_id, epic_board_id);
@@ -32633,8 +32915,6 @@ CREATE INDEX index_imported_projects_on_import_type_id ON projects USING btree (
CREATE INDEX index_in_product_marketing_emails_on_track_series_id_clicked ON in_product_marketing_emails USING btree (track, series, id, cta_clicked_at);
-CREATE UNIQUE INDEX index_in_product_marketing_emails_on_user_campaign ON in_product_marketing_emails USING btree (user_id, campaign);
-
CREATE INDEX index_in_product_marketing_emails_on_user_id ON in_product_marketing_emails USING btree (user_id);
CREATE UNIQUE INDEX index_in_product_marketing_emails_on_user_track_series ON in_product_marketing_emails USING btree (user_id, track, series);
@@ -32929,8 +33209,6 @@ CREATE INDEX index_members_on_user_id_and_access_level_requested_at_is_null ON m
CREATE INDEX index_members_on_user_id_created_at ON members USING btree (user_id, created_at) WHERE ((ldap = true) AND ((type)::text = 'GroupMember'::text) AND ((source_type)::text = 'Namespace'::text));
-CREATE INDEX index_merge_request_assignees_on_merge_request_id ON merge_request_assignees USING btree (merge_request_id);
-
CREATE UNIQUE INDEX index_merge_request_assignees_on_merge_request_id_and_user_id ON merge_request_assignees USING btree (merge_request_id, user_id);
CREATE INDEX index_merge_request_assignees_on_user_id ON merge_request_assignees USING btree (user_id);
@@ -32983,8 +33261,6 @@ CREATE INDEX index_merge_request_metrics_on_merged_by_id ON merge_request_metric
CREATE INDEX index_merge_request_metrics_on_pipeline_id ON merge_request_metrics USING btree (pipeline_id);
-CREATE INDEX index_merge_request_metrics_on_target_project_id ON merge_request_metrics USING btree (target_project_id);
-
CREATE INDEX index_merge_request_review_llm_summaries_on_mr_diff_id ON merge_request_review_llm_summaries USING btree (merge_request_diff_id);
CREATE INDEX index_merge_request_review_llm_summaries_on_review_id ON merge_request_review_llm_summaries USING btree (review_id);
@@ -33107,6 +33383,8 @@ CREATE INDEX index_ml_candidates_on_ci_build_id ON ml_candidates USING btree (ci
CREATE UNIQUE INDEX index_ml_candidates_on_experiment_id_and_eid ON ml_candidates USING btree (experiment_id, eid);
+CREATE UNIQUE INDEX index_ml_candidates_on_model_version_id ON ml_candidates USING btree (model_version_id);
+
CREATE INDEX index_ml_candidates_on_package_id ON ml_candidates USING btree (package_id);
CREATE INDEX index_ml_candidates_on_project_id ON ml_candidates USING btree (project_id);
@@ -33135,6 +33413,8 @@ CREATE INDEX index_ml_models_on_project_id ON ml_models USING btree (project_id)
CREATE UNIQUE INDEX index_ml_models_on_project_id_and_name ON ml_models USING btree (project_id, name);
+CREATE INDEX index_ml_models_on_user_id ON ml_models USING btree (user_id);
+
CREATE UNIQUE INDEX index_mr_blocks_on_blocking_and_blocked_mr_ids ON merge_request_blocks USING btree (blocking_merge_request_id, blocked_merge_request_id);
CREATE INDEX index_mr_cleanup_schedules_timestamps_status ON merge_request_cleanup_schedules USING btree (scheduled_at) WHERE ((completed_at IS NULL) AND (status = 0));
@@ -33193,8 +33473,6 @@ CREATE INDEX index_namespaces_on_require_two_factor_authentication ON namespaces
CREATE UNIQUE INDEX index_namespaces_on_runners_token_encrypted ON namespaces USING btree (runners_token_encrypted);
-CREATE INDEX index_namespaces_on_shared_and_extra_runners_minutes_limit ON namespaces USING btree (shared_runners_minutes_limit, extra_shared_runners_minutes_limit);
-
CREATE INDEX index_namespaces_on_traversal_ids ON namespaces USING gin (traversal_ids);
CREATE INDEX index_namespaces_on_traversal_ids_for_groups ON namespaces USING gin (traversal_ids) WHERE ((type)::text = 'Group'::text);
@@ -33307,8 +33585,6 @@ CREATE UNIQUE INDEX index_on_project_id_escalation_policy_name_unique ON inciden
CREATE INDEX index_on_projects_lower_path ON projects USING btree (lower((path)::text));
-CREATE INDEX index_on_projects_path ON projects USING btree (path);
-
CREATE INDEX index_on_routes_lower_path ON routes USING btree (lower((path)::text));
CREATE INDEX index_on_sbom_sources_package_manager_name ON sbom_sources USING btree ((((source -> 'package_manager'::text) ->> 'name'::text)));
@@ -33473,6 +33749,8 @@ CREATE INDEX index_packages_tags_on_package_id ON packages_tags USING btree (pac
CREATE INDEX index_packages_tags_on_package_id_and_updated_at ON packages_tags USING btree (package_id, updated_at DESC);
+CREATE INDEX index_packages_tags_on_project_id ON packages_tags USING btree (project_id);
+
CREATE INDEX index_pages_deployment_states_failed_verification ON pages_deployment_states USING btree (verification_retry_at NULLS FIRST) WHERE (verification_state = 3);
CREATE INDEX index_pages_deployment_states_needs_verification ON pages_deployment_states USING btree (verification_state) WHERE ((verification_state = 0) OR (verification_state = 3));
@@ -33587,8 +33865,6 @@ CREATE INDEX index_project_compliance_framework_settings_on_framework_id ON proj
CREATE INDEX index_project_compliance_framework_settings_on_project_id ON project_compliance_framework_settings USING btree (project_id);
-CREATE INDEX index_project_compliance_standards_adherence_on_namespace_id ON project_compliance_standards_adherence USING btree (namespace_id);
-
CREATE INDEX index_project_compliance_standards_adherence_on_project_id ON project_compliance_standards_adherence USING btree (project_id);
CREATE INDEX index_project_custom_attributes_on_key_and_value ON project_custom_attributes USING btree (key, value);
@@ -34049,8 +34325,6 @@ CREATE UNIQUE INDEX index_sbom_occurrences_on_uuid ON sbom_occurrences USING btr
CREATE UNIQUE INDEX index_sbom_sources_on_source_type_and_source ON sbom_sources USING btree (source_type, source);
-CREATE INDEX index_scan_result_policies_on_policy_configuration_id ON scan_result_policies USING btree (security_orchestration_policy_configuration_id);
-
CREATE UNIQUE INDEX index_scan_result_policies_on_position_in_configuration ON scan_result_policies USING btree (security_orchestration_policy_configuration_id, project_id, orchestration_policy_idx, rule_idx);
CREATE INDEX index_scan_result_policies_on_project_id ON scan_result_policies USING btree (project_id);
@@ -34163,6 +34437,8 @@ CREATE INDEX index_snippet_repositories_verification_state ON snippet_repositori
CREATE INDEX index_snippet_repository_storage_moves_on_snippet_id ON snippet_repository_storage_moves USING btree (snippet_id);
+CREATE INDEX index_snippet_repository_storage_moves_on_state ON snippet_repository_storage_moves USING btree (state) WHERE (state = ANY (ARRAY[2, 3]));
+
CREATE UNIQUE INDEX index_snippet_user_mentions_on_note_id ON snippet_user_mentions USING btree (note_id) WHERE (note_id IS NOT NULL);
CREATE INDEX index_snippets_on_author_id ON snippets USING btree (author_id);
@@ -34203,6 +34479,8 @@ CREATE INDEX index_sop_schedules_on_sop_configuration_id ON security_orchestrati
CREATE INDEX index_sop_schedules_on_user_id ON security_orchestration_policy_rule_schedules USING btree (user_id);
+CREATE UNIQUE INDEX index_source_package_names_on_component_and_purl ON sbom_components USING btree (component_type, source_package_name, purl_type);
+
CREATE INDEX index_spam_logs_on_user_id ON spam_logs USING btree (user_id);
CREATE INDEX index_sprints_iterations_cadence_id ON sprints USING btree (iterations_cadence_id);
@@ -34411,7 +34689,7 @@ CREATE INDEX index_user_custom_attributes_on_key_and_value ON user_custom_attrib
CREATE UNIQUE INDEX index_user_custom_attributes_on_user_id_and_key ON user_custom_attributes USING btree (user_id, key);
-CREATE INDEX index_user_details_on_enterprise_group_id ON user_details USING btree (enterprise_group_id);
+CREATE INDEX index_user_details_on_enterprise_group_id_and_user_id ON user_details USING btree (enterprise_group_id, user_id);
CREATE INDEX index_user_details_on_password_last_changed_at ON user_details USING btree (password_last_changed_at);
@@ -34433,6 +34711,8 @@ CREATE INDEX index_user_namespace_callouts_on_namespace_id ON user_namespace_cal
CREATE INDEX index_user_permission_export_uploads_on_user_id_and_status ON user_permission_export_uploads USING btree (user_id, status);
+CREATE INDEX index_user_phone_number_validations_on_telesign_reference_xid ON user_phone_number_validations USING btree (telesign_reference_xid);
+
CREATE INDEX index_user_phone_validations_on_dial_code_phone_number ON user_phone_number_validations USING btree (international_dial_code, phone_number);
CREATE INDEX index_user_preferences_on_gitpod_enabled ON user_preferences USING btree (gitpod_enabled);
@@ -34523,8 +34803,6 @@ CREATE UNIQUE INDEX index_verification_codes_on_phone_and_visitor_id_code ON ONL
COMMENT ON INDEX index_verification_codes_on_phone_and_visitor_id_code IS 'JiHu-specific index';
-CREATE INDEX index_vs_code_settings_on_user_id ON vs_code_settings USING btree (user_id);
-
CREATE UNIQUE INDEX index_vuln_findings_on_uuid_including_vuln_id_1 ON vulnerability_occurrences USING btree (uuid) INCLUDE (vulnerability_id);
CREATE UNIQUE INDEX index_vuln_historical_statistics_on_project_id_and_date ON vulnerability_historical_statistics USING btree (project_id, date);
@@ -34783,6 +35061,16 @@ CREATE INDEX index_zentao_tracker_data_on_integration_id ON zentao_tracker_data
CREATE INDEX index_zoekt_indexed_namespaces_on_namespace_id ON zoekt_indexed_namespaces USING btree (namespace_id);
+CREATE UNIQUE INDEX index_zoekt_node_and_namespace ON zoekt_indexed_namespaces USING btree (zoekt_node_id, namespace_id);
+
+CREATE UNIQUE INDEX index_zoekt_nodes_on_index_base_url ON zoekt_nodes USING btree (index_base_url);
+
+CREATE INDEX index_zoekt_nodes_on_last_seen_at ON zoekt_nodes USING btree (last_seen_at);
+
+CREATE UNIQUE INDEX index_zoekt_nodes_on_search_base_url ON zoekt_nodes USING btree (search_base_url);
+
+CREATE UNIQUE INDEX index_zoekt_nodes_on_uuid ON zoekt_nodes USING btree (uuid);
+
CREATE UNIQUE INDEX index_zoekt_shard_and_namespace ON zoekt_indexed_namespaces USING btree (zoekt_shard_id, namespace_id);
CREATE UNIQUE INDEX index_zoekt_shards_on_index_base_url ON zoekt_shards USING btree (index_base_url);
@@ -34837,8 +35125,6 @@ CREATE UNIQUE INDEX partial_index_bulk_import_exports_on_project_id_and_relation
CREATE INDEX partial_index_ci_builds_on_scheduled_at_with_scheduled_jobs ON ci_builds USING btree (scheduled_at) WHERE ((scheduled_at IS NOT NULL) AND ((type)::text = 'Ci::Build'::text) AND ((status)::text = 'scheduled'::text));
-CREATE INDEX partial_index_deployments_for_legacy_successful_deployments ON deployments USING btree (id) WHERE ((finished_at IS NULL) AND (status = 2));
-
CREATE INDEX partial_index_slack_integrations_with_bot_user_id ON slack_integrations USING btree (id) WHERE (bot_user_id IS NOT NULL);
CREATE UNIQUE INDEX partial_index_sop_configs_on_namespace_id ON security_orchestration_policy_configurations USING btree (namespace_id) WHERE (namespace_id IS NOT NULL);
@@ -34889,7 +35175,11 @@ CREATE INDEX tmp_idx_orphaned_approval_merge_request_rules ON approval_merge_req
CREATE INDEX tmp_idx_orphaned_approval_project_rules ON approval_project_rules USING btree (id) WHERE ((report_type = ANY (ARRAY[2, 4])) AND (security_orchestration_policy_configuration_id IS NULL));
-CREATE INDEX tmp_idx_packages_on_project_id_when_npm_not_pending_destruction ON packages_packages USING btree (project_id) WHERE ((package_type = 2) AND (status <> 4));
+CREATE INDEX tmp_idx_protected_branch_merge_access_levels_on_id_with_group ON protected_branch_merge_access_levels USING btree (id) WHERE (group_id IS NOT NULL);
+
+CREATE INDEX tmp_idx_protected_branch_push_access_levels_on_id_with_group ON protected_branch_push_access_levels USING btree (id) WHERE (group_id IS NOT NULL);
+
+CREATE INDEX tmp_idx_protected_tag_create_access_levels_on_id_with_group ON protected_tag_create_access_levels USING btree (id) WHERE (group_id IS NOT NULL);
CREATE INDEX tmp_index_ci_job_artifacts_on_expire_at_where_locked_unknown ON ci_job_artifacts USING btree (expire_at, job_id) WHERE ((locked = 2) AND (expire_at IS NOT NULL));
@@ -34941,10 +35231,16 @@ CREATE UNIQUE INDEX unique_amazon_s3_configurations_namespace_id_and_name ON aud
CREATE UNIQUE INDEX unique_any_approver_merge_request_rule_type_post_merge ON approval_merge_request_rules USING btree (merge_request_id, rule_type, applicable_post_merge) WHERE (rule_type = 4);
+CREATE UNIQUE INDEX unique_audit_events_group_namespace_filters_destination_id ON audit_events_streaming_http_group_namespace_filters USING btree (external_audit_event_destination_id);
+
+CREATE UNIQUE INDEX unique_audit_events_group_namespace_filters_namespace_id ON audit_events_streaming_http_group_namespace_filters USING btree (namespace_id);
+
CREATE UNIQUE INDEX unique_batched_background_migrations_queued_migration_version ON batched_background_migrations USING btree (queued_migration_version);
CREATE UNIQUE INDEX unique_ci_builds_token_encrypted_and_partition_id ON ci_builds USING btree (token_encrypted, partition_id) WHERE (token_encrypted IS NOT NULL);
+CREATE UNIQUE INDEX unique_compliance_framework_security_policies_framework_id ON compliance_framework_security_policies USING btree (framework_id, policy_configuration_id, policy_index);
+
CREATE UNIQUE INDEX unique_external_audit_event_destination_namespace_id_and_name ON audit_events_external_audit_event_destinations USING btree (namespace_id, name);
CREATE UNIQUE INDEX unique_google_cloud_logging_configurations_on_namespace_id ON audit_events_google_cloud_logging_configurations USING btree (namespace_id, google_project_id_name, log_id_name);
@@ -34955,6 +35251,8 @@ CREATE UNIQUE INDEX unique_index_ci_build_pending_states_on_partition_id_build_i
CREATE UNIQUE INDEX unique_index_for_project_pages_unique_domain ON project_settings USING btree (pages_unique_domain) WHERE (pages_unique_domain IS NOT NULL);
+CREATE UNIQUE INDEX unique_index_ml_model_metadata_name ON ml_model_metadata USING btree (model_id, name);
+
CREATE UNIQUE INDEX unique_index_on_system_note_metadata_id ON resource_link_events USING btree (system_note_metadata_id);
CREATE UNIQUE INDEX unique_index_sysaccess_ms_access_tokens_on_sysaccess_ms_app_id ON system_access_microsoft_graph_access_tokens USING btree (system_access_microsoft_application_id);
@@ -34985,6 +35283,8 @@ CREATE UNIQUE INDEX unique_streaming_event_type_filters_destination_id ON audit_
CREATE UNIQUE INDEX unique_streaming_instance_event_type_filters_destination_id ON audit_events_streaming_instance_event_type_filters USING btree (instance_external_audit_event_destination_id, audit_event_type);
+CREATE UNIQUE INDEX unique_user_id_and_setting_type ON vs_code_settings USING btree (user_id, setting_type);
+
CREATE UNIQUE INDEX unique_vuln_merge_request_link_vuln_id_and_mr_id ON vulnerability_merge_request_links USING btree (vulnerability_id, merge_request_id);
CREATE UNIQUE INDEX unique_zoekt_shards_uuid ON zoekt_shards USING btree (uuid);
@@ -36627,24 +36927,18 @@ CREATE TRIGGER tags_loose_fk_trigger AFTER DELETE ON tags REFERENCING OLD TABLE
CREATE TRIGGER trigger_07bc3c48f407 BEFORE INSERT OR UPDATE ON ci_stages FOR EACH ROW EXECUTE FUNCTION trigger_07bc3c48f407();
-CREATE TRIGGER trigger_1bd97da9c1a4 BEFORE INSERT OR UPDATE ON ci_pipelines FOR EACH ROW EXECUTE FUNCTION trigger_1bd97da9c1a4();
-
-CREATE TRIGGER trigger_239c8032a8d6 BEFORE INSERT OR UPDATE ON ci_pipeline_chat_data FOR EACH ROW EXECUTE FUNCTION trigger_239c8032a8d6();
+CREATE TRIGGER trigger_10ee1357e825 BEFORE INSERT OR UPDATE ON p_ci_builds FOR EACH ROW EXECUTE FUNCTION trigger_10ee1357e825();
-CREATE TRIGGER trigger_68d7b6653c7d BEFORE INSERT OR UPDATE ON ci_sources_pipelines FOR EACH ROW EXECUTE FUNCTION trigger_68d7b6653c7d();
+CREATE TRIGGER trigger_1bd97da9c1a4 BEFORE INSERT OR UPDATE ON ci_pipelines FOR EACH ROW EXECUTE FUNCTION trigger_1bd97da9c1a4();
CREATE TRIGGER trigger_7f3d66a7d7f5 BEFORE INSERT OR UPDATE ON ci_pipeline_variables FOR EACH ROW EXECUTE FUNCTION trigger_7f3d66a7d7f5();
CREATE TRIGGER trigger_b2d852e1e2cb BEFORE INSERT OR UPDATE ON ci_pipelines FOR EACH ROW EXECUTE FUNCTION trigger_b2d852e1e2cb();
-CREATE TRIGGER trigger_bbb95b2d6929 BEFORE INSERT OR UPDATE ON ci_project_monthly_usages FOR EACH ROW EXECUTE FUNCTION trigger_bbb95b2d6929();
-
-CREATE TRIGGER trigger_bfad0e2b9c86 BEFORE INSERT OR UPDATE ON ci_pipeline_messages FOR EACH ROW EXECUTE FUNCTION trigger_bfad0e2b9c86();
-
-CREATE TRIGGER trigger_c0353bbb6145 BEFORE INSERT OR UPDATE ON ci_namespace_monthly_usages FOR EACH ROW EXECUTE FUNCTION trigger_c0353bbb6145();
-
CREATE TRIGGER trigger_delete_project_namespace_on_project_delete AFTER DELETE ON projects FOR EACH ROW WHEN ((old.project_namespace_id IS NOT NULL)) EXECUTE FUNCTION delete_associated_project_namespace();
+CREATE TRIGGER trigger_eaec934fe6b2 BEFORE INSERT OR UPDATE ON system_note_metadata FOR EACH ROW EXECUTE FUNCTION trigger_eaec934fe6b2();
+
CREATE TRIGGER trigger_has_external_issue_tracker_on_delete AFTER DELETE ON integrations FOR EACH ROW WHEN ((((old.category)::text = 'issue_tracker'::text) AND (old.active = true) AND (old.project_id IS NOT NULL))) EXECUTE FUNCTION set_has_external_issue_tracker();
CREATE TRIGGER trigger_has_external_issue_tracker_on_insert AFTER INSERT ON integrations FOR EACH ROW WHEN ((((new.category)::text = 'issue_tracker'::text) AND (new.active = true) AND (new.project_id IS NOT NULL))) EXECUTE FUNCTION set_has_external_issue_tracker();
@@ -36733,15 +37027,15 @@ ALTER TABLE ONLY user_interacted_projects
ALTER TABLE ONLY merge_request_assignment_events
ADD CONSTRAINT fk_08f7602bfd FOREIGN KEY (merge_request_id) REFERENCES merge_requests(id) ON DELETE CASCADE;
-ALTER TABLE ONLY ci_pipeline_messages
- ADD CONSTRAINT fk_0946fea681 FOREIGN KEY (pipeline_id_convert_to_bigint) REFERENCES ci_pipelines(id) ON DELETE CASCADE;
-
ALTER TABLE ONLY remote_development_agent_configs
ADD CONSTRAINT fk_0a3c0ada56 FOREIGN KEY (cluster_agent_id) REFERENCES cluster_agents(id) ON DELETE CASCADE;
ALTER TABLE ONLY dast_sites
ADD CONSTRAINT fk_0a57f2271b FOREIGN KEY (dast_site_validation_id) REFERENCES dast_site_validations(id) ON DELETE SET NULL;
+ALTER TABLE ONLY approval_group_rules_protected_branches
+ ADD CONSTRAINT fk_0b85e6c388 FOREIGN KEY (protected_branch_id) REFERENCES protected_branches(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY issue_customer_relations_contacts
ADD CONSTRAINT fk_0c0037f723 FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE;
@@ -36772,6 +37066,9 @@ ALTER TABLE ONLY vulnerabilities
ALTER TABLE ONLY vulnerabilities
ADD CONSTRAINT fk_131d289c65 FOREIGN KEY (milestone_id) REFERENCES milestones(id) ON DELETE SET NULL;
+ALTER TABLE ONLY approval_group_rules
+ ADD CONSTRAINT fk_1485c451e3 FOREIGN KEY (scan_result_policy_id) REFERENCES scan_result_policies(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY catalog_resource_versions
ADD CONSTRAINT fk_15376d917e FOREIGN KEY (release_id) REFERENCES releases(id) ON DELETE CASCADE;
@@ -36814,9 +37111,6 @@ ALTER TABLE ONLY agent_project_authorizations
ALTER TABLE ONLY vulnerabilities
ADD CONSTRAINT fk_1d37cddf91 FOREIGN KEY (epic_id) REFERENCES epics(id) ON DELETE SET NULL;
-ALTER TABLE ONLY ci_sources_pipelines
- ADD CONSTRAINT fk_1df371767f FOREIGN KEY (source_pipeline_id_convert_to_bigint) REFERENCES ci_pipelines(id) ON DELETE CASCADE;
-
ALTER TABLE ONLY boards
ADD CONSTRAINT fk_1e9a074a35 FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
@@ -37021,12 +37315,18 @@ ALTER TABLE ONLY user_achievements
ALTER TABLE ONLY vulnerability_reads
ADD CONSTRAINT fk_4f593f6c62 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE;
+ALTER TABLE ONLY approval_group_rules_protected_branches
+ ADD CONSTRAINT fk_4f85f13b20 FOREIGN KEY (approval_group_rule_id) REFERENCES approval_group_rules(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY project_compliance_standards_adherence
ADD CONSTRAINT fk_4fd1d9d9b0 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE SET NULL;
ALTER TABLE ONLY vulnerability_reads
ADD CONSTRAINT fk_5001652292 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
+ALTER TABLE ONLY approval_group_rules_groups
+ ADD CONSTRAINT fk_50edc8134e FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY alert_management_alerts
ADD CONSTRAINT fk_51ab4b6089 FOREIGN KEY (prometheus_alert_id) REFERENCES prometheus_alerts(id) ON DELETE CASCADE;
@@ -37057,8 +37357,8 @@ ALTER TABLE ONLY approval_merge_request_rules
ALTER TABLE ONLY deploy_keys_projects
ADD CONSTRAINT fk_58a901ca7e FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
-ALTER TABLE ONLY ci_pipeline_chat_data
- ADD CONSTRAINT fk_5b21bde562 FOREIGN KEY (pipeline_id_convert_to_bigint) REFERENCES ci_pipelines(id) ON DELETE CASCADE;
+ALTER TABLE ONLY packages_tags
+ ADD CONSTRAINT fk_5a230894f6 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY dependency_list_exports
ADD CONSTRAINT fk_5b3d11e1ef FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL;
@@ -37105,6 +37405,9 @@ ALTER TABLE ONLY vulnerability_reads
ALTER TABLE ONLY merge_requests
ADD CONSTRAINT fk_641731faff FOREIGN KEY (updated_by_id) REFERENCES users(id) ON DELETE SET NULL;
+ALTER TABLE ONLY approval_group_rules
+ ADD CONSTRAINT fk_64450bea52 FOREIGN KEY (security_orchestration_policy_configuration_id) REFERENCES security_orchestration_policy_configurations(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY ci_pipeline_chat_data
ADD CONSTRAINT fk_64ebfab6b3 FOREIGN KEY (pipeline_id) REFERENCES ci_pipelines(id) ON DELETE CASCADE;
@@ -37117,6 +37420,9 @@ ALTER TABLE ONLY ci_pipelines
ALTER TABLE ONLY merge_requests
ADD CONSTRAINT fk_6a5165a692 FOREIGN KEY (milestone_id) REFERENCES milestones(id) ON DELETE SET NULL;
+ALTER TABLE ONLY ml_models
+ ADD CONSTRAINT fk_6c95e61a6e FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL;
+
ALTER TABLE ONLY projects
ADD CONSTRAINT fk_6ca23af0a3 FOREIGN KEY (project_namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE;
@@ -37255,6 +37561,9 @@ ALTER TABLE ONLY packages_package_files
ALTER TABLE p_ci_builds
ADD CONSTRAINT fk_87f4cefcda FOREIGN KEY (upstream_pipeline_id) REFERENCES ci_pipelines(id) ON DELETE CASCADE;
+ALTER TABLE ONLY approval_group_rules_users
+ ADD CONSTRAINT fk_888a0df3b7 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY vulnerabilities
ADD CONSTRAINT fk_88b4d546ef FOREIGN KEY (start_date_sourcing_milestone_id) REFERENCES milestones(id) ON DELETE SET NULL;
@@ -37294,6 +37603,9 @@ ALTER TABLE ONLY merge_request_review_llm_summaries
ALTER TABLE ONLY todos
ADD CONSTRAINT fk_91d1f47b13 FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE;
+ALTER TABLE ONLY zoekt_indexed_namespaces
+ ADD CONSTRAINT fk_9267f4de0c FOREIGN KEY (zoekt_node_id) REFERENCES zoekt_nodes(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY dast_site_profiles_builds
ADD CONSTRAINT fk_94e80df60e FOREIGN KEY (dast_site_profile_id) REFERENCES dast_site_profiles(id) ON DELETE CASCADE;
@@ -37324,6 +37636,9 @@ ALTER TABLE ONLY protected_branch_merge_access_levels
ALTER TABLE ONLY notes
ADD CONSTRAINT fk_99e097b079 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
+ALTER TABLE ONLY approval_group_rules_users
+ ADD CONSTRAINT fk_9a4b673183 FOREIGN KEY (approval_group_rule_id) REFERENCES approval_group_rules(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY import_failures
ADD CONSTRAINT fk_9a9b9ba21c FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
@@ -37441,6 +37756,9 @@ ALTER TABLE ONLY issues
ALTER TABLE ONLY protected_tag_create_access_levels
ADD CONSTRAINT fk_b4eb82fe3c FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
+ALTER TABLE ONLY compliance_framework_security_policies
+ ADD CONSTRAINT fk_b5df066d8f FOREIGN KEY (framework_id) REFERENCES compliance_management_frameworks(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY catalog_resource_versions
ADD CONSTRAINT fk_b670eae96b FOREIGN KEY (catalog_resource_id) REFERENCES catalog_resources(id) ON DELETE CASCADE;
@@ -37510,9 +37828,6 @@ ALTER TABLE ONLY design_management_versions
ALTER TABLE ONLY packages_packages
ADD CONSTRAINT fk_c188f0dba4 FOREIGN KEY (creator_id) REFERENCES users(id) ON DELETE SET NULL;
-ALTER TABLE ONLY ci_sources_pipelines
- ADD CONSTRAINT fk_c1b5dc6b6f FOREIGN KEY (pipeline_id_convert_to_bigint) REFERENCES ci_pipelines(id) ON DELETE CASCADE;
-
ALTER TABLE ONLY sbom_occurrences
ADD CONSTRAINT fk_c2a5562923 FOREIGN KEY (source_id) REFERENCES sbom_sources(id) ON DELETE CASCADE;
@@ -37573,6 +37888,9 @@ ALTER TABLE ONLY todos
ALTER TABLE ONLY dast_site_profiles_pipelines
ADD CONSTRAINT fk_cf05cf8fe1 FOREIGN KEY (dast_site_profile_id) REFERENCES dast_site_profiles(id) ON DELETE CASCADE;
+ALTER TABLE ONLY compliance_framework_security_policies
+ ADD CONSTRAINT fk_cf3c0ac207 FOREIGN KEY (policy_configuration_id) REFERENCES security_orchestration_policy_configurations(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY issue_assignment_events
ADD CONSTRAINT fk_cfd2073177 FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE;
@@ -37696,6 +38014,9 @@ ALTER TABLE ONLY namespaces
ALTER TABLE ONLY fork_networks
ADD CONSTRAINT fk_e7b436b2b5 FOREIGN KEY (root_project_id) REFERENCES projects(id) ON DELETE SET NULL;
+ALTER TABLE ONLY ml_candidates
+ ADD CONSTRAINT fk_e86e0bfa5a FOREIGN KEY (model_version_id) REFERENCES ml_model_versions(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY integrations
ADD CONSTRAINT fk_e8fe908a34 FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
@@ -37729,6 +38050,9 @@ ALTER TABLE ONLY approval_project_rules
ALTER TABLE ONLY vulnerabilities
ADD CONSTRAINT fk_efb96ab1e2 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
+ALTER TABLE ONLY approval_group_rules_groups
+ ADD CONSTRAINT fk_efff219a48 FOREIGN KEY (approval_group_rule_id) REFERENCES approval_group_rules(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY emails
ADD CONSTRAINT fk_emails_user_id FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
@@ -37987,6 +38311,9 @@ ALTER TABLE ONLY security_orchestration_policy_rule_schedules
ALTER TABLE ONLY incident_management_escalation_rules
ADD CONSTRAINT fk_rails_17dbea07a6 FOREIGN KEY (policy_id) REFERENCES incident_management_escalation_policies(id) ON DELETE CASCADE;
+ALTER TABLE ONLY audit_events_streaming_http_group_namespace_filters
+ ADD CONSTRAINT fk_rails_17f19c81df FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY cluster_providers_aws
ADD CONSTRAINT fk_rails_18983d9ea4 FOREIGN KEY (cluster_id) REFERENCES clusters(id) ON DELETE CASCADE;
@@ -38269,6 +38596,9 @@ ALTER TABLE ONLY batched_background_migration_jobs
ALTER TABLE ONLY operations_strategies_user_lists
ADD CONSTRAINT fk_rails_43241e8d29 FOREIGN KEY (strategy_id) REFERENCES operations_strategies(id) ON DELETE CASCADE;
+ALTER TABLE ONLY activity_pub_releases_subscriptions
+ ADD CONSTRAINT fk_rails_4337598314 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY analytics_cycle_analytics_value_stream_settings
ADD CONSTRAINT fk_rails_4360d37256 FOREIGN KEY (value_stream_id) REFERENCES analytics_cycle_analytics_group_value_streams(id) ON DELETE CASCADE;
@@ -38551,6 +38881,9 @@ ALTER TABLE ONLY namespace_admin_notes
ALTER TABLE ONLY ci_runner_machines
ADD CONSTRAINT fk_rails_666b61f04f FOREIGN KEY (runner_id) REFERENCES ci_runners(id) ON DELETE CASCADE;
+ALTER TABLE ONLY approval_group_rules
+ ADD CONSTRAINT fk_rails_6727675176 FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY jira_imports
ADD CONSTRAINT fk_rails_675d38c03b FOREIGN KEY (label_id) REFERENCES labels(id) ON DELETE SET NULL;
@@ -38632,6 +38965,9 @@ ALTER TABLE ONLY dast_site_profiles
ALTER TABLE ONLY merge_request_context_commit_diff_files
ADD CONSTRAINT fk_rails_74a00a1787 FOREIGN KEY (merge_request_context_commit_id) REFERENCES merge_request_context_commits(id) ON DELETE CASCADE;
+ALTER TABLE ONLY audit_events_streaming_http_group_namespace_filters
+ ADD CONSTRAINT fk_rails_74a28d2432 FOREIGN KEY (external_audit_event_destination_id) REFERENCES audit_events_external_audit_event_destinations(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY group_crm_settings
ADD CONSTRAINT fk_rails_74fdf2f13d FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
@@ -39304,6 +39640,9 @@ ALTER TABLE ONLY packages_rpm_metadata
ALTER TABLE ONLY note_metadata
ADD CONSTRAINT fk_rails_d853224d37 FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE;
+ALTER TABLE ONLY ml_model_metadata
+ ADD CONSTRAINT fk_rails_d907835e01 FOREIGN KEY (model_id) REFERENCES ml_models(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY merge_request_reviewers
ADD CONSTRAINT fk_rails_d9fec24b9d FOREIGN KEY (merge_request_id) REFERENCES merge_requests(id) ON DELETE CASCADE;
diff --git a/doc/.vale/gitlab/LatinTerms.yml b/doc/.vale/gitlab/LatinTerms.yml
index 0bac0448bb1..0f098979b16 100644
--- a/doc/.vale/gitlab/LatinTerms.yml
+++ b/doc/.vale/gitlab/LatinTerms.yml
@@ -15,3 +15,4 @@ swap:
e\. g\.: for example
i\.e\.: that is
i\. e\.: that is
+ via: "Use 'with', 'through', or 'by using' instead."
diff --git a/doc/.vale/gitlab/Wordy.yml b/doc/.vale/gitlab/Wordy.yml
index 808bedad35a..9c472f66570 100644
--- a/doc/.vale/gitlab/Wordy.yml
+++ b/doc/.vale/gitlab/Wordy.yml
@@ -10,6 +10,7 @@ link: https://docs.gitlab.com/ee/development/documentation/styleguide/word_list.
level: suggestion
ignorecase: true
swap:
+ a number of: "Specify the number or remove the phrase."
as well as: "Use 'and' instead of 'as well as'."
note that: "Remove the phrase 'note that'."
please: "Use 'please' only if we've inconvenienced the user."
diff --git a/doc/administration/audit_event_streaming/audit_event_types.md b/doc/administration/audit_event_streaming/audit_event_types.md
index 3b2ae098469..88212045d8e 100644
--- a/doc/administration/audit_event_streaming/audit_event_types.md
+++ b/doc/administration/audit_event_streaming/audit_event_types.md
@@ -37,6 +37,7 @@ Audit event types belong to the following product categories.
| Name | Description | Saved to database | Streamed | Introduced in |
|:-----|:------------|:------------------|:---------|:--------------|
| [`amazon_s3_configuration_created`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132443) | Triggered when Amazon S3 configuration for audit events streaming is created| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.5](https://gitlab.com/gitlab-org/gitlab/-/issues/423229) |
+| [`amazon_s3_configuration_deleted`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/133695) | Triggered when Amazon S3 configuration for audit events streaming is deleted.| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.5](https://gitlab.com/gitlab-org/gitlab/-/issues/423229) |
| [`amazon_s3_configuration_updated`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/133691) | Triggered when Amazon S3 configuration for audit events streaming is updated.| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.5](https://gitlab.com/gitlab-org/gitlab/-/issues/423229) |
| [`audit_events_streaming_headers_create`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/92068) | Triggered when a streaming header for audit events is created| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [15.3](https://gitlab.com/gitlab-org/gitlab/-/issues/366350) |
| [`audit_events_streaming_headers_destroy`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/92068) | Triggered when a streaming header for audit events is deleted| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [15.3](https://gitlab.com/gitlab-org/gitlab/-/issues/366350) |
@@ -44,6 +45,7 @@ Audit event types belong to the following product categories.
| [`audit_events_streaming_instance_headers_destroy`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/127228) | Triggered when a streaming header for instance level external audit event destination is deleted| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.3](https://gitlab.com/gitlab-org/gitlab/-/issues/417433) |
| [`audit_events_streaming_instance_headers_update`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/127228) | Triggered when a streaming header for instance level external audit event destination is updated| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.3](https://gitlab.com/gitlab-org/gitlab/-/issues/417433) |
| [`create_event_streaming_destination`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74632) | Event triggered when an external audit event destination is created| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [14.6](https://gitlab.com/gitlab-org/gitlab/-/issues/344664) |
+| [`create_http_namespace_filter`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/136047) | Event triggered when a namespace filter for an external audit event destination for a top-level group is created.| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.6](https://gitlab.com/gitlab-org/gitlab/-/issues/424176) |
| [`create_instance_event_streaming_destination`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/123882) | Event triggered when an instance level external audit event destination is created| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.2](https://gitlab.com/gitlab-org/gitlab/-/issues/404730) |
| [`destroy_event_streaming_destination`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74632) | Event triggered when an external audit event destination is deleted| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [14.6](https://gitlab.com/gitlab-org/gitlab/-/issues/344664) |
| [`destroy_instance_event_streaming_destination`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/125846) | Event triggered when an instance level external audit event destination is deleted| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.2](https://gitlab.com/gitlab-org/gitlab/-/issues/404730) |
@@ -171,6 +173,7 @@ Audit event types belong to the following product categories.
| [`ci_variable_created`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91983) | Triggered when a CI variable is created at a project level| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [15.2](https://gitlab.com/gitlab-org/gitlab/-/issues/363090) |
| [`ci_variable_deleted`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91983) | Triggered when a project's CI variable is deleted| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [15.2](https://gitlab.com/gitlab-org/gitlab/-/issues/363090) |
| [`ci_variable_updated`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91983) | Triggered when a project's CI variable is updated| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [15.2](https://gitlab.com/gitlab-org/gitlab/-/issues/363090) |
+| [`destroy_pipeline`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/135255) | Event triggered when a pipeline is deleted| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.6](https://gitlab.com/gitlab-org/gitlab/-/issues/339041) |
### Deployment management
@@ -227,6 +230,8 @@ Audit event types belong to the following product categories.
| Name | Description | Saved to database | Streamed | Introduced in |
|:-----|:------------|:------------------|:---------|:--------------|
+| [`create_ssh_certificate`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134556) | Event triggered when an SSH certificate is created.| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.6](https://gitlab.com/gitlab-org/gitlab/-/issues/427413) |
+| [`delete_ssh_certificate`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134556) | Event triggered when an SSH certificate is deleted.| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.6](https://gitlab.com/gitlab-org/gitlab/-/issues/427413) |
| [`group_created`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121005) | Event triggered when a group is created.| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.3](https://gitlab.com/gitlab-org/gitlab/-/issues/411595) |
| [`group_lfs_enabled_updated`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/106079) | Event triggered when a groups lfs enabled is updated.| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [15.7](https://gitlab.com/gitlab-org/gitlab/-/issues/369323) |
| [`group_membership_lock_updated`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/106079) | Event triggered when a groups membership lock is updated.| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [15.7](https://gitlab.com/gitlab-org/gitlab/-/issues/369323) |
@@ -306,7 +311,6 @@ Audit event types belong to the following product categories.
| Name | Description | Saved to database | Streamed | Introduced in |
|:-----|:------------|:------------------|:---------|:--------------|
| [`experiment_features_enabled_updated`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/118222) | Event triggered on toggling setting for enabling experiment AI features| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.0](https://gitlab.com/gitlab-org/gitlab/-/issues/404856/) |
-| [`third_party_ai_features_enabled_updated`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/118222) | Event triggered on toggling setting for enabling third-party AI features| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.0](https://gitlab.com/gitlab-org/gitlab/-/issues/404856/) |
### Portfolio management
@@ -442,6 +446,7 @@ Audit event types belong to the following product categories.
| [`email_confirmation_sent`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/129261) | Triggered when users add or change and email address and it needs to be confirmed.| **{dotted-circle}** No | **{check-circle}** Yes | GitLab [16.3](https://gitlab.com/gitlab-org/gitlab/-/issues/377625) |
| [`remove_ssh_key`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/65615) | Audit event triggered when a SSH key is removed| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [14.1](https://gitlab.com/gitlab-org/gitlab/-/issues/220127) |
| [`user_admin_status_updated`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/65168) | Adds an audit event when a user is either made an administrator, or removed as an administrator| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [14.1](https://gitlab.com/gitlab-org/gitlab/-/issues/323905) |
+| [`user_auditor_status_updated`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/136456) | Adds an audit event when a user is either made an auditor, or removed as an auditor| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.6](https://gitlab.com/gitlab-org/gitlab/-/issues/430235) |
| [`user_email_address_updated`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/2103) | Adds an audit event when a user updates their email address| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [10.1](https://gitlab.com/gitlab-org/gitlab-ee/issues/1370) |
| [`user_profile_visiblity_updated`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/129149) | Triggered when user toggles private profile user setting| **{dotted-circle}** No | **{check-circle}** Yes | GitLab [16.3](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/129149) |
| [`user_username_updated`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/106086) | Event triggered on updating a user's username| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [15.7](https://gitlab.com/gitlab-org/gitlab/-/issues/369329) |
diff --git a/doc/administration/audit_event_streaming/graphql_api.md b/doc/administration/audit_event_streaming/graphql_api.md
index 6e1a3424929..58668902b8e 100644
--- a/doc/administration/audit_event_streaming/graphql_api.md
+++ b/doc/administration/audit_event_streaming/graphql_api.md
@@ -177,9 +177,8 @@ Prerequisites:
- Owner role for a top-level group.
-Users with the Owner role for a group can update streaming destinations' custom HTTP headers using the
-`auditEventsStreamingHeadersUpdate` mutation type. You can retrieve the custom HTTP headers ID
-by [listing all the custom HTTP headers](#list-streaming-destinations) for the group.
+To update streaming destinations for a group, use the `externalAuditEventDestinationUpdate` mutation type. You can retrieve the destinations ID
+by [listing all the streaming destinations](#list-streaming-destinations) for the group.
```graphql
mutation {
@@ -206,6 +205,24 @@ Streaming destination is updated if:
- The returned `errors` object is empty.
- The API responds with `200 OK`.
+Users with the Owner role for a group can update streaming destinations' custom HTTP headers using the
+`auditEventsStreamingHeadersUpdate` mutation type. You can retrieve the custom HTTP headers ID
+by [listing all the custom HTTP headers](#list-streaming-destinations) for the group.
+
+```graphql
+mutation {
+ auditEventsStreamingHeadersUpdate(input: { headerId: "gid://gitlab/AuditEvents::Streaming::Header/2", key: "new-key", value: "new-value", active: false }) {
+ errors
+ header {
+ id
+ key
+ value
+ active
+ }
+ }
+}
+```
+
Group owners can remove an HTTP header using the GraphQL `auditEventsStreamingHeadersDestroy` mutation. You can retrieve the header ID
by [listing all the custom HTTP headers](#list-streaming-destinations) for the group.
diff --git a/doc/administration/audit_event_streaming/index.md b/doc/administration/audit_event_streaming/index.md
index 8f40dc6c34c..09474db1e08 100644
--- a/doc/administration/audit_event_streaming/index.md
+++ b/doc/administration/audit_event_streaming/index.md
@@ -206,7 +206,9 @@ To add Google Cloud Logging streaming destinations to a top-level group:
1. Select **Secure > Audit events**.
1. On the main area, select **Streams** tab.
1. Select **Add streaming destination** and select **Google Cloud Logging** to show the section for adding destinations.
-1. Enter the Google project ID, Google client email, log ID, and Google private key to add.
+1. Enter a random string to use as a name for the new destination.
+1. Enter the Google project ID, Google client email, and Google private key from previously-created Google Cloud service account key to add to the new destination.
+1. Enter a random string to use as a log ID for the new destination. You can use this later to filter log results in Google Cloud.
1. Select **Add** to add the new streaming destination.
#### List Google Cloud Logging destinations
@@ -236,7 +238,9 @@ To update Google Cloud Logging streaming destinations to a top-level group:
1. Select **Secure > Audit events**.
1. On the main area, select **Streams** tab.
1. Select the Google Cloud Logging stream to expand.
-1. Enter the Google project ID, Google client email, and log ID to update.
+1. Enter a random string to use as a name for the destination.
+1. Enter the Google project ID and Google client email from previously-created Google Cloud service account key to update the destination.
+1. Enter a random string to update the log ID for the destination. You can use this later to filter log results in Google Cloud.
1. Select **Add a new private key** and enter a Google private key to update the private key.
1. Select **Save** to update the streaming destination.
@@ -255,6 +259,85 @@ To delete Google Cloud Logging streaming destinations to a top-level group:
1. Select **Delete destination**.
1. Confirm by selecting **Delete destination** in the dialog.
+### AWS S3 destinations
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132603) in GitLab 16.6 [with a flag](../feature_flags.md) named `allow_streaming_audit_events_to_amazon_s3`. Enabled by default.
+
+FLAG:
+On self-managed GitLab, by default this feature is available. To hide the feature per group, an administrator can [disable the feature flag](../feature_flags.md) named `allow_streaming_audit_events_to_amazon_s3`.
+On GitLab.com, this feature is available.
+
+Manage AWS S3 destinations for top-level groups.
+
+#### Prerequisites
+
+Before setting up AWS S3 streaming audit events, you must:
+
+1. Create a access key for AWS with the appropriate credentials and permissions. This account is used to configure audit log streaming authentication.
+ For more information, see [Managing access keys](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html?icmpid=docs_iam_console#Using_CreateAccessKey).
+1. Create a AWS S3 bucket. This bucket is used to store audit log streaming data. For more information, see [Creating a bucket](https://docs.aws.amazon.com/AmazonS3/latest/userguide/create-bucket-overview.html)
+
+#### Add a new AWS S3 destination
+
+Prerequisites:
+
+- Owner role for a top-level group.
+
+To add AWS S3 streaming destinations to a top-level group:
+
+1. On the left sidebar, select **Search or go to** and find your group.
+1. Select **Secure > Audit events**.
+1. On the main area, select **Streams** tab.
+1. Select **Add streaming destination** and select **AWS S3** to show the section for adding destinations.
+1. Enter a random string to use as a name for the new destination.
+1. Enter the Access Key ID, Secret Access Key, Bucket Name, and AWS Region from previously-created AWS access key and bucket to add to the new destination.
+1. Select **Add** to add the new streaming destination.
+
+#### List AWS S3 destinations
+
+Prerequisites:
+
+- Owner role for a top-level group.
+
+To list AWS S3 streaming destinations for a top-level group:
+
+1. On the left sidebar, select **Search or go to** and find your group.
+1. Select **Secure > Audit events**.
+1. On the main area, select **Streams** tab.
+1. Select the AWS S3 stream to expand and see all the fields.
+
+#### Update a AWS S3 destination
+
+Prerequisites:
+
+- Owner role for a top-level group.
+
+To update AWS S3 streaming destinations to a top-level group:
+
+1. On the left sidebar, select **Search or go to** and find your group.
+1. Select **Secure > Audit events**.
+1. On the main area, select **Streams** tab.
+1. Select the AWS S3 stream to expand.
+1. Enter a random string to use as a name for the destination.
+1. Enter the Access Key ID, Secret Access Key, Bucket Name, and AWS Region from previously-created AWS access key and bucket to update the destination.
+1. Select **Add a new Secret Access Key** and enter a AWS Secret Access Key to update the Secret Access Key.
+1. Select **Save** to update the streaming destination.
+
+#### Delete a AWS S3 streaming destination
+
+Prerequisites:
+
+- Owner role for a top-level group.
+
+To delete AWS S3 streaming destinations to a top-level group:
+
+1. On the left sidebar, select **Search or go to** and find your group.
+1. Select **Secure > Audit events**.
+1. On the main area, select the **Streams** tab.
+1. Select the AWS S3 stream to expand.
+1. Select **Delete destination**.
+1. Confirm by selecting **Delete destination** in the dialog.
+
## Instance streaming destinations **(ULTIMATE SELF)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/398107) in GitLab 16.1 [with a flag](../feature_flags.md) named `ff_external_audit_events`. Disabled by default.
@@ -446,7 +529,9 @@ To add Google Cloud Logging streaming destinations to an instance:
1. On the left sidebar, select **Monitoring > Audit Events**.
1. On the main area, select **Streams** tab.
1. Select **Add streaming destination** and select **Google Cloud Logging** to show the section for adding destinations.
-1. Enter the Google project ID, Google client email, log ID, and Google private key to add.
+1. Enter a random string to use as a name for the new destination.
+1. Enter the Google project ID, Google client email, and Google private key from previously-created Google Cloud service account key to add to the new destination.
+1. Enter a random string to use as a log ID for the new destination. You can use this later to filter log results in Google Cloud.
1. Select **Add** to add the new streaming destination.
#### List Google Cloud Logging destinations
@@ -476,7 +561,9 @@ To update Google Cloud Logging streaming destinations to an instance:
1. On the left sidebar, select **Monitoring > Audit Events**.
1. On the main area, select **Streams** tab.
1. Select the Google Cloud Logging stream to expand.
-1. Enter the Google project ID, Google client email, and log ID to update.
+1. Enter a random string to use as a name for the destination.
+1. Enter the Google project ID and Google client email from previously-created Google Cloud service account key to update the destination.
+1. Enter a random string to update the log ID for the destination. You can use this later to filter log results in Google Cloud.
1. Select **Add a new private key** and enter a Google private key to update the private key.
1. Select **Save** to update the streaming destination.
diff --git a/doc/administration/audit_events.md b/doc/administration/audit_events.md
index 736f381e9d7..ba1a4ca05c4 100644
--- a/doc/administration/audit_events.md
+++ b/doc/administration/audit_events.md
@@ -6,171 +6,146 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Audit events **(PREMIUM ALL)**
-Use audit events to track important events, including who performed the related action and when.
-You can use audit events to track, for example:
+A security audit is a in-depth analysis and review of your infrastructure, which is used to display
+areas of concern and potentially hazardous practices. To assist with the audit process, GitLab provides
+audit events which allow you to track a variety of different actions within GitLab.
+
+For example, you can use audit events to track:
- Who changed the permission level of a particular user for a GitLab project, and when.
- Who added a new user or removed a user, and when.
-Audit events are similar to the [log system](logs/index.md).
-
-The GitLab API, database, and `audit_json.log` record many audit events. Some audit events are only available through
-[streaming audit events](audit_event_streaming.md).
+These events can be used to in an audit to assess risk, strengthen security measures, respond to incidents, and adhere to compliance. For a complete list the audit events GitLab provides, see [Audit event types](../administration/audit_event_streaming/audit_event_types.md).
-You can also generate an [audit report](audit_reports.md) of audit events.
+## Prerequisites
-NOTE:
-You can't configure a retention policy for audit events, but epic
-[7917](https://gitlab.com/groups/gitlab-org/-/epics/7917) proposes to change this.
+To view specific types of audit events, you need a minimum role.
-## Time zones
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/242014) in GitLab 15.7, GitLab UI shows dates and times in the user's local time zone instead of UTC.
+- To view the group audit events of all users in a group, you must have the [Owner role](../user/permissions.md#roles) for the group.
+- To view the project audit events of all users in a project, you must have at least the [Maintainer role](../user/permissions.md#roles) for the project.
+- To view the group and project audit events based on your own actions in a group or project, you must have at least the [Developer role](../user/permissions.md#roles)
+ for the group or project.
-The time zone used for audit events depends on where you view them:
+Users with the [Auditor access level](auditor_users.md) can see group and project events for all users.
-- In GitLab UI, your local time zone (GitLab 15.7 and later) or UTC (GitLab 15.6 and earlier) is used.
-- The [Audit Events API](../api/audit_events.md) returns dates and times in UTC by default, or the
- [configured time zone](timezone.md) on a self-managed GitLab instance.
-- In `audit_json.log`, UTC is used.
-- In CSV exports, UTC is used.
+## Viewing audit events
-## View audit events
+Audit events can be viewed at the group, project, instance, and sign-in level. Each level has different audit events which it logs.
-Depending on the events you want to view, at a minimum you must have:
-
-- For group audit events of all users in the group, the Owner role for the group.
-- For project audit events of all users in the project, the Maintainer role for the project.
-- For group and project audit events based on your own actions, the Developer role for the group or project.
-- [Auditor users](auditor_users.md) can see group and project events for all users.
-
-You can view audit events scoped to a group or project.
+### Group audit events
To view a group's audit events:
-1. Go to the group.
+1. On the left sidebar, select **Search or go to** and find your group.
1. On the left sidebar, select **Secure > Audit events**.
+1. Filter the audit events by the member of the project (user) who performed the action and date range.
-Group events do not include project audit events. Group events can also be accessed using the
-[Group Audit Events API](../api/audit_events.md#group-audit-events). Group event queries are limited to a maximum of 30
-days.
+Group audit events can also be accessed using the [Group Audit Events API](../api/audit_events.md#group-audit-events). Group audit event queries are limited to a maximum of 30 days.
-To view a project's audit events:
+### Project audit events
-1. Go to the project.
+1. On the left sidebar, select **Search or go to** and find your project.
1. On the left sidebar, select **Secure > Audit events**.
+1. Filter the audit events by the member of the project (user) who performed the action and date range.
-Project events can also be accessed using the [Project Audit Events API](../api/audit_events.md#project-audit-events).
-Project event queries are limited to a maximum of 30 days.
+Project audit events can also be accessed using the [Project Audit Events API](../api/audit_events.md#project-audit-events). Project audit event queries are limited to a maximum of 30 days.
-## View instance audit events **(PREMIUM SELF)**
+### Instance audit events **(PREMIUM SAAS)**
You can view audit events from user actions across an entire GitLab instance.
-
To view instance audit events:
1. On the left sidebar, select **Search or go to**.
1. Select **Admin Area**.
1. On the left sidebar, select **Monitoring > Audit Events**.
+1. Filter by the following:
+ - Member of the project (user) who performed the action
+ - Group
+ - Project
+ - Date Range
+
+### Sign-in audit events **(FREE ALL)**
+
+Successful sign-in events are the only audit events available at all tiers. To see successful sign-in events:
+
+1. On the left sidebar, select your avatar.
+1. Select **Edit profile > Authentication log**.
-### Export to CSV
+After upgrading to a paid tier, you can also see successful sign-in events on audit event pages.
+
+## Exporting audit events
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/1449) in GitLab 13.4.
> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/285441) in GitLab 13.7.
> - Entity type `Gitlab::Audit::InstanceScope` for instance audit events [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/418185) in GitLab 16.2.
-You can export the current view (including filters) of your instance audit events as a CSV file. To export the instance
-audit events to CSV:
+You can export the current view (including filters) of your instance audit events as a
+CSV(comma-separated values) file. To export the instance audit events to CSV:
1. On the left sidebar, select **Search or go to**.
1. Select **Admin Area**.
1. On the left sidebar, select **Monitoring > Audit Events**.
-1. Select the available search [filters](#filter-audit-events).
+1. Select the available search filters.
1. Select **Export as CSV**.
-The exported file:
-
-- Is sorted by `created_at` in ascending order.
-- Is limited to a maximum of 100 000 events. The remaining records are truncated when this limit is reached.
-
-Data is encoded with:
-
-- Comma as the column delimiter.
-- `"` to quote fields if necessary.
-- New lines separate rows.
+A download confirmation dialog then appears for you to download the CSV file. The exported CSV is limited
+to a maximum of 100000 events. The remaining records are truncated when this limit is reached.
-The first row contains the headers, which are listed in the following table along with a description of the values:
+### Audit event CSV encoding
-| Column | Description |
-|:---------------------|:-------------------------------------------------------------------|
-| **ID** | Audit event `id`. |
-| **Author ID** | ID of the author. |
-| **Author Name** | Full name of the author. |
-| **Entity ID** | ID of the scope. |
-| **Entity Type** | Type of the scope (`Project`, `Group`, `User`, or `Gitlab::Audit::InstanceScope`). |
-| **Entity Path** | Path of the scope. |
-| **Target ID** | ID of the target. |
-| **Target Type** | Type of the target. |
-| **Target Details** | Details of the target. |
-| **Action** | Description of the action. |
-| **IP Address** | IP address of the author who performed the action. |
-| **Created At (UTC)** | Formatted as `YYYY-MM-DD HH:MM:SS`. |
+The exported CSV file is encoded as follows:
-## View sign-in events **(FREE ALL)**
+- `,` is used as the column delimiter
+- `"` is used to quote fields if necessary.
+- `\n` is used to separate rows.
-Successful sign-in events are the only audit events available at all tiers. To see successful sign-in events:
+The first row contains the headers, which are listed in the following table along
+with a description of the values:
-1. On the left sidebar, select your avatar.
-1. Select **Edit profile > Authentication log**.
-
-After upgrading to a paid tier, you can also see successful sign-in events on audit event pages.
+| Column | Description |
+| --------------------- | ---------------------------------------------------------------------------------- |
+| **ID** | Audit event `id`. |
+| **Author ID** | ID of the author. |
+| **Author Name** | Full name of the author. |
+| **Entity ID** | ID of the scope. |
+| **Entity Type** | Type of the scope (`Project`, `Group`, `User`, or `Gitlab::Audit::InstanceScope`). |
+| **Entity Path** | Path of the scope. |
+| **Target ID** | ID of the target. |
+| **Target Type** | Type of the target. |
+| **Target Details** | Details of the target. |
+| **Action** | Description of the action. |
+| **IP Address** | IP address of the author who performed the action. |
+| **Created At (UTC)** | Formatted as `YYYY-MM-DD HH:MM:SS`. |
-## Filter audit events
-
-From audit events pages, different filters are available depending on the page you're on.
-
-| Audit event page | Available filter |
-|:-----------------|:-----------------------------------------------------------------------------------------------------------------------|
-| Project | User (member of the project) who performed the action. |
-| Group | User (member of the group) who performed the action. |
-| Instance | Group, project, or user. |
-| All | Date range buttons and pickers (maximum range of 31 days). Default is from the first day of the month to today's date. |
+All items are sorted by `created_at` in ascending order.
## User impersonation
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/536) in GitLab 13.0.
> - Impersonation session events included in group audit events in GitLab 14.8.
-When a user is [impersonated](../administration/admin_area.md#user-impersonation), their actions are logged as audit events
-with additional details:
+When a user is [impersonated](../administration/admin_area.md#user-impersonation), their actions are logged as audit events with the following additional details:
-- Audit events include information about the impersonating administrator. These audit events are visible in audit event
- pages depending on the audit event type (group, project, or user).
-- Extra audit events are recorded for the start and end of the administrator's impersonation session. These audit events
- are visible as:
- - Instance audit events.
- - Group audit events for all groups the user belongs to. For performance reasons, group audit events are limited to
- the oldest 20 groups you belong to.
+- Audit events include information about the impersonating administrator.
+- Extra audit events are recorded for the start and end of the administrator's impersonation session.
![Audit event with impersonated user](img/impersonated_audit_events_v15_7.png)
-## Available audit events
+## Time zones
-For a list of available audit events, see [Audit event types](../administration/audit_event_streaming/audit_event_types.md).
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/242014) in GitLab 15.7, GitLab UI shows dates and times in the user's local time zone instead of UTC.
-## Unsupported events
+The time zone used for audit events depends on where you view them:
-Some events are not tracked in audit events. The following epics and issues propose support for more events:
+- In GitLab UI, your local time zone (GitLab 15.7 and later) or UTC (GitLab 15.6 and earlier) is used.
+- The [Audit Events API](../api/audit_events.md) returns dates and times in UTC by default, or the
+ [configured time zone](timezone.md) on a self-managed GitLab instance.
+- In CSV exports, UTC is used.
-- [Project settings and activity](https://gitlab.com/groups/gitlab-org/-/epics/474).
-- [Group settings and activity](https://gitlab.com/groups/gitlab-org/-/epics/475).
-- [Instance-level settings and activity](https://gitlab.com/groups/gitlab-org/-/epics/476).
-- [Deployment Approval activity](https://gitlab.com/gitlab-org/gitlab/-/issues/354782).
-- [Approval rules processing by a non GitLab user](https://gitlab.com/gitlab-org/gitlab/-/issues/407384).
+## Contribute to audit events
If you don't see the event you want in any of the epics, you can either:
- Use the **Audit Event Proposal** issue template to
- [create an issue](https://gitlab.com/gitlab-org/gitlab/-/issues/new?issuable_template=Audit%20Event%20Proposal) to
- request it.
+ [create an issue](https://gitlab.com/gitlab-org/gitlab/-/issues/new?issuable_template=Audit%20Event%20Proposal) to request it.
- [Add it yourself](../development/audit_event_guide/index.md).
diff --git a/doc/administration/auditor_users.md b/doc/administration/auditor_users.md
index e9df9cc6e37..09d68e82782 100644
--- a/doc/administration/auditor_users.md
+++ b/doc/administration/auditor_users.md
@@ -25,6 +25,9 @@ Situations where auditor access for users could be helpful include:
you can create an account with auditor access and then share the credentials
with those users to which you want to grant access.
+NOTE:
+An auditor user counts as a billable user and consumes a license seat.
+
## Add a user with auditor access
To create a new user account with auditor access (or change an existing user):
diff --git a/doc/administration/auth/ldap/index.md b/doc/administration/auth/ldap/index.md
index bf2b3d7e53e..0c42ce90346 100644
--- a/doc/administration/auth/ldap/index.md
+++ b/doc/administration/auth/ldap/index.md
@@ -448,7 +448,7 @@ These LDAP sync configuration settings are available:
| Setting | Description | Required | Examples |
|-------------------|-------------|----------|----------|
-| `group_base` | Base used to search for groups. | **{dotted-circle}** No (required when `external_groups` is configured) | `'ou=groups,dc=gitlab,dc=example'` |
+| `group_base` | Base used to search for groups. All valid groups have this base as part of their DN. | **{dotted-circle}** No (required when `external_groups` is configured) | `'ou=groups,dc=gitlab,dc=example'` |
| `admin_group` | The CN of a group containing GitLab administrators. Not `cn=administrators` or the full DN. | **{dotted-circle}** No | `'administrators'` |
| `external_groups` | An array of CNs of groups containing users that should be considered external. Not `cn=interns` or the full DN. | **{dotted-circle}** No | `['interns', 'contractors']` |
| `sync_ssh_keys` | The LDAP attribute containing a user's public SSH key. | **{dotted-circle}** No | `'sshPublicKey'` or false if not set |
diff --git a/doc/administration/backup_restore/backup_gitlab.md b/doc/administration/backup_restore/backup_gitlab.md
index 05a330bf3f5..5c0fcbbc4ef 100644
--- a/doc/administration/backup_restore/backup_gitlab.md
+++ b/doc/administration/backup_restore/backup_gitlab.md
@@ -437,7 +437,9 @@ sudo -u git -H bundle exec rake gitlab:backup:create SKIP=tar RAILS_ENV=producti
#### Create server-side repository backups
-> [Introduced](https://gitlab.com/gitlab-org/gitaly/-/issues/4941) in GitLab 16.3.
+> - [Introduced](https://gitlab.com/gitlab-org/gitaly/-/issues/4941) in GitLab 16.3.
+> - Server-side support for restoring a specified backup instead of the latest backup [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132188) in GitLab 16.6.
+> - Server-side support for creating incremental backups [introduced](https://gitlab.com/gitlab-org/gitaly/-/merge_requests/6475) in GitLab 16.6.
Instead of storing large repository backups in the backup archive, repository
backups can be configured so that the Gitaly node that hosts each repository is
@@ -504,6 +506,7 @@ sudo -u git -H bundle exec rake gitlab:backup:create GITLAB_BACKUP_MAX_CONCURREN
> - Introduced in GitLab 14.9 [with a flag](../feature_flags.md) named `incremental_repository_backup`. Disabled by default.
> - [Enabled on self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/355945) in GitLab 14.10.
> - `PREVIOUS_BACKUP` option [introduced](https://gitlab.com/gitlab-org/gitaly/-/issues/4184) in GitLab 15.0.
+> - Server-side support for creating incremental backups [introduced](https://gitlab.com/gitlab-org/gitaly/-/merge_requests/6475) in GitLab 16.6.
FLAG:
On self-managed GitLab, by default this feature is available. To hide the feature, an administrator can [disable the feature flag](../feature_flags.md) named `incremental_repository_backup`.
@@ -853,7 +856,7 @@ For the Linux package (Omnibus):
## If you have CNAME buckets (foo.example.com), you might run into SSL issues
## when uploading backups ("hostname foo.example.com.storage.googleapis.com
- ## does not match the server certificate"). In that case, uncomnent the following
+ ## does not match the server certificate"). In that case, uncomment the following
## setting. See: https://github.com/fog/fog/issues/2834
#'path_style' => true
}
@@ -1272,7 +1275,7 @@ Gitaly Cluster [does not support snapshot backups](../gitaly/index.md#snapshot-b
When considering using file system data transfer or snapshots:
- Don't use these methods to migrate from one operating system to another. The operating systems of the source and destination should be as similar as possible. For example,
- don't use these methods to migrate from Ubuntu to Fedora.
+ don't use these methods to migrate from Ubuntu to RHEL.
- Data consistency is very important. You should stop GitLab with `sudo gitlab-ctl stop` before taking doing a file system transfer (with `rsync`, for example) or taking a
snapshot.
diff --git a/doc/administration/cicd.md b/doc/administration/cicd.md
index 7a6316a1e50..10bc60fe399 100644
--- a/doc/administration/cicd.md
+++ b/doc/administration/cicd.md
@@ -18,7 +18,7 @@ CI/CD to be disabled by default in new projects by modifying the settings in:
- `gitlab.rb` for Linux package installations.
Existing projects that already had CI/CD enabled are unchanged. Also, this setting only changes
-the project default, so project owners [can still enable CI/CD in the project settings](../ci/enable_or_disable_ci.md).
+the project default, so project owners [can still enable CI/CD in the project settings](../ci/pipelines/settings.md#disable-gitlab-cicd-pipelines).
For self-compiled installations:
@@ -93,14 +93,96 @@ To change the frequency of the pipeline schedule worker:
For example, to set the maximum frequency of pipelines to twice a day, set `pipeline_schedule_worker_cron`
to a cron value of `0 */12 * * *` (`00:00` and `12:00` every day).
-<!-- ## Troubleshooting
+## Disaster recovery
-Include any troubleshooting steps that you can foresee. If you know beforehand what issues
-one might have when setting this up, or when something is changed, or on upgrading, it's
-important to describe those, too. Think of things that may go wrong and include them here.
-This is important to minimize requests for support, and to avoid doc comments with
-questions that you know someone might ask.
+You can disable some important but computationally expensive parts of the application
+to relieve stress on the database during ongoing downtime.
-Each scenario can be a third-level heading, for example `### Getting error message X`.
-If you have none to add when creating a doc, leave this section in place
-but commented out to help encourage others to add to it in the future. -->
+### Disable fair scheduling on shared runners
+
+When clearing a large backlog of jobs, you can temporarily enable the `ci_queueing_disaster_recovery_disable_fair_scheduling`
+[feature flag](../administration/feature_flags.md). This flag disables fair scheduling
+on shared runners, which reduces system resource usage on the `jobs/request` endpoint.
+
+When enabled, jobs are processed in the order they were put in the system, instead of
+balanced across many projects.
+
+### Disable compute quota enforcement
+
+To disable the enforcement of [compute quotas](../ci/pipelines/cicd_minutes.md) on shared runners, you can temporarily
+enable the `ci_queueing_disaster_recovery_disable_quota` [feature flag](../administration/feature_flags.md).
+This flag reduces system resource usage on the `jobs/request` endpoint.
+
+When enabled, jobs created in the last hour can run in projects which are out of quota.
+Earlier jobs are already canceled by a periodic background worker (`StuckCiJobsWorker`).
+
+## CI/CD troubleshooting Rails console commands
+
+The following commands are run in the [Rails console](../administration/operations/rails_console.md#starting-a-rails-console-session).
+
+WARNING:
+Any command that changes data directly could be damaging if not run correctly, or under the right conditions.
+We highly recommend running them in a test environment with a backup of the instance ready to be restored, just in case.
+
+### Cancel stuck pending pipelines
+
+```ruby
+project = Project.find_by_full_path('<project_path>')
+Ci::Pipeline.where(project_id: project.id).where(status: 'pending').count
+Ci::Pipeline.where(project_id: project.id).where(status: 'pending').each {|p| p.cancel if p.stuck?}
+Ci::Pipeline.where(project_id: project.id).where(status: 'pending').count
+```
+
+### Try merge request integration
+
+```ruby
+project = Project.find_by_full_path('<project_path>')
+mr = project.merge_requests.find_by(iid: <merge_request_iid>)
+mr.project.try(:ci_integration)
+```
+
+### Validate the `.gitlab-ci.yml` file
+
+```ruby
+project = Project.find_by_full_path('<project_path>')
+content = p.ci_config_for(project.repository.root_ref_sha)
+Gitlab::Ci::Lint.new(project: project, current_user: User.first).validate(content)
+```
+
+### Disable AutoDevOps on Existing Projects
+
+```ruby
+Project.all.each do |p|
+ p.auto_devops_attributes={"enabled"=>"0"}
+ p.save
+end
+```
+
+### Obtain runners registration token
+
+```ruby
+Gitlab::CurrentSettings.current_application_settings.runners_registration_token
+```
+
+### Seed runners registration token
+
+```ruby
+appSetting = Gitlab::CurrentSettings.current_application_settings
+appSetting.set_runners_registration_token('<new-runners-registration-token>')
+appSetting.save!
+```
+
+### Run pipeline schedules manually
+
+You can run pipeline schedules manually through the Rails console to reveal any errors that are usually not visible.
+
+```ruby
+# schedule_id can be obtained from Edit Pipeline Schedule page
+schedule = Ci::PipelineSchedule.find_by(id: <schedule_id>)
+
+# Select the user that you want to run the schedule for
+user = User.find_by_username('<username>')
+
+# Run the schedule
+ps = Ci::CreatePipelineService.new(schedule.project, user, ref: schedule.ref).execute!(:schedule, ignore_skip_ci: true, save_on_errors: false, schedule: schedule)
+```
diff --git a/doc/administration/dedicated/index.md b/doc/administration/dedicated/index.md
index 2889fb9b389..16efc353c84 100644
--- a/doc/administration/dedicated/index.md
+++ b/doc/administration/dedicated/index.md
@@ -38,7 +38,7 @@ After you first sign in to Switchboard, you must update your password and set up
The following stages guide you through a series of four steps to provide the information required to create your GitLab Dedicated tenant.
1. Confirm account details: Confirm key attributes of your GitLab Dedicated account:
- - Reference architecture: Corresponds with the number of users you provided to your account team when beginning the onboarding process. For more information, see [reference architectures](../../administration/reference_architectures/index.md).
+ - Reference architecture: Corresponds with the number of users you provided to your account team when beginning the onboarding process. For more information, see [reference architectures](../../subscriptions/gitlab_dedicated/index.md#availability-and-scalability).
- Total repository storage size: Corresponds with the storage size you provided to your account team when beginning the onboarding process.
- If you need to make changes to these attributes, [submit a support ticket](https://support.gitlab.com/hc/en-us/requests/new?ticket_form_id=4414917877650).
1. Tenant configuration: Provides the minimum required information needed to create your GitLab Dedicated tenant:
@@ -214,7 +214,9 @@ Make sure the AWS KMS keys are replicated to your desired primary, secondary and
## Configuration changes
-To change or update the configuration for your GitLab Dedicated instance, open a [support ticket](https://support.gitlab.com/hc/en-us/requests/new?ticket_form_id=4414917877650) with your request. You can request configuration changes for the options originally specified during onboarding, or for any of the following optional features.
+Switchboard empowers the user to make limited configuration changes to their Dedicated Tenant Instance. As Switchboard matures further configuration changes will be made available.
+
+To change or update the configuration of your GitLab Dedicated instance, use Switchboard following the instructions in the relevant section or open a [support ticket](https://support.gitlab.com/hc/en-us/requests/new?ticket_form_id=4414917877650) with your request. You can request configuration changes for the options originally specified during onboarding, or for any of the following optional features.
The turnaround time to process configuration change requests is [documented in the GitLab handbook](https://about.gitlab.com/handbook/engineering/infrastructure/team/gitlab-dedicated/#handling-configuration-changes-for-tenant-environments).
@@ -278,10 +280,22 @@ To enable an Outbound Private Link:
GitLab then configures the tenant instance to create the necessary Endpoint Interfaces based on the service names you provided. Any matching outbound
connections made from the tenant GitLab instance are directed through the PrivateLink into your VPC.
-#### Custom certificates
+### Custom certificates
In some cases, the GitLab Dedicated instance can't reach an internal service you own because it exposes a certificate that can't be validated using a public Certification Authority (CA). In these cases, custom certificates are required.
+#### Add a custom certificate with Switchboard
+
+1. Log in to [Switchboard](https://console.gitlab-dedicated.com/).
+1. At the top of the page, select **Configuration**.
+1. Expand **Custom Certificate Authorities**.
+1. Select **+ Add Certificate**.
+1. Paste the certificate into the text box.
+1. Select **Save**.
+1. Scroll up to the top of the page and select whether to apply the changes immediately or during the next maintenance window.
+
+#### Add a custom certificate with a Support Request
+
To request that GitLab add custom certificates when communicating with your services over PrivateLink, attach the custom public certificate files to your [support ticket](https://support.gitlab.com/hc/en-us/requests/new?ticket_form_id=4414917877650).
#### Maximum number of reverse PrivateLink connections
@@ -292,6 +306,19 @@ GitLab Dedicated limits the number of reverse PrivateLink connections to 10.
GitLab Dedicated allows you to control which IP addresses can access your instance through an IP allowlist.
+#### Add an IP to the allowlist with Switchboard
+
+1. Log in to [Switchboard](https://console.gitlab-dedicated.com/).
+1. At the top of the page, select **Configuration**.
+1. Expand **Allowed Source List Config / IP allowlist**.
+1. Turn on the **Enable** toggle.
+1. Select **Add Item**.
+1. Enter the IP address and description. To add another IP address, repeat steps 5 and 6.
+1. Select **Save**.
+1. Scroll up to the top of the page and select whether to apply the changes immediately or during the next maintenance window.
+
+#### Add an IP to the allowlist with a Support Request
+
Specify a comma separated list of IP addresses that can access your GitLab Dedicated instance in your [support ticket](https://support.gitlab.com/hc/en-us/requests/new?ticket_form_id=4414917877650). After the configuration has been applied, when an IP not on the allowlist tries to access your instance, the connection is refused.
### SAML
@@ -303,6 +330,23 @@ Prerequisites:
- You must configure the identity provider before sending the required data to GitLab.
+#### Activate SAML with Switchboard
+
+To activate SAML for your GitLab Dedicated instance:
+
+1. Log in to [Switchboard](https://console.gitlab-dedicated.com/).
+1. At the top of the page, select **Configuration**.
+1. Expand **SAML Config**.
+1. Turn on the **Enable** toggle.
+1. Complete the fields.
+1. Select **Save**.
+1. Scroll up to the top of the page and select whether to apply the changes immediately or during the next maintenance window.
+1. To verify the SAML configuration is successful:
+ - Check that the SSO button description is displayed on your instance's sign-in page.
+ - Go to the metadata URL of your instance (`https://INSTANCE-URL/users/auth/saml/metadata`). This page can be used to simplify much of the configuration of the identity provider, and manually validate the settings.
+
+#### Activate SAML with a Support Request
+
To activate SAML for your GitLab Dedicated instance:
1. To make the necessary changes, include the desired [SAML configuration block](../../integration/saml.md#configure-saml-support-in-gitlab) for your GitLab application in your [support ticket](https://support.gitlab.com/hc/en-us/requests/new?ticket_form_id=4414917877650). At a minimum, GitLab needs the following information to enable SAML for your instance:
diff --git a/doc/administration/geo/disaster_recovery/index.md b/doc/administration/geo/disaster_recovery/index.md
index d6f6211ed4c..2f636dc6ba4 100644
--- a/doc/administration/geo/disaster_recovery/index.md
+++ b/doc/administration/geo/disaster_recovery/index.md
@@ -88,6 +88,7 @@ Note the following when promoting a secondary:
- If you encounter an `ActiveRecord::RecordInvalid: Validation failed: Name has already been taken`
error message during this process, for more information, see this
[troubleshooting advice](../replication/troubleshooting.md#fixing-errors-during-a-failover-or-when-promoting-a-secondary-to-a-primary-site).
+- You should [point the primary domain DNS at the newly promoted site](#step-4-optional-updating-the-primary-domain-dns-record). Otherwise, runners must be registered again with the newly promoted site, and all Git remotes, bookmarks, and external integrations must be updated.
#### Promoting a **secondary** site running on a single node running GitLab 14.5 and later
diff --git a/doc/administration/geo/index.md b/doc/administration/geo/index.md
index 78bd685e06f..e8b2cb38563 100644
--- a/doc/administration/geo/index.md
+++ b/doc/administration/geo/index.md
@@ -19,8 +19,6 @@ Fetching large repositories can take a long time for teams located far from a si
Geo provides local, read-only sites of your GitLab instances. This can reduce the time it takes
to clone and fetch large repositories, speeding up development.
-For a video introduction to Geo, see [Introduction to GitLab Geo - GitLab Features](https://www.youtube.com/watch?v=-HDLxSjEh6w).
-
To make sure you're using the right version of the documentation, go to [the Geo page on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/administration/geo/index.md) and choose the appropriate release from the **Switch branch/tag** dropdown list. For example, [`v13.7.6-ee`](https://gitlab.com/gitlab-org/gitlab/-/blob/v13.7.6-ee/doc/administration/geo/index.md).
Geo uses a set of defined terms that are described in the [Geo Glossary](glossary.md).
@@ -208,6 +206,7 @@ This list of limitations only reflects the latest version of GitLab. If you are
- For Git over SSH, to make the project clone URL display correctly regardless of which site you are browsing, secondary sites must use the same port as the primary. [GitLab issue #339262](https://gitlab.com/gitlab-org/gitlab/-/issues/339262) proposes to remove this limitation.
- Git push over SSH against a secondary site does not work for pushes over 1.86 GB. [GitLab issue #413109](https://gitlab.com/gitlab-org/gitlab/-/issues/413109) tracks this bug.
- Backups [cannot be run on secondaries](replication/troubleshooting.md#message-error-canceling-statement-due-to-conflict-with-recovery).
+- Git clone and fetch requests with option `--depth` over SSH against a secondary site does not work and hangs indefinitely if the secondary site is not up to date at the time the request is initiated. For more information, see [issue 391980](https://gitlab.com/gitlab-org/gitlab/-/issues/391980).
### Limitations on replication/verification
diff --git a/doc/administration/geo/replication/troubleshooting.md b/doc/administration/geo/replication/troubleshooting.md
index 3c2d43d196a..dd021695800 100644
--- a/doc/administration/geo/replication/troubleshooting.md
+++ b/doc/administration/geo/replication/troubleshooting.md
@@ -1231,7 +1231,7 @@ status
### Failed verification of Uploads on the primary Geo site
-If some Uploads verification is failing on the primary Geo site with the `verification_checksum: nil` and `verification_failure: Error during verification: undefined method 'underscore' for NilClass:Class` errros, this can be due to orphaned Uploads. The parent record owning the Upload (the Upload's `model`) has somehow been deleted, but the Upload record still exists. These verification failures are false.
+If verification of some uploads is failing on the primary Geo site with `verification_checksum = nil` and with the ``verification_failure = Error during verification: undefined method `underscore' for NilClass:Class``, this can be due to orphaned Uploads. The parent record owning the Upload (the upload's model) has somehow been deleted, but the Upload record still exists. These verification failures are false.
You can find these errors in the `geo.log` file on the primary Geo site.
@@ -1249,7 +1249,7 @@ You can delete these Upload records on the primary Geo site to get rid of these
uploads = Geo::UploadState.where(
verification_checksum: nil,
verification_state: 3,
- verification_failure: "Error during verification: undefined method 'underscore' for NilClass:Class"
+ verification_failure: "Error during verification: undefined method `underscore' for NilClass:Class"
).pluck(:upload_id)
uploads_deleted = 0
@@ -1434,8 +1434,8 @@ If you are using the Linux package installation, something might have failed dur
### GitLab indicates that more than 100% of repositories were synced
-This can be caused by orphaned records in the project registry. You can clear them
-[using the Rake task to remove orphaned project registries](../../../administration/raketasks/geo.md#remove-orphaned-project-registries).
+This can be caused by orphaned records in the project registry. They are being cleaned
+periodically using a registry worker, so give it some time to fix it itself.
### Secondary site shows "Unhealthy" in UI after changing the value of `external_url` for the primary site
diff --git a/doc/administration/geo/setup/index.md b/doc/administration/geo/setup/index.md
index ea3bb5afc24..f59dec17f8b 100644
--- a/doc/administration/geo/setup/index.md
+++ b/doc/administration/geo/setup/index.md
@@ -31,6 +31,8 @@ a single-node Geo site or a multi-node Geo site.
If both Geo sites are based on the [1K reference architecture](../../reference_architectures/1k_users.md), follow
[Set up Geo for two single-node sites](two_single_node_sites.md).
+If using external PostgreSQL services, for example Amazon RDS, follow [Set up Geo for two single-node sites (with external PostgreSQL services)](two_single_node_external_services.md).
+
Depending on your GitLab deployment, [additional configuration](#additional-configuration) for LDAP, object storage, and the Container Registry might be required.
### Multi-node Geo sites
diff --git a/doc/administration/gitaly/configure_gitaly.md b/doc/administration/gitaly/configure_gitaly.md
index f62f0a5a4e2..15ace9c4ed9 100644
--- a/doc/administration/gitaly/configure_gitaly.md
+++ b/doc/administration/gitaly/configure_gitaly.md
@@ -27,6 +27,7 @@ The following configuration options are also available:
- Enabling [TLS support](#enable-tls-support).
- Limiting [RPC concurrency](#limit-rpc-concurrency).
+- Limiting [pack-objects concurrency](#limit-pack-objects-concurrency).
## About the Gitaly token
@@ -361,7 +362,7 @@ Configure Gitaly server in one of two ways:
WARNING:
If directly copying repository data from a GitLab server to Gitaly, ensure that the metadata file,
default path `/var/opt/gitlab/git-data/repositories/.gitaly-metadata`, is not included in the transfer.
-Copying this file causes GitLab to use the [Rugged patches](index.md#direct-access-to-git-in-gitlab) for repositories hosted on the Gitaly server,
+Copying this file causes GitLab to use the direct disk access to repositories hosted on the Gitaly server,
leading to `Error creating pipeline` and `Commit not found` errors, or stale data.
### Configure Gitaly clients
@@ -665,6 +666,8 @@ Configure Gitaly with TLS in one of two ways:
```
1. Save the file and [reconfigure GitLab](../restart_gitlab.md#reconfigure-a-linux-package-installation).
+1. Run `sudo gitlab-rake gitlab:gitaly:check` on the Gitaly client (for example, the
+ Rails application) to confirm it can connect to Gitaly servers.
1. Verify Gitaly traffic is being served over TLS by
[observing the types of Gitaly connections](#observe-type-of-gitaly-connections).
1. Optional. Improve security by:
@@ -751,6 +754,43 @@ Configure Gitaly with TLS in one of two ways:
::EndTabs
+#### Update the certificates
+
+To update the Gitaly certificates after initial configuration:
+
+::Tabs
+
+:::TabTitle Linux package (Omnibus)
+
+If the content of your SSL certificates under the `/etc/gitlab/ssl` directory have been updated, but no configuration changes have been made to
+`/etc/gitlab/gitlab.rb`, then reconfiguring GitLab doesn’t affect Gitaly. Instead, you must restart Gitaly manually for the certificates to be loaded
+by the Gitaly process:
+
+```shell
+sudo gitlab-ctl restart gitaly
+```
+
+If you change or update the certificates in `/etc/gitlab/trusted-certs` without making changes to the `/etc/gitlab/gitlab.rb` file, you must:
+
+1. [Reconfigure GitLab](../restart_gitlab.md#reconfigure-a-linux-package-installation) so the symlinks for the trusted certificates are updated.
+1. Restart Gitaly manually for the certificates to be loaded by the Gitaly process:
+
+ ```shell
+ sudo gitlab-ctl restart gitaly
+ ```
+
+:::TabTitle Self-compiled (source)
+
+If the content of your SSL certificates under the `/etc/gitlab/ssl` directory have been updated, you must
+[restart GitLab](../restart_gitlab.md#self-compiled-installations) for the certificates to be loaded by the Gitaly process.
+
+If you change or update the certificates in `/usr/local/share/ca-certificates`, you must:
+
+1. Run `sudo update-ca-certificates` to update the system's trusted store.
+1. [Restart GitLab](../restart_gitlab.md#self-compiled-installations) for the certificates to be loaded by the Gitaly process.
+
+::EndTabs
+
### Observe type of Gitaly connections
For information on observing the type of Gitaly connections being served, see the
@@ -866,6 +906,126 @@ When the pack-object cache is enabled, pack-objects limiting kicks in only if th
You can observe the behavior of this queue using Gitaly logs and Prometheus. For more information, see
[Monitor Gitaly pack-objects concurrency limiting](monitoring.md#monitor-gitaly-pack-objects-concurrency-limiting).
+## Adaptive concurrency limiting
+
+> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/10734) in GitLab 16.6.
+
+Gitaly supports two concurrency limits:
+
+- An [RPC concurrency limit](#limit-rpc-concurrency), which allow you to configure a maximum number of simultaneous in-flight requests for each
+ Gitaly RPC. The limit is scoped by RPC and repository.
+- A [Pack-objects concurrency limit](#limit-pack-objects-concurrency), which restricts the number of concurrent Git data transfer request by IP.
+
+If this limit is exceeded, either:
+
+- The request is put in a queue.
+- The request is rejected if the queue is full or if the request remains in the queue for too long.
+
+Both of these concurrency limits can be configured statically. Though static limits can yield good protection results, they have some drawbacks:
+
+- Static limits are not good for all usage patterns. There is no one-size-fits-all value. If the limit is too low, big repositories are
+ negatively impacted. If the limit is too high, the protection is essentially lost.
+- It's tedious to maintain a sane value for the concurrency limit, especially when the workload of each repository changes over time.
+- A request can be rejected even though the server is idle because the rate doesn't factor in the load on the server.
+
+You can overcome all of these drawbacks and keep the benefits of concurrency limiting by configuring adaptive concurrency limits. Adaptive
+concurrency limits are optional and build on the two concurrency limiting types. It uses Additive Increase/Multiplicative Decrease (AIMD)
+algorithm. Each adaptive limit:
+
+- Gradually increases up to a certain upper limit during typical process functioning.
+- Quickly decreases when the host machine has a resource problem.
+
+This mechanism provides some headroom for the machine to "breathe" and speeds up current inflight requests.
+
+![Gitaly Adaptive Concurrency Limit](img/gitaly_adaptive_concurrency_limit.png)
+
+The adaptive limiter calibrates the limits every 30 seconds and:
+
+- Increases the limits by one until reaching the upper limit.
+- Decreases the limits by half when the top-level cgroup has either memory usage that exceeds 90%, excluding highly-evictable page caches,
+ or CPU throttled for 50% or more of the observation time.
+
+Otherwise, the limits increase by one until reaching the upper bound. For more information about technical implementation
+of this system, please refer to [this blueprint](../../architecture/blueprints/gitaly_adaptive_concurrency_limit/index.md).
+
+Adaptive limiting is enabled for each RPC or pack-objects cache individually. However, limits are calibrated at the same time.
+
+### Enable adaptiveness for RPC concurrency
+
+Prerequisites:
+
+- Because adaptive limiting depends on [control groups](#control-groups), control groups must be enabled before using adaptive limiting.
+
+The following is an example to configure an adaptive limit for RPC concurrency:
+
+```ruby
+# in /etc/gitlab/gitlab.rb
+gitaly['configuration'] = {
+ # ...
+ concurrency: [
+ {
+ rpc: '/gitaly.SmartHTTPService/PostUploadPackWithSidechannel',
+ max_queue_wait: '1s',
+ max_queue_size: 10,
+ adaptive: true,
+ min_limit: 10,
+ initial_limit: 20,
+ max_limit: 40
+ },
+ {
+ rpc: '/gitaly.SSHService/SSHUploadPackWithSidechannel',
+ max_queue_wait: '10s',
+ max_queue_size: 20,
+ adaptive: true,
+ min_limit: 10,
+ initial_limit: 50,
+ max_limit: 100
+ },
+ ],
+}
+```
+
+In this example:
+
+- `adaptive` sets whether the adaptiveness is enabled. If set, the `max_per_repo` value is ignored in favor of the following configuration.
+- `initial_limit` is the per-repository concurrency limit to use when Gitaly starts.
+- `max_limit` is the minimum per-repository concurrency limit of the configured RPC. Gitaly increases the current limit
+ until it reaches this number.
+- `min_limit` is the is the minimum per-repository concurrency limit of the configured RPC. When the host machine has a resource problem,
+ Gitaly quickly reduces the limit until reaching this value.
+
+For more information, see [RPC concurrency](#limit-rpc-concurrency).
+
+### Enable adaptiveness for pack-objects concurrency
+
+Prerequisites:
+
+- Because adaptive limiting depends on [control groups](#control-groups), control groups must be enabled before using adaptive limiting.
+
+The following is an example to configure an adaptive limit for pack-objects concurrency:
+
+```ruby
+# in /etc/gitlab/gitlab.rb
+gitaly['pack_objects_limiting'] = {
+ 'max_queue_length' => 200,
+ 'max_queue_wait' => '60s',
+ 'adaptive' => true,
+ 'min_limit' => 10,
+ 'initial_limit' => 20,
+ 'max_limit' => 40
+}
+```
+
+In this example:
+
+- `adaptive` sets whether the adaptiveness is enabled. If set, the value of `max_concurrency` is ignored in favor of the following configuration.
+- `initial_limit` is the per-IP concurrency limit to use when Gitaly starts.
+- `max_limit` is the minimum per-IP concurrency limit for pack-objects. Gitaly increases the current limit until it reaches this number.
+- `min_limit` is the is the minimum per-IP concurrency limit for pack-objects. When the host machine has a resources problem, Gitaly quickly
+ reduces the limit until it reaches this value.
+
+For more information, see [pack-objects concurrency](#limit-pack-objects-concurrency).
+
## Control groups
WARNING:
@@ -1673,7 +1833,9 @@ Gitaly fails to start up if either:
## Configure server-side backups
-> [Introduced](https://gitlab.com/gitlab-org/gitaly/-/issues/4941) in GitLab 16.3.
+> - [Introduced](https://gitlab.com/gitlab-org/gitaly/-/issues/4941) in GitLab 16.3.
+> - Server-side support for restoring a specified backup instead of the latest backup [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132188) in GitLab 16.6.
+> - Server-side support for creating incremental backups [introduced](https://gitlab.com/gitlab-org/gitaly/-/merge_requests/6475) in GitLab 16.6.
Repository backups can be configured so that the Gitaly node that hosts each
repository is responsible for creating the backup and streaming it to
diff --git a/doc/administration/gitaly/img/gitaly_adaptive_concurrency_limit.png b/doc/administration/gitaly/img/gitaly_adaptive_concurrency_limit.png
new file mode 100644
index 00000000000..ce6bb1a8dfc
--- /dev/null
+++ b/doc/administration/gitaly/img/gitaly_adaptive_concurrency_limit.png
Binary files differ
diff --git a/doc/administration/gitaly/index.md b/doc/administration/gitaly/index.md
index 46f6a5829c8..6784ff4d970 100644
--- a/doc/administration/gitaly/index.md
+++ b/doc/administration/gitaly/index.md
@@ -587,92 +587,6 @@ off Gitaly Cluster to a sharded Gitaly instance:
1. [Move the repositories](../operations/moving_repositories.md#moving-repositories) to the newly created storage. You can
move them by shard or by group, which gives you the opportunity to spread them over multiple Gitaly servers.
-## Direct access to Git in GitLab
-
-Direct access to Git uses code in GitLab known as the "Rugged patches".
-
-Before Gitaly existed, what are now Gitaly clients accessed Git repositories directly, either:
-
-- On a local disk in the case of a single-machine Linux package installation.
-- Using NFS in the case of a horizontally-scaled GitLab installation.
-
-In addition to running plain `git` commands, GitLab used a Ruby library called
-[Rugged](https://github.com/libgit2/rugged). Rugged is a wrapper around
-[libgit2](https://libgit2.org/), a stand-alone implementation of Git in the form of a C library.
-
-Over time it became clear that Rugged, particularly in combination with
-[Unicorn](https://yhbt.net/unicorn/), is extremely efficient. Because `libgit2` is a library and
-not an external process, there was very little overhead between:
-
-- GitLab application code that tried to look up data in Git repositories.
-- The Git implementation itself.
-
-Because the combination of Rugged and Unicorn was so efficient, the GitLab application code ended up
-with lots of duplicate Git object lookups. For example, looking up the default branch commit a dozen
-times in one request. We could write inefficient code without poor performance.
-
-When we migrated these Git lookups to Gitaly calls, we suddenly had a much higher fixed cost per Git
-lookup. Even when Gitaly is able to re-use an already-running `git` process (for example, to look up
-a commit), you still have:
-
-- The cost of a network roundtrip to Gitaly.
-- Inside Gitaly, a write/read roundtrip on the Unix pipes that connect Gitaly to the `git` process.
-
-Using GitLab.com to measure, we reduced the number of Gitaly calls per request until we no longer felt
-the efficiency loss of losing Rugged. It also helped that we run Gitaly itself directly on the Git
-file servers, rather than by using NFS mounts. This gave us a speed boost that counteracted the
-negative effect of not using Rugged anymore.
-
-Unfortunately, other deployments of GitLab could not remove NFS like we did on GitLab.com, and they
-got the worst of both worlds:
-
-- The slowness of NFS.
-- The increased inherent overhead of Gitaly.
-
-The code removed from GitLab during the Gitaly migration project affected these deployments. As a
-performance workaround for these NFS-based deployments, we re-introduced some of the old Rugged
-code. This re-introduced code is informally referred to as the "Rugged patches".
-
-### Automatic detection
-
-> Automatic detection for Rugged [disabled](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/95445) in GitLab 15.3.
-
-FLAG:
-On self-managed GitLab, by default automatic detection of whether Rugged should be used (per storage) is not available.
-To make it available, an administrator can [disable the feature flag](../../administration/feature_flags.md) named
-`skip_rugged_auto_detect`.
-
-The Ruby methods that perform direct Git access are behind
-[feature flags](../../development/gitaly.md#legacy-rugged-code), disabled by default. It wasn't
-convenient to set feature flags to get the best performance, so we added an automatic mechanism that
-enables direct Git access.
-
-When GitLab calls a function that has a "Rugged patch", it performs two checks:
-
-- Is the feature flag for this patch set in the database? If so, the feature flag setting controls
- the GitLab use of "Rugged patch" code.
-- If the feature flag is not set, GitLab tries accessing the file system underneath the
- Gitaly server directly. If it can, it uses the "Rugged patch":
- - If using Puma and [thread count](../../install/requirements.md#puma-threads) is set
- to `1`.
-
-The result of these checks is cached.
-
-To see if GitLab can access the repository file system directly, we use the following heuristic:
-
-- Gitaly ensures that the file system has a metadata file in its root with a UUID in it.
-- Gitaly reports this UUID to GitLab by using the `ServerInfo` RPC.
-- GitLab Rails tries to read the metadata file directly. If it exists, and if the UUIDs match,
- assume we have direct access.
-
-Direct Git access is:
-
-- [Disabled](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/95445) by default in GitLab 15.3 and later for
- compatibility with [Praefect-generated replica paths](#praefect-generated-replica-paths-gitlab-150-and-later). It
- can be enabled if Rugged [feature flags](../../development/gitaly.md#legacy-rugged-code) are enabled.
-- Enabled by default in GitLab 15.2 and earlier because it fills in the correct repository paths in the GitLab
- configuration file `config/gitlab.yml`. This satisfies the UUID check.
-
### Transition to Gitaly Cluster
For the sake of removing complexity, we must remove direct Git access in GitLab. However, we can't
diff --git a/doc/administration/gitaly/monitoring.md b/doc/administration/gitaly/monitoring.md
index cbf5722f2c5..5d8de42666b 100644
--- a/doc/administration/gitaly/monitoring.md
+++ b/doc/administration/gitaly/monitoring.md
@@ -90,6 +90,47 @@ In Prometheus, look for the following metrics:
- `gitaly_pack_objects_queued` indicates how many requests for pack-objects processes are waiting due to the concurrency limit being reached.
- `gitaly_pack_objects_acquiring_seconds` indicates how long a request for a pack-object process has to wait due to concurrency limits before being processed.
+## Monitor Gitaly adaptive concurrency limiting
+
+> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/10734) in GitLab 16.6.
+
+You can observe specific behavior of [adaptive concurrency limiting](configure_gitaly.md#adaptive-concurrency-limiting) using Gitaly logs and Prometheus.
+
+In the [Gitaly logs](../logs/index.md#gitaly-logs), you can identify logs related to the adaptive concurrency limiting when the current limits are adjusted.
+You can filter the content of the logs (`msg`) for "Multiplicative decrease" and "Additive increase" messages.
+
+| Log Field | Description |
+|:---|:---|
+| `limit` | The name of the limit being adjusted. |
+| `previous_limit` | The previous limit before it was increased or decreased. |
+| `new_limit` | The new limit after it was increased or decreased. |
+| `watcher` | The resource watcher that decided the node is under pressure. For example: `CgroupCpu` or `CgroupMemory`. |
+| `reason` | The reason behind limit adjustment. |
+| `stats.*` | Some statistics behind an adjustment decision. They are for debugging purposes. |
+
+Example log:
+
+```json
+{
+ "msg": "Multiplicative decrease",
+ "limit": "pack-objects",
+ "new_limit": 14,
+ "previous_limit": 29,
+ "reason": "cgroup CPU throttled too much",
+ "watcher": "CgroupCpu",
+ "stats.time_diff": 15.0,
+ "stats.throttled_duration": 13.0,
+ "stat.sthrottled_threshold": 0.5
+}
+```
+
+In Prometheus, look for the following metrics:
+
+- `gitaly_concurrency_limiting_current_limit` The current limit value of an adaptive concurrency limit.
+- `gitaly_concurrency_limiting_watcher_errors_total` indicates the total number of watcher errors while fetching resource metrics.
+- `gitaly_concurrency_limiting_backoff_events_total` indicates the total number of backoff events, which are when the limits being
+ adjusted due to resource pressure.
+
## Monitor Gitaly cgroups
You can observe the status of [control groups (cgroups)](configure_gitaly.md#control-groups) using Prometheus:
diff --git a/doc/administration/gitaly/recovery.md b/doc/administration/gitaly/recovery.md
index 45bde083a1a..6779823c941 100644
--- a/doc/administration/gitaly/recovery.md
+++ b/doc/administration/gitaly/recovery.md
@@ -15,12 +15,17 @@ You can add and replace Gitaly nodes on a Gitaly Cluster.
### Add new Gitaly nodes
-To add a new Gitaly node to a Gitaly Cluster that has [replication factor](praefect.md#configure-replication-factor):
+The steps to add a new Gitaly node to a Gitaly Cluster depend on whether a [custom replication factor](praefect.md#configure-replication-factor) is set.
-- Set, set the [replication factor](praefect.md#configure-replication-factor) for each repository using `set-replication-factor` Praefect command. New repositories are
- replicated based on [replication factor](praefect.md#configure-replication-factor). Praefect doesn't automatically replicate existing repositories to the new Gitaly node.
-- Not set, add the new node in your [Praefect configuration](praefect.md#praefect) under `praefect['virtual_storages']`. Praefect automatically replicates all data to any
- new Gitaly node added to the configuration.
+#### Custom replication factor
+
+If a custom replication factor is set, set the [replication factor](praefect.md#configure-replication-factor) for each repository using the
+`set-replication-factor` Praefect command. New repositories are replicated based on the [replication factor](praefect.md#configure-replication-factor). Praefect doesn't automatically replicate existing repositories to the new Gitaly node.
+
+#### Default replication factor
+
+If the default replication factor is used, add the new node in your [Praefect configuration](praefect.md#praefect) under `praefect['virtual_storages']`.
+Praefect automatically replicates all data to any new Gitaly node added to the configuration.
### Replace an existing Gitaly node
@@ -33,32 +38,37 @@ To use the same name for the replacement node, use [repository verifier](praefec
#### With a node with a different name
-To use a different name for the replacement node for a Gitaly Cluster that has [replication factor](praefect.md#configure-replication-factor):
+The steps use a different name for the replacement node for a Gitaly Cluster depend on if a [custom replication factor](praefect.md#configure-replication-factor)
+is set.
-- Set, use [`praefect set-replication-factor`](praefect.md#configure-replication-factor) to set the replication factor per repository again to get new storage assigned.
- For example:
+##### Custom replication factor set
- ```shell
- $ sudo /opt/gitlab/embedded/bin/praefect -config /var/opt/gitlab/praefect/config.toml set-replication-factor -virtual-storage default -relative-path @hashed/3f/db/3fdba35f04dc8c462986c992bcf875546257113072a909c162f7e470e581e278.git -replication-factor 2
+If a custom replication factor is set, use [`praefect set-replication-factor`](praefect.md#configure-replication-factor) to set the replication factor per repository again to get new storage assigned. For example:
- current assignments: gitaly-1, gitaly-2
- ```
+```shell
+$ sudo /opt/gitlab/embedded/bin/praefect -config /var/opt/gitlab/praefect/config.toml set-replication-factor -virtual-storage default -relative-path @hashed/3f/db/3fdba35f04dc8c462986c992bcf875546257113072a909c162f7e470e581e278.git -replication-factor 2
+
+current assignments: gitaly-1, gitaly-2
+```
+
+To reassign all repositories from the old storage to the new one, after configuring the new Gitaly node:
- To reassign all repositories from the old storage to the new one, after configuring the new Gitaly node:
+1. Connect to Praefect database:
- 1. Connect to Praefect database:
+ ```shell
+ /opt/gitlab/embedded/bin/psql -h <psql host> -U <user> -d <database name>
+ ```
- ```shell
- /opt/gitlab/embedded/bin/psql -h <psql host> -U <user> -d <database name>
- ```
+1. Update the `repository_assignments` table to replace the old Gitaly node name (for example, `old-gitaly`) with the new Gitaly node name
+ (for example, `new-gitaly`):
- 1. Update `repository_assignments` table to replace the old Gitaly node name (for example, `old-gitaly`) with the new Gitaly node name (for example, `new-gitaly`):
+ ```sql
+ UPDATE repository_assignments SET storage='new-gitaly' WHERE storage='old-gitaly';
+ ```
- ```sql
- UPDATE repository_assignments SET storage='new-gitaly' WHERE storage='old-gitaly';
- ```
+##### Default replication factor
-- Not set, replace the node in the configuration. The old node's state remains in the Praefect database but it is ignored.
+If the default replication factor is used, replace the node in the configuration. The old node's state remains in the Praefect database but it is ignored.
## Primary node failure
diff --git a/doc/administration/gitaly/troubleshooting.md b/doc/administration/gitaly/troubleshooting.md
index 556bc29b76f..17687cbb181 100644
--- a/doc/administration/gitaly/troubleshooting.md
+++ b/doc/administration/gitaly/troubleshooting.md
@@ -387,6 +387,43 @@ If Git pushes are too slow when Dynatrace is enabled, disable Dynatrace.
One way to resolve this is to make sure the entry is correct for the GitLab internal API URL configured in `gitlab.rb` with `gitlab_rails['internal_api_url']`.
+### Changes (diffs) don't load for new merge requests when using Gitaly TLS
+
+After enabling [Gitaly with TLS](configure_gitaly.md#enable-tls-support), changes (diffs) for new merge requests are not generated
+and you see the following message in GitLab:
+
+```plaintext
+Building your merge request... This page will update when the build is complete
+```
+
+Gitaly must be able to connect to itself to complete some operations. If the Gitaly certificate is not trusted by the Gitaly server,
+merge request diffs can't be generated.
+
+If Gitaly can't connect to itself, you see messages in the [Gitaly logs](../../administration/logs/index.md#gitaly-logs) like the following messages:
+
+```json
+{
+ "level":"warning",
+ "msg":"[core] [Channel #16 SubChannel #17] grpc: addrConn.createTransport failed to connect to {Addr: \"ext-gitaly.example.com:9999\", ServerName: \"ext-gitaly.example.com:9999\", }. Err: connection error: desc = \"transport: authentication handshake failed: tls: failed to verify certificate: x509: certificate signed by unknown authority\"",
+ "pid":820,
+ "system":"system",
+ "time":"2023-11-06T05:40:04.169Z"
+}
+{
+ "level":"info",
+ "msg":"[core] [Server #3] grpc: Server.Serve failed to create ServerTransport: connection error: desc = \"ServerHandshake(\\\"x.x.x.x:x\\\") failed: wrapped server handshake: remote error: tls: bad certificate\"",
+ "pid":820,
+ "system":"system",
+ "time":"2023-11-06T05:40:04.169Z"
+}
+```
+
+To resolve the problem, ensure that you have added your Gitaly certificate to the `/etc/gitlab/trusted-certs` folder on the Gitaly server
+and:
+
+1. [Reconfigure GitLab](../restart_gitlab.md#reconfigure-a-linux-package-installation) so the certificates are symlinked
+1. Restart Gitaly manually `sudo gitlab-ctl restart gitaly` for the certificates to be loaded by the Gitaly process.
+
## Gitaly fails to fork processes stored on `noexec` file systems
Because of changes [introduced](https://gitlab.com/gitlab-org/omnibus-gitlab/-/merge_requests/5999) in GitLab 14.10, applying the `noexec` option to a mount
diff --git a/doc/administration/inactive_project_deletion.md b/doc/administration/inactive_project_deletion.md
index b7f71505e70..7ccd3455011 100644
--- a/doc/administration/inactive_project_deletion.md
+++ b/doc/administration/inactive_project_deletion.md
@@ -34,10 +34,13 @@ To configure deletion of inactive projects:
1. Select **Save changes**.
Inactive projects that meet the criteria are scheduled for deletion and a warning email is sent. If the
-projects remain inactive, they are deleted after the specified duration.
+projects remain inactive, they are deleted after the specified duration. These projects are deleted even if
+[the project is archived](../user/project/settings/index.md#archive-a-project).
### Configuration example
+#### Example 1
+
If you use these settings:
- **Delete inactive projects** enabled.
@@ -52,6 +55,20 @@ If a project is more than 50 MB and it is inactive for:
- More than 6 months: A deletion warning email is sent. This mail includes the date that the project will be deleted.
- More than 12 months: The project is scheduled for deletion.
+#### Example 2
+
+If you use these settings:
+
+- **Delete inactive projects** enabled.
+- **Delete inactive projects that exceed** set to `0`.
+- **Delete project after** set to `12`.
+- **Send warning email** set to `11`.
+
+If a project exists that has already been inactive for more than 12 months when you configure these settings:
+
+- A deletion warning email is sent immediately. This email includes the date that the project will be deleted.
+- The project is scheduled for deletion 1 month (12 months - 11 months) after warning email.
+
## Determine when a project was last active
You can view a project's activities and determine when the project was last active in the following ways:
diff --git a/doc/administration/incoming_email.md b/doc/administration/incoming_email.md
index 6948009aab2..33afaf19220 100644
--- a/doc/administration/incoming_email.md
+++ b/doc/administration/incoming_email.md
@@ -68,7 +68,8 @@ this method only supports replies, and not the other features of [incoming email
## Accepted headers
-> Accepting `Received` headers [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/81489) in GitLab 14.9.
+> - Accepting `Received` headers [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/81489) in GitLab 14.9.
+> - Accepting `Cc` headers [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/348572) in GitLab 16.5.
Email is processed correctly when a configured email address is present in one of the following headers
(sorted in the order they are checked):
@@ -77,6 +78,7 @@ Email is processed correctly when a configured email address is present in one o
- `Delivered-To`
- `Envelope-To` or `X-Envelope-To`
- `Received`
+- `Cc`
The `References` header is also accepted, however it is used specifically to relate email responses to existing discussion threads. It is not used for creating issues by email.
@@ -86,8 +88,7 @@ also checks accepted headers.
Usually, the "To" field contains the email address of the primary receiver.
However, it might not include the configured GitLab email address if:
-- The address is in the "CC" field.
-- The address was included when using "Reply all".
+- The address is in the "BCC" field.
- The email was forwarded.
The `Received` header can contain multiple email addresses. These are checked in the order that they appear.
diff --git a/doc/administration/instance_limits.md b/doc/administration/instance_limits.md
index 8f03a2224ec..d5855e3c832 100644
--- a/doc/administration/instance_limits.md
+++ b/doc/administration/instance_limits.md
@@ -309,7 +309,7 @@ The number of seconds GitLab waits for an HTTP response after sending a webhook.
To change the webhook timeout value:
-1. Edit `/etc/gitlab/gitlab.rb`:
+1. Edit `/etc/gitlab/gitlab.rb` on all GitLab nodes that are running Sidekiq:
```ruby
gitlab_rails['webhook_timeout'] = 60
@@ -992,18 +992,19 @@ Set the limit to `0` to disable it.
## Math rendering limits
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132939) in GitLab 16.5.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132939) in GitLab 16.5.
+> - [Removed](https://gitlab.com/gitlab-org/gitlab/-/issues/368009) the 50-node limit from Wiki and repository files.
GitLab imposes default limits when rendering math in Markdown fields. These limits provide better security and performance.
-The limits for issues, merge requests, wikis, and repositories:
+The limits for issues, merge requests, epics, wikis, and repository files:
-- Maximum number of nodes rendered: `50`.
- Maximum number of macro expansions: `1000`.
-- Maximum user-specified size in em: `20`.
+- Maximum user-specified size in [em](https://en.wikipedia.org/wiki/Em_(typography)): `20`.
-The limits for issues and merge requests:
+The limits for issues, merge requests, and epics:
+- Maximum number of nodes rendered: `50`.
- Maximum number of characters in a math block: `1000`.
- Maximum rendering time: `2000 ms`.
diff --git a/doc/administration/integration/plantuml.md b/doc/administration/integration/plantuml.md
index 0155f0300d4..dae400ff755 100644
--- a/doc/administration/integration/plantuml.md
+++ b/doc/administration/integration/plantuml.md
@@ -180,7 +180,7 @@ see the [Tomcat Documentation](https://tomcat.apache.org/tomcat-10.1-doc/index.h
1. Install and configure Tomcat 10:
```shell
- wget https://dlcdn.apache.org/tomcat/tomcat-10/v10.1.9/bin/apache-tomcat-10.1.9.tar.gz -P /tmp
+ wget https://dlcdn.apache.org/tomcat/tomcat-10/v10.1.15/bin/apache-tomcat-10.1.15.tar.gz -P /tmp
sudo tar xzvf /tmp/apache-tomcat-10*tar.gz -C /opt/tomcat --strip-components=1
sudo chown -R tomcat:tomcat /opt/tomcat/
sudo chmod -R u+x /opt/tomcat/bin
@@ -266,12 +266,11 @@ see the [Tomcat Documentation](https://tomcat.apache.org/tomcat-10.1-doc/index.h
1. Install PlantUML and copy the `.war` file:
- Use the [latest release](https://github.com/plantuml/plantuml-server/releases) of plantuml-jsp (example: plantuml-jsp-v1.2023.8.war). For context, see [this issue](https://github.com/plantuml/plantuml-server/issues/265).
+ Use the [latest release](https://github.com/plantuml/plantuml-server/releases) of plantuml-jsp (example: plantuml-jsp-v1.2023.12.war). For context, see [this issue](https://github.com/plantuml/plantuml-server/issues/265).
```shell
- cd /
- wget https://github.com/plantuml/plantuml-server/releases/download/v1.2023.8/plantuml-jsp-v1.2023.8.war
- sudo cp plantuml-jsp-v1.2023.8.war /opt/tomcat/webapps/plantuml.war
+ wget -P /tmp https://github.com/plantuml/plantuml-server/releases/download/v1.2023.12/plantuml-jsp-v1.2023.12.war
+ sudo cp /tmp/plantuml-jsp-v1.2023.12.war /opt/tomcat/webapps/plantuml.war
sudo chown tomcat:tomcat /opt/tomcat/webapps/plantuml.war
sudo systemctl restart tomcat
```
diff --git a/doc/administration/logs/index.md b/doc/administration/logs/index.md
index e7277ab3186..3bb26681fae 100644
--- a/doc/administration/logs/index.md
+++ b/doc/administration/logs/index.md
@@ -806,12 +806,12 @@ GraphQL queries are recorded in the file. For example:
{"query_string":"query IntrospectionQuery{__schema {queryType { name },mutationType { name }}}...(etc)","variables":{"a":1,"b":2},"complexity":181,"depth":1,"duration_s":7}
```
-## `clickhouse.log` **(SAAS)**
+## `clickhouse.log`
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/133371) in GitLab 16.5.
-The `clickhouse.log` file logs information related to
-Clickhouse database client within GitLab.
+The `clickhouse.log` file logs information related to the
+ClickHouse database client in GitLab.
## `migrations.log`
diff --git a/doc/administration/logs/log_parsing.md b/doc/administration/logs/log_parsing.md
index 21ce3d7f17f..b281620fcf3 100644
--- a/doc/administration/logs/log_parsing.md
+++ b/doc/administration/logs/log_parsing.md
@@ -96,10 +96,10 @@ grep <PROJECT_NAME> <FILE> | jq .
jq 'select(.duration_s > 5000)' <FILE>
```
-#### Find all project requests with more than 5 rugged calls
+#### Find all project requests with more than 5 Gitaly calls
```shell
-grep <PROJECT_NAME> <FILE> | jq 'select(.rugged_calls > 5)'
+grep <PROJECT_NAME> <FILE> | jq 'select(.gitaly_calls > 5)'
```
#### Find all requests with a Gitaly duration > 10 seconds
@@ -273,8 +273,8 @@ jq --raw-output --slurp '
.[2]."grpc.time_ms",
.[0]."grpc.request.glProjectPath"
]
- | @sh' current \
-| awk 'BEGIN { printf "%7s %10s %10s %10s\t%s\n", "CT", "MAX DURS", "", "", "PROJECT" }
+ | @sh' current |
+ awk 'BEGIN { printf "%7s %10s %10s %10s\t%s\n", "CT", "MAX DURS", "", "", "PROJECT" }
{ printf "%7u %7u ms, %7u ms, %7u ms\t%s\n", $1, $2, $3, $4, $5 }'
```
@@ -288,12 +288,18 @@ jq --raw-output --slurp '
...
```
+#### Types of user and project activity overview
+
+```shell
+jq --raw-output '[.username, ."grpc.method", ."grpc.request.glProjectPath"] | @tsv' current | sort | uniq -c | sort -n
+```
+
#### Find all projects affected by a fatal Git problem
```shell
-grep "fatal: " current | \
- jq '."grpc.request.glProjectPath"' | \
- sort | uniq
+grep "fatal: " current |
+ jq '."grpc.request.glProjectPath"' |
+ sort | uniq
```
### Parsing `gitlab-shell/gitlab-shell.log`
diff --git a/doc/administration/merge_request_diffs.md b/doc/administration/merge_request_diffs.md
index 746dccb99d6..9c4ddcdc094 100644
--- a/doc/administration/merge_request_diffs.md
+++ b/doc/administration/merge_request_diffs.md
@@ -21,7 +21,9 @@ that only [stores outdated diffs](#alternative-in-database-storage) outside of d
## Using external storage
-For Linux package installations:
+::Tabs
+
+:::TabTitle Linux package (Omnibus)
1. Edit `/etc/gitlab/gitlab.rb` and add the following line:
@@ -41,7 +43,7 @@ For Linux package installations:
1. Save the file and [reconfigure GitLab](restart_gitlab.md#reconfigure-a-linux-package-installation) for the changes to take effect.
GitLab then migrates your existing merge request diffs to external storage.
-For self-compiled installations:
+:::TabTitle Self-compiled (source)
1. Edit `/home/git/gitlab/config/gitlab.yml` and add or amend the following
lines:
@@ -65,6 +67,8 @@ For self-compiled installations:
1. Save the file and [restart GitLab](restart_gitlab.md#self-compiled-installations) for the changes to take effect.
GitLab then migrates your existing merge request diffs to external storage.
+::EndTabs
+
## Using object storage
WARNING:
@@ -74,7 +78,9 @@ Instead of storing the external diffs on disk, we recommended the use of an obje
store like AWS S3 instead. This configuration relies on valid AWS credentials to
be configured already.
-For Linux package installations:
+::Tabs
+
+:::TabTitle Linux package (Omnibus)
1. Edit `/etc/gitlab/gitlab.rb` and add the following line:
@@ -86,7 +92,7 @@ For Linux package installations:
1. Save the file and [reconfigure GitLab](restart_gitlab.md#reconfigure-a-linux-package-installation) for the changes to take effect.
GitLab then migrates your existing merge request diffs to external storage.
-For self-compiled installations:
+:::TabTitle Self-compiled (source)
1. Edit `/home/git/gitlab/config/gitlab.yml` and add or amend the following
lines:
@@ -100,6 +106,8 @@ For self-compiled installations:
1. Save the file and [restart GitLab](restart_gitlab.md#self-compiled-installations) for the changes to take effect.
GitLab then migrates your existing merge request diffs to external storage.
+::EndTabs
+
[Read more about using object storage with GitLab](object_storage.md).
### Object Storage Settings
@@ -123,7 +131,9 @@ then `object_store:`. On Linux package installations, they are prefixed by
See [the available connection settings for different providers](object_storage.md#configure-the-connection-settings).
-For Linux package installations:
+::Tabs
+
+:::TabTitle Linux package (Omnibus)
1. Edit `/etc/gitlab/gitlab.rb` and add the following lines by replacing with
the values you want:
@@ -153,7 +163,7 @@ For Linux package installations:
1. Save the file and [reconfigure GitLab](restart_gitlab.md#reconfigure-a-linux-package-installation) for the changes to take effect.
-For self-compiled installations:
+:::TabTitle Self-compiled (source)
1. Edit `/home/git/gitlab/config/gitlab.yml` and add or amend the following
lines:
@@ -173,6 +183,8 @@ For self-compiled installations:
1. Save the file and [restart GitLab](restart_gitlab.md#self-compiled-installations) for the changes to take effect.
+::EndTabs
+
## Alternative in-database storage
Enabling external diffs may reduce the performance of merge requests, as they
@@ -182,7 +194,9 @@ in the database.
To enable this feature, perform the following steps:
-For Linux package installations:
+::Tabs
+
+:::TabTitle Linux package (Omnibus)
1. Edit `/etc/gitlab/gitlab.rb` and add the following line:
@@ -192,7 +206,7 @@ For Linux package installations:
1. Save the file and [reconfigure GitLab](restart_gitlab.md#reconfigure-a-linux-package-installation) for the changes to take effect.
-For self-compiled installations:
+:::TabTitle Self-compiled (source)
1. Edit `/home/git/gitlab/config/gitlab.yml` and add or amend the following
lines:
@@ -205,6 +219,8 @@ For self-compiled installations:
1. Save the file and [restart GitLab](restart_gitlab.md#self-compiled-installations) for the changes to take effect.
+::EndTabs
+
With this feature enabled, diffs are initially stored in the database, rather
than externally. They are moved to external storage after any of these
conditions become true:
@@ -217,64 +233,45 @@ These rules strike a balance between space and performance by only storing
frequently-accessed diffs in the database. Diffs that are less likely to be
accessed are moved to external storage instead.
-## Correcting incorrectly-migrated diffs
-
-Versions of GitLab earlier than `v13.0.0` would incorrectly record the location
-of some merge request diffs when [external diffs in object storage](#object-storage-settings)
-were enabled. This mainly affected imported merge requests, and was resolved
-with [this merge request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/31005).
-
-If you are using object storage, or have never used on-disk storage for external
-diffs, the **Changes** tab for some merge requests fails to load with a 500 error,
-and the exception for that error is of this form:
-
-```plain
-Errno::ENOENT (No such file or directory @ rb_sysopen - /var/opt/gitlab/gitlab-rails/shared/external-diffs/merge_request_diffs/mr-6167082/diff-8199789)
-```
-
-Then you are affected by this issue. Because it's not possible to safely determine
-all these conditions automatically, we've provided a Rake task in GitLab v13.2.0
-that you can run manually to correct the data:
-
-For Linux package installations:
-
-```shell
-sudo gitlab-rake gitlab:external_diffs:force_object_storage
-```
-
-For self-compiled installations:
+## Switching from external storage to object storage
-```shell
-sudo -u git -H bundle exec rake gitlab:external_diffs:force_object_storage RAILS_ENV=production
-```
+Automatic migration moves diffs stored in the database, but it does not move diffs between storage types.
+To switch from external storage to object storage:
-Environment variables can be provided to modify the behavior of the task. The
-available variables are:
+1. Move files stored on local or NFS storage to object storage manually.
+1. Run this Rake task to change their location in the database.
-| Name | Default value | Purpose |
-| ---- | ------------- | ------- |
-| `ANSI` | `true` | Use ANSI escape codes to make output more understandable |
-| `BATCH_SIZE` | `1000` | Iterate through the table in batches of this size |
-| `START_ID` | `nil` | If set, begin scanning at this ID |
-| `END_ID` | `nil` | If set, stop scanning at this ID |
-| `UPDATE_DELAY` | `1` | Number of seconds to sleep between updates |
+ For Linux package installations:
-The `START_ID` and `END_ID` variables may be used to run the update in parallel,
-by assigning different processes to different parts of the table. The `BATCH`
-and `UPDATE_DELAY` parameters allow the speed of the migration to be traded off
-against concurrent access to the table. The `ANSI` parameter should be set to
-false if your terminal does not support ANSI escape codes.
+ ```shell
+ sudo gitlab-rake gitlab:external_diffs:force_object_storage
+ ```
-By default, `sudo` does not preserve existing environment variables. You should append them, rather than prefix them.
+ For self-compiled installations:
-```shell
-sudo gitlab-rake gitlab:external_diffs:force_object_storage START_ID=59946109 END_ID=59946109 UPDATE_DELAY=5
-```
+ ```shell
+ sudo -u git -H bundle exec rake gitlab:external_diffs:force_object_storage RAILS_ENV=production
+ ```
-## Switching from external storage to object storage
+ By default, `sudo` does not preserve existing environment variables. You should
+ append them, rather than prefix them, like this:
-Automatic migration moves diffs stored in the database, but it does not move diffs between storage types.
-To switch from external storage to object storage:
+ ```shell
+ sudo gitlab-rake gitlab:external_diffs:force_object_storage START_ID=59946109 END_ID=59946109 UPDATE_DELAY=5
+ ```
-1. Move files stored on local or NFS storage to object storage manually.
-1. Run the Rake task in the [previous section](#correcting-incorrectly-migrated-diffs) to change their location in the database.
+These environment variables modify the behavior of the Rake task:
+
+| Name | Default value | Purpose |
+|----------------|---------------|---------|
+| `ANSI` | `true` | Use ANSI escape codes to make output more understandable. |
+| `BATCH_SIZE` | `1000` | Iterate through the table in batches of this size. |
+| `START_ID` | `nil` | If set, begin scanning at this ID. |
+| `END_ID` | `nil` | If set, stop scanning at this ID. |
+| `UPDATE_DELAY` | `1` | Number of seconds to sleep between updates. |
+
+- `START_ID` and `END_ID` can be used to run the update in parallel,
+ by assigning different processes to different parts of the table.
+- `BATCH` and `UPDATE_DELAY` enable the speed of the migration to be traded off
+ against concurrent access to the table.
+- `ANSI` should be set to `false` if your terminal does not support ANSI escape codes.
diff --git a/doc/administration/moderate_users.md b/doc/administration/moderate_users.md
index b30294c5fe0..c12eb2b9a95 100644
--- a/doc/administration/moderate_users.md
+++ b/doc/administration/moderate_users.md
@@ -287,6 +287,45 @@ You can also delete a user and their contributions, such as merge requests, issu
NOTE:
Before 15.1, additionally groups of which deleted user were the only owner among direct members were deleted.
+## Trust and untrust users
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132402) in GitLab 16.5.
+
+You can trust and untrust users from the Admin Area.
+
+By default, a user is not trusted and is blocked from creating issues, notes, and snippets considered to be spam. When you trust a user, they can create issues, notes, and snippets without being blocked.
+
+Prerequisite:
+
+- You must be an administrator.
+
+::Tabs
+
+:::TabTitle Trust a user
+
+1. On the left sidebar, select **Search or go to**.
+1. Select **Admin Area**.
+1. Select **Overview > Users**.
+1. Select a user.
+1. From the **User administration** dropdown list, select **Trust user**.
+1. On the confirmation dialog, select **Trust user**.
+
+The user is trusted.
+
+:::TabTitle Untrust a user
+
+1. On the left sidebar, select **Search or go to**.
+1. Select **Admin Area**.
+1. Select **Overview > Users**.
+1. Select the **Trusted** tab.
+1. Select a user.
+1. From the **User administration** dropdown list, select **Untrust user**.
+1. On the confirmation dialog, select **Untrust user**.
+
+The user is untrusted.
+
+::EndTabs
+
## Troubleshooting
When moderating users, you may need to perform bulk actions on them based on certain conditions. The following rails console scripts show some examples of this. You may [start a rails console session](../administration/operations/rails_console.md#starting-a-rails-console-session) and use scripts similar to the following:
diff --git a/doc/administration/monitoring/performance/performance_bar.md b/doc/administration/monitoring/performance/performance_bar.md
index 12fa79b3c13..95717f0c54f 100644
--- a/doc/administration/monitoring/performance/performance_bar.md
+++ b/doc/administration/monitoring/performance/performance_bar.md
@@ -17,6 +17,8 @@ For example:
## Available information
+> Rugged calls [removed](https://gitlab.com/gitlab-org/gitlab/-/issues/421591) in GitLab 16.6.
+
From left to right, the performance bar displays:
- **Current Host**: the current host serving the page.
@@ -37,8 +39,6 @@ From left to right, the performance bar displays:
- **Gitaly calls**: the time taken (in milliseconds) and the total number of
[Gitaly](../../gitaly/index.md) calls. Select to display a modal window with more
details.
-- **Rugged calls**: the time taken (in milliseconds) and the total number of
- Rugged calls. Select to display a modal window with more details.
- **Redis calls**: the time taken (in milliseconds) and the total number of
Redis calls. Select to display a modal window with more details.
- **Elasticsearch calls**: the time taken (in milliseconds) and the total number of
diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md
index 9efe39b8d3a..2eb482cae69 100644
--- a/doc/administration/monitoring/prometheus/gitlab_metrics.md
+++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md
@@ -48,8 +48,6 @@ The following metrics are available:
| `gitlab_ci_runner_authentication_failure_total` | Counter | 15.2 | Total number of times that runner authentication has failed
| `gitlab_ghost_user_migration_lag_seconds` | Gauge | 15.6 | The waiting time in seconds of the oldest scheduled record for ghost user migration | |
| `gitlab_ghost_user_migration_scheduled_records_total` | Gauge | 15.6 | The total number of scheduled ghost user migrations | |
-| `job_waiter_started_total` | Counter | 12.9 | Number of batches of jobs started where a web request is waiting for the jobs to complete | `worker` |
-| `job_waiter_timeouts_total` | Counter | 12.9 | Number of batches of jobs that timed out where a web request is waiting for the jobs to complete | `worker` |
| `gitlab_ci_active_jobs` | Histogram | 14.2 | Count of active jobs when pipeline is created | |
| `gitlab_database_transaction_seconds` | Histogram | 12.1 | Time spent in database transactions, in seconds | |
| `gitlab_method_call_duration_seconds` | Histogram | 10.2 | Method calls real duration | `controller`, `action`, `module`, `method` |
@@ -245,7 +243,6 @@ configuration option in `gitlab.yml`. These metrics are served from the
| `geo_cursor_last_event_timestamp` | Gauge | 10.2 | Last UNIX timestamp of the event log processed by the secondary | `url` |
| `geo_status_failed_total` | Counter | 10.2 | Number of times retrieving the status from the Geo Node failed | `url` |
| `geo_last_successful_status_check_timestamp` | Gauge | 10.2 | Last timestamp when the status was successfully updated | `url` |
-| `geo_job_artifacts_synced_missing_on_primary` | Gauge | 10.7 | Number of job artifacts marked as synced due to the file missing on the primary | `url` |
| `geo_package_files` | Gauge | 13.0 | Number of package files on primary | `url` |
| `geo_package_files_checksummed` | Gauge | 13.0 | Number of package files checksummed on primary | `url` |
| `geo_package_files_checksum_failed` | Gauge | 13.0 | Number of package files failed to calculate the checksum on primary | `url` |
@@ -386,7 +383,12 @@ configuration option in `gitlab.yml`. These metrics are served from the
| `geo_project_repositories_verification_total` | Gauge | 16.2 | Number of Project Repositories to attempt to verify on secondary | `url` |
| `geo_project_repositories_verified` | Gauge | 16.2 | Number of Project Repositories successfully verified on secondary | `url` |
| `geo_project_repositories_verification_failed` | Gauge | 16.2 | Number of Project Repositories that failed verification on secondary | `url` |
-
+| `geo_repositories_synced` | Gauge | 10.2 | Deprecated for removal in 17.0. Missing in 16.3 and 16.4. Replaced by `geo_project_repositories_synced`. Number of repositories synced on secondary | `url` |
+| `geo_repositories_failed` | Gauge | 10.2 | Deprecated for removal in 17.0. Missing in 16.3 and 16.4. Replaced by `geo_project_repositories_failed`. Number of repositories failed to sync on secondary | `url` |
+| `geo_repositories_checksummed` | Gauge | 10.7 | Deprecated for removal in 17.0. Missing in 16.3 and 16.4. Replaced by `geo_project_repositories_checksummed`. Number of repositories checksummed on primary | `url` |
+| `geo_repositories_checksum_failed` | Gauge | 10.7 | Deprecated for removal in 17.0. Missing in 16.3 and 16.4. Replaced by `geo_project_repositories_checksum_failed`. Number of repositories failed to calculate the checksum on primary | `url` |
+| `geo_repositories_verified` | Gauge | 10.7 | Deprecated for removal in 17.0. Missing in 16.3 and 16.4. Replaced by `geo_project_repositories_verified`. Number of repositories successfully verified on secondary | `url` |
+| `geo_repositories_verification_failed` | Gauge | 10.7 | Deprecated for removal in 17.0. Missing in 16.3 and 16.4. Replaced by `geo_project_repositories_verification_failed`. Number of repositories that failed verification on secondary | `url` |
| `gitlab_memwd_violations_total` | Counter | 15.9 | Total number of times a Sidekiq process violated a memory threshold | |
| `gitlab_memwd_violations_handled_total` | Counter | 15.9 | Total number of times Sidekiq process memory violations were handled | |
| `sidekiq_watchdog_running_jobs_total` | Counter | 15.9 | Current running jobs when RSS limit was reached | `worker_class` |
diff --git a/doc/administration/monitoring/prometheus/index.md b/doc/administration/monitoring/prometheus/index.md
index df6dd87c896..01b1851ab7f 100644
--- a/doc/administration/monitoring/prometheus/index.md
+++ b/doc/administration/monitoring/prometheus/index.md
@@ -302,6 +302,10 @@ update the firewall on the instance to only allow traffic from your Prometheus I
static_configs:
- targets:
- 1.1.1.1:9236
+ - job_name: registry
+ static_configs:
+ - targets:
+ - 1.1.1.1:5001
```
WARNING:
diff --git a/doc/administration/monitoring/prometheus/web_exporter.md b/doc/administration/monitoring/prometheus/web_exporter.md
index a2dee80f6d4..fbf4a109813 100644
--- a/doc/administration/monitoring/prometheus/web_exporter.md
+++ b/doc/administration/monitoring/prometheus/web_exporter.md
@@ -71,3 +71,11 @@ To serve metrics via HTTPS instead of HTTP, enable TLS in the exporter settings:
When TLS is enabled, the same `port` and `address` is used as described above.
The metrics server cannot serve both HTTP and HTTPS at the same time.
+
+## Troubleshooting
+
+### Docker container runs out of space
+
+When running [GitLab in Docker](../../../install/docker.md), your container might run out of space. This can happen if you enable certain features which increase your space consumption, for example Web Exporter.
+
+To work around this issue, [update your `shm-size`](../../../install/docker.md#devshm-mount-not-having-enough-space-in-docker-container).
diff --git a/doc/administration/operations/puma.md b/doc/administration/operations/puma.md
index f16f1ac46ae..89f1574697f 100644
--- a/doc/administration/operations/puma.md
+++ b/doc/administration/operations/puma.md
@@ -140,37 +140,6 @@ When running Puma in single mode, some features are not supported:
For more information, see [epic 5303](https://gitlab.com/groups/gitlab-org/-/epics/5303).
-## Performance caveat when using Puma with Rugged
-
-For deployments where NFS is used to store Git repositories, GitLab uses
-[direct Git access](../gitaly/index.md#direct-access-to-git-in-gitlab) to improve performance by using
-[Rugged](https://github.com/libgit2/rugged).
-
-Rugged usage is automatically enabled if direct Git access [is available](../gitaly/index.md#automatic-detection) and
-Puma is running single threaded, unless it is disabled by a [feature flag](../../development/gitaly.md#legacy-rugged-code).
-
-MRI Ruby uses a Global VM Lock (GVL). GVL allows MRI Ruby to be multi-threaded, but running at
-most on a single core.
-
-Git includes intensive I/O operations. When Rugged uses a thread for a long period of time,
-other threads that might be processing requests can starve. Puma running in single thread mode
-does not have this issue, because concurrently at most one request is being processed.
-
-GitLab is working to remove Rugged usage. Even though performance without Rugged
-is acceptable today, in some cases it might be still beneficial to run with it.
-
-Given the caveat of running Rugged with multi-threaded Puma, and acceptable
-performance of Gitaly, we disable Rugged usage if Puma multi-threaded is
-used (when Puma is configured to run with more than one thread).
-
-This default behavior may not be the optimal configuration in some situations. If Rugged
-plays an important role in your deployment, we suggest you benchmark to find the
-optimal configuration:
-
-- The safest option is to start with single-threaded Puma.
-- To force Rugged to be used with multi-threaded Puma, you can use a
- [feature flag](../../development/gitaly.md#legacy-rugged-code).
-
## Configuring Puma to listen over SSL
Puma, when deployed with a Linux package installation, listens over a Unix socket by
diff --git a/doc/administration/package_information/supported_os.md b/doc/administration/package_information/supported_os.md
index 2064ee2a8e2..ab579ca93c6 100644
--- a/doc/administration/package_information/supported_os.md
+++ b/doc/administration/package_information/supported_os.md
@@ -24,7 +24,7 @@ architecture.
| ------------------------------------------------------------ | ------------------------------ | --------------- | :----------------------------------------------------------: | ---------- | ------------------------------------------------------------ |
| AlmaLinux 8 | GitLab CE / GitLab EE 14.5.0 | x86_64, aarch64 | [AlmaLinux Install Documentation](https://about.gitlab.com/install/#almalinux) | 2029 | <https://almalinux.org/> |
| AlmaLinux 9 | GitLab CE / GitLab EE 16.0.0 | x86_64, aarch64 | [AlmaLinux Install Documentation](https://about.gitlab.com/install/#almalinux) | 2032 | <https://almalinux.org/> |
-| CentOS 7 | GitLab CE / GitLab EE 7.10.0 | x86_64 | [CentOS Install Documentation](https://about.gitlab.com/install/#centos-7) | June 2024 | <https://wiki.centos.org/About/Product> |
+| CentOS 7 | GitLab CE / GitLab EE 7.10.0 | x86_64 | [CentOS Install Documentation](https://about.gitlab.com/install/#centos-7) | June 2024 | <https://www.centos.org/about/> |
| Debian 10 | GitLab CE / GitLab EE 12.2.0 | amd64, arm64 | [Debian Install Documentation](https://about.gitlab.com/install/#debian) | 2024 | <https://wiki.debian.org/LTS> |
| Debian 11 | GitLab CE / GitLab EE 14.6.0 | amd64, arm64 | [Debian Install Documentation](https://about.gitlab.com/install/#debian) | 2026 | <https://wiki.debian.org/LTS> |
| Debian 12 | GitLab CE / GitLab EE 16.1.0 | amd64, arm64 | [Debian Install Documentation](https://about.gitlab.com/install/#debian) | TBD | <https://wiki.debian.org/LTS> |
diff --git a/doc/administration/packages/container_registry.md b/doc/administration/packages/container_registry.md
index dcc6b768eed..74dd71c19bf 100644
--- a/doc/administration/packages/container_registry.md
+++ b/doc/administration/packages/container_registry.md
@@ -9,7 +9,11 @@ info: To determine the technical writer assigned to the Stage/Group associated w
With the GitLab Container Registry, every project can have its
own space to store Docker images.
-Read more about the Docker Registry in [the Docker documentation](https://docs.docker.com/registry/introduction/).
+For more details about the Distribution Registry:
+
+- [Configuration](https://distribution.github.io/distribution/about/configuration/)
+- [Storage drivers](https://distribution.github.io/distribution/storage-drivers/)
+- [Deploy a registry server](https://distribution.github.io/distribution/about/deploying/)
This document is the administrator's guide. To learn how to use the GitLab Container
Registry, see the [user documentation](../../user/packages/container_registry/index.md).
@@ -33,14 +37,12 @@ Otherwise, the Container Registry is not enabled. To enable it:
The Container Registry works under HTTPS by default. You can use HTTP
but it's not recommended and is beyond the scope of this document.
-Read the [insecure Registry documentation](https://docs.docker.com/registry/insecure/)
-if you want to implement this.
### Self-compiled installations
If you self-compiled your GitLab installation:
-1. You must [deploy a registry](https://docs.docker.com/registry/deploying/) using the image corresponding to the
+1. You must deploy a registry using the image corresponding to the
version of GitLab you are installing
(for example: `registry.gitlab.com/gitlab-org/build/cng/gitlab-container-registry:v3.15.0-gitlab`)
1. After the installation is complete, to enable it, you must configure the Registry's
@@ -70,15 +72,15 @@ Where:
| `host` | The host URL under which the Registry runs and users can use. |
| `port` | The port the external Registry domain listens on. |
| `api_url` | The internal API URL under which the Registry is exposed. It defaults to `http://localhost:5000`. Do not change this unless you are setting up an [external Docker registry](#use-an-external-container-registry-with-gitlab-as-an-auth-endpoint). |
-| `key` | The private key location that is a pair of Registry's `rootcertbundle`. Read the [token auth configuration documentation](https://docs.docker.com/registry/configuration/#token). |
-| `path` | This should be the same directory like specified in Registry's `rootdirectory`. Read the [storage configuration documentation](https://docs.docker.com/registry/configuration/#storage). This path needs to be readable by the GitLab user, the web-server user and the Registry user. Read more in [#configure-storage-for-the-container-registry](#configure-storage-for-the-container-registry). |
-| `issuer` | This should be the same value as configured in Registry's `issuer`. Read the [token auth configuration documentation](https://docs.docker.com/registry/configuration/#token). |
+| `key` | The private key location that is a pair of Registry's `rootcertbundle`. |
+| `path` | This should be the same directory like specified in Registry's `rootdirectory`. This path needs to be readable by the GitLab user, the web-server user and the Registry user. |
+| `issuer` | This should be the same value as configured in Registry's `issuer`. |
A Registry init file is not shipped with GitLab if you install it from source.
Hence, [restarting GitLab](../restart_gitlab.md#self-compiled-installations) does not restart the Registry should
you modify its settings. Read the upstream documentation on how to achieve that.
-At the **absolute** minimum, make sure your [Registry configuration](https://docs.docker.com/registry/configuration/#auth)
+At the **absolute** minimum, make sure your Registry configuration
has `container_registry` as the service and `https://gitlab.example.com/jwt/auth`
as the realm:
@@ -383,9 +385,6 @@ The different supported drivers are:
Although most S3 compatible services (like [MinIO](https://min.io/)) should work with the Container Registry, we only guarantee support for AWS S3. Because we cannot assert the correctness of third-party S3 implementations, we can debug issues, but we cannot patch the registry unless an issue is reproducible against an AWS S3 bucket.
-Read more about the individual driver's configuration options in the
-[Docker Registry docs](https://docs.docker.com/registry/configuration/#storage).
-
### Use file system
If you want to store your images on the file system, you can change the storage
@@ -532,14 +531,14 @@ To configure the `gcs` storage driver for a Linux package installation:
}
```
- GitLab supports all [available parameters](https://docs.docker.com/registry/storage-drivers/gcs/).
+ GitLab supports all available parameters.
1. Save the file and [reconfigure GitLab](../restart_gitlab.md#reconfigure-a-linux-package-installation) for the changes to take effect.
#### Self-compiled installations
Configuring the storage driver is done in the registry configuration YAML file created
-when you [deployed your Docker registry](https://docs.docker.com/registry/deploying/).
+when you deployed your Docker registry.
`s3` storage driver example:
@@ -638,11 +637,11 @@ you can pull from the Container Registry, but you cannot push.
<!--- start_remove The following content will be removed on remove_date: '2023-10-22' -->
WARNING:
-The default configuration for the storage driver is scheduled to be [changed](https://gitlab.com/gitlab-org/container-registry/-/issues/854) in GitLab 16.0. The storage driver will use `/` as the default root directory. You can add `trimlegacyrootprefix: false` to your current configuration now to avoid any disruptions. For more information, see the [Container Registry configuration](https://gitlab.com/gitlab-org/container-registry/-/tree/master/docs-gitlab#azure-storage-driver) documentation.
+The default configuration for the storage driver is scheduled to be [changed](https://gitlab.com/gitlab-org/container-registry/-/issues/854) in GitLab 16.0. The storage driver will use `/` as the default root directory. You can add `trimlegacyrootprefix: false` to your current configuration now to avoid any disruptions. For more information, see the [Container Registry configuration](https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs/upstream-differences.md#azure-storage-driver) documentation.
<!--- end_remove -->
When moving from an existing file system or another object storage provider to Azure Object Storage, you must configure the registry to use the standard root directory.
-Configure it by setting [`trimlegacyrootprefix: true`](https://gitlab.com/gitlab-org/container-registry/-/blob/a3f64464c3ec1c5a599c0a2daa99ebcbc0100b9a/docs-gitlab/README.md#azure-storage-driver) in the Azure storage driver section of the registry configuration.
+Configure it by setting [`trimlegacyrootprefix: true`](https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs/upstream-differences.md#azure-storage-driver) in the Azure storage driver section of the registry configuration.
Without this configuration, the Azure storage driver uses `//` instead of `/` as the first section of the root path, rendering the migrated images inaccessible.
::Tabs
@@ -675,7 +674,7 @@ storage:
::EndTabs
-By default, Azure Storage Driver uses the `core.windows.net` realm. You can set another value for `realm` in the `azure` section (for example, `core.usgovcloudapi.net` for Azure Government Cloud). For more information, see the [Docker documentation](https://docs.docker.com/registry/storage-drivers/azure/).
+By default, Azure Storage Driver uses the `core.windows.net` realm. You can set another value for `realm` in the `azure` section (for example, `core.usgovcloudapi.net` for Azure Government Cloud).
### Disable redirect for storage driver
@@ -876,8 +875,7 @@ You can use GitLab as an auth endpoint with an external container registry.
- `gitlab_rails['registry_api_url'] = "http://<external_registry_host>:5000"`
must be changed to match the host where Registry is installed.
It must also specify `https` if the external registry is
- configured to use TLS. Read more on the
- [Docker registry documentation](https://docs.docker.com/registry/deploying/).
+ configured to use TLS.
1. A certificate-key pair is required for GitLab and the external container
registry to communicate securely. You need to create a certificate-key
@@ -972,7 +970,7 @@ To configure a notification endpoint for a Linux package installation:
:::TabTitle Self-compiled (source)
Configuring the notification endpoint is done in your registry configuration YAML file created
-when you [deployed your Docker registry](https://docs.docker.com/registry/deploying/).
+when you deployed your Docker registry.
Example:
@@ -1028,7 +1026,7 @@ projects.each do |p|
end
if project_total_size > 0
- projects_and_size << [p.project_id, p.creator.id, project_total_size, p.full_path]
+ projects_and_size << [p.project_id, p.creator&.id, project_total_size, p.full_path]
end
end
@@ -1374,7 +1372,7 @@ By default, the container registry uses object storage to persist metadata
related to container images. This method to store metadata limits how efficiently
the data can be accessed, especially data spanning multiple images, such as when listing tags.
By using a database to store this data, many new features are possible, including
-[online garbage collection](https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs-gitlab/db/online-garbage-collection.md)
+[online garbage collection](https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs/spec/gitlab/online-garbage-collection.md)
which removes old data automatically with zero downtime.
This database works in conjunction with the object storage already used by the registry, but does not replace object storage.
@@ -1580,7 +1578,7 @@ You can add a configuration option for backwards compatibility.
:::TabTitle Self-compiled (source)
-1. Edit the YAML configuration file you created when you [deployed the registry](https://docs.docker.com/registry/deploying/). Add the following snippet:
+1. Edit the YAML configuration file you created when you deployed the registry. Add the following snippet:
```yaml
compatibility:
@@ -1632,7 +1630,7 @@ and a simple solution would be to enable relative URLs in the Registry.
:::TabTitle Self-compiled (source)
-1. Edit the YAML configuration file you created when you [deployed the registry](https://docs.docker.com/registry/deploying/). Add the following snippet:
+1. Edit the YAML configuration file you created when you deployed the registry. Add the following snippet:
```yaml
http:
diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md
index f64c53e28a2..97acbf717fe 100644
--- a/doc/administration/pages/index.md
+++ b/doc/administration/pages/index.md
@@ -200,13 +200,13 @@ then run `gitlab-ctl reconfigure`. For more information, read
**Requirements:**
- [Wildcard DNS setup](#dns-configuration)
-- [TLS-terminating load balancer](../../install/aws/manual_install_aws.md#load-balancer)
+- [TLS-terminating load balancer](../../install/aws/index.md#load-balancer)
---
URL scheme: `https://<namespace>.example.io/<project_slug>`
-This setup is primarily intended to be used when [installing a GitLab POC on Amazon Web Services](../../install/aws/manual_install_aws.md). This includes a TLS-terminating [classic load balancer](../../install/aws/manual_install_aws.md#load-balancer) that listens for HTTPS connections, manages TLS certificates, and forwards HTTP traffic to the instance.
+This setup is primarily intended to be used when [installing a GitLab POC on Amazon Web Services](../../install/aws/index.md). This includes a TLS-terminating [classic load balancer](../../install/aws/index.md#load-balancer) that listens for HTTPS connections, manages TLS certificates, and forwards HTTP traffic to the instance.
1. In `/etc/gitlab/gitlab.rb` specify the following configuration:
diff --git a/doc/administration/postgresql/external.md b/doc/administration/postgresql/external.md
index a9f857d8f00..b9bfda80b83 100644
--- a/doc/administration/postgresql/external.md
+++ b/doc/administration/postgresql/external.md
@@ -63,7 +63,6 @@ pg_dump: error: Error message from server: SSL SYSCALL error: EOF detected
To resolve this error, ensure that you are meeting the
[minimum PostgreSQL requirements](../../install/requirements.md#postgresql-requirements). After
-upgrading your RDS instance to a suitable version, you should be able to perform a backup without
-this error. Refer to issue #64763
-([Segmentation fault citing `LooseForeignKeys::CleanupWorker` causes complete database restart](https://gitlab.com/gitlab-org/gitlab/-/issues/364763))
-for more information.
+upgrading your RDS instance to a [supported version](../../install/requirements.md#database),
+you should be able to perform a backup without this error.
+See [issue 64763](https://gitlab.com/gitlab-org/gitlab/-/issues/364763) for more information.
diff --git a/doc/administration/postgresql/external_metrics.md b/doc/administration/postgresql/external_metrics.md
new file mode 100644
index 00000000000..fc4c5652a18
--- /dev/null
+++ b/doc/administration/postgresql/external_metrics.md
@@ -0,0 +1,33 @@
+---
+stage: Data Stores
+group: Database
+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
+---
+
+# Monitoring and logging setup for external databases
+
+External PostgreSQL database systems have different logging options for monitoring performance and troubleshooting, however they are not enabled by default. In this section we provide the recommendations for self-managed PostgreSQL, and recommendations for some major providers of PostgreSQL managed services.
+
+## Recommended PostgreSQL Logging settings
+
+You should enable the following logging settings:
+
+- `log_statement=ddl`: log changes of database model definition (DDL), such as `CREATE`, `ALTER` or `DROP` of objects. This helps track recent model changes that could be causing performance issues and identify security breaches and human errors.
+- `log_lock_waits=on`: log of processes holding [locks](https://www.postgresql.org/docs/current/explicit-locking.html) for long periods, a common cause of poor query performance.
+- `log_temp_files=0`: log usage of intense and unusual temporary files that can indicate poor query performance.
+- `log_autovacuum_min_duration=0`: log all autovacuum executions. Autovacuum is a key component for overall PostgreSQL engine performance. Essential for troubleshooting and tuning if dead tuples are not being removed from tables.
+- `log_min_duration_statement=1000`: log slow queries (slower than 1 second).
+
+The full description of the above parameter settings can be found in
+[PostgreSQL error reporting and logging documentation](https://www.postgresql.org/docs/current/runtime-config-logging.html#RUNTIME-CONFIG-LOGGING-WHAT).
+
+## Amazon RDS
+
+The Amazon Relational Database Service (RDS) provides a large number of [monitoring metrics](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Monitoring.html) and [logging interfaces](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Monitor_Logs_Events.html). Here are a few you should configure:
+
+- Change all above [recommended PostgreSQL Logging settings](#recommended-postgresql-logging-settings) through [RDS Parameter Groups](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_WorkingWithDBInstanceParamGroups.html).
+ - As the recommended logging parameters are [dynamic in RDS](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Appendix.PostgreSQL.CommonDBATasks.Parameters.html) you don't require a reboot after changing these settings.
+ - The PostgreSQL logs can be observed through the [RDS console](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/logs-events-streams-console.html).
+- Enable [RDS performance insight](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_PerfInsights.html) allows you to visualise your database load with many important performance metrics of a PostgreSQL database engine.
+- Enable [RDS Enhanced Monitoring](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_Monitoring.OS.html) to monitor the operating system metrics. These metrics can indicate bottlenecks in your underlying hardware and OS that are impacting your database performance.
+ - In production environments set the monitoring interval to 10 seconds (or less) to capture micro bursts of resource usage that can be the cause of many performance issues. Set `Granularity=10` in the console or `monitoring-interval=10` in the CLI.
diff --git a/doc/administration/postgresql/external_upgrade.md b/doc/administration/postgresql/external_upgrade.md
new file mode 100644
index 00000000000..3e2c3b09853
--- /dev/null
+++ b/doc/administration/postgresql/external_upgrade.md
@@ -0,0 +1,48 @@
+---
+stage: Data Stores
+group: Database
+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
+---
+
+# Upgrading external PostgreSQL databases
+
+When upgrading your PostgreSQL database engine, it is important to follow all steps
+recommended by the PostgreSQL community and your cloud provider. Two
+kinds of upgrades exist for PostgreSQL databases:
+
+- **Minor version upgrades**: These include only bug and security fixes. They are
+ always backward-compatible with your existing application database model.
+
+ The minor version upgrade process consists of replacing the PostgreSQL binaries
+ and restarting the database service. The data directory remains unchanged.
+
+- **Major version upgrades**: These change the internal storage format and the database
+ catalog. As a result, object statistics used by the query optimizer
+ [are not transferred to the new version](https://www.postgresql.org/docs/current/pgupgrade.html)
+ and must be rebuilt with `ANALYZE`.
+
+ Not following the documented major version upgrade process often results in
+ poor database performance and high CPU use on the database server.
+
+All major cloud providers support in-place major version upgrades of database
+instances, using the `pg_upgrade` utility. However you must follow the pre- and
+post- upgrade steps to reduce the risk of performance degradation or database disruption.
+
+Read carefully the major version upgrade steps of your external database platform:
+
+- [Amazon RDS for PostgreSQL](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_UpgradeDBInstance.PostgreSQL.html#USER_UpgradeDBInstance.PostgreSQL.MajorVersion.Process)
+- [Azure Database for PostgreSQL Flexible Server](https://learn.microsoft.com/en-us/azure/postgresql/flexible-server/concepts-major-version-upgrade)
+- [Google Cloud SQL for PostgreSQL](https://cloud.google.com/sql/docs/postgres/upgrade-major-db-version-inplace)
+- [PostgreSQL community `pg_upgrade`](https://www.postgresql.org/docs/current/pgupgrade.html)
+
+## Always `ANALYZE` your database after a major version upgrade
+
+It is mandatory to run the [`ANALYZE` operation](https://www.postgresql.org/docs/current/sql-analyze.html)
+to refresh the `pg_statistic` table after a major version upgrade, because optimizer statistics
+[are not transferred by `pg_upgrade`](https://www.postgresql.org/docs/current/pgupgrade.html).
+This should be done for all databases on the upgraded PostgreSQL service/instance/cluster.
+
+To speed up the `ANALYZE` operation, use the
+[`vacuumdb` utility](https://www.postgresql.org/docs/current/app-vacuumdb.html),
+with `--analyze-only --jobs=njobs` to execute the `ANALYZE` command in parallel by
+running `njobs` commands simultaneously.
diff --git a/doc/administration/postgresql/index.md b/doc/administration/postgresql/index.md
index af0a86c3d72..4d73ba49846 100644
--- a/doc/administration/postgresql/index.md
+++ b/doc/administration/postgresql/index.md
@@ -30,6 +30,10 @@ your own external PostgreSQL server.
Read how to [set up an external PostgreSQL instance](external.md).
+When setting up an external database there are some metrics that are useful for monitoring and troubleshooting.
+When setting up an external database there are monitoring and logging settings required for troubleshooting various database related issues.
+Read more about [monitoring and logging setup for external Databases](external_metrics.md).
+
### PostgreSQL replication and failover for Linux package installations **(PREMIUM SELF)**
This setup is for when you have installed GitLab using the
@@ -47,3 +51,4 @@ Read how to [set up PostgreSQL replication and failover](replication_and_failove
- [Moving GitLab databases to a different PostgreSQL instance](moving.md)
- [Multiple databases](multiple_databases.md)
- [Database guides for GitLab development](../../development/database/index.md)
+- [Upgrade external database](external_upgrade.md)
diff --git a/doc/administration/raketasks/geo.md b/doc/administration/raketasks/geo.md
index c6bc891f529..a4b14b132db 100644
--- a/doc/administration/raketasks/geo.md
+++ b/doc/administration/raketasks/geo.md
@@ -2,82 +2,14 @@
stage: Systems
group: Geo
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
+remove_date: '2024-02-06'
+redirect_to: '../../update/deprecations.md#geo-housekeeping-rake-tasks'
---
-# Geo Rake tasks **(PREMIUM SELF)**
+# Geo Rake tasks (removed) **(PREMIUM SELF)**
-The following Rake tasks are for [Geo installations](../geo/index.md).
-See also [troubleshooting Geo](../geo/replication/troubleshooting.md) for additional Geo Rake tasks.
-
-## Git housekeeping
-
-There are few tasks you can run to schedule a Git housekeeping to start at the
-next repository sync in a **secondary** node:
-
-### Incremental Repack
-
-This is equivalent of running `git repack -d` on a _bare_ repository.
-
-- Linux package installations:
-
- ```shell
- sudo gitlab-rake geo:git:housekeeping:incremental_repack
- ```
-
-- Self-compiled installations:
-
- ```shell
- sudo -u git -H bundle exec rake geo:git:housekeeping:incremental_repack RAILS_ENV=production
- ```
-
-### Full Repack
-
-This is equivalent of running `git repack -d -A --pack-kept-objects` on a
-_bare_ repository which optionally, writes a reachability bitmap index
-when this is enabled in GitLab.
-
-- Linux package installations:
-
- ```shell
- sudo gitlab-rake geo:git:housekeeping:full_repack
- ```
-
-- Self-compiled installations:
-
- ```shell
- sudo -u git -H bundle exec rake geo:git:housekeeping:full_repack RAILS_ENV=production
- ```
-
-### GC
-
-This is equivalent of running `git gc` on a _bare_ repository, optionally writing
-a reachability bitmap index when this is enabled in GitLab.
-
-- Linux package installations:
-
- ```shell
- sudo gitlab-rake geo:git:housekeeping:gc
- ```
-
-- Self-compiled installations:
-
- ```shell
- sudo -u git -H bundle exec rake geo:git:housekeeping:gc RAILS_ENV=production
- ```
-
-## Remove orphaned project registries
-
-Under certain conditions your project registry can contain obsolete records, you
-can remove them using the Rake task `geo:run_orphaned_project_registry_cleaner`:
-
-- Linux package installations:
-
- ```shell
- sudo gitlab-rake geo:run_orphaned_project_registry_cleaner
- ```
-
-- Self-compiled installations:
-
- ```shell
- sudo -u git -H bundle exec rake geo:run_orphaned_project_registry_cleaner RAILS_ENV=production
- ```
+The Geo housekeeping Rake tasks were
+[deprecated](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/125927) in
+GitLab 16.3 and
+[removed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/130565) in
+GitLab 16.5.
diff --git a/doc/administration/raketasks/github_import.md b/doc/administration/raketasks/github_import.md
index 82f3ffa2193..a4d52899f21 100644
--- a/doc/administration/raketasks/github_import.md
+++ b/doc/administration/raketasks/github_import.md
@@ -4,11 +4,15 @@ group: Distribution
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
---
-# GitHub import Rake task **(FREE SELF)**
+# GitHub import Rake task (deprecated) **(FREE SELF)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/390690) in GitLab 15.9, Rake task no longer automatically creates namespaces or groups that don't exist.
> - Requirement for Maintainer role instead of Developer role introduced in GitLab 16.0 and backported to GitLab 15.11.1 and GitLab 15.10.5.
+WARNING:
+This feature was [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/428225) in GitLab 16.6 and is planned for
+removal in GitLab 17.0. Use the [GitHub import feature](../../user/project/import/github.md) instead.
+
To retrieve and import GitHub repositories, you need a [GitHub personal access token](https://github.com/settings/tokens).
A username should be passed as the second argument to the Rake task,
which becomes the owner of the project. You can resume an import
diff --git a/doc/administration/reference_architectures/10k_users.md b/doc/administration/reference_architectures/10k_users.md
index 2e208c4eca1..2203f4b3a02 100644
--- a/doc/administration/reference_architectures/10k_users.md
+++ b/doc/administration/reference_architectures/10k_users.md
@@ -6,18 +6,21 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Reference architecture: up to 10,000 users **(PREMIUM SELF)**
-This page describes GitLab reference architecture for up to 10,000 users. For a
-full list of reference architectures, see
+This page describes the GitLab reference architecture designed for the load of up to 10,000 users
+with notable headroom.
+
+For a full list of reference architectures, see
[Available reference architectures](index.md#available-reference-architectures).
-> - **Supported users (approximate):** 10,000
+NOTE:
+Before deploying this architecture it's recommended to read through the [main documentation](index.md) first,
+specifically the [Before you start](index.md#before-you-start) and [Deciding which architecture to use](index.md#deciding-which-architecture-to-use) sections.
+
+> - **Target load:** API: 200 RPS, Web: 20 RPS, Git (Pull): 20 RPS, Git (Push): 4 RPS
> - **High Availability:** Yes ([Praefect](#configure-praefect-postgresql) needs a third-party PostgreSQL solution for HA)
> - **Estimated Costs:** [See cost table](index.md#cost-to-run)
> - **Cloud Native Hybrid Alternative:** [Yes](#cloud-native-hybrid-reference-architecture-with-helm-charts-alternative)
-> - **Validation and test results:** The Quality Engineering team does [regular smoke and performance tests](index.md#validation-and-test-results) to ensure the reference architectures remain compliant
-> - **Test requests per second (RPS) rates:** API: 200 RPS, Web: 20 RPS, Git (Pull): 20 RPS, Git (Push): 4 RPS
-> - **[Latest Results](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Benchmarks/Latest/10k)**
-> - **Unsure which Reference Architecture to use?** [Go to this guide for more info](index.md#deciding-which-architecture-to-use).
+> - **Unsure which Reference Architecture to use?** [Go to this guide for more info](index.md#deciding-which-architecture-to-use)
| Service | Nodes | Configuration | GCP | AWS |
|------------------------------------------|-------|-------------------------|------------------|----------------|
@@ -144,6 +147,27 @@ monitor .[#7FFFD4,norank]u--> elb
Before starting, see the [requirements](index.md#requirements) for reference architectures.
+## Testing methodology
+
+The 10k architecture is designed to cover a large majority of workflows and is regularly
+[smoke and performance tested](index.md#validation-and-test-results) by the Quality Engineering team
+against the following endpoint throughput targets:
+
+- API: 200 RPS
+- Web: 20 RPS
+- Git (Pull): 20 RPS
+- Git (Push): 4 RPS
+
+The above targets were selected based on real customer data of total environmental loads corresponding to the user count,
+including CI and other workloads along with additional substantial headroom added.
+
+If you have metrics to suggest that you have regularly higher throughput against the above endpoint targets, [large monorepos](index.md#large-monorepos)
+or notable [additional workloads](index.md#additional-workloads) these can notably impact the performance environment and [further adjustments may be required](index.md#scaling-an-environment).
+If this applies to you, we strongly recommended referring to the linked documentation as well as reaching out to your [Customer Success Manager](https://handbook.gitlab.com/job-families/sales/customer-success-management/) or our [Support team](https://about.gitlab.com/support/) for further guidance.
+
+Testing is done regularly via our [GitLab Performance Tool (GPT)](https://gitlab.com/gitlab-org/quality/performance) and its dataset, which is available for anyone to use.
+The results of this testing are [available publicly on the GPT wiki](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Benchmarks/Latest). For more information on our testing strategy [refer to this section of the documentation](index.md#validation-and-test-results).
+
## Setup components
To set up GitLab and its components to accommodate up to 10,000 users:
@@ -1307,7 +1331,7 @@ This is how this would work with a Linux package PostgreSQL setup:
1. Create the new user `praefect`, replacing `<praefect_postgresql_password>`:
```shell
- CREATE ROLE praefect WITH LOGIN CREATEDB PASSWORD <praefect_postgresql_password>;
+ CREATE ROLE praefect WITH LOGIN CREATEDB PASSWORD '<praefect_postgresql_password>';
```
1. Reconnect to the PostgreSQL server, this time as the `praefect` user:
@@ -1763,7 +1787,8 @@ Updates to example must be made at:
-->
```ruby
- roles ["sidekiq_role"]
+ # https://docs.gitlab.com/omnibus/roles/#sidekiq-roles
+ roles(["sidekiq_role"])
# External URL
## This should match the URL of the external load balancer
diff --git a/doc/administration/reference_architectures/1k_users.md b/doc/administration/reference_architectures/1k_users.md
index 2f7c8209a44..362da0bd7c6 100644
--- a/doc/administration/reference_architectures/1k_users.md
+++ b/doc/administration/reference_architectures/1k_users.md
@@ -6,24 +6,18 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Reference architecture: up to 1,000 users **(FREE SELF)**
-This page describes GitLab reference architecture for up to 1,000 users. For a
-full list of reference architectures, see
-[Available reference architectures](index.md#available-reference-architectures).
+This page describes the GitLab reference architecture designed for the load of up to 1,000 users
+with notable headroom (non-HA standalone).
-If you are serving up to 1,000 users, and you don't have strict availability
-requirements, a [standalone](index.md#standalone-non-ha) single-node solution with
-frequent backups is appropriate for
-many organizations.
+For a full list of reference architectures, see
+[Available reference architectures](index.md#available-reference-architectures).
-> - **Supported users (approximate):** 1,000
+> - **Target Load:** API: 20 RPS, Web: 2 RPS, Git (Pull): 2 RPS, Git (Push): 1 RPS
> - **High Availability:** No. For a highly-available environment, you can
> follow a modified [3K reference architecture](3k_users.md#supported-modifications-for-lower-user-counts-ha).
> - **Estimated Costs:** [See cost table](index.md#cost-to-run)
> - **Cloud Native Hybrid:** No. For a cloud native hybrid environment, you
> can follow a [modified hybrid reference architecture](#cloud-native-hybrid-reference-architecture-with-helm-charts).
-> - **Validation and test results:** The Quality Engineering team does [regular smoke and performance tests](index.md#validation-and-test-results) to ensure the reference architectures remain compliant
-> - **Test requests per second (RPS) rates:** API: 20 RPS, Web: 2 RPS, Git (Pull): 2 RPS, Git (Push): 1 RPS
-> - **[Latest Results](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Benchmarks/Latest/1k)**
> - **Unsure which Reference Architecture to use?** [Go to this guide for more info](index.md#deciding-which-architecture-to-use).
| Users | Configuration | GCP | AWS | Azure |
@@ -73,6 +67,27 @@ WARNING:
**However, if you have [large monorepos](index.md#large-monorepos) (larger than several gigabytes) or [additional workloads](index.md#additional-workloads) these can *significantly* impact the performance of the environment and further adjustments may be required.**
If this applies to you, we strongly recommended referring to the linked documentation as well as reaching out to your [Customer Success Manager](https://handbook.gitlab.com/job-families/sales/customer-success-management/) or our [Support team](https://about.gitlab.com/support/) for further guidance.
+## Testing methodology
+
+The 1k architecture is designed to cover a large majority of workflows and is regularly
+[smoke and performance tested](index.md#validation-and-test-results) by the Quality Engineering team
+against the following endpoint throughput targets:
+
+- API: 20 RPS
+- Web: 2 RPS
+- Git (Pull): 2 RPS
+- Git (Push): 1 RPS
+
+The above targets were selected based on real customer data of total environmental loads corresponding to the user count,
+including CI and other workloads along with additional substantial headroom added.
+
+If you have metrics to suggest that you have regularly higher throughput against the above endpoint targets, [large monorepos](index.md#large-monorepos)
+or notable [additional workloads](index.md#additional-workloads) these can notably impact the performance environment and [further adjustments may be required](index.md#scaling-an-environment).
+If this applies to you, we strongly recommended referring to the linked documentation as well as reaching out to your [Customer Success Manager](https://handbook.gitlab.com/job-families/sales/customer-success-management/) or our [Support team](https://about.gitlab.com/support/) for further guidance.
+
+Testing is done regularly via our [GitLab Performance Tool (GPT)](https://gitlab.com/gitlab-org/quality/performance) and its dataset, which is available for anyone to use.
+The results of this testing are [available publicly on the GPT wiki](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Benchmarks/Latest). For more information on our testing strategy [refer to this section of the documentation](index.md#validation-and-test-results).
+
## Setup instructions
To install GitLab for this default reference architecture, use the standard
diff --git a/doc/administration/reference_architectures/25k_users.md b/doc/administration/reference_architectures/25k_users.md
index 355fe45cc2f..a5d44edf877 100644
--- a/doc/administration/reference_architectures/25k_users.md
+++ b/doc/administration/reference_architectures/25k_users.md
@@ -6,18 +6,21 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Reference architecture: up to 25,000 users **(PREMIUM SELF)**
-This page describes GitLab reference architecture for up to 25,000 users. For a
-full list of reference architectures, see
+This page describes the GitLab reference architecture designed for the load of up to 25,000 users
+with notable headroom.
+
+For a full list of reference architectures, see
[Available reference architectures](index.md#available-reference-architectures).
-> - **Supported users (approximate):** 25,000
+NOTE:
+Before deploying this architecture it's recommended to read through the [main documentation](index.md) first,
+specifically the [Before you start](index.md#before-you-start) and [Deciding which architecture to use](index.md#deciding-which-architecture-to-use) sections.
+
+> - **Target load:** API: 500 RPS, Web: 50 RPS, Git (Pull): 50 RPS, Git (Push): 10 RPS
> - **High Availability:** Yes ([Praefect](#configure-praefect-postgresql) needs a third-party PostgreSQL solution for HA)
> - **Estimated Costs:** [See cost table](index.md#cost-to-run)
> - **Cloud Native Hybrid Alternative:** [Yes](#cloud-native-hybrid-reference-architecture-with-helm-charts-alternative)
-> - **Validation and test results:** The Quality Engineering team does [regular smoke and performance tests](index.md#validation-and-test-results) to ensure the reference architectures remain compliant
-> - **Test requests per second (RPS) rates:** API: 500 RPS, Web: 50 RPS, Git (Pull): 50 RPS, Git (Push): 10 RPS
-> - **[Latest Results](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Benchmarks/Latest/25k)**
-> - **Unsure which Reference Architecture to use?** [Go to this guide for more info](index.md#deciding-which-architecture-to-use).
+> - **Unsure which Reference Architecture to use?** [Go to this guide for more info](index.md#deciding-which-architecture-to-use)
| Service | Nodes | Configuration | GCP | AWS |
|------------------------------------------|-------|-------------------------|------------------|--------------|
@@ -144,6 +147,27 @@ monitor .[#7FFFD4,norank]u--> elb
Before starting, see the [requirements](index.md#requirements) for reference architectures.
+## Testing methodology
+
+The 25k architecture is designed to cover a large majority of workflows and is regularly
+[smoke and performance tested](index.md#validation-and-test-results) by the Quality Engineering team
+against the following endpoint throughput targets:
+
+- API: 500 RPS
+- Web: 50 RPS
+- Git (Pull): 50 RPS
+- Git (Push): 10 RPS
+
+The above targets were selected based on real customer data of total environmental loads corresponding to the user count,
+including CI and other workloads along with additional substantial headroom added.
+
+If you have metrics to suggest that you have regularly higher throughput against the above endpoint targets, [large monorepos](index.md#large-monorepos)
+or notable [additional workloads](index.md#additional-workloads) these can notably impact the performance environment and [further adjustments may be required](index.md#scaling-an-environment).
+If this applies to you, we strongly recommended referring to the linked documentation as well as reaching out to your [Customer Success Manager](https://handbook.gitlab.com/job-families/sales/customer-success-management/) or our [Support team](https://about.gitlab.com/support/) for further guidance.
+
+Testing is done regularly via our [GitLab Performance Tool (GPT)](https://gitlab.com/gitlab-org/quality/performance) and its dataset, which is available for anyone to use.
+The results of this testing are [available publicly on the GPT wiki](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Benchmarks/Latest). For more information on our testing strategy [refer to this section of the documentation](index.md#validation-and-test-results).
+
## Setup components
To set up GitLab and its components to accommodate up to 25,000 users:
@@ -1324,7 +1348,7 @@ This is how this would work with a Linux package PostgreSQL setup:
1. Create the new user `praefect`, replacing `<praefect_postgresql_password>`:
```shell
- CREATE ROLE praefect WITH LOGIN CREATEDB PASSWORD <praefect_postgresql_password>;
+ CREATE ROLE praefect WITH LOGIN CREATEDB PASSWORD '<praefect_postgresql_password>';
```
1. Reconnect to the PostgreSQL server, this time as the `praefect` user:
@@ -1780,7 +1804,8 @@ Updates to example must be made at:
-->
```ruby
- roles ["sidekiq_role"]
+ # https://docs.gitlab.com/omnibus/roles/#sidekiq-roles
+ roles(["sidekiq_role"])
# External URL
## This should match the URL of the external load balancer
diff --git a/doc/administration/reference_architectures/2k_users.md b/doc/administration/reference_architectures/2k_users.md
index 5814d6c1e2d..fb8b9d8de45 100644
--- a/doc/administration/reference_architectures/2k_users.md
+++ b/doc/administration/reference_architectures/2k_users.md
@@ -6,18 +6,17 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Reference architecture: up to 2,000 users **(FREE SELF)**
-This page describes GitLab reference architecture for up to 2,000 users.
+This page describes the GitLab reference architecture designed for the load of up to 2,000 users
+with notable headroom (non-HA).
+
For a full list of reference architectures, see
[Available reference architectures](index.md#available-reference-architectures).
-> - **Supported users (approximate):** 2,000
+> - **Target Load:** API: 40 RPS, Web: 4 RPS, Git (Pull): 4 RPS, Git (Push): 1 RPS
> - **High Availability:** No. For a highly-available environment, you can
> follow a modified [3K reference architecture](3k_users.md#supported-modifications-for-lower-user-counts-ha).
> - **Estimated Costs:** [See cost table](index.md#cost-to-run)
> - **Cloud Native Hybrid:** [Yes](#cloud-native-hybrid-reference-architecture-with-helm-charts-alternative)
-> - **Validation and test results:** The Quality Engineering team does [regular smoke and performance tests](index.md#validation-and-test-results) to ensure the reference architectures remain compliant
-> - **Test requests per second (RPS) rates:** API: 40 RPS, Web: 4 RPS, Git (Pull): 4 RPS, Git (Push): 1 RPS
-> - **[Latest Results](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Benchmarks/Latest/2k)**
> - **Unsure which Reference Architecture to use?** [Go to this guide for more info](index.md#deciding-which-architecture-to-use).
| Service | Nodes | Configuration | GCP | AWS | Azure |
@@ -81,6 +80,27 @@ monitor .[#7FFFD4,norank]u--> elb
Before starting, see the [requirements](index.md#requirements) for reference architectures.
+## Testing methodology
+
+The 2k architecture is designed to cover a large majority of workflows and is regularly
+[smoke and performance tested](index.md#validation-and-test-results) by the Quality Engineering team
+against the following endpoint throughput targets:
+
+- API: 40 RPS
+- Web: 4 RPS
+- Git (Pull): 4 RPS
+- Git (Push): 1 RPS
+
+The above targets were selected based on real customer data of total environmental loads corresponding to the user count,
+including CI and other workloads along with additional substantial headroom added.
+
+If you have metrics to suggest that you have regularly higher throughput against the above endpoint targets, [large monorepos](index.md#large-monorepos)
+or notable [additional workloads](index.md#additional-workloads) these can notably impact the performance environment and [further adjustments may be required](index.md#scaling-an-environment).
+If this applies to you, we strongly recommended referring to the linked documentation as well as reaching out to your [Customer Success Manager](https://handbook.gitlab.com/job-families/sales/customer-success-management/) or our [Support team](https://about.gitlab.com/support/) for further guidance.
+
+Testing is done regularly via our [GitLab Performance Tool (GPT)](https://gitlab.com/gitlab-org/quality/performance) and its dataset, which is available for anyone to use.
+The results of this testing are [available publicly on the GPT wiki](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Benchmarks/Latest). For more information on our testing strategy [refer to this section of the documentation](index.md#validation-and-test-results).
+
## Setup components
To set up GitLab and its components to accommodate up to 2,000 users:
@@ -609,7 +629,8 @@ Updates to example must be made at:
-->
```ruby
- roles ["sidekiq_role"]
+ # https://docs.gitlab.com/omnibus/roles/#sidekiq-roles
+ roles(["sidekiq_role"])
# External URL
external_url 'https://gitlab.example.com'
diff --git a/doc/administration/reference_architectures/3k_users.md b/doc/administration/reference_architectures/3k_users.md
index 1fd8239c93f..73b0291ab95 100644
--- a/doc/administration/reference_architectures/3k_users.md
+++ b/doc/administration/reference_architectures/3k_users.md
@@ -6,27 +6,20 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Reference architecture: up to 3,000 users **(PREMIUM SELF)**
-This GitLab reference architecture can help you deploy GitLab to up to 3,000
-users, and then maintain uptime and access for those users. You can also use
-this architecture to provide improved GitLab uptime and availability for fewer
-than 3,000 users. For fewer users, reduce the stated node sizes as needed.
+This page describes the GitLab reference architecture designed for the load of up to 3,000 users
+with notable headroom.
-If maintaining a high level of uptime for your GitLab environment isn't a
-requirement, or if you don't have the expertise to maintain this sort of
-environment, we recommend using the non-HA [2,000-user reference architecture](2k_users.md)
-for your GitLab installation. If HA is still a requirement, there's several supported
-tweaks you can make to this architecture to reduce complexity as detailed here.
+This architecture is the smallest one available with HA built in. If you require HA but
+have a lower user count or total load the [Supported Modifications for lower user counts](#supported-modifications-for-lower-user-counts-ha)
+section details how to reduce this architecture's size while maintaining HA.
For a full list of reference architectures, see
[Available reference architectures](index.md#available-reference-architectures).
-> - **Supported users (approximate):** 3,000
+> - **Target Load:** 60 RPS, Web: 6 RPS, Git (Pull): 6 RPS, Git (Push): 1 RPS
> - **High Availability:** Yes, although [Praefect](#configure-praefect-postgresql) needs a third-party PostgreSQL solution
> - **Estimated Costs:** [See cost table](index.md#cost-to-run)
> - **Cloud Native Hybrid Alternative:** [Yes](#cloud-native-hybrid-reference-architecture-with-helm-charts-alternative)
-> - **Validation and test results:** The Quality Engineering team does [regular smoke and performance tests](index.md#validation-and-test-results) to ensure the reference architectures remain compliant
-> - **Test requests per second (RPS) rates:** API: 60 RPS, Web: 6 RPS, Git (Pull): 6 RPS, Git (Push): 1 RPS
-> - **[Latest Results](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Benchmarks/Latest/3k)**
> - **Unsure which Reference Architecture to use?** [Go to this guide for more info](index.md#deciding-which-architecture-to-use).
| Service | Nodes | Configuration | GCP | AWS |
@@ -149,6 +142,27 @@ monitor .[#7FFFD4,norank]u--> elb
Before starting, see the [requirements](index.md#requirements) for reference architectures.
+## Testing methodology
+
+The 3k architecture is designed to cover a large majority of workflows and is regularly
+[smoke and performance tested](index.md#validation-and-test-results) by the Quality Engineering team
+against the following endpoint throughput targets:
+
+- API: 60 RPS
+- Web: 6 RPS
+- Git (Pull): 6 RPS
+- Git (Push): 1 RPS
+
+The above targets were selected based on real customer data of total environmental loads corresponding to the user count,
+including CI and other workloads along with additional substantial headroom added.
+
+If you have metrics to suggest that you have regularly higher throughput against the above endpoint targets, [large monorepos](index.md#large-monorepos)
+or notable [additional workloads](index.md#additional-workloads) these can notably impact the performance environment and [further adjustments may be required](index.md#scaling-an-environment).
+If this applies to you, we strongly recommended referring to the linked documentation as well as reaching out to your [Customer Success Manager](https://handbook.gitlab.com/job-families/sales/customer-success-management/) or our [Support team](https://about.gitlab.com/support/) for further guidance.
+
+Testing is done regularly via our [GitLab Performance Tool (GPT)](https://gitlab.com/gitlab-org/quality/performance) and its dataset, which is available for anyone to use.
+The results of this testing are [available publicly on the GPT wiki](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Benchmarks/Latest). For more information on our testing strategy [refer to this section of the documentation](index.md#validation-and-test-results).
+
## Setup components
To set up GitLab and its components to accommodate up to 3,000 users:
@@ -1248,7 +1262,7 @@ This is how this would work with a Linux package PostgreSQL setup:
1. Create the new user `praefect`, replacing `<praefect_postgresql_password>`:
```shell
- CREATE ROLE praefect WITH LOGIN CREATEDB PASSWORD <praefect_postgresql_password>;
+ CREATE ROLE praefect WITH LOGIN CREATEDB PASSWORD '<praefect_postgresql_password>';
```
1. Reconnect to the PostgreSQL server, this time as the `praefect` user:
@@ -1708,7 +1722,8 @@ Updates to example must be made at:
-->
```ruby
- roles ["sidekiq_role"]
+ # https://docs.gitlab.com/omnibus/roles/#sidekiq-roles
+ roles(["sidekiq_role"])
# External URL
## This should match the URL of the external load balancer
diff --git a/doc/administration/reference_architectures/50k_users.md b/doc/administration/reference_architectures/50k_users.md
index 72ddd347856..ca39468a76e 100644
--- a/doc/administration/reference_architectures/50k_users.md
+++ b/doc/administration/reference_architectures/50k_users.md
@@ -6,18 +6,21 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Reference architecture: up to 50,000 users **(PREMIUM SELF)**
-This page describes GitLab reference architecture for up to 50,000 users. For a
-full list of reference architectures, see
+This page describes the GitLab reference architecture designed for the load of up to 50,000 users
+with notable headroom.
+
+For a full list of reference architectures, see
[Available reference architectures](index.md#available-reference-architectures).
-> - **Supported users (approximate):** 50,000
+NOTE:
+Before deploying this architecture it's recommended to read through the [main documentation](index.md) first,
+specifically the [Before you start](index.md#before-you-start) and [Deciding which architecture to use](index.md#deciding-which-architecture-to-use) sections.
+
+> - **Target load:** API: 1000 RPS, Web: 100 RPS, Git (Pull): 100 RPS, Git (Push): 20 RPS
> - **High Availability:** Yes ([Praefect](#configure-praefect-postgresql) needs a third-party PostgreSQL solution for HA)
> - **Estimated Costs:** [See cost table](index.md#cost-to-run)
> - **Cloud Native Hybrid Alternative:** [Yes](#cloud-native-hybrid-reference-architecture-with-helm-charts-alternative)
-> - **Validation and test results:** The Quality Engineering team does [regular smoke and performance tests](index.md#validation-and-test-results) to ensure the reference architectures remain compliant
-> - **Test requests per second (RPS) rates:** API: 1000 RPS, Web: 100 RPS, Git (Pull): 100 RPS, Git (Push): 20 RPS
-> - **[Latest Results](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Benchmarks/Latest/50k)**
-> - **Unsure which Reference Architecture to use?** [Go to this guide for more info](index.md#deciding-which-architecture-to-use).
+> - **Unsure which Reference Architecture to use?** [Go to this guide for more info](index.md#deciding-which-architecture-to-use)
| Service | Nodes | Configuration | GCP | AWS |
|------------------------------------------|-------|-------------------------|------------------|---------------|
@@ -144,6 +147,27 @@ monitor .[#7FFFD4,norank]u--> elb
Before starting, see the [requirements](index.md#requirements) for reference architectures.
+## Testing methodology
+
+The 50k architecture is designed to cover a large majority of workflows and is regularly
+[smoke and performance tested](index.md#validation-and-test-results) by the Quality Engineering team
+against the following endpoint throughput targets:
+
+- API: 1000 RPS
+- Web: 100 RPS
+- Git (Pull): 100 RPS
+- Git (Push): 20 RPS
+
+The above targets were selected based on real customer data of total environmental loads corresponding to the user count,
+including CI and other workloads along with additional substantial headroom added.
+
+If you have metrics to suggest that you have regularly higher throughput against the above endpoint targets, [large monorepos](index.md#large-monorepos)
+or notable [additional workloads](index.md#additional-workloads) these can notably impact the performance environment and [further adjustments may be required](index.md#scaling-an-environment).
+If this applies to you, we strongly recommended referring to the linked documentation as well as reaching out to your [Customer Success Manager](https://handbook.gitlab.com/job-families/sales/customer-success-management/) or our [Support team](https://about.gitlab.com/support/) for further guidance.
+
+Testing is done regularly via our [GitLab Performance Tool (GPT)](https://gitlab.com/gitlab-org/quality/performance) and its dataset, which is available for anyone to use.
+The results of this testing are [available publicly on the GPT wiki](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Benchmarks/Latest). For more information on our testing strategy [refer to this section of the documentation](index.md#validation-and-test-results).
+
## Setup components
To set up GitLab and its components to accommodate up to 50,000 users:
@@ -1320,7 +1344,7 @@ This is how this would work with a Linux package PostgreSQL setup:
1. Create the new user `praefect`, replacing `<praefect_postgresql_password>`:
```shell
- CREATE ROLE praefect WITH LOGIN CREATEDB PASSWORD <praefect_postgresql_password>;
+ CREATE ROLE praefect WITH LOGIN CREATEDB PASSWORD '<praefect_postgresql_password>';
```
1. Reconnect to the PostgreSQL server, this time as the `praefect` user:
@@ -1776,7 +1800,8 @@ Updates to example must be made at:
-->
```ruby
- roles ["sidekiq_role"]
+ # https://docs.gitlab.com/omnibus/roles/#sidekiq-roles
+ roles(["sidekiq_role"])
# External URL
## This should match the URL of the external load balancer
diff --git a/doc/administration/reference_architectures/5k_users.md b/doc/administration/reference_architectures/5k_users.md
index e2bf0aa59f4..e908565e27e 100644
--- a/doc/administration/reference_architectures/5k_users.md
+++ b/doc/administration/reference_architectures/5k_users.md
@@ -6,25 +6,21 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Reference architecture: up to 5,000 users **(PREMIUM SELF)**
-This page describes GitLab reference architecture for up to 5,000 users. For a
-full list of reference architectures, see
+This page describes the GitLab reference architecture designed for the load of up to 5,000 users
+with notable headroom.
+
+For a full list of reference architectures, see
[Available reference architectures](index.md#available-reference-architectures).
NOTE:
-This reference architecture is designed to help your organization achieve a
-highly-available GitLab deployment. If you do not have the expertise or need to
-maintain a highly-available environment, you can have a simpler and less
-costly-to-operate environment by using the
-[2,000-user reference architecture](2k_users.md).
+Before deploying this architecture it's recommended to read through the [main documentation](index.md) first,
+specifically the [Before you start](index.md#before-you-start) and [Deciding which architecture to use](index.md#deciding-which-architecture-to-use) sections.
-> - **Supported users (approximate):** 5,000
+> - **Target load:** API: 100 RPS, Web: 10 RPS, Git (Pull): 10 RPS, Git (Push): 2 RPS
> - **High Availability:** Yes ([Praefect](#configure-praefect-postgresql) needs a third-party PostgreSQL solution for HA)
> - **Estimated Costs:** [See cost table](index.md#cost-to-run)
> - **Cloud Native Hybrid Alternative:** [Yes](#cloud-native-hybrid-reference-architecture-with-helm-charts-alternative)
-> - **Validation and test results:** The Quality Engineering team does [regular smoke and performance tests](index.md#validation-and-test-results) to ensure the reference architectures remain compliant
-> - **Test requests per second (RPS) rates:** API: 100 RPS, Web: 10 RPS, Git (Pull): 10 RPS, Git (Push): 2 RPS
-> - **[Latest Results](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Benchmarks/Latest/5k)**
-> - **Unsure which Reference Architecture to use?** [Go to this guide for more info](index.md#deciding-which-architecture-to-use).
+> - **Unsure which Reference Architecture to use?** [Go to this guide for more info](index.md#deciding-which-architecture-to-use)
| Service | Nodes | Configuration | GCP | AWS |
|-------------------------------------------|-------|-------------------------|-----------------|--------------|
@@ -146,6 +142,27 @@ monitor .[#7FFFD4,norank]u--> elb
Before starting, see the [requirements](index.md#requirements) for reference architectures.
+## Testing methodology
+
+The 5k architecture is designed to cover a large majority of workflows and is regularly
+[smoke and performance tested](index.md#validation-and-test-results) by the Quality Engineering team
+against the following endpoint throughput targets:
+
+- API: 100 RPS
+- Web: 10 RPS
+- Git (Pull): 10 RPS
+- Git (Push): 2 RPS
+
+The above targets were selected based on real customer data of total environmental loads corresponding to the user count,
+including CI and other workloads along with additional substantial headroom added.
+
+If you have metrics to suggest that you have regularly higher throughput against the above endpoint targets, [large monorepos](index.md#large-monorepos)
+or notable [additional workloads](index.md#additional-workloads) these can notably impact the performance environment and [further adjustments may be required](index.md#scaling-an-environment).
+If this applies to you, we strongly recommended referring to the linked documentation as well as reaching out to your [Customer Success Manager](https://handbook.gitlab.com/job-families/sales/customer-success-management/) or our [Support team](https://about.gitlab.com/support/) for further guidance.
+
+Testing is done regularly via our [GitLab Performance Tool (GPT)](https://gitlab.com/gitlab-org/quality/performance) and its dataset, which is available for anyone to use.
+The results of this testing are [available publicly on the GPT wiki](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Benchmarks/Latest). For more information on our testing strategy [refer to this section of the documentation](index.md#validation-and-test-results).
+
## Setup components
To set up GitLab and its components to accommodate up to 5,000 users:
@@ -1242,7 +1259,7 @@ This is how this would work with a Linux package PostgreSQL setup:
1. Create the new user `praefect`, replacing `<praefect_postgresql_password>`:
```shell
- CREATE ROLE praefect WITH LOGIN CREATEDB PASSWORD <praefect_postgresql_password>;
+ CREATE ROLE praefect WITH LOGIN CREATEDB PASSWORD '<praefect_postgresql_password>';
```
1. Reconnect to the PostgreSQL server, this time as the `praefect` user:
@@ -1696,7 +1713,8 @@ Updates to example must be made at:
-->
```ruby
- roles ["sidekiq_role"]
+ # https://docs.gitlab.com/omnibus/roles/#sidekiq-roles
+ roles(["sidekiq_role"])
# External URL
## This should match the URL of the external load balancer
diff --git a/doc/administration/reference_architectures/index.md b/doc/administration/reference_architectures/index.md
index 44aa3d648ad..fcbfaf46009 100644
--- a/doc/administration/reference_architectures/index.md
+++ b/doc/administration/reference_architectures/index.md
@@ -12,36 +12,37 @@ GitLab Quality Engineering and Support teams to provide recommended deployments
## Available reference architectures
-Depending on your workflow, the following recommended reference architectures
-may need to be adapted accordingly. Your workload is influenced by factors
-including how active your users are, how much automation you use, mirroring,
-and repository/change size. Additionally, the displayed memory values are
-provided by [GCP machine types](https://cloud.google.com/compute/docs/machine-resource).
-For different cloud vendors, attempt to select options that best match the
-provided architecture.
+The following Reference Architectures are available as recommended starting points for your environment.
+
+The architectures are named in terms of user count, which in this case means the architecture is designed against
+the _total_ load that comes with such a user count based on real data along with substantial headroom added to cover most scenarios such as CI or other automated workloads.
+
+However, it should be noted that in some cases, known heavy scenarios such as [large monorepos](#large-monorepos) or notable [additional workloads](#additional-workloads) may require adjustments to be made.
+
+For each Reference Architecture, the details of what they have been tested against can be found respectively in the "Testing Methodology" section of each page.
### GitLab package (Omnibus)
-The following reference architectures, where the GitLab package is used, are available:
+Below is a list of Linux package based architectures:
-- [Up to 1,000 users](1k_users.md)
-- [Up to 2,000 users](2k_users.md)
-- [Up to 3,000 users](3k_users.md)
-- [Up to 5,000 users](5k_users.md)
-- [Up to 10,000 users](10k_users.md)
-- [Up to 25,000 users](25k_users.md)
-- [Up to 50,000 users](50k_users.md)
+- [Up to 1,000 users](1k_users.md) <span style="color: darkgrey;">_API: 20 RPS, Web: 2 RPS, Git (Pull): 2 RPS, Git (Push): 1 RPS_</span>
+- [Up to 2,000 users](2k_users.md) <span style="color: darkgrey;">_API: 40 RPS, Web: 4 RPS, Git (Pull): 4 RPS, Git (Push): 1 RPS_</span>
+- [Up to 3,000 users](3k_users.md) <span style="color: darkgrey;">_API: 60 RPS, Web: 6 RPS, Git (Pull): 6 RPS, Git (Push): 1 RPS_</span>
+- [Up to 5,000 users](5k_users.md) <span style="color: darkgrey;">_API: 100 RPS, Web: 10 RPS, Git (Pull): 10 RPS, Git (Push): 2 RPS_</span>
+- [Up to 10,000 users](10k_users.md) <span style="color: darkgrey;">_API: 200 RPS, Web: 20 RPS, Git (Pull): 20 RPS, Git (Push): 4 RPS_</span>
+- [Up to 25,000 users](25k_users.md) <span style="color: darkgrey;">_API: 500 RPS, Web: 50 RPS, Git (Pull): 50 RPS, Git (Push): 10 RPS_</span>
+- [Up to 50,000 users](50k_users.md) <span style="color: darkgrey;">_API: 1000 RPS, Web: 100 RPS, Git (Pull): 100 RPS, Git (Push): 20 RPS_</span>
### Cloud native hybrid
-The following Cloud Native Hybrid reference architectures, where select recommended components can be run in Kubernetes, are available:
+Below is a list of Cloud Native Hybrid reference architectures, where select recommended components can be run in Kubernetes:
-- [Up to 2,000 users](2k_users.md#cloud-native-hybrid-reference-architecture-with-helm-charts-alternative)
-- [Up to 3,000 users](3k_users.md#cloud-native-hybrid-reference-architecture-with-helm-charts-alternative)
-- [Up to 5,000 users](5k_users.md#cloud-native-hybrid-reference-architecture-with-helm-charts-alternative)
-- [Up to 10,000 users](10k_users.md#cloud-native-hybrid-reference-architecture-with-helm-charts-alternative)
-- [Up to 25,000 users](25k_users.md#cloud-native-hybrid-reference-architecture-with-helm-charts-alternative)
-- [Up to 50,000 users](50k_users.md#cloud-native-hybrid-reference-architecture-with-helm-charts-alternative)
+- [Up to 2,000 users](2k_users.md#cloud-native-hybrid-reference-architecture-with-helm-charts-alternative) <span style="color: darkgrey;">_API: 40 RPS, Web: 4 RPS, Git (Pull): 4 RPS, Git (Push): 1 RPS_</span>
+- [Up to 3,000 users](3k_users.md#cloud-native-hybrid-reference-architecture-with-helm-charts-alternative) <span style="color: darkgrey;">_API: 60 RPS, Web: 6 RPS, Git (Pull): 6 RPS, Git (Push): 1 RPS_</span>
+- [Up to 5,000 users](5k_users.md#cloud-native-hybrid-reference-architecture-with-helm-charts-alternative) <span style="color: darkgrey;">_API: 100 RPS, Web: 10 RPS, Git (Pull): 10 RPS, Git (Push): 2 RPS_</span>
+- [Up to 10,000 users](10k_users.md#cloud-native-hybrid-reference-architecture-with-helm-charts-alternative) <span style="color: darkgrey;">_API: 200 RPS, Web: 20 RPS, Git (Pull): 20 RPS, Git (Push): 4 RPS_</span>
+- [Up to 25,000 users](25k_users.md#cloud-native-hybrid-reference-architecture-with-helm-charts-alternative) <span style="color: darkgrey;">_API: 500 RPS, Web: 50 RPS, Git (Pull): 50 RPS, Git (Push): 10 RPS_</span>
+- [Up to 50,000 users](50k_users.md#cloud-native-hybrid-reference-architecture-with-helm-charts-alternative) <span style="color: darkgrey;">_API: 1000 RPS, Web: 100 RPS, Git (Pull): 100 RPS, Git (Push): 20 RPS_</span>
## Before you start
@@ -63,6 +64,19 @@ As a general guide, **the more performant and/or resilient you want your environ
This section explains the designs you can choose from. It begins with the least complexity, goes to the most, and ends with a decision tree.
+### Expected Load (RPS)
+
+The first thing to check is what the expected load is your environment would be expected to serve.
+
+The Reference Architectures have been designed with substantial headroom by default, but it's recommended to also check the
+load of what each architecture has been tested against under the "Testing Methodology" section found on each page,
+comparing those values with what load you are expecting against your existing GitLab environment to help select the right Reference Architecture
+size.
+
+Load is given in terms of Requests per Section (RPS) for each endpoint type (API, Web, Git). This information on your existing infrastructure
+can typically be surfaced by most reputable monitoring solutions or in some other ways such as load balancer metrics. For example, on existing GitLab environments,
+[Prometheus metrics](../monitoring/prometheus/gitlab_metrics.md) such as `gitlab_transaction_duration_seconds` can be used to see this data.
+
### Standalone (non-HA)
For environments serving 2,000 or fewer users, we generally recommend a standalone approach by deploying a non-highly available single or multi-node environment. With this approach, you can employ strategies such as [automated backups](../../administration/backup_restore/backup_gitlab.md#configuring-cron-to-make-daily-backups) for recovery to provide a good level of RPO / RTO while avoiding the complexities that come with HA.
@@ -144,10 +158,11 @@ Below you can find the above guidance in the form of a decision tree. It's recom
```mermaid
%%{init: { 'theme': 'base' } }%%
graph TD
- L1A(<b>What Reference Architecture should I use?</b>)
+ L0A(<b>What Reference Architecture should I use?</b>)
+ L1A(<b>What is your <a href=#expected-load-rps>expected load</a>?</b>)
- L2A(3,000 users or more?)
- L2B(2,000 users or less?)
+ L2A("Equivalent to <a href=3k_users.md#testing-methodology>3,000 users</a> or more?")
+ L2B("Equivalent to <a href=2k_users.md#testing-methodology>2,000 users</a> or less?")
L3A("<a href=#do-you-need-high-availability-ha>Do you need HA?</a><br>(or Zero-Downtime Upgrades)")
L3B[Do you have experience with<br/>and want additional resilience<br/>with select components in Kubernetes?]
@@ -157,6 +172,7 @@ graph TD
L4C><b>Recommendation</b><br><br>Cloud Native Hybrid architecture<br>closest to user count]
L4D>"<b>Recommendation</b><br><br>Standalone 1K or 2K<br/>architecture with Backups"]
+ L0A --> L1A
L1A --> L2A
L1A --> L2B
L2A -->|Yes| L3B
@@ -191,13 +207,22 @@ Before implementing a reference architecture, refer to the following requirement
These reference architectures were built and tested on Google Cloud Platform (GCP) using the
[Intel Xeon E5 v3 (Haswell)](https://cloud.google.com/compute/docs/cpu-platforms)
CPU platform as a lowest common denominator baseline ([Sysbench benchmark](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Reference-Architectures/GCP-CPU-Benchmarks)).
+Newer, similarly-sized CPUs are supported and may have improved performance as a result.
-Newer, similarly-sized CPUs are supported and may have improved performance as a result. For Linux package environments,
-ARM-based equivalents are also supported.
+ARM CPUs are supported for Linux package environments as well as for any [Cloud Provider services](#cloud-provider-services) where applicable.
NOTE:
Any "burstable" instance types are not recommended due to inconsistent performance.
+### Supported disk types
+
+As a general guidance, most standard disk types are expected to work for GitLab, but be aware of the following specific call outs:
+
+- [Gitaly](../gitaly/index.md#disk-requirements) requires at least 8,000 input/output operations per second (IOPS) for read operations, and 2,000 IOPS for write operations.
+- We don't recommend the use of any disk types that are "burstable" due to inconsistent performance.
+
+Outside the above standard, disk types are expected to work for GitLab and the choice of each depends on your specific requirements around areas, such as durability or costs.
+
### Supported infrastructure
As a general guidance, GitLab should run on most infrastructure such as reputable Cloud Providers (AWS, GCP, Azure) and
@@ -356,6 +381,12 @@ If you choose to use a third party external service:
Redis is primarily single threaded. For the 10,000 user and above Reference Architectures, separate out the instances as specified into Cache and Persistent data to achieve optimum performance at this scale.
+### Recommendation notes for Object Storage
+
+GitLab has been tested against [various Object Storage providers](../object_storage.md#supported-object-storage-providers) that are expected to work.
+
+As a general guidance, it's recommended to use a reputable solution that has full S3 compatibility.
+
#### Unsupported database services
Several database cloud provider services are known not to support the above or have been found to have other issues and aren't recommended:
@@ -649,22 +680,35 @@ You should upgrade a Reference Architecture in the same order as you created it.
### Scaling an environment
-Scaling a GitLab environment is designed to be as seamless as possible.
+Scaling a GitLab environment is designed to be as flexible and seamless as possible.
+
+This can be done iteratively or wholesale to the next size of architecture depending on your circumstances.
+For example, if any of your GitLab Rails, Sidekiq, Gitaly, Redis or PostgreSQL nodes are consistently oversaturated, then increase their resources accordingly while leaving the rest of the environment as is.
-In terms of the Reference Architectures, you would look to the next size and adjust accordingly.
-Most setups would only need vertical scaling, but there are some specific areas that can be adjusted depending on the setup:
+If expecting a large increase in users, you may elect to scale up the whole environment to the next
+size of architecture.
+
+If the overall design is being followed, you can scale the environment vertically as required.
+
+If robust metrics are in place that show the environment is over-provisioned, you can apply the same process for
+scaling downwards. You should take an iterative approach when scaling downwards to ensure there are no issues.
+
+#### Scaling from a non-HA to an HA architecture
+
+While in most cases vertical scaling is only required to increase an environment's resources, if you are moving to an HA environment,
+there may be some additional steps required as shown below:
- If you're scaling from a non-HA environment to an HA environment, various components are recommended to be deployed in their HA forms:
- - Redis to multi-node Redis w/ Redis Sentinel
- - Postgres to multi-node Postgres w/ Consul + PgBouncer
- - Gitaly to Gitaly Cluster w/ Praefect
+ - [Redis to multi-node Redis w/ Redis Sentinel](../redis/replication_and_failover.md#switching-from-an-existing-single-machine-installation)
+ - [Postgres to multi-node Postgres w/ Consul + PgBouncer](../postgresql/moving.md)
+ - [Gitaly to Gitaly Cluster w/ Praefect](../gitaly/index.md#migrate-to-gitaly-cluster)
- From 10k users and higher, Redis is recommended to be split into multiple HA servers as it's single threaded.
Conversely, if you have robust metrics in place that show the environment is over-provisioned, you can apply the same process for
scaling downwards. You should take an iterative approach when scaling downwards, however, to ensure there are no issues.
-### How to monitor your environment
+### Monitoring
+
+There are numerous options available to monitor your infrastructure, as well as [GitLab itself](../monitoring/index.md), and you should refer to your chosen monitoring solution's documentation for more information.
-To monitor your GitLab environment, you can use the tools
-[bundled with GitLab](../monitoring/index.md), but it's also possible to use third-party
-options if desired.
+Of note, the GitLab application is bundled with [Prometheus as well as various Prometheus compatible exporters](../monitoring/prometheus/index.md) that could be hooked into your solution.
diff --git a/doc/administration/review_spam_logs.md b/doc/administration/review_spam_logs.md
new file mode 100644
index 00000000000..e3b96cdae95
--- /dev/null
+++ b/doc/administration/review_spam_logs.md
@@ -0,0 +1,40 @@
+---
+stage: Govern
+group: Anti-Abuse
+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: reference, howto
+---
+
+# Review spam logs **(FREE SELF)**
+
+GitLab tracks user activity and flags certain behavior for potential spam.
+
+In the Admin Area, a GitLab administrator can view and resolve spam logs.
+
+## Manage spam logs
+
+> **Trust user** [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131812) in GitLab 16.5.
+
+View and resolve spam logs to moderate user activity in your instance.
+
+To view spam logs:
+
+1. On the left sidebar, select **Search or go to**.
+1. Select **Admin Area**.
+1. Select **Spam Logs**.
+1. Optional. To resolve a spam log, select a log and then select **Remove user**, **Block user**, **Remove log**, or **Trust user**.
+
+### Resolving spam logs
+
+You can resolve a spam log with one of the following effects:
+
+| Option | Description |
+|---------|-------------|
+| **Remove user** | The user is [deleted](../user/profile/account/delete_account.md) from the instance. |
+| **Block user** | The user is blocked from the instance. The spam log remains in the list. |
+| **Remove log** | The spam log is removed from the list. |
+| **Trust user** | The user is trusted, and can create issues, notes, snippets, and merge requests without being blocked for spam. Spam logs are not created for trusted users. |
+
+NOTE:
+Users can be [blocked](../api/users.md#block-user) and
+[unblocked](../api/users.md#unblock-user) using the GitLab API.
diff --git a/doc/administration/settings/continuous_integration.md b/doc/administration/settings/continuous_integration.md
index 841b6e644eb..0e2a512302d 100644
--- a/doc/administration/settings/continuous_integration.md
+++ b/doc/administration/settings/continuous_integration.md
@@ -266,6 +266,22 @@ To enable or disable the banner:
1. Select or clear the **Enable pipeline suggestion banner** checkbox.
1. Select **Save changes**.
+## Enable or disable the external redirect page for job artifacts
+
+By default, GitLab Pages shows an external redirect page when a user tries to view
+a job artifact served by GitLab Pages. This page warns about the potential for
+malicious user-generated content, as described in
+[issue 352611](https://gitlab.com/gitlab-org/gitlab/-/issues/352611).
+
+Self-managed administrators can disable the external redirect warning page,
+so you can view job artifact pages directly:
+
+1. On the left sidebar, select **Search or go to**.
+1. Select **Admin Area**.
+1. Select **Settings > CI/CD**.
+1. Expand **Continuous Integration and Deployment**.
+1. Deselect **Enable the external redirect page for job artifacts**.
+
## Required pipeline configuration **(ULTIMATE SELF)**
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/352316) from GitLab Premium to GitLab Ultimate in 15.0.
diff --git a/doc/administration/settings/gitaly_timeouts.md b/doc/administration/settings/gitaly_timeouts.md
index 3304db3d148..1cab1e9fd01 100644
--- a/doc/administration/settings/gitaly_timeouts.md
+++ b/doc/administration/settings/gitaly_timeouts.md
@@ -20,8 +20,10 @@ To access Gitaly timeout settings:
The following timeouts are available.
-| Timeout | Default | Description |
-|:--------|:-----------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| Timeout | Default | Description |
+|:--------|:-----------|:------------|
| Default | 55 seconds | Timeout for most Gitaly calls (not enforced for `git` `fetch` and `push` operations, or Sidekiq jobs). For example, checking if a repository exists on disk. Makes sure that Gitaly calls made within a web request cannot exceed the entire request timeout. It should be shorter than the [worker timeout](../operations/puma.md#change-the-worker-timeout) that can be configured for [Puma](../../install/requirements.md#puma-settings). If a Gitaly call timeout exceeds the worker timeout, the remaining time from the worker timeout is used to avoid having to terminate the worker. |
-| Fast | 10 seconds | Timeout for fast Gitaly operations used within requests, sometimes multiple times. For example, checking if a repository exists on disk. If fast operations exceed this threshold, there may be a problem with a storage shard. Failing fast can help maintain the stability of the GitLab instance. |
-| Medium | 30 seconds | Timeout for Gitaly operations that should be fast (possibly within requests) but preferably not used multiple times within a request. For example, loading blobs. Timeout that should be set between Default and Fast. |
+| Fast | 10 seconds | Timeout for fast Gitaly operations used within requests, sometimes multiple times. For example, checking if a repository exists on disk. If fast operations exceed this threshold, there may be a problem with a storage shard. Failing fast can help maintain the stability of the GitLab instance. |
+| Medium | 30 seconds | Timeout for Gitaly operations that should be fast (possibly within requests) but preferably not used multiple times within a request. For example, loading blobs. Timeout that should be set between Default and Fast. |
+
+You can also [configure negotiation timeouts](../gitaly/configure_gitaly.md#configure-negotiation-timeouts).
diff --git a/doc/administration/settings/jira_cloud_app.md b/doc/administration/settings/jira_cloud_app.md
index f4f1db3617e..8ff2a9acdb8 100644
--- a/doc/administration/settings/jira_cloud_app.md
+++ b/doc/administration/settings/jira_cloud_app.md
@@ -37,6 +37,9 @@ To create an OAuth application on your self-managed instance:
- If you're installing the app from the official marketplace listing, enter `https://gitlab.com/-/jira_connect/oauth_callbacks`.
- If you're installing the app manually, enter `<instance_url>/-/jira_connect/oauth_callbacks` and replace `<instance_url>` with the URL of your instance.
1. Clear the **Trusted** and **Confidential** checkboxes.
+
+ NOTE:
+ You must clear these checkboxes to avoid errors.
1. In **Scopes**, select the `api` checkbox only.
1. Select **Save application**.
1. Copy the **Application ID** value.
@@ -45,6 +48,28 @@ To create an OAuth application on your self-managed instance:
1. Paste the **Application ID** value into **Jira Connect Application ID**.
1. Select **Save changes**.
+## Jira user requirements
+
+> Support for the `org-admins` group [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/420687) in GitLab 16.6.
+
+In your [Atlassian organization](https://admin.atlassian.com), you must ensure that the Jira user that is used to set up the GitLab for Jira Cloud app is a member of
+either:
+
+- The Organization Administrators (`org-admins`) group. Newer Atlassian organizations are using
+ [centralized user management](https://support.atlassian.com/user-management/docs/give-users-admin-permissions/#Centralized-user-management-content),
+ which contains the `org-admins` group. Existing Atlassian organizations are being migrated to centralized user management.
+ If available, you should use the `org-admins` group to indicate which Jira users can manage the GitLab for Jira app. Alternatively you can use the
+ `site-admins` group.
+- The Site Administrators (`site-admins`) group. The `site-admins` group was used under
+ [original user management](https://support.atlassian.com/user-management/docs/give-users-admin-permissions/#Original-user-management-content).
+
+If necessary:
+
+1. [Create your preferred group](https://support.atlassian.com/user-management/docs/create-groups/).
+1. [Edit the group](https://support.atlassian.com/user-management/docs/edit-a-group/) to add your Jira user as a member of it.
+1. If you customized your global permissions in Jira, you might also need to grant the
+ [`Browse users and groups` permission](https://confluence.atlassian.com/jirakb/unable-to-browse-for-users-and-groups-120521888.html) to the Jira user.
+
## Connect the GitLab for Jira Cloud app
> Introduced in GitLab 15.7.
@@ -76,6 +101,7 @@ With this method:
- Set up an internet-facing reverse proxy in front of your self-managed instance. To secure this proxy further, only allow inbound
traffic from [Atlassian IP addresses](https://support.atlassian.com/organization-administration/docs/ip-addresses-and-domains-for-atlassian-cloud-products/#Outgoing-Connections).
- Add [GitLab IP addresses](../../user/gitlab_com/index.md#ip-range) to the allowlist of your firewall.
+- The Jira user that installs and configures the GitLab for Jira Cloud app must meet certain [requirements](#jira-user-requirements).
### Set up your instance
@@ -144,6 +170,7 @@ To support your self-managed instance with Jira Cloud, do one of the following:
- The instance must be publicly available.
- You must set up [OAuth authentication](#set-up-oauth-authentication).
+- The Jira user that installs and configures the GitLab for Jira Cloud app must meet certain [requirements](#jira-user-requirements).
### Install the app in development mode
@@ -314,6 +341,8 @@ To resolve this issue, ensure all prerequisites for your installation method hav
- [Prerequisites for connecting the GitLab for Jira Cloud app](#prerequisites)
- [Prerequisites for installing the GitLab for Jira Cloud app manually](#prerequisites-1)
+If you have configured a Jira Connect Proxy URL and the problem persists after checking the prerequisites, review [Debugging Jira Connect Proxy issues](#debugging-jira-connect-proxy-issues).
+
If you're using GitLab 15.8 and earlier and have previously enabled both the `jira_connect_oauth_self_managed`
and the `jira_connect_oauth` feature flags, you must disable the `jira_connect_oauth_self_managed` flag
due to a [known issue](https://gitlab.com/gitlab-org/gitlab/-/issues/388943). To check for these flags:
@@ -331,6 +360,46 @@ due to a [known issue](https://gitlab.com/gitlab-org/gitlab/-/issues/388943). To
Feature.disable(:jira_connect_oauth_self_managed)
```
+#### Debugging Jira Connect Proxy issues
+
+If you are using a self-managed GitLab instance and you have configured `https://gitlab.com` for the Jira Connect Proxy URL when
+[setting up the OAuth authentication](#set-up-oauth-authentication), you can inspect the network traffic in your browser's development
+tools while reproducing the `Failed to update the GitLab instance` error to see a more precise error.
+
+You should see a `GET` request to `https://gitlab.com/-/jira_connect/installations`.
+
+This request should return a `200` status code, but it can return a `422` status code if there was a problem. The response body can be checked for the error.
+
+If you cannot resolve the problem and you are a GitLab customer, contact [GitLab Support](https://about.gitlab.com/support/) for assistance. Provide
+GitLab Support with:
+
+1. Your GitLab self-managed instance URL.
+1. Your GitLab.com username.
+1. If possible, the `X-Request-Id` response header for the failed `GET` request to `https://gitlab.com/-/jira_connect/installations`.
+1. Optional. [A HAR file that captured the problem](https://support.zendesk.com/hc/en-us/articles/4408828867098-Generating-a-HAR-file-for-troubleshooting).
+
+The GitLab Support team can then look up why this is failing in the GitLab.com server logs.
+
+##### Process for GitLab Support
+
+NOTE:
+These steps can only be completed by GitLab Support.
+
+In Kibana, the logs should be filtered for `json.meta.caller_id: JiraConnect::InstallationsController#update` and `NOT json.status: 200`.
+If you have been provided the `X-Request-Id` value, you can use that against `json.correlation_id` to narrow down the results.
+
+Each `GET` request to the Jira Connect Proxy URL `https://gitlab.com/-/jira_connect/installations` generates two log entries.
+
+For the first log:
+
+- `json.status` is `422`.
+- `json.params.value` should match the GitLab self-managed URL `[[FILTERED], {"instance_url"=>"https://gitlab.example.com"}]`.
+
+For the second log:
+
+- `json.message` is `Proxy lifecycle event received error response` or similar.
+- `json.jira_status_code` and `json.jira_body` might contain details on why GitLab.com wasn't able to connect back to the self-managed instance.
+
### `Failed to link group`
After you connect the GitLab for Jira Cloud app for self-managed instances, you might get one of these errors:
@@ -349,9 +418,6 @@ When you check the browser console, you might see the following message:
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at https://gitlab.example.com/-/jira_connect/oauth_application_id. (Reason: CORS header 'Access-Control-Allow-Origin' missing). Status code: 403.
```
-`403` status code is returned if:
-
-- The user information cannot be fetched from Jira.
-- The authenticated Jira user does not have [site administrator](https://support.atlassian.com/user-management/docs/give-users-admin-permissions/#Make-someone-a-site-admin) access.
+`403` status code is returned if the user information cannot be fetched from Jira because of insufficient permissions.
-To resolve this issue, ensure the authenticated user is a Jira site administrator and try again.
+To resolve this issue, ensure that the Jira user that installs and configures the GitLab for Jira Cloud app meets certain [requirements](#jira-user-requirements).
diff --git a/doc/administration/settings/rate_limits_on_git_ssh_operations.md b/doc/administration/settings/rate_limits_on_git_ssh_operations.md
index 677d8fea195..4e60fd55b19 100644
--- a/doc/administration/settings/rate_limits_on_git_ssh_operations.md
+++ b/doc/administration/settings/rate_limits_on_git_ssh_operations.md
@@ -20,8 +20,6 @@ Each command has a rate limit of 600 per minute. For example:
Because the same commands are shared by `git-upload-pack`, `git pull`, and `git clone`, they share a rate limit.
-Users on self-managed GitLab can disable this rate limit.
-
## Configure GitLab Shell operation limit
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/123761) in GitLab 16.2.
@@ -33,4 +31,5 @@ Users on self-managed GitLab can disable this rate limit.
1. Select **Settings > Network**.
1. Expand **Git SSH operations rate limit**.
1. Enter a value for **Maximum number of Git operations per minute**.
+ - To disable the rate limit, set it to `0`.
1. Select **Save changes**.
diff --git a/doc/administration/settings/scim_setup.md b/doc/administration/settings/scim_setup.md
index 432c8598cf7..45020fdfb59 100644
--- a/doc/administration/settings/scim_setup.md
+++ b/doc/administration/settings/scim_setup.md
@@ -53,3 +53,7 @@ adding them to the SCIM identity provider.
After the identity provider performs a sync based on its configured schedule,
the user's SCIM identity is reactivated and their GitLab instance access is restored.
+
+## Troubleshooting
+
+See our [troubleshooting SCIM guide](../../user/group/saml_sso/troubleshooting_scim.md).
diff --git a/doc/administration/settings/sign_in_restrictions.md b/doc/administration/settings/sign_in_restrictions.md
index 6d38610192b..942b706b9a3 100644
--- a/doc/administration/settings/sign_in_restrictions.md
+++ b/doc/administration/settings/sign_in_restrictions.md
@@ -118,7 +118,7 @@ The following access methods are **not** protected by Admin Mode:
In other words, administrators who are otherwise limited by Admin Mode can still use
Git clients without additional authentication steps.
-To use the GitLab REST- or GraphQL API, administrators must [create a personal access token](../../user/profile/personal_access_tokens.md#create-a-personal-access-token) with the [`admin_mode` scope](../../user/profile/personal_access_tokens.md#personal-access-token-scopes).
+To use the GitLab REST- or GraphQL API, administrators must [create a personal access token](../../user/profile/personal_access_tokens.md#create-a-personal-access-token) or [OAuth token](../../api/oauth2.md) with the [`admin_mode` scope](../../user/profile/personal_access_tokens.md#personal-access-token-scopes).
If an administrator with a personal access token with the `admin_mode` scope loses their administrator access, that user cannot access the API as an administrator even though they still have the token with the `admin_mode` scope.
diff --git a/doc/administration/settings/slack_app.md b/doc/administration/settings/slack_app.md
index ef756dfeff7..de11da281e4 100644
--- a/doc/administration/settings/slack_app.md
+++ b/doc/administration/settings/slack_app.md
@@ -105,9 +105,13 @@ To enable the GitLab for Slack app functionality, your network must allow inboun
## Troubleshooting
-### Slash commands return `/gitlab failed with the error "dispatch_failed"` in Slack
+When administering the GitLab for Slack app for self-managed instances, you might encounter the following issues.
+
+For GitLab.com, see [GitLab for Slack app](../../user/project/integrations/gitlab_slack_application.md#troubleshooting).
+
+### Slash commands return an error in Slack
Slash commands might return `/gitlab failed with the error "dispatch_failed"` in Slack. To resolve this issue, ensure:
-- The GitLab for Slack app is properly [configured](#configure-the-settings), and the **Enable GitLab for Slack app** checkbox is selected.
+- The GitLab for Slack app is properly [configured](#configure-the-settings) and the **Enable GitLab for Slack app** checkbox is selected.
- Your GitLab instance [allows requests to and from Slack](#connectivity-requirements).
diff --git a/doc/administration/settings/usage_statistics.md b/doc/administration/settings/usage_statistics.md
index 4887ebd8cfe..b9080f49f5d 100644
--- a/doc/administration/settings/usage_statistics.md
+++ b/doc/administration/settings/usage_statistics.md
@@ -9,7 +9,8 @@ info: To determine the technical writer assigned to the Stage/Group associated w
GitLab Inc. periodically collects information about your instance in order
to perform various actions.
-All usage statistics are [opt-out](#enable-or-disable-usage-statistics).
+For free self-managed instances, all usage statistics are [opt-out](#enable-or-disable-service-ping).
+For information about other tiers, see [Customer Product Usage Information](https://about.gitlab.com/handbook/legal/privacy/customer-product-usage-information/#service-ping-formerly-known-as-usage-ping).
## Service Ping
@@ -63,6 +64,13 @@ In the following table, you can see:
| [Issue analytics](../../user/group/issues_analytics/index.md) | GitLab 16.5 and later |
| [Custom Text in Emails](../../administration/settings/email.md#custom-additional-text) | GitLab 16.5 and later |
| [Contribution analytics](../../user/group/contribution_analytics/index.md) | GitLab 16.5 and later |
+| [Group file templates](../../user/group/manage.md#group-file-templates) | GitLab 16.6 and later |
+| [Group webhooks](../../user/project/integrations/webhooks.md#group-webhooks) | GitLab 16.6 and later |
+| [Service Level Agreement countdown timer](../../operations/incident_management/incidents.md#service-level-agreement-countdown-timer) | GitLab 16.6 and later |
+| [Lock project membership to group](../../user/group/access_and_permissions.md#prevent-members-from-being-added-to-projects-in-a-group) | GitLab 16.6 and later |
+| [Users and permissions report](../../administration/admin_area.md#user-permission-export) | GitLab 16.6 and later |
+| [Advanced search](../../user/search/advanced_search.md) | GitLab 16.6 and later |
+| [DevOps Adoption](../../user/group/devops_adoption/index.md) | GitLab 16.6 and later |
### Enable registration features
@@ -95,7 +103,16 @@ This information is used, among other things, to identify to which versions
patches must be backported, making sure active GitLab instances remain
secure.
-If you [disable version check](#enable-or-disable-usage-statistics), this information isn't collected.
+If you disable version check, this information isn't collected.
+
+### Enable or disable version check
+
+1. On the left sidebar, select **Search or go to**.
+1. Select **Admin Area**.
+1. Select **Settings > Metrics and profiling**.
+1. Expand **Usage statistics**.
+1. Select or clear the **Enable version check** checkbox.
+1. Select **Save changes**.
### Request flow example
@@ -121,23 +138,26 @@ GitLab instance to the host `version.gitlab.com` on port `443`.
If your GitLab instance is behind a proxy, set the appropriate
[proxy configuration variables](https://docs.gitlab.com/omnibus/settings/environment-variables.html).
-## Enable or disable usage statistics
+## Enable or disable Service Ping
+
+### Through the UI
-To enable or disable Service Ping and version check:
+To enable or disable Service Ping:
1. On the left sidebar, select **Search or go to**.
1. Select **Admin Area**.
1. Select **Settings > Metrics and profiling**.
1. Expand **Usage statistics**.
-1. Select or clear the **Enable version check** and **Enable Service Ping** checkboxes.
+1. Select or clear the **Enable Service Ping** checkbox.
1. Select **Save changes**.
NOTE:
-Service Ping settings only control whether the data is being shared with GitLab, or used only internally.
+The effect of disabling Service Ping depends on the instance's tier. For more information, see [Customer Product Usage Information](https://about.gitlab.com/handbook/legal/privacy/customer-product-usage-information/#service-ping-formerly-known-as-usage-ping).
+Service Ping settings only control whether the data is being shared with GitLab, or limited to only internal use by the instance.
Even if you disable Service Ping, the `gitlab_service_ping_worker` background job still periodically generates a Service Ping payload for your instance.
-The payload is available in the [Service Usage data](#manually-upload-service-ping-payload) admin section.
+The payload is available in the [Metrics and profiling](#manually-upload-service-ping-payload) admin section.
-## Disable usage statistics with the configuration file
+### Through the configuration file
NOTE:
The method to disable Service Ping in the GitLab configuration file does not work in
@@ -189,7 +209,7 @@ You can view the exact JSON payload sent to GitLab Inc. in the Admin Area. To vi
1. Sign in as a user with administrator access.
1. On the left sidebar, select **Search or go to**.
1. Select **Admin Area**.
-1. Select **Settings > Service usage data**.
+1. Select **Settings > Metrics and profiling > Usage statistics**.
1. Select **Preview payload**.
For an example payload, see [Example Service Ping payload](../../development/internal_analytics/service_ping/index.md#example-service-ping-payload).
@@ -207,7 +227,7 @@ To upload the payload manually:
1. Sign in as a user with administrator access.
1. On the left sidebar, select **Search or go to**.
1. Select **Admin Area**.
-1. Select **Settings > Service usage data**.
+1. Select **Settings > Metrics and profiling > Usage statistics**.
1. Select **Download payload**.
1. Save the JSON file.
1. Visit [Service usage data center](https://version.gitlab.com/usage_data/new).
diff --git a/doc/administration/sidekiq/index.md b/doc/administration/sidekiq/index.md
index 10fadc40a82..0a7974c9622 100644
--- a/doc/administration/sidekiq/index.md
+++ b/doc/administration/sidekiq/index.md
@@ -95,27 +95,8 @@ Updates to example must be made at:
-->
```ruby
- ########################################
- ##### Services Disabled ###
- ########################################
- #
- # When running GitLab on just one server, you have a single `gitlab.rb`
- # to enable all services you want to run.
- # When running GitLab on N servers, you have N `gitlab.rb` files.
- # Enable only the services you want to run on each
- # specific server, while disabling all others.
- #
- gitaly['enable'] = false
- postgresql['enable'] = false
- redis['enable'] = false
- nginx['enable'] = false
- puma['enable'] = false
- gitlab_workhorse['enable'] = false
- prometheus['enable'] = false
- alertmanager['enable'] = false
- grafana['enable'] = false
- gitlab_exporter['enable'] = false
- gitlab_kas['enable'] = false
+ # https://docs.gitlab.com/omnibus/roles/#sidekiq-roles
+ roles(["sidekiq_role"])
##
## To maintain uniformity of links across nodes, the
@@ -375,20 +356,6 @@ To enable LDAP with the synchronization worker for Sidekiq:
If you use [SAML Group Sync](../../user/group/saml_sso/group_sync.md), you must configure [SAML Groups](../../integration/saml.md#configure-users-based-on-saml-group-membership) on all your Sidekiq nodes.
-## Disable Rugged
-
-Calls into Rugged, Ruby bindings for `libgit2`, [lock the Sidekiq processes (GVL)](https://silverhammermba.github.io/emberb/c/#c-in-ruby-threads),
-blocking all jobs on that worker from proceeding. If Rugged calls performed by Sidekiq are slow, this can cause significant delays in
-background task processing.
-
-By default, Rugged is used when Git repository data is stored on local storage or on an NFS mount.
-Using Rugged is recommended when using NFS, but if
-you are using local storage, disabling Rugged can improve Sidekiq performance:
-
-```shell
-sudo gitlab-rake gitlab:features:disable_rugged
-```
-
## Related topics
- [Extra Sidekiq processes](extra_sidekiq_processes.md)
diff --git a/doc/administration/sidekiq/processing_specific_job_classes.md b/doc/administration/sidekiq/processing_specific_job_classes.md
index 696b0b9444c..74cbb6ca89b 100644
--- a/doc/administration/sidekiq/processing_specific_job_classes.md
+++ b/doc/administration/sidekiq/processing_specific_job_classes.md
@@ -179,14 +179,16 @@ nodes. In this example, we exclude all import-related jobs from a Sidekiq node.
sudo gitlab-ctl reconfigure
```
-### Migrating from queue selectors to routing rules
+## Migrating from queue selectors to routing rules
We recommend GitLab deployments add more Sidekiq processes listening to all queues, as in the
[Reference Architectures](../reference_architectures/index.md). For very large-scale deployments, we recommend
[routing rules](#routing-rules) instead of [queue selectors](#queue-selectors-deprecated). We use routing rules on GitLab.com as
it helps to lower the load on Redis.
-To migrate from queue selectors to routing rules:
+### Single node setup
+
+To migrate from queue selectors to routing rules in a [single node setup](../reference_architectures/index.md#standalone-non-ha):
1. Open `/etc/gitlab/gitlab.rb`.
1. Set `sidekiq['queue_selector']` to `false`.
@@ -213,9 +215,11 @@ NOTE:
It is important to run the Rake task immediately after reconfiguring GitLab.
After reconfiguring GitLab, existing jobs are not processed until the Rake task starts to migrate the jobs.
+#### Migration example
+
The following example better illustrates the migration process above:
-1. Check the following content of `/etc/gitlab/gitlab.rb`:
+1. In `/etc/gitlab/gitlab.rb`, check the `urgency` queries in the `sidekiq['queue_groups']`. For example:
```ruby
sidekiq['routing_rules'] = []
@@ -228,7 +232,7 @@ The following example better illustrates the migration process above:
]
```
-1. Update `/etc/gitlab/gitlab.rb` to use routing rules:
+1. Use these same `urgency` queries to update `/etc/gitlab/gitlab.rb` to use routing rules:
```ruby
sidekiq['min_concurrency'] = 20
@@ -270,6 +274,31 @@ in a queue group entry is 1, while `min_concurrency` is set to `0`, and `max_con
concurrency is set to `2` instead. A concurrency of `2` might be too low in most cases, except for very highly-CPU
bound tasks.
+### Multiple node setup
+
+For a multiple node setup:
+
+- Reconfigure all GitLab Rails and Sidekiq nodes with the same `sidekiq['routing_rules']` setting.
+- Alternate between GitLab Rails and Sidekiq nodes as you update and reconfigure the nodes. This ensures the newly configured Sidekiq is ready to consume jobs from the new set of
+ queues during the migration. Otherwise, the new jobs hang until the end of the migration.
+
+Consider the following example of three GitLab Rails nodes and two Sidekiq nodes. To migrate from queue selectors to routing rules:
+
+1. In Sidekiq 1, follow all steps but one in [single node setup](#single-node-setup).
+ **Do not** run the Rake task to [migrate existing jobs](sidekiq_job_migration.md).
+1. Configure the external load balancer to remove Rails 1 from accepting traffic. This step ensures Rails 1 is not serving any request while the Rails process is restarting. For more information, see [issue 428794](https://gitlab.com/gitlab-org/gitlab/-/issues/428794#note_1619505870).
+1. In Rails 1, update `/etc/gitlab/gitlab.rb` to use the same `sidekiq['routing_rules']` setting as Sidekiq 1.
+ Only `sidekiq['routing_rules']` is required in Rails nodes.
+1. Configure the external load balancer to register Rails 1 back.
+1. Repeat steps 1 to 4 for Sidekiq 2 and Rails 2.
+1. Repeat steps 2 to 4 for Rails 3.
+1. If there are more Sidekiq nodes than Rails nodes, follow step 1 on the remaining Sidekiq nodes.
+1. Run the Rake task to [migrate existing jobs](sidekiq_job_migration.md):
+
+ ```shell
+ sudo gitlab-rake gitlab:sidekiq:migrate_jobs:retry gitlab:sidekiq:migrate_jobs:schedule gitlab:sidekiq:migrate_jobs:queued
+ ```
+
<!--- end_remove -->
## Worker matching query
diff --git a/doc/administration/sidekiq/sidekiq_troubleshooting.md b/doc/administration/sidekiq/sidekiq_troubleshooting.md
index 9ae2a59251a..2990110150f 100644
--- a/doc/administration/sidekiq/sidekiq_troubleshooting.md
+++ b/doc/administration/sidekiq/sidekiq_troubleshooting.md
@@ -536,6 +536,28 @@ The list of available jobs can be found in the [workers](https://gitlab.com/gitl
For more information about Sidekiq jobs, see the [Sidekiq-cron](https://github.com/sidekiq-cron/sidekiq-cron#work-with-job) documentation.
+## Disabling cron jobs
+
+You can disable any Sidekiq cron jobs by visiting the [Monitoring section in the Admin area](../admin_area.md#monitoring-section). You can also perform the same action using the command line and [Rails Runner](../operations/rails_console.md#using-the-rails-runner).
+
+To disable all cron jobs:
+
+```shell
+sudo gitlab-rails runner 'Sidekiq::Cron::Job.all.map(&:disable!)'
+```
+
+To enable all cron jobs:
+
+```shell
+sudo gitlab-rails runner 'Sidekiq::Cron::Job.all.map(&:enable!)'
+```
+
+If you wish to enable only a subset of the jobs at a time you can use name matching. For example, to enable only jobs with `geo` in the name:
+
+```shell
+ sudo gitlab-rails runner 'Sidekiq::Cron::Job.all.select{ |j| j.name.match("geo") }.map(&:disable!)'
+```
+
## Clearing a Sidekiq job deduplication idempotency key
Occasionally, jobs that are expected to run (for example, cron jobs) are observed to not run at all. When checking the logs, there might be instances where jobs are seen to not run with a `"job_status": "deduplicated"`.
diff --git a/doc/administration/silent_mode/index.md b/doc/administration/silent_mode/index.md
index 379b00536f3..4f68a765585 100644
--- a/doc/administration/silent_mode/index.md
+++ b/doc/administration/silent_mode/index.md
@@ -4,10 +4,11 @@ group: Geo
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
-# GitLab Silent Mode **(FREE SELF EXPERIMENT)**
+# GitLab Silent Mode **(FREE SELF)**
-> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/9826) in GitLab 15.11. This feature is an [Experiment](../../policy/experiment-beta-support.md#experiment).
-> - Enabling and disabling Silent Mode through the web UI was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131090) in GitLab 16.4
+> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/9826) in GitLab 15.11. This feature was an [Experiment](../../policy/experiment-beta-support.md#experiment).
+> - Enabling and disabling Silent Mode through the web UI was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131090) in GitLab 16.4.
+> - Silent Mode was updated to [Generally Available (GA)](../../policy/experiment-beta-support.md#generally-available-ga) in GitLab 16.6.
Silent Mode allows you to silence outbound communication, such as emails, from GitLab. Silent Mode is not intended to be used on environments which are in-use. Two use-cases are:
@@ -76,7 +77,7 @@ It may take up to a minute to take effect. [Issue 405433](https://gitlab.com/git
## Behavior of GitLab features in Silent Mode
-This section documents the current behavior of GitLab when Silent Mode is enabled. While Silent Mode is an Experiment, the behavior may change without notice. The work for the first iteration of Silent Mode is tracked by [Epic 9826](https://gitlab.com/groups/gitlab-org/-/epics/9826).
+This section documents the current behavior of GitLab when Silent Mode is enabled. The work for the first iteration of Silent Mode is tracked by [Epic 9826](https://gitlab.com/groups/gitlab-org/-/epics/9826).
When Silent Mode is enabled, a banner is displayed at the top of the page for all users stating the setting is enabled and **All outbound communications are blocked.**.
diff --git a/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md b/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md
index 9432836c22b..01c75c32366 100644
--- a/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md
+++ b/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md
@@ -46,11 +46,11 @@ This content has been moved to [Troubleshooting Repository mirroring](../../user
## CI
-This content has been moved to [Troubleshooting CI/CD](../../ci/troubleshooting.md).
+This content has been moved to [Troubleshooting CI/CD](../cicd.md#cicd-troubleshooting-rails-console-commands).
## License
-This content has been moved to [Activate GitLab EE with a license file or key](../../administration/license_file.md).
+This content has been moved to [Activate GitLab EE with a license file or key](../license_file.md).
## Registry
diff --git a/doc/api/api_resources.md b/doc/api/api_resources.md
index 3c3430dead4..76c91b00eb9 100644
--- a/doc/api/api_resources.md
+++ b/doc/api/api_resources.md
@@ -8,14 +8,14 @@ info: To determine the technical writer assigned to the Stage/Group associated w
Available resources for the [GitLab REST API](index.md) can be grouped in the following contexts:
-- [Projects](#project-resources).
-- [Groups](#group-resources).
-- [Standalone](#standalone-resources).
+- [Projects](#project-resources)
+- [Groups](#group-resources)
+- [Standalone](#standalone-resources)
See also:
-- Adding [deploy keys for multiple projects](deploy_keys.md#add-deploy-keys-to-multiple-projects).
-- [API Resources for various templates](#templates-api-resources).
+- Adding [deploy keys for multiple projects](deploy_keys.md#add-deploy-keys-to-multiple-projects)
+- [API Resources for various templates](#templates-api-resources)
## Project resources
@@ -206,7 +206,7 @@ The following API resources are available outside of project and group contexts
Endpoints are available for:
-- [Dockerfile templates](templates/dockerfiles.md).
-- [`.gitignore` templates](templates/gitignores.md).
-- [GitLab CI/CD YAML templates](templates/gitlab_ci_ymls.md).
-- [Open source license templates](templates/licenses.md).
+- [Dockerfile templates](templates/dockerfiles.md)
+- [`.gitignore` templates](templates/gitignores.md)
+- [GitLab CI/CD YAML templates](templates/gitlab_ci_ymls.md)
+- [Open source license templates](templates/licenses.md)
diff --git a/doc/api/bulk_imports.md b/doc/api/bulk_imports.md
index db508d1edfa..0f9df4eba31 100644
--- a/doc/api/bulk_imports.md
+++ b/doc/api/bulk_imports.md
@@ -257,3 +257,26 @@ curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab
"updated_at": "2021-06-18T09:46:27.003Z"
}
```
+
+## Get list of failed import records for group or project migration entity
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/428016) in GitLab 16.6.
+
+```plaintext
+GET /bulk_imports/:id/entities/:entity_id/failures
+```
+
+```shell
+curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/bulk_imports/1/entities/2/failures"
+```
+
+```json
+{
+ "relation": "issues",
+ "exception_message": "Error!",
+ "exception_class": "StandardError",
+ "correlation_id_value": "06289e4b064329a69de7bb2d7a1b5a97",
+ "source_url": "https://gitlab.example/project/full/path/-/issues/1",
+ "source_title": "Issue title"
+}
+```
diff --git a/doc/api/container_registry.md b/doc/api/container_registry.md
index 901b0b93529..35b74965d2e 100644
--- a/doc/api/container_registry.md
+++ b/doc/api/container_registry.md
@@ -425,7 +425,7 @@ curl --request DELETE --data-urlencode 'name_regex_delete=dev-.+' \
Beside the group- and project-specific GitLab APIs explained above,
the Container Registry has its own endpoints.
To query those, follow the Registry's built-in mechanism to obtain and use an
-[authentication token](https://docs.docker.com/registry/spec/auth/token/).
+[authentication token](https://distribution.github.io/distribution/spec/auth/token/).
NOTE:
These are different from project or personal access tokens in the GitLab application.
@@ -436,7 +436,7 @@ These are different from project or personal access tokens in the GitLab applica
GET ${CI_SERVER_URL}/jwt/auth?service=container_registry&scope=*
```
-You must specify the correct [scopes and actions](https://docs.docker.com/registry/spec/auth/scope/) to retrieve a valid token:
+You must specify the correct [scopes and actions](https://distribution.github.io/distribution/spec/auth/scope/) to retrieve a valid token:
```shell
$ SCOPE="repository:${CI_REGISTRY_IMAGE}:delete" #or push,pull
@@ -448,17 +448,28 @@ $ curl --request GET --user "${CI_REGISTRY_USER}:${CI_REGISTRY_PASSWORD}" \
### Delete image tags by reference
+> Endpoint `v2/<name>/manifests/<tag>` [introduced](https://gitlab.com/gitlab-org/container-registry/-/issues/1091) and endpoint `v2/<name>/tags/reference/<tag>` [deprecated](https://gitlab.com/gitlab-org/container-registry/-/issues/1094) in GitLab 16.4.
+
+<!--- start_remove The following content will be removed on remove_date: '2024-08-15' -->
+
+WARNING:
+Endpoint `v2/<name>/tags/reference/<tag>` [deprecated](https://gitlab.com/gitlab-org/container-registry/-/issues/1095)
+in GitLab 16.4 and is planned for removal in 17.0. Use [`v2/<name>/manifests/<tag>`](https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs/spec/docker/v2/api.md#delete-manifest) instead.
+This change is a breaking change.
+
+<!--- end_remove -->
+
```plaintext
DELETE http(s)://${CI_REGISTRY}/v2/${CI_REGISTRY_IMAGE}/tags/reference/${CI_COMMIT_SHORT_SHA}
```
You can use the token retrieved with the predefined `CI_REGISTRY_USER` and `CI_REGISTRY_PASSWORD` variables to delete container image tags by reference on your GitLab instance.
-The `tag_delete` [Container-Registry-Feature](https://gitlab.com/gitlab-org/container-registry/-/tree/v3.61.0-gitlab/docs-gitlab#api) must be enabled.
+The `tag_delete` [Container-Registry-Feature](https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs/spec/docker/v2/api.md#delete-tag) must be enabled.
```shell
$ curl --request DELETE --header "Authorization: Bearer <token_from_above>" \
--header "Accept: application/vnd.docker.distribution.manifest.v2+json" \
- "https://gitlab.example.com:5050/v2/${CI_REGISTRY_IMAGE}/tags/reference/${CI_COMMIT_SHORT_SHA}"
+ "https://gitlab.example.com:5050/v2/${CI_REGISTRY_IMAGE}/manifests/${CI_COMMIT_SHORT_SHA}"
```
### Listing all container repositories
diff --git a/doc/api/dependency_list_export.md b/doc/api/dependency_list_export.md
index 744309a402e..db43ea238c1 100644
--- a/doc/api/dependency_list_export.md
+++ b/doc/api/dependency_list_export.md
@@ -23,7 +23,7 @@ and subject to change without notice.
Create a new CycloneDX JSON export for all the project dependencies detected in a pipeline.
-If an authenticated user doesn't have permission to [read_dependency](../user/custom_roles.md#custom-role-requirements),
+If an authenticated user does not have permission to [read_dependency](../user/custom_roles.md#available-permissions),
this request returns a `403 Forbidden` status code.
SBOM exports can be only accessed by the export's author.
@@ -59,7 +59,7 @@ Example response:
Get a single dependency list export.
```plaintext
-GET /security/dependency_list_exports/:id
+GET /dependency_list_exports/:id
```
| Attribute | Type | Required | Description |
@@ -67,7 +67,7 @@ GET /security/dependency_list_exports/:id
| `id` | integer | yes | The ID of the dependency list export. |
```shell
-curl --header "PRIVATE-TOKEN: <private_token>" "https://gitlab.example.com/api/v4/security/dependency_list_exports/2"
+curl --header "PRIVATE-TOKEN: <private_token>" "https://gitlab.example.com/api/v4/dependency_list_exports/2"
```
The status code is `202 Accepted` when the dependency list export is being generated, and `200 OK` when it's ready.
@@ -88,7 +88,7 @@ Example response:
Download a single dependency list export.
```plaintext
-GET /security/dependency_list_exports/:id/download
+GET /dependency_list_exports/:id/download
```
| Attribute | Type | Required | Description |
@@ -96,7 +96,7 @@ GET /security/dependency_list_exports/:id/download
| `id` | integer | yes | The ID of the dependency list export. |
```shell
-curl --header "PRIVATE-TOKEN: <private_token>" "https://gitlab.example.com/api/v4/security/dependency_list_exports/2/download"
+curl --header "PRIVATE-TOKEN: <private_token>" "https://gitlab.example.com/api/v4/dependency_list_exports/2/download"
```
The response is `404 Not Found` if the dependency list export is not finished yet or was not found.
diff --git a/doc/api/deployments.md b/doc/api/deployments.md
index aad3567879a..2dbc4bd0831 100644
--- a/doc/api/deployments.md
+++ b/doc/api/deployments.md
@@ -306,7 +306,7 @@ When the [unified approval setting](../ci/environments/deployment_approvals.md#u
}
```
-When the [multiple approval rules](../ci/environments/deployment_approvals.md#multiple-approval-rules) is configured, deployments created by users on GitLab Premium or Ultimate include the `approval_summary` property:
+When the [multiple approval rules](../ci/environments/deployment_approvals.md#add-multiple-approval-rules) is configured, deployments created by users on GitLab Premium or Ultimate include the `approval_summary` property:
```json
{
@@ -547,7 +547,7 @@ POST /projects/:id/deployments/:deployment_id/approval
| `deployment_id` | integer | yes | The ID of the deployment. |
| `status` | string | yes | The status of the approval (either `approved` or `rejected`). |
| `comment` | string | no | A comment to go with the approval |
-| `represented_as`| string | no | The name of the User/Group/Role to use for the approval, when the user belongs to [multiple approval rules](../ci/environments/deployment_approvals.md#multiple-approval-rules). |
+| `represented_as`| string | no | The name of the User/Group/Role to use for the approval, when the user belongs to [multiple approval rules](../ci/environments/deployment_approvals.md#add-multiple-approval-rules). |
```shell
curl --data "status=approved&comment=Looks good to me&represented_as=security" \
diff --git a/doc/api/geo_nodes.md b/doc/api/geo_nodes.md
index 3f7fd537abf..c376d7a6774 100644
--- a/doc/api/geo_nodes.md
+++ b/doc/api/geo_nodes.md
@@ -332,7 +332,6 @@ Example response:
"job_artifacts_count": 2,
"job_artifacts_synced_count": null,
"job_artifacts_failed_count": null,
- "job_artifacts_synced_missing_on_primary_count": 0,
"job_artifacts_synced_in_percentage": "0.00%",
"projects_count": 41,
"repositories_count": 41,
@@ -470,7 +469,6 @@ Example response:
"job_artifacts_verification_failed_count": 0,
"job_artifacts_synced_in_percentage": "100.00%",
"job_artifacts_verified_in_percentage": "100.00%",
- "job_artifacts_synced_missing_on_primary_count": 0,
"ci_secure_files_count": 5,
"ci_secure_files_checksum_total_count": 5,
"ci_secure_files_checksummed_count": 5,
@@ -483,7 +481,6 @@ Example response:
"ci_secure_files_verification_failed_count": 0,
"ci_secure_files_synced_in_percentage": "100.00%",
"ci_secure_files_verified_in_percentage": "100.00%",
- "ci_secure_files_synced_missing_on_primary_count": 0,
"dependency_proxy_blobs_count": 5,
"dependency_proxy_blobs_checksum_total_count": 5,
"dependency_proxy_blobs_checksummed_count": 5,
@@ -496,13 +493,11 @@ Example response:
"dependency_proxy_blobs_verification_failed_count": 0,
"dependency_proxy_blobs_synced_in_percentage": "100.00%",
"dependency_proxy_blobs_verified_in_percentage": "100.00%",
- "dependency_proxy_blobs_synced_missing_on_primary_count": 0,
"container_repositories_count": 5,
"container_repositories_synced_count": 5,
"container_repositories_failed_count": 0,
"container_repositories_registry_count": 5,
"container_repositories_synced_in_percentage": "100.00%",
- "container_repositories_synced_missing_on_primary_count": 0,
"container_repositories_checksum_total_count": 0,
"container_repositories_checksummed_count": 0,
"container_repositories_checksum_failed_count": 0,
@@ -569,7 +564,6 @@ Example response:
"job_artifacts_count": 2,
"job_artifacts_synced_count": 1,
"job_artifacts_failed_count": 1,
- "job_artifacts_synced_missing_on_primary_count": 0,
"job_artifacts_synced_in_percentage": "50.00%",
"design_management_repositories_count": 5,
"design_management_repositories_synced_count": 5,
@@ -695,7 +689,6 @@ Example response:
"job_artifacts_verification_failed_count": 0,
"job_artifacts_synced_in_percentage": "100.00%",
"job_artifacts_verified_in_percentage": "100.00%",
- "job_artifacts_synced_missing_on_primary_count": 0,
"dependency_proxy_blobs_count": 5,
"dependency_proxy_blobs_checksum_total_count": 5,
"dependency_proxy_blobs_checksummed_count": 5,
@@ -708,13 +701,11 @@ Example response:
"dependency_proxy_blobs_verification_failed_count": 0,
"dependency_proxy_blobs_synced_in_percentage": "100.00%",
"dependency_proxy_blobs_verified_in_percentage": "100.00%",
- "dependency_proxy_blobs_synced_missing_on_primary_count": 0,
"container_repositories_count": 5,
"container_repositories_synced_count": 5,
"container_repositories_failed_count": 0,
"container_repositories_registry_count": 5,
"container_repositories_synced_in_percentage": "100.00%",
- "container_repositories_synced_missing_on_primary_count": 0,
"container_repositories_checksum_total_count": 0,
"container_repositories_checksummed_count": 0,
"container_repositories_checksum_failed_count": 0,
@@ -785,7 +776,6 @@ Example response:
"job_artifacts_count": 2,
"job_artifacts_synced_count": 1,
"job_artifacts_failed_count": 1,
- "job_artifacts_synced_missing_on_primary_count": 0,
"job_artifacts_synced_in_percentage": "50.00%",
"projects_count": 41,
"repositories_count": 41,
@@ -896,7 +886,6 @@ Example response:
"job_artifacts_verification_failed_count": 0,
"job_artifacts_synced_in_percentage": "100.00%",
"job_artifacts_verified_in_percentage": "100.00%",
- "job_artifacts_synced_missing_on_primary_count": 0,
"ci_secure_files_count": 5,
"ci_secure_files_checksum_total_count": 5,
"ci_secure_files_checksummed_count": 5,
@@ -909,7 +898,6 @@ Example response:
"ci_secure_files_verification_failed_count": 0,
"ci_secure_files_synced_in_percentage": "100.00%",
"ci_secure_files_verified_in_percentage": "100.00%",
- "ci_secure_files_synced_missing_on_primary_count": 0,
"dependency_proxy_blobs_count": 5,
"dependency_proxy_blobs_checksum_total_count": 5,
"dependency_proxy_blobs_checksummed_count": 5,
@@ -922,13 +910,11 @@ Example response:
"dependency_proxy_blobs_verification_failed_count": 0,
"dependency_proxy_blobs_synced_in_percentage": "100.00%",
"dependency_proxy_blobs_verified_in_percentage": "100.00%",
- "dependency_proxy_blobs_synced_missing_on_primary_count": 0,
"container_repositories_count": 5,
"container_repositories_synced_count": 5,
"container_repositories_failed_count": 0,
"container_repositories_registry_count": 5,
"container_repositories_synced_in_percentage": "100.00%",
- "container_repositories_synced_missing_on_primary_count": 0,
"container_repositories_checksum_total_count": 0,
"container_repositories_checksummed_count": 0,
"container_repositories_checksum_failed_count": 0,
diff --git a/doc/api/geo_sites.md b/doc/api/geo_sites.md
index eaf813ae201..95691960a78 100644
--- a/doc/api/geo_sites.md
+++ b/doc/api/geo_sites.md
@@ -292,7 +292,6 @@ Example response:
[
{
"geo_node_id": 1,
- "job_artifacts_synced_missing_on_primary_count": null,
"projects_count": 19,
"container_repositories_replication_enabled": null,
"lfs_objects_count": 0,
@@ -510,7 +509,6 @@ Example response:
},
{
"geo_node_id": 2,
- "job_artifacts_synced_missing_on_primary_count": null,
"projects_count": 19,
"container_repositories_replication_enabled": null,
"lfs_objects_count": 0,
@@ -744,7 +742,6 @@ Example response:
```json
{
"geo_node_id": 2,
- "job_artifacts_synced_missing_on_primary_count": null,
"projects_count": 19,
"container_repositories_replication_enabled": null,
"lfs_objects_count": 0,
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 6015323f7f7..4a1b536fd40 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -136,7 +136,8 @@ Returns [`CiCatalogResource`](#cicatalogresource).
| Name | Type | Description |
| ---- | ---- | ----------- |
-| <a id="querycicatalogresourceid"></a>`id` | [`CiCatalogResourceID!`](#cicatalogresourceid) | CI/CD Catalog resource global ID. |
+| <a id="querycicatalogresourcefullpath"></a>`fullPath` | [`ID`](#id) | CI/CD Catalog resource full path. |
+| <a id="querycicatalogresourceid"></a>`id` | [`CiCatalogResourceID`](#cicatalogresourceid) | CI/CD Catalog resource global ID. |
### `Query.ciCatalogResources`
@@ -157,7 +158,9 @@ four standard [pagination arguments](#connection-pagination-arguments):
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="querycicatalogresourcesprojectpath"></a>`projectPath` | [`ID`](#id) | Project with the namespace catalog. |
-| <a id="querycicatalogresourcessort"></a>`sort` | [`CiCatalogResourceSort`](#cicatalogresourcesort) | Sort Catalog Resources by given criteria. |
+| <a id="querycicatalogresourcesscope"></a>`scope` | [`CiCatalogResourceScope`](#cicatalogresourcescope) | Scope of the returned catalog resources. |
+| <a id="querycicatalogresourcessearch"></a>`search` | [`String`](#string) | Search term to filter the catalog resources by name or description. |
+| <a id="querycicatalogresourcessort"></a>`sort` | [`CiCatalogResourceSort`](#cicatalogresourcesort) | Sort catalog resources by given criteria. |
### `Query.ciConfig`
@@ -324,6 +327,26 @@ Returns [`ExplainVulnerabilityPrompt`](#explainvulnerabilityprompt).
| ---- | ---- | ----------- |
| <a id="queryexplainvulnerabilitypromptvulnerabilityid"></a>`vulnerabilityId` | [`VulnerabilityID!`](#vulnerabilityid) | Vulnerability to generate a prompt for. |
+### `Query.frecentGroups`
+
+A user's frecently visited groups. Requires the `frecent_namespaces_suggestions` feature flag to be enabled.
+
+WARNING:
+**Introduced** in 16.6.
+This feature is an Experiment. It can be changed or removed at any time.
+
+Returns [`[Group!]`](#group).
+
+### `Query.frecentProjects`
+
+A user's frecently visited projects. Requires the `frecent_namespaces_suggestions` feature flag to be enabled.
+
+WARNING:
+**Introduced** in 16.6.
+This feature is an Experiment. It can be changed or removed at any time.
+
+Returns [`[Project!]`](#project).
+
### `Query.geoNode`
Find a Geo node.
@@ -505,6 +528,22 @@ This field returns a [connection](#connections). It accepts the
four standard [pagination arguments](#connection-pagination-arguments):
`before: String`, `after: String`, `first: Int`, `last: Int`.
+### `Query.memberRole`
+
+Finds a single custom role.
+
+WARNING:
+**Introduced** in 16.6.
+This feature is an Experiment. It can be changed or removed at any time.
+
+Returns [`MemberRole`](#memberrole).
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="querymemberroleid"></a>`id` | [`MemberRoleID`](#memberroleid) | Global ID of the member role to look up. |
+
### `Query.memberRolePermissions`
List of all customizable permissions.
@@ -627,6 +666,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| Name | Type | Description |
| ---- | ---- | ----------- |
+| <a id="queryprojectsfullpaths"></a>`fullPaths` | [`[String!]`](#string) | Filter projects by full paths. You cannot provide more than 50 full paths. |
| <a id="queryprojectsids"></a>`ids` | [`[ID!]`](#id) | Filter projects by IDs. |
| <a id="queryprojectsmembership"></a>`membership` | [`Boolean`](#boolean) | Return only projects that the current user is a member of. |
| <a id="queryprojectssearch"></a>`search` | [`String`](#string) | Search query, which can be for the project name, a path, or a description. |
@@ -702,6 +742,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="queryrunnersactive"></a>`active` **{warning-solid}** | [`Boolean`](#boolean) | **Deprecated** in 14.8. This was renamed. Use: `paused`. |
+| <a id="queryrunnerscreatorid"></a>`creatorId` | [`UserID`](#userid) | Filter runners by creator ID. |
| <a id="queryrunnerspaused"></a>`paused` | [`Boolean`](#boolean) | Filter runners by `paused` (true) or `active` (false) status. |
| <a id="queryrunnerssearch"></a>`search` | [`String`](#string) | Filter by full token or partial text in description field. |
| <a id="queryrunnerssort"></a>`sort` | [`CiRunnerSort`](#cirunnersort) | Sort order of results. |
@@ -709,6 +750,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| <a id="queryrunnerstaglist"></a>`tagList` | [`[String!]`](#string) | Filter by tags associated with the runner (comma-separated or array). |
| <a id="queryrunnerstype"></a>`type` | [`CiRunnerType`](#cirunnertype) | Filter runners by type. |
| <a id="queryrunnersupgradestatus"></a>`upgradeStatus` | [`CiRunnerUpgradeStatus`](#cirunnerupgradestatus) | Filter by upgrade status. |
+| <a id="queryrunnersversionprefix"></a>`versionPrefix` **{warning-solid}** | [`String`](#string) | **Introduced** in 16.6. This feature is an Experiment. It can be changed or removed at any time. Filter runners by version. Runners that contain runner managers with the version at the start of the search term are returned. For example, the search term '14.' returns runner managers with versions '14.11.1' and '14.2.3'. |
### `Query.snippets`
@@ -1218,6 +1260,7 @@ Input type: `AiActionInput`
| <a id="mutationaiactiongeneratecommitmessage"></a>`generateCommitMessage` | [`AiGenerateCommitMessageInput`](#aigeneratecommitmessageinput) | Input for generate_commit_message AI action. |
| <a id="mutationaiactiongeneratedescription"></a>`generateDescription` | [`AiGenerateDescriptionInput`](#aigeneratedescriptioninput) | Input for generate_description AI action. |
| <a id="mutationaiactiongeneratetestfile"></a>`generateTestFile` | [`GenerateTestFileInput`](#generatetestfileinput) | Input for generate_test_file AI action. |
+| <a id="mutationaiactionresolvevulnerability"></a>`resolveVulnerability` | [`AiResolveVulnerabilityInput`](#airesolvevulnerabilityinput) | Input for resolve_vulnerability AI action. |
| <a id="mutationaiactionsummarizecomments"></a>`summarizeComments` | [`AiSummarizeCommentsInput`](#aisummarizecommentsinput) | Input for summarize_comments AI action. |
| <a id="mutationaiactionsummarizereview"></a>`summarizeReview` | [`AiSummarizeReviewInput`](#aisummarizereviewinput) | Input for summarize_review AI action. |
| <a id="mutationaiactiontanukibot"></a>`tanukiBot` | [`AiTanukiBotInput`](#aitanukibotinput) | Input for tanuki_bot AI action. |
@@ -1276,94 +1319,112 @@ Input type: `AlertTodoCreateInput`
| <a id="mutationalerttodocreateissue"></a>`issue` | [`Issue`](#issue) | Issue created after mutation. |
| <a id="mutationalerttodocreatetodo"></a>`todo` | [`Todo`](#todo) | To-do item after mutation. |
-### `Mutation.amazonS3ConfigurationCreate`
+### `Mutation.approveDeployment`
-Input type: `AmazonS3ConfigurationCreateInput`
+Input type: `ApproveDeploymentInput`
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
-| <a id="mutationamazons3configurationcreateaccesskeyxid"></a>`accessKeyXid` | [`String!`](#string) | Access key ID of the Amazon S3 account. |
-| <a id="mutationamazons3configurationcreateawsregion"></a>`awsRegion` | [`String!`](#string) | AWS region where the bucket is created. |
-| <a id="mutationamazons3configurationcreatebucketname"></a>`bucketName` | [`String!`](#string) | Name of the bucket where the audit events would be logged. |
-| <a id="mutationamazons3configurationcreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
-| <a id="mutationamazons3configurationcreategrouppath"></a>`groupPath` | [`ID!`](#id) | Group path. |
-| <a id="mutationamazons3configurationcreatename"></a>`name` | [`String`](#string) | Destination name. |
-| <a id="mutationamazons3configurationcreatesecretaccesskey"></a>`secretAccessKey` | [`String!`](#string) | Secret access key of the Amazon S3 account. |
+| <a id="mutationapprovedeploymentclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationapprovedeploymentcomment"></a>`comment` | [`String`](#string) | Comment to go with the approval. |
+| <a id="mutationapprovedeploymentid"></a>`id` | [`DeploymentID!`](#deploymentid) | ID of the deployment. |
+| <a id="mutationapprovedeploymentrepresentedas"></a>`representedAs` | [`String`](#string) | Name of the User/Group/Role to use for the approval, when the user belongs to multiple approval rules. |
+| <a id="mutationapprovedeploymentstatus"></a>`status` | [`DeploymentsApprovalStatus!`](#deploymentsapprovalstatus) | Status of the approval (either `APPROVED` or `REJECTED`). |
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
-| <a id="mutationamazons3configurationcreateamazons3configuration"></a>`amazonS3Configuration` | [`AmazonS3ConfigurationType`](#amazons3configurationtype) | configuration created. |
-| <a id="mutationamazons3configurationcreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
-| <a id="mutationamazons3configurationcreateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+| <a id="mutationapprovedeploymentclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationapprovedeploymentdeploymentapproval"></a>`deploymentApproval` | [`DeploymentApproval!`](#deploymentapproval) | DeploymentApproval after mutation. |
+| <a id="mutationapprovedeploymenterrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
-### `Mutation.amazonS3ConfigurationUpdate`
+### `Mutation.artifactDestroy`
-Input type: `AmazonS3ConfigurationUpdateInput`
+Input type: `ArtifactDestroyInput`
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
-| <a id="mutationamazons3configurationupdateaccesskeyxid"></a>`accessKeyXid` | [`String`](#string) | Access key ID of the Amazon S3 account. |
-| <a id="mutationamazons3configurationupdateawsregion"></a>`awsRegion` | [`String`](#string) | AWS region where the bucket is created. |
-| <a id="mutationamazons3configurationupdatebucketname"></a>`bucketName` | [`String`](#string) | Name of the bucket where the audit events would be logged. |
-| <a id="mutationamazons3configurationupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
-| <a id="mutationamazons3configurationupdateid"></a>`id` | [`AuditEventsAmazonS3ConfigurationID!`](#auditeventsamazons3configurationid) | ID of the Amazon S3 configuration to update. |
-| <a id="mutationamazons3configurationupdatename"></a>`name` | [`String`](#string) | Destination name. |
-| <a id="mutationamazons3configurationupdatesecretaccesskey"></a>`secretAccessKey` | [`String`](#string) | Secret access key of the Amazon S3 account. |
+| <a id="mutationartifactdestroyclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationartifactdestroyid"></a>`id` | [`CiJobArtifactID!`](#cijobartifactid) | ID of the artifact to delete. |
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
-| <a id="mutationamazons3configurationupdateamazons3configuration"></a>`amazonS3Configuration` | [`AmazonS3ConfigurationType`](#amazons3configurationtype) | Updated Amazon S3 configuration. |
-| <a id="mutationamazons3configurationupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
-| <a id="mutationamazons3configurationupdateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+| <a id="mutationartifactdestroyartifact"></a>`artifact` | [`CiJobArtifact`](#cijobartifact) | Deleted artifact. |
+| <a id="mutationartifactdestroyclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationartifactdestroyerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
-### `Mutation.approveDeployment`
+### `Mutation.auditEventsAmazonS3ConfigurationCreate`
-Input type: `ApproveDeploymentInput`
+Input type: `AuditEventsAmazonS3ConfigurationCreateInput`
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
-| <a id="mutationapprovedeploymentclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
-| <a id="mutationapprovedeploymentcomment"></a>`comment` | [`String`](#string) | Comment to go with the approval. |
-| <a id="mutationapprovedeploymentid"></a>`id` | [`DeploymentID!`](#deploymentid) | ID of the deployment. |
-| <a id="mutationapprovedeploymentrepresentedas"></a>`representedAs` | [`String`](#string) | Name of the User/Group/Role to use for the approval, when the user belongs to multiple approval rules. |
-| <a id="mutationapprovedeploymentstatus"></a>`status` | [`DeploymentsApprovalStatus!`](#deploymentsapprovalstatus) | Status of the approval (either `APPROVED` or `REJECTED`). |
+| <a id="mutationauditeventsamazons3configurationcreateaccesskeyxid"></a>`accessKeyXid` | [`String!`](#string) | Access key ID of the Amazon S3 account. |
+| <a id="mutationauditeventsamazons3configurationcreateawsregion"></a>`awsRegion` | [`String!`](#string) | AWS region where the bucket is created. |
+| <a id="mutationauditeventsamazons3configurationcreatebucketname"></a>`bucketName` | [`String!`](#string) | Name of the bucket where the audit events would be logged. |
+| <a id="mutationauditeventsamazons3configurationcreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationauditeventsamazons3configurationcreategrouppath"></a>`groupPath` | [`ID!`](#id) | Group path. |
+| <a id="mutationauditeventsamazons3configurationcreatename"></a>`name` | [`String`](#string) | Destination name. |
+| <a id="mutationauditeventsamazons3configurationcreatesecretaccesskey"></a>`secretAccessKey` | [`String!`](#string) | Secret access key of the Amazon S3 account. |
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
-| <a id="mutationapprovedeploymentclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
-| <a id="mutationapprovedeploymentdeploymentapproval"></a>`deploymentApproval` | [`DeploymentApproval!`](#deploymentapproval) | DeploymentApproval after mutation. |
-| <a id="mutationapprovedeploymenterrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+| <a id="mutationauditeventsamazons3configurationcreateamazons3configuration"></a>`amazonS3Configuration` | [`AmazonS3ConfigurationType`](#amazons3configurationtype) | configuration created. |
+| <a id="mutationauditeventsamazons3configurationcreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationauditeventsamazons3configurationcreateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
-### `Mutation.artifactDestroy`
+### `Mutation.auditEventsAmazonS3ConfigurationDelete`
-Input type: `ArtifactDestroyInput`
+Input type: `AuditEventsAmazonS3ConfigurationDeleteInput`
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
-| <a id="mutationartifactdestroyclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
-| <a id="mutationartifactdestroyid"></a>`id` | [`CiJobArtifactID!`](#cijobartifactid) | ID of the artifact to delete. |
+| <a id="mutationauditeventsamazons3configurationdeleteclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationauditeventsamazons3configurationdeleteid"></a>`id` | [`AuditEventsAmazonS3ConfigurationID!`](#auditeventsamazons3configurationid) | ID of the Amazon S3 configuration to destroy. |
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
-| <a id="mutationartifactdestroyartifact"></a>`artifact` | [`CiJobArtifact`](#cijobartifact) | Deleted artifact. |
-| <a id="mutationartifactdestroyclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
-| <a id="mutationartifactdestroyerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+| <a id="mutationauditeventsamazons3configurationdeleteclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationauditeventsamazons3configurationdeleteerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+
+### `Mutation.auditEventsAmazonS3ConfigurationUpdate`
+
+Input type: `AuditEventsAmazonS3ConfigurationUpdateInput`
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationauditeventsamazons3configurationupdateaccesskeyxid"></a>`accessKeyXid` | [`String`](#string) | Access key ID of the Amazon S3 account. |
+| <a id="mutationauditeventsamazons3configurationupdateawsregion"></a>`awsRegion` | [`String`](#string) | AWS region where the bucket is created. |
+| <a id="mutationauditeventsamazons3configurationupdatebucketname"></a>`bucketName` | [`String`](#string) | Name of the bucket where the audit events would be logged. |
+| <a id="mutationauditeventsamazons3configurationupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationauditeventsamazons3configurationupdateid"></a>`id` | [`AuditEventsAmazonS3ConfigurationID!`](#auditeventsamazons3configurationid) | ID of the Amazon S3 configuration to update. |
+| <a id="mutationauditeventsamazons3configurationupdatename"></a>`name` | [`String`](#string) | Destination name. |
+| <a id="mutationauditeventsamazons3configurationupdatesecretaccesskey"></a>`secretAccessKey` | [`String`](#string) | Secret access key of the Amazon S3 account. |
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationauditeventsamazons3configurationupdateamazons3configuration"></a>`amazonS3Configuration` | [`AmazonS3ConfigurationType`](#amazons3configurationtype) | Updated Amazon S3 configuration. |
+| <a id="mutationauditeventsamazons3configurationupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationauditeventsamazons3configurationupdateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
### `Mutation.auditEventsStreamingDestinationEventsAdd`
@@ -1505,6 +1566,27 @@ Input type: `AuditEventsStreamingHeadersUpdateInput`
| <a id="mutationauditeventsstreamingheadersupdateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationauditeventsstreamingheadersupdateheader"></a>`header` | [`AuditEventStreamingHeader`](#auditeventstreamingheader) | Updates header. |
+### `Mutation.auditEventsStreamingHttpNamespaceFiltersAdd`
+
+Input type: `AuditEventsStreamingHTTPNamespaceFiltersAddInput`
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationauditeventsstreaminghttpnamespacefiltersaddclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationauditeventsstreaminghttpnamespacefiltersadddestinationid"></a>`destinationId` | [`AuditEventsExternalAuditEventDestinationID!`](#auditeventsexternalauditeventdestinationid) | Destination ID. |
+| <a id="mutationauditeventsstreaminghttpnamespacefiltersaddgrouppath"></a>`groupPath` | [`ID`](#id) | Full path of the group. |
+| <a id="mutationauditeventsstreaminghttpnamespacefiltersaddprojectpath"></a>`projectPath` | [`ID`](#id) | Full path of the project. |
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationauditeventsstreaminghttpnamespacefiltersaddclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationauditeventsstreaminghttpnamespacefiltersadderrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+| <a id="mutationauditeventsstreaminghttpnamespacefiltersaddnamespacefilter"></a>`namespaceFilter` | [`AuditEventStreamingHTTPNamespaceFilter`](#auditeventstreaminghttpnamespacefilter) | Namespace filter created. |
+
### `Mutation.auditEventsStreamingInstanceHeadersCreate`
Input type: `AuditEventsStreamingInstanceHeadersCreateInput`
@@ -1792,6 +1874,28 @@ Input type: `BulkRunnerDeleteInput`
| <a id="mutationbulkrunnerdeletedeletedids"></a>`deletedIds` | [`[CiRunnerID!]`](#cirunnerid) | IDs of records effectively deleted. Only present if operation was performed synchronously. |
| <a id="mutationbulkrunnerdeleteerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+### `Mutation.catalogResourceUnpublish`
+
+WARNING:
+**Introduced** in 16.6.
+This feature is an Experiment. It can be changed or removed at any time.
+
+Input type: `CatalogResourceUnpublishInput`
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationcatalogresourceunpublishclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationcatalogresourceunpublishid"></a>`id` | [`CiCatalogResourceID!`](#cicatalogresourceid) | Global ID of the catalog resource to unpublish. |
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationcatalogresourceunpublishclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationcatalogresourceunpublisherrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+
### `Mutation.catalogResourcesCreate`
WARNING:
@@ -2246,6 +2350,34 @@ Input type: `CreateComplianceFrameworkInput`
| <a id="mutationcreatecomplianceframeworkerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationcreatecomplianceframeworkframework"></a>`framework` | [`ComplianceFramework`](#complianceframework) | Created compliance framework. |
+### `Mutation.createContainerRegistryProtectionRule`
+
+Creates a protection rule to restrict access to a project's container registry. Available only when feature flag `container_registry_protected_containers` is enabled.
+
+WARNING:
+**Introduced** in 16.6.
+This feature is an Experiment. It can be changed or removed at any time.
+
+Input type: `CreateContainerRegistryProtectionRuleInput`
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationcreatecontainerregistryprotectionruleclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationcreatecontainerregistryprotectionrulecontainerpathpattern"></a>`containerPathPattern` | [`String!`](#string) | ContainerRegistryname protected by the protection rule. For example `@my-scope/my-container-*`. Wildcard character `*` allowed. |
+| <a id="mutationcreatecontainerregistryprotectionruledeleteprotecteduptoaccesslevel"></a>`deleteProtectedUpToAccessLevel` | [`ContainerRegistryProtectionRuleAccessLevel!`](#containerregistryprotectionruleaccesslevel) | Max GitLab access level to prevent from deleting container images in the container registry. For example `DEVELOPER`, `MAINTAINER`, `OWNER`. |
+| <a id="mutationcreatecontainerregistryprotectionruleprojectpath"></a>`projectPath` | [`ID!`](#id) | Full path of the project where a protection rule is located. |
+| <a id="mutationcreatecontainerregistryprotectionrulepushprotecteduptoaccesslevel"></a>`pushProtectedUpToAccessLevel` | [`ContainerRegistryProtectionRuleAccessLevel!`](#containerregistryprotectionruleaccesslevel) | Max GitLab access level to prevent from pushing container images to the container registry. For example `DEVELOPER`, `MAINTAINER`, `OWNER`. |
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationcreatecontainerregistryprotectionruleclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationcreatecontainerregistryprotectionrulecontainerregistryprotectionrule"></a>`containerRegistryProtectionRule` | [`ContainerRegistryProtectionRule`](#containerregistryprotectionrule) | Container registry protection rule after mutation. |
+| <a id="mutationcreatecontainerregistryprotectionruleerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+
### `Mutation.createCustomEmoji`
WARNING:
@@ -2990,6 +3122,31 @@ Input type: `DeleteAnnotationInput`
| <a id="mutationdeleteannotationclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationdeleteannotationerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+### `Mutation.deletePackagesProtectionRule`
+
+Deletes a protection rule for packages. Available only when feature flag `packages_protected_packages` is enabled.
+
+WARNING:
+**Introduced** in 16.6.
+This feature is an Experiment. It can be changed or removed at any time.
+
+Input type: `DeletePackagesProtectionRuleInput`
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationdeletepackagesprotectionruleclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationdeletepackagesprotectionruleid"></a>`id` | [`PackagesProtectionRuleID!`](#packagesprotectionruleid) | Global ID of the package protection rule to delete. |
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationdeletepackagesprotectionruleclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationdeletepackagesprotectionruleerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+| <a id="mutationdeletepackagesprotectionrulepackageprotectionrule"></a>`packageProtectionRule` | [`PackagesProtectionRule`](#packagesprotectionrule) | Packages protection rule that was deleted successfully. |
+
### `Mutation.designManagementDelete`
Input type: `DesignManagementDeleteInput`
@@ -3856,7 +4013,7 @@ Input type: `ExternalAuditEventDestinationUpdateInput`
### `Mutation.geoRegistriesBulkUpdate`
-Mutates multiple Geo registries for a given registry class. Does not mutate the registries if `geo_registries_update_mutation` feature flag is disabled.
+Mutates multiple Geo registries for a given registry class.
WARNING:
**Introduced** in 16.4.
@@ -3882,7 +4039,7 @@ Input type: `GeoRegistriesBulkUpdateInput`
### `Mutation.geoRegistriesUpdate`
-Mutates a Geo registry. Does not mutate the registry entry if `geo_registries_update_mutation` feature flag is disabled.
+Mutates a Geo registry.
WARNING:
**Introduced** in 16.1.
@@ -4962,6 +5119,40 @@ Input type: `MarkAsSpamSnippetInput`
| <a id="mutationmarkasspamsnippeterrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationmarkasspamsnippetsnippet"></a>`snippet` | [`Snippet`](#snippet) | Snippet after mutation. |
+### `Mutation.memberRoleCreate`
+
+WARNING:
+**Introduced** in 16.5.
+This feature is an Experiment. It can be changed or removed at any time.
+
+Input type: `MemberRoleCreateInput`
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationmemberrolecreateadmingroupmember"></a>`adminGroupMember` | [`Boolean`](#boolean) | Permission to admin group members. |
+| <a id="mutationmemberrolecreateadminmergerequest"></a>`adminMergeRequest` | [`Boolean`](#boolean) | Permission to admin merge requests. |
+| <a id="mutationmemberrolecreateadminvulnerability"></a>`adminVulnerability` | [`Boolean`](#boolean) | Permission to admin vulnerability. |
+| <a id="mutationmemberrolecreatearchiveproject"></a>`archiveProject` | [`Boolean`](#boolean) | Permission to archive projects. |
+| <a id="mutationmemberrolecreatebaseaccesslevel"></a>`baseAccessLevel` | [`MemberAccessLevel!`](#memberaccesslevel) | Base access level for the custom role. |
+| <a id="mutationmemberrolecreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationmemberrolecreatedescription"></a>`description` | [`String`](#string) | Description of the member role. |
+| <a id="mutationmemberrolecreategrouppath"></a>`groupPath` | [`ID!`](#id) | Group the member role to mutate is in. |
+| <a id="mutationmemberrolecreatemanageprojectaccesstokens"></a>`manageProjectAccessTokens` | [`Boolean`](#boolean) | Permission to admin project access tokens. |
+| <a id="mutationmemberrolecreatename"></a>`name` | [`String`](#string) | Name of the member role. |
+| <a id="mutationmemberrolecreatereadcode"></a>`readCode` | [`Boolean`](#boolean) | Permission to read code. |
+| <a id="mutationmemberrolecreatereaddependency"></a>`readDependency` | [`Boolean`](#boolean) | Permission to read dependency. |
+| <a id="mutationmemberrolecreatereadvulnerability"></a>`readVulnerability` | [`Boolean`](#boolean) | Permission to read vulnerability. |
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationmemberrolecreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationmemberrolecreateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+| <a id="mutationmemberrolecreatememberrole"></a>`memberRole` | [`MemberRole`](#memberrole) | Updated member role. |
+
### `Mutation.memberRoleUpdate`
Input type: `MemberRoleUpdateInput`
@@ -4989,6 +5180,10 @@ Accepts a merge request.
When accepted, the source branch will be scheduled to merge into the target branch, either
immediately if possible, or using one of the automatic merge strategies.
+[In GitLab 16.5](https://gitlab.com/gitlab-org/gitlab/-/issues/421510), the merging happens asynchronously.
+This results in `mergeRequest` and `state` not updating after a mutation request,
+because the merging may not have happened yet.
+
Input type: `MergeRequestAcceptInput`
#### Arguments
@@ -5456,6 +5651,30 @@ Input type: `OncallScheduleUpdateInput`
| <a id="mutationoncallscheduleupdateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationoncallscheduleupdateoncallschedule"></a>`oncallSchedule` | [`IncidentManagementOncallSchedule`](#incidentmanagementoncallschedule) | On-call schedule. |
+### `Mutation.organizationCreate`
+
+WARNING:
+**Introduced** in 16.6.
+This feature is an Experiment. It can be changed or removed at any time.
+
+Input type: `OrganizationCreateInput`
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationorganizationcreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationorganizationcreatename"></a>`name` | [`String!`](#string) | Name for the organization. |
+| <a id="mutationorganizationcreatepath"></a>`path` | [`String!`](#string) | Path for the organization. |
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationorganizationcreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationorganizationcreateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+| <a id="mutationorganizationcreateorganization"></a>`organization` | [`Organization`](#organization) | Organization created. |
+
### `Mutation.pagesMarkOnboardingComplete`
Input type: `PagesMarkOnboardingCompleteInput`
@@ -5839,6 +6058,45 @@ Input type: `ProjectSetLockedInput`
| <a id="mutationprojectsetlockederrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationprojectsetlockedproject"></a>`project` | [`Project`](#project) | Project after mutation. |
+### `Mutation.projectSubscriptionCreate`
+
+Input type: `ProjectSubscriptionCreateInput`
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationprojectsubscriptioncreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationprojectsubscriptioncreateprojectpath"></a>`projectPath` | [`String!`](#string) | Full path of the downstream project of the Project Subscription. |
+| <a id="mutationprojectsubscriptioncreateupstreampath"></a>`upstreamPath` | [`String!`](#string) | Full path of the upstream project of the Project Subscription. |
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationprojectsubscriptioncreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationprojectsubscriptioncreateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+| <a id="mutationprojectsubscriptioncreatesubscription"></a>`subscription` | [`CiSubscriptionsProject`](#cisubscriptionsproject) | Project Subscription created by the mutation. |
+
+### `Mutation.projectSubscriptionDelete`
+
+Input type: `ProjectSubscriptionDeleteInput`
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationprojectsubscriptiondeleteclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationprojectsubscriptiondeletesubscriptionid"></a>`subscriptionId` | [`CiSubscriptionsProjectID!`](#cisubscriptionsprojectid) | ID of the subscription to delete. |
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationprojectsubscriptiondeleteclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationprojectsubscriptiondeleteerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+| <a id="mutationprojectsubscriptiondeleteproject"></a>`project` | [`Project`](#project) | Project after mutation. |
+
### `Mutation.projectSyncFork`
WARNING:
@@ -6415,7 +6673,7 @@ Input type: `SecurityPolicyProjectAssignInput`
### `Mutation.securityPolicyProjectCreate`
-Creates and assigns a security policy project for the given project (`full_path`).
+Creates and assigns a security policy project for the given project or group (`full_path`).
Input type: `SecurityPolicyProjectCreateInput`
@@ -7156,8 +7414,8 @@ Input type: `UpdateNamespacePackageSettingsInput`
| <a id="mutationupdatenamespacepackagesettingsmavenpackagerequestsforwarding"></a>`mavenPackageRequestsForwarding` | [`Boolean`](#boolean) | Indicates whether Maven package forwarding is allowed for this namespace. |
| <a id="mutationupdatenamespacepackagesettingsnamespacepath"></a>`namespacePath` | [`ID!`](#id) | Namespace path where the namespace package setting is located. |
| <a id="mutationupdatenamespacepackagesettingsnpmpackagerequestsforwarding"></a>`npmPackageRequestsForwarding` | [`Boolean`](#boolean) | Indicates whether npm package forwarding is allowed for this namespace. |
-| <a id="mutationupdatenamespacepackagesettingsnugetduplicateexceptionregex"></a>`nugetDuplicateExceptionRegex` | [`UntrustedRegexp`](#untrustedregexp) | When nuget_duplicates_allowed is false, you can publish duplicate packages with names that match this regex. Otherwise, this setting has no effect. Error is raised if `nuget_duplicates_option` feature flag is disabled. |
-| <a id="mutationupdatenamespacepackagesettingsnugetduplicatesallowed"></a>`nugetDuplicatesAllowed` | [`Boolean`](#boolean) | Indicates whether duplicate NuGet packages are allowed for this namespace. Error is raised if `nuget_duplicates_option` feature flag is disabled. |
+| <a id="mutationupdatenamespacepackagesettingsnugetduplicateexceptionregex"></a>`nugetDuplicateExceptionRegex` | [`UntrustedRegexp`](#untrustedregexp) | When nuget_duplicates_allowed is false, you can publish duplicate packages with names that match this regex. Otherwise, this setting has no effect. |
+| <a id="mutationupdatenamespacepackagesettingsnugetduplicatesallowed"></a>`nugetDuplicatesAllowed` | [`Boolean`](#boolean) | Indicates whether duplicate NuGet packages are allowed for this namespace. |
| <a id="mutationupdatenamespacepackagesettingspypipackagerequestsforwarding"></a>`pypiPackageRequestsForwarding` | [`Boolean`](#boolean) | Indicates whether PyPI package forwarding is allowed for this namespace. |
#### Fields
@@ -7441,6 +7699,83 @@ Input type: `UserSetNamespaceCommitEmailInput`
| <a id="mutationusersetnamespacecommitemailerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationusersetnamespacecommitemailnamespacecommitemail"></a>`namespaceCommitEmail` | [`NamespaceCommitEmail`](#namespacecommitemail) | User namespace commit email after mutation. |
+### `Mutation.valueStreamCreate`
+
+Creates a value stream.
+
+WARNING:
+**Introduced** in 16.6.
+This feature is an Experiment. It can be changed or removed at any time.
+
+Input type: `ValueStreamCreateInput`
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationvaluestreamcreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationvaluestreamcreatename"></a>`name` | [`String!`](#string) | Value stream name. |
+| <a id="mutationvaluestreamcreatenamespacepath"></a>`namespacePath` | [`ID!`](#id) | Full path of the namespace(project or group) the value stream is created in. |
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationvaluestreamcreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationvaluestreamcreateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+| <a id="mutationvaluestreamcreatevaluestream"></a>`valueStream` | [`ValueStream`](#valuestream) | Created value stream. |
+
+### `Mutation.valueStreamDestroy`
+
+Destroy a value stream.
+
+WARNING:
+**Introduced** in 16.6.
+This feature is an Experiment. It can be changed or removed at any time.
+
+Input type: `ValueStreamDestroyInput`
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationvaluestreamdestroyclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationvaluestreamdestroyid"></a>`id` | [`AnalyticsCycleAnalyticsValueStreamID!`](#analyticscycleanalyticsvaluestreamid) | Global ID of the value stream to destroy. |
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationvaluestreamdestroyclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationvaluestreamdestroyerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+| <a id="mutationvaluestreamdestroyvaluestream"></a>`valueStream` | [`ValueStream`](#valuestream) | Value stream deleted after mutation. |
+
+### `Mutation.valueStreamUpdate`
+
+Updates a value stream.
+
+WARNING:
+**Introduced** in 16.6.
+This feature is an Experiment. It can be changed or removed at any time.
+
+Input type: `ValueStreamUpdateInput`
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationvaluestreamupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationvaluestreamupdateid"></a>`id` | [`AnalyticsCycleAnalyticsValueStreamID!`](#analyticscycleanalyticsvaluestreamid) | Global ID of the value stream to update. |
+| <a id="mutationvaluestreamupdatename"></a>`name` | [`String!`](#string) | Value stream name. |
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationvaluestreamupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationvaluestreamupdateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+| <a id="mutationvaluestreamupdatevaluestream"></a>`valueStream` | [`ValueStream`](#valuestream) | Updated value stream. |
+
### `Mutation.vulnerabilitiesDismiss`
Input type: `VulnerabilitiesDismissInput`
@@ -10749,6 +11084,29 @@ The edge type for [`MemberInterface`](#memberinterface).
| <a id="memberinterfaceedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="memberinterfaceedgenode"></a>`node` | [`MemberInterface`](#memberinterface) | The item at the end of the edge. |
+#### `MemberRoleConnection`
+
+The connection type for [`MemberRole`](#memberrole).
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="memberroleconnectionedges"></a>`edges` | [`[MemberRoleEdge]`](#memberroleedge) | A list of edges. |
+| <a id="memberroleconnectionnodes"></a>`nodes` | [`[MemberRole]`](#memberrole) | A list of nodes. |
+| <a id="memberroleconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
+
+#### `MemberRoleEdge`
+
+The edge type for [`MemberRole`](#memberrole).
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="memberroleedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
+| <a id="memberroleedgenode"></a>`node` | [`MemberRole`](#memberrole) | The item at the end of the edge. |
+
#### `MergeAccessLevelConnection`
The connection type for [`MergeAccessLevel`](#mergeaccesslevel).
@@ -11123,6 +11481,29 @@ The edge type for [`OncallParticipantType`](#oncallparticipanttype).
| <a id="oncallparticipanttypeedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="oncallparticipanttypeedgenode"></a>`node` | [`OncallParticipantType`](#oncallparticipanttype) | The item at the end of the edge. |
+#### `OrganizationConnection`
+
+The connection type for [`Organization`](#organization).
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="organizationconnectionedges"></a>`edges` | [`[OrganizationEdge]`](#organizationedge) | A list of edges. |
+| <a id="organizationconnectionnodes"></a>`nodes` | [`[Organization]`](#organization) | A list of nodes. |
+| <a id="organizationconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
+
+#### `OrganizationEdge`
+
+The edge type for [`Organization`](#organization).
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="organizationedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
+| <a id="organizationedgenode"></a>`node` | [`Organization`](#organization) | The item at the end of the edge. |
+
#### `OrganizationUserConnection`
The connection type for [`OrganizationUser`](#organizationuser).
@@ -11355,6 +11736,29 @@ The edge type for [`PathLock`](#pathlock).
| <a id="pathlockedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="pathlockedgenode"></a>`node` | [`PathLock`](#pathlock) | The item at the end of the edge. |
+#### `PendingGroupMemberConnection`
+
+The connection type for [`PendingGroupMember`](#pendinggroupmember).
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="pendinggroupmemberconnectionedges"></a>`edges` | [`[PendingGroupMemberEdge]`](#pendinggroupmemberedge) | A list of edges. |
+| <a id="pendinggroupmemberconnectionnodes"></a>`nodes` | [`[PendingGroupMember]`](#pendinggroupmember) | A list of nodes. |
+| <a id="pendinggroupmemberconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
+
+#### `PendingGroupMemberEdge`
+
+The edge type for [`PendingGroupMember`](#pendinggroupmember).
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="pendinggroupmemberedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
+| <a id="pendinggroupmemberedgenode"></a>`node` | [`PendingGroupMember`](#pendinggroupmember) | The item at the end of the edge. |
+
#### `PipelineArtifactRegistryConnection`
The connection type for [`PipelineArtifactRegistry`](#pipelineartifactregistry).
@@ -12931,7 +13335,38 @@ An abuse report.
| Name | Type | Description |
| ---- | ---- | ----------- |
+| <a id="abusereportcommenters"></a>`commenters` | [`UserCoreConnection!`](#usercoreconnection) | All commenters on this noteable. (see [Connections](#connections)) |
+| <a id="abusereportdiscussions"></a>`discussions` | [`DiscussionConnection!`](#discussionconnection) | All discussions on this noteable. (see [Connections](#connections)) |
+| <a id="abusereportid"></a>`id` | [`AbuseReportID!`](#abusereportid) | Global ID of the abuse report. |
| <a id="abusereportlabels"></a>`labels` | [`LabelConnection`](#labelconnection) | Labels of the abuse report. (see [Connections](#connections)) |
+| <a id="abusereportuserpermissions"></a>`userPermissions` | [`AbuseReportPermissions!`](#abusereportpermissions) | Permissions for the current user on the resource. |
+
+#### Fields with arguments
+
+##### `AbuseReport.notes`
+
+All notes on this noteable.
+
+Returns [`NoteConnection!`](#noteconnection).
+
+This field returns a [connection](#connections). It accepts the
+four standard [pagination arguments](#connection-pagination-arguments):
+`before: String`, `after: String`, `first: Int`, `last: Int`.
+
+###### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="abusereportnotesfilter"></a>`filter` | [`NotesFilterType`](#notesfiltertype) | Type of notes collection: ALL_NOTES, ONLY_COMMENTS, ONLY_ACTIVITY. |
+
+### `AbuseReportPermissions`
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="abusereportpermissionscreatenote"></a>`createNote` | [`Boolean!`](#boolean) | If `true`, the user can perform `create_note` on this resource. |
+| <a id="abusereportpermissionsreadabusereport"></a>`readAbuseReport` | [`Boolean!`](#boolean) | If `true`, the user can perform `read_abuse_report` on this resource. |
### `AccessLevel`
@@ -13046,12 +13481,13 @@ A user with add-on data.
| <a id="addonusernamespace"></a>`namespace` | [`Namespace`](#namespace) | Personal namespace of the user. |
| <a id="addonusernamespacecommitemails"></a>`namespaceCommitEmails` | [`NamespaceCommitEmailConnection`](#namespacecommitemailconnection) | User's custom namespace commit emails. (see [Connections](#connections)) |
| <a id="addonuserorganization"></a>`organization` | [`String`](#string) | Who the user represents or works for. |
+| <a id="addonuserorganizations"></a>`organizations` **{warning-solid}** | [`OrganizationConnection`](#organizationconnection) | **Introduced** in 16.6. This feature is an Experiment. It can be changed or removed at any time. Organizations where the user has access. |
| <a id="addonuserpreferencesgitpodpath"></a>`preferencesGitpodPath` | [`String`](#string) | Web path to the Gitpod section within user preferences. |
| <a id="addonuserprofileenablegitpodpath"></a>`profileEnableGitpodPath` | [`String`](#string) | Web path to enable Gitpod for the user. |
| <a id="addonuserprojectmemberships"></a>`projectMemberships` | [`ProjectMemberConnection`](#projectmemberconnection) | Project memberships of the user. (see [Connections](#connections)) |
| <a id="addonuserpronouns"></a>`pronouns` | [`String`](#string) | Pronouns of the user. |
| <a id="addonuserpublicemail"></a>`publicEmail` | [`String`](#string) | User's public email. |
-| <a id="addonusersavedreplies"></a>`savedReplies` | [`SavedReplyConnection`](#savedreplyconnection) | Saved replies authored by the user. Will not return saved replies if `saved_replies` feature flag is disabled. (see [Connections](#connections)) |
+| <a id="addonusersavedreplies"></a>`savedReplies` | [`SavedReplyConnection`](#savedreplyconnection) | Saved replies authored by the user. (see [Connections](#connections)) |
| <a id="addonuserstate"></a>`state` | [`UserState!`](#userstate) | State of the user. |
| <a id="addonuserstatus"></a>`status` | [`UserStatus`](#userstatus) | User status. |
| <a id="addonusertwitter"></a>`twitter` | [`String`](#string) | Twitter username of the user. |
@@ -13210,7 +13646,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
##### `AddOnUser.savedReply`
-Saved reply authored by the user. Will not return saved reply if `saved_replies` feature flag is disabled.
+Saved reply authored by the user.
Returns [`SavedReply`](#savedreply).
@@ -13589,11 +14025,24 @@ Describes a rule for who can approve merge requests.
| <a id="approvalruleinvalid"></a>`invalid` | [`Boolean`](#boolean) | Indicates if the rule is invalid and cannot be approved. |
| <a id="approvalrulename"></a>`name` | [`String`](#string) | Name of the rule. |
| <a id="approvalruleoverridden"></a>`overridden` | [`Boolean`](#boolean) | Indicates if the rule was overridden for the merge request. |
+| <a id="approvalrulescanresultpolicies"></a>`scanResultPolicies` | [`[ApprovalScanResultPolicy!]`](#approvalscanresultpolicy) | List of scan result policies associated with the rule. |
| <a id="approvalrulesection"></a>`section` | [`String`](#string) | Named section of the Code Owners file that the rule applies to. |
| <a id="approvalrulesourcerule"></a>`sourceRule` | [`ApprovalRule`](#approvalrule) | Source rule used to create the rule. |
| <a id="approvalruletype"></a>`type` | [`ApprovalRuleType`](#approvalruletype) | Type of the rule. |
| <a id="approvalruleusers"></a>`users` | [`UserCoreConnection`](#usercoreconnection) | List of users added as approvers for the rule. (see [Connections](#connections)) |
+### `ApprovalScanResultPolicy`
+
+Represents the scan result policy.
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="approvalscanresultpolicyapprovalsrequired"></a>`approvalsRequired` | [`Int!`](#int) | Represents the required approvals defined in the policy. |
+| <a id="approvalscanresultpolicyname"></a>`name` | [`String!`](#string) | Represents the name of the policy. |
+| <a id="approvalscanresultpolicyreporttype"></a>`reportType` | [`ApprovalReportType!`](#approvalreporttype) | Represents the report_type of the approval rule. |
+
### `AssetType`
Represents a vulnerability asset type.
@@ -13623,6 +14072,18 @@ Represents the YAML definitions for audit events defined in `ee/config/audit_eve
| <a id="auditeventdefinitionsavedtodatabase"></a>`savedToDatabase` | [`Boolean!`](#boolean) | Indicates if the event is saved to PostgreSQL database. |
| <a id="auditeventdefinitionstreamed"></a>`streamed` | [`Boolean!`](#boolean) | Indicates if the event is streamed to an external destination. |
+### `AuditEventStreamingHTTPNamespaceFilter`
+
+Represents a subgroup or project filter that belongs to an external audit event streaming destination.
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="auditeventstreaminghttpnamespacefilterexternalauditeventdestination"></a>`externalAuditEventDestination` | [`ExternalAuditEventDestination!`](#externalauditeventdestination) | Destination to which the filter belongs. |
+| <a id="auditeventstreaminghttpnamespacefilterid"></a>`id` | [`ID!`](#id) | ID of the filter. |
+| <a id="auditeventstreaminghttpnamespacefilternamespace"></a>`namespace` | [`Namespace!`](#namespace) | Group or project namespace the filter belongs to. |
+
### `AuditEventStreamingHeader`
Represents a HTTP header key/value that belongs to an audit streaming destination.
@@ -13636,6 +14097,18 @@ Represents a HTTP header key/value that belongs to an audit streaming destinatio
| <a id="auditeventstreamingheaderkey"></a>`key` | [`String!`](#string) | Key of the header. |
| <a id="auditeventstreamingheadervalue"></a>`value` | [`String!`](#string) | Value of the header. |
+### `AuditEventsStreamingHTTPNamespaceFiltersAddPayload`
+
+Autogenerated return type of AuditEventsStreamingHTTPNamespaceFiltersAdd.
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="auditeventsstreaminghttpnamespacefiltersaddpayloadclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="auditeventsstreaminghttpnamespacefiltersaddpayloaderrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+| <a id="auditeventsstreaminghttpnamespacefiltersaddpayloadnamespacefilter"></a>`namespaceFilter` | [`AuditEventStreamingHTTPNamespaceFilter`](#auditeventstreaminghttpnamespacefilter) | Namespace filter created. |
+
### `AuditEventsStreamingInstanceHeader`
Represents a HTTP header key/value that belongs to an instance level audit streaming destination.
@@ -13672,18 +14145,20 @@ Core representation of a GitLab user.
| <a id="autocompleteduserid"></a>`id` | [`ID!`](#id) | ID of the user. |
| <a id="autocompleteduseride"></a>`ide` | [`Ide`](#ide) | IDE settings. |
| <a id="autocompleteduserjobtitle"></a>`jobTitle` | [`String`](#string) | Job title of the user. |
+| <a id="autocompleteduserlastactivityon"></a>`lastActivityOn` | [`Date`](#date) | Date the user last performed any actions. |
| <a id="autocompleteduserlinkedin"></a>`linkedin` | [`String`](#string) | LinkedIn profile name of the user. |
| <a id="autocompleteduserlocation"></a>`location` | [`String`](#string) | Location of the user. |
| <a id="autocompletedusername"></a>`name` | [`String!`](#string) | Human-readable name of the user. Returns `****` if the user is a project bot and the requester does not have permission to view the project. |
| <a id="autocompletedusernamespace"></a>`namespace` | [`Namespace`](#namespace) | Personal namespace of the user. |
| <a id="autocompletedusernamespacecommitemails"></a>`namespaceCommitEmails` | [`NamespaceCommitEmailConnection`](#namespacecommitemailconnection) | User's custom namespace commit emails. (see [Connections](#connections)) |
| <a id="autocompleteduserorganization"></a>`organization` | [`String`](#string) | Who the user represents or works for. |
+| <a id="autocompleteduserorganizations"></a>`organizations` **{warning-solid}** | [`OrganizationConnection`](#organizationconnection) | **Introduced** in 16.6. This feature is an Experiment. It can be changed or removed at any time. Organizations where the user has access. |
| <a id="autocompleteduserpreferencesgitpodpath"></a>`preferencesGitpodPath` | [`String`](#string) | Web path to the Gitpod section within user preferences. |
| <a id="autocompleteduserprofileenablegitpodpath"></a>`profileEnableGitpodPath` | [`String`](#string) | Web path to enable Gitpod for the user. |
| <a id="autocompleteduserprojectmemberships"></a>`projectMemberships` | [`ProjectMemberConnection`](#projectmemberconnection) | Project memberships of the user. (see [Connections](#connections)) |
| <a id="autocompleteduserpronouns"></a>`pronouns` | [`String`](#string) | Pronouns of the user. |
| <a id="autocompleteduserpublicemail"></a>`publicEmail` | [`String`](#string) | User's public email. |
-| <a id="autocompletedusersavedreplies"></a>`savedReplies` | [`SavedReplyConnection`](#savedreplyconnection) | Saved replies authored by the user. Will not return saved replies if `saved_replies` feature flag is disabled. (see [Connections](#connections)) |
+| <a id="autocompletedusersavedreplies"></a>`savedReplies` | [`SavedReplyConnection`](#savedreplyconnection) | Saved replies authored by the user. (see [Connections](#connections)) |
| <a id="autocompleteduserstate"></a>`state` | [`UserState!`](#userstate) | State of the user. |
| <a id="autocompleteduserstatus"></a>`status` | [`UserStatus`](#userstatus) | User status. |
| <a id="autocompletedusertwitter"></a>`twitter` | [`String`](#string) | Twitter username of the user. |
@@ -13834,7 +14309,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
##### `AutocompletedUser.savedReply`
-Saved reply authored by the user. Will not return saved reply if `saved_replies` feature flag is disabled.
+Saved reply authored by the user.
Returns [`SavedReply`](#savedreply).
@@ -14939,6 +15414,17 @@ Represents the Geo replication and verification state of a ci_secure_file.
| <a id="cistagename"></a>`name` | [`String`](#string) | Name of the stage. |
| <a id="cistagestatus"></a>`status` | [`String`](#string) | Status of the pipeline stage. |
+### `CiSubscriptionsProject`
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="cisubscriptionsprojectauthor"></a>`author` | [`UserCore`](#usercore) | Author of the subscription. |
+| <a id="cisubscriptionsprojectdownstreamproject"></a>`downstreamProject` | [`Project`](#project) | Downstream project of the subscription. |
+| <a id="cisubscriptionsprojectid"></a>`id` | [`CiSubscriptionsProjectID`](#cisubscriptionsprojectid) | Global ID of the subscription. |
+| <a id="cisubscriptionsprojectupstreamproject"></a>`upstreamProject` | [`Project`](#project) | Upstream project of the subscription. |
+
### `CiTemplate`
GitLab CI/CD configuration template.
@@ -15079,6 +15565,7 @@ Represents reports comparison for code quality.
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="codequalityreportscomparerreport"></a>`report` | [`CodequalityReportsComparerReport`](#codequalityreportscomparerreport) | Compared codequality report. |
+| <a id="codequalityreportscomparerstatus"></a>`status` | [`CodequalityReportsComparerReportGenerationStatus`](#codequalityreportscomparerreportgenerationstatus) | Compared codequality report generation status. |
### `CodequalityReportsComparerReport`
@@ -15091,7 +15578,7 @@ Represents compared code quality report.
| <a id="codequalityreportscomparerreportexistingerrors"></a>`existingErrors` | [`[CodequalityReportsComparerReportDegradation!]`](#codequalityreportscomparerreportdegradation) | All code quality degradations. |
| <a id="codequalityreportscomparerreportnewerrors"></a>`newErrors` | [`[CodequalityReportsComparerReportDegradation!]!`](#codequalityreportscomparerreportdegradation) | New code quality degradations. |
| <a id="codequalityreportscomparerreportresolvederrors"></a>`resolvedErrors` | [`[CodequalityReportsComparerReportDegradation!]`](#codequalityreportscomparerreportdegradation) | Resolved code quality degradations. |
-| <a id="codequalityreportscomparerreportstatus"></a>`status` | [`CodequalityReportsComparerReportStatus!`](#codequalityreportscomparerreportstatus) | Status of report. |
+| <a id="codequalityreportscomparerreportstatus"></a>`status` | [`CodequalityReportsComparerStatus!`](#codequalityreportscomparerstatus) | Status of report. |
| <a id="codequalityreportscomparerreportsummary"></a>`summary` | [`CodequalityReportsComparerReportSummary!`](#codequalityreportscomparerreportsummary) | Codequality report summary. |
### `CodequalityReportsComparerReportDegradation`
@@ -15412,6 +15899,19 @@ A tag expiration policy designed to keep only the images that matter most.
| <a id="containerexpirationpolicyolderthan"></a>`olderThan` | [`ContainerExpirationPolicyOlderThanEnum`](#containerexpirationpolicyolderthanenum) | Tags older that this will expire. |
| <a id="containerexpirationpolicyupdatedat"></a>`updatedAt` | [`Time!`](#time) | Timestamp of when the container expiration policy was updated. |
+### `ContainerRegistryProtectionRule`
+
+A container registry protection rule designed to prevent users with a certain access level or lower from altering the container registry.
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="containerregistryprotectionrulecontainerpathpattern"></a>`containerPathPattern` | [`String!`](#string) | Container repository path pattern protected by the protection rule. For example `@my-scope/my-container-*`. Wildcard character `*` allowed. |
+| <a id="containerregistryprotectionruledeleteprotecteduptoaccesslevel"></a>`deleteProtectedUpToAccessLevel` | [`ContainerRegistryProtectionRuleAccessLevel!`](#containerregistryprotectionruleaccesslevel) | Max GitLab access level to prevent from pushing container images to the container registry. For example `DEVELOPER`, `MAINTAINER`, `OWNER`. |
+| <a id="containerregistryprotectionruleid"></a>`id` | [`ContainerRegistryProtectionRuleID!`](#containerregistryprotectionruleid) | ID of the container registry protection rule. |
+| <a id="containerregistryprotectionrulepushprotecteduptoaccesslevel"></a>`pushProtectedUpToAccessLevel` | [`ContainerRegistryProtectionRuleAccessLevel!`](#containerregistryprotectionruleaccesslevel) | Max GitLab access level to prevent from pushing container images to the container registry. For example `DEVELOPER`, `MAINTAINER`, `OWNER`. |
+
### `ContainerRepository`
A container repository.
@@ -15595,9 +16095,9 @@ A custom emoji uploaded by user.
| Name | Type | Description |
| ---- | ---- | ----------- |
-| <a id="customemojipermissionscreatecustomemoji"></a>`createCustomEmoji` | [`Boolean!`](#boolean) | Indicates the user can perform `create_custom_emoji` on this resource. |
-| <a id="customemojipermissionsdeletecustomemoji"></a>`deleteCustomEmoji` | [`Boolean!`](#boolean) | Indicates the user can perform `delete_custom_emoji` on this resource. |
-| <a id="customemojipermissionsreadcustomemoji"></a>`readCustomEmoji` | [`Boolean!`](#boolean) | Indicates the user can perform `read_custom_emoji` on this resource. |
+| <a id="customemojipermissionscreatecustomemoji"></a>`createCustomEmoji` | [`Boolean!`](#boolean) | If `true`, the user can perform `create_custom_emoji` on this resource. |
+| <a id="customemojipermissionsdeletecustomemoji"></a>`deleteCustomEmoji` | [`Boolean!`](#boolean) | If `true`, the user can perform `delete_custom_emoji` on this resource. |
+| <a id="customemojipermissionsreadcustomemoji"></a>`readCustomEmoji` | [`Boolean!`](#boolean) | If `true`, the user can perform `read_custom_emoji` on this resource. |
### `CustomerRelationsContact`
@@ -15834,7 +16334,7 @@ Check permissions for the current user on site profile.
| Name | Type | Description |
| ---- | ---- | ----------- |
-| <a id="dastsiteprofilepermissionscreateondemanddastscan"></a>`createOnDemandDastScan` | [`Boolean!`](#boolean) | Indicates the user can perform `create_on_demand_dast_scan` on this resource. |
+| <a id="dastsiteprofilepermissionscreateondemanddastscan"></a>`createOnDemandDastScan` | [`Boolean!`](#boolean) | If `true`, the user can perform `create_on_demand_dast_scan` on this resource. |
### `DastSiteValidation`
@@ -16059,8 +16559,8 @@ Approval summary of the deployment.
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="deploymentpermissionsapprovedeployment"></a>`approveDeployment` | [`Boolean!`](#boolean) | Indicates the user can perform `approve_deployment` on this resource. This field can only be resolved for one environment in any single request. |
-| <a id="deploymentpermissionsdestroydeployment"></a>`destroyDeployment` | [`Boolean!`](#boolean) | Indicates the user can perform `destroy_deployment` on this resource. |
-| <a id="deploymentpermissionsupdatedeployment"></a>`updateDeployment` | [`Boolean!`](#boolean) | Indicates the user can perform `update_deployment` on this resource. |
+| <a id="deploymentpermissionsdestroydeployment"></a>`destroyDeployment` | [`Boolean!`](#boolean) | If `true`, the user can perform `destroy_deployment` on this resource. |
+| <a id="deploymentpermissionsupdatedeployment"></a>`updateDeployment` | [`Boolean!`](#boolean) | If `true`, the user can perform `update_deployment` on this resource. |
### `DeploymentTag`
@@ -16394,6 +16894,22 @@ four standard [pagination arguments](#connection-pagination-arguments):
| <a id="designversiondesignsatversionfilenames"></a>`filenames` | [`[String!]`](#string) | Filters designs by their filename. |
| <a id="designversiondesignsatversionids"></a>`ids` | [`[DesignManagementDesignID!]`](#designmanagementdesignid) | Filters designs by their ID. |
+### `DetailedImportStatus`
+
+Details of the import status of a project.
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="detailedimportstatusid"></a>`id` | [`ProjectImportStateID`](#projectimportstateid) | ID of the import state. |
+| <a id="detailedimportstatuslasterror"></a>`lastError` | [`String`](#string) | Last error of the import. |
+| <a id="detailedimportstatuslastsuccessfulupdateat"></a>`lastSuccessfulUpdateAt` | [`Time`](#time) | Time of the last successful update. |
+| <a id="detailedimportstatuslastupdateat"></a>`lastUpdateAt` | [`Time`](#time) | Time of the last update. |
+| <a id="detailedimportstatuslastupdatestartedat"></a>`lastUpdateStartedAt` | [`Time`](#time) | Time of the start of the last update. |
+| <a id="detailedimportstatusstatus"></a>`status` | [`String`](#string) | Current status of the import. |
+| <a id="detailedimportstatusurl"></a>`url` | [`String`](#string) | Import url. |
+
### `DetailedStatus`
#### Fields
@@ -16692,9 +17208,9 @@ Returns [`Deployment`](#deployment).
| Name | Type | Description |
| ---- | ---- | ----------- |
-| <a id="environmentpermissionsdestroyenvironment"></a>`destroyEnvironment` | [`Boolean!`](#boolean) | Indicates the user can perform `destroy_environment` on this resource. |
-| <a id="environmentpermissionsstopenvironment"></a>`stopEnvironment` | [`Boolean!`](#boolean) | Indicates the user can perform `stop_environment` on this resource. |
-| <a id="environmentpermissionsupdateenvironment"></a>`updateEnvironment` | [`Boolean!`](#boolean) | Indicates the user can perform `update_environment` on this resource. |
+| <a id="environmentpermissionsdestroyenvironment"></a>`destroyEnvironment` | [`Boolean!`](#boolean) | If `true`, the user can perform `destroy_environment` on this resource. |
+| <a id="environmentpermissionsstopenvironment"></a>`stopEnvironment` | [`Boolean!`](#boolean) | If `true`, the user can perform `stop_environment` on this resource. |
+| <a id="environmentpermissionsupdateenvironment"></a>`updateEnvironment` | [`Boolean!`](#boolean) | If `true`, the user can perform `update_environment` on this resource. |
### `Epic`
@@ -16936,8 +17452,10 @@ Total weight of open and closed descendant issues.
| Name | Type | Description |
| ---- | ---- | ----------- |
-| <a id="epicdescendantweightsclosedissues"></a>`closedIssues` | [`Int`](#int) | Total weight of completed (closed) issues in this epic, including epic descendants. |
-| <a id="epicdescendantweightsopenedissues"></a>`openedIssues` | [`Int`](#int) | Total weight of opened issues in this epic, including epic descendants. |
+| <a id="epicdescendantweightsclosedissues"></a>`closedIssues` **{warning-solid}** | [`Int`](#int) | **Deprecated** in 16.6. Use `closedIssuesTotal`. |
+| <a id="epicdescendantweightsclosedissuestotal"></a>`closedIssuesTotal` | [`BigInt`](#bigint) | Total weight of completed (closed) issues in this epic, including epic descendants, encoded as a string. |
+| <a id="epicdescendantweightsopenedissues"></a>`openedIssues` **{warning-solid}** | [`Int`](#int) | **Deprecated** in 16.6. Use `OpenedIssuesTotal`. |
+| <a id="epicdescendantweightsopenedissuestotal"></a>`openedIssuesTotal` | [`BigInt`](#bigint) | Total weight of opened issues in this epic, including epic descendants, encoded as a string. |
### `EpicHealthStatus`
@@ -17167,14 +17685,14 @@ Check permissions for the current user on an epic.
| Name | Type | Description |
| ---- | ---- | ----------- |
-| <a id="epicpermissionsadminepic"></a>`adminEpic` | [`Boolean!`](#boolean) | Indicates the user can perform `admin_epic` on this resource. |
-| <a id="epicpermissionsawardemoji"></a>`awardEmoji` | [`Boolean!`](#boolean) | Indicates the user can perform `award_emoji` on this resource. |
-| <a id="epicpermissionscreateepic"></a>`createEpic` | [`Boolean!`](#boolean) | Indicates the user can perform `create_epic` on this resource. |
-| <a id="epicpermissionscreatenote"></a>`createNote` | [`Boolean!`](#boolean) | Indicates the user can perform `create_note` on this resource. |
-| <a id="epicpermissionsdestroyepic"></a>`destroyEpic` | [`Boolean!`](#boolean) | Indicates the user can perform `destroy_epic` on this resource. |
-| <a id="epicpermissionsreadepic"></a>`readEpic` | [`Boolean!`](#boolean) | Indicates the user can perform `read_epic` on this resource. |
-| <a id="epicpermissionsreadepiciid"></a>`readEpicIid` | [`Boolean!`](#boolean) | Indicates the user can perform `read_epic_iid` on this resource. |
-| <a id="epicpermissionsupdateepic"></a>`updateEpic` | [`Boolean!`](#boolean) | Indicates the user can perform `update_epic` on this resource. |
+| <a id="epicpermissionsadminepic"></a>`adminEpic` | [`Boolean!`](#boolean) | If `true`, the user can perform `admin_epic` on this resource. |
+| <a id="epicpermissionsawardemoji"></a>`awardEmoji` | [`Boolean!`](#boolean) | If `true`, the user can perform `award_emoji` on this resource. |
+| <a id="epicpermissionscreateepic"></a>`createEpic` | [`Boolean!`](#boolean) | If `true`, the user can perform `create_epic` on this resource. |
+| <a id="epicpermissionscreatenote"></a>`createNote` | [`Boolean!`](#boolean) | If `true`, the user can perform `create_note` on this resource. |
+| <a id="epicpermissionsdestroyepic"></a>`destroyEpic` | [`Boolean!`](#boolean) | If `true`, the user can perform `destroy_epic` on this resource. |
+| <a id="epicpermissionsreadepic"></a>`readEpic` | [`Boolean!`](#boolean) | If `true`, the user can perform `read_epic` on this resource. |
+| <a id="epicpermissionsreadepiciid"></a>`readEpicIid` | [`Boolean!`](#boolean) | If `true`, the user can perform `read_epic_iid` on this resource. |
+| <a id="epicpermissionsupdateepic"></a>`updateEpic` | [`Boolean!`](#boolean) | If `true`, the user can perform `update_epic` on this resource. |
### `EscalationPolicyType`
@@ -17250,6 +17768,7 @@ Represents an external resource to send audit events to.
| <a id="externalauditeventdestinationheaders"></a>`headers` | [`AuditEventStreamingHeaderConnection!`](#auditeventstreamingheaderconnection) | List of additional HTTP headers sent with each event. (see [Connections](#connections)) |
| <a id="externalauditeventdestinationid"></a>`id` | [`ID!`](#id) | ID of the destination. |
| <a id="externalauditeventdestinationname"></a>`name` | [`String!`](#string) | Name of the external destination to send audit events to. |
+| <a id="externalauditeventdestinationnamespacefilter"></a>`namespaceFilter` | [`AuditEventStreamingHTTPNamespaceFilter`](#auditeventstreaminghttpnamespacefilter) | List of subgroup or project filters for the destination. |
| <a id="externalauditeventdestinationverificationtoken"></a>`verificationToken` | [`String!`](#string) | Verification token to validate source of event. |
### `ExternalIssue`
@@ -17782,6 +18301,7 @@ GPG signature for a signed commit.
| <a id="grouppackagesettings"></a>`packageSettings` | [`PackageSettings`](#packagesettings) | Package settings for the namespace. |
| <a id="groupparent"></a>`parent` | [`Group`](#group) | Parent group. |
| <a id="grouppath"></a>`path` | [`String!`](#string) | Path of the namespace. |
+| <a id="grouppendingmembers"></a>`pendingMembers` **{warning-solid}** | [`PendingGroupMemberConnection`](#pendinggroupmemberconnection) | **Introduced** in 16.6. This feature is an Experiment. It can be changed or removed at any time. A pending membership of a user within this group. |
| <a id="groupprojectcreationlevel"></a>`projectCreationLevel` | [`String`](#string) | Permission level required to create projects in the group. |
| <a id="grouprecentissueboards"></a>`recentIssueBoards` | [`BoardConnection`](#boardconnection) | List of recently visited boards of the group. Maximum size is 4. (see [Connections](#connections)) |
| <a id="grouprepositorysizeexcessprojectcount"></a>`repositorySizeExcessProjectCount` | [`Int!`](#int) | Number of projects in the root namespace where the repository size exceeds the limit. This only applies to namespaces under Project limit enforcement. |
@@ -18394,6 +18914,26 @@ four standard [pagination arguments](#connection-pagination-arguments):
| <a id="grouplabelsonlygrouplabels"></a>`onlyGroupLabels` | [`Boolean`](#boolean) | Include only group level labels. |
| <a id="grouplabelssearchterm"></a>`searchTerm` | [`String`](#string) | Search term to find labels with. |
+##### `Group.memberRoles`
+
+Member roles available for the group.
+
+WARNING:
+**Introduced** in 16.5.
+This feature is an Experiment. It can be changed or removed at any time.
+
+Returns [`MemberRoleConnection`](#memberroleconnection).
+
+This field returns a [connection](#connections). It accepts the
+four standard [pagination arguments](#connection-pagination-arguments):
+`before: String`, `after: String`, `first: Int`, `last: Int`.
+
+###### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="groupmemberrolesid"></a>`id` | [`MemberRoleID`](#memberroleid) | Global ID of the member role to look up. |
+
##### `Group.mergeRequestViolations`
Compliance violations reported on merge requests merged within the group.
@@ -18519,6 +19059,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| <a id="grouppackagesincludeversionless"></a>`includeVersionless` | [`Boolean`](#boolean) | Include versionless packages. |
| <a id="grouppackagespackagename"></a>`packageName` | [`String`](#string) | Search a package by name. |
| <a id="grouppackagespackagetype"></a>`packageType` | [`PackageTypeEnum`](#packagetypeenum) | Filter a package by type. |
+| <a id="grouppackagespackageversion"></a>`packageVersion` | [`String`](#string) | Filter a package by version. If used in combination with `include_versionless`, then no versionless packages are returned. |
| <a id="grouppackagessort"></a>`sort` | [`PackageGroupSort`](#packagegroupsort) | Sort packages by this criteria. |
| <a id="grouppackagesstatus"></a>`status` | [`PackageStatus`](#packagestatus) | Filter a package by status. |
@@ -18595,6 +19136,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="grouprunnersactive"></a>`active` **{warning-solid}** | [`Boolean`](#boolean) | **Deprecated** in 14.8. This was renamed. Use: `paused`. |
+| <a id="grouprunnerscreatorid"></a>`creatorId` | [`UserID`](#userid) | Filter runners by creator ID. |
| <a id="grouprunnersmembership"></a>`membership` | [`CiRunnerMembershipFilter`](#cirunnermembershipfilter) | Control which runners to include in the results. |
| <a id="grouprunnerspaused"></a>`paused` | [`Boolean`](#boolean) | Filter runners by `paused` (true) or `active` (false) status. |
| <a id="grouprunnerssearch"></a>`search` | [`String`](#string) | Filter by full token or partial text in description field. |
@@ -18603,6 +19145,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| <a id="grouprunnerstaglist"></a>`tagList` | [`[String!]`](#string) | Filter by tags associated with the runner (comma-separated or array). |
| <a id="grouprunnerstype"></a>`type` | [`CiRunnerType`](#cirunnertype) | Filter runners by type. |
| <a id="grouprunnersupgradestatus"></a>`upgradeStatus` | [`CiRunnerUpgradeStatus`](#cirunnerupgradestatus) | Filter by upgrade status. |
+| <a id="grouprunnersversionprefix"></a>`versionPrefix` **{warning-solid}** | [`String`](#string) | **Introduced** in 16.6. This feature is an Experiment. It can be changed or removed at any time. Filter runners by version. Runners that contain runner managers with the version at the start of the search term are returned. For example, the search term '14.' returns runner managers with versions '14.11.1' and '14.2.3'. |
##### `Group.scanExecutionPolicies`
@@ -18842,9 +19385,9 @@ Represents a Group Membership.
| <a id="groupmembercreatedat"></a>`createdAt` | [`Time`](#time) | Date and time the membership was created. |
| <a id="groupmembercreatedby"></a>`createdBy` | [`UserCore`](#usercore) | User that authorized membership. |
| <a id="groupmemberexpiresat"></a>`expiresAt` | [`Time`](#time) | Date and time the membership expires. |
-| <a id="groupmembergroup"></a>`group` | [`Group`](#group) | Group that a User is a member of. |
+| <a id="groupmembergroup"></a>`group` | [`Group`](#group) | Group that a user is a member of. |
| <a id="groupmemberid"></a>`id` | [`ID!`](#id) | ID of the member. |
-| <a id="groupmembernotificationemail"></a>`notificationEmail` | [`String`](#string) | Group notification email for User. Only available for admins. |
+| <a id="groupmembernotificationemail"></a>`notificationEmail` | [`String`](#string) | Group notification email for user. Only available for admins. |
| <a id="groupmemberupdatedat"></a>`updatedAt` | [`Time`](#time) | Date and time the membership was last updated. |
| <a id="groupmemberuser"></a>`user` | [`UserCore`](#usercore) | User that is associated with the member object. |
| <a id="groupmemberuserpermissions"></a>`userPermissions` | [`GroupPermissions!`](#grouppermissions) | Permissions for the current user on the resource. |
@@ -18869,9 +19412,9 @@ Returns [`UserMergeRequestInteraction`](#usermergerequestinteraction).
| Name | Type | Description |
| ---- | ---- | ----------- |
-| <a id="grouppermissionscreatecustomemoji"></a>`createCustomEmoji` | [`Boolean!`](#boolean) | Indicates the user can perform `create_custom_emoji` on this resource. |
-| <a id="grouppermissionscreateprojects"></a>`createProjects` | [`Boolean!`](#boolean) | Indicates the user can perform `create_projects` on this resource. |
-| <a id="grouppermissionsreadgroup"></a>`readGroup` | [`Boolean!`](#boolean) | Indicates the user can perform `read_group` on this resource. |
+| <a id="grouppermissionscreatecustomemoji"></a>`createCustomEmoji` | [`Boolean!`](#boolean) | If `true`, the user can perform `create_custom_emoji` on this resource. |
+| <a id="grouppermissionscreateprojects"></a>`createProjects` | [`Boolean!`](#boolean) | If `true`, the user can perform `create_projects` on this resource. |
+| <a id="grouppermissionsreadgroup"></a>`readGroup` | [`Boolean!`](#boolean) | If `true`, the user can perform `read_group` on this resource. |
### `GroupReleaseStats`
@@ -19459,15 +20002,15 @@ Check permissions for the current user on a issue.
| Name | Type | Description |
| ---- | ---- | ----------- |
-| <a id="issuepermissionsadminissue"></a>`adminIssue` | [`Boolean!`](#boolean) | Indicates the user can perform `admin_issue` on this resource. |
-| <a id="issuepermissionscreatedesign"></a>`createDesign` | [`Boolean!`](#boolean) | Indicates the user can perform `create_design` on this resource. |
-| <a id="issuepermissionscreatenote"></a>`createNote` | [`Boolean!`](#boolean) | Indicates the user can perform `create_note` on this resource. |
-| <a id="issuepermissionsdestroydesign"></a>`destroyDesign` | [`Boolean!`](#boolean) | Indicates the user can perform `destroy_design` on this resource. |
-| <a id="issuepermissionsreaddesign"></a>`readDesign` | [`Boolean!`](#boolean) | Indicates the user can perform `read_design` on this resource. |
-| <a id="issuepermissionsreadissue"></a>`readIssue` | [`Boolean!`](#boolean) | Indicates the user can perform `read_issue` on this resource. |
-| <a id="issuepermissionsreopenissue"></a>`reopenIssue` | [`Boolean!`](#boolean) | Indicates the user can perform `reopen_issue` on this resource. |
-| <a id="issuepermissionsupdatedesign"></a>`updateDesign` | [`Boolean!`](#boolean) | Indicates the user can perform `update_design` on this resource. |
-| <a id="issuepermissionsupdateissue"></a>`updateIssue` | [`Boolean!`](#boolean) | Indicates the user can perform `update_issue` on this resource. |
+| <a id="issuepermissionsadminissue"></a>`adminIssue` | [`Boolean!`](#boolean) | If `true`, the user can perform `admin_issue` on this resource. |
+| <a id="issuepermissionscreatedesign"></a>`createDesign` | [`Boolean!`](#boolean) | If `true`, the user can perform `create_design` on this resource. |
+| <a id="issuepermissionscreatenote"></a>`createNote` | [`Boolean!`](#boolean) | If `true`, the user can perform `create_note` on this resource. |
+| <a id="issuepermissionsdestroydesign"></a>`destroyDesign` | [`Boolean!`](#boolean) | If `true`, the user can perform `destroy_design` on this resource. |
+| <a id="issuepermissionsreaddesign"></a>`readDesign` | [`Boolean!`](#boolean) | If `true`, the user can perform `read_design` on this resource. |
+| <a id="issuepermissionsreadissue"></a>`readIssue` | [`Boolean!`](#boolean) | If `true`, the user can perform `read_issue` on this resource. |
+| <a id="issuepermissionsreopenissue"></a>`reopenIssue` | [`Boolean!`](#boolean) | If `true`, the user can perform `reopen_issue` on this resource. |
+| <a id="issuepermissionsupdatedesign"></a>`updateDesign` | [`Boolean!`](#boolean) | If `true`, the user can perform `update_design` on this resource. |
+| <a id="issuepermissionsupdateissue"></a>`updateIssue` | [`Boolean!`](#boolean) | If `true`, the user can perform `update_issue` on this resource. |
### `IssueStatusCountsType`
@@ -19633,9 +20176,10 @@ Represents the Geo replication and verification state of a job_artifact.
| Name | Type | Description |
| ---- | ---- | ----------- |
-| <a id="jobpermissionsreadbuild"></a>`readBuild` | [`Boolean!`](#boolean) | Indicates the user can perform `read_build` on this resource. |
-| <a id="jobpermissionsreadjobartifacts"></a>`readJobArtifacts` | [`Boolean!`](#boolean) | Indicates the user can perform `read_job_artifacts` on this resource. |
-| <a id="jobpermissionsupdatebuild"></a>`updateBuild` | [`Boolean!`](#boolean) | Indicates the user can perform `update_build` on this resource. |
+| <a id="jobpermissionscancelbuild"></a>`cancelBuild` | [`Boolean!`](#boolean) | If `true`, the user can perform `cancel_build` on this resource. |
+| <a id="jobpermissionsreadbuild"></a>`readBuild` | [`Boolean!`](#boolean) | If `true`, the user can perform `read_build` on this resource. |
+| <a id="jobpermissionsreadjobartifacts"></a>`readJobArtifacts` | [`Boolean!`](#boolean) | If `true`, the user can perform `read_job_artifacts` on this resource. |
+| <a id="jobpermissionsupdatebuild"></a>`updateBuild` | [`Boolean!`](#boolean) | If `true`, the user can perform `update_build` on this resource. |
### `Kas`
@@ -19741,7 +20285,7 @@ Represents an entry from the Cloud License history.
| <a id="linkedworkitemtypelinkid"></a>`linkId` | [`WorkItemsRelatedWorkItemLinkID!`](#workitemsrelatedworkitemlinkid) | Global ID of the link. |
| <a id="linkedworkitemtypelinktype"></a>`linkType` | [`String!`](#string) | Type of link. |
| <a id="linkedworkitemtypelinkupdatedat"></a>`linkUpdatedAt` | [`Time!`](#time) | Timestamp the link was updated. |
-| <a id="linkedworkitemtypeworkitem"></a>`workItem` | [`WorkItem!`](#workitem) | Linked work item. |
+| <a id="linkedworkitemtypeworkitem"></a>`workItem` | [`WorkItem`](#workitem) | Linked work item. |
### `Location`
@@ -19776,9 +20320,19 @@ Represents a member role.
| Name | Type | Description |
| ---- | ---- | ----------- |
+| <a id="memberroleadmingroupmember"></a>`adminGroupMember` **{warning-solid}** | [`Boolean`](#boolean) | **Introduced** in 16.5. This feature is an Experiment. It can be changed or removed at any time. Permission to admin group members. |
+| <a id="memberroleadminmergerequest"></a>`adminMergeRequest` **{warning-solid}** | [`Boolean`](#boolean) | **Introduced** in 16.5. This feature is an Experiment. It can be changed or removed at any time. Permission to admin merge requests. |
+| <a id="memberroleadminvulnerability"></a>`adminVulnerability` **{warning-solid}** | [`Boolean`](#boolean) | **Introduced** in 16.5. This feature is an Experiment. It can be changed or removed at any time. Permission to admin vulnerability. |
+| <a id="memberrolearchiveproject"></a>`archiveProject` **{warning-solid}** | [`Boolean`](#boolean) | **Introduced** in 16.6. This feature is an Experiment. It can be changed or removed at any time. Permission to archive projects. |
+| <a id="memberrolebaseaccesslevel"></a>`baseAccessLevel` **{warning-solid}** | [`AccessLevel!`](#accesslevel) | **Introduced** in 16.5. This feature is an Experiment. It can be changed or removed at any time. Base access level for the custom role. |
| <a id="memberroledescription"></a>`description` | [`String`](#string) | Description of the member role. |
+| <a id="memberroleenabledpermissions"></a>`enabledPermissions` **{warning-solid}** | [`[MemberRolePermission!]`](#memberrolepermission) | **Introduced** in 16.5. This feature is an Experiment. It can be changed or removed at any time. Array of all permissions enabled for the custom role. |
| <a id="memberroleid"></a>`id` | [`MemberRoleID!`](#memberroleid) | ID of the member role. |
+| <a id="memberrolemanageprojectaccesstokens"></a>`manageProjectAccessTokens` **{warning-solid}** | [`Boolean`](#boolean) | **Introduced** in 16.5. This feature is an Experiment. It can be changed or removed at any time. Permission to admin project access tokens. |
| <a id="memberrolename"></a>`name` | [`String!`](#string) | Name of the member role. |
+| <a id="memberrolereadcode"></a>`readCode` **{warning-solid}** | [`Boolean`](#boolean) | **Introduced** in 16.5. This feature is an Experiment. It can be changed or removed at any time. Permission to read code. |
+| <a id="memberrolereaddependency"></a>`readDependency` **{warning-solid}** | [`Boolean`](#boolean) | **Introduced** in 16.5. This feature is an Experiment. It can be changed or removed at any time. Permission to read dependency. |
+| <a id="memberrolereadvulnerability"></a>`readVulnerability` **{warning-solid}** | [`Boolean`](#boolean) | **Introduced** in 16.5. This feature is an Experiment. It can be changed or removed at any time. Permission to read vulnerability. |
### `MergeAccessLevel`
@@ -20030,6 +20584,7 @@ A user assigned to a merge request.
| <a id="mergerequestassigneeid"></a>`id` | [`ID!`](#id) | ID of the user. |
| <a id="mergerequestassigneeide"></a>`ide` | [`Ide`](#ide) | IDE settings. |
| <a id="mergerequestassigneejobtitle"></a>`jobTitle` | [`String`](#string) | Job title of the user. |
+| <a id="mergerequestassigneelastactivityon"></a>`lastActivityOn` | [`Date`](#date) | Date the user last performed any actions. |
| <a id="mergerequestassigneelinkedin"></a>`linkedin` | [`String`](#string) | LinkedIn profile name of the user. |
| <a id="mergerequestassigneelocation"></a>`location` | [`String`](#string) | Location of the user. |
| <a id="mergerequestassigneemergerequestinteraction"></a>`mergeRequestInteraction` | [`UserMergeRequestInteraction`](#usermergerequestinteraction) | Details of this user's interactions with the merge request. |
@@ -20037,12 +20592,13 @@ A user assigned to a merge request.
| <a id="mergerequestassigneenamespace"></a>`namespace` | [`Namespace`](#namespace) | Personal namespace of the user. |
| <a id="mergerequestassigneenamespacecommitemails"></a>`namespaceCommitEmails` | [`NamespaceCommitEmailConnection`](#namespacecommitemailconnection) | User's custom namespace commit emails. (see [Connections](#connections)) |
| <a id="mergerequestassigneeorganization"></a>`organization` | [`String`](#string) | Who the user represents or works for. |
+| <a id="mergerequestassigneeorganizations"></a>`organizations` **{warning-solid}** | [`OrganizationConnection`](#organizationconnection) | **Introduced** in 16.6. This feature is an Experiment. It can be changed or removed at any time. Organizations where the user has access. |
| <a id="mergerequestassigneepreferencesgitpodpath"></a>`preferencesGitpodPath` | [`String`](#string) | Web path to the Gitpod section within user preferences. |
| <a id="mergerequestassigneeprofileenablegitpodpath"></a>`profileEnableGitpodPath` | [`String`](#string) | Web path to enable Gitpod for the user. |
| <a id="mergerequestassigneeprojectmemberships"></a>`projectMemberships` | [`ProjectMemberConnection`](#projectmemberconnection) | Project memberships of the user. (see [Connections](#connections)) |
| <a id="mergerequestassigneepronouns"></a>`pronouns` | [`String`](#string) | Pronouns of the user. |
| <a id="mergerequestassigneepublicemail"></a>`publicEmail` | [`String`](#string) | User's public email. |
-| <a id="mergerequestassigneesavedreplies"></a>`savedReplies` | [`SavedReplyConnection`](#savedreplyconnection) | Saved replies authored by the user. Will not return saved replies if `saved_replies` feature flag is disabled. (see [Connections](#connections)) |
+| <a id="mergerequestassigneesavedreplies"></a>`savedReplies` | [`SavedReplyConnection`](#savedreplyconnection) | Saved replies authored by the user. (see [Connections](#connections)) |
| <a id="mergerequestassigneestate"></a>`state` | [`UserState!`](#userstate) | State of the user. |
| <a id="mergerequestassigneestatus"></a>`status` | [`UserStatus`](#userstatus) | User status. |
| <a id="mergerequestassigneetwitter"></a>`twitter` | [`String`](#string) | Twitter username of the user. |
@@ -20181,7 +20737,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
##### `MergeRequestAssignee.savedReply`
-Saved reply authored by the user. Will not return saved reply if `saved_replies` feature flag is disabled.
+Saved reply authored by the user.
Returns [`SavedReply`](#savedreply).
@@ -20310,6 +20866,7 @@ The author of the merge request.
| <a id="mergerequestauthorid"></a>`id` | [`ID!`](#id) | ID of the user. |
| <a id="mergerequestauthoride"></a>`ide` | [`Ide`](#ide) | IDE settings. |
| <a id="mergerequestauthorjobtitle"></a>`jobTitle` | [`String`](#string) | Job title of the user. |
+| <a id="mergerequestauthorlastactivityon"></a>`lastActivityOn` | [`Date`](#date) | Date the user last performed any actions. |
| <a id="mergerequestauthorlinkedin"></a>`linkedin` | [`String`](#string) | LinkedIn profile name of the user. |
| <a id="mergerequestauthorlocation"></a>`location` | [`String`](#string) | Location of the user. |
| <a id="mergerequestauthormergerequestinteraction"></a>`mergeRequestInteraction` | [`UserMergeRequestInteraction`](#usermergerequestinteraction) | Details of this user's interactions with the merge request. |
@@ -20317,12 +20874,13 @@ The author of the merge request.
| <a id="mergerequestauthornamespace"></a>`namespace` | [`Namespace`](#namespace) | Personal namespace of the user. |
| <a id="mergerequestauthornamespacecommitemails"></a>`namespaceCommitEmails` | [`NamespaceCommitEmailConnection`](#namespacecommitemailconnection) | User's custom namespace commit emails. (see [Connections](#connections)) |
| <a id="mergerequestauthororganization"></a>`organization` | [`String`](#string) | Who the user represents or works for. |
+| <a id="mergerequestauthororganizations"></a>`organizations` **{warning-solid}** | [`OrganizationConnection`](#organizationconnection) | **Introduced** in 16.6. This feature is an Experiment. It can be changed or removed at any time. Organizations where the user has access. |
| <a id="mergerequestauthorpreferencesgitpodpath"></a>`preferencesGitpodPath` | [`String`](#string) | Web path to the Gitpod section within user preferences. |
| <a id="mergerequestauthorprofileenablegitpodpath"></a>`profileEnableGitpodPath` | [`String`](#string) | Web path to enable Gitpod for the user. |
| <a id="mergerequestauthorprojectmemberships"></a>`projectMemberships` | [`ProjectMemberConnection`](#projectmemberconnection) | Project memberships of the user. (see [Connections](#connections)) |
| <a id="mergerequestauthorpronouns"></a>`pronouns` | [`String`](#string) | Pronouns of the user. |
| <a id="mergerequestauthorpublicemail"></a>`publicEmail` | [`String`](#string) | User's public email. |
-| <a id="mergerequestauthorsavedreplies"></a>`savedReplies` | [`SavedReplyConnection`](#savedreplyconnection) | Saved replies authored by the user. Will not return saved replies if `saved_replies` feature flag is disabled. (see [Connections](#connections)) |
+| <a id="mergerequestauthorsavedreplies"></a>`savedReplies` | [`SavedReplyConnection`](#savedreplyconnection) | Saved replies authored by the user. (see [Connections](#connections)) |
| <a id="mergerequestauthorstate"></a>`state` | [`UserState!`](#userstate) | State of the user. |
| <a id="mergerequestauthorstatus"></a>`status` | [`UserStatus`](#userstatus) | User status. |
| <a id="mergerequestauthortwitter"></a>`twitter` | [`String`](#string) | Twitter username of the user. |
@@ -20461,7 +21019,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
##### `MergeRequestAuthor.savedReply`
-Saved reply authored by the user. Will not return saved reply if `saved_replies` feature flag is disabled.
+Saved reply authored by the user.
Returns [`SavedReply`](#savedreply).
@@ -20653,6 +21211,7 @@ A user participating in a merge request.
| <a id="mergerequestparticipantid"></a>`id` | [`ID!`](#id) | ID of the user. |
| <a id="mergerequestparticipantide"></a>`ide` | [`Ide`](#ide) | IDE settings. |
| <a id="mergerequestparticipantjobtitle"></a>`jobTitle` | [`String`](#string) | Job title of the user. |
+| <a id="mergerequestparticipantlastactivityon"></a>`lastActivityOn` | [`Date`](#date) | Date the user last performed any actions. |
| <a id="mergerequestparticipantlinkedin"></a>`linkedin` | [`String`](#string) | LinkedIn profile name of the user. |
| <a id="mergerequestparticipantlocation"></a>`location` | [`String`](#string) | Location of the user. |
| <a id="mergerequestparticipantmergerequestinteraction"></a>`mergeRequestInteraction` | [`UserMergeRequestInteraction`](#usermergerequestinteraction) | Details of this user's interactions with the merge request. |
@@ -20660,12 +21219,13 @@ A user participating in a merge request.
| <a id="mergerequestparticipantnamespace"></a>`namespace` | [`Namespace`](#namespace) | Personal namespace of the user. |
| <a id="mergerequestparticipantnamespacecommitemails"></a>`namespaceCommitEmails` | [`NamespaceCommitEmailConnection`](#namespacecommitemailconnection) | User's custom namespace commit emails. (see [Connections](#connections)) |
| <a id="mergerequestparticipantorganization"></a>`organization` | [`String`](#string) | Who the user represents or works for. |
+| <a id="mergerequestparticipantorganizations"></a>`organizations` **{warning-solid}** | [`OrganizationConnection`](#organizationconnection) | **Introduced** in 16.6. This feature is an Experiment. It can be changed or removed at any time. Organizations where the user has access. |
| <a id="mergerequestparticipantpreferencesgitpodpath"></a>`preferencesGitpodPath` | [`String`](#string) | Web path to the Gitpod section within user preferences. |
| <a id="mergerequestparticipantprofileenablegitpodpath"></a>`profileEnableGitpodPath` | [`String`](#string) | Web path to enable Gitpod for the user. |
| <a id="mergerequestparticipantprojectmemberships"></a>`projectMemberships` | [`ProjectMemberConnection`](#projectmemberconnection) | Project memberships of the user. (see [Connections](#connections)) |
| <a id="mergerequestparticipantpronouns"></a>`pronouns` | [`String`](#string) | Pronouns of the user. |
| <a id="mergerequestparticipantpublicemail"></a>`publicEmail` | [`String`](#string) | User's public email. |
-| <a id="mergerequestparticipantsavedreplies"></a>`savedReplies` | [`SavedReplyConnection`](#savedreplyconnection) | Saved replies authored by the user. Will not return saved replies if `saved_replies` feature flag is disabled. (see [Connections](#connections)) |
+| <a id="mergerequestparticipantsavedreplies"></a>`savedReplies` | [`SavedReplyConnection`](#savedreplyconnection) | Saved replies authored by the user. (see [Connections](#connections)) |
| <a id="mergerequestparticipantstate"></a>`state` | [`UserState!`](#userstate) | State of the user. |
| <a id="mergerequestparticipantstatus"></a>`status` | [`UserStatus`](#userstatus) | User status. |
| <a id="mergerequestparticipanttwitter"></a>`twitter` | [`String`](#string) | Twitter username of the user. |
@@ -20804,7 +21364,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
##### `MergeRequestParticipant.savedReply`
-Saved reply authored by the user. Will not return saved reply if `saved_replies` feature flag is disabled.
+Saved reply authored by the user.
Returns [`SavedReply`](#savedreply).
@@ -20918,16 +21478,16 @@ Check permissions for the current user on a merge request.
| Name | Type | Description |
| ---- | ---- | ----------- |
-| <a id="mergerequestpermissionsadminmergerequest"></a>`adminMergeRequest` | [`Boolean!`](#boolean) | Indicates the user can perform `admin_merge_request` on this resource. |
-| <a id="mergerequestpermissionscanapprove"></a>`canApprove` | [`Boolean!`](#boolean) | Indicates the user can perform `can_approve` on this resource. |
-| <a id="mergerequestpermissionscanmerge"></a>`canMerge` | [`Boolean!`](#boolean) | Indicates the user can perform `can_merge` on this resource. |
-| <a id="mergerequestpermissionscherrypickoncurrentmergerequest"></a>`cherryPickOnCurrentMergeRequest` | [`Boolean!`](#boolean) | Indicates the user can perform `cherry_pick_on_current_merge_request` on this resource. |
-| <a id="mergerequestpermissionscreatenote"></a>`createNote` | [`Boolean!`](#boolean) | Indicates the user can perform `create_note` on this resource. |
-| <a id="mergerequestpermissionspushtosourcebranch"></a>`pushToSourceBranch` | [`Boolean!`](#boolean) | Indicates the user can perform `push_to_source_branch` on this resource. |
-| <a id="mergerequestpermissionsreadmergerequest"></a>`readMergeRequest` | [`Boolean!`](#boolean) | Indicates the user can perform `read_merge_request` on this resource. |
-| <a id="mergerequestpermissionsremovesourcebranch"></a>`removeSourceBranch` | [`Boolean!`](#boolean) | Indicates the user can perform `remove_source_branch` on this resource. |
-| <a id="mergerequestpermissionsrevertoncurrentmergerequest"></a>`revertOnCurrentMergeRequest` | [`Boolean!`](#boolean) | Indicates the user can perform `revert_on_current_merge_request` on this resource. |
-| <a id="mergerequestpermissionsupdatemergerequest"></a>`updateMergeRequest` | [`Boolean!`](#boolean) | Indicates the user can perform `update_merge_request` on this resource. |
+| <a id="mergerequestpermissionsadminmergerequest"></a>`adminMergeRequest` | [`Boolean!`](#boolean) | If `true`, the user can perform `admin_merge_request` on this resource. |
+| <a id="mergerequestpermissionscanapprove"></a>`canApprove` | [`Boolean!`](#boolean) | If `true`, the user can perform `can_approve` on this resource. |
+| <a id="mergerequestpermissionscanmerge"></a>`canMerge` | [`Boolean!`](#boolean) | If `true`, the user can perform `can_merge` on this resource. |
+| <a id="mergerequestpermissionscherrypickoncurrentmergerequest"></a>`cherryPickOnCurrentMergeRequest` | [`Boolean!`](#boolean) | If `true`, the user can perform `cherry_pick_on_current_merge_request` on this resource. |
+| <a id="mergerequestpermissionscreatenote"></a>`createNote` | [`Boolean!`](#boolean) | If `true`, the user can perform `create_note` on this resource. |
+| <a id="mergerequestpermissionspushtosourcebranch"></a>`pushToSourceBranch` | [`Boolean!`](#boolean) | If `true`, the user can perform `push_to_source_branch` on this resource. |
+| <a id="mergerequestpermissionsreadmergerequest"></a>`readMergeRequest` | [`Boolean!`](#boolean) | If `true`, the user can perform `read_merge_request` on this resource. |
+| <a id="mergerequestpermissionsremovesourcebranch"></a>`removeSourceBranch` | [`Boolean!`](#boolean) | If `true`, the user can perform `remove_source_branch` on this resource. |
+| <a id="mergerequestpermissionsrevertoncurrentmergerequest"></a>`revertOnCurrentMergeRequest` | [`Boolean!`](#boolean) | If `true`, the user can perform `revert_on_current_merge_request` on this resource. |
+| <a id="mergerequestpermissionsupdatemergerequest"></a>`updateMergeRequest` | [`Boolean!`](#boolean) | If `true`, the user can perform `update_merge_request` on this resource. |
### `MergeRequestReviewLlmSummary`
@@ -20969,6 +21529,7 @@ A user assigned to a merge request as a reviewer.
| <a id="mergerequestreviewerid"></a>`id` | [`ID!`](#id) | ID of the user. |
| <a id="mergerequestrevieweride"></a>`ide` | [`Ide`](#ide) | IDE settings. |
| <a id="mergerequestreviewerjobtitle"></a>`jobTitle` | [`String`](#string) | Job title of the user. |
+| <a id="mergerequestreviewerlastactivityon"></a>`lastActivityOn` | [`Date`](#date) | Date the user last performed any actions. |
| <a id="mergerequestreviewerlinkedin"></a>`linkedin` | [`String`](#string) | LinkedIn profile name of the user. |
| <a id="mergerequestreviewerlocation"></a>`location` | [`String`](#string) | Location of the user. |
| <a id="mergerequestreviewermergerequestinteraction"></a>`mergeRequestInteraction` | [`UserMergeRequestInteraction`](#usermergerequestinteraction) | Details of this user's interactions with the merge request. |
@@ -20976,12 +21537,13 @@ A user assigned to a merge request as a reviewer.
| <a id="mergerequestreviewernamespace"></a>`namespace` | [`Namespace`](#namespace) | Personal namespace of the user. |
| <a id="mergerequestreviewernamespacecommitemails"></a>`namespaceCommitEmails` | [`NamespaceCommitEmailConnection`](#namespacecommitemailconnection) | User's custom namespace commit emails. (see [Connections](#connections)) |
| <a id="mergerequestreviewerorganization"></a>`organization` | [`String`](#string) | Who the user represents or works for. |
+| <a id="mergerequestreviewerorganizations"></a>`organizations` **{warning-solid}** | [`OrganizationConnection`](#organizationconnection) | **Introduced** in 16.6. This feature is an Experiment. It can be changed or removed at any time. Organizations where the user has access. |
| <a id="mergerequestreviewerpreferencesgitpodpath"></a>`preferencesGitpodPath` | [`String`](#string) | Web path to the Gitpod section within user preferences. |
| <a id="mergerequestreviewerprofileenablegitpodpath"></a>`profileEnableGitpodPath` | [`String`](#string) | Web path to enable Gitpod for the user. |
| <a id="mergerequestreviewerprojectmemberships"></a>`projectMemberships` | [`ProjectMemberConnection`](#projectmemberconnection) | Project memberships of the user. (see [Connections](#connections)) |
| <a id="mergerequestreviewerpronouns"></a>`pronouns` | [`String`](#string) | Pronouns of the user. |
| <a id="mergerequestreviewerpublicemail"></a>`publicEmail` | [`String`](#string) | User's public email. |
-| <a id="mergerequestreviewersavedreplies"></a>`savedReplies` | [`SavedReplyConnection`](#savedreplyconnection) | Saved replies authored by the user. Will not return saved replies if `saved_replies` feature flag is disabled. (see [Connections](#connections)) |
+| <a id="mergerequestreviewersavedreplies"></a>`savedReplies` | [`SavedReplyConnection`](#savedreplyconnection) | Saved replies authored by the user. (see [Connections](#connections)) |
| <a id="mergerequestreviewerstate"></a>`state` | [`UserState!`](#userstate) | State of the user. |
| <a id="mergerequestreviewerstatus"></a>`status` | [`UserStatus`](#userstatus) | User status. |
| <a id="mergerequestreviewertwitter"></a>`twitter` | [`String`](#string) | Twitter username of the user. |
@@ -21120,7 +21682,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
##### `MergeRequestReviewer.savedReply`
-Saved reply authored by the user. Will not return saved reply if `saved_replies` feature flag is disabled.
+Saved reply authored by the user.
Returns [`SavedReply`](#savedreply).
@@ -21573,12 +22135,12 @@ Represents the network policy.
| Name | Type | Description |
| ---- | ---- | ----------- |
-| <a id="notepermissionsadminnote"></a>`adminNote` | [`Boolean!`](#boolean) | Indicates the user can perform `admin_note` on this resource. |
-| <a id="notepermissionsawardemoji"></a>`awardEmoji` | [`Boolean!`](#boolean) | Indicates the user can perform `award_emoji` on this resource. |
-| <a id="notepermissionscreatenote"></a>`createNote` | [`Boolean!`](#boolean) | Indicates the user can perform `create_note` on this resource. |
-| <a id="notepermissionsreadnote"></a>`readNote` | [`Boolean!`](#boolean) | Indicates the user can perform `read_note` on this resource. |
-| <a id="notepermissionsrepositionnote"></a>`repositionNote` | [`Boolean!`](#boolean) | Indicates the user can perform `reposition_note` on this resource. |
-| <a id="notepermissionsresolvenote"></a>`resolveNote` | [`Boolean!`](#boolean) | Indicates the user can perform `resolve_note` on this resource. |
+| <a id="notepermissionsadminnote"></a>`adminNote` | [`Boolean!`](#boolean) | If `true`, the user can perform `admin_note` on this resource. |
+| <a id="notepermissionsawardemoji"></a>`awardEmoji` | [`Boolean!`](#boolean) | If `true`, the user can perform `award_emoji` on this resource. |
+| <a id="notepermissionscreatenote"></a>`createNote` | [`Boolean!`](#boolean) | If `true`, the user can perform `create_note` on this resource. |
+| <a id="notepermissionsreadnote"></a>`readNote` | [`Boolean!`](#boolean) | If `true`, the user can perform `read_note` on this resource. |
+| <a id="notepermissionsrepositionnote"></a>`repositionNote` | [`Boolean!`](#boolean) | If `true`, the user can perform `reposition_note` on this resource. |
+| <a id="notepermissionsresolvenote"></a>`resolveNote` | [`Boolean!`](#boolean) | If `true`, the user can perform `resolve_note` on this resource. |
### `NugetDependencyLinkMetadata`
@@ -21638,6 +22200,7 @@ Active period time range for on-call rotation.
| <a id="organizationname"></a>`name` **{warning-solid}** | [`String!`](#string) | **Introduced** in 16.4. This feature is an Experiment. It can be changed or removed at any time. Name of the organization. |
| <a id="organizationorganizationusers"></a>`organizationUsers` **{warning-solid}** | [`OrganizationUserConnection!`](#organizationuserconnection) | **Introduced** in 16.4. This feature is an Experiment. It can be changed or removed at any time. Users with access to the organization. |
| <a id="organizationpath"></a>`path` **{warning-solid}** | [`String!`](#string) | **Introduced** in 16.4. This feature is an Experiment. It can be changed or removed at any time. Path of the organization. |
+| <a id="organizationweburl"></a>`webUrl` **{warning-solid}** | [`String!`](#string) | **Introduced** in 16.6. This feature is an Experiment. It can be changed or removed at any time. Web URL of the organization. |
#### Fields with arguments
@@ -21682,10 +22245,21 @@ A user with access to the organization.
| Name | Type | Description |
| ---- | ---- | ----------- |
-| <a id="organizationuserbadges"></a>`badges` **{warning-solid}** | [`[String!]`](#string) | **Introduced** in 16.4. This feature is an Experiment. It can be changed or removed at any time. Badges describing the user within the organization. |
+| <a id="organizationuserbadges"></a>`badges` **{warning-solid}** | [`[OrganizationUserBadge!]`](#organizationuserbadge) | **Introduced** in 16.4. This feature is an Experiment. It can be changed or removed at any time. Badges describing the user within the organization. |
| <a id="organizationuserid"></a>`id` **{warning-solid}** | [`ID!`](#id) | **Introduced** in 16.4. This feature is an Experiment. It can be changed or removed at any time. ID of the organization user. |
| <a id="organizationuseruser"></a>`user` **{warning-solid}** | [`UserCore!`](#usercore) | **Introduced** in 16.4. This feature is an Experiment. It can be changed or removed at any time. User that is associated with the organization. |
+### `OrganizationUserBadge`
+
+An organization user badge.
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="organizationuserbadgetext"></a>`text` | [`String!`](#string) | Badge text. |
+| <a id="organizationuserbadgevariant"></a>`variant` | [`String!`](#string) | Badge variant. |
+
### `Package`
Represents a package with pipelines in the Package Registry.
@@ -21695,7 +22269,7 @@ Represents a package with pipelines in the Package Registry.
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="package_links"></a>`_links` | [`PackageLinks!`](#packagelinks) | Map of links to perform actions on the package. |
-| <a id="packagecandestroy"></a>`canDestroy` | [`Boolean!`](#boolean) | Whether the user can destroy the package. |
+| <a id="packagecandestroy"></a>`canDestroy` **{warning-solid}** | [`Boolean!`](#boolean) | **Deprecated** in 16.6. Superseded by `user_permissions` field. See `Types::PermissionTypes::Package` type. |
| <a id="packagecreatedat"></a>`createdAt` | [`Time!`](#time) | Date of creation. |
| <a id="packageid"></a>`id` | [`PackagesPackageID!`](#packagespackageid) | ID of the package. |
| <a id="packagemetadata"></a>`metadata` | [`PackageMetadata`](#packagemetadata) | Package metadata. |
@@ -21707,6 +22281,7 @@ Represents a package with pipelines in the Package Registry.
| <a id="packagestatusmessage"></a>`statusMessage` | [`String`](#string) | Status message. |
| <a id="packagetags"></a>`tags` | [`PackageTagConnection`](#packagetagconnection) | Package tags. (see [Connections](#connections)) |
| <a id="packageupdatedat"></a>`updatedAt` | [`Time!`](#time) | Date of most recent update. |
+| <a id="packageuserpermissions"></a>`userPermissions` | [`PackagePermissions!`](#packagepermissions) | Permissions for the current user on the resource. |
| <a id="packageversion"></a>`version` | [`String`](#string) | Version string. |
### `PackageBase`
@@ -21718,7 +22293,7 @@ Represents a package in the Package Registry.
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="packagebase_links"></a>`_links` | [`PackageLinks!`](#packagelinks) | Map of links to perform actions on the package. |
-| <a id="packagebasecandestroy"></a>`canDestroy` | [`Boolean!`](#boolean) | Whether the user can destroy the package. |
+| <a id="packagebasecandestroy"></a>`canDestroy` **{warning-solid}** | [`Boolean!`](#boolean) | **Deprecated** in 16.6. Superseded by `user_permissions` field. See `Types::PermissionTypes::Package` type. |
| <a id="packagebasecreatedat"></a>`createdAt` | [`Time!`](#time) | Date of creation. |
| <a id="packagebaseid"></a>`id` | [`PackagesPackageID!`](#packagespackageid) | ID of the package. |
| <a id="packagebasemetadata"></a>`metadata` | [`PackageMetadata`](#packagemetadata) | Package metadata. |
@@ -21729,6 +22304,7 @@ Represents a package in the Package Registry.
| <a id="packagebasestatusmessage"></a>`statusMessage` | [`String`](#string) | Status message. |
| <a id="packagebasetags"></a>`tags` | [`PackageTagConnection`](#packagetagconnection) | Package tags. (see [Connections](#connections)) |
| <a id="packagebaseupdatedat"></a>`updatedAt` | [`Time!`](#time) | Date of most recent update. |
+| <a id="packagebaseuserpermissions"></a>`userPermissions` | [`PackagePermissions!`](#packagepermissions) | Permissions for the current user on the resource. |
| <a id="packagebaseversion"></a>`version` | [`String`](#string) | Version string. |
### `PackageComposerJsonType`
@@ -21778,7 +22354,7 @@ Represents a package details in the Package Registry.
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="packagedetailstype_links"></a>`_links` | [`PackageLinks!`](#packagelinks) | Map of links to perform actions on the package. |
-| <a id="packagedetailstypecandestroy"></a>`canDestroy` | [`Boolean!`](#boolean) | Whether the user can destroy the package. |
+| <a id="packagedetailstypecandestroy"></a>`canDestroy` **{warning-solid}** | [`Boolean!`](#boolean) | **Deprecated** in 16.6. Superseded by `user_permissions` field. See `Types::PermissionTypes::Package` type. |
| <a id="packagedetailstypecomposerconfigrepositoryurl"></a>`composerConfigRepositoryUrl` | [`String`](#string) | Url of the Composer setup endpoint. |
| <a id="packagedetailstypecomposerurl"></a>`composerUrl` | [`String`](#string) | Url of the Composer endpoint. |
| <a id="packagedetailstypeconanurl"></a>`conanUrl` | [`String`](#string) | Url of the Conan project endpoint. |
@@ -21802,6 +22378,7 @@ Represents a package details in the Package Registry.
| <a id="packagedetailstypestatusmessage"></a>`statusMessage` | [`String`](#string) | Status message. |
| <a id="packagedetailstypetags"></a>`tags` | [`PackageTagConnection`](#packagetagconnection) | Package tags. (see [Connections](#connections)) |
| <a id="packagedetailstypeupdatedat"></a>`updatedAt` | [`Time!`](#time) | Date of most recent update. |
+| <a id="packagedetailstypeuserpermissions"></a>`userPermissions` | [`PackagePermissions!`](#packagepermissions) | Permissions for the current user on the resource. |
| <a id="packagedetailstypeversion"></a>`version` | [`String`](#string) | Version string. |
| <a id="packagedetailstypeversions"></a>`versions` | [`PackageBaseConnection`](#packagebaseconnection) | Other versions of the package. (see [Connections](#connections)) |
@@ -21913,6 +22490,14 @@ Represents links to perform actions on the package.
| ---- | ---- | ----------- |
| <a id="packagelinkswebpath"></a>`webPath` | [`String`](#string) | Path to the package details page. |
+### `PackagePermissions`
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="packagepermissionsdestroypackage"></a>`destroyPackage` | [`Boolean!`](#boolean) | If `true`, the user can perform `destroy_package` on this resource. |
+
### `PackageSettings`
Namespace-level Package Registry settings.
@@ -21932,8 +22517,8 @@ Namespace-level Package Registry settings.
| <a id="packagesettingsmavenpackagerequestsforwardinglocked"></a>`mavenPackageRequestsForwardingLocked` | [`Boolean!`](#boolean) | Indicates whether Maven package forwarding settings are locked by a parent namespace. |
| <a id="packagesettingsnpmpackagerequestsforwarding"></a>`npmPackageRequestsForwarding` | [`Boolean`](#boolean) | Indicates whether npm package forwarding is allowed for this namespace. |
| <a id="packagesettingsnpmpackagerequestsforwardinglocked"></a>`npmPackageRequestsForwardingLocked` | [`Boolean!`](#boolean) | Indicates whether npm package forwarding settings are locked by a parent namespace. |
-| <a id="packagesettingsnugetduplicateexceptionregex"></a>`nugetDuplicateExceptionRegex` | [`UntrustedRegexp`](#untrustedregexp) | When nuget_duplicates_allowed is false, you can publish duplicate packages with names that match this regex. Otherwise, this setting has no effect. Error is raised if `nuget_duplicates_option` feature flag is disabled. |
-| <a id="packagesettingsnugetduplicatesallowed"></a>`nugetDuplicatesAllowed` | [`Boolean!`](#boolean) | Indicates whether duplicate NuGet packages are allowed for this namespace. Error is raised if `nuget_duplicates_option` feature flag is disabled. |
+| <a id="packagesettingsnugetduplicateexceptionregex"></a>`nugetDuplicateExceptionRegex` | [`UntrustedRegexp`](#untrustedregexp) | When nuget_duplicates_allowed is false, you can publish duplicate packages with names that match this regex. Otherwise, this setting has no effect. |
+| <a id="packagesettingsnugetduplicatesallowed"></a>`nugetDuplicatesAllowed` | [`Boolean!`](#boolean) | Indicates whether duplicate NuGet packages are allowed for this namespace. |
| <a id="packagesettingspypipackagerequestsforwarding"></a>`pypiPackageRequestsForwarding` | [`Boolean`](#boolean) | Indicates whether PyPI package forwarding is allowed for this namespace. |
| <a id="packagesettingspypipackagerequestsforwardinglocked"></a>`pypiPackageRequestsForwardingLocked` | [`Boolean!`](#boolean) | Indicates whether PyPI package forwarding settings are locked by a parent namespace. |
@@ -21969,6 +22554,7 @@ A packages protection rule designed to protect packages from being pushed by use
| Name | Type | Description |
| ---- | ---- | ----------- |
+| <a id="packagesprotectionruleid"></a>`id` | [`PackagesProtectionRuleID!`](#packagesprotectionruleid) | ID of the package protection rule. |
| <a id="packagesprotectionrulepackagenamepattern"></a>`packageNamePattern` | [`String!`](#string) | Package name protected by the protection rule. For example `@my-scope/my-package-*`. Wildcard character `*` allowed. |
| <a id="packagesprotectionrulepackagetype"></a>`packageType` | [`PackagesProtectionRulePackageType!`](#packagesprotectionrulepackagetype) | Package type protected by the protection rule. For example `NPM`. |
| <a id="packagesprotectionrulepushprotecteduptoaccesslevel"></a>`pushProtectedUpToAccessLevel` | [`PackagesProtectionRuleAccessLevel!`](#packagesprotectionruleaccesslevel) | Max GitLab access level unable to push a package. For example `DEVELOPER`, `MAINTAINER`, `OWNER`. |
@@ -22022,6 +22608,46 @@ Represents a file or directory in the project repository that has been locked.
| <a id="pathlockpath"></a>`path` | [`String`](#string) | Locked path. |
| <a id="pathlockuser"></a>`user` | [`UserCore`](#usercore) | User that has locked this path. |
+### `PendingGroupMember`
+
+Represents a Pending Group Membership.
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="pendinggroupmemberaccesslevel"></a>`accessLevel` | [`AccessLevel`](#accesslevel) | GitLab::Access level. |
+| <a id="pendinggroupmemberapproved"></a>`approved` | [`Boolean`](#boolean) | Whether the pending group member has been approved. |
+| <a id="pendinggroupmemberavatarurl"></a>`avatarUrl` | [`String`](#string) | URL to avatar image file of the pending group member. |
+| <a id="pendinggroupmembercreatedat"></a>`createdAt` | [`Time`](#time) | Date and time the membership was created. |
+| <a id="pendinggroupmembercreatedby"></a>`createdBy` | [`UserCore`](#usercore) | User that authorized membership. |
+| <a id="pendinggroupmemberemail"></a>`email` | [`String`](#string) | Public email of the pending group member. |
+| <a id="pendinggroupmemberexpiresat"></a>`expiresAt` | [`Time`](#time) | Date and time the membership expires. |
+| <a id="pendinggroupmembergroup"></a>`group` | [`Group`](#group) | Group that a user is a member of. |
+| <a id="pendinggroupmemberid"></a>`id` | [`ID!`](#id) | ID of the member. |
+| <a id="pendinggroupmemberinvited"></a>`invited` | [`Boolean`](#boolean) | Whether the pending group member has been invited. |
+| <a id="pendinggroupmembername"></a>`name` | [`String`](#string) | Name of the pending group member. |
+| <a id="pendinggroupmembernotificationemail"></a>`notificationEmail` | [`String`](#string) | Group notification email for user. Only available for admins. |
+| <a id="pendinggroupmemberupdatedat"></a>`updatedAt` | [`Time`](#time) | Date and time the membership was last updated. |
+| <a id="pendinggroupmemberuser"></a>`user` | [`UserCore`](#usercore) | User that is associated with the member object. |
+| <a id="pendinggroupmemberuserpermissions"></a>`userPermissions` | [`GroupPermissions!`](#grouppermissions) | Permissions for the current user on the resource. |
+| <a id="pendinggroupmemberusername"></a>`username` | [`String`](#string) | Username of the pending group member. |
+| <a id="pendinggroupmemberweburl"></a>`webUrl` | [`String`](#string) | Web URL of the pending group member. |
+
+#### Fields with arguments
+
+##### `PendingGroupMember.mergeRequestInteraction`
+
+Find a merge request.
+
+Returns [`UserMergeRequestInteraction`](#usermergerequestinteraction).
+
+###### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="pendinggroupmembermergerequestinteractionid"></a>`id` | [`MergeRequestID!`](#mergerequestid) | Global ID of the merge request. |
+
### `Pipeline`
#### Fields
@@ -22067,7 +22693,7 @@ Represents a file or directory in the project repository that has been locked.
| <a id="pipelinesourcejob"></a>`sourceJob` | [`CiJob`](#cijob) | Job where pipeline was triggered from. |
| <a id="pipelinestages"></a>`stages` | [`CiStageConnection`](#cistageconnection) | Stages of the pipeline. (see [Connections](#connections)) |
| <a id="pipelinestartedat"></a>`startedAt` | [`Time`](#time) | Timestamp when the pipeline was started. |
-| <a id="pipelinestatus"></a>`status` | [`PipelineStatusEnum!`](#pipelinestatusenum) | Status of the pipeline (CREATED, WAITING_FOR_RESOURCE, PREPARING, PENDING, RUNNING, FAILED, SUCCESS, CANCELED, SKIPPED, MANUAL, SCHEDULED). |
+| <a id="pipelinestatus"></a>`status` | [`PipelineStatusEnum!`](#pipelinestatusenum) | Status of the pipeline (CREATED, WAITING_FOR_RESOURCE, PREPARING, WAITING_FOR_CALLBACK, PENDING, RUNNING, FAILED, SUCCESS, CANCELED, SKIPPED, MANUAL, SCHEDULED). |
| <a id="pipelinestuck"></a>`stuck` | [`Boolean!`](#boolean) | If the pipeline is stuck. |
| <a id="pipelinetestreportsummary"></a>`testReportSummary` | [`TestReportSummary!`](#testreportsummary) | Summary of the test report generated by the pipeline. |
| <a id="pipelinetotaljobs"></a>`totalJobs` | [`Int!`](#int) | The total number of jobs in the pipeline. |
@@ -22240,9 +22866,10 @@ Represents pipeline counts for the project.
| Name | Type | Description |
| ---- | ---- | ----------- |
-| <a id="pipelinepermissionsadminpipeline"></a>`adminPipeline` | [`Boolean!`](#boolean) | Indicates the user can perform `admin_pipeline` on this resource. |
-| <a id="pipelinepermissionsdestroypipeline"></a>`destroyPipeline` | [`Boolean!`](#boolean) | Indicates the user can perform `destroy_pipeline` on this resource. |
-| <a id="pipelinepermissionsupdatepipeline"></a>`updatePipeline` | [`Boolean!`](#boolean) | Indicates the user can perform `update_pipeline` on this resource. |
+| <a id="pipelinepermissionsadminpipeline"></a>`adminPipeline` | [`Boolean!`](#boolean) | If `true`, the user can perform `admin_pipeline` on this resource. |
+| <a id="pipelinepermissionscancelpipeline"></a>`cancelPipeline` | [`Boolean!`](#boolean) | If `true`, the user can perform `cancel_pipeline` on this resource. |
+| <a id="pipelinepermissionsdestroypipeline"></a>`destroyPipeline` | [`Boolean!`](#boolean) | If `true`, the user can perform `destroy_pipeline` on this resource. |
+| <a id="pipelinepermissionsupdatepipeline"></a>`updatePipeline` | [`Boolean!`](#boolean) | If `true`, the user can perform `update_pipeline` on this resource. |
### `PipelineSchedule`
@@ -22278,10 +22905,10 @@ Represents a pipeline schedule.
| Name | Type | Description |
| ---- | ---- | ----------- |
-| <a id="pipelineschedulepermissionsadminpipelineschedule"></a>`adminPipelineSchedule` | [`Boolean!`](#boolean) | Indicates the user can perform `admin_pipeline_schedule` on this resource. |
-| <a id="pipelineschedulepermissionsplaypipelineschedule"></a>`playPipelineSchedule` | [`Boolean!`](#boolean) | Indicates the user can perform `play_pipeline_schedule` on this resource. |
+| <a id="pipelineschedulepermissionsadminpipelineschedule"></a>`adminPipelineSchedule` | [`Boolean!`](#boolean) | If `true`, the user can perform `admin_pipeline_schedule` on this resource. |
+| <a id="pipelineschedulepermissionsplaypipelineschedule"></a>`playPipelineSchedule` | [`Boolean!`](#boolean) | If `true`, the user can perform `play_pipeline_schedule` on this resource. |
| <a id="pipelineschedulepermissionstakeownershippipelineschedule"></a>`takeOwnershipPipelineSchedule` **{warning-solid}** | [`Boolean!`](#boolean) | **Deprecated** in 15.9. Use admin_pipeline_schedule permission to determine if the user can take ownership of a pipeline schedule. |
-| <a id="pipelineschedulepermissionsupdatepipelineschedule"></a>`updatePipelineSchedule` | [`Boolean!`](#boolean) | Indicates the user can perform `update_pipeline_schedule` on this resource. |
+| <a id="pipelineschedulepermissionsupdatepipelineschedule"></a>`updatePipelineSchedule` | [`Boolean!`](#boolean) | If `true`, the user can perform `update_pipeline_schedule` on this resource. |
### `PipelineScheduleVariable`
@@ -22396,6 +23023,7 @@ Represents vulnerability finding of a security report on the pipeline.
| <a id="projectdependencyproxypackagessetting"></a>`dependencyProxyPackagesSetting` **{warning-solid}** | [`DependencyProxyPackagesSetting`](#dependencyproxypackagessetting) | **Introduced** in 16.5. This feature is an Experiment. It can be changed or removed at any time. Packages Dependency Proxy settings for the project. Requires the packages and dependency proxy to be enabled in the config. Requires the packages feature to be enabled at the project level. Returns `null` if `packages_dependency_proxy_maven` feature flag is disabled. |
| <a id="projectdescription"></a>`description` | [`String`](#string) | Short description of the project. |
| <a id="projectdescriptionhtml"></a>`descriptionHtml` | [`String`](#string) | GitLab Flavored Markdown rendering of `description`. |
+| <a id="projectdetailedimportstatus"></a>`detailedImportStatus` | [`DetailedImportStatus`](#detailedimportstatus) | Detailed import status of the project. |
| <a id="projectdora"></a>`dora` | [`Dora`](#dora) | Project's DORA metrics. |
| <a id="projectflowmetrics"></a>`flowMetrics` **{warning-solid}** | [`ProjectValueStreamAnalyticsFlowMetrics`](#projectvaluestreamanalyticsflowmetrics) | **Introduced** in 15.10. This feature is an Experiment. It can be changed or removed at any time. Flow metrics for value stream analytics. |
| <a id="projectforkscount"></a>`forksCount` | [`Int!`](#int) | Number of times the project has been forked. |
@@ -23291,6 +23919,26 @@ four standard [pagination arguments](#connection-pagination-arguments):
| <a id="projectlabelsincludeancestorgroups"></a>`includeAncestorGroups` | [`Boolean`](#boolean) | Include labels from ancestor groups. |
| <a id="projectlabelssearchterm"></a>`searchTerm` | [`String`](#string) | Search term to find labels with. |
+##### `Project.memberRoles`
+
+Member roles available for the group.
+
+WARNING:
+**Introduced** in 16.5.
+This feature is an Experiment. It can be changed or removed at any time.
+
+Returns [`MemberRoleConnection`](#memberroleconnection).
+
+This field returns a [connection](#connections). It accepts the
+four standard [pagination arguments](#connection-pagination-arguments):
+`before: String`, `after: String`, `first: Int`, `last: Int`.
+
+###### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="projectmemberrolesid"></a>`id` | [`MemberRoleID`](#memberroleid) | Global ID of the member role to look up. |
+
##### `Project.mergeRequest`
A single merge request of the project.
@@ -23416,6 +24064,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| <a id="projectpackagesincludeversionless"></a>`includeVersionless` | [`Boolean`](#boolean) | Include versionless packages. |
| <a id="projectpackagespackagename"></a>`packageName` | [`String`](#string) | Search a package by name. |
| <a id="projectpackagespackagetype"></a>`packageType` | [`PackageTypeEnum`](#packagetypeenum) | Filter a package by type. |
+| <a id="projectpackagespackageversion"></a>`packageVersion` | [`String`](#string) | Filter a package by version. If used in combination with `include_versionless`, then no versionless packages are returned. |
| <a id="projectpackagessort"></a>`sort` | [`PackageSort`](#packagesort) | Sort packages by this criteria. |
| <a id="projectpackagesstatus"></a>`status` | [`PackageStatus`](#packagestatus) | Filter a package by status. |
@@ -23604,6 +24253,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="projectrunnersactive"></a>`active` **{warning-solid}** | [`Boolean`](#boolean) | **Deprecated** in 14.8. This was renamed. Use: `paused`. |
+| <a id="projectrunnerscreatorid"></a>`creatorId` | [`UserID`](#userid) | Filter runners by creator ID. |
| <a id="projectrunnerspaused"></a>`paused` | [`Boolean`](#boolean) | Filter runners by `paused` (true) or `active` (false) status. |
| <a id="projectrunnerssearch"></a>`search` | [`String`](#string) | Filter by full token or partial text in description field. |
| <a id="projectrunnerssort"></a>`sort` | [`CiRunnerSort`](#cirunnersort) | Sort order of results. |
@@ -23611,6 +24261,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| <a id="projectrunnerstaglist"></a>`tagList` | [`[String!]`](#string) | Filter by tags associated with the runner (comma-separated or array). |
| <a id="projectrunnerstype"></a>`type` | [`CiRunnerType`](#cirunnertype) | Filter runners by type. |
| <a id="projectrunnersupgradestatus"></a>`upgradeStatus` | [`CiRunnerUpgradeStatus`](#cirunnerupgradestatus) | Filter by upgrade status. |
+| <a id="projectrunnersversionprefix"></a>`versionPrefix` **{warning-solid}** | [`String`](#string) | **Introduced** in 16.6. This feature is an Experiment. It can be changed or removed at any time. Filter runners by version. Runners that contain runner managers with the version at the start of the search term are returned. For example, the search term '14.' returns runner managers with versions '14.11.1' and '14.2.3'. |
##### `Project.scanExecutionPolicies`
@@ -23960,50 +24611,50 @@ Returns [`UserMergeRequestInteraction`](#usermergerequestinteraction).
| Name | Type | Description |
| ---- | ---- | ----------- |
-| <a id="projectpermissionsadminoperations"></a>`adminOperations` | [`Boolean!`](#boolean) | Indicates the user can perform `admin_operations` on this resource. |
-| <a id="projectpermissionsadminpathlocks"></a>`adminPathLocks` | [`Boolean!`](#boolean) | Indicates the user can perform `admin_path_locks` on this resource. |
-| <a id="projectpermissionsadminproject"></a>`adminProject` | [`Boolean!`](#boolean) | Indicates the user can perform `admin_project` on this resource. |
-| <a id="projectpermissionsadminremotemirror"></a>`adminRemoteMirror` | [`Boolean!`](#boolean) | Indicates the user can perform `admin_remote_mirror` on this resource. |
-| <a id="projectpermissionsadminwiki"></a>`adminWiki` | [`Boolean!`](#boolean) | Indicates the user can perform `admin_wiki` on this resource. |
-| <a id="projectpermissionsarchiveproject"></a>`archiveProject` | [`Boolean!`](#boolean) | Indicates the user can perform `archive_project` on this resource. |
-| <a id="projectpermissionschangenamespace"></a>`changeNamespace` | [`Boolean!`](#boolean) | Indicates the user can perform `change_namespace` on this resource. |
-| <a id="projectpermissionschangevisibilitylevel"></a>`changeVisibilityLevel` | [`Boolean!`](#boolean) | Indicates the user can perform `change_visibility_level` on this resource. |
-| <a id="projectpermissionscreatedeployment"></a>`createDeployment` | [`Boolean!`](#boolean) | Indicates the user can perform `create_deployment` on this resource. |
-| <a id="projectpermissionscreatedesign"></a>`createDesign` | [`Boolean!`](#boolean) | Indicates the user can perform `create_design` on this resource. |
-| <a id="projectpermissionscreateissue"></a>`createIssue` | [`Boolean!`](#boolean) | Indicates the user can perform `create_issue` on this resource. |
-| <a id="projectpermissionscreatelabel"></a>`createLabel` | [`Boolean!`](#boolean) | Indicates the user can perform `create_label` on this resource. |
-| <a id="projectpermissionscreatemergerequestfrom"></a>`createMergeRequestFrom` | [`Boolean!`](#boolean) | Indicates the user can perform `create_merge_request_from` on this resource. |
-| <a id="projectpermissionscreatemergerequestin"></a>`createMergeRequestIn` | [`Boolean!`](#boolean) | Indicates the user can perform `create_merge_request_in` on this resource. |
-| <a id="projectpermissionscreatepages"></a>`createPages` | [`Boolean!`](#boolean) | Indicates the user can perform `create_pages` on this resource. |
-| <a id="projectpermissionscreatepipeline"></a>`createPipeline` | [`Boolean!`](#boolean) | Indicates the user can perform `create_pipeline` on this resource. |
-| <a id="projectpermissionscreatepipelineschedule"></a>`createPipelineSchedule` | [`Boolean!`](#boolean) | Indicates the user can perform `create_pipeline_schedule` on this resource. |
-| <a id="projectpermissionscreatesnippet"></a>`createSnippet` | [`Boolean!`](#boolean) | Indicates the user can perform `create_snippet` on this resource. |
-| <a id="projectpermissionscreatewiki"></a>`createWiki` | [`Boolean!`](#boolean) | Indicates the user can perform `create_wiki` on this resource. |
-| <a id="projectpermissionsdestroydesign"></a>`destroyDesign` | [`Boolean!`](#boolean) | Indicates the user can perform `destroy_design` on this resource. |
-| <a id="projectpermissionsdestroypages"></a>`destroyPages` | [`Boolean!`](#boolean) | Indicates the user can perform `destroy_pages` on this resource. |
-| <a id="projectpermissionsdestroywiki"></a>`destroyWiki` | [`Boolean!`](#boolean) | Indicates the user can perform `destroy_wiki` on this resource. |
-| <a id="projectpermissionsdownloadcode"></a>`downloadCode` | [`Boolean!`](#boolean) | Indicates the user can perform `download_code` on this resource. |
-| <a id="projectpermissionsdownloadwikicode"></a>`downloadWikiCode` | [`Boolean!`](#boolean) | Indicates the user can perform `download_wiki_code` on this resource. |
-| <a id="projectpermissionsforkproject"></a>`forkProject` | [`Boolean!`](#boolean) | Indicates the user can perform `fork_project` on this resource. |
-| <a id="projectpermissionspushcode"></a>`pushCode` | [`Boolean!`](#boolean) | Indicates the user can perform `push_code` on this resource. |
-| <a id="projectpermissionspushtodeleteprotectedbranch"></a>`pushToDeleteProtectedBranch` | [`Boolean!`](#boolean) | Indicates the user can perform `push_to_delete_protected_branch` on this resource. |
-| <a id="projectpermissionsreadcommitstatus"></a>`readCommitStatus` | [`Boolean!`](#boolean) | Indicates the user can perform `read_commit_status` on this resource. |
-| <a id="projectpermissionsreadcycleanalytics"></a>`readCycleAnalytics` | [`Boolean!`](#boolean) | Indicates the user can perform `read_cycle_analytics` on this resource. |
-| <a id="projectpermissionsreaddesign"></a>`readDesign` | [`Boolean!`](#boolean) | Indicates the user can perform `read_design` on this resource. |
-| <a id="projectpermissionsreadenvironment"></a>`readEnvironment` | [`Boolean!`](#boolean) | Indicates the user can perform `read_environment` on this resource. |
-| <a id="projectpermissionsreadmergerequest"></a>`readMergeRequest` | [`Boolean!`](#boolean) | Indicates the user can perform `read_merge_request` on this resource. |
-| <a id="projectpermissionsreadpagescontent"></a>`readPagesContent` | [`Boolean!`](#boolean) | Indicates the user can perform `read_pages_content` on this resource. |
-| <a id="projectpermissionsreadproject"></a>`readProject` | [`Boolean!`](#boolean) | Indicates the user can perform `read_project` on this resource. |
-| <a id="projectpermissionsreadprojectmember"></a>`readProjectMember` | [`Boolean!`](#boolean) | Indicates the user can perform `read_project_member` on this resource. |
-| <a id="projectpermissionsreadwiki"></a>`readWiki` | [`Boolean!`](#boolean) | Indicates the user can perform `read_wiki` on this resource. |
-| <a id="projectpermissionsremoveforkproject"></a>`removeForkProject` | [`Boolean!`](#boolean) | Indicates the user can perform `remove_fork_project` on this resource. |
-| <a id="projectpermissionsremovepages"></a>`removePages` | [`Boolean!`](#boolean) | Indicates the user can perform `remove_pages` on this resource. |
-| <a id="projectpermissionsremoveproject"></a>`removeProject` | [`Boolean!`](#boolean) | Indicates the user can perform `remove_project` on this resource. |
-| <a id="projectpermissionsrenameproject"></a>`renameProject` | [`Boolean!`](#boolean) | Indicates the user can perform `rename_project` on this resource. |
-| <a id="projectpermissionsrequestaccess"></a>`requestAccess` | [`Boolean!`](#boolean) | Indicates the user can perform `request_access` on this resource. |
-| <a id="projectpermissionsupdatepages"></a>`updatePages` | [`Boolean!`](#boolean) | Indicates the user can perform `update_pages` on this resource. |
-| <a id="projectpermissionsupdatewiki"></a>`updateWiki` | [`Boolean!`](#boolean) | Indicates the user can perform `update_wiki` on this resource. |
-| <a id="projectpermissionsuploadfile"></a>`uploadFile` | [`Boolean!`](#boolean) | Indicates the user can perform `upload_file` on this resource. |
+| <a id="projectpermissionsadminoperations"></a>`adminOperations` | [`Boolean!`](#boolean) | If `true`, the user can perform `admin_operations` on this resource. |
+| <a id="projectpermissionsadminpathlocks"></a>`adminPathLocks` | [`Boolean!`](#boolean) | If `true`, the user can perform `admin_path_locks` on this resource. |
+| <a id="projectpermissionsadminproject"></a>`adminProject` | [`Boolean!`](#boolean) | If `true`, the user can perform `admin_project` on this resource. |
+| <a id="projectpermissionsadminremotemirror"></a>`adminRemoteMirror` | [`Boolean!`](#boolean) | If `true`, the user can perform `admin_remote_mirror` on this resource. |
+| <a id="projectpermissionsadminwiki"></a>`adminWiki` | [`Boolean!`](#boolean) | If `true`, the user can perform `admin_wiki` on this resource. |
+| <a id="projectpermissionsarchiveproject"></a>`archiveProject` | [`Boolean!`](#boolean) | If `true`, the user can perform `archive_project` on this resource. |
+| <a id="projectpermissionschangenamespace"></a>`changeNamespace` | [`Boolean!`](#boolean) | If `true`, the user can perform `change_namespace` on this resource. |
+| <a id="projectpermissionschangevisibilitylevel"></a>`changeVisibilityLevel` | [`Boolean!`](#boolean) | If `true`, the user can perform `change_visibility_level` on this resource. |
+| <a id="projectpermissionscreatedeployment"></a>`createDeployment` | [`Boolean!`](#boolean) | If `true`, the user can perform `create_deployment` on this resource. |
+| <a id="projectpermissionscreatedesign"></a>`createDesign` | [`Boolean!`](#boolean) | If `true`, the user can perform `create_design` on this resource. |
+| <a id="projectpermissionscreateissue"></a>`createIssue` | [`Boolean!`](#boolean) | If `true`, the user can perform `create_issue` on this resource. |
+| <a id="projectpermissionscreatelabel"></a>`createLabel` | [`Boolean!`](#boolean) | If `true`, the user can perform `create_label` on this resource. |
+| <a id="projectpermissionscreatemergerequestfrom"></a>`createMergeRequestFrom` | [`Boolean!`](#boolean) | If `true`, the user can perform `create_merge_request_from` on this resource. |
+| <a id="projectpermissionscreatemergerequestin"></a>`createMergeRequestIn` | [`Boolean!`](#boolean) | If `true`, the user can perform `create_merge_request_in` on this resource. |
+| <a id="projectpermissionscreatepages"></a>`createPages` | [`Boolean!`](#boolean) | If `true`, the user can perform `create_pages` on this resource. |
+| <a id="projectpermissionscreatepipeline"></a>`createPipeline` | [`Boolean!`](#boolean) | If `true`, the user can perform `create_pipeline` on this resource. |
+| <a id="projectpermissionscreatepipelineschedule"></a>`createPipelineSchedule` | [`Boolean!`](#boolean) | If `true`, the user can perform `create_pipeline_schedule` on this resource. |
+| <a id="projectpermissionscreatesnippet"></a>`createSnippet` | [`Boolean!`](#boolean) | If `true`, the user can perform `create_snippet` on this resource. |
+| <a id="projectpermissionscreatewiki"></a>`createWiki` | [`Boolean!`](#boolean) | If `true`, the user can perform `create_wiki` on this resource. |
+| <a id="projectpermissionsdestroydesign"></a>`destroyDesign` | [`Boolean!`](#boolean) | If `true`, the user can perform `destroy_design` on this resource. |
+| <a id="projectpermissionsdestroypages"></a>`destroyPages` | [`Boolean!`](#boolean) | If `true`, the user can perform `destroy_pages` on this resource. |
+| <a id="projectpermissionsdestroywiki"></a>`destroyWiki` | [`Boolean!`](#boolean) | If `true`, the user can perform `destroy_wiki` on this resource. |
+| <a id="projectpermissionsdownloadcode"></a>`downloadCode` | [`Boolean!`](#boolean) | If `true`, the user can perform `download_code` on this resource. |
+| <a id="projectpermissionsdownloadwikicode"></a>`downloadWikiCode` | [`Boolean!`](#boolean) | If `true`, the user can perform `download_wiki_code` on this resource. |
+| <a id="projectpermissionsforkproject"></a>`forkProject` | [`Boolean!`](#boolean) | If `true`, the user can perform `fork_project` on this resource. |
+| <a id="projectpermissionspushcode"></a>`pushCode` | [`Boolean!`](#boolean) | If `true`, the user can perform `push_code` on this resource. |
+| <a id="projectpermissionspushtodeleteprotectedbranch"></a>`pushToDeleteProtectedBranch` | [`Boolean!`](#boolean) | If `true`, the user can perform `push_to_delete_protected_branch` on this resource. |
+| <a id="projectpermissionsreadcommitstatus"></a>`readCommitStatus` | [`Boolean!`](#boolean) | If `true`, the user can perform `read_commit_status` on this resource. |
+| <a id="projectpermissionsreadcycleanalytics"></a>`readCycleAnalytics` | [`Boolean!`](#boolean) | If `true`, the user can perform `read_cycle_analytics` on this resource. |
+| <a id="projectpermissionsreaddesign"></a>`readDesign` | [`Boolean!`](#boolean) | If `true`, the user can perform `read_design` on this resource. |
+| <a id="projectpermissionsreadenvironment"></a>`readEnvironment` | [`Boolean!`](#boolean) | If `true`, the user can perform `read_environment` on this resource. |
+| <a id="projectpermissionsreadmergerequest"></a>`readMergeRequest` | [`Boolean!`](#boolean) | If `true`, the user can perform `read_merge_request` on this resource. |
+| <a id="projectpermissionsreadpagescontent"></a>`readPagesContent` | [`Boolean!`](#boolean) | If `true`, the user can perform `read_pages_content` on this resource. |
+| <a id="projectpermissionsreadproject"></a>`readProject` | [`Boolean!`](#boolean) | If `true`, the user can perform `read_project` on this resource. |
+| <a id="projectpermissionsreadprojectmember"></a>`readProjectMember` | [`Boolean!`](#boolean) | If `true`, the user can perform `read_project_member` on this resource. |
+| <a id="projectpermissionsreadwiki"></a>`readWiki` | [`Boolean!`](#boolean) | If `true`, the user can perform `read_wiki` on this resource. |
+| <a id="projectpermissionsremoveforkproject"></a>`removeForkProject` | [`Boolean!`](#boolean) | If `true`, the user can perform `remove_fork_project` on this resource. |
+| <a id="projectpermissionsremovepages"></a>`removePages` | [`Boolean!`](#boolean) | If `true`, the user can perform `remove_pages` on this resource. |
+| <a id="projectpermissionsremoveproject"></a>`removeProject` | [`Boolean!`](#boolean) | If `true`, the user can perform `remove_project` on this resource. |
+| <a id="projectpermissionsrenameproject"></a>`renameProject` | [`Boolean!`](#boolean) | If `true`, the user can perform `rename_project` on this resource. |
+| <a id="projectpermissionsrequestaccess"></a>`requestAccess` | [`Boolean!`](#boolean) | If `true`, the user can perform `request_access` on this resource. |
+| <a id="projectpermissionsupdatepages"></a>`updatePages` | [`Boolean!`](#boolean) | If `true`, the user can perform `update_pages` on this resource. |
+| <a id="projectpermissionsupdatewiki"></a>`updateWiki` | [`Boolean!`](#boolean) | If `true`, the user can perform `update_wiki` on this resource. |
+| <a id="projectpermissionsuploadfile"></a>`uploadFile` | [`Boolean!`](#boolean) | If `true`, the user can perform `upload_file` on this resource. |
### `ProjectRepositoryRegistry`
@@ -24062,7 +24713,13 @@ Represents the source of a security policy belonging to a project.
| <a id="projectstatisticsbuildartifactssize"></a>`buildArtifactsSize` | [`Float!`](#float) | Build artifacts size of the project in bytes. |
| <a id="projectstatisticscommitcount"></a>`commitCount` | [`Float!`](#float) | Commit count of the project. |
| <a id="projectstatisticscontainerregistrysize"></a>`containerRegistrySize` | [`Float`](#float) | Container Registry size of the project in bytes. |
+| <a id="projectstatisticscostfactoredbuildartifactssize"></a>`costFactoredBuildArtifactsSize` **{warning-solid}** | [`Float!`](#float) | **Introduced** in 16.6. This feature is an Experiment. It can be changed or removed at any time. Build artifacts size in bytes with any applicable cost factor for forks applied. This will equal build_artifacts_size if there is no applicable cost factor. |
+| <a id="projectstatisticscostfactoredlfsobjectssize"></a>`costFactoredLfsObjectsSize` **{warning-solid}** | [`Float!`](#float) | **Introduced** in 16.6. This feature is an Experiment. It can be changed or removed at any time. LFS objects size in bytes with any applicable cost factor for forks applied. This will equal lfs_objects_size if there is no applicable cost factor. |
+| <a id="projectstatisticscostfactoredpackagessize"></a>`costFactoredPackagesSize` **{warning-solid}** | [`Float!`](#float) | **Introduced** in 16.6. This feature is an Experiment. It can be changed or removed at any time. Packages size in bytes with any applicable cost factor for forks applied. This will equal packages_size if there is no applicable cost factor. |
+| <a id="projectstatisticscostfactoredrepositorysize"></a>`costFactoredRepositorySize` **{warning-solid}** | [`Float!`](#float) | **Introduced** in 16.6. This feature is an Experiment. It can be changed or removed at any time. Repository size in bytes with any applicable cost factor for forks applied. This will equal repository_size if there is no applicable cost factor. |
+| <a id="projectstatisticscostfactoredsnippetssize"></a>`costFactoredSnippetsSize` **{warning-solid}** | [`Float!`](#float) | **Introduced** in 16.6. This feature is an Experiment. It can be changed or removed at any time. Snippets size in bytes with any applicable cost factor for forks applied. This will equal snippets_size if there is no applicable cost factor. |
| <a id="projectstatisticscostfactoredstoragesize"></a>`costFactoredStorageSize` **{warning-solid}** | [`Float!`](#float) | **Introduced** in 16.2. This feature is an Experiment. It can be changed or removed at any time. Storage size in bytes with any applicable cost factor for forks applied. This will equal storage_size if there is no applicable cost factor. |
+| <a id="projectstatisticscostfactoredwikisize"></a>`costFactoredWikiSize` **{warning-solid}** | [`Float!`](#float) | **Introduced** in 16.6. This feature is an Experiment. It can be changed or removed at any time. Wiki size in bytes with any applicable cost factor for forks applied. This will equal wiki_size if there is no applicable cost factor. |
| <a id="projectstatisticslfsobjectssize"></a>`lfsObjectsSize` | [`Float!`](#float) | Large File Storage (LFS) object size of the project in bytes. |
| <a id="projectstatisticspackagessize"></a>`packagesSize` | [`Float!`](#float) | Packages size of the project in bytes. |
| <a id="projectstatisticspipelineartifactssize"></a>`pipelineArtifactsSize` | [`Float`](#float) | CI Pipeline artifacts size in bytes. |
@@ -24316,8 +24973,14 @@ Pypi metadata.
| Name | Type | Description |
| ---- | ---- | ----------- |
+| <a id="pypimetadataauthoremail"></a>`authorEmail` | [`String`](#string) | Author email address(es) in RFC-822 format. |
+| <a id="pypimetadatadescription"></a>`description` | [`String`](#string) | Longer description that can run to several paragraphs. |
+| <a id="pypimetadatadescriptioncontenttype"></a>`descriptionContentType` | [`String`](#string) | Markup syntax used in the description field. |
| <a id="pypimetadataid"></a>`id` | [`PackagesPypiMetadatumID!`](#packagespypimetadatumid) | ID of the metadatum. |
+| <a id="pypimetadatakeywords"></a>`keywords` | [`String`](#string) | List of keywords, separated by commas. |
+| <a id="pypimetadatametadataversion"></a>`metadataVersion` | [`String`](#string) | Metadata version. |
| <a id="pypimetadatarequiredpython"></a>`requiredPython` | [`String`](#string) | Required Python version of the Pypi package. |
+| <a id="pypimetadatasummary"></a>`summary` | [`String`](#string) | One-line summary of the description. |
### `QueryComplexity`
@@ -24702,11 +25365,11 @@ Check permissions for the current user on a requirement.
| Name | Type | Description |
| ---- | ---- | ----------- |
-| <a id="requirementpermissionsadminrequirement"></a>`adminRequirement` | [`Boolean!`](#boolean) | Indicates the user can perform `admin_requirement` on this resource. |
-| <a id="requirementpermissionscreaterequirement"></a>`createRequirement` | [`Boolean!`](#boolean) | Indicates the user can perform `create_requirement` on this resource. |
-| <a id="requirementpermissionsdestroyrequirement"></a>`destroyRequirement` | [`Boolean!`](#boolean) | Indicates the user can perform `destroy_requirement` on this resource. |
-| <a id="requirementpermissionsreadrequirement"></a>`readRequirement` | [`Boolean!`](#boolean) | Indicates the user can perform `read_requirement` on this resource. |
-| <a id="requirementpermissionsupdaterequirement"></a>`updateRequirement` | [`Boolean!`](#boolean) | Indicates the user can perform `update_requirement` on this resource. |
+| <a id="requirementpermissionsadminrequirement"></a>`adminRequirement` | [`Boolean!`](#boolean) | If `true`, the user can perform `admin_requirement` on this resource. |
+| <a id="requirementpermissionscreaterequirement"></a>`createRequirement` | [`Boolean!`](#boolean) | If `true`, the user can perform `create_requirement` on this resource. |
+| <a id="requirementpermissionsdestroyrequirement"></a>`destroyRequirement` | [`Boolean!`](#boolean) | If `true`, the user can perform `destroy_requirement` on this resource. |
+| <a id="requirementpermissionsreadrequirement"></a>`readRequirement` | [`Boolean!`](#boolean) | If `true`, the user can perform `read_requirement` on this resource. |
+| <a id="requirementpermissionsupdaterequirement"></a>`updateRequirement` | [`Boolean!`](#boolean) | If `true`, the user can perform `update_requirement` on this resource. |
### `RequirementStatesCount`
@@ -24755,10 +25418,10 @@ Counts of requirements by their state.
| Name | Type | Description |
| ---- | ---- | ----------- |
-| <a id="runnerpermissionsassignrunner"></a>`assignRunner` | [`Boolean!`](#boolean) | Indicates the user can perform `assign_runner` on this resource. |
-| <a id="runnerpermissionsdeleterunner"></a>`deleteRunner` | [`Boolean!`](#boolean) | Indicates the user can perform `delete_runner` on this resource. |
-| <a id="runnerpermissionsreadrunner"></a>`readRunner` | [`Boolean!`](#boolean) | Indicates the user can perform `read_runner` on this resource. |
-| <a id="runnerpermissionsupdaterunner"></a>`updateRunner` | [`Boolean!`](#boolean) | Indicates the user can perform `update_runner` on this resource. |
+| <a id="runnerpermissionsassignrunner"></a>`assignRunner` | [`Boolean!`](#boolean) | If `true`, the user can perform `assign_runner` on this resource. |
+| <a id="runnerpermissionsdeleterunner"></a>`deleteRunner` | [`Boolean!`](#boolean) | If `true`, the user can perform `delete_runner` on this resource. |
+| <a id="runnerpermissionsreadrunner"></a>`readRunner` | [`Boolean!`](#boolean) | If `true`, the user can perform `read_runner` on this resource. |
+| <a id="runnerpermissionsupdaterunner"></a>`updateRunner` | [`Boolean!`](#boolean) | If `true`, the user can perform `update_runner` on this resource. |
### `RunnerPlatform`
@@ -25252,12 +25915,12 @@ Represents how the blob content should be displayed.
| Name | Type | Description |
| ---- | ---- | ----------- |
-| <a id="snippetpermissionsadminsnippet"></a>`adminSnippet` | [`Boolean!`](#boolean) | Indicates the user can perform `admin_snippet` on this resource. |
-| <a id="snippetpermissionsawardemoji"></a>`awardEmoji` | [`Boolean!`](#boolean) | Indicates the user can perform `award_emoji` on this resource. |
-| <a id="snippetpermissionscreatenote"></a>`createNote` | [`Boolean!`](#boolean) | Indicates the user can perform `create_note` on this resource. |
-| <a id="snippetpermissionsreadsnippet"></a>`readSnippet` | [`Boolean!`](#boolean) | Indicates the user can perform `read_snippet` on this resource. |
-| <a id="snippetpermissionsreportsnippet"></a>`reportSnippet` | [`Boolean!`](#boolean) | Indicates the user can perform `report_snippet` on this resource. |
-| <a id="snippetpermissionsupdatesnippet"></a>`updateSnippet` | [`Boolean!`](#boolean) | Indicates the user can perform `update_snippet` on this resource. |
+| <a id="snippetpermissionsadminsnippet"></a>`adminSnippet` | [`Boolean!`](#boolean) | If `true`, the user can perform `admin_snippet` on this resource. |
+| <a id="snippetpermissionsawardemoji"></a>`awardEmoji` | [`Boolean!`](#boolean) | If `true`, the user can perform `award_emoji` on this resource. |
+| <a id="snippetpermissionscreatenote"></a>`createNote` | [`Boolean!`](#boolean) | If `true`, the user can perform `create_note` on this resource. |
+| <a id="snippetpermissionsreadsnippet"></a>`readSnippet` | [`Boolean!`](#boolean) | If `true`, the user can perform `read_snippet` on this resource. |
+| <a id="snippetpermissionsreportsnippet"></a>`reportSnippet` | [`Boolean!`](#boolean) | If `true`, the user can perform `report_snippet` on this resource. |
+| <a id="snippetpermissionsupdatesnippet"></a>`updateSnippet` | [`Boolean!`](#boolean) | If `true`, the user can perform `update_snippet` on this resource. |
### `SnippetRepositoryRegistry`
@@ -25642,7 +26305,7 @@ Describes an incident management timeline event.
| Name | Type | Description |
| ---- | ---- | ----------- |
-| <a id="timelogpermissionsadmintimelog"></a>`adminTimelog` | [`Boolean!`](#boolean) | Indicates the user can perform `admin_timelog` on this resource. |
+| <a id="timelogpermissionsadmintimelog"></a>`adminTimelog` | [`Boolean!`](#boolean) | If `true`, the user can perform `admin_timelog` on this resource. |
### `Todo`
@@ -25812,18 +26475,20 @@ Core representation of a GitLab user.
| <a id="usercoreid"></a>`id` | [`ID!`](#id) | ID of the user. |
| <a id="usercoreide"></a>`ide` | [`Ide`](#ide) | IDE settings. |
| <a id="usercorejobtitle"></a>`jobTitle` | [`String`](#string) | Job title of the user. |
+| <a id="usercorelastactivityon"></a>`lastActivityOn` | [`Date`](#date) | Date the user last performed any actions. |
| <a id="usercorelinkedin"></a>`linkedin` | [`String`](#string) | LinkedIn profile name of the user. |
| <a id="usercorelocation"></a>`location` | [`String`](#string) | Location of the user. |
| <a id="usercorename"></a>`name` | [`String!`](#string) | Human-readable name of the user. Returns `****` if the user is a project bot and the requester does not have permission to view the project. |
| <a id="usercorenamespace"></a>`namespace` | [`Namespace`](#namespace) | Personal namespace of the user. |
| <a id="usercorenamespacecommitemails"></a>`namespaceCommitEmails` | [`NamespaceCommitEmailConnection`](#namespacecommitemailconnection) | User's custom namespace commit emails. (see [Connections](#connections)) |
| <a id="usercoreorganization"></a>`organization` | [`String`](#string) | Who the user represents or works for. |
+| <a id="usercoreorganizations"></a>`organizations` **{warning-solid}** | [`OrganizationConnection`](#organizationconnection) | **Introduced** in 16.6. This feature is an Experiment. It can be changed or removed at any time. Organizations where the user has access. |
| <a id="usercorepreferencesgitpodpath"></a>`preferencesGitpodPath` | [`String`](#string) | Web path to the Gitpod section within user preferences. |
| <a id="usercoreprofileenablegitpodpath"></a>`profileEnableGitpodPath` | [`String`](#string) | Web path to enable Gitpod for the user. |
| <a id="usercoreprojectmemberships"></a>`projectMemberships` | [`ProjectMemberConnection`](#projectmemberconnection) | Project memberships of the user. (see [Connections](#connections)) |
| <a id="usercorepronouns"></a>`pronouns` | [`String`](#string) | Pronouns of the user. |
| <a id="usercorepublicemail"></a>`publicEmail` | [`String`](#string) | User's public email. |
-| <a id="usercoresavedreplies"></a>`savedReplies` | [`SavedReplyConnection`](#savedreplyconnection) | Saved replies authored by the user. Will not return saved replies if `saved_replies` feature flag is disabled. (see [Connections](#connections)) |
+| <a id="usercoresavedreplies"></a>`savedReplies` | [`SavedReplyConnection`](#savedreplyconnection) | Saved replies authored by the user. (see [Connections](#connections)) |
| <a id="usercorestate"></a>`state` | [`UserState!`](#userstate) | State of the user. |
| <a id="usercorestatus"></a>`status` | [`UserStatus`](#userstatus) | User status. |
| <a id="usercoretwitter"></a>`twitter` | [`String`](#string) | Twitter username of the user. |
@@ -25962,7 +26627,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
##### `UserCore.savedReply`
-Saved reply authored by the user. Will not return saved reply if `saved_replies` feature flag is disabled.
+Saved reply authored by the user.
Returns [`SavedReply`](#savedreply).
@@ -26092,7 +26757,7 @@ fields relate to interactions between the two entities.
| Name | Type | Description |
| ---- | ---- | ----------- |
-| <a id="userpermissionscreatesnippet"></a>`createSnippet` | [`Boolean!`](#boolean) | Indicates the user can perform `create_snippet` on this resource. |
+| <a id="userpermissionscreatesnippet"></a>`createSnippet` | [`Boolean!`](#boolean) | If `true`, the user can perform `create_snippet` on this resource. |
### `UserPreferences`
@@ -26114,6 +26779,17 @@ fields relate to interactions between the two entities.
| <a id="userstatusmessage"></a>`message` | [`String`](#string) | User status message. |
| <a id="userstatusmessagehtml"></a>`messageHtml` | [`String`](#string) | HTML of the user status message. |
+### `ValueStream`
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="valuestreamid"></a>`id` | [`AnalyticsCycleAnalyticsValueStreamID!`](#analyticscycleanalyticsvaluestreamid) | ID of the value stream. |
+| <a id="valuestreamname"></a>`name` | [`String!`](#string) | Name of the value stream. |
+| <a id="valuestreamnamespace"></a>`namespace` | [`Namespace!`](#namespace) | Namespace the value stream belongs to. |
+| <a id="valuestreamproject"></a>`project` **{warning-solid}** | [`Project`](#project) | **Introduced** in 15.6. This feature is an Experiment. It can be changed or removed at any time. Project the value stream belongs to, returns empty if it belongs to a group. |
+
### `ValueStreamAnalyticsMetric`
#### Fields
@@ -26675,15 +27351,15 @@ Check permissions for the current user on a vulnerability.
| Name | Type | Description |
| ---- | ---- | ----------- |
-| <a id="vulnerabilitypermissionsadminvulnerability"></a>`adminVulnerability` | [`Boolean!`](#boolean) | Indicates the user can perform `admin_vulnerability` on this resource. |
-| <a id="vulnerabilitypermissionsadminvulnerabilityexternalissuelink"></a>`adminVulnerabilityExternalIssueLink` | [`Boolean!`](#boolean) | Indicates the user can perform `admin_vulnerability_external_issue_link` on this resource. |
-| <a id="vulnerabilitypermissionsadminvulnerabilityissuelink"></a>`adminVulnerabilityIssueLink` | [`Boolean!`](#boolean) | Indicates the user can perform `admin_vulnerability_issue_link` on this resource. |
-| <a id="vulnerabilitypermissionscreatevulnerabilityexport"></a>`createVulnerabilityExport` | [`Boolean!`](#boolean) | Indicates the user can perform `create_vulnerability_export` on this resource. |
-| <a id="vulnerabilitypermissionscreatevulnerabilityfeedback"></a>`createVulnerabilityFeedback` | [`Boolean!`](#boolean) | Indicates the user can perform `create_vulnerability_feedback` on this resource. |
-| <a id="vulnerabilitypermissionsdestroyvulnerabilityfeedback"></a>`destroyVulnerabilityFeedback` | [`Boolean!`](#boolean) | Indicates the user can perform `destroy_vulnerability_feedback` on this resource. |
-| <a id="vulnerabilitypermissionsreadvulnerability"></a>`readVulnerability` | [`Boolean!`](#boolean) | Indicates the user can perform `read_vulnerability` on this resource. |
-| <a id="vulnerabilitypermissionsreadvulnerabilityfeedback"></a>`readVulnerabilityFeedback` | [`Boolean!`](#boolean) | Indicates the user can perform `read_vulnerability_feedback` on this resource. |
-| <a id="vulnerabilitypermissionsupdatevulnerabilityfeedback"></a>`updateVulnerabilityFeedback` | [`Boolean!`](#boolean) | Indicates the user can perform `update_vulnerability_feedback` on this resource. |
+| <a id="vulnerabilitypermissionsadminvulnerability"></a>`adminVulnerability` | [`Boolean!`](#boolean) | If `true`, the user can perform `admin_vulnerability` on this resource. |
+| <a id="vulnerabilitypermissionsadminvulnerabilityexternalissuelink"></a>`adminVulnerabilityExternalIssueLink` | [`Boolean!`](#boolean) | If `true`, the user can perform `admin_vulnerability_external_issue_link` on this resource. |
+| <a id="vulnerabilitypermissionsadminvulnerabilityissuelink"></a>`adminVulnerabilityIssueLink` | [`Boolean!`](#boolean) | If `true`, the user can perform `admin_vulnerability_issue_link` on this resource. |
+| <a id="vulnerabilitypermissionscreatevulnerabilityexport"></a>`createVulnerabilityExport` | [`Boolean!`](#boolean) | If `true`, the user can perform `create_vulnerability_export` on this resource. |
+| <a id="vulnerabilitypermissionscreatevulnerabilityfeedback"></a>`createVulnerabilityFeedback` | [`Boolean!`](#boolean) | If `true`, the user can perform `create_vulnerability_feedback` on this resource. |
+| <a id="vulnerabilitypermissionsdestroyvulnerabilityfeedback"></a>`destroyVulnerabilityFeedback` | [`Boolean!`](#boolean) | If `true`, the user can perform `destroy_vulnerability_feedback` on this resource. |
+| <a id="vulnerabilitypermissionsreadvulnerability"></a>`readVulnerability` | [`Boolean!`](#boolean) | If `true`, the user can perform `read_vulnerability` on this resource. |
+| <a id="vulnerabilitypermissionsreadvulnerabilityfeedback"></a>`readVulnerabilityFeedback` | [`Boolean!`](#boolean) | If `true`, the user can perform `read_vulnerability_feedback` on this resource. |
+| <a id="vulnerabilitypermissionsupdatevulnerabilityfeedback"></a>`updateVulnerabilityFeedback` | [`Boolean!`](#boolean) | If `true`, the user can perform `update_vulnerability_feedback` on this resource. |
### `VulnerabilityRemediationType`
@@ -26876,14 +27552,14 @@ Check permissions for the current user on a work item.
| Name | Type | Description |
| ---- | ---- | ----------- |
-| <a id="workitempermissionsadminparentlink"></a>`adminParentLink` | [`Boolean!`](#boolean) | Indicates the user can perform `admin_parent_link` on this resource. |
-| <a id="workitempermissionsadminworkitem"></a>`adminWorkItem` | [`Boolean!`](#boolean) | Indicates the user can perform `admin_work_item` on this resource. |
-| <a id="workitempermissionsadminworkitemlink"></a>`adminWorkItemLink` | [`Boolean!`](#boolean) | Indicates the user can perform `admin_work_item_link` on this resource. |
-| <a id="workitempermissionscreatenote"></a>`createNote` | [`Boolean!`](#boolean) | Indicates the user can perform `create_note` on this resource. |
-| <a id="workitempermissionsdeleteworkitem"></a>`deleteWorkItem` | [`Boolean!`](#boolean) | Indicates the user can perform `delete_work_item` on this resource. |
-| <a id="workitempermissionsreadworkitem"></a>`readWorkItem` | [`Boolean!`](#boolean) | Indicates the user can perform `read_work_item` on this resource. |
-| <a id="workitempermissionssetworkitemmetadata"></a>`setWorkItemMetadata` | [`Boolean!`](#boolean) | Indicates the user can perform `set_work_item_metadata` on this resource. |
-| <a id="workitempermissionsupdateworkitem"></a>`updateWorkItem` | [`Boolean!`](#boolean) | Indicates the user can perform `update_work_item` on this resource. |
+| <a id="workitempermissionsadminparentlink"></a>`adminParentLink` | [`Boolean!`](#boolean) | If `true`, the user can perform `admin_parent_link` on this resource. |
+| <a id="workitempermissionsadminworkitem"></a>`adminWorkItem` | [`Boolean!`](#boolean) | If `true`, the user can perform `admin_work_item` on this resource. |
+| <a id="workitempermissionsadminworkitemlink"></a>`adminWorkItemLink` | [`Boolean!`](#boolean) | If `true`, the user can perform `admin_work_item_link` on this resource. |
+| <a id="workitempermissionscreatenote"></a>`createNote` | [`Boolean!`](#boolean) | If `true`, the user can perform `create_note` on this resource. |
+| <a id="workitempermissionsdeleteworkitem"></a>`deleteWorkItem` | [`Boolean!`](#boolean) | If `true`, the user can perform `delete_work_item` on this resource. |
+| <a id="workitempermissionsreadworkitem"></a>`readWorkItem` | [`Boolean!`](#boolean) | If `true`, the user can perform `read_work_item` on this resource. |
+| <a id="workitempermissionssetworkitemmetadata"></a>`setWorkItemMetadata` | [`Boolean!`](#boolean) | If `true`, the user can perform `set_work_item_metadata` on this resource. |
+| <a id="workitempermissionsupdateworkitem"></a>`updateWorkItem` | [`Boolean!`](#boolean) | If `true`, the user can perform `update_work_item` on this resource. |
### `WorkItemType`
@@ -27411,6 +28087,14 @@ All possible ways to specify the API surface for an API fuzzing scan.
| <a id="apifuzzingscanmodeopenapi"></a>`OPENAPI` | The API surface is specified by a OPENAPI file. |
| <a id="apifuzzingscanmodepostman"></a>`POSTMAN` | The API surface is specified by a POSTMAN file. |
+### `ApprovalReportType`
+
+| Value | Description |
+| ----- | ----------- |
+| <a id="approvalreporttypeany_merge_request"></a>`ANY_MERGE_REQUEST` | Represents report_type for any_merge_request related approval rules. |
+| <a id="approvalreporttypelicense_scanning"></a>`LICENSE_SCANNING` | Represents report_type for license scanning related approval rules. |
+| <a id="approvalreporttypescan_finding"></a>`SCAN_FINDING` | Represents report_type for vulnerability check related approval rules. |
+
### `ApprovalRuleType`
The kind of an approval rule.
@@ -27464,24 +28148,26 @@ Types of blob viewers.
| <a id="blobviewerstyperich"></a>`rich` | Rich blob viewers type. |
| <a id="blobviewerstypesimple"></a>`simple` | Simple blob viewers type. |
+### `CiCatalogResourceScope`
+
+Values for scoping catalog resources.
+
+| Value | Description |
+| ----- | ----------- |
+| <a id="cicatalogresourcescopeall"></a>`ALL` | All catalog resources visible to the current user. |
+
### `CiCatalogResourceSort`
Values for sorting catalog resources.
| Value | Description |
| ----- | ----------- |
-| <a id="cicatalogresourcesortcreated_asc"></a>`CREATED_ASC` | Created at ascending order. |
-| <a id="cicatalogresourcesortcreated_desc"></a>`CREATED_DESC` | Created at descending order. |
+| <a id="cicatalogresourcesortcreated_asc"></a>`CREATED_ASC` | Created date by ascending order. |
+| <a id="cicatalogresourcesortcreated_desc"></a>`CREATED_DESC` | Created date by descending order. |
| <a id="cicatalogresourcesortlatest_released_at_asc"></a>`LATEST_RELEASED_AT_ASC` | Latest release date by ascending order. |
| <a id="cicatalogresourcesortlatest_released_at_desc"></a>`LATEST_RELEASED_AT_DESC` | Latest release date by descending order. |
| <a id="cicatalogresourcesortname_asc"></a>`NAME_ASC` | Name by ascending order. |
| <a id="cicatalogresourcesortname_desc"></a>`NAME_DESC` | Name by descending order. |
-| <a id="cicatalogresourcesortupdated_asc"></a>`UPDATED_ASC` | Updated at ascending order. |
-| <a id="cicatalogresourcesortupdated_desc"></a>`UPDATED_DESC` | Updated at descending order. |
-| <a id="cicatalogresourcesortcreated_asc"></a>`created_asc` **{warning-solid}** | **Deprecated** in 13.5. This was renamed. Use: `CREATED_ASC`. |
-| <a id="cicatalogresourcesortcreated_desc"></a>`created_desc` **{warning-solid}** | **Deprecated** in 13.5. This was renamed. Use: `CREATED_DESC`. |
-| <a id="cicatalogresourcesortupdated_asc"></a>`updated_asc` **{warning-solid}** | **Deprecated** in 13.5. This was renamed. Use: `UPDATED_ASC`. |
-| <a id="cicatalogresourcesortupdated_desc"></a>`updated_desc` **{warning-solid}** | **Deprecated** in 13.5. This was renamed. Use: `UPDATED_DESC`. |
### `CiConfigIncludeType`
@@ -27586,6 +28272,7 @@ Values for sorting inherited variables.
| <a id="cijobstatusscheduled"></a>`SCHEDULED` | A job that is scheduled. |
| <a id="cijobstatusskipped"></a>`SKIPPED` | A job that is skipped. |
| <a id="cijobstatussuccess"></a>`SUCCESS` | A job that is success. |
+| <a id="cijobstatuswaiting_for_callback"></a>`WAITING_FOR_CALLBACK` | A job that is waiting for callback. |
| <a id="cijobstatuswaiting_for_resource"></a>`WAITING_FOR_RESOURCE` | A job that is waiting for resource. |
### `CiJobTokenScopeDirection`
@@ -27690,15 +28377,25 @@ Values for sorting variables.
| <a id="codequalitydegradationseverityminor"></a>`MINOR` | Code Quality degradation has a status of minor. |
| <a id="codequalitydegradationseverityunknown"></a>`UNKNOWN` | Code Quality degradation has a status of unknown. |
-### `CodequalityReportsComparerReportStatus`
+### `CodequalityReportsComparerReportGenerationStatus`
-Report comparison status.
+Represents the generation status of the compared codequality report.
| Value | Description |
| ----- | ----------- |
-| <a id="codequalityreportscomparerreportstatusfailed"></a>`FAILED` | Report failed to generate. |
-| <a id="codequalityreportscomparerreportstatusnot_found"></a>`NOT_FOUND` | Head report or base report not found. |
-| <a id="codequalityreportscomparerreportstatussuccess"></a>`SUCCESS` | Report successfully generated. |
+| <a id="codequalityreportscomparerreportgenerationstatuserror"></a>`ERROR` | An error happened while generating the report. |
+| <a id="codequalityreportscomparerreportgenerationstatusparsed"></a>`PARSED` | Report was generated. |
+| <a id="codequalityreportscomparerreportgenerationstatusparsing"></a>`PARSING` | Report is being generated. |
+
+### `CodequalityReportsComparerStatus`
+
+Represents the state of the code quality report.
+
+| Value | Description |
+| ----- | ----------- |
+| <a id="codequalityreportscomparerstatusfailed"></a>`FAILED` | Report generated and there are new code quality degradations. |
+| <a id="codequalityreportscomparerstatusnot_found"></a>`NOT_FOUND` | Head report or base report not found. |
+| <a id="codequalityreportscomparerstatussuccess"></a>`SUCCESS` | No degradations found in the head pipeline report. |
### `CommitActionMode`
@@ -27873,6 +28570,16 @@ Values for sorting contacts.
| <a id="containerexpirationpolicyolderthanenumsixty_days"></a>`SIXTY_DAYS` | 60 days until tags are automatically removed. |
| <a id="containerexpirationpolicyolderthanenumthirty_days"></a>`THIRTY_DAYS` | 30 days until tags are automatically removed. |
+### `ContainerRegistryProtectionRuleAccessLevel`
+
+Access level of a container registry protection rule resource.
+
+| Value | Description |
+| ----- | ----------- |
+| <a id="containerregistryprotectionruleaccessleveldeveloper"></a>`DEVELOPER` | Developer access. |
+| <a id="containerregistryprotectionruleaccesslevelmaintainer"></a>`MAINTAINER` | Maintainer access. |
+| <a id="containerregistryprotectionruleaccesslevelowner"></a>`OWNER` | Owner access. |
+
### `ContainerRepositoryCleanupStatus`
Status of the tags cleanup of a container repository.
@@ -28693,6 +29400,21 @@ Name of access levels of a group or project member.
| <a id="memberaccesslevelnameowner"></a>`OWNER` | Owner access. |
| <a id="memberaccesslevelnamereporter"></a>`REPORTER` | Reporter access. |
+### `MemberRolePermission`
+
+Member role permission.
+
+| Value | Description |
+| ----- | ----------- |
+| <a id="memberrolepermissionadmin_group_member"></a>`ADMIN_GROUP_MEMBER` | Allows admin access to group members. |
+| <a id="memberrolepermissionadmin_merge_request"></a>`ADMIN_MERGE_REQUEST` | Allows admin access to the merge requests. |
+| <a id="memberrolepermissionadmin_vulnerability"></a>`ADMIN_VULNERABILITY` | Allows admin access to the vulnerability reports. |
+| <a id="memberrolepermissionarchive_project"></a>`ARCHIVE_PROJECT` | Allows to archive projects. |
+| <a id="memberrolepermissionmanage_project_access_tokens"></a>`MANAGE_PROJECT_ACCESS_TOKENS` | Allows manage access to the project access tokens. |
+| <a id="memberrolepermissionread_code"></a>`READ_CODE` | Allows read-only access to the source code. |
+| <a id="memberrolepermissionread_dependency"></a>`READ_DEPENDENCY` | Allows read-only access to the dependencies. |
+| <a id="memberrolepermissionread_vulnerability"></a>`READ_VULNERABILITY` | Allows read-only access to the vulnerability reports. |
+
### `MemberSort`
Values for sorting members.
@@ -28727,8 +29449,9 @@ State of a review of a GitLab merge request.
| Value | Description |
| ----- | ----------- |
-| <a id="mergerequestreviewstatereviewed"></a>`REVIEWED` | The merge request is reviewed. |
-| <a id="mergerequestreviewstateunreviewed"></a>`UNREVIEWED` | The merge request is unreviewed. |
+| <a id="mergerequestreviewstaterequested_changes"></a>`REQUESTED_CHANGES` | Merge request reviewer has requested changes. |
+| <a id="mergerequestreviewstatereviewed"></a>`REVIEWED` | Merge request reviewer has reviewed. |
+| <a id="mergerequestreviewstateunreviewed"></a>`UNREVIEWED` | Awaiting review from merge request reviewer. |
### `MergeRequestSort`
@@ -29015,6 +29738,7 @@ Values for package manager.
| <a id="packagemanagerpip"></a>`PIP` | Package manager: pip. |
| <a id="packagemanagerpipenv"></a>`PIPENV` | Package manager: pipenv. |
| <a id="packagemanagerpnpm"></a>`PNPM` | Package manager: pnpm. |
+| <a id="packagemanagerpoetry"></a>`POETRY` | Package manager: poetry. |
| <a id="packagemanagersbt"></a>`SBT` | Package manager: sbt. |
| <a id="packagemanagersetuptools"></a>`SETUPTOOLS` | Package manager: setuptools. |
| <a id="packagemanageryarn"></a>`YARN` | Package manager: yarn. |
@@ -29149,6 +29873,7 @@ Event type of the pipeline associated with a merge request.
| <a id="pipelinestatusenumscheduled"></a>`SCHEDULED` | Pipeline is scheduled to run. |
| <a id="pipelinestatusenumskipped"></a>`SKIPPED` | Pipeline was skipped. |
| <a id="pipelinestatusenumsuccess"></a>`SUCCESS` | Pipeline completed successfully. |
+| <a id="pipelinestatusenumwaiting_for_callback"></a>`WAITING_FOR_CALLBACK` | Pipeline is waiting for an external action. |
| <a id="pipelinestatusenumwaiting_for_resource"></a>`WAITING_FOR_RESOURCE` | A resource (for example, a runner) that the pipeline requires to run is unavailable. |
### `ProductAnalyticsState`
@@ -29565,7 +30290,6 @@ Name of the feature that the callout is for.
| <a id="usercalloutfeaturenameenumci_deprecation_warning_for_types_keyword"></a>`CI_DEPRECATION_WARNING_FOR_TYPES_KEYWORD` | Callout feature name for ci_deprecation_warning_for_types_keyword. |
| <a id="usercalloutfeaturenameenumcloud_licensing_subscription_activation_banner"></a>`CLOUD_LICENSING_SUBSCRIPTION_ACTIVATION_BANNER` | Callout feature name for cloud_licensing_subscription_activation_banner. |
| <a id="usercalloutfeaturenameenumcluster_security_warning"></a>`CLUSTER_SECURITY_WARNING` | Callout feature name for cluster_security_warning. |
-| <a id="usercalloutfeaturenameenumcreate_runner_workflow_banner"></a>`CREATE_RUNNER_WORKFLOW_BANNER` | Callout feature name for create_runner_workflow_banner. |
| <a id="usercalloutfeaturenameenumeoa_bronze_plan_banner"></a>`EOA_BRONZE_PLAN_BANNER` | Callout feature name for eoa_bronze_plan_banner. |
| <a id="usercalloutfeaturenameenumfeature_flags_new_version"></a>`FEATURE_FLAGS_NEW_VERSION` | Callout feature name for feature_flags_new_version. |
| <a id="usercalloutfeaturenameenumgcp_signup_offer"></a>`GCP_SIGNUP_OFFER` | Callout feature name for gcp_signup_offer. |
@@ -29580,7 +30304,7 @@ Name of the feature that the callout is for.
| <a id="usercalloutfeaturenameenumnamespace_storage_limit_alert_error_threshold"></a>`NAMESPACE_STORAGE_LIMIT_ALERT_ERROR_THRESHOLD` | Callout feature name for namespace_storage_limit_alert_error_threshold. |
| <a id="usercalloutfeaturenameenumnamespace_storage_limit_alert_warning_threshold"></a>`NAMESPACE_STORAGE_LIMIT_ALERT_WARNING_THRESHOLD` | Callout feature name for namespace_storage_limit_alert_warning_threshold. |
| <a id="usercalloutfeaturenameenumnamespace_storage_pre_enforcement_banner"></a>`NAMESPACE_STORAGE_PRE_ENFORCEMENT_BANNER` | Callout feature name for namespace_storage_pre_enforcement_banner. |
-| <a id="usercalloutfeaturenameenumnew_navigation_callout"></a>`NEW_NAVIGATION_CALLOUT` | Callout feature name for new_navigation_callout. |
+| <a id="usercalloutfeaturenameenumnew_nav_for_everyone_callout"></a>`NEW_NAV_FOR_EVERYONE_CALLOUT` | Callout feature name for new_nav_for_everyone_callout. |
| <a id="usercalloutfeaturenameenumnew_top_level_group_alert"></a>`NEW_TOP_LEVEL_GROUP_ALERT` | Callout feature name for new_top_level_group_alert. |
| <a id="usercalloutfeaturenameenumnew_user_signups_cap_reached"></a>`NEW_USER_SIGNUPS_CAP_REACHED` | Callout feature name for new_user_signups_cap_reached. |
| <a id="usercalloutfeaturenameenumpersonal_access_token_expiry"></a>`PERSONAL_ACCESS_TOKEN_EXPIRY` | Callout feature name for personal_access_token_expiry. |
@@ -29952,6 +30676,12 @@ A `AlertManagementHttpIntegrationID` is a global ID. It is encoded as a string.
An example `AlertManagementHttpIntegrationID` is: `"gid://gitlab/AlertManagement::HttpIntegration/1"`.
+### `AnalyticsCycleAnalyticsValueStreamID`
+
+A `AnalyticsCycleAnalyticsValueStreamID` is a global ID. It is encoded as a string.
+
+An example `AnalyticsCycleAnalyticsValueStreamID` is: `"gid://gitlab/Analytics::CycleAnalytics::ValueStream/1"`.
+
### `AnalyticsDevopsAdoptionEnabledNamespaceID`
A `AnalyticsDevopsAdoptionEnabledNamespaceID` is a global ID. It is encoded as a string.
@@ -30098,6 +30828,12 @@ A `CiStageID` is a global ID. It is encoded as a string.
An example `CiStageID` is: `"gid://gitlab/Ci::Stage/1"`.
+### `CiSubscriptionsProjectID`
+
+A `CiSubscriptionsProjectID` is a global ID. It is encoded as a string.
+
+An example `CiSubscriptionsProjectID` is: `"gid://gitlab/Ci::Subscriptions::Project/1"`.
+
### `CiTriggerID`
A `CiTriggerID` is a global ID. It is encoded as a string.
@@ -30134,6 +30870,12 @@ A `ComplianceManagementFrameworkID` is a global ID. It is encoded as a string.
An example `ComplianceManagementFrameworkID` is: `"gid://gitlab/ComplianceManagement::Framework/1"`.
+### `ContainerRegistryProtectionRuleID`
+
+A `ContainerRegistryProtectionRuleID` is a global ID. It is encoded as a string.
+
+An example `ContainerRegistryProtectionRuleID` is: `"gid://gitlab/ContainerRegistry::Protection::Rule/1"`.
+
### `ContainerRepositoryID`
A `ContainerRepositoryID` is a global ID. It is encoded as a string.
@@ -30537,6 +31279,12 @@ A `PackagesPackageID` is a global ID. It is encoded as a string.
An example `PackagesPackageID` is: `"gid://gitlab/Packages::Package/1"`.
+### `PackagesProtectionRuleID`
+
+A `PackagesProtectionRuleID` is a global ID. It is encoded as a string.
+
+An example `PackagesProtectionRuleID` is: `"gid://gitlab/Packages::Protection::Rule/1"`.
+
### `PackagesPypiMetadatumID`
A `PackagesPypiMetadatumID` is a global ID. It is encoded as a string.
@@ -30559,6 +31307,12 @@ A `ProjectID` is a global ID. It is encoded as a string.
An example `ProjectID` is: `"gid://gitlab/Project/1"`.
+### `ProjectImportStateID`
+
+A `ProjectImportStateID` is a global ID. It is encoded as a string.
+
+An example `ProjectImportStateID` is: `"gid://gitlab/ProjectImportState/1"`.
+
### `ReleaseID`
A `ReleaseID` is a global ID. It is encoded as a string.
@@ -31039,6 +31793,7 @@ Implementations:
Implementations:
- [`GroupMember`](#groupmember)
+- [`PendingGroupMember`](#pendinggroupmember)
- [`ProjectMember`](#projectmember)
##### Fields
@@ -31071,6 +31826,7 @@ Returns [`UserMergeRequestInteraction`](#usermergerequestinteraction).
Implementations:
+- [`AbuseReport`](#abusereport)
- [`AlertManagementAlert`](#alertmanagementalert)
- [`BoardEpic`](#boardepic)
- [`Design`](#design)
@@ -31245,18 +32001,20 @@ Implementations:
| <a id="userid"></a>`id` | [`ID!`](#id) | ID of the user. |
| <a id="useride"></a>`ide` | [`Ide`](#ide) | IDE settings. |
| <a id="userjobtitle"></a>`jobTitle` | [`String`](#string) | Job title of the user. |
+| <a id="userlastactivityon"></a>`lastActivityOn` | [`Date`](#date) | Date the user last performed any actions. |
| <a id="userlinkedin"></a>`linkedin` | [`String`](#string) | LinkedIn profile name of the user. |
| <a id="userlocation"></a>`location` | [`String`](#string) | Location of the user. |
| <a id="username"></a>`name` | [`String!`](#string) | Human-readable name of the user. Returns `****` if the user is a project bot and the requester does not have permission to view the project. |
| <a id="usernamespace"></a>`namespace` | [`Namespace`](#namespace) | Personal namespace of the user. |
| <a id="usernamespacecommitemails"></a>`namespaceCommitEmails` | [`NamespaceCommitEmailConnection`](#namespacecommitemailconnection) | User's custom namespace commit emails. (see [Connections](#connections)) |
| <a id="userorganization"></a>`organization` | [`String`](#string) | Who the user represents or works for. |
+| <a id="userorganizations"></a>`organizations` **{warning-solid}** | [`OrganizationConnection`](#organizationconnection) | **Introduced** in 16.6. This feature is an Experiment. It can be changed or removed at any time. Organizations where the user has access. |
| <a id="userpreferencesgitpodpath"></a>`preferencesGitpodPath` | [`String`](#string) | Web path to the Gitpod section within user preferences. |
| <a id="userprofileenablegitpodpath"></a>`profileEnableGitpodPath` | [`String`](#string) | Web path to enable Gitpod for the user. |
| <a id="userprojectmemberships"></a>`projectMemberships` | [`ProjectMemberConnection`](#projectmemberconnection) | Project memberships of the user. (see [Connections](#connections)) |
| <a id="userpronouns"></a>`pronouns` | [`String`](#string) | Pronouns of the user. |
| <a id="userpublicemail"></a>`publicEmail` | [`String`](#string) | User's public email. |
-| <a id="usersavedreplies"></a>`savedReplies` | [`SavedReplyConnection`](#savedreplyconnection) | Saved replies authored by the user. Will not return saved replies if `saved_replies` feature flag is disabled. (see [Connections](#connections)) |
+| <a id="usersavedreplies"></a>`savedReplies` | [`SavedReplyConnection`](#savedreplyconnection) | Saved replies authored by the user. (see [Connections](#connections)) |
| <a id="userstate"></a>`state` | [`UserState!`](#userstate) | State of the user. |
| <a id="userstatus"></a>`status` | [`UserStatus`](#userstatus) | User status. |
| <a id="usertwitter"></a>`twitter` | [`String`](#string) | Twitter username of the user. |
@@ -31395,7 +32153,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
###### `User.savedReply`
-Saved reply authored by the user. Will not return saved reply if `saved_replies` feature flag is disabled.
+Saved reply authored by the user.
Returns [`SavedReply`](#savedreply).
@@ -31545,9 +32303,21 @@ see the associated mutation type above.
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="aichatinputcontent"></a>`content` | [`String!`](#string) | Content of the message. |
+| <a id="aichatinputcurrentfile"></a>`currentFile` **{warning-solid}** | [`AiCurrentFileInput`](#aicurrentfileinput) | **Deprecated:** This feature is an Experiment. It can be changed or removed at any time. Introduced in 16.6. |
| <a id="aichatinputnamespaceid"></a>`namespaceId` | [`NamespaceID`](#namespaceid) | Global ID of the namespace the user is acting on. |
| <a id="aichatinputresourceid"></a>`resourceId` | [`AiModelID`](#aimodelid) | Global ID of the resource to mutate. |
+### `AiCurrentFileInput`
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="aicurrentfileinputcontentabovecursor"></a>`contentAboveCursor` | [`String`](#string) | Content above cursor. |
+| <a id="aicurrentfileinputcontentbelowcursor"></a>`contentBelowCursor` | [`String`](#string) | Content below cursor. |
+| <a id="aicurrentfileinputfilename"></a>`fileName` | [`String!`](#string) | File name. |
+| <a id="aicurrentfileinputselectedtext"></a>`selectedText` | [`String!`](#string) | Selected text. |
+
### `AiExplainCodeInput`
#### Arguments
@@ -31606,6 +32376,14 @@ see the associated mutation type above.
| <a id="aigeneratedescriptioninputdescriptiontemplatename"></a>`descriptionTemplateName` | [`String`](#string) | Name of the description template to use to generate message off of. |
| <a id="aigeneratedescriptioninputresourceid"></a>`resourceId` | [`AiModelID!`](#aimodelid) | Global ID of the resource to mutate. |
+### `AiResolveVulnerabilityInput`
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="airesolvevulnerabilityinputresourceid"></a>`resourceId` | [`AiModelID!`](#aimodelid) | Global ID of the resource to mutate. |
+
### `AiSummarizeCommentsInput`
#### Arguments
@@ -32174,8 +32952,10 @@ A time-frame defined as a closed inclusive range of two dates.
| Name | Type | Description |
| ---- | ---- | ----------- |
-| <a id="unionedepicfilterinputauthorusername"></a>`authorUsername` | [`[String!]`](#string) | Filters epics that are authored by one of the given users. |
-| <a id="unionedepicfilterinputlabelname"></a>`labelName` | [`[String!]`](#string) | Filters epics that have at least one of the given labels. |
+| <a id="unionedepicfilterinputauthorusername"></a>`authorUsername` **{warning-solid}** | [`[String!]`](#string) | **Deprecated:** Use authorUsernames instead. Deprecated in 16.6. |
+| <a id="unionedepicfilterinputauthorusernames"></a>`authorUsernames` | [`[String!]`](#string) | Filters epics that are authored by one of the given users. |
+| <a id="unionedepicfilterinputlabelname"></a>`labelName` **{warning-solid}** | [`[String!]`](#string) | **Deprecated:** Use labelNames instead. Deprecated in 16.6. |
+| <a id="unionedepicfilterinputlabelnames"></a>`labelNames` | [`[String!]`](#string) | Filters epics that have at least one of the given labels. |
### `UnionedIssueFilterInput`
diff --git a/doc/api/group_iterations.md b/doc/api/group_iterations.md
index a2e23e29d89..b8cb1b7e053 100644
--- a/doc/api/group_iterations.md
+++ b/doc/api/group_iterations.md
@@ -16,6 +16,10 @@ There's a separate [project iterations API](iterations.md) page.
Returns a list of group iterations.
+Iterations created by **Enable automatic scheduling** in
+[Iteration cadences](../user/group/iterations/index.md#iteration-cadences) return `null` for
+the `title` and `description` fields.
+
```plaintext
GET /groups/:id/iterations
GET /groups/:id/iterations?state=opened
diff --git a/doc/api/group_protected_environments.md b/doc/api/group_protected_environments.md
index a7b0eee08b7..3010c9794b6 100644
--- a/doc/api/group_protected_environments.md
+++ b/doc/api/group_protected_environments.md
@@ -109,7 +109,7 @@ POST /groups/:id/protected_environments
| `name` | string | yes | The deployment tier of the protected environment. One of `production`, `staging`, `testing`, `development`, or `other`. Read more about [deployment tiers](../ci/environments/index.md#deployment-tier-of-environments).|
| `deploy_access_levels` | array | yes | Array of access levels allowed to deploy, with each described by a hash. One of `user_id`, `group_id` or `access_level`. They take the form of `{user_id: integer}`, `{group_id: integer}` or `{access_level: integer}` respectively. |
| `required_approval_count` | integer | no | The number of approvals required to deploy to this environment. |
-| `approval_rules` | array | no | Array of access levels allowed to approve, with each described by a hash. One of `user_id`, `group_id` or `access_level`. They take the form of `{user_id: integer}`, `{group_id: integer}` or `{access_level: integer}` respectively. You can also specify the number of required approvals from the specified entity with `required_approvals` field. See [Multiple approval rules](../ci/environments/deployment_approvals.md#multiple-approval-rules) for more information. |
+| `approval_rules` | array | no | Array of access levels allowed to approve, with each described by a hash. One of `user_id`, `group_id` or `access_level`. They take the form of `{user_id: integer}`, `{group_id: integer}` or `{access_level: integer}` respectively. You can also specify the number of required approvals from the specified entity with `required_approvals` field. See [Multiple approval rules](../ci/environments/deployment_approvals.md#add-multiple-approval-rules) for more information. |
The assignable `user_id` are the users who belong to the given group with the Maintainer role (or above).
The assignable `group_id` are the subgroups under the given group.
@@ -136,6 +136,19 @@ Example response:
}
```
+An example with multiple approval rules:
+
+```shell
+curl --header 'Content-Type: application/json' --request POST \
+ --data '{"name": "production", "deploy_access_levels": [{"group_id": 138}], "approval_rules": [{"group_id": 134}, {"group_id": 135, "required_approvals": 2}]}' \
+ --header "PRIVATE-TOKEN: <your_access_token>" \
+ "https://gitlab.example.com/api/v4/groups/128/protected_environments"
+```
+
+In this configuration, the operator group `"group_id": 138` can execute the deployment job
+to `production` only after the QA group `"group_id": 134` and security group
+`"group_id": 135` have approved the deployment.
+
## Update a protected environment
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/351854) in GitLab 15.4.
@@ -152,7 +165,7 @@ PUT /groups/:id/protected_environments/:name
| `name` | string | yes | The deployment tier of the protected environment. One of `production`, `staging`, `testing`, `development`, or `other`. Read more about [deployment tiers](../ci/environments/index.md#deployment-tier-of-environments).|
| `deploy_access_levels` | array | no | Array of access levels allowed to deploy, with each described by a hash. One of `user_id`, `group_id` or `access_level`. They take the form of `{user_id: integer}`, `{group_id: integer}` or `{access_level: integer}` respectively. |
| `required_approval_count` | integer | no | The number of approvals required to deploy to this environment. |
-| `approval_rules` | array | no | Array of access levels allowed to approve, with each described by a hash. One of `user_id`, `group_id` or `access_level`. They take the form of `{user_id: integer}`, `{group_id: integer}` or `{access_level: integer}` respectively. You can also specify the number of required approvals from the specified entity with `required_approvals` field. See [Multiple approval rules](../ci/environments/deployment_approvals.md#multiple-approval-rules) for more information. |
+| `approval_rules` | array | no | Array of access levels allowed to approve, with each described by a hash. One of `user_id`, `group_id` or `access_level`. They take the form of `{user_id: integer}`, `{group_id: integer}` or `{access_level: integer}` respectively. You can also specify the number of required approvals from the specified entity with `required_approvals` field. See [Multiple approval rules](../ci/environments/deployment_approvals.md#add-multiple-approval-rules) for more information. |
To update:
diff --git a/doc/api/groups.md b/doc/api/groups.md
index 6b17af63853..c9ec64e83db 100644
--- a/doc/api/groups.md
+++ b/doc/api/groups.md
@@ -59,6 +59,7 @@ GET /groups
"auto_devops_enabled": null,
"subgroup_creation_level": "owner",
"emails_disabled": null,
+ "emails_enabled": null,
"mentions_disabled": null,
"lfs_enabled": true,
"default_branch_protection": 2,
@@ -97,6 +98,7 @@ GET /groups?statistics=true
"auto_devops_enabled": null,
"subgroup_creation_level": "owner",
"emails_disabled": null,
+ "emails_enabled": null,
"mentions_disabled": null,
"lfs_enabled": true,
"default_branch_protection": 2,
@@ -181,6 +183,7 @@ GET /groups/:id/subgroups
"auto_devops_enabled": null,
"subgroup_creation_level": "owner",
"emails_disabled": null,
+ "emails_enabled": null,
"mentions_disabled": null,
"lfs_enabled": true,
"default_branch_protection": 2,
@@ -242,6 +245,7 @@ GET /groups/:id/descendant_groups
"auto_devops_enabled": null,
"subgroup_creation_level": "owner",
"emails_disabled": null,
+ "emails_enabled": null,
"mentions_disabled": null,
"lfs_enabled": true,
"default_branch_protection": 2,
@@ -267,6 +271,7 @@ GET /groups/:id/descendant_groups
"auto_devops_enabled": null,
"subgroup_creation_level": "owner",
"emails_disabled": null,
+ "emails_enabled": null,
"mentions_disabled": null,
"lfs_enabled": true,
"default_branch_protection": 2,
@@ -467,6 +472,7 @@ Example response:
"pages_access_level":"enabled",
"security_and_compliance_access_level":"enabled",
"emails_disabled":null,
+ "emails_enabled": null,
"shared_runners_enabled":true,
"lfs_enabled":true,
"creator_id":1,
@@ -818,7 +824,8 @@ Parameters:
| `avatar` | mixed | no | Image file for avatar of the group. [Introduced in GitLab 12.9](https://gitlab.com/gitlab-org/gitlab/-/issues/36681) |
| `default_branch_protection` | integer | no | See [Options for `default_branch_protection`](#options-for-default_branch_protection). Default to the global level default branch protection setting. |
| `description` | string | no | The group's description. |
-| `emails_disabled` | boolean | no | Disable email notifications. |
+| `emails_disabled` | boolean | no | _([Deprecated](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/127899) in GitLab 16.5.)_ Disable email notifications. Use `emails_enabled` instead. |
+| `emails_enabled` | boolean | no | Enable email notifications. |
| `lfs_enabled` | boolean | no | Enable/disable Large File Storage (LFS) for the projects in this group. |
| `mentions_disabled` | boolean | no | Disable the capability of a group from getting mentioned. |
| `parent_id` | integer | no | The parent group ID for creating nested group. |
@@ -975,7 +982,8 @@ PUT /groups/:id
| `avatar` | mixed | no | Image file for avatar of the group. [Introduced in GitLab 12.9](https://gitlab.com/gitlab-org/gitlab/-/issues/36681) |
| `default_branch_protection` | integer | no | See [Options for `default_branch_protection`](#options-for-default_branch_protection). |
| `description` | string | no | The description of the group. |
-| `emails_disabled` | boolean | no | Disable email notifications. |
+| `emails_disabled` | boolean | no | _([Deprecated](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/127899) in GitLab 16.5.)_ Disable email notifications. Use `emails_enabled` instead. |
+| `emails_enabled` | boolean | no | Enable email notifications. |
| `lfs_enabled` | boolean | no | Enable/disable Large File Storage (LFS) for the projects in this group. |
| `mentions_disabled` | boolean | no | Disable the capability of a group from getting mentioned. |
| `prevent_sharing_groups_outside_hierarchy` | boolean | no | See [Prevent group sharing outside the group hierarchy](../user/group/access_and_permissions.md#prevent-group-sharing-outside-the-group-hierarchy). This attribute is only available on top-level groups. [Introduced in GitLab 14.1](https://gitlab.com/gitlab-org/gitlab/-/issues/333721) |
@@ -1332,6 +1340,10 @@ Example response:
}
```
+| Attribute | Type | Required | Description |
+| --------- | --------------- | -------- | ----------- |
+| `expires_at` | date | no | Personal access token expiry date. When left blank, the token follows the [standard rule of expiry for personal access tokens](../user/profile/personal_access_tokens.md#when-personal-access-tokens-expire). |
+
### Rotate a Personal Access Token for Service Account User
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/406781) in GitLab 16.1.
@@ -1476,6 +1488,7 @@ PUT /groups/:id/hooks/:hook_id
| `releases_events` | boolean | no | Trigger hook on release events. |
| `subgroup_events` | boolean | no | Trigger hook on subgroup events. |
| `enable_ssl_verification` | boolean | no | Do SSL verification when triggering the hook. |
+| `service_access_tokens_expiration_enforced` | boolean | no | Require service account access tokens to have an expiration date. |
| `token` | string | no | Secret token to validate received payloads. Not returned in the response. When you change the webhook URL, the secret token is reset and not retained. |
### Delete group hook
diff --git a/doc/api/import.md b/doc/api/import.md
index 677848a0ed3..4b7abfdfec1 100644
--- a/doc/api/import.md
+++ b/doc/api/import.md
@@ -34,7 +34,6 @@ POST /import/github
| `target_namespace` | string | yes | Namespace to import repository into. Supports subgroups like `/namespace/subgroup`. In GitLab 15.8 and later, must not be blank |
| `github_hostname` | string | no | Custom GitHub Enterprise hostname. Do not set for GitHub.com. |
| `optional_stages` | object | no | [Additional items to import](../user/project/import/github.md#select-additional-items-to-import). [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/373705) in GitLab 15.5 |
-| `additional_access_tokens` | string | no | Comma-separated list of [additional](#use-multiple-github-personal-access-tokens) GitHub personal access tokens. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/337232) in GitLab 16.2 |
| `timeout_strategy` | string | no | Strategy for handling import timeouts. Valid values are `optimistic` (continue to next stage of import) or `pessimistic` (fail immediately). Defaults to `pessimistic`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/422979) in GitLab 16.5. |
```shell
@@ -80,17 +79,6 @@ Example response:
}
```
-### Use multiple GitHub personal access tokens
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/337232) in GitLab 16.2.
-
-The GitHub import API can accept more than one GitHub personal access token using the `additional_access_tokens`
-property so the API can make more calls to GitHub before hitting the rate limit. The additional GitHub personal access
-tokens:
-
-- Cannot be from the same account because they would all share one rate limit.
-- Must have the same permissions and sufficient privileges to the repositories to import.
-
### Import a public project through the API using a group access token
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/362683) in GitLab 15.7, projects are not imported into a [bot user's](../user/group/settings/group_access_tokens.md#bot-users-for-groups) namespace in any circumstances. Projects imported into a bot user's namespace could not be deleted by users with valid tokens, which represented a security risk.
diff --git a/doc/api/invitations.md b/doc/api/invitations.md
index e3619932fea..0bf38b6e616 100644
--- a/doc/api/invitations.md
+++ b/doc/api/invitations.md
@@ -43,6 +43,7 @@ POST /projects/:id/invitations
| `access_level` | integer | yes | A valid access level |
| `expires_at` | string | no | A date string in the format YEAR-MONTH-DAY |
| `invite_source` | string | no | The source of the invitation that starts the member creation process. See [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/327120). |
+| `member_role_id` **(ULTIMATE ALL)** | integer | no | Assigns the new member to the provided custom role. ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134100) in GitLab 16.6. |
```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \
diff --git a/doc/api/iterations.md b/doc/api/iterations.md
index 364cca9c977..ef718fffe0a 100644
--- a/doc/api/iterations.md
+++ b/doc/api/iterations.md
@@ -18,6 +18,10 @@ As of GitLab 13.5, we don't have project-level iterations, but you can use this
Returns a list of project iterations.
+Iterations created by **Enable automatic scheduling** in
+[Iteration cadences](../user/group/iterations/index.md#iteration-cadences) return `null` for
+the `title` and `description` fields.
+
```plaintext
GET /projects/:id/iterations
GET /projects/:id/iterations?state=opened
diff --git a/doc/api/jobs.md b/doc/api/jobs.md
index 92ab12ec0d0..06fd354f2be 100644
--- a/doc/api/jobs.md
+++ b/doc/api/jobs.md
@@ -14,8 +14,9 @@ Get a list of jobs in a project. Jobs are sorted in descending order of their ID
By default, this request returns 20 results at a time because the API results [are paginated](rest/index.md#pagination)
-This endpoint supports both offset-based and [keyset-based](rest/index.md#keyset-based-pagination) pagination. Keyset-based
-pagination is recommended when requesting consecutive pages of results.
+NOTE:
+This endpoint supports both offset-based and [keyset-based](rest/index.md#keyset-based-pagination) pagination, but keyset-based
+pagination is strongly recommended when requesting consecutive pages of results.
```plaintext
GET /projects/:id/jobs
diff --git a/doc/api/lint.md b/doc/api/lint.md
index 7b288c34343..45ae739ef86 100644
--- a/doc/api/lint.md
+++ b/doc/api/lint.md
@@ -20,7 +20,7 @@ POST /projects/:id/ci/lint
| `content` | string | Yes | The CI/CD configuration content. |
| `dry_run` | boolean | No | Run [pipeline creation simulation](../ci/lint.md#simulate-a-pipeline), or only do static check. Default: `false`. |
| `include_jobs` | boolean | No | If the list of jobs that would exist in a static check or pipeline simulation should be included in the response. Default: `false`. |
-| `ref` | string | No | When `dry_run` is `true`, sets the branch or tag to use. Defaults to the project's default branch when not set. |
+| `ref` | string | No | When `dry_run` is `true`, sets the branch or tag context to use to validate the CI/CD YAML configuration. Defaults to the project's default branch when not set. |
Example request:
@@ -71,7 +71,7 @@ GET /projects/:id/ci/lint
|----------------|---------|----------|-------------|
| `dry_run` | boolean | No | Run pipeline creation simulation, or only do static check. |
| `include_jobs` | boolean | No | If the list of jobs that would exist in a static check or pipeline simulation should be included in the response. Default: `false`. |
-| `ref` | string | No | When `dry_run` is `true`, sets the branch or tag to use. Defaults to the project's default branch when not set. |
+| `ref` | string | No | When `dry_run` is `true`, sets the branch or tag context to use to validate the CI/CD YAML configuration. Defaults to the project's default branch when not set. |
| `sha` | string | No | The commit SHA of a branch or tag. Defaults to the SHA of the head of the project's default branch when not set. |
Example request:
diff --git a/doc/api/member_roles.md b/doc/api/member_roles.md
index 79f7bc2b3ad..63de583de25 100644
--- a/doc/api/member_roles.md
+++ b/doc/api/member_roles.md
@@ -13,12 +13,14 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> - [Read dependency added](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/126247) in GitLab 16.3.
> - [Name and description fields added](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/126423) in GitLab 16.3.
> - [Admin merge request introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/128302) in GitLab 16.4 [with a flag](../administration/feature_flags.md) named `admin_merge_request`. Disabled by default.
-> - [Admin group members introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131914) in GitLab 16.5 [with a flag](../administration/feature_flags.md) named `admin_group_member`. Disabled by default.
+> - [Feature flag `admin_merge_request` removed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132578) in GitLab 16.5.
+> - [Admin group members introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131914) in GitLab 16.5 [with a flag](../administration/feature_flags.md) named `admin_group_member`. Disabled by default. The feature flag has been removed in GitLab 16.6.
> - [Manage project access tokens introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132342) in GitLab 16.5 in [with a flag](../administration/feature_flags.md) named `manage_project_access_tokens`. Disabled by default.
+> - [Archive project introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134998) in GitLab 16.6 in [with a flag](../administration/feature_flags.md) named `archive_project`. Disabled by default.
FLAG:
-On self-managed GitLab, by default these two features are not available. To make them available, an administrator can [enable the feature flags](../administration/feature_flags.md) named `admin_merge_request` and `admin_member_custom_role`.
-On GitLab.com, this feature is not available.
+On self-managed GitLab, by default these features are not available. To make them available, an administrator can [enable the feature flags](../administration/feature_flags.md) named `admin_group_member`, `manage_project_access_tokens` and `archive_project`.
+On GitLab.com, these features are not available.
## List all member roles of a group
@@ -48,6 +50,7 @@ If successful, returns [`200`](rest/index.md#status-codes) and the following res
| `[].read_vulnerability` | boolean | Permission to read project vulnerabilities. |
| `[].admin_group_member` | boolean | Permission to admin members of a group. |
| `[].manage_project_access_tokens` | boolean | Permission to manage project access tokens. |
+| `[].archive_project` | boolean | Permission to archive projects. |
Example request:
@@ -70,7 +73,8 @@ Example response:
"read_code": true,
"read_dependency": false,
"read_vulnerability": false,
- "manage_project_access_tokens": false
+ "manage_project_access_tokens": false,
+ "archive_project": false
},
{
"id": 3,
@@ -83,7 +87,8 @@ Example response:
"read_code": false,
"read_dependency": true,
"read_vulnerability": true,
- "manage_project_access_tokens": false
+ "manage_project_access_tokens": false,
+ "archive_project": false
}
]
```
diff --git a/doc/api/merge_request_approvals.md b/doc/api/merge_request_approvals.md
index fd8026d3077..628f274c38f 100644
--- a/doc/api/merge_request_approvals.md
+++ b/doc/api/merge_request_approvals.md
@@ -1039,7 +1039,7 @@ Supported attributes:
| Attribute | Type | Required | Description |
|---------------------|-------------------|------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `id` | integer or string | Yes | The ID or [URL-encoded path of a project](rest/index.md#namespaced-path-encoding). |
-| `approval_password` | string | No | Current user's password. Required if [**Require user password to approve**](../user/project/merge_requests/approvals/settings.md#require-user-password-to-approve) is enabled in the project settings. |
+| `approval_password` | string | No | Current user's password. Required if [**Require user re-authentication to approve**](../user/project/merge_requests/approvals/settings.md#require-user-re-authentication-to-approve) is enabled in the project settings. |
| `merge_request_iid` | integer | Yes | The IID of the merge request. |
| `sha` | string | No | The `HEAD` of the merge request. |
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index e32c6a2ab56..bf071e9ae51 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -1161,6 +1161,10 @@ Example response:
]
```
+NOTE:
+This endpoint is subject to [Merge requests diff limits](../administration/instance_limits.md#diff-limits).
+Merge requests that exceed the diff limits return limited results.
+
## List merge request pipelines
Get a list of merge request pipelines. The pagination parameters `page` and
diff --git a/doc/api/packages.md b/doc/api/packages.md
index a378be26a24..7c8dfeb8710 100644
--- a/doc/api/packages.md
+++ b/doc/api/packages.md
@@ -8,11 +8,11 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/349418) support for [GitLab CI/CD job token](../ci/jobs/ci_job_token.md) authentication for the project-level API in GitLab 15.3.
-This is the API documentation of [GitLab Packages](../administration/packages/index.md).
+The API documentation of [GitLab Packages](../administration/packages/index.md).
## List packages
-### Within a project
+### For a project
Get a list of project packages. All package types are included in results. When
accessed without authentication, only packages of public projects are returned.
@@ -23,15 +23,16 @@ packages.
GET /projects/:id/packages
```
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) |
-| `order_by`| string | no | The field to use as order. One of `created_at` (default), `name`, `version`, or `type`. |
-| `sort` | string | no | The direction of the order, either `asc` (default) for ascending order or `desc` for descending order. |
-| `package_type` | string | no | Filter the returned packages by type. One of `conan`, `maven`, `npm`, `pypi`, `composer`, `nuget`, `helm`, `terraform_module`, or `golang`. (_Introduced in GitLab 12.9_)
-| `package_name` | string | no | Filter the project packages with a fuzzy search by name. (_Introduced in GitLab 12.9_)
-| `include_versionless` | boolean | no | When set to true, versionless packages are included in the response. (_Introduced in GitLab 13.8_)
-| `status` | string | no | Filter the returned packages by status. One of `default` (default), `hidden`, `processing`, `error`, or `pending_destruction`. (_Introduced in GitLab 13.9_)
+| Attribute | Type | Required | Description |
+|:----------------------|:---------------|:---------|:------------|
+| `id` | integer/string | yes | ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding). |
+| `order_by` | string | no | The field to use as order. One of `created_at` (default), `name`, `version`, or `type`. |
+| `sort` | string | no | The direction of the order, either `asc` (default) for ascending order or `desc` for descending order. |
+| `package_type` | string | no | Filter the returned packages by type. One of `conan`, `maven`, `npm`, `pypi`, `composer`, `nuget`, `helm`, `terraform_module`, or `golang`. |
+| `package_name` | string | no | Filter the project packages with a fuzzy search by name. |
+| `package_version` | string | no | Filter the project packages by version. If used in combination with `include_versionless`, then no versionless packages are returned. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/349065) in GitLab 16.6. |
+| `include_versionless` | boolean | no | When set to true, versionless packages are included in the response. |
+| `status` | string | no | Filter the returned packages by status. One of `default` (default), `hidden`, `processing`, `error`, or `pending_destruction`. |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/:id/packages"
@@ -76,9 +77,7 @@ By default, the `GET` request returns 20 results, because the API is [paginated]
Although you can filter packages by status, working with packages that have a `processing` status
can result in malformed data or broken packages.
-### Within a group
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/18871) in GitLab 12.5.
+### For a group
Get a list of project packages at the group level.
When accessed without authentication, only packages of public projects are returned.
@@ -89,26 +88,22 @@ packages.
GET /groups/:id/packages
```
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | ID or [URL-encoded path of the group](rest/index.md#namespaced-path-encoding). |
-| `exclude_subgroups` | boolean | false | If the parameter is included as true, packages from projects from subgroups are not listed. Default is `false`. |
-| `order_by`| string | no | The field to use as order. One of `created_at` (default), `name`, `version`, `type`, or `project_path`. |
-| `sort` | string | no | The direction of the order, either `asc` (default) for ascending order or `desc` for descending order. |
-| `package_type` | string | no | Filter the returned packages by type. One of `conan`, `maven`, `npm`, `pypi`, `composer`, `nuget`, `helm`, or `golang`. (_Introduced in GitLab 12.9_) |
-| `package_name` | string | no | Filter the project packages with a fuzzy search by name. (_[Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/30980) in GitLab 13.0_)
-| `include_versionless` | boolean | no | When set to true, versionless packages are included in the response. (_Introduced in GitLab 13.8_)
-| `status` | string | no | Filter the returned packages by status. One of `default` (default), `hidden`, `processing`, `error`, or `pending_destruction`. (_Introduced in GitLab 13.9_)
+| Attribute | Type | Required | Description |
+|:----------------------|:---------------|:---------|:------------|
+| `id` | integer/string | yes | ID or [URL-encoded path of the group](rest/index.md#namespaced-path-encoding). |
+| `exclude_subgroups` | boolean | false | If the parameter is included as true, packages from projects from subgroups are not listed. Default is `false`. |
+| `order_by` | string | no | The field to use as order. One of `created_at` (default), `name`, `version`, `type`, or `project_path`. |
+| `sort` | string | no | The direction of the order, either `asc` (default) for ascending order or `desc` for descending order. |
+| `package_type` | string | no | Filter the returned packages by type. One of `conan`, `maven`, `npm`, `pypi`, `composer`, `nuget`, `helm`, or `golang`. |
+| `package_name` | string | no | Filter the project packages with a fuzzy search by name. |
+| `package_version` | string | no | Filter the returned packages by version. If used in combination with `include_versionless`, then no versionless packages are returned. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/349065) in GitLab 16.6. |
+| `include_versionless` | boolean | no | When set to true, versionless packages are included in the response. |
+| `status` | string | no | Filter the returned packages by status. One of `default` (default), `hidden`, `processing`, `error`, or `pending_destruction`. |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/:id/packages?exclude_subgroups=false"
```
-> **Deprecation:**
->
-> The `pipeline` attribute in the response is deprecated in favor of `pipelines`, which was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44348) in GitLab 13.6. Both are available until 13.7.
-> The `build_info` attribute in the response is deprecated in favor of `pipeline`, which was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28040) in GitLab 12.10.
-
Example response:
```json
@@ -195,11 +190,6 @@ GET /projects/:id/packages/:package_id
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/:id/packages/:package_id"
```
-> **Deprecation:**
->
-> The `pipeline` attribute in the response is deprecated in favor of `pipelines`, which was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44348) in GitLab 13.6. Both are available until 13.7.
-> The `build_info` attribute in the response is deprecated in favor of `pipeline`, which was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28040) in GitLab 12.10.
-
Example response:
```json
@@ -213,7 +203,7 @@ Example response:
"delete_api_path": "/namespace1/project1/-/packages/1"
},
"created_at": "2019-11-27T03:37:38.711Z",
- "last_downloaded_at": "2022-09-07T07:51:50.504Z"
+ "last_downloaded_at": "2022-09-07T07:51:50.504Z",
"pipelines": [
{
"id": 123,
@@ -425,8 +415,6 @@ deleting a package can introduce a [dependency confusion risk](../user/packages/
## Delete a package file
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/32107) in GitLab 13.12.
-
WARNING:
Deleting a package file may corrupt your package making it unusable or unpullable from your package
manager client. When deleting a package file, be sure that you understand what you're doing.
diff --git a/doc/api/pipelines.md b/doc/api/pipelines.md
index e908f4adb34..50616974ae1 100644
--- a/doc/api/pipelines.md
+++ b/doc/api/pipelines.md
@@ -38,7 +38,7 @@ GET /projects/:id/pipelines
| `id` | integer/string | Yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) |
| `scope` | string | No | The scope of pipelines, one of: `running`, `pending`, `finished`, `branches`, `tags` |
| `status` | string | No | The status of pipelines, one of: `created`, `waiting_for_resource`, `preparing`, `pending`, `running`, `success`, `failed`, `canceled`, `skipped`, `manual`, `scheduled` |
-| `source` | string | No | In [GitLab 14.3 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/325439), how the pipeline was triggered, one of: `push`, `web`, `trigger`, `schedule`, `api`, `external`, `pipeline`, `chat`, `webide`, `merge_request_event`, `external_pull_request_event`, `parent_pipeline`, `ondemand_dast_scan`, or `ondemand_dast_validation`. |
+| `source` | string | No | In [GitLab 14.3 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/325439), how the pipeline was triggered, one of: `api`, `chat`, `external`, `external_pull_request_event`, `merge_request_event`, `ondemand_dast_scan`, `ondemand_dast_validation`, `parent_pipeline`, `pipeline`, `push`, `schedule`, `security_orchestration_policy`, `trigger`, `web`, or `webide`. |
| `ref` | string | No | The ref of pipelines |
| `sha` | string | No | The SHA of pipelines |
| `yaml_errors` | boolean | No | Returns pipelines with invalid configurations |
@@ -518,3 +518,57 @@ DELETE /projects/:id/pipelines/:pipeline_id
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" --request "DELETE" "https://gitlab.example.com/api/v4/projects/1/pipelines/46"
```
+
+## Update pipeline metadata
+
+You can update the metadata of a pipeline. The metadata contains the name of the pipeline.
+
+```plaintext
+PUT /projects/:id/pipelines/:pipeline_id/metadata
+```
+
+| Attribute | Type | Required | Description |
+|---------------|----------------|----------|-------------|
+| `id` | integer/string | Yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) |
+| `pipeline_id` | integer | Yes | The ID of a pipeline |
+| `name` | string | Yes | The new name of the pipeline |
+
+Sample request:
+
+```shell
+curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" --data "name=Some new pipeline name" "https://gitlab.example.com/api/v4/projects/1/pipelines/46/metadata"
+```
+
+Sample response:
+
+```json
+{
+ "id": 46,
+ "iid": 11,
+ "project_id": 1,
+ "status": "running",
+ "ref": "main",
+ "sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
+ "before_sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
+ "tag": false,
+ "yaml_errors": null,
+ "user": {
+ "name": "Administrator",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "http://localhost:3000/root"
+ },
+ "created_at": "2016-08-11T11:28:34.085Z",
+ "updated_at": "2016-08-11T11:32:35.169Z",
+ "started_at": null,
+ "finished_at": "2016-08-11T11:32:35.145Z",
+ "committed_at": null,
+ "duration": null,
+ "queued_duration": 0.010,
+ "coverage": null,
+ "web_url": "https://example.com/foo/bar/pipelines/46",
+ "name": "Some new pipeline name"
+}
+```
diff --git a/doc/api/projects.md b/doc/api/projects.md
index f909f376fce..f4a9e396930 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -57,6 +57,8 @@ GET /projects
| `id_after` | integer | No | Limit results to projects with IDs greater than the specified ID. |
| `id_before` | integer | No | Limit results to projects with IDs less than the specified ID. |
| `imported` | boolean | No | Limit results to projects which were imported from external systems by current user. |
+| `include_hidden` **(PREMIUM ALL)** | boolean | No | Include hidden projects. _(administrators only)_ |
+| `include_pending_delete` | boolean | No | Include projects pending deletion. _(administrators only)_ |
| `last_activity_after` | datetime | No | Limit results to projects with last activity after specified time. Format: ISO 8601 (`YYYY-MM-DDTHH:MM:SSZ`) |
| `last_activity_before` | datetime | No | Limit results to projects with last activity before specified time. Format: ISO 8601 (`YYYY-MM-DDTHH:MM:SSZ`) |
| `membership` | boolean | No | Limit by projects that the current user is a member of. |
@@ -1794,6 +1796,7 @@ POST /projects/:id/fork
| `namespace` | integer or string | No | _(Deprecated)_ The ID or path of the namespace that the project is forked to. |
| `path` | string | No | The path assigned to the resultant project after forking. |
| `visibility` | string | No | The [visibility level](#project-visibility-level) assigned to the resultant project after forking. |
+| `branches` | string | No | Branches to fork (empty for all branches). |
## List forks of a project
diff --git a/doc/api/protected_environments.md b/doc/api/protected_environments.md
index 5a25844c754..8b502d78d0d 100644
--- a/doc/api/protected_environments.md
+++ b/doc/api/protected_environments.md
@@ -117,11 +117,11 @@ POST /projects/:id/protected_environments
| `name` | string | yes | The name of the environment. |
| `deploy_access_levels` | array | yes | Array of access levels allowed to deploy, with each described by a hash. |
| `required_approval_count` | integer | no | The number of approvals required to deploy to this environment. |
-| `approval_rules` | array | no | Array of access levels allowed to approve, with each described by a hash. See [Multiple approval rules](../ci/environments/deployment_approvals.md#multiple-approval-rules) for more information. |
+| `approval_rules` | array | no | Array of access levels allowed to approve, with each described by a hash. See [Multiple approval rules](../ci/environments/deployment_approvals.md#add-multiple-approval-rules). |
Elements in the `deploy_access_levels` and `approval_rules` array should be one of `user_id`, `group_id` or
`access_level`, and take the form `{user_id: integer}`, `{group_id: integer}` or
-`{access_level: integer}`. Optionally you can specify the `group_inheritance_type` on each as one of the [valid group inheritance types](#group-inheritance-types).
+`{access_level: integer}`. Optionally, you can specify the `group_inheritance_type` on each as one of the [valid group inheritance types](#group-inheritance-types).
Each user must have access to the project and each group must [have this project shared](../user/project/members/share_project_with_groups.md).
@@ -187,7 +187,7 @@ PUT /projects/:id/protected_environments/:name
| `name` | string | yes | The name of the environment. |
| `deploy_access_levels` | array | no | Array of access levels allowed to deploy, with each described by a hash. |
| `required_approval_count` | integer | no | The number of approvals required to deploy to this environment. |
-| `approval_rules` | array | no | Array of access levels allowed to approve, with each described by a hash. See [Multiple approval rules](../ci/environments/deployment_approvals.md#multiple-approval-rules) for more information. |
+| `approval_rules` | array | no | Array of access levels allowed to approve, with each described by a hash. See [Multiple approval rules](../ci/environments/deployment_approvals.md#add-multiple-approval-rules) for more information. |
Elements in the `deploy_access_levels` and `approval_rules` array should be one of `user_id`, `group_id` or
`access_level`, and take the form `{user_id: integer}`, `{group_id: integer}` or
diff --git a/doc/api/rest/index.md b/doc/api/rest/index.md
index 039129d24c6..fd98952185b 100644
--- a/doc/api/rest/index.md
+++ b/doc/api/rest/index.md
@@ -804,6 +804,7 @@ For questions about these integrations, use the [GitLab community forum](https:/
### `C#`
- [`GitLabApiClient`](https://github.com/nmklotas/GitLabApiClient)
+- [`NGitLab`](https://github.com/ubisoft/NGitLab)
### Go
diff --git a/doc/api/runners.md b/doc/api/runners.md
index dba37edcb01..372ce397332 100644
--- a/doc/api/runners.md
+++ b/doc/api/runners.md
@@ -52,13 +52,14 @@ GET /runners?paused=true
GET /runners?tag_list=tag1,tag2
```
-| Attribute | Type | Required | Description |
-|------------|--------------|----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| `scope` | string | no | Deprecated: Use `type` or `status` instead. The scope of runners to return, one of: `active`, `paused`, `online` and `offline`; showing all runners if none provided |
-| `type` | string | no | The type of runners to return, one of: `instance_type`, `group_type`, `project_type` |
-| `status` | string | no | The status of runners to return, one of: `online`, `offline`, `stale`, and `never_contacted`. `active` and `paused` are also possible values which were deprecated in GitLab 14.8 and will be removed in GitLab 16.0 |
-| `paused` | boolean | no | Whether to include only runners that are accepting or ignoring new jobs |
-| `tag_list` | string array | no | A list of runner tags |
+| Attribute | Type | Required | Description |
+|------------------|--------------|----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `scope` | string | no | Deprecated: Use `type` or `status` instead. The scope of runners to return, one of: `active`, `paused`, `online` and `offline`; showing all runners if none provided |
+| `type` | string | no | The type of runners to return, one of: `instance_type`, `group_type`, `project_type` |
+| `status` | string | no | The status of runners to return, one of: `online`, `offline`, `stale`, and `never_contacted`. `active` and `paused` are also possible values which were deprecated in GitLab 14.8 and will be removed in a future version of the REST API |
+| `paused` | boolean | no | Whether to include only runners that are accepting or ignoring new jobs |
+| `tag_list` | string array | no | A list of runner tags |
+| `version_prefix` | string | no | The prefix of the version of the runners to return. For example, `15.0`, `14`, `16.1.241` |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/runners"
@@ -66,11 +67,11 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/a
NOTE:
The `active` and `paused` values in the `status` query parameter were deprecated [in GitLab 14.8](https://gitlab.com/gitlab-org/gitlab/-/issues/347211).
-and will be removed in [GitLab 16.0](https://gitlab.com/gitlab-org/gitlab/-/issues/351109). They are replaced by the `paused` query parameter.
+and will be removed in [a future version of the REST API](https://gitlab.com/gitlab-org/gitlab/-/issues/351109). They are replaced by the `paused` query parameter.
NOTE:
The `active` attribute in the response was deprecated [in GitLab 14.8](https://gitlab.com/gitlab-org/gitlab/-/issues/347211).
-and will be removed in [GitLab 16.0](https://gitlab.com/gitlab-org/gitlab/-/issues/351109). It is replaced by the `paused` attribute.
+and will be removed in [a future version of the REST API](https://gitlab.com/gitlab-org/gitlab/-/issues/351109). It is replaced by the `paused` attribute.
Example response:
@@ -117,13 +118,14 @@ GET /runners/all?paused=true
GET /runners/all?tag_list=tag1,tag2
```
-| Attribute | Type | Required | Description |
-|------------|--------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| `scope` | string | no | Deprecated: Use `type` or `status` instead. The scope of runners to return, one of: `specific`, `shared`, `active`, `paused`, `online` and `offline`; showing all runners if none provided |
-| `type` | string | no | The type of runners to return, one of: `instance_type`, `group_type`, `project_type` |
-| `status` | string | no | The status of runners to return, one of: `online`, `offline`, `stale`, and `never_contacted`. `active` and `paused` are also possible values which were deprecated in GitLab 14.8 and will be removed in GitLab 16.0 |
-| `paused` | boolean | no | Whether to include only runners that are accepting or ignoring new jobs |
-| `tag_list` | string array | no | A list of runner tags |
+| Attribute | Type | Required | Description |
+|------------------|--------------|----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `scope` | string | no | Deprecated: Use `type` or `status` instead. The scope of runners to return, one of: `specific`, `shared`, `active`, `paused`, `online` and `offline`; showing all runners if none provided |
+| `type` | string | no | The type of runners to return, one of: `instance_type`, `group_type`, `project_type` |
+| `status` | string | no | The status of runners to return, one of: `online`, `offline`, `stale`, and `never_contacted`. `active` and `paused` are also possible values which were deprecated in GitLab 14.8 and will be removed in a future version of the REST API |
+| `paused` | boolean | no | Whether to include only runners that are accepting or ignoring new jobs |
+| `tag_list` | string array | no | A list of runner tags |
+| `version_prefix` | string | no | The prefix of the version of the runners to return. For example, `15.0`, `14`, `16.1.241` |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/runners/all"
@@ -131,11 +133,11 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/a
NOTE:
The `active` and `paused` values in the `status` query parameter were deprecated [in GitLab 14.8](https://gitlab.com/gitlab-org/gitlab/-/issues/347211).
-and will be removed in [GitLab 16.0](https://gitlab.com/gitlab-org/gitlab/-/issues/351109). They are replaced by the `paused` query parameter.
+and will be removed in [a future version of the REST API](https://gitlab.com/gitlab-org/gitlab/-/issues/351109). They are replaced by the `paused` query parameter.
NOTE:
The `active` attribute in the response was deprecated [in GitLab 14.8](https://gitlab.com/gitlab-org/gitlab/-/issues/347211).
-and will be removed in [GitLab 16.0](https://gitlab.com/gitlab-org/gitlab/-/issues/351109). It is replaced by the `paused` attribute.
+and will be removed in [a future version of the REST API](https://gitlab.com/gitlab-org/gitlab/-/issues/351109). It is replaced by the `paused` attribute.
Example response:
@@ -221,7 +223,7 @@ and removed in [GitLab 13.0](https://gitlab.com/gitlab-org/gitlab/-/issues/21432
NOTE:
The `active` attribute in the response was deprecated [in GitLab 14.8](https://gitlab.com/gitlab-org/gitlab/-/issues/347211).
-and will be removed in [GitLab 16.0](https://gitlab.com/gitlab-org/gitlab/-/issues/351109). It is replaced by the `paused` attribute.
+and will be removed in [a future version of the REST API](https://gitlab.com/gitlab-org/gitlab/-/issues/351109). It is replaced by the `paused` attribute.
Example response:
@@ -291,7 +293,7 @@ and [removed](https://gitlab.com/gitlab-org/gitlab/-/issues/214322) in GitLab 13
NOTE:
The `active` query parameter was deprecated [in GitLab 14.8](https://gitlab.com/gitlab-org/gitlab/-/issues/347211).
-and will be removed in [GitLab 16.0](https://gitlab.com/gitlab-org/gitlab/-/issues/351109). It is replaced by the `paused` attribute.
+and will be removed in [a future version of the REST API](https://gitlab.com/gitlab-org/gitlab/-/issues/351109). It is replaced by the `paused` attribute.
Example response:
@@ -361,7 +363,7 @@ curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" \
NOTE:
The `active` form attribute was deprecated [in GitLab 14.8](https://gitlab.com/gitlab-org/gitlab/-/issues/347211).
-and will be removed in [GitLab 16.0](https://gitlab.com/gitlab-org/gitlab/-/issues/351109). It is replaced by the `paused` attribute.
+and will be removed in [a future version of the REST API](https://gitlab.com/gitlab-org/gitlab/-/issues/351109). It is replaced by the `paused` attribute.
## List runner's jobs
@@ -468,14 +470,15 @@ GET /projects/:id/runners/all?paused=true
GET /projects/:id/runners?tag_list=tag1,tag2
```
-| Attribute | Type | Required | Description |
-|------------|----------------|----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) owned by the authenticated user |
-| `scope` | string | no | Deprecated: Use `type` or `status` instead. The scope of runners to return, one of: `active`, `paused`, `online` and `offline`; showing all runners if none provided |
-| `type` | string | no | The type of runners to return, one of: `instance_type`, `group_type`, `project_type` |
-| `status` | string | no | The status of runners to return, one of: `online`, `offline`, `stale`, and `never_contacted`. `active` and `paused` are also possible values which were deprecated in GitLab 14.8 and will be removed in GitLab 16.0 |
-| `paused` | boolean | no | Whether to include only runners that are accepting or ignoring new jobs |
-| `tag_list` | string array | no | A list of runner tags |
+| Attribute | Type | Required | Description |
+|------------------|----------------|----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) owned by the authenticated user |
+| `scope` | string | no | Deprecated: Use `type` or `status` instead. The scope of runners to return, one of: `active`, `paused`, `online` and `offline`; showing all runners if none provided |
+| `type` | string | no | The type of runners to return, one of: `instance_type`, `group_type`, `project_type` |
+| `status` | string | no | The status of runners to return, one of: `online`, `offline`, `stale`, and `never_contacted`. `active` and `paused` are also possible values which were deprecated in GitLab 14.8 and will be removed in a future version of the REST API |
+| `paused` | boolean | no | Whether to include only runners that are accepting or ignoring new jobs |
+| `tag_list` | string array | no | A list of runner tags |
+| `version_prefix` | string | no | The prefix of the version of the runners to return. For example, `15.0`, `14`, `16.1.241` |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/9/runners"
@@ -483,11 +486,11 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/a
NOTE:
The `active` and `paused` values in the `status` query parameter were deprecated [in GitLab 14.8](https://gitlab.com/gitlab-org/gitlab/-/issues/347211).
-and will be removed in [GitLab 16.0](https://gitlab.com/gitlab-org/gitlab/-/issues/351109). They are replaced by the `paused` query parameter.
+and will be removed in [a future version of the REST API](https://gitlab.com/gitlab-org/gitlab/-/issues/351109). They are replaced by the `paused` query parameter.
NOTE:
The `active` attribute in the response was deprecated [in GitLab 14.8](https://gitlab.com/gitlab-org/gitlab/-/issues/347211).
-and will be removed in [GitLab 16.0](https://gitlab.com/gitlab-org/gitlab/-/issues/351109). It is replaced by the `paused` attribute.
+and will be removed in [a future version of the REST API](https://gitlab.com/gitlab-org/gitlab/-/issues/351109). It is replaced by the `paused` attribute.
Example response:
@@ -585,13 +588,14 @@ GET /groups/:id/runners/all?paused=true
GET /groups/:id/runners?tag_list=tag1,tag2
```
-| Attribute | Type | Required | Description |
-|------------|----------------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| `id` | integer | yes | The ID of the group owned by the authenticated user |
-| `type` | string | no | The type of runners to return, one of: `instance_type`, `group_type`, `project_type`. The `project_type` value is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/351466) and will be removed in GitLab 15.0 |
-| `status` | string | no | The status of runners to return, one of: `online`, `offline`, `stale`, and `never_contacted`. `active` and `paused` are also possible values which were deprecated in GitLab 14.8 and will be removed in GitLab 16.0 |
-| `paused` | boolean | no | Whether to include only runners that are accepting or ignoring new jobs |
-| `tag_list` | string array | no | A list of runner tags |
+| Attribute | Type | Required | Description |
+|------------------|----------------|----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `id` | integer | yes | The ID of the group owned by the authenticated user |
+| `type` | string | no | The type of runners to return, one of: `instance_type`, `group_type`, `project_type`. The `project_type` value is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/351466) and will be removed in GitLab 15.0 |
+| `status` | string | no | The status of runners to return, one of: `online`, `offline`, `stale`, and `never_contacted`. `active` and `paused` are also possible values which were deprecated in GitLab 14.8 and will be removed in a future version of the REST API |
+| `paused` | boolean | no | Whether to include only runners that are accepting or ignoring new jobs |
+| `tag_list` | string array | no | A list of runner tags |
+| `version_prefix` | string | no | The prefix of the version of the runners to return. For example, `15.0`, `14`, `16.1.241` |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/9/runners"
@@ -599,11 +603,11 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/a
NOTE:
The `active` and `paused` values in the `status` query parameter were deprecated [in GitLab 14.8](https://gitlab.com/gitlab-org/gitlab/-/issues/347211).
-and will be removed in [GitLab 16.0](https://gitlab.com/gitlab-org/gitlab/-/issues/351109). They are replaced by the `paused` query parameter.
+and will be removed in [a future version of the REST API](https://gitlab.com/gitlab-org/gitlab/-/issues/351109). They are replaced by the `paused` query parameter.
NOTE:
The `active` attribute in the response was deprecated [in GitLab 14.8](https://gitlab.com/gitlab-org/gitlab/-/issues/347211).
-and will be removed in [GitLab 16.0](https://gitlab.com/gitlab-org/gitlab/-/issues/351109). It is replaced by the `paused` attribute.
+and will be removed in [a future version of the REST API](https://gitlab.com/gitlab-org/gitlab/-/issues/351109). It is replaced by the `paused` attribute.
Example response:
diff --git a/doc/api/saml.md b/doc/api/saml.md
index 911586933fa..5c6eee2b73c 100644
--- a/doc/api/saml.md
+++ b/doc/api/saml.md
@@ -43,7 +43,7 @@ Example response:
```json
[
{
- "extern_uid": "4",
+ "extern_uid": "yrnZW46BrtBFqM7xDzE7dddd",
"user_id": 48
}
]
@@ -67,14 +67,14 @@ Supported attributes:
Example request:
```shell
-curl --location --request GET "https://gitlab.example.com/api/v4/groups/33/saml/sydney_jones" --header "PRIVATE-TOKEN: <PRIVATE TOKEN>"
+curl --location --request GET "https://gitlab.example.com/api/v4/groups/33/saml/yrnZW46BrtBFqM7xDzE7dddd" --header "PRIVATE-TOKEN: <PRIVATE TOKEN>"
```
Example response:
```json
{
- "extern_uid": "4",
+ "extern_uid": "yrnZW46BrtBFqM7xDzE7dddd",
"user_id": 48
}
```
@@ -101,9 +101,9 @@ Supported attributes:
Example request:
```shell
-curl --location --request PATCH "https://gitlab.example.com/api/v4/groups/33/saml/sydney_jones" \
+curl --location --request PATCH "https://gitlab.example.com/api/v4/groups/33/saml/yrnZW46BrtBFqM7xDzE7dddd" \
--header "PRIVATE-TOKEN: <PRIVATE TOKEN>" \
---form "extern_uid=sydney_jones_new"
+--form "extern_uid=be20d8dcc028677c931e04f387"
```
## Delete a single SAML identity
@@ -124,7 +124,7 @@ Supported attributes:
Example request:
```shell
-curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/33/saml/sydney_jones"
+curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/33/saml/be20d8dcc028677c931e04f387"
```
diff --git a/doc/api/scim.md b/doc/api/scim.md
index 8840935e646..f3be1a479a8 100644
--- a/doc/api/scim.md
+++ b/doc/api/scim.md
@@ -8,7 +8,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/98354) in GitLab 15.5.
-The GitLab SCIM API manages SCIM identities within groups and provides the `/Users` endpoint. The base URL is `/api/scim/v2/groups/:group_path/Users/`.
+The GitLab SCIM API manages SCIM identities within groups and provides the `/groups/:groups_id/scim/identities` and `/groups/:groups_id/scim/:uid` endpoints. The base URL is `<http|https>://<GitLab host>/api/v4`.
To use this API, [Group SSO](../user/group/saml_sso/index.md) must be enabled for the group.
This API is only in use where [SCIM for Group SSO](../user/group/saml_sso/scim_setup.md) is enabled. It's a prerequisite to the creation of SCIM identities.
@@ -53,7 +53,7 @@ Example response:
```json
[
{
- "extern_uid": "4",
+ "extern_uid": "be20d8dcc028677c931e04f387",
"user_id": 48,
"active": true
}
@@ -85,14 +85,14 @@ Supported attributes:
Example request:
```shell
-curl --location --request GET "https://gitlab.example.com/api/v4/groups/33/scim/sydney_jones" --header "PRIVATE-TOKEN: <PRIVATE TOKEN>"
+curl --location --request GET "https://gitlab.example.com/api/v4/groups/33/scim/be20d8dcc028677c931e04f387" --header "PRIVATE-TOKEN: <PRIVATE TOKEN>"
```
Example response:
```json
{
- "extern_uid": "4",
+ "extern_uid": "be20d8dcc028677c931e04f387",
"user_id": 48,
"active": true
}
@@ -122,9 +122,9 @@ Parameters:
Example request:
```shell
-curl --location --request PATCH "https://gitlab.example.com/api/v4/groups/33/scim/sydney_jones" \
+curl --location --request PATCH "https://gitlab.example.com/api/v4/groups/33/scim/be20d8dcc028677c931e04f387" \
--header "PRIVATE-TOKEN: <PRIVATE TOKEN>" \
---form "extern_uid=sydney_jones_new"
+--form "extern_uid=yrnZW46BrtBFqM7xDzE7dddd"
```
## Delete a single SCIM identity
@@ -145,7 +145,7 @@ Supported attributes:
Example request:
```shell
-curl --request DELETE --header "Content-Type: application/json" --header "Authorization: Bearer <your_access_token>" "https://gitlab.example.com/api/v4/groups/33/scim/sydney_jones"
+curl --request DELETE --header "Content-Type: application/json" --header "Authorization: Bearer <your_access_token>" "https://gitlab.example.com/api/v4/groups/33/scim/yrnZW46BrtBFqM7xDzE7dddd"
```
diff --git a/doc/api/settings.md b/doc/api/settings.md
index 03877c6c489..9c0a1e8e4a8 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -19,6 +19,8 @@ For information on how to control the application settings cache for an instance
> - `always_perform_delayed_deletion` feature flag [enabled](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/113332) in GitLab 15.11.
> - `delayed_project_deletion` and `delayed_group_deletion` attributes removed in GitLab 16.0.
+> - `in_product_marketing_emails_enabled` attribute [removed](https://gitlab.com/gitlab-org/gitlab/-/issues/418137) in GitLab 16.6.
+> - `repository_storages` attribute [removed](https://gitlab.com/gitlab-org/gitlab/-/issues/429675) in GitLab 16.6.
List the current [application settings](#list-of-settings-that-can-be-accessed-via-api-calls)
of the GitLab instance.
@@ -215,7 +217,6 @@ Example response:
"container_registry_token_expire_delay": 5,
"decompress_archive_file_timeout": 210,
"package_registry_cleanup_policies_worker_capacity": 2,
- "repository_storages": ["default"],
"plantuml_enabled": false,
"plantuml_url": null,
"diagramsnet_enabled": true,
@@ -433,6 +434,7 @@ listed in the descriptions of the relevant settings.
| `gitaly_timeout_fast` | integer | no | Gitaly fast operation timeout, in seconds. Some Gitaly operations are expected to be fast. If they exceed this threshold, there may be a problem with a storage shard and 'failing fast' can help maintain the stability of the GitLab instance. Set to `0` to disable timeouts. |
| `gitaly_timeout_medium` | integer | no | Medium Gitaly timeout, in seconds. This should be a value between the Fast and the Default timeout. Set to `0` to disable timeouts. |
| `gitlab_dedicated_instance` | boolean | no | Indicates whether the instance was provisioned for GitLab Dedicated. |
+| `gitlab_shell_operation_limit` | integer | no | Maximum number of Git operations per minute a user can perform. Default: `600`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/412088) in GitLab 16.2. |
| `grafana_enabled` | boolean | no | Enable Grafana. |
| `grafana_url` | string | no | Grafana URL. |
| `gravatar_enabled` | boolean | no | Enable Gravatar. |
@@ -452,9 +454,10 @@ listed in the descriptions of the relevant settings.
| `housekeeping_optimize_repository_period`| integer | no | Number of Git pushes after which an incremental `git repack` is run. |
| `html_emails_enabled` | boolean | no | Enable HTML emails. |
| `import_sources` | array of strings | no | Sources to allow project import from, possible values: `github`, `bitbucket`, `bitbucket_server`, `fogbugz`, `git`, `gitlab_project`, `gitea`, and `manifest`. |
-| `in_product_marketing_emails_enabled` | boolean | no | Enable [in-product marketing emails](../user/profile/notifications.md#global-notification-settings). Enabled by default. |
| `invisible_captcha_enabled` | boolean | no | Enable Invisible CAPTCHA spam detection during sign-up. Disabled by default. |
| `issues_create_limit` | integer | no | Max number of issue creation requests per minute per user. Disabled by default.|
+| `jira_connect_application_key` | String | no | Application ID of the OAuth application that should be used to authenticate with the GitLab for Jira Cloud app |
+| `jira_connect_proxy_url` | String | no | URL of the GitLab instance that should be used as a proxy for the GitLab for Jira Cloud app |
| `keep_latest_artifact` | boolean | no | Prevent the deletion of the artifacts from the most recent successful jobs, regardless of the expiry time. Enabled by default. |
| `local_markdown_version` | integer | no | Increase this value when any cached Markdown should be invalidated. |
| `mailgun_signing_key` | string | no | The Mailgun HTTP webhook signing key for receiving events from webhook. |
@@ -534,13 +537,13 @@ listed in the descriptions of the relevant settings.
| `repository_checks_enabled` | boolean | no | GitLab periodically runs `git fsck` in all project and wiki repositories to look for silent disk corruption issues. |
| `repository_size_limit` **(PREMIUM ALL)** | integer | no | Size limit per repository (MB) |
| `repository_storages_weighted` | hash of strings to integers | no | (GitLab 13.1 and later) Hash of names of taken from `gitlab.yml` to [weights](../administration/repository_storage_paths.md#configure-where-new-repositories-are-stored). New projects are created in one of these stores, chosen by a weighted random selection. |
-| `repository_storages` | array of strings | no | (GitLab 13.0 and earlier) List of names of enabled storage paths, taken from `gitlab.yml`. New projects are created in one of these stores, chosen at random. |
| `require_admin_approval_after_user_signup` | boolean | no | When enabled, any user that signs up for an account using the registration form is placed under a **Pending approval** state and has to be explicitly [approved](../administration/moderate_users.md) by an administrator. |
| `require_two_factor_authentication` | boolean | no | (**If enabled, requires:** `two_factor_grace_period`) Require all users to set up Two-factor authentication. |
| `restricted_visibility_levels` | array of strings | no | Selected levels cannot be used by non-Administrator users for groups, projects or snippets. Can take `private`, `internal` and `public` as a parameter. Default is `null` which means there is no restriction.[Changed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131203) in GitLab 16.4: cannot select levels that are set as `default_project_visibility` and `default_group_visibility`. |
| `rsa_key_restriction` | integer | no | The minimum allowed bit length of an uploaded RSA key. Default is `0` (no restriction). `-1` disables RSA keys. |
| `session_expire_delay` | integer | no | Session duration in minutes. GitLab restart is required to apply changes. |
| `security_policy_global_group_approvers_enabled` | boolean | no | Whether to look up scan result policy approval groups globally or within project hierarchies. |
+| `service_access_tokens_expiration_enforced` | boolean | no | Flag to indicate if token expiry date can be optional for service account users |
| `shared_runners_enabled` | boolean | no | (**If enabled, requires:** `shared_runners_text` and `shared_runners_minutes`) Enable shared runners for new projects. |
| `shared_runners_minutes` **(PREMIUM ALL)** | integer | required by: `shared_runners_enabled` | Set the maximum number of compute minutes that a group can use on shared runners per month. |
| `shared_runners_text` | string | required by: `shared_runners_enabled` | Shared runners text. |
@@ -572,6 +575,7 @@ listed in the descriptions of the relevant settings.
| `spam_check_endpoint_url` | string | no | URL of the external Spamcheck service endpoint. Valid URI schemes are `grpc` or `tls`. Specifying `tls` forces communication to be encrypted.|
| `spam_check_api_key` | string | no | API key used by GitLab for accessing the Spam Check service endpoint. |
| `suggest_pipeline_enabled` | boolean | no | Enable pipeline suggestion banner. |
+| `enable_artifact_external_redirect_warning_page` | boolean | no | Show the external redirect page that warns you about user-generated content in GitLab Pages. |
| `terminal_max_session_time` | integer | no | Maximum time for web terminal websocket connection (in seconds). Set to `0` for unlimited time. |
| `terms` | text | required by: `enforce_terms` | (**Required by:** `enforce_terms`) Markdown content for the ToS. |
| `throttle_authenticated_api_enabled` | boolean | no | (**If enabled, requires:** `throttle_authenticated_api_period_in_seconds` and `throttle_authenticated_api_requests_per_period`) Enable authenticated API request rate limit. Helps reduce request volume (for example, from crawlers or abusive bots). |
@@ -613,9 +617,6 @@ listed in the descriptions of the relevant settings.
| `valid_runner_registrars` | array of strings | no | List of types which are allowed to register a GitLab Runner. Can be `[]`, `['group']`, `['project']` or `['group', 'project']`. |
| `whats_new_variant` | string | no | What's new variant, possible values: `all_tiers`, `current_tier`, and `disabled`. |
| `wiki_page_max_content_bytes` | integer | no | Maximum wiki page content size in **bytes**. Default: 52428800 Bytes (50 MB). The minimum value is 1024 bytes. |
-| `jira_connect_application_key` | String | no | Application ID of the OAuth application that should be used to authenticate with the GitLab for Jira Cloud app |
-| `jira_connect_proxy_url` | String | no | URL of the GitLab instance that should be used as a proxy for the GitLab for Jira Cloud app |
-| `gitlab_shell_operation_limit` | integer | no | Maximum number of Git operations per minute a user can perform. Default: `600`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/412088) in GitLab 16.2. |
### Configure inactive project deletion
diff --git a/doc/api/users.md b/doc/api/users.md
index 118008848f3..cb9951a1c45 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -2142,9 +2142,14 @@ Example response:
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131923) in GitLab 16.5.
Use this API to create a new personal access token for the currently authenticated user.
-For security purposes, the scopes are limited to only `k8s_proxy` and by default the token will expire by
-the end of the day it was created at.
-Token values are returned once so, make sure you save it as you can't access it again.
+For security purposes, the token:
+
+- Is limited to the [`k8s_proxy` scope](../user/profile/personal_access_tokens.md#personal-access-token-scopes).
+ This scope grants permission to perform Kubernetes API calls using the agent for Kubernetes.
+- By default, expires at the end of the day it was created on.
+
+Token values are returned once, so make sure you save the token as you cannot access
+it again.
```plaintext
POST /user/personal_access_tokens
@@ -2331,6 +2336,7 @@ Prerequisites:
- You must be an administrator or have the Owner role of the target namespace or project.
- For `instance_type`, you must be an administrator of the GitLab instance.
+- For `group_type` or `project_type` with an Owner role, an administrator must not have enabled [restrict runner registration](../administration/settings/continuous_integration.md#restrict-runner-registration-by-all-users-in-an-instance).
- An access token with the `create_runner` scope.
Be sure to copy or save the `token` in the response, the value cannot be retrieved again.
diff --git a/doc/architecture/blueprints/cdot_orders/index.md b/doc/architecture/blueprints/cdot_orders/index.md
new file mode 100644
index 00000000000..924a50d2b8a
--- /dev/null
+++ b/doc/architecture/blueprints/cdot_orders/index.md
@@ -0,0 +1,265 @@
+---
+status: proposed
+creation-date: "2023-10-12"
+authors: [ "@tyleramos" ]
+coach: "@fabiopitino"
+approvers: [ "@tgolubeva", "@jameslopez" ]
+owning-stage: "~devops::fulfillment"
+participating-stages: []
+---
+
+# Align CustomersDot Orders with Zuora Orders
+
+## Summary
+
+The [GitLab Customers Portal](https://customers.gitlab.com/) is an application separate from the GitLab product that allows GitLab Customers to manage their account and subscriptions, tasks like purchasing additional seats. More information about the Customers Portal can be found in [the GitLab docs](../../../subscriptions/customers_portal.md). Internally, the application is known as [CustomersDot](https://gitlab.com/gitlab-org/customers-gitlab-com) (also known as CDot).
+
+GitLab uses [Zuora's platform](https://about.gitlab.com/handbook/business-technology/enterprise-applications/guides/zuora/) to manage their subscription-based services. CustomersDot integrates directly with Zuora Billing and treats [Zuora Billing](https://about.gitlab.com/handbook/finance/accounting/finance-ops/billing-ops/zuora-billing/) as the single source of truth for subscription data.
+
+CustomersDot stores some subscription and order data locally, in the form of the `orders` database table, which at times can be out of sync with Zuora Billing. The main objective for this blueprint is to lay out a plan for improving the integration with Zuora Billing, making it more reliable, accurate, and performant.
+
+## Motivation
+
+Working with the `Order` model in CustomersDot has been a challenge for Fulfillment engineers. It is difficult to trust `Order` data as it can get out of sync with the single source of truth for subscription data, Zuora Billing. This has led to bugs, confusion and delays in feature development. An [epic exists for aligning CustomersDot Orders with Zuora objects](https://gitlab.com/groups/gitlab-org/-/epics/9748) which lists a variety of issues related to these data integrity problems. The motivation of this blueprint is to develop a better data architecture in CustomersDot for Subscriptions and associated data models which builds trust and reduces bugs.
+
+### Goals
+
+This re-architecture project has several multifaceted objectives.
+
+- Increase the accuracy of CustomersDot data pertaining to Subscriptions and its entitlements. This data is stored as `Order` records in CustomersDot - it is not granular enough to represent what the customer has purchased, and it is error prone as shown by the following issues:
+ - [Multiple order records for the same subscription](https://gitlab.com/gitlab-org/customers-gitlab-com/-/issues/6971)
+ - [Multiple subscriptions active for the same namespace](https://gitlab.com/gitlab-org/customers-gitlab-com/-/issues/6972)
+ - [Support Multiple Active Orders on a Namespace](https://gitlab.com/groups/gitlab-org/-/epics/9486)
+- Continue to align with Zuora Billing being the SSoT for Subscription and Order data.
+- Decrease dependency and reliance on Zuora Billing uptime.
+- Improve CustomersDot performance by storing relevant Subscription data locally and keeping it in sync with Zuora Billing. This could be a key piece to making Seat Link more efficient and reliable.
+- Eliminate confusion between CustomersDot Orders, which contain data more closely resembling a Subscription, and [Zuora Orders](https://knowledgecenter.zuora.com/Zuora_Billing/Manage_subscription_transactions/Orders), which represent a transaction between a customer and merchant and can apply to multiple Subscriptions.
+ - The CustomersDot `orders` table contains a mixture of Zuora Subscription and trials, along with GitLab-specific metadata like sync timestamps with GitLab.com. GitLab does not store trial subscriptions in Zuora at this time.
+
+## Proposal
+
+As the list of goals above shows, there are a good number of desired outcomes we would like to see at the end of implementation. To reach these goals, we will break this work up into smaller iterations.
+
+1. [Phase one: Zuora Subscription Cache](#phase-one-zuora-subscription-cache)
+
+ The first iteration focuses on adding a local cache for Zuora Subscription objects, including Rate Plans, Rate Plan Charges, and Rate Plan Charge Tiers, in CustomersDot.
+
+1. [Phase two: Utilize Zuora Cache Models](#phase-two-utilize-zuora-cache-models)
+
+ The second phase involves using the Zuora cache models introduced in phase one. Any code in CustomersDot that makes a read request to Zuora for Subscription data should be replaced with an ActiveRecord query. This should result in a big performance improvement.
+
+1. [Phase three: Transition from `Order` to `Subscription`](#phase-three-transition-from-order-to-subscription)
+
+ The next iteration focuses on transitioning away from the CustomersDot `Order` model to a new model for Subscription.
+
+## Design and implementation details
+
+### Phase one: Zuora Subscription Cache
+
+The first phase for this blueprint focuses on adding new models for caching Zuora Subscription data locally in CustomersDot. These local data models will allow CustomersDot to query the local database for Zuora Subscriptions. Currently, this requires querying directly to Zuora which can be problematic if Zuora is experiencing downtime. Zuora also has rate limits for API usage which we want to avoid as CustomersDot continues to scale.
+
+This phase will consist of creating the new data models, building the mechanisms to keep the local data in sync with Zuora, and backfilling the existing data. It will be important that the local cache models are read-only for most of the application to ensure the data is always in sync. Only the syncing mechanism should have the ability to write to these models.
+
+#### Proposed DB schema
+
+```mermaid
+erDiagram
+ "Zuora::Subscription" ||--|{ "Zuora::RatePlan" : "has many"
+ "Zuora::RatePlan" ||--|{ "Zuora::RatePlanCharge" : "has many"
+ "Zuora::RatePlanCharge" ||--|{ "Zuora::RatePlanChargeTier" : "has many"
+
+ "Zuora::Subscription" {
+ string(64) zuora_id PK "`id` field on Zuora Subscription"
+ string(64) account_id
+ string name
+ string(64) previous_subscription_id
+ string status
+ date term_start_date
+ date term_end_date
+ int version
+ boolean auto_renew "null:false default:false"
+ date cancelled_date
+ string(64) created_by_id
+ integer current_term
+ string current_term_period_type
+ string eoa_starter_bronze_offer_accepted__c
+ string external_subscription_id__c
+ string external_subscription_source__c
+ string git_lab_namespace_id__c
+ string git_lab_namespace_name__c
+ integer initial_term
+ string(64) invoice_owner_id
+ string notes
+ string opportunity_id__c
+ string(64) original_id
+ string(64) ramp_id
+ string renewal_subscription__c__c
+ integer renewal_term
+ date subscription_end_date
+ date subscription_start_date
+ string turn_on_auto_renew__c
+ string turn_on_cloud_licensing__c
+ string turn_on_operational_metrics__c
+ string turn_on_seat_reconciliation__c
+ datetime created_date
+ datetime updated_date
+ datetime created_at
+ datetime updated_at
+ }
+
+ "Zuora::RatePlan" {
+ string(64) zuora_id PK "`id` field on Zuora RatePlan"
+ string(64) subscription_id FK
+ string name
+ string(64) product_rate_plan_id
+ datetime created_date
+ datetime updated_date
+ datetime created_at
+ datetime updated_at
+ }
+
+ "Zuora::RatePlanCharge" {
+ string(64) zuora_id PK "`id` field on Zuora RatePlanCharge"
+ string(64) rate_plan_id FK
+ string(64) product_rate_plan_charge_id
+ int quantity
+ date effective_start_date
+ date effective_end_date
+ string price_change_option
+ string charge_number
+ string charge_type
+ boolean is_last_segment "null:false default:false"
+ int segment
+ int mrr
+ int tcv
+ int dmrc
+ int dtcv
+ string(64) subscription_id
+ string(64) subscription_owner_id
+ int version
+ datetime created_date
+ datetime updated_date
+ datetime created_at
+ datetime updated_at
+ }
+
+ "Zuora::RatePlanChargeTier" {
+ string zuora_id PK "`id` field on Zuora RatePlanChargeTier"
+ string rate_plan_charge_id FK
+ string price
+ datetime created_date
+ datetime updated_date
+ datetime created_at
+ datetime updated_at
+ }
+```
+
+#### Notes
+
+- The namespace `Zuora` is already taken by the classes used to extend `IronBank` resource classes. It was decided to move these to the namespace `Zuora::Remote` to indicate these are intended to reach out to Zuora. This frees up the `Zuora` namespace to be used to group the models related to Zuora cached data.
+- All versions of Zuora Subscriptions will be stored in this table to be able to support display of current as well as future purchases when Zuora is down. One of the guiding principles from the Architecture Review meeting on 2023-08-06 was "Customers should be able to view and access what they purchased even if Zuora is down". Given that customers can make future-dated purchases, CustomersDot needs to store current and future versions of Subscriptions.
+- `zuora_id` would be the primary key given we want to avoid the field name `id` which is magical in ActiveRecord.
+- The timezone for Zuora Billing is configured as Pacific Time. Let's account for this timezone as we sync data from Zuora into CDot's cached models to allow for more accurate comparisons.
+
+#### Keeping data in sync with Zuora
+
+CDot currently receives and processes `Order Processed` Zuora callouts for Order actions like `Update Product` ([full list](https://gitlab.com/gitlab-org/customers-gitlab-com/-/blob/64c5d17bac38bef1156e9a15008cc7d2b9aa46a9/lib/zuora/order.rb#L26)). These callouts help to keep CustomersDot in sync with Zuora and trigger provisioning events. These callouts will be important to keeping `Zuora::Subscription` and related cached models in sync with changes in Zuora.
+
+This existing callout would not be sufficient to cover all changes to a Zuora Subscription though. In particular, changes to custom fields may not be captured by these existing callouts. We will need to create custom events and callouts for any custom field cached in CustomersDot for any of these resources to ensure CDot is in sync with Zuora. This should only affect `Zuora::Subscription` though as no custom fields are used by CustomersDot on any of the other proposed cached resources at this time.
+
+#### Rollout of Zuora Cache models
+
+With the first iteration of introducing the cached Zuora data models, we will take an iterative approach to the rollout. There should be no impact to existing functionality as we build out the models, start populating the data through callouts, and backfill these models. Once this is in place, we will iteratively update existing features to use these cached data models instead of querying Zuora directly.
+
+We will make this transition using many small scoped feature flags, rather than one large feature flag to gate all of the new logic using these cache models. This will help us deliver more quickly and reduce the length with which feature flag logic is maintained and test cases are retained.
+
+Testing can be performed before the cached models are used in the codebase to ensure data integrity of the cached models.
+
+### Phase two: Utilize Zuora Cache Models
+
+This phase covers the second phase of work of the Orders re-architecture. In this phase, the focus will be utilizing the new Zuora cache data models introduced in phase one. Querying Zuora for Subscription data is fundamental to Customers so there are plenty of places that will need to be updated. In the places where CDot is reading from Zuora, it can be replaced by querying the local cache data models instead. This should result in a big performance boost by avoiding third party requests, particularly in components like the Seat Link Service.
+
+This transition will be completed using many small scoped feature flags, rather than one large feature flag to gate all of the new logic using these cache models. This will help to deliver more quickly and reduce the length with which feature flag logic is maintained and test cases are retained.
+
+### Phase three: Transition from `Order` to `Subscription`
+
+The second phase for this blueprint focuses on transitioning away from the CustomersDot `Order` model to a new model for `Subscription`. This phase will consist of creating a new model for `Subscription`, supporting both models during the transition period, updating existing code to use `Subscription` and finally removing the `Order` model once it is no longer needed.
+
+Replacing the `Order` model with a `Subscription` model should address the goal of eliminating confusion around the `Order` model. The data stored in the CustomersDot `Order` model does not correspond to a Zuora Order. It more closely resembles a Zuora Subscription with some additional metadata about syncing with GitLab.com. The transition to a `Subscription` model, along with the local cache layer in phase one, should address the goal of better data accuracy and building trust in CustomersDot data.
+
+#### Proposed DB schema
+
+```mermaid
+erDiagram
+ Subscription ||--|{ "Zuora::Subscription" : "has many"
+
+ Subscription {
+ bigint id PK
+ bigint billing_account_id
+ string(64) zuora_account_id
+ string(64) zuora_subscription_id
+ string zuora_subscription_name
+ string gitlab_namespace_id
+ string gitlab_namespace_name
+ datetime last_extra_ci_minutes_sync_at
+ datetime increased_billing_rate_notified_at
+ boolean reconciliation_accepted "null:false default:false"
+ datetime seat_overage_notified_at
+ datetime auto_renew_error_notified_at
+ date monthly_seat_digest_notified_on
+ datetime created_at
+ datetime updated_at
+ }
+
+ "Zuora::Subscription" {
+ string(64) zuora_id PK "`id` field on Zuora Subscription"
+ string(64) account_id
+ string name
+ }
+```
+
+#### Notes
+
+- The name for this model is up for debate given a `Subscription` model already exists. The existing model could be renamed with the hope of eventually replacing it with the new model.
+- This model serves as a record of the Subscription that is modifiable by the CDot application, whereas the `Zuora::Subscription` table below should be read-only.
+- `zuora_account_id` could be added as a convenience but could also be fetched via the `billing_account`.
+- There will be one `Subscription` record per actual subscription instead of a Subscription version.
+ - This has the advantage of avoiding duplication of fields like `gitlab_namespace_id` or `last_extra_ci_minutes_sync_at`.
+ - The `zuora_subscription_id` column could be removed or kept as a reference to the latest Zuora Subscription version.
+
+#### Keeping data in sync with Zuora
+
+The `Subscription` model should stay in sync with Zuora as subscriptions are created or updated. This model will be synced when we sync `Zuora::Subscription` records, similar to how the cached models are synced when processing Zuora callouts as described in phase one. When saving a new version of a `Zuora::Subscription`, an update could be made to the `Subscription` record with the matching `zuora_subscription_name`, or create a `Subscription` if one does not exist. The `zuora_subscription_id` would be set to the latest version on typical updates. Most of the data on `Subscription` is GitLab metadata (e.g. `last_extra_ci_minutes_sync_at`) so it wouldn't need to be updated.
+
+The exception to this update rule are the `zuora_account_id` and `billing_account_id` attributes. Let's consider the current behavior when processing an `Order Processed` callout in CDot if the `zuora_account_id` changes for a Zuora Subscription:
+
+1. The Billing Account Membership is updated to the new Billing Account for the CDot `Customer` matching the Sold To email address.
+1. CDot attempts to find the CDot `Order` with the new `billing_account_id` and `subscription_name`.
+1. If an `Order` isn't found matching this criteria, a new `Order` is created. This leads to two `Order` records for the same Zuora Subscription.
+
+This scenario should be avoided for the new `Subscription` model. One `Subscription` should exist for a unique `Zuora::Subscription` name. If the Zuora Subscription transfers Accounts, the `Subscription` should as well.
+
+#### Unknowns
+
+Several unknowns are outlined below. As we get further into implementation, these unknown should become clearer.
+
+##### Trial data in Subscription?
+
+The CDot `Order` model contains paid subscription data as well as trials. For `Subscription`, we could choose to continue to have paid subscription and trial data together in the same table, or break them into their own models.
+
+The `orders` table has fields for `customer_id` and `trial` which only really concern trials. Should these fields be added to the `Subscription` table? Should `Subscription` contain trial information if it doesn't exist in Zuora?
+
+If trial orders were broken out into their own table, these are the columns likely needed for a (SaaS) `trials` table:
+
+- `customer_id`
+- `product_rate_plan_id` (or rename to `plan_id` or use `plan_code`)
+- `quantity`
+- `start_date`
+- `end_date`
+- `gl_namespace_id`
+- `gl_namespace_name`
+
+### Resources
+
+- [FY24Q3 OKR - Create plan to align CustomersDot Orders to Zuora Orders](https://gitlab.com/gitlab-com/gitlab-OKRs/-/work_items/3378)
+- [Epic &9748 - Align CustomersDot Orders to Zuora objects](https://gitlab.com/groups/gitlab-org/-/epics/9748)
diff --git a/doc/architecture/blueprints/cells/impacted_features/personal-access-tokens.md b/doc/architecture/blueprints/cells/impacted_features/personal-access-tokens.md
index 3aca9f1e116..a493a1c4395 100644
--- a/doc/architecture/blueprints/cells/impacted_features/personal-access-tokens.md
+++ b/doc/architecture/blueprints/cells/impacted_features/personal-access-tokens.md
@@ -17,13 +17,37 @@ we can document the reasons for not choosing this approach.
## 1. Definition
-Personal Access Tokens associated with a User are a way for Users to interact with the API of GitLab to perform operations.
-Personal Access Tokens today are scoped to the User, and can access all Groups that a User has access to.
+Personal Access Tokens (PATs) associated with a User are a way for Users to interact with the API of GitLab to perform operations.
+PATs today are scoped to the User, and can access all Groups that a User has access to.
## 2. Data flow
## 3. Proposal
+### 3.1. Organization-scoped PATs
+
+Pros:
+
+- Can be managed entirely from Rails application.
+- Increased security. PAT is limited only to Organization.
+
+Cons:
+
+- Different PAT needed for different Organizations.
+- Cannot tell at a glance if PAT will apply to a certain Project/Namespace.
+
+### 3.2. Cluster-wide PATs
+
+Pros:
+
+- User does not have to worry about which scope the PAT applies to.
+
+Cons:
+
+- User has to worry about wide-ranging scope of PAT (e.g. separation of personal items versus work items).
+- Organization cannot limit scope of PAT to only their Organization.
+- Increases complexity. All cluster-wide data likely will be moved to a separate [data access layer](../../cells/index.md#1-data-access-layer).
+
## 4. Evaluation
## 4.1. Pros
diff --git a/doc/architecture/blueprints/cells/index.md b/doc/architecture/blueprints/cells/index.md
index 1366d308487..c9a03830a4a 100644
--- a/doc/architecture/blueprints/cells/index.md
+++ b/doc/architecture/blueprints/cells/index.md
@@ -338,6 +338,7 @@ Below is a list of known affected features with preliminary proposed solutions.
- [Cells: Global Search](impacted_features/global-search.md)
- [Cells: GraphQL](impacted_features/graphql.md)
- [Cells: Organizations](impacted_features/organizations.md)
+- [Cells: Personal Access Tokens](impacted_features/personal-access-tokens.md)
- [Cells: Personal Namespaces](impacted_features/personal-namespaces.md)
- [Cells: Secrets](impacted_features/secrets.md)
- [Cells: Snippets](impacted_features/snippets.md)
@@ -354,7 +355,6 @@ The following list of impacted features only represents placeholders that still
- [Cells: Group Transfer](impacted_features/group-transfer.md)
- [Cells: Issues](impacted_features/issues.md)
- [Cells: Merge Requests](impacted_features/merge-requests.md)
-- [Cells: Personal Access Tokens](impacted_features/personal-access-tokens.md)
- [Cells: Project Transfer](impacted_features/project-transfer.md)
- [Cells: Router Endpoints Classification](impacted_features/router-endpoints-classification.md)
- [Cells: Schema changes (Postgres and Elasticsearch migrations)](impacted_features/schema-changes.md)
diff --git a/doc/architecture/blueprints/ci_pipeline_components/img/catalogs.png b/doc/architecture/blueprints/ci_pipeline_components/img/catalogs.png
deleted file mode 100644
index 8c83aede186..00000000000
--- a/doc/architecture/blueprints/ci_pipeline_components/img/catalogs.png
+++ /dev/null
Binary files differ
diff --git a/doc/architecture/blueprints/ci_pipeline_components/index.md b/doc/architecture/blueprints/ci_pipeline_components/index.md
index 46b8f361949..9fdbf8cb70b 100644
--- a/doc/architecture/blueprints/ci_pipeline_components/index.md
+++ b/doc/architecture/blueprints/ci_pipeline_components/index.md
@@ -105,6 +105,7 @@ identifying abstract concepts and are subject to changes as we refine the design
allows components to be pinned to a specific revision.
- **Step** is a type of component that contains a collection of instructions for job execution.
- **Template** is a type of component that contains a snippet of CI/CD configuration that can be [included](../../../ci/yaml/includes.md) in a project's pipeline configuration.
+- **Publishing** is the act of listing a version of the resource (for example, a project release) on the Catalog.
## Definition of pipeline component
@@ -524,17 +525,26 @@ spec:
The CI Catalog is an index of resources that users can leverage in CI/CD. It initially
contains a list of components repositories that users can discover and use in their pipelines.
+The user sees only resources based on their permissions and project visibility level.
+Unauthenticated users will only see public resources.
+
+Project admins are responsible for setting the project private or public.
+The CI Catalog should not provide security functionalities like prevent projects from appearing in the Community Catalog.
+If the project is public it's visible to the world anyway.
+
+The Catalog page can provide different filters to refine the user search including
+predefined filters such as resources from groups the user is member of.
In the future, the Catalog could contain also other types of resources (for example:
-integrations, project templates, etc.).
+integrations, project templates, container images, etc.).
To list a components repository in the Catalog we need to mark the project as being a
-catalog resource. We do that initially with an API endpoint, similar to changing a project setting.
+catalog resource. We do that initially with a project setting.
-Once a project is marked as a "catalog resource" it can be displayed in the Catalog.
+Once a project is marked as a "catalog resource" it can eventually be displayed in the Catalog.
-We could create a database record when the API endpoint is used and remove the record when
-the same is disabled/removed.
+We could create a database record when the setting is enabled and modify the record's state when
+the same is disabled.
## Catalog resource
@@ -552,9 +562,6 @@ Other properties of a catalog resource:
- indicators of popularity (stars, forks).
- categorization: user should select a category and or define search tags
-As soon as a components repository is marked as being a "catalog resource"
-we should be seeing the resource listed in the Catalog.
-
Initially for the resource, the project may not have any released tags.
Users would be able to use the components repository by specifying a branch name or
commit SHA for the version. However, these types of version qualifiers should not
@@ -564,10 +571,14 @@ be listed in the catalog resource's page for various reasons:
- Branches and tags may not be meaningful for the end-user.
- Branches and tags don't communicate versioning thoroughly.
+To list a catalog resource in the Catalog we first need to create a release for
+the project.
+
## Releasing new resource versions to the Catalog
-The versions that should be displayed for the resource should be the project [releases](../../../user/project/releases/index.md).
-Creating project releases is an official act of versioning a resource.
+The versions that will be published for the resource should be the project
+[releases](../../../user/project/releases/index.md). Creating project releases is an official
+act of versioning a resource.
A resource page would have:
@@ -599,29 +610,6 @@ For example: index the content of `spec:` section for CI components.
See an [example of development workflow](dev_workflow.md) for a components repository.
-## Availability of CI catalog as a feature
-
-We plan to introduce 2 features of CI catalog as separate views:
-
-1. **Namespace Catalog (GitLab Ultimate):** allows organizations to share and discover catalog resources
- created inside the top-level namespace.
- Users will be able to access the Namespace Catalog from a project or subgroup inside the top-level
- namespace.
-1. **Community Catalog (GitLab free):** allows anyone in a GitLab instance to share and discover catalog
- resources. The Community Catalog presents only resources/projects that are public.
-
-If a resource in a Namespace Catalog is made public (changing the project's visibility) the resource is
-available in both Namespace Catalog (because it comes from there) as well as the Community Catalog
-(because it's public).
-
-![Namespace and Community Catalogs](img/catalogs.png)
-
-There is only 1 CI catalog. The Namespace and Community Catalogs are different views of the CI catalog.
-
-Project admins are responsible for setting the project private or public.
-The CI Catalog should not provide security functionalities like prevent projects from appearing in the Community Catalog.
-If the project is public it's visible to the world anyway.
-
## Note about future resource types
In the future, to support multiple types of resources in the Catalog we could
@@ -673,6 +661,8 @@ metadata:
## Iterations
+The first plan of iterations constisted in:
+
1. Experimentation phase
- Build an MVC behind a feature flag with `namespace` actor.
- Enable the feature flag only for `gitlab-com` and `gitlab-org` namespaces to initiate the dogfooding.
@@ -691,6 +681,9 @@ metadata:
components from GitLab.com or from repository exports.
- Iterate on feedback.
+In October 2023, after releasing the namespace-view (previously called private catalog view) as Experiment we changed
+focus moving away from 2 separate views (namespace view and global view) and combining the UX in a single global view.
+
## Limits
Any MVC that exposes a feature should be added with limitations from the beginning.
diff --git a/doc/architecture/blueprints/cloud_connector/decisions/001_lb_entry_point.md b/doc/architecture/blueprints/cloud_connector/decisions/001_lb_entry_point.md
new file mode 100644
index 00000000000..d49b702be94
--- /dev/null
+++ b/doc/architecture/blueprints/cloud_connector/decisions/001_lb_entry_point.md
@@ -0,0 +1,52 @@
+---
+owning-stage: "~devops::data stores"
+description: 'Cloud Connector ADR 001: Use load balancer as single entry point'
+---
+
+# Cloud Connector ADR 001: Load balancer as single entry point
+
+## Context
+
+The original iteration of the blueprint suggested to stand up a dedicated Cloud Connector edge service,
+through which all traffic that uses features under the Cloud Connector umbrella would pass.
+
+The primary reasons for why we wanted this to be a dedicated service were to:
+
+1. **Provide a single entry point for customers.** We identified the ability for any GitLab instance
+ around the world to consume Cloud Connector features through a single endpoint such as
+ `cloud.gitlab.com` as a must-have property.
+1. **Have the ability to execute custom logic.** There was a desire from product to create a space where we can
+ run cross-cutting business logic such as application-level rate limiting, which is hard or impossible to
+ do using a traditional load balancer such as HAProxy.
+
+## Decision
+
+We decided to take a smaller incremental step toward having a "smart router" by focusing on
+the ability to provide a single endpoint through which Cloud Connector traffic enters our
+infrastructure. This can be accomplished using simpler means than deploying dedicated services, specifically
+by pulling in a load balancing layer listening at `cloud.gitlab.com` that can also perform simple routing
+tasks to forward traffic into feature backends.
+
+Our reasons for this decision were:
+
+1. **Unclear requirements for custom logic to run.** We are still exploring how and to what extent we would
+ apply rate limiting logic at the Cloud Connector level. This is being explored in
+ [issue 429592](https://gitlab.com/gitlab-org/gitlab/-/issues/429592). Because we need to have a single
+ entry point by January, and because we think we will not be ready by then to implement such logic at the
+ Cloud Connector level, a web service is not required yet.
+1. **New use cases found that are not suitable to run through a dedicated service.** We started to work with
+ the Observability group to see how we can bring the GitLab Observability Backend (GOB) to Cloud Connector
+ customers in [MR 131577](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131577).
+ In this discussion it became clear that due to the large amounts of traffic and data volume passing
+ through GOB each day, putting another service in front of this stack does not provide a sensible
+ risk/benefit trade-off. Instead, we will probably split traffic and make Cloud Connector components
+ available through other means for special cases like these (for example, through a Cloud Connector library).
+
+We are exploring several options for load-balancing this new endpoint in [issue 429818](https://gitlab.com/gitlab-org/gitlab/-/issues/429818)
+and are working with the `Infrastructure:Foundations` team to deploy this in [issue 24711](https://gitlab.com/gitlab-com/gl-infra/reliability/-/issues/24711).
+
+## Consequences
+
+We have not yet discarded the plan to build a smart router eventually, either as a service or
+through other means, but have delayed this decision in face of uncertainty at both a product
+and technical level. We will reassess how to proceed in Q1 2024.
diff --git a/doc/architecture/blueprints/cloud_connector/index.md b/doc/architecture/blueprints/cloud_connector/index.md
index 840e17a438a..9aef8bc7a98 100644
--- a/doc/architecture/blueprints/cloud_connector/index.md
+++ b/doc/architecture/blueprints/cloud_connector/index.md
@@ -68,7 +68,7 @@ Introducing a dedicated edge service for Cloud Connector serves the following go
we do not currently support.
- **Independently scalable.** For reasons of fault tolerance and scalability, it is beneficial to have all SM traffic go
through a separate service. For example, if an excess of unexpected requests arrive from SM instances due to a bug
- in a milestone release, this traffic could be absorbed at the CC gateway level without cascading downstream, thus leaving
+ in a milestone release, this traffic could be absorbed at the CC gateway level without cascading further, thus leaving
SaaS users unaffected.
### Non-goals
@@ -82,6 +82,10 @@ Introducing a dedicated edge service for Cloud Connector serves the following go
other systems using public key cryptographic checks. We may move some of the code around that currently implements this,
however.
+## Decisions
+
+- [ADR-001: Use load balancer as single entry point](decisions/001_lb_entry_point.md)
+
## Proposal
We propose to make two major changes to the current architecture:
@@ -133,7 +137,7 @@ The new service would be made available at `cloud.gitlab.com` and act as a "smar
It will have the following responsibilities:
1. **Request handling.** The service will make decisions about whether a particular request is handled
- in the service itself or forwarded to a downstream service. For example, a request to `/ai/code_suggestions/completions`
+ in the service itself or forwarded to other backends. For example, a request to `/ai/code_suggestions/completions`
could be handled by forwarding this request to an appropriate endpoint in the AI gateway unchanged, while a request
to `/-/metrics` could be handled by the service itself. As mentioned in [non-goals](#non-goals), the latter would not
include domain logic as it pertains to an end user feature, but rather cross-cutting logic such as telemetry, or
@@ -141,14 +145,14 @@ It will have the following responsibilities:
When handling requests, the service should be unopinionated about which protocol is used, to the extent possible.
Reasons for injecting custom logic could be setting additional HTTP header fields. A design principle should be
- to not require CC service deployments if a downstream service merely changes request payload or endpoint definitions. However,
+ to not require CC service deployments if a backend service merely changes request payload or endpoint definitions. However,
supporting more protocols on top of HTTP may require adding support in the CC service itself.
1. **Authentication/authorization.** The service will be the first point of contact for authenticating clients and verifying
they are authorized to use a particular CC feature. This will include fetching and caching public keys served from GitLab SaaS
and CustomersDot to decode JWT access tokens sent by GitLab instances, including matching token scopes to feature endpoints
to ensure an instance is eligible to consume this feature. This functionality will largely be lifted out of the AI gateway
where it currently lives. To maintain a ZeroTrust environment, the service will implement a more lightweight auth/z protocol
- with internal services downstream that merely performs general authenticity checks but forgoes billing and permission
+ with internal backends that merely performs general authenticity checks but forgoes billing and permission
related scoping checks. How this protocol will look like is to be decided, and might be further explored in
[Discussion: Standardized Authentication and Authorization between internal services and GitLab Rails](https://gitlab.com/gitlab-org/gitlab/-/issues/421983).
1. **Organization-level rate limits.** It is to be decided if this is needed, but there could be value in having application-level rate limits
diff --git a/doc/architecture/blueprints/container_registry_metadata_database/index.md b/doc/architecture/blueprints/container_registry_metadata_database/index.md
index 243270afdb2..c9f7f1c0d27 100644
--- a/doc/architecture/blueprints/container_registry_metadata_database/index.md
+++ b/doc/architecture/blueprints/container_registry_metadata_database/index.md
@@ -30,7 +30,7 @@ graph LR
R -- Write/read metadata --> B
```
-Client applications (for example, GitLab Rails and Docker CLI) interact with the Container Registry through its [HTTP API](https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs/spec/api.md). The most common operations are pushing and pulling images to/from the registry, which require a series of HTTP requests in a specific order. The request flow for these operations is detailed in the [Request flow](https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs-gitlab/push-pull-request-flow.md).
+Client applications (for example, GitLab Rails and Docker CLI) interact with the Container Registry through its [HTTP API](https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs/spec/gitlab/api.md). The most common operations are pushing and pulling images to/from the registry, which require a series of HTTP requests in a specific order. The request flow for these operations is detailed in the [Request flow](https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs/push-pull-request-flow.md).
The registry supports multiple [storage backends](https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs/configuration.md#storage), including Google Cloud Storage (GCS) which is used for the GitLab.com registry. In the storage backend, images are stored as blobs, deduplicated, and shared across repositories. These are then linked (like a symlink) to each repository that relies on them, giving them access to the central storage location.
@@ -69,7 +69,7 @@ Please refer to the [Docker documentation](https://docs.docker.com/registry/spec
##### Push and Pull
-Push and pull commands are used to upload and download images, more precisely manifests and blobs. The push/pull flow is described in the [documentation](https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs-gitlab/push-pull-request-flow.md).
+Push and pull commands are used to upload and download images, more precisely manifests and blobs. The push/pull flow is described in the [documentation](https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs/push-pull-request-flow.md).
#### GitLab Rails
@@ -86,7 +86,7 @@ The single entrypoint for the registry is the [HTTP API](https://gitlab.com/gitl
| [Check if manifest exists](https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs/spec/api.md#existing-manifests) | **{check-circle}** Yes | **{dotted-circle}** No | Used to get the digest of a manifest by tag. This is then used to pull the manifest and show the tag details in the UI. |
| [Pull manifest](https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs/spec/api.md#pulling-an-image-manifest) | **{check-circle}** Yes | **{dotted-circle}** No | Used to show the image size and the manifest digest in the tag details UI. |
| [Pull blob](https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs/spec/api.md#pulling-a-layer) | **{check-circle}** Yes | **{dotted-circle}** No | Used to show the configuration digest and the creation date in the tag details UI. |
-| [Delete tag](https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs/spec/api.md#deleting-a-tag) | **{check-circle}** Yes | **{check-circle}** Yes | Used to delete a tag from the UI and in background (cleanup policies). |
+| [Delete tag](https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs/spec/api.md#delete-tag) | **{check-circle}** Yes | **{check-circle}** Yes | Used to delete a tag from the UI and in background (cleanup policies). |
A valid authentication token is generated in GitLab Rails and embedded in all these requests before sending them to the registry.
@@ -154,7 +154,7 @@ Following the GitLab [Go standards and style guidelines](../../../development/go
The design and development of the registry database adhere to the GitLab [database guidelines](../../../development/database/index.md). Being a Go application, the required tooling to support the database will have to be developed, such as for running database migrations.
-Running *online* and [*post deployment*](../../../development/database/post_deployment_migrations.md) migrations is already supported by the registry CLI, as described in the [documentation](https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs-gitlab/database-migrations.md).
+Running *online* and [*post deployment*](../../../development/database/post_deployment_migrations.md) migrations is already supported by the registry CLI, as described in the [documentation](https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs/database-migrations.md).
#### Partitioning
@@ -224,7 +224,7 @@ This is a list of all the registry HTTP API operations and how they depend on th
| [Check API version](https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs/spec/api.md#api-version-check) | `GET` | `/v2/` | **{dotted-circle}** No | **{dotted-circle}** No | **{check-circle}** Yes |
| [List repositories](https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs/spec/api.md#listing-repositories) | `GET` | `/v2/_catalog` | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No |
| [List repository tags](https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs/spec/api.md#listing-image-tags) | `GET` | `/v2/<name>/tags/list` | **{check-circle}** Yes | **{dotted-circle}** No | **{check-circle}** Yes |
-| [Delete tag](https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs/spec/api.md#deleting-a-tag) | `DELETE` | `/v2/<name>/tags/reference/<reference>` | **{check-circle}** Yes | **{dotted-circle}** No | **{check-circle}** Yes |
+| [Delete tag](https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs/spec/api.md#delete-tag) | `DELETE` | `/v2/<name>/manifests/<reference>` | **{check-circle}** Yes | **{dotted-circle}** No | **{check-circle}** Yes |
| [Check if manifest exists](https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs/spec/api.md#existing-manifests) | `HEAD` | `/v2/<name>/manifests/<reference>` | **{check-circle}** Yes | **{dotted-circle}** No | **{check-circle}** Yes |
| [Pull manifest](https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs/spec/api.md#pulling-an-image-manifest) | `GET` | `/v2/<name>/manifests/<reference>` | **{check-circle}** Yes | **{dotted-circle}** No | **{check-circle}** Yes |
| [Push manifest](https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs/spec/api.md#pushing-an-image-manifest) | `PUT` | `/v2/<name>/manifests/<reference>` | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No |
diff --git a/doc/architecture/blueprints/container_registry_metadata_database_self_managed_rollout/index.md b/doc/architecture/blueprints/container_registry_metadata_database_self_managed_rollout/index.md
index 84a95e3e7c3..d91f2fdddbf 100644
--- a/doc/architecture/blueprints/container_registry_metadata_database_self_managed_rollout/index.md
+++ b/doc/architecture/blueprints/container_registry_metadata_database_self_managed_rollout/index.md
@@ -160,7 +160,7 @@ import which would lead to greater consistency across all storage driver impleme
### The Import Tool
-The [import tool](https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs-gitlab/database-import-tool.md)
+The [import tool](https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs/database-import-tool.md)
is a well-validated component of the Container Registry project that we have used
from the beginning as a way to perform local testing. This tool is a thin wrapper
over the core import functionality — the code which handles the import logic has
diff --git a/doc/architecture/blueprints/email_ingestion/index.md b/doc/architecture/blueprints/email_ingestion/index.md
index 9579a903133..59086aed86a 100644
--- a/doc/architecture/blueprints/email_ingestion/index.md
+++ b/doc/architecture/blueprints/email_ingestion/index.md
@@ -36,7 +36,7 @@ The current implementation lacks scalability and requires significant infrastruc
Because we are using a fork of the `mail_room` gem ([`gitlab-mail_room`](https://gitlab.com/gitlab-org/ruby/gems/gitlab-mail_room)), which contains some GitLab specific features that won't be ported upstream, we have a noteable maintenance overhead.
-The [Service Desk Single-Engineer-Group (SEG)](https://about.gitlab.com/handbook/engineering/incubation/service-desk/) started work on [customizable email addresses for Service Desk](https://gitlab.com/gitlab-org/gitlab/-/issues/329990) and [released the first iteration in beta in `16.4`](https://about.gitlab.com/releases/2023/09/22/gitlab-16-4-released/#custom-email-address-for-service-desk). As a [MVC we introduced a `Forwarding & SMTP` mode](https://gitlab.com/gitlab-org/gitlab/-/issues/329990#note_1201344150) where administrators set up email forwarding from their custom email address to the projects' `incoming_mail` email address. They also provide SMTP credentials so GitLab can send emails from the custom email address on their behalf. We don't need any additional email ingestion other than the existing mechanics for this approach to work.
+The [Service Desk Single-Engineer-Group (SEG)](https://about.gitlab.com/handbook/engineering/development/incubation/service-desk/) started work on [customizable email addresses for Service Desk](https://gitlab.com/gitlab-org/gitlab/-/issues/329990) and [released the first iteration in beta in `16.4`](https://about.gitlab.com/releases/2023/09/22/gitlab-16-4-released/#custom-email-address-for-service-desk). As a [MVC we introduced a `Forwarding & SMTP` mode](https://gitlab.com/gitlab-org/gitlab/-/issues/329990#note_1201344150) where administrators set up email forwarding from their custom email address to the projects' `incoming_mail` email address. They also provide SMTP credentials so GitLab can send emails from the custom email address on their behalf. We don't need any additional email ingestion other than the existing mechanics for this approach to work.
As a second iteration we'd like to add Microsoft Graph support for custom email addresses for Service Desk as well. Therefore we need a way to ingest more than the system defined two addresses. We will explore a solution path for Microsoft Graph support where privileged users can connect a custom email account and we can [receive messages via a Microsoft Graph webhook (`Outlook message`)](https://learn.microsoft.com/en-us/graph/webhooks#supported-resources). GitLab would need a public endpoint to receive updates on emails. That might not work for Self-managed instances, so we'll need direct email ingestion for Microsoft customers as well. But using the webhook approach could improve performance and efficiency for GitLab SaaS where we potentially have thousands of mailboxes to poll.
diff --git a/doc/architecture/blueprints/feature_flags_usage_in_dev_and_ops/index.md b/doc/architecture/blueprints/feature_flags_usage_in_dev_and_ops/index.md
new file mode 100644
index 00000000000..ad6dd755607
--- /dev/null
+++ b/doc/architecture/blueprints/feature_flags_usage_in_dev_and_ops/index.md
@@ -0,0 +1,285 @@
+---
+status: proposed
+creation-date: "2023-11-01"
+authors: [ "@rymai" ]
+coach: "@DylanGriffith"
+approvers: []
+owning-stage: "~devops::non_devops"
+participating-stages: []
+---
+
+# Feature Flags usage in GitLab development and operations
+
+This blueprint builds upon [the Development Feature Flags Architecture blueprint](../feature_flags_development/index.md).
+
+## Summary
+
+Feature flags are critical both in developing and operating GitLab, but in the current state
+of the process, they can lead to production issues, and introduce a lot of manual and maintenance work.
+
+The goals of this blueprint is to make the process safer, more maintainable, lightweight, automated and transparent.
+
+## Motivations
+
+### Feature flag use-cases
+
+Feature flags can be used for different purposes:
+
+- De-risking GitLab.com deployments (most feature flags): Allows to quickly enable/disable
+ a feature flag in production in the event of a production incident.
+- Work-in-progress feature: Some features are complex and need to be implemented through several MRs. Until they're fully implemented, it needs
+ to be hidden from anyone. In that case, the feature flag allows to merge all the changes to the main branch without actually using
+ the feature yet.
+- Beta features: We might
+ [not be confident we'll be able to scale, support, and maintain a feature](https://about.gitlab.com/handbook/product/gitlab-the-product/#experiment-beta-ga)
+ in its current form for every designed use case ([example](https://gitlab.com/gitlab-org/gitlab/-/issues/336070#note_1523983444)).
+ There are also scenarios where a feature is not complete enough to be considered an MVC.
+ Providing a flag in this case allows engineers and customers to disable the new feature until it's performant enough.
+- Operations: Site reliability engineer or Support engineer can use these flags to
+ disable potentially resource-heavy features in order to the instance back to a
+ more stable and available state. Another example is SaaS-only features.
+- Experiment: A/B testing on GitLab.com.
+- Worker (special `ops` feature flag): Used for controlling Sidekiq workers behavior, such as deferring Sidekiq jobs.
+
+We need to better categorize our feature flags.
+
+### Production incidents related to feature flags
+
+Feature flags have caused production incidents on GitLab.com ([1](https://gitlab.com/gitlab-com/gl-infra/production/-/issues/5289), [2](https://gitlab.com/gitlab-com/gl-infra/production/-/issues/4155), [3](https://gitlab.com/gitlab-com/gl-infra/production/-/issues/16366)).
+
+We need to prevent this for the sake of GitLab.com stability.
+
+### Technical debt caused by feature flags
+
+Feature flags are also becoming an ever-growing source of technical debt: there are currently
+[591 feature flags in the GitLab codebase](../../../user/feature_flags.md).
+
+We need to reduce feature flags count for the sake of long-term maintainability & quality of the GitLab codebase.
+
+## Goal
+
+The goal of this blueprint is to improve the feature flag process by making it:
+
+- safer
+- more maintainable
+- more lightweight & automated
+- more transparent
+
+## Challenges
+
+### Complex feature flag rollout process
+
+The feature flag rollout process is currently:
+
+- Complex: Rollout issues that are very manual and includes a lot of checkboxes
+ (including non-relevant checkboxes).
+ Engineers often don't use these issues, which tend to become stale and forgotten over time.
+- Not very transparent: Feature flag changes are logged in several places far from the rollout
+ issue, which makes it hard to understand the latest feature flag state.
+- Far from production processes: Rollout issues are created in the `gitlab-org/gitlab` project
+ (far from the production issue tracker).
+- There is no consistent path to rolling out feature flags: we leave to the judgement of the
+ engineer to trade-off between speed and safety. There should be a standardized set of rollout
+ steps.
+
+### Technical debt and codebase complexity
+
+[The challenges from the Development Feature Flags Architecture blueprint still stand](../feature_flags_development/index.md#challenges).
+
+Additionally, there are new challenges:
+
+- If a feature flag is enabled by default, and is disabled in an on-premise installation,
+ then when the feature flag is removed, the feature suddenly becomes enabled on the
+ on-premise instance and cannot be rolled backed to the previous behavior.
+
+### Multiple source of truth for feature flag default states and observability
+
+We currently show the feature flag default states in several places, for different intended audiences:
+
+**GitLab customers**
+
+- [User documentation](../../../user/feature_flags.md):
+ List all feature flags and their metadata so that GitLab customers can tweak feature flags on
+ their instance. Also useful for GitLab.com users that want to check the default state of a feature flag.
+
+**Site reliability and Delivery engineers**
+
+- [Internal GitLab.com feature flag state change issues](https://gitlab.com/gitlab-com/gl-infra/feature-flag-log/-/issues):
+ For each change of a feature flag state on GitLab.com, an issue is created in this project.
+- [Internal GitLab.com feature flag state change logs](https://nonprod-log.gitlab.net):
+ Filter logs with `source: feature` and `env: gprd` to see feature flag state change logs.
+
+**GitLab Engineering & Infra/Quality Directors / VPs, and CTO**
+
+- [Internal Sisense dashboard](https://app.periscopedata.com/app/gitlab/792066/Engineering-::-Feature-Flags):
+ Feature flag metrics over time, grouped per DevOps groups.
+
+**GitLab Engineering and Product managers**
+
+- ["Feature flags requiring attention" monthly reports](https://gitlab.com/gitlab-org/quality/triage-reports/-/issues/?sort=created_date&state=opened&search=Feature%20flags&in=TITLE&assignee_id=None&first_page_size=100):
+ Same data as the above Internal Sisense dashboard but for a specific DevOps
+ group, presented in an issue and assigned to the group's Engineering managers.
+
+**Anyone who wants to check feature flag default states**
+
+- [Unofficial feature flags dashboard](https://samdbeckham.gitlab.io/feature-flags/):
+ A user-friendly dashboard which provides useful filtering.
+
+This leads to confusion for almost all feature flag stakeholders (Development engineers, Engineering managers, Site reliability, Delivery engineers).
+
+## Proposal
+
+### Improve feature flags implementation and usage
+
+- [Reduce the likelihood of mis-configuration and human-error at the implementation step](https://gitlab.com/groups/gitlab-org/-/epics/11553)
+ - Remove the "percentage of time" strategy in favor of "percentage of actors"
+- [Improve the feature flag development documentation](https://gitlab.com/groups/gitlab-org/-/epics/5324)
+
+### Introduce new feature flag `type`s
+
+It's clear that the `development` feature flag type actually includes several use-cases:
+
+- GitLab.com deployment de-risking. YAML value: `gitlab_com_derisk`.
+- Work-in-progress feature. YAML value: `wip`. Once the feature is complete, the feature flag type can be changed to `beta`
+ if there still are some doubts on the scalability of the feature.
+- Beta features. YAML value: `beta`.
+
+Notes:
+
+- These new types replace the broad `development` type, which shouldn't be used anymore in the future.
+- Backward-compatibility will be kept until there's no `development` feature flags in the codebase anymore.
+
+### Introduce constraints per feature flag type
+
+Each feature flag type will be assigned specific constraints regarding:
+
+- Allowed values for the `default_enabled` attribute
+- Maximum Lifespan (MLS): the duration starting on the introduction of the feature flag (i.e. when it's merged into `master`).
+ We don't introduce a life span that would start on the global GitLab.com enablement (or `default_enabled: true` when
+ applicable) so that there's incentive to rollout and delete feature flags as quickly as possible.
+
+The MLS will be enforced through automation, reporting & regular review meetings at the section level.
+
+Following are the constraints for each feature flag type:
+
+- `gitlab_com_derisk`
+ - `default_enabled` **must not** be set to `true`. This kind of feature flag is meant to lower the risk on GitLab.com, thus
+ there's no need to keep the flag in the codebase after it's been enabled on GitLab.com.
+ **`default_enabled: true` will not have any effect for this type of feature flag.**
+ - Maximum Lifespan: 2 months.
+ - Additional note: This type of feature flag won't be documented in the [All feature flags in GitLab](../../../user/feature_flags.md)
+ page given they're short-lived and deployment-related.
+- `wip`
+ - `default_enabled` **must not** be set to `true`. If needed, this type can be changed to `beta` once the feature is complete.
+ - Maximum Lifespan: 4 months.
+- `beta`
+ - `default_enabled` can be set to `true` so that a feature can be "released" to everyone in Beta with the possibility to disable
+ it in the case of scalability issues (ideally it should only be disabled for this reason on specific on-premise installations).
+ - Maximum Lifespan: 6 months.
+- `ops`
+ - `default_enabled` can be set to `true`.
+ - Maximum Lifespan: Unlimited.
+ - Additional note: Remember that using this type should follow a conscious decision not to introduce an instance setting.
+- `experiment`
+ - `default_enabled` **must not** be set to `true`.
+ - Maximum Lifespan: 6 months.
+
+### Introduce a new `feature_issue_url` field
+
+Keeping the URL to the original feature issue will allow automated cross-linking from the rollout
+and logging issues. The new field for this information is `feature_issue_url`.
+
+For instance:
+
+```yaml
+---
+name: auto_devops_banner_disabled
+feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/12345
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/678910
+rollout_issue_url: https://gitlab.com/gitlab-com/gl-infra/production/-/issues/9876
+milestone: '16.5'
+type: gitlab_com_derisk
+group: group::pipeline execution
+```
+
+```yaml
+---
+name: ai_mr_creation
+feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/12345
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/14218
+rollout_issue_url: https://gitlab.com/gitlab-com/gl-infra/production/-/issues/83652
+milestone: '16.3'
+type: beta
+group: group::code review
+default_enabled: true
+```
+
+### Streamline the feature flag rollout process
+
+1. (Process) Transition to **create rollout issues in the
+ [Production issue tracker](https://gitlab.com/gitlab-com/gl-infra/production/-/issues)** and adapt the
+ template to be closer to the
+ [Change management issue template](https://gitlab.com/gitlab-com/gl-infra/production/-/blob/master/.gitlab/issue_templates/change_management.md)
+ (see [this issue](https://gitlab.com/gitlab-com/gl-infra/production/-/issues/2780) for inspiration)
+ That way, the rollout issue would only concern the actual production changes (i.e. enablement/disablement
+ of the flag on production) and should be closed as soon as the production change is confirmed to work as expected.
+1. (Automation) Automate most rollout steps, such as:
+ - (Done) [Let the author know that their feature has been deployed to staging / canary / production environments](https://gitlab.com/gitlab-org/quality/triage-ops/-/issues/1403)
+ - (Done) [Cross-link actual feature flag state change (from Chatops project) to rollout issues](https://gitlab.com/gitlab-org/gitlab/-/issues/290770)
+ - (Done) [Let the author know that their `default_enabled: true` MR has been deployed to production and that the feature flag can be removed from production](https://gitlab.com/gitlab-org/quality/triage-ops/-/merge_requests/2482)
+ - Automate the creation of rollout issues when a feature flag is first introduced in a merge request,
+ and provide an diff suggestion to fill the `rollout_issue_url` field (Danger)
+ - Check and enforce feature flag definition constraints in merge requests (Danger)
+ - Provide a diff suggestion to correct the `milestone` field when it's not the same value as
+ the MR milestone (Danger)
+ - Upon feature flag state change, notify on Slack the group responsible for it (chatops)
+ - 7 days before the Maximum Lifespan of a feature flag is reached, automatically create a "cleanup MR" with the group label set, and
+ assigned to the feature flag author (if they're still with GitLab). We could take advantage of the [automation of repetitive developer tasks](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134487)
+ - Enforce Maximum Lifespan of feature flags through automated reporting & regular review at the section level
+1. (Documentation/process) Ensure the rollout DRI stays online for a few hours after enabling a feature flag (ideally they'd enable the flag at the
+ beginning of their day) in case of any issue with the feature flag
+1. (Process) Provide a standardized set of rollout steps. Trade-offs to consider include:
+ - Likelihood of errors occurring
+ - Total actors (users / requests / projects / groups) affected by the feature flag rollout,
+ e.g. it will be bad if 100,000 users cannot log in when we roll out for 1%
+ - How long to wait between each step. Some feature flags only need to wait 10 minutes per step, some
+ flags should wait 24 hours. Ideally there should be automation to actively verify there
+ is no adverse effect for each step.
+
+### Provide better SSOT for the feature flag default states and current states & state changes on GitLab.com
+
+**GitLab customers**
+
+- [User documentation](../../../user/feature_flags.md):
+ Keep the current page but add filtering and sorting, similarly to the
+ [unofficial feature flags dashboard](https://samdbeckham.gitlab.io/feature-flags/).
+
+**Site reliability and Delivery engineers**
+
+We [assessed the usefulness of feature flag state change logging strategies](https://gitlab.com/gitlab-org/quality/engineering-productivity/team/-/issues/309)
+and it appears that both
+[internal GitLab.com feature flag state change issues](https://gitlab.com/gitlab-com/gl-infra/feature-flag-log/-/issues)
+and [internal GitLab.com feature flag state change logs](https://nonprod-log.gitlab.net) are useful for different
+audiences.
+
+**GitLab Engineering & Infra/Quality Directors / VPs, and CTO**
+
+- [Internal Sisense dashboard](https://app.periscopedata.com/app/gitlab/792066/Engineering-::-Feature-Flags):
+ Streamline the current dashboard to be more useful for its stakeholders.
+
+**GitLab Engineering and Product managers**
+
+- ["Feature flags requiring attention" monthly reports](https://gitlab.com/gitlab-org/quality/triage-reports/-/issues/?sort=created_date&state=opened&search=Feature%20flags&in=TITLE&assignee_id=None&first_page_size=100):
+ Make the current reports more actionable by linking to automatically created MRs for removing feature flags as well as improving documentation and best-practices around feature flags.
+
+## Iterations
+
+This work is being done as part of dedicated epic:
+[Improve internal usage of Feature Flags](https://gitlab.com/groups/gitlab-org/-/epics/3551).
+This epic describes a meta reasons for making these changes.
+
+## Resources
+
+- [What Are Feature Flags?](https://launchdarkly.com/blog/what-are-feature-flags/#:~:text=Feature%20flags%20are%20a%20software,portions%20of%20code%20are%20executed)
+- [Feature Flags Best Practices](https://featureflags.io/feature-flags-best-practices/)
+- [Short-lived or Long-lived Flags? Explaining Feature Flag lifespans](https://configcat.com/blog/2022/07/08/how-long-should-you-keep-feature-flags/)
diff --git a/doc/architecture/blueprints/gitlab_ml_experiments/index.md b/doc/architecture/blueprints/gitlab_ml_experiments/index.md
index e0675bb5be6..b9830778902 100644
--- a/doc/architecture/blueprints/gitlab_ml_experiments/index.md
+++ b/doc/architecture/blueprints/gitlab_ml_experiments/index.md
@@ -120,51 +120,46 @@ However, Service-Integration will establish certain necessary and optional requi
###### Ease of Use, Ownership Requirements
-1. <a name="R100">`R100`</a>: Required: the platform should be easy to use: imagine Heroku with [GitLab Production Readiness-approved](https://about.gitlab.com/handbook/engineering/infrastructure/production/readiness/) defaults.
-1. <a name="R110">`R110`</a>: Required: with the exception of an Infrastructure-led onboarding process, services are owned, deployed and managed by stage-group teams. In other words,services follow a "You Build It, You Run It" model of ownership.
-1. <a name="R120">`R120`</a>: Required: programming-language agnostic: no requirements for services. Services should be packaged as container images.
-1. <a name="R130">`R130`</a>: Recommended: Each service should be evaluated against the GitLab.com [Service Maturity Model](https://about.gitlab.com/handbook/engineering/infrastructure/service-maturity-model/).
-1. <a name="R140">`R140`</a>: Recommended: services using the platform have expedited production-readiness processes.
- 1. Production-readiness requirements graded by service maturity: low-traffic, low-maturity experimental services will have lower requirement thresholds than more mature services.
- 1. By default, the platform should provide services with defaults that would pass production-readiness review for the lowest service maturity-level.
- 1. At introduction, lowest maturity services can be deployed without production readiness, provided the meet certain automatically validated requirements. This removes Infrastructure gate-keeping from being a blocker to experimental service delivery.
+| ID | Required | Detail | Epic/Issue | Done? |
+|---|---|---|---|---|
+| `R100` | Required | The platform should be easy to use: imagine Heroku with [GitLab Production Readiness-approved](https://about.gitlab.com/handbook/engineering/infrastructure/production/readiness/) defaults. | [Runway to [BETA] : Increased Adoption and Self Service](https://gitlab.com/groups/gitlab-com/gl-infra/-/epics/1115) | **{dotted-circle}** No |
+| `R110` | Required | With the exception of an Infrastructure-led onboarding process, services are owned, deployed and managed by stage-group teams. In other words,services follow a “You Build It, You Run It” model of ownership.| [[Paused] Discussion: Tiered Support Model for Runway](https://gitlab.com/gitlab-com/gl-infra/platform/runway/team/-/issues/97) | **{dotted-circle}** No |
+| `R120` | Required | Programming-language agnostic: no requirements for services. Services should be packaged as container images.| [Runway to [BETA] : Increased Adoption and Self Service](https://gitlab.com/groups/gitlab-com/gl-infra/-/epics/1115) | **{dotted-circle}** No |
+| `R130` | Recommended | Each service should be evaluated against the GitLab.com [Service Maturity Model](https://about.gitlab.com/handbook/engineering/infrastructure/service-maturity-model/).| [Discussion: Introduce an 'Infrastructure Well-Architected Service Framework'](https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/2537) | **{dotted-circle}** No |
+| `R140` | Recommended | Services using the platform have expedited production-readiness processes. {::nomarkdown}<ol><li>Production-readiness requirements graded by service maturity: low-traffic, low-maturity experimental services will have lower requirement thresholds than more mature services. </li><li> By default, the platform should provide services with defaults that would pass production-readiness review for the lowest service maturity-level. </li><li> At introduction, lowest maturity services can be deployed without production readiness, provided the meet certain automatically validated requirements. This removes Infrastructure gate-keeping from being a blocker to experimental service delivery.</li></ol>{:/} | | |
###### Observability Requirements
-1. <a name="R200">`R200`</a>: Required: the platform must provide SLIs for services out-of-the-box.
- 1. While it is recommended that services expose internal metrics, it is not mandatory. The platform will provide monitoring from the load-balancer. This is to speed up deployment by removing barriers to experimentation.
- 1. For services that provide internal metrics scrape endpoints, the platform must be configurable to collect these.
- 1. The platform must provide generic load-balancer level SLIs for all services. Service owners must be able to select from constructing SLIs from internal application metrics, the platform-provided external SLIs, or a combination of both.
-1. <a name="R210">`R210`</a>: Required: Observability dashboards, rules, alerts (with per-term routing) must be generated from a manifest.
-1. <a name="R220">`R220`</a>:Required: standardized logging infrastructure.
- 1. Mandate that all logging emitted from services must be Structured JSON. Text logs are permitted but not recommended.
- 1. See [Common Service Libraries](#common-service-libraries) for more details of building common SDKs for observability.
+| ID | Required | Detail | Epic/Issue | Done? |
+|---|---|---|---|---|
+| `R200` | Required | The platform must provide SLIs for services out-of-the-box.{::nomarkdown}<ol><li>While it is recommended that services expose internal metrics, it is not mandatory. The platform will provide monitoring from the load-balancer. This is to speed up deployment by removing barriers to experimentation.</li><li>For services that provide internal metrics scrape endpoints, the platform must be configurable to collect these.</li><li>The platform must provide generic load-balancer level SLIs for all services. Service owners must be able to select from constructing SLIs from internal application metrics, the platform-provided external SLIs, or a combination of both.</li></ol>{:/} | [Observability: Default Metrics](https://gitlab.com/gitlab-com/gl-infra/platform/runway/team/-/issues/72), [Observability: Custom Metrics](https://gitlab.com/gitlab-com/gl-infra/platform/runway/team/-/issues/67) | **{check-circle}** Yes |
+| `R210` | Required | Observability dashboards, rules, alerts (with per-term routing) must be generated from a manifest. | [Observability: Metrics Catalog](https://gitlab.com/gitlab-com/gl-infra/platform/runway/team/-/issues/74) | **{check-circle}** Yes |
+| `R220` | Required | Standardized logging infrastructure.{::nomarkdown}<ol><li>Mandate that all logging emitted from services must be Structured JSON. Text logs are permitted but not recommended.</li><li>See <a href="#common-service-libraries">Common Service Libraries</a> for more details of building common SDKs for observability.</li></ol>{:/} | [Observability: Logs in Elasticsearch for model-gateway](https://gitlab.com/gitlab-com/gl-infra/platform/runway/team/-/issues/75), [Observability: Runway logs available to users](https://gitlab.com/gitlab-com/gl-infra/platform/runway/team/-/issues/84) | |
###### Deployment Requirements
-1. <a name="R300">`R300`</a>: Required: No secrets stored in CI/CD.
- 1. Authentication with Cloud Provider Resources should be exclusively via OIDC, managed as part of the platform.
- 1. Secrets should be stored in the Infrastructure-provided Hashicorp Vault for the environment and passed to applications through files or environment variables.
- 1. Generation and management of service account tokens should be done declaratively, without manual interaction.
-1. <a name="R310">`R310`</a>: Required: multiple environment should be supported, eg Staging and Production.
-1. <a name="R320">`R320`</a>: Required: the platform should be cost-effective. Kubernetes clusters should support multiple services and teams.
-1. <a name="R330">`R330`</a>: Recommended: gradual rollouts, rollbacks, blue-green deployments.
-1. <a name="R340">`R340`</a>: Required: services should be isolated from one another.
-1. <a name="R350">`R350`</a>: Recommended: services should have the ability to specify node characteristic requirements (eg, GPU).
-1. <a name="R360">`R360`</a>: Required: Developers should not need knowledge of Helm, Kubernetes, Prometheus in order to deploy. All required values are configured and validated in project-hosted manifest before generating Kubernetes manifests, Prometheus rules, etc.
-1. <a name="R370">`R370`</a>: Initially services should be synchronous only - using REST or GRPC requests.
- 1. This does not however preclude long-running HTTP(s) requests, for example long-polling or Websocket requests.
-1. <a name="R390">`R390`</a>: Each service hosted in its own GitLab repository with deployment manifest stored in the repository.
- 1. Continuous deployments that are initiated from the CI pipeline of the corresponding GitLab repository.
+| ID | Required | Detail | Epic/Issue | Done? |
+|---|---|---|---|---|
+| `R300` | Required | No secrets stored in CI/CD. {::nomarkdown} <ol><li>Authentication with Cloud Provider Resources should be exclusively via OIDC, managed as part of the platform.</li><li> Secrets should be stored in the Infrastructure-provided Hashicorp Vault for the environment and passed to applications through files or environment variables. </li><li>Generation and management of service account tokens should be done declaratively, without manual interaction.</li></ul>{:/} | [Secrets Management](https://gitlab.com/gitlab-com/gl-infra/platform/runway/team/-/issues/52) | **{dotted-circle}** No |
+| `R310` | Required | Multiple environment should be supported, eg Staging and Production. | | **{check-circle}** Yes |
+| `R320` | Required | The platform should be cost-effective. Kubernetes clusters should support multiple services and teams. | | |
+| `R330` | Recommended | Gradual rollouts, rollbacks, blue-green deployments. | | |
+| `R340` | Required | Services should be isolated from one another. | | |
+| `R350` | Recommended | Services should have the ability to specify node characteristic requirements (eg, GPU). | | |
+| `R360` | Required | Developers should not need knowledge of Helm, Kubernetes, Prometheus in order to deploy. All required values are configured and validated in project-hosted manifest before generating Kubernetes manifests, Prometheus rules, etc. | | |
+| `R370` | | Initially services should be synchronous only - using REST or GRPC requests.{::nomarkdown}<ol><li>This does not however preclude long-running HTTP(s) requests, for example long-polling or Websocket requests.</li></ol>{:/} | | |
+| `R390` | | Each service hosted in its own GitLab repository with deployment manifest stored in the repository. {::nomarkdown}<ol><li>Continuous deployments that are initiated from the CI pipeline of the corresponding GitLab repository.</li></ol>{:/} | | |
##### Security Requirements
-1. <a name="R400">`R400`</a>: stateful services deployed on the platform that utilize their own stateful storage (for example, custom deployed Postgres instance), must not store application security tokens, cloud-provider service keys or other long-lived security tokens in their stateful stores.
-1. <a name="R410">`R410`</a>: long-lived shared secrets are discouraged, and should be referenced in the service manifest as such, to allow for accounting and monitoring.
-1. <a name="R420">`R420`</a>: services using long-lived shared secrets should ensure that secret rotation can take place without downtime.
- 1. During a rotation, old and new generations of secrets should pass authentication, allowing gradual roll-out of new secrets.
+| ID | Required | Detail | Epic/Issue | Done? |
+|---|---|---|---|---|
+| `R400` | | Stateful services deployed on the platform that utilize their own stateful storage (for example, custom deployed Postgres instance), must not store application security tokens, cloud-provider service keys or other long-lived security tokens in their stateful stores. | | |
+| `R410` | | Long-lived shared secrets are discouraged, and should be referenced in the service manifest as such, to allow for accounting and monitoring. | | |
+| `R420` | | Services using long-lived shared secrets should ensure that secret rotation can take place without downtime. {::nomarkdown}<ol><li>During a rotation, old and new generations of secrets should pass authentication, allowing gradual roll-out of new secrets.</li></ol>{:/} | | |
##### Common Service Libraries
-1. <a name="R500">`R500`</a>: Experimental services would be strongly encouraged to adopt and use [LabKit](https://gitlab.com/gitlab-org/labkit) (for Go services), or [LabKit-Ruby](https://gitlab.com/gitlab-org/ruby/gems/labkit-ruby) for observability, context, correlation, FIPs verification, etc.
- 1. At present, there is no LabKit-Python library, but some experiments will run in Python, so building a library to providing observability, context, correlation services in Python will be required.
+| ID | Required | Detail | Epic/Issue | Done? |
+|---|---|---|---|---|
+| `R500` | Required | Experimental services would be strongly encouraged to adopt and use [LabKit](https://gitlab.com/gitlab-org/labkit) (for Go services), or [LabKit-Ruby](https://gitlab.com/gitlab-org/ruby/gems/labkit-ruby) for observability, context, correlation, FIPs verification, etc. {::nomarkdown}<ol><li>At present, there is no LabKit-Python library, but some experiments will run in Python, so building a library to providing observability, context, correlation services in Python will be required. </li></ol>{:/} | | |
diff --git a/doc/architecture/blueprints/gitlab_steps/gitlab-ci.md b/doc/architecture/blueprints/gitlab_steps/gitlab-ci.md
new file mode 100644
index 00000000000..8f97c307b37
--- /dev/null
+++ b/doc/architecture/blueprints/gitlab_steps/gitlab-ci.md
@@ -0,0 +1,247 @@
+---
+owning-stage: "~devops::verify"
+description: Usage of the [GitLab Steps](index.md) with [`.gitlab-ci.yml`](../../../ci/yaml/index.md).
+---
+
+# Usage of the [GitLab Steps](index.md) with [`.gitlab-ci.yml`](../../../ci/yaml/index.md)
+
+This document describes how [GitLab Steps](index.md) are integrated into the `.gitlab-ci.yml`.
+
+GitLab Steps will be integrated using a three-stage execution cycle
+and replace `before_script:`, `script:` and `after_script:`.
+
+- `setup:`: Execution stage responsible for provisioning the environment,
+ including cloning the repository, restoring artifacts, or installing all dependencies.
+ This stage will replace implicitly cloning, restoring artifacts, and cache download.
+- `run:`: Execution stage responsible for running a test, build,
+ or any other main command required by that job.
+- `teardown:`: Execution stage responsible for cleaning the environment,
+ uploading artifacts, or storing cache. This stage will replace implicit
+ artifacts and cache uploads.
+
+Before we can achieve three-stage execution we will ship minimal initial support
+that does not require any prior GitLab integration.
+
+## Phase 1: Initial support
+
+Initially the Step Runner will be used externally, without any prior dependencies
+to GitLab:
+
+- The `step-runner` will be provided as part of a container image.
+- The `step-runner` will be explicitly run in the `script:` section.
+- The `$STEPS` environment variable will be executed as [`type: steps`](step-definition.md#the-steps-step-type).
+
+```yaml
+hello-world:
+ image: registry.gitlab.com/gitlab-org/step-runner
+ variables:
+ STEPS: |
+ - step: gitlab.com/josephburnett/component-hello-steppy@master
+ inputs:
+ greeting: "hello world"
+ script:
+ - /step-runner ci
+```
+
+## Phase 2: The addition of `run:` to `.gitlab-ci.yml`
+
+In Phase 2 we will add `run:` as a first class way to use GitLab Steps:
+
+- `run:` will use a [`type: steps`](step-definition.md#the-steps-step-type) syntax.
+- `run:` will replace usage of `before_script`, `script` and `after_script`.
+- All existing functions to support Git cloning, artifacts, and cache would continue to be supported.
+- It is yet to be defined how we would support `after_script`, which is executed unconditionally
+ or when the job is canceled.
+- `run:` will not be allowed to be combined with `before_script:`, `script:` or `after_script:`.
+- GitLab Rails would not parse `run:`, instead it would only perform static validation
+ with a JSON schema provided by the Step Runner.
+
+```yaml
+hello-world:
+ image: registry.gitlab.com/gitlab-org/step-runner
+ run:
+ - step: gitlab.com/josephburnett/component-hello-steppy@master
+ inputs:
+ greeting: "hello world"
+```
+
+The following example would **fail** syntax validation:
+
+```yaml
+hello-world:
+ image: registry.gitlab.com/gitlab-org/step-runner
+ run:
+ - step: gitlab.com/josephburnett/component-hello-steppy@master
+ inputs:
+ greeting: "hello world"
+ script: echo "This is ambiguous and invalid example"
+```
+
+### Transitioning from `before_script:`, `script:` and `after_script:`
+
+GitLab Rails would automatically convert the `*script:` syntax into relevant `run:` specification:
+
+- Today `before_script:` and `script:` are joined together as a single script for execution.
+- The `after_script:` section is always executed in a separate context, representing a separate step to be executed.
+- It is yet to be defined how we would retain the existing behavior of `after_script`, which is always executed
+ regardless of the job status or timeout, and uses a separate timeout.
+- We would retain all implicit behavior which defines all environment variables when translating `script:`
+ into step-based execution.
+
+For example, this CI/CD configuration:
+
+```yaml
+hello-world:
+ before_script:
+ - echo "Run before_script"
+ script:
+ - echo "Run script"
+ after_script:
+ - echo "Run after_script"
+```
+
+Could be translated into this equivalent specification:
+
+```yaml
+hello-world:
+ run:
+ - step: gitlab.com/gitlab-org/components/steps/legacy/script@v1.0
+ inputs:
+ script:
+ - echo "Run before_script"
+ - echo "Run script"
+ - step: gitlab.com/gitlab-org/components/steps/legacy/script@v1.0
+ inputs:
+ script:
+ - echo "Run after_script"
+ when: always
+```
+
+## Phase 3: The addition of `setup:` and `teardown:` to `.gitlab-ci.yml`
+
+The addition of `setup:` and `teardown:` will replace the implicit functions
+provided by GitLab Runner: Git clone, artifacts and cache handling:
+
+- The usage of `setup:` would stop GitLab Runner from implicitly cloning the repository.
+- `artifacts:` and `cache:`, when specified, would be translated and appended to `setup:` and `teardown:`
+ to provide backward compatibility for the old syntax.
+- `release:`, when specified, would be translated and appended to `teardown:`
+ to provide backward compatibility for the old syntax.
+- `setup:` and `teardown:` could be used in `default:` to simplify support
+ of common workflows like where the repository is cloned, or how the artifacts are handled.
+- The split into 3-stage execution additionally improves composability of steps with `extends:`.
+- The `hooks:pre_get_sources_script` would be implemented similar to [`script:`](#transitioning-from-before_script-script-and-after_script)
+ and be prepended to `setup:`.
+
+For example, this CI/CD configuration:
+
+```yaml
+rspec:
+ script:
+ - echo "This job uses a cache."
+ artifacts:
+ paths: [binaries/, .config]
+ cache:
+ key: binaries-cache
+ paths: [binaries/*.apk, .config]
+```
+
+Could be translated into this equivalent specification executed by a step runner:
+
+```yaml
+rspec:
+ setup:
+ - step: gitlab.com/gitlab-org/components/git/clone@v1.0
+ - step: gitlab.com/gitlab-org/components/artifacts/download@v1.0
+ - step: gitlab.com/gitlab-org/components/cache/restore@v1.0
+ inputs:
+ key: binaries-cache
+ run:
+ - step: gitlab.com/gitlab-org/components/steps/legacy/script@v1.0
+ inputs:
+ script:
+ - echo "This job uses a cache."
+ teardown:
+ - step: gitlab.com/gitlab-org/components/artifacts/upload@v1.0
+ inputs:
+ paths: [binaries/, .config]
+ - step: gitlab.com/gitlab-org/components/cache/restore@v1.0
+ inputs:
+ key: binaries-cache
+ paths: [binaries/*.apk, .config]
+```
+
+### Inheriting common operations with `default:`
+
+`setup:` and `teardown:` are likely to become very verbose over time. One way to simplify them
+is to allow inheriting the common `setup:` and `teardown:` operations
+with `default:`.
+
+The previous example could be simplified to:
+
+```yaml
+default:
+ setup:
+ - step: gitlab.com/gitlab-org/components/git/clone@v1.0
+ - step: gitlab.com/gitlab-org/components/artifacts/download@v1.0
+ - step: gitlab.com/gitlab-org/components/cache/restore@v1.0
+ inputs:
+ key: binaries-cache
+ teardown:
+ - step: gitlab.com/gitlab-org/components/artifacts/upload@v1.0
+ inputs:
+ paths: [binaries/, .config]
+ - step: gitlab.com/gitlab-org/components/cache/restore@v1.0
+ inputs:
+ key: binaries-cache
+ paths: [binaries/*.apk, .config]
+
+rspec:
+ run:
+ - step: gitlab.com/gitlab-org/components/steps/legacy/script@v1.0
+ inputs:
+ script:
+ - echo "This job uses a cache."
+
+linter:
+ run:
+ - step: gitlab.com/gitlab-org/components/steps/legacy/script@v1.0
+ inputs:
+ script:
+ - echo "Run linting"
+```
+
+### Parallel jobs and `setup:`
+
+With the introduction of `setup:` at some point in the future we will introduce
+an efficient way to parallelize the jobs:
+
+- `setup:` would define all steps required to provision the environment.
+- The result of `setup:` would be snapshot and distributed as the base
+ for all parallel jobs, if `parallel: N` is used.
+- The `run:` and `teardown:` would be run on top of cloned job, and all its services.
+- The runner would control and intelligently distribute all parallel
+ jobs, significantly cutting the resource requirements for fixed
+ parts of the job (Git clone, artifacts, installing dependencies.)
+
+```yaml
+rspec-parallel:
+ image: ruby:3.2
+ services: [postgres, redis]
+ parallel: 10
+ setup:
+ - step: gitlab.com/gitlab-org/components/git/clone@v1.0
+ - step: gitlab.com/gitlab-org/components/artifacts/download@v1.0
+ inputs:
+ jobs: [setup-all]
+ - script: bundle install --without production
+ run:
+ - script: bundle exec knapsack
+```
+
+Potential GitLab Runner flow:
+
+1. Runner receives the `rspec-parallel` job with `setup:` and `parallel:` configured.
+1. Runner executes a job on top of Kubernetes cluster using block volumes up to the `setup`.
+1. Runner then runs 10 parallel jobs in Kubernetes, overlaying the block volume from 2
+ and continue execution of `run:` and `teardown:`.
diff --git a/doc/architecture/blueprints/gitlab_steps/index.md b/doc/architecture/blueprints/gitlab_steps/index.md
index 74c9ba1498d..5e3becfec19 100644
--- a/doc/architecture/blueprints/gitlab_steps/index.md
+++ b/doc/architecture/blueprints/gitlab_steps/index.md
@@ -33,12 +33,12 @@ shows a need for a better way to define CI job execution.
## Motivation
-Even though the current [`.gitlab-ci.yml`](../../../ci/yaml/gitlab_ci_yaml.md) is reasonably flexible, it easily becomes very
+Even though the current [`.gitlab-ci.yml`](../../../ci/index.md#the-gitlab-ciyml-file) is reasonably flexible, it easily becomes very
complex when trying to support complex workflows. This complexity is represented
with repetetitve patterns, a purpose-specific syntax, or a complex sequence of commands
to execute.
-This is particularly challenging, because the [`.gitlab-ci.yml`](../../../ci/yaml/gitlab_ci_yaml.md)
+This is particularly challenging, because the [`.gitlab-ci.yml`](../../../ci/index.md#the-gitlab-ciyml-file)
is inflexible on more complex workflows that require fine-tuning or special behavior
for the CI job execution. Its prescriptive approach how to handle Git cloning,
when artifacts are downloaded, or how the shell script is being executed quite often
@@ -46,7 +46,7 @@ results in the need to work around the system for pipelines that are not "standa
or when new features are requested.
This proves especially challenging when trying to add a new syntax to the
-[`.gitlab-ci.yml`](../../../ci/yaml/gitlab_ci_yaml.md)
+[`.gitlab-ci.yml`](../../../ci/index.md#the-gitlab-ciyml-file)
to support a specific feature, like [`secure files`](../../../ci/secure_files/index.md)
or `release:` keyword. Adding these special features on a syntax level
results in a more complex config, which is harder to maintain, and more complex
@@ -131,7 +131,14 @@ TBD
## Proposal
-TBD
+### GitLab Steps definition and syntax
+
+- [Step Definition](step-definition.md).
+- [Syntactic Sugar extensions](steps-syntactic-sugar.md).
+
+### Integration of GitLab Steps in `.gitlab-ci.yml`
+
+- [Usage of the GitLab Steps with `.gitlab-ci.yml`](gitlab-ci.md).
## Design and implementation details
diff --git a/doc/architecture/blueprints/gitlab_steps/step-definition.md b/doc/architecture/blueprints/gitlab_steps/step-definition.md
new file mode 100644
index 00000000000..08ca1ab7c31
--- /dev/null
+++ b/doc/architecture/blueprints/gitlab_steps/step-definition.md
@@ -0,0 +1,368 @@
+---
+owning-stage: "~devops::verify"
+description: The Step Definition for [GitLab Steps](index.md).
+---
+
+# The Step definition
+
+A step is the minimum executable unit that user can provide and is defined in a `step.yml` file.
+
+The following step definition describes the minimal syntax supported.
+The syntax is extended with [syntactic sugar](steps-syntactic-sugar.md).
+
+A step definition consists of two documents. The purpose of the document split is
+to distinguish between the declaration and implementation:
+
+1. [Specification / Declaration](#step-specification):
+
+ Provides the specification which describes step inputs and outputs,
+ as well any other metadata that might be needed by the step in the future (license, author, etc.).
+ In programming language terms, this is similar to a function declaration with arguments and return values.
+
+1. [Implementation](#step-implementation):
+
+ The implementation part of the document describes how to execute the step, including how the environment
+ has to be configured, or how actions can be configured.
+
+## Example step that prints a message to stdout
+
+In the following step example:
+
+1. The declaration specifies that the step accepts a single input named `message`.
+ The `message` is a required argument that needs to be provided when running the step
+ because it does not define `default:`.
+1. The implementation section specifies that the step is of type `exec`. When run, the step
+ will execute an `echo` command with a single argument (the `message` value).
+
+```yaml
+# .gitlab/ci/steps/exec-echo.yaml
+spec:
+ inputs:
+ message:
+---
+type: exec
+exec:
+ command: [echo, "${{inputs.message}}"]
+```
+
+## Step specification
+
+The step specification currently only defines inputs and outputs:
+
+- Inputs:
+ - Can be required or optional.
+ - Have a name and can have a description.
+ - Can contain a list of accepted options. Options limit what value can be provided for the input.
+ - Can define matching regexp. The matching regexp limits what value can be provided for the input.
+ - Can be expanded with the usage of syntax `${{ inputs.input_name }}`.
+- All **input values** can be accessed when `type: exec` is used,
+ by decoding the `$STEP_JSON` file that does provide information about the context of the execution.
+- Outputs:
+ - Have a name and can have a description.
+ - Can be set by writing to a special [dotenv](https://github.com/bkeepers/dotenv) file named:
+ `$OUTPUT_FILE` with a format of `output_name=VALUE` per output.
+
+For example:
+
+```yaml
+spec:
+ inputs:
+ message_with_default:
+ default: "Hello World"
+ message_that_is_required:
+ description: "This description explains that the input is required, because it does not specify a default:"
+ type_with_limited_options:
+ options: [bash, powershell, detect]
+ type_with_default_and_limited_options:
+ default: bash
+ options: [bash, powershell, detect]
+ description: "Since the options are provided, the default: needs to be one of the options"
+ version_with_matching_regexp:
+ match: ^v\d+\.\d+$
+ description: "The match pattern only allows values similar to `v1.2`"
+ outputs:
+ code_coverage:
+ description: "Measured code coverage that was calculated as part of the step"
+---
+type: steps
+steps:
+ - step: ./bash-script.yaml
+ inputs:
+ script: "echo Code Coverage = 95.4% >> $OUTPUT_FILE"
+```
+
+## Step Implementation
+
+The step definition can use the following types to implement the step:
+
+- `type: exec`: Run a binary command, using STDOUT/STDERR for tracing the executed process.
+- `type: steps`: Run a sequence of steps.
+- `type: parallel` (Planned): Run all steps in parallel, waiting for all of them to finish.
+- `type: grpc` (Planned): Run a binary command but use gRPC for intra-process communication.
+- `type: container` (Planned): Run a nested Step Runner in a container image of choice,
+ transferring all execution flow.
+
+### The `exec` step type
+
+The ability to run binary commands is one of the primitive functions:
+
+- The command to execute is defined by the `exec:` section.
+- The result of the execution is the exit code of the command to be executed, unless the default behavior is overwritten.
+- The default working directory in which the command is executed is the directory in which the
+ step is located.
+- By default, the command is not time-limited, but can be time-limited during job execution with `timeout:`.
+
+For example, an `exec` step with no inputs:
+
+```yaml
+spec:
+---
+type: exec
+exec:
+ command: [/bin/bash, ./my-script.sh]
+ timeout: 30m
+ workdir: /tmp
+```
+
+#### Example step that executes user-defined command
+
+The following example is a minimal step definition that executes a user-provided command:
+
+- The declaration section specifies that the step accepts a single input named `script`.
+- The `script` input is a required argument that needs to be provided when running the step
+ because no `default:` is defined.
+- The implementation section specifies that the step is of type `exec`. When run, the step
+ will execute in `bash` passing the user command with `-c` argument.
+- The command to be executed will be prefixed with `set -veo pipefail` to print the execution
+ to the job log and exit on the first failure.
+
+```yaml
+# .gitlab/ci/steps/exec-script.yaml
+
+spec:
+ inputs:
+ script:
+ description: 'Run user script.'
+---
+type: exec
+exec:
+ command: [/usr/bin/env, bash, -c, "set -veo pipefail; ${{inputs.script}}"]
+```
+
+### The `steps` step type
+
+The ability to run multiple steps in sequence is one of the primitive functions:
+
+- A sequence of steps is defined by an array of step references: `steps: []`.
+- The next step is run only if previous step succeeded, unless the default behavior is overwritten.
+- The result of the execution is either:
+ - A failure at the first failed step.
+ - Success if all steps in sequence succeed.
+
+#### Steps that use other steps
+
+The `steps` type depends extensively on being able to use other steps.
+Each item in a sequence can reference other external steps, for example:
+
+```yaml
+spec:
+---
+type: steps
+steps:
+ - step: ./.gitlab/ci/steps/ruby/install.yml
+ inputs:
+ version: 3.1
+ env:
+ HTTP_TIMEOUT: 10s
+ - step: gitlab.com/gitlab-org/components/bash/script@v1.0
+ inputs:
+ script: echo Hello World
+```
+
+The `step:` value is a string that describes where the step definition is located:
+
+- **Local**: The definition can be retrieved from a local source with `step: ./path/to/local/step.yml`.
+ A local reference is used when the path starts with `./` or `../`.
+ The resolved path to another local step is always **relative** to the location of the current step.
+ There is no limitation where the step is located in the repository.
+- **Remote**: The definition can also be retrieved from a remote source with `step: gitlab.com/gitlab-org/components/bash/script@v1.0`.
+ Using a FQDN makes the Step Runner pull the repository or archive containing
+ the step, using the version provided after the `@`.
+
+The `inputs:` section is a list of key-value pairs. The `inputs:` specify values
+that are passed and matched against the [step specification](#step-specification).
+
+The `env:` section is a list of key-value pairs. `env:` exposes the given environment
+variables to all children steps, including [`type: exec`](#the-exec-step-type) or [`type: steps`](#the-steps-step-type).
+
+#### Remote Steps
+
+To use remote steps with `step: gitlab.com/gitlab-org/components/bash/script@v1.0`
+the step definitions must be stored in a structured-way. The step definitions:
+
+- Must be stored in the `steps/` folder.
+- Can be nested in sub-directories.
+- Can be referenced by the directory name alone if the step definition
+ is stored in a `step.yml` file.
+
+For example, the file structure for a repository hosted in `git clone https://gitlab.com/gitlab-org/components.git`:
+
+```plaintext
+├── steps/
+├── ├── secret_detection.yml
+| ├── sast/
+│ | └── step.yml
+│ └── dast
+│ ├── java.yml
+│ └── ruby.yml
+```
+
+This structure exposes the following steps:
+
+- `step: gitlab.com/gitlab-org/components/secret_detection@v1.0`: From the definition stored at `steps/secret_detection.yml`.
+- `step: gitlab.com/gitlab-org/components/sast@v1.0`: From the definition stored at `steps/sast/step.yml`.
+- `step: gitlab.com/gitlab-org/components/dast/java@v1.0`: From the definition stored at `steps/dast/java.yml`.
+- `step: gitlab.com/gitlab-org/components/dast/ruby@v1.0`: From the definition stored at `steps/dast/ruby.yml`.
+
+#### Example step that runs other steps
+
+The following example is a minimal step definition that
+runs other steps that are local to the current step.
+
+- The declaration specifies that the step accepts two inputs, each with
+ a default value.
+- The implementation section specifies that the step is of type `steps`, meaning
+ the step will execute the listed steps in sequence. The usage of a top-level
+ `env:` makes the `HTTP_TIMEOUT` variable available in all executed steps.
+
+```yaml
+spec:
+ inputs:
+ ruby_version:
+ default: 3.1
+ http_timeout:
+ default: 10s
+---
+type: steps
+env:
+ HTTP_TIMEOUT: ${{inputs.http_timeout}}
+steps:
+ - step: ./.gitlab/ci/steps/exec-echo.yaml
+ inputs:
+ message: "Installing Ruby ${{inputs.ruby_version}}..."
+ - step: ./.gitlab/ci/ruby/install.yaml
+ inputs:
+ version: ${{inputs.ruby_version}}
+```
+
+## Context and interpolation
+
+Every step definition is executed in a context object which
+stores the following information that can be used by the step definition:
+
+- `inputs`: The list of inputs, including user-provided or default.
+- `outputs`: The list of expected outputs.
+- `env`: The current environment variable values.
+- `job`: The metadata about the current job being executed.
+ - `job.project`: Information about the project, for example ID, name, or full path.
+ - `job.variables`: All [CI/CD Variables](../../../ci/variables/predefined_variables.md) as provided by the CI/CD execution,
+ including project variables, predefined variables, etc.
+ - `job.pipeline`: Information about the current executed pipeline, like the ID, name, full path
+- `step`: Information about the current executed step, like the location of the step, the version used, or the [specification](#step-specification).
+- `steps` (only for `type: exec`): - Information about each step in sequence to be run, containing information about the
+ result of the step execution, like status or trace log.
+ - `steps.<name-of-the-step>.status`: The status of the step, like `success` or `failed`.
+ - `steps.<name-of-the-step>.outputs.<output-name>`: To fetch the output provided by the step
+
+The context object is used to enable support for the interpolation in the form of `${{ <value> }}`.
+
+Interpolation:
+
+- Is forbidden in the [step specification](#step-specification) section.
+ The specification is static configuration that should not affected by the runtime environment.
+- Can be used in the [step implementation](#step-implementation) section. The implementation
+ describes the runtime set of instructions for how step should be executed.
+- Is applied to every value of the hash of each data structure.
+- Of the *values* of each hash is possible (for now). The interpolation of *keys* is forbidden.
+- Is done when executing and passing control to a given step, instead of running
+ it once when the configuration is loaded. This enables chaining outputs to inputs, or making steps depend on the execution
+ of earlier steps.
+
+For example:
+
+```yaml
+# .gitlab/ci/steps/exec-echo.yaml
+spec:
+ inputs:
+ timeout:
+ default: 10s
+ bash_support_version:
+---
+type: steps
+env:
+ HTTP_TIMEOUT: ${{inputs.timeout}}
+ PROJECT_ID: ${{job.project.id}}
+steps:
+ - step: ./my/local/step/to/echo.yml
+ inputs:
+ message: "I'm currently building a project: ${{job.project.full_path}}"
+ - step: gitlab.com/gitlab-org/components/bash/script@v${{inputs.bash_support_version}}
+```
+
+## Reference data structures describing YAML document
+
+```go
+package main
+
+type StepEnvironment map[string]string
+
+type StepSpecInput struct {
+ Default *string `yaml:"default"`
+ Description string `yaml:"description"`
+ Options *[]string `yaml:"options"`
+ Match *string `yaml:"match"`
+}
+
+type StepSpecOutput struct {
+}
+
+type StepSpecInputs map[string]StepSpecInput
+type StepSpecOutputs map[string]StepSpecOutput
+
+type StepSpec struct {
+ Inputs StepSpecInput `yaml:"inputs"`
+ Outputs StepSpecOutputs `yaml:"outputs"`
+}
+
+type StepSpecDoc struct {
+ Spec StepSpec `yaml:"spec"`
+}
+
+type StepType string
+
+const StepTypeExec StepType = "exec"
+const StepTypeSteps StepType = "steps"
+
+type StepDefinition struct {
+ Def StepSpecDoc `yaml:"-"`
+ Env StepEnvironment `yaml:"env"`
+ Steps *StepDefinitionSequence `yaml:"steps"`
+ Exec *StepDefinitionExec `yaml:"exec"`
+}
+
+type StepDefinitionExec struct {
+ Command []string `yaml:"command"`
+ WorkingDir *string `yaml:"working_dir"`
+ Timeout *time.Duration `yaml:"timeout"`
+}
+
+type StepDefinitionSequence []StepReference
+
+type StepReferenceInputs map[string]string
+
+type StepReference struct {
+ Step string `yaml:"step"`
+ Inputs StepReferenceInputs `yaml:"inputs"`
+ Env StepEnvironment `yaml:"env"`
+}
+```
diff --git a/doc/architecture/blueprints/gitlab_steps/steps-syntactic-sugar.md b/doc/architecture/blueprints/gitlab_steps/steps-syntactic-sugar.md
new file mode 100644
index 00000000000..3ca54a45477
--- /dev/null
+++ b/doc/architecture/blueprints/gitlab_steps/steps-syntactic-sugar.md
@@ -0,0 +1,66 @@
+---
+owning-stage: "~devops::verify"
+description: The Syntactic Sugar extensions to the Step Definition
+---
+
+# The Syntactic Sugar extensions to the Step Definition
+
+[The Step Definition](step-definition.md) describes a minimal required syntax
+to be supported. To aid common workflows the following syntactic sugar is used
+to extend different parts of that document.
+
+## Syntactic Sugar for Step Reference
+
+Each of syntactic sugar extensions is converted into the simple
+[step reference](step-definition.md#steps-that-use-other-steps).
+
+### Easily execute scripts in a target environment
+
+`script:` is a shorthand syntax to aid execution of simple scripts, which cannot be used with `step:`
+and is run by an externally stored step component provided by GitLab.
+
+The GitLab-provided step component performs shell auto-detection unless overwritten,
+similar to how GitLab Runner does that now: based on a running system.
+
+`inputs:` and `env:` can be used for additional control of some aspects of that step component.
+
+For example:
+
+```yaml
+spec:
+---
+type: steps
+steps:
+ - script: bundle exec rspec
+ - script: bundle exec rspec
+ inputs:
+ shell: sh # Force runner to use `sh` shell, instead of performing auto-detection
+```
+
+This syntax example translates into the following equivalent syntax for
+execution by the Step Runner:
+
+```yaml
+spec:
+---
+type: steps
+steps:
+ - step: gitlab.com/gitlab-org/components/steps/script@v1.0
+ inputs:
+ script: bundle exec rspec
+ - step: gitlab.com/gitlab-org/components/steps/script@v1.0
+ inputs:
+ script: bundle exec rspec
+ shell: sh # Force runner to use `sh` shell, instead of performing auto-detection
+```
+
+This syntax example is **invalid** (and ambiguous) because the `script:` and `step:` cannot be used together:
+
+```yaml
+spec:
+---
+type: steps
+steps:
+ - step: gitlab.com/my-component/ruby/install@v1.0
+ script: bundle exec rspec
+```
diff --git a/doc/architecture/blueprints/google_artifact_registry_integration/index.md b/doc/architecture/blueprints/google_artifact_registry_integration/index.md
index 4c2bfe95c5e..ef66ae33b2a 100644
--- a/doc/architecture/blueprints/google_artifact_registry_integration/index.md
+++ b/doc/architecture/blueprints/google_artifact_registry_integration/index.md
@@ -116,6 +116,6 @@ One alternative solution considered was to use the Docker/OCI API provided by GA
- **Multiple Requests**: To retrieve all the required information about each image, multiple requests to different endpoints (listing tags, obtaining image manifests, and image configuration blobs) would have been necessary, leading to a `1+N` performance issue.
-GitLab had previously faced significant challenges with the last two limitations, prompting the development of a custom [GitLab Container Registry API](https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs-gitlab/api.md) to address them. Additionally, GitLab decided to [deprecate support](../../../update/deprecations.md#use-of-third-party-container-registries-is-deprecated) for connecting to third-party container registries using the Docker/OCI API due to these same limitations and the increased cost of maintaining two solutions in parallel. As a result, there is an ongoing effort to replace the use of the Docker/OCI API endpoints with custom API endpoints for all container registry functionalities in GitLab.
+GitLab had previously faced significant challenges with the last two limitations, prompting the development of a custom [GitLab Container Registry API](https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs/spec/gitlab/api.md) to address them. Additionally, GitLab decided to [deprecate support](../../../update/deprecations.md#use-of-third-party-container-registries-is-deprecated) for connecting to third-party container registries using the Docker/OCI API due to these same limitations and the increased cost of maintaining two solutions in parallel. As a result, there is an ongoing effort to replace the use of the Docker/OCI API endpoints with custom API endpoints for all container registry functionalities in GitLab.
Considering these factors, the decision was made to build the GAR integration from scratch using the proprietary GAR API. This approach provides more flexibility and control over the integration and can serve as a foundation for future expansions, such as support for other GAR artifact formats.
diff --git a/doc/architecture/blueprints/new_diffs.md b/doc/architecture/blueprints/new_diffs.md
index b5aeb9b8aa8..af1e4679c14 100644
--- a/doc/architecture/blueprints/new_diffs.md
+++ b/doc/architecture/blueprints/new_diffs.md
@@ -68,6 +68,35 @@ compared with the pros and cons of alternatives.
## Design and implementation details
+### Workspace & Artifacts
+
+- We will store implementation details like metrics, budgets, and development & architectural patterns here in the docs
+- We will store large bodies of research, the results of audits, etc. in the [wiki](https://gitlab.com/gitlab-com/create-stage/new-diffs/-/wikis/home) of the [New Diffs project](https://gitlab.com/gitlab-com/create-stage/new-diffs)
+- We will store audio & video recordings on the public Youtube channel in the Code Review / New Diffs playlist
+- We will store drafts, meeting notes, and other temporary documents in public Google docs
+
+### Definitions
+
+#### Maintainability
+
+Maintainable projects are _simple_ projects.
+
+Simplicity is the opposite of complexity. This uses a definition of simple and complex [described by Rich Hickey in "Simple Made Easy"](https://www.infoq.com/presentations/Simple-Made-Easy/) (Strange Loop, 2011).
+
+- Maintainable code is simple (single task, single concept, separate from other things).
+- Maintainable projects expand on simple code by having simple structure (folders define classes of behaviors, e.g. you can be assured that a component directory will never initiate a network call, because that would be complecting visual display with data access)
+- Maintainable applications flow out of simple organization and simple code. The old saying is a cluttered desk is representative of a cluttered mind. Rigorous discipline on simplicity will be represented in our output (the product). By being strict about working simply, we will naturally produce applications where our users can more easily reason about their behavior.
+
+#### Done
+
+GitLab has an existing [definition of done](/ee/development/contributing/merge_request_workflow.md#definition-of-done) which is geared primarily toward identifying when an MR is ready to be merged.
+
+In addition to the items in the GitLab definition of done, work on new diffs should also adhere to the following requirements:
+
+- Meets or exceeds all metrics
+ - Meets or exceeds our minimum accessibility metrics (these are explicitly not part of our defined priorities, since they are non-negotiable)
+- All work is fully documented for engineers (user documentation is a requirement of the standard definition of done)
+
<!--
This section should contain enough information that the specifics of your
change are understandable. This may include API specs (though not always
diff --git a/doc/architecture/blueprints/observability_logging/diagrams.drawio b/doc/architecture/blueprints/observability_logging/diagrams.drawio
new file mode 100644
index 00000000000..79b05247437
--- /dev/null
+++ b/doc/architecture/blueprints/observability_logging/diagrams.drawio
@@ -0,0 +1 @@
+<mxfile host="Electron" modified="2023-10-29T14:03:45.654Z" agent="5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/20.7.4 Chrome/106.0.5249.199 Electron/21.3.3 Safari/537.36" etag="mgCNcxJzZIj4Fii1_swS" version="20.7.4" type="device"><diagram id="eudcs1I04LxSKviLHc7n" name="Page-1">7VrZcuMoFP0aP9olCS3Wo+MsPVPpjnuSqkk/TWGJ2FSwcSG8zdcPWEiWQF5aXuRUzZPhglgOh8Pl4hboT1ZPDM7G32mMSMux4lUL3Lccx3aBK36kZZ1aur6fGkYMx6rS1vCK/0XKaCnrHMcoKVXklBKOZ2VjRKdTFPGSDTJGl+VqH5SUe53BETIMrxEkpvVvHPOxmoUTbO3fEB6Ns55tP0xLJjCrrGaSjGFMlwUTeGiBPqOUp6nJqo+IBC/DJf3ucUdpPjCGpvyYD+gIg8WPhP75GFv47m244iuvrVpZQDJXE/5jOmIoSdSY+ToDgtH5NEayLbsF7pZjzNHrDEaydCmWXtjGfEJUsTm2rCPEOFoVTGqsT4hOEGdrUSWnjsJNEQcEKr/cLoOdYTsuLIGvbFCt/ChveguOSCh8qrF6/t5++Qn+wqHDrX8GsfVu9WAFVn2Co89vdJ4gAy6x2jOZjNYEC9wYOAzaMEX4eZgbYPQ52uD+MueiGaTsSbpLbC9H2oC1AvydSAeOhrTnmUhbFUiHl0LaMZD+OUebIb8itsCRCXcNdp6EmeuEJcxyvSpgFlRAZoNLYeYZmO3k5AdBq57URoEFmsYqeR8RmCQ4KmO1Bdba1BZDfVdFm8wvWdLxsuz9qljzfp3lVpi/F9K/CuntJzKzzogtcwPEsEAHMVXjAxPSp4SyzXwAsB4fQSDXW4G+3RAoNmTd0KKEzlmEDm94DtkI8UN0NalSoIJXQYXMxhCBHC/Kw62ih+phQLGYSM5EPyjvXkP/0mmqr4rng9aQB3bIQNZQioPR0Iat+bTrE9g3CPzy9vAsLGLFiTjaxao7PuFSF2VqJFNvaAo3c+g1rgigW1YEF1ScV/t4cHZFCK6qCFYn9Iqi0LY6Vi1Z6Hg7hWGfBmxa1gXjQsLgmMKw27tqShgCfT936wqDpZ113esKQ/cUYbi7OWHwrKaFITwF0B+nAQpZpC55rnUZfP3GhTfzBfcpb0Flp1R69ncxTMY5igXEpH0AuRC16cbiWPIukXBGP/MrqWPosiaTLkKe45b1uiTX9rFSncvzfh/OOq/ihke6Yt0mFdfVrqxuXVfMDbWGbK2hCyuubV51L+A6aBR98OwQlCla75Jh/4Y3cbqDsC+scpCu/v90PQddzXjBl6VrcNN8bVReneAAzY7lKwi1q5p/XYfWBrfIV6cOYe1bZmt4U2x1z8RWD1yZra7B1l56R9hB2fmEpBXAnXTscQTJMxwiMqAJ5phKL3ZIOaeTQoUewSNZwKl2X6Bp5LufP++c6dagx7wrXhdABTku97hwlfhtURPUtaNODCYRlONZt0NCo89mNnhKzaZ2ONDdnroBFsMR0x8JLr3DzdBr8+fRnsPIr+vtXzNG6B/L4Zs6pL6sS3WVYPfvUXhfSKWdv3HdMoe7X4LDXY3DTl0Z1uLcbnBlDpuB7kY43LHdo0ODbbcDdjPZuR0qHxtAbJbKukehOwJHU9nWqHzlJxvbfGJ4wpzAobC9DBPEFnCICeYS2oGA7IOyiUhGZJ5IJhisX+IJgRt39fLvNfofj/J8gQN+WEUC3f874nIgstt/gKXob/9HBx7+Aw==</diagram></mxfile> \ No newline at end of file
diff --git a/doc/architecture/blueprints/observability_logging/index.md b/doc/architecture/blueprints/observability_logging/index.md
new file mode 100644
index 00000000000..d8259e0a736
--- /dev/null
+++ b/doc/architecture/blueprints/observability_logging/index.md
@@ -0,0 +1,632 @@
+---
+status: proposed
+creation-date: "2023-10-29"
+authors: [ "@vespian_gl" ]
+coach: "@mappelman"
+approvers: [ "@sguyon", "@nicholasklick" ]
+owning-stage: "~monitor::observability"
+participating-stages: []
+---
+
+<!-- vale gitlab.FutureTense = NO -->
+
+# GitLab Observability - Logging
+
+## Summary
+
+This design document outlines a system for storing and querying logs which will be a part of GitLab Observability Backend (GOB), together with [tracing](../observability_tracing/index.md) and [metrics](../observability_metrics/index.md).
+At its core the system is leveraging [OpenTelemetry logging](https://opentelemetry.io/docs/specs/otel/logs/) specification for data ingestion and ClickHouse database for storage.
+The users will interact with the data through GitLab UI.
+The system itself is multi-tenant and offers our users a way to store their application logs, query them, and in future iterations correlate with other observability signals (traces, errors, metrics, etc...).
+
+## Motivation
+
+After [tracing](../observability_tracing/index.md) and [metrics](../observability_metrics/index.md), logging is the last observability signal that we need to support to be able to provide our users with a fully-fledged observability solution.
+
+One could argue that logging itself is also the most important observability signal because it is so widespread.
+It predates metrics and tracing in the history of application observability and is usually implemented as one of the first things during development.
+
+Without logging support, it would be very hard if not impossible to fully understand for our users the performance and operation of the applications developed by them with the help of our platform.
+
+### Goals
+
+- **multi-tenant**: each user and their data should be isolated from others that are using the platform.
+ Users may query only the data that they have sent to the platform.
+- **follows OpenTelemetry standards**: logs ingestion should follow the [OpenTelemetry protocol](https://opentelemetry.io/docs/specs/otel/logs/data-model/).
+ Apart from being able to re-use the tooling and know-how that was already developed for OpenTelemetry protocol, we will not have to reinvent the wheel when it comes to wire protocol and data storage format.
+- **uses ClickHouse as a data storage backend**: ClickHouse has become the go-to solution for observability data at GitLab for a plethora of reasons.
+ Our tracing and metrics solutions already use it, so logging should be consistent with it and not introduce new dependencies.
+- **Users can query their data using reasonably complex queries**: storing logs by itself will not bring much value to our users.
+
+### Non-Goals
+
+- **complex query support and logs analytics** - at least in the first iteration we do not plan to support complex queries, in particular `GROUP BY` queries that users may want to use for quantitative logs analytics.
+ Supporting it is not trivial and requires some more research and work in the area of query language syntax.
+- **advanced data retention** - logs differ from traces and metrics concerning legal requirements.
+ Authorities may request logs stored by us as part of e.g. ongoing investigations.
+ In the initial iteration, we need to caution our users that our system is not ready for that and they need a secondary system for now if they intend to store e.g. access logs.
+ We will need more work around logs/data integrity and long-term storage policies to handle this use case.
+- **data deletion** - apart from the case where the data simply expires after a predefined storage period, we do not plan to support deleting individual logs by users.
+ This is left for later iterations.
+- **linking logs to traces** - we do not intend to support linking logs to traces in the first iteration, at least not in the UI.
+- **logs sampling** - for traces we expect users to sample their data before sending it to us while we focus only on enforcing the limits/quotas.
+ Logs should follow this pattern.
+ The log sampling implementation seems immature as well - a log sampler is [implemented in OTEL Collector](https://github.com/open-telemetry/opentelemetry-collector-contrib/pull/14920), but it is not clear if it can work together with traces sampling, and there is no official specification ([issue](https://github.com/open-telemetry/opentelemetry-specification/issues/2237), [pull request](https://github.com/open-telemetry/opentelemetry-specification/pull/2482)).
+
+## Proposal
+
+The architecture of logs ingestion follows the patterns outlined in the [tracing](../observability_tracing/index.md) and [metrics](../observability_metrics/index.md) proposals:
+
+![System Overview](system_overview.png)
+
+We re-use the components that were introduced by these proposals, so there are not going to be any new services added.
+Each top-level GitLab namespace has its own OTEL collector to which ingestion requests are directed by the cluster-wide Ingress.
+On the other hand, there is a single, cluster-wide query service that handles queries from users.
+The query service is tenant-aware.
+Rate-limiting of the user requests is done at the Ingress level.
+The cluster-wide Ingress is currently done using Traefik, and it is shared with all other services in the cluster.
+
+### Ingestion path
+
+We receive Log objects from customers in the JSON format over HTTP.
+The request arrives at the cluster-wide Ingress which routes the request to the appropriate OTEL collector.
+The collector then processes this request and executes INSERT statements against Clickhouse.
+
+### Read path
+
+GOB exposes an HTTP/JSON API that e.g. GitLab UI uses to query and then render logs.
+The cluster-wide Ingress is routing the requests to the query service which in turn parses the API request and executes an SQL query against ClickHouse.
+The results are then formatted into JSON response and sent back to the client.
+
+## Design and implementation details
+
+### Legacy code
+
+Handling logging signals is heavily influenced by the large amount of legacy code that needs to be supported, contrary to trace and metric signals.
+For metrics and tracing, OpenTelemetry specification defines new APIs and SDKs that can be leveraged.
+With logs, OpenTelemetry acts more like a bridge and enables legacy libraries/code to send their data to us.
+
+Users may create Log signals from plain log files using [filelogreceiver](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/receiver/filelogreceiver) or [fluentd](https://docs.fluentbit.io/manual/pipeline/outputs/opentelemetry).
+Existing log libraries may use [Log Bridge API](https://opentelemetry.io/docs/specs/otel/logs/bridge-api/) to emit logs using OTEL protocol.
+In time the ecosystem will most probably develop and the number of options will grow.
+The assumption is made that _how_ logs are ingested is up to the user.
+
+Hence we expose only an HTTP endpoint that accepts logs in OTEL format and assume that logs are already properly parsed and formatted.
+
+### Logs, Events, and Span Events
+
+Log messages can be sent using three different objects according to the OTEL specification:
+
+- [Log](https://opentelemetry.io/docs/specs/otel/logs/)
+- [Event](https://opentelemetry.io/docs/specs/otel/logs/event-api/)
+- [Span Event](https://opentelemetry.io/docs/concepts/signals/traces/#span-events)
+
+At least in the first iteration we can only either support Logs, Events, or Span-Events.
+
+We can't send Span Events as there are lots of legacy code that can not or will not implement tracing for various reasons.
+
+Even though Events use the same data model internally, their semantics differ.
+Logs have a mandatory severity level as a first-class parameter that Events do not need to have, and Events have a mandatory `event.name` and optional `event.domain` keys in the `Attributes` field of the Log record.
+Further, logs typically have messages in string form and events have data in the form of key-value pairs.
+There is a [discussion](https://github.com/open-telemetry/oteps/blob/main/text/0202-events-and-logs-api.md) to separate Log and Event APIs.
+More information on the differences between these two can be found [here](https://github.com/open-telemetry/oteps/blob/main/text/0202-events-and-logs-api.md#subtle-differences-between-logs-and-events).
+
+From the perspective of a developer/potential user, there seems to be no logging use case that couldn't be modeled as a Log record instead of sending an Event explicitly.
+Examples that the community gives e.g. [here](https://github.com/open-telemetry/opentelemetry-specification/issues/3254) or [here](https://github.com/open-telemetry/oteps/blob/main/text/0202-events-and-logs-api.md#subtle-differences-between-logs-and-events) are not convincing enough and could simply be modeled as Log records.
+
+Hence the decision to only support Log objects seems like a boring and simple solution.
+
+### Rate-limiting
+
+Similar to traces, logging data ingestion will be done at the Ingress level.
+As part of [the forward-auth](https://doc.traefik.io/traefik/middlewares/http/forwardauth/) flow, Traefik will forward the request to Gatekeeper which in turn leverages Redis for counting.
+This is currently done only for [the ingestion path](https://gitlab.com/gitlab-org/opstrace/opstrace/-/merge_requests/2236).
+Please check the MR description for more details on how it works.
+The read path rate limiting implementation is tracked [here](https://gitlab.com/gitlab-org/opstrace/opstrace/-/issues/2356).
+
+### Database schema
+
+[OpenTelemetry specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/data-model.md) defines a set of fields that are required by the implementations.
+There are some small discrepancies between the documented schema and the [protobuf definition](https://github.com/open-telemetry/opentelemetry-proto/blob/main/opentelemetry/proto/logs/v1/logs.proto), namely, TraceFlags is defined as an 8-bit field in the documentation whereas it is a 32-bit wide field in the proto definition.
+The remaining 24 bits are reserved.
+The Log message body may be any object and there is [no size limitation for the record](https://github.com/open-telemetry/opentelemetry-specification/issues/1251).
+For the purpose of this design document, we will assume that it is going to be an arbitrary string, either plain text or e.g. JSON, without length limits.
+
+#### Data filtering
+
+The schema uses Bloom Filters extensively.
+They prevent false negatives, but false positives are still possible, hence we will not be able to provide `!=` queries to users.
+The `Body` field is a special case, as it uses [`tokenbf_v1` tokenized Bloom Filter](https://clickhouse.com/docs/en/optimize/skipping-indexes#bloom-filter-types).
+The `tokenbf_v1` skipping index sees like a simpler and more lightweight approach than the `ngrambf_v1` index.
+Based on the very preliminary benchmarks below `ngrambf_v1` index will be also much more difficult to tune.
+The limitation is though that our users will be able to search only the full words for now.
+We (gu)estimate that there may be up to 10,000 different words in a given granule, and we aim for a 0.1% probability of false positives
+Using [this tool](https://krisives.github.io/bloom-calculator/) the optimal size of the filter was calculated at 143776 bits and 10 hash functions.
+
+#### Skipping indexes, `==`, `!=` and `LIKE` operators
+
+Skipping indexes only optimize searching for granules to scan.
+`==` and `LIKE` queries work as they should, the `!=` always results in a full scan due to Bloom Filters limitations.
+At least in the first iteration we will not make `!=` operator available to users.
+
+Based on the data, it may be much easier for us to tune the `tokenbf_v1` filter in the first iteration than the `ngrambf_v1`, because in the case of `ngrambf_v1` queries almost always result in a full scan for any reasonably big dataset.
+The reason for that is that the number of ngrams in the index is much higher than tokens hence matches are more frequent for data with high cardinality of words/symbols.
+
+A very preliminary benchmark was conducted to verify these assumptions.
+
+As testing data, we used the following table schemas and inserts/functions.
+They simulate a single tenant, as we want to focus only on the `Body` field.
+Normally the primary index would allow us to skip granules where there is no data for a given tenant.
+
+`tokenbf_v1` version of the table:
+
+```plaintext
+CREATE TABLE tbl2
+(
+ `Timestamp` DateTime64(9) CODEC(Delta(8), ZSTD(1)),
+ `TraceId` String CODEC(ZSTD(1)),
+ `ServiceName` LowCardinality(String) CODEC(ZSTD(1)),
+ `Duration` UInt8 CODEC(ZSTD(1)),
+ `SpanName` LowCardinality(String) CODEC(ZSTD(1)),
+ `Body` String CODEC(ZSTD(1)),
+ INDEX idx_body Body TYPE tokenbf_v1(143776, 10, 0) GRANULARITY 1
+)
+ENGINE = MergeTree
+PARTITION BY toDate(Timestamp)
+ORDER BY (ServiceName, SpanName, toUnixTimestamp(Timestamp), TraceId)
+SETTINGS index_granularity = 8192
+```
+
+`ngrambf_v1` version of the table:
+
+```plaintext
+CREATE TABLE tbl3
+(
+ `Timestamp` DateTime64(9) CODEC(Delta(8), ZSTD(1)),
+ `TraceId` String CODEC(ZSTD(1)),
+ `ServiceName` LowCardinality(String) CODEC(ZSTD(1)),
+ `Duration` UInt8 CODEC(ZSTD(1)),
+ `SpanName` LowCardinality(String) CODEC(ZSTD(1)),
+ `Body` String CODEC(ZSTD(1)),
+ INDEX idx_body Body TYPE ngrambf_v1(4,143776, 10, 0) GRANULARITY 1
+)
+ENGINE = MergeTree
+PARTITION BY toDate(Timestamp)
+ORDER BY (ServiceName, SpanName, toUnixTimestamp(Timestamp), TraceId)
+SETTINGS index_granularity = 8192
+```
+
+In both cases, their `Body` fields were filled with data that simulates a JSON map object:
+
+```plaintext
+CREATE FUNCTION genmap AS (n) -> arrayMap (x-> (x::String, (x*(rand()%40000+1))::String), range(1, n));
+
+INSERT INTO tbl(2|3)
+SELECT
+ now() - randUniform(1, 1_000_000) as Timestamp,
+ randomPrintableASCII(2) as TraceId,
+ randomPrintableASCII(2) as ServiceName,
+ rand32() as Duration,
+ randomPrintableASCII(2) as SpanName,
+ toJSONString(genmap(rand()%40+1)::Map(String, String)) as Body
+FROM numbers(10_000_000);
+```
+
+In the case of the `tokenbf_v1` table, we have:
+
+- `==` equality works, skipping index resulted in 224/1264 granules scanned:
+
+```plaintext
+zara.engel.vespian.net :) explain indexes=1 select count(*) from tbl2 where Body == '{"1":"14732","2":"29464","3":"44196","4":"58928","5":"73660","6":"88392","7":"103124","8":"117856","9":"132588","10":"147320","11":"162052"}'
+
+EXPLAIN indexes = 1
+SELECT count(*)
+FROM tbl2
+WHERE Body = '{"1":"14732","2":"29464","3":"44196","4":"58928","5":"73660","6":"88392","7":"103124","8":"117856","9":"132588","10":"147320","11":"162052"}'
+
+Query id: 60827945-a9b0-42f9-86a8-dfe77758a6b1
+
+┌─explain───────────────────────────────────────────┐
+│ Expression ((Projection + Before ORDER BY)) │
+│ Aggregating │
+│ Expression (Before GROUP BY) │
+│ Filter (WHERE) │
+│ ReadFromMergeTree (logging.tbl2) │
+│ Indexes: │
+│ MinMax │
+│ Condition: true │
+│ Parts: 69/69 │
+│ Granules: 1264/1264 │
+│ Partition │
+│ Condition: true │
+│ Parts: 69/69 │
+│ Granules: 1264/1264 │
+│ PrimaryKey │
+│ Condition: true │
+│ Parts: 69/69 │
+│ Granules: 1264/1264 │
+│ Skip │
+│ Name: idx_body │
+│ Description: tokenbf_v1 GRANULARITY 1 │
+│ Parts: 62/69 │
+│ Granules: 224/1264 │
+└───────────────────────────────────────────────────┘
+
+23 rows in set. Elapsed: 0.019 sec.
+```
+
+- `!=` inequality works as well, but results in fulltext scan - all granules were scanned:
+
+```plaintext
+zara.engel.vespian.net :) explain indexes=1 select count(*) from tbl2 where Body != '{"1":"14732","2":"29464","3":"44196","4":"58928","5":"73660","6":"88392","7":"103124","8":"117856","9":"132588","10":"147320","11":"162052"}'
+
+EXPLAIN indexes = 1
+SELECT count(*)
+FROM tbl2
+WHERE Body != '{"1":"14732","2":"29464","3":"44196","4":"58928","5":"73660","6":"88392","7":"103124","8":"117856","9":"132588","10":"147320","11":"162052"}'
+
+Query id: 01584696-30d8-4711-8469-44d4f2629c98
+
+┌─explain───────────────────────────────────────────┐
+│ Expression ((Projection + Before ORDER BY)) │
+│ Aggregating │
+│ Expression (Before GROUP BY) │
+│ Filter (WHERE) │
+│ ReadFromMergeTree (logging.tbl2) │
+│ Indexes: │
+│ MinMax │
+│ Condition: true │
+│ Parts: 69/69 │
+│ Granules: 1264/1264 │
+│ Partition │
+│ Condition: true │
+│ Parts: 69/69 │
+│ Granules: 1264/1264 │
+│ PrimaryKey │
+│ Condition: true │
+│ Parts: 69/69 │
+│ Granules: 1264/1264 │
+│ Skip │
+│ Name: idx_body │
+│ Description: tokenbf_v1 GRANULARITY 1 │
+│ Parts: 69/69 │
+│ Granules: 1264/1264 │
+└───────────────────────────────────────────────────┘
+
+23 rows in set. Elapsed: 0.017 sec.
+```
+
+- `LIKE` queries work, 271/1264 granules scanned:
+
+```plaintext
+zara.engel.vespian.net :) explain indexes=1 select * from tbl2 where Body like '%"11":"162052"%';
+
+EXPLAIN indexes = 1
+SELECT *
+FROM tbl2
+WHERE Body LIKE '%"11":"162052"%'
+
+Query id: 86e99d7a-6567-4000-badc-d0b8b2dc8936
+
+┌─explain─────────────────────────────────────┐
+│ Expression ((Projection + Before ORDER BY)) │
+│ ReadFromMergeTree (logging.tbl2) │
+│ Indexes: │
+│ MinMax │
+│ Condition: true │
+│ Parts: 69/69 │
+│ Granules: 1264/1264 │
+│ Partition │
+│ Condition: true │
+│ Parts: 69/69 │
+│ Granules: 1264/1264 │
+│ PrimaryKey │
+│ Condition: true │
+│ Parts: 69/69 │
+│ Granules: 1264/1264 │
+│ Skip │
+│ Name: idx_body │
+│ Description: tokenbf_v1 GRANULARITY 1 │
+│ Parts: 64/69 │
+│ Granules: 271/1264 │
+└─────────────────────────────────────────────┘
+
+20 rows in set. Elapsed: 0.047 sec.
+```
+
+`ngrambf_v1` tokenizer will be much harder to tune and use correctly:
+
+- equality using n-gram indexes works as well, but due to the high granularity of tokens in the bloom filter, we aren't skipping many granules:
+
+```plaintext
+zara.engel.vespian.net :) explain indexes=1 select count(*) from tbl3 where Body == '{"1":"14732","2":"29464","3":"44196","4":"58928","5":"73660","6":"88392","7":"103124","8":"117856","9":"132588","10":"147320","11":"162052"}'
+
+EXPLAIN indexes = 1
+SELECT count(*)
+FROM tbl3
+WHERE Body = '{"1":"14732","2":"29464","3":"44196","4":"58928","5":"73660","6":"88392","7":"103124","8":"117856","9":"132588","10":"147320","11":"162052"}'
+
+Query id: 22836e2d-5e49-4f51-b23c-facf5a3102c2
+
+┌─explain───────────────────────────────────────────┐
+│ Expression ((Projection + Before ORDER BY)) │
+│ Aggregating │
+│ Expression (Before GROUP BY) │
+│ Filter (WHERE) │
+│ ReadFromMergeTree (logging.tbl3) │
+│ Indexes: │
+│ MinMax │
+│ Condition: true │
+│ Parts: 60/60 │
+│ Granules: 1257/1257 │
+│ Partition │
+│ Condition: true │
+│ Parts: 60/60 │
+│ Granules: 1257/1257 │
+│ PrimaryKey │
+│ Condition: true │
+│ Parts: 60/60 │
+│ Granules: 1257/1257 │
+│ Skip │
+│ Name: idx_body │
+│ Description: ngrambf_v1 GRANULARITY 1 │
+│ Parts: 60/60 │
+│ Granules: 1239/1257 │
+└───────────────────────────────────────────────────┘
+
+23 rows in set. Elapsed: 0.025 sec.
+```
+
+- inequality here also results in a full scan:
+
+```plaintext
+zara.engel.vespian.net :) explain indexes=1 select count(*) from tbl3 where Body != '{"1":"14732","2":"29464","3":"44196","4":"58928","5":"73660","6":"88392","7":"103124","8":"117856","9":"132588","10":"147320","11":"162052"}'
+
+EXPLAIN indexes = 1
+SELECT count(*)
+FROM tbl3
+WHERE Body != '{"1":"14732","2":"29464","3":"44196","4":"58928","5":"73660","6":"88392","7":"103124","8":"117856","9":"132588","10":"147320","11":"162052"}'
+
+Query id: 2378c885-65b0-4be0-9564-fa7ba7c79172
+
+┌─explain───────────────────────────────────────────┐
+│ Expression ((Projection + Before ORDER BY)) │
+│ Aggregating │
+│ Expression (Before GROUP BY) │
+│ Filter (WHERE) │
+│ ReadFromMergeTree (logging.tbl3) │
+│ Indexes: │
+│ MinMax │
+│ Condition: true │
+│ Parts: 60/60 │
+│ Granules: 1257/1257 │
+│ Partition │
+│ Condition: true │
+│ Parts: 60/60 │
+│ Granules: 1257/1257 │
+│ PrimaryKey │
+│ Condition: true │
+│ Parts: 60/60 │
+│ Granules: 1257/1257 │
+│ Skip │
+│ Name: idx_body │
+│ Description: ngrambf_v1 GRANULARITY 1 │
+│ Parts: 60/60 │
+│ Granules: 1257/1257 │
+└───────────────────────────────────────────────────┘
+
+23 rows in set. Elapsed: 0.022 sec.
+```
+
+- LIKE statements work, but result in a fullscan as the ngrams match all the granules:
+
+```plaintext
+zara.engel.vespian.net :) explain indexes=1 select * from tbl3 where Body like '%"11":"162052"%';
+
+EXPLAIN indexes = 1
+SELECT *
+FROM tbl3
+WHERE Body LIKE '%"11":"162052"%'
+
+Query id: 957d8c98-819e-4487-93ac-868ffe0485ec
+
+┌─explain─────────────────────────────────────┐
+│ Expression ((Projection + Before ORDER BY)) │
+│ ReadFromMergeTree (logging.tbl3) │
+│ Indexes: │
+│ MinMax │
+│ Condition: true │
+│ Parts: 60/60 │
+│ Granules: 1257/1257 │
+│ Partition │
+│ Condition: true │
+│ Parts: 60/60 │
+│ Granules: 1257/1257 │
+│ PrimaryKey │
+│ Condition: true │
+│ Parts: 60/60 │
+│ Granules: 1257/1257 │
+│ Skip │
+│ Name: idx_body │
+│ Description: ngrambf_v1 GRANULARITY 1 │
+│ Parts: 60/60 │
+│ Granules: 1251/1257 │
+└─────────────────────────────────────────────┘
+
+20 rows in set. Elapsed: 0.023 sec.
+```
+
+#### Data Deduplication
+
+To provide cost-efficient service to our users, we need to think about deduplicating the data we get from our users.
+ClickHouse [ReplacingMergeTree](https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/replacingmergetree) deduplicates data automatically based on the primary key.
+We can't include all the relevant `Log` entry fields in the primary field, hence the idea of a Fingerprint as the very last part of the Primary Key.
+We normally do not use it for indexing, just to prevent unique records from being garbage collected.
+The fingerprint calculation algorithm and length have not been chosen yet, we may use the same one that `metrics` are using to calculate their Fingerprint.
+For now, we assume that it is 128-bit wide (16 8-bit chars).
+The columns we use for fingerprint calculation are the columns that are not present in the primary key: `Body`, `ResourceAttributes`, and `LogAttributes`.
+The fingerprint, due to very high cardinality, will need to go into the last place in the primary index.
+
+#### Data Retention
+
+There is a legal question of how long logs need to be stored and whether we allow for their deletion (e.g. due to the leak of some private data or data related to an investigation).
+In some jurisdictions, logs need to be kept for years and there must be no way to delete them.
+This affects deduplication unless we include the ObservedTimestamp in the fingerprint.
+As pointed out in the `Non-Goals` section, this is an issue we are going to tackle in future iterations.
+
+#### Ingestion-time fields
+
+I am intentionally not pulling [semantic convention fields](https://opentelemetry.io/docs/specs/semconv/general/logs/) into separate columns as users will use countless number of log formats, and it will probably not be possible to identify properties worth becoming a column.
+
+The `ObservedTimestamp` field is set by the collector during the ingestion.
+Users query by the `Timestamp` field and the log pruning is driven by the `ObservedTimestamp` field.
+The disadvantage of this approach is that `TTL DELETE` may not remove parts as early as we would like to because the primary index and TTL column differ so the data may not be localized.
+This seems like a good tradeoff though.
+We will offer users a predefined storage period that starts with the ingestion.
+In case when users ingest logs that have timestamps in the future or the past, the pruning of old logs could start too early or too late.
+Users could abuse the claimed log timestamp too to delay pruning.
+The `ObservedTimestamp` approach does not have these issues.
+
+During the ingestion, the `SeverityText` field is parsed into `SeverityNumber` if the `SeverityNumber` field has not been set.
+Queries will be using the `SeverityNumber` field as it is more efficient than plain text and offers higher granularity.
+
+```plaintext
+DROP TABLE if exists logs;
+CREATE TABLE logs
+(
+ `ProjectId` String CODEC(ZSTD(1)),
+ `Fingerprint` FixedString(16) CODEC(ZSTD(1)),
+ `Timestamp` DateTime64(9) CODEC(Delta(8), ZSTD(1)),
+ `ObservedTimestamp` DateTime64(9) CODEC(Delta(8), ZSTD(1)),
+ `TraceId` FixedString(16) CODEC(ZSTD(1)),
+ `SpanId` FixedString(8) CODEC(ZSTD(1)),
+ `TraceFlags` UInt32 CODEC(ZSTD(1)),
+ `SeverityText` LowCardinality(String) CODEC(ZSTD(1)),
+ `SeverityNumber` UInt8 CODEC(ZSTD(1)),
+ `ServiceName` String CODEC(ZSTD(1)),
+ `Body` String CODEC(ZSTD(1)),
+ `ResourceAttributes` Map(LowCardinality(String), String) CODEC(ZSTD(1)),
+ `LogAttributes` Map(LowCardinality(String), String) CODEC(ZSTD(1)),
+ INDEX idx_trace_id TraceId TYPE bloom_filter(0.001) GRANULARITY 1,
+ INDEX idx_span_id SpanId TYPE bloom_filter(0.001) GRANULARITY 1,
+ INDEX idx_trace_flags TraceFlags TYPE set(2) GRANULARITY 1,
+ INDEX idx_res_attr_key mapKeys(ResourceAttributes) TYPE bloom_filter(0.01) GRANULARITY 1,
+ INDEX idx_res_attr_value mapValues(ResourceAttributes) TYPE bloom_filter(0.01) GRANULARITY 1,
+ INDEX idx_log_attr_key mapKeys(LogAttributes) TYPE bloom_filter(0.01) GRANULARITY 1,
+ INDEX idx_log_attr_value mapValues(LogAttributes) TYPE bloom_filter(0.01) GRANULARITY 1,
+ INDEX idx_body Body TYPE tokenbf_v1(143776, 10, 0) GRANULARITY 1
+)
+ENGINE = ReplacingMergeTree
+PARTITION BY toDate(Timestamp)
+ORDER BY (ProjectId, ServiceName, SeverityNumber, toUnixTimestamp(Timestamp), TraceId, Fingerprint)
+TTL toDateTime(ObservedTimestamp) + toIntervalDay(30)
+SETTINGS index_granularity = 8192, ttl_only_drop_parts = 1;
+```
+
+### Query API, querying UI
+
+The main idea behind query API/workflow introduced by this proposal is to give users the freedom to query while at the same time providing limits both when it comes to query complexity and query resource usage/execution time.
+We can't foresee how users are going to query their data, nor how the data will look exactly - some will use Attributes, some will just focus on log-level, etc...
+
+In Clickhouse, individual queries [may have settings](https://clickhouse.com/docs/knowledgebase/configure-a-user-setting), which include [query complexity settings](https://clickhouse.com/docs/en/operations/settings/query-complexity).
+The query limits would be appended to each query automatically by the query service when constructing SQL statements.
+
+Fulltext queries for the Log entry `Body` field would be handled transparently by the query service as well thanks to ClickHouse optimizing `LIKE` queries using BloomFilters and tokenization of the search term.
+In future iterations we may want to consider n-gram tokenization, for now, the queries will be limited to full words only.
+
+It is up for debate whether we want to deduplicate log entries in the UI in case the user ingests duplicates.
+We could use the `max(ObservedTimestamp)` function to avoid duplicated entries in the time between records ingestion and ReplacingMergeTree's eventual deduplication kicking in.
+Definitely not in the first iteration though.
+
+The query service would also transparently translate the `SeverityText` attributes of the query into `SeverityNumber` while constructing the query.
+
+#### Query Service API schema
+
+We can't allow UI to send us SQL queries as that would open the possibility of abusing the system by users.
+We are also unable to support all the use cases that users could come up with when given the full flexibility of SQL query language.
+So the idea is for UI to provide a simple creator-like experience that would guide users.
+Something very similar to what GitLab currently has for searching MRs and Issues.
+UI code would then translate the query that the user came up with into a JSON and send it for processing to the query service.
+Based on the JSON received, the query service would then template an SQL query together with the query limits we mentioned above.
+
+For now, the UI and the JSON API would support only a basic set of operations on given fields:
+
+- Timestamp: `>`, `<`, `==`
+- TraceId: `==`, later iterations `in`
+- SpanId: `==`, later iterations `in`
+- TraceFlags: `==`, `!=`, later iterations:`in`, `notIn`
+- SeverityText: `==`, `!=`, later iterations: `in`, `notIn`
+- SeverityNumber: `<`,`>`, `==`, `!=`, later iterations: `in`, `notIn`
+- ServiceName: `==`, `!=`, later iterations: `in`, `notIn`
+- Body: `==`, `CONTAINS`
+- ResourceAttributes: `key==value`, `mapContains(key)`
+- LogAttributes: `key==value`, `mapContains(key)`
+
+The format of the intermediate JSON could look like the following:
+
+```yaml
+{
+ "query": [
+ { "type": "()|AND|OR",
+ "operands": {
+ [...]
+ },
+ {
+ "type": "==|!=|<|>|CONTAINS",
+ "column": "...",
+ "val": "..."
+ }
+ ]
+}
+```
+
+The `==|!=|<|>|CONTAINS` are non-nesting operands, they operate on concrete columns and result in `WHEN` conditions after being processed by the query service.
+The `()|AND|OR` are nesting operands and can only include other non-nesting operands.
+We may defer the implementation of the nesting operands for later iterations.
+There is implicit AND between the operands at the top level of the query structure.
+
+The query schema is intentionally kept simple compared to [the one used in the metrics proposal](../observability_metrics/index.md#api-structure).
+We may add fields like `QueryContext`, `BackendContext`, etc... in later iterations once a need arises.
+For now, we keep the schema as simple as possible and just make sure that the API is versioned so that we can change it easily in the future.
+
+## Open questions
+
+### Logging SDK Maturity
+
+OTEL standard does not intend to provide a standalone SDK for logging just like it did e.g. tracing.
+It may consider doing so only for a programming language that does not have its logging libraries which should be a pretty rare thing.
+All the existing logging libraries should instead use [bridge API](https://opentelemetry.io/docs/specs/otel/logs/bridge-api/) to interact with OTEL collector/send logs using OTEL Logs standard.
+
+The majority of languages have already made the required adjustments, except for Go.
+There is only very minimal support for GO ([repo](https://github.com/agoda-com/opentelemetry-go), [repo](https://github.com/agoda-com/opentelemetry-logs-go)).
+The official Uber Zap repository has barely an [issue](https://github.com/uber-go/zap/issues/654) about emitting events in spans.
+Opentelemetry [status page](https://opentelemetry.io/docs/instrumentation/go/) states that Go support as not implemented yet.
+
+The lack of native OTEL SDK support for emitting logs in Go may be an issue for us if we want to dogfood logging.
+We could work around these limitations in large extent by parsing log files using [filelogreceiver](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/receiver/filelogreceiver) or [fluentd](https://docs.fluentbit.io/manual/pipeline/outputs/opentelemetry).
+Contributing and improving the support of Go in OTEL is also a valid option.
+
+## Future work
+
+### Support for != operators in queries
+
+Bloom filters that we use in schemas do not allow for testing if the given term is NOT present in the log entry's body/attributes.
+This is a small but valid use case.
+A solution for that may be [inverted indexes](https://clickhouse.com/blog/clickhouse-search-with-inverted-indices) but this is still an experimental feature.
+
+### Documentation
+
+As part of the documentation effort, we may want to provide examples of how sending data to GOB can be done in different languages (uber-zap, logrus, log4j, etc...) just like we do for error tracking.
+Some of the applications can't be easily modified to send data to us (e.g. systemd/journald) and a log tailing/parsing needs to be employed using [filelogreceiver](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/receiver/filelogreceiver) or [fluentd](https://docs.fluentbit.io/manual/pipeline/outputs/opentelemetry).
+We could probably address both cases above by instrumenting our infrastructure and linking to our code from the documentation.
+This way we can both dog-food our solution, save some money as the GCE logging solution is pretty expensive, and give users real-life examples of how they can instrument their infrastructure.
+
+This could be one of the follow-up tasks once we are done with the implementation.
+
+### User query resource usage monitoring
+
+Long-term, we will need a way to monitor the number of user queries that failed due to limits enforcement and resource usage in general to fine-tune the query limits and make sure that users are not too aggressively restricted.
+
+## Iterations
+
+Please refer to [Observability Group planning epic](https://gitlab.com/groups/gitlab-org/opstrace/-/epics/92) and its linked issues for up-to-date information.
diff --git a/doc/architecture/blueprints/observability_logging/system_overview.png b/doc/architecture/blueprints/observability_logging/system_overview.png
new file mode 100644
index 00000000000..30c6510c3dc
--- /dev/null
+++ b/doc/architecture/blueprints/observability_logging/system_overview.png
Binary files differ
diff --git a/doc/architecture/blueprints/organization/diagrams/organization-isolation-broken.drawio.png b/doc/architecture/blueprints/organization/diagrams/organization-isolation-broken.drawio.png
new file mode 100644
index 00000000000..cd1301bb0bc
--- /dev/null
+++ b/doc/architecture/blueprints/organization/diagrams/organization-isolation-broken.drawio.png
Binary files differ
diff --git a/doc/architecture/blueprints/organization/diagrams/organization-isolation.drawio.png b/doc/architecture/blueprints/organization/diagrams/organization-isolation.drawio.png
new file mode 100644
index 00000000000..a9ff4ae5165
--- /dev/null
+++ b/doc/architecture/blueprints/organization/diagrams/organization-isolation.drawio.png
Binary files differ
diff --git a/doc/architecture/blueprints/organization/index.md b/doc/architecture/blueprints/organization/index.md
index 258a624e371..49bf18442e9 100644
--- a/doc/architecture/blueprints/organization/index.md
+++ b/doc/architecture/blueprints/organization/index.md
@@ -323,6 +323,7 @@ In iteration 2, an Organization MVC Experiment will be released. We will test th
- Organizations can be deleted.
- Organization Owners can access the Activity page for the Organization.
- Forking across Organizations will be defined.
+- [Organization Isolation](isolation.md) will be finished to meet the requirements of the initial set of customers
### Iteration 3: Organization MVC Beta (FY25Q1)
@@ -333,6 +334,7 @@ In iteration 3, the Organization MVC Beta will be released.
- Organization Owners can create, edit and delete Groups from the Groups overview.
- Organization Owners can create, edit and delete Projects from the Projects overview.
- The Organization URL path can be changed.
+- [Organization Isolation](isolation.md) is available.
### Iteration 4: Organization MVC GA (FY25Q2)
@@ -398,3 +400,4 @@ See [Organization: Frequently Asked Questions](organization-faq.md).
- [Cells blueprint](../cells/index.md)
- [Cells epic](https://gitlab.com/groups/gitlab-org/-/epics/7582)
- [Namespaces](../../../user/namespace/index.md)
+- [Organization Isolation](isolation.md)
diff --git a/doc/architecture/blueprints/organization/isolation.md b/doc/architecture/blueprints/organization/isolation.md
new file mode 100644
index 00000000000..238269c4329
--- /dev/null
+++ b/doc/architecture/blueprints/organization/isolation.md
@@ -0,0 +1,152 @@
+---
+status: ongoing
+creation-date: "2023-10-11"
+authors: [ "@DylanGriffith" ]
+coach:
+approvers: [ "@lohrc", "@alexpooley" ]
+owning-stage: "~devops::data stores"
+participating-stages: []
+---
+
+<!-- vale gitlab.FutureTense = NO -->
+
+# Organization Isolation
+
+This blueprint details requirements for Organizations to be isolated.
+Watch a [video introduction](https://www.youtube.com/watch?v=kDinjEHVVi0) that summarizes what Organization isolation is and why we need it.
+Read more about what an Organization is in [Organization](index.md).
+
+## What?
+
+<img src="diagrams/organization-isolation.drawio.png" width="800">
+
+All Cell-local data and functionality in GitLab (all data except the few
+things that need to exist on all Cells in the cluster) must be isolated.
+Isolation means that data or features can never cross Organization boundaries.
+Many features in GitLab can link data together.
+A few examples of things that would be disallowed by Organization Isolation are:
+
+1. [Related issues](../../../user/project/issues/related_issues.md): Users would not be able to take an issue in one Project in `Organization A` and relate that issue to another issue in a Project in `Organization B`.
+1. [Share a project/group with a group](../../../user/group/manage.md#share-a-group-with-another-group): Users would not be allowed to share a Group or Project in `Organization A` with another Group or Project in `Organization B`.
+1. [System notes](../../../user/project/system_notes.md): Users would not get a system note added to an issue in `Organization A` if it is mentioned in a comment on an issue in `Organization B`.
+
+## Why?
+
+<img src="diagrams/organization-isolation-broken.drawio.png" width="800">
+
+[GitLab Cells](../cells/index.md) depend on using the Organization as the sharding key, which will allow us to shard data between different Cells.
+Initially, when we start rolling out Organizations, we will be working with a single Cell `Cell 1`.
+`Cell 1` is our current GitLab.com deployment.
+Newly created Organizations will be created on `Cell 1`.
+Once Cells are ready, we will deploy `Cell 2` and begin migrating Organizations from `Cell 1` to `Cell 2`.
+Migrating workloads off will be critical to allowing us to rebalance our data across a fleet of servers and eventually run much smaller GitLab instances (and databases).
+
+If today we allowed users to create Organizations that linked to data in other Organizations, these links would suddenly break when an Organization is moved to a different Cell (because it won't know about the other Organization).
+For this reason we need to ensure from the very beginning of rolling out Organizations to customers that it is impossible to create any links that cross the Organization boundary, even when Organizations are still on the same Cell.
+If we don't, we will create even more mixed up related data that cannot be migrated between Cells.
+Not fulfilling the requirement of isolation means we risk creating a new top-level data wrapper (Organization) that cannot actually be used as a sharding key.
+
+The Cells project initially started with the assumption that we'd be able to shard by top-level Groups.
+We quickly learned that there were no constraints in the application that isolated top-level Groups.
+Many users (including ourselves) had created multiple top-level Groups and linked data across them.
+So we decided that the only way to create a viable sharding key was to create another wrapper around top-level Groups.
+Organizations were something our customers already wanted to gain more administrative capabilities as available in self-managed, and aggregate data across multiple top-level Groups, so this became a logical choice.
+Again, this leads us to realize that we cannot allow multiple Organizations to get mixed in together the same way we had with top-level Groups, otherwise we will end up back where we started.
+
+## How?
+
+Multiple POCs have been implemented to demonstrate how we will provide robust developer facing and customer facing constraints in the GitLab application and database that enforce the described isolation constraint.
+These are:
+
+1. [Enforce Organization Isolation based on `project_id` and `namespace_id` column on every table](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/133576)
+1. [Enforce Organization Isolation based on `organization_id` on every table](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/129889)
+1. [Validate if a top-level group is isolated to be migrated to an Organization](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131968)
+
+The major constraint these POCs were trying to overcome was that there is no standard way in the GitLab application or database to even determine what Organization (or Project or namespace) a piece of data belongs to.
+This means that the first step is to implement a standard way to efficiently find the parent Organization for any model or row in the database.
+
+The proposed solution is ensuring that every single table that exists in the `gitlab_main_cell` and `gitlab_ci_cell` (Cell-local) databases must include a valid sharding key that is either `project_id` or `namespace_id`.
+At first we considered enforcing everything to have an `organization_id`, but we determined that this would be too expensive to update for customers that need to migrate large Groups out of the default Organization.
+The added benefit is that more than half of our tables already have one of these columns.
+Additionally, if we can't consistently attribute data to a top-level Group, then we won't be able to validate if a top-level Group is safe to be moved to a new Organization.
+
+Once we have consistent sharding keys we can use them to validate all data on insert are not crossing any Organization boundaries.
+We can also use these sharding keys to help us decide whether:
+
+- Existing namespaces in the default Organization can be moved safely to a new Organization, because the namespace is already isolated.
+- The namespace owner would need to remove some links before migrating to a new Organization.
+- A set of namespaces is isolated as a group and could be moved together in bulk to a new Organization.
+
+## Detailed steps
+
+1. Implement developer facing documentation explaining the requirement to add these sharding keys and how they should choose between `project_id` and `namespace_id`.
+1. Add a way to declare a sharding key in `db/docs` and automatically populate it for all tables that already have a sharding key
+1. Implement automation in our CI pipelines and/or DB migrations that makes it impossible to create new tables without a sharding key.
+1. Implement a way for people to declare a desired sharding key in `db/docs` as
+ well as a path to the parent table from which it is migrated. Will only be
+ needed temporarily for tables that don't have a sharding key
+1. Attempt to populate as many "desired sharding key" as possible in an
+ automated way and delegate the MRs to other teams
+1. Fan out issues to other teams to manually populate the remaining "desired
+ sharding key"
+1. Start manually creating then automating the creation of migrations for
+ tables to populate sharding keys from "desired sharding key"
+1. Once all tables have sharding keys or "desired sharding key", we ship an
+ evolved version of the
+ [POC](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/133576), which
+ will enforce that newly inserted data cannot cross Organization boundaries.
+ This may need to be expanded to more than just foreign keys, and should also
+ include loose foreign keys and possibly any relationships described in
+ models. It can temporarily depend on inferring, at runtime, the sharding key
+ from the "desired sharding key" which will be a less performant option while
+ we backfill the sharding keys to all tables but allow us to unblock
+ implementing the isolation rules and user experience of isolation.
+1. Finish migration of ~300 tables that are missing a sharding key:
+ 1. The Tenant Scale team migrates the first few tables.
+ 1. We build a dashboard showing our progress and continue to create
+ automated MRs for the sharding keys that can be automatically inferred
+ and automate creating issues for all the sharding keys that can't be
+ automatically inferred
+1. Validate that all existing `project_id` and `namespace_id` columns on all Cell-local tables can reliably be assumed to be the sharding key. This requires assigning issues to teams to confirm that these columns aren't used for some other purpose that would actually not be suitable. If there is an issue with a table we need to migrate and rename these columns, and then add a new `project_id` or `namespace_id` column with the correct sharding key.
+1. We allow customers to create new Organizations without the option to migrate namespaces into them. All namespaces need to be newly created in their new Organization.
+1. Implement new functionality in GitLab similar to the [POC](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131968), which allows a namespace owner to see if their namespace is fully isolated.
+1. Implement functionality that allows namespace owners to migrate an existing namespace from one Organization to another. Most likely this will be existing customers that want to migrate their namespace out of the default Organization into a newly created Organization. Only isolated namespaces as implemented in the previous step will be allowed to move.
+1. Expand functionality to validate if a namespace is isolated, so that users can select multiple namespaces they own and validate that the selected group of namespaces is isolated. Links between the selected namespaces would stay intact.
+1. Implement functionality that allows namespace owners to migrate multiple existing namespaces from one Organization to another. Only isolated namespaces as implemented in the previous step will be allowed to move.
+1. We build better tooling to help namespace owners with cleaning up unwanted links outside of their namespace to allow more customers to migrate to a new Organization. This step would be dependent on the amount of existing customers that actually have links to clean up.
+
+The implementation of this effort will be tracked in [#11670](https://gitlab.com/groups/gitlab-org/-/epics/11670).
+
+## Alternatives considered
+
+### Add any data that need to cross Organizations to cluster-wide tables
+
+We plan on having some data at the cluster level in our Cells architecture (for example
+Users), so it might stand to reason that we can make any data cluster-wide
+that might need to cross Organization boundaries and this would solve the problem.
+
+This could be an option for a limited set of features and may turn out to be
+necessary for some critical workflows.
+However, this should not become the default option, because it will ultimately lead to the Cells architecture not achieving the horizontal scaling goals.
+Features like [sharing a group with a group](../../../user/group/manage.md#share-a-group-with-another-group) are very tightly connected to some of the worst performing functionality in our
+application with regard to scalability.
+We are hoping that by splitting up our databases in Cells we will be able to unlock more scaling headroom and reduce the problems associated with supporting these features.
+
+### Do nothing and treat these anomalies as an acceptable edge case
+
+This idea hasn't been explored deeply but is rejected on the basis that these
+anomalies will appear as data loss while moving customer data between Cells.
+Data loss is a very serious kind of bug, especially when customers are not opting into being moved between servers.
+
+### Solve these problems feature by feature
+
+This could be done, for example, by implementing an application rule that
+prevents users from adding an issue link between Projects on different Organizations.
+We would need to find all such features by asking teams, and
+they would need to fix them all as a special case business rule.
+
+This may be a viable, less robust option, but it does not give us a lot of confidence in our system.
+Without a robust way to ensure that all Organization data is isolated, we would have to trust that each feature we implement has been manually checked.
+This creates a real risk that we miss something, and again we would end up with customer data loss.
+Another challenge here is that if we are not confident in our isolation constraints, then we may end up attributing various unrelated bugs to possible data loss.
+As such it could become a rabbit hole to debug all kinds of unrelated bugs.
diff --git a/doc/architecture/blueprints/runner_admission_controller/index.md b/doc/architecture/blueprints/runner_admission_controller/index.md
index 92c824527ec..21dc1d53303 100644
--- a/doc/architecture/blueprints/runner_admission_controller/index.md
+++ b/doc/architecture/blueprints/runner_admission_controller/index.md
@@ -1,7 +1,7 @@
---
status: proposed
creation-date: "2023-03-07"
-authors: [ "@ajwalker" ]
+authors: [ "@ajwalker", "@johnwparent" ]
coach: [ "@ayufan" ]
approvers: [ "@DarrenEastman", "@engineering-manager" ]
owning-stage: "~devops::<stage>"
@@ -14,7 +14,7 @@ The GitLab `admission controller` (inspired by the [Kubernetes admission control
An admission controller can be registered to the GitLab instance and receive a payload containing jobs to be created. Admission controllers can be _mutating_, _validating_, or both.
-- When _mutating_, mutatable job information can be modified and sent back to the GitLab instance. Jobs can be modified to conform to organizational policy, security requirements, or have, for example, their tag list modified so that they're routed to specific runners.
+- When _mutating_, mutable job information can be modified and sent back to the GitLab instance. Jobs can be modified to conform to organizational policy, security requirements, or have, for example, their tag list modified so that they're routed to specific runners.
- When _validating_, a job can be denied execution.
## Motivation
@@ -35,12 +35,12 @@ Before going further, it is helpful to level-set the current job handling mechan
- On the request from a runner to the API for a job, the database is queried to verify that the job parameters matches that of the runner. In other words, when runners poll a GitLab instance for a job to execute they're assigned a job if it matches the specified criteria.
- If the job matches the runner in question, then the GitLab instance connects the job to the runner and changes the job state to running. In other words, GitLab connects the `job` object with the `Runner` object.
- A runner can be configured to run un-tagged jobs. Tags are the primary mechanism used today to enable customers to have some control of which Runners run certain types of jobs.
-- So while runners are scoped to the instance, group, or project, there are no additional access control mechanisms today that can easily be expanded on to deny access to a runner based on a user or group identifier.
+- So while runners are scoped to the instance, group, or project, there are no additional access control mechanisms today that can be expanded on to deny access to a runner based on a user or group identifier.
-The current CI jobs queue logic is as follows. **Note - in the code ww still use the very old `build` naming construct, but we've migrated from `build` to `job` in the product and documentation.
+The current CI jobs queue logic is as follows. **Note - in the code we still use the very old `build` naming construct, but we've migrated from `build` to `job` in the product and documentation.
```ruby
-jobs =
+jobs =
if runner.instance_type?
jobs_for_shared_runner
elsif runner.group_type?
@@ -96,22 +96,31 @@ Each runner has a tag such as `zone_a`, `zone_b`. In this scenario the customer
1. When a job is created the `project information` (`project_id`, `job_id`, `api_token`) will be used to query GitLab for specific details.
1. If the `user_id` matches then the admissions controller modifies the job tag list. `zone_a` is added to the tag list as the controller has detected that the user triggering the job should have their jobs run IN `zone_a`.
+**Scenario 3**: Runner pool with specific tag scheme, user only has access to a specific subset
+
+Each runner has a tag identifier unique to that runner, e.g. `DiscoveryOne`, `tugNostromo`, `MVSeamus`, etc. Users have arbitrary access to these runners, however we don't want to fail a job on access denial, instead we want to prevent the job from being executed on runners to which the user does not have access. We also don't want to reduce the pool of runners the job can be run on.
+
+1. Configure an admissions controller to mutate jobs based on `user_id`.
+1. When a job is created the `project information` (`project_id`, `job_id`, `api_token`) will be used to query GitLab for specific details.
+1. The admission controller queries available runners with the `user_id` and collects all runners for which the job cannot be run. If this is _all_ runners, the admission controller rejects the job, which is dropped. No tags are modified, and a message is included indicating the reasoning. If there are runners for which the user has permissions, the admission controller filters the associated runners for which there are no permissions.
+
### MVC
#### Admission controller
1. A single admission controller can be registered at the instance level only.
-1. The admission controller must respond within 30 seconds.
-1. The admission controller will receive an array of individual jobs. These jobs may or may not be related to each other. The response must contain only responses to the jobs made as part of the request.
+1. The admission controller must respond within 1 hr.
+1. The admission controller will receive individual jobs. The response must contain only responses to that job.
+1. The admission controller will recieve an API callback for rejection and acceptance, with the acceptance callback accepting mutation parameters.
#### Job Lifecycle
-1. The lifecycle of a job will be updated to include a new `validating` state.
+1. The `preparing` job state will be expanded to include the validation process prerequisite.
```mermaid
stateDiagram-v2
- created --> validating
- state validating {
+ created --> preparing
+ state preparing {
[*] --> accept
[*] --> reject
}
@@ -127,10 +136,12 @@ Each runner has a tag such as `zone_a`, `zone_b`. In this scenario the customer
executed --> created: retry
```
-1. When the state is `validating`, the mutating webhook payload is sent to the admission controller.
-1. For jobs where the webhook times out (30 seconds) their status should be set as though the admission was denied. This should
+1. When the state is `preparing`, the mutating webhook payload is sent to the admission controller asynchronously. This will be retried a number of times as needed.
+1. The `preparing` state will wait for a response from the webhook or until timeout.
+1. The UI should be updated with the current status of the job prerequisites and admission
+1. For jobs where the webhook times out (1 hour) their status should be set as though the admission was denied with a timeout reasoning. This should
be rare in typical circumstances.
-1. Jobs with denied admission can be retried. Retried jobs will be resent to the admission controller along with any mutations that they received previously.
+1. Jobs with denied admission can be retried. Retried jobs will be resent to the admission controller without tag mutations or runner filtering reset.
1. [`allow_failure`](../../../ci/yaml/index.md#allow_failure) should be updated to support jobs that fail on denied admissions, for example:
```yaml
@@ -141,8 +152,8 @@ be rare in typical circumstances.
on_denied_admission: true
```
-1. The UI should be updated to display the reason for any job mutations (if provided).
-1. A table in the database should be created to store the mutations. Any changes that were made, like tags, should be persisted and attached to `ci_builds` with `acts_as_taggable :admission_tags`.
+1. The UI should be updated to display the reason for any job mutations (if provided) or rejection.
+1. Tag modifications applied by the Admission Controller should be persisted by the system with associated reasoning for any modifications, acceptances, or rejections
#### Payload
@@ -153,8 +164,10 @@ be rare in typical circumstances.
1. The response payload is comprised of individual job entries consisting of:
- Job ID.
- Admission state: `accepted` or `denied`.
- - Mutations: Only `tags` is supported for now. The tags provided replaces the original tag list.
+ - Mutations: `additions` and `removals`. `additions` supplements the existing set of tags, `removals` removes tags from the current tag list
- Reason: A controller can provide a reason for admission and mutation.
+ - Accepted Runners: runners to be considered for job matching, can be empty to match all runners
+ - Rejected Runners: runners that should not be considered for job matching, can be empty to match all runners
##### Example request
@@ -170,7 +183,9 @@ be rare in typical circumstances.
...
},
"tags": [ "docker", "windows" ]
- },
+ }
+]
+[
{
"id": 245,
"variables": {
@@ -180,7 +195,9 @@ be rare in typical circumstances.
...
},
"tags": [ "linux", "eu-west" ]
- },
+ }
+]
+[
{
"id": 666,
"variables": {
@@ -202,20 +219,29 @@ be rare in typical circumstances.
"id": 123,
"admission": "accepted",
"reason": "it's always-allow-day-wednesday"
- },
+ }
+]
+[
{
"id": 245,
"admission": "accepted",
- "mutations": {
- "tags": [ "linux", "us-west" ]
+ "tags": {
+ "add": [ "linux", "us-west" ],
+ "remove": [...]
},
- "reason": "user is US employee: retagged region"
- },
+ "runners": {
+ "accepted_ids": ["822993167"],
+ "rejected_ids": ["822993168"]
+ },
+ "reason": "user is US employee: retagged region; user only has uid on runner 822993167"
+ }
+]
+[
{
"id": 666,
"admission": "rejected",
"reason": "you have no power here"
- },
+ }
]
```
@@ -229,13 +255,32 @@ be rare in typical circumstances.
### Implementation Details
-1. _placeholder for steps required to code the admissions controller MVC_
+#### GitLab
+
+1. Expand `preparing` state to engage the validation process via the `prerequsite` interface.
+1. Amend `preparing` state to indicate to user, via the UI and API, the status of job preparation with regard to the job prerequisites
+ 1. Should indicate status of each prerequisite resource for the job separately as they are asynchronous
+ 1. Should indicate overall prerequisite status
+1. Introduce a 1 hr timeout to the entire `preparing` state
+1. Add an `AdmissionValidation` prerequisite to the `preparing` status dependencies via `Gitlab::Ci::Build::Prerequisite::Factory`
+1. Convert the Prerequisite factory and `preparing` status to operate asynchronously
+1. Convert `PreparingBuildService` to operate asynchronously
+1. `PreparingBuildService` transitions the job from preparing to failed or pending depending on success of validation.
+1. AdmissionValidation performs a reasonable amount of retries when sending request
+1. Add API endpoint for Webhook/Admission Controller response callback
+ 1. Accepts Parameters:
+ - Acceptance/Rejection
+ - Reason String
+ - Tag mutations (if accepted, otherwise ignored)
+ 1. Callback encodes one time auth token
+1. Introduce new failure reasoning on validation rejection
+1. Admission controller impacts on job should be persisted
+1. Runner selection filtering per job as a function of the response from the Admission controller (mutating web hook) should be added
## Technical issues to resolve
| issue | resolution|
| ------ | ------ |
-|We may have conflicting tag-sets as mutating controller can make it possible to define AND, OR and NONE logical definition of tags. This can get quite complex quickly. | |
|Rule definition for the queue web hook|
|What data to send to the admissions controller? Is it a subset or all of the [predefined variables](../../../ci/variables/predefined_variables.md)?|
|Is the `queueing web hook` able to run at GitLab.com scale? On GitLab.com we would trigger millions of webhooks per second and the concern is that would overload Sidekiq or be used to abuse the system.
diff --git a/doc/architecture/blueprints/secret_detection/index.md b/doc/architecture/blueprints/secret_detection/index.md
index fc97ca71d7f..76bf6dd4088 100644
--- a/doc/architecture/blueprints/secret_detection/index.md
+++ b/doc/architecture/blueprints/secret_detection/index.md
@@ -26,28 +26,22 @@ job logs, and project management features such as issues, epics, and MRs.
### Goals
-- Support asynchronous secret detection for the following scan targets:
- - push events
- - issuable creation
- - issuable updates
- - issuable comments
+- Support platform-wide detection of tokens to avoid secret leaks
+- Prevent exposure by rejecting detected secrets
+- Provide scalable means of detection without harming end user experience
-### Non-Goals
+See [target types](#target-types) for scan target priorities.
-The current proposal is limited to asynchronous detection and alerting only.
+### Non-Goals
-**Blocking** secrets on push events is high-risk to a critical path and
-would require extensive performance profiling before implementing. See
-[a recent example](https://gitlab.com/gitlab-org/gitlab/-/issues/246819#note_1164411983)
-of a customer incident where this was attempted.
+Initial proposal is limited to detection and alerting across platform, with rejection only
+during [preceive Git interactions and browser-based detection](#iterations).
Secret revocation and rotation is also beyond the scope of this new capability.
Scanned object types beyond the scope of this MVC include:
-- Media types (JPEGs, PDFs,...)
-- Snippets
-- Wikis
+See [target types](#target-types) for scan target priorities.
#### Management UI
@@ -69,7 +63,13 @@ which remain focused on active detection.
## Proposal
-To achieve scalable secret detection for a variety of domain objects a dedicated
+The first iteration of the experimental capability will feature a blocking
+pre-receive hook implemented within the Rails application. This iteration
+will be released in an experimental state to select users and provide
+opportunity for the team to profile the capability before considering extraction
+into a dedicated service.
+
+In the future state, to achieve scalable secret detection for a variety of domain objects a dedicated
scanning service must be created and deployed alongside the GitLab distribution.
This is referred to as the `SecretScanningService`.
@@ -94,10 +94,10 @@ as self-managed instances.
The critical paths as outlined under [goals above](#goals) cover two major object
types: Git blobs (corresponding to push events) and arbitrary text blobs.
-The detection flow for push events relies on subscribing to the PostReceive hook
-to enqueue Sidekiq requests to the `SecretScanningService`. The `SecretScanningService`
-service fetches enqueued refs, queries Gitaly for the ref blob contents, scans
-the commit contents, and notifies the Rails application when a secret is detected.
+The detection flow for push events relies on subscribing to the PreReceive hook
+to scan commit data using the [PushCheck interface](https://gitlab.com/gitlab-org/gitlab/blob/3f1653f5706cd0e7bbd60ed7155010c0a32c681d/lib/gitlab/checks/push_check.rb). This `SecretScanningService`
+service fetches the specified blob contents from Gitaly, scans
+the commit contents, and rejects the push when a secret is detected.
See [Push event detection flow](#push-event-detection-flow) for sequence.
The detection flow for arbitrary text blobs, such as issue comments, relies on
@@ -112,13 +112,33 @@ storage. See discussion [in this issue](https://gitlab.com/groups/gitlab-org/-/e
around scanning during streaming and the added complexity in buffering lookbacks
for arbitrary trace chunks.
-In any case of detection, the Rails application manually creates a vulnerability
+In the case of a push detection, the commit is rejected and error returned to the end user.
+In any other case of detection, the Rails application manually creates a vulnerability
using the `Vulnerabilities::ManuallyCreateService` to surface the finding in the
existing Vulnerability Management UI.
See [technical discovery](https://gitlab.com/gitlab-org/gitlab/-/issues/376716)
for further background exploration.
+### Target types
+
+Target object types refer to the scanning targets prioritized for detection of leaked secrets.
+
+In order of priority this includes:
+
+1. non-binary Git blobs
+1. job logs
+1. issuable creation (issues, MRs, epics)
+1. issuable updates (issues, MRs, epics)
+1. issuable comments (issues, MRs, epics)
+
+Targets out of scope for the initial phases include:
+
+- Media types (JPEG, PDF, ...)
+- Snippets
+- Wikis
+- Container images
+
### Token types
The existing Secret Detection configuration covers ~100 rules across a variety
@@ -135,16 +155,17 @@ Token types to identify in order of importance:
### Detection engine
-Our current secret detection offering utilizes [Gitleaks](https://github.com/zricethezav/gitleaks/)
+Our current secret detection offering uses [Gitleaks](https://github.com/zricethezav/gitleaks/)
for all secret scanning in pipeline contexts. By using its `--no-git` configuration
we can scan arbitrary text blobs outside of a repository context and continue to
-utilize it for non-pipeline scanning.
+use it for non-pipeline scanning.
-Given our existing familiarity with the tool and its extensibility, it should
-remain our engine of choice. Changes to the detection engine are out of scope
-unless benchmarking unveils performance concerns.
+In the case of PreReceive detection, we rely on a combination of keyword/substring matches
+for pre-filtering and `re2` for regex detections. See [spike issue](https://gitlab.com/gitlab-org/gitlab/-/issues/423832) for initial benchmarks
-Notable alternatives include high-performance regex engines such as [hyperscan](https://github.com/intel/hyperscan) or it's portable fork [vectorscan](https://github.com/VectorCamp/vectorscan).
+Changes to the detection engine are out of scope until benchmarking unveils performance concerns.
+
+Notable alternatives include high-performance regex engines such as [Hyperscan](https://github.com/intel/hyperscan) or it's portable fork [Vectorscan](https://github.com/VectorCamp/vectorscan).
### High-level architecture
@@ -167,37 +188,42 @@ for past discussion around scaling approaches.
sequenceDiagram
autonumber
actor User
- User->>+Workhorse: git push
+ User->>+Workhorse: git push with-secret
+ Workhorse->>+Gitaly: tcp
+ Gitaly->>+Rails: PreReceive
+ Rails->>-Gitaly: ListAllBlobs
+ Gitaly->>-Rails: ListAllBlobsResponse
+
+ Rails->>+GitLabSecretDetection: Scan(blob)
+ GitLabSecretDetection->>-Rails: found
+
+ Rails->>User: rejected: secret found
+
+ User->>+Workhorse: git push without-secret
Workhorse->>+Gitaly: tcp
- Gitaly->>+Rails: grpc
- Sidekiq->>+Rails: poll job
- Rails->>-Sidekiq: PostReceive worker
- Sidekiq-->>+Sidekiq: enqueue PostReceiveSecretScanWorker
-
- Sidekiq->>+Rails: poll job
- loop PostReceiveSecretScanWorker
- Rails->>-Sidekiq: PostReceiveSecretScanWorker
- Sidekiq->>+SecretScanningSvc: ScanBlob(ref)
- SecretScanningSvc->>+Sidekiq: accepted
- Note right of SecretScanningSvc: Scanning job enqueued
- Sidekiq-->>+Rails: done
- SecretScanningSvc->>+Gitaly: retrieve blob
- SecretScanningSvc->>+SecretScanningSvc: scan blob
- SecretScanningSvc->>+Rails: secret found
- end
+ Gitaly->>+Rails: PreReceive
+ Rails->>-Gitaly: ListAllBlobs
+ Gitaly->>-Rails: ListAllBlobsResponse
+
+ Rails->>+GitLabSecretDetection: Scan(blob)
+ GitLabSecretDetection->>-Rails: not_found
+
+ Rails->>User: OK
```
## Iterations
- ✓ Define [requirements for detection coverage and actions](https://gitlab.com/gitlab-org/gitlab/-/issues/376716)
-- ✓ Implement [Clientside detection of GitLab tokens in comments/issues](https://gitlab.com/gitlab-org/gitlab/-/issues/368434)
-- PoC of secret scanning service
- - Benchmarking of issuables, comments, job logs and blobs to gain confidence that the total costs will be viable
- - Capacity planning for addition of service component to Reference Architectures headroom
- - Service capabilities
+- ✓ Implement [Browser-based detection of GitLab tokens in comments/issues](https://gitlab.com/gitlab-org/gitlab/-/issues/368434)
+- ✓ [PoC of secret scanning service](https://gitlab.com/gitlab-org/secure/pocs/secret-detection-go-poc/)
+- ✓ [PoC of secret scanning gem](https://gitlab.com/gitlab-org/gitlab/-/issues/426823)
+- [Pre-Production Performance Profiling for pre-receive PoCs](https://gitlab.com/gitlab-org/gitlab/-/issues/428499)
+ - Profiling service capabilities
+ - ✓ [Benchmarking regex performance between Ruby and Go approaches](https://gitlab.com/gitlab-org/gitlab/-/issues/423832)
- gRPC commit retrieval from Gitaly
- - blob scanning
+ - transfer latency, CPU, and memory footprint
- Implementation of secret scanning service MVC (targeting individual commits)
+- Capacity planning for addition of service component to Reference Architectures headroom
- Security and readiness review
- Deployment and monitoring
- Implementation of secret scanning service MVC (targeting arbitrary text blobs)
diff --git a/doc/architecture/blueprints/secret_manager/decisions/002_gcp_kms.md b/doc/architecture/blueprints/secret_manager/decisions/002_gcp_kms.md
new file mode 100644
index 00000000000..c750164632f
--- /dev/null
+++ b/doc/architecture/blueprints/secret_manager/decisions/002_gcp_kms.md
@@ -0,0 +1,101 @@
+---
+owning-stage: "~devops::verify"
+description: 'GitLab Secrets Manager ADR 002: Use GCP Key Management Service'
+---
+
+# GitLab Secrets Manager ADR 002: Use GCP Key Management Service
+
+## Context
+
+Following from [ADR 001: Use envelope encryption](001_envelop_encryption.md), we need to find a solution to securely
+store asymmetric keys belonging to each vault.
+
+## Decision
+
+We decided to rely on Google CLoud Platform (GCP) Key Management Service (KMS) to manage the asymmetric keys
+used by the GitLab Secrets Manager vaults.
+
+Using GCP provides a few advantages:
+
+1. Avoid implementing our own secure storage of cryptographic keys.
+1. Support for Hardware Security Modules (HSM).
+
+```mermaid
+sequenceDiagram
+ participant A as Client
+ participant B as GitLab Rails
+ participant C as GitLab Secrets Service
+ participant D as GCP Key Management Service
+
+ Note over B,D: Initialize vault for project/group/organization
+
+ B->>C: Initialize vault - create key pair
+
+ Note over D: Incurs cost per key
+ C->>D: Create new asymmetric key
+ D->>C: Returns public key
+ C->>B: Returns vault public key
+ B->>B: Stores vault public key
+
+ Note over A,C: Creating a new secret
+
+ A->>B: Create new secret
+ B->>B: Generate new symmetric data key
+ B->>B: Encrypts secret with data key
+ B->>B: Encrypts data key with vault public key
+ B->>B: Stores envelope (encrypted secret + encrypted data key)
+ B-->>B: Discards plain-text data key
+ B->>A: Success
+
+ Note over A,D: Retrieving a secret
+
+ A->>B: Get secret
+ B->>B: Retrieves envelope (encrypted secret + encrypted data key)
+ B->>C: Decrypt data key
+ Note over D: Incurs cost per decryption request
+ C->>D: Decrypt data key
+ D->>C: Returns plain-text data key
+ C->>B: Returns plain-text data key
+ B->>B: Decrypts secret
+ B-->>B: Discards plain-text data key
+ B->>A: Returns secret
+```
+
+For security purpose, we decided to use Hardware Security Module (HSM) to protect the keys in GCP KMS.
+
+## Consequences
+
+### Authentication
+
+With keys stored in GCP KMS, we need to de-multiplex between identities configured in GCP KMS and
+identities defined in GitLab so that decryption requests can be authenticated accordingly.
+
+### Cost
+
+With the use of GCP KMS, we need to account for the following cost:
+
+1. Number of keys required
+1. Number of key operations
+1. HSM Protection level
+
+The number of keys required would be dependent on the number of projects, groups, and organizations using this feature.
+A single asymmetric key is required for each project, group or organization.
+
+Each cryptographic key operation would also incur cost and it varies per protection level.
+Based on the proposed design above, this would incur cost at each secret decryption request.
+
+We may implement a multi-tier protection level, supporting different protection types for different users.
+
+The pricing table of GCP KMS can be found [here](https://cloud.google.com/kms/pricing).
+
+### Feature availability for Self-Managed customers
+
+Using GCP KMS as a backend means that this solution cannot be deployed into self-managed environments.
+To make this feature available to Self-Managed customers, this feature needs to be a GitLab Cloud Connector feature.
+
+## Alternatives
+
+We considered generating and storing private keys within GitLab Secrets Service,
+but this would not meet the requirements for [FIPS Compliance](../../../../development/fips_compliance.md).
+
+On the other hand, GCP HSM Keys comply with [FIPS 140-2 Level 3](https://cloud.google.com/docs/security/key-management-deep-dive#fips_140-2_validation).
diff --git a/doc/architecture/blueprints/secret_manager/decisions/003_go_service.md b/doc/architecture/blueprints/secret_manager/decisions/003_go_service.md
new file mode 100644
index 00000000000..561a1bde24e
--- /dev/null
+++ b/doc/architecture/blueprints/secret_manager/decisions/003_go_service.md
@@ -0,0 +1,37 @@
+---
+owning-stage: "~devops::verify"
+description: 'GitLab Secrets Manager ADR 003: Implement Secrets Manager in Go'
+---
+
+# GitLab Secrets Manager ADR 003: Implement Secrets Manager in Go
+
+Following [ADR-002](002_gcp_kms.md) highlighting the need to integrate with GCP
+services, we do need to decide what tech stack is going to be used to build
+GitLab Secrets Manager Service (GSMS).
+
+## Context
+
+At GitLab, we usually build satellite services around GitLab Rails in Go.
+This is especially a good choice of technology for services that may heavily
+leverage concurrency and caching, where cache could be invalidated / refreshed
+asynchronously.
+
+Go-based [GCP KMS](https://cloud.google.com/kms/docs/reference/libraries#client-libraries-usage-go)
+client library also seems to expose a reliable interface to access KMS.
+
+## Decision
+
+Implement GitLab Secrets Manager Service in Go. Use
+[labkit](https://gitlab.com/gitlab-org/labkit) as a minimalist library to
+provide common functionality shared by satellite servicies.
+
+## Consequences
+
+The team that is going to own GitLab Secrets Manager feature will need to gain
+more Go expertise.
+
+## Alternatives
+
+We considered implementing GitLab Secrets Manager Service in Ruby, but we
+concluded that using Ruby will not allow us to build a service that will be
+efficient enough.
diff --git a/doc/architecture/blueprints/secret_manager/decisions/004_staleless_kms.md b/doc/architecture/blueprints/secret_manager/decisions/004_staleless_kms.md
new file mode 100644
index 00000000000..3de8adfd3a7
--- /dev/null
+++ b/doc/architecture/blueprints/secret_manager/decisions/004_staleless_kms.md
@@ -0,0 +1,49 @@
+---
+owning-stage: "~devops::verify"
+description: 'GitLab Secrets Manager ADR 004: Sateless Key Management Service'
+---
+
+# GitLab Secrets Manager ADR 004: Stateless Key Management Service
+
+In [ADR-002](002_gcp_kms.md) we decided that we want to use Google's Cloud Key
+Management Service to store private encryption keys. This will allow us to meet
+various compliance requirements easier.
+
+In this ADR we are going to describe the desired architecture of GitLab Secrets
+Management Service, making it a stateless service, that is not connected to a
+persistent datastore, other than an ephemeral local storage.
+
+## Context
+
+## Decision
+
+Make GitLab Secrets Management Service a stateless application, not being
+connected to a global data storage, like a relational or NoSQL database.
+
+We are only going to support local block storage, presumably only for caching
+purposes.
+
+In order to manage decryption cost wisely, we would need to implement
+multi-tier protection layers, and in-memory, per-instance,
+[symmetric decryption key](001_envelop_encryption.md) caching, with cache TTL
+depending on the protection tier. A hardware or software key can be used in
+Google's Cloud KMS, depending on the tier too.
+
+## Consequences
+
+1. All private keys are going to be stored in Google's Cloud KMS.
+1. Multi-tier protection will be implemented, with higher tries offering more protection.
+1. Protection tier will be defined on per-organization level on the GitLab Rails Service side.
+1. Depending on the protection level used, symmetric decryption keys can be in-memory cached.
+1. The symmetric key's cache must not be valid for more than 24 hours..
+1. The highest protection tier will use Hardware Security Module and no caching.
+1. The GitLab Secrets Management Service will not store access-control metadata.
+1. Identity de-multiplexing will happen on GitLab Rails Service side.
+1. Decryption request will be signed by an organization's public key.
+1. The service will verify decryption requestor's identity by checking the signature.
+
+## Alternatives
+
+We considered using a relational database, or a NoSQL database, both
+self-managed and managed by a Cloud Provider, but concluded that this would add
+a lot of complexity and would weaken the security posture of the service.
diff --git a/doc/architecture/blueprints/secret_manager/index.md b/doc/architecture/blueprints/secret_manager/index.md
index 2a840f8d846..ac30f3399d8 100644
--- a/doc/architecture/blueprints/secret_manager/index.md
+++ b/doc/architecture/blueprints/secret_manager/index.md
@@ -59,12 +59,18 @@ This blueprint does not cover the following:
- Secrets such as access tokens created within GitLab to allow external resources to access GitLab, e.g personal access tokens.
+## Decisions
+
+- [ADR-001: Use envelope encryption](decisions/001_envelop_encryption.md)
+- [ADR-002: Use GCP Key Management Service](decisions/002_gcp_kms.md)
+- [ADR-003: Build Secrets Manager in Go](decisions/003_go_service.md)
+
## Proposal
The secrets manager feature will consist of three core components:
1. GitLab Rails
-1. GitLab Secrets Service
+1. GitLab Secrets Manager Service
1. GCP Key Management
At a high level, secrets will be stored using unique encryption keys in order to achieve isolation
@@ -86,13 +92,15 @@ The plain-text secret would be encrypted using a single use data key.
The data key is then encrypted using the public key belonging to the group or project.
Both, the encrypted secret and the encrypted data key, are being stored in the database.
-**2. GitLab Secrets Manager**
+**2. GitLab Secrets Manager Service**
-GitLab Secrets Manager will be a new component in the GitLab overall architecture. This component serves the following purpose:
+GitLab Secrets Manager Service will be a new component in the GitLab overall architecture. This component serves the following purpose:
1. Correlating GitLab identities into GCP identities for access control.
1. A proxy over GCP Key Management for decrypting operations.
+[The service will use Go-based tech stack](decisions/003_go_service.md) and [labkit](https://gitlab.com/gitlab-org/labkit).
+
**3. GCP Key Management**
We choose to leverage GCP Key Management to build on the security and trust that GCP provides on cryptographic operations.
@@ -120,10 +128,6 @@ Hence, GCP Key Management is the natural choice for a cloud-based key management
To extend this service to self-managed GitLab instances, we would consider using GitLab Cloud Connector as a proxy between
self-managed GitLab instances and the GitLab Secrets Manager.
-## Decision Records
-
-- [001: Use envelope encryption](decisions/001_envelop_encryption.md)
-
## Alternative Solutions
Other solutions we have explored:
diff --git a/doc/architecture/blueprints/work_items/index.md b/doc/architecture/blueprints/work_items/index.md
index e12bb4d8773..74690d34088 100644
--- a/doc/architecture/blueprints/work_items/index.md
+++ b/doc/architecture/blueprints/work_items/index.md
@@ -64,7 +64,7 @@ You can also refer to fields of [Work Item](../../../api/graphql/reference/index
All Work Item types share the same pool of predefined widgets and are customized by which widgets are active on a specific type. The list of widgets for any certain Work Item type is currently predefined and is not customizable. However, in the future we plan to allow users to create new Work Item types and define a set of widgets for them.
-### Work Item widget types (updating)
+### Widget types (updating)
| Widget | Description | Feature flag | Write permission | GraphQL Subscription Support |
|---|---|---|---|---|
@@ -86,6 +86,36 @@ All Work Item types share the same pool of predefined widgets and are customized
| [WorkItemWidgetTestReports](../../../api/graphql/reference/index.md#workitemwidgettestreports) | Test reports associated with a work item | | | |
| [WorkItemWidgetWeight](../../../api/graphql/reference/index.md#workitemwidgetweight) | Set weight of a work item | |`Reporter`|No|
+#### Widget availability (updating)
+
+| Widget | Epic | Issue | Task | Objective | Key Result |
+|---|---|---|---|---|---|
+| [WorkItemWidgetAssignees](../../../api/graphql/reference/index.md#workitemwidgetassignees) | ✅ | ✅ | ✅ | ✅ | ✅ |
+| [WorkItemWidgetAwardEmoji](../../../api/graphql/reference/index.md#workitemwidgetawardemoji) | ✅ | ✔️ | ✅ | ✅ | ✅ |
+| [WorkItemWidgetCurrentUserTodos](../../../api/graphql/reference/index.md#workitemwidgetcurrentusertodos) | ✅ | ✅ | ✅ | ✅ | ✅ |
+| [WorkItemWidgetDescription](../../../api/graphql/reference/index.md#workitemwidgetdescription) | ✅ | ✅ | ✅ | ✅ | ✅ |
+| [WorkItemWidgetHealthStatus](../../../api/graphql/reference/index.md#workitemwidgethealthstatus) | ✅ | ✅ | ✅ | ✅ | ✅ |
+| [WorkItemWidgetHierarchy](../../../api/graphql/reference/index.md#workitemwidgethierarchy) | ✔ | ✔️ | ❌ | ✅ | ❌ |
+| [WorkItemWidgetIteration](../../../api/graphql/reference/index.md#workitemwidgetiteration) | ❌ | ✅ | ✅ | ❌ | ❌ |
+| [WorkItemWidgetLabels](../../../api/graphql/reference/index.md#workitemwidgetlabels) | ✅ | ✅ | ✅ | ✅ | ✅ |
+| [WorkItemWidgetLinkedItems](../../../api/graphql/reference/index.md#workitemwidgetlinkeditems) | ✔️ | ✔️ | ✔️ | ✅ | ✅ |
+| [WorkItemWidgetMilestone](../../../api/graphql/reference/index.md#workitemwidgetmilestone) | 🔍 | ✅ | ✅ | ✅ | ❌ |
+| [WorkItemWidgetNotes](../../../api/graphql/reference/index.md#workitemwidgetnotes) | ✅ | ✅ | ✅ | ✅ | ✅ |
+| [WorkItemWidgetNotifications](../../../api/graphql/reference/index.md#workitemwidgetnotifications) | ✅ | ✅ | ✅ | ✅ | ✅ |
+| [WorkItemWidgetProgress](../../../api/graphql/reference/index.md#workitemwidgetprogress) | ❌ | ❌ | ❌ | ✅ | ✅ |
+| [WorkItemWidgetStartAndDueDate](../../../api/graphql/reference/index.md#workitemwidgetstartandduedate) | 🔍 | ✅ | ✅ | ❌ | ✅ |
+| [WorkItemWidgetStatus](../../../api/graphql/reference/index.md#workitemwidgetstatus) | ❓ | ❓ | ❓ | ❓ | ❓ |
+| [WorkItemWidgetTestReports](../../../api/graphql/reference/index.md#workitemwidgettestreports) | ❌ | ❌ | ❌ | ❌ | ❌ |
+| [WorkItemWidgetWeight](../../../api/graphql/reference/index.md#workitemwidgetweight) | 🔍 | ✅ | ✅ | ❌ | ❌ |
+
+##### Legend
+
+- ✅ - Widget available
+- ✔️ - Widget planned to be available
+- ❌ - Widget not available
+- ❓ - Widget pending for consideration
+- 🔍 - Alternative widget planned
+
### Work item relationships
Work items can be related to other work items in a number of different ways:
diff --git a/doc/ci/chatops/index.md b/doc/ci/chatops/index.md
index 10276df6291..454266942f6 100644
--- a/doc/ci/chatops/index.md
+++ b/doc/ci/chatops/index.md
@@ -14,10 +14,20 @@ type: index, concepts, howto
Use GitLab ChatOps to interact with CI/CD jobs through chat services
like Slack.
-Many organizations use chat services to collaborate, troubleshoot, and plan work. With ChatOps,
+Many organizations use Slack or Mattermost to collaborate, troubleshoot, and plan work. With ChatOps,
you can discuss work with your team, run CI/CD jobs, and view job output, all from the same
application.
+## Slash command integrations
+
+You can trigger ChatOps with the [`run` slash command](../../user/project/integrations/gitlab_slack_application.md#slash-commands).
+
+The following integrations are available:
+
+- [GitLab for Slack app](../../user/project/integrations/gitlab_slack_application.md) (recommended for Slack)
+- [Slack slash commands](../../user/project/integrations/slack_slash_commands.md)
+- [Mattermost slash commands](../../user/project/integrations/mattermost_slash_commands.md)
+
## ChatOps workflow and CI/CD configuration
ChatOps looks for the specified job in the
@@ -37,7 +47,7 @@ run as part of the standard CI/CD pipeline.
ChatOps passes the following [CI/CD variables](../variables/index.md#predefined-cicd-variables)
to the job:
-- `CHAT_INPUT` - The arguments passed to `/project-name run`.
+- `CHAT_INPUT` - The arguments passed to the `run` slash command.
- `CHAT_CHANNEL` - The name of the chat channel the job is run from.
- `CHAT_USER_ID` - The chat service ID of the user who runs the job.
@@ -47,30 +57,13 @@ When the job runs:
- If the job completes in more than 30 minutes, you must use a method like the
[Slack API](https://api.slack.com/) to send data to the channel.
-## Run a CI/CD job
-
-Prerequisite:
-
-- You must have at least the Developer role for the project.
-
-You can run a CI/CD job on the default branch from chat. To run a CI/CD job:
-
-- In the chat client, enter `/<project-name> run <job name> <arguments>` where:
-
- - `<project-name>` is the name of the project.
- - `<job name>` is the name of the CI/CD job to run.
- - `<arguments>` is the arguments to pass to the CI/CD job.
-
-ChatOps schedules a pipeline that contains only the specified job.
-Other [slash commands](../../user/project/integrations/gitlab_slack_application.md#slash-commands) are also available.
-
### Exclude a job from ChatOps
To prevent a job from being run from chat:
- In `.gitlab-ci.yml`, set the job to `except: [chat]`.
-## Customize the ChatOps reply
+### Customize the ChatOps reply
ChatOps sends the output for a job with a single command to the
channel as a reply. For example, when the following job runs,
@@ -108,8 +101,34 @@ ls:
- echo -e "section_start:$( date +%s ):chat_reply\r\033[0K\n$( ls -la )\nsection_end:$( date +%s ):chat_reply\r\033[0K"
```
+## Trigger a CI/CD job using ChatOps
+
+Prerequisite:
+
+- You must have at least the Developer role for the project.
+- The project is configured to use a slash command integration.
+
+You can run a CI/CD job on the default branch from Slack or Mattermost.
+
+The slash command to trigger a CI/CD job depends on which slash command integration
+is configured for the project.
+
+- For the GitLab for Slack app, use `/gitlab <project-name> run <job name> <arguments>`.
+- For Slack or Mattermost slash commands, use `/<trigger-name> run <job name> <arguments>`.
+
+Where:
+
+- `<job name>` is the name of the CI/CD job to run.
+- `<arguments>` are the arguments to pass to the CI/CD job.
+- `<trigger-name>` is the trigger name configured for the Slack or Mattermost integration.
+
+ChatOps schedules a pipeline that contains only the specified job.
+
## Related topics
-- [The official GitLab ChatOps icon](img/gitlab-chatops-icon.png)
- [A repository of common ChatOps scripts](https://gitlab.com/gitlab-com/chatops)
that GitLab uses to interact with GitLab.com
+- [GitLab for Slack app](../../user/project/integrations/gitlab_slack_application.md)
+- [Slack slash commands](../../user/project/integrations/slack_slash_commands.md)
+- [Mattermost slash commands](../../user/project/integrations/mattermost_slash_commands.md)
+- [The official GitLab ChatOps icon](img/gitlab-chatops-icon.png)
diff --git a/doc/ci/cloud_services/azure/index.md b/doc/ci/cloud_services/azure/index.md
index 3a882cf6820..b26533562f4 100644
--- a/doc/ci/cloud_services/azure/index.md
+++ b/doc/ci/cloud_services/azure/index.md
@@ -25,6 +25,7 @@ Prerequisites:
- Access to the corresponding Azure Active Directory Tenant with at least the `Application Developer` access level.
- A local installation of the [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli).
Alternatively, you can follow all the steps below with the [Azure Cloud Shell](https://portal.azure.com/#cloudshell/).
+- Your GitLab instance must be publicly accessible over the internet as Azure must to connect to the GitLab OIDC endpoint.
- A GitLab project.
To complete this tutorial:
@@ -167,3 +168,23 @@ CI/CD variables, from the Azure Portal:
Azure AD federated identity credentials.
Review [Connect to cloud services](../index.md) for further details.
+
+### `Request to External OIDC endpoint failed` message
+
+If you receive the error `ERROR: AADSTS501661: Request to External OIDC endpoint failed.`
+you should verify that your GitLab instance is publicly accessible from the internet.
+
+Azure must be able to access the following GitLab endpoints to authenticate with OIDC:
+
+- `GET /.well-known/openid-configuration`
+- `GET /oauth/discovery/keys`
+
+If you update your firewall and still receive this error, [clear the Redis cache](../../../administration/raketasks/maintenance.md#clear-redis-cache)
+and try again.
+
+### `No matching federated identity record found for presented assertion audience` message
+
+If you receive the error `ERROR: AADSTS700212: No matching federated identity record found for presented assertion audience 'https://gitlab.com'`
+you should verify that your CI/CD job uses the correct `aud` value.
+
+The `aud` value should match the audience used to [create the federated identity credentials](#create-azure-ad-federated-identity-credentials).
diff --git a/doc/ci/cloud_services/google_cloud/index.md b/doc/ci/cloud_services/google_cloud/index.md
index a733f3d59cb..fd8aca7045c 100644
--- a/doc/ci/cloud_services/google_cloud/index.md
+++ b/doc/ci/cloud_services/google_cloud/index.md
@@ -22,6 +22,10 @@ This tutorial assumes you have a Google Cloud account and a Google Cloud project
Your account must have at least the **Workload Identity Pool Admin** permission
on the Google Cloud project.
+NOTE:
+If you would prefer to use a Terraform module and a CI/CD template instead of this tutorial,
+see [How OIDC can simplify authentication of GitLab CI/CD pipelines with Google Cloud](https://about.gitlab.com/blog/2023/06/28/introduction-of-oidc-modules-for-integration-between-google-cloud-and-gitlab-ci/).
+
To complete this tutorial:
1. [Create the Google Cloud Workload Identity Pool](#create-the-google-cloud-workload-identity-pool).
diff --git a/doc/ci/components/index.md b/doc/ci/components/index.md
index a3d6d7224e4..3d46ec5bbd5 100644
--- a/doc/ci/components/index.md
+++ b/doc/ci/components/index.md
@@ -4,14 +4,12 @@ group: Pipeline Authoring
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
---
-# CI/CD components **(FREE ALL EXPERIMENT)**
+# CI/CD components **(FREE ALL BETA)**
> - Introduced as an [experimental feature](../../policy/experiment-beta-support.md) in GitLab 16.0, [with a flag](../../administration/feature_flags.md) named `ci_namespace_catalog_experimental`. Disabled by default.
> - [Enabled on GitLab.com and self-managed](https://gitlab.com/groups/gitlab-org/-/epics/9897) in GitLab 16.2.
> - [Feature flag `ci_namespace_catalog_experimental` removed](https://gitlab.com/gitlab-org/gitlab/-/issues/394772) in GitLab 16.3.
-
-This feature is an experimental feature and [an epic exists](https://gitlab.com/groups/gitlab-org/-/epics/9897)
-to track future work. Tell us about your use case by leaving comments in the epic.
+> - [Moved](https://gitlab.com/gitlab-com/www-gitlab-com/-/merge_requests/130824) to [Beta status](../../policy/experiment-beta-support.md) in GitLab 16.6.
A CI/CD component is a reusable single pipeline configuration unit. Use them to compose an entire pipeline configuration or a small part of a larger pipeline.
@@ -29,6 +27,8 @@ A components repository is a GitLab project with a repository that hosts one or
If a component requires different versioning from other components, the component should be migrated to its own components repository.
+One component repository can have a maximum of 10 components.
+
## Create a components repository
To create a components repository, you must:
@@ -65,17 +65,17 @@ the file structure should be similar to:
```plaintext
├── templates/
-│ └── only_template.yml
+│ └── secret-detection.yml
├── README.md
└── .gitlab-ci.yml
```
-This example component could be referenced with a path similar to `gitlab.com/my-username/my-component/only_template@<version>`,
+This example component could be referenced with a path similar to `gitlab.com/my-namespace/my-project/secret-detection@<version>`,
if the project is:
- On GitLab.com
-- Named `my-component`
-- In a personal namespace named `my-username`
+- Named `my-project`
+- In a personal namespace or group named `my-namespace`
The templates directory and the suffix of the configuration file should be excluded from the referenced path.
@@ -85,26 +85,32 @@ If the project contains multiple components, then the file structure should be s
├── README.md
├── .gitlab-ci.yml
└── templates/
- └── all-scans.yml
+ ├── all-scans.yml
└── secret-detection.yml
```
These components would be referenced with these paths:
-- `gitlab.com/my-username/my-component/all-scans`
-- `gitlab.com/my-username/my-component/secret-detection`
+- `gitlab.com/my-namespace/my-project/all-scans@<version>`
+- `gitlab.com/my-namespace/my-project/secret-detection@<version>`
+
+You can also have components defined as a directory if you want to bundle together multiple related files.
+In this case GitLab expects a `template.yml` file to be present:
-You can omit the filename in the path if the configuration file is named `template.yml`.
-For example, the following component could be referenced with `gitlab.com/my-username/my-component/dast`:
+For example:
```plaintext
├── README.md
├── .gitlab-ci.yml
-├── templates/
-│ └── dast
-│ └── template.yml
+└── templates/
+ └── dast
+ ├── docs.md
+ ├── Dockerfile
+ └── template.yml
```
+In this example, the component could be referenced with `gitlab.com/my-namespace/my-project/dast@<version>`.
+
#### Component configurations saved in any directory (deprecated)
WARNING:
@@ -117,8 +123,8 @@ Components configurations can be saved through the following directory structure
components, each file must be in a separate subdirectory.
- `README.md`: A documentation file explaining the details of all the components in the repository.
-For example, if the project is on GitLab.com, named `my-component`, and in a personal
-namespace named `my-username`:
+For example, if the project is on GitLab.com, named `my-project`, and in a personal
+namespace named `my-namespace`:
- Containing a single component and a simple pipeline to test the component, then
the file structure might be:
@@ -132,7 +138,7 @@ namespace named `my-username`:
The `.gitlab-ci.yml` file is not required for a CI/CD component to work, but
[testing the component](#test-the-component) in a pipeline in the project is recommended.
- This component is referenced with the path `gitlab.com/my-username/my-component@<version>`.
+ This component is referenced with the path `gitlab.com/my-namespace/my-project@<version>`.
- Containing one default component and multiple sub-components, then the file structure
might be:
@@ -149,9 +155,9 @@ namespace named `my-username`:
These components are identified by these paths:
- - `gitlab.com/my-username/my-component`
- - `gitlab.com/my-username/my-component/unit`
- - `gitlab.com/my-username/my-component/integration`
+ - `gitlab.com/my-namespace/my-project`
+ - `gitlab.com/my-namespace/my-project/unit`
+ - `gitlab.com/my-namespace/my-project/integration`
It is possible to have a components repository with no default component, by having
no `template.yml` in the root directory.
@@ -169,19 +175,41 @@ Nesting of components is not possible. For example:
## Release a component
-To create a release for a CI/CD component, use either:
+To create a release for a CI/CD component, use the [`release`](../yaml/index.md#release)
+keyword in a CI/CD pipeline.
+
+For example:
+
+```yaml
+create-release:
+ stage: deploy
+ image: registry.gitlab.com/gitlab-org/release-cli:latest
+ rules:
+ - if: $CI_COMMIT_TAG =~ /^v\d+/
+ script: echo "Creating release $CI_COMMIT_TAG"
+ release:
+ tag_name: $CI_COMMIT_TAG
+ description: "Release $CI_COMMIT_TAG of components repository $CI_PROJECT_PATH"
+```
+
+In this example, the job runs only for tags formatted as `v` + version number.
+If all previous jobs succeed, the release is created.
-- The [`release`](../yaml/index.md#release) keyword in a CI/CD pipeline. Like in the
- [component testing example](#test-the-component), you can set a component to automatically
- be released after all tests pass in pipelines for new tags.
-- The [UI for creating a release](../../user/project/releases/index.md#create-a-release).
+Like in the [component testing example](#test-the-component), you can set a component to automatically
+be released after all tests pass in pipelines for new tags.
-All released versions of the components are displayed in the [CI/CD Catalog](catalog.md)
-page for the given resource, providing users with information about official releases.
+All released versions of the components repositories are displayed in the [CI/CD Catalog](catalog.md),
+providing users with information about official releases.
Components [can be used](#use-a-component-in-a-cicd-configuration) without being released,
-but only with a commit SHA or a branch name. To enable the use of tags or the `~latest` version keyword,
-you must create a release.
+by using the commit SHA or ref. However, the `~latest` version keyword can only be used with released tags.
+
+NOTE:
+The `~latest` keyword always returns the most recent release, not the release with
+the latest semantic version. For example, if you first release `v2.0.0`, and later release
+a patch fix like `v1.5.1`, then `~latest` returns the `v1.5.1` release.
+[Issue #427286](https://gitlab.com/gitlab-org/gitlab/-/issues/427286) proposes to
+change this behavior.
## Use a component in a CI/CD configuration
@@ -190,7 +218,7 @@ For example:
```yaml
include:
- - component: gitlab.example.com/my-namespace/my-component@1.0
+ - component: gitlab.example.com/my-namespace/my-project@1.0
inputs:
stage: build
```
@@ -395,7 +423,7 @@ For example:
```yaml
include:
# include the component located in the current project from the current SHA
- - component: gitlab.com/$CI_PROJECT_PATH@$CI_COMMIT_SHA
+ - component: gitlab.com/$CI_PROJECT_PATH/my-project@$CI_COMMIT_SHA
inputs:
stage: build
diff --git a/doc/ci/debugging.md b/doc/ci/debugging.md
new file mode 100644
index 00000000000..5bcf834b61d
--- /dev/null
+++ b/doc/ci/debugging.md
@@ -0,0 +1,295 @@
+---
+stage: Verify
+group: Pipeline Authoring
+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: reference
+---
+
+# Debugging CI/CD pipelines **(FREE ALL)**
+
+GitLab provides several tools to help make it easier to debug your CI/CD configuration.
+
+If you are unable to resolve pipeline issues, you can get help from:
+
+- The [GitLab community forum](https://forum.gitlab.com/)
+- GitLab [Support](https://about.gitlab.com/support/)
+
+## Verify syntax
+
+An early source of problems can be incorrect syntax. The pipeline shows a `yaml invalid`
+badge and does not start running if any syntax or formatting problems are found.
+
+### Edit `.gitlab-ci.yml` with the pipeline editor
+
+The [pipeline editor](pipeline_editor/index.md) is the recommended editing
+experience (rather than the single file editor or the Web IDE). It includes:
+
+- Code completion suggestions that ensure you are only using accepted keywords.
+- Automatic syntax highlighting and validation.
+- The [CI/CD configuration visualization](pipeline_editor/index.md#visualize-ci-configuration),
+ a graphical representation of your `.gitlab-ci.yml` file.
+
+### Edit `.gitlab-ci.yml` locally
+
+If you prefer to edit your pipeline configuration locally, you can use the
+GitLab CI/CD schema in your editor to verify basic syntax issues. Any
+[editor with Schemastore support](https://www.schemastore.org/json/#editors) uses
+the GitLab CI/CD schema by default.
+
+If you need to link to the schema directly, use this URL:
+
+```plaintext
+https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/assets/javascripts/editor/schema/ci.json
+```
+
+To see the full list of custom tags covered by the CI/CD schema, check the
+latest version of the schema.
+
+### Verify syntax with CI Lint tool
+
+You can use the [CI Lint tool](lint.md) to verify that the syntax of a CI/CD configuration
+snippet is correct. Paste in full `.gitlab-ci.yml` files or individual job configurations,
+to verify the basic syntax.
+
+When a `.gitlab-ci.yml` file is present in a project, you can also use the CI Lint
+tool to [simulate the creation of a full pipeline](lint.md#simulate-a-pipeline).
+It does deeper verification of the configuration syntax.
+
+## Verify variables
+
+A key part of troubleshooting CI/CD is to verify which variables are present in a
+pipeline, and what their values are. A lot of pipeline configuration is dependent
+on variables, and verifying them is one of the fastest ways to find the source of
+a problem.
+
+[Export the full list of variables](variables/index.md#list-all-variables)
+available in each problematic job. Check if the variables you expect are present,
+and check if their values are what you expect.
+
+## Job configuration issues
+
+A lot of common pipeline issues can be fixed by analyzing the behavior of the `rules`
+or `only/except` configuration used to [control when jobs are added to a pipeline](jobs/job_control.md).
+You shouldn't use these two configurations in the same pipeline, as they behave differently.
+It's hard to predict how a pipeline runs with this mixed behavior. `rules` is the preferred
+choice for controlling jobs, as `only` and `except` are no longer being actively developed.
+
+If your `rules` or `only/except` configuration makes use of [predefined variables](variables/predefined_variables.md)
+like `CI_PIPELINE_SOURCE`, `CI_MERGE_REQUEST_ID`, you should [verify them](#verify-variables)
+as the first troubleshooting step.
+
+### Jobs or pipelines don't run when expected
+
+The `rules` or `only/except` keywords are what determine whether or not a job is
+added to a pipeline. If a pipeline runs, but a job is not added to the pipeline,
+it's usually due to `rules` or `only/except` configuration issues.
+
+If a pipeline does not seem to run at all, with no error message, it may also be
+due to `rules` or `only/except` configuration, or the `workflow: rules` keyword.
+
+If you are converting from `only/except` to the `rules` keyword, you should check
+the [`rules` configuration details](yaml/index.md#rules) carefully. The behavior
+of `only/except` and `rules` is different and can cause unexpected behavior when migrating
+between the two.
+
+The [common `if` clauses for `rules`](jobs/job_control.md#common-if-clauses-for-rules)
+can be very helpful for examples of how to write rules that behave the way you expect.
+
+### A job with the `changes` keyword runs unexpectedly
+
+A common reason a job is added to a pipeline unexpectedly is because the `changes`
+keyword always evaluates to true in certain cases. For example, `changes` is always
+true in certain pipeline types, including scheduled pipelines and pipelines for tags.
+
+The `changes` keyword is used in combination with [`only/except`](yaml/index.md#onlychanges--exceptchanges)
+or [`rules`](yaml/index.md#ruleschanges). It's recommended to only use `changes` with
+`if` sections in `rules` or `only/except` configuration that ensures the job is only added to
+branch pipelines or merge request pipelines.
+
+### Two pipelines run at the same time
+
+Two pipelines can run when pushing a commit to a branch that has an open merge request
+associated with it. Usually one pipeline is a merge request pipeline, and the other
+is a branch pipeline.
+
+This situation is usually caused by the `rules` configuration, and there are several ways to
+[prevent duplicate pipelines](jobs/job_control.md#avoid-duplicate-pipelines).
+
+### No pipeline or the wrong type of pipeline runs
+
+Before a pipeline can run, GitLab evaluates all the jobs in the configuration and tries
+to add them to all available pipeline types. A pipeline does not run if no jobs are added
+to it at the end of the evaluation.
+
+If a pipeline did not run, it's likely that all the jobs had `rules` or `only/except` that
+blocked them from being added to the pipeline.
+
+If the wrong pipeline type ran, then the `rules` or `only/except` configuration should
+be checked to make sure the jobs are added to the correct pipeline type. For
+example, if a merge request pipeline did not run, the jobs may have been added to
+a branch pipeline instead.
+
+It's also possible that your [`workflow: rules`](yaml/index.md#workflow) configuration
+blocked the pipeline, or allowed the wrong pipeline type.
+
+### Pipeline with many jobs fails to start
+
+A Pipeline that has more jobs than the instance's defined [CI/CD limits](../administration/settings/continuous_integration.md#set-cicd-limits)
+fails to start.
+
+To reduce the number of jobs in a single pipeline, you can split your `.gitlab-ci.yml`
+configuration into more independent [parent-child pipelines](../ci/pipelines/pipeline_architectures.md#parent-child-pipelines).
+
+## Pipeline warnings
+
+Pipeline configuration warnings are shown when you:
+
+- [Validate configuration with the CI Lint tool](yaml/index.md).
+- [Manually run a pipeline](pipelines/index.md#run-a-pipeline-manually).
+
+### `Job may allow multiple pipelines to run for a single action` warning
+
+When you use [`rules`](yaml/index.md#rules) with a `when` clause without an `if`
+clause, multiple pipelines may run. Usually this occurs when you push a commit to
+a branch that has an open merge request associated with it.
+
+To [prevent duplicate pipelines](jobs/job_control.md#avoid-duplicate-pipelines), use
+[`workflow: rules`](yaml/index.md#workflow) or rewrite your rules to control
+which pipelines can run.
+
+## Troubleshooting
+
+For help with a specific area, see:
+
+- [Caching](caching/index.md#troubleshooting).
+- [CI/CD job tokens](jobs/ci_job_token.md).
+- [Container Registry](../user/packages/container_registry/troubleshoot_container_registry.md).
+- [Docker](docker/using_docker_build.md#troubleshooting).
+- [Downstream pipelines](pipelines/downstream_pipelines.md#troubleshooting).
+- [Environments](environments/deployment_safety.md#ensure-only-one-deployment-job-runs-at-a-time).
+- [GitLab Runner](https://docs.gitlab.com/runner/faq/).
+- [ID tokens](secrets/id_token_authentication.md#troubleshooting).
+- [Jobs](jobs/index.md#troubleshooting).
+- [Job control](jobs/job_control.md).
+- [Job artifacts](jobs/job_artifacts_troubleshooting.md).
+- [Merge request pipelines](pipelines/merge_request_pipelines.md#troubleshooting),
+ [merged results pipelines](pipelines/merged_results_pipelines.md#troubleshooting),
+ and [Merge trains](pipelines/merge_trains.md#troubleshooting).
+- [Pipeline editor](pipeline_editor/index.md#troubleshooting).
+- [Variables](variables/index.md#troubleshooting).
+- [YAML `includes` keyword](yaml/includes.md#troubleshooting).
+- [YAML `script` keyword](yaml/script.md#troubleshooting).
+
+Otherwise, review the following troubleshooting sections for known status messages
+and error messages.
+
+### `A CI/CD pipeline must run and be successful before merge` message
+
+This message is shown if the [**Pipelines must succeed**](../user/project/merge_requests/merge_when_pipeline_succeeds.md#require-a-successful-pipeline-for-merge)
+setting is enabled in the project and a pipeline has not yet run successfully.
+This also applies if the pipeline has not been created yet, or if you are waiting
+for an external CI service.
+
+If you don't use pipelines for your project, then you should disable **Pipelines must succeed**
+so you can accept merge requests.
+
+### `Checking ability to merge automatically` message
+
+If your merge request is stuck with a `Checking ability to merge automatically`
+message that does not disappear after a few minutes, you can try one of these workarounds:
+
+- Refresh the merge request page.
+- Close & Re-open the merge request.
+- Rebase the merge request with the `/rebase` [quick action](../user/project/quick_actions.md).
+- If you have already confirmed the merge request is ready to be merged, you can merge
+ it with the `/merge` quick action.
+
+This issue is [resolved](https://gitlab.com/gitlab-org/gitlab/-/issues/229352) in GitLab 15.5.
+
+### `Checking pipeline status` message
+
+This message displays when the merge request does not yet have a pipeline associated with the
+latest commit. This might be because:
+
+- GitLab hasn't finished creating the pipeline yet.
+- You are using an external CI service and GitLab hasn't heard back from the service yet.
+- You are not using CI/CD pipelines in your project.
+- You are using CI/CD pipelines in your project, but your configuration prevented a pipeline from running on the source branch for your merge request.
+- The latest pipeline was deleted (this is a [known issue](https://gitlab.com/gitlab-org/gitlab/-/issues/214323)).
+- The source branch of the merge request is on a private fork.
+
+After the pipeline is created, the message updates with the pipeline status.
+
+### `Project <group/project> not found or access denied` message
+
+This message is shown if configuration is added with [`include`](yaml/index.md#include) and either:
+
+- The configuration refers to a project that can't be found.
+- The user that is running the pipeline is unable to access any included projects.
+
+To resolve this, check that:
+
+- The path of the project is in the format `my-group/my-project` and does not include
+ any folders in the repository.
+- The user running the pipeline is a [member of the projects](../user/project/members/index.md#add-users-to-a-project)
+ that contain the included files. Users must also have the [permission](../user/permissions.md#job-permissions)
+ to run CI/CD jobs in the same projects.
+
+### `The parsed YAML is too big` message
+
+This message displays when the YAML configuration is too large or nested too deeply.
+YAML files with a large number of includes, and thousands of lines overall, are
+more likely to hit this memory limit. For example, a YAML file that is 200 kb is
+likely to hit the default memory limit.
+
+To reduce the configuration size, you can:
+
+- Check the length of the expanded CI/CD configuration in the pipeline editor's
+ [Full configuration](pipeline_editor/index.md#view-full-configuration) tab. Look for
+ duplicated configuration that can be removed or simplified.
+- Move long or repeated `script` sections into standalone scripts in the project.
+- Use [parent and child pipelines](pipelines/downstream_pipelines.md#parent-child-pipelines) to move some
+ work to jobs in an independent child pipeline.
+
+On a self-managed instance, you can [increase the size limits](../administration/instance_limits.md#maximum-size-and-depth-of-cicd-configuration-yaml-files).
+
+### `500` error when editing the `.gitlab-ci.yml` file
+
+A [loop of included configuration files](pipeline_editor/index.md#configuration-validation-currently-not-available-message)
+can cause a `500` error when editing the `.gitlab-ci.yml` file with the [web editor](../user/project/repository/web_editor.md).
+
+Ensure that included configuration files do not create a loop of references to each other.
+
+### `Failed to pull image` messages
+
+> **Allow access to this project with a CI_JOB_TOKEN** setting [renamed to **Limit access _to_ this project**](https://gitlab.com/gitlab-org/gitlab/-/issues/411406) in GitLab 16.3.
+
+A runner might return a `Failed to pull image` message when trying to pull a container image
+in a CI/CD job.
+
+The runner authenticates with a [CI/CD job token](jobs/ci_job_token.md)
+when fetching a container image defined with [`image`](yaml/index.md#image)
+from another project's container registry.
+
+If the job token settings prevent access to the other project's container registry,
+the runner returns an error message.
+
+For example:
+
+- ```plaintext
+ WARNING: Failed to pull image with policy "always": Error response from daemon: pull access denied for registry.example.com/path/to/project, repository does not exist or may require 'docker login': denied: requested access to the resource is denied
+ ```
+
+- ```plaintext
+ WARNING: Failed to pull image with policy "": image pull failed: rpc error: code = Unknown desc = failed to pull and unpack image "registry.example.com/path/to/project/image:v1.2.3": failed to resolve reference "registry.example.com/path/to/project/image:v1.2.3": pull access denied, repository does not exist or may require authorization: server message: insufficient_scope: authorization failed
+ ```
+
+These errors can happen if the following are both true:
+
+- The [**Limit access _to_ this project**](jobs/ci_job_token.md#limit-job-token-scope-for-public-or-internal-projects)
+ option is enabled in the private project hosting the image.
+- The job attempting to fetch the image is running in a project that is not listed in
+ the private project's allowlist.
+
+To resolve this issue, add any projects with CI/CD jobs that fetch images from the container
+registry to the target project's [job token allowlist](jobs/ci_job_token.md#allow-access-to-your-project-with-a-job-token).
diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md
index 269ce2c3212..2505089e4be 100644
--- a/doc/ci/docker/using_docker_build.md
+++ b/doc/ci/docker/using_docker_build.md
@@ -390,9 +390,7 @@ sudo gitlab-runner register -n \
--docker-volumes /var/run/docker.sock:/var/run/docker.sock
```
-To use more complex Docker-in-Docker configurations, such as is necessary to run Code Quality checks
-with Code Climate, you need to ensure that the paths to the build directory are the same on the host
-as well as inside the Docker container. For more details, see
+For complex Docker-in-Docker setups like Code Quality checks using Code Climate, you must match host and container paths for proper execution. For more details, see
[Improve Code Quality performance with private runners](../testing/code_quality.md#improve-code-quality-performance-with-private-runners).
#### Enable registry mirror for `docker:dind` service
diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md
index 455731f6c65..dd6cd2099a9 100644
--- a/doc/ci/docker/using_docker_images.md
+++ b/doc/ci/docker/using_docker_images.md
@@ -471,3 +471,78 @@ REPOSITORY TAG DIGE
gitlab/gitlab-ee latest sha256:723aa6edd8f122d50cae490b1743a616d54d4a910db892314d68470cc39dfb24 (...)
gitlab/gitlab-runner latest sha256:4a18a80f5be5df44cb7575f6b89d1fdda343297c6fd666c015c0e778b276e726 (...)
```
+
+## Creating a Custom GitLab Runner Docker Image
+
+You can create a custom GitLab Runner Docker image to package AWS CLI and Amazon ECR Credential Helper. This setup facilitates
+secure and streamlined interactions with AWS services, especially for containerized applications. For example, to reduce time
+and error-prone manual configurations, teams who deploy microservices on AWS can use this setup to manage, deploy,
+and update Docker images on Amazon ECR, without using manual credential management.
+
+1. [Authenticate GitLab with AWS](../cloud_deployment/index.md#authenticate-gitlab-with-aws).
+1. Create a `Dockerfile` with the following content:
+
+ ```Dockerfile
+ # Control package versions
+ ARG GITLAB_RUNNER_VERSION=v16.4.0
+ ARG AWS_CLI_VERSION=2.2.30
+
+ # AWS CLI and Amazon ECR Credential Helper
+ FROM amazonlinux as aws-tools
+ RUN set -e \
+ && yum update -y \
+ && yum install -y --allowerasing git make gcc curl unzip \
+ && curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" --output "awscliv2.zip" \
+ && unzip awscliv2.zip && ./aws/install -i /usr/local/bin \
+ && yum clean all
+
+ # Download and install ECR Credential Helper
+ RUN curl --location --output /usr/local/bin/docker-credential-ecr-login "https://github.com/awslabs/amazon-ecr-credential-helper/releases/latest/download/docker-credential-ecr-login-linux-amd64"
+ RUN chmod +x /usr/local/bin/docker-credential-ecr-login
+
+ # Configure the ECR Credential Helper
+ RUN mkdir -p /root/.docker
+ RUN echo '{ "credsStore": "ecr-login" }' > /root/.docker/config.json
+
+ # Final image based on GitLab Runner
+ FROM gitlab/gitlab-runner:${GITLAB_RUNNER_VERSION}
+
+ # Install necessary packages
+ RUN apt-get update \
+ && apt-get install -y --no-install-recommends jq procps curl unzip groff libgcrypt20 tar gzip less openssh-client \
+ && apt-get clean && rm -rf /var/lib/apt/lists/*
+
+ # Copy AWS CLI and Amazon ECR Credential Helper binaries
+ COPY --from=aws-tools /usr/local/bin/ /usr/local/bin/
+
+ # Copy ECR Credential Helper Configuration
+ COPY --from=aws-tools /root/.docker/config.json /root/.docker/config.json
+ ```
+
+1. To build the custom GitLab Runner Docker image within a `.gitlab-ci.yml`, include the following example below:
+
+ ```yaml
+ variables:
+ DOCKER_DRIVER: overlay2
+ IMAGE_NAME: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME
+ GITLAB_RUNNER_VERSION: v16.4.0
+ AWS_CLI_VERSION: 2.13.21
+
+ stages:
+ - build
+
+ build-image:
+ stage: build
+ script:
+ - echo "Logging into GitLab Container Registry..."
+ - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
+ - echo "Building Docker image..."
+ - docker build --build-arg GITLAB_RUNNER_VERSION=${GITLAB_RUNNER_VERSION} --build-arg AWS_CLI_VERSION=${AWS_CLI_VERSION} -t ${IMAGE_NAME} .
+ - echo "Pushing Docker image to GitLab Container Registry..."
+ - docker push ${IMAGE_NAME}
+ rules:
+ - changes:
+ - Dockerfile
+ ```
+
+1. [Register the runner](https://docs.gitlab.com/runner/register/index.html#docker).
diff --git a/doc/ci/enable_or_disable_ci.md b/doc/ci/enable_or_disable_ci.md
index 3081b8d1b39..d8a2fd66228 100644
--- a/doc/ci/enable_or_disable_ci.md
+++ b/doc/ci/enable_or_disable_ci.md
@@ -1,59 +1,11 @@
---
-stage: Verify
-group: Pipeline Execution
-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: howto
+redirect_to: 'pipelines/settings.md#disable-gitlab-cicd-pipelines'
+remove_date: '2024-01-30'
---
-# Disabling GitLab CI/CD **(FREE ALL)**
+This document was moved to [another location](pipelines/settings.md#disable-gitlab-cicd-pipelines).
-GitLab CI/CD is enabled by default on all new projects.
-If you use an external CI/CD server like Jenkins or Drone CI, you can
-disable GitLab CI/CD to avoid conflicts with the commits status
-API.
-
-You can disable GitLab CI/CD:
-
-- [For each project](#disable-cicd-in-a-project).
-- [For all new projects on an instance](../administration/cicd.md).
-
-These changes do not apply to projects in an
-[external integration](../user/project/integrations/index.md#available-integrations).
-
-## Disable CI/CD in a project
-
-When you disable GitLab CI/CD:
-
-- The **CI/CD** item in the left sidebar is removed.
-- The `/pipelines` and `/jobs` pages are no longer available.
-- Existing jobs and pipelines are hidden, not removed.
-
-To disable GitLab CI/CD in your project:
-
-1. On the left sidebar, select **Search or go to** and find your project.
-1. Select **Settings > General**.
-1. Expand **Visibility, project features, permissions**.
-1. In the **Repository** section, turn off **CI/CD**.
-1. Select **Save changes**.
-
-## Enable CI/CD in a project
-
-To enable GitLab CI/CD in your project:
-
-1. On the left sidebar, select **Search or go to** and find your project.
-1. Select **Settings > General**.
-1. Expand **Visibility, project features, permissions**.
-1. In the **Repository** section, turn on **CI/CD**.
-1. Select **Save changes**.
-
-<!-- ## Troubleshooting
-
-Include any troubleshooting steps that you can foresee. If you know beforehand what issues
-one might have when setting this up, or when something is changed, or on upgrading, it's
-important to describe those, too. Think of things that may go wrong and include them here.
-This is important to minimize requests for support, and to avoid doc comments with
-questions that you know someone might ask.
-
-Each scenario can be a third-level heading, for example `### Getting error message X`.
-If you have none to add when creating a doc, leave this section in place
-but commented out to help encourage others to add to it in the future. -->
+<!-- This redirect file can be deleted after <2024-01-30>. -->
+<!-- Redirects that point to other docs in the same project expire in three months. -->
+<!-- Redirects that point to docs in a different project or site (link is not relative and starts with `https:`) expire in one year. -->
+<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/redirects.html -->
diff --git a/doc/ci/environments/deployment_approvals.md b/doc/ci/environments/deployment_approvals.md
index 754dcafb9f7..b14ee5eb3eb 100644
--- a/doc/ci/environments/deployment_approvals.md
+++ b/doc/ci/environments/deployment_approvals.md
@@ -23,6 +23,10 @@ require approvals for deployments to production environments.
You can require approvals for deployments to protected environments in
a project.
+Prerequisite:
+
+- To update an environment, you must have at least the Maintainer role.
+
To configure deployment approvals for a project:
1. Create a deployment job in the `.gitlab-ci.yml` file of your project:
@@ -41,10 +45,26 @@ To configure deployment approvals for a project:
The job does not need to be manual (`when: manual`).
-1. Add the required [approval rules](#multiple-approval-rules).
+1. Add the required [approval rules](#add-multiple-approval-rules).
The environments in your project require approval before deployment.
+### Add multiple approval rules
+
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/345678) in GitLab 14.10 with a flag named `deployment_approval_rules`. Disabled by default.
+> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/345678) in GitLab 15.0. [Feature flag `deployment_approval_rules`](https://gitlab.com/gitlab-org/gitlab/-/issues/345678) removed.
+> - UI configuration [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/378445) in GitLab 15.11.
+
+Add multiple approval rules to control who can approve and execute deployment jobs.
+
+To configure multiple approval rules, use the [CI/CD settings](protected_environments.md#protecting-environments).
+You can [also use the API](../../api/group_protected_environments.md#protect-a-single-environment).
+
+All jobs deploying to the environment are blocked and wait for approvals before running.
+Make sure the number of required approvals is less than the number of users allowed to deploy.
+
+After a deployment job is approved, you must [run the job manually](../jobs/job_control.md#run-a-manual-job).
+
<!--- start_remove The following content will be removed on remove_date: '2024-05-22' -->
### Unified approval setting (deprecated)
@@ -62,7 +82,7 @@ To configure approvals for a protected environment:
- Using the [REST API](../../api/protected_environments.md#protect-a-single-environment),
set the `required_approval_count` field to 1 or more.
-After this is configured, all jobs deploying to this environment automatically go into a blocked state and wait for approvals before running. Ensure that the number of required approvals is less than the number of users allowed to deploy.
+After this setting is configured, all jobs deploying to this environment automatically go into a blocked state and wait for approvals before running. Ensure that the number of required approvals is less than the number of users allowed to deploy.
Example:
@@ -73,46 +93,8 @@ curl --header 'Content-Type: application/json' --request POST \
"https://gitlab.example.com/api/v4/projects/22034114/protected_environments"
```
-NOTE:
-To protect, update, or unprotect an environment, you must have at least the
-Maintainer role.
-
<!--- end_remove -->
-### Multiple approval rules
-
-> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/345678) in GitLab 14.10 with a flag named `deployment_approval_rules`. Disabled by default.
-> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/345678) in GitLab 15.0. [Feature flag `deployment_approval_rules`](https://gitlab.com/gitlab-org/gitlab/-/issues/345678) removed.
-> - UI configuration [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/378445) in GitLab 15.11.
-
-- Using the [REST API](../../api/group_protected_environments.md#protect-a-single-environment).
- - `deploy_access_levels` represents which entity can execute the deployment job.
- - `approval_rules` represents which entity can approve the deployment job.
-- Using the [UI](protected_environments.md#protecting-environments).
- - **Allowed to deploy** sets which entities can execute the deployment job.
- - **Approvers** sets which entities can approve the deployment job.
-
-After this is configured, all jobs deploying to this environment automatically go into a blocked state and wait for approvals before running. Ensure that the number of required approvals is less than the number of users allowed to deploy. Once a deployment job is approved, it must be [run manually](../jobs/job_control.md#run-a-manual-job).
-
-A configuration that uses the REST API might look like:
-
-```shell
-curl --header 'Content-Type: application/json' --request POST \
- --data '{"name": "production", "deploy_access_levels": [{"group_id": 138}], "approval_rules": [{"group_id": 134}, {"group_id": 135, "required_approvals": 2}]}' \
- --header "PRIVATE-TOKEN: <your_access_token>" \
- "https://gitlab.example.com/api/v4/groups/128/protected_environments"
-```
-
-With this setup:
-
-- The operator group (`group_id: 138`) has permission to execute the deployment jobs to the `production` environment in the organization (`group_id: 128`).
-- The QA tester group (`group_id: 134`) and security group (`group_id: 135`) have permission to approve the deployment jobs to the `production` environment in the organization (`group_id: 128`).
-- Unless two approvals from security group and one approval from QA tester group have been collected, the operator group can't execute the deployment jobs.
-
-NOTE:
-To protect, update, or unprotect an environment, you must have at least the
-Maintainer role.
-
### Migrate to multiple approval rules
You can migrate a protected environment from unified approval rules to multiple
@@ -128,7 +110,7 @@ To migrate with the UI:
1. From the **Environment** list, select your environment.
1. For each entity allowed to deploy to the environment:
1. Select **Add approval rules**.
- 1. In the modal window, select which entity is allowed to approve the
+ 1. On the dialog, select which entity is allowed to approve the
deployment job.
1. Enter the number of required approvals.
1. Select **Save**.
@@ -154,6 +136,9 @@ require `Administrator` to approve every deployment job in `Production`.
> - Automatic approval [removed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/124638) in GitLab 16.2 due to [usability issues](https://gitlab.com/gitlab-org/gitlab/-/issues/391258).
By default, the user who triggers a deployment pipeline can't also approve the deployment job.
+
+A GitLab administrator can approve or reject all deployments.
+
To allow self-approval of a deployment job:
1. On the left sidebar, select **Search or go to** and find your project.
@@ -165,55 +150,53 @@ To allow self-approval of a deployment job:
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/342180/) in GitLab 14.9
-Using either the GitLab UI or the API, you can:
+Using the GitLab UI or the API, you can:
- Approve a deployment to allow it to proceed.
- Reject a deployment to prevent it.
-NOTE:
-GitLab administrators can approve or reject all deployments.
+Prerequisites:
-### Approve or reject a deployment using the UI
+- You have permission to deploy to the protected environment.
-Prerequisites:
+::Tabs
-- Permission to deploy to the protected environment.
+:::TabTitle With the UI
-To approve or reject a deployment to a protected environment using the UI:
+To approve or reject a deployment with the UI:
1. On the left sidebar, select **Search or go to** and find your project.
1. Select **Operate > Environments**.
1. Select the environment's name.
1. In the deployment's row, select **Approval options** (**{thumb-up}**).
- Before approving or rejecting the deployment, you can view the number of approvals granted and
- remaining, also who has approved or rejected it.
+ Before you approve or reject the deployment, you can view the deployment's approval details.
1. Optional. Add a comment which describes your reason for approving or rejecting the deployment.
1. Select **Approve** or **Reject**.
-### Approve or reject a deployment using the API
+:::TabTitle With the API
-Prerequisites:
+To approve or reject a deployment with the API:
-- Permission to deploy to the protected environment.
+- Pass the required attributes to the deployment endpoint.
-To approve or reject a deployment to a protected environment using the API, pass the
-required attributes. For more details, see
-[Approve or reject a blocked deployment](../../api/deployments.md#approve-or-reject-a-blocked-deployment).
+For details, see [Approve or reject a blocked deployment](../../api/deployments.md#approve-or-reject-a-blocked-deployment).
-Example:
+For example:
```shell
curl --data "status=approved&comment=Looks good to me" \
--header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/deployments/1/approval"
```
+::EndTabs
+
### View the approval details of a deployment
Prerequisites:
-- Permission to deploy to the protected environment.
+- You have permission to deploy to the protected environment.
-A deployment to a protected environment can only proceed after all required approvals have been
+A deployment to a protected environment can proceed only after all required approvals have been
granted.
To view the approval details of a deployment:
@@ -230,25 +213,31 @@ The approval status details are shown:
- Users who have granted approval
- History of approvals or rejections
-## How to see blocked deployments
+## View blocked deployments
+
+Use the UI or API to review the status of your deployments, including whether a deployment is blocked.
-### Using the UI
+::Tabs
+
+:::TabTitle With the UI
+
+To view your deployments:
1. On the left sidebar, select **Search or go to** and find your project.
1. Select **Operate > Environments**.
1. Select the environment being deployed to.
-1. Look for the `blocked` label.
-### Using the API
+A deployment with the **blocked** label is blocked.
+
+:::TabTitle With the API
+
+To view your deployments:
+
+- Using the [deployments API](../../api/deployments.md#get-a-specific-deployment), get a specific deployment, or a list of all deployments in a project.
-Use the [Deployments API](../../api/deployments.md#get-a-specific-deployment) to see deployments.
+The `status` field indicates whether a deployment is blocked.
-- The `status` field indicates if a deployment is blocked.
-- When the [unified approval setting](#unified-approval-setting-deprecated) is configured:
- - The `pending_approval_count` field indicates how many approvals are remaining to run a deployment.
- - The `approvals` field contains the deployment's approvals.
-- When the [multiple approval rules](#multiple-approval-rules) is configured:
- - The `approval_summary` field contains the current approval status per rule.
+::EndTabs
## Related topics
diff --git a/doc/ci/environments/kubernetes_dashboard.md b/doc/ci/environments/kubernetes_dashboard.md
index 0f9e1d808ec..42fa560ad76 100644
--- a/doc/ci/environments/kubernetes_dashboard.md
+++ b/doc/ci/environments/kubernetes_dashboard.md
@@ -55,6 +55,17 @@ Prerequisites:
## View a dashboard
+> Kubernetes watch API integration [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/422945) in GitLab 16.6 [with a flag](../../administration/feature_flags.md) named `k8s_watch_api`. Disabled by default.
+
+FLAG:
+On self-managed GitLab, by default the Kubernetes watch API integration is not available.
+To make it available, an administrator can [enable the feature flag](../../administration/feature_flags.md) named `k8s_watch_api`.
+On GitLab.com, this feature is not available.
+
+View a dashboard to see the status of any connected clusters.
+If the `k8s_watch_api` feature flag is enabled, the status of your
+pods and Flux reconciliation updates in real time.
+
To view a configured dashboard:
1. On the left sidebar, select **Search or go to** and find your project.
@@ -72,8 +83,7 @@ You can review the sync status of your Flux deployments from a dashboard.
To display the deployment status, your dashboard must be able to retrieve the `Kustomization` and `HelmRelease` resources,
which requires a namespace to be configured for the environment.
-By default, GitLab searches the `Kustomization` and `HelmRelease` resources for the name of the project slug.
-You can specify the resource names with the **Flux resource** dropdown list in the environment settings.
+GitLab searches the `Kustomization` and `HelmRelease` resources specified by the **Flux resource** dropdown list in the environment settings.
A dashboard displays one of the following status badges:
diff --git a/doc/ci/index.md b/doc/ci/index.md
index 413116b0e51..c0c63d13d3a 100644
--- a/doc/ci/index.md
+++ b/doc/ci/index.md
@@ -21,23 +21,34 @@ If you're new to GitLab CI/CD, start by reviewing some of the commonly used term
### The `.gitlab-ci.yml` file
-To use GitLab CI/CD, you start with a `.gitlab-ci.yml` file at the root of your project.
-In this file, you specify the list of things you want to do, like test and deploy your application.
-This file follows the YAML format and has its own special syntax.
+To use GitLab CI/CD, you start with a `.gitlab-ci.yml` file at the root of your project
+which contains the configuration for your CI/CD pipeline. This file follows the YAML format
+and has its own special syntax.
You can name this file anything you want, but `.gitlab-ci.yml` is the most common name.
-Use the pipeline editor to edit the `.gitlab-ci.yml` file and test the syntax before you commit changes.
+
+In the `.gitlab-ci.yml` file, you can define:
+
+- The tasks you want to complete, for example test and deploy your application.
+- Other configuration files and templates you want to include.
+- Dependencies and caches.
+- The commands you want to run in sequence and those you want to run in parallel.
+- The location to deploy your application to.
+- Whether you want to run the scripts automatically or trigger any of them manually.
**Get started:**
- [Create your first `.gitlab-ci.yml` file](quick_start/index.md).
- [View all the possible keywords that you can use in the `.gitlab-ci.yml` file](yaml/index.md).
+the configuration.
+- Use the [pipeline editor](pipeline_editor/index.md) to edit or [visualize](pipeline_editor/index.md#visualize-ci-configuration)
+ your CI/CD configuration.
### Runners
Runners are the agents that run your jobs. These agents can run on physical machines or virtual instances.
In your `.gitlab-ci.yml` file, you can specify a container image you want to use when running the job.
-The runner loads the image and runs the job either locally or in the container.
+The runner loads the image, clones your project and runs the job either locally or in the container.
If you use GitLab.com, SaaS runners on Linux, Windows, and macOS are already available for use. And you can register your own
runners on GitLab.com if you'd like.
@@ -68,16 +79,20 @@ Pipelines are made up of jobs and stages:
### CI/CD variables
CI/CD variables help you customize jobs by making values defined elsewhere accessible to jobs.
-They can be hard-coded in your `.gitlab-ci.yml` file, project settings, or dynamically generated
-[predefined variables](variables/predefined_variables.md).
+They can be hard-coded in your `.gitlab-ci.yml` file, project settings, or dynamically generated.
**Get started:**
- [Learn more about CI/CD variables](variables/index.md).
+- [Learn about dynamically generated predefined variables](variables/predefined_variables.md).
### CI/CD components
-A [CI/CD component](components/index.md) is a reusable single pipeline configuration unit. Use them to compose an entire pipeline configuration or a small part of a larger pipeline.
+A CI/CD component is a reusable single pipeline configuration unit. Use them to compose an entire pipeline configuration or a small part of a larger pipeline.
+
+**Get started:**
+
+- [Learn more about CI/CD components](components/index.md).
## Videos
diff --git a/doc/ci/jobs/ci_job_token.md b/doc/ci/jobs/ci_job_token.md
index a335794b209..cf8b4ccd092 100644
--- a/doc/ci/jobs/ci_job_token.md
+++ b/doc/ci/jobs/ci_job_token.md
@@ -22,6 +22,7 @@ You can use a GitLab CI/CD job token to authenticate with specific API endpoints
- [Get job token's job](../../api/jobs.md#get-job-tokens-job).
- [Pipeline triggers](../../api/pipeline_triggers.md), using the `token=` parameter
to [trigger a multi-project pipeline](../pipelines/downstream_pipelines.md#trigger-a-multi-project-pipeline-by-using-the-api).
+- [Update pipeline metadata](../../api/pipelines.md#update-pipeline-metadata)
- [Releases](../../api/releases/index.md) and [Release links](../../api/releases/links.md).
- [Terraform plan](../../user/infrastructure/index.md).
- [Deployments](../../api/deployments.md).
@@ -69,9 +70,7 @@ tries to steal tokens from other jobs.
You can control what projects a CI/CD job token can access to increase the
job token's security. A job token might give extra permissions that aren't necessary
-to access specific private resources. The job token scope only controls access
-to private projects. If an accessed project is public or internal, token scoping does
-not apply.
+to access specific private resources.
When enabled, and the job token is being used to access a different project:
@@ -80,7 +79,7 @@ When enabled, and the job token is being used to access a different project:
- The accessed project must have the project attempting to access it [added to the allowlist](#add-a-project-to-the-job-token-scope-allowlist).
If a job token is leaked, it could potentially be used to access private data
-to the job token's user. By limiting the job token access scope, private data cannot
+to the job token's user. By limiting the job token access scope, project data cannot
be accessed unless projects are explicitly authorized.
There is a proposal to add more strategic control of the access permissions,
@@ -100,8 +99,7 @@ their `CI_JOB_TOKEN`.
For example, project `A` can add project `B` to the allowlist. CI/CD jobs
in project `B` (the "allowed project") can now use their CI/CD job token to
-authenticate API calls to access project `A`. If project `A` is public or internal,
-the project can be accessed by project `B` without adding it to the allowlist.
+authenticate API calls to access project `A`.
By default, the allowlist of any project only includes itself.
@@ -109,6 +107,32 @@ It is a security risk to disable this feature, so project maintainers or owners
keep this setting enabled at all times. Add projects to the allowlist only when cross-project
access is needed.
+### Limit job token scope for public or internal projects
+
+Projects can use a job token to authenticate with public or internal projects for
+the following actions without being added to the allowlist:
+
+- Fetch artifacts
+- Access the container registry
+- Access the package registry
+- Access releases, deployments, and environments
+
+To limit access to these actions to only the projects on the allowlist, set the visibility
+of each feature to be only accessible to project members:
+
+Prerequisite:
+
+- You must have the Maintainer role for the project.
+
+1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your project.
+1. On the left sidebar, select **Settings > General**.
+1. Expand **Visibility, project features, permissions**.
+1. Set the visibility to **Only project members** for the features you want to restrict access to.
+ - The ability to fetch artifacts is controlled by the CI/CD visibility setting.
+1. Select **Save changes**.
+
+Triggering pipelines and fetching Terraform plans is not affected by feature visibility.
+
### Disable the job token scope allowlist
> **Allow access to this project with a CI_JOB_TOKEN** setting [renamed to **Limit access _to_ this project**](https://gitlab.com/gitlab-org/gitlab/-/issues/411406) in GitLab 16.3.
@@ -180,9 +204,7 @@ limited only by the user's access permissions.
For example, when the setting is enabled, jobs in a pipeline in project `A` have
a `CI_JOB_TOKEN` scope limited to project `A`. If the job needs to use the token
-to make an API request to a private project `B`, then `B` must be added to the allowlist for `A`.
-If project `B` is public or internal, you do not need to add
-`B` to the allowlist to grant access.
+to make an API request to project `B`, then `B` must be added to the allowlist for `A`.
### Configure the job token scope
diff --git a/doc/ci/jobs/index.md b/doc/ci/jobs/index.md
index 90a64ea7569..b5fc32e69dc 100644
--- a/doc/ci/jobs/index.md
+++ b/doc/ci/jobs/index.md
@@ -297,7 +297,8 @@ For example, if you start rolling out new code and:
## Expand and collapse job log sections
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/14664) in GitLab 12.0.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/14664) in GitLab 12.0.
+> - Support for output of multi-line command bash shell output [Introduced](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/3486) in GitLab 16.5 behind the [GitLab Runner feature flag](https://docs.gitlab.com/runner/configuration/feature-flags.html), `FF_SCRIPT_SECTIONS`.
Job logs are divided into sections that can be collapsed or expanded. Each section displays
the duration.
@@ -397,3 +398,67 @@ The behavior of deployment jobs can be controlled with
[deployment safety](../environments/deployment_safety.md) settings like
[preventing outdated deployment jobs](../environments/deployment_safety.md#prevent-outdated-deployment-jobs)
and [ensuring only one deployment job runs at a time](../environments/deployment_safety.md#ensure-only-one-deployment-job-runs-at-a-time).
+
+## Troubleshooting
+
+### Job log slow to update
+
+When you visit the job log page for a running job, there could be a delay of up to
+60 seconds before a log update. The default refresh time is 60 seconds, but after
+the log is viewed in the UI one time, log updates should occur every 3 seconds.
+
+### `get_sources` job section fails because of an HTTP/2 problem
+
+Sometimes, jobs fail with the following cURL error:
+
+```plaintext
+++ git -c 'http.userAgent=gitlab-runner <version>' fetch origin +refs/pipelines/<id>:refs/pipelines/<id> ...
+error: RPC failed; curl 16 HTTP/2 send again with decreased length
+fatal: ...
+```
+
+You can work around this problem by configuring Git and `libcurl` to
+[use HTTP/1.1](https://git-scm.com/docs/git-config#Documentation/git-config.txt-httpversion).
+The configuration can be added to:
+
+- A job's [`pre_get_sources_script`](../yaml/index.md#hookspre_get_sources_script):
+
+ ```yaml
+ job_name:
+ hooks:
+ pre_get_sources_script:
+ - git config --global http.version "HTTP/1.1"
+ ```
+
+- The [runner's `config.toml`](https://docs.gitlab.com/runner/configuration/advanced-configuration.html)
+ with [Git configuration environment variables](https://git-scm.com/docs/git-config#ENVIRONMENT):
+
+ ```toml
+ [[runners]]
+ ...
+ environment = [
+ "GIT_CONFIG_COUNT=1",
+ "GIT_CONFIG_KEY_0=http.version",
+ "GIT_CONFIG_VALUE_0=HTTP/1.1"
+ ]
+ ```
+
+### Job using `resource_group` gets stuck **(FREE SELF)**
+
+If a job using [`resource_group`](../yaml/index.md#resource_group) gets stuck, a
+GitLab administrator can try run the following commands from the [rails console](../../administration/operations/rails_console.md#starting-a-rails-console-session):
+
+```ruby
+# find resource group by name
+resource_group = Project.find_by_full_path('...').resource_groups.find_by(key: 'the-group-name')
+busy_resources = resource_group.resources.where('build_id IS NOT NULL')
+
+# identify which builds are occupying the resource
+# (I think it should be 1 as of today)
+busy_resources.pluck(:build_id)
+
+# it's good to check why this build is holding the resource.
+# Is it stuck? Has it been forcefully dropped by the system?
+# free up busy resources
+busy_resources.update_all(build_id: nil)
+```
diff --git a/doc/ci/jobs/job_control.md b/doc/ci/jobs/job_control.md
index 1065ee93389..0c8e4fc593f 100644
--- a/doc/ci/jobs/job_control.md
+++ b/doc/ci/jobs/job_control.md
@@ -174,8 +174,7 @@ multiple pipelines. You don't have to explicitly configure rules for multiple ty
of pipeline to trigger them accidentally.
Some configurations that have the potential to cause duplicate pipelines cause a
-[pipeline warning](../troubleshooting.md#pipeline-warnings) to be displayed.
-[Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/219431) in GitLab 13.3.
+[pipeline warning](../debugging.md#pipeline-warnings) to be displayed.
For example:
@@ -209,7 +208,7 @@ To avoid duplicate pipelines, you can:
You can also avoid duplicate pipelines by changing the job rules to avoid either push (branch)
pipelines or merge request pipelines. However, if you use a `- when: always` rule without
-`workflow: rules`, GitLab still displays a [pipeline warning](../troubleshooting.md#pipeline-warnings).
+`workflow: rules`, GitLab still displays a [pipeline warning](../debugging.md#pipeline-warnings).
For example, the following does not trigger double pipelines, but is not recommended
without `workflow: rules`:
@@ -933,7 +932,7 @@ types the variables can control for:
| `CI_COMMIT_BRANCH` | Yes | | | Yes |
| `CI_COMMIT_TAG` | | Yes | | Yes, if the scheduled pipeline is configured to run on a tag. |
| `CI_PIPELINE_SOURCE = push` | Yes | Yes | | |
-| `CI_PIPELINE_SOURCE = scheduled` | | | | Yes |
+| `CI_PIPELINE_SOURCE = schedule` | | | | Yes |
| `CI_PIPELINE_SOURCE = merge_request_event` | | | Yes | |
| `CI_MERGE_REQUEST_IID` | | | Yes | |
@@ -1194,3 +1193,20 @@ To run protected manual jobs:
- Add the administrator as a direct member of the private project (any role)
- [Impersonate a user](../../administration/admin_area.md#user-impersonation) who is a
direct member of the project.
+
+### A CI/CD job does not use newer configuration when run again
+
+The configuration for a pipeline is only fetched when the pipeline is created.
+When you rerun a job, uses the same configuration each time. If you update configuration files,
+including separate files added with [`include`](../yaml/index.md#include), you must
+start a new pipeline to use the new configuration.
+
+### `Job may allow multiple pipelines to run for a single action` warning
+
+When you use [`rules`](../yaml/index.md#rules) with a `when` clause without an `if`
+clause, multiple pipelines may run. Usually this occurs when you push a commit to
+a branch that has an open merge request associated with it.
+
+To [prevent duplicate pipelines](#avoid-duplicate-pipelines), use
+[`workflow: rules`](../yaml/index.md#workflow) or rewrite your rules to control
+which pipelines can run.
diff --git a/doc/ci/migration/bamboo.md b/doc/ci/migration/bamboo.md
new file mode 100644
index 00000000000..93091d2a30a
--- /dev/null
+++ b/doc/ci/migration/bamboo.md
@@ -0,0 +1,780 @@
+---
+stage: Verify
+group: Pipeline Authoring
+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: index, howto
+---
+
+# Migrating from Bamboo **(FREE ALL)**
+
+This migration guide looks at how you can migrate from Atlassian Bamboo to GitLab CI/CD.
+The focus is on [Bamboo Specs YAML](https://docs.atlassian.com/bamboo-specs-docs/8.1.12/specs.html?yaml)
+exported from the Bamboo UI or stored in Spec repositories.
+
+## GitLab CI/CD Primer
+
+If you are new to GitLab CI/CD, use the [Getting started guide](../index.md) to learn
+the basic concepts and how to create your first [`.gitlab-ci.yml` file](../quick_start/index.md).
+If you already have some experience using GitLab CI/CD, you can review [keywords reference documentation](../yaml/index.md)
+to see the full list of available keywords.
+
+You can also take a look at [Auto DevOps](../../topics/autodevops/index.md), which automatically
+builds, tests, and deploys your application using a collection of
+pre-configured features and integrations.
+
+## Key similarities and differences
+
+### Offerings
+
+Atlassian offers Bamboo in its Cloud (SaaS) or Data center (Self-managed) options.
+A third Server option is scheduled for [EOL on February 15, 2024](https://about.gitlab.com/blog/2023/09/26/atlassian-server-ending-move-to-a-single-devsecops-platform/).
+
+These options are similar to GitLab [SaaS](../../subscriptions/gitlab_com/index.md)
+and [Self-Managed](../../subscriptions/self_managed/index.md). GitLab also offers
+[GitLab Dedicated](../../subscriptions/gitlab_dedicated/index.md), a fully isolated
+single-tenant SaaS service.
+
+### Agents vs Runners
+
+Bamboo uses [agents](https://confluence.atlassian.com/confeval/development-tools-evaluator-resources/bamboo/bamboo-remote-agents-and-local-agents)
+to run builds and deployments. Agents can be local agents running on the Bamboo server or
+remote agents running external to the server.
+
+GitLab uses a similar concept to agents called [runners](https://docs.gitlab.com/runner/)
+which use [executors](https://docs.gitlab.com/runner/executors/) to run builds.
+
+Examples of executors are shell, Docker, or Kubernetes. You can choose to use GitLab [SaaS runners](../runners/index.md)
+or deploy your own [self-managed runners](https://docs.gitlab.com/runner/install/index.md).
+
+### Workflow
+
+[Bamboo workflow](https://confluence.atlassian.com/bamboo/understanding-the-bamboo-ci-server-289277285.html)
+is organized into projects. Projects are used to organize Plans, along with variables,
+shared credentials, and permissions needed by multiple plans. A plan groups jobs into
+stages and links to code repositories where applications to be built are hosted.
+Repositories could be in Bitbucket, GitLab, or other services.
+
+A job is a series of tasks that are executed sequentially on the same Bamboo agent.
+CI and deployments are treated separately in Bamboo. [Deployment project workflow](https://confluence.atlassian.com/bamboo/deployment-projects-workflow-362971857.html)
+is different from the build plans workflow. [Learn more](https://confluence.atlassian.com/bamboo/understanding-the-bamboo-ci-server-289277285.html)
+about Bamboo workflow.
+
+GitLab CI/CD uses a similar workflow. Jobs are organized into [stages](../yaml/index.md#stage),
+and projects have individual `.gitlab-ci.yml` configuration files or include existing templates.
+
+### Templating & Configuration as Code
+
+#### Bamboo Specs
+
+Bamboo plans can be configured in either the Web UI or with Bamboo Specs.
+[Bamboo Specs](https://confluence.atlassian.com/bamboo/bamboo-specs-894743906.html)
+is configuration as code, which can be written in Java or YAML. [YAML Specs](https://docs.atlassian.com/bamboo-specs-docs/8.1.12/specs.html?yaml)
+is the easiest to use but lacks in Bamboo feature coverage. [Java Specs](https://docs.atlassian.com/bamboo-specs-docs/8.1.12/specs.html?java)
+has complete Bamboo feature coverage and can be written in any JVM language like Groovy, Scala, or Kotlin.
+If you configured your plans using the Web UI, you can [export your Bamboo configuration](https://confluence.atlassian.com/bamboo/exporting-existing-plan-configuration-to-bamboo-yaml-specs-1018270696.html)
+into Bamboo Specs.
+
+Bamboo Specs can also be [repository-stored](https://confluence.atlassian.com/bamboo/enabling-repository-stored-bamboo-specs-938641941.html).
+
+#### `.gitlab-ci.yml` configuration file
+
+GitLab, by default, uses a [`.gitlab-ci.yml` file](../yaml/index.md) for CI/CD configuration.
+Alternatively, [Auto DevOps](../../topics/autodevops/index.md) can automatically build,
+test, and deploy your application without a manually configured `.gitlab-ci.yml` file.
+
+GitLab CI/CD configuration can be organized into templates that are reusable across projects.
+GitLab also provides pre-built [templates](../examples/index.md#cicd-templates)
+that help you get started quickly and avoid re-inventing the wheel.
+
+### Configuration
+
+#### Bamboo YAML Spec syntax
+
+This Bamboo Spec was exported from a Bamboo Server instance, which creates quite verbose output:
+
+```yaml
+version: 2
+plan:
+ project-key: AB
+ key: TP
+ name: test plan
+stages:
+- Default Stage:
+ manual: false
+ final: false
+ jobs:
+ - Default Job
+Default Job:
+ key: JOB1
+ tasks:
+ - checkout:
+ force-clean-build: false
+ description: Checkout Default Repository
+ - script:
+ interpreter: SHELL
+ scripts:
+ - |-
+ ruby -v # Print out ruby version for debugging
+ bundle config set --local deployment true # Install dependencies into ./vendor/ruby
+ bundle install -j $(nproc)
+ rubocop
+ rspec spec
+ description: run bundler
+ artifact-subscriptions: []
+repositories:
+- Demo Project:
+ scope: global
+triggers:
+- polling:
+ period: '180'
+branches:
+ create: manually
+ delete: never
+ link-to-jira: true
+notifications: []
+labels: []
+dependencies:
+ require-all-stages-passing: false
+ enabled-for-branches: true
+ block-strategy: none
+ plans: []
+other:
+ concurrent-build-plugin: system-default
+
+---
+
+version: 2
+plan:
+ key: AB-TP
+plan-permissions:
+- users:
+ - root
+ permissions:
+ - view
+ - edit
+ - build
+ - clone
+ - admin
+ - view-configuration
+- roles:
+ - logged-in
+ - anonymous
+ permissions:
+ - view
+...
+
+```
+
+A GitLab CI/CD `.gitlab-ci.yml` configuration with similar behavior would be:
+
+```yaml
+default:
+ image: ruby:latest
+
+stages:
+- default-stage
+
+job1:
+ stage: default-stage
+ script:
+ - ruby -v # Print out ruby version for debugging
+ - bundle config set --local deployment true # Install dependencies into ./vendor/ruby
+ - bundle install -j $(nproc)
+ - rubocop
+ - rspec spec
+```
+
+### Common Configurations
+
+This section reviews some common Bamboo configurations and the GitLab CI/CD equivalents.
+
+#### Workflow
+
+Bamboo is structured differently compared to GitLab CI/CD. With GitLab, CI/CD can be enabled
+in a project in a number of ways: by adding a `.gitlab-ci.yml` file to the project,
+the existence of a Compliance pipeline in the group the project belongs to, or enabling AutoDevOps.
+Pipelines are then triggered automatically, depending on rules or context, where AutoDevOps is used.
+
+Bamboo is structured differently, [repositories need to be added](https://confluence.atlassian.com/bamboo0903/linking-to-source-code-repositories-1236445195.html)
+to a Bamboo project, with authentication provided and [triggers](https://confluence.atlassian.com/bamboo0903/triggering-builds-1236445226.html)
+are set. Repositories added to projects are available to all plans in the project.
+Plans used for testing and building applications are called Build plans.
+
+#### Build Plans
+
+Build Plans in Bamboo are composed of Stages that run sequentially to build an application and generate artifacts where relevant. Build Plans require
+a default repository attached to it or inherit linked repositories from its parent project.
+Variables, triggers, and relationships between different plans can be defined at the plan level.
+
+An example of a Bamboo build plan:
+
+```yaml
+version: 2
+plan:
+ project-key: SAMPLE
+ name: Build Ruby App
+ key: BUILD-APP
+
+stages:
+ - Test App:
+ jobs:
+ - Test Application
+ - Perform Security checks
+ - Build App:
+ jobs:
+ - Build Application
+
+Test Application:
+ tasks:
+ - script:
+ - # Run tests
+
+Perform Security checks:
+ tasks:
+ - script:
+ - # Run Security Checks
+
+Build Application:
+ tasks:
+ - script:
+ - # Run buils
+```
+
+In this example:
+
+- Plan Specs include a YAML Spec version. Version 2 is the latest.
+- The `project-key` links the plan to its parent project. The key is specified when creating the project.
+- Plan `key` uniquely identifies the plan.
+
+In GitLab CI/CD, a Bamboo Build plan is similar to the `.gitlab-ci.yml` file in a project,
+which can include CI/CD scripts from other projects or templates.
+
+The equivalent GitLab CI/CD `.gitlab-ci.yml` file would be:
+
+```yaml
+default:
+ image: alpine:latest
+
+stages:
+ - test
+ - build
+
+test-application:
+ stage: test
+ script:
+ - # Run tests
+
+security-checks:
+ stage: test
+ script:
+ - # Run Security Checks
+
+build-application:
+ stage: build
+ script:
+ - # Run builds
+```
+
+#### Container Images
+
+Builds and deployments are run by default on the Bamboo agent's native operating system,
+but can be configured to run in containers. To make jobs run in a container, Bamboo uses
+the `docker` keyword at the plan or job level.
+
+For example, in a Bamboo build plan:
+
+```yaml
+version: 2
+plan:
+ project-key: SAMPLE
+ name: Build Ruby App
+ key: BUILD-APP
+
+docker: alpine:latest
+
+stages:
+ - Build App:
+ jobs:
+ - Build Application
+
+Build Application:
+ tasks:
+ - script:
+ - # Run builds
+ docker:
+ image: alpine:edge
+```
+
+In GitLab CI/CD, you only need the `image` keyword.
+
+The equivalent GitLab CI/CD `.gitlab-ci.yml` file would be:
+
+```yaml
+default:
+ image: alpine:latest
+
+stages:
+ - build
+
+build-application:
+ stage: build
+ script:
+ - # Run builds
+ image:
+ name: alpine:edge
+```
+
+#### Variables
+
+Bamboo has the following types of [variables](https://confluence.atlassian.com/bamboo/bamboo-variables-289277087.html)
+based on scope:
+
+- Build-specific variables which are evaluated at build time. For example `${bamboo.planKey}`.
+- System variables inherited from the Bamboo instance or system environment.
+- Global variables defined at the instance level and accessible to every plan.
+- Project variables defined at the project level and accessible by plans in the same project.
+- Plan variables specific to a plan.
+
+You can access variables in Bamboo using the format `${system.variableName}` for System variables
+and `${bamboo.variableName}` for other types of variables. When using a variable in a script task,
+the full stops, are converted to underscores, `${bamboo.variableName}` becomes `$bamboo_variableName`.
+
+In GitLab, [CI/CD variables](../variables/index.md) can be defined at these levels:
+
+- Instance.
+- Group.
+- Project.
+- At the global level in the CI/CD configuration.
+- At the job level in the CI/CD configuration.
+
+Like Bamboo's System and Global variables, GitLab has [predefined CI/CD variables](../variables/predefined_variables.md)
+that are available to every job.
+
+Defining variables in CI/CD scripts is similar in both Bamboo and GitLab.
+
+For example, in a Bamboo build plan:
+
+```yaml
+version: 2
+# ...
+variables:
+ username: admin
+ releaseType: milestone
+
+Default job:
+ tasks:
+ - script: echo '$bamboo_username is the DRI for $bamboo_releaseType'
+```
+
+The equivalent GitLab CI/CD `.gitlab-ci.yml` file would be:
+
+```yaml
+variables:
+ GLOBAL_VAR: "A global variable"
+
+job1:
+ variables:
+ JOB_VAR: "A job variable"
+ script:
+ - echo "Variables are '$GLOBAL_VAR' and '$JOB_VAR'"
+```
+
+In GitLab CI/CD, variables are accessed like regular Shell script variables. For example, `$VARIABLE_NAME`.
+
+#### Jobs & Tasks
+
+In both GitLab and Bamboo, jobs in the same stage run in parallel, except where there is a dependency
+that needs to be met before a job runs.
+
+The number of jobs that can run in Bamboo depends on availability of Bamboo agents
+and Bamboo license Size. With [GitLab CI/CD](../jobs/index.md), the number of parallel
+jobs depends on the number of runners integrated with the GitLab instance and the
+concurrency set in the runners.
+
+In Bamboo, Jobs are composed of [Tasks](https://confluence.atlassian.com/bamboo/configuring-tasks-289277036.html),
+which can be:
+
+- A set of commands run as a [script](https://confluence.atlassian.com/bamboo/script-289277046.html)
+- Predefined tasks like source code checkout, artifact download, and other tasks available in the
+ Atlassian [tasks marketplace](https://marketplace.atlassian.com/addons/app/bamboo).
+
+For example, in a Bamboo build plan:
+
+```yaml
+version: 2
+#...
+
+Default Job:
+ key: JOB1
+ tasks:
+ - checkout:
+ force-clean-build: false
+ description: Checkout Default Repository
+ - script:
+ interpreter: SHELL
+ scripts:
+ - |-
+ ruby -v
+ bundle config set --local deployment true
+ bundle install -j $(nproc)
+ description: run bundler
+other:
+ concurrent-build-plugin: system-default
+```
+
+The equivalent of Tasks in GitLab is the `script`, which specifies the commands
+for the runner to execute.
+
+For example, in a GitLab CI/CD `.gitlab-ci.yml` file:
+
+```yaml
+job1:
+ script: "bundle exec rspec"
+
+job2:
+ script:
+ - ruby -v
+ - bundle config set --local deployment true
+ - bundle install -j $(nproc)
+```
+
+With GitLab, you can use [CI/CD templates](https://gitlab.com/gitlab-org/gitlab-foss/tree/master/lib/gitlab/ci/templates)
+and [CI/CD components](../components/index.md) to compose your pipelines without the need to write
+everything yourself.
+
+#### Conditionals
+
+In Bamboo, every task can have conditions that determine if a task runs.
+
+For example, in a Bamboo build plan:
+
+```yaml
+version: 2
+# ...
+tasks:
+ - script:
+ interpreter: SHELL
+ scripts:
+ - echo "Hello"
+ conditions:
+ - variable:
+ equals:
+ planRepository.branch: development
+```
+
+With GitLab, this can be done with the `rules` keyword to [control when jobs run](../jobs/job_control.md) in GitLab CI/CD.
+
+For example, in a GitLab CI/CD `.gitlab-ci.yml` file:
+
+```yaml
+job:
+ script: echo "Hello, Rules!"
+ rules:
+ - if: $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME = development
+```
+
+#### Triggers
+
+Bamboo has a number of options for [triggering builds](https://confluence.atlassian.com/bamboo/triggering-builds-289276897.html),
+which can be based on code changes, a schedule, the outcomes of other plans, or on demand.
+A plan can be configured to periodically poll a project for new changes,
+as shown below.
+
+For example, in a Bamboo build plan:
+
+```yaml
+version: 2
+#...
+triggers:
+- polling:
+ period: '180'
+```
+
+GitLab CI/CD pipelines can be triggered based on code change, on schedule, or triggered by
+other jobs or API calls. GitLab CI/CD pipelines do not need to use polling, but can be triggered
+on schedule as well.
+
+You can configure when pipelines themselves run with the [`workflow` keyword](../yaml/workflow.md),
+and `rules`.
+
+For example, in a GitLab CI/CD `.gitlab-ci.yml` file:
+
+```yaml
+workflow:
+ rules:
+ - changes:
+ - .gitlab/**/**.md
+ when: never
+```
+
+#### Artifacts
+
+You can define Job artifacts using the `artifacts` keyword in both GitLab and Bamboo.
+
+For example, in a Bamboo build plan:
+
+```yaml
+version: 2
+# ...
+ artifacts:
+ -
+ name: Test Reports
+ location: target/reports
+ pattern: '*.xml'
+ required: false
+ shared: false
+ -
+ name: Special Reports
+ location: target/reports
+ pattern: 'special/*.xml'
+ shared: true
+```
+
+In this example, artifacts are defined with a name, location, pattern, and the optional
+ability to share the artifacts with other jobs or plans. You canalso define jobs that
+subscribe to the artifact.
+
+`artifact-subscriptions` is used to access artifacts from another job in the same plan,
+for example:
+
+```yaml
+Test app:
+ artifact-subscriptions:
+ -
+ artifact: Test Reports
+ destination: deploy
+```
+
+`artifact-download` is used to access artifacts from jobs in a different plan, for example:
+
+```yaml
+version: 2
+# ...
+ tasks:
+ - artifact-download:
+ source-plan: PROJECTKEY-PLANKEY
+```
+
+You need to provide the key of the plan you are downloading artifacts from in the `source-plan` keyword.
+
+In GitLab, all [artifacts](../jobs/job_artifacts.md) from completed jobs in earlier
+stages are downloaded by default.
+
+For example, in a GitLab CI/CD `.gitlab-ci.yml` file:
+
+```yaml
+stages:
+ - build
+
+pdf:
+ stage: build
+ script: #generate XML reports
+ artifacts:
+ name: "test-report-files"
+ untracked: true
+ paths:
+ - target/reports
+```
+
+In this example:
+
+- The name of the artifact is specific explicitly, but you can make it dynamic by using a CI/CD variable.
+- The `untracked` keyword sets the artifact to also include Git untracked files,
+ along with those specified explictly with `paths`.
+
+#### Caching
+
+In Bamboo, [Git caches](https://confluence.atlassian.com/bamkb/how-stored-git-caches-speed-up-builds-690848923.html)
+can be used to speed up builds. Git caches are configured in Bamboo administration settings
+and are stored either on the Bamboo server or remote agents.
+
+GitLab supports both Git Caches and Job cache. [Caches](../caching/index.md) are defined per job
+using the `cache` keyword.
+
+For example, in a GitLab CI/CD `.gitlab-ci.yml` file:
+
+```yaml
+test-job:
+ stage: build
+ cache:
+ - key:
+ files:
+ - Gemfile.lock
+ paths:
+ - vendor/ruby
+ - key:
+ files:
+ - yarn.lock
+ paths:
+ - .yarn-cache/
+ script:
+ - bundle config set --local path 'vendor/ruby'
+ - bundle install
+ - yarn install --cache-folder .yarn-cache
+ - echo Run tests...
+```
+
+#### Deployment Projects
+
+Bamboo has [Deployments project](https://confluence.atlassian.com/bamboo/deployment-projects-338363438.html),
+which link to Build plans to track, fetch, and deploy artifacts to [deployment environments](https://confluence.atlassian.com/bamboo0903/creating-a-deployment-environment-1236445634.html).
+
+When creating a project you link it to a build plan, specify the deployment environment
+and the tasks to perform the deployments. A [deployment task](https://confluence.atlassian.com/bamboo0903/tasks-for-deployment-environments-1236445662.html)
+can either be a script or a Bamboo task from the Atlassian marketplace.
+
+For example in a Deployment project Spec:
+
+```yaml
+version: 2
+
+deployment:
+ name: Deploy ruby app
+ source-plan: build-app
+
+release-naming: release-1.0
+
+environments:
+ - Production
+
+Production:
+ tasks:
+ - # scripts to deploy app to production
+ - ./.ci/deploy_prod.sh
+```
+
+In GitLab CI/CD, You can create a [deployment job](../jobs/index.md#deployment-jobs)
+that deploys to an [environment](../environments/index.md) or creates a [release](../../user/project/releases/index.md).
+
+For example, in a GitLab CI/CD `.gitlab-ci.yml` file:
+
+```yaml
+deploy-to-production:
+ stage: deploy
+ script:
+ - # Run Deployment script
+ - ./.ci/deploy_prod.sh
+ environment:
+ name: production
+```
+
+To create release instead, use the [`release`](../yaml/index.md#release)
+keyword with the [release-cli](https://gitlab.com/gitlab-org/release-cli/-/tree/master/docs)
+tool to create releases for [Git tags](../../user/project/repository/tags/index.md).
+
+For example, in a GitLab CI/CD `.gitlab-ci.yml` file:
+
+```yaml
+release_job:
+ stage: release
+ image: registry.gitlab.com/gitlab-org/release-cli:latest
+ rules:
+ - if: $CI_COMMIT_TAG # Run this job when a tag is created manually
+ script:
+ - echo "Building release version"
+ release:
+ tag_name: $CI_COMMIT_TAG
+ name: 'Release $CI_COMMIT_TAG'
+ description: 'Release created using the release-cli.'
+```
+
+### Security Scanning features
+
+Bamboo relies on third-party tasks provided in the Atlassian Marketplace to run security scans.
+GitLab provides [security scanners](../../user/application_security/index.md) out-of-the-box to detect
+vulnerabilities in all parts of the SDLC. You can add these plugins in GitLab using templates, for example to add
+SAST scanning to your pipeline, add the following to your `.gitlab-ci.yml`:
+
+```yaml
+include:
+ - template: Security/SAST.gitlab-ci.yml
+```
+
+You can customize the behavior of security scanners by using CI/CD variables, for example
+with the [SAST scanners](../../user/application_security/sast/index.md#available-cicd-variables).
+
+### Secrets Management
+
+Privileged information, often referred to as "secrets", is sensitive information
+or credentials you need in your CI/CD workflow. You might use secrets to unlock protected resources
+or sensitive information in tools, applications, containers, and cloud-native environments.
+
+Secrets management in Bamboo is usually handled using [Shared credentials](https://confluence.atlassian.com/bamboo/shared-credentials-424313357.html),
+or via third-party applications from the Atlassian market place.
+
+For secrets management in GitLab, you can use one of the supported integrations
+for an external service. These services securely store secrets outside of your GitLab project,
+though you must have a subscription for the service:
+
+- [HashiCorp Vault](../secrets/id_token_authentication.md#automatic-id-token-authentication-with-hashicorp-vault)
+- [Azure Key Vault](../secrets/azure_key_vault.md).
+
+GitLab also supports [OIDC authentication](../secrets/id_token_authentication.md)
+for other third party services that support OIDC.
+
+Additionally, you can make credentials available to jobs by storing them in CI/CD variables, though secrets
+stored in plain text are susceptible to accidental exposure, [the same as in Bamboo](https://confluence.atlassian.com/bamboo/bamboo-specs-encryption-970268127.html).
+You should always store sensitive information in [masked](../variables/index.md#mask-a-cicd-variable)
+and [protected](../variables/index.md#protect-a-cicd-variable) variables, which mitigates
+some of the risk.
+
+Also, never store secrets as variables in your `.gitlab-ci.yml` file, which is public to all
+users with access to the project. Storing sensitive information in variables should
+only be done in [the project, group, or instance settings](../variables/index.md#define-a-cicd-variable-in-the-ui).
+
+Review the [security guidelines](../variables/index.md#cicd-variable-security) to improve
+the safety of your CI/CD variables.
+
+### Migration Plan
+
+The following list of recommended steps was created after observing organizations
+that were able to quickly complete this migration.
+
+#### Create a Migration Plan
+
+Before starting a migration you should create a [migration plan](plan_a_migration.md)
+to make preparations for the migration. For a migration from Bamboo, ask yourself
+the following questions in preparation:
+
+- What Bamboo Tasks are used by jobs in Bamboo today?
+ - Do you know what these Tasks do exactly?
+ - Do any Task wrap a common build tool? For example, Maven, Gradle, or NPM?
+- What is installed on the Bamboo agents?
+- Are there any shared libraries in use?
+- How are you authenticating from Bamboo? Are you using SSH keys, API tokens, or other secrets?
+- Are there other projects that you need to access from your pipeline?
+- Are there credentials in Bamboo to access outside services? For example Ansible Tower,
+ Artifactory, or other Cloud Providers or deployment targets?
+
+#### Prerequisites
+
+Before doing any migration work, you should first:
+
+1. Get familiar with GitLab.
+ - Read about the [key GitLab CI/CD features](../../ci/index.md).
+ - Follow tutorials to create [your first GitLab pipeline](../quick_start/index.md)
+ and [more complex pipelines](../quick_start/tutorial.md) that build, test, and deploy
+ a static site.
+ - Review the [`.gitlab-ci.yml` keyword reference](../yaml/index.md).
+1. Set up and configure GitLab.
+1. Test your GitLab instance.
+ - Ensure [runners](../runners/index.md) are available, either by using shared GitLab.com runners or installing new runners.
+
+#### Migration Steps
+
+1. Migrate projects from your SCM solution to GitLab.
+ - (Recommended) You can use the available [importers](../../user/project/import/index.md)
+ to automate mass imports from external SCM providers.
+ - You can [import repositories by URL](../../user/project/import/repo_by_url.md).
+1. Create a `.gitlab-ci.yml` file in each project.
+1. Export your Bamboo Projects/Plans as YAML Spec
+1. Migrate Bamboo YAML Spec configuration to GitLab CI/CD jobs and configure them to show results directly in merge requests.
+1. Migrate deployment jobs by using [cloud deployment templates](../cloud_deployment/index.md),
+ [environments](../environments/index.md), and the [GitLab agent for Kubernetes](../../user/clusters/agent/index.md).
+1. Check if any CI/CD configuration can be reused across different projects, then create
+ and share CI/CD templates.
+1. Check the [pipeline efficiency documentation](../pipelines/pipeline_efficiency.md)
+ to learn how to make your GitLab CI/CD pipelines faster and more efficient.
+
+If you have questions that are not answered here, the [GitLab community forum](https://forum.gitlab.com/)
+can be a great resource.
diff --git a/doc/ci/migration/github_actions.md b/doc/ci/migration/github_actions.md
index 86ce6c4846a..46d15f506ac 100644
--- a/doc/ci/migration/github_actions.md
+++ b/doc/ci/migration/github_actions.md
@@ -39,7 +39,7 @@ functionality.
### Configuration file
GitHub Actions can be configured with a [workflow YAML file](https://docs.github.com/en/actions/learn-github-actions/understanding-github-actions#understanding-the-workflow-file).
-GitLab CI/CD uses a [`.gitlab-ci.yml` YAML file](../../ci/yaml/gitlab_ci_yaml.md) by default.
+GitLab CI/CD uses a [`.gitlab-ci.yml` YAML file](../../ci/index.md#the-gitlab-ciyml-file) by default.
For example, in a GitHub Actions `workflow` file:
@@ -88,7 +88,7 @@ from GitHub Actions to GitLab CI/CD.
generate automated CI/CD jobs that are triggered when certain event take place, for example
pushing a new commit. A GitHub Action workflow is a YAML file defined in the `.github/workflows`
directory located in the root of the repository. The GitLab equivalent is the
-[`.gitlab-ci.yml` configuration file](../../ci/yaml/gitlab_ci_yaml.md) which also resides
+[`.gitlab-ci.yml` configuration file](../../ci/index.md#the-gitlab-ciyml-file) which also resides
in the repository's root directory.
#### Jobs
diff --git a/doc/ci/migration/jenkins.md b/doc/ci/migration/jenkins.md
index e9f39e2d7af..4352b495e7b 100644
--- a/doc/ci/migration/jenkins.md
+++ b/doc/ci/migration/jenkins.md
@@ -38,7 +38,7 @@ functionality.
### Configuration file
-Jenkins can be configured with a [`Jenkinsfile` in the Groovy format](https://www.jenkins.io/doc/book/pipeline/jenkinsfile/). GitLab CI/CD uses a [`.gitlab-ci.yml` YAML file](../../ci/yaml/gitlab_ci_yaml.md) by default.
+Jenkins can be configured with a [`Jenkinsfile` in the Groovy format](https://www.jenkins.io/doc/book/pipeline/jenkinsfile/). GitLab CI/CD uses a [`.gitlab-ci.yml` YAML file](../../ci/index.md#the-gitlab-ciyml-file) by default.
Example of a `Jenkinsfile`:
@@ -101,7 +101,7 @@ from Jenkins to GitLab CI/CD.
[Jenkins pipelines](https://www.jenkins.io/doc/book/pipeline/) generate automated CI/CD jobs
that are triggered when certain event take place, such as a new commit being pushed.
-A Jenkins pipeline is defined in a `Jenkinsfile`. The GitLab equivalent is the [`.gitlab-ci.yml` configuration file](../../ci/yaml/gitlab_ci_yaml.md).
+A Jenkins pipeline is defined in a `Jenkinsfile`. The GitLab equivalent is the [`.gitlab-ci.yml` configuration file](../../ci/index.md#the-gitlab-ciyml-file).
Jenkins does not provide a place to store source code, so the `Jenkinsfile` must be stored
in a separate source control repository.
diff --git a/doc/ci/pipelines/merge_request_pipelines.md b/doc/ci/pipelines/merge_request_pipelines.md
index 37febfd90ee..fb1c19d8770 100644
--- a/doc/ci/pipelines/merge_request_pipelines.md
+++ b/doc/ci/pipelines/merge_request_pipelines.md
@@ -262,3 +262,25 @@ Some possible reasons for this error message:
If **Run pipeline** is available, but the project does not have merge request pipelines
enabled, do not use this option. You can push a commit or rebase the branch to trigger
new branch pipelines.
+
+### `Merge blocked: pipeline must succeed. Push a new commit that fixes the failure` message
+
+This message is shown if the merge request pipeline, [merged results pipeline](merged_results_pipelines.md),
+or [merge train pipeline](merge_trains.md) has failed or been canceled.
+This does not happen when a branch pipeline fails.
+
+If a merge request pipeline or merged result pipeline was canceled or failed, you can:
+
+- Re-run the entire pipeline by selecting **Run pipeline** in the pipeline tab in the merge request.
+- [Retry only the jobs that failed](index.md#view-pipelines). If you re-run the entire pipeline, this is not necessary.
+- Push a new commit to fix the failure.
+
+If the merge train pipeline has failed, you can:
+
+- Check the failure and determine if you can use the [`/merge` quick action](../../user/project/quick_actions.md) to immediately add the merge request to the train again.
+- Re-run the entire pipeline by selecting **Run pipeline** in the pipeline tab in the merge request, then add the merge request to the train again.
+- Push a commit to fix the failure, then add the merge request to the train again.
+
+If the merge train pipeline was canceled before the merge request was merged, without a failure, you can:
+
+- Add it to the train again.
diff --git a/doc/ci/pipelines/merge_trains.md b/doc/ci/pipelines/merge_trains.md
index b7f081886a6..a54087262e7 100644
--- a/doc/ci/pipelines/merge_trains.md
+++ b/doc/ci/pipelines/merge_trains.md
@@ -90,6 +90,8 @@ are cancelled.
## Enable merge trains
+> `disable_merge_trains` feature flag [removed](https://gitlab.com/gitlab-org/gitlab/-/issues/282477) in GitLab 16.5.
+
Prerequisites:
- You must have the Maintainer role.
@@ -97,17 +99,15 @@ Prerequisites:
- Your pipeline must be [configured to use merge request pipelines](merge_request_pipelines.md#prerequisites).
Otherwise your merge requests may become stuck in an unresolved state or your pipelines
might be dropped.
+- You must have [merged results pipelines enabled](merged_results_pipelines.md#enable-merged-results-pipelines).
To enable merge trains:
1. On the left sidebar, select **Search or go to** and find your project.
1. Select **Settings > Merge requests**.
1. In the **Merge method** section, verify that **Merge commit** is selected.
-1. In the **Merge options** section:
- - In GitLab 13.6 and later, select **Enable merged results pipelines** and **Enable merge trains**.
- - In GitLab 13.5 and earlier, select **Enable merge trains and pipelines for merged results**.
- Additionally, [a feature flag](#disable-merge-trains-in-gitlab-135-and-earlier)
- must be set correctly.
+1. In the **Merge options** section, ensure **Enable merged results pipelines** is enabled
+ and select **Enable merge trains**.
1. Select **Save changes**.
## Start a merge train
@@ -174,31 +174,6 @@ WARNING:
Merging immediately can use a lot of CI/CD resources. Use this option
only in critical situations.
-## Disable merge trains in GitLab 13.5 and earlier **(PREMIUM SELF)**
-
-In [GitLab 13.6 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/244831),
-you can [enable or disable merge trains in the project settings](#enable-merge-trains).
-
-In GitLab 13.5 and earlier, merge trains are automatically enabled when
-[merged results pipelines](merged_results_pipelines.md) are enabled.
-To use merged results pipelines but not merge trains, enable the `disable_merge_trains`
-[feature flag](../../user/feature_flags.md).
-
-[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md)
-can enable the feature flag to disable merge trains:
-
-```ruby
-Feature.enable(:disable_merge_trains)
-```
-
-After you enable this feature flag, GitLab cancels existing merge trains.
-
-To disable the feature flag, which enables merge trains again:
-
-```ruby
-Feature.disable(:disable_merge_trains)
-```
-
## Troubleshooting
### Merge request dropped from the merge train
diff --git a/doc/ci/pipelines/merged_results_pipelines.md b/doc/ci/pipelines/merged_results_pipelines.md
index e4f739e8242..afe7a450370 100644
--- a/doc/ci/pipelines/merged_results_pipelines.md
+++ b/doc/ci/pipelines/merged_results_pipelines.md
@@ -61,19 +61,6 @@ Upgrade to 13.8 or later, or make sure the `:merge_ref_auto_sync`
[feature flag is enabled](../../administration/feature_flags.md#check-if-a-feature-flag-is-enabled)
on your GitLab instance.
-### Pipelines fail intermittently with a `fatal: reference is not a tree:` error
-
-Merged results pipelines run on a merge ref for a merge request
-(`refs/merge-requests/<iid>/merge`), so the Git reference could be overwritten at an
-unexpected time.
-
-For example, when a source or target branch is advanced, the pipeline fails with
-the `fatal: reference is not a tree:` error, which indicates that the checkout-SHA
-is not found in the merge ref.
-
-This behavior was improved in GitLab 12.4 by introducing [persistent pipeline refs](../troubleshooting.md#fatal-reference-is-not-a-tree-error).
-Upgrade to GitLab 12.4 or later to resolve the problem.
-
### Successful merged results pipeline overrides a failed branch pipeline
A failed branch pipeline is sometimes ignored when the
diff --git a/doc/ci/pipelines/settings.md b/doc/ci/pipelines/settings.md
index 265fd674190..321eae183eb 100644
--- a/doc/ci/pipelines/settings.md
+++ b/doc/ci/pipelines/settings.md
@@ -204,6 +204,7 @@ You can define how long a job can run before it times out.
1. Expand **General pipelines**.
1. In the **Timeout** field, enter the number of minutes, or a human-readable value like `2 hours`.
Must be 10 minutes or more, and less than one month. Default is 60 minutes.
+ Pending jobs are dropped after 24 hours of inactivity.
Jobs that exceed the timeout are marked as failed.
@@ -213,3 +214,26 @@ You can override this value [for individual runners](../runners/configure_runner
You can use [pipeline badges](../../user/project/badges.md) to indicate the pipeline status and
test coverage of your projects. These badges are determined by the latest successful pipeline.
+
+## Disable GitLab CI/CD pipelines
+
+GitLab CI/CD pipelines are enabled by default on all new projects. If you use an external CI/CD server like
+Jenkins or Drone CI, you can disable GitLab CI/CD to avoid conflicts with the commits status API.
+
+You can disable GitLab CI/CD per project or [for all new projects on an instance](../../administration/cicd.md).
+
+When you disable GitLab CI/CD:
+
+- The **CI/CD** item in the left sidebar is removed.
+- The `/pipelines` and `/jobs` pages are no longer available.
+- Existing jobs and pipelines are hidden, not removed.
+
+To disable GitLab CI/CD in your project:
+
+1. On the left sidebar, select **Search or go to** and find your project.
+1. Select **Settings > General**.
+1. Expand **Visibility, project features, permissions**.
+1. In the **Repository** section, turn off **CI/CD**.
+1. Select **Save changes**.
+
+These changes do not apply to projects in an [external integration](../../user/project/integrations/index.md#available-integrations).
diff --git a/doc/ci/quick_start/index.md b/doc/ci/quick_start/index.md
index 8e6fa965aa4..1f8c33a9700 100644
--- a/doc/ci/quick_start/index.md
+++ b/doc/ci/quick_start/index.md
@@ -9,7 +9,7 @@ type: reference
This tutorial shows you how to configure and run your first CI/CD pipeline in GitLab.
-If you are already familiar with basic CI/CD concepts, you can learn about
+If you are already familiar with [basic CI/CD concepts](../index.md), you can learn about
common keywords in [Tutorial: Create a complex pipeline](tutorial.md).
## Prerequisites
diff --git a/doc/ci/runners/new_creation_workflow.md b/doc/ci/runners/new_creation_workflow.md
index 3465aaf94fc..022f7af11ec 100644
--- a/doc/ci/runners/new_creation_workflow.md
+++ b/doc/ci/runners/new_creation_workflow.md
@@ -99,7 +99,7 @@ If you specify a runner authentication token with:
Authentication tokens have the prefix, `glrt-`.
To ensure minimal disruption to your automation workflow,
-[legacy-compatible registration processing](https://docs.gitlab.com/runner/register/#legacy-compatible-registration-processing)
+[legacy-compatible registration processing](https://docs.gitlab.com/runner/register/#legacy-compatible-registration-process)
triggers if a runner authentication token is specified in the legacy parameter `--registration-token`.
Example command for GitLab 15.9:
@@ -202,5 +202,22 @@ you can set it to any string - it will be ignored when `runner-token` is present
## Known issues
-- When you use the new registration workflow to register your runners with the Helm chart, the pod name is not visible
- in the runner details page. For more information, see [issue 423523](https://gitlab.com/gitlab-org/gitlab/-/issues/423523).
+### Pod name is not visible in runner details page
+
+When you use the new registration workflow to register your runners with the Helm chart, the pod name is not visible
+in the runner details page.
+For more information, see [issue 423523](https://gitlab.com/gitlab-org/gitlab/-/issues/423523).
+
+### Runner authentication token does not update when rotated
+
+When you use the new registration workflow to register your runners with the GitLab Operator,
+the runner authentication token referenced by the Custom Resource Definition does not update when the token is rotated.
+This occurs when:
+
+- You're using a runner authentication token (prefixed with `glrt-`) in a secret
+ [referenced by a Custom Resource Definition](https://docs.gitlab.com/runner/install/operator.html#install-gitlab-runner).
+- The runner authentication token is due to expire.
+ For more information about runner authentication token expiration,
+ see [Authentication token security](configure_runners.md#authentication-token-security).
+
+For more information, see [issue 186](https://gitlab.com/gitlab-org/gl-openshift/gitlab-runner-operator/-/issues/186).
diff --git a/doc/ci/runners/runners_scope.md b/doc/ci/runners/runners_scope.md
index 5341f19fbbc..6b6493db2c4 100644
--- a/doc/ci/runners/runners_scope.md
+++ b/doc/ci/runners/runners_scope.md
@@ -89,7 +89,7 @@ To create a shared runner:
1. Select **CI/CD > Runners**.
1. Select **Register an instance runner**.
1. Copy the registration token.
-1. [Register the runner](https://docs.gitlab.com/runner/register/).
+1. [Register the runner](https://docs.gitlab.com/runner/register/#register-with-a-runner-registration-token-deprecated).
### Pause or resume a shared runner
@@ -289,7 +289,7 @@ To create a group runner:
These instructions include the token, URL, and a command to register a runner.
Alternately, you can copy the registration token and follow the documentation for
-how to [register a runner](https://docs.gitlab.com/runner/register/).
+how to [register a runner](https://docs.gitlab.com/runner/register/#register-with-a-runner-registration-token-deprecated).
### View group runners
@@ -481,7 +481,7 @@ To create a project runner:
1. Select **Settings > CI/CD**.
1. Expand **Runners**.
1. In the **Project runners** section, note the URL and token.
-1. [Register the runner](https://docs.gitlab.com/runner/register/).
+1. [Register the runner](https://docs.gitlab.com/runner/register/#register-with-a-runner-registration-token-deprecated).
The runner is now enabled for the project.
diff --git a/doc/ci/runners/saas/linux_saas_runner.md b/doc/ci/runners/saas/linux_saas_runner.md
index c026ccf3d22..23a9b26a8d7 100644
--- a/doc/ci/runners/saas/linux_saas_runner.md
+++ b/doc/ci/runners/saas/linux_saas_runner.md
@@ -28,6 +28,8 @@ For Free, Premium, and Ultimate plan customers, jobs on these instances consume
The `small` machine type is set as default. If no [tag](../../yaml/index.md#tags) keyword in your `.gitlab-ci.yml` file is specified,
the jobs will run on this default runner.
+There are [different rates of compute minutes consumption](../../pipelines/cicd_minutes.md#additional-costs-on-gitlab-saas), based on the type of machine that is used.
+
All SaaS runners on Linux currently run on
[`n2d-standard`](https://cloud.google.com/compute/docs/general-purpose-machines#n2d_machines) general-purpose compute from GCP.
The machine type and underlying processor type can change. Jobs optimized for a specific processor design could behave inconsistently.
diff --git a/doc/ci/runners/saas/macos_saas_runner.md b/doc/ci/runners/saas/macos_saas_runner.md
index 1445ae58bd4..b503fea4f2f 100644
--- a/doc/ci/runners/saas/macos_saas_runner.md
+++ b/doc/ci/runners/saas/macos_saas_runner.md
@@ -34,34 +34,26 @@ In comparison to our SaaS runners on Linux, where you can run any Docker image,
GitLab SaaS provides a set of VM images for macOS.
You can execute your build in one of the following images, which you specify
-in your `.gitlab-ci.yml` file.
-
-Each image runs a specific version of macOS and Xcode.
+in your `.gitlab-ci.yml` file. Each image runs a specific version of macOS and Xcode.
| VM image | Status |
|----------------------------|--------|
-| `macos-12-xcode-13` | `GA` |
| `macos-12-xcode-14` | `GA` |
-| `macos-13-xcode-14` | `Beta` |
-
-## Image update policy for macOS
+| `macos-13-xcode-14` | `GA` |
+| `macos-14-xcode-15` | `Beta` |
-macOS and Xcode follow a yearly release cadence, during which GitLab increments its versions synchronously. GitLab typically supports multiple versions of preinstalled tools. For more information, see
-a [full list of preinstalled software](https://gitlab.com/gitlab-org/ci-cd/shared-runners/images/job-images/-/tree/main/toolchain).
+If no image is specified, the macOS runner uses `macos-13-xcode-14`.
-GitLab provides `stable` and `latest` macOS images that follow different update patterns:
+## Image update policy for macOS
-- **Stable image:** The `stable` images and installed components are updated every release. Images without the `:latest` prefix are considered stable images.
-- **Latest image:** The `latest` images are typically updated on a weekly cadence and use a `:latest` prefix in the image name. Using the `latest` image results in more regularly updated components and shorter update times for Homebrew or asdf. The `latest` images are used to test software components before releasing the components to the `stable` images.
-By definition, the `latest` images are always Beta.
-A `latest` image is not available.
+macOS and Xcode follow a yearly release cadence, during which GitLab increments its versions synchronously. GitLab typically supports multiple versions of preinstalled tools. For more information, see the [full list of preinstalled software](https://gitlab.com/gitlab-org/ci-cd/shared-runners/images/job-images/-/tree/main/toolchain).
-### Image release process
+When Apple releases a new macOS version, GitLab releases a new `stable` image based on the OS in the next release,
+which is in Beta.
-When Apple releases a new macOS version, GitLab releases both `stable` and `latest` images based on the OS in the next release. Both images are Beta.
+With the release of the first patch to macOS, the `stable` image becomes Generally Available (GA). As only two GA images are supported at a time, the prior OS version becomes deprecated and is deleted after three months in accordance with the [supported image lifecycle](../index.md#supported-image-lifecycle).
-With the release of the first patch to macOS, the `stable` image becomes Generally Available (GA).
-As only two GA images are supported at a time, the prior OS version becomes deprecated and is deleted after three months in accordance with the [supported image lifecycle](../index.md#supported-image-lifecycle).
+The `stable` images and installed components are updated every release, to keep the preinstalled software up-to-date.
## Example `.gitlab-ci.yml` file
diff --git a/doc/ci/secrets/azure_key_vault.md b/doc/ci/secrets/azure_key_vault.md
index 645ab5db0d1..d8a511e8bdf 100644
--- a/doc/ci/secrets/azure_key_vault.md
+++ b/doc/ci/secrets/azure_key_vault.md
@@ -9,14 +9,19 @@ type: concepts, howto
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/271271) in GitLab and GitLab Runner 16.3.
+NOTE:
+A [bug was discovered](https://gitlab.com/gitlab-org/gitlab/-/issues/424746) and this feature might not work as expected or at all. A fix is scheduled for a future release.
+
You can use secrets stored in the [Azure Key Vault](https://azure.microsoft.com/en-us/products/key-vault/)
in your GitLab CI/CD pipelines.
Prerequisites:
-- Have a key vault on Azure.
-- Have an application with key vault permissions.
-- [Configure OpenID Connect in Azure to retrieve temporary credentials](../../ci/cloud_services/azure/index.md).
+- Have a [Key Vault](https://learn.microsoft.com/en-us/azure/key-vault/general/quick-create-portal) on Azure.
+ - Your IAM user must be granted [granted the **Key Vault Administrator** role assignment](https://learn.microsoft.com/en-us/azure/role-based-access-control/quickstart-assign-role-user-portal#grant-access)
+ for the **resource group** assigned to the Key Vault. Otherwise, you can't create secrets inside the Key Vault.
+- [Configure OpenID Connect in Azure to retrieve temporary credentials](../../ci/cloud_services/azure/index.md). These
+ steps include instructions on how to create an Azure AD application for Key Vault access.
- Add [CI/CD variables to your project](../variables/index.md#for-a-project) to provide details about your Vault server:
- `AZURE_KEY_VAULT_SERVER_URL`: The URL of your Azure Key Vault server, such as `https://vault.example.com`.
- `AZURE_CLIENT_ID`: The client ID of the Azure application.
@@ -31,19 +36,64 @@ You can use a secret stored in your Azure Key Vault in a job by defining it with
job:
id_tokens:
AZURE_JWT:
- aud: 'azure'
+ aud: 'https://gitlab.com'
secrets:
DATABASE_PASSWORD:
- token: AZURE_JWT
+ token: $AZURE_JWT
azure_key_vault:
name: 'test'
- version: 'test'
+ version: '00000000000000000000000000000000'
```
In this example:
-- `name` is the name of the secret.
-- `version` is the version of the secret.
+- `aud` is the audience, which must match the audience used when [creating the federated identity credentials](../../ci/cloud_services/azure/index.md#create-azure-ad-federated-identity-credentials)
+- `name` is the name of the secret in Azure Key Vault.
+- `version` is the version of the secret in Azure Key Vault. The version is a generated
+ GUID without dashes, which can be found on the Azure Key Vault secrets page.
- GitLab fetches the secret from Azure Key Vault and stores the value in a temporary file.
The path to this file is stored in a `DATABASE_PASSWORD` CI/CD variable, similar to
[file type CI/CD variables](../variables/index.md#use-file-type-cicd-variables).
+
+## Troubleshooting
+
+Refer to [OIDC for Azure troubleshooting](../../ci/cloud_services/azure/index.md#troubleshooting) for general
+problems when setting up OIDC with Azure.
+
+### `JWT token is invalid or malformed` message
+
+You might receive this error when fetching secrets from Azure Key Vault:
+
+```plaintext
+RESPONSE 400 Bad Request
+AADSTS50027: JWT token is invalid or malformed.
+```
+
+This occurs due to a known issue in GitLab Runner where the JWT token isn't parsed correctly.
+A fix is [scheduled for a future GitLab Runner release](https://gitlab.com/gitlab-org/gitlab/-/issues/424746).
+
+### `Caller is not authorized to perform action on resource` message
+
+You might receive this error when fetching secrets from Azure Key Vault:
+
+```plaintext
+RESPONSE 403: 403 Forbidden
+ERROR CODE: Forbidden
+Caller is not authorized to perform action on resource.\r\nIf role assignments, deny assignments or role definitions were changed recently, please observe propagation time.
+ForbiddenByRbac
+```
+
+If your Azure Key Vault is using RBAC, you must add the **Key Vault Secrets User** to your Azure AD
+application.
+
+For example:
+
+```shell
+appId=$(az ad app list --display-name gitlab-oidc --query '[0].appId' -otsv)
+az role assignment create --assignee $appId --role "Key Vault Secrets User" --scope /subscriptions/<subscription-id>
+```
+
+You can find your subscription ID in:
+
+- The [Azure Portal](https://learn.microsoft.com/en-us/azure/azure-portal/get-subscription-tenant-id#find-your-azure-subscription).
+- The [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/manage-azure-subscriptions-azure-cli#get-the-active-subscription).
diff --git a/doc/ci/testing/browser_performance_testing.md b/doc/ci/testing/browser_performance_testing.md
index 9e81f243e50..d8c66c2d4d5 100644
--- a/doc/ci/testing/browser_performance_testing.md
+++ b/doc/ci/testing/browser_performance_testing.md
@@ -55,6 +55,8 @@ merge request targeting that branch.
## Configuring Browser Performance Testing
+> Support for the `SITESPEED_DOCKER_OPTIONS` variable [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134024) in GitLab 16.6.
+
This example shows how to run the [sitespeed.io container](https://hub.docker.com/r/sitespeedio/sitespeed.io/)
on your code by using GitLab CI/CD and [sitespeed.io](https://www.sitespeed.io)
using Docker-in-Docker.
@@ -91,6 +93,7 @@ You can also customize the jobs with CI/CD variables:
- `SITESPEED_IMAGE`: Configure the Docker image to use for the job (default `sitespeedio/sitespeed.io`), but not the image version.
- `SITESPEED_VERSION`: Configure the version of the Docker image to use for the job (default `14.1.0`).
- `SITESPEED_OPTIONS`: Configure any additional sitespeed.io options as required (default `nil`). Refer to the [sitespeed.io documentation](https://www.sitespeed.io/documentation/sitespeed.io/configuration/) for more details.
+- `SITESPEED_DOCKER_OPTIONS`: Configure any additional Docker options (default `nil`). Refer to the [Docker options documentation](https://docs.docker.com/engine/reference/commandline/run/#options) for more details.
For example, you can override the number of runs sitespeed.io
makes on the given URL, and change the version:
diff --git a/doc/ci/testing/code_coverage.md b/doc/ci/testing/code_coverage.md
index fb846f52a72..a39586a9eb0 100644
--- a/doc/ci/testing/code_coverage.md
+++ b/doc/ci/testing/code_coverage.md
@@ -40,7 +40,10 @@ using the [`coverage`](../yaml/index.md#coverage) keyword.
#### Test coverage examples
-Use this regex for commonly used test tools.
+The following list shows sample regex patterns for many common test coverage tools.
+If the tooling has changed after these samples were created, or if the tooling was customized,
+the regex might not work. Test the regex carefully to make sure it correctly finds the
+coverage in the tool's output:
<!-- vale gitlab.Spelling = NO -->
diff --git a/doc/ci/testing/code_quality.md b/doc/ci/testing/code_quality.md
index 1d857b8f543..6b4275d8055 100644
--- a/doc/ci/testing/code_quality.md
+++ b/doc/ci/testing/code_quality.md
@@ -12,6 +12,8 @@ Use Code Quality to analyze your source code's quality and complexity. This help
project's code simple, readable, and easier to maintain. Code Quality should supplement your
other review processes, not replace them.
+Code Quality runs in CI/CD pipelines, and helps you avoid merging changes that would degrade your code's quality.
+
Code Quality uses the open source Code Climate tool, and selected
[plugins](https://docs.codeclimate.com/docs/list-of-engines), to analyze your source code.
To confirm if your code's languages are covered, see the Code Climate list of
@@ -20,9 +22,6 @@ You can extend the code coverage either by using Code Climate
[Analysis Plugins](https://docs.codeclimate.com/docs/list-of-engines) or a
[custom tool](#implement-a-custom-tool).
-Run Code Quality reports in your CI/CD pipeline to verify changes don't degrade your code's quality,
-_before_ committing them to the default branch.
-
## Features per tier
Different features are available in different [GitLab tiers](https://about.gitlab.com/pricing/),
@@ -344,9 +343,9 @@ code_quality:
> [Introduced](https://gitlab.com/gitlab-org/ci-cd/codequality/-/merge_requests/30) in GitLab 13.7.
Using a private container image registry can reduce the time taken to download images, and also
-reduce external dependencies. Because of the nested architecture of container execution, the
-registry prefix must be specifically configured to be passed down into CodeClimate's subsequent
-`docker pull` commands for individual engines.
+reduce external dependencies. You must configure the registry prefix to be passed down
+to CodeClimate's subsequent `docker pull` commands for individual engines, because of
+the nested method of container execution.
The following variables can address all of the required image pulls:
@@ -710,3 +709,39 @@ Replace `gitlab.example.com` with the actual domain of the registry.
mount_path = "/etc/docker/certs.d/gitlab.example.com/ca.crt"
sub_path = "gitlab.example.com.crt"
```
+
+### Failed to load Code Quality report
+
+The Code Quality report can fail to load when there are issues parsing data from the artifact file.
+To gain insight into the errors, you can execute a GraphQL query using the following steps:
+
+1. Go to the pipeline details page.
+1. Append `.json` to the URL.
+1. Copy the `iid` of the pipeline.
+1. Go to [GraphiQL explorer](../../api/graphql/index.md#graphiql).
+1. Run the following query:
+
+ ```graphql
+ {
+ project(fullPath: "<fullpath-to-your-project>") {
+ pipeline(iid: "<iid>") {
+ codeQualityReports {
+ count
+ nodes {
+ line
+ description
+ path
+ fingerprint
+ severity
+ }
+ pageInfo {
+ hasNextPage
+ hasPreviousPage
+ startCursor
+ endCursor
+ }
+ }
+ }
+ }
+ }
+ ```
diff --git a/doc/ci/triggers/index.md b/doc/ci/triggers/index.md
index 698118f457f..ee1e05c4fc9 100644
--- a/doc/ci/triggers/index.md
+++ b/doc/ci/triggers/index.md
@@ -14,6 +14,7 @@ When authenticating with the API, you can use:
- A [pipeline trigger token](#create-a-pipeline-trigger-token) to trigger a branch or tag pipeline.
- A [CI/CD job token](../jobs/ci_job_token.md) to [trigger a multi-project pipeline](../pipelines/downstream_pipelines.md#trigger-a-multi-project-pipeline-by-using-the-api).
+- A [personal access token](../../user/profile/personal_access_tokens.md).
## Create a pipeline trigger token
diff --git a/doc/ci/troubleshooting.md b/doc/ci/troubleshooting.md
index cc7e5594466..77ee6b11d92 100644
--- a/doc/ci/troubleshooting.md
+++ b/doc/ci/troubleshooting.md
@@ -1,555 +1,11 @@
---
-stage: Verify
-group: Pipeline Authoring
-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: reference
+redirect_to: 'debugging.md'
+remove_date: '2024-02-01'
---
-# Troubleshooting CI/CD **(FREE ALL)**
+This document was moved to [another location](debugging.md).
-GitLab provides several tools to help make troubleshooting your pipelines easier.
-
-This guide also lists common issues and possible solutions.
-
-## Verify syntax
-
-An early source of problems can be incorrect syntax. The pipeline shows a `yaml invalid`
-badge and does not start running if any syntax or formatting problems are found.
-
-### Edit `.gitlab-ci.yml` with the pipeline editor
-
-The [pipeline editor](pipeline_editor/index.md) is the recommended editing
-experience (rather than the single file editor or the Web IDE). It includes:
-
-- Code completion suggestions that ensure you are only using accepted keywords.
-- Automatic syntax highlighting and validation.
-- The [CI/CD configuration visualization](pipeline_editor/index.md#visualize-ci-configuration),
- a graphical representation of your `.gitlab-ci.yml` file.
-
-### Edit `.gitlab-ci.yml` locally
-
-If you prefer to edit your pipeline configuration locally, you can use the
-GitLab CI/CD schema in your editor to verify basic syntax issues. Any
-[editor with Schemastore support](https://www.schemastore.org/json/#editors) uses
-the GitLab CI/CD schema by default.
-
-If you need to link to the schema directly, it
-is at:
-
-```plaintext
-https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/assets/javascripts/editor/schema/ci.json
-```
-
-To see the full list of custom tags covered by the CI/CD schema, check the
-latest version of the schema.
-
-### Verify syntax with CI Lint tool
-
-The [CI Lint tool](lint.md) is a simple way to ensure the syntax of a CI/CD configuration
-file is correct. Paste in full `.gitlab-ci.yml` files or individual jobs configuration,
-to verify the basic syntax.
-
-When a `.gitlab-ci.yml` file is present in a project, you can also use the CI Lint
-tool to [simulate the creation of a full pipeline](lint.md#simulate-a-pipeline).
-It does deeper verification of the configuration syntax.
-
-## Verify variables
-
-A key part of troubleshooting CI/CD is to verify which variables are present in a
-pipeline, and what their values are. A lot of pipeline configuration is dependent
-on variables, and verifying them is one of the fastest ways to find the source of
-a problem.
-
-[Export the full list of variables](variables/index.md#list-all-variables)
-available in each problematic job. Check if the variables you expect are present,
-and check if their values are what you expect.
-
-## GitLab CI/CD documentation
-
-The [complete `.gitlab-ci.yml` reference](yaml/index.md) contains a full list of
-every keyword you can use to configure your pipelines.
-
-You can also look at a large number of pipeline configuration [examples](examples/index.md)
-and [templates](examples/index.md#cicd-templates).
-
-### Documentation for pipeline types
-
-Branch pipelines are the most basic type.
-Other pipeline types have their own detailed usage guides that you should read
-if you are using that type:
-
-- [Multi-project pipelines](pipelines/downstream_pipelines.md#multi-project-pipelines): Have your pipeline trigger
- a pipeline in a different project.
-- [Parent/child pipelines](pipelines/downstream_pipelines.md#parent-child-pipelines): Have your main pipeline trigger
- and run separate pipelines in the same project. You can also
- [dynamically generate the child pipeline's configuration](pipelines/downstream_pipelines.md#dynamic-child-pipelines)
- at runtime.
-- [Merge request pipelines](pipelines/merge_request_pipelines.md): Run a pipeline
- in the context of a merge request.
- - [Merged results pipelines](pipelines/merged_results_pipelines.md):
- Merge request pipelines that run on the combined source and target branch
- - [Merge trains](pipelines/merge_trains.md):
- Multiple merged results pipelines that queue and run automatically before
- changes are merged.
-
-### Troubleshooting Guides for CI/CD features
-
-Troubleshooting guides are available for some CI/CD features and related topics:
-
-- [Container Registry](../user/packages/container_registry/troubleshoot_container_registry.md)
-- [GitLab Runner](https://docs.gitlab.com/runner/faq/)
-- [Merge Trains](pipelines/merge_trains.md#troubleshooting)
-- [Docker Build](docker/using_docker_build.md#troubleshooting)
-- [Environments](environments/deployment_safety.md#ensure-only-one-deployment-job-runs-at-a-time)
-
-## Common CI/CD issues
-
-A lot of common pipeline issues can be fixed by analyzing the behavior of the `rules`
-or `only/except` configuration. You shouldn't use these two configurations in the same
-pipeline, as they behave differently. It's hard to predict how a pipeline runs with
-this mixed behavior.
-
-If your `rules` or `only/except` configuration makes use of [predefined variables](variables/predefined_variables.md)
-like `CI_PIPELINE_SOURCE`, `CI_MERGE_REQUEST_ID`, you should [verify them](#verify-variables)
-as the first troubleshooting step.
-
-### Jobs or pipelines don't run when expected
-
-The `rules` or `only/except` keywords are what determine whether or not a job is
-added to a pipeline. If a pipeline runs, but a job is not added to the pipeline,
-it's usually due to `rules` or `only/except` configuration issues.
-
-If a pipeline does not seem to run at all, with no error message, it may also be
-due to `rules` or `only/except` configuration, or the `workflow: rules` keyword.
-
-If you are converting from `only/except` to the `rules` keyword, you should check
-the [`rules` configuration details](yaml/index.md#rules) carefully. The behavior
-of `only/except` and `rules` is different and can cause unexpected behavior when migrating
-between the two.
-
-The [common `if` clauses for `rules`](jobs/job_control.md#common-if-clauses-for-rules)
-can be very helpful for examples of how to write rules that behave the way you expect.
-
-#### Two pipelines run at the same time
-
-Two pipelines can run when pushing a commit to a branch that has an open merge request
-associated with it. Usually one pipeline is a merge request pipeline, and the other
-is a branch pipeline.
-
-This situation is usually caused by the `rules` configuration, and there are several ways to
-[prevent duplicate pipelines](jobs/job_control.md#avoid-duplicate-pipelines).
-
-#### A job is not in the pipeline
-
-GitLab determines if a job is added to a pipeline based on the [`only/except`](yaml/index.md#only--except)
-or [`rules`](yaml/index.md#rules) defined for the job. If it didn't run, it's probably
-not evaluating as you expect.
-
-#### No pipeline or the wrong type of pipeline runs
-
-Before a pipeline can run, GitLab evaluates all the jobs in the configuration and tries
-to add them to all available pipeline types. A pipeline does not run if no jobs are added
-to it at the end of the evaluation.
-
-If a pipeline did not run, it's likely that all the jobs had `rules` or `only/except` that
-blocked them from being added to the pipeline.
-
-If the wrong pipeline type ran, then the `rules` or `only/except` configuration should
-be checked to make sure the jobs are added to the correct pipeline type. For
-example, if a merge request pipeline did not run, the jobs may have been added to
-a branch pipeline instead.
-
-It's also possible that your [`workflow: rules`](yaml/index.md#workflow) configuration
-blocked the pipeline, or allowed the wrong pipeline type.
-
-### Pipeline with many jobs fails to start
-
-A Pipeline that has more jobs than the instance's defined [CI/CD limits](../administration/settings/continuous_integration.md#set-cicd-limits)
-fails to start.
-
-To reduce the number of jobs in your pipeline, you can split your `.gitlab-ci.yml`
-configuration using [parent-child pipelines](../ci/pipelines/pipeline_architectures.md#parent-child-pipelines).
-
-### A job runs unexpectedly
-
-A common reason a job is added to a pipeline unexpectedly is because the `changes`
-keyword always evaluates to true in certain cases. For example, `changes` is always
-true in certain pipeline types, including scheduled pipelines and pipelines for tags.
-
-The `changes` keyword is used in combination with [`only/except`](yaml/index.md#onlychanges--exceptchanges)
-or [`rules`](yaml/index.md#ruleschanges)). It's recommended to use `changes` with
-`rules` or `only/except` configuration that ensures the job is only added to branch
-pipelines or merge request pipelines.
-
-### "fatal: reference is not a tree" error
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/17043) in GitLab 12.4.
-
-Previously, you'd have encountered unexpected pipeline failures when you force-pushed
-a branch to its remote repository. To illustrate the problem, suppose you've had the current workflow:
-
-1. A user creates a feature branch named `example` and pushes it to a remote repository.
-1. A new pipeline starts running on the `example` branch.
-1. A user rebases the `example` branch on the latest default branch and force-pushes it to its remote repository.
-1. A new pipeline starts running on the `example` branch again, however,
- the previous pipeline (2) fails because of `fatal: reference is not a tree:` error.
-
-This occurs because the previous pipeline cannot find a checkout-SHA (which is associated with the pipeline record)
-from the `example` branch that the commit history has already been overwritten by the force-push.
-Similarly, [Merged results pipelines](pipelines/merged_results_pipelines.md)
-might have failed intermittently due to [the same reason](pipelines/merged_results_pipelines.md#pipelines-fail-intermittently-with-a-fatal-reference-is-not-a-tree-error).
-
-As of GitLab 12.4, we've improved this behavior by persisting pipeline refs exclusively.
-To illustrate its life cycle:
-
-1. A pipeline is created on a feature branch named `example`.
-1. A persistent pipeline ref is created at `refs/pipelines/<pipeline-id>`,
- which retains the checkout-SHA of the associated pipeline record.
- This persistent ref stays intact during the pipeline execution,
- even if the commit history of the `example` branch has been overwritten by force-push.
-1. The runner fetches the persistent pipeline ref and gets source code from the checkout-SHA.
-1. When the pipeline finishes, its persistent ref is cleaned up in a background process.
-
-### `get_sources` job section fails because of an HTTP/2 problem
-
-Sometimes, jobs fail with the following cURL error:
-
-```plaintext
-++ git -c 'http.userAgent=gitlab-runner <version>' fetch origin +refs/pipelines/<id>:refs/pipelines/<id> ...
-error: RPC failed; curl 16 HTTP/2 send again with decreased length
-fatal: ...
-```
-
-You can work around this problem by configuring Git and `libcurl` to
-[use HTTP/1.1](https://git-scm.com/docs/git-config#Documentation/git-config.txt-httpversion).
-The configuration can be added to:
-
-- A job's [`pre_get_sources_script`](yaml/index.md#hookspre_get_sources_script):
-
- ```yaml
- job_name:
- hooks:
- pre_get_sources_script:
- - git config --global http.version "HTTP/1.1"
- ```
-
-- The [runner's `config.toml`](https://docs.gitlab.com/runner/configuration/advanced-configuration.html)
- with [Git configuration environment variables](https://git-scm.com/docs/git-config#ENVIRONMENT):
-
- ```toml
- [[runners]]
- ...
- environment = [
- "GIT_CONFIG_COUNT=1",
- "GIT_CONFIG_KEY_0=http.version",
- "GIT_CONFIG_VALUE_0=HTTP/1.1"
- ]
- ```
-
-### Merge request pipeline messages
-
-The merge request pipeline widget shows information about the pipeline status in
-a merge request. It's displayed above the [ability to merge status widget](#merge-request-status-messages).
-
-#### "Checking ability to merge automatically" message
-
-There is a [known issue](https://gitlab.com/gitlab-org/gitlab/-/issues/229352)
-where a merge request can be stuck with the `Checking ability to merge automatically`
-message.
-
-If your merge request has this message and it does not disappear after a few minutes,
-you can try one of these workarounds:
-
-- Refresh the merge request page.
-- Close & Re-open the merge request.
-- Rebase the merge request with the `/rebase` [quick action](../user/project/quick_actions.md).
-- If you have already confirmed the merge request is ready to be merged, you can merge
- it with the `/merge` quick action.
-
-#### "Checking pipeline status" message
-
-This message is shown when the merge request has no pipeline associated with the
-latest commit yet. This might be because:
-
-- GitLab hasn't finished creating the pipeline yet.
-- You are using an external CI service and GitLab hasn't heard back from the service yet.
-- You are not using CI/CD pipelines in your project.
-- You are using CI/CD pipelines in your project, but your configuration prevented a pipeline from running on the source branch for your merge request.
-- The latest pipeline was deleted (this is a [known issue](https://gitlab.com/gitlab-org/gitlab/-/issues/214323)).
-- The source branch of the merge request is on a private fork.
-
-After the pipeline is created, the message updates with the pipeline status.
-
-### Merge request status messages
-
-The merge request status widget shows:
-
-- If the merge request is ready to merge. If the merge request can't be merged, the reason is displayed.
-- **Merge**, if the pipeline is complete, or **Set to auto-merge** if the pipeline is still running.
-
-#### "A CI/CD pipeline must run and be successful before merge" message
-
-This message is shown if the [Pipelines must succeed](../user/project/merge_requests/merge_when_pipeline_succeeds.md#require-a-successful-pipeline-for-merge)
-setting is enabled in the project and a pipeline has not yet run successfully.
-This also applies if the pipeline has not been created yet, or if you are waiting
-for an external CI service. If you don't use pipelines for your project, then you
-should disable **Pipelines must succeed** so you can accept merge requests.
-
-#### "Merge blocked: pipeline must succeed. Push a new commit that fixes the failure" message
-
-This message is shown if the [merge request pipeline](pipelines/merge_request_pipelines.md),
-[merged results pipeline](pipelines/merged_results_pipelines.md),
-or [merge train pipeline](pipelines/merge_trains.md)
-has failed or been canceled.
-This does not happen when a basic branch pipeline fails.
-
-If a merge request pipeline or merged result pipeline was canceled or failed, you can:
-
-- Re-run the entire pipeline by selecting **Run pipeline** in the pipeline tab in the merge request.
-- [Retry only the jobs that failed](pipelines/index.md#view-pipelines). If you re-run the entire pipeline, this is not necessary.
-- Push a new commit to fix the failure.
-
-If the merge train pipeline has failed, you can:
-
-- Check the failure and determine if you can use the [`/merge` quick action](../user/project/quick_actions.md) to immediately add the merge request to the train again.
-- Re-run the entire pipeline by selecting **Run pipeline** in the pipeline tab in the merge request, then add the merge request to the train again.
-- Push a commit to fix the failure, then add the merge request to the train again.
-
-If the merge train pipeline was canceled before the merge request was merged, without a failure, you can:
-
-- Add it to the train again.
-
-### Merge request rules widget shows a scan result policy is invalid or duplicated **(ULTIMATE SELF)**
-
-On GitLab self-managed 15.0 and later, the most likely cause is that the project was exported from a
-group and imported into another, and had scan result policy rules. These rules are stored in a
-separate project to the one that was exported. As a result, the project contains policy rules that
-reference entities that don't exist in the imported project's group. The result is policy rules that
-are invalid, duplicated, or both.
-
-To remove all invalid scan result policy rules from a GitLab instance, an administrator can run
-the following script in the [Rails console](../administration/operations/rails_console.md).
-
-```ruby
-Project.joins(:approval_rules).where(approval_rules: { report_type: %i[scan_finding license_scanning] }).where.not(approval_rules: { security_orchestration_policy_configuration_id: nil }).find_in_batches.flat_map do |batch|
- batch.map do |project|
- # Get projects and their configuration_ids for applicable project rules
- [project, project.approval_rules.where(report_type: %i[scan_finding license_scanning]).pluck(:security_orchestration_policy_configuration_id).uniq]
- end.uniq.map do |project, configuration_ids| # We take only unique combinations of project + configuration_ids
- # If we find more configurations than what is available for the project, we take records with the extra configurations
- [project, configuration_ids - project.all_security_orchestration_policy_configurations.pluck(:id)]
- end.select { |_project, configuration_ids| configuration_ids.any? }
-end.each do |project, configuration_ids|
- # For each found pair project + ghost configuration, we remove these rules for a given project
- Security::OrchestrationPolicyConfiguration.where(id: configuration_ids).each do |configuration|
- configuration.delete_scan_finding_rules_for_project(project.id)
- end
- # Ensure we sync any potential rules from new group's policy
- Security::ScanResultPolicies::SyncProjectWorker.perform_async(project.id)
-end
-```
-
-### Project `group/project` not found or access denied
-
-This message is shown if configuration is added with [`include`](yaml/index.md#include) and one of the following:
-
-- The configuration refers to a project that can't be found.
-- The user that is running the pipeline is unable to access any included projects.
-
-To resolve this, check that:
-
-- The path of the project is in the format `my-group/my-project` and does not include
- any folders in the repository.
-- The user running the pipeline is a [member of the projects](../user/project/members/index.md#add-users-to-a-project)
- that contain the included files. Users must also have the [permission](../user/permissions.md#job-permissions)
- to run CI/CD jobs in the same projects.
-
-### "The parsed YAML is too big" message
-
-This message displays when the YAML configuration is too large or nested too deeply.
-YAML files with a large number of includes, and thousands of lines overall, are
-more likely to hit this memory limit. For example, a YAML file that is 200kb is
-likely to hit the default memory limit.
-
-To reduce the configuration size, you can:
-
-- Check the length of the expanded CI/CD configuration in the pipeline editor's
- [Full configuration](pipeline_editor/index.md#view-full-configuration) tab. Look for
- duplicated configuration that can be removed or simplified.
-- Move long or repeated `script` sections into standalone scripts in the project.
-- Use [parent and child pipelines](pipelines/downstream_pipelines.md#parent-child-pipelines) to move some
- work to jobs in an independent child pipeline.
-
-On a self-managed instance, you can [increase the size limits](../administration/instance_limits.md#maximum-size-and-depth-of-cicd-configuration-yaml-files).
-
-### Error 500 when editing the `.gitlab-ci.yml` file
-
-A [loop of included configuration files](pipeline_editor/index.md#configuration-validation-currently-not-available-message)
-can cause a `500` error when editing the `.gitlab-ci.yml` file with the [web editor](../user/project/repository/web_editor.md).
-
-### A CI/CD job does not use newer configuration when run again
-
-The configuration for a pipeline is only fetched when the pipeline is created.
-When you rerun a job, uses the same configuration each time. If you update configuration files,
-including separate files added with [`include`](yaml/index.md#include), you must
-start a new pipeline to use the new configuration.
-
-### Unable to pull image from another project
-
-> **Allow access to this project with a CI_JOB_TOKEN** setting [renamed to **Limit access _to_ this project**](https://gitlab.com/gitlab-org/gitlab/-/issues/411406) in GitLab 16.3.
-
-When a runner tries to pull an image from a private project, the job could fail with the following error:
-
-```shell
-WARNING: Failed to pull image with policy "always": Error response from daemon: pull access denied for registry.example.com/path/to/project, repository does not exist or may require 'docker login': denied: requested access to the resource is denied
-```
-
-This error can happen if the following are both true:
-
-- The **Limit access _to_ this project** option is enabled in the private project
- hosting the image.
-- The job attempting to fetch the image is running for a project that is not listed in
- the private project's allowlist.
-
-The recommended solution is to [add your project to the private project's job token scope allowlist](jobs/ci_job_token.md#add-a-project-to-the-job-token-scope-allowlist).
-
-## Pipeline warnings
-
-Pipeline configuration warnings are shown when you:
-
-- [Validate configuration with the CI Lint tool](yaml/index.md).
-- [Manually run a pipeline](pipelines/index.md#run-a-pipeline-manually).
-
-### "Job may allow multiple pipelines to run for a single action" warning
-
-When you use [`rules`](yaml/index.md#rules) with a `when` clause without an `if`
-clause, multiple pipelines may run. Usually this occurs when you push a commit to
-a branch that has an open merge request associated with it.
-
-To [prevent duplicate pipelines](jobs/job_control.md#avoid-duplicate-pipelines), use
-[`workflow: rules`](yaml/index.md#workflow) or rewrite your rules to control
-which pipelines can run.
-
-### Console workaround if job using `resource_group` gets stuck **(FREE SELF)**
-
-```ruby
-# find resource group by name
-resource_group = Project.find_by_full_path('...').resource_groups.find_by(key: 'the-group-name')
-busy_resources = resource_group.resources.where('build_id IS NOT NULL')
-
-# identify which builds are occupying the resource
-# (I think it should be 1 as of today)
-busy_resources.pluck(:build_id)
-
-# it's good to check why this build is holding the resource.
-# Is it stuck? Has it been forcefully dropped by the system?
-# free up busy resources
-busy_resources.update_all(build_id: nil)
-```
-
-### Job log slow to update
-
-When you visit the job log page for a running job, there could be a delay of up to
-60 seconds before the log updates. The default refresh time is 60 seconds, but after
-the log is viewed in the UI, the following log updates should occur every 3 seconds.
-
-## Disaster recovery
-
-You can disable some important but computationally expensive parts of the application
-to relieve stress on the database during ongoing downtime.
-
-### Disable fair scheduling on shared runners
-
-When clearing a large backlog of jobs, you can temporarily enable the `ci_queueing_disaster_recovery_disable_fair_scheduling`
-[feature flag](../administration/feature_flags.md). This flag disables fair scheduling
-on shared runners, which reduces system resource usage on the `jobs/request` endpoint.
-
-When enabled, jobs are processed in the order they were put in the system, instead of
-balanced across many projects.
-
-### Disable compute quota enforcement
-
-To disable the enforcement of [compute quotas](pipelines/cicd_minutes.md) on shared runners, you can temporarily
-enable the `ci_queueing_disaster_recovery_disable_quota` [feature flag](../administration/feature_flags.md).
-This flag reduces system resource usage on the `jobs/request` endpoint.
-
-When enabled, jobs created in the last hour can run in projects which are out of quota.
-Earlier jobs are already canceled by a periodic background worker (`StuckCiJobsWorker`).
-
-## CI/CD troubleshooting Rails console commands
-
-The following commands are run in the [Rails console](../administration/operations/rails_console.md#starting-a-rails-console-session).
-
-WARNING:
-Any command that changes data directly could be damaging if not run correctly, or under the right conditions.
-We highly recommend running them in a test environment with a backup of the instance ready to be restored, just in case.
-
-### Cancel stuck pending pipelines
-
-```ruby
-project = Project.find_by_full_path('<project_path>')
-Ci::Pipeline.where(project_id: project.id).where(status: 'pending').count
-Ci::Pipeline.where(project_id: project.id).where(status: 'pending').each {|p| p.cancel if p.stuck?}
-Ci::Pipeline.where(project_id: project.id).where(status: 'pending').count
-```
-
-### Try merge request integration
-
-```ruby
-project = Project.find_by_full_path('<project_path>')
-mr = project.merge_requests.find_by(iid: <merge_request_iid>)
-mr.project.try(:ci_integration)
-```
-
-### Validate the `.gitlab-ci.yml` file
-
-```ruby
-project = Project.find_by_full_path('<project_path>')
-content = p.repository.gitlab_ci_yml_for(project.repository.root_ref_sha)
-Gitlab::Ci::Lint.new(project: project, current_user: User.first).validate(content)
-```
-
-### Disable AutoDevOps on Existing Projects
-
-```ruby
-Project.all.each do |p|
- p.auto_devops_attributes={"enabled"=>"0"}
- p.save
-end
-```
-
-### Obtain runners registration token
-
-```ruby
-Gitlab::CurrentSettings.current_application_settings.runners_registration_token
-```
-
-### Seed runners registration token
-
-```ruby
-appSetting = Gitlab::CurrentSettings.current_application_settings
-appSetting.set_runners_registration_token('<new-runners-registration-token>')
-appSetting.save!
-```
-
-### Run pipeline schedules manually
-
-You can run pipeline schedules manually through the Rails console to reveal any errors that are usually not visible.
-
-```ruby
-# schedule_id can be obtained from Edit Pipeline Schedule page
-schedule = Ci::PipelineSchedule.find_by(id: <schedule_id>)
-
-# Select the user that you want to run the schedule for
-user = User.find_by_username('<username>')
-
-# Run the schedule
-ps = Ci::CreatePipelineService.new(schedule.project, user, ref: schedule.ref).execute!(:schedule, ignore_skip_ci: true, save_on_errors: false, schedule: schedule)
-```
-
-## How to get help
-
-If you are unable to resolve pipeline issues, you can get help from:
-
-- The [GitLab community forum](https://forum.gitlab.com/)
-- GitLab [Support](https://about.gitlab.com/support/)
+<!-- This redirect file can be deleted after <2024-02-01>. -->
+<!-- Redirects that point to other docs in the same project expire in three months. -->
+<!-- Redirects that point to docs in a different project or site (link is not relative and starts with `https:`) expire in one year. -->
+<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/redirects.html -->
diff --git a/doc/ci/variables/index.md b/doc/ci/variables/index.md
index 975157ff917..0c05129fb1e 100644
--- a/doc/ci/variables/index.md
+++ b/doc/ci/variables/index.md
@@ -278,11 +278,15 @@ The method used to mask variables [limits what can be included in a masked varia
The value of the variable must:
- Be a single line.
-- Be 8 characters or longer, consisting only of:
- - Characters from the Base64 alphabet (RFC4648).
- - The `@`, `:`, `.`, or `~` characters.
+- Be 8 characters or longer.
- Not match the name of an existing predefined or custom CI/CD variable.
+Additionally, if [variable expansion](#prevent-cicd-variable-expansion) is enabled,
+the value can contain only:
+
+- Characters from the Base64 alphabet (RFC4648).
+- The `@`, `:`, `.`, or `~` characters.
+
Different versions of [GitLab Runner](../runners/index.md) have different masking limitations:
| Version | Limitations |
@@ -703,6 +707,68 @@ to enable the `restrict_user_defined_variables` setting. The setting is `disable
If you [store your CI/CD configurations in a different repository](../../ci/pipelines/settings.md#specify-a-custom-cicd-configuration-file),
use this setting for control over the environment the pipeline runs in.
+## Exporting variables
+
+Scripts executed in separate shell contexts do not share exports, aliases,
+local function definitions, or any other local shell updates.
+
+This means that if a job fails, variables created by user-defined scripts are not
+exported.
+
+When runners execute jobs defined in `.gitlab-ci.yml`:
+
+- Scripts specified in `before_script` and the main script are executed together in
+ a single shell context, and are concatenated.
+- Scripts specified in `after_script` run in a shell context completely separate to
+ the `before_script` and the specified scripts.
+
+Regardless of the shell the scripts are executed in, the runner output includes:
+
+- Predefined variables.
+- Variables defined in:
+ - Instance, group, or project CI/CD settings.
+ - The `.gitlab-ci.yml` file in the `variables:` section.
+ - The `.gitlab-ci.yml` file in the `secrets:` section.
+ - The `config.toml`.
+
+The runner cannot handle manual exports, shell aliases, and functions executed in the body of the script, like `export MY_VARIABLE=1`.
+
+For example, in the following `.gitlab-ci.yml` file, the following scripts are defined:
+
+```yaml
+ job:
+ variables:
+ JOB_DEFINED_VARIABLE: "job variable"
+ before_script:
+ - echo "This is the 'before_script' script"
+ - export MY_VARIABLE="variable"
+ script:
+ - echo "This is the 'script' script"
+ - echo "JOB_DEFINED_VARIABLE's value is ${JOB_DEFINED_VARIABLE}"
+ - echo "CI_COMMIT_SHA's value is ${CI_COMMIT_SHA}"
+ - echo "MY_VARIABLE's value is ${MY_VARIABLE}"
+ after_script:
+ - echo "JOB_DEFINED_VARIABLE's value is ${JOB_DEFINED_VARIABLE}"
+ - echo "CI_COMMIT_SHA's value is ${CI_COMMIT_SHA}"
+ - echo "MY_VARIABLE's value is ${MY_VARIABLE}"
+```
+
+When the runner executes the job:
+
+1. `before_script` is executed:
+ 1. Prints to the output.
+ 1. Defines the variable for `MY_VARIABLE`.
+1. `script` is executed:
+ 1. Prints to the output.
+ 1. Prints the value of `JOB_DEFINED_VARIABLE`.
+ 1. Prints the value of `CI_COMMIT_SHA`.
+ 1. Prints the value of `MY_VARIABLE`.
+1. `after_script` is executed in a new, separate shell context:
+ 1. Prints to the output.
+ 1. Prints the value of `JOB_DEFINED_VARIABLE`.
+ 1. Prints the value of `CI_COMMIT_SHA`.
+ 1. Prints an empty value of `MY_VARIABLE`. The variable value cannot be detected because `after_script` is in a separate shell context to `before_script`.
+
## Related topics
- You can configure [Auto DevOps](../../topics/autodevops/index.md) to pass CI/CD variables
diff --git a/doc/ci/variables/predefined_variables.md b/doc/ci/variables/predefined_variables.md
index a77ba781d7d..cd23b903d30 100644
--- a/doc/ci/variables/predefined_variables.md
+++ b/doc/ci/variables/predefined_variables.md
@@ -107,10 +107,10 @@ as it can cause the pipeline to behave unexpectedly.
| `CI_PROJECT_URL` | 8.10 | 0.5 | The HTTP(S) address of the project. |
| `CI_PROJECT_VISIBILITY` | 10.3 | all | The project visibility. Can be `internal`, `private`, or `public`. |
| `CI_PROJECT_CLASSIFICATION_LABEL` | 14.2 | all | The project [external authorization classification label](../../administration/settings/external_authorization.md). |
-| `CI_REGISTRY_IMAGE` | 8.10 | 0.5 | The address of the project's Container Registry. Only available if the Container Registry is enabled for the project. |
+| `CI_REGISTRY` | 8.10 | 0.5 | Address of the [Container Registry](../../user/packages/container_registry/index.md) server, formatted as `<host>[:<port>]`. For example: `registry.gitlab.example.com`. Only available if the Container Registry is enabled for the GitLab instance. |
+| `CI_REGISTRY_IMAGE` | 8.10 | 0.5 | Base address for the container registry to push, pull, or tag project's images, formatted as `<host>[:<port>]/<project_full_path>`. For example: `registry.gitlab.example.com/my_group/my_project`. Image names must follow the [container registry naming convention](../../user/packages/container_registry/index.md#naming-convention-for-your-container-images). Only available if the Container Registry is enabled for the project. |
| `CI_REGISTRY_PASSWORD` | 9.0 | all | The password to push containers to the project's GitLab Container Registry. Only available if the Container Registry is enabled for the project. This password value is the same as the `CI_JOB_TOKEN` and is valid only as long as the job is running. Use the `CI_DEPLOY_PASSWORD` for long-lived access to the registry |
| `CI_REGISTRY_USER` | 9.0 | all | The username to push containers to the project's GitLab Container Registry. Only available if the Container Registry is enabled for the project. |
-| `CI_REGISTRY` | 8.10 | 0.5 | The address of the GitLab Container Registry. Only available if the Container Registry is enabled for the project. This variable includes a `:port` value if one is specified in the registry configuration. |
| `CI_REPOSITORY_URL` | 9.0 | all | The full path to Git clone (HTTP) the repository with a [CI/CD job token](../jobs/ci_job_token.md), in the format `https://gitlab-ci-token:$CI_JOB_TOKEN@gitlab.example.com/my-group/my-project.git`. |
| `CI_RUNNER_DESCRIPTION` | 8.10 | 0.5 | The description of the runner. |
| `CI_RUNNER_EXECUTABLE_ARCH` | all | 10.6 | The OS/architecture of the GitLab Runner executable. Might not be the same as the environment of the executor. |
@@ -140,9 +140,9 @@ as it can cause the pipeline to behave unexpectedly.
| `GITLAB_CI` | all | all | Available for all jobs executed in CI/CD. `true` when available. |
| `GITLAB_FEATURES` | 10.6 | all | The comma-separated list of licensed features available for the GitLab instance and license. |
| `GITLAB_USER_EMAIL` | 8.12 | all | The email of the user who started the pipeline, unless the job is a manual job. In manual jobs, the value is the email of the user who started the job. |
-| `GITLAB_USER_ID` | 8.12 | all | The ID of the user who started the pipeline, unless the job is a manual job. In manual jobs, the value is the ID of the user who started the job. |
+| `GITLAB_USER_ID` | 8.12 | all | The numeric ID of the user who started the pipeline, unless the job is a manual job. In manual jobs, the value is the ID of the user who started the job. |
| `GITLAB_USER_LOGIN` | 10.0 | all | The username of the user who started the pipeline, unless the job is a manual job. In manual jobs, the value is the username of the user who started the job. |
-| `GITLAB_USER_NAME` | 10.0 | all | The name of the user who started the pipeline, unless the job is a manual job. In manual jobs, the value is the name of the user who started the job. |
+| `GITLAB_USER_NAME` | 10.0 | all | The display name of the user who started the pipeline, unless the job is a manual job. In manual jobs, the value is the name of the user who started the job. |
| `KUBECONFIG` | 14.2 | all | The path to the `kubeconfig` file with contexts for every shared agent connection. Only available when a [GitLab agent is authorized to access the project](../../user/clusters/agent/ci_cd_workflow.md#authorize-the-agent). |
| `TRIGGER_PAYLOAD` | 13.9 | all | The webhook payload. Only available when a pipeline is [triggered with a webhook](../triggers/index.md#access-webhook-payload). |
@@ -157,8 +157,8 @@ These variables are available when:
|---------------------------------------------|--------|--------|-------------|
| `CI_MERGE_REQUEST_APPROVED` | 14.1 | all | Approval status of the merge request. `true` when [merge request approvals](../../user/project/merge_requests/approvals/index.md) is available and the merge request has been approved. |
| `CI_MERGE_REQUEST_ASSIGNEES` | 11.9 | all | Comma-separated list of usernames of assignees for the merge request. |
-| `CI_MERGE_REQUEST_ID` | 11.6 | all | The instance-level ID of the merge request. This is a unique ID across all projects on GitLab. |
-| `CI_MERGE_REQUEST_IID` | 11.6 | all | The project-level IID (internal ID) of the merge request. This ID is unique for the current project. |
+| `CI_MERGE_REQUEST_ID` | 11.6 | all | The instance-level ID of the merge request. This is a unique ID across all projects on the GitLab instance. |
+| `CI_MERGE_REQUEST_IID` | 11.6 | all | The project-level IID (internal ID) of the merge request. This ID is unique for the current project, and is the number used in the merge request URL, page title, and other visible locations. |
| `CI_MERGE_REQUEST_LABELS` | 11.9 | all | Comma-separated label names of the merge request. |
| `CI_MERGE_REQUEST_MILESTONE` | 11.9 | all | The milestone title of the merge request. |
| `CI_MERGE_REQUEST_PROJECT_ID` | 11.6 | all | The ID of the project of the merge request. |
diff --git a/doc/ci/yaml/gitlab_ci_yaml.md b/doc/ci/yaml/gitlab_ci_yaml.md
index 920abf50546..a0e1ce04fad 100644
--- a/doc/ci/yaml/gitlab_ci_yaml.md
+++ b/doc/ci/yaml/gitlab_ci_yaml.md
@@ -1,89 +1,11 @@
---
-stage: Verify
-group: Pipeline Authoring
-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: reference
+redirect_to: '../index.md#the-gitlab-ciyml-file'
+remove_date: '2024-01-30'
---
-# The `.gitlab-ci.yml` file **(FREE ALL)**
+This document was moved to [another location](../index.md#the-gitlab-ciyml-file).
-To use GitLab CI/CD, you need:
-
-- Application code hosted in a Git repository.
-- A file called [`.gitlab-ci.yml`](index.md) in the root of your repository, which
- contains the CI/CD configuration.
-
-In the `.gitlab-ci.yml` file, you can define:
-
-- The scripts you want to run.
-- Other configuration files and templates you want to include.
-- Dependencies and caches.
-- The commands you want to run in sequence and those you want to run in parallel.
-- The location to deploy your application to.
-- Whether you want to run the scripts automatically or trigger any of them manually.
-
-The scripts are grouped into **jobs**, and jobs run as part of a larger
-**pipeline**. You can group multiple independent jobs into **stages** that run in a defined order.
-The CI/CD configuration needs at least one job that is [not hidden](../jobs/index.md#hide-jobs).
-
-You should organize your jobs in a sequence that suits your application and is in accordance with
-the tests you wish to perform. To [visualize](../pipeline_editor/index.md#visualize-ci-configuration) the process, imagine
-the scripts you add to jobs are the same as CLI commands you run on your computer.
-
-When you add a `.gitlab-ci.yml` file to your
-repository, GitLab detects it and an application called [GitLab Runner](https://docs.gitlab.com/runner/)
-runs the scripts defined in the jobs.
-
-A `.gitlab-ci.yml` file might contain:
-
-```yaml
-stages:
- - build
- - test
-
-build-code-job:
- stage: build
- script:
- - echo "Check the ruby version, then build some Ruby project files:"
- - ruby -v
- - rake
-
-test-code-job1:
- stage: test
- script:
- - echo "If the files are built successfully, test some files with one command:"
- - rake test1
-
-test-code-job2:
- stage: test
- script:
- - echo "If the files are built successfully, test other files with a different command:"
- - rake test2
-```
-
-In this example, the `build-code-job` job in the `build` stage runs first. It outputs
-the Ruby version the job is using, then runs `rake` to build project files.
-If this job completes successfully, the two `test-code-job` jobs in the `test` stage start
-in parallel and run tests on the files.
-
-The full pipeline in the example is composed of three jobs, grouped into two stages,
-`build` and `test`. The pipeline runs every time changes are pushed to any
-branch in the project.
-
-GitLab CI/CD not only executes the jobs but also shows you what's happening during execution,
-just as you would see in your terminal:
-
-![job running](img/job_running_v13_10.png)
-
-You create the strategy for your app and GitLab runs the pipeline
-according to what you've defined. Your pipeline status is also
-displayed by GitLab:
-
-![pipeline status](img/pipeline_status.png)
-
-If anything goes wrong, you can
-[roll back](../environments/index.md#retry-or-roll-back-a-deployment) the changes:
-
-![rollback button](img/rollback.png)
-
-[View the full syntax for the `.gitlab-ci.yml` file](index.md).
+<!-- This redirect file can be deleted after <2024-01-30>. -->
+<!-- Redirects that point to other docs in the same project expire in three months. -->
+<!-- Redirects that point to docs in a different project or site (link is not relative and starts with `https:`) expire in one year. -->
+<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/redirects.html -->
diff --git a/doc/ci/yaml/img/job_running_v13_10.png b/doc/ci/yaml/img/job_running_v13_10.png
deleted file mode 100644
index b1f21b8445f..00000000000
--- a/doc/ci/yaml/img/job_running_v13_10.png
+++ /dev/null
Binary files differ
diff --git a/doc/ci/yaml/img/pipeline_status.png b/doc/ci/yaml/img/pipeline_status.png
deleted file mode 100644
index 96881f072e1..00000000000
--- a/doc/ci/yaml/img/pipeline_status.png
+++ /dev/null
Binary files differ
diff --git a/doc/ci/yaml/img/rollback.png b/doc/ci/yaml/img/rollback.png
deleted file mode 100644
index 38e0552f4f1..00000000000
--- a/doc/ci/yaml/img/rollback.png
+++ /dev/null
Binary files differ
diff --git a/doc/ci/yaml/index.md b/doc/ci/yaml/index.md
index 66a5fe61a1d..9b781ca6d13 100644
--- a/doc/ci/yaml/index.md
+++ b/doc/ci/yaml/index.md
@@ -5,13 +5,14 @@ info: To determine the technical writer assigned to the Stage/Group associated w
type: reference
---
-# `.gitlab-ci.yml` keyword reference **(FREE ALL)**
+# CI/CD YAML syntax reference **(FREE ALL)**
This document lists the configuration options for the GitLab `.gitlab-ci.yml` file.
This file is where you define the CI/CD jobs that make up your pipeline.
-- To create your own `.gitlab-ci.yml` file, try a tutorial that demonstrates a
- [simple](../quick_start/index.md) or [complex](../quick_start/tutorial.md) pipeline.
+- If you are already familiar with [basic CI/CD concepts](../index.md), try creating
+ your own `.gitlab-ci.yml` file by following a tutorial that demonstrates a [simple](../quick_start/index.md)
+ or [complex](../quick_start/tutorial.md) pipeline.
- For a collection of examples, see [GitLab CI/CD examples](../examples/index.md).
- To view a large `.gitlab-ci.yml` file used in an enterprise, see the
[`.gitlab-ci.yml` file for `gitlab`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab-ci.yml).
@@ -35,6 +36,12 @@ A GitLab CI/CD pipeline configuration includes:
| [`variables`](#variables) | Define CI/CD variables for all job in the pipeline. |
| [`workflow`](#workflow) | Control what types of pipeline run. |
+- [Header keywords](#header-keywords)
+
+ | Keyword | Description |
+ |-----------------|:------------|
+ | [`spec`](#spec) | Define specifications for external configuration files. |
+
- [Jobs](../jobs/index.md) configured with [job keywords](#job-keywords):
| Keyword | Description |
@@ -349,6 +356,42 @@ include:
- All [nested includes](includes.md#use-nested-includes) are executed without context as a public user,
so you can only include public projects or templates. No variables are available in the `include` section of nested includes.
+#### `include:inputs`
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/391331) in GitLab 15.11 as a Beta feature.
+
+Use `include:inputs` to set the values for input parameters when the included configuration
+uses [`spec:inputs`](#specinputs) and is added to the pipeline.
+
+**Keyword type**: Global keyword.
+
+**Possible inputs**: A string, numeric value, or boolean.
+
+**Example of `include:inputs`**:
+
+```yaml
+include:
+ - local: 'custom_configuration.yml'
+ inputs:
+ website: "My website"
+```
+
+In this example:
+
+- The configuration contained in `custom_configuration.yml` is added to the pipeline,
+ with a `website` input set to a value of `My website` for the included configuration.
+
+**Additional details**:
+
+- If the included configuration file uses [`spec:inputs:type`](#specinputstype),
+ the input value must match the defined type.
+- If the included configuration file uses [`spec:inputs:options`](#specinputsoptions),
+ the input value must match one of the listed options.
+
+**Related topics**:
+
+- [Set input values when using `include`](inputs.md#set-input-values-when-using-include).
+
### `stages`
Use `stages` to define stages that contain groups of jobs. Use [`stage`](#stage)
@@ -592,6 +635,193 @@ When the branch is something else:
- Use [`inherit:variables`](#inheritvariables) in the trigger job and list the
exact variables you want to forward to the downstream pipeline.
+## Header keywords
+
+Some keywords must be defined in a header section of a YAML configuration file.
+The header must be at the top of the file, separated from the rest of the configuration
+with `---`.
+
+### `spec`
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/391331) in GitLab 15.11 as a Beta feature.
+
+Add a `spec` section to the header of a YAML file to configure the behavior of a pipeline
+when a configuration is added to the pipeline with the `include` keyword.
+
+#### `spec:inputs`
+
+You can use `spec:inputs` to define input parameters for the CI/CD configuration you intend to add
+to a pipeline with `include`. Use `include:inputs` to define the values to use when the pipeline runs.
+
+Use the inputs to customize the behavior of the configuration when included in CI/CD configuration.
+
+Use the interpolation format `$[[ input.input-id ]]` to reference the values outside of the header section.
+Inputs are evaluated and interpolated when the configuration is fetched during pipeline creation, but before the
+configuration is merged with the contents of the `.gitlab-ci.yml` file.
+
+**Keyword type**: Header keyword. `specs` must be declared at the top of the configuration file,
+in a header section.
+
+**Possible inputs**: A hash of strings representing the expected inputs.
+
+**Example of `spec:inputs`**:
+
+```yaml
+spec:
+ inputs:
+ environment:
+ job-stage:
+---
+
+scan-website:
+ stage: $[[ inputs.job-stage ]]
+ script: ./scan-website $[[ inputs.environment ]]
+```
+
+**Additional details**:
+
+- Inputs are mandatory unless you use [`spec:inputs:default`](#specinputsdefault)
+ to set a default value.
+- Inputs expect strings unless you use [`spec:inputs:type`](#specinputstype) to set a
+ different input type.
+- A string containing an interpolation block must not exceed 1 MB.
+- The string inside an interpolation block must not exceed 1 KB.
+
+**Related topics**:
+
+- [Define input parameters with `spec:inputs`](inputs.md#define-input-parameters-with-specinputs).
+
+##### `spec:inputs:default`
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/391331) in GitLab 15.11 as a Beta feature.
+
+Inputs are mandatory when included, unless you set a default value with `spec:inputs:default`.
+
+Use `default: null` to have no default value.
+
+**Keyword type**: Header keyword. `specs` must be declared at the top of the configuration file,
+in a header section.
+
+**Possible inputs**: A string representing the default value, or `null`.
+
+**Example of `spec:inputs:default`**:
+
+```yaml
+spec:
+ inputs:
+ website:
+ user:
+ default: 'test-user'
+ flags:
+ default: null
+---
+
+# The pipeline configuration would follow...
+```
+
+In this example:
+
+- `website` is mandatory and must be defined.
+- `user` is optional. If not defined, the value is `test-user`.
+- `flags` is optional. If not defined, it has no value.
+
+**Additional details**:
+
+- If an input uses both `default` and [`options`](#specinputsoptions), the default value
+ must be one of the listed options. If not, the pipeline fails with a validation error.
+
+##### `spec:inputs:description`
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/415637) in GitLab 16.5.
+
+Use `description` to give a description to a specific input. The description does
+not affect the behavior of the input and is only used to help users of the file
+understand the input.
+
+**Keyword type**: Header keyword. `specs` must be declared at the top of the configuration file,
+in a header section.
+
+**Possible inputs**: A string representing the description.
+
+**Example of `spec:inputs:description`**:
+
+```yaml
+spec:
+ inputs:
+ flags:
+ description: 'Sample description of the `flags` input details.'
+---
+
+# The pipeline configuration would follow...
+```
+
+##### `spec:inputs:options`
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/393401) in GitLab 16.6.
+
+Inputs can use `options` to specify a list of allowed values for an input.
+The limit is 50 options per input.
+
+**Keyword type**: Header keyword. `specs` must be declared at the top of the configuration file,
+in a header section.
+
+**Possible inputs**: An array of input options.
+
+**Example of `spec:inputs:options`**:
+
+```yaml
+spec:
+ inputs:
+ environment:
+ options:
+ - development
+ - staging
+ - production
+---
+
+# The pipeline configuration would follow...
+```
+
+In this example:
+
+- `environment` is mandatory and must be defined with one of the values in the list.
+
+**Additional details**:
+
+- If an input uses both [`default`](#specinputsdefault) and `options`, the default value
+ must be one of the listed options. If not, the pipeline fails with a validation error.
+
+##### `spec:inputs:type`
+
+By default, inputs expect strings. Use `spec:inputs:type` to set a different required
+type for inputs.
+
+**Keyword type**: Header keyword. `specs` must be declared at the top of the configuration file,
+in a header section.
+
+**Possible inputs**: Can be one of:
+
+- `string`, to accept string inputs (default when not defined).
+- `number`, to only accept numeric inputs.
+- `boolean`, to only accept `true` or `false` inputs.
+
+**Example of `spec:inputs:type`**:
+
+```yaml
+spec:
+ inputs:
+ job_name:
+ website:
+ type: string
+ port:
+ type: number
+ available:
+ type: boolean
+---
+
+# The pipeline configuration would follow...
+```
+
## Job keywords
The following topics explain how to use keywords to configure CI/CD pipelines.
@@ -2025,7 +2255,7 @@ Use `hooks:pre_get_sources_script` to specify a list of commands to execute on t
before cloning the Git repository and any submodules.
You can use it for example to:
-- Adjust the [Git configuration](../troubleshooting.md#get_sources-job-section-fails-because-of-an-http2-problem).
+- Adjust the [Git configuration](../jobs/index.md#get_sources-job-section-fails-because-of-an-http2-problem).
- Export [tracing variables](../../topics/git/useful_git_commands.md).
**Possible inputs**: An array including:
@@ -2421,8 +2651,8 @@ This example creates four paths of execution:
**Additional details**:
- The maximum number of jobs that a single job can have in the `needs` array is limited:
- - For GitLab.com, the limit is 50. For more information, see our
- [infrastructure issue](https://gitlab.com/gitlab-com/gl-infra/reliability/-/issues/7541).
+ - For GitLab.com, the limit is 50. For more information, see
+ [issue 350398](https://gitlab.com/gitlab-org/gitlab/-/issues/350398).
- For self-managed instances, the default limit is 50. This limit [can be changed](../../administration/cicd.md#set-the-needs-job-limit).
- If `needs` refers to a job that uses the [`parallel`](#parallel) keyword,
it depends on all jobs created in parallel, not just one job. It also downloads
@@ -2793,226 +3023,6 @@ The `linux:rspec` job runs as soon as the `linux:build: [aws, app1]` job finishe
script: echo "Running rspec on linux..."
```
-### `only` / `except`
-
-NOTE:
-`only` and `except` are not being actively developed. To control when to add jobs to pipelines,
-use [`rules`](#rules) instead.
-
-You can use `only` and `except` to control when to add jobs to pipelines.
-
-- Use `only` to define when a job runs.
-- Use `except` to define when a job **does not** run.
-
-See [specify when jobs run with `only` and `except`](../jobs/job_control.md#specify-when-jobs-run-with-only-and-except)
-for more details and examples.
-
-#### `only:refs` / `except:refs`
-
-NOTE:
-`only:refs` and `except:refs` are not being actively developed. To use refs, regular expressions,
-or variables to control when to add jobs to pipelines, use [`rules:if`](#rulesif) instead.
-
-Use the `only:refs` and `except:refs` keywords to control when to add jobs to a
-pipeline based on branch names or pipeline types.
-
-**Keyword type**: Job keyword. You can use it only as part of a job.
-
-**Possible inputs**: An array including any number of:
-
-- Branch names, for example `main` or `my-feature-branch`.
-- [Regular expressions](../jobs/job_control.md#only--except-regex-syntax)
- that match against branch names, for example `/^feature-.*/`.
-- The following keywords:
-
- | **Value** | **Description** |
- | -------------------------|-----------------|
- | `api` | For pipelines triggered by the [pipelines API](../../api/pipelines.md#create-a-new-pipeline). |
- | `branches` | When the Git reference for a pipeline is a branch. |
- | `chat` | For pipelines created by using a [GitLab ChatOps](../chatops/index.md) command. |
- | `external` | When you use CI services other than GitLab. |
- | `external_pull_requests` | When an external pull request on GitHub is created or updated (See [Pipelines for external pull requests](../ci_cd_for_external_repos/index.md#pipelines-for-external-pull-requests)). |
- | `merge_requests` | For pipelines created when a merge request is created or updated. Enables [merge request pipelines](../pipelines/merge_request_pipelines.md), [merged results pipelines](../pipelines/merged_results_pipelines.md), and [merge trains](../pipelines/merge_trains.md). |
- | `pipelines` | For [multi-project pipelines](../pipelines/downstream_pipelines.md#multi-project-pipelines) created by [using the API with `CI_JOB_TOKEN`](../pipelines/downstream_pipelines.md#trigger-a-multi-project-pipeline-by-using-the-api), or the [`trigger`](#trigger) keyword. |
- | `pushes` | For pipelines triggered by a `git push` event, including for branches and tags. |
- | `schedules` | For [scheduled pipelines](../pipelines/schedules.md). |
- | `tags` | When the Git reference for a pipeline is a tag. |
- | `triggers` | For pipelines created by using a [trigger token](../triggers/index.md#configure-cicd-jobs-to-run-in-triggered-pipelines). |
- | `web` | For pipelines created by selecting **Run pipeline** in the GitLab UI, from the project's **Build > Pipelines** section. |
-
-**Example of `only:refs` and `except:refs`**:
-
-```yaml
-job1:
- script: echo
- only:
- - main
- - /^issue-.*$/
- - merge_requests
-
-job2:
- script: echo
- except:
- - main
- - /^stable-branch.*$/
- - schedules
-```
-
-**Additional details**:
-
-- Scheduled pipelines run on specific branches, so jobs configured with `only: branches`
- run on scheduled pipelines too. Add `except: schedules` to prevent jobs with `only: branches`
- from running on scheduled pipelines.
-- `only` or `except` used without any other keywords are equivalent to `only: refs`
- or `except: refs`. For example, the following two jobs configurations have the same
- behavior:
-
- ```yaml
- job1:
- script: echo
- only:
- - branches
-
- job2:
- script: echo
- only:
- refs:
- - branches
- ```
-
-- If a job does not use `only`, `except`, or [`rules`](#rules), then `only` is set to `branches`
- and `tags` by default.
-
- For example, `job1` and `job2` are equivalent:
-
- ```yaml
- job1:
- script: echo "test"
-
- job2:
- script: echo "test"
- only:
- - branches
- - tags
- ```
-
-#### `only:variables` / `except:variables`
-
-NOTE:
-`only:variables` and `except:variables` are not being actively developed. To use refs,
-regular expressions, or variables to control when to add jobs to pipelines, use [`rules:if`](#rulesif) instead.
-
-Use the `only:variables` or `except:variables` keywords to control when to add jobs
-to a pipeline, based on the status of [CI/CD variables](../variables/index.md).
-
-**Keyword type**: Job keyword. You can use it only as part of a job.
-
-**Possible inputs**:
-
-- An array of [CI/CD variable expressions](../jobs/job_control.md#cicd-variable-expressions).
-
-**Example of `only:variables`**:
-
-```yaml
-deploy:
- script: cap staging deploy
- only:
- variables:
- - $RELEASE == "staging"
- - $STAGING
-```
-
-**Related topics**:
-
-- [`only:variables` and `except:variables` examples](../jobs/job_control.md#only-variables--except-variables-examples).
-
-#### `only:changes` / `except:changes`
-
-NOTE:
-`only:changes` and `except:changes` are not being actively developed. To use changed files
-to control when to add a job to a pipeline, use [`rules:changes`](#ruleschanges) instead.
-
-Use the `changes` keyword with `only` to run a job, or with `except` to skip a job,
-when a Git push event modifies a file.
-
-Use `changes` in pipelines with the following refs:
-
-- `branches`
-- `external_pull_requests`
-- `merge_requests` (see additional details about [using `only:changes` with merge request pipelines](../jobs/job_control.md#use-onlychanges-with-merge-request-pipelines))
-
-**Keyword type**: Job keyword. You can use it only as part of a job.
-
-**Possible inputs**: An array including any number of:
-
-- Paths to files.
-- Wildcard paths for single directories, for example `path/to/directory/*`, or a directory
- and all its subdirectories, for example `path/to/directory/**/*`.
-- Wildcard [glob](https://en.wikipedia.org/wiki/Glob_(programming)) paths for all
- files with the same extension or multiple extensions, for example `*.md` or `path/to/directory/*.{rb,py,sh}`.
- See the [Ruby `fnmatch` documentation](https://docs.ruby-lang.org/en/master/File.html#method-c-fnmatch)
- for the supported syntax list.
-- Wildcard paths to files in the root directory, or all directories, wrapped in double quotes.
- For example `"*.json"` or `"**/*.json"`.
-
-**Example of `only:changes`**:
-
-```yaml
-docker build:
- script: docker build -t my-image:$CI_COMMIT_REF_SLUG .
- only:
- refs:
- - branches
- changes:
- - Dockerfile
- - docker/scripts/*
- - dockerfiles/**/*
- - more_scripts/*.{rb,py,sh}
- - "**/*.json"
-```
-
-**Additional details**:
-
-- `changes` resolves to `true` if any of the matching files are changed (an `OR` operation).
-- If you use refs other than `branches`, `external_pull_requests`, or `merge_requests`,
- `changes` can't determine if a given file is new or old and always returns `true`.
-- If you use `only: changes` with other refs, jobs ignore the changes and always run.
-- If you use `except: changes` with other refs, jobs ignore the changes and never run.
-
-**Related topics**:
-
-- [`only: changes` and `except: changes` examples](../jobs/job_control.md#onlychanges--exceptchanges-examples).
-- If you use `changes` with [only allow merge requests to be merged if the pipeline succeeds](../../user/project/merge_requests/merge_when_pipeline_succeeds.md#require-a-successful-pipeline-for-merge),
- you should [also use `only:merge_requests`](../jobs/job_control.md#use-onlychanges-with-merge-request-pipelines).
-- [Jobs or pipelines can run unexpectedly when using `only: changes`](../jobs/job_control.md#jobs-or-pipelines-run-unexpectedly-when-using-changes).
-
-#### `only:kubernetes` / `except:kubernetes`
-
-NOTE:
-`only:refs` and `except:refs` are not being actively developed. To control if jobs are added
-to the pipeline when the Kubernetes service is active in the project, use [`rules:if`](#rulesif)
-with the [`CI_KUBERNETES_ACTIVE`](../variables/predefined_variables.md) predefined CI/CD variable instead.
-
-Use `only:kubernetes` or `except:kubernetes` to control if jobs are added to the pipeline
-when the Kubernetes service is active in the project.
-
-**Keyword type**: Job-specific. You can use it only as part of a job.
-
-**Possible inputs**:
-
-- The `kubernetes` strategy accepts only the `active` keyword.
-
-**Example of `only:kubernetes`**:
-
-```yaml
-deploy:
- only:
- kubernetes: active
-```
-
-In this example, the `deploy` job runs only when the Kubernetes service is active
-in the project.
-
### `pages`
Use `pages` to define a [GitLab Pages](../../user/project/pages/index.md) job that
@@ -4929,9 +4939,9 @@ The following keywords are deprecated.
### Globally-defined `image`, `services`, `cache`, `before_script`, `after_script`
-Defining `image`, `services`, `cache`, `before_script`, and
-`after_script` globally is deprecated. Support could be removed
-from a future release.
+Defining `image`, `services`, `cache`, `before_script`, and `after_script` globally is deprecated.
+Using these keywords at the top level is still possible to ensure backwards compatibility,
+but could be scheduled for removal in a future milestone.
Use [`default`](#default) instead. For example:
@@ -4949,14 +4959,233 @@ default:
- rm -rf tmp/
```
-<!-- ## Troubleshooting
+### `only` / `except`
+
+NOTE:
+`only` and `except` are deprecated and not being actively developed. These keywords
+are still usable to ensure backwards compatibility, but could be scheduled for removal
+in a future milestone. To control when to add jobs to pipelines, use [`rules`](#rules) instead.
+
+You can use `only` and `except` to control when to add jobs to pipelines.
+
+- Use `only` to define when a job runs.
+- Use `except` to define when a job **does not** run.
+
+See [specify when jobs run with `only` and `except`](../jobs/job_control.md#specify-when-jobs-run-with-only-and-except)
+for more details and examples.
-Include any troubleshooting steps that you can foresee. If you know beforehand what issues
-one might have when setting this up, or when something is changed, or on upgrading, it's
-important to describe those, too. Think of things that may go wrong and include them here.
-This is important to minimize requests for support, and to avoid doc comments with
-questions that you know someone might ask.
+#### `only:refs` / `except:refs`
-Each scenario can be a third-level heading, for example, `### Getting error message X`.
-If you have none to add when creating a doc, leave this section in place
-but commented out to help encourage others to add to it in the future. -->
+NOTE:
+`only:refs` and `except:refs` are deprecated and not being actively developed. These keywords
+are still usable to ensure backwards compatibility, but could be scheduled for removal
+in a future milestone. To use refs, regular expressions, or variables to control
+when to add jobs to pipelines, use [`rules:if`](#rulesif) instead.
+
+You can use the `only:refs` and `except:refs` keywords to control when to add jobs to a
+pipeline based on branch names or pipeline types.
+
+**Keyword type**: Job keyword. You can use it only as part of a job.
+
+**Possible inputs**: An array including any number of:
+
+- Branch names, for example `main` or `my-feature-branch`.
+- [Regular expressions](../jobs/job_control.md#only--except-regex-syntax)
+ that match against branch names, for example `/^feature-.*/`.
+- The following keywords:
+
+ | **Value** | **Description** |
+ | -------------------------|-----------------|
+ | `api` | For pipelines triggered by the [pipelines API](../../api/pipelines.md#create-a-new-pipeline). |
+ | `branches` | When the Git reference for a pipeline is a branch. |
+ | `chat` | For pipelines created by using a [GitLab ChatOps](../chatops/index.md) command. |
+ | `external` | When you use CI services other than GitLab. |
+ | `external_pull_requests` | When an external pull request on GitHub is created or updated (See [Pipelines for external pull requests](../ci_cd_for_external_repos/index.md#pipelines-for-external-pull-requests)). |
+ | `merge_requests` | For pipelines created when a merge request is created or updated. Enables [merge request pipelines](../pipelines/merge_request_pipelines.md), [merged results pipelines](../pipelines/merged_results_pipelines.md), and [merge trains](../pipelines/merge_trains.md). |
+ | `pipelines` | For [multi-project pipelines](../pipelines/downstream_pipelines.md#multi-project-pipelines) created by [using the API with `CI_JOB_TOKEN`](../pipelines/downstream_pipelines.md#trigger-a-multi-project-pipeline-by-using-the-api), or the [`trigger`](#trigger) keyword. |
+ | `pushes` | For pipelines triggered by a `git push` event, including for branches and tags. |
+ | `schedules` | For [scheduled pipelines](../pipelines/schedules.md). |
+ | `tags` | When the Git reference for a pipeline is a tag. |
+ | `triggers` | For pipelines created by using a [trigger token](../triggers/index.md#configure-cicd-jobs-to-run-in-triggered-pipelines). |
+ | `web` | For pipelines created by selecting **Run pipeline** in the GitLab UI, from the project's **Build > Pipelines** section. |
+
+**Example of `only:refs` and `except:refs`**:
+
+```yaml
+job1:
+ script: echo
+ only:
+ - main
+ - /^issue-.*$/
+ - merge_requests
+
+job2:
+ script: echo
+ except:
+ - main
+ - /^stable-branch.*$/
+ - schedules
+```
+
+**Additional details**:
+
+- Scheduled pipelines run on specific branches, so jobs configured with `only: branches`
+ run on scheduled pipelines too. Add `except: schedules` to prevent jobs with `only: branches`
+ from running on scheduled pipelines.
+- `only` or `except` used without any other keywords are equivalent to `only: refs`
+ or `except: refs`. For example, the following two jobs configurations have the same
+ behavior:
+
+ ```yaml
+ job1:
+ script: echo
+ only:
+ - branches
+
+ job2:
+ script: echo
+ only:
+ refs:
+ - branches
+ ```
+
+- If a job does not use `only`, `except`, or [`rules`](#rules), then `only` is set to `branches`
+ and `tags` by default.
+
+ For example, `job1` and `job2` are equivalent:
+
+ ```yaml
+ job1:
+ script: echo "test"
+
+ job2:
+ script: echo "test"
+ only:
+ - branches
+ - tags
+ ```
+
+#### `only:variables` / `except:variables`
+
+NOTE:
+`only:variables` and `except:variables` are deprecated and not being actively developed.
+These keywords are still usable to ensure backwards compatibility, but could be scheduled
+for removal in a future milestone. To use refs, regular expressions, or variables
+to control when to add jobs to pipelines, use [`rules:if`](#rulesif) instead.
+
+You can use the `only:variables` or `except:variables` keywords to control when to add jobs
+to a pipeline, based on the status of [CI/CD variables](../variables/index.md).
+
+**Keyword type**: Job keyword. You can use it only as part of a job.
+
+**Possible inputs**:
+
+- An array of [CI/CD variable expressions](../jobs/job_control.md#cicd-variable-expressions).
+
+**Example of `only:variables`**:
+
+```yaml
+deploy:
+ script: cap staging deploy
+ only:
+ variables:
+ - $RELEASE == "staging"
+ - $STAGING
+```
+
+**Related topics**:
+
+- [`only:variables` and `except:variables` examples](../jobs/job_control.md#only-variables--except-variables-examples).
+
+#### `only:changes` / `except:changes`
+
+`only:variables` and `except:variables`
+
+NOTE:
+`only:changes` and `except:changes` are deprecated and not being actively developed.
+These keywords are still usable to ensure backwards compatibility, but could be scheduled
+for removal in a future milestone. To use changed files to control when to add a job to a pipeline,
+use [`rules:changes`](#ruleschanges) instead.
+
+Use the `changes` keyword with `only` to run a job, or with `except` to skip a job,
+when a Git push event modifies a file.
+
+Use `changes` in pipelines with the following refs:
+
+- `branches`
+- `external_pull_requests`
+- `merge_requests` (see additional details about [using `only:changes` with merge request pipelines](../jobs/job_control.md#use-onlychanges-with-merge-request-pipelines))
+
+**Keyword type**: Job keyword. You can use it only as part of a job.
+
+**Possible inputs**: An array including any number of:
+
+- Paths to files.
+- Wildcard paths for single directories, for example `path/to/directory/*`, or a directory
+ and all its subdirectories, for example `path/to/directory/**/*`.
+- Wildcard [glob](https://en.wikipedia.org/wiki/Glob_(programming)) paths for all
+ files with the same extension or multiple extensions, for example `*.md` or `path/to/directory/*.{rb,py,sh}`.
+ See the [Ruby `fnmatch` documentation](https://docs.ruby-lang.org/en/master/File.html#method-c-fnmatch)
+ for the supported syntax list.
+- Wildcard paths to files in the root directory, or all directories, wrapped in double quotes.
+ For example `"*.json"` or `"**/*.json"`.
+
+**Example of `only:changes`**:
+
+```yaml
+docker build:
+ script: docker build -t my-image:$CI_COMMIT_REF_SLUG .
+ only:
+ refs:
+ - branches
+ changes:
+ - Dockerfile
+ - docker/scripts/*
+ - dockerfiles/**/*
+ - more_scripts/*.{rb,py,sh}
+ - "**/*.json"
+```
+
+**Additional details**:
+
+- `changes` resolves to `true` if any of the matching files are changed (an `OR` operation).
+- If you use refs other than `branches`, `external_pull_requests`, or `merge_requests`,
+ `changes` can't determine if a given file is new or old and always returns `true`.
+- If you use `only: changes` with other refs, jobs ignore the changes and always run.
+- If you use `except: changes` with other refs, jobs ignore the changes and never run.
+
+**Related topics**:
+
+- [`only: changes` and `except: changes` examples](../jobs/job_control.md#onlychanges--exceptchanges-examples).
+- If you use `changes` with [only allow merge requests to be merged if the pipeline succeeds](../../user/project/merge_requests/merge_when_pipeline_succeeds.md#require-a-successful-pipeline-for-merge),
+ you should [also use `only:merge_requests`](../jobs/job_control.md#use-onlychanges-with-merge-request-pipelines).
+- [Jobs or pipelines can run unexpectedly when using `only: changes`](../jobs/job_control.md#jobs-or-pipelines-run-unexpectedly-when-using-changes).
+
+#### `only:kubernetes` / `except:kubernetes`
+
+NOTE:
+`only:kubernetes` and `except:kubernetes` are deprecated and not being actively developed.
+These keywords are still usable to ensure backwards compatibility, but could be scheduled
+for removal in a future milestone. To control if jobs are added to the pipeline when
+the Kubernetes service is active in the project, use [`rules:if`](#rulesif) with the
+[`CI_KUBERNETES_ACTIVE`](../variables/predefined_variables.md) predefined CI/CD variable instead.
+
+Use `only:kubernetes` or `except:kubernetes` to control if jobs are added to the pipeline
+when the Kubernetes service is active in the project.
+
+**Keyword type**: Job-specific. You can use it only as part of a job.
+
+**Possible inputs**:
+
+- The `kubernetes` strategy accepts only the `active` keyword.
+
+**Example of `only:kubernetes`**:
+
+```yaml
+deploy:
+ only:
+ kubernetes: active
+```
+
+In this example, the `deploy` job runs only when the Kubernetes service is active
+in the project.
diff --git a/doc/ci/yaml/inputs.md b/doc/ci/yaml/inputs.md
index 9e084cf0020..089d6bc5b62 100644
--- a/doc/ci/yaml/inputs.md
+++ b/doc/ci/yaml/inputs.md
@@ -4,34 +4,34 @@ group: Pipeline Authoring
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
---
-# Define inputs for configuration added with `include` **(FREE ALL BETA)**
+# Define inputs for configuration added with `include` **(FREE ALL)**
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/391331) in GitLab 15.11 as a Beta feature.
-
-FLAG:
-`spec` and `inputs` are experimental [Open Beta features](../../policy/experiment-beta-support.md#beta)
-and subject to change without notice.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/391331) in GitLab 15.11 as a Beta feature.
+> - Made generally available in GitLab 16.6.
## Define input parameters with `spec:inputs`
-> `description` keyword [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/415637) in GitLab 16.5.
+> - `description` keyword [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/415637) in GitLab 16.5.
+> - `options` keyword [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/393401) in GitLab 16.6.
Use `spec:inputs` to define input parameters for CI/CD configuration intended to be added
-to a pipeline with `include`. Use [`include:inputs`](#set-input-parameter-values-with-includeinputs)
+to a pipeline with `include`. Use [`include:inputs`](#set-input-values-when-using-include)
to define the values to use when the pipeline runs.
The specs must be declared at the top of the configuration file, in a header section.
Separate the header from the rest of the configuration with `---`.
Use the interpolation format `$[[ input.input-id ]]` to reference the values outside of the header section.
-The inputs are evaluated and interpolated once, when the configuration is fetched
-during pipeline creation, but before the configuration is merged with the contents of the `.gitlab-ci.yml`.
+The inputs are evaluated and interpolated when the configuration is fetched during pipeline creation, but before the
+configuration is merged with the contents of the `.gitlab-ci.yml` file.
+
+For example, in a file named `custom_website_scan.yml`:
```yaml
spec:
inputs:
- environment:
job-stage:
+ environment:
---
scan-website:
@@ -41,58 +41,58 @@ scan-website:
When using `spec:inputs`:
-- Defined inputs are mandatory by default.
-- Inputs can be made optional by specifying a `default`. Use `default: null` to have no default value.
-- You can optionally use `description` to give a description to a specific input.
+- Inputs are mandatory by default.
+- Inputs must be strings by default.
- A string containing an interpolation block must not exceed 1 MB.
- The string inside an interpolation block must not exceed 1 KB.
-For example, a `custom_configuration.yml`:
-
-```yaml
-spec:
- inputs:
- website:
- user:
- default: 'test-user'
- flags:
- default: null
- description: 'Sample description of the `flags` input detail.'
----
-
-# The pipeline configuration would follow...
-```
-
-In this example:
+Additionally, use:
-- `website` is mandatory and must be defined.
-- `user` is optional. If not defined, the value is `test-user`.
-- `flags` is optional. If not defined, it has no value. The optional description should give details about the input.
+- [`spec:inputs:default`](index.md#specinputsdefault) to define default values for inputs
+ when not specified. When you specify a default, the inputs are no longer mandatory.
+- [`spec:inputs:description`](index.md#specinputsdescription) to give a description to
+ a specific input. The description does not affect the input, but can help people
+ understand the input details or expected values.
+- [`spec:inputs:options`](index.md#specinputsoptions) to specify a list of allowed values
+ for an input.
+- [`spec:inputs:type`](index.md#specinputstype) to force a specific input type, which
+ can be `string` (the default type), `number`, or `boolean`.
-## Set input parameter values with `include:inputs`
+## Set input values when using `include`
> `include:with` [renamed to `include:inputs`](https://gitlab.com/gitlab-org/gitlab/-/issues/406780) in GitLab 16.0.
-Use `include:inputs` to set the values for the parameters when the included configuration
-is added to the pipeline.
+Use [`include:inputs`](index.md#includeinputs) to set the values for the parameters
+when the included configuration is added to the pipeline.
-For example, to include a `custom_configuration.yml` that has the same specs
+For example, to include a `custom_website_scan.yml` that has the same specs
as the [example above](#define-input-parameters-with-specinputs):
```yaml
include:
- - local: 'custom_configuration.yml'
+ - local: 'custom_website_scan.yml'
inputs:
- website: "My website"
+ job-stage: post-deploy
+ environment: production
+
+stages:
+ - build
+ - test
+ - deploy
+ - post-deploy
+
+# The pipeline configuration would follow...
```
-In this example:
+In this example, the included configuration is added with:
-- `website` has a value of `My website` for the included configuration.
+- `job-stage` set to `post-deploy`, so the included job runs in the custom `post-deploy` stage.
+- `environment` set to `production`, so the included job runs for the production environment.
### Use `include:inputs` with multiple files
-`inputs` must be specified separately for each included file. For example:
+[`inputs`](index.md#includeinputs) must be specified separately for each included file.
+For example:
```yaml
include:
diff --git a/doc/development/ai_architecture.md b/doc/development/ai_architecture.md
index f03ffa748fa..54ad52f0c39 100644
--- a/doc/development/ai_architecture.md
+++ b/doc/development/ai_architecture.md
@@ -55,9 +55,8 @@ It is possible to utilize other models or technologies, however they will need t
The following models have been approved for use:
-- [OpenAI models](https://platform.openai.com/docs/models)
- Google's [Vertex AI](https://cloud.google.com/vertex-ai) and [model garden](https://cloud.google.com/model-garden)
-- [AI Code Suggestions](https://gitlab.com/gitlab-org/modelops/applied-ml/code-suggestions/ai-assist/-/tree/main)
+- [Anthropic models](https://docs.anthropic.com/claude/reference/selecting-a-model)
- [Suggested reviewer](https://gitlab.com/gitlab-org/modelops/applied-ml/applied-ml-updates/-/issues/10)
### Vector stores
@@ -77,7 +76,7 @@ A [draft MR](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/122035) has b
The index function has been updated to improve search quality. This was tested locally by setting the `ivfflat.probes` value to `10` with the following SQL command:
```ruby
-Embedding::TanukiBotMvc.connection.execute("SET ivfflat.probes = 10")
+::Embedding::Vertex::GitlabDocumentation.connection.execute("SET ivfflat.probes = 10")
```
Setting the `probes` value for indexing improves results, as per the neighbor [documentation](https://github.com/ankane/neighbor#indexing).
diff --git a/doc/development/ai_features/duo_chat.md b/doc/development/ai_features/duo_chat.md
index 841123c803a..ad044f4a923 100644
--- a/doc/development/ai_features/duo_chat.md
+++ b/doc/development/ai_features/duo_chat.md
@@ -12,7 +12,6 @@ NOTE:
Use [this snippet](https://gitlab.com/gitlab-org/gitlab/-/snippets/2554994) for help automating the following section.
1. [Enable Anthropic API features](index.md#configure-anthropic-access).
-1. [Enable OpenAI support](index.md#configure-openai-access).
1. [Ensure the embedding database is configured](index.md#set-up-the-embedding-database).
1. Ensure that your current branch is up-to-date with `master`.
1. To access the GitLab Duo Chat interface, in the lower-left corner of any page, select **Help** and **Ask GitLab Duo Chat**.
@@ -86,19 +85,45 @@ gdk start
tail -f log/llm.log
```
-## Testing GitLab Duo Chat with predefined questions
+## Testing GitLab Duo Chat against real LLMs
-Because success of answers to user questions in GitLab Duo Chat heavily depends on toolchain and prompts of each tool, it's common that even a minor change in a prompt or a tool impacts processing of some questions. To make sure that a change in the toolchain doesn't break existing functionality, you can use the following rspecs to validate answers to some predefined questions:
+Because success of answers to user questions in GitLab Duo Chat heavily depends
+on toolchain and prompts of each tool, it's common that even a minor change in a
+prompt or a tool impacts processing of some questions.
+
+To make sure that a change in the toolchain doesn't break existing
+functionality, you can use the following RSpec tests to validate answers to some
+predefined questions when using real LLMs:
```ruby
-export OPENAI_API_KEY='<key>'
-export ANTHROPIC_API_KEY='<key>'
-REAL_AI_REQUEST=1 rspec ee/spec/lib/gitlab/llm/chain/agents/zero_shot/executor_spec.rb
+export VERTEX_AI_EMBEDDINGS='true' # if using Vertex embeddings
+export ANTHROPIC_API_KEY='<key>' # can use dev value of Gitlab::CurrentSettings
+export VERTEX_AI_CREDENTIALS='<vertex-ai-credentials>' # can set as dev value of Gitlab::CurrentSettings.vertex_ai_credentials
+export VERTEX_AI_PROJECT='<vertex-project-name>' # can use dev value of Gitlab::CurrentSettings.vertex_ai_project
+
+REAL_AI_REQUEST=1 bundle exec rspec ee/spec/lib/gitlab/llm/chain/agents/zero_shot/executor_real_requests_spec.rb
```
When you need to update the test questions that require documentation embeddings,
make sure a new fixture is generated and committed together with the change.
+## Running the rspecs tagged with `real_ai_request`
+
+The rspecs tagged with the metadata `real_ai_request` can be run in GitLab project's CI by triggering
+`rspec-ee unit gitlab-duo-chat`.
+The former runs with Vertex APIs enabled. The CI jobs are optional and allowed to fail to account for
+the non-deterministic nature of LLM responses.
+
+### Management of credentials and API keys for CI jobs
+
+All API keys required to run the rspecs should be [masked](../../ci/variables/index.md#mask-a-cicd-variable)
+
+The exception is GCP credentials as they contain characters that prevent them from being masked.
+Because `rspec-ee unit gitlab-duo-chat` needs to run on MR branches, GCP credentials cannot be added as a protected variable
+and must be added as a regular CI variable.
+For security, the GCP credentials and the associated project added to
+GitLab project's CI must not be able to access any production infrastructure and sandboxed.
+
## GraphQL Subscription
The GraphQL Subscription for Chat behaves slightly different because it's user-centric. A user could have Chat open on multiple browser tabs, or also on their IDE.
diff --git a/doc/development/ai_features/index.md b/doc/development/ai_features/index.md
index 4401a7e3fb1..df1627f2dc3 100644
--- a/doc/development/ai_features/index.md
+++ b/doc/development/ai_features/index.md
@@ -15,7 +15,6 @@ info: To determine the technical writer assigned to the Stage/Group associated w
- Background workers execute
- GraphQL subscriptions deliver results back in real time
- Abstraction for
- - OpenAI
- Google Vertex AI
- Anthropic
- Rate Limiting
@@ -28,7 +27,6 @@ info: To determine the technical writer assigned to the Stage/Group associated w
- Automatic Markdown Rendering of responses
- Centralised Group Level settings for experiment and 3rd party
- Experimental API endpoints for exploration of AI APIs by GitLab team members without the need for credentials
- - OpenAI
- Google Vertex AI
- Anthropic
@@ -36,7 +34,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
Apply the following two feature flags to any AI feature work:
-- A general that applies to all AI features.
+- A general flag (`ai_global_switch`) that applies to all AI features.
- A flag specific to that feature. The feature flag name [must be different](../feature_flags/index.md#feature-flags-for-licensed-features) than the licensed feature name.
See the [feature flag tracker](https://gitlab.com/gitlab-org/gitlab/-/issues/405161) for the list of all feature flags and how to use them.
@@ -58,20 +56,19 @@ Use [this snippet](https://gitlab.com/gitlab-org/gitlab/-/snippets/2554994) for
1. Enable the required general feature flags:
```ruby
- Feature.enable(:openai_experimentation)
+ Feature.enable(:ai_global_switch, type: :ops)
```
1. Ensure you have followed [the process to obtain an EE license](https://about.gitlab.com/handbook/developer-onboarding/#working-on-gitlab-ee-developer-licenses) for your local instance
1. Simulate the GDK to [simulate SaaS](../ee_features.md#simulate-a-saas-instance) and ensure the group you want to test has an Ultimate license
-1. Enable `Experimental features` and `Third-party AI services`
+1. Enable `Experimental features`:
1. Go to the group with the Ultimate license
1. **Group Settings** > **General** -> **Permissions and group features**
1. Enable **Experiment features**
- 1. Enable **Third-party AI services**
1. Enable the specific feature flag for the feature you want to test
1. Set the required access token. To receive an access token:
1. For Vertex, follow the [instructions below](#configure-gcp-vertex-access).
- 1. For all other providers, like Anthropic or OpenAI, create an access request where `@m_gill`, `@wayne`, and `@timzallmann` are the tech stack owners.
+ 1. For all other providers, like Anthropic, create an access request where `@m_gill`, `@wayne`, and `@timzallmann` are the tech stack owners.
### Set up the embedding database
@@ -117,12 +114,6 @@ In order to obtain a GCP service key for local development, please follow the st
Gitlab::CurrentSettings.update(vertex_ai_project: PROJECT_ID)
```
-### Configure OpenAI access
-
-```ruby
-Gitlab::CurrentSettings.update(openai_api_key: "<open-ai-key>")
-```
-
### Configure Anthropic access
```ruby
@@ -131,36 +122,9 @@ Gitlab::CurrentSettings.update!(anthropic_api_key: <insert API key>)
### Populating embeddings and using embeddings fixture
-Currently we have embeddings generate both with OpenAI and VertexAI. Bellow sections explain how to populate
+Embeddings are generated through VertexAI text embeddings endpoint. The sections below explain how to populate
embeddings in the DB or extract embeddings to be used in specs.
-FLAG:
-We are moving towards having VertexAI embeddings only, so eventually the OpenAI embeddings support will be drop
-as well as the section bellow will be removed.
-
-#### OpenAI embeddings
-
-To seed your development database with the embeddings for GitLab Documentation,
-you may use the pre-generated embeddings and a Rake task.
-
-```shell
-RAILS_ENV=development bundle exec rake gitlab:llm:embeddings:seed_pre_generated
-```
-
-The DBCleaner gem we use clear the database tables before each test runs.
-Instead of fully populating the table `tanuki_bot_mvc` where we store OpenAI embeddings for the documentations,
-we can add a few selected embeddings to the table from a pre-generated fixture.
-
-For instance, to test that the question "How can I reset my password" is correctly
-retrieving the relevant embeddings and answered, we can extract the top N closet embeddings
-to the question into a fixture and only restore a small number of embeddings quickly.
-To facilitate an extraction process, a Rake task been written.
-You can add or remove the questions needed to be tested in the Rake task and run the task to generate a new fixture.
-
-```shell
-RAILS_ENV=development bundle exec rake gitlab:llm:embeddings:extract_embeddings
-```
-
#### VertexAI embeddings
To seed your development database with the embeddings for GitLab Documentation,
@@ -210,9 +174,6 @@ Use the [experimental REST API endpoints](https://gitlab.com/gitlab-org/gitlab/-
The endpoints are:
-- `https://gitlab.example.com/api/v4/ai/experimentation/openai/completions`
-- `https://gitlab.example.com/api/v4/ai/experimentation/openai/embeddings`
-- `https://gitlab.example.com/api/v4/ai/experimentation/openai/chat/completions`
- `https://gitlab.example.com/api/v4/ai/experimentation/anthropic/complete`
- `https://gitlab.example.com/api/v4/ai/experimentation/vertex/chat`
@@ -257,11 +218,9 @@ mutation {
}
```
-The GraphQL API then uses the [OpenAI Client](https://gitlab.com/gitlab-org/gitlab/blob/master/ee/lib/gitlab/llm/open_ai/client.rb)
+The GraphQL API then uses the [Anthropic Client](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/gitlab/llm/anthropic/client.rb)
to send the response.
-Remember that other clients are available and you should not use OpenAI.
-
#### How to receive a response
The API requests to AI providers are handled in a background job. We therefore do not keep the request alive and the Frontend needs to match the request to the response from the subscription.
@@ -302,7 +261,7 @@ To not have many concurrent subscriptions, you should also only subscribe to the
#### Current abstraction layer flow
-The following graph uses OpenAI as an example. You can use different providers.
+The following graph uses VertexAI as an example. You can use different providers.
```mermaid
flowchart TD
@@ -311,9 +270,9 @@ B --> C[Llm::ExecuteMethodService]
C --> D[One of services, for example: Llm::GenerateSummaryService]
D -->|scheduled| E[AI worker:Llm::CompletionWorker]
E -->F[::Gitlab::Llm::Completions::Factory]
-F -->G[`::Gitlab::Llm::OpenAi::Completions::...` class using `::Gitlab::Llm::OpenAi::Templates::...` class]
-G -->|calling| H[Gitlab::Llm::OpenAi::Client]
-H --> |response| I[::Gitlab::Llm::OpenAi::ResponseService]
+F -->G[`::Gitlab::Llm::VertexAi::Completions::...` class using `::Gitlab::Llm::Templates::...` class]
+G -->|calling| H[Gitlab::Llm::VertexAi::Client]
+H --> |response| I[::Gitlab::Llm::GraphqlSubscriptionResponseService]
I --> J[GraphqlTriggers.ai_completion_response]
J --> K[::GitlabSchema.subscriptions.trigger]
```
@@ -419,11 +378,11 @@ end
We recommend to use [policies](../policies.md) to deal with authorization for a feature. Currently we need to make sure to cover the following checks:
-1. General AI feature flag is enabled
+1. General AI feature flag (`ai_global_switch`) is enabled
1. Feature specific feature flag is enabled
1. The namespace has the required license for the feature
1. User is a member of the group/project
-1. `experiment_features_enabled` and `third_party_ai_features_enabled` flags are set on the `Namespace`
+1. `experiment_features_enabled` settings are set on the `Namespace`
For our example, we need to implement the `allowed?(:amazing_new_ai_feature)` call. As an example, you can look at the [Issue Policy for the summarize comments feature](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/app/policies/ee/issue_policy.rb). In our example case, we want to implement the feature for Issues as well:
@@ -436,7 +395,7 @@ module EE
prepended do
with_scope :subject
condition(:ai_available) do
- ::Feature.enabled?(:openai_experimentation)
+ ::Feature.enabled?(:ai_global_switch, type: :ops)
end
with_scope :subject
@@ -501,10 +460,9 @@ Caching has following limitations:
### Check if feature is allowed for this resource based on namespace settings
-There are two settings allowed on root namespace level that restrict the use of AI features:
+There is one setting allowed on root namespace level that restrict the use of AI features:
- `experiment_features_enabled`
-- `third_party_ai_features_enabled`.
To check if that feature is allowed for a given namespace, call:
@@ -512,46 +470,39 @@ To check if that feature is allowed for a given namespace, call:
Gitlab::Llm::StageCheck.available?(namespace, :name_of_the_feature)
```
-Add the name of the feature to the `Gitlab::Llm::StageCheck` class. There are arrays there that differentiate
-between experimental and beta features.
+Add the name of the feature to the `Gitlab::Llm::StageCheck` class. There are
+arrays there that differentiate between experimental and beta features.
This way we are ready for the following different cases:
-- If the feature is not in any array, the check will return `true`. For example, the feature was moved to GA and does not use a third-party setting.
-- If feature is in GA, but uses a third-party setting, the class will return a proper answer based on the namespace third-party setting.
+- If the feature is not in any array, the check will return `true`. For example, the feature was moved to GA.
To move the feature from the experimental phase to the beta phase, move the name of the feature from the `EXPERIMENTAL_FEATURES` array to the `BETA_FEATURES` array.
### Implement calls to AI APIs and the prompts
The `CompletionWorker` will call the `Completions::Factory` which will initialize the Service and execute the actual call to the API.
-In our example, we will use OpenAI and implement two new classes:
+In our example, we will use VertexAI and implement two new classes:
```ruby
-# /ee/lib/gitlab/llm/open_ai/completions/amazing_new_ai_feature.rb
+# /ee/lib/gitlab/llm/vertex_ai/completions/amazing_new_ai_feature.rb
module Gitlab
module Llm
- module OpenAi
+ module VertexAi
module Completions
- class AmazingNewAiFeature
- def initialize(ai_prompt_class)
- @ai_prompt_class = ai_prompt_class
- end
+ class AmazingNewAiFeature < Gitlab::Llm::Completions::Base
+ def execute
+ prompt = ai_prompt_class.new(options[:user_input]).to_prompt
- def execute(user, issue, options)
- options = ai_prompt_class.get_options(options[:messages])
+ response = Gitlab::Llm::VertexAi::Client.new(user).text(content: prompt)
- ai_response = Gitlab::Llm::OpenAi::Client.new(user).chat(content: nil, **options)
+ response_modifier = ::Gitlab::Llm::VertexAi::ResponseModifiers::Predictions.new(response)
- ::Gitlab::Llm::OpenAi::ResponseService.new(user, issue, ai_response, options: {}).execute(
- Gitlab::Llm::OpenAi::ResponseModifiers::Chat.new
- )
+ ::Gitlab::Llm::GraphqlSubscriptionResponseService.new(
+ user, nil, response_modifier, options: response_options
+ ).execute
end
-
- private
-
- attr_reader :ai_prompt_class
end
end
end
@@ -560,28 +511,23 @@ end
```
```ruby
-# /ee/lib/gitlab/llm/open_ai/templates/amazing_new_ai_feature.rb
+# /ee/lib/gitlab/llm/vertex_ai/templates/amazing_new_ai_feature.rb
module Gitlab
module Llm
- module OpenAi
+ module VertexAi
module Templates
class AmazingNewAiFeature
- TEMPERATURE = 0.3
-
- def self.get_options(messages)
- system_content = <<-TEMPLATE
- You are an assistant that writes code for the following input:
- """
- TEMPLATE
-
- {
- messages: [
- { role: "system", content: system_content },
- { role: "user", content: messages },
- ],
- temperature: TEMPERATURE
- }
+ def initialize(user_input)
+ @user_input = user_input
+ end
+
+ def to_prompt
+ <<-PROMPT
+ You are an assistant that writes code for the following context:
+
+ context: #{user_input}
+ PROMPT
end
end
end
diff --git a/doc/development/api_graphql_styleguide.md b/doc/development/api_graphql_styleguide.md
index 3662b21eb9e..318f9bed6d3 100644
--- a/doc/development/api_graphql_styleguide.md
+++ b/doc/development/api_graphql_styleguide.md
@@ -154,7 +154,14 @@ developers must familiarize themselves with our [Deprecation and Removal process
Breaking changes are:
- Removing or renaming a field, argument, enum value, or mutation.
-- Changing the type of a field, argument or enum value.
+- Changing the type or type name of an argument. The type of an argument
+ is declared by the client when [using variables](https://graphql.org/learn/queries/#variables),
+ and a change would cause a query using the old type name to be rejected by the API.
+- Changing the [_scalar type_](https://graphql.org/learn/schema/#scalar-types) of a field or enum
+ value where it results in a change to how the value serializes to JSON.
+ For example, a change from a JSON String to a JSON Number, or a change to how a String is formatted.
+ A change to another [_object type_](https://graphql.org/learn/schema/#object-types-and-fields) can be
+ allowed so long as all scalar type fields of the object continue to serialize in the same way.
- Raising the [complexity](#max-complexity) of a field or complexity multipliers in a resolver.
- Changing a field from being _not_ nullable (`null: false`) to nullable (`null: true`), as
discussed in [Nullable fields](#nullable-fields).
diff --git a/doc/development/backend/create_source_code_be/gitaly_touch_points.md b/doc/development/backend/create_source_code_be/gitaly_touch_points.md
index c689af2f150..98607c7f6c7 100644
--- a/doc/development/backend/create_source_code_be/gitaly_touch_points.md
+++ b/doc/development/backend/create_source_code_be/gitaly_touch_points.md
@@ -19,9 +19,3 @@ All access to Gitaly from other parts of GitLab are through Create: Source Code
After a call is made to Gitaly, Git `commit` information is stored in memory. This information is wrapped by the [Ruby `Commit` Model](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/models/commit.rb), which is a wrapper around [`Gitlab::Git::Commit`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/git/commit.rb).
The `Commit` model acts like an ActiveRecord object, but it does not have a PostgreSQL backend. Instead, it maps back to Gitaly RPCs.
-
-## Rugged Patches
-
-Historically in GitLab, access to the server-based `git` repositories was provided through the [rugged](https://github.com/libgit2/rugged) RubyGem, which provides Ruby bindings to `libgit2`. This was further extended by what is termed "Rugged Patches", [a set of extensions to the Rugged library](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/57317). Rugged implementations of some of the most commonly-used RPCs can be [enabled via feature flags](../../gitaly.md#legacy-rugged-code).
-
-Rugged access requires the use of a NFS file system, a direction GitLab is moving away from in favor of Gitaly Cluster. Rugged has been proposed for [deprecation and removal](https://gitlab.com/gitlab-org/gitaly/-/issues/1690). Several large customers are still using NFS, and a specific removal date is not planned at this point.
diff --git a/doc/development/bulk_import.md b/doc/development/bulk_import.md
index 081af2b4e17..502bee97c9c 100644
--- a/doc/development/bulk_import.md
+++ b/doc/development/bulk_import.md
@@ -51,3 +51,12 @@ and its users.
The migration process starts with the creation of a [`BulkImport`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/models/bulk_import.rb)
record to keep track of the migration. From there all the code related to the
GitLab Group Migration can be found under the new `BulkImports` namespace in all the application layers.
+
+### Idempotency
+
+To ensure we don't get duplicate entries when re-running the same Sidekiq job, we cache each entry as it's processed and skip entries if they're present in the cache.
+
+There are two different strategies:
+
+- `BulkImports::Pipeline::HexdigestCacheStrategy`, which caches a hexdigest representation of the data.
+- `BulkImports::Pipeline::IndexCacheStrategy`, which caches the last processed index of an entry in a pipeline.
diff --git a/doc/development/cells/index.md b/doc/development/cells/index.md
index 30dccd91c9d..1ab88e0d8c6 100644
--- a/doc/development/cells/index.md
+++ b/doc/development/cells/index.md
@@ -16,6 +16,7 @@ To make the application work within the GitLab Cells architecture, we need to fi
Here is the suggested approach:
1. Pick a workflow to fix.
+1. Firstly, we need to find out the tables that are affected while performing the chosen workflow. As an example, in [this note](https://gitlab.com/gitlab-org/gitlab/-/issues/428600#note_1610331742) we have described how to figure out the list of all tables that are affected when a project is created in a group.
1. For each table affected for the chosen workflow, choose the approriate
[GitLab schema](../database/multiple_databases.md#gitlab-schema).
1. Identify all cross-joins, cross-transactions, and cross-database foreign keys for
diff --git a/doc/development/code_review.md b/doc/development/code_review.md
index 8e6ea3d68e9..c2f2a7643ae 100644
--- a/doc/development/code_review.md
+++ b/doc/development/code_review.md
@@ -115,10 +115,10 @@ It picks reviewers and maintainers from the list at the
page, with these behaviors:
- It doesn't pick people whose Slack or [GitLab status](../user/profile/index.md#set-your-current-status):
- - Contains the string `OOO`, `PTO`, `Parental Leave`, or `Friends and Family`.
+ - Contains the string `OOO`, `PTO`, `Parental Leave`, `Friends and Family`, or `Conference`.
- GitLab user **Busy** indicator is set to `True`.
- Emoji is from one of these categories:
- - **On leave** - 🌴 `:palm_tree:`, 🏖️ `:beach:`, ⛱ `:beach_umbrella:`, 🏖 `:beach_with_umbrella:`, 🌞 `:sun_with_face:`, 🎡 `:ferris_wheel:`
+ - **On leave** - 🌴 `:palm_tree:`, 🏖️ `:beach:`, ⛱ `:beach_umbrella:`, 🏖 `:beach_with_umbrella:`, 🌞 `:sun_with_face:`, 🎡 `:ferris_wheel:`, 🏙 `:cityscape:`
- **Out sick** - 🌡️ `:thermometer:`, 🤒 `:face_with_thermometer:`
- **At capacity** - 🔴 `:red_circle:`
- **Focus mode** - 💡 `:bulb:` (focusing on their team's work)
@@ -295,6 +295,10 @@ up confusion or verify that the end result matches what they had in mind, to
database specialists to get input on the data model or specific queries, or to
any other developer to get an in-depth review of the solution.
+If you know you'll need many merge requests to deliver a feature (for example, you created a proof of concept and it is clear the feature will consist of 10+ merge requests),
+consider identifying reviewers and maintainers who possess the necessary understanding of the feature (you share the context with them). Then direct all merge requests to these reviewers.
+The best DRI for finding these reviewers is the EM or Staff Engineer. Having stable reviewer counterparts for multiple merge requests with the same context improves efficiency.
+
If your merge request touches more than one domain (for example, Dynamic Analysis and GraphQL), ask for reviews from an expert from each domain.
If an author is unsure if a merge request needs a [domain expert's](#domain-experts) opinion,
@@ -764,7 +768,7 @@ A merge request may benefit from being considered a customer critical priority b
Properties of customer critical merge requests:
-- The [VP of Development](https://about.gitlab.com/job-families/engineering/development/management/vp/) ([@clefelhocz1](https://gitlab.com/clefelhocz1)) is the approver for deciding if a merge request qualifies as customer critical. Also, if two of his direct reports approve, that can also serve as approval.
+- A senior director or higher in Development must approve that a merge request qualifies as customer-critical. Alternatively, if two of their direct reports approve, that can also serve as approval.
- The DRI applies the `customer-critical-merge-request` label to the merge request.
- It is required that the reviewers and maintainers involved with a customer critical merge request are engaged as soon as this decision is made.
- It is required to prioritize work for those involved on a customer critical merge request so that they have the time available necessary to focus on it.
diff --git a/doc/development/contributing/first_contribution.md b/doc/development/contributing/first_contribution.md
index 3477590f40b..834f34328bc 100644
--- a/doc/development/contributing/first_contribution.md
+++ b/doc/development/contributing/first_contribution.md
@@ -343,7 +343,7 @@ Now you're ready to push changes from the community fork to the main GitLab repo
1. If you're happy with this merge request and want to start the review process, type
`@gitlab-bot ready` in a comment and then select **Comment**.
- ![GitLab bot ready comment](img/bot_ready.png)
+ ![GitLab bot ready comment](img/bot_ready_v16_6.png)
Someone from GitLab will look at your request and let you know what the next steps are.
diff --git a/doc/development/contributing/img/bot_ready.png b/doc/development/contributing/img/bot_ready.png
deleted file mode 100644
index 85116c8957b..00000000000
--- a/doc/development/contributing/img/bot_ready.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/contributing/img/bot_ready_v16_6.png b/doc/development/contributing/img/bot_ready_v16_6.png
new file mode 100644
index 00000000000..a26971eefad
--- /dev/null
+++ b/doc/development/contributing/img/bot_ready_v16_6.png
Binary files differ
diff --git a/doc/development/dangerbot.md b/doc/development/dangerbot.md
index ef1e563b668..476d370e7ee 100644
--- a/doc/development/dangerbot.md
+++ b/doc/development/dangerbot.md
@@ -158,10 +158,9 @@ To enable the Dangerfile on another existing GitLab project, complete the follow
- if: $CI_SERVER_HOST == "gitlab.com"
```
-1. If your project is in the `gitlab-org` group, you don't need to set up any token as the `DANGER_GITLAB_API_TOKEN`
- variable is available at the group level. If not, follow these last steps:
- 1. Create a [Project access tokens](../user/project/settings/project_access_tokens.md).
- 1. Add the token as a CI/CD project variable named `DANGER_GITLAB_API_TOKEN`.
+1. Create a [Project access tokens](../user/project/settings/project_access_tokens.md) with the `api` scope,
+ `Developer` permission (so that it can add labels), and no expiration date (which actually means one year).
+1. Add the token as a CI/CD project variable named `DANGER_GITLAB_API_TOKEN`.
You should add the ~"Danger bot" label to the merge request before sending it
for review.
diff --git a/doc/development/database/avoiding_downtime_in_migrations.md b/doc/development/database/avoiding_downtime_in_migrations.md
index 27ffd356df6..3b4b45935b9 100644
--- a/doc/development/database/avoiding_downtime_in_migrations.md
+++ b/doc/development/database/avoiding_downtime_in_migrations.md
@@ -583,7 +583,7 @@ visualized in Thanos ([see an example](https://thanos-query.ops.gitlab.net/graph
### Swap the columns (release N + 1)
-After the background is completed and the new `bigint` columns are populated for all records, we can
+After the background migration is complete and the new `bigint` columns are populated for all records, we can
swap the columns. Swapping is done with post-deployment migration. The exact process depends on the
table being converted, but in general it's done in the following steps:
@@ -591,8 +591,11 @@ table being converted, but in general it's done in the following steps:
migration has finished ([see an example](https://gitlab.com/gitlab-org/gitlab/-/blob/41fbe34a4725a4e357a83fda66afb382828767b2/db/post_migrate/20210707210916_finalize_ci_stages_bigint_conversion.rb#L13-18)).
If the migration has not completed, the subsequent steps fail anyway. By checking in advance we
aim to have more helpful error message.
-1. Create indexes using the `bigint` columns that match the existing indexes using the `integer`
-column ([see an example](https://gitlab.com/gitlab-org/gitlab/-/blob/41fbe34a4725a4e357a83fda66afb382828767b2/db/post_migrate/20210707210916_finalize_ci_stages_bigint_conversion.rb#L28-34)).
+1. Use the `add_bigint_column_indexes` helper method from `Gitlab::Database::MigrationHelpers::ConvertToBigint` module
+ to create indexes with the `bigint` columns that match the existing indexes using the `integer` column.
+ - The helper method is expected to create all required `bigint` indexes, but it's advised to recheck to make sure
+ we are not missing any of the existing indexes. More information about the helper can be
+ found in merge request [135781](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/135781).
1. Create foreign keys (FK) using the `bigint` columns that match the existing FK using the
`integer` column. Do this both for FK referencing other tables, and FK that reference the table
that is being migrated ([see an example](https://gitlab.com/gitlab-org/gitlab/-/blob/41fbe34a4725a4e357a83fda66afb382828767b2/db/post_migrate/20210707210916_finalize_ci_stages_bigint_conversion.rb#L36-43)).
@@ -603,6 +606,8 @@ that is being migrated ([see an example](https://gitlab.com/gitlab-org/gitlab/-/
1. Swap the defaults ([see an example](https://gitlab.com/gitlab-org/gitlab/-/blob/41fbe34a4725a4e357a83fda66afb382828767b2/db/post_migrate/20210707210916_finalize_ci_stages_bigint_conversion.rb#L59-62)).
1. Swap the PK constraint (if any) ([see an example](https://gitlab.com/gitlab-org/gitlab/-/blob/41fbe34a4725a4e357a83fda66afb382828767b2/db/post_migrate/20210707210916_finalize_ci_stages_bigint_conversion.rb#L64-68)).
1. Remove old indexes and rename new ones ([see an example](https://gitlab.com/gitlab-org/gitlab/-/blob/41fbe34a4725a4e357a83fda66afb382828767b2/db/post_migrate/20210707210916_finalize_ci_stages_bigint_conversion.rb#L70-72)).
+ - Names of the `bigint` indexes created using `add_bigint_column_indexes` helper can be retrieved by calling
+ `bigint_index_name` from `Gitlab::Database::MigrationHelpers::ConvertToBigint` module.
1. Remove old foreign keys (if still present) and rename new ones ([see an example](https://gitlab.com/gitlab-org/gitlab/-/blob/41fbe34a4725a4e357a83fda66afb382828767b2/db/post_migrate/20210707210916_finalize_ci_stages_bigint_conversion.rb#L74)).
See example [merge request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66088), and [migration](https://gitlab.com/gitlab-org/gitlab/-/blob/41fbe34a4725a4e357a83fda66afb382828767b2/db/post_migrate/20210707210916_finalize_ci_stages_bigint_conversion.rb).
diff --git a/doc/development/database/clickhouse/clickhouse_within_gitlab.md b/doc/development/database/clickhouse/clickhouse_within_gitlab.md
index 297776429d7..2f7a3c4dfe0 100644
--- a/doc/development/database/clickhouse/clickhouse_within_gitlab.md
+++ b/doc/development/database/clickhouse/clickhouse_within_gitlab.md
@@ -45,22 +45,39 @@ ClickHouse::Client.select('SELECT 1', :main)
## Database schema and migrations
-For the ClickHouse database there are no established schema migration procedures yet. We have very basic tooling to build up the database schema in the test environment from scratch using timestamp-prefixed SQL files.
-
-You can create a table by placing a new SQL file in the `db/click_house/main` folder:
-
-```sql
-// 20230811124511_create_issues.sql
-CREATE TABLE issues
-(
- id UInt64 DEFAULT 0,
- title String DEFAULT ''
-)
-ENGINE = MergeTree
-PRIMARY KEY (id)
+There are `bundle exec rake gitlab:clickhouse:migrate` and `bundle exec rake gitlab:clickhouse:rollback` tasks
+(introduced in [!136103](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/136103)).
+
+You can create a migration by creating a Ruby migration file in `db/click_house/migrate` folder. It should be prefixed with a timestamp in the format `YYYYMMDDHHMMSS_description_of_migration.rb`
+
+```ruby
+# 20230811124511_create_issues.rb
+# frozen_string_literal: true
+
+class CreateIssues < ClickHouse::Migration
+ def up
+ execute <<~SQL
+ CREATE TABLE issues
+ (
+ id UInt64 DEFAULT 0,
+ title String DEFAULT ''
+ )
+ ENGINE = MergeTree
+ PRIMARY KEY (id)
+ SQL
+ end
+
+ def down
+ execute <<~SQL
+ DROP TABLE sync_cursors
+ SQL
+ end
+end
```
-When you're working locally in your development environment, you can create or re-create your table schema by executing the respective `CREATE TABLE` statement. Alternatively, you can use the following snippet in the Rails console:
+When you're working locally in your development environment, you can create or re-create your table schema by
+executing `rake gitlab:clickhouse:rollback` and `rake gitlab:clickhouse:migrate`.
+Alternatively, you can use the following snippet in the Rails console:
```ruby
require_relative 'spec/support/database/click_house/hooks.rb'
diff --git a/doc/development/database/database_lab.md b/doc/development/database/database_lab.md
index 7edb8ab4de5..7cdf034844d 100644
--- a/doc/development/database/database_lab.md
+++ b/doc/development/database/database_lab.md
@@ -18,7 +18,7 @@ schema changes, like additional indexes or columns, in an isolated copy of produ
1. Select **Sign in with Google**. (Not GitLab, as you need Google SSO to connect with our project.)
1. After you sign in, select the GitLab organization and then visit "Ask Joe" in the sidebar.
1. Select the database you're testing against:
- - Most queries for the GitLab project run against `gitlab-production-tunnel-pg12`.
+ - Most queries for the GitLab project run against `gitlab-production-main`.
- If the query is for a CI table, select `gitlab-production-ci`.
- If the query is for the container registry, select `gitlab-production-registry`.
1. Type `explain <Query Text>` in the chat box to get a plan.
diff --git a/doc/development/database/iterating_tables_in_batches.md b/doc/development/database/iterating_tables_in_batches.md
index 84b82b16255..44a8c72ea2c 100644
--- a/doc/development/database/iterating_tables_in_batches.md
+++ b/doc/development/database/iterating_tables_in_batches.md
@@ -523,14 +523,14 @@ and resumed at any point. This capability is demonstrated in the following code
stop_at = Time.current + 3.minutes
count, last_value = Issue.each_batch_count do
- Time.current > stop_at # condition for stopping the counting
+ stop_at.past? # condition for stopping the counting
end
# Continue the counting later
stop_at = Time.current + 3.minutes
count, last_value = Issue.each_batch_count(last_count: count, last_value: last_value) do
- Time.current > stop_at
+ stop_at.past?
end
```
diff --git a/doc/development/database/loose_foreign_keys.md b/doc/development/database/loose_foreign_keys.md
index fd380bee385..08d618a26ae 100644
--- a/doc/development/database/loose_foreign_keys.md
+++ b/doc/development/database/loose_foreign_keys.md
@@ -251,8 +251,12 @@ When the loose foreign key definition is no longer needed (parent table is remov
we need to remove the definition from the YAML file and ensure that we don't leave pending deleted
records in the database.
-1. Remove the deletion tracking trigger from the parent table (if the parent table is still there).
1. Remove the loose foreign key definition from the configuration (`config/gitlab_loose_foreign_keys.yml`).
+
+The deletion tracking trigger needs to be removed only when the parent table no longer uses loose foreign keys.
+If the model still has at least one `loose_foreign_key` definition remaining, then these steps can be skipped:
+
+1. Remove the trigger from the parent table (if the parent table is still there).
1. Remove leftover deleted records from the `loose_foreign_keys_deleted_records` table.
Migration for removing the trigger:
diff --git a/doc/development/database/multiple_databases.md b/doc/development/database/multiple_databases.md
index 79e1d3c0578..a045d8ad144 100644
--- a/doc/development/database/multiple_databases.md
+++ b/doc/development/database/multiple_databases.md
@@ -49,11 +49,21 @@ The usage of schema enforces the base class to be used:
### Guidelines on choosing between `gitlab_main_cell` and `gitlab_main_clusterwide` schema
+Depending on the use case, your feature may be [cell-local or clusterwide](../../architecture/blueprints/cells/index.md#how-do-i-decide-whether-to-move-my-feature-to-the-cluster-cell-or-organization-level) and hence the tables used for the feature should also use the appropriate schema.
+
When you choose the appropriate schema for tables, consider the following guidelines as part of the [Cells](../../architecture/blueprints/cells/index.md) architecture:
- Default to `gitlab_main_cell`: We expect most tables to be assigned to the `gitlab_main_cell` schema by default. Choose this schema if the data in the table is related to `projects` or `namespaces`.
- Consult with the Tenant Scale group: If you believe that the `gitlab_main_clusterwide` schema is more suitable for a table, seek approval from the Tenant Scale group This is crucial because it has scaling implications and may require reconsideration of the schema choice.
+To understand how existing tables are classified, you can use [this dashboard](https://manojmj.gitlab.io/tenant-scale-schema-progress/).
+
+After a schema has been assigned, the merge request pipeline might fail due to one or more of the following reasons, which can be rectified by following the linked guidelines:
+
+- [Cross-database joins](#suggestions-for-removing-cross-database-joins)
+- [Cross-database transactions](#fixing-cross-database-transactions)
+- [Cross-database foreign keys](#foreign-keys-that-cross-databases)
+
### The impact of `gitlab_schema`
The usage of `gitlab_schema` has a significant impact on the application.
diff --git a/doc/development/database/understanding_explain_plans.md b/doc/development/database/understanding_explain_plans.md
index 92688eb01dc..3e8978e1046 100644
--- a/doc/development/database/understanding_explain_plans.md
+++ b/doc/development/database/understanding_explain_plans.md
@@ -352,7 +352,6 @@ Indexes:
"index_users_on_static_object_token" UNIQUE, btree (static_object_token)
"index_users_on_unlock_token" UNIQUE, btree (unlock_token)
"index_on_users_name_lower" btree (lower(name::text))
- "index_users_on_accepted_term_id" btree (accepted_term_id)
"index_users_on_admin" btree (admin)
"index_users_on_created_at" btree (created_at)
"index_users_on_email_trigram" gin (email gin_trgm_ops)
diff --git a/doc/development/development_processes.md b/doc/development/development_processes.md
index 1cdf667a35f..fa221d5b51f 100644
--- a/doc/development/development_processes.md
+++ b/doc/development/development_processes.md
@@ -1,7 +1,7 @@
---
stage: none
group: unassigned
-info: "See the Technical Writers assigned to Development Guidelines: https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments-to-development-guidelines"
+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.
---
# Development processes
@@ -35,32 +35,12 @@ Complementary reads:
### Development guidelines review
-When you submit a change to the GitLab development guidelines, who
-you ask for reviews depends on the level of change.
+For changes to development guidelines, request review and approval from an experienced GitLab Team Member.
-#### Wording, style, or link changes
-
-Not all changes require extensive review. For example, MRs that don't change the
-content's meaning or function can be reviewed, approved, and merged by any
-maintainer or Technical Writer. These can include:
-
-- Typo fixes.
-- Clarifying links, such as to external programming language documentation.
-- Changes to comply with the [Documentation Style Guide](documentation/index.md)
- that don't change the intent of the documentation page.
-
-#### Specific changes
-
-If the MR proposes changes that are limited to a particular stage, group, or team,
-request a review and approval from an experienced GitLab Team Member in that
-group. For example, if you're documenting a new internal API used exclusively by
+For example, if you're documenting a new internal API used exclusively by
a given group, request an engineering review from one of the group's members.
-After the engineering review is complete, assign the MR to the
-[Technical Writer associated with the stage and group](https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments)
-in the modified documentation page's metadata.
-If the page is not assigned to a specific group, follow the
-[Technical Writing review process for development guidelines](https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments-to-development-guidelines).
+Small fixes, like typos, can be merged by any user with at least the Maintainer role.
#### Broader changes
@@ -85,7 +65,6 @@ In these cases, use the following workflow:
- [Quality](https://about.gitlab.com/handbook/engineering/quality/)
- [Engineering Productivity](https://about.gitlab.com/handbook/engineering/quality/engineering-productivity/)
- [Infrastructure](https://about.gitlab.com/handbook/engineering/infrastructure/)
- - [Technical Writing](https://about.gitlab.com/handbook/product/ux/technical-writing/)
You can skip this step for MRs authored by EMs or Staff Engineers responsible
for their area.
@@ -97,15 +76,15 @@ In these cases, use the following workflow:
author / approver of the MR.
If this is a significant change across multiple areas, request final review
- and approval from the VP of Development, the DRI for Development Guidelines,
- @clefelhocz1.
+ and approval from the VP of Development, who is the DRI for development guidelines.
+
+Any Maintainer can merge the MR.
-1. After all approvals are complete, assign the MR to the
- [Technical Writer associated with the stage and group](https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments)
- in the modified documentation page's metadata.
- If the page is not assigned to a specific group, follow the
- [Technical Writing review process for development guidelines](https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments-to-development-guidelines).
- The Technical Writer may ask for additional approvals as previously suggested before merging the MR.
+#### Technical writing reviews
+
+If you would like a review by a technical writer, post a message in the `#docs` Slack channel.
+Technical writers do not need to review the content, however, and any Maintainer
+other than the MR author can merge.
### Reviewer values
@@ -114,6 +93,8 @@ In these cases, use the following workflow:
As a reviewer or as a reviewee, make sure to familiarize yourself with
the [reviewer values](https://about.gitlab.com/handbook/engineering/workflow/reviewer-values/) we strive for at GitLab.
+Also, any doc content should follow the [Documentation Style Guide](documentation/index.md).
+
## Language-specific guides
### Go guides
@@ -123,3 +104,13 @@ the [reviewer values](https://about.gitlab.com/handbook/engineering/workflow/rev
### Shell Scripting guides
- [Shell scripting standards and style guidelines](shell_scripting_guide/index.md)
+
+## Clear written communication
+
+While writing any comment in an issue or merge request or any other mode of communication,
+follow [IETF standard](https://www.ietf.org/rfc/rfc2119.txt) while using terms like
+"MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT","RECOMMENDED", "MAY",
+and "OPTIONAL".
+
+This ensures that different team members from different cultures have a clear understanding of
+the terms being used.
diff --git a/doc/development/distributed_tracing.md b/doc/development/distributed_tracing.md
index da6af8b95ef..56c114ba8de 100644
--- a/doc/development/distributed_tracing.md
+++ b/doc/development/distributed_tracing.md
@@ -221,8 +221,8 @@ This configuration string uses the Jaeger driver `opentracing://jaeger` with the
| Name | Example | Description |
|------|-------|-------------|
| `udp_endpoint` | `localhost:6831` | This is the default. Configures Jaeger to send trace information to the UDP listener on port `6831` using compact thrift protocol. Note that we've experienced some issues with the [Jaeger Client for Ruby](https://github.com/salemove/jaeger-client-ruby) when using this protocol. |
-| `sampler` | `probabalistic` | Configures Jaeger to use a probabilistic random sampler. The rate of samples is configured by the `sampler_param` value. |
-| `sampler_param` | `0.01` | Use a ratio of `0.01` to configure the `probabalistic` sampler to randomly sample _1%_ of traces. |
+| `sampler` | `probabilistic` | Configures Jaeger to use a probabilistic random sampler. The rate of samples is configured by the `sampler_param` value. |
+| `sampler_param` | `0.01` | Use a ratio of `0.01` to configure the `probabilistic` sampler to randomly sample _1%_ of traces. |
| `service_name` | `api` | Override the service name used by the Jaeger backend. This parameter takes precedence over the application-supplied value. |
NOTE:
diff --git a/doc/development/documentation/styleguide/index.md b/doc/development/documentation/styleguide/index.md
index c3df15f1890..6158d60a0ba 100644
--- a/doc/development/documentation/styleguide/index.md
+++ b/doc/development/documentation/styleguide/index.md
@@ -1281,11 +1281,10 @@ You can use an automatic screenshot generator to take and compress screenshots.
#### Extending the tool
-To add an additional **screenshot generator**, complete the following steps:
+To add an additional screenshot generator:
-1. Locate the `spec/docs_screenshots` directory.
-1. Add a new file with a `_docs.rb` extension.
-1. Be sure to include the following information in the file:
+1. In the `spec/docs_screenshots` directory, add a new file with a `_docs.rb` extension.
+1. Add the following information to your file:
```ruby
require 'spec_helper'
@@ -1298,29 +1297,29 @@ To add an additional **screenshot generator**, complete the following steps:
end
```
-1. In addition, every `it` block must include the path where the screenshot is saved:
+1. To each `it` block, add the path where the screenshot is saved:
```ruby
- it 'user/packages/container_registry/img/project_image_repositories_list'
+ it '<path/to/images/directory>'
```
-##### Full page screenshots
+You can take a screenshot of a page with `visit <path>`.
+To avoid blank screenshots, use `expect` to wait for the content to load.
-To take a full page screenshot, `visit the page` and perform any expectation on real content (to have capybara wait till the page is ready and not take a white screenshot).
+##### Single-element screenshots
-##### Element screenshot
+You can take a screenshot of a single element.
-To have the screenshot focuses few more steps are needed:
+- Add the following to your screenshot generator file:
-- **find the area**: `screenshot_area = find('#js-registry-policies')`
-- **scroll the area in focus**: `scroll_to screenshot_area`
-- **wait for the content**: `expect(screenshot_area).to have_content 'Expiration interval'`
-- **set the crop area**: `set_crop_data(screenshot_area, 20)`
-
-In particular, `set_crop_data` accepts as arguments: a `DOM` element and a
-padding. The padding is added around the element, enlarging the screenshot area.
+ ```ruby
+ screenshot_area = find('<element>') # Find the element
+ scroll_to screenshot_area # Scroll to the element
+ expect(screenshot_area).to have_content '<content>' # Wait for the content you want to capture
+ set_crop_data(screenshot_area, <padding>) # Capture the element with added padding
+ ```
-Use `spec/docs_screenshots/container_registry_docs.rb` as a guide and as an example to create your own scripts.
+Use `spec/docs_screenshots/container_registry_docs.rb` as a guide to create your own scripts.
## Emoji
@@ -1731,6 +1730,7 @@ Some pages won't have a tier badge, because no obvious tier badge applies. For e
- Tutorials.
- Pages that compare features from different tiers.
- Pages in the `/development` folder. These pages are automatically assigned a `Contribute` badge.
+- Pages in the `/solutions` folder. These pages are automatically assigned a `Solutions` badge.
##### Administrator documentation tier badges
diff --git a/doc/development/documentation/styleguide/word_list.md b/doc/development/documentation/styleguide/word_list.md
index ad2cbee974b..1888d72f991 100644
--- a/doc/development/documentation/styleguide/word_list.md
+++ b/doc/development/documentation/styleguide/word_list.md
@@ -26,6 +26,15 @@ For guidance not on this page, we defer to these style guides:
<!-- Disable trailing punctuation in heading rule https://github.com/DavidAnson/markdownlint/blob/main/doc/Rules.md#md026---trailing-punctuation-in-heading -->
<!-- markdownlint-disable MD026 -->
+## `.gitlab-ci.yml` file
+
+Use backticks and lowercase for **the `.gitlab-ci.yml` file**.
+
+When possible, use the full phrase: **the `.gitlab-ci.yml` file**
+
+Although users can specify another name for their CI/CD configuration file,
+in most cases, use **the `.gitlab-ci.yml` file** instead.
+
## `&`
Do not use Latin abbreviations. Use **and** instead, unless you are documenting a UI element that uses an `&`.
@@ -383,9 +392,14 @@ Use **confirmation dialog** to describe the dialog that asks you to confirm an a
Do not use **confirmation box** or **confirmation dialog box**. See also [**dialog**](#dialog).
-## Container Registry
+## container registry
+
+When documenting the GitLab container registry features and functionality, use lower case.
+
+Use:
-Use title case for the GitLab Container Registry.
+- The GitLab container registry supports A, B, and C.
+- You can push a Docker image to your project's container registry.
## currently
@@ -783,7 +797,9 @@ Do not use **handy**. If the user doesn't find the feature or process to be hand
## high availability, HA
-Do not use **high availability** or **HA**. Instead, direct readers to the GitLab [reference architectures](../../../administration/reference_architectures/index.md) for information about configuring GitLab for handling greater amounts of users.
+Do not use **high availability** or **HA**, except in the GitLab [reference architectures](../../../administration/reference_architectures/index.md#high-availability-ha). Instead, direct readers to the reference architectures for more information about configuring GitLab for handling greater amounts of users.
+
+Do not use phrases like **high availability setup** to mean a multiple node environment. Instead, use **multi-node setup** or similar.
## higher
@@ -1303,6 +1319,14 @@ For example, you might write something like:
Use lowercase for **push rules**.
+## `README` file
+
+Use backticks and lowercase for **the `README` file**, or **the `README.md` file**.
+
+When possible, use the full phrase: **the `README` file**
+
+For plural, use **`README` files**.
+
## recommend, we recommend
Instead of **we recommend**, use **you should**. We want to talk to the user the way
diff --git a/doc/development/documentation/versions.md b/doc/development/documentation/versions.md
index dadae134f4c..bd83ed7eff2 100644
--- a/doc/development/documentation/versions.md
+++ b/doc/development/documentation/versions.md
@@ -119,9 +119,8 @@ To deprecate a page or topic:
You can add any additional context-specific details that might help users.
-1. Add the following HTML comments above and below the content.
- For `remove_date`, set a date three months after the release where it
- will be removed.
+1. Add the following HTML comments above and below the content. For `remove_date`,
+ set a date three months after the [release where it will be removed](https://about.gitlab.com/releases/).
```markdown
<!--- start_remove The following content will be removed on remove_date: 'YYYY-MM-DD' -->
diff --git a/doc/development/documentation/workflow.md b/doc/development/documentation/workflow.md
index eb1ea28d3b8..5c99f5c48df 100644
--- a/doc/development/documentation/workflow.md
+++ b/doc/development/documentation/workflow.md
@@ -36,6 +36,13 @@ A member of the Technical Writing team adds these labels:
`docs::` prefix. For example, `~docs::improvement`.
- The [`~Technical Writing` team label](../labels/index.md#team-labels).
+NOTE:
+With the exception of `/doc/development/documentation`,
+technical writers do not review content in the `doc/development` directory.
+Any Maintainer can merge content in the `doc/development` directory.
+If you would like a technical writer review of content in the `doc/development` directory,
+ask in the `#docs` Slack channel.
+
## Post-merge reviews
If not assigned to a Technical Writer for review prior to merging, a review must be scheduled
@@ -65,6 +72,11 @@ Remember:
- The Technical Writer can also help decide that documentation can be merged without Technical
writer review, with the review to occur soon after merge.
+## Pages with no tech writer review
+
+The documentation under `/doc/solutions` is created, maintained, copy edited,
+and merged by the Solutions Architect team.
+
## Do not use ChatGPT or AI-generated content for the docs
GitLab documentation is distributed under the [CC BY-SA 4.0 license](https://creativecommons.org/licenses/by-sa/4.0/), which presupposes that GitLab owns the documentation.
diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md
index 10943b2d135..d05249f3d3f 100644
--- a/doc/development/ee_features.md
+++ b/doc/development/ee_features.md
@@ -38,10 +38,10 @@ context rich definitions around the reason the feature is SaaS-only.
1. Add the new feature to `FEATURE` in `ee/lib/ee/gitlab/saas.rb`.
```ruby
- FEATURES = %w[purchases/additional_minutes some_domain/new_feature_name].freeze
+ FEATURES = %i[purchases_additional_minutes some_domain_new_feature_name].freeze
```
-1. Use the new feature in code with `Gitlab::Saas.feature_available?('some_domain/new_feature_name')`.
+1. Use the new feature in code with `Gitlab::Saas.feature_available?(:some_domain_new_feature_name)`.
#### SaaS-only feature definition and validation
@@ -68,7 +68,7 @@ Each SaaS feature is defined in a separate YAML file consisting of a number of f
Prepend the `ee/lib/ee/gitlab/saas.rb` module and override the `Gitlab::Saas.feature_available?` method.
```ruby
-JH_DISABLED_FEATURES = %w[some_domain/new_feature_name].freeze
+JH_DISABLED_FEATURES = %i[some_domain_new_feature_name].freeze
override :feature_available?
def feature_available?(feature)
@@ -78,7 +78,7 @@ end
### Do not use SaaS-only features for functionality in CE
-`Gitlab::Saas.feature_vailable?` must not appear in CE.
+`Gitlab::Saas.feature_available?` must not appear in CE.
See [extending CE with EE guide](#extend-ce-features-with-ee-backend-code).
### SaaS-only features in tests
@@ -88,30 +88,30 @@ It is strongly advised to include automated tests for all code affected by a Saa
to ensure the feature works properly.
To enable a SaaS-only feature in a test, use the `stub_saas_features`
-helper. For example, to globally disable the `purchases/additional_minutes` feature
+helper. For example, to globally disable the `purchases_additional_minutes` feature
flag in a test:
```ruby
-stub_saas_features('purchases/additional_minutes' => false)
+stub_saas_features(purchases_additional_minutes: false)
-::Gitlab::Saas.feature_available?('purchases/additional_minutes') # => false
+::Gitlab::Saas.feature_available?(:purchases_additional_minutes) # => false
```
A common pattern of testing both paths looks like:
```ruby
it 'purchases/additional_minutes is not available' do
- # tests assuming purchases/additional_minutes is not enabled by default
- ::Gitlab::Saas.feature_available?('purchases/additional_minutes') # => false
+ # tests assuming purchases_additional_minutes is not enabled by default
+ ::Gitlab::Saas.feature_available?(:purchases_additional_minutes) # => false
end
-context 'when purchases/additional_minutes is available' do
+context 'when purchases_additional_minutes is available' do
before do
- stub_saas_features('purchases/additional_minutes' => true)
+ stub_saas_features(purchases_additional_minutes: true)
end
it 'returns true' do
- ::Gitlab::Saas.feature_available?('purchases/additional_minutes') # => true
+ ::Gitlab::Saas.feature_available?(:purchases_additional_minutes) # => true
end
end
```
diff --git a/doc/development/experiment_guide/implementing_experiments.md b/doc/development/experiment_guide/implementing_experiments.md
index 83369ad8e34..15b8f8fc192 100644
--- a/doc/development/experiment_guide/implementing_experiments.md
+++ b/doc/development/experiment_guide/implementing_experiments.md
@@ -8,7 +8,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
## Implementing an experiment
-[Examples](https://gitlab.com/gitlab-org/growth/growth/-/wikis/GLEX-Framework-code-examples)
+[Examples](https://gitlab.com/groups/gitlab-org/growth/-/wikis/GLEX-How-Tos)
Start by generating a feature flag using the `bin/feature-flag` command as you
usually would for a development feature flag, making sure to use `experiment` for
diff --git a/doc/development/export_csv.md b/doc/development/export_csv.md
index 9b0205166bf..ce0a6e026ff 100644
--- a/doc/development/export_csv.md
+++ b/doc/development/export_csv.md
@@ -10,7 +10,7 @@ This document lists the different implementations of CSV export in GitLab codeba
| Export type | How it works | Advantages | Disadvantages | Existing examples |
|---|---|---|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| Streaming | - Query and yield data in batches to a response stream.<br>- Download starts immediately. | - Report available immediately. | - No progress indicator.<br>- Requires a reliable connection. | [Export Audit Event Log](../administration/audit_events.md#export-to-csv) |
+| Streaming | - Query and yield data in batches to a response stream.<br>- Download starts immediately. | - Report available immediately. | - No progress indicator.<br>- Requires a reliable connection. | [Export Audit Event Log](../administration/audit_events.md#exporting-audit-events) |
| Downloading | - Query and write data in batches to a temporary file.<br>- Loads the file into memory.<br>- Sends the file to the client. | - Report available immediately. | - Large amount of data might cause request timeout.<br>- Memory intensive.<br>- Request expires when user navigates to a different page. | - [Export Chain of Custody Report](../user/compliance/compliance_center/index.md#chain-of-custody-report)<br>- [Export License Usage File](../subscriptions/self_managed/index.md#export-your-license-usage) |
| As email attachment | - Asynchronously process the query with background job.<br>- Email uses the export as an attachment. | - Asynchronous processing. | - Requires users use a different app (email) to download the CSV.<br>- Email providers may limit attachment size. | - [Export issues](../user/project/issues/csv_export.md)<br>- [Export merge requests](../user/project/merge_requests/csv_export.md) |
| As downloadable link in email (*) | - Asynchronously process the query with background job.<br>- Email uses an export link. | - Asynchronous processing.<br>- Bypasses email provider attachment size limit. | - Requires users use a different app (email).<br>- Requires additional storage and cleanup. | [Export User Permissions](https://gitlab.com/gitlab-org/gitlab/-/issues/1772) |
diff --git a/doc/development/fe_guide/graphql.md b/doc/development/fe_guide/graphql.md
index 99070f3d31c..5807c9c5621 100644
--- a/doc/development/fe_guide/graphql.md
+++ b/doc/development/fe_guide/graphql.md
@@ -974,28 +974,6 @@ const data = store.readQuery({
Read more about the `@connection` directive in [Apollo's documentation](https://www.apollographql.com/docs/react/caching/advanced-topics/#the-connection-directive).
-### Managing performance
-
-The Apollo client batches queries by default. Given 3 deferred queries,
-Apollo groups them into one request, sends the single request to the server, and
-responds after all 3 queries have completed.
-
-If you need to have queries sent as individual requests, additional context can be provided
-to tell Apollo to do this.
-
-```javascript
-export default {
- apollo: {
- user: {
- query: QUERY_IMPORT,
- context: {
- isSingleRequest: true,
- }
- }
- },
-};
-```
-
#### Polling and Performance
While the Apollo client has support for simple polling, for performance reasons, our [ETag-based caching](../polling.md) is preferred to hitting the database each time.
@@ -1081,21 +1059,6 @@ await this.$apollo.mutate({
});
```
-ETags depend on the request being a `GET` instead of GraphQL's usual `POST`. Our default link library does not support `GET` requests, so we must let our default Apollo client know to use a different library. Keep in mind, this means your app cannot batch queries.
-
-```javascript
-/* componentMountIndex.js */
-
-const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(
- {},
- {
- useGet: true,
- },
- ),
-});
-```
-
Finally, we can add a visibility check so that the component pauses polling when the browser tab is not active. This should lessen the request load on the page.
```javascript
diff --git a/doc/development/fe_guide/security.md b/doc/development/fe_guide/security.md
index d578449e578..4e06c22b383 100644
--- a/doc/development/fe_guide/security.md
+++ b/doc/development/fe_guide/security.md
@@ -12,57 +12,6 @@ info: To determine the technical writer assigned to the Stage/Group associated w
[Qualys SSL Labs Server Test](https://www.ssllabs.com/ssltest/analyze.html) are good resources for finding
potential problems and ensuring compliance with security best practices.
-<!-- Uncomment these sections when CSP/SRI are implemented.
-### Content Security Policy (CSP)
-
-Content Security Policy is a web standard that intends to mitigate certain
-forms of Cross-Site Scripting (XSS) as well as data injection.
-
-Content Security Policy rules should be taken into consideration when
-implementing new features, especially those that may rely on connection with
-external services.
-
-GitLab's CSP is used for the following:
-
-- Blocking plugins like Flash and Silverlight from running at all on our pages.
-- Blocking the use of scripts and stylesheets downloaded from external sources.
-- Upgrading `http` requests to `https` when possible.
-- Preventing `iframe` elements from loading in most contexts.
-
-Some exceptions include:
-
-- Scripts from Google Analytics and Matomo if either is enabled.
-- Connecting with GitHub, Bitbucket, GitLab.com, etc. to allow project importing.
-- Connecting with Google, Twitter, GitHub, etc. to allow OAuth authentication.
-
-We use [the Secure Headers gem](https://github.com/twitter/secureheaders) to enable Content
-Security Policy headers in the GitLab Rails app.
-
-Some resources on implementing Content Security Policy:
-
-- [MDN Article on CSP](https://developer.mozilla.org/en-US/docs/Web/Security/CSP)
-- [GitHub's CSP Journey on the GitHub Engineering Blog](https://github.blog/2016-04-12-githubs-csp-journey/)
-- The Dropbox Engineering Blog's series on CSP: [1](https://blogs.dropbox.com/tech/2015/09/on-csp-reporting-and-filtering/), [2](https://blogs.dropbox.com/tech/2015/09/unsafe-inline-and-nonce-deployment/), [3](https://blogs.dropbox.com/tech/2015/09/csp-the-unexpected-eval/), [4](https://blogs.dropbox.com/tech/2015/09/csp-third-party-integrations-and-privilege-separation/)
-
-### Subresource Integrity (SRI)
-
-Subresource Integrity prevents malicious assets from being provided by a CDN by
-guaranteeing that the asset downloaded is identical to the asset the server
-is expecting.
-
-The Rails app generates a unique hash of the asset, which is used as the
-asset's `integrity` attribute. The browser generates the hash of the asset
-on-load and will reject the asset if the hashes do not match.
-
-All CSS and JavaScript assets should use Subresource Integrity.
-
-Some resources on implementing Subresource Integrity:
-
-- [MDN Article on SRI](https://developer.mozilla.org/en-us/docs/web/security/subresource_integrity)
-- [Subresource Integrity on the GitHub Engineering Blog](https://github.blog/2015-09-19-subresource-integrity/)
-
--->
-
## Including external resources
External fonts, CSS, and JavaScript should never be used with the exception of
diff --git a/doc/development/fe_guide/sentry.md b/doc/development/fe_guide/sentry.md
index 929de1499c7..95a170b7976 100644
--- a/doc/development/fe_guide/sentry.md
+++ b/doc/development/fe_guide/sentry.md
@@ -39,7 +39,7 @@ to our Sentry instance under the project
The most common way to report errors to Sentry is to call `captureException(error)`, for example:
```javascript
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
try {
// Code that may fail in runtime
@@ -53,6 +53,9 @@ about, or have no control over. For example, we shouldn't report validation erro
out a form incorrectly. However, if that form submission fails because or a server error,
this is an error we want Sentry to know about.
+By default your local development instance does not have Sentry configured. Calls to Sentry are
+stubbed and shown in the console with a `[Sentry stub]` prefix for debugging.
+
### Unhandled/unknown errors
Additionally, we capture unhandled errors automatically in all of our pages.
diff --git a/doc/development/fe_guide/storybook.md b/doc/development/fe_guide/storybook.md
index 6049dd7c7d3..cbda9d5efa2 100644
--- a/doc/development/fe_guide/storybook.md
+++ b/doc/development/fe_guide/storybook.md
@@ -135,3 +135,37 @@ export const Default = Template.bind({});
Default.args = {};
```
+
+## Using a Vuex store
+
+To write a story for a component that requires access to a Vuex store, use the `createVuexStore` method provided in
+the Story context.
+
+```javascript
+import Vue from 'vue';
+import { withVuexStore } from 'storybook_addons/vuex_store';
+import DurationChart from './duration-chart.vue';
+
+const Template = (_, { argTypes, createVuexStore }) => {
+ return {
+ components: { DurationChart },
+ store: createVuexStore({
+ state: {},
+ getters: {},
+ modules: {},
+ }),
+ props: Object.keys(argTypes),
+ template: '<duration-chart />',
+ };
+};
+
+export default {
+ component: DurationChart,
+ title: 'ee/analytics/cycle_analytics/components/duration_chart',
+ decorators: [withVuexStore],
+};
+
+export const Default = Template.bind({});
+
+Default.args = {};
+```
diff --git a/doc/development/fe_guide/style/scss.md b/doc/development/fe_guide/style/scss.md
index e760b0adaaa..400b178d9a4 100644
--- a/doc/development/fe_guide/style/scss.md
+++ b/doc/development/fe_guide/style/scss.md
@@ -6,18 +6,11 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# SCSS style guide
-This style guide recommends best practices for SCSS to make styles easy to read,
-easy to maintain, and performant for the end-user.
-
-## Rules
-
-Our CSS is a mixture of current and legacy approaches. That means sometimes it may be difficult to follow this guide to the letter; it means you are likely to run into exceptions, where following the guide is difficult to impossible without major effort. In those cases, you may work with your reviewers and maintainers to identify an approach that does not fit these rules. Try to limit these cases.
-
-### Utility Classes
+## Utility Classes
In order to reduce the generation of more CSS as our site grows, prefer the use of utility classes over adding new CSS. In complex cases, CSS can be addressed by adding component classes.
-#### Where are utility classes defined?
+### Where are utility classes defined?
Prefer the use of [utility classes defined in GitLab UI](https://gitlab.com/gitlab-org/gitlab-ui/-/blob/main/doc/css.md#utilities).
@@ -27,6 +20,8 @@ An easy list of classes can also be [seen on Unpkg](https://unpkg.com/browse/@gi
<!-- vale gitlab.Spelling = YES -->
+Or using an extension like [CSS Class completion](https://marketplace.visualstudio.com/items?itemName=Zignd.html-css-class-completion).
+
Classes in [`utilities.scss`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/assets/stylesheets/utilities.scss) and [`common.scss`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/assets/stylesheets/framework/common.scss) are being deprecated.
Classes in [`common.scss`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/assets/stylesheets/framework/common.scss) that use non-design-system values should be avoided. Use classes with conforming values instead.
@@ -40,13 +35,13 @@ GitLab differs from the scale used in the Bootstrap library. For a Bootstrap pad
utility, you may need to double the size of the applied utility to achieve the same visual
result (such as `ml-1` becoming `gl-ml-2`).
-#### Where should you put new utility classes?
+### Where should you put new utility classes?
If a class you need has not been added to GitLab UI, you get to add it! Follow the naming patterns documented in the [utility files](https://gitlab.com/gitlab-org/gitlab-ui/-/tree/main/src/scss/utility-mixins) and refer to the [GitLab UI CSS documentation](https://gitlab.com/gitlab-org/gitlab-ui/-/blob/main/doc/contributing/adding_css.md#adding-utility-mixins) for more details, especially about adding responsive and stateful rules.
If it is not possible to wait for a GitLab UI update (generally one day), add the class to [`utilities.scss`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/assets/stylesheets/utilities.scss) following the same naming conventions documented in GitLab UI. A follow-up issue to backport the class to GitLab UI and delete it from GitLab should be opened.
-#### When should you create component classes?
+### When should you create component classes?
We recommend a "utility-first" approach.
@@ -60,7 +55,7 @@ Inspiration:
- <https://tailwindcss.com/docs/utility-first>
- <https://tailwindcss.com/docs/extracting-components>
-#### Utility mixins
+### Utility mixins
In addition to utility classes GitLab UI provides utility mixins named after the utility classes.
@@ -95,7 +90,7 @@ For example prefer `display: flex` over `@include gl-display-flex`. Utility mixi
}
```
-### Naming
+## Naming
Filenames should use `snake_case`.
@@ -119,6 +114,23 @@ CSS classes should use the `lowercase-hyphenated` format rather than
}
```
+Avoid making compound class names with SCSS `&` features. It makes
+searching for usages harder, and provides limited benefit.
+
+```scss
+// Bad
+.class {
+ &-name {
+ color: orange;
+ }
+}
+
+// Good
+.class-name {
+ color: #fff;
+}
+```
+
Class names should be used instead of tag name selectors.
Using tag name selectors is discouraged because they can affect
unintended elements in the hierarchy.
@@ -154,53 +166,47 @@ the page.
}
```
-### Selectors with a `js-` Prefix
-
-Do not use any selector prefixed with `js-` for styling purposes. These
-selectors are intended for use only with JavaScript to allow for removal or
-renaming without breaking styling.
-
-### Variables
-
-Before adding a new variable for a color or a size, guarantee:
-
-- There isn't an existing one.
-- There isn't a similar one we can use instead.
-
-### Using `extend` at-rule
+## Nesting
-Usage of the `extend` at-rule is prohibited due to [memory leaks](https://gitlab.com/gitlab-org/gitlab/-/issues/323021) and [the rule doesn't work as it should to](https://sass-lang.com/documentation/breaking-changes/extend-compound). Use mixins instead:
+Avoid unnecessary nesting. The extra specificity of a wrapper component
+makes things harder to override.
```scss
// Bad
-.gl-pt-3 {
- padding-top: 12px;
-}
-
-.my-element {
- @extend .gl-pt-3;
-}
+.component-container {
+ .component-header {
+ /* ... */
+ }
-// compiles to
-.gl-pt-3, .my-element {
- padding-top: 12px;
+ .component-body {
+ /* ... */
+ }
}
// Good
-@mixin gl-pt-3 {
- padding-top: 12px;
+.component-container {
+ /* ... */
}
-.my-element {
- @include gl-pt-3;
+.component-header {
+ /* ... */
}
-// compiles to
-.my-element {
- padding-top: 12px;
+.component-body {
+ /* ... */
}
```
+## Selectors with a `js-` Prefix
+
+Do not use any selector prefixed with `js-` for styling purposes. These
+selectors are intended for use only with JavaScript to allow for removal or
+renaming without breaking styling.
+
+## Using `extend` at-rule
+
+Usage of the `extend` at-rule is prohibited due to [memory leaks](https://gitlab.com/gitlab-org/gitlab/-/issues/323021) and [the rule doesn't work as it should](https://sass-lang.com/documentation/breaking-changes/extend-compound).
+
## Linting
We use [stylelint](https://stylelint.io) to check for style guide conformity. It uses the
diff --git a/doc/development/fe_guide/style/typescript.md b/doc/development/fe_guide/style/typescript.md
new file mode 100644
index 00000000000..529459097b4
--- /dev/null
+++ b/doc/development/fe_guide/style/typescript.md
@@ -0,0 +1,215 @@
+---
+type: reference, dev
+stage: none
+group: unassigned
+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
+---
+
+# TypeScript
+
+## History with GitLab
+
+TypeScript has been [considered](https://gitlab.com/gitlab-org/frontend/rfcs/-/issues/35),
+discussed, promoted, and rejected for years at GitLab. The general
+conclusion is that we are unable to integrate TypeScript into the main
+project because the costs outweigh the benefits.
+
+- The main project has **a lot** of pre-existing code that is not strongly typed.
+- The main contributors to the main project are not all familiar with TypeScript.
+
+Apart from the main project, TypeScript has been profitably employed in
+a handful of satellite projects.
+
+## Projects using TypeScript
+
+The following GitLab projects use TypeScript:
+
+- [`gitlab-web-ide`](https://gitlab.com/gitlab-org/gitlab-web-ide/)
+- [`gitlab-vscode-extension`](https://gitlab.com/gitlab-org/gitlab-vscode-extension/)
+- [`gitlab-language-server-for-code-suggestions`](https://gitlab.com/gitlab-org/editor-extensions/gitlab-language-server-for-code-suggestions)
+- [`gitlab-org/cluster-integration/javascript-client`](https://gitlab.com/gitlab-org/cluster-integration/javascript-client)
+
+## Recommendations
+
+### Setup ESLint and TypeScript configuration
+
+When setting up a new TypeScript project, configure strict type-safety rules for
+ESLint and TypeScript. This ensures that the project remains as type-safe as possible.
+
+The [GitLab Workflow Extension](https://gitlab.com/gitlab-org/gitlab-vscode-extension/)
+project is a good model for a TypeScript project's boilerplate and configuration.
+Consider copying the `tsconfig.json` and `.eslintrc.json` from there.
+
+For `tsconfig.json`:
+
+- Use [`"strict": true`](https://www.typescriptlang.org/tsconfig#strict).
+ This enforces the strongest type-checking capabilities in the project and
+ prohibits overriding type-safety.
+- Use [`"skipLibCheck": true`](https://www.typescriptlang.org/tsconfig#skipLibCheck).
+ This improves compile time by only checking references `.d.ts`
+ files as opposed to all `.d.ts` files in `node_modules`.
+
+For `.eslintrc.json` (or `.eslintrc.js`):
+
+- Make sure that TypeScript-specific parsing and linting are placed in an `overrides`
+ for `**/*.ts` files. This way, linting regular `.js` files
+ remains unaffected by the TypeScript-specific rules.
+- Extend from [`plugin:@typescript-eslint/recommended`](https://typescript-eslint.io/rules?supported-rules=recommended)
+ which has some very sensible defaults, such as:
+ - [`"@typescript-eslint/no-explicit-any": "error"`](https://typescript-eslint.io/rules/no-explicit-any/)
+ - [`"@typescript-eslint/no-unsafe-assignment": "error"`](https://typescript-eslint.io/rules/no-unsafe-assignment/)
+ - [`"@typescript-eslint/no-unsafe-return": "error"`](https://typescript-eslint.io/rules/no-unsafe-return)
+
+### Avoid `any`
+
+Avoid `any` at all costs. This should already be configured in the project's linter,
+but it's worth calling out here.
+
+Developers commonly resort to `any` when dealing with data structures that cross
+domain boundaries, such as handling HTTP responses or interacting with untyped
+libraries. This appears convenient at first. However, opting for a well-defined type (or using
+`unknown` and employing type narrowing through predicates) carries substantial benefits.
+
+```typescript
+// Bad :(
+function handleMessage(data: any) {
+ console.log("We don't know what data is. This could blow up!", data.special.stuff);
+}
+
+// Good :)
+function handleMessage(data: unknown) {
+ console.log("Sometimes it's okay that it remains unknown.", JSON.stringify(data));
+}
+
+// Also good :)
+function isFooMessage(data: unknown): data is { foo: string } {
+ return typeof data === 'object' && data && 'foo' in data;
+}
+
+function handleMessage(data: unknown) {
+ if (isFooMessage(data)) {
+ console.log("We know it's a foo now. This is safe!", data.foo);
+ }
+}
+```
+
+### Avoid casting with `<>` or `as`
+
+Avoid casting with `<>` or `as` as much as possible.
+
+Type casting explicitly circumvents type-safety. Consider using
+[type predicates](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates).
+
+```typescript
+// Bad :(
+function handler(data: unknown) {
+ console.log((data as StuffContainer).stuff);
+}
+
+// Good :)
+function hasStuff(data: unknown): data is StuffContainer {
+ if (data && typeof data === 'object') {
+ return 'stuff' in data;
+ }
+
+ return false;
+}
+
+function handler(data: unknown) {
+ if (hasStuff(data)) {
+ // No casting needed :)
+ console.log(data.stuff);
+ }
+ throw new Error('Expected data to have stuff. Catastrophic consequences might follow...');
+}
+
+```
+
+There's some rare cases this might be acceptable (consider
+[this test utility](https://gitlab.com/gitlab-org/gitlab-web-ide/-/blob/3ea8191ed066811caa4fb108713e7538b8d8def1/packages/vscode-extension-web-ide/test-utils/createFakePartial.ts#L1)). However, 99% of the
+time, there's a better way.
+
+### Prefer `interface` over `type` for new structures
+
+Prefer declaring a new `interface` over declaring a new `type` alias when defining new structures.
+
+Interfaces and type aliases have a lot of cross-over, but only interfaces can be used
+with the `implements` keyword. A class is not able to `implement` a `type` (only an `interface`),
+so using `type` would restrict the usability of the structure.
+
+```typescript
+// Bad :(
+type Fooer = {
+ foo: () => string;
+}
+
+// Good :)
+interface Fooer {
+ foo: () => string;
+}
+```
+
+From the [TypeScript guide](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#differences-between-type-aliases-and-interfaces):
+
+> If you would like a heuristic, use `interface` until you need to use features from `type`.
+
+### Use `type` to define aliases for existing types
+
+Use type to define aliases for existing types, classes or interfaces. Use
+the TypeScript [Utility Types](https://www.typescriptlang.org/docs/handbook/utility-types.html)
+to provide transformations.
+
+```typescript
+interface Config = {
+ foo: string;
+
+ isBad: boolean;
+}
+
+// Bad :(
+type PartialConfig = {
+ foo?: string;
+
+ isBad?: boolean;
+}
+
+// Good :)
+type PartialConfig = Partial<Config>;
+```
+
+### Use union types to improve inference
+
+```typescript
+// Bad :(
+interface Foo { type: string }
+interface FooBar extends Foo { bar: string }
+interface FooZed extends Foo { zed: string }
+
+const doThing = (foo: Foo) => {
+ if (foo.type === 'bar') {
+ // Casting bad :(
+ console.log((foo as FooBar).bar);
+ }
+}
+
+// Good :)
+interface FooBar { type: 'bar', bar: string }
+interface FooZed { type: 'zed', zed: string }
+type Foo = FooBar | FooZed;
+
+const doThing = (foo: Foo) => {
+ if (foo.type === 'bar') {
+ // No casting needed :) - TS knows we are FooBar now
+ console.log(foo.bar);
+ }
+}
+```
+
+## Future plans
+
+- Shared ESLint configuration to reuse across TypeScript projects.
+
+## Related topics
+
+- [TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/intro.html)
+- [TypeScript notes in GitLab Workflow Extension](https://gitlab.com/gitlab-org/gitlab-vscode-extension/-/blob/main/docs/developer/coding-guidelines.md?ref_type=heads#typescript)
diff --git a/doc/development/fe_guide/type_hinting.md b/doc/development/fe_guide/type_hinting.md
new file mode 100644
index 00000000000..026bf855e27
--- /dev/null
+++ b/doc/development/fe_guide/type_hinting.md
@@ -0,0 +1,215 @@
+---
+stage: none
+group: unassigned
+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 hinting overview
+
+The Frontend codebase of the GitLab project currently does not require nor enforces types. Adding
+type annotations is optional, and we don't currently enforce any type safety in the JavaScript
+codebase. However, type annotations might be very helpful in adding clarity to the codebase,
+especially in shared utilities code. This document aims to cover how type hinting currently works,
+how to add new type annotations, and how to set up type hinting in the GitLab project.
+
+## JSDoc
+
+[JSDoc](https://jsdoc.app/) is a tool to document and describe types in JavaScript code, using
+specially formed comments. JSDoc's types vocabulary is relatively limited, but it is widely
+supported [by many IDEs](https://en.wikipedia.org/wiki/JSDoc#JSDoc_in_use).
+
+### Examples
+
+#### Describing functions
+
+Use [`@param`](https://jsdoc.app/tags-param.html) and [`@returns`](https://jsdoc.app/tags-returns.html)
+to describe function type:
+
+```javascript
+/**
+ * Adds two numbers
+ * @param {number} a first number
+ * @param {number} b second number
+ * @returns {number} sum of two numbers
+ */
+function add(a, b) {
+ return a + b;
+}
+```
+
+##### Optional parameters
+
+Use square brackets `[]` around a parameter name to mark it as optional. A default value can be
+provided by using the `[name=value]` syntax:
+
+```javascript
+/**
+ * Adds two numbers
+ * @param {number} value
+ * @param {number} [increment=1] optional param
+ * @returns {number} sum of two numbers
+ */
+function increment(a, b=1) {
+ return a + b;
+}
+```
+
+##### Object parameters
+
+Functions that accept objects can be typed by using `object.field` notation in `@param` names:
+
+```javascript
+/**
+ * Adds two numbers
+ * @param {object} config
+ * @param {string} config.path path
+ * @param {string} [config.anchor] anchor
+ * @returns {string}
+ */
+function createUrl(config) {
+ if (config.anchor) {
+ return path + '#' + anchor;
+ }
+ return path;
+}
+```
+
+#### Annotating types of variables that are not immediately assigned a value
+
+For tools and IDEs it's hard to infer type of a value that doesn't immediately receive a value. We
+can use [`@type`](https://jsdoc.app/tags-type.html) notation to assign type to such variables:
+
+```javascript
+/** @type {number} */
+let value;
+```
+
+Consult [JSDoc official website](https://jsdoc.app/) for more syntax details.
+
+### Tips for using JSDoc
+
+#### Use lower-case names for basic types
+
+While both uppercase `Boolean` and lowercase `boolean` are acceptable, in most cases when we need a
+primitive or an object — lower case versions are the right choice: `boolean`, `number`, `string`,
+`symbol`, `object`.
+
+```javascript
+/**
+ * Translates `text`.
+ * @param {string} text - The text to be translated
+ * @returns {string} The translated text
+ */
+const gettext = (text) => locale.gettext(ensureSingleLine(text));
+```
+
+#### Use well-known types
+
+Well-known types, like `HTMLDivElement` or `Intl` are available and can be used directly:
+
+```javascript
+/** @type {HTMLDivElement} */
+let element;
+```
+
+```javascript
+/**
+ * Creates an instance of Intl.DateTimeFormat for the current locale.
+ * @param {Intl.DateTimeFormatOptions} [formatOptions] - for available options, please see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat
+ * @returns {Intl.DateTimeFormat}
+ */
+const createDateTimeFormat = (formatOptions) =>
+ Intl.DateTimeFormat(getPreferredLocales(), formatOptions);
+```
+
+#### Import existing type definitions via `import('path/to/module')`
+
+Here are examples of how to annotate a type of the Vue Test Utils Wrapper variables, that are not
+immediately defined:
+
+```javascript
+/** @type {import('helpers/vue_test_utils_helper').ExtendedWrapper} */
+let wrapper;
+// ...
+wrapper = mountExtended(/* ... */);
+```
+
+```javascript
+/** @type {import('@vue/test-utils').Wrapper} */
+let wrapper;
+// ...
+wrapper = shallowMount(/* ... */);
+```
+
+NOTE:
+`import()` is [not a native JSDoc construct](https://github.com/jsdoc/jsdoc/issues/1645), but it is
+recognized by many IDEs and tools. In this case we're aiming for better clarity in the code and
+improved Developer Experience with an IDE.
+
+#### JSDoc is limited
+
+As was stated above, JSDoc has limited vocabulary. And using it would not describe the type fully.
+But sometimes it's possible to use 3rd party library's type definitions to make type inference to
+work for our code. Here's an example of such approach:
+
+```diff
+- export const mountExtended = (...args) => extendedWrapper(mount(...args));
++ import { compose } from 'lodash/fp';
++ export const mountExtended = compose(extendedWrapper, mount);
+```
+
+Here we use TypeScript type definitions from `compose` function, to add inferred type definitions to
+`mountExtended` function. In this case `mountExtended` arguments will be of same type as `mount`
+arguments. And return type will be the same as `extendedWrapper` return type.
+
+We can still use JSDoc's syntax to add description to the function, for example:
+
+```javascript
+/** Mounts a component and returns an extended wrapper for it */
+export const mountExtended = compose(extendedWrapper, mount);
+```
+
+## System requirements
+
+A setup might be required for type definitions from GitLab codebase and from 3rd party packages to
+be properly displayed in IDEs and tools.
+
+### Aliases
+
+Our codebase uses many aliases for imports. For example, `import Api from '~/api';` would import a
+`app/assets/javascripts/api.js` file. But IDEs might not know that alias and thus might not know the
+type of the `Api`. To fix that for most IDEs — we need to create a
+[`jsconfig.json`](https://code.visualstudio.com/docs/languages/jsconfig) file.
+
+There is a script in the GitLab project that can generate a `jsconfig.json` file based on webpack
+configuration and current environment variables. To generate or update the `jsconfig.json` file —
+run from the GitLab project root:
+
+```shell
+node scripts/frontend/create_jsconfig.js
+```
+
+`jsconfig.json` is added to gitignore list, so creating or changing it does not cause Git changes in
+the GitLab project. This also means it is not included in Git pulls, so it has to be manually
+generated or updated.
+
+### 3rd party TypeScript definitions
+
+While more and more libraries use TypeScript for type definitions, some still might have JSDoc
+annotated types or no types at all. To cover that gap, TypeScript community started a
+[DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped) initiative, that creates and
+supports standalone type definitions for popular JavaScript libraries. We can use those definitions
+by either explicitly installing the type packages (`yarn add -D "@types/lodash"`) or by using a
+feature called [Automatic Type Acquisition (ATA)](https://www.typescriptlang.org/tsconfig#typeAcquisition),
+that is available in some Language Services
+(for example, [ATA in VS Code](https://github.com/microsoft/TypeScript/wiki/JavaScript-Language-Service-in-Visual-Studio#user-content--automatic-acquisition-of-type-definitions)).
+
+Automatic Type Acquisition (ATA) automatically fetches type definitions from the DefinitelyTyped
+list. But for ATA to work, a globally installed `npm` might be required. IDEs can provide a fallback
+configuration options to set location of the `npm` executables. Consult your IDE documentation for
+details.
+
+Because ATA is not guaranteed to work and Lodash is a backbone for many of our utility functions
+— we have [DefinitelyTyped definitions for Lodash](https://www.npmjs.com/package/@types/lodash)
+explicitly added to our `devDependencies` in the `package.json`. This ensures that everyone gets
+type hints for `lodash`-based functions out of the box.
diff --git a/doc/development/feature_flags/controls.md b/doc/development/feature_flags/controls.md
index 6c46780a5d7..6e0f0e8dbcf 100644
--- a/doc/development/feature_flags/controls.md
+++ b/doc/development/feature_flags/controls.md
@@ -507,15 +507,8 @@ Once the above MR has been merged, you should:
When a feature gate has been removed from the codebase, the feature
record still exists in the database that the flag was deployed too.
-The record can be deleted once the MR is deployed to each environment:
+The record can be deleted once the MR is deployed to all the environments:
```shell
-/chatops run feature delete some_feature --dev
-/chatops run feature delete some_feature --staging
-```
-
-Then, you can delete it from production after the MR is deployed to prod:
-
-```shell
-/chatops run feature delete some_feature
+/chatops run feature delete <feature-flag-name> --dev --ops --pre --staging --staging-ref --production
```
diff --git a/doc/development/feature_flags/index.md b/doc/development/feature_flags/index.md
index 552a4ccc84b..c1a5963e97f 100644
--- a/doc/development/feature_flags/index.md
+++ b/doc/development/feature_flags/index.md
@@ -203,7 +203,7 @@ Only feature flags that have a YAML definition file can be used when running the
```shell
$ bin/feature-flag my_feature_flag
>> Specify the group introducing the feature flag, like `group::project management`:
-?> group::application performance
+?> group::cloud connector
>> URL of the MR introducing the feature flag (enter to skip):
?> https://gitlab.com/gitlab-org/gitlab/-/merge_requests/38602
@@ -218,7 +218,7 @@ create config/feature_flags/development/my_feature_flag.yml
name: my_feature_flag
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/38602
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/232533
-group: group::application performance
+group: group::cloud connector
type: development
default_enabled: false
```
@@ -625,7 +625,7 @@ A common pattern of testing both paths looks like:
```ruby
it 'ci_live_trace works' do
# tests assuming ci_live_trace is enabled in tests by default
- Feature.enabled?(:ci_live_trace) # => true
+ Feature.enabled?(:ci_live_trace) # => true
end
context 'when ci_live_trace is disabled' do
diff --git a/doc/development/gems.md b/doc/development/gems.md
index c9672483e8d..54d6e6dc30d 100644
--- a/doc/development/gems.md
+++ b/doc/development/gems.md
@@ -254,13 +254,12 @@ The project for a new Gem should always be created in [`gitlab-org/ruby/gems` na
1. Create a project in the [`gitlab-org/ruby/gems` group](https://gitlab.com/gitlab-org/ruby/gems/) (or in a subgroup of it):
1. Follow the [instructions for new projects](https://about.gitlab.com/handbook/engineering/gitlab-repositories/#creating-a-new-project).
1. Follow the instructions for setting up a [CI/CD configuration](https://about.gitlab.com/handbook/engineering/gitlab-repositories/#cicd-configuration).
- 1. Use the [shared CI/CD config](https://gitlab.com/gitlab-org/quality/pipeline-common/-/blob/master/ci/gem-release.yml)
+ 1. Use the [gem-release CI component](https://gitlab.com/gitlab-org/quality/pipeline-common/-/tree/master/gem-release)
to release and publish new gem versions by adding the following to their `.gitlab-ci.yml`:
```yaml
include:
- - project: 'gitlab-org/quality/pipeline-common'
- file: '/ci/gem-release.yml'
+ - component: gitlab.com/gitlab-org/quality/pipeline-common/gem-release@<REPLACE WITH LATEST TAG FROM https://gitlab.com/gitlab-org/quality/pipeline-common/-/releases>
```
This job will handle building and publishing the gem (it uses a `gitlab_rubygems` Rubygems.org
diff --git a/doc/development/gitaly.md b/doc/development/gitaly.md
index e6a853c107e..ed7fb6325d6 100644
--- a/doc/development/gitaly.md
+++ b/doc/development/gitaly.md
@@ -41,8 +41,8 @@ To read or write Git data, a request has to be made to Gitaly. This means that
if you're developing a new feature where you need data that's not yet available
in `lib/gitlab/git` changes have to be made to Gitaly.
-There should be no new code that touches Git repositories via disk access (for example,
-Rugged, `git`, `rm -rf`) anywhere in the `gitlab` repository. Anything that
+There should be no new code that touches Git repositories via disk access
+anywhere in the `gitlab` repository. Anything that
needs direct access to the Git repository *must* be implemented in Gitaly, and
exposed via an RPC.
@@ -64,45 +64,6 @@ rm -rf tmp/tests/gitaly
During RSpec tests, the Gitaly instance writes logs to `gitlab/log/gitaly-test.log`.
-## Legacy Rugged code
-
-While Gitaly can handle all Git access, many of GitLab customers still
-run Gitaly atop NFS. The legacy Rugged implementation for Git calls may
-be faster than the Gitaly RPC due to N+1 Gitaly calls and other
-reasons. See [the issue](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/57317) for more
-details.
-
-Until GitLab has eliminated most of these inefficiencies or the use of
-NFS is discontinued for Git data, Rugged implementations of some of the
-most commonly-used RPCs can be enabled via feature flags:
-
-- `rugged_find_commit`
-- `rugged_get_tree_entries`
-- `rugged_tree_entry`
-- `rugged_commit_is_ancestor`
-- `rugged_commit_tree_entry`
-- `rugged_list_commits_by_oid`
-
-A convenience Rake task can be used to enable or disable these flags
-all together. To enable:
-
-```shell
-bundle exec rake gitlab:features:enable_rugged
-```
-
-To disable:
-
-```shell
-bundle exec rake gitlab:features:disable_rugged
-```
-
-Most of this code exists in the `lib/gitlab/git/rugged_impl` directory.
-
-NOTE:
-You should *not* have to add or modify code related to Rugged unless explicitly discussed with the
-[Gitaly Team](https://gitlab.com/groups/gl-gitaly/group_members). This code does not work on GitLab.com or other GitLab
-instances that do not use NFS.
-
## `TooManyInvocationsError` errors
During development and testing, you may experience `Gitlab::GitalyClient::TooManyInvocationsError` failures.
diff --git a/doc/development/github_importer.md b/doc/development/github_importer.md
index 45554ae465d..9ce95cf7da1 100644
--- a/doc/development/github_importer.md
+++ b/doc/development/github_importer.md
@@ -34,21 +34,42 @@ The importer's codebase is broken up into the following directories:
## Architecture overview
-When a GitHub project is imported, we schedule and execute a job for the
-`RepositoryImportWorker` worker as all other importers. However, unlike other
-importers, we don't immediately perform the work necessary. Instead work is
-divided into separate stages, with each stage consisting out of a set of Sidekiq
-jobs that are executed. Between every stage a job is scheduled that periodically
-checks if all work of the current stage is completed, advancing the import
-process to the next stage when this is the case. The worker handling this is
-called `Gitlab::GithubImport::AdvanceStageWorker`.
+When a GitHub project is imported, work is divided into separate stages, with
+each stage consisting of a set of Sidekiq jobs that are executed. Between
+every stage a job is scheduled that periodically checks if all work of the
+current stage is completed, advancing the import process to the next stage when
+this is the case. The worker handling this is called
+`Gitlab::GithubImport::AdvanceStageWorker`.
+
+- An import is initiated via an API request to
+ [`POST /import/github`](https://gitlab.com/gitlab-org/gitlab/-/blob/18878b90991e2d478f3c79a68013b156d83b5db8/lib/api/import_github.rb#L42)
+- The API endpoint calls [`Import::GitHubService`](https://gitlab.com/gitlab-org/gitlab/-/blob/18878b90991e2d478f3c79a68013b156d83b5db8/lib/api/import_github.rb#L43).
+- Which calls
+ [`Gitlab::LegacyGithubImport::ProjectCreator`](https://gitlab.com/gitlab-org/gitlab/-/blob/18878b90991e2d478f3c79a68013b156d83b5db8/app/services/import/github_service.rb#L31-38)
+- Which calls
+ [`Projects::CreateService`](https://gitlab.com/gitlab-org/gitlab/-/blob/18878b90991e2d478f3c79a68013b156d83b5db8/lib/gitlab/legacy_github_import/project_creator.rb#L30)
+- Which calls
+ [`@project.import_state.schedule`](https://gitlab.com/gitlab-org/gitlab/-/blob/18878b90991e2d478f3c79a68013b156d83b5db8/app/services/projects/create_service.rb#L325)
+- Which calls
+ [`project.add_import_job`](https://gitlab.com/gitlab-org/gitlab/-/blob/1d154fa0b9121566aebf3afe3d28808d025cc5af/app/models/project_import_state.rb#L43)
+- Which calls
+ [`RepositoryImportWorker`](https://gitlab.com/gitlab-org/gitlab/-/blob/1d154fa0b9121566aebf3afe3d28808d025cc5af/app/models/project.rb#L1105)
## Stages
### 1. RepositoryImportWorker
-This worker starts the import process by scheduling a job for the
-next worker.
+This worker calls
+[`Projects::ImportService.new.execute`](https://gitlab.com/gitlab-org/gitlab/-/blob/651e6a0139396ed6fa9ce73e27587ca88f9f4d96/app/workers/repository_import_worker.rb#L23-24),
+which calls
+[`importer.execute`](https://gitlab.com/gitlab-org/gitlab/-/blob/fcccaaac8d62191ad233cebeffc67111145b1ad7/app/services/projects/import_service.rb#L143).
+
+In this context, `importer` is an instance of
+[`Gitlab::ImportSources.importer(project.import_type)`](https://gitlab.com/gitlab-org/gitlab/-/blob/fcccaaac8d62191ad233cebeffc67111145b1ad7/app/services/projects/import_service.rb#L149),
+which for `github` import types maps to
+[`ParallelImporter`](https://gitlab.com/gitlab-org/gitlab/-/blob/651e6a0139396ed6fa9ce73e27587ca88f9f4d96/lib/gitlab/import_sources.rb#L13).
+
+`ParallelImporter` schedules a job for the next worker.
### 2. Stage::ImportRepositoryWorker
@@ -222,9 +243,8 @@ them to GitLab users. Other data such as issue pages and comments typically only
We handle the rate limit by doing the following:
-1. After we hit the rate limit, we either:
- - Automatically reschedule jobs in such a way that they are not executed until the rate limit has been reset.
- - Move onto another GitHub access token if multiple GitHub access tokens were passed to the API.
+1. After we hit the rate limit, we automatically reschedule jobs in such a way that they are not executed until the rate
+ limit has been reset.
1. We cache the mapping of GitHub users to GitLab users in Redis.
More information on user caching can be found below.
diff --git a/doc/development/i18n/externalization.md b/doc/development/i18n/externalization.md
index 68c2778eabe..1ce35b254f1 100644
--- a/doc/development/i18n/externalization.md
+++ b/doc/development/i18n/externalization.md
@@ -232,7 +232,7 @@ If strings are reused throughout a component, it can be useful to define these s
If we are reusing the same translated string in multiple components, it is tempting to add them to a `constants.js` file instead and import them across our components. However, there are multiple pitfalls to this approach:
- It creates distance between the HTML template and the copy, adding an additional level of complexity while navigating our codebase.
-- Copy strings are rarely, if ever, truly the same entity. The benefit of having a reusable variable is to have one easy place to go to update a value, but for copy it is quite common to have similar strings that aren't quite the same.
+- The benefit of having a reusable variable is to have one easy place to go to update a value, but for copy it is quite common to have similar strings that aren't quite the same.
Another practice to avoid when exporting copy strings is to import them in specs. While it might seem like a much more efficient test (if we change the copy, the test will still pass!) it creates additional problems:
diff --git a/doc/development/i18n/proofreader.md b/doc/development/i18n/proofreader.md
index cea59bae41b..f24ebacab18 100644
--- a/doc/development/i18n/proofreader.md
+++ b/doc/development/i18n/proofreader.md
@@ -140,7 +140,6 @@ are very appreciative of the work done by translators and proofreaders!
- Rıfat Ünalmış (Rifat Unalmis) - [GitLab](https://gitlab.com/runalmis), [Crowdin](https://crowdin.com/profile/runalmis)
- İsmail Arılık - [GitLab](https://gitlab.com/ismailarilik), [Crowdin](https://crowdin.com/profile/ismailarilik)
- Ukrainian
- - Volodymyr Sobotovych - [GitLab](https://gitlab.com/wheleph), [Crowdin](https://crowdin.com/profile/wheleph)
- Andrew Vityuk - [GitLab](https://gitlab.com/3_1_3_u), [Crowdin](https://crowdin.com/profile/andruwa13)
- Welsh
- Delyth Prys - [GitLab](https://gitlab.com/Delyth), [Crowdin](https://crowdin.com/profile/DelythPrys)
diff --git a/doc/development/img/runner_fleet_dashboard.png b/doc/development/img/runner_fleet_dashboard.png
new file mode 100644
index 00000000000..242ebf4aea9
--- /dev/null
+++ b/doc/development/img/runner_fleet_dashboard.png
Binary files differ
diff --git a/doc/development/index.md b/doc/development/index.md
index 71ab54c8a73..abc19645ecb 100644
--- a/doc/development/index.md
+++ b/doc/development/index.md
@@ -10,7 +10,7 @@ description: "Development Guidelines: learn how to contribute to GitLab."
Learn how to contribute to the development of the GitLab product.
-This content is intended for GitLab team members as well as members of the wider community.
+This content is intended for both GitLab team members and members of the wider community.
- [Contribute to GitLab development](contributing/index.md)
- [Contribute to GitLab Runner development](https://docs.gitlab.com/runner/development/)
diff --git a/doc/development/internal_analytics/index.md b/doc/development/internal_analytics/index.md
index 64b9c7af037..b0e47233777 100644
--- a/doc/development/internal_analytics/index.md
+++ b/doc/development/internal_analytics/index.md
@@ -14,6 +14,13 @@ when developing new features or instrumenting existing ones.
## Fundamental concepts
+<div class="video-fallback">
+ See the video about <a href="https://www.youtube.com/watch?v=GtFNXbjygWo">the concepts of events and metrics.</a>
+</div>
+<figure class="video_container">
+ <iframe src="https://www.youtube-nocookie.com/embed/GtFNXbjygWo" frameborder="0" allowfullscreen="true"> </iframe>
+</figure>
+
Events and metrics are the foundation of the internal analytics system.
Understanding the difference between the two concepts is vital to using the system.
@@ -50,9 +57,53 @@ such as the value of a setting or the count of rows in a database table.
- To instrument an event-based metric, see the [internal event tracking quick start guide](internal_event_instrumentation/quick_start.md).
- To instrument a metric that observes the GitLab instances state, see [the metrics instrumentation](metrics/metrics_instrumentation.md).
-## Data flow
+## Data availability
For GitLab there is an essential difference in analytics setup between SaaS and self-managed or GitLab Dedicated instances.
+On our SaaS instance both individual events and pre-computed metrics are available for analysis.
+Additionally for SaaS page views are automatically instrumented.
+For self-managed only the metrics instrumenented on the version installed on the instance are available.
+
+## Data discovery
+
+The data visualization tools [Sisense](https://about.gitlab.com/handbook/business-technology/data-team/platform/sisensecdt/) and [Tableau](https://about.gitlab.com/handbook/business-technology/data-team/platform/tableau/),
+which have access to our Data Warehouse, can be used to query the internal analytics data.
+
+### Querying metrics
+
+The following example query returns all values reported for `count_distinct_user_id_from_feature_used_7d` within the last six months and the according `instance_id`:
+
+```sql
+SELECT
+ date_trunc('week', ping_created_at),
+ dim_instance_id,
+ metric_value
+FROM common.fct_ping_instance_metric_rolling_6_months --model limited to last 6 months for performance
+WHERE metrics_path = 'counts.users_visiting_dashboard_weekly' --set to metric of interest
+ORDER BY ping_created_at DESC
+```
+
+For a list of other metrics tables refer to the [Data Models Cheat Sheet](https://about.gitlab.com/handbook/product/product-analysis/data-model-cheat-sheet/#commonly-used-data-models).
+
+### Querying events
+
+The following example query returns the number of daily event occurences for the `feature_used` event.
+
+```sql
+SELECT
+ behavior_date,
+ COUNT(*) as event_occurences
+FROM common_mart.mart_behavior_structured_event
+WHERE event_action = 'feature_used'
+AND event_category = 'InternalEventTracking'
+AND behavior_date > '2023-08-01' --restricted minimum date for performance
+GROUP BY 1 ORDER BY 1 desc
+```
+
+For a list of other event tables refer to the [Data Models Cheat Sheet](https://about.gitlab.com/handbook/product/product-analysis/data-model-cheat-sheet/#commonly-used-data-models-2).
+
+## Data flow
+
On SaaS event records are directly sent to a collection system, called Snowplow, and imported into our data warehouse.
Self-managed and GitLab Dedicated instances record event counts locally. Every week, a process called Service Ping sends the current
values for all pre-defined and active metrics to our data warehouse. For GitLab.com, metrics are calculated directly in the data warehouse.
diff --git a/doc/development/internal_analytics/internal_event_instrumentation/local_setup_and_debugging.md b/doc/development/internal_analytics/internal_event_instrumentation/local_setup_and_debugging.md
index d68e5565775..d9f45a2d93e 100644
--- a/doc/development/internal_analytics/internal_event_instrumentation/local_setup_and_debugging.md
+++ b/doc/development/internal_analytics/internal_event_instrumentation/local_setup_and_debugging.md
@@ -14,7 +14,7 @@ Internal events are using a tool called Snowplow under the hood. To develop and
| Snowplow Micro | Yes | Yes | Yes | No | No |
For local development you will have to either [setup a local event collector](#setup-local-event-collector) or [configure a remote event collector](#configure-a-remote-event-collector).
-We recommend the local setup when actively developing new events.
+We recommend using the local setup together with the [internal events monitor](#internal-events-monitor) when actively developing new events.
## Setup local event collector
@@ -68,6 +68,57 @@ You can configure your self-managed GitLab instance to use a custom Snowplow col
1. Select **Save changes**.
+## Internal Events Monitor
+
+<div class="video-fallback">
+ Watch the demo video about the <a href="https://www.youtube.com/watch?v=R7vT-VEzZOI">Internal Events Tracking Monitor</a>
+</div>
+<figure class="video_container">
+ <iframe src="https://www.youtube-nocookie.com/embed/R7vT-VEzZOI" frameborder="0" allowfullscreen="true"> </iframe>
+</figure>
+
+To understand how events are triggered and metrics are updated while you use the Rails app locally or `rails console`,
+you can use the monitor.
+
+Start the monitor and list one or more events that you would like to monitor. In this example we would like to monitor `i_code_review_user_create_mr`.
+
+```shell
+rails runner scripts/internal_events/monitor.rb i_code_review_user_create_mr
+```
+
+The monitor shows two tables. The top table lists all the metrics that are defined on the `i_code_review_user_create_mr` event.
+The second right-most column shows the value of each metric when the monitor was started and the right most column shows the current value of each metric.
+The bottom table has a list selected properties of all Snowplow events that matches the event name.
+
+If a new `i_code_review_user_create_mr` event is fired, the metrics values will get updated and a new event will appear in the `SNOWPLOW EVENTS` table.
+
+The monitor looks like below.
+
+```plaintext
+Updated at 2023-10-11 10:17:59 UTC
+Monitored events: i_code_review_user_create_mr
+
++--------------------------------------------------------------------------------------------------------------------------------------------------------------------+
+| RELEVANT METRICS |
++-----------------------------------------------------------------------------+------------------------------+-----------------------+---------------+---------------+
+| Key Path | Monitored Events | Instrumentation Class | Initial Value | Current Value |
++-----------------------------------------------------------------------------+------------------------------+-----------------------+---------------+---------------+
+| counts_monthly.aggregated_metrics.code_review_category_monthly_active_users | i_code_review_user_create_mr | AggregatedMetric | 13 | 14 |
+| counts_monthly.aggregated_metrics.code_review_group_monthly_active_users | i_code_review_user_create_mr | AggregatedMetric | 13 | 14 |
+| counts_weekly.aggregated_metrics.code_review_category_monthly_active_users | i_code_review_user_create_mr | AggregatedMetric | 0 | 1 |
+| counts_weekly.aggregated_metrics.code_review_group_monthly_active_users | i_code_review_user_create_mr | AggregatedMetric | 0 | 1 |
+| redis_hll_counters.code_review.i_code_review_user_create_mr_monthly | i_code_review_user_create_mr | RedisHLLMetric | 8 | 9 |
+| redis_hll_counters.code_review.i_code_review_user_create_mr_weekly | i_code_review_user_create_mr | RedisHLLMetric | 0 | 1 |
++-----------------------------------------------------------------------------+------------------------------+-----------------------+---------------+---------------+
++---------------------------------------------------------------------------------------------------------+
+| SNOWPLOW EVENTS |
++------------------------------+--------------------------+---------+--------------+------------+---------+
+| Event Name | Collector Timestamp | user_id | namespace_id | project_id | plan |
++------------------------------+--------------------------+---------+--------------+------------+---------+
+| i_code_review_user_create_mr | 2023-10-11T10:17:15.504Z | 29 | 93 | | default |
++------------------------------+--------------------------+---------+--------------+------------+---------+
+```
+
## Snowplow Analytics Debugger Chrome Extension
[Snowplow Analytics Debugger](https://chrome.google.com/webstore/detail/snowplow-analytics-debugg/jbnlcgeengmijcghameodeaenefieedm) is a browser extension for testing frontend events.
diff --git a/doc/development/internal_analytics/internal_event_instrumentation/quick_start.md b/doc/development/internal_analytics/internal_event_instrumentation/quick_start.md
index 271cb5f98a6..15ad4266d1b 100644
--- a/doc/development/internal_analytics/internal_event_instrumentation/quick_start.md
+++ b/doc/development/internal_analytics/internal_event_instrumentation/quick_start.md
@@ -148,3 +148,27 @@ Sometimes we want to send internal events when the component is rendered or load
= render Pajamas::ButtonComponent.new(button_options: { data: { event_tracking_load: 'true', event_tracking: 'i_devops' } }) do
= _("New project")
```
+
+### Props
+
+Apart from `eventName`, the `trackEvent` method also supports `extra` and `context` props.
+
+`extra`: Use this property to append supplementary information to GitLab standard context.
+`context`: Use this property to attach an additional context, if needed.
+
+The following example shows how to use the `extra` and `context` props with the `trackEvent` method:
+
+```javascript
+this.trackEvent('i_code_review_user_apply_suggestion', {
+ extra: {
+ projectId : 123,
+ },
+ context: {
+ schema: 'iglu:com.gitlab/design_management_context/jsonschema/1-0-0',
+ data: {
+ 'design-version-number': '1.0.0',
+ 'design-is-current-version': '1.0.1',
+ },
+ },
+});
+```
diff --git a/doc/development/internal_analytics/metrics/metrics_dictionary.md b/doc/development/internal_analytics/metrics/metrics_dictionary.md
index afdbd17c63b..6a3291eaba5 100644
--- a/doc/development/internal_analytics/metrics/metrics_dictionary.md
+++ b/doc/development/internal_analytics/metrics/metrics_dictionary.md
@@ -104,7 +104,7 @@ A metric's time frame is calculated based on the `time_frame` field and the `dat
We use the following categories to classify a metric:
- `operational`: Required data for operational purposes.
-- `optional`: Default value for a metric. Data that is optional to collect. This can be [enabled or disabled](../../../administration/settings/usage_statistics.md#enable-or-disable-usage-statistics) in the Admin Area.
+- `optional`: Default value for a metric. Data that is optional to collect. This can be [enabled or disabled](../../../administration/settings/usage_statistics.md#enable-or-disable-service-ping) in the Admin Area.
- `subscription`: Data related to licensing.
- `standard`: Standard set of identifiers that are included when collecting data.
diff --git a/doc/development/internal_analytics/service_ping/index.md b/doc/development/internal_analytics/service_ping/index.md
index bae4e35149d..f010884272b 100644
--- a/doc/development/internal_analytics/service_ping/index.md
+++ b/doc/development/internal_analytics/service_ping/index.md
@@ -22,7 +22,7 @@ and sales teams understand how GitLab is used. The data helps to:
Service Ping information is not anonymous. It's linked to the instance's hostname, but does
not contain project names, usernames, or any other specific data.
-Service Ping is enabled by default. However, you can [disable](../../../administration/settings/usage_statistics.md#enable-or-disable-usage-statistics) it on any self-managed instance. When Service Ping is enabled, GitLab gathers data from the other instances and can show your instance's usage statistics to your users.
+Service Ping is enabled by default. However, you can [disable](../../../administration/settings/usage_statistics.md#enable-or-disable-service-ping) certain metrics on any self-managed instance. When Service Ping is enabled, GitLab gathers data from the other instances and can show your instance's usage statistics to your users.
## Service Ping terminology
@@ -38,13 +38,8 @@ We use the following terminology to describe the Service Ping components:
### Limitations
-- Service Ping does not track frontend events things like page views, link clicks, or user sessions.
-- Service Ping focuses only on aggregated backend events.
-
-Because of these limitations we recommend you:
-
-- Instrument your products with Snowplow for more detailed analytics on GitLab.com.
-- Use Service Ping to track aggregated backend events on self-managed instances.
+- Service Ping delivers only [metrics](../index.md#metric), not individual events.
+- A metric has to be present and instrumented in the codebase for a GitLab version to be delivered in Service Pings for that version.
## Service Ping request flow
@@ -358,14 +353,6 @@ The following is example content of the Service Ping payload.
}
```
-## Notable changes
-
-In GitLab 14.6, [`flavor`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/75587) was added to try to detect the underlying managed database variant.
-Possible values are "Amazon Aurora PostgreSQL", "PostgreSQL on Amazon RDS", "Cloud SQL for PostgreSQL",
-"Azure Database for PostgreSQL - Flexible Server", or "null".
-
-In GitLab 13.5, `pg_system_id` was added to send the [PostgreSQL system identifier](https://www.2ndquadrant.com/en/blog/support-for-postgresqls-system-identifier-in-barman/).
-
## Export Service Ping data
Rake tasks exist to export Service Ping data in different formats.
@@ -390,105 +377,7 @@ bin/rake gitlab:usage_data:dump_non_sql_in_json
bin/rake gitlab:usage_data:dump_sql_in_yaml > ~/Desktop/usage-metrics-2020-09-02.yaml
```
-## Generate Service Ping
-
-To generate Service Ping, use [Teleport](https://goteleport.com/docs/) or a detached screen session on a remote server.
-
-### Triggering
-
-#### Trigger Service Ping with Teleport
-
-1. Request temporary [access](https://gitlab.com/gitlab-com/runbooks/-/blob/master/docs/teleport/Connect_to_Rails_Console_via_Teleport.md#how-to-use-teleport-to-connect-to-rails-console) to the required environment.
-1. After your approval is issued, [access the Rails console](https://gitlab.com/gitlab-com/runbooks/-/blob/master/docs/teleport/Connect_to_Rails_Console_via_Teleport.md#access-approval).
-1. Run `GitlabServicePingWorker.new.perform('triggered_from_cron' => false)`.
-
-#### Trigger Service Ping with a detached screen session
-
-1. Connect to bastion with agent forwarding:
-
- ```shell
- ssh -A lb-bastion.gprd.gitlab.com
- ```
-
-1. Create named screen:
-
- ```shell
- screen -S <username>_usage_ping_<date>
- ```
-
-1. Connect to console host:
-
- ```shell
- ssh $USER-rails@console-01-sv-gprd.c.gitlab-production.internal
- ```
-
-1. Run:
-
- ```shell
- GitlabServicePingWorker.new.perform('triggered_from_cron' => false)
- ```
-
-1. To detach from screen, press `ctrl + A`, `ctrl + D`.
-1. Exit from bastion:
-
- ```shell
- exit
- ```
-
-1. Get the metrics duration from logs:
-
-Search in Google Console logs for `time_elapsed`. [Query example](https://cloudlogging.app.goo.gl/nWheZvD8D3nWazNe6).
-
-### Verification (After approx 30 hours)
-
-#### Verify with Teleport
-
-1. Follow [the steps](https://gitlab.com/gitlab-com/runbooks/-/blob/master/docs/teleport/Connect_to_Rails_Console_via_Teleport.md#how-to-use-teleport-to-connect-to-rails-console) to request a new access to the required environment and connect to the Rails console
-1. Check the last payload in `raw_usage_data` table: `RawUsageData.last.payload`
-1. Check the when the payload was sent: `RawUsageData.last.sent_at`
-
-#### Verify using detached screen session
-
-1. Reconnect to bastion:
-
- ```shell
- ssh -A lb-bastion.gprd.gitlab.com
- ```
-
-1. Find your screen session:
-
- ```shell
- screen -ls
- ```
-
-1. Attach to your screen session:
-
- ```shell
- screen -x 14226.mwawrzyniak_usage_ping_2021_01_22
- ```
-
-1. Check the last payload in `raw_usage_data` table:
-
- ```shell
- RawUsageData.last.payload
- ```
-
-1. Check the when the payload was sent:
-
- ```shell
- RawUsageData.last.sent_at
- ```
-
-### Skip database write operations
-
-To skip database write operations, DevOps report creation, and storage of usage data payload, pass an optional argument:
-
-```shell
-skip_db_write:
-GitlabServicePingWorker.new.perform('triggered_from_cron' => false, 'skip_db_write' => true)
-```
-
-### Fallback values for Service Ping
+## Fallback values for Service Ping
We return fallback values in these cases:
diff --git a/doc/development/internal_api/index.md b/doc/development/internal_api/index.md
index f9b494b80c2..9b5bafaad8f 100644
--- a/doc/development/internal_api/index.md
+++ b/doc/development/internal_api/index.md
@@ -1215,7 +1215,7 @@ Example response:
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/9388) in GitLab 11.10.
-The group SCIM API implements the [RFC7644 protocol](https://www.rfc-editor.org/rfc/rfc7644). As this API is for
+The group SCIM API partially implements the [RFC7644 protocol](https://www.rfc-editor.org/rfc/rfc7644). This API provides the `/groups/:group_path/Users` and `/groups/:group_path/Users/:id` endpoints. The base URL is `<http|https>://<GitLab host>/api/scim/v2`. Because this API is for
**system** use for SCIM provider integration, it is subject to change without notice.
To use this API, enable [Group SSO](../../user/group/saml_sso/index.md) for the group.
@@ -1452,7 +1452,7 @@ Returns an empty response with a `204` status code if successful.
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/378599) in GitLab 15.8.
-The Instance SCIM API implements the [RFC7644 protocol](https://www.rfc-editor.org/rfc/rfc7644). As this API is for
+The instance SCIM API partially implements the [RFC7644 protocol](https://www.rfc-editor.org/rfc/rfc7644). This API provides the `/application/Users` and `/application/Users/:id` endpoints. The base URL is `<http|https>://<GitLab host>/api/scim/v2`. Because this API is for
**system** use for SCIM provider integration, it is subject to change without notice.
To use this API, enable [SAML SSO](../../integration/saml.md) for the instance.
diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md
index 29181dd1b9d..afb36519b8d 100644
--- a/doc/development/migration_style_guide.md
+++ b/doc/development/migration_style_guide.md
@@ -1563,3 +1563,23 @@ Any table which has some high read operation compared to current [high-traffic t
As a general rule, we discourage adding columns to high-traffic tables that are purely for
analytics or reporting of GitLab.com. This can have negative performance impacts for all
self-managed instances without providing direct feature value to them.
+
+## Milestone
+
+Beginning in GitLab 16.6, all new migrations must specify a milestone, using the following syntax:
+
+```ruby
+class AddFooToBar < Gitlab::Database::Migration[2.2]
+ milestone '16.6'
+
+ def change
+ # Your migration here
+ end
+end
+```
+
+Adding the correct milestone to a migration enables us to logically partition migrations into
+their corresponding GitLab minor versions. This:
+
+- Simplifies the upgrade process.
+- Alleviates potential migration ordering issues that arise when we rely solely on the migration's timestamp for ordering.
diff --git a/doc/development/permissions/custom_roles.md b/doc/development/permissions/custom_roles.md
index a060d7a740b..1630ea7b9ab 100644
--- a/doc/development/permissions/custom_roles.md
+++ b/doc/development/permissions/custom_roles.md
@@ -200,6 +200,10 @@ Examples of merge requests adding new abilities to custom roles:
You should make sure a new custom roles ability is under a feature flag.
+### Privilege escalation consideration
+
+A base role typically has permissions that allow creation or management of artifacts corresponding to the base role when interacting with that artifact. For example, when a `Developer` creates an access token for a project, it is created with `Developer` access encoded into that credential. It is important to keep in mind that as new custom permissions are created, there might be a risk of elevated privileges when interacting with GitLab artifacts, and appropriate safeguards or base role checks should be added.
+
### Consuming seats
If a new user with a role `Guest` is added to a member role that includes enablement of an ability that is **not** in the `CUSTOMIZABLE_PERMISSIONS_EXEMPT_FROM_CONSUMING_SEAT` array, a seat is consumed. We simply want to make sure we are charging Ultimate customers for guest users, who have "elevated" abilities. This only applies to billable users on SaaS (billable users that are counted towards namespace subscription). More details about this topic can be found in [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/390269).
diff --git a/doc/development/pipelines/index.md b/doc/development/pipelines/index.md
index 2266bdbe459..77f91300a57 100644
--- a/doc/development/pipelines/index.md
+++ b/doc/development/pipelines/index.md
@@ -610,15 +610,26 @@ Exceptions to this general guideline should be motivated and documented.
### Ruby versions testing
-We're running Ruby 3.0 on GitLab.com, as well as for merge requests and the default branch.
-To prepare for the next release, Ruby 3.1, we also run our test suite against Ruby 3.1 on
-a dedicated 2-hourly scheduled pipelines.
+We're running Ruby 3.0 on GitLab.com, as well as for the default branch.
+To prepare for the next Ruby version, we run merge requests in Ruby 3.1.
-For merge requests, you can add the `pipeline:run-in-ruby3_1` label to switch
-the Ruby version used for running the whole test suite to 3.1. When you do
-this, the test suite will no longer run in Ruby 3.0 (default), and an
-additional job `verify-ruby-3.0` will also run and always fail to remind us to
-remove the label and run in Ruby 3.0 before merging the merge request.
+This takes effects at the time when
+[Run merge requests in Ruby 3.1 by default](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134290)
+is merged. See
+[Ruby 3.1 epic](https://gitlab.com/groups/gitlab-org/-/epics/10034)
+for the roadmap to fully make Ruby 3.1 the default.
+
+To make sure both Ruby versions are working, we also run our test suite
+against both Ruby 3.0 and Ruby 3.1 on dedicated 2-hourly scheduled pipelines.
+
+For merge requests, you can add the `pipeline:run-in-ruby3_0` label to switch
+the Ruby version to 3.0. When you do this, the test suite will no longer run
+in Ruby 3.1 (default for merge requests).
+
+When the pipeline is running in a Ruby version not considered default, an
+additional job `verify-default-ruby` will also run and always fail to remind
+us to remove the label and run in default Ruby before merging the merge
+request. At the moment both Ruby 3.0 and Ruby 3.1 are considered default.
This should let us:
@@ -632,17 +643,17 @@ Our test suite runs against PostgreSQL 14 as GitLab.com runs on PostgreSQL 14 an
We do run our test suite against PostgreSQL 14 on nightly scheduled pipelines.
-We also run our test suite against PostgreSQL 12 and PostgreSQL 13 upon specific database library changes in merge requests and `main` pipelines (with the `rspec db-library-code pg12` and `rspec db-library-code pg13` jobs).
+We also run our test suite against PostgreSQL 13 upon specific database library changes in merge requests and `main` pipelines (with the `rspec db-library-code pg13` job).
#### Current versions testing
| Where? | PostgreSQL version | Ruby version |
|--------------------------------------------------------------------------------------------------|-------------------------------------------------|-----------------------|
-| Merge requests | 14 (default version), 13 for DB library changes | 3.0 (default version) |
+| Merge requests | 14 (default version), 13 for DB library changes | 3.1 |
| `master` branch commits | 14 (default version), 13 for DB library changes | 3.0 (default version) |
| `maintenance` scheduled pipelines for the `master` branch (every even-numbered hour) | 14 (default version), 13 for DB library changes | 3.0 (default version) |
| `maintenance` scheduled pipelines for the `ruby3_1` branch (every odd-numbered hour), see below. | 14 (default version), 13 for DB library changes | 3.1 |
-| `nightly` scheduled pipelines for the `master` branch | 14 (default version), 12, 13, 15 | 3.0 (default version) |
+| `nightly` scheduled pipelines for the `master` branch | 14 (default version), 13, 15 | 3.0 (default version) |
There are 2 pipeline schedules used for testing Ruby 3.1. One is triggering a
pipeline in `ruby3_1-sync` branch, which updates the `ruby3_1` branch with latest
diff --git a/doc/development/repository_storage_moves/index.md b/doc/development/repository_storage_moves/index.md
new file mode 100644
index 00000000000..578bc1eabee
--- /dev/null
+++ b/doc/development/repository_storage_moves/index.md
@@ -0,0 +1,102 @@
+---
+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
+---
+
+# Project Repository Storage Moves
+
+This document was created to help contributors understand the code design of
+[project repository storage moves](../../api/project_repository_storage_moves.md).
+Read this document before making changes to the code for this feature.
+
+This document is intentionally limited to an overview of how the code is
+designed, as code can change often. To understand how a specific part of the
+feature works, view the code and the specs. The details here explain how the
+major components of the Code Owners feature work.
+
+NOTE:
+This document should be updated when parts of the codebase referenced in this
+document are updated, removed, or new parts are added.
+
+## Business logic
+
+- `Projects::RepositoryStorageMove`: Tracks the move, includes state machine.
+ - Defined in `app/models/projects/repository_storage_move.rb`.
+- `RepositoryStorageMovable`: Contains the state machine logic, validators, and some helper methods.
+ - Defined in `app/models/concerns/repository_storage_movable.rb`.
+- `Project`: The project model.
+ - Defined in `app/models/project.rb`.
+- `CanMoveRepositoryStorage`: Contains helper methods that are into `Project`.
+ - Defined in `app/models/concerns/can_move_repository_storage.rb`.
+- `API::ProjectRepositoryStorageMoves`: API class for project repository storage moves.
+ - Defined in `lib/api/project_repository_storage_moves.rb`.
+- `Entities::Projects::RepositoryStorageMove`: API entity for serializing the `Projects::RepositoryStorageMove` model.
+ - Defined in `lib/api/entities/projects/repository_storage_moves.rb`.
+- `Projects::ScheduleBulkRepositoryShardMovesService`: Service to schedule bulk moves.
+ - Defined in `app/services/projects/schedule_bulk_repository_shard_moves_service.rb`.
+- `ScheduleBulkRepositoryShardMovesMethods`: Generic methods for bulk moves.
+ - Defined in `app/services/concerns/schedule_bulk_repository_shard_moves_methods.rb`.
+- `Projects::ScheduleBulkRepositoryShardMovesWorker`: Worker to handle bulk moves.
+ - Defined in `app/workers/projects/schedule_bulk_repository_shard_moves_worker.rb`.
+- `Projects::UpdateRepositoryStorageWorker`: Finds repository storage move and then calls the update storage service.
+ - Defined in `app/workers/projects/update_repository_storage_worker.rb`.
+- `UpdateRepositoryStorageWorker`: Module containing generic logic for `Projects::UpdateRepositoryStorageWorker`.
+ - Defined in `app/workers/concerns/update_repository_storage_worker.rb`.
+- `Projects::UpdateRepositoryStorageService`: Performs the move.
+ - Defined in `app/services/projects/update_repository_storage_service.rb`.
+- `UpdateRepositoryStorageMethods`: Module with generic methods included in `Projects::UpdateRepositoryStorageService`.
+ - Defined in `app/services/concerns/update_repository_storage_methods.rb`.
+- `Projects::UpdateService`: Schedules move if the passed parameters request a move.
+ - Defined in `app/services/projects/update_service.rb`.
+- `PoolRepository`: Ruby object representing Gitaly `ObjectPool`.
+ - Defined in `app/models/pool_repository.rb`.
+- `ObjectPool::CreateWorker`: Worker to create an `ObjectPool` via `Gitaly`.
+ - Defined in `app/workers/object_pool/create_worker.rb`.
+- `ObjectPool::JoinWorker`: Worker to join an `ObjectPool` via `Gitaly`.
+ - Defined in `app/workers/object_pool/join_worker.rb`.
+- `ObjectPool::ScheduleJoinWorker`: Worker to schedule an `ObjectPool::JoinWorker`.
+ - Defined in `app/workers/object_pool/schedule_join_worker.rb`.
+- `ObjectPool::DestroyWorker`: Worker to destroy an `ObjectPool` via `Gitaly`.
+ - Defined in `app/workers/object_pool/destroy_worker.rb`.
+- `ObjectPoolQueue`: Module to configure `ObjectPool` workers.
+ - Defined in `app/workers/concerns/object_pool_queue.rb`.
+- `Repositories::ReplicateService`: Handles replication of data from one repository to another.
+ - Defined in `app/services/repositories/replicate_service.rb`.
+
+## Flow
+
+These flowcharts should help explain the flow from the endpoints down to the
+models for different features.
+
+### Schedule a repository storage move via the API
+
+```mermaid
+graph TD
+ A[<code>POST /api/:version/project_repository_storage_moves</code>] --> C
+ B[<code>POST /api/:version/projects/:id/repository_storage_moves</code>] --> D
+ C[Schedule move for each project in shard] --> D[Set state to scheduled]
+ D --> E[<code>after_transition callback</code>]
+ E --> F{<code>set_repository_read_only!</code>}
+ F -->|success| H[Schedule repository update worker]
+ F -->|error| G[Set state to failed]
+```
+
+### Moving the storage after being scheduled
+
+```mermaid
+graph TD
+ A[Repository update worker scheduled] --> B{State is scheduled?}
+ B -->|Yes| C[Set state to started]
+ B -->|No| D[Return success]
+ C --> E{Same filesystem?}
+ E -.-> G[Set project repo to writable]
+ E -->|Yes| F["Mirror repositories (project, wiki, design, & pool)"]
+ G --> H[Update repo storage value]
+ H --> I[Set state to finished]
+ I --> J[Associate project with new pool repository]
+ J --> K[Unlink old pool repository]
+ K --> L[Update project repository storage values]
+ L --> N[Remove old paths if same filesystem]
+ N --> M[Set state to finished]
+```
diff --git a/doc/development/rubocop_development_guide.md b/doc/development/rubocop_development_guide.md
index 6568d025ca5..807544b71d4 100644
--- a/doc/development/rubocop_development_guide.md
+++ b/doc/development/rubocop_development_guide.md
@@ -28,15 +28,51 @@ discussions, nitpicking, or back-and-forth in reviews. The
[GitLab Ruby style guide](backend/ruby_style_guide.md) includes a non-exhaustive
list of styles that commonly come up in reviews and are not enforced.
-By default, we should not
-[disable a RuboCop rule inline](https://docs.rubocop.org/rubocop/configuration.html#disabling-cops-within-source-code), because it negates agreed-upon code standards that the rule is attempting to apply to the codebase.
-
-If you must use inline disable, provide the reason on the MR and ensure the reviewers agree
-before merging.
-
Additionally, we have dedicated
[test-specific style guides and best practices](testing_guide/index.md).
+## Disabling rules inline
+
+By default, RuboCop rules should not be
+[disabled inline](https://docs.rubocop.org/rubocop/configuration.html#disabling-cops-within-source-code),
+because it negates agreed-upon code standards that the rule is attempting to
+apply to the codebase.
+
+If you must use inline disable provide the reason as a code comment in
+the same line where the rule is disabled.
+
+More context can go into code comments above this inline disable comment. To
+reduce verbose code comments link a resource (issue, epic, ...) to provide
+detailed context.
+
+For example:
+
+```ruby
+# bad
+module Types
+ module Domain
+ # rubocop:disable Graphql/AuthorizeTypes
+ class SomeType < BaseObject
+ object.public_send(action) # rubocop:disable GitlabSecurity/PublicSend
+ end
+ # rubocop:enable Graphql/AuthorizeTypes
+ end
+end
+
+# good
+module Types
+ module Domain
+ # rubocop:disable Graphql/AuthorizeTypes -- already authroized in parent entity
+ class SomeType < BaseObject
+ # At this point `action` is safe to be used in `public_send`.
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/123457890.
+ object.public_send(action) # rubocop:disable GitlabSecurity/PublicSend -- User input verified
+ end
+ # rubocop:enable Graphql/AuthorizeTypes
+ end
+end
+```
+
## Creating new RuboCop cops
Typically it is better for the linting rules to be enforced programmatically as it
diff --git a/doc/development/ruby_upgrade.md b/doc/development/ruby_upgrade.md
index 52f0f72e72a..61bc629e8c8 100644
--- a/doc/development/ruby_upgrade.md
+++ b/doc/development/ruby_upgrade.md
@@ -84,6 +84,8 @@ order reversed as described above.
Tracking this work in an epic is useful to get a sense of progress. For larger upgrades, include a
timeline in the epic description so stakeholders know when the final switch is expected to go live.
+Include the designated [performance testing template](https://gitlab.com/gitlab-org/quality/performance-testing/ruby-rollout-performance-testing)
+to help ensure performance standards on the upgrade.
Break changes to individual repositories into separate issues under this epic.
@@ -141,14 +143,13 @@ A [build matrix definition](../ci/yaml/index.md#parallelmatrix) can do this effi
#### Decide which repositories to update
-When upgrading Ruby, consider updating the following repositories:
+When upgrading Ruby, consider updating the repositories in the [`ruby/gems` group](https://gitlab.com/gitlab-org/ruby/gems/) as well.
+For reference, here is a list of merge requests that have updated Ruby for some of these projects in the past:
-- [Gitaly](https://gitlab.com/gitlab-org/gitaly) ([example](https://gitlab.com/gitlab-org/gitaly/-/merge_requests/3771))
- [GitLab LabKit](https://gitlab.com/gitlab-org/labkit-ruby) ([example](https://gitlab.com/gitlab-org/labkit-ruby/-/merge_requests/79))
- [GitLab Exporter](https://gitlab.com/gitlab-org/ruby/gems/gitlab-exporter) ([example](https://gitlab.com/gitlab-org/ruby/gems/gitlab-exporter/-/merge_requests/150))
- [GitLab Experiment](https://gitlab.com/gitlab-org/ruby/gems/gitlab-experiment) ([example](https://gitlab.com/gitlab-org/ruby/gems/gitlab-experiment/-/merge_requests/128))
- [Gollum Lib](https://gitlab.com/gitlab-org/gollum-lib) ([example](https://gitlab.com/gitlab-org/gollum-lib/-/merge_requests/21))
-- [GitLab Helm Chart](https://gitlab.com/gitlab-org/charts/gitlab) ([example](https://gitlab.com/gitlab-org/charts/gitlab/-/merge_requests/2162))
- [GitLab Sidekiq fetcher](https://gitlab.com/gitlab-org/sidekiq-reliable-fetch) ([example](https://gitlab.com/gitlab-org/sidekiq-reliable-fetch/-/merge_requests/33))
- [Prometheus Ruby Mmap Client](https://gitlab.com/gitlab-org/prometheus-client-mmap) ([example](https://gitlab.com/gitlab-org/prometheus-client-mmap/-/merge_requests/59))
- [GitLab-mail_room](https://gitlab.com/gitlab-org/gitlab-mail_room) ([example](https://gitlab.com/gitlab-org/gitlab-mail_room/-/merge_requests/16))
@@ -213,8 +214,6 @@ the new Ruby to be the new default.
The last step is to use the new Ruby in production. This
requires updating Omnibus and production Docker images to use the new version.
-Helm charts may also have to be updated if there were changes to related systems that maintain
-their own charts (such as `gitlab-exporter`.)
To use the new Ruby in production, update the following projects:
@@ -222,6 +221,11 @@ To use the new Ruby in production, update the following projects:
- [Omnibus GitLab](https://gitlab.com/gitlab-org/omnibus-gitlab) ([example](https://gitlab.com/gitlab-org/omnibus-gitlab/-/merge_requests/5545))
- [Self-compiled installations](../install/installation.md): update the [Ruby system version check](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/system_check/app/ruby_version_check.rb)
+Charts like the [GitLab Helm Chart](https://gitlab.com/gitlab-org/charts/gitlab) should also be updated if
+they use Ruby in some capacity, for example
+to run tests (see [this example](https://gitlab.com/gitlab-org/charts/gitlab/-/merge_requests/2162)), though
+this may not strictly be necessary.
+
If you submit a change management request, coordinate the rollout with infrastructure
engineers. When dealing with larger upgrades, involve [Release Managers](https://about.gitlab.com/community/release-managers/)
in the rollout plan.
diff --git a/doc/development/runner_fleet_dashboard.md b/doc/development/runner_fleet_dashboard.md
new file mode 100644
index 00000000000..2a7c7d05453
--- /dev/null
+++ b/doc/development/runner_fleet_dashboard.md
@@ -0,0 +1,245 @@
+---
+stage: Verify
+group: Runner
+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
+---
+# Runner Fleet Dashboard **(ULTIMATE BETA)**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/424495) in GitLab 16.6 behind several [feature flags](#enable-feature-flags).
+
+This feature is in [BETA](../policy/experiment-beta-support.md).
+To join the list of users testing this feature, contact us in
+[epic 11180](https://gitlab.com/groups/gitlab-org/-/epics/11180).
+
+GitLab administrators can use the Runner Fleet Dashboard to assess the health of your instance runners.
+The Runner Fleet Dashboard shows:
+
+- Recent CI errors related caused by runner infrastructure.
+- Number of concurrent jobs executed on most busy runners.
+- Histogram of job queue times (available only with ClickHouse).
+
+There is a proposal to introduce [more features](#whats-next) to the Runner Fleet Dashboard.
+
+![Runner Fleet Dashboard](img/runner_fleet_dashboard.png)
+
+## View the Runner Fleet Dashboard
+
+Prerequisites:
+
+- You must be an administrator.
+
+To view the runner fleet dashboard:
+
+1. On the left sidebar, select **Search or go to**.
+1. Select **Admin Area**.
+1. Select **Runners**.
+1. Click **Fleet dashboard**.
+
+Most of the dashboard works without any additional actions, with the
+exception of **Wait time to pick a job** chart and [proposed features](#whats-next).
+These features require setting up an additional infrastructure, described in this page.
+
+To test the Runner Fleet Dashboard and gather feedback, we have launched an early adopters program
+for some customers to try this feature.
+
+## Requirements
+
+To test the Runner Fleet Dashboard as part of the early adopters program, you must:
+
+- Run GitLab 16.6 or above.
+- Have an [Ultimate license](https://about.gitlab.com/pricing/).
+- Be able to run ClickHouse database. We recommend using [ClickHouse Cloud](https://clickhouse.cloud/).
+
+## Setup
+
+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. [Enable the feature flags](#enable-feature-flags).
+
+### Run and configure ClickHouse
+
+The most straightforward way to run ClickHouse is with [ClickHouse Cloud](https://clickhouse.cloud/).
+You can also [run ClickHouse on your own server](https://clickhouse.com/docs/en/install). Refer to the ClickHouse
+documentation regarding [recommendations for self-managed instances](https://clickhouse.com/docs/en/install#recommendations-for-self-managed-clickhouse).
+
+When you run ClickHouse on a hosted server, various data points might impact the resource consumption, like the number
+of builds that run on your instance each month, the selected hardware, the data center choice to host ClickHouse, and more.
+Regardless, the cost should not be significant.
+
+NOTE:
+ClickHouse is a secondary data store for GitLab. All your data is still stored in Postgres,
+and only duplicated in ClickHouse for analytics purposes.
+
+To create necessary user and database objects:
+
+1. Generate a secure password and save it.
+1. Sign in to the ClickHouse SQL console.
+1. Execute the following command. Replace `PASSWORD_HERE` with the generated password.
+
+ ```sql
+ CREATE DATABASE gitlab_clickhouse_main_production;
+ CREATE USER gitlab IDENTIFIED WITH sha256_password BY 'PASSWORD_HERE';
+ CREATE ROLE gitlab_app;
+ GRANT SELECT, INSERT, ALTER, CREATE, UPDATE, DROP, TRUNCATE, OPTIMIZE ON gitlab_clickhouse_main_production.* TO gitlab_app;
+ GRANT gitlab_app TO gitlab;
+ ```
+
+1. Connect to the `gitlab_clickhouse_main_production` database (or just switch it in the ClickHouse Cloud UI).
+
+1. To create the required database objects, execute:
+
+ ```sql
+ CREATE TABLE ci_finished_builds
+ (
+ id UInt64 DEFAULT 0,
+ project_id UInt64 DEFAULT 0,
+ pipeline_id UInt64 DEFAULT 0,
+ status LowCardinality(String) DEFAULT '',
+ created_at DateTime64(6, 'UTC') DEFAULT now(),
+ queued_at DateTime64(6, 'UTC') DEFAULT now(),
+ finished_at DateTime64(6, 'UTC') DEFAULT now(),
+ started_at DateTime64(6, 'UTC') DEFAULT now(),
+ runner_id UInt64 DEFAULT 0,
+ runner_manager_system_xid String DEFAULT '',
+ runner_run_untagged Boolean DEFAULT FALSE,
+ runner_type UInt8 DEFAULT 0,
+ runner_manager_version LowCardinality(String) DEFAULT '',
+ runner_manager_revision LowCardinality(String) DEFAULT '',
+ runner_manager_platform LowCardinality(String) DEFAULT '',
+ runner_manager_architecture LowCardinality(String) DEFAULT '',
+ duration Int64 MATERIALIZED age('ms', started_at, finished_at),
+ queueing_duration Int64 MATERIALIZED age('ms', queued_at, started_at)
+ )
+ ENGINE = ReplacingMergeTree
+ ORDER BY (status, runner_type, project_id, finished_at, id)
+ PARTITION BY toYear(finished_at);
+
+ CREATE TABLE ci_finished_builds_aggregated_queueing_delay_percentiles
+ (
+ status LowCardinality(String) DEFAULT '',
+ runner_type UInt8 DEFAULT 0,
+ started_at_bucket DateTime64(6, 'UTC') DEFAULT now(),
+
+ count_builds AggregateFunction(count),
+ queueing_duration_quantile AggregateFunction(quantile, Int64)
+ )
+ ENGINE = AggregatingMergeTree()
+ ORDER BY (started_at_bucket, status, runner_type);
+
+ CREATE MATERIALIZED VIEW ci_finished_builds_aggregated_queueing_delay_percentiles_mv
+ TO ci_finished_builds_aggregated_queueing_delay_percentiles
+ AS
+ SELECT
+ status,
+ runner_type,
+ toStartOfInterval(started_at, INTERVAL 5 minute) AS started_at_bucket,
+
+ countState(*) as count_builds,
+ quantileState(queueing_duration) AS queueing_duration_quantile
+ FROM ci_finished_builds
+ GROUP BY status, runner_type, started_at_bucket;
+ ```
+
+### Configure the GitLab connection to ClickHouse
+
+::Tabs
+
+:::TabTitle Linux package
+
+To provide GitLab with ClickHouse credentials:
+
+1. Edit `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ gitlab_rails['clickhouse_databases']['main']['database'] = 'gitlab_clickhouse_main_production'
+ gitlab_rails['clickhouse_databases']['main']['url'] = 'https://example.com/path'
+ gitlab_rails['clickhouse_databases']['main']['username'] = 'gitlab'
+ gitlab_rails['clickhouse_databases']['main']['password'] = 'PASSWORD_HERE' # replace with the actual password
+ ```
+
+1. Save the file and reconfigure GitLab:
+
+ ```shell
+ sudo gitlab-ctl reconfigure
+ ```
+
+:::TabTitle Helm chart (Kubernetes)
+
+1. Save the ClickHouse password as a Kubernetes Secret:
+
+ ```shell
+ kubectl create secret generic gitlab-clickhouse-password --from-literal="main_password=PASSWORD_HERE"
+ ```
+
+1. Export the Helm values:
+
+ ```shell
+ helm get values gitlab > gitlab_values.yaml
+ ```
+
+1. Edit `gitlab_values.yaml`:
+
+ ```yaml
+ global:
+ clickhouse:
+ enabled: true
+ main:
+ username: default
+ password:
+ secret: gitlab-clickhouse-password
+ key: main_password
+ database: gitlab_clickhouse_main_production
+ url: 'http://example.com'
+ ```
+
+1. Save the file and apply the new values:
+
+ ```shell
+ helm upgrade -f gitlab_values.yaml gitlab gitlab/gitlab
+ ```
+
+::EndTabs
+
+To verify that your connection is set up successfully:
+
+1. Log in to [Rails console](../administration/operations/rails_console.md#starting-a-rails-console-session)
+1. Execute the following:
+
+ ```ruby
+ ClickHouse::Client.select('SELECT 1', :main)
+ ```
+
+ If successful, the command returns `[{"1"=>1}]`
+
+### Enable feature flags
+
+Features that use ClickHouse are currently under development and are disabled by feature flags.
+
+To enable these features, [enable](../administration/feature_flags.md#how-to-enable-and-disable-features-behind-flags)
+the following feature flags:
+
+| Feature flag name | Purpose |
+|------------------------------------|---------------------------------------------------------------------------|
+| `ci_data_ingestion_to_click_house` | Enables synchronization of new finished CI builds to Clickhouse database. |
+| `clickhouse_ci_analytics` | Enables the **Wait time to pick a job** chart. |
+
+## What's next
+
+Support for usage and cost analysis are proposed in
+[epic 11183](https://gitlab.com/groups/gitlab-org/-/epics/11183).
+
+## Feedback
+
+To help us improve the Runner Fleet Dashboard, you can provide feedback in
+[issue 421737](https://gitlab.com/gitlab-org/gitlab/-/issues/421737).
+In particular:
+
+- How easy or difficult it was to setup GitLab to make the dashboard work.
+- How useful you found the dashboard.
+- What other information you would like to see on that dashboard.
+- Any other related thoughts and ideas.
diff --git a/doc/development/testing_guide/end_to_end/beginners_guide.md b/doc/development/testing_guide/end_to_end/beginners_guide.md
index 12f90e0d88c..4a3aec97d29 100644
--- a/doc/development/testing_guide/end_to_end/beginners_guide.md
+++ b/doc/development/testing_guide/end_to_end/beginners_guide.md
@@ -127,7 +127,7 @@ Assign `product_group` metadata and specify what product group this test belongs
module QA
RSpec.describe 'Manage' do
- describe 'Login', product_group: :authentication_and_authorization do
+ describe 'Login', product_group: :authentication do
end
end
@@ -142,7 +142,7 @@ writing end-to-end tests is to write test case descriptions as `it` blocks:
```ruby
module QA
RSpec.describe 'Manage' do
- describe 'Login', product_group: :authentication_and_authorization do
+ describe 'Login', product_group: :authentication do
it 'can login' do
end
@@ -166,7 +166,7 @@ Begin by logging in.
module QA
RSpec.describe 'Manage' do
- describe 'Login', product_group: :authentication_and_authorization do
+ describe 'Login', product_group: :authentication do
it 'can login' do
Flow::Login.sign_in
@@ -189,7 +189,7 @@ should answer the question "What do we test?"
module QA
RSpec.describe 'Manage' do
- describe 'Login', product_group: :authentication_and_authorization do
+ describe 'Login', product_group: :authentication do
it 'can login' do
Flow::Login.sign_in
@@ -236,7 +236,7 @@ a call to `sign_in`.
module QA
RSpec.describe 'Manage' do
- describe 'Login', product_group: :authentication_and_authorization do
+ describe 'Login', product_group: :authentication do
before do
Flow::Login.sign_in
end
diff --git a/doc/development/testing_guide/end_to_end/capybara_to_chemlab_migration_guide.md b/doc/development/testing_guide/end_to_end/capybara_to_chemlab_migration_guide.md
index 7bac76c88e8..025f998c0c9 100644
--- a/doc/development/testing_guide/end_to_end/capybara_to_chemlab_migration_guide.md
+++ b/doc/development/testing_guide/end_to_end/capybara_to_chemlab_migration_guide.md
@@ -35,44 +35,6 @@ Given the view:
| ------ | ----- |
| ![before](img/gl-capybara_V13_12.png) | ![after](img/gl-chemlab_V13_12.png) |
-<!--
-```ruby
-# frozen_string_literal: true
-
-module QA
- module Page
- class Form < Page::Base
- view '_form.html' do
- element :first_name
- element :last_name
- element :company_name
- element :user_name
- element :password
- element :continue
- end
- end
- end
-end
-```
-```ruby
-# frozen_string_literal: true
-
-module QA
- module Page
- class Form < Chemlab::Page
- text_field :first_name
- text_field :last_name
- text_field :company_name
- text_field :user_name
- text_field :password
-
- button :continue
- end
- end
-end
-```
--->
-
## Key Differences
### Page Library Design vs Page Object Design
diff --git a/doc/development/utilities.md b/doc/development/utilities.md
index 343d03b9d68..83b87d6d289 100644
--- a/doc/development/utilities.md
+++ b/doc/development/utilities.md
@@ -206,7 +206,7 @@ Refer to [`strong_memoize.rb`](https://gitlab.com/gitlab-org/gitlab/-/blob/maste
# good
def expensive_method(arg)
- strong_memoize_with(:expensive_method, arg)
+ strong_memoize_with(:expensive_method, arg) do
# ...
end
end
diff --git a/doc/development/wikis.md b/doc/development/wikis.md
index a814fa76ec9..eca43f6df03 100644
--- a/doc/development/wikis.md
+++ b/doc/development/wikis.md
@@ -28,9 +28,6 @@ Some notable gems that are used for wikis are:
| Component | Description | Gem name | GitLab project | Upstream project |
|:--------------|:-----------------------------------------------|:-------------------------------|:--------------------------------------------------------------------------------------------------------|:--------------------------------------------------------------------|
| `gitlab` | Markup renderer, depends on various other gems | `gitlab-markup` | [`gitlab-org/gitlab-markup`](https://gitlab.com/gitlab-org/gitlab-markup) | [`github/markup`](https://github.com/github/markup) |
-| `gollum-lib` | Main Gollum library | `gitlab-gollum-lib` | [`gitlab-org/gollum-lib`](https://gitlab.com/gitlab-org/gollum-lib) | [`gollum/gollum-lib`](https://github.com/gollum/gollum-lib) |
-| | Gollum Git adapter for Rugged | `gitlab-gollum-rugged_adapter` | [`gitlab-org/gitlab-gollum-rugged_adapter`](https://gitlab.com/gitlab-org/gitlab-gollum-rugged_adapter) | [`gollum/rugged_adapter`](https://github.com/gollum/rugged_adapter) |
-| | Rugged (also used in Gitaly itself) | `rugged` | - | [`libgit2/rugged`](https://github.com/libgit2/rugged) |
### Notes on Gollum
diff --git a/doc/devsecops.md b/doc/devsecops.md
new file mode 100644
index 00000000000..f035121898a
--- /dev/null
+++ b/doc/devsecops.md
@@ -0,0 +1,60 @@
+---
+stage: none
+group: unassigned
+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
+description: 'Learn how to use and administer GitLab, the most scalable Git-based fully integrated platform for software development.'
+---
+
+# GitLab: The DevSecOps platform
+
+ DevSecOps is a combination of development, security, and operations.
+ It is an approach to software development that integrates security throughout the development lifecycle.
+
+## DevSecOps compared to DevOps
+
+DevOps combines development and operations, with the intent to increase the efficiency,
+speed, and security of software development and delivery.
+
+DevOps means working together to conceive, build, and deliver secure software at top speed.
+DevOps practices include automation, collaboration, fast feedback, and iterative improvement.
+
+DevSecOps is an evolution of DevOps. DevSecOps includes application security practices in every stage of software development.
+
+Throughout the development process, tools and methods protect and monitor your live applications.
+New attack surfaces, like containers and orchestrators, must also be monitored and protected.
+DevSecOps tools automate security workflows to create an adaptable process for your development
+and security teams, improving collaboration and breaking down silos.
+By embedding security into the software development lifecycle, you can consistently secure fast-moving
+and iterative processes, improving efficiency without sacrificing quality.
+
+## DevSecOps fundamentals
+
+DevSecOps fundamentals include:
+
+- Automation
+- Collaboration
+- Policy guardrails
+- Visibility
+
+For details, see [this article about DevSecOps](https://about.gitlab.com/topics/devsecops/).
+
+## Is DevSecOps right for you?
+
+If your organization is facing any of the following challenges, a DevSecOps approach might be for you.
+
+- **Development, security, and operations teams are siloed.**
+ If development and operations are isolated from security issues,
+ they can't build secure software. And if security teams aren't part of the development process,
+ they can't identify risks proactively. DevSecOps brings teams together to improve workflows
+ and share ideas. Organizations might even see improved employee morale and retention.
+
+- **Long development cycles are making it difficult to meet customer or stakeholder demands.**
+ One reason for the struggle could be security. DevSecOps implements security at every step of
+ the development lifecycle, meaning that solid security doesn’t require the whole process to come to a halt.
+
+- **You’re migrating to the cloud (or considering it).**
+ Moving to the cloud often means bringing on new development processes, tools, and systems.
+ It’s a great time to make processes faster and more secure — and DevSecOps could make that a lot easier.
+
+To get started with DevSecOps,
+[learn more, and try GitLab Ultimate for free](https://about.gitlab.com/solutions/security-compliance/).
diff --git a/doc/gitlab-basics/start-using-git.md b/doc/gitlab-basics/start-using-git.md
index 91fa91e3a6a..c46b89f7620 100644
--- a/doc/gitlab-basics/start-using-git.md
+++ b/doc/gitlab-basics/start-using-git.md
@@ -117,8 +117,10 @@ This connection requires you to add credentials. You can either use SSH or HTTPS
Clone with SSH when you want to authenticate only one time.
1. Authenticate with GitLab by following the instructions in the [SSH documentation](../user/ssh.md).
-1. Go to your project's landing page and select **Clone**. Copy the URL for **Clone with SSH**.
-1. Open a terminal and go to the directory where you want to clone the files. Git automatically creates a folder with the repository name and downloads the files there.
+1. On the left sidebar, select **Search or go to** and find the project you want to clone.
+1. On the right-hand side of the page, select **Clone**, then copy the URL for **Clone with SSH**.
+1. Open a terminal and go to the directory where you want to clone the files.
+ Git automatically creates a folder with the repository name and downloads the files there.
1. Run this command:
```shell
@@ -139,7 +141,8 @@ You can also
Clone with HTTPS when you want to authenticate each time you perform an operation
between your computer and GitLab.
-1. Go to your project's landing page and select **Clone**. Copy the URL for **Clone with HTTPS**.
+1. On the left sidebar, select **Search or go to** and find the project you want to clone.
+1. On the right-hand side of the page, select **Clone**, then copy the URL for **Clone with HTTPS**.
1. Open a terminal and go to the directory where you want to clone the files.
1. Run the following command. Git automatically creates a folder with the repository name and downloads the files there.
diff --git a/doc/install/aws/eks_clusters_aws.md b/doc/install/aws/eks_clusters_aws.md
index 45ba46fce1e..b05749bdde3 100644
--- a/doc/install/aws/eks_clusters_aws.md
+++ b/doc/install/aws/eks_clusters_aws.md
@@ -1,46 +1,11 @@
---
-stage: Systems
-group: Distribution
-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
+redirect_to: '../../solutions/cloud/aws/index.md'
+remove_date: '2024-03-31'
---
-# EKS cluster provisioning best practices **(FREE SELF)**
+This document was moved to [Solutions](../../solutions/cloud/aws/index.md).
-GitLab can be used to provision an EKS cluster into AWS, however, it necessarily focuses on a basic EKS configuration. Using the AWS tools can help with advanced cluster configuration, automation, and maintenance.
-
-This documentation is not for clusters for deployment of GitLab itself, but instead clusters purpose built for:
-
-- EKS Clusters for GitLab Runners
-- Application Deployment Clusters for GitLab review apps
-- Application Deployment Cluster for production applications
-
-Information on deploying GitLab onto EKS can be found in [Provisioning GitLab Cloud Native Hybrid on AWS EKS](gitlab_hybrid_on_aws.md).
-
-## Use `eksctl`
-
-Using `eksctl` enables the following when building an EKS Cluster:
-
-- You have various cluster configuration options:
- - Selection of operating system: Amazon Linux 2, Windows, Bottlerocket
- - Selection of Hardware Architecture: x86, ARM, GPU
- - Selection of Kubernetes version (the GitLab-managed clusters for your project's applications have [specific Kubernetes version requirements](../../user/clusters/agent/index.md#supported-kubernetes-versions-for-gitlab-features))
-- It can deploy high value-add items to the cluster, including:
- - A bastion host to keep the cluster endpoint private and possible perform performance testing.
- - Prometheus and Grafana for monitoring.
-- EKS Autoscaler for automatic K8s Node scaling.
-- 2 or 3 Availability Zones (AZ) spread for balance between High Availability (HA) and cost control.
-- Ability to specify spot compute.
-
-Read more about configuring Amazon EKS in the [`eksctl` guide](https://eksctl.io/getting-started/) and the [Amazon EKS User Guide](https://docs.aws.amazon.com/eks/latest/userguide/getting-started.html).
-
-## Inject GitLab configuration for integrating clusters
-
-Read more how to [configure an App Deployment cluster](../../user/project/clusters/add_existing_cluster.md) and extract information from it to integrate it into GitLab.
-
-## Provision GitLab Runners using Helm charts
-
-Read how to [use the GitLab Runner Helm Chart](https://docs.gitlab.com/runner/install/kubernetes.html) to deploy a runner into a cluster.
-
-## Runner Cache
-
-Because the EKS Quick Start provides for EFS provisioning, the best approach is to use EFS for runner caching. Eventually we will publish information on using an S3 bucket for runner caching here.
+<!-- This redirect file can be deleted after <YYYY-MM-DD>. -->
+<!-- Redirects that point to other docs in the same project expire in three months. -->
+<!-- Redirects that point to docs in a different project or site (for example, link is not relative and starts with `https:`) expire in one year. -->
+<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/redirects.html --> \ No newline at end of file
diff --git a/doc/install/aws/gitlab_hybrid_on_aws.md b/doc/install/aws/gitlab_hybrid_on_aws.md
index b39f39f293e..84474e6615c 100644
--- a/doc/install/aws/gitlab_hybrid_on_aws.md
+++ b/doc/install/aws/gitlab_hybrid_on_aws.md
@@ -1,377 +1,11 @@
---
-stage: Systems
-group: Distribution
-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
+redirect_to: '../../solutions/cloud/aws/gitlab_instance_on_aws.md'
+remove_date: '2024-03-31'
---
-{::options parse_block_html="true" /}
+This document was moved to [Solutions](../../solutions/cloud/aws/gitlab_instance_on_aws.md).
-# Provision GitLab Cloud Native Hybrid on AWS EKS **(FREE SELF)**
-
-GitLab "Cloud Native Hybrid" is a hybrid of the cloud native technology Kubernetes (EKS) and EC2. While as much of the GitLab application as possible runs in Kubernetes or on AWS services (PaaS), the GitLab service Gitaly must still be run on EC2. Gitaly is a layer designed to overcome limitations of the Git binaries in a horizontally scaled architecture. You can read more here about why Gitaly was built and why the limitations of Git mean that it must currently run on instance compute in [Git Characteristics That Make Horizontal Scaling Difficult](https://gitlab.com/gitlab-org/gitaly/-/blob/master/doc/DESIGN.md#git-characteristics-that-make-horizontal-scaling-difficult).
-
-Amazon provides a managed Kubernetes service offering known as [Amazon Elastic Kubernetes Service (EKS)](https://aws.amazon.com/eks/).
-
-## Tested AWS Bill of Materials by reference architecture size
-
-| GitLab Cloud Native Hybrid Ref Arch | GitLab Baseline Performance Test Results (using the Linux package on instances) | AWS Bill of Materials (BOM) for CNH | AWS Build Performance Testing Results for [CNH](https://gitlab.com/guided-explorations/aws/implementation-patterns/gitlab-cloud-native-hybrid-on-eks/-/blob/master/gitlab-alliances-testing/5K/5k-QuickStart-ARM-RDS-Redis_v13-12-3-ee_2021-07-23_140128/5k-QuickStart-ARM-RDS-Redis_v13-12-3-ee_2021-07-23_140128_results.txt) | CNH Cost Estimate 3 AZs* |
-| ------------------------------------------------------------ | ------------------------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ |
-| [2K Linux package installation](../../administration/reference_architectures/2k_users.md) | [2K Baseline](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Benchmarks/Latest/2k) | [2K Cloud Native Hybrid on EKS](#2k-cloud-native-hybrid-on-eks) | GPT Test Results | [1 YR Ec2 Compute Savings + 1 YR RDS & ElastiCache RIs](https://calculator.aws/#/estimate?id=544bcf1162beae6b8130ad257d081cdf9d4504e3)<br />(2 AZ Cost Estimate is in BOM Below) |
-| [3K](../../administration/reference_architectures/3k_users.md#cloud-native-hybrid-reference-architecture-with-helm-charts-alternative) | [3k Baseline](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Benchmarks/Latest/3k) | [3K Cloud Native Hybrid on EKS](#3k-cloud-native-hybrid-on-eks) | [3K Full Fixed Scale GPT Test Results](https://gitlab.com/guided-explorations/aws/implementation-patterns/gitlab-cloud-native-hybrid-on-eks/-/blob/master/gitlab-alliances-testing/3K/3k-QuickStart-ARM-RDS-Cache_v13-12-3-ee_2021-07-23_124216/3k-QuickStart-ARM-RDS-Cache_v13-12-3-ee_2021-07-23_124216_results.txt)<br /><br />[3K Elastic Auto Scale GPT Test Results](https://gitlab.com/guided-explorations/aws/implementation-patterns/gitlab-cloud-native-hybrid-on-eks/-/blob/master/gitlab-alliances-testing/3K/3k-QuickStart-AutoScale-ARM-RDS-Cache_v13-12-3-ee_2021-07-23_194200/3k-QuickStart-AutoScale-ARM-RDS-Cache_v13-12-3-ee_2021-07-23_194200_results.txt) | [1 YR Ec2 Compute Savings + 1 YR RDS & ElastiCache RIs](https://calculator.aws/#/estimate?id=f1294fec554e21be999711cddcdab9c5e7f83f14)<br />(2 AZ Cost Estimate is in BOM Below) |
-| [5K](../../administration/reference_architectures/5k_users.md#cloud-native-hybrid-reference-architecture-with-helm-charts-alternative) | [5k Baseline](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Benchmarks/Latest/5k) | [5K Cloud Native Hybrid on EKS](#5k-cloud-native-hybrid-on-eks) | [5K Full Fixed Scale GPT Test Results](https://gitlab.com/guided-explorations/aws/implementation-patterns/gitlab-cloud-native-hybrid-on-eks/-/blob/master/gitlab-alliances-testing/5K/5k-QuickStart-ARM-RDS-Redis_v13-12-3-ee_2021-07-23_140128/5k-QuickStart-ARM-RDS-Redis_v13-12-3-ee_2021-07-23_140128_results.txt)<br /><br />[5K AutoScale from 25% GPT Test Results](https://gitlab.com/guided-explorations/aws/implementation-patterns/gitlab-cloud-native-hybrid-on-eks/-/blob/master/gitlab-alliances-testing/5K/5k-QuickStart-AutoScale-From-25Percent-ARM-RDS-Redis_v13-12-3-ee_2021-07-24_102717/5k-QuickStart-AutoScale-From-25Percent-ARM-RDS-Redis_v13-12-3-ee_2021-07-24_102717_results.txt) | [1 YR Ec2 Compute Savings + 1 YR RDS & ElastiCache RIs](https://calculator.aws/#/estimate?id=330ee43c5b14662db5df6e52b34898d181a09e16) |
-| [10K](../../administration/reference_architectures/10k_users.md#cloud-native-hybrid-reference-architecture-with-helm-charts-alternative) | [10k Baseline](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Benchmarks/Latest/10k) | [10K Cloud Native Hybrid on EKS](#10k-cloud-native-hybrid-on-eks) | [10K Full Fixed Scale GPT Test Results](https://gitlab.com/guided-explorations/aws/implementation-patterns/gitlab-cloud-native-hybrid-on-eks/-/blob/master/gitlab-alliances-testing/10K/GL-CloudNative-10k-RDS-Graviton_v13-12-3-ee_2021-07-08_194647/GL-CloudNative-10k-RDS-Graviton_v13-12-3-ee_2021-07-08_194647_results.txt)<br /><br />[10K Elastic Auto Scale GPT Test Results](https://gitlab.com/guided-explorations/aws/implementation-patterns/gitlab-cloud-native-hybrid-on-eks/-/blob/master/gitlab-alliances-testing/10K/GL-CloudNative-10k-AutoScaling-Test_v13-12-3-ee_2021-07-09_115139/GL-CloudNative-10k-AutoScaling-Test_v13-12-3-ee_2021-07-09_115139_results.txt) | [10K 1 YR Ec2 Compute Savings + 1 YR RDS & ElastiCache RIs](https://calculator.aws/#/estimate?id=5ac2e07a22e01c36ee76b5477c5a046cd1bea792) |
-| [50K](../../administration/reference_architectures/50k_users.md#cloud-native-hybrid-reference-architecture-with-helm-charts-alternative) | [50k Baseline](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Benchmarks/Latest/50k) | [50K Cloud Native Hybrid on EKS](#50k-cloud-native-hybrid-on-eks) | [50K Full Fixed Scale GPT Test Results](https://gitlab.com/guided-explorations/aws/implementation-patterns/gitlab-cloud-native-hybrid-on-eks/-/blob/master/gitlab-alliances-testing/50K/50k-Fixed-Scale-Test_v13-12-3-ee_2021-08-13_172819/50k-Fixed-Scale-Test_v13-12-3-ee_2021-08-13_172819_results.txt)<br /><br />[10K Elastic Auto Scale GPT Test Results](https://gitlab.com/guided-explorations/aws/implementation-patterns/gitlab-cloud-native-hybrid-on-eks/-/blob/master/gitlab-alliances-testing/50K/50k-AutoScale-Test_v13-12-3-ee_2021-08-13_192633/50k-AutoScale-Test_v13-12-3-ee_2021-08-13_192633.txt) | [50K 1 YR Ec2 Compute Savings + 1 YR RDS & ElastiCache RIs](https://calculator.aws/#/estimate?id=b9c9d6ac1d4a7848011d2050cef3120931fb7c22) |
-
-\*Cost calculations for actual implementations are a rough guideline with the following considerations:
-
-- Actual choices about instance types should be based on GPT testing of your configuration.
-- The first year of actual usage will reveal potential savings due to lower than expected usage, especially for ramping migrations where the full loading takes months, so be careful not to commit to savings plans too early or for too long.
-- The cost estimates assume full scale of the Kubernetes cluster nodes 24 x 7 x 365. Savings due to 'idling scale-in' are not considered because they are highly dependent on the usage patterns of the specific implementation.
-- Costs such as GitLab Runners, data egress and storage costs are not included as they are very dependent on the configuration of a specific implementation and on development behaviors (for example, frequency of committing or frequency of builds).
-- These estimates will change over time as GitLab tests and optimizes compute choices.
-
-## Available Infrastructure as Code for GitLab Cloud Native Hybrid
-
-The [GitLab Environment Toolkit (GET)](https://gitlab.com/gitlab-org/gitlab-environment-toolkit/-/blob/main/README.md) is a set of opinionated Terraform
-and Ansible scripts. These scripts help with the deployment of Linux package or Cloud Native Hybrid environments on selected cloud providers and are used
-by GitLab developers for [GitLab Dedicated](../../subscriptions/gitlab_dedicated/index.md) (for example).
-
-You can use the GitLab Environment Toolkit to deploy a Cloud Native Hybrid environment on AWS. However, it's not required and may not support every valid
-permutation. That said, the scripts are presented as-is and you can adapt them accordingly.
-
-### Two and Three Zone High Availability
-
-While GitLab Reference Architectures generally encourage three zone redundancy, AWS Quick Starts and AWS Well Architected consider two zone redundancy as AWS Well Architected. Individual implementations should weigh the costs of two and three zone configurations against their own high availability requirements for a final configuration.
-
-Gitaly Cluster uses a consistency voting system to implement strong consistency between synchronized nodes. Regardless of the number of availability zones implemented, there will always need to be a minimum of three Gitaly and three Praefect nodes in the cluster to avoid voting stalemates cause by an even number of nodes.
-
-### Streamlined Performance Testing of AWS Quick Start Prepared GitLab Instances
-
-A set of performance testing instructions have been abbreviated for testing a GitLab instance prepared using the AWS Quick Start for GitLab Cloud Native Hybrid on EKS. They assume zero familiarity with GitLab Performance Tool. They can be accessed here: [Performance Testing an Instance Prepared using AWS Quick Start for GitLab Cloud Native Hybrid on EKS](https://gitlab.com/guided-explorations/aws/implementation-patterns/getting-started-gitlab-aws-quick-start/-/wikis/Easy-Performance-Testing-for-AWS-Quick-Start-for-GitLab-CNH).
-
-### AWS GovCloud Support for AWS Quick Start for GitLab CNH on EKS
-
-The AWS Quick Start for GitLab Cloud Native Hybrid on EKS has been tested with GovCloud and works with the following restrictions and understandings.
-
-- GovCloud does not have public Route53 hosted zones, so you must set the following parameters:
-
- | CloudFormation Quick Start form field | CloudFormation Parameter | Setting |
- | --------------------------------------------------- | ------------------------ | ------- |
- | **Create Route 53 hosted zone** | CreatedHostedZone | No |
- | **Request AWS Certificate Manager SSL certificate** | CreateSslCertificate | No |
-
-- The Quick Start creates public load balancer IPs, so that you can easily configure your local hosts file to get to the GUI for GitLab when deploying tests. However, you may need to manually alter this if public load balancers are not part of your provisioning plan. We are planning to make non-public load balancers a configuration option issue link: [Short Term: Documentation and/or Automation for private GitLab instance with no internet Ingress](https://github.com/aws-quickstart/quickstart-eks-gitlab/issues/55)
-- As of 2021-08-19, AWS GovCloud has Graviton instances for Amazon RDS PostgreSQL available, but does not for ElastiCache Redis.
-- It is challenging to get the Quick Start template to load in GovCloud from the Standard Quick Start URL, so the generic ones are provided here:
- - [Launch for New VPC in us-gov-east-1](https://us-gov-east-1.console.amazonaws-us-gov.com/cloudformation/home?region=us-gov-east-1#/stacks/quickcreate?templateUrl=https://aws-quickstart.s3.us-east-1.amazonaws.com/quickstart-eks-gitlab/templates/gitlab-entry-new-vpc.template.yaml&stackName=Gitlab-for-EKS-New-VPC)
- - [Launch for New VPC in us-gov-west-1](https://us-gov-west-1.console.amazonaws-us-gov.com/cloudformation/home?region=us-gov-west-1#/stacks/quickcreate?templateUrl=https://aws-quickstart.s3.us-east-1.amazonaws.com/quickstart-eks-gitlab/templates/gitlab-entry-new-vpc.template.yaml&stackName=Gitlab-for-EKS-New-VPC)
-
-## AWS PaaS qualified for all GitLab implementations
-
-For both implementations that used the Linux package or Cloud Native Hybrid implementations, the following GitLab Service roles can be performed by AWS Services (PaaS). Any PaaS solutions that require preconfigured sizing based on the scale of your instance will also be listed in the per-instance size Bill of Materials lists. Those PaaS that do not require specific sizing, are not repeated in the BOM lists (for example, AWS Certification Manager).
-
-These services have been tested with GitLab.
-
-Some services, such as log aggregation, outbound email are not specified by GitLab, but where provided are noted.
-
-| GitLab Services | AWS PaaS (Tested) | Provided by AWS Cloud <br />Native Hybrid Quick Start |
-| ------------------------------------------------------------ | ------------------------------ | ------------------------------------------------------------ |
-| <u>Tested PaaS Mentioned in Reference Architectures</u> | | |
-| **PostgreSQL Database** | Amazon RDS PostgreSQL | Yes. |
-| **Redis Caching** | Redis ElastiCache | Yes. |
-| **Gitaly Cluster (Git Repository Storage)**<br />(Including Praefect and PostgreSQL) | ASG and Instances | Yes - ASG and Instances<br />**Note: Gitaly cannot be put into a Kubernetes Cluster.** |
-| **All GitLab storages besides Git Repository Storage**<br />(Includes Git-LFS which is S3 Compatible) | AWS S3 | Yes |
-| | | |
-| <u>Tested PaaS for Supplemental Services</u> | | |
-| **Front End Load Balancing** | AWS ELB | Yes |
-| **Internal Load Balancing** | AWS ELB | Yes |
-| **Outbound Email Services** | AWS Simple Email Service (SES) | Yes |
-| **Certificate Authority and Management** | AWS Certificate Manager (ACM) | Yes |
-| **DNS** | AWS Route53 (tested) | Yes |
-| **GitLab and Infrastructure Log Aggregation** | AWS CloudWatch Logs | Yes (ContainerInsights Agent for EKS) |
-| **Infrastructure Performance Metrics** | AWS CloudWatch Metrics | Yes |
-| | | |
-| <u>Supplemental Services and Configurations (Tested)</u> | | |
-| **Prometheus for GitLab** | AWS EKS (Cloud Native Only) | Yes |
-| **Grafana for GitLab** | AWS EKS (Cloud Native Only) | Yes |
-| **Administrative Access to GitLab Backend** | Bastion Host in VPC | Yes - HA - Preconfigured for Cluster Management. |
-| **Encryption (In Transit / At Rest)** | AWS KMS | Yes |
-| **Secrets Storage for Provisioning** | AWS Secrets Manager | Yes |
-| **Configuration Data for Provisioning** | AWS Parameter Store | Yes |
-| **AutoScaling Kubernetes** | EKS AutoScaling Agent | Yes |
-
-## GitLab Cloud Native Hybrid on AWS
-
-### 2K Cloud Native Hybrid on EKS
-
-**2K Cloud Native Hybrid on EKS Bill of Materials (BOM)**
-
-**GPT Test Results**
-
-- TBD
-
- **Deploy Now**
- Deploy Now links leverage the AWS Quick Start automation and only pre-populate the number of instances and instance types for the Quick Start based on the Bill of Materials below. You must provide appropriate input for all other parameters by following the guidance in the [Quick Start documentation's Deployment steps](https://aws-quickstart.github.io/quickstart-eks-gitlab/#_deployment_steps) section.
-
-- **Deploy Now: AWS Quick Start for 2 AZs**
-- **Deploy Now: AWS Quick Start for 3 AZs**
-
-NOTE:
-On Demand pricing is used in this table for comparisons, but should not be used for budgeting nor purchasing AWS resources for a GitLab production instance. Do not use these tables to calculate actual monthly or yearly price estimates, instead use the AWS Calculator links in the "GitLab on AWS Compute" table above and customize it with your desired savings plan.
-
-**BOM Total:** = Bill of Materials Total - this is what you use when building this configuration
-
-**Ref Arch Raw Total:** = The totals if the configuration was built on regular VMs with no PaaS services. Configuring on pure VMs generally requires additional VMs for cluster management activities.
-
-**Idle Configuration (Scaled-In)** = can be used to scale-in during time of low demand and/or for warm standby Geo instances. Requires configuration, testing and management of EKS autoscaling to meet your internal requirements.
-
-| Service | Ref Arch Raw (Full Scaled) | AWS BOM | Example Full Scaled Cost<br />(On Demand, US East) |
-| ------------------------------------------------------------ | -------------------------- | ------------------------------------------------------------ | -------------------------------------------------- |
-| Webservice | 12 vCPU,16 GB | | |
-| Sidekiq | 2 vCPU, 8 GB | | |
-| Supporting services such as NGINX, Prometheus, etc | 2 vCPU, 8 GB | | |
-| **GitLab Ref Arch Raw Total K8s Node Capacity** | 16 vCPU, 32 GB | | |
-| One Node for Overhead and Miscellaneous (EKS Cluster AutoScaler, Grafana, Prometheus, etc) | + 8 vCPU, 16 GB | | |
-| **Grand Total w/ Overheads**<br />Minimum hosts = 3 | 24 vCPU, 48 GB | **c5.2xlarge** <br />(8vCPU/16 GB) x 3 nodes<br />24 vCPU, 48 GB | $1.02/hr |
-| **Idle Configuration (Scaled-In)** | 16 vCPU, 32 GB | **c5.2xlarge** x 2 | $0.68/hr |
-
-NOTE:
-If EKS node autoscaling is employed, it is likely that your average loading will run lower than this, especially during non-working hours and weekends.
-
-| Non-Kubernetes Compute | Ref Arch Raw Total | AWS BOM<br />(Directly Usable in AWS Quick Start) | Example Cost<br />US East, 3 AZ | Example Cost<br />US East, 2 AZ |
-| ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------- | ------------------------------- | ------------------------------- |
-| **Bastion Host (Quick Start)** | 1 HA instance in ASG | **t2.micro** for prod, **m4.2xlarge** for performance testing | | |
-| **PostgreSQL**<br />AWS Amazon RDS PostgreSQL Nodes Configuration (GPT tested) | 2vCPU, 7.5 GB<br />Tested with Graviton ARM | **db.r6g.large** x 3 nodes <br />(6vCPU, 48 GB) | 3 nodes x $0.26 = $0.78/hr | 3 nodes x $0.26 = $0.78/hr |
-| **Redis** | 1vCPU, 3.75GB<br />(across 12 nodes for Redis Cache, Redis Queues/Shared State, Sentinel Cache, Sentinel Queues/Shared State) | **cache.m6g.large** x 3 nodes<br />(6vCPU, 19 GB) | 3 nodes x $0.15 = $0.45/hr | 2 nodes x $0.15 = $0.30/hr |
-| **<u>Gitaly Cluster</u>** [Details](gitlab_sre_for_aws.md#gitaly-sre-considerations) | [Gitaly & Praefect Must Have an Uneven Node Count for HA](gitlab_sre_for_aws.md#gitaly-and-praefect-elections) | | | |
-| Gitaly Instances (in ASG) | 12 vCPU, 45GB<br />(across 3 nodes) | **m5.xlarge** x 3 nodes<br />(48 vCPU, 180 GB) | $0.192 x 3 = $0.58/hr | $0.192 x 3 = $0.58/hr |
-| | The GitLab Reference architecture for 2K is not Highly Available and therefore has a single Gitaly no Praefect. AWS Quick Starts MUST be HA, so it implements Praefect from the 3K Ref Architecture to meet that requirement | | | |
-| Praefect (Instances in ASG with load balancer) | 6 vCPU, 10 GB<br />([across 3 nodes](gitlab_sre_for_aws.md#gitaly-and-praefect-elections)) | **c5.large** x 3 nodes<br />(6 vCPU, 12 GB) | $0.09 x 3 = $0.21/hr | $0.09 x 3 = $0.21/hr |
-| Praefect PostgreSQL(1) (AWS RDS) | 6 vCPU, 5.4 GB<br />([across 3 nodes](gitlab_sre_for_aws.md#gitaly-and-praefect-elections)) | Not applicable; reuses GitLab PostgreSQL | $0 | $0 |
-| Internal Load Balancing Node | 2 vCPU, 1.8 GB | AWS ELB | $0.10/hr | $0.10/hr |
-
-### 3K Cloud Native Hybrid on EKS
-
-**3K Cloud Native Hybrid on EKS Bill of Materials (BOM)**
-
-**GPT Test Results**
-
-- [3K Full Fixed Scale GPT Test Results](https://gitlab.com/guided-explorations/aws/implementation-patterns/gitlab-cloud-native-hybrid-on-eks/-/blob/master/gitlab-alliances-testing/3K/3k-QuickStart-ARM-RDS-Cache_v13-12-3-ee_2021-07-23_124216/3k-QuickStart-ARM-RDS-Cache_v13-12-3-ee_2021-07-23_124216_results.txt)
-
-- [3K AutoScale from 25% GPT Test Results](https://gitlab.com/guided-explorations/aws/implementation-patterns/gitlab-cloud-native-hybrid-on-eks/-/blob/master/gitlab-alliances-testing/3K/3k-QuickStart-AutoScale-ARM-RDS-Cache_v13-12-3-ee_2021-07-23_194200/3k-QuickStart-AutoScale-ARM-RDS-Cache_v13-12-3-ee_2021-07-23_194200_results.txt)
-
- Elastic Auto Scale GPT Test Results start with an idle scaled cluster and then start the standard GPT test to determine if the EKS Auto Scaler performs well enough to keep up with performance test demands. In general this is substantially harder ramping than the scaling required when the ramping is driven by standard production workloads.
-
-**Deploy Now**
-
-Deploy Now links leverage the AWS Quick Start automation and only pre-populate the number of instances and instance types for the Quick Start based on the Bill of Materials below. You must provide appropriate input for all other parameters by following the guidance in the [Quick Start documentation's Deployment steps](https://aws-quickstart.github.io/quickstart-eks-gitlab/#_deployment_steps) section.
-
-- **[Deploy Now: AWS Quick Start for 2 AZs](https://us-east-2.console.aws.amazon.com/cloudformation/home?region=us-east-2#/stacks/quickcreate?templateUrl=https://aws-quickstart.s3.us-east-1.amazonaws.com/quickstart-eks-gitlab/templates/gitlab-entry-new-vpc.template.yaml&stackName=Gitlab-EKS-3K-Users-2AZs&param_NumberOfAZs=2&param_NodeInstanceType=c5.2xlarge&param_NumberOfNodes=3&param_MaxNumberOfNodes=3&param_DBInstanceClass=db.r6g.xlarge&param_CacheNodes=2&param_CacheNodeType=cache.m6g.large&param_GitalyInstanceType=m5.large&param_NumberOfGitalyReplicas=3&param_PraefectInstanceType=c5.large&param_NumberOfPraefectReplicas=3)**
-- **[Deploy Now: AWS Quick Start for 3 AZs](https://us-east-2.console.aws.amazon.com/cloudformation/home?region=us-east-2#/stacks/quickcreate?templateUrl=https://aws-quickstart.s3.us-east-1.amazonaws.com/quickstart-eks-gitlab/templates/gitlab-entry-new-vpc.template.yaml&stackName=Gitlab-EKS-3K-Users-3AZs&param_NumberOfAZs=3&param_NodeInstanceType=c5.2xlarge&param_NumberOfNodes=3&param_MaxNumberOfNodes=3&param_DBInstanceClass=db.r6g.xlarge&param_CacheNodes=3&param_CacheNodeType=cache.m6g.large&param_GitalyInstanceType=m5.large&param_NumberOfGitalyReplicas=3&param_PraefectInstanceType=c5.large&param_NumberOfPraefectReplicas=3)**
-
-NOTE:
-On Demand pricing is used in this table for comparisons, but should not be used for budgeting nor purchasing AWS resources for a GitLab production instance. Do not use these tables to calculate actual monthly or yearly price estimates, instead use the AWS Calculator links in the "GitLab on AWS Compute" table above and customize it with your desired savings plan.
-
-**BOM Total:** = Bill of Materials Total - this is what you use when building this configuration
-
-**Ref Arch Raw Total:** = The totals if the configuration was built on regular VMs with no PaaS services. Configuring on pure VMs generally requires additional VMs for cluster management activities.
-
- **Idle Configuration (Scaled-In)** = can be used to scale-in during time of low demand and/or for warm standby Geo instances. Requires configuration, testing and management of EKS autoscaling to meet your internal requirements.
-
-| Service | Ref Arch Raw (Full Scaled) | AWS BOM | Example Full Scaled Cost<br />(On Demand, US East) |
-| ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | -------------------------------------------------- |
-| Webservice | [4 pods](https://gitlab.com/gitlab-org/charts/gitlab/-/blob/master/examples/ref/3k.yaml#L7) x ([5 vCPU & 6.25 GB](../../administration/reference_architectures/3k_users.md#webservice)) = <br />20 vCPU, 25 GB | | |
-| Sidekiq | [8 pods](https://gitlab.com/gitlab-org/charts/gitlab/-/blob/master/examples/ref/3k.yaml#L24) x ([1 vCPU & 2 GB](../../administration/reference_architectures/3k_users.md#sidekiq)) = <br />8 vCPU, 16 GB | | |
-| Supporting services such as NGINX, Prometheus, etc | [2 allocations](../../administration/reference_architectures/3k_users.md#cluster-topology) x ([2 vCPU and 7.5 GB](../../administration/reference_architectures/3k_users.md#cluster-topology)) = <br />4 vCPU, 15 GB | | |
-| **GitLab Ref Arch Raw Total K8s Node Capacity** | 32 vCPU, 56 GB | | |
-| One Node for Overhead and Miscellaneous (EKS Cluster AutoScaler, Grafana, Prometheus, etc) | + 16 vCPU, 32GB | | |
-| **Grand Total w/ Overheads Full Scale**<br />Minimum hosts = 3 | 48 vCPU, 88 GB | **c5.2xlarge** (8vCPU/16 GB) x 5 nodes<br />40 vCPU, 80 GB<br />[Full Fixed Scale GPT Test Results](https://gitlab.com/guided-explorations/aws/implementation-patterns/gitlab-cloud-native-hybrid-on-eks/-/blob/master/gitlab-alliances-testing/3K/3k-QuickStart-ARM-RDS-Cache_v13-12-3-ee_2021-07-23_124216/3k-QuickStart-ARM-RDS-Cache_v13-12-3-ee_2021-07-23_124216_results.txt) | $1.70/hr |
-| **Possible Idle Configuration (Scaled-In 75% - round up)**<br />Pod autoscaling must be also adjusted to enable lower idling configuration. | 24 vCPU, 48 GB | c5.2xlarge x 4 | $1.36/hr |
-
-Other combinations of node type and quantity can be used to meet the Grand Total. Due to the properties of pods, hosts that are overly small may have significant unused capacity.
-
-NOTE:
-If EKS node autoscaling is employed, it is likely that your average loading will run lower than this, especially during non-working hours and weekends.
-
-| Non-Kubernetes Compute | Ref Arch Raw Total | AWS BOM<br />(Directly Usable in AWS Quick Start) | Example Cost<br />US East, 3 AZ | Example Cost<br />US East, 2 AZ |
-| ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------- | ------------------------------- | ------------------------------------------------------------ |
-| **Bastion Host (Quick Start)** | 1 HA instance in ASG | **t2.micro** for prod, **m4.2xlarge** for performance testing | | |
-| **PostgreSQL**<br />Amazon RDS PostgreSQL Nodes Configuration (GPT tested) | 18vCPU, 36 GB <br />(across 9 nodes for PostgreSQL, PgBouncer, Consul)<br />Tested with Graviton ARM | **db.r6g.xlarge** x 3 nodes <br />(12vCPU, 96 GB) | 3 nodes x $0.52 = $1.56/hr | 3 nodes x $0.52 = $1.56/hr |
-| **Redis** | 6vCPU, 18 GB<br />(across 6 nodes for Redis Cache, Sentinel) | **cache.m6g.large** x 3 nodes<br />(6vCPU, 19 GB) | 3 nodes x $0.15 = $0.45/hr | 2 nodes x $0.15 = $0.30/hr |
-| **<u>Gitaly Cluster</u>** [Details](gitlab_sre_for_aws.md#gitaly-sre-considerations) | | | | |
-| Gitaly Instances (in ASG) | 12 vCPU, 45GB<br />([across 3 nodes](gitlab_sre_for_aws.md#gitaly-and-praefect-elections)) | **m5.large** x 3 nodes<br />(12 vCPU, 48 GB) | $0.192 x 3 = $0.58/hr | [Gitaly & Praefect Must Have an Uneven Node Count for HA](gitlab_sre_for_aws.md#gitaly-and-praefect-elections) |
-| Praefect (Instances in ASG with load balancer) | 6 vCPU, 5.4 GB<br />([across 3 nodes](gitlab_sre_for_aws.md#gitaly-and-praefect-elections)) | **c5.large** x 3 nodes<br />(6 vCPU, 12 GB) | $0.09 x 3 = $0.21/hr | [Gitaly & Praefect Must Have an Uneven Node Count for HA](gitlab_sre_for_aws.md#gitaly-and-praefect-elections) |
-| Praefect PostgreSQL(1) (Amazon RDS) | 6 vCPU, 5.4 GB<br />([across 3 nodes](gitlab_sre_for_aws.md#gitaly-and-praefect-elections)) | Not applicable; reuses GitLab PostgreSQL | $0 | |
-| Internal Load Balancing Node | 2 vCPU, 1.8 GB | AWS ELB | $0.10/hr | $0.10/hr |
-
-### 5K Cloud Native Hybrid on EKS
-
-**5K Cloud Native Hybrid on EKS Bill of Materials (BOM)**
-
-**GPT Test Results**
-
-- [5K Full Fixed Scale GPT Test Results](https://gitlab.com/guided-explorations/aws/implementation-patterns/gitlab-cloud-native-hybrid-on-eks/-/blob/master/gitlab-alliances-testing/5K/5k-QuickStart-ARM-RDS-Redis_v13-12-3-ee_2021-07-23_140128/5k-QuickStart-ARM-RDS-Redis_v13-12-3-ee_2021-07-23_140128_results.txt)
-
-- [5K AutoScale from 25% GPT Test Results](https://gitlab.com/guided-explorations/aws/implementation-patterns/gitlab-cloud-native-hybrid-on-eks/-/blob/master/gitlab-alliances-testing/5K/5k-QuickStart-AutoScale-From-25Percent-ARM-RDS-Redis_v13-12-3-ee_2021-07-24_102717/5k-QuickStart-AutoScale-From-25Percent-ARM-RDS-Redis_v13-12-3-ee_2021-07-24_102717_results.txt)
-
- Elastic Auto Scale GPT Test Results start with an idle scaled cluster and then start the standard GPT test to determine if the EKS Auto Scaler performs well enough to keep up with performance test demands. In general this is substantially harder ramping than the scaling required when the ramping is driven by standard production workloads.
-
-**Deploy Now**
-
-Deploy Now links leverage the AWS Quick Start automation and only prepopulate the number of instances and instance types for the Quick Start based on the Bill of Materials below. You must provide appropriate input for all other parameters by following the guidance in the [Quick Start documentation's Deployment steps](https://aws-quickstart.github.io/quickstart-eks-gitlab/#_deployment_steps) section.
-
-- **[Deploy Now: AWS Quick Start for 2 AZs](https://us-east-2.console.aws.amazon.com/cloudformation/home?region=us-east-2#/stacks/quickcreate?templateUrl=https://aws-quickstart.s3.us-east-1.amazonaws.com/quickstart-eks-gitlab/templates/gitlab-entry-new-vpc.template.yaml&stackName=Gitlab-EKS-5K-Users-2AZs&param_NumberOfAZs=2&param_NodeInstanceType=c5.2xlarge&param_NumberOfNodes=5&param_MaxNumberOfNodes=5&param_DBInstanceClass=db.r6g.2xlarge&param_CacheNodes=2&param_CacheNodeType=cache.m6g.xlarge&param_GitalyInstanceType=m5.2xlarge&param_NumberOfGitalyReplicas=2&param_PraefectInstanceType=c5.large&param_NumberOfPraefectReplicas=2)**
-- **[Deploy Now: AWS Quick Start for 3 AZs](https://us-east-2.console.aws.amazon.com/cloudformation/home?region=us-east-2#/stacks/quickcreate?templateUrl=https://aws-quickstart.s3.us-east-1.amazonaws.com/quickstart-eks-gitlab/templates/gitlab-entry-new-vpc.template.yaml&stackName=Gitlab-EKS-5K-Users-3AZs&param_NumberOfAZs=3&param_NodeInstanceType=c5.2xlarge&param_NumberOfNodes=5&param_MaxNumberOfNodes=5&param_DBInstanceClass=db.r6g.2xlarge&param_CacheNodes=3&param_CacheNodeType=cache.m6g.xlarge&param_GitalyInstanceType=m5.2xlarge&param_NumberOfGitalyReplicas=3&param_PraefectInstanceType=c5.large&param_NumberOfPraefectReplicas=3)**
-
-NOTE:
-On Demand pricing is used in this table for comparisons, but should not be used for budgeting nor purchasing AWS resources for a GitLab production instance. Do not use these tables to calculate actual monthly or yearly price estimates, instead use the AWS Calculator links in the "GitLab on AWS Compute" table above and customize it with your desired savings plan.
-
-**BOM Total:** = Bill of Materials Total - this is what you use when building this configuration
-
-**Ref Arch Raw Total:** = The totals if the configuration was built on regular VMs with no PaaS services. Configuring on pure VMs generally requires additional VMs for cluster management activities.
-
-**Idle Configuration (Scaled-In)** = can be used to scale-in during time of low demand and/or for warm standby Geo instances. Requires configuration, testing and management of EKS autoscaling to meet your internal requirements.
-
-| Service | Ref Arch Raw (Full Scaled) | AWS BOM | Example Full Scaled Cost<br />(On Demand, US East) |
-| ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | -------------------------------------------------- |
-| Webservice | [10 pods](https://gitlab.com/gitlab-org/charts/gitlab/-/blob/master/examples/ref/5k.yaml#L7) x ([5 vCPU & 6.25GB](../../administration/reference_architectures/5k_users.md#webservice)) = <br />50 vCPU, 62.5 GB | | |
-| Sidekiq | [8 pods](https://gitlab.com/gitlab-org/charts/gitlab/-/blob/master/examples/ref/5k.yaml#L24) x ([1 vCPU & 2 GB](../../administration/reference_architectures/5k_users.md#sidekiq)) = <br />8 vCPU, 16 GB | | |
-| Supporting services such as NGINX, Prometheus, etc | [2 allocations](../../administration/reference_architectures/5k_users.md#cluster-topology) x ([2 vCPU and 7.5 GB](../../administration/reference_architectures/5k_users.md#cluster-topology)) = <br />4 vCPU, 15 GB | | |
-| **GitLab Ref Arch Raw Total K8s Node Capacity** | 62 vCPU, 96.5 GB | | |
-| One Node for Quick Start Overhead and Miscellaneous (EKS Cluster AutoScaler, Grafana, Prometheus, etc) | + 8 vCPU, 16 GB | | |
-| **Grand Total w/ Overheads Full Scale**<br />Minimum hosts = 3 | 70 vCPU, 112.5 GB | **c5.2xlarge** (8vCPU/16 GB) x 9 nodes<br />72 vCPU, 144 GB<br />[Full Fixed Scale GPT Test Results](https://gitlab.com/guided-explorations/aws/implementation-patterns/gitlab-cloud-native-hybrid-on-eks/-/blob/master/gitlab-alliances-testing/5K/5k-QuickStart-ARM-RDS-Redis_v13-12-3-ee_2021-07-23_140128/5k-QuickStart-ARM-RDS-Redis_v13-12-3-ee_2021-07-23_140128_results.txt) | $2.38/hr |
-| **Possible Idle Configuration (Scaled-In 75% - round up)**<br />Pod autoscaling must be also adjusted to enable lower idling configuration. | 24 vCPU, 48 GB | c5.2xlarge x 7 | $1.85/hr |
-
-Other combinations of node type and quantity can be used to meet the Grand Total. Due to the CPU and memory requirements of pods, hosts that are overly small may have significant unused capacity.
-
-NOTE:
-If EKS node autoscaling is employed, it is likely that your average loading will run lower than this, especially during non-working hours and weekends.
-
-| Non-Kubernetes Compute | Ref Arch Raw Total | AWS BOM<br />(Directly Usable in AWS Quick Start) | Example Cost<br />US East, 3 AZ | Example Cost<br />US East, 2 AZ |
-| ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------- | ------------------------------- | ------------------------------------------------------------ |
-| **Bastion Host (Quick Start)** | 1 HA instance in ASG | **t2.micro** for prod, **m4.2xlarge** for performance testing | | |
-| **PostgreSQL**<br />Amazon RDS PostgreSQL Nodes Configuration (GPT tested) | 21vCPU, 51 GB <br />(across 9 nodes for PostgreSQL, PgBouncer, Consul)<br />Tested with Graviton ARM | **db.r6g.2xlarge** x 3 nodes <br />(24vCPU, 192 GB) | 3 nodes x $1.04 = $3.12/hr | 3 nodes x $1.04 = $3.12/hr |
-| **Redis** | 9vCPU, 27GB<br />(across 6 nodes for Redis, Sentinel) | **cache.m6g.xlarge** x 3 nodes<br />(12vCPU, 39GB) | 3 nodes x $0.30 = $0.90/hr | 2 nodes x $0.30 = $0.60/hr |
-| **<u>Gitaly Cluster</u>** [Details](gitlab_sre_for_aws.md#gitaly-sre-considerations) | | | | |
-| Gitaly Instances (in ASG) | 24 vCPU, 90GB<br />([across 3 nodes](gitlab_sre_for_aws.md#gitaly-and-praefect-elections)) | **m5.2xlarge** x 3 nodes<br />(24 vCPU, 96GB) | $0.384 x 3 = $1.15/hr | [Gitaly & Praefect Must Have an Uneven Node Count for HA](gitlab_sre_for_aws.md#gitaly-and-praefect-elections) |
-| Praefect (Instances in ASG with load balancer) | 6 vCPU, 5.4 GB<br />([across 3 nodes](gitlab_sre_for_aws.md#gitaly-and-praefect-elections)) | **c5.large** x 3 nodes<br />(6 vCPU, 12 GB) | $0.09 x 3 = $0.21/hr | [Gitaly & Praefect Must Have an Uneven Node Count for HA](gitlab_sre_for_aws.md#gitaly-and-praefect-elections) |
-| Praefect PostgreSQL(1) (Amazon RDS) | 6 vCPU, 5.4 GB<br />([across 3 nodes](gitlab_sre_for_aws.md#gitaly-and-praefect-elections)) | Not applicable; reuses GitLab PostgreSQL | $0 | |
-| Internal Load Balancing Node | 2 vCPU, 1.8 GB | AWS ELB | $0.10/hr | $0.10/hr |
-
-### 10K Cloud Native Hybrid on EKS
-
-**10K Cloud Native Hybrid on EKS Bill of Materials (BOM)**
-
-**GPT Test Results**
-
-- [10K Full Fixed Scale GPT Test Results](https://gitlab.com/guided-explorations/aws/implementation-patterns/gitlab-cloud-native-hybrid-on-eks/-/blob/master/gitlab-alliances-testing/10K/GL-CloudNative-10k-RDS-Graviton_v13-12-3-ee_2021-07-08_194647/GL-CloudNative-10k-RDS-Graviton_v13-12-3-ee_2021-07-08_194647_results.txt)
-
-- [10K Elastic Auto Scale GPT Test Results](https://gitlab.com/guided-explorations/aws/implementation-patterns/gitlab-cloud-native-hybrid-on-eks/-/blob/master/gitlab-alliances-testing/10K/GL-CloudNative-10k-AutoScaling-Test_v13-12-3-ee_2021-07-09_115139/GL-CloudNative-10k-AutoScaling-Test_v13-12-3-ee_2021-07-09_115139_results.txt)
-
- Elastic Auto Scale GPT Test Results start with an idle scaled cluster and then start the standard GPT test to determine if the EKS Auto Scaler performs well enough to keep up with performance test demands. In general this is substantially harder ramping than the scaling required when the ramping is driven by standard production workloads.
-
-**Deploy Now**
-
-Deploy Now links leverage the AWS Quick Start automation and only prepopulate the number of instances and instance types for the Quick Start based on the Bill of Materials below. You must provide appropriate input for all other parameters by following the guidance in the [Quick Start documentation's Deployment steps](https://aws-quickstart.github.io/quickstart-eks-gitlab/#_deployment_steps) section.
-
-- **[Deploy Now: AWS Quick Start for 3 AZs](https://us-east-2.console.aws.amazon.com/cloudformation/home?region=us-east-2#/stacks/quickcreate?templateUrl=https://aws-quickstart.s3.us-east-1.amazonaws.com/quickstart-eks-gitlab/templates/gitlab-entry-new-vpc.template.yaml&stackName=Gitlab-EKS-10K-Users-3AZs&param_NumberOfAZs=3&param_NodeInstanceType=c5.4xlarge&param_NumberOfNodes=9&param_MaxNumberOfNodes=9&param_DBInstanceClass=db.r6g.2xlarge&param_CacheNodes=3&param_CacheNodeType=cache.m6g.2xlarge&param_GitalyInstanceType=m5.4xlarge&param_NumberOfGitalyReplicas=3&param_PraefectInstanceType=c5.large&param_NumberOfPraefectReplicas=3)**
-
-NOTE:
-On Demand pricing is used in this table for comparisons, but should not be used for budgeting nor purchasing AWS resources for a GitLab production instance. Do not use these tables to calculate actual monthly or yearly price estimates, instead use the AWS Calculator links in the "GitLab on AWS Compute" table above and customize it with your desired savings plan.
-
-**BOM Total:** = Bill of Materials Total - this is what you use when building this configuration
-
-**Ref Arch Raw Total:** = The totals if the configuration was built on regular VMs with no PaaS services. Configuring on pure VMs generally requires additional VMs for cluster management activities.
-
- **Idle Configuration (Scaled-In)** = can be used to scale-in during time of low demand and/or for warm standby Geo instances. Requires configuration, testing and management of EKS autoscaling to meet your internal requirements.
-
-| Service | Ref Arch Raw (Full Scaled) | AWS BOM<br />(Directly Usable in AWS Quick Start) | Example Full Scaled Cost<br />(On Demand, US East) |
-| ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | -------------------------------------------------- |
-| Webservice | [20 pods](https://gitlab.com/gitlab-org/charts/gitlab/-/blob/master/examples/ref/10k.yaml#L7) x ([5 vCPU & 6.25 GB](../../administration/reference_architectures/10k_users.md#webservice)) = <br />100 vCPU, 125 GB | | |
-| Sidekiq | [14 pods](https://gitlab.com/gitlab-org/charts/gitlab/-/blob/master/examples/ref/10k.yaml#L24) x ([1 vCPU & 2 GB](../../administration/reference_architectures/10k_users.md#sidekiq))<br />14 vCPU, 28 GB | | |
-| Supporting services such as NGINX, Prometheus, etc | [2 allocations](../../administration/reference_architectures/10k_users.md#cluster-topology) x ([2 vCPU and 7.5 GB](../../administration/reference_architectures/10k_users.md#cluster-topology))<br />4 vCPU, 15 GB | | |
-| **GitLab Ref Arch Raw Total K8s Node Capacity** | 128 vCPU, 158 GB | | |
-| One Node for Overhead and Miscellaneous (EKS Cluster AutoScaler, Grafana, Prometheus, etc) | + 16 vCPU, 32GB | | |
-| **Grand Total w/ Overheads Fully Scaled**<br />Minimum hosts = 3 | 142 vCPU, 190 GB | **c5.4xlarge** (16vCPU/32GB) x 9 nodes<br />144 vCPU, 288GB<br /><br />[Full Fixed Scale GPT Test Results](https://gitlab.com/guided-explorations/aws/implementation-patterns/gitlab-cloud-native-hybrid-on-eks/-/blob/master/gitlab-alliances-testing/10K/GL-CloudNative-10k-RDS-Graviton_v13-12-3-ee_2021-07-08_194647/GL-CloudNative-10k-RDS-Graviton_v13-12-3-ee_2021-07-08_194647_results.txt) | $6.12/hr |
-| **Possible Idle Configuration (Scaled-In 75% - round up)**<br />Pod autoscaling must be also adjusted to enable lower idling configuration. | 40 vCPU, 80 GB | c5.4xlarge x 7<br /><br />[Elastic Auto Scale GPT Test Results](https://gitlab.com/guided-explorations/aws/implementation-patterns/gitlab-cloud-native-hybrid-on-eks/-/blob/master/gitlab-alliances-testing/10K/GL-CloudNative-10k-AutoScaling-Test_v13-12-3-ee_2021-07-09_115139/GL-CloudNative-10k-AutoScaling-Test_v13-12-3-ee_2021-07-09_115139_results.txt) | $4.76/hr |
-
-Other combinations of node type and quantity can be used to meet the Grand Total. Due to the CPU and memory requirements of pods, hosts that are overly small may have significant unused capacity.
-
-NOTE:
-If EKS node autoscaling is employed, it is likely that your average loading will run lower than this, especially during non-working hours and weekends.
-
-| Non-Kubernetes Compute | Ref Arch Raw Total | AWS BOM | Example Cost<br />US East, 3 AZ | Example Cost<br />US East, 2 AZ |
-| ------------------------------------------------------------ | ------------------------------ | ------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
-| **Bastion Host (Quick Start)** | 1 HA instance in ASG | **t2.micro** for prod, **m4.2xlarge** for performance testing | | |
-| **PostgreSQL**<br />Amazon RDS PostgreSQL Nodes Configuration (GPT tested) | 36vCPU, 102 GB <br />(across 9 nodes for PostgreSQL, PgBouncer, Consul) | **db.r6g.2xlarge** x 3 nodes <br />(24vCPU, 192 GB) | 3 nodes x $1.04 = $3.12/hr | 3 nodes x $1.04 = $3.12/hr |
-| **Redis** | 30vCPU, 114 GB<br />(across 12 nodes for Redis Cache, Redis Queues/Shared State, Sentinel Cache, Sentinel Queues/Shared State) | **cache.m5.2xlarge** x 3 nodes<br />(24vCPU, 78GB) | 3 nodes x $0.62 = $1.86/hr | 2 nodes x $0.62 = $1.24/hr |
-| **<u>Gitaly Cluster</u>** [Details](gitlab_sre_for_aws.md#gitaly-sre-considerations) | | | | |
-| Gitaly Instances (in ASG) | 48 vCPU, 180 GB<br />([across 3 nodes](gitlab_sre_for_aws.md#gitaly-and-praefect-elections)) | **m5.4xlarge** x 3 nodes<br />(48 vCPU, 180 GB) | $0.77 x 3 = $2.31/hr | [Gitaly & Praefect Must Have an Uneven Node Count for HA](gitlab_sre_for_aws.md#gitaly-and-praefect-elections) |
-| Praefect (Instances in ASG with load balancer) | 6 vCPU, 5.4 GB<br />([across 3 nodes](gitlab_sre_for_aws.md#gitaly-and-praefect-elections)) | **c5.large** x 3 nodes<br />(6 vCPU, 12 GB) | $0.09 x 3 = $0.21/hr | [Gitaly & Praefect Must Have an Uneven Node Count for HA](gitlab_sre_for_aws.md#gitaly-and-praefect-elections) |
-| Praefect PostgreSQL(1) (Amazon RDS) | 6 vCPU, 5.4 GB<br />([across 3 nodes](gitlab_sre_for_aws.md#gitaly-and-praefect-elections)) | Not applicable; reuses GitLab PostgreSQL | $0 | |
-| Internal Load Balancing Node | 2 vCPU, 1.8 GB | AWS ELB | $0.10/hr | $0.10/hr |
-
-### 50K Cloud Native Hybrid on EKS
-
-**50K Cloud Native Hybrid on EKS Bill of Materials (BOM)**
-
-**GPT Test Results**
-
-- [50K Full Fixed Scale GPT Test Results](https://gitlab.com/guided-explorations/aws/implementation-patterns/gitlab-cloud-native-hybrid-on-eks/-/blob/master/gitlab-alliances-testing/50K/50k-Fixed-Scale-Test_v13-12-3-ee_2021-08-13_172819/50k-Fixed-Scale-Test_v13-12-3-ee_2021-08-13_172819_results.txt)
-
-- [50K Elastic Auto Scale GPT Test Results](https://gitlab.com/guided-explorations/aws/implementation-patterns/gitlab-cloud-native-hybrid-on-eks/-/blob/master/gitlab-alliances-testing/50K/50k-AutoScale-Test_v13-12-3-ee_2021-08-13_192633/50k-AutoScale-Test_v13-12-3-ee_2021-08-13_192633.txt)
-
- Elastic Auto Scale GPT Test Results start with an idle scaled cluster and then start the standard GPT test to determine if the EKS Auto Scaler performs well enough to keep up with performance test demands. In general this is substantially harder ramping than the scaling required when the ramping is driven by standard production workloads.
-
-**Deploy Now**
-
-Deploy Now links leverage the AWS Quick Start automation and only prepopulate the number of instances and instance types for the Quick Start based on the Bill of Materials below. You must provide appropriate input for all other parameters by following the guidance in the [Quick Start documentation's Deployment steps](https://aws-quickstart.github.io/quickstart-eks-gitlab/#_deployment_steps) section.
-
-- **[Deploy Now: AWS Quick Start for 3 AZs - 1/4 Scale EKS](https://us-east-2.console.aws.amazon.com/cloudformation/home?region=us-east-2#/stacks/quickcreate?templateUrl=https://aws-quickstart.s3.us-east-1.amazonaws.com/quickstart-eks-gitlab/templates/gitlab-entry-new-vpc.template.yaml&stackName=Gitlab-EKS-50K-Users-3AZs&param_NumberOfAZs=3&param_NodeInstanceType=c5.4xlarge&param_NumberOfNodes=7&param_MaxNumberOfNodes=9&param_DBInstanceClass=db.r6g.8xlarge&param_CacheNodes=3&param_CacheNodeType=cache.m6g.2xlarge&param_GitalyInstanceType=m5.16xlarge&param_NumberOfGitalyReplicas=3&param_PraefectInstanceType=c5.xlarge&param_NumberOfPraefectReplicas=3)**
-
-NOTE:
-On Demand pricing is used in this table for comparisons, but should not be used for budgeting nor purchasing AWS resources for a GitLab production instance. Do not use these tables to calculate actual monthly or yearly price estimates, instead use the AWS Calculator links in the "GitLab on AWS Compute" table above and customize it with your desired savings plan.
-
-**BOM Total:** = Bill of Materials Total - this is what you use when building this configuration
-
-**Ref Arch Raw Total:** = The totals if the configuration was built on regular VMs with no PaaS services. Configuring on pure VMs generally requires additional VMs for cluster management activities.
-
- **Idle Configuration (Scaled-In)** = can be used to scale-in during time of low demand and/or for warm standby Geo instances. Requires configuration, testing and management of EKS autoscaling to meet your internal requirements.
-
-| Service | Ref Arch Raw (Full Scaled) | AWS BOM<br />(Directly Usable in AWS Quick Start) | Example Full Scaled Cost<br />(On Demand, US East) |
-| ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | -------------------------------------------------- |
-| Webservice | [80 pods](https://gitlab.com/gitlab-org/charts/gitlab/-/blob/master/examples/ref/10k.yaml#L7) x ([5 vCPU & 6.25 GB](../../administration/reference_architectures/10k_users.md#webservice)) = <br />400 vCPU, 500 GB | | |
-| Sidekiq | [14 pods](https://gitlab.com/gitlab-org/charts/gitlab/-/blob/master/examples/ref/10k.yaml#L24) x ([1 vCPU & 2 GB](../../administration/reference_architectures/10k_users.md#sidekiq))<br />14 vCPU, 28 GB | | |
-| Supporting services such as NGINX, Prometheus, etc | [2 allocations](../../administration/reference_architectures/10k_users.md#cluster-topology) x ([2 vCPU and 7.5 GB](../../administration/reference_architectures/10k_users.md#cluster-topology))<br />4 vCPU, 15 GB | | |
-| **GitLab Ref Arch Raw Total K8s Node Capacity** | 428 vCPU, 533 GB | | |
-| One Node for Overhead and Miscellaneous (EKS Cluster AutoScaler, Grafana, Prometheus, etc) | + 16 vCPU, 32GB | | |
-| **Grand Total w/ Overheads Fully Scaled**<br />Minimum hosts = 3 | 444 vCPU, 565 GB | **c5.4xlarge** (16vCPU/32GB) x 28 nodes<br />448 vCPU, 896GB<br /><br />[Full Fixed Scale GPT Test Results](https://gitlab.com/guided-explorations/aws/implementation-patterns/gitlab-cloud-native-hybrid-on-eks/-/blob/master/gitlab-alliances-testing/50K/50k-Fixed-Scale-Test_v13-12-3-ee_2021-08-13_172819/50k-Fixed-Scale-Test_v13-12-3-ee_2021-08-13_172819_results.txt) | $19.04/hr |
-| **Possible Idle Configuration (Scaled-In 75% - round up)**<br />Pod autoscaling must be also adjusted to enable lower idling configuration. | 40 vCPU, 80 GB | c5.2xlarge x 10<br /><br />[Elastic Auto Scale GPT Test Results](https://gitlab.com/guided-explorations/aws/implementation-patterns/gitlab-cloud-native-hybrid-on-eks/-/blob/master/gitlab-alliances-testing/50K/50k-AutoScale-Test_v13-12-3-ee_2021-08-13_192633/50k-AutoScale-Test_v13-12-3-ee_2021-08-13_192633.txt) | $6.80/hr |
-
-Other combinations of node type and quantity can be used to meet the Grand Total. Due to the CPU and memory requirements of pods, hosts that are overly small may have significant unused capacity.
-
-NOTE:
-If EKS node autoscaling is employed, it is likely that your average loading will run lower than this, especially during non-working hours and weekends.
-
-| Non-Kubernetes Compute | Ref Arch Raw Total | AWS BOM | Example Cost<br />US East, 3 AZ | Example Cost<br />US East, 2 AZ |
-| ------------------------------------------------------------ | ------------------------------------------------------------ | --------------------------------------------------------- | ------------------------------- | ------------------------------------------------------------ |
-| **Bastion Host (Quick Start)** | 1 HA instance in ASG | **t2.micro** for prod, **m4.2xlarge** for performance testing | | |
-| **PostgreSQL**<br />Amazon RDS PostgreSQL Nodes Configuration (GPT tested) | 96vCPU, 360 GB <br />(across 3 nodes) | **db.r6g.8xlarge** x 3 nodes <br />(96vCPU, 768 GB total) | 3 nodes x $4.15 = $12.45/hr | 3 nodes x $4.15 = $12.45/hr |
-| **Redis** | 30vCPU, 114 GB<br />(across 12 nodes for Redis Cache, Redis Queues/Shared State, Sentinel Cache, Sentinel Queues/Shared State) | **cache.m6g.2xlarge** x 3 nodes<br />(24vCPU, 78GB total) | 3 nodes x $0.60 = $1.80/hr | 2 nodes x $0.60 = $1.20/hr |
-| **<u>Gitaly Cluster</u>** [Details](gitlab_sre_for_aws.md#gitaly-sre-considerations) | | | | |
-| Gitaly Instances (in ASG) | 64 vCPU, 240GB x [3 nodes](gitlab_sre_for_aws.md#gitaly-and-praefect-elections) | **m5.16xlarge** x 3 nodes<br />(64 vCPU, 256 GB each) | $3.07 x 3 = $9.21/hr | [Gitaly & Praefect Must Have an Uneven Node Count for HA](gitlab_sre_for_aws.md#gitaly-and-praefect-elections) |
-| Praefect (Instances in ASG with load balancer) | 4 vCPU, 3.6 GB x [3 nodes](gitlab_sre_for_aws.md#gitaly-and-praefect-elections) | **c5.xlarge** x 3 nodes<br />(4 vCPU, 8 GB each) | $0.17 x 3 = $0.51/hr | [Gitaly & Praefect Must Have an Uneven Node Count for HA](gitlab_sre_for_aws.md#gitaly-and-praefect-elections) |
-| Praefect PostgreSQL(1) (AWS RDS) | 2 vCPU, 1.8 GB x [3 nodes](gitlab_sre_for_aws.md#gitaly-and-praefect-elections) | Not applicable; reuses GitLab PostgreSQL | $0 | |
-| Internal Load Balancing Node | 2 vCPU, 1.8 GB | AWS ELB | $0.10/hr | $0.10/hr |
-
-## Helpful Resources
-
-- [Architecting Kubernetes clusters — choosing a worker node size](https://learnk8s.io/kubernetes-node-size)
-
-DISCLAIMER:
-This page contains information related to upcoming products, features, and functionality.
-It is important to note that the information presented is for informational purposes only.
-Please do not rely on this information for purchasing or planning purposes.
-As with all projects, the items mentioned on this page are subject to change or delay.
-The development, release, and timing of any products, features, or functionality remain at the
-sole discretion of GitLab Inc.
+<!-- This redirect file can be deleted after <YYYY-MM-DD>. -->
+<!-- Redirects that point to other docs in the same project expire in three months. -->
+<!-- Redirects that point to docs in a different project or site (for example, link is not relative and starts with `https:`) expire in one year. -->
+<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/redirects.html --> \ No newline at end of file
diff --git a/doc/install/aws/gitlab_sre_for_aws.md b/doc/install/aws/gitlab_sre_for_aws.md
index 5f3fe9fefac..222bcbc1ed8 100644
--- a/doc/install/aws/gitlab_sre_for_aws.md
+++ b/doc/install/aws/gitlab_sre_for_aws.md
@@ -1,95 +1,11 @@
---
-stage: Systems
-group: Distribution
-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
-description: Doing SRE for GitLab instances and runners on AWS.
+redirect_to: '../../solutions/cloud/aws/gitaly_sre_for_aws.md'
+remove_date: '2024-03-31'
---
-# GitLab Site Reliability Engineering for AWS **(FREE SELF)**
+This document was moved to [Solutions](../../solutions/cloud/aws/gitaly_sre_for_aws.md).
-## Gitaly SRE considerations
-
-Gitaly is an embedded service for Git Repository Storage. Gitaly and Gitaly Cluster have been engineered by GitLab to overcome fundamental challenges with horizontal scaling of the open source Git binaries that must be used on the service side of GitLab. Here is in-depth technical reading on the topic:
-
-### Why Gitaly was built
-
-If you would like to understand the underlying rationale on why GitLab had to invest in creating Gitaly, read the following minimal list of topics:
-
-- [Git characteristics that make horizontal scaling difficult](https://gitlab.com/gitlab-org/gitaly/-/blob/master/doc/DESIGN.md#git-characteristics-that-make-horizontal-scaling-difficult)
-- [Git architectural characteristics and assumptions](https://gitlab.com/gitlab-org/gitaly/-/blob/master/doc/DESIGN.md#git-architectural-characteristics-and-assumptions)
-- [Affects on horizontal compute architecture](https://gitlab.com/gitlab-org/gitaly/-/blob/master/doc/DESIGN.md#affects-on-horizontal-compute-architecture)
-- [Evidence to back building a new horizontal layer to scale Git](https://gitlab.com/gitlab-org/gitaly/-/blob/master/doc/DESIGN.md#evidence-to-back-building-a-new-horizontal-layer-to-scale-git)
-
-### Gitaly and Praefect elections
-
-As part of Gitaly cluster consistency, Praefect nodes must occasionally vote on what data copy is the most accurate. This requires an uneven number of Praefect nodes to avoid stalemates. This means that for HA, Gitaly and Praefect require a minimum of three nodes.
-
-### Gitaly performance monitoring
-
-Complete performance metrics should be collected for Gitaly instances for identification of bottlenecks, as they could have to do with disk IO, network IO, or memory.
-
-### Gitaly performance guidelines
-
-Gitaly functions as the primary Git Repository Storage in GitLab. However, it's not a streaming file server. It also does a lot of demanding computing work, such as preparing and caching Git packfiles which informs some of the performance recommendations below.
-
-NOTE:
-All recommendations are for production configurations, including performance testing. For test configurations, like training or functional testing, you can use less expensive options. However, you should adjust or rebuild if performance is an issue.
-
-#### Overall recommendations
-
-- Production-grade Gitaly must be implemented on instance compute due to all of the above and below characteristics.
-- Never use [burstable instance types](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/burstable-performance-instances.html) (such as `t2`, `t3`, `t4g`) for Gitaly.
-- Always use at least the [AWS Nitro generation of instances](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-types.html#ec2-nitro-instances) to ensure many of the below concerns are automatically handled.
-- Use Amazon Linux 2 to ensure that all [AWS oriented hardware and OS optimizations](https://aws.amazon.com/amazon-linux-2/faqs/) are maximized without additional configuration or SRE management.
-
-#### CPU and memory recommendations
-
-- The general GitLab Gitaly node recommendations for CPU and Memory assume relatively even loading across repositories. GitLab Performance Tool (GPT) testing of any non-characteristic repositories and/or SRE monitoring of Gitaly metrics may inform when to choose memory and/or CPU higher than general recommendations.
-
-**To accommodate:**
-
-- Git packfile operations are memory and CPU intensive.
-- If repository commit traffic is dense, large, or very frequent, then more CPU and Memory are required to handle the load. Patterns such as storing binaries and/or busy or large monorepos are examples that can cause high loading.
-
-#### Disk I/O recommendations
-
-- Use only SSD storage and the [class of Elastic Block Store (EBS) storage](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-volume-types.html) that suites your durability and speed requirements.
-- When not using provisioned EBS IO, EBS volume size determines the I/O level, so provisioning volumes that are much larger than needed can be the least expensive way to improve EBS IO.
-- If Gitaly performance monitoring shows signs of disk stress then one of the provisioned IOPS levels can be chosen. EBS IOPS levels also have enhanced durability which may be appealing for some implementations aside from performance considerations.
-
-**To accommodate:**
-
-- Gitaly storage is expected to be local (not NFS of any type including EFS).
-- Gitaly servers also need disk space for building and caching Git packfiles. This is above and beyond the permanent storage of your Git Repositories.
-- Git packfiles are cached in Gitaly. Creation of packfiles in temporary disk benefits from fast disk, and disk caching of packfiles benefits from ample disk space.
-
-#### Network I/O recommendations
-
-- Use only instance types [from the list of ones that support Elastic Network Adapter (ENA) advanced networking](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-types.html#instance-type-summary-table) to ensure that cluster replication latency is not due to instance level network I/O bottlenecks.
-- Choose instances with sizes with more than 10 Gbps - but only if needed and only when having proven a node level network bottleneck with monitoring and/or stress testing.
-
-**To accommodate:**
-
-- Gitaly nodes do the main work of streaming repositories for push and pull operations (to add development endpoints, and to CI/CD).
-- Gitaly servers need reasonable low latency between cluster nodes and with Praefect services in order for the cluster to maintain operational and data integrity.
-- Gitaly nodes should be selected with network bottleneck avoidance as a primary consideration.
-- Gitaly nodes should be monitored for network saturation.
-- Not all networking issues can be solved through optimizing the node level networking:
- - Gitaly cluster node replication depends on all networking between nodes.
- - Gitaly networking performance to pull and push endpoints depends on all networking in between.
-
-### AWS Gitaly backup
-
-Due to the nature of how Praefect tracks the replication metadata of Gitaly disk information, the best backup method is [the official backup and restore Rake tasks](../../administration/backup_restore/index.md).
-
-### AWS Gitaly recovery
-
-Gitaly Cluster does not support snapshot backups as these can cause issues where the Praefect database becomes out of syn with the disk storage. Due to the nature of how Praefect rebuilds the replication metadata of Gitaly disk information during a restore, the best recovery method is [the official backup and restore Rake tasks](../../administration/backup_restore/index.md).
-
-### Gitaly HA in EKS quick start
-
-The [AWS GitLab Cloud Native Hybrid on EKS Quick Start](gitlab_hybrid_on_aws.md#available-infrastructure-as-code-for-gitlab-cloud-native-hybrid) for GitLab Cloud Native implements Gitaly as a multi-zone, self-healing infrastructure. It has specific code for reestablishing a Gitaly node when one fails, including AZ failure.
-
-### Gitaly long term management
-
-Gitaly node disk sizes must be monitored and increased to accommodate Git repository growth and Gitaly temporary and caching storage needs. The storage configuration on all nodes should be kept identical.
+<!-- This redirect file can be deleted after <YYYY-MM-DD>. -->
+<!-- Redirects that point to other docs in the same project expire in three months. -->
+<!-- Redirects that point to docs in a different project or site (for example, link is not relative and starts with `https:`) expire in one year. -->
+<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/redirects.html --> \ No newline at end of file
diff --git a/doc/install/aws/index.md b/doc/install/aws/index.md
index febe54a8bb6..2c1f2529426 100644
--- a/doc/install/aws/index.md
+++ b/doc/install/aws/index.md
@@ -3,167 +3,857 @@ stage: Systems
group: Distribution
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
description: Read through the GitLab installation methods.
-type: index
---
-# AWS implementation patterns **(FREE SELF)**
+{::options parse_block_html="true" /}
-GitLab [Reference Architectures](../../administration/reference_architectures/index.md) give qualified and tested guidance on the recommended ways GitLab can be configured to meet the performance requirements of various workloads. Reference Architectures are purpose-designed to be non-implementation specific so they can be extrapolated to as many environments as possible. This generally means they have a highly-granular "machine" to "server role" specification and focus on system elements that impact performance. This is what enables Reference Architectures to be adaptable to the broadest number of supported implementations.
+# Installing a GitLab POC on Amazon Web Services (AWS) **(FREE SELF)**
-Implementation patterns are built on the foundational information and testing done for Reference Architectures and allow architects and implementers at GitLab, GitLab Customers, and GitLab Partners to build out deployments with less experimentation and a higher degree of confidence that the results perform as expected. A more thorough discussion of implementation patterns is below in [Additional details on implementation patterns](#additional-details-on-implementation-patterns).
+This page offers a walkthrough of a common configuration for GitLab on AWS using the official Linux package. You should customize it to accommodate your needs.
-## AWS Implementation patterns information
+NOTE:
+For organizations with 1,000 users or less, the recommended AWS installation method is to launch an EC2 single box [Linux package installation](https://about.gitlab.com/install/) and implement a snapshot strategy for backing up the data. See the [1,000 user reference architecture](../../administration/reference_architectures/1k_users.md) for more information.
+
+## Getting started for production-grade GitLab
+
+NOTE:
+This document is an installation guide for a proof of concept instance. It is not a reference architecture and it does not result in a highly available configuration.
+
+Following this guide exactly results in a proof of concept instance that roughly equates to a **scaled down** version of a **two availability zone implementation** of the **Non-HA** [2000 User Reference Architecture](../../administration/reference_architectures/2k_users.md). The 2K reference architecture is not HA because it is primarily intended to provide some scaling while keeping costs and complexity low. The [3000 User Reference Architecture](../../administration/reference_architectures/3k_users.md) is the smallest size that is GitLab HA. It has additional service roles to achieve HA, most notably it uses Gitaly Cluster to achieve HA for Git repository storage and specifies triple redundancy.
+
+GitLab maintains and tests two main types of Reference Architectures. The **Linux package architectures** are implemented on instance compute while **Cloud Native Hybrid architectures** maximize the use of a Kubernetes cluster. Cloud Native Hybrid reference architecture specifications are addendum sections to the Reference Architecture size pages that start by describing the Linux package architecture. For example, the 3000 User Cloud Native Reference Architecture is in the subsection titled [Cloud Native Hybrid reference architecture with Helm Charts (alternative)](../../administration/reference_architectures/3k_users.md#cloud-native-hybrid-reference-architecture-with-helm-charts-alternative) in the 3000 User Reference Architecture page.
+
+### Getting started for production-grade Linux package installations
+
+The Infrastructure as Code tooling [GitLab Environment Tool (GET)](https://gitlab.com/gitlab-org/gitlab-environment-toolkit/-/tree/main) is the best place to start for building using the Linux package on AWS and most especially if you are targeting an HA setup. While it does not automate everything, it does complete complex setups like Gitaly Cluster for you. GET is open source so anyone can build on top of it and contribute improvements to it.
+
+### Getting started for production-grade Cloud Native Hybrid GitLab
+
+The [GitLab Environment Toolkit (GET)](https://gitlab.com/gitlab-org/gitlab-environment-toolkit/-/blob/main/README.md) is a set of opinionated Terraform and Ansible scripts. These scripts help with the deployment of Linux package or Cloud Native Hybrid environments on selected cloud providers and are used by GitLab developers for [GitLab Dedicated](../../subscriptions/gitlab_dedicated/index.md) (for example).
+
+You can use the GitLab Environment Toolkit to deploy a Cloud Native Hybrid environment on AWS. However, it's not required and may not support every valid permutation. That said, the scripts are presented as-is and you can adapt them accordingly.
+
+## Introduction
+
+For the most part, we make use of the Linux package in our setup, but we also leverage native AWS services. Instead of using the Linux package-bundled PostgreSQL and Redis, we use Amazon RDS and ElastiCache.
+
+In this guide, we go through a multi-node setup where we start by
+configuring our Virtual Private Cloud and subnets to later integrate
+services such as RDS for our database server and ElastiCache as a Redis
+cluster to finally manage them in an auto scaling group with custom
+scaling policies.
+
+## Requirements
+
+In addition to having a basic familiarity with [AWS](https://docs.aws.amazon.com/) and [Amazon EC2](https://docs.aws.amazon.com/ec2/), you need:
+
+- [An AWS account](https://console.aws.amazon.com/console/home)
+- [To create or upload an SSH key](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-key-pairs.html)
+ to connect to the instance via SSH
+- A domain name for the GitLab instance
+- An SSL/TLS certificate to secure your domain. If you do not already own one, you can provision a free public SSL/TLS certificate through [AWS Certificate Manager](https://aws.amazon.com/certificate-manager/)(ACM) for use with the [Elastic Load Balancer](#load-balancer) we create.
-The following are the currently available implementation patterns for GitLab when it is implemented on AWS.
+NOTE:
+It can take a few hours to validate a certificate provisioned through ACM. To avoid delays later, request your certificate as soon as possible.
+
+## Architecture
+
+Below is a diagram of the recommended architecture.
+
+![AWS architecture diagram](img/aws_ha_architecture_diagram.png)
+
+## AWS costs
+
+GitLab uses the following AWS services, with links to pricing information:
+
+- **EC2**: GitLab is deployed on shared hardware, for which
+ [on-demand pricing](https://aws.amazon.com/ec2/pricing/on-demand/) applies.
+ If you want to run GitLab on a dedicated or reserved instance, see the
+ [EC2 pricing page](https://aws.amazon.com/ec2/pricing/) for information about
+ its cost.
+- **S3**: GitLab uses S3 ([pricing page](https://aws.amazon.com/s3/pricing/)) to
+ store backups, artifacts, and LFS objects.
+- **ELB**: A Classic Load Balancer ([pricing page](https://aws.amazon.com/elasticloadbalancing/pricing/)),
+ used to route requests to the GitLab instances.
+- **RDS**: An Amazon Relational Database Service using PostgreSQL
+ ([pricing page](https://aws.amazon.com/rds/postgresql/pricing/)).
+- **ElastiCache**: An in-memory cache environment ([pricing page](https://aws.amazon.com/elasticache/pricing/)),
+ used to provide a Redis configuration.
+
+## Create an IAM EC2 instance role and profile
+
+As we are using [Amazon S3 object storage](#amazon-s3-object-storage), our EC2 instances must have read, write, and list permissions for our S3 buckets. To avoid embedding AWS keys in our GitLab configuration, we make use of an [IAM Role](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles.html) to allow our GitLab instance with this access. We must create an IAM policy to attach to our IAM role:
+
+### Create an IAM Policy
+
+1. Go to the IAM dashboard and select **Policies** in the left menu.
+1. Select **Create policy**, select the `JSON` tab, and add a policy. We want to [follow security best practices and grant _least privilege_](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege), giving our role only the permissions needed to perform the required actions.
+ 1. Assuming you prefix the S3 bucket names with `gl-` as shown in the diagram, add the following policy:
+
+ ```json
+ { "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Effect": "Allow",
+ "Action": [
+ "s3:PutObject",
+ "s3:GetObject",
+ "s3:DeleteObject",
+ "s3:PutObjectAcl"
+ ],
+ "Resource": "arn:aws:s3:::gl-*/*"
+ },
+ {
+ "Effect": "Allow",
+ "Action": [
+ "s3:ListBucket",
+ "s3:AbortMultipartUpload",
+ "s3:ListMultipartUploadParts",
+ "s3:ListBucketMultipartUploads"
+ ],
+ "Resource": "arn:aws:s3:::gl-*"
+ }
+ ]
+ }
+ ```
+
+1. Select **Review policy**, give your policy a name (we use `gl-s3-policy`), and select **Create policy**.
+
+### Create an IAM Role
+
+1. Still on the IAM dashboard, select **Roles** in the left menu, and
+ select **Create role**.
+1. Create a new role by selecting **AWS service > EC2**, then select
+ **Next: Permissions**.
+1. In the policy filter, search for the `gl-s3-policy` we created above, select it, and select **Tags**.
+1. Add tags if needed and select **Review**.
+1. Give the role a name (we use `GitLabS3Access`) and select **Create Role**.
+
+We use this role when we [create a launch configuration](#create-a-launch-configuration) later on.
+
+## Configuring the network
+
+We start by creating a VPC for our GitLab cloud infrastructure, then
+we can create subnets to have public and private instances in at least
+two [Availability Zones (AZs)](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html). Public subnets require a Route Table keep and an associated
+Internet Gateway.
-### GitLab Site Reliability Engineering (SRE) for AWS
+### Creating the Virtual Private Cloud (VPC)
-[GitLab Site Reliability Engineering (SRE) for AWS](gitlab_sre_for_aws.md) - information for planning, implementing, upgrading, and long term management of GitLab instances and runners on AWS.
+We now create a VPC, a virtual networking environment that you control:
-### Patterns to Install GitLab Cloud Native Hybrid on AWS EKS (HA)
+1. Sign in to [Amazon Web Services](https://console.aws.amazon.com/vpc/home).
+1. Select **Your VPCs** from the left menu and then select **Create VPC**.
+ At the "Name tag" enter `gitlab-vpc` and at the "IPv4 CIDR block" enter
+ `10.0.0.0/16`. If you don't require dedicated hardware, you can leave
+ "Tenancy" as default. Select **Yes, Create** when ready.
-[Provision GitLab Cloud Native Hybrid on AWS EKS (HA)](gitlab_hybrid_on_aws.md). This document includes instructions, patterns, and automation for installing GitLab Cloud Native Hybrid on AWS EKS. It also includes [Bill of Materials](https://en.wikipedia.org/wiki/Bill_of_materials) listings and links to Infrastructure as Code. GitLab Cloud Native Hybrid is the supported way to put as much of GitLab as possible into Kubernetes.
+ ![Create VPC](img/create_vpc.png)
-### Patterns to Install GitLab by using the Linux package on AWS EC2 (HA)
+1. Select the VPC, select **Actions**, select **Edit DNS resolution**, and enable DNS resolution. Select **Save** when done.
-[Installing a GitLab POC on Amazon Web Services (AWS)](manual_install_aws.md) - instructions for installing GitLab on EC2 instances. Manual instructions to build a GitLab instance or create your own Infrastructure as Code (IaC).
+### Subnets
-### Patterns for EKS cluster provisioning
+Now, let's create some subnets in different Availability Zones. Make sure
+that each subnet is associated to the VPC we just created and
+that CIDR blocks don't overlap. This also
+allows us to enable multi AZ for redundancy.
-[EKS Cluster Provisioning Patterns](eks_clusters_aws.md) - considerations for setting up EKS cluster for runners and for integrating.
+We create private and public subnets to match load balancers and
+RDS instances as well:
-### Patterns for Scaling HA GitLab Runner on AWS EC2 Auto Scaling group (ASG)
+1. Select **Subnets** from the left menu.
+1. Select **Create subnet**. Give it a descriptive name tag based on the IP,
+ for example `gitlab-public-10.0.0.0`, select the VPC we created previously, select an availability zone (we use `us-west-2a`),
+ and at the IPv4 CIDR block let's give it a 24 subnet `10.0.0.0/24`:
+
+ ![Create subnet](img/create_subnet.png)
+
+1. Follow the same steps to create all subnets:
+
+ | Name tag | Type | Availability Zone | CIDR block |
+ | ------------------------- | ------- | ----------------- | ------------- |
+ | `gitlab-public-10.0.0.0` | public | `us-west-2a` | `10.0.0.0/24` |
+ | `gitlab-private-10.0.1.0` | private | `us-west-2a` | `10.0.1.0/24` |
+ | `gitlab-public-10.0.2.0` | public | `us-west-2b` | `10.0.2.0/24` |
+ | `gitlab-private-10.0.3.0` | private | `us-west-2b` | `10.0.3.0/24` |
-The following repository is self-contained in regard to enabling this pattern: [GitLab HA Scaling Runner Vending Machine for AWS EC2 ASG](https://gitlab.com/guided-explorations/aws/gitlab-runner-autoscaling-aws-asg/). The [feature list for this implementation pattern](https://gitlab.com/guided-explorations/aws/gitlab-runner-autoscaling-aws-asg/-/blob/main/FEATURES.md) is good to review to understand the complete value it can deliver.
+1. Once all the subnets are created, enable **Auto-assign IPv4** for the two public subnets:
+ 1. Select each public subnet in turn, select **Actions**, and select **Modify auto-assign IP settings**. Enable the option and save.
-### Patterns for Using GitLab with AWS
+### Internet Gateway
-[The Guided Explorations' subgroup for AWS](https://gitlab.com/guided-explorations/aws) contains a variety of working example projects for:
+Now, still on the same dashboard, go to Internet Gateways and
+create a new one:
-- Using GitLab and AWS together.
-- Running GitLab infrastructure on AWS.
-- Retrieving temporary credentials for access to AWS services.
+1. Select **Internet Gateways** from the left menu.
+1. Select **Create internet gateway**, give it the name `gitlab-gateway` and
+ select **Create**.
+1. Select it from the table, and then under the **Actions** dropdown list choose
+ "Attach to VPC".
+
+ ![Create gateway](img/create_gateway.png)
+
+1. Choose `gitlab-vpc` from the list and hit **Attach**.
+
+### Create NAT Gateways
+
+Instances deployed in our private subnets must connect to the internet for updates, but should not be reachable from the public internet. To achieve this, we make use of [NAT Gateways](https://docs.aws.amazon.com/vpc/latest/userguide/vpc-nat-gateway.html) deployed in each of our public subnets:
-## AWS known issues list
+1. Go to the VPC dashboard and select **NAT Gateways** in the left menu bar.
+1. Select **Create NAT Gateway** and complete the following:
+ 1. **Subnet**: Select `gitlab-public-10.0.0.0` from the dropdown list.
+ 1. **Elastic IP Allocation ID**: Enter an existing Elastic IP or select **Allocate Elastic IP address** to allocate a new IP to your NAT gateway.
+ 1. Add tags if needed.
+ 1. Select **Create NAT Gateway**.
-Known issues are gathered from within GitLab and from customer reported issues. Customers successfully implement GitLab with a variety of "as a Service" components that GitLab has not specifically been designed for, nor has ongoing testing for. While GitLab does take partner technologies very seriously, the highlighting of known issues here is a convenience for implementers and it does not imply that GitLab has targeted compatibility with, nor carries any type of guarantee of running on the partner technology where the issues occur. Consult individual issues to understand the GitLab stance and plans on any given known issue.
+Create a second NAT gateway but this time place it in the second public subnet, `gitlab-public-10.0.2.0`.
-See the [GitLab AWS known issues list](https://gitlab.com/gitlab-com/alliances/aws/public-tracker/-/issues?label_name%5B%5D=AWS+Known+Issue) for a complete list.
+### Route Tables
-## Provision a single GitLab instance on AWS
+#### Public Route Table
-If you want to provision a single GitLab instance on AWS, you have two options:
+We must create a route table for our public subnets to reach the internet via the internet gateway we created in the previous step.
-- The marketplace subscription
-- The official GitLab AMIs
+On the VPC dashboard:
-### Marketplace subscription
+1. Select **Route Tables** from the left menu.
+1. Select **Create Route Table**.
+1. At the "Name tag" enter `gitlab-public` and choose `gitlab-vpc` under "VPC".
+1. Select **Create**.
-GitLab provides a 5 user subscription as an AWS Marketplace subscription to help teams of all sizes to get started with an Ultimate licensed instance in record time. The Marketplace subscription can be easily upgraded to any GitLab licensing via an AWS Marketplace Private Offer, with the convenience of continued AWS billing. No migration is necessary to obtain a larger, non-time based license from GitLab. Per-minute licensing is automatically removed when you accept the private offer.
+We now must add our internet gateway as a new target and have
+it receive traffic from any destination.
-For a tutorial on provisioning a GitLab Instance via a Marketplace Subscription, [use this tutorial](https://gitlab.awsworkshop.io/040_partner_setup.html). The tutorial links to the [GitLab Ultimate Marketplace Listing](https://aws.amazon.com/marketplace/pp/prodview-g6ktjmpuc33zk), but you can also use the [GitLab Premium Marketplace Listing](https://aws.amazon.com/marketplace/pp/prodview-amk6tacbois2k) to provision an instance.
+1. Select **Route Tables** from the left menu and select the `gitlab-public`
+ route to show the options at the bottom.
+1. Select the **Routes** tab, select **Edit routes > Add route** and set `0.0.0.0/0`
+ as the destination. In the target column, select the `gitlab-gateway` we created previously.
+ Select **Save routes** when done.
-### Official GitLab releases as AMIs
+Next, we must associate the **public** subnets to the route table:
-GitLab produces Amazon Machine Images (AMI) during the regular release process. The AMIs can be used for single instance GitLab installation or, by configuring `/etc/gitlab/gitlab.rb`, can be specialized for specific GitLab service roles (for example a Gitaly server). Older releases remain available and can be used to migrate an older GitLab server to AWS.
+1. Select the **Subnet Associations** tab and select **Edit subnet associations**.
+1. Check only the public subnets and select **Save**.
-Initial licensing can either be the Free Enterprise License (EE) or the open source Community Edition (CE). The Enterprise Edition provides the easiest path forward to a licensed version if the need arises.
+#### Private Route Tables
-Currently the Amazon AMI uses the Amazon prepared Ubuntu AMI (x86 and ARM are available) as its starting point.
+We also must create two private route tables so that instances in each private subnet can reach the internet via the NAT gateway in the corresponding public subnet in the same availability zone.
+
+1. Follow the same steps as above to create two private route tables. Name them `gitlab-private-a` and `gitlab-private-b`.
+1. Next, add a new route to each of the private route tables where the destination is `0.0.0.0/0` and the target is one of the NAT gateways we created earlier.
+ 1. Add the NAT gateway we created in `gitlab-public-10.0.0.0` as the target for the new route in the `gitlab-private-a` route table.
+ 1. Similarly, add the NAT gateway in `gitlab-public-10.0.2.0` as the target for the new route in the `gitlab-private-b`.
+1. Lastly, associate each private subnet with a private route table.
+ 1. Associate `gitlab-private-10.0.1.0` with `gitlab-private-a`.
+ 1. Associate `gitlab-private-10.0.3.0` with `gitlab-private-b`.
+
+## Load Balancer
+
+We create a load balancer to evenly distribute inbound traffic on ports `80` and `443` across our GitLab application servers. Based on the [scaling policies](#create-an-auto-scaling-group) we create later, instances are added to or removed from our load balancer as needed. Additionally, the load balancer performs health checks on our instances.
+
+On the EC2 dashboard, look for Load Balancer in the left navigation bar:
+
+1. Select **Create Load Balancer**.
+ 1. Choose the **Classic Load Balancer**.
+ 1. Give it a name (we use `gitlab-loadbalancer`) and for the **Create LB Inside** option, select `gitlab-vpc` from the dropdown list.
+ 1. In the **Listeners** section, set the following listeners:
+ - HTTP port 80 for both load balancer and instance protocol and ports
+ - TCP port 22 for both load balancer and instance protocols and ports
+ - HTTPS port 443 for load balancer protocol and ports, forwarding to HTTP port 80 on the instance (we configure GitLab to listen on port 80 [later in the guide](#add-support-for-proxied-ssl))
+ 1. In the **Select Subnets** section, select both public subnets from the list so that the load balancer can route traffic to both availability zones.
+1. We add a security group for our load balancer to act as a firewall to control what traffic is allowed through. Select **Assign Security Groups** and select **Create a new security group**, give it a name
+ (we use `gitlab-loadbalancer-sec-group`) and description, and allow both HTTP and HTTPS traffic
+ from anywhere (`0.0.0.0/0, ::/0`). Also allow SSH traffic, select a custom source, and add a single trusted IP address or an IP address range in CIDR notation. This allows users to perform Git actions over SSH.
+1. Select **Configure Security Settings** and set the following:
+ 1. Select an SSL/TLS certificate from ACM or upload a certificate to IAM.
+ 1. Under **Select a Cipher**, pick a predefined security policy from the dropdown list. You can see a breakdown of [Predefined SSL Security Policies for Classic Load Balancers](https://docs.aws.amazon.com/elasticloadbalancing/latest/classic/elb-security-policy-table.html) in the AWS documentation. Check the GitLab codebase for a list of [supported SSL ciphers and protocols](https://gitlab.com/gitlab-org/gitlab/-/blob/9ee7ad433269b37251e0dd5b5e00a0f00d8126b4/lib/support/nginx/gitlab-ssl#L97-99).
+1. Select **Configure Health Check** and set up a health check for your EC2 instances.
+ 1. For **Ping Protocol**, select HTTP.
+ 1. For **Ping Port**, enter 80.
+ 1. For **Ping Path** - we recommend that you [use the Readiness check endpoint](../../administration/load_balancer.md#readiness-check). You must add [the VPC IP Address Range (CIDR)](https://docs.aws.amazon.com/elasticloadbalancing/latest/classic/elb-security-groups.html#elb-vpc-nacl) to the [IP allowlist](../../administration/monitoring/ip_allowlist.md) for the [Health Check endpoints](../../administration/monitoring/health_check.md)
+ 1. Keep the default **Advanced Details** or adjust them according to your needs.
+1. Select **Add EC2 Instances** - don't add anything as we create an Auto Scaling Group later to manage instances for us.
+1. Select **Add Tags** and add any tags you need.
+1. Select **Review and Create**, review all your settings, and select **Create** if you're happy.
+
+After the Load Balancer is up and running, you can revisit your Security
+Groups to refine the access only through the ELB and any other requirements
+you might have.
+
+### Configure DNS for Load Balancer
+
+On the Route 53 dashboard, select **Hosted zones** in the left navigation bar:
+
+1. Select an existing hosted zone or, if you do not already have one for your domain, select **Create Hosted Zone**, enter your domain name, and select **Create**.
+1. Select **Create Record Set** and provide the following values:
+ 1. **Name:** Use the domain name (the default value) or enter a subdomain.
+ 1. **Type:** Select **A - IPv4 address**.
+ 1. **Alias:** Defaults to **No**. Select **Yes**.
+ 1. **Alias Target:** Find the **ELB Classic Load Balancers** section and select the classic load balancer we created earlier.
+ 1. **Routing Policy:** We use **Simple** but you can choose a different policy based on your use case.
+ 1. **Evaluate Target Health:** We set this to **No** but you can choose to have the load balancer route traffic based on target health.
+ 1. Select **Create**.
+1. If you registered your domain through Route 53, you're done. If you used a different domain registrar, you must update your DNS records with your domain registrar. You must:
+ 1. Select **Hosted zones** and select the domain you added above.
+ 1. You see a list of `NS` records. From your domain registrar's administrator panel, add each of these as `NS` records to your domain's DNS records. These steps may vary between domain registrars. If you're stuck, Google **"name of your registrar" add DNS records** and you should find a help article specific to your domain registrar.
+
+The steps for doing this vary depending on which registrar you use and is beyond the scope of this guide.
+
+## PostgreSQL with RDS
+
+For our database server we use Amazon RDS for PostgreSQL which offers Multi AZ
+for redundancy (Aurora is **not** supported). First we create a security group and subnet group, then we
+create the actual RDS instance.
+
+### RDS Security Group
+
+We need a security group for our database that allows inbound traffic from the instances we deploy in our `gitlab-loadbalancer-sec-group` later on:
+
+1. From the EC2 dashboard, select **Security Groups** from the left menu bar.
+1. Select **Create security group**.
+1. Give it a name (we use `gitlab-rds-sec-group`), a description, and select the `gitlab-vpc` from the **VPC** dropdown list.
+1. In the **Inbound rules** section, select **Add rule** and set the following:
+ 1. **Type:** search for and select the **PostgreSQL** rule.
+ 1. **Source type:** set as "Custom".
+ 1. **Source:** select the `gitlab-loadbalancer-sec-group` we created earlier.
+1. When done, select **Create security group**.
+
+### RDS Subnet Group
+
+1. Go to the RDS dashboard and select **Subnet Groups** from the left menu.
+1. Select **Create DB Subnet Group**.
+1. Under **Subnet group details**, enter a name (we use `gitlab-rds-group`), a description, and choose the `gitlab-vpc` from the VPC dropdown list.
+1. From the **Availability Zones** dropdown list, select the Availability Zones that include the subnets you've configured. In our case, we add `eu-west-2a` and `eu-west-2b`.
+1. From the **Subnets** dropdown list, select the two private subnets (`10.0.1.0/24` and `10.0.3.0/24`) as we defined them in the [subnets section](#subnets).
+1. Select **Create** when ready.
+
+### Create the database
+
+WARNING:
+Avoid using burstable instances (t class instances) for the database as this could lead to performance issues due to CPU credits running out during sustained periods of high load.
+
+Now, it's time to create the database:
+
+1. Go to the RDS dashboard, select **Databases** from the left menu, and select **Create database**.
+1. Select **Standard Create** for the database creation method.
+1. Select **PostgreSQL** as the database engine and select the minimum PostgreSQL version as defined for your GitLab version in our [database requirements](../../install/requirements.md#postgresql-requirements).
+1. Because this is a production server, let's choose **Production** from the **Templates** section.
+1. Under **Settings**, use:
+ - `gitlab-db-ha` for the DB instance identifier.
+ - `gitlab` for a master username.
+ - A very secure password for the master password.
+
+ Make a note of these as we need them later.
+
+1. For the DB instance size, select **Standard classes** and select an instance size that meets your requirements from the dropdown list. We use a `db.m4.large` instance.
+1. Under **Storage**, configure the following:
+ 1. Select **Provisioned IOPS (SSD)** from the storage type dropdown list. Provisioned IOPS (SSD) storage is best suited for this use (though you can choose General Purpose (SSD) to reduce the costs). Read more about it at [Storage for Amazon RDS](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Storage.html).
+ 1. Allocate storage and set provisioned IOPS. We use the minimum values, `100` and `1000`, respectively.
+ 1. Enable storage autoscaling (optional) and set a maximum storage threshold.
+1. Under **Availability & durability**, select **Create a standby instance** to have a standby RDS instance provisioned in a different [Availability Zone](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.MultiAZ.html).
+1. Under **Connectivity**, configure the following:
+ 1. Select the VPC we created earlier (`gitlab-vpc`) from the **Virtual Private Cloud (VPC)** dropdown list.
+ 1. Expand the **Additional connectivity configuration** section and select the subnet group (`gitlab-rds-group`) we created earlier.
+ 1. Set public accessibility to **No**.
+ 1. Under **VPC security group**, select **Choose existing** and select the `gitlab-rds-sec-group` we create above from the dropdown list.
+ 1. Leave the database port as the default `5432`.
+1. For **Database authentication**, select **Password authentication**.
+1. Expand the **Additional configuration** section and complete the following:
+ 1. The initial database name. We use `gitlabhq_production`.
+ 1. Configure your preferred backup settings.
+ 1. The only other change we make here is to disable auto minor version updates under **Maintenance**.
+ 1. Leave all the other settings as is or tweak according to your needs.
+ 1. If you're happy, select **Create database**.
+
+Now that the database is created, let's move on to setting up Redis with ElastiCache.
+
+## Redis with ElastiCache
+
+ElastiCache is an in-memory hosted caching solution. Redis maintains its own
+persistence and is used to store session data, temporary cache information, and background job queues for the GitLab application.
+
+### Create a Redis Security Group
+
+1. Go to the EC2 dashboard.
+1. Select **Security Groups** from the left menu.
+1. Select **Create security group** and fill in the details. Give it a name (we use `gitlab-redis-sec-group`),
+ add a description, and choose the VPC we created previously
+1. In the **Inbound rules** section, select **Add rule** and add a **Custom TCP** rule, set port `6379`, and set the "Custom" source as the `gitlab-loadbalancer-sec-group` we created earlier.
+1. When done, select **Create security group**.
+
+### Redis Subnet Group
+
+1. Go to the ElastiCache dashboard from your AWS console.
+1. Go to **Subnet Groups** in the left menu, and create a new subnet group (we name ours `gitlab-redis-group`).
+ Make sure to select our VPC and its [private subnets](#subnets).
+1. Select **Create** when ready.
+
+ ![ElastiCache subnet](img/ec_subnet.png)
+
+### Create the Redis Cluster
+
+1. Go back to the ElastiCache dashboard.
+1. Select **Redis** on the left menu and select **Create** to create a new
+ Redis cluster. Do not enable **Cluster Mode** as it is [not supported](../../administration/redis/replication_and_failover_external.md#requirements). Even without cluster mode on, you still get the
+ chance to deploy Redis in multiple availability zones.
+1. In the settings section:
+ 1. Give the cluster a name (`gitlab-redis`) and a description.
+ 1. For the version, select the latest.
+ 1. Leave the port as `6379` because this is what we used in our Redis security group above.
+ 1. Select the node type (at least `cache.t3.medium`, but adjust to your needs) and the number of replicas.
+1. In the advanced settings section:
+ 1. Select the multi-AZ auto-failover option.
+ 1. Select the subnet group we created previously.
+ 1. Manually select the preferred availability zones, and under "Replica 2"
+ choose a different zone than the other two.
+
+ ![Redis availability zones](img/ec_az.png)
+
+1. In the security settings, edit the security groups and choose the
+ `gitlab-redis-sec-group` we had previously created.
+1. Leave the rest of the settings to their default values or edit to your liking.
+1. When done, select **Create**.
+
+## Setting up Bastion Hosts
+
+Because our GitLab instances are in private subnets, we need a way to connect
+to these instances with SSH for actions that include making configuration changes
+and performing upgrades. One way of doing this is by using a [bastion host](https://en.wikipedia.org/wiki/Bastion_host),
+sometimes also referred to as a jump box.
NOTE:
-When deploying a GitLab instance using the official AMI, the root password to the instance is the EC2 **Instance** ID (not the AMI ID). This way of setting the root account password is specific to official GitLab published AMIs ONLY.
+If you do not want to maintain bastion hosts, you can set up [AWS Systems Manager Session Manager](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager.html) for access to instances. This is beyond the scope of this document.
+
+### Create Bastion Host A
+
+1. Go to the EC2 Dashboard and select **Launch instance**.
+1. Select the **Ubuntu Server 18.04 LTS (HVM)** AMI.
+1. Choose an instance type. We use a `t2.micro` as we only use the bastion host to SSH into our other instances.
+1. Select **Configure Instance Details**.
+ 1. Under **Network**, select the `gitlab-vpc` from the dropdown list.
+ 1. Under **Subnet**, select the public subnet we created earlier (`gitlab-public-10.0.0.0`).
+ 1. Double check that under **Auto-assign Public IP** you have **Use subnet setting (Enable)** selected.
+ 1. Leave everything else as default and select **Add Storage**.
+1. For storage, we leave everything as default and only add an 8GB root volume. We do not store anything on this instance.
+1. Select **Add Tags** and on the next screen select **Add Tag**.
+ 1. We only set `Key: Name` and `Value: Bastion Host A`.
+1. Select **Configure Security Group**.
+ 1. Select **Create a new security group**, enter a **Security group name** (we use `bastion-sec-group`), and add a description.
+ 1. We enable SSH access from anywhere (`0.0.0.0/0`). If you want stricter security, specify a single IP address or an IP address range in CIDR notation.
+ 1. Select **Review and Launch**
+1. Review all your settings and, if you're happy, select **Launch**.
+1. Acknowledge that you have access to an existing key pair or create a new one. Select **Launch Instance**.
+
+Confirm that you can SSH into the instance:
+
+1. On the EC2 Dashboard, select **Instances** in the left menu.
+1. Select **Bastion Host A** from your list of instances.
+1. Select **Connect** and follow the connection instructions.
+1. If you are able to connect successfully, let's move on to setting up our second bastion host for redundancy.
+
+### Create Bastion Host B
+
+1. Create an EC2 instance following the same steps as above with the following changes:
+ 1. For the **Subnet**, select the second public subnet we created earlier (`gitlab-public-10.0.2.0`).
+ 1. Under the **Add Tags** section, we set `Key: Name` and `Value: Bastion Host B` so that we can easily identify our two instances.
+ 1. For the security group, select the existing `bastion-sec-group` we created above.
+
+### Use SSH Agent Forwarding
+
+EC2 instances running Linux use private key files for SSH authentication. You connect to your bastion host using an SSH client and the private key file stored on your client. Because the private key file is not present on the bastion host, you are not able to connect to your instances in private subnets.
+
+Storing private key files on your bastion host is a bad idea. To get around this, use SSH agent forwarding on your client. See [Securely Connect to Linux Instances Running in a Private Amazon VPC](https://aws.amazon.com/blogs/security/securely-connect-to-linux-instances-running-in-a-private-amazon-vpc/) for a step-by-step guide on how to use SSH agent forwarding.
+
+## Install GitLab and create custom AMI
+
+We need a preconfigured, custom GitLab AMI to use in our launch configuration later. As a starting point, we use the official GitLab AMI to create a GitLab instance. Then, we add our custom configuration for PostgreSQL, Redis, and Gitaly. If you prefer, instead of using the official GitLab AMI, you can also spin up an EC2 instance of your choosing and [manually install GitLab](https://about.gitlab.com/install/).
+
+### Install GitLab
+
+From the EC2 dashboard:
+
+1. Use the section below titled "[Find official GitLab-created AMI IDs on AWS](#find-official-gitlab-created-ami-ids-on-aws)" to find the correct AMI to launch.
+1. After selecting **Launch** on the desired AMI, select an instance type based on your workload. Consult the [hardware requirements](../../install/requirements.md#hardware-requirements) to choose one that fits your needs (at least `c5.xlarge`, which is sufficient to accommodate 100 users).
+1. Select **Configure Instance Details**:
+ 1. In the **Network** dropdown list, select `gitlab-vpc`, the VPC we created earlier.
+ 1. In the **Subnet** dropdown list, select `gitlab-private-10.0.1.0` from the list of subnets we created earlier.
+ 1. Double check that **Auto-assign Public IP** is set to `Use subnet setting (Disable)`.
+ 1. Select **Add Storage**.
+ 1. The root volume is 8GiB by default and should be enough given that we do not store any data there.
+1. Select **Add Tags** and add any tags you may need. In our case, we only set `Key: Name` and `Value: GitLab`.
+1. Select **Configure Security Group**. Check **Select an existing security group** and select the `gitlab-loadbalancer-sec-group` we created earlier.
+1. Select **Review and launch** followed by **Launch** if you're happy with your settings.
+1. Finally, acknowledge that you have access to the selected private key file or create a new one. Select **Launch Instances**.
+
+### Add custom configuration
+
+Connect to your GitLab instance via **Bastion Host A** using [SSH Agent Forwarding](#use-ssh-agent-forwarding). Once connected, add the following custom configuration:
+
+#### Disable Let's Encrypt
+
+Because we're adding our SSL certificate at the load balancer, we do not need the GitLab built-in support for Let's Encrypt. Let's Encrypt [is enabled by default](https://docs.gitlab.com/omnibus/settings/ssl/index.html#enable-the-lets-encrypt-integration) when using an `https` domain in GitLab 10.7 and later, so we must explicitly disable it:
+
+1. Open `/etc/gitlab/gitlab.rb` and disable it:
+
+ ```ruby
+ letsencrypt['enable'] = false
+ ```
+
+1. Save the file and reconfigure for the changes to take effect:
+
+ ```shell
+ sudo gitlab-ctl reconfigure
+ ```
+
+#### Install the required extensions for PostgreSQL
+
+From your GitLab instance, connect to the RDS instance to verify access and to install the required `pg_trgm` and `btree_gist` extensions.
+
+To find the host or endpoint, go to **Amazon RDS > Databases** and select the database you created earlier. Look for the endpoint under the **Connectivity & security** tab.
+
+Do not to include the colon and port number:
+
+```shell
+sudo /opt/gitlab/embedded/bin/psql -U gitlab -h <rds-endpoint> -d gitlabhq_production
+```
+
+At the `psql` prompt create the extension and then quit the session:
+
+```shell
+psql (10.9)
+Type "help" for help.
+
+gitlab=# CREATE EXTENSION pg_trgm;
+gitlab=# CREATE EXTENSION btree_gist;
+gitlab=# \q
+```
+
+#### Configure GitLab to connect to PostgreSQL and Redis
+
+1. Edit `/etc/gitlab/gitlab.rb`, find the `external_url 'http://<domain>'` option
+ and change it to the `https` domain you are using.
+
+1. Look for the GitLab database settings and uncomment as necessary. In
+ our current case we specify the database adapter, encoding, host, name,
+ username, and password:
+
+ ```ruby
+ # Disable the built-in Postgres
+ postgresql['enable'] = false
+
+ # Fill in the connection details
+ gitlab_rails['db_adapter'] = "postgresql"
+ gitlab_rails['db_encoding'] = "unicode"
+ gitlab_rails['db_database'] = "gitlabhq_production"
+ gitlab_rails['db_username'] = "gitlab"
+ gitlab_rails['db_password'] = "mypassword"
+ gitlab_rails['db_host'] = "<rds-endpoint>"
+ ```
+
+1. Next, we must configure the Redis section by adding the host and
+ uncommenting the port:
-Instances running on Community Edition (CE) require a migration to Enterprise Edition (EE) to subscribe to the GitLab Premium or Ultimate plan. If you want to pursue a subscription, using the Free-forever plan of Enterprise Edition is the least disruptive method.
+ ```ruby
+ # Disable the built-in Redis
+ redis['enable'] = false
+
+ # Fill in the connection details
+ gitlab_rails['redis_host'] = "<redis-endpoint>"
+ gitlab_rails['redis_port'] = 6379
+ ```
+
+1. Finally, reconfigure GitLab for the changes to take effect:
+
+ ```shell
+ sudo gitlab-ctl reconfigure
+ ```
+
+1. You can also run a check and a service status to make sure
+ everything has been setup correctly:
+
+ ```shell
+ sudo gitlab-rake gitlab:check
+ sudo gitlab-ctl status
+ ```
+
+#### Set up Gitaly
+
+WARNING:
+In this architecture, having a single Gitaly server creates a single point of failure. Use
+[Gitaly Cluster](../../administration/gitaly/praefect.md) to remove this limitation.
+
+Gitaly is a service that provides high-level RPC access to Git repositories.
+It should be enabled and configured on a separate EC2 instance in one of the
+[private subnets](#subnets) we configured previously.
+
+Let's create an EC2 instance where we install Gitaly:
+
+1. From the EC2 dashboard, select **Launch instance**.
+1. Choose an AMI. In this example, we select the **Ubuntu Server 18.04 LTS (HVM), SSD Volume Type**.
+1. Choose an instance type. We pick a `c5.xlarge`.
+1. Select **Configure Instance Details**.
+ 1. In the **Network** dropdown list, select `gitlab-vpc`, the VPC we created earlier.
+ 1. In the **Subnet** dropdown list, select `gitlab-private-10.0.1.0` from the list of subnets we created earlier.
+ 1. Double check that **Auto-assign Public IP** is set to `Use subnet setting (Disable)`.
+ 1. Select **Add Storage**.
+1. Increase the Root volume size to `20 GiB` and change the **Volume Type** to `Provisioned IOPS SSD (io1)`. (This is an arbitrary size. Create a volume big enough for your repository storage requirements.)
+ 1. For **IOPS** set `1000` (20 GiB x 50 IOPS). You can provision up to 50 IOPS per GiB. If you select a larger volume, increase the IOPS accordingly. Workloads where many small files are written in a serialized manner, like `git`, requires performant storage, hence the choice of `Provisioned IOPS SSD (io1)`.
+1. Select **Add Tags** and add your tags. In our case, we only set `Key: Name` and `Value: Gitaly`.
+1. Select **Configure Security Group** and let's **Create a new security group**.
+ 1. Give your security group a name and description. We use `gitlab-gitaly-sec-group` for both.
+ 1. Create a **Custom TCP** rule and add port `8075` to the **Port Range**. For the **Source**, select the `gitlab-loadbalancer-sec-group`.
+ 1. Also add an inbound rule for SSH from the `bastion-sec-group` so that we can connect using [SSH Agent Forwarding](#use-ssh-agent-forwarding) from the Bastion hosts.
+1. Select **Review and launch** followed by **Launch** if you're happy with your settings.
+1. Finally, acknowledge that you have access to the selected private key file or create a new one. Select **Launch Instances**.
NOTE:
-Because any given GitLab upgrade might involve data disk updates or database schema upgrades, swapping out the AMI is not sufficient for taking upgrades.
+Instead of storing configuration _and_ repository data on the root volume, you can also choose to add an additional EBS volume for repository storage. Follow the same guidance as above. See the [Amazon EBS pricing](https://aws.amazon.com/ebs/pricing/). We do not recommend using EFS as it may negatively impact the performance of GitLab. You can review the [relevant documentation](../../administration/nfs.md#avoid-using-cloud-based-file-systems) for more details.
+
+Now that we have our EC2 instance ready, follow the [documentation to install GitLab and set up Gitaly on its own server](../../administration/gitaly/configure_gitaly.md#run-gitaly-on-its-own-server). Perform the client setup steps from that document on the [GitLab instance we created](#install-gitlab) above.
+
+#### Add Support for Proxied SSL
+
+As we are terminating SSL at our [load balancer](#load-balancer), follow the steps at [Supporting proxied SSL](https://docs.gitlab.com/omnibus/settings/ssl/index.html#configure-a-reverse-proxy-or-load-balancer-ssl-termination) to configure this in `/etc/gitlab/gitlab.rb`.
+
+Remember to run `sudo gitlab-ctl reconfigure` after saving the changes to the `gitlab.rb` file.
+
+#### Fast lookup of authorized SSH keys
+
+The public SSH keys for users allowed to access GitLab are stored in `/var/opt/gitlab/.ssh/authorized_keys`. Typically we'd use shared storage so that all the instances are able to access this file when a user performs a Git action over SSH. Because we do not have shared storage in our setup, we update our configuration to authorize SSH users via indexed lookup in the GitLab database.
+
+Follow the instructions at [Set up fast SSH key lookup](../../administration/operations/fast_ssh_key_lookup.md#set-up-fast-lookup) to switch from using the `authorized_keys` file to the database.
-1. Log in to the AWS Web Console, so that selecting the links in the following step take you directly to the AMI list.
-1. Pick the edition you want:
+If you do not configure fast lookup, Git actions over SSH results in the following error:
- - [GitLab Enterprise Edition](https://console.aws.amazon.com/ec2/v2/home?region=us-east-1#Images:visibility=public-images;ownerAlias=782774275127;search=GitLab%20EE;sort=desc:name): If you want to unlock the enterprise features, a license is needed.
- - [GitLab Community Edition](https://console.aws.amazon.com/ec2/v2/home?region=us-east-1#Images:visibility=public-images;ownerAlias=782774275127;search=GitLab%20CE;sort=desc:name): The open source version of GitLab.
- - [GitLab Premium or Ultimate Marketplace (pre-licensed)](https://console.aws.amazon.com/ec2/v2/home?region=us-east-1#Images:visibility=public-images;source=Marketplace;search=GitLab%20EE;sort=desc:name): 5 user license built into per-minute billing.
+```shell
+Permission denied (publickey).
+fatal: Could not read from remote repository.
-1. AMI IDs are unique per region. After you've loaded any of these editions, in the upper-right corner, select the desired target region of the console to see the appropriate AMIs.
-1. After the console is loaded, you can add additional search criteria to narrow further. For instance, type `13.` to find only 13.x versions.
-1. To launch an EC2 Machine with one of the listed AMIs, check the box at the start of the relevant row, and select **Launch** near the top of left of the page.
+Please make sure you have the correct access rights
+and the repository exists.
+```
+
+#### Configure host keys
+
+Ordinarily we would manually copy the contents (primary and public keys) of `/etc/ssh/` on the primary application server to `/etc/ssh` on all secondary servers. This prevents false man-in-the-middle-attack alerts when accessing servers in your cluster behind a load balancer.
+
+We automate this by creating static host keys as part of our custom AMI. As these host keys are also rotated every time an EC2 instance boots up, "hard coding" them into our custom AMI serves as a workaround.
+
+On your GitLab instance run the following:
+
+```shell
+sudo mkdir /etc/ssh_static
+sudo cp -R /etc/ssh/* /etc/ssh_static
+```
+
+In `/etc/ssh/sshd_config` update the following:
+
+```shell
+# HostKeys for protocol version 2
+HostKey /etc/ssh_static/ssh_host_rsa_key
+HostKey /etc/ssh_static/ssh_host_dsa_key
+HostKey /etc/ssh_static/ssh_host_ecdsa_key
+HostKey /etc/ssh_static/ssh_host_ed25519_key
+```
+
+#### Amazon S3 object storage
+
+Because we're not using NFS for shared storage, we use [Amazon S3](https://aws.amazon.com/s3/) buckets to store backups, artifacts, LFS objects, uploads, merge request diffs, container registry images, and more. Our documentation includes [instructions on how to configure object storage](../../administration/object_storage.md) for each of these data types, and other information about using object storage with GitLab.
NOTE:
-If you are trying to restore from an older version of GitLab while moving to AWS, find the
-[Enterprise and Community Editions before GitLab 11.10.3](https://console.aws.amazon.com/ec2/v2/home?region=us-east-1#Images:visibility=public-images;ownerAlias=855262394183;sort=desc:name).
+Because we are using the [AWS IAM profile](#create-an-iam-role) we created earlier, be sure to omit the AWS access key and secret access key/value pairs when configuring object storage. Instead, use `'use_iam_profile' => true` in your configuration as shown in the object storage documentation linked above.
+
+Remember to run `sudo gitlab-ctl reconfigure` after saving the changes to the `gitlab.rb` file.
+
+---
+
+That concludes the configuration changes for our GitLab instance. Next, we create a custom AMI based on this instance to use for our launch configuration and auto scaling group.
+
+### Log in for the first time
+
+Using the domain name you used when setting up [DNS for the load balancer](#configure-dns-for-load-balancer), you should now be able to visit GitLab in your browser.
+
+Depending on how you installed GitLab and if you did not change the password by any other means, the default password is either:
-## Additional details on implementation patterns
+- Your instance ID if you used the official GitLab AMI.
+- A randomly generated password stored for 24 hours in `/etc/gitlab/initial_root_password`.
-GitLab implementation patterns build upon [GitLab Reference Architectures](../../administration/reference_architectures/index.md) in the following ways.
+To change the default password, log in as the `root` user with the default password and [change it in the user profile](../../user/profile/user_passwords.md#change-your-password).
-### Cloud platform well architected compliance
+When our [auto scaling group](#create-an-auto-scaling-group) spins up new instances, we are able to sign in with username `root` and the newly created password.
-Testing-backed architectural qualification is a fundamental concept behind implementation patterns:
+### Create custom AMI
-- Implementation patterns maintain GitLab Reference Architecture compliance and provide [GitLab Performance Tool](https://gitlab.com/gitlab-org/quality/performance) (GPT) reports to demonstrate adherence to them.
-- Implementation patterns may be qualified by and/or contributed to by the technology vendor. For instance, an implementation pattern for AWS may be officially reviewed by AWS.
-- Implementation patterns may specify and test Cloud Platform PaaS services for suitability for GitLab. This testing can be coordinated and help qualify these technologies for Reference Architectures. For instance, qualifying compatibility with and availability of runtime versions of top level PaaS such as those for PostgreSQL and Redis.
-- Implementation patterns can provided qualified testing for platform limitations, for example, ensuring Gitaly Cluster can work correctly on specific Cloud Platform availability zone latency and throughput characteristics or qualifying what levels of available platform partner local disk performance is workable for Gitaly server to operate with integrity.
+On the EC2 dashboard:
-### Platform partner specificity
+1. Select the `GitLab` instance we [created earlier](#install-gitlab).
+1. Select **Actions**, scroll down to **Image** and select **Create Image**.
+1. Give your image a name and description (we use `GitLab-Source` for both).
+1. Leave everything else as default and select **Create Image**
-Implementation patterns enable platform-specific terminology, best practice architecture, and platform-specific build manifests:
+Now we have a custom AMI that we use to create our launch configuration the next step.
-- Implementation patterns are more vendor specific. For instance, advising specific compute instances / VMs / nodes instead of vCPUs or other generalized measures.
-- Implementation patterns are oriented to implementing good architecture for the vendor in view.
-- Implementation patterns are written to an audience who is familiar with building on the infrastructure that the implementation pattern targets. For example, if the implementation pattern is for GCP, the specific terminology of GCP is used - including using the specific names for PaaS services.
-- Implementation patterns can test and qualify if the versions of PaaS available are compatible with GitLab (for example, PostgreSQL, Redis, etc.).
+## Deploy GitLab inside an auto scaling group
+
+### Create a launch configuration
+
+From the EC2 dashboard:
+
+1. Select **Launch Configurations** from the left menu and select **Create launch configuration**.
+1. Select **My AMIs** from the left menu and select the `GitLab` custom AMI we created above.
+1. Select an instance type best suited for your needs (at least a `c5.xlarge`) and select **Configure details**.
+1. Enter a name for your launch configuration (we use `gitlab-ha-launch-config`).
+1. **Do not** check **Request Spot Instance**.
+1. From the **IAM Role** dropdown list, pick the `GitLabAdmin` instance role we [created earlier](#create-an-iam-ec2-instance-role-and-profile).
+1. Leave the rest as defaults and select **Add Storage**.
+1. The root volume is 8GiB by default and should be enough given that we do not store any data there. Select **Configure Security Group**.
+1. Check **Select and existing security group** and select the `gitlab-loadbalancer-sec-group` we created earlier.
+1. Select **Review**, review your changes, and select **Create launch configuration**.
+1. Acknowledge that you have access to the private key or create a new one. Select **Create launch configuration**.
+
+### Create an auto scaling group
+
+1. After the launch configuration is created, select **Create an Auto Scaling group using this launch configuration** to start creating the auto scaling group.
+1. Enter a **Group name** (we use `gitlab-auto-scaling-group`).
+1. For **Group size**, enter the number of instances you want to start with (we enter `2`).
+1. Select the `gitlab-vpc` from the **Network** dropdown list.
+1. Add both the private [subnets we created earlier](#subnets).
+1. Expand the **Advanced Details** section and check the **Receive traffic from one or more load balancers** option.
+1. From the **Classic Load Balancers** dropdown list, select the load balancer we created earlier.
+1. For **Health Check Type**, select **ELB**.
+1. We leave our **Health Check Grace Period** as the default `300` seconds. Select **Configure scaling policies**.
+1. Check **Use scaling policies to adjust the capacity of this group**.
+1. For this group we scale between 2 and 4 instances where one instance is added if CPU
+utilization is greater than 60% and one instance is removed if it falls
+to less than 45%.
+
+![Auto scaling group policies](img/policies.png)
+
+1. Finally, configure notifications and tags as you see fit, review your changes, and create the
+auto scaling group.
+
+As the auto scaling group is created, you see your new instances spinning up in your EC2 dashboard. You also see the new instances added to your load balancer. After the instances pass the heath check, they are ready to start receiving traffic from the load balancer.
+
+Because our instances are created by the auto scaling group, go back to your instances and terminate the [instance we created manually above](#install-gitlab). We only needed this instance to create our custom AMI.
+
+## Health check and monitoring with Prometheus
+
+Apart from Amazon's Cloudwatch which you can enable on various services,
+GitLab provides its own integrated monitoring solution based on Prometheus.
+For more information about how to set it up, see
+[GitLab Prometheus](../../administration/monitoring/prometheus/index.md).
+
+GitLab also has various [health check endpoints](../../administration/monitoring/health_check.md)
+that you can ping and get reports.
+
+## GitLab Runner
+
+If you want to take advantage of [GitLab CI/CD](../../ci/index.md), you have to
+set up at least one [runner](https://docs.gitlab.com/runner/).
+
+Read more on configuring an
+[autoscaling GitLab Runner on AWS](https://docs.gitlab.com/runner/configuration/runner_autoscale_aws/).
+
+## Backup and restore
+
+GitLab provides [a tool to back up](../../administration/backup_restore/index.md)
+and restore its Git data, database, attachments, LFS objects, and so on.
+
+Some important things to know:
+
+- The backup/restore tool **does not** store some configuration files, like secrets; you
+ must [configure this yourself](../../administration/backup_restore/backup_gitlab.md#storing-configuration-files).
+- By default, the backup files are stored locally, but you can
+ [backup GitLab using S3](../../administration/backup_restore/backup_gitlab.md#using-amazon-s3).
+- You can [exclude specific directories form the backup](../../administration/backup_restore/backup_gitlab.md#excluding-specific-directories-from-the-backup).
+
+### Backing up GitLab
+
+To back up GitLab:
+
+1. SSH into your instance.
+1. Take a backup:
+
+ ```shell
+ sudo gitlab-backup create
+ ```
+
+NOTE:
+For GitLab 12.1 and earlier, use `gitlab-rake gitlab:backup:create`.
+
+### Restoring GitLab from a backup
+
+To restore GitLab, first review the [restore documentation](../../administration/backup_restore/index.md#restore-gitlab),
+and primarily the restore prerequisites. Then, follow the steps under the
+[Linux package installations section](../../administration/backup_restore/restore_gitlab.md#restore-for-linux-package-installations).
+
+## Updating GitLab
+
+GitLab releases a new version every month on the [release date](https://about.gitlab.com/releases/). Whenever a new version is
+released, you can update your GitLab instance:
+
+1. SSH into your instance
+1. Take a backup:
+
+ ```shell
+ sudo gitlab-backup create
+ ```
+
+NOTE:
+For GitLab 12.1 and earlier, use `gitlab-rake gitlab:backup:create`.
-### Platform as a Service (PaaS) specification and usage
+1. Update the repositories and install GitLab:
-Platform as a Service options are a huge portion of the value provided by Cloud Platforms as they simplify operational complexity and reduce the SRE and security skilling required to operate advanced, highly available technology services. Implementation patterns can be pre-qualified against the partner PaaS options.
+ ```shell
+ sudo apt update
+ sudo apt install gitlab-ee
+ ```
-- Implementation patterns help implementers understand what PaaS options are known to work and how to choose between PaaS solutions when a single platform has more than one PaaS option for the same GitLab role.
-- For instance, where reference architectures do not have a specific recommendation on what technology is leveraged for GitLab outbound email services or what the sizing should be - a Reference Implementation may advise using a cloud providers Email as a Service (PaaS) and possibly even with specific settings.
+After a few minutes, the new version should be up and running.
-### Cost optimizing engineering
+## Find official GitLab-created AMI IDs on AWS
-Cost engineering is a fundamental aspect of Cloud Architecture and frequently the savings capabilities available on a platform exert strong influence on how to build out scaled computing.
+Read more on how to use [GitLab releases as AMIs](../../solutions/cloud/aws/gitlab_single_box_on_aws.md#official-gitlab-releases-as-amis).
-- Implementation patterns may define GPT tested autoscaling for various aspects of GitLab infrastructure, including minimum idling configurations and scaling speeds.
-- Implementation patterns may provide GPT testing for advised configurations that go beyond the scope of reference architectures, for instance GPT tested elastic scaling configurations for Cloud Native Hybrid that enable lower resourcing during periods of lower usage (for example on the weekend).
-- Implementation patterns may engineer specifically for the savings models available on a platform provider. An AWS example would be maximizing the occurrence of a specific instance type for taking advantage of reserved instances.
-- Implementation patterns may leverage ephemeral compute where appropriate and with appropriate customer guidelines. For instance, a Kubernetes node group dedicated to runners on ephemeral compute (with appropriate GitLab Runner tagging to indicate the compute type).
-- Implementation patterns may include vendor specific cost calculators.
+## Conclusion
-### Actionability and automatability orientation
+In this guide, we went mostly through scaling and some redundancy options,
+your mileage may vary.
-Implementation patterns are one step closer to specifics that can be used as a source for build instructions and automation code:
+Keep in mind that all solutions come with a trade-off between
+cost/complexity and uptime. The more uptime you want, the more complex the solution.
+And the more complex the solution, the more work is involved in setting up and
+maintaining it.
-- Implementation patterns enable builders to generate a list of vendor specific resources required to implement GitLab for a given Reference Architecture.
-- Implementation patterns enable builders to use manual instructions or to create automation to build out the reference implementation.
+Have a read through these other resources and feel free to
+[open an issue](https://gitlab.com/gitlab-org/gitlab/-/issues/new)
+to request additional material:
-## Supplementary implementation patterns
+- [Scaling GitLab](../../administration/reference_architectures/index.md):
+ GitLab supports several different types of clustering.
+- [Geo replication](../../administration/geo/index.md):
+ Geo is the solution for widely distributed development teams.
+- [Linux package](https://docs.gitlab.com/omnibus/) - Everything you must know
+ about administering your GitLab instance.
+- [Add a license](../../administration/license.md):
+ Activate all GitLab Enterprise Edition functionality with a license.
+- [Pricing](https://about.gitlab.com/pricing/): Pricing for the different tiers.
-Implementation patterns may also provide specialized implementations beyond the scope of reference architecture compliance, especially where the cost of enablement can be more appropriately managed.
+## Troubleshooting
-For example:
+### Instances are failing health checks
-- Small, self-contained GitLab instances for per-person administration training, perhaps on Kubernetes so that a deployment cluster is self-contained as well.
-- GitLab Runner implementation patterns, including using platform-specific PaaS.
+If your instances are failing the load balancer's health checks, verify that they are returning a status `200` from the health check endpoint we configured earlier. Any other status, including redirects like status `302`, causes the health check to fail.
-## Intended audiences and contributors
+You may have to set a password on the `root` user to prevent automatic redirects on the sign-in endpoint before health checks pass.
-The primary audiences for and contributors to this information is the GitLab **Implementation Eco System** which consists of at least:
+### "The change you requested was rejected (422)"
-GitLab Implementation Community:
+If you see this page when trying to set a password via the web interface, make sure `external_url` in `gitlab.rb` matches the domain you are making a request from, and run `sudo gitlab-ctl reconfigure` after making any changes to it.
-- Customers
-- GitLab Channel Partners (Integrators)
-- Platform Partners
+### Some job logs are not uploaded to object storage
-GitLab Internal Implementation Teams:
+When the GitLab deployment is scaled up to more than one node, some job logs may not be uploaded to [object storage](../../administration/object_storage.md) properly. [Incremental logging is required](../../administration/object_storage.md#alternatives-to-file-system-storage) for CI to use object storage.
-- Quality / Distribution / Self-Managed
-- Alliances
-- Training
-- Support
-- Professional Services
-- Public Sector
+Enable [incremental logging](../../administration/job_logs.md#enable-or-disable-incremental-logging) if it has not already been enabled.
diff --git a/doc/install/aws/manual_install_aws.md b/doc/install/aws/manual_install_aws.md
index a952180674c..0019c8c3472 100644
--- a/doc/install/aws/manual_install_aws.md
+++ b/doc/install/aws/manual_install_aws.md
@@ -1,856 +1,11 @@
---
-stage: Systems
-group: Distribution
-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
+redirect_to: 'index.md'
+remove_date: '2024-03-31'
---
-{::options parse_block_html="true" /}
+This document was moved to [AWS](index.md).
-# Installing a GitLab POC on Amazon Web Services (AWS) **(FREE SELF)**
-
-This page offers a walkthrough of a common configuration for GitLab on AWS using the official Linux package. You should customize it to accommodate your needs.
-
-NOTE:
-For organizations with 1,000 users or less, the recommended AWS installation method is to launch an EC2 single box [Linux package installation](https://about.gitlab.com/install/) and implement a snapshot strategy for backing up the data. See the [1,000 user reference architecture](../../administration/reference_architectures/1k_users.md) for more information.
-
-## Getting started for production-grade GitLab
-
-NOTE:
-This document is an installation guide for a proof of concept instance. It is not a reference architecture and it does not result in a highly available configuration.
-
-Following this guide exactly results in a proof of concept instance that roughly equates to a **scaled down** version of a **two availability zone implementation** of the **Non-HA** [2000 User Reference Architecture](../../administration/reference_architectures/2k_users.md). The 2K reference architecture is not HA because it is primarily intended to provide some scaling while keeping costs and complexity low. The [3000 User Reference Architecture](../../administration/reference_architectures/3k_users.md) is the smallest size that is GitLab HA. It has additional service roles to achieve HA, most notably it uses Gitaly Cluster to achieve HA for Git repository storage and specifies triple redundancy.
-
-GitLab maintains and tests two main types of Reference Architectures. The **Linux package architectures** are implemented on instance compute while **Cloud Native Hybrid architectures** maximize the use of a Kubernetes cluster. Cloud Native Hybrid reference architecture specifications are addendum sections to the Reference Architecture size pages that start by describing the Linux package architecture. For example, the 3000 User Cloud Native Reference Architecture is in the subsection titled [Cloud Native Hybrid reference architecture with Helm Charts (alternative)](../../administration/reference_architectures/3k_users.md#cloud-native-hybrid-reference-architecture-with-helm-charts-alternative) in the 3000 User Reference Architecture page.
-
-### Getting started for production-grade Linux package installations
-
-The Infrastructure as Code tooling [GitLab Environment Tool (GET)](https://gitlab.com/gitlab-org/gitlab-environment-toolkit/-/tree/main) is the best place to start for building using the Linux package on AWS and most especially if you are targeting an HA setup. While it does not automate everything, it does complete complex setups like Gitaly Cluster for you. GET is open source so anyone can build on top of it and contribute improvements to it.
-
-### Getting started for production-grade Cloud Native Hybrid GitLab
-
-For the Cloud Native Hybrid architectures there are two Infrastructure as Code options which are compared in GitLab Cloud Native Hybrid on AWS EKS implementation pattern in the section [Available Infrastructure as Code for GitLab Cloud Native Hybrid](gitlab_hybrid_on_aws.md#available-infrastructure-as-code-for-gitlab-cloud-native-hybrid). It compares the [GitLab Environment Toolkit](https://gitlab.com/gitlab-org/gitlab-environment-toolkit/-/tree/main) to the AWS Quick Start for GitLab Cloud Native Hybrid on EKS which was co-developed by GitLab and AWS. GET and the AWS Quick Start are both open source so anyone can build on top of them and contribute improvements to them.
-
-## Introduction
-
-For the most part, we make use of the Linux package in our setup, but we also leverage native AWS services. Instead of using the Linux package-bundled PostgreSQL and Redis, we use Amazon RDS and ElastiCache.
-
-In this guide, we go through a multi-node setup where we start by
-configuring our Virtual Private Cloud and subnets to later integrate
-services such as RDS for our database server and ElastiCache as a Redis
-cluster to finally manage them in an auto scaling group with custom
-scaling policies.
-
-## Requirements
-
-In addition to having a basic familiarity with [AWS](https://docs.aws.amazon.com/) and [Amazon EC2](https://docs.aws.amazon.com/ec2/), you need:
-
-- [An AWS account](https://console.aws.amazon.com/console/home)
-- [To create or upload an SSH key](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-key-pairs.html)
- to connect to the instance via SSH
-- A domain name for the GitLab instance
-- An SSL/TLS certificate to secure your domain. If you do not already own one, you can provision a free public SSL/TLS certificate through [AWS Certificate Manager](https://aws.amazon.com/certificate-manager/)(ACM) for use with the [Elastic Load Balancer](#load-balancer) we create.
-
-NOTE:
-It can take a few hours to validate a certificate provisioned through ACM. To avoid delays later, request your certificate as soon as possible.
-
-## Architecture
-
-Below is a diagram of the recommended architecture.
-
-![AWS architecture diagram](img/aws_ha_architecture_diagram.png)
-
-## AWS costs
-
-GitLab uses the following AWS services, with links to pricing information:
-
-- **EC2**: GitLab is deployed on shared hardware, for which
- [on-demand pricing](https://aws.amazon.com/ec2/pricing/on-demand/) applies.
- If you want to run GitLab on a dedicated or reserved instance, see the
- [EC2 pricing page](https://aws.amazon.com/ec2/pricing/) for information about
- its cost.
-- **S3**: GitLab uses S3 ([pricing page](https://aws.amazon.com/s3/pricing/)) to
- store backups, artifacts, and LFS objects.
-- **ELB**: A Classic Load Balancer ([pricing page](https://aws.amazon.com/elasticloadbalancing/pricing/)),
- used to route requests to the GitLab instances.
-- **RDS**: An Amazon Relational Database Service using PostgreSQL
- ([pricing page](https://aws.amazon.com/rds/postgresql/pricing/)).
-- **ElastiCache**: An in-memory cache environment ([pricing page](https://aws.amazon.com/elasticache/pricing/)),
- used to provide a Redis configuration.
-
-## Create an IAM EC2 instance role and profile
-
-As we are using [Amazon S3 object storage](#amazon-s3-object-storage), our EC2 instances must have read, write, and list permissions for our S3 buckets. To avoid embedding AWS keys in our GitLab configuration, we make use of an [IAM Role](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles.html) to allow our GitLab instance with this access. We must create an IAM policy to attach to our IAM role:
-
-### Create an IAM Policy
-
-1. Go to the IAM dashboard and select **Policies** in the left menu.
-1. Select **Create policy**, select the `JSON` tab, and add a policy. We want to [follow security best practices and grant _least privilege_](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege), giving our role only the permissions needed to perform the required actions.
- 1. Assuming you prefix the S3 bucket names with `gl-` as shown in the diagram, add the following policy:
-
- ```json
- { "Version": "2012-10-17",
- "Statement": [
- {
- "Effect": "Allow",
- "Action": [
- "s3:PutObject",
- "s3:GetObject",
- "s3:DeleteObject",
- "s3:PutObjectAcl"
- ],
- "Resource": "arn:aws:s3:::gl-*/*"
- },
- {
- "Effect": "Allow",
- "Action": [
- "s3:ListBucket",
- "s3:AbortMultipartUpload",
- "s3:ListMultipartUploadParts",
- "s3:ListBucketMultipartUploads"
- ],
- "Resource": "arn:aws:s3:::gl-*"
- }
- ]
- }
- ```
-
-1. Select **Review policy**, give your policy a name (we use `gl-s3-policy`), and select **Create policy**.
-
-### Create an IAM Role
-
-1. Still on the IAM dashboard, select **Roles** in the left menu, and
- select **Create role**.
-1. Create a new role by selecting **AWS service > EC2**, then select
- **Next: Permissions**.
-1. In the policy filter, search for the `gl-s3-policy` we created above, select it, and select **Tags**.
-1. Add tags if needed and select **Review**.
-1. Give the role a name (we use `GitLabS3Access`) and select **Create Role**.
-
-We use this role when we [create a launch configuration](#create-a-launch-configuration) later on.
-
-## Configuring the network
-
-We start by creating a VPC for our GitLab cloud infrastructure, then
-we can create subnets to have public and private instances in at least
-two [Availability Zones (AZs)](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html). Public subnets require a Route Table keep and an associated
-Internet Gateway.
-
-### Creating the Virtual Private Cloud (VPC)
-
-We now create a VPC, a virtual networking environment that you control:
-
-1. Sign in to [Amazon Web Services](https://console.aws.amazon.com/vpc/home).
-1. Select **Your VPCs** from the left menu and then select **Create VPC**.
- At the "Name tag" enter `gitlab-vpc` and at the "IPv4 CIDR block" enter
- `10.0.0.0/16`. If you don't require dedicated hardware, you can leave
- "Tenancy" as default. Select **Yes, Create** when ready.
-
- ![Create VPC](img/create_vpc.png)
-
-1. Select the VPC, select **Actions**, select **Edit DNS resolution**, and enable DNS resolution. Select **Save** when done.
-
-### Subnets
-
-Now, let's create some subnets in different Availability Zones. Make sure
-that each subnet is associated to the VPC we just created and
-that CIDR blocks don't overlap. This also
-allows us to enable multi AZ for redundancy.
-
-We create private and public subnets to match load balancers and
-RDS instances as well:
-
-1. Select **Subnets** from the left menu.
-1. Select **Create subnet**. Give it a descriptive name tag based on the IP,
- for example `gitlab-public-10.0.0.0`, select the VPC we created previously, select an availability zone (we use `us-west-2a`),
- and at the IPv4 CIDR block let's give it a 24 subnet `10.0.0.0/24`:
-
- ![Create subnet](img/create_subnet.png)
-
-1. Follow the same steps to create all subnets:
-
- | Name tag | Type | Availability Zone | CIDR block |
- | ------------------------- | ------- | ----------------- | ------------- |
- | `gitlab-public-10.0.0.0` | public | `us-west-2a` | `10.0.0.0/24` |
- | `gitlab-private-10.0.1.0` | private | `us-west-2a` | `10.0.1.0/24` |
- | `gitlab-public-10.0.2.0` | public | `us-west-2b` | `10.0.2.0/24` |
- | `gitlab-private-10.0.3.0` | private | `us-west-2b` | `10.0.3.0/24` |
-
-1. Once all the subnets are created, enable **Auto-assign IPv4** for the two public subnets:
- 1. Select each public subnet in turn, select **Actions**, and select **Modify auto-assign IP settings**. Enable the option and save.
-
-### Internet Gateway
-
-Now, still on the same dashboard, go to Internet Gateways and
-create a new one:
-
-1. Select **Internet Gateways** from the left menu.
-1. Select **Create internet gateway**, give it the name `gitlab-gateway` and
- select **Create**.
-1. Select it from the table, and then under the **Actions** dropdown list choose
- "Attach to VPC".
-
- ![Create gateway](img/create_gateway.png)
-
-1. Choose `gitlab-vpc` from the list and hit **Attach**.
-
-### Create NAT Gateways
-
-Instances deployed in our private subnets must connect to the internet for updates, but should not be reachable from the public internet. To achieve this, we make use of [NAT Gateways](https://docs.aws.amazon.com/vpc/latest/userguide/vpc-nat-gateway.html) deployed in each of our public subnets:
-
-1. Go to the VPC dashboard and select **NAT Gateways** in the left menu bar.
-1. Select **Create NAT Gateway** and complete the following:
- 1. **Subnet**: Select `gitlab-public-10.0.0.0` from the dropdown list.
- 1. **Elastic IP Allocation ID**: Enter an existing Elastic IP or select **Allocate Elastic IP address** to allocate a new IP to your NAT gateway.
- 1. Add tags if needed.
- 1. Select **Create NAT Gateway**.
-
-Create a second NAT gateway but this time place it in the second public subnet, `gitlab-public-10.0.2.0`.
-
-### Route Tables
-
-#### Public Route Table
-
-We must create a route table for our public subnets to reach the internet via the internet gateway we created in the previous step.
-
-On the VPC dashboard:
-
-1. Select **Route Tables** from the left menu.
-1. Select **Create Route Table**.
-1. At the "Name tag" enter `gitlab-public` and choose `gitlab-vpc` under "VPC".
-1. Select **Create**.
-
-We now must add our internet gateway as a new target and have
-it receive traffic from any destination.
-
-1. Select **Route Tables** from the left menu and select the `gitlab-public`
- route to show the options at the bottom.
-1. Select the **Routes** tab, select **Edit routes > Add route** and set `0.0.0.0/0`
- as the destination. In the target column, select the `gitlab-gateway` we created previously.
- Select **Save routes** when done.
-
-Next, we must associate the **public** subnets to the route table:
-
-1. Select the **Subnet Associations** tab and select **Edit subnet associations**.
-1. Check only the public subnets and select **Save**.
-
-#### Private Route Tables
-
-We also must create two private route tables so that instances in each private subnet can reach the internet via the NAT gateway in the corresponding public subnet in the same availability zone.
-
-1. Follow the same steps as above to create two private route tables. Name them `gitlab-private-a` and `gitlab-private-b`.
-1. Next, add a new route to each of the private route tables where the destination is `0.0.0.0/0` and the target is one of the NAT gateways we created earlier.
- 1. Add the NAT gateway we created in `gitlab-public-10.0.0.0` as the target for the new route in the `gitlab-private-a` route table.
- 1. Similarly, add the NAT gateway in `gitlab-public-10.0.2.0` as the target for the new route in the `gitlab-private-b`.
-1. Lastly, associate each private subnet with a private route table.
- 1. Associate `gitlab-private-10.0.1.0` with `gitlab-private-a`.
- 1. Associate `gitlab-private-10.0.3.0` with `gitlab-private-b`.
-
-## Load Balancer
-
-We create a load balancer to evenly distribute inbound traffic on ports `80` and `443` across our GitLab application servers. Based on the [scaling policies](#create-an-auto-scaling-group) we create later, instances are added to or removed from our load balancer as needed. Additionally, the load balancer performs health checks on our instances.
-
-On the EC2 dashboard, look for Load Balancer in the left navigation bar:
-
-1. Select **Create Load Balancer**.
- 1. Choose the **Classic Load Balancer**.
- 1. Give it a name (we use `gitlab-loadbalancer`) and for the **Create LB Inside** option, select `gitlab-vpc` from the dropdown list.
- 1. In the **Listeners** section, set the following listeners:
- - HTTP port 80 for both load balancer and instance protocol and ports
- - TCP port 22 for both load balancer and instance protocols and ports
- - HTTPS port 443 for load balancer protocol and ports, forwarding to HTTP port 80 on the instance (we configure GitLab to listen on port 80 [later in the guide](#add-support-for-proxied-ssl))
- 1. In the **Select Subnets** section, select both public subnets from the list so that the load balancer can route traffic to both availability zones.
-1. We add a security group for our load balancer to act as a firewall to control what traffic is allowed through. Select **Assign Security Groups** and select **Create a new security group**, give it a name
- (we use `gitlab-loadbalancer-sec-group`) and description, and allow both HTTP and HTTPS traffic
- from anywhere (`0.0.0.0/0, ::/0`). Also allow SSH traffic, select a custom source, and add a single trusted IP address or an IP address range in CIDR notation. This allows users to perform Git actions over SSH.
-1. Select **Configure Security Settings** and set the following:
- 1. Select an SSL/TLS certificate from ACM or upload a certificate to IAM.
- 1. Under **Select a Cipher**, pick a predefined security policy from the dropdown list. You can see a breakdown of [Predefined SSL Security Policies for Classic Load Balancers](https://docs.aws.amazon.com/elasticloadbalancing/latest/classic/elb-security-policy-table.html) in the AWS documentation. Check the GitLab codebase for a list of [supported SSL ciphers and protocols](https://gitlab.com/gitlab-org/gitlab/-/blob/9ee7ad433269b37251e0dd5b5e00a0f00d8126b4/lib/support/nginx/gitlab-ssl#L97-99).
-1. Select **Configure Health Check** and set up a health check for your EC2 instances.
- 1. For **Ping Protocol**, select HTTP.
- 1. For **Ping Port**, enter 80.
- 1. For **Ping Path** - we recommend that you [use the Readiness check endpoint](../../administration/load_balancer.md#readiness-check). You must add [the VPC IP Address Range (CIDR)](https://docs.aws.amazon.com/elasticloadbalancing/latest/classic/elb-security-groups.html#elb-vpc-nacl) to the [IP allowlist](../../administration/monitoring/ip_allowlist.md) for the [Health Check endpoints](../../administration/monitoring/health_check.md)
- 1. Keep the default **Advanced Details** or adjust them according to your needs.
-1. Select **Add EC2 Instances** - don't add anything as we create an Auto Scaling Group later to manage instances for us.
-1. Select **Add Tags** and add any tags you need.
-1. Select **Review and Create**, review all your settings, and select **Create** if you're happy.
-
-After the Load Balancer is up and running, you can revisit your Security
-Groups to refine the access only through the ELB and any other requirements
-you might have.
-
-### Configure DNS for Load Balancer
-
-On the Route 53 dashboard, select **Hosted zones** in the left navigation bar:
-
-1. Select an existing hosted zone or, if you do not already have one for your domain, select **Create Hosted Zone**, enter your domain name, and select **Create**.
-1. Select **Create Record Set** and provide the following values:
- 1. **Name:** Use the domain name (the default value) or enter a subdomain.
- 1. **Type:** Select **A - IPv4 address**.
- 1. **Alias:** Defaults to **No**. Select **Yes**.
- 1. **Alias Target:** Find the **ELB Classic Load Balancers** section and select the classic load balancer we created earlier.
- 1. **Routing Policy:** We use **Simple** but you can choose a different policy based on your use case.
- 1. **Evaluate Target Health:** We set this to **No** but you can choose to have the load balancer route traffic based on target health.
- 1. Select **Create**.
-1. If you registered your domain through Route 53, you're done. If you used a different domain registrar, you must update your DNS records with your domain registrar. You must:
- 1. Select **Hosted zones** and select the domain you added above.
- 1. You see a list of `NS` records. From your domain registrar's administrator panel, add each of these as `NS` records to your domain's DNS records. These steps may vary between domain registrars. If you're stuck, Google **"name of your registrar" add DNS records** and you should find a help article specific to your domain registrar.
-
-The steps for doing this vary depending on which registrar you use and is beyond the scope of this guide.
-
-## PostgreSQL with RDS
-
-For our database server we use Amazon RDS for PostgreSQL which offers Multi AZ
-for redundancy (Aurora is **not** supported). First we create a security group and subnet group, then we
-create the actual RDS instance.
-
-### RDS Security Group
-
-We need a security group for our database that allows inbound traffic from the instances we deploy in our `gitlab-loadbalancer-sec-group` later on:
-
-1. From the EC2 dashboard, select **Security Groups** from the left menu bar.
-1. Select **Create security group**.
-1. Give it a name (we use `gitlab-rds-sec-group`), a description, and select the `gitlab-vpc` from the **VPC** dropdown list.
-1. In the **Inbound rules** section, select **Add rule** and set the following:
- 1. **Type:** search for and select the **PostgreSQL** rule.
- 1. **Source type:** set as "Custom".
- 1. **Source:** select the `gitlab-loadbalancer-sec-group` we created earlier.
-1. When done, select **Create security group**.
-
-### RDS Subnet Group
-
-1. Go to the RDS dashboard and select **Subnet Groups** from the left menu.
-1. Select **Create DB Subnet Group**.
-1. Under **Subnet group details**, enter a name (we use `gitlab-rds-group`), a description, and choose the `gitlab-vpc` from the VPC dropdown list.
-1. From the **Availability Zones** dropdown list, select the Availability Zones that include the subnets you've configured. In our case, we add `eu-west-2a` and `eu-west-2b`.
-1. From the **Subnets** dropdown list, select the two private subnets (`10.0.1.0/24` and `10.0.3.0/24`) as we defined them in the [subnets section](#subnets).
-1. Select **Create** when ready.
-
-### Create the database
-
-WARNING:
-Avoid using burstable instances (t class instances) for the database as this could lead to performance issues due to CPU credits running out during sustained periods of high load.
-
-Now, it's time to create the database:
-
-1. Go to the RDS dashboard, select **Databases** from the left menu, and select **Create database**.
-1. Select **Standard Create** for the database creation method.
-1. Select **PostgreSQL** as the database engine and select the minimum PostgreSQL version as defined for your GitLab version in our [database requirements](../../install/requirements.md#postgresql-requirements).
-1. Because this is a production server, let's choose **Production** from the **Templates** section.
-1. Under **Settings**, use:
- - `gitlab-db-ha` for the DB instance identifier.
- - `gitlab` for a master username.
- - A very secure password for the master password.
-
- Make a note of these as we need them later.
-
-1. For the DB instance size, select **Standard classes** and select an instance size that meets your requirements from the dropdown list. We use a `db.m4.large` instance.
-1. Under **Storage**, configure the following:
- 1. Select **Provisioned IOPS (SSD)** from the storage type dropdown list. Provisioned IOPS (SSD) storage is best suited for this use (though you can choose General Purpose (SSD) to reduce the costs). Read more about it at [Storage for Amazon RDS](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Storage.html).
- 1. Allocate storage and set provisioned IOPS. We use the minimum values, `100` and `1000`, respectively.
- 1. Enable storage autoscaling (optional) and set a maximum storage threshold.
-1. Under **Availability & durability**, select **Create a standby instance** to have a standby RDS instance provisioned in a different [Availability Zone](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.MultiAZ.html).
-1. Under **Connectivity**, configure the following:
- 1. Select the VPC we created earlier (`gitlab-vpc`) from the **Virtual Private Cloud (VPC)** dropdown list.
- 1. Expand the **Additional connectivity configuration** section and select the subnet group (`gitlab-rds-group`) we created earlier.
- 1. Set public accessibility to **No**.
- 1. Under **VPC security group**, select **Choose existing** and select the `gitlab-rds-sec-group` we create above from the dropdown list.
- 1. Leave the database port as the default `5432`.
-1. For **Database authentication**, select **Password authentication**.
-1. Expand the **Additional configuration** section and complete the following:
- 1. The initial database name. We use `gitlabhq_production`.
- 1. Configure your preferred backup settings.
- 1. The only other change we make here is to disable auto minor version updates under **Maintenance**.
- 1. Leave all the other settings as is or tweak according to your needs.
- 1. If you're happy, select **Create database**.
-
-Now that the database is created, let's move on to setting up Redis with ElastiCache.
-
-## Redis with ElastiCache
-
-ElastiCache is an in-memory hosted caching solution. Redis maintains its own
-persistence and is used to store session data, temporary cache information, and background job queues for the GitLab application.
-
-### Create a Redis Security Group
-
-1. Go to the EC2 dashboard.
-1. Select **Security Groups** from the left menu.
-1. Select **Create security group** and fill in the details. Give it a name (we use `gitlab-redis-sec-group`),
- add a description, and choose the VPC we created previously
-1. In the **Inbound rules** section, select **Add rule** and add a **Custom TCP** rule, set port `6379`, and set the "Custom" source as the `gitlab-loadbalancer-sec-group` we created earlier.
-1. When done, select **Create security group**.
-
-### Redis Subnet Group
-
-1. Go to the ElastiCache dashboard from your AWS console.
-1. Go to **Subnet Groups** in the left menu, and create a new subnet group (we name ours `gitlab-redis-group`).
- Make sure to select our VPC and its [private subnets](#subnets).
-1. Select **Create** when ready.
-
- ![ElastiCache subnet](img/ec_subnet.png)
-
-### Create the Redis Cluster
-
-1. Go back to the ElastiCache dashboard.
-1. Select **Redis** on the left menu and select **Create** to create a new
- Redis cluster. Do not enable **Cluster Mode** as it is [not supported](../../administration/redis/replication_and_failover_external.md#requirements). Even without cluster mode on, you still get the
- chance to deploy Redis in multiple availability zones.
-1. In the settings section:
- 1. Give the cluster a name (`gitlab-redis`) and a description.
- 1. For the version, select the latest.
- 1. Leave the port as `6379` because this is what we used in our Redis security group above.
- 1. Select the node type (at least `cache.t3.medium`, but adjust to your needs) and the number of replicas.
-1. In the advanced settings section:
- 1. Select the multi-AZ auto-failover option.
- 1. Select the subnet group we created previously.
- 1. Manually select the preferred availability zones, and under "Replica 2"
- choose a different zone than the other two.
-
- ![Redis availability zones](img/ec_az.png)
-
-1. In the security settings, edit the security groups and choose the
- `gitlab-redis-sec-group` we had previously created.
-1. Leave the rest of the settings to their default values or edit to your liking.
-1. When done, select **Create**.
-
-## Setting up Bastion Hosts
-
-Because our GitLab instances are in private subnets, we need a way to connect
-to these instances with SSH for actions that include making configuration changes
-and performing upgrades. One way of doing this is by using a [bastion host](https://en.wikipedia.org/wiki/Bastion_host),
-sometimes also referred to as a jump box.
-
-NOTE:
-If you do not want to maintain bastion hosts, you can set up [AWS Systems Manager Session Manager](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager.html) for access to instances. This is beyond the scope of this document.
-
-### Create Bastion Host A
-
-1. Go to the EC2 Dashboard and select **Launch instance**.
-1. Select the **Ubuntu Server 18.04 LTS (HVM)** AMI.
-1. Choose an instance type. We use a `t2.micro` as we only use the bastion host to SSH into our other instances.
-1. Select **Configure Instance Details**.
- 1. Under **Network**, select the `gitlab-vpc` from the dropdown list.
- 1. Under **Subnet**, select the public subnet we created earlier (`gitlab-public-10.0.0.0`).
- 1. Double check that under **Auto-assign Public IP** you have **Use subnet setting (Enable)** selected.
- 1. Leave everything else as default and select **Add Storage**.
-1. For storage, we leave everything as default and only add an 8GB root volume. We do not store anything on this instance.
-1. Select **Add Tags** and on the next screen select **Add Tag**.
- 1. We only set `Key: Name` and `Value: Bastion Host A`.
-1. Select **Configure Security Group**.
- 1. Select **Create a new security group**, enter a **Security group name** (we use `bastion-sec-group`), and add a description.
- 1. We enable SSH access from anywhere (`0.0.0.0/0`). If you want stricter security, specify a single IP address or an IP address range in CIDR notation.
- 1. Select **Review and Launch**
-1. Review all your settings and, if you're happy, select **Launch**.
-1. Acknowledge that you have access to an existing key pair or create a new one. Select **Launch Instance**.
-
-Confirm that you can SSH into the instance:
-
-1. On the EC2 Dashboard, select **Instances** in the left menu.
-1. Select **Bastion Host A** from your list of instances.
-1. Select **Connect** and follow the connection instructions.
-1. If you are able to connect successfully, let's move on to setting up our second bastion host for redundancy.
-
-### Create Bastion Host B
-
-1. Create an EC2 instance following the same steps as above with the following changes:
- 1. For the **Subnet**, select the second public subnet we created earlier (`gitlab-public-10.0.2.0`).
- 1. Under the **Add Tags** section, we set `Key: Name` and `Value: Bastion Host B` so that we can easily identify our two instances.
- 1. For the security group, select the existing `bastion-sec-group` we created above.
-
-### Use SSH Agent Forwarding
-
-EC2 instances running Linux use private key files for SSH authentication. You connect to your bastion host using an SSH client and the private key file stored on your client. Because the private key file is not present on the bastion host, you are not able to connect to your instances in private subnets.
-
-Storing private key files on your bastion host is a bad idea. To get around this, use SSH agent forwarding on your client. See [Securely Connect to Linux Instances Running in a Private Amazon VPC](https://aws.amazon.com/blogs/security/securely-connect-to-linux-instances-running-in-a-private-amazon-vpc/) for a step-by-step guide on how to use SSH agent forwarding.
-
-## Install GitLab and create custom AMI
-
-We need a preconfigured, custom GitLab AMI to use in our launch configuration later. As a starting point, we use the official GitLab AMI to create a GitLab instance. Then, we add our custom configuration for PostgreSQL, Redis, and Gitaly. If you prefer, instead of using the official GitLab AMI, you can also spin up an EC2 instance of your choosing and [manually install GitLab](https://about.gitlab.com/install/).
-
-### Install GitLab
-
-From the EC2 dashboard:
-
-1. Use the section below titled "[Find official GitLab-created AMI IDs on AWS](#find-official-gitlab-created-ami-ids-on-aws)" to find the correct AMI to launch.
-1. After selecting **Launch** on the desired AMI, select an instance type based on your workload. Consult the [hardware requirements](../../install/requirements.md#hardware-requirements) to choose one that fits your needs (at least `c5.xlarge`, which is sufficient to accommodate 100 users).
-1. Select **Configure Instance Details**:
- 1. In the **Network** dropdown list, select `gitlab-vpc`, the VPC we created earlier.
- 1. In the **Subnet** dropdown list, select `gitlab-private-10.0.1.0` from the list of subnets we created earlier.
- 1. Double check that **Auto-assign Public IP** is set to `Use subnet setting (Disable)`.
- 1. Select **Add Storage**.
- 1. The root volume is 8GiB by default and should be enough given that we do not store any data there.
-1. Select **Add Tags** and add any tags you may need. In our case, we only set `Key: Name` and `Value: GitLab`.
-1. Select **Configure Security Group**. Check **Select an existing security group** and select the `gitlab-loadbalancer-sec-group` we created earlier.
-1. Select **Review and launch** followed by **Launch** if you're happy with your settings.
-1. Finally, acknowledge that you have access to the selected private key file or create a new one. Select **Launch Instances**.
-
-### Add custom configuration
-
-Connect to your GitLab instance via **Bastion Host A** using [SSH Agent Forwarding](#use-ssh-agent-forwarding). Once connected, add the following custom configuration:
-
-#### Disable Let's Encrypt
-
-Because we're adding our SSL certificate at the load balancer, we do not need the GitLab built-in support for Let's Encrypt. Let's Encrypt [is enabled by default](https://docs.gitlab.com/omnibus/settings/ssl/index.html#enable-the-lets-encrypt-integration) when using an `https` domain in GitLab 10.7 and later, so we must explicitly disable it:
-
-1. Open `/etc/gitlab/gitlab.rb` and disable it:
-
- ```ruby
- letsencrypt['enable'] = false
- ```
-
-1. Save the file and reconfigure for the changes to take effect:
-
- ```shell
- sudo gitlab-ctl reconfigure
- ```
-
-#### Install the required extensions for PostgreSQL
-
-From your GitLab instance, connect to the RDS instance to verify access and to install the required `pg_trgm` and `btree_gist` extensions.
-
-To find the host or endpoint, go to **Amazon RDS > Databases** and select the database you created earlier. Look for the endpoint under the **Connectivity & security** tab.
-
-Do not to include the colon and port number:
-
-```shell
-sudo /opt/gitlab/embedded/bin/psql -U gitlab -h <rds-endpoint> -d gitlabhq_production
-```
-
-At the `psql` prompt create the extension and then quit the session:
-
-```shell
-psql (10.9)
-Type "help" for help.
-
-gitlab=# CREATE EXTENSION pg_trgm;
-gitlab=# CREATE EXTENSION btree_gist;
-gitlab=# \q
-```
-
-#### Configure GitLab to connect to PostgreSQL and Redis
-
-1. Edit `/etc/gitlab/gitlab.rb`, find the `external_url 'http://<domain>'` option
- and change it to the `https` domain you are using.
-
-1. Look for the GitLab database settings and uncomment as necessary. In
- our current case we specify the database adapter, encoding, host, name,
- username, and password:
-
- ```ruby
- # Disable the built-in Postgres
- postgresql['enable'] = false
-
- # Fill in the connection details
- gitlab_rails['db_adapter'] = "postgresql"
- gitlab_rails['db_encoding'] = "unicode"
- gitlab_rails['db_database'] = "gitlabhq_production"
- gitlab_rails['db_username'] = "gitlab"
- gitlab_rails['db_password'] = "mypassword"
- gitlab_rails['db_host'] = "<rds-endpoint>"
- ```
-
-1. Next, we must configure the Redis section by adding the host and
- uncommenting the port:
-
- ```ruby
- # Disable the built-in Redis
- redis['enable'] = false
-
- # Fill in the connection details
- gitlab_rails['redis_host'] = "<redis-endpoint>"
- gitlab_rails['redis_port'] = 6379
- ```
-
-1. Finally, reconfigure GitLab for the changes to take effect:
-
- ```shell
- sudo gitlab-ctl reconfigure
- ```
-
-1. You can also run a check and a service status to make sure
- everything has been setup correctly:
-
- ```shell
- sudo gitlab-rake gitlab:check
- sudo gitlab-ctl status
- ```
-
-#### Set up Gitaly
-
-WARNING:
-In this architecture, having a single Gitaly server creates a single point of failure. Use
-[Gitaly Cluster](../../administration/gitaly/praefect.md) to remove this limitation.
-
-Gitaly is a service that provides high-level RPC access to Git repositories.
-It should be enabled and configured on a separate EC2 instance in one of the
-[private subnets](#subnets) we configured previously.
-
-Let's create an EC2 instance where we install Gitaly:
-
-1. From the EC2 dashboard, select **Launch instance**.
-1. Choose an AMI. In this example, we select the **Ubuntu Server 18.04 LTS (HVM), SSD Volume Type**.
-1. Choose an instance type. We pick a `c5.xlarge`.
-1. Select **Configure Instance Details**.
- 1. In the **Network** dropdown list, select `gitlab-vpc`, the VPC we created earlier.
- 1. In the **Subnet** dropdown list, select `gitlab-private-10.0.1.0` from the list of subnets we created earlier.
- 1. Double check that **Auto-assign Public IP** is set to `Use subnet setting (Disable)`.
- 1. Select **Add Storage**.
-1. Increase the Root volume size to `20 GiB` and change the **Volume Type** to `Provisioned IOPS SSD (io1)`. (This is an arbitrary size. Create a volume big enough for your repository storage requirements.)
- 1. For **IOPS** set `1000` (20 GiB x 50 IOPS). You can provision up to 50 IOPS per GiB. If you select a larger volume, increase the IOPS accordingly. Workloads where many small files are written in a serialized manner, like `git`, requires performant storage, hence the choice of `Provisioned IOPS SSD (io1)`.
-1. Select **Add Tags** and add your tags. In our case, we only set `Key: Name` and `Value: Gitaly`.
-1. Select **Configure Security Group** and let's **Create a new security group**.
- 1. Give your security group a name and description. We use `gitlab-gitaly-sec-group` for both.
- 1. Create a **Custom TCP** rule and add port `8075` to the **Port Range**. For the **Source**, select the `gitlab-loadbalancer-sec-group`.
- 1. Also add an inbound rule for SSH from the `bastion-sec-group` so that we can connect using [SSH Agent Forwarding](#use-ssh-agent-forwarding) from the Bastion hosts.
-1. Select **Review and launch** followed by **Launch** if you're happy with your settings.
-1. Finally, acknowledge that you have access to the selected private key file or create a new one. Select **Launch Instances**.
-
-NOTE:
-Instead of storing configuration _and_ repository data on the root volume, you can also choose to add an additional EBS volume for repository storage. Follow the same guidance as above. See the [Amazon EBS pricing](https://aws.amazon.com/ebs/pricing/). We do not recommend using EFS as it may negatively impact the performance of GitLab. You can review the [relevant documentation](../../administration/nfs.md#avoid-using-cloud-based-file-systems) for more details.
-
-Now that we have our EC2 instance ready, follow the [documentation to install GitLab and set up Gitaly on its own server](../../administration/gitaly/configure_gitaly.md#run-gitaly-on-its-own-server). Perform the client setup steps from that document on the [GitLab instance we created](#install-gitlab) above.
-
-#### Add Support for Proxied SSL
-
-As we are terminating SSL at our [load balancer](#load-balancer), follow the steps at [Supporting proxied SSL](https://docs.gitlab.com/omnibus/settings/ssl/index.html#configure-a-reverse-proxy-or-load-balancer-ssl-termination) to configure this in `/etc/gitlab/gitlab.rb`.
-
-Remember to run `sudo gitlab-ctl reconfigure` after saving the changes to the `gitlab.rb` file.
-
-#### Fast lookup of authorized SSH keys
-
-The public SSH keys for users allowed to access GitLab are stored in `/var/opt/gitlab/.ssh/authorized_keys`. Typically we'd use shared storage so that all the instances are able to access this file when a user performs a Git action over SSH. Because we do not have shared storage in our setup, we update our configuration to authorize SSH users via indexed lookup in the GitLab database.
-
-Follow the instructions at [Set up fast SSH key lookup](../../administration/operations/fast_ssh_key_lookup.md#set-up-fast-lookup) to switch from using the `authorized_keys` file to the database.
-
-If you do not configure fast lookup, Git actions over SSH results in the following error:
-
-```shell
-Permission denied (publickey).
-fatal: Could not read from remote repository.
-
-Please make sure you have the correct access rights
-and the repository exists.
-```
-
-#### Configure host keys
-
-Ordinarily we would manually copy the contents (primary and public keys) of `/etc/ssh/` on the primary application server to `/etc/ssh` on all secondary servers. This prevents false man-in-the-middle-attack alerts when accessing servers in your cluster behind a load balancer.
-
-We automate this by creating static host keys as part of our custom AMI. As these host keys are also rotated every time an EC2 instance boots up, "hard coding" them into our custom AMI serves as a workaround.
-
-On your GitLab instance run the following:
-
-```shell
-sudo mkdir /etc/ssh_static
-sudo cp -R /etc/ssh/* /etc/ssh_static
-```
-
-In `/etc/ssh/sshd_config` update the following:
-
-```shell
-# HostKeys for protocol version 2
-HostKey /etc/ssh_static/ssh_host_rsa_key
-HostKey /etc/ssh_static/ssh_host_dsa_key
-HostKey /etc/ssh_static/ssh_host_ecdsa_key
-HostKey /etc/ssh_static/ssh_host_ed25519_key
-```
-
-#### Amazon S3 object storage
-
-Because we're not using NFS for shared storage, we use [Amazon S3](https://aws.amazon.com/s3/) buckets to store backups, artifacts, LFS objects, uploads, merge request diffs, container registry images, and more. Our documentation includes [instructions on how to configure object storage](../../administration/object_storage.md) for each of these data types, and other information about using object storage with GitLab.
-
-NOTE:
-Because we are using the [AWS IAM profile](#create-an-iam-role) we created earlier, be sure to omit the AWS access key and secret access key/value pairs when configuring object storage. Instead, use `'use_iam_profile' => true` in your configuration as shown in the object storage documentation linked above.
-
-Remember to run `sudo gitlab-ctl reconfigure` after saving the changes to the `gitlab.rb` file.
-
----
-
-That concludes the configuration changes for our GitLab instance. Next, we create a custom AMI based on this instance to use for our launch configuration and auto scaling group.
-
-### Log in for the first time
-
-Using the domain name you used when setting up [DNS for the load balancer](#configure-dns-for-load-balancer), you should now be able to visit GitLab in your browser.
-
-Depending on how you installed GitLab and if you did not change the password by any other means, the default password is either:
-
-- Your instance ID if you used the official GitLab AMI.
-- A randomly generated password stored for 24 hours in `/etc/gitlab/initial_root_password`.
-
-To change the default password, log in as the `root` user with the default password and [change it in the user profile](../../user/profile/user_passwords.md#change-your-password).
-
-When our [auto scaling group](#create-an-auto-scaling-group) spins up new instances, we are able to sign in with username `root` and the newly created password.
-
-### Create custom AMI
-
-On the EC2 dashboard:
-
-1. Select the `GitLab` instance we [created earlier](#install-gitlab).
-1. Select **Actions**, scroll down to **Image** and select **Create Image**.
-1. Give your image a name and description (we use `GitLab-Source` for both).
-1. Leave everything else as default and select **Create Image**
-
-Now we have a custom AMI that we use to create our launch configuration the next step.
-
-## Deploy GitLab inside an auto scaling group
-
-### Create a launch configuration
-
-From the EC2 dashboard:
-
-1. Select **Launch Configurations** from the left menu and select **Create launch configuration**.
-1. Select **My AMIs** from the left menu and select the `GitLab` custom AMI we created above.
-1. Select an instance type best suited for your needs (at least a `c5.xlarge`) and select **Configure details**.
-1. Enter a name for your launch configuration (we use `gitlab-ha-launch-config`).
-1. **Do not** check **Request Spot Instance**.
-1. From the **IAM Role** dropdown list, pick the `GitLabAdmin` instance role we [created earlier](#create-an-iam-ec2-instance-role-and-profile).
-1. Leave the rest as defaults and select **Add Storage**.
-1. The root volume is 8GiB by default and should be enough given that we do not store any data there. Select **Configure Security Group**.
-1. Check **Select and existing security group** and select the `gitlab-loadbalancer-sec-group` we created earlier.
-1. Select **Review**, review your changes, and select **Create launch configuration**.
-1. Acknowledge that you have access to the private key or create a new one. Select **Create launch configuration**.
-
-### Create an auto scaling group
-
-1. After the launch configuration is created, select **Create an Auto Scaling group using this launch configuration** to start creating the auto scaling group.
-1. Enter a **Group name** (we use `gitlab-auto-scaling-group`).
-1. For **Group size**, enter the number of instances you want to start with (we enter `2`).
-1. Select the `gitlab-vpc` from the **Network** dropdown list.
-1. Add both the private [subnets we created earlier](#subnets).
-1. Expand the **Advanced Details** section and check the **Receive traffic from one or more load balancers** option.
-1. From the **Classic Load Balancers** dropdown list, select the load balancer we created earlier.
-1. For **Health Check Type**, select **ELB**.
-1. We leave our **Health Check Grace Period** as the default `300` seconds. Select **Configure scaling policies**.
-1. Check **Use scaling policies to adjust the capacity of this group**.
-1. For this group we scale between 2 and 4 instances where one instance is added if CPU
-utilization is greater than 60% and one instance is removed if it falls
-to less than 45%.
-
-![Auto scaling group policies](img/policies.png)
-
-1. Finally, configure notifications and tags as you see fit, review your changes, and create the
-auto scaling group.
-
-As the auto scaling group is created, you see your new instances spinning up in your EC2 dashboard. You also see the new instances added to your load balancer. After the instances pass the heath check, they are ready to start receiving traffic from the load balancer.
-
-Because our instances are created by the auto scaling group, go back to your instances and terminate the [instance we created manually above](#install-gitlab). We only needed this instance to create our custom AMI.
-
-## Health check and monitoring with Prometheus
-
-Apart from Amazon's Cloudwatch which you can enable on various services,
-GitLab provides its own integrated monitoring solution based on Prometheus.
-For more information about how to set it up, see
-[GitLab Prometheus](../../administration/monitoring/prometheus/index.md).
-
-GitLab also has various [health check endpoints](../../administration/monitoring/health_check.md)
-that you can ping and get reports.
-
-## GitLab Runner
-
-If you want to take advantage of [GitLab CI/CD](../../ci/index.md), you have to
-set up at least one [runner](https://docs.gitlab.com/runner/).
-
-Read more on configuring an
-[autoscaling GitLab Runner on AWS](https://docs.gitlab.com/runner/configuration/runner_autoscale_aws/).
-
-## Backup and restore
-
-GitLab provides [a tool to back up](../../administration/backup_restore/index.md)
-and restore its Git data, database, attachments, LFS objects, and so on.
-
-Some important things to know:
-
-- The backup/restore tool **does not** store some configuration files, like secrets; you
- must [configure this yourself](../../administration/backup_restore/backup_gitlab.md#storing-configuration-files).
-- By default, the backup files are stored locally, but you can
- [backup GitLab using S3](../../administration/backup_restore/backup_gitlab.md#using-amazon-s3).
-- You can [exclude specific directories form the backup](../../administration/backup_restore/backup_gitlab.md#excluding-specific-directories-from-the-backup).
-
-### Backing up GitLab
-
-To back up GitLab:
-
-1. SSH into your instance.
-1. Take a backup:
-
- ```shell
- sudo gitlab-backup create
- ```
-
-NOTE:
-For GitLab 12.1 and earlier, use `gitlab-rake gitlab:backup:create`.
-
-### Restoring GitLab from a backup
-
-To restore GitLab, first review the [restore documentation](../../administration/backup_restore/index.md#restore-gitlab),
-and primarily the restore prerequisites. Then, follow the steps under the
-[Linux package installations section](../../administration/backup_restore/restore_gitlab.md#restore-for-linux-package-installations).
-
-## Updating GitLab
-
-GitLab releases a new version every month on the 22nd. Whenever a new version is
-released, you can update your GitLab instance:
-
-1. SSH into your instance
-1. Take a backup:
-
- ```shell
- sudo gitlab-backup create
- ```
-
-NOTE:
-For GitLab 12.1 and earlier, use `gitlab-rake gitlab:backup:create`.
-
-1. Update the repositories and install GitLab:
-
- ```shell
- sudo apt update
- sudo apt install gitlab-ee
- ```
-
-After a few minutes, the new version should be up and running.
-
-## Find official GitLab-created AMI IDs on AWS
-
-Read more on how to use [GitLab releases as AMIs](index.md#official-gitlab-releases-as-amis).
-
-## Conclusion
-
-In this guide, we went mostly through scaling and some redundancy options,
-your mileage may vary.
-
-Keep in mind that all solutions come with a trade-off between
-cost/complexity and uptime. The more uptime you want, the more complex the solution.
-And the more complex the solution, the more work is involved in setting up and
-maintaining it.
-
-Have a read through these other resources and feel free to
-[open an issue](https://gitlab.com/gitlab-org/gitlab/-/issues/new)
-to request additional material:
-
-- [Scaling GitLab](../../administration/reference_architectures/index.md):
- GitLab supports several different types of clustering.
-- [Geo replication](../../administration/geo/index.md):
- Geo is the solution for widely distributed development teams.
-- [Linux package](https://docs.gitlab.com/omnibus/) - Everything you must know
- about administering your GitLab instance.
-- [Add a license](../../administration/license.md):
- Activate all GitLab Enterprise Edition functionality with a license.
-- [Pricing](https://about.gitlab.com/pricing/): Pricing for the different tiers.
-
-## Troubleshooting
-
-### Instances are failing health checks
-
-If your instances are failing the load balancer's health checks, verify that they are returning a status `200` from the health check endpoint we configured earlier. Any other status, including redirects like status `302`, causes the health check to fail.
-
-You may have to set a password on the `root` user to prevent automatic redirects on the sign-in endpoint before health checks pass.
-
-### "The change you requested was rejected (422)"
-
-If you see this page when trying to set a password via the web interface, make sure `external_url` in `gitlab.rb` matches the domain you are making a request from, and run `sudo gitlab-ctl reconfigure` after making any changes to it.
-
-### Some job logs are not uploaded to object storage
-
-When the GitLab deployment is scaled up to more than one node, some job logs may not be uploaded to [object storage](../../administration/object_storage.md) properly. [Incremental logging is required](../../administration/object_storage.md#alternatives-to-file-system-storage) for CI to use object storage.
-
-Enable [incremental logging](../../administration/job_logs.md#enable-or-disable-incremental-logging) if it has not already been enabled.
+<!-- This redirect file can be deleted after <YYYY-MM-DD>. -->
+<!-- Redirects that point to other docs in the same project expire in three months. -->
+<!-- Redirects that point to docs in a different project or site (for example, link is not relative and starts with `https:`) expire in one year. -->
+<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/redirects.html --> \ No newline at end of file
diff --git a/doc/install/docker.md b/doc/install/docker.md
index ac15b5490ce..0ba41e06b65 100644
--- a/doc/install/docker.md
+++ b/doc/install/docker.md
@@ -35,7 +35,10 @@ to community resources (such as IRC or forums) to seek help from other users.
## Prerequisites
-Docker is required. See the [official installation documentation](https://docs.docker.com/get-docker/).
+To use the GitLab Docker images:
+
+- You must install Docker.
+- You must use a valid externally-accessible hostname. Do not use `localhost`.
## Set up the volumes location
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 68e69316f46..c8682fc154f 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -48,7 +48,7 @@ If the highest number stable branch is unclear, check the [GitLab blog](https://
| [Ruby](#2-ruby) | `3.0.x` | From GitLab 15.10, Ruby 3.0 is required. You must use the standard MRI implementation of Ruby. We love [JRuby](https://www.jruby.org/) and [Rubinius](https://github.com/rubinius/rubinius#the-rubinius-language-platform), but GitLab needs several Gems that have native extensions. |
| [RubyGems](#3-rubygems) | `3.4.x` | A specific RubyGems version is not fully needed, but it's recommended to update so you can enjoy some known performance improvements. |
| [Go](#4-go) | `1.20.x` | From GitLab 16.4, Go 1.20 or later is required. |
-| [Git](#git) | `2.41.x` | From GitLab 16.2, Git 2.41.x and later is required. You should use the [Git version provided by Gitaly](#git). |
+| [Git](#git) | `2.42.x` | From GitLab 16.5, Git 2.42.x and later is required. You should use the [Git version provided by Gitaly](#git). |
| [Node.js](#5-node) | `18.17.x` | From GitLab 16.3, Node.js 18.17 or later is required. |
## GitLab directory structure
diff --git a/doc/install/relative_url.md b/doc/install/relative_url.md
index 885dcba952e..07e1f150521 100644
--- a/doc/install/relative_url.md
+++ b/doc/install/relative_url.md
@@ -6,7 +6,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Install GitLab under a relative URL **(FREE SELF)**
-While we recommend to install GitLab on its own (sub)domain, sometimes
+While you should install GitLab on its own (sub)domain, sometimes
this is not possible due to a variety of reasons. In that case, GitLab can also
be installed under a relative URL, for example `https://example.com/gitlab`.
diff --git a/doc/install/requirements.md b/doc/install/requirements.md
index 81244594a59..d20a5ecc561 100644
--- a/doc/install/requirements.md
+++ b/doc/install/requirements.md
@@ -46,7 +46,6 @@ Memory requirements are dependent on the number of users and expected workload.
The following is the recommended minimum Memory hardware guidance for a handful of example GitLab user base sizes.
- **4 GB RAM** is the **required** minimum memory size and supports up to 500 users
- - Our [Memory Team](https://about.gitlab.com/handbook/engineering/development/enablement/data_stores/application_performance/) is working to reduce the memory requirement.
- 8 GB RAM supports up to 1000 users
- More users? Consult the [reference architectures page](../administration/reference_architectures/index.md)
@@ -227,12 +226,10 @@ optimal settings for your infrastructure.
### Puma threads
-The recommended number of threads is dependent on several factors, including total memory, and use
-of [legacy Rugged code](../administration/gitaly/index.md#direct-access-to-git-in-gitlab).
+The recommended number of threads is dependent on several factors, including total memory.
- If the operating system has a maximum 2 GB of memory, the recommended number of threads is `1`.
A higher value results in excess swapping, and decrease performance.
-- If legacy Rugged code is in use, the recommended number of threads is `1`.
- In all other cases, the recommended number of threads is `4`. We don't recommend setting this
higher, due to how [Ruby MRI multi-threading](https://en.wikipedia.org/wiki/Global_interpreter_lock)
works.
diff --git a/doc/integration/advanced_search/elasticsearch.md b/doc/integration/advanced_search/elasticsearch.md
index 986bdb9a667..ef756be3ba4 100644
--- a/doc/integration/advanced_search/elasticsearch.md
+++ b/doc/integration/advanced_search/elasticsearch.md
@@ -972,6 +972,15 @@ For the steps below, consider the entry of `sidekiq['routing_rules']`:
At least one process in `sidekiq['queue_groups']` has to include the `mailers` queue, otherwise mailers jobs are not processed at all.
+NOTE:
+Routing rules (`sidekiq['routing_rules']`) must be the same across all GitLab nodes (especially GitLab Rails and Sidekiq nodes).
+
+WARNING:
+When starting multiple processes, the number of processes cannot exceed the number of CPU
+cores you want to dedicate to Sidekiq. Each Sidekiq process can use only one CPU core, subject
+to the available workload and concurrency settings. For more details, see how to
+[run multiple Sidekiq processes](../../administration/sidekiq/extra_sidekiq_processes.md).
+
### Single node, two processes
To create both an indexing and a non-indexing Sidekiq process in one node:
@@ -998,12 +1007,12 @@ To create both an indexing and a non-indexing Sidekiq process in one node:
1. Save the file and [reconfigure GitLab](../../administration/restart_gitlab.md)
for the changes to take effect.
+1. On all other Rails and Sidekiq nodes, ensure that `sidekiq['routing_rules']` is the same as above.
+1. Run the Rake task to [migrate existing jobs](../../administration/sidekiq/sidekiq_job_migration.md):
-WARNING:
-When starting multiple processes, the number of processes cannot exceed the number of CPU
-cores you want to dedicate to Sidekiq. Each Sidekiq process can use only one CPU core, subject
-to the available workload and concurrency settings. For more details, see how to
-[run multiple Sidekiq processes](../../administration/sidekiq/extra_sidekiq_processes.md).
+NOTE:
+It is important to run the Rake task immediately after reconfiguring GitLab.
+After reconfiguring GitLab, existing jobs are not processed until the Rake task starts to migrate the jobs.
### Two nodes, one process for each
@@ -1035,6 +1044,8 @@ for the changes to take effect.
```ruby
sidekiq['enable'] = true
+ sidekiq['queue_selector'] = false
+
sidekiq['routing_rules'] = [
["feature_category=global_search", "global_search"],
["*", "default"],
@@ -1048,10 +1059,18 @@ for the changes to take effect.
sidekiq['max_concurrency'] = 20
```
- to set up a non-indexing Sidekiq process.
-
+1. On all other Rails and Sidekiq nodes, ensure that `sidekiq['routing_rules']` is the same as above.
1. Save the file and [reconfigure GitLab](../../administration/restart_gitlab.md)
for the changes to take effect.
+1. Run the Rake task to [migrate existing jobs](../../administration/sidekiq/sidekiq_job_migration.md):
+
+ ```shell
+ sudo gitlab-rake gitlab:sidekiq:migrate_jobs:retry gitlab:sidekiq:migrate_jobs:schedule gitlab:sidekiq:migrate_jobs:queued
+ ```
+
+NOTE:
+It is important to run the Rake task immediately after reconfiguring GitLab.
+After reconfiguring GitLab, existing jobs are not processed until the Rake task starts to migrate the jobs.
## Reverting to Basic Search
diff --git a/doc/integration/advanced_search/elasticsearch_troubleshooting.md b/doc/integration/advanced_search/elasticsearch_troubleshooting.md
index df1e1f49083..1531e01577f 100644
--- a/doc/integration/advanced_search/elasticsearch_troubleshooting.md
+++ b/doc/integration/advanced_search/elasticsearch_troubleshooting.md
@@ -519,3 +519,13 @@ unexpectedly high `buff/cache` usage.
When you reindex, you might get a `Couldn't load task status` error. A `sliceId must be greater than 0 but was [-1]` error might also appear on the Elasticsearch host. As a workaround, consider [reindexing from scratch](../../integration/advanced_search/elasticsearch_troubleshooting.md#last-resort-to-recreate-an-index) or upgrading to GitLab 16.3.
For more information, see [issue 422938](https://gitlab.com/gitlab-org/gitlab/-/issues/422938).
+
+## Migration `BackfillProjectPermissionsInBlobs` has been halted in GitLab 15.11
+
+In GitLab 15.11, it is possible for the `BackfillProjectPermissionsInBlobs` migration to be halted with the following error message in the `elasticsearch.log`:
+
+```shell
+migration has failed with NoMethodError:undefined method `<<' for nil:NilClass, no retries left
+```
+
+If `BackfillProjectPermissionsInBlobs` is the only halted migration, you can upgrade to the latest patch version of GitLab 16.0, which includes [the fix](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/118494). Otherwise, you can ignore the error as it will not affect the current functionality of advanced search.
diff --git a/doc/integration/jenkins.md b/doc/integration/jenkins.md
index 260bb3f7108..b90ae3c3b79 100644
--- a/doc/integration/jenkins.md
+++ b/doc/integration/jenkins.md
@@ -157,6 +157,7 @@ If you cannot [provide GitLab with your Jenkins server URL and authentication in
- [GitLab Jenkins Integration](https://about.gitlab.com/solutions/jenkins/)
- [How to set up Jenkins on your local machine](../development/integrations/jenkins.md)
- [How to migrate from Jenkins to GitLab CI/CD](../ci/migration/jenkins.md)
+- [Jenkins to GitLab: The ultimate guide to modernizing your CI/CD environment](https://about.gitlab.com/blog/2023/11/01/jenkins-gitlab-ultimate-guide-to-modernizing-cicd-environment/?utm_campaign=devrel&utm_source=twitter&utm_medium=social&utm_budget=devrel)
## Troubleshooting
diff --git a/doc/integration/jira/connect-app.md b/doc/integration/jira/connect-app.md
index 4f0adb2771a..78cfc406d19 100644
--- a/doc/integration/jira/connect-app.md
+++ b/doc/integration/jira/connect-app.md
@@ -107,9 +107,7 @@ After you connect the GitLab for Jira Cloud app, you might get this error:
Failed to link group. Please try again.
```
-`403` status code is returned if:
+`403` status code is returned if the user information cannot be fetched from Jira due to insufficient permissions.
-- The user information cannot be fetched from Jira.
-- The authenticated Jira user does not have [site administrator](https://support.atlassian.com/user-management/docs/give-users-admin-permissions/#Make-someone-a-site-admin) access.
-
-To resolve this issue, ensure the authenticated user is a Jira site administrator and try again.
+To resolve this issue, ensure that the Jira user that installs and configures the GitLab for Jira Cloud app meets certain
+[requirements](../../administration/settings/jira_cloud_app.md#jira-user-requirements).
diff --git a/doc/integration/jira/development_panel.md b/doc/integration/jira/development_panel.md
index 02838239156..70e3534a32b 100644
--- a/doc/integration/jira/development_panel.md
+++ b/doc/integration/jira/development_panel.md
@@ -49,6 +49,8 @@ You can [view GitLab activity for a Jira issue](https://support.atlassian.com/ji
in the Jira development panel by referring to the Jira issue by ID in GitLab. The information displayed in the development panel
depends on where you mention the Jira issue ID in GitLab.
+For the [GitLab for Jira Cloud app](connect-app.md), the following information is displayed.
+
| GitLab: where you mention the Jira issue ID | Jira development panel: what information is displayed |
|------------------------------------------------|-------------------------------------------------------|
| Merge request title or description | Link to the merge request<br>Link to the deployment<br>Link to the pipeline through merge request title<br>Link to the pipeline through merge request description <sup>1</sup><br>Link to the branch <sup>2</sup><br>Reviewer information and approval status <sup>3</sup> |
diff --git a/doc/integration/jira/issues.md b/doc/integration/jira/issues.md
index ae4b726327c..f6716f49ea5 100644
--- a/doc/integration/jira/issues.md
+++ b/doc/integration/jira/issues.md
@@ -117,6 +117,9 @@ For example, use any of these trigger words to close the Jira issue `PROJECT-1`:
The commit or merge request must target your project's [default branch](../../user/project/repository/branches/default.md).
You can change your project's default branch in [project settings](../../user/project/settings/index.md).
+When your branch name matches the Jira issue ID, `Closes <JIRA-ID>` is automatically appended to your existing merge request template.
+If you do not want to close the issue, [disable automatic issue closing](../../user/project/issues/managing_issues.md#disable-automatic-issue-closing).
+
### Use case for closing issues
Consider this example:
diff --git a/doc/integration/kerberos.md b/doc/integration/kerberos.md
index 77d70010aa5..a01d31421ec 100644
--- a/doc/integration/kerberos.md
+++ b/doc/integration/kerberos.md
@@ -4,7 +4,7 @@ group: Authentication
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"
---
-# Use Kerberos as an OAuth 2.0 authentication provider **(PREMIUM SELF)**
+# Use Kerberos as an OAuth 2.0 authentication provider **(FREE SELF)**
GitLab can integrate with [Kerberos](https://web.mit.edu/kerberos/) as an authentication mechanism.
diff --git a/doc/integration/mattermost/index.md b/doc/integration/mattermost/index.md
index 73f3140db2b..c8a58f0692f 100644
--- a/doc/integration/mattermost/index.md
+++ b/doc/integration/mattermost/index.md
@@ -338,6 +338,7 @@ Below is a list of Mattermost version changes for GitLab 14.0 and later:
| GitLab version | Mattermost version | Notes |
| :------------- | :----------------- | ---------------------------------------------------------------------------------------- |
+| 16.6 | 9.1 | |
| 16.5 | 9.0 | |
| 16.4 | 8.1 | |
| 16.3 | 8.0 | |
diff --git a/doc/integration/oauth2_generic.md b/doc/integration/oauth2_generic.md
index fa65020a4dc..6bcecffaeda 100644
--- a/doc/integration/oauth2_generic.md
+++ b/doc/integration/oauth2_generic.md
@@ -6,6 +6,9 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Use Generic OAuth2 gem as an OAuth 2.0 authentication provider **(FREE SELF)**
+NOTE:
+If your provider supports the OpenID specification, you should use [`omniauth-openid-connect`](../administration/auth/oidc.md) as your authentication provider.
+
The [`omniauth-oauth2-generic` gem](https://gitlab.com/satorix/omniauth-oauth2-generic) allows single sign-on (SSO) between GitLab
and your OAuth 2.0 provider, or any OAuth 2.0 provider compatible with this gem.
diff --git a/doc/integration/shibboleth.md b/doc/integration/shibboleth.md
index bfb75eba402..f30f073bf08 100644
--- a/doc/integration/shibboleth.md
+++ b/doc/integration/shibboleth.md
@@ -4,7 +4,7 @@ group: Authentication
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
-# Use Shibboleth as an OAuth 2.0 authentication provider **(FREE ALL)**
+# Use Shibboleth as an OAuth 2.0 authentication provider **(FREE SELF)**
NOTE:
Use the [GitLab SAML integration](saml.md) to integrate specific Shibboleth identity providers (IdPs). For Shibboleth federation support (Discovery Service), use this document.
diff --git a/doc/operations/feature_flags.md b/doc/operations/feature_flags.md
index 5fd497a79e1..fe21f0db1c7 100644
--- a/doc/operations/feature_flags.md
+++ b/doc/operations/feature_flags.md
@@ -24,8 +24,7 @@ To contribute to the development of the GitLab product, view
## How it works
-GitLab uses [Unleash](https://github.com/Unleash/unleash), a feature
-toggle service.
+GitLab offers an [Unleash](https://github.com/Unleash/unleash)-compatible API for feature flags.
By enabling or disabling a flag in GitLab, your application
can determine which features to enable or disable.
@@ -76,10 +75,9 @@ is 200. For GitLab SaaS, the maximum number is determined by [tier](https://abou
You can apply a feature flag strategy across multiple environments, without defining
the strategy multiple times.
-GitLab feature flags use [Unleash](https://docs.getunleash.io/) as the feature flag
-engine. In Unleash, there are [strategies](https://docs.getunleash.io/reference/activation-strategies)
-for granular feature flag controls. GitLab feature flags can have multiple strategies,
-and the supported strategies are:
+GitLab feature flags are based on [Unleash](https://docs.getunleash.io/). In Unleash, there are
+[strategies](https://docs.getunleash.io/reference/activation-strategies) for granular feature
+flag controls. GitLab feature flags can have multiple strategies, and the supported strategies are:
- [All users](#all-users)
- [Percent of Users](#percent-of-users)
@@ -372,7 +370,11 @@ end
### Unleash Proxy example
As of [Unleash Proxy](https://docs.getunleash.io/reference/unleash-proxy) version
-0.2, the proxy is compatible with feature flags. To run a Docker container to
+0.2, the proxy is compatible with feature flags.
+
+You should use Unleash Proxy for production on GitLab.com. See the [performance note](#maximum-supported-clients-in-application-nodes) for details.
+
+To run a Docker container to
connect to your project's feature flags, run the following command:
```shell
@@ -418,10 +420,8 @@ Read [How it works](#how-it-works) section before diving into the details.
### Maximum supported clients in application nodes
-GitLab accepts client requests as much as possible until it hits the [rate limiting](../security/rate_limits.md).
-At the moment, the feature flag API falls into **Unauthenticated traffic (from a given IP address)**
-in the [GitLab.com specific limits](../user/gitlab_com/index.md),
-so it's **500 requests per minute**.
+GitLab accepts as many client requests as possible until it hits the [rate limit](../security/rate_limits.md).
+The feature flag API is considered **Unauthenticated traffic (from a given IP address)**. For GitLab.com, see the [GitLab.com specific limits](../user/gitlab_com/index.md).
The polling rate is configurable in SDKs. Provided that all clients are requesting from the same IP:
@@ -429,7 +429,8 @@ The polling rate is configurable in SDKs. Provided that all clients are requesti
- Request once per 15 sec ... 125 clients can be supported.
For applications looking for more scalable solution, you should use [Unleash Proxy](#unleash-proxy-example).
-This proxy server sits between the server and clients. It requests to the server as a behalf of the client groups,
+On GitLab.com, you should use Unleash Proxy to reduce the chance of being rate limited across endpoints.
+This proxy server sits between the server and clients. It makes requests to the server on behalf of the client groups,
so the number of outbound requests can be greatly reduced.
There is also an [issue](https://gitlab.com/gitlab-org/gitlab/-/issues/295472) to give more
diff --git a/doc/operations/incident_management/manage_incidents.md b/doc/operations/incident_management/manage_incidents.md
index ba21a210359..1b48de9e478 100644
--- a/doc/operations/incident_management/manage_incidents.md
+++ b/doc/operations/incident_management/manage_incidents.md
@@ -114,7 +114,7 @@ To view an incident's [details page](incidents.md#incident-details), select it f
Whether you can view an incident depends on the [project visibility level](../../user/public_access.md) and
the incident's confidentiality status:
-- Public project and a non-confidential incident: You don't have to be a member of the project.
+- Public project and a non-confidential incident: Anyone can view the incident.
- Private project and non-confidential incident: You must have at least the Guest role for the project.
- Confidential incident (regardless of project visibility): You must have at least the Reporter role for the project.
diff --git a/doc/policy/experiment-beta-support.md b/doc/policy/experiment-beta-support.md
index a87a72d7910..41ffaec3aa4 100644
--- a/doc/policy/experiment-beta-support.md
+++ b/doc/policy/experiment-beta-support.md
@@ -13,7 +13,7 @@ All other features are considered to be Generally Available (GA).
## Experiment
Support is not provided for features listed as "Experimental" or "Alpha" or any similar designation. Issues regarding such features should be opened in the GitLab issue tracker. Teams should release features as GA from the start unless there are strong reasons to release them as Experiment or Beta versions first.
-All Experimental features must [initiate Production Readiness Review](https://about.gitlab.com/handbook/engineering/infrastructure/production/readiness/#process) and complete the [experiment section in the readiness template](https://gitlab.com/gitlab-com/gl-infra/readiness/-/blob/master/.gitlab/issue_templates/production_readiness.md#experiment).
+All Experimental features that [meet the review criteria](https://about.gitlab.com/handbook/engineering/infrastructure/production/readiness/#criteria-for-starting-a-production-readiness-review) must [initiate Production Readiness Review](https://about.gitlab.com/handbook/engineering/infrastructure/production/readiness/#process) and complete the [experiment section in the readiness template](https://gitlab.com/gitlab-com/gl-infra/readiness/-/blob/master/.gitlab/issue_templates/production_readiness.md#experiment).
Experimental features are:
@@ -36,7 +36,7 @@ Experimental features are:
## Beta
Commercially-reasonable efforts are made to provide limited support for features designated as "Beta," with the expectation that issues require extra time and assistance from development to troubleshoot.
-All Beta features must complete all sections up to and including the [beta section in the readiness template](https://gitlab.com/gitlab-com/gl-infra/readiness/-/blob/master/.gitlab/issue_templates/production_readiness.md#beta) by following the [Production Readiness Review process](https://about.gitlab.com/handbook/engineering/infrastructure/production/readiness/#process).
+All Beta features that [meet the review criteria](https://about.gitlab.com/handbook/engineering/infrastructure/production/readiness/#criteria-for-starting-a-production-readiness-review) must complete all sections up to and including the [beta section in the readiness template](https://gitlab.com/gitlab-com/gl-infra/readiness/-/blob/master/.gitlab/issue_templates/production_readiness.md#beta) by following the [Production Readiness Review process](https://about.gitlab.com/handbook/engineering/infrastructure/production/readiness/#process).
Beta features are:
@@ -56,7 +56,7 @@ Beta features are:
## Generally Available (GA)
-Generally Available features must complete the [Production Readiness Review](https://about.gitlab.com/handbook/engineering/infrastructure/production/readiness) and complete all sections up to and including the [GA section in the readiness template](https://gitlab.com/gitlab-com/gl-infra/readiness/-/blob/master/.gitlab/issue_templates/production_readiness.md#general-availability).
+Generally Available features that [meet the review criteria](https://about.gitlab.com/handbook/engineering/infrastructure/production/readiness/#criteria-for-starting-a-production-readiness-review) must complete the [Production Readiness Review](https://about.gitlab.com/handbook/engineering/infrastructure/production/readiness) and complete all sections up to and including the [GA section in the readiness template](https://gitlab.com/gitlab-com/gl-infra/readiness/-/blob/master/.gitlab/issue_templates/production_readiness.md#general-availability).
GA features are:
diff --git a/doc/security/email_verification.md b/doc/security/email_verification.md
index d87f43dec6a..67d8764a118 100644
--- a/doc/security/email_verification.md
+++ b/doc/security/email_verification.md
@@ -18,6 +18,8 @@ you must verify your identity or reset your password to sign in to GitLab.
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
For a demo, see [Require email verification - demo](https://www.youtube.com/watch?v=wU6BVEGB3Y0).
+On GitLab.com, if you don't receive a verification email, select **Resend Code** before you contact the support team.
+
## Accounts without two-factor authentication (2FA)
An account is locked when either:
@@ -36,10 +38,12 @@ To unlock your account, sign in and enter the verification code. You can also
## Accounts with 2FA or OAuth
-An account is locked when there are three or more failed sign-in attempts.
+An account is locked when there are ten or more failed sign-in attempts, or more than the
+amount defined in the [configurable locked user policy](unlock_user.md#self-managed-users).
-Accounts with 2FA or OAuth are automatically unlocked after 30 minutes. To unlock an account manually,
-reset your password.
+Accounts with 2FA or OAuth are automatically unlocked after ten minutes, or more than the
+amount defined in the [configurable locked user policy](unlock_user.md#self-managed-users).
+To unlock an account manually, reset your password.
## Related topics
diff --git a/doc/security/reset_user_password.md b/doc/security/reset_user_password.md
index d79ede70abd..9835509897e 100644
--- a/doc/security/reset_user_password.md
+++ b/doc/security/reset_user_password.md
@@ -168,3 +168,7 @@ attempt to fix this issue in a Rails console. For example, if a new `root` passw
The password might be too short, too weak, or not meet complexity
requirements. Ensure the password you are attempting to set meets all
[password requirements](../user/profile/user_passwords.md#password-requirements).
+
+### Expired password
+
+You might not be able to reset a user's expired password due to the [Password Expired error on Git Fetch via SSH for LDAP users](../topics/git/troubleshooting_git.md#password-expired-error-on-git-fetch-via-ssh-for-ldap-user).
diff --git a/doc/security/token_overview.md b/doc/security/token_overview.md
index c56fe0b9260..82e16694470 100644
--- a/doc/security/token_overview.md
+++ b/doc/security/token_overview.md
@@ -222,6 +222,39 @@ This table shows available scopes per token. Scopes can be limited further on to
1. Runner registration and authentication token don't provide direct access to repositories, but can be used to register and authenticate a new runner that may execute jobs which do have access to the repository
1. Limited to certain [endpoints](../ci/jobs/ci_job_token.md).
+## Token prefixes
+
+The following tables show the prefixes for each type of token where applicable.
+
+### GitLab tokens
+
+| Token name | Prefix |
+|-----------------------------------|--------------------|
+| Personal access token | `glpat-` |
+| OAuth Application Secret | `gloas-` |
+| Impersonation token | Not applicable. |
+| Project access token | Not applicable. |
+| Group access token | Not applicable. |
+| Deploy token | Not applicable. |
+| Deploy key | Not applicable. |
+| Runner registration token | Not applicable. |
+| Runner authentication token | `glrt-` |
+| Job token | Not applicable. |
+| Trigger token | `glptt-` |
+| Legacy runner registration token | GR1348941 |
+| Feed token | `glft-` |
+| Incoming mail token | `glimt-` |
+| GitLab Agent for Kubernetes token | `glagent-` |
+| GitLab session cookies | `_gitlab_session=` |
+
+### External system tokens
+
+| Token name | Prefix |
+|-----------------|-----------------|
+| Omamori tokens | `omamori_pat_` |
+| AWS credentials | `AKIA` |
+| GCP credentials | Not applicable. |
+
## Security considerations
1. Treat access tokens like passwords and keep them secure.
diff --git a/doc/security/unlock_user.md b/doc/security/unlock_user.md
index fe10274ce5a..8184bdfdd8c 100644
--- a/doc/security/unlock_user.md
+++ b/doc/security/unlock_user.md
@@ -18,10 +18,14 @@ By default, users are locked after 10 failed sign-in attempts. These users remai
In GitLab 16.5 and later, administrators can [use the API](../api/settings.md#list-of-settings-that-can-be-accessed-via-api-calls) to configure:
-- The number of failed sign-in attempts that locks a user.
-- The time period in minutes that the locked user is locked for, after the maximum number of failed sign-in attempts is reached.
+- The number of failed sign-in attempts that locks a user (`max_login_attempts`).
+- The time period in minutes that the locked user is locked for, after the maximum number of failed sign-in attempts is reached (`failed_login_attempts_unlock_period_in_minutes`).
-For example, an administrator can configure that five failed sign-in attempts locks a user, and that user will be locked for 60 minutes.
+For example, an administrator can configure that five failed sign-in attempts locks a user, and that user will be locked for 60 minutes, with the following API call:
+
+```shell
+curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/application/settings?max_login_attempts=5&failed_login_attempts_unlock_period_in_minutes=60"
+```
## GitLab.com users
diff --git a/doc/solutions/cloud/aws/gitaly_sre_for_aws.md b/doc/solutions/cloud/aws/gitaly_sre_for_aws.md
new file mode 100644
index 00000000000..318316b95b8
--- /dev/null
+++ b/doc/solutions/cloud/aws/gitaly_sre_for_aws.md
@@ -0,0 +1,91 @@
+---
+stage: Solutions Architecture
+group: Solutions Architecture
+info: This page is owned by the Solutions Architecture team.
+description: Doing SRE for Gitaly instances on AWS.
+---
+
+# SRE Considerations for Gitaly on AWS **(FREE SELF)**
+
+## Gitaly SRE considerations
+
+Gitaly is an embedded service for Git Repository Storage. Gitaly and Gitaly Cluster have been engineered by GitLab to overcome fundamental challenges with horizontal scaling of the open source Git binaries that must be used on the service side of GitLab. Here is in-depth technical reading on the topic:
+
+### Why Gitaly was built
+
+If you would like to understand the underlying rationale on why GitLab had to invest in creating Gitaly, read the following minimal list of topics:
+
+- [Git characteristics that make horizontal scaling difficult](https://gitlab.com/gitlab-org/gitaly/-/blob/master/doc/DESIGN.md#git-characteristics-that-make-horizontal-scaling-difficult)
+- [Git architectural characteristics and assumptions](https://gitlab.com/gitlab-org/gitaly/-/blob/master/doc/DESIGN.md#git-architectural-characteristics-and-assumptions)
+- [Affects on horizontal compute architecture](https://gitlab.com/gitlab-org/gitaly/-/blob/master/doc/DESIGN.md#affects-on-horizontal-compute-architecture)
+- [Evidence to back building a new horizontal layer to scale Git](https://gitlab.com/gitlab-org/gitaly/-/blob/master/doc/DESIGN.md#evidence-to-back-building-a-new-horizontal-layer-to-scale-git)
+
+### Gitaly and Praefect elections
+
+As part of Gitaly cluster consistency, Praefect nodes must occasionally vote on what data copy is the most accurate. This requires an uneven number of Praefect nodes to avoid stalemates. This means that for HA, Gitaly and Praefect require a minimum of three nodes.
+
+### Gitaly performance monitoring
+
+Complete performance metrics should be collected for Gitaly instances for identification of bottlenecks, as they could have to do with disk IO, network IO, or memory.
+
+### Gitaly performance guidelines
+
+Gitaly functions as the primary Git Repository Storage in GitLab. However, it's not a streaming file server. It also does a lot of demanding computing work, such as preparing and caching Git packfiles which informs some of the performance recommendations below.
+
+NOTE:
+All recommendations are for production configurations, including performance testing. For test configurations, like training or functional testing, you can use less expensive options. However, you should adjust or rebuild if performance is an issue.
+
+#### Overall recommendations
+
+- Production-grade Gitaly must be implemented on instance compute due to all of the above and below characteristics.
+- Never use [burstable instance types](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/burstable-performance-instances.html) (such as `t2`, `t3`, `t4g`) for Gitaly.
+- Always use at least the [AWS Nitro generation of instances](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-types.html#ec2-nitro-instances) to ensure many of the below concerns are automatically handled.
+- Use Amazon Linux 2 to ensure that all [AWS oriented hardware and OS optimizations](https://aws.amazon.com/amazon-linux-2/faqs/) are maximized without additional configuration or SRE management.
+
+#### CPU and memory recommendations
+
+- The general GitLab Gitaly node recommendations for CPU and Memory assume relatively even loading across repositories. GitLab Performance Tool (GPT) testing of any non-characteristic repositories and/or SRE monitoring of Gitaly metrics may inform when to choose memory and/or CPU higher than general recommendations.
+
+**To accommodate:**
+
+- Git packfile operations are memory and CPU intensive.
+- If repository commit traffic is dense, large, or very frequent, then more CPU and Memory are required to handle the load. Patterns such as storing binaries and/or busy or large monorepos are examples that can cause high loading.
+
+#### Disk I/O recommendations
+
+- Use only SSD storage and the [class of Elastic Block Store (EBS) storage](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-volume-types.html) that suites your durability and speed requirements.
+- When not using provisioned EBS IO, EBS volume size determines the I/O level, so provisioning volumes that are much larger than needed can be the least expensive way to improve EBS IO.
+- If Gitaly performance monitoring shows signs of disk stress then one of the provisioned IOPS levels can be chosen. EBS IOPS levels also have enhanced durability which may be appealing for some implementations aside from performance considerations.
+
+**To accommodate:**
+
+- Gitaly storage is expected to be local (not NFS of any type including EFS).
+- Gitaly servers also need disk space for building and caching Git packfiles. This is above and beyond the permanent storage of your Git Repositories.
+- Git packfiles are cached in Gitaly. Creation of packfiles in temporary disk benefits from fast disk, and disk caching of packfiles benefits from ample disk space.
+
+#### Network I/O recommendations
+
+- Use only instance types [from the list of ones that support Elastic Network Adapter (ENA) advanced networking](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-types.html#instance-type-summary-table) to ensure that cluster replication latency is not due to instance level network I/O bottlenecks.
+- Choose instances with sizes with more than 10 Gbps - but only if needed and only when having proven a node level network bottleneck with monitoring and/or stress testing.
+
+**To accommodate:**
+
+- Gitaly nodes do the main work of streaming repositories for push and pull operations (to add development endpoints, and to CI/CD).
+- Gitaly servers need reasonable low latency between cluster nodes and with Praefect services in order for the cluster to maintain operational and data integrity.
+- Gitaly nodes should be selected with network bottleneck avoidance as a primary consideration.
+- Gitaly nodes should be monitored for network saturation.
+- Not all networking issues can be solved through optimizing the node level networking:
+ - Gitaly cluster node replication depends on all networking between nodes.
+ - Gitaly networking performance to pull and push endpoints depends on all networking in between.
+
+### AWS Gitaly backup
+
+Due to the nature of how Praefect tracks the replication metadata of Gitaly disk information, the best backup method is [the official backup and restore Rake tasks](../../../administration/backup_restore/index.md).
+
+### AWS Gitaly recovery
+
+Gitaly Cluster does not support snapshot backups as these can cause issues where the Praefect database becomes out of syn with the disk storage. Due to the nature of how Praefect rebuilds the replication metadata of Gitaly disk information during a restore, the best recovery method is [the official backup and restore Rake tasks](../../../administration/backup_restore/index.md).
+
+### Gitaly long term management
+
+Gitaly node disk sizes must be monitored and increased to accommodate Git repository growth and Gitaly temporary and caching storage needs. The storage configuration on all nodes should be kept identical.
diff --git a/doc/solutions/cloud/aws/gitlab_aws_integration.md b/doc/solutions/cloud/aws/gitlab_aws_integration.md
new file mode 100644
index 00000000000..ba0b9717562
--- /dev/null
+++ b/doc/solutions/cloud/aws/gitlab_aws_integration.md
@@ -0,0 +1,103 @@
+---
+stage: Solutions Architecture
+group: Solutions Architecture
+info: This page is owned by the Solutions Architecture team.
+description: "Integrations Solutions Index for GitLab and AWS."
+---
+
+# Integrate with AWS
+
+Learn how to integrate GitLab and AWS.
+
+This content is intended for GitLab team members as well as members of the wider community.
+
+This page attempts to index the ways in which GitLab can integrate with AWS. It does so whether the integration is the result of configuring general functionality, was built in to AWS or GitLab or is provided as a solution.
+
+| Text Tag | Configuration / Built / Solution | Support/Maintenance |
+| -------------------- | ------------------------------------------------------------ | ------------------- |
+| `[AWS Configuration]` | Integration via Configuring Existing AWS Functionality | AWS |
+| `[GitLab Configuration]` | Integration via Configuring Existing GitLab Functionality | GitLab |
+| `[AWS Built]` | Built into AWS by Product Team to Address AWS Integration | AWS |
+| `[GitLab Built]` | Built into GitLab by Product Team to Address AWS Integration | GitLab |
+| `[AWS Solution]` | Built as Solution Example by AWS or AWS Partners | Community/Example |
+| `[GitLab Solution]` | Built as Solution Example by GitLab or GitLab Partners | Community/Example |
+| `[CI Solution]` | Built, at least in part, using GitLab CI and therefore <br />more customer customizable. | Items tagged `[CI Solution will]` <br />also carry one of the other tags <br />that indicates the maintenance status. |
+
+## Integrations For Development Activities
+
+### SCM Integrations
+
+- **AWS CodeStar Connections** - enables SCM connections to multiple AWS Services. **Currently for GitLab.com SaaS only**. [Configure GitLab](https://docs.aws.amazon.com/dtconsole/latest/userguide/connections-create-gitlab.html). [Supported Providers](https://docs.aws.amazon.com/dtconsole/latest/userguide/supported-versions-connections.html). [Supported AWS Services](https://docs.aws.amazon.com/dtconsole/latest/userguide/integrations-connections.html) - each one may have to make updates to support GitLab, so here is the subset that currently support GitLab `[AWS Built]`
+ - [AWS CodePipeline Integration](https://docs.aws.amazon.com/codepipeline/latest/userguide/connections-gitlab.html) - use GitLab as source for CodePipeline. `[AWS Built]`
+ - **AWS CodeBuild Integration** - indirectly through CodePipeline support. `[AWS Built]`
+ - **Amazon CodeWhisperer Customization Capability** [can connect to a GitLab repo](https://aws.amazon.com/blogs/aws/new-customization-capability-in-amazon-codewhisperer-generates-even-better-suggestions-preview/). `[AWS Built]`
+ - **AWS Service Catalog** directly inherits CodeStar Connections, there is not any specific documentation about GitLab since it just uses any GitLab CodeStar Connection that has been created in the account. `[AWS Built]`
+ - **AWS Proton** directly inherits CodeStar Connections, there is not any specific documentation about GitLab since it just uses any GitLab CodeStar Connection that has been created in the account. `[AWS Built]`
+ - **AWS Glue Notebook Jobs** directly inherit CodeStar Connections, there is not any specific documentation about GitLab since it just uses any GitLab CodeStar Connection that has been created in the account. `[AWS Built]`
+ - **Amazon SageMaker MLOps Projects** are done in CodePipeline and so directly inherit CodeStar Connections ([as noted here](https://docs.aws.amazon.com/sagemaker/latest/dg/sagemaker-projects-walkthrough-3rdgit.html#sagemaker-proejcts-walkthrough-connect-3rdgit)), there is not any specific documentation about GitLab since it just uses any GitLab CodeStar Connection that has been created in the account. `[AWS Built]`
+ - **Amazon SageMaker Notebooks** [allow Git repositories to be specified by the Git clone URL](https://docs.aws.amazon.com/sagemaker/latest/dg/nbi-git-resource.html) and configuration of a secret - so GitLab is configurable. `[AWS Configuration]`
+ - **AWS CloudFormation** publishing of public extensions - **not yet supported**. `[AWS Built]`
+ - **Amazon CodeGuru Reviewer Repositories** - **not yet supported**. `[AWS Built]`
+- [GitLab Push Mirroring to CodeCommit](../../../user/project/repository/mirror/push.md#set-up-a-push-mirror-from-gitlab-to-aws-codecommit) Workaround enables GitLab repositories to leverage CodePipeline SCM Triggers. GitLab can already leverage S3 and Container Triggers for CodePipeline. **Still required for Self-Managed and Dedicated for the time being.** `[GitLab Configuration]`
+
+### CI Integrations
+
+- **Direct CI Integrations That Use Keys, IAM or OIDC/JWT to Authenticate to AWS Services from GitLab Runners**
+ - **Amazon CodeGuru Reviewer CI workflows using GitLab CI** - can be done, not yet documented. `[AWS Solution]` `[CI Solution]`
+ - [Amazon CodeGuru Secure Scanning using GitLab CI](https://docs.aws.amazon.com/codeguru/latest/security-ug/get-started-gitlab.html) `[AWS Solution]` `[CI Solution]`
+
+### CD and Operations Integrations
+
+- **AWS CodeDeploy Integration** - indirectly through CodePipeline support. `[AWS Built]`
+- [Integrate EKS clusters for application deployment](../../../user/infrastructure/clusters/connect/new_eks_cluster.md). `[GitLab Built]`
+
+## Solutions For Specific Development Frameworks and Ecosystems
+
+Generally solutions demonstrate end-to-end capabilities for the development framework - leveraging all relevant integration techniques to show the art of maximum value for using GitLab and AWS together.
+
+### Serverless Development
+
+- [Serverless Framework Deployment to AWS with GitLab Serverless SAST Scanning and Managed DevOps Environments](https://gitlab.com/guided-explorations/aws/serverless/serverless-framework-aws) - working example code and tutorials. `[GitLab Solution]` `[CI Solution]`
+ - [Tutorial: Serverless Framework Deployment to AWS with GitLab Serverless SAST Scanning](https://gitlab.com/guided-explorations/aws/serverless/serverless-framework-aws/-/blob/master/TUTORIAL.md) `[GitLab Solution]` `[CI Solution]`
+ - [Tutorial: Secure Serverless Framework Development with GitLab Security Policy Approval Rules and Managed DevOps Environments](https://gitlab.com/guided-explorations/aws/serverless/serverless-framework-aws/-/blob/master/TUTORIAL2-SecurityAndManagedEnvs.md) `[GitLab Solution]` `[CI Solution]`
+
+### Infrastructure as Code
+
+- [Terraform Deployment to AWS with GitLab MR Managed DevOps Environments](https://gitlab.com/guided-explorations/aws/terraform/terraform-web-server-cluster)
+ - [Tutorial: Terraform Deployment to AWS with GitLab IaC SAST Scanning](https://gitlab.com/guided-explorations/aws/terraform/terraform-web-server-cluster/-/blob/prod/TUTORIAL.md) `[GitLab Solution]` `[CI Solution]`
+ - [Terraform Deployment to AWS with GitLab Security Policy Approval Rules and Managed DevOps Environments](https://gitlab.com/guided-explorations/aws/terraform/terraform-web-server-cluster/-/blob/prod/TUTORIAL2-SecurityAndManagedEnvs.md) `[GitLab Solution]` `[CI Solution]`
+- [Tutorial: CloudFormation Deployment With GitLab MR Managed DevOps Environments](https://gitlab.com/guided-explorations/aws/cloudformation-deploy) `[GitLab Solution]` `[CI Solution]`
+
+### .Net on AWS
+
+- [Working Example Code for Scaling .NET Framework 4.x Runners on AWS](https://gitlab.com/guided-explorations/aws/dotnet-aws-toolkit) `[GitLab Solution]` `[CI Solution]`
+- [Video Walkthrough of Code and Building a .NET Framework 4.x Project](https://www.youtube.com/watch?v=_4r79ZLmDuo) `[GitLab Solution]` `[CI Solution]`
+
+## Authentication Integration
+
+- [Runner Job Authentication using Open ID & JWT Authentication](../../../ci/cloud_services/aws/index.md). `[GitLab Built]`
+ - [Configure OpenID Connect between GitLab and AWS](https://gitlab.com/guided-explorations/aws/configure-openid-connect-in-aws) `[GitLab Solution]` `[CI Solution]`
+ - [OIDC and Multi-Account Deployment with GitLab and ECS](https://gitlab.com/guided-explorations/aws/oidc-and-multi-account-deployment-with-ecs) `[GitLab Solution]` `[CI Solution]`
+
+## GitLab Instance Compute & Operations Integration
+
+- Installing GitLab Self-Managed on AWS
+ - GitLab Single EC2 Instance. `[GitLab Built]`
+ - [Using 5 Seat AWS marketplace subscription](gitlab_single_box_on_aws.md#marketplace-subscription)
+ - [Using Prepared AMIs](gitlab_single_box_on_aws.md#official-gitlab-releases-as-amis) - Bring Your Own License for Enterprise Edition.
+
+ - GitLab Cloud Native Hybrid Scaled on AWS EKS and Paas. `[GitLab Built]`
+ - Using GitLab Environment Toolkit (GET) - `[GitLab Solution]`
+
+ - GitLab Instance Scaled on AWS EC2 and PaaS. `[GitLab Built]`
+ - Using GitLab Environment Toolkit (GET) - `[GitLab Solution]`
+
+- [Amazon Managed Grafana](https://docs.aws.amazon.com/grafana/latest/userguide/gitlab-AMG-datasource.html) for GitLab self-managed Prometheus metrics. `[AWS Built]`
+
+## GitLab Runner on AWS Compute
+
+- [Autoscaling GitLab Runner on AWS EC2](https://docs.gitlab.com/runner/configuration/runner_autoscale_aws/). `[GitLab Built]`
+- [GitLab HA Scaling Runner Vending Machine for AWS EC2 ASG](https://gitlab.com/guided-explorations/aws/gitlab-runner-autoscaling-aws-asg/). `[GitLab Solution]`
+ - Runner vending machine training resources.
+
+- [GitLab EKS Fargate Runners](https://gitlab.com/guided-explorations/aws/eks-runner-configs/gitlab-runner-eks-fargate/-/blob/main/README.md). `[GitLab Solution]`
diff --git a/doc/solutions/cloud/aws/gitlab_aws_partner_designations.md b/doc/solutions/cloud/aws/gitlab_aws_partner_designations.md
new file mode 100644
index 00000000000..c48c3f95f9d
--- /dev/null
+++ b/doc/solutions/cloud/aws/gitlab_aws_partner_designations.md
@@ -0,0 +1,38 @@
+---
+stage: Solutions Architecture
+group: Solutions Architecture
+info: This page is owned by the Solutions Architecture team.
+description: GitLab partnership certifications and designations from AWS.
+---
+
+# GitLab partnership certifications and designations from AWS
+
+The certifications and designations outlined here can be validated on [GitLabs partner page at AWS](https://partners.amazonaws.com/partners/001E0000018YWFfIAO/GitLab,%20Inc.).
+
+All AWS partner qualifications require submission and validation of extensive checklists and submission of backing evidence that AWS utilizes to determine whether to grant the qualification.
+
+## DevOps Software / ISV Competency
+
+This competency validates that GitLab delivers DevOps solutions that work with and on AWS. [AWS Program Information](https://aws.amazon.com/devops/partner-solutions/)
+
+## DevSecOps Specialty Category
+
+[AWS Program Information](https://aws.amazon.com/blogs/apn/aws-devops-competency-expands-to-include-devsecops-category/) [GitLab Announcement](https://about.gitlab.com/blog/2023/09/25/aws-devsecops-competency-partner/)
+
+## Public Sector Partner
+
+This designation indicates that GitLab has been deemed qualified to work with AWS Public Sector customers. In fact, we have an entire organization dedicated to this practice. [AWS Program Information](https://aws.amazon.com/partners/programs/public-sector/)
+
+## AWS Graviton
+
+GitLab Instances and Runners have been tested and work on AWS Graviton. For Amazon Linux we maintain YUM packages for ARM architecture. [AWS Program Information](https://aws.amazon.com/ec2/graviton/partners/)
+
+## Amazon Linux Ready
+
+GitLab Instances and Runner have been validated on Amazon Linux 2 and 2023 - this includes YUM packages and package repositories for both and over 2300 CI tests for both before packaging. [AWS Program Information](https://aws.amazon.com/amazon-linux/partners/)
+
+## AWS Marketplace Seller
+
+GitLab is a marketplace seller and you can purchase and deploy it through AWS marketplace [AWS Program Information](https://aws.amazon.com/marketplace/partners/management-tour)
+
+![AWS Partner Designations Logo](img/all-aws-partner-designations.png){: .right}
diff --git a/doc/solutions/cloud/aws/gitlab_instance_on_aws.md b/doc/solutions/cloud/aws/gitlab_instance_on_aws.md
new file mode 100644
index 00000000000..320c317d446
--- /dev/null
+++ b/doc/solutions/cloud/aws/gitlab_instance_on_aws.md
@@ -0,0 +1,55 @@
+---
+stage: Solutions Architecture
+group: Solutions Architecture
+info: This page is owned by the Solutions Architecture team.
+---
+
+{::options parse_block_html="true" /}
+
+# Provision GitLab Instances on AWS EKS **(FREE SELF)**
+
+## Available Infrastructure as Code for GitLab Instance Installation on AWS
+
+The [GitLab Environment Toolkit (GET)](https://gitlab.com/gitlab-org/gitlab-environment-toolkit/-/blob/main/README.md) is a set of opinionated Terraform and Ansible scripts. These scripts help with the deployment of Linux package or Cloud Native Hybrid environments on selected cloud providers and are used by GitLab developers for [GitLab Dedicated](../../../subscriptions/gitlab_dedicated/index.md) (for example).
+
+You can use the GitLab Environment Toolkit to deploy a Cloud Native Hybrid environment on AWS. However, it's not required and may not support every valid permutation. That said, the scripts are presented as-is and you can adapt them accordingly.
+
+### Two and Three Zone High Availability
+
+While GitLab Reference Architectures generally encourage three zone redundancy, AWS Quick Starts and AWS Well Architected consider two zone redundancy as AWS Well Architected. Individual implementations should weigh the costs of two and three zone configurations against their own high availability requirements for a final configuration.
+
+Gitaly Cluster uses a consistency voting system to implement strong consistency between synchronized nodes. Regardless of the number of availability zones implemented, there will always need to be a minimum of three Gitaly and three Praefect nodes in the cluster to avoid voting stalemates cause by an even number of nodes.
+
+## AWS PaaS qualified for all GitLab implementations
+
+For both implementations that used the Linux package or Cloud Native Hybrid implementations, the following GitLab Service roles can be performed by AWS Services (PaaS). Any PaaS solutions that require preconfigured sizing based on the scale of your instance will also be listed in the per-instance size Bill of Materials lists. Those PaaS that do not require specific sizing, are not repeated in the BOM lists (for example, AWS Certification Manager).
+
+These services have been tested with GitLab.
+
+Some services, such as log aggregation, outbound email are not specified by GitLab, but where provided are noted.
+
+| GitLab Services | AWS PaaS (Tested) | Provided by AWS Cloud <br />Native Hybrid Quick Start |
+| ------------------------------------------------------------ | ------------------------------ | ------------------------------------------------------------ |
+| <u>Tested PaaS Mentioned in Reference Architectures</u> | | |
+| **PostgreSQL Database** | Amazon RDS PostgreSQL | Yes. |
+| **Redis Caching** | Redis ElastiCache | Yes. |
+| **Gitaly Cluster (Git Repository Storage)**<br />(Including Praefect and PostgreSQL) | ASG and Instances | Yes - ASG and Instances<br />**Note: Gitaly cannot be put into a Kubernetes Cluster.** |
+| **All GitLab storages besides Git Repository Storage**<br />(Includes Git-LFS which is S3 Compatible) | AWS S3 | Yes |
+| | | |
+| <u>Tested PaaS for Supplemental Services</u> | | |
+| **Front End Load Balancing** | AWS ELB | Yes |
+| **Internal Load Balancing** | AWS ELB | Yes |
+| **Outbound Email Services** | AWS Simple Email Service (SES) | Yes |
+| **Certificate Authority and Management** | AWS Certificate Manager (ACM) | Yes |
+| **DNS** | AWS Route53 (tested) | Yes |
+| **GitLab and Infrastructure Log Aggregation** | AWS CloudWatch Logs | Yes (ContainerInsights Agent for EKS) |
+| **Infrastructure Performance Metrics** | AWS CloudWatch Metrics | Yes |
+| | | |
+| <u>Supplemental Services and Configurations (Tested)</u> | | |
+| **Prometheus for GitLab** | AWS EKS (Cloud Native Only) | Yes |
+| **Grafana for GitLab** | AWS EKS (Cloud Native Only) | Yes |
+| **Administrative Access to GitLab Backend** | Bastion Host in VPC | Yes - HA - Preconfigured for Cluster Management. |
+| **Encryption (In Transit / At Rest)** | AWS KMS | Yes |
+| **Secrets Storage for Provisioning** | AWS Secrets Manager | Yes |
+| **Configuration Data for Provisioning** | AWS Parameter Store | Yes |
+| **AutoScaling Kubernetes** | EKS AutoScaling Agent | Yes |
diff --git a/doc/solutions/cloud/aws/gitlab_single_box_on_aws.md b/doc/solutions/cloud/aws/gitlab_single_box_on_aws.md
new file mode 100644
index 00000000000..7a647f1d8d7
--- /dev/null
+++ b/doc/solutions/cloud/aws/gitlab_single_box_on_aws.md
@@ -0,0 +1,51 @@
+---
+stage: Solutions Architecture
+group: Solutions Architecture
+info: This page is owned by the Solutions Architecture team.
+---
+
+{::options parse_block_html="true" /}
+
+# Provision GitLab on a single EC2 instance in AWS **(FREE SELF)**
+
+If you want to provision a single GitLab instance on AWS, you have two options:
+
+- The marketplace subscription
+- The official GitLab AMIs
+
+## Marketplace subscription
+
+GitLab provides a 5 user subscription as an AWS Marketplace subscription to help teams of all sizes to get started with an Ultimate licensed instance in record time. The Marketplace subscription can be easily upgraded to any GitLab licensing via an AWS Marketplace Private Offer, with the convenience of continued AWS billing. No migration is necessary to obtain a larger, non-time based license from GitLab. Per-minute licensing is automatically removed when you accept the private offer.
+
+For a tutorial on provisioning a GitLab Instance via a Marketplace Subscription, [use this tutorial](https://gitlab.awsworkshop.io/040_partner_setup.html). The tutorial links to the [GitLab Ultimate Marketplace Listing](https://aws.amazon.com/marketplace/pp/prodview-g6ktjmpuc33zk), but you can also use the [GitLab Premium Marketplace Listing](https://aws.amazon.com/marketplace/pp/prodview-amk6tacbois2k) to provision an instance.
+
+## Official GitLab releases as AMIs
+
+GitLab produces Amazon Machine Images (AMI) during the regular release process. The AMIs can be used for single instance GitLab installation or, by configuring `/etc/gitlab/gitlab.rb`, can be specialized for specific GitLab service roles (for example a Gitaly server). Older releases remain available and can be used to migrate an older GitLab server to AWS.
+
+Initial licensing can either be the Free Enterprise License (EE) or the open source Community Edition (CE). The Enterprise Edition provides the easiest path forward to a licensed version if the need arises.
+
+Currently the Amazon AMI uses the Amazon prepared Ubuntu AMI (x86 and ARM are available) as its starting point.
+
+NOTE:
+When deploying a GitLab instance using the official AMI, the root password to the instance is the EC2 **Instance** ID (not the AMI ID). This way of setting the root account password is specific to official GitLab published AMIs ONLY.
+
+Instances running on Community Edition (CE) require a migration to Enterprise Edition (EE) to subscribe to the GitLab Premium or Ultimate plan. If you want to pursue a subscription, using the Free-forever plan of Enterprise Edition is the least disruptive method.
+
+NOTE:
+Because any given GitLab upgrade might involve data disk updates or database schema upgrades, swapping out the AMI is not sufficient for taking upgrades.
+
+1. Log in to the AWS Web Console, so that selecting the links in the following step take you directly to the AMI list.
+1. Pick the edition you want:
+
+ - [GitLab Enterprise Edition](https://console.aws.amazon.com/ec2/v2/home?region=us-east-1#Images:visibility=public-images;ownerAlias=782774275127;search=GitLab%20EE;sort=desc:name): If you want to unlock the enterprise features, a license is needed.
+ - [GitLab Community Edition](https://console.aws.amazon.com/ec2/v2/home?region=us-east-1#Images:visibility=public-images;ownerAlias=782774275127;search=GitLab%20CE;sort=desc:name): The open source version of GitLab.
+ - [GitLab Premium or Ultimate Marketplace (pre-licensed)](https://console.aws.amazon.com/ec2/v2/home?region=us-east-1#Images:visibility=public-images;source=Marketplace;search=GitLab%20EE;sort=desc:name): 5 user license built into per-minute billing.
+
+1. AMI IDs are unique per region. After you've loaded any of these editions, in the upper-right corner, select the desired target region of the console to see the appropriate AMIs.
+1. After the console is loaded, you can add additional search criteria to narrow further. For instance, type `13.` to find only 13.x versions.
+1. To launch an EC2 Machine with one of the listed AMIs, check the box at the start of the relevant row, and select **Launch** near the top of left of the page.
+
+NOTE:
+If you are trying to restore from an older version of GitLab while moving to AWS, find the
+[Enterprise and Community Editions before GitLab 11.10.3](https://console.aws.amazon.com/ec2/v2/home?region=us-east-1#Images:visibility=public-images;ownerAlias=855262394183;sort=desc:name).
diff --git a/doc/solutions/cloud/aws/img/all-aws-partner-designations.png b/doc/solutions/cloud/aws/img/all-aws-partner-designations.png
new file mode 100644
index 00000000000..76925656fec
--- /dev/null
+++ b/doc/solutions/cloud/aws/img/all-aws-partner-designations.png
Binary files differ
diff --git a/doc/solutions/cloud/aws/index.md b/doc/solutions/cloud/aws/index.md
new file mode 100644
index 00000000000..7e9eed235ff
--- /dev/null
+++ b/doc/solutions/cloud/aws/index.md
@@ -0,0 +1,84 @@
+---
+stage: Solutions Architecture
+group: Solutions Architecture
+info: This page is owned by the Solutions Architecture team.
+---
+
+# AWS Solutions
+
+This documentation covers solutions relating to leveraging GitLab with and on Amazon Web Services (AWS).
+
+- [GitLab partnership certifications and designations from AWS](gitlab_aws_integration.md)
+- [GitLab AWS Integration Index](gitlab_aws_partner_designations.md)
+- [GitLab Instances on AWS EKS](gitlab_instance_on_aws.md)
+- [SRE Considerations Gitaly on AWS](gitaly_sre_for_aws.md)
+- [Provision GitLab on a single EC2 instance in AWS](gitlab_single_box_on_aws.md)
+
+## Cloud platform well architected compliance
+
+Testing-backed architectural qualification is a fundamental concept behind implementation patterns:
+
+- Implementation patterns maintain GitLab Reference Architecture compliance and provide [GitLab Performance Tool](https://gitlab.com/gitlab-org/quality/performance) (GPT) reports to demonstrate adherence to them.
+- Implementation patterns may be qualified by and/or contributed to by the technology vendor. For instance, an implementation pattern for AWS may be officially reviewed by AWS.
+- Implementation patterns may specify and test Cloud Platform PaaS services for suitability for GitLab. This testing can be coordinated and help qualify these technologies for Reference Architectures. For instance, qualifying compatibility with and availability of runtime versions of top level PaaS such as those for PostgreSQL and Redis.
+- Implementation patterns can provided qualified testing for platform limitations, for example, ensuring Gitaly Cluster can work correctly on specific Cloud Platform availability zone latency and throughput characteristics or qualifying what levels of available platform partner local disk performance is workable for Gitaly server to operate with integrity.
+
+## AWS known issues list
+
+Known issues are gathered from within GitLab and from customer reported issues. Customers successfully implement GitLab with a variety of “as a Service” components that GitLab has not specifically been designed for, nor has ongoing testing for. While GitLab does take partner technologies very seriously, the highlighting of known issues here is a convenience for implementers and it does not imply that GitLab has targeted compatibility with, nor carries any type of guarantee of running on the partner technology where the issues occur. Consult individual issues to understand the GitLab stance and plans on any given known issue.
+
+See the [GitLab AWS known issues list](https://gitlab.com/gitlab-com/alliances/aws/public-tracker/-/issues?label_name[]=AWS+Known+Issue) for a complete list.
+
+## Patterns with working code examples for using GitLab with AWS
+
+[The Guided Explorations' subgroup for AWS](https://gitlab.com/guided-explorations/aws) contains a variety of working example projects.
+
+## Platform partner specificity
+
+Implementation patterns enable platform-specific terminology, best practice architecture, and platform-specific build manifests:
+
+- Implementation patterns are more vendor specific. For instance, advising specific compute instances / VMs / nodes instead of vCPUs or other generalized measures.
+- Implementation patterns are oriented to implementing good architecture for the vendor in view.
+- Implementation patterns are written to an audience who is familiar with building on the infrastructure that the implementation pattern targets. For example, if the implementation pattern is for GCP, the specific terminology of GCP is used - including using the specific names for PaaS services.
+- Implementation patterns can test and qualify if the versions of PaaS available are compatible with GitLab (for example, PostgreSQL, Redis, etc.).
+
+## Platform as a Service (PaaS) specification and usage
+
+Platform as a Service options are a huge portion of the value provided by Cloud Platforms as they simplify operational complexity and reduce the SRE and security skilling required to operate advanced, highly available technology services. Implementation patterns can be pre-qualified against the partner PaaS options.
+
+- Implementation patterns help implementers understand what PaaS options are known to work and how to choose between PaaS solutions when a single platform has more than one PaaS option for the same GitLab role.
+- For instance, where reference architectures do not have a specific recommendation on what technology is leveraged for GitLab outbound email services or what the sizing should be - a Reference Implementation may advise using a cloud providers Email as a Service (PaaS) and possibly even with specific settings.
+
+## Cost optimizing engineering
+
+Cost engineering is a fundamental aspect of Cloud Architecture and frequently the savings capabilities available on a platform exert strong influence on how to build out scaled computing.
+
+- Implementation patterns may engineer specifically for the savings models available on a platform provider. An AWS example would be maximizing the occurrence of a specific instance type for taking advantage of reserved instances.
+- Implementation patterns may leverage ephemeral compute where appropriate and with appropriate customer guidelines. For instance, a Kubernetes node group dedicated to runners on ephemeral compute (with appropriate GitLab Runner tagging to indicate the compute type).
+- Implementation patterns may include vendor specific cost calculators.
+
+## Actionability and automatability orientation
+
+Implementation patterns are one step closer to specifics that can be used as a source for build instructions and automation code:
+
+- Implementation patterns enable builders to generate a list of vendor specific resources required to implement GitLab for a given Reference Architecture.
+- Implementation patterns enable builders to use manual instructions or to create automation to build out the reference implementation.
+
+## Intended audiences and contributors
+
+The primary audiences for and contributors to this information is the GitLab **Implementation Eco System** which consists of at least:
+
+GitLab Implementation Community:
+
+- Customers
+- GitLab Channel Partners (Integrators)
+- Platform Partners
+
+GitLab Internal Implementation Teams:
+
+- Quality / Distribution / Self-Managed
+- Alliances
+- Training
+- Support
+- Professional Services
+- Public Sector
diff --git a/doc/solutions/cloud/index.md b/doc/solutions/cloud/index.md
new file mode 100644
index 00000000000..27a90223382
--- /dev/null
+++ b/doc/solutions/cloud/index.md
@@ -0,0 +1,13 @@
+---
+stage: Solutions Architecture
+group: Solutions Architecture
+info: This page is owned by the Solutions Architecture team.
+---
+
+# Cloud solutions
+
+This documentation section covers a variety of Cloud Solutions.
+
+## Cloud solutions by provider
+
+[AWS Solutions](aws/index.md)
diff --git a/doc/solutions/index.md b/doc/solutions/index.md
new file mode 100644
index 00000000000..9d7fec1b549
--- /dev/null
+++ b/doc/solutions/index.md
@@ -0,0 +1,19 @@
+---
+stage: Solutions Architecture
+group: Solutions Architecture
+info: This page is owned by the Solutions Architecture team.
+---
+
+# Solutions architecture
+
+As with all extensible platforms, GitLab has many features that can be creatively combined together with third party functionality to create solutions that address the specific people, process, and technology challenges of the organizations that use it. Reference solutions and implementations can also be crafted at a more general level so that they can be adopted and customized by customers with similar needs to the reference solution.
+
+This documentation is the home for solutions GitLab wishes to share with customers.
+
+## Relationship to documentation
+
+While information in this section gives valuable and qualified guidance on ways to solve problems by using the GitLab platform, the product documentation is the authoritative reference for product features and functions.
+
+## Solutions categories
+
+[Cloud Solutions](cloud/index.md)
diff --git a/doc/subscriptions/bronze_starter.md b/doc/subscriptions/bronze_starter.md
index 3b2ef601136..90e0e77cb9a 100644
--- a/doc/subscriptions/bronze_starter.md
+++ b/doc/subscriptions/bronze_starter.md
@@ -106,7 +106,7 @@ the tiers are no longer mentioned in GitLab documentation:
- [Filtering merge requests](../user/project/merge_requests/index.md#filter-the-list-of-merge-requests) by "approved by"
- [Advanced search (Elasticsearch)](../user/search/advanced_search.md)
- [Service Desk](../user/project/service_desk/index.md)
-- [Storage usage statistics](../user/usage_quotas.md#storage-usage-statistics)
+- [Storage usage statistics](../user/usage_quotas.md)
The following developer features continue to be available to Starter and
Bronze-level subscribers:
diff --git a/doc/subscriptions/gitlab_com/index.md b/doc/subscriptions/gitlab_com/index.md
index b4efc463910..317cdb1e1d5 100644
--- a/doc/subscriptions/gitlab_com/index.md
+++ b/doc/subscriptions/gitlab_com/index.md
@@ -327,8 +327,11 @@ For details on upgrading your subscription tier, see
### Automatic subscription renewal
-When a subscription is set to auto-renew, it renews automatically on the
-expiration date without a gap in available service. Subscriptions purchased through the Customers Portal or GitLab.com are set to auto-renew by default. The number of seats is adjusted to fit the [number of billable users in your group](#view-seat-usage) at the time of renewal, if that number is higher than the current subscription quantity. You can view and download your renewal invoice on the Customers Portal [View invoices](https://customers.gitlab.com/receipts) page. If your account has a [saved credit card](../customers_portal.md#change-your-payment-method), the card is charged for the invoice amount. If we are unable to process a payment, or the auto-renewal fails for any other reason, you have 14 days to renew your subscription, after which your access is downgraded.
+When a subscription is set to auto-renew, it renews automatically on the expiration date without a gap in available service. Subscriptions purchased through the Customers Portal or GitLab.com are set to auto-renew by default.
+
+The number of seats is adjusted to fit the [number of billable users in your group](#view-seat-usage) at the time of renewal, if that number is higher than the current subscription quantity.
+
+You can view and download your renewal invoice on the Customers Portal [View invoices](https://customers.gitlab.com/receipts) page. If your account has a [saved credit card](../customers_portal.md#change-your-payment-method), the card is charged for the invoice amount. If we are unable to process a payment, or the auto-renewal fails for any other reason, you have 14 days to renew your subscription, after which your access is downgraded.
#### Email notifications
@@ -412,7 +415,7 @@ You can [cancel the subscription](#enable-or-disable-automatic-subscription-rene
1. Sign in to GitLab SaaS.
1. From either your personal homepage or the group's page, go to **Settings > Usage Quotas**.
-1. For each locked project, total by how much its **Usage** exceeds the free quota and purchased
+1. For each read-only project, total by how much its **Usage** exceeds the free quota and purchased
storage. You must purchase the storage increment that exceeds this total.
1. Select **Purchase more storage** and you are taken to the Customers Portal.
1. Select **Add new subscription**.
@@ -425,8 +428,8 @@ You can [cancel the subscription](#enable-or-disable-automatic-subscription-rene
1. Sign out of the Customers Portal.
1. Switch back to the GitLab SaaS tab and refresh the page.
-The **Purchased storage available** total is incremented by the amount purchased. All locked
-projects are unlocked and their excess usage is deducted from the additional storage.
+The **Purchased storage available** total is incremented by the amount purchased. The read-only
+state for all projects is removed, and their excess usage is deducted from the additional storage.
#### For your group namespace
diff --git a/doc/subscriptions/gitlab_dedicated/index.md b/doc/subscriptions/gitlab_dedicated/index.md
index d07979cfda5..07abfb223ef 100644
--- a/doc/subscriptions/gitlab_dedicated/index.md
+++ b/doc/subscriptions/gitlab_dedicated/index.md
@@ -23,7 +23,10 @@ GitLab Dedicated allows you to select the cloud region where your data will be s
### Availability and scalability
-GitLab Dedicated leverages the GitLab [Cloud Native Hybrid reference architectures](../../administration/reference_architectures/index.md#cloud-native-hybrid) with high availability enabled. When [onboarding](../../administration/dedicated/index.md#onboarding-to-gitlab-dedicated-using-switchboard), GitLab will match you to the closest reference architecture size based on your number of users. Learn about the [current Service Level Objective](https://about.gitlab.com/handbook/engineering/infrastructure/team/gitlab-dedicated/slas/#current-service-level-objective).
+GitLab Dedicated leverages modified versions of the GitLab [Cloud Native Hybrid reference architectures](../../administration/reference_architectures/index.md#cloud-native-hybrid) with high availability enabled. When [onboarding](../../administration/dedicated/index.md#onboarding-to-gitlab-dedicated-using-switchboard), GitLab will match you to the closest reference architecture size based on your number of users. Learn about the [current Service Level Objective](https://about.gitlab.com/handbook/engineering/infrastructure/team/gitlab-dedicated/slas/#current-service-level-objective).
+
+NOTE:
+The published [reference architectures](../../administration/reference_architectures/index.md) act as a starting point in defining the cloud resources deployed inside GitLab Dedicated environments, but they are not comprehensive. GitLab Dedicated leverages additional Cloud Provider services beyond what's included in the standard reference architectures for enhanced security and stability of the environment. Therefore, GitLab Dedicated costs differ from standard reference architecture costs.
#### Disaster Recovery
diff --git a/doc/subscriptions/self_managed/index.md b/doc/subscriptions/self_managed/index.md
index 05d00323e2a..a1573132ab2 100644
--- a/doc/subscriptions/self_managed/index.md
+++ b/doc/subscriptions/self_managed/index.md
@@ -34,7 +34,8 @@ Prorated charges are not possible without a quarterly usage report.
## View user totals
-You can view users for your license and determine if you've gone over your subscription.
+View the amount of users in your instance to determine if they exceed the amount
+paid for in your subscription.
1. On the left sidebar, select **Search or go to**.
1. Select **Admin Area**.
@@ -44,17 +45,19 @@ The lists of users are displayed.
### Billable users
-A _billable user_ counts against the number of subscription seats. Every user is considered a
-billable user, with the following exceptions:
-
-- [Deactivated users](../../administration/moderate_users.md#deactivate-a-user) and
- [blocked users](../../administration/moderate_users.md#block-a-user) don't count as billable users in the current subscription. When they are either deactivated or blocked they release a _billable user_ seat. However, they may
- count toward overages in the subscribed seat count.
-- Users who are [pending approval](../../administration/moderate_users.md#users-pending-approval).
-- Users with only the [Minimal Access role](../../user/permissions.md#users-with-minimal-access) on self-managed Ultimate subscriptions or any GitLab.com subscriptions.
-- Users with only the [Guest or Minimal Access roles on an Ultimate subscription](#free-guest-users).
-- Users without project or group memberships on an Ultimate subscription.
-- GitLab-created service accounts:
+Billable users count toward the number of subscription seats purchased in your subscription.
+
+A user is not counted as a billable user if:
+
+- They are [deactivated](../../administration/moderate_users.md#deactivate-a-user) or
+ [blocked](../../administration/moderate_users.md#block-a-user).
+ If the user occupied a seat prior to being deactivated or blocked,
+ the user is included in the number of [maximum users](#maximum-users).
+- They are [pending approval](../../administration/moderate_users.md#users-pending-approval).
+- They have only the [Minimal Access role](../../user/permissions.md#users-with-minimal-access) on self-managed Ultimate subscriptions or any GitLab.com subscriptions.
+- They have the [Guest or Minimal Access roles on an Ultimate subscription](#free-guest-users).
+- They have project or group memberships on an Ultimate subscription.
+- The account is a GitLab-created service account:
- [Ghost User](../../user/profile/account/delete_account.md#associated-records).
- Bots such as:
- [Support Bot](../../user/project/service_desk/configure.md#support-bot-user).
@@ -62,7 +65,7 @@ billable user, with the following exceptions:
- [Bot users for groups](../../user/group/settings/group_access_tokens.md#bot-users-for-groups).
- Other [internal users](../../development/internal_users.md#internal-users).
-**Billable users** as reported in the `/admin` section is updated once per day.
+The amount of **Billable users** is reported once a day in the Admin Area.
### Maximum users
@@ -373,14 +376,12 @@ An invoice is generated for the renewal and available for viewing or download on
### Automatic subscription renewal
-When a subscription is set to auto-renew, it renews automatically on the
-expiration date (at midnight UTC) without a gap in available service. Subscriptions purchased through Customers Portal are set to auto-renew by default.
-The number of user licenses is adjusted to fit the [number of billable users in your instance](#view-user-totals) at the time of renewal, if that number is higher than the current subscription quantity.
-Before auto-renewal you should [prepare for the renewal](#prepare-for-renewal-by-reviewing-your-account) at least 2 days before the renewal date, so that your changes synchronize to GitLab in time for your renewal. To auto-renew your subscription,
+When a subscription is set to auto-renew, it renews automatically on the expiration date (at midnight UTC) without a gap in available service. Subscriptions purchased through Customers Portal are set to auto-renew by default.
+
+The number of user licenses is adjusted to fit the [number of billable users in your instance](#view-user-totals) at the time of renewal, if that number is higher than the current subscription quantity. Before auto-renewal you should [prepare for the renewal](#prepare-for-renewal-by-reviewing-your-account) at least 2 days before the renewal date, so that your changes synchronize to GitLab in time for your renewal. To auto-renew your subscription,
you must have enabled the [synchronization of subscription data](#subscription-data-synchronization).
-You can view and download your renewal invoice on the Customers Portal
-[View invoices](https://customers.gitlab.com/receipts) page. If your account has a [saved credit card](../customers_portal.md#change-your-payment-method), the card is charged for the invoice amount. If we are unable to process a payment or the auto-renewal fails for any other reason, you have 14 days to renew your subscription, after which your GitLab tier is downgraded.
+You can view and download your renewal invoice on the Customers Portal [View invoices](https://customers.gitlab.com/receipts) page. If your account has a [saved credit card](../customers_portal.md#change-your-payment-method), the card is charged for the invoice amount. If we are unable to process a payment or the auto-renewal fails for any other reason, you have 14 days to renew your subscription, after which your GitLab tier is downgraded.
#### Email notifications
diff --git a/doc/topics/autodevops/cicd_variables.md b/doc/topics/autodevops/cicd_variables.md
index 21d9dd0b3d3..4fa2ee10c75 100644
--- a/doc/topics/autodevops/cicd_variables.md
+++ b/doc/topics/autodevops/cicd_variables.md
@@ -31,6 +31,9 @@ Use these variables to customize and deploy your build.
| `AUTO_DEVOPS_CHART_REPOSITORY_USERNAME` | Used to set a username to connect to the Helm repository. Defaults to no credentials. Also set `AUTO_DEVOPS_CHART_REPOSITORY_PASSWORD`. |
| `AUTO_DEVOPS_CHART_REPOSITORY_PASSWORD` | Used to set a password to connect to the Helm repository. Defaults to no credentials. Also set `AUTO_DEVOPS_CHART_REPOSITORY_USERNAME`. |
| `AUTO_DEVOPS_CHART_REPOSITORY_PASS_CREDENTIALS` | From GitLab 14.2, set to a non-empty value to enable forwarding of the Helm repository credentials to the chart server when the chart artifacts are on a different host than repository. |
+| `AUTO_DEVOPS_CHART_REPOSITORY_INSECURE` | Set to a non-empty value to add a `--insecure-skip-tls-verify` argument to the Helm commands. By default, Helm uses TLS verification. |
+| `AUTO_DEVOPS_CHART_CUSTOM_ONLY` | Set to a non-empty value to use only a custom chart. By default, the latest chart is downloaded from GitLab. |
+| `AUTO_DEVOPS_CHART_VERSION` | Set the version of the deployment chart. Defaults to the latest available version. |
| `AUTO_DEVOPS_COMMON_NAME` | From GitLab 15.5, set to a valid domain name to customize the common name used for the TLS certificate. Defaults to `le-$CI_PROJECT_ID.$KUBE_INGRESS_BASE_DOMAIN`. Set to `false` to not set this alternative host on the Ingress. |
| `AUTO_DEVOPS_DEPLOY_DEBUG` | From GitLab 13.1, if this variable is present, Helm outputs debug logs. |
| `AUTO_DEVOPS_ALLOW_TO_FORCE_DEPLOY_V<N>` | From [auto-deploy-image](https://gitlab.com/gitlab-org/cluster-integration/auto-deploy-image) v1.0.0, if this variable is present, a new major version of chart is forcibly deployed. For more information, see [Ignore warnings and continue deploying](upgrading_auto_deploy_dependencies.md#ignore-warnings-and-continue-deploying). |
diff --git a/doc/topics/autodevops/customize.md b/doc/topics/autodevops/customize.md
index e920ae5e5e1..2e6672e3ab0 100644
--- a/doc/topics/autodevops/customize.md
+++ b/doc/topics/autodevops/customize.md
@@ -208,11 +208,14 @@ repository or by specifying a project CI/CD variable:
file in it, Auto DevOps detects the chart and uses it instead of the
[default chart](https://gitlab.com/gitlab-org/cluster-integration/auto-deploy-image/-/tree/master/assets/auto-deploy-app).
- **Project variable** - Create a [project CI/CD variable](../../ci/variables/index.md)
- `AUTO_DEVOPS_CHART` with the URL of a custom chart. You can also create two project
+ `AUTO_DEVOPS_CHART` with the URL of a custom chart. You can also create five project
variables:
- `AUTO_DEVOPS_CHART_REPOSITORY` - The URL of a custom chart repository.
- `AUTO_DEVOPS_CHART` - The path to the chart.
+ - `AUTO_DEVOPS_CHART_REPOSITORY_INSECURE` - Set to a non-empty value to add a `--insecure-skip-tls-verify` argument to the Helm commands.
+ - `AUTO_DEVOPS_CHART_CUSTOM_ONLY` - Set to a non-empty value to use only a custom chart. By default, the latest chart is downloaded from GitLab.
+ - `AUTO_DEVOPS_CHART_VERSION` - The version of the deployment chart.
### Customize Helm chart values
diff --git a/doc/topics/offline/quick_start_guide.md b/doc/topics/offline/quick_start_guide.md
index 301f73a268d..4ff9975b317 100644
--- a/doc/topics/offline/quick_start_guide.md
+++ b/doc/topics/offline/quick_start_guide.md
@@ -204,7 +204,7 @@ Version Check and Service Ping improve the GitLab user experience and ensure tha
users are on the most up-to-date instances of GitLab. These two services can be turned off for offline
environments so that they do not attempt and fail to reach out to GitLab services.
-For more information, see [Enable or disable usage statistics](../../administration/settings/usage_statistics.md#enable-or-disable-usage-statistics).
+For more information, see [Enable or disable service ping](../../administration/settings/usage_statistics.md#enable-or-disable-service-ping).
### Configure NTP
diff --git a/doc/tutorials/build_application.md b/doc/tutorials/build_application.md
index 2b1f63874b1..5f4b9da2aa3 100644
--- a/doc/tutorials/build_application.md
+++ b/doc/tutorials/build_application.md
@@ -31,7 +31,7 @@ Set up runners to run jobs in a pipeline.
|-------|-------------|--------------------|
| [Create, register, and run your own project runner](create_register_first_runner/index.md) | Learn the basics of how to create and register a project runner that runs jobs for your project. | **{star}** |
| [Configure GitLab Runner to use the Google Kubernetes Engine](configure_gitlab_runner_to_use_gke/index.md) | Learn how to configure GitLab Runner to use the GKE to run jobs. | |
-| [Automate the creation of runners](https://about.gitlab.com/blog/2023/07/06/how-to-automate-creation-of-runners/) | Learn how to automate runner creation as an authenticated user to optimize your runner fleet. | |
+| [Automate runner creation and registration](automate_runner_creation/index.md) | Learn how to automate runner creation as an authenticated user to optimize your runner fleet. | |
## Publish a static website
diff --git a/doc/tutorials/left_sidebar/index.md b/doc/tutorials/left_sidebar/index.md
index 55e3b1dc30d..be631a20d50 100644
--- a/doc/tutorials/left_sidebar/index.md
+++ b/doc/tutorials/left_sidebar/index.md
@@ -12,12 +12,12 @@ Follow this tutorial to learn how to use the new left sidebar to navigate the UI
## Enable the new left sidebar
-To view the new sidebar:
+From 16.0 through 16.5, you can turn the new sidebar on and off:
1. On the left sidebar, select your avatar.
-1. Turn on the **New navigation** toggle.
+1. Change the **New navigation** toggle.
-To turn off this sidebar, return to your avatar and turn off the toggle.
+Return to your avatar to change the setting.
## Layout of the left sidebar
diff --git a/doc/tutorials/product_analytics_onboarding_website_project/index.md b/doc/tutorials/product_analytics_onboarding_website_project/index.md
new file mode 100644
index 00000000000..c0c3d7bb3d9
--- /dev/null
+++ b/doc/tutorials/product_analytics_onboarding_website_project/index.md
@@ -0,0 +1,139 @@
+---
+stage: Analyze
+group: Product Analytics
+info: For assistance with this tutorial, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments-to-other-projects-and-subjects.
+---
+
+# Tutorial: Set up product analytics in a GitLab Pages website project **(ULTIMATE ALL EXPERIMENT)**
+
+Understanding how your users engage with your website or application is important for making data-driven decisions.
+By identifying the most and least used features by your users, your team can decide where and how to spend their time effectively.
+
+Follow along to learn how to set up an example website project, onboard product analytics for the project, instrument the website to start collecting events,
+and use project-level analytics dashboards to understand user behavior.
+
+Here's an overview of what we're going to do:
+
+1. Create a project from a template
+1. Onboard the project with product analytics
+1. Instrument the website with tracking snippet
+1. Collect usage data
+1. View dashboards
+
+## Before you begin
+
+To follow along this tutorial, you must:
+
+- [Enable product analytics](../../user/product_analytics/index.md#enable-product-analytics) for your instance.
+- Have the Owner role for the group you create the project in.
+
+## Create a project from a template
+
+First of all, you need to create a project in your group.
+
+GitLab provides project templates,
+which make it easier to set up a project with all the necessary files for various use cases.
+Here, you'll create a project for a plain HTML website.
+
+To create a project:
+
+1. On the left sidebar, at the top, select **Create new** (**{plus}**) and **New project/repository**.
+1. Select **Create from template**.
+1. Select the **Pages/Plain HTML** template.
+1. In the **Project name** text box, enter a name (for example `My website`).
+1. From the **Project URL** dropdown list, select the group you want to create the project in.
+1. In the **Project slug** text box, enter a slug for your project (for example, `my-website`).
+1. Optional. In the **Project description** text box, enter a description of your project.
+ For example, `Plain HTML website with product analytics`. You can add or edit this description at any time.
+1. Under **Visibility Level**, select the desired level for the project.
+ If you create the project in a group, the visibility setting for a project must be at least as restrictive as the visibility of its parent group.
+1. Select **Create project**.
+
+Now you have a project with all the files you need for a plain HTML website.
+
+## Onboard the project with product analytics
+
+To collect events and view dashboards about your website usage, the project must have product analytics onboarded.
+
+To onboard your new project with product analytics:
+
+1. In the project, select **Analyze > Analytics dashboards**.
+1. Find the **Product analytics** item and select **Set up**.
+1. Select **Set up product analytics**.
+1. Wait for your instance to finish creating.
+1. Copy the **HTML script setup** snippet. You will need it in the next steps.
+
+Your project is now onboarded and ready for your application to start sending events.
+
+## Instrument your website
+
+To collect and send usage events to GitLab, you must include a code snippet in your website.
+You can choose from several platform and technology-specific tracking SDKs to integrate with your application.
+For this example website, we use the [Browser SDK](../../user/product_analytics/instrumentation/browser_sdk.md).
+
+To instrument your new website:
+
+1. In the project, select **Code > Repository**.
+1. Select the **Edit > Web IDE**.
+1. In the left Web IDE toolbar, select **File Explorer** and open the `public/index.html` file.
+1. In the `public/index.html` file, before the closing `</body>` tag, paste the snippet you copied in the previous section.
+
+ The code in the `index.html` file should look like this (where `appId` and `host` have the values provided in the onboarding section):
+
+ ```html
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <meta charset="utf-8">
+ <meta name="generator" content="GitLab Pages">
+ <title>Plain HTML site using GitLab Pages</title>
+ <link rel="stylesheet" href="style.css">
+ </head>
+ <body>
+ <div class="navbar">
+ <a href="https://pages.gitlab.io/plain-html/">Plain HTML Example</a>
+ <a href="https://gitlab.com/pages/plain-html/">Repository</a>
+ <a href="https://gitlab.com/pages/">Other Examples</a>
+ </div>
+
+ <h1>Hello World!</h1>
+
+ <p>
+ This is a simple plain-HTML website on GitLab Pages, without any fancy static site generator.
+ </p>
+ <script src="https://unpkg.com/@gitlab/application-sdk-browser/dist/gl-sdk.min.js"></script>
+ <script>
+ window.glClient = window.glSDK.glClientSDK({
+ appId: 'YOUR_APP_ID',
+ host: 'YOUR_HOST',
+ });
+ </script>
+ </body>
+ </html>
+ ```
+
+1. In the left Web IDE toolbar, select **Source Control**.
+1. Enter a commit message, such as `Add GitLab product analytics tracking snippet`.
+1. Select **Commit**, and if prompted to create a new branch or continue, select **Continue**. You can then close the Web IDE.
+1. In the project, select **Build > Pipelines**.
+ A pipeline is triggered from your recent commit. Wait for it to finish running and deploying your updated website.
+
+## Collect usage data
+
+After the instrumented website is deployed, events start being collected.
+
+1. In the project, select **Deploy > Pages**.
+1. To open the website, in **Access pages** select your unique URL.
+1. To collect some page view events, refresh the page a few times.
+
+## View dashboards
+
+GitLab provides two product analytics dashboards by default: **Audience** and **Behavior**.
+These dashboards become available after your project has received some events.
+
+To view these dashboards:
+
+1. In the project, select **Analyze > Dashboards**.
+1. From the list of available dashboards, select **Audience** or **Behavior**.
+
+That was it! Now you have a website project with product analytics, which help you collect and visualize data to understand your users' behavior, and make your team work more efficiently.
diff --git a/doc/update/deprecations.md b/doc/update/deprecations.md
index 3bb7f9816b4..333dad86086 100644
--- a/doc/update/deprecations.md
+++ b/doc/update/deprecations.md
@@ -102,6 +102,28 @@ This change is a breaking change. You should [create a runner in the UI](../ci/r
<div class="deprecation breaking-change" data-milestone="18.0">
+### Running a single database is deprecated
+
+<div class="deprecation-notes">
+- Announced in GitLab <span class="milestone">16.1</span>
+- Removal in GitLab <span class="milestone">18.0</span> ([breaking change](https://docs.gitlab.com/ee/update/terminology.html#breaking-change))
+- To discuss this change or learn more, see the [deprecation issue](https://gitlab.com/gitlab-org/gitlab/-/issues/411239).
+</div>
+
+From GitLab 18.0, we will require a [separate database for CI features](https://gitlab.com/groups/gitlab-org/-/epics/7509).
+We recommend running both databases on the same Postgres instance(s) due to ease of management for most deployments.
+
+We are providing this as an informational advance notice but we do not recommend taking action yet.
+We will have another update communicated (as well as the deprecation note) when we recommend admins to start the migration process.
+
+This change provides additional scalability for the largest of GitLab instances, like GitLab.com.
+This change applies to all installation methods: Omnibus GitLab, GitLab Helm chart, GitLab Operator, GitLab Docker images, and installation from source.
+Before upgrading to GitLab 18.0, please ensure you have [migrated](https://docs.gitlab.com/ee/administration/postgresql/multiple_databases.html) to two databases.
+
+</div>
+
+<div class="deprecation breaking-change" data-milestone="18.0">
+
### Support for REST API endpoints that reset runner registration tokens
<div class="deprecation-notes">
@@ -187,6 +209,26 @@ Because Cloud Native Buildpacks do not support automatic testing, the Auto Test
<div class="deprecation breaking-change" data-milestone="17.0">
+### Breaking change to the Maven repository group permissions
+
+<div class="deprecation-notes">
+- Announced in GitLab <span class="milestone">16.6</span>
+- Removal in GitLab <span class="milestone">17.0</span> ([breaking change](https://docs.gitlab.com/ee/update/terminology.html#breaking-change))
+- To discuss this change or learn more, see the [deprecation issue](https://gitlab.com/gitlab-org/gitlab/-/issues/393933).
+</div>
+
+The Maven repository exposes an API endpoint at the group level that allows Maven clients to download files from a specific package. The package finder first locates the package within the group, and then finds the file within the package.
+However, there is a limitation that affects duplicate package names hosted in different projects. The Maven package finder always returns the most recent package, but the "most recent" filter depends on user permissions. It is possible for a user with different permissions in different projects to download the wrong Maven package.
+
+In GitLab 17.0, the package finder logic will be fixed so that the "most recent" package is the last updated name and version of a package in a group. User permissions will be checked after the most recent package is found.
+After the change, download requests for users without correct permissions will be rejected. If your workflow depends on the current bugged behavior, this fix will introduce a breaking change.
+
+The change will be introduced in GitLab 16.6 behind a feature flag. If you are interested in enabling the feature flag for your group, leave a comment in [issue 393933](https://gitlab.com/gitlab-org/gitlab/-/issues/393933).
+
+</div>
+
+<div class="deprecation breaking-change" data-milestone="17.0">
+
### CiRunner.projects default sort is changing to `id_desc`
<div class="deprecation-notes">
@@ -217,6 +259,24 @@ the aliasing for the `CiRunnerUpgradeStatusType` type will be removed.
<div class="deprecation breaking-change" data-milestone="17.0">
+### Container Registry support for the Swift and OSS storage drivers
+
+<div class="deprecation-notes">
+- Announced in GitLab <span class="milestone">16.6</span>
+- Removal in GitLab <span class="milestone">17.0</span> ([breaking change](https://docs.gitlab.com/ee/update/terminology.html#breaking-change))
+- To discuss this change or learn more, see the [deprecation issue](https://gitlab.com/gitlab-org/container-registry/-/issues/1141).
+</div>
+
+The container registry uses storage drivers to work with various object storage platforms. While each driver's code is relatively self-contained, there is a high maintenance burden for these drivers. Each driver implementation is unique and making changes to a driver requires a high level of domain expertise with that specific driver.
+
+As we look to reduce maintenance costs, we are deprecating support for OSS (Object Storage Service) and OpenStack Swift. Both have already been removed from the upstream Docker Distribution. This helps align the container registry with the broader GitLab product offering with regards to [object storage support](https://docs.gitlab.com/ee/administration/object_storage.html#supported-object-storage-providers).
+
+OSS has an [S3 compatibility mode](https://www.alibabacloud.com/help/en/oss/developer-reference/compatibility-with-amazon-s3), so consider using that if you can't migrate to a supported driver. Swift is [compatible with S3 API operations](https://docs.openstack.org/swift/latest/s3_compat.html), required by the S3 storage driver as well.
+
+</div>
+
+<div class="deprecation breaking-change" data-milestone="17.0">
+
### DAST ZAP advanced configuration variables deprecation
<div class="deprecation-notes">
@@ -387,6 +447,24 @@ major release, GitLab 17.0. This gem sees very little use and is better suited f
<div class="deprecation breaking-change" data-milestone="17.0">
+### File type variable expansion fixed in downstream pipelines
+
+<div class="deprecation-notes">
+- Announced in GitLab <span class="milestone">16.6</span>
+- Removal in GitLab <span class="milestone">17.0</span> ([breaking change](https://docs.gitlab.com/ee/update/terminology.html#breaking-change))
+- To discuss this change or learn more, see the [deprecation issue](https://gitlab.com/gitlab-org/gitlab/-/issues/419445).
+</div>
+
+Previously, if you tried to reference a [file type CI/CD variable](https://docs.gitlab.com/ee/ci/variables/#use-file-type-cicd-variables) in another CI/CD variable, the CI/CD variable would expand to contain the contents of the file. This behavior was incorrect because it did not comply with typical shell variable expansion rules. The CI/CD variable reference should expand to only contain the path to the file, not the contents of the file itself. This was [fixed for most use cases in GitLab 15.7](https://gitlab.com/gitlab-org/gitlab/-/issues/29407). Unfortunately, passing CI/CD variables to downstream pipelines was an edge case not yet fixed, but which will now be fixed in GitLab 17.0.
+
+With this change, a variable configured in the `.gitlab-ci.yml` file can reference a file variable and be passed to a downstream pipeline, and the file variable will be passed to the downstream pipeline as well. The downstream pipeline will expand the variable reference to the file path, not the file contents.
+
+This breaking change could disrupt user workflows that depend on expanding a file variable in a downstream pipeline.
+
+</div>
+
+<div class="deprecation breaking-change" data-milestone="17.0">
+
### Filepath field in Releases and Release Links APIs
<div class="deprecation-notes">
@@ -560,6 +638,24 @@ In GitLab 17.0, the `DISABLED_WITH_OVERRIDE` value of the `SharedRunnersSetting`
<div class="deprecation breaking-change" data-milestone="17.0">
+### GraphQL: deprecate support for `canDestroy` and `canDelete`
+
+<div class="deprecation-notes">
+- Announced in GitLab <span class="milestone">16.6</span>
+- Removal in GitLab <span class="milestone">17.0</span> ([breaking change](https://docs.gitlab.com/ee/update/terminology.html#breaking-change))
+- To discuss this change or learn more, see the [deprecation issue](https://gitlab.com/gitlab-org/gitlab/-/issues/390754).
+</div>
+
+The Package Registry user interface relies on the GitLab GraphQL API. To make it easy for everyone to contribute, it's important that the frontend is coded consistently across all GitLab product areas. Before GitLab 16.6, however, the Package Registry UI handled permissions differently from other areas of the product.
+
+In 16.6, we added a new `UserPermissions` field under the `Types::PermissionTypes::Package` type to align the Package Registry with the rest of GitLab. This new field replaces the `canDestroy` field under the `Package`, `PackageBase`, and `PackageDetailsType` types. It also replaces the field `canDelete` for `ContainerRepository`, `ContainerRepositoryDetails`, and `ContainerRepositoryTag`. In GitLab 17.0, the `canDestroy` and `canDelete` fields will be removed.
+
+This is a breaking change that will be completed in 17.0.
+
+</div>
+
+<div class="deprecation breaking-change" data-milestone="17.0">
+
### HashiCorp Vault integration will no longer use CI_JOB_JWT by default
<div class="deprecation-notes">
@@ -615,6 +711,33 @@ If you do access the internal container registry API and use the original tag de
<div class="deprecation breaking-change" data-milestone="17.0">
+### Legacy Geo Prometheus metrics
+
+<div class="deprecation-notes">
+- Announced in GitLab <span class="milestone">16.6</span>
+- Removal in GitLab <span class="milestone">17.0</span> ([breaking change](https://docs.gitlab.com/ee/update/terminology.html#breaking-change))
+- To discuss this change or learn more, see the [deprecation issue](https://gitlab.com/gitlab-org/gitlab/-/issues/430192).
+</div>
+
+Following the migration of projects to the [Geo self-service framework](https://docs.gitlab.com/ee/development/geo/framework.html) we have deprecated a number of [Prometheus](https://docs.gitlab.com/ee/administration/monitoring/prometheus/) metrics.
+The following Geo-related Prometheus metrics are deprecated and will be removed in 17.0.
+The table below lists the deprecated metrics and their respective replacements. The replacements are available in GitLab 16.3.0 and later.
+
+| Deprecated metric | Replacement metric |
+| ---------------------------------------- | ---------------------------------------------- |
+| `geo_repositories_synced` | `geo_project_repositories_synced` |
+| `geo_repositories_failed` | `geo_project_repositories_failed` |
+| `geo_repositories_checksummed` | `geo_project_repositories_checksummed` |
+| `geo_repositories_checksum_failed` | `geo_project_repositories_checksum_failed` |
+| `geo_repositories_verified` | `geo_project_repositories_verified` |
+| `geo_repositories_verification_failed` | `geo_project_repositories_verification_failed` |
+| `geo_repositories_checksum_mismatch` | None available |
+| `geo_repositories_retrying_verification` | None available |
+
+</div>
+
+<div class="deprecation breaking-change" data-milestone="17.0">
+
### Maintainer role providing the ability to change Package settings using GraphQL API
<div class="deprecation-notes">
@@ -757,6 +880,20 @@ PostgreSQL 14 will also be supported for instances that want to upgrade prior to
<div class="deprecation breaking-change" data-milestone="17.0">
+### Proxy-based DAST deprecated
+
+<div class="deprecation-notes">
+- Announced in GitLab <span class="milestone">16.6</span>
+- Removal in GitLab <span class="milestone">17.0</span> ([breaking change](https://docs.gitlab.com/ee/update/terminology.html#breaking-change))
+- To discuss this change or learn more, see the [deprecation issue](https://gitlab.com/gitlab-org/gitlab/-/issues/430966).
+</div>
+
+As of GitLab 17.0, Proxy-based DAST will not be supported. Please migrate to Browser-based DAST to continue analyzing your projects for security findings via dynamic analysis.
+
+</div>
+
+<div class="deprecation breaking-change" data-milestone="17.0">
+
### Queue selector for running Sidekiq is deprecated
<div class="deprecation-notes">
@@ -822,28 +959,6 @@ that is available now. We recommend this alternative solution because it provide
<div class="deprecation breaking-change" data-milestone="17.0">
-### Running a single database is deprecated
-
-<div class="deprecation-notes">
-- Announced in GitLab <span class="milestone">16.1</span>
-- Removal in GitLab <span class="milestone">17.0</span> ([breaking change](https://docs.gitlab.com/ee/update/terminology.html#breaking-change))
-- To discuss this change or learn more, see the [deprecation issue](https://gitlab.com/gitlab-org/gitlab/-/issues/411239).
-</div>
-
-From GitLab 17.0, we will require a [separate database for CI features](https://gitlab.com/groups/gitlab-org/-/epics/7509).
-We recommend running both databases on the same Postgres instance(s) due to ease of management for most deployments.
-
-We are providing this as an informational advance notice but we do not recommend taking action yet.
-We will have another update communicated (as well as the deprecation note) when we recommend admins to start the migration process.
-
-This change provides additional scalability for the largest of GitLab instances, like GitLab.com.
-This change applies to all installation methods: Omnibus GitLab, GitLab Helm chart, GitLab Operator, GitLab Docker images, and installation from source.
-Before upgrading to GitLab 17.0, please ensure you have [migrated](https://docs.gitlab.com/ee/administration/postgresql/multiple_databases.html) to two databases.
-
-</div>
-
-<div class="deprecation breaking-change" data-milestone="17.0">
-
### Security policy field `newly_detected` is deprecated
<div class="deprecation-notes">
@@ -892,8 +1007,6 @@ For updates and details about this deprecation, follow [this epic](https://gitla
- To discuss this change or learn more, see the [deprecation issue](https://gitlab.com/gitlab-org/gitlab/-/issues/387898).
</div>
-This deprecation is now superseded by another [deprecation notice](#running-a-single-database-is-deprecated).
-
Previously, [GitLab's database](https://docs.gitlab.com/omnibus/settings/database.html)
configuration had a single `main:` section. This is being deprecated. The new
configuration has both a `main:` and a `ci:` section.
@@ -926,6 +1039,24 @@ we'll be introducing support in [this epic](https://gitlab.com/groups/gitlab-org
<div class="deprecation breaking-change" data-milestone="17.0">
+### The GitHub importer Rake task
+
+<div class="deprecation-notes">
+- Announced in GitLab <span class="milestone">16.6</span>
+- Removal in GitLab <span class="milestone">17.0</span> ([breaking change](https://docs.gitlab.com/ee/update/terminology.html#breaking-change))
+- To discuss this change or learn more, see the [deprecation issue](https://gitlab.com/gitlab-org/gitlab/-/issues/428225).
+</div>
+
+In GitLab 16.6 the [GitHub importer Rake task](https://docs.gitlab.com/ee/administration/raketasks/github_import.html) is deprecated. The Rake task lacks several features that are supported by the API and is not actively maintained.
+
+In GitLab 17.0, the Rake task will be removed.
+
+Instead, GitHub repositories can be imported by using the [API](https://docs.gitlab.com/ee/api/import.html#import-repository-from-github) or the [UI](https://docs.gitlab.com/ee/user/project/import/github.html).
+
+</div>
+
+<div class="deprecation breaking-change" data-milestone="17.0">
+
### The GitLab legacy requirement IID is deprecated in favor of work item IID
<div class="deprecation-notes">
@@ -1142,6 +1273,29 @@ Previous work helped [align the vulnerabilities calls for pipeline security tabs
</div>
</div>
+<div class="milestone-wrapper" data-milestone="16.9">
+
+## GitLab 16.9
+
+<div class="deprecation " data-milestone="16.9">
+
+### Deprecation of `lfs_check` feature flag
+
+<div class="deprecation-notes">
+- Announced in GitLab <span class="milestone">16.6</span>
+- Removal in GitLab <span class="milestone">16.9</span>
+- To discuss this change or learn more, see the [deprecation issue](https://gitlab.com/gitlab-org/gitlab/-/issues/233550).
+</div>
+
+In GitLab 16.9, we will remove the `lfs_check` feature flag. This feature flag was [introduced 4 years ago](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/60588) and controls whether the LFS integrity check is enabled. The feature flag is enabled by default, but some customers experienced performance issues with the LFS integrity check and explicitly disabled it.
+
+After [dramatically improving the performance](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/61355) of the LFS integrity check, we are ready to remove the feature flag. After the flag is removed, the feature will automatically be turned on for any environment in which it is currently disabled.
+
+If this feature flag is disabled for your environment, and you are concerned about performance issues, please enable it and monitor the performance before it is removed in 16.9. If you see any performance issues after enabling it, please let us know in [this feedback issue](https://gitlab.com/gitlab-org/gitlab/-/issues/233550).
+
+</div>
+</div>
+
<div class="milestone-wrapper" data-milestone="16.8">
## GitLab 16.8
@@ -1206,6 +1360,33 @@ If you have [public or internal](https://docs.gitlab.com/ee/user/public_access.h
Enabling the `ldap_settings_unlock_groups_by_owners` feature flag allowed non-LDAP synced users to be added to a locked LDAP group. This [feature](https://gitlab.com/gitlab-org/gitlab/-/issues/1793) has always been disabled by default and behind a feature flag. We are removing this feature to keep continuity with our SAML integration, and because allowing non-synced group members defeats the "single source of truth" principle of using a directory service. Once this feature is removed, any LDAP group members that are not synced with LDAP will lose access to that group.
</div>
+
+<div class="deprecation breaking-change" data-milestone="16.5">
+
+### Geo: Housekeeping Rake tasks
+
+<div class="deprecation-notes">
+- Announced in GitLab <span class="milestone">16.3</span>
+- Removal in GitLab <span class="milestone">16.5</span> ([breaking change](https://docs.gitlab.com/ee/update/terminology.html#breaking-change))
+- To discuss this change or learn more, see the [deprecation issue](https://gitlab.com/gitlab-org/gitlab/-/issues/416384).
+</div>
+
+As part of the migration of the replication and verification to the
+[Geo self-service framework (SSF)](https://docs.gitlab.com/ee/development/geo/framework.html),
+the legacy replication for project repositories has been
+[removed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/130565).
+As a result, the following Rake tasks that relied on legacy code have also been removed. The work invoked by these Rake tasks are now triggered automatically either periodically or based on trigger events.
+
+| Rake task | Replacement |
+| --------- | ----------- |
+| `geo:git:housekeeping:full_repack` | [Moved to UI](https://docs.gitlab.com/ee/administration/housekeeping.html#heuristical-housekeeping). No equivalent Rake task in the SSF. |
+| `geo:git:housekeeping:gc` | Always executed for new repositories, and then when it's needed. No equivalent Rake task in the SSF. |
+| `geo:git:housekeeping:incremental_repack` | Executed when needed. No equivalent Rake task in the SSF. |
+| `geo:run_orphaned_project_registry_cleaner` | Executed regularly by a registry [consistency worker](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/app/workers/geo/secondary/registry_consistency_worker.rb) which removes orphaned registries. No equivalent Rake task in the SSF. |
+| `geo:verification:repository:reset` | Moved to UI. No equivalent Rake task in the SSF. |
+| `geo:verification:wiki:reset` | Moved to UI. No equivalent Rake task in the SSF. |
+
+</div>
</div>
<div class="milestone-wrapper" data-milestone="16.3">
diff --git a/doc/update/versions/gitlab_15_changes.md b/doc/update/versions/gitlab_15_changes.md
index 019b8929a45..bd5efef8f1b 100644
--- a/doc/update/versions/gitlab_15_changes.md
+++ b/doc/update/versions/gitlab_15_changes.md
@@ -136,6 +136,7 @@ if you can't upgrade to 15.11.12 and later.
- `pg_upgrade` fails to upgrade the bundled PostregSQL database to version 13. See
[the details and workaround](#pg_upgrade-fails-to-upgrade-the-bundled-postregsql-database-to-version-13).
+- Cloning LFS objects from secondary site downloads from the primary site even when secondary is fully synced. See [the details and workaround](gitlab_16_changes.md#cloning-lfs-objects-from-secondary-site-downloads-from-the-primary-site-even-when-secondary-is-fully-synced).
## 15.9.0
@@ -181,6 +182,7 @@ if you can't upgrade to 15.11.12 and later.
- `pg_upgrade` fails to upgrade the bundled PostregSQL database to version 13. See
[the details and workaround](#pg_upgrade-fails-to-upgrade-the-bundled-postregsql-database-to-version-13).
+- Cloning LFS objects from secondary site downloads from the primary site even when secondary is fully synced. See [the details and workaround](gitlab_16_changes.md#cloning-lfs-objects-from-secondary-site-downloads-from-the-primary-site-even-when-secondary-is-fully-synced).
## 15.8.2
@@ -212,6 +214,7 @@ if you can't upgrade to 15.11.12 and later.
- We discovered an issue where [replication and verification of projects and wikis was not keeping up](https://gitlab.com/gitlab-org/gitlab/-/issues/387980) on small number of Geo installations. Your installation may be affected if you see some projects and/or wikis persistently in the "Queued" state for verification. This can lead to data loss after a failover.
- Affected versions: GitLab versions 15.6.x, 15.7.x, and 15.8.0 - 15.8.2.
- Versions containing fix: GitLab 15.8.3 and later.
+- Cloning LFS objects from secondary site downloads from the primary site even when secondary is fully synced. See [the details and workaround](gitlab_16_changes.md#cloning-lfs-objects-from-secondary-site-downloads-from-the-primary-site-even-when-secondary-is-fully-synced).
## 15.7.6
@@ -324,6 +327,7 @@ if you can't upgrade to 15.11.12 and later.
contents printed. For example, if they were printed in an echo output. For more information,
see [Understanding the file type variable expansion change in GitLab 15.7](https://about.gitlab.com/blog/2023/02/13/impact-of-the-file-type-variable-change-15-7/).
- Due to [a bug introduced in GitLab 15.4](https://gitlab.com/gitlab-org/gitlab/-/issues/390155), if one or more Git repositories in Gitaly Cluster is [unavailable](../../administration/gitaly/recovery.md#unavailable-repositories), then [Repository checks](../../administration/repository_checks.md#repository-checks) and [Geo replication and verification](../../administration/geo/index.md) stop running for all project or project wiki repositories in the affected Gitaly Cluster. The bug was fixed by [reverting the change in GitLab 15.9.0](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/110823). Before upgrading to this version, check if you have any "unavailable" repositories. See [the bug issue](https://gitlab.com/gitlab-org/gitlab/-/issues/390155) for more information.
+- Cloning LFS objects from secondary site downloads from the primary site even when secondary is fully synced. See [the details and workaround](gitlab_16_changes.md#cloning-lfs-objects-from-secondary-site-downloads-from-the-primary-site-even-when-secondary-is-fully-synced).
### Geo installations **(PREMIUM SELF)**
@@ -441,6 +445,7 @@ potentially cause downtime.
- Affected versions: GitLab versions 15.6.x, 15.7.x, and 15.8.0 - 15.8.2.
- Versions containing fix: GitLab 15.8.3 and later.
- Due to [a bug introduced in GitLab 15.4](https://gitlab.com/gitlab-org/gitlab/-/issues/390155), if one or more Git repositories in Gitaly Cluster is [unavailable](../../administration/gitaly/recovery.md#unavailable-repositories), then [Repository checks](../../administration/repository_checks.md#repository-checks) and [Geo replication and verification](../../administration/geo/index.md) stop running for all project or project wiki repositories in the affected Gitaly Cluster. The bug was fixed by [reverting the change in GitLab 15.9.0](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/110823). Before upgrading to this version, check if you have any "unavailable" repositories. See [the bug issue](https://gitlab.com/gitlab-org/gitlab/-/issues/390155) for more information.
+- Cloning LFS objects from secondary site downloads from the primary site even when secondary is fully synced. See [the details and workaround](gitlab_16_changes.md#cloning-lfs-objects-from-secondary-site-downloads-from-the-primary-site-even-when-secondary-is-fully-synced).
## 15.5.5
@@ -502,6 +507,7 @@ potentially cause downtime.
- `pg_upgrade` fails to upgrade the bundled PostregSQL database to version 13. See
[the details and workaround](#pg_upgrade-fails-to-upgrade-the-bundled-postregsql-database-to-version-13).
+- Cloning LFS objects from secondary site downloads from the primary site even when secondary is fully synced. See [the details and workaround](gitlab_16_changes.md#cloning-lfs-objects-from-secondary-site-downloads-from-the-primary-site-even-when-secondary-is-fully-synced).
## 15.4.6
@@ -576,6 +582,7 @@ potentially cause downtime.
- `pg_upgrade` fails to upgrade the bundled PostregSQL database to version 13. See
[the details and workaround](#pg_upgrade-fails-to-upgrade-the-bundled-postregsql-database-to-version-13).
+- Cloning LFS objects from secondary site downloads from the primary site even when secondary is fully synced. See [the details and workaround](gitlab_16_changes.md#cloning-lfs-objects-from-secondary-site-downloads-from-the-primary-site-even-when-secondary-is-fully-synced).
## 15.3.4
@@ -666,6 +673,7 @@ This issue is resolved in GitLab 15.3.3, so customers with the following configu
- LFS is enabled.
- LFS objects are being replicated across Geo sites.
- Repositories are being pulled by using a Geo secondary site.
+- Cloning LFS objects from secondary site downloads from the primary site even when secondary is fully synced. See [the details and workaround](gitlab_16_changes.md#cloning-lfs-objects-from-secondary-site-downloads-from-the-primary-site-even-when-secondary-is-fully-synced).
#### Incorrect object storage LFS file deletion on secondary sites
@@ -722,6 +730,7 @@ A [license caching issue](https://gitlab.com/gitlab-org/gitlab/-/issues/376706)
[the details and workaround](#lfs-transfers-redirect-to-primary-from-secondary-site-mid-session).
- Incorrect object storage LFS files deletion on Geo secondary sites. See
[the details and workaround](#incorrect-object-storage-lfs-file-deletion-on-secondary-sites).
+- Cloning LFS objects from secondary site downloads from the primary site even when secondary is fully synced. See [the details and workaround](gitlab_16_changes.md#cloning-lfs-objects-from-secondary-site-downloads-from-the-primary-site-even-when-secondary-is-fully-synced).
## 15.1.0
@@ -760,6 +769,7 @@ A [license caching issue](https://gitlab.com/gitlab-org/gitlab/-/issues/376706)
[the details and workaround](#lfs-transfers-redirect-to-primary-from-secondary-site-mid-session).
- Incorrect object storage LFS files deletion on Geo secondary sites. See
[the details and workaround](#incorrect-object-storage-lfs-file-deletion-on-secondary-sites).
+- Cloning LFS objects from secondary site downloads from the primary site even when secondary is fully synced. See [the details and workaround](gitlab_16_changes.md#cloning-lfs-objects-from-secondary-site-downloads-from-the-primary-site-even-when-secondary-is-fully-synced).
## 15.0.0
diff --git a/doc/update/versions/gitlab_16_changes.md b/doc/update/versions/gitlab_16_changes.md
index 7c5dd8ae6ae..836f5d188c5 100644
--- a/doc/update/versions/gitlab_16_changes.md
+++ b/doc/update/versions/gitlab_16_changes.md
@@ -30,6 +30,52 @@ For more information about upgrading GitLab Helm Chart, see [the release notes f
- [Praefect configuration structure change](#praefect-configuration-structure-change).
- [Gitaly configuration structure change](#gitaly-configuration-structure-change).
+## 16.5.0
+
+- Git 2.42.0 and later is required by Gitaly. For self-compiled installations, you should use the [Git version provided by Gitaly](../../install/installation.md#git).
+
+### Geo installations
+
+Specific information applies to installations using Geo:
+
+- A number of Prometheus metrics were incorrectly removed in 16.3.0, which can break dashboards and alerting:
+
+ | Affected metric | Metric restored in 16.5.2 and later | Replacement available in 16.3+ |
+ | ---------------------------------------- | ------------------------------------ | ---------------------------------------------- |
+ | `geo_repositories_synced` | Yes | `geo_project_repositories_synced` |
+ | `geo_repositories_failed` | Yes | `geo_project_repositories_failed` |
+ | `geo_repositories_checksummed` | Yes | `geo_project_repositories_checksummed` |
+ | `geo_repositories_checksum_failed` | Yes | `geo_project_repositories_checksum_failed` |
+ | `geo_repositories_verified` | Yes | `geo_project_repositories_verified` |
+ | `geo_repositories_verification_failed` | Yes | `geo_project_repositories_verification_failed` |
+ | `geo_repositories_checksum_mismatch` | No | None available |
+ | `geo_repositories_retrying_verification` | No | None available |
+
+ - Impacted versions:
+ - 16.3.0 to 16.5.1
+ - Versions containing fix:
+ - 16.5.2 and later
+
+ For more information, see [issue 429617](https://gitlab.com/gitlab-org/gitlab/-/issues/429617).
+
+- [Object storage verification](https://about.gitlab.com/releases/2023/09/22/gitlab-16-4-released/#geo-verifies-object-storage) was added in GitLab 16.4. Due to an [issue](https://gitlab.com/gitlab-org/gitlab/-/issues/429242) some Geo installations are reporting high memory usage which can lead to the GitLab application on the primary becoming unresponsive.
+
+ Your installation may be impacted if you have configured it to use [object storage](../../administration/object_storage.md) and have enabled [GitLab-managed object storage replication](../../administration/geo/replication/object_storage.md#enabling-gitlab-managed-object-storage-replication)
+
+ Until this is fixed, the workaround is to disable object storage verification.
+ Run the following command on one of the Rails nodes on the primary site:
+
+ ```shell
+ sudo gitlab-rails runner 'Feature.disable(:geo_object_storage_verification)'
+ ```
+
+ **Affected releases**:
+
+ | Affected minor releases | Affected patch releases | Fixed in |
+ | ------ | ------ | ------ |
+ | 16.4 | All | None |
+ | 16.5 | All | None |
+
## 16.4.0
- Updating a group path [received a bug fix](https://gitlab.com/gitlab-org/gitlab/-/issues/419289) that uses a database index introduced in 16.3.
@@ -71,9 +117,33 @@ For more information about upgrading GitLab Helm Chart, see [the release notes f
SELECT id FROM push_rules WHERE LENGTH(delete_branch_regex) > 511;
```
+ To find out if a push rule belongs to a project, group, or instance, run this script
+ in the [Rails console](../../administration/operations/rails_console.md#starting-a-rails-console-session):
+
+ ```ruby
+ # replace `delete_branch_regex` with a name of the field used in constraint
+ long_rules = PushRule.where("length(delete_branch_regex) > 511")
+
+ array = long_rules.map do |lr|
+ if lr.project
+ "Push rule with ID #{lr.id} is configured in a project #{lr.project.full_name}"
+ elsif lr.group
+ "Push rule with ID #{lr.id} is configured in a group #{lr.group.full_name}"
+ else
+ "Push rule with ID #{lr.id} is configured on the instance level"
+ end
+ end
+
+ puts "Total long rules: #{array.count}"
+ puts array.join("\n")
+ ```
+
Reduce the value length of the regex field for affected push rules records, then
retry the migration.
+ If you have too many affected push rules, and you can't update them through the GitLab UI,
+ contact [GitLab support](https://about.gitlab.com/support/).
+
### Self-compiled installations
- A new method of configuring paths for the GitLab secret and custom hooks is preferred in GitLab 16.4 and later:
@@ -82,6 +152,57 @@ For more information about upgrading GitLab Helm Chart, see [the release notes f
server-side custom hooks.
1. Remove the `[gitlab-shell] dir` configuration.
+### Geo installations
+
+Specific information applies to installations using Geo:
+
+- A number of Prometheus metrics were incorrectly removed in 16.3.0, which can break dashboards and alerting:
+
+ | Affected metric | Metric restored in 16.5.2 and later | Replacement available in 16.3+ |
+ | ---------------------------------------- | ------------------------------------ | ---------------------------------------------- |
+ | `geo_repositories_synced` | Yes | `geo_project_repositories_synced` |
+ | `geo_repositories_failed` | Yes | `geo_project_repositories_failed` |
+ | `geo_repositories_checksummed` | Yes | `geo_project_repositories_checksummed` |
+ | `geo_repositories_checksum_failed` | Yes | `geo_project_repositories_checksum_failed` |
+ | `geo_repositories_verified` | Yes | `geo_project_repositories_verified` |
+ | `geo_repositories_verification_failed` | Yes | `geo_project_repositories_verification_failed` |
+ | `geo_repositories_checksum_mismatch` | No | None available |
+ | `geo_repositories_retrying_verification` | No | None available |
+
+ - Impacted versions:
+ - 16.3.0 to 16.5.1
+ - Versions containing fix:
+ - 16.5.2 and later
+
+ For more information, see [issue 429617](https://gitlab.com/gitlab-org/gitlab/-/issues/429617).
+
+- [Object storage verification](https://about.gitlab.com/releases/2023/09/22/gitlab-16-4-released/#geo-verifies-object-storage) was added in GitLab 16.4. Due to an [issue](https://gitlab.com/gitlab-org/gitlab/-/issues/429242) some Geo installations are reporting high memory usage which can lead to the GitLab application on the primary becoming unresponsive.
+
+ Your installation may be impacted if you have configured it to use [object storage](../../administration/object_storage.md) and have enabled [GitLab-managed object storage replication](../../administration/geo/replication/object_storage.md#enabling-gitlab-managed-object-storage-replication)
+
+ Until this is fixed, the workaround is to disable object storage verification.
+ Run the following command on one of the Rails nodes on the primary site:
+
+ ```shell
+ sudo gitlab-rails runner 'Feature.disable(:geo_object_storage_verification)'
+ ```
+
+ **Affected releases**:
+
+ | Affected minor releases | Affected patch releases | Fixed in |
+ | ------ | ------ | ------ |
+ | 16.4 | All | None |
+ | 16.5 | All | None |
+
+- An [issue](https://gitlab.com/gitlab-org/gitlab/-/issues/419370) with sync states getting stuck in pending state results in replication being stuck indefinitely for impacted items leading to risk of data loss in the event of a failover. This mostly impact repository syncs but can also can also affect container registry syncs. You are advised to upgrade to a fixed version to avoid risk of data loss.
+
+ **Affected releases**:
+
+ | Affected minor releases | Affected patch releases | Fixed in |
+ | ------ | ------ | ------ |
+ | 16.3 | 16.3.0 - 16.3.5 | 16.3.6 |
+ | 16.4 | 16.4.0 - 16.4.1 | 16.4.2 |
+
## 16.3.0
- **Update to GitLab 16.3.5 or later**. This avoids [issue 425971](https://gitlab.com/gitlab-org/gitlab/-/issues/425971) that causes an excessive use of database disk space for GitLab 16.3.3 and 16.3.4.
@@ -149,6 +270,35 @@ Specific information applies to installations using Geo:
For more information, see [issue 425224](https://gitlab.com/gitlab-org/gitlab/-/issues/425224).
+- A number of Prometheus metrics were incorrectly removed in 16.3.0, which can break dashboards and alerting:
+
+ | Affected metric | Metric restored in 16.5.2 and later | Replacement available in 16.3+ |
+ | ---------------------------------------- | ------------------------------------ | ---------------------------------------------- |
+ | `geo_repositories_synced` | Yes | `geo_project_repositories_synced` |
+ | `geo_repositories_failed` | Yes | `geo_project_repositories_failed` |
+ | `geo_repositories_checksummed` | Yes | `geo_project_repositories_checksummed` |
+ | `geo_repositories_checksum_failed` | Yes | `geo_project_repositories_checksum_failed` |
+ | `geo_repositories_verified` | Yes | `geo_project_repositories_verified` |
+ | `geo_repositories_verification_failed` | Yes | `geo_project_repositories_verification_failed` |
+ | `geo_repositories_checksum_mismatch` | No | None available |
+ | `geo_repositories_retrying_verification` | No | None available |
+
+ - Impacted versions:
+ - 16.3.0 to 16.5.1
+ - Versions containing fix:
+ - 16.5.2 and later
+
+ For more information, see [issue 429617](https://gitlab.com/gitlab-org/gitlab/-/issues/429617).
+
+- An [issue](https://gitlab.com/gitlab-org/gitlab/-/issues/419370) with sync states getting stuck in pending state results in replication being stuck indefinitely for impacted items leading to risk of data loss in the event of a failover. This mostly impact repository syncs but can also can also affect container registry syncs. You are advised to upgrade to a fixed version to avoid risk of data loss.
+
+ **Affected releases**:
+
+ | Affected minor releases | Affected patch releases | Fixed in |
+ | ------ | ------ | ------ |
+ | 16.3 | 16.3.0 - 16.3.5 | 16.3.6 |
+ | 16.4 | 16.4.0 - 16.4.1 | 16.4.2 |
+
## 16.2.0
- Legacy LDAP configuration settings may cause
@@ -227,6 +377,24 @@ Specific information applies to installations using Geo:
Affected artifacts are automatically resynced upon upgrade to 16.1.5, 16.2.5, 16.3.1, 16.4.0, or later.
You can [manually resync affected job artifacts](https://gitlab.com/gitlab-org/gitlab/-/issues/419742#to-fix-data) if needed.
+#### Cloning LFS objects from secondary site downloads from the primary site even when secondary is fully synced
+
+A [bug](https://gitlab.com/gitlab-org/gitlab/-/issues/410413) in the Geo proxying logic for LFS objects meant that all LFS clone requests against a secondary site are proxied to the primary even if the secondary site is up-to-date. This can result in increased load on the primary site and longer access times for LFS objects for users cloning from the secondary site.
+
+In GitLab 15.1 proxying was enabled by default.
+
+You are not impacted:
+
+- If your installation is not configured to use LFS objects
+- If you do not use Geo to accelerate remote users
+- If you are using Geo to accelerate remote users but have disabled proxying
+
+| Affected minor releases | Affected patch releases | Fixed in |
+|-------------------------|-------------------------|----------|
+| 15.1 - 16.2 | All | 16.3 and later |
+
+Workaround: A possible workaround is to [disable proxying](../../administration/geo/secondary_proxy/index.md#disable-geo-proxying). Note that the secondary site fails to serve LFS files that have not been replicated at the time of cloning.
+
## 16.1.0
- A `BackfillPreparedAtMergeRequests` background migration is finalized with
@@ -273,6 +441,7 @@ Specific information applies to installations using Geo:
- While running an affected version, artifacts which appeared to become synced may actually be missing on the secondary site.
Affected artifacts are automatically resynced upon upgrade to 16.1.5, 16.2.5, 16.3.1, 16.4.0, or later.
You can [manually resync affected job artifacts](https://gitlab.com/gitlab-org/gitlab/-/issues/419742#to-fix-data) if needed.
+ - Cloning LFS objects from secondary site downloads from the primary site even when secondary is fully synced. See [the details and workaround](#cloning-lfs-objects-from-secondary-site-downloads-from-the-primary-site-even-when-secondary-is-fully-synced).
#### Wiki repositories not initialized on project creation
@@ -302,6 +471,7 @@ by this issue.
[throw errors on startup](../../install/docker.md#threaderror-cant-create-thread-operation-not-permitted).
- Starting with 16.0, GitLab self-managed installations now have two database connections by default, instead of one. This change doubles the number of PostgreSQL connections. It makes self-managed versions of GitLab behave similarly to GitLab.com, and is a step toward enabling a separate database for CI features for self-managed versions of GitLab. Before upgrading to 16.0, determine if you need to [increase max connections for PostgreSQL](https://docs.gitlab.com/omnibus/settings/database.html#configuring-multiple-database-connections).
- This change applies to installation methods with Linux packages (Omnibus), GitLab Helm chart, GitLab Operator, GitLab Docker images, and self-compiled installations.
+- Container registry using Azure storage might be empty with zero tags. You can fix this by following the [breaking change instructions](../deprecations.md#azure-storage-driver-defaults-to-the-correct-root-prefix).
### Linux package installations
@@ -334,6 +504,7 @@ Specific information applies to installations using Geo:
- Some project imports do not initialize wiki repositories on project creation. See
[the details and workaround](#wiki-repositories-not-initialized-on-project-creation).
+- Cloning LFS objects from secondary site downloads from the primary site even when secondary is fully synced. See [the details and workaround](#cloning-lfs-objects-from-secondary-site-downloads-from-the-primary-site-even-when-secondary-is-fully-synced).
### Gitaly configuration structure change
diff --git a/doc/user/ai_features.md b/doc/user/ai_features.md
index e24d50efee1..222752a4561 100644
--- a/doc/user/ai_features.md
+++ b/doc/user/ai_features.md
@@ -7,43 +7,37 @@ type: index, reference
# GitLab Duo
+> - [First GitLab Duo features introduced](https://about.gitlab.com/blog/2023/05/03/gitlab-ai-assisted-features/) in GitLab 16.0.
+> - [Removed third-party AI setting](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/136144) in GitLab 16.6.
+> - [Removed support for OpenAI from all GitLab Duo features](https://gitlab.com/groups/gitlab-org/-/epics/10964) in GitLab 16.6.
+
GitLab is creating AI-assisted features across our DevSecOps platform. These features aim to help increase velocity and solve key pain points across the software development lifecycle.
| Feature | Purpose | Large Language Model | Current availability | Maturity |
|-|-|-|-|-|
| [Suggested Reviewers](project/merge_requests/reviews/index.md#gitlab-duo-suggested-reviewers) | Assists in creating faster and higher-quality reviews by automatically suggesting reviewers for your merge request. | GitLab creates a machine learning model for each project, which is used to generate reviewers <br><br> [View the issue](https://gitlab.com/gitlab-org/modelops/applied-ml/applied-ml-updates/-/issues/10) | SaaS only <br><br> Ultimate tier | [Generally Available (GA)](../policy/experiment-beta-support.md#generally-available-ga) |
-| [Code Suggestions](project/repository/code_suggestions/index.md) | Helps you write code more efficiently by viewing code suggestions as you type. | [`code-gecko`](https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/code-completion) and [`code-bison`](https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/code-generation) <br><br> [Anthropic's Claude](https://www.anthropic.com/product) model | SaaS <br> Self-managed <br><br> All tiers | [Beta](../policy/experiment-beta-support.md#beta) |
-| [Vulnerability summary](application_security/vulnerabilities/index.md#explaining-a-vulnerability) | Helps you remediate vulnerabilities more efficiently, uplevel your skills, and write more secure code. | [`text-bison`](https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/text) <br><br> Anthropic's claude model if degraded performance | SaaS only <br><br> Ultimate tier | [Beta](../policy/experiment-beta-support.md#beta) |
-| [Code explanation](#explain-code-in-the-web-ui-with-code-explanation) | Helps you understand code by explaining it in English language. | [`codechat-bison`](https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/code-chat) | SaaS only <br><br> Ultimate tier | [Experiment](../policy/experiment-beta-support.md#experiment) |
-| [GitLab Duo Chat](#answer-questions-with-gitlab-duo-chat) | Process and generate text and code in a conversational manner. Helps you quickly identify useful information in large volumes of text in issues, epics, code, and GitLab documentation. | Anthropic's claude model <br><br> OpenAI Embeddings | SaaS only <br><br> Ultimate tier | [Experiment](../policy/experiment-beta-support.md#experiment) |
+| [Code Suggestions](project/repository/code_suggestions/index.md) | Helps you write code more efficiently by viewing code suggestions as you type. | For Code Completion: Vertex AI Codey [`code-gecko`](https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/code-completion) <br><br> For Code Generation: Anthropic [`Claude-2`](https://docs.anthropic.com/claude/reference/selecting-a-model)| [SaaS: All tiers](project/repository/code_suggestions/saas.md) <br><br> [Self-managed: Premium and Ultimate with Cloud Licensing](project/repository/code_suggestions/self_managed.md) | [Beta](../policy/experiment-beta-support.md#beta) |
+| [Vulnerability summary](application_security/vulnerabilities/index.md#explaining-a-vulnerability) | Helps you remediate vulnerabilities more efficiently, boost your skills, and write more secure code. | Vertex AI Codey [`text-bison`](https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/text) <br><br> Anthropic [`Claude-2`](https://docs.anthropic.com/claude/reference/selecting-a-model) if degraded performance | SaaS only <br><br> Ultimate tier | [Beta](../policy/experiment-beta-support.md#beta) |
+| [Code explanation](#explain-code-in-the-web-ui-with-code-explanation) | Helps you understand code by explaining it in English language. | Vertex AI Codey [`codechat-bison`](https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/code-chat) | SaaS only <br><br> Ultimate tier | [Experiment](../policy/experiment-beta-support.md#experiment) |
+| [GitLab Duo Chat](gitlab_duo_chat.md) | Process and generate text and code in a conversational manner. Helps you quickly identify useful information in large volumes of text in issues, epics, code, and GitLab documentation. | Anthropic [`Claude-2`](https://docs.anthropic.com/claude/reference/selecting-a-model) <br><br> Vertex AI Codey [`textembedding-gecko`](https://cloud.google.com/vertex-ai/docs/generative-ai/embeddings/get-text-embeddings) | SaaS only <br><br> Ultimate tier | [Experiment](../policy/experiment-beta-support.md#experiment) |
| [Value stream forecasting](#forecast-deployment-frequency-with-value-stream-forecasting) | Assists you with predicting productivity metrics and identifying anomalies across your software development lifecycle. | Statistical forecasting | SaaS only <br> Self-managed <br><br> Ultimate tier | [Experiment](../policy/experiment-beta-support.md#experiment) |
-| [Discussion summary](#summarize-issue-discussions-with-discussion-summary) | Assists with quickly getting everyone up to speed on lengthy conversations to help ensure you are all on the same page. | OpenAI's GPT-3 | SaaS only <br><br> Ultimate tier | [Experiment](../policy/experiment-beta-support.md#experiment) |
-| [Merge request summary](project/merge_requests/ai_in_merge_requests.md#summarize-merge-request-changes) | Efficiently communicate the impact of your merge request changes. | [`text-bison`](https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/text) | SaaS only <br><br> Ultimate tier | [Experiment](../policy/experiment-beta-support.md#experiment) |
-| [Code review summary](project/merge_requests/ai_in_merge_requests.md#summarize-my-merge-request-review) | Helps ease merge request handoff between authors and reviewers and help reviewers efficiently understand suggestions. | [`text-bison`](https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/text) | SaaS only <br><br> Ultimate tier | [Experiment](../policy/experiment-beta-support.md#experiment) |
-| [Merge request template population](project/merge_requests/ai_in_merge_requests.md#fill-in-merge-request-templates) | Generate a description for the merge request based on the contents of the template. | [`text-bison`](https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/text) | SaaS only <br><br> Ultimate tier | [Experiment](../policy/experiment-beta-support.md#experiment) |
-| [Test generation](project/merge_requests/ai_in_merge_requests.md#generate-suggested-tests-in-merge-requests) | Automates repetitive tasks and helps catch bugs early. | [`text-bison`](https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/text) | SaaS only <br><br> Ultimate tier | [Experiment](../policy/experiment-beta-support.md#experiment) |
-| [Git suggestions](https://gitlab.com/gitlab-org/gitlab/-/issues/409636) | Helps you discover or recall Git commands when and where you need them. | [Google Vertex Codey APIs](https://cloud.google.com/vertex-ai/docs/generative-ai/code/code-models-overview) | SaaS only <br><br> Ultimate tier | [Experiment](../policy/experiment-beta-support.md#experiment) |
-| [Root cause analysis](#root-cause-analysis) | Assists you in determining the root cause for a pipeline failure and failed CI/CD build. | [Google Vertex Codey APIs](https://cloud.google.com/vertex-ai/docs/generative-ai/code/code-models-overview) | SaaS only <br><br> Ultimate tier | [Experiment](../policy/experiment-beta-support.md#experiment) |
-| [Issue description generation](#summarize-an-issue-with-issue-description-generation) | Generate issue descriptions. | OpenAI's GPT-3 | SaaS only <br><br> Ultimate tier | [Experiment](../policy/experiment-beta-support.md#experiment) |
+| [Discussion summary](#summarize-issue-discussions-with-discussion-summary) | Assists with quickly getting everyone up to speed on lengthy conversations to help ensure you are all on the same page. | Vertex AI Codey [`text-bison`](https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/text) | SaaS only <br><br> Ultimate tier | [Experiment](../policy/experiment-beta-support.md#experiment) |
+| [Merge request summary](project/merge_requests/ai_in_merge_requests.md#summarize-merge-request-changes) | Efficiently communicate the impact of your merge request changes. | Vertex AI Codey [`text-bison`](https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/text) | SaaS only <br><br> Ultimate tier | [Experiment](../policy/experiment-beta-support.md#experiment) |
+| [Code review summary](project/merge_requests/ai_in_merge_requests.md#summarize-my-merge-request-review) | Helps ease merge request handoff between authors and reviewers and help reviewers efficiently understand suggestions. | Vertex AI Codey [`text-bison`](https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/text) | SaaS only <br><br> Ultimate tier | [Experiment](../policy/experiment-beta-support.md#experiment) |
+| [Merge request template population](project/merge_requests/ai_in_merge_requests.md#fill-in-merge-request-templates) | Generate a description for the merge request based on the contents of the template. | Vertex AI Codey [`text-bison`](https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/text) | SaaS only <br><br> Ultimate tier | [Experiment](../policy/experiment-beta-support.md#experiment) |
+| [Test generation](project/merge_requests/ai_in_merge_requests.md#generate-suggested-tests-in-merge-requests) | Automates repetitive tasks and helps catch bugs early. | Vertex AI Codey [`text-bison`](https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/text) | SaaS only <br><br> Ultimate tier | [Experiment](../policy/experiment-beta-support.md#experiment) |
+| [Git suggestions](https://gitlab.com/gitlab-org/gitlab/-/issues/409636) | Helps you discover or recall Git commands when and where you need them. | Vertex AI Codey [`codechat-bison`](https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/code-chat) | SaaS only <br><br> Ultimate tier | [Experiment](../policy/experiment-beta-support.md#experiment) |
+| [Root cause analysis](#root-cause-analysis) | Assists you in determining the root cause for a pipeline failure and failed CI/CD build. | Vertex AI Codey [`text-bison`](https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/text) | SaaS only <br><br> Ultimate tier | [Experiment](../policy/experiment-beta-support.md#experiment) |
+| [Issue description generation](#summarize-an-issue-with-issue-description-generation) | Generate issue descriptions. | Anthropic [`Claude-2`](https://docs.anthropic.com/claude/reference/selecting-a-model) | SaaS only <br><br> Ultimate tier | [Experiment](../policy/experiment-beta-support.md#experiment) |
## Enable AI/ML features
-- Third-party AI features
- - All features built on large language models (LLM) from Google,
- Anthropic or OpenAI (besides Code Suggestions) require that this setting is
- enabled at the group level.
- - [Generally Available](../policy/experiment-beta-support.md#generally-available-ga)
- features are available when third-party AI features are enabled.
- - Third-party AI features are enabled by default.
- - This setting is available to Ultimate groups on SaaS and can be
- set by a user who has the Owner role in the group.
- - View [how to enable this setting](group/manage.md#enable-third-party-ai-features).
- Experiment and Beta features
- All features categorized as
[Experiment features](../policy/experiment-beta-support.md#experiment) or
[Beta features](../policy/experiment-beta-support.md#beta)
(besides Code Suggestions) require that this setting is enabled at the group
- level. This is in addition to the Third-party AI features setting.
+ level.
- Their usage is subject to the
[Testing Terms of Use](https://about.gitlab.com/handbook/legal/testing-agreement/).
- Experiment and Beta features are disabled by default.
@@ -65,7 +59,6 @@ The following subsections describe the experimental AI features in more detail.
To use this feature:
- The parent group of the project must:
- - Enable the [third-party AI features setting](group/manage.md#enable-third-party-ai-features).
- Enable the [experiment and beta features setting](group/manage.md#enable-experiment-and-beta-features).
- You must be a member of the project with sufficient permissions to view the repository.
@@ -104,52 +97,6 @@ code in a merge request:
We cannot guarantee that the large language model produces results that are correct. Use the explanation with caution.
-### Answer questions with GitLab Duo Chat **(ULTIMATE SAAS EXPERIMENT)**
-
-> Introduced in GitLab 16.0 as an [Experiment](../policy/experiment-beta-support.md#experiment).
-
-To use this feature, at least one group you're a member of must:
-
-- Have the [third-party AI features setting](group/manage.md#enable-third-party-ai-features) enabled.
-- Have the [experiment and beta features setting](group/manage.md#enable-experiment-and-beta-features) enabled.
-
-You can get AI generated support from GitLab Duo Chat about the following topics:
-
-- How to use GitLab.
-- Questions about an issue.
-- Summarizing an issue.
-
-Example questions you might ask:
-
-- `What is a fork?`
-- `How to reset my password`
-- `Summarize the issue <link to your issue>`
-- `Summarize the description of the current issue`
-
-The examples above all use data from either the issue or the GitLab documentation. However, you can also ask to generate code, CI/CD configurations, or to explain code. For example:
-
-- `Write a hello world function in Ruby`
-- `Write a tic tac toe game in JavaScript`
-- `Write a .gitlab-ci.yml file to test and build a rails application`
-- `Explain the following code: def sum(a, b) a + b end`
-
-You can also ask follow-up questions.
-
-This is an experimental feature and we're continuously extending the capabilities and reliability of the chat.
-
-1. In the lower-left corner, select the Help icon.
- The [new left sidebar must be enabled](../tutorials/left_sidebar/index.md#enable-the-new-left-sidebar).
-1. Select **Ask in GitLab Duo Chat**. A drawer opens on the right side of your screen.
-1. Enter your question in the chat input box and press **Enter** or select **Send**. It may take a few seconds for the interactive AI chat to produce an answer.
-1. You can ask a follow-up question.
-1. If you want to ask a new question unrelated to the previous conversation, you may receive better answers if you clear the context by typing `/reset` into the input box and selecting **Send**.
-
-To give feedback about a specific response, use the feedback buttons in the response message.
-Or, you can add a comment in the [feedback issue](https://gitlab.com/gitlab-org/gitlab/-/issues/415591).
-
-NOTE:
-Only the last 50 messages are retained in the chat history. The chat history expires 3 days after last use.
-
### Summarize issue discussions with Discussion summary **(ULTIMATE SAAS EXPERIMENT)**
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/10344) in GitLab 16.0 as an [Experiment](../policy/experiment-beta-support.md#experiment).
@@ -157,7 +104,6 @@ Only the last 50 messages are retained in the chat history. The chat history exp
To use this feature:
- The parent group of the issue must:
- - Enable the [third-party AI features setting](group/manage.md#enable-third-party-ai-features).
- Enable the [experiment and beta features setting](group/manage.md#enable-experiment-and-beta-features).
- You must be a member of the project with sufficient permissions to view the issue.
@@ -181,7 +127,6 @@ language model referenced above.
To use this feature:
- The parent group of the project must:
- - Enable the [third-party AI features setting](group/manage.md#enable-third-party-ai-features).
- Enable the [experiment and beta features setting](group/manage.md#enable-experiment-and-beta-features).
- You must be a member of the project with sufficient permissions to view the CI/CD analytics.
@@ -207,7 +152,6 @@ Provide feedback on this experimental feature in [issue 416833](https://gitlab.c
To use this feature:
- The parent group of the project must:
- - Enable the [third-party AI features setting](group/manage.md#enable-third-party-ai-features).
- Enable the [experiment and beta features setting](group/manage.md#enable-experiment-and-beta-features).
- You must be a member of the project with sufficient permissions to view the CI/CD job.
@@ -222,7 +166,6 @@ reason for the failure.
To use this feature:
- The parent group of the project must:
- - Enable the [third-party AI features setting](group/manage.md#enable-third-party-ai-features).
- Enable the [experiment and beta features setting](group/manage.md#enable-experiment-and-beta-features).
- You must be a member of the project with sufficient permissions to view the issue.
@@ -239,9 +182,13 @@ Provide feedback on this experimental feature in [issue 409844](https://gitlab.c
**Data usage**: When you use this feature, the text you enter is sent to the large
language model referenced above.
+### GitLab Duo Chat **(ULTIMATE SAAS EXPERIMENT)**
+
+For details about this Experimental feature, see [GitLab Duo Chat](gitlab_duo_chat.md).
+
## Data usage
-GitLab AI features leverage generative AI to help increase velocity and aim to help make you more productive. Each feature operates independently of other features and is not required for other features to function.
+GitLab AI features leverage generative AI to help increase velocity and aim to help make you more productive. Each feature operates independently of other features and is not required for other features to function. GitLab selects the best-in-class large-language models for specific tasks. We use [Google Vertex AI Models](https://cloud.google.com/vertex-ai/docs/generative-ai/learn/overview#genai-models) and [Anthropic Claude](https://www.anthropic.com/product).
### Progressive enhancement
@@ -251,13 +198,38 @@ These features are designed as a progressive enhancement to existing GitLab feat
These features are in a variety of [feature support levels](../policy/experiment-beta-support.md#beta). Due to the nature of these features, there may be high demand for usage which may cause degraded performance or unexpected downtime of the feature. We have built these features to gracefully degrade and have controls in place to allow us to mitigate abuse or misuse. GitLab may disable **beta and experimental** features for any or all customers at any time at our discretion.
-## Third party services
-
### Data privacy
-Some AI features require the use of third-party AI services models and APIs from: Google AI and OpenAI. The processing of any personal data is in accordance with our [Privacy Statement](https://about.gitlab.com/privacy/). You may also visit the [Sub-Processors page](https://about.gitlab.com/privacy/subprocessors/#third-party-sub-processors) to see the list of our Sub-Processors that we use to provide these features.
+GitLab Duo AI features are powered by a generative AI models. The processing of any personal data is in accordance with our [Privacy Statement](https://about.gitlab.com/privacy/). You may also visit the [Sub-Processors page](https://about.gitlab.com/privacy/subprocessors/#third-party-sub-processors) to see the list of our Sub-Processors that we use to provide these features.
+
+### Data retention
+
+The below reflects the current retention periods of GitLab AI model [Sub-Processors](https://about.gitlab.com/privacy/subprocessors/#third-party-sub-processors):
+
+- Anthropic retains input and output data for 30 days.
+- Google discards input and output data immediately after the output is provided. Google currently does not store data for abuse monitoring.
+
+All of these AI providers are under data protection agreements with GitLab that prohibit the use of Customer Content for their own purposes, except to perform their independent legal obligations.
+
+### Telemetry
+
+GitLab Duo collects aggregated or de-identified first-party usage data through our [Snowplow collector](https://about.gitlab.com/handbook/business-technology/data-team/platform/snowplow/). This usage data includes the following metrics:
+
+- Number of unique users
+- Number of unique instances
+- Prompt lengths
+- Model used
+- Status code responses
+- API responses times
+
+### Training data
+
+GitLab does not train generative AI models based on private (non-public) data. The vendors we work with also do not train models based on private data.
+
+For more information on our AI [sub-processors](https://about.gitlab.com/privacy/subprocessors/#third-party-sub-processors), see:
-Group owners can control which top-level groups have access to third-party AI features by using the [group level third-party AI features setting](group/manage.md#enable-third-party-ai-features).
+- Google Vertex AI Models APIs [data governance](https://cloud.google.com/vertex-ai/docs/generative-ai/data-governance) and [responsible AI](https://cloud.google.com/vertex-ai/docs/generative-ai/learn/responsible-ai).
+- Anthropic Claude's [constitution](https://www.anthropic.com/index/claudes-constitution).
### Model accuracy and quality
diff --git a/doc/user/analytics/analytics_dashboards.md b/doc/user/analytics/analytics_dashboards.md
index 448a46fdc26..8bed8018eb8 100644
--- a/doc/user/analytics/analytics_dashboards.md
+++ b/doc/user/analytics/analytics_dashboards.md
@@ -39,6 +39,12 @@ When [product analytics](../product_analytics/index.md) is enabled and onboarded
- **Audience** displays metrics related to traffic, such as the number of users and sessions.
- **Behavior** displays metrics related to user activity, such as the number of page views and events.
+For more information about the development of product analytics, see the [group direction page](https://about.gitlab.com/direction/analytics/product-analytics/). To leave feedback about bugs or functionality:
+
+- Comment on issue [391970](https://gitlab.com/gitlab-org/gitlab/-/issues/391970).
+- Create an issue with the `group::product analytics` label.
+- [Schedule a call](https://calendly.com/jheimbuck/30-minute-call) with the team.
+
### Value Stream Management
- **Value Streams Dashboard** displays metrics related to [DevOps performance, security exposure, and workstream optimization](../analytics/value_streams_dashboard.md#devsecops-metrics-comparison-panel).
diff --git a/doc/user/analytics/dora_metrics.md b/doc/user/analytics/dora_metrics.md
index 391a1c7965f..e90bfd690ca 100644
--- a/doc/user/analytics/dora_metrics.md
+++ b/doc/user/analytics/dora_metrics.md
@@ -65,9 +65,14 @@ For software leaders, Lead time for changes reflects the efficiency of CI/CD pip
Over time, the lead time for changes should decrease, while your team's performance should increase. Low lead time for changes means more efficient CI/CD pipelines.
In GitLab, Lead time for changes is measure by the `Median time it takes for a merge request to get merged into production (from master)`.
+By default, Lead time for changes measures only one-branch operations with multiple deployment jobs (for example, jobs moving from development to staging to production jobs on the main branch).
+When a merge request gets merged in staging and then merge to production, GitLab processes them as two deployed merge requests, not one.
+
### How lead time for changes is calculated
-GitLab calculates Lead time for changes base on the number of seconds to successfully deliver a commit into production - **from** code committed **to** code successfully running in production, without adding the `coding_time` to the calculation.
+GitLab calculates Lead time for changes based on the number of seconds to successfully deliver a commit into production - **from** code committed **to** code successfully running in production, without adding the `coding_time` to the calculation.
+
+By default, Lead time for changes supports measuring only one branch operation with multiple deployment jobs (for example, from development to staging to production on the default branch). When a merge request gets merged on staging, and then on production, GitLab interprets them as two deployed merge requests, not one.
### How to improve lead time for changes
@@ -127,41 +132,37 @@ To improve this metric, you should consider:
- Improving the efficacy of code review processes.
- Adding more automated testing.
-## DORA metrics in GitLab
+## DORA custom calculation rules **(ULTIMATE ALL EXPERIMENT)**
-The DORA metrics are displayed on the following charts:
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96561) in GitLab 15.4 [with a flag](../../administration/feature_flags.md) named `dora_configuration`. Disabled by default. This feature is an [Experiment](../../policy/experiment-beta-support.md).
-- [Value Streams Dashboard](value_streams_dashboard.md), which helps you identify trends, patterns, and opportunities for improvement. DORA metrics are displayed in the [metrics comparison panel](value_streams_dashboard.md#devsecops-metrics-comparison-panel) and the [DORA Performers score panel](value_streams_dashboard.md#dora-performers-score-panel).
-- [CI/CD analytics charts](ci_cd_analytics.md), which show pipeline success rates and duration, and the history of DORA metrics over time.
-- Insights reports for [groups](../group/insights/index.md) and [projects](../group/value_stream_analytics/index.md), where you can also use [DORA query parameters](../../user/project/insights/index.md#dora-query-parameters) to create custom charts.
+FLAG:
+On self-managed GitLab, by default this feature is not available. To make it available per project or for your entire instance, an administrator can [enable the feature flag](../../administration/feature_flags.md) named `dora_configuration`.
+On GitLab.com, this feature is not available.
-The table below provides an overview of the DORA metrics' data aggregation in different charts.
+This feature is an [Experiment](../../policy/experiment-beta-support.md).
+To join the list of users testing this feature, [here is a suggested test flow](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96561#steps-to-check-on-localhost).
+If you find a bug, [open an issue here](https://gitlab.com/groups/gitlab-org/-/epics/11490).
+To share your use cases and feedback, comment in [epic 11490](https://gitlab.com/groups/gitlab-org/-/epics/11490).
-| Metric name | Measured values | Data aggregation in the [Value Streams Dashboard](value_streams_dashboard.md) | Data aggregation in [CI/CD analytics charts](ci_cd_analytics.md) | Data aggregation in [Custom insights reporting](../../user/project/insights/index.md#dora-query-parameters) |
-|---------------------------|-------------------|-----------------------------------------------------|------------------------|----------|
-| Deployment frequency | Number of successful deployments | daily average per month | daily average | `day` (default) or `month` |
-| Lead time for changes | Number of seconds to successfully deliver a commit into production | daily median per month | median time | `day` (default) or `month` |
-| Time to restore service | Number of seconds an incident was open for | daily median per month | daily median | `day` (default) or `month` |
-| Change failure rate | percentage of deployments that cause an incident in production | daily median per month | percentage of failed deployments | `day` (default) or `month` |
+### DORA Lead Time For Changes - multi-branch rule
-## Configure DORA metrics calculation **(ULTIMATE ALL BETA)**
+Unlike the default [calculation of Lead time for changes](#how-lead-time-for-changes-is-calculated), this calculation rule allows measuring multi-branch operations with a single deployment job for each operation.
+For example, from development job on development branch, to staging job on staging branch, to production job on production branch.
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96561) in GitLab 15.4 [with a flag](../../administration/feature_flags.md) named `dora_configuration`. Disabled by default. This feature is in [Beta](../../policy/experiment-beta-support.md).
+This calculation rule has been implemented by updating the `dora_configurations` table with the target branches that are part of the development flow.
+This way, GitLab can recognize the branches as one, and filter out other merge requests.
-FLAG:
-On self-managed GitLab, by default this feature is not available. To make it available per project or for your entire instance, an administrator can [enable the feature flag](../../administration/feature_flags.md) named `dora_configuration`.
-On GitLab.com, this feature is not available.
-This feature is not ready for production use.
+This configuration changes how daily DORA metrics are calculated for the selected project, but doesn't affect other projects, groups, or users.
+
+This feature supports only project-level propagation.
-You can configure the behavior of DORA metrics calculations.
To do this, in the Rails console run the following command:
```ruby
Dora::Configuration.create!(project: my_project, ltfc_target_branches: \['master', 'main'\])
```
-This feature is in [Beta](../../policy/experiment-beta-support.md).
-
## Retrieve DORA metrics data
To retrieve DORA data, use the [GraphQL](../../api/graphql/reference/index.md) or the [REST](../../api/dora/metrics.md) APIs.
@@ -193,7 +194,9 @@ and use it to automatically:
1. [Create an incident when an alert is triggered](../../operations/incident_management/manage_incidents.md#automatically-when-an-alert-is-triggered).
1. [Close incidents via recovery alerts](../../operations/incident_management/manage_incidents.md#automatically-close-incidents-via-recovery-alerts).
-### Supported DORA metrics in GitLab
+## DORA metrics in GitLab
+
+GitLab supports the following DORA metrics:
| Metric | Level | API | UI chart | Comments |
|---------------------------|-------------------|-----------------------------------------------------|------------------------|----------|
@@ -203,3 +206,22 @@ and use it to automatically:
| `lead_time_for_changes` | Group | [GitLab 13.10 and later](../../api/dora/metrics.md) | GitLab 14.0 and later | Unit in seconds. Aggregation method is median. |
| `time_to_restore_service` | Project and group | [GitLab 14.9 and later](../../api/dora/metrics.md) | GitLab 15.1 and later | Unit in days. Aggregation method is median. |
| `change_failure_rate` | Project and group | [GitLab 14.10 and later](../../api/dora/metrics.md) | GitLab 15.2 and later | Percentage of deployments. |
+
+### DORA metrics charts
+
+The DORA metrics are displayed on the following charts:
+
+- [Value Streams Dashboard](value_streams_dashboard.md), which helps you identify trends, patterns, and opportunities for improvement. DORA metrics are displayed in the [metrics comparison panel](value_streams_dashboard.md#devsecops-metrics-comparison-panel) and the [DORA Performers score panel](value_streams_dashboard.md#dora-performers-score-panel).
+- [CI/CD analytics charts](ci_cd_analytics.md), which show pipeline success rates and duration, and the history of DORA metrics over time.
+- Insights reports for [groups](../group/insights/index.md) and [projects](../group/value_stream_analytics/index.md), where you can also use [DORA query parameters](../../user/project/insights/index.md#dora-query-parameters) to create custom charts.
+
+### DORA metrics data aggregation
+
+The table below provides an overview of the DORA metrics' data aggregation in different charts.
+
+| Metric name | Measured values | Data aggregation in the [Value Streams Dashboard](value_streams_dashboard.md) | Data aggregation in [CI/CD analytics charts](ci_cd_analytics.md) | Data aggregation in [Custom insights reporting](../../user/project/insights/index.md#dora-query-parameters) |
+|---------------------------|-------------------|-----------------------------------------------------|------------------------|----------|
+| Deployment frequency | Number of successful deployments | daily average per month | daily average | `day` (default) or `month` |
+| Lead time for changes | Number of seconds to successfully deliver a commit into production | daily median per month | median time | `day` (default) or `month` |
+| Time to restore service | Number of seconds an incident was open for | daily median per month | daily median | `day` (default) or `month` |
+| Change failure rate | percentage of deployments that cause an incident in production | daily median per month | percentage of failed deployments | `day` (default) or `month` |
diff --git a/doc/user/analytics/value_streams_dashboard.md b/doc/user/analytics/value_streams_dashboard.md
index 45be6f5aa25..b5358cc81c8 100644
--- a/doc/user/analytics/value_streams_dashboard.md
+++ b/doc/user/analytics/value_streams_dashboard.md
@@ -214,8 +214,8 @@ Label filters are appended as query parameters to the URL of the drill-down repo
| Change failure rate | Percentage of deployments that cause an incident in production. | [Change failure rate tab](https://gitlab.com/groups/gitlab-org/-/analytics/ci_cd?tab=change-failure-rate) | [Change failure rate](dora_metrics.md#change-failure-rate) | `change_failure_rate` |
| Lead time | Median time from issue created to issue closed. | [Value Stream Analytics](https://gitlab.com/groups/gitlab-org/-/analytics/value_stream_analytics) | [View the lead time and cycle time for issues](../group/value_stream_analytics/index.md#lifecycle-metrics) | `lead_time` |
| Cycle time | Median time from the earliest commit of a linked issue's merge request to when that issue is closed. | [VSA overview](https://gitlab.com/groups/gitlab-org/-/analytics/value_stream_analytics) | [View the lead time and cycle time for issues](../group/value_stream_analytics/index.md#lifecycle-metrics) | `cycle_time` |
-| New issues | Number of new issues created. | [Issue Analytics](https://gitlab.com/groups/gitlab-org/-/issues_analytics) | Issue analytics [for projects](issue_analytics.md) and [for groups](../../user/group/issues_analytics/index.md) | `issues` |
-| Closed issues | Number of issues closed by month. | [Value Stream Analytics](https://gitlab.com/groups/gitlab-org/-/analytics/value_stream_analytics) | [Value Stream Analytics](../group/value_stream_analytics/index.md) | `issues_completed` |
+| Issues created | Number of new issues created. | [Issue Analytics](https://gitlab.com/groups/gitlab-org/-/issues_analytics) | Issue analytics [for projects](issue_analytics.md) and [for groups](../../user/group/issues_analytics/index.md) | `issues` |
+| Issues closed | Number of issues closed by month. | [Value Stream Analytics](https://gitlab.com/groups/gitlab-org/-/analytics/value_stream_analytics) | [Value Stream Analytics](../group/value_stream_analytics/index.md) | `issues_completed` |
| Number of deploys | Total number of deploys to production. | [Merge Request Analytics](https://gitlab.com/gitlab-org/gitlab/-/analytics/merge_request_analytics) | [Merge request analytics](merge_request_analytics.md) | `deploys` |
| Merge request throughput | The number of merge requests merged by month. | [Groups Productivity analytics](productivity_analytics.md), [Projects Merge Request Analytics](https://gitlab.com/gitlab-org/gitlab/-/analytics/merge_request_analytics) | [Groups Productivity analytics](productivity_analytics.md) [Projects Merge request analytics](merge_request_analytics.md) | `merge_request_throughput` |
| Critical vulnerabilities over time | Critical vulnerabilities over time in project or group | [Vulnerability report](https://gitlab.com/gitlab-org/gitlab/-/security/vulnerability_report) | [Vulnerability report](../application_security/vulnerability_report/index.md) | `vulnerability_critical` |
diff --git a/doc/user/application_security/container_scanning/index.md b/doc/user/application_security/container_scanning/index.md
index 6ee8be822da..ac03f08e23b 100644
--- a/doc/user/application_security/container_scanning/index.md
+++ b/doc/user/application_security/container_scanning/index.md
@@ -7,11 +7,6 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Container Scanning **(FREE ALL)**
-> - Improved support for FIPS [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/263482) in GitLab 13.6 by upgrading `CS_MAJOR_VERSION` from `2` to `3`.
-> - Integration with Trivy [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/322656) in GitLab 13.9 by upgrading `CS_MAJOR_VERSION` from `3` to `4`.
-> - Integration with Clair [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/321451) in GitLab 13.9.
-> - Default container scanning with Trivy [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/61850) in GitLab 14.0.
-> - Integration with Grype as an alternative scanner [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/326279) in GitLab 14.0.
> - [Changed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/86092) the major analyzer version from `4` to `5` in GitLab 15.0.
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/86783) from GitLab Ultimate to GitLab Free in 15.0.
> - Container Scanning variables that reference Docker [renamed](https://gitlab.com/gitlab-org/gitlab/-/issues/357264) in GitLab 15.4.
@@ -22,8 +17,9 @@ vulnerabilities. By including an extra Container Scanning job in your pipeline t
vulnerabilities and displays them in a merge request, you can use GitLab to audit your Docker-based
apps.
-<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
+- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
For an overview, see [Container Scanning](https://www.youtube.com/watch?v=C0jn2eN5MAs).
+- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> For a video walkthrough, see [How to set up Container Scanning using GitLab](https://youtu.be/h__mcXpil_4?si=w_BVG68qnkL9x4l1).
Container Scanning is often considered part of Software Composition Analysis (SCA). SCA can contain
aspects of inspecting the items your code uses. These items typically include application and system
@@ -58,23 +54,23 @@ information directly in the merge request.
### Capabilities
-| Capability | In Free | In Ultimate |
+| Capability | In Free and Premium | In Ultimate |
| --- | ------ | ------ |
-| [Configure Scanners](#configuration) | Yes | Yes |
-| Customize Settings ([Variables](#available-cicd-variables), [Overriding](#overriding-the-container-scanning-template), [offline environment support](#running-container-scanning-in-an-offline-environment), etc) | Yes | Yes |
-| [View JSON Report](#reports-json-format) as a CI job artifact | Yes | Yes |
-| Generation of a JSON report of [dependencies](#dependency-list) as a CI job artifact | Yes | Yes |
-| Ability to enable container scanning via an MR in the GitLab UI | Yes | Yes |
-| [UBI Image Support](#fips-enabled-images) | Yes | Yes |
-| Support for Trivy | Yes | Yes |
-| Support for Grype | Yes | Yes |
+| [Configure Scanners](#configuration) | **{check-circle}** Yes | **{check-circle}** Yes |
+| Customize Settings ([Variables](#available-cicd-variables), [Overriding](#overriding-the-container-scanning-template), [offline environment support](#running-container-scanning-in-an-offline-environment), etc) | **{check-circle}** Yes | **{check-circle}** Yes |
+| [View JSON Report](#reports-json-format) as a CI job artifact | **{check-circle}** Yes | **{check-circle}** Yes |
+| Generation of a JSON report of [dependencies](#dependency-list) as a CI job artifact | **{check-circle}** Yes | **{check-circle}** Yes |
+| Ability to enable container scanning via an MR in the GitLab UI | **{check-circle}** Yes | **{check-circle}** Yes |
+| [UBI Image Support](#fips-enabled-images) | **{check-circle}** Yes | **{check-circle}** Yes |
+| Support for Trivy | **{check-circle}** Yes | **{check-circle}** Yes |
+| Support for Grype | **{check-circle}** Yes | **{check-circle}** Yes |
| Inclusion of GitLab Advisory Database | Limited to the time-delayed content from GitLab [advisories-communities](https://gitlab.com/gitlab-org/advisories-community/) project | Yes - all the latest content from [Gemnasium DB](https://gitlab.com/gitlab-org/security-products/gemnasium-db) |
-| Presentation of Report data in Merge Request and Security tab of the CI pipeline job | No | Yes |
-| [Interaction with Vulnerabilities](#interacting-with-the-vulnerabilities) such as merge request approvals | No | Yes |
-| [Solutions for vulnerabilities (auto-remediation)](#solutions-for-vulnerabilities-auto-remediation) | No | Yes |
-| Support for the [vulnerability allow list](#vulnerability-allowlisting) | No | Yes |
-| [Access to Security Dashboard page](#security-dashboard) | No | Yes |
-| [Access to Dependency List page](../dependency_list/index.md) | No | Yes |
+| Presentation of Report data in Merge Request and Security tab of the CI pipeline job | **{dotted-circle}** No | **{check-circle}** Yes |
+| [Interaction with Vulnerabilities](#interacting-with-the-vulnerabilities) such as merge request approvals | **{dotted-circle}** No | **{check-circle}** Yes |
+| [Solutions for vulnerabilities (auto-remediation)](#solutions-for-vulnerabilities-auto-remediation) | **{dotted-circle}** No | **{check-circle}** Yes |
+| Support for the [vulnerability allow list](#vulnerability-allowlisting) | **{dotted-circle}** No | **{check-circle}** Yes |
+| [Access to Security Dashboard page](#security-dashboard) | **{dotted-circle}** No | **{check-circle}** Yes |
+| [Access to Dependency List page](../dependency_list/index.md) | **{dotted-circle}** No | **{check-circle}** Yes |
## Prerequisites
@@ -133,6 +129,10 @@ Setting `CS_DEFAULT_BRANCH_IMAGE` avoids duplicate vulnerability findings when a
The value of `CS_DEFAULT_BRANCH_IMAGE` indicates the name of the scanned image as it appears on the default branch.
For more details on how this deduplication is achieved, see [Setting the default branch image](#setting-the-default-branch-image).
+## Running jobs in merge request pipelines
+
+See [Use security scanning tools with merge request pipelines](../index.md#use-security-scanning-tools-with-merge-request-pipelines)
+
### Customizing the container scanning settings
There may be cases where you want to customize how GitLab scans your containers. For example, you
@@ -272,28 +272,30 @@ including a large number of false positives.
| `CS_REGISTRY_USER` | `$CI_REGISTRY_USER` | Username for accessing a Docker registry requiring authentication. The default is only set if `$CS_IMAGE` resides at [`$CI_REGISTRY`](../../../ci/variables/predefined_variables.md). Not supported when [FIPS mode](../../../development/fips_compliance.md#enable-fips-mode) is enabled. | All |
| `CS_DOCKERFILE_PATH` | `Dockerfile` | The path to the `Dockerfile` to use for generating remediations. By default, the scanner looks for a file named `Dockerfile` in the root directory of the project. You should configure this variable only if your `Dockerfile` is in a non-standard location, such as a subdirectory. See [Solutions for vulnerabilities](#solutions-for-vulnerabilities-auto-remediation) for more details. | All |
| `CS_QUIET` | `""` | If set, this variable disables output of the [vulnerabilities table](#container-scanning-job-log-format) in the job log. [Introduced](https://gitlab.com/gitlab-org/security-products/analyzers/container-scanning/-/merge_requests/50) in GitLab 15.1. | All |
-| `SECURE_LOG_LEVEL` | `info` | Set the minimum logging level. Messages of this logging level or higher are output. From highest to lowest severity, the logging levels are: `fatal`, `error`, `warn`, `info`, `debug`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/10880) in GitLab 13.1. | All |
+| `CS_TRIVY_JAVA_DB` | `"ghcr.io/aquasecurity/trivy-java-db"` | Specify an alternate location for the [trivy-java-db](https://github.com/aquasecurity/trivy-java-db) vulnerability database. | Trivy |
+| `CS_IGNORE_STATUSES` | `""` | Force the analyzer to ignore vulnerability findings with specified statuses in a comma-delimited list. For `trivy`, the following values are allowed: `unknown,not_affected,affected,fixed,under_investigation,will_not_fix,fix_deferred,end_of_life`. For `grype`, the following values are allowed: `fixed,not-fixed,unknown,wont-fix` | All |
+| `SECURE_LOG_LEVEL` | `info` | Set the minimum logging level. Messages of this logging level or higher are output. From highest to lowest severity, the logging levels are: `fatal`, `error`, `warn`, `info`, `debug`. | All |
### Supported distributions
Support depends on which scanner is used:
-| Distribution | Grype | Trivy |
-| -------------- | ----- | ----- |
-| Alma Linux | | ✅ |
-| Alpine Linux | ✅ | ✅ |
-| Amazon Linux | ✅ | ✅ |
-| BusyBox | ✅ | |
-| CentOS | ✅ | ✅ |
-| CBL-Mariner | | ✅ |
-| Debian | ✅ | ✅ |
-| Distroless | ✅ | ✅ |
-| Oracle Linux | ✅ | ✅ |
-| Photon OS | | ✅ |
-| Red Hat (RHEL) | ✅ | ✅ |
-| Rocky Linux | | ✅ |
-| SUSE | | ✅ |
-| Ubuntu | ✅ | ✅ |
+| Distribution | Grype | Trivy |
+|----------------|------------------------|------------------------|
+| Alma Linux | **{dotted-circle}** No | **{check-circle}** Yes |
+| Alpine Linux | **{check-circle}** Yes | **{check-circle}** Yes |
+| Amazon Linux | **{check-circle}** Yes | **{check-circle}** Yes |
+| BusyBox | **{check-circle}** Yes | **{dotted-circle}** No |
+| CentOS | **{check-circle}** Yes | **{check-circle}** Yes |
+| CBL-Mariner | **{dotted-circle}** No | **{check-circle}** Yes |
+| Debian | **{check-circle}** Yes | **{check-circle}** Yes |
+| Distroless | **{check-circle}** Yes | **{check-circle}** Yes |
+| Oracle Linux | **{check-circle}** Yes | **{check-circle}** Yes |
+| Photon OS | **{dotted-circle}** No | **{check-circle}** Yes |
+| Red Hat (RHEL) | **{check-circle}** Yes | **{check-circle}** Yes |
+| Rocky Linux | **{dotted-circle}** No | **{check-circle}** Yes |
+| SUSE | **{dotted-circle}** No | **{check-circle}** Yes |
+| Ubuntu | **{check-circle}** Yes | **{check-circle}** Yes |
#### FIPS-enabled images
@@ -654,6 +656,32 @@ Also:
Scanning images in external private registries is not supported when [FIPS mode](../../../development/fips_compliance.md#enable-fips-mode) is enabled.
+#### Create and use a Trivy Java database mirror
+
+When the `trivy` scanner is used and a `jar` file is encountered in a container image being scanned, `trivy` downloads an additional `trivy-java-db` vulnerability database. By default, the `trivy-java-db` database is hosted as an [OCI artifact](https://oras.land/docs/quickstart) at `ghcr.io/aquasecurity/trivy-java-db:1`. If this registry is not accessible, for example in a network-isolated offline GitLab instance, one solution is to mirror the `trivy-java-db` to a container registry that can be accessed in the offline instance:
+
+```yaml
+mirror trivy java db:
+ image:
+ name: ghcr.io/oras-project/oras:v1.1.0
+ entrypoint: [""]
+ script:
+ - oras login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
+ - oras pull ghcr.io/aquasecurity/trivy-java-db:1
+ - oras push $CI_REGISTRY_IMAGE:1 --config /dev/null:application/vnd.aquasec.trivy.config.v1+json javadb.tar.gz:application/vnd.aquasec.trivy.javadb.layer.v1.tar+gzip
+```
+
+If the above container registry is `gitlab.example.com/trivy-java-db-mirror`, then the container scanning job should be configured in the following way:
+
+```yaml
+include:
+ - template: Security/Container-Scanning.gitlab-ci.yml
+
+container_scanning:
+ variables:
+ CS_TRIVY_JAVA_DB: gitlab.example.com/trivy-java-db-mirror:1
+```
+
## Running the standalone container scanning tool
It's possible to run the [GitLab container scanning tool](https://gitlab.com/gitlab-org/security-products/analyzers/container-scanning)
@@ -715,24 +743,24 @@ All analyzer images are [updated daily](https://gitlab.com/gitlab-org/security-p
The images use data from upstream advisory databases depending on which scanner is used:
-| Data Source | Trivy | Grype |
-| ------------------------------ | ----- | ----- |
-| AlmaLinux Security Advisory | ✅ | ✅ |
-| Amazon Linux Security Center | ✅ | ✅ |
-| Arch Linux Security Tracker | ✅ | |
-| SUSE CVRF | ✅ | ✅ |
-| CWE Advisories | ✅ | |
-| Debian Security Bug Tracker | ✅ | ✅ |
-| GitHub Security Advisory | ✅ | ✅ |
-| Go Vulnerability Database | ✅ | |
-| CBL-Mariner Vulnerability Data | ✅ | |
-| NVD | ✅ | ✅ |
-| OSV | ✅ | |
-| Red Hat OVAL v2 | ✅ | ✅ |
-| Red Hat Security Data API | ✅ | ✅ |
-| Photon Security Advisories | ✅ | |
-| Rocky Linux UpdateInfo | ✅ | |
-| Ubuntu CVE Tracker (only data sources from mid 2021 and later) | ✅ | ✅ |
+| Data Source | Trivy | Grype |
+|----------------------------------------------------------------|------------------------|------------------------|
+| AlmaLinux Security Advisory | **{check-circle}** Yes | **{check-circle}** Yes |
+| Amazon Linux Security Center | **{check-circle}** Yes | **{check-circle}** Yes |
+| Arch Linux Security Tracker | **{check-circle}** Yes | **{dotted-circle}** No |
+| SUSE CVRF | **{check-circle}** Yes | **{check-circle}** Yes |
+| CWE Advisories | **{check-circle}** Yes | **{dotted-circle}** No |
+| Debian Security Bug Tracker | **{check-circle}** Yes | **{check-circle}** Yes |
+| GitHub Security Advisory | **{check-circle}** Yes | **{check-circle}** Yes |
+| Go Vulnerability Database | **{check-circle}** Yes | **{dotted-circle}** No |
+| CBL-Mariner Vulnerability Data | **{check-circle}** Yes | **{dotted-circle}** No |
+| NVD | **{check-circle}** Yes | **{check-circle}** Yes |
+| OSV | **{check-circle}** Yes | **{dotted-circle}** No |
+| Red Hat OVAL v2 | **{check-circle}** Yes | **{check-circle}** Yes |
+| Red Hat Security Data API | **{check-circle}** Yes | **{check-circle}** Yes |
+| Photon Security Advisories | **{check-circle}** Yes | **{dotted-circle}** No |
+| Rocky Linux UpdateInfo | **{check-circle}** Yes | **{dotted-circle}** No |
+| Ubuntu CVE Tracker (only data sources from mid 2021 and later) | **{check-circle}** Yes | **{check-circle}** Yes |
In addition to the sources provided by these scanners, GitLab maintains the following vulnerability databases:
diff --git a/doc/user/application_security/continuous_vulnerability_scanning/index.md b/doc/user/application_security/continuous_vulnerability_scanning/index.md
index 4094a0add28..e31fc5f7eb0 100644
--- a/doc/user/application_security/continuous_vulnerability_scanning/index.md
+++ b/doc/user/application_security/continuous_vulnerability_scanning/index.md
@@ -29,10 +29,9 @@ To enable Continuous Vulnerability Scanning:
- Enable the Continuous Vulnerability Scanning setting in the project's [security configuration](../configuration/index.md).
- Enable [Dependency Scanning](../dependency_scanning/index.md#configuration) and ensure that its prerequisites are met.
+- On GitLab self-managed only, you can [choose package registry metadata to synchronize](../../../administration/settings/security_and_compliance.md#choose-package-registry-metadata-to-sync) in the Admin Area for the GitLab instance. For this data synchronization to work, you must allow outbound network traffic from your GitLab instance to the domain `storage.googleapis.com`. If you have limited or no network connectivity then please refer to the documentation section [running in an offline environment](#running-in-an-offline-environment) for further guidance.
-On GitLab self-managed only, you can [choose package registry metadata to sync](../../../administration/settings/security_and_compliance.md#choose-package-registry-metadata-to-sync) in the Admin Area for the GitLab instance.
-
-### Requirements for offline environments
+### Running in an offline environment
For self-managed GitLab instances in an environment with limited, restricted, or intermittent access to external resources through the internet,
some adjustments are required to successfully scan CycloneDX reports for vulnerabilities.
diff --git a/doc/user/application_security/dast/browser_based.md b/doc/user/application_security/dast/browser_based.md
index 26782c319b1..207db52ed71 100644
--- a/doc/user/application_security/dast/browser_based.md
+++ b/doc/user/application_security/dast/browser_based.md
@@ -66,6 +66,8 @@ See [checks](checks/index.md) for more information about individual checks.
Active scans check for vulnerabilities by injecting attack payloads into HTTP requests recorded during the crawl phase of the scan.
Active scans are disabled by default due to the nature of their probing attacks.
+#### How active scans work
+
DAST analyzes each recorded HTTP request for injection locations, such as query values, header values, cookie values, form posts, and JSON string values.
Attack payloads are injected into the injection location, forming a new request.
DAST sends the request to the target application and uses the HTTP response to determine attack success.
@@ -84,6 +86,12 @@ A simplified timing attack works as follows:
1. The target application is vulnerable if it executes the query parameter value as a system command without validation, for example, `system(params[:search])`
1. DAST creates a finding if the response time takes longer than 10 seconds.
+#### Known issues
+
+Active scans do not use a browser to send HTTP requests in an effort to minimize scan time.
+
+Anti-CSRF tokens are not regenerated for attacks that submit forms. Please disable anti-CSRF tokens when running an active scan.
+
## Getting started
To run a DAST scan:
@@ -167,7 +175,7 @@ For authentication CI/CD variables, see [Authentication](authentication.md).
| CI/CD variable | Type | Example | Description |
|:--------------------------------------------|:---------------------------------------------------------|----------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| `DAST_ADVERTISE_SCAN` | boolean | `true` | Set to `true` to add a `Via` header to every request sent, advertising that the request was sent as part of a GitLab DAST scan. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/334947) in GitLab 14.1. |
+| `DAST_ADVERTISE_SCAN` | boolean | `true` | Set to `true` to add a `Via` header to every request sent, advertising that the request was sent as part of a GitLab DAST scan. The header value starts with `GitLab DAST`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/334947) in GitLab 14.1. |
| `DAST_BROWSER_ACTION_STABILITY_TIMEOUT` | [Duration string](https://pkg.go.dev/time#ParseDuration) | `800ms` | The maximum amount of time to wait for a browser to consider a page loaded and ready for analysis after completing an action. |
| `DAST_BROWSER_ACTION_TIMEOUT` | [Duration string](https://pkg.go.dev/time#ParseDuration) | `7s` | The maximum amount of time to wait for a browser to complete an action. |
| `DAST_BROWSER_ALLOWED_HOSTS` | List of strings | `site.com,another.com` | Hostnames included in this variable are considered in scope when crawled. By default the `DAST_WEBSITE` hostname is included in the allowed hosts list. Headers set using `DAST_REQUEST_HEADERS` are added to every request made to these hostnames. |
@@ -209,7 +217,7 @@ For authentication CI/CD variables, see [Authentication](authentication.md).
| `DAST_REQUEST_HEADERS` | string | `Cache-control:no-cache` | Set to a comma-separated list of request header names and values. |
| `DAST_SKIP_TARGET_CHECK` | boolean | `true` | Set to `true` to prevent DAST from checking that the target is available before scanning. Default: `false`. |
| `DAST_TARGET_AVAILABILITY_TIMEOUT` | number | `60` | Time limit in seconds to wait for target availability. |
-| `DAST_WEBSITE` | URL | `https://example.com` | The URL of the website to scan. |
+| `DAST_WEBSITE` | URL | `https://example.com` | The URL of the target application to scan. |
| `SECURE_ANALYZERS_PREFIX` | URL | `registry.organization.com` | Set the Docker registry base address from which to download the analyzer. |
## Managing scope
@@ -281,22 +289,17 @@ dast:
DAST_EXCLUDE_URLS: "https://my.site.com/user/logout" # don't visit this URL
```
-## Vulnerability detection
+## Vulnerability check migration
+
+A migration is underway that changes the browser-based analyzer from using the proxy-based analyzer Zed Attack Proxy (ZAP) active vulnerability checks, to using GitLab-built active vulnerability checks.
+
+The browser-based analyzer continues to use a combination of proxy-based analyzer and GitLab-built vulnerability checks until the migration is complete. See [browser-based vulnerability checks](checks/index.md) for details of which checks have been migrated.
-Vulnerability detection is gradually being migrated from the default Zed Attack Proxy (ZAP) solution
-to the browser-based analyzer. For details of the vulnerability detection already migrated, see
-[browser-based vulnerability checks](checks/index.md).
+### Why browser-based scans produce different results to proxy-based scans
-The crawler runs the target website in a browser with DAST/ZAP configured as the proxy server. This
-ensures that all requests and responses made by the browser are passively scanned by DAST/ZAP. When
-running a full scan, active vulnerability checks executed by DAST/ZAP do not use a browser. This
-difference in how vulnerabilities are checked can cause issues that require certain features of the
-target website to be disabled to ensure the scan works as intended.
+Browser-based and proxy-based scans do not produce the same results because they use a different set of vulnerability checks.
-For example, for a target website that contains forms with Anti-CSRF tokens, a passive scan works as
-intended because the browser displays pages and forms as if a user is viewing the page. However,
-active vulnerability checks that run in a full scan cannot submit forms containing Anti-CSRF tokens.
-In such cases, we recommend you disable Anti-CSRF tokens when running a full scan.
+The browser-based analyzer does not have an equivalent for proxy-based checks that create too many false positives, are not worth running because modern browsers don't allow the vulnerability to be exploited, or are no longer considered relevant. The browser-based analyzer includes checks that proxy-based analyzer does not.
## Managing scan time
diff --git a/doc/user/application_security/dast/checks/89.1.md b/doc/user/application_security/dast/checks/89.1.md
new file mode 100644
index 00000000000..ca7ff5e4593
--- /dev/null
+++ b/doc/user/application_security/dast/checks/89.1.md
@@ -0,0 +1,37 @@
+---
+stage: Secure
+group: Dynamic Analysis
+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
+---
+
+# SQL Injection
+
+## Description
+
+It is possible to execute arbitrary SQL commands on the target application server's
+backend database.
+SQL Injection is a critical vulnerability that can lead to a data or system
+compromise.
+
+## Remediation
+
+Always use parameterized queries when issuing requests to backend database systems. In
+situations where dynamic queries must be created, never use direct user input, but
+instead use a map or dictionary of valid values and resolve them using a user-supplied key.
+
+For example, some database drivers do not allow parameterized queries for `>` or `<` comparison
+operators. In these cases, do not use a user supplied `>` or `<` value, but rather have the user
+supply a `gt` or `lt` value. The alphabetical values are then used to look up the `>` and `<`
+values to be used in the construction of the dynamic query. The same goes for other queries where
+column or table names are required but can not be parameterized.
+
+## Details
+
+| ID | Aggregated | CWE | Type | Risk |
+|:---|:--------|:--------|:--------|:--------|
+| 89.1 | false | 89 | Active | high |
+
+## Links
+
+- [OWASP](https://owasp.org/www-community/attacks/SQL_Injection)
+- [CWE](https://cwe.mitre.org/data/definitions/89.html)
diff --git a/doc/user/application_security/dast/checks/917.1.md b/doc/user/application_security/dast/checks/917.1.md
new file mode 100644
index 00000000000..68b9665e393
--- /dev/null
+++ b/doc/user/application_security/dast/checks/917.1.md
@@ -0,0 +1,33 @@
+---
+stage: Secure
+group: Dynamic Analysis
+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
+---
+
+# Expression Language Injection
+
+## Description
+
+It is possible to execute arbitrary Expression Language (EL) statements on the target
+application server. EL injection is a critical severity vulnerability that can lead to
+full system compromise. EL injection can occur when attacker-controlled data is used to construct
+EL statements without neutralizing special characters. These special characters could modify the
+intended EL statement prior to it being executed by an interpreter.
+
+## Remediation
+
+User-controlled data should always have special elements neutralized when used as part of
+constructing Expression Language statements. Please consult the documentation for the EL
+interpreter in use on how properly neutralize user controlled data.
+
+## Details
+
+| ID | Aggregated | CWE | Type | Risk |
+|:---|:--------|:--------|:--------|:--------|
+| 917.1 | false | 917 | Active | high |
+
+## Links
+
+- [CWE](https://cwe.mitre.org/data/definitions/917.html)
+- [OWASP](https://owasp.org/www-community/vulnerabilities/Expression_Language_Injection)
+- [Expression Language Injection [PDF]](https://mindedsecurity.com/wp-content/uploads/2020/10/ExpressionLanguageInjection.pdf)
diff --git a/doc/user/application_security/dast/checks/94.1.md b/doc/user/application_security/dast/checks/94.1.md
new file mode 100644
index 00000000000..ec30b41c5e8
--- /dev/null
+++ b/doc/user/application_security/dast/checks/94.1.md
@@ -0,0 +1,53 @@
+---
+stage: Secure
+group: Dynamic Analysis
+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
+---
+
+# Server-side code injection (PHP)
+
+## Description
+
+The target application was found vulnerable to code injection. A malicious actor could inject arbitrary
+PHP code to be executed on the server. This could lead to a full system compromise by accessing
+stored secrets, injecting code to take over accounts, or executing OS commands.
+
+## Remediation
+
+Never pass user input directly into functions which evaluate string data as code, such as `eval`.
+There is almost no benefit of passing string values to `eval`, as such the best recommendation is
+to replace the current logic with more safe implementations of dynamically evaluating logic with
+user input. One alternative is to use an `array()`, storing expected user inputs in an array
+key, and use that key as a look up to execute functions:
+
+```php
+$func_to_run = function()
+{
+ print('hello world');
+};
+
+$function_map = array();
+$function_map["fn"] = $func_to_run; // store additional input to function mappings here
+
+$input = "fn";
+
+// lookup "fn" as the key
+if (array_key_exists($input, $function_map)) {
+ // run the $func_to_run that was stored in the "fn" array hash value.
+ $func = $function_map[$input];
+ $func();
+} else {
+ print('invalid input');
+}
+```
+
+## Details
+
+| ID | Aggregated | CWE | Type | Risk |
+|:---|:--------|:--------|:--------|:--------|
+| 94.1 | false | 94 | Active | high |
+
+## Links
+
+- [CWE](https://cwe.mitre.org/data/definitions/94.html)
+- [OWASP](https://owasp.org/www-community/attacks/Code_Injection)
diff --git a/doc/user/application_security/dast/checks/94.2.md b/doc/user/application_security/dast/checks/94.2.md
new file mode 100644
index 00000000000..666052807b5
--- /dev/null
+++ b/doc/user/application_security/dast/checks/94.2.md
@@ -0,0 +1,51 @@
+---
+stage: Secure
+group: Dynamic Analysis
+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
+---
+
+# Server-side code injection (Ruby)
+
+## Description
+
+The target application was found vulnerable to code injection. A malicious actor could inject arbitrary
+Ruby code to be executed on the server. This could lead to a full system compromise by accessing
+stored secrets, injecting code to take over accounts, or executing OS commands.
+
+## Remediation
+
+Never pass user input directly into functions which evaluate string data as code, such as `eval`,
+`send`, `public_send`, `instance_eval` or `class_eval`. There is almost no benefit of passing string
+values to these methods, as such the best recommendation is to replace the current logic with more safe
+implementations of dynamically evaluating logic with user input. If using `send` or `public_send` ensure
+the first argument is to a known, hardcoded method/symbol and does not come from user input.
+
+For `eval`, `instance_eval` and `class_eval`, user input should never be sent directly to these methods.
+One alternative is to store functions or methods in a Hash that can be looked up using a key. If the key
+exists, the function can be executed.
+
+```ruby
+def func_to_run
+ puts 'hello world'
+end
+
+input = 'fn'
+
+function_map = { fn: method(:func_to_run) }
+
+if function_map.key?(input.to_sym)
+ function_map[input.to_sym].call
+else
+ puts 'invalid input'
+end
+```
+
+## Details
+
+| ID | Aggregated | CWE | Type | Risk |
+|:---|:--------|:--------|:--------|:--------|
+| 94.2 | false | 94 | Active | high |
+
+## Links
+
+- [CWE](https://cwe.mitre.org/data/definitions/94.html)
diff --git a/doc/user/application_security/dast/checks/94.3.md b/doc/user/application_security/dast/checks/94.3.md
new file mode 100644
index 00000000000..772cdb1d3ea
--- /dev/null
+++ b/doc/user/application_security/dast/checks/94.3.md
@@ -0,0 +1,45 @@
+---
+stage: Secure
+group: Dynamic Analysis
+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
+---
+
+# Server-side code injection (Python)
+
+## Description
+
+The target application was found vulnerable to code injection. A malicious actor could inject arbitrary
+Python code to be executed on the server. This could lead to a full system compromise by accessing
+stored secrets, injecting code to take over accounts, or executing OS commands.
+
+## Remediation
+
+Never pass user input directly into functions which evaluate string data as code, such as `eval`,
+or `exec`. There is almost no benefit of passing string values to these methods, as such the best
+recommendation is to replace the current logic with more safe implementations of dynamically evaluating
+logic with user input. One alternative is to store functions or methods in a hashmap that can be looked
+up using a key. If the key exists, the function can be executed.
+
+```python
+def func_to_run():
+ print('hello world')
+
+function_map = {'fn': func_to_run}
+
+input = 'fn'
+
+if input in function_map:
+ function_map[input]()
+else:
+ print('invalid input')
+```
+
+## Details
+
+| ID | Aggregated | CWE | Type | Risk |
+|:---|:--------|:--------|:--------|:--------|
+| 94.3 | false | 94 | Active | high |
+
+## Links
+
+- [CWE](https://cwe.mitre.org/data/definitions/94.html)
diff --git a/doc/user/application_security/dast/checks/943.1.md b/doc/user/application_security/dast/checks/943.1.md
new file mode 100644
index 00000000000..debae65669a
--- /dev/null
+++ b/doc/user/application_security/dast/checks/943.1.md
@@ -0,0 +1,30 @@
+---
+stage: Secure
+group: Dynamic Analysis
+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
+---
+
+# Improper neutralization of special elements in data query logic
+
+## Description
+
+The application generates a query intended to interact with MongoDB,
+but it does not neutralize or incorrectly neutralizes special elements
+that can modify the intended logic of the query.
+
+## Remediation
+
+Refactor find or search queries to use standard
+filtering operators such as `$gt` or `$in` instead of broad operators such
+as `$where`. If possible, disable the MongoDB JavaScript interface entirely.
+
+## Details
+
+| ID | Aggregated | CWE | Type | Risk |
+|:---|:--------|:--------|:--------|:--------|
+| 943.1 | false | 943 | Active | high |
+
+## Links
+
+- [CWE](https://cwe.mitre.org/data/definitions/943.html)
+- [Disabling MongoDB Server Side JS](https://www.mongodb.com/docs/manual/core/server-side-javascript/#std-label-disable-server-side-js)
diff --git a/doc/user/application_security/dast/checks/index.md b/doc/user/application_security/dast/checks/index.md
index 4d41f08672e..c239fdb5e74 100644
--- a/doc/user/application_security/dast/checks/index.md
+++ b/doc/user/application_security/dast/checks/index.md
@@ -170,4 +170,10 @@ The [DAST browser-based crawler](../browser_based.md) provides a number of vulne
| [113.1](113.1.md) | Improper Neutralization of CRLF Sequences in HTTP Headers | High | Active |
| [22.1](22.1.md) | Improper limitation of a pathname to a restricted directory (Path traversal) | High | Active |
| [611.1](611.1.md) | External XML Entity Injection (XXE) | High | Active |
+| [89.1](89.1.md) | SQL Injection | High | Active |
+| [917.1](917.1.md) | Expression Language Injection | High | Active |
+| [94.1](94.1.md) | Server-side code injection (PHP) | High | Active |
+| [94.2](94.2.md) | Server-side code injection (Ruby) | High | Active |
+| [94.3](94.3.md) | Server-side code injection (Python) | High | Active |
| [94.4](94.4.md) | Server-side code injection (NodeJS) | High | Active |
+| [943.1](943.1.md) | Improper neutralization of special elements in data query logic | High | Active |
diff --git a/doc/user/application_security/dast/proxy-based.md b/doc/user/application_security/dast/proxy-based.md
index 230d8ef5ca3..9e59ecc64d9 100644
--- a/doc/user/application_security/dast/proxy-based.md
+++ b/doc/user/application_security/dast/proxy-based.md
@@ -11,11 +11,14 @@ The DAST proxy-based analyzer can be added to your [GitLab CI/CD](../../../ci/in
This helps you discover vulnerabilities in web applications that do not use JavaScript heavily. For applications that do,
see the [DAST browser-based analyzer](browser_based.md).
+<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
+For a video walkthrough, see [How to set up Dynamic Application Security Testing (DAST) with GitLab](https://youtu.be/EiFE1QrUQfk?si=6rpgwgUpalw3ByiV).
+
WARNING:
Do not run DAST scans against a production server. Not only can it perform *any* function that
a user can, such as clicking buttons or submitting forms, but it may also trigger bugs, leading to modification or loss of production data. Only run DAST scans against a test server.
-The analyzer uses the [OWASP Zed Attack Proxy](https://www.zaproxy.org/) (ZAP) to scan in two different ways:
+The analyzer uses the [Software Security Project Zed Attack Proxy](https://www.zaproxy.org/) (ZAP) to scan in two different ways:
- Passive scan only (default). DAST executes
[ZAP's Baseline Scan](https://www.zaproxy.org/docs/docker/baseline-scan/) and doesn't
@@ -382,7 +385,7 @@ including a large number of false positives.
| `DAST_REQUEST_HEADERS` <sup>1</sup> | string | Set to a comma-separated list of request header names and values. Headers are added to every request made by DAST. For example, `Cache-control: no-cache,User-Agent: DAST/1.0` |
| `DAST_SKIP_TARGET_CHECK` | boolean | Set to `true` to prevent DAST from checking that the target is available before scanning. Default: `false`. |
| `DAST_SPIDER_MINS` <sup>1</sup> | number | The maximum duration of the spider scan in minutes. Set to `0` for unlimited. Default: One minute, or unlimited when the scan is a full scan. |
-| `DAST_SPIDER_START_AT_HOST` | boolean | Set to `false` to prevent DAST from resetting the target to its host before scanning. When `true`, non-host targets `http://test.site/some_path` is reset to `http://test.site` before scan. Default: `true`. |
+| `DAST_SPIDER_START_AT_HOST` | boolean | Set to `false` to prevent DAST from resetting the target to its host before scanning. When `true`, non-host targets `http://test.site/some_path` is reset to `http://test.site` before scan. Default: `false`. |
| `DAST_TARGET_AVAILABILITY_TIMEOUT` <sup>1</sup> | number | Time limit in seconds to wait for target availability. |
| `DAST_USE_AJAX_SPIDER` <sup>1</sup> | boolean | Set to `true` to use the AJAX spider in addition to the traditional spider, useful for crawling sites that require JavaScript. Default: `false`. |
| `DAST_XML_REPORT` | string | **{warning}** **[Deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/384340)** in GitLab 15.7. The filename of the XML report written at the end of a scan. |
diff --git a/doc/user/application_security/dependency_scanning/index.md b/doc/user/application_security/dependency_scanning/index.md
index c04134de2b2..683ba6ad19b 100644
--- a/doc/user/application_security/dependency_scanning/index.md
+++ b/doc/user/application_security/dependency_scanning/index.md
@@ -6,11 +6,6 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Dependency Scanning **(ULTIMATE ALL)**
-<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
-For an interactive reading and how-to demo of this Dependency Scanning doc, see [How to use dependency scanning tutorial hands-on GitLab Application Security part 3](https://youtu.be/ii05cMbJ4xQ?feature=shared)
-<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
-For an interactive reading and how-to demo playlist, see [Get Started With GitLab Application Security Playlist](https://www.youtube.com/playlist?list=PL05JrBw4t0KrUrjDoefSkgZLx5aJYFaF9)
-
Dependency Scanning analyzes your application's dependencies for known vulnerabilities. All
dependencies are scanned, including transitive dependencies, also known as nested dependencies.
@@ -33,12 +28,16 @@ we encourage you to use all of our security scanners. For a comparison of these
![Dependency scanning Widget](img/dependency_scanning_v13_2.png)
-<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
-For an overview, see [Dependency Scanning](https://www.youtube.com/watch?v=TBnfbGk4c4o).
-
WARNING:
Dependency Scanning does not support runtime installation of compilers and interpreters.
+- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
+For an overview, see [Dependency Scanning](https://www.youtube.com/watch?v=TBnfbGk4c4o)
+- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
+For an interactive reading and how-to demo of this Dependency Scanning documentation, see [How to use dependency scanning tutorial hands-on GitLab Application Security part 3](https://youtu.be/ii05cMbJ4xQ?feature=shared)
+- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
+For other interactive reading and how-to demos, see [Get Started With GitLab Application Security Playlist](https://www.youtube.com/playlist?list=PL05JrBw4t0KrUrjDoefSkgZLx5aJYFaF9)
+
## Supported languages and package managers
The following languages and dependency managers are supported:
@@ -230,7 +229,8 @@ table.supported-languages ul {
<li>
<a id="notes-regarding-supported-languages-and-package-managers-2"></a>
<p>
- Java 21 LTS is only available when using <a href="https://maven.apache.org/">Maven</a> or <a href="https://gradle.org/">Gradle</a>. Java 21 LTS for <a href="https://www.scala-sbt.org/">sbt</a> is not yet available and tracked in <a href="https://gitlab.com/gitlab-org/gitlab/-/issues/421174">issue 421174</a>. It is not supported when <a href="https://docs.gitlab.com/ee/development/fips_compliance.html#enable-fips-mode">FIPS mode</a> is enabled.
+ Java 21 LTS for <a href="https://www.scala-sbt.org/">sbt</a> is limited to version 1.9.7. Support for more <a href="https://www.scala-sbt.org/">sbt</a> versions can be tracked in <a href="https://gitlab.com/gitlab-org/gitlab/-/issues/430335">issue 430335</a>.
+ It is not supported when <a href="https://docs.gitlab.com/ee/development/fips_compliance.html#enable-fips-mode">FIPS mode</a> is enabled.
</p>
</li>
<li>
@@ -599,6 +599,10 @@ To enable dependency scanning:
Pipelines now include a dependency scanning job.
+### Running jobs in merge request pipelines
+
+See [Use security scanning tools with merge request pipelines](../index.md#use-security-scanning-tools-with-merge-request-pipelines)
+
### Customizing analyzer behavior
You can use CI/CD variables to customize dependency scanning behavior.
@@ -1093,6 +1097,17 @@ variables:
GRADLE_CLI_OPTS: "-Dhttps.proxyHost=squid-proxy -Dhttps.proxyPort=3128 -Dhttp.proxyHost=squid-proxy -Dhttp.proxyPort=3128 -Dhttp.nonProxyHosts=localhost"
```
+## Using a proxy with Maven projects
+
+Maven does not read the `HTTP(S)_PROXY` environment variables.
+
+To make the Maven dependency scanner use a proxy, you can specify the options using the `MAVEN_CLI_OPTS` CI/CD variable:
+
+```yaml
+variables:
+ MAVEN_CLI_OPTS: "-DproxySet=true -Dhttps.proxyHost=squid-proxy -Dhttps.proxyPort=3128 -Dhttp.proxyHost=squid-proxy -Dhttp.proxyPort=3218"
+```
+
## Specific settings for languages and package managers
See the following sections for configuring specific languages and package managers.
diff --git a/doc/user/application_security/get-started-security.md b/doc/user/application_security/get-started-security.md
index 3e73fbc5955..6143dd59373 100644
--- a/doc/user/application_security/get-started-security.md
+++ b/doc/user/application_security/get-started-security.md
@@ -11,32 +11,42 @@ For an overview, see [Adopting GitLab application security](https://www.youtube.
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
For an interactive reading and how-to demo playlist, see [Get Started With GitLab Application Security Playlist](https://www.youtube.com/playlist?list=PL05JrBw4t0KrUrjDoefSkgZLx5aJYFaF9)
-The following steps help you get the most from GitLab application security tools. These steps are a recommended order of operations. You can choose to implement capabilities in a different order or omit features that do not apply to your specific needs.
-
-1. Enable [Secret Detection](secret_detection/index.md) and [Dependency Scanning](dependency_scanning/index.md)
- to identify any leaked secrets and vulnerable packages in your codebase.
-
- - For all security scanners, enable them by updating your [`.gitlab-ci.yml`](../../ci/yaml/gitlab_ci_yaml.md) directly on your `default` branch. This creates a baseline scan of your `default` branch, which is necessary for
- feature branch scans to be compared against. This allows [merge requests](../project/merge_requests/index.md)
- to display only newly-introduced vulnerabilities. Otherwise, merge requests display every
- vulnerability in the branch, regardless of whether it was introduced by a change in the branch.
- - If you are after simplicity, enable only Secret Detection first. It only has one analyzer,
- no build requirements, and relatively simple findings: is this a secret or not?
- - It is good practice to enable Dependency Scanning early so you can start identifying existing
- vulnerable packages in your codebase.
-1. Let your team get comfortable with [vulnerability reports](vulnerability_report/index.md) and
- establish a vulnerability triage workflow.
-1. Consider creating [labels](../project/labels.md) and [issue boards](../project/issue_board.md) to
+The following steps help introduce you to GitLab application security tools incrementally.
+You can choose to enable features in a different order, or skip features that don't apply to your specific needs.
+You should start with:
+
+- [Secret Detection](secret_detection/index.md), which works with all programming languages and creates understandable results.
+- [Dependency Scanning](dependency_scanning/index.md), which finds known vulnerabilities in the dependencies your code uses.
+
+If it's your first time setting up GitLab security scanning, you should start with a single project.
+After you've gotten familiar with how scanning works, you can then choose to:
+
+- Follow [the same steps](#recommended-steps) to enable scanning in more projects.
+- [Enforce scanning](index.md#enforce-scan-execution) across more of your projects at once.
+
+## Recommended steps
+
+1. Choose a project to enable and test security features. Consider choosing a project:
+ - That uses your organization's typical programming languages and technologies, because some scanning features work differently across languages.
+ - Where you can try out new settings, like required approvals, without interrupting your team's daily work.
+ You could create a copy of a higher-traffic project for testing, or select a project that's not as busy.
+1. Create a merge request to [enable Secret Detection](secret_detection/index.md#enable-secret-detection) and [enable Dependency Scanning](dependency_scanning/index.md#configuration)
+ to identify any leaked secrets and vulnerable packages in that project.
+ - Security scanners run in your project's [CI/CD pipelines](../../ci/pipelines/index.md). Creating a merge request to update your [`.gitlab-ci.yml`](../../ci/index.md#the-gitlab-ciyml-file) helps you check how the scanners work with your project before they start running in every pipeline. In the merge request, you can change relevant [Secret Detection settings](secret_detection/index.md#configure-scan-settings) or [Dependency Scanning settings](dependency_scanning/index.md#available-cicd-variables) to accommodate your project's layout or configuration. For example, you might choose to exclude a directory of third-party code from scanning.
+ - After you merge this MR to your [default branch](../project/repository/branches/default.md), the system creates a baseline scan. This scan identifies which vulnerabilities already exist on the default branch so [merge requests](../project/merge_requests/index.md) can highlight only newly-introduced problems. Without a baseline scan, merge requests display every
+ vulnerability in the branch, even if the vulnerability already exists on the default branch.
+1. Let your team get comfortable with [viewing security findings in merge requests](index.md#view-security-scan-information) and the [vulnerability report](vulnerability_report/index.md).
+1. Establish a vulnerability triage workflow.
+ - Consider creating [labels](../project/labels.md) and [issue boards](../project/issue_board.md) to
help manage issues created from vulnerabilities. Issue boards allow all stakeholders to have a
common view of all issues and track remediation progress.
+1. Monitor the [Security Dashboard](security_dashboard/index.md) trends to gauge success in remediating existing vulnerabilities and preventing the introduction of new ones.
1. Enforce scheduled security scanning jobs by using a [scan execution policy](policies/scan-execution-policies.md).
- These scheduled jobs run independently from any other security scans you may have defined in a compliance framework pipeline or in the project's `.gitlab-ci.yml` file.
- Running regular dependency and [container scans](container_scanning/index.md) surface newly-discovered vulnerabilities that already exist in your repository.
- Scheduled scans are most useful for projects or important branches with low development activity where pipeline scans are infrequent.
1. Create a [scan result policy](policies/index.md) to limit new vulnerabilities from being merged
- into your `default` branch.
-1. Monitor the [Security Dashboard](security_dashboard/index.md) trends to gauge success in
- remediating existing vulnerabilities and preventing the introduction of new ones.
+ into your [default branch](../project/repository/branches/default.md).
1. Enable other scan types such as [SAST](sast/index.md), [DAST](dast/index.md),
[Fuzz testing](coverage_fuzzing/index.md), or [Container Scanning](container_scanning/index.md).
1. Use [Compliance Pipelines](../group/compliance_frameworks.md#compliance-pipelines)
diff --git a/doc/user/application_security/index.md b/doc/user/application_security/index.md
index 62155e07fbc..25fa1f5cbaf 100644
--- a/doc/user/application_security/index.md
+++ b/doc/user/application_security/index.md
@@ -177,6 +177,9 @@ By default, the application security jobs are configured to run for branch pipel
To use them with [merge request pipelines](../../ci/pipelines/merge_request_pipelines.md),
you must reference the [`latest` templates](../../development/cicd/templates.md).
+The latest version of the template may include breaking changes. Use the stable template unless you
+need a feature provided only in the latest template.
+
All `latest` security templates support merge request pipelines.
For example, to run both SAST and Dependency Scanning, the following template is used:
@@ -193,6 +196,9 @@ Mixing `latest` and `stable` security templates can cause both MR and branch pip
NOTE:
Latest templates can receive breaking changes in any release.
+For more information about template versioning, see the
+[CI/CD documentation](../../development/cicd/templates.md#latest-version).
+
## Default behavior of GitLab security scanning tools
### Secure jobs in your pipeline
@@ -264,7 +270,7 @@ In the Free tier, the reports above aren't parsed by GitLab. As a result, the wi
A merge request contains a security widget which displays a summary of the _new_ results. New results are determined by comparing the findings of the merge request against the findings of the most recent completed pipeline (`success`, `failed`, `canceled` or `skipped`) for the commit when the feature branch was created from the target branch.
-If security scans have not run for the completed pipeline in the target branch when the feature branch was created, there is no base for comparison. The vulnerabilities from the merge request findings are listed as new in the merge request security widget. We recommend you run a scan of the `default` (target) branch before enabling feature branch scans for your developers.
+GitLab checks the last 10 pipelines for the commit when the feature was created from the target branch to find one with security reports to use in comparison logic. If security scans have not run for the last 10 completed pipelines in the target branch when the feature branch was created, there is no base for comparison. The vulnerabilities from the merge request findings are listed as new in the merge request security widget. We recommend you run a scan of the `default` (target) branch before enabling feature branch scans for your developers.
The merge request security widget displays only a subset of the vulnerabilities in the generated JSON artifact because it contains both new and existing findings.
@@ -472,9 +478,9 @@ You can always find supported and deprecated schema versions in the [source code
You can interact with the results of the security scanning tools in several locations:
- [Scan information in merge requests](#merge-request)
-- [Project Security Dashboard](security_dashboard/index.md#view-vulnerabilities-over-time-for-a-project)
+- [Project Security Dashboard](security_dashboard/index.md#project-security-dashboard)
- [Security pipeline tab](security_dashboard/index.md)
-- [Group Security Dashboard](security_dashboard/index.md#view-vulnerabilities-over-time-for-a-group)
+- [Group Security Dashboard](security_dashboard/index.md#group-security-dashboard)
- [Security Center](security_dashboard/index.md#security-center)
- [Vulnerability Report](vulnerability_report/index.md)
- [Vulnerability Pages](vulnerabilities/index.md)
diff --git a/doc/user/application_security/policies/scan-execution-policies.md b/doc/user/application_security/policies/scan-execution-policies.md
index 0eb2355beb7..f6ef8a2c49e 100644
--- a/doc/user/application_security/policies/scan-execution-policies.md
+++ b/doc/user/application_security/policies/scan-execution-policies.md
@@ -28,9 +28,6 @@ implicitly so that the policies can be enforced. This ensures policies enabling
secret detection, static analysis, or other scanners that do not require a build in the
project, are still able to execute and be enforced.
-<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
-For an overview, see [Enforcing scan execution policies on projects with no GitLab CI/CD configuration](https://www.youtube.com/watch?v=sUfwQQ4-qHs).
-
In the event of a job name collision, GitLab appends a hyphen and a number to the job name. GitLab
increments the number until the name no longer conflicts with existing job names. If you create a
policy at the group level, it applies to every child project or subgroup. You cannot edit a
@@ -46,6 +43,9 @@ Policy jobs for scans other than DAST scans are created in the `test` stage of t
[`stages`](../../../ci/yaml/index.md#stages),
to remove the `test` stage, jobs will run in the `scan-policies` stage instead. This stage is injected into the CI pipeline at evaluation time if it doesn't exist. If the `build` stage exists, it is injected just after the `build` stage. If the `build` stage does not exist, it is injected at the beginning of the pipeline. DAST scans always run in the `dast` stage. If this stage does not exist, then a `dast` stage is injected at the end of the pipeline.
+- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> For a video walkthrough, see [How to set up Security Scan Policies in GitLab](https://youtu.be/ZBcqGmEwORA?si=aeT4EXtmHjosgjBY).
+- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> For an overview, see [Enforcing scan execution policies on projects with no GitLab CI/CD configuration](https://www.youtube.com/watch?v=sUfwQQ4-qHs).
+
## Scan execution policy editor
NOTE:
diff --git a/doc/user/application_security/policies/scan-result-policies.md b/doc/user/application_security/policies/scan-result-policies.md
index d892012c365..d0d3cb2ca03 100644
--- a/doc/user/application_security/policies/scan-result-policies.md
+++ b/doc/user/application_security/policies/scan-result-policies.md
@@ -27,13 +27,18 @@ The following video gives you an overview of GitLab scan result policies:
<iframe src="https://www.youtube-nocookie.com/embed/w5I9gcUgr9U" frameborder="0" allowfullscreen> </iframe>
</figure>
+## Requirements and limitations
+
+- You must add the respective [security scanning tools](../index.md#application-coverage).
+ Otherwise, scan result policies do not have any effect.
+- The maximum number of policies is five.
+- Each policy can have a maximum of five rules.
+- All configured scanners must be present in the merge request's latest pipeline. If not, approvals are required even if some vulnerability criteria have not been met.
+
## Merge request with multiple pipelines
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/379108) in GitLab 16.2 [with a flag](../../../administration/feature_flags.md) named `multi_pipeline_scan_result_policies`. Disabled by default.
-> - [Enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/409482) in GitLab 16.3.
-
-FLAG:
-On self-managed GitLab, by default this feature is available. To hide the feature, an administrator can [disable the feature flag](../../../administration/feature_flags.md) named `multi_pipeline_scan_result_policies`. On GitLab.com, this feature is available.
+> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/409482) in GitLab 16.3. Feature flag `multi_pipeline_scan_result_policies` removed.
A project can have multiple pipeline types configured. A single commit can initiate multiple
pipelines, each of which may contain a security scan.
@@ -78,36 +83,31 @@ When you save a new policy, GitLab validates its contents against [this JSON sch
If you're not familiar with how to read [JSON schemas](https://json-schema.org/),
the following sections and tables provide an alternative.
-| Field | Type | Required | Possible values | Description |
-|-------|------|----------|-----------------|-------------|
-| `scan_result_policy` | `array` of Scan Result Policy | true | | List of scan result policies (maximum 5). |
+| Field | Type | Required | Possible values | Description |
+|----------------------|-------------------------------|----------|-----------------|------------------------------------------|
+| `scan_result_policy` | `array` of Scan Result Policy | true | | List of scan result policies (maximum 5). |
## Scan result policy schema
-> The `approval_settings` field was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/418752) in GitLab 16.4 [with a flag](../../../administration/feature_flags.md) named `scan_result_policy_settings`. Disabled by default.
+> The `approval_settings` fields was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/418752) in GitLab 16.4 [with flags](../../../administration/feature_flags.md) named `scan_result_policies_block_unprotecting_branches`, `scan_result_any_merge_request`, or `scan_result_policies_block_force_push`. All are disabled by default.
FLAG:
-On self-managed GitLab, by default this feature is not available. To make it available, ask an administrator to [enable the feature flag](../../../administration/feature_flags.md) named `scan_result_policy_settings`.
-On GitLab.com, this feature is not available.
+On self-managed GitLab, by default the `approval_settings` field is unavailable. To show the feature, an administrator can [enable the feature flags](../../../administration/feature_flags.md) named `scan_result_policies_block_unprotecting_branches`, `scan_result_any_merge_request`, or `scan_result_policies_block_force_push`. See the `approval_settings` section below for more information.
| Field | Type | Required |Possible values | Description |
-|-------|------|----------|----------------|-------------|
+|--------|------|----------|----------------|-------------|
| `name` | `string` | true | | Name of the policy. Maximum of 255 characters.|
-| `description` (optional) | `string` | true | | Description of the policy. |
+| `description` | `string` | false | | Description of the policy. |
| `enabled` | `boolean` | true | `true`, `false` | Flag to enable (`true`) or disable (`false`) the policy. |
| `rules` | `array` of rules | true | | List of rules that the policy applies. |
-| `actions` | `array` of actions | false| | List of actions that the policy enforces. |
-| `approval_settings` | `object` | false | `{prevent_approval_by_author: boolean, prevent_approval_by_commit_author: boolean, remove_approvals_with_new_commit: boolean, require_password_to_approve: boolean}` | Project settings that the policy overrides. |
+| `actions` | `array` of actions | false | | List of actions that the policy enforces. |
+| `approval_settings` | `object` | false | | Project settings that the policy overrides. |
## `scan_finding` rule type
-> - The scan result policy field `vulnerability_attributes` was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/123052) in GitLab 16.2 [with a flag](../../../administration/feature_flags.md) named `enforce_vulnerability_attributes_rules`. [Enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/418784) in GitLab 16.3. Feature flag `enforce_vulnerability_attributes_rules` removed in GitLab 16.5.
+> - The scan result policy field `vulnerability_attributes` was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/123052) in GitLab 16.2 [with a flag](../../../administration/feature_flags.md) named `enforce_vulnerability_attributes_rules`. [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/418784) in GitLab 16.3. Feature flag removed.
> - The scan result policy field `vulnerability_age` was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/123956) in GitLab 16.2.
-> - The `branch_exceptions` field was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/418741) in GitLab 16.3 [with a flag](../../../administration/feature_flags.md) named `security_policies_branch_exceptions`. Generally available in GitLab 16.5. Feature flag removed.
-
-FLAG:
-On self-managed GitLab, by default the `branch_exceptions` field is available. To hide the feature, an administrator can [disable the feature flag](../../../administration/feature_flags.md) named `security_policies_branch_exceptions`.
-On GitLab.com, this feature is available.
+> - The `branch_exceptions` field was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/418741) in GitLab 16.3 [with a flag](../../../administration/feature_flags.md) named `security_policies_branch_exceptions`. [Generally available](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/133753) in GitLab 16.5. Feature flag removed.
This rule enforces the defined actions based on security scan findings.
@@ -128,11 +128,7 @@ This rule enforces the defined actions based on security scan findings.
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/8092) in GitLab 15.9 [with a flag](../../../administration/feature_flags.md) named `license_scanning_policies`.
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/397644) in GitLab 15.11. Feature flag `license_scanning_policies` removed.
-> - The `branch_exceptions` field was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/418741) in GitLab 16.3 [with a flag](../../../administration/feature_flags.md) named `security_policies_branch_exceptions`. Enabled by default.
-
-FLAG:
-On self-managed GitLab, by default the `branch_exceptions` field is available. To hide the feature, an administrator can [disable the feature flag](../../../administration/feature_flags.md) named `security_policies_branch_exceptions`.
-On GitLab.com, this feature is available.
+> - The `branch_exceptions` field was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/418741) in GitLab 16.3 [with a flag](../../../administration/feature_flags.md) named `security_policies_branch_exceptions`. Enabled by default. [Generally available](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/133753) in GitLab 16.5. Feature flag removed.
This rule enforces the defined actions based on license findings.
@@ -148,12 +144,11 @@ This rule enforces the defined actions based on license findings.
## `any_merge_request` rule type
-> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/418752) in GitLab 16.4.
-> - The `branch_exceptions` field was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/418741) in GitLab 16.3 [with a flag](../../../administration/feature_flags.md) named `security_policies_branch_exceptions`. Enabled by default.
+> - The `branch_exceptions` field was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/418741) in GitLab 16.3 [with a flag](../../../administration/feature_flags.md) named `security_policies_branch_exceptions`. Enabled by default. [Generally available](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/133753) in GitLab 16.5. Feature flag removed.
+> - The `any_merge_request` rule type was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/418752) in GitLab 16.4. Disabled by default.
FLAG:
-On self-managed GitLab, by default the `branch_exceptions` field is available. To hide the feature, an administrator can [disable the feature flag](../../../administration/feature_flags.md) named `security_policies_branch_exceptions`.
-On GitLab.com, this feature is available.
+On self-managed GitLab, by default the `any_merge_request` field is not available. To show the feature, an administrator can [enable the feature flag](../../../administration/feature_flags.md) named `any_merge_request`.
This rule enforces the defined actions for any merge request based on the commits signature.
@@ -180,13 +175,28 @@ the defined policy.
| `group_approvers_ids` | `array` of `integer` | false | ID of one of more groups | The IDs of groups to consider as approvers. Users with [direct membership in the group](../../project/merge_requests/approvals/rules.md#group-approvers) are eligible to approve. |
| `role_approvers` | `array` of `string` | false | One or more [roles](../../../user/permissions.md#roles) (for example: `owner`, `maintainer`) | The roles to consider as approvers that are eligible to approve. |
-Requirements and limitations:
+## `approval_settings`
-- You must add the respective [security scanning tools](../index.md#application-coverage).
- Otherwise, scan result policies do not have any effect.
-- The maximum number of policies is five.
-- Each policy can have a maximum of five rules.
-- All configured scanners must be present in the merge request's latest pipeline. If not, approvals are required even if some vulnerability criteria have not been met.
+> - The `block_unprotecting_branches` field was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/423101) in GitLab 16.4 [with flag](../../../administration/feature_flags.md) named `scan_result_policy_settings`. Disabled by default.
+> - The `scan_result_policy_settings` feature flag was replaced by the `scan_result_policies_block_unprotecting_branches` feature flag in 16.4.
+> - The `prevent_approval_by_author`, `prevent_approval_by_commit_author`, `remove_approvals_with_new_commit`, and `require_password_to_approve` fields were [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/418752) in GitLab 16.4 [with flag](../../../administration/feature_flags.md) named `scan_result_any_merge_request`. Disabled by default.
+> - The `prevent_force_pushing` field was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/420629) in GitLab 16.4 [with flag](../../../administration/feature_flags.md) named `scan_result_policies_block_force_push`. Disabled by default.
+
+FLAG:
+On self-managed GitLab, by default the `block_unprotecting_branches` field is unavailable. To show the feature, an administrator can [enable the feature flag](../../../administration/feature_flags.md) named `scan_result_policies_block_unprotecting_branches`. On GitLab.com, this feature is unavailable.
+On self-managed GitLab, by default the `prevent_approval_by_author`, `prevent_approval_by_commit_author`, `remove_approvals_with_new_commit`, and `require_password_to_approve` fields are unavailable. To show the feature, an administrator can [enable the feature flag](../../../administration/feature_flags.md) named `scan_result_any_merge_request`. On GitLab.com, this feature is available.
+On self-managed GitLab, by default the `prevent_force_pushing` field is unavailable. To show the feature, an administrator can [enable the feature flag](../../../administration/feature_flags.md) named `security_policies_branch_exceptions`. On GitLab.com, this feature is unavailable.
+
+The settings set in the policy overwrite settings in the project.
+
+| Field | Type | Required | Possible values | Description |
+|-------|------|----------|-----------------|-------------|
+| `block_unprotecting_branches` | `boolean` | false | `true`, `false` | Prevent a user from removing a branch from the protected branches list, deleting a protected branch, or changing the default branch if that branch is included in the security policy. |
+| `prevent_approval_by_author` | `boolean` | false | `true`, `false` | When enabled, two person approval is required on all MRs as merge request authors cannot approve their own MRs and merge them unilaterally. |
+| `prevent_approval_by_commit_author` | `boolean` | false | `true`, `false` | When enabled, users who have contributed code to the MR are ineligible for approval, ensuring code committers cannot introduce vulnerabilities and approve code to merge. |
+| `remove_approvals_with_new_commit` | `boolean` | false | `true`, `false` | If an MR receives all necessary approvals to merge, but then a new commit is added, new approvals are required. This ensures new commits that may include vulnerabilities cannot be introduced. |
+| `require_password_to_approve` | `boolean` | false | `true`, `false` | Password confirmation on approvals provides an additional level of security. Enabling this enforces the setting on all projects targeted by this policy. |
+| `prevent_force_pushing` | `boolean` | false | `true`, `false` | Prevent pushing and force pushing to a protected branch. |
## Example security scan result policies project
@@ -257,28 +267,47 @@ You can use this example in the YAML mode of the [Scan Result Policy editor](#sc
It corresponds to a single object from the previous example:
```yaml
-- name: critical vulnerability CS approvals
- description: critical severity level only for container scanning
- enabled: true
- rules:
- - type: scan_finding
- branches:
- - main
- scanners:
- - container_scanning
- vulnerabilities_allowed: 1
- severity_levels:
- - critical
- vulnerability_states:
- - newly_detected
- actions:
- - type: require_approval
- approvals_required: 1
- user_approvers:
- - adalberto.dare
+type: scan_result_policy
+name: critical vulnerability CS approvals
+description: critical severity level only for container scanning
+enabled: true
+rules:
+- type: scan_finding
+ branches:
+ - main
+ scanners:
+ - container_scanning
+ vulnerabilities_allowed: 1
+ severity_levels:
+ - critical
+ vulnerability_states:
+ - newly_detected
+actions:
+- type: require_approval
+ approvals_required: 1
+ user_approvers:
+ - adalberto.dare
```
-## Example situations where scan result policies require additional approval
+## Understanding scan result policy approvals
+
+### Scope of scan result policy comparison
+
+- To determine when approval is required on a merge request, we compare the latest completed pipelines for each supported pipeline source for the source and target branch (for example, `feature`/`main`). This ensures the most comprehensive evaluation of scan results.
+- We compare findings from the latest completed pipelines that ran on `HEAD` of the source and target branch.
+- Scan result policies considers all supported pipeline sources (based on the [`CI_PIPELINE_SOURCE` variable](../../../ci/variables/predefined_variables.md)) when comparing results from both the source and target branches when determining if a merge request requires approval. Pipeline sources `webide` and `parent_pipeline` are not supported.
+
+### Accepting risk and ignoring vulnerabilities in future merge requests
+
+For scan result policies that are scoped to `newly_detected` findings, it's important to understand the implications of this vulnerability state. A finding is considered `newly_detected` if it exists on the merge request's branch but not on the default branch. When a merge request whose branch contains `newly_detected` findings is approved and merged, approvers are "accepting the risk" of those vulnerabilities. If one or more of the same vulnerabilities were detected after this time, their status would be `previously_detected` and so not be out of scope of a policy aimed at `newly_detected` findings. For example:
+
+- A scan result policy is created to block critical SAST findings. If a SAST finding for CVE-1234 is approved, future merge requests with the same violation will not require approval in the project.
+
+When using license approval policies, the combination of project, component (dependency), and license are considered in the evaluation. If a license is approved as an exception, future merge requests don't require approval for the same combination of project, component (dependency), and license. The component's version is not be considered in this case. If a previously approved package is updated to a new version, approvers will not need to re-approve. For example:
+
+- A license approval policy is created to block merge requests with newly detected licenses matching `AGPL-1.0`. A change is made in project `demo` for component `osframework` that violates the policy. If approved and merged, future merge requests to `osframework` in project `demo` with the license `AGPL-1.0` don't require approval.
+
+### Multiple approvals
There are several situations where the scan result policy requires an additional approval step. For example:
@@ -295,3 +324,43 @@ There are several situations where the scan result policy requires an additional
- Someone stops a pipeline security job, and users can't skip the security scan.
- A job in a merge request fails and is configured with `allow_failure: false`. As a result, the pipeline is in a blocked state.
- A pipeline has a manual job that must run successfully for the entire pipeline to pass.
+
+### Known issues
+
+We have identified in [epic 11020](https://gitlab.com/groups/gitlab-org/-/epics/11020) common areas of confusion in scan result findings that need to be addressed. Below are a few of the known issues:
+
+- When using `newly_detected`, some findings may require approval when they are not introduced by the merge request (such as a new CVE on a related dependency). We currently use `main tip` of the target branch for comparison. In the future, we plan to use `merge base` for `newly_detected` policies (see [issue 428518](https://gitlab.com/gitlab-org/gitlab/-/issues/428518)).
+- Findings or errors that cause approval to be required on a scan result policy may not be evident in the Security MR Widget. By using `merge base` in [issue 428518](https://gitlab.com/gitlab-org/gitlab/-/issues/428518) some cases will be addressed. We will additionally be [displaying more granular details](https://gitlab.com/groups/gitlab-org/-/epics/11185) about what caused security policy violations.
+- Security policy violations are distinct compared to findings displayed in the MR widgets. Some violations may not be present in the MR widget. We are working to harmonize our features in [epic 11020](https://gitlab.com/groups/gitlab-org/-/epics/11020) and to display policy violations explicitly in merge requests in [epic 11185](https://gitlab.com/groups/gitlab-org/-/epics/11185).
+
+## Troubleshooting
+
+### Merge request rules widget shows a scan result policy is invalid or duplicated **(ULTIMATE SELF)**
+
+On GitLab self-managed from 15.0 to 16.4, the most likely cause is that the project was exported from a
+group and imported into another, and had scan result policy rules. These rules are stored in a
+separate project to the one that was exported. As a result, the project contains policy rules that
+reference entities that don't exist in the imported project's group. The result is policy rules that
+are invalid, duplicated, or both.
+
+To remove all invalid scan result policy rules from a GitLab instance, an administrator can run
+the following script in the [Rails console](../../../administration/operations/rails_console.md).
+
+```ruby
+Project.joins(:approval_rules).where(approval_rules: { report_type: %i[scan_finding license_scanning] }).where.not(approval_rules: { security_orchestration_policy_configuration_id: nil }).find_in_batches.flat_map do |batch|
+ batch.map do |project|
+ # Get projects and their configuration_ids for applicable project rules
+ [project, project.approval_rules.where(report_type: %i[scan_finding license_scanning]).pluck(:security_orchestration_policy_configuration_id).uniq]
+ end.uniq.map do |project, configuration_ids| # We take only unique combinations of project + configuration_ids
+ # If we find more configurations than what is available for the project, we take records with the extra configurations
+ [project, configuration_ids - project.all_security_orchestration_policy_configurations.pluck(:id)]
+ end.select { |_project, configuration_ids| configuration_ids.any? }
+end.each do |project, configuration_ids|
+ # For each found pair project + ghost configuration, we remove these rules for a given project
+ Security::OrchestrationPolicyConfiguration.where(id: configuration_ids).each do |configuration|
+ configuration.delete_scan_finding_rules_for_project(project.id)
+ end
+ # Ensure we sync any potential rules from new group's policy
+ Security::ScanResultPolicies::SyncProjectWorker.perform_async(project.id)
+end
+```
diff --git a/doc/user/application_security/sast/customize_rulesets.md b/doc/user/application_security/sast/customize_rulesets.md
index 90731114303..ed3b33fc35b 100644
--- a/doc/user/application_security/sast/customize_rulesets.md
+++ b/doc/user/application_security/sast/customize_rulesets.md
@@ -597,7 +597,7 @@ rules:
The following example [enables SAST](index.md#configure-sast-in-your-cicd-yaml) and uses a shared ruleset customization file. The file is:
-- Downloaded from a private project that requires authentication, by using a [Group Access Token](../../group/settings/group_access_tokens.md).
+- Downloaded from a private project that requires authentication, by using a [Group Access Token](../../group/settings/group_access_tokens.md) securely stored within a CI variable.
- Checked out at a specific Git commit SHA instead of the default branch.
See [group access tokens](../../group/settings/group_access_tokens.md#bot-users-for-groups) for how to find the username associated with a group token.
@@ -607,5 +607,5 @@ include:
- template: Security/SAST.gitlab-ci.yml
variables:
- SAST_RULESET_GIT_REFERENCE: "group_2504721_bot_7c9311ffb83f2850e794d478ccee36f5:glpat-1234567@gitlab.com/example-group/example-ruleset-project@c8ea7e3ff126987fb4819cc35f2310755511c2ab"
+ SAST_RULESET_GIT_REFERENCE: "group_2504721_bot_7c9311ffb83f2850e794d478ccee36f5:$PERSONAL_ACCESS_TOKEN@gitlab.com/example-group/example-ruleset-project@c8ea7e3ff126987fb4819cc35f2310755511c2ab"
```
diff --git a/doc/user/application_security/sast/index.md b/doc/user/application_security/sast/index.md
index acc7e9d9e84..770e24d87ca 100644
--- a/doc/user/application_security/sast/index.md
+++ b/doc/user/application_security/sast/index.md
@@ -273,6 +273,10 @@ When downloading, you always receive the most recent SAST artifact available.
You can enable and configure SAST by using the UI, either with the default settings or with customizations.
The method you can use depends on your GitLab license tier.
+### Running jobs in merge request pipelines
+
+See [Use security scanning tools with merge request pipelines](../index.md#use-security-scanning-tools-with-merge-request-pipelines)
+
#### Configure SAST with customizations **(ULTIMATE ALL)**
> [Removed](https://gitlab.com/gitlab-org/gitlab/-/issues/410013) individual SAST analyzers configuration options from the UI in GitLab 16.2.
diff --git a/doc/user/application_security/sast/rules.md b/doc/user/application_security/sast/rules.md
index e4054764e1f..3fb24bcd66b 100644
--- a/doc/user/application_security/sast/rules.md
+++ b/doc/user/application_security/sast/rules.md
@@ -102,7 +102,7 @@ More details are available in release announcements and in the CHANGELOG links p
Key changes to the GitLab-managed ruleset for Semgrep-based scanning include:
-- Beginning in GitLab 16.3, the GitLab Static Analysis and Vulnerability Research teams are working to remove rules that tend to produce too many false positive results or not enough actionable true positive results. Existing findings from these removed rules are [automatically resolved](index.md#automatic-vulnerability-resolution); they no longer appear in the [Security Dashboard](../security_dashboard/index.md#view-vulnerabilities-over-time-for-a-project) or in the default view of the [Vulnerability Report](../vulnerability_report/index.md). This work is tracked in [epic 10907](https://gitlab.com/groups/gitlab-org/-/epics/10907).
+- Beginning in GitLab 16.3, the GitLab Static Analysis and Vulnerability Research teams are working to remove rules that tend to produce too many false positive results or not enough actionable true positive results. Existing findings from these removed rules are [automatically resolved](index.md#automatic-vulnerability-resolution); they no longer appear in the [Security Dashboard](../security_dashboard/index.md#project-security-dashboard) or in the default view of the [Vulnerability Report](../vulnerability_report/index.md). This work is tracked in [epic 10907](https://gitlab.com/groups/gitlab-org/-/epics/10907).
- In GitLab 16.0 through 16.2, the GitLab Vulnerability Research team updated the guidance that's included in each result.
- In GitLab 15.10, the `detect-object-injection` rule was [removed by default](https://gitlab.com/gitlab-org/gitlab/-/issues/373920) and its findings were [automatically resolved](index.md#automatic-vulnerability-resolution).
diff --git a/doc/user/application_security/sast/troubleshooting.md b/doc/user/application_security/sast/troubleshooting.md
index 34a2a3d01af..77a2f20c934 100644
--- a/doc/user/application_security/sast/troubleshooting.md
+++ b/doc/user/application_security/sast/troubleshooting.md
@@ -56,14 +56,14 @@ For information on this, see the [general Application Security troubleshooting s
For information on this, see the [GitLab Secure troubleshooting section](../index.md#error-job-is-used-for-configuration-only-and-its-script-should-not-be-executed).
-## Limitation when using rules:exists
+## SAST jobs are running unexpectedly
The [SAST CI template](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml)
-uses the `rules:exists` parameter. For performance reasons, a maximum number of matches are made
-against the given glob pattern. If the number of matches exceeds the maximum, the `rules:exists`
+uses the `rules:exists` parameter. For performance reasons, a maximum number of 10000 matches are
+made against the given glob pattern. If the number of matches exceeds the maximum, the `rules:exists`
parameter returns `true`. Depending on the number of files in your repository, a SAST job might be
-triggered even if the scanner doesn't support your project. For more details about this issue, see
-the [`rules:exists` documentation](../../../ci/yaml/index.md#rulesexists).
+triggered even if the scanner doesn't support your project. For more details about this limitation,
+see the [`rules:exists` documentation](../../../ci/yaml/index.md#rulesexists).
## SpotBugs UTF-8 unmappable character errors
diff --git a/doc/user/application_security/secret_detection/index.md b/doc/user/application_security/secret_detection/index.md
index 18016f6f342..4332b91c0f9 100644
--- a/doc/user/application_security/secret_detection/index.md
+++ b/doc/user/application_security/secret_detection/index.md
@@ -6,19 +6,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Secret Detection **(FREE ALL)**
-> - In GitLab 13.1, Secret Detection was split from the [SAST configuration](../sast/index.md#configuration)
-> into its own CI/CD template. If you're using GitLab 13.0 or earlier and SAST is enabled, then
-> Secret Detection is already enabled.
-> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/222788) from GitLab Ultimate to GitLab
-> Free in 13.3.
-> - [In GitLab 14.0](https://gitlab.com/gitlab-org/gitlab/-/issues/297269), Secret Detection jobs
-> `secret_detection_default_branch` and `secret_detection` were consolidated into one job,
-> `secret_detection`.
-
-<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
-For an interactive reading and how-to demo of this Secret Detection doc, see [How to enable secret detection in GitLab Application Security Part 1/2](https://youtu.be/dbMxeO6nJCE?feature=shared) and [How to enable secret detection in GitLab Application Security Part 2/2](https://youtu.be/VL-_hdiTazo?feature=shared)
-<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
-For an interactive reading and how-to demo playlist, see [Get Started With GitLab Application Security Playlist](https://www.youtube.com/playlist?list=PL05JrBw4t0KrUrjDoefSkgZLx5aJYFaF9)
+> [In GitLab 14.0](https://gitlab.com/gitlab-org/gitlab/-/issues/297269), Secret Detection jobs `secret_detection_default_branch` and `secret_detection` were consolidated into one job, `secret_detection`.
People sometimes accidentally commit secrets like keys or API tokens to Git repositories.
After a sensitive value is pushed to a remote repository, anyone with access to the repository can impersonate the authorized user of the secret for malicious purposes.
@@ -37,6 +25,13 @@ With GitLab Ultimate, Secret Detection results are also processed so you can:
- Review them in the security dashboard.
- [Automatically respond](automatic_response.md) to leaks in public repositories.
+<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> For an interactive reading and how-to demo of this Secret Detection documentation see:
+
+- [How to enable secret detection in GitLab Application Security Part 1/2](https://youtu.be/dbMxeO6nJCE?feature=shared)
+- [How to enable secret detection in GitLab Application Security Part 2/2](https://youtu.be/VL-_hdiTazo?feature=shared)
+
+<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> For other interactive reading and how-to demos, see the [Get Started With GitLab Application Security Playlist](https://www.youtube.com/playlist?list=PL05JrBw4t0KrUrjDoefSkgZLx5aJYFaF9).
+
## Detected secrets
GitLab maintains the detection rules used in Secret Detection.
@@ -111,26 +106,13 @@ Secret Detection can detect if a secret was added in one commit and removed in a
- Merge request
In a merge request, Secret Detection scans every commit made on the source branch. To use this
- feature, you must use the [`latest` Secret Detection template](#templates), as it supports
+ feature, you must use the [`latest` Secret Detection template](../index.md#use-security-scanning-tools-with-merge-request-pipelines), as it supports
[merge request pipelines](../../../ci/pipelines/merge_request_pipelines.md). Secret Detection's
results are only available after the pipeline is completed.
-## Templates
+## Running jobs in merge request pipelines
-Secret Detection default configuration is defined in CI/CD templates. Updates to the template are
-provided with GitLab upgrades, allowing you to benefit from any improvements and additions.
-
-Available templates:
-
-- [`Secret-Detection.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Jobs/Secret-Detection.gitlab-ci.yml): Stable, default version of the Secret Detection CI/CD template.
-- [`Secret-Detection.latest.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Jobs/Secret-Detection.latest.gitlab-ci.yml): Latest version of the Secret Detection template.
-
-WARNING:
-The latest version of the template may include breaking changes. Use the stable template unless you
-need a feature provided only in the latest template.
-
-For more information about template versioning, see the
-[CI/CD documentation](../../../development/cicd/templates.md#latest-version).
+See [Use security scanning tools with merge request pipelines](../index.md#use-security-scanning-tools-with-merge-request-pipelines)
## Enable Secret Detection
@@ -166,7 +148,7 @@ your GitLab CI/CD configuration file is complex.
```yaml
include:
- - template: Security/Secret-Detection.gitlab-ci.yml
+ - template: Jobs/Secret-Detection.gitlab-ci.yml
```
1. Select the **Validate** tab, then select **Validate pipeline**.
@@ -232,7 +214,7 @@ This example uses a specific minor version of the analyzer:
```yaml
include:
- - template: Security/Secret-Detection.gitlab-ci.yml
+ - template: Jobs/Secret-Detection.gitlab-ci.yml
secret_detection:
variables:
@@ -262,7 +244,7 @@ In the following example _extract_ of a `.gitlab-ci.yml` file:
```yaml
include:
- - template: Security/Secret-Detection.gitlab-ci.yml
+ - template: Jobs/Secret-Detection.gitlab-ci.yml
secret_detection:
variables:
@@ -322,7 +304,7 @@ variables:
SECRET_DETECTION_IMAGE_SUFFIX: '-fips'
include:
- - template: Security/Secret-Detection.gitlab-ci.yml
+ - template: Jobs/Secret-Detection.gitlab-ci.yml
```
## Full history Secret Detection
@@ -576,7 +558,7 @@ Prerequisites:
```yaml
include:
- - template: Security/Secret-Detection.gitlab-ci.yml
+ - template: Jobs/Secret-Detection.gitlab-ci.yml
variables:
SECURE_ANALYZERS_PREFIX: "localhost:5000/analyzers"
diff --git a/doc/user/application_security/security_dashboard/img/group_security_dashboard.png b/doc/user/application_security/security_dashboard/img/group_security_dashboard.png
new file mode 100644
index 00000000000..1d324b8207a
--- /dev/null
+++ b/doc/user/application_security/security_dashboard/img/group_security_dashboard.png
Binary files differ
diff --git a/doc/user/application_security/security_dashboard/img/project_security_dashboard.png b/doc/user/application_security/security_dashboard/img/project_security_dashboard.png
new file mode 100644
index 00000000000..46fdebca9cd
--- /dev/null
+++ b/doc/user/application_security/security_dashboard/img/project_security_dashboard.png
Binary files differ
diff --git a/doc/user/application_security/security_dashboard/img/security_center_dashboard_v15_10.png b/doc/user/application_security/security_dashboard/img/security_center_dashboard_v15_10.png
deleted file mode 100644
index c2780fce787..00000000000
--- a/doc/user/application_security/security_dashboard/img/security_center_dashboard_v15_10.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/application_security/security_dashboard/index.md b/doc/user/application_security/security_dashboard/index.md
index 53a6dfe6d0a..89c950f2473 100644
--- a/doc/user/application_security/security_dashboard/index.md
+++ b/doc/user/application_security/security_dashboard/index.md
@@ -7,64 +7,42 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# GitLab Security Dashboards and Security Center **(ULTIMATE ALL)**
-You can use Security Dashboards to view trends about vulnerabilities
-detected by [security scanners](../index.md#application-coverage).
-These trends are shown in projects, groups, and the Security Center.
+## Security Dashboards
-To use the Security Dashboards, you must:
+Security Dashboards are used to assess the security posture of your applications. GitLab provides
+you with a collection of metrics, ratings, and charts for the vulnerabilities detected by the [security scanners](../index.md#application-coverage) run on your project. The security dashboard provides data such as:
-- Configure at least one [security scanner](../index.md#application-coverage) in a project.
-- Configure jobs to use the [`reports` syntax](../../../ci/yaml/index.md#artifactsreports).
-- Use [GitLab Runner](https://docs.gitlab.com/runner/) 11.5 or later. If you use the
- shared runners on GitLab.com, you are using the correct version.
-- Have the [correct role](../../permissions.md) for the project or group.
+- Vulnerability trends over a 30, 60, or 90-day time-frame for all projects in a group
+- A letter grade rating for each project based on vulnerability severity
+- The total number of vulnerabilities detected within the last 365 days including their severity
+
+The data provided by the Security Dashboards can be used supply to insight on what decisions can be made to improve your security posture. For example, using the 365 day trend view, you can see on which days a significant number of vulnerabilities were introduced. Then you can examine the code changes performed on those particular days in order perform a root-cause analysis to create better policies for preventing the introduction of vulnerabilities in the future.
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
For an overview, see [Security Dashboard](https://www.youtube.com/watch?v=QHQHN4luNpc).
-## When Security Dashboards are updated
-
-The Security Dashboards show results of scans from the most recent completed pipeline on the
-[default branch](../../project/repository/branches/default.md).
-Dashboards are updated with the result of completed pipelines run on the default branch; they do not include vulnerabilities discovered in pipelines from other un-merged branches.
-
-If you use manual jobs, for example gate deployments, in the default branch's pipeline,
-the results of any scans are only updated when the job has been successfully run.
-If manual jobs are skipped regularly, you should to define the job as optional,
-using the [`allow_failure`](../../../ci/jobs/job_control.md#types-of-manual-jobs) attribute.
-
-To ensure regular security scans (even on infrequently developed projects),
-you should use [scan execution policies](../../../user/application_security/policies/scan-execution-policies.md).
-Alternatively, you can
-[configure a scheduled pipeline](../../../ci/pipelines/schedules.md).
+## Prerequisites
-## Reduce false negatives in dependency scans
+To view the Security Dashboards, the following is required:
-WARNING:
-False negatives occur when you resolve dependency versions during a scan, which differ from those
-resolved when your project built and released in a previous pipeline.
+- [Maintainer Role](../../permissions.md#roles) for the project or group.
+- At least one [security scanner](../index.md#application-coverage) configured within your project.
+- A successful security scan performed on the [default branch](../../project/repository/branches/default.md) of your project
-To reduce false negatives in [dependency scans](../../../user/application_security/dependency_scanning/index.md) in scheduled pipelines, ensure you:
-
-- Include a lock file in your project. A lock file lists all transient dependencies and tracks their versions.
- - Java projects can't have lock files.
- - Python projects can have lock files, but GitLab Secure tools don't support them.
-- Configure your project for [Continuous Delivery](../../../ci/introduction/index.md).
+**Note**:
+The Security Dashboards show results of scans from the most recent completed pipeline on the
+[default branch](../../project/repository/branches/default.md). Dashboards are updated with the result of completed pipelines run on the default branch; they do not include vulnerabilities discovered in pipelines from other un-merged branches.
-## View vulnerabilities over time for a project
+## Viewing the Security Dashboard
-> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/235558) in GitLab 13.6.
-> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/285476) in GitLab 13.10, options to zoom in on a date range, and download the vulnerabilities chart.
-> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/285477) in GitLab 13.11, date range slider to visualize data between given dates.
+The Security Dashboard can be seen at the project, group, and the Security Center levels.
+Each dashboard provides a unique viewpoint of your security posture.
-The project Security Dashboard shows the total number of vulnerabilities
-over time, with up to 365 days of historical data. Data refresh begins daily at 01:15 UTC via a scheduled job.
-Each refresh captures a snapshot of open vulnerabilities. Data is not backported to prior days
-so vulnerabilities opened after the job has already run for the day cannot be reflected in the
-counts until the following day's refresh job.
-Project Security Dashboards show statistics for all vulnerabilities with a current status of `Needs triage` or `Confirmed` .
+### Project Security Dashboard
-To view total number of vulnerabilities over time:
+The Project Security Dashboard shows the total number of vulnerabilities detected over time,
+with up to 365 days of historical data for a given project. You can view the Project Security
+Dashboard:
1. On the left sidebar, select **Search or go to** and find your project.
1. Select **Secure > Security dashboard**.
@@ -75,70 +53,63 @@ To view total number of vulnerabilities over time:
across the chart.
- To reset to the original range, select **Remove Selection** (**{redo}**).
-### Download the vulnerabilities chart
+![Project Security Dashboard](img/project_security_dashboard.png)
-To download an SVG image of the vulnerabilities chart:
+#### Downloading the vulnerability chart
+
+You can download an image of the vulnerability chart from the Project Security Dashboard
+to use in documentation, presentations, and so on. To download the image of the vulnerability
+chart:
1. On the left sidebar, select **Search or go to** and find your project.
1. Select **Secure > Security dashboard**.
1. Select **Save chart as an image** (**{download}**).
-## View vulnerabilities over time for a group
-
-The group Security Dashboard gives an overview of vulnerabilities found in the default
-branches of projects in a group and its subgroups.
-
-To view vulnerabilities over time for a group:
-
-1. On the left sidebar, select **Search or go to** and find your group.
-1. Select **Security > Security dashboard**.
-1. Hover over the chart to get more details about vulnerabilities.
- - You can display the vulnerability trends over a 30, 60, or 90-day time frame (the default is 90 days).
- - To view aggregated data beyond a 90-day time frame, use the
- [VulnerabilitiesCountByDay GraphQL API](../../../api/graphql/reference/index.md#vulnerabilitiescountbyday).
- GitLab retains the data for 365 days.
-
-## View project security status for a group
-
-Use the group Security Dashboard to view the security status of projects.
-
-To view project security status for a group:
+You will then be prompted to download the image in SVG format.
-1. On the left sidebar, select **Search or go to** and find your group.
-1. Select **Secure > Security dashboard**.
-
-Each project is assigned a letter [grade](#project-vulnerability-grades) according to the highest-severity open vulnerability.
-Dismissed or resolved vulnerabilities are excluded. Each project can receive only one letter grade and appears only once
-in the Project security status report.
+### Group Security Dashboard
-To view vulnerabilities, go to the group's [vulnerability report](../vulnerability_report/index.md).
+The group Security Dashboard provides an overview of vulnerabilities found in the default
+branches of all projects in a group and its subgroups. The Group Security Dashboard
+supplies the following:
-### Project vulnerability grades
+- Vulnerability trends over a 30, 60, or 90-day time frame
+- A letter grade for each project in the group according to its highest-severity open vulnerability. The letter grades are assigned using the following criteria:
| Grade | Description |
-| --- | --- |
+| ----- | ----------- |
| **F** | One or more `critical` vulnerabilities |
| **D** | One or more `high` or `unknown` vulnerabilities |
| **C** | One or more `medium` vulnerabilities |
| **B** | One or more `low` vulnerabilities |
| **A** | Zero vulnerabilities |
-## Security Center
+To view group security dashboard:
-> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/3426) in GitLab 13.4.
+1. On the left sidebar, select **Search or go to** and find your group.
+1. Select **Security > Security dashboard**.
+1. Hover over the **Vulnerabilities over time** chart to get more details about vulnerabilities.
+ - You can display the vulnerability trends over a 30, 60, or 90-day time frame (the default is 90 days).
+ - To view aggregated data beyond a 90-day time frame, use the [VulnerabilitiesCountByDay GraphQL API](../../../api/graphql/reference/index.md#vulnerabilitiescountbyday). GitLab retains the data for 365 days.
-The Security Center is a personal space where you view vulnerabilities across all your projects. It
-shows the vulnerabilities present in the default branches of the projects.
+1. Select the arrows under the **Project security status** section to see the what projects fall under a particular letter-grade rating:
+ - You can see how many vulnerabilities of a particular severity are found in a project
+ - You can select a project's name to directly access its project security dashboard
-The Security Center includes:
+![Group Security Dashboard](img/group_security_dashboard.png)
-- The group Security Dashboard.
-- A [vulnerability report](../vulnerability_report/index.md).
-- A settings area to configure which projects to display.
+## Security Center
-![Security Center Dashboard with projects](img/security_center_dashboard_v15_10.png)
+> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/3426) in GitLab 13.4.
+
+The Security Center is a configurable personal space where you can view vulnerabilities across all the
+projects you belong to. The Security Center includes:
-### View the Security Center
+- The group Security Dashboard
+- A [vulnerability report](../vulnerability_report/index.md)
+- A settings area to configure which projects to display
+
+### Viewing the Security Center
To view the Security Center:
@@ -146,7 +117,9 @@ To view the Security Center:
1. Select **Your work**.
1. Select **Security > Security dashboard**.
-### Add projects to the Security Center
+The Security Center is blank by default. You must add a project which have been configured with at least one security scanner.
+
+### Adding Projects to the Security Center
To add projects to the Security Center:
@@ -157,26 +130,9 @@ To add projects to the Security Center:
1. Use the **Search your projects** text box to search for and select projects.
1. Select **Add projects**.
-After you add projects, the security dashboard and vulnerability report show the vulnerabilities
-found in those projects' default branches.
-
-You can add a maximum of 1,000 projects, however the **Project** filter in the **Vulnerability
-Report** is limited to 100 projects.
-
-<!-- ## Troubleshooting
-
-Include any troubleshooting steps that you can foresee. If you know beforehand what issues
-one might have when setting this up, or when something is changed, or on upgrading, it's
-important to describe those, too. Think of things that may go wrong and include them here.
-This is important to minimize requests for support, and to avoid doc comments with
-questions that you know someone might ask.
-
-Each scenario can be a third-level heading, for example `### Getting error message X`.
-If you have none to add when creating a doc, leave this section in place
-but commented out to help encourage others to add to it in the future. -->
+After you add projects, the security dashboard and vulnerability report show the vulnerabilities found in those projects' default branches. You can add a maximum of 1,000 projects, however the **Project** filter in the **Vulnerability Report** is limited to 100 projects.
## Related topics
-- [Address the vulnerabilities](../vulnerabilities/index.md)
- [Vulnerability reports](../vulnerability_report/index.md)
- [Vulnerability Page](../vulnerabilities/index.md)
diff --git a/doc/user/application_security/terminology/index.md b/doc/user/application_security/terminology/index.md
index 0f0a61a2b02..f09672685de 100644
--- a/doc/user/application_security/terminology/index.md
+++ b/doc/user/application_security/terminology/index.md
@@ -259,7 +259,7 @@ A finding's primary identifier is a value that is unique to each finding. The ex
of the finding's [first identifier](https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/blob/v2.4.0-rc1/dist/sast-report-format.json#L228)
combine to create the value.
-Examples of primary identifiers include `PluginID` for OWASP Zed Attack Proxy (ZAP), or `CVE` for
+Examples of primary identifiers include `PluginID` for Zed Attack Proxy (ZAP), or `CVE` for
Trivy. The identifier must be stable. Subsequent scans must return the same value for the
same finding, even if the location has slightly changed.
diff --git a/doc/user/application_security/vulnerabilities/img/create_mr_from_vulnerability_v13_4.png b/doc/user/application_security/vulnerabilities/img/create_mr_from_vulnerability_v13_4.png
deleted file mode 100644
index 55694fc7926..00000000000
--- a/doc/user/application_security/vulnerabilities/img/create_mr_from_vulnerability_v13_4.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/application_security/vulnerabilities/img/create_mr_from_vulnerability_v13_4_updated.png b/doc/user/application_security/vulnerabilities/img/create_mr_from_vulnerability_v13_4_updated.png
new file mode 100644
index 00000000000..7c1a5d4e298
--- /dev/null
+++ b/doc/user/application_security/vulnerabilities/img/create_mr_from_vulnerability_v13_4_updated.png
Binary files differ
diff --git a/doc/user/application_security/vulnerabilities/index.md b/doc/user/application_security/vulnerabilities/index.md
index 34c57292767..476b2411621 100644
--- a/doc/user/application_security/vulnerabilities/index.md
+++ b/doc/user/application_security/vulnerabilities/index.md
@@ -104,7 +104,13 @@ When dismissing a vulnerability, one of the following reasons must be chosen to
- **Used in tests**: The finding is not a vulnerability because it is part of a test or is test data.
- **Not applicable**: The vulnerability is known, and has not been remediated or mitigated, but is considered to be in a part of the application that will not be updated.
-## Change status of a vulnerability
+## Change the status of a vulnerability
+
+> In GitLab 16.4 the ability for `Developers` to change the status of a vulnerability (`admin_vulnerability`) was [deprecated](../../../update/deprecations.md#deprecate-change-vulnerability-status-from-the-developer-role). The `admin_vulnerability` permission will be removed, by default, from all `Developer` roles in GitLab 17.0.
+
+Prerequisites:
+
+- You must have at least the Developer role for the project.
To change a vulnerability's status from its Vulnerability Page:
@@ -146,8 +152,9 @@ The issue is then opened so you can take further action.
Prerequisites:
-- [Enable Jira integration](../../../integration/jira/index.md). The **Enable Jira issue creation
- from vulnerabilities** option must be selected as part of the configuration.
+- [Enable Jira integration](../../../integration/jira/configure.md). The
+ **Enable Jira issue creation from vulnerabilities** option must be selected as part
+ of the configuration.
- Each user must have a personal Jira user account with permission to create issues in the target
project.
@@ -242,7 +249,7 @@ To resolve a vulnerability, you can either:
- [Resolve a vulnerability with a merge request](#resolve-a-vulnerability-with-a-merge-request).
- [Resolve a vulnerability manually](#resolve-a-vulnerability-manually).
-![Create merge request from vulnerability](img/create_mr_from_vulnerability_v13_4.png)
+![Create merge request from vulnerability](img/create_mr_from_vulnerability_v13_4_updated.png)
### Resolve a vulnerability with a merge request
diff --git a/doc/user/application_security/vulnerability_report/index.md b/doc/user/application_security/vulnerability_report/index.md
index 24ed318e688..e71aab5839e 100644
--- a/doc/user/application_security/vulnerability_report/index.md
+++ b/doc/user/application_security/vulnerability_report/index.md
@@ -11,7 +11,8 @@ The Vulnerability Report provides information about vulnerabilities from scans o
cumulative results of all successful jobs, regardless of whether the pipeline was successful. The scan results from a
pipeline are only ingested after all the jobs in the pipeline complete.
-The report is available for users with the [correct role](../../permissions.md) on projects, groups, and the Security Center.
+<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
+For an overview, see [Vulnerability Management](https://www.youtube.com/watch?v=8SJHz6BCgXM).
At all levels, the Vulnerability Report contains:
@@ -19,8 +20,11 @@ At all levels, the Vulnerability Report contains:
- Filters for common vulnerability attributes.
- Details of each vulnerability, presented in tabular layout.
-<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
-For an overview, see [Vulnerability Management](https://www.youtube.com/watch?v=8SJHz6BCgXM).
+At the project level, the Vulnerability Report also contains:
+
+- A time stamp showing when it was updated, including a link to the latest pipeline.
+- The number of failures that occurred in the most recent pipeline. Select the failure
+ notification to view the **Failed jobs** tab of the pipeline's page.
The **Activity** column contains icons to indicate the activity, if any, taken on the vulnerability
in that row:
@@ -38,56 +42,38 @@ status of a Jira issue is not shown in the GitLab UI.
![Example project-level Vulnerability Report](img/project_level_vulnerability_report_v14_5.png)
-## Project-level Vulnerability Report
-
-At the project level, the Vulnerability Report also contains:
-
-- A time stamp showing when it was updated, including a link to the latest pipeline.
-- The number of failures that occurred in the most recent pipeline. Select the failure
- notification to view the **Failed jobs** tab of the pipeline's page.
-
When vulnerabilities originate from a multi-project pipeline setup,
this page displays the vulnerabilities that originate from the selected project.
-### View the project-level vulnerability report
+## View the vulnerability report
-To view the project-level vulnerability report:
+View the vulnerability report to list all vulnerabilities in the project or group.
-1. On the left sidebar, select **Search or go to** and find your project.
-1. Select **Secure > Vulnerability report**.
+Prerequisites:
-## Vulnerability Report actions
+- You must have at least the Developer role for the project or group.
-From the Vulnerability Report you can:
+To view the vulnerability report:
-- [Filter the list of vulnerabilities](#filter-the-list-of-vulnerabilities).
-- [View more details about a vulnerability](#view-details-of-a-vulnerability).
-- [View vulnerable source location](#view-vulnerable-source-location) (if available).
-- [Change the status of vulnerabilities](#change-status-of-vulnerabilities).
-- [Export details of vulnerabilities](#export-vulnerability-details).
-- [Sort vulnerabilities by date](#sort-vulnerabilities-by-date-detected).
-- [Manually add a vulnerability finding](#manually-add-a-vulnerability-finding).
-- [Grouping vulnerability report](#group-vulnerabilities)
+1. On the left sidebar, select **Search or go to** and find your project or group.
+1. Select **Secure > Vulnerability report**.
## Vulnerability Report filters
You can filter the Vulnerability Report to narrow focus on only vulnerabilities matching specific
criteria.
-The available filters are:
+The filters available at all levels are:
<!-- vale gitlab.SubstitutionWarning = NO -->
-- **Status**: Detected, Confirmed, Dismissed, Resolved. For details on what each status means, see
+- **Status**: Detected, confirmed, dismissed, resolved. For details on what each status means, see
[vulnerability status values](../vulnerabilities/index.md#vulnerability-status-values).
-- **Severity**: Critical, High, Medium, Low, Info, Unknown.
+- **Severity**: Critical, high, medium, low, info, unknown.
- **Tool**: For more details, see [Tool filter](#tool-filter).
-- **Project**: For more details, see [Project filter](#project-filter).
- **Activity**: For more details, see [Activity filter](#activity-filter).
-The filters' criteria are combined to show only vulnerabilities matching all criteria.
-An exception to this behavior is the Activity filter. For more details about how it works, see
-[Activity filter](#activity-filter).
+Additionally, the [project filter](#project-filter) is available at the group level.
<!-- vale gitlab.SubstitutionWarning = YES -->
@@ -106,8 +92,6 @@ After each filter is selected:
### Tool filter
-> The third-party tool filter was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/229661) in GitLab 13.12.
-
The tool filter allows you to focus on vulnerabilities detected by selected tools.
When using the tool filter, you can choose:
@@ -122,23 +106,28 @@ For details of each of the available tools, see [Security scanning tools](../ind
The content of the Project filter depends on the current level:
-- **Security Center**: Only projects you've [added to your personal Security Center](../security_dashboard/index.md#add-projects-to-the-security-center).
+- **Security Center**: Only projects you've [added to your personal Security Center](../security_dashboard/index.md#adding-projects-to-the-security-center).
- **Group level**: All projects in the group.
- **Project level**: Not applicable.
### Activity filter
-The Activity filter behaves differently from the other filters. The selected values form mutually
-exclusive sets to allow for precisely locating the desired vulnerability records. Additionally, not
-all options can be selected in combination.
+The activity filter behaves differently from the other filters. You can select only one value in
+each category.
-Selection behavior when using the Activity filter:
+Selection behavior when using the activity filter:
-- **All**: Vulnerabilities with any Activity status (same as ignoring this filter). Selecting this deselects any other Activity filter options.
-- **No activity**: Only vulnerabilities without either an associated issue or that are no longer detected. Selecting this deselects any other Activity filter options.
-- **With issues**: Only vulnerabilities with one or more associated issues. Does not include vulnerabilities that also are no longer detected.
-- **No longer detected**: Only vulnerabilities that are no longer detected in the latest pipeline scan of the `default` branch. Does not include vulnerabilities with one or more associated issues.
-- **With issues** and **No longer detected**: Only vulnerabilities that have one or more associated issues and also are no longer detected in the latest pipeline scan of the `default` branch.
+- **Activity**
+ - **All activity**: Vulnerabilities with any activity status (same as ignoring this filter). Selecting this deselects all other activity filter options.
+- **Detection**
+ - **Still detected**: Vulnerabilities that are still detected in the latest pipeline scan of the `default` branch.
+ - **No longer detected**: Vulnerabilities that are no longer detected in the latest pipeline scan of the `default` branch.
+- **Issue**
+ - **Has issues**: Vulnerabilities with one or more associated issues.
+ - **Does not have issue**: Vulnerabilities without an associated issue.
+- **Merge request**
+ - **Has merge request**: Vulnerabilities with one or more associated merge requests.
+ - **Does not have merge request**: Vulnerabilities without an associated merge request.
## View details of a vulnerability
@@ -186,7 +175,7 @@ Fields included are:
- Group name
- Project name
-- Scanner type
+- Tool
- Scanner name
- Status
- Vulnerability
@@ -200,6 +189,8 @@ Fields included are:
- Location
- Activity: Returns `true` if the vulnerability is resolved on the default branch, and `false` if not.
- Comments
+- Full Path
+- CVSS Vectors
NOTE:
Full details are available through our
@@ -259,8 +250,8 @@ Group, Project, and Security Center Vulnerability Reports. To filter them, use t
## Group vulnerabilities
-> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/420055) in GitLab 16.4. Disabled by default.
-> - [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/422509) in GitLab 16.5.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/420055) in GitLab 16.4 [with a flag](../../../administration/feature_flags.md) named `vulnerability_report_grouping`. Disabled by default.
+> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/422509) in GitLab 16.6. Feature flag `vulnerability_report_grouping` removed.
To group the Vulnerability Report:
diff --git a/doc/user/clusters/agent/gitops/example_repository_structure.md b/doc/user/clusters/agent/gitops/example_repository_structure.md
index 02eea3300af..52855b9731c 100644
--- a/doc/user/clusters/agent/gitops/example_repository_structure.md
+++ b/doc/user/clusters/agent/gitops/example_repository_structure.md
@@ -96,7 +96,7 @@ You've successfully created a repository with a protected deployment branch!
Next, you'll configure CI/CD to merge changes from the default branch to your deployment branch.
-In the root of `web-app-manifests`, create and push a [`.gitlab-ci.yml`](../../../../ci/yaml/gitlab_ci_yaml.md) file with the following contents:
+In the root of `web-app-manifests`, create and push a [`.gitlab-ci.yml`](../../../../ci/index.md#the-gitlab-ciyml-file) file with the following contents:
```yaml
deploy:
diff --git a/doc/user/clusters/agent/gitops/flux_oci_tutorial.md b/doc/user/clusters/agent/gitops/flux_oci_tutorial.md
index b970c818a72..2c4796adf2b 100644
--- a/doc/user/clusters/agent/gitops/flux_oci_tutorial.md
+++ b/doc/user/clusters/agent/gitops/flux_oci_tutorial.md
@@ -65,7 +65,7 @@ First, create a repository for your Kubernetes manifests:
Next, configure [GitLab CI/CD](../../../../ci/index.md) to package your manifests into an OCI artifact,
and push the artifact to the [GitLab Container Registry](../../../packages/container_registry/index.md):
-1. In the root of `web-app-manifests`, create and push a [`.gitlab-ci.yml`](../../../../ci/yaml/gitlab_ci_yaml.md) file with the following contents:
+1. In the root of `web-app-manifests`, create and push a [`.gitlab-ci.yml`](../../../../ci/index.md#the-gitlab-ciyml-file) file with the following contents:
```yaml
package:
diff --git a/doc/user/clusters/agent/gitops/flux_tutorial.md b/doc/user/clusters/agent/gitops/flux_tutorial.md
index 27724a95291..832f91691e8 100644
--- a/doc/user/clusters/agent/gitops/flux_tutorial.md
+++ b/doc/user/clusters/agent/gitops/flux_tutorial.md
@@ -121,6 +121,7 @@ To install `agentk`:
kind: Secret
metadata:
name: gitlab-agent-token
+ namespace: gitlab
type: Opaque
stringData:
token: "<your-token-here>"
diff --git a/doc/user/clusters/agent/install/index.md b/doc/user/clusters/agent/install/index.md
index d620a9f658c..588be3a1223 100644
--- a/doc/user/clusters/agent/install/index.md
+++ b/doc/user/clusters/agent/install/index.md
@@ -76,7 +76,7 @@ In GitLab 14.10, a [flag](../../../../administration/feature_flags.md) named `ce
Prerequisites:
- For a [GitLab CI/CD workflow](../ci_cd_workflow.md), ensure that
- [GitLab CI/CD is not disabled](../../../../ci/enable_or_disable_ci.md#disable-cicd-in-a-project).
+ [GitLab CI/CD is not disabled](../../../../ci/pipelines/settings.md#disable-gitlab-cicd-pipelines).
You must register an agent before you can install the agent in your cluster. To register an agent:
@@ -220,7 +220,7 @@ The following example projects can help you get started with the agent.
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/340882) in GitLab 14.8, GitLab warns you on the agent's list page to update the agent version installed on your cluster.
-For the best experience, the version of the agent installed in your cluster should match the GitLab major and minor version. The previous minor version is also supported. For example, if your GitLab version is v14.9.4 (major version 14, minor version 9), then versions v14.9.0 and v14.9.1 of the agent are ideal, but any v14.8.x version of the agent is also supported. See [the release page](https://gitlab.com/gitlab-org/cluster-integration/gitlab-agent/-/releases) of the GitLab agent.
+For the best experience, the version of the agent installed in your cluster should match the GitLab major and minor version. The previous and next minor versions are also supported. For example, if your GitLab version is v14.9.4 (major version 14, minor version 9), then versions v14.9.0 and v14.9.1 of the agent are ideal, but any v14.8.x or v14.10.x version of the agent is also supported. See [the release page](https://gitlab.com/gitlab-org/cluster-integration/gitlab-agent/-/releases) of the GitLab agent.
### Update the agent version
diff --git a/doc/user/clusters/agent/user_access.md b/doc/user/clusters/agent/user_access.md
index 21dc249b1d1..b3735770a97 100644
--- a/doc/user/clusters/agent/user_access.md
+++ b/doc/user/clusters/agent/user_access.md
@@ -151,15 +151,66 @@ Prerequisite:
- You have an agent configured with the `user_access` entry.
-To grant Kubernetes API access:
+### Configure local access with the GitLab CLI (recommended)
+
+You can use the [GitLab CLI `glab`](../../../editor_extensions/gitlab_cli/index.md) to create or update
+a Kubernetes configuration file to access the agent Kubernetes API.
+
+Use `glab cluster agent` commands to manage cluster connections:
+
+1. View a list of all the agents associated with your project:
+
+```shell
+glab cluster agent list --repo '<group>/<project>'
+
+# If your current working directory is the Git repository of the project with the agent, you can omit the --repo option:
+glab cluster agent list
+```
+
+1. Use the numerical agent ID presented in the first column of the output to update your `kubeconfig`:
+
+```shell
+glab cluster agent update-kubeconfig --repo '<group>/<project>' --agent '<agent-id>' --use-context
+```
+
+1. Verify the update with `kubectl` or your preferred Kubernetes tooling:
+
+```shell
+kubectl get nodes
+```
+
+The `update-kubeconfig` command sets `glab cluster agent get-token` as a
+[credential plugin](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#client-go-credential-plugins)
+for Kubernetes tools to retrieve a token. The `get-token` command creates and
+returns a personal access token that is valid until the end of the current day.
+Kubernetes tools cache the token until it expires, the API returns an authorization error, or the process exits. Expect all subsequent calls to your Kubernetes tooling to create a new token.
+
+The `glab cluster agent update-kubeconfig` command supports a number of command line flags. You can view all supported flags with `glab cluster agent update-kubeconfig --help`.
+
+Some examples:
+
+```shell
+# When the current working directory is the Git repository where the agent is registered the --repo / -R flag can be omitted
+glab cluster agent update-kubeconfig --agent '<agent-id>'
+
+# When the --use-context option is specified the `current-context` of the kubeconfig file is changed to the agent context
+glab cluster agent update-kubeconfig --agent '<agent-id>' --use-context
+
+# The --kubeconfig flag can be used to specify an alternative kubeconfig path
+glab cluster agent update-kubeconfig --agent '<agent-id>' --kubeconfig ~/gitlab.kubeconfig
+```
+
+### Configure local access manually using a personal access token
+
+You can configure access to a Kubernetes cluster using a long-lived personal access token:
1. On the left sidebar, select **Search or go to** and find your project.
1. Select **Operate > Kubernetes clusters** and retrieve the numerical ID of the agent you want to access. You need the ID to construct the full API token.
1. Create a [personal access token](../../profile/personal_access_tokens.md) with the `k8s_proxy` scope. You need the access token to construct the full API token.
-1. Construct `kube config` entries to access the cluster:
- 1. Make sure that the proper `kube config` is selected.
+1. Construct `kubeconfig` entries to access the cluster:
+ 1. Make sure that the proper `kubeconfig` is selected.
For example, you can set the `KUBECONFIG` environment variable.
- 1. Add the GitLab KAS proxy cluster to the `kube config`:
+ 1. Add the GitLab KAS proxy cluster to the `kubeconfig`:
```shell
kubectl config set-cluster <cluster_name> --server "https://kas.gitlab.com/k8s-proxy"
diff --git a/doc/user/clusters/agent/vulnerabilities.md b/doc/user/clusters/agent/vulnerabilities.md
index a2dc50e43d7..e57551fc8c1 100644
--- a/doc/user/clusters/agent/vulnerabilities.md
+++ b/doc/user/clusters/agent/vulnerabilities.md
@@ -20,7 +20,7 @@ If both `agent config` and `scan execution policies` are configured, the configu
### Enable via agent configuration
-To enable scanning of all images within your Kubernetes cluster via the agent configuration, add a `container_scanning` configuration block to your agent
+To enable scanning of images within your Kubernetes cluster via the agent configuration, add a `container_scanning` configuration block to your agent
configuration with a `cadence` field containing a [CRON expression](https://en.wikipedia.org/wiki/Cron) for when the scans are run.
```yaml
@@ -39,9 +39,9 @@ Other elements of the [CRON syntax](https://docs.oracle.com/cd/E12058_01/doc/doc
NOTE:
The CRON expression is evaluated in [UTC](https://www.timeanddate.com/worldclock/timezone/utc) using the system-time of the Kubernetes-agent pod.
-By default, operational container scanning attempts to scan the workloads in all
-namespaces for vulnerabilities. You can set the `vulnerability_report` block with the `namespaces`
-field which can be used to restrict which namespaces are scanned. For example,
+By default, operational container scanning does not scan any workloads for vulnerabilities.
+You can set the `vulnerability_report` block with the `namespaces`
+field which can be used to select which namespaces are scanned. For example,
if you would like to scan only the `default`, `kube-system` namespaces, you can use this configuration:
```yaml
@@ -112,13 +112,15 @@ You can customize it with a `resource_requirements` field.
container_scanning:
resource_requirements:
requests:
- cpu: 200m
+ cpu: '0.2'
memory: 200Mi
limits:
- cpu: 700m
+ cpu: '0.7'
memory: 700Mi
```
+When using a fractional value for CPU, format the value as a string.
+
NOTE:
Resource requirements can only be set up using the agent configuration. If you enabled `Operational Container Scanning` through `scan execution policies`, you would need to define the resource requirements within the agent configuration file.
@@ -143,3 +145,10 @@ You must have at least the Developer role.
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/415451) in GitLab 16.4.
To scan private images, the scanner relies on the image pull secrets (direct references and from the service account) to pull the image.
+
+## Troubleshooting
+
+### `Error running Trivy scan. Container terminated reason: OOMKilled`
+
+OCS might fail with an OOM error if there are too many resources to be scanned or if the images being scanned are large.
+To resolve this, [configure the resource requirement](#configure-scanner-resource-requirements) to increase the amount of memory available.
diff --git a/doc/user/compliance/compliance_center/index.md b/doc/user/compliance/compliance_center/index.md
index 0e205a29920..4a42a70a7e7 100644
--- a/doc/user/compliance/compliance_center/index.md
+++ b/doc/user/compliance/compliance_center/index.md
@@ -111,9 +111,9 @@ You can sort the compliance report on:
You can filter the compliance violations report on:
-- Project.
-- Date range of merge.
-- Target branch.
+- The project that the violation was found on.
+- The date range of violation.
+- The target branch of the violation.
Select a row to see details of the compliance violation.
@@ -393,6 +393,7 @@ On self-managed GitLab, by default this feature is not available. To make it ava
With compliance frameworks report, you can see all the compliance frameworks in a group. Each row of the report shows:
- Framework name.
+- Associated projects.
The default framework for the group has a **default** badge.
diff --git a/doc/user/compliance/license_list.md b/doc/user/compliance/license_list.md
index f315f319b71..7ad19775509 100644
--- a/doc/user/compliance/license_list.md
+++ b/doc/user/compliance/license_list.md
@@ -16,7 +16,7 @@ For the licenses to appear under the license list, the following
requirements must be met:
1. You must be generating an SBOM file with components from one of our [one of our supported languages](license_scanning_of_cyclonedx_files/index.md#supported-languages-and-package-managers).
-1. If using our [`Dependency-Scanning.gitlab-ci.yml` template](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Jobs/License-Scanning.gitlab-ci.yml) to generate the SBOM file, then your project must use at least one of the [supported languages and package managers](license_scanning_of_cyclonedx_files/index.md#supported-languages-and-package-managers).
+1. If using our [`Dependency-Scanning.gitlab-ci.yml` template](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Jobs/Dependency-Scanning.gitlab-ci.yml) to generate the SBOM file, then your project must use at least one of the [supported languages and package managers](license_scanning_of_cyclonedx_files/index.md#supported-languages-and-package-managers).
Alternatively, licenses will also appear under the license list when using our deprecated [`License-Scanning.gitlab-ci.yml` template](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Jobs/License-Scanning.gitlab-ci.yml) as long as the following requirements are met:
diff --git a/doc/user/compliance/license_scanning_of_cyclonedx_files/index.md b/doc/user/compliance/license_scanning_of_cyclonedx_files/index.md
index 81f7cc61782..5d7a689e610 100644
--- a/doc/user/compliance/license_scanning_of_cyclonedx_files/index.md
+++ b/doc/user/compliance/license_scanning_of_cyclonedx_files/index.md
@@ -22,16 +22,11 @@ Licenses not in the SPDX list are reported as "Unknown". License information can
## Configuration
-Prerequisites:
+To enable License scanning of CycloneDX files:
-- On GitLab self-managed only, enable [Synchronization with the GitLab License Database](../../../administration/settings/security_and_compliance.md#choose-package-registry-metadata-to-sync) in the Admin Area for the GitLab instance. On GitLab SaaS this step has already been completed.
- Enable [Dependency Scanning](../../application_security/dependency_scanning/index.md#enabling-the-analyzer)
and ensure that its prerequisites are met.
-
-From the `.gitlab-ci.yml` file, remove the deprecated line `Jobs/License-Scanning.gitlab-ci.yml`, if
-it's present.
-
-On GitLab self-managed only, you can [choose package registry metadata to sync](../../../administration/settings/security_and_compliance.md#choose-package-registry-metadata-to-sync) in the Admin Area for the GitLab instance.
+- On GitLab self-managed only, you can [choose package registry metadata to synchronize](../../../administration/settings/security_and_compliance.md#choose-package-registry-metadata-to-sync) in the Admin Area for the GitLab instance. For this data synchronization to work, you must allow outbound network traffic from your GitLab instance to the domain `storage.googleapis.com`. If you have limited or no network connectivity then please refer to the documentation section [running in an offline environment](#running-in-an-offline-environment) for further guidance.
## Supported languages and package managers
diff --git a/doc/user/custom_roles.md b/doc/user/custom_roles.md
index a13c45306ad..bbb48724078 100644
--- a/doc/user/custom_roles.md
+++ b/doc/user/custom_roles.md
@@ -13,35 +13,18 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> - Ability to view a vulnerability report [enabled by default](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/123835) in GitLab 16.1.
> - [Feature flag `custom_roles_vulnerability` removed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/124049) in GitLab 16.2.
> - Ability to create and remove a custom role with the UI [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/393235) in GitLab 16.4.
-> - Ability to manage group members [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/17364) in GitLab 16.5 under `admin_group_member` Feature flag.
-> - Ability to manage project access tokens [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/421778) in GitLab 16.5 under `manage_project_access_tokens` Feature flag.
+> - Ability to manage group members [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/17364) in GitLab 16.5.
+> - Ability to manage project access tokens [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/421778) in GitLab 16.5 [with a flag](../administration/feature_flags.md) named `manage_project_access_tokens`.
+> - Ability to archive projects [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/425957) in GitLab 16.6 in [with a flag](../administration/feature_flags.md) named `archive_project`. Disabled by default.
-Custom roles allow group members who are assigned the Owner role to create roles
+Custom roles allow group Owners or instance administrators to create roles
specific to the needs of their organization.
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
For a demo of the custom roles feature, see [[Demo] Ultimate Guest can view code on private repositories via custom role](https://www.youtube.com/watch?v=46cp_-Rtxps).
-The following granular permissions are available. You can add these permissions to any base role, and add them in combination with each other to create a customized role:
-
-- The Guest+1 role, which allows users with the Guest role to view code.
-- In GitLab 16.1 and later, you can create a custom role that can view vulnerability reports and change the status of the vulnerabilities.
-- In GitLab 16.3 and later, you can create a custom role that can view the dependency list.
-- In GitLab 16.4 and later, you can create a custom role that can approve merge requests.
-- In GitLab 16.5 and later, you can create a custom role that can manage group members.
-
You can discuss individual custom role and permission requests in [issue 391760](https://gitlab.com/gitlab-org/gitlab/-/issues/391760).
-When you enable a custom role for a user with the Guest role, that user has
-access to elevated permissions, and therefore:
-
-- Is considered a [billable user](../subscriptions/self_managed/index.md#billable-users) on self-managed GitLab.
-- [Uses a seat](../subscriptions/gitlab_com/index.md#how-seat-usage-is-determined) on GitLab.com.
-
-This does not apply to Guest+1, a Guest custom role that only enables the `read_code`
-permission. Users with that specific custom role are not considered billable users
-and do not use a seat.
-
## Create a custom role
Prerequisites:
@@ -51,9 +34,19 @@ Prerequisites:
- The group must be in the Ultimate tier.
- You must have:
- At least one private project so that you can see the effect of giving a
- user with the Guest role a custom role. The project can be in the group itself
+ user a custom role. The project can be in the group itself
or one of that group's subgroups.
- - A [personal access token with the API scope](profile/personal_access_tokens.md#create-a-personal-access-token).
+ - If you are using the API to create the custom role, a [personal access token with the API scope](profile/personal_access_tokens.md#create-a-personal-access-token).
+
+You create a custom role by selecting [permissions](#available-permissions) to add
+to a base role.
+
+You can select any number of permissions. For example, you can create a custom role
+with the ability to:
+
+- View vulnerability reports.
+- Change the status of vulnerabilities.
+- Approve merge requests.
### GitLab SaaS
@@ -64,7 +57,7 @@ Prerequisite:
1. On the left sidebar, select **Search or go to** and find your group.
1. Select **Settings > Roles and Permissions**.
1. Select **Add new role**.
-1. In **Base role to use as template**, select **Guest**.
+1. In **Base role to use as template**, select an existing non-custom role.
1. In **Role name**, enter the custom role's title.
1. Select the **Permissions** for the new custom role.
1. Select **Create new role**.
@@ -80,30 +73,44 @@ Prerequisite:
1. Select **Settings > Roles and Permissions**.
1. From the top dropdown list, select the group you want to create a custom role in.
1. Select **Add new role**.
-1. In **Base role to use as template**, select **Guest**.
+1. In **Base role to use as template**, select an existing non-custom role.
1. In **Role name**, enter the custom role's title.
1. Select the **Permissions** for the new custom role.
1. Select **Create new role**.
To create a custom role, you can also [use the API](../api/member_roles.md#add-a-member-role-to-a-group).
-### Custom role requirements
+### Available permissions
+
+The following permissions are available. You can add these permissions in any combination
+to a base role to create a custom role.
+
+Some permissions require having other permissions enabled first. For example, administration of vulnerabilities (`admin_vulnerability`) can only be enabled if reading vulnerabilities (`read_vulnerability`) is also enabled.
+
+These requirements are documented in the `Required permission` column in the following table.
-For every ability, a minimal access level is defined. To be able to create a custom role which enables a certain ability, the `member_roles` table record has to have the associated minimal access level. For all abilities, the minimal access level is Guest. Only users who have at least the Guest role can be assigned to a custom role.
+| Permission | Version | Required permission | Description |
+| ------------------------------- | -----------------------| -------------------- | ----------- |
+| `read_code` | GitLab 15.7 and later | Not applicable | View project code. Does not include the ability to pull code. |
+| `read_vulnerability` | GitLab 16.1 and later | Not applicable | View [vulnerability reports](application_security/vulnerability_report/index.md). |
+| `admin_vulnerability` | GitLab 16.1 and later | `read_vulnerability` | Change the [status of vulnerabilities](application_security/vulnerabilities/index.md#vulnerability-status-values). |
+| `read_dependency` | GitLab 16.3 and later | Not applicable | View [project dependencies](application_security/dependency_list/index.md). |
+| `admin_merge_request` | GitLab 16.4 and later | Not applicable | View and approve [merge requests](project/merge_requests/index.md), and view the associated merge request code. <br> Does not allow users to view or change merge request approval rules. |
+| `manage_project_access_tokens` | GitLab 16.5 and later | Not applicable | Create, delete, and list [project access tokens](project/settings/project_access_tokens.md). |
+| `admin_group_member` | GitLab 16.5 and later | Not applicable | Add or remove [group members](group/manage.md). |
+| `archive_project` | GitLab 16.6 and later | Not applicable | Archive and unarchive [projects](project/settings/index.md#archive-a-project). |
-Some roles and abilities require having other abilities enabled. For example, a custom role can only have administration of vulnerabilities (`admin_vulnerability`) enabled if reading vulnerabilities (`read_vulnerability`) is also enabled.
+## Billing and seat usage
-You can see the abilities requirements in the following table.
+When you enable a custom role for a user with the Guest role, that user has
+access to elevated permissions over the base role, and therefore:
-| Ability | Required ability |
-| -- | -- |
-| `read_code` | - |
-| `read_dependency` | - |
-| `read_vulnerability` | - |
-| `admin_merge_request` | - |
-| `admin_vulnerability` | `read_vulnerability` |
-| `admin_group_member` | - |
-| `manage_project_access_tokens` | - |
+- Is considered a [billable user](../subscriptions/self_managed/index.md#billable-users) on self-managed GitLab.
+- [Uses a seat](../subscriptions/gitlab_com/index.md#how-seat-usage-is-determined) on GitLab.com.
+
+This does not apply when the user's custom role only has the `read_code` permission
+enabled. Guest users with that specific permission only are not considered billable users
+and do not use a seat.
## Associate a custom role with an existing group member
@@ -147,14 +154,14 @@ To do this, you can either remove the custom role from all group members with th
### Remove a custom role from a group member
To remove a custom role from a group member, use the [Group and Project Members API endpoint](../api/members.md#edit-a-member-of-a-group-or-project)
-and pass an empty `member_role_id` value.
+and pass an empty `member_role_id` value:
```shell
# to update a project membership
-curl --request PUT --header "Content-Type: application/json" --header "Authorization: Bearer <your_access_token>" --data '{"member_role_id": "", "access_level": 10}' "https://gitlab.example.com/api/v4/projects/<project_id>/members/<user_id>"
+curl --request PUT --header "Content-Type: application/json" --header "Authorization: Bearer <your_access_token>" --data '{"member_role_id": null, "access_level": 10}' "https://gitlab.example.com/api/v4/projects/<project_id>/members/<user_id>"
# to update a group membership
-curl --request PUT --header "Content-Type: application/json" --header "Authorization: Bearer <your_access_token>" --data '{"member_role_id": "", "access_level": 10}' "https://gitlab.example.com/api/v4/groups/<group_id>/members/<user_id>"
+curl --request PUT --header "Content-Type: application/json" --header "Authorization: Bearer <your_access_token>" --data '{"member_role_id": null, "access_level": 10}' "https://gitlab.example.com/api/v4/groups/<group_id>/members/<user_id>"
```
### Remove a group member with a custom role from the group
diff --git a/doc/user/discussions/img/add_internal_note_v15_0.png b/doc/user/discussions/img/add_internal_note_v15_0.png
deleted file mode 100644
index cf052edd5e7..00000000000
--- a/doc/user/discussions/img/add_internal_note_v15_0.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/discussions/img/add_internal_note_v16_6.png b/doc/user/discussions/img/add_internal_note_v16_6.png
new file mode 100644
index 00000000000..0d6b4c05160
--- /dev/null
+++ b/doc/user/discussions/img/add_internal_note_v16_6.png
Binary files differ
diff --git a/doc/user/discussions/img/create_thread_v16_6.png b/doc/user/discussions/img/create_thread_v16_6.png
new file mode 100644
index 00000000000..3e0abb3d589
--- /dev/null
+++ b/doc/user/discussions/img/create_thread_v16_6.png
Binary files differ
diff --git a/doc/user/discussions/img/discussion_comment.png b/doc/user/discussions/img/discussion_comment.png
deleted file mode 100644
index 3fec5962363..00000000000
--- a/doc/user/discussions/img/discussion_comment.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/discussions/img/quickly_assign_commenter_v13_1.png b/doc/user/discussions/img/quickly_assign_commenter_v13_1.png
deleted file mode 100644
index aa8f65ef6c4..00000000000
--- a/doc/user/discussions/img/quickly_assign_commenter_v13_1.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/discussions/img/quickly_assign_commenter_v16_6.png b/doc/user/discussions/img/quickly_assign_commenter_v16_6.png
new file mode 100644
index 00000000000..7d6e54fdfa2
--- /dev/null
+++ b/doc/user/discussions/img/quickly_assign_commenter_v16_6.png
Binary files differ
diff --git a/doc/user/discussions/index.md b/doc/user/discussions/index.md
index ae74b534e02..a3ed888ed53 100644
--- a/doc/user/discussions/index.md
+++ b/doc/user/discussions/index.md
@@ -156,12 +156,12 @@ Prerequisite:
To lock an issue or merge request:
-1. On the right sidebar, next to **Lock issue** or **Lock merge request**, select **Edit**.
+1. On the right sidebar, next to **Lock discussion**, select **Edit**.
1. On the confirmation dialog, select **Lock**.
Notes are added to the page details.
-If an issue or merge request is locked and closed, you cannot reopen it.
+If an issue or merge request is closed with a locked discussion, then you cannot reopen it until the discussion is unlocked.
<!-- Delete when the `moved_mr_sidebar` feature flag is removed -->
If you don't see this action on the right sidebar, your project or instance might have [moved sidebar actions](../project/merge_requests/index.md#move-sidebar-actions) enabled.
@@ -192,7 +192,7 @@ To add an internal note:
1. Below the comment, select the **Make this an internal note** checkbox.
1. Select **Add internal note**.
-![Internal notes](img/add_internal_note_v15_0.png)
+![Internal notes](img/add_internal_note_v16_6.png)
You can also mark an [issue as confidential](../project/issues/confidential_issues.md).
@@ -233,7 +233,7 @@ You can assign an issue to a user who made a comment.
1. In the comment, select the **More Actions** (**{ellipsis_v}**) menu.
1. Select **Assign to commenting user**:
- ![Assign to commenting user](img/quickly_assign_commenter_v13_1.png)
+ ![Assign to commenting user](img/quickly_assign_commenter_v16_6.png)
1. To unassign the commenter, select the button again.
## Create a thread by replying to a standard comment
@@ -272,9 +272,9 @@ To create a thread:
1. From the list, select **Start thread**.
1. Select **Start thread** again.
-A threaded comment is created.
+![Create a thread](img/create_thread_v16_6.png)
-![Thread comment](img/discussion_comment.png)
+A threaded comment is created.
## Resolve a thread
diff --git a/doc/user/feature_flags.md b/doc/user/feature_flags.md
index f665395b103..88928ab6d47 100644
--- a/doc/user/feature_flags.md
+++ b/doc/user/feature_flags.md
@@ -1,6 +1,6 @@
---
stage: none
-group: Development
+group: unassigned
info: "See the Technical Writers assigned to Development Guidelines: https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments-to-development-guidelines"
description: "View a list of all the flags available in the GitLab application."
layout: 'feature_flags'
diff --git a/doc/user/free_push_limit.md b/doc/user/free_push_limit.md
index c0b23720ab1..c1be8287eb1 100644
--- a/doc/user/free_push_limit.md
+++ b/doc/user/free_push_limit.md
@@ -6,9 +6,9 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Free push limit **(FREE SAAS)**
-A 100 MB per-file limit applies when pushing new files to any project in the Free tier.
+A 100 MiB per-file limit applies when pushing new files to any project in the Free tier.
-If a new file that is 100 MB or large is pushed to a project in the Free tier, an error is displayed. For example:
+If a new file that is 100 MiB or large is pushed to a project in the Free tier, an error is displayed. For example:
```shell
Enumerating objects: 3, done.
diff --git a/doc/user/gitlab_duo_chat.md b/doc/user/gitlab_duo_chat.md
new file mode 100644
index 00000000000..ba6cd9b8f21
--- /dev/null
+++ b/doc/user/gitlab_duo_chat.md
@@ -0,0 +1,67 @@
+---
+stage: AI-powered
+group: Duo Chat
+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: index, reference
+---
+
+# Answer questions with GitLab Duo Chat **(ULTIMATE SAAS EXPERIMENT)**
+
+> Introduced in GitLab 16.0 as an [Experiment](../policy/experiment-beta-support.md#experiment).
+
+You can get AI generated support from GitLab Duo Chat about the following topics:
+
+- How to use GitLab.
+- Questions about an issue.
+- Question about an epic.
+- Questions about a code file.
+- Follow-up questions to answers from the chat.
+
+Example questions you might ask:
+
+- `Explain the concept of a 'fork' in a concise manner.`
+- `Provide step-by-step instructions on how to reset a user's password.`
+- `Generate a summary for the issue identified via this link: <link to your issue>`
+- `Generate a concise summary of the description of the current issue.`
+
+The examples above all use data from either the issue or the GitLab documentation. However, you can also ask to generate code, CI/CD configurations, or to explain code. For example:
+
+- `Write a Ruby function that prints 'Hello, World!' when called.`
+- `Develop a JavaScript program that simulates a two-player Tic-Tac-Toe game. Provide both game logic and user interface, if applicable.`
+- `Create a .gitlab-ci.yml configuration file for testing and building a Ruby on Rails application in a GitLab CI/CD pipeline.`
+- `Provide a clear explanation of the given Ruby code: def sum(a, b) a + b end. Describe what this code does and how it works.`
+
+In addition to the provided prompts, feel free to ask follow-up questions to delve deeper into the topic or task at hand. This helps you get more detailed and precise responses tailored to your specific needs, whether it's for further clarification, elaboration, or additional assistance.
+
+- A follow-up to the question `Write a Ruby function that prints 'Hello, World!' when called.` could be:
+ - `Could you also explain how I can call and execute this Ruby function in a typical Ruby environment, such as the command line?`
+
+This is an experimental feature and we're continuously extending the capabilities and reliability of the chat.
+
+## Enable GitLab Duo Chat
+
+To use this feature, at least one group you're a member of must:
+
+- Have the [experiment and beta features setting](group/manage.md#enable-experiment-and-beta-features) enabled.
+
+## Use GitLab Duo Chat
+
+1. In the lower-left corner, select the **Help** icon.
+ The [new left sidebar must be enabled](../tutorials/left_sidebar/index.md).
+1. Select **GitLab Duo Chat**. A drawer opens on the right side of your screen.
+1. Enter your question in the chat input box and press **Enter** or select **Send**. It may take a few seconds for the interactive AI chat to produce an answer.
+1. You can ask a follow-up question.
+1. If you want to ask a new question unrelated to the previous conversation, you may receive better answers if you clear the context by typing `/reset` into the input box and selecting **Send**.
+
+NOTE:
+Only the last 50 messages are retained in the chat history. The chat history expires 3 days after last use.
+
+## Give Feedback
+
+Your feedback is important to us as we continually enhance your GitLab Duo Chat experience:
+
+- **Enhance Your Experience**: Leaving feedback helps us customize the Chat for your needs and improve its performance for everyone.
+- **Privacy Assurance**: Rest assured, we don't collect your prompts. Your privacy is respected, and your interactions remain private.
+
+To give feedback about a specific response, use the feedback buttons in the response message.
+Or, you can add a comment in the [feedback issue](https://gitlab.com/gitlab-org/gitlab/-/issues/415591).
diff --git a/doc/user/group/access_and_permissions.md b/doc/user/group/access_and_permissions.md
index 966945b6b12..53a62a60157 100644
--- a/doc/user/group/access_and_permissions.md
+++ b/doc/user/group/access_and_permissions.md
@@ -118,7 +118,7 @@ To allow runner downloading, add the [outbound runner CIDR ranges](../gitlab_com
> - Support for restricting access to projects in the group [added](https://gitlab.com/gitlab-org/gitlab/-/issues/14004) in GitLab 14.1.2.
> - Support for restricting group memberships to groups with a subset of the allowed email domains [added](https://gitlab.com/gitlab-org/gitlab/-/issues/354791) in GitLab 15.1.1
-You can prevent users with email addresses in specific domains from being added to a group and its projects.
+You can prevent users with email addresses in specific domains from being added to a group and its projects. You can define an email domain allowlist at the top-level namespace only. Subgroups do not offer the ability to define an alternative allowlist.
To restrict group access by domain:
@@ -260,6 +260,13 @@ Group syncing allows LDAP groups to be mapped to GitLab groups. This provides mo
Group links can be created by using either a CN or a filter. To create these group links, go to the group's **Settings > LDAP Synchronization** page. After configuring the link, it may take more than an hour for the users to sync with the GitLab group.
+If a user is a member of two configured LDAP groups for the same GitLab group, they are granted the higher of the roles associated with the two LDAP groups.
+For example:
+
+- User is a member of LDAP groups `Owner` and `Dev`.
+- The GitLab Group is configured with these two LDAP groups.
+- When group sync is completed, the user is granted the Owner role as this is the higher of the two LDAP group roles.
+
For more information on the administration of LDAP and group sync, refer to the [main LDAP documentation](../../administration/auth/ldap/ldap_synchronization.md#group-sync).
NOTE:
diff --git a/doc/user/group/epics/manage_epics.md b/doc/user/group/epics/manage_epics.md
index 5675393441e..a5cc3ad9070 100644
--- a/doc/user/group/epics/manage_epics.md
+++ b/doc/user/group/epics/manage_epics.md
@@ -206,7 +206,7 @@ To view epics in a group:
Whether you can view an epic depends on the [group visibility level](../../public_access.md) and
the epic's [confidentiality status](#make-an-epic-confidential):
-- Public group and a non-confidential epic: You don't have to be a member of the group.
+- Public group and a non-confidential epic: Anyone can view the epic.
- Private group and non-confidential epic: You must have at least the Guest role for the group.
- Confidential epic (regardless of group visibility): You must have at least the Reporter
role for the group.
diff --git a/doc/user/group/import/index.md b/doc/user/group/import/index.md
index e1d5c8e5f0a..24d5ca5b214 100644
--- a/doc/user/group/import/index.md
+++ b/doc/user/group/import/index.md
@@ -240,7 +240,16 @@ To view group import history:
1. On the left sidebar, at the top, select **Create new** (**{plus}**) and **New group**.
1. Select **Import group**.
1. In the upper-right corner, select **History**.
-1. If there are any errors for a particular import, you can see them by selecting **Details**.
+1. If there are any errors for a particular import, select **See failures** to see their details.
+
+### Review results of the import
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/429109) in GitLab 16.6 [with a flag](../../feature_flags.md) named `bulk_import_details_page`. Enabled by default.
+
+To review the results of an import:
+
+1. Go to the [Group import history page](#group-import-history).
+1. To see the details of a failed import, select the **See failures** link on any import with a **Failed** status.
### Migrated group items
@@ -337,7 +346,7 @@ Project items that are migrated to the destination GitLab instance include:
| Projects | [GitLab 14.4](https://gitlab.com/gitlab-org/gitlab/-/issues/267945) |
| Auto DevOps | [GitLab 14.6](https://gitlab.com/gitlab-org/gitlab/-/issues/339410) |
| Badges | [GitLab 14.6](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/75029) |
-| Branches (including protected branches) | [GitLab 14.7](https://gitlab.com/gitlab-org/gitlab/-/issues/339414) |
+| Branches (including protected branches) <sup>1</sup> | [GitLab 14.7](https://gitlab.com/gitlab-org/gitlab/-/issues/339414) |
| CI Pipelines | [GitLab 14.6](https://gitlab.com/gitlab-org/gitlab/-/issues/339407) |
| Commit comments | [GitLab 15.10](https://gitlab.com/gitlab-org/gitlab/-/issues/391601) |
| Designs | [GitLab 15.1](https://gitlab.com/gitlab-org/gitlab/-/issues/339421) |
@@ -361,6 +370,14 @@ Project items that are migrated to the destination GitLab instance include:
| Uploads | [GitLab 14.5](https://gitlab.com/gitlab-org/gitlab/-/issues/339401) |
| Wikis | [GitLab 14.6](https://gitlab.com/gitlab-org/gitlab/-/issues/345923) |
+<html>
+<small>Footnotes:
+ <ol>
+ <li>Imported branches respect the [default branch protection settings](../../project/protected_branches.md) of the destination group, which can cause an unprotected branch to be imported as protected.</li>
+ </ol>
+</small>
+</html>
+
#### Issue-related items
Issue-related project items that are migrated to the destination GitLab instance include:
diff --git a/doc/user/group/index.md b/doc/user/group/index.md
index 484fd8c533b..1a4fa9df305 100644
--- a/doc/user/group/index.md
+++ b/doc/user/group/index.md
@@ -202,7 +202,7 @@ A table displays the member's:
NOTE:
The display of group members' **Source** might be inconsistent.
-For more information, see [issue 414557](https://gitlab.com/gitlab-org/gitlab/-/issues/414557).
+For more information, see [issue 23020](https://gitlab.com/gitlab-org/gitlab/-/issues/23020).
## Filter and sort members in a group
@@ -219,7 +219,7 @@ Filter a group to find members. By default, all members in the group and subgrou
In lists of group members, entries can display the following badges:
- **SAML**, to indicate the member has a [SAML account](saml_sso/index.md) connected to them.
-- **Enterprise**, to indicate that the member is an [enterprise user](../enterprise_user/index.md).
+- **Enterprise**, to indicate that the member of the top-level group is an [enterprise user](../enterprise_user/index.md).
1. On the left sidebar, select **Search or go to** and find your group.
1. Select **Manage > Members**.
@@ -227,7 +227,7 @@ In lists of group members, entries can display the following badges:
- To view members in the group only, select **Membership = Direct**.
- To view members of the group and its subgroups, select **Membership = Inherited**.
- To view members with two-factor authentication enabled or disabled, select **2FA = Enabled** or **Disabled**.
- - [In GitLab 14.0 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/349887), to view GitLab users created by [SAML SSO](saml_sso/index.md) or [SCIM provisioning](saml_sso/scim_setup.md) select **Enterprise = true**.
+ - To view members of the top-level group who are [enterprise users](../enterprise_user/index.md), select **Enterprise = true**.
### Search a group
diff --git a/doc/user/group/manage.md b/doc/user/group/manage.md
index d671b0434b6..48f86ee4f0e 100644
--- a/doc/user/group/manage.md
+++ b/doc/user/group/manage.md
@@ -130,6 +130,11 @@ After sharing the `Frontend` group with the `Engineering` group:
- The **Groups** tab lists the `Engineering` group.
- The **Groups** tab lists a group regardless of whether it is a public or private group.
+- From [GitLab 16.6](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134623),
+ the invited group's name and membership source will be masked unless:
+ - the invited group is public, or
+ - the current user is a member of the invited group, or
+ - the current user is a member of the current group.
- All direct members of the `Engineering` group have access to the `Frontend` group. The least access is granted between the access in the `Engineering` group and the access in the `Frontend` group.
- If `Member1` has the Maintainer role in `Engineering` and `Engineering` is added to `Frontend` with the Developer role, `Member1` has the Developer role in `Frontend`.
- If `Member2` has the Guest role in `Engineering` and `Engineering` is added to `Frontend` with the Developer role, `Member2` has the Guest role in `Frontend`.
@@ -487,29 +492,6 @@ To enable Experiment features for a top-level group:
1. Under **Experiment and Beta features**, select the **Use Experiment and Beta features** checkbox.
1. Select **Save changes**.
-## Enable third-party AI features **(ULTIMATE SAAS)**
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/118222) in GitLab 16.0.
-
-WARNING:
-These AI features use [third-party services](../ai_features.md#data-usage)
-and require transmission of data, including personal data.
-
-All users in the group have third-party AI features enabled by default.
-This setting [cascades to all projects](../project/merge_requests/approvals/settings.md#settings-cascading)
-that belong to the group.
-
-To disable third-party AI features for a group:
-
-1. On the left sidebar, select **Search or go to** and find your group.
-1. Select **Settings > General**.
-1. Expand **Permissions and group features**.
-1. Under **Third-party AI services**, uncheck the **Use third-party AI services** checkbox.
-1. Select **Save changes**.
-
-When Code Suggestions are enabled and disabled, an
-[audit event](../../administration/audit_events.md#view-audit-events) is created.
-
## Group activity analytics **(PREMIUM ALL)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/207164) in GitLab 12.10 as a [Beta feature](../../policy/experiment-beta-support.md#beta).
diff --git a/doc/user/group/reporting/git_abuse_rate_limit.md b/doc/user/group/reporting/git_abuse_rate_limit.md
index 1b14edb04d9..d32524b8f5f 100644
--- a/doc/user/group/reporting/git_abuse_rate_limit.md
+++ b/doc/user/group/reporting/git_abuse_rate_limit.md
@@ -13,7 +13,7 @@ On self-managed GitLab, by default this feature is not available. To make it ava
This is the group-level documentation. For self-managed instances, see the [administration documentation](../../admin_area/reporting/git_abuse_rate_limit.md).
-Git abuse rate limiting is a feature to automatically ban users who download, clone, pull, fetch, or fork more than a specified number of repositories of a group in a given time frame. Banned users cannot access the top-level group or any of its non-public subgroups via HTTP or SSH. The rate limit also applies to users who authenticate with a [personal](../../../user/profile/personal_access_tokens.md) or [group access token](../../../user/group/settings/group_access_tokens.md). Access to unrelated groups is unaffected.
+Git abuse rate limiting is a feature to automatically ban users who download, clone, pull, fetch, or fork more than a specified number of repositories of a group in a given time frame. Banned users cannot access the top-level group or any of its non-public subgroups via HTTP or SSH. The rate limit also applies to users who authenticate with [personal](../../../user/profile/personal_access_tokens.md) or [group access tokens](../../../user/group/settings/group_access_tokens.md), as well as [CI/CD job tokens](../../../ci/jobs/ci_job_token.md). Access to unrelated groups is unaffected.
Git abuse rate limiting does not apply to top-level group owners, [deploy tokens](../../../user/project/deploy_tokens/index.md), or [deploy keys](../../../user/project/deploy_keys/index.md).
diff --git a/doc/user/group/saml_sso/group_sync.md b/doc/user/group/saml_sso/group_sync.md
index c18ccaf9c20..7b10da016b9 100644
--- a/doc/user/group/saml_sso/group_sync.md
+++ b/doc/user/group/saml_sso/group_sync.md
@@ -81,6 +81,8 @@ When SAML is enabled, users with the Maintainer or Owner role
see a new menu item in group **Settings > SAML Group Links**. You can configure one or more **SAML Group Links** to map
a SAML identity provider group name to a GitLab role. This can be done for a top-level group or any subgroup.
+SAML Group Sync only manages a group if that group has one or more SAML group links. If a SAML group link is created then removed, the user remains in the group until they are removed from the group in the identity provider.
+
To link the SAML groups:
1. In **SAML Group Name**, enter the value of the relevant `saml:AttributeValue`. The value entered here must exactly match the value sent in the SAML response. For some IdPs, this may be a group ID or object ID (Azure AD) instead of a friendly group name.
diff --git a/doc/user/group/saml_sso/index.md b/doc/user/group/saml_sso/index.md
index 444afd3442b..70af800b180 100644
--- a/doc/user/group/saml_sso/index.md
+++ b/doc/user/group/saml_sso/index.md
@@ -54,7 +54,8 @@ To set up SSO with Azure as your identity provider:
1. You should set the following attributes:
- **Unique User Identifier (Name identifier)** to `user.objectID`.
- **nameid-format** to `persistent`. For more information, see how to [manage user SAML identity](#manage-user-saml-identity).
- - **Additional claims** to [supported attributes](#user-attributes).
+ - **email** to `user.mail` or similar.
+ - **Additional claims** to [supported attributes](#configure-assertions).
1. Make sure the identity provider is set to have provider-initiated calls
to link existing GitLab accounts.
@@ -98,7 +99,7 @@ To set up Google Workspace as your identity provider:
- For **Last name**: `last_name`.
- For **Name ID format**: `EMAIL`.
- For **NameID**: `Basic Information > Primary email`.
- For more information, see [manage user SAML identity](#manage-user-saml-identity).
+ For more information, see [supported attributes](#configure-assertions).
1. Make sure the identity provider is set to have provider-initiated calls
to link existing GitLab accounts.
@@ -134,6 +135,8 @@ To set up SSO with Okta as your identity provider:
1. Set these values:
- For **Application username (NameID)**: **Custom** `user.getInternalProperty("id")`.
- For **Name ID Format**: `Persistent`. For more information, see [manage user SAML identity](#manage-user-saml-identity).
+ - For **email**: `user.email` or similar.
+ - For additional **Attribute Statements**, see [supported attributes](#configure-assertions).
1. Make sure the identity provider is set to have provider-initiated calls
to link existing GitLab accounts.
@@ -170,10 +173,28 @@ To set up OneLogin as your identity provider:
| **Identity provider single sign-on URL** | **SAML 2.0 Endpoint** |
1. For **NameID**, use `OneLogin ID`. For more information, see [manage user SAML identity](#manage-user-saml-identity).
-
+1. Configure [required and supported attributes](#configure-assertions).
1. Make sure the identity provider is set to have provider-initiated calls
to link existing GitLab accounts.
+### Configure assertions
+
+At minimum, you must configure the following assertions:
+
+1. [NameID](#manage-user-saml-identity).
+1. Email.
+
+Optionally, you can pass user information to GitLab as attributes in the SAML assertion.
+
+- The user's email address can be an **email** or **mail** attribute.
+- The username can be either a **username** or **nickname** attribute. You should specify only
+ one of these.
+
+For more information, see the [attributes available for self-managed GitLab instances](../../../integration/saml.md#configure-assertions).
+
+NOTE:
+Attribute names starting with phrases such as `http://schemas.microsoft.com/ws/2008/06/identity/claims/` are not supported. For more information on configuring required attribute names in the SAML identity provider's settings, see [example group SAML and SCIM configurations](../../../user/group/saml_sso/example_saml_config.md).
+
### Use metadata
To configure some identity providers, you need a GitLab metadata URL.
@@ -253,19 +274,6 @@ When a user tries to sign in with Group SSO, GitLab attempts to find or create a
- Create a new account with another email address.
- Sign-in to their existing account to link the SAML identity.
-### User attributes
-
-You can pass user information to GitLab as attributes in the SAML assertion.
-
-- The user's email address can be an **email** or **mail** attribute.
-- The username can be either a **username** or **nickname** attribute. You should specify only
- one of these.
-
-For more information, see the [attributes available for self-managed GitLab instances](../../../integration/saml.md#configure-assertions).
-
-NOTE:
-Attribute names starting with phrases such as `http://schemas.microsoft.com/ws/2008/06/identity/claims/` are not supported. For more information on configuring required attribute names in the SAML identity provider's settings, see [example group SAML and SCIM configurations](../../../user/group/saml_sso/example_saml_config.md).
-
### Link SAML to your existing GitLab.com account
> **Remember me** checkbox [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/121569) in GitLab 15.7.
diff --git a/doc/user/group/saml_sso/troubleshooting.md b/doc/user/group/saml_sso/troubleshooting.md
index 9d3cc0bef50..527d710058a 100644
--- a/doc/user/group/saml_sso/troubleshooting.md
+++ b/doc/user/group/saml_sso/troubleshooting.md
@@ -222,7 +222,7 @@ to [reset their password](https://gitlab.com/users/password/new) if both:
Users might get an error that states "SAML Name ID and email address do not match your user account. Contact an administrator."
This means:
-- The NameID value sent by SAML does not match the existing SAML identity `extern_uid` value.
+- The NameID value sent by SAML does not match the existing SAML identity `extern_uid` value. Both the NameID and the `extern_uid` are case sensitive. For more information, see [manage user SAML identity](index.md#manage-user-saml-identity).
- Either the SAML response did not include an email address or the email address did not match the user's GitLab email address.
The workaround is that a GitLab group Owner uses the [SAML API](../../../api/saml.md) to update the user's SAML `extern_uid`.
@@ -356,3 +356,21 @@ If you see this message after trying to invite a user to a group:
1. Ensure the user is a [member of the top-level group](../index.md#search-a-group).
Additionally, see [troubleshooting users receiving a 404 after sign in](#users-receive-a-404).
+
+## Message: The SAML response did not contain an email address. Either the SAML identity provider is not configured to send the attribute, or the identity provider directory does not have an email address value for your user
+
+This error appears when the SAML response does not contain the user's email address in an **email** or **mail** attribute as shown in the following example:
+
+```xml
+<Attribute Name="email">
+ <AttributeValue>user@domain.com‹/AttributeValue>
+</Attribute>
+```
+
+Attribute names starting with phrases such as `http://schemas.microsoft.com/ws/2008/06/identity/claims/` like in the following example are not supported. Remove this type of attribute name from the SAML response on the IDP side.
+
+```xml
+<Attribute Name="http://schemas.microsoft.com/ws/2008/06/identity/claims/email">
+ <AttributeValue>user@domain.com‹/AttributeValue>
+</Attribute>
+```
diff --git a/doc/user/group/saml_sso/troubleshooting_scim.md b/doc/user/group/saml_sso/troubleshooting_scim.md
index 703dff16fd5..b31c2eed9df 100644
--- a/doc/user/group/saml_sso/troubleshooting_scim.md
+++ b/doc/user/group/saml_sso/troubleshooting_scim.md
@@ -4,7 +4,7 @@ group: Authentication
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
---
-# Troubleshooting SCIM **(PREMIUM SAAS)**
+# Troubleshooting SCIM **(FREE ALL)**
This section contains possible solutions for problems you might encounter.
@@ -31,6 +31,8 @@ To solve this problem:
1. Have the user sign in directly to GitLab.
1. [Manually link](scim_setup.md#link-scim-and-saml-identities) their account.
+Alternatively, self-managed administrators can [add a user identity](../../../administration/admin_area.md#user-identities).
+
## User cannot sign in
The following are possible solutions for problems where users cannot sign in:
@@ -38,10 +40,11 @@ The following are possible solutions for problems where users cannot sign in:
- Ensure that the user was added to the SCIM app.
- If you receive the `User is not linked to a SAML account` error, the user probably already exists in GitLab. Have the
user follow the [Link SCIM and SAML identities](scim_setup.md#link-scim-and-saml-identities) instructions.
+ Alternatively, self-managed administrators can [add a user identity](../../../administration/admin_area.md#user-identities).
- The **Identity** (`extern_uid`) value stored by GitLab is updated by SCIM whenever `id` or `externalId` changes. Users
- cannot sign in unless the GitLab Identity (`extern_uid`) value matches the `NameId` sent by SAML. This value is also
- used by SCIM to match users on the `id`, and is updated by SCIM whenever the `id` or `externalId` values change.
-- The SCIM `id` and SCIM `externalId` must be configured to the same value as the SAML `NameId`. You can trace SAML responses
+ cannot sign in unless the GitLab identifier (`extern_uid`) of the sign-in method matches the ID sent by the provider, such as
+ the `NameId` sent by SAML. This value is also used by SCIM to match users on the `id`, and is updated by SCIM whenever the `id` or `externalId` values change.
+- On GitLab.com, the SCIM `id` and SCIM `externalId` must be configured to the same value as the SAML `NameId`. You can trace SAML responses
using [debugging tools](troubleshooting.md#saml-debugging-tools), and check any errors against the
[SAML troubleshooting](troubleshooting.md) information.
@@ -94,10 +97,12 @@ When the SCIM app changes:
- Users can follow the instructions in the [Change the SAML app](index.md#change-the-identity-provider) section.
- Administrators of the identity provider can:
- 1. Remove users from the SCIM app, which unlinks all removed users.
+ 1. Remove users from the SCIM app, which:
+ - In GitLab.com, removes all removed users from the group.
+ - In GitLab self-managed, blocks users.
1. Turn on sync for the new SCIM app to [link existing users](scim_setup.md#link-scim-and-saml-identities).
-## SCIM app returns `"User has already been taken","status":409` error
+## SCIM app returns `"User has already been taken","status":409` error **(PREMIUM SAAS)**
Changing the SAML or SCIM configuration or provider can cause the following problems:
@@ -109,7 +114,7 @@ Changing the SAML or SCIM configuration or provider can cause the following prob
the SCIM app.
1. Use the same SCIM API to update the SCIM `extern_uid` for the user on GitLab.com.
-## Search Rails logs for SCIM requests
+## Search Rails logs for SCIM requests **(PREMIUM SAAS)**
GitLab.com administrators can search for SCIM requests in the `api_json.log` using the `pubsub-rails-inf-gprd-*` index in
[Kibana](https://about.gitlab.com/handbook/support/workflows/kibana.html#using-kibana). Use the following filters based
diff --git a/doc/user/group/value_stream_analytics/index.md b/doc/user/group/value_stream_analytics/index.md
index df9986e32e7..2ed01a0ec05 100644
--- a/doc/user/group/value_stream_analytics/index.md
+++ b/doc/user/group/value_stream_analytics/index.md
@@ -125,14 +125,17 @@ To view when the data was most recently updated, in the right corner next to **E
### How value stream analytics measures stages
Value stream analytics measures each stage from its start event to its end event.
+Only items that have reached their end event are included in the stage time calculation.
-For example, a stage might start when a user adds a label to an issue, and ends when they add another label.
-Items aren't included in the stage time calculation if they have not reached the end event.
+By default, blocked issues are not included in the life cycle overview.
+However, you can use custom labels (for example `workflow::blocked`) to track them.
-Value stream analytics allows you to customize your stages based on pre-defined events. To make the
-configuration easier, GitLab provides a pre-defined list of stages that can be used as a template
+You can customize stages in value stream analytics based on pre-defined events.
+To help you with the configuration, GitLab provides a pre-defined list of stages that you can use as a template.
+For example, you can define a stage that starts when you add a label to an issue,
+and ends when you add another label.
-Each pre-defined stages of value stream analytics is further described in the table below.
+The following table gives an overview of the pre-defined stages in value stream analytics.
| Stage | Measurement method |
| ------- | -------------------- |
@@ -156,7 +159,7 @@ If a stage does not include a start and a stop time, its data is not included in
In this example, milestones have been created and CI/CD for testing and setting environments is configured.
- 09:00: Create issue. **Issue** stage starts.
-- 11:00: Add issue to a milestone, start work on the issue, and create a branch locally.
+- 11:00: Add issue to a milestone (or backlog), start work on the issue, and create a branch locally.
**Issue** stage stops and **Plan** stage starts.
- 12:00: Make the first commit.
- 12:30: Make the second commit to the branch that mentions the issue number.
diff --git a/doc/user/img/snippet_clone_button_v13_0.png b/doc/user/img/snippet_clone_button_v13_0.png
deleted file mode 100644
index bf681e7349b..00000000000
--- a/doc/user/img/snippet_clone_button_v13_0.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/img/snippet_intro_v13_11.png b/doc/user/img/snippet_intro_v13_11.png
deleted file mode 100644
index 4b6818341b7..00000000000
--- a/doc/user/img/snippet_intro_v13_11.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/img/snippet_sample_v16_6.png b/doc/user/img/snippet_sample_v16_6.png
new file mode 100644
index 00000000000..035947a2b82
--- /dev/null
+++ b/doc/user/img/snippet_sample_v16_6.png
Binary files differ
diff --git a/doc/user/infrastructure/clusters/connect/new_gke_cluster.md b/doc/user/infrastructure/clusters/connect/new_gke_cluster.md
index 96819860a2f..5412ced3e6d 100644
--- a/doc/user/infrastructure/clusters/connect/new_gke_cluster.md
+++ b/doc/user/infrastructure/clusters/connect/new_gke_cluster.md
@@ -95,7 +95,7 @@ Use CI/CD environment variables to configure your project.
1. On the left sidebar, select **Settings > CI/CD**.
1. Expand **Variables**.
1. Set the variable `BASE64_GOOGLE_CREDENTIALS` to the `base64` encoded JSON file you just created.
-1. Set the variable `TF_VAR_gcp_project` to your GCP `project` name.
+1. Set the variable `TF_VAR_gcp_project` to your GCP `project` ID.
1. Set the variable `TF_VAR_agent_token` to the agent token displayed in the previous task.
1. Set the variable `TF_VAR_kas_address` to the agent server address displayed in the previous task.
@@ -113,6 +113,10 @@ contains other variables that you can override according to your needs:
Refer to the [Google Terraform provider](https://registry.terraform.io/providers/hashicorp/google/latest/docs/guides/provider_reference) and the [Kubernetes Terraform provider](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs) documentation for further resource options.
+## Enable Kubernetes Engine API
+
+From the Google Cloud console, enable the [Kubernetes Engine API](https://console.cloud.google.com/apis/library/container.googleapis.com).
+
## Provision your cluster
After configuring your project, manually trigger the provisioning of your cluster. In GitLab:
diff --git a/doc/user/infrastructure/iac/index.md b/doc/user/infrastructure/iac/index.md
index 1e6c59c2253..65ec84652ef 100644
--- a/doc/user/infrastructure/iac/index.md
+++ b/doc/user/infrastructure/iac/index.md
@@ -85,7 +85,6 @@ To use a Terraform template:
```yaml
variables:
TF_STATE_NAME: default
- TF_CACHE_KEY: default
# If your terraform files are in a subdirectory, set TF_ROOT accordingly. For example:
# TF_ROOT: terraform/production
```
diff --git a/doc/user/infrastructure/iac/mr_integration.md b/doc/user/infrastructure/iac/mr_integration.md
index 24ae3c998f8..8fe639bb453 100644
--- a/doc/user/infrastructure/iac/mr_integration.md
+++ b/doc/user/infrastructure/iac/mr_integration.md
@@ -16,10 +16,13 @@ enabling you to see statistics about the resources that Terraform creates,
modifies, or destroys.
WARNING:
-Like any other job artifact, Terraform Plan data is viewable by anyone with the Guest role for the repository.
-Neither Terraform nor GitLab encrypts the plan file by default. If your Terraform Plan
-includes sensitive data such as passwords, access tokens, or certificates, we strongly
-recommend encrypting plan output or modifying the project visibility settings.
+Like any other job artifact, Terraform plan data is viewable by anyone with the Guest role on the repository.
+Neither Terraform nor GitLab encrypts the plan file by default. If your Terraform `plan.json` or `plan.cache`
+files include sensitive data like passwords, access tokens, or certificates, you should
+encrypt the plan output or modify the project visibility settings. You should also **disable**
+[public pipelines](../../../ci/pipelines/settings.md#change-pipeline-visibility-for-non-project-members-in-public-projects)
+and set the [artifact's public flag to false](../../../ci/yaml/index.md#artifactspublic) (`public: false`).
+This setting ensures artifacts are accessible only to GitLab administrators and project members with at least the Reporter role.
## Configure Terraform report artifacts
diff --git a/doc/user/infrastructure/iac/terraform_state.md b/doc/user/infrastructure/iac/terraform_state.md
index 081e20b158e..876300a7794 100644
--- a/doc/user/infrastructure/iac/terraform_state.md
+++ b/doc/user/infrastructure/iac/terraform_state.md
@@ -54,12 +54,12 @@ Prerequisites:
WARNING:
Like any other job artifact, Terraform plan data is viewable by anyone with the Guest role on the repository.
-Neither Terraform nor GitLab encrypts the plan file by default. If your Terraform plan
-includes sensitive data, like passwords, access tokens, or certificates, you should
-encrypt plan output or modify the project visibility settings. We also strongly recommend that you **disable**
+Neither Terraform nor GitLab encrypts the plan file by default. If your Terraform `plan.json` or `plan.cache`
+files include sensitive data like passwords, access tokens, or certificates, you should
+encrypt the plan output or modify the project visibility settings. You should also **disable**
[public pipelines](../../../ci/pipelines/settings.md#change-pipeline-visibility-for-non-project-members-in-public-projects)
-by setting the artifact's public flag to false (`public: false`). This setting ensures artifacts are
-accessible only to GitLab Administrators and project members with the Reporter role and above.
+and set the [artifact's public flag to false](../../../ci/yaml/index.md#artifactspublic) (`public: false`).
+This setting ensures artifacts are accessible only to GitLab administrators and project members with at least the Reporter role.
To configure GitLab CI/CD as a backend:
diff --git a/doc/user/markdown.md b/doc/user/markdown.md
index 7f097891e92..a06e26c3e82 100644
--- a/doc/user/markdown.md
+++ b/doc/user/markdown.md
@@ -379,7 +379,8 @@ the [Asciidoctor user manual](https://asciidoctor.org/docs/user-manual/#activati
To prevent malicious activity, GitLab renders only the first 50 inline math instances.
The number of math blocks is also limited based on render time. If the limit is exceeded,
-GitLab renders the excess math instances as text.
+GitLab renders the excess math instances as text. Wiki and repository files do not have
+these limits.
Math written between dollar signs with backticks (``$`...`$``) or single dollar signs (`$...$`)
is rendered inline with the text.
diff --git a/doc/user/okrs.md b/doc/user/okrs.md
index 46390cd0275..ca5882da22a 100644
--- a/doc/user/okrs.md
+++ b/doc/user/okrs.md
@@ -399,6 +399,24 @@ To turn off a check-in reminder, enter:
/checkin_reminder never
```
+## Set an objective as a parent
+
+> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/11198) in GitLab 16.6.
+
+Prerequisite:
+
+- You must have at least the Reporter role for the project.
+- The parent objective and child OKR must belong to the same project.
+
+To set an objective as a parent of an OKR:
+
+1. [Open the objective](#view-an-objective) or [key result](#view-a-key-result) that you want to edit.
+1. Next to **Parent**, from the dropdown list, select the parent to add.
+1. Select any area outside the dropdown list.
+
+To remove the parent of the objective or key result,
+next to **Parent**, select the dropdown list and then select **Unassign**.
+
## Confidential OKRs
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/8410) in GitLab 15.3.
diff --git a/doc/user/organization/index.md b/doc/user/organization/index.md
index 2a33543fea5..5a08307cc11 100644
--- a/doc/user/organization/index.md
+++ b/doc/user/organization/index.md
@@ -6,6 +6,13 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Organization
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/409913) in GitLab 16.1 [with a flag](../../administration/feature_flags.md) named `ui_for_organizations`. Disabled by default.
+
+FLAG:
+This feature is not ready for production use.
+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 `ui_for_organizations`.
+On GitLab.com, this feature is not available.
+
DISCLAIMER:
This page contains information related to upcoming products, features, and functionality.
It is important to note that the information presented is for informational purposes only.
@@ -37,6 +44,37 @@ see [epic 9265](https://gitlab.com/groups/gitlab-org/-/epics/9265).
For a video introduction to the new hierarchy concept for groups and projects for epics, see
[Consolidating groups and projects update (August 2021)](https://www.youtube.com/watch?v=fE74lsG_8yM).
+## View organizations
+
+To view the organizations you have access to:
+
+- On the left sidebar, select **Organizations** (**{organization}**).
+
+## Create an organization
+
+1. On the left sidebar, at the top, select **Create new** (**{plus}**) and **New organization**.
+1. In the **Organization name** field, enter a name for the organization.
+1. In the **Organization URL** field, enter a path for the organization.
+1. Select **Create organization**.
+
+## Edit an organization's name
+
+1. On the left sidebar, select **Organizations** (**{organization}**) and find the organization you want to edit.
+1. Select **Settings > General**.
+1. Update the **Organization name** field.
+1. Select **Save changes**.
+
+## Manage groups and projects
+
+1. On the left sidebar, select **Organizations** (**{organization}**) and find the organization you want to manage.
+1. Select **Manage > Groups and projects**.
+1. To switch between groups and projects, use the **Display** filter next to the search box.
+
+## Manage users
+
+1. On the left sidebar, select **Organizations** (**{organization}**) and find the organization you want to manage.
+1. Select **Manage > Users**.
+
## Related topics
- [Organization developer documentation](../../development/organization/index.md)
diff --git a/doc/user/packages/composer_repository/index.md b/doc/user/packages/composer_repository/index.md
index d8662ef6512..6eac299e71f 100644
--- a/doc/user/packages/composer_repository/index.md
+++ b/doc/user/packages/composer_repository/index.md
@@ -225,7 +225,7 @@ To install a package:
Using a CI/CD job token:
```shell
- composer config gitlab-token.<DOMAIN-NAME> gitlab-ci-token ${CI_JOB_TOKEN}
+ composer config -- gitlab-token.<DOMAIN-NAME> gitlab-ci-token "${CI_JOB_TOKEN}"
```
Result in the `auth.json` file:
diff --git a/doc/user/packages/container_registry/index.md b/doc/user/packages/container_registry/index.md
index 1f95d2f9403..786fd0ca658 100644
--- a/doc/user/packages/container_registry/index.md
+++ b/doc/user/packages/container_registry/index.md
@@ -79,7 +79,7 @@ For more information on running container images, see the [Docker documentation]
Your container images must follow this naming convention:
```plaintext
-<registry URL>/<namespace>/<project>/<image>
+<registry server>/<namespace>/<project>[/<optional path>]
```
For example, if your project is `gitlab.example.com/mynamespace/myproject`,
diff --git a/doc/user/packages/container_registry/reduce_container_registry_storage.md b/doc/user/packages/container_registry/reduce_container_registry_storage.md
index 2af16dcc85a..8c4f25af2e1 100644
--- a/doc/user/packages/container_registry/reduce_container_registry_storage.md
+++ b/doc/user/packages/container_registry/reduce_container_registry_storage.md
@@ -15,14 +15,61 @@ if you add a large number of images or tags:
You should delete unnecessary images and tags and set up a [cleanup policy](#cleanup-policy)
to automatically manage your container registry usage.
-## Check Container Registry storage use
+## Check Container Registry storage use **(FREE SAAS)**
The Usage Quotas page (**Settings > Usage Quotas > Storage**) displays storage usage for Packages.
-This page includes the [Container Registry usage](../../usage_quotas.md#container-registry-usage), which is only available on GitLab.com.
Measuring usage is only possible on the new version of the GitLab Container Registry backed by a
metadata database, which is [available on GitLab.com](https://gitlab.com/groups/gitlab-org/-/epics/5523) since GitLab 15.7.
For information on the planned availability for self-managed instances, see [epic 5521](https://gitlab.com/groups/gitlab-org/-/epics/5521).
+## How container registry usage is calculated
+
+Image layers stored in the Container Registry are deduplicated at the root namespace level.
+
+An image is only counted once if:
+
+- You tag the same image more than once in the same repository.
+- You tag the same image across distinct repositories under the same root namespace.
+
+An image layer is only counted once if:
+
+- You share the image layer across multiple images in the same container repository, project, or group.
+- You share the image layer across different repositories.
+
+Only layers that are referenced by tagged images are accounted for. Untagged images and any layers
+referenced exclusively by them are subject to [online garbage collection](../container_registry/delete_container_registry_images.md#garbage-collection).
+Untagged image layers are automatically deleted after 24 hours if they remain unreferenced during that period.
+
+Image layers are stored on the storage backend in the original (usually compressed) format. This
+means that the measured size for any given image layer should match the size displayed on the
+corresponding [image manifest](https://github.com/opencontainers/image-spec/blob/main/manifest.md#example-image-manifest).
+
+Namespace usage is refreshed a few minutes after a tag is pushed or deleted from any container repository under the namespace.
+
+### Delayed refresh
+
+It is not possible to calculate container registry usage
+with maximum precision in real time for extremely large namespaces (about 1% of namespaces).
+To enable maintainers of these namespaces to see their usage, there is a delayed fallback mechanism.
+See [epic 9413](https://gitlab.com/groups/gitlab-org/-/epics/9413) for more details.
+
+If the usage for a namespace cannot be calculated with precision, GitLab falls back to the delayed method.
+In the delayed method, the displayed usage size is the sum of **all** unique image layers
+in the namespace. Untagged image layers are not ignored. As a result,
+the displayed usage size might not change significantly after deleting tags. Instead,
+the size value only changes when:
+
+- An automated [garbage collection process](../container_registry/delete_container_registry_images.md#garbage-collection)
+ runs and deletes untagged image layers. After a user deletes a tag, a garbage collection run
+ is scheduled to start 24 hours later. During that run, images that were previously tagged
+ are analyzed and their layers deleted if not referenced by any other tagged image.
+ If any layers are deleted, the namespace usage is updated.
+- The namespace's registry usage shrinks enough that GitLab can measure it with maximum precision.
+ As usage for namespaces shrinks to be under the [limits](../../../user/usage_quotas.md#namespace-storage-limit),
+ the measurement switches automatically from delayed to precise usage measurement.
+ There is no place in the UI to determine which measurement method is being used,
+ but [issue 386468](https://gitlab.com/gitlab-org/gitlab/-/issues/386468) proposes to improve this.
+
## Cleanup policy
> - [Renamed](https://gitlab.com/gitlab-org/gitlab/-/issues/218737) from "expiration policy" to "cleanup policy" in GitLab 13.2.
diff --git a/doc/user/packages/container_registry/troubleshoot_container_registry.md b/doc/user/packages/container_registry/troubleshoot_container_registry.md
index 13e14dfdeb4..3fb2754eb9c 100644
--- a/doc/user/packages/container_registry/troubleshoot_container_registry.md
+++ b/doc/user/packages/container_registry/troubleshoot_container_registry.md
@@ -128,6 +128,12 @@ time is set to 15 minutes.
If you are using self-managed GitLab, an administrator can
[increase the token duration](../../../administration/packages/container_registry.md#increase-token-duration).
+## `Failed to pull image` messages
+
+You might receive a [`Failed to pull image'](../../../ci/debugging.md#failed-to-pull-image-messages)
+error message when a CI/CD job is unable to pull a container image from a project with a limited
+[CI/CD job token scope](../../../ci/jobs/ci_job_token.md#limit-job-token-scope-for-public-or-internal-projects).
+
## Slow uploads when using `kaniko` to push large images
When you push large images with `kaniko`, you might experience uncharacteristically long delays.
@@ -136,3 +142,24 @@ This is typically a result of [a performance issue with `kaniko` and HTTP/2](htt
The current workaround is to use HTTP/1.1 when pushing with `kaniko`.
To use HTTP/1.1, set the `GODEBUG` environment variable to `"http2client=0"`.
+
+## `docker login` command fails with `access forbidden`
+
+The container registry [returns the GitLab API URL to the Docker client](../../../administration/packages/container_registry.md#architecture-of-gitlab-container-registry)
+to validate credentials. The Docker client uses basic auth, so the request contains
+the `Authorization` header. If the `Authorization` header is missing in the request to the
+`/jwt/auth` endpoint configured in the `token_realm` for the registry configuration,
+you receive an `access forbidden` error message.
+
+For example:
+
+```plaintext
+> docker login gitlab.example.com:4567
+
+Username: user
+Password:
+Error response from daemon: Get "https://gitlab.company.com:4567/v2/": denied: access forbidden
+```
+
+To avoid this error, ensure the `Authorization` header is not stripped from the request.
+For example, a proxy in front of GitLab might be redirecting to the `/jwt/auth` endpoint.
diff --git a/doc/user/packages/generic_packages/index.md b/doc/user/packages/generic_packages/index.md
index 938093f2a27..1416dcde14f 100644
--- a/doc/user/packages/generic_packages/index.md
+++ b/doc/user/packages/generic_packages/index.md
@@ -33,7 +33,7 @@ Prerequisites:
- You must [authenticate with the API](../../../api/rest/index.md#authentication).
If authenticating with a deploy token, it must be configured with the `write_package_registry`
scope. If authenticating with a personal access token or project access token, it must be
- configured with the `api` scope.
+ configured with the `api` scope. Project access tokens must have at least the Developer role.
- You must call this API endpoint serially when attempting to upload multiple files under the
same package name and version. Attempts to concurrently upload multiple files into
a new package name and version may face partial failures with
@@ -142,7 +142,9 @@ If multiple packages have the same name, version, and filename, then the most re
Prerequisites:
-- You need to [authenticate with the API](../../../api/rest/index.md#authentication). If authenticating with a deploy token, it must be configured with the `read_package_registry` and/or `write_package_registry` scope.
+- You need to [authenticate with the API](../../../api/rest/index.md#authentication).
+ - If authenticating with a deploy token, it must be configured with the `read_package_registry` and/or `write_package_registry` scope.
+ - Project access tokens require the `read_api` scope and at least the `Reporter` role.
```plaintext
GET /projects/:id/packages/generic/:package_name/:package_version/:file_name
diff --git a/doc/user/packages/maven_repository/index.md b/doc/user/packages/maven_repository/index.md
index 6765aa2cbb1..c8730c42022 100644
--- a/doc/user/packages/maven_repository/index.md
+++ b/doc/user/packages/maven_repository/index.md
@@ -24,7 +24,7 @@ Supported clients:
### Authenticate to the Package Registry
-You need an token to publish a package. There are different tokens available depending on what you're trying to achieve. For more information, review the [guidance on tokens](../package_registry/index.md#authenticate-with-the-registry).
+You need a token to publish a package. There are different tokens available depending on what you're trying to achieve. For more information, review the [guidance on tokens](../package_registry/index.md#authenticate-with-the-registry).
Create a token and save it to use later in the process.
@@ -32,6 +32,10 @@ Do not use authentication methods other than the methods documented here. Undocu
#### Edit the client configuration
+Update your configuration to authenticate to the Maven repository with HTTP.
+
+##### Custom HTTP header
+
You must add the authentication details to the configuration file
for your client.
@@ -127,6 +131,97 @@ file:
}
```
+::EndTabs
+
+##### Basic HTTP Authentication
+
+You can also use basic HTTP authentication to authenticate to the Maven Package Registry.
+
+::Tabs
+
+:::TabTitle `mvn`
+
+| Token type | Name must be | Token |
+| --------------------- | ---------------------------- | ---------------------------------------------------------------------- |
+| Personal access token | The username of the user | Paste token as-is, or define an environment variable to hold the token |
+| Deploy token | The username of deploy token | Paste token as-is, or define an environment variable to hold the token |
+| CI Job token | `gitlab-ci-token` | `${CI_JOB_TOKEN}` |
+
+Add the following section to your
+[`settings.xml`](https://maven.apache.org/settings.html) file.
+
+```xml
+<settings>
+ <servers>
+ <server>
+ <id>gitlab-maven</id>
+ <username>REPLACE_WITH_NAME</username>
+ <password>REPLACE_WITH_TOKEN</password>
+ <configuration>
+ <authenticationInfo>
+ <userName>REPLACE_WITH_NAME</userName>
+ <password>REPLACE_WITH_TOKEN</password>
+ </authenticationInfo>
+ </configuration>
+ </server>
+ </servers>
+</settings>
+```
+
+:::TabTitle `gradle`
+
+| Token type | Name must be | Token |
+| --------------------- | ---------------------------- | ---------------------------------------------------------------------- |
+| Personal access token | The username of the user | Paste token as-is, or define an environment variable to hold the token |
+| Deploy token | The username of deploy token | Paste token as-is, or define an environment variable to hold the token |
+| CI Job token | `gitlab-ci-token` | `System.getenv("CI_JOB_TOKEN")` |
+
+In [your `GRADLE_USER_HOME` directory](https://docs.gradle.org/current/userguide/directory_layout.html#dir:gradle_user_home),
+create a file `gradle.properties` with the following content:
+
+```properties
+gitLabPrivateToken=REPLACE_WITH_YOUR_TOKEN
+```
+
+Add a `repositories` section to your
+[`build.gradle`](https://docs.gradle.org/current/userguide/tutorial_using_tasks.html).
+
+- In Groovy DSL:
+
+ ```groovy
+ repositories {
+ maven {
+ url "https://gitlab.example.com/api/v4/groups/<group>/-/packages/maven"
+ name "GitLab"
+ credentials(PasswordCredentials) {
+ username = 'REPLACE_WITH_NAME'
+ password = gitLabPrivateToken
+ }
+ authentication {
+ basic(BasicAuthentication)
+ }
+ }
+ }
+ ```
+
+- In Kotlin DSL:
+
+ ```kotlin
+ repositories {
+ maven {
+ url = uri("https://gitlab.example.com/api/v4/groups/<group>/-/packages/maven")
+ name = "GitLab"
+ credentials(BasicAuthentication::class) {
+ username = "REPLACE_WITH_NAME"
+ password = findProperty("gitLabPrivateToken") as String?
+ }
+ authentication {
+ create("basic", BasicAuthentication::class)
+ }
+ }
+ }
+ ```
+
:::TabTitle `sbt`
| Token type | Name must be | Token |
diff --git a/doc/user/packages/npm_registry/index.md b/doc/user/packages/npm_registry/index.md
index 9d789c27d1f..43defb29fd5 100644
--- a/doc/user/packages/npm_registry/index.md
+++ b/doc/user/packages/npm_registry/index.md
@@ -87,6 +87,10 @@ Your package should now publish to the Package Registry.
When publishing by using a CI/CD pipeline, you can use the [predefined variables](../../../ci/variables/predefined_variables.md) `${CI_PROJECT_ID}` and `${CI_JOB_TOKEN}` to authenticate with your project's Package Registry. We use these variables to create a `.npmrc` file [for authentication](#authenticating-via-the-npmrc) during execution of your CI/CD job.
+WARNING:
+When generating the `.npmrc` file, do not specify the port after `${CI_SERVER_HOST}` if it is a default port,
+such as `80` for a URL starting with `http` or `443` for a URL starting with `https`.
+
In the GitLab project containing your `package.json`, edit or create a `.gitlab-ci.yml` file. For example:
```yaml
@@ -98,8 +102,8 @@ stages:
publish-npm:
stage: deploy
script:
- - echo "@scope:registry=https://${CI_SERVER_HOST}:${CI_SERVER_PORT}/api/v4/projects/${CI_PROJECT_ID}/packages/npm/" > .npmrc
- - echo "//${CI_SERVER_HOST}:${CI_SERVER_PORT}/api/v4/projects/${CI_PROJECT_ID}/packages/npm/:_authToken=${CI_JOB_TOKEN}" >> .npmrc
+ - echo "@scope:registry=https://${CI_SERVER_HOST}/api/v4/projects/${CI_PROJECT_ID}/packages/npm/" > .npmrc
+ - echo "//${CI_SERVER_HOST}/api/v4/projects/${CI_PROJECT_ID}/packages/npm/:_authToken=${CI_JOB_TOKEN}" >> .npmrc
- npm publish
```
@@ -265,7 +269,7 @@ npm deprecate @scope/package ""
### Package forwarding to npmjs.com
-When an npm package is not found in the Package Registry, the request is forwarded to [npmjs.com](https://www.npmjs.com/).
+When an npm package is not found in the Package Registry, the request is forwarded to [npmjs.com](https://www.npmjs.com/). The forward is performed by sending an HTTP redirect back to the requesting client.
Administrators can disable this behavior in the [Continuous Integration settings](../../admin_area/settings/continuous_integration.md).
diff --git a/doc/user/packages/nuget_repository/index.md b/doc/user/packages/nuget_repository/index.md
index f5430c5328c..8db79dc6c5f 100644
--- a/doc/user/packages/nuget_repository/index.md
+++ b/doc/user/packages/nuget_repository/index.md
@@ -434,14 +434,19 @@ the existing package is overwritten.
### Do not allow duplicate NuGet packages
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/293748) in GitLab 16.3 [with a flag](../../../administration/feature_flags.md) named `nuget_duplicates_option`. Disabled by default.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/293748) in GitLab 16.3 [with a flag](../../../administration/feature_flags.md) named `nuget_duplicates_option`. Disabled by default.
+> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/419078) in GitLab 16.6. Feature flag `nuget_duplicates_option` removed.
-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 `nuget_duplicates_option`.
-The feature is not ready for production use.
+To prevent users from publishing duplicate NuGet packages, you can use the [GraphQl API](../../../api/graphql/reference/index.md#packagesettings) or the UI.
-To prevent users from publishing duplicate NuGet packages, you can use the [GraphQl API](../../../api/graphql/reference/index.md#packagesettings).
+In the UI:
+
+1. On the left sidebar, select **Search or go to** and find your group.
+1. Select **Settings > Packages and registries**.
+1. In the **NuGet** row of the **Duplicate packages** table, turn off the **Allow duplicates** toggle.
+1. Optional. In the **Exceptions** text box, enter a regular expression that matches the names and versions of packages to allow.
+
+Your changes are automatically saved.
WARNING:
If the .nuspec file isn't located in the root of the package, the package might
diff --git a/doc/user/packages/package_registry/supported_functionality.md b/doc/user/packages/package_registry/supported_functionality.md
index 3e8852da808..eb6b415ee06 100644
--- a/doc/user/packages/package_registry/supported_functionality.md
+++ b/doc/user/packages/package_registry/supported_functionality.md
@@ -160,9 +160,9 @@ The following authentication protocols are supported:
| Package type | Supported auth protocols |
|-------------------------------------------------------|-------------------------------------------------------------|
-| [Maven (with `mvn`)](../maven_repository/index.md) | Headers, Basic auth ([pulling](#pulling-packages) only) (1) |
-| [Maven (with `gradle`)](../maven_repository/index.md) | Headers, Basic auth ([pulling](#pulling-packages) only) (1) |
-| [Maven (with `sbt`)](../maven_repository/index.md) | Basic auth (1) |
+| [Maven (with `mvn`)](../maven_repository/index.md) | Headers, Basic auth |
+| [Maven (with `gradle`)](../maven_repository/index.md) | Headers, Basic auth |
+| [Maven (with `sbt`)](../maven_repository/index.md) | Basic auth ([pulling](#pulling-packages) only) (1) |
| [npm](../npm_registry/index.md) | OAuth |
| [NuGet](../nuget_repository/index.md) | Basic auth |
| [PyPI](../pypi_repository/index.md) | Basic auth |
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index a83ce6a56c6..ab26e490f51 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -195,16 +195,16 @@ The following table lists project permissions available for each role:
| [Repository](project/repository/index.md):<br>Turn on or off protected branch push for developers | | | | ✓ | ✓ |
| [Repository](project/repository/index.md):<br>Remove fork relationship | | | | | ✓ |
| [Repository](project/repository/index.md):<br>Force push to protected branches (3) | | | | | |
-| [Repository](project/repository/index.md):<br>Remove protected branches (3) | | | | | |
+| [Repository](project/repository/index.md):<br>Remove protected branches by using the UI or API | | | | ✓ | ✓ |
| [Requirements Management](project/requirements/index.md):<br>Archive / reopen | | ✓ | ✓ | ✓ | ✓ |
| [Requirements Management](project/requirements/index.md):<br>Create / edit | | ✓ | ✓ | ✓ | ✓ |
| [Requirements Management](project/requirements/index.md):<br>Import / export | | ✓ | ✓ | ✓ | ✓ |
| [Security dashboard](application_security/security_dashboard/index.md):<br>Create issue from vulnerability finding | | | ✓ | ✓ | ✓ |
| [Security dashboard](application_security/security_dashboard/index.md):<br>Create vulnerability from vulnerability finding | | | ✓ | ✓ | ✓ |
-| [Security dashboard](application_security/security_dashboard/index.md):<br>Dismiss vulnerability | | | ✓ | ✓ | ✓ |
-| [Security dashboard](application_security/security_dashboard/index.md):<br>Dismiss vulnerability finding | | | ✓ | ✓ | ✓ |
-| [Security dashboard](application_security/security_dashboard/index.md):<br>Resolve vulnerability | | | ✓ | ✓ | ✓ |
-| [Security dashboard](application_security/security_dashboard/index.md):<br>Revert vulnerability to detected state | | | ✓ | ✓ | ✓ |
+| [Security dashboard](application_security/security_dashboard/index.md):<br>Dismiss vulnerability | | | ✓ (24) | ✓ | ✓ |
+| [Security dashboard](application_security/security_dashboard/index.md):<br>Dismiss vulnerability finding | | | ✓ | ✓ (24) | ✓ |
+| [Security dashboard](application_security/security_dashboard/index.md):<br>Resolve vulnerability | | | ✓ (24) | ✓ | ✓ |
+| [Security dashboard](application_security/security_dashboard/index.md):<br>Revert vulnerability to detected state | | | ✓ (24) | ✓ | ✓ |
| [Security dashboard](application_security/security_dashboard/index.md):<br>Use security dashboard | | | ✓ | ✓ | ✓ |
| [Security dashboard](application_security/security_dashboard/index.md):<br>View vulnerability | | | ✓ | ✓ | ✓ |
| [Security dashboard](application_security/security_dashboard/index.md):<br>View vulnerability findings in [dependency list](application_security/dependency_list/index.md) | | | ✓ | ✓ | ✓ |
@@ -249,6 +249,7 @@ The following table lists project permissions available for each role:
21. Authors of tasks can delete them even if they don't have the Owner role, but they have to have at least the Guest role for the project.
22. You must have permission to [view the epic](group/epics/manage_epics.md#who-can-view-an-epic).
23. In GitLab 15.9 and later, users with the Guest role and an Ultimate license can view private repository content if an administrator (on self-managed) or group owner (on GitLab.com) gives those users permission. The administrator or group owner can create a [custom role](custom_roles.md) through the API and assign that role to the users.
+24. In GitLab 16.4 the ability for `Developers` to change the status of a vulnerability (`admin_vulnerability`) was [deprecated](../update/deprecations.md#deprecate-change-vulnerability-status-from-the-developer-role). The `admin_vulnerability` permission will be removed, by default, from all `Developer` roles in GitLab 17.0.
<!-- markdownlint-enable MD029 -->
diff --git a/doc/user/product_analytics/index.md b/doc/user/product_analytics/index.md
index ca55ab758da..94217f985cf 100644
--- a/doc/user/product_analytics/index.md
+++ b/doc/user/product_analytics/index.md
@@ -32,7 +32,7 @@ Product analytics uses several tools:
- [**Snowplow**](https://docs.snowplow.io/docs) - A developer-first engine for collecting behavioral data, and passing it through to ClickHouse.
- [**ClickHouse**](https://clickhouse.com/docs) - A database suited to store, query, and retrieve analytical data.
-- [**Cube**](https://cube.dev/docs/) - An analytical graphing library that provides an API to run queries against the data stored in Clickhouse.
+- [**Cube**](https://cube.dev/docs/) - An analytical graphing library that provides an API to run queries against the data stored in ClickHouse.
The following diagram illustrates the product analytics flow:
@@ -46,7 +46,7 @@ flowchart TB
B --Pass data through--> C[Snowplow Enricher]
end
subgraph Data warehouse
- C --Transform and enrich data--> D([Clickhouse])
+ C --Transform and enrich data--> D([ClickHouse])
end
subgraph Data visualization with dashboards
E([Dashboards]) --Generated from the YAML definition--> F[Panels/Visualizations]
@@ -101,11 +101,35 @@ Prerequisites:
1. Expand **Configure** and enter the configuration values.
1. Select **Save changes**.
-## Instrument a GitLab project
+## Onboard a GitLab project
+
+Onboarding a GitLab project means preparing it to receive events that are used for product analytics.
+
+To onboard a project:
+
+1. On the left sidebar, select **Search or go to** and find your project.
+1. Select **Analyze > Analytics dashboards**.
+1. Under **Product analytics**, select **Set up**.
+1. Select **Set up product analytics**.
+Your instance is being created, and the project onboarded.
+
+### Onboard an internal project
+
+GitLab team members can enable Product Analytics on their internal projects on GitLab.com (Ultimate) during the experiment phase.
+
+1. Send a message to the Product Analytics team (`#g_analyze_product_analytics`) informing them of the repository to be enabled.
+1. Using ChatOps, enable both the `product_analytics_dashboards` and `combined_analytics_dashboards`:
+
+ ```plaintext
+ /chatops run feature set product_analytics_dashboards true --project=FULLPATH_TO_PROJECT
+ /chatops run feature set combined_analytics_dashboards true --project=FULLPATH_TO_PROJECT
+ ```
+
+## Instrument your application
To instrument code to collect data, use one or more of the existing SDKs:
-- [Browser SDK](https://gitlab.com/gitlab-org/analytics-section/product-analytics/gl-application-sdk-browser)
+- [Browser SDK](instrumentation/browser_sdk.md)
- [Ruby SDK](https://gitlab.com/gitlab-org/analytics-section/product-analytics/gl-application-sdk-rb)
- [Python SDK](https://gitlab.com/gitlab-org/analytics-section/product-analytics/gl-application-sdk-python)
- [Node SDK](https://gitlab.com/gitlab-org/analytics-section/product-analytics/gl-application-sdk-node)
@@ -273,18 +297,24 @@ POST /api/v4/projects/PROJECT_ID/product_analytics/request/load?queryType=multi
If the request is successful, the returned JSON includes an array of rows of results.
-## Onboarding GitLab internal projects
+## View product analytics usage quota
-GitLab team members can enable Product Analytics on their own internal projects on GitLab.com during the experiment phase.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/424153) in GitLab 16.6 with a [flag](../../administration/feature_flags.md) named `product_analytics_usage_quota`. Disabled by default.
-1. Send a message to the Product Analytics team (`#g_analyze_product_analytics`) informing them of the repository to be enabled.
-1. Ensure that the project is within an Ultimate namespace.
-1. Using ChatOps, enable both the `product_analytics_dashboards` and `combined_analytics_dashboards`
+FLAG:
+On self-managed GitLab, by default this feature is not available. To make it available per project or for your entire instance, an administrator can [enable the feature flag](../../administration/feature_flags.md) named `product_analytics_usage_quota`.
+On GitLab.com, this feature is not available.
+This feature is not ready for production use.
- ```plaintext
- /chatops run feature set product_analytics_dashboards true --project=FULLPATH_TO_PROJECT
- /chatops run feature set combined_analytics_dashboards true --project=FULLPATH_TO_PROJECT
- ```
+Product analytics usage quota is calculated from the number of events received from instrumented applications.
+The tab displays the monthly totals for the group, and a breakdown of usage per project. Current month shows events counted to date.
+
+To view product analytics usage quota:
+
+1. On the left sidebar, select **Search or go to** and find your group.
+1. Select **Settings > Usage quota** and select the **Product analytics** tab.
+
+The usage quota excludes projects that are not onboarded with product analytics.
## Troubleshooting
diff --git a/doc/user/product_analytics/instrumentation/browser_sdk.md b/doc/user/product_analytics/instrumentation/browser_sdk.md
new file mode 100644
index 00000000000..f2beafab8e0
--- /dev/null
+++ b/doc/user/product_analytics/instrumentation/browser_sdk.md
@@ -0,0 +1,282 @@
+---
+stage: Analyze
+group: Analytics Instrumentation
+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
+---
+
+# Browser SDK
+
+This SDK is for instrumenting web sites and applications to send data for the GitLab [product analytics functionality](../index.md).
+
+## How to use the Browser SDK
+
+### Using the NPM package
+
+Add the NPM package to your package JSON using your preferred package manager:
+
+::Tabs
+
+:::TabTitle yarn
+
+```shell
+yarn add @gitlab/application-sdk-browser
+```
+
+:::TabTitle npm
+
+```shell
+npm i @gitlab/application-sdk-browser
+```
+
+::EndTabs
+
+Then, for browser usage import the client SDK:
+
+```javascript
+import { glClientSDK } from '@gitlab/application-sdk-browser';
+
+this.glClient = glClientSDK({ appId, host });
+```
+
+### Using the script directly
+
+Add the script to the page and assign the client SDK to `window`:
+
+```html
+<script src="https://unpkg.com/@gitlab/application-sdk-browser/dist/gl-sdk.min.js"></script>
+<script>
+ window.glClient = window.glSDK.glClientSDK({
+ appId: 'YOUR_APP_ID',
+ host: 'YOUR_HOST',
+ });
+</script>
+```
+
+You can use a specific version of the SDK like this:
+
+```html
+<script src="https://unpkg.com/@gitlab/application-sdk-browser@0.2.5/dist/gl-sdk.min.js"></script>
+```
+
+## Browser SDK initialization options
+
+Apart from `appId` and `host`, you can configure the Browser SDK with the following options:
+
+```typescript
+interface GitLabClientSDKOptions {
+ appId: string;
+ host: string;
+ hasCookieConsent?: boolean;
+ respectGlobalPrivacyControl?: boolean;
+ trackerId?: string;
+ pagePingTracking?:
+ | boolean
+ | {
+ minimumVisitLength?: number;
+ heartbeatDelay?: number;
+ };
+ plugins?: AllowedPlugins;
+}
+```
+
+| Option | Description |
+| :---------------------------- | :---------- |
+| `appId` | The ID provided by the GitLab Project Analytics setup guide. This ID ensures your data is sent to your analytics instance. |
+| `host` | The GitLab Project Analytics instance provided by the setup guide. |
+| `hasCookieConsent` | Whether to use cookies to identify unique users and record their full IP address. Set to `false` by default. When `false`, users are considered anonymous users. No cookies or other storage mechanisms are used to identify users. |
+| `respectGlobalPrivacyControl` | Whether to respect the user's [GPC](https://globalprivacycontrol.org/) configuration to permit or refuse tracking. Set to `true` by default. When `false`, events are emitted regardless of user configuration. |
+| `trackerId` | Used to differentiate between multiple trackers running on the same page or application, because each tracker instance can be configured differently to capture different sets of data. This identifier helps ensure that the data sent to the collector is correctly associated with the correct tracker configuration. Default value is `gitlab`. |
+| `pagePingTracking` | Option to track user engagement on your website or application by sending periodic events while a user is actively browsing a page. Page pings provide valuable insight into how users interact with your content, such as how long they spend on a page, which sections they are viewing, and whether they are scrolling. `pagePingTracking` can be boolean or an object. As a boolean, set to `true` it enables page ping with default options, and set to `false` it disables page ping tracking. As an object, it has two options: `minimumVisitLength` (the minimum time that must have elapsed before the first heartbeat) and `heartbeatDelay` (the interval at which the callback is fired). |
+| `plugins` | Specify which plugins to enable or disable. By default all plugins are enabled. |
+
+### Plugins
+
+- `Client Hints`: An alternative to tracking the User Agent, which is particularly useful in browsers that are freezing the User Agent string.
+Enabling this plugin will automatically capture the following context:
+
+ For example,
+ [iglu:org.ietf/http_client_hints/jsonschema/1-0-0](https://github.com/snowplow/iglu-central/blob/master/schemas/org.ietf/http_client_hints/jsonschema/1-0-0)
+ has the following configuration:
+
+ ```json
+ {
+ "isMobile":false,
+ "brands":[
+ {
+ "brand":"Google Chrome",
+ "version":"89"
+ },
+ {
+ "brand":"Chromium",
+ "version":"89"
+ }
+ ]
+ }
+ ```
+
+- `Link Click Tracking`: With this plugin, the tracker adds click event listeners to all link elements. Link clicks are tracked as self-describing events. Each link-click event captures the link's `href` attribute. The event also has fields for the link's ID, classes, and target (where the linked document is opened, such as a new tab or new window).
+
+- `Performance Timing`: It collects performance-related data from a user's browser using the `Navigation Timing API`. This API provides detailed information about the various stages of loading a web page, such as domain lookup, connection time, content download, and rendering times. This plugin helps to gather insights into how well a website performs for users, identify potential performance bottlenecks, and improve the overall user experience.
+
+- `Error Tracking`: It helps to capture and track errors that occur on a website or application. By monitoring these errors, you can gain insights into potential issues with code or third-party libraries, which can help to improve the overall user experience, and maintain the quality of the website or application.
+
+By default all plugins are enabled. You can disable or enable these plugins through the `plugins` object:
+
+```typescript
+const tracker = glClientSDK({
+ ...options,
+ plugins: {
+ clientHints: true,
+ linkTracking: true,
+ performanceTiming: true,
+ errorTracking: true,
+ },
+});
+```
+
+## Methods
+
+### `identify`
+
+Used to associate a user and their attributes with the session and tracking events.
+
+```javascript
+glClient.identify(userId, userAttributes);
+```
+
+| Property | Type | Description |
+| :--------------- | :-------------------------- | :---------------------------------------------------------------------------- |
+| `userId` | `String` | The user identifier your application uses to identify individual users. |
+| `userAttributes` | `Object`/`Null`/`undefined` | The user attributes that need to be added to the session and tracking events. |
+
+### `page`
+
+Used to trigger a pageview event.
+
+```javascript
+glClient.page(eventAttributes);
+```
+
+| Property | Type | Description |
+| :---------------- | :-------------------------- | :---------------------------------------------------------------- |
+| `eventAttributes` | `Object`/`Null`/`undefined` | The event attributes that need to be added to the pageview event. |
+
+The `eventAttributes` object supports the following optional properties:
+
+| Property | Type | Description |
+| :--------------- | :-------------------------- | :---------------------------------------------------------------------------- |
+| `title` | `String` | Override the default page title. |
+| `contextCallback` | `Function` | A callback that fires on the page view. |
+| `context` | `Object` | Add context (additional information) on the page view. |
+| `timestamp` | `timestamp` | Set the true timestamp or overwrite the device-sent timestamp on an event. |
+
+### `track`
+
+Used to trigger a custom event.
+
+```javascript
+glClient.track(eventName, eventAttributes);
+```
+
+| Property | Type | Description |
+| :---------------- | :-------------------------- | :--------------------------------------------------------------- |
+| `eventName` | `String` | The name of the custom event. |
+| `eventAttributes` | `Object`/`Null`/`undefined` | The event attributes that need to be added to the tracked event. |
+
+### `refreshLinkClickTracking`
+
+`enableLinkClickTracking` tracks only clicks on links that exist when the page has loaded. To track new links added to the page after it has been loaded, use `refreshLinkClickTracking`.
+
+```javascript
+glClient.refreshLinkClickTracking();
+```
+
+### `trackError`
+
+NOTE:
+`trackError` is supported on the Browser SDK, but the resulting events are not used or available.
+
+Used to capture errors. This works only when the `errorTracking` plugin is enabled. The [plugin](#plugins) is enabled by default.
+
+```javascript
+glClient.trackError(eventAttributes);
+```
+
+For example, `trackError` can be used in `try...catch` like below:
+
+```javascript
+try {
+ // Call the function that throws an error
+ throwError();
+} catch (error) {
+ glClient.trackError({
+ message: error.message, // "This is a custom error"
+ filename: error.fileName || 'unknown', // The file in which the error occurred (e.g., "index.html")
+ lineno: error.lineNumber || 0, // The line number where the error occurred (e.g., 2)
+ colno: error.columnNumber || 0, // The column number where the error occurred (e.g., 6)
+ error: error, // The Error object itself
+ });
+}
+```
+
+| Property | Type | Description |
+| :---------------- | :------- | :------------------------------------------------------------------------------------------------------------------- |
+| `eventAttributes` | `Object` | The event attributes that need to be added to the tracked event. `message` is a mandatory key in `eventAttributes`. |
+
+### `addCookieConsent`
+
+`addCookieConsent` is used to allow tracking of user identifiers via cookies. By default `hasCookieConsent` is false, and no user identifiers are passed. To enable tracking of user identifiers, call the `addCookieConsent` method. This step is not needed if you intialized the Browser SDK with `hasCookieConsent` set to true.
+
+```javascript
+glClient.addCookieConsent();
+```
+
+### `setCustomUrl`
+
+Used to set a custom URL for tracking.
+
+```javascript
+glClient.setCustomUrl(url);
+```
+
+| Property | Type | Description |
+| :------- | :------- | :------------------------------------------------ |
+| `url` | `String` | The custom URL that you want to set for tracking. |
+
+### `setReferrerUrl`
+
+Used to set a referrer URL for tracking.
+
+```javascript
+glClient.setReferrerUrl(url);
+```
+
+| Property | Type | Description |
+| :------- | :------- | :-------------------------------------------------- |
+| `url` | `String` | The referrer URL that you want to set for tracking. |
+
+### `setDocumentTitle`
+
+Used to override the document title.
+
+```javascript
+glClient.setDocumentTitle(title);
+```
+
+| Property | Type | Description |
+| :------- | :------- | :--------------------------------- |
+| `title` | `String` | The document title you want to set. |
+
+## Contribute
+
+If you would like to contribute to Browser SDK, follow the [contributing guide](https://gitlab.com/gitlab-org/analytics-section/product-analytics/gl-application-sdk-js/-/blob/main/docs/Contributing.md).
+
+## Troubleshooting
+
+If the Browser SDK is not sending events, or behaving in an unexpected way, take the following actions:
+
+1. Verify that the `appId` and host values in the options object are correct.
+1. Check if any browser privacy settings, extensions, or ad blockers are interfering with the Browser SDK.
+
+For more information and assistance, see the [Snowplow documentation](https://docs.snowplow.io/docs/collecting-data/collecting-from-own-applications/javascript-trackers/browser-tracker/browser-tracker-v3-reference/)
+or contact the [Analytics Instrumentation team](https://about.gitlab.com/handbook/engineering/development/analytics/analytics-instrumentation/#team-members).
diff --git a/doc/user/product_analytics/instrumentation/index.md b/doc/user/product_analytics/instrumentation/index.md
new file mode 100644
index 00000000000..f909a01ff59
--- /dev/null
+++ b/doc/user/product_analytics/instrumentation/index.md
@@ -0,0 +1,15 @@
+---
+stage: Analyze
+group: Analytics Instrumentation
+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
+---
+
+# Instrumentation
+
+To instrument an application to send events to GitLab product analytics you can use one of the following language and platform specific tracking SDKs:
+
+- [Browser SDK](browser_sdk.md)
+- [Ruby SDK](https://gitlab.com/gitlab-org/analytics-section/product-analytics/gl-application-sdk-rb)
+- [Python SDK](https://gitlab.com/gitlab-org/analytics-section/product-analytics/gl-application-sdk-python)
+- [Node SDK](https://gitlab.com/gitlab-org/analytics-section/product-analytics/gl-application-sdk-node)
+- [.NET SDK](https://gitlab.com/gitlab-org/analytics-section/product-analytics/gl-application-sdk-dotnet)
diff --git a/doc/user/profile/account/delete_account.md b/doc/user/profile/account/delete_account.md
index d41eee911f9..70c12cbcf00 100644
--- a/doc/user/profile/account/delete_account.md
+++ b/doc/user/profile/account/delete_account.md
@@ -54,10 +54,9 @@ Using the **Delete user and contributions** option may result in removing more d
When deleting users, you can either:
-- Delete just the user. Not all associated records are deleted with the user. Instead of being deleted, these records
- are moved to a system-wide user with the username Ghost User. The Ghost User's purpose is to act as a container for
- such records. Any commits made by a deleted user still display the username of the original user.
- The user's personal projects are deleted, not moved to the Ghost User.
+- Delete just the user, but move contributions to a system-wide "Ghost User":
+ - The `@ghost` acts as a container for all deleted users' contributions.
+ - The user's profile and personal projects are deleted, instead of moved to the Ghost User.
- Delete the user and their contributions, including:
- Abuse reports.
- Emoji reactions.
@@ -74,6 +73,9 @@ When deleting users, you can either:
[merge requests](../../project/merge_requests/index.md)
and [snippets](../../snippets.md).
+In both cases, commits retain [user information](https://git-scm.com/book/en/v2/Git-Internals-Git-Objects#_git_commit_objects)
+and therefore data integrity within a [Git repository](../../project/repository/index.md).
+
An alternative to deleting is [blocking a user](../../../administration/moderate_users.md#block-a-user).
When a user is deleted from an [abuse report](../../../administration/review_abuse_reports.md) or spam log, these associated
diff --git a/doc/user/profile/account/two_factor_authentication.md b/doc/user/profile/account/two_factor_authentication.md
index d1f1d28663e..d26f2193124 100644
--- a/doc/user/profile/account/two_factor_authentication.md
+++ b/doc/user/profile/account/two_factor_authentication.md
@@ -544,3 +544,9 @@ generates the codes. For example:
1. Select General.
1. Select Date & Time.
1. Enable Set Automatically. If it's already enabled, disable it, wait a few seconds, and re-enable.
+
+### Error: "Permission denied (publickey)" when regenerating recovery codes
+
+If you receive a `Permission denied (publickey)` error when attempting to [generate new recovery codes using an SSH key](#generate-new-recovery-codes-using-ssh)
+and you are using a non-default SSH key pair file path,
+you might need to [manually register your private SSH key](../../ssh.md#configure-ssh-to-point-to-a-different-directory) using `ssh-agent`.
diff --git a/doc/user/profile/comment_templates.md b/doc/user/profile/comment_templates.md
index 50df5f8fdb4..98fabdb0a35 100644
--- a/doc/user/profile/comment_templates.md
+++ b/doc/user/profile/comment_templates.md
@@ -10,10 +10,7 @@ type: howto
> - GraphQL support [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/352956) in GitLab 14.9 [with a flag](../../administration/feature_flags.md) named `saved_replies`. Disabled by default.
> - User interface [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/113232) in GitLab 15.10 [with a flag](../../administration/feature_flags.md) named `saved_replies`. Disabled by default. Enabled for GitLab team members only.
> - [Enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/119468) in GitLab 16.0.
-
-FLAG:
-On self-managed GitLab, by default this feature is available. To hide the feature, an administrator can [disable the feature flag](../../administration/feature_flags.md) named `saved_replies`.
-On GitLab.com, this feature is available.
+> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/123363) in GitLab 16.6.
With comment templates, create and reuse text for any text area in:
@@ -25,7 +22,7 @@ With comment templates, create and reuse text for any text area in:
Comment templates can be small, like approving a merge request and unassigning yourself from it,
or large, like chunks of boilerplate text you use frequently:
-![Comment templates dropdown list](img/saved_replies_dropdown_v16_0.png)
+![Comment templates dropdown list](img/comment_template_v16_6.png)
## Use comment templates in a text area
@@ -65,4 +62,4 @@ To edit or delete a previously comment template:
1. On the left sidebar, select **Comment templates** (**{comment-lines}**).
1. Scroll to **My comment templates**, and identify the comment template you want to edit.
1. To edit, select **Edit** (**{pencil}**).
-1. To delete, select **Delete** (**{remove}**), then select **Delete** again from the modal window.
+1. To delete, select **Delete** (**{remove}**), then select **Delete** again on the dialog.
diff --git a/doc/user/profile/img/comment_template_v16_6.png b/doc/user/profile/img/comment_template_v16_6.png
new file mode 100644
index 00000000000..7990ca604ce
--- /dev/null
+++ b/doc/user/profile/img/comment_template_v16_6.png
Binary files differ
diff --git a/doc/user/profile/img/saved_replies_dropdown_v16_0.png b/doc/user/profile/img/saved_replies_dropdown_v16_0.png
deleted file mode 100644
index 4608484a496..00000000000
--- a/doc/user/profile/img/saved_replies_dropdown_v16_0.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/profile/index.md b/doc/user/profile/index.md
index 6536a992292..64fa5d7b448 100644
--- a/doc/user/profile/index.md
+++ b/doc/user/profile/index.md
@@ -62,6 +62,10 @@ To add new email to your account:
1. Select **Add email address**.
1. Verify your email address with the verification email received.
+NOTE:
+[Making your email non-public](#set-your-public-email) does not prevent it from being used for commit matching,
+[project imports](../project/import/index.md), and [group migrations](../group/import/index.md).
+
## Make your user profile page private
You can make your user profile visible to only you and GitLab administrators.
@@ -128,6 +132,8 @@ to match your username.
## Add external accounts to your user profile page
+> Mastodon user account [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132892) in 16.6 [with a flag](../feature_flags.md) named `mastodon_social_ui`. Disabled by default. This feature is in [Beta](../../policy/experiment-beta-support.md#beta).
+
You can add links to certain other external accounts you might have, like Skype and Twitter.
They can help other users connect with you on other platforms.
@@ -138,6 +144,7 @@ To add links to other accounts:
1. In the **Main settings** section, add your:
- Discord [user ID](https://support.discord.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID-).
- LinkedIn profile name.
+ - Mastodon username.
- Skype username.
- Twitter @username.
diff --git a/doc/user/profile/notifications.md b/doc/user/profile/notifications.md
index 706065d4693..8d34055d42c 100644
--- a/doc/user/profile/notifications.md
+++ b/doc/user/profile/notifications.md
@@ -9,6 +9,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> - Enhanced email styling [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/78604) in GitLab 14.9 [with a feature flag](../../administration/feature_flags.md) named `enhanced_notify_css`. Disabled by default.
> - Enhanced email styling [enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/355907) in GitLab 14.9.
> - Enhanced email styling [enabled on self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/355907) in GitLab 15.0.
+> - Product marketing emails [removed](https://gitlab.com/gitlab-org/gitlab/-/issues/418137) in GitLab 16.6.
Stay informed about what's happening in GitLab with email notifications.
You can receive updates about activity in issues, merge requests, epics, and designs.
@@ -84,8 +85,6 @@ different values for a project or a group.
- **Notification email**: the email address your notifications are sent to.
Defaults to your primary email address.
-- **Receive product marketing emails**: select this checkbox to receive
- [periodic emails](#opt-out-of-product-marketing-emails) about GitLab features.
- **Global notification level**: the default [notification level](#notification-levels)
which applies to all your notifications.
- **Receive notifications about your own activity**: select this checkbox to receive
@@ -145,32 +144,6 @@ Or:
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
To learn how to be notified when a new release is available, watch [Notification for releases](https://www.youtube.com/watch?v=qyeNkGgqmH4).
-### Opt out of product marketing emails
-
-You can receive emails that teach you about various GitLab features.
-These emails are enabled by default.
-
-To opt out:
-
-1. On the left sidebar, select your avatar.
-1. Select **Preferences**.
-1. On the left sidebar, select **Notifications**.
-1. Clear the **Receive product marketing emails** checkbox.
- Edited settings are automatically saved and enabled.
-
-Disabling these emails does not disable all emails.
-Learn how to [opt out of all emails from GitLab](#opt-out-of-all-gitlab-emails).
-
-#### Self-managed product marketing emails **(FREE SELF)**
-
-The self-managed installation generates and automatically sends these emails based on user actions.
-Turning this on does not cause your GitLab instance or your company to send any personal information to
-GitLab Inc.
-
-An instance administrator can configure this setting for all users. If you choose to opt out, your
-setting overrides the instance-wide setting, even when an administrator later enables these emails
-for all users.
-
## Notification events
Users are notified of the following events:
@@ -348,7 +321,6 @@ If you no longer wish to receive any email notifications:
1. On the left sidebar, select your avatar.
1. Select **Preferences**.
1. On the left sidebar, select **Notifications**.
-1. Clear the **Receive product marketing emails** checkbox.
1. Set your **Global notification level** to **Disabled**.
1. Clear the **Receive notifications about your own activity** checkbox.
1. If you belong to any groups or projects, set their notification setting to **Global** or
diff --git a/doc/user/profile/personal_access_tokens.md b/doc/user/profile/personal_access_tokens.md
index 9135a142612..a953a878cc9 100644
--- a/doc/user/profile/personal_access_tokens.md
+++ b/doc/user/profile/personal_access_tokens.md
@@ -137,6 +137,42 @@ Personal access tokens expire on the date you define, at midnight, 00:00 AM UTC.
- In GitLab Ultimate, administrators can
[limit the allowable lifetime of access tokens](../../administration/settings/account_and_limit_settings.md#limit-the-lifetime-of-access-tokens). If not set, the maximum allowable lifetime of a personal access token is 365 days.
- In GitLab Free and Premium, the maximum allowable lifetime of a personal access token is 365 days.
+- If you do not set an expiry date when creating a personal access token, the expiry date is set to the
+ [maximum allowed lifetime for the token](../../administration/settings/account_and_limit_settings.md#limit-the-lifetime-of-access-tokens).
+ If the maximum allowed lifetime is not set, the default expiry date is 365 days from the date of creation.
+
+### Service Accounts
+
+You can [create a personal access token for a service account](../../api/groups.md#create-personal-access-token-for-service-account-user) with no expiry date.
+
+NOTE:
+Allowing personal access tokens for service accounts to be created with no expiry date only affects tokens created after you change this setting. It does not affect existing tokens.
+
+#### GitLab.com
+
+Prerequisite:
+
+- You must have the Owner role in the top-level group.
+
+1. On the left sidebar, select **Search or go to** and find your group.
+1. Select **Settings > Permissions and group features**.
+1. Clear the **Service account token expiration** checkbox.
+
+You can now create personal access tokens for a service account user with no expiry date.
+
+#### Self-managed GitLab
+
+Prerequisite:
+
+- You must be an administrator for your self-managed instance.
+
+1. On the left sidebar, select **Search or go to**.
+1. Select **Admin Area**.
+1. Select **Settings > General**.
+1. Expand **Account and limit**.
+1. Clear the **Service account token expiration** checkbox.
+
+You can now create personal access tokens for a service account user with no expiry date.
## Create a personal access token programmatically **(FREE SELF)**
diff --git a/doc/user/profile/preferences.md b/doc/user/profile/preferences.md
index 170545d851f..34f083e0b48 100644
--- a/doc/user/profile/preferences.md
+++ b/doc/user/profile/preferences.md
@@ -268,6 +268,22 @@ To use exact times on the GitLab UI:
1. Clear the **Use relative times** checkbox.
1. Select **Save changes**.
+### Customize time format
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/15206) in GitLab 16.6.
+
+You can customize the format used to display times of activities on your group and project overview pages and user profiles. You can display times as:
+
+- 12 hour format. For example: `2:34 PM`.
+- 24 hour format. For example: `14:34`.
+
+To customize the time format:
+
+1. On the left sidebar, select your avatar.
+1. Select **Preferences** > **Time preferences**.
+1. In **Time format**, select either the **12-hour** or **24-hour** option.
+1. Select **Save changes**.
+
## User identities in CI job JSON web tokens
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/387537) in GitLab 16.0.
diff --git a/doc/user/profile/service_accounts.md b/doc/user/profile/service_accounts.md
index 6bb96b9c552..8fa0067f150 100644
--- a/doc/user/profile/service_accounts.md
+++ b/doc/user/profile/service_accounts.md
@@ -53,6 +53,8 @@ Prerequisite:
You define the scopes for the service account by [setting the scopes for the personal access token](personal_access_tokens.md#personal-access-token-scopes).
+ Optional. You can [create a personal access token with no expiry date](personal_access_tokens.md#when-personal-access-tokens-expire).
+
The response includes the personal access token value.
1. Make this service account a group or project member by [manually adding the service account user to the group or project](#add-a-service-account-to-subgroup-or-project).
@@ -74,6 +76,8 @@ Prerequisite:
You define the scopes for the service account by [setting the scopes for the personal access token](personal_access_tokens.md#personal-access-token-scopes).
+ Optional. You can [create a personal access token with no expiry date](personal_access_tokens.md#when-personal-access-tokens-expire).
+
The response includes the personal access token value.
1. Make this service account a group or project member by
diff --git a/doc/user/project/codeowners/index.md b/doc/user/project/codeowners/index.md
index d783471f0da..0fa9983e93b 100644
--- a/doc/user/project/codeowners/index.md
+++ b/doc/user/project/codeowners/index.md
@@ -54,6 +54,10 @@ GitLab shows the Code Owners at the top of the page.
## Set up Code Owners
+Prerequisites:
+
+- You must be able to either push to the default branch or create a merge request.
+
1. Create a `CODEOWNERS` file in your [preferred location](#codeowners-file).
1. Define some rules in the file following the [Code Owners syntax reference](reference.md).
Some suggestions:
@@ -145,7 +149,7 @@ of the merge request becomes optional.
Inviting **Subgroup Y** to a parent group of **Project A**
[is not supported](https://gitlab.com/gitlab-org/gitlab/-/issues/288851). To set **Subgroup Y** as
-Code Owners [invite this group directly to the project](#inviting-subgroups-to-projects-in-parent-groups) itself.
+Code Owners, [invite this group directly to the project](#inviting-subgroups-to-projects-in-parent-groups) itself.
NOTE:
For approval to be required, groups as Code Owners must have a direct membership
@@ -196,7 +200,7 @@ You can organize Code Owners by putting them into named sections.
You can use sections for shared directories, so that multiple
teams can be reviewers.
-To add a section to the `CODEOWNERS` file, enter a section name in brackets,
+To add a section to the `CODEOWNERS` file, enter a section name in square brackets,
followed by the files or directories, and users, groups, or subgroups:
```plaintext
@@ -206,7 +210,7 @@ internal/README.md @user2
```
Each Code Owner in the merge request widget is listed under a label.
-The following image shows a **Groups** and **Documentation** section:
+The following image shows **Groups** and **Documentation** sections:
![MR widget - Sectional Code Owners](../img/sectional_code_owners_v13.2.png)
@@ -221,7 +225,9 @@ All paths in that section inherit this default, unless you override the section
default on a specific line.
Default owners are applied when specific owners are not specified for file paths.
-Specific owners defined beside the file path override default owners:
+Specific owners defined beside the file path override default owners.
+
+For example:
```plaintext
[Documentation] @docs-team
@@ -259,8 +265,8 @@ config/db/database-setup.md @docs-team
#### Use regular entries and sections together
-If you set a default Code Owner for a path outside a section, their approval is always required, and
-the entry isn't overridden.
+If you set a default Code Owner for a path **outside a section**, their approval is always required.
+Such entries aren't overridden by sections.
Entries without sections are treated as if they were another, unnamed section:
```plaintext
@@ -287,7 +293,7 @@ In this example:
of the `@general-approvers`,`@docs-team`, and `@database-team` groups.
Compare this behavior to when you use only [default owners for sections](#set-default-owner-for-a-section),
-when specific entries within a section override the section default.
+when specific entries in a section override the section default.
#### Sections with duplicate names
@@ -313,13 +319,14 @@ entries under **Database**. The entries defined under the sections **Documentati
#### Make a Code Owners section optional
-You can designate optional sections in your Code Owners file. Prepend the
-section name with the caret `^` character to treat the entire section as optional.
+You can designate optional sections in your Code Owners file.
Optional sections enable you to designate responsible parties for various parts
of your codebase, but not require approval from them. This approach provides
a more relaxed policy for parts of your project that are frequently updated,
but don't require stringent reviews.
+To treat the entire section as optional, prepend the section name with the caret `^` character.
+
In this example, the `[Go]` section is optional:
```plaintext
@@ -333,7 +340,7 @@ In this example, the `[Go]` section is optional:
*.go @root
```
-The optional Code Owners section displays in merge requests under the **Approval Rules** area:
+The optional Code Owners section displays in merge requests under the description:
![MR widget - Optional Code Owners sections](../img/optional_code_owners_sections_v13_8.png)
@@ -348,18 +355,25 @@ section is marked as optional.
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/335451) in GitLab 15.9.
-You can require multiple approvals for the Code Owners sections under the Approval Rules area in merge requests.
-Append the section name with a number `n` in brackets. This requires `n` approvals from the Code Owners in this section.
+You can require multiple approvals for the Code Owners sections in the Approvals area in merge requests.
+Append the section name with a number `n` in brackets, for example, `[2]` or `[3]`.
+This requires `n` approvals from the Code Owners in this section.
Valid entries for `n` are integers `≥ 1`. `[1]` is optional because it is the default. Invalid values for `n` are treated as `1`.
WARNING:
-[Issue #384881](https://gitlab.com/gitlab-org/gitlab/-/issues/385881) proposes changes
+[Issue 384881](https://gitlab.com/gitlab-org/gitlab/-/issues/385881) proposes changes
to the behavior of this setting. Do not intentionally set invalid values. They may
-become valid in the future, and cause unexpected behavior.
+become valid in the future and cause unexpected behavior.
+
+To require multiple approvals from Code Owners:
-Make sure you enabled `Require approval from code owners` in `Settings > Repository > Protected branches`, otherwise the Code Owner approvals are optional.
+1. On the left sidebar, select **Search or go to** and find your project.
+1. Select **Settings > Repository**.
+1. Expand **Protected branches**.
+1. Next to the default branch, turn on the toggle under **Code owner approval**.
+1. Edit the `CODEOWNERS` file to add a rule for multiple approvals.
-In this example, the `[Documentation]` section requires 2 approvals:
+For example, to require two approvals for the `[Documentation]` section:
```plaintext
[Documentation][2]
@@ -369,7 +383,7 @@ In this example, the `[Documentation]` section requires 2 approvals:
*.rb @dev-team
```
-The `Documentation` Code Owners section under the **Approval Rules** area displays 2 approvals are required:
+The `Documentation` Code Owners section in the Approvals area displays two approvals are required:
![MR widget - Multiple Approval Code Owners sections](../img/multi_approvals_code_owners_sections_v15_9.png)
@@ -377,7 +391,7 @@ The `Documentation` Code Owners section under the **Approval Rules** area displa
Users who are **Allowed to push** can choose to create a merge request
for their changes, or push the changes directly to a branch. If the user
-skips the merge request process, the protected-branch features
+skips the merge request process, the protected branch features
and Code Owner approvals built into merge requests are also skipped.
This permission is often granted to accounts associated with
diff --git a/doc/user/project/deploy_tokens/index.md b/doc/user/project/deploy_tokens/index.md
index 8b7e185508b..351762228fb 100644
--- a/doc/user/project/deploy_tokens/index.md
+++ b/doc/user/project/deploy_tokens/index.md
@@ -88,7 +88,8 @@ Create a deploy token to automate deployment tasks that can run independently of
Prerequisites:
-- You must have at least the Maintainer role for the project or group.
+- To create a group deploy token, you must have the Owner role for the group.
+- To create a project deploy token, you must have at least the Maintainer role for the project.
1. On the left sidebar, select **Search or go to** and find your project or group.
1. Select **Settings > Repository**.
@@ -106,7 +107,8 @@ Revoke a token when it's no longer required.
Prerequisites:
-- You must have at least the Maintainer role for the project or group.
+- To revoke a group deploy token, you must have the Owner role for the group.
+- To revoke a project deploy token, you must have at least the Maintainer role for the project.
To revoke a deploy token:
diff --git a/doc/user/project/import/github.md b/doc/user/project/import/github.md
index 4da756b05ea..f9b94774809 100644
--- a/doc/user/project/import/github.md
+++ b/doc/user/project/import/github.md
@@ -17,11 +17,10 @@ The namespace is a user or group in GitLab, such as `gitlab.com/sidney-jones` or
`gitlab.com/customer-success`. You can use bulk actions in the rails console to move projects to
different namespaces.
-- If you are importing to a self-managed GitLab instance, you can use the [GitHub Rake task](../../../administration/raketasks/github_import.md) instead. The
- Rake task imports projects without the constraints of a [Sidekiq](../../../development/sidekiq/index.md) worker.
-- If you are importing from GitHub Enterprise to GitLab.com, use the
- [GitLab Import API](../../../api/import.md#import-repository-from-github) GitHub endpoint instead. This allows you to provide a different domain to import the project from.
- Using the UI, the GitHub importer always imports from the `github.com` domain.
+If you are importing from GitHub Enterprise to GitLab.com, use the
+[GitLab Import API](../../../api/import.md#import-repository-from-github) GitHub endpoint instead. The API allows you to
+provide a different domain to import the project from. Using the UI, the GitHub importer always imports from the
+`github.com` domain.
When importing projects:
@@ -123,9 +122,10 @@ The [GitHub integration method (above)](#use-the-github-integration) is recommen
If you are not using the GitHub integration, you can still perform an authorization with GitHub to grant GitLab access your repositories:
-1. Go to <https://github.com/settings/tokens/new>
+1. Go to `https://github.com/settings/tokens/new`.
1. Enter a token description.
-1. Select the repository scope.
+1. Select the `repo` scope.
+1. Optional. To [import collaborators](#select-additional-items-to-import), select the `read:org` scope.
1. Select **Generate token**.
1. Copy the token hash.
1. Go back to GitLab and provide the token to the GitHub importer.
diff --git a/doc/user/project/import/jira.md b/doc/user/project/import/jira.md
index b2092082bf8..921669e4b70 100644
--- a/doc/user/project/import/jira.md
+++ b/doc/user/project/import/jira.md
@@ -23,8 +23,8 @@ GitLab imports the following information directly:
Other Jira issue metadata that is not formally mapped to GitLab issue fields is
imported into the GitLab issue's description as plain text.
-Our parser for converting text in Jira issues to GitLab Flavored Markdown is only compatible with
-Jira V3 REST API.
+Text in Jira issues is not parsed to GitLab Flavored Markdown which can result in broken text formatting.
+For more information, see [issue 379104](https://gitlab.com/gitlab-org/gitlab/-/issues/379104).
There is an [epic](https://gitlab.com/groups/gitlab-org/-/epics/2738) tracking the addition of issue assignees, comments, and much more in the future
iterations of the GitLab Jira importer.
diff --git a/doc/user/project/index.md b/doc/user/project/index.md
index b60d87adbd3..9ee1e33ecdd 100644
--- a/doc/user/project/index.md
+++ b/doc/user/project/index.md
@@ -143,19 +143,24 @@ To push your repository and create a project:
1. Push with SSH or HTTPS:
- To push with SSH:
- ```shell
- git push --set-upstream git@gitlab.example.com:namespace/myproject.git master
- ```
+ ```shell
+ # Use this version if your project uses the standard port 22
+ $ git push --set-upstream git@gitlab.example.com:namespace/myproject.git main
+
+ # Use this version if your project requires a non-standard port number
+ $ git push --set-upstream ssh://git@gitlab.example.com:00/namespace/myproject.git main
+ ```
- To push with HTTPS:
- ```shell
- git push --set-upstream https://gitlab.example.com/namespace/myproject.git master
- ```
+ ```shell
+ git push --set-upstream https://gitlab.example.com/namespace/myproject.git master
+ ```
- For `gitlab.example.com`, use the domain name of the machine that hosts your Git repository.
- For `namespace`, use the name of your [namespace](../namespace/index.md).
- For `myproject`, use the name of your project.
+ - If specifying a port, change `00` to your project's required port number.
- Optional. To export existing repository tags, append the `--tags` flag to your `git push` command.
1. Optional. To configure the remote:
diff --git a/doc/user/project/integrations/aws_codepipeline.md b/doc/user/project/integrations/aws_codepipeline.md
index b081544199e..5404101b4f6 100644
--- a/doc/user/project/integrations/aws_codepipeline.md
+++ b/doc/user/project/integrations/aws_codepipeline.md
@@ -1,6 +1,6 @@
---
-stage: Manage
-group: Import and Integrate
+stage: none
+group: unassigned
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/integrations/gitlab_slack_application.md b/doc/user/project/integrations/gitlab_slack_application.md
index 6f70305ce8b..abfd4243e07 100644
--- a/doc/user/project/integrations/gitlab_slack_application.md
+++ b/doc/user/project/integrations/gitlab_slack_application.md
@@ -74,7 +74,8 @@ You can use slash commands to run common GitLab operations. Replace `<project>`
- You must authorize your Slack user on GitLab.com when you run your first slash command.
- You can [create a shorter project alias](#create-a-project-alias-for-slash-commands) for slash commands.
-**For [Slack slash commands](slack_slash_commands.md) on self-managed GitLab, [Mattermost slash commands](mattermost_slash_commands.md), and [ChatOps](../../../ci/chatops/index.md)**, replace `/gitlab` with the slash command trigger name configured for your integration.
+**For [Slack slash commands](slack_slash_commands.md) on self-managed GitLab and [Mattermost slash commands](mattermost_slash_commands.md)**,
+replace `/gitlab` with the slash command trigger name configured for your integration.
The following slash commands are available:
@@ -172,7 +173,11 @@ The following events are available for Slack notifications:
## Troubleshooting
-### GitLab for Slack app does not appear in the list of integrations
+When configuring the GitLab for Slack app on GitLab.com, you might encounter the following issues.
+
+For self-managed GitLab, see [GitLab for Slack app administration](../../../administration/settings/slack_app.md#troubleshooting).
+
+### The app does not appear in the list of integrations
The GitLab for Slack app might not appear in the list of integrations. To have the GitLab for Slack app on your self-managed instance, an administrator must [enable the integration](../../../administration/settings/slack_app.md). On GitLab.com, the GitLab for Slack app is available by default.
@@ -193,9 +198,10 @@ As a workaround, ensure:
- If using a [project alias](#create-a-project-alias-for-slash-commands), the alias is correct.
- The GitLab for Slack app is [enabled for the project](#from-project-integration-settings).
-### Slash commands return `/gitlab failed with the error "dispatch_failed"` in Slack
+### Slash commands return an error in Slack
-Slash commands might return `/gitlab failed with the error "dispatch_failed"` in Slack. To resolve this issue, ensure an administrator has properly configured the [GitLab for Slack app settings](../../../administration/settings/slack_app.md) on your self-managed instance.
+Slash commands might return `/gitlab failed with the error "dispatch_failed"` in Slack.
+To resolve this issue, ensure an administrator has properly configured the [GitLab for Slack app settings](../../../administration/settings/slack_app.md) on your self-managed instance.
### Notifications are not received to a channel
diff --git a/doc/user/project/issues/associate_zoom_meeting.md b/doc/user/project/issues/associate_zoom_meeting.md
index bb8f0ccd186..e112c5ebd0d 100644
--- a/doc/user/project/issues/associate_zoom_meeting.md
+++ b/doc/user/project/issues/associate_zoom_meeting.md
@@ -30,7 +30,7 @@ a system alert notifies you of its successful addition.
The issue's description is automatically edited to include the Zoom link, and a button
appears right under the issue's title.
-![Link Zoom Call in Issue](img/zoom-quickaction-button.png)
+![Link Zoom Call in Issue](img/zoom_quickaction_button_v16_6.png)
You are only allowed to attach a single Zoom meeting to an issue. If you attempt
to add a second Zoom meeting using the `/zoom` quick action, it doesn't work. You
diff --git a/doc/user/project/issues/img/zoom-quickaction-button.png b/doc/user/project/issues/img/zoom-quickaction-button.png
deleted file mode 100644
index 3be4f36f88f..00000000000
--- a/doc/user/project/issues/img/zoom-quickaction-button.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/issues/img/zoom_quickaction_button_v16_6.png b/doc/user/project/issues/img/zoom_quickaction_button_v16_6.png
new file mode 100644
index 00000000000..cf869b59714
--- /dev/null
+++ b/doc/user/project/issues/img/zoom_quickaction_button_v16_6.png
Binary files differ
diff --git a/doc/user/project/issues/issue_weight.md b/doc/user/project/issues/issue_weight.md
index b1a1390d3d2..ddd08ee1de0 100644
--- a/doc/user/project/issues/issue_weight.md
+++ b/doc/user/project/issues/issue_weight.md
@@ -10,7 +10,8 @@ info: To determine the technical writer assigned to the Stage/Group associated w
When you have a lot of issues, it can be hard to get an overview.
With weighted issues, you can get a better idea of how much time,
-value, or complexity a given issue has or costs.
+value, or complexity a given issue has or costs. You can also [sort by weight](sorting_issue_lists.md#sorting-by-weight)
+to see which issues need to be prioritized.
## View the issue weight
diff --git a/doc/user/project/members/index.md b/doc/user/project/members/index.md
index 901a8fe9850..6df33a4fb06 100644
--- a/doc/user/project/members/index.md
+++ b/doc/user/project/members/index.md
@@ -190,6 +190,7 @@ To add a group to a project:
1. Select **Invite**.
The members of the group are not displayed on the **Members** tab.
+Private groups are masked from unauthorized users.
The **Members** tab shows:
- Members who are directly assigned to the project.
diff --git a/doc/user/project/members/share_project_with_groups.md b/doc/user/project/members/share_project_with_groups.md
index deefe9040fa..94dbb922c0b 100644
--- a/doc/user/project/members/share_project_with_groups.md
+++ b/doc/user/project/members/share_project_with_groups.md
@@ -76,6 +76,11 @@ In addition:
- On the group's page, the project is listed on the **Shared projects** tab.
- On the project's **Members** page, the group is listed on the **Groups** tab.
+- From [GitLab 16.6](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134623),
+ the invited group's name and membership source will be masked unless:
+ - the group is public, or
+ - the current user is a member of the group, or
+ - the current user is a member of the project.
- Each user is assigned a maximum role.
- Members who have the **Project Invite** badge next to their profile on the usage quota page count towards the billable members of the shared project's top-level group.
diff --git a/doc/user/project/merge_requests/ai_in_merge_requests.md b/doc/user/project/merge_requests/ai_in_merge_requests.md
index c29060bf44b..2b4b28dafa2 100644
--- a/doc/user/project/merge_requests/ai_in_merge_requests.md
+++ b/doc/user/project/merge_requests/ai_in_merge_requests.md
@@ -14,7 +14,7 @@ Additional information on enabling these features and maturity can be found in o
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/10591) in GitLab 16.3 as an [Experiment](../../../policy/experiment-beta-support.md#experiment).
-This feature is an [Experiment](../../../policy/experiment-beta-support.md) on GitLab.com that is using Google's Vertex service and the `text-bison` model. It requires the [group-level third-party AI features setting](../../group/manage.md#enable-third-party-ai-features) to be enabled.
+This feature is an [Experiment](../../../policy/experiment-beta-support.md) on GitLab.com.
Merge requests in projects often have [templates](../description_templates.md#create-a-merge-request-template) defined that need to be filled out. This helps reviewers and other users understand the purpose and changes a merge request might propose.
@@ -40,7 +40,7 @@ Provide feedback on this experimental feature in [issue 416537](https://gitlab.c
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/10401) in GitLab 16.2 as an [Experiment](../../../policy/experiment-beta-support.md#experiment).
-This feature is an [Experiment](../../../policy/experiment-beta-support.md) on GitLab.com that is using Google's Vertex service and the `text-bison` model. It requires the [group-level third-party AI features setting](../../group/manage.md#enable-third-party-ai-features) to be enabled.
+This feature is an [Experiment](../../../policy/experiment-beta-support.md) on GitLab.com.
GitLab Duo Merge request summaries are available on the merge request page in:
@@ -56,7 +56,7 @@ Provide feedback on this experimental feature in [issue 408726](https://gitlab.c
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/10466) in GitLab 16.0 as an [Experiment](../../../policy/experiment-beta-support.md#experiment).
-This feature is an [Experiment](../../../policy/experiment-beta-support.md) on GitLab.com that is using Google's Vertex service and the `text-bison` model. It requires the [group-level third-party AI features setting](../../group/manage.md#enable-third-party-ai-features) to be enabled.
+This feature is an [Experiment](../../../policy/experiment-beta-support.md) on GitLab.com.
When you've completed your review of a merge request and are ready to [submit your review](reviews/index.md#submit-a-review), generate a GitLab Duo Code review summary:
@@ -78,7 +78,7 @@ Provide feedback on this experimental feature in [issue 408991](https://gitlab.c
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/10453) in GitLab 16.2 as an [Experiment](../../../policy/experiment-beta-support.md#experiment).
-This feature is an [Experiment](../../../policy/experiment-beta-support.md) on GitLab.com that is using Google's Vertex service and the `text-bison` model. It requires the [group-level third-party AI features setting](../../group/manage.md#enable-third-party-ai-features) to be enabled.
+This feature is an [Experiment](../../../policy/experiment-beta-support.md) on GitLab.com.
When preparing to merge your merge request you may wish to edit the proposed squash or merge commit message.
@@ -99,7 +99,7 @@ Provide feedback on this experimental feature in [issue 408994](https://gitlab.c
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/10366) in GitLab 16.0 as an [Experiment](../../../policy/experiment-beta-support.md#experiment).
-This feature is an [Experiment](../../../policy/experiment-beta-support.md) on GitLab.com that is using Google's Vertex service and the `code-bison` model. It requires the [group-level third-party AI features setting](../../group/manage.md#enable-third-party-ai-features) to be enabled.
+This feature is an [Experiment](../../../policy/experiment-beta-support.md) on GitLab.com.
Use GitLab Duo Test generation in a merge request to see a list of suggested tests for the file you are reviewing. This functionality can help determine if appropriate test coverage has been provided, or if you need more coverage for your project.
diff --git a/doc/user/project/merge_requests/approvals/settings.md b/doc/user/project/merge_requests/approvals/settings.md
index ae16eb2a790..3be546faabe 100644
--- a/doc/user/project/merge_requests/approvals/settings.md
+++ b/doc/user/project/merge_requests/approvals/settings.md
@@ -29,8 +29,8 @@ These settings limit who can approve merge requests:
Prevents users who add commits to a merge request from also approving it.
- [**Prevent editing approval rules in merge requests**](#prevent-editing-approval-rules-in-merge-requests):
Prevents users from overriding project level approval rules on merge requests.
-- [**Require user password to approve**](#require-user-password-to-approve):
- Force potential approvers to first authenticate with a password.
+- [**Require user re-authentication (password or SAML) to approve**](#require-user-re-authentication-to-approve):
+ Force potential approvers to first authenticate with either a password or with SAML.
- Code Owner approval removals: Define what happens to existing approvals when
commits are added to the merge request.
- **Keep approvals**: Do not remove any approvals.
@@ -104,20 +104,29 @@ on merge requests, you can disable this setting:
This change affects all open merge requests.
-## Require user password to approve
+## Require user re-authentication to approve
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/5981) in GitLab 12.0.
> - Moved to GitLab Premium in 13.9.
+> - SAML authentication for GitLab.com groups [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/5981) in GitLab 16.6.
-You can force potential approvers to first authenticate with a password. This
+You can force potential approvers to first authenticate with either:
+
+- A password.
+- SAML. Available on GitLab.com groups only.
+
+This
permission enables an electronic signature for approvals, such as the one defined by
[Code of Federal Regulations (CFR) Part 11](https://www.accessdata.fda.gov/scripts/cdrh/cfdocs/cfcfr/CFRSearch.cfm?CFRPart=11&showFR=1&subpartNode=21:1.0.1.1.8.3)):
-1. Enable password authentication for the web interface, as described in the
- [sign-in restrictions documentation](../../../../administration/settings/sign_in_restrictions.md#password-authentication-enabled).
+1. Enable password authentication and SAML authentication (available only on GitLab.com groups). For more information on:
+ - Password authentication, see
+ [sign-in restrictions documentation](../../../../administration/settings/sign_in_restrictions.md#password-authentication-enabled).
+ - SAML authentication for GitLab.com groups, see
+ [SAML SSO for GitLab.com groups documentation](../../../../user/group/saml_sso).
1. On the left sidebar, select **Settings > Merge requests**.
1. In the **Merge request approvals** section, scroll to **Approval settings** and
- select **Require user password to approve**.
+ select **Require user re-authentication (password or SAML) to approve**.
1. Select **Save changes**.
## Remove all approvals when commits are added to the source branch
diff --git a/doc/user/project/merge_requests/cherry_pick_changes.md b/doc/user/project/merge_requests/cherry_pick_changes.md
index ef1554f3b86..af76aa100c1 100644
--- a/doc/user/project/merge_requests/cherry_pick_changes.md
+++ b/doc/user/project/merge_requests/cherry_pick_changes.md
@@ -50,7 +50,18 @@ Commit `G` is added after the cherry-pick.
## Cherry-pick all changes from a merge request
After a merge request is merged, you can cherry-pick all changes introduced
-by the merge request:
+by the merge request.
+
+Prerequisites:
+
+- You must have a role in the project that allows you to edit merge requests, and add
+ code to the repository.
+- Your project must use the [merge method](methods/index.md#fast-forward-merge) **Merge Commit**,
+ which is set in the project's **Settings > Merge requests**. Fast-forwarded commits
+ can't be cherry-picked from the GitLab UI, but the individual commits can
+ [still be cherry-picked](#cherry-pick-a-single-commit).
+
+To do this:
1. On the left sidebar, select **Search or go to** and find your project.
1. Select **Code > Merge requests**, and find your merge request.
diff --git a/doc/user/project/merge_requests/dependencies.md b/doc/user/project/merge_requests/dependencies.md
index 89305e65dfb..8fb5230c497 100644
--- a/doc/user/project/merge_requests/dependencies.md
+++ b/doc/user/project/merge_requests/dependencies.md
@@ -145,6 +145,12 @@ information, read [issue #12549](https://gitlab.com/gitlab-org/gitlab/-/issues/1
### Complex merge order dependencies are unsupported
+- Support [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/11393) in GitLab 16.6 [with a flag](../../../administration/feature_flags.md) named `remove_mr_blocking_constraints`. 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 `remove_mr_blocking_constraints`.
+On GitLab.com, this feature is available.
+
If you attempt to create an indirect, nested dependency, GitLab shows the error message:
- Dependencies failed to save: Dependency chains are not supported
diff --git a/doc/user/project/merge_requests/drafts.md b/doc/user/project/merge_requests/drafts.md
index 85ebc75e61f..a3b1920e375 100644
--- a/doc/user/project/merge_requests/drafts.md
+++ b/doc/user/project/merge_requests/drafts.md
@@ -7,22 +7,19 @@ type: reference, concepts
# Draft merge requests **(FREE ALL)**
-If a merge request isn't ready to merge, potentially because of continued development
-or open threads, you can prevent it from being accepted before you
-[mark it as ready](#mark-merge-requests-as-ready). Flag it as a draft to disable
-the **Merge** button until you remove the **Draft** flag:
+If a merge request isn't ready to merge, you can block it from merging until you
+[mark it as ready](#mark-merge-requests-as-ready). Merge requests marked as **Draft**
+cannot merge until the **Draft** flag is removed, even if all other merge criteria are met:
-![Blocked Merge Button](img/merge_request_draft_blocked_v16_0.png)
+![merge is blocked](img/merge_request_draft_blocked_v16_0.png)
## Mark merge requests as drafts
-> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/32692) in GitLab 13.2, Work-In-Progress (WIP) merge requests were renamed to **Draft**.
-> - [Removed](https://gitlab.com/gitlab-org/gitlab/-/issues/228685) all support for using **WIP** in GitLab 14.8.
-> - **Mark as draft** and **Mark as ready** buttons [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/227421) in GitLab 13.5.
+> - [Removed](https://gitlab.com/gitlab-org/gitlab/-/issues/228685) all support for the term **WIP** in GitLab 14.8.
> `/draft` quick action as a toggle [deprecated](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/92654) in GitLab 15.4.
> - [Changed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/108073) the draft status to use a checkbox in GitLab 15.8.
-There are several ways to flag a merge request as a draft:
+You can flag a merge request as a draft in several ways:
- **Viewing a merge request**: In the upper-right corner of the merge request, select **Mark as draft**.
- **Creating or editing a merge request**: Add `[Draft]`, `Draft:` or `(Draft)` to
@@ -33,12 +30,12 @@ There are several ways to flag a merge request as a draft:
in a comment. To mark a merge request as ready, use `/ready`.
- **Creating a commit**: Add `draft:`, `Draft:`, `fixup!`, or `Fixup!` to the
beginning of a commit message targeting the merge request's source branch. This
- is not a toggle, and adding this text again in a later commit doesn't mark the
+ method is not a toggle. Adding this text again in a later commit doesn't mark the
merge request as ready.
## Mark merge requests as ready
-When a merge request is ready to be merged, you can remove the `Draft` flag in several ways:
+When a merge request is ready to merge, you can remove the `Draft` flag in several ways:
- **Viewing a merge request**: In the upper-right corner of the merge request, select **Mark as ready**.
Users with at least the Developer role
@@ -50,18 +47,18 @@ When a merge request is ready to be merged, you can remove the `Draft` flag in s
[quick action](../quick_actions.md#issues-merge-requests-and-epics)
in a comment in the merge request.
-In [GitLab 13.10 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/15332),
-when you mark a merge request as ready, notifications are triggered to
-[merge request participants and watchers](../../profile/notifications.md#notifications-on-issues-merge-requests-and-epics).
+When you mark a merge request as ready,
+[merge request participants and watchers](../../profile/notifications.md#notifications-on-issues-merge-requests-and-epics)
+are notified.
## Include or exclude drafts when searching
-When viewing or searching in your project's merge requests list, you can include or exclude
+When you view or search in your project's merge requests list, to include or exclude
draft merge requests:
1. Go to your project and select **Code > Merge requests**.
-1. In the navigation bar, select **Open**, **Merged**, **Closed**, or **All** to
- filter by merge request status.
+1. To filter by merge request status, select **Open**, **Merged**, **Closed**,
+ or **All** in the navigation bar.
1. Select the search box to display a list of filters and select **Draft**, or
enter the word `draft`.
1. Select `=`.
@@ -72,9 +69,9 @@ draft merge requests:
## Pipelines for drafts
-Draft merge requests run the same pipelines as merge request that are marked as ready.
+Draft merge requests run the same pipelines as merge requests marked as ready.
-In GitLab 15.0 and older, you must [mark the merge request as ready](#mark-merge-requests-as-ready)
+In GitLab 15.0 and earlier, you must [mark the merge request as ready](#mark-merge-requests-as-ready)
if you want to run [merged results pipelines](../../../ci/pipelines/merged_results_pipelines.md).
<!-- ## Troubleshooting
diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md
index 22cd8f9b89e..63e5cc93e7d 100644
--- a/doc/user/project/merge_requests/index.md
+++ b/doc/user/project/merge_requests/index.md
@@ -82,6 +82,7 @@ or:
> - Filtering by `reviewer` [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/47605) in GitLab 13.7.
> - Filtering by potential approvers was moved to GitLab Premium in 13.9.
> - Filtering by `approved-by` moved to GitLab Premium in 13.9.
+> - Filtering by `source-branch` [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134555) in GitLab 16.6.
To filter the list of merge requests:
@@ -489,3 +490,31 @@ p = Project.find_by_full_path('<namespace/project>')
m = p.merge_requests.find_by(iid: <iid>)
Issuable::DestroyService.new(container: m.project, current_user: u).execute(m)
```
+
+### Merge request pre-receive hook failed
+
+If a merge request times out, you might see messages that indicate a Puma worker
+timeout problem:
+
+- In the GitLab UI:
+
+ ```plaintext
+ Something went wrong during merge pre-receive hook.
+ 500 Internal Server Error. Try again.
+ ```
+
+- In the `gitlab-rails/api_json.log` log file:
+
+ ```plaintext
+ Rack::Timeout::RequestTimeoutException
+ Request ran for longer than 60000ms
+ ```
+
+This error can happen if your merge request:
+
+- Contains many diffs.
+- Is many commits behind the target branch.
+
+Users in self-managed installations can request an administrator review server logs
+to determine the cause of the error. GitLab SaaS users should
+[contact Support](https://about.gitlab.com/support/#contact-support) for help.
diff --git a/doc/user/project/merge_requests/merge_when_pipeline_succeeds.md b/doc/user/project/merge_requests/merge_when_pipeline_succeeds.md
index 699c79806f0..c4c38ef9eaf 100644
--- a/doc/user/project/merge_requests/merge_when_pipeline_succeeds.md
+++ b/doc/user/project/merge_requests/merge_when_pipeline_succeeds.md
@@ -79,7 +79,7 @@ merge. This configuration works for both:
- GitLab CI/CD pipelines.
- Pipelines run from an [external CI integration](../integrations/index.md#available-integrations).
-As a result, [disabling GitLab CI/CD pipelines](../../../ci/enable_or_disable_ci.md#disable-cicd-in-a-project)
+As a result, [disabling GitLab CI/CD pipelines](../../../ci/pipelines/settings.md#disable-gitlab-cicd-pipelines)
does not disable this feature, but you can use pipelines from external
CI providers with it.
diff --git a/doc/user/project/merge_requests/revert_changes.md b/doc/user/project/merge_requests/revert_changes.md
index 7e6bf606f10..4476ec8c670 100644
--- a/doc/user/project/merge_requests/revert_changes.md
+++ b/doc/user/project/merge_requests/revert_changes.md
@@ -25,7 +25,7 @@ Prerequisites:
- You must have a role in the project that allows you to edit merge requests, and add
code to the repository.
- Your project must use the [merge method](methods/index.md#fast-forward-merge) **Merge Commit**,
- which is set in the project's **Settings > General > Merge request**. You can't revert
+ which is set in the project's **Settings > Merge requests**. You can't revert
fast-forwarded commits from the GitLab UI.
To do this:
diff --git a/doc/user/project/merge_requests/reviews/data_usage.md b/doc/user/project/merge_requests/reviews/data_usage.md
index b4b9b19c932..b32c527ab75 100644
--- a/doc/user/project/merge_requests/reviews/data_usage.md
+++ b/doc/user/project/merge_requests/reviews/data_usage.md
@@ -13,7 +13,7 @@ GitLab Duo Suggested Reviewers is the first user-facing GitLab machine learning
### Enabling the feature
-When a Project Maintainer or Owner enables Suggested Reviewers in project settings GitLab kicks off a data extraction job for the project which leverages the Merge Request API to understand pattern of review including recency, domain experience, and frequency to suggest an appropriate reviewer.
+When a Project Maintainer or Owner enables Suggested Reviewers in project settings, GitLab kicks off a data extraction job for the project which leverages the Merge Request API to understand pattern of review including recency, domain experience, and frequency to suggest an appropriate reviewer. If projects do not use the [merge request approval process](../approvals/index.md) or do not have any historical merge request data, Suggested Reviewers cannot suggest reviewers.
This data extraction job can take a few hours to complete (possibly up to a day), which is largely dependent on the size of the project. The process is automated and no action is needed during this process. Once data extraction is complete, you start getting suggestions in merge requests.
diff --git a/doc/user/project/merge_requests/reviews/img/comment-on-any-diff-line_v13_10.png b/doc/user/project/merge_requests/reviews/img/comment-on-any-diff-line_v13_10.png
deleted file mode 100644
index a31fea85be9..00000000000
--- a/doc/user/project/merge_requests/reviews/img/comment-on-any-diff-line_v13_10.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/merge_requests/reviews/img/comment_on_any_diff_line_v16_6.png b/doc/user/project/merge_requests/reviews/img/comment_on_any_diff_line_v16_6.png
new file mode 100644
index 00000000000..5ed210ad8bb
--- /dev/null
+++ b/doc/user/project/merge_requests/reviews/img/comment_on_any_diff_line_v16_6.png
Binary files differ
diff --git a/doc/user/project/merge_requests/reviews/img/mr_review_new_comment_v15_3.png b/doc/user/project/merge_requests/reviews/img/mr_review_new_comment_v15_3.png
deleted file mode 100644
index b73dbb50cd2..00000000000
--- a/doc/user/project/merge_requests/reviews/img/mr_review_new_comment_v15_3.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/merge_requests/reviews/img/mr_review_new_comment_v16_6.png b/doc/user/project/merge_requests/reviews/img/mr_review_new_comment_v16_6.png
new file mode 100644
index 00000000000..3e11440a71b
--- /dev/null
+++ b/doc/user/project/merge_requests/reviews/img/mr_review_new_comment_v16_6.png
Binary files differ
diff --git a/doc/user/project/merge_requests/reviews/img/mr_summary_comment_v15_4.png b/doc/user/project/merge_requests/reviews/img/mr_summary_comment_v15_4.png
deleted file mode 100644
index 47b7be3886d..00000000000
--- a/doc/user/project/merge_requests/reviews/img/mr_summary_comment_v15_4.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/merge_requests/reviews/img/mr_summary_comment_v16_6.png b/doc/user/project/merge_requests/reviews/img/mr_summary_comment_v16_6.png
new file mode 100644
index 00000000000..965ce84a70f
--- /dev/null
+++ b/doc/user/project/merge_requests/reviews/img/mr_summary_comment_v16_6.png
Binary files differ
diff --git a/doc/user/project/merge_requests/reviews/index.md b/doc/user/project/merge_requests/reviews/index.md
index 0a3efa38440..d3124b716da 100644
--- a/doc/user/project/merge_requests/reviews/index.md
+++ b/doc/user/project/merge_requests/reviews/index.md
@@ -26,9 +26,13 @@ For an overview, see [Merge request review](https://www.youtube.com/watch?v=2May
> - [Introduced](https://gitlab.com/groups/gitlab-org/modelops/applied-ml/review-recommender/-/epics/3) in GitLab 15.4 as a [Beta](../../../../policy/experiment-beta-support.md#beta) feature [with a flag](../../../../administration/feature_flags.md) named `suggested_reviewers_control`. Disabled by default.
> - [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/368356) in GitLab 15.6.
> - Beta designation [removed from the UI](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/113058) in GitLab 15.10.
+> - Feature flag [removed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134728) in GitLab 16.6.
GitLab uses machine learning to suggest reviewers for your merge request.
+<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
+For an overview, see [GitLab Duo Suggested Reviewers](https://www.youtube.com/embed/ivwZQgh4Rxw).
+
To suggest reviewers, GitLab uses:
- The changes in the merge request
@@ -164,7 +168,7 @@ You can submit your completed review in multiple ways:
In the modal window, you can supply a **Summary comment**, approve the merge request, and
include quick actions:
- ![Finish review with comment](img/mr_summary_comment_v15_4.png)
+ ![Finish review with comment](img/mr_summary_comment_v16_6.png)
When you submit your review, GitLab:
@@ -193,7 +197,7 @@ Pending comments display information about the action to be taken when the comme
If you have a review in progress, you can also add a comment from the **Overview** tab by selecting
**Add to review**:
-![New thread](img/mr_review_new_comment_v15_3.png)
+![New thread](img/mr_review_new_comment_v16_6.png)
### Approval Rule information for Reviewers **(PREMIUM ALL)**
@@ -227,8 +231,6 @@ them a notification email.
When commenting on a diff, you can select which lines of code your comment refers
to by either:
-![Comment on any diff file line](img/comment-on-any-diff-line_v13_10.png)
-
- Dragging **Add a comment to this line** (**{comment}**) in the gutter to highlight
lines in the diff. GitLab expands the diff lines and displays a comment box.
- After starting a comment by selecting **Add a comment to this line** (**{comment}**) in the
@@ -236,6 +238,8 @@ to by either:
select box. New comments default to single-line comments, unless you select
a different starting line.
+![Comment on any diff file line](img/comment_on_any_diff_line_v16_6.png)
+
Multiline comments display the comment's line numbers above the body of the comment:
![Multiline comment selection displayed above comment](img/multiline-comment-saved.png)
@@ -340,6 +344,9 @@ from the command line by running `git checkout <branch-name>`.
### Checkout merge requests locally through the `head` ref
+> - Deleting `head` refs 14 days after a merge request closes or merges [enabled on self-managed and GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/130098) in GitLab 16.4.
+> - Deleting `head` refs 14 days after a merge request closes or merges [generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/336070) in GitLab 16.6. Feature flag `merge_request_refs_cleanup` removed.
+
A merge request contains all the history from a repository, plus the additional
commits added to the branch associated with the merge request. Here's a few
ways to check out a merge request locally.
@@ -351,9 +358,8 @@ This relies on the merge request `head` ref (`refs/merge-requests/:iid/head`)
that is available for each merge request. It allows checking out a merge
request by using its ID instead of its branch.
-[Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/223156) in GitLab
-13.4, 14 days after a merge request gets closed or merged, the merge request
-`head` ref is deleted. This means that the merge request isn't available
+In GitLab 16.6 and later, the merge request `head` ref is deleted 14 days after
+a merge request is closed or merged. The merge request is then no longer available
for local checkout from the merge request `head` ref anymore. The merge request
can still be re-opened. If the merge request's branch
exists, you can still check out the branch, as it isn't affected.
diff --git a/doc/user/project/merge_requests/status_checks.md b/doc/user/project/merge_requests/status_checks.md
index 698078351e2..c330af0fc9b 100644
--- a/doc/user/project/merge_requests/status_checks.md
+++ b/doc/user/project/merge_requests/status_checks.md
@@ -10,6 +10,8 @@ type: reference, concepts
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/3869) in GitLab 14.0, disabled behind the `:ff_external_status_checks` feature flag.
> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/320783) in GitLab 14.1.
> - `failed` status [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/329636) in GitLab 14.9.
+> - `pending` status [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/413723) in GitLab 16.5
+> - Timeout interval of two minutes for `pending` status checks [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/388725) in GitLab 16.6.
Status checks are API calls to external systems that request the status of an external requirement.
@@ -25,6 +27,8 @@ at the merge request level itself.
You can configure merge request status checks for each individual project. These are not shared between projects.
+Status checks fail if they stay in the pending state for more than two minutes.
+
For more information about use cases, feature discovery, and development timelines,
see [epic 3869](https://gitlab.com/groups/gitlab-org/-/epics/3869).
diff --git a/doc/user/project/pages/public_folder.md b/doc/user/project/pages/public_folder.md
index 8471a4ec55a..39d80517bc7 100644
--- a/doc/user/project/pages/public_folder.md
+++ b/doc/user/project/pages/public_folder.md
@@ -126,6 +126,15 @@ pages:
NOTE:
GitLab Pages supports only static sites.
+By default, Nuxt uses the `public` folder to store static assets. For GitLab
+Pages, rename the `public` folder to a collision-free alternative first:
+
+1. In your project directory, run:
+
+ ```shell
+ mv public static
+ ```
+
1. Add the following to your `nuxt.config.js`:
```javascript
@@ -133,6 +142,12 @@ GitLab Pages supports only static sites.
target: 'static',
generate: {
dir: 'public'
+ },
+ dir: {
+ // The folder name Nuxt uses for static files (`public`) is already
+ // reserved for the build output. So in deviation from the defaults we're
+ // using a folder called `static` instead.
+ public: 'static'
}
}
```
diff --git a/doc/user/project/protected_branches.md b/doc/user/project/protected_branches.md
index fac07a1313a..f8f44d344d1 100644
--- a/doc/user/project/protected_branches.md
+++ b/doc/user/project/protected_branches.md
@@ -17,6 +17,7 @@ A protected branch controls:
- If users can force push to the branch.
- If changes to files listed in the CODEOWNERS file can be pushed directly to the branch.
- Which users can unprotect the branch.
+- Which users can modify the branch via the [Commits API](../../api/commits.md).
The [default branch](repository/branches/default.md) for your repository is protected by default.
@@ -26,12 +27,12 @@ The [default branch](repository/branches/default.md) for your repository is prot
When a branch is protected, the default behavior enforces these restrictions on the branch.
-| Action | Who can do it |
-|:-------------------------|:------------------------------------------------------------------|
-| Protect a branch | At least the Maintainer role. |
+| Action | Who can do it |
+|:-------------------------|:----------------------------------------|
+| Protect a branch | At least the Maintainer role. |
| Push to the branch | Anyone with **Allowed** permission. (1) |
-| Force push to the branch | No one. (3) |
-| Delete the branch | No one. (2) |
+| Force push to the branch | No one. (3) |
+| Delete the branch | No one. (2) |
1. Users with the Developer role can create a project in a group, but might not be allowed to
initially push to the [default branch](repository/branches/default.md).
@@ -49,12 +50,12 @@ level of protection for the branch. For example, consider these rules, which inc
[wildcards](#protect-multiple-branches-with-wildcard-rules):
| Branch name pattern | Allowed to merge | Allowed to push and merge |
-|---------------------|------------------------|-----------------|
-| `v1.x` | Maintainer | Maintainer |
-| `v1.*` | Maintainer + Developer | Maintainer |
-| `v*` | No one | No one |
+|---------------------|------------------------|---------------------------|
+| `v1.x` | Maintainer | Maintainer |
+| `v1.*` | Maintainer + Developer | Maintainer |
+| `v*` | No one | No one |
-A branch named `v1.x` matches all three branch name patterns: `v1.x`, `v1.*`, and `v*`.
+A branch named `v1.x` is a case-sensitive match for all three branch name patterns: `v1.x`, `v1.*`, and `v*`.
As the most permissive option determines the behavior, the resulting permissions for branch `v1.x` are:
- **Allowed to merge:** Of the three settings, `Maintainer + Developer` is most permissive,
@@ -71,10 +72,10 @@ If you want to ensure that `No one` is allowed to push to branch `v1.x`, every p
that matches `v1.x` must set `Allowed to push and merge` to `No one`, like this:
| Branch name pattern | Allowed to merge | Allowed to push and merge |
-|---------------------|------------------------|-----------------|
-| `v1.x` | Maintainer | No one |
-| `v1.*` | Maintainer + Developer | No one |
-| `v*` | No one | No one |
+|---------------------|------------------------|---------------------------|
+| `v1.x` | Maintainer | No one |
+| `v1.*` | Maintainer + Developer | No one |
+| `v*` | No one | No one |
### Set the default branch protection level
@@ -138,6 +139,7 @@ To protect a branch for all the projects in a group:
1. Expand **Protected branches**.
1. Select **Add protected branch**.
1. In the **Branch** text box, type the branch name or a wildcard.
+ Branch names and wildcards [are case-sensitive](repository/branches/index.md#name-your-branch).
1. From the **Allowed to merge** list, select a role that can merge into this branch.
1. From the **Allowed to push and merge** list, select a role that can push to this branch.
1. Select **Protect**.
@@ -162,7 +164,7 @@ To protect multiple branches at the same time:
1. Expand **Protected branches**.
1. Select **Add protected branch**.
1. From the **Branch** dropdown list, type the branch name and a wildcard.
- For example:
+ Branch names and wildcards [are case-sensitive](repository/branches/index.md#name-your-branch). For example:
| Wildcard protected branch | Matching branches |
|---------------------------|--------------------------------------------------------|
@@ -370,6 +372,7 @@ branches by using the GitLab web interface:
1. Select **Code > Branches**.
1. Next to the branch you want to delete, select **Delete** (**{remove}**).
1. On the confirmation dialog, enter the branch name and select **Yes, delete protected branch**.
+ Branch names [are case-sensitive](repository/branches/index.md#name-your-branch).
Protected branches can only be deleted by using GitLab either from the UI or API.
This prevents accidentally deleting a branch through local Git commands or
@@ -381,14 +384,10 @@ third-party Git clients.
- [Branches](repository/branches/index.md)
- [Branches API](../../api/branches.md)
-<!-- ## Troubleshooting
+## Troubleshooting
-Include any troubleshooting steps that you can foresee. If you know beforehand what issues
-one might have when setting this up, or when something is changed, or on upgrading, it's
-important to describe those, too. Think of things that may go wrong and include them here.
-This is important to minimize requests for support, and to avoid doc comments with
-questions that you know someone might ask.
+### Branch names are case-sensitive
-Each scenario can be a third-level heading, for example `### Getting error message X`.
-If you have none to add when creating a doc, leave this section in place
-but commented out to help encourage others to add to it in the future. -->
+Branch names in `git` are case-sensitive. When configuring your protected branch
+or [target branch rule](repository/branches/index.md#configure-rules-for-target-branches),
+`dev` is not the same `DEV` or `Dev`.
diff --git a/doc/user/project/push_options.md b/doc/user/project/push_options.md
index e8451e3049d..6c89e09bd47 100644
--- a/doc/user/project/push_options.md
+++ b/doc/user/project/push_options.md
@@ -45,7 +45,8 @@ Git push options can perform actions for merge requests while pushing changes:
| Push option | Description |
|----------------------------------------------|-------------|
| `merge_request.create` | Create a new merge request for the pushed branch. |
-| `merge_request.target=<branch_name>` | Set the target of the merge request to a particular branch or upstream project, such as: `git push -o merge_request.target=project_path/branch`. |
+| `merge_request.target=<branch_name>` | Set the target of the merge request to a particular branch, such as: `git push -o merge_request.target=branch_name`. |
+| `merge_request.target_project=<project>` | Set the target of the merge request to a particular upstream project, such as: `git push -o merge_request.target_project=path/to/project`. Introduced in [GitLab 16.6](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132475). |
| `merge_request.merge_when_pipeline_succeeds` | Set the merge request to [merge when its pipeline succeeds](merge_requests/merge_when_pipeline_succeeds.md). |
| `merge_request.remove_source_branch` | Set the merge request to remove the source branch when it's merged. |
| `merge_request.title="<title>"` | Set the title of the merge request. For example: `git push -o merge_request.title="The title I want"`. |
diff --git a/doc/user/project/repository/branches/index.md b/doc/user/project/repository/branches/index.md
index 30ddf8d3230..3640beebdfb 100644
--- a/doc/user/project/repository/branches/index.md
+++ b/doc/user/project/repository/branches/index.md
@@ -176,6 +176,7 @@ GitLab enforces these additional rules on all branches:
- No spaces are allowed in branch names.
- Branch names with 40 hexadecimal characters are prohibited, because they are similar to Git commit hashes.
+- Branch names are case-sensitive.
Common software packages, like Docker, can enforce
[additional branch naming restrictions](../../../../administration/packages/container_registry.md#docker-connection-error).
@@ -313,6 +314,27 @@ To create a target branch rule:
1. Select the **Target branch** to use when the branch name matches the **Rule name**.
1. Select **Save**.
+### Example
+
+You could configure your project to have the following target branch rules:
+
+| Rule name | Target branch |
+|-------------|---------------|
+| `feature/*` | `develop` |
+| `bug/*` | `develop` |
+| `release/*` | `main` |
+
+These rules simplify the process of creating merge requests for a project that:
+
+- Uses `main` to represent the deployed state of your application.
+- Tracks current, unreleased development work in another long-running branch, like `develop`.
+
+If your workflow initially places new features in `develop` instead of `main`, these rules
+ensure all branches matching either `feature/*` or `bug/*` do not target `main` by mistake.
+
+When you're ready to release to `main`, create a branch named `release/*`, and the rules
+ensure this branch targets `main`.
+
## Delete a target branch rule
When you remove a target branch rule, existing merge requests remain unchanged.
@@ -389,3 +411,18 @@ To fix this problem:
Git versions [2.16.0 and later](https://github.com/git/git/commit/a625b092cc59940521789fe8a3ff69c8d6b14eb2),
prevent you from creating a branch with this name.
+
+### Find all branches you've authored
+
+To find all branches you've authored in a project, run this command in a Git repository:
+
+```shell
+git for-each-ref --format='%(refname:short) %(authoremail)' | grep $(git config --get user.email)
+```
+
+To get a total of all branches in a project, sorted by author, run this command
+in a Git repository:
+
+```shell
+git for-each-ref --format='%(authoremail)' | sort | uniq -c | sort -g
+```
diff --git a/doc/user/project/repository/code_suggestions/index.md b/doc/user/project/repository/code_suggestions/index.md
index 151792089ce..b44e26f8daf 100644
--- a/doc/user/project/repository/code_suggestions/index.md
+++ b/doc/user/project/repository/code_suggestions/index.md
@@ -17,6 +17,11 @@ Beta users should read about the [known limitations](#known-limitations). We loo
Write code more efficiently by using generative AI to suggest code while you're developing.
+Code Suggestions supports two distinct types of interactions:
+
+- Code Completion, which suggests completions the current line you are typing. These suggestions are usually low latency.
+- Code Generation, which generates code based on a natural language code comment block. Generating code can exceed multiple seconds.
+
GitLab Duo Code Suggestions are available:
- On [self-managed](self_managed.md) and [SaaS](saas.md).
@@ -31,7 +36,7 @@ GitLab Duo Code Suggestions are available:
</figure>
During Beta, usage of Code Suggestions is governed by the [GitLab Testing Agreement](https://about.gitlab.com/handbook/legal/testing-agreement/).
-Learn about [data usage when using Code Suggestions](#code-suggestions-data-usage).
+Learn about [data usage when using Code Suggestions](#code-suggestions-data-usage). As Code Suggestions matures to General Availibility it will be governed by our [AI Functionality Terms](https://about.gitlab.com/handbook/legal/ai-functionality-terms/).
## Use Code Suggestions
@@ -62,22 +67,13 @@ Code Suggestions do not prevent you from writing code in your IDE.
## Supported languages
-The best results from Code Suggestions are expected for languages that [Anthropic Claude](https://www.anthropic.com/product) and the [Google Vertex AI Codey APIs](https://cloud.google.com/vertex-ai/docs/generative-ai/code/code-models-overview#supported_coding_languages) directly support:
-
-- C++
-- C#
-- Go
-- Google SQL
-- Java
-- JavaScript
-- Kotlin
-- PHP
-- Python
-- Ruby
-- Rust
-- Scala
-- Swift
-- TypeScript
+Code Suggestions support is a function of the:
+
+- Underlying large language model.
+- IDE used.
+- Extension or plug-in support in the IDE.
+
+For languages not listed in the following table, Code Suggestions might not function as expected.
### Supported languages in IDEs
@@ -129,10 +125,12 @@ This improvement should result in:
Code Suggestions is powered by a generative AI model.
Your personal access token enables a secure API connection to GitLab.com or to your GitLab instance.
-This API connection securely transmits a context window from your IDE/editor to the [GitLab AI Gateway](https://gitlab.com/gitlab-org/modelops/applied-ml/code-suggestions/ai-assist), a GitLab hosted service. The gateway calls the large language model APIs, and then the generated suggestion is transmitted back to your IDE/editor.
+This API connection securely transmits a context window from your IDE/editor to the [GitLab AI Gateway](https://gitlab.com/gitlab-org/modelops/applied-ml/code-suggestions/ai-assist), a GitLab hosted service. The [gateway](../../../../development/ai_architecture.md) calls the large language model APIs, and then the generated suggestion is transmitted back to your IDE/editor.
GitLab selects the best-in-class large-language models for specific tasks. We use [Google Vertex AI Code Models](https://cloud.google.com/vertex-ai/docs/generative-ai/code/code-models-overview) and [Anthropic Claude](https://www.anthropic.com/product) for Code Suggestions.
+[View data retention policies](../../../ai_features.md#data-retention).
+
### Telemetry
For self-managed instances that have enabled Code Suggestions and SaaS accounts, we collect aggregated or de-identified first-party usage data through our [Snowplow collector](https://about.gitlab.com/handbook/business-technology/data-team/platform/snowplow/). This usage data includes the following metrics:
diff --git a/doc/user/project/repository/code_suggestions/self_managed.md b/doc/user/project/repository/code_suggestions/self_managed.md
index ee501212027..fd363e56021 100644
--- a/doc/user/project/repository/code_suggestions/self_managed.md
+++ b/doc/user/project/repository/code_suggestions/self_managed.md
@@ -164,7 +164,7 @@ A self-managed GitLab instance does not generate the code suggestion. After succ
authentication to the self-managed instance, a token is generated.
The IDE/editor then uses this token to securely transmit data directly to
-GitLab.com's Code Suggestions service for processing.
+GitLab.com's Code Suggestions service via the [Cloud Connector gateway service](../../../../architecture/blueprints/cloud_connector/index.md) for processing.
The Code Suggestions service then securely returns an AI-generated code suggestion.
diff --git a/doc/user/project/repository/code_suggestions/troubleshooting.md b/doc/user/project/repository/code_suggestions/troubleshooting.md
index 2faf20b3035..86400ea8860 100644
--- a/doc/user/project/repository/code_suggestions/troubleshooting.md
+++ b/doc/user/project/repository/code_suggestions/troubleshooting.md
@@ -18,9 +18,6 @@ In GitLab, ensure Code Suggestions is enabled:
- [For your user account](../../../profile/preferences.md#enable-code-suggestions).
- [For *all* top-level groups your account belongs to](../../../group/manage.md#enable-code-suggestions). If you don't have a role that lets you view the top-level group's settings, contact a group owner.
-To confirm that your account is enabled, go to [https://gitlab.com/api/v4/ml/ai-assist](https://gitlab.com/api/v4/ml/ai-assist). The `user_is_allowed` key should have should have a value of `true`.
-A `404 Not Found` result is returned if either of the previous conditions is not met.
-
### Code Suggestions not displayed in VS Code or GitLab WebIDE
Check all the steps in [Code Suggestions aren't displayed](#code-suggestions-arent-displayed) first.
diff --git a/doc/user/project/repository/forking_workflow.md b/doc/user/project/repository/forking_workflow.md
index ddc650c3924..c71c89b68c3 100644
--- a/doc/user/project/repository/forking_workflow.md
+++ b/doc/user/project/repository/forking_workflow.md
@@ -24,17 +24,24 @@ can access the object pool connected to the source project.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/15013) a new form in GitLab 13.11 [with a flag](../../../user/feature_flags.md) named `fork_project_form`. Disabled by default.
> - [Enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/77181) in GitLab 14.8. Feature flag `fork_project_form` removed.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/24894) in GitLab 16.6.
To fork an existing project in GitLab:
1. On the project's homepage, in the upper-right corner, select **Fork** (**{fork}**):
+
![Fork this project](img/forking_workflow_fork_button_v13_10.png)
+
1. Optional. Edit the **Project name**.
1. For **Project URL**, select the [namespace](../../namespace/index.md)
your fork should belong to.
1. Add a **Project slug**. This value becomes part of the URL to your fork.
It must be unique in the namespace.
1. Optional. Add a **Project description**.
+1. Select one of the **Branches to include** options:
+ - **All branches** (default).
+ - **Only the default branch**. Uses the `--single-branch` and `--no-tags`
+ [Git options](https://git-scm.com/docs/git-clone).
1. Select the **Visibility level** for your fork. For more information about
visibility levels, read [Project and group visibility](../../public_access.md).
1. Select **Fork project**.
diff --git a/doc/user/project/repository/reducing_the_repo_size_using_git.md b/doc/user/project/repository/reducing_the_repo_size_using_git.md
index ff9ef5b78f8..ca7f2ae2043 100644
--- a/doc/user/project/repository/reducing_the_repo_size_using_git.md
+++ b/doc/user/project/repository/reducing_the_repo_size_using_git.md
@@ -325,12 +325,15 @@ are accurate.
To expedite this process, see the
['Prune Unreachable Objects' housekeeping task](../../../administration/housekeeping.md).
-### Sidekiq process fails to export a project
+### Sidekiq process fails to export a project **(FREE SELF)**
Occasionally the Sidekiq process can fail to export a project, for example if
it is terminated during execution.
-To bypass the Sidekiq process, use the Rails console to manually trigger the project export:
+GitLab.com users should [contact Support](https://about.gitlab.com/support/#contact-support) to resolve this issue.
+
+Self-managed users can use the Rails console to bypass the Sidekiq process and
+manually trigger the project export:
```ruby
project = Project.find(1)
diff --git a/doc/user/project/service_desk/configure.md b/doc/user/project/service_desk/configure.md
index 172a105cc28..8d0fbd81ebd 100644
--- a/doc/user/project/service_desk/configure.md
+++ b/doc/user/project/service_desk/configure.md
@@ -191,6 +191,8 @@ The custom email address you want to use must meet all of the following requirem
by any text to the local part. Given the email address `support@example.com`, check whether sub-addressing is supported by
sending an email to `support+1@example.com`. This email should appear in your mailbox.
- You have SMTP credentials (ideally, you should use an app password).
+ The username and password are stored in the database using the Advanced Encryption Standard (AES)
+ with a 256-bit key.
- You must have at least the Maintainer role for the project.
- Service Desk must be configured for the project.
diff --git a/doc/user/project/service_desk/using_service_desk.md b/doc/user/project/service_desk/using_service_desk.md
index ad97a36bbb0..5f3c725b83b 100644
--- a/doc/user/project/service_desk/using_service_desk.md
+++ b/doc/user/project/service_desk/using_service_desk.md
@@ -138,10 +138,7 @@ HTML emails show HTML formatting, such as:
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/11733) in GitLab 15.8 [with a flag](../../../administration/feature_flags.md) named `service_desk_new_note_email_native_attachments`. Disabled by default.
> - [Enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/386860) in GitLab 15.10.
-
-FLAG:
-On self-managed GitLab, by default this feature is available. To hide the feature per project or for your entire instance, an administrator can [disable the feature flag](../../../administration/feature_flags.md) named `service_desk_new_note_email_native_attachments`.
-On GitLab.com, this feature is available.
+> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/11733) in GitLab 16.6. Feature flag `service_desk_new_note_email_native_attachments` removed.
If a comment contains any attachments and their total size is less than or equal to 10 MB, these
attachments are sent as part of the email. In other cases, the email contains links to the attachments.
diff --git a/doc/user/project/settings/project_access_tokens.md b/doc/user/project/settings/project_access_tokens.md
index 7de8a7beab5..3526425c912 100644
--- a/doc/user/project/settings/project_access_tokens.md
+++ b/doc/user/project/settings/project_access_tokens.md
@@ -60,7 +60,7 @@ To create a project access token:
1. Enter a name. The token name is visible to any user with permissions to view the project.
1. Enter an expiry date for the token.
- The token expires on that date at midnight UTC.
- - If you do not enter an expiry date, the expiry date is automatically set to 365 days later than the current date.
+ - If you do not enter an expiry date, the expiry date is automatically set to 30 days later than the current date.
- By default, this date can be a maximum of 365 days later than the current date.
- An instance-wide [maximum lifetime](../../../administration/settings/account_and_limit_settings.md#limit-the-lifetime-of-access-tokens) setting can limit the maximum allowable lifetime in self-managed instances.
1. Select a role for the token.
diff --git a/doc/user/project/system_notes.md b/doc/user/project/system_notes.md
index 73509846990..546b3250180 100644
--- a/doc/user/project/system_notes.md
+++ b/doc/user/project/system_notes.md
@@ -23,12 +23,14 @@ in system notes. System notes use the format `<Author> <action> <time ago>`.
By default, system notes do not display. When displayed, they are shown oldest first.
If you change the filter or sort options, your selection is remembered across sections.
-The filtering options are:
+For all item types except merge requests, the filtering options are:
- **Show all activity** displays both comments and history.
- **Show comments only** hides system notes.
- **Show history only** hides user comments.
+Merge requests provide more granular filtering options.
+
### On an epic
1. On the left sidebar, select **Search or go to** and find your project.
@@ -49,7 +51,19 @@ The filtering options are:
1. On the left sidebar, select **Search or go to** and find your project.
1. Select **Code > Merge requests** and find your merge request.
1. Go to **Activity**.
-1. For **Sort or filter**, select **Show all activity**.
+1. For **Sort or filter**, select **Show all activity** to see all system notes.
+ To narrow the types of system notes returned, select one or more of:
+
+ - **Approvals**
+ - **Assignees &amp; Reviewers**
+ - **Comments**
+ - **Commits &amp; branches**
+ - **Edits**
+ - **Labels**
+ - **Lock status**
+ - **Mentions**
+ - **Merge request status**
+ - **Tracking**
## Privacy considerations
diff --git a/doc/user/project/wiki/index.md b/doc/user/project/wiki/index.md
index a80c699eab7..fd543263ebd 100644
--- a/doc/user/project/wiki/index.md
+++ b/doc/user/project/wiki/index.md
@@ -181,11 +181,7 @@ You need at least the Developer role to move a wiki page:
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/414691) in GitLab 16.3 [with a flag](../../../administration/feature_flags.md) named `print_wiki`. Disabled by default.
> - [Enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134251/) in GitLab 16.5.
-
-FLAG:
-On self-managed GitLab, by default this feature is available.
-To hide the feature, an administrator can [disable the feature flag](../../../administration/feature_flags.md) named `print_wiki`.
-On GitLab.com, this feature is available.
+> - Feature flag `print_wiki` removed in GitLab 16.6.
You can export a wiki page as a PDF file:
diff --git a/doc/user/read_only_namespaces.md b/doc/user/read_only_namespaces.md
index 5b302d976dd..d5697ec5a94 100644
--- a/doc/user/read_only_namespaces.md
+++ b/doc/user/read_only_namespaces.md
@@ -27,7 +27,7 @@ To restore a namespace to its standard state, you can:
- [Purchase a paid tier](https://about.gitlab.com/pricing/).
- For exceeded storage quota:
- [Purchase more storage for the namespace](../subscriptions/gitlab_com/index.md#purchase-more-storage-and-transfer).
- - [Manage your storage usage](usage_quotas.md#manage-your-storage-usage).
+ - [Manage your storage usage](usage_quotas.md#manage-storage-usage).
## Restricted actions
diff --git a/doc/user/report_abuse.md b/doc/user/report_abuse.md
index 45113562e87..9e13d1fe263 100644
--- a/doc/user/report_abuse.md
+++ b/doc/user/report_abuse.md
@@ -26,17 +26,12 @@ You can report a user through their:
> - Report abuse from overflow menu [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/414773) in GitLab 16.4 [with a flag](../administration/feature_flags.md) named `user_profile_overflow_menu_vue`. Disabled by default.
> - [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/414773) in GitLab 16.4.
-
-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 `user_profile_overflow_menu_vue`.
-On GitLab.com, this feature is available.
+> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/414773) in GitLab 16.6. Feature flag `user_profile_overflow_menu_vue` removed.
To report abuse from a user's profile page:
1. Anywhere in GitLab, select the name of the user.
-1. In the upper-right corner of the user's profile, if the `user_profile_overflow_menu_vue` feature flag is:
- - Enabled, select the vertical ellipsis (**{ellipsis_v}**), then **Report abuse to administrator**.
- - Disabled, select **Report abuse to administrator** (**{information-o}**).
+1. In the upper-right corner of the user's profile select the vertical ellipsis (**{ellipsis_v}**), then **Report abuse to administrator**.
1. Select a reason for reporting the user.
1. Complete an abuse report.
1. Select **Send report**.
diff --git a/doc/user/reserved_names.md b/doc/user/reserved_names.md
index b9c64739de0..697f5711396 100644
--- a/doc/user/reserved_names.md
+++ b/doc/user/reserved_names.md
@@ -6,31 +6,30 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Reserved project and group names **(FREE ALL)**
-Not all project & group names are allowed because they would conflict with
-existing routes used by GitLab.
+To not conflict with existing routes used by GitLab, some words cannot be used as project or group names.
+These words are listed in the
+[`path_regex.rb` file](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/path_regex.rb),
+where:
-For a list of words that are not allowed to be used as group or project names, see the
-[`path_regex.rb` file](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/path_regex.rb)
-under the `TOP_LEVEL_ROUTES`, `PROJECT_WILDCARD_ROUTES` and `GROUP_ROUTES` lists:
-
-- `TOP_LEVEL_ROUTES`: are names that are reserved as usernames or top level groups
-- `PROJECT_WILDCARD_ROUTES`: are names that are reserved for child groups or projects.
-- `GROUP_ROUTES`: are names that are reserved for all groups or projects.
+- `TOP_LEVEL_ROUTES` are names reserved as usernames or top-level groups.
+- `PROJECT_WILDCARD_ROUTES` are names reserved for child groups or projects.
+- `GROUP_ROUTES` are names reserved for all groups or projects.
## Limitations on project and group names
-- Project or group names must start with a letter, digit, emoji, or "_".
-- Project names can only contain letters, digits, emoji, "_", ".", "+", dashes, or spaces.
-- Group names can only contain letters, digits, emoji, "_", ".", parenthesis, dashes, or spaces.
-- Project or group slugs must start with a letter or digit.
-- Project or group slugs can only contain letters, digits, '_', '.', or dashes.
-- Project or group slugs must not contain consecutive special characters.
-- Project or group slugs cannot start or end with a special character.
-- Project or group slugs cannot end in `.git` or `.atom`.
+- Project or group names must start with a letter (`a-zA-Z`), digit (`0-9`), emoji, or underscore (`_`). Additionally:
+ - Project names can contain only letters (`a-zA-Z`), digits (`0-9`), emoji, underscores (`_`), dots (`.`), pluses (`+`), dashes (`-`), or spaces.
+ - Group names can contain only letters (`a-zA-Z`), digits (`0-9`), emoji, underscores (`_`), dots (`.`), parentheses (`()`), dashes (`-`), or spaces.
+- Project or group slugs:
+ - Must start with a letter (`a-zA-Z`) or digit (`0-9`).
+ - Must not contain consecutive special characters.
+ - Cannot start or end with a special character.
+ - Cannot end in `.git` or `.atom`.
+ - Can contain only letters (`a-zA-Z`), digits (`0-9`), underscores (`_`), dots (`.`), or dashes (`-`).
## Reserved project names
-It is not possible to create a project with the following names:
+You cannot create projects with the following names:
- `\-`
- `badges`
@@ -56,7 +55,7 @@ It is not possible to create a project with the following names:
## Reserved group names
-The following names are reserved as top level groups:
+You cannot create groups with the following names, because they are reserved for top-level groups:
- `\-`
- `.well-known`
@@ -98,6 +97,6 @@ The following names are reserved as top level groups:
- `users`
- `v2`
-These group names are unavailable as subgroup names:
+You cannot create subgroups with the following names:
- `\-`
diff --git a/doc/user/search/index.md b/doc/user/search/index.md
index e8dfbfa675a..79782b1c880 100644
--- a/doc/user/search/index.md
+++ b/doc/user/search/index.md
@@ -103,29 +103,15 @@ For example:
## Include archived projects in search results
-> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121981) in GitLab 16.1 [with a flag](../../administration/feature_flags.md) named `search_projects_hide_archived`. Disabled by default.
-> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/413821) in GitLab 16.3. Feature flag `search_projects_hide_archived` removed.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121981) in GitLab 16.1 [with a flag](../../administration/feature_flags.md) named `search_projects_hide_archived` for project search. Disabled by default.
+> - [Generally available](https://gitlab.com/groups/gitlab-org/-/epics/10957) in GitLab 16.6 for all search scopes.
By default, archived projects are excluded from search results.
-To include archived projects:
+To include archived projects in search results:
-1. On the project search page, on the left sidebar, select the **Include archived** checkbox.
+1. On the search page, on the left sidebar, select the **Include archived** checkbox.
1. On the left sidebar, select **Apply**.
-## Exclude issues in archived projects from search results
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/124846) in GitLab 16.2 [with a flag](../../administration/feature_flags.md) named `search_issues_hide_archived_projects`. 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 `search_issues_hide_archived_projects`. On GitLab.com, this feature is not available.
-
-By default, issues in archived projects are included in search results.
-To exclude issues in archived projects, ensure the `search_issues_hide_archived_projects` flag is enabled.
-
-To include issues in archived projects with `search_issues_hide_archived_projects` enabled,
-you must add the parameter `include_archived=true` to the URL.
-
## Search for code
To search for code in a project:
diff --git a/doc/user/shortcuts.md b/doc/user/shortcuts.md
index fa03cb54ba3..e504ee90821 100644
--- a/doc/user/shortcuts.md
+++ b/doc/user/shortcuts.md
@@ -1,6 +1,6 @@
---
-stage: none
-group: unassigned
+stage: Manage
+group: Foundations
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: reference
---
@@ -135,6 +135,7 @@ These shortcuts are available when browsing the files in a project (go to
| <kbd>Enter</kbd> | Open selection. |
| <kbd>Escape</kbd> | Go back to file list screen (only while searching for files, **Code > Repository**, then select **Find File**). |
| <kbd>y</kbd> | Go to file permalink (only while viewing a file). |
+| <kbd>Shift</kbd> + <kbd>c</kbd> | Go to compare branches view. |
| <kbd>.</kbd> | Open the [Web IDE](project/web_ide/index.md). |
### Web IDE
diff --git a/doc/user/snippets.md b/doc/user/snippets.md
index dbcc90c26df..fe782227701 100644
--- a/doc/user/snippets.md
+++ b/doc/user/snippets.md
@@ -17,7 +17,7 @@ and you can maintain your snippets with the [snippets API](../api/snippets.md).
You can create and manage your snippets through the GitLab user interface, or by
using the [GitLab Workflow VS Code extension](project/repository/vscode.md).
-![Example of snippet](img/snippet_intro_v13_11.png)
+![Example of a snippet](img/snippet_sample_v16_6.png)
GitLab provides two types of snippets:
@@ -168,10 +168,11 @@ To delete a file from your snippet through the GitLab UI:
## Clone snippets
To ensure you receive updates, clone the snippet instead of copying it locally. Cloning
-maintains the snippet's connection with the repository. Select **Clone** on a snippet
-to display the URLs to clone with SSH or HTTPS:
+maintains the snippet's connection with the repository.
-![Clone snippet](img/snippet_clone_button_v13_0.png)
+To clone a snippet:
+
+- Select **Clone**, then copy the URL to clone with SSH or HTTPS.
You can commit changes to a cloned snippet, and push the changes to GitLab.
diff --git a/doc/user/storage_management_automation.md b/doc/user/storage_management_automation.md
index 96f9ecd11a8..a83af4ab6c6 100644
--- a/doc/user/storage_management_automation.md
+++ b/doc/user/storage_management_automation.md
@@ -14,6 +14,10 @@ You can also manage your storage usage by improving [pipeline efficiency](../ci/
For more help with API automation, you can also use the [GitLab community forum and Discord](https://about.gitlab.com/community/).
+WARNING:
+The script examples in this page are for demonstration purposes only and should not
+be used in production. You can use the examples to design and test your own scripts for storage automation.
+
## API requirements
To automate storage management, your GitLab.com SaaS or self-managed instance must have access to the [GitLab REST API](../api/api_resources.md).
@@ -567,11 +571,17 @@ Support for creating a retention policy for job logs is proposed in [issue 37471
### Delete old pipelines
-Pipelines do not add to the overall storage consumption, but if required you can delete them with the following methods.
+Pipelines do not add to the overall storage usage, but if required you can automate their deletion.
-Automatic deletion of old pipelines is proposed in [issue 338480](https://gitlab.com/gitlab-org/gitlab/-/issues/338480).
+To delete pipelines based on a specific date, specify the `created_at` key.
+You can use the date to calculate the difference between the current date and
+when the pipeline was created. If the age is larger than the threshold, the pipeline is deleted.
-Example with the GitLab CLI:
+NOTE:
+The `created_at` key must be converted from a timestamp to Unix epoch time,
+for example with `date -d '2023-08-08T18:59:47.581Z' +%s`.
+
+Example with GitLab CLI:
```shell
export GL_PROJECT_ID=48349590
@@ -589,12 +599,10 @@ glab api --method GET projects/$GL_PROJECT_ID/pipelines | jq --compact-output '.
"2023-08-08T18:59:47.581Z"
```
-The `created_at` key must be converted from a timestamp to Unix epoch time,
-for example with `date -d '2023-08-08T18:59:47.581Z' +%s`. In the next step, the
-age can be calculated with the difference between now, and the pipeline creation
-date. If the age is larger than the threshold, the pipeline should be deleted.
+In the following example that uses a Bash script:
-The following example uses a Bash script that expects `jq` and the GitLab CLI installed, and authorized, and the exported environment variable `GL_PROJECT_ID`.
+- `jq` and the GitLab CLI are installed and authorized.
+- The exported environment variable `GL_PROJECT_ID`.
The full script `get_cicd_pipelines_compare_age_threshold_example.sh` is located in the [GitLab API with Linux Shell](https://gitlab.com/gitlab-de/use-cases/gitlab-api/gitlab-api-linux-shell) project.
@@ -624,7 +632,7 @@ do
done
```
-You can use the [`python-gitlab` API library](https://python-gitlab.readthedocs.io/en/stable/gl_objects/pipelines_and_jobs.html#project-pipelines) and
+You can also use the [`python-gitlab` API library](https://python-gitlab.readthedocs.io/en/stable/gl_objects/pipelines_and_jobs.html#project-pipelines) and
the `created_at` attribute to implement a similar algorithm that compares the job artifact age:
```python
@@ -645,6 +653,8 @@ the `created_at` attribute to implement a similar algorithm that compares the jo
pipeline_obj.delete()
```
+Automatic deletion of old pipelines is proposed in [issue 338480](https://gitlab.com/gitlab-org/gitlab/-/issues/338480).
+
### List expiry settings for job artifacts
To manage artifact storage, you can update or configure when an artifact expires.
@@ -770,7 +780,7 @@ default:
## Manage Container Registries storage
-Container registries are available [in a project](../api/container_registry.md#within-a-project) or [in a group](../api/container_registry.md#within-a-group). You can analyze both locations to implement a cleanup strategy.
+Container registries are available [for projects](../api/container_registry.md#within-a-project) or [for groups](../api/container_registry.md#within-a-group). You can analyze both locations to implement a cleanup strategy.
### List container registries
@@ -818,8 +828,6 @@ glab api --method GET projects/$GL_PROJECT_ID/registry/repositories/4435617/tags
::EndTabs
-A similar automation shell script is created in the [delete old pipelines](#delete-old-pipelines) section.
-
### Delete container images in bulk
When you [delete container image tags in bulk](../api/container_registry.md#delete-registry-repository-tags-in-bulk),
@@ -886,7 +894,7 @@ You can optimize container images to reduce the image size and overall storage c
## Manage Package Registry storage
-Package registries are available [in a project](../api/packages.md#within-a-project) or [in a group](../api/packages.md#within-a-group).
+Package registries are available [for projects](../api/packages.md#for-a-project) or [for groups](../api/packages.md#for-a-group).
### List packages and files
diff --git a/doc/user/tasks.md b/doc/user/tasks.md
index 347aedd6e74..173d2e44cf1 100644
--- a/doc/user/tasks.md
+++ b/doc/user/tasks.md
@@ -360,6 +360,25 @@ To copy the task's email address:
1. Select **Plan > Issues**, then select your issue to view it.
1. In the top right corner, select the vertical ellipsis (**{ellipsis_v}**), then select **Copy task email address**.
+## Set an issue as a parent
+
+> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/11198) in GitLab 16.5.
+
+Prerequisite:
+
+- You must have at least the Reporter role for the project.
+- The issue and task must belong to the same project.
+
+To set an issue as a parent of a task:
+
+1. In the issue description, in the **Tasks** section, select the title of the task you want to edit.
+ The task window opens.
+1. Next to **Parent**, from the dropdown list, select the parent to add.
+1. Select any area outside the dropdown list.
+
+To remove the parent item of the task,
+next to **Parent**, select the dropdown list and then select **Unassign**.
+
## Confidential tasks
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/8410) in GitLab 15.3.
diff --git a/doc/user/usage_quotas.md b/doc/user/usage_quotas.md
index 305a46e1f15..7dea2b97249 100644
--- a/doc/user/usage_quotas.md
+++ b/doc/user/usage_quotas.md
@@ -5,7 +5,7 @@ group: Utilization
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
---
-# Storage usage quota **(FREE ALL)**
+# Storage **(FREE ALL)**
Storage usage statistics are available for projects and namespaces. You can use that information to
manage storage usage within the applicable quotas.
@@ -13,8 +13,8 @@ manage storage usage within the applicable quotas.
Statistics include:
- Storage usage across projects in a namespace.
-- Storage usage that exceeds the storage quota.
-- Available purchased storage.
+- Storage usage that exceeds the storage SaaS limit or [self-managed storage quota](../administration/settings/account_and_limit_settings.md#repository-size-limit).
+- Available purchased storage for SaaS.
Storage and network usage are calculated with the binary measurement system (1024 unit multiples).
Storage usage is displayed in kibibytes (KiB), mebibytes (MiB),
@@ -30,87 +30,33 @@ you might see references to `KB`, `MB`, and `GB` in the UI and documentation.
Prerequisites:
- To view storage usage for a project, you must have at least the Maintainer role for the project or Owner role for the namespace.
-- To view storage usage for a namespace, you must have the Owner role for the namespace.
+- To view storage usage for a group namespace, you must have the Owner role for the namespace.
1. On the left sidebar, select **Search or go to** and find your project or group.
1. On the left sidebar, select **Settings > Usage Quotas**.
-1. Select the **Storage** tab.
+1. Select the **Storage** tab to see namespace storage usage.
+1. To view storage usage for a project, select one of the projects from the table at the bottom of the **Storage** tab of the **Usage Quotas** page.
-Select any title to view details. The information on this page
-is updated every 90 minutes.
+The information on the **Usage Quotas** page is updated every 90 minutes.
If your namespace shows `'Not applicable.'`, push a commit to any project in the
namespace to recalculate the storage.
-### Container Registry usage **(FREE SAAS)**
+### View project fork storage usage **(FREE SAAS)**
-Container Registry usage is available only for GitLab.com. This feature requires a
-[new version](https://about.gitlab.com/blog/2022/04/12/next-generation-container-registry/)
-of the GitLab Container Registry. To learn about the proposed release for self-managed
-installations, see [epic 5521](https://gitlab.com/groups/gitlab-org/-/epics/5521).
-
-#### How container registry usage is calculated
-
-Image layers stored in the Container Registry are deduplicated at the root namespace level.
-
-An image is only counted once if:
-
-- You tag the same image more than once in the same repository.
-- You tag the same image across distinct repositories under the same root namespace.
-
-An image layer is only counted once if:
-
-- You share the image layer across multiple images in the same container repository, project, or group.
-- You share the image layer across different repositories.
-
-Only layers that are referenced by tagged images are accounted for. Untagged images and any layers
-referenced exclusively by them are subject to [online garbage collection](packages/container_registry/delete_container_registry_images.md#garbage-collection).
-Untagged image layers are automatically deleted after 24 hours if they remain unreferenced during that period.
-
-Image layers are stored on the storage backend in the original (usually compressed) format. This
-means that the measured size for any given image layer should match the size displayed on the
-corresponding [image manifest](https://github.com/opencontainers/image-spec/blob/main/manifest.md#example-image-manifest).
-
-Namespace usage is refreshed a few minutes after a tag is pushed or deleted from any container repository under the namespace.
-
-#### Delayed refresh
-
-It is not possible to calculate [container registry usage](#container-registry-usage)
-with maximum precision in real time for extremely large namespaces (about 1% of namespaces).
-To enable maintainers of these namespaces to see their usage, there is a delayed fallback mechanism.
-See [epic 9413](https://gitlab.com/groups/gitlab-org/-/epics/9413) for more details.
-
-If the usage for a namespace cannot be calculated with precision, GitLab falls back to the delayed method.
-In the delayed method, the displayed usage size is the sum of **all** unique image layers
-in the namespace. Untagged image layers are not ignored. As a result,
-the displayed usage size might not change significantly after deleting tags. Instead,
-the size value only changes when:
-
-- An automated [garbage collection process](packages/container_registry/delete_container_registry_images.md#garbage-collection)
- runs and deletes untagged image layers. After a user deletes a tag, a garbage collection run
- is scheduled to start 24 hours later. During that run, images that were previously tagged
- are analyzed and their layers deleted if not referenced by any other tagged image.
- If any layers are deleted, the namespace usage is updated.
-- The namespace's registry usage shrinks enough that GitLab can measure it with maximum precision.
- As usage for namespaces shrinks to be under the [limits](#namespace-storage-limit),
- the measurement switches automatically from delayed to precise usage measurement.
- There is no place in the UI to determine which measurement method is being used,
- but [issue 386468](https://gitlab.com/gitlab-org/gitlab/-/issues/386468) proposes to improve this.
+A cost factor is applied to the storage consumed by project forks so that forks consume less namespace storage than their actual size.
-### Storage usage statistics
+To view the amount of namespace storage the fork has used:
-> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68898) project-level graph in GitLab 14.4 [with a flag](../administration/feature_flags.md) named `project_storage_ui`. Disabled by default.
-> - Enabled on GitLab.com in GitLab 14.4.
-> - Enabled on self-managed in GitLab 14.5.
-> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/71270) in GitLab 14.5.
+1. On the left sidebar, select **Search or go to** and find your project or group.
+1. On the left sidebar, select **Settings > Usage Quotas**.
+1. Select the **Storage** tab. The **Total** column displays the amount of namespace storage used by the fork as a portion of the actual size of the fork on disk.
-The following storage usage statistics are available to a maintainer:
+The cost factor applies to the project repository, LFS objects, job artifacts, packages, snippets, and the wiki.
-- Total namespace storage used: Total amount of storage used across projects in this namespace.
-- Total excess storage used: Total amount of storage used that exceeds their allocated storage.
-- Purchased storage available: Total storage that has been purchased but is not yet used.
+The cost factor does not apply to private forks in namespaces on the Free plan.
-## Manage your storage usage
+## Manage storage usage
To manage your storage, if you are a namespace Owner you can [purchase more storage for the namespace](../subscriptions/gitlab_com/index.md#purchase-more-storage-and-transfer).
@@ -126,14 +72,16 @@ Depending on your role, you can also use the following methods to manage or redu
To automate storage usage analysis and management, see the [storage management automation](storage_management_automation.md) documentation.
-## Manage your transfer usage
+## Set usage quotas **(FREE SELF)**
+
+There are no application limits on the amount of storage and transfer for self-managed instances. The administrators are responsible for the underlying infrastructure costs. Administrators can set [repository size limits](../administration/settings/account_and_limit_settings.md#repository-size-limit) to manage your repositories’ size.
-Depending on your role, to manage your transfer usage you can [reduce Container Registry data transfers](packages/container_registry/reduce_container_registry_data_transfer.md).
+## Storage limits **(FREE SAAS)**
-## Project storage limit
+### Project storage limit
-Projects on GitLab SaaS have a 10 GiB storage limit on their Git repository and LFS storage.
-After namespace-level storage limits are applied, the project limit is removed. A namespace has either a namespace-level storage limit or a project-level storage limit, but not both.
+Projects on GitLab SaaS have a 10 GiB storage limit on their Git repository and LFS storage. Limits on project storage
+will be removed before limits are applied to GitLab SaaS namespace storage in the future.
When a project's repository and LFS reaches the quota, the project is set to a read-only state.
You cannot push changes to a read-only project. To monitor the size of each
@@ -141,7 +89,7 @@ repository in a namespace, including a breakdown for each project,
[view storage usage](#view-storage-usage). To allow a project's repository and LFS to exceed the free quota
you must purchase additional storage. For more details, see [Excess storage usage](#excess-storage-usage).
-### Excess storage usage
+#### Excess storage usage
Excess storage usage is the amount that a project's repository and LFS exceeds the [project storage limit](#project-storage-limit). If no
purchased storage is available the project is set to a read-only state. You cannot push changes to a read-only project.
@@ -185,12 +133,19 @@ available decreases. All projects no longer have the read-only status because 40
| Yellow | 5 GiB | 0 GiB | 10 GiB | Not read-only |
| **Totals** | **45 GiB** | **10 GiB** | - | - |
-## Namespace storage limit
+### Namespace storage limit **(FREE SAAS)**
-Namespaces on GitLab SaaS have a storage limit. For more information, see our [pricing page](https://about.gitlab.com/pricing/).
+GitLab plans to enforce a storage limit for namespaces on GitLab SaaS. For more information, see
+the FAQs for the following tiers:
-After namespace storage limits are enforced, view them in the **Usage quotas** page.
-For more information about the namespace storage limit enforcement, see the FAQ pages for the [Free](https://about.gitlab.com/pricing/faq-efficient-free-tier/#storage-limits-on-gitlab-saas-free-tier) and [Paid](https://about.gitlab.com/pricing/faq-paid-storage-transfer/) tiers.
+- [Free tier](https://about.gitlab.com/pricing/faq-efficient-free-tier/#storage-limits-on-gitlab-saas-free-tier).
+- [Premium and Ultimate](https://about.gitlab.com/pricing/faq-paid-storage-transfer/).
+
+Namespaces on GitLab SaaS have a [10 GiB project limit](#project-storage-limit) with a soft limit on
+namespace storage. Soft storage limits are limits that have not yet been enforced by GitLab, and will become
+hard limits after namespace storage limits apply. To avoid your namespace from becoming
+[read-only](../user/read_only_namespaces.md) after namespace storage limits apply,
+you should ensure that your namespace storage adheres to the soft storage limit.
Namespace storage limits do not apply to self-managed deployments, but administrators can [manage the repository size](../administration/settings/account_and_limit_settings.md#repository-size-limit).
@@ -209,13 +164,13 @@ If your total namespace storage exceeds the available namespace storage quota, a
To notify you that you have nearly exceeded your namespace storage quota:
-- In the command-line interface, a notification displays after each `git push` action when you've reached 95% and 100% of your namespace storage quota.
-- In the GitLab UI, a notification displays when you've reached 75%, 95%, and 100% of your namespace storage quota.
+- In the command-line interface, a notification displays after each `git push` action when your namespace has reached between 95% and 100%+ of your namespace storage quota.
+- In the GitLab UI, a notification displays when your namespace has reached between 75% and 100%+ of your namespace storage quota.
- GitLab sends an email to members with the Owner role to notify them when namespace storage usage is at 70%, 85%, 95%, and 100%.
To prevent exceeding the namespace storage limit, you can:
-- [Manage your storage usage](#manage-your-storage-usage).
+- [Manage your storage usage](#manage-storage-usage).
- If you meet the eligibility requirements, you can apply for:
- [GitLab for Education](https://about.gitlab.com/solutions/education/join/)
- [GitLab for Open Source](https://about.gitlab.com/solutions/open-source/join/)
@@ -225,16 +180,8 @@ To prevent exceeding the namespace storage limit, you can:
- [Start a trial](https://about.gitlab.com/free-trial/) or [upgrade to GitLab Premium or Ultimate](https://about.gitlab.com/pricing/), which include higher limits and features to enable growing teams to ship faster without sacrificing on quality.
- [Talk to an expert](https://page.gitlab.com/usage_limits_help.html) for more information about your options.
-### View project fork storage usage
-
-A cost factor is applied to the storage consumed by project forks so that forks consume less namespace storage than their actual size.
-
-To view the amount of namespace storage the fork has used:
-
-1. On the left sidebar, select **Search or go to** and find your project or group.
-1. On the left sidebar, select **Settings > Usage Quotas**.
-1. Select the **Storage** tab. The **Total** column displays the amount of namespace storage used by the fork as a portion of the actual size of the fork on disk.
-
-The cost factor applies to the project repository, LFS objects, job artifacts, packages, snippets, and the wiki.
+## Related Topics
-The cost factor does not apply to private forks in namespaces on the Free plan.
+- [Automate storage management](storage_management_automation.md)
+- [Purchase storage and transfer](../subscriptions/gitlab_com/index.md#purchase-more-storage-and-transfer)
+- [Transfer usage](packages/container_registry/reduce_container_registry_data_transfer.md)
diff --git a/doc/user/workspace/index.md b/doc/user/workspace/index.md
index 1284067a391..21905381577 100644
--- a/doc/user/workspace/index.md
+++ b/doc/user/workspace/index.md
@@ -95,18 +95,28 @@ Only these properties are relevant to the GitLab implementation of the `containe
| `endpoints` | Port mappings to expose from the container. |
| `volumeMounts` | Storage volume to mount in the container. |
+### Using variables in a devfile
+
+You can define variables to use in your devfile.
+The `variables` object is a map of name-value pairs that you can use for string replacement in the devfile.
+
+Variables cannot have names that start with `gl-`, `gl_`, `GL-`, or `GL_`.
+For more information about how and where to use variables, see the [devfile documentation](https://devfile.io/docs/2.2.0/defining-variables).
+
### Example configurations
The following is an example devfile configuration:
```yaml
schemaVersion: 2.2.0
+variables:
+ registry-root: registry.gitlab.com
components:
- name: tooling-container
attributes:
gl/inject-editor: true
container:
- image: registry.gitlab.com/gitlab-org/remote-development/gitlab-remote-development-docs/debian-bullseye-ruby-3.2-node-18.12:rubygems-3.4-git-2.33-lfs-2.9-yarn-1.22-graphicsmagick-1.3.36-gitlab-workspaces
+ image: "{{registry-root}}/gitlab-org/remote-development/gitlab-remote-development-docs/debian-bullseye-ruby-3.2-node-18.12:rubygems-3.4-git-2.33-lfs-2.9-yarn-1.22-graphicsmagick-1.3.36-gitlab-workspaces"
env:
- name: KEY
value: VALUE
diff --git a/fixtures/emojis/digests.json b/fixtures/emojis/digests.json
index 4b8a74d62e5..8cd980df3ec 100644
--- a/fixtures/emojis/digests.json
+++ b/fixtures/emojis/digests.json
@@ -1,199 +1,409 @@
{
- "100": {
- "category": "symbols",
- "moji": "💯",
- "description": "hundred points symbol",
+ "grinning": {
+ "category": "people",
+ "moji": "😀",
+ "description": "grinning face",
+ "unicodeVersion": "6.1",
+ "digest": "c83774596b63aed388259582de228aab02f912bc79200e6368a7926df2a1fad8"
+ },
+ "smiley": {
+ "category": "people",
+ "moji": "😃",
+ "description": "smiling face with open mouth",
"unicodeVersion": "6.0",
- "digest": "66b1338bd0bc6efa35033dec6d9f8962c98f1eda08e50bae6a319e7f069ae861"
+ "digest": "9b0f2fca8ba5bb1b3de39686302f2f9ef7e1c93d4af47c71828931f874bd4db1"
},
- "1234": {
- "category": "symbols",
- "moji": "🔢",
- "description": "input symbol for numbers",
+ "smile": {
+ "category": "people",
+ "moji": "😄",
+ "description": "smiling face with open mouth and smiling eyes",
"unicodeVersion": "6.0",
- "digest": "6f276a9127f2de22f508978bd645974d5e21dfd3cf138e0cb00f33599677b533"
+ "digest": "fb06bf4088887ca1aadbc0201b63d75f3d2b5b5779bd81f1767f17e794b0c0a7"
},
- "8ball": {
- "category": "activity",
- "moji": "🎱",
- "description": "billiards",
+ "grin": {
+ "category": "people",
+ "moji": "😁",
+ "description": "grinning face with smiling eyes",
"unicodeVersion": "6.0",
- "digest": "de5dbbd700f078ed6d780e79cb1b5b3214180b42a38917b3e1222af731cf5e3d"
+ "digest": "15b73c02a8456b4b41164090c27409606a60440bbe1a1932ee58702ecacabcbe"
},
- "a": {
- "category": "symbols",
- "moji": "🅰",
- "description": "negative squared latin capital letter a",
+ "laughing": {
+ "category": "people",
+ "moji": "😆",
+ "description": "smiling face with open mouth and tightly-closed ey",
"unicodeVersion": "6.0",
- "digest": "3a5aea7fbabb9e1a5e364f937704fd21296323470fdb1e2bf767d07516c94d21"
+ "digest": "43f119b4cac94c33c49e35381710d74d4f81883364ade30088cd92e5130287e1"
},
- "ab": {
- "category": "symbols",
- "moji": "🆎",
- "description": "negative squared ab",
+ "sweat_smile": {
+ "category": "people",
+ "moji": "😅",
+ "description": "smiling face with open mouth and cold sweat",
"unicodeVersion": "6.0",
- "digest": "2a58932a5ab57aa3c82f77e5df5cd9ce5103667483bb93a6c96f31e171577654"
+ "digest": "18e9821a9dd3f90342ed952660654ddbb8e46671b5e95ab88df637406b6cc0fb"
},
- "abc": {
- "category": "symbols",
- "moji": "🔤",
- "description": "input symbol for latin letters",
+ "rofl": {
+ "category": "people",
+ "moji": "🤣",
+ "description": "rolling on the floor laughing",
+ "unicodeVersion": "9.0",
+ "digest": "1a997e5e1a86c52ced7f4685ad6eb6ce93d50aef0b4cde72f143dd75e5139b43"
+ },
+ "joy": {
+ "category": "people",
+ "moji": "😂",
+ "description": "face with tears of joy",
"unicodeVersion": "6.0",
- "digest": "9991fd68e58377848e6e1b8d4b74bdbfd09e575686047651f139c126e9df6c4c"
+ "digest": "3c7d20273bbe976dc8cf8d5cf44ac4cb9c71b02ec358b50427e9d0662e67a557"
},
- "abcd": {
- "category": "symbols",
- "moji": "🔡",
- "description": "input symbol for latin small letters",
+ "slight_smile": {
+ "category": "people",
+ "moji": "🙂",
+ "description": "slightly smiling face",
+ "unicodeVersion": "7.0",
+ "digest": "04feb9e847c67936ddd0e40d6dd6c90333abc9bfbd81fae7fef9bd1e5265ba9e"
+ },
+ "upside_down": {
+ "category": "people",
+ "moji": "🙃",
+ "description": "upside-down face",
+ "unicodeVersion": "8.0",
+ "digest": "3211b742f7fefdca6b0e817fb45070d6a306e3d31debcfd11d006ea24ba08983"
+ },
+ "wink": {
+ "category": "people",
+ "moji": "😉",
+ "description": "winking face",
"unicodeVersion": "6.0",
- "digest": "ad9982f0e3f4c346a6536d8b2a65c625f05278510910f656e59a6d280a90d3c2"
+ "digest": "a9746d44d7fd9f51c0b0329aeb9eaa438e4690162d6c82c482ec3f4bc2def8b9"
},
- "accept": {
- "category": "symbols",
- "moji": "🉑",
- "description": "circled ideograph accept",
+ "blush": {
+ "category": "people",
+ "moji": "😊",
+ "description": "smiling face with smiling eyes",
"unicodeVersion": "6.0",
- "digest": "d4dcdfdb5dfcd5374044568d879662e89bb5269fb789901e5468c07243f32143"
+ "digest": "3d7d115f7da861e3565a446d9aea177c7bc27592bac17d0d96d815d853b68467"
},
- "aerial_tramway": {
- "category": "travel",
- "moji": "🚡",
- "description": "aerial tramway",
+ "innocent": {
+ "category": "people",
+ "moji": "😇",
+ "description": "smiling face with halo",
"unicodeVersion": "6.0",
- "digest": "716dae206b786d985ddfa6b311369a708d00539f35b2612500afb19ac537261d"
+ "digest": "3571bdd00112793ecf4ade131f2e50b6234b6f794fcb799e6dadacbb4106a92d"
},
- "airplane": {
- "category": "travel",
- "moji": "✈",
- "description": "airplane",
- "unicodeVersion": "1.1",
- "digest": "b223b20d905ace04c602f7fcf22eb66c8defb22f2589434a83ae39e1622dcc67"
+ "heart_eyes": {
+ "category": "people",
+ "moji": "😍",
+ "description": "smiling face with heart-shaped eyes",
+ "unicodeVersion": "6.0",
+ "digest": "997c08afa77ab1bd0b08ae58854024286669ce94385c2a5c7bcab02d219a3667"
},
- "airplane_arriving": {
- "category": "travel",
- "moji": "🛬",
- "description": "airplane arriving",
- "unicodeVersion": "7.0",
- "digest": "a8ea037bc27226bd7e7ce07fdcedfcc74d0cd9c99737c93f2a588066c01fe1fe"
+ "kissing_heart": {
+ "category": "people",
+ "moji": "😘",
+ "description": "face throwing a kiss",
+ "unicodeVersion": "6.0",
+ "digest": "6dd07e9fa9892aec92ba42b78fe23646d31701fb1d29f968ce5cf7f3c5f1336e"
},
- "airplane_departure": {
- "category": "travel",
- "moji": "🛫",
- "description": "airplane departure",
- "unicodeVersion": "7.0",
- "digest": "c371879cd5b6bd5df2954f5c6c7eb5b07b3358253e8b35cd806ca21e2af2d794"
+ "kissing": {
+ "category": "people",
+ "moji": "😗",
+ "description": "kissing face",
+ "unicodeVersion": "6.1",
+ "digest": "9339112fdb5a89aca2b8baed88215ba09c698487713ea6c3a4906d6370ba8a8e"
},
- "airplane_small": {
- "category": "travel",
- "moji": "🛩",
- "description": "small airplane",
- "unicodeVersion": "7.0",
- "digest": "ad2f6b4f6f141bd184c743c31fa0eadbef7653f1915b410c8144293dbb4b3720"
+ "relaxed": {
+ "category": "people",
+ "moji": "☺",
+ "description": "white smiling face",
+ "unicodeVersion": "1.1",
+ "digest": "27bb85737e7f969e392a23141f27c75c82e327fcd0614a445dec47a00578057d"
},
- "alarm_clock": {
- "category": "objects",
- "moji": "⏰",
- "description": "alarm clock",
+ "kissing_closed_eyes": {
+ "category": "people",
+ "moji": "😚",
+ "description": "kissing face with closed eyes",
"unicodeVersion": "6.0",
- "digest": "b125863048df0f332c2af68df61d919d3ff61863bced1aca9269759aff4dfe10"
+ "digest": "0a58401451a4c7daad884fbcc0343f6d08efc5ce4f97cb9c455019ed57b7a979"
},
- "alembic": {
- "category": "objects",
- "moji": "⚗",
- "description": "alembic",
- "unicodeVersion": "4.1",
- "digest": "9af1181b6190b06ed4fd78d13c64d17c16a59f9b1b512fb0b92a9be9cfb92a2b"
+ "kissing_smiling_eyes": {
+ "category": "people",
+ "moji": "😙",
+ "description": "kissing face with smiling eyes",
+ "unicodeVersion": "6.1",
+ "digest": "75b7829612e5e0a3c96c33cb3add78892ef8fb2012b95d24bb9e45888091648e"
},
- "alien": {
+ "yum": {
"category": "people",
- "moji": "👽",
- "description": "extraterrestrial alien",
+ "moji": "😋",
+ "description": "face savouring delicious food",
"unicodeVersion": "6.0",
- "digest": "77ecf901092da0e96501eb86281f2d85d5c813dba15dbaf490f024a835c8da58"
+ "digest": "796badd831c75797cd4acb88694d3bf19b2727678b3c2e63e465e4d8125e4ad4"
},
- "ambulance": {
- "category": "travel",
- "moji": "🚑",
- "description": "ambulance",
+ "stuck_out_tongue": {
+ "category": "people",
+ "moji": "😛",
+ "description": "face with stuck-out tongue",
+ "unicodeVersion": "6.1",
+ "digest": "04df5c3e122e85ebafea184a808d090cebe8fda6c08ab08bb756a21d43b6661f"
+ },
+ "stuck_out_tongue_winking_eye": {
+ "category": "people",
+ "moji": "😜",
+ "description": "face with stuck-out tongue and winking eye",
"unicodeVersion": "6.0",
- "digest": "1ca176a46c2f020e0386e7a0ab662711f66af1683ae165c06853ec5b6bcc4cf1"
+ "digest": "73443f4962da500d4ebe32abf7a9d95a217fa3f58df5567f8ac623b439f8b265"
},
- "amphora": {
- "category": "objects",
- "moji": "🏺",
- "description": "amphora",
- "unicodeVersion": "8.0",
- "digest": "ce9f7d0bd6b4d04c033eb2b3b8c2d339e4a8d19ef46cb23e500cd2854578d4e5"
+ "stuck_out_tongue_closed_eyes": {
+ "category": "people",
+ "moji": "😝",
+ "description": "face with stuck-out tongue and tightly-closed eyes",
+ "unicodeVersion": "6.0",
+ "digest": "88bceb40811057945decca24c3fb69f5703a15417dd5bf16787019e9067cb125"
},
- "anchor": {
- "category": "travel",
- "moji": "⚓",
- "description": "anchor",
- "unicodeVersion": "4.1",
- "digest": "b5f2eacb26d6e550286eb7c01beb0bd5072baf33f6c68c8d7110f634a8b2fdf6"
+ "money_mouth": {
+ "category": "people",
+ "moji": "🤑",
+ "description": "money-mouth face",
+ "unicodeVersion": "8.0",
+ "digest": "99ba4973b84ecb2dbf7e6303190c22c67eedf750f49313135ddbe8e541650688"
},
- "angel": {
+ "hugging": {
"category": "people",
- "moji": "👼",
- "description": "baby angel",
- "unicodeVersion": "6.0",
- "digest": "93d8abd48b9a0eac8332ed79e1f95c206dd29895e74d31931db32f4feab664fb"
+ "moji": "🤗",
+ "description": "hugging face",
+ "unicodeVersion": "8.0",
+ "digest": "c52e3522e798301a973ab2e8829ba85d50b428ac853dbe096dd09a81d2fc5b29"
},
- "angel_tone1": {
+ "thinking": {
"category": "people",
- "moji": "👼🏻",
- "description": "baby angel tone 1",
+ "moji": "🤔",
+ "description": "thinking face",
"unicodeVersion": "8.0",
- "digest": "032faac5736197bb9a75da5743891334264d278378f8510b40623c02bc597601"
+ "digest": "2b2d2b844f147e1be7f4c9019c54ce1b96561b4a8e5bd0af9c8d955b3ceabefa"
},
- "angel_tone2": {
+ "zipper_mouth": {
"category": "people",
- "moji": "👼🏼",
- "description": "baby angel tone 2",
+ "moji": "🤐",
+ "description": "zipper-mouth face",
"unicodeVersion": "8.0",
- "digest": "2f37a1d960eba5ada71b166c4371fdeb69f560f57759de1b24984d0848f74c68"
+ "digest": "dfeeb9947458d1bb04805e46211d4f29aa89210239c475425ac04a1cef340701"
},
- "angel_tone3": {
+ "neutral_face": {
"category": "people",
- "moji": "👼🏽",
- "description": "baby angel tone 3",
+ "moji": "😐",
+ "description": "neutral face",
+ "unicodeVersion": "6.0",
+ "digest": "d69ad475b00bc3770047b0d3e06ebd1f3b6523c285d80402c233bf42fe967e8e"
+ },
+ "expressionless": {
+ "category": "people",
+ "moji": "😑",
+ "description": "expressionless face",
+ "unicodeVersion": "6.1",
+ "digest": "d818ca9cf4ba0c02756559d0e870517f298a88466b3e27002a86389942d89145"
+ },
+ "no_mouth": {
+ "category": "people",
+ "moji": "😶",
+ "description": "face without mouth",
+ "unicodeVersion": "6.0",
+ "digest": "47a0110f84c97673d86cca26854505e47b0e94af996e23eff58e3861baca4b43"
+ },
+ "smirk": {
+ "category": "people",
+ "moji": "😏",
+ "description": "smirking face",
+ "unicodeVersion": "6.0",
+ "digest": "e02911a76fe7c40dde28998741f201789b7ab5c6be6e5168e4eddbd9886ef790"
+ },
+ "unamused": {
+ "category": "people",
+ "moji": "😒",
+ "description": "unamused face",
+ "unicodeVersion": "6.0",
+ "digest": "68eaad1164a9cfdcfb28e6e247ab733d0698a4ddd8f9c72add082d28d3a74445"
+ },
+ "rolling_eyes": {
+ "category": "people",
+ "moji": "🙄",
+ "description": "face with rolling eyes",
"unicodeVersion": "8.0",
- "digest": "96fb1d8e568752aed2c716a062ef650cf4ccca609fdf78cde2e81e3e8f86a098"
+ "digest": "a5fec5606c1cd4b295fe69c261326c848e246622c02ca6cda9f2c5a5bf0aed98"
},
- "angel_tone4": {
+ "grimacing": {
"category": "people",
- "moji": "👼🏾",
- "description": "baby angel tone 4",
+ "moji": "😬",
+ "description": "grimacing face",
+ "unicodeVersion": "6.1",
+ "digest": "781a6548b4b6e394bfd08d57ad222def3eb28d1f1743c9f305296b6108a93958"
+ },
+ "lying_face": {
+ "category": "people",
+ "moji": "🤥",
+ "description": "lying face",
+ "unicodeVersion": "9.0",
+ "digest": "b7a8bcad9036fa6c0441bbc0558cf6ca32464db8ab7d522af505deb1a07623b3"
+ },
+ "relieved": {
+ "category": "people",
+ "moji": "😌",
+ "description": "relieved face",
+ "unicodeVersion": "6.0",
+ "digest": "ed96de2532a1fd5f96f52621d233b391830d31119b9604e7505340aac2dd1fa5"
+ },
+ "pensive": {
+ "category": "people",
+ "moji": "😔",
+ "description": "pensive face",
+ "unicodeVersion": "6.0",
+ "digest": "9da78949740dfdb9c72127f5b7aef68c3b0ba1e5462f5cefcd4d22bdbc4857f9"
+ },
+ "sleepy": {
+ "category": "people",
+ "moji": "😪",
+ "description": "sleepy face",
+ "unicodeVersion": "6.0",
+ "digest": "afc0c40fb97bd1fe79e828f76f03aa08beeed09b42e307b5053758d9889fcc01"
+ },
+ "drooling_face": {
+ "category": "people",
+ "moji": "🤤",
+ "description": "drooling face",
+ "unicodeVersion": "9.0",
+ "digest": "bed3de639ae375a5683806f5661cda66790f0e991912044ec6d8bcdf6ab56b55"
+ },
+ "sleeping": {
+ "category": "people",
+ "moji": "😴",
+ "description": "sleeping face",
+ "unicodeVersion": "6.1",
+ "digest": "061017b6fea9012cdfc7f90ab5dbf18a55830743fdd062f1ea0a085f52e0a564"
+ },
+ "mask": {
+ "category": "people",
+ "moji": "😷",
+ "description": "face with medical mask",
+ "unicodeVersion": "6.0",
+ "digest": "20b1988145e75b2ba72f5c595245fc5574315ee8c26fd39f7785c0a6fc5a9906"
+ },
+ "thermometer_face": {
+ "category": "people",
+ "moji": "🤒",
+ "description": "face with thermometer",
"unicodeVersion": "8.0",
- "digest": "b17721fd657278ee719963b0df074a9290c52cf58d094ed2722881030f57a79e"
+ "digest": "8300d80af44461b1da2aeed90203901753705ec3418a288646a00dc59da70c93"
},
- "angel_tone5": {
+ "head_bandage": {
"category": "people",
- "moji": "👼🏿",
- "description": "baby angel tone 5",
+ "moji": "🤕",
+ "description": "face with head-bandage",
"unicodeVersion": "8.0",
- "digest": "c6ebaa89eb3b2e6ee6e16b483587a6f6b341cae187d282e27cf71d1a18117735"
+ "digest": "53fef09c38e83bc82c90bd3d63e01767767d2f25cec1cd5a6742e3be8cb0ef12"
},
- "anger": {
- "category": "symbols",
- "moji": "💢",
- "description": "anger symbol",
+ "nauseated_face": {
+ "category": "people",
+ "moji": "🤢",
+ "description": "nauseated face",
+ "unicodeVersion": "9.0",
+ "digest": "3b3f3fe5fdd6aa6e30bf433d3533438cbee50d337fb2ad83b154873aaac0d9d1"
+ },
+ "sneezing_face": {
+ "category": "people",
+ "moji": "🤧",
+ "description": "sneezing face",
+ "unicodeVersion": "9.0",
+ "digest": "fa08b2714d529efb670662a65b19201333217d31152a1e4c48b3ee7ab4398eb5"
+ },
+ "dizzy_face": {
+ "category": "people",
+ "moji": "😵",
+ "description": "dizzy face",
"unicodeVersion": "6.0",
- "digest": "f63add6c0e9483fb1ee5d2fffdc6e0a818b233713a6da85f6ce1ec3da0a7049a"
+ "digest": "5e3c7e1f97d4d9a330b89358c89d2fbdf31a945f3be40c352600a985ac49b197"
},
- "anger_right": {
- "category": "symbols",
- "moji": "🗯",
- "description": "right anger bubble",
+ "cowboy": {
+ "category": "people",
+ "moji": "🤠",
+ "description": "face with cowboy hat",
+ "unicodeVersion": "9.0",
+ "digest": "0c3a81e8bc276a84073ae94db2cf08d378b3e17ca09e2e03abf248c66c00f34b"
+ },
+ "sunglasses": {
+ "category": "people",
+ "moji": "😎",
+ "description": "smiling face with sunglasses",
+ "unicodeVersion": "6.0",
+ "digest": "1b2ba362ef41c55b05bf8d28df5508e8f4f2b0418c22c47ecd9e8e772ac1c19b"
+ },
+ "nerd": {
+ "category": "people",
+ "moji": "🤓",
+ "description": "nerd face",
+ "unicodeVersion": "8.0",
+ "digest": "da428d87fe165911944fb1a15ef2fa4859a1c181fc4dc712907eaea7daba1c85"
+ },
+ "confused": {
+ "category": "people",
+ "moji": "😕",
+ "description": "confused face",
+ "unicodeVersion": "6.1",
+ "digest": "4b8a05e1d84cb6314b217fdd4f89065411021f3ed3f5a824dbf538d54b5c3132"
+ },
+ "worried": {
+ "category": "people",
+ "moji": "😟",
+ "description": "worried face",
+ "unicodeVersion": "6.1",
+ "digest": "66814ad2b00574ed539af543224116a3564b162b57e9d02d58f19513cb2c80f2"
+ },
+ "slight_frown": {
+ "category": "people",
+ "moji": "🙁",
+ "description": "slightly frowning face",
"unicodeVersion": "7.0",
- "digest": "f47446e92c821d4f4d1d9861b99b3b3fff69ef52364c0aea3d99dc8e5fa40c3e"
+ "digest": "2bccd273d6445ddf54366b9aa565370af3110b7722cb9a85e76534c729b397b8"
},
- "angry": {
+ "frowning2": {
"category": "people",
- "moji": "😠",
- "description": "angry face",
+ "moji": "☹",
+ "description": "white frowning face",
+ "unicodeVersion": "1.1",
+ "digest": "aa6b9f39cd2511d918a395f1e363a34642761cdc7f5a0170405b40455a47f0b6"
+ },
+ "open_mouth": {
+ "category": "people",
+ "moji": "😮",
+ "description": "face with open mouth",
+ "unicodeVersion": "6.1",
+ "digest": "346f4923115965b864ef63bfc4b34deaadaa2dcbc965ef6285a1b81f9966b9a6"
+ },
+ "hushed": {
+ "category": "people",
+ "moji": "😯",
+ "description": "hushed face",
+ "unicodeVersion": "6.1",
+ "digest": "a1b0d468e68dff4b3ab40b5980c838635e9680cfb35592ee62260b597cc8b551"
+ },
+ "astonished": {
+ "category": "people",
+ "moji": "😲",
+ "description": "astonished face",
"unicodeVersion": "6.0",
- "digest": "f4fb17bf4cacd87fefdf9f235a9f6d77a5b12514061bc277645bccd410d3720a"
+ "digest": "ecfb4bc1b9617e8de75b5c7b97e09e97e7bac08de23d4e76ce27b3c04277ab0e"
+ },
+ "flushed": {
+ "category": "people",
+ "moji": "😳",
+ "description": "flushed face",
+ "unicodeVersion": "6.0",
+ "digest": "d29c62b5892744d9d95cd876c703d079f1e8eed6091e51291e01667b8f4a6c7a"
+ },
+ "frowning": {
+ "category": "people",
+ "moji": "😦",
+ "description": "frowning face with open mouth",
+ "unicodeVersion": "6.1",
+ "digest": "615dc050d755f9fb9aa325fba6efc812ec78984dd282c8e81e4b0bf2ffb00b79"
},
"anguished": {
"category": "people",
@@ -202,838 +412,2497 @@
"unicodeVersion": "6.1",
"digest": "b0670af3e3d615d03f5e20b371b6ca7852583f5ac0b272018ac71cc67096dc56"
},
- "ant": {
- "category": "nature",
- "moji": "🐜",
- "description": "ant",
+ "fearful": {
+ "category": "people",
+ "moji": "😨",
+ "description": "fearful face",
"unicodeVersion": "6.0",
- "digest": "d60a32588453dd0a17b55358089bc62e53f09fc262c7aa30a1d6fc2c1e7cde2c"
+ "digest": "ce0c48c3ede7231acd645ffa0fe3dfec012e6f88a30431c44e3b816063ac42b4"
},
- "apple": {
- "category": "food",
- "moji": "🍎",
- "description": "red apple",
+ "cold_sweat": {
+ "category": "people",
+ "moji": "😰",
+ "description": "face with open mouth and cold sweat",
"unicodeVersion": "6.0",
- "digest": "2a2d7a1fb558b2f77f9ef57b11229992a6ba4a1a679707f6339fc47744d64df2"
+ "digest": "d96b966a52919667857c96c5d03596cb29daa1a7d87acba0286556f5dcaa25af"
},
- "aquarius": {
- "category": "symbols",
- "moji": "♒",
- "description": "aquarius",
- "unicodeVersion": "1.1",
- "digest": "a40fb6ccb866eaf296e083c57f46ca29f3d9732de897e62a8201481b526ae6b9"
+ "disappointed_relieved": {
+ "category": "people",
+ "moji": "😥",
+ "description": "disappointed but relieved face",
+ "unicodeVersion": "6.0",
+ "digest": "cfe92ebfbaaa0b02b84b05c17c94f07db315a8771d3ebfea8043862559c6ce74"
},
- "aries": {
- "category": "symbols",
- "moji": "♈",
- "description": "aries",
- "unicodeVersion": "1.1",
- "digest": "4e35bd481a7c73be42faba7afa67c522b4340180efa18f0523e394a90a2944aa"
+ "cry": {
+ "category": "people",
+ "moji": "😢",
+ "description": "crying face",
+ "unicodeVersion": "6.0",
+ "digest": "5b16d711fc3d2e611dbd406e5849c22ebdad9c9812fd6897f304a89f21f05755"
},
- "arrow_backward": {
- "category": "symbols",
- "moji": "◀",
- "description": "black left-pointing triangle",
+ "sob": {
+ "category": "people",
+ "moji": "😭",
+ "description": "loudly crying face",
+ "unicodeVersion": "6.0",
+ "digest": "2bd275f629a26cb40ce648eff68155a5625e944ed724b8a6d2890a80a099503a"
+ },
+ "scream": {
+ "category": "people",
+ "moji": "😱",
+ "description": "face screaming in fear",
+ "unicodeVersion": "6.0",
+ "digest": "3403d66a449c643d1dbc3029d11bf9a9edfd503b5594b524517356a8eeef296e"
+ },
+ "confounded": {
+ "category": "people",
+ "moji": "😖",
+ "description": "confounded face",
+ "unicodeVersion": "6.0",
+ "digest": "084da8b9e9e24eaee418df200ed9369e04fded381d756382b34f96a362d2c93c"
+ },
+ "persevere": {
+ "category": "people",
+ "moji": "😣",
+ "description": "persevering face",
+ "unicodeVersion": "6.0",
+ "digest": "5297ca44798cb08da3651322d7f34d45e88333f820cfcd9cce4a540b3333ca63"
+ },
+ "disappointed": {
+ "category": "people",
+ "moji": "😞",
+ "description": "disappointed face",
+ "unicodeVersion": "6.0",
+ "digest": "0e8f9fdef204d2684a92666709ff4d785d5b3690f1ff0f7ebed9f37d2a3651f5"
+ },
+ "sweat": {
+ "category": "people",
+ "moji": "😓",
+ "description": "face with cold sweat",
+ "unicodeVersion": "6.0",
+ "digest": "54f6998fabdc88fd169a6c9013f6471608f29554dd304d3abe9ee246b4a0cb16"
+ },
+ "weary": {
+ "category": "people",
+ "moji": "😩",
+ "description": "weary face",
+ "unicodeVersion": "6.0",
+ "digest": "44fd697167f1403eaf6bc6778a394dee514f900a964bb9e7b6a45aac86a5d985"
+ },
+ "tired_face": {
+ "category": "people",
+ "moji": "😫",
+ "description": "tired face",
+ "unicodeVersion": "6.0",
+ "digest": "60d0656f21c7937c3f2e9c5a90d1dfd2deee068804fb17d813e8b6e9c9f994d5"
+ },
+ "triumph": {
+ "category": "people",
+ "moji": "😤",
+ "description": "face with look of triumph",
+ "unicodeVersion": "6.0",
+ "digest": "b258f96aa69a0c5bbe672097bb58d0b7bd6c1dfcc93e66f73d632c2a42c9ecc4"
+ },
+ "rage": {
+ "category": "people",
+ "moji": "😡",
+ "description": "pouting face",
+ "unicodeVersion": "6.0",
+ "digest": "55c5a1450a9c4ba539c4c2f6760209c7cedfbbc93abee597d63703e7bc96743c"
+ },
+ "angry": {
+ "category": "people",
+ "moji": "😠",
+ "description": "angry face",
+ "unicodeVersion": "6.0",
+ "digest": "f4fb17bf4cacd87fefdf9f235a9f6d77a5b12514061bc277645bccd410d3720a"
+ },
+ "smiling_imp": {
+ "category": "people",
+ "moji": "😈",
+ "description": "smiling face with horns",
+ "unicodeVersion": "6.0",
+ "digest": "7609669c056339bec4dc916c3b0fb56d4adc55d37b8c3e0fb078af59594500d9"
+ },
+ "imp": {
+ "category": "people",
+ "moji": "👿",
+ "description": "imp",
+ "unicodeVersion": "6.0",
+ "digest": "bf3ec6b5b728a98f16b630cea877fe3cef79e5ebe7e58ee0caec197279ed3a80"
+ },
+ "skull": {
+ "category": "people",
+ "moji": "💀",
+ "description": "skull",
+ "unicodeVersion": "6.0",
+ "digest": "ccf317cd63caa24cd1a008dd26cda83d6487a0a7fca71843e42715cd8cbaafc9"
+ },
+ "skull_crossbones": {
+ "category": "objects",
+ "moji": "☠",
+ "description": "skull and crossbones",
"unicodeVersion": "1.1",
- "digest": "c4e1ba32b806674ba5f7cdb1433d4b45cf9b31f546918aa5ad169c0c8af50a8a"
+ "digest": "81f050043fc49fb83d5e87753337f77fb2acd599e53432212de42ec58345d567"
},
- "arrow_double_down": {
- "category": "symbols",
- "moji": "⏬",
- "description": "black down-pointing double triangle",
+ "poop": {
+ "category": "people",
+ "moji": "💩",
+ "description": "pile of poo",
"unicodeVersion": "6.0",
- "digest": "079bfc85ed3a1b354a6c8d9d041ed2049c449a696a2a2d1ff11e65efc91bc420"
+ "digest": "b5c6a197435c518508edf1cc7bc015c14c120965b574813838797507fab21994"
},
- "arrow_double_up": {
+ "clown": {
+ "category": "people",
+ "moji": "🤡",
+ "description": "clown face",
+ "unicodeVersion": "9.0",
+ "digest": "4cabd73ae323f53200eb179e177ffbcc984f07847b71912f0c2874fa9c5e53f3"
+ },
+ "japanese_ogre": {
+ "category": "people",
+ "moji": "👹",
+ "description": "japanese ogre",
+ "unicodeVersion": "6.0",
+ "digest": "3ecbc95d1e43ebda0a0c9988e8dc012dc4985cdaa2b8e81a42cbf8cff30a93e3"
+ },
+ "japanese_goblin": {
+ "category": "people",
+ "moji": "👺",
+ "description": "japanese goblin",
+ "unicodeVersion": "6.0",
+ "digest": "4c5b8cfc3b172269a943341583e938cd1c8030e9de9fc9008ec0cfbca53d2f81"
+ },
+ "ghost": {
+ "category": "people",
+ "moji": "👻",
+ "description": "ghost",
+ "unicodeVersion": "6.0",
+ "digest": "02e92350f546b637c7070d01b71d84062b6cdc72cde79fce39b41c5945c5ff6c"
+ },
+ "alien": {
+ "category": "people",
+ "moji": "👽",
+ "description": "extraterrestrial alien",
+ "unicodeVersion": "6.0",
+ "digest": "77ecf901092da0e96501eb86281f2d85d5c813dba15dbaf490f024a835c8da58"
+ },
+ "space_invader": {
+ "category": "activity",
+ "moji": "👾",
+ "description": "alien monster",
+ "unicodeVersion": "6.0",
+ "digest": "84897a48330cb0ae9ac42111cfaa0e0baefb5c314cb49d1eae77c7ace3a7ab25"
+ },
+ "robot": {
+ "category": "people",
+ "moji": "🤖",
+ "description": "robot face",
+ "unicodeVersion": "8.0",
+ "digest": "363bacd1c9c3bb115d4fe363ac212fc0a81270c057aaf432ab866581b976e38d"
+ },
+ "smiley_cat": {
+ "category": "people",
+ "moji": "😺",
+ "description": "smiling cat face with open mouth",
+ "unicodeVersion": "6.0",
+ "digest": "eb6c8fa3e46a9ea9c0e79b3db5578299ea041792ae46c54c50799e5c3970c372"
+ },
+ "smile_cat": {
+ "category": "people",
+ "moji": "😸",
+ "description": "grinning cat face with smiling eyes",
+ "unicodeVersion": "6.0",
+ "digest": "5882f8784080c11ae3b95bccb4ecf00dacd127047ff76d3b4158fbba0ddb1f14"
+ },
+ "joy_cat": {
+ "category": "people",
+ "moji": "😹",
+ "description": "cat face with tears of joy",
+ "unicodeVersion": "6.0",
+ "digest": "fd65d87249121b7e1b1b48af53179ff8ccc7d5f072fcb07e498dc20e9370c436"
+ },
+ "heart_eyes_cat": {
+ "category": "people",
+ "moji": "😻",
+ "description": "smiling cat face with heart-shaped eyes",
+ "unicodeVersion": "6.0",
+ "digest": "9ea3bd6876a5833b702730a8b21ba0a20b3f95c64131e46414402ad485719069"
+ },
+ "smirk_cat": {
+ "category": "people",
+ "moji": "😼",
+ "description": "cat face with wry smile",
+ "unicodeVersion": "6.0",
+ "digest": "8aed1a44a0b0673c1f62cf9f77d89239725258b7b3b482b66b5d39c6306b601a"
+ },
+ "kissing_cat": {
+ "category": "people",
+ "moji": "😽",
+ "description": "kissing cat face with closed eyes",
+ "unicodeVersion": "6.0",
+ "digest": "e4c818629b8482ec9f3747125dbc4ea0c08ca12c151eb29f103e19d66ba39e78"
+ },
+ "scream_cat": {
+ "category": "people",
+ "moji": "🙀",
+ "description": "weary cat face",
+ "unicodeVersion": "6.0",
+ "digest": "e4d277a511c2e1edc5873579e78a94320133ff730502c1ebf36272e4a2e5c598"
+ },
+ "crying_cat_face": {
+ "category": "people",
+ "moji": "😿",
+ "description": "crying cat face",
+ "unicodeVersion": "6.0",
+ "digest": "32cc70455196cddbd0664789c5b1dc7d777a81884b748bdf43c8de6cd892d498"
+ },
+ "pouting_cat": {
+ "category": "people",
+ "moji": "😾",
+ "description": "pouting cat face",
+ "unicodeVersion": "6.0",
+ "digest": "e253bae99f9859322bc02b5e9b87cb33c68a5e38aebaa57478d5f4a5b1c23bb0"
+ },
+ "see_no_evil": {
+ "category": "nature",
+ "moji": "🙈",
+ "description": "see-no-evil monkey",
+ "unicodeVersion": "6.0",
+ "digest": "b5659d1f0ae7dc35ba729bee05ef351dbf8fe299b768937a1e271c19ac1dd9a9"
+ },
+ "hear_no_evil": {
+ "category": "nature",
+ "moji": "🙉",
+ "description": "hear-no-evil monkey",
+ "unicodeVersion": "6.0",
+ "digest": "6358afb4d187b86c325b5b0113b46d0ab080968fbd8c477d54f733c169b0242e"
+ },
+ "speak_no_evil": {
+ "category": "nature",
+ "moji": "🙊",
+ "description": "speak-no-evil monkey",
+ "unicodeVersion": "6.0",
+ "digest": "7cb1d4a61d2947bb0624a57a7355089f751d576c3bf26b61e3a2f1c413b4c293"
+ },
+ "love_letter": {
+ "category": "objects",
+ "moji": "💌",
+ "description": "love letter",
+ "unicodeVersion": "6.0",
+ "digest": "2a263ff736055811ce621c61f5ef9d9393bb71e180515a0fdc75107b89c60093"
+ },
+ "cupid": {
"category": "symbols",
- "moji": "⏫",
- "description": "black up-pointing double triangle",
+ "moji": "💘",
+ "description": "heart with arrow",
"unicodeVersion": "6.0",
- "digest": "79fe28485f924df1c66447c2e633ebc02d01ab46c0d2ad4b319f950f060d5525"
+ "digest": "c13b5e7a7a9824b3921b07805ffc08bb07e01f5384c49909ac375e9f55a3102c"
},
- "arrow_down": {
+ "gift_heart": {
"category": "symbols",
- "moji": "⬇",
- "description": "downwards black arrow",
- "unicodeVersion": "4.0",
- "digest": "70c8ffa3178143b62de0d2e739700456ed470f5990599f86f518307d77bace97"
+ "moji": "💝",
+ "description": "heart with ribbon",
+ "unicodeVersion": "6.0",
+ "digest": "9d747c69520b804e5cf3810475ff764a1d88f9f4e90f2fb3030a85ea35cd63a2"
},
- "arrow_down_small": {
+ "sparkling_heart": {
"category": "symbols",
- "moji": "🔽",
- "description": "down-pointing small red triangle",
+ "moji": "💖",
+ "description": "sparkling heart",
"unicodeVersion": "6.0",
- "digest": "6432caec004e9eadb0d160cb857a2eeb43cfab1ace23a904fa5a624f56bfca95"
+ "digest": "cc017b631dae27a01e15faa5f7d24c35983a4a2d928c23e9449b1b183636cb05"
},
- "arrow_forward": {
+ "heartpulse": {
"category": "symbols",
- "moji": "▶",
- "description": "black right-pointing triangle",
- "unicodeVersion": "1.1",
- "digest": "b3f0c8db863157c1c4121602ecaee91845346b2b7a1ef0ad4d46744db7866e98"
+ "moji": "💗",
+ "description": "growing heart",
+ "unicodeVersion": "6.0",
+ "digest": "bb45713a2195b5f4742bc57aa4e72e26a850c8860e0ca395fcc8e2a64e13334b"
},
- "arrow_heading_down": {
+ "heartbeat": {
"category": "symbols",
- "moji": "⤵",
- "description": "arrow pointing rightwards then curving downwards",
- "unicodeVersion": "3.2",
- "digest": "f351d6b66a0e73f41bc58446486c5fbe35d70b715fdc4ae9adef474df7aa1f69"
+ "moji": "💓",
+ "description": "beating heart",
+ "unicodeVersion": "6.0",
+ "digest": "1b8ecc1830cb706a354bd340c6c127bf045102a5fe7e1f472daecf13a772628f"
},
- "arrow_heading_up": {
+ "revolving_hearts": {
"category": "symbols",
- "moji": "⤴",
- "description": "arrow pointing rightwards then curving upwards",
- "unicodeVersion": "3.2",
- "digest": "b684802de1239962bd0c076a5a0aeb268d2cf43620dde214520817e522a60792"
+ "moji": "💞",
+ "description": "revolving hearts",
+ "unicodeVersion": "6.0",
+ "digest": "f6d44311823de89d93f7f0c0758e60a804491237b18b4b0bd20a9843570c9c04"
},
- "arrow_left": {
+ "two_hearts": {
"category": "symbols",
- "moji": "⬅",
- "description": "leftwards black arrow",
- "unicodeVersion": "4.0",
- "digest": "431289c3759f093a1a2f3460d6b912897098d3344a23a31fa9ce778c484f60ba"
+ "moji": "💕",
+ "description": "two hearts",
+ "unicodeVersion": "6.0",
+ "digest": "52fba958d8153422ae667827dd2dd44a58bf36ac3f7d3d9433527b6a92b7e6e7"
},
- "arrow_lower_left": {
+ "heart_decoration": {
"category": "symbols",
- "moji": "↙",
- "description": "south west arrow",
- "unicodeVersion": "1.1",
- "digest": "53a95853a65f3add101d64cd4ca26d677b1519d388d9c5ac75088e1965e8b856"
+ "moji": "💟",
+ "description": "heart decoration",
+ "unicodeVersion": "6.0",
+ "digest": "f68f4ff3043101c8bb2ce52257ee918a016128a962ba3d8e0bb4a640076743f9"
},
- "arrow_lower_right": {
+ "heart_exclamation": {
"category": "symbols",
- "moji": "↘",
- "description": "south east arrow",
+ "moji": "❣",
+ "description": "heavy heart exclamation mark ornament",
"unicodeVersion": "1.1",
- "digest": "603a00a2370d8872a037fdd195315bbbe936ccf12db2801bd93211a55068daa7"
+ "digest": "35c8dd5c38c09f8bcee07145e68f299f662866c7d05294aff6ab2a9b11bb83de"
},
- "arrow_right": {
+ "broken_heart": {
"category": "symbols",
- "moji": "➡",
- "description": "black rightwards arrow",
- "unicodeVersion": "1.1",
- "digest": "cca969d90670944613bcaa9463a35abfdbfa6474d1177683e2fa1327d8b52c91"
+ "moji": "💔",
+ "description": "broken heart",
+ "unicodeVersion": "6.0",
+ "digest": "6b60f5c0d0a7702308a85e0d8eca3ca54ac348a9f66bb56f89f9f3aae9303ca4"
},
- "arrow_right_hook": {
+ "heart": {
"category": "symbols",
- "moji": "↪",
- "description": "rightwards arrow with hook",
+ "moji": "❤",
+ "description": "heavy black heart",
"unicodeVersion": "1.1",
- "digest": "65e1489951134f221d8b7d45d6857321530dfad720c43fa0590d123b3cf29c00"
+ "digest": "f4f1ba1aa7118b2ccb2693eeb523fd1ef44766e88bbda830fe2154a7791bb677"
},
- "arrow_up": {
+ "yellow_heart": {
"category": "symbols",
- "moji": "⬆",
- "description": "upwards black arrow",
- "unicodeVersion": "4.0",
- "digest": "9ea5aba6d658bc8bed6a8eb9ea073e010d916dd7e6cde9de71a760026754ed6a"
+ "moji": "💛",
+ "description": "yellow heart",
+ "unicodeVersion": "6.0",
+ "digest": "1785103b3aab8606869692986a2ff5e320dae4b6d58f7dca33beed221aff8f42"
},
- "arrow_up_down": {
+ "green_heart": {
"category": "symbols",
- "moji": "↕",
- "description": "up down arrow",
- "unicodeVersion": "1.1",
- "digest": "83bda8fd50d42e169679a9825df1f5d5b7c421c710fb965b9ad4885c3c61268a"
+ "moji": "💚",
+ "description": "green heart",
+ "unicodeVersion": "6.0",
+ "digest": "426e89957ea1b6631948c5607f1806695af2a339a27159bf93c4c42a6110595c"
},
- "arrow_up_small": {
+ "blue_heart": {
"category": "symbols",
- "moji": "🔼",
- "description": "up-pointing small red triangle",
+ "moji": "💙",
+ "description": "blue heart",
"unicodeVersion": "6.0",
- "digest": "2335b064c64d375d54e4e59111cd169781e633969b57d57dd9e1abba7555e9f1"
+ "digest": "dde3ac8cbd84903c39310d0a9a0121f47e1701bf3298d0cecaba552076b462c1"
},
- "arrow_upper_left": {
+ "purple_heart": {
"category": "symbols",
- "moji": "↖",
- "description": "north west arrow",
- "unicodeVersion": "1.1",
- "digest": "035fd0f1149c8af6bec2c3dd09465b494427fcc34213749802e2e18e5ae8159a"
+ "moji": "💜",
+ "description": "purple heart",
+ "unicodeVersion": "6.0",
+ "digest": "68bc43f94a83b183d3ae134cfb36ef801dbb08f8ed46ed2972caa24cad3c8d2c"
},
- "arrow_upper_right": {
+ "black_heart": {
"category": "symbols",
- "moji": "↗",
- "description": "north east arrow",
- "unicodeVersion": "1.1",
- "digest": "e8894fed9c62b652add3bac3ad3dd84bf3a215c45a0883c6e701cc02f89e5bfc"
+ "moji": "🖤",
+ "description": "black heart",
+ "unicodeVersion": "9.0",
+ "digest": "d32225e69f8013ed033687a275d2cf0bf19bc7d38f54353ae8441ec50ca25e0b"
},
- "arrows_clockwise": {
- "category": "symbols",
- "moji": "🔃",
- "description": "clockwise downwards and upwards open circle arrows",
+ "kiss": {
+ "category": "people",
+ "moji": "💋",
+ "description": "kiss mark",
"unicodeVersion": "6.0",
- "digest": "0f4be61ded4f219dc1582b002635b538c15d40234e8571a3bfbc35921f0e83a6"
+ "digest": "751426045d0e8e59b148b2f2dd275373d361ac8e90d9e6ee65f356a8a9c5e24c"
},
- "arrows_counterclockwise": {
+ "100": {
"category": "symbols",
- "moji": "🔄",
- "description": "anticlockwise downwards and upwards open circle ar",
+ "moji": "💯",
+ "description": "hundred points symbol",
"unicodeVersion": "6.0",
- "digest": "e94e5cd47117e7dd196d0b1f32a97f2f87e76b6ad2652359849cda8d773c284c"
+ "digest": "66b1338bd0bc6efa35033dec6d9f8962c98f1eda08e50bae6a319e7f069ae861"
},
- "art": {
- "category": "activity",
- "moji": "🎨",
- "description": "artist palette",
+ "anger": {
+ "category": "symbols",
+ "moji": "💢",
+ "description": "anger symbol",
"unicodeVersion": "6.0",
- "digest": "ee525dbf572a127ac99ccc165520e429e3d24f7cf3b8d787496b63415454d49c"
+ "digest": "f63add6c0e9483fb1ee5d2fffdc6e0a818b233713a6da85f6ce1ec3da0a7049a"
},
- "articulated_lorry": {
- "category": "travel",
- "moji": "🚛",
- "description": "articulated lorry",
+ "boom": {
+ "category": "nature",
+ "moji": "💥",
+ "description": "collision symbol",
"unicodeVersion": "6.0",
- "digest": "35d63d2bb71429a9da433fa2464827f53996a738e22efd275b2326a6cc30063c"
+ "digest": "eb699268e39f7fa16a39fc731a7ebed9f7dcdbde083e222cffbac860c1bc643b"
},
- "asterisk": {
- "category": "symbols",
- "moji": "*⃣",
- "description": "keycap asterisk",
- "unicodeVersion": "3.0",
- "digest": "7f65396609bdbffe6bf305cbfe56c4274f063f235d9e505b66367a71ee1cf233"
+ "dizzy": {
+ "category": "nature",
+ "moji": "💫",
+ "description": "dizzy symbol",
+ "unicodeVersion": "6.0",
+ "digest": "b3d16f5748abede6f133bac2104e51eaff0e59f184256aa6902ebb6d8a3e32b7"
},
- "astonished": {
- "category": "people",
- "moji": "😲",
- "description": "astonished face",
+ "sweat_drops": {
+ "category": "nature",
+ "moji": "💦",
+ "description": "splashing sweat symbol",
"unicodeVersion": "6.0",
- "digest": "ecfb4bc1b9617e8de75b5c7b97e09e97e7bac08de23d4e76ce27b3c04277ab0e"
+ "digest": "48642bb76350a7be33303751b18ca1150085d20070e18eb9e3617833ae406b11"
},
- "athletic_shoe": {
- "category": "people",
- "moji": "👟",
- "description": "athletic shoe",
+ "dash": {
+ "category": "nature",
+ "moji": "💨",
+ "description": "dash symbol",
"unicodeVersion": "6.0",
- "digest": "74ad8b5b9f0612ab983841e94637e02e91fc0de90b4a12997be5d2a2eb56d12c"
+ "digest": "6c0ab681346b90d7b75e1e16531890a6ebed5c7b8bc63b269cc4a6080328fd6f"
},
- "atm": {
+ "hole": {
+ "category": "objects",
+ "moji": "🕳",
+ "description": "hole",
+ "unicodeVersion": "7.0",
+ "digest": "a486f10fd58f9e9424feb4b1409e5fccac7706952b616460156a55db836a46f0"
+ },
+ "speech_balloon": {
"category": "symbols",
- "moji": "🏧",
- "description": "automated teller machine",
+ "moji": "💬",
+ "description": "speech balloon",
"unicodeVersion": "6.0",
- "digest": "58d4d4ab5df9de2f88f7ce4af21f1e5146ebd61731967d47ec5209affd987a24"
+ "digest": "8a0b9329452cb5b6d529bb5a5a56656eceaba92177f566e3748d7910588a938b"
},
- "atom": {
+ "eye_in_speech_bubble": {
"category": "symbols",
- "moji": "⚛",
- "description": "atom symbol",
- "unicodeVersion": "4.1",
- "digest": "1a7ca89822b91c1acfe7a10d57c859bfa87b6797713efa2ec87f01d36d302056"
+ "moji": "👁‍🗨",
+ "description": "eye in speech bubble",
+ "unicodeVersion": "7.0",
+ "digest": "4b4d96038c0883d99091f9718a3c1e9c096268a3bcead903a7f6551db2b779a8"
},
- "avocado": {
- "category": "food",
- "moji": "🥑",
- "description": "avocado",
- "unicodeVersion": "9.0",
- "digest": "465bdf47c670c469b14e17a61f6d60b5975aa8d2c1e8764c8e7305185f831d58"
+ "speech_left": {
+ "category": "symbols",
+ "moji": "🗨",
+ "description": "left speech bubble",
+ "unicodeVersion": "7.0",
+ "digest": "45487904f8cbf1a1de421f85fbdd212e0a7e51d8540d9f99b65a1aea187477b5"
},
- "b": {
+ "anger_right": {
"category": "symbols",
- "moji": "🅱",
- "description": "negative squared latin capital letter b",
+ "moji": "🗯",
+ "description": "right anger bubble",
+ "unicodeVersion": "7.0",
+ "digest": "f47446e92c821d4f4d1d9861b99b3b3fff69ef52364c0aea3d99dc8e5fa40c3e"
+ },
+ "thought_balloon": {
+ "category": "symbols",
+ "moji": "💭",
+ "description": "thought balloon",
"unicodeVersion": "6.0",
- "digest": "ae89fc972ef5c80863022ef169e3ceb4bfdece3cf052fe9023516904db23b8a0"
+ "digest": "d6a36d105964c8184aa889193b812be4307508c10a9bf99d6eb199565be2c5cc"
},
- "baby": {
+ "zzz": {
"category": "people",
- "moji": "👶",
- "description": "baby",
+ "moji": "💤",
+ "description": "sleeping symbol",
"unicodeVersion": "6.0",
- "digest": "16999f24cdb63691baf5a2a8252de265a8a73bbd10ba0d4214c85b2f43c5b02e"
+ "digest": "6b19746f5be6ee5f10dcb0969557eb9b02972d4429052237facf8d4b4f768546"
},
- "baby_bottle": {
- "category": "food",
- "moji": "🍼",
- "description": "baby bottle",
+ "wave": {
+ "category": "people",
+ "moji": "👋",
+ "description": "waving hand sign",
"unicodeVersion": "6.0",
- "digest": "2bd1cb4a294c83eb07b6d12e2abf8ab42a5083941096dd11ee772cdb1b1e3091"
+ "digest": "5b877d50f49e858c453871fc380f0449633870118d487217c0a1f7f9cab02a06"
},
- "baby_chick": {
- "category": "nature",
- "moji": "🐤",
- "description": "baby chick",
- "unicodeVersion": "6.0",
- "digest": "997e1f93ed8fb08c4362d5b0907b8243dc623a62e6cf0b3a9034112f187a3314"
+ "wave_tone1": {
+ "category": "people",
+ "moji": "👋🏻",
+ "description": "waving hand sign tone 1",
+ "unicodeVersion": "8.0",
+ "digest": "d27aa7181be2fab9d0281889496ab100a6a9473d7b1b4b0b4bcaa7523a311706"
},
- "baby_symbol": {
- "category": "symbols",
- "moji": "🚼",
- "description": "baby symbol",
- "unicodeVersion": "6.0",
- "digest": "19ea4b3a81368933b6e78901205747e4fbc1dcdbfc3162f27191c1a8e0c395f7"
+ "wave_tone2": {
+ "category": "people",
+ "moji": "👋🏼",
+ "description": "waving hand sign tone 2",
+ "unicodeVersion": "8.0",
+ "digest": "7656b85268eb3318a0e8f954d334ca585c780da567a9a57ffdf24ecc3758e123"
},
- "baby_tone1": {
+ "wave_tone3": {
"category": "people",
- "moji": "👶🏻",
- "description": "baby tone 1",
+ "moji": "👋🏽",
+ "description": "waving hand sign tone 3",
"unicodeVersion": "8.0",
- "digest": "f6262f7773de6b2cdbd5f0464ccbdd9584d8077d11c8b27cf353329d8dfdbded"
+ "digest": "002912d69d16d423253db2e90d04164b2a861847dd6eff31f9a28f32e8720c9b"
},
- "baby_tone2": {
+ "wave_tone4": {
"category": "people",
- "moji": "👶🏼",
- "description": "baby tone 2",
+ "moji": "👋🏾",
+ "description": "waving hand sign tone 4",
"unicodeVersion": "8.0",
- "digest": "c90cf6fa9a95e38ab449cfd25bd422e4ead60351666dc9221ade4e2eac17f38e"
+ "digest": "8e91cbf4b2eb22caa7c06e816d4083a861882b38cced8c59b5f19f7371114044"
},
- "baby_tone3": {
+ "wave_tone5": {
"category": "people",
- "moji": "👶🏽",
- "description": "baby tone 3",
+ "moji": "👋🏿",
+ "description": "waving hand sign tone 5",
"unicodeVersion": "8.0",
- "digest": "290215d0ad2cd110ea918fa7020aff5b5e8b08849ff2a5acae5ed1f8e9f59958"
+ "digest": "40f696691a3ee439029d7914abe0ccb5efe66bab0ed2c057080991fc878eb0f4"
},
- "baby_tone4": {
+ "raised_back_of_hand": {
"category": "people",
- "moji": "👶🏾",
- "description": "baby tone 4",
+ "moji": "🤚",
+ "description": "raised back of hand",
+ "unicodeVersion": "9.0",
+ "digest": "3335f2a4f8ac26c22418968e1836e697ee03af488a9349bab6795076ab5dd771"
+ },
+ "raised_back_of_hand_tone1": {
+ "category": "people",
+ "moji": "🤚🏻",
+ "description": "raised back of hand tone 1",
+ "unicodeVersion": "9.0",
+ "digest": "6388f3e4b61cc32967aa6b9bceb60e8673f01cb1cdf94c37332d61040be48d6b"
+ },
+ "raised_back_of_hand_tone2": {
+ "category": "people",
+ "moji": "🤚🏼",
+ "description": "raised back of hand tone 2",
+ "unicodeVersion": "9.0",
+ "digest": "66491196ad238a2a12c14d9be5cec12d22788d719725e7d22e6edad40afc0c8e"
+ },
+ "raised_back_of_hand_tone3": {
+ "category": "people",
+ "moji": "🤚🏽",
+ "description": "raised back of hand tone 3",
+ "unicodeVersion": "9.0",
+ "digest": "27e2f03168b0733a0492a30df1f2f924783bf309a3a05a22a27529953783d0cd"
+ },
+ "raised_back_of_hand_tone4": {
+ "category": "people",
+ "moji": "🤚🏾",
+ "description": "raised back of hand tone 4",
+ "unicodeVersion": "9.0",
+ "digest": "729c1c34a2aa7c236d11c73665fa7b29fa1c31cc3ce56ea0a7e46f0754483efd"
+ },
+ "raised_back_of_hand_tone5": {
+ "category": "people",
+ "moji": "🤚🏿",
+ "description": "raised back of hand tone 5",
+ "unicodeVersion": "9.0",
+ "digest": "50cca64dcbf0dff9a896668c6d909bd91805d699873224933e91af524c781320"
+ },
+ "hand_splayed": {
+ "category": "people",
+ "moji": "🖐",
+ "description": "raised hand with fingers splayed",
+ "unicodeVersion": "7.0",
+ "digest": "cf0d977763f453074d581b2f305065ec0237ff8d242ea99641b85e1041c4aa62"
+ },
+ "hand_splayed_tone1": {
+ "category": "people",
+ "moji": "🖐🏻",
+ "description": "raised hand with fingers splayed tone 1",
"unicodeVersion": "8.0",
- "digest": "dfda26c31ab16afde0500631e08875205deb3ebd87bbb96e8b746ae49ecefa11"
+ "digest": "33aeacad6f84a936dbcc8787d49cb0358afbde289bc324320f00f9582512762e"
},
- "baby_tone5": {
+ "hand_splayed_tone2": {
"category": "people",
- "moji": "👶🏿",
- "description": "baby tone 5",
+ "moji": "🖐🏼",
+ "description": "raised hand with fingers splayed tone 2",
"unicodeVersion": "8.0",
- "digest": "8c32dba5be741fb3c009e55e9766526dbfec3e90977c4ab24b8ee3472a38f8e8"
+ "digest": "e78d9a420e4b28549c90869764b09edb030884049e49202515cf31d3f95e04e3"
},
- "back": {
- "category": "symbols",
- "moji": "🔙",
- "description": "back with leftwards arrow above",
- "unicodeVersion": "6.0",
- "digest": "c473fa1c3be08fb1920ce88072cf4f6ce3a031707d765d5e24b47aab0aac45be"
+ "hand_splayed_tone3": {
+ "category": "people",
+ "moji": "🖐🏽",
+ "description": "raised hand with fingers splayed tone 3",
+ "unicodeVersion": "8.0",
+ "digest": "252148eb798f804b341c862ca04b64bc499eef820e95d481abf85d88d2cadca7"
},
- "bacon": {
- "category": "food",
- "moji": "🥓",
- "description": "bacon",
- "unicodeVersion": "9.0",
- "digest": "a076ea85c09e7783948f5710294c32d34487402779227bf47dc6f8d0fe7a9689"
+ "hand_splayed_tone4": {
+ "category": "people",
+ "moji": "🖐🏾",
+ "description": "raised hand with fingers splayed tone 4",
+ "unicodeVersion": "8.0",
+ "digest": "551ed9bcf75b5fd83b8d4d5e4d01700b494fd8787a40ab84db4783e670fa404c"
},
- "badminton": {
- "category": "activity",
- "moji": "🏸",
- "description": "badminton racquet",
+ "hand_splayed_tone5": {
+ "category": "people",
+ "moji": "🖐🏿",
+ "description": "raised hand with fingers splayed tone 5",
"unicodeVersion": "8.0",
- "digest": "52efeaba6a27cef40bbd624eb945bf8712c8ce3a135de8b979c1e04b440ec51a"
+ "digest": "a1b05e2a3dbd79b673e058f22533142c2e4bf6c542054ffbf3f61dc48f3aa274"
},
- "baggage_claim": {
- "category": "symbols",
- "moji": "🛄",
- "description": "baggage claim",
+ "raised_hand": {
+ "category": "people",
+ "moji": "✋",
+ "description": "raised hand",
"unicodeVersion": "6.0",
- "digest": "c99d1d554d119f4a1f8ffa7f6c53d03071972d2f0907ffcba4f5ef92739edb1d"
+ "digest": "0bfd815713f428f4408c4225abd10c73e1200dbabe07216cb5d07098f8314270"
},
- "balloon": {
- "category": "objects",
- "moji": "🎈",
- "description": "balloon",
- "unicodeVersion": "6.0",
- "digest": "9f76188ad32199d081dfc6757b68f66c8b4e8629f420e85d4cb88e00a72256ab"
+ "raised_hand_tone1": {
+ "category": "people",
+ "moji": "✋🏻",
+ "description": "raised hand tone 1",
+ "unicodeVersion": "8.0",
+ "digest": "5d8a093e609223ce89bb3813d1b673d8558b81597ec747002a5e30792e1bcd72"
},
- "ballot_box": {
- "category": "objects",
- "moji": "🗳",
- "description": "ballot box with ballot",
+ "raised_hand_tone2": {
+ "category": "people",
+ "moji": "✋🏼",
+ "description": "raised hand tone 2",
+ "unicodeVersion": "8.0",
+ "digest": "7b76fb17f3da3719ee18ca00903b0b89b4fad7718aa18df52c7920d0ae049fb2"
+ },
+ "raised_hand_tone3": {
+ "category": "people",
+ "moji": "✋🏽",
+ "description": "raised hand tone 3",
+ "unicodeVersion": "8.0",
+ "digest": "44bce7c38e3b814d00fee161df4cdf94b2c73f5e044b65317010588029aae4be"
+ },
+ "raised_hand_tone4": {
+ "category": "people",
+ "moji": "✋🏾",
+ "description": "raised hand tone 4",
+ "unicodeVersion": "8.0",
+ "digest": "b23fad6235d1e3dfbbf7c613013cf294b9c36c6d1f2228fd97fb4802aa3ef0af"
+ },
+ "raised_hand_tone5": {
+ "category": "people",
+ "moji": "✋🏿",
+ "description": "raised hand tone 5",
+ "unicodeVersion": "8.0",
+ "digest": "a66a0fc82b6d8abd282f5c7f7e35cc31a3f83dd425f4621a3538a2455113b02a"
+ },
+ "vulcan": {
+ "category": "people",
+ "moji": "🖖",
+ "description": "raised hand with part between middle and ring fingers",
"unicodeVersion": "7.0",
- "digest": "375bf5ca28895dd54acc3f7927d62fed9d56ad79b5218f6f3be2533929ad47a5"
+ "digest": "829687cca319f7293457db7d49b7eb236c681def71b96711556534c6d9123279"
},
- "ballot_box_with_check": {
- "category": "symbols",
- "moji": "☑",
- "description": "ballot box with check",
- "unicodeVersion": "1.1",
- "digest": "81e61184a557724ccfe7c2f8e2005f053e134ed19c9c8e4aa5890d9fefc32392"
+ "vulcan_tone1": {
+ "category": "people",
+ "moji": "🖖🏻",
+ "description": "raised hand with part between middle and ring fingers tone 1",
+ "unicodeVersion": "8.0",
+ "digest": "993550c5e6d01f173ce710e8d721474f21359f706cff863f913db3e31212ab56"
},
- "bamboo": {
- "category": "nature",
- "moji": "🎍",
- "description": "pine decoration",
- "unicodeVersion": "6.0",
- "digest": "1d26da431d87de2787fc0b8a18559ea14cc2fdbb928d40bcc0de6810a84be2bd"
+ "vulcan_tone2": {
+ "category": "people",
+ "moji": "🖖🏼",
+ "description": "raised hand with part between middle and ring fingers tone 2",
+ "unicodeVersion": "8.0",
+ "digest": "fad2b5ba5fef661214bee2d43a93d2cedb9024d1ba6d1f0369e76e8167926156"
},
- "banana": {
- "category": "food",
- "moji": "🍌",
- "description": "banana",
+ "vulcan_tone3": {
+ "category": "people",
+ "moji": "🖖🏽",
+ "description": "raised hand with part between middle and ring fingers tone 3",
+ "unicodeVersion": "8.0",
+ "digest": "344f09198268734de3a4f300b410a65c6a35d2ff958e7b675329e5ddffd1dd3f"
+ },
+ "vulcan_tone4": {
+ "category": "people",
+ "moji": "🖖🏾",
+ "description": "raised hand with part between middle and ring fingers tone 4",
+ "unicodeVersion": "8.0",
+ "digest": "92f84231b71044b20d6132617fd1d2553472d3402175d85a42e24a3e5b40316d"
+ },
+ "vulcan_tone5": {
+ "category": "people",
+ "moji": "🖖🏿",
+ "description": "raised hand with part between middle and ring fingers tone 5",
+ "unicodeVersion": "8.0",
+ "digest": "ab3aec60fd46b425d0ff4bed7e96f25007b6943e6e46b1754e695ef5e289c7a5"
+ },
+ "ok_hand": {
+ "category": "people",
+ "moji": "👌",
+ "description": "ok hand sign",
"unicodeVersion": "6.0",
- "digest": "9e11c486281f89713ed8853946434210f068e7cc5e26776f80125d7c1c303a84"
+ "digest": "7ef74c756b59eb60e85daf27cb92962c376733220fbfdb5569be95e67496abc0"
},
- "bangbang": {
- "category": "symbols",
- "moji": "‼",
- "description": "double exclamation mark",
+ "ok_hand_tone1": {
+ "category": "people",
+ "moji": "👌🏻",
+ "description": "ok hand sign tone 1",
+ "unicodeVersion": "8.0",
+ "digest": "285a19578d98da6686b597b5e384edab263d8129194dd3672767d9c67632dae5"
+ },
+ "ok_hand_tone2": {
+ "category": "people",
+ "moji": "👌🏼",
+ "description": "ok hand sign tone 2",
+ "unicodeVersion": "8.0",
+ "digest": "b746860ad63866d6afac53fb82ac6b593e9fbcedad18728bf874091f12c4284d"
+ },
+ "ok_hand_tone3": {
+ "category": "people",
+ "moji": "👌🏽",
+ "description": "ok hand sign tone 3",
+ "unicodeVersion": "8.0",
+ "digest": "b5eba516e1d45861434c3871ef11450771aecc6d219b9328cea618424f2c0f4e"
+ },
+ "ok_hand_tone4": {
+ "category": "people",
+ "moji": "👌🏾",
+ "description": "ok hand sign tone 4",
+ "unicodeVersion": "8.0",
+ "digest": "c4f1bf219363ef580b95fcb99e7aa541ddf1464e17d6d837f4a535b8c6eb0b58"
+ },
+ "ok_hand_tone5": {
+ "category": "people",
+ "moji": "👌🏿",
+ "description": "ok hand sign tone 5",
+ "unicodeVersion": "8.0",
+ "digest": "fccfab629162bb963f7dd60b84a2fd101f52a1f51edaf2e84a4dca5149ec1516"
+ },
+ "v": {
+ "category": "people",
+ "moji": "✌",
+ "description": "victory hand",
"unicodeVersion": "1.1",
- "digest": "905c1dc3100192f2052c72531cf796e106815f1ce11ba1993d18c0585ed75e18"
+ "digest": "cf0a1553b56d27c678ee71819933807339d0134eb71119aecc0c185bcd922996"
},
- "bank": {
- "category": "travel",
- "moji": "🏦",
- "description": "bank",
- "unicodeVersion": "6.0",
- "digest": "ec529c75f3ccf2110adbe912d06269357d84f831ba737eab90aa408d8097bb35"
+ "v_tone1": {
+ "category": "people",
+ "moji": "✌🏻",
+ "description": "victory hand tone 1",
+ "unicodeVersion": "8.0",
+ "digest": "1d4b156f48968318917284d695cf72f56f2ad8d2ed318f3886fa1eca5a27439f"
},
- "bar_chart": {
- "category": "objects",
- "moji": "📊",
- "description": "bar chart",
- "unicodeVersion": "6.0",
- "digest": "ba858a33edbab84f6c0505404a0f10cd89031747ab4e9104524973813d05970a"
+ "v_tone2": {
+ "category": "people",
+ "moji": "✌🏼",
+ "description": "victory hand tone 2",
+ "unicodeVersion": "8.0",
+ "digest": "6d842f89c4fe8d344d7748b4de74733a7b680566589556e542f808b28de8c12a"
},
- "barber": {
- "category": "objects",
- "moji": "💈",
- "description": "barber pole",
- "unicodeVersion": "6.0",
- "digest": "9ef01a8d9556863264c72b59b404f302f870b676a836ba3beb8892f745b40c80"
+ "v_tone3": {
+ "category": "people",
+ "moji": "✌🏽",
+ "description": "victory hand tone 3",
+ "unicodeVersion": "8.0",
+ "digest": "0125d0d20a51b11716399dcb0e0b81220a1fe755ea7a2e2fed22c5688516ae52"
},
- "baseball": {
- "category": "activity",
- "moji": "⚾",
- "description": "baseball",
- "unicodeVersion": "5.2",
- "digest": "8c9c750fe39ff65807d9deabccce9dbf96cdff585f320919973576027093c7a6"
+ "v_tone4": {
+ "category": "people",
+ "moji": "✌🏾",
+ "description": "victory hand tone 4",
+ "unicodeVersion": "8.0",
+ "digest": "10502f0b7ce2aaaf9e9823f74f7cbe88503ec5bd614218b75c8f277b8a280328"
},
- "basketball": {
- "category": "activity",
- "moji": "🏀",
- "description": "basketball and hoop",
- "unicodeVersion": "6.0",
- "digest": "1c38475863ccaf78b869bdd50d0eb71d189299e189be0ab8b00405096a047628"
+ "v_tone5": {
+ "category": "people",
+ "moji": "✌🏿",
+ "description": "victory hand tone 5",
+ "unicodeVersion": "8.0",
+ "digest": "1ea229068485d71074f7b6c782c6d275d7ff0a039f8c4ace5ba2700ee94315c4"
},
- "basketball_player": {
- "category": "activity",
- "moji": "⛹",
- "description": "person with ball",
- "unicodeVersion": "5.2",
- "digest": "518a3f9b20138447812b9ea81f879c035a03694c426f77d8bdc8cef51c04592f"
+ "fingers_crossed": {
+ "category": "people",
+ "moji": "🤞",
+ "description": "hand with first and index finger crossed",
+ "unicodeVersion": "9.0",
+ "digest": "fee443087ed38c26487edd1bd8866ce399ef6a8eb98342fc0110bd2f77835253"
},
- "basketball_player_tone1": {
- "category": "activity",
- "moji": "⛹🏻",
- "description": "person with ball tone 1",
+ "fingers_crossed_tone1": {
+ "category": "people",
+ "moji": "🤞🏻",
+ "description": "hand with index and middle fingers crossed tone 1",
+ "unicodeVersion": "9.0",
+ "digest": "b91c28f9644867ff41f22c1f80b6c9ee120413ae62f30772c9853d76987fed05"
+ },
+ "fingers_crossed_tone2": {
+ "category": "people",
+ "moji": "🤞🏼",
+ "description": "hand with index and middle fingers crossed tone 2",
+ "unicodeVersion": "9.0",
+ "digest": "9d4ea025f1daa063d14e8d26c3c94a0c86379b214ac8125acc404820fd1ddbb5"
+ },
+ "fingers_crossed_tone3": {
+ "category": "people",
+ "moji": "🤞🏽",
+ "description": "hand with index and middle fingers crossed tone 3",
+ "unicodeVersion": "9.0",
+ "digest": "23ee099706ec89b6bbc6f2911f9f1571822192d913516279a5318d92d01bc754"
+ },
+ "fingers_crossed_tone4": {
+ "category": "people",
+ "moji": "🤞🏾",
+ "description": "hand with index and middle fingers crossed tone 4",
+ "unicodeVersion": "9.0",
+ "digest": "cda52831530605511b13a4953136dd5f36fca1fbf3d11ac261caac862086aff9"
+ },
+ "fingers_crossed_tone5": {
+ "category": "people",
+ "moji": "🤞🏿",
+ "description": "hand with index and middle fingers crossed tone 5",
+ "unicodeVersion": "9.0",
+ "digest": "b49c1b15808715547f6f04113cf1631f4d79f8c1fec5f9ecdeea60284f9b4dad"
+ },
+ "metal": {
+ "category": "people",
+ "moji": "🤘",
+ "description": "sign of the horns",
"unicodeVersion": "8.0",
- "digest": "e34414295433335df2783de205076e9c44cdbd7e07a041d2695a83e4fe4f70ba"
+ "digest": "9bc7445e2832356d34c88f498c426fcc3fced736323af13cd8bfa18ab4a795f2"
},
- "basketball_player_tone2": {
- "category": "activity",
- "moji": "⛹🏼",
- "description": "person with ball tone 2",
+ "metal_tone1": {
+ "category": "people",
+ "moji": "🤘🏻",
+ "description": "sign of the horns tone 1",
"unicodeVersion": "8.0",
- "digest": "59308fa1cbe4b778990ced93eb866bbad7f88fda3c1b68837d0da822801f0f20"
+ "digest": "c2107bd9851d508f8128c0dbcd02d3d623597d866d4c938889ec5b4cb2dccf84"
},
- "basketball_player_tone3": {
- "category": "activity",
- "moji": "⛹🏽",
- "description": "person with ball tone 3",
+ "metal_tone2": {
+ "category": "people",
+ "moji": "🤘🏼",
+ "description": "sign of the horns tone 2",
"unicodeVersion": "8.0",
- "digest": "e334bddf438b49c145a5c11c010a05964dc50495510017dc37ea8bd6ddc400b3"
+ "digest": "85583c2c1eff98dc005d2c7cd80f75b18fe4723055b677c8f1bc90207cf0b1fd"
},
- "basketball_player_tone4": {
- "category": "activity",
- "moji": "⛹🏾",
- "description": "person with ball tone 4",
+ "metal_tone3": {
+ "category": "people",
+ "moji": "🤘🏽",
+ "description": "sign of the horns tone 3",
"unicodeVersion": "8.0",
- "digest": "24a472bcb4f27a2d8fe2dce4ed6a060391a798721b2146cf6921ff7059bb8446"
+ "digest": "f004a5b303b1e7bcf20d46bc42214e21f703658f7f83503888c326d9e76cf29f"
},
- "basketball_player_tone5": {
- "category": "activity",
- "moji": "⛹🏿",
- "description": "person with ball tone 5",
+ "metal_tone4": {
+ "category": "people",
+ "moji": "🤘🏾",
+ "description": "sign of the horns tone 4",
"unicodeVersion": "8.0",
- "digest": "8c6a17f2c938aa60aba16634d81dcb069f2c6e7bbf8d79bf36fe888ff83951ae"
+ "digest": "968ebedf7b100f33773f73cfd98c24a62870d022b46ac5c442a8b34184c9a5cf"
},
- "bat": {
- "category": "nature",
- "moji": "🦇",
- "description": "bat",
+ "metal_tone5": {
+ "category": "people",
+ "moji": "🤘🏿",
+ "description": "sign of the horns tone 5",
+ "unicodeVersion": "8.0",
+ "digest": "be4add5e381ffb482ed191f1f305eeb22707c67e251660ccf76bf550d32e16eb"
+ },
+ "call_me": {
+ "category": "people",
+ "moji": "🤙",
+ "description": "call me hand",
"unicodeVersion": "9.0",
- "digest": "5f66d15070c283ae9a293719ada7d88d6837f858dda94c61cf56835f734985d8"
+ "digest": "3701197e18ffedc242a6b20ab74e7a339c7f77df27c5f92d5124a42e39a9502a"
},
- "bath": {
- "category": "activity",
- "moji": "🛀",
- "description": "bath",
+ "call_me_tone1": {
+ "category": "people",
+ "moji": "🤙🏻",
+ "description": "call me hand tone 1",
+ "unicodeVersion": "9.0",
+ "digest": "1c5c681c9b588a2b07a57d27d3aaf209e7d23c4cba938ffce07bfa4a53d720e9"
+ },
+ "call_me_tone2": {
+ "category": "people",
+ "moji": "🤙🏼",
+ "description": "call me hand tone 2",
+ "unicodeVersion": "9.0",
+ "digest": "4c848da3d8849de81922aa2eb6611489ad72649cf189849176217e946cf9aad8"
+ },
+ "call_me_tone3": {
+ "category": "people",
+ "moji": "🤙🏽",
+ "description": "call me hand tone 3",
+ "unicodeVersion": "9.0",
+ "digest": "b697c1a4aa15f7793002b79aee8e5240f48b832b33f8ad39b8d46b90624c6c0a"
+ },
+ "call_me_tone4": {
+ "category": "people",
+ "moji": "🤙🏾",
+ "description": "call me hand tone 4",
+ "unicodeVersion": "9.0",
+ "digest": "f1b07c2a8071f5f704b5354277777aa651202f56ef7d334ce66e0475fdba086a"
+ },
+ "call_me_tone5": {
+ "category": "people",
+ "moji": "🤙🏿",
+ "description": "call me hand tone 5",
+ "unicodeVersion": "9.0",
+ "digest": "d7d8d96a3980e3c558d3e3ef62aa49dc237c075f0c3dfb0b06ae33fc98f565c5"
+ },
+ "point_left": {
+ "category": "people",
+ "moji": "👈",
+ "description": "white left pointing backhand index",
"unicodeVersion": "6.0",
- "digest": "2cac99346c8054b8f883c45194395b35121e7108aaa36ecddccc16d30c32efae"
+ "digest": "b7f186ed45ddd21e5c62cbc3d5040194ae1282caad89508fa628c248b3edf0bd"
},
- "bath_tone1": {
- "category": "activity",
- "moji": "🛀🏻",
- "description": "bath tone 1",
+ "point_left_tone1": {
+ "category": "people",
+ "moji": "👈🏻",
+ "description": "white left pointing backhand index tone 1",
"unicodeVersion": "8.0",
- "digest": "17a3cd2bf235984c097eb1ab875b44516e1deba49c9237d0d25585b3df326a57"
+ "digest": "439f771d094340a43f83b8f2e05b39d90b2ec3921756fea2878754119aafd683"
},
- "bath_tone2": {
- "category": "activity",
- "moji": "🛀🏼",
- "description": "bath tone 2",
+ "point_left_tone2": {
+ "category": "people",
+ "moji": "👈🏼",
+ "description": "white left pointing backhand index tone 2",
"unicodeVersion": "8.0",
- "digest": "4ca1d1c1a48290c175551fe9ec30d1b82dc7a952530e978d085d72180c1886e0"
+ "digest": "73d6b8d0df34df349653fcfdbcc2adbc98ba6c25f612d933cc52da1154af37f2"
},
- "bath_tone3": {
- "category": "activity",
- "moji": "🛀🏽",
- "description": "bath tone 3",
+ "point_left_tone3": {
+ "category": "people",
+ "moji": "👈🏽",
+ "description": "white left pointing backhand index tone 3",
"unicodeVersion": "8.0",
- "digest": "084a77c7f583653d8c1d228432144b3210fcd50ed96630591663b3c4dff282fd"
+ "digest": "1cfd0d1db8a06ead619a12bd248c4c9fda7b6b50309a2526a20bfc66dcb86177"
},
- "bath_tone4": {
- "category": "activity",
- "moji": "🛀🏾",
- "description": "bath tone 4",
+ "point_left_tone4": {
+ "category": "people",
+ "moji": "👈🏾",
+ "description": "white left pointing backhand index tone 4",
"unicodeVersion": "8.0",
- "digest": "c605e25a1efd41be4ded235b71924c495b0aa861bdd6f43d9f5137c51de14bb1"
+ "digest": "26b4755890f8e290e7d8958566612c14caa6522a7bd43a3d7e8c206436fad70b"
},
- "bath_tone5": {
- "category": "activity",
- "moji": "🛀🏿",
- "description": "bath tone 5",
+ "point_left_tone5": {
+ "category": "people",
+ "moji": "👈🏿",
+ "description": "white left pointing backhand index tone 5",
"unicodeVersion": "8.0",
- "digest": "e15b1bd11177d6a342dfe5eadb52969d60e6b7a1662afe55610aebbcc1a44242"
+ "digest": "8ca52d65aaedcf7ca346e90da3f53b50af83dfd544ed8631b4da1ab31e5e1497"
},
- "bathtub": {
- "category": "objects",
- "moji": "🛁",
- "description": "bathtub",
+ "point_right": {
+ "category": "people",
+ "moji": "👉",
+ "description": "white right pointing backhand index",
"unicodeVersion": "6.0",
- "digest": "1caecc05b8ae78774b7620480f4cfbbb549ca2bf6b2fb93badecf440db9ce9c8"
+ "digest": "2d27c0ffba78c5c5fbd502dd42fbbae220c8619c8a2b964bef03e4a84ad4a1b6"
},
- "battery": {
- "category": "objects",
- "moji": "🔋",
- "description": "battery",
- "unicodeVersion": "6.0",
- "digest": "8c50a487dfc349dd3b57ab4000e892efaeec967e5862485b3a023fb63c8c2949"
+ "point_right_tone1": {
+ "category": "people",
+ "moji": "👉🏻",
+ "description": "white right pointing backhand index tone 1",
+ "unicodeVersion": "8.0",
+ "digest": "c2f84a57648a6de49b2b9000afd2c45ce1ef8f303d71f248b292e3fe83b8cf94"
},
- "beach": {
- "category": "travel",
- "moji": "🏖",
- "description": "beach with umbrella",
- "unicodeVersion": "7.0",
- "digest": "98bb54c59c818b30ae9024d85156424a345f2912ca2b37b0ef18f2ef60b96619"
+ "point_right_tone2": {
+ "category": "people",
+ "moji": "👉🏼",
+ "description": "white right pointing backhand index tone 2",
+ "unicodeVersion": "8.0",
+ "digest": "0a1aae46415401cd3557f61ce491e975acaa995b6c2bf487977beb91e4b1bc7e"
},
- "beach_umbrella": {
- "category": "objects",
- "moji": "⛱",
- "description": "umbrella on ground",
- "unicodeVersion": "5.2",
- "digest": "8222557bcf3669971279b80855fad3d97cd891e8a446b2e82ca220627a4283d5"
+ "point_right_tone3": {
+ "category": "people",
+ "moji": "👉🏽",
+ "description": "white right pointing backhand index tone 3",
+ "unicodeVersion": "8.0",
+ "digest": "86c13a18c53b548907ec0ec468922beab040c29a365f55e18f265e79f1bb42bf"
},
- "bear": {
- "category": "nature",
- "moji": "🐻",
- "description": "bear face",
+ "point_right_tone4": {
+ "category": "people",
+ "moji": "👉🏾",
+ "description": "white right pointing backhand index tone 4",
+ "unicodeVersion": "8.0",
+ "digest": "1605160b761b975c0f11490eb1a7b724c674ec371d72e73f824fdbe873aeddb2"
+ },
+ "point_right_tone5": {
+ "category": "people",
+ "moji": "👉🏿",
+ "description": "white right pointing backhand index tone 5",
+ "unicodeVersion": "8.0",
+ "digest": "1cfee9fdcdaa1a790c14a4f8436dad4b3b6677860bf60dd1da3985fc7cb25a00"
+ },
+ "point_up_2": {
+ "category": "people",
+ "moji": "👆",
+ "description": "white up pointing backhand index",
"unicodeVersion": "6.0",
- "digest": "6d7ed0e469e7146c5fa5caf7496e516f553eecf2b212ec7835a0636cff147a51"
+ "digest": "0e5a1d7841d0f54762d0fadf460086cad4fcd05fbf65cabdf1df90d1ab0c3f2b"
},
- "bed": {
- "category": "objects",
- "moji": "🛏",
- "description": "bed",
+ "point_up_2_tone1": {
+ "category": "people",
+ "moji": "👆🏻",
+ "description": "white up pointing backhand index tone 1",
+ "unicodeVersion": "8.0",
+ "digest": "bdcd26a212498dcddc69342caefe4a68d2b4bfbebcfa94045a1c27dcce158311"
+ },
+ "point_up_2_tone2": {
+ "category": "people",
+ "moji": "👆🏼",
+ "description": "white up pointing backhand index tone 2",
+ "unicodeVersion": "8.0",
+ "digest": "fd5a6d912f3533b3356392e68df8b155dcacd3bb2d2e1c44d84807d587ef1ed5"
+ },
+ "point_up_2_tone3": {
+ "category": "people",
+ "moji": "👆🏽",
+ "description": "white up pointing backhand index tone 3",
+ "unicodeVersion": "8.0",
+ "digest": "57af38773077d28200e033dc3dd28913f570311a51b833df32f23a85bfcc530c"
+ },
+ "point_up_2_tone4": {
+ "category": "people",
+ "moji": "👆🏾",
+ "description": "white up pointing backhand index tone 4",
+ "unicodeVersion": "8.0",
+ "digest": "7110f1d42dffcab536906e176baa36e817142f9d71329fdfc1b74ee9813cffd6"
+ },
+ "point_up_2_tone5": {
+ "category": "people",
+ "moji": "👆🏿",
+ "description": "white up pointing backhand index tone 5",
+ "unicodeVersion": "8.0",
+ "digest": "1795ede377cdd58189471af3f6488d8197f2a742f817f8a61523ccff8d08581b"
+ },
+ "middle_finger": {
+ "category": "people",
+ "moji": "🖕",
+ "description": "reversed hand with middle finger extended",
"unicodeVersion": "7.0",
- "digest": "73395ab70c867c776ef56dd14507f7feeaa7871f98e0c0c417df5345e8034976"
+ "digest": "7d543ffbc78a5e8b9162c0f48e4503f6e32efefea7f3b22f436463ace77066fa"
},
- "bee": {
- "category": "nature",
- "moji": "🐝",
- "description": "honeybee",
- "unicodeVersion": "6.0",
- "digest": "57d564f50abfd154473c3ebb16aeac058dc15253fed2b319daf1f2cb063e29af"
+ "middle_finger_tone1": {
+ "category": "people",
+ "moji": "🖕🏻",
+ "description": "reversed hand with middle finger extended tone 1",
+ "unicodeVersion": "8.0",
+ "digest": "40a676ff704d57f6b5d2bd8c31b183600781bf3f0ff4342f1e4886717228e0ee"
},
- "beer": {
- "category": "food",
- "moji": "🍺",
- "description": "beer mug",
+ "middle_finger_tone2": {
+ "category": "people",
+ "moji": "🖕🏼",
+ "description": "reversed hand with middle finger extended tone 2",
+ "unicodeVersion": "8.0",
+ "digest": "9dcd0cc6a88d67d7fd561fccf95fd3a6030b53598fea69d5de6f14ffd72b3a82"
+ },
+ "middle_finger_tone3": {
+ "category": "people",
+ "moji": "🖕🏽",
+ "description": "reversed hand with middle finger extended tone 3",
+ "unicodeVersion": "8.0",
+ "digest": "7787a192eff949d308e925c9df0a44153429df4a290c6f348a950e8414b1d4dc"
+ },
+ "middle_finger_tone4": {
+ "category": "people",
+ "moji": "🖕🏾",
+ "description": "reversed hand with middle finger extended tone 4",
+ "unicodeVersion": "8.0",
+ "digest": "b5b4b65aa300d498aaef8753cffb34455111887a12a378b2517297622f428330"
+ },
+ "middle_finger_tone5": {
+ "category": "people",
+ "moji": "🖕🏿",
+ "description": "reversed hand with middle finger extended tone 5",
+ "unicodeVersion": "8.0",
+ "digest": "c08f75c25bd88a288d685849a8575d868b257296c1afae80d241ec5e9193bea3"
+ },
+ "point_down": {
+ "category": "people",
+ "moji": "👇",
+ "description": "white down pointing backhand index",
"unicodeVersion": "6.0",
- "digest": "c7e45519f39d1cfb1c97d05bca80ee8881dca7aced770d3dac5f282f5bb34773"
+ "digest": "4fa9f01922409ef7831ad0ab78782174dd59c46e74e9a76c38111fcdbc1666a1"
},
- "beers": {
- "category": "food",
- "moji": "🍻",
- "description": "clinking beer mugs",
+ "point_down_tone1": {
+ "category": "people",
+ "moji": "👇🏻",
+ "description": "white down pointing backhand index tone 1",
+ "unicodeVersion": "8.0",
+ "digest": "e64f86886b78db3088952713caef3144f99ba1d8b5f730e640c4ae42762fb645"
+ },
+ "point_down_tone2": {
+ "category": "people",
+ "moji": "👇🏼",
+ "description": "white down pointing backhand index tone 2",
+ "unicodeVersion": "8.0",
+ "digest": "7d44c5c286b83afdc301c8a39bda0e1619d8a099cc576cbcebf55af15931a55e"
+ },
+ "point_down_tone3": {
+ "category": "people",
+ "moji": "👇🏽",
+ "description": "white down pointing backhand index tone 3",
+ "unicodeVersion": "8.0",
+ "digest": "33d981c6cd9641f2864b807bc2e288798bbd56bbd413e1aa4d23d42bb6e1af74"
+ },
+ "point_down_tone4": {
+ "category": "people",
+ "moji": "👇🏾",
+ "description": "white down pointing backhand index tone 4",
+ "unicodeVersion": "8.0",
+ "digest": "19dc8c01dd571e65f0314cc18b55bde4038617b782808faae976cffb0fadfce4"
+ },
+ "point_down_tone5": {
+ "category": "people",
+ "moji": "👇🏿",
+ "description": "white down pointing backhand index tone 5",
+ "unicodeVersion": "8.0",
+ "digest": "67b1413272b5a0c8efbbd5461bd6fd89849ac5f8dc0d1dac26e2edfcc6b9d1ea"
+ },
+ "point_up": {
+ "category": "people",
+ "moji": "☝",
+ "description": "white up pointing index",
+ "unicodeVersion": "1.1",
+ "digest": "dc20d84a1a808e2d207f10f2f292cb78e05b9e67b4a26f7491e0c6c7f8059af5"
+ },
+ "point_up_tone1": {
+ "category": "people",
+ "moji": "☝🏻",
+ "description": "white up pointing index tone 1",
+ "unicodeVersion": "8.0",
+ "digest": "16f0e85643558fd2f471cc8d317058914f42279f4aef2ba0e8390728efb4992b"
+ },
+ "point_up_tone2": {
+ "category": "people",
+ "moji": "☝🏼",
+ "description": "white up pointing index tone 2",
+ "unicodeVersion": "8.0",
+ "digest": "fe8f930134adc4be29b7e659c6acbfa76ba52ab3d0b46dd4797e79c365708666"
+ },
+ "point_up_tone3": {
+ "category": "people",
+ "moji": "☝🏽",
+ "description": "white up pointing index tone 3",
+ "unicodeVersion": "8.0",
+ "digest": "d7258aeab80e697649a0e8ad13445380462bb5814c90b1183cc105ebe3eaa5ef"
+ },
+ "point_up_tone4": {
+ "category": "people",
+ "moji": "☝🏾",
+ "description": "white up pointing index tone 4",
+ "unicodeVersion": "8.0",
+ "digest": "4c4aca5e2e436421b26d5d58a82bd52fdb9135593fb1afd92c30fa8f1ed51dd1"
+ },
+ "point_up_tone5": {
+ "category": "people",
+ "moji": "☝🏿",
+ "description": "white up pointing index tone 5",
+ "unicodeVersion": "8.0",
+ "digest": "ab94fd7fe02205894add98c4c97d812f2228c7c766b1b0e01fa1e9e5dc7c669b"
+ },
+ "thumbsup": {
+ "category": "people",
+ "moji": "👍",
+ "description": "thumbs up sign",
"unicodeVersion": "6.0",
- "digest": "8f11a1397605165bfd346619faa5fcddfd1f6b4f9fc2ddc4756cd2518ed92062"
+ "digest": "38755ce0360171dd24005d6f4d6b06c2df337adb0cfd590e2e381cf44a9c24ec"
},
- "beetle": {
- "category": "nature",
- "moji": "🐞",
- "description": "lady beetle",
+ "thumbsup_tone1": {
+ "category": "people",
+ "moji": "👍🏻",
+ "description": "thumbs up sign tone 1",
+ "unicodeVersion": "8.0",
+ "digest": "15492d43b5bafa46473154505b431ed81e365b6ebce507c97f509a00333420f3"
+ },
+ "thumbsup_tone2": {
+ "category": "people",
+ "moji": "👍🏼",
+ "description": "thumbs up sign tone 2",
+ "unicodeVersion": "8.0",
+ "digest": "87f9941a2d3afba4ff5737a113cf070dcfbc3a2292ad15cb5b07459692cb0fdd"
+ },
+ "thumbsup_tone3": {
+ "category": "people",
+ "moji": "👍🏽",
+ "description": "thumbs up sign tone 3",
+ "unicodeVersion": "8.0",
+ "digest": "f09364411db2331284b3deb85c6107049cbb41d2e5edfb50f61fc5907641a7a0"
+ },
+ "thumbsup_tone4": {
+ "category": "people",
+ "moji": "👍🏾",
+ "description": "thumbs up sign tone 4",
+ "unicodeVersion": "8.0",
+ "digest": "8c1322a624b0ebcab1f08d7675df39383eac8b1d4b627889d3393b0d3cdc946d"
+ },
+ "thumbsup_tone5": {
+ "category": "people",
+ "moji": "👍🏿",
+ "description": "thumbs up sign tone 5",
+ "unicodeVersion": "8.0",
+ "digest": "6dec547ee282457cbbcb7e3dffcf804188aed47960a70069df81a547a8f40df9"
+ },
+ "thumbsdown": {
+ "category": "people",
+ "moji": "👎",
+ "description": "thumbs down sign",
"unicodeVersion": "6.0",
- "digest": "c05375aae35bdedd627fa55d63bda6ec9351d9a261a4a60145a8a775d99deaee"
+ "digest": "e5e3594f30f8b3c59f22963c3a903ec69569e9735690cbce1c96da981cea5f91"
},
- "beginner": {
- "category": "symbols",
- "moji": "🔰",
- "description": "japanese symbol for beginner",
+ "thumbsdown_tone1": {
+ "category": "people",
+ "moji": "👎🏻",
+ "description": "thumbs down sign tone 1",
+ "unicodeVersion": "8.0",
+ "digest": "e360d25bc6fc05243ec5ea6489fdc80702899783faa83c88686c9b73c9e8684c"
+ },
+ "thumbsdown_tone2": {
+ "category": "people",
+ "moji": "👎🏼",
+ "description": "thumbs down sign tone 2",
+ "unicodeVersion": "8.0",
+ "digest": "bf0ad5d01e7ac0ab4d2be9db76615ff303bd01d2b4915b2b846a494aa36878fd"
+ },
+ "thumbsdown_tone3": {
+ "category": "people",
+ "moji": "👎🏽",
+ "description": "thumbs down sign tone 3",
+ "unicodeVersion": "8.0",
+ "digest": "e3d907396c17971a6d533900dd857ad273c1b0ff1af520c6fda780a54616b3c8"
+ },
+ "thumbsdown_tone4": {
+ "category": "people",
+ "moji": "👎🏾",
+ "description": "thumbs down sign tone 4",
+ "unicodeVersion": "8.0",
+ "digest": "31a952ef8b0fe9c0a9b04e46033e052d8104539929509010f7ab74a09b72d396"
+ },
+ "thumbsdown_tone5": {
+ "category": "people",
+ "moji": "👎🏿",
+ "description": "thumbs down sign tone 5",
+ "unicodeVersion": "8.0",
+ "digest": "afe38eb9c879ba04556cb4bf211ef0dae63b51957389256e5af93dc7e7e94cef"
+ },
+ "fist": {
+ "category": "people",
+ "moji": "✊",
+ "description": "raised fist",
"unicodeVersion": "6.0",
- "digest": "330defe4283c7387645422a58f8ccdba013087938a21e726bad64abaafc5d2c3"
+ "digest": "5902870e6121df3f316a3c5f62b3e0a32679f3fca37fda165267d15001fbcfe4"
},
- "bell": {
- "category": "symbols",
- "moji": "🔔",
- "description": "bell",
+ "fist_tone1": {
+ "category": "people",
+ "moji": "✊🏻",
+ "description": "raised fist tone 1",
+ "unicodeVersion": "8.0",
+ "digest": "9c4e0289af6fd5a9fdb5b77396d37d93f97bf0f302c20a980dc4663121b31eb9"
+ },
+ "fist_tone2": {
+ "category": "people",
+ "moji": "✊🏼",
+ "description": "raised fist tone 2",
+ "unicodeVersion": "8.0",
+ "digest": "6784ddda1bef3dee456860266c9d408efddbfd50566115f674091ae338d8a8bd"
+ },
+ "fist_tone3": {
+ "category": "people",
+ "moji": "✊🏽",
+ "description": "raised fist tone 3",
+ "unicodeVersion": "8.0",
+ "digest": "0355b2238cabd2edcedbb47b548db789bcb1ccbd1fc56090e2b5976db0f792ef"
+ },
+ "fist_tone4": {
+ "category": "people",
+ "moji": "✊🏾",
+ "description": "raised fist tone 4",
+ "unicodeVersion": "8.0",
+ "digest": "2e29b3c36c7260c6f327941b4345be2642f034f36c6b5cb9ffcff0ee7a6140b3"
+ },
+ "fist_tone5": {
+ "category": "people",
+ "moji": "✊🏿",
+ "description": "raised fist tone 5",
+ "unicodeVersion": "8.0",
+ "digest": "3d69c0e6b12f4e6ec6b9e6a3b2974b880c4017d92bfe77bf5e14e8cc713d1c8c"
+ },
+ "punch": {
+ "category": "people",
+ "moji": "👊",
+ "description": "fisted hand sign",
"unicodeVersion": "6.0",
- "digest": "dae95427928c10693b249c1d324838702dd43295658faa85a9687ae4ea8cc36d"
+ "digest": "1f8bdf4ead54a6d9ccad648c98f126a49e4a16a1d525abf8d8194b2e253461e2"
},
- "bellhop": {
- "category": "objects",
- "moji": "🛎",
- "description": "bellhop bell",
- "unicodeVersion": "7.0",
- "digest": "9ea748fc595b9bdc1f012b38d2b86d42092baa8d4c58dc38040f624c0a893f2a"
+ "punch_tone1": {
+ "category": "people",
+ "moji": "👊🏻",
+ "description": "fisted hand sign tone 1",
+ "unicodeVersion": "8.0",
+ "digest": "6e368dfc6f762bc1db1127592d91fa29437f182391a71d61472e8459b2dfc770"
},
- "bento": {
- "category": "food",
- "moji": "🍱",
- "description": "bento box",
+ "punch_tone2": {
+ "category": "people",
+ "moji": "👊🏼",
+ "description": "fisted hand sign tone 2",
+ "unicodeVersion": "8.0",
+ "digest": "62e40f8ac33d5f28cc2bba06dc0871627db3f40a04f16a7dd2ab426bb9c740a4"
+ },
+ "punch_tone3": {
+ "category": "people",
+ "moji": "👊🏽",
+ "description": "fisted hand sign tone 3",
+ "unicodeVersion": "8.0",
+ "digest": "94a9d410d1ac2252be7386a2110708686ea2bba4859f3fd84a11e64bbd6a1432"
+ },
+ "punch_tone4": {
+ "category": "people",
+ "moji": "👊🏾",
+ "description": "fisted hand sign tone 4",
+ "unicodeVersion": "8.0",
+ "digest": "b378775978c26547d87fe8d2f7d20a05c2bbca386d40533fe774e4bca2e7540a"
+ },
+ "punch_tone5": {
+ "category": "people",
+ "moji": "👊🏿",
+ "description": "fisted hand sign tone 5",
+ "unicodeVersion": "8.0",
+ "digest": "4300760b118dee45c53098601e7d8f9d3ec66e87f2d11e87b07adb79793c53e8"
+ },
+ "left_facing_fist": {
+ "category": "people",
+ "moji": "🤛",
+ "description": "left-facing fist",
+ "unicodeVersion": "9.0",
+ "digest": "69390c51d4188f7f5381f7d8f4658c8ea4e52455ed648ebf59b6fb3157c5c6e0"
+ },
+ "left_facing_fist_tone1": {
+ "category": "people",
+ "moji": "🤛🏻",
+ "description": "left facing fist tone 1",
+ "unicodeVersion": "9.0",
+ "digest": "57e9a2288243d2024cf25e4258c48e1b0bfe5c12aee218629bb03f8a5ab0cb61"
+ },
+ "left_facing_fist_tone2": {
+ "category": "people",
+ "moji": "🤛🏼",
+ "description": "left facing fist tone 2",
+ "unicodeVersion": "9.0",
+ "digest": "061ef44811174e10f7598aadf1f4c69cbc915d029ba83d5101cc34d0cfd44431"
+ },
+ "left_facing_fist_tone3": {
+ "category": "people",
+ "moji": "🤛🏽",
+ "description": "left facing fist tone 3",
+ "unicodeVersion": "9.0",
+ "digest": "c313fcb7d4e1505b76ce25e45b50c0c4d0842be34c472abc328915f07e6b6efe"
+ },
+ "left_facing_fist_tone4": {
+ "category": "people",
+ "moji": "🤛🏾",
+ "description": "left facing fist tone 4",
+ "unicodeVersion": "9.0",
+ "digest": "825e7feac0a934250246dcb97cae5daafe31aef100c9614a17ba0c7669cb18ee"
+ },
+ "left_facing_fist_tone5": {
+ "category": "people",
+ "moji": "🤛🏿",
+ "description": "left facing fist tone 5",
+ "unicodeVersion": "9.0",
+ "digest": "20bfe330f9c1ae22b0cd4d0af0a8a5d062e898fafe10c8a4153f89e582c0de54"
+ },
+ "right_facing_fist": {
+ "category": "people",
+ "moji": "🤜",
+ "description": "right-facing fist",
+ "unicodeVersion": "9.0",
+ "digest": "d1abe6e551a7b336ed0c7234db5c5a653db975647ac603a320f05b1635378736"
+ },
+ "right_facing_fist_tone1": {
+ "category": "people",
+ "moji": "🤜🏻",
+ "description": "right facing fist tone 1",
+ "unicodeVersion": "9.0",
+ "digest": "d281365007abb9150174089f9a1baa7033237f6e566aca72763e538861674a74"
+ },
+ "right_facing_fist_tone2": {
+ "category": "people",
+ "moji": "🤜🏼",
+ "description": "right facing fist tone 2",
+ "unicodeVersion": "9.0",
+ "digest": "841c242354f94f30eea0e16ab3373d6c6d6d3e6a90ac20baf7765a08bf6e7f07"
+ },
+ "right_facing_fist_tone3": {
+ "category": "people",
+ "moji": "🤜🏽",
+ "description": "right facing fist tone 3",
+ "unicodeVersion": "9.0",
+ "digest": "6843fe9b8f162bc43bff78c16555f9eadece02285ca7758818db5e8c4caf064b"
+ },
+ "right_facing_fist_tone4": {
+ "category": "people",
+ "moji": "🤜🏾",
+ "description": "right facing fist tone 4",
+ "unicodeVersion": "9.0",
+ "digest": "af349e6f8b54e0124667e7a6cf01fb4eb48eb21653e8f6d0ddd22e8114dde797"
+ },
+ "right_facing_fist_tone5": {
+ "category": "people",
+ "moji": "🤜🏿",
+ "description": "right facing fist tone 5",
+ "unicodeVersion": "9.0",
+ "digest": "efd40f38ab91c5ea8e66ba129d4aa2d5af2c35c5030182695cfe81d6e9123987"
+ },
+ "clap": {
+ "category": "people",
+ "moji": "👏",
+ "description": "clapping hands sign",
"unicodeVersion": "6.0",
- "digest": "dee568150c21a99f0b6ce11af58ac7c8a298443fc3971e3efead7728b1bc7459"
+ "digest": "68fca08d9340cd18b061a9ceef10111b006df9f888b534dd9b307c490cdb045c"
},
- "bicyclist": {
- "category": "activity",
- "moji": "🚴",
- "description": "bicyclist",
+ "clap_tone1": {
+ "category": "people",
+ "moji": "👏🏻",
+ "description": "clapping hands sign tone 1",
+ "unicodeVersion": "8.0",
+ "digest": "dc842eb100206a92eab5cefadeb5dab004ee34d054b0adc02de27f525a0a724c"
+ },
+ "clap_tone2": {
+ "category": "people",
+ "moji": "👏🏼",
+ "description": "clapping hands sign tone 2",
+ "unicodeVersion": "8.0",
+ "digest": "6a92a954a6ea47ce0e42dceee82cd4220f710582f7589cb86037555f0a5e9dce"
+ },
+ "clap_tone3": {
+ "category": "people",
+ "moji": "👏🏽",
+ "description": "clapping hands sign tone 3",
+ "unicodeVersion": "8.0",
+ "digest": "6b7dfeae569488df0bd85c16233abcf770bc2bb5942c178cbe5107e798a9ca88"
+ },
+ "clap_tone4": {
+ "category": "people",
+ "moji": "👏🏾",
+ "description": "clapping hands sign tone 4",
+ "unicodeVersion": "8.0",
+ "digest": "2fa8961ad763675c6619b5f4137e912ce4126fb9b5df71d23c59b05e54533afa"
+ },
+ "clap_tone5": {
+ "category": "people",
+ "moji": "👏🏿",
+ "description": "clapping hands sign tone 5",
+ "unicodeVersion": "8.0",
+ "digest": "7b8b5e0befd920ac4e02ef1823b63c2513dbe58fe6471dc54b8265b75b4c8f41"
+ },
+ "raised_hands": {
+ "category": "people",
+ "moji": "🙌",
+ "description": "person raising both hands in celebration",
"unicodeVersion": "6.0",
- "digest": "3dd17f72beea2fcc4fc9124af3f4997ac00009c699096495b3515cd3ad2453f7"
+ "digest": "6bc3e746c276ce1ea46ba5233d30ec5cdb8340b5c9c15768873404d59b1f3496"
},
- "bicyclist_tone1": {
- "category": "activity",
- "moji": "🚴🏻",
- "description": "bicyclist tone 1",
+ "raised_hands_tone1": {
+ "category": "people",
+ "moji": "🙌🏻",
+ "description": "person raising both hands in celebration tone 1",
"unicodeVersion": "8.0",
- "digest": "9bb97a68bdb7d081c1ddfa277be0842fccf3497752fde007e08e46ff831ab4dc"
+ "digest": "c3e5095f41e49954c688edefeb223069c57979052735b06277d7f3620161796f"
},
- "bicyclist_tone2": {
- "category": "activity",
- "moji": "🚴🏼",
- "description": "bicyclist tone 2",
+ "raised_hands_tone2": {
+ "category": "people",
+ "moji": "🙌🏼",
+ "description": "person raising both hands in celebration tone 2",
"unicodeVersion": "8.0",
- "digest": "36329bb0d01bfbd7f99458081ea6ff071317de2525879dc211dafc4e2e7c299b"
+ "digest": "4bd2f620dba790a58a42ab6d234de14772e4224994b929d731acc50ed6a8d259"
},
- "bicyclist_tone3": {
- "category": "activity",
- "moji": "🚴🏽",
- "description": "bicyclist tone 3",
+ "raised_hands_tone3": {
+ "category": "people",
+ "moji": "🙌🏽",
+ "description": "person raising both hands in celebration tone 3",
"unicodeVersion": "8.0",
- "digest": "0ef3ff2cbf408d29b7921ffcdcea3ad7d1faa6ec6a1c1b20582a58e4850c6569"
+ "digest": "1db8bfc21ab03d98849684f6d32e8e333bb9b33dd540a38c4673a33ea78698d8"
},
- "bicyclist_tone4": {
- "category": "activity",
- "moji": "🚴🏾",
- "description": "bicyclist tone 4",
+ "raised_hands_tone4": {
+ "category": "people",
+ "moji": "🙌🏾",
+ "description": "person raising both hands in celebration tone 4",
"unicodeVersion": "8.0",
- "digest": "9283b7e63bd6a17048e33661e6792568d889ba0f3655a359db159589d6767a47"
+ "digest": "65991dd419d1a02f126dfc401252faa929eb9b437d990b7802a2323ce6e929cb"
},
- "bicyclist_tone5": {
- "category": "activity",
- "moji": "🚴🏿",
- "description": "bicyclist tone 5",
+ "raised_hands_tone5": {
+ "category": "people",
+ "moji": "🙌🏿",
+ "description": "person raising both hands in celebration tone 5",
"unicodeVersion": "8.0",
- "digest": "c2849bb847cc9cbe75ec219ebbd2968891c1ebc2fca62bf86f362eba5e3d78f6"
+ "digest": "ffe251cc1a777836f40217630cb6652d3ec7f4f80c88045d1ec1430c8ce368d1"
},
- "bike": {
- "category": "travel",
- "moji": "🚲",
- "description": "bicycle",
+ "open_hands": {
+ "category": "people",
+ "moji": "👐",
+ "description": "open hands sign",
"unicodeVersion": "6.0",
- "digest": "65d94129ac0445495a0cf55f529f416682fa7a1dc7a16b275e3c267a132777ee"
+ "digest": "6200ecf734f542da4a9450f14d4254f2fb5a74fcae3e44a8e96dc639e1271779"
},
- "bikini": {
+ "open_hands_tone1": {
"category": "people",
- "moji": "👙",
- "description": "bikini",
+ "moji": "👐🏻",
+ "description": "open hands sign tone 1",
+ "unicodeVersion": "8.0",
+ "digest": "d07b0f6efe5a85d1f09f94a297520496659ab93002f9c2ac6d82c73cbadab400"
+ },
+ "open_hands_tone2": {
+ "category": "people",
+ "moji": "👐🏼",
+ "description": "open hands sign tone 2",
+ "unicodeVersion": "8.0",
+ "digest": "de7ad131fe4f9e4a20f24fe4bd22fd11a0b896c1bdf216c85045ad2a4407890e"
+ },
+ "open_hands_tone3": {
+ "category": "people",
+ "moji": "👐🏽",
+ "description": "open hands sign tone 3",
+ "unicodeVersion": "8.0",
+ "digest": "a607b5ca058c240eb530031ec63d57bb4290fa7df42c71a63bc7511759712ae1"
+ },
+ "open_hands_tone4": {
+ "category": "people",
+ "moji": "👐🏾",
+ "description": "open hands sign tone 4",
+ "unicodeVersion": "8.0",
+ "digest": "f101e21968a7bc2365758664eb3d121ecc019b2555e2f6d6f984257ba3237b87"
+ },
+ "open_hands_tone5": {
+ "category": "people",
+ "moji": "👐🏿",
+ "description": "open hands sign tone 5",
+ "unicodeVersion": "8.0",
+ "digest": "908cd7f4bdfb6670358287dee92cfdabd18a802f0b7d673f07f3cbb46689e91f"
+ },
+ "handshake": {
+ "category": "people",
+ "moji": "🤝",
+ "description": "handshake",
+ "unicodeVersion": "9.0",
+ "digest": "59e0dfc9059d643a6e88a2c498ac62175876f78394c68605e984d25597f4e177"
+ },
+ "handshake_tone1": {
+ "category": "people",
+ "moji": "🤝🏻",
+ "description": "handshake tone 1",
+ "unicodeVersion": "9.0",
+ "digest": "d16474fe7d1eb0e3b6441111370051caf5b20847d4080a89c4474a6af8c9f711"
+ },
+ "handshake_tone2": {
+ "category": "people",
+ "moji": "🤝🏼",
+ "description": "handshake tone 2",
+ "unicodeVersion": "9.0",
+ "digest": "f101fcd171fedad33bb5a90bc51cdbd5041902adef5e94f4893eafdac76512a0"
+ },
+ "handshake_tone3": {
+ "category": "people",
+ "moji": "🤝🏽",
+ "description": "handshake tone 3",
+ "unicodeVersion": "9.0",
+ "digest": "9a519adf10d9bd3ef8f0c94beeeb64de8922d85b428e74663f9a9eec3be15a71"
+ },
+ "handshake_tone4": {
+ "category": "people",
+ "moji": "🤝🏾",
+ "description": "handshake tone 4",
+ "unicodeVersion": "9.0",
+ "digest": "e0aff05eaed64f9c03c91a821982d425bd4f237b0fb09fffb23476912b100ffa"
+ },
+ "handshake_tone5": {
+ "category": "people",
+ "moji": "🤝🏿",
+ "description": "handshake tone 5",
+ "unicodeVersion": "9.0",
+ "digest": "76ec70c0b6bfca7dfc3d2b202f36c6cd59c030e6414c49f3f1e3f68cee77be9b"
+ },
+ "pray": {
+ "category": "people",
+ "moji": "🙏",
+ "description": "person with folded hands",
"unicodeVersion": "6.0",
- "digest": "f7bf17cea90c1d7e18c55af498ea3019e1515efefd4f692b868998861d40000e"
+ "digest": "175a97bcdf0110e4a435cc760d5203b8493c5bbf6d4ce70fb7a5643b2d1021dc"
},
- "biohazard": {
- "category": "symbols",
- "moji": "☣",
- "description": "biohazard sign",
+ "pray_tone1": {
+ "category": "people",
+ "moji": "🙏🏻",
+ "description": "person with folded hands tone 1",
+ "unicodeVersion": "8.0",
+ "digest": "0a453f3e7292c7d813da211b9d1359c6d893426077b010bca5a4bc3f17a695c1"
+ },
+ "pray_tone2": {
+ "category": "people",
+ "moji": "🙏🏼",
+ "description": "person with folded hands tone 2",
+ "unicodeVersion": "8.0",
+ "digest": "f66640530c3818fff333ebd6636dfb912aa6e986060ad03555766bbc5888d8b4"
+ },
+ "pray_tone3": {
+ "category": "people",
+ "moji": "🙏🏽",
+ "description": "person with folded hands tone 3",
+ "unicodeVersion": "8.0",
+ "digest": "5d8f49ada7ce2e5c473220aa881b32cda4fe73c024ec9260fd2af09b55478239"
+ },
+ "pray_tone4": {
+ "category": "people",
+ "moji": "🙏🏾",
+ "description": "person with folded hands tone 4",
+ "unicodeVersion": "8.0",
+ "digest": "c830c04f893a424a6694c478d8356a549a43b2ba3e9e912becc018178ca0c54c"
+ },
+ "pray_tone5": {
+ "category": "people",
+ "moji": "🙏🏿",
+ "description": "person with folded hands tone 5",
+ "unicodeVersion": "8.0",
+ "digest": "3fc8c39440ac0ae1646949dad41cc56305e51fbd299378de5356be3e5b4bda24"
+ },
+ "writing_hand": {
+ "category": "people",
+ "moji": "✍",
+ "description": "writing hand",
"unicodeVersion": "1.1",
- "digest": "aac5d8bf19b5e33c0a1d600a5e86522023415fb2a98160c58f541c862e439f9a"
+ "digest": "efe3bfa1c36098242e2c4cb2d66e8d76fd3c0bba57335059dd1ad25748752c41"
},
- "bird": {
- "category": "nature",
- "moji": "🐦",
- "description": "bird",
- "unicodeVersion": "6.0",
- "digest": "b19660ba47b3a0151dc470064a6594cdb9aaaa14a42a0a18d70c253206c954e5"
+ "writing_hand_tone1": {
+ "category": "people",
+ "moji": "✍🏻",
+ "description": "writing hand tone 1",
+ "unicodeVersion": "8.0",
+ "digest": "13e6782eea27e216721c4d1aeaa049f9ae3d93c9052f131cc17618102d16e4f6"
},
- "birthday": {
- "category": "food",
- "moji": "🎂",
- "description": "birthday cake",
+ "writing_hand_tone2": {
+ "category": "people",
+ "moji": "✍🏼",
+ "description": "writing hand tone 2",
+ "unicodeVersion": "8.0",
+ "digest": "6ec5eeef36ef27fae05f2d3ac9bd97460d75b08fa52e01e239cbbe4101eb8ad8"
+ },
+ "writing_hand_tone3": {
+ "category": "people",
+ "moji": "✍🏽",
+ "description": "writing hand tone 3",
+ "unicodeVersion": "8.0",
+ "digest": "e3d68b62cbda6579641f49b45581deca7653e6322df43413ee6b80967861e315"
+ },
+ "writing_hand_tone4": {
+ "category": "people",
+ "moji": "✍🏾",
+ "description": "writing hand tone 4",
+ "unicodeVersion": "8.0",
+ "digest": "52b7652348605b384761de53c28ffc3dc84b1b34210f795b40bb3e6235b8a959"
+ },
+ "writing_hand_tone5": {
+ "category": "people",
+ "moji": "✍🏿",
+ "description": "writing hand tone 5",
+ "unicodeVersion": "8.0",
+ "digest": "ce22c8e6c71ca3a07d32fb0c336301d015b81975a1019f20760ee908349c76dd"
+ },
+ "nail_care": {
+ "category": "people",
+ "moji": "💅",
+ "description": "nail polish",
"unicodeVersion": "6.0",
- "digest": "f8087a3df94702f990da6a6dd1a0f143f679aa84ea0d178e030dd13e9d414bfa"
+ "digest": "1f88d57808e1e93dd8870eb24235b1700337b441ea96b7ddabfdae1377bd5795"
},
- "black_circle": {
- "category": "symbols",
- "moji": "⚫",
- "description": "medium black circle",
- "unicodeVersion": "4.1",
- "digest": "dd472d37a09519c4b35d7cce0c1d988cc78abc7eec6ab794f49c75ac1c506450"
+ "nail_care_tone1": {
+ "category": "people",
+ "moji": "💅🏻",
+ "description": "nail polish tone 1",
+ "unicodeVersion": "8.0",
+ "digest": "4faa570dee925e2eaebfc2cd8ccd93dcefd5087e38b6aedadd356ae5234e89a5"
},
- "black_heart": {
- "category": "symbols",
- "moji": "🖤",
- "description": "black heart",
+ "nail_care_tone2": {
+ "category": "people",
+ "moji": "💅🏼",
+ "description": "nail polish tone 2",
+ "unicodeVersion": "8.0",
+ "digest": "320a8d5586f108d66b39cc6df25677fed43bb02914c3c867736881af4195d739"
+ },
+ "nail_care_tone3": {
+ "category": "people",
+ "moji": "💅🏽",
+ "description": "nail polish tone 3",
+ "unicodeVersion": "8.0",
+ "digest": "70d8d5a203e23ada34c18c2f368352efb247823a3db919c75c02f659bdc2601f"
+ },
+ "nail_care_tone4": {
+ "category": "people",
+ "moji": "💅🏾",
+ "description": "nail polish tone 4",
+ "unicodeVersion": "8.0",
+ "digest": "71d4e886b73ccdbdab9963bedbd23cbaa3693af5e2b9911db8a51d3d3fbb9843"
+ },
+ "nail_care_tone5": {
+ "category": "people",
+ "moji": "💅🏿",
+ "description": "nail polish tone 5",
+ "unicodeVersion": "8.0",
+ "digest": "c3cd47041cf0095e03893e165c9b591d7497abf7271588463ef9e8ca42191108"
+ },
+ "selfie": {
+ "category": "people",
+ "moji": "🤳",
+ "description": "selfie",
"unicodeVersion": "9.0",
- "digest": "d32225e69f8013ed033687a275d2cf0bf19bc7d38f54353ae8441ec50ca25e0b"
+ "digest": "4914fbc5b8a0838d275c286b7a3626c9353e233ef75f2b1f600a647ff56a7ff4"
},
- "black_joker": {
- "category": "symbols",
- "moji": "🃏",
- "description": "playing card black joker",
+ "selfie_tone1": {
+ "category": "people",
+ "moji": "🤳🏻",
+ "description": "selfie tone 1",
+ "unicodeVersion": "9.0",
+ "digest": "0e9e3090566876f49dc5a03321e190dce2737a5c8547b07f0244056d59623182"
+ },
+ "selfie_tone2": {
+ "category": "people",
+ "moji": "🤳🏼",
+ "description": "selfie tone 2",
+ "unicodeVersion": "9.0",
+ "digest": "5d2c271f2cf39d3ffacf1192b1804edef0849a2ad8f0e81e493a752960de97ed"
+ },
+ "selfie_tone3": {
+ "category": "people",
+ "moji": "🤳🏽",
+ "description": "selfie tone 3",
+ "unicodeVersion": "9.0",
+ "digest": "41670b6b45ab178205692a6bca8816e79f99ed7bbab1754b99bcf973ff178e30"
+ },
+ "selfie_tone4": {
+ "category": "people",
+ "moji": "🤳🏾",
+ "description": "selfie tone 4",
+ "unicodeVersion": "9.0",
+ "digest": "4b1c5145f0e454aed5c37b3dd3bef07dfa3576880997e6bd552871de474867ec"
+ },
+ "selfie_tone5": {
+ "category": "people",
+ "moji": "🤳🏿",
+ "description": "selfie tone 5",
+ "unicodeVersion": "9.0",
+ "digest": "bbe0db1f762ad830a38a6ff85e36565c0ef446c22b8ab25054d6b266f1b8421d"
+ },
+ "muscle": {
+ "category": "people",
+ "moji": "💪",
+ "description": "flexed biceps",
"unicodeVersion": "6.0",
- "digest": "40b7dccd258d4d1323fc744fb545e0e06c1e813bc9030147fb28e0f5b3a76ef3"
+ "digest": "5e6bff383eb4b63009779c1872797eb8b6651788b4005fa0af12b254bb67d404"
},
- "black_large_square": {
- "category": "symbols",
- "moji": "⬛",
- "description": "black large square",
- "unicodeVersion": "5.1",
- "digest": "2c606063c452385e38808861d820d799c25450d777c58faa3a58030e32d40d0c"
+ "muscle_tone1": {
+ "category": "people",
+ "moji": "💪🏻",
+ "description": "flexed biceps tone 1",
+ "unicodeVersion": "8.0",
+ "digest": "921fd25fcc812896f4d4f8ba7f84af84adc72f624ec464a8a3d7414a788d25e7"
},
- "black_medium_small_square": {
- "category": "symbols",
- "moji": "◾",
- "description": "black medium small square",
- "unicodeVersion": "3.2",
- "digest": "abd57777f919013b8c0686541263d62492ffd042a1d4b05000856dd1a3ff33ef"
+ "muscle_tone2": {
+ "category": "people",
+ "moji": "💪🏼",
+ "description": "flexed biceps tone 2",
+ "unicodeVersion": "8.0",
+ "digest": "e43dd3f98fa6e0916a3da459a66d8e39cc4d83aab68848343fa830b589ad39d2"
},
- "black_medium_square": {
- "category": "symbols",
- "moji": "◼",
- "description": "black medium square",
- "unicodeVersion": "3.2",
- "digest": "7811b577922f508cf89ac34abdbe5285213b3519de340fe1d57ca6780a500066"
+ "muscle_tone3": {
+ "category": "people",
+ "moji": "💪🏽",
+ "description": "flexed biceps tone 3",
+ "unicodeVersion": "8.0",
+ "digest": "508b89b10736e79d6e951cdaf0418912dfed191b464ffe93cfd8621493d7b382"
},
- "black_nib": {
- "category": "objects",
- "moji": "✒",
- "description": "black nib",
- "unicodeVersion": "1.1",
- "digest": "196230be1ae39d5841e9ed322f74816c61e47c4235dab7abff0e6d86fba786ed"
+ "muscle_tone4": {
+ "category": "people",
+ "moji": "💪🏾",
+ "description": "flexed biceps tone 4",
+ "unicodeVersion": "8.0",
+ "digest": "ac2e279defdba9ba232b254936f5069f3a17313454d5f2c2f7ca62084a6b50d8"
},
- "black_small_square": {
- "category": "symbols",
- "moji": "▪",
- "description": "black small square",
- "unicodeVersion": "1.1",
- "digest": "958483632202cf3181ef96532b0371cad1ea9b693100def41df69388b1ee468a"
+ "muscle_tone5": {
+ "category": "people",
+ "moji": "💪🏿",
+ "description": "flexed biceps tone 5",
+ "unicodeVersion": "8.0",
+ "digest": "91b6e61e0815b7f746dae45fae5cdb15d9f4b8608188673260d7e4aa134f58ed"
},
- "black_square_button": {
- "category": "symbols",
- "moji": "🔲",
- "description": "black square button",
+ "ear": {
+ "category": "people",
+ "moji": "👂",
+ "description": "ear",
"unicodeVersion": "6.0",
- "digest": "2211e8f2193751c4d1716a4d13145e945293f0158d413333829e7a20a265e55b"
+ "digest": "29f21bcb6963c709173aa895704029e48f233493ece1f0bb442705d90713e20e"
},
- "blossom": {
- "category": "nature",
- "moji": "🌼",
- "description": "blossom",
+ "ear_tone1": {
+ "category": "people",
+ "moji": "👂🏻",
+ "description": "ear tone 1",
+ "unicodeVersion": "8.0",
+ "digest": "5974f347c09ed99eb26af3b38ed5ff6d270920d5a5cf4bc2ca1e4ce1c6ca86d8"
+ },
+ "ear_tone2": {
+ "category": "people",
+ "moji": "👂🏼",
+ "description": "ear tone 2",
+ "unicodeVersion": "8.0",
+ "digest": "10a215f5a1b0ca5c39abb4b2ba1c95fc541b21d03eb74b437a732bc155772b7f"
+ },
+ "ear_tone3": {
+ "category": "people",
+ "moji": "👂🏽",
+ "description": "ear tone 3",
+ "unicodeVersion": "8.0",
+ "digest": "339cb06fc200e7a2f175de0862fa0f69792725532002e26446a64b47bae4e7f0"
+ },
+ "ear_tone4": {
+ "category": "people",
+ "moji": "👂🏾",
+ "description": "ear tone 4",
+ "unicodeVersion": "8.0",
+ "digest": "4fc70d5c353ad59518bb3829b0c544a4092b59924bf6fe0dd573f8fee3d00c68"
+ },
+ "ear_tone5": {
+ "category": "people",
+ "moji": "👂🏿",
+ "description": "ear tone 5",
+ "unicodeVersion": "8.0",
+ "digest": "fa4d1bdab5ed8dc2e8976edb389571c21942cbdcf7ad51281b4e064a2b260a58"
+ },
+ "nose": {
+ "category": "people",
+ "moji": "👃",
+ "description": "nose",
"unicodeVersion": "6.0",
- "digest": "4b330c92cc58a402534cb0bbbde2d18d680a88da5e1321d460e2d5ce2644d774"
+ "digest": "b53c2b2226bbaf8505e196a760717d503e7bd886c0b3a87c08bc56138867492e"
},
- "blowfish": {
- "category": "nature",
- "moji": "🐡",
- "description": "blowfish",
+ "nose_tone1": {
+ "category": "people",
+ "moji": "👃🏻",
+ "description": "nose tone 1",
+ "unicodeVersion": "8.0",
+ "digest": "bfc69572dac70db3a59abaac127f854ba648d3a8acf4d5dd0478d6c381910776"
+ },
+ "nose_tone2": {
+ "category": "people",
+ "moji": "👃🏼",
+ "description": "nose tone 2",
+ "unicodeVersion": "8.0",
+ "digest": "b6dfa564b8c1859930b3f01da0131c43e3e96f055e45ff7de166493e1d14aab1"
+ },
+ "nose_tone3": {
+ "category": "people",
+ "moji": "👃🏽",
+ "description": "nose tone 3",
+ "unicodeVersion": "8.0",
+ "digest": "1dc363dd57dda74e17467b06eb82bea745bf48faca589ffca709cc1e8dd4ad5e"
+ },
+ "nose_tone4": {
+ "category": "people",
+ "moji": "👃🏾",
+ "description": "nose tone 4",
+ "unicodeVersion": "8.0",
+ "digest": "76c8919041fa06f25d0bc2eb80d8491f673a1f708680bad1e5f8240d1d97949b"
+ },
+ "nose_tone5": {
+ "category": "people",
+ "moji": "👃🏿",
+ "description": "nose tone 5",
+ "unicodeVersion": "8.0",
+ "digest": "08db1747f19cf6f1129f38f5e9fcecc51d65043cfd408cb3b6ccbbd9f491165e"
+ },
+ "eyes": {
+ "category": "people",
+ "moji": "👀",
+ "description": "eyes",
"unicodeVersion": "6.0",
- "digest": "084c33958f40081d19012e35fcf72f085cf1f749964ba7256d8721e8a2d929e0"
+ "digest": "659c9a040950bd320a33177be730c086cc6cf390d1b99d7cbf3e6bc7705f21bf"
},
- "blue_book": {
- "category": "objects",
- "moji": "📘",
- "description": "blue book",
+ "eye": {
+ "category": "people",
+ "moji": "👁",
+ "description": "eye",
+ "unicodeVersion": "7.0",
+ "digest": "d48868ab77a09456b5f80553f08af363983ead45173759e3ff8a9ddf626effc2"
+ },
+ "tongue": {
+ "category": "people",
+ "moji": "👅",
+ "description": "tongue",
"unicodeVersion": "6.0",
- "digest": "bc47b3b8f1bfa21c2a4bf185cc247e0c9fc7f4f221292c3b0286e13b32f7a6da"
+ "digest": "da75f4b8859b698b941cd091e1d3bf4be3c4e86bb72b2407b9d7b9abe063ed84"
},
- "blue_car": {
- "category": "travel",
- "moji": "🚙",
- "description": "recreational vehicle",
+ "lips": {
+ "category": "people",
+ "moji": "👄",
+ "description": "mouth",
"unicodeVersion": "6.0",
- "digest": "c0faa7f7d4344391478aba15cb4cbfbbe1eb5fd5cdcdb9afdb7d49984abb8108"
+ "digest": "26fb9b50ab57120d2bc613eb1537fc1517f0c962bf3ae0b43fb2da42581dde5c"
},
- "blue_heart": {
- "category": "symbols",
- "moji": "💙",
- "description": "blue heart",
+ "baby": {
+ "category": "people",
+ "moji": "👶",
+ "description": "baby",
"unicodeVersion": "6.0",
- "digest": "dde3ac8cbd84903c39310d0a9a0121f47e1701bf3298d0cecaba552076b462c1"
+ "digest": "16999f24cdb63691baf5a2a8252de265a8a73bbd10ba0d4214c85b2f43c5b02e"
},
- "blush": {
+ "baby_tone1": {
"category": "people",
- "moji": "😊",
- "description": "smiling face with smiling eyes",
+ "moji": "👶🏻",
+ "description": "baby tone 1",
+ "unicodeVersion": "8.0",
+ "digest": "f6262f7773de6b2cdbd5f0464ccbdd9584d8077d11c8b27cf353329d8dfdbded"
+ },
+ "baby_tone2": {
+ "category": "people",
+ "moji": "👶🏼",
+ "description": "baby tone 2",
+ "unicodeVersion": "8.0",
+ "digest": "c90cf6fa9a95e38ab449cfd25bd422e4ead60351666dc9221ade4e2eac17f38e"
+ },
+ "baby_tone3": {
+ "category": "people",
+ "moji": "👶🏽",
+ "description": "baby tone 3",
+ "unicodeVersion": "8.0",
+ "digest": "290215d0ad2cd110ea918fa7020aff5b5e8b08849ff2a5acae5ed1f8e9f59958"
+ },
+ "baby_tone4": {
+ "category": "people",
+ "moji": "👶🏾",
+ "description": "baby tone 4",
+ "unicodeVersion": "8.0",
+ "digest": "dfda26c31ab16afde0500631e08875205deb3ebd87bbb96e8b746ae49ecefa11"
+ },
+ "baby_tone5": {
+ "category": "people",
+ "moji": "👶🏿",
+ "description": "baby tone 5",
+ "unicodeVersion": "8.0",
+ "digest": "8c32dba5be741fb3c009e55e9766526dbfec3e90977c4ab24b8ee3472a38f8e8"
+ },
+ "boy": {
+ "category": "people",
+ "moji": "👦",
+ "description": "boy",
"unicodeVersion": "6.0",
- "digest": "3d7d115f7da861e3565a446d9aea177c7bc27592bac17d0d96d815d853b68467"
+ "digest": "7a5a1eb2e38f835e8fe6506a78f0f05dd4e831bdd8ea30f3f5f473bc1d28baa2"
},
- "boar": {
- "category": "nature",
- "moji": "🐗",
- "description": "boar",
+ "boy_tone1": {
+ "category": "people",
+ "moji": "👦🏻",
+ "description": "boy tone 1",
+ "unicodeVersion": "8.0",
+ "digest": "b0edd5f1171b2969e5273cb0be90d57f2d7c605ff413478eabecb9501bcfe811"
+ },
+ "boy_tone2": {
+ "category": "people",
+ "moji": "👦🏼",
+ "description": "boy tone 2",
+ "unicodeVersion": "8.0",
+ "digest": "52f5dba881f3fb1867619dfc35d503022e6455a475137d2d5366830fe6966e09"
+ },
+ "boy_tone3": {
+ "category": "people",
+ "moji": "👦🏽",
+ "description": "boy tone 3",
+ "unicodeVersion": "8.0",
+ "digest": "57d11252ed3e92e5093ee70a11549f98f70f623a18b75dd8736dc5908f361c8c"
+ },
+ "boy_tone4": {
+ "category": "people",
+ "moji": "👦🏾",
+ "description": "boy tone 4",
+ "unicodeVersion": "8.0",
+ "digest": "b18387f39f18a50afc737c13d5387a904d27210e5e147e0ad4f0966f9091156b"
+ },
+ "boy_tone5": {
+ "category": "people",
+ "moji": "👦🏿",
+ "description": "boy tone 5",
+ "unicodeVersion": "8.0",
+ "digest": "37527ef42b6e163d273b95178e0c385c46172e45bac09070e421fa8230d5b04c"
+ },
+ "girl": {
+ "category": "people",
+ "moji": "👧",
+ "description": "girl",
"unicodeVersion": "6.0",
- "digest": "0e6774094f935bf5eaccc85077e357d2f002276a76aa7a3ba8457e11c7458314"
+ "digest": "5b6e2d936ee671b82328adbb0d77a8a23ad169aa455f8f3e07a974620d62b698"
},
- "bomb": {
- "category": "objects",
- "moji": "💣",
- "description": "bomb",
+ "girl_tone1": {
+ "category": "people",
+ "moji": "👧🏻",
+ "description": "girl tone 1",
+ "unicodeVersion": "8.0",
+ "digest": "34f82173f73d9e3048ffd339c480ae72017be5fb99616f40cb90ca507ce81365"
+ },
+ "girl_tone2": {
+ "category": "people",
+ "moji": "👧🏼",
+ "description": "girl tone 2",
+ "unicodeVersion": "8.0",
+ "digest": "e1efcff7f827dbea4258ec5cb0b40555266119e1f2e4526c251ea166ec252eb7"
+ },
+ "girl_tone3": {
+ "category": "people",
+ "moji": "👧🏽",
+ "description": "girl tone 3",
+ "unicodeVersion": "8.0",
+ "digest": "2589873f9c4792091359126462bfb83d3f14bf1bcf977b834e33d11969c551d5"
+ },
+ "girl_tone4": {
+ "category": "people",
+ "moji": "👧🏾",
+ "description": "girl tone 4",
+ "unicodeVersion": "8.0",
+ "digest": "af208f0ab6e4e26b80d6bb02994ef6f38d9395414e23978973d651c4bc895367"
+ },
+ "girl_tone5": {
+ "category": "people",
+ "moji": "👧🏿",
+ "description": "girl tone 5",
+ "unicodeVersion": "8.0",
+ "digest": "253012160b7a9a57708377062b43396ef620b9b63784736e1e6180f320715dfb"
+ },
+ "person_with_blond_hair": {
+ "category": "people",
+ "moji": "👱",
+ "description": "person with blond hair",
"unicodeVersion": "6.0",
- "digest": "c06294f2bc2ca60c09544598a699662609f41bd06efa01d92d8ab7b48356b830"
+ "digest": "9194bee1257b169fdcb1858023922b83a0ea10cb2f6c2bbc8824e1d15250416d"
},
- "book": {
- "category": "objects",
- "moji": "📖",
- "description": "open book",
+ "person_with_blond_hair_tone1": {
+ "category": "people",
+ "moji": "👱🏻",
+ "description": "person with blond hair tone 1",
+ "unicodeVersion": "8.0",
+ "digest": "bbbd79e4de2b447b081cbc82426f7d2584760489576426768cdf7e1e0e43c249"
+ },
+ "person_with_blond_hair_tone2": {
+ "category": "people",
+ "moji": "👱🏼",
+ "description": "person with blond hair tone 2",
+ "unicodeVersion": "8.0",
+ "digest": "23885c8e9d0d34eadf6941e6001c3f03863ebcef8c97733929cb0f61a03b48a9"
+ },
+ "person_with_blond_hair_tone3": {
+ "category": "people",
+ "moji": "👱🏽",
+ "description": "person with blond hair tone 3",
+ "unicodeVersion": "8.0",
+ "digest": "87ab121eb227c13fbe454537078761484acf1f09eeae4981bb46021ccd7b71de"
+ },
+ "person_with_blond_hair_tone4": {
+ "category": "people",
+ "moji": "👱🏾",
+ "description": "person with blond hair tone 4",
+ "unicodeVersion": "8.0",
+ "digest": "cd12e7c5a19a4a1b5445ecf98adca9e3ac4361a9b01ff8a18d13693af50bbf3a"
+ },
+ "person_with_blond_hair_tone5": {
+ "category": "people",
+ "moji": "👱🏿",
+ "description": "person with blond hair tone 5",
+ "unicodeVersion": "8.0",
+ "digest": "d11a5989a12155e1bb3432d672aff88311afc947a65c3e1c640b9e1d42cdeb0c"
+ },
+ "man": {
+ "category": "people",
+ "moji": "👨",
+ "description": "man",
"unicodeVersion": "6.0",
- "digest": "07f1af90a8f2bdddaa585b4da1da9ac7c52dbb7365d409d264e2b66f43b165d7"
+ "digest": "72f5a4ee76d2f91fcce52673cdf08867d9322dde33b2b859a2687d20f2875d7a"
},
- "bookmark": {
- "category": "objects",
- "moji": "🔖",
- "description": "bookmark",
+ "man_tone1": {
+ "category": "people",
+ "moji": "👨🏻",
+ "description": "man tone 1",
+ "unicodeVersion": "8.0",
+ "digest": "fd5531169631dbed6b6380c40732afda0d943980e2a399d9ce65167026f9f33c"
+ },
+ "man_tone2": {
+ "category": "people",
+ "moji": "👨🏼",
+ "description": "man tone 2",
+ "unicodeVersion": "8.0",
+ "digest": "628574c3994302e2d1ee0742951f26eb3d44aa1abe1161f78df5db3d18bc377f"
+ },
+ "man_tone3": {
+ "category": "people",
+ "moji": "👨🏽",
+ "description": "man tone 3",
+ "unicodeVersion": "8.0",
+ "digest": "431642916465938d09bd6ca5097e716abae550dc5c54dd514519e1561f372b90"
+ },
+ "man_tone4": {
+ "category": "people",
+ "moji": "👨🏾",
+ "description": "man tone 4",
+ "unicodeVersion": "8.0",
+ "digest": "26662b4aca1f0a9a24b148a36db1f1119a28e458b1b9addd4ecb4fa36c4c3d3d"
+ },
+ "man_tone5": {
+ "category": "people",
+ "moji": "👨🏿",
+ "description": "man tone 5",
+ "unicodeVersion": "8.0",
+ "digest": "e2f45518af9033350ad4c44a42f564403a7248b4d67c8ec21b19199458aaa4de"
+ },
+ "woman": {
+ "category": "people",
+ "moji": "👩",
+ "description": "woman",
"unicodeVersion": "6.0",
- "digest": "cafb404fab72e67a7c0254ebe91f9631be9a6f7da763bbd948358e8544e2fd53"
+ "digest": "7f06a5df2103c959228c15f44a76d39f87f792bef6aaadf7e4f47fe31a0e85fa"
},
- "bookmark_tabs": {
- "category": "objects",
- "moji": "📑",
- "description": "bookmark tabs",
+ "woman_tone1": {
+ "category": "people",
+ "moji": "👩🏻",
+ "description": "woman tone 1",
+ "unicodeVersion": "8.0",
+ "digest": "e1e1bc0d9e6c06fc37e54251c9d492c83852016baeb16acfedd7242a0f4a289e"
+ },
+ "woman_tone2": {
+ "category": "people",
+ "moji": "👩🏼",
+ "description": "woman tone 2",
+ "unicodeVersion": "8.0",
+ "digest": "f3e0dd2ee081ca179d8c70e3c5a77254f98880f732cabdf601d54b64ae8702cd"
+ },
+ "woman_tone3": {
+ "category": "people",
+ "moji": "👩🏽",
+ "description": "woman tone 3",
+ "unicodeVersion": "8.0",
+ "digest": "1ad974a8aad0dc2cb62d22e7c6c155bd07030222c3f115e62c153ce5cb6b240d"
+ },
+ "woman_tone4": {
+ "category": "people",
+ "moji": "👩🏾",
+ "description": "woman tone 4",
+ "unicodeVersion": "8.0",
+ "digest": "c11d39ec49db3d36256f372521461b9d668726cab6d63166ef23c9c99e3a9c55"
+ },
+ "woman_tone5": {
+ "category": "people",
+ "moji": "👩🏿",
+ "description": "woman tone 5",
+ "unicodeVersion": "8.0",
+ "digest": "7220ffd6a38cf1587f1c45e3461c38f65162b604fb4261318c342bb6f1a4d95f"
+ },
+ "older_man": {
+ "category": "people",
+ "moji": "👴",
+ "description": "older man",
"unicodeVersion": "6.0",
- "digest": "78450d6c894fee9badbed92a07144718908a305be825304dc47839e259884e9c"
+ "digest": "84b2a44e5fcc74bc599566caffd79472cdb5c521d7c1b011753d36b8a629cd3f"
},
- "books": {
- "category": "objects",
- "moji": "📚",
- "description": "books",
+ "older_man_tone1": {
+ "category": "people",
+ "moji": "👴🏻",
+ "description": "older man tone 1",
+ "unicodeVersion": "8.0",
+ "digest": "1f9e38cfb593a30d9afc29bdb18fa00ead144290dbd5a1c34190855e58f3f930"
+ },
+ "older_man_tone2": {
+ "category": "people",
+ "moji": "👴🏼",
+ "description": "older man tone 2",
+ "unicodeVersion": "8.0",
+ "digest": "ddef54810d7a168ada23e30686ea9a09d2abb440c230ac5c7823ef731b7aac49"
+ },
+ "older_man_tone3": {
+ "category": "people",
+ "moji": "👴🏽",
+ "description": "older man tone 3",
+ "unicodeVersion": "8.0",
+ "digest": "e8f881c06e41460f3e8bb37bba81f11a194b23f2354ddd7ddf3aae44faed2ad9"
+ },
+ "older_man_tone4": {
+ "category": "people",
+ "moji": "👴🏾",
+ "description": "older man tone 4",
+ "unicodeVersion": "8.0",
+ "digest": "924a841c5d3ef0a6e3a79d519c38bdff2dff6700dbf51985e06f819f6690b35e"
+ },
+ "older_man_tone5": {
+ "category": "people",
+ "moji": "👴🏿",
+ "description": "older man tone 5",
+ "unicodeVersion": "8.0",
+ "digest": "5f893aa727caa220c06c180099c20834abaeeeb7d7b29d64a72259ad7239e487"
+ },
+ "older_woman": {
+ "category": "people",
+ "moji": "👵",
+ "description": "older woman",
"unicodeVersion": "6.0",
- "digest": "566cb93cf32782ae39aec670e22b711fa33afd52bc75c9e7069174cd252125d1"
+ "digest": "b96d5fbaa0fe6d0d21998a617b2c07776542ee2ab1d79424e22b25a1906d42a5"
},
- "boom": {
- "category": "nature",
- "moji": "💥",
- "description": "collision symbol",
+ "older_woman_tone1": {
+ "category": "people",
+ "moji": "👵🏻",
+ "description": "older woman tone 1",
+ "unicodeVersion": "8.0",
+ "digest": "a5a0475ecc4b452d65f04d31f8ff838531724fbd59c3e00db7c70bc0d87d850f"
+ },
+ "older_woman_tone2": {
+ "category": "people",
+ "moji": "👵🏼",
+ "description": "older woman tone 2",
+ "unicodeVersion": "8.0",
+ "digest": "0ede94d29641f3cd592c084b1281a475db4e09a0c98ede0af8500b953e26730b"
+ },
+ "older_woman_tone3": {
+ "category": "people",
+ "moji": "👵🏽",
+ "description": "older woman tone 3",
+ "unicodeVersion": "8.0",
+ "digest": "6a4878555493bc7f80aea33c0766d2781ef427d96c1e73d623ee74a77c3dab6e"
+ },
+ "older_woman_tone4": {
+ "category": "people",
+ "moji": "👵🏾",
+ "description": "older woman tone 4",
+ "unicodeVersion": "8.0",
+ "digest": "cf7afc02b1d4fb584af1e224be6afa2410a39d3ae1ada553e683b6f14f50dda7"
+ },
+ "older_woman_tone5": {
+ "category": "people",
+ "moji": "👵🏿",
+ "description": "older woman tone 5",
+ "unicodeVersion": "8.0",
+ "digest": "35aebc57848405f369eacb1defafce49ecc0f3e3833b21e53e46353b8bcdf755"
+ },
+ "person_frowning": {
+ "category": "people",
+ "moji": "🙍",
+ "description": "person frowning",
"unicodeVersion": "6.0",
- "digest": "eb699268e39f7fa16a39fc731a7ebed9f7dcdbde083e222cffbac860c1bc643b"
+ "digest": "cc38a861c7edbc5eae6bb91beb2acfc5d044453960f612c36693ef71663b1d5e"
},
- "boot": {
+ "person_frowning_tone1": {
"category": "people",
- "moji": "👢",
- "description": "womans boots",
+ "moji": "🙍🏻",
+ "description": "person frowning tone 1",
+ "unicodeVersion": "8.0",
+ "digest": "d7350116b174ba8902292c382a410a458e86d68fd977d7e8e80ce8b47d80eb3e"
+ },
+ "person_frowning_tone2": {
+ "category": "people",
+ "moji": "🙍🏼",
+ "description": "person frowning tone 2",
+ "unicodeVersion": "8.0",
+ "digest": "09bfa39a6b743494967806691248ab94a90bb8efc934a3e08eb13cc9a1221989"
+ },
+ "person_frowning_tone3": {
+ "category": "people",
+ "moji": "🙍🏽",
+ "description": "person frowning tone 3",
+ "unicodeVersion": "8.0",
+ "digest": "3631d7c4d757007ca5b1543c0819b351fa70dd1fa76dc4e947bb074119837143"
+ },
+ "person_frowning_tone4": {
+ "category": "people",
+ "moji": "🙍🏾",
+ "description": "person frowning tone 4",
+ "unicodeVersion": "8.0",
+ "digest": "852c4794d10336cb636d615d702504cf58bbdcf5a7269e9bc3e2b215074d11a0"
+ },
+ "person_frowning_tone5": {
+ "category": "people",
+ "moji": "🙍🏿",
+ "description": "person frowning tone 5",
+ "unicodeVersion": "8.0",
+ "digest": "ac64dda090f40addeab42d79bee681060d3b43de56b461fb94883d67fb24e1c6"
+ },
+ "person_with_pouting_face": {
+ "category": "people",
+ "moji": "🙎",
+ "description": "person with pouting face",
"unicodeVersion": "6.0",
- "digest": "3ac88fcfbe55d073949e89aeeadf168c04633c9af4ce6673c897358e595511c2"
+ "digest": "687a13af899b65a14f27c205c9fffdc4ce50edc86b55f9dab05711f21a8fa06f"
},
- "bouquet": {
- "category": "nature",
- "moji": "💐",
- "description": "bouquet",
+ "person_with_pouting_face_tone1": {
+ "category": "people",
+ "moji": "🙎🏻",
+ "description": "person with pouting face tone1",
+ "unicodeVersion": "8.0",
+ "digest": "dd6d64d1d51bca83c0b3212d028394cdb84e8550e6740e3a862ce24379a522ea"
+ },
+ "person_with_pouting_face_tone2": {
+ "category": "people",
+ "moji": "🙎🏼",
+ "description": "person with pouting face tone2",
+ "unicodeVersion": "8.0",
+ "digest": "7ef959a93142351a77e62074a5b339afa63b7019b23e3b823e8d95a5c82863f2"
+ },
+ "person_with_pouting_face_tone3": {
+ "category": "people",
+ "moji": "🙎🏽",
+ "description": "person with pouting face tone3",
+ "unicodeVersion": "8.0",
+ "digest": "777c69c3b59b9688b5cda0cd61b67aaedf742c2758b4ea1537170f1648185dcf"
+ },
+ "person_with_pouting_face_tone4": {
+ "category": "people",
+ "moji": "🙎🏾",
+ "description": "person with pouting face tone4",
+ "unicodeVersion": "8.0",
+ "digest": "ee918ba63a9dbb71aa7a2a19552da4a86ecd3a15ff6b01b00595b04c98a8d3f4"
+ },
+ "person_with_pouting_face_tone5": {
+ "category": "people",
+ "moji": "🙎🏿",
+ "description": "person with pouting face tone5",
+ "unicodeVersion": "8.0",
+ "digest": "a1d4e4e89b7c8044a38a68d7cdb96d7ee576296689c996225e778af9b033dbfa"
+ },
+ "no_good": {
+ "category": "people",
+ "moji": "🙅",
+ "description": "face with no good gesture",
"unicodeVersion": "6.0",
- "digest": "0d737305d51cced40a4de5da6dacb5dfb049c7d8ad4136c312193049a9915a3b"
+ "digest": "9db57840d894a17c23a77c4a5ed315aa852d85ab2841e3393494c600b635f341"
+ },
+ "no_good_tone1": {
+ "category": "people",
+ "moji": "🙅🏻",
+ "description": "face with no good gesture tone 1",
+ "unicodeVersion": "8.0",
+ "digest": "ecb77f526ecedd1414faf252ec85a7f468ee0429c0684f69eefa28e3cf4e5710"
+ },
+ "no_good_tone2": {
+ "category": "people",
+ "moji": "🙅🏼",
+ "description": "face with no good gesture tone 2",
+ "unicodeVersion": "8.0",
+ "digest": "509882d9171d11cec481bb561d805c3719828954f71ac6b3ec86b34eeea85bfe"
+ },
+ "no_good_tone3": {
+ "category": "people",
+ "moji": "🙅🏽",
+ "description": "face with no good gesture tone 3",
+ "unicodeVersion": "8.0",
+ "digest": "7c8d9ba5acdbcad2e99cee95808de8d65f1d8c8a2474bb1a71aca04158e0a542"
+ },
+ "no_good_tone4": {
+ "category": "people",
+ "moji": "🙅🏾",
+ "description": "face with no good gesture tone 4",
+ "unicodeVersion": "8.0",
+ "digest": "64a58084e10960f9d2c118b927a6bba759f5aa0038988f1984e7578aee4b16e4"
+ },
+ "no_good_tone5": {
+ "category": "people",
+ "moji": "🙅🏿",
+ "description": "face with no good gesture tone 5",
+ "unicodeVersion": "8.0",
+ "digest": "20266de1fbdc6ccb83b4da026c7c71c853dc566d49a39ece5b7379f79c3cf68b"
+ },
+ "ok_woman": {
+ "category": "people",
+ "moji": "🙆",
+ "description": "face with ok gesture",
+ "unicodeVersion": "6.0",
+ "digest": "0b08d0e64c0a55e47d0f783034a46fbfe7a742388b2961dd13adcd91217f5551"
+ },
+ "ok_woman_tone1": {
+ "category": "people",
+ "moji": "🙆🏻",
+ "description": "face with ok gesture tone1",
+ "unicodeVersion": "8.0",
+ "digest": "daaa1c03e25a234c577fa82d868cd7341e4b21262be8a19ebc8ec52c1dfaf3be"
+ },
+ "ok_woman_tone2": {
+ "category": "people",
+ "moji": "🙆🏼",
+ "description": "face with ok gesture tone2",
+ "unicodeVersion": "8.0",
+ "digest": "4441187c1bfb3953731fa3cc98fca07f8ef499001af7c954e9e6c766bc113215"
+ },
+ "ok_woman_tone3": {
+ "category": "people",
+ "moji": "🙆🏽",
+ "description": "face with ok gesture tone3",
+ "unicodeVersion": "8.0",
+ "digest": "08ef7d0a47d54ba9b8ed19c5d317219ea6d675154085c0e311af845a3cc753f9"
+ },
+ "ok_woman_tone4": {
+ "category": "people",
+ "moji": "🙆🏾",
+ "description": "face with ok gesture tone4",
+ "unicodeVersion": "8.0",
+ "digest": "eb8183accaeae7920c7150c13f3d484fefc2868d3203760d4a1e42b04ac14e40"
+ },
+ "ok_woman_tone5": {
+ "category": "people",
+ "moji": "🙆🏿",
+ "description": "face with ok gesture tone5",
+ "unicodeVersion": "8.0",
+ "digest": "a8fbb88db77c35ce9fdf85ede6ed71de726277464613b46fcc024b0c38acc675"
+ },
+ "information_desk_person": {
+ "category": "people",
+ "moji": "💁",
+ "description": "information desk person",
+ "unicodeVersion": "6.0",
+ "digest": "ee9a1e94470a107b494840ac5b9bd0d59c38fcbc5d6a9f0842c910688ea9ec74"
+ },
+ "information_desk_person_tone1": {
+ "category": "people",
+ "moji": "💁🏻",
+ "description": "information desk person tone 1",
+ "unicodeVersion": "8.0",
+ "digest": "bb91f3dedc55cca320cd14343c04c388b4dfa870a9d5860957a0180f24161fea"
+ },
+ "information_desk_person_tone2": {
+ "category": "people",
+ "moji": "💁🏼",
+ "description": "information desk person tone 2",
+ "unicodeVersion": "8.0",
+ "digest": "a556b596a0d9cd480adaff344d3ba5c02a16ef2eb73695e9d9fc231ab8e3599f"
+ },
+ "information_desk_person_tone3": {
+ "category": "people",
+ "moji": "💁🏽",
+ "description": "information desk person tone 3",
+ "unicodeVersion": "8.0",
+ "digest": "b5a7ca77e15e749731fcd63da9c274c47eade033da8cfc8a6ab5413d939a8a8e"
+ },
+ "information_desk_person_tone4": {
+ "category": "people",
+ "moji": "💁🏾",
+ "description": "information desk person tone 4",
+ "unicodeVersion": "8.0",
+ "digest": "a7912c3121b1bb31b55c528812f54b89cdbc1982045e226c3cbce29c83970a59"
+ },
+ "information_desk_person_tone5": {
+ "category": "people",
+ "moji": "💁🏿",
+ "description": "information desk person tone 5",
+ "unicodeVersion": "8.0",
+ "digest": "a3e3fb0ae133513f565e5331a699a6d147200a5141a4e692fec86cfca03b9f5f"
+ },
+ "raising_hand": {
+ "category": "people",
+ "moji": "🙋",
+ "description": "happy person raising one hand",
+ "unicodeVersion": "6.0",
+ "digest": "b9cc5deb1d33ace5ab80f9b4fe7d2d277c2ee04dfdbc8bc3d41ca69edd3d5c52"
+ },
+ "raising_hand_tone1": {
+ "category": "people",
+ "moji": "🙋🏻",
+ "description": "happy person raising one hand tone1",
+ "unicodeVersion": "8.0",
+ "digest": "6d7aea2c2c9620448ffc6be9e5bf5cddcc7a1e18f9f816a030dc818d93d45d61"
+ },
+ "raising_hand_tone2": {
+ "category": "people",
+ "moji": "🙋🏼",
+ "description": "happy person raising one hand tone2",
+ "unicodeVersion": "8.0",
+ "digest": "8af44fa3d2d7eadc95c8193993eeeaea3bd764c1ac884d897804a074bba87a21"
+ },
+ "raising_hand_tone3": {
+ "category": "people",
+ "moji": "🙋🏽",
+ "description": "happy person raising one hand tone3",
+ "unicodeVersion": "8.0",
+ "digest": "cc39cadc42f9ec7fba6e74661e757f0d7a36fd10807fe90fddd511a07e290059"
+ },
+ "raising_hand_tone4": {
+ "category": "people",
+ "moji": "🙋🏾",
+ "description": "happy person raising one hand tone4",
+ "unicodeVersion": "8.0",
+ "digest": "7ef0c37921ced3c64898531b746eaf1e6ed7133ebefd74394e5b0164ee57085d"
+ },
+ "raising_hand_tone5": {
+ "category": "people",
+ "moji": "🙋🏿",
+ "description": "happy person raising one hand tone5",
+ "unicodeVersion": "8.0",
+ "digest": "96927ec26049ce6efc65917605f9039f9ec746eff23fe00c4f35ceb29d4f2c93"
},
"bow": {
"category": "people",
@@ -1042,13 +2911,6 @@
"unicodeVersion": "6.0",
"digest": "7f87a4acccfe257981459813185e6379477e5a892241c30e035db16d661119fe"
},
- "bow_and_arrow": {
- "category": "activity",
- "moji": "🏹",
- "description": "bow and arrow",
- "unicodeVersion": "8.0",
- "digest": "c98949db3391c74e299232ea99a28e035cc1cd747e281e52ccb9afd6a6ac40c3"
- },
"bow_tone1": {
"category": "people",
"moji": "🙇🏻",
@@ -1084,68 +2946,467 @@
"unicodeVersion": "8.0",
"digest": "123679fd642e0b47297e69becaa3d490e52c6b45a3ec810f50fe4e75744bf990"
},
- "bowling": {
- "category": "activity",
- "moji": "🎳",
- "description": "bowling",
+ "face_palm": {
+ "category": "people",
+ "moji": "🤦",
+ "description": "face palm",
+ "unicodeVersion": "9.0",
+ "digest": "60baca7855516c86883de6fd63f154b047d27325590c751089404e7287df56aa"
+ },
+ "face_palm_tone1": {
+ "category": "people",
+ "moji": "🤦🏻",
+ "description": "face palm tone 1",
+ "unicodeVersion": "9.0",
+ "digest": "f505ab5fc98f2e3bf5893f59484b90900e7c04c6f0467e0c89ccf99c5fd76715"
+ },
+ "face_palm_tone2": {
+ "category": "people",
+ "moji": "🤦🏼",
+ "description": "face palm tone 2",
+ "unicodeVersion": "9.0",
+ "digest": "9a3361cef95736983bbd54375ce9bf59c0fb1608f445da9dce79617cf05841bb"
+ },
+ "face_palm_tone3": {
+ "category": "people",
+ "moji": "🤦🏽",
+ "description": "face palm tone 3",
+ "unicodeVersion": "9.0",
+ "digest": "d53cda9e9909988ec6ca4b9ee5735bac82ca53ee890f1c0eca50471ddbb044d3"
+ },
+ "face_palm_tone4": {
+ "category": "people",
+ "moji": "🤦🏾",
+ "description": "face palm tone 4",
+ "unicodeVersion": "9.0",
+ "digest": "51d8cd0103356c08c4fe5cb6d68ac09688d08ea9253d6ccc09b4900730a89d80"
+ },
+ "face_palm_tone5": {
+ "category": "people",
+ "moji": "🤦🏿",
+ "description": "face palm tone 5",
+ "unicodeVersion": "9.0",
+ "digest": "7c044e1662880c143f8f2b15eee0d5aeb2c6831ee70ff5dcc3796f2585f5f6b0"
+ },
+ "shrug": {
+ "category": "people",
+ "moji": "🤷",
+ "description": "shrug",
+ "unicodeVersion": "9.0",
+ "digest": "1203afd3973f34c726c8e8ca66b76c2f1e7036a45d595d6f4cfd104c00d76d63"
+ },
+ "shrug_tone1": {
+ "category": "people",
+ "moji": "🤷🏻",
+ "description": "shrug tone 1",
+ "unicodeVersion": "9.0",
+ "digest": "fb6eb588f019cf7a25bac347cb2b5c124f9d333314d040daef8b70ed1de5032d"
+ },
+ "shrug_tone2": {
+ "category": "people",
+ "moji": "🤷🏼",
+ "description": "shrug tone 2",
+ "unicodeVersion": "9.0",
+ "digest": "a3b64ffa33f602adae12924b5576b6ed1f4bebbcf2db4952ec846b9778aadeaa"
+ },
+ "shrug_tone3": {
+ "category": "people",
+ "moji": "🤷🏽",
+ "description": "shrug tone 3",
+ "unicodeVersion": "9.0",
+ "digest": "e7148fbdb7194182d5f3c66e9a1e7a9e5a8c4d88ceab38f5d0ecd40bc231f6b8"
+ },
+ "shrug_tone4": {
+ "category": "people",
+ "moji": "🤷🏾",
+ "description": "shrug tone 4",
+ "unicodeVersion": "9.0",
+ "digest": "8977c5afbde154d944f965ff7f10d26167ea427b8b191a6ec8708c6517168334"
+ },
+ "shrug_tone5": {
+ "category": "people",
+ "moji": "🤷🏿",
+ "description": "shrug tone 5",
+ "unicodeVersion": "9.0",
+ "digest": "3968a312496e479a86b63274f529c2b283d180118c04edf3c6b6ea5f812c05bb"
+ },
+ "cop": {
+ "category": "people",
+ "moji": "👮",
+ "description": "police officer",
"unicodeVersion": "6.0",
- "digest": "2bc7f10afb9d6c61e015e4b6ca8c65c038a6a802840e1879142ee9bb86369b38"
+ "digest": "ca75e6722bc6520f3375af4d588cd5af15b52d5a856a67d99414e149d8ea9ca0"
},
- "boxing_glove": {
- "category": "activity",
- "moji": "🥊",
- "description": "boxing glove",
+ "cop_tone1": {
+ "category": "people",
+ "moji": "👮🏻",
+ "description": "police officer tone 1",
+ "unicodeVersion": "8.0",
+ "digest": "309280c5b2f6f957134015fe62a262b7853b67c6d7a020e59e942a21ac52c561"
+ },
+ "cop_tone2": {
+ "category": "people",
+ "moji": "👮🏼",
+ "description": "police officer tone 2",
+ "unicodeVersion": "8.0",
+ "digest": "2db60b30bb0533a82590190ba1e9aa2a4a21b13219c9b5fdf67e0c88a63cac59"
+ },
+ "cop_tone3": {
+ "category": "people",
+ "moji": "👮🏽",
+ "description": "police officer tone 3",
+ "unicodeVersion": "8.0",
+ "digest": "724bf942373fe976683449d871bd76275f0b21966abd69dfe046249731f17fee"
+ },
+ "cop_tone4": {
+ "category": "people",
+ "moji": "👮🏾",
+ "description": "police officer tone 4",
+ "unicodeVersion": "8.0",
+ "digest": "c95ba196d60964cdc5252cef740ee98369ce6a9553f6da55e8b0dc3e6cfc345b"
+ },
+ "cop_tone5": {
+ "category": "people",
+ "moji": "👮🏿",
+ "description": "police officer tone 5",
+ "unicodeVersion": "8.0",
+ "digest": "8aa857e06fa050ae584e2be28e61944e336f7b53d94ec10763be04ce9b59a812"
+ },
+ "spy": {
+ "category": "people",
+ "moji": "🕵",
+ "description": "sleuth or spy",
+ "unicodeVersion": "7.0",
+ "digest": "af9ec39cb9ec28b9b3917628187b28899f8f778b593ce722c59575af6a8cff75"
+ },
+ "spy_tone1": {
+ "category": "people",
+ "moji": "🕵🏻",
+ "description": "sleuth or spy tone 1",
+ "unicodeVersion": "8.0",
+ "digest": "26134ab9a163d03fde9a4ce7e77ba06acfd6a385d6534bd94cbd0430fdb56054"
+ },
+ "spy_tone2": {
+ "category": "people",
+ "moji": "🕵🏼",
+ "description": "sleuth or spy tone 2",
+ "unicodeVersion": "8.0",
+ "digest": "6f0d3ae8b0980c4586aaacdc8df8ed5815fe5f1fe18be566316cb0f21b1beeba"
+ },
+ "spy_tone3": {
+ "category": "people",
+ "moji": "🕵🏽",
+ "description": "sleuth or spy tone 3",
+ "unicodeVersion": "8.0",
+ "digest": "154818ede71c0b8ae5cdf621efe9560e419f62f67ad03049c29cf505bd160bbc"
+ },
+ "spy_tone4": {
+ "category": "people",
+ "moji": "🕵🏾",
+ "description": "sleuth or spy tone 4",
+ "unicodeVersion": "8.0",
+ "digest": "2ff959799064e9e47c9e1698357b04cc0fd1343bfc8b1be0d20fe49852b05f32"
+ },
+ "spy_tone5": {
+ "category": "people",
+ "moji": "🕵🏿",
+ "description": "sleuth or spy tone 5",
+ "unicodeVersion": "8.0",
+ "digest": "6046a8031fd6d410380d41f2a7cd658d9aec9b61a5759265a26ae1e2b9409096"
+ },
+ "guardsman": {
+ "category": "people",
+ "moji": "💂",
+ "description": "guardsman",
+ "unicodeVersion": "6.0",
+ "digest": "b8fe4960df2f06ada3c1d6d8a7c53bb7c01b976a7f43afce50c7dadc4840771b"
+ },
+ "guardsman_tone1": {
+ "category": "people",
+ "moji": "💂🏻",
+ "description": "guardsman tone 1",
+ "unicodeVersion": "8.0",
+ "digest": "0e7fbf3faddaac2246c318fac9a8f40908ad44c98bd6543091c601abe62a5c96"
+ },
+ "guardsman_tone2": {
+ "category": "people",
+ "moji": "💂🏼",
+ "description": "guardsman tone 2",
+ "unicodeVersion": "8.0",
+ "digest": "56a81767c023dd6ac42b586fc50804b34b4da8fcbbffe9a6d6e5dc12e89868f8"
+ },
+ "guardsman_tone3": {
+ "category": "people",
+ "moji": "💂🏽",
+ "description": "guardsman tone 3",
+ "unicodeVersion": "8.0",
+ "digest": "1c15ae587f90408781f5560366febafbee7e5318d1f618135408827ef82b09ee"
+ },
+ "guardsman_tone4": {
+ "category": "people",
+ "moji": "💂🏾",
+ "description": "guardsman tone 4",
+ "unicodeVersion": "8.0",
+ "digest": "4fd65aa93d212fd58e5c41dc70816d691635fa6fb271bf55d72c33f3acc72c4a"
+ },
+ "guardsman_tone5": {
+ "category": "people",
+ "moji": "💂🏿",
+ "description": "guardsman tone 5",
+ "unicodeVersion": "8.0",
+ "digest": "dcf7b0462d5b5b0ae09673c8ea3b7e48648edc28541680dc0adbaef1fb7e738d"
+ },
+ "construction_worker": {
+ "category": "people",
+ "moji": "👷",
+ "description": "construction worker",
+ "unicodeVersion": "6.0",
+ "digest": "47476b5eb51a1e00d0c95f032c79ca6472b41ece0bdf057bbb87da088605c589"
+ },
+ "construction_worker_tone1": {
+ "category": "people",
+ "moji": "👷🏻",
+ "description": "construction worker tone 1",
+ "unicodeVersion": "8.0",
+ "digest": "087fe998c1e316591d4a0c09c7ef80b0cf73d7f0f295e4bc18405e9153eb7b6c"
+ },
+ "construction_worker_tone2": {
+ "category": "people",
+ "moji": "👷🏼",
+ "description": "construction worker tone 2",
+ "unicodeVersion": "8.0",
+ "digest": "2a829b907f78a0d197677d19be96e177e0a021a04decb3c0bbf637cf74716787"
+ },
+ "construction_worker_tone3": {
+ "category": "people",
+ "moji": "👷🏽",
+ "description": "construction worker tone 3",
+ "unicodeVersion": "8.0",
+ "digest": "551d3bddffe6e7c739732a1d759994e2a8090e81df401a70c5b4eab315242840"
+ },
+ "construction_worker_tone4": {
+ "category": "people",
+ "moji": "👷🏾",
+ "description": "construction worker tone 4",
+ "unicodeVersion": "8.0",
+ "digest": "d563f84633598c86bbad12a96418eee7e7a6cd945d9ecdb60d93be56697b8d96"
+ },
+ "construction_worker_tone5": {
+ "category": "people",
+ "moji": "👷🏿",
+ "description": "construction worker tone 5",
+ "unicodeVersion": "8.0",
+ "digest": "56950bb7e235ccd82c8b1e689268ecd471809b8b65ec8654949a15cd6b42e7b5"
+ },
+ "prince": {
+ "category": "people",
+ "moji": "🤴",
+ "description": "prince",
"unicodeVersion": "9.0",
- "digest": "4acdaf7627f8a70148dadec0012474c3eb5042d2a165522c9a221cb6c63376c3"
+ "digest": "07fcbaf5d14e5e77d1a44cae21006ab245da1fd799773aa836a9bb2fdac24f61"
},
- "boy": {
+ "prince_tone1": {
"category": "people",
- "moji": "👦",
- "description": "boy",
+ "moji": "🤴🏻",
+ "description": "prince tone 1",
+ "unicodeVersion": "9.0",
+ "digest": "2db61fd0e8bb7d9d5941df7c680d2e4fff6b282d2a356c34bf32e55bf41d1245"
+ },
+ "prince_tone2": {
+ "category": "people",
+ "moji": "🤴🏼",
+ "description": "prince tone 2",
+ "unicodeVersion": "9.0",
+ "digest": "283dbc3550fe80ce1bf4f6eabd7e216026f3996f4e0fa9b9b881df8d741c26d3"
+ },
+ "prince_tone3": {
+ "category": "people",
+ "moji": "🤴🏽",
+ "description": "prince tone 3",
+ "unicodeVersion": "9.0",
+ "digest": "374f58411517a2c3532adad34108db23e8587a86d15e32aa0e67e8a82d1c2ef3"
+ },
+ "prince_tone4": {
+ "category": "people",
+ "moji": "🤴🏾",
+ "description": "prince tone 4",
+ "unicodeVersion": "9.0",
+ "digest": "a6d44eb1caf4dc0fbb1b5821809cd55bea127f5a6b4b4b7e259f161ef502a52b"
+ },
+ "prince_tone5": {
+ "category": "people",
+ "moji": "🤴🏿",
+ "description": "prince tone 5",
+ "unicodeVersion": "9.0",
+ "digest": "72bbb711de954db8d01bf0270ae23ead3fabc76a1e9edc208b2ee08b3c282c17"
+ },
+ "princess": {
+ "category": "people",
+ "moji": "👸",
+ "description": "princess",
"unicodeVersion": "6.0",
- "digest": "7a5a1eb2e38f835e8fe6506a78f0f05dd4e831bdd8ea30f3f5f473bc1d28baa2"
+ "digest": "0255742ad246f6245302c68965ce7243618c62e27a55927a1101db0a58144205"
},
- "boy_tone1": {
+ "princess_tone1": {
"category": "people",
- "moji": "👦🏻",
- "description": "boy tone 1",
+ "moji": "👸🏻",
+ "description": "princess tone 1",
"unicodeVersion": "8.0",
- "digest": "b0edd5f1171b2969e5273cb0be90d57f2d7c605ff413478eabecb9501bcfe811"
+ "digest": "6634c586ce395cc2417acfffc4997bd9152b200abb357921f9bcb37d5501c99b"
},
- "boy_tone2": {
+ "princess_tone2": {
"category": "people",
- "moji": "👦🏼",
- "description": "boy tone 2",
+ "moji": "👸🏼",
+ "description": "princess tone 2",
"unicodeVersion": "8.0",
- "digest": "52f5dba881f3fb1867619dfc35d503022e6455a475137d2d5366830fe6966e09"
+ "digest": "9ef16a00fa3f044559e9a8f5abbd0d8be7c6d665f8007c59094e2581d92236d1"
},
- "boy_tone3": {
+ "princess_tone3": {
"category": "people",
- "moji": "👦🏽",
- "description": "boy tone 3",
+ "moji": "👸🏽",
+ "description": "princess tone 3",
"unicodeVersion": "8.0",
- "digest": "57d11252ed3e92e5093ee70a11549f98f70f623a18b75dd8736dc5908f361c8c"
+ "digest": "0a6687e1da8a427e55fc2769f8927fa3f285b9e3dfc4d0a89de2339f9c7ad5d6"
},
- "boy_tone4": {
+ "princess_tone4": {
"category": "people",
- "moji": "👦🏾",
- "description": "boy tone 4",
+ "moji": "👸🏾",
+ "description": "princess tone 4",
"unicodeVersion": "8.0",
- "digest": "b18387f39f18a50afc737c13d5387a904d27210e5e147e0ad4f0966f9091156b"
+ "digest": "687368a236801772418314f1ca4271d97511a3c5875a22886abc21e2383d784d"
},
- "boy_tone5": {
+ "princess_tone5": {
"category": "people",
- "moji": "👦🏿",
- "description": "boy tone 5",
+ "moji": "👸🏿",
+ "description": "princess tone 5",
"unicodeVersion": "8.0",
- "digest": "37527ef42b6e163d273b95178e0c385c46172e45bac09070e421fa8230d5b04c"
+ "digest": "cd1a5425ebe5bd0334ae35e3fcdbe50fffcceac150a86b842b92bbb78bbb0e3b"
},
- "bread": {
- "category": "food",
- "moji": "🍞",
- "description": "bread",
+ "man_with_turban": {
+ "category": "people",
+ "moji": "👳",
+ "description": "man with turban",
"unicodeVersion": "6.0",
- "digest": "6108ad957056be1eeeb3c28e5fbd1b215becb07ad8669513466a26dfd481fe74"
+ "digest": "7741fa53fa283478eec7746c6d3952551980fa2faf2cadd79ad3e60b22413093"
+ },
+ "man_with_turban_tone1": {
+ "category": "people",
+ "moji": "👳🏻",
+ "description": "man with turban tone 1",
+ "unicodeVersion": "8.0",
+ "digest": "e5163b3d793ff9f7f3efe04a4264a160aef3d1c4b6e731a25601a5ebc1f91dc9"
+ },
+ "man_with_turban_tone2": {
+ "category": "people",
+ "moji": "👳🏼",
+ "description": "man with turban tone 2",
+ "unicodeVersion": "8.0",
+ "digest": "f99883c8d09281cdb9a63374e4e418767cf104976fe68f353443e3687b93ecf5"
+ },
+ "man_with_turban_tone3": {
+ "category": "people",
+ "moji": "👳🏽",
+ "description": "man with turban tone 3",
+ "unicodeVersion": "8.0",
+ "digest": "168392861e99c39719618454721047f1dc75b8fcef07233079806eccdf0b63be"
+ },
+ "man_with_turban_tone4": {
+ "category": "people",
+ "moji": "👳🏾",
+ "description": "man with turban tone 4",
+ "unicodeVersion": "8.0",
+ "digest": "1ed7b32cb652d66421a90378b3fd3dbec0da4124886114152ff199f538d8d593"
+ },
+ "man_with_turban_tone5": {
+ "category": "people",
+ "moji": "👳🏿",
+ "description": "man with turban tone 5",
+ "unicodeVersion": "8.0",
+ "digest": "0f639e64c4393c5c81d7c8bc3a37b4988acfc7112616dec45c82226fff15f245"
+ },
+ "man_with_gua_pi_mao": {
+ "category": "people",
+ "moji": "👲",
+ "description": "man with gua pi mao",
+ "unicodeVersion": "6.0",
+ "digest": "f8376151b1df1cca64805f8b64e4b2b3b7358bbd3eb1bbd2cbf363a4a1d542a5"
+ },
+ "man_with_gua_pi_mao_tone1": {
+ "category": "people",
+ "moji": "👲🏻",
+ "description": "man with gua pi mao tone 1",
+ "unicodeVersion": "8.0",
+ "digest": "858d738820987110c6dc6811b69f4b9b7107304ab4e094c55d2f09e8f506dff4"
+ },
+ "man_with_gua_pi_mao_tone2": {
+ "category": "people",
+ "moji": "👲🏼",
+ "description": "man with gua pi mao tone 2",
+ "unicodeVersion": "8.0",
+ "digest": "52e61c5ba607cdb6c49a9252bbb9290d117aa991e34c7d352ab82b9ee708680b"
+ },
+ "man_with_gua_pi_mao_tone3": {
+ "category": "people",
+ "moji": "👲🏽",
+ "description": "man with gua pi mao tone 3",
+ "unicodeVersion": "8.0",
+ "digest": "6ed7de2cb0d2d5434e3fe454b9d56f92f498b7f77a99611ee23f6137627e014c"
+ },
+ "man_with_gua_pi_mao_tone4": {
+ "category": "people",
+ "moji": "👲🏾",
+ "description": "man with gua pi mao tone 4",
+ "unicodeVersion": "8.0",
+ "digest": "c0c6caca9dfd6e3420483929b16556414d0d64708cecc05b1e6969f9e15cb103"
+ },
+ "man_with_gua_pi_mao_tone5": {
+ "category": "people",
+ "moji": "👲🏿",
+ "description": "man with gua pi mao tone 5",
+ "unicodeVersion": "8.0",
+ "digest": "a1ce066b7903e2156919a88e1f4824ece7598cc3b8384ee27342cdf4b69310f1"
+ },
+ "man_in_tuxedo": {
+ "category": "people",
+ "moji": "🤵",
+ "description": "man in tuxedo",
+ "unicodeVersion": "9.0",
+ "digest": "2da1693a18afdd9722380c4778183195d006563787b2d8b839a9810a18626798"
+ },
+ "man_in_tuxedo_tone1": {
+ "category": "people",
+ "moji": "🤵🏻",
+ "description": "man in tuxedo tone 1",
+ "unicodeVersion": "9.0",
+ "digest": "506256c184c13806736c599a07f62ac910b5a10130480355d012a53e11894c79"
+ },
+ "man_in_tuxedo_tone2": {
+ "category": "people",
+ "moji": "🤵🏼",
+ "description": "man in tuxedo tone 2",
+ "unicodeVersion": "9.0",
+ "digest": "1413dbbe32c1578fbc3a9afe0bec950aab0da5277f6ae286aaae03870aeb0846"
+ },
+ "man_in_tuxedo_tone3": {
+ "category": "people",
+ "moji": "🤵🏽",
+ "description": "man in tuxedo tone 3",
+ "unicodeVersion": "9.0",
+ "digest": "1c42dc5683bf2f9a5ea719de1eda6f9f872950495581ca2201b61cbcdc74a342"
+ },
+ "man_in_tuxedo_tone4": {
+ "category": "people",
+ "moji": "🤵🏾",
+ "description": "man in tuxedo tone 4",
+ "unicodeVersion": "9.0",
+ "digest": "9fcf33a14bec175bd7587db3778fe50c5d31dcf2db37e3e941944df133b5b722"
+ },
+ "man_in_tuxedo_tone5": {
+ "category": "people",
+ "moji": "🤵🏿",
+ "description": "man in tuxedo tone 5",
+ "unicodeVersion": "9.0",
+ "digest": "941765104eab002f58217ad7ba418a5082732b32faba92e372bafc8debe457f4"
},
"bride_with_veil": {
"category": "people",
@@ -1189,271 +3450,803 @@
"unicodeVersion": "8.0",
"digest": "e542a466332ea26f2b94a6504912659d4ed72e6fcf399830828a478124e2d79c"
},
- "bridge_at_night": {
- "category": "travel",
- "moji": "🌉",
- "description": "bridge at night",
- "unicodeVersion": "6.0",
- "digest": "212feaa05b96bff46d9699d3e9f1d61f6fc0e228414534b1bf8f1383c591784c"
+ "pregnant_woman": {
+ "category": "people",
+ "moji": "🤰",
+ "description": "pregnant woman",
+ "unicodeVersion": "9.0",
+ "digest": "541e4d6245b5b243121e4666298cd5ae5a31fd38228ca2f527865019d7fa25b5"
},
- "briefcase": {
+ "pregnant_woman_tone1": {
"category": "people",
- "moji": "💼",
- "description": "briefcase",
- "unicodeVersion": "6.0",
- "digest": "f72220b3fa933c6b503c041643543369177efc2bee094d392cbf8ee9d5cf11a0"
+ "moji": "🤰🏻",
+ "description": "pregnant woman tone 1",
+ "unicodeVersion": "9.0",
+ "digest": "8c16b193ce1c39aafcc4ba1b7db7524486cfbd640ece996bd795e91198db6196"
},
- "broken_heart": {
- "category": "symbols",
- "moji": "💔",
- "description": "broken heart",
- "unicodeVersion": "6.0",
- "digest": "6b60f5c0d0a7702308a85e0d8eca3ca54ac348a9f66bb56f89f9f3aae9303ca4"
+ "pregnant_woman_tone2": {
+ "category": "people",
+ "moji": "🤰🏼",
+ "description": "pregnant woman tone 2",
+ "unicodeVersion": "9.0",
+ "digest": "44261300c22052fdc4f82cb41d63754b5bd9d5bc6dac92ac68440a62f3456b87"
},
- "bug": {
- "category": "nature",
- "moji": "🐛",
- "description": "bug",
- "unicodeVersion": "6.0",
- "digest": "8c3a35433d6c2d63c57d2fdfa8be50bd35c334dfe0ba1b1554555e872091b857"
+ "pregnant_woman_tone3": {
+ "category": "people",
+ "moji": "🤰🏽",
+ "description": "pregnant woman tone 3",
+ "unicodeVersion": "9.0",
+ "digest": "f734c133c3d9dc59aeb050024c420e2de231e2fabefda255169666ee955ab8e4"
},
- "bulb": {
- "category": "objects",
- "moji": "💡",
- "description": "electric light bulb",
- "unicodeVersion": "6.0",
- "digest": "2cdc43e4bb17fc00c903a911e065fe1bb96f4f57103a3836cfa908c4618e6d45"
+ "pregnant_woman_tone4": {
+ "category": "people",
+ "moji": "🤰🏾",
+ "description": "pregnant woman tone 4",
+ "unicodeVersion": "9.0",
+ "digest": "6990dabfc19d92c061fdffd82ab99f582378fd84c535db0dc042f20cb9db6c16"
},
- "bullettrain_front": {
- "category": "travel",
- "moji": "🚅",
- "description": "high-speed train with bullet nose",
- "unicodeVersion": "6.0",
- "digest": "b8cd48a543a5ed0cf750cfa8bcfa536e26b72a77e4b7f506041b623e9081fb69"
+ "pregnant_woman_tone5": {
+ "category": "people",
+ "moji": "🤰🏿",
+ "description": "pregnant woman tone 5",
+ "unicodeVersion": "9.0",
+ "digest": "6981c98ac30d7ff3dc5b44279a3d3a8891a1fd33de5a41050aced7e37ec9ea21"
},
- "bullettrain_side": {
- "category": "travel",
- "moji": "🚄",
- "description": "high-speed train",
+ "angel": {
+ "category": "people",
+ "moji": "👼",
+ "description": "baby angel",
"unicodeVersion": "6.0",
- "digest": "401e85f01801e39f8325a96bdee7e27626fc4124d92eeb3e39227d78ef7d26c0"
+ "digest": "93d8abd48b9a0eac8332ed79e1f95c206dd29895e74d31931db32f4feab664fb"
},
- "burrito": {
- "category": "food",
- "moji": "🌯",
- "description": "burrito",
+ "angel_tone1": {
+ "category": "people",
+ "moji": "👼🏻",
+ "description": "baby angel tone 1",
"unicodeVersion": "8.0",
- "digest": "99ad1315f80dd9c1a9dab3a88e4c272d813b072a10f085c44ec4d261f7cb1d83"
+ "digest": "032faac5736197bb9a75da5743891334264d278378f8510b40623c02bc597601"
},
- "bus": {
- "category": "travel",
- "moji": "🚌",
- "description": "bus",
- "unicodeVersion": "6.0",
- "digest": "70534ef30162b8af4b548c3fc0f4d7964e01c8bcb2b52c3b2dc2df4854aa308c"
+ "angel_tone2": {
+ "category": "people",
+ "moji": "👼🏼",
+ "description": "baby angel tone 2",
+ "unicodeVersion": "8.0",
+ "digest": "2f37a1d960eba5ada71b166c4371fdeb69f560f57759de1b24984d0848f74c68"
},
- "busstop": {
- "category": "travel",
- "moji": "🚏",
- "description": "bus stop",
- "unicodeVersion": "6.0",
- "digest": "ccb2c93fe9f2a03c0674d921bb3a6a6c80e2eef64a274bd31b1efb3147785e6f"
+ "angel_tone3": {
+ "category": "people",
+ "moji": "👼🏽",
+ "description": "baby angel tone 3",
+ "unicodeVersion": "8.0",
+ "digest": "96fb1d8e568752aed2c716a062ef650cf4ccca609fdf78cde2e81e3e8f86a098"
},
- "bust_in_silhouette": {
+ "angel_tone4": {
"category": "people",
- "moji": "👤",
- "description": "bust in silhouette",
- "unicodeVersion": "6.0",
- "digest": "17f35781aa264a26812ca9a13a32e7ab73a7eb95af1bc0b812a35d6fc9ac521f"
+ "moji": "👼🏾",
+ "description": "baby angel tone 4",
+ "unicodeVersion": "8.0",
+ "digest": "b17721fd657278ee719963b0df074a9290c52cf58d094ed2722881030f57a79e"
},
- "busts_in_silhouette": {
+ "angel_tone5": {
"category": "people",
- "moji": "👥",
- "description": "busts in silhouette",
+ "moji": "👼🏿",
+ "description": "baby angel tone 5",
+ "unicodeVersion": "8.0",
+ "digest": "c6ebaa89eb3b2e6ee6e16b483587a6f6b341cae187d282e27cf71d1a18117735"
+ },
+ "santa": {
+ "category": "people",
+ "moji": "🎅",
+ "description": "father christmas",
"unicodeVersion": "6.0",
- "digest": "f9aa0c03d90e5d0f57f8114b998019452c5756a8dcbf1750b07f9aeae2f4ab08"
+ "digest": "bc9f3a14f824d9299d2132822c6341c4e87f53ed0c5050c31b5b96d801bc5c3c"
},
- "butterfly": {
- "category": "nature",
- "moji": "🦋",
- "description": "butterfly",
- "unicodeVersion": "9.0",
- "digest": "a7c33f816a9705a83a0debf8cbeb1ff9dc430200f52e8d0f5682fe11c39b4473"
+ "santa_tone1": {
+ "category": "people",
+ "moji": "🎅🏻",
+ "description": "father christmas tone 1",
+ "unicodeVersion": "8.0",
+ "digest": "7e527394c52da94c740197b0d05d15d7c5ee113dce7a7800ec617bd3a3fa297f"
},
- "cactus": {
- "category": "nature",
- "moji": "🌵",
- "description": "cactus",
- "unicodeVersion": "6.0",
- "digest": "1690158bb63afbe907bbc343627b9c353198ad611b9bb195f5eda5c3545aa2e9"
+ "santa_tone2": {
+ "category": "people",
+ "moji": "🎅🏼",
+ "description": "father christmas tone 2",
+ "unicodeVersion": "8.0",
+ "digest": "26c649ec3f466952dbb6d3234ad3254c735259c83bfbbf96c68d0203ed5744e1"
},
- "cake": {
- "category": "food",
- "moji": "🍰",
- "description": "shortcake",
- "unicodeVersion": "6.0",
- "digest": "807cc6dd0621c1c4dbd5380191d056ce44ccd2b92dda0cad6ca1c57e19e9f785"
+ "santa_tone3": {
+ "category": "people",
+ "moji": "🎅🏽",
+ "description": "father christmas tone 3",
+ "unicodeVersion": "8.0",
+ "digest": "d8e656829487c0808af4efb095e55cbc8a9b18576572ebd61dbac422119a1a01"
},
- "calendar": {
- "category": "objects",
- "moji": "📆",
- "description": "tear-off calendar",
- "unicodeVersion": "6.0",
- "digest": "275fa5b2c113c3d117b1c8970f01dd897b90405db7a55ff6a3dda767817ed4f5"
+ "santa_tone4": {
+ "category": "people",
+ "moji": "🎅🏾",
+ "description": "father christmas tone 4",
+ "unicodeVersion": "8.0",
+ "digest": "e12990b6edb39eea5f8bcd0f3c4d0f404cd47beb8fe180066455f833e7b99cf8"
},
- "calendar_spiral": {
- "category": "objects",
- "moji": "🗓",
- "description": "spiral calendar pad",
- "unicodeVersion": "7.0",
- "digest": "01aefae6a6d0d517be52e7918d6893d55a9fa5b7480d60cb8b8b5a743573ea0b"
+ "santa_tone5": {
+ "category": "people",
+ "moji": "🎅🏿",
+ "description": "father christmas tone 5",
+ "unicodeVersion": "8.0",
+ "digest": "0310b472690b6f3aac9fa4f611fa00c3d9d5d50edb94d08a676c77f28c0913da"
},
- "call_me": {
+ "mrs_claus": {
"category": "people",
- "moji": "🤙",
- "description": "call me hand",
+ "moji": "🤶",
+ "description": "mother christmas",
"unicodeVersion": "9.0",
- "digest": "3701197e18ffedc242a6b20ab74e7a339c7f77df27c5f92d5124a42e39a9502a"
+ "digest": "67d7fccbfd20aa195e526485ce40e52994aa4fa9fb71c70533fb579f4ca0ef1f"
},
- "call_me_tone1": {
+ "mrs_claus_tone1": {
"category": "people",
- "moji": "🤙🏻",
- "description": "call me hand tone 1",
+ "moji": "🤶🏻",
+ "description": "mother christmas tone 1",
"unicodeVersion": "9.0",
- "digest": "1c5c681c9b588a2b07a57d27d3aaf209e7d23c4cba938ffce07bfa4a53d720e9"
+ "digest": "c95a39dcdabe20d1f18a2c5cf7cbf1e530fe9ced7ffdbb0f7f8cf833663f8775"
},
- "call_me_tone2": {
+ "mrs_claus_tone2": {
"category": "people",
- "moji": "🤙🏼",
- "description": "call me hand tone 2",
+ "moji": "🤶🏼",
+ "description": "mother christmas tone 2",
"unicodeVersion": "9.0",
- "digest": "4c848da3d8849de81922aa2eb6611489ad72649cf189849176217e946cf9aad8"
+ "digest": "425a73cb7d57dbaaf13b1156329ef31a0e9b8cc3917c16b90cc561d4d3373bad"
},
- "call_me_tone3": {
+ "mrs_claus_tone3": {
"category": "people",
- "moji": "🤙🏽",
- "description": "call me hand tone 3",
+ "moji": "🤶🏽",
+ "description": "mother christmas tone 3",
"unicodeVersion": "9.0",
- "digest": "b697c1a4aa15f7793002b79aee8e5240f48b832b33f8ad39b8d46b90624c6c0a"
+ "digest": "59fd4b47738832f3996b94f67e049edf5d64e0c346b55c9ff09cb893db898fbb"
},
- "call_me_tone4": {
+ "mrs_claus_tone4": {
"category": "people",
- "moji": "🤙🏾",
- "description": "call me hand tone 4",
+ "moji": "🤶🏾",
+ "description": "mother christmas tone 4",
"unicodeVersion": "9.0",
- "digest": "f1b07c2a8071f5f704b5354277777aa651202f56ef7d334ce66e0475fdba086a"
+ "digest": "e96e7945124124002f13cb381f43dad4f40d77b3ec9eb691a58c929ea3214c0d"
},
- "call_me_tone5": {
+ "mrs_claus_tone5": {
"category": "people",
- "moji": "🤙🏿",
- "description": "call me hand tone 5",
+ "moji": "🤶🏿",
+ "description": "mother christmas tone 5",
"unicodeVersion": "9.0",
- "digest": "d7d8d96a3980e3c558d3e3ef62aa49dc237c075f0c3dfb0b06ae33fc98f565c5"
+ "digest": "a1639e1ed2d862d48e528a5b3f1da14e8afddbed09fafed8f0ad1dbb75c335dc"
},
- "calling": {
- "category": "objects",
- "moji": "📲",
- "description": "mobile phone with rightwards arrow at left",
+ "massage": {
+ "category": "people",
+ "moji": "💆",
+ "description": "face massage",
"unicodeVersion": "6.0",
- "digest": "3fc285fd0fc7a6beb6c64091671d0f789458e32aaba9d9fd74dfac16b9549826"
+ "digest": "3c5ede480d35f567954a1dd7082836f0898b44cd41037ce37c0539f8062209a1"
},
- "camel": {
- "category": "nature",
- "moji": "🐫",
- "description": "bactrian camel",
+ "massage_tone1": {
+ "category": "people",
+ "moji": "💆🏻",
+ "description": "face massage tone 1",
+ "unicodeVersion": "8.0",
+ "digest": "d70f8df999a2f2a69eceb60cc09be2d8cabd5121deb051e1e6ef1b60504209c6"
+ },
+ "massage_tone2": {
+ "category": "people",
+ "moji": "💆🏼",
+ "description": "face massage tone 2",
+ "unicodeVersion": "8.0",
+ "digest": "1487da84572a32db39b018369df3ddabb1903294425d9b60091d83ac6b59a48a"
+ },
+ "massage_tone3": {
+ "category": "people",
+ "moji": "💆🏽",
+ "description": "face massage tone 3",
+ "unicodeVersion": "8.0",
+ "digest": "c10b984c5225440f3da97df2d00b4a88f41ee1cd9902042f64c557e31ddf944d"
+ },
+ "massage_tone4": {
+ "category": "people",
+ "moji": "💆🏾",
+ "description": "face massage tone 4",
+ "unicodeVersion": "8.0",
+ "digest": "3140b503be64c8a91a7be46b12585a11b03aba0d3c5186095b734de23b9919e5"
+ },
+ "massage_tone5": {
+ "category": "people",
+ "moji": "💆🏿",
+ "description": "face massage tone 5",
+ "unicodeVersion": "8.0",
+ "digest": "ee0d958448ae7c5f0314520d6dab1220d827ad037d2880a72749433cf6381ab3"
+ },
+ "haircut": {
+ "category": "people",
+ "moji": "💇",
+ "description": "haircut",
"unicodeVersion": "6.0",
- "digest": "666d8efce2033fd7a69fa8537a0d724b3e1e0d9d5093b8d7666a45a4f7d1831f"
+ "digest": "d6da39a1156f21ca140f28c5477fbbc37c2a9903bb2eb183e9288e16eedf134e"
},
- "camera": {
- "category": "objects",
- "moji": "📷",
- "description": "camera",
+ "haircut_tone1": {
+ "category": "people",
+ "moji": "💇🏻",
+ "description": "haircut tone 1",
+ "unicodeVersion": "8.0",
+ "digest": "186dfd3d0512fc316dd48a75b4201cb9006bf1f01ad17afe41c1e9fc4f1b440a"
+ },
+ "haircut_tone2": {
+ "category": "people",
+ "moji": "💇🏼",
+ "description": "haircut tone 2",
+ "unicodeVersion": "8.0",
+ "digest": "4e85e6281419b2bc948d529d883bdcea6c115a4195299c1fc3c0bb839ac95b26"
+ },
+ "haircut_tone3": {
+ "category": "people",
+ "moji": "💇🏽",
+ "description": "haircut tone 3",
+ "unicodeVersion": "8.0",
+ "digest": "08d8b031cb43180346a7be2e6e010b31abfdc091471c357ff1d428bd8bd66bba"
+ },
+ "haircut_tone4": {
+ "category": "people",
+ "moji": "💇🏾",
+ "description": "haircut tone 4",
+ "unicodeVersion": "8.0",
+ "digest": "b3d2c66952b6d141ab000c6c99a4b8a978f135e2b880aed83517fb5cbced7f14"
+ },
+ "haircut_tone5": {
+ "category": "people",
+ "moji": "💇🏿",
+ "description": "haircut tone 5",
+ "unicodeVersion": "8.0",
+ "digest": "eab0facef35c9b3e920c79adce756116b68fe8042bc77f9505d93cdd75a58e79"
+ },
+ "walking": {
+ "category": "people",
+ "moji": "🚶",
+ "description": "pedestrian",
"unicodeVersion": "6.0",
- "digest": "5d86baa4baede9c6d02fbe02fd2d4ad07a47a77e09c1d2d045ea20b36bd2938c"
+ "digest": "595b89b7ed1359e120f4aeeaccc8dbce01fc96358ce6b9501b6f4ab6559428f4"
},
- "camera_with_flash": {
- "category": "objects",
- "moji": "📸",
- "description": "camera with flash",
- "unicodeVersion": "7.0",
- "digest": "9b701460de9ba318811d771bb451fb109850bc08ad6592f9365bc350de571f9f"
+ "walking_tone1": {
+ "category": "people",
+ "moji": "🚶🏻",
+ "description": "pedestrian tone 1",
+ "unicodeVersion": "8.0",
+ "digest": "794562b94ed8c825c4facad1b3d8fca0c90f49582209cc0f0923e33d2d5a083d"
},
- "camping": {
- "category": "travel",
- "moji": "🏕",
- "description": "camping",
- "unicodeVersion": "7.0",
- "digest": "70c596c5d9ab030e26b46c95e011a28050aa6082f4ba2b8b197f24915ce03c86"
+ "walking_tone2": {
+ "category": "people",
+ "moji": "🚶🏼",
+ "description": "pedestrian tone 2",
+ "unicodeVersion": "8.0",
+ "digest": "3fc7452b3b324a3a9f7a34faa82175865681248094f13ea7977ee1eb64e6e201"
},
- "cancer": {
- "category": "symbols",
- "moji": "♋",
- "description": "cancer",
- "unicodeVersion": "1.1",
- "digest": "f707db14878e03608dee3eca4eacf0a0be4a709c6d33b2d5b7ab0e8f05473b2b"
+ "walking_tone3": {
+ "category": "people",
+ "moji": "🚶🏽",
+ "description": "pedestrian tone 3",
+ "unicodeVersion": "8.0",
+ "digest": "b9200fae09036c6919e8a348b51a2fe09e2e2909120077000dcba0fd62fb9fa5"
},
- "candle": {
- "category": "objects",
- "moji": "🕯",
- "description": "candle",
+ "walking_tone4": {
+ "category": "people",
+ "moji": "🚶🏾",
+ "description": "pedestrian tone 4",
+ "unicodeVersion": "8.0",
+ "digest": "c1dbd3327bed96613834dd05dbf43bd233f5d4ab96b1c6f42684ecc690d62fa7"
+ },
+ "walking_tone5": {
+ "category": "people",
+ "moji": "🚶🏿",
+ "description": "pedestrian tone 5",
+ "unicodeVersion": "8.0",
+ "digest": "60e0ceb95d5c555835dda6b1fa368938f50e2ee6ccbda723f2ad9c511fd9d16b"
+ },
+ "runner": {
+ "category": "people",
+ "moji": "🏃",
+ "description": "runner",
+ "unicodeVersion": "6.0",
+ "digest": "6a87e5a783e98a571bdbe4d983b0cce46c0b337707a99a8bea7dcc5a5d78c46e"
+ },
+ "runner_tone1": {
+ "category": "people",
+ "moji": "🏃🏻",
+ "description": "runner tone 1",
+ "unicodeVersion": "8.0",
+ "digest": "788912454e5a001a96cfbbf7ae07d22c7d53bd0a0a333add62bd38f50f890202"
+ },
+ "runner_tone2": {
+ "category": "people",
+ "moji": "🏃🏼",
+ "description": "runner tone 2",
+ "unicodeVersion": "8.0",
+ "digest": "0e0c0509fe054b31b82629bb97d410d56154143e0612af9ced7f754bc16876b8"
+ },
+ "runner_tone3": {
+ "category": "people",
+ "moji": "🏃🏽",
+ "description": "runner tone 3",
+ "unicodeVersion": "8.0",
+ "digest": "33b6233182bec5958325115fc8b238913773b03fe616ceb465152c75a83aa3bd"
+ },
+ "runner_tone4": {
+ "category": "people",
+ "moji": "🏃🏾",
+ "description": "runner tone 4",
+ "unicodeVersion": "8.0",
+ "digest": "d14b09b58e47a80781df8f7d6687be75f2bc22f312b96129109166c1535e6001"
+ },
+ "runner_tone5": {
+ "category": "people",
+ "moji": "🏃🏿",
+ "description": "runner tone 5",
+ "unicodeVersion": "8.0",
+ "digest": "a816106153bf16d304e766386d3ad641fc931faa302d998fc2bd3e3334de5d7f"
+ },
+ "dancer": {
+ "category": "people",
+ "moji": "💃",
+ "description": "dancer",
+ "unicodeVersion": "6.0",
+ "digest": "21117e63374e501b1daf8350d5921314cc8b306558c8d6803d43c71066438648"
+ },
+ "dancer_tone1": {
+ "category": "people",
+ "moji": "💃🏻",
+ "description": "dancer tone 1",
+ "unicodeVersion": "8.0",
+ "digest": "35c64d92577272999d1151cacb584e248a2a005ce9388ce64250bcc8a7178dc1"
+ },
+ "dancer_tone2": {
+ "category": "people",
+ "moji": "💃🏼",
+ "description": "dancer tone 2",
+ "unicodeVersion": "8.0",
+ "digest": "9dfdd28db7267f172b1bae228216d0111583c4903934e4afa081cdbf83facd45"
+ },
+ "dancer_tone3": {
+ "category": "people",
+ "moji": "💃🏽",
+ "description": "dancer tone 3",
+ "unicodeVersion": "8.0",
+ "digest": "79b7c1e313b235e0acec3ff50d3eb7ecff53b5ae4a2ba7d363b0992fa939ac9b"
+ },
+ "dancer_tone4": {
+ "category": "people",
+ "moji": "💃🏾",
+ "description": "dancer tone 4",
+ "unicodeVersion": "8.0",
+ "digest": "16e6f14fef6464ac20273713c196e189eab75f3ecab98b9eee172bc33c8f2847"
+ },
+ "dancer_tone5": {
+ "category": "people",
+ "moji": "💃🏿",
+ "description": "dancer tone 5",
+ "unicodeVersion": "8.0",
+ "digest": "c090f007657b775acab02656ad3de9d02f97373d6b897a7052e1aa5b032de2ee"
+ },
+ "man_dancing": {
+ "category": "people",
+ "moji": "🕺",
+ "description": "man dancing",
+ "unicodeVersion": "9.0",
+ "digest": "1d8c16790d9c7affa997923ea15ce09221cdc9d26b6f82150e91d22463b96319"
+ },
+ "man_dancing_tone1": {
+ "category": "activity",
+ "moji": "🕺🏻",
+ "description": "man dancing tone 1",
+ "unicodeVersion": "9.0",
+ "digest": "317e619d66577c49fcba699a0b9bf3ee63d100b487c182b4637b5fd46f532bc2"
+ },
+ "man_dancing_tone2": {
+ "category": "activity",
+ "moji": "🕺🏼",
+ "description": "man dancing tone 2",
+ "unicodeVersion": "9.0",
+ "digest": "72a9eea3cee40692a56f234ca59093dfd7ff1113b710a1aaa146b5a137fa213a"
+ },
+ "man_dancing_tone3": {
+ "category": "activity",
+ "moji": "🕺🏽",
+ "description": "man dancing tone 3",
+ "unicodeVersion": "9.0",
+ "digest": "97824a84dbb9058b4b90e802c1aa72556d3c5f5941599e3e23fd662534b6421a"
+ },
+ "man_dancing_tone4": {
+ "category": "activity",
+ "moji": "🕺🏾",
+ "description": "man dancing tone 4",
+ "unicodeVersion": "9.0",
+ "digest": "632fae4e335dae2713a26cc6ac77e62229f88f24db5b33d3c50da13ae7ce8686"
+ },
+ "man_dancing_tone5": {
+ "category": "activity",
+ "moji": "🕺🏿",
+ "description": "man dancing tone 5",
+ "unicodeVersion": "9.0",
+ "digest": "b33a036b2b7f202dc980786796171eeda66caa3cb06320f94617191ec6133ee3"
+ },
+ "levitate": {
+ "category": "activity",
+ "moji": "🕴",
+ "description": "man in business suit levitating",
"unicodeVersion": "7.0",
- "digest": "67400dd5e800a6e496a661da639d4964bf6646525bcf6d6c4773529ae5f7f075"
+ "digest": "103fabb2260fef61982731785dc3d09884cbfcb6bd4ef93f2d5db8403ae4c40a"
},
- "candy": {
- "category": "food",
- "moji": "🍬",
- "description": "candy",
+ "dancers": {
+ "category": "people",
+ "moji": "👯",
+ "description": "woman with bunny ears",
"unicodeVersion": "6.0",
- "digest": "7064f2eb0dc768e3deb1e6ebe449950eb5a72ea882fb13bb06e23e7e2012c740"
+ "digest": "8449ee0de1754c317e82371ae80d7f6b840b69b657960953a50ddcb918f363c2"
},
- "canoe": {
- "category": "travel",
- "moji": "🛶",
- "description": "canoe",
+ "fencer": {
+ "category": "activity",
+ "moji": "🤺",
+ "description": "fencer",
"unicodeVersion": "9.0",
- "digest": "20d70c92c33a8bc6629e6d0b87bfca0dbbc9c32458d253ee62eb0845398fe095"
+ "digest": "8fe2320e2bf8ae87bdd7b2cb519146fba1cc5f1df8c426f83a780544bfec3785"
},
- "capital_abcd": {
- "category": "symbols",
- "moji": "🔠",
- "description": "input symbol for latin capital letters",
+ "horse_racing": {
+ "category": "activity",
+ "moji": "🏇",
+ "description": "horse racing",
"unicodeVersion": "6.0",
- "digest": "6cd31a4765c108d5e85f0ad9e25dee8e8f8af1fc40de161c0407f4041db9e288"
+ "digest": "1c1a2fa09a64da5b442f334a45054b7cbe39dbf7ac1d2a26d7a1ef2bb2debdc4"
},
- "capricorn": {
- "category": "symbols",
- "moji": "♑",
- "description": "capricorn",
- "unicodeVersion": "1.1",
- "digest": "d769793914f9c1ea4237aea85d288344c7422ccb286f8c1d3b18fa99ee8fa7ce"
+ "horse_racing_tone1": {
+ "category": "activity",
+ "moji": "🏇🏻",
+ "description": "horse racing tone 1",
+ "unicodeVersion": "8.0",
+ "digest": "5d55d2cc1a8efc0b754cd75d56d2643784aa41fc7a09e1e9d4ddee767f35ffd6"
},
- "card_box": {
- "category": "objects",
- "moji": "🗃",
- "description": "card file box",
+ "horse_racing_tone2": {
+ "category": "activity",
+ "moji": "🏇🏼",
+ "description": "horse racing tone 2",
+ "unicodeVersion": "8.0",
+ "digest": "f8b5b8f7247f6526ea3bca0e6a1136978449815a105f766b42ff6a2bfe24a7a3"
+ },
+ "horse_racing_tone3": {
+ "category": "activity",
+ "moji": "🏇🏽",
+ "description": "horse racing tone 3",
+ "unicodeVersion": "8.0",
+ "digest": "13a6b8ceeba67ea1dcda173f8c4ce012ec5333460bff51646ecd3c26092b492b"
+ },
+ "horse_racing_tone4": {
+ "category": "activity",
+ "moji": "🏇🏾",
+ "description": "horse racing tone 4",
+ "unicodeVersion": "8.0",
+ "digest": "042cb542ea8008f51f7750923a8f88090bc91c9301234b6eb39284735930f5ae"
+ },
+ "horse_racing_tone5": {
+ "category": "activity",
+ "moji": "🏇🏿",
+ "description": "horse racing tone 5",
+ "unicodeVersion": "8.0",
+ "digest": "07a0a17002c5f623540377cf24a86d364bcb42b57b759e41d07af17ae36c1206"
+ },
+ "skier": {
+ "category": "activity",
+ "moji": "⛷",
+ "description": "skier",
+ "unicodeVersion": "5.2",
+ "digest": "7a490189499bc88ed15fe945813665ba3114edc039d25eb003026d27c84b5f78"
+ },
+ "snowboarder": {
+ "category": "activity",
+ "moji": "🏂",
+ "description": "snowboarder",
+ "unicodeVersion": "6.0",
+ "digest": "b2acc118ae84560f980a44a95af7c2de8e57c2c95ff69ef9c25c9e5d535306b8"
+ },
+ "golfer": {
+ "category": "activity",
+ "moji": "🏌",
+ "description": "golfer",
"unicodeVersion": "7.0",
- "digest": "25acb25ee37044c67b69ce3a7cb412f7ec2fad76009b9e022cf45b58e0d1d898"
+ "digest": "969e342b2749a357f110ebd60fc3f8e49899e0a0777304f8bb4d03416f076d75"
},
- "card_index": {
- "category": "objects",
- "moji": "📇",
- "description": "card index",
+ "surfer": {
+ "category": "activity",
+ "moji": "🏄",
+ "description": "surfer",
"unicodeVersion": "6.0",
- "digest": "262bfa04568a192dd8696ec43c6f916a45e16a7c19d77f830e366de0d930bb7a"
+ "digest": "39564fb830c8bd3e37cc30f227ffa454bee97a9f5a3df9d062df656fb7cca740"
},
- "carousel_horse": {
- "category": "travel",
- "moji": "🎠",
- "description": "carousel horse",
+ "surfer_tone1": {
+ "category": "activity",
+ "moji": "🏄🏻",
+ "description": "surfer tone 1",
+ "unicodeVersion": "8.0",
+ "digest": "c47d4c1057a86878179b86d4a56e432b9cc9d34f5d3aa9b817c84be29a029904"
+ },
+ "surfer_tone2": {
+ "category": "activity",
+ "moji": "🏄🏼",
+ "description": "surfer tone 2",
+ "unicodeVersion": "8.0",
+ "digest": "d15f802cf36c352a1817b15bc508a1983c9362e24d2707f12f7e6a2c4b1f8b90"
+ },
+ "surfer_tone3": {
+ "category": "activity",
+ "moji": "🏄🏽",
+ "description": "surfer tone 3",
+ "unicodeVersion": "8.0",
+ "digest": "1377c1f74dd0987032b564c7ad55d0a2bb418e1f372937db50cb0e6806e9c11a"
+ },
+ "surfer_tone4": {
+ "category": "activity",
+ "moji": "🏄🏾",
+ "description": "surfer tone 4",
+ "unicodeVersion": "8.0",
+ "digest": "6155339508cf035a4eab763083f73e6c78bdaa15ce2acba5abe808b6d4f9d9c8"
+ },
+ "surfer_tone5": {
+ "category": "activity",
+ "moji": "🏄🏿",
+ "description": "surfer tone 5",
+ "unicodeVersion": "8.0",
+ "digest": "c3f84fd38dfe40f539fbf1201c319b990aa4fb81bb57548377a7dfb2e3ee8661"
+ },
+ "rowboat": {
+ "category": "activity",
+ "moji": "🚣",
+ "description": "rowboat",
"unicodeVersion": "6.0",
- "digest": "d8a609d98fab29e04443f12457a8c8c7fe321b3b940289f1c9fe55f711583b20"
+ "digest": "a0a0b5f15fffb7be60e7467c2a0a368b34a8f9d47af65ead06881bad9e0bd8c2"
},
- "carrot": {
- "category": "food",
- "moji": "🥕",
- "description": "carrot",
- "unicodeVersion": "9.0",
- "digest": "8d8a49f70f7f339347d4a59afcb9a58d9113ebb4d7488d681e409cefd5cb9e47"
+ "rowboat_tone1": {
+ "category": "activity",
+ "moji": "🚣🏻",
+ "description": "rowboat tone 1",
+ "unicodeVersion": "8.0",
+ "digest": "2f6403643528646b73013979e3564e54a9a529782f019a44a956ff275b2017c0"
+ },
+ "rowboat_tone2": {
+ "category": "activity",
+ "moji": "🚣🏼",
+ "description": "rowboat tone 2",
+ "unicodeVersion": "8.0",
+ "digest": "cd925816e16fce63bca0dbd0fc2832fd69ff98a9ee7836ddbc939f3bd9b5a189"
+ },
+ "rowboat_tone3": {
+ "category": "activity",
+ "moji": "🚣🏽",
+ "description": "rowboat tone 3",
+ "unicodeVersion": "8.0",
+ "digest": "09cee7c79709dfdb07e98b585617a068dd624713a02d0f5c21fd7b09a5695baf"
+ },
+ "rowboat_tone4": {
+ "category": "activity",
+ "moji": "🚣🏾",
+ "description": "rowboat tone 4",
+ "unicodeVersion": "8.0",
+ "digest": "12408bd1b720f2799c46fb22d91c9d62fe5fe4f0f2449e2eff921d7f392ab22f"
+ },
+ "rowboat_tone5": {
+ "category": "activity",
+ "moji": "🚣🏿",
+ "description": "rowboat tone 5",
+ "unicodeVersion": "8.0",
+ "digest": "cb64137080640be1bcb7e577e0984bc5cf5dd524e60e229c09003191c4ca1680"
+ },
+ "swimmer": {
+ "category": "activity",
+ "moji": "🏊",
+ "description": "swimmer",
+ "unicodeVersion": "6.0",
+ "digest": "16c5a68b9f1cc7d0f5da1f288be73a0419d059e76f22bed5f7d7d902a1320af4"
+ },
+ "swimmer_tone1": {
+ "category": "activity",
+ "moji": "🏊🏻",
+ "description": "swimmer tone 1",
+ "unicodeVersion": "8.0",
+ "digest": "2159c9ecb0580a2183e921e3a3988643caaa56ad3037993b83e2776988a92e70"
+ },
+ "swimmer_tone2": {
+ "category": "activity",
+ "moji": "🏊🏼",
+ "description": "swimmer tone 2",
+ "unicodeVersion": "8.0",
+ "digest": "8aeeafc91941162d71eaf9d2a2313d9af6cfdf4ea081ccacbc6a28389e0e77c0"
+ },
+ "swimmer_tone3": {
+ "category": "activity",
+ "moji": "🏊🏽",
+ "description": "swimmer tone 3",
+ "unicodeVersion": "8.0",
+ "digest": "e6811d73ef31041bb9c602e20e1853b34d6db4774dee657851b0e1952b7a038e"
+ },
+ "swimmer_tone4": {
+ "category": "activity",
+ "moji": "🏊🏾",
+ "description": "swimmer tone 4",
+ "unicodeVersion": "8.0",
+ "digest": "627184d5dae69aea7345661c1c721beb78ccbeae9cf6dc3f844f6368fddf1018"
+ },
+ "swimmer_tone5": {
+ "category": "activity",
+ "moji": "🏊🏿",
+ "description": "swimmer tone 5",
+ "unicodeVersion": "8.0",
+ "digest": "db6862ca44bd4375ed8ccf2085398e4003342360f7f8325de3f7126d69716432"
+ },
+ "basketball_player": {
+ "category": "activity",
+ "moji": "⛹",
+ "description": "person with ball",
+ "unicodeVersion": "5.2",
+ "digest": "518a3f9b20138447812b9ea81f879c035a03694c426f77d8bdc8cef51c04592f"
+ },
+ "basketball_player_tone1": {
+ "category": "activity",
+ "moji": "⛹🏻",
+ "description": "person with ball tone 1",
+ "unicodeVersion": "8.0",
+ "digest": "e34414295433335df2783de205076e9c44cdbd7e07a041d2695a83e4fe4f70ba"
+ },
+ "basketball_player_tone2": {
+ "category": "activity",
+ "moji": "⛹🏼",
+ "description": "person with ball tone 2",
+ "unicodeVersion": "8.0",
+ "digest": "59308fa1cbe4b778990ced93eb866bbad7f88fda3c1b68837d0da822801f0f20"
+ },
+ "basketball_player_tone3": {
+ "category": "activity",
+ "moji": "⛹🏽",
+ "description": "person with ball tone 3",
+ "unicodeVersion": "8.0",
+ "digest": "e334bddf438b49c145a5c11c010a05964dc50495510017dc37ea8bd6ddc400b3"
+ },
+ "basketball_player_tone4": {
+ "category": "activity",
+ "moji": "⛹🏾",
+ "description": "person with ball tone 4",
+ "unicodeVersion": "8.0",
+ "digest": "24a472bcb4f27a2d8fe2dce4ed6a060391a798721b2146cf6921ff7059bb8446"
+ },
+ "basketball_player_tone5": {
+ "category": "activity",
+ "moji": "⛹🏿",
+ "description": "person with ball tone 5",
+ "unicodeVersion": "8.0",
+ "digest": "8c6a17f2c938aa60aba16634d81dcb069f2c6e7bbf8d79bf36fe888ff83951ae"
+ },
+ "lifter": {
+ "category": "activity",
+ "moji": "🏋",
+ "description": "weight lifter",
+ "unicodeVersion": "7.0",
+ "digest": "d8a22a5258a05e8c31453955103531b59c6018e3e3dfd1f507d6f21ee28b3f00"
+ },
+ "lifter_tone1": {
+ "category": "activity",
+ "moji": "🏋🏻",
+ "description": "weight lifter tone 1",
+ "unicodeVersion": "8.0",
+ "digest": "f13372d0812c433eafca5d943a6be6f8f6481bde4e681aa2ee5942bf3a74224f"
+ },
+ "lifter_tone2": {
+ "category": "activity",
+ "moji": "🏋🏼",
+ "description": "weight lifter tone 2",
+ "unicodeVersion": "8.0",
+ "digest": "6a58152d31b5d8992c231e76823beb81dc9d440944d0ebc6bbc7630f9385f163"
+ },
+ "lifter_tone3": {
+ "category": "activity",
+ "moji": "🏋🏽",
+ "description": "weight lifter tone 3",
+ "unicodeVersion": "8.0",
+ "digest": "9fe7befca13df23016568b785e1174122f1b60f8f7a1104bfd1d948ffe2f552e"
+ },
+ "lifter_tone4": {
+ "category": "activity",
+ "moji": "🏋🏾",
+ "description": "weight lifter tone 4",
+ "unicodeVersion": "8.0",
+ "digest": "510cfdae9be775ef9a5866f780a0cd95080b3b6e9959cfbaa41833aa3f24db83"
+ },
+ "lifter_tone5": {
+ "category": "activity",
+ "moji": "🏋🏿",
+ "description": "weight lifter tone 5",
+ "unicodeVersion": "8.0",
+ "digest": "7347d561a692bd604d3763115cecc66203f1c9a00515de53acecb4d6e0689d65"
+ },
+ "bicyclist": {
+ "category": "activity",
+ "moji": "🚴",
+ "description": "bicyclist",
+ "unicodeVersion": "6.0",
+ "digest": "3dd17f72beea2fcc4fc9124af3f4997ac00009c699096495b3515cd3ad2453f7"
+ },
+ "bicyclist_tone1": {
+ "category": "activity",
+ "moji": "🚴🏻",
+ "description": "bicyclist tone 1",
+ "unicodeVersion": "8.0",
+ "digest": "9bb97a68bdb7d081c1ddfa277be0842fccf3497752fde007e08e46ff831ab4dc"
+ },
+ "bicyclist_tone2": {
+ "category": "activity",
+ "moji": "🚴🏼",
+ "description": "bicyclist tone 2",
+ "unicodeVersion": "8.0",
+ "digest": "36329bb0d01bfbd7f99458081ea6ff071317de2525879dc211dafc4e2e7c299b"
+ },
+ "bicyclist_tone3": {
+ "category": "activity",
+ "moji": "🚴🏽",
+ "description": "bicyclist tone 3",
+ "unicodeVersion": "8.0",
+ "digest": "0ef3ff2cbf408d29b7921ffcdcea3ad7d1faa6ec6a1c1b20582a58e4850c6569"
+ },
+ "bicyclist_tone4": {
+ "category": "activity",
+ "moji": "🚴🏾",
+ "description": "bicyclist tone 4",
+ "unicodeVersion": "8.0",
+ "digest": "9283b7e63bd6a17048e33661e6792568d889ba0f3655a359db159589d6767a47"
+ },
+ "bicyclist_tone5": {
+ "category": "activity",
+ "moji": "🚴🏿",
+ "description": "bicyclist tone 5",
+ "unicodeVersion": "8.0",
+ "digest": "c2849bb847cc9cbe75ec219ebbd2968891c1ebc2fca62bf86f362eba5e3d78f6"
+ },
+ "mountain_bicyclist": {
+ "category": "activity",
+ "moji": "🚵",
+ "description": "mountain bicyclist",
+ "unicodeVersion": "6.0",
+ "digest": "f3a2fd3bc93fe4e5a039a81e8d5e11b25c949ae041f822a49b15ea7dc35f560e"
+ },
+ "mountain_bicyclist_tone1": {
+ "category": "activity",
+ "moji": "🚵🏻",
+ "description": "mountain bicyclist tone 1",
+ "unicodeVersion": "8.0",
+ "digest": "dc3b22a219da2c45a6cc45e67ea76e5b35a74eae46f9202dc2f19dd66effc595"
+ },
+ "mountain_bicyclist_tone2": {
+ "category": "activity",
+ "moji": "🚵🏼",
+ "description": "mountain bicyclist tone 2",
+ "unicodeVersion": "8.0",
+ "digest": "1ac6898b085ce24e5d80fbafc2c9c9afe6390beae052097f92bfeb9f85a5f906"
+ },
+ "mountain_bicyclist_tone3": {
+ "category": "activity",
+ "moji": "🚵🏽",
+ "description": "mountain bicyclist tone 3",
+ "unicodeVersion": "8.0",
+ "digest": "b0c9d179b0ba26c618b69c46aad4f7470b49b9f6c173c452c7a56b61a54537bc"
+ },
+ "mountain_bicyclist_tone4": {
+ "category": "activity",
+ "moji": "🚵🏾",
+ "description": "mountain bicyclist tone 4",
+ "unicodeVersion": "8.0",
+ "digest": "e59f743d7e8e8802973da998bc3627f12101508caef7ca2512b8fecb7f9c34d4"
+ },
+ "mountain_bicyclist_tone5": {
+ "category": "activity",
+ "moji": "🚵🏿",
+ "description": "mountain bicyclist tone 5",
+ "unicodeVersion": "8.0",
+ "digest": "ca19109c586fc3d0ddf1662b497f517096a813bfcec8184989fa6b578eb57424"
},
"cartwheel": {
"category": "activity",
@@ -1497,6 +4290,510 @@
"unicodeVersion": "9.0",
"digest": "b9a51589e53cb61afe0b470de87deb135b8396d8300848bf7a240190787b5863"
},
+ "wrestlers": {
+ "category": "activity",
+ "moji": "🤼",
+ "description": "wrestlers",
+ "unicodeVersion": "9.0",
+ "digest": "919cf0ff49517c62a0abc8a6067e196fa6a1d5f73fa5902537879775ee9fad13"
+ },
+ "wrestlers_tone1": {
+ "category": "activity",
+ "moji": "🤼🏻",
+ "description": "wrestlers tone 1",
+ "unicodeVersion": "9.0",
+ "digest": "9611c3dfb8a18b8332135642d0fac74bd6557054909066141a81c9684bbc7207"
+ },
+ "wrestlers_tone2": {
+ "category": "activity",
+ "moji": "🤼🏼",
+ "description": "wrestlers tone 2",
+ "unicodeVersion": "9.0",
+ "digest": "17c529119acb4aafd8c834874d6285e41d2d9694243cce9eb51c87f67d22a3be"
+ },
+ "wrestlers_tone3": {
+ "category": "activity",
+ "moji": "🤼🏽",
+ "description": "wrestlers tone 3",
+ "unicodeVersion": "9.0",
+ "digest": "0f98afb56ecc165b4b8193c86565eaf0ded90456d6370b9d9f2e61eef572642e"
+ },
+ "wrestlers_tone4": {
+ "category": "activity",
+ "moji": "🤼🏾",
+ "description": "wrestlers tone 4",
+ "unicodeVersion": "9.0",
+ "digest": "595507c7163ed2349d6f5cd3afeb3cb961a889bda1091bb5b03bf97c743ddd83"
+ },
+ "wrestlers_tone5": {
+ "category": "activity",
+ "moji": "🤼🏿",
+ "description": "wrestlers tone 5",
+ "unicodeVersion": "9.0",
+ "digest": "9a844250d5dbce6369a7923154fd3e7192e9bbc8773044ede577e587c86a6481"
+ },
+ "water_polo": {
+ "category": "activity",
+ "moji": "🤽",
+ "description": "water polo",
+ "unicodeVersion": "9.0",
+ "digest": "b1ddaec4c4a506a89462a8d72173e44e0766c27e936e41d894de7ffc3da48236"
+ },
+ "water_polo_tone1": {
+ "category": "activity",
+ "moji": "🤽🏻",
+ "description": "water polo tone 1",
+ "unicodeVersion": "9.0",
+ "digest": "a86b73dac5b378283ca59e0985b98524c813aa34bc08e0c3bb5e0279a572adbf"
+ },
+ "water_polo_tone2": {
+ "category": "activity",
+ "moji": "🤽🏼",
+ "description": "water polo tone 2",
+ "unicodeVersion": "9.0",
+ "digest": "080f98b0afbc5a6a087ca9774c817717f8bae0bb2ff7d2968eecebeb7ef8fee6"
+ },
+ "water_polo_tone3": {
+ "category": "activity",
+ "moji": "🤽🏽",
+ "description": "water polo tone 3",
+ "unicodeVersion": "9.0",
+ "digest": "08a235850af9851f27148f53bd3e6f29668ce5d37afaaab0226a59195139e41c"
+ },
+ "water_polo_tone4": {
+ "category": "activity",
+ "moji": "🤽🏾",
+ "description": "water polo tone 4",
+ "unicodeVersion": "9.0",
+ "digest": "7e69265f86e139af75bc1d1bea03c21b7f2cc445e4b7a767b0515ffa7be1a99c"
+ },
+ "water_polo_tone5": {
+ "category": "activity",
+ "moji": "🤽🏿",
+ "description": "water polo tone 5",
+ "unicodeVersion": "9.0",
+ "digest": "db363a1e0a5f9b9d807258799d12e0a9010c99c383a4382f0f9a99feff2fd683"
+ },
+ "handball": {
+ "category": "activity",
+ "moji": "🤾",
+ "description": "handball",
+ "unicodeVersion": "9.0",
+ "digest": "66eb58f2b6fed026008dc73d99259f3023145b3955a52e47033f09055502992e"
+ },
+ "handball_tone1": {
+ "category": "activity",
+ "moji": "🤾🏻",
+ "description": "handball tone 1",
+ "unicodeVersion": "9.0",
+ "digest": "52b5a29715638415371c6546795930fa3b589e0723f59af9969a0de6822cfe04"
+ },
+ "handball_tone2": {
+ "category": "activity",
+ "moji": "🤾🏼",
+ "description": "handball tone 2",
+ "unicodeVersion": "9.0",
+ "digest": "8454813e2e2aabd6eb897bb973129c41d3628e0418e611c4968e8aee477b077d"
+ },
+ "handball_tone3": {
+ "category": "activity",
+ "moji": "🤾🏽",
+ "description": "handball tone 3",
+ "unicodeVersion": "9.0",
+ "digest": "b7f821482e3bcb63e36ba9b5d3838f1444c35095a9d68648cec24438cc860f4f"
+ },
+ "handball_tone4": {
+ "category": "activity",
+ "moji": "🤾🏾",
+ "description": "handball tone 4",
+ "unicodeVersion": "9.0",
+ "digest": "604e7948a99d23d469e3914040eea00e40faeab8e0f92d837fc1aebb7953104b"
+ },
+ "handball_tone5": {
+ "category": "activity",
+ "moji": "🤾🏿",
+ "description": "handball tone 5",
+ "unicodeVersion": "9.0",
+ "digest": "d989a7bd045a77163055d7973ee4ccbd3b04029a06df25cecd50a76466ca1526"
+ },
+ "juggling": {
+ "category": "activity",
+ "moji": "🤹",
+ "description": "juggling",
+ "unicodeVersion": "9.0",
+ "digest": "dbc4b794cb55d03b091b86d1b1b0682535c4a2f9a4d6d6b4ad5c413f1279a4f6"
+ },
+ "juggling_tone1": {
+ "category": "activity",
+ "moji": "🤹🏻",
+ "description": "juggling tone 1",
+ "unicodeVersion": "9.0",
+ "digest": "124e52052704f34a91e0cf8ef9ec7d08176942b8bee6faf2e4214df827443ae2"
+ },
+ "juggling_tone2": {
+ "category": "activity",
+ "moji": "🤹🏼",
+ "description": "juggling tone 2",
+ "unicodeVersion": "9.0",
+ "digest": "1d8609d3e765fbbed6659e5fbd5026911a276b5703b1e7593b12702de2d0555e"
+ },
+ "juggling_tone3": {
+ "category": "activity",
+ "moji": "🤹🏽",
+ "description": "juggling tone 3",
+ "unicodeVersion": "9.0",
+ "digest": "1666f54f93b744f8ee5cf8cc29be022ea5d3afd15436e642c5592d583c87830b"
+ },
+ "juggling_tone4": {
+ "category": "activity",
+ "moji": "🤹🏾",
+ "description": "juggling tone 4",
+ "unicodeVersion": "9.0",
+ "digest": "efe82bf4c3759b48435a7575ff0b016035d505dbdb2c7a3774d3878eed671602"
+ },
+ "juggling_tone5": {
+ "category": "activity",
+ "moji": "🤹🏿",
+ "description": "juggling tone 5",
+ "unicodeVersion": "9.0",
+ "digest": "ebf39b7b85e8c5e2fc899bb98de9f35739b7e656efd0526d9a50e8039e7bee86"
+ },
+ "bath": {
+ "category": "activity",
+ "moji": "🛀",
+ "description": "bath",
+ "unicodeVersion": "6.0",
+ "digest": "2cac99346c8054b8f883c45194395b35121e7108aaa36ecddccc16d30c32efae"
+ },
+ "bath_tone1": {
+ "category": "activity",
+ "moji": "🛀🏻",
+ "description": "bath tone 1",
+ "unicodeVersion": "8.0",
+ "digest": "17a3cd2bf235984c097eb1ab875b44516e1deba49c9237d0d25585b3df326a57"
+ },
+ "bath_tone2": {
+ "category": "activity",
+ "moji": "🛀🏼",
+ "description": "bath tone 2",
+ "unicodeVersion": "8.0",
+ "digest": "4ca1d1c1a48290c175551fe9ec30d1b82dc7a952530e978d085d72180c1886e0"
+ },
+ "bath_tone3": {
+ "category": "activity",
+ "moji": "🛀🏽",
+ "description": "bath tone 3",
+ "unicodeVersion": "8.0",
+ "digest": "084a77c7f583653d8c1d228432144b3210fcd50ed96630591663b3c4dff282fd"
+ },
+ "bath_tone4": {
+ "category": "activity",
+ "moji": "🛀🏾",
+ "description": "bath tone 4",
+ "unicodeVersion": "8.0",
+ "digest": "c605e25a1efd41be4ded235b71924c495b0aa861bdd6f43d9f5137c51de14bb1"
+ },
+ "bath_tone5": {
+ "category": "activity",
+ "moji": "🛀🏿",
+ "description": "bath tone 5",
+ "unicodeVersion": "8.0",
+ "digest": "e15b1bd11177d6a342dfe5eadb52969d60e6b7a1662afe55610aebbcc1a44242"
+ },
+ "sleeping_accommodation": {
+ "category": "objects",
+ "moji": "🛌",
+ "description": "sleeping accommodation",
+ "unicodeVersion": "7.0",
+ "digest": "18ea38c6da5ac6f86c56b546c78ba60bca1aca9eba397b9500d91e8533e81823"
+ },
+ "two_women_holding_hands": {
+ "category": "people",
+ "moji": "👭",
+ "description": "two women holding hands",
+ "unicodeVersion": "6.0",
+ "digest": "4a6dc2a4b900084faa7b934300b1f07717e41971a9eb9d7830cec83a78c14e46"
+ },
+ "couple": {
+ "category": "people",
+ "moji": "👫",
+ "description": "man and woman holding hands",
+ "unicodeVersion": "6.0",
+ "digest": "d6523ae18c5e2a10b355dda179ca7b172ece9ef3ac70548dba3c45a5d5169e70"
+ },
+ "two_men_holding_hands": {
+ "category": "people",
+ "moji": "👬",
+ "description": "two men holding hands",
+ "unicodeVersion": "6.0",
+ "digest": "8953ff520ff541f4ee8001ef4dfc22462b6ebf15f2e1adf9ba46d0c6384a0091"
+ },
+ "couplekiss": {
+ "category": "people",
+ "moji": "💏",
+ "description": "kiss",
+ "unicodeVersion": "6.0",
+ "digest": "14c66d83cace54fb7326ef04eb989ee27d5f2dec2e3884374cd346cd30340d5e"
+ },
+ "kiss_mm": {
+ "category": "people",
+ "moji": "👨‍❤️‍💋‍👨",
+ "description": "kiss (man,man)",
+ "unicodeVersion": "6.0",
+ "digest": "a256b66869e47ee51a6b3b08c56e9ecfe9f0ed5279aeb15e0045be30891ac70f"
+ },
+ "kiss_ww": {
+ "category": "people",
+ "moji": "👩‍❤️‍💋‍👩",
+ "description": "kiss (woman,woman)",
+ "unicodeVersion": "6.0",
+ "digest": "989937e58c7862cd6cedc74b8c7774fb01efaa7ab3b424f4472b638d101abdd9"
+ },
+ "couple_with_heart": {
+ "category": "people",
+ "moji": "💑",
+ "description": "couple with heart",
+ "unicodeVersion": "6.0",
+ "digest": "224c8f5d7c5a446b69e7175dfa6b07a9d36f2b2db3f908579813b6bcffb35f92"
+ },
+ "couple_mm": {
+ "category": "people",
+ "moji": "👨‍❤️‍👨",
+ "description": "couple (man,man)",
+ "unicodeVersion": "6.0",
+ "digest": "0c3344e2abffd74868fb52ffa5614c77737094bac369030ceac6218eee2a0edf"
+ },
+ "couple_ww": {
+ "category": "people",
+ "moji": "👩‍❤️‍👩",
+ "description": "couple (woman,woman)",
+ "unicodeVersion": "6.0",
+ "digest": "56651c521bbaade1979708f52d827e2251581b576c3afa651c89ea4c63739916"
+ },
+ "family_mwg": {
+ "category": "people",
+ "moji": "👨‍👩‍👧",
+ "description": "family (man,woman,girl)",
+ "unicodeVersion": "6.0",
+ "digest": "3c256c9982e40db556a4667e6a666c06f6088249b5cc260e92bd443cc5d85b1c"
+ },
+ "family_mwgb": {
+ "category": "people",
+ "moji": "👨‍👩‍👧‍👦",
+ "description": "family (man,woman,girl,boy)",
+ "unicodeVersion": "6.0",
+ "digest": "09481d128ece6416b169b99a9b770a1c72f662adde86fa6c214a1f979e98f8bd"
+ },
+ "family_mwbb": {
+ "category": "people",
+ "moji": "👨‍👩‍👦‍👦",
+ "description": "family (man,woman,boy,boy)",
+ "unicodeVersion": "6.0",
+ "digest": "e0056e604ec4d411ebb4aefc079f47efb11bf7db4c3ebf9d17759f40075a19a5"
+ },
+ "family_mwgg": {
+ "category": "people",
+ "moji": "👨‍👩‍👧‍👧",
+ "description": "family (man,woman,girl,girl)",
+ "unicodeVersion": "6.0",
+ "digest": "2e9670f98d310c15491a66a4108773cc994401cc864923c0fc1c61123b99c1b5"
+ },
+ "family_mmb": {
+ "category": "people",
+ "moji": "👨‍👨‍👦",
+ "description": "family (man,man,boy)",
+ "unicodeVersion": "6.0",
+ "digest": "4464331734f8afbc33ebd38552ea1452813dc9e0295e315e4bcadf4876444474"
+ },
+ "family_mmg": {
+ "category": "people",
+ "moji": "👨‍👨‍👧",
+ "description": "family (man,man,girl)",
+ "unicodeVersion": "6.0",
+ "digest": "961e428e8dfccab9575c6add7fc1523643caa2da47890b2baaa265c5f0ea0d8b"
+ },
+ "family_mmgb": {
+ "category": "people",
+ "moji": "👨‍👨‍👧‍👦",
+ "description": "family (man,man,girl,boy)",
+ "unicodeVersion": "6.0",
+ "digest": "7240b99930a16295b3396d7183ecad125423d019b08f11746969c3939005ceff"
+ },
+ "family_mmbb": {
+ "category": "people",
+ "moji": "👨‍👨‍👦‍👦",
+ "description": "family (man,man,boy,boy)",
+ "unicodeVersion": "6.0",
+ "digest": "ad976771f346f8c48d0a0077ee8ec427048937814a03a8691d422dd664991734"
+ },
+ "family_mmgg": {
+ "category": "people",
+ "moji": "👨‍👨‍👧‍👧",
+ "description": "family (man,man,girl,girl)",
+ "unicodeVersion": "6.0",
+ "digest": "2f2d726791f01c880df79431dc9472724f61d621b8b17344a9839db6f65b8cd7"
+ },
+ "family_wwb": {
+ "category": "people",
+ "moji": "👩‍👩‍👦",
+ "description": "family (woman,woman,boy)",
+ "unicodeVersion": "6.0",
+ "digest": "df970eeb8fe917398b9dfd68b181ec6cfe1f7e981af2f67e5d8c96a228aa8147"
+ },
+ "family_wwg": {
+ "category": "people",
+ "moji": "👩‍👩‍👧",
+ "description": "family (woman,woman,girl)",
+ "unicodeVersion": "6.0",
+ "digest": "1f64c8a10f813344c4d10ab78fb55c2a2c221101e8645c6b6dcc0c8ca40c029c"
+ },
+ "family_wwgb": {
+ "category": "people",
+ "moji": "👩‍👩‍👧‍👦",
+ "description": "family (woman,woman,girl,boy)",
+ "unicodeVersion": "6.0",
+ "digest": "d0962105cff6755305a08c0a0f748cb4fdfe3f78c9add25640dac05a8e4200e6"
+ },
+ "family_wwbb": {
+ "category": "people",
+ "moji": "👩‍👩‍👦‍👦",
+ "description": "family (woman,woman,boy,boy)",
+ "unicodeVersion": "6.0",
+ "digest": "d6e262911da6a7fc47d49642666f5068e597976c1760d25758114cad16b1ac1c"
+ },
+ "family_wwgg": {
+ "category": "people",
+ "moji": "👩‍👩‍👧‍👧",
+ "description": "family (woman,woman,girl,girl)",
+ "unicodeVersion": "6.0",
+ "digest": "9733e9a03cccda66612c0274842e514092d1816dba914f337ba422e988225352"
+ },
+ "speaking_head": {
+ "category": "people",
+ "moji": "🗣",
+ "description": "speaking head in silhouette",
+ "unicodeVersion": "7.0",
+ "digest": "d6e536f54711d04899135d966b9ba64286e58b05879c90855e49e5c64fca7567"
+ },
+ "bust_in_silhouette": {
+ "category": "people",
+ "moji": "👤",
+ "description": "bust in silhouette",
+ "unicodeVersion": "6.0",
+ "digest": "17f35781aa264a26812ca9a13a32e7ab73a7eb95af1bc0b812a35d6fc9ac521f"
+ },
+ "busts_in_silhouette": {
+ "category": "people",
+ "moji": "👥",
+ "description": "busts in silhouette",
+ "unicodeVersion": "6.0",
+ "digest": "f9aa0c03d90e5d0f57f8114b998019452c5756a8dcbf1750b07f9aeae2f4ab08"
+ },
+ "family": {
+ "category": "people",
+ "moji": "👪",
+ "description": "family",
+ "unicodeVersion": "6.0",
+ "digest": "363da6588b6816c4f9811c76fee6601e3111030a1ba86fea9464c60568144c50"
+ },
+ "footprints": {
+ "category": "people",
+ "moji": "👣",
+ "description": "footprints",
+ "unicodeVersion": "6.0",
+ "digest": "43a8daa470e070c1cfdf541a502076f0a258e8cde2f707ad842bc24e499ad69c"
+ },
+ "tone1": {
+ "category": "modifier",
+ "moji": "🏻",
+ "description": "emoji modifier Fitzpatrick type-1-2",
+ "unicodeVersion": "8.0",
+ "digest": "545d866024aa7d4de2e2254420a9d8cca667534672d9e7122bd3e67cf81b694b"
+ },
+ "tone2": {
+ "category": "modifier",
+ "moji": "🏼",
+ "description": "emoji modifier Fitzpatrick type-3",
+ "unicodeVersion": "8.0",
+ "digest": "c2f93946364e79ed130e3c355416ada791c09d9b3d1bbfa44d03324bfabd642e"
+ },
+ "tone3": {
+ "category": "modifier",
+ "moji": "🏽",
+ "description": "emoji modifier Fitzpatrick type-4",
+ "unicodeVersion": "8.0",
+ "digest": "ea55f80c25ec5df8232d509913b8f5e3260ecc98a2db0f5de333d7e3c9f05bb2"
+ },
+ "tone4": {
+ "category": "modifier",
+ "moji": "🏾",
+ "description": "emoji modifier Fitzpatrick type-5",
+ "unicodeVersion": "8.0",
+ "digest": "676bd7f45026066bb72a6b5344e288c6b42ce9524eb0065d50d3354cc716cc6a"
+ },
+ "tone5": {
+ "category": "modifier",
+ "moji": "🏿",
+ "description": "emoji modifier Fitzpatrick type-6",
+ "unicodeVersion": "8.0",
+ "digest": "b9ab9e2f6307e2e5b8e0560d6b3048f2d0cd58e11c90b03d90688b91fdb4b5f8"
+ },
+ "monkey_face": {
+ "category": "nature",
+ "moji": "🐵",
+ "description": "monkey face",
+ "unicodeVersion": "6.0",
+ "digest": "f006cc5b32745c0e315ad2b2e995587f7f3dec115f81db7f681e00e040e1ab02"
+ },
+ "monkey": {
+ "category": "nature",
+ "moji": "🐒",
+ "description": "monkey",
+ "unicodeVersion": "6.0",
+ "digest": "0e555b55cdaeea97aba5e054ce6c283578036d739466a78be63662872729cf55"
+ },
+ "gorilla": {
+ "category": "nature",
+ "moji": "🦍",
+ "description": "gorilla",
+ "unicodeVersion": "9.0",
+ "digest": "bfd0ef21e14e0e11f426dca6ed062fbc360038df043855dde873bed4cef247bd"
+ },
+ "dog": {
+ "category": "nature",
+ "moji": "🐶",
+ "description": "dog face",
+ "unicodeVersion": "6.0",
+ "digest": "d6ee83df6b3a233b90c0b589c6f290164239fb4062be21566430183d9b4e507a"
+ },
+ "dog2": {
+ "category": "nature",
+ "moji": "🐕",
+ "description": "dog",
+ "unicodeVersion": "6.0",
+ "digest": "ba52e04ae1e024c596fc453b9af53347ecf76d342150b7701eccb83ef2df67f0"
+ },
+ "poodle": {
+ "category": "nature",
+ "moji": "🐩",
+ "description": "poodle",
+ "unicodeVersion": "6.0",
+ "digest": "4e73855cbdb10f644146fb3bc1636a74faff02a5d69ad0f91302e920973b03f0"
+ },
+ "wolf": {
+ "category": "nature",
+ "moji": "🐺",
+ "description": "wolf face",
+ "unicodeVersion": "6.0",
+ "digest": "f961f617f4b6af5a0b1683132476ce765cb3ce74c3c5c32083593622ff8c10e7"
+ },
+ "fox": {
+ "category": "nature",
+ "moji": "🦊",
+ "description": "fox face",
+ "unicodeVersion": "9.0",
+ "digest": "0d98c8fd904dceaa53041b5c8dcf7c357805712ed547668ee53ab48808c6255b"
+ },
"cat": {
"category": "nature",
"moji": "🐱",
@@ -1511,68 +4808,747 @@
"unicodeVersion": "6.0",
"digest": "a12d0ab04554c3a7c2b45e6501da312773777587bef28ba233202f3812901af4"
},
- "cd": {
- "category": "objects",
- "moji": "💿",
- "description": "optical disc",
+ "lion_face": {
+ "category": "nature",
+ "moji": "🦁",
+ "description": "lion face",
+ "unicodeVersion": "8.0",
+ "digest": "59067f1acf8b42395164dc02089624d696549bfdbd60a6dd24d04199a0c3dda2"
+ },
+ "tiger": {
+ "category": "nature",
+ "moji": "🐯",
+ "description": "tiger face",
"unicodeVersion": "6.0",
- "digest": "d7242edbba09de17d223d7e3178a241b1aafe05d14a19000428522dc9b2891f4"
+ "digest": "0ea11ea4b71adee37b67019634153f09ca6d5b762d4c9507a3d3f7b47ff59274"
},
- "chains": {
- "category": "objects",
- "moji": "⛓",
- "description": "chains",
- "unicodeVersion": "5.2",
- "digest": "245a60d0dcdf759898d5f6093d3b67e7e3234a5799de52af87166886bb8e67ff"
+ "tiger2": {
+ "category": "nature",
+ "moji": "🐅",
+ "description": "tiger",
+ "unicodeVersion": "6.0",
+ "digest": "84d80ae536dfad345dd878970c339451d4189f73f0d786cafcaa2421dab9fe97"
},
- "champagne": {
- "category": "food",
- "moji": "🍾",
- "description": "bottle with popping cork",
+ "leopard": {
+ "category": "nature",
+ "moji": "🐆",
+ "description": "leopard",
+ "unicodeVersion": "6.0",
+ "digest": "00204970970b9dd38bb9b1ae30a11832c992ade57bce1496a1f3c366e7d31f00"
+ },
+ "horse": {
+ "category": "nature",
+ "moji": "🐴",
+ "description": "horse face",
+ "unicodeVersion": "6.0",
+ "digest": "fa6681d0536051b55e189bdd7435edfd4d4967cc28c255b55baf25cb25f40865"
+ },
+ "racehorse": {
+ "category": "nature",
+ "moji": "🐎",
+ "description": "horse",
+ "unicodeVersion": "6.0",
+ "digest": "44ade212bafa562416e9df439229052db2e45cf631d6d32a8261aa541a675a07"
+ },
+ "unicorn": {
+ "category": "nature",
+ "moji": "🦄",
+ "description": "unicorn face",
"unicodeVersion": "8.0",
- "digest": "6e686856c9e39b9947ef669394e6621fffd2a91ebe8f21c2cdc06b06669d1160"
+ "digest": "00d784632f584c953340b1f1474be2a1ab6f065625e69e2d06dabd38ab45531c"
},
- "champagne_glass": {
- "category": "food",
- "moji": "🥂",
- "description": "clinking glasses",
+ "deer": {
+ "category": "nature",
+ "moji": "🦌",
+ "description": "deer",
"unicodeVersion": "9.0",
- "digest": "8e59ae799d7c7844dc19f5fd4fbefebc34f597059373c94e8639a7ce73d13519"
+ "digest": "83a9c2a0bfb5d82631dc653337279545d72ccea10fb2ccf6da6ab82586fb1d56"
},
- "chart": {
+ "cow": {
+ "category": "nature",
+ "moji": "🐮",
+ "description": "cow face",
+ "unicodeVersion": "6.0",
+ "digest": "9959b4d621471e4c3ddd23f52ddeb5ddb5792a0b6ac8697d2c2940bd6cb01b82"
+ },
+ "ox": {
+ "category": "nature",
+ "moji": "🐂",
+ "description": "ox",
+ "unicodeVersion": "6.0",
+ "digest": "060595f1707bd4b0182c2610e397dbb8800a2410e2205c0156043163b32b9965"
+ },
+ "water_buffalo": {
+ "category": "nature",
+ "moji": "🐃",
+ "description": "water buffalo",
+ "unicodeVersion": "6.0",
+ "digest": "dd9f6609e0ea97f610ab30e34c6564fe1504a54e2bd258fa64601cdbc91de177"
+ },
+ "cow2": {
+ "category": "nature",
+ "moji": "🐄",
+ "description": "cow",
+ "unicodeVersion": "6.0",
+ "digest": "a9f5cdb317d96e7536d87faff2f2c651d6fb5bd00b89991c977b270e9f522820"
+ },
+ "pig": {
+ "category": "nature",
+ "moji": "🐷",
+ "description": "pig face",
+ "unicodeVersion": "6.0",
+ "digest": "7ce19e18b6fb941b8075e18f8c680048828c8b129d1a74a9ac3c2ae937a02aa5"
+ },
+ "pig2": {
+ "category": "nature",
+ "moji": "🐖",
+ "description": "pig",
+ "unicodeVersion": "6.0",
+ "digest": "ba4acbc207d817071d485e33186f5ddc20d470a0dc468fb1fea502d4768c25d7"
+ },
+ "boar": {
+ "category": "nature",
+ "moji": "🐗",
+ "description": "boar",
+ "unicodeVersion": "6.0",
+ "digest": "0e6774094f935bf5eaccc85077e357d2f002276a76aa7a3ba8457e11c7458314"
+ },
+ "pig_nose": {
+ "category": "nature",
+ "moji": "🐽",
+ "description": "pig nose",
+ "unicodeVersion": "6.0",
+ "digest": "f9fb90cd7919ed06aec8bf6b0cda13fcd6a58b25329b48a57714d0909a290e4b"
+ },
+ "ram": {
+ "category": "nature",
+ "moji": "🐏",
+ "description": "ram",
+ "unicodeVersion": "6.0",
+ "digest": "8f4a19d64e01593a7487fb4d58fe1d300055e458d3daa1659ec59877a8a3b00f"
+ },
+ "sheep": {
+ "category": "nature",
+ "moji": "🐑",
+ "description": "sheep",
+ "unicodeVersion": "6.0",
+ "digest": "7bd6d2af15a7d13587b9521d80668d5db70c67188df59a33921e78edbf40e42e"
+ },
+ "goat": {
+ "category": "nature",
+ "moji": "🐐",
+ "description": "goat",
+ "unicodeVersion": "6.0",
+ "digest": "83e2498db2f088b8bfaf7614a543bc691da93eec2d8a156cfdde67e86ccbade8"
+ },
+ "dromedary_camel": {
+ "category": "nature",
+ "moji": "🐪",
+ "description": "dromedary camel",
+ "unicodeVersion": "6.0",
+ "digest": "8401968c38aa200463e4f8dd68287cff7ccc0c2be93c425329ccc56bcfa1b672"
+ },
+ "camel": {
+ "category": "nature",
+ "moji": "🐫",
+ "description": "bactrian camel",
+ "unicodeVersion": "6.0",
+ "digest": "666d8efce2033fd7a69fa8537a0d724b3e1e0d9d5093b8d7666a45a4f7d1831f"
+ },
+ "elephant": {
+ "category": "nature",
+ "moji": "🐘",
+ "description": "elephant",
+ "unicodeVersion": "6.0",
+ "digest": "c151ce8cf6aee435ed27099fac1d5e55501e643f69d7e39476fa6bcd73be9559"
+ },
+ "rhino": {
+ "category": "nature",
+ "moji": "🦏",
+ "description": "rhinoceros",
+ "unicodeVersion": "9.0",
+ "digest": "6b23d83c2b2cc252e5983b75840086971807c2ea5425f17753d049b5b7a5ebf7"
+ },
+ "mouse": {
+ "category": "nature",
+ "moji": "🐭",
+ "description": "mouse face",
+ "unicodeVersion": "6.0",
+ "digest": "ba3d78feeca02888c96bc81ffbe07d8be073f695ab17e167d63c4fe0ad2edfc0"
+ },
+ "mouse2": {
+ "category": "nature",
+ "moji": "🐁",
+ "description": "mouse",
+ "unicodeVersion": "6.0",
+ "digest": "abd7dded58299599e4cd419c3052e0d4e52ccc788e905201f5dd225b74152aff"
+ },
+ "rat": {
+ "category": "nature",
+ "moji": "🐀",
+ "description": "rat",
+ "unicodeVersion": "6.0",
+ "digest": "7f15ecd2a5c5dd340a5e1c69454a512bf176cbb74b042efc65a7d6c8efad5e6e"
+ },
+ "hamster": {
+ "category": "nature",
+ "moji": "🐹",
+ "description": "hamster face",
+ "unicodeVersion": "6.0",
+ "digest": "aa2bb7c3ca254800ae7bb02b49ae4206984d4a07350be9fe8e0d645133918863"
+ },
+ "rabbit": {
+ "category": "nature",
+ "moji": "🐰",
+ "description": "rabbit face",
+ "unicodeVersion": "6.0",
+ "digest": "1a7201ef67f5ce4c9deb0f5c9e1e14d0e3d6ffaac17bbb73d6ca6c1de62c43ea"
+ },
+ "rabbit2": {
+ "category": "nature",
+ "moji": "🐇",
+ "description": "rabbit",
+ "unicodeVersion": "6.0",
+ "digest": "120075f5ca435b5dc903bfe3934bbc9adb2c17c16a8b839625320a46c299827a"
+ },
+ "chipmunk": {
+ "category": "nature",
+ "moji": "🐿",
+ "description": "chipmunk",
+ "unicodeVersion": "7.0",
+ "digest": "ec1f696b11273364e8c7f386d38bbb133e5d7c387b9ea7ebc49b4fd5c84717c4"
+ },
+ "bat": {
+ "category": "nature",
+ "moji": "🦇",
+ "description": "bat",
+ "unicodeVersion": "9.0",
+ "digest": "5f66d15070c283ae9a293719ada7d88d6837f858dda94c61cf56835f734985d8"
+ },
+ "bear": {
+ "category": "nature",
+ "moji": "🐻",
+ "description": "bear face",
+ "unicodeVersion": "6.0",
+ "digest": "6d7ed0e469e7146c5fa5caf7496e516f553eecf2b212ec7835a0636cff147a51"
+ },
+ "koala": {
+ "category": "nature",
+ "moji": "🐨",
+ "description": "koala",
+ "unicodeVersion": "6.0",
+ "digest": "73549917efa845ecc4a5b4c347fc6f686717e95323fed97977e6677c881f801d"
+ },
+ "panda_face": {
+ "category": "nature",
+ "moji": "🐼",
+ "description": "panda face",
+ "unicodeVersion": "6.0",
+ "digest": "8439a209f086e61dde97c5a578dc7b3a23099fd4f5a939e27535956a2c78e5c9"
+ },
+ "feet": {
+ "category": "nature",
+ "moji": "🐾",
+ "description": "paw prints",
+ "unicodeVersion": "6.0",
+ "digest": "cb4b59f2c5df407bae3d02b638721b7f47b685d4e03f7ddb8d1fedaae9e2179a"
+ },
+ "turkey": {
+ "category": "nature",
+ "moji": "🦃",
+ "description": "turkey",
+ "unicodeVersion": "8.0",
+ "digest": "2b141b75a1df1c8e347fe96a5193060d6feb230e4ced5976e3a22ab4a145887b"
+ },
+ "chicken": {
+ "category": "nature",
+ "moji": "🐔",
+ "description": "chicken",
+ "unicodeVersion": "6.0",
+ "digest": "6fb2f9392e8e78ec3e46187013e53a19dcb319e9528c27e453ae2dc470d91cf3"
+ },
+ "rooster": {
+ "category": "nature",
+ "moji": "🐓",
+ "description": "rooster",
+ "unicodeVersion": "6.0",
+ "digest": "8ebd22e8776d16c6557f777a731871e93a20b8b828b718f57267033f13a5e50b"
+ },
+ "hatching_chick": {
+ "category": "nature",
+ "moji": "🐣",
+ "description": "hatching chick",
+ "unicodeVersion": "6.0",
+ "digest": "839de0c6a31a0c0ff1e4ee36f64fe462667d15acda79747dd0a362f59a37f144"
+ },
+ "baby_chick": {
+ "category": "nature",
+ "moji": "🐤",
+ "description": "baby chick",
+ "unicodeVersion": "6.0",
+ "digest": "997e1f93ed8fb08c4362d5b0907b8243dc623a62e6cf0b3a9034112f187a3314"
+ },
+ "hatched_chick": {
+ "category": "nature",
+ "moji": "🐥",
+ "description": "front-facing baby chick",
+ "unicodeVersion": "6.0",
+ "digest": "e87f9edbc75c2f65940e59ec3ee752bf15cca10767e83766d3bb2e2cacbb30d1"
+ },
+ "bird": {
+ "category": "nature",
+ "moji": "🐦",
+ "description": "bird",
+ "unicodeVersion": "6.0",
+ "digest": "b19660ba47b3a0151dc470064a6594cdb9aaaa14a42a0a18d70c253206c954e5"
+ },
+ "penguin": {
+ "category": "nature",
+ "moji": "🐧",
+ "description": "penguin",
+ "unicodeVersion": "6.0",
+ "digest": "0735168781cd9316b40fbce845a55980acc33491112aea840b26b0cef226ccc0"
+ },
+ "dove": {
+ "category": "nature",
+ "moji": "🕊",
+ "description": "dove of peace",
+ "unicodeVersion": "7.0",
+ "digest": "66d335c04e2daef92ed0ee5979d6b072d99551e2950cdbc78882bf28269773f8"
+ },
+ "eagle": {
+ "category": "nature",
+ "moji": "🦅",
+ "description": "eagle",
+ "unicodeVersion": "9.0",
+ "digest": "996a7d29861f0ebd0762baeefccd1a8f7179b52bbb16e34e54787c9f6e959857"
+ },
+ "duck": {
+ "category": "nature",
+ "moji": "🦆",
+ "description": "duck",
+ "unicodeVersion": "9.0",
+ "digest": "41f179dcac9de4a0eb5335ce435176db90fb1e35c043a100a9be09ce1ce3fac0"
+ },
+ "owl": {
+ "category": "nature",
+ "moji": "🦉",
+ "description": "owl",
+ "unicodeVersion": "9.0",
+ "digest": "f5c1451adf6d1192cc377bc86f25604233d3c9fee42d4b4d4e8b4c4d76d0e981"
+ },
+ "frog": {
+ "category": "nature",
+ "moji": "🐸",
+ "description": "frog face",
+ "unicodeVersion": "6.0",
+ "digest": "f1b86d108560eb022ed026b7158477f664bdebe2b313bf49e4bd4f3c79278bf3"
+ },
+ "crocodile": {
+ "category": "nature",
+ "moji": "🐊",
+ "description": "crocodile",
+ "unicodeVersion": "6.0",
+ "digest": "94f36ae30277cc802bbe827c727358540548ebdd85d3e1f70e976e205a885756"
+ },
+ "turtle": {
+ "category": "nature",
+ "moji": "🐢",
+ "description": "turtle",
+ "unicodeVersion": "6.0",
+ "digest": "cb9ebbbd5c861943b8e52ad447959cb489edcb64c93a878a2db00060220e56da"
+ },
+ "lizard": {
+ "category": "nature",
+ "moji": "🦎",
+ "description": "lizard",
+ "unicodeVersion": "9.0",
+ "digest": "7c21a4d9e165efe57eca3982944b93ad30bf7ea462bdbf5cb4d63e3dc93e7707"
+ },
+ "snake": {
+ "category": "nature",
+ "moji": "🐍",
+ "description": "snake",
+ "unicodeVersion": "6.0",
+ "digest": "1b28000702a5b3b294b22c5bd1ad8c203937712fb30dc1cb12f63fc6244e38e6"
+ },
+ "dragon_face": {
+ "category": "nature",
+ "moji": "🐲",
+ "description": "dragon face",
+ "unicodeVersion": "6.0",
+ "digest": "8990c5f5a5cf7d94e4eafb7515e6367cfaeea6bd648c36d65dca1525b60d591c"
+ },
+ "dragon": {
+ "category": "nature",
+ "moji": "🐉",
+ "description": "dragon",
+ "unicodeVersion": "6.0",
+ "digest": "460c7674101d32edf90b563329947c620c1e6bea9dfe2c81483e3313248d498c"
+ },
+ "whale": {
+ "category": "nature",
+ "moji": "🐳",
+ "description": "spouting whale",
+ "unicodeVersion": "6.0",
+ "digest": "6a5de13dec0e0bbb05c9b3222ce04c16c079367124e59a49d015bbbee7a174a7"
+ },
+ "whale2": {
+ "category": "nature",
+ "moji": "🐋",
+ "description": "whale",
+ "unicodeVersion": "6.0",
+ "digest": "939087b5e3b24f0cc27978a8777eec9c38f6e8bafc98200ff2606c3648fa589e"
+ },
+ "dolphin": {
+ "category": "nature",
+ "moji": "🐬",
+ "description": "dolphin",
+ "unicodeVersion": "6.0",
+ "digest": "92298c8287cbda0eaad24b50145236bb29ff9a6473a6faac07deddd8ae97ff1b"
+ },
+ "fish": {
+ "category": "nature",
+ "moji": "🐟",
+ "description": "fish",
+ "unicodeVersion": "6.0",
+ "digest": "036332286b852c6dd8c672f1540a4b4c5082885555f60a7d0f93b82c2acdc0f5"
+ },
+ "tropical_fish": {
+ "category": "nature",
+ "moji": "🐠",
+ "description": "tropical fish",
+ "unicodeVersion": "6.0",
+ "digest": "26974903529bbda281c4f1302d8ced6eeca6f3d316ef0bd4ab6617062bd10ca9"
+ },
+ "blowfish": {
+ "category": "nature",
+ "moji": "🐡",
+ "description": "blowfish",
+ "unicodeVersion": "6.0",
+ "digest": "084c33958f40081d19012e35fcf72f085cf1f749964ba7256d8721e8a2d929e0"
+ },
+ "shark": {
+ "category": "nature",
+ "moji": "🦈",
+ "description": "shark",
+ "unicodeVersion": "9.0",
+ "digest": "552b7265f53c435c860583c2aaee8acec5f854fa2092f6e8be74d89f9f3132da"
+ },
+ "octopus": {
+ "category": "nature",
+ "moji": "🐙",
+ "description": "octopus",
+ "unicodeVersion": "6.0",
+ "digest": "215314d376c54c12b73eef4115a8d91a3a21f39e39d41a72b24ef2228595b6af"
+ },
+ "shell": {
+ "category": "nature",
+ "moji": "🐚",
+ "description": "spiral shell",
+ "unicodeVersion": "6.0",
+ "digest": "4d30626d66ac2921fb7a81761841923b855e363c4198eea67c76d86c696cc805"
+ },
+ "snail": {
+ "category": "nature",
+ "moji": "🐌",
+ "description": "snail",
+ "unicodeVersion": "6.0",
+ "digest": "4244f824afbbf8f60e41654f35596395a9a45715a3f229e351aaea6dab361e02"
+ },
+ "butterfly": {
+ "category": "nature",
+ "moji": "🦋",
+ "description": "butterfly",
+ "unicodeVersion": "9.0",
+ "digest": "a7c33f816a9705a83a0debf8cbeb1ff9dc430200f52e8d0f5682fe11c39b4473"
+ },
+ "bug": {
+ "category": "nature",
+ "moji": "🐛",
+ "description": "bug",
+ "unicodeVersion": "6.0",
+ "digest": "8c3a35433d6c2d63c57d2fdfa8be50bd35c334dfe0ba1b1554555e872091b857"
+ },
+ "ant": {
+ "category": "nature",
+ "moji": "🐜",
+ "description": "ant",
+ "unicodeVersion": "6.0",
+ "digest": "d60a32588453dd0a17b55358089bc62e53f09fc262c7aa30a1d6fc2c1e7cde2c"
+ },
+ "bee": {
+ "category": "nature",
+ "moji": "🐝",
+ "description": "honeybee",
+ "unicodeVersion": "6.0",
+ "digest": "57d564f50abfd154473c3ebb16aeac058dc15253fed2b319daf1f2cb063e29af"
+ },
+ "beetle": {
+ "category": "nature",
+ "moji": "🐞",
+ "description": "lady beetle",
+ "unicodeVersion": "6.0",
+ "digest": "c05375aae35bdedd627fa55d63bda6ec9351d9a261a4a60145a8a775d99deaee"
+ },
+ "spider": {
+ "category": "nature",
+ "moji": "🕷",
+ "description": "spider",
+ "unicodeVersion": "7.0",
+ "digest": "204672675b8f272185eb58517e6cac6e9398a9c27279b8bb0da97330d35da094"
+ },
+ "spider_web": {
+ "category": "nature",
+ "moji": "🕸",
+ "description": "spider web",
+ "unicodeVersion": "7.0",
+ "digest": "82a223ba2c1dc71ac0945544a16a2f607a679c5727cd86070b537bec6e1fdcb1"
+ },
+ "scorpion": {
+ "category": "nature",
+ "moji": "🦂",
+ "description": "scorpion",
+ "unicodeVersion": "8.0",
+ "digest": "2e97ed412be63a4eaefd4205d13e5d4957389ce7497c4c79e00e9729bdb39e0c"
+ },
+ "bouquet": {
+ "category": "nature",
+ "moji": "💐",
+ "description": "bouquet",
+ "unicodeVersion": "6.0",
+ "digest": "0d737305d51cced40a4de5da6dacb5dfb049c7d8ad4136c312193049a9915a3b"
+ },
+ "cherry_blossom": {
+ "category": "nature",
+ "moji": "🌸",
+ "description": "cherry blossom",
+ "unicodeVersion": "6.0",
+ "digest": "67e42777b2d2b8e71dc303c096b45f5b66733d2099533c6c6f798c98e314234d"
+ },
+ "white_flower": {
"category": "symbols",
- "moji": "💹",
- "description": "chart with upwards trend and yen sign",
+ "moji": "💮",
+ "description": "white flower",
"unicodeVersion": "6.0",
- "digest": "b513f5b5ff0e9a8139aaa378982e1937b09da1827adef70158d07d8c4a288139"
+ "digest": "30a1761e5672133bf4172bfecad661e951488c89e22ceee88d04fd3bcbd7e6d1"
},
- "chart_with_downwards_trend": {
- "category": "objects",
- "moji": "📉",
- "description": "chart with downwards trend",
+ "rosette": {
+ "category": "activity",
+ "moji": "🏵",
+ "description": "rosette",
+ "unicodeVersion": "7.0",
+ "digest": "b5d6fac78383056f66f48e9576f3e8e1f38c473330f44382e0f2a13bb0b6cf89"
+ },
+ "rose": {
+ "category": "nature",
+ "moji": "🌹",
+ "description": "rose",
"unicodeVersion": "6.0",
- "digest": "0c12dfccd80564f29dc56d3e994e55793ec02d1bd4e3109c7e1b003163579ecc"
+ "digest": "a86cff9a79b2296c8c2c39dbeb7bd9e42edf2e5af402ae5dfb63a3bf910083cd"
},
- "chart_with_upwards_trend": {
- "category": "objects",
- "moji": "📈",
- "description": "chart with upwards trend",
+ "wilted_rose": {
+ "category": "nature",
+ "moji": "🥀",
+ "description": "wilted flower",
+ "unicodeVersion": "9.0",
+ "digest": "401fb35a8c6d26db708b2756c0bb55e0c62490a8cbdd51a21d6634881a62d32c"
+ },
+ "hibiscus": {
+ "category": "nature",
+ "moji": "🌺",
+ "description": "hibiscus",
"unicodeVersion": "6.0",
- "digest": "c09ad5dd7817106a1df65a1f8da086006de804d2de22107423a5805da5ee3f9f"
+ "digest": "2dc90fc37f140ac3a56600042ba44116f28e40bdda1d041d2218a6251099cbf6"
},
- "checkered_flag": {
- "category": "travel",
- "moji": "🏁",
- "description": "chequered flag",
+ "sunflower": {
+ "category": "nature",
+ "moji": "🌻",
+ "description": "sunflower",
"unicodeVersion": "6.0",
- "digest": "b2527f2ce6797b261083947b9e9dd588382da6e7e46f81a1919d0f039635f21b"
+ "digest": "ea2947ff8994128b131e8e692d6183f38553d709fafa7611f3c15d00e5e2b9d6"
},
- "cheese": {
+ "blossom": {
+ "category": "nature",
+ "moji": "🌼",
+ "description": "blossom",
+ "unicodeVersion": "6.0",
+ "digest": "4b330c92cc58a402534cb0bbbde2d18d680a88da5e1321d460e2d5ce2644d774"
+ },
+ "tulip": {
+ "category": "nature",
+ "moji": "🌷",
+ "description": "tulip",
+ "unicodeVersion": "6.0",
+ "digest": "78b7615137fedb534bb1a760b1c7bfdd09b60e1c770ba21ee9f5a3799aa7c93b"
+ },
+ "seedling": {
+ "category": "nature",
+ "moji": "🌱",
+ "description": "seedling",
+ "unicodeVersion": "6.0",
+ "digest": "4227f4780193ccd7d807c0e6d18b42cbaa247554708efbf64dbd3b7c6919a466"
+ },
+ "evergreen_tree": {
+ "category": "nature",
+ "moji": "🌲",
+ "description": "evergreen tree",
+ "unicodeVersion": "6.0",
+ "digest": "62ae1c4109a7458bcc754f89311fa37be31a0cdc63ee96f420482568bc74000c"
+ },
+ "deciduous_tree": {
+ "category": "nature",
+ "moji": "🌳",
+ "description": "deciduous tree",
+ "unicodeVersion": "6.0",
+ "digest": "89804981f62825a45150919436442c70838d90a015de398d2bb9aaffa4b2e998"
+ },
+ "palm_tree": {
+ "category": "nature",
+ "moji": "🌴",
+ "description": "palm tree",
+ "unicodeVersion": "6.0",
+ "digest": "4e5f7c8c216d86839a59a559db1c6b8741d80ec8e2cb8e6c055f7ee5dcf1ceaf"
+ },
+ "cactus": {
+ "category": "nature",
+ "moji": "🌵",
+ "description": "cactus",
+ "unicodeVersion": "6.0",
+ "digest": "1690158bb63afbe907bbc343627b9c353198ad611b9bb195f5eda5c3545aa2e9"
+ },
+ "ear_of_rice": {
+ "category": "nature",
+ "moji": "🌾",
+ "description": "ear of rice",
+ "unicodeVersion": "6.0",
+ "digest": "9ea7efc5ebf3cea2cfd17ac2318a1ea0dd1519af6a60a95af33aabe1baa9042e"
+ },
+ "herb": {
+ "category": "nature",
+ "moji": "🌿",
+ "description": "herb",
+ "unicodeVersion": "6.0",
+ "digest": "26a5958f4afaa7ec0f0fbbddfe50b6a467dff109fab45345c5a63b0316d20370"
+ },
+ "shamrock": {
+ "category": "nature",
+ "moji": "☘",
+ "description": "shamrock",
+ "unicodeVersion": "4.1",
+ "digest": "cb9408a7b1884bfca8fe7cd2ea93440a49a44b226298325a3f138f6e772f5d64"
+ },
+ "four_leaf_clover": {
+ "category": "nature",
+ "moji": "🍀",
+ "description": "four leaf clover",
+ "unicodeVersion": "6.0",
+ "digest": "a9ca1028812de7da08319eb0403b28951d381ace771b1050e90d1aa05e62baef"
+ },
+ "maple_leaf": {
+ "category": "nature",
+ "moji": "🍁",
+ "description": "maple leaf",
+ "unicodeVersion": "6.0",
+ "digest": "f16bc6dfd5bd33811f8cbf1bca6733715bb1e8b35109d7038f25497a89e79f8e"
+ },
+ "fallen_leaf": {
+ "category": "nature",
+ "moji": "🍂",
+ "description": "fallen leaf",
+ "unicodeVersion": "6.0",
+ "digest": "96c9e1139937dc4be942855f4b1f140cb06809c51a818d078f4680e0375ddc1c"
+ },
+ "leaves": {
+ "category": "nature",
+ "moji": "🍃",
+ "description": "leaf fluttering in wind",
+ "unicodeVersion": "6.0",
+ "digest": "814ac13b38d78820a050dd97155abfcd6c98fc56c5793044de7b7d2855e5acb9"
+ },
+ "mushroom": {
+ "category": "nature",
+ "moji": "🍄",
+ "description": "mushroom",
+ "unicodeVersion": "6.0",
+ "digest": "76123b383ae3515904cc8015b730a419c7dac5b16df83e8f0baf7b472e08adbe"
+ },
+ "grapes": {
"category": "food",
- "moji": "🧀",
- "description": "cheese wedge",
- "unicodeVersion": "8.0",
- "digest": "5d0152d394bb97558cdd52eb5c9c6f9924e04071e7aa76fa1abde43ae727ca78"
+ "moji": "🍇",
+ "description": "grapes",
+ "unicodeVersion": "6.0",
+ "digest": "6df4590ee90aa5dd376597fa8e500ad99b4dacae5ceaad8d0b5c2a4662e09e74"
+ },
+ "melon": {
+ "category": "food",
+ "moji": "🍈",
+ "description": "melon",
+ "unicodeVersion": "6.0",
+ "digest": "75e1353511e5b2c345ec411ca9b8dc7d172fcbc7fbe5366ef0ed96c74035ef32"
+ },
+ "watermelon": {
+ "category": "food",
+ "moji": "🍉",
+ "description": "watermelon",
+ "unicodeVersion": "6.0",
+ "digest": "8a18d12c6fb648b5cadc8d3ff390b9e24f5505a649055a2d4c840df7eb6bad78"
+ },
+ "tangerine": {
+ "category": "food",
+ "moji": "🍊",
+ "description": "tangerine",
+ "unicodeVersion": "6.0",
+ "digest": "6858dcb7aa079a6639511d86328f55fd70319787efe06934573c774adec2ade0"
+ },
+ "lemon": {
+ "category": "food",
+ "moji": "🍋",
+ "description": "lemon",
+ "unicodeVersion": "6.0",
+ "digest": "b6d67cb631ddc6d448f658fb151d325018ef01b791c005ed1d601f8223b7a722"
+ },
+ "banana": {
+ "category": "food",
+ "moji": "🍌",
+ "description": "banana",
+ "unicodeVersion": "6.0",
+ "digest": "9e11c486281f89713ed8853946434210f068e7cc5e26776f80125d7c1c303a84"
+ },
+ "pineapple": {
+ "category": "food",
+ "moji": "🍍",
+ "description": "pineapple",
+ "unicodeVersion": "6.0",
+ "digest": "62e7ffdfac3638f71d1381861a87d6f361718912099ddd4df1bc8245d6a1fc2f"
+ },
+ "apple": {
+ "category": "food",
+ "moji": "🍎",
+ "description": "red apple",
+ "unicodeVersion": "6.0",
+ "digest": "2a2d7a1fb558b2f77f9ef57b11229992a6ba4a1a679707f6339fc47744d64df2"
+ },
+ "green_apple": {
+ "category": "food",
+ "moji": "🍏",
+ "description": "green apple",
+ "unicodeVersion": "6.0",
+ "digest": "7f07991633eb29615cd7da645921adfdf01dfd83186d30f29c9ca02b6ed4384b"
+ },
+ "pear": {
+ "category": "food",
+ "moji": "🍐",
+ "description": "pear",
+ "unicodeVersion": "6.0",
+ "digest": "73114dab6e3ec572ae1f9a98e85961605b6a98cdd19497e6957eec60941959b5"
+ },
+ "peach": {
+ "category": "food",
+ "moji": "🍑",
+ "description": "peach",
+ "unicodeVersion": "6.0",
+ "digest": "952c053be4d68b53c35b0d43e83557606dc235afd821595236c8f80fb8602608"
},
"cherries": {
"category": "food",
@@ -1581,12 +5557,82 @@
"unicodeVersion": "6.0",
"digest": "49f2c738ad924302542d2c0b6d1f30076c77f9651234e1c6d57bc200540f0476"
},
- "cherry_blossom": {
- "category": "nature",
- "moji": "🌸",
- "description": "cherry blossom",
+ "strawberry": {
+ "category": "food",
+ "moji": "🍓",
+ "description": "strawberry",
"unicodeVersion": "6.0",
- "digest": "67e42777b2d2b8e71dc303c096b45f5b66733d2099533c6c6f798c98e314234d"
+ "digest": "4563a502fa27cbc543f6ad287a6c40eee76319e29a98fbf818c93e7b48c2249f"
+ },
+ "kiwi": {
+ "category": "food",
+ "moji": "🥝",
+ "description": "kiwifruit",
+ "unicodeVersion": "9.0",
+ "digest": "bc2ee501a2c313cee1816720975228ab34fda6b83581248012112af1ba5ce1ae"
+ },
+ "tomato": {
+ "category": "food",
+ "moji": "🍅",
+ "description": "tomato",
+ "unicodeVersion": "6.0",
+ "digest": "9fc42d1837bf67d7845f2a00c83c6b8001f96b6bb350f009063d211b20e96c16"
+ },
+ "avocado": {
+ "category": "food",
+ "moji": "🥑",
+ "description": "avocado",
+ "unicodeVersion": "9.0",
+ "digest": "465bdf47c670c469b14e17a61f6d60b5975aa8d2c1e8764c8e7305185f831d58"
+ },
+ "eggplant": {
+ "category": "food",
+ "moji": "🍆",
+ "description": "aubergine",
+ "unicodeVersion": "6.0",
+ "digest": "717719283180637a9929192d3af0459a871c144a5d6134cd1b31482cf065fc04"
+ },
+ "potato": {
+ "category": "food",
+ "moji": "🥔",
+ "description": "potato",
+ "unicodeVersion": "9.0",
+ "digest": "72e5b1d7dd118dd85886d880a88a92b60e1542587d92591dd42f2d59a721b4c4"
+ },
+ "carrot": {
+ "category": "food",
+ "moji": "🥕",
+ "description": "carrot",
+ "unicodeVersion": "9.0",
+ "digest": "8d8a49f70f7f339347d4a59afcb9a58d9113ebb4d7488d681e409cefd5cb9e47"
+ },
+ "corn": {
+ "category": "food",
+ "moji": "🌽",
+ "description": "ear of maize",
+ "unicodeVersion": "6.0",
+ "digest": "18c605b675251470f411303851450e3010e6a4683c06f1ad0df48eb2c8560a46"
+ },
+ "hot_pepper": {
+ "category": "food",
+ "moji": "🌶",
+ "description": "hot pepper",
+ "unicodeVersion": "7.0",
+ "digest": "78301b29d9426d81938771c3c25a95d70c9eecf78b39a3f49f30cb63f0cf6c50"
+ },
+ "cucumber": {
+ "category": "food",
+ "moji": "🥒",
+ "description": "cucumber",
+ "unicodeVersion": "9.0",
+ "digest": "752ddddf7b18831b74cc2b14b3fd787f9f914c1a9e72b7119f2e0b7267e4f749"
+ },
+ "peanuts": {
+ "category": "food",
+ "moji": "🥜",
+ "description": "peanuts",
+ "unicodeVersion": "9.0",
+ "digest": "f18fe7dfb1eb5e9db42fc3c37d4c4fb4866d9ad26ecf9f6cf5be798a09fc58eb"
},
"chestnut": {
"category": "nature",
@@ -1595,26 +5641,313 @@
"unicodeVersion": "6.0",
"digest": "0a6528154b5f4039d32a2e7e0550b1e2666bee8d55d4195402829d169fed350c"
},
- "chicken": {
- "category": "nature",
- "moji": "🐔",
- "description": "chicken",
+ "bread": {
+ "category": "food",
+ "moji": "🍞",
+ "description": "bread",
"unicodeVersion": "6.0",
- "digest": "6fb2f9392e8e78ec3e46187013e53a19dcb319e9528c27e453ae2dc470d91cf3"
+ "digest": "6108ad957056be1eeeb3c28e5fbd1b215becb07ad8669513466a26dfd481fe74"
},
- "children_crossing": {
- "category": "symbols",
- "moji": "🚸",
- "description": "children crossing",
+ "croissant": {
+ "category": "food",
+ "moji": "🥐",
+ "description": "croissant",
+ "unicodeVersion": "9.0",
+ "digest": "8524b964d39d5193569c03177162dc3bf2d6b2e1d4b5ba2b651cd906199f053f"
+ },
+ "french_bread": {
+ "category": "food",
+ "moji": "🥖",
+ "description": "baguette bread",
+ "unicodeVersion": "9.0",
+ "digest": "356771456e476bfc85b9ba08b8bd8473fef0e8045af88bafe563aed815cba8e9"
+ },
+ "pancakes": {
+ "category": "food",
+ "moji": "🥞",
+ "description": "pancakes",
+ "unicodeVersion": "9.0",
+ "digest": "5df3886ed4e65b22054269943718a69dcb75744e2dff9e414cf9ab62a2ea0596"
+ },
+ "cheese": {
+ "category": "food",
+ "moji": "🧀",
+ "description": "cheese wedge",
+ "unicodeVersion": "8.0",
+ "digest": "5d0152d394bb97558cdd52eb5c9c6f9924e04071e7aa76fa1abde43ae727ca78"
+ },
+ "meat_on_bone": {
+ "category": "food",
+ "moji": "🍖",
+ "description": "meat on bone",
"unicodeVersion": "6.0",
- "digest": "092e5fa179cce029edae31733f6c1dc0e147cde9050183b3f7b121cc7839c2ab"
+ "digest": "df7239ae70b5612f38b3bb309d685464cf529a75a6faa27084acbf71dd633a33"
},
- "chipmunk": {
+ "poultry_leg": {
+ "category": "food",
+ "moji": "🍗",
+ "description": "poultry leg",
+ "unicodeVersion": "6.0",
+ "digest": "c3c667f9795aa2d14fd1a500438d7fd2441e924a3d1af68bd6be1f082b935f4c"
+ },
+ "bacon": {
+ "category": "food",
+ "moji": "🥓",
+ "description": "bacon",
+ "unicodeVersion": "9.0",
+ "digest": "a076ea85c09e7783948f5710294c32d34487402779227bf47dc6f8d0fe7a9689"
+ },
+ "hamburger": {
+ "category": "food",
+ "moji": "🍔",
+ "description": "hamburger",
+ "unicodeVersion": "6.0",
+ "digest": "5d302067792941f85238d73dabc26e5565891ffd23514c93a26e5ac759d86198"
+ },
+ "fries": {
+ "category": "food",
+ "moji": "🍟",
+ "description": "french fries",
+ "unicodeVersion": "6.0",
+ "digest": "492b7d015ce5bcd3b9674049e5421cd2c65451c65a175f2e7ffca391b178d1e5"
+ },
+ "pizza": {
+ "category": "food",
+ "moji": "🍕",
+ "description": "slice of pizza",
+ "unicodeVersion": "6.0",
+ "digest": "74f0ca139931c470d814ed91f1bc7d395205b3d836fa2f425c50dff56d77ccb9"
+ },
+ "hotdog": {
+ "category": "food",
+ "moji": "🌭",
+ "description": "hot dog",
+ "unicodeVersion": "8.0",
+ "digest": "09bc0ee460411220b1e853893eb161106d173909d7754b35c36d16e99b54a30f"
+ },
+ "taco": {
+ "category": "food",
+ "moji": "🌮",
+ "description": "taco",
+ "unicodeVersion": "8.0",
+ "digest": "13802015749117fc3889d27eb8a66846bb81e2a469dfd90a66b9603b54526c5b"
+ },
+ "burrito": {
+ "category": "food",
+ "moji": "🌯",
+ "description": "burrito",
+ "unicodeVersion": "8.0",
+ "digest": "99ad1315f80dd9c1a9dab3a88e4c272d813b072a10f085c44ec4d261f7cb1d83"
+ },
+ "stuffed_flatbread": {
+ "category": "food",
+ "moji": "🥙",
+ "description": "stuffed flatbread",
+ "unicodeVersion": "9.0",
+ "digest": "ace05b5608aa3c51ad0aff66949ff5c6d06812597d1f55cb21a29834e531c46c"
+ },
+ "egg": {
+ "category": "food",
+ "moji": "🥚",
+ "description": "egg",
+ "unicodeVersion": "9.0",
+ "digest": "97287d7b86252a2e3b8a112aaa1a3096f48ee930691e200470e6b35876d9d3c3"
+ },
+ "cooking": {
+ "category": "food",
+ "moji": "🍳",
+ "description": "cooking",
+ "unicodeVersion": "6.0",
+ "digest": "b4bcd344fb394300464b8a81f325f8652c032f39ebe2acf85e046d594a58b84a"
+ },
+ "shallow_pan_of_food": {
+ "category": "food",
+ "moji": "🥘",
+ "description": "shallow pan of food",
+ "unicodeVersion": "9.0",
+ "digest": "1c90318cf2f78c965a0e4dfc710781e5da862874fa8e29f010231e5dc5657976"
+ },
+ "stew": {
+ "category": "food",
+ "moji": "🍲",
+ "description": "pot of food",
+ "unicodeVersion": "6.0",
+ "digest": "0c9cdd4de27a6108da2567070dad1ab9220dc513dd8ec6543ad66dddb7c09bdc"
+ },
+ "salad": {
+ "category": "food",
+ "moji": "🥗",
+ "description": "green salad",
+ "unicodeVersion": "9.0",
+ "digest": "2498d846c9ae599cd1fac3c74e87f595e1938df6b9a0eeb001c7f19ca0c7477d"
+ },
+ "popcorn": {
+ "category": "food",
+ "moji": "🍿",
+ "description": "popcorn",
+ "unicodeVersion": "8.0",
+ "digest": "6571fce7fb3cdf92db6fdb69b89b21b664eab348f7fd9ccb37da09eefa65d14f"
+ },
+ "bento": {
+ "category": "food",
+ "moji": "🍱",
+ "description": "bento box",
+ "unicodeVersion": "6.0",
+ "digest": "dee568150c21a99f0b6ce11af58ac7c8a298443fc3971e3efead7728b1bc7459"
+ },
+ "rice_cracker": {
+ "category": "food",
+ "moji": "🍘",
+ "description": "rice cracker",
+ "unicodeVersion": "6.0",
+ "digest": "33d54212f8418a5148b4b7e7b38edc72e7f03c5cf54356cdc15319c56160a3f3"
+ },
+ "rice_ball": {
+ "category": "food",
+ "moji": "🍙",
+ "description": "rice ball",
+ "unicodeVersion": "6.0",
+ "digest": "40e8af6fcac8dbc63e18809e12a00e41dc8cf75eba0c856f8c031c866e27099c"
+ },
+ "rice": {
+ "category": "food",
+ "moji": "🍚",
+ "description": "cooked rice",
+ "unicodeVersion": "6.0",
+ "digest": "ca2eb63d32044cf29435eec542dea6d2aad9eed4dcf4d12bb092a221c4e17eac"
+ },
+ "curry": {
+ "category": "food",
+ "moji": "🍛",
+ "description": "curry and rice",
+ "unicodeVersion": "6.0",
+ "digest": "6c317ba8a3dba9a294c2a3cb3b6385c946549a06bf934fd4b6964304f3b9a9b3"
+ },
+ "ramen": {
+ "category": "food",
+ "moji": "🍜",
+ "description": "steaming bowl",
+ "unicodeVersion": "6.0",
+ "digest": "2dab0bf5560aacda31e87d8a86a1a39eaaa7f6b4fb3dbc1b9768f3a49a9e20f9"
+ },
+ "spaghetti": {
+ "category": "food",
+ "moji": "🍝",
+ "description": "spaghetti",
+ "unicodeVersion": "6.0",
+ "digest": "b2de3171e90345dc777aa7554d97d6de659f4a9928b7da6a871e3925e234484c"
+ },
+ "sweet_potato": {
+ "category": "food",
+ "moji": "🍠",
+ "description": "roasted sweet potato",
+ "unicodeVersion": "6.0",
+ "digest": "0a322b21e76c9c487b8a8cb158c60d6e0be1aaa0495f865262f4c69e55a870b0"
+ },
+ "oden": {
+ "category": "food",
+ "moji": "🍢",
+ "description": "oden",
+ "unicodeVersion": "6.0",
+ "digest": "af6a824831041aacce2e5c510a3bc1a46ddc68f0083bcbfbbd30df941e50ec8b"
+ },
+ "sushi": {
+ "category": "food",
+ "moji": "🍣",
+ "description": "sushi",
+ "unicodeVersion": "6.0",
+ "digest": "a5bbbe7979621cc830f8a6860623af797530eb1d6cb4fb909f5e728ca8684864"
+ },
+ "fried_shrimp": {
+ "category": "food",
+ "moji": "🍤",
+ "description": "fried shrimp",
+ "unicodeVersion": "6.0",
+ "digest": "77985671090380d5e36b5c1bed476dcfe8e6e09d7a3f090aaae534be6297ecfd"
+ },
+ "fish_cake": {
+ "category": "food",
+ "moji": "🍥",
+ "description": "fish cake with swirl design",
+ "unicodeVersion": "6.0",
+ "digest": "2289ec22dc4e1cb1e143c56809f4d4f920e221f11237533cee95472a30985068"
+ },
+ "dango": {
+ "category": "food",
+ "moji": "🍡",
+ "description": "dango",
+ "unicodeVersion": "6.0",
+ "digest": "c3047205e462d958f6e5ab2ad318bc70e903343c4a87956055f76ea8adcc9456"
+ },
+ "crab": {
"category": "nature",
- "moji": "🐿",
- "description": "chipmunk",
- "unicodeVersion": "7.0",
- "digest": "ec1f696b11273364e8c7f386d38bbb133e5d7c387b9ea7ebc49b4fd5c84717c4"
+ "moji": "🦀",
+ "description": "crab",
+ "unicodeVersion": "8.0",
+ "digest": "8cdd21a6eaa56df3e503e8710e703b60ba6b059eaf2a9e93d8d25a4cbe2b55dc"
+ },
+ "shrimp": {
+ "category": "nature",
+ "moji": "🦐",
+ "description": "shrimp",
+ "unicodeVersion": "9.0",
+ "digest": "fd240e3208f6221cf6e7053645d40767898ea430733e0ebc5b81a8f834be2eb1"
+ },
+ "squid": {
+ "category": "nature",
+ "moji": "🦑",
+ "description": "squid",
+ "unicodeVersion": "9.0",
+ "digest": "f483430d758e7432b8696b5f95bf606b17cb017ea0beb5dc97dcf7fae7ed2455"
+ },
+ "icecream": {
+ "category": "food",
+ "moji": "🍦",
+ "description": "soft ice cream",
+ "unicodeVersion": "6.0",
+ "digest": "434307d583d20429b1646cbe1bd8317a53f445cae61e8d8ff1258d3996e6b9fe"
+ },
+ "shaved_ice": {
+ "category": "food",
+ "moji": "🍧",
+ "description": "shaved ice",
+ "unicodeVersion": "6.0",
+ "digest": "5f3f65f3974f30d1c9557d2c4af3052c7548f534b854f964647cafc2b4ab73a8"
+ },
+ "ice_cream": {
+ "category": "food",
+ "moji": "🍨",
+ "description": "ice cream",
+ "unicodeVersion": "6.0",
+ "digest": "3294b57aa673511686ad1c6a01f6e4c0ecb0eb7d5472c3015c3271b4f6e16d50"
+ },
+ "doughnut": {
+ "category": "food",
+ "moji": "🍩",
+ "description": "doughnut",
+ "unicodeVersion": "6.0",
+ "digest": "f249df4adc65187348d3a57d110c672dbf9e5a4d7919db473aab71f24f85e9b6"
+ },
+ "cookie": {
+ "category": "food",
+ "moji": "🍪",
+ "description": "cookie",
+ "unicodeVersion": "6.0",
+ "digest": "246ade6def10ac3e98af1ba9517990a4b41e391b9f1d8323a333d4cb69acbe36"
+ },
+ "birthday": {
+ "category": "food",
+ "moji": "🎂",
+ "description": "birthday cake",
+ "unicodeVersion": "6.0",
+ "digest": "f8087a3df94702f990da6a6dd1a0f143f679aa84ea0d178e030dd13e9d414bfa"
+ },
+ "cake": {
+ "category": "food",
+ "moji": "🍰",
+ "description": "shortcake",
+ "unicodeVersion": "6.0",
+ "digest": "807cc6dd0621c1c4dbd5380191d056ce44ccd2b92dda0cad6ca1c57e19e9f785"
},
"chocolate_bar": {
"category": "food",
@@ -1623,12 +5956,425 @@
"unicodeVersion": "6.0",
"digest": "89fbf46c19d87b7d86c400a54929fc7c3a0c4bd2a7c19f76f31e47486080b37f"
},
- "christmas_tree": {
+ "candy": {
+ "category": "food",
+ "moji": "🍬",
+ "description": "candy",
+ "unicodeVersion": "6.0",
+ "digest": "7064f2eb0dc768e3deb1e6ebe449950eb5a72ea882fb13bb06e23e7e2012c740"
+ },
+ "lollipop": {
+ "category": "food",
+ "moji": "🍭",
+ "description": "lollipop",
+ "unicodeVersion": "6.0",
+ "digest": "6499b3140dcc958c4bb99a80ac0424a209d7a978138feb5b919db36443eb529f"
+ },
+ "custard": {
+ "category": "food",
+ "moji": "🍮",
+ "description": "custard",
+ "unicodeVersion": "6.0",
+ "digest": "9790383c5841ec63d87182fc0f4dd05862c973b0e1b9f2fc2c047654089a404c"
+ },
+ "honey_pot": {
+ "category": "food",
+ "moji": "🍯",
+ "description": "honey pot",
+ "unicodeVersion": "6.0",
+ "digest": "bc66ee9940f2c1af04e066e78d4a61bc261f1dca4419d028b133ea9bc42e24d4"
+ },
+ "baby_bottle": {
+ "category": "food",
+ "moji": "🍼",
+ "description": "baby bottle",
+ "unicodeVersion": "6.0",
+ "digest": "2bd1cb4a294c83eb07b6d12e2abf8ab42a5083941096dd11ee772cdb1b1e3091"
+ },
+ "milk": {
+ "category": "food",
+ "moji": "🥛",
+ "description": "glass of milk",
+ "unicodeVersion": "9.0",
+ "digest": "3f9229a2c754345be8e721dde032f289fa92c23a11cfd1afe6b38b44d34e435d"
+ },
+ "coffee": {
+ "category": "food",
+ "moji": "☕",
+ "description": "hot beverage",
+ "unicodeVersion": "4.0",
+ "digest": "bd09e4d36ebe4df49c695147a2d1ae91b6cbdd7eb7bb5066a1bada8ca2fa7c6f"
+ },
+ "tea": {
+ "category": "food",
+ "moji": "🍵",
+ "description": "teacup without handle",
+ "unicodeVersion": "6.0",
+ "digest": "520da660803cd133832badfa170c6795e2673b6881b6b52e22db3771c430cafa"
+ },
+ "sake": {
+ "category": "food",
+ "moji": "🍶",
+ "description": "sake bottle and cup",
+ "unicodeVersion": "6.0",
+ "digest": "bd09899bee1411e26464b5f0d86d69a3f57cfd6e3f557f0f87fcb61bd2fc51ba"
+ },
+ "champagne": {
+ "category": "food",
+ "moji": "🍾",
+ "description": "bottle with popping cork",
+ "unicodeVersion": "8.0",
+ "digest": "6e686856c9e39b9947ef669394e6621fffd2a91ebe8f21c2cdc06b06669d1160"
+ },
+ "wine_glass": {
+ "category": "food",
+ "moji": "🍷",
+ "description": "wine glass",
+ "unicodeVersion": "6.0",
+ "digest": "d7551c39ea933566603e8ccd18c9c116e8d4dd2dca14697f5d6f6c0dc481e05c"
+ },
+ "cocktail": {
+ "category": "food",
+ "moji": "🍸",
+ "description": "cocktail glass",
+ "unicodeVersion": "6.0",
+ "digest": "91b4e4dc85b87b50d85d4831a18de7dd952c45c427f46a1a2c55d0abf4c4f5dd"
+ },
+ "tropical_drink": {
+ "category": "food",
+ "moji": "🍹",
+ "description": "tropical drink",
+ "unicodeVersion": "6.0",
+ "digest": "085a51de7c6df9e8734450b0b7b30577c3afaed56cf02f6a481808c2d90a0c13"
+ },
+ "beer": {
+ "category": "food",
+ "moji": "🍺",
+ "description": "beer mug",
+ "unicodeVersion": "6.0",
+ "digest": "c7e45519f39d1cfb1c97d05bca80ee8881dca7aced770d3dac5f282f5bb34773"
+ },
+ "beers": {
+ "category": "food",
+ "moji": "🍻",
+ "description": "clinking beer mugs",
+ "unicodeVersion": "6.0",
+ "digest": "8f11a1397605165bfd346619faa5fcddfd1f6b4f9fc2ddc4756cd2518ed92062"
+ },
+ "champagne_glass": {
+ "category": "food",
+ "moji": "🥂",
+ "description": "clinking glasses",
+ "unicodeVersion": "9.0",
+ "digest": "8e59ae799d7c7844dc19f5fd4fbefebc34f597059373c94e8639a7ce73d13519"
+ },
+ "tumbler_glass": {
+ "category": "food",
+ "moji": "🥃",
+ "description": "tumbler glass",
+ "unicodeVersion": "9.0",
+ "digest": "41ca844a7ae31e28bac5320298ff16c2adf9124b9a649ce5112e47ddce42ab06"
+ },
+ "fork_knife_plate": {
+ "category": "food",
+ "moji": "🍽",
+ "description": "fork and knife with plate",
+ "unicodeVersion": "7.0",
+ "digest": "3161044fa7ac67ef1f09c35ecde7ca8eca471a36e3e67ccbaa3a42f35dd67f16"
+ },
+ "fork_and_knife": {
+ "category": "food",
+ "moji": "🍴",
+ "description": "fork and knife",
+ "unicodeVersion": "6.0",
+ "digest": "a22c9fabbcae702d4043e4bfab3f15cd96e585a291f93f820f01723c0465410c"
+ },
+ "spoon": {
+ "category": "food",
+ "moji": "🥄",
+ "description": "spoon",
+ "unicodeVersion": "9.0",
+ "digest": "c5fbd1bd2ca6ca2cf13a6b7bfaeba67305d21b2d86bba97e7d283ca1e9b1aacf"
+ },
+ "knife": {
+ "category": "objects",
+ "moji": "🔪",
+ "description": "hocho",
+ "unicodeVersion": "6.0",
+ "digest": "bf3934138e3dd112241807efbe2aa91ec9913fad890236276d4c36c710c400b8"
+ },
+ "amphora": {
+ "category": "objects",
+ "moji": "🏺",
+ "description": "amphora",
+ "unicodeVersion": "8.0",
+ "digest": "ce9f7d0bd6b4d04c033eb2b3b8c2d339e4a8d19ef46cb23e500cd2854578d4e5"
+ },
+ "earth_africa": {
"category": "nature",
- "moji": "🎄",
- "description": "christmas tree",
+ "moji": "🌍",
+ "description": "earth globe europe-africa",
"unicodeVersion": "6.0",
- "digest": "cb28845d8770cc86de0034f03e38e46b251fb5bd4357a923c107404e8c5116bb"
+ "digest": "597e652a0be2d59d9f0190423767a763500c724ac53321c1cb6fc7fc0f9b9e8c"
+ },
+ "earth_americas": {
+ "category": "nature",
+ "moji": "🌎",
+ "description": "earth globe americas",
+ "unicodeVersion": "6.0",
+ "digest": "b65c2ce55702e00286792a5007609bafdf1f184e2da5a8c6b949054534d5e8ad"
+ },
+ "earth_asia": {
+ "category": "nature",
+ "moji": "🌏",
+ "description": "earth globe asia-australia",
+ "unicodeVersion": "6.0",
+ "digest": "0334bd2fcfd55b0759c7bf900c7ef9a70fb3408ecb7762038e6b587d8226d204"
+ },
+ "globe_with_meridians": {
+ "category": "symbols",
+ "moji": "🌐",
+ "description": "globe with meridians",
+ "unicodeVersion": "6.0",
+ "digest": "e85db2d778b99bf7557d48fed2ff9ed1db94a615124b47d85ed5a3eeb5c3b554"
+ },
+ "map": {
+ "category": "objects",
+ "moji": "🗺",
+ "description": "world map",
+ "unicodeVersion": "7.0",
+ "digest": "8eb87e7238c5dca1b1b8efb181e42f0d91ca515d3e12dac67aafbe1028338d8e"
+ },
+ "japan": {
+ "category": "travel",
+ "moji": "🗾",
+ "description": "silhouette of japan",
+ "unicodeVersion": "6.0",
+ "digest": "a27fd17e252497aa0220540d778427dc5fba138b6adeac10b132feae8175d554"
+ },
+ "mountain_snow": {
+ "category": "travel",
+ "moji": "🏔",
+ "description": "snow capped mountain",
+ "unicodeVersion": "7.0",
+ "digest": "9b7e802436798fd7eb042bbe31cdb77c124196cd262dca3e671e5ad278fe742f"
+ },
+ "mountain": {
+ "category": "travel",
+ "moji": "⛰",
+ "description": "mountain",
+ "unicodeVersion": "5.2",
+ "digest": "5e0087bb9324c99b5b4f730b8878cadab69cd3af0f580df6c31490a7b7b0abe4"
+ },
+ "volcano": {
+ "category": "travel",
+ "moji": "🌋",
+ "description": "volcano",
+ "unicodeVersion": "6.0",
+ "digest": "75b53e25406d31b630fd1bb75c363044129f481f24f7d225a6d30eef5257f92c"
+ },
+ "mount_fuji": {
+ "category": "travel",
+ "moji": "🗻",
+ "description": "mount fuji",
+ "unicodeVersion": "6.0",
+ "digest": "dd27d0550d5df7dd376a43e56e0819a41c48f21cc89e58f36d290140c4effd5e"
+ },
+ "camping": {
+ "category": "travel",
+ "moji": "🏕",
+ "description": "camping",
+ "unicodeVersion": "7.0",
+ "digest": "70c596c5d9ab030e26b46c95e011a28050aa6082f4ba2b8b197f24915ce03c86"
+ },
+ "beach": {
+ "category": "travel",
+ "moji": "🏖",
+ "description": "beach with umbrella",
+ "unicodeVersion": "7.0",
+ "digest": "98bb54c59c818b30ae9024d85156424a345f2912ca2b37b0ef18f2ef60b96619"
+ },
+ "desert": {
+ "category": "travel",
+ "moji": "🏜",
+ "description": "desert",
+ "unicodeVersion": "7.0",
+ "digest": "c60bd12a5864c0789c2e8fb93a392a42600e08cc23450c716c0bd008c16e1c93"
+ },
+ "island": {
+ "category": "travel",
+ "moji": "🏝",
+ "description": "desert island",
+ "unicodeVersion": "7.0",
+ "digest": "9cd601f946f247ad80810f256b5c4f0c71658e9e52b974c3d8d36173eedcd05e"
+ },
+ "park": {
+ "category": "travel",
+ "moji": "🏞",
+ "description": "national park",
+ "unicodeVersion": "7.0",
+ "digest": "b49486510df4e66e1beb21abdf1eb7745dfb4381a3f76b3f2ffcb2966bf5f5c4"
+ },
+ "stadium": {
+ "category": "travel",
+ "moji": "🏟",
+ "description": "stadium",
+ "unicodeVersion": "7.0",
+ "digest": "807c4f6f19b9819ca3c846126ccfa2d24a2b00b66cb946e8be75536032426815"
+ },
+ "classical_building": {
+ "category": "travel",
+ "moji": "🏛",
+ "description": "classical building",
+ "unicodeVersion": "7.0",
+ "digest": "73e1b32e337042475e76fdbdb03c0b04588f626018bd81adce7f3183be907a92"
+ },
+ "construction_site": {
+ "category": "travel",
+ "moji": "🏗",
+ "description": "building construction",
+ "unicodeVersion": "7.0",
+ "digest": "5973b6f1b1c75fd083d975c2c11f68fff6583f10056418c699da1cd3049a5f89"
+ },
+ "homes": {
+ "category": "travel",
+ "moji": "🏘",
+ "description": "house buildings",
+ "unicodeVersion": "7.0",
+ "digest": "2ffdafabb89623b096e12f9a81936e699c436d0e948af6c1dd8786690b1a601b"
+ },
+ "house_abandoned": {
+ "category": "travel",
+ "moji": "🏚",
+ "description": "derelict house building",
+ "unicodeVersion": "7.0",
+ "digest": "0931ce0e42ca7206910a62452e1b7975aed4692d362c81bf62cafef25f2bcefa"
+ },
+ "house": {
+ "category": "travel",
+ "moji": "🏠",
+ "description": "house building",
+ "unicodeVersion": "6.0",
+ "digest": "6be52f020a7e12a3298044d270d9322742e930cc7543e2d6dfb9c26f25b529ec"
+ },
+ "house_with_garden": {
+ "category": "travel",
+ "moji": "🏡",
+ "description": "house with garden",
+ "unicodeVersion": "6.0",
+ "digest": "b62e44e69e1a04f88280518fd48c4870035f0abb5ae6a6664daa7cf005c8f8de"
+ },
+ "office": {
+ "category": "travel",
+ "moji": "🏢",
+ "description": "office building",
+ "unicodeVersion": "6.0",
+ "digest": "404b17445c4da0dc0ec8561604d741bb1c4f1aad8d7d49299249c9732fb33d40"
+ },
+ "post_office": {
+ "category": "travel",
+ "moji": "🏣",
+ "description": "japanese post office",
+ "unicodeVersion": "6.0",
+ "digest": "7dc4ef6e09bab68f31dfb7f594a6488b62c9dcb956b36ac2788c1fbefa908251"
+ },
+ "european_post_office": {
+ "category": "travel",
+ "moji": "🏤",
+ "description": "european post office",
+ "unicodeVersion": "6.0",
+ "digest": "664ed0206445f1511bc2ecdfaf77c3df14dfd4e98fd73b58c6272e54e4117810"
+ },
+ "hospital": {
+ "category": "travel",
+ "moji": "🏥",
+ "description": "hospital",
+ "unicodeVersion": "6.0",
+ "digest": "84895d822ef2e2a77b3703b80c5d5b29574f655b88420ef6f39ab3d93bb123df"
+ },
+ "bank": {
+ "category": "travel",
+ "moji": "🏦",
+ "description": "bank",
+ "unicodeVersion": "6.0",
+ "digest": "ec529c75f3ccf2110adbe912d06269357d84f831ba737eab90aa408d8097bb35"
+ },
+ "hotel": {
+ "category": "travel",
+ "moji": "🏨",
+ "description": "hotel",
+ "unicodeVersion": "6.0",
+ "digest": "7f04757da4f1a537cb92ac9a0537e3a7ebd90ae9e2c3e73ad79c95623b3126ef"
+ },
+ "love_hotel": {
+ "category": "travel",
+ "moji": "🏩",
+ "description": "love hotel",
+ "unicodeVersion": "6.0",
+ "digest": "05e846b25799923b3a85826f40d2d22f9e2ea35855620fdde4d1baec25cf5f25"
+ },
+ "convenience_store": {
+ "category": "travel",
+ "moji": "🏪",
+ "description": "convenience store",
+ "unicodeVersion": "6.0",
+ "digest": "07d3989157223a8249db3532a642050529831d47f05c0733fbe2cb6a6a780ea6"
+ },
+ "school": {
+ "category": "travel",
+ "moji": "🏫",
+ "description": "school",
+ "unicodeVersion": "6.0",
+ "digest": "d6cf41776c0b3c6e3a20d672b85ef91a65085dc7ee40aa4b0c94b0683f3ab2b4"
+ },
+ "department_store": {
+ "category": "travel",
+ "moji": "🏬",
+ "description": "department store",
+ "unicodeVersion": "6.0",
+ "digest": "0cbab31777aeecc148e54777e1a65e794eb4b5f45e0f8ecaffa990efd06a0fb2"
+ },
+ "factory": {
+ "category": "travel",
+ "moji": "🏭",
+ "description": "factory",
+ "unicodeVersion": "6.0",
+ "digest": "e71fc6d2fb903490f28565cbea6c3ca7384adc24aa0ba3ae481ca12255a015d3"
+ },
+ "japanese_castle": {
+ "category": "travel",
+ "moji": "🏯",
+ "description": "japanese castle",
+ "unicodeVersion": "6.0",
+ "digest": "4beaf1e9f6d7e25d3faa5076896f337fce68e75d25acdd91ceaceb9c15faa5eb"
+ },
+ "european_castle": {
+ "category": "travel",
+ "moji": "🏰",
+ "description": "european castle",
+ "unicodeVersion": "6.0",
+ "digest": "183e813391199158b855481acefea11061b1722e51f2d2880486782b7639598d"
+ },
+ "wedding": {
+ "category": "travel",
+ "moji": "💒",
+ "description": "wedding",
+ "unicodeVersion": "6.0",
+ "digest": "bdc97890b2ccd38285d4d9832e700c0cb99cf740305b37eac5cee1bf03137a92"
+ },
+ "tokyo_tower": {
+ "category": "travel",
+ "moji": "🗼",
+ "description": "tokyo tower",
+ "unicodeVersion": "6.0",
+ "digest": "e012cdbb4648219ccee443cc09fa2946b99dadb483aeca5a3fdee39270dd3061"
+ },
+ "statue_of_liberty": {
+ "category": "travel",
+ "moji": "🗽",
+ "description": "statue of liberty",
+ "unicodeVersion": "6.0",
+ "digest": "9779b56242c4eb3de2060b060ae39de21ad1082b1135ec9c29f9d1aca6bf13cb"
},
"church": {
"category": "travel",
@@ -1637,19 +6383,82 @@
"unicodeVersion": "5.2",
"digest": "5920057525f6b395d3aead6f44f5a1cbf76662fac740f993694afe8f482c0864"
},
- "cinema": {
- "category": "symbols",
- "moji": "🎦",
- "description": "cinema",
+ "mosque": {
+ "category": "travel",
+ "moji": "🕌",
+ "description": "mosque",
+ "unicodeVersion": "8.0",
+ "digest": "f15b137d0d23275694d6743602054d05334c9dfb15964378f38defddc8a25b2a"
+ },
+ "synagogue": {
+ "category": "travel",
+ "moji": "🕍",
+ "description": "synagogue",
+ "unicodeVersion": "8.0",
+ "digest": "07bcf08d94008462f001e6512e3ba3e9e70cfd57ac4e84e04b94e6f74684f51c"
+ },
+ "shinto_shrine": {
+ "category": "travel",
+ "moji": "⛩",
+ "description": "shinto shrine",
+ "unicodeVersion": "5.2",
+ "digest": "e3027766f283d86acb7f0730dd04cbda16bb6fa16c90edb40dad7f09f1bca3fe"
+ },
+ "kaaba": {
+ "category": "travel",
+ "moji": "🕋",
+ "description": "kaaba",
+ "unicodeVersion": "8.0",
+ "digest": "363ef4a89268542427e494f0dbe04f04eabad2849fa24be57a77eb55a3d45122"
+ },
+ "fountain": {
+ "category": "travel",
+ "moji": "⛲",
+ "description": "fountain",
+ "unicodeVersion": "5.2",
+ "digest": "f940be34d7db82c7dcf5e4343c05f74e5834f1123edd0382b9aa0c3b8b72e90d"
+ },
+ "tent": {
+ "category": "travel",
+ "moji": "⛺",
+ "description": "tent",
+ "unicodeVersion": "5.2",
+ "digest": "a2fc1e803dc0eef13ea7d3847e1e92a3d0790b1336a7ed5675ea60069fb76676"
+ },
+ "foggy": {
+ "category": "travel",
+ "moji": "🌁",
+ "description": "foggy",
"unicodeVersion": "6.0",
- "digest": "acfdea199d4ab48103e5491dd3ab0a616ffbef57641a07aef315273559ccf690"
+ "digest": "0ccc53edb0aba767c59682d74a29221f829297e334120d3b90e3030054698ec7"
},
- "circus_tent": {
- "category": "activity",
- "moji": "🎪",
- "description": "circus tent",
+ "night_with_stars": {
+ "category": "travel",
+ "moji": "🌃",
+ "description": "night with stars",
"unicodeVersion": "6.0",
- "digest": "5bc6aae497746b5af5cbc03d703df1754903cc71c25a2c7fc8a068292f5614fc"
+ "digest": "0ca3f52bf56337784d36c545285fce5777bfe6fb01631c3491e33fec61e12474"
+ },
+ "cityscape": {
+ "category": "travel",
+ "moji": "🏙",
+ "description": "cityscape",
+ "unicodeVersion": "7.0",
+ "digest": "1ae1932db38c72652e85dfe56dd173a37ed96b6913ec74f1718d19fd6ea948ac"
+ },
+ "sunrise_over_mountains": {
+ "category": "travel",
+ "moji": "🌄",
+ "description": "sunrise over mountains",
+ "unicodeVersion": "6.0",
+ "digest": "089412d5a9ce8f71fa184bc90cd9a092bfea41361a792cc6b9f94ac1bc741fb7"
+ },
+ "sunrise": {
+ "category": "travel",
+ "moji": "🌅",
+ "description": "sunrise",
+ "unicodeVersion": "6.0",
+ "digest": "5e1511462f5e0bfaaf865baa62eb7dd1f76ce22acebb0bd5ef27c55e3a69fed3"
},
"city_dusk": {
"category": "travel",
@@ -1665,124 +6474,544 @@
"unicodeVersion": "6.0",
"digest": "7da54c98e269c041a20dc94861338b546af78ffaa577249e14f1258e232ca5dc"
},
- "cityscape": {
+ "bridge_at_night": {
"category": "travel",
- "moji": "🏙",
- "description": "cityscape",
- "unicodeVersion": "7.0",
- "digest": "1ae1932db38c72652e85dfe56dd173a37ed96b6913ec74f1718d19fd6ea948ac"
+ "moji": "🌉",
+ "description": "bridge at night",
+ "unicodeVersion": "6.0",
+ "digest": "212feaa05b96bff46d9699d3e9f1d61f6fc0e228414534b1bf8f1383c591784c"
},
- "cl": {
+ "hotsprings": {
"category": "symbols",
- "moji": "🆑",
- "description": "squared cl",
+ "moji": "♨",
+ "description": "hot springs",
+ "unicodeVersion": "1.1",
+ "digest": "755da4733bacad870d226b5ec054c803368c4ab10ae577e193a3015ee25a8271"
+ },
+ "carousel_horse": {
+ "category": "travel",
+ "moji": "🎠",
+ "description": "carousel horse",
"unicodeVersion": "6.0",
- "digest": "071117bab8c92f8ea4b734b358d3591ef4b4df067514b80f747b6293efc77ec7"
+ "digest": "d8a609d98fab29e04443f12457a8c8c7fe321b3b940289f1c9fe55f711583b20"
},
- "clap": {
- "category": "people",
- "moji": "👏",
- "description": "clapping hands sign",
+ "ferris_wheel": {
+ "category": "travel",
+ "moji": "🎡",
+ "description": "ferris wheel",
"unicodeVersion": "6.0",
- "digest": "68fca08d9340cd18b061a9ceef10111b006df9f888b534dd9b307c490cdb045c"
+ "digest": "a2eabc2a9f677373e013a100fc0f3b1e55d9aab24fc77056ee2bfef7dfc19add"
},
- "clap_tone1": {
- "category": "people",
- "moji": "👏🏻",
- "description": "clapping hands sign tone 1",
- "unicodeVersion": "8.0",
- "digest": "dc842eb100206a92eab5cefadeb5dab004ee34d054b0adc02de27f525a0a724c"
+ "roller_coaster": {
+ "category": "travel",
+ "moji": "🎢",
+ "description": "roller coaster",
+ "unicodeVersion": "6.0",
+ "digest": "5c4c5d8639d4bbeab07bc46304e5233a95f379fad92e395d087afe5af00529fc"
},
- "clap_tone2": {
- "category": "people",
- "moji": "👏🏼",
- "description": "clapping hands sign tone 2",
- "unicodeVersion": "8.0",
- "digest": "6a92a954a6ea47ce0e42dceee82cd4220f710582f7589cb86037555f0a5e9dce"
+ "barber": {
+ "category": "objects",
+ "moji": "💈",
+ "description": "barber pole",
+ "unicodeVersion": "6.0",
+ "digest": "9ef01a8d9556863264c72b59b404f302f870b676a836ba3beb8892f745b40c80"
},
- "clap_tone3": {
- "category": "people",
- "moji": "👏🏽",
- "description": "clapping hands sign tone 3",
- "unicodeVersion": "8.0",
- "digest": "6b7dfeae569488df0bd85c16233abcf770bc2bb5942c178cbe5107e798a9ca88"
+ "circus_tent": {
+ "category": "activity",
+ "moji": "🎪",
+ "description": "circus tent",
+ "unicodeVersion": "6.0",
+ "digest": "5bc6aae497746b5af5cbc03d703df1754903cc71c25a2c7fc8a068292f5614fc"
},
- "clap_tone4": {
- "category": "people",
- "moji": "👏🏾",
- "description": "clapping hands sign tone 4",
- "unicodeVersion": "8.0",
- "digest": "2fa8961ad763675c6619b5f4137e912ce4126fb9b5df71d23c59b05e54533afa"
+ "steam_locomotive": {
+ "category": "travel",
+ "moji": "🚂",
+ "description": "steam locomotive",
+ "unicodeVersion": "6.0",
+ "digest": "479aa4a2c2704d79f642036eef9c8ddf00f8df17b9389a87d7d5f688827c7f56"
},
- "clap_tone5": {
- "category": "people",
- "moji": "👏🏿",
- "description": "clapping hands sign tone 5",
- "unicodeVersion": "8.0",
- "digest": "7b8b5e0befd920ac4e02ef1823b63c2513dbe58fe6471dc54b8265b75b4c8f41"
+ "railway_car": {
+ "category": "travel",
+ "moji": "🚃",
+ "description": "railway car",
+ "unicodeVersion": "6.0",
+ "digest": "5aaa2731f58c2a08c2c731cf361da3a9ea407d2754134dcacf14d9ea4cfb702f"
},
- "clapper": {
- "category": "activity",
- "moji": "🎬",
- "description": "clapper board",
+ "bullettrain_side": {
+ "category": "travel",
+ "moji": "🚄",
+ "description": "high-speed train",
"unicodeVersion": "6.0",
- "digest": "73820f057b2d7d50f6be4a51c0ebaa39a4e0463f0f1121471fb3816fdd05f5bd"
+ "digest": "401e85f01801e39f8325a96bdee7e27626fc4124d92eeb3e39227d78ef7d26c0"
},
- "classical_building": {
+ "bullettrain_front": {
"category": "travel",
- "moji": "🏛",
- "description": "classical building",
+ "moji": "🚅",
+ "description": "high-speed train with bullet nose",
+ "unicodeVersion": "6.0",
+ "digest": "b8cd48a543a5ed0cf750cfa8bcfa536e26b72a77e4b7f506041b623e9081fb69"
+ },
+ "train2": {
+ "category": "travel",
+ "moji": "🚆",
+ "description": "train",
+ "unicodeVersion": "6.0",
+ "digest": "4809681fd588ad64f82483ecb825bb16ff387e31f45e5fc5637f0c2547acd42f"
+ },
+ "metro": {
+ "category": "travel",
+ "moji": "🚇",
+ "description": "metro",
+ "unicodeVersion": "6.0",
+ "digest": "d5053b49a13908a38615c71096d9377d6e8a091f169171eb32d0ab1f209a3424"
+ },
+ "light_rail": {
+ "category": "travel",
+ "moji": "🚈",
+ "description": "light rail",
+ "unicodeVersion": "6.0",
+ "digest": "f7ee16c0a5853c542570a684e25e3fc388e5f52ef8fe7ffaa1120ad7de74a3bd"
+ },
+ "station": {
+ "category": "travel",
+ "moji": "🚉",
+ "description": "station",
+ "unicodeVersion": "6.0",
+ "digest": "a4e784b6c4238269932befca55cd7b41af85fc899f023a49c7f1ceaf81652496"
+ },
+ "tram": {
+ "category": "travel",
+ "moji": "🚊",
+ "description": "tram",
+ "unicodeVersion": "6.0",
+ "digest": "ab9d03c841a0ef60218aae50fe2b2e0ee045052b8b8f73c001c4203199209ba0"
+ },
+ "monorail": {
+ "category": "travel",
+ "moji": "🚝",
+ "description": "monorail",
+ "unicodeVersion": "6.0",
+ "digest": "551b6d88c465c54bd2fc03eb396a6afbc68e24aa41c07203c84fd36e31608836"
+ },
+ "mountain_railway": {
+ "category": "travel",
+ "moji": "🚞",
+ "description": "mountain railway",
+ "unicodeVersion": "6.0",
+ "digest": "3ce97b097df39e9e46eb09d1d544b1bd45596a232fd9163b8f02c37ecaffe794"
+ },
+ "train": {
+ "category": "travel",
+ "moji": "🚋",
+ "description": "Tram Car",
+ "unicodeVersion": "6.0",
+ "digest": "2b991f1b04ea9a364de103d751d4e3ca7d263b29e169517cf0b6bc098c0544fd"
+ },
+ "bus": {
+ "category": "travel",
+ "moji": "🚌",
+ "description": "bus",
+ "unicodeVersion": "6.0",
+ "digest": "70534ef30162b8af4b548c3fc0f4d7964e01c8bcb2b52c3b2dc2df4854aa308c"
+ },
+ "oncoming_bus": {
+ "category": "travel",
+ "moji": "🚍",
+ "description": "oncoming bus",
+ "unicodeVersion": "6.0",
+ "digest": "7fba3ec5ef62299568f19384826914c3dd1b4b7d2f27ee575a451e8a764f60a0"
+ },
+ "trolleybus": {
+ "category": "travel",
+ "moji": "🚎",
+ "description": "trolleybus",
+ "unicodeVersion": "6.0",
+ "digest": "587261051e9e8a6c1354406c62830d8895eaa83b1d9dc6df7516a835788805ec"
+ },
+ "minibus": {
+ "category": "travel",
+ "moji": "🚐",
+ "description": "minibus",
+ "unicodeVersion": "6.0",
+ "digest": "7e71052ea22df57fa12c7e3829924ebd3f144b9d71301c34755c10d33ddee697"
+ },
+ "ambulance": {
+ "category": "travel",
+ "moji": "🚑",
+ "description": "ambulance",
+ "unicodeVersion": "6.0",
+ "digest": "1ca176a46c2f020e0386e7a0ab662711f66af1683ae165c06853ec5b6bcc4cf1"
+ },
+ "fire_engine": {
+ "category": "travel",
+ "moji": "🚒",
+ "description": "fire engine",
+ "unicodeVersion": "6.0",
+ "digest": "c81e1df702689c4c3895862e2da1092f30f5c415c0e9726f0b2c5c42ed9660ce"
+ },
+ "police_car": {
+ "category": "travel",
+ "moji": "🚓",
+ "description": "police car",
+ "unicodeVersion": "6.0",
+ "digest": "38e3b0bdee599b71a5f556dc86b12aa01df38dcf533179515bbdbf2512828548"
+ },
+ "oncoming_police_car": {
+ "category": "travel",
+ "moji": "🚔",
+ "description": "oncoming police car",
+ "unicodeVersion": "6.0",
+ "digest": "2c13ed1f79d19a50de28061b822b76242ec2ee979a838aa85833a3dc361a3523"
+ },
+ "taxi": {
+ "category": "travel",
+ "moji": "🚕",
+ "description": "taxi",
+ "unicodeVersion": "6.0",
+ "digest": "260b37ae31fcfe5b30682f0592ec4e32ce308f9cb9574daa532099c47735252b"
+ },
+ "oncoming_taxi": {
+ "category": "travel",
+ "moji": "🚖",
+ "description": "oncoming taxi",
+ "unicodeVersion": "6.0",
+ "digest": "4cf0f88b9520d40772b743dcf2e618c49c5f74fa57fd46cda4add3bee9e18ef3"
+ },
+ "red_car": {
+ "category": "travel",
+ "moji": "🚗",
+ "description": "automobile",
+ "unicodeVersion": "6.0",
+ "digest": "db3f7e0dd1e1aa92541afc4db067e0c6bf9d325d912b1bafce182e683f68a06e"
+ },
+ "oncoming_automobile": {
+ "category": "travel",
+ "moji": "🚘",
+ "description": "oncoming automobile",
+ "unicodeVersion": "6.0",
+ "digest": "a51563ac9f0674e9d398a0da88c94865583f7478b222717e9187064707ec792b"
+ },
+ "blue_car": {
+ "category": "travel",
+ "moji": "🚙",
+ "description": "recreational vehicle",
+ "unicodeVersion": "6.0",
+ "digest": "c0faa7f7d4344391478aba15cb4cbfbbe1eb5fd5cdcdb9afdb7d49984abb8108"
+ },
+ "truck": {
+ "category": "travel",
+ "moji": "🚚",
+ "description": "delivery truck",
+ "unicodeVersion": "6.0",
+ "digest": "c58cdc9790438031e3acd58bd224b83428a6e89be5cec3af01233d257d7b54a6"
+ },
+ "articulated_lorry": {
+ "category": "travel",
+ "moji": "🚛",
+ "description": "articulated lorry",
+ "unicodeVersion": "6.0",
+ "digest": "35d63d2bb71429a9da433fa2464827f53996a738e22efd275b2326a6cc30063c"
+ },
+ "tractor": {
+ "category": "travel",
+ "moji": "🚜",
+ "description": "tractor",
+ "unicodeVersion": "6.0",
+ "digest": "0bec7945ef52027a634bfbe75b71902e31c03a8d91814f3c37c2d1dbfa21de10"
+ },
+ "race_car": {
+ "category": "travel",
+ "moji": "🏎",
+ "description": "racing car",
"unicodeVersion": "7.0",
- "digest": "73e1b32e337042475e76fdbdb03c0b04588f626018bd81adce7f3183be907a92"
+ "digest": "183bf5e7abb9e7c7e94a08f212040d6dbe24633c2fb25a452096a2062c18acc6"
},
- "clipboard": {
- "category": "objects",
- "moji": "📋",
- "description": "clipboard",
+ "motorcycle": {
+ "category": "travel",
+ "moji": "🏍",
+ "description": "racing motorcycle",
+ "unicodeVersion": "7.0",
+ "digest": "b30c2e98f3a439b0c0b6481a5025ee3d380c35a7aa43d7b46c3e948fe732613c"
+ },
+ "motor_scooter": {
+ "category": "travel",
+ "moji": "🛵",
+ "description": "motor scooter",
+ "unicodeVersion": "9.0",
+ "digest": "cb76bb0e2f94960426a85f273e2a74046eefebd0b6a265bafecfb8850f0a0c6c"
+ },
+ "bike": {
+ "category": "travel",
+ "moji": "🚲",
+ "description": "bicycle",
"unicodeVersion": "6.0",
- "digest": "d3d42f1ff9c8dacfd8219d9563e3fab709290d887695d3bf0a0e51afac7b7149"
+ "digest": "65d94129ac0445495a0cf55f529f416682fa7a1dc7a16b275e3c267a132777ee"
},
- "clock": {
+ "scooter": {
+ "category": "travel",
+ "moji": "🛴",
+ "description": "scooter",
+ "unicodeVersion": "9.0",
+ "digest": "baa3060602995716fba048ed2be1559712abc6edc3d8822b4ae6c0fe185197e4"
+ },
+ "busstop": {
+ "category": "travel",
+ "moji": "🚏",
+ "description": "bus stop",
+ "unicodeVersion": "6.0",
+ "digest": "ccb2c93fe9f2a03c0674d921bb3a6a6c80e2eef64a274bd31b1efb3147785e6f"
+ },
+ "motorway": {
+ "category": "travel",
+ "moji": "🛣",
+ "description": "motorway",
+ "unicodeVersion": "7.0",
+ "digest": "96694ecc0e48e7b0ab1e0a8fdf5e75169ae404fad47620986dfd120571e39e58"
+ },
+ "railway_track": {
+ "category": "travel",
+ "moji": "🛤",
+ "description": "railway track",
+ "unicodeVersion": "7.0",
+ "digest": "2a36d0f6b16ecb70328a34502c639ce931fcb558c72fad0118b7380a13de2076"
+ },
+ "oil": {
"category": "objects",
- "moji": "🕰",
- "description": "mantlepiece clock",
+ "moji": "🛢",
+ "description": "oil drum",
"unicodeVersion": "7.0",
- "digest": "c62364007eb0e3a3c873476ff4b97c832947769502944f11875fab15c41dbe36"
+ "digest": "d2307ad3c2e74c0c5c2594d34e77e0a31a5ef5c0c052a147f24f119054697e1e"
},
- "clock1": {
- "category": "symbols",
- "moji": "🕐",
- "description": "clock face one oclock",
+ "fuelpump": {
+ "category": "travel",
+ "moji": "⛽",
+ "description": "fuel pump",
+ "unicodeVersion": "5.2",
+ "digest": "7c8c6c22de83e52ef399607cd6b17b535bf5e3a0aa29a9739f20c6bfe64d52d6"
+ },
+ "rotating_light": {
+ "category": "travel",
+ "moji": "🚨",
+ "description": "police cars revolving light",
"unicodeVersion": "6.0",
- "digest": "633f923dc3e76cd6e502f60e7445c81dc00372bb9aaae80e702c601ddcfb4821"
+ "digest": "353edbbfb893497b28d753273a11eb94ae67136365dae826ad5c7ac5a37b0d08"
},
- "clock10": {
- "category": "symbols",
- "moji": "🕙",
- "description": "clock face ten oclock",
+ "traffic_light": {
+ "category": "travel",
+ "moji": "🚥",
+ "description": "horizontal traffic light",
"unicodeVersion": "6.0",
- "digest": "d993967e8116003d6de796cba9caf743297fdf8f900bc8d1f0b7eb9bd3a5194f"
+ "digest": "7ba4ce5e0bab82e8a78afb05e7705c1dfcaebba6bce6eec0dae63fd62896cd38"
},
- "clock1030": {
- "category": "symbols",
- "moji": "🕥",
- "description": "clock face ten-thirty",
+ "vertical_traffic_light": {
+ "category": "travel",
+ "moji": "🚦",
+ "description": "vertical traffic light",
"unicodeVersion": "6.0",
- "digest": "fe6cdac5e9d3f8177ab18efd7f2acce65314964872fd659b7b498049bbf1a0eb"
+ "digest": "ff094eb787c114159f19f263994531db073cd9b3bd98c55fe4913bfa9bda53ec"
},
- "clock11": {
+ "octagonal_sign": {
"category": "symbols",
- "moji": "🕚",
- "description": "clock face eleven oclock",
+ "moji": "🛑",
+ "description": "octagonal sign",
+ "unicodeVersion": "9.0",
+ "digest": "0b138b4b190a12e0601bf71573378d5cce2012d72b9606b01a572ad7f7631c9d"
+ },
+ "construction": {
+ "category": "travel",
+ "moji": "🚧",
+ "description": "construction sign",
"unicodeVersion": "6.0",
- "digest": "2e676cc1f8a7dcbe76ba414a2a2a4b5724a0cda372d936cb869a4cfa540b0728"
+ "digest": "3a406d5baeede21f5ae078a4a641ee42d36c1b877a97bb10191fec729a6c0fb1"
},
- "clock1130": {
- "category": "symbols",
- "moji": "🕦",
- "description": "clock face eleven-thirty",
+ "anchor": {
+ "category": "travel",
+ "moji": "⚓",
+ "description": "anchor",
+ "unicodeVersion": "4.1",
+ "digest": "b5f2eacb26d6e550286eb7c01beb0bd5072baf33f6c68c8d7110f634a8b2fdf6"
+ },
+ "sailboat": {
+ "category": "travel",
+ "moji": "⛵",
+ "description": "sailboat",
+ "unicodeVersion": "5.2",
+ "digest": "252e917b1bc71019256db272af716e77bbe5839e4e5b77416ee566657ec3ffaf"
+ },
+ "canoe": {
+ "category": "travel",
+ "moji": "🛶",
+ "description": "canoe",
+ "unicodeVersion": "9.0",
+ "digest": "20d70c92c33a8bc6629e6d0b87bfca0dbbc9c32458d253ee62eb0845398fe095"
+ },
+ "speedboat": {
+ "category": "travel",
+ "moji": "🚤",
+ "description": "speedboat",
"unicodeVersion": "6.0",
- "digest": "0b95e14406f4b60ba855a4b2d7655a9af98601ce0306f1d113569e033ba054f8"
+ "digest": "54ce5a81c70e2f1e57a8d0f90829db96d146d84911462b8412e1cb6ee62ddf22"
+ },
+ "cruise_ship": {
+ "category": "travel",
+ "moji": "🛳",
+ "description": "passenger ship",
+ "unicodeVersion": "7.0",
+ "digest": "8dc923a465e8183b0a1410f810825c190e494c77b01de7c2f5ee0a64f7580206"
+ },
+ "ferry": {
+ "category": "travel",
+ "moji": "⛴",
+ "description": "ferry",
+ "unicodeVersion": "5.2",
+ "digest": "977c47292fd51ecd5d558a309405b15fc64b9b8edbedf95d8af30fa40636e6d0"
+ },
+ "motorboat": {
+ "category": "travel",
+ "moji": "🛥",
+ "description": "motorboat",
+ "unicodeVersion": "7.0",
+ "digest": "edee0a9d9d7b9b9201fc41d5b5a38c939e5f6e2e7baa7891e495b91dc5198e65"
+ },
+ "ship": {
+ "category": "travel",
+ "moji": "🚢",
+ "description": "ship",
+ "unicodeVersion": "6.0",
+ "digest": "9d0c13fcfcefd0a07424396626cc955e0b5e6cdf8b6d7d3ae1c1f746826af89b"
+ },
+ "airplane": {
+ "category": "travel",
+ "moji": "✈",
+ "description": "airplane",
+ "unicodeVersion": "1.1",
+ "digest": "b223b20d905ace04c602f7fcf22eb66c8defb22f2589434a83ae39e1622dcc67"
+ },
+ "airplane_small": {
+ "category": "travel",
+ "moji": "🛩",
+ "description": "small airplane",
+ "unicodeVersion": "7.0",
+ "digest": "ad2f6b4f6f141bd184c743c31fa0eadbef7653f1915b410c8144293dbb4b3720"
+ },
+ "airplane_departure": {
+ "category": "travel",
+ "moji": "🛫",
+ "description": "airplane departure",
+ "unicodeVersion": "7.0",
+ "digest": "c371879cd5b6bd5df2954f5c6c7eb5b07b3358253e8b35cd806ca21e2af2d794"
+ },
+ "airplane_arriving": {
+ "category": "travel",
+ "moji": "🛬",
+ "description": "airplane arriving",
+ "unicodeVersion": "7.0",
+ "digest": "a8ea037bc27226bd7e7ce07fdcedfcc74d0cd9c99737c93f2a588066c01fe1fe"
+ },
+ "seat": {
+ "category": "travel",
+ "moji": "💺",
+ "description": "seat",
+ "unicodeVersion": "6.0",
+ "digest": "ef4b820995bafb53c28877312060b1b6ef1d2e5d65268f9942ac3567914ef2da"
+ },
+ "helicopter": {
+ "category": "travel",
+ "moji": "🚁",
+ "description": "helicopter",
+ "unicodeVersion": "6.0",
+ "digest": "e5cacdf04612e3e2bb7a1c8f6968c2dd5b0b715ee2bdbfa703d5d0ddedf6be5e"
+ },
+ "suspension_railway": {
+ "category": "travel",
+ "moji": "🚟",
+ "description": "suspension railway",
+ "unicodeVersion": "6.0",
+ "digest": "8c3f5852d6b7e363ef8dc14c483154ed7b65456aaf6cbfb16b3f46539de17c8d"
+ },
+ "mountain_cableway": {
+ "category": "travel",
+ "moji": "🚠",
+ "description": "mountain cableway",
+ "unicodeVersion": "6.0",
+ "digest": "22969301dc7395450c11ba2c81af096b2ce98da13786109dee70d801563a59b5"
+ },
+ "aerial_tramway": {
+ "category": "travel",
+ "moji": "🚡",
+ "description": "aerial tramway",
+ "unicodeVersion": "6.0",
+ "digest": "716dae206b786d985ddfa6b311369a708d00539f35b2612500afb19ac537261d"
+ },
+ "satellite_orbital": {
+ "category": "travel",
+ "moji": "🛰",
+ "description": "satellite",
+ "unicodeVersion": "7.0",
+ "digest": "f80a557315729acf11186808c9bf031570a12f34c8da29e6f4506d732609d541"
+ },
+ "rocket": {
+ "category": "travel",
+ "moji": "🚀",
+ "description": "rocket",
+ "unicodeVersion": "6.0",
+ "digest": "136f56b7d54596e10ba226a05a8f4628eba967d8fc90d55c115bdf4366d102d8"
+ },
+ "bellhop": {
+ "category": "objects",
+ "moji": "🛎",
+ "description": "bellhop bell",
+ "unicodeVersion": "7.0",
+ "digest": "9ea748fc595b9bdc1f012b38d2b86d42092baa8d4c58dc38040f624c0a893f2a"
+ },
+ "hourglass": {
+ "category": "objects",
+ "moji": "⌛",
+ "description": "hourglass",
+ "unicodeVersion": "1.1",
+ "digest": "88925df394060ad7e8285bc7cc6cdb3c54453316b75658c780bb956ca812d2fc"
+ },
+ "hourglass_flowing_sand": {
+ "category": "objects",
+ "moji": "⏳",
+ "description": "hourglass with flowing sand",
+ "unicodeVersion": "6.0",
+ "digest": "899fa78157223e27acf260c2cd902646b7271087aabd4724a2d05095484ac724"
+ },
+ "watch": {
+ "category": "objects",
+ "moji": "⌚",
+ "description": "watch",
+ "unicodeVersion": "1.1",
+ "digest": "1e540e8c6856ebfab897c71132525d2f3a1a51513d86423b862116515489f55b"
+ },
+ "alarm_clock": {
+ "category": "objects",
+ "moji": "⏰",
+ "description": "alarm clock",
+ "unicodeVersion": "6.0",
+ "digest": "b125863048df0f332c2af68df61d919d3ff61863bced1aca9269759aff4dfe10"
+ },
+ "stopwatch": {
+ "category": "objects",
+ "moji": "⏱",
+ "description": "stopwatch",
+ "unicodeVersion": "6.0",
+ "digest": "162489af83ccc7e09349637fa9e23b97a22208b05ff6bfbd31271a50c3745ee9"
+ },
+ "timer": {
+ "category": "objects",
+ "moji": "⏲",
+ "description": "timer clock",
+ "unicodeVersion": "6.0",
+ "digest": "b94e3cdd84834063f72e333339b27f5aeaa2e8b3082eff51a871d706f0038349"
+ },
+ "clock": {
+ "category": "objects",
+ "moji": "🕰",
+ "description": "mantlepiece clock",
+ "unicodeVersion": "7.0",
+ "digest": "c62364007eb0e3a3c873476ff4b97c832947769502944f11875fab15c41dbe36"
},
"clock12": {
"category": "symbols",
@@ -1798,6 +7027,13 @@
"unicodeVersion": "6.0",
"digest": "112ac4095b40ea4fffb33c0341e6bdfff09fca08b4c748eb7ece8544111d02e1"
},
+ "clock1": {
+ "category": "symbols",
+ "moji": "🕐",
+ "description": "clock face one oclock",
+ "unicodeVersion": "6.0",
+ "digest": "633f923dc3e76cd6e502f60e7445c81dc00372bb9aaae80e702c601ddcfb4821"
+ },
"clock130": {
"category": "symbols",
"moji": "🕜",
@@ -1917,26 +7153,173 @@
"unicodeVersion": "6.0",
"digest": "6a9e6a5775f11cea55103d4c3c02fef87da6196c4d4b58b91820b0074fabfa67"
},
- "closed_book": {
- "category": "objects",
- "moji": "📕",
- "description": "closed book",
+ "clock10": {
+ "category": "symbols",
+ "moji": "🕙",
+ "description": "clock face ten oclock",
"unicodeVersion": "6.0",
- "digest": "2b60f7ca436be9aa12cc95d9733e3c72cf36ca7c98a83be70748d1452fa758da"
+ "digest": "d993967e8116003d6de796cba9caf743297fdf8f900bc8d1f0b7eb9bd3a5194f"
},
- "closed_lock_with_key": {
+ "clock1030": {
+ "category": "symbols",
+ "moji": "🕥",
+ "description": "clock face ten-thirty",
+ "unicodeVersion": "6.0",
+ "digest": "fe6cdac5e9d3f8177ab18efd7f2acce65314964872fd659b7b498049bbf1a0eb"
+ },
+ "clock11": {
+ "category": "symbols",
+ "moji": "🕚",
+ "description": "clock face eleven oclock",
+ "unicodeVersion": "6.0",
+ "digest": "2e676cc1f8a7dcbe76ba414a2a2a4b5724a0cda372d936cb869a4cfa540b0728"
+ },
+ "clock1130": {
+ "category": "symbols",
+ "moji": "🕦",
+ "description": "clock face eleven-thirty",
+ "unicodeVersion": "6.0",
+ "digest": "0b95e14406f4b60ba855a4b2d7655a9af98601ce0306f1d113569e033ba054f8"
+ },
+ "new_moon": {
+ "category": "nature",
+ "moji": "🌑",
+ "description": "new moon symbol",
+ "unicodeVersion": "6.0",
+ "digest": "bb7c1576993e7aed13ae107a4f51c41aa1e811038c6c86a82388c8f6fa05dd72"
+ },
+ "waxing_crescent_moon": {
+ "category": "nature",
+ "moji": "🌒",
+ "description": "waxing crescent moon symbol",
+ "unicodeVersion": "6.0",
+ "digest": "a3534c77cf0a8da83fc2cef14481983bafc745e35da96231259dc926cd15937d"
+ },
+ "first_quarter_moon": {
+ "category": "nature",
+ "moji": "🌓",
+ "description": "first quarter moon symbol",
+ "unicodeVersion": "6.0",
+ "digest": "8bcc5d70d27f52098378fc800041429f726323ee840276c7901f4b525778a8f2"
+ },
+ "waxing_gibbous_moon": {
+ "category": "nature",
+ "moji": "🌔",
+ "description": "waxing gibbous moon symbol",
+ "unicodeVersion": "6.0",
+ "digest": "46fc534be196723eabfe1a8be62217c1cff70e2ea806ff5bc15e9e09c116d9c1"
+ },
+ "full_moon": {
+ "category": "nature",
+ "moji": "🌕",
+ "description": "full moon symbol",
+ "unicodeVersion": "6.0",
+ "digest": "094a0a8bbf01f188d48c6bd1bbb035be622f2046d8b0c1a1252a96424e95d856"
+ },
+ "waning_gibbous_moon": {
+ "category": "nature",
+ "moji": "🌖",
+ "description": "waning gibbous moon symbol",
+ "unicodeVersion": "6.0",
+ "digest": "bba224edaa61c1f00f4ea0679ebb96e15090c2948463bc7414734fd13307a3cc"
+ },
+ "last_quarter_moon": {
+ "category": "nature",
+ "moji": "🌗",
+ "description": "last quarter moon symbol",
+ "unicodeVersion": "6.0",
+ "digest": "7801bf738898b75b09c7ee3f89d71fb11f41bf838f2766b8e80c1028b2873f88"
+ },
+ "waning_crescent_moon": {
+ "category": "nature",
+ "moji": "🌘",
+ "description": "waning crescent moon symbol",
+ "unicodeVersion": "6.0",
+ "digest": "5790b3f1f581dd7e5480978f31a8c2ed67bca238a2f26eb6f415a2dc3b6ff7c5"
+ },
+ "crescent_moon": {
+ "category": "nature",
+ "moji": "🌙",
+ "description": "crescent moon",
+ "unicodeVersion": "6.0",
+ "digest": "bab44314af40c16b245f2e91b2cec98592cddda05630012ac9d6554e1d9b8b43"
+ },
+ "new_moon_with_face": {
+ "category": "nature",
+ "moji": "🌚",
+ "description": "new moon with face",
+ "unicodeVersion": "6.0",
+ "digest": "7d341f6f48648d0c4c45677509d3aac49c7b38162a06d28540fca49fb3ec9de6"
+ },
+ "first_quarter_moon_with_face": {
+ "category": "nature",
+ "moji": "🌛",
+ "description": "first quarter moon with face",
+ "unicodeVersion": "6.0",
+ "digest": "a878335d845f1ec830614307fe3d48e346730ec62d3334687aaef43d4a9033e6"
+ },
+ "last_quarter_moon_with_face": {
+ "category": "nature",
+ "moji": "🌜",
+ "description": "last quarter moon with face",
+ "unicodeVersion": "6.0",
+ "digest": "e1e5678ea54dfafb83bb94879150bc43c495dc546be92c8780469f24b52d036e"
+ },
+ "thermometer": {
"category": "objects",
- "moji": "🔐",
- "description": "closed lock with key",
+ "moji": "🌡",
+ "description": "thermometer",
+ "unicodeVersion": "7.0",
+ "digest": "4b54cd4fc758bfc9e8830d36726ba06a0ac9e0d0d397ecba99599c9bde44f4e5"
+ },
+ "sunny": {
+ "category": "nature",
+ "moji": "☀",
+ "description": "black sun with rays",
+ "unicodeVersion": "1.1",
+ "digest": "254e2e15e1e548aeb54048217501d7da60f57ebe8c9de2e61e84e0714deba7a4"
+ },
+ "full_moon_with_face": {
+ "category": "nature",
+ "moji": "🌝",
+ "description": "full moon with face",
"unicodeVersion": "6.0",
- "digest": "5772854e275a722c97338858e416961e9812c19a6e908323f02d400158fbd6da"
+ "digest": "aeccdf5baf685650ee34da23f14cb4d659598c44c260a8f549489cbed1d46058"
},
- "closed_umbrella": {
- "category": "people",
- "moji": "🌂",
- "description": "closed umbrella",
+ "sun_with_face": {
+ "category": "nature",
+ "moji": "🌞",
+ "description": "sun with face",
"unicodeVersion": "6.0",
- "digest": "464550b8a3f71825b81ba11f158b815dd54d33e010c1bc1ba67ea196efcbccb3"
+ "digest": "631ad6d36e45769ebfe03c3d9fc18d5ad8f333c58ed7f92dcc5dcb8bf7f6321e"
+ },
+ "star": {
+ "category": "nature",
+ "moji": "⭐",
+ "description": "white medium star",
+ "unicodeVersion": "5.1",
+ "digest": "3dc3b69f9789146c64cd333666f35ce1e1efdd4fe335f6b8574685015bf8bd09"
+ },
+ "star2": {
+ "category": "nature",
+ "moji": "🌟",
+ "description": "glowing star",
+ "unicodeVersion": "6.0",
+ "digest": "c242d4e9c64d0ba3d8b5e3c83888ee4561c4250e48bd72f80d3264497a66ce77"
+ },
+ "stars": {
+ "category": "travel",
+ "moji": "🌠",
+ "description": "shooting star",
+ "unicodeVersion": "6.0",
+ "digest": "46bfa86253fb531e4357590f5244a88c3713926d1f28349ca641705dc069265f"
+ },
+ "milky_way": {
+ "category": "travel",
+ "moji": "🌌",
+ "description": "milky way",
+ "unicodeVersion": "6.0",
+ "digest": "6664f60e65321fccf491333caca68f961acfdd35a24f299e5909369a94816a6b"
},
"cloud": {
"category": "nature",
@@ -1945,12 +7328,40 @@
"unicodeVersion": "1.1",
"digest": "5a44592a6e7185d800b540d087049b75c1a8fd706725c980001633f64dad1472"
},
- "cloud_lightning": {
+ "partly_sunny": {
"category": "nature",
- "moji": "🌩",
- "description": "cloud with lightning",
+ "moji": "⛅",
+ "description": "sun behind cloud",
+ "unicodeVersion": "5.2",
+ "digest": "3e3e120cd4b1fc10548064d7cc529ea3c04eec3f8ac457b90628c119ba62d1f6"
+ },
+ "thunder_cloud_rain": {
+ "category": "nature",
+ "moji": "⛈",
+ "description": "thunder cloud and rain",
+ "unicodeVersion": "5.2",
+ "digest": "c1d9417ce640885540743b4fdb7df6efcccb6a04a31a1c62780b4c952c9f11d4"
+ },
+ "white_sun_small_cloud": {
+ "category": "nature",
+ "moji": "🌤",
+ "description": "white sun with small cloud",
"unicodeVersion": "7.0",
- "digest": "6e0db4d854dfb78548f458630b0f66a2a5bfa415116d87e89c9ee3c1595eccdb"
+ "digest": "7f6a12cec242c9fd4ece3a014eb12f771b4d5195280783ad771b5110c444c028"
+ },
+ "white_sun_cloud": {
+ "category": "nature",
+ "moji": "🌥",
+ "description": "white sun behind cloud",
+ "unicodeVersion": "7.0",
+ "digest": "78c9ade346888e8758e56107bed5bd0a16b4da60ef591cb71e1c68625feec8c8"
+ },
+ "white_sun_rain_cloud": {
+ "category": "nature",
+ "moji": "🌦",
+ "description": "white sun behind cloud with rain",
+ "unicodeVersion": "7.0",
+ "digest": "7d95f929a5679e52f4c9f93d2fe87d33524e38ab08115cd9a20aa481611f83ab"
},
"cloud_rain": {
"category": "nature",
@@ -1966,6 +7377,13 @@
"unicodeVersion": "7.0",
"digest": "34fadb93556a1c2b94104ba4b533b0a2b56399f41f4d4e9f4898a8c8add204b6"
},
+ "cloud_lightning": {
+ "category": "nature",
+ "moji": "🌩",
+ "description": "cloud with lightning",
+ "unicodeVersion": "7.0",
+ "digest": "6e0db4d854dfb78548f458630b0f66a2a5bfa415116d87e89c9ee3c1595eccdb"
+ },
"cloud_tornado": {
"category": "nature",
"moji": "🌪",
@@ -1973,47 +7391,89 @@
"unicodeVersion": "7.0",
"digest": "d7ff0075ec6ad30866c1f44d6b091e11e9d56ec5395481f9372edf08cfb7e002"
},
- "clown": {
- "category": "people",
- "moji": "🤡",
- "description": "clown face",
- "unicodeVersion": "9.0",
- "digest": "4cabd73ae323f53200eb179e177ffbcc984f07847b71912f0c2874fa9c5e53f3"
+ "fog": {
+ "category": "nature",
+ "moji": "🌫",
+ "description": "fog",
+ "unicodeVersion": "7.0",
+ "digest": "d216852000dd40e0f8b4c407368631b60a56d8eac32832461b303cc8f4d47488"
},
- "clubs": {
+ "wind_blowing_face": {
+ "category": "nature",
+ "moji": "🌬",
+ "description": "wind blowing face",
+ "unicodeVersion": "7.0",
+ "digest": "acb691375579ce301ce94a0c9f23bebcac90139e2c3cea8aa8c6cb3b581e8ae3"
+ },
+ "cyclone": {
"category": "symbols",
- "moji": "♣",
- "description": "black club suit",
- "unicodeVersion": "1.1",
- "digest": "eca840fc5c181df26e30df8ac9de5303c534d7532d86a4c4f29e029ebb1eb18d"
+ "moji": "🌀",
+ "description": "cyclone",
+ "unicodeVersion": "6.0",
+ "digest": "ead05cbe9adcf38310712af602c8caea12eac26e87756579efb8c542dde8b391"
},
- "cocktail": {
- "category": "food",
- "moji": "🍸",
- "description": "cocktail glass",
+ "rainbow": {
+ "category": "travel",
+ "moji": "🌈",
+ "description": "rainbow",
"unicodeVersion": "6.0",
- "digest": "91b4e4dc85b87b50d85d4831a18de7dd952c45c427f46a1a2c55d0abf4c4f5dd"
+ "digest": "f3e13a1ac2f7a2ae4096f47a2e1a1afd18f2998a67f6491002c3bcb019093222"
},
- "coffee": {
- "category": "food",
- "moji": "☕",
- "description": "hot beverage",
+ "closed_umbrella": {
+ "category": "people",
+ "moji": "🌂",
+ "description": "closed umbrella",
+ "unicodeVersion": "6.0",
+ "digest": "464550b8a3f71825b81ba11f158b815dd54d33e010c1bc1ba67ea196efcbccb3"
+ },
+ "umbrella2": {
+ "category": "nature",
+ "moji": "☂",
+ "description": "umbrella",
+ "unicodeVersion": "1.1",
+ "digest": "c8e5f34916627bd8053727b3ae40c5fb1df6bad76049e925696140825b413af4"
+ },
+ "umbrella": {
+ "category": "nature",
+ "moji": "☔",
+ "description": "umbrella with rain drops",
"unicodeVersion": "4.0",
- "digest": "bd09e4d36ebe4df49c695147a2d1ae91b6cbdd7eb7bb5066a1bada8ca2fa7c6f"
+ "digest": "99c2f2a4331ff3f17192394ce766808e725e0701cf903cea606efb5f1d4b3f4a"
},
- "coffin": {
+ "beach_umbrella": {
"category": "objects",
- "moji": "⚰",
- "description": "coffin",
- "unicodeVersion": "4.1",
- "digest": "08e3596ffa53801967dd6ff1715999513d8d9b3ae1f1f7c2b841a414981ee5d4"
+ "moji": "⛱",
+ "description": "umbrella on ground",
+ "unicodeVersion": "5.2",
+ "digest": "8222557bcf3669971279b80855fad3d97cd891e8a446b2e82ca220627a4283d5"
},
- "cold_sweat": {
- "category": "people",
- "moji": "😰",
- "description": "face with open mouth and cold sweat",
- "unicodeVersion": "6.0",
- "digest": "d96b966a52919667857c96c5d03596cb29daa1a7d87acba0286556f5dcaa25af"
+ "zap": {
+ "category": "nature",
+ "moji": "⚡",
+ "description": "high voltage sign",
+ "unicodeVersion": "4.0",
+ "digest": "fbbfbda066d067a5b92ca815c784b577354de3e6c7a3dfa9a1656c29dc804f0c"
+ },
+ "snowflake": {
+ "category": "nature",
+ "moji": "❄",
+ "description": "snowflake",
+ "unicodeVersion": "1.1",
+ "digest": "256f84d43855ee7699d7bfa2e3bb4cf71ec61ee9cc83a1ffb2973393cc43a5fa"
+ },
+ "snowman2": {
+ "category": "nature",
+ "moji": "☃",
+ "description": "snowman",
+ "unicodeVersion": "1.1",
+ "digest": "ce7bc1b374999e94c2f4a08f77ed979ccb6ee86ec9a6c1b8f4a93680080a25a7"
+ },
+ "snowman": {
+ "category": "nature",
+ "moji": "⛄",
+ "description": "snowman without snow",
+ "unicodeVersion": "5.2",
+ "digest": "c0908d9bc9b9fabff1e5eb18c6db07e981a4b9d886c7babbe2ce109e244f8182"
},
"comet": {
"category": "nature",
@@ -2022,19 +7482,75 @@
"unicodeVersion": "1.1",
"digest": "647209d932dbca09e1a0f3e13b0579251f76ba5f314faf6fc010eee6eb1606bb"
},
- "compression": {
+ "fire": {
+ "category": "nature",
+ "moji": "🔥",
+ "description": "fire",
+ "unicodeVersion": "6.0",
+ "digest": "37ab0faba2cb2a665775cdde7aaf4b4a0dfdd21030b57a613ddc9cb91883d7e4"
+ },
+ "droplet": {
+ "category": "nature",
+ "moji": "💧",
+ "description": "droplet",
+ "unicodeVersion": "6.0",
+ "digest": "df3a38da24ffe4b245eaffe3a16c9f8d995a23f07030d4de3d7244a057939a5a"
+ },
+ "ocean": {
+ "category": "nature",
+ "moji": "🌊",
+ "description": "water wave",
+ "unicodeVersion": "6.0",
+ "digest": "495ef59f29b98d5e87b9353274051950dbf7ac9fe486571775389c10e658fa5c"
+ },
+ "jack_o_lantern": {
+ "category": "nature",
+ "moji": "🎃",
+ "description": "jack-o-lantern",
+ "unicodeVersion": "6.0",
+ "digest": "876808e4ffa5ef7d736b88d9b1d646ea1af3bdf91f494190740502491d82edbd"
+ },
+ "christmas_tree": {
+ "category": "nature",
+ "moji": "🎄",
+ "description": "christmas tree",
+ "unicodeVersion": "6.0",
+ "digest": "cb28845d8770cc86de0034f03e38e46b251fb5bd4357a923c107404e8c5116bb"
+ },
+ "fireworks": {
+ "category": "travel",
+ "moji": "🎆",
+ "description": "fireworks",
+ "unicodeVersion": "6.0",
+ "digest": "6242b48df4b30753b154ec6d5f3883646fa42b55a629a7a1273d290e97df8a41"
+ },
+ "sparkler": {
+ "category": "travel",
+ "moji": "🎇",
+ "description": "firework sparkler",
+ "unicodeVersion": "6.0",
+ "digest": "d603ef03cdc4ec05338005c357c3a41215f180545f422fea5b40b766ecfe6d1f"
+ },
+ "sparkles": {
+ "category": "nature",
+ "moji": "✨",
+ "description": "sparkles",
+ "unicodeVersion": "6.0",
+ "digest": "8f68cb167489b1055a8acedaf8ea4c0553d0a5f7bf0983fc3660537ff09a5360"
+ },
+ "balloon": {
"category": "objects",
- "moji": "🗜",
- "description": "compression",
- "unicodeVersion": "7.0",
- "digest": "e762761f41d6c41d204ca149b161ea4286ae71517ea714fa75951c0eea600eeb"
+ "moji": "🎈",
+ "description": "balloon",
+ "unicodeVersion": "6.0",
+ "digest": "9f76188ad32199d081dfc6757b68f66c8b4e8629f420e85d4cb88e00a72256ab"
},
- "computer": {
+ "tada": {
"category": "objects",
- "moji": "💻",
- "description": "personal computer",
+ "moji": "🎉",
+ "description": "party popper",
"unicodeVersion": "6.0",
- "digest": "08200fc0c04bb776d3b7e187de415a1a6a6bc82a7beb70829e03c40c5312d7d1"
+ "digest": "879b8a892411b1839ad7c4a4d9ffcae074d3829bb40767c916471b06a39da9ec"
},
"confetti_ball": {
"category": "objects",
@@ -2043,551 +7559,1328 @@
"unicodeVersion": "6.0",
"digest": "745790484353edffb62c78d1460baa049411e32fed88bd6424c0fd804fe67ca8"
},
- "confounded": {
- "category": "people",
- "moji": "😖",
- "description": "confounded face",
+ "tanabata_tree": {
+ "category": "nature",
+ "moji": "🎋",
+ "description": "tanabata tree",
"unicodeVersion": "6.0",
- "digest": "084da8b9e9e24eaee418df200ed9369e04fded381d756382b34f96a362d2c93c"
+ "digest": "6ec94e277dc3027dbaf8d844dd26202c00b36602f5f73d0d2c36c087d5d674b9"
},
- "confused": {
- "category": "people",
- "moji": "😕",
- "description": "confused face",
- "unicodeVersion": "6.1",
- "digest": "4b8a05e1d84cb6314b217fdd4f89065411021f3ed3f5a824dbf538d54b5c3132"
+ "bamboo": {
+ "category": "nature",
+ "moji": "🎍",
+ "description": "pine decoration",
+ "unicodeVersion": "6.0",
+ "digest": "1d26da431d87de2787fc0b8a18559ea14cc2fdbb928d40bcc0de6810a84be2bd"
},
- "congratulations": {
- "category": "symbols",
- "moji": "㊗",
- "description": "circled ideograph congratulation",
- "unicodeVersion": "1.1",
- "digest": "3c4f04a0b13353e975bdcd489f5cc2899b3dace2c83ca3012605f1b2801b581e"
+ "dolls": {
+ "category": "objects",
+ "moji": "🎎",
+ "description": "japanese dolls",
+ "unicodeVersion": "6.0",
+ "digest": "da477dfcb1ecfab98835f4ed721354240d1a7f82e1d7df4b80e67baabb237b9f"
},
- "construction": {
- "category": "travel",
- "moji": "🚧",
- "description": "construction sign",
+ "flags": {
+ "category": "objects",
+ "moji": "🎏",
+ "description": "carp streamer",
"unicodeVersion": "6.0",
- "digest": "3a406d5baeede21f5ae078a4a641ee42d36c1b877a97bb10191fec729a6c0fb1"
+ "digest": "fe687c07453ae2869f11816bc2c604c67ba8c3754e597ce6871289cc358253c1"
},
- "construction_site": {
+ "wind_chime": {
+ "category": "objects",
+ "moji": "🎐",
+ "description": "wind chime",
+ "unicodeVersion": "6.0",
+ "digest": "57b721fd94e359f9a8dcd9fc702a3c9f82b06f4347d5b6e33c9d42117dd26e8a"
+ },
+ "rice_scene": {
"category": "travel",
- "moji": "🏗",
- "description": "building construction",
+ "moji": "🎑",
+ "description": "moon viewing ceremony",
+ "unicodeVersion": "6.0",
+ "digest": "18b94e88b72cd2158d831d86f78c91d3ae44b279ec104b3606fd177a2ab1372f"
+ },
+ "ribbon": {
+ "category": "objects",
+ "moji": "🎀",
+ "description": "ribbon",
+ "unicodeVersion": "6.0",
+ "digest": "498ca699cf15df0de221f962a03566a1944f9eab0cdd44353460d9198265f35c"
+ },
+ "gift": {
+ "category": "objects",
+ "moji": "🎁",
+ "description": "wrapped present",
+ "unicodeVersion": "6.0",
+ "digest": "f05374b0464ab426379251722208599f95b4dc00f8ca551b5fbd6cea5ee98ee6"
+ },
+ "reminder_ribbon": {
+ "category": "activity",
+ "moji": "🎗",
+ "description": "reminder ribbon",
"unicodeVersion": "7.0",
- "digest": "5973b6f1b1c75fd083d975c2c11f68fff6583f10056418c699da1cd3049a5f89"
+ "digest": "3f3b9a65033a2e9e9d58ee18aa3cb3158ce2bf243c6c0352f7278bf2b4f8106c"
},
- "construction_worker": {
- "category": "people",
- "moji": "👷",
- "description": "construction worker",
+ "tickets": {
+ "category": "activity",
+ "moji": "🎟",
+ "description": "admission tickets",
+ "unicodeVersion": "7.0",
+ "digest": "f70e7c3f2a059c85b36587941a861dc9e41c8db0475ddbe2d5dd50ad440dd579"
+ },
+ "ticket": {
+ "category": "activity",
+ "moji": "🎫",
+ "description": "ticket",
"unicodeVersion": "6.0",
- "digest": "47476b5eb51a1e00d0c95f032c79ca6472b41ece0bdf057bbb87da088605c589"
+ "digest": "0575c271a22acdc294505b2aae0f288e67b0d78d9b60af97420806ac893e4ea9"
},
- "construction_worker_tone1": {
- "category": "people",
- "moji": "👷🏻",
- "description": "construction worker tone 1",
+ "military_medal": {
+ "category": "activity",
+ "moji": "🎖",
+ "description": "military medal",
+ "unicodeVersion": "7.0",
+ "digest": "745687033024bc5d1dae74f427504058f17f29c1f02a59b25c9d488a6307cae3"
+ },
+ "trophy": {
+ "category": "activity",
+ "moji": "🏆",
+ "description": "trophy",
+ "unicodeVersion": "6.0",
+ "digest": "33ba0ae0619cb3c2cfa350a36e535601d327b7a0cb7371f2686790336559f912"
+ },
+ "medal": {
+ "category": "activity",
+ "moji": "🏅",
+ "description": "sports medal",
+ "unicodeVersion": "7.0",
+ "digest": "b7518a4c832aa937d85741738e1c74c09ff645197fbbdc7d66cd4a8e78977073"
+ },
+ "first_place": {
+ "category": "activity",
+ "moji": "🥇",
+ "description": "first place medal",
+ "unicodeVersion": "9.0",
+ "digest": "d250c1890f341665d689d705c13fc4c5448d870d193b8b3405832e2fd92f5385"
+ },
+ "second_place": {
+ "category": "activity",
+ "moji": "🥈",
+ "description": "second place medal",
+ "unicodeVersion": "9.0",
+ "digest": "9c9a125c7085e8ab5b89cbd10d9458eac835330dbb78737e8cf979a39c2ae5fd"
+ },
+ "third_place": {
+ "category": "activity",
+ "moji": "🥉",
+ "description": "third place medal",
+ "unicodeVersion": "9.0",
+ "digest": "0afe5ea1a8963329439e0a5ed4922939cc4ab733f5fb50f40edcc2e4fafa12ed"
+ },
+ "soccer": {
+ "category": "activity",
+ "moji": "⚽",
+ "description": "soccer ball",
+ "unicodeVersion": "5.2",
+ "digest": "1807b8f9e9b0a3cbf390a582f52d2ec4dad7a19008d9d0215ae6cded7b6dd691"
+ },
+ "baseball": {
+ "category": "activity",
+ "moji": "⚾",
+ "description": "baseball",
+ "unicodeVersion": "5.2",
+ "digest": "8c9c750fe39ff65807d9deabccce9dbf96cdff585f320919973576027093c7a6"
+ },
+ "basketball": {
+ "category": "activity",
+ "moji": "🏀",
+ "description": "basketball and hoop",
+ "unicodeVersion": "6.0",
+ "digest": "1c38475863ccaf78b869bdd50d0eb71d189299e189be0ab8b00405096a047628"
+ },
+ "volleyball": {
+ "category": "activity",
+ "moji": "🏐",
+ "description": "volleyball",
"unicodeVersion": "8.0",
- "digest": "087fe998c1e316591d4a0c09c7ef80b0cf73d7f0f295e4bc18405e9153eb7b6c"
+ "digest": "a9dc3a3516d1e22aededc7198fdf3a4f304b7c2f7fa528806c2e0bdfe320d007"
},
- "construction_worker_tone2": {
- "category": "people",
- "moji": "👷🏼",
- "description": "construction worker tone 2",
+ "football": {
+ "category": "activity",
+ "moji": "🏈",
+ "description": "american football",
+ "unicodeVersion": "6.0",
+ "digest": "aefdff9cd010c9d82e2cdd83c4430db38bf311bfeac7f86583b122cba146c73f"
+ },
+ "rugby_football": {
+ "category": "activity",
+ "moji": "🏉",
+ "description": "rugby football",
+ "unicodeVersion": "6.0",
+ "digest": "db852921f30f88e9604440a5d7f8ce513c3001293a1ffbc35c66c45fee1159c4"
+ },
+ "tennis": {
+ "category": "activity",
+ "moji": "🎾",
+ "description": "tennis racquet and ball",
+ "unicodeVersion": "6.0",
+ "digest": "857b0f96109f4534cef496348b76036354eecdee640855fb83e0e3b253c7d914"
+ },
+ "bowling": {
+ "category": "activity",
+ "moji": "🎳",
+ "description": "bowling",
+ "unicodeVersion": "6.0",
+ "digest": "2bc7f10afb9d6c61e015e4b6ca8c65c038a6a802840e1879142ee9bb86369b38"
+ },
+ "cricket": {
+ "category": "activity",
+ "moji": "🏏",
+ "description": "cricket bat and ball",
+ "unicodeVersion": "10.0",
+ "digest": "c0447966114b1db5c9d1dd5e47bd7f85a19accb8c6daeb9984644834e72e7598"
+ },
+ "field_hockey": {
+ "category": "activity",
+ "moji": "🏑",
+ "description": "field hockey stick and ball",
"unicodeVersion": "8.0",
- "digest": "2a829b907f78a0d197677d19be96e177e0a021a04decb3c0bbf637cf74716787"
+ "digest": "8608cd6944c9d3791e5c0b3111f7c5ca6155f64552ae9ab3e2f93514d053e042"
},
- "construction_worker_tone3": {
- "category": "people",
- "moji": "👷🏽",
- "description": "construction worker tone 3",
+ "hockey": {
+ "category": "activity",
+ "moji": "🏒",
+ "description": "ice hockey stick and puck",
"unicodeVersion": "8.0",
- "digest": "551d3bddffe6e7c739732a1d759994e2a8090e81df401a70c5b4eab315242840"
+ "digest": "9c5e4e47bca98e73610173eb568ab9f2abf6cbdb8357438db30244946ebd7dac"
},
- "construction_worker_tone4": {
- "category": "people",
- "moji": "👷🏾",
- "description": "construction worker tone 4",
+ "ping_pong": {
+ "category": "activity",
+ "moji": "🏓",
+ "description": "table tennis paddle and ball",
"unicodeVersion": "8.0",
- "digest": "d563f84633598c86bbad12a96418eee7e7a6cd945d9ecdb60d93be56697b8d96"
+ "digest": "f55f8483698dd2252aa5e2819f98e2f5d753616319446067de16acc5ffe12129"
},
- "construction_worker_tone5": {
- "category": "people",
- "moji": "👷🏿",
- "description": "construction worker tone 5",
+ "badminton": {
+ "category": "activity",
+ "moji": "🏸",
+ "description": "badminton racquet",
"unicodeVersion": "8.0",
- "digest": "56950bb7e235ccd82c8b1e689268ecd471809b8b65ec8654949a15cd6b42e7b5"
+ "digest": "52efeaba6a27cef40bbd624eb945bf8712c8ce3a135de8b979c1e04b440ec51a"
},
- "control_knobs": {
+ "boxing_glove": {
+ "category": "activity",
+ "moji": "🥊",
+ "description": "boxing glove",
+ "unicodeVersion": "9.0",
+ "digest": "4acdaf7627f8a70148dadec0012474c3eb5042d2a165522c9a221cb6c63376c3"
+ },
+ "martial_arts_uniform": {
+ "category": "activity",
+ "moji": "🥋",
+ "description": "martial arts uniform",
+ "unicodeVersion": "9.0",
+ "digest": "d1953d1f75350bcde491bb2a551e55bafc34e14e5de0ca6f27b1d55e0dffa135"
+ },
+ "goal": {
+ "category": "activity",
+ "moji": "🥅",
+ "description": "goal net",
+ "unicodeVersion": "9.0",
+ "digest": "e867e3f1d97159e2301c2903bbfab130921490cbf4d5fdc4d5d0a62088dbfda8"
+ },
+ "golf": {
+ "category": "activity",
+ "moji": "⛳",
+ "description": "flag in hole",
+ "unicodeVersion": "5.2",
+ "digest": "90dbc9461dfb3f37bcf171eedaf1131d4567ceace94a86f21f6e6befd6473a98"
+ },
+ "ice_skate": {
+ "category": "activity",
+ "moji": "⛸",
+ "description": "ice skate",
+ "unicodeVersion": "5.2",
+ "digest": "e06869a74874d409372ef2714ca8f3e3050ad8a81e589709ecb541d5c77603df"
+ },
+ "fishing_pole_and_fish": {
+ "category": "activity",
+ "moji": "🎣",
+ "description": "fishing pole and fish",
+ "unicodeVersion": "6.0",
+ "digest": "539df418ee29c240216b28aa1af12feefe9273de148a80289c2a512785d98aee"
+ },
+ "running_shirt_with_sash": {
+ "category": "activity",
+ "moji": "🎽",
+ "description": "running shirt with sash",
+ "unicodeVersion": "6.0",
+ "digest": "a5f5ff9bf3e3eab82f370c9b81dc2bce186bb0a667dee54a2066d42f04d97eb4"
+ },
+ "ski": {
+ "category": "activity",
+ "moji": "🎿",
+ "description": "ski and ski boot",
+ "unicodeVersion": "6.0",
+ "digest": "973ff4abd90a020e2c608c32ab88324e1a11ca7a4541e436f37c5d858681eba6"
+ },
+ "dart": {
+ "category": "activity",
+ "moji": "🎯",
+ "description": "direct hit",
+ "unicodeVersion": "6.0",
+ "digest": "01d4bb3f51417e7861404746bbebc4704edf5fa80bde6ad9cf9e12c238a1206c"
+ },
+ "gun": {
"category": "objects",
- "moji": "🎛",
- "description": "control knobs",
+ "moji": "🔫",
+ "description": "pistol",
+ "unicodeVersion": "6.0",
+ "digest": "586379358f302248a7be52d185997a0904e31635d32a0fac86366d98ac1ab242"
+ },
+ "8ball": {
+ "category": "activity",
+ "moji": "🎱",
+ "description": "billiards",
+ "unicodeVersion": "6.0",
+ "digest": "de5dbbd700f078ed6d780e79cb1b5b3214180b42a38917b3e1222af731cf5e3d"
+ },
+ "crystal_ball": {
+ "category": "objects",
+ "moji": "🔮",
+ "description": "crystal ball",
+ "unicodeVersion": "6.0",
+ "digest": "225f048f2fefb378d3178931ad5e5e8feb419a19436e45d03c199bd726ec5151"
+ },
+ "video_game": {
+ "category": "activity",
+ "moji": "🎮",
+ "description": "video game",
+ "unicodeVersion": "6.0",
+ "digest": "95a218fcb12095024a77ee4826d0574190b5ebb8878d0fba7a640dcbeb3b78ba"
+ },
+ "joystick": {
+ "category": "objects",
+ "moji": "🕹",
+ "description": "joystick",
"unicodeVersion": "7.0",
- "digest": "437abc26fe18f903c498b5223ab35de813b8166e8460cfe9bc5b59177d949bc1"
+ "digest": "def1450af8fc7e3e4d968a6f6a2f5644b17f2786941d49c688c91dc4fde36836"
},
- "convenience_store": {
- "category": "travel",
- "moji": "🏪",
- "description": "convenience store",
+ "slot_machine": {
+ "category": "activity",
+ "moji": "🎰",
+ "description": "slot machine",
"unicodeVersion": "6.0",
- "digest": "07d3989157223a8249db3532a642050529831d47f05c0733fbe2cb6a6a780ea6"
+ "digest": "c057d62fa2bca301d20f6103606873b82d0136711f4d922eb05ab29d3179ecfe"
},
- "cookie": {
- "category": "food",
- "moji": "🍪",
- "description": "cookie",
+ "game_die": {
+ "category": "activity",
+ "moji": "🎲",
+ "description": "game die",
"unicodeVersion": "6.0",
- "digest": "246ade6def10ac3e98af1ba9517990a4b41e391b9f1d8323a333d4cb69acbe36"
+ "digest": "9a9eb1efd149aa229f39ffb93605f7c231a149892d9cedfeebc6e15eb7235656"
},
- "cooking": {
- "category": "food",
- "moji": "🍳",
- "description": "cooking",
+ "spades": {
+ "category": "symbols",
+ "moji": "♠",
+ "description": "black spade suit",
+ "unicodeVersion": "1.1",
+ "digest": "4581ce17d9b2a29ba4cc38794d7869e12cd1ceda5861e67b2d09130730ccc378"
+ },
+ "hearts": {
+ "category": "symbols",
+ "moji": "♥",
+ "description": "black heart suit",
+ "unicodeVersion": "1.1",
+ "digest": "86712f800e461be471d9d64d427f4e751eab584cae33b2c7a72634161c51e622"
+ },
+ "diamonds": {
+ "category": "symbols",
+ "moji": "♦",
+ "description": "black diamond suit",
+ "unicodeVersion": "1.1",
+ "digest": "77c773b770a293129c33513fc22124c6953db07d1b254905b0c734c853e65387"
+ },
+ "clubs": {
+ "category": "symbols",
+ "moji": "♣",
+ "description": "black club suit",
+ "unicodeVersion": "1.1",
+ "digest": "eca840fc5c181df26e30df8ac9de5303c534d7532d86a4c4f29e029ebb1eb18d"
+ },
+ "black_joker": {
+ "category": "symbols",
+ "moji": "🃏",
+ "description": "playing card black joker",
"unicodeVersion": "6.0",
- "digest": "b4bcd344fb394300464b8a81f325f8652c032f39ebe2acf85e046d594a58b84a"
+ "digest": "40b7dccd258d4d1323fc744fb545e0e06c1e813bc9030147fb28e0f5b3a76ef3"
},
- "cool": {
+ "mahjong": {
"category": "symbols",
- "moji": "🆒",
- "description": "squared cool",
+ "moji": "🀄",
+ "description": "mahjong tile red dragon",
+ "unicodeVersion": "5.1",
+ "digest": "adf2b23065245bbffa1f0e7d0a4656bb2c69d6862f162cd77e9eda26aee06353"
+ },
+ "flower_playing_cards": {
+ "category": "symbols",
+ "moji": "🎴",
+ "description": "flower playing cards",
"unicodeVersion": "6.0",
- "digest": "d9e004043f68d0dfea5cb4ae961c4e66edf32a840bf5f1dae43df3c522db6322"
+ "digest": "814948cd185c22b3a394a5bb15924ddf6cf07b612658bd79c5192cd453057681"
},
- "cop": {
+ "performing_arts": {
+ "category": "activity",
+ "moji": "🎭",
+ "description": "performing arts",
+ "unicodeVersion": "6.0",
+ "digest": "d4f55655f113bc672739e74b6e3b7684b05f49a68a8ce1a230a32295d0d7a847"
+ },
+ "frame_photo": {
+ "category": "objects",
+ "moji": "🖼",
+ "description": "frame with picture",
+ "unicodeVersion": "7.0",
+ "digest": "bcc4eade7fb066c301457bfd79daae84f0b2a4aa56c71dcb6bcb8fb76c2c4a96"
+ },
+ "art": {
+ "category": "activity",
+ "moji": "🎨",
+ "description": "artist palette",
+ "unicodeVersion": "6.0",
+ "digest": "ee525dbf572a127ac99ccc165520e429e3d24f7cf3b8d787496b63415454d49c"
+ },
+ "eyeglasses": {
"category": "people",
- "moji": "👮",
- "description": "police officer",
+ "moji": "👓",
+ "description": "eyeglasses",
"unicodeVersion": "6.0",
- "digest": "ca75e6722bc6520f3375af4d588cd5af15b52d5a856a67d99414e149d8ea9ca0"
+ "digest": "7f413b054509c24ee233b944df5a0b314ede67006d64f03df4a1c055a45245ee"
},
- "cop_tone1": {
+ "dark_sunglasses": {
"category": "people",
- "moji": "👮🏻",
- "description": "police officer tone 1",
- "unicodeVersion": "8.0",
- "digest": "309280c5b2f6f957134015fe62a262b7853b67c6d7a020e59e942a21ac52c561"
+ "moji": "🕶",
+ "description": "dark sunglasses",
+ "unicodeVersion": "7.0",
+ "digest": "0f941dc4ed9dcc861863176f49812966bcae7e3179044c1c8fa22de9ed8dbd09"
},
- "cop_tone2": {
+ "necktie": {
"category": "people",
- "moji": "👮🏼",
- "description": "police officer tone 2",
- "unicodeVersion": "8.0",
- "digest": "2db60b30bb0533a82590190ba1e9aa2a4a21b13219c9b5fdf67e0c88a63cac59"
+ "moji": "👔",
+ "description": "necktie",
+ "unicodeVersion": "6.0",
+ "digest": "1ae4a9a8c38b68c883bf843a044ebb7902ec5c9d73c6bbc1111b847d0353f657"
},
- "cop_tone3": {
+ "shirt": {
"category": "people",
- "moji": "👮🏽",
- "description": "police officer tone 3",
- "unicodeVersion": "8.0",
- "digest": "724bf942373fe976683449d871bd76275f0b21966abd69dfe046249731f17fee"
+ "moji": "👕",
+ "description": "t-shirt",
+ "unicodeVersion": "6.0",
+ "digest": "a165e8bd82e5ef701aafaabcfac6946af3a0c10c22d4c84010103ecf8d74e424"
},
- "cop_tone4": {
+ "jeans": {
"category": "people",
- "moji": "👮🏾",
- "description": "police officer tone 4",
- "unicodeVersion": "8.0",
- "digest": "c95ba196d60964cdc5252cef740ee98369ce6a9553f6da55e8b0dc3e6cfc345b"
+ "moji": "👖",
+ "description": "jeans",
+ "unicodeVersion": "6.0",
+ "digest": "e470f82274f2b149f98d4620e61c374c3737e722bd0a08576335cc269e86f83f"
},
- "cop_tone5": {
+ "dress": {
"category": "people",
- "moji": "👮🏿",
- "description": "police officer tone 5",
- "unicodeVersion": "8.0",
- "digest": "8aa857e06fa050ae584e2be28e61944e336f7b53d94ec10763be04ce9b59a812"
+ "moji": "👗",
+ "description": "dress",
+ "unicodeVersion": "6.0",
+ "digest": "9a1c024dd6f402a2764ad097eeb5656488df5bd60905e8deb04dc3f91ab890f4"
},
- "copyright": {
- "category": "symbols",
- "moji": "©️",
- "description": "copyright sign",
- "unicodeVersion": "1.1",
- "digest": "681f772f6710df90e3b53e2ee6c3170f344873bd4c1e41e2016a411125b90f4a"
+ "kimono": {
+ "category": "people",
+ "moji": "👘",
+ "description": "kimono",
+ "unicodeVersion": "6.0",
+ "digest": "987ec803ad9f64bd7ab8c41b487df6afd3416ef25fba11df698c2dddc779e59e"
},
- "corn": {
- "category": "food",
- "moji": "🌽",
- "description": "ear of maize",
+ "bikini": {
+ "category": "people",
+ "moji": "👙",
+ "description": "bikini",
"unicodeVersion": "6.0",
- "digest": "18c605b675251470f411303851450e3010e6a4683c06f1ad0df48eb2c8560a46"
+ "digest": "f7bf17cea90c1d7e18c55af498ea3019e1515efefd4f692b868998861d40000e"
},
- "couch": {
+ "womans_clothes": {
+ "category": "people",
+ "moji": "👚",
+ "description": "womans clothes",
+ "unicodeVersion": "6.0",
+ "digest": "4b47a76b3fa144213583b47c2284eee1e4148b9b4cd578f1d02ddb474c78a3ae"
+ },
+ "purse": {
+ "category": "people",
+ "moji": "👛",
+ "description": "purse",
+ "unicodeVersion": "6.0",
+ "digest": "069d6ce3046648578d3c04796bb6190ecfb55e1fd708be2021144f1716c3ae2f"
+ },
+ "handbag": {
+ "category": "people",
+ "moji": "👜",
+ "description": "handbag",
+ "unicodeVersion": "6.0",
+ "digest": "0f1d0849440c523c026b3dea64850f48c3fccbc01fc08878927a25900f393a76"
+ },
+ "pouch": {
+ "category": "people",
+ "moji": "👝",
+ "description": "pouch",
+ "unicodeVersion": "6.0",
+ "digest": "e333815505f48dad6240f0d6ac9476ab256b26ec53108c0454f77c6ac31c4874"
+ },
+ "shopping_bags": {
"category": "objects",
- "moji": "🛋",
- "description": "couch and lamp",
+ "moji": "🛍",
+ "description": "shopping bags",
"unicodeVersion": "7.0",
- "digest": "e9421d26e96ce4d68b09280e77e699963a3d8259385a12fd28375d61de4aff6b"
+ "digest": "1a6df85fd8117c2c9361c7524a43613a80d6c6d65a1940f2320cbc7d451ebf6f"
},
- "couple": {
+ "school_satchel": {
"category": "people",
- "moji": "👫",
- "description": "man and woman holding hands",
+ "moji": "🎒",
+ "description": "school satchel",
"unicodeVersion": "6.0",
- "digest": "d6523ae18c5e2a10b355dda179ca7b172ece9ef3ac70548dba3c45a5d5169e70"
+ "digest": "500fdb662493897890ad00c67200190fc2b48a5629231994d7bf43a5a9897e6d"
},
- "couple_mm": {
+ "mans_shoe": {
"category": "people",
- "moji": "👨‍❤️‍👨",
- "description": "couple (man,man)",
+ "moji": "👞",
+ "description": "mans shoe",
"unicodeVersion": "6.0",
- "digest": "0c3344e2abffd74868fb52ffa5614c77737094bac369030ceac6218eee2a0edf"
+ "digest": "ba3da1749562ad332de9a2c0916c1e72a8b6ccaf277d5c379bf6847f9b6fc148"
},
- "couple_with_heart": {
+ "athletic_shoe": {
"category": "people",
- "moji": "💑",
- "description": "couple with heart",
+ "moji": "👟",
+ "description": "athletic shoe",
"unicodeVersion": "6.0",
- "digest": "224c8f5d7c5a446b69e7175dfa6b07a9d36f2b2db3f908579813b6bcffb35f92"
+ "digest": "74ad8b5b9f0612ab983841e94637e02e91fc0de90b4a12997be5d2a2eb56d12c"
},
- "couple_ww": {
+ "high_heel": {
"category": "people",
- "moji": "👩‍❤️‍👩",
- "description": "couple (woman,woman)",
+ "moji": "👠",
+ "description": "high-heeled shoe",
"unicodeVersion": "6.0",
- "digest": "56651c521bbaade1979708f52d827e2251581b576c3afa651c89ea4c63739916"
+ "digest": "f2aee61e8ae1cb532d9f005317476bbbe26b6e7d18cf2b2cac3d21ab39a86dd1"
},
- "couplekiss": {
+ "sandal": {
"category": "people",
- "moji": "💏",
- "description": "kiss",
+ "moji": "👡",
+ "description": "womans sandal",
"unicodeVersion": "6.0",
- "digest": "14c66d83cace54fb7326ef04eb989ee27d5f2dec2e3884374cd346cd30340d5e"
+ "digest": "9737db7518eaf91a7294503c471ba0b015260677ca842988ef3764d30a0d402b"
},
- "cow": {
- "category": "nature",
- "moji": "🐮",
- "description": "cow face",
+ "boot": {
+ "category": "people",
+ "moji": "👢",
+ "description": "womans boots",
"unicodeVersion": "6.0",
- "digest": "9959b4d621471e4c3ddd23f52ddeb5ddb5792a0b6ac8697d2c2940bd6cb01b82"
+ "digest": "3ac88fcfbe55d073949e89aeeadf168c04633c9af4ce6673c897358e595511c2"
},
- "cow2": {
- "category": "nature",
- "moji": "🐄",
- "description": "cow",
+ "crown": {
+ "category": "people",
+ "moji": "👑",
+ "description": "crown",
"unicodeVersion": "6.0",
- "digest": "a9f5cdb317d96e7536d87faff2f2c651d6fb5bd00b89991c977b270e9f522820"
+ "digest": "6fd07901b1aedf7f0fe7ebcdbd6f7fe40f768a66a0e862203274097b16b5431d"
},
- "cowboy": {
+ "womans_hat": {
"category": "people",
- "moji": "🤠",
- "description": "face with cowboy hat",
- "unicodeVersion": "9.0",
- "digest": "0c3a81e8bc276a84073ae94db2cf08d378b3e17ca09e2e03abf248c66c00f34b"
+ "moji": "👒",
+ "description": "womans hat",
+ "unicodeVersion": "6.0",
+ "digest": "7feded8ca13ddee2d9fd71e97a0e7f719d85f85190a69d6fde985d69e20bc422"
},
- "crab": {
- "category": "nature",
- "moji": "🦀",
- "description": "crab",
+ "tophat": {
+ "category": "people",
+ "moji": "🎩",
+ "description": "top hat",
+ "unicodeVersion": "6.0",
+ "digest": "dff54bdac5a4d1df8e406a5dfa517c5ea60f9ebb6952f27b2780878478c4c8dc"
+ },
+ "mortar_board": {
+ "category": "people",
+ "moji": "🎓",
+ "description": "graduation cap",
+ "unicodeVersion": "6.0",
+ "digest": "6524faad267ba769fb1997527d41c97cac42cc5d99c57800adaf5a6892c79371"
+ },
+ "helmet_with_cross": {
+ "category": "people",
+ "moji": "⛑",
+ "description": "helmet with white cross",
+ "unicodeVersion": "5.2",
+ "digest": "12a73fcfb9d2a62305f4a59f028215c81868a4b8de862b276c3b508872ac70ad"
+ },
+ "prayer_beads": {
+ "category": "objects",
+ "moji": "📿",
+ "description": "prayer beads",
"unicodeVersion": "8.0",
- "digest": "8cdd21a6eaa56df3e503e8710e703b60ba6b059eaf2a9e93d8d25a4cbe2b55dc"
+ "digest": "aedb143e4798a14b97ff6595293d7ed873988023efbc1e3de3aa9f17344359fd"
},
- "crayon": {
+ "lipstick": {
+ "category": "people",
+ "moji": "💄",
+ "description": "lipstick",
+ "unicodeVersion": "6.0",
+ "digest": "102278217b4f4088fd48ccb08af558822258d85d24a4febdcad1b5cc78cbe2a2"
+ },
+ "ring": {
+ "category": "people",
+ "moji": "💍",
+ "description": "ring",
+ "unicodeVersion": "6.0",
+ "digest": "ec4386554d3b001d9b64cfa534094b67844b55bcbec118146c5d238079107f6f"
+ },
+ "gem": {
"category": "objects",
- "moji": "🖍",
- "description": "lower left crayon",
+ "moji": "💎",
+ "description": "gem stone",
+ "unicodeVersion": "6.0",
+ "digest": "abb9f5097963624e3e1e0ef7dd973a264910c22baa549d24f396defc0fcd8ef6"
+ },
+ "mute": {
+ "category": "symbols",
+ "moji": "🔇",
+ "description": "speaker with cancellation stroke",
+ "unicodeVersion": "6.0",
+ "digest": "5f80daf97d6bffc8451bbf9b4bc3780a2b555e9449c23922a84feef93a238953"
+ },
+ "speaker": {
+ "category": "symbols",
+ "moji": "🔈",
+ "description": "speaker",
+ "unicodeVersion": "6.0",
+ "digest": "f83fd9518675bb83fa037a49d762f774eccea08f2a981dc85c37c46a6621cd6d"
+ },
+ "sound": {
+ "category": "symbols",
+ "moji": "🔉",
+ "description": "speaker with one sound wave",
+ "unicodeVersion": "6.0",
+ "digest": "c1e588da701bb5e139cfb4c8e068b8785d4ac08afc255792d28b06c7e0e565a9"
+ },
+ "loud_sound": {
+ "category": "symbols",
+ "moji": "🔊",
+ "description": "speaker with three sound waves",
+ "unicodeVersion": "6.0",
+ "digest": "6d6affb03b43fbfa71796d83521735af6038bfd236f3dacb77d2097496a73a01"
+ },
+ "loudspeaker": {
+ "category": "symbols",
+ "moji": "📢",
+ "description": "public address loudspeaker",
+ "unicodeVersion": "6.0",
+ "digest": "04f791bc8d3eb6486448deb2654d9a0cd1c19f946ba623116f556f27399b34a0"
+ },
+ "mega": {
+ "category": "symbols",
+ "moji": "📣",
+ "description": "cheering megaphone",
+ "unicodeVersion": "6.0",
+ "digest": "f28088b3880bf25bb1daa9d624ff3d58d8ab768c1d442ab8a9bfec4032b47343"
+ },
+ "postal_horn": {
+ "category": "objects",
+ "moji": "📯",
+ "description": "postal horn",
+ "unicodeVersion": "6.0",
+ "digest": "0cee35210e86afe9bdf0e60345d0c8c23c25d0b43e74bfd4bb8f76e223ffdfe3"
+ },
+ "bell": {
+ "category": "symbols",
+ "moji": "🔔",
+ "description": "bell",
+ "unicodeVersion": "6.0",
+ "digest": "dae95427928c10693b249c1d324838702dd43295658faa85a9687ae4ea8cc36d"
+ },
+ "no_bell": {
+ "category": "symbols",
+ "moji": "🔕",
+ "description": "bell with cancellation stroke",
+ "unicodeVersion": "6.0",
+ "digest": "d04d96e4032e288d191079579c2cc3a24e41e02cc4bb681e85cd0f45e01af0a4"
+ },
+ "musical_score": {
+ "category": "activity",
+ "moji": "🎼",
+ "description": "musical score",
+ "unicodeVersion": "6.0",
+ "digest": "a379d47ab59308c0faabf74167d53085595a491ced7b1c2d8b9e9fc53575a650"
+ },
+ "musical_note": {
+ "category": "symbols",
+ "moji": "🎵",
+ "description": "musical note",
+ "unicodeVersion": "6.0",
+ "digest": "9204e70e4d68df5d8ab44087881c90c6d6cce0ac0403d767781cfb8ec23f3a98"
+ },
+ "notes": {
+ "category": "symbols",
+ "moji": "🎶",
+ "description": "multiple musical notes",
+ "unicodeVersion": "6.0",
+ "digest": "e18a0e7c520b862def1df85e1d56ef4a18f42e9998a2e9a000045cfddb6bf75d"
+ },
+ "microphone2": {
+ "category": "objects",
+ "moji": "🎙",
+ "description": "studio microphone",
"unicodeVersion": "7.0",
- "digest": "d2d609470d943ad4a320273180eb9c9bced90c472e24cac3f0a52f238f8caaf9"
+ "digest": "c1b14280e7a0bfd9ee4c8f83024f9cbf5a15a5bdadf3a4795f101864a4ad2c6d"
},
- "credit_card": {
+ "level_slider": {
"category": "objects",
- "moji": "💳",
- "description": "credit card",
+ "moji": "🎚",
+ "description": "level slider",
+ "unicodeVersion": "7.0",
+ "digest": "d57965d6b267a20a4529adbc71dd4efcb7f5e8f1146c03746b65922cd3f271c6"
+ },
+ "control_knobs": {
+ "category": "objects",
+ "moji": "🎛",
+ "description": "control knobs",
+ "unicodeVersion": "7.0",
+ "digest": "437abc26fe18f903c498b5223ab35de813b8166e8460cfe9bc5b59177d949bc1"
+ },
+ "microphone": {
+ "category": "activity",
+ "moji": "🎤",
+ "description": "microphone",
"unicodeVersion": "6.0",
- "digest": "b96a52f41dfd125f63f3f927e8333db5dd31388e3b9cfce4664bad8bc6a750a3"
+ "digest": "5d7d70a4347d677eeed8dcd0f87f9ed4b10a14a6ebeb9279eab9d6ed4d505d28"
},
- "crescent_moon": {
- "category": "nature",
- "moji": "🌙",
- "description": "crescent moon",
+ "headphones": {
+ "category": "activity",
+ "moji": "🎧",
+ "description": "headphone",
"unicodeVersion": "6.0",
- "digest": "bab44314af40c16b245f2e91b2cec98592cddda05630012ac9d6554e1d9b8b43"
+ "digest": "ca903e1013a0be982e3281dd357b9acf3cccd41db7021385a6d4d84ce1f31c04"
},
- "cricket": {
+ "radio": {
+ "category": "objects",
+ "moji": "📻",
+ "description": "radio",
+ "unicodeVersion": "6.0",
+ "digest": "a397a47cb1ab6621628112afcdc7ce4338f9e77954f2ef7e9bb461d519705099"
+ },
+ "saxophone": {
"category": "activity",
- "moji": "🏏",
- "description": "cricket bat and ball",
- "unicodeVersion": "10.0",
- "digest": "c0447966114b1db5c9d1dd5e47bd7f85a19accb8c6daeb9984644834e72e7598"
+ "moji": "🎷",
+ "description": "saxophone",
+ "unicodeVersion": "6.0",
+ "digest": "e49b31381a32c4ddf5b47b631e3c260bc62c793084b3cf026af5bea3105b6e0e"
},
- "crocodile": {
- "category": "nature",
- "moji": "🐊",
- "description": "crocodile",
+ "guitar": {
+ "category": "activity",
+ "moji": "🎸",
+ "description": "guitar",
"unicodeVersion": "6.0",
- "digest": "94f36ae30277cc802bbe827c727358540548ebdd85d3e1f70e976e205a885756"
+ "digest": "c51339829b282ea6007d42be71949ff646bb978d5e8989d15b21857aeb2b5f12"
},
- "croissant": {
- "category": "food",
- "moji": "🥐",
- "description": "croissant",
+ "musical_keyboard": {
+ "category": "activity",
+ "moji": "🎹",
+ "description": "musical keyboard",
+ "unicodeVersion": "6.0",
+ "digest": "85f8bb1668f64cde65c886b207ae588a794e98b6b64dfaee96659b80bc42b644"
+ },
+ "trumpet": {
+ "category": "activity",
+ "moji": "🎺",
+ "description": "trumpet",
+ "unicodeVersion": "6.0",
+ "digest": "21803bcdcfd7681ce77b8fdd30b115a4f67fb8f2cb91908ac9eb02613c413719"
+ },
+ "violin": {
+ "category": "activity",
+ "moji": "🎻",
+ "description": "violin",
+ "unicodeVersion": "6.0",
+ "digest": "3adf18cfe5778c508be936827ff1812503e02b8ebaf3db45053f0fab962cca39"
+ },
+ "drum": {
+ "category": "activity",
+ "moji": "🥁",
+ "description": "drum with drumsticks",
"unicodeVersion": "9.0",
- "digest": "8524b964d39d5193569c03177162dc3bf2d6b2e1d4b5ba2b651cd906199f053f"
+ "digest": "ba6e2d42af351338819b48803792d5dd1597e831fbae53d2f2aaef275306c8ed"
},
- "cross": {
- "category": "symbols",
- "moji": "✝",
- "description": "latin cross",
+ "iphone": {
+ "category": "objects",
+ "moji": "📱",
+ "description": "mobile phone",
+ "unicodeVersion": "6.0",
+ "digest": "6ef1372438c96383e375ab88a965db818efe0e3ae4b87f49870487c72f62ab9f"
+ },
+ "calling": {
+ "category": "objects",
+ "moji": "📲",
+ "description": "mobile phone with rightwards arrow at left",
+ "unicodeVersion": "6.0",
+ "digest": "3fc285fd0fc7a6beb6c64091671d0f789458e32aaba9d9fd74dfac16b9549826"
+ },
+ "telephone": {
+ "category": "objects",
+ "moji": "☎",
+ "description": "black telephone",
"unicodeVersion": "1.1",
- "digest": "4c96506b864fefcb67c75ab76262065ca34dc247bdf0a65a859a7c18051aa8be"
+ "digest": "63655f8f945e2b0a7bb235b7b2d118db845bea995f5d04adab25b9b1ea7fc9ae"
},
- "crossed_flags": {
+ "telephone_receiver": {
"category": "objects",
- "moji": "🎌",
- "description": "crossed flags",
+ "moji": "📞",
+ "description": "telephone receiver",
"unicodeVersion": "6.0",
- "digest": "451e17cbb40af3f42c7f687186c3ba7d8c9a99826635f3733256c663a0be31cb"
+ "digest": "83bc544dc191e7145a4d59059780fbd5c020c64f090de11816d681a504d65472"
},
- "crossed_swords": {
+ "pager": {
"category": "objects",
- "moji": "⚔",
- "description": "crossed swords",
- "unicodeVersion": "4.1",
- "digest": "a69a11c517efe421e0c36c3e231520967e8791d41b7a83370229e166b7a25d13"
+ "moji": "📟",
+ "description": "pager",
+ "unicodeVersion": "6.0",
+ "digest": "196d7a020ab2bb15c8d2ae723c8c8efe161d0e50109af3238727654956c554e5"
},
- "crown": {
- "category": "people",
- "moji": "👑",
- "description": "crown",
+ "fax": {
+ "category": "objects",
+ "moji": "📠",
+ "description": "fax machine",
"unicodeVersion": "6.0",
- "digest": "6fd07901b1aedf7f0fe7ebcdbd6f7fe40f768a66a0e862203274097b16b5431d"
+ "digest": "cf7823d54b57edbf46384c7833efc4ce9ddef29786d74a89a8af1e8ed2e7d599"
},
- "cruise_ship": {
- "category": "travel",
- "moji": "🛳",
- "description": "passenger ship",
+ "battery": {
+ "category": "objects",
+ "moji": "🔋",
+ "description": "battery",
+ "unicodeVersion": "6.0",
+ "digest": "8c50a487dfc349dd3b57ab4000e892efaeec967e5862485b3a023fb63c8c2949"
+ },
+ "electric_plug": {
+ "category": "objects",
+ "moji": "🔌",
+ "description": "electric plug",
+ "unicodeVersion": "6.0",
+ "digest": "78feabb40b541ad46e396f9508de72a52f0e4ca2f48717078eea100b3c125a24"
+ },
+ "computer": {
+ "category": "objects",
+ "moji": "💻",
+ "description": "personal computer",
+ "unicodeVersion": "6.0",
+ "digest": "08200fc0c04bb776d3b7e187de415a1a6a6bc82a7beb70829e03c40c5312d7d1"
+ },
+ "desktop": {
+ "category": "objects",
+ "moji": "🖥",
+ "description": "desktop computer",
"unicodeVersion": "7.0",
- "digest": "8dc923a465e8183b0a1410f810825c190e494c77b01de7c2f5ee0a64f7580206"
+ "digest": "a4d1664692b50035f04b1236960a59c61fd5bedd35b77ce0772aabb776631fa1"
},
- "cry": {
- "category": "people",
- "moji": "😢",
- "description": "crying face",
+ "printer": {
+ "category": "objects",
+ "moji": "🖨",
+ "description": "printer",
+ "unicodeVersion": "7.0",
+ "digest": "6d955ef121cac0fb8f24e617c664e13b44cb318db041be39ebbbd158e731ae2f"
+ },
+ "keyboard": {
+ "category": "objects",
+ "moji": "⌨",
+ "description": "keyboard",
+ "unicodeVersion": "1.1",
+ "digest": "0f3ca37b19de485983e39b02db94b0f6243d94081918703510e020ef1d269810"
+ },
+ "mouse_three_button": {
+ "category": "objects",
+ "moji": "🖱",
+ "description": "three button mouse",
+ "unicodeVersion": "7.0",
+ "digest": "aea26b9ebd3ea81d0e8b56c8a0281a94e9862e9deda989ec679150e7aec36b5b"
+ },
+ "trackball": {
+ "category": "objects",
+ "moji": "🖲",
+ "description": "trackball",
+ "unicodeVersion": "7.0",
+ "digest": "80003d0b886adf81afc07427b73b6ba462032c9fad318d1dcc142eae6e943238"
+ },
+ "minidisc": {
+ "category": "objects",
+ "moji": "💽",
+ "description": "minidisc",
"unicodeVersion": "6.0",
- "digest": "5b16d711fc3d2e611dbd406e5849c22ebdad9c9812fd6897f304a89f21f05755"
+ "digest": "5b6ddb7b5242dffdc04755e8dcac4cc3ae237040481bf076a8c1f95ce0ada3b6"
},
- "crying_cat_face": {
- "category": "people",
- "moji": "😿",
- "description": "crying cat face",
+ "floppy_disk": {
+ "category": "objects",
+ "moji": "💾",
+ "description": "floppy disk",
"unicodeVersion": "6.0",
- "digest": "32cc70455196cddbd0664789c5b1dc7d777a81884b748bdf43c8de6cd892d498"
+ "digest": "24b4d593478f5619160a95967deb34662d0b1a3f6da96f84910d749914179e13"
},
- "crystal_ball": {
+ "cd": {
"category": "objects",
- "moji": "🔮",
- "description": "crystal ball",
+ "moji": "💿",
+ "description": "optical disc",
"unicodeVersion": "6.0",
- "digest": "225f048f2fefb378d3178931ad5e5e8feb419a19436e45d03c199bd726ec5151"
+ "digest": "d7242edbba09de17d223d7e3178a241b1aafe05d14a19000428522dc9b2891f4"
},
- "cucumber": {
- "category": "food",
- "moji": "🥒",
- "description": "cucumber",
- "unicodeVersion": "9.0",
- "digest": "752ddddf7b18831b74cc2b14b3fd787f9f914c1a9e72b7119f2e0b7267e4f749"
+ "dvd": {
+ "category": "objects",
+ "moji": "📀",
+ "description": "dvd",
+ "unicodeVersion": "6.0",
+ "digest": "06080cd1b46afcbdd854c06936389fc990aec029dd6909345861ff18d2275045"
},
- "cupid": {
- "category": "symbols",
- "moji": "💘",
- "description": "heart with arrow",
+ "movie_camera": {
+ "category": "objects",
+ "moji": "🎥",
+ "description": "movie camera",
"unicodeVersion": "6.0",
- "digest": "c13b5e7a7a9824b3921b07805ffc08bb07e01f5384c49909ac375e9f55a3102c"
+ "digest": "f82751e6f069482fbc01b0d2d135fdf58ef3cf58526b7dbf5b50a71b5ea0ae75"
},
- "curly_loop": {
- "category": "symbols",
- "moji": "➰",
- "description": "curly loop",
+ "film_frames": {
+ "category": "objects",
+ "moji": "🎞",
+ "description": "film frames",
+ "unicodeVersion": "7.0",
+ "digest": "ddc2c8bf00765b8a91bff22a06dba1cef2a78d89afb9515e55dc417cfcba570e"
+ },
+ "projector": {
+ "category": "objects",
+ "moji": "📽",
+ "description": "film projector",
+ "unicodeVersion": "7.0",
+ "digest": "ad03ae86f9188ccc2909335de034ed0de68dadc4a6e59fb678e752d219c41f92"
+ },
+ "clapper": {
+ "category": "activity",
+ "moji": "🎬",
+ "description": "clapper board",
"unicodeVersion": "6.0",
- "digest": "365b37a4afb451ace34de1a59727467bfe6f501437dc3459ec55a0234d98e307"
+ "digest": "73820f057b2d7d50f6be4a51c0ebaa39a4e0463f0f1121471fb3816fdd05f5bd"
},
- "currency_exchange": {
- "category": "symbols",
- "moji": "💱",
- "description": "currency exchange",
+ "tv": {
+ "category": "objects",
+ "moji": "📺",
+ "description": "television",
"unicodeVersion": "6.0",
- "digest": "3703307c690bd113fbe5826720c6466ee9dccc80ef8d3556cfe2ba7401240df8"
+ "digest": "86ab2eef07bed93f56e29e20ef67d566ec7b4b1e96463ba3f9614a1db28aec11"
},
- "curry": {
- "category": "food",
- "moji": "🍛",
- "description": "curry and rice",
+ "camera": {
+ "category": "objects",
+ "moji": "📷",
+ "description": "camera",
"unicodeVersion": "6.0",
- "digest": "6c317ba8a3dba9a294c2a3cb3b6385c946549a06bf934fd4b6964304f3b9a9b3"
+ "digest": "5d86baa4baede9c6d02fbe02fd2d4ad07a47a77e09c1d2d045ea20b36bd2938c"
},
- "custard": {
- "category": "food",
- "moji": "🍮",
- "description": "custard",
+ "camera_with_flash": {
+ "category": "objects",
+ "moji": "📸",
+ "description": "camera with flash",
+ "unicodeVersion": "7.0",
+ "digest": "9b701460de9ba318811d771bb451fb109850bc08ad6592f9365bc350de571f9f"
+ },
+ "video_camera": {
+ "category": "objects",
+ "moji": "📹",
+ "description": "video camera",
"unicodeVersion": "6.0",
- "digest": "9790383c5841ec63d87182fc0f4dd05862c973b0e1b9f2fc2c047654089a404c"
+ "digest": "b297b7675c9e69638a29f5332a4024fd68675101d03a059d90f151534de1a3b5"
},
- "customs": {
- "category": "symbols",
- "moji": "🛃",
- "description": "customs",
+ "vhs": {
+ "category": "objects",
+ "moji": "📼",
+ "description": "videocassette",
"unicodeVersion": "6.0",
- "digest": "0f374b6f60c5bc6da41b0682a352304bc258ef6274da8afd6820ff9bb50a5c39"
+ "digest": "7fbd915d2b660e32fc5720179a113deea2997601f8dc0f7dcf4ec754f4d8d17b"
},
- "cyclone": {
- "category": "symbols",
- "moji": "🌀",
- "description": "cyclone",
+ "mag": {
+ "category": "objects",
+ "moji": "🔍",
+ "description": "left-pointing magnifying glass",
"unicodeVersion": "6.0",
- "digest": "ead05cbe9adcf38310712af602c8caea12eac26e87756579efb8c542dde8b391"
+ "digest": "bfb8b2d3ef82281c7b821c3814c8213d6f853322546f0107575dedc20de31559"
},
- "dagger": {
+ "mag_right": {
"category": "objects",
- "moji": "🗡",
- "description": "dagger knife",
+ "moji": "🔎",
+ "description": "right-pointing magnifying glass",
+ "unicodeVersion": "6.0",
+ "digest": "3ee06dcf290a822c5084d8e0cf245a97b354fb03ff0b5f4ae2a7063a6c967ce7"
+ },
+ "candle": {
+ "category": "objects",
+ "moji": "🕯",
+ "description": "candle",
"unicodeVersion": "7.0",
- "digest": "6038c5c863bdacbade2f2426a48bd530e286d6624813960ae0e390d71d4cd58e"
+ "digest": "67400dd5e800a6e496a661da639d4964bf6646525bcf6d6c4773529ae5f7f075"
},
- "dancer": {
- "category": "people",
- "moji": "💃",
- "description": "dancer",
+ "bulb": {
+ "category": "objects",
+ "moji": "💡",
+ "description": "electric light bulb",
"unicodeVersion": "6.0",
- "digest": "21117e63374e501b1daf8350d5921314cc8b306558c8d6803d43c71066438648"
+ "digest": "2cdc43e4bb17fc00c903a911e065fe1bb96f4f57103a3836cfa908c4618e6d45"
},
- "dancer_tone1": {
- "category": "people",
- "moji": "💃🏻",
- "description": "dancer tone 1",
- "unicodeVersion": "8.0",
- "digest": "35c64d92577272999d1151cacb584e248a2a005ce9388ce64250bcc8a7178dc1"
+ "flashlight": {
+ "category": "objects",
+ "moji": "🔦",
+ "description": "electric torch",
+ "unicodeVersion": "6.0",
+ "digest": "2fd24a87d1609384d671d0d6b6c5d27afde221e28e0ce1443f09b8ab96200329"
},
- "dancer_tone2": {
- "category": "people",
- "moji": "💃🏼",
- "description": "dancer tone 2",
- "unicodeVersion": "8.0",
- "digest": "9dfdd28db7267f172b1bae228216d0111583c4903934e4afa081cdbf83facd45"
+ "izakaya_lantern": {
+ "category": "objects",
+ "moji": "🏮",
+ "description": "izakaya lantern",
+ "unicodeVersion": "6.0",
+ "digest": "de36e5f5fe5da0c922e194b1ae93ed07039620b83e518a259c374563a166f4bf"
},
- "dancer_tone3": {
- "category": "people",
- "moji": "💃🏽",
- "description": "dancer tone 3",
- "unicodeVersion": "8.0",
- "digest": "79b7c1e313b235e0acec3ff50d3eb7ecff53b5ae4a2ba7d363b0992fa939ac9b"
+ "notebook_with_decorative_cover": {
+ "category": "objects",
+ "moji": "📔",
+ "description": "notebook with decorative cover",
+ "unicodeVersion": "6.0",
+ "digest": "6054dca42cc303e7a8d89111d06aaca38950e179a348fc1a2a7c3405401de8a0"
},
- "dancer_tone4": {
- "category": "people",
- "moji": "💃🏾",
- "description": "dancer tone 4",
- "unicodeVersion": "8.0",
- "digest": "16e6f14fef6464ac20273713c196e189eab75f3ecab98b9eee172bc33c8f2847"
+ "closed_book": {
+ "category": "objects",
+ "moji": "📕",
+ "description": "closed book",
+ "unicodeVersion": "6.0",
+ "digest": "2b60f7ca436be9aa12cc95d9733e3c72cf36ca7c98a83be70748d1452fa758da"
},
- "dancer_tone5": {
- "category": "people",
- "moji": "💃🏿",
- "description": "dancer tone 5",
- "unicodeVersion": "8.0",
- "digest": "c090f007657b775acab02656ad3de9d02f97373d6b897a7052e1aa5b032de2ee"
+ "book": {
+ "category": "objects",
+ "moji": "📖",
+ "description": "open book",
+ "unicodeVersion": "6.0",
+ "digest": "07f1af90a8f2bdddaa585b4da1da9ac7c52dbb7365d409d264e2b66f43b165d7"
},
- "dancers": {
- "category": "people",
- "moji": "👯",
- "description": "woman with bunny ears",
+ "green_book": {
+ "category": "objects",
+ "moji": "📗",
+ "description": "green book",
"unicodeVersion": "6.0",
- "digest": "8449ee0de1754c317e82371ae80d7f6b840b69b657960953a50ddcb918f363c2"
+ "digest": "6a638f78d0ccb18907a2606afb45e9d32852fbaeee2e41250432e102b8366984"
},
- "dango": {
- "category": "food",
- "moji": "🍡",
- "description": "dango",
+ "blue_book": {
+ "category": "objects",
+ "moji": "📘",
+ "description": "blue book",
"unicodeVersion": "6.0",
- "digest": "c3047205e462d958f6e5ab2ad318bc70e903343c4a87956055f76ea8adcc9456"
+ "digest": "bc47b3b8f1bfa21c2a4bf185cc247e0c9fc7f4f221292c3b0286e13b32f7a6da"
},
- "dark_sunglasses": {
- "category": "people",
- "moji": "🕶",
- "description": "dark sunglasses",
- "unicodeVersion": "7.0",
- "digest": "0f941dc4ed9dcc861863176f49812966bcae7e3179044c1c8fa22de9ed8dbd09"
+ "orange_book": {
+ "category": "objects",
+ "moji": "📙",
+ "description": "orange book",
+ "unicodeVersion": "6.0",
+ "digest": "b2744b07ad93a32b7f114bb8e11f0b26445ce0b2b4edf4720f14fc32da3b3cce"
},
- "dart": {
- "category": "activity",
- "moji": "🎯",
- "description": "direct hit",
+ "books": {
+ "category": "objects",
+ "moji": "📚",
+ "description": "books",
"unicodeVersion": "6.0",
- "digest": "01d4bb3f51417e7861404746bbebc4704edf5fa80bde6ad9cf9e12c238a1206c"
+ "digest": "566cb93cf32782ae39aec670e22b711fa33afd52bc75c9e7069174cd252125d1"
},
- "dash": {
- "category": "nature",
- "moji": "💨",
- "description": "dash symbol",
+ "notebook": {
+ "category": "objects",
+ "moji": "📓",
+ "description": "notebook",
"unicodeVersion": "6.0",
- "digest": "6c0ab681346b90d7b75e1e16531890a6ebed5c7b8bc63b269cc4a6080328fd6f"
+ "digest": "aeb529f82e76ab543f7ab44ef530cbc3558b586559b9e04a6fddc0a0a06ebbb2"
},
- "date": {
+ "ledger": {
"category": "objects",
- "moji": "📅",
- "description": "calendar",
+ "moji": "📒",
+ "description": "ledger",
"unicodeVersion": "6.0",
- "digest": "3d0a59825a932b1a12f6b0980aaabc93b77f55499d5c6d81f61cf03dbb7188cb"
+ "digest": "826379f5164a8c3a10ec43ae1296f994ffd40bc3eb6c0595117993eff65f8f2a"
},
- "deciduous_tree": {
- "category": "nature",
- "moji": "🌳",
- "description": "deciduous tree",
+ "page_with_curl": {
+ "category": "objects",
+ "moji": "📃",
+ "description": "page with curl",
"unicodeVersion": "6.0",
- "digest": "89804981f62825a45150919436442c70838d90a015de398d2bb9aaffa4b2e998"
+ "digest": "da5752233d152e49c5fac55d89b82fbd1e98b5a6d7f2f574fe7d218879899282"
},
- "deer": {
- "category": "nature",
- "moji": "🦌",
- "description": "deer",
- "unicodeVersion": "9.0",
- "digest": "83a9c2a0bfb5d82631dc653337279545d72ccea10fb2ccf6da6ab82586fb1d56"
+ "scroll": {
+ "category": "objects",
+ "moji": "📜",
+ "description": "scroll",
+ "unicodeVersion": "6.0",
+ "digest": "abdb7024b5f50b04389b1a65e5cb5a3b7929f4e9c5719388684603210774b3c1"
},
- "department_store": {
- "category": "travel",
- "moji": "🏬",
- "description": "department store",
+ "page_facing_up": {
+ "category": "objects",
+ "moji": "📄",
+ "description": "page facing up",
"unicodeVersion": "6.0",
- "digest": "0cbab31777aeecc148e54777e1a65e794eb4b5f45e0f8ecaffa990efd06a0fb2"
+ "digest": "d8aa1a6d309f70108aef98ee066419652a91fe5d793a1230f2de922ef6275f03"
},
- "desert": {
- "category": "travel",
- "moji": "🏜",
- "description": "desert",
+ "newspaper": {
+ "category": "objects",
+ "moji": "📰",
+ "description": "newspaper",
+ "unicodeVersion": "6.0",
+ "digest": "591892ad44fd9168ec274c76bf212a15020dd91091623f79526514b9d83f84c3"
+ },
+ "newspaper2": {
+ "category": "objects",
+ "moji": "🗞",
+ "description": "rolled-up newspaper",
"unicodeVersion": "7.0",
- "digest": "c60bd12a5864c0789c2e8fb93a392a42600e08cc23450c716c0bd008c16e1c93"
+ "digest": "a2d8413b95004751aa47bbb80bd5bd067b0591cf1c832ecb6b7911b88be7043c"
},
- "desktop": {
+ "bookmark_tabs": {
"category": "objects",
- "moji": "🖥",
- "description": "desktop computer",
+ "moji": "📑",
+ "description": "bookmark tabs",
+ "unicodeVersion": "6.0",
+ "digest": "78450d6c894fee9badbed92a07144718908a305be825304dc47839e259884e9c"
+ },
+ "bookmark": {
+ "category": "objects",
+ "moji": "🔖",
+ "description": "bookmark",
+ "unicodeVersion": "6.0",
+ "digest": "cafb404fab72e67a7c0254ebe91f9631be9a6f7da763bbd948358e8544e2fd53"
+ },
+ "label": {
+ "category": "objects",
+ "moji": "🏷",
+ "description": "label",
"unicodeVersion": "7.0",
- "digest": "a4d1664692b50035f04b1236960a59c61fd5bedd35b77ce0772aabb776631fa1"
+ "digest": "ad9f3bfc709138edfb61c692d96c1ca08b0bd97203d370342e7b70f46ae23aa8"
},
- "diamond_shape_with_a_dot_inside": {
- "category": "symbols",
- "moji": "💠",
- "description": "diamond shape with a dot inside",
+ "moneybag": {
+ "category": "objects",
+ "moji": "💰",
+ "description": "money bag",
"unicodeVersion": "6.0",
- "digest": "785dedaf65b4c8a8b2a758608563528cae6d65864e36b17a45b7b4b15fce67e5"
+ "digest": "5fbc74d9eb713ca5d00c8ecb528361bc22c24ab97fdab6606427acc5af480c07"
},
- "diamonds": {
+ "yen": {
+ "category": "objects",
+ "moji": "💴",
+ "description": "banknote with yen sign",
+ "unicodeVersion": "6.0",
+ "digest": "b12182adf55be97d7d5e52176bb149c16f57d0645f508dcb97efbeab4607d439"
+ },
+ "dollar": {
+ "category": "objects",
+ "moji": "💵",
+ "description": "banknote with dollar sign",
+ "unicodeVersion": "6.0",
+ "digest": "8e7ee431f4c5afac19aa8d93a433619c96952178e6cf7e20c6e6e0768cccf7e0"
+ },
+ "euro": {
+ "category": "objects",
+ "moji": "💶",
+ "description": "banknote with euro sign",
+ "unicodeVersion": "6.0",
+ "digest": "5c08342b52ba2417f1daf6c357107eddc3ed9e46ed82443665859dba944b0754"
+ },
+ "pound": {
+ "category": "objects",
+ "moji": "💷",
+ "description": "banknote with pound sign",
+ "unicodeVersion": "6.0",
+ "digest": "2cb892a8131edb282cb8f5dadaa8db602e6b6934fdf06588d8ccb8f3e7e6eae6"
+ },
+ "money_with_wings": {
+ "category": "objects",
+ "moji": "💸",
+ "description": "money with wings",
+ "unicodeVersion": "6.0",
+ "digest": "3d2b0e5939f92d0ab9e40758a9ec239817ad6e9463fd2140803ee229bdc95720"
+ },
+ "credit_card": {
+ "category": "objects",
+ "moji": "💳",
+ "description": "credit card",
+ "unicodeVersion": "6.0",
+ "digest": "b96a52f41dfd125f63f3f927e8333db5dd31388e3b9cfce4664bad8bc6a750a3"
+ },
+ "chart": {
"category": "symbols",
- "moji": "♦",
- "description": "black diamond suit",
+ "moji": "💹",
+ "description": "chart with upwards trend and yen sign",
+ "unicodeVersion": "6.0",
+ "digest": "b513f5b5ff0e9a8139aaa378982e1937b09da1827adef70158d07d8c4a288139"
+ },
+ "envelope": {
+ "category": "objects",
+ "moji": "✉",
+ "description": "envelope",
"unicodeVersion": "1.1",
- "digest": "77c773b770a293129c33513fc22124c6953db07d1b254905b0c734c853e65387"
+ "digest": "ff00c491ba40989d83d6586ebb8e18e1507cdf55e056981803c3f77d0b246314"
},
- "disappointed": {
- "category": "people",
- "moji": "😞",
- "description": "disappointed face",
+ "e-mail": {
+ "category": "objects",
+ "moji": "📧",
+ "description": "e-mail symbol",
"unicodeVersion": "6.0",
- "digest": "0e8f9fdef204d2684a92666709ff4d785d5b3690f1ff0f7ebed9f37d2a3651f5"
+ "digest": "7ca1119b9683e5e34185c9cbee9bcd59b6e24feae3a691826672c72d1d7401d1"
},
- "disappointed_relieved": {
+ "incoming_envelope": {
+ "category": "objects",
+ "moji": "📨",
+ "description": "incoming envelope",
+ "unicodeVersion": "6.0",
+ "digest": "c23f819ce0abd3b695b05051f8c9e9be72f9bf5176207106c56e30015c199252"
+ },
+ "envelope_with_arrow": {
+ "category": "objects",
+ "moji": "📩",
+ "description": "envelope with downwards arrow above",
+ "unicodeVersion": "6.0",
+ "digest": "1af1da925637995928c8f83f7f5effee40db1f5a33f1cd7d626ff9ed408bfe19"
+ },
+ "outbox_tray": {
+ "category": "objects",
+ "moji": "📤",
+ "description": "outbox tray",
+ "unicodeVersion": "6.0",
+ "digest": "9ccb7f9dfbec078899a97725772841bc3f2752352abf9fd29765a782169b2330"
+ },
+ "inbox_tray": {
+ "category": "objects",
+ "moji": "📥",
+ "description": "inbox tray",
+ "unicodeVersion": "6.0",
+ "digest": "fa79ca5efc93767858677871df687d0666cc9174e7f226d7acb65c15a4cad0df"
+ },
+ "package": {
+ "category": "objects",
+ "moji": "📦",
+ "description": "package",
+ "unicodeVersion": "6.0",
+ "digest": "06ce28df9b4abdb483f7d7eccad4a388fce4ed3596635b8ff148e7399fa99d53"
+ },
+ "mailbox": {
+ "category": "objects",
+ "moji": "📫",
+ "description": "closed mailbox with raised flag",
+ "unicodeVersion": "6.0",
+ "digest": "492df72deb2679ad387f76edf9c9e7475ef7221c5eb8e24fa6c964e5a3250a61"
+ },
+ "mailbox_closed": {
+ "category": "objects",
+ "moji": "📪",
+ "description": "closed mailbox with lowered flag",
+ "unicodeVersion": "6.0",
+ "digest": "be27aef10401f26e8f539216b01e2e0774756cb3abb9535d55f91de5415d9737"
+ },
+ "mailbox_with_mail": {
+ "category": "objects",
+ "moji": "📬",
+ "description": "open mailbox with raised flag",
+ "unicodeVersion": "6.0",
+ "digest": "bfe3df313a2f57bdb99192ce6e674044e385d044020b6e472b2414fe17649805"
+ },
+ "mailbox_with_no_mail": {
+ "category": "objects",
+ "moji": "📭",
+ "description": "open mailbox with lowered flag",
+ "unicodeVersion": "6.0",
+ "digest": "cad6a927c392ed3181284f005eb260976cf69ab6608d59a43ea252a89c89b6e1"
+ },
+ "postbox": {
+ "category": "objects",
+ "moji": "📮",
+ "description": "postbox",
+ "unicodeVersion": "6.0",
+ "digest": "3e12a4b2a7bce5c1257ecd05f69ba6f51d9db39b21f6393c304e6870c5f432da"
+ },
+ "ballot_box": {
+ "category": "objects",
+ "moji": "🗳",
+ "description": "ballot box with ballot",
+ "unicodeVersion": "7.0",
+ "digest": "375bf5ca28895dd54acc3f7927d62fed9d56ad79b5218f6f3be2533929ad47a5"
+ },
+ "pencil2": {
+ "category": "objects",
+ "moji": "✏",
+ "description": "pencil",
+ "unicodeVersion": "1.1",
+ "digest": "537f6d69a4039270d8856febea24fd3021005da6f35dca29e5491ee49cc3b217"
+ },
+ "black_nib": {
+ "category": "objects",
+ "moji": "✒",
+ "description": "black nib",
+ "unicodeVersion": "1.1",
+ "digest": "196230be1ae39d5841e9ed322f74816c61e47c4235dab7abff0e6d86fba786ed"
+ },
+ "pen_fountain": {
+ "category": "objects",
+ "moji": "🖋",
+ "description": "lower left fountain pen",
+ "unicodeVersion": "7.0",
+ "digest": "114ba929b5f8812b9767e8260112ebcebd04a46175c3d5d487d71ca36c372d40"
+ },
+ "pen_ballpoint": {
+ "category": "objects",
+ "moji": "🖊",
+ "description": "lower left ballpoint pen",
+ "unicodeVersion": "7.0",
+ "digest": "7739947fa359e641e04f51bae4ba292613fa2692b17b3f2b3c4bf27c82e97d82"
+ },
+ "paintbrush": {
+ "category": "objects",
+ "moji": "🖌",
+ "description": "lower left paintbrush",
+ "unicodeVersion": "7.0",
+ "digest": "093064c1459d5b84932aab78fdc8d597e333ab2ce73524733e42c2b609b40f6b"
+ },
+ "crayon": {
+ "category": "objects",
+ "moji": "🖍",
+ "description": "lower left crayon",
+ "unicodeVersion": "7.0",
+ "digest": "d2d609470d943ad4a320273180eb9c9bced90c472e24cac3f0a52f238f8caaf9"
+ },
+ "pencil": {
+ "category": "objects",
+ "moji": "📝",
+ "description": "memo",
+ "unicodeVersion": "6.0",
+ "digest": "d18a7c355267e820ac7694c6f7da276b19a59fd42fca4eab74b7f3653e4acdd6"
+ },
+ "briefcase": {
"category": "people",
- "moji": "😥",
- "description": "disappointed but relieved face",
+ "moji": "💼",
+ "description": "briefcase",
"unicodeVersion": "6.0",
- "digest": "cfe92ebfbaaa0b02b84b05c17c94f07db315a8771d3ebfea8043862559c6ce74"
+ "digest": "f72220b3fa933c6b503c041643543369177efc2bee094d392cbf8ee9d5cf11a0"
+ },
+ "file_folder": {
+ "category": "objects",
+ "moji": "📁",
+ "description": "file folder",
+ "unicodeVersion": "6.0",
+ "digest": "25f9840f4a2bc80558ef9ed231f88a7031bc4b0aad087a7be9748fb006a37288"
+ },
+ "open_file_folder": {
+ "category": "objects",
+ "moji": "📂",
+ "description": "open file folder",
+ "unicodeVersion": "6.0",
+ "digest": "ebcb375b6039c7a7f35fd36bc9240fc401ac9b4225922fc015f7b59773a89e95"
},
"dividers": {
"category": "objects",
@@ -2596,264 +8889,1034 @@
"unicodeVersion": "7.0",
"digest": "e0b0b297473b27e2163e7b9d38be02a98a025b9c0c679da3bb5af2af7b4d5001"
},
- "dizzy": {
- "category": "nature",
- "moji": "💫",
- "description": "dizzy symbol",
+ "date": {
+ "category": "objects",
+ "moji": "📅",
+ "description": "calendar",
"unicodeVersion": "6.0",
- "digest": "b3d16f5748abede6f133bac2104e51eaff0e59f184256aa6902ebb6d8a3e32b7"
+ "digest": "3d0a59825a932b1a12f6b0980aaabc93b77f55499d5c6d81f61cf03dbb7188cb"
},
- "dizzy_face": {
- "category": "people",
- "moji": "😵",
- "description": "dizzy face",
+ "calendar": {
+ "category": "objects",
+ "moji": "📆",
+ "description": "tear-off calendar",
"unicodeVersion": "6.0",
- "digest": "5e3c7e1f97d4d9a330b89358c89d2fbdf31a945f3be40c352600a985ac49b197"
+ "digest": "275fa5b2c113c3d117b1c8970f01dd897b90405db7a55ff6a3dda767817ed4f5"
},
- "do_not_litter": {
- "category": "symbols",
- "moji": "🚯",
- "description": "do not litter symbol",
+ "notepad_spiral": {
+ "category": "objects",
+ "moji": "🗒",
+ "description": "spiral note pad",
+ "unicodeVersion": "7.0",
+ "digest": "cff2a9d4657b423731ba9d91e366ffd774a8504650039b5757ace4c0911eda9f"
+ },
+ "calendar_spiral": {
+ "category": "objects",
+ "moji": "🗓",
+ "description": "spiral calendar pad",
+ "unicodeVersion": "7.0",
+ "digest": "01aefae6a6d0d517be52e7918d6893d55a9fa5b7480d60cb8b8b5a743573ea0b"
+ },
+ "card_index": {
+ "category": "objects",
+ "moji": "📇",
+ "description": "card index",
"unicodeVersion": "6.0",
- "digest": "13dd4fc59cd518187d2827a935ad7ca61c8f4542ff9843d882ee44c942e57045"
+ "digest": "262bfa04568a192dd8696ec43c6f916a45e16a7c19d77f830e366de0d930bb7a"
},
- "dog": {
- "category": "nature",
- "moji": "🐶",
- "description": "dog face",
+ "chart_with_upwards_trend": {
+ "category": "objects",
+ "moji": "📈",
+ "description": "chart with upwards trend",
"unicodeVersion": "6.0",
- "digest": "d6ee83df6b3a233b90c0b589c6f290164239fb4062be21566430183d9b4e507a"
+ "digest": "c09ad5dd7817106a1df65a1f8da086006de804d2de22107423a5805da5ee3f9f"
},
- "dog2": {
- "category": "nature",
- "moji": "🐕",
- "description": "dog",
+ "chart_with_downwards_trend": {
+ "category": "objects",
+ "moji": "📉",
+ "description": "chart with downwards trend",
"unicodeVersion": "6.0",
- "digest": "ba52e04ae1e024c596fc453b9af53347ecf76d342150b7701eccb83ef2df67f0"
+ "digest": "0c12dfccd80564f29dc56d3e994e55793ec02d1bd4e3109c7e1b003163579ecc"
},
- "dollar": {
+ "bar_chart": {
"category": "objects",
- "moji": "💵",
- "description": "banknote with dollar sign",
+ "moji": "📊",
+ "description": "bar chart",
"unicodeVersion": "6.0",
- "digest": "8e7ee431f4c5afac19aa8d93a433619c96952178e6cf7e20c6e6e0768cccf7e0"
+ "digest": "ba858a33edbab84f6c0505404a0f10cd89031747ab4e9104524973813d05970a"
},
- "dolls": {
+ "clipboard": {
"category": "objects",
- "moji": "🎎",
- "description": "japanese dolls",
+ "moji": "📋",
+ "description": "clipboard",
"unicodeVersion": "6.0",
- "digest": "da477dfcb1ecfab98835f4ed721354240d1a7f82e1d7df4b80e67baabb237b9f"
+ "digest": "d3d42f1ff9c8dacfd8219d9563e3fab709290d887695d3bf0a0e51afac7b7149"
},
- "dolphin": {
- "category": "nature",
- "moji": "🐬",
- "description": "dolphin",
+ "pushpin": {
+ "category": "objects",
+ "moji": "📌",
+ "description": "pushpin",
"unicodeVersion": "6.0",
- "digest": "92298c8287cbda0eaad24b50145236bb29ff9a6473a6faac07deddd8ae97ff1b"
+ "digest": "c8b820588fe733d505e95d93eeadcc3e1073f745b2b53bae8dcc1c0bb8624aa7"
},
- "door": {
+ "round_pushpin": {
"category": "objects",
- "moji": "🚪",
- "description": "door",
+ "moji": "📍",
+ "description": "round pushpin",
"unicodeVersion": "6.0",
- "digest": "4e3117984e88544efda68c78d0f90e7ba44533ba74beba28e7ea6b9718ca3225"
+ "digest": "c79c44a8563cd0777ce2f26d1dea54b283d76d4309eccfe048ba3b2ea8a9645b"
},
- "doughnut": {
- "category": "food",
- "moji": "🍩",
- "description": "doughnut",
+ "paperclip": {
+ "category": "objects",
+ "moji": "📎",
+ "description": "paperclip",
"unicodeVersion": "6.0",
- "digest": "f249df4adc65187348d3a57d110c672dbf9e5a4d7919db473aab71f24f85e9b6"
+ "digest": "dbe70e01e1d9bdb96851f18b51cc2380436ab1affe4922c1ad9f4c76811e8453"
},
- "dove": {
- "category": "nature",
- "moji": "🕊",
- "description": "dove of peace",
+ "paperclips": {
+ "category": "objects",
+ "moji": "🖇",
+ "description": "linked paperclips",
"unicodeVersion": "7.0",
- "digest": "66d335c04e2daef92ed0ee5979d6b072d99551e2950cdbc78882bf28269773f8"
+ "digest": "d71e54755ddaf37aa86ff2c1cd1d8dc55d2a5b806a0b9080168ea8e28332d46b"
},
- "dragon": {
- "category": "nature",
- "moji": "🐉",
- "description": "dragon",
+ "straight_ruler": {
+ "category": "objects",
+ "moji": "📏",
+ "description": "straight ruler",
"unicodeVersion": "6.0",
- "digest": "460c7674101d32edf90b563329947c620c1e6bea9dfe2c81483e3313248d498c"
+ "digest": "ab8b04cfbb19178452fc5eb32eea3a619049b0d46ea21b4c48023e01c30b6510"
},
- "dragon_face": {
- "category": "nature",
- "moji": "🐲",
- "description": "dragon face",
+ "triangular_ruler": {
+ "category": "objects",
+ "moji": "📐",
+ "description": "triangular ruler",
"unicodeVersion": "6.0",
- "digest": "8990c5f5a5cf7d94e4eafb7515e6367cfaeea6bd648c36d65dca1525b60d591c"
+ "digest": "8db7d5546f2c1403a5dbcd87216d15c9ded1c137def938f6cff3e14282b3b659"
},
- "dress": {
- "category": "people",
- "moji": "👗",
- "description": "dress",
+ "scissors": {
+ "category": "objects",
+ "moji": "✂",
+ "description": "black scissors",
+ "unicodeVersion": "1.1",
+ "digest": "295ce7c48d3f8e58daa454f18d534146bfaf98d46614e2783535927f23d22215"
+ },
+ "card_box": {
+ "category": "objects",
+ "moji": "🗃",
+ "description": "card file box",
+ "unicodeVersion": "7.0",
+ "digest": "25acb25ee37044c67b69ce3a7cb412f7ec2fad76009b9e022cf45b58e0d1d898"
+ },
+ "file_cabinet": {
+ "category": "objects",
+ "moji": "🗄",
+ "description": "file cabinet",
+ "unicodeVersion": "7.0",
+ "digest": "29cf5c9587eb08a6736e37a6c7e01ad7e1453dd770c02c743604c2abf314d90f"
+ },
+ "wastebasket": {
+ "category": "objects",
+ "moji": "🗑",
+ "description": "wastebasket",
+ "unicodeVersion": "7.0",
+ "digest": "2a099e1431b96a2de10f79ddb319a1bcdc74d5bd7d05dee9a27b67274e4d86b0"
+ },
+ "lock": {
+ "category": "objects",
+ "moji": "🔒",
+ "description": "lock",
"unicodeVersion": "6.0",
- "digest": "9a1c024dd6f402a2764ad097eeb5656488df5bd60905e8deb04dc3f91ab890f4"
+ "digest": "63408513eaf29059c6025981c27434f71fa2be87dcd060dd9f971d0e54a73922"
},
- "dromedary_camel": {
- "category": "nature",
- "moji": "🐪",
- "description": "dromedary camel",
+ "unlock": {
+ "category": "objects",
+ "moji": "🔓",
+ "description": "open lock",
"unicodeVersion": "6.0",
- "digest": "8401968c38aa200463e4f8dd68287cff7ccc0c2be93c425329ccc56bcfa1b672"
+ "digest": "129328bd903055394ded6f0427d24c8f4ffc1a8ccb0cc831d1343b5c6c8d4416"
},
- "drooling_face": {
- "category": "people",
- "moji": "🤤",
- "description": "drooling face",
- "unicodeVersion": "9.0",
- "digest": "bed3de639ae375a5683806f5661cda66790f0e991912044ec6d8bcdf6ab56b55"
+ "lock_with_ink_pen": {
+ "category": "objects",
+ "moji": "🔏",
+ "description": "lock with ink pen",
+ "unicodeVersion": "6.0",
+ "digest": "85c1713e44becc6543464d2ff4a07dc46b25a4e40fe233695055236d7c4f62aa"
},
- "droplet": {
- "category": "nature",
- "moji": "💧",
- "description": "droplet",
+ "closed_lock_with_key": {
+ "category": "objects",
+ "moji": "🔐",
+ "description": "closed lock with key",
"unicodeVersion": "6.0",
- "digest": "df3a38da24ffe4b245eaffe3a16c9f8d995a23f07030d4de3d7244a057939a5a"
+ "digest": "5772854e275a722c97338858e416961e9812c19a6e908323f02d400158fbd6da"
},
- "drum": {
+ "key": {
+ "category": "objects",
+ "moji": "🔑",
+ "description": "key",
+ "unicodeVersion": "6.0",
+ "digest": "8f2ac6bfd01430b2350a91b747c58d9d7a20e58096e22f1b73ea8e1d53dd2ac7"
+ },
+ "key2": {
+ "category": "objects",
+ "moji": "🗝",
+ "description": "old key",
+ "unicodeVersion": "7.0",
+ "digest": "f1e9a01ce355b9be051eca6f2211d542f190a77ccb95e1d5b148dca8da422bce"
+ },
+ "hammer": {
+ "category": "objects",
+ "moji": "🔨",
+ "description": "hammer",
+ "unicodeVersion": "6.0",
+ "digest": "ad9c4c0d7613bf9022426fbd46fe1966c98a46222f81fc13402a93d5101ba3ca"
+ },
+ "pick": {
+ "category": "objects",
+ "moji": "⛏",
+ "description": "pick",
+ "unicodeVersion": "5.2",
+ "digest": "c74411a556653f5b4b0825627089ab96c01ecdd8d472697c961c22c8a99a6265"
+ },
+ "hammer_pick": {
+ "category": "objects",
+ "moji": "⚒",
+ "description": "hammer and pick",
+ "unicodeVersion": "4.1",
+ "digest": "e5fd4464456d11cfe9fb50c460cf784174a36013a81771d029c8cfbb73c5ce9a"
+ },
+ "tools": {
+ "category": "objects",
+ "moji": "🛠",
+ "description": "hammer and wrench",
+ "unicodeVersion": "7.0",
+ "digest": "f74b73f7a143d6e5a4efe0b293d998d7dfb681b23f0d6764137c6d9373a28374"
+ },
+ "dagger": {
+ "category": "objects",
+ "moji": "🗡",
+ "description": "dagger knife",
+ "unicodeVersion": "7.0",
+ "digest": "6038c5c863bdacbade2f2426a48bd530e286d6624813960ae0e390d71d4cd58e"
+ },
+ "crossed_swords": {
+ "category": "objects",
+ "moji": "⚔",
+ "description": "crossed swords",
+ "unicodeVersion": "4.1",
+ "digest": "a69a11c517efe421e0c36c3e231520967e8791d41b7a83370229e166b7a25d13"
+ },
+ "bomb": {
+ "category": "objects",
+ "moji": "💣",
+ "description": "bomb",
+ "unicodeVersion": "6.0",
+ "digest": "c06294f2bc2ca60c09544598a699662609f41bd06efa01d92d8ab7b48356b830"
+ },
+ "bow_and_arrow": {
"category": "activity",
- "moji": "🥁",
- "description": "drum with drumsticks",
- "unicodeVersion": "9.0",
- "digest": "ba6e2d42af351338819b48803792d5dd1597e831fbae53d2f2aaef275306c8ed"
+ "moji": "🏹",
+ "description": "bow and arrow",
+ "unicodeVersion": "8.0",
+ "digest": "c98949db3391c74e299232ea99a28e035cc1cd747e281e52ccb9afd6a6ac40c3"
},
- "duck": {
- "category": "nature",
- "moji": "🦆",
- "description": "duck",
- "unicodeVersion": "9.0",
- "digest": "41f179dcac9de4a0eb5335ce435176db90fb1e35c043a100a9be09ce1ce3fac0"
+ "shield": {
+ "category": "objects",
+ "moji": "🛡",
+ "description": "shield",
+ "unicodeVersion": "7.0",
+ "digest": "ce3512d081c31c26df95a8f791aa413db09d25e8fba29a1e9f6a2470d6cb3430"
},
- "dvd": {
+ "wrench": {
"category": "objects",
- "moji": "📀",
- "description": "dvd",
+ "moji": "🔧",
+ "description": "wrench",
"unicodeVersion": "6.0",
- "digest": "06080cd1b46afcbdd854c06936389fc990aec029dd6909345861ff18d2275045"
+ "digest": "8478c1b8b0565f87fe6e4f429975f16919d80622a3cdbcc2e83f17fd6978a1d7"
},
- "e-mail": {
+ "nut_and_bolt": {
"category": "objects",
- "moji": "📧",
- "description": "e-mail symbol",
+ "moji": "🔩",
+ "description": "nut and bolt",
"unicodeVersion": "6.0",
- "digest": "7ca1119b9683e5e34185c9cbee9bcd59b6e24feae3a691826672c72d1d7401d1"
+ "digest": "e1a81f58c68ec8371dc7fc146e225fe3b95fcedcea4194da744793a28c3fe263"
},
- "eagle": {
- "category": "nature",
- "moji": "🦅",
- "description": "eagle",
+ "gear": {
+ "category": "objects",
+ "moji": "⚙",
+ "description": "gear",
+ "unicodeVersion": "4.1",
+ "digest": "c752b015d8b37be10c77820e951f9d999ac5860a7a0c453c5eceaf0eb442103b"
+ },
+ "compression": {
+ "category": "objects",
+ "moji": "🗜",
+ "description": "compression",
+ "unicodeVersion": "7.0",
+ "digest": "e762761f41d6c41d204ca149b161ea4286ae71517ea714fa75951c0eea600eeb"
+ },
+ "scales": {
+ "category": "objects",
+ "moji": "⚖",
+ "description": "scales",
+ "unicodeVersion": "4.1",
+ "digest": "2d0fcd2d6d6fe368d142ce7cd1ba78d9fc802131d84e0097d89e49041a9342e8"
+ },
+ "link": {
+ "category": "objects",
+ "moji": "🔗",
+ "description": "link symbol",
+ "unicodeVersion": "6.0",
+ "digest": "eb23a200ad464e4d16f3d977e8aef3ee0b55da62d2a7cd72743c1f57839c4e02"
+ },
+ "chains": {
+ "category": "objects",
+ "moji": "⛓",
+ "description": "chains",
+ "unicodeVersion": "5.2",
+ "digest": "245a60d0dcdf759898d5f6093d3b67e7e3234a5799de52af87166886bb8e67ff"
+ },
+ "alembic": {
+ "category": "objects",
+ "moji": "⚗",
+ "description": "alembic",
+ "unicodeVersion": "4.1",
+ "digest": "9af1181b6190b06ed4fd78d13c64d17c16a59f9b1b512fb0b92a9be9cfb92a2b"
+ },
+ "microscope": {
+ "category": "objects",
+ "moji": "🔬",
+ "description": "microscope",
+ "unicodeVersion": "6.0",
+ "digest": "b732bdd52a38057a56cfcddf2598c9a2d91f0838686f960a91ddc7333fa0ae12"
+ },
+ "telescope": {
+ "category": "objects",
+ "moji": "🔭",
+ "description": "telescope",
+ "unicodeVersion": "6.0",
+ "digest": "202137ea4dc1f3f544100942d597ec9a31c4c4a96f0386a77d437d2274a3fbe7"
+ },
+ "satellite": {
+ "category": "objects",
+ "moji": "📡",
+ "description": "satellite antenna",
+ "unicodeVersion": "6.0",
+ "digest": "84aa893218e8cc97a0f8b74d2d34c0b05a62d8eef47d5f0fede8ee956b222acd"
+ },
+ "syringe": {
+ "category": "objects",
+ "moji": "💉",
+ "description": "syringe",
+ "unicodeVersion": "6.0",
+ "digest": "7c1f7fcc64d14e129f02f6cdf63ba6d13839be54263c1c9c2471826583ca2431"
+ },
+ "pill": {
+ "category": "objects",
+ "moji": "💊",
+ "description": "pill",
+ "unicodeVersion": "6.0",
+ "digest": "dea2366892fb3739ba549da27d7d0f75871d073099ae61a1d0e1c7683bd274d5"
+ },
+ "door": {
+ "category": "objects",
+ "moji": "🚪",
+ "description": "door",
+ "unicodeVersion": "6.0",
+ "digest": "4e3117984e88544efda68c78d0f90e7ba44533ba74beba28e7ea6b9718ca3225"
+ },
+ "bed": {
+ "category": "objects",
+ "moji": "🛏",
+ "description": "bed",
+ "unicodeVersion": "7.0",
+ "digest": "73395ab70c867c776ef56dd14507f7feeaa7871f98e0c0c417df5345e8034976"
+ },
+ "couch": {
+ "category": "objects",
+ "moji": "🛋",
+ "description": "couch and lamp",
+ "unicodeVersion": "7.0",
+ "digest": "e9421d26e96ce4d68b09280e77e699963a3d8259385a12fd28375d61de4aff6b"
+ },
+ "toilet": {
+ "category": "objects",
+ "moji": "🚽",
+ "description": "toilet",
+ "unicodeVersion": "6.0",
+ "digest": "ae590884c0bf0b5f5d721b4d7791ea3878a9514642f6a57ad4784b2ca7ce6fbd"
+ },
+ "shower": {
+ "category": "objects",
+ "moji": "🚿",
+ "description": "shower",
+ "unicodeVersion": "6.0",
+ "digest": "dc732f36a76bbd98ccc3ec886bb697fe8b0e799bc063abc1d5aaa9da4adb655a"
+ },
+ "bathtub": {
+ "category": "objects",
+ "moji": "🛁",
+ "description": "bathtub",
+ "unicodeVersion": "6.0",
+ "digest": "1caecc05b8ae78774b7620480f4cfbbb549ca2bf6b2fb93badecf440db9ce9c8"
+ },
+ "shopping_cart": {
+ "category": "objects",
+ "moji": "🛒",
+ "description": "shopping trolley",
"unicodeVersion": "9.0",
- "digest": "996a7d29861f0ebd0762baeefccd1a8f7179b52bbb16e34e54787c9f6e959857"
+ "digest": "cc32f38d94856b58620bd817fe40641c937ceacdc6b3c7e9ed4350c8926f128f"
},
- "ear": {
- "category": "people",
- "moji": "👂",
- "description": "ear",
+ "smoking": {
+ "category": "objects",
+ "moji": "🚬",
+ "description": "smoking symbol",
"unicodeVersion": "6.0",
- "digest": "29f21bcb6963c709173aa895704029e48f233493ece1f0bb442705d90713e20e"
+ "digest": "3fa148109d83f785ad90999c0d362fb9d6aadd38986f7d483fd0f70ecb5b0447"
},
- "ear_of_rice": {
- "category": "nature",
- "moji": "🌾",
- "description": "ear of rice",
+ "coffin": {
+ "category": "objects",
+ "moji": "⚰",
+ "description": "coffin",
+ "unicodeVersion": "4.1",
+ "digest": "08e3596ffa53801967dd6ff1715999513d8d9b3ae1f1f7c2b841a414981ee5d4"
+ },
+ "urn": {
+ "category": "objects",
+ "moji": "⚱",
+ "description": "funeral urn",
+ "unicodeVersion": "4.1",
+ "digest": "0730518fad9fab0d5070432a6e771d6526349fb075ed9e42b046b88464b0f13c"
+ },
+ "moyai": {
+ "category": "objects",
+ "moji": "🗿",
+ "description": "moyai",
"unicodeVersion": "6.0",
- "digest": "9ea7efc5ebf3cea2cfd17ac2318a1ea0dd1519af6a60a95af33aabe1baa9042e"
+ "digest": "19b4b5efdb559f958b114fbaf6a3ad017a42012528de7a1dfedee5dbc3bce3d3"
},
- "ear_tone1": {
- "category": "people",
- "moji": "👂🏻",
- "description": "ear tone 1",
- "unicodeVersion": "8.0",
- "digest": "5974f347c09ed99eb26af3b38ed5ff6d270920d5a5cf4bc2ca1e4ce1c6ca86d8"
+ "atm": {
+ "category": "symbols",
+ "moji": "🏧",
+ "description": "automated teller machine",
+ "unicodeVersion": "6.0",
+ "digest": "58d4d4ab5df9de2f88f7ce4af21f1e5146ebd61731967d47ec5209affd987a24"
},
- "ear_tone2": {
- "category": "people",
- "moji": "👂🏼",
- "description": "ear tone 2",
- "unicodeVersion": "8.0",
- "digest": "10a215f5a1b0ca5c39abb4b2ba1c95fc541b21d03eb74b437a732bc155772b7f"
+ "put_litter_in_its_place": {
+ "category": "symbols",
+ "moji": "🚮",
+ "description": "put litter in its place symbol",
+ "unicodeVersion": "6.0",
+ "digest": "8f4d77e14f81634081b21e9bc062082d5a18cad551560eb8479c0c5d14ab3358"
},
- "ear_tone3": {
- "category": "people",
- "moji": "👂🏽",
- "description": "ear tone 3",
- "unicodeVersion": "8.0",
- "digest": "339cb06fc200e7a2f175de0862fa0f69792725532002e26446a64b47bae4e7f0"
+ "potable_water": {
+ "category": "symbols",
+ "moji": "🚰",
+ "description": "potable water symbol",
+ "unicodeVersion": "6.0",
+ "digest": "e35ad512f4da69fa132475c68cdbb831ab617f013127aba15cd7508f0b732d19"
},
- "ear_tone4": {
- "category": "people",
- "moji": "👂🏾",
- "description": "ear tone 4",
+ "wheelchair": {
+ "category": "symbols",
+ "moji": "♿",
+ "description": "wheelchair symbol",
+ "unicodeVersion": "4.1",
+ "digest": "805e8f94922c2214849af36b2de79cb812472cf6c3cf7c55dbf7fe9e11f0380b"
+ },
+ "mens": {
+ "category": "symbols",
+ "moji": "🚹",
+ "description": "mens symbol",
+ "unicodeVersion": "6.0",
+ "digest": "eca8312aaade2705ca15be8c1d1fcd897ed4ea0189e995dee3727bcda9d900df"
+ },
+ "womens": {
+ "category": "symbols",
+ "moji": "🚺",
+ "description": "womens symbol",
+ "unicodeVersion": "6.0",
+ "digest": "d0dd248ac8f17c5eb5c24f636f2d6387f1e4126d2227834ffb89057e9428a19f"
+ },
+ "restroom": {
+ "category": "symbols",
+ "moji": "🚻",
+ "description": "restroom",
+ "unicodeVersion": "6.0",
+ "digest": "f2a6d958afccd904f504425f024c968e22d13466f122006b245ea3db25e22bdd"
+ },
+ "baby_symbol": {
+ "category": "symbols",
+ "moji": "🚼",
+ "description": "baby symbol",
+ "unicodeVersion": "6.0",
+ "digest": "19ea4b3a81368933b6e78901205747e4fbc1dcdbfc3162f27191c1a8e0c395f7"
+ },
+ "wc": {
+ "category": "symbols",
+ "moji": "🚾",
+ "description": "water closet",
+ "unicodeVersion": "6.0",
+ "digest": "a1576f1731a68646c9de74c135674955eee3df8f740437ab9ef04a45a49b282c"
+ },
+ "passport_control": {
+ "category": "symbols",
+ "moji": "🛂",
+ "description": "passport control",
+ "unicodeVersion": "6.0",
+ "digest": "cd2221bd39e8517b14d1509afcebf3aeb138d0b286c1012544e2950671affa1a"
+ },
+ "customs": {
+ "category": "symbols",
+ "moji": "🛃",
+ "description": "customs",
+ "unicodeVersion": "6.0",
+ "digest": "0f374b6f60c5bc6da41b0682a352304bc258ef6274da8afd6820ff9bb50a5c39"
+ },
+ "baggage_claim": {
+ "category": "symbols",
+ "moji": "🛄",
+ "description": "baggage claim",
+ "unicodeVersion": "6.0",
+ "digest": "c99d1d554d119f4a1f8ffa7f6c53d03071972d2f0907ffcba4f5ef92739edb1d"
+ },
+ "left_luggage": {
+ "category": "symbols",
+ "moji": "🛅",
+ "description": "left luggage",
+ "unicodeVersion": "6.0",
+ "digest": "18bade3d46e8ea3ba9d6bc52679e367d7a1721757719ca6903e68331a1fd1c47"
+ },
+ "warning": {
+ "category": "symbols",
+ "moji": "⚠",
+ "description": "warning sign",
+ "unicodeVersion": "4.0",
+ "digest": "1d6cf2ec8990304aaca53a2eecd878c17e4eb8685c48d3be59c8f0a1cfb66202"
+ },
+ "children_crossing": {
+ "category": "symbols",
+ "moji": "🚸",
+ "description": "children crossing",
+ "unicodeVersion": "6.0",
+ "digest": "092e5fa179cce029edae31733f6c1dc0e147cde9050183b3f7b121cc7839c2ab"
+ },
+ "no_entry": {
+ "category": "symbols",
+ "moji": "⛔",
+ "description": "no entry",
+ "unicodeVersion": "5.2",
+ "digest": "a960fb2c78aac5ca7ad01e6f5c3dc2212f050f3e4e0dace7bbf79168b63857f6"
+ },
+ "no_entry_sign": {
+ "category": "symbols",
+ "moji": "🚫",
+ "description": "no entry sign",
+ "unicodeVersion": "6.0",
+ "digest": "ed0f2355d1edca66757f78849bebeeac010c19bf7d443c04eafa01e15a3f12fe"
+ },
+ "no_bicycles": {
+ "category": "symbols",
+ "moji": "🚳",
+ "description": "no bicycles",
+ "unicodeVersion": "6.0",
+ "digest": "0f0d8e8ab9e421e5f87af7d8dd2afde646d47e35846483e350e11d46039181b6"
+ },
+ "no_smoking": {
+ "category": "symbols",
+ "moji": "🚭",
+ "description": "no smoking symbol",
+ "unicodeVersion": "6.0",
+ "digest": "90337e0742354ba1e87c0262300e48172cfc5db1c9efb4b6837be49efdec73d3"
+ },
+ "do_not_litter": {
+ "category": "symbols",
+ "moji": "🚯",
+ "description": "do not litter symbol",
+ "unicodeVersion": "6.0",
+ "digest": "13dd4fc59cd518187d2827a935ad7ca61c8f4542ff9843d882ee44c942e57045"
+ },
+ "non-potable_water": {
+ "category": "symbols",
+ "moji": "🚱",
+ "description": "non-potable water symbol",
+ "unicodeVersion": "6.0",
+ "digest": "fded06ba8a998777f04af38833981c8b7980772be1b37509bfbe1692a5e9c81b"
+ },
+ "no_pedestrians": {
+ "category": "symbols",
+ "moji": "🚷",
+ "description": "no pedestrians",
+ "unicodeVersion": "6.0",
+ "digest": "490933e9068e71aa8ab4e864768dbba8c0dcb76d3958261ba0a3da1139984862"
+ },
+ "no_mobile_phones": {
+ "category": "symbols",
+ "moji": "📵",
+ "description": "no mobile phones",
+ "unicodeVersion": "6.0",
+ "digest": "e7fc1cf8ab08e5144cd9a515098e9ea5e5c6dc7d098b764792a6a175c7ac7cab"
+ },
+ "underage": {
+ "category": "symbols",
+ "moji": "🔞",
+ "description": "no one under eighteen symbol",
+ "unicodeVersion": "6.0",
+ "digest": "afdd3883748acb01bb18bfe034c39b4510957c7bfbdb9cec33b4e6b4a0068089"
+ },
+ "radioactive": {
+ "category": "symbols",
+ "moji": "☢",
+ "description": "radioactive sign",
+ "unicodeVersion": "1.1",
+ "digest": "adae20f9d65f9e4c3d2ff97012c58475ed56d446010999b75af2ddc78961cf54"
+ },
+ "biohazard": {
+ "category": "symbols",
+ "moji": "☣",
+ "description": "biohazard sign",
+ "unicodeVersion": "1.1",
+ "digest": "aac5d8bf19b5e33c0a1d600a5e86522023415fb2a98160c58f541c862e439f9a"
+ },
+ "arrow_up": {
+ "category": "symbols",
+ "moji": "⬆",
+ "description": "upwards black arrow",
+ "unicodeVersion": "4.0",
+ "digest": "9ea5aba6d658bc8bed6a8eb9ea073e010d916dd7e6cde9de71a760026754ed6a"
+ },
+ "arrow_upper_right": {
+ "category": "symbols",
+ "moji": "↗",
+ "description": "north east arrow",
+ "unicodeVersion": "1.1",
+ "digest": "e8894fed9c62b652add3bac3ad3dd84bf3a215c45a0883c6e701cc02f89e5bfc"
+ },
+ "arrow_right": {
+ "category": "symbols",
+ "moji": "➡",
+ "description": "black rightwards arrow",
+ "unicodeVersion": "1.1",
+ "digest": "cca969d90670944613bcaa9463a35abfdbfa6474d1177683e2fa1327d8b52c91"
+ },
+ "arrow_lower_right": {
+ "category": "symbols",
+ "moji": "↘",
+ "description": "south east arrow",
+ "unicodeVersion": "1.1",
+ "digest": "603a00a2370d8872a037fdd195315bbbe936ccf12db2801bd93211a55068daa7"
+ },
+ "arrow_down": {
+ "category": "symbols",
+ "moji": "⬇",
+ "description": "downwards black arrow",
+ "unicodeVersion": "4.0",
+ "digest": "70c8ffa3178143b62de0d2e739700456ed470f5990599f86f518307d77bace97"
+ },
+ "arrow_lower_left": {
+ "category": "symbols",
+ "moji": "↙",
+ "description": "south west arrow",
+ "unicodeVersion": "1.1",
+ "digest": "53a95853a65f3add101d64cd4ca26d677b1519d388d9c5ac75088e1965e8b856"
+ },
+ "arrow_left": {
+ "category": "symbols",
+ "moji": "⬅",
+ "description": "leftwards black arrow",
+ "unicodeVersion": "4.0",
+ "digest": "431289c3759f093a1a2f3460d6b912897098d3344a23a31fa9ce778c484f60ba"
+ },
+ "arrow_upper_left": {
+ "category": "symbols",
+ "moji": "↖",
+ "description": "north west arrow",
+ "unicodeVersion": "1.1",
+ "digest": "035fd0f1149c8af6bec2c3dd09465b494427fcc34213749802e2e18e5ae8159a"
+ },
+ "arrow_up_down": {
+ "category": "symbols",
+ "moji": "↕",
+ "description": "up down arrow",
+ "unicodeVersion": "1.1",
+ "digest": "83bda8fd50d42e169679a9825df1f5d5b7c421c710fb965b9ad4885c3c61268a"
+ },
+ "left_right_arrow": {
+ "category": "symbols",
+ "moji": "↔",
+ "description": "left right arrow",
+ "unicodeVersion": "1.1",
+ "digest": "292108ff7e529974269eb98b0d417f651a9d97258a8070aaec6579c934139bb0"
+ },
+ "leftwards_arrow_with_hook": {
+ "category": "symbols",
+ "moji": "↩",
+ "description": "leftwards arrow with hook",
+ "unicodeVersion": "1.1",
+ "digest": "1f4c8e03c92083ddd647f3328fd1ff9b919c10a98d0325b877f1b7034a7387ea"
+ },
+ "arrow_right_hook": {
+ "category": "symbols",
+ "moji": "↪",
+ "description": "rightwards arrow with hook",
+ "unicodeVersion": "1.1",
+ "digest": "65e1489951134f221d8b7d45d6857321530dfad720c43fa0590d123b3cf29c00"
+ },
+ "arrow_heading_up": {
+ "category": "symbols",
+ "moji": "⤴",
+ "description": "arrow pointing rightwards then curving upwards",
+ "unicodeVersion": "3.2",
+ "digest": "b684802de1239962bd0c076a5a0aeb268d2cf43620dde214520817e522a60792"
+ },
+ "arrow_heading_down": {
+ "category": "symbols",
+ "moji": "⤵",
+ "description": "arrow pointing rightwards then curving downwards",
+ "unicodeVersion": "3.2",
+ "digest": "f351d6b66a0e73f41bc58446486c5fbe35d70b715fdc4ae9adef474df7aa1f69"
+ },
+ "arrows_clockwise": {
+ "category": "symbols",
+ "moji": "🔃",
+ "description": "clockwise downwards and upwards open circle arrows",
+ "unicodeVersion": "6.0",
+ "digest": "0f4be61ded4f219dc1582b002635b538c15d40234e8571a3bfbc35921f0e83a6"
+ },
+ "arrows_counterclockwise": {
+ "category": "symbols",
+ "moji": "🔄",
+ "description": "anticlockwise downwards and upwards open circle ar",
+ "unicodeVersion": "6.0",
+ "digest": "e94e5cd47117e7dd196d0b1f32a97f2f87e76b6ad2652359849cda8d773c284c"
+ },
+ "back": {
+ "category": "symbols",
+ "moji": "🔙",
+ "description": "back with leftwards arrow above",
+ "unicodeVersion": "6.0",
+ "digest": "c473fa1c3be08fb1920ce88072cf4f6ce3a031707d765d5e24b47aab0aac45be"
+ },
+ "end": {
+ "category": "symbols",
+ "moji": "🔚",
+ "description": "end with leftwards arrow above",
+ "unicodeVersion": "6.0",
+ "digest": "b809c2bda4b924a3e2f43ae45a3bfc1a3e09d1750b1f17a19b82f66a2456038b"
+ },
+ "on": {
+ "category": "symbols",
+ "moji": "🔛",
+ "description": "on with exclamation mark with left right arrow abo",
+ "unicodeVersion": "6.0",
+ "digest": "2e96678d4a15fd6be2b87c4fbccbe51f4bfafcc9d04c866ce1bc640a80144730"
+ },
+ "soon": {
+ "category": "symbols",
+ "moji": "🔜",
+ "description": "soon with rightwards arrow above",
+ "unicodeVersion": "6.0",
+ "digest": "6325b67539559992fc3d1ed23f2dc20ee95b3052b93a674f82ed53dc2e199270"
+ },
+ "top": {
+ "category": "symbols",
+ "moji": "🔝",
+ "description": "top with upwards arrow above",
+ "unicodeVersion": "6.0",
+ "digest": "69250cda059411b279e6503ff1afac1188b2507d861db7efc956d7758468aa01"
+ },
+ "place_of_worship": {
+ "category": "symbols",
+ "moji": "🛐",
+ "description": "place of worship",
"unicodeVersion": "8.0",
- "digest": "4fc70d5c353ad59518bb3829b0c544a4092b59924bf6fe0dd573f8fee3d00c68"
+ "digest": "39562f00c92f6a75028f57faf3f951026866f42bca6e51bcbbf30f442e24c63a"
},
- "ear_tone5": {
- "category": "people",
- "moji": "👂🏿",
- "description": "ear tone 5",
+ "atom": {
+ "category": "symbols",
+ "moji": "⚛",
+ "description": "atom symbol",
+ "unicodeVersion": "4.1",
+ "digest": "1a7ca89822b91c1acfe7a10d57c859bfa87b6797713efa2ec87f01d36d302056"
+ },
+ "om_symbol": {
+ "category": "symbols",
+ "moji": "🕉",
+ "description": "om symbol",
+ "unicodeVersion": "7.0",
+ "digest": "8cc8000af09220f887582df7d0300da13889d371180fda773894b89ecd0053bb"
+ },
+ "star_of_david": {
+ "category": "symbols",
+ "moji": "✡",
+ "description": "star of david",
+ "unicodeVersion": "1.1",
+ "digest": "dbe79ef9f506a4f46368a8a5e9953579c97fc1bca97c1ddc7f2bcc76398f0149"
+ },
+ "wheel_of_dharma": {
+ "category": "symbols",
+ "moji": "☸",
+ "description": "wheel of dharma",
+ "unicodeVersion": "1.1",
+ "digest": "bdc92990dc2dcde7de136ed934e9f2476dc011257ffbeeb5dc45e1f86e26a067"
+ },
+ "yin_yang": {
+ "category": "symbols",
+ "moji": "☯",
+ "description": "yin yang",
+ "unicodeVersion": "1.1",
+ "digest": "2d97f8a9eca001163c5895ff25ca66aad5dfabe5e2e00bda102fb6b9a27fc02b"
+ },
+ "cross": {
+ "category": "symbols",
+ "moji": "✝",
+ "description": "latin cross",
+ "unicodeVersion": "1.1",
+ "digest": "4c96506b864fefcb67c75ab76262065ca34dc247bdf0a65a859a7c18051aa8be"
+ },
+ "orthodox_cross": {
+ "category": "symbols",
+ "moji": "☦",
+ "description": "orthodox cross",
+ "unicodeVersion": "1.1",
+ "digest": "29f438c972e101a1305c0bd5138b6b998778b33bcfd98418538340dc4126a601"
+ },
+ "star_and_crescent": {
+ "category": "symbols",
+ "moji": "☪",
+ "description": "star and crescent",
+ "unicodeVersion": "1.1",
+ "digest": "550cf94a0efe6ef0211e51e2d84554bd789505c861a16efa79704f8ccda086b1"
+ },
+ "peace": {
+ "category": "symbols",
+ "moji": "☮",
+ "description": "peace symbol",
+ "unicodeVersion": "1.1",
+ "digest": "375030a1230116742eed0b632822936e92179f2da97b73faef675140db76f4db"
+ },
+ "menorah": {
+ "category": "symbols",
+ "moji": "🕎",
+ "description": "menorah with nine branches",
"unicodeVersion": "8.0",
- "digest": "fa4d1bdab5ed8dc2e8976edb389571c21942cbdcf7ad51281b4e064a2b260a58"
+ "digest": "befdb755f7f872a9061119929ce34d9f0368a5ca7506ba4f3984b24f69b01a27"
},
- "earth_africa": {
- "category": "nature",
- "moji": "🌍",
- "description": "earth globe europe-africa",
+ "six_pointed_star": {
+ "category": "symbols",
+ "moji": "🔯",
+ "description": "six pointed star with middle dot",
"unicodeVersion": "6.0",
- "digest": "597e652a0be2d59d9f0190423767a763500c724ac53321c1cb6fc7fc0f9b9e8c"
+ "digest": "1ee9c385a74dc6954e37727615d99209a61627562c8675d4354262b2c421418f"
},
- "earth_americas": {
- "category": "nature",
- "moji": "🌎",
- "description": "earth globe americas",
- "unicodeVersion": "6.0",
- "digest": "b65c2ce55702e00286792a5007609bafdf1f184e2da5a8c6b949054534d5e8ad"
+ "aries": {
+ "category": "symbols",
+ "moji": "♈",
+ "description": "aries",
+ "unicodeVersion": "1.1",
+ "digest": "4e35bd481a7c73be42faba7afa67c522b4340180efa18f0523e394a90a2944aa"
},
- "earth_asia": {
- "category": "nature",
- "moji": "🌏",
- "description": "earth globe asia-australia",
+ "taurus": {
+ "category": "symbols",
+ "moji": "♉",
+ "description": "taurus",
+ "unicodeVersion": "1.1",
+ "digest": "e64c53547f42dc3e07c06f0891641395773a9cdca0adb3257ea584a3102d084f"
+ },
+ "gemini": {
+ "category": "symbols",
+ "moji": "♊",
+ "description": "gemini",
+ "unicodeVersion": "1.1",
+ "digest": "3ddb938fe1196593b21a3380b20107ea22253c6fcf5b9fccdca6badf7384047b"
+ },
+ "cancer": {
+ "category": "symbols",
+ "moji": "♋",
+ "description": "cancer",
+ "unicodeVersion": "1.1",
+ "digest": "f707db14878e03608dee3eca4eacf0a0be4a709c6d33b2d5b7ab0e8f05473b2b"
+ },
+ "leo": {
+ "category": "symbols",
+ "moji": "♌",
+ "description": "leo",
+ "unicodeVersion": "1.1",
+ "digest": "25787c495211576604fbcdf72aa47ab7e5ad4e8a42905a995edbbec073e20c52"
+ },
+ "virgo": {
+ "category": "symbols",
+ "moji": "♍",
+ "description": "virgo",
+ "unicodeVersion": "1.1",
+ "digest": "2cae076c31fe134ca69caef07db75283f72c99f8fd746b1c1fff6a897f613655"
+ },
+ "libra": {
+ "category": "symbols",
+ "moji": "♎",
+ "description": "libra",
+ "unicodeVersion": "1.1",
+ "digest": "0189ff934df698ed87a6bac3b29c559567c958a9fbd0259378af8c910e0337e1"
+ },
+ "scorpius": {
+ "category": "symbols",
+ "moji": "♏",
+ "description": "scorpius",
+ "unicodeVersion": "1.1",
+ "digest": "32550597084a3b17fdab6bc4f56513a5952581ad07e79fb56abe05d6665a86af"
+ },
+ "sagittarius": {
+ "category": "symbols",
+ "moji": "♐",
+ "description": "sagittarius",
+ "unicodeVersion": "1.1",
+ "digest": "23c2aa3cbb29c0fb6c443d8d388e41454b4da2aae8f239c4572ecf0e9d580276"
+ },
+ "capricorn": {
+ "category": "symbols",
+ "moji": "♑",
+ "description": "capricorn",
+ "unicodeVersion": "1.1",
+ "digest": "d769793914f9c1ea4237aea85d288344c7422ccb286f8c1d3b18fa99ee8fa7ce"
+ },
+ "aquarius": {
+ "category": "symbols",
+ "moji": "♒",
+ "description": "aquarius",
+ "unicodeVersion": "1.1",
+ "digest": "a40fb6ccb866eaf296e083c57f46ca29f3d9732de897e62a8201481b526ae6b9"
+ },
+ "pisces": {
+ "category": "symbols",
+ "moji": "♓",
+ "description": "pisces",
+ "unicodeVersion": "1.1",
+ "digest": "2b9982d42db6ef3b280174b79faa2ce0178f349b826474883297fbbea088b2ea"
+ },
+ "ophiuchus": {
+ "category": "symbols",
+ "moji": "⛎",
+ "description": "ophiuchus",
"unicodeVersion": "6.0",
- "digest": "0334bd2fcfd55b0759c7bf900c7ef9a70fb3408ecb7762038e6b587d8226d204"
+ "digest": "d1b29a9339ee7bce6ff2dc8b12e424ae48f419c8bbf12fb771c0c20c3bcd76ef"
},
- "egg": {
- "category": "food",
- "moji": "🥚",
- "description": "egg",
- "unicodeVersion": "9.0",
- "digest": "97287d7b86252a2e3b8a112aaa1a3096f48ee930691e200470e6b35876d9d3c3"
+ "twisted_rightwards_arrows": {
+ "category": "symbols",
+ "moji": "🔀",
+ "description": "twisted rightwards arrows",
+ "unicodeVersion": "6.0",
+ "digest": "f559a51e80d26d7bb196ea380936ff60c4a281c3943cf6e86aaebeacce8d957f"
},
- "eggplant": {
- "category": "food",
- "moji": "🍆",
- "description": "aubergine",
+ "repeat": {
+ "category": "symbols",
+ "moji": "🔁",
+ "description": "clockwise rightwards and leftwards open circle arr",
"unicodeVersion": "6.0",
- "digest": "717719283180637a9929192d3af0459a871c144a5d6134cd1b31482cf065fc04"
+ "digest": "6e7a4196d2899b28b0c85a77c6abafd202f91189710793e9a6b4a95ab55c0fc5"
},
- "eight": {
+ "repeat_one": {
"category": "symbols",
- "moji": "8️⃣",
- "description": "keycap digit eight",
- "unicodeVersion": "3.0",
- "digest": "0cc5beaaf4cf5efc0dc7450327ca264669641deebe0696997b578ca603eadd80"
+ "moji": "🔂",
+ "description": "clockwise rightwards and leftwards open circle arr",
+ "unicodeVersion": "6.0",
+ "digest": "6f6ace59c5d36c66d5816247e9c29f6aa3efef428ee83c99c9d341f7c055a64c"
},
- "eight_pointed_black_star": {
+ "arrow_forward": {
"category": "symbols",
- "moji": "✴",
- "description": "eight pointed black star",
+ "moji": "▶",
+ "description": "black right-pointing triangle",
"unicodeVersion": "1.1",
- "digest": "a06b0825d8288487c0a27cf858b1bdc3f9144683fe9ceb9608450f21dc278db8"
+ "digest": "b3f0c8db863157c1c4121602ecaee91845346b2b7a1ef0ad4d46744db7866e98"
},
- "eight_spoked_asterisk": {
+ "fast_forward": {
"category": "symbols",
- "moji": "✳",
- "description": "eight spoked asterisk",
+ "moji": "⏩",
+ "description": "black right-pointing double triangle",
+ "unicodeVersion": "6.0",
+ "digest": "4ad792e67e07120968468561f20df83f39fa5800b8212a64e76fc36cf27464c3"
+ },
+ "track_next": {
+ "category": "symbols",
+ "moji": "⏭",
+ "description": "black right-pointing double triangle with vertical bar",
+ "unicodeVersion": "6.0",
+ "digest": "adc496351d784f266e6addac74e29d328d24f3e6d3f4a2f1270e88de00f48116"
+ },
+ "play_pause": {
+ "category": "symbols",
+ "moji": "⏯",
+ "description": "black right-pointing double triangle with double vertical bar",
+ "unicodeVersion": "6.0",
+ "digest": "a4fe3a6a5928f88e77c643e7869e4edade8cd19839ec7e347265439483463019"
+ },
+ "arrow_backward": {
+ "category": "symbols",
+ "moji": "◀",
+ "description": "black left-pointing triangle",
"unicodeVersion": "1.1",
- "digest": "65fb63267575cb5637fed6ff99327cab54301baa39024ffae8fc8dd3db3516f9"
+ "digest": "c4e1ba32b806674ba5f7cdb1433d4b45cf9b31f546918aa5ad169c0c8af50a8a"
+ },
+ "rewind": {
+ "category": "symbols",
+ "moji": "⏪",
+ "description": "black left-pointing double triangle",
+ "unicodeVersion": "6.0",
+ "digest": "12a3f494d633a469015ed63e737a4bf0fac43206bf4a55c4d0e0c588a81bdffc"
+ },
+ "track_previous": {
+ "category": "symbols",
+ "moji": "⏮",
+ "description": "black left-pointing double triangle with vertical bar",
+ "unicodeVersion": "6.0",
+ "digest": "aaa9c3c003a2839067dde8aab59c2887532b8f00d00726baa33c995b21b06dcc"
+ },
+ "arrow_up_small": {
+ "category": "symbols",
+ "moji": "🔼",
+ "description": "up-pointing small red triangle",
+ "unicodeVersion": "6.0",
+ "digest": "2335b064c64d375d54e4e59111cd169781e633969b57d57dd9e1abba7555e9f1"
+ },
+ "arrow_double_up": {
+ "category": "symbols",
+ "moji": "⏫",
+ "description": "black up-pointing double triangle",
+ "unicodeVersion": "6.0",
+ "digest": "79fe28485f924df1c66447c2e633ebc02d01ab46c0d2ad4b319f950f060d5525"
+ },
+ "arrow_down_small": {
+ "category": "symbols",
+ "moji": "🔽",
+ "description": "down-pointing small red triangle",
+ "unicodeVersion": "6.0",
+ "digest": "6432caec004e9eadb0d160cb857a2eeb43cfab1ace23a904fa5a624f56bfca95"
+ },
+ "arrow_double_down": {
+ "category": "symbols",
+ "moji": "⏬",
+ "description": "black down-pointing double triangle",
+ "unicodeVersion": "6.0",
+ "digest": "079bfc85ed3a1b354a6c8d9d041ed2049c449a696a2a2d1ff11e65efc91bc420"
+ },
+ "pause_button": {
+ "category": "symbols",
+ "moji": "⏸",
+ "description": "double vertical bar",
+ "unicodeVersion": "7.0",
+ "digest": "08bf08733cf20f2afebc56318c86ad3a0a783305dd7c3ee72d7b8898dcd39fc8"
+ },
+ "stop_button": {
+ "category": "symbols",
+ "moji": "⏹",
+ "description": "black square for stop",
+ "unicodeVersion": "7.0",
+ "digest": "ea16a3e7a6ffa4741509cc909944975dd24c4a0678a23cc03e02c3773e6bba92"
+ },
+ "record_button": {
+ "category": "symbols",
+ "moji": "⏺",
+ "description": "black circle for record",
+ "unicodeVersion": "7.0",
+ "digest": "2b7ef01bcbfb5310cceffada4a966b874dd3bf4b8cfa23848effac1386457fc3"
},
"eject": {
"category": "symbols",
@@ -2862,68 +9925,110 @@
"unicodeVersion": "4.0",
"digest": "d12767c5ad910e8718971d315520934e86d607f3493a72a984545a4fee7a0b0c"
},
- "electric_plug": {
- "category": "objects",
- "moji": "🔌",
- "description": "electric plug",
+ "cinema": {
+ "category": "symbols",
+ "moji": "🎦",
+ "description": "cinema",
"unicodeVersion": "6.0",
- "digest": "78feabb40b541ad46e396f9508de72a52f0e4ca2f48717078eea100b3c125a24"
+ "digest": "acfdea199d4ab48103e5491dd3ab0a616ffbef57641a07aef315273559ccf690"
},
- "elephant": {
- "category": "nature",
- "moji": "🐘",
- "description": "elephant",
+ "low_brightness": {
+ "category": "symbols",
+ "moji": "🔅",
+ "description": "low brightness symbol",
"unicodeVersion": "6.0",
- "digest": "c151ce8cf6aee435ed27099fac1d5e55501e643f69d7e39476fa6bcd73be9559"
+ "digest": "be7fc79c265d5c02ee7d29fe8290db80cfd794e271463853d6f5d01948c62dfb"
},
- "end": {
+ "high_brightness": {
"category": "symbols",
- "moji": "🔚",
- "description": "end with leftwards arrow above",
+ "moji": "🔆",
+ "description": "high brightness symbol",
"unicodeVersion": "6.0",
- "digest": "b809c2bda4b924a3e2f43ae45a3bfc1a3e09d1750b1f17a19b82f66a2456038b"
+ "digest": "9ee294cb514a00f831f3fec6275d75685f157b6d22a7ee1417c28b4c93e1d3ba"
},
- "envelope": {
- "category": "objects",
- "moji": "✉",
- "description": "envelope",
+ "signal_strength": {
+ "category": "symbols",
+ "moji": "📶",
+ "description": "antenna with bars",
+ "unicodeVersion": "6.0",
+ "digest": "eabf6d0cae69aea6027f1dede7df1ac51fc09d85af8a4ae9b1df1fcb8ee4a0f0"
+ },
+ "vibration_mode": {
+ "category": "symbols",
+ "moji": "📳",
+ "description": "vibration mode",
+ "unicodeVersion": "6.0",
+ "digest": "15ef296e1ef2747dfc47d8999ff7bbcca42a2ea682d57e224064335d9fa1c84e"
+ },
+ "mobile_phone_off": {
+ "category": "symbols",
+ "moji": "📴",
+ "description": "mobile phone off",
+ "unicodeVersion": "6.0",
+ "digest": "93373556567d92fdb11d4e562e74c15ba6b0987cb26dea909b0e922921087628"
+ },
+ "heavy_multiplication_x": {
+ "category": "symbols",
+ "moji": "✖",
+ "description": "heavy multiplication x",
"unicodeVersion": "1.1",
- "digest": "ff00c491ba40989d83d6586ebb8e18e1507cdf55e056981803c3f77d0b246314"
+ "digest": "879a6b81e3fba890a3261029c611bc8baf6371e012b70a648fec0c2156e690d7"
},
- "envelope_with_arrow": {
- "category": "objects",
- "moji": "📩",
- "description": "envelope with downwards arrow above",
+ "heavy_plus_sign": {
+ "category": "symbols",
+ "moji": "➕",
+ "description": "heavy plus sign",
"unicodeVersion": "6.0",
- "digest": "1af1da925637995928c8f83f7f5effee40db1f5a33f1cd7d626ff9ed408bfe19"
+ "digest": "d7e684b6d4a4f0f9fa51dddf10c844616d0bb14184ba8b015088b5a4793aa4c4"
},
- "euro": {
- "category": "objects",
- "moji": "💶",
- "description": "banknote with euro sign",
+ "heavy_minus_sign": {
+ "category": "symbols",
+ "moji": "➖",
+ "description": "heavy minus sign",
"unicodeVersion": "6.0",
- "digest": "5c08342b52ba2417f1daf6c357107eddc3ed9e46ed82443665859dba944b0754"
+ "digest": "9f2d6d303ab3c87bfdc78a933351f1659304758a330828961bc053fac1f5cdae"
},
- "european_castle": {
- "category": "travel",
- "moji": "🏰",
- "description": "european castle",
+ "heavy_division_sign": {
+ "category": "symbols",
+ "moji": "➗",
+ "description": "heavy division sign",
"unicodeVersion": "6.0",
- "digest": "183e813391199158b855481acefea11061b1722e51f2d2880486782b7639598d"
+ "digest": "563f5723eb45d2903a37ae66b3a388b06c1576a35cbcf203a6ea951bbbcb10f3"
},
- "european_post_office": {
- "category": "travel",
- "moji": "🏤",
- "description": "european post office",
+ "bangbang": {
+ "category": "symbols",
+ "moji": "‼",
+ "description": "double exclamation mark",
+ "unicodeVersion": "1.1",
+ "digest": "905c1dc3100192f2052c72531cf796e106815f1ce11ba1993d18c0585ed75e18"
+ },
+ "interrobang": {
+ "category": "symbols",
+ "moji": "⁉",
+ "description": "exclamation question mark",
+ "unicodeVersion": "3.0",
+ "digest": "d209c9aa46c89e290a053d458f6cda0c719831518ce91265680673065cfb77d5"
+ },
+ "question": {
+ "category": "symbols",
+ "moji": "❓",
+ "description": "black question mark ornament",
"unicodeVersion": "6.0",
- "digest": "664ed0206445f1511bc2ecdfaf77c3df14dfd4e98fd73b58c6272e54e4117810"
+ "digest": "fd18ff641d27854c3996ec9c58210fde5411537847c0f3c55d9318244c3e8854"
},
- "evergreen_tree": {
- "category": "nature",
- "moji": "🌲",
- "description": "evergreen tree",
+ "grey_question": {
+ "category": "symbols",
+ "moji": "❔",
+ "description": "white question mark ornament",
"unicodeVersion": "6.0",
- "digest": "62ae1c4109a7458bcc754f89311fa37be31a0cdc63ee96f420482568bc74000c"
+ "digest": "54b5c454718c213942187fe846ee10466d1144050456559e3782799ed631f72f"
+ },
+ "grey_exclamation": {
+ "category": "symbols",
+ "moji": "❕",
+ "description": "white exclamation mark ornament",
+ "unicodeVersion": "6.0",
+ "digest": "75bdc292ebb381f9c5bd236e8298665f3c536d049ea8df735b677f5997dc9df5"
},
"exclamation": {
"category": "symbols",
@@ -2932,432 +10037,726 @@
"unicodeVersion": "5.2",
"digest": "8caf962a884874258642f7df358cd2693c85a111d96d0e72a26d7010ac595c61"
},
- "expressionless": {
- "category": "people",
- "moji": "😑",
- "description": "expressionless face",
- "unicodeVersion": "6.1",
- "digest": "d818ca9cf4ba0c02756559d0e870517f298a88466b3e27002a86389942d89145"
+ "wavy_dash": {
+ "category": "symbols",
+ "moji": "〰",
+ "description": "wavy dash",
+ "unicodeVersion": "1.1",
+ "digest": "d7bcb62068abe4e1c168ba0445c1d3d1b1e76373bbe6f2d84f4d78b38be524c7"
},
- "eye": {
- "category": "people",
- "moji": "👁",
- "description": "eye",
- "unicodeVersion": "7.0",
- "digest": "d48868ab77a09456b5f80553f08af363983ead45173759e3ff8a9ddf626effc2"
+ "currency_exchange": {
+ "category": "symbols",
+ "moji": "💱",
+ "description": "currency exchange",
+ "unicodeVersion": "6.0",
+ "digest": "3703307c690bd113fbe5826720c6466ee9dccc80ef8d3556cfe2ba7401240df8"
},
- "eye_in_speech_bubble": {
+ "heavy_dollar_sign": {
"category": "symbols",
- "moji": "👁‍🗨",
- "description": "eye in speech bubble",
- "unicodeVersion": "7.0",
- "digest": "4b4d96038c0883d99091f9718a3c1e9c096268a3bcead903a7f6551db2b779a8"
+ "moji": "💲",
+ "description": "heavy dollar sign",
+ "unicodeVersion": "6.0",
+ "digest": "25a39a89a62f45d7bd4948682ed23753d16179fe0c80dc60ff06bc29549fd04f"
},
- "eyeglasses": {
- "category": "people",
- "moji": "👓",
- "description": "eyeglasses",
+ "recycle": {
+ "category": "symbols",
+ "moji": "♻",
+ "description": "black universal recycling symbol",
+ "unicodeVersion": "3.2",
+ "digest": "9256cf44c41b5b3479c92a453c26adcc32800af9ffbf32f2d08e7570732788b2"
+ },
+ "fleur-de-lis": {
+ "category": "symbols",
+ "moji": "⚜",
+ "description": "fleur-de-lis",
+ "unicodeVersion": "4.1",
+ "digest": "aad1d75ba9bce98d639b82b8c2f6481947edec6ceaa48cb890f2503eb2964202"
+ },
+ "trident": {
+ "category": "symbols",
+ "moji": "🔱",
+ "description": "trident emblem",
"unicodeVersion": "6.0",
- "digest": "7f413b054509c24ee233b944df5a0b314ede67006d64f03df4a1c055a45245ee"
+ "digest": "c9fcbc5e98ed51cd228a1b0eedde2851624c3a7fc354abab9efd96eaa52dcb52"
},
- "eyes": {
- "category": "people",
- "moji": "👀",
- "description": "eyes",
+ "name_badge": {
+ "category": "symbols",
+ "moji": "📛",
+ "description": "name badge",
"unicodeVersion": "6.0",
- "digest": "659c9a040950bd320a33177be730c086cc6cf390d1b99d7cbf3e6bc7705f21bf"
+ "digest": "42cca8b42700765726f33adac7fd6ddb8911d4e2b5ea680c4348c0c699d61c3f"
},
- "face_palm": {
- "category": "people",
- "moji": "🤦",
- "description": "face palm",
- "unicodeVersion": "9.0",
- "digest": "60baca7855516c86883de6fd63f154b047d27325590c751089404e7287df56aa"
+ "beginner": {
+ "category": "symbols",
+ "moji": "🔰",
+ "description": "japanese symbol for beginner",
+ "unicodeVersion": "6.0",
+ "digest": "330defe4283c7387645422a58f8ccdba013087938a21e726bad64abaafc5d2c3"
},
- "face_palm_tone1": {
- "category": "people",
- "moji": "🤦🏻",
- "description": "face palm tone 1",
- "unicodeVersion": "9.0",
- "digest": "f505ab5fc98f2e3bf5893f59484b90900e7c04c6f0467e0c89ccf99c5fd76715"
+ "o": {
+ "category": "symbols",
+ "moji": "⭕",
+ "description": "heavy large circle",
+ "unicodeVersion": "5.2",
+ "digest": "31fd3373121e1690bd2368bb8964007bae2b151345184bd30bc719320b7540b0"
},
- "face_palm_tone2": {
- "category": "people",
- "moji": "🤦🏼",
- "description": "face palm tone 2",
- "unicodeVersion": "9.0",
- "digest": "9a3361cef95736983bbd54375ce9bf59c0fb1608f445da9dce79617cf05841bb"
+ "white_check_mark": {
+ "category": "symbols",
+ "moji": "✅",
+ "description": "white heavy check mark",
+ "unicodeVersion": "6.0",
+ "digest": "6f37f4b2dd017d42bb070d2544dce135a1c11203c5cb537c760b3c90d17bc0c3"
},
- "face_palm_tone3": {
- "category": "people",
- "moji": "🤦🏽",
- "description": "face palm tone 3",
- "unicodeVersion": "9.0",
- "digest": "d53cda9e9909988ec6ca4b9ee5735bac82ca53ee890f1c0eca50471ddbb044d3"
+ "ballot_box_with_check": {
+ "category": "symbols",
+ "moji": "☑",
+ "description": "ballot box with check",
+ "unicodeVersion": "1.1",
+ "digest": "81e61184a557724ccfe7c2f8e2005f053e134ed19c9c8e4aa5890d9fefc32392"
},
- "face_palm_tone4": {
- "category": "people",
- "moji": "🤦🏾",
- "description": "face palm tone 4",
- "unicodeVersion": "9.0",
- "digest": "51d8cd0103356c08c4fe5cb6d68ac09688d08ea9253d6ccc09b4900730a89d80"
+ "heavy_check_mark": {
+ "category": "symbols",
+ "moji": "✔",
+ "description": "heavy check mark",
+ "unicodeVersion": "1.1",
+ "digest": "206dc92526366341e4eef274354104ac2d6a464dfc253c0459c6943556ab3d64"
},
- "face_palm_tone5": {
- "category": "people",
- "moji": "🤦🏿",
- "description": "face palm tone 5",
- "unicodeVersion": "9.0",
- "digest": "7c044e1662880c143f8f2b15eee0d5aeb2c6831ee70ff5dcc3796f2585f5f6b0"
+ "x": {
+ "category": "symbols",
+ "moji": "❌",
+ "description": "cross mark",
+ "unicodeVersion": "6.0",
+ "digest": "d07158957d15f5b6c6fc56a9658a82ee1fa58f7b8b19d121a1713c9bf38cbdfc"
},
- "factory": {
- "category": "travel",
- "moji": "🏭",
- "description": "factory",
+ "negative_squared_cross_mark": {
+ "category": "symbols",
+ "moji": "❎",
+ "description": "negative squared cross mark",
"unicodeVersion": "6.0",
- "digest": "e71fc6d2fb903490f28565cbea6c3ca7384adc24aa0ba3ae481ca12255a015d3"
+ "digest": "cb116b6722949b59acdcb92a8a46fb4ab8f0208dafca73a55b49b09b2d8d37a4"
},
- "fallen_leaf": {
- "category": "nature",
- "moji": "🍂",
- "description": "fallen leaf",
+ "curly_loop": {
+ "category": "symbols",
+ "moji": "➰",
+ "description": "curly loop",
"unicodeVersion": "6.0",
- "digest": "96c9e1139937dc4be942855f4b1f140cb06809c51a818d078f4680e0375ddc1c"
+ "digest": "365b37a4afb451ace34de1a59727467bfe6f501437dc3459ec55a0234d98e307"
},
- "family": {
- "category": "people",
- "moji": "👪",
- "description": "family",
+ "loop": {
+ "category": "symbols",
+ "moji": "➿",
+ "description": "double curly loop",
"unicodeVersion": "6.0",
- "digest": "363da6588b6816c4f9811c76fee6601e3111030a1ba86fea9464c60568144c50"
+ "digest": "f9044b19663fb8e3e21aeafa6e1022e20be4d61683f13f1472b1feedc0140d7b"
},
- "family_mmb": {
- "category": "people",
- "moji": "👨‍👨‍👦",
- "description": "family (man,man,boy)",
+ "part_alternation_mark": {
+ "category": "symbols",
+ "moji": "〽",
+ "description": "part alternation mark",
+ "unicodeVersion": "3.2",
+ "digest": "2cf463fbc9bc4b1f63ec7c4c5cf9601b860c52c6f052c971c8b0c7f4cabe413d"
+ },
+ "eight_spoked_asterisk": {
+ "category": "symbols",
+ "moji": "✳",
+ "description": "eight spoked asterisk",
+ "unicodeVersion": "1.1",
+ "digest": "65fb63267575cb5637fed6ff99327cab54301baa39024ffae8fc8dd3db3516f9"
+ },
+ "eight_pointed_black_star": {
+ "category": "symbols",
+ "moji": "✴",
+ "description": "eight pointed black star",
+ "unicodeVersion": "1.1",
+ "digest": "a06b0825d8288487c0a27cf858b1bdc3f9144683fe9ceb9608450f21dc278db8"
+ },
+ "sparkle": {
+ "category": "symbols",
+ "moji": "❇",
+ "description": "sparkle",
+ "unicodeVersion": "1.1",
+ "digest": "8aab76f3a4f25b2e583fe675546e400e96417bc99e1c7ed08007d3afaaffc9a1"
+ },
+ "copyright": {
+ "category": "symbols",
+ "moji": "©️",
+ "description": "copyright sign",
+ "unicodeVersion": "1.1",
+ "digest": "681f772f6710df90e3b53e2ee6c3170f344873bd4c1e41e2016a411125b90f4a"
+ },
+ "registered": {
+ "category": "symbols",
+ "moji": "®️",
+ "description": "registered sign",
+ "unicodeVersion": "1.1",
+ "digest": "aeabdec7e5ddeed91ca77a6b926d76362e90d955225cbba6cee8b26442661e10"
+ },
+ "tm": {
+ "category": "symbols",
+ "moji": "™️",
+ "description": "trade mark sign",
+ "unicodeVersion": "1.1",
+ "digest": "aaa0898628f473e4e8df05d707b2d49a854ce3fbb78b8ca37ac0560b329fd8ea"
+ },
+ "hash": {
+ "category": "symbols",
+ "moji": "#⃣",
+ "description": "number sign",
+ "unicodeVersion": "3.0",
+ "digest": "941108d47089055455e13b981cd37b0437cd72d5c19ad397254494184060366e"
+ },
+ "asterisk": {
+ "category": "symbols",
+ "moji": "*⃣",
+ "description": "keycap asterisk",
+ "unicodeVersion": "3.0",
+ "digest": "7f65396609bdbffe6bf305cbfe56c4274f063f235d9e505b66367a71ee1cf233"
+ },
+ "zero": {
+ "category": "symbols",
+ "moji": "0️⃣",
+ "description": "keycap digit zero",
+ "unicodeVersion": "3.0",
+ "digest": "ef62df7416e51b02ff22787a981235053baabbb872247605e97834d9a7caff49"
+ },
+ "one": {
+ "category": "symbols",
+ "moji": "1️⃣",
+ "description": "keycap digit one",
+ "unicodeVersion": "3.0",
+ "digest": "6e3c571545f5ec85e72b1d694adbcb764099bbcb512b99c89ac8c8b1b6e1de89"
+ },
+ "two": {
+ "category": "symbols",
+ "moji": "2️⃣",
+ "description": "keycap digit two",
+ "unicodeVersion": "3.0",
+ "digest": "d091a45f6754587ac8602259955a5d2d1e41d090a1da1c81e46f286ab3de1385"
+ },
+ "three": {
+ "category": "symbols",
+ "moji": "3️⃣",
+ "description": "keycap digit three",
+ "unicodeVersion": "3.0",
+ "digest": "e66cfca0c1871d16283fcdddc7b170ad67c0a52f21d54985e4789daa1c61ee63"
+ },
+ "four": {
+ "category": "symbols",
+ "moji": "4️⃣",
+ "description": "keycap digit four",
+ "unicodeVersion": "3.0",
+ "digest": "10495232058ba7d32dad0aae87f30c50a96a95055dd4fa0f278fcc8c1aa24d84"
+ },
+ "five": {
+ "category": "symbols",
+ "moji": "5️⃣",
+ "description": "keycap digit five",
+ "unicodeVersion": "3.0",
+ "digest": "f763042ce9ec1e56342edf89b643c68019b103e0b1c812e130289d2534d6d914"
+ },
+ "six": {
+ "category": "symbols",
+ "moji": "6️⃣",
+ "description": "keycap digit six",
+ "unicodeVersion": "3.0",
+ "digest": "f455fcc89917bf67c1ac4245399146912578582cdff0e1e8bc216a4c4b7c43a0"
+ },
+ "seven": {
+ "category": "symbols",
+ "moji": "7️⃣",
+ "description": "keycap digit seven",
+ "unicodeVersion": "3.0",
+ "digest": "e9c95466693be79dff2e1d8eadddfc75967e6f7f641a21efcabccf173a8224c7"
+ },
+ "eight": {
+ "category": "symbols",
+ "moji": "8️⃣",
+ "description": "keycap digit eight",
+ "unicodeVersion": "3.0",
+ "digest": "0cc5beaaf4cf5efc0dc7450327ca264669641deebe0696997b578ca603eadd80"
+ },
+ "nine": {
+ "category": "symbols",
+ "moji": "9️⃣",
+ "description": "keycap digit nine",
+ "unicodeVersion": "3.0",
+ "digest": "c4f0670503ff77ffd10b8de9f0b5f240283cdf7323889dec6a38240fa5ddeae1"
+ },
+ "ten": {
+ "category": "symbols",
+ "moji": "🔟",
+ "description": "keycap ten",
"unicodeVersion": "6.0",
- "digest": "4464331734f8afbc33ebd38552ea1452813dc9e0295e315e4bcadf4876444474"
+ "digest": "4474d95b39371042bf8dfac128d68f391fb10efbc9c245de5dc6aefb10544439"
},
- "family_mmbb": {
- "category": "people",
- "moji": "👨‍👨‍👦‍👦",
- "description": "family (man,man,boy,boy)",
+ "capital_abcd": {
+ "category": "symbols",
+ "moji": "🔠",
+ "description": "input symbol for latin capital letters",
"unicodeVersion": "6.0",
- "digest": "ad976771f346f8c48d0a0077ee8ec427048937814a03a8691d422dd664991734"
+ "digest": "6cd31a4765c108d5e85f0ad9e25dee8e8f8af1fc40de161c0407f4041db9e288"
},
- "family_mmg": {
- "category": "people",
- "moji": "👨‍👨‍👧",
- "description": "family (man,man,girl)",
+ "abcd": {
+ "category": "symbols",
+ "moji": "🔡",
+ "description": "input symbol for latin small letters",
"unicodeVersion": "6.0",
- "digest": "961e428e8dfccab9575c6add7fc1523643caa2da47890b2baaa265c5f0ea0d8b"
+ "digest": "ad9982f0e3f4c346a6536d8b2a65c625f05278510910f656e59a6d280a90d3c2"
},
- "family_mmgb": {
- "category": "people",
- "moji": "👨‍👨‍👧‍👦",
- "description": "family (man,man,girl,boy)",
+ "1234": {
+ "category": "symbols",
+ "moji": "🔢",
+ "description": "input symbol for numbers",
"unicodeVersion": "6.0",
- "digest": "7240b99930a16295b3396d7183ecad125423d019b08f11746969c3939005ceff"
+ "digest": "6f276a9127f2de22f508978bd645974d5e21dfd3cf138e0cb00f33599677b533"
},
- "family_mmgg": {
- "category": "people",
- "moji": "👨‍👨‍👧‍👧",
- "description": "family (man,man,girl,girl)",
+ "symbols": {
+ "category": "symbols",
+ "moji": "🔣",
+ "description": "input symbol for symbols",
"unicodeVersion": "6.0",
- "digest": "2f2d726791f01c880df79431dc9472724f61d621b8b17344a9839db6f65b8cd7"
+ "digest": "ff91761a5def3885f52b44abc14a3073800f6ca6f5a61480c902917896843dc1"
},
- "family_mwbb": {
- "category": "people",
- "moji": "👨‍👩‍👦‍👦",
- "description": "family (man,woman,boy,boy)",
+ "abc": {
+ "category": "symbols",
+ "moji": "🔤",
+ "description": "input symbol for latin letters",
"unicodeVersion": "6.0",
- "digest": "e0056e604ec4d411ebb4aefc079f47efb11bf7db4c3ebf9d17759f40075a19a5"
+ "digest": "9991fd68e58377848e6e1b8d4b74bdbfd09e575686047651f139c126e9df6c4c"
},
- "family_mwg": {
- "category": "people",
- "moji": "👨‍👩‍👧",
- "description": "family (man,woman,girl)",
+ "a": {
+ "category": "symbols",
+ "moji": "🅰",
+ "description": "negative squared latin capital letter a",
"unicodeVersion": "6.0",
- "digest": "3c256c9982e40db556a4667e6a666c06f6088249b5cc260e92bd443cc5d85b1c"
+ "digest": "3a5aea7fbabb9e1a5e364f937704fd21296323470fdb1e2bf767d07516c94d21"
},
- "family_mwgb": {
- "category": "people",
- "moji": "👨‍👩‍👧‍👦",
- "description": "family (man,woman,girl,boy)",
+ "ab": {
+ "category": "symbols",
+ "moji": "🆎",
+ "description": "negative squared ab",
"unicodeVersion": "6.0",
- "digest": "09481d128ece6416b169b99a9b770a1c72f662adde86fa6c214a1f979e98f8bd"
+ "digest": "2a58932a5ab57aa3c82f77e5df5cd9ce5103667483bb93a6c96f31e171577654"
},
- "family_mwgg": {
- "category": "people",
- "moji": "👨‍👩‍👧‍👧",
- "description": "family (man,woman,girl,girl)",
+ "b": {
+ "category": "symbols",
+ "moji": "🅱",
+ "description": "negative squared latin capital letter b",
"unicodeVersion": "6.0",
- "digest": "2e9670f98d310c15491a66a4108773cc994401cc864923c0fc1c61123b99c1b5"
+ "digest": "ae89fc972ef5c80863022ef169e3ceb4bfdece3cf052fe9023516904db23b8a0"
},
- "family_wwb": {
- "category": "people",
- "moji": "👩‍👩‍👦",
- "description": "family (woman,woman,boy)",
+ "cl": {
+ "category": "symbols",
+ "moji": "🆑",
+ "description": "squared cl",
"unicodeVersion": "6.0",
- "digest": "df970eeb8fe917398b9dfd68b181ec6cfe1f7e981af2f67e5d8c96a228aa8147"
+ "digest": "071117bab8c92f8ea4b734b358d3591ef4b4df067514b80f747b6293efc77ec7"
},
- "family_wwbb": {
- "category": "people",
- "moji": "👩‍👩‍👦‍👦",
- "description": "family (woman,woman,boy,boy)",
+ "cool": {
+ "category": "symbols",
+ "moji": "🆒",
+ "description": "squared cool",
"unicodeVersion": "6.0",
- "digest": "d6e262911da6a7fc47d49642666f5068e597976c1760d25758114cad16b1ac1c"
+ "digest": "d9e004043f68d0dfea5cb4ae961c4e66edf32a840bf5f1dae43df3c522db6322"
},
- "family_wwg": {
- "category": "people",
- "moji": "👩‍👩‍👧",
- "description": "family (woman,woman,girl)",
+ "free": {
+ "category": "symbols",
+ "moji": "🆓",
+ "description": "squared free",
"unicodeVersion": "6.0",
- "digest": "1f64c8a10f813344c4d10ab78fb55c2a2c221101e8645c6b6dcc0c8ca40c029c"
+ "digest": "a3afee0d4e8da8f6fc2dc42b453b6f0aa9b6e159e24fce1afa04e92b74a69fc4"
},
- "family_wwgb": {
- "category": "people",
- "moji": "👩‍👩‍👧‍👦",
- "description": "family (woman,woman,girl,boy)",
+ "information_source": {
+ "category": "symbols",
+ "moji": "ℹ",
+ "description": "information source",
+ "unicodeVersion": "3.0",
+ "digest": "9deada49f605373b64d9eefa432f73cf07287ebe254233823109d133dc86dc23"
+ },
+ "id": {
+ "category": "symbols",
+ "moji": "🆔",
+ "description": "squared id",
"unicodeVersion": "6.0",
- "digest": "d0962105cff6755305a08c0a0f748cb4fdfe3f78c9add25640dac05a8e4200e6"
+ "digest": "63c1717c82c487db9a3e2a31be498e3cc88349cb0f2a5010b77be4bd51ba012b"
},
- "family_wwgg": {
- "category": "people",
- "moji": "👩‍👩‍👧‍👧",
- "description": "family (woman,woman,girl,girl)",
+ "m": {
+ "category": "symbols",
+ "moji": "Ⓜ",
+ "description": "circled latin capital letter m",
+ "unicodeVersion": "1.1",
+ "digest": "45f66b77808cb780aee7c3440bf57f20f0a7ba3a6048806395e88c47b58263b2"
+ },
+ "new": {
+ "category": "symbols",
+ "moji": "🆕",
+ "description": "squared new",
"unicodeVersion": "6.0",
- "digest": "9733e9a03cccda66612c0274842e514092d1816dba914f337ba422e988225352"
+ "digest": "fe42aef2603c69544297c0330168c37a844a191cb541344f49b2b0e3b7b01457"
},
- "fast_forward": {
+ "ng": {
"category": "symbols",
- "moji": "⏩",
- "description": "black right-pointing double triangle",
+ "moji": "🆖",
+ "description": "squared ng",
"unicodeVersion": "6.0",
- "digest": "4ad792e67e07120968468561f20df83f39fa5800b8212a64e76fc36cf27464c3"
+ "digest": "53b461719257c3a5d4c413bebc6a4ab5b6c8c66a76f47f5354fc800746fef815"
},
- "fax": {
- "category": "objects",
- "moji": "📠",
- "description": "fax machine",
+ "o2": {
+ "category": "symbols",
+ "moji": "🅾",
+ "description": "negative squared latin capital letter o",
"unicodeVersion": "6.0",
- "digest": "cf7823d54b57edbf46384c7833efc4ce9ddef29786d74a89a8af1e8ed2e7d599"
+ "digest": "672d9a179c75b9375f51175db75154a8b0006e60a9b4546658d9cddca8ab0a86"
},
- "fearful": {
- "category": "people",
- "moji": "😨",
- "description": "fearful face",
+ "ok": {
+ "category": "symbols",
+ "moji": "🆗",
+ "description": "squared ok",
"unicodeVersion": "6.0",
- "digest": "ce0c48c3ede7231acd645ffa0fe3dfec012e6f88a30431c44e3b816063ac42b4"
+ "digest": "df301dcd8dc7df31e26a6ba360e8f8b3c0f5bd4869301fde41ccfa3271866ab9"
},
- "feet": {
- "category": "nature",
- "moji": "🐾",
- "description": "paw prints",
+ "parking": {
+ "category": "symbols",
+ "moji": "🅿",
+ "description": "negative squared latin capital letter p",
+ "unicodeVersion": "5.2",
+ "digest": "ccc4a7c9d2d6a1f1fd0788a91618a5178ce8e926d1ebe033ab6d6f77b47b2440"
+ },
+ "sos": {
+ "category": "symbols",
+ "moji": "🆘",
+ "description": "squared sos",
"unicodeVersion": "6.0",
- "digest": "cb4b59f2c5df407bae3d02b638721b7f47b685d4e03f7ddb8d1fedaae9e2179a"
+ "digest": "13dcfd9239e12ebdb00b2e3b632e26fa1160ed645226f21b6cef5a7f3a9690fe"
},
- "fencer": {
- "category": "activity",
- "moji": "🤺",
- "description": "fencer",
- "unicodeVersion": "9.0",
- "digest": "8fe2320e2bf8ae87bdd7b2cb519146fba1cc5f1df8c426f83a780544bfec3785"
+ "up": {
+ "category": "symbols",
+ "moji": "🆙",
+ "description": "squared up with exclamation mark",
+ "unicodeVersion": "6.0",
+ "digest": "807046ea55d94a542e355a6cf9c6bab5fe8523513669211ab1c13765348ff41a"
},
- "ferris_wheel": {
- "category": "travel",
- "moji": "🎡",
- "description": "ferris wheel",
+ "vs": {
+ "category": "symbols",
+ "moji": "🆚",
+ "description": "squared vs",
"unicodeVersion": "6.0",
- "digest": "a2eabc2a9f677373e013a100fc0f3b1e55d9aab24fc77056ee2bfef7dfc19add"
+ "digest": "1e67a891ce028db2e6fb8506879d3c0edbf024012a75a5073e89aa834c6f314e"
},
- "ferry": {
- "category": "travel",
- "moji": "⛴",
- "description": "ferry",
- "unicodeVersion": "5.2",
- "digest": "977c47292fd51ecd5d558a309405b15fc64b9b8edbedf95d8af30fa40636e6d0"
+ "koko": {
+ "category": "symbols",
+ "moji": "🈁",
+ "description": "squared katakana koko",
+ "unicodeVersion": "6.0",
+ "digest": "3d49c8510a5e6c915d88c026897987d8a1d52aca5d498ef7d86fcc76ebb838e9"
},
- "field_hockey": {
- "category": "activity",
- "moji": "🏑",
- "description": "field hockey stick and ball",
- "unicodeVersion": "8.0",
- "digest": "8608cd6944c9d3791e5c0b3111f7c5ca6155f64552ae9ab3e2f93514d053e042"
+ "sa": {
+ "category": "symbols",
+ "moji": "🈂",
+ "description": "squared katakana sa",
+ "unicodeVersion": "6.0",
+ "digest": "58e18447fd35e85c8a7c45eab01da1e2d6ff67d933e0ea9d5875a5e83273e733"
},
- "file_cabinet": {
- "category": "objects",
- "moji": "🗄",
- "description": "file cabinet",
- "unicodeVersion": "7.0",
- "digest": "29cf5c9587eb08a6736e37a6c7e01ad7e1453dd770c02c743604c2abf314d90f"
+ "u6708": {
+ "category": "symbols",
+ "moji": "🈷",
+ "description": "squared cjk unified ideograph-6708",
+ "unicodeVersion": "6.0",
+ "digest": "de0a0568e1a0de7653dac4dd093ee95027087b301d914752bafbda4648724847"
},
- "file_folder": {
- "category": "objects",
- "moji": "📁",
- "description": "file folder",
+ "u6709": {
+ "category": "symbols",
+ "moji": "🈶",
+ "description": "squared cjk unified ideograph-6709",
"unicodeVersion": "6.0",
- "digest": "25f9840f4a2bc80558ef9ed231f88a7031bc4b0aad087a7be9748fb006a37288"
+ "digest": "5370fc51eed9c35a7a695881cdb3218b2fda9d70b1a887a5f2dcd7bdd0f5f3bc"
},
- "film_frames": {
- "category": "objects",
- "moji": "🎞",
- "description": "film frames",
- "unicodeVersion": "7.0",
- "digest": "ddc2c8bf00765b8a91bff22a06dba1cef2a78d89afb9515e55dc417cfcba570e"
+ "u6307": {
+ "category": "symbols",
+ "moji": "🈯",
+ "description": "squared cjk unified ideograph-6307",
+ "unicodeVersion": "5.2",
+ "digest": "6ecd33c04af54469548189adc89a12ea3677b7d76af6349221bab3ac17c51011"
},
- "fingers_crossed": {
- "category": "people",
- "moji": "🤞",
- "description": "hand with first and index finger crossed",
- "unicodeVersion": "9.0",
- "digest": "fee443087ed38c26487edd1bd8866ce399ef6a8eb98342fc0110bd2f77835253"
+ "ideograph_advantage": {
+ "category": "symbols",
+ "moji": "🉐",
+ "description": "circled ideograph advantage",
+ "unicodeVersion": "6.0",
+ "digest": "b07fab8069772a132928e38b64d8e35661b33757176572c153f4563460dd5ddc"
},
- "fingers_crossed_tone1": {
- "category": "people",
- "moji": "🤞🏻",
- "description": "hand with index and middle fingers crossed tone 1",
- "unicodeVersion": "9.0",
- "digest": "b91c28f9644867ff41f22c1f80b6c9ee120413ae62f30772c9853d76987fed05"
+ "u5272": {
+ "category": "symbols",
+ "moji": "🈹",
+ "description": "squared cjk unified ideograph-5272",
+ "unicodeVersion": "6.0",
+ "digest": "146800348d2954ab707184573fd7a5be20971d6df655f37829f88a8678d4f54b"
},
- "fingers_crossed_tone2": {
- "category": "people",
- "moji": "🤞🏼",
- "description": "hand with index and middle fingers crossed tone 2",
- "unicodeVersion": "9.0",
- "digest": "9d4ea025f1daa063d14e8d26c3c94a0c86379b214ac8125acc404820fd1ddbb5"
+ "u7121": {
+ "category": "symbols",
+ "moji": "🈚",
+ "description": "squared cjk unified ideograph-7121",
+ "unicodeVersion": "5.2",
+ "digest": "3df28fb3345962cc97b32391bcb9dfd1df82bfadf1dc3567122fd086aff59cb3"
},
- "fingers_crossed_tone3": {
- "category": "people",
- "moji": "🤞🏽",
- "description": "hand with index and middle fingers crossed tone 3",
- "unicodeVersion": "9.0",
- "digest": "23ee099706ec89b6bbc6f2911f9f1571822192d913516279a5318d92d01bc754"
+ "u7981": {
+ "category": "symbols",
+ "moji": "🈲",
+ "description": "squared cjk unified ideograph-7981",
+ "unicodeVersion": "6.0",
+ "digest": "e61e1ed43e0aa5747ad99050d48550ac23a8c6bfa7de7114b03a17eb0366872a"
},
- "fingers_crossed_tone4": {
- "category": "people",
- "moji": "🤞🏾",
- "description": "hand with index and middle fingers crossed tone 4",
- "unicodeVersion": "9.0",
- "digest": "cda52831530605511b13a4953136dd5f36fca1fbf3d11ac261caac862086aff9"
+ "accept": {
+ "category": "symbols",
+ "moji": "🉑",
+ "description": "circled ideograph accept",
+ "unicodeVersion": "6.0",
+ "digest": "d4dcdfdb5dfcd5374044568d879662e89bb5269fb789901e5468c07243f32143"
},
- "fingers_crossed_tone5": {
- "category": "people",
- "moji": "🤞🏿",
- "description": "hand with index and middle fingers crossed tone 5",
- "unicodeVersion": "9.0",
- "digest": "b49c1b15808715547f6f04113cf1631f4d79f8c1fec5f9ecdeea60284f9b4dad"
+ "u7533": {
+ "category": "symbols",
+ "moji": "🈸",
+ "description": "squared cjk unified ideograph-7533",
+ "unicodeVersion": "6.0",
+ "digest": "014087496357083b2fa0f6372f3c768c9d0cde6f290460c8b4ecfa322955a065"
},
- "fire": {
- "category": "nature",
- "moji": "🔥",
- "description": "fire",
+ "u5408": {
+ "category": "symbols",
+ "moji": "🈴",
+ "description": "squared cjk unified ideograph-5408",
"unicodeVersion": "6.0",
- "digest": "37ab0faba2cb2a665775cdde7aaf4b4a0dfdd21030b57a613ddc9cb91883d7e4"
+ "digest": "c65c6b24648ec959bb497872003adf9f29db19f3bc3ffc5f3c6d03ec36f7e7ed"
},
- "fire_engine": {
- "category": "travel",
- "moji": "🚒",
- "description": "fire engine",
+ "u7a7a": {
+ "category": "symbols",
+ "moji": "🈳",
+ "description": "squared cjk unified ideograph-7a7a",
"unicodeVersion": "6.0",
- "digest": "c81e1df702689c4c3895862e2da1092f30f5c415c0e9726f0b2c5c42ed9660ce"
+ "digest": "d306d00de7a978823adf885d3beb23c41359a2bb804425b8652f59f5ceadd40d"
},
- "fireworks": {
- "category": "travel",
- "moji": "🎆",
- "description": "fireworks",
+ "congratulations": {
+ "category": "symbols",
+ "moji": "㊗",
+ "description": "circled ideograph congratulation",
+ "unicodeVersion": "1.1",
+ "digest": "3c4f04a0b13353e975bdcd489f5cc2899b3dace2c83ca3012605f1b2801b581e"
+ },
+ "secret": {
+ "category": "symbols",
+ "moji": "㊙",
+ "description": "circled ideograph secret",
+ "unicodeVersion": "1.1",
+ "digest": "a67c62b033cebaec448c90f0f98960058fd020fe523dbd315e6b60966fb89da7"
+ },
+ "u55b6": {
+ "category": "symbols",
+ "moji": "🈺",
+ "description": "squared cjk unified ideograph-55b6",
"unicodeVersion": "6.0",
- "digest": "6242b48df4b30753b154ec6d5f3883646fa42b55a629a7a1273d290e97df8a41"
+ "digest": "0c441fbe4e8527c04c44a297150b2c3dab5f5ba51acffd1adc3109057ec20522"
},
- "first_place": {
- "category": "activity",
- "moji": "🥇",
- "description": "first place medal",
- "unicodeVersion": "9.0",
- "digest": "d250c1890f341665d689d705c13fc4c5448d870d193b8b3405832e2fd92f5385"
+ "u6e80": {
+ "category": "symbols",
+ "moji": "🈵",
+ "description": "squared cjk unified ideograph-6e80",
+ "unicodeVersion": "6.0",
+ "digest": "cc9fbe7d7a29f3d309e0c7dedacf3b19afb4f0689aebcb0ffbdf06febd961842"
},
- "first_quarter_moon": {
- "category": "nature",
- "moji": "🌓",
- "description": "first quarter moon symbol",
+ "red_circle": {
+ "category": "symbols",
+ "moji": "🔴",
+ "description": "large red circle",
"unicodeVersion": "6.0",
- "digest": "8bcc5d70d27f52098378fc800041429f726323ee840276c7901f4b525778a8f2"
+ "digest": "a76514fffb0fec7b8994aa202144350f3257c4562c02a451c5727d20ce0f21ac"
},
- "first_quarter_moon_with_face": {
- "category": "nature",
- "moji": "🌛",
- "description": "first quarter moon with face",
+ "large_blue_circle": {
+ "category": "symbols",
+ "moji": "🔵",
+ "description": "large blue circle",
"unicodeVersion": "6.0",
- "digest": "a878335d845f1ec830614307fe3d48e346730ec62d3334687aaef43d4a9033e6"
+ "digest": "7588a2b1c1baef733d6e7572580c51dcd81bb0c85c77080dfd136d3a995ef3cc"
},
- "fish": {
- "category": "nature",
- "moji": "🐟",
- "description": "fish",
+ "black_circle": {
+ "category": "symbols",
+ "moji": "⚫",
+ "description": "medium black circle",
+ "unicodeVersion": "4.1",
+ "digest": "dd472d37a09519c4b35d7cce0c1d988cc78abc7eec6ab794f49c75ac1c506450"
+ },
+ "white_circle": {
+ "category": "symbols",
+ "moji": "⚪",
+ "description": "medium white circle",
+ "unicodeVersion": "4.1",
+ "digest": "386429066322ba83e4790d7e930c167a6789f978f65e2131f0534230f196d40a"
+ },
+ "black_large_square": {
+ "category": "symbols",
+ "moji": "⬛",
+ "description": "black large square",
+ "unicodeVersion": "5.1",
+ "digest": "2c606063c452385e38808861d820d799c25450d777c58faa3a58030e32d40d0c"
+ },
+ "white_large_square": {
+ "category": "symbols",
+ "moji": "⬜",
+ "description": "white large square",
+ "unicodeVersion": "5.1",
+ "digest": "a56e16d3a7778cfec18b031b9f0f04aa008d3b81a2dc4642366f7d92adc1d862"
+ },
+ "black_medium_square": {
+ "category": "symbols",
+ "moji": "◼",
+ "description": "black medium square",
+ "unicodeVersion": "3.2",
+ "digest": "7811b577922f508cf89ac34abdbe5285213b3519de340fe1d57ca6780a500066"
+ },
+ "white_medium_square": {
+ "category": "symbols",
+ "moji": "◻",
+ "description": "white medium square",
+ "unicodeVersion": "3.2",
+ "digest": "0328185858eb63e113136674509fb89f43ab5306449345e2e65d1ee2d616a54d"
+ },
+ "black_medium_small_square": {
+ "category": "symbols",
+ "moji": "◾",
+ "description": "black medium small square",
+ "unicodeVersion": "3.2",
+ "digest": "abd57777f919013b8c0686541263d62492ffd042a1d4b05000856dd1a3ff33ef"
+ },
+ "white_medium_small_square": {
+ "category": "symbols",
+ "moji": "◽",
+ "description": "white medium small square",
+ "unicodeVersion": "3.2",
+ "digest": "9950d2efd6bca2b2583f670c389f839b5346b9e608eb9b00628b196f9d6b37a3"
+ },
+ "black_small_square": {
+ "category": "symbols",
+ "moji": "▪",
+ "description": "black small square",
+ "unicodeVersion": "1.1",
+ "digest": "958483632202cf3181ef96532b0371cad1ea9b693100def41df69388b1ee468a"
+ },
+ "white_small_square": {
+ "category": "symbols",
+ "moji": "▫",
+ "description": "white small square",
+ "unicodeVersion": "1.1",
+ "digest": "a805e7c1edcf6668a6bdc8324e9ec78da8aa4a79f789f55ce6828337b2f8e43c"
+ },
+ "large_orange_diamond": {
+ "category": "symbols",
+ "moji": "🔶",
+ "description": "large orange diamond",
"unicodeVersion": "6.0",
- "digest": "036332286b852c6dd8c672f1540a4b4c5082885555f60a7d0f93b82c2acdc0f5"
+ "digest": "30bee888a5d3dc39a11132fac8a490d8dab4237cc62f1529f5c3e6e77b8f29a4"
},
- "fish_cake": {
- "category": "food",
- "moji": "🍥",
- "description": "fish cake with swirl design",
+ "large_blue_diamond": {
+ "category": "symbols",
+ "moji": "🔷",
+ "description": "large blue diamond",
"unicodeVersion": "6.0",
- "digest": "2289ec22dc4e1cb1e143c56809f4d4f920e221f11237533cee95472a30985068"
+ "digest": "2e69e9e80dee0192403fe95aab3223208ccf97963dff54a061b6f2e3419820ef"
},
- "fishing_pole_and_fish": {
- "category": "activity",
- "moji": "🎣",
- "description": "fishing pole and fish",
+ "small_orange_diamond": {
+ "category": "symbols",
+ "moji": "🔸",
+ "description": "small orange diamond",
"unicodeVersion": "6.0",
- "digest": "539df418ee29c240216b28aa1af12feefe9273de148a80289c2a512785d98aee"
+ "digest": "7de47af62764c8136415214e7eb7b8e985ec600b79991c0ba44b96824350eeef"
},
- "fist": {
- "category": "people",
- "moji": "✊",
- "description": "raised fist",
+ "small_blue_diamond": {
+ "category": "symbols",
+ "moji": "🔹",
+ "description": "small blue diamond",
"unicodeVersion": "6.0",
- "digest": "5902870e6121df3f316a3c5f62b3e0a32679f3fca37fda165267d15001fbcfe4"
+ "digest": "22016e16c1e769099e972a93eaed9fd4131867020c0d6669aeb323d414ef1f93"
},
- "fist_tone1": {
- "category": "people",
- "moji": "✊🏻",
- "description": "raised fist tone 1",
- "unicodeVersion": "8.0",
- "digest": "9c4e0289af6fd5a9fdb5b77396d37d93f97bf0f302c20a980dc4663121b31eb9"
+ "small_red_triangle": {
+ "category": "symbols",
+ "moji": "🔺",
+ "description": "up-pointing red triangle",
+ "unicodeVersion": "6.0",
+ "digest": "521e2b28387cd5d13d17c5dedf6b944c0139f36bc6136e5bcca5b59ee1a59823"
},
- "fist_tone2": {
- "category": "people",
- "moji": "✊🏼",
- "description": "raised fist tone 2",
- "unicodeVersion": "8.0",
- "digest": "6784ddda1bef3dee456860266c9d408efddbfd50566115f674091ae338d8a8bd"
+ "small_red_triangle_down": {
+ "category": "symbols",
+ "moji": "🔻",
+ "description": "down-pointing red triangle",
+ "unicodeVersion": "6.0",
+ "digest": "cfbea3a1506cd1f26aa11603421d2137c3a847f4dc1d0728c16901e0e4195adc"
},
- "fist_tone3": {
- "category": "people",
- "moji": "✊🏽",
- "description": "raised fist tone 3",
- "unicodeVersion": "8.0",
- "digest": "0355b2238cabd2edcedbb47b548db789bcb1ccbd1fc56090e2b5976db0f792ef"
+ "diamond_shape_with_a_dot_inside": {
+ "category": "symbols",
+ "moji": "💠",
+ "description": "diamond shape with a dot inside",
+ "unicodeVersion": "6.0",
+ "digest": "785dedaf65b4c8a8b2a758608563528cae6d65864e36b17a45b7b4b15fce67e5"
},
- "fist_tone4": {
- "category": "people",
- "moji": "✊🏾",
- "description": "raised fist tone 4",
- "unicodeVersion": "8.0",
- "digest": "2e29b3c36c7260c6f327941b4345be2642f034f36c6b5cb9ffcff0ee7a6140b3"
+ "radio_button": {
+ "category": "symbols",
+ "moji": "🔘",
+ "description": "radio button",
+ "unicodeVersion": "6.0",
+ "digest": "7e74da4889650286464a22035c3297d914c1e52c266d5accbffe03d8f76069de"
},
- "fist_tone5": {
- "category": "people",
- "moji": "✊🏿",
- "description": "raised fist tone 5",
- "unicodeVersion": "8.0",
- "digest": "3d69c0e6b12f4e6ec6b9e6a3b2974b880c4017d92bfe77bf5e14e8cc713d1c8c"
+ "white_square_button": {
+ "category": "symbols",
+ "moji": "🔳",
+ "description": "white square button",
+ "unicodeVersion": "6.0",
+ "digest": "bc65aefa55ddee130c006624bcd381313eba7261c99f8b273a938d273b84570a"
},
- "five": {
+ "black_square_button": {
"category": "symbols",
- "moji": "5️⃣",
- "description": "keycap digit five",
- "unicodeVersion": "3.0",
- "digest": "f763042ce9ec1e56342edf89b643c68019b103e0b1c812e130289d2534d6d914"
+ "moji": "🔲",
+ "description": "black square button",
+ "unicodeVersion": "6.0",
+ "digest": "2211e8f2193751c4d1716a4d13145e945293f0158d413333829e7a20a265e55b"
+ },
+ "checkered_flag": {
+ "category": "travel",
+ "moji": "🏁",
+ "description": "chequered flag",
+ "unicodeVersion": "6.0",
+ "digest": "b2527f2ce6797b261083947b9e9dd588382da6e7e46f81a1919d0f039635f21b"
+ },
+ "triangular_flag_on_post": {
+ "category": "objects",
+ "moji": "🚩",
+ "description": "triangular flag on post",
+ "unicodeVersion": "6.0",
+ "digest": "135c571161ba6bd55d4940366202164a6c4c325064a7ebc76fbdb3f92428fd73"
+ },
+ "crossed_flags": {
+ "category": "objects",
+ "moji": "🎌",
+ "description": "crossed flags",
+ "unicodeVersion": "6.0",
+ "digest": "451e17cbb40af3f42c7f687186c3ba7d8c9a99826635f3733256c663a0be31cb"
+ },
+ "flag_black": {
+ "category": "objects",
+ "moji": "🏴",
+ "description": "waving black flag",
+ "unicodeVersion": "7.0",
+ "digest": "f147434c7baea64732877fa3d6e821071f31dad1ac50dcd77a991bd1c9052611"
+ },
+ "flag_white": {
+ "category": "objects",
+ "moji": "🏳",
+ "description": "waving white flag",
+ "unicodeVersion": "7.0",
+ "digest": "9793e7ce774497489f7b6023b7b2d51ce01d83dda21b0903432883905cd5aa0b"
+ },
+ "gay_pride_flag": {
+ "category": "flags",
+ "moji": "🏳️‍🌈",
+ "description": "gay_pride_flag",
+ "unicodeVersion": "7.0",
+ "digest": "f23d3e6d08cda225b5778b7f2ffe7ef6ab32709f43bd1e7ccb453ee660f64fa1"
},
"flag_ac": {
"category": "flags",
@@ -3548,13 +10947,6 @@
"unicodeVersion": "6.0",
"digest": "0d322e0289bab47daf300db3c0b88dc348548f62adb27abb20b972e409e2c0c6"
},
- "flag_black": {
- "category": "objects",
- "moji": "🏴",
- "description": "waving black flag",
- "unicodeVersion": "7.0",
- "digest": "f147434c7baea64732877fa3d6e821071f31dad1ac50dcd77a991bd1c9052611"
- },
"flag_bm": {
"category": "flags",
"moji": "🇧🇲",
@@ -5116,13 +12508,6 @@
"unicodeVersion": "6.0",
"digest": "9c3a87c9a8d72e7158ff64fd73754dab8df3c36ae7ffb1fac8e6326cac226995"
},
- "flag_white": {
- "category": "objects",
- "moji": "🏳",
- "description": "waving white flag",
- "unicodeVersion": "7.0",
- "digest": "9793e7ce774497489f7b6023b7b2d51ce01d83dda21b0903432883905cd5aa0b"
- },
"flag_ws": {
"category": "flags",
"moji": "🇼🇸",
@@ -5171,7390 +12556,5 @@
"description": "zimbabwe",
"unicodeVersion": "6.0",
"digest": "5c78e2789c261c2e8fddb8a790d5b6e0dfb4c4922de54a8fa26b1ecdc89eb58d"
- },
- "flags": {
- "category": "objects",
- "moji": "🎏",
- "description": "carp streamer",
- "unicodeVersion": "6.0",
- "digest": "fe687c07453ae2869f11816bc2c604c67ba8c3754e597ce6871289cc358253c1"
- },
- "flashlight": {
- "category": "objects",
- "moji": "🔦",
- "description": "electric torch",
- "unicodeVersion": "6.0",
- "digest": "2fd24a87d1609384d671d0d6b6c5d27afde221e28e0ce1443f09b8ab96200329"
- },
- "fleur-de-lis": {
- "category": "symbols",
- "moji": "⚜",
- "description": "fleur-de-lis",
- "unicodeVersion": "4.1",
- "digest": "aad1d75ba9bce98d639b82b8c2f6481947edec6ceaa48cb890f2503eb2964202"
- },
- "floppy_disk": {
- "category": "objects",
- "moji": "💾",
- "description": "floppy disk",
- "unicodeVersion": "6.0",
- "digest": "24b4d593478f5619160a95967deb34662d0b1a3f6da96f84910d749914179e13"
- },
- "flower_playing_cards": {
- "category": "symbols",
- "moji": "🎴",
- "description": "flower playing cards",
- "unicodeVersion": "6.0",
- "digest": "814948cd185c22b3a394a5bb15924ddf6cf07b612658bd79c5192cd453057681"
- },
- "flushed": {
- "category": "people",
- "moji": "😳",
- "description": "flushed face",
- "unicodeVersion": "6.0",
- "digest": "d29c62b5892744d9d95cd876c703d079f1e8eed6091e51291e01667b8f4a6c7a"
- },
- "fog": {
- "category": "nature",
- "moji": "🌫",
- "description": "fog",
- "unicodeVersion": "7.0",
- "digest": "d216852000dd40e0f8b4c407368631b60a56d8eac32832461b303cc8f4d47488"
- },
- "foggy": {
- "category": "travel",
- "moji": "🌁",
- "description": "foggy",
- "unicodeVersion": "6.0",
- "digest": "0ccc53edb0aba767c59682d74a29221f829297e334120d3b90e3030054698ec7"
- },
- "football": {
- "category": "activity",
- "moji": "🏈",
- "description": "american football",
- "unicodeVersion": "6.0",
- "digest": "aefdff9cd010c9d82e2cdd83c4430db38bf311bfeac7f86583b122cba146c73f"
- },
- "footprints": {
- "category": "people",
- "moji": "👣",
- "description": "footprints",
- "unicodeVersion": "6.0",
- "digest": "43a8daa470e070c1cfdf541a502076f0a258e8cde2f707ad842bc24e499ad69c"
- },
- "fork_and_knife": {
- "category": "food",
- "moji": "🍴",
- "description": "fork and knife",
- "unicodeVersion": "6.0",
- "digest": "a22c9fabbcae702d4043e4bfab3f15cd96e585a291f93f820f01723c0465410c"
- },
- "fork_knife_plate": {
- "category": "food",
- "moji": "🍽",
- "description": "fork and knife with plate",
- "unicodeVersion": "7.0",
- "digest": "3161044fa7ac67ef1f09c35ecde7ca8eca471a36e3e67ccbaa3a42f35dd67f16"
- },
- "fountain": {
- "category": "travel",
- "moji": "⛲",
- "description": "fountain",
- "unicodeVersion": "5.2",
- "digest": "f940be34d7db82c7dcf5e4343c05f74e5834f1123edd0382b9aa0c3b8b72e90d"
- },
- "four": {
- "category": "symbols",
- "moji": "4️⃣",
- "description": "keycap digit four",
- "unicodeVersion": "3.0",
- "digest": "10495232058ba7d32dad0aae87f30c50a96a95055dd4fa0f278fcc8c1aa24d84"
- },
- "four_leaf_clover": {
- "category": "nature",
- "moji": "🍀",
- "description": "four leaf clover",
- "unicodeVersion": "6.0",
- "digest": "a9ca1028812de7da08319eb0403b28951d381ace771b1050e90d1aa05e62baef"
- },
- "fox": {
- "category": "nature",
- "moji": "🦊",
- "description": "fox face",
- "unicodeVersion": "9.0",
- "digest": "0d98c8fd904dceaa53041b5c8dcf7c357805712ed547668ee53ab48808c6255b"
- },
- "frame_photo": {
- "category": "objects",
- "moji": "🖼",
- "description": "frame with picture",
- "unicodeVersion": "7.0",
- "digest": "bcc4eade7fb066c301457bfd79daae84f0b2a4aa56c71dcb6bcb8fb76c2c4a96"
- },
- "free": {
- "category": "symbols",
- "moji": "🆓",
- "description": "squared free",
- "unicodeVersion": "6.0",
- "digest": "a3afee0d4e8da8f6fc2dc42b453b6f0aa9b6e159e24fce1afa04e92b74a69fc4"
- },
- "french_bread": {
- "category": "food",
- "moji": "🥖",
- "description": "baguette bread",
- "unicodeVersion": "9.0",
- "digest": "356771456e476bfc85b9ba08b8bd8473fef0e8045af88bafe563aed815cba8e9"
- },
- "fried_shrimp": {
- "category": "food",
- "moji": "🍤",
- "description": "fried shrimp",
- "unicodeVersion": "6.0",
- "digest": "77985671090380d5e36b5c1bed476dcfe8e6e09d7a3f090aaae534be6297ecfd"
- },
- "fries": {
- "category": "food",
- "moji": "🍟",
- "description": "french fries",
- "unicodeVersion": "6.0",
- "digest": "492b7d015ce5bcd3b9674049e5421cd2c65451c65a175f2e7ffca391b178d1e5"
- },
- "frog": {
- "category": "nature",
- "moji": "🐸",
- "description": "frog face",
- "unicodeVersion": "6.0",
- "digest": "f1b86d108560eb022ed026b7158477f664bdebe2b313bf49e4bd4f3c79278bf3"
- },
- "frowning": {
- "category": "people",
- "moji": "😦",
- "description": "frowning face with open mouth",
- "unicodeVersion": "6.1",
- "digest": "615dc050d755f9fb9aa325fba6efc812ec78984dd282c8e81e4b0bf2ffb00b79"
- },
- "frowning2": {
- "category": "people",
- "moji": "☹",
- "description": "white frowning face",
- "unicodeVersion": "1.1",
- "digest": "aa6b9f39cd2511d918a395f1e363a34642761cdc7f5a0170405b40455a47f0b6"
- },
- "fuelpump": {
- "category": "travel",
- "moji": "⛽",
- "description": "fuel pump",
- "unicodeVersion": "5.2",
- "digest": "7c8c6c22de83e52ef399607cd6b17b535bf5e3a0aa29a9739f20c6bfe64d52d6"
- },
- "full_moon": {
- "category": "nature",
- "moji": "🌕",
- "description": "full moon symbol",
- "unicodeVersion": "6.0",
- "digest": "094a0a8bbf01f188d48c6bd1bbb035be622f2046d8b0c1a1252a96424e95d856"
- },
- "full_moon_with_face": {
- "category": "nature",
- "moji": "🌝",
- "description": "full moon with face",
- "unicodeVersion": "6.0",
- "digest": "aeccdf5baf685650ee34da23f14cb4d659598c44c260a8f549489cbed1d46058"
- },
- "game_die": {
- "category": "activity",
- "moji": "🎲",
- "description": "game die",
- "unicodeVersion": "6.0",
- "digest": "9a9eb1efd149aa229f39ffb93605f7c231a149892d9cedfeebc6e15eb7235656"
- },
- "gear": {
- "category": "objects",
- "moji": "⚙",
- "description": "gear",
- "unicodeVersion": "4.1",
- "digest": "c752b015d8b37be10c77820e951f9d999ac5860a7a0c453c5eceaf0eb442103b"
- },
- "gem": {
- "category": "objects",
- "moji": "💎",
- "description": "gem stone",
- "unicodeVersion": "6.0",
- "digest": "abb9f5097963624e3e1e0ef7dd973a264910c22baa549d24f396defc0fcd8ef6"
- },
- "gay_pride_flag": {
- "category": "flags",
- "moji": "🏳️‍🌈",
- "description": "gay_pride_flag",
- "unicodeVersion": "7.0",
- "digest": "f23d3e6d08cda225b5778b7f2ffe7ef6ab32709f43bd1e7ccb453ee660f64fa1"
- },
- "gemini": {
- "category": "symbols",
- "moji": "♊",
- "description": "gemini",
- "unicodeVersion": "1.1",
- "digest": "3ddb938fe1196593b21a3380b20107ea22253c6fcf5b9fccdca6badf7384047b"
- },
- "ghost": {
- "category": "people",
- "moji": "👻",
- "description": "ghost",
- "unicodeVersion": "6.0",
- "digest": "02e92350f546b637c7070d01b71d84062b6cdc72cde79fce39b41c5945c5ff6c"
- },
- "gift": {
- "category": "objects",
- "moji": "🎁",
- "description": "wrapped present",
- "unicodeVersion": "6.0",
- "digest": "f05374b0464ab426379251722208599f95b4dc00f8ca551b5fbd6cea5ee98ee6"
- },
- "gift_heart": {
- "category": "symbols",
- "moji": "💝",
- "description": "heart with ribbon",
- "unicodeVersion": "6.0",
- "digest": "9d747c69520b804e5cf3810475ff764a1d88f9f4e90f2fb3030a85ea35cd63a2"
- },
- "girl": {
- "category": "people",
- "moji": "👧",
- "description": "girl",
- "unicodeVersion": "6.0",
- "digest": "5b6e2d936ee671b82328adbb0d77a8a23ad169aa455f8f3e07a974620d62b698"
- },
- "girl_tone1": {
- "category": "people",
- "moji": "👧🏻",
- "description": "girl tone 1",
- "unicodeVersion": "8.0",
- "digest": "34f82173f73d9e3048ffd339c480ae72017be5fb99616f40cb90ca507ce81365"
- },
- "girl_tone2": {
- "category": "people",
- "moji": "👧🏼",
- "description": "girl tone 2",
- "unicodeVersion": "8.0",
- "digest": "e1efcff7f827dbea4258ec5cb0b40555266119e1f2e4526c251ea166ec252eb7"
- },
- "girl_tone3": {
- "category": "people",
- "moji": "👧🏽",
- "description": "girl tone 3",
- "unicodeVersion": "8.0",
- "digest": "2589873f9c4792091359126462bfb83d3f14bf1bcf977b834e33d11969c551d5"
- },
- "girl_tone4": {
- "category": "people",
- "moji": "👧🏾",
- "description": "girl tone 4",
- "unicodeVersion": "8.0",
- "digest": "af208f0ab6e4e26b80d6bb02994ef6f38d9395414e23978973d651c4bc895367"
- },
- "girl_tone5": {
- "category": "people",
- "moji": "👧🏿",
- "description": "girl tone 5",
- "unicodeVersion": "8.0",
- "digest": "253012160b7a9a57708377062b43396ef620b9b63784736e1e6180f320715dfb"
- },
- "globe_with_meridians": {
- "category": "symbols",
- "moji": "🌐",
- "description": "globe with meridians",
- "unicodeVersion": "6.0",
- "digest": "e85db2d778b99bf7557d48fed2ff9ed1db94a615124b47d85ed5a3eeb5c3b554"
- },
- "goal": {
- "category": "activity",
- "moji": "🥅",
- "description": "goal net",
- "unicodeVersion": "9.0",
- "digest": "e867e3f1d97159e2301c2903bbfab130921490cbf4d5fdc4d5d0a62088dbfda8"
- },
- "goat": {
- "category": "nature",
- "moji": "🐐",
- "description": "goat",
- "unicodeVersion": "6.0",
- "digest": "83e2498db2f088b8bfaf7614a543bc691da93eec2d8a156cfdde67e86ccbade8"
- },
- "golf": {
- "category": "activity",
- "moji": "⛳",
- "description": "flag in hole",
- "unicodeVersion": "5.2",
- "digest": "90dbc9461dfb3f37bcf171eedaf1131d4567ceace94a86f21f6e6befd6473a98"
- },
- "golfer": {
- "category": "activity",
- "moji": "🏌",
- "description": "golfer",
- "unicodeVersion": "7.0",
- "digest": "969e342b2749a357f110ebd60fc3f8e49899e0a0777304f8bb4d03416f076d75"
- },
- "gorilla": {
- "category": "nature",
- "moji": "🦍",
- "description": "gorilla",
- "unicodeVersion": "9.0",
- "digest": "bfd0ef21e14e0e11f426dca6ed062fbc360038df043855dde873bed4cef247bd"
- },
- "grapes": {
- "category": "food",
- "moji": "🍇",
- "description": "grapes",
- "unicodeVersion": "6.0",
- "digest": "6df4590ee90aa5dd376597fa8e500ad99b4dacae5ceaad8d0b5c2a4662e09e74"
- },
- "green_apple": {
- "category": "food",
- "moji": "🍏",
- "description": "green apple",
- "unicodeVersion": "6.0",
- "digest": "7f07991633eb29615cd7da645921adfdf01dfd83186d30f29c9ca02b6ed4384b"
- },
- "green_book": {
- "category": "objects",
- "moji": "📗",
- "description": "green book",
- "unicodeVersion": "6.0",
- "digest": "6a638f78d0ccb18907a2606afb45e9d32852fbaeee2e41250432e102b8366984"
- },
- "green_heart": {
- "category": "symbols",
- "moji": "💚",
- "description": "green heart",
- "unicodeVersion": "6.0",
- "digest": "426e89957ea1b6631948c5607f1806695af2a339a27159bf93c4c42a6110595c"
- },
- "grey_exclamation": {
- "category": "symbols",
- "moji": "❕",
- "description": "white exclamation mark ornament",
- "unicodeVersion": "6.0",
- "digest": "75bdc292ebb381f9c5bd236e8298665f3c536d049ea8df735b677f5997dc9df5"
- },
- "grey_question": {
- "category": "symbols",
- "moji": "❔",
- "description": "white question mark ornament",
- "unicodeVersion": "6.0",
- "digest": "54b5c454718c213942187fe846ee10466d1144050456559e3782799ed631f72f"
- },
- "grimacing": {
- "category": "people",
- "moji": "😬",
- "description": "grimacing face",
- "unicodeVersion": "6.1",
- "digest": "781a6548b4b6e394bfd08d57ad222def3eb28d1f1743c9f305296b6108a93958"
- },
- "grin": {
- "category": "people",
- "moji": "😁",
- "description": "grinning face with smiling eyes",
- "unicodeVersion": "6.0",
- "digest": "15b73c02a8456b4b41164090c27409606a60440bbe1a1932ee58702ecacabcbe"
- },
- "grinning": {
- "category": "people",
- "moji": "😀",
- "description": "grinning face",
- "unicodeVersion": "6.1",
- "digest": "c83774596b63aed388259582de228aab02f912bc79200e6368a7926df2a1fad8"
- },
- "guardsman": {
- "category": "people",
- "moji": "💂",
- "description": "guardsman",
- "unicodeVersion": "6.0",
- "digest": "b8fe4960df2f06ada3c1d6d8a7c53bb7c01b976a7f43afce50c7dadc4840771b"
- },
- "guardsman_tone1": {
- "category": "people",
- "moji": "💂🏻",
- "description": "guardsman tone 1",
- "unicodeVersion": "8.0",
- "digest": "0e7fbf3faddaac2246c318fac9a8f40908ad44c98bd6543091c601abe62a5c96"
- },
- "guardsman_tone2": {
- "category": "people",
- "moji": "💂🏼",
- "description": "guardsman tone 2",
- "unicodeVersion": "8.0",
- "digest": "56a81767c023dd6ac42b586fc50804b34b4da8fcbbffe9a6d6e5dc12e89868f8"
- },
- "guardsman_tone3": {
- "category": "people",
- "moji": "💂🏽",
- "description": "guardsman tone 3",
- "unicodeVersion": "8.0",
- "digest": "1c15ae587f90408781f5560366febafbee7e5318d1f618135408827ef82b09ee"
- },
- "guardsman_tone4": {
- "category": "people",
- "moji": "💂🏾",
- "description": "guardsman tone 4",
- "unicodeVersion": "8.0",
- "digest": "4fd65aa93d212fd58e5c41dc70816d691635fa6fb271bf55d72c33f3acc72c4a"
- },
- "guardsman_tone5": {
- "category": "people",
- "moji": "💂🏿",
- "description": "guardsman tone 5",
- "unicodeVersion": "8.0",
- "digest": "dcf7b0462d5b5b0ae09673c8ea3b7e48648edc28541680dc0adbaef1fb7e738d"
- },
- "guitar": {
- "category": "activity",
- "moji": "🎸",
- "description": "guitar",
- "unicodeVersion": "6.0",
- "digest": "c51339829b282ea6007d42be71949ff646bb978d5e8989d15b21857aeb2b5f12"
- },
- "gun": {
- "category": "objects",
- "moji": "🔫",
- "description": "pistol",
- "unicodeVersion": "6.0",
- "digest": "586379358f302248a7be52d185997a0904e31635d32a0fac86366d98ac1ab242"
- },
- "haircut": {
- "category": "people",
- "moji": "💇",
- "description": "haircut",
- "unicodeVersion": "6.0",
- "digest": "d6da39a1156f21ca140f28c5477fbbc37c2a9903bb2eb183e9288e16eedf134e"
- },
- "haircut_tone1": {
- "category": "people",
- "moji": "💇🏻",
- "description": "haircut tone 1",
- "unicodeVersion": "8.0",
- "digest": "186dfd3d0512fc316dd48a75b4201cb9006bf1f01ad17afe41c1e9fc4f1b440a"
- },
- "haircut_tone2": {
- "category": "people",
- "moji": "💇🏼",
- "description": "haircut tone 2",
- "unicodeVersion": "8.0",
- "digest": "4e85e6281419b2bc948d529d883bdcea6c115a4195299c1fc3c0bb839ac95b26"
- },
- "haircut_tone3": {
- "category": "people",
- "moji": "💇🏽",
- "description": "haircut tone 3",
- "unicodeVersion": "8.0",
- "digest": "08d8b031cb43180346a7be2e6e010b31abfdc091471c357ff1d428bd8bd66bba"
- },
- "haircut_tone4": {
- "category": "people",
- "moji": "💇🏾",
- "description": "haircut tone 4",
- "unicodeVersion": "8.0",
- "digest": "b3d2c66952b6d141ab000c6c99a4b8a978f135e2b880aed83517fb5cbced7f14"
- },
- "haircut_tone5": {
- "category": "people",
- "moji": "💇🏿",
- "description": "haircut tone 5",
- "unicodeVersion": "8.0",
- "digest": "eab0facef35c9b3e920c79adce756116b68fe8042bc77f9505d93cdd75a58e79"
- },
- "hamburger": {
- "category": "food",
- "moji": "🍔",
- "description": "hamburger",
- "unicodeVersion": "6.0",
- "digest": "5d302067792941f85238d73dabc26e5565891ffd23514c93a26e5ac759d86198"
- },
- "hammer": {
- "category": "objects",
- "moji": "🔨",
- "description": "hammer",
- "unicodeVersion": "6.0",
- "digest": "ad9c4c0d7613bf9022426fbd46fe1966c98a46222f81fc13402a93d5101ba3ca"
- },
- "hammer_pick": {
- "category": "objects",
- "moji": "⚒",
- "description": "hammer and pick",
- "unicodeVersion": "4.1",
- "digest": "e5fd4464456d11cfe9fb50c460cf784174a36013a81771d029c8cfbb73c5ce9a"
- },
- "hamster": {
- "category": "nature",
- "moji": "🐹",
- "description": "hamster face",
- "unicodeVersion": "6.0",
- "digest": "aa2bb7c3ca254800ae7bb02b49ae4206984d4a07350be9fe8e0d645133918863"
- },
- "hand_splayed": {
- "category": "people",
- "moji": "🖐",
- "description": "raised hand with fingers splayed",
- "unicodeVersion": "7.0",
- "digest": "cf0d977763f453074d581b2f305065ec0237ff8d242ea99641b85e1041c4aa62"
- },
- "hand_splayed_tone1": {
- "category": "people",
- "moji": "🖐🏻",
- "description": "raised hand with fingers splayed tone 1",
- "unicodeVersion": "8.0",
- "digest": "33aeacad6f84a936dbcc8787d49cb0358afbde289bc324320f00f9582512762e"
- },
- "hand_splayed_tone2": {
- "category": "people",
- "moji": "🖐🏼",
- "description": "raised hand with fingers splayed tone 2",
- "unicodeVersion": "8.0",
- "digest": "e78d9a420e4b28549c90869764b09edb030884049e49202515cf31d3f95e04e3"
- },
- "hand_splayed_tone3": {
- "category": "people",
- "moji": "🖐🏽",
- "description": "raised hand with fingers splayed tone 3",
- "unicodeVersion": "8.0",
- "digest": "252148eb798f804b341c862ca04b64bc499eef820e95d481abf85d88d2cadca7"
- },
- "hand_splayed_tone4": {
- "category": "people",
- "moji": "🖐🏾",
- "description": "raised hand with fingers splayed tone 4",
- "unicodeVersion": "8.0",
- "digest": "551ed9bcf75b5fd83b8d4d5e4d01700b494fd8787a40ab84db4783e670fa404c"
- },
- "hand_splayed_tone5": {
- "category": "people",
- "moji": "🖐🏿",
- "description": "raised hand with fingers splayed tone 5",
- "unicodeVersion": "8.0",
- "digest": "a1b05e2a3dbd79b673e058f22533142c2e4bf6c542054ffbf3f61dc48f3aa274"
- },
- "handbag": {
- "category": "people",
- "moji": "👜",
- "description": "handbag",
- "unicodeVersion": "6.0",
- "digest": "0f1d0849440c523c026b3dea64850f48c3fccbc01fc08878927a25900f393a76"
- },
- "handball": {
- "category": "activity",
- "moji": "🤾",
- "description": "handball",
- "unicodeVersion": "9.0",
- "digest": "66eb58f2b6fed026008dc73d99259f3023145b3955a52e47033f09055502992e"
- },
- "handball_tone1": {
- "category": "activity",
- "moji": "🤾🏻",
- "description": "handball tone 1",
- "unicodeVersion": "9.0",
- "digest": "52b5a29715638415371c6546795930fa3b589e0723f59af9969a0de6822cfe04"
- },
- "handball_tone2": {
- "category": "activity",
- "moji": "🤾🏼",
- "description": "handball tone 2",
- "unicodeVersion": "9.0",
- "digest": "8454813e2e2aabd6eb897bb973129c41d3628e0418e611c4968e8aee477b077d"
- },
- "handball_tone3": {
- "category": "activity",
- "moji": "🤾🏽",
- "description": "handball tone 3",
- "unicodeVersion": "9.0",
- "digest": "b7f821482e3bcb63e36ba9b5d3838f1444c35095a9d68648cec24438cc860f4f"
- },
- "handball_tone4": {
- "category": "activity",
- "moji": "🤾🏾",
- "description": "handball tone 4",
- "unicodeVersion": "9.0",
- "digest": "604e7948a99d23d469e3914040eea00e40faeab8e0f92d837fc1aebb7953104b"
- },
- "handball_tone5": {
- "category": "activity",
- "moji": "🤾🏿",
- "description": "handball tone 5",
- "unicodeVersion": "9.0",
- "digest": "d989a7bd045a77163055d7973ee4ccbd3b04029a06df25cecd50a76466ca1526"
- },
- "handshake": {
- "category": "people",
- "moji": "🤝",
- "description": "handshake",
- "unicodeVersion": "9.0",
- "digest": "59e0dfc9059d643a6e88a2c498ac62175876f78394c68605e984d25597f4e177"
- },
- "handshake_tone1": {
- "category": "people",
- "moji": "🤝🏻",
- "description": "handshake tone 1",
- "unicodeVersion": "9.0",
- "digest": "d16474fe7d1eb0e3b6441111370051caf5b20847d4080a89c4474a6af8c9f711"
- },
- "handshake_tone2": {
- "category": "people",
- "moji": "🤝🏼",
- "description": "handshake tone 2",
- "unicodeVersion": "9.0",
- "digest": "f101fcd171fedad33bb5a90bc51cdbd5041902adef5e94f4893eafdac76512a0"
- },
- "handshake_tone3": {
- "category": "people",
- "moji": "🤝🏽",
- "description": "handshake tone 3",
- "unicodeVersion": "9.0",
- "digest": "9a519adf10d9bd3ef8f0c94beeeb64de8922d85b428e74663f9a9eec3be15a71"
- },
- "handshake_tone4": {
- "category": "people",
- "moji": "🤝🏾",
- "description": "handshake tone 4",
- "unicodeVersion": "9.0",
- "digest": "e0aff05eaed64f9c03c91a821982d425bd4f237b0fb09fffb23476912b100ffa"
- },
- "handshake_tone5": {
- "category": "people",
- "moji": "🤝🏿",
- "description": "handshake tone 5",
- "unicodeVersion": "9.0",
- "digest": "76ec70c0b6bfca7dfc3d2b202f36c6cd59c030e6414c49f3f1e3f68cee77be9b"
- },
- "hash": {
- "category": "symbols",
- "moji": "#⃣",
- "description": "number sign",
- "unicodeVersion": "3.0",
- "digest": "941108d47089055455e13b981cd37b0437cd72d5c19ad397254494184060366e"
- },
- "hatched_chick": {
- "category": "nature",
- "moji": "🐥",
- "description": "front-facing baby chick",
- "unicodeVersion": "6.0",
- "digest": "e87f9edbc75c2f65940e59ec3ee752bf15cca10767e83766d3bb2e2cacbb30d1"
- },
- "hatching_chick": {
- "category": "nature",
- "moji": "🐣",
- "description": "hatching chick",
- "unicodeVersion": "6.0",
- "digest": "839de0c6a31a0c0ff1e4ee36f64fe462667d15acda79747dd0a362f59a37f144"
- },
- "head_bandage": {
- "category": "people",
- "moji": "🤕",
- "description": "face with head-bandage",
- "unicodeVersion": "8.0",
- "digest": "53fef09c38e83bc82c90bd3d63e01767767d2f25cec1cd5a6742e3be8cb0ef12"
- },
- "headphones": {
- "category": "activity",
- "moji": "🎧",
- "description": "headphone",
- "unicodeVersion": "6.0",
- "digest": "ca903e1013a0be982e3281dd357b9acf3cccd41db7021385a6d4d84ce1f31c04"
- },
- "hear_no_evil": {
- "category": "nature",
- "moji": "🙉",
- "description": "hear-no-evil monkey",
- "unicodeVersion": "6.0",
- "digest": "6358afb4d187b86c325b5b0113b46d0ab080968fbd8c477d54f733c169b0242e"
- },
- "heart": {
- "category": "symbols",
- "moji": "❤",
- "description": "heavy black heart",
- "unicodeVersion": "1.1",
- "digest": "f4f1ba1aa7118b2ccb2693eeb523fd1ef44766e88bbda830fe2154a7791bb677"
- },
- "heart_decoration": {
- "category": "symbols",
- "moji": "💟",
- "description": "heart decoration",
- "unicodeVersion": "6.0",
- "digest": "f68f4ff3043101c8bb2ce52257ee918a016128a962ba3d8e0bb4a640076743f9"
- },
- "heart_exclamation": {
- "category": "symbols",
- "moji": "❣",
- "description": "heavy heart exclamation mark ornament",
- "unicodeVersion": "1.1",
- "digest": "35c8dd5c38c09f8bcee07145e68f299f662866c7d05294aff6ab2a9b11bb83de"
- },
- "heart_eyes": {
- "category": "people",
- "moji": "😍",
- "description": "smiling face with heart-shaped eyes",
- "unicodeVersion": "6.0",
- "digest": "997c08afa77ab1bd0b08ae58854024286669ce94385c2a5c7bcab02d219a3667"
- },
- "heart_eyes_cat": {
- "category": "people",
- "moji": "😻",
- "description": "smiling cat face with heart-shaped eyes",
- "unicodeVersion": "6.0",
- "digest": "9ea3bd6876a5833b702730a8b21ba0a20b3f95c64131e46414402ad485719069"
- },
- "heartbeat": {
- "category": "symbols",
- "moji": "💓",
- "description": "beating heart",
- "unicodeVersion": "6.0",
- "digest": "1b8ecc1830cb706a354bd340c6c127bf045102a5fe7e1f472daecf13a772628f"
- },
- "heartpulse": {
- "category": "symbols",
- "moji": "💗",
- "description": "growing heart",
- "unicodeVersion": "6.0",
- "digest": "bb45713a2195b5f4742bc57aa4e72e26a850c8860e0ca395fcc8e2a64e13334b"
- },
- "hearts": {
- "category": "symbols",
- "moji": "♥",
- "description": "black heart suit",
- "unicodeVersion": "1.1",
- "digest": "86712f800e461be471d9d64d427f4e751eab584cae33b2c7a72634161c51e622"
- },
- "heavy_check_mark": {
- "category": "symbols",
- "moji": "✔",
- "description": "heavy check mark",
- "unicodeVersion": "1.1",
- "digest": "206dc92526366341e4eef274354104ac2d6a464dfc253c0459c6943556ab3d64"
- },
- "heavy_division_sign": {
- "category": "symbols",
- "moji": "➗",
- "description": "heavy division sign",
- "unicodeVersion": "6.0",
- "digest": "563f5723eb45d2903a37ae66b3a388b06c1576a35cbcf203a6ea951bbbcb10f3"
- },
- "heavy_dollar_sign": {
- "category": "symbols",
- "moji": "💲",
- "description": "heavy dollar sign",
- "unicodeVersion": "6.0",
- "digest": "25a39a89a62f45d7bd4948682ed23753d16179fe0c80dc60ff06bc29549fd04f"
- },
- "heavy_minus_sign": {
- "category": "symbols",
- "moji": "➖",
- "description": "heavy minus sign",
- "unicodeVersion": "6.0",
- "digest": "9f2d6d303ab3c87bfdc78a933351f1659304758a330828961bc053fac1f5cdae"
- },
- "heavy_multiplication_x": {
- "category": "symbols",
- "moji": "✖",
- "description": "heavy multiplication x",
- "unicodeVersion": "1.1",
- "digest": "879a6b81e3fba890a3261029c611bc8baf6371e012b70a648fec0c2156e690d7"
- },
- "heavy_plus_sign": {
- "category": "symbols",
- "moji": "➕",
- "description": "heavy plus sign",
- "unicodeVersion": "6.0",
- "digest": "d7e684b6d4a4f0f9fa51dddf10c844616d0bb14184ba8b015088b5a4793aa4c4"
- },
- "helicopter": {
- "category": "travel",
- "moji": "🚁",
- "description": "helicopter",
- "unicodeVersion": "6.0",
- "digest": "e5cacdf04612e3e2bb7a1c8f6968c2dd5b0b715ee2bdbfa703d5d0ddedf6be5e"
- },
- "helmet_with_cross": {
- "category": "people",
- "moji": "⛑",
- "description": "helmet with white cross",
- "unicodeVersion": "5.2",
- "digest": "12a73fcfb9d2a62305f4a59f028215c81868a4b8de862b276c3b508872ac70ad"
- },
- "herb": {
- "category": "nature",
- "moji": "🌿",
- "description": "herb",
- "unicodeVersion": "6.0",
- "digest": "26a5958f4afaa7ec0f0fbbddfe50b6a467dff109fab45345c5a63b0316d20370"
- },
- "hibiscus": {
- "category": "nature",
- "moji": "🌺",
- "description": "hibiscus",
- "unicodeVersion": "6.0",
- "digest": "2dc90fc37f140ac3a56600042ba44116f28e40bdda1d041d2218a6251099cbf6"
- },
- "high_brightness": {
- "category": "symbols",
- "moji": "🔆",
- "description": "high brightness symbol",
- "unicodeVersion": "6.0",
- "digest": "9ee294cb514a00f831f3fec6275d75685f157b6d22a7ee1417c28b4c93e1d3ba"
- },
- "high_heel": {
- "category": "people",
- "moji": "👠",
- "description": "high-heeled shoe",
- "unicodeVersion": "6.0",
- "digest": "f2aee61e8ae1cb532d9f005317476bbbe26b6e7d18cf2b2cac3d21ab39a86dd1"
- },
- "hockey": {
- "category": "activity",
- "moji": "🏒",
- "description": "ice hockey stick and puck",
- "unicodeVersion": "8.0",
- "digest": "9c5e4e47bca98e73610173eb568ab9f2abf6cbdb8357438db30244946ebd7dac"
- },
- "hole": {
- "category": "objects",
- "moji": "🕳",
- "description": "hole",
- "unicodeVersion": "7.0",
- "digest": "a486f10fd58f9e9424feb4b1409e5fccac7706952b616460156a55db836a46f0"
- },
- "homes": {
- "category": "travel",
- "moji": "🏘",
- "description": "house buildings",
- "unicodeVersion": "7.0",
- "digest": "2ffdafabb89623b096e12f9a81936e699c436d0e948af6c1dd8786690b1a601b"
- },
- "honey_pot": {
- "category": "food",
- "moji": "🍯",
- "description": "honey pot",
- "unicodeVersion": "6.0",
- "digest": "bc66ee9940f2c1af04e066e78d4a61bc261f1dca4419d028b133ea9bc42e24d4"
- },
- "horse": {
- "category": "nature",
- "moji": "🐴",
- "description": "horse face",
- "unicodeVersion": "6.0",
- "digest": "fa6681d0536051b55e189bdd7435edfd4d4967cc28c255b55baf25cb25f40865"
- },
- "horse_racing": {
- "category": "activity",
- "moji": "🏇",
- "description": "horse racing",
- "unicodeVersion": "6.0",
- "digest": "1c1a2fa09a64da5b442f334a45054b7cbe39dbf7ac1d2a26d7a1ef2bb2debdc4"
- },
- "horse_racing_tone1": {
- "category": "activity",
- "moji": "🏇🏻",
- "description": "horse racing tone 1",
- "unicodeVersion": "8.0",
- "digest": "5d55d2cc1a8efc0b754cd75d56d2643784aa41fc7a09e1e9d4ddee767f35ffd6"
- },
- "horse_racing_tone2": {
- "category": "activity",
- "moji": "🏇🏼",
- "description": "horse racing tone 2",
- "unicodeVersion": "8.0",
- "digest": "f8b5b8f7247f6526ea3bca0e6a1136978449815a105f766b42ff6a2bfe24a7a3"
- },
- "horse_racing_tone3": {
- "category": "activity",
- "moji": "🏇🏽",
- "description": "horse racing tone 3",
- "unicodeVersion": "8.0",
- "digest": "13a6b8ceeba67ea1dcda173f8c4ce012ec5333460bff51646ecd3c26092b492b"
- },
- "horse_racing_tone4": {
- "category": "activity",
- "moji": "🏇🏾",
- "description": "horse racing tone 4",
- "unicodeVersion": "8.0",
- "digest": "042cb542ea8008f51f7750923a8f88090bc91c9301234b6eb39284735930f5ae"
- },
- "horse_racing_tone5": {
- "category": "activity",
- "moji": "🏇🏿",
- "description": "horse racing tone 5",
- "unicodeVersion": "8.0",
- "digest": "07a0a17002c5f623540377cf24a86d364bcb42b57b759e41d07af17ae36c1206"
- },
- "hospital": {
- "category": "travel",
- "moji": "🏥",
- "description": "hospital",
- "unicodeVersion": "6.0",
- "digest": "84895d822ef2e2a77b3703b80c5d5b29574f655b88420ef6f39ab3d93bb123df"
- },
- "hot_pepper": {
- "category": "food",
- "moji": "🌶",
- "description": "hot pepper",
- "unicodeVersion": "7.0",
- "digest": "78301b29d9426d81938771c3c25a95d70c9eecf78b39a3f49f30cb63f0cf6c50"
- },
- "hotdog": {
- "category": "food",
- "moji": "🌭",
- "description": "hot dog",
- "unicodeVersion": "8.0",
- "digest": "09bc0ee460411220b1e853893eb161106d173909d7754b35c36d16e99b54a30f"
- },
- "hotel": {
- "category": "travel",
- "moji": "🏨",
- "description": "hotel",
- "unicodeVersion": "6.0",
- "digest": "7f04757da4f1a537cb92ac9a0537e3a7ebd90ae9e2c3e73ad79c95623b3126ef"
- },
- "hotsprings": {
- "category": "symbols",
- "moji": "♨",
- "description": "hot springs",
- "unicodeVersion": "1.1",
- "digest": "755da4733bacad870d226b5ec054c803368c4ab10ae577e193a3015ee25a8271"
- },
- "hourglass": {
- "category": "objects",
- "moji": "⌛",
- "description": "hourglass",
- "unicodeVersion": "1.1",
- "digest": "88925df394060ad7e8285bc7cc6cdb3c54453316b75658c780bb956ca812d2fc"
- },
- "hourglass_flowing_sand": {
- "category": "objects",
- "moji": "⏳",
- "description": "hourglass with flowing sand",
- "unicodeVersion": "6.0",
- "digest": "899fa78157223e27acf260c2cd902646b7271087aabd4724a2d05095484ac724"
- },
- "house": {
- "category": "travel",
- "moji": "🏠",
- "description": "house building",
- "unicodeVersion": "6.0",
- "digest": "6be52f020a7e12a3298044d270d9322742e930cc7543e2d6dfb9c26f25b529ec"
- },
- "house_abandoned": {
- "category": "travel",
- "moji": "🏚",
- "description": "derelict house building",
- "unicodeVersion": "7.0",
- "digest": "0931ce0e42ca7206910a62452e1b7975aed4692d362c81bf62cafef25f2bcefa"
- },
- "house_with_garden": {
- "category": "travel",
- "moji": "🏡",
- "description": "house with garden",
- "unicodeVersion": "6.0",
- "digest": "b62e44e69e1a04f88280518fd48c4870035f0abb5ae6a6664daa7cf005c8f8de"
- },
- "hugging": {
- "category": "people",
- "moji": "🤗",
- "description": "hugging face",
- "unicodeVersion": "8.0",
- "digest": "c52e3522e798301a973ab2e8829ba85d50b428ac853dbe096dd09a81d2fc5b29"
- },
- "hushed": {
- "category": "people",
- "moji": "😯",
- "description": "hushed face",
- "unicodeVersion": "6.1",
- "digest": "a1b0d468e68dff4b3ab40b5980c838635e9680cfb35592ee62260b597cc8b551"
- },
- "ice_cream": {
- "category": "food",
- "moji": "🍨",
- "description": "ice cream",
- "unicodeVersion": "6.0",
- "digest": "3294b57aa673511686ad1c6a01f6e4c0ecb0eb7d5472c3015c3271b4f6e16d50"
- },
- "ice_skate": {
- "category": "activity",
- "moji": "⛸",
- "description": "ice skate",
- "unicodeVersion": "5.2",
- "digest": "e06869a74874d409372ef2714ca8f3e3050ad8a81e589709ecb541d5c77603df"
- },
- "icecream": {
- "category": "food",
- "moji": "🍦",
- "description": "soft ice cream",
- "unicodeVersion": "6.0",
- "digest": "434307d583d20429b1646cbe1bd8317a53f445cae61e8d8ff1258d3996e6b9fe"
- },
- "id": {
- "category": "symbols",
- "moji": "🆔",
- "description": "squared id",
- "unicodeVersion": "6.0",
- "digest": "63c1717c82c487db9a3e2a31be498e3cc88349cb0f2a5010b77be4bd51ba012b"
- },
- "ideograph_advantage": {
- "category": "symbols",
- "moji": "🉐",
- "description": "circled ideograph advantage",
- "unicodeVersion": "6.0",
- "digest": "b07fab8069772a132928e38b64d8e35661b33757176572c153f4563460dd5ddc"
- },
- "imp": {
- "category": "people",
- "moji": "👿",
- "description": "imp",
- "unicodeVersion": "6.0",
- "digest": "bf3ec6b5b728a98f16b630cea877fe3cef79e5ebe7e58ee0caec197279ed3a80"
- },
- "inbox_tray": {
- "category": "objects",
- "moji": "📥",
- "description": "inbox tray",
- "unicodeVersion": "6.0",
- "digest": "fa79ca5efc93767858677871df687d0666cc9174e7f226d7acb65c15a4cad0df"
- },
- "incoming_envelope": {
- "category": "objects",
- "moji": "📨",
- "description": "incoming envelope",
- "unicodeVersion": "6.0",
- "digest": "c23f819ce0abd3b695b05051f8c9e9be72f9bf5176207106c56e30015c199252"
- },
- "information_desk_person": {
- "category": "people",
- "moji": "💁",
- "description": "information desk person",
- "unicodeVersion": "6.0",
- "digest": "ee9a1e94470a107b494840ac5b9bd0d59c38fcbc5d6a9f0842c910688ea9ec74"
- },
- "information_desk_person_tone1": {
- "category": "people",
- "moji": "💁🏻",
- "description": "information desk person tone 1",
- "unicodeVersion": "8.0",
- "digest": "bb91f3dedc55cca320cd14343c04c388b4dfa870a9d5860957a0180f24161fea"
- },
- "information_desk_person_tone2": {
- "category": "people",
- "moji": "💁🏼",
- "description": "information desk person tone 2",
- "unicodeVersion": "8.0",
- "digest": "a556b596a0d9cd480adaff344d3ba5c02a16ef2eb73695e9d9fc231ab8e3599f"
- },
- "information_desk_person_tone3": {
- "category": "people",
- "moji": "💁🏽",
- "description": "information desk person tone 3",
- "unicodeVersion": "8.0",
- "digest": "b5a7ca77e15e749731fcd63da9c274c47eade033da8cfc8a6ab5413d939a8a8e"
- },
- "information_desk_person_tone4": {
- "category": "people",
- "moji": "💁🏾",
- "description": "information desk person tone 4",
- "unicodeVersion": "8.0",
- "digest": "a7912c3121b1bb31b55c528812f54b89cdbc1982045e226c3cbce29c83970a59"
- },
- "information_desk_person_tone5": {
- "category": "people",
- "moji": "💁🏿",
- "description": "information desk person tone 5",
- "unicodeVersion": "8.0",
- "digest": "a3e3fb0ae133513f565e5331a699a6d147200a5141a4e692fec86cfca03b9f5f"
- },
- "information_source": {
- "category": "symbols",
- "moji": "ℹ",
- "description": "information source",
- "unicodeVersion": "3.0",
- "digest": "9deada49f605373b64d9eefa432f73cf07287ebe254233823109d133dc86dc23"
- },
- "innocent": {
- "category": "people",
- "moji": "😇",
- "description": "smiling face with halo",
- "unicodeVersion": "6.0",
- "digest": "3571bdd00112793ecf4ade131f2e50b6234b6f794fcb799e6dadacbb4106a92d"
- },
- "interrobang": {
- "category": "symbols",
- "moji": "⁉",
- "description": "exclamation question mark",
- "unicodeVersion": "3.0",
- "digest": "d209c9aa46c89e290a053d458f6cda0c719831518ce91265680673065cfb77d5"
- },
- "iphone": {
- "category": "objects",
- "moji": "📱",
- "description": "mobile phone",
- "unicodeVersion": "6.0",
- "digest": "6ef1372438c96383e375ab88a965db818efe0e3ae4b87f49870487c72f62ab9f"
- },
- "island": {
- "category": "travel",
- "moji": "🏝",
- "description": "desert island",
- "unicodeVersion": "7.0",
- "digest": "9cd601f946f247ad80810f256b5c4f0c71658e9e52b974c3d8d36173eedcd05e"
- },
- "izakaya_lantern": {
- "category": "objects",
- "moji": "🏮",
- "description": "izakaya lantern",
- "unicodeVersion": "6.0",
- "digest": "de36e5f5fe5da0c922e194b1ae93ed07039620b83e518a259c374563a166f4bf"
- },
- "jack_o_lantern": {
- "category": "nature",
- "moji": "🎃",
- "description": "jack-o-lantern",
- "unicodeVersion": "6.0",
- "digest": "876808e4ffa5ef7d736b88d9b1d646ea1af3bdf91f494190740502491d82edbd"
- },
- "japan": {
- "category": "travel",
- "moji": "🗾",
- "description": "silhouette of japan",
- "unicodeVersion": "6.0",
- "digest": "a27fd17e252497aa0220540d778427dc5fba138b6adeac10b132feae8175d554"
- },
- "japanese_castle": {
- "category": "travel",
- "moji": "🏯",
- "description": "japanese castle",
- "unicodeVersion": "6.0",
- "digest": "4beaf1e9f6d7e25d3faa5076896f337fce68e75d25acdd91ceaceb9c15faa5eb"
- },
- "japanese_goblin": {
- "category": "people",
- "moji": "👺",
- "description": "japanese goblin",
- "unicodeVersion": "6.0",
- "digest": "4c5b8cfc3b172269a943341583e938cd1c8030e9de9fc9008ec0cfbca53d2f81"
- },
- "japanese_ogre": {
- "category": "people",
- "moji": "👹",
- "description": "japanese ogre",
- "unicodeVersion": "6.0",
- "digest": "3ecbc95d1e43ebda0a0c9988e8dc012dc4985cdaa2b8e81a42cbf8cff30a93e3"
- },
- "jeans": {
- "category": "people",
- "moji": "👖",
- "description": "jeans",
- "unicodeVersion": "6.0",
- "digest": "e470f82274f2b149f98d4620e61c374c3737e722bd0a08576335cc269e86f83f"
- },
- "joy": {
- "category": "people",
- "moji": "😂",
- "description": "face with tears of joy",
- "unicodeVersion": "6.0",
- "digest": "3c7d20273bbe976dc8cf8d5cf44ac4cb9c71b02ec358b50427e9d0662e67a557"
- },
- "joy_cat": {
- "category": "people",
- "moji": "😹",
- "description": "cat face with tears of joy",
- "unicodeVersion": "6.0",
- "digest": "fd65d87249121b7e1b1b48af53179ff8ccc7d5f072fcb07e498dc20e9370c436"
- },
- "joystick": {
- "category": "objects",
- "moji": "🕹",
- "description": "joystick",
- "unicodeVersion": "7.0",
- "digest": "def1450af8fc7e3e4d968a6f6a2f5644b17f2786941d49c688c91dc4fde36836"
- },
- "juggling": {
- "category": "activity",
- "moji": "🤹",
- "description": "juggling",
- "unicodeVersion": "9.0",
- "digest": "dbc4b794cb55d03b091b86d1b1b0682535c4a2f9a4d6d6b4ad5c413f1279a4f6"
- },
- "juggling_tone1": {
- "category": "activity",
- "moji": "🤹🏻",
- "description": "juggling tone 1",
- "unicodeVersion": "9.0",
- "digest": "124e52052704f34a91e0cf8ef9ec7d08176942b8bee6faf2e4214df827443ae2"
- },
- "juggling_tone2": {
- "category": "activity",
- "moji": "🤹🏼",
- "description": "juggling tone 2",
- "unicodeVersion": "9.0",
- "digest": "1d8609d3e765fbbed6659e5fbd5026911a276b5703b1e7593b12702de2d0555e"
- },
- "juggling_tone3": {
- "category": "activity",
- "moji": "🤹🏽",
- "description": "juggling tone 3",
- "unicodeVersion": "9.0",
- "digest": "1666f54f93b744f8ee5cf8cc29be022ea5d3afd15436e642c5592d583c87830b"
- },
- "juggling_tone4": {
- "category": "activity",
- "moji": "🤹🏾",
- "description": "juggling tone 4",
- "unicodeVersion": "9.0",
- "digest": "efe82bf4c3759b48435a7575ff0b016035d505dbdb2c7a3774d3878eed671602"
- },
- "juggling_tone5": {
- "category": "activity",
- "moji": "🤹🏿",
- "description": "juggling tone 5",
- "unicodeVersion": "9.0",
- "digest": "ebf39b7b85e8c5e2fc899bb98de9f35739b7e656efd0526d9a50e8039e7bee86"
- },
- "kaaba": {
- "category": "travel",
- "moji": "🕋",
- "description": "kaaba",
- "unicodeVersion": "8.0",
- "digest": "363ef4a89268542427e494f0dbe04f04eabad2849fa24be57a77eb55a3d45122"
- },
- "key": {
- "category": "objects",
- "moji": "🔑",
- "description": "key",
- "unicodeVersion": "6.0",
- "digest": "8f2ac6bfd01430b2350a91b747c58d9d7a20e58096e22f1b73ea8e1d53dd2ac7"
- },
- "key2": {
- "category": "objects",
- "moji": "🗝",
- "description": "old key",
- "unicodeVersion": "7.0",
- "digest": "f1e9a01ce355b9be051eca6f2211d542f190a77ccb95e1d5b148dca8da422bce"
- },
- "keyboard": {
- "category": "objects",
- "moji": "⌨",
- "description": "keyboard",
- "unicodeVersion": "1.1",
- "digest": "0f3ca37b19de485983e39b02db94b0f6243d94081918703510e020ef1d269810"
- },
- "kimono": {
- "category": "people",
- "moji": "👘",
- "description": "kimono",
- "unicodeVersion": "6.0",
- "digest": "987ec803ad9f64bd7ab8c41b487df6afd3416ef25fba11df698c2dddc779e59e"
- },
- "kiss": {
- "category": "people",
- "moji": "💋",
- "description": "kiss mark",
- "unicodeVersion": "6.0",
- "digest": "751426045d0e8e59b148b2f2dd275373d361ac8e90d9e6ee65f356a8a9c5e24c"
- },
- "kiss_mm": {
- "category": "people",
- "moji": "👨‍❤️‍💋‍👨",
- "description": "kiss (man,man)",
- "unicodeVersion": "6.0",
- "digest": "a256b66869e47ee51a6b3b08c56e9ecfe9f0ed5279aeb15e0045be30891ac70f"
- },
- "kiss_ww": {
- "category": "people",
- "moji": "👩‍❤️‍💋‍👩",
- "description": "kiss (woman,woman)",
- "unicodeVersion": "6.0",
- "digest": "989937e58c7862cd6cedc74b8c7774fb01efaa7ab3b424f4472b638d101abdd9"
- },
- "kissing": {
- "category": "people",
- "moji": "😗",
- "description": "kissing face",
- "unicodeVersion": "6.1",
- "digest": "9339112fdb5a89aca2b8baed88215ba09c698487713ea6c3a4906d6370ba8a8e"
- },
- "kissing_cat": {
- "category": "people",
- "moji": "😽",
- "description": "kissing cat face with closed eyes",
- "unicodeVersion": "6.0",
- "digest": "e4c818629b8482ec9f3747125dbc4ea0c08ca12c151eb29f103e19d66ba39e78"
- },
- "kissing_closed_eyes": {
- "category": "people",
- "moji": "😚",
- "description": "kissing face with closed eyes",
- "unicodeVersion": "6.0",
- "digest": "0a58401451a4c7daad884fbcc0343f6d08efc5ce4f97cb9c455019ed57b7a979"
- },
- "kissing_heart": {
- "category": "people",
- "moji": "😘",
- "description": "face throwing a kiss",
- "unicodeVersion": "6.0",
- "digest": "6dd07e9fa9892aec92ba42b78fe23646d31701fb1d29f968ce5cf7f3c5f1336e"
- },
- "kissing_smiling_eyes": {
- "category": "people",
- "moji": "😙",
- "description": "kissing face with smiling eyes",
- "unicodeVersion": "6.1",
- "digest": "75b7829612e5e0a3c96c33cb3add78892ef8fb2012b95d24bb9e45888091648e"
- },
- "kiwi": {
- "category": "food",
- "moji": "🥝",
- "description": "kiwifruit",
- "unicodeVersion": "9.0",
- "digest": "bc2ee501a2c313cee1816720975228ab34fda6b83581248012112af1ba5ce1ae"
- },
- "knife": {
- "category": "objects",
- "moji": "🔪",
- "description": "hocho",
- "unicodeVersion": "6.0",
- "digest": "bf3934138e3dd112241807efbe2aa91ec9913fad890236276d4c36c710c400b8"
- },
- "koala": {
- "category": "nature",
- "moji": "🐨",
- "description": "koala",
- "unicodeVersion": "6.0",
- "digest": "73549917efa845ecc4a5b4c347fc6f686717e95323fed97977e6677c881f801d"
- },
- "koko": {
- "category": "symbols",
- "moji": "🈁",
- "description": "squared katakana koko",
- "unicodeVersion": "6.0",
- "digest": "3d49c8510a5e6c915d88c026897987d8a1d52aca5d498ef7d86fcc76ebb838e9"
- },
- "label": {
- "category": "objects",
- "moji": "🏷",
- "description": "label",
- "unicodeVersion": "7.0",
- "digest": "ad9f3bfc709138edfb61c692d96c1ca08b0bd97203d370342e7b70f46ae23aa8"
- },
- "large_blue_circle": {
- "category": "symbols",
- "moji": "🔵",
- "description": "large blue circle",
- "unicodeVersion": "6.0",
- "digest": "7588a2b1c1baef733d6e7572580c51dcd81bb0c85c77080dfd136d3a995ef3cc"
- },
- "large_blue_diamond": {
- "category": "symbols",
- "moji": "🔷",
- "description": "large blue diamond",
- "unicodeVersion": "6.0",
- "digest": "2e69e9e80dee0192403fe95aab3223208ccf97963dff54a061b6f2e3419820ef"
- },
- "large_orange_diamond": {
- "category": "symbols",
- "moji": "🔶",
- "description": "large orange diamond",
- "unicodeVersion": "6.0",
- "digest": "30bee888a5d3dc39a11132fac8a490d8dab4237cc62f1529f5c3e6e77b8f29a4"
- },
- "last_quarter_moon": {
- "category": "nature",
- "moji": "🌗",
- "description": "last quarter moon symbol",
- "unicodeVersion": "6.0",
- "digest": "7801bf738898b75b09c7ee3f89d71fb11f41bf838f2766b8e80c1028b2873f88"
- },
- "last_quarter_moon_with_face": {
- "category": "nature",
- "moji": "🌜",
- "description": "last quarter moon with face",
- "unicodeVersion": "6.0",
- "digest": "e1e5678ea54dfafb83bb94879150bc43c495dc546be92c8780469f24b52d036e"
- },
- "laughing": {
- "category": "people",
- "moji": "😆",
- "description": "smiling face with open mouth and tightly-closed ey",
- "unicodeVersion": "6.0",
- "digest": "43f119b4cac94c33c49e35381710d74d4f81883364ade30088cd92e5130287e1"
- },
- "leaves": {
- "category": "nature",
- "moji": "🍃",
- "description": "leaf fluttering in wind",
- "unicodeVersion": "6.0",
- "digest": "814ac13b38d78820a050dd97155abfcd6c98fc56c5793044de7b7d2855e5acb9"
- },
- "ledger": {
- "category": "objects",
- "moji": "📒",
- "description": "ledger",
- "unicodeVersion": "6.0",
- "digest": "826379f5164a8c3a10ec43ae1296f994ffd40bc3eb6c0595117993eff65f8f2a"
- },
- "left_facing_fist": {
- "category": "people",
- "moji": "🤛",
- "description": "left-facing fist",
- "unicodeVersion": "9.0",
- "digest": "69390c51d4188f7f5381f7d8f4658c8ea4e52455ed648ebf59b6fb3157c5c6e0"
- },
- "left_facing_fist_tone1": {
- "category": "people",
- "moji": "🤛🏻",
- "description": "left facing fist tone 1",
- "unicodeVersion": "9.0",
- "digest": "57e9a2288243d2024cf25e4258c48e1b0bfe5c12aee218629bb03f8a5ab0cb61"
- },
- "left_facing_fist_tone2": {
- "category": "people",
- "moji": "🤛🏼",
- "description": "left facing fist tone 2",
- "unicodeVersion": "9.0",
- "digest": "061ef44811174e10f7598aadf1f4c69cbc915d029ba83d5101cc34d0cfd44431"
- },
- "left_facing_fist_tone3": {
- "category": "people",
- "moji": "🤛🏽",
- "description": "left facing fist tone 3",
- "unicodeVersion": "9.0",
- "digest": "c313fcb7d4e1505b76ce25e45b50c0c4d0842be34c472abc328915f07e6b6efe"
- },
- "left_facing_fist_tone4": {
- "category": "people",
- "moji": "🤛🏾",
- "description": "left facing fist tone 4",
- "unicodeVersion": "9.0",
- "digest": "825e7feac0a934250246dcb97cae5daafe31aef100c9614a17ba0c7669cb18ee"
- },
- "left_facing_fist_tone5": {
- "category": "people",
- "moji": "🤛🏿",
- "description": "left facing fist tone 5",
- "unicodeVersion": "9.0",
- "digest": "20bfe330f9c1ae22b0cd4d0af0a8a5d062e898fafe10c8a4153f89e582c0de54"
- },
- "left_luggage": {
- "category": "symbols",
- "moji": "🛅",
- "description": "left luggage",
- "unicodeVersion": "6.0",
- "digest": "18bade3d46e8ea3ba9d6bc52679e367d7a1721757719ca6903e68331a1fd1c47"
- },
- "left_right_arrow": {
- "category": "symbols",
- "moji": "↔",
- "description": "left right arrow",
- "unicodeVersion": "1.1",
- "digest": "292108ff7e529974269eb98b0d417f651a9d97258a8070aaec6579c934139bb0"
- },
- "leftwards_arrow_with_hook": {
- "category": "symbols",
- "moji": "↩",
- "description": "leftwards arrow with hook",
- "unicodeVersion": "1.1",
- "digest": "1f4c8e03c92083ddd647f3328fd1ff9b919c10a98d0325b877f1b7034a7387ea"
- },
- "lemon": {
- "category": "food",
- "moji": "🍋",
- "description": "lemon",
- "unicodeVersion": "6.0",
- "digest": "b6d67cb631ddc6d448f658fb151d325018ef01b791c005ed1d601f8223b7a722"
- },
- "leo": {
- "category": "symbols",
- "moji": "♌",
- "description": "leo",
- "unicodeVersion": "1.1",
- "digest": "25787c495211576604fbcdf72aa47ab7e5ad4e8a42905a995edbbec073e20c52"
- },
- "leopard": {
- "category": "nature",
- "moji": "🐆",
- "description": "leopard",
- "unicodeVersion": "6.0",
- "digest": "00204970970b9dd38bb9b1ae30a11832c992ade57bce1496a1f3c366e7d31f00"
- },
- "level_slider": {
- "category": "objects",
- "moji": "🎚",
- "description": "level slider",
- "unicodeVersion": "7.0",
- "digest": "d57965d6b267a20a4529adbc71dd4efcb7f5e8f1146c03746b65922cd3f271c6"
- },
- "levitate": {
- "category": "activity",
- "moji": "🕴",
- "description": "man in business suit levitating",
- "unicodeVersion": "7.0",
- "digest": "103fabb2260fef61982731785dc3d09884cbfcb6bd4ef93f2d5db8403ae4c40a"
- },
- "libra": {
- "category": "symbols",
- "moji": "♎",
- "description": "libra",
- "unicodeVersion": "1.1",
- "digest": "0189ff934df698ed87a6bac3b29c559567c958a9fbd0259378af8c910e0337e1"
- },
- "lifter": {
- "category": "activity",
- "moji": "🏋",
- "description": "weight lifter",
- "unicodeVersion": "7.0",
- "digest": "d8a22a5258a05e8c31453955103531b59c6018e3e3dfd1f507d6f21ee28b3f00"
- },
- "lifter_tone1": {
- "category": "activity",
- "moji": "🏋🏻",
- "description": "weight lifter tone 1",
- "unicodeVersion": "8.0",
- "digest": "f13372d0812c433eafca5d943a6be6f8f6481bde4e681aa2ee5942bf3a74224f"
- },
- "lifter_tone2": {
- "category": "activity",
- "moji": "🏋🏼",
- "description": "weight lifter tone 2",
- "unicodeVersion": "8.0",
- "digest": "6a58152d31b5d8992c231e76823beb81dc9d440944d0ebc6bbc7630f9385f163"
- },
- "lifter_tone3": {
- "category": "activity",
- "moji": "🏋🏽",
- "description": "weight lifter tone 3",
- "unicodeVersion": "8.0",
- "digest": "9fe7befca13df23016568b785e1174122f1b60f8f7a1104bfd1d948ffe2f552e"
- },
- "lifter_tone4": {
- "category": "activity",
- "moji": "🏋🏾",
- "description": "weight lifter tone 4",
- "unicodeVersion": "8.0",
- "digest": "510cfdae9be775ef9a5866f780a0cd95080b3b6e9959cfbaa41833aa3f24db83"
- },
- "lifter_tone5": {
- "category": "activity",
- "moji": "🏋🏿",
- "description": "weight lifter tone 5",
- "unicodeVersion": "8.0",
- "digest": "7347d561a692bd604d3763115cecc66203f1c9a00515de53acecb4d6e0689d65"
- },
- "light_rail": {
- "category": "travel",
- "moji": "🚈",
- "description": "light rail",
- "unicodeVersion": "6.0",
- "digest": "f7ee16c0a5853c542570a684e25e3fc388e5f52ef8fe7ffaa1120ad7de74a3bd"
- },
- "link": {
- "category": "objects",
- "moji": "🔗",
- "description": "link symbol",
- "unicodeVersion": "6.0",
- "digest": "eb23a200ad464e4d16f3d977e8aef3ee0b55da62d2a7cd72743c1f57839c4e02"
- },
- "lion_face": {
- "category": "nature",
- "moji": "🦁",
- "description": "lion face",
- "unicodeVersion": "8.0",
- "digest": "59067f1acf8b42395164dc02089624d696549bfdbd60a6dd24d04199a0c3dda2"
- },
- "lips": {
- "category": "people",
- "moji": "👄",
- "description": "mouth",
- "unicodeVersion": "6.0",
- "digest": "26fb9b50ab57120d2bc613eb1537fc1517f0c962bf3ae0b43fb2da42581dde5c"
- },
- "lipstick": {
- "category": "people",
- "moji": "💄",
- "description": "lipstick",
- "unicodeVersion": "6.0",
- "digest": "102278217b4f4088fd48ccb08af558822258d85d24a4febdcad1b5cc78cbe2a2"
- },
- "lizard": {
- "category": "nature",
- "moji": "🦎",
- "description": "lizard",
- "unicodeVersion": "9.0",
- "digest": "7c21a4d9e165efe57eca3982944b93ad30bf7ea462bdbf5cb4d63e3dc93e7707"
- },
- "lock": {
- "category": "objects",
- "moji": "🔒",
- "description": "lock",
- "unicodeVersion": "6.0",
- "digest": "63408513eaf29059c6025981c27434f71fa2be87dcd060dd9f971d0e54a73922"
- },
- "lock_with_ink_pen": {
- "category": "objects",
- "moji": "🔏",
- "description": "lock with ink pen",
- "unicodeVersion": "6.0",
- "digest": "85c1713e44becc6543464d2ff4a07dc46b25a4e40fe233695055236d7c4f62aa"
- },
- "lollipop": {
- "category": "food",
- "moji": "🍭",
- "description": "lollipop",
- "unicodeVersion": "6.0",
- "digest": "6499b3140dcc958c4bb99a80ac0424a209d7a978138feb5b919db36443eb529f"
- },
- "loop": {
- "category": "symbols",
- "moji": "➿",
- "description": "double curly loop",
- "unicodeVersion": "6.0",
- "digest": "f9044b19663fb8e3e21aeafa6e1022e20be4d61683f13f1472b1feedc0140d7b"
- },
- "loud_sound": {
- "category": "symbols",
- "moji": "🔊",
- "description": "speaker with three sound waves",
- "unicodeVersion": "6.0",
- "digest": "6d6affb03b43fbfa71796d83521735af6038bfd236f3dacb77d2097496a73a01"
- },
- "loudspeaker": {
- "category": "symbols",
- "moji": "📢",
- "description": "public address loudspeaker",
- "unicodeVersion": "6.0",
- "digest": "04f791bc8d3eb6486448deb2654d9a0cd1c19f946ba623116f556f27399b34a0"
- },
- "love_hotel": {
- "category": "travel",
- "moji": "🏩",
- "description": "love hotel",
- "unicodeVersion": "6.0",
- "digest": "05e846b25799923b3a85826f40d2d22f9e2ea35855620fdde4d1baec25cf5f25"
- },
- "love_letter": {
- "category": "objects",
- "moji": "💌",
- "description": "love letter",
- "unicodeVersion": "6.0",
- "digest": "2a263ff736055811ce621c61f5ef9d9393bb71e180515a0fdc75107b89c60093"
- },
- "low_brightness": {
- "category": "symbols",
- "moji": "🔅",
- "description": "low brightness symbol",
- "unicodeVersion": "6.0",
- "digest": "be7fc79c265d5c02ee7d29fe8290db80cfd794e271463853d6f5d01948c62dfb"
- },
- "lying_face": {
- "category": "people",
- "moji": "🤥",
- "description": "lying face",
- "unicodeVersion": "9.0",
- "digest": "b7a8bcad9036fa6c0441bbc0558cf6ca32464db8ab7d522af505deb1a07623b3"
- },
- "m": {
- "category": "symbols",
- "moji": "Ⓜ",
- "description": "circled latin capital letter m",
- "unicodeVersion": "1.1",
- "digest": "45f66b77808cb780aee7c3440bf57f20f0a7ba3a6048806395e88c47b58263b2"
- },
- "mag": {
- "category": "objects",
- "moji": "🔍",
- "description": "left-pointing magnifying glass",
- "unicodeVersion": "6.0",
- "digest": "bfb8b2d3ef82281c7b821c3814c8213d6f853322546f0107575dedc20de31559"
- },
- "mag_right": {
- "category": "objects",
- "moji": "🔎",
- "description": "right-pointing magnifying glass",
- "unicodeVersion": "6.0",
- "digest": "3ee06dcf290a822c5084d8e0cf245a97b354fb03ff0b5f4ae2a7063a6c967ce7"
- },
- "mahjong": {
- "category": "symbols",
- "moji": "🀄",
- "description": "mahjong tile red dragon",
- "unicodeVersion": "5.1",
- "digest": "adf2b23065245bbffa1f0e7d0a4656bb2c69d6862f162cd77e9eda26aee06353"
- },
- "mailbox": {
- "category": "objects",
- "moji": "📫",
- "description": "closed mailbox with raised flag",
- "unicodeVersion": "6.0",
- "digest": "492df72deb2679ad387f76edf9c9e7475ef7221c5eb8e24fa6c964e5a3250a61"
- },
- "mailbox_closed": {
- "category": "objects",
- "moji": "📪",
- "description": "closed mailbox with lowered flag",
- "unicodeVersion": "6.0",
- "digest": "be27aef10401f26e8f539216b01e2e0774756cb3abb9535d55f91de5415d9737"
- },
- "mailbox_with_mail": {
- "category": "objects",
- "moji": "📬",
- "description": "open mailbox with raised flag",
- "unicodeVersion": "6.0",
- "digest": "bfe3df313a2f57bdb99192ce6e674044e385d044020b6e472b2414fe17649805"
- },
- "mailbox_with_no_mail": {
- "category": "objects",
- "moji": "📭",
- "description": "open mailbox with lowered flag",
- "unicodeVersion": "6.0",
- "digest": "cad6a927c392ed3181284f005eb260976cf69ab6608d59a43ea252a89c89b6e1"
- },
- "man": {
- "category": "people",
- "moji": "👨",
- "description": "man",
- "unicodeVersion": "6.0",
- "digest": "72f5a4ee76d2f91fcce52673cdf08867d9322dde33b2b859a2687d20f2875d7a"
- },
- "man_dancing": {
- "category": "people",
- "moji": "🕺",
- "description": "man dancing",
- "unicodeVersion": "9.0",
- "digest": "1d8c16790d9c7affa997923ea15ce09221cdc9d26b6f82150e91d22463b96319"
- },
- "man_dancing_tone1": {
- "category": "activity",
- "moji": "🕺🏻",
- "description": "man dancing tone 1",
- "unicodeVersion": "9.0",
- "digest": "317e619d66577c49fcba699a0b9bf3ee63d100b487c182b4637b5fd46f532bc2"
- },
- "man_dancing_tone2": {
- "category": "activity",
- "moji": "🕺🏼",
- "description": "man dancing tone 2",
- "unicodeVersion": "9.0",
- "digest": "72a9eea3cee40692a56f234ca59093dfd7ff1113b710a1aaa146b5a137fa213a"
- },
- "man_dancing_tone3": {
- "category": "activity",
- "moji": "🕺🏽",
- "description": "man dancing tone 3",
- "unicodeVersion": "9.0",
- "digest": "97824a84dbb9058b4b90e802c1aa72556d3c5f5941599e3e23fd662534b6421a"
- },
- "man_dancing_tone4": {
- "category": "activity",
- "moji": "🕺🏾",
- "description": "man dancing tone 4",
- "unicodeVersion": "9.0",
- "digest": "632fae4e335dae2713a26cc6ac77e62229f88f24db5b33d3c50da13ae7ce8686"
- },
- "man_dancing_tone5": {
- "category": "activity",
- "moji": "🕺🏿",
- "description": "man dancing tone 5",
- "unicodeVersion": "9.0",
- "digest": "b33a036b2b7f202dc980786796171eeda66caa3cb06320f94617191ec6133ee3"
- },
- "man_in_tuxedo": {
- "category": "people",
- "moji": "🤵",
- "description": "man in tuxedo",
- "unicodeVersion": "9.0",
- "digest": "2da1693a18afdd9722380c4778183195d006563787b2d8b839a9810a18626798"
- },
- "man_in_tuxedo_tone1": {
- "category": "people",
- "moji": "🤵🏻",
- "description": "man in tuxedo tone 1",
- "unicodeVersion": "9.0",
- "digest": "506256c184c13806736c599a07f62ac910b5a10130480355d012a53e11894c79"
- },
- "man_in_tuxedo_tone2": {
- "category": "people",
- "moji": "🤵🏼",
- "description": "man in tuxedo tone 2",
- "unicodeVersion": "9.0",
- "digest": "1413dbbe32c1578fbc3a9afe0bec950aab0da5277f6ae286aaae03870aeb0846"
- },
- "man_in_tuxedo_tone3": {
- "category": "people",
- "moji": "🤵🏽",
- "description": "man in tuxedo tone 3",
- "unicodeVersion": "9.0",
- "digest": "1c42dc5683bf2f9a5ea719de1eda6f9f872950495581ca2201b61cbcdc74a342"
- },
- "man_in_tuxedo_tone4": {
- "category": "people",
- "moji": "🤵🏾",
- "description": "man in tuxedo tone 4",
- "unicodeVersion": "9.0",
- "digest": "9fcf33a14bec175bd7587db3778fe50c5d31dcf2db37e3e941944df133b5b722"
- },
- "man_in_tuxedo_tone5": {
- "category": "people",
- "moji": "🤵🏿",
- "description": "man in tuxedo tone 5",
- "unicodeVersion": "9.0",
- "digest": "941765104eab002f58217ad7ba418a5082732b32faba92e372bafc8debe457f4"
- },
- "man_tone1": {
- "category": "people",
- "moji": "👨🏻",
- "description": "man tone 1",
- "unicodeVersion": "8.0",
- "digest": "fd5531169631dbed6b6380c40732afda0d943980e2a399d9ce65167026f9f33c"
- },
- "man_tone2": {
- "category": "people",
- "moji": "👨🏼",
- "description": "man tone 2",
- "unicodeVersion": "8.0",
- "digest": "628574c3994302e2d1ee0742951f26eb3d44aa1abe1161f78df5db3d18bc377f"
- },
- "man_tone3": {
- "category": "people",
- "moji": "👨🏽",
- "description": "man tone 3",
- "unicodeVersion": "8.0",
- "digest": "431642916465938d09bd6ca5097e716abae550dc5c54dd514519e1561f372b90"
- },
- "man_tone4": {
- "category": "people",
- "moji": "👨🏾",
- "description": "man tone 4",
- "unicodeVersion": "8.0",
- "digest": "26662b4aca1f0a9a24b148a36db1f1119a28e458b1b9addd4ecb4fa36c4c3d3d"
- },
- "man_tone5": {
- "category": "people",
- "moji": "👨🏿",
- "description": "man tone 5",
- "unicodeVersion": "8.0",
- "digest": "e2f45518af9033350ad4c44a42f564403a7248b4d67c8ec21b19199458aaa4de"
- },
- "man_with_gua_pi_mao": {
- "category": "people",
- "moji": "👲",
- "description": "man with gua pi mao",
- "unicodeVersion": "6.0",
- "digest": "f8376151b1df1cca64805f8b64e4b2b3b7358bbd3eb1bbd2cbf363a4a1d542a5"
- },
- "man_with_gua_pi_mao_tone1": {
- "category": "people",
- "moji": "👲🏻",
- "description": "man with gua pi mao tone 1",
- "unicodeVersion": "8.0",
- "digest": "858d738820987110c6dc6811b69f4b9b7107304ab4e094c55d2f09e8f506dff4"
- },
- "man_with_gua_pi_mao_tone2": {
- "category": "people",
- "moji": "👲🏼",
- "description": "man with gua pi mao tone 2",
- "unicodeVersion": "8.0",
- "digest": "52e61c5ba607cdb6c49a9252bbb9290d117aa991e34c7d352ab82b9ee708680b"
- },
- "man_with_gua_pi_mao_tone3": {
- "category": "people",
- "moji": "👲🏽",
- "description": "man with gua pi mao tone 3",
- "unicodeVersion": "8.0",
- "digest": "6ed7de2cb0d2d5434e3fe454b9d56f92f498b7f77a99611ee23f6137627e014c"
- },
- "man_with_gua_pi_mao_tone4": {
- "category": "people",
- "moji": "👲🏾",
- "description": "man with gua pi mao tone 4",
- "unicodeVersion": "8.0",
- "digest": "c0c6caca9dfd6e3420483929b16556414d0d64708cecc05b1e6969f9e15cb103"
- },
- "man_with_gua_pi_mao_tone5": {
- "category": "people",
- "moji": "👲🏿",
- "description": "man with gua pi mao tone 5",
- "unicodeVersion": "8.0",
- "digest": "a1ce066b7903e2156919a88e1f4824ece7598cc3b8384ee27342cdf4b69310f1"
- },
- "man_with_turban": {
- "category": "people",
- "moji": "👳",
- "description": "man with turban",
- "unicodeVersion": "6.0",
- "digest": "7741fa53fa283478eec7746c6d3952551980fa2faf2cadd79ad3e60b22413093"
- },
- "man_with_turban_tone1": {
- "category": "people",
- "moji": "👳🏻",
- "description": "man with turban tone 1",
- "unicodeVersion": "8.0",
- "digest": "e5163b3d793ff9f7f3efe04a4264a160aef3d1c4b6e731a25601a5ebc1f91dc9"
- },
- "man_with_turban_tone2": {
- "category": "people",
- "moji": "👳🏼",
- "description": "man with turban tone 2",
- "unicodeVersion": "8.0",
- "digest": "f99883c8d09281cdb9a63374e4e418767cf104976fe68f353443e3687b93ecf5"
- },
- "man_with_turban_tone3": {
- "category": "people",
- "moji": "👳🏽",
- "description": "man with turban tone 3",
- "unicodeVersion": "8.0",
- "digest": "168392861e99c39719618454721047f1dc75b8fcef07233079806eccdf0b63be"
- },
- "man_with_turban_tone4": {
- "category": "people",
- "moji": "👳🏾",
- "description": "man with turban tone 4",
- "unicodeVersion": "8.0",
- "digest": "1ed7b32cb652d66421a90378b3fd3dbec0da4124886114152ff199f538d8d593"
- },
- "man_with_turban_tone5": {
- "category": "people",
- "moji": "👳🏿",
- "description": "man with turban tone 5",
- "unicodeVersion": "8.0",
- "digest": "0f639e64c4393c5c81d7c8bc3a37b4988acfc7112616dec45c82226fff15f245"
- },
- "mans_shoe": {
- "category": "people",
- "moji": "👞",
- "description": "mans shoe",
- "unicodeVersion": "6.0",
- "digest": "ba3da1749562ad332de9a2c0916c1e72a8b6ccaf277d5c379bf6847f9b6fc148"
- },
- "map": {
- "category": "objects",
- "moji": "🗺",
- "description": "world map",
- "unicodeVersion": "7.0",
- "digest": "8eb87e7238c5dca1b1b8efb181e42f0d91ca515d3e12dac67aafbe1028338d8e"
- },
- "maple_leaf": {
- "category": "nature",
- "moji": "🍁",
- "description": "maple leaf",
- "unicodeVersion": "6.0",
- "digest": "f16bc6dfd5bd33811f8cbf1bca6733715bb1e8b35109d7038f25497a89e79f8e"
- },
- "martial_arts_uniform": {
- "category": "activity",
- "moji": "🥋",
- "description": "martial arts uniform",
- "unicodeVersion": "9.0",
- "digest": "d1953d1f75350bcde491bb2a551e55bafc34e14e5de0ca6f27b1d55e0dffa135"
- },
- "mask": {
- "category": "people",
- "moji": "😷",
- "description": "face with medical mask",
- "unicodeVersion": "6.0",
- "digest": "20b1988145e75b2ba72f5c595245fc5574315ee8c26fd39f7785c0a6fc5a9906"
- },
- "massage": {
- "category": "people",
- "moji": "💆",
- "description": "face massage",
- "unicodeVersion": "6.0",
- "digest": "3c5ede480d35f567954a1dd7082836f0898b44cd41037ce37c0539f8062209a1"
- },
- "massage_tone1": {
- "category": "people",
- "moji": "💆🏻",
- "description": "face massage tone 1",
- "unicodeVersion": "8.0",
- "digest": "d70f8df999a2f2a69eceb60cc09be2d8cabd5121deb051e1e6ef1b60504209c6"
- },
- "massage_tone2": {
- "category": "people",
- "moji": "💆🏼",
- "description": "face massage tone 2",
- "unicodeVersion": "8.0",
- "digest": "1487da84572a32db39b018369df3ddabb1903294425d9b60091d83ac6b59a48a"
- },
- "massage_tone3": {
- "category": "people",
- "moji": "💆🏽",
- "description": "face massage tone 3",
- "unicodeVersion": "8.0",
- "digest": "c10b984c5225440f3da97df2d00b4a88f41ee1cd9902042f64c557e31ddf944d"
- },
- "massage_tone4": {
- "category": "people",
- "moji": "💆🏾",
- "description": "face massage tone 4",
- "unicodeVersion": "8.0",
- "digest": "3140b503be64c8a91a7be46b12585a11b03aba0d3c5186095b734de23b9919e5"
- },
- "massage_tone5": {
- "category": "people",
- "moji": "💆🏿",
- "description": "face massage tone 5",
- "unicodeVersion": "8.0",
- "digest": "ee0d958448ae7c5f0314520d6dab1220d827ad037d2880a72749433cf6381ab3"
- },
- "meat_on_bone": {
- "category": "food",
- "moji": "🍖",
- "description": "meat on bone",
- "unicodeVersion": "6.0",
- "digest": "df7239ae70b5612f38b3bb309d685464cf529a75a6faa27084acbf71dd633a33"
- },
- "medal": {
- "category": "activity",
- "moji": "🏅",
- "description": "sports medal",
- "unicodeVersion": "7.0",
- "digest": "b7518a4c832aa937d85741738e1c74c09ff645197fbbdc7d66cd4a8e78977073"
- },
- "mega": {
- "category": "symbols",
- "moji": "📣",
- "description": "cheering megaphone",
- "unicodeVersion": "6.0",
- "digest": "f28088b3880bf25bb1daa9d624ff3d58d8ab768c1d442ab8a9bfec4032b47343"
- },
- "melon": {
- "category": "food",
- "moji": "🍈",
- "description": "melon",
- "unicodeVersion": "6.0",
- "digest": "75e1353511e5b2c345ec411ca9b8dc7d172fcbc7fbe5366ef0ed96c74035ef32"
- },
- "menorah": {
- "category": "symbols",
- "moji": "🕎",
- "description": "menorah with nine branches",
- "unicodeVersion": "8.0",
- "digest": "befdb755f7f872a9061119929ce34d9f0368a5ca7506ba4f3984b24f69b01a27"
- },
- "mens": {
- "category": "symbols",
- "moji": "🚹",
- "description": "mens symbol",
- "unicodeVersion": "6.0",
- "digest": "eca8312aaade2705ca15be8c1d1fcd897ed4ea0189e995dee3727bcda9d900df"
- },
- "metal": {
- "category": "people",
- "moji": "🤘",
- "description": "sign of the horns",
- "unicodeVersion": "8.0",
- "digest": "9bc7445e2832356d34c88f498c426fcc3fced736323af13cd8bfa18ab4a795f2"
- },
- "metal_tone1": {
- "category": "people",
- "moji": "🤘🏻",
- "description": "sign of the horns tone 1",
- "unicodeVersion": "8.0",
- "digest": "c2107bd9851d508f8128c0dbcd02d3d623597d866d4c938889ec5b4cb2dccf84"
- },
- "metal_tone2": {
- "category": "people",
- "moji": "🤘🏼",
- "description": "sign of the horns tone 2",
- "unicodeVersion": "8.0",
- "digest": "85583c2c1eff98dc005d2c7cd80f75b18fe4723055b677c8f1bc90207cf0b1fd"
- },
- "metal_tone3": {
- "category": "people",
- "moji": "🤘🏽",
- "description": "sign of the horns tone 3",
- "unicodeVersion": "8.0",
- "digest": "f004a5b303b1e7bcf20d46bc42214e21f703658f7f83503888c326d9e76cf29f"
- },
- "metal_tone4": {
- "category": "people",
- "moji": "🤘🏾",
- "description": "sign of the horns tone 4",
- "unicodeVersion": "8.0",
- "digest": "968ebedf7b100f33773f73cfd98c24a62870d022b46ac5c442a8b34184c9a5cf"
- },
- "metal_tone5": {
- "category": "people",
- "moji": "🤘🏿",
- "description": "sign of the horns tone 5",
- "unicodeVersion": "8.0",
- "digest": "be4add5e381ffb482ed191f1f305eeb22707c67e251660ccf76bf550d32e16eb"
- },
- "metro": {
- "category": "travel",
- "moji": "🚇",
- "description": "metro",
- "unicodeVersion": "6.0",
- "digest": "d5053b49a13908a38615c71096d9377d6e8a091f169171eb32d0ab1f209a3424"
- },
- "microphone": {
- "category": "activity",
- "moji": "🎤",
- "description": "microphone",
- "unicodeVersion": "6.0",
- "digest": "5d7d70a4347d677eeed8dcd0f87f9ed4b10a14a6ebeb9279eab9d6ed4d505d28"
- },
- "microphone2": {
- "category": "objects",
- "moji": "🎙",
- "description": "studio microphone",
- "unicodeVersion": "7.0",
- "digest": "c1b14280e7a0bfd9ee4c8f83024f9cbf5a15a5bdadf3a4795f101864a4ad2c6d"
- },
- "microscope": {
- "category": "objects",
- "moji": "🔬",
- "description": "microscope",
- "unicodeVersion": "6.0",
- "digest": "b732bdd52a38057a56cfcddf2598c9a2d91f0838686f960a91ddc7333fa0ae12"
- },
- "middle_finger": {
- "category": "people",
- "moji": "🖕",
- "description": "reversed hand with middle finger extended",
- "unicodeVersion": "7.0",
- "digest": "7d543ffbc78a5e8b9162c0f48e4503f6e32efefea7f3b22f436463ace77066fa"
- },
- "middle_finger_tone1": {
- "category": "people",
- "moji": "🖕🏻",
- "description": "reversed hand with middle finger extended tone 1",
- "unicodeVersion": "8.0",
- "digest": "40a676ff704d57f6b5d2bd8c31b183600781bf3f0ff4342f1e4886717228e0ee"
- },
- "middle_finger_tone2": {
- "category": "people",
- "moji": "🖕🏼",
- "description": "reversed hand with middle finger extended tone 2",
- "unicodeVersion": "8.0",
- "digest": "9dcd0cc6a88d67d7fd561fccf95fd3a6030b53598fea69d5de6f14ffd72b3a82"
- },
- "middle_finger_tone3": {
- "category": "people",
- "moji": "🖕🏽",
- "description": "reversed hand with middle finger extended tone 3",
- "unicodeVersion": "8.0",
- "digest": "7787a192eff949d308e925c9df0a44153429df4a290c6f348a950e8414b1d4dc"
- },
- "middle_finger_tone4": {
- "category": "people",
- "moji": "🖕🏾",
- "description": "reversed hand with middle finger extended tone 4",
- "unicodeVersion": "8.0",
- "digest": "b5b4b65aa300d498aaef8753cffb34455111887a12a378b2517297622f428330"
- },
- "middle_finger_tone5": {
- "category": "people",
- "moji": "🖕🏿",
- "description": "reversed hand with middle finger extended tone 5",
- "unicodeVersion": "8.0",
- "digest": "c08f75c25bd88a288d685849a8575d868b257296c1afae80d241ec5e9193bea3"
- },
- "military_medal": {
- "category": "activity",
- "moji": "🎖",
- "description": "military medal",
- "unicodeVersion": "7.0",
- "digest": "745687033024bc5d1dae74f427504058f17f29c1f02a59b25c9d488a6307cae3"
- },
- "milk": {
- "category": "food",
- "moji": "🥛",
- "description": "glass of milk",
- "unicodeVersion": "9.0",
- "digest": "3f9229a2c754345be8e721dde032f289fa92c23a11cfd1afe6b38b44d34e435d"
- },
- "milky_way": {
- "category": "travel",
- "moji": "🌌",
- "description": "milky way",
- "unicodeVersion": "6.0",
- "digest": "6664f60e65321fccf491333caca68f961acfdd35a24f299e5909369a94816a6b"
- },
- "minibus": {
- "category": "travel",
- "moji": "🚐",
- "description": "minibus",
- "unicodeVersion": "6.0",
- "digest": "7e71052ea22df57fa12c7e3829924ebd3f144b9d71301c34755c10d33ddee697"
- },
- "minidisc": {
- "category": "objects",
- "moji": "💽",
- "description": "minidisc",
- "unicodeVersion": "6.0",
- "digest": "5b6ddb7b5242dffdc04755e8dcac4cc3ae237040481bf076a8c1f95ce0ada3b6"
- },
- "mobile_phone_off": {
- "category": "symbols",
- "moji": "📴",
- "description": "mobile phone off",
- "unicodeVersion": "6.0",
- "digest": "93373556567d92fdb11d4e562e74c15ba6b0987cb26dea909b0e922921087628"
- },
- "money_mouth": {
- "category": "people",
- "moji": "🤑",
- "description": "money-mouth face",
- "unicodeVersion": "8.0",
- "digest": "99ba4973b84ecb2dbf7e6303190c22c67eedf750f49313135ddbe8e541650688"
- },
- "money_with_wings": {
- "category": "objects",
- "moji": "💸",
- "description": "money with wings",
- "unicodeVersion": "6.0",
- "digest": "3d2b0e5939f92d0ab9e40758a9ec239817ad6e9463fd2140803ee229bdc95720"
- },
- "moneybag": {
- "category": "objects",
- "moji": "💰",
- "description": "money bag",
- "unicodeVersion": "6.0",
- "digest": "5fbc74d9eb713ca5d00c8ecb528361bc22c24ab97fdab6606427acc5af480c07"
- },
- "monkey": {
- "category": "nature",
- "moji": "🐒",
- "description": "monkey",
- "unicodeVersion": "6.0",
- "digest": "0e555b55cdaeea97aba5e054ce6c283578036d739466a78be63662872729cf55"
- },
- "monkey_face": {
- "category": "nature",
- "moji": "🐵",
- "description": "monkey face",
- "unicodeVersion": "6.0",
- "digest": "f006cc5b32745c0e315ad2b2e995587f7f3dec115f81db7f681e00e040e1ab02"
- },
- "monorail": {
- "category": "travel",
- "moji": "🚝",
- "description": "monorail",
- "unicodeVersion": "6.0",
- "digest": "551b6d88c465c54bd2fc03eb396a6afbc68e24aa41c07203c84fd36e31608836"
- },
- "mortar_board": {
- "category": "people",
- "moji": "🎓",
- "description": "graduation cap",
- "unicodeVersion": "6.0",
- "digest": "6524faad267ba769fb1997527d41c97cac42cc5d99c57800adaf5a6892c79371"
- },
- "mosque": {
- "category": "travel",
- "moji": "🕌",
- "description": "mosque",
- "unicodeVersion": "8.0",
- "digest": "f15b137d0d23275694d6743602054d05334c9dfb15964378f38defddc8a25b2a"
- },
- "motor_scooter": {
- "category": "travel",
- "moji": "🛵",
- "description": "motor scooter",
- "unicodeVersion": "9.0",
- "digest": "cb76bb0e2f94960426a85f273e2a74046eefebd0b6a265bafecfb8850f0a0c6c"
- },
- "motorboat": {
- "category": "travel",
- "moji": "🛥",
- "description": "motorboat",
- "unicodeVersion": "7.0",
- "digest": "edee0a9d9d7b9b9201fc41d5b5a38c939e5f6e2e7baa7891e495b91dc5198e65"
- },
- "motorcycle": {
- "category": "travel",
- "moji": "🏍",
- "description": "racing motorcycle",
- "unicodeVersion": "7.0",
- "digest": "b30c2e98f3a439b0c0b6481a5025ee3d380c35a7aa43d7b46c3e948fe732613c"
- },
- "motorway": {
- "category": "travel",
- "moji": "🛣",
- "description": "motorway",
- "unicodeVersion": "7.0",
- "digest": "96694ecc0e48e7b0ab1e0a8fdf5e75169ae404fad47620986dfd120571e39e58"
- },
- "mount_fuji": {
- "category": "travel",
- "moji": "🗻",
- "description": "mount fuji",
- "unicodeVersion": "6.0",
- "digest": "dd27d0550d5df7dd376a43e56e0819a41c48f21cc89e58f36d290140c4effd5e"
- },
- "mountain": {
- "category": "travel",
- "moji": "⛰",
- "description": "mountain",
- "unicodeVersion": "5.2",
- "digest": "5e0087bb9324c99b5b4f730b8878cadab69cd3af0f580df6c31490a7b7b0abe4"
- },
- "mountain_bicyclist": {
- "category": "activity",
- "moji": "🚵",
- "description": "mountain bicyclist",
- "unicodeVersion": "6.0",
- "digest": "f3a2fd3bc93fe4e5a039a81e8d5e11b25c949ae041f822a49b15ea7dc35f560e"
- },
- "mountain_bicyclist_tone1": {
- "category": "activity",
- "moji": "🚵🏻",
- "description": "mountain bicyclist tone 1",
- "unicodeVersion": "8.0",
- "digest": "dc3b22a219da2c45a6cc45e67ea76e5b35a74eae46f9202dc2f19dd66effc595"
- },
- "mountain_bicyclist_tone2": {
- "category": "activity",
- "moji": "🚵🏼",
- "description": "mountain bicyclist tone 2",
- "unicodeVersion": "8.0",
- "digest": "1ac6898b085ce24e5d80fbafc2c9c9afe6390beae052097f92bfeb9f85a5f906"
- },
- "mountain_bicyclist_tone3": {
- "category": "activity",
- "moji": "🚵🏽",
- "description": "mountain bicyclist tone 3",
- "unicodeVersion": "8.0",
- "digest": "b0c9d179b0ba26c618b69c46aad4f7470b49b9f6c173c452c7a56b61a54537bc"
- },
- "mountain_bicyclist_tone4": {
- "category": "activity",
- "moji": "🚵🏾",
- "description": "mountain bicyclist tone 4",
- "unicodeVersion": "8.0",
- "digest": "e59f743d7e8e8802973da998bc3627f12101508caef7ca2512b8fecb7f9c34d4"
- },
- "mountain_bicyclist_tone5": {
- "category": "activity",
- "moji": "🚵🏿",
- "description": "mountain bicyclist tone 5",
- "unicodeVersion": "8.0",
- "digest": "ca19109c586fc3d0ddf1662b497f517096a813bfcec8184989fa6b578eb57424"
- },
- "mountain_cableway": {
- "category": "travel",
- "moji": "🚠",
- "description": "mountain cableway",
- "unicodeVersion": "6.0",
- "digest": "22969301dc7395450c11ba2c81af096b2ce98da13786109dee70d801563a59b5"
- },
- "mountain_railway": {
- "category": "travel",
- "moji": "🚞",
- "description": "mountain railway",
- "unicodeVersion": "6.0",
- "digest": "3ce97b097df39e9e46eb09d1d544b1bd45596a232fd9163b8f02c37ecaffe794"
- },
- "mountain_snow": {
- "category": "travel",
- "moji": "🏔",
- "description": "snow capped mountain",
- "unicodeVersion": "7.0",
- "digest": "9b7e802436798fd7eb042bbe31cdb77c124196cd262dca3e671e5ad278fe742f"
- },
- "mouse": {
- "category": "nature",
- "moji": "🐭",
- "description": "mouse face",
- "unicodeVersion": "6.0",
- "digest": "ba3d78feeca02888c96bc81ffbe07d8be073f695ab17e167d63c4fe0ad2edfc0"
- },
- "mouse2": {
- "category": "nature",
- "moji": "🐁",
- "description": "mouse",
- "unicodeVersion": "6.0",
- "digest": "abd7dded58299599e4cd419c3052e0d4e52ccc788e905201f5dd225b74152aff"
- },
- "mouse_three_button": {
- "category": "objects",
- "moji": "🖱",
- "description": "three button mouse",
- "unicodeVersion": "7.0",
- "digest": "aea26b9ebd3ea81d0e8b56c8a0281a94e9862e9deda989ec679150e7aec36b5b"
- },
- "movie_camera": {
- "category": "objects",
- "moji": "🎥",
- "description": "movie camera",
- "unicodeVersion": "6.0",
- "digest": "f82751e6f069482fbc01b0d2d135fdf58ef3cf58526b7dbf5b50a71b5ea0ae75"
- },
- "moyai": {
- "category": "objects",
- "moji": "🗿",
- "description": "moyai",
- "unicodeVersion": "6.0",
- "digest": "19b4b5efdb559f958b114fbaf6a3ad017a42012528de7a1dfedee5dbc3bce3d3"
- },
- "mrs_claus": {
- "category": "people",
- "moji": "🤶",
- "description": "mother christmas",
- "unicodeVersion": "9.0",
- "digest": "67d7fccbfd20aa195e526485ce40e52994aa4fa9fb71c70533fb579f4ca0ef1f"
- },
- "mrs_claus_tone1": {
- "category": "people",
- "moji": "🤶🏻",
- "description": "mother christmas tone 1",
- "unicodeVersion": "9.0",
- "digest": "c95a39dcdabe20d1f18a2c5cf7cbf1e530fe9ced7ffdbb0f7f8cf833663f8775"
- },
- "mrs_claus_tone2": {
- "category": "people",
- "moji": "🤶🏼",
- "description": "mother christmas tone 2",
- "unicodeVersion": "9.0",
- "digest": "425a73cb7d57dbaaf13b1156329ef31a0e9b8cc3917c16b90cc561d4d3373bad"
- },
- "mrs_claus_tone3": {
- "category": "people",
- "moji": "🤶🏽",
- "description": "mother christmas tone 3",
- "unicodeVersion": "9.0",
- "digest": "59fd4b47738832f3996b94f67e049edf5d64e0c346b55c9ff09cb893db898fbb"
- },
- "mrs_claus_tone4": {
- "category": "people",
- "moji": "🤶🏾",
- "description": "mother christmas tone 4",
- "unicodeVersion": "9.0",
- "digest": "e96e7945124124002f13cb381f43dad4f40d77b3ec9eb691a58c929ea3214c0d"
- },
- "mrs_claus_tone5": {
- "category": "people",
- "moji": "🤶🏿",
- "description": "mother christmas tone 5",
- "unicodeVersion": "9.0",
- "digest": "a1639e1ed2d862d48e528a5b3f1da14e8afddbed09fafed8f0ad1dbb75c335dc"
- },
- "muscle": {
- "category": "people",
- "moji": "💪",
- "description": "flexed biceps",
- "unicodeVersion": "6.0",
- "digest": "5e6bff383eb4b63009779c1872797eb8b6651788b4005fa0af12b254bb67d404"
- },
- "muscle_tone1": {
- "category": "people",
- "moji": "💪🏻",
- "description": "flexed biceps tone 1",
- "unicodeVersion": "8.0",
- "digest": "921fd25fcc812896f4d4f8ba7f84af84adc72f624ec464a8a3d7414a788d25e7"
- },
- "muscle_tone2": {
- "category": "people",
- "moji": "💪🏼",
- "description": "flexed biceps tone 2",
- "unicodeVersion": "8.0",
- "digest": "e43dd3f98fa6e0916a3da459a66d8e39cc4d83aab68848343fa830b589ad39d2"
- },
- "muscle_tone3": {
- "category": "people",
- "moji": "💪🏽",
- "description": "flexed biceps tone 3",
- "unicodeVersion": "8.0",
- "digest": "508b89b10736e79d6e951cdaf0418912dfed191b464ffe93cfd8621493d7b382"
- },
- "muscle_tone4": {
- "category": "people",
- "moji": "💪🏾",
- "description": "flexed biceps tone 4",
- "unicodeVersion": "8.0",
- "digest": "ac2e279defdba9ba232b254936f5069f3a17313454d5f2c2f7ca62084a6b50d8"
- },
- "muscle_tone5": {
- "category": "people",
- "moji": "💪🏿",
- "description": "flexed biceps tone 5",
- "unicodeVersion": "8.0",
- "digest": "91b6e61e0815b7f746dae45fae5cdb15d9f4b8608188673260d7e4aa134f58ed"
- },
- "mushroom": {
- "category": "nature",
- "moji": "🍄",
- "description": "mushroom",
- "unicodeVersion": "6.0",
- "digest": "76123b383ae3515904cc8015b730a419c7dac5b16df83e8f0baf7b472e08adbe"
- },
- "musical_keyboard": {
- "category": "activity",
- "moji": "🎹",
- "description": "musical keyboard",
- "unicodeVersion": "6.0",
- "digest": "85f8bb1668f64cde65c886b207ae588a794e98b6b64dfaee96659b80bc42b644"
- },
- "musical_note": {
- "category": "symbols",
- "moji": "🎵",
- "description": "musical note",
- "unicodeVersion": "6.0",
- "digest": "9204e70e4d68df5d8ab44087881c90c6d6cce0ac0403d767781cfb8ec23f3a98"
- },
- "musical_score": {
- "category": "activity",
- "moji": "🎼",
- "description": "musical score",
- "unicodeVersion": "6.0",
- "digest": "a379d47ab59308c0faabf74167d53085595a491ced7b1c2d8b9e9fc53575a650"
- },
- "mute": {
- "category": "symbols",
- "moji": "🔇",
- "description": "speaker with cancellation stroke",
- "unicodeVersion": "6.0",
- "digest": "5f80daf97d6bffc8451bbf9b4bc3780a2b555e9449c23922a84feef93a238953"
- },
- "nail_care": {
- "category": "people",
- "moji": "💅",
- "description": "nail polish",
- "unicodeVersion": "6.0",
- "digest": "1f88d57808e1e93dd8870eb24235b1700337b441ea96b7ddabfdae1377bd5795"
- },
- "nail_care_tone1": {
- "category": "people",
- "moji": "💅🏻",
- "description": "nail polish tone 1",
- "unicodeVersion": "8.0",
- "digest": "4faa570dee925e2eaebfc2cd8ccd93dcefd5087e38b6aedadd356ae5234e89a5"
- },
- "nail_care_tone2": {
- "category": "people",
- "moji": "💅🏼",
- "description": "nail polish tone 2",
- "unicodeVersion": "8.0",
- "digest": "320a8d5586f108d66b39cc6df25677fed43bb02914c3c867736881af4195d739"
- },
- "nail_care_tone3": {
- "category": "people",
- "moji": "💅🏽",
- "description": "nail polish tone 3",
- "unicodeVersion": "8.0",
- "digest": "70d8d5a203e23ada34c18c2f368352efb247823a3db919c75c02f659bdc2601f"
- },
- "nail_care_tone4": {
- "category": "people",
- "moji": "💅🏾",
- "description": "nail polish tone 4",
- "unicodeVersion": "8.0",
- "digest": "71d4e886b73ccdbdab9963bedbd23cbaa3693af5e2b9911db8a51d3d3fbb9843"
- },
- "nail_care_tone5": {
- "category": "people",
- "moji": "💅🏿",
- "description": "nail polish tone 5",
- "unicodeVersion": "8.0",
- "digest": "c3cd47041cf0095e03893e165c9b591d7497abf7271588463ef9e8ca42191108"
- },
- "name_badge": {
- "category": "symbols",
- "moji": "📛",
- "description": "name badge",
- "unicodeVersion": "6.0",
- "digest": "42cca8b42700765726f33adac7fd6ddb8911d4e2b5ea680c4348c0c699d61c3f"
- },
- "nauseated_face": {
- "category": "people",
- "moji": "🤢",
- "description": "nauseated face",
- "unicodeVersion": "9.0",
- "digest": "3b3f3fe5fdd6aa6e30bf433d3533438cbee50d337fb2ad83b154873aaac0d9d1"
- },
- "necktie": {
- "category": "people",
- "moji": "👔",
- "description": "necktie",
- "unicodeVersion": "6.0",
- "digest": "1ae4a9a8c38b68c883bf843a044ebb7902ec5c9d73c6bbc1111b847d0353f657"
- },
- "negative_squared_cross_mark": {
- "category": "symbols",
- "moji": "❎",
- "description": "negative squared cross mark",
- "unicodeVersion": "6.0",
- "digest": "cb116b6722949b59acdcb92a8a46fb4ab8f0208dafca73a55b49b09b2d8d37a4"
- },
- "nerd": {
- "category": "people",
- "moji": "🤓",
- "description": "nerd face",
- "unicodeVersion": "8.0",
- "digest": "da428d87fe165911944fb1a15ef2fa4859a1c181fc4dc712907eaea7daba1c85"
- },
- "neutral_face": {
- "category": "people",
- "moji": "😐",
- "description": "neutral face",
- "unicodeVersion": "6.0",
- "digest": "d69ad475b00bc3770047b0d3e06ebd1f3b6523c285d80402c233bf42fe967e8e"
- },
- "new": {
- "category": "symbols",
- "moji": "🆕",
- "description": "squared new",
- "unicodeVersion": "6.0",
- "digest": "fe42aef2603c69544297c0330168c37a844a191cb541344f49b2b0e3b7b01457"
- },
- "new_moon": {
- "category": "nature",
- "moji": "🌑",
- "description": "new moon symbol",
- "unicodeVersion": "6.0",
- "digest": "bb7c1576993e7aed13ae107a4f51c41aa1e811038c6c86a82388c8f6fa05dd72"
- },
- "new_moon_with_face": {
- "category": "nature",
- "moji": "🌚",
- "description": "new moon with face",
- "unicodeVersion": "6.0",
- "digest": "7d341f6f48648d0c4c45677509d3aac49c7b38162a06d28540fca49fb3ec9de6"
- },
- "newspaper": {
- "category": "objects",
- "moji": "📰",
- "description": "newspaper",
- "unicodeVersion": "6.0",
- "digest": "591892ad44fd9168ec274c76bf212a15020dd91091623f79526514b9d83f84c3"
- },
- "newspaper2": {
- "category": "objects",
- "moji": "🗞",
- "description": "rolled-up newspaper",
- "unicodeVersion": "7.0",
- "digest": "a2d8413b95004751aa47bbb80bd5bd067b0591cf1c832ecb6b7911b88be7043c"
- },
- "ng": {
- "category": "symbols",
- "moji": "🆖",
- "description": "squared ng",
- "unicodeVersion": "6.0",
- "digest": "53b461719257c3a5d4c413bebc6a4ab5b6c8c66a76f47f5354fc800746fef815"
- },
- "night_with_stars": {
- "category": "travel",
- "moji": "🌃",
- "description": "night with stars",
- "unicodeVersion": "6.0",
- "digest": "0ca3f52bf56337784d36c545285fce5777bfe6fb01631c3491e33fec61e12474"
- },
- "nine": {
- "category": "symbols",
- "moji": "9️⃣",
- "description": "keycap digit nine",
- "unicodeVersion": "3.0",
- "digest": "c4f0670503ff77ffd10b8de9f0b5f240283cdf7323889dec6a38240fa5ddeae1"
- },
- "no_bell": {
- "category": "symbols",
- "moji": "🔕",
- "description": "bell with cancellation stroke",
- "unicodeVersion": "6.0",
- "digest": "d04d96e4032e288d191079579c2cc3a24e41e02cc4bb681e85cd0f45e01af0a4"
- },
- "no_bicycles": {
- "category": "symbols",
- "moji": "🚳",
- "description": "no bicycles",
- "unicodeVersion": "6.0",
- "digest": "0f0d8e8ab9e421e5f87af7d8dd2afde646d47e35846483e350e11d46039181b6"
- },
- "no_entry": {
- "category": "symbols",
- "moji": "⛔",
- "description": "no entry",
- "unicodeVersion": "5.2",
- "digest": "a960fb2c78aac5ca7ad01e6f5c3dc2212f050f3e4e0dace7bbf79168b63857f6"
- },
- "no_entry_sign": {
- "category": "symbols",
- "moji": "🚫",
- "description": "no entry sign",
- "unicodeVersion": "6.0",
- "digest": "ed0f2355d1edca66757f78849bebeeac010c19bf7d443c04eafa01e15a3f12fe"
- },
- "no_good": {
- "category": "people",
- "moji": "🙅",
- "description": "face with no good gesture",
- "unicodeVersion": "6.0",
- "digest": "9db57840d894a17c23a77c4a5ed315aa852d85ab2841e3393494c600b635f341"
- },
- "no_good_tone1": {
- "category": "people",
- "moji": "🙅🏻",
- "description": "face with no good gesture tone 1",
- "unicodeVersion": "8.0",
- "digest": "ecb77f526ecedd1414faf252ec85a7f468ee0429c0684f69eefa28e3cf4e5710"
- },
- "no_good_tone2": {
- "category": "people",
- "moji": "🙅🏼",
- "description": "face with no good gesture tone 2",
- "unicodeVersion": "8.0",
- "digest": "509882d9171d11cec481bb561d805c3719828954f71ac6b3ec86b34eeea85bfe"
- },
- "no_good_tone3": {
- "category": "people",
- "moji": "🙅🏽",
- "description": "face with no good gesture tone 3",
- "unicodeVersion": "8.0",
- "digest": "7c8d9ba5acdbcad2e99cee95808de8d65f1d8c8a2474bb1a71aca04158e0a542"
- },
- "no_good_tone4": {
- "category": "people",
- "moji": "🙅🏾",
- "description": "face with no good gesture tone 4",
- "unicodeVersion": "8.0",
- "digest": "64a58084e10960f9d2c118b927a6bba759f5aa0038988f1984e7578aee4b16e4"
- },
- "no_good_tone5": {
- "category": "people",
- "moji": "🙅🏿",
- "description": "face with no good gesture tone 5",
- "unicodeVersion": "8.0",
- "digest": "20266de1fbdc6ccb83b4da026c7c71c853dc566d49a39ece5b7379f79c3cf68b"
- },
- "no_mobile_phones": {
- "category": "symbols",
- "moji": "📵",
- "description": "no mobile phones",
- "unicodeVersion": "6.0",
- "digest": "e7fc1cf8ab08e5144cd9a515098e9ea5e5c6dc7d098b764792a6a175c7ac7cab"
- },
- "no_mouth": {
- "category": "people",
- "moji": "😶",
- "description": "face without mouth",
- "unicodeVersion": "6.0",
- "digest": "47a0110f84c97673d86cca26854505e47b0e94af996e23eff58e3861baca4b43"
- },
- "no_pedestrians": {
- "category": "symbols",
- "moji": "🚷",
- "description": "no pedestrians",
- "unicodeVersion": "6.0",
- "digest": "490933e9068e71aa8ab4e864768dbba8c0dcb76d3958261ba0a3da1139984862"
- },
- "no_smoking": {
- "category": "symbols",
- "moji": "🚭",
- "description": "no smoking symbol",
- "unicodeVersion": "6.0",
- "digest": "90337e0742354ba1e87c0262300e48172cfc5db1c9efb4b6837be49efdec73d3"
- },
- "non-potable_water": {
- "category": "symbols",
- "moji": "🚱",
- "description": "non-potable water symbol",
- "unicodeVersion": "6.0",
- "digest": "fded06ba8a998777f04af38833981c8b7980772be1b37509bfbe1692a5e9c81b"
- },
- "nose": {
- "category": "people",
- "moji": "👃",
- "description": "nose",
- "unicodeVersion": "6.0",
- "digest": "b53c2b2226bbaf8505e196a760717d503e7bd886c0b3a87c08bc56138867492e"
- },
- "nose_tone1": {
- "category": "people",
- "moji": "👃🏻",
- "description": "nose tone 1",
- "unicodeVersion": "8.0",
- "digest": "bfc69572dac70db3a59abaac127f854ba648d3a8acf4d5dd0478d6c381910776"
- },
- "nose_tone2": {
- "category": "people",
- "moji": "👃🏼",
- "description": "nose tone 2",
- "unicodeVersion": "8.0",
- "digest": "b6dfa564b8c1859930b3f01da0131c43e3e96f055e45ff7de166493e1d14aab1"
- },
- "nose_tone3": {
- "category": "people",
- "moji": "👃🏽",
- "description": "nose tone 3",
- "unicodeVersion": "8.0",
- "digest": "1dc363dd57dda74e17467b06eb82bea745bf48faca589ffca709cc1e8dd4ad5e"
- },
- "nose_tone4": {
- "category": "people",
- "moji": "👃🏾",
- "description": "nose tone 4",
- "unicodeVersion": "8.0",
- "digest": "76c8919041fa06f25d0bc2eb80d8491f673a1f708680bad1e5f8240d1d97949b"
- },
- "nose_tone5": {
- "category": "people",
- "moji": "👃🏿",
- "description": "nose tone 5",
- "unicodeVersion": "8.0",
- "digest": "08db1747f19cf6f1129f38f5e9fcecc51d65043cfd408cb3b6ccbbd9f491165e"
- },
- "notebook": {
- "category": "objects",
- "moji": "📓",
- "description": "notebook",
- "unicodeVersion": "6.0",
- "digest": "aeb529f82e76ab543f7ab44ef530cbc3558b586559b9e04a6fddc0a0a06ebbb2"
- },
- "notebook_with_decorative_cover": {
- "category": "objects",
- "moji": "📔",
- "description": "notebook with decorative cover",
- "unicodeVersion": "6.0",
- "digest": "6054dca42cc303e7a8d89111d06aaca38950e179a348fc1a2a7c3405401de8a0"
- },
- "notepad_spiral": {
- "category": "objects",
- "moji": "🗒",
- "description": "spiral note pad",
- "unicodeVersion": "7.0",
- "digest": "cff2a9d4657b423731ba9d91e366ffd774a8504650039b5757ace4c0911eda9f"
- },
- "notes": {
- "category": "symbols",
- "moji": "🎶",
- "description": "multiple musical notes",
- "unicodeVersion": "6.0",
- "digest": "e18a0e7c520b862def1df85e1d56ef4a18f42e9998a2e9a000045cfddb6bf75d"
- },
- "nut_and_bolt": {
- "category": "objects",
- "moji": "🔩",
- "description": "nut and bolt",
- "unicodeVersion": "6.0",
- "digest": "e1a81f58c68ec8371dc7fc146e225fe3b95fcedcea4194da744793a28c3fe263"
- },
- "o": {
- "category": "symbols",
- "moji": "⭕",
- "description": "heavy large circle",
- "unicodeVersion": "5.2",
- "digest": "31fd3373121e1690bd2368bb8964007bae2b151345184bd30bc719320b7540b0"
- },
- "o2": {
- "category": "symbols",
- "moji": "🅾",
- "description": "negative squared latin capital letter o",
- "unicodeVersion": "6.0",
- "digest": "672d9a179c75b9375f51175db75154a8b0006e60a9b4546658d9cddca8ab0a86"
- },
- "ocean": {
- "category": "nature",
- "moji": "🌊",
- "description": "water wave",
- "unicodeVersion": "6.0",
- "digest": "495ef59f29b98d5e87b9353274051950dbf7ac9fe486571775389c10e658fa5c"
- },
- "octagonal_sign": {
- "category": "symbols",
- "moji": "🛑",
- "description": "octagonal sign",
- "unicodeVersion": "9.0",
- "digest": "0b138b4b190a12e0601bf71573378d5cce2012d72b9606b01a572ad7f7631c9d"
- },
- "octopus": {
- "category": "nature",
- "moji": "🐙",
- "description": "octopus",
- "unicodeVersion": "6.0",
- "digest": "215314d376c54c12b73eef4115a8d91a3a21f39e39d41a72b24ef2228595b6af"
- },
- "oden": {
- "category": "food",
- "moji": "🍢",
- "description": "oden",
- "unicodeVersion": "6.0",
- "digest": "af6a824831041aacce2e5c510a3bc1a46ddc68f0083bcbfbbd30df941e50ec8b"
- },
- "office": {
- "category": "travel",
- "moji": "🏢",
- "description": "office building",
- "unicodeVersion": "6.0",
- "digest": "404b17445c4da0dc0ec8561604d741bb1c4f1aad8d7d49299249c9732fb33d40"
- },
- "oil": {
- "category": "objects",
- "moji": "🛢",
- "description": "oil drum",
- "unicodeVersion": "7.0",
- "digest": "d2307ad3c2e74c0c5c2594d34e77e0a31a5ef5c0c052a147f24f119054697e1e"
- },
- "ok": {
- "category": "symbols",
- "moji": "🆗",
- "description": "squared ok",
- "unicodeVersion": "6.0",
- "digest": "df301dcd8dc7df31e26a6ba360e8f8b3c0f5bd4869301fde41ccfa3271866ab9"
- },
- "ok_hand": {
- "category": "people",
- "moji": "👌",
- "description": "ok hand sign",
- "unicodeVersion": "6.0",
- "digest": "7ef74c756b59eb60e85daf27cb92962c376733220fbfdb5569be95e67496abc0"
- },
- "ok_hand_tone1": {
- "category": "people",
- "moji": "👌🏻",
- "description": "ok hand sign tone 1",
- "unicodeVersion": "8.0",
- "digest": "285a19578d98da6686b597b5e384edab263d8129194dd3672767d9c67632dae5"
- },
- "ok_hand_tone2": {
- "category": "people",
- "moji": "👌🏼",
- "description": "ok hand sign tone 2",
- "unicodeVersion": "8.0",
- "digest": "b746860ad63866d6afac53fb82ac6b593e9fbcedad18728bf874091f12c4284d"
- },
- "ok_hand_tone3": {
- "category": "people",
- "moji": "👌🏽",
- "description": "ok hand sign tone 3",
- "unicodeVersion": "8.0",
- "digest": "b5eba516e1d45861434c3871ef11450771aecc6d219b9328cea618424f2c0f4e"
- },
- "ok_hand_tone4": {
- "category": "people",
- "moji": "👌🏾",
- "description": "ok hand sign tone 4",
- "unicodeVersion": "8.0",
- "digest": "c4f1bf219363ef580b95fcb99e7aa541ddf1464e17d6d837f4a535b8c6eb0b58"
- },
- "ok_hand_tone5": {
- "category": "people",
- "moji": "👌🏿",
- "description": "ok hand sign tone 5",
- "unicodeVersion": "8.0",
- "digest": "fccfab629162bb963f7dd60b84a2fd101f52a1f51edaf2e84a4dca5149ec1516"
- },
- "ok_woman": {
- "category": "people",
- "moji": "🙆",
- "description": "face with ok gesture",
- "unicodeVersion": "6.0",
- "digest": "0b08d0e64c0a55e47d0f783034a46fbfe7a742388b2961dd13adcd91217f5551"
- },
- "ok_woman_tone1": {
- "category": "people",
- "moji": "🙆🏻",
- "description": "face with ok gesture tone1",
- "unicodeVersion": "8.0",
- "digest": "daaa1c03e25a234c577fa82d868cd7341e4b21262be8a19ebc8ec52c1dfaf3be"
- },
- "ok_woman_tone2": {
- "category": "people",
- "moji": "🙆🏼",
- "description": "face with ok gesture tone2",
- "unicodeVersion": "8.0",
- "digest": "4441187c1bfb3953731fa3cc98fca07f8ef499001af7c954e9e6c766bc113215"
- },
- "ok_woman_tone3": {
- "category": "people",
- "moji": "🙆🏽",
- "description": "face with ok gesture tone3",
- "unicodeVersion": "8.0",
- "digest": "08ef7d0a47d54ba9b8ed19c5d317219ea6d675154085c0e311af845a3cc753f9"
- },
- "ok_woman_tone4": {
- "category": "people",
- "moji": "🙆🏾",
- "description": "face with ok gesture tone4",
- "unicodeVersion": "8.0",
- "digest": "eb8183accaeae7920c7150c13f3d484fefc2868d3203760d4a1e42b04ac14e40"
- },
- "ok_woman_tone5": {
- "category": "people",
- "moji": "🙆🏿",
- "description": "face with ok gesture tone5",
- "unicodeVersion": "8.0",
- "digest": "a8fbb88db77c35ce9fdf85ede6ed71de726277464613b46fcc024b0c38acc675"
- },
- "older_man": {
- "category": "people",
- "moji": "👴",
- "description": "older man",
- "unicodeVersion": "6.0",
- "digest": "84b2a44e5fcc74bc599566caffd79472cdb5c521d7c1b011753d36b8a629cd3f"
- },
- "older_man_tone1": {
- "category": "people",
- "moji": "👴🏻",
- "description": "older man tone 1",
- "unicodeVersion": "8.0",
- "digest": "1f9e38cfb593a30d9afc29bdb18fa00ead144290dbd5a1c34190855e58f3f930"
- },
- "older_man_tone2": {
- "category": "people",
- "moji": "👴🏼",
- "description": "older man tone 2",
- "unicodeVersion": "8.0",
- "digest": "ddef54810d7a168ada23e30686ea9a09d2abb440c230ac5c7823ef731b7aac49"
- },
- "older_man_tone3": {
- "category": "people",
- "moji": "👴🏽",
- "description": "older man tone 3",
- "unicodeVersion": "8.0",
- "digest": "e8f881c06e41460f3e8bb37bba81f11a194b23f2354ddd7ddf3aae44faed2ad9"
- },
- "older_man_tone4": {
- "category": "people",
- "moji": "👴🏾",
- "description": "older man tone 4",
- "unicodeVersion": "8.0",
- "digest": "924a841c5d3ef0a6e3a79d519c38bdff2dff6700dbf51985e06f819f6690b35e"
- },
- "older_man_tone5": {
- "category": "people",
- "moji": "👴🏿",
- "description": "older man tone 5",
- "unicodeVersion": "8.0",
- "digest": "5f893aa727caa220c06c180099c20834abaeeeb7d7b29d64a72259ad7239e487"
- },
- "older_woman": {
- "category": "people",
- "moji": "👵",
- "description": "older woman",
- "unicodeVersion": "6.0",
- "digest": "b96d5fbaa0fe6d0d21998a617b2c07776542ee2ab1d79424e22b25a1906d42a5"
- },
- "older_woman_tone1": {
- "category": "people",
- "moji": "👵🏻",
- "description": "older woman tone 1",
- "unicodeVersion": "8.0",
- "digest": "a5a0475ecc4b452d65f04d31f8ff838531724fbd59c3e00db7c70bc0d87d850f"
- },
- "older_woman_tone2": {
- "category": "people",
- "moji": "👵🏼",
- "description": "older woman tone 2",
- "unicodeVersion": "8.0",
- "digest": "0ede94d29641f3cd592c084b1281a475db4e09a0c98ede0af8500b953e26730b"
- },
- "older_woman_tone3": {
- "category": "people",
- "moji": "👵🏽",
- "description": "older woman tone 3",
- "unicodeVersion": "8.0",
- "digest": "6a4878555493bc7f80aea33c0766d2781ef427d96c1e73d623ee74a77c3dab6e"
- },
- "older_woman_tone4": {
- "category": "people",
- "moji": "👵🏾",
- "description": "older woman tone 4",
- "unicodeVersion": "8.0",
- "digest": "cf7afc02b1d4fb584af1e224be6afa2410a39d3ae1ada553e683b6f14f50dda7"
- },
- "older_woman_tone5": {
- "category": "people",
- "moji": "👵🏿",
- "description": "older woman tone 5",
- "unicodeVersion": "8.0",
- "digest": "35aebc57848405f369eacb1defafce49ecc0f3e3833b21e53e46353b8bcdf755"
- },
- "om_symbol": {
- "category": "symbols",
- "moji": "🕉",
- "description": "om symbol",
- "unicodeVersion": "7.0",
- "digest": "8cc8000af09220f887582df7d0300da13889d371180fda773894b89ecd0053bb"
- },
- "on": {
- "category": "symbols",
- "moji": "🔛",
- "description": "on with exclamation mark with left right arrow abo",
- "unicodeVersion": "6.0",
- "digest": "2e96678d4a15fd6be2b87c4fbccbe51f4bfafcc9d04c866ce1bc640a80144730"
- },
- "oncoming_automobile": {
- "category": "travel",
- "moji": "🚘",
- "description": "oncoming automobile",
- "unicodeVersion": "6.0",
- "digest": "a51563ac9f0674e9d398a0da88c94865583f7478b222717e9187064707ec792b"
- },
- "oncoming_bus": {
- "category": "travel",
- "moji": "🚍",
- "description": "oncoming bus",
- "unicodeVersion": "6.0",
- "digest": "7fba3ec5ef62299568f19384826914c3dd1b4b7d2f27ee575a451e8a764f60a0"
- },
- "oncoming_police_car": {
- "category": "travel",
- "moji": "🚔",
- "description": "oncoming police car",
- "unicodeVersion": "6.0",
- "digest": "2c13ed1f79d19a50de28061b822b76242ec2ee979a838aa85833a3dc361a3523"
- },
- "oncoming_taxi": {
- "category": "travel",
- "moji": "🚖",
- "description": "oncoming taxi",
- "unicodeVersion": "6.0",
- "digest": "4cf0f88b9520d40772b743dcf2e618c49c5f74fa57fd46cda4add3bee9e18ef3"
- },
- "one": {
- "category": "symbols",
- "moji": "1️⃣",
- "description": "keycap digit one",
- "unicodeVersion": "3.0",
- "digest": "6e3c571545f5ec85e72b1d694adbcb764099bbcb512b99c89ac8c8b1b6e1de89"
- },
- "open_file_folder": {
- "category": "objects",
- "moji": "📂",
- "description": "open file folder",
- "unicodeVersion": "6.0",
- "digest": "ebcb375b6039c7a7f35fd36bc9240fc401ac9b4225922fc015f7b59773a89e95"
- },
- "open_hands": {
- "category": "people",
- "moji": "👐",
- "description": "open hands sign",
- "unicodeVersion": "6.0",
- "digest": "6200ecf734f542da4a9450f14d4254f2fb5a74fcae3e44a8e96dc639e1271779"
- },
- "open_hands_tone1": {
- "category": "people",
- "moji": "👐🏻",
- "description": "open hands sign tone 1",
- "unicodeVersion": "8.0",
- "digest": "d07b0f6efe5a85d1f09f94a297520496659ab93002f9c2ac6d82c73cbadab400"
- },
- "open_hands_tone2": {
- "category": "people",
- "moji": "👐🏼",
- "description": "open hands sign tone 2",
- "unicodeVersion": "8.0",
- "digest": "de7ad131fe4f9e4a20f24fe4bd22fd11a0b896c1bdf216c85045ad2a4407890e"
- },
- "open_hands_tone3": {
- "category": "people",
- "moji": "👐🏽",
- "description": "open hands sign tone 3",
- "unicodeVersion": "8.0",
- "digest": "a607b5ca058c240eb530031ec63d57bb4290fa7df42c71a63bc7511759712ae1"
- },
- "open_hands_tone4": {
- "category": "people",
- "moji": "👐🏾",
- "description": "open hands sign tone 4",
- "unicodeVersion": "8.0",
- "digest": "f101e21968a7bc2365758664eb3d121ecc019b2555e2f6d6f984257ba3237b87"
- },
- "open_hands_tone5": {
- "category": "people",
- "moji": "👐🏿",
- "description": "open hands sign tone 5",
- "unicodeVersion": "8.0",
- "digest": "908cd7f4bdfb6670358287dee92cfdabd18a802f0b7d673f07f3cbb46689e91f"
- },
- "open_mouth": {
- "category": "people",
- "moji": "😮",
- "description": "face with open mouth",
- "unicodeVersion": "6.1",
- "digest": "346f4923115965b864ef63bfc4b34deaadaa2dcbc965ef6285a1b81f9966b9a6"
- },
- "ophiuchus": {
- "category": "symbols",
- "moji": "⛎",
- "description": "ophiuchus",
- "unicodeVersion": "6.0",
- "digest": "d1b29a9339ee7bce6ff2dc8b12e424ae48f419c8bbf12fb771c0c20c3bcd76ef"
- },
- "orange_book": {
- "category": "objects",
- "moji": "📙",
- "description": "orange book",
- "unicodeVersion": "6.0",
- "digest": "b2744b07ad93a32b7f114bb8e11f0b26445ce0b2b4edf4720f14fc32da3b3cce"
- },
- "orthodox_cross": {
- "category": "symbols",
- "moji": "☦",
- "description": "orthodox cross",
- "unicodeVersion": "1.1",
- "digest": "29f438c972e101a1305c0bd5138b6b998778b33bcfd98418538340dc4126a601"
- },
- "outbox_tray": {
- "category": "objects",
- "moji": "📤",
- "description": "outbox tray",
- "unicodeVersion": "6.0",
- "digest": "9ccb7f9dfbec078899a97725772841bc3f2752352abf9fd29765a782169b2330"
- },
- "owl": {
- "category": "nature",
- "moji": "🦉",
- "description": "owl",
- "unicodeVersion": "9.0",
- "digest": "f5c1451adf6d1192cc377bc86f25604233d3c9fee42d4b4d4e8b4c4d76d0e981"
- },
- "ox": {
- "category": "nature",
- "moji": "🐂",
- "description": "ox",
- "unicodeVersion": "6.0",
- "digest": "060595f1707bd4b0182c2610e397dbb8800a2410e2205c0156043163b32b9965"
- },
- "package": {
- "category": "objects",
- "moji": "📦",
- "description": "package",
- "unicodeVersion": "6.0",
- "digest": "06ce28df9b4abdb483f7d7eccad4a388fce4ed3596635b8ff148e7399fa99d53"
- },
- "page_facing_up": {
- "category": "objects",
- "moji": "📄",
- "description": "page facing up",
- "unicodeVersion": "6.0",
- "digest": "d8aa1a6d309f70108aef98ee066419652a91fe5d793a1230f2de922ef6275f03"
- },
- "page_with_curl": {
- "category": "objects",
- "moji": "📃",
- "description": "page with curl",
- "unicodeVersion": "6.0",
- "digest": "da5752233d152e49c5fac55d89b82fbd1e98b5a6d7f2f574fe7d218879899282"
- },
- "pager": {
- "category": "objects",
- "moji": "📟",
- "description": "pager",
- "unicodeVersion": "6.0",
- "digest": "196d7a020ab2bb15c8d2ae723c8c8efe161d0e50109af3238727654956c554e5"
- },
- "paintbrush": {
- "category": "objects",
- "moji": "🖌",
- "description": "lower left paintbrush",
- "unicodeVersion": "7.0",
- "digest": "093064c1459d5b84932aab78fdc8d597e333ab2ce73524733e42c2b609b40f6b"
- },
- "palm_tree": {
- "category": "nature",
- "moji": "🌴",
- "description": "palm tree",
- "unicodeVersion": "6.0",
- "digest": "4e5f7c8c216d86839a59a559db1c6b8741d80ec8e2cb8e6c055f7ee5dcf1ceaf"
- },
- "pancakes": {
- "category": "food",
- "moji": "🥞",
- "description": "pancakes",
- "unicodeVersion": "9.0",
- "digest": "5df3886ed4e65b22054269943718a69dcb75744e2dff9e414cf9ab62a2ea0596"
- },
- "panda_face": {
- "category": "nature",
- "moji": "🐼",
- "description": "panda face",
- "unicodeVersion": "6.0",
- "digest": "8439a209f086e61dde97c5a578dc7b3a23099fd4f5a939e27535956a2c78e5c9"
- },
- "paperclip": {
- "category": "objects",
- "moji": "📎",
- "description": "paperclip",
- "unicodeVersion": "6.0",
- "digest": "dbe70e01e1d9bdb96851f18b51cc2380436ab1affe4922c1ad9f4c76811e8453"
- },
- "paperclips": {
- "category": "objects",
- "moji": "🖇",
- "description": "linked paperclips",
- "unicodeVersion": "7.0",
- "digest": "d71e54755ddaf37aa86ff2c1cd1d8dc55d2a5b806a0b9080168ea8e28332d46b"
- },
- "park": {
- "category": "travel",
- "moji": "🏞",
- "description": "national park",
- "unicodeVersion": "7.0",
- "digest": "b49486510df4e66e1beb21abdf1eb7745dfb4381a3f76b3f2ffcb2966bf5f5c4"
- },
- "parking": {
- "category": "symbols",
- "moji": "🅿",
- "description": "negative squared latin capital letter p",
- "unicodeVersion": "5.2",
- "digest": "ccc4a7c9d2d6a1f1fd0788a91618a5178ce8e926d1ebe033ab6d6f77b47b2440"
- },
- "part_alternation_mark": {
- "category": "symbols",
- "moji": "〽",
- "description": "part alternation mark",
- "unicodeVersion": "3.2",
- "digest": "2cf463fbc9bc4b1f63ec7c4c5cf9601b860c52c6f052c971c8b0c7f4cabe413d"
- },
- "partly_sunny": {
- "category": "nature",
- "moji": "⛅",
- "description": "sun behind cloud",
- "unicodeVersion": "5.2",
- "digest": "3e3e120cd4b1fc10548064d7cc529ea3c04eec3f8ac457b90628c119ba62d1f6"
- },
- "passport_control": {
- "category": "symbols",
- "moji": "🛂",
- "description": "passport control",
- "unicodeVersion": "6.0",
- "digest": "cd2221bd39e8517b14d1509afcebf3aeb138d0b286c1012544e2950671affa1a"
- },
- "pause_button": {
- "category": "symbols",
- "moji": "⏸",
- "description": "double vertical bar",
- "unicodeVersion": "7.0",
- "digest": "08bf08733cf20f2afebc56318c86ad3a0a783305dd7c3ee72d7b8898dcd39fc8"
- },
- "peace": {
- "category": "symbols",
- "moji": "☮",
- "description": "peace symbol",
- "unicodeVersion": "1.1",
- "digest": "375030a1230116742eed0b632822936e92179f2da97b73faef675140db76f4db"
- },
- "peach": {
- "category": "food",
- "moji": "🍑",
- "description": "peach",
- "unicodeVersion": "6.0",
- "digest": "952c053be4d68b53c35b0d43e83557606dc235afd821595236c8f80fb8602608"
- },
- "peanuts": {
- "category": "food",
- "moji": "🥜",
- "description": "peanuts",
- "unicodeVersion": "9.0",
- "digest": "f18fe7dfb1eb5e9db42fc3c37d4c4fb4866d9ad26ecf9f6cf5be798a09fc58eb"
- },
- "pear": {
- "category": "food",
- "moji": "🍐",
- "description": "pear",
- "unicodeVersion": "6.0",
- "digest": "73114dab6e3ec572ae1f9a98e85961605b6a98cdd19497e6957eec60941959b5"
- },
- "pen_ballpoint": {
- "category": "objects",
- "moji": "🖊",
- "description": "lower left ballpoint pen",
- "unicodeVersion": "7.0",
- "digest": "7739947fa359e641e04f51bae4ba292613fa2692b17b3f2b3c4bf27c82e97d82"
- },
- "pen_fountain": {
- "category": "objects",
- "moji": "🖋",
- "description": "lower left fountain pen",
- "unicodeVersion": "7.0",
- "digest": "114ba929b5f8812b9767e8260112ebcebd04a46175c3d5d487d71ca36c372d40"
- },
- "pencil": {
- "category": "objects",
- "moji": "📝",
- "description": "memo",
- "unicodeVersion": "6.0",
- "digest": "d18a7c355267e820ac7694c6f7da276b19a59fd42fca4eab74b7f3653e4acdd6"
- },
- "pencil2": {
- "category": "objects",
- "moji": "✏",
- "description": "pencil",
- "unicodeVersion": "1.1",
- "digest": "537f6d69a4039270d8856febea24fd3021005da6f35dca29e5491ee49cc3b217"
- },
- "penguin": {
- "category": "nature",
- "moji": "🐧",
- "description": "penguin",
- "unicodeVersion": "6.0",
- "digest": "0735168781cd9316b40fbce845a55980acc33491112aea840b26b0cef226ccc0"
- },
- "pensive": {
- "category": "people",
- "moji": "😔",
- "description": "pensive face",
- "unicodeVersion": "6.0",
- "digest": "9da78949740dfdb9c72127f5b7aef68c3b0ba1e5462f5cefcd4d22bdbc4857f9"
- },
- "performing_arts": {
- "category": "activity",
- "moji": "🎭",
- "description": "performing arts",
- "unicodeVersion": "6.0",
- "digest": "d4f55655f113bc672739e74b6e3b7684b05f49a68a8ce1a230a32295d0d7a847"
- },
- "persevere": {
- "category": "people",
- "moji": "😣",
- "description": "persevering face",
- "unicodeVersion": "6.0",
- "digest": "5297ca44798cb08da3651322d7f34d45e88333f820cfcd9cce4a540b3333ca63"
- },
- "person_frowning": {
- "category": "people",
- "moji": "🙍",
- "description": "person frowning",
- "unicodeVersion": "6.0",
- "digest": "cc38a861c7edbc5eae6bb91beb2acfc5d044453960f612c36693ef71663b1d5e"
- },
- "person_frowning_tone1": {
- "category": "people",
- "moji": "🙍🏻",
- "description": "person frowning tone 1",
- "unicodeVersion": "8.0",
- "digest": "d7350116b174ba8902292c382a410a458e86d68fd977d7e8e80ce8b47d80eb3e"
- },
- "person_frowning_tone2": {
- "category": "people",
- "moji": "🙍🏼",
- "description": "person frowning tone 2",
- "unicodeVersion": "8.0",
- "digest": "09bfa39a6b743494967806691248ab94a90bb8efc934a3e08eb13cc9a1221989"
- },
- "person_frowning_tone3": {
- "category": "people",
- "moji": "🙍🏽",
- "description": "person frowning tone 3",
- "unicodeVersion": "8.0",
- "digest": "3631d7c4d757007ca5b1543c0819b351fa70dd1fa76dc4e947bb074119837143"
- },
- "person_frowning_tone4": {
- "category": "people",
- "moji": "🙍🏾",
- "description": "person frowning tone 4",
- "unicodeVersion": "8.0",
- "digest": "852c4794d10336cb636d615d702504cf58bbdcf5a7269e9bc3e2b215074d11a0"
- },
- "person_frowning_tone5": {
- "category": "people",
- "moji": "🙍🏿",
- "description": "person frowning tone 5",
- "unicodeVersion": "8.0",
- "digest": "ac64dda090f40addeab42d79bee681060d3b43de56b461fb94883d67fb24e1c6"
- },
- "person_with_blond_hair": {
- "category": "people",
- "moji": "👱",
- "description": "person with blond hair",
- "unicodeVersion": "6.0",
- "digest": "9194bee1257b169fdcb1858023922b83a0ea10cb2f6c2bbc8824e1d15250416d"
- },
- "person_with_blond_hair_tone1": {
- "category": "people",
- "moji": "👱🏻",
- "description": "person with blond hair tone 1",
- "unicodeVersion": "8.0",
- "digest": "bbbd79e4de2b447b081cbc82426f7d2584760489576426768cdf7e1e0e43c249"
- },
- "person_with_blond_hair_tone2": {
- "category": "people",
- "moji": "👱🏼",
- "description": "person with blond hair tone 2",
- "unicodeVersion": "8.0",
- "digest": "23885c8e9d0d34eadf6941e6001c3f03863ebcef8c97733929cb0f61a03b48a9"
- },
- "person_with_blond_hair_tone3": {
- "category": "people",
- "moji": "👱🏽",
- "description": "person with blond hair tone 3",
- "unicodeVersion": "8.0",
- "digest": "87ab121eb227c13fbe454537078761484acf1f09eeae4981bb46021ccd7b71de"
- },
- "person_with_blond_hair_tone4": {
- "category": "people",
- "moji": "👱🏾",
- "description": "person with blond hair tone 4",
- "unicodeVersion": "8.0",
- "digest": "cd12e7c5a19a4a1b5445ecf98adca9e3ac4361a9b01ff8a18d13693af50bbf3a"
- },
- "person_with_blond_hair_tone5": {
- "category": "people",
- "moji": "👱🏿",
- "description": "person with blond hair tone 5",
- "unicodeVersion": "8.0",
- "digest": "d11a5989a12155e1bb3432d672aff88311afc947a65c3e1c640b9e1d42cdeb0c"
- },
- "person_with_pouting_face": {
- "category": "people",
- "moji": "🙎",
- "description": "person with pouting face",
- "unicodeVersion": "6.0",
- "digest": "687a13af899b65a14f27c205c9fffdc4ce50edc86b55f9dab05711f21a8fa06f"
- },
- "person_with_pouting_face_tone1": {
- "category": "people",
- "moji": "🙎🏻",
- "description": "person with pouting face tone1",
- "unicodeVersion": "8.0",
- "digest": "dd6d64d1d51bca83c0b3212d028394cdb84e8550e6740e3a862ce24379a522ea"
- },
- "person_with_pouting_face_tone2": {
- "category": "people",
- "moji": "🙎🏼",
- "description": "person with pouting face tone2",
- "unicodeVersion": "8.0",
- "digest": "7ef959a93142351a77e62074a5b339afa63b7019b23e3b823e8d95a5c82863f2"
- },
- "person_with_pouting_face_tone3": {
- "category": "people",
- "moji": "🙎🏽",
- "description": "person with pouting face tone3",
- "unicodeVersion": "8.0",
- "digest": "777c69c3b59b9688b5cda0cd61b67aaedf742c2758b4ea1537170f1648185dcf"
- },
- "person_with_pouting_face_tone4": {
- "category": "people",
- "moji": "🙎🏾",
- "description": "person with pouting face tone4",
- "unicodeVersion": "8.0",
- "digest": "ee918ba63a9dbb71aa7a2a19552da4a86ecd3a15ff6b01b00595b04c98a8d3f4"
- },
- "person_with_pouting_face_tone5": {
- "category": "people",
- "moji": "🙎🏿",
- "description": "person with pouting face tone5",
- "unicodeVersion": "8.0",
- "digest": "a1d4e4e89b7c8044a38a68d7cdb96d7ee576296689c996225e778af9b033dbfa"
- },
- "pick": {
- "category": "objects",
- "moji": "⛏",
- "description": "pick",
- "unicodeVersion": "5.2",
- "digest": "c74411a556653f5b4b0825627089ab96c01ecdd8d472697c961c22c8a99a6265"
- },
- "pig": {
- "category": "nature",
- "moji": "🐷",
- "description": "pig face",
- "unicodeVersion": "6.0",
- "digest": "7ce19e18b6fb941b8075e18f8c680048828c8b129d1a74a9ac3c2ae937a02aa5"
- },
- "pig2": {
- "category": "nature",
- "moji": "🐖",
- "description": "pig",
- "unicodeVersion": "6.0",
- "digest": "ba4acbc207d817071d485e33186f5ddc20d470a0dc468fb1fea502d4768c25d7"
- },
- "pig_nose": {
- "category": "nature",
- "moji": "🐽",
- "description": "pig nose",
- "unicodeVersion": "6.0",
- "digest": "f9fb90cd7919ed06aec8bf6b0cda13fcd6a58b25329b48a57714d0909a290e4b"
- },
- "pill": {
- "category": "objects",
- "moji": "💊",
- "description": "pill",
- "unicodeVersion": "6.0",
- "digest": "dea2366892fb3739ba549da27d7d0f75871d073099ae61a1d0e1c7683bd274d5"
- },
- "pineapple": {
- "category": "food",
- "moji": "🍍",
- "description": "pineapple",
- "unicodeVersion": "6.0",
- "digest": "62e7ffdfac3638f71d1381861a87d6f361718912099ddd4df1bc8245d6a1fc2f"
- },
- "ping_pong": {
- "category": "activity",
- "moji": "🏓",
- "description": "table tennis paddle and ball",
- "unicodeVersion": "8.0",
- "digest": "f55f8483698dd2252aa5e2819f98e2f5d753616319446067de16acc5ffe12129"
- },
- "pisces": {
- "category": "symbols",
- "moji": "♓",
- "description": "pisces",
- "unicodeVersion": "1.1",
- "digest": "2b9982d42db6ef3b280174b79faa2ce0178f349b826474883297fbbea088b2ea"
- },
- "pizza": {
- "category": "food",
- "moji": "🍕",
- "description": "slice of pizza",
- "unicodeVersion": "6.0",
- "digest": "74f0ca139931c470d814ed91f1bc7d395205b3d836fa2f425c50dff56d77ccb9"
- },
- "place_of_worship": {
- "category": "symbols",
- "moji": "🛐",
- "description": "place of worship",
- "unicodeVersion": "8.0",
- "digest": "39562f00c92f6a75028f57faf3f951026866f42bca6e51bcbbf30f442e24c63a"
- },
- "play_pause": {
- "category": "symbols",
- "moji": "⏯",
- "description": "black right-pointing double triangle with double vertical bar",
- "unicodeVersion": "6.0",
- "digest": "a4fe3a6a5928f88e77c643e7869e4edade8cd19839ec7e347265439483463019"
- },
- "point_down": {
- "category": "people",
- "moji": "👇",
- "description": "white down pointing backhand index",
- "unicodeVersion": "6.0",
- "digest": "4fa9f01922409ef7831ad0ab78782174dd59c46e74e9a76c38111fcdbc1666a1"
- },
- "point_down_tone1": {
- "category": "people",
- "moji": "👇🏻",
- "description": "white down pointing backhand index tone 1",
- "unicodeVersion": "8.0",
- "digest": "e64f86886b78db3088952713caef3144f99ba1d8b5f730e640c4ae42762fb645"
- },
- "point_down_tone2": {
- "category": "people",
- "moji": "👇🏼",
- "description": "white down pointing backhand index tone 2",
- "unicodeVersion": "8.0",
- "digest": "7d44c5c286b83afdc301c8a39bda0e1619d8a099cc576cbcebf55af15931a55e"
- },
- "point_down_tone3": {
- "category": "people",
- "moji": "👇🏽",
- "description": "white down pointing backhand index tone 3",
- "unicodeVersion": "8.0",
- "digest": "33d981c6cd9641f2864b807bc2e288798bbd56bbd413e1aa4d23d42bb6e1af74"
- },
- "point_down_tone4": {
- "category": "people",
- "moji": "👇🏾",
- "description": "white down pointing backhand index tone 4",
- "unicodeVersion": "8.0",
- "digest": "19dc8c01dd571e65f0314cc18b55bde4038617b782808faae976cffb0fadfce4"
- },
- "point_down_tone5": {
- "category": "people",
- "moji": "👇🏿",
- "description": "white down pointing backhand index tone 5",
- "unicodeVersion": "8.0",
- "digest": "67b1413272b5a0c8efbbd5461bd6fd89849ac5f8dc0d1dac26e2edfcc6b9d1ea"
- },
- "point_left": {
- "category": "people",
- "moji": "👈",
- "description": "white left pointing backhand index",
- "unicodeVersion": "6.0",
- "digest": "b7f186ed45ddd21e5c62cbc3d5040194ae1282caad89508fa628c248b3edf0bd"
- },
- "point_left_tone1": {
- "category": "people",
- "moji": "👈🏻",
- "description": "white left pointing backhand index tone 1",
- "unicodeVersion": "8.0",
- "digest": "439f771d094340a43f83b8f2e05b39d90b2ec3921756fea2878754119aafd683"
- },
- "point_left_tone2": {
- "category": "people",
- "moji": "👈🏼",
- "description": "white left pointing backhand index tone 2",
- "unicodeVersion": "8.0",
- "digest": "73d6b8d0df34df349653fcfdbcc2adbc98ba6c25f612d933cc52da1154af37f2"
- },
- "point_left_tone3": {
- "category": "people",
- "moji": "👈🏽",
- "description": "white left pointing backhand index tone 3",
- "unicodeVersion": "8.0",
- "digest": "1cfd0d1db8a06ead619a12bd248c4c9fda7b6b50309a2526a20bfc66dcb86177"
- },
- "point_left_tone4": {
- "category": "people",
- "moji": "👈🏾",
- "description": "white left pointing backhand index tone 4",
- "unicodeVersion": "8.0",
- "digest": "26b4755890f8e290e7d8958566612c14caa6522a7bd43a3d7e8c206436fad70b"
- },
- "point_left_tone5": {
- "category": "people",
- "moji": "👈🏿",
- "description": "white left pointing backhand index tone 5",
- "unicodeVersion": "8.0",
- "digest": "8ca52d65aaedcf7ca346e90da3f53b50af83dfd544ed8631b4da1ab31e5e1497"
- },
- "point_right": {
- "category": "people",
- "moji": "👉",
- "description": "white right pointing backhand index",
- "unicodeVersion": "6.0",
- "digest": "2d27c0ffba78c5c5fbd502dd42fbbae220c8619c8a2b964bef03e4a84ad4a1b6"
- },
- "point_right_tone1": {
- "category": "people",
- "moji": "👉🏻",
- "description": "white right pointing backhand index tone 1",
- "unicodeVersion": "8.0",
- "digest": "c2f84a57648a6de49b2b9000afd2c45ce1ef8f303d71f248b292e3fe83b8cf94"
- },
- "point_right_tone2": {
- "category": "people",
- "moji": "👉🏼",
- "description": "white right pointing backhand index tone 2",
- "unicodeVersion": "8.0",
- "digest": "0a1aae46415401cd3557f61ce491e975acaa995b6c2bf487977beb91e4b1bc7e"
- },
- "point_right_tone3": {
- "category": "people",
- "moji": "👉🏽",
- "description": "white right pointing backhand index tone 3",
- "unicodeVersion": "8.0",
- "digest": "86c13a18c53b548907ec0ec468922beab040c29a365f55e18f265e79f1bb42bf"
- },
- "point_right_tone4": {
- "category": "people",
- "moji": "👉🏾",
- "description": "white right pointing backhand index tone 4",
- "unicodeVersion": "8.0",
- "digest": "1605160b761b975c0f11490eb1a7b724c674ec371d72e73f824fdbe873aeddb2"
- },
- "point_right_tone5": {
- "category": "people",
- "moji": "👉🏿",
- "description": "white right pointing backhand index tone 5",
- "unicodeVersion": "8.0",
- "digest": "1cfee9fdcdaa1a790c14a4f8436dad4b3b6677860bf60dd1da3985fc7cb25a00"
- },
- "point_up": {
- "category": "people",
- "moji": "☝",
- "description": "white up pointing index",
- "unicodeVersion": "1.1",
- "digest": "dc20d84a1a808e2d207f10f2f292cb78e05b9e67b4a26f7491e0c6c7f8059af5"
- },
- "point_up_2": {
- "category": "people",
- "moji": "👆",
- "description": "white up pointing backhand index",
- "unicodeVersion": "6.0",
- "digest": "0e5a1d7841d0f54762d0fadf460086cad4fcd05fbf65cabdf1df90d1ab0c3f2b"
- },
- "point_up_2_tone1": {
- "category": "people",
- "moji": "👆🏻",
- "description": "white up pointing backhand index tone 1",
- "unicodeVersion": "8.0",
- "digest": "bdcd26a212498dcddc69342caefe4a68d2b4bfbebcfa94045a1c27dcce158311"
- },
- "point_up_2_tone2": {
- "category": "people",
- "moji": "👆🏼",
- "description": "white up pointing backhand index tone 2",
- "unicodeVersion": "8.0",
- "digest": "fd5a6d912f3533b3356392e68df8b155dcacd3bb2d2e1c44d84807d587ef1ed5"
- },
- "point_up_2_tone3": {
- "category": "people",
- "moji": "👆🏽",
- "description": "white up pointing backhand index tone 3",
- "unicodeVersion": "8.0",
- "digest": "57af38773077d28200e033dc3dd28913f570311a51b833df32f23a85bfcc530c"
- },
- "point_up_2_tone4": {
- "category": "people",
- "moji": "👆🏾",
- "description": "white up pointing backhand index tone 4",
- "unicodeVersion": "8.0",
- "digest": "7110f1d42dffcab536906e176baa36e817142f9d71329fdfc1b74ee9813cffd6"
- },
- "point_up_2_tone5": {
- "category": "people",
- "moji": "👆🏿",
- "description": "white up pointing backhand index tone 5",
- "unicodeVersion": "8.0",
- "digest": "1795ede377cdd58189471af3f6488d8197f2a742f817f8a61523ccff8d08581b"
- },
- "point_up_tone1": {
- "category": "people",
- "moji": "☝🏻",
- "description": "white up pointing index tone 1",
- "unicodeVersion": "8.0",
- "digest": "16f0e85643558fd2f471cc8d317058914f42279f4aef2ba0e8390728efb4992b"
- },
- "point_up_tone2": {
- "category": "people",
- "moji": "☝🏼",
- "description": "white up pointing index tone 2",
- "unicodeVersion": "8.0",
- "digest": "fe8f930134adc4be29b7e659c6acbfa76ba52ab3d0b46dd4797e79c365708666"
- },
- "point_up_tone3": {
- "category": "people",
- "moji": "☝🏽",
- "description": "white up pointing index tone 3",
- "unicodeVersion": "8.0",
- "digest": "d7258aeab80e697649a0e8ad13445380462bb5814c90b1183cc105ebe3eaa5ef"
- },
- "point_up_tone4": {
- "category": "people",
- "moji": "☝🏾",
- "description": "white up pointing index tone 4",
- "unicodeVersion": "8.0",
- "digest": "4c4aca5e2e436421b26d5d58a82bd52fdb9135593fb1afd92c30fa8f1ed51dd1"
- },
- "point_up_tone5": {
- "category": "people",
- "moji": "☝🏿",
- "description": "white up pointing index tone 5",
- "unicodeVersion": "8.0",
- "digest": "ab94fd7fe02205894add98c4c97d812f2228c7c766b1b0e01fa1e9e5dc7c669b"
- },
- "police_car": {
- "category": "travel",
- "moji": "🚓",
- "description": "police car",
- "unicodeVersion": "6.0",
- "digest": "38e3b0bdee599b71a5f556dc86b12aa01df38dcf533179515bbdbf2512828548"
- },
- "poodle": {
- "category": "nature",
- "moji": "🐩",
- "description": "poodle",
- "unicodeVersion": "6.0",
- "digest": "4e73855cbdb10f644146fb3bc1636a74faff02a5d69ad0f91302e920973b03f0"
- },
- "poop": {
- "category": "people",
- "moji": "💩",
- "description": "pile of poo",
- "unicodeVersion": "6.0",
- "digest": "b5c6a197435c518508edf1cc7bc015c14c120965b574813838797507fab21994"
- },
- "popcorn": {
- "category": "food",
- "moji": "🍿",
- "description": "popcorn",
- "unicodeVersion": "8.0",
- "digest": "6571fce7fb3cdf92db6fdb69b89b21b664eab348f7fd9ccb37da09eefa65d14f"
- },
- "post_office": {
- "category": "travel",
- "moji": "🏣",
- "description": "japanese post office",
- "unicodeVersion": "6.0",
- "digest": "7dc4ef6e09bab68f31dfb7f594a6488b62c9dcb956b36ac2788c1fbefa908251"
- },
- "postal_horn": {
- "category": "objects",
- "moji": "📯",
- "description": "postal horn",
- "unicodeVersion": "6.0",
- "digest": "0cee35210e86afe9bdf0e60345d0c8c23c25d0b43e74bfd4bb8f76e223ffdfe3"
- },
- "postbox": {
- "category": "objects",
- "moji": "📮",
- "description": "postbox",
- "unicodeVersion": "6.0",
- "digest": "3e12a4b2a7bce5c1257ecd05f69ba6f51d9db39b21f6393c304e6870c5f432da"
- },
- "potable_water": {
- "category": "symbols",
- "moji": "🚰",
- "description": "potable water symbol",
- "unicodeVersion": "6.0",
- "digest": "e35ad512f4da69fa132475c68cdbb831ab617f013127aba15cd7508f0b732d19"
- },
- "potato": {
- "category": "food",
- "moji": "🥔",
- "description": "potato",
- "unicodeVersion": "9.0",
- "digest": "72e5b1d7dd118dd85886d880a88a92b60e1542587d92591dd42f2d59a721b4c4"
- },
- "pouch": {
- "category": "people",
- "moji": "👝",
- "description": "pouch",
- "unicodeVersion": "6.0",
- "digest": "e333815505f48dad6240f0d6ac9476ab256b26ec53108c0454f77c6ac31c4874"
- },
- "poultry_leg": {
- "category": "food",
- "moji": "🍗",
- "description": "poultry leg",
- "unicodeVersion": "6.0",
- "digest": "c3c667f9795aa2d14fd1a500438d7fd2441e924a3d1af68bd6be1f082b935f4c"
- },
- "pound": {
- "category": "objects",
- "moji": "💷",
- "description": "banknote with pound sign",
- "unicodeVersion": "6.0",
- "digest": "2cb892a8131edb282cb8f5dadaa8db602e6b6934fdf06588d8ccb8f3e7e6eae6"
- },
- "pouting_cat": {
- "category": "people",
- "moji": "😾",
- "description": "pouting cat face",
- "unicodeVersion": "6.0",
- "digest": "e253bae99f9859322bc02b5e9b87cb33c68a5e38aebaa57478d5f4a5b1c23bb0"
- },
- "pray": {
- "category": "people",
- "moji": "🙏",
- "description": "person with folded hands",
- "unicodeVersion": "6.0",
- "digest": "175a97bcdf0110e4a435cc760d5203b8493c5bbf6d4ce70fb7a5643b2d1021dc"
- },
- "pray_tone1": {
- "category": "people",
- "moji": "🙏🏻",
- "description": "person with folded hands tone 1",
- "unicodeVersion": "8.0",
- "digest": "0a453f3e7292c7d813da211b9d1359c6d893426077b010bca5a4bc3f17a695c1"
- },
- "pray_tone2": {
- "category": "people",
- "moji": "🙏🏼",
- "description": "person with folded hands tone 2",
- "unicodeVersion": "8.0",
- "digest": "f66640530c3818fff333ebd6636dfb912aa6e986060ad03555766bbc5888d8b4"
- },
- "pray_tone3": {
- "category": "people",
- "moji": "🙏🏽",
- "description": "person with folded hands tone 3",
- "unicodeVersion": "8.0",
- "digest": "5d8f49ada7ce2e5c473220aa881b32cda4fe73c024ec9260fd2af09b55478239"
- },
- "pray_tone4": {
- "category": "people",
- "moji": "🙏🏾",
- "description": "person with folded hands tone 4",
- "unicodeVersion": "8.0",
- "digest": "c830c04f893a424a6694c478d8356a549a43b2ba3e9e912becc018178ca0c54c"
- },
- "pray_tone5": {
- "category": "people",
- "moji": "🙏🏿",
- "description": "person with folded hands tone 5",
- "unicodeVersion": "8.0",
- "digest": "3fc8c39440ac0ae1646949dad41cc56305e51fbd299378de5356be3e5b4bda24"
- },
- "prayer_beads": {
- "category": "objects",
- "moji": "📿",
- "description": "prayer beads",
- "unicodeVersion": "8.0",
- "digest": "aedb143e4798a14b97ff6595293d7ed873988023efbc1e3de3aa9f17344359fd"
- },
- "pregnant_woman": {
- "category": "people",
- "moji": "🤰",
- "description": "pregnant woman",
- "unicodeVersion": "9.0",
- "digest": "541e4d6245b5b243121e4666298cd5ae5a31fd38228ca2f527865019d7fa25b5"
- },
- "pregnant_woman_tone1": {
- "category": "people",
- "moji": "🤰🏻",
- "description": "pregnant woman tone 1",
- "unicodeVersion": "9.0",
- "digest": "8c16b193ce1c39aafcc4ba1b7db7524486cfbd640ece996bd795e91198db6196"
- },
- "pregnant_woman_tone2": {
- "category": "people",
- "moji": "🤰🏼",
- "description": "pregnant woman tone 2",
- "unicodeVersion": "9.0",
- "digest": "44261300c22052fdc4f82cb41d63754b5bd9d5bc6dac92ac68440a62f3456b87"
- },
- "pregnant_woman_tone3": {
- "category": "people",
- "moji": "🤰🏽",
- "description": "pregnant woman tone 3",
- "unicodeVersion": "9.0",
- "digest": "f734c133c3d9dc59aeb050024c420e2de231e2fabefda255169666ee955ab8e4"
- },
- "pregnant_woman_tone4": {
- "category": "people",
- "moji": "🤰🏾",
- "description": "pregnant woman tone 4",
- "unicodeVersion": "9.0",
- "digest": "6990dabfc19d92c061fdffd82ab99f582378fd84c535db0dc042f20cb9db6c16"
- },
- "pregnant_woman_tone5": {
- "category": "people",
- "moji": "🤰🏿",
- "description": "pregnant woman tone 5",
- "unicodeVersion": "9.0",
- "digest": "6981c98ac30d7ff3dc5b44279a3d3a8891a1fd33de5a41050aced7e37ec9ea21"
- },
- "prince": {
- "category": "people",
- "moji": "🤴",
- "description": "prince",
- "unicodeVersion": "9.0",
- "digest": "07fcbaf5d14e5e77d1a44cae21006ab245da1fd799773aa836a9bb2fdac24f61"
- },
- "prince_tone1": {
- "category": "people",
- "moji": "🤴🏻",
- "description": "prince tone 1",
- "unicodeVersion": "9.0",
- "digest": "2db61fd0e8bb7d9d5941df7c680d2e4fff6b282d2a356c34bf32e55bf41d1245"
- },
- "prince_tone2": {
- "category": "people",
- "moji": "🤴🏼",
- "description": "prince tone 2",
- "unicodeVersion": "9.0",
- "digest": "283dbc3550fe80ce1bf4f6eabd7e216026f3996f4e0fa9b9b881df8d741c26d3"
- },
- "prince_tone3": {
- "category": "people",
- "moji": "🤴🏽",
- "description": "prince tone 3",
- "unicodeVersion": "9.0",
- "digest": "374f58411517a2c3532adad34108db23e8587a86d15e32aa0e67e8a82d1c2ef3"
- },
- "prince_tone4": {
- "category": "people",
- "moji": "🤴🏾",
- "description": "prince tone 4",
- "unicodeVersion": "9.0",
- "digest": "a6d44eb1caf4dc0fbb1b5821809cd55bea127f5a6b4b4b7e259f161ef502a52b"
- },
- "prince_tone5": {
- "category": "people",
- "moji": "🤴🏿",
- "description": "prince tone 5",
- "unicodeVersion": "9.0",
- "digest": "72bbb711de954db8d01bf0270ae23ead3fabc76a1e9edc208b2ee08b3c282c17"
- },
- "princess": {
- "category": "people",
- "moji": "👸",
- "description": "princess",
- "unicodeVersion": "6.0",
- "digest": "0255742ad246f6245302c68965ce7243618c62e27a55927a1101db0a58144205"
- },
- "princess_tone1": {
- "category": "people",
- "moji": "👸🏻",
- "description": "princess tone 1",
- "unicodeVersion": "8.0",
- "digest": "6634c586ce395cc2417acfffc4997bd9152b200abb357921f9bcb37d5501c99b"
- },
- "princess_tone2": {
- "category": "people",
- "moji": "👸🏼",
- "description": "princess tone 2",
- "unicodeVersion": "8.0",
- "digest": "9ef16a00fa3f044559e9a8f5abbd0d8be7c6d665f8007c59094e2581d92236d1"
- },
- "princess_tone3": {
- "category": "people",
- "moji": "👸🏽",
- "description": "princess tone 3",
- "unicodeVersion": "8.0",
- "digest": "0a6687e1da8a427e55fc2769f8927fa3f285b9e3dfc4d0a89de2339f9c7ad5d6"
- },
- "princess_tone4": {
- "category": "people",
- "moji": "👸🏾",
- "description": "princess tone 4",
- "unicodeVersion": "8.0",
- "digest": "687368a236801772418314f1ca4271d97511a3c5875a22886abc21e2383d784d"
- },
- "princess_tone5": {
- "category": "people",
- "moji": "👸🏿",
- "description": "princess tone 5",
- "unicodeVersion": "8.0",
- "digest": "cd1a5425ebe5bd0334ae35e3fcdbe50fffcceac150a86b842b92bbb78bbb0e3b"
- },
- "printer": {
- "category": "objects",
- "moji": "🖨",
- "description": "printer",
- "unicodeVersion": "7.0",
- "digest": "6d955ef121cac0fb8f24e617c664e13b44cb318db041be39ebbbd158e731ae2f"
- },
- "projector": {
- "category": "objects",
- "moji": "📽",
- "description": "film projector",
- "unicodeVersion": "7.0",
- "digest": "ad03ae86f9188ccc2909335de034ed0de68dadc4a6e59fb678e752d219c41f92"
- },
- "punch": {
- "category": "people",
- "moji": "👊",
- "description": "fisted hand sign",
- "unicodeVersion": "6.0",
- "digest": "1f8bdf4ead54a6d9ccad648c98f126a49e4a16a1d525abf8d8194b2e253461e2"
- },
- "punch_tone1": {
- "category": "people",
- "moji": "👊🏻",
- "description": "fisted hand sign tone 1",
- "unicodeVersion": "8.0",
- "digest": "6e368dfc6f762bc1db1127592d91fa29437f182391a71d61472e8459b2dfc770"
- },
- "punch_tone2": {
- "category": "people",
- "moji": "👊🏼",
- "description": "fisted hand sign tone 2",
- "unicodeVersion": "8.0",
- "digest": "62e40f8ac33d5f28cc2bba06dc0871627db3f40a04f16a7dd2ab426bb9c740a4"
- },
- "punch_tone3": {
- "category": "people",
- "moji": "👊🏽",
- "description": "fisted hand sign tone 3",
- "unicodeVersion": "8.0",
- "digest": "94a9d410d1ac2252be7386a2110708686ea2bba4859f3fd84a11e64bbd6a1432"
- },
- "punch_tone4": {
- "category": "people",
- "moji": "👊🏾",
- "description": "fisted hand sign tone 4",
- "unicodeVersion": "8.0",
- "digest": "b378775978c26547d87fe8d2f7d20a05c2bbca386d40533fe774e4bca2e7540a"
- },
- "punch_tone5": {
- "category": "people",
- "moji": "👊🏿",
- "description": "fisted hand sign tone 5",
- "unicodeVersion": "8.0",
- "digest": "4300760b118dee45c53098601e7d8f9d3ec66e87f2d11e87b07adb79793c53e8"
- },
- "purple_heart": {
- "category": "symbols",
- "moji": "💜",
- "description": "purple heart",
- "unicodeVersion": "6.0",
- "digest": "68bc43f94a83b183d3ae134cfb36ef801dbb08f8ed46ed2972caa24cad3c8d2c"
- },
- "purse": {
- "category": "people",
- "moji": "👛",
- "description": "purse",
- "unicodeVersion": "6.0",
- "digest": "069d6ce3046648578d3c04796bb6190ecfb55e1fd708be2021144f1716c3ae2f"
- },
- "pushpin": {
- "category": "objects",
- "moji": "📌",
- "description": "pushpin",
- "unicodeVersion": "6.0",
- "digest": "c8b820588fe733d505e95d93eeadcc3e1073f745b2b53bae8dcc1c0bb8624aa7"
- },
- "put_litter_in_its_place": {
- "category": "symbols",
- "moji": "🚮",
- "description": "put litter in its place symbol",
- "unicodeVersion": "6.0",
- "digest": "8f4d77e14f81634081b21e9bc062082d5a18cad551560eb8479c0c5d14ab3358"
- },
- "question": {
- "category": "symbols",
- "moji": "❓",
- "description": "black question mark ornament",
- "unicodeVersion": "6.0",
- "digest": "fd18ff641d27854c3996ec9c58210fde5411537847c0f3c55d9318244c3e8854"
- },
- "rabbit": {
- "category": "nature",
- "moji": "🐰",
- "description": "rabbit face",
- "unicodeVersion": "6.0",
- "digest": "1a7201ef67f5ce4c9deb0f5c9e1e14d0e3d6ffaac17bbb73d6ca6c1de62c43ea"
- },
- "rabbit2": {
- "category": "nature",
- "moji": "🐇",
- "description": "rabbit",
- "unicodeVersion": "6.0",
- "digest": "120075f5ca435b5dc903bfe3934bbc9adb2c17c16a8b839625320a46c299827a"
- },
- "race_car": {
- "category": "travel",
- "moji": "🏎",
- "description": "racing car",
- "unicodeVersion": "7.0",
- "digest": "183bf5e7abb9e7c7e94a08f212040d6dbe24633c2fb25a452096a2062c18acc6"
- },
- "racehorse": {
- "category": "nature",
- "moji": "🐎",
- "description": "horse",
- "unicodeVersion": "6.0",
- "digest": "44ade212bafa562416e9df439229052db2e45cf631d6d32a8261aa541a675a07"
- },
- "radio": {
- "category": "objects",
- "moji": "📻",
- "description": "radio",
- "unicodeVersion": "6.0",
- "digest": "a397a47cb1ab6621628112afcdc7ce4338f9e77954f2ef7e9bb461d519705099"
- },
- "radio_button": {
- "category": "symbols",
- "moji": "🔘",
- "description": "radio button",
- "unicodeVersion": "6.0",
- "digest": "7e74da4889650286464a22035c3297d914c1e52c266d5accbffe03d8f76069de"
- },
- "radioactive": {
- "category": "symbols",
- "moji": "☢",
- "description": "radioactive sign",
- "unicodeVersion": "1.1",
- "digest": "adae20f9d65f9e4c3d2ff97012c58475ed56d446010999b75af2ddc78961cf54"
- },
- "rage": {
- "category": "people",
- "moji": "😡",
- "description": "pouting face",
- "unicodeVersion": "6.0",
- "digest": "55c5a1450a9c4ba539c4c2f6760209c7cedfbbc93abee597d63703e7bc96743c"
- },
- "railway_car": {
- "category": "travel",
- "moji": "🚃",
- "description": "railway car",
- "unicodeVersion": "6.0",
- "digest": "5aaa2731f58c2a08c2c731cf361da3a9ea407d2754134dcacf14d9ea4cfb702f"
- },
- "railway_track": {
- "category": "travel",
- "moji": "🛤",
- "description": "railway track",
- "unicodeVersion": "7.0",
- "digest": "2a36d0f6b16ecb70328a34502c639ce931fcb558c72fad0118b7380a13de2076"
- },
- "rainbow": {
- "category": "travel",
- "moji": "🌈",
- "description": "rainbow",
- "unicodeVersion": "6.0",
- "digest": "f3e13a1ac2f7a2ae4096f47a2e1a1afd18f2998a67f6491002c3bcb019093222"
- },
- "raised_back_of_hand": {
- "category": "people",
- "moji": "🤚",
- "description": "raised back of hand",
- "unicodeVersion": "9.0",
- "digest": "3335f2a4f8ac26c22418968e1836e697ee03af488a9349bab6795076ab5dd771"
- },
- "raised_back_of_hand_tone1": {
- "category": "people",
- "moji": "🤚🏻",
- "description": "raised back of hand tone 1",
- "unicodeVersion": "9.0",
- "digest": "6388f3e4b61cc32967aa6b9bceb60e8673f01cb1cdf94c37332d61040be48d6b"
- },
- "raised_back_of_hand_tone2": {
- "category": "people",
- "moji": "🤚🏼",
- "description": "raised back of hand tone 2",
- "unicodeVersion": "9.0",
- "digest": "66491196ad238a2a12c14d9be5cec12d22788d719725e7d22e6edad40afc0c8e"
- },
- "raised_back_of_hand_tone3": {
- "category": "people",
- "moji": "🤚🏽",
- "description": "raised back of hand tone 3",
- "unicodeVersion": "9.0",
- "digest": "27e2f03168b0733a0492a30df1f2f924783bf309a3a05a22a27529953783d0cd"
- },
- "raised_back_of_hand_tone4": {
- "category": "people",
- "moji": "🤚🏾",
- "description": "raised back of hand tone 4",
- "unicodeVersion": "9.0",
- "digest": "729c1c34a2aa7c236d11c73665fa7b29fa1c31cc3ce56ea0a7e46f0754483efd"
- },
- "raised_back_of_hand_tone5": {
- "category": "people",
- "moji": "🤚🏿",
- "description": "raised back of hand tone 5",
- "unicodeVersion": "9.0",
- "digest": "50cca64dcbf0dff9a896668c6d909bd91805d699873224933e91af524c781320"
- },
- "raised_hand": {
- "category": "people",
- "moji": "✋",
- "description": "raised hand",
- "unicodeVersion": "6.0",
- "digest": "0bfd815713f428f4408c4225abd10c73e1200dbabe07216cb5d07098f8314270"
- },
- "raised_hand_tone1": {
- "category": "people",
- "moji": "✋🏻",
- "description": "raised hand tone 1",
- "unicodeVersion": "8.0",
- "digest": "5d8a093e609223ce89bb3813d1b673d8558b81597ec747002a5e30792e1bcd72"
- },
- "raised_hand_tone2": {
- "category": "people",
- "moji": "✋🏼",
- "description": "raised hand tone 2",
- "unicodeVersion": "8.0",
- "digest": "7b76fb17f3da3719ee18ca00903b0b89b4fad7718aa18df52c7920d0ae049fb2"
- },
- "raised_hand_tone3": {
- "category": "people",
- "moji": "✋🏽",
- "description": "raised hand tone 3",
- "unicodeVersion": "8.0",
- "digest": "44bce7c38e3b814d00fee161df4cdf94b2c73f5e044b65317010588029aae4be"
- },
- "raised_hand_tone4": {
- "category": "people",
- "moji": "✋🏾",
- "description": "raised hand tone 4",
- "unicodeVersion": "8.0",
- "digest": "b23fad6235d1e3dfbbf7c613013cf294b9c36c6d1f2228fd97fb4802aa3ef0af"
- },
- "raised_hand_tone5": {
- "category": "people",
- "moji": "✋🏿",
- "description": "raised hand tone 5",
- "unicodeVersion": "8.0",
- "digest": "a66a0fc82b6d8abd282f5c7f7e35cc31a3f83dd425f4621a3538a2455113b02a"
- },
- "raised_hands": {
- "category": "people",
- "moji": "🙌",
- "description": "person raising both hands in celebration",
- "unicodeVersion": "6.0",
- "digest": "6bc3e746c276ce1ea46ba5233d30ec5cdb8340b5c9c15768873404d59b1f3496"
- },
- "raised_hands_tone1": {
- "category": "people",
- "moji": "🙌🏻",
- "description": "person raising both hands in celebration tone 1",
- "unicodeVersion": "8.0",
- "digest": "c3e5095f41e49954c688edefeb223069c57979052735b06277d7f3620161796f"
- },
- "raised_hands_tone2": {
- "category": "people",
- "moji": "🙌🏼",
- "description": "person raising both hands in celebration tone 2",
- "unicodeVersion": "8.0",
- "digest": "4bd2f620dba790a58a42ab6d234de14772e4224994b929d731acc50ed6a8d259"
- },
- "raised_hands_tone3": {
- "category": "people",
- "moji": "🙌🏽",
- "description": "person raising both hands in celebration tone 3",
- "unicodeVersion": "8.0",
- "digest": "1db8bfc21ab03d98849684f6d32e8e333bb9b33dd540a38c4673a33ea78698d8"
- },
- "raised_hands_tone4": {
- "category": "people",
- "moji": "🙌🏾",
- "description": "person raising both hands in celebration tone 4",
- "unicodeVersion": "8.0",
- "digest": "65991dd419d1a02f126dfc401252faa929eb9b437d990b7802a2323ce6e929cb"
- },
- "raised_hands_tone5": {
- "category": "people",
- "moji": "🙌🏿",
- "description": "person raising both hands in celebration tone 5",
- "unicodeVersion": "8.0",
- "digest": "ffe251cc1a777836f40217630cb6652d3ec7f4f80c88045d1ec1430c8ce368d1"
- },
- "raising_hand": {
- "category": "people",
- "moji": "🙋",
- "description": "happy person raising one hand",
- "unicodeVersion": "6.0",
- "digest": "b9cc5deb1d33ace5ab80f9b4fe7d2d277c2ee04dfdbc8bc3d41ca69edd3d5c52"
- },
- "raising_hand_tone1": {
- "category": "people",
- "moji": "🙋🏻",
- "description": "happy person raising one hand tone1",
- "unicodeVersion": "8.0",
- "digest": "6d7aea2c2c9620448ffc6be9e5bf5cddcc7a1e18f9f816a030dc818d93d45d61"
- },
- "raising_hand_tone2": {
- "category": "people",
- "moji": "🙋🏼",
- "description": "happy person raising one hand tone2",
- "unicodeVersion": "8.0",
- "digest": "8af44fa3d2d7eadc95c8193993eeeaea3bd764c1ac884d897804a074bba87a21"
- },
- "raising_hand_tone3": {
- "category": "people",
- "moji": "🙋🏽",
- "description": "happy person raising one hand tone3",
- "unicodeVersion": "8.0",
- "digest": "cc39cadc42f9ec7fba6e74661e757f0d7a36fd10807fe90fddd511a07e290059"
- },
- "raising_hand_tone4": {
- "category": "people",
- "moji": "🙋🏾",
- "description": "happy person raising one hand tone4",
- "unicodeVersion": "8.0",
- "digest": "7ef0c37921ced3c64898531b746eaf1e6ed7133ebefd74394e5b0164ee57085d"
- },
- "raising_hand_tone5": {
- "category": "people",
- "moji": "🙋🏿",
- "description": "happy person raising one hand tone5",
- "unicodeVersion": "8.0",
- "digest": "96927ec26049ce6efc65917605f9039f9ec746eff23fe00c4f35ceb29d4f2c93"
- },
- "ram": {
- "category": "nature",
- "moji": "🐏",
- "description": "ram",
- "unicodeVersion": "6.0",
- "digest": "8f4a19d64e01593a7487fb4d58fe1d300055e458d3daa1659ec59877a8a3b00f"
- },
- "ramen": {
- "category": "food",
- "moji": "🍜",
- "description": "steaming bowl",
- "unicodeVersion": "6.0",
- "digest": "2dab0bf5560aacda31e87d8a86a1a39eaaa7f6b4fb3dbc1b9768f3a49a9e20f9"
- },
- "rat": {
- "category": "nature",
- "moji": "🐀",
- "description": "rat",
- "unicodeVersion": "6.0",
- "digest": "7f15ecd2a5c5dd340a5e1c69454a512bf176cbb74b042efc65a7d6c8efad5e6e"
- },
- "record_button": {
- "category": "symbols",
- "moji": "⏺",
- "description": "black circle for record",
- "unicodeVersion": "7.0",
- "digest": "2b7ef01bcbfb5310cceffada4a966b874dd3bf4b8cfa23848effac1386457fc3"
- },
- "recycle": {
- "category": "symbols",
- "moji": "♻",
- "description": "black universal recycling symbol",
- "unicodeVersion": "3.2",
- "digest": "9256cf44c41b5b3479c92a453c26adcc32800af9ffbf32f2d08e7570732788b2"
- },
- "red_car": {
- "category": "travel",
- "moji": "🚗",
- "description": "automobile",
- "unicodeVersion": "6.0",
- "digest": "db3f7e0dd1e1aa92541afc4db067e0c6bf9d325d912b1bafce182e683f68a06e"
- },
- "red_circle": {
- "category": "symbols",
- "moji": "🔴",
- "description": "large red circle",
- "unicodeVersion": "6.0",
- "digest": "a76514fffb0fec7b8994aa202144350f3257c4562c02a451c5727d20ce0f21ac"
- },
- "registered": {
- "category": "symbols",
- "moji": "®️",
- "description": "registered sign",
- "unicodeVersion": "1.1",
- "digest": "aeabdec7e5ddeed91ca77a6b926d76362e90d955225cbba6cee8b26442661e10"
- },
- "relaxed": {
- "category": "people",
- "moji": "☺",
- "description": "white smiling face",
- "unicodeVersion": "1.1",
- "digest": "27bb85737e7f969e392a23141f27c75c82e327fcd0614a445dec47a00578057d"
- },
- "relieved": {
- "category": "people",
- "moji": "😌",
- "description": "relieved face",
- "unicodeVersion": "6.0",
- "digest": "ed96de2532a1fd5f96f52621d233b391830d31119b9604e7505340aac2dd1fa5"
- },
- "reminder_ribbon": {
- "category": "activity",
- "moji": "🎗",
- "description": "reminder ribbon",
- "unicodeVersion": "7.0",
- "digest": "3f3b9a65033a2e9e9d58ee18aa3cb3158ce2bf243c6c0352f7278bf2b4f8106c"
- },
- "repeat": {
- "category": "symbols",
- "moji": "🔁",
- "description": "clockwise rightwards and leftwards open circle arr",
- "unicodeVersion": "6.0",
- "digest": "6e7a4196d2899b28b0c85a77c6abafd202f91189710793e9a6b4a95ab55c0fc5"
- },
- "repeat_one": {
- "category": "symbols",
- "moji": "🔂",
- "description": "clockwise rightwards and leftwards open circle arr",
- "unicodeVersion": "6.0",
- "digest": "6f6ace59c5d36c66d5816247e9c29f6aa3efef428ee83c99c9d341f7c055a64c"
- },
- "restroom": {
- "category": "symbols",
- "moji": "🚻",
- "description": "restroom",
- "unicodeVersion": "6.0",
- "digest": "f2a6d958afccd904f504425f024c968e22d13466f122006b245ea3db25e22bdd"
- },
- "revolving_hearts": {
- "category": "symbols",
- "moji": "💞",
- "description": "revolving hearts",
- "unicodeVersion": "6.0",
- "digest": "f6d44311823de89d93f7f0c0758e60a804491237b18b4b0bd20a9843570c9c04"
- },
- "rewind": {
- "category": "symbols",
- "moji": "⏪",
- "description": "black left-pointing double triangle",
- "unicodeVersion": "6.0",
- "digest": "12a3f494d633a469015ed63e737a4bf0fac43206bf4a55c4d0e0c588a81bdffc"
- },
- "rhino": {
- "category": "nature",
- "moji": "🦏",
- "description": "rhinoceros",
- "unicodeVersion": "9.0",
- "digest": "6b23d83c2b2cc252e5983b75840086971807c2ea5425f17753d049b5b7a5ebf7"
- },
- "ribbon": {
- "category": "objects",
- "moji": "🎀",
- "description": "ribbon",
- "unicodeVersion": "6.0",
- "digest": "498ca699cf15df0de221f962a03566a1944f9eab0cdd44353460d9198265f35c"
- },
- "rice": {
- "category": "food",
- "moji": "🍚",
- "description": "cooked rice",
- "unicodeVersion": "6.0",
- "digest": "ca2eb63d32044cf29435eec542dea6d2aad9eed4dcf4d12bb092a221c4e17eac"
- },
- "rice_ball": {
- "category": "food",
- "moji": "🍙",
- "description": "rice ball",
- "unicodeVersion": "6.0",
- "digest": "40e8af6fcac8dbc63e18809e12a00e41dc8cf75eba0c856f8c031c866e27099c"
- },
- "rice_cracker": {
- "category": "food",
- "moji": "🍘",
- "description": "rice cracker",
- "unicodeVersion": "6.0",
- "digest": "33d54212f8418a5148b4b7e7b38edc72e7f03c5cf54356cdc15319c56160a3f3"
- },
- "rice_scene": {
- "category": "travel",
- "moji": "🎑",
- "description": "moon viewing ceremony",
- "unicodeVersion": "6.0",
- "digest": "18b94e88b72cd2158d831d86f78c91d3ae44b279ec104b3606fd177a2ab1372f"
- },
- "right_facing_fist": {
- "category": "people",
- "moji": "🤜",
- "description": "right-facing fist",
- "unicodeVersion": "9.0",
- "digest": "d1abe6e551a7b336ed0c7234db5c5a653db975647ac603a320f05b1635378736"
- },
- "right_facing_fist_tone1": {
- "category": "people",
- "moji": "🤜🏻",
- "description": "right facing fist tone 1",
- "unicodeVersion": "9.0",
- "digest": "d281365007abb9150174089f9a1baa7033237f6e566aca72763e538861674a74"
- },
- "right_facing_fist_tone2": {
- "category": "people",
- "moji": "🤜🏼",
- "description": "right facing fist tone 2",
- "unicodeVersion": "9.0",
- "digest": "841c242354f94f30eea0e16ab3373d6c6d6d3e6a90ac20baf7765a08bf6e7f07"
- },
- "right_facing_fist_tone3": {
- "category": "people",
- "moji": "🤜🏽",
- "description": "right facing fist tone 3",
- "unicodeVersion": "9.0",
- "digest": "6843fe9b8f162bc43bff78c16555f9eadece02285ca7758818db5e8c4caf064b"
- },
- "right_facing_fist_tone4": {
- "category": "people",
- "moji": "🤜🏾",
- "description": "right facing fist tone 4",
- "unicodeVersion": "9.0",
- "digest": "af349e6f8b54e0124667e7a6cf01fb4eb48eb21653e8f6d0ddd22e8114dde797"
- },
- "right_facing_fist_tone5": {
- "category": "people",
- "moji": "🤜🏿",
- "description": "right facing fist tone 5",
- "unicodeVersion": "9.0",
- "digest": "efd40f38ab91c5ea8e66ba129d4aa2d5af2c35c5030182695cfe81d6e9123987"
- },
- "ring": {
- "category": "people",
- "moji": "💍",
- "description": "ring",
- "unicodeVersion": "6.0",
- "digest": "ec4386554d3b001d9b64cfa534094b67844b55bcbec118146c5d238079107f6f"
- },
- "robot": {
- "category": "people",
- "moji": "🤖",
- "description": "robot face",
- "unicodeVersion": "8.0",
- "digest": "363bacd1c9c3bb115d4fe363ac212fc0a81270c057aaf432ab866581b976e38d"
- },
- "rocket": {
- "category": "travel",
- "moji": "🚀",
- "description": "rocket",
- "unicodeVersion": "6.0",
- "digest": "136f56b7d54596e10ba226a05a8f4628eba967d8fc90d55c115bdf4366d102d8"
- },
- "rofl": {
- "category": "people",
- "moji": "🤣",
- "description": "rolling on the floor laughing",
- "unicodeVersion": "9.0",
- "digest": "1a997e5e1a86c52ced7f4685ad6eb6ce93d50aef0b4cde72f143dd75e5139b43"
- },
- "roller_coaster": {
- "category": "travel",
- "moji": "🎢",
- "description": "roller coaster",
- "unicodeVersion": "6.0",
- "digest": "5c4c5d8639d4bbeab07bc46304e5233a95f379fad92e395d087afe5af00529fc"
- },
- "rolling_eyes": {
- "category": "people",
- "moji": "🙄",
- "description": "face with rolling eyes",
- "unicodeVersion": "8.0",
- "digest": "a5fec5606c1cd4b295fe69c261326c848e246622c02ca6cda9f2c5a5bf0aed98"
- },
- "rooster": {
- "category": "nature",
- "moji": "🐓",
- "description": "rooster",
- "unicodeVersion": "6.0",
- "digest": "8ebd22e8776d16c6557f777a731871e93a20b8b828b718f57267033f13a5e50b"
- },
- "rose": {
- "category": "nature",
- "moji": "🌹",
- "description": "rose",
- "unicodeVersion": "6.0",
- "digest": "a86cff9a79b2296c8c2c39dbeb7bd9e42edf2e5af402ae5dfb63a3bf910083cd"
- },
- "rosette": {
- "category": "activity",
- "moji": "🏵",
- "description": "rosette",
- "unicodeVersion": "7.0",
- "digest": "b5d6fac78383056f66f48e9576f3e8e1f38c473330f44382e0f2a13bb0b6cf89"
- },
- "rotating_light": {
- "category": "travel",
- "moji": "🚨",
- "description": "police cars revolving light",
- "unicodeVersion": "6.0",
- "digest": "353edbbfb893497b28d753273a11eb94ae67136365dae826ad5c7ac5a37b0d08"
- },
- "round_pushpin": {
- "category": "objects",
- "moji": "📍",
- "description": "round pushpin",
- "unicodeVersion": "6.0",
- "digest": "c79c44a8563cd0777ce2f26d1dea54b283d76d4309eccfe048ba3b2ea8a9645b"
- },
- "rowboat": {
- "category": "activity",
- "moji": "🚣",
- "description": "rowboat",
- "unicodeVersion": "6.0",
- "digest": "a0a0b5f15fffb7be60e7467c2a0a368b34a8f9d47af65ead06881bad9e0bd8c2"
- },
- "rowboat_tone1": {
- "category": "activity",
- "moji": "🚣🏻",
- "description": "rowboat tone 1",
- "unicodeVersion": "8.0",
- "digest": "2f6403643528646b73013979e3564e54a9a529782f019a44a956ff275b2017c0"
- },
- "rowboat_tone2": {
- "category": "activity",
- "moji": "🚣🏼",
- "description": "rowboat tone 2",
- "unicodeVersion": "8.0",
- "digest": "cd925816e16fce63bca0dbd0fc2832fd69ff98a9ee7836ddbc939f3bd9b5a189"
- },
- "rowboat_tone3": {
- "category": "activity",
- "moji": "🚣🏽",
- "description": "rowboat tone 3",
- "unicodeVersion": "8.0",
- "digest": "09cee7c79709dfdb07e98b585617a068dd624713a02d0f5c21fd7b09a5695baf"
- },
- "rowboat_tone4": {
- "category": "activity",
- "moji": "🚣🏾",
- "description": "rowboat tone 4",
- "unicodeVersion": "8.0",
- "digest": "12408bd1b720f2799c46fb22d91c9d62fe5fe4f0f2449e2eff921d7f392ab22f"
- },
- "rowboat_tone5": {
- "category": "activity",
- "moji": "🚣🏿",
- "description": "rowboat tone 5",
- "unicodeVersion": "8.0",
- "digest": "cb64137080640be1bcb7e577e0984bc5cf5dd524e60e229c09003191c4ca1680"
- },
- "rugby_football": {
- "category": "activity",
- "moji": "🏉",
- "description": "rugby football",
- "unicodeVersion": "6.0",
- "digest": "db852921f30f88e9604440a5d7f8ce513c3001293a1ffbc35c66c45fee1159c4"
- },
- "runner": {
- "category": "people",
- "moji": "🏃",
- "description": "runner",
- "unicodeVersion": "6.0",
- "digest": "6a87e5a783e98a571bdbe4d983b0cce46c0b337707a99a8bea7dcc5a5d78c46e"
- },
- "runner_tone1": {
- "category": "people",
- "moji": "🏃🏻",
- "description": "runner tone 1",
- "unicodeVersion": "8.0",
- "digest": "788912454e5a001a96cfbbf7ae07d22c7d53bd0a0a333add62bd38f50f890202"
- },
- "runner_tone2": {
- "category": "people",
- "moji": "🏃🏼",
- "description": "runner tone 2",
- "unicodeVersion": "8.0",
- "digest": "0e0c0509fe054b31b82629bb97d410d56154143e0612af9ced7f754bc16876b8"
- },
- "runner_tone3": {
- "category": "people",
- "moji": "🏃🏽",
- "description": "runner tone 3",
- "unicodeVersion": "8.0",
- "digest": "33b6233182bec5958325115fc8b238913773b03fe616ceb465152c75a83aa3bd"
- },
- "runner_tone4": {
- "category": "people",
- "moji": "🏃🏾",
- "description": "runner tone 4",
- "unicodeVersion": "8.0",
- "digest": "d14b09b58e47a80781df8f7d6687be75f2bc22f312b96129109166c1535e6001"
- },
- "runner_tone5": {
- "category": "people",
- "moji": "🏃🏿",
- "description": "runner tone 5",
- "unicodeVersion": "8.0",
- "digest": "a816106153bf16d304e766386d3ad641fc931faa302d998fc2bd3e3334de5d7f"
- },
- "running_shirt_with_sash": {
- "category": "activity",
- "moji": "🎽",
- "description": "running shirt with sash",
- "unicodeVersion": "6.0",
- "digest": "a5f5ff9bf3e3eab82f370c9b81dc2bce186bb0a667dee54a2066d42f04d97eb4"
- },
- "sa": {
- "category": "symbols",
- "moji": "🈂",
- "description": "squared katakana sa",
- "unicodeVersion": "6.0",
- "digest": "58e18447fd35e85c8a7c45eab01da1e2d6ff67d933e0ea9d5875a5e83273e733"
- },
- "sagittarius": {
- "category": "symbols",
- "moji": "♐",
- "description": "sagittarius",
- "unicodeVersion": "1.1",
- "digest": "23c2aa3cbb29c0fb6c443d8d388e41454b4da2aae8f239c4572ecf0e9d580276"
- },
- "sailboat": {
- "category": "travel",
- "moji": "⛵",
- "description": "sailboat",
- "unicodeVersion": "5.2",
- "digest": "252e917b1bc71019256db272af716e77bbe5839e4e5b77416ee566657ec3ffaf"
- },
- "sake": {
- "category": "food",
- "moji": "🍶",
- "description": "sake bottle and cup",
- "unicodeVersion": "6.0",
- "digest": "bd09899bee1411e26464b5f0d86d69a3f57cfd6e3f557f0f87fcb61bd2fc51ba"
- },
- "salad": {
- "category": "food",
- "moji": "🥗",
- "description": "green salad",
- "unicodeVersion": "9.0",
- "digest": "2498d846c9ae599cd1fac3c74e87f595e1938df6b9a0eeb001c7f19ca0c7477d"
- },
- "sandal": {
- "category": "people",
- "moji": "👡",
- "description": "womans sandal",
- "unicodeVersion": "6.0",
- "digest": "9737db7518eaf91a7294503c471ba0b015260677ca842988ef3764d30a0d402b"
- },
- "santa": {
- "category": "people",
- "moji": "🎅",
- "description": "father christmas",
- "unicodeVersion": "6.0",
- "digest": "bc9f3a14f824d9299d2132822c6341c4e87f53ed0c5050c31b5b96d801bc5c3c"
- },
- "santa_tone1": {
- "category": "people",
- "moji": "🎅🏻",
- "description": "father christmas tone 1",
- "unicodeVersion": "8.0",
- "digest": "7e527394c52da94c740197b0d05d15d7c5ee113dce7a7800ec617bd3a3fa297f"
- },
- "santa_tone2": {
- "category": "people",
- "moji": "🎅🏼",
- "description": "father christmas tone 2",
- "unicodeVersion": "8.0",
- "digest": "26c649ec3f466952dbb6d3234ad3254c735259c83bfbbf96c68d0203ed5744e1"
- },
- "santa_tone3": {
- "category": "people",
- "moji": "🎅🏽",
- "description": "father christmas tone 3",
- "unicodeVersion": "8.0",
- "digest": "d8e656829487c0808af4efb095e55cbc8a9b18576572ebd61dbac422119a1a01"
- },
- "santa_tone4": {
- "category": "people",
- "moji": "🎅🏾",
- "description": "father christmas tone 4",
- "unicodeVersion": "8.0",
- "digest": "e12990b6edb39eea5f8bcd0f3c4d0f404cd47beb8fe180066455f833e7b99cf8"
- },
- "santa_tone5": {
- "category": "people",
- "moji": "🎅🏿",
- "description": "father christmas tone 5",
- "unicodeVersion": "8.0",
- "digest": "0310b472690b6f3aac9fa4f611fa00c3d9d5d50edb94d08a676c77f28c0913da"
- },
- "satellite": {
- "category": "objects",
- "moji": "📡",
- "description": "satellite antenna",
- "unicodeVersion": "6.0",
- "digest": "84aa893218e8cc97a0f8b74d2d34c0b05a62d8eef47d5f0fede8ee956b222acd"
- },
- "satellite_orbital": {
- "category": "travel",
- "moji": "🛰",
- "description": "satellite",
- "unicodeVersion": "7.0",
- "digest": "f80a557315729acf11186808c9bf031570a12f34c8da29e6f4506d732609d541"
- },
- "saxophone": {
- "category": "activity",
- "moji": "🎷",
- "description": "saxophone",
- "unicodeVersion": "6.0",
- "digest": "e49b31381a32c4ddf5b47b631e3c260bc62c793084b3cf026af5bea3105b6e0e"
- },
- "scales": {
- "category": "objects",
- "moji": "⚖",
- "description": "scales",
- "unicodeVersion": "4.1",
- "digest": "2d0fcd2d6d6fe368d142ce7cd1ba78d9fc802131d84e0097d89e49041a9342e8"
- },
- "school": {
- "category": "travel",
- "moji": "🏫",
- "description": "school",
- "unicodeVersion": "6.0",
- "digest": "d6cf41776c0b3c6e3a20d672b85ef91a65085dc7ee40aa4b0c94b0683f3ab2b4"
- },
- "school_satchel": {
- "category": "people",
- "moji": "🎒",
- "description": "school satchel",
- "unicodeVersion": "6.0",
- "digest": "500fdb662493897890ad00c67200190fc2b48a5629231994d7bf43a5a9897e6d"
- },
- "scissors": {
- "category": "objects",
- "moji": "✂",
- "description": "black scissors",
- "unicodeVersion": "1.1",
- "digest": "295ce7c48d3f8e58daa454f18d534146bfaf98d46614e2783535927f23d22215"
- },
- "scooter": {
- "category": "travel",
- "moji": "🛴",
- "description": "scooter",
- "unicodeVersion": "9.0",
- "digest": "baa3060602995716fba048ed2be1559712abc6edc3d8822b4ae6c0fe185197e4"
- },
- "scorpion": {
- "category": "nature",
- "moji": "🦂",
- "description": "scorpion",
- "unicodeVersion": "8.0",
- "digest": "2e97ed412be63a4eaefd4205d13e5d4957389ce7497c4c79e00e9729bdb39e0c"
- },
- "scorpius": {
- "category": "symbols",
- "moji": "♏",
- "description": "scorpius",
- "unicodeVersion": "1.1",
- "digest": "32550597084a3b17fdab6bc4f56513a5952581ad07e79fb56abe05d6665a86af"
- },
- "scream": {
- "category": "people",
- "moji": "😱",
- "description": "face screaming in fear",
- "unicodeVersion": "6.0",
- "digest": "3403d66a449c643d1dbc3029d11bf9a9edfd503b5594b524517356a8eeef296e"
- },
- "scream_cat": {
- "category": "people",
- "moji": "🙀",
- "description": "weary cat face",
- "unicodeVersion": "6.0",
- "digest": "e4d277a511c2e1edc5873579e78a94320133ff730502c1ebf36272e4a2e5c598"
- },
- "scroll": {
- "category": "objects",
- "moji": "📜",
- "description": "scroll",
- "unicodeVersion": "6.0",
- "digest": "abdb7024b5f50b04389b1a65e5cb5a3b7929f4e9c5719388684603210774b3c1"
- },
- "seat": {
- "category": "travel",
- "moji": "💺",
- "description": "seat",
- "unicodeVersion": "6.0",
- "digest": "ef4b820995bafb53c28877312060b1b6ef1d2e5d65268f9942ac3567914ef2da"
- },
- "second_place": {
- "category": "activity",
- "moji": "🥈",
- "description": "second place medal",
- "unicodeVersion": "9.0",
- "digest": "9c9a125c7085e8ab5b89cbd10d9458eac835330dbb78737e8cf979a39c2ae5fd"
- },
- "secret": {
- "category": "symbols",
- "moji": "㊙",
- "description": "circled ideograph secret",
- "unicodeVersion": "1.1",
- "digest": "a67c62b033cebaec448c90f0f98960058fd020fe523dbd315e6b60966fb89da7"
- },
- "see_no_evil": {
- "category": "nature",
- "moji": "🙈",
- "description": "see-no-evil monkey",
- "unicodeVersion": "6.0",
- "digest": "b5659d1f0ae7dc35ba729bee05ef351dbf8fe299b768937a1e271c19ac1dd9a9"
- },
- "seedling": {
- "category": "nature",
- "moji": "🌱",
- "description": "seedling",
- "unicodeVersion": "6.0",
- "digest": "4227f4780193ccd7d807c0e6d18b42cbaa247554708efbf64dbd3b7c6919a466"
- },
- "selfie": {
- "category": "people",
- "moji": "🤳",
- "description": "selfie",
- "unicodeVersion": "9.0",
- "digest": "4914fbc5b8a0838d275c286b7a3626c9353e233ef75f2b1f600a647ff56a7ff4"
- },
- "selfie_tone1": {
- "category": "people",
- "moji": "🤳🏻",
- "description": "selfie tone 1",
- "unicodeVersion": "9.0",
- "digest": "0e9e3090566876f49dc5a03321e190dce2737a5c8547b07f0244056d59623182"
- },
- "selfie_tone2": {
- "category": "people",
- "moji": "🤳🏼",
- "description": "selfie tone 2",
- "unicodeVersion": "9.0",
- "digest": "5d2c271f2cf39d3ffacf1192b1804edef0849a2ad8f0e81e493a752960de97ed"
- },
- "selfie_tone3": {
- "category": "people",
- "moji": "🤳🏽",
- "description": "selfie tone 3",
- "unicodeVersion": "9.0",
- "digest": "41670b6b45ab178205692a6bca8816e79f99ed7bbab1754b99bcf973ff178e30"
- },
- "selfie_tone4": {
- "category": "people",
- "moji": "🤳🏾",
- "description": "selfie tone 4",
- "unicodeVersion": "9.0",
- "digest": "4b1c5145f0e454aed5c37b3dd3bef07dfa3576880997e6bd552871de474867ec"
- },
- "selfie_tone5": {
- "category": "people",
- "moji": "🤳🏿",
- "description": "selfie tone 5",
- "unicodeVersion": "9.0",
- "digest": "bbe0db1f762ad830a38a6ff85e36565c0ef446c22b8ab25054d6b266f1b8421d"
- },
- "seven": {
- "category": "symbols",
- "moji": "7️⃣",
- "description": "keycap digit seven",
- "unicodeVersion": "3.0",
- "digest": "e9c95466693be79dff2e1d8eadddfc75967e6f7f641a21efcabccf173a8224c7"
- },
- "shallow_pan_of_food": {
- "category": "food",
- "moji": "🥘",
- "description": "shallow pan of food",
- "unicodeVersion": "9.0",
- "digest": "1c90318cf2f78c965a0e4dfc710781e5da862874fa8e29f010231e5dc5657976"
- },
- "shamrock": {
- "category": "nature",
- "moji": "☘",
- "description": "shamrock",
- "unicodeVersion": "4.1",
- "digest": "cb9408a7b1884bfca8fe7cd2ea93440a49a44b226298325a3f138f6e772f5d64"
- },
- "shark": {
- "category": "nature",
- "moji": "🦈",
- "description": "shark",
- "unicodeVersion": "9.0",
- "digest": "552b7265f53c435c860583c2aaee8acec5f854fa2092f6e8be74d89f9f3132da"
- },
- "shaved_ice": {
- "category": "food",
- "moji": "🍧",
- "description": "shaved ice",
- "unicodeVersion": "6.0",
- "digest": "5f3f65f3974f30d1c9557d2c4af3052c7548f534b854f964647cafc2b4ab73a8"
- },
- "sheep": {
- "category": "nature",
- "moji": "🐑",
- "description": "sheep",
- "unicodeVersion": "6.0",
- "digest": "7bd6d2af15a7d13587b9521d80668d5db70c67188df59a33921e78edbf40e42e"
- },
- "shell": {
- "category": "nature",
- "moji": "🐚",
- "description": "spiral shell",
- "unicodeVersion": "6.0",
- "digest": "4d30626d66ac2921fb7a81761841923b855e363c4198eea67c76d86c696cc805"
- },
- "shield": {
- "category": "objects",
- "moji": "🛡",
- "description": "shield",
- "unicodeVersion": "7.0",
- "digest": "ce3512d081c31c26df95a8f791aa413db09d25e8fba29a1e9f6a2470d6cb3430"
- },
- "shinto_shrine": {
- "category": "travel",
- "moji": "⛩",
- "description": "shinto shrine",
- "unicodeVersion": "5.2",
- "digest": "e3027766f283d86acb7f0730dd04cbda16bb6fa16c90edb40dad7f09f1bca3fe"
- },
- "ship": {
- "category": "travel",
- "moji": "🚢",
- "description": "ship",
- "unicodeVersion": "6.0",
- "digest": "9d0c13fcfcefd0a07424396626cc955e0b5e6cdf8b6d7d3ae1c1f746826af89b"
- },
- "shirt": {
- "category": "people",
- "moji": "👕",
- "description": "t-shirt",
- "unicodeVersion": "6.0",
- "digest": "a165e8bd82e5ef701aafaabcfac6946af3a0c10c22d4c84010103ecf8d74e424"
- },
- "shopping_bags": {
- "category": "objects",
- "moji": "🛍",
- "description": "shopping bags",
- "unicodeVersion": "7.0",
- "digest": "1a6df85fd8117c2c9361c7524a43613a80d6c6d65a1940f2320cbc7d451ebf6f"
- },
- "shopping_cart": {
- "category": "objects",
- "moji": "🛒",
- "description": "shopping trolley",
- "unicodeVersion": "9.0",
- "digest": "cc32f38d94856b58620bd817fe40641c937ceacdc6b3c7e9ed4350c8926f128f"
- },
- "shower": {
- "category": "objects",
- "moji": "🚿",
- "description": "shower",
- "unicodeVersion": "6.0",
- "digest": "dc732f36a76bbd98ccc3ec886bb697fe8b0e799bc063abc1d5aaa9da4adb655a"
- },
- "shrimp": {
- "category": "nature",
- "moji": "🦐",
- "description": "shrimp",
- "unicodeVersion": "9.0",
- "digest": "fd240e3208f6221cf6e7053645d40767898ea430733e0ebc5b81a8f834be2eb1"
- },
- "shrug": {
- "category": "people",
- "moji": "🤷",
- "description": "shrug",
- "unicodeVersion": "9.0",
- "digest": "1203afd3973f34c726c8e8ca66b76c2f1e7036a45d595d6f4cfd104c00d76d63"
- },
- "shrug_tone1": {
- "category": "people",
- "moji": "🤷🏻",
- "description": "shrug tone 1",
- "unicodeVersion": "9.0",
- "digest": "fb6eb588f019cf7a25bac347cb2b5c124f9d333314d040daef8b70ed1de5032d"
- },
- "shrug_tone2": {
- "category": "people",
- "moji": "🤷🏼",
- "description": "shrug tone 2",
- "unicodeVersion": "9.0",
- "digest": "a3b64ffa33f602adae12924b5576b6ed1f4bebbcf2db4952ec846b9778aadeaa"
- },
- "shrug_tone3": {
- "category": "people",
- "moji": "🤷🏽",
- "description": "shrug tone 3",
- "unicodeVersion": "9.0",
- "digest": "e7148fbdb7194182d5f3c66e9a1e7a9e5a8c4d88ceab38f5d0ecd40bc231f6b8"
- },
- "shrug_tone4": {
- "category": "people",
- "moji": "🤷🏾",
- "description": "shrug tone 4",
- "unicodeVersion": "9.0",
- "digest": "8977c5afbde154d944f965ff7f10d26167ea427b8b191a6ec8708c6517168334"
- },
- "shrug_tone5": {
- "category": "people",
- "moji": "🤷🏿",
- "description": "shrug tone 5",
- "unicodeVersion": "9.0",
- "digest": "3968a312496e479a86b63274f529c2b283d180118c04edf3c6b6ea5f812c05bb"
- },
- "signal_strength": {
- "category": "symbols",
- "moji": "📶",
- "description": "antenna with bars",
- "unicodeVersion": "6.0",
- "digest": "eabf6d0cae69aea6027f1dede7df1ac51fc09d85af8a4ae9b1df1fcb8ee4a0f0"
- },
- "six": {
- "category": "symbols",
- "moji": "6️⃣",
- "description": "keycap digit six",
- "unicodeVersion": "3.0",
- "digest": "f455fcc89917bf67c1ac4245399146912578582cdff0e1e8bc216a4c4b7c43a0"
- },
- "six_pointed_star": {
- "category": "symbols",
- "moji": "🔯",
- "description": "six pointed star with middle dot",
- "unicodeVersion": "6.0",
- "digest": "1ee9c385a74dc6954e37727615d99209a61627562c8675d4354262b2c421418f"
- },
- "ski": {
- "category": "activity",
- "moji": "🎿",
- "description": "ski and ski boot",
- "unicodeVersion": "6.0",
- "digest": "973ff4abd90a020e2c608c32ab88324e1a11ca7a4541e436f37c5d858681eba6"
- },
- "skier": {
- "category": "activity",
- "moji": "⛷",
- "description": "skier",
- "unicodeVersion": "5.2",
- "digest": "7a490189499bc88ed15fe945813665ba3114edc039d25eb003026d27c84b5f78"
- },
- "skull": {
- "category": "people",
- "moji": "💀",
- "description": "skull",
- "unicodeVersion": "6.0",
- "digest": "ccf317cd63caa24cd1a008dd26cda83d6487a0a7fca71843e42715cd8cbaafc9"
- },
- "skull_crossbones": {
- "category": "objects",
- "moji": "☠",
- "description": "skull and crossbones",
- "unicodeVersion": "1.1",
- "digest": "81f050043fc49fb83d5e87753337f77fb2acd599e53432212de42ec58345d567"
- },
- "sleeping": {
- "category": "people",
- "moji": "😴",
- "description": "sleeping face",
- "unicodeVersion": "6.1",
- "digest": "061017b6fea9012cdfc7f90ab5dbf18a55830743fdd062f1ea0a085f52e0a564"
- },
- "sleeping_accommodation": {
- "category": "objects",
- "moji": "🛌",
- "description": "sleeping accommodation",
- "unicodeVersion": "7.0",
- "digest": "18ea38c6da5ac6f86c56b546c78ba60bca1aca9eba397b9500d91e8533e81823"
- },
- "sleepy": {
- "category": "people",
- "moji": "😪",
- "description": "sleepy face",
- "unicodeVersion": "6.0",
- "digest": "afc0c40fb97bd1fe79e828f76f03aa08beeed09b42e307b5053758d9889fcc01"
- },
- "slight_frown": {
- "category": "people",
- "moji": "🙁",
- "description": "slightly frowning face",
- "unicodeVersion": "7.0",
- "digest": "2bccd273d6445ddf54366b9aa565370af3110b7722cb9a85e76534c729b397b8"
- },
- "slight_smile": {
- "category": "people",
- "moji": "🙂",
- "description": "slightly smiling face",
- "unicodeVersion": "7.0",
- "digest": "04feb9e847c67936ddd0e40d6dd6c90333abc9bfbd81fae7fef9bd1e5265ba9e"
- },
- "slot_machine": {
- "category": "activity",
- "moji": "🎰",
- "description": "slot machine",
- "unicodeVersion": "6.0",
- "digest": "c057d62fa2bca301d20f6103606873b82d0136711f4d922eb05ab29d3179ecfe"
- },
- "small_blue_diamond": {
- "category": "symbols",
- "moji": "🔹",
- "description": "small blue diamond",
- "unicodeVersion": "6.0",
- "digest": "22016e16c1e769099e972a93eaed9fd4131867020c0d6669aeb323d414ef1f93"
- },
- "small_orange_diamond": {
- "category": "symbols",
- "moji": "🔸",
- "description": "small orange diamond",
- "unicodeVersion": "6.0",
- "digest": "7de47af62764c8136415214e7eb7b8e985ec600b79991c0ba44b96824350eeef"
- },
- "small_red_triangle": {
- "category": "symbols",
- "moji": "🔺",
- "description": "up-pointing red triangle",
- "unicodeVersion": "6.0",
- "digest": "521e2b28387cd5d13d17c5dedf6b944c0139f36bc6136e5bcca5b59ee1a59823"
- },
- "small_red_triangle_down": {
- "category": "symbols",
- "moji": "🔻",
- "description": "down-pointing red triangle",
- "unicodeVersion": "6.0",
- "digest": "cfbea3a1506cd1f26aa11603421d2137c3a847f4dc1d0728c16901e0e4195adc"
- },
- "smile": {
- "category": "people",
- "moji": "😄",
- "description": "smiling face with open mouth and smiling eyes",
- "unicodeVersion": "6.0",
- "digest": "fb06bf4088887ca1aadbc0201b63d75f3d2b5b5779bd81f1767f17e794b0c0a7"
- },
- "smile_cat": {
- "category": "people",
- "moji": "😸",
- "description": "grinning cat face with smiling eyes",
- "unicodeVersion": "6.0",
- "digest": "5882f8784080c11ae3b95bccb4ecf00dacd127047ff76d3b4158fbba0ddb1f14"
- },
- "smiley": {
- "category": "people",
- "moji": "😃",
- "description": "smiling face with open mouth",
- "unicodeVersion": "6.0",
- "digest": "9b0f2fca8ba5bb1b3de39686302f2f9ef7e1c93d4af47c71828931f874bd4db1"
- },
- "smiley_cat": {
- "category": "people",
- "moji": "😺",
- "description": "smiling cat face with open mouth",
- "unicodeVersion": "6.0",
- "digest": "eb6c8fa3e46a9ea9c0e79b3db5578299ea041792ae46c54c50799e5c3970c372"
- },
- "smiling_imp": {
- "category": "people",
- "moji": "😈",
- "description": "smiling face with horns",
- "unicodeVersion": "6.0",
- "digest": "7609669c056339bec4dc916c3b0fb56d4adc55d37b8c3e0fb078af59594500d9"
- },
- "smirk": {
- "category": "people",
- "moji": "😏",
- "description": "smirking face",
- "unicodeVersion": "6.0",
- "digest": "e02911a76fe7c40dde28998741f201789b7ab5c6be6e5168e4eddbd9886ef790"
- },
- "smirk_cat": {
- "category": "people",
- "moji": "😼",
- "description": "cat face with wry smile",
- "unicodeVersion": "6.0",
- "digest": "8aed1a44a0b0673c1f62cf9f77d89239725258b7b3b482b66b5d39c6306b601a"
- },
- "smoking": {
- "category": "objects",
- "moji": "🚬",
- "description": "smoking symbol",
- "unicodeVersion": "6.0",
- "digest": "3fa148109d83f785ad90999c0d362fb9d6aadd38986f7d483fd0f70ecb5b0447"
- },
- "snail": {
- "category": "nature",
- "moji": "🐌",
- "description": "snail",
- "unicodeVersion": "6.0",
- "digest": "4244f824afbbf8f60e41654f35596395a9a45715a3f229e351aaea6dab361e02"
- },
- "snake": {
- "category": "nature",
- "moji": "🐍",
- "description": "snake",
- "unicodeVersion": "6.0",
- "digest": "1b28000702a5b3b294b22c5bd1ad8c203937712fb30dc1cb12f63fc6244e38e6"
- },
- "sneezing_face": {
- "category": "people",
- "moji": "🤧",
- "description": "sneezing face",
- "unicodeVersion": "9.0",
- "digest": "fa08b2714d529efb670662a65b19201333217d31152a1e4c48b3ee7ab4398eb5"
- },
- "snowboarder": {
- "category": "activity",
- "moji": "🏂",
- "description": "snowboarder",
- "unicodeVersion": "6.0",
- "digest": "b2acc118ae84560f980a44a95af7c2de8e57c2c95ff69ef9c25c9e5d535306b8"
- },
- "snowflake": {
- "category": "nature",
- "moji": "❄",
- "description": "snowflake",
- "unicodeVersion": "1.1",
- "digest": "256f84d43855ee7699d7bfa2e3bb4cf71ec61ee9cc83a1ffb2973393cc43a5fa"
- },
- "snowman": {
- "category": "nature",
- "moji": "⛄",
- "description": "snowman without snow",
- "unicodeVersion": "5.2",
- "digest": "c0908d9bc9b9fabff1e5eb18c6db07e981a4b9d886c7babbe2ce109e244f8182"
- },
- "snowman2": {
- "category": "nature",
- "moji": "☃",
- "description": "snowman",
- "unicodeVersion": "1.1",
- "digest": "ce7bc1b374999e94c2f4a08f77ed979ccb6ee86ec9a6c1b8f4a93680080a25a7"
- },
- "sob": {
- "category": "people",
- "moji": "😭",
- "description": "loudly crying face",
- "unicodeVersion": "6.0",
- "digest": "2bd275f629a26cb40ce648eff68155a5625e944ed724b8a6d2890a80a099503a"
- },
- "soccer": {
- "category": "activity",
- "moji": "⚽",
- "description": "soccer ball",
- "unicodeVersion": "5.2",
- "digest": "1807b8f9e9b0a3cbf390a582f52d2ec4dad7a19008d9d0215ae6cded7b6dd691"
- },
- "soon": {
- "category": "symbols",
- "moji": "🔜",
- "description": "soon with rightwards arrow above",
- "unicodeVersion": "6.0",
- "digest": "6325b67539559992fc3d1ed23f2dc20ee95b3052b93a674f82ed53dc2e199270"
- },
- "sos": {
- "category": "symbols",
- "moji": "🆘",
- "description": "squared sos",
- "unicodeVersion": "6.0",
- "digest": "13dcfd9239e12ebdb00b2e3b632e26fa1160ed645226f21b6cef5a7f3a9690fe"
- },
- "sound": {
- "category": "symbols",
- "moji": "🔉",
- "description": "speaker with one sound wave",
- "unicodeVersion": "6.0",
- "digest": "c1e588da701bb5e139cfb4c8e068b8785d4ac08afc255792d28b06c7e0e565a9"
- },
- "space_invader": {
- "category": "activity",
- "moji": "👾",
- "description": "alien monster",
- "unicodeVersion": "6.0",
- "digest": "84897a48330cb0ae9ac42111cfaa0e0baefb5c314cb49d1eae77c7ace3a7ab25"
- },
- "spades": {
- "category": "symbols",
- "moji": "♠",
- "description": "black spade suit",
- "unicodeVersion": "1.1",
- "digest": "4581ce17d9b2a29ba4cc38794d7869e12cd1ceda5861e67b2d09130730ccc378"
- },
- "spaghetti": {
- "category": "food",
- "moji": "🍝",
- "description": "spaghetti",
- "unicodeVersion": "6.0",
- "digest": "b2de3171e90345dc777aa7554d97d6de659f4a9928b7da6a871e3925e234484c"
- },
- "sparkle": {
- "category": "symbols",
- "moji": "❇",
- "description": "sparkle",
- "unicodeVersion": "1.1",
- "digest": "8aab76f3a4f25b2e583fe675546e400e96417bc99e1c7ed08007d3afaaffc9a1"
- },
- "sparkler": {
- "category": "travel",
- "moji": "🎇",
- "description": "firework sparkler",
- "unicodeVersion": "6.0",
- "digest": "d603ef03cdc4ec05338005c357c3a41215f180545f422fea5b40b766ecfe6d1f"
- },
- "sparkles": {
- "category": "nature",
- "moji": "✨",
- "description": "sparkles",
- "unicodeVersion": "6.0",
- "digest": "8f68cb167489b1055a8acedaf8ea4c0553d0a5f7bf0983fc3660537ff09a5360"
- },
- "sparkling_heart": {
- "category": "symbols",
- "moji": "💖",
- "description": "sparkling heart",
- "unicodeVersion": "6.0",
- "digest": "cc017b631dae27a01e15faa5f7d24c35983a4a2d928c23e9449b1b183636cb05"
- },
- "speak_no_evil": {
- "category": "nature",
- "moji": "🙊",
- "description": "speak-no-evil monkey",
- "unicodeVersion": "6.0",
- "digest": "7cb1d4a61d2947bb0624a57a7355089f751d576c3bf26b61e3a2f1c413b4c293"
- },
- "speaker": {
- "category": "symbols",
- "moji": "🔈",
- "description": "speaker",
- "unicodeVersion": "6.0",
- "digest": "f83fd9518675bb83fa037a49d762f774eccea08f2a981dc85c37c46a6621cd6d"
- },
- "speaking_head": {
- "category": "people",
- "moji": "🗣",
- "description": "speaking head in silhouette",
- "unicodeVersion": "7.0",
- "digest": "d6e536f54711d04899135d966b9ba64286e58b05879c90855e49e5c64fca7567"
- },
- "speech_balloon": {
- "category": "symbols",
- "moji": "💬",
- "description": "speech balloon",
- "unicodeVersion": "6.0",
- "digest": "8a0b9329452cb5b6d529bb5a5a56656eceaba92177f566e3748d7910588a938b"
- },
- "speech_left": {
- "category": "symbols",
- "moji": "🗨",
- "description": "left speech bubble",
- "unicodeVersion": "7.0",
- "digest": "45487904f8cbf1a1de421f85fbdd212e0a7e51d8540d9f99b65a1aea187477b5"
- },
- "speedboat": {
- "category": "travel",
- "moji": "🚤",
- "description": "speedboat",
- "unicodeVersion": "6.0",
- "digest": "54ce5a81c70e2f1e57a8d0f90829db96d146d84911462b8412e1cb6ee62ddf22"
- },
- "spider": {
- "category": "nature",
- "moji": "🕷",
- "description": "spider",
- "unicodeVersion": "7.0",
- "digest": "204672675b8f272185eb58517e6cac6e9398a9c27279b8bb0da97330d35da094"
- },
- "spider_web": {
- "category": "nature",
- "moji": "🕸",
- "description": "spider web",
- "unicodeVersion": "7.0",
- "digest": "82a223ba2c1dc71ac0945544a16a2f607a679c5727cd86070b537bec6e1fdcb1"
- },
- "spoon": {
- "category": "food",
- "moji": "🥄",
- "description": "spoon",
- "unicodeVersion": "9.0",
- "digest": "c5fbd1bd2ca6ca2cf13a6b7bfaeba67305d21b2d86bba97e7d283ca1e9b1aacf"
- },
- "spy": {
- "category": "people",
- "moji": "🕵",
- "description": "sleuth or spy",
- "unicodeVersion": "7.0",
- "digest": "af9ec39cb9ec28b9b3917628187b28899f8f778b593ce722c59575af6a8cff75"
- },
- "spy_tone1": {
- "category": "people",
- "moji": "🕵🏻",
- "description": "sleuth or spy tone 1",
- "unicodeVersion": "8.0",
- "digest": "26134ab9a163d03fde9a4ce7e77ba06acfd6a385d6534bd94cbd0430fdb56054"
- },
- "spy_tone2": {
- "category": "people",
- "moji": "🕵🏼",
- "description": "sleuth or spy tone 2",
- "unicodeVersion": "8.0",
- "digest": "6f0d3ae8b0980c4586aaacdc8df8ed5815fe5f1fe18be566316cb0f21b1beeba"
- },
- "spy_tone3": {
- "category": "people",
- "moji": "🕵🏽",
- "description": "sleuth or spy tone 3",
- "unicodeVersion": "8.0",
- "digest": "154818ede71c0b8ae5cdf621efe9560e419f62f67ad03049c29cf505bd160bbc"
- },
- "spy_tone4": {
- "category": "people",
- "moji": "🕵🏾",
- "description": "sleuth or spy tone 4",
- "unicodeVersion": "8.0",
- "digest": "2ff959799064e9e47c9e1698357b04cc0fd1343bfc8b1be0d20fe49852b05f32"
- },
- "spy_tone5": {
- "category": "people",
- "moji": "🕵🏿",
- "description": "sleuth or spy tone 5",
- "unicodeVersion": "8.0",
- "digest": "6046a8031fd6d410380d41f2a7cd658d9aec9b61a5759265a26ae1e2b9409096"
- },
- "squid": {
- "category": "nature",
- "moji": "🦑",
- "description": "squid",
- "unicodeVersion": "9.0",
- "digest": "f483430d758e7432b8696b5f95bf606b17cb017ea0beb5dc97dcf7fae7ed2455"
- },
- "stadium": {
- "category": "travel",
- "moji": "🏟",
- "description": "stadium",
- "unicodeVersion": "7.0",
- "digest": "807c4f6f19b9819ca3c846126ccfa2d24a2b00b66cb946e8be75536032426815"
- },
- "star": {
- "category": "nature",
- "moji": "⭐",
- "description": "white medium star",
- "unicodeVersion": "5.1",
- "digest": "3dc3b69f9789146c64cd333666f35ce1e1efdd4fe335f6b8574685015bf8bd09"
- },
- "star2": {
- "category": "nature",
- "moji": "🌟",
- "description": "glowing star",
- "unicodeVersion": "6.0",
- "digest": "c242d4e9c64d0ba3d8b5e3c83888ee4561c4250e48bd72f80d3264497a66ce77"
- },
- "star_and_crescent": {
- "category": "symbols",
- "moji": "☪",
- "description": "star and crescent",
- "unicodeVersion": "1.1",
- "digest": "550cf94a0efe6ef0211e51e2d84554bd789505c861a16efa79704f8ccda086b1"
- },
- "star_of_david": {
- "category": "symbols",
- "moji": "✡",
- "description": "star of david",
- "unicodeVersion": "1.1",
- "digest": "dbe79ef9f506a4f46368a8a5e9953579c97fc1bca97c1ddc7f2bcc76398f0149"
- },
- "stars": {
- "category": "travel",
- "moji": "🌠",
- "description": "shooting star",
- "unicodeVersion": "6.0",
- "digest": "46bfa86253fb531e4357590f5244a88c3713926d1f28349ca641705dc069265f"
- },
- "station": {
- "category": "travel",
- "moji": "🚉",
- "description": "station",
- "unicodeVersion": "6.0",
- "digest": "a4e784b6c4238269932befca55cd7b41af85fc899f023a49c7f1ceaf81652496"
- },
- "statue_of_liberty": {
- "category": "travel",
- "moji": "🗽",
- "description": "statue of liberty",
- "unicodeVersion": "6.0",
- "digest": "9779b56242c4eb3de2060b060ae39de21ad1082b1135ec9c29f9d1aca6bf13cb"
- },
- "steam_locomotive": {
- "category": "travel",
- "moji": "🚂",
- "description": "steam locomotive",
- "unicodeVersion": "6.0",
- "digest": "479aa4a2c2704d79f642036eef9c8ddf00f8df17b9389a87d7d5f688827c7f56"
- },
- "stew": {
- "category": "food",
- "moji": "🍲",
- "description": "pot of food",
- "unicodeVersion": "6.0",
- "digest": "0c9cdd4de27a6108da2567070dad1ab9220dc513dd8ec6543ad66dddb7c09bdc"
- },
- "stop_button": {
- "category": "symbols",
- "moji": "⏹",
- "description": "black square for stop",
- "unicodeVersion": "7.0",
- "digest": "ea16a3e7a6ffa4741509cc909944975dd24c4a0678a23cc03e02c3773e6bba92"
- },
- "stopwatch": {
- "category": "objects",
- "moji": "⏱",
- "description": "stopwatch",
- "unicodeVersion": "6.0",
- "digest": "162489af83ccc7e09349637fa9e23b97a22208b05ff6bfbd31271a50c3745ee9"
- },
- "straight_ruler": {
- "category": "objects",
- "moji": "📏",
- "description": "straight ruler",
- "unicodeVersion": "6.0",
- "digest": "ab8b04cfbb19178452fc5eb32eea3a619049b0d46ea21b4c48023e01c30b6510"
- },
- "strawberry": {
- "category": "food",
- "moji": "🍓",
- "description": "strawberry",
- "unicodeVersion": "6.0",
- "digest": "4563a502fa27cbc543f6ad287a6c40eee76319e29a98fbf818c93e7b48c2249f"
- },
- "stuck_out_tongue": {
- "category": "people",
- "moji": "😛",
- "description": "face with stuck-out tongue",
- "unicodeVersion": "6.1",
- "digest": "04df5c3e122e85ebafea184a808d090cebe8fda6c08ab08bb756a21d43b6661f"
- },
- "stuck_out_tongue_closed_eyes": {
- "category": "people",
- "moji": "😝",
- "description": "face with stuck-out tongue and tightly-closed eyes",
- "unicodeVersion": "6.0",
- "digest": "88bceb40811057945decca24c3fb69f5703a15417dd5bf16787019e9067cb125"
- },
- "stuck_out_tongue_winking_eye": {
- "category": "people",
- "moji": "😜",
- "description": "face with stuck-out tongue and winking eye",
- "unicodeVersion": "6.0",
- "digest": "73443f4962da500d4ebe32abf7a9d95a217fa3f58df5567f8ac623b439f8b265"
- },
- "stuffed_flatbread": {
- "category": "food",
- "moji": "🥙",
- "description": "stuffed flatbread",
- "unicodeVersion": "9.0",
- "digest": "ace05b5608aa3c51ad0aff66949ff5c6d06812597d1f55cb21a29834e531c46c"
- },
- "sun_with_face": {
- "category": "nature",
- "moji": "🌞",
- "description": "sun with face",
- "unicodeVersion": "6.0",
- "digest": "631ad6d36e45769ebfe03c3d9fc18d5ad8f333c58ed7f92dcc5dcb8bf7f6321e"
- },
- "sunflower": {
- "category": "nature",
- "moji": "🌻",
- "description": "sunflower",
- "unicodeVersion": "6.0",
- "digest": "ea2947ff8994128b131e8e692d6183f38553d709fafa7611f3c15d00e5e2b9d6"
- },
- "sunglasses": {
- "category": "people",
- "moji": "😎",
- "description": "smiling face with sunglasses",
- "unicodeVersion": "6.0",
- "digest": "1b2ba362ef41c55b05bf8d28df5508e8f4f2b0418c22c47ecd9e8e772ac1c19b"
- },
- "sunny": {
- "category": "nature",
- "moji": "☀",
- "description": "black sun with rays",
- "unicodeVersion": "1.1",
- "digest": "254e2e15e1e548aeb54048217501d7da60f57ebe8c9de2e61e84e0714deba7a4"
- },
- "sunrise": {
- "category": "travel",
- "moji": "🌅",
- "description": "sunrise",
- "unicodeVersion": "6.0",
- "digest": "5e1511462f5e0bfaaf865baa62eb7dd1f76ce22acebb0bd5ef27c55e3a69fed3"
- },
- "sunrise_over_mountains": {
- "category": "travel",
- "moji": "🌄",
- "description": "sunrise over mountains",
- "unicodeVersion": "6.0",
- "digest": "089412d5a9ce8f71fa184bc90cd9a092bfea41361a792cc6b9f94ac1bc741fb7"
- },
- "surfer": {
- "category": "activity",
- "moji": "🏄",
- "description": "surfer",
- "unicodeVersion": "6.0",
- "digest": "39564fb830c8bd3e37cc30f227ffa454bee97a9f5a3df9d062df656fb7cca740"
- },
- "surfer_tone1": {
- "category": "activity",
- "moji": "🏄🏻",
- "description": "surfer tone 1",
- "unicodeVersion": "8.0",
- "digest": "c47d4c1057a86878179b86d4a56e432b9cc9d34f5d3aa9b817c84be29a029904"
- },
- "surfer_tone2": {
- "category": "activity",
- "moji": "🏄🏼",
- "description": "surfer tone 2",
- "unicodeVersion": "8.0",
- "digest": "d15f802cf36c352a1817b15bc508a1983c9362e24d2707f12f7e6a2c4b1f8b90"
- },
- "surfer_tone3": {
- "category": "activity",
- "moji": "🏄🏽",
- "description": "surfer tone 3",
- "unicodeVersion": "8.0",
- "digest": "1377c1f74dd0987032b564c7ad55d0a2bb418e1f372937db50cb0e6806e9c11a"
- },
- "surfer_tone4": {
- "category": "activity",
- "moji": "🏄🏾",
- "description": "surfer tone 4",
- "unicodeVersion": "8.0",
- "digest": "6155339508cf035a4eab763083f73e6c78bdaa15ce2acba5abe808b6d4f9d9c8"
- },
- "surfer_tone5": {
- "category": "activity",
- "moji": "🏄🏿",
- "description": "surfer tone 5",
- "unicodeVersion": "8.0",
- "digest": "c3f84fd38dfe40f539fbf1201c319b990aa4fb81bb57548377a7dfb2e3ee8661"
- },
- "sushi": {
- "category": "food",
- "moji": "🍣",
- "description": "sushi",
- "unicodeVersion": "6.0",
- "digest": "a5bbbe7979621cc830f8a6860623af797530eb1d6cb4fb909f5e728ca8684864"
- },
- "suspension_railway": {
- "category": "travel",
- "moji": "🚟",
- "description": "suspension railway",
- "unicodeVersion": "6.0",
- "digest": "8c3f5852d6b7e363ef8dc14c483154ed7b65456aaf6cbfb16b3f46539de17c8d"
- },
- "sweat": {
- "category": "people",
- "moji": "😓",
- "description": "face with cold sweat",
- "unicodeVersion": "6.0",
- "digest": "54f6998fabdc88fd169a6c9013f6471608f29554dd304d3abe9ee246b4a0cb16"
- },
- "sweat_drops": {
- "category": "nature",
- "moji": "💦",
- "description": "splashing sweat symbol",
- "unicodeVersion": "6.0",
- "digest": "48642bb76350a7be33303751b18ca1150085d20070e18eb9e3617833ae406b11"
- },
- "sweat_smile": {
- "category": "people",
- "moji": "😅",
- "description": "smiling face with open mouth and cold sweat",
- "unicodeVersion": "6.0",
- "digest": "18e9821a9dd3f90342ed952660654ddbb8e46671b5e95ab88df637406b6cc0fb"
- },
- "sweet_potato": {
- "category": "food",
- "moji": "🍠",
- "description": "roasted sweet potato",
- "unicodeVersion": "6.0",
- "digest": "0a322b21e76c9c487b8a8cb158c60d6e0be1aaa0495f865262f4c69e55a870b0"
- },
- "swimmer": {
- "category": "activity",
- "moji": "🏊",
- "description": "swimmer",
- "unicodeVersion": "6.0",
- "digest": "16c5a68b9f1cc7d0f5da1f288be73a0419d059e76f22bed5f7d7d902a1320af4"
- },
- "swimmer_tone1": {
- "category": "activity",
- "moji": "🏊🏻",
- "description": "swimmer tone 1",
- "unicodeVersion": "8.0",
- "digest": "2159c9ecb0580a2183e921e3a3988643caaa56ad3037993b83e2776988a92e70"
- },
- "swimmer_tone2": {
- "category": "activity",
- "moji": "🏊🏼",
- "description": "swimmer tone 2",
- "unicodeVersion": "8.0",
- "digest": "8aeeafc91941162d71eaf9d2a2313d9af6cfdf4ea081ccacbc6a28389e0e77c0"
- },
- "swimmer_tone3": {
- "category": "activity",
- "moji": "🏊🏽",
- "description": "swimmer tone 3",
- "unicodeVersion": "8.0",
- "digest": "e6811d73ef31041bb9c602e20e1853b34d6db4774dee657851b0e1952b7a038e"
- },
- "swimmer_tone4": {
- "category": "activity",
- "moji": "🏊🏾",
- "description": "swimmer tone 4",
- "unicodeVersion": "8.0",
- "digest": "627184d5dae69aea7345661c1c721beb78ccbeae9cf6dc3f844f6368fddf1018"
- },
- "swimmer_tone5": {
- "category": "activity",
- "moji": "🏊🏿",
- "description": "swimmer tone 5",
- "unicodeVersion": "8.0",
- "digest": "db6862ca44bd4375ed8ccf2085398e4003342360f7f8325de3f7126d69716432"
- },
- "symbols": {
- "category": "symbols",
- "moji": "🔣",
- "description": "input symbol for symbols",
- "unicodeVersion": "6.0",
- "digest": "ff91761a5def3885f52b44abc14a3073800f6ca6f5a61480c902917896843dc1"
- },
- "synagogue": {
- "category": "travel",
- "moji": "🕍",
- "description": "synagogue",
- "unicodeVersion": "8.0",
- "digest": "07bcf08d94008462f001e6512e3ba3e9e70cfd57ac4e84e04b94e6f74684f51c"
- },
- "syringe": {
- "category": "objects",
- "moji": "💉",
- "description": "syringe",
- "unicodeVersion": "6.0",
- "digest": "7c1f7fcc64d14e129f02f6cdf63ba6d13839be54263c1c9c2471826583ca2431"
- },
- "taco": {
- "category": "food",
- "moji": "🌮",
- "description": "taco",
- "unicodeVersion": "8.0",
- "digest": "13802015749117fc3889d27eb8a66846bb81e2a469dfd90a66b9603b54526c5b"
- },
- "tada": {
- "category": "objects",
- "moji": "🎉",
- "description": "party popper",
- "unicodeVersion": "6.0",
- "digest": "879b8a892411b1839ad7c4a4d9ffcae074d3829bb40767c916471b06a39da9ec"
- },
- "tanabata_tree": {
- "category": "nature",
- "moji": "🎋",
- "description": "tanabata tree",
- "unicodeVersion": "6.0",
- "digest": "6ec94e277dc3027dbaf8d844dd26202c00b36602f5f73d0d2c36c087d5d674b9"
- },
- "tangerine": {
- "category": "food",
- "moji": "🍊",
- "description": "tangerine",
- "unicodeVersion": "6.0",
- "digest": "6858dcb7aa079a6639511d86328f55fd70319787efe06934573c774adec2ade0"
- },
- "taurus": {
- "category": "symbols",
- "moji": "♉",
- "description": "taurus",
- "unicodeVersion": "1.1",
- "digest": "e64c53547f42dc3e07c06f0891641395773a9cdca0adb3257ea584a3102d084f"
- },
- "taxi": {
- "category": "travel",
- "moji": "🚕",
- "description": "taxi",
- "unicodeVersion": "6.0",
- "digest": "260b37ae31fcfe5b30682f0592ec4e32ce308f9cb9574daa532099c47735252b"
- },
- "tea": {
- "category": "food",
- "moji": "🍵",
- "description": "teacup without handle",
- "unicodeVersion": "6.0",
- "digest": "520da660803cd133832badfa170c6795e2673b6881b6b52e22db3771c430cafa"
- },
- "telephone": {
- "category": "objects",
- "moji": "☎",
- "description": "black telephone",
- "unicodeVersion": "1.1",
- "digest": "63655f8f945e2b0a7bb235b7b2d118db845bea995f5d04adab25b9b1ea7fc9ae"
- },
- "telephone_receiver": {
- "category": "objects",
- "moji": "📞",
- "description": "telephone receiver",
- "unicodeVersion": "6.0",
- "digest": "83bc544dc191e7145a4d59059780fbd5c020c64f090de11816d681a504d65472"
- },
- "telescope": {
- "category": "objects",
- "moji": "🔭",
- "description": "telescope",
- "unicodeVersion": "6.0",
- "digest": "202137ea4dc1f3f544100942d597ec9a31c4c4a96f0386a77d437d2274a3fbe7"
- },
- "ten": {
- "category": "symbols",
- "moji": "🔟",
- "description": "keycap ten",
- "unicodeVersion": "6.0",
- "digest": "4474d95b39371042bf8dfac128d68f391fb10efbc9c245de5dc6aefb10544439"
- },
- "tennis": {
- "category": "activity",
- "moji": "🎾",
- "description": "tennis racquet and ball",
- "unicodeVersion": "6.0",
- "digest": "857b0f96109f4534cef496348b76036354eecdee640855fb83e0e3b253c7d914"
- },
- "tent": {
- "category": "travel",
- "moji": "⛺",
- "description": "tent",
- "unicodeVersion": "5.2",
- "digest": "a2fc1e803dc0eef13ea7d3847e1e92a3d0790b1336a7ed5675ea60069fb76676"
- },
- "thermometer": {
- "category": "objects",
- "moji": "🌡",
- "description": "thermometer",
- "unicodeVersion": "7.0",
- "digest": "4b54cd4fc758bfc9e8830d36726ba06a0ac9e0d0d397ecba99599c9bde44f4e5"
- },
- "thermometer_face": {
- "category": "people",
- "moji": "🤒",
- "description": "face with thermometer",
- "unicodeVersion": "8.0",
- "digest": "8300d80af44461b1da2aeed90203901753705ec3418a288646a00dc59da70c93"
- },
- "thinking": {
- "category": "people",
- "moji": "🤔",
- "description": "thinking face",
- "unicodeVersion": "8.0",
- "digest": "2b2d2b844f147e1be7f4c9019c54ce1b96561b4a8e5bd0af9c8d955b3ceabefa"
- },
- "third_place": {
- "category": "activity",
- "moji": "🥉",
- "description": "third place medal",
- "unicodeVersion": "9.0",
- "digest": "0afe5ea1a8963329439e0a5ed4922939cc4ab733f5fb50f40edcc2e4fafa12ed"
- },
- "thought_balloon": {
- "category": "symbols",
- "moji": "💭",
- "description": "thought balloon",
- "unicodeVersion": "6.0",
- "digest": "d6a36d105964c8184aa889193b812be4307508c10a9bf99d6eb199565be2c5cc"
- },
- "three": {
- "category": "symbols",
- "moji": "3️⃣",
- "description": "keycap digit three",
- "unicodeVersion": "3.0",
- "digest": "e66cfca0c1871d16283fcdddc7b170ad67c0a52f21d54985e4789daa1c61ee63"
- },
- "thumbsdown": {
- "category": "people",
- "moji": "👎",
- "description": "thumbs down sign",
- "unicodeVersion": "6.0",
- "digest": "e5e3594f30f8b3c59f22963c3a903ec69569e9735690cbce1c96da981cea5f91"
- },
- "thumbsdown_tone1": {
- "category": "people",
- "moji": "👎🏻",
- "description": "thumbs down sign tone 1",
- "unicodeVersion": "8.0",
- "digest": "e360d25bc6fc05243ec5ea6489fdc80702899783faa83c88686c9b73c9e8684c"
- },
- "thumbsdown_tone2": {
- "category": "people",
- "moji": "👎🏼",
- "description": "thumbs down sign tone 2",
- "unicodeVersion": "8.0",
- "digest": "bf0ad5d01e7ac0ab4d2be9db76615ff303bd01d2b4915b2b846a494aa36878fd"
- },
- "thumbsdown_tone3": {
- "category": "people",
- "moji": "👎🏽",
- "description": "thumbs down sign tone 3",
- "unicodeVersion": "8.0",
- "digest": "e3d907396c17971a6d533900dd857ad273c1b0ff1af520c6fda780a54616b3c8"
- },
- "thumbsdown_tone4": {
- "category": "people",
- "moji": "👎🏾",
- "description": "thumbs down sign tone 4",
- "unicodeVersion": "8.0",
- "digest": "31a952ef8b0fe9c0a9b04e46033e052d8104539929509010f7ab74a09b72d396"
- },
- "thumbsdown_tone5": {
- "category": "people",
- "moji": "👎🏿",
- "description": "thumbs down sign tone 5",
- "unicodeVersion": "8.0",
- "digest": "afe38eb9c879ba04556cb4bf211ef0dae63b51957389256e5af93dc7e7e94cef"
- },
- "thumbsup": {
- "category": "people",
- "moji": "👍",
- "description": "thumbs up sign",
- "unicodeVersion": "6.0",
- "digest": "38755ce0360171dd24005d6f4d6b06c2df337adb0cfd590e2e381cf44a9c24ec"
- },
- "thumbsup_tone1": {
- "category": "people",
- "moji": "👍🏻",
- "description": "thumbs up sign tone 1",
- "unicodeVersion": "8.0",
- "digest": "15492d43b5bafa46473154505b431ed81e365b6ebce507c97f509a00333420f3"
- },
- "thumbsup_tone2": {
- "category": "people",
- "moji": "👍🏼",
- "description": "thumbs up sign tone 2",
- "unicodeVersion": "8.0",
- "digest": "87f9941a2d3afba4ff5737a113cf070dcfbc3a2292ad15cb5b07459692cb0fdd"
- },
- "thumbsup_tone3": {
- "category": "people",
- "moji": "👍🏽",
- "description": "thumbs up sign tone 3",
- "unicodeVersion": "8.0",
- "digest": "f09364411db2331284b3deb85c6107049cbb41d2e5edfb50f61fc5907641a7a0"
- },
- "thumbsup_tone4": {
- "category": "people",
- "moji": "👍🏾",
- "description": "thumbs up sign tone 4",
- "unicodeVersion": "8.0",
- "digest": "8c1322a624b0ebcab1f08d7675df39383eac8b1d4b627889d3393b0d3cdc946d"
- },
- "thumbsup_tone5": {
- "category": "people",
- "moji": "👍🏿",
- "description": "thumbs up sign tone 5",
- "unicodeVersion": "8.0",
- "digest": "6dec547ee282457cbbcb7e3dffcf804188aed47960a70069df81a547a8f40df9"
- },
- "thunder_cloud_rain": {
- "category": "nature",
- "moji": "⛈",
- "description": "thunder cloud and rain",
- "unicodeVersion": "5.2",
- "digest": "c1d9417ce640885540743b4fdb7df6efcccb6a04a31a1c62780b4c952c9f11d4"
- },
- "ticket": {
- "category": "activity",
- "moji": "🎫",
- "description": "ticket",
- "unicodeVersion": "6.0",
- "digest": "0575c271a22acdc294505b2aae0f288e67b0d78d9b60af97420806ac893e4ea9"
- },
- "tickets": {
- "category": "activity",
- "moji": "🎟",
- "description": "admission tickets",
- "unicodeVersion": "7.0",
- "digest": "f70e7c3f2a059c85b36587941a861dc9e41c8db0475ddbe2d5dd50ad440dd579"
- },
- "tiger": {
- "category": "nature",
- "moji": "🐯",
- "description": "tiger face",
- "unicodeVersion": "6.0",
- "digest": "0ea11ea4b71adee37b67019634153f09ca6d5b762d4c9507a3d3f7b47ff59274"
- },
- "tiger2": {
- "category": "nature",
- "moji": "🐅",
- "description": "tiger",
- "unicodeVersion": "6.0",
- "digest": "84d80ae536dfad345dd878970c339451d4189f73f0d786cafcaa2421dab9fe97"
- },
- "timer": {
- "category": "objects",
- "moji": "⏲",
- "description": "timer clock",
- "unicodeVersion": "6.0",
- "digest": "b94e3cdd84834063f72e333339b27f5aeaa2e8b3082eff51a871d706f0038349"
- },
- "tired_face": {
- "category": "people",
- "moji": "😫",
- "description": "tired face",
- "unicodeVersion": "6.0",
- "digest": "60d0656f21c7937c3f2e9c5a90d1dfd2deee068804fb17d813e8b6e9c9f994d5"
- },
- "tm": {
- "category": "symbols",
- "moji": "™️",
- "description": "trade mark sign",
- "unicodeVersion": "1.1",
- "digest": "aaa0898628f473e4e8df05d707b2d49a854ce3fbb78b8ca37ac0560b329fd8ea"
- },
- "toilet": {
- "category": "objects",
- "moji": "🚽",
- "description": "toilet",
- "unicodeVersion": "6.0",
- "digest": "ae590884c0bf0b5f5d721b4d7791ea3878a9514642f6a57ad4784b2ca7ce6fbd"
- },
- "tokyo_tower": {
- "category": "travel",
- "moji": "🗼",
- "description": "tokyo tower",
- "unicodeVersion": "6.0",
- "digest": "e012cdbb4648219ccee443cc09fa2946b99dadb483aeca5a3fdee39270dd3061"
- },
- "tomato": {
- "category": "food",
- "moji": "🍅",
- "description": "tomato",
- "unicodeVersion": "6.0",
- "digest": "9fc42d1837bf67d7845f2a00c83c6b8001f96b6bb350f009063d211b20e96c16"
- },
- "tone1": {
- "category": "modifier",
- "moji": "🏻",
- "description": "emoji modifier Fitzpatrick type-1-2",
- "unicodeVersion": "8.0",
- "digest": "545d866024aa7d4de2e2254420a9d8cca667534672d9e7122bd3e67cf81b694b"
- },
- "tone2": {
- "category": "modifier",
- "moji": "🏼",
- "description": "emoji modifier Fitzpatrick type-3",
- "unicodeVersion": "8.0",
- "digest": "c2f93946364e79ed130e3c355416ada791c09d9b3d1bbfa44d03324bfabd642e"
- },
- "tone3": {
- "category": "modifier",
- "moji": "🏽",
- "description": "emoji modifier Fitzpatrick type-4",
- "unicodeVersion": "8.0",
- "digest": "ea55f80c25ec5df8232d509913b8f5e3260ecc98a2db0f5de333d7e3c9f05bb2"
- },
- "tone4": {
- "category": "modifier",
- "moji": "🏾",
- "description": "emoji modifier Fitzpatrick type-5",
- "unicodeVersion": "8.0",
- "digest": "676bd7f45026066bb72a6b5344e288c6b42ce9524eb0065d50d3354cc716cc6a"
- },
- "tone5": {
- "category": "modifier",
- "moji": "🏿",
- "description": "emoji modifier Fitzpatrick type-6",
- "unicodeVersion": "8.0",
- "digest": "b9ab9e2f6307e2e5b8e0560d6b3048f2d0cd58e11c90b03d90688b91fdb4b5f8"
- },
- "tongue": {
- "category": "people",
- "moji": "👅",
- "description": "tongue",
- "unicodeVersion": "6.0",
- "digest": "da75f4b8859b698b941cd091e1d3bf4be3c4e86bb72b2407b9d7b9abe063ed84"
- },
- "tools": {
- "category": "objects",
- "moji": "🛠",
- "description": "hammer and wrench",
- "unicodeVersion": "7.0",
- "digest": "f74b73f7a143d6e5a4efe0b293d998d7dfb681b23f0d6764137c6d9373a28374"
- },
- "top": {
- "category": "symbols",
- "moji": "🔝",
- "description": "top with upwards arrow above",
- "unicodeVersion": "6.0",
- "digest": "69250cda059411b279e6503ff1afac1188b2507d861db7efc956d7758468aa01"
- },
- "tophat": {
- "category": "people",
- "moji": "🎩",
- "description": "top hat",
- "unicodeVersion": "6.0",
- "digest": "dff54bdac5a4d1df8e406a5dfa517c5ea60f9ebb6952f27b2780878478c4c8dc"
- },
- "track_next": {
- "category": "symbols",
- "moji": "⏭",
- "description": "black right-pointing double triangle with vertical bar",
- "unicodeVersion": "6.0",
- "digest": "adc496351d784f266e6addac74e29d328d24f3e6d3f4a2f1270e88de00f48116"
- },
- "track_previous": {
- "category": "symbols",
- "moji": "⏮",
- "description": "black left-pointing double triangle with vertical bar",
- "unicodeVersion": "6.0",
- "digest": "aaa9c3c003a2839067dde8aab59c2887532b8f00d00726baa33c995b21b06dcc"
- },
- "trackball": {
- "category": "objects",
- "moji": "🖲",
- "description": "trackball",
- "unicodeVersion": "7.0",
- "digest": "80003d0b886adf81afc07427b73b6ba462032c9fad318d1dcc142eae6e943238"
- },
- "tractor": {
- "category": "travel",
- "moji": "🚜",
- "description": "tractor",
- "unicodeVersion": "6.0",
- "digest": "0bec7945ef52027a634bfbe75b71902e31c03a8d91814f3c37c2d1dbfa21de10"
- },
- "traffic_light": {
- "category": "travel",
- "moji": "🚥",
- "description": "horizontal traffic light",
- "unicodeVersion": "6.0",
- "digest": "7ba4ce5e0bab82e8a78afb05e7705c1dfcaebba6bce6eec0dae63fd62896cd38"
- },
- "train": {
- "category": "travel",
- "moji": "🚋",
- "description": "Tram Car",
- "unicodeVersion": "6.0",
- "digest": "2b991f1b04ea9a364de103d751d4e3ca7d263b29e169517cf0b6bc098c0544fd"
- },
- "train2": {
- "category": "travel",
- "moji": "🚆",
- "description": "train",
- "unicodeVersion": "6.0",
- "digest": "4809681fd588ad64f82483ecb825bb16ff387e31f45e5fc5637f0c2547acd42f"
- },
- "tram": {
- "category": "travel",
- "moji": "🚊",
- "description": "tram",
- "unicodeVersion": "6.0",
- "digest": "ab9d03c841a0ef60218aae50fe2b2e0ee045052b8b8f73c001c4203199209ba0"
- },
- "triangular_flag_on_post": {
- "category": "objects",
- "moji": "🚩",
- "description": "triangular flag on post",
- "unicodeVersion": "6.0",
- "digest": "135c571161ba6bd55d4940366202164a6c4c325064a7ebc76fbdb3f92428fd73"
- },
- "triangular_ruler": {
- "category": "objects",
- "moji": "📐",
- "description": "triangular ruler",
- "unicodeVersion": "6.0",
- "digest": "8db7d5546f2c1403a5dbcd87216d15c9ded1c137def938f6cff3e14282b3b659"
- },
- "trident": {
- "category": "symbols",
- "moji": "🔱",
- "description": "trident emblem",
- "unicodeVersion": "6.0",
- "digest": "c9fcbc5e98ed51cd228a1b0eedde2851624c3a7fc354abab9efd96eaa52dcb52"
- },
- "triumph": {
- "category": "people",
- "moji": "😤",
- "description": "face with look of triumph",
- "unicodeVersion": "6.0",
- "digest": "b258f96aa69a0c5bbe672097bb58d0b7bd6c1dfcc93e66f73d632c2a42c9ecc4"
- },
- "trolleybus": {
- "category": "travel",
- "moji": "🚎",
- "description": "trolleybus",
- "unicodeVersion": "6.0",
- "digest": "587261051e9e8a6c1354406c62830d8895eaa83b1d9dc6df7516a835788805ec"
- },
- "trophy": {
- "category": "activity",
- "moji": "🏆",
- "description": "trophy",
- "unicodeVersion": "6.0",
- "digest": "33ba0ae0619cb3c2cfa350a36e535601d327b7a0cb7371f2686790336559f912"
- },
- "tropical_drink": {
- "category": "food",
- "moji": "🍹",
- "description": "tropical drink",
- "unicodeVersion": "6.0",
- "digest": "085a51de7c6df9e8734450b0b7b30577c3afaed56cf02f6a481808c2d90a0c13"
- },
- "tropical_fish": {
- "category": "nature",
- "moji": "🐠",
- "description": "tropical fish",
- "unicodeVersion": "6.0",
- "digest": "26974903529bbda281c4f1302d8ced6eeca6f3d316ef0bd4ab6617062bd10ca9"
- },
- "truck": {
- "category": "travel",
- "moji": "🚚",
- "description": "delivery truck",
- "unicodeVersion": "6.0",
- "digest": "c58cdc9790438031e3acd58bd224b83428a6e89be5cec3af01233d257d7b54a6"
- },
- "trumpet": {
- "category": "activity",
- "moji": "🎺",
- "description": "trumpet",
- "unicodeVersion": "6.0",
- "digest": "21803bcdcfd7681ce77b8fdd30b115a4f67fb8f2cb91908ac9eb02613c413719"
- },
- "tulip": {
- "category": "nature",
- "moji": "🌷",
- "description": "tulip",
- "unicodeVersion": "6.0",
- "digest": "78b7615137fedb534bb1a760b1c7bfdd09b60e1c770ba21ee9f5a3799aa7c93b"
- },
- "tumbler_glass": {
- "category": "food",
- "moji": "🥃",
- "description": "tumbler glass",
- "unicodeVersion": "9.0",
- "digest": "41ca844a7ae31e28bac5320298ff16c2adf9124b9a649ce5112e47ddce42ab06"
- },
- "turkey": {
- "category": "nature",
- "moji": "🦃",
- "description": "turkey",
- "unicodeVersion": "8.0",
- "digest": "2b141b75a1df1c8e347fe96a5193060d6feb230e4ced5976e3a22ab4a145887b"
- },
- "turtle": {
- "category": "nature",
- "moji": "🐢",
- "description": "turtle",
- "unicodeVersion": "6.0",
- "digest": "cb9ebbbd5c861943b8e52ad447959cb489edcb64c93a878a2db00060220e56da"
- },
- "tv": {
- "category": "objects",
- "moji": "📺",
- "description": "television",
- "unicodeVersion": "6.0",
- "digest": "86ab2eef07bed93f56e29e20ef67d566ec7b4b1e96463ba3f9614a1db28aec11"
- },
- "twisted_rightwards_arrows": {
- "category": "symbols",
- "moji": "🔀",
- "description": "twisted rightwards arrows",
- "unicodeVersion": "6.0",
- "digest": "f559a51e80d26d7bb196ea380936ff60c4a281c3943cf6e86aaebeacce8d957f"
- },
- "two": {
- "category": "symbols",
- "moji": "2️⃣",
- "description": "keycap digit two",
- "unicodeVersion": "3.0",
- "digest": "d091a45f6754587ac8602259955a5d2d1e41d090a1da1c81e46f286ab3de1385"
- },
- "two_hearts": {
- "category": "symbols",
- "moji": "💕",
- "description": "two hearts",
- "unicodeVersion": "6.0",
- "digest": "52fba958d8153422ae667827dd2dd44a58bf36ac3f7d3d9433527b6a92b7e6e7"
- },
- "two_men_holding_hands": {
- "category": "people",
- "moji": "👬",
- "description": "two men holding hands",
- "unicodeVersion": "6.0",
- "digest": "8953ff520ff541f4ee8001ef4dfc22462b6ebf15f2e1adf9ba46d0c6384a0091"
- },
- "two_women_holding_hands": {
- "category": "people",
- "moji": "👭",
- "description": "two women holding hands",
- "unicodeVersion": "6.0",
- "digest": "4a6dc2a4b900084faa7b934300b1f07717e41971a9eb9d7830cec83a78c14e46"
- },
- "u5272": {
- "category": "symbols",
- "moji": "🈹",
- "description": "squared cjk unified ideograph-5272",
- "unicodeVersion": "6.0",
- "digest": "146800348d2954ab707184573fd7a5be20971d6df655f37829f88a8678d4f54b"
- },
- "u5408": {
- "category": "symbols",
- "moji": "🈴",
- "description": "squared cjk unified ideograph-5408",
- "unicodeVersion": "6.0",
- "digest": "c65c6b24648ec959bb497872003adf9f29db19f3bc3ffc5f3c6d03ec36f7e7ed"
- },
- "u55b6": {
- "category": "symbols",
- "moji": "🈺",
- "description": "squared cjk unified ideograph-55b6",
- "unicodeVersion": "6.0",
- "digest": "0c441fbe4e8527c04c44a297150b2c3dab5f5ba51acffd1adc3109057ec20522"
- },
- "u6307": {
- "category": "symbols",
- "moji": "🈯",
- "description": "squared cjk unified ideograph-6307",
- "unicodeVersion": "5.2",
- "digest": "6ecd33c04af54469548189adc89a12ea3677b7d76af6349221bab3ac17c51011"
- },
- "u6708": {
- "category": "symbols",
- "moji": "🈷",
- "description": "squared cjk unified ideograph-6708",
- "unicodeVersion": "6.0",
- "digest": "de0a0568e1a0de7653dac4dd093ee95027087b301d914752bafbda4648724847"
- },
- "u6709": {
- "category": "symbols",
- "moji": "🈶",
- "description": "squared cjk unified ideograph-6709",
- "unicodeVersion": "6.0",
- "digest": "5370fc51eed9c35a7a695881cdb3218b2fda9d70b1a887a5f2dcd7bdd0f5f3bc"
- },
- "u6e80": {
- "category": "symbols",
- "moji": "🈵",
- "description": "squared cjk unified ideograph-6e80",
- "unicodeVersion": "6.0",
- "digest": "cc9fbe7d7a29f3d309e0c7dedacf3b19afb4f0689aebcb0ffbdf06febd961842"
- },
- "u7121": {
- "category": "symbols",
- "moji": "🈚",
- "description": "squared cjk unified ideograph-7121",
- "unicodeVersion": "5.2",
- "digest": "3df28fb3345962cc97b32391bcb9dfd1df82bfadf1dc3567122fd086aff59cb3"
- },
- "u7533": {
- "category": "symbols",
- "moji": "🈸",
- "description": "squared cjk unified ideograph-7533",
- "unicodeVersion": "6.0",
- "digest": "014087496357083b2fa0f6372f3c768c9d0cde6f290460c8b4ecfa322955a065"
- },
- "u7981": {
- "category": "symbols",
- "moji": "🈲",
- "description": "squared cjk unified ideograph-7981",
- "unicodeVersion": "6.0",
- "digest": "e61e1ed43e0aa5747ad99050d48550ac23a8c6bfa7de7114b03a17eb0366872a"
- },
- "u7a7a": {
- "category": "symbols",
- "moji": "🈳",
- "description": "squared cjk unified ideograph-7a7a",
- "unicodeVersion": "6.0",
- "digest": "d306d00de7a978823adf885d3beb23c41359a2bb804425b8652f59f5ceadd40d"
- },
- "umbrella": {
- "category": "nature",
- "moji": "☔",
- "description": "umbrella with rain drops",
- "unicodeVersion": "4.0",
- "digest": "99c2f2a4331ff3f17192394ce766808e725e0701cf903cea606efb5f1d4b3f4a"
- },
- "umbrella2": {
- "category": "nature",
- "moji": "☂",
- "description": "umbrella",
- "unicodeVersion": "1.1",
- "digest": "c8e5f34916627bd8053727b3ae40c5fb1df6bad76049e925696140825b413af4"
- },
- "unamused": {
- "category": "people",
- "moji": "😒",
- "description": "unamused face",
- "unicodeVersion": "6.0",
- "digest": "68eaad1164a9cfdcfb28e6e247ab733d0698a4ddd8f9c72add082d28d3a74445"
- },
- "underage": {
- "category": "symbols",
- "moji": "🔞",
- "description": "no one under eighteen symbol",
- "unicodeVersion": "6.0",
- "digest": "afdd3883748acb01bb18bfe034c39b4510957c7bfbdb9cec33b4e6b4a0068089"
- },
- "unicorn": {
- "category": "nature",
- "moji": "🦄",
- "description": "unicorn face",
- "unicodeVersion": "8.0",
- "digest": "00d784632f584c953340b1f1474be2a1ab6f065625e69e2d06dabd38ab45531c"
- },
- "unlock": {
- "category": "objects",
- "moji": "🔓",
- "description": "open lock",
- "unicodeVersion": "6.0",
- "digest": "129328bd903055394ded6f0427d24c8f4ffc1a8ccb0cc831d1343b5c6c8d4416"
- },
- "up": {
- "category": "symbols",
- "moji": "🆙",
- "description": "squared up with exclamation mark",
- "unicodeVersion": "6.0",
- "digest": "807046ea55d94a542e355a6cf9c6bab5fe8523513669211ab1c13765348ff41a"
- },
- "upside_down": {
- "category": "people",
- "moji": "🙃",
- "description": "upside-down face",
- "unicodeVersion": "8.0",
- "digest": "3211b742f7fefdca6b0e817fb45070d6a306e3d31debcfd11d006ea24ba08983"
- },
- "urn": {
- "category": "objects",
- "moji": "⚱",
- "description": "funeral urn",
- "unicodeVersion": "4.1",
- "digest": "0730518fad9fab0d5070432a6e771d6526349fb075ed9e42b046b88464b0f13c"
- },
- "v": {
- "category": "people",
- "moji": "✌",
- "description": "victory hand",
- "unicodeVersion": "1.1",
- "digest": "cf0a1553b56d27c678ee71819933807339d0134eb71119aecc0c185bcd922996"
- },
- "v_tone1": {
- "category": "people",
- "moji": "✌🏻",
- "description": "victory hand tone 1",
- "unicodeVersion": "8.0",
- "digest": "1d4b156f48968318917284d695cf72f56f2ad8d2ed318f3886fa1eca5a27439f"
- },
- "v_tone2": {
- "category": "people",
- "moji": "✌🏼",
- "description": "victory hand tone 2",
- "unicodeVersion": "8.0",
- "digest": "6d842f89c4fe8d344d7748b4de74733a7b680566589556e542f808b28de8c12a"
- },
- "v_tone3": {
- "category": "people",
- "moji": "✌🏽",
- "description": "victory hand tone 3",
- "unicodeVersion": "8.0",
- "digest": "0125d0d20a51b11716399dcb0e0b81220a1fe755ea7a2e2fed22c5688516ae52"
- },
- "v_tone4": {
- "category": "people",
- "moji": "✌🏾",
- "description": "victory hand tone 4",
- "unicodeVersion": "8.0",
- "digest": "10502f0b7ce2aaaf9e9823f74f7cbe88503ec5bd614218b75c8f277b8a280328"
- },
- "v_tone5": {
- "category": "people",
- "moji": "✌🏿",
- "description": "victory hand tone 5",
- "unicodeVersion": "8.0",
- "digest": "1ea229068485d71074f7b6c782c6d275d7ff0a039f8c4ace5ba2700ee94315c4"
- },
- "vertical_traffic_light": {
- "category": "travel",
- "moji": "🚦",
- "description": "vertical traffic light",
- "unicodeVersion": "6.0",
- "digest": "ff094eb787c114159f19f263994531db073cd9b3bd98c55fe4913bfa9bda53ec"
- },
- "vhs": {
- "category": "objects",
- "moji": "📼",
- "description": "videocassette",
- "unicodeVersion": "6.0",
- "digest": "7fbd915d2b660e32fc5720179a113deea2997601f8dc0f7dcf4ec754f4d8d17b"
- },
- "vibration_mode": {
- "category": "symbols",
- "moji": "📳",
- "description": "vibration mode",
- "unicodeVersion": "6.0",
- "digest": "15ef296e1ef2747dfc47d8999ff7bbcca42a2ea682d57e224064335d9fa1c84e"
- },
- "video_camera": {
- "category": "objects",
- "moji": "📹",
- "description": "video camera",
- "unicodeVersion": "6.0",
- "digest": "b297b7675c9e69638a29f5332a4024fd68675101d03a059d90f151534de1a3b5"
- },
- "video_game": {
- "category": "activity",
- "moji": "🎮",
- "description": "video game",
- "unicodeVersion": "6.0",
- "digest": "95a218fcb12095024a77ee4826d0574190b5ebb8878d0fba7a640dcbeb3b78ba"
- },
- "violin": {
- "category": "activity",
- "moji": "🎻",
- "description": "violin",
- "unicodeVersion": "6.0",
- "digest": "3adf18cfe5778c508be936827ff1812503e02b8ebaf3db45053f0fab962cca39"
- },
- "virgo": {
- "category": "symbols",
- "moji": "♍",
- "description": "virgo",
- "unicodeVersion": "1.1",
- "digest": "2cae076c31fe134ca69caef07db75283f72c99f8fd746b1c1fff6a897f613655"
- },
- "volcano": {
- "category": "travel",
- "moji": "🌋",
- "description": "volcano",
- "unicodeVersion": "6.0",
- "digest": "75b53e25406d31b630fd1bb75c363044129f481f24f7d225a6d30eef5257f92c"
- },
- "volleyball": {
- "category": "activity",
- "moji": "🏐",
- "description": "volleyball",
- "unicodeVersion": "8.0",
- "digest": "a9dc3a3516d1e22aededc7198fdf3a4f304b7c2f7fa528806c2e0bdfe320d007"
- },
- "vs": {
- "category": "symbols",
- "moji": "🆚",
- "description": "squared vs",
- "unicodeVersion": "6.0",
- "digest": "1e67a891ce028db2e6fb8506879d3c0edbf024012a75a5073e89aa834c6f314e"
- },
- "vulcan": {
- "category": "people",
- "moji": "🖖",
- "description": "raised hand with part between middle and ring fingers",
- "unicodeVersion": "7.0",
- "digest": "829687cca319f7293457db7d49b7eb236c681def71b96711556534c6d9123279"
- },
- "vulcan_tone1": {
- "category": "people",
- "moji": "🖖🏻",
- "description": "raised hand with part between middle and ring fingers tone 1",
- "unicodeVersion": "8.0",
- "digest": "993550c5e6d01f173ce710e8d721474f21359f706cff863f913db3e31212ab56"
- },
- "vulcan_tone2": {
- "category": "people",
- "moji": "🖖🏼",
- "description": "raised hand with part between middle and ring fingers tone 2",
- "unicodeVersion": "8.0",
- "digest": "fad2b5ba5fef661214bee2d43a93d2cedb9024d1ba6d1f0369e76e8167926156"
- },
- "vulcan_tone3": {
- "category": "people",
- "moji": "🖖🏽",
- "description": "raised hand with part between middle and ring fingers tone 3",
- "unicodeVersion": "8.0",
- "digest": "344f09198268734de3a4f300b410a65c6a35d2ff958e7b675329e5ddffd1dd3f"
- },
- "vulcan_tone4": {
- "category": "people",
- "moji": "🖖🏾",
- "description": "raised hand with part between middle and ring fingers tone 4",
- "unicodeVersion": "8.0",
- "digest": "92f84231b71044b20d6132617fd1d2553472d3402175d85a42e24a3e5b40316d"
- },
- "vulcan_tone5": {
- "category": "people",
- "moji": "🖖🏿",
- "description": "raised hand with part between middle and ring fingers tone 5",
- "unicodeVersion": "8.0",
- "digest": "ab3aec60fd46b425d0ff4bed7e96f25007b6943e6e46b1754e695ef5e289c7a5"
- },
- "walking": {
- "category": "people",
- "moji": "🚶",
- "description": "pedestrian",
- "unicodeVersion": "6.0",
- "digest": "595b89b7ed1359e120f4aeeaccc8dbce01fc96358ce6b9501b6f4ab6559428f4"
- },
- "walking_tone1": {
- "category": "people",
- "moji": "🚶🏻",
- "description": "pedestrian tone 1",
- "unicodeVersion": "8.0",
- "digest": "794562b94ed8c825c4facad1b3d8fca0c90f49582209cc0f0923e33d2d5a083d"
- },
- "walking_tone2": {
- "category": "people",
- "moji": "🚶🏼",
- "description": "pedestrian tone 2",
- "unicodeVersion": "8.0",
- "digest": "3fc7452b3b324a3a9f7a34faa82175865681248094f13ea7977ee1eb64e6e201"
- },
- "walking_tone3": {
- "category": "people",
- "moji": "🚶🏽",
- "description": "pedestrian tone 3",
- "unicodeVersion": "8.0",
- "digest": "b9200fae09036c6919e8a348b51a2fe09e2e2909120077000dcba0fd62fb9fa5"
- },
- "walking_tone4": {
- "category": "people",
- "moji": "🚶🏾",
- "description": "pedestrian tone 4",
- "unicodeVersion": "8.0",
- "digest": "c1dbd3327bed96613834dd05dbf43bd233f5d4ab96b1c6f42684ecc690d62fa7"
- },
- "walking_tone5": {
- "category": "people",
- "moji": "🚶🏿",
- "description": "pedestrian tone 5",
- "unicodeVersion": "8.0",
- "digest": "60e0ceb95d5c555835dda6b1fa368938f50e2ee6ccbda723f2ad9c511fd9d16b"
- },
- "waning_crescent_moon": {
- "category": "nature",
- "moji": "🌘",
- "description": "waning crescent moon symbol",
- "unicodeVersion": "6.0",
- "digest": "5790b3f1f581dd7e5480978f31a8c2ed67bca238a2f26eb6f415a2dc3b6ff7c5"
- },
- "waning_gibbous_moon": {
- "category": "nature",
- "moji": "🌖",
- "description": "waning gibbous moon symbol",
- "unicodeVersion": "6.0",
- "digest": "bba224edaa61c1f00f4ea0679ebb96e15090c2948463bc7414734fd13307a3cc"
- },
- "warning": {
- "category": "symbols",
- "moji": "⚠",
- "description": "warning sign",
- "unicodeVersion": "4.0",
- "digest": "1d6cf2ec8990304aaca53a2eecd878c17e4eb8685c48d3be59c8f0a1cfb66202"
- },
- "wastebasket": {
- "category": "objects",
- "moji": "🗑",
- "description": "wastebasket",
- "unicodeVersion": "7.0",
- "digest": "2a099e1431b96a2de10f79ddb319a1bcdc74d5bd7d05dee9a27b67274e4d86b0"
- },
- "watch": {
- "category": "objects",
- "moji": "⌚",
- "description": "watch",
- "unicodeVersion": "1.1",
- "digest": "1e540e8c6856ebfab897c71132525d2f3a1a51513d86423b862116515489f55b"
- },
- "water_buffalo": {
- "category": "nature",
- "moji": "🐃",
- "description": "water buffalo",
- "unicodeVersion": "6.0",
- "digest": "dd9f6609e0ea97f610ab30e34c6564fe1504a54e2bd258fa64601cdbc91de177"
- },
- "water_polo": {
- "category": "activity",
- "moji": "🤽",
- "description": "water polo",
- "unicodeVersion": "9.0",
- "digest": "b1ddaec4c4a506a89462a8d72173e44e0766c27e936e41d894de7ffc3da48236"
- },
- "water_polo_tone1": {
- "category": "activity",
- "moji": "🤽🏻",
- "description": "water polo tone 1",
- "unicodeVersion": "9.0",
- "digest": "a86b73dac5b378283ca59e0985b98524c813aa34bc08e0c3bb5e0279a572adbf"
- },
- "water_polo_tone2": {
- "category": "activity",
- "moji": "🤽🏼",
- "description": "water polo tone 2",
- "unicodeVersion": "9.0",
- "digest": "080f98b0afbc5a6a087ca9774c817717f8bae0bb2ff7d2968eecebeb7ef8fee6"
- },
- "water_polo_tone3": {
- "category": "activity",
- "moji": "🤽🏽",
- "description": "water polo tone 3",
- "unicodeVersion": "9.0",
- "digest": "08a235850af9851f27148f53bd3e6f29668ce5d37afaaab0226a59195139e41c"
- },
- "water_polo_tone4": {
- "category": "activity",
- "moji": "🤽🏾",
- "description": "water polo tone 4",
- "unicodeVersion": "9.0",
- "digest": "7e69265f86e139af75bc1d1bea03c21b7f2cc445e4b7a767b0515ffa7be1a99c"
- },
- "water_polo_tone5": {
- "category": "activity",
- "moji": "🤽🏿",
- "description": "water polo tone 5",
- "unicodeVersion": "9.0",
- "digest": "db363a1e0a5f9b9d807258799d12e0a9010c99c383a4382f0f9a99feff2fd683"
- },
- "watermelon": {
- "category": "food",
- "moji": "🍉",
- "description": "watermelon",
- "unicodeVersion": "6.0",
- "digest": "8a18d12c6fb648b5cadc8d3ff390b9e24f5505a649055a2d4c840df7eb6bad78"
- },
- "wave": {
- "category": "people",
- "moji": "👋",
- "description": "waving hand sign",
- "unicodeVersion": "6.0",
- "digest": "5b877d50f49e858c453871fc380f0449633870118d487217c0a1f7f9cab02a06"
- },
- "wave_tone1": {
- "category": "people",
- "moji": "👋🏻",
- "description": "waving hand sign tone 1",
- "unicodeVersion": "8.0",
- "digest": "d27aa7181be2fab9d0281889496ab100a6a9473d7b1b4b0b4bcaa7523a311706"
- },
- "wave_tone2": {
- "category": "people",
- "moji": "👋🏼",
- "description": "waving hand sign tone 2",
- "unicodeVersion": "8.0",
- "digest": "7656b85268eb3318a0e8f954d334ca585c780da567a9a57ffdf24ecc3758e123"
- },
- "wave_tone3": {
- "category": "people",
- "moji": "👋🏽",
- "description": "waving hand sign tone 3",
- "unicodeVersion": "8.0",
- "digest": "002912d69d16d423253db2e90d04164b2a861847dd6eff31f9a28f32e8720c9b"
- },
- "wave_tone4": {
- "category": "people",
- "moji": "👋🏾",
- "description": "waving hand sign tone 4",
- "unicodeVersion": "8.0",
- "digest": "8e91cbf4b2eb22caa7c06e816d4083a861882b38cced8c59b5f19f7371114044"
- },
- "wave_tone5": {
- "category": "people",
- "moji": "👋🏿",
- "description": "waving hand sign tone 5",
- "unicodeVersion": "8.0",
- "digest": "40f696691a3ee439029d7914abe0ccb5efe66bab0ed2c057080991fc878eb0f4"
- },
- "wavy_dash": {
- "category": "symbols",
- "moji": "〰",
- "description": "wavy dash",
- "unicodeVersion": "1.1",
- "digest": "d7bcb62068abe4e1c168ba0445c1d3d1b1e76373bbe6f2d84f4d78b38be524c7"
- },
- "waxing_crescent_moon": {
- "category": "nature",
- "moji": "🌒",
- "description": "waxing crescent moon symbol",
- "unicodeVersion": "6.0",
- "digest": "a3534c77cf0a8da83fc2cef14481983bafc745e35da96231259dc926cd15937d"
- },
- "waxing_gibbous_moon": {
- "category": "nature",
- "moji": "🌔",
- "description": "waxing gibbous moon symbol",
- "unicodeVersion": "6.0",
- "digest": "46fc534be196723eabfe1a8be62217c1cff70e2ea806ff5bc15e9e09c116d9c1"
- },
- "wc": {
- "category": "symbols",
- "moji": "🚾",
- "description": "water closet",
- "unicodeVersion": "6.0",
- "digest": "a1576f1731a68646c9de74c135674955eee3df8f740437ab9ef04a45a49b282c"
- },
- "weary": {
- "category": "people",
- "moji": "😩",
- "description": "weary face",
- "unicodeVersion": "6.0",
- "digest": "44fd697167f1403eaf6bc6778a394dee514f900a964bb9e7b6a45aac86a5d985"
- },
- "wedding": {
- "category": "travel",
- "moji": "💒",
- "description": "wedding",
- "unicodeVersion": "6.0",
- "digest": "bdc97890b2ccd38285d4d9832e700c0cb99cf740305b37eac5cee1bf03137a92"
- },
- "whale": {
- "category": "nature",
- "moji": "🐳",
- "description": "spouting whale",
- "unicodeVersion": "6.0",
- "digest": "6a5de13dec0e0bbb05c9b3222ce04c16c079367124e59a49d015bbbee7a174a7"
- },
- "whale2": {
- "category": "nature",
- "moji": "🐋",
- "description": "whale",
- "unicodeVersion": "6.0",
- "digest": "939087b5e3b24f0cc27978a8777eec9c38f6e8bafc98200ff2606c3648fa589e"
- },
- "wheel_of_dharma": {
- "category": "symbols",
- "moji": "☸",
- "description": "wheel of dharma",
- "unicodeVersion": "1.1",
- "digest": "bdc92990dc2dcde7de136ed934e9f2476dc011257ffbeeb5dc45e1f86e26a067"
- },
- "wheelchair": {
- "category": "symbols",
- "moji": "♿",
- "description": "wheelchair symbol",
- "unicodeVersion": "4.1",
- "digest": "805e8f94922c2214849af36b2de79cb812472cf6c3cf7c55dbf7fe9e11f0380b"
- },
- "white_check_mark": {
- "category": "symbols",
- "moji": "✅",
- "description": "white heavy check mark",
- "unicodeVersion": "6.0",
- "digest": "6f37f4b2dd017d42bb070d2544dce135a1c11203c5cb537c760b3c90d17bc0c3"
- },
- "white_circle": {
- "category": "symbols",
- "moji": "⚪",
- "description": "medium white circle",
- "unicodeVersion": "4.1",
- "digest": "386429066322ba83e4790d7e930c167a6789f978f65e2131f0534230f196d40a"
- },
- "white_flower": {
- "category": "symbols",
- "moji": "💮",
- "description": "white flower",
- "unicodeVersion": "6.0",
- "digest": "30a1761e5672133bf4172bfecad661e951488c89e22ceee88d04fd3bcbd7e6d1"
- },
- "white_large_square": {
- "category": "symbols",
- "moji": "⬜",
- "description": "white large square",
- "unicodeVersion": "5.1",
- "digest": "a56e16d3a7778cfec18b031b9f0f04aa008d3b81a2dc4642366f7d92adc1d862"
- },
- "white_medium_small_square": {
- "category": "symbols",
- "moji": "◽",
- "description": "white medium small square",
- "unicodeVersion": "3.2",
- "digest": "9950d2efd6bca2b2583f670c389f839b5346b9e608eb9b00628b196f9d6b37a3"
- },
- "white_medium_square": {
- "category": "symbols",
- "moji": "◻",
- "description": "white medium square",
- "unicodeVersion": "3.2",
- "digest": "0328185858eb63e113136674509fb89f43ab5306449345e2e65d1ee2d616a54d"
- },
- "white_small_square": {
- "category": "symbols",
- "moji": "▫",
- "description": "white small square",
- "unicodeVersion": "1.1",
- "digest": "a805e7c1edcf6668a6bdc8324e9ec78da8aa4a79f789f55ce6828337b2f8e43c"
- },
- "white_square_button": {
- "category": "symbols",
- "moji": "🔳",
- "description": "white square button",
- "unicodeVersion": "6.0",
- "digest": "bc65aefa55ddee130c006624bcd381313eba7261c99f8b273a938d273b84570a"
- },
- "white_sun_cloud": {
- "category": "nature",
- "moji": "🌥",
- "description": "white sun behind cloud",
- "unicodeVersion": "7.0",
- "digest": "78c9ade346888e8758e56107bed5bd0a16b4da60ef591cb71e1c68625feec8c8"
- },
- "white_sun_rain_cloud": {
- "category": "nature",
- "moji": "🌦",
- "description": "white sun behind cloud with rain",
- "unicodeVersion": "7.0",
- "digest": "7d95f929a5679e52f4c9f93d2fe87d33524e38ab08115cd9a20aa481611f83ab"
- },
- "white_sun_small_cloud": {
- "category": "nature",
- "moji": "🌤",
- "description": "white sun with small cloud",
- "unicodeVersion": "7.0",
- "digest": "7f6a12cec242c9fd4ece3a014eb12f771b4d5195280783ad771b5110c444c028"
- },
- "wilted_rose": {
- "category": "nature",
- "moji": "🥀",
- "description": "wilted flower",
- "unicodeVersion": "9.0",
- "digest": "401fb35a8c6d26db708b2756c0bb55e0c62490a8cbdd51a21d6634881a62d32c"
- },
- "wind_blowing_face": {
- "category": "nature",
- "moji": "🌬",
- "description": "wind blowing face",
- "unicodeVersion": "7.0",
- "digest": "acb691375579ce301ce94a0c9f23bebcac90139e2c3cea8aa8c6cb3b581e8ae3"
- },
- "wind_chime": {
- "category": "objects",
- "moji": "🎐",
- "description": "wind chime",
- "unicodeVersion": "6.0",
- "digest": "57b721fd94e359f9a8dcd9fc702a3c9f82b06f4347d5b6e33c9d42117dd26e8a"
- },
- "wine_glass": {
- "category": "food",
- "moji": "🍷",
- "description": "wine glass",
- "unicodeVersion": "6.0",
- "digest": "d7551c39ea933566603e8ccd18c9c116e8d4dd2dca14697f5d6f6c0dc481e05c"
- },
- "wink": {
- "category": "people",
- "moji": "😉",
- "description": "winking face",
- "unicodeVersion": "6.0",
- "digest": "a9746d44d7fd9f51c0b0329aeb9eaa438e4690162d6c82c482ec3f4bc2def8b9"
- },
- "wolf": {
- "category": "nature",
- "moji": "🐺",
- "description": "wolf face",
- "unicodeVersion": "6.0",
- "digest": "f961f617f4b6af5a0b1683132476ce765cb3ce74c3c5c32083593622ff8c10e7"
- },
- "woman": {
- "category": "people",
- "moji": "👩",
- "description": "woman",
- "unicodeVersion": "6.0",
- "digest": "7f06a5df2103c959228c15f44a76d39f87f792bef6aaadf7e4f47fe31a0e85fa"
- },
- "woman_tone1": {
- "category": "people",
- "moji": "👩🏻",
- "description": "woman tone 1",
- "unicodeVersion": "8.0",
- "digest": "e1e1bc0d9e6c06fc37e54251c9d492c83852016baeb16acfedd7242a0f4a289e"
- },
- "woman_tone2": {
- "category": "people",
- "moji": "👩🏼",
- "description": "woman tone 2",
- "unicodeVersion": "8.0",
- "digest": "f3e0dd2ee081ca179d8c70e3c5a77254f98880f732cabdf601d54b64ae8702cd"
- },
- "woman_tone3": {
- "category": "people",
- "moji": "👩🏽",
- "description": "woman tone 3",
- "unicodeVersion": "8.0",
- "digest": "1ad974a8aad0dc2cb62d22e7c6c155bd07030222c3f115e62c153ce5cb6b240d"
- },
- "woman_tone4": {
- "category": "people",
- "moji": "👩🏾",
- "description": "woman tone 4",
- "unicodeVersion": "8.0",
- "digest": "c11d39ec49db3d36256f372521461b9d668726cab6d63166ef23c9c99e3a9c55"
- },
- "woman_tone5": {
- "category": "people",
- "moji": "👩🏿",
- "description": "woman tone 5",
- "unicodeVersion": "8.0",
- "digest": "7220ffd6a38cf1587f1c45e3461c38f65162b604fb4261318c342bb6f1a4d95f"
- },
- "womans_clothes": {
- "category": "people",
- "moji": "👚",
- "description": "womans clothes",
- "unicodeVersion": "6.0",
- "digest": "4b47a76b3fa144213583b47c2284eee1e4148b9b4cd578f1d02ddb474c78a3ae"
- },
- "womans_hat": {
- "category": "people",
- "moji": "👒",
- "description": "womans hat",
- "unicodeVersion": "6.0",
- "digest": "7feded8ca13ddee2d9fd71e97a0e7f719d85f85190a69d6fde985d69e20bc422"
- },
- "womens": {
- "category": "symbols",
- "moji": "🚺",
- "description": "womens symbol",
- "unicodeVersion": "6.0",
- "digest": "d0dd248ac8f17c5eb5c24f636f2d6387f1e4126d2227834ffb89057e9428a19f"
- },
- "worried": {
- "category": "people",
- "moji": "😟",
- "description": "worried face",
- "unicodeVersion": "6.1",
- "digest": "66814ad2b00574ed539af543224116a3564b162b57e9d02d58f19513cb2c80f2"
- },
- "wrench": {
- "category": "objects",
- "moji": "🔧",
- "description": "wrench",
- "unicodeVersion": "6.0",
- "digest": "8478c1b8b0565f87fe6e4f429975f16919d80622a3cdbcc2e83f17fd6978a1d7"
- },
- "wrestlers": {
- "category": "activity",
- "moji": "🤼",
- "description": "wrestlers",
- "unicodeVersion": "9.0",
- "digest": "919cf0ff49517c62a0abc8a6067e196fa6a1d5f73fa5902537879775ee9fad13"
- },
- "wrestlers_tone1": {
- "category": "activity",
- "moji": "🤼🏻",
- "description": "wrestlers tone 1",
- "unicodeVersion": "9.0",
- "digest": "9611c3dfb8a18b8332135642d0fac74bd6557054909066141a81c9684bbc7207"
- },
- "wrestlers_tone2": {
- "category": "activity",
- "moji": "🤼🏼",
- "description": "wrestlers tone 2",
- "unicodeVersion": "9.0",
- "digest": "17c529119acb4aafd8c834874d6285e41d2d9694243cce9eb51c87f67d22a3be"
- },
- "wrestlers_tone3": {
- "category": "activity",
- "moji": "🤼🏽",
- "description": "wrestlers tone 3",
- "unicodeVersion": "9.0",
- "digest": "0f98afb56ecc165b4b8193c86565eaf0ded90456d6370b9d9f2e61eef572642e"
- },
- "wrestlers_tone4": {
- "category": "activity",
- "moji": "🤼🏾",
- "description": "wrestlers tone 4",
- "unicodeVersion": "9.0",
- "digest": "595507c7163ed2349d6f5cd3afeb3cb961a889bda1091bb5b03bf97c743ddd83"
- },
- "wrestlers_tone5": {
- "category": "activity",
- "moji": "🤼🏿",
- "description": "wrestlers tone 5",
- "unicodeVersion": "9.0",
- "digest": "9a844250d5dbce6369a7923154fd3e7192e9bbc8773044ede577e587c86a6481"
- },
- "writing_hand": {
- "category": "people",
- "moji": "✍",
- "description": "writing hand",
- "unicodeVersion": "1.1",
- "digest": "efe3bfa1c36098242e2c4cb2d66e8d76fd3c0bba57335059dd1ad25748752c41"
- },
- "writing_hand_tone1": {
- "category": "people",
- "moji": "✍🏻",
- "description": "writing hand tone 1",
- "unicodeVersion": "8.0",
- "digest": "13e6782eea27e216721c4d1aeaa049f9ae3d93c9052f131cc17618102d16e4f6"
- },
- "writing_hand_tone2": {
- "category": "people",
- "moji": "✍🏼",
- "description": "writing hand tone 2",
- "unicodeVersion": "8.0",
- "digest": "6ec5eeef36ef27fae05f2d3ac9bd97460d75b08fa52e01e239cbbe4101eb8ad8"
- },
- "writing_hand_tone3": {
- "category": "people",
- "moji": "✍🏽",
- "description": "writing hand tone 3",
- "unicodeVersion": "8.0",
- "digest": "e3d68b62cbda6579641f49b45581deca7653e6322df43413ee6b80967861e315"
- },
- "writing_hand_tone4": {
- "category": "people",
- "moji": "✍🏾",
- "description": "writing hand tone 4",
- "unicodeVersion": "8.0",
- "digest": "52b7652348605b384761de53c28ffc3dc84b1b34210f795b40bb3e6235b8a959"
- },
- "writing_hand_tone5": {
- "category": "people",
- "moji": "✍🏿",
- "description": "writing hand tone 5",
- "unicodeVersion": "8.0",
- "digest": "ce22c8e6c71ca3a07d32fb0c336301d015b81975a1019f20760ee908349c76dd"
- },
- "x": {
- "category": "symbols",
- "moji": "❌",
- "description": "cross mark",
- "unicodeVersion": "6.0",
- "digest": "d07158957d15f5b6c6fc56a9658a82ee1fa58f7b8b19d121a1713c9bf38cbdfc"
- },
- "yellow_heart": {
- "category": "symbols",
- "moji": "💛",
- "description": "yellow heart",
- "unicodeVersion": "6.0",
- "digest": "1785103b3aab8606869692986a2ff5e320dae4b6d58f7dca33beed221aff8f42"
- },
- "yen": {
- "category": "objects",
- "moji": "💴",
- "description": "banknote with yen sign",
- "unicodeVersion": "6.0",
- "digest": "b12182adf55be97d7d5e52176bb149c16f57d0645f508dcb97efbeab4607d439"
- },
- "yin_yang": {
- "category": "symbols",
- "moji": "☯",
- "description": "yin yang",
- "unicodeVersion": "1.1",
- "digest": "2d97f8a9eca001163c5895ff25ca66aad5dfabe5e2e00bda102fb6b9a27fc02b"
- },
- "yum": {
- "category": "people",
- "moji": "😋",
- "description": "face savouring delicious food",
- "unicodeVersion": "6.0",
- "digest": "796badd831c75797cd4acb88694d3bf19b2727678b3c2e63e465e4d8125e4ad4"
- },
- "zap": {
- "category": "nature",
- "moji": "⚡",
- "description": "high voltage sign",
- "unicodeVersion": "4.0",
- "digest": "fbbfbda066d067a5b92ca815c784b577354de3e6c7a3dfa9a1656c29dc804f0c"
- },
- "zero": {
- "category": "symbols",
- "moji": "0️⃣",
- "description": "keycap digit zero",
- "unicodeVersion": "3.0",
- "digest": "ef62df7416e51b02ff22787a981235053baabbb872247605e97834d9a7caff49"
- },
- "zipper_mouth": {
- "category": "people",
- "moji": "🤐",
- "description": "zipper-mouth face",
- "unicodeVersion": "8.0",
- "digest": "dfeeb9947458d1bb04805e46211d4f29aa89210239c475425ac04a1cef340701"
- },
- "zzz": {
- "category": "people",
- "moji": "💤",
- "description": "sleeping symbol",
- "unicodeVersion": "6.0",
- "digest": "6b19746f5be6ee5f10dcb0969557eb9b02972d4429052237facf8d4b4f768546"
}
-} \ No newline at end of file
+}
diff --git a/gems/config/rubocop.yml b/gems/config/rubocop.yml
index d6139bef1b5..ca46e30e2cd 100644
--- a/gems/config/rubocop.yml
+++ b/gems/config/rubocop.yml
@@ -100,6 +100,9 @@ RSpec/FeatureCategory:
Style/HashSyntax:
Enabled: false
+Style/InlineDisableAnnotation:
+ Enabled: false
+
Style/Lambda:
EnforcedStyle: literal
diff --git a/gems/gitlab-backup-cli/.gitignore b/gems/gitlab-backup-cli/.gitignore
new file mode 100644
index 00000000000..b04a8c840df
--- /dev/null
+++ b/gems/gitlab-backup-cli/.gitignore
@@ -0,0 +1,11 @@
+/.bundle/
+/.yardoc
+/_yardoc/
+/coverage/
+/doc/
+/pkg/
+/spec/reports/
+/tmp/
+
+# rspec failure tracking
+.rspec_status
diff --git a/gems/gitlab-backup-cli/.gitlab-ci.yml b/gems/gitlab-backup-cli/.gitlab-ci.yml
new file mode 100644
index 00000000000..884ca22a853
--- /dev/null
+++ b/gems/gitlab-backup-cli/.gitlab-ci.yml
@@ -0,0 +1,4 @@
+include:
+ - local: gems/gem.gitlab-ci.yml
+ inputs:
+ gem_name: "gitlab-backup-cli"
diff --git a/gems/gitlab-backup-cli/.rspec b/gems/gitlab-backup-cli/.rspec
new file mode 100644
index 00000000000..34c5164d9b5
--- /dev/null
+++ b/gems/gitlab-backup-cli/.rspec
@@ -0,0 +1,3 @@
+--format documentation
+--color
+--require spec_helper
diff --git a/gems/gitlab-backup-cli/.rubocop.yml b/gems/gitlab-backup-cli/.rubocop.yml
new file mode 100644
index 00000000000..8c670b439d3
--- /dev/null
+++ b/gems/gitlab-backup-cli/.rubocop.yml
@@ -0,0 +1,2 @@
+inherit_from:
+ - ../config/rubocop.yml
diff --git a/gems/gitlab-backup-cli/Gemfile b/gems/gitlab-backup-cli/Gemfile
new file mode 100644
index 00000000000..e5c133f93f2
--- /dev/null
+++ b/gems/gitlab-backup-cli/Gemfile
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+source "https://rubygems.org"
+
+# Specify your gem's dependencies in gitlab-backup-cli.gemspec
+gemspec
diff --git a/gems/gitlab-backup-cli/Gemfile.lock b/gems/gitlab-backup-cli/Gemfile.lock
new file mode 100644
index 00000000000..73c2b942865
--- /dev/null
+++ b/gems/gitlab-backup-cli/Gemfile.lock
@@ -0,0 +1,114 @@
+PATH
+ remote: .
+ specs:
+ gitlab-backup-cli (0.0.1)
+ thor (~> 1.3)
+
+GEM
+ remote: https://rubygems.org/
+ specs:
+ activesupport (7.1.1)
+ base64
+ bigdecimal
+ concurrent-ruby (~> 1.0, >= 1.0.2)
+ connection_pool (>= 2.2.5)
+ drb
+ i18n (>= 1.6, < 2)
+ minitest (>= 5.1)
+ mutex_m
+ tzinfo (~> 2.0)
+ ast (2.4.2)
+ base64 (0.2.0)
+ bigdecimal (3.1.4)
+ concurrent-ruby (1.2.2)
+ connection_pool (2.4.1)
+ diff-lcs (1.5.0)
+ drb (2.2.0)
+ ruby2_keywords
+ gitlab-styles (11.0.0)
+ rubocop (~> 1.57.1)
+ rubocop-graphql (~> 0.18)
+ rubocop-performance (~> 1.15)
+ rubocop-rails (~> 2.17)
+ rubocop-rspec (~> 2.22)
+ i18n (1.14.1)
+ concurrent-ruby (~> 1.0)
+ json (2.6.3)
+ language_server-protocol (3.17.0.3)
+ minitest (5.20.0)
+ mutex_m (0.2.0)
+ parallel (1.23.0)
+ parser (3.2.2.4)
+ ast (~> 2.4.1)
+ racc
+ racc (1.7.3)
+ rack (3.0.8)
+ rainbow (3.1.1)
+ rake (13.1.0)
+ regexp_parser (2.8.2)
+ rexml (3.2.6)
+ rspec (3.12.0)
+ rspec-core (~> 3.12.0)
+ rspec-expectations (~> 3.12.0)
+ rspec-mocks (~> 3.12.0)
+ rspec-core (3.12.2)
+ rspec-support (~> 3.12.0)
+ rspec-expectations (3.12.3)
+ diff-lcs (>= 1.2.0, < 2.0)
+ rspec-support (~> 3.12.0)
+ rspec-mocks (3.12.6)
+ diff-lcs (>= 1.2.0, < 2.0)
+ rspec-support (~> 3.12.0)
+ rspec-support (3.12.1)
+ rubocop (1.57.2)
+ json (~> 2.3)
+ language_server-protocol (>= 3.17.0)
+ parallel (~> 1.10)
+ parser (>= 3.2.2.4)
+ rainbow (>= 2.2.2, < 4.0)
+ regexp_parser (>= 1.8, < 3.0)
+ rexml (>= 3.2.5, < 4.0)
+ rubocop-ast (>= 1.28.1, < 2.0)
+ ruby-progressbar (~> 1.7)
+ unicode-display_width (>= 2.4.0, < 3.0)
+ rubocop-ast (1.30.0)
+ parser (>= 3.2.1.0)
+ rubocop-capybara (2.19.0)
+ rubocop (~> 1.41)
+ rubocop-factory_bot (2.24.0)
+ rubocop (~> 1.33)
+ rubocop-graphql (0.19.0)
+ rubocop (>= 0.87, < 2)
+ rubocop-performance (1.19.1)
+ rubocop (>= 1.7.0, < 2.0)
+ rubocop-ast (>= 0.4.0)
+ rubocop-rails (2.20.0)
+ activesupport (>= 4.2.0)
+ rack (>= 1.1)
+ rubocop (>= 1.33.0, < 2.0)
+ rubocop-rspec (2.25.0)
+ rubocop (~> 1.40)
+ rubocop-capybara (~> 2.17)
+ rubocop-factory_bot (~> 2.22)
+ ruby-progressbar (1.13.0)
+ ruby2_keywords (0.0.5)
+ thor (1.3.0)
+ tzinfo (2.0.6)
+ concurrent-ruby (~> 1.0)
+ unicode-display_width (2.5.0)
+
+PLATFORMS
+ arm64-darwin-21
+ arm64-darwin-22
+ ruby
+ x86_64-linux
+
+DEPENDENCIES
+ gitlab-backup-cli!
+ gitlab-styles (~> 11.0)
+ rake (~> 13.0)
+ rspec (~> 3.0)
+ rubocop-rails (<= 2.20)
+
+BUNDLED WITH
+ 2.4.21
diff --git a/gems/gitlab-backup-cli/LICENSE.txt b/gems/gitlab-backup-cli/LICENSE.txt
new file mode 100644
index 00000000000..3def1245ee6
--- /dev/null
+++ b/gems/gitlab-backup-cli/LICENSE.txt
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2023-present GitLab B.V.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/gems/gitlab-backup-cli/README.md b/gems/gitlab-backup-cli/README.md
new file mode 100644
index 00000000000..0d6b12b65aa
--- /dev/null
+++ b/gems/gitlab-backup-cli/README.md
@@ -0,0 +1,7 @@
+# Gitlab::Backup::Cli
+
+This gem will contain the Backup CLI logic.
+
+## License
+
+The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
diff --git a/gems/gitlab-backup-cli/Rakefile b/gems/gitlab-backup-cli/Rakefile
new file mode 100644
index 00000000000..cca71754493
--- /dev/null
+++ b/gems/gitlab-backup-cli/Rakefile
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+require "bundler/gem_tasks"
+require "rspec/core/rake_task"
+
+RSpec::Core::RakeTask.new(:spec)
+
+require "rubocop/rake_task"
+
+RuboCop::RakeTask.new
+
+task default: %i[spec rubocop]
diff --git a/gems/gitlab-backup-cli/bin/console b/gems/gitlab-backup-cli/bin/console
new file mode 100755
index 00000000000..3aa2eb40f21
--- /dev/null
+++ b/gems/gitlab-backup-cli/bin/console
@@ -0,0 +1,11 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+require "bundler/setup"
+require "gitlab/backup/cli"
+
+# You can add fixtures and/or initialization code here to make experimenting
+# with your gem easier. You can also use a different console, if you like.
+
+require "irb"
+IRB.start(__FILE__)
diff --git a/gems/gitlab-backup-cli/bin/setup b/gems/gitlab-backup-cli/bin/setup
new file mode 100755
index 00000000000..dce67d860af
--- /dev/null
+++ b/gems/gitlab-backup-cli/bin/setup
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+set -euo pipefail
+IFS=$'\n\t'
+set -vx
+
+bundle install
+
+# Do any other automated setup that you need to do here
diff --git a/gems/gitlab-backup-cli/gitlab-backup-cli.gemspec b/gems/gitlab-backup-cli/gitlab-backup-cli.gemspec
new file mode 100644
index 00000000000..8863e9b750b
--- /dev/null
+++ b/gems/gitlab-backup-cli/gitlab-backup-cli.gemspec
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require_relative "lib/gitlab/backup/cli/version"
+
+Gem::Specification.new do |spec|
+ spec.name = "gitlab-backup-cli"
+ spec.version = Gitlab::Backup::Cli::VERSION
+ spec.authors = ["Gabriel Mazetto"]
+ spec.email = ["brodock@gmail.com"]
+
+ spec.summary = "GitLab Backup CLI"
+ spec.description = "GitLab Backup CLI"
+ spec.homepage = "https://gitlab.com/gitlab-org/gitlab/-/tree/master/gems/gitlab-backup-cli"
+ spec.license = "MIT"
+ spec.required_ruby_version = ">= 3.0"
+
+ spec.metadata["rubygems_mfa_required"] = "true"
+ spec.metadata["homepage_uri"] = spec.homepage
+ spec.metadata["source_code_uri"] = spec.homepage
+
+ spec.files = Dir['lib/**/*.rb']
+
+ spec.bindir = "exe"
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
+ spec.require_paths = ["lib"]
+
+ spec.add_dependency "thor", "~> 1.3"
+
+ spec.add_development_dependency "gitlab-styles", "~> 11.0"
+ spec.add_development_dependency "rake", "~> 13.0"
+ spec.add_development_dependency "rspec", "~> 3.0"
+ spec.add_development_dependency "rubocop-rails", "<= 2.20" # https://github.com/rubocop/rubocop-rails/issues/1173
+end
diff --git a/gems/gitlab-backup-cli/lib/gitlab/backup/cli.rb b/gems/gitlab-backup-cli/lib/gitlab/backup/cli.rb
new file mode 100644
index 00000000000..01ea934863f
--- /dev/null
+++ b/gems/gitlab-backup-cli/lib/gitlab/backup/cli.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Backup
+ # GitLab Backup CLI
+ module Cli
+ autoload :VERSION, 'gitlab/backup/cli/version'
+ autoload :Runner, 'gitlab/backup/cli/runner'
+
+ Error = Class.new(StandardError)
+ # Your code goes here...
+ end
+ end
+end
diff --git a/gems/gitlab-backup-cli/lib/gitlab/backup/cli/runner.rb b/gems/gitlab-backup-cli/lib/gitlab/backup/cli/runner.rb
new file mode 100644
index 00000000000..b04386f75c0
--- /dev/null
+++ b/gems/gitlab-backup-cli/lib/gitlab/backup/cli/runner.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require 'thor'
+
+module Gitlab
+ module Backup
+ module Cli
+ # GitLab Backup CLI
+ #
+ # This supersedes the previous backup rake files and will be
+ # the default interface to handle backups
+ class Runner < Thor
+ def self.exit_on_failure?
+ true
+ end
+
+ map %w[--version -v] => :version
+ desc 'version', 'Display the version information'
+
+ def version
+ puts "GitLab Backup CLI (#{VERSION})" # rubocop:disable Rails/Output -- CLI output
+ end
+
+ private
+
+ def rails_environment!
+ require APP_PATH
+
+ Rails.application.load_tasks
+ end
+ end
+ end
+ end
+end
diff --git a/gems/gitlab-backup-cli/lib/gitlab/backup/cli/version.rb b/gems/gitlab-backup-cli/lib/gitlab/backup/cli/version.rb
new file mode 100644
index 00000000000..32bf3de31da
--- /dev/null
+++ b/gems/gitlab-backup-cli/lib/gitlab/backup/cli/version.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Backup
+ module Cli
+ VERSION = "0.0.1"
+ end
+ end
+end
diff --git a/gems/gitlab-backup-cli/sig/gitlab/backup/cli.rbs b/gems/gitlab-backup-cli/sig/gitlab/backup/cli.rbs
new file mode 100644
index 00000000000..25540c06400
--- /dev/null
+++ b/gems/gitlab-backup-cli/sig/gitlab/backup/cli.rbs
@@ -0,0 +1,7 @@
+module Gitlab
+ module Backup
+ module Cli
+ VERSION: String
+ end
+ end
+end
diff --git a/gems/gitlab-backup-cli/sig/gitlab/backup/cli/runner.rbs b/gems/gitlab-backup-cli/sig/gitlab/backup/cli/runner.rbs
new file mode 100644
index 00000000000..56b031b82bc
--- /dev/null
+++ b/gems/gitlab-backup-cli/sig/gitlab/backup/cli/runner.rbs
@@ -0,0 +1,15 @@
+module Gitlab
+ module Backup
+ module Cli
+ class Runner
+ def self.exit_on_failure?: -> bool
+
+ def version: -> void
+
+ private
+
+ def rails_environment!: -> void
+ end
+ end
+ end
+end
diff --git a/gems/gitlab-backup-cli/spec/gitlab/backup/cli_spec.rb b/gems/gitlab-backup-cli/spec/gitlab/backup/cli_spec.rb
new file mode 100644
index 00000000000..db3aaeccb05
--- /dev/null
+++ b/gems/gitlab-backup-cli/spec/gitlab/backup/cli_spec.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+RSpec.describe Gitlab::Backup::Cli do
+ it "has a version number" do
+ expect(Gitlab::Backup::Cli::VERSION).not_to be nil
+ end
+end
diff --git a/gems/gitlab-backup-cli/spec/spec_helper.rb b/gems/gitlab-backup-cli/spec/spec_helper.rb
new file mode 100644
index 00000000000..6c4ae2df96c
--- /dev/null
+++ b/gems/gitlab-backup-cli/spec/spec_helper.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require "gitlab/backup/cli"
+
+RSpec.configure do |config|
+ # Enable flags like --only-failures and --next-failure
+ config.example_status_persistence_file_path = ".rspec_status"
+
+ # Disable RSpec exposing methods globally on `Module` and `main`
+ config.disable_monkey_patching!
+
+ config.expect_with :rspec do |c|
+ c.syntax = :expect
+ end
+end
diff --git a/gems/gitlab-http/Gemfile.lock b/gems/gitlab-http/Gemfile.lock
index c15bcd7cc18..1f4910d1d57 100644
--- a/gems/gitlab-http/Gemfile.lock
+++ b/gems/gitlab-http/Gemfile.lock
@@ -2,7 +2,7 @@ PATH
remote: ../gitlab-rspec
specs:
gitlab-rspec (0.1.0)
- activesupport (>= 6.1, < 7.1)
+ activesupport (>= 6.1, < 8)
rspec (~> 3.0)
PATH
@@ -20,6 +20,7 @@ PATH
specs:
gitlab-http (0.1.0)
activesupport (~> 7)
+ concurrent-ruby (~> 1.2)
httparty (~> 0.21.0)
ipaddress (~> 0.8.3)
nokogiri (~> 1.15.4)
diff --git a/gems/gitlab-http/README.md b/gems/gitlab-http/README.md
index 13ff330bb19..e717afbdb2c 100644
--- a/gems/gitlab-http/README.md
+++ b/gems/gitlab-http/README.md
@@ -24,16 +24,27 @@ end
### Actions
-Basic examples;
+Basic examples:
```ruby
Gitlab::HTTP_V2.post(uri, body: body)
Gitlab::HTTP_V2.try_get(uri, params)
-response = Gitlab::HTTP_V2.head(project_url, verify: true)
+response = Gitlab::HTTP_V2.head(project_url, verify: true) # returns an HTTParty::Response object
-Gitlab::HTTP_V2.post(path, base_uri: base_uri, **params)
+Gitlab::HTTP_V2.post(path, base_uri: base_uri, **params) # returns an HTTParty::Response object
+```
+
+Async usage examples:
+
+```ruby
+lazy_response = Gitlab::HTTP_V2.get(location, async: true)
+
+lazy_response.execute # starts the request and returns the same LazyResponse object
+lazy_response.wait # waits for the request to finish and returns the same LazyResponse object
+
+response = lazy_response.value # returns an HTTParty::Response object
```
## Development
diff --git a/gems/gitlab-http/gitlab-http.gemspec b/gems/gitlab-http/gitlab-http.gemspec
index 6146ba7f78b..0033f17447b 100644
--- a/gems/gitlab-http/gitlab-http.gemspec
+++ b/gems/gitlab-http/gitlab-http.gemspec
@@ -20,6 +20,7 @@ Gem::Specification.new do |spec|
spec.require_paths = ["lib"]
spec.add_runtime_dependency 'activesupport', '~> 7'
+ spec.add_runtime_dependency 'concurrent-ruby', '~> 1.2'
spec.add_runtime_dependency 'httparty', '~> 0.21.0'
spec.add_runtime_dependency 'ipaddress', '~> 0.8.3'
spec.add_runtime_dependency 'nokogiri', '~> 1.15.4'
diff --git a/gems/gitlab-http/lib/gitlab/http_v2/client.rb b/gems/gitlab-http/lib/gitlab/http_v2/client.rb
index c10197e0385..52c9ab897f5 100644
--- a/gems/gitlab-http/lib/gitlab/http_v2/client.rb
+++ b/gems/gitlab-http/lib/gitlab/http_v2/client.rb
@@ -4,7 +4,8 @@ require 'httparty'
require 'net/http'
require 'active_support/all'
require_relative 'new_connection_adapter'
-require_relative "exceptions"
+require_relative 'exceptions'
+require_relative 'lazy_response'
module Gitlab
module HTTP_V2
@@ -45,9 +46,12 @@ module Gitlab
# TODO: This overwrites a method implemented by `HTTPParty`
# The calls to `get/...` will call this method instead of `httparty_perform_request`
def perform_request(http_method, path, options, &block)
+ raise_if_options_are_invalid(options)
raise_if_blocked_by_silent_mode(http_method) if options.delete(:silent_mode_enabled)
log_info = options.delete(:extra_log_info)
+ async = options.delete(:async)
+
options_with_timeouts =
if !options.has_key?(:timeout)
options.with_defaults(DEFAULT_TIMEOUT_OPTIONS)
@@ -57,29 +61,57 @@ module Gitlab
if options[:stream_body]
httparty_perform_request(http_method, path, options_with_timeouts, &block)
+ elsif async
+ async_perform_request(http_method, path, options, options_with_timeouts, log_info, &block)
else
- begin
- start_time = nil
- read_total_timeout = options.fetch(:timeout, DEFAULT_READ_TOTAL_TIMEOUT)
-
- httparty_perform_request(http_method, path, options_with_timeouts) do |fragment|
- start_time ||= system_monotonic_time
- elapsed = system_monotonic_time - start_time
-
- raise ReadTotalTimeout, "Request timed out after #{elapsed} seconds" if elapsed > read_total_timeout
-
- yield fragment if block
- end
- rescue HTTParty::RedirectionTooDeep
- raise RedirectionTooDeep
- rescue *HTTP_ERRORS => e
- extra_info = log_info || {}
- extra_info = log_info.call(e, path, options) if log_info.respond_to?(:call)
- configuration.log_exception(e, extra_info)
-
- raise e
+ sync_perform_request(http_method, path, options, options_with_timeouts, log_info, &block)
+ end
+ end
+
+ def async_perform_request(http_method, path, options, options_with_timeouts, log_info, &block)
+ start_time = nil
+ read_total_timeout = options.fetch(:timeout, DEFAULT_READ_TOTAL_TIMEOUT)
+
+ promise = Concurrent::Promise.new do
+ httparty_perform_request(http_method, path, options_with_timeouts) do |fragment|
+ start_time ||= system_monotonic_time
+ elapsed = system_monotonic_time - start_time
+
+ raise ReadTotalTimeout, "Request timed out after #{elapsed} seconds" if elapsed > read_total_timeout
+
+ yield fragment if block
end
end
+
+ LazyResponse.new(promise, path, options, log_info)
+ end
+
+ def sync_perform_request(http_method, path, options, options_with_timeouts, log_info, &block)
+ start_time = nil
+ read_total_timeout = options.fetch(:timeout, DEFAULT_READ_TOTAL_TIMEOUT)
+
+ httparty_perform_request(http_method, path, options_with_timeouts) do |fragment|
+ start_time ||= system_monotonic_time
+ elapsed = system_monotonic_time - start_time
+
+ raise ReadTotalTimeout, "Request timed out after #{elapsed} seconds" if elapsed > read_total_timeout
+
+ yield fragment if block
+ end
+ rescue HTTParty::RedirectionTooDeep
+ raise RedirectionTooDeep
+ rescue *HTTP_ERRORS => e
+ extra_info = log_info || {}
+ extra_info = log_info.call(e, path, options) if log_info.respond_to?(:call)
+ configuration.log_exception(e, extra_info)
+
+ raise e
+ end
+
+ def raise_if_options_are_invalid(options)
+ return unless options[:async] && (options[:stream_body] || options[:silent_mode_enabled])
+
+ raise ArgumentError, '`async` cannot be used with `stream_body` or `silent_mode_enabled`'
end
def raise_if_blocked_by_silent_mode(http_method)
diff --git a/gems/gitlab-http/lib/gitlab/http_v2/lazy_response.rb b/gems/gitlab-http/lib/gitlab/http_v2/lazy_response.rb
new file mode 100644
index 00000000000..65d1ab96644
--- /dev/null
+++ b/gems/gitlab-http/lib/gitlab/http_v2/lazy_response.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module HTTP_V2
+ class LazyResponse
+ NotExecutedError = Class.new(StandardError)
+
+ attr_reader :promise
+
+ delegate :state, to: :promise
+
+ def initialize(promise, path, options, log_info)
+ @promise = promise
+ @path = path
+ @options = options
+ @log_info = log_info
+ end
+
+ def execute
+ @promise.execute
+ self
+ end
+
+ def wait
+ @promise.wait
+ self
+ end
+
+ def value
+ raise NotExecutedError, '`execute` must be called before `value`' if @promise.unscheduled?
+
+ wait # wait for the promise to be completed
+
+ raise @promise.reason if @promise.rejected?
+
+ @promise.value
+ rescue HTTParty::RedirectionTooDeep
+ raise HTTP_V2::RedirectionTooDeep
+ rescue *HTTP_V2::HTTP_ERRORS => e
+ extra_info = @log_info || {}
+ extra_info = @log_info.call(e, @path, @options) if @log_info.respond_to?(:call)
+ Gitlab::HTTP_V2.configuration.log_exception(e, extra_info)
+
+ raise e
+ end
+ end
+ end
+end
diff --git a/gems/gitlab-http/spec/gitlab/http_v2_spec.rb b/gems/gitlab-http/spec/gitlab/http_v2_spec.rb
index bfa1dcd2633..3151761d375 100644
--- a/gems/gitlab-http/spec/gitlab/http_v2_spec.rb
+++ b/gems/gitlab-http/spec/gitlab/http_v2_spec.rb
@@ -450,4 +450,101 @@ RSpec.describe Gitlab::HTTP_V2, feature_category: :shared do
end
end
end
+
+ context 'when options[:async] is true' do
+ context 'when it is a valid request' do
+ before do
+ stub_full_request('http://example.org', method: :any).to_return(status: 200, body: 'hello world')
+ end
+
+ it 'returns a LazyResponse' do
+ result = described_class.get('http://example.org', async: true)
+
+ expect(result).to be_a(Gitlab::HTTP_V2::LazyResponse)
+ expect(result.state).to eq(:unscheduled)
+
+ expect(result.execute).to be_a(Gitlab::HTTP_V2::LazyResponse)
+ expect(result.wait).to be_a(Gitlab::HTTP_V2::LazyResponse)
+
+ expect(result.value).to be_a(HTTParty::Response)
+ expect(result.value.body).to eq('hello world')
+ end
+ end
+
+ context 'when the URL is denied' do
+ let(:url) { 'http://localhost:3003' }
+ let(:error_class) { Gitlab::HTTP_V2::BlockedUrlError }
+ let(:opts) { {} }
+
+ let(:result) do
+ described_class.get(url, allow_local_requests: false, async: true, **opts)
+ end
+
+ it 'returns a LazyResponse with error value' do
+ expect(result).to be_a(Gitlab::HTTP_V2::LazyResponse)
+
+ expect { result.execute.value }.to raise_error(error_class)
+ end
+
+ it 'logs the exception' do
+ expect(described_class.configuration)
+ .to receive(:log_exception)
+ .with(instance_of(error_class), {})
+
+ expect { result.execute.value }.to raise_error(error_class)
+ end
+
+ context 'with extra_log_info as hash' do
+ let(:opts) { { extra_log_info: { a: :b } } }
+
+ it 'handles the request' do
+ expect(described_class.configuration)
+ .to receive(:log_exception)
+ .with(instance_of(error_class), { a: :b })
+
+ expect { result.execute.value }.to raise_error(error_class)
+ end
+ end
+
+ context 'with extra_log_info as proc' do
+ let(:extra_log_info) do
+ proc do |error, url, options|
+ { klass: error.class, url: url, options: options }
+ end
+ end
+
+ let(:opts) { { extra_log_info: extra_log_info } }
+
+ it 'handles the request' do
+ expect(described_class.configuration)
+ .to receive(:log_exception)
+ .with(instance_of(error_class), { url: url, klass: error_class, options: { allow_local_requests: false } })
+
+ expect { result.execute.value }.to raise_error(error_class)
+ end
+ end
+ end
+ end
+
+ context 'when options[:async] and options[:stream_body] are true' do
+ before do
+ stub_full_request('http://example.org', method: :any)
+ end
+
+ it 'raises an ArgumentError' do
+ expect { described_class.get('http://example.org', async: true, stream_body: true) }
+ .to raise_error(ArgumentError, '`async` cannot be used with `stream_body` or `silent_mode_enabled`')
+ end
+ end
+
+ context 'when options[:async] and options[:silent_mode_enabled] are true' do
+ before do
+ stub_full_request('http://example.org', method: :any)
+ end
+
+ it 'raises an ArgumentError' do
+ expect { described_class.get('http://example.org', async: true, silent_mode_enabled: true) }
+ .to raise_error(ArgumentError, '`async` cannot be used with `stream_body` or `silent_mode_enabled`')
+ end
+ end
end
diff --git a/gems/gitlab-rspec/Gemfile.lock b/gems/gitlab-rspec/Gemfile.lock
index dcdb4dd009e..7dff91cbd2d 100644
--- a/gems/gitlab-rspec/Gemfile.lock
+++ b/gems/gitlab-rspec/Gemfile.lock
@@ -2,7 +2,7 @@ PATH
remote: .
specs:
gitlab-rspec (0.1.0)
- activesupport (>= 6.1, < 7.1)
+ activesupport (>= 6.1, < 8)
rspec (~> 3.0)
GEM
diff --git a/gems/gitlab-rspec/gitlab-rspec.gemspec b/gems/gitlab-rspec/gitlab-rspec.gemspec
index c2c5b6c60b7..47c1e420ecd 100644
--- a/gems/gitlab-rspec/gitlab-rspec.gemspec
+++ b/gems/gitlab-rspec/gitlab-rspec.gemspec
@@ -18,7 +18,7 @@ Gem::Specification.new do |spec|
spec.files = Dir["lib/**/*.rb"]
spec.require_paths = ["lib"]
- spec.add_runtime_dependency "activesupport", ">= 6.1", "< 7.1"
+ spec.add_runtime_dependency "activesupport", ">= 6.1", "< 8"
spec.add_runtime_dependency "rspec", "~> 3.0"
spec.add_development_dependency "factory_bot_rails", "~> 6.2.0"
diff --git a/gems/gitlab-utils/Gemfile.lock b/gems/gitlab-utils/Gemfile.lock
index 971d90ce146..e6cfe03e60e 100644
--- a/gems/gitlab-utils/Gemfile.lock
+++ b/gems/gitlab-utils/Gemfile.lock
@@ -2,7 +2,7 @@ PATH
remote: ../gitlab-rspec
specs:
gitlab-rspec (0.1.0)
- activesupport (>= 6.1, < 7.1)
+ activesupport (>= 6.1, < 8)
rspec (~> 3.0)
PATH
diff --git a/gems/gitlab-utils/lib/gitlab/utils/strong_memoize.rb b/gems/gitlab-utils/lib/gitlab/utils/strong_memoize.rb
index 2b3841b8f09..8f1ddfbd578 100644
--- a/gems/gitlab-utils/lib/gitlab/utils/strong_memoize.rb
+++ b/gems/gitlab-utils/lib/gitlab/utils/strong_memoize.rb
@@ -44,7 +44,7 @@ module Gitlab
if instance_variable_defined?(expiration_key)
expire_at = instance_variable_get(expiration_key)
- clear_memoization(name) if Time.current > expire_at
+ clear_memoization(name) if expire_at.past?
end
if instance_variable_defined?(key)
diff --git a/gems/rspec_flaky/Gemfile.lock b/gems/rspec_flaky/Gemfile.lock
index 3f40a41483e..6be845e81fb 100644
--- a/gems/rspec_flaky/Gemfile.lock
+++ b/gems/rspec_flaky/Gemfile.lock
@@ -2,7 +2,7 @@ PATH
remote: ../gitlab-rspec
specs:
gitlab-rspec (0.1.0)
- activesupport (>= 6.1, < 7.1)
+ activesupport (>= 6.1, < 8)
rspec (~> 3.0)
PATH
diff --git a/generator_templates/active_record/migration/create_table_migration.rb.tt b/generator_templates/active_record/migration/create_table_migration.rb.tt
index e3eae729139..fbd21c28387 100644
--- a/generator_templates/active_record/migration/create_table_migration.rb.tt
+++ b/generator_templates/active_record/migration/create_table_migration.rb.tt
@@ -16,6 +16,7 @@ class <%= migration_class_name %> < Gitlab::Database::Migration[<%= Gitlab::Data
# To disable transactions uncomment the following line and remove these
# comments:
# disable_ddl_transaction!
+ milestone '<%= Gitlab.current_milestone %>'
# Add dependent 'batched_background_migrations.queued_migration_version' values.
# DEPENDENT_BATCHED_BACKGROUND_MIGRATIONS = []
diff --git a/generator_templates/active_record/migration/migration.rb.tt b/generator_templates/active_record/migration/migration.rb.tt
index 40481aed6ea..a8e715789e6 100644
--- a/generator_templates/active_record/migration/migration.rb.tt
+++ b/generator_templates/active_record/migration/migration.rb.tt
@@ -23,6 +23,7 @@ class <%= migration_class_name %> < Gitlab::Database::Migration[<%= Gitlab::Data
# Add dependent 'batched_background_migrations.queued_migration_version' values.
# DEPENDENT_BATCHED_BACKGROUND_MIGRATIONS = []
+ milestone '<%= Gitlab.current_milestone %>'
<%- if migration_action == 'add' -%>
def change
diff --git a/generator_templates/gitlab_internal_events/metric_definition.yml b/generator_templates/gitlab_internal_events/metric_definition.yml
index 3b09544207c..c31f7d54df0 100644
--- a/generator_templates/gitlab_internal_events/metric_definition.yml
+++ b/generator_templates/gitlab_internal_events/metric_definition.yml
@@ -13,8 +13,8 @@ time_frame: <%= args.third %>
data_source: internal_events
data_category: optional
instrumentation_class: <%= class_name(args.third) %>
-distribution: <%= distributions %>
-tier: <%= tiers %>
+distribution:<%= distributions %>
+tier:<%= tiers %>
options:
events:
- <%= event %>
diff --git a/generator_templates/post_deployment_migration/post_deployment_migration/migration.rb.tt b/generator_templates/post_deployment_migration/post_deployment_migration/migration.rb.tt
index dbce7eb201e..005b1e9c5e9 100644
--- a/generator_templates/post_deployment_migration/post_deployment_migration/migration.rb.tt
+++ b/generator_templates/post_deployment_migration/post_deployment_migration/migration.rb.tt
@@ -23,6 +23,7 @@ class <%= migration_class_name %> < Gitlab::Database::Migration[<%= Gitlab::Data
# Add dependent 'batched_background_migrations.queued_migration_version' values.
# DEPENDENT_BATCHED_BACKGROUND_MIGRATIONS = []
+ milestone '<%= Gitlab.current_milestone %>'
def up
end
diff --git a/generator_templates/snowplow_event_definition/event_definition.yml b/generator_templates/snowplow_event_definition/event_definition.yml
deleted file mode 100644
index 475c2ef876f..00000000000
--- a/generator_templates/snowplow_event_definition/event_definition.yml
+++ /dev/null
@@ -1,27 +0,0 @@
----
-description:
-category: <%= event_category %>
-action: <%= event_action %>
-label_description:
-property_description:
-value_description:
-extra_properties:
-identifiers:
-#- project
-#- user
-#- namespace
-product_section:
-product_stage:
-product_group:
-milestone: "<%= milestone %>"
-introduced_by_url:
-distributions:
-<%= distributions %>
-tiers: <% if ee? %>
-#- premium
-- ultimate
-<% else %>
-- free
-- premium
-- ultimate
-<% end %>
diff --git a/glfm_specification/input/gitlab_flavored_markdown/glfm_internal_extensions.md b/glfm_specification/input/gitlab_flavored_markdown/glfm_internal_extensions.md
index 266e1c7723d..e7453d8c556 100644
--- a/glfm_specification/input/gitlab_flavored_markdown/glfm_internal_extensions.md
+++ b/glfm_specification/input/gitlab_flavored_markdown/glfm_internal_extensions.md
@@ -777,4 +777,232 @@ footnote text
</section>
````````````````````````````````
+# GFM undocumented extensions and more robust test
+
+This section contains tests borrowed from https://github.com/github/cmark-gfm/blob/master/test/extensions.txt.
+It includes items not found in the official GFM specification, such as footnotes and additional tests for tables,
+task lists, etc.
+
+## Footnotes
+
+```````````````````````````````` example
+This is some text![^1]. Other text.[^footnote].
+
+Here's a thing[^other-note].
+
+And another thing[^codeblock-note].
+
+This doesn't have a referent[^nope].
+
+
+[^other-note]: no code block here (spaces are stripped away)
+
+[^codeblock-note]:
+ this is now a code block (8 spaces indentation)
+
+[^1]: Some *bolded* footnote definition.
+
+Hi!
+
+[^footnote]:
+ > Blockquotes can be in a footnote.
+
+ as well as code blocks
+
+ or, naturally, simple paragraphs.
+
+[^unused]: This is unused.
+.
+<p>This is some text!<sup class="footnote-ref"><a href="#fn-1" id="fnref-1" data-footnote-ref>1</a></sup>. Other text.<sup class="footnote-ref"><a href="#fn-footnote" id="fnref-footnote" data-footnote-ref>2</a></sup>.</p>
+<p>Here's a thing<sup class="footnote-ref"><a href="#fn-other-note" id="fnref-other-note" data-footnote-ref>3</a></sup>.</p>
+<p>And another thing<sup class="footnote-ref"><a href="#fn-codeblock-note" id="fnref-codeblock-note" data-footnote-ref>4</a></sup>.</p>
+<p>This doesn't have a referent[^nope].</p>
+<p>Hi!</p>
+<section class="footnotes" data-footnotes>
+<ol>
+<li id="fn-1">
+<p>Some <em>bolded</em> footnote definition. <a href="#fnref-1" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="1" aria-label="Back to reference 1">↩</a></p>
+</li>
+<li id="fn-footnote">
+<blockquote>
+<p>Blockquotes can be in a footnote.</p>
+</blockquote>
+<pre><code>as well as code blocks
+</code></pre>
+<p>or, naturally, simple paragraphs. <a href="#fnref-footnote" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="2" aria-label="Back to reference 2">↩</a></p>
+</li>
+<li id="fn-other-note">
+<p>no code block here (spaces are stripped away) <a href="#fnref-other-note" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="3" aria-label="Back to reference 3">↩</a></p>
+</li>
+<li id="fn-codeblock-note">
+<pre><code>this is now a code block (8 spaces indentation)
+</code></pre>
+<a href="#fnref-codeblock-note" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="4" aria-label="Back to reference 4">↩</a>
+</li>
+</ol>
+</section>
+````````````````````````````````
+
+## When a footnote is used multiple times, we insert multiple backrefs.
+
+```````````````````````````````` example
+This is some text. It has a footnote[^a-footnote].
+
+This footnote is referenced[^a-footnote] multiple times, in lots of different places.[^a-footnote]
+
+[^a-footnote]: This footnote definition should have three backrefs.
+.
+<p>This is some text. It has a footnote<sup class="footnote-ref"><a href="#fn-a-footnote" id="fnref-a-footnote" data-footnote-ref>1</a></sup>.</p>
+<p>This footnote is referenced<sup class="footnote-ref"><a href="#fn-a-footnote" id="fnref-a-footnote-2" data-footnote-ref>1</a></sup> multiple times, in lots of different places.<sup class="footnote-ref"><a href="#fn-a-footnote" id="fnref-a-footnote-3" data-footnote-ref>1</a></sup></p>
+<section class="footnotes" data-footnotes>
+<ol>
+<li id="fn-a-footnote">
+<p>This footnote definition should have three backrefs. <a href="#fnref-a-footnote" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="1" aria-label="Back to reference 1">↩</a> <a href="#fnref-a-footnote-2" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="1-2" aria-label="Back to reference 1-2">↩<sup class="footnote-ref">2</sup></a> <a href="#fnref-a-footnote-3" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="1-3" aria-label="Back to reference 1-3">↩<sup class="footnote-ref">3</sup></a></p>
+</li>
+</ol>
+</section>
+````````````````````````````````
+
+## Footnote reference labels are href escaped
+
+```````````````````````````````` example
+Hello[^"><script>alert(1)</script>]
+
+[^"><script>alert(1)</script>]: pwned
+.
+<p>Hello<sup class="footnote-ref"><a href="#fn-%22%3E%3Cscript%3Ealert(1)%3C/script%3E" id="fnref-%22%3E%3Cscript%3Ealert(1)%3C/script%3E" data-footnote-ref>1</a></sup></p>
+<section class="footnotes" data-footnotes>
+<ol>
+<li id="fn-%22%3E%3Cscript%3Ealert(1)%3C/script%3E">
+<p>pwned <a href="#fnref-%22%3E%3Cscript%3Ealert(1)%3C/script%3E" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="1" aria-label="Back to reference 1">↩</a></p>
+</li>
+</ol>
+</section>
+````````````````````````````````
+
+## Interop
+
+Autolink and strikethrough.
+
+```````````````````````````````` example
+~~www.google.com~~
+
+~~http://google.com~~
+.
+<p><del><a href="http://www.google.com">www.google.com</a></del></p>
+<p><del><a href="http://google.com">http://google.com</a></del></p>
+````````````````````````````````
+
+Autolink and tables.
+
+```````````````````````````````` example
+| a | b |
+| --- | --- |
+| https://github.com www.github.com | http://pokemon.com |
+.
+<table>
+<thead>
+<tr>
+<th>a</th>
+<th>b</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td><a href="https://github.com">https://github.com</a> <a href="http://www.github.com">www.github.com</a></td>
+<td><a href="http://pokemon.com">http://pokemon.com</a></td>
+</tr>
+</tbody>
+</table>
+````````````````````````````````
+
+## Task lists
+
+```````````````````````````````` example
+- [ ] foo
+- [x] bar
+.
+<ul>
+<li><input type="checkbox" disabled="" /> foo</li>
+<li><input type="checkbox" checked="" disabled="" /> bar</li>
+</ul>
+````````````````````````````````
+
+Show that a task list and a regular list get processed the same in
+the way that sublists are created. If something works in a list
+item, then it should work the same way with a task. The only
+difference should be the tasklist marker. So, if we use something
+other than a space or x, it won't be recognized as a task item, and
+so will be treated as a regular item.
+
+```````````````````````````````` example
+- [x] foo
+ - [ ] bar
+ - [x] baz
+- [ ] bim
+
+Show a regular (non task) list to show that it has the same structure
+- [@] foo
+ - [@] bar
+ - [@] baz
+- [@] bim
+.
+<ul>
+<li><input type="checkbox" checked="" disabled="" /> foo
+<ul>
+<li><input type="checkbox" disabled="" /> bar</li>
+<li><input type="checkbox" checked="" disabled="" /> baz</li>
+</ul>
+</li>
+<li><input type="checkbox" disabled="" /> bim</li>
+</ul>
+<p>Show a regular (non task) list to show that it has the same structure</p>
+<ul>
+<li>[@] foo
+<ul>
+<li>[@] bar</li>
+<li>[@] baz</li>
+</ul>
+</li>
+<li>[@] bim</li>
+</ul>
+````````````````````````````````
+Use a larger indent -- a task list and a regular list should produce
+the same structure.
+
+```````````````````````````````` example
+- [x] foo
+ - [ ] bar
+ - [x] baz
+- [ ] bim
+
+Show a regular (non task) list to show that it has the same structure
+- [@] foo
+ - [@] bar
+ - [@] baz
+- [@] bim
+.
+<ul>
+<li><input type="checkbox" checked="" disabled="" /> foo
+<ul>
+<li><input type="checkbox" disabled="" /> bar</li>
+<li><input type="checkbox" checked="" disabled="" /> baz</li>
+</ul>
+</li>
+<li><input type="checkbox" disabled="" /> bim</li>
+</ul>
+<p>Show a regular (non task) list to show that it has the same structure</p>
+<ul>
+<li>[@] foo
+<ul>
+<li>[@] bar</li>
+<li>[@] baz</li>
+</ul>
+</li>
+<li>[@] bim</li>
+</ul>
+````````````````````````````````
+
+<!-- end of the "GFM undocumented extensions and more robust test" section -->
+
<!-- END TESTS -->
diff --git a/glfm_specification/output_example_snapshots/examples_index.yml b/glfm_specification/output_example_snapshots/examples_index.yml
index da9420ffa85..f55ad43eb48 100644
--- a/glfm_specification/output_example_snapshots/examples_index.yml
+++ b/glfm_specification/output_example_snapshots/examples_index.yml
@@ -2255,3 +2255,27 @@
08_06_00__gitlab_internal_extension_markdown__footnotes__001:
spec_example_position: 754
source_specification: gitlab
+09_00_00__gfm_undocumented_extensions_and_more_robust_test__footnotes__002:
+ spec_example_position: 755
+ source_specification: commonmark
+? 09_01_00__gfm_undocumented_extensions_and_more_robust_test__when_a_footnote_is_used_multiple_times,_we_insert_multiple_backrefs.__001
+: spec_example_position: 756
+ source_specification: commonmark
+09_02_00__gfm_undocumented_extensions_and_more_robust_test__footnote_reference_labels_are_href_escaped__001:
+ spec_example_position: 757
+ source_specification: commonmark
+09_03_00__gfm_undocumented_extensions_and_more_robust_test__interop__001:
+ spec_example_position: 758
+ source_specification: commonmark
+09_03_00__gfm_undocumented_extensions_and_more_robust_test__interop__002:
+ spec_example_position: 759
+ source_specification: commonmark
+09_04_00__gfm_undocumented_extensions_and_more_robust_test__task_lists__001:
+ spec_example_position: 760
+ source_specification: commonmark
+09_04_00__gfm_undocumented_extensions_and_more_robust_test__task_lists__002:
+ spec_example_position: 761
+ source_specification: commonmark
+09_04_00__gfm_undocumented_extensions_and_more_robust_test__task_lists__003:
+ spec_example_position: 762
+ source_specification: commonmark
diff --git a/glfm_specification/output_example_snapshots/html.yml b/glfm_specification/output_example_snapshots/html.yml
index a301ab48fc0..ab98ee36a1d 100644
--- a/glfm_specification/output_example_snapshots/html.yml
+++ b/glfm_specification/output_example_snapshots/html.yml
@@ -8650,3 +8650,278 @@
wysiwyg: |-
<p dir="auto">footnote reference tag <sup dir="auto" identifier="fortytwo">fortytwo</sup></p>
<div node="footnoteDefinition(paragraph(&quot;footnote text&quot;))" htmlattributes="[object Object]"><p dir="auto">footnote text</p></div>
+09_00_00__gfm_undocumented_extensions_and_more_robust_test__footnotes__002:
+ canonical: |
+ <p>This is some text!<sup class="footnote-ref"><a href="#fn-1" id="fnref-1" data-footnote-ref>1</a></sup>. Other text.<sup class="footnote-ref"><a href="#fn-footnote" id="fnref-footnote" data-footnote-ref>2</a></sup>.</p>
+ <p>Here's a thing<sup class="footnote-ref"><a href="#fn-other-note" id="fnref-other-note" data-footnote-ref>3</a></sup>.</p>
+ <p>And another thing<sup class="footnote-ref"><a href="#fn-codeblock-note" id="fnref-codeblock-note" data-footnote-ref>4</a></sup>.</p>
+ <p>This doesn't have a referent[^nope].</p>
+ <p>Hi!</p>
+ <section class="footnotes" data-footnotes>
+ <ol>
+ <li id="fn-1">
+ <p>Some <em>bolded</em> footnote definition. <a href="#fnref-1" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="1" aria-label="Back to reference 1">↩</a></p>
+ </li>
+ <li id="fn-footnote">
+ <blockquote>
+ <p>Blockquotes can be in a footnote.</p>
+ </blockquote>
+ <pre><code>as well as code blocks
+ </code></pre>
+ <p>or, naturally, simple paragraphs. <a href="#fnref-footnote" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="2" aria-label="Back to reference 2">↩</a></p>
+ </li>
+ <li id="fn-other-note">
+ <p>no code block here (spaces are stripped away) <a href="#fnref-other-note" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="3" aria-label="Back to reference 3">↩</a></p>
+ </li>
+ <li id="fn-codeblock-note">
+ <pre><code>this is now a code block (8 spaces indentation)
+ </code></pre>
+ <a href="#fnref-codeblock-note" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="4" aria-label="Back to reference 4">↩</a>
+ </li>
+ </ol>
+ </section>
+ static: |-
+ <p data-sourcepos="1:1-1:47" dir="auto">This is some text!<sup class="footnote-ref"><a href="#fn-1-42" id="fnref-1-42" data-footnote-ref>1</a></sup>. Other text.<sup class="footnote-ref"><a href="#fn-footnote-42" id="fnref-footnote-42" data-footnote-ref>2</a></sup>.</p>
+ <p data-sourcepos="3:1-3:28" dir="auto">Here's a thing<sup class="footnote-ref"><a href="#fn-other-note-42" id="fnref-other-note-42" data-footnote-ref>3</a></sup>.</p>
+ <p data-sourcepos="5:1-5:35" dir="auto">And another thing<sup class="footnote-ref"><a href="#fn-codeblock-note-42" id="fnref-codeblock-note-42" data-footnote-ref>4</a></sup>.</p>
+ <p data-sourcepos="7:1-7:36" dir="auto">This doesn't have a referent[^nope].</p>
+ <p data-sourcepos="17:1-17:3" dir="auto">Hi!</p>
+ <section data-footnotes class="footnotes">
+ <ol>
+ <li id="fn-1-42">
+ <p data-sourcepos="15:7-15:40">Some <em>bolded</em> footnote definition. <a href="#fnref-1-42" data-footnote-backref data-footnote-backref-idx="1" aria-label="Back to reference 1" class="footnote-backref"><gl-emoji title="leftwards arrow with hook" data-name="leftwards_arrow_with_hook" data-unicode-version="1.1">↩</gl-emoji></a></p>
+ </li>
+ <li id="fn-footnote-42">
+ <blockquote data-sourcepos="20:5-20:39">
+ <p data-sourcepos="20:7-20:39">Blockquotes can be in a footnote.</p>
+ </blockquote>
+ <div class="gl-relative markdown-code-block js-markdown-code">
+ <pre data-sourcepos="22:9-23:0" class="code highlight js-syntax-highlight language-plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">as well as code blocks</span></code></pre>
+ <copy-code></copy-code>
+ </div>
+ <p data-sourcepos="24:5-24:37">or, naturally, simple paragraphs. <a href="#fnref-footnote-42" data-footnote-backref data-footnote-backref-idx="2" aria-label="Back to reference 2" class="footnote-backref"><gl-emoji title="leftwards arrow with hook" data-name="leftwards_arrow_with_hook" data-unicode-version="1.1">↩</gl-emoji></a></p>
+ </li>
+ <li id="fn-other-note-42">
+ <p data-sourcepos="10:22-10:66">no code block here (spaces are stripped away) <a href="#fnref-other-note-42" data-footnote-backref data-footnote-backref-idx="3" aria-label="Back to reference 3" class="footnote-backref"><gl-emoji title="leftwards arrow with hook" data-name="leftwards_arrow_with_hook" data-unicode-version="1.1">↩</gl-emoji></a></p>
+ </li>
+ <li id="fn-codeblock-note-42">
+ <div class="gl-relative markdown-code-block js-markdown-code">
+ <pre data-sourcepos="13:9-14:0" class="code highlight js-syntax-highlight language-plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">this is now a code block (8 spaces indentation)</span></code></pre>
+ <copy-code></copy-code>
+ </div>
+ <a href="#fnref-codeblock-note-42" data-footnote-backref data-footnote-backref-idx="4" aria-label="Back to reference 4" class="footnote-backref"><gl-emoji title="leftwards arrow with hook" data-name="leftwards_arrow_with_hook" data-unicode-version="1.1">↩</gl-emoji></a>
+ </li>
+ </ol>
+ </section>
+ wysiwyg: |-
+ <p dir="auto">This is some text!<sup dir="auto" identifier="1">1</sup>. Other text.<sup dir="auto" identifier="footnote">footnote</sup>.</p>
+ <p dir="auto">Here's a thing<sup dir="auto" identifier="other-note">other-note</sup>.</p>
+ <p dir="auto">And another thing<sup dir="auto" identifier="codeblock-note">codeblock-note</sup>.</p>
+ <p dir="auto">This doesn't have a referent[^nope].</p>
+ <div node="footnoteDefinition(paragraph(&quot;no code block here (spaces are stripped away)&quot;))" htmlattributes="[object Object]"><p dir="auto">no code block here (spaces are stripped away)</p></div>
+ <div node="footnoteDefinition(paragraph(&quot;Some &quot;, italic(&quot;bolded&quot;), &quot; footnote definition.&quot;))" htmlattributes="[object Object]"><p dir="auto">Some <em>bolded</em> footnote definition.</p></div>
+ <p dir="auto">Hi!</p>
+ <div node="footnoteDefinition(paragraph(&quot;This is unused.&quot;))" htmlattributes="[object Object]"><p dir="auto">This is unused.</p></div>
+? 09_01_00__gfm_undocumented_extensions_and_more_robust_test__when_a_footnote_is_used_multiple_times,_we_insert_multiple_backrefs.__001
+: canonical: |
+ <p>This is some text. It has a footnote<sup class="footnote-ref"><a href="#fn-a-footnote" id="fnref-a-footnote" data-footnote-ref>1</a></sup>.</p>
+ <p>This footnote is referenced<sup class="footnote-ref"><a href="#fn-a-footnote" id="fnref-a-footnote-2" data-footnote-ref>1</a></sup> multiple times, in lots of different places.<sup class="footnote-ref"><a href="#fn-a-footnote" id="fnref-a-footnote-3" data-footnote-ref>1</a></sup></p>
+ <section class="footnotes" data-footnotes>
+ <ol>
+ <li id="fn-a-footnote">
+ <p>This footnote definition should have three backrefs. <a href="#fnref-a-footnote" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="1" aria-label="Back to reference 1">↩</a> <a href="#fnref-a-footnote-2" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="1-2" aria-label="Back to reference 1-2">↩<sup class="footnote-ref">2</sup></a> <a href="#fnref-a-footnote-3" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="1-3" aria-label="Back to reference 1-3">↩<sup class="footnote-ref">3</sup></a></p>
+ </li>
+ </ol>
+ </section>
+ static: |-
+ <p data-sourcepos="1:1-1:50" dir="auto">This is some text. It has a footnote<sup class="footnote-ref"><a href="#fn-a-footnote-42" id="fnref-a-footnote-42" data-footnote-ref>1</a></sup>.</p>
+ <p data-sourcepos="3:1-3:98" dir="auto">This footnote is referenced<sup><a href="#fn-a-footnote" id="fnref-a-footnote-2" data-footnote-ref>1</a></sup> multiple times, in lots of different places.<sup><a href="#fn-a-footnote" id="fnref-a-footnote-3" data-footnote-ref>1</a></sup></p>
+ <section data-footnotes class="footnotes">
+ <ol>
+ <li id="fn-a-footnote-42">
+ <p data-sourcepos="5:16-5:67">This footnote definition should have three backrefs. <a href="#fnref-a-footnote-42" data-footnote-backref data-footnote-backref-idx="1" aria-label="Back to reference 1" class="footnote-backref"><gl-emoji title="leftwards arrow with hook" data-name="leftwards_arrow_with_hook" data-unicode-version="1.1">↩</gl-emoji></a> <a href="#fnref-a-footnote-2" data-footnote-backref data-footnote-backref-idx="1-2" aria-label="Back to reference 1-2"><gl-emoji title="leftwards arrow with hook" data-name="leftwards_arrow_with_hook" data-unicode-version="1.1">↩</gl-emoji><sup>2</sup></a> <a href="#fnref-a-footnote-3" data-footnote-backref data-footnote-backref-idx="1-3" aria-label="Back to reference 1-3"><gl-emoji title="leftwards arrow with hook" data-name="leftwards_arrow_with_hook" data-unicode-version="1.1">↩</gl-emoji><sup>3</sup></a></p>
+ </li>
+ </ol>
+ </section>
+ wysiwyg: |-
+ <p dir="auto">This is some text. It has a footnote<sup dir="auto" identifier="a-footnote">a-footnote</sup>.</p>
+ <p dir="auto">This footnote is referenced<sup dir="auto" identifier="a-footnote">a-footnote</sup> multiple times, in lots of different places.<sup dir="auto" identifier="a-footnote">a-footnote</sup></p>
+ <div node="footnoteDefinition(paragraph(&quot;This footnote definition should have three backrefs.&quot;))" htmlattributes="[object Object]"><p dir="auto">This footnote definition should have three backrefs.</p></div>
+09_02_00__gfm_undocumented_extensions_and_more_robust_test__footnote_reference_labels_are_href_escaped__001:
+ canonical: |
+ <p>Hello<sup class="footnote-ref"><a href="#fn-%22%3E%3Cscript%3Ealert(1)%3C/script%3E" id="fnref-%22%3E%3Cscript%3Ealert(1)%3C/script%3E" data-footnote-ref>1</a></sup></p>
+ <section class="footnotes" data-footnotes>
+ <ol>
+ <li id="fn-%22%3E%3Cscript%3Ealert(1)%3C/script%3E">
+ <p>pwned <a href="#fnref-%22%3E%3Cscript%3Ealert(1)%3C/script%3E" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="1" aria-label="Back to reference 1">↩</a></p>
+ </li>
+ </ol>
+ </section>
+ static: |-
+ <p data-sourcepos="1:1-1:35" dir="auto">Hello<sup class="footnote-ref"><a href="#fn-%22%3E%3Cscript%3Ealert(1)%3C/script%3E-42" id="fnref-%22%3E%3Cscript%3Ealert(1)%3C/script%3E-42" data-footnote-ref>1</a></sup></p>
+ <section data-footnotes class="footnotes">
+ <ol>
+ <li id="fn-%22%3E%3Cscript%3Ealert(1)%3C/script%3E-42">
+ <p data-sourcepos="3:33-3:37">pwned <a href="#fnref-%22%3E%3Cscript%3Ealert(1)%3C/script%3E-42" data-footnote-backref data-footnote-backref-idx="1" aria-label="Back to reference 1" class="footnote-backref"><gl-emoji title="leftwards arrow with hook" data-name="leftwards_arrow_with_hook" data-unicode-version="1.1">↩</gl-emoji></a></p>
+ </li>
+ </ol>
+ </section>
+ wysiwyg: |-
+ <p dir="auto">Hello<sup dir="auto" identifier="&quot;><script>alert(1)</script>">"&gt;&lt;script&gt;alert(1)&lt;/script&gt;</sup></p>
+ <div node="footnoteDefinition(paragraph(&quot;pwned&quot;))" htmlattributes="[object Object]"><p dir="auto">pwned</p></div>
+09_03_00__gfm_undocumented_extensions_and_more_robust_test__interop__001:
+ canonical: |
+ <p><del><a href="http://www.google.com">www.google.com</a></del></p>
+ <p><del><a href="http://google.com">http://google.com</a></del></p>
+ static: |-
+ <p data-sourcepos="1:1-1:18" dir="auto"><del><a href="http://www.google.com" rel="nofollow noreferrer noopener" target="_blank">www.google.com</a></del></p>
+ <p data-sourcepos="3:1-3:21" dir="auto"><del><a href="http://google.com" rel="nofollow noreferrer noopener" target="_blank">http://google.com</a></del></p>
+ wysiwyg: |-
+ <p dir="auto"><a target="_blank" rel="noopener noreferrer nofollow" href="http://www.google.com"><s>www.google.com</s></a></p>
+ <p dir="auto"><a target="_blank" rel="noopener noreferrer nofollow" href="http://google.com"><s>http://google.com</s></a></p>
+09_03_00__gfm_undocumented_extensions_and_more_robust_test__interop__002:
+ canonical: |
+ <table>
+ <thead>
+ <tr>
+ <th>a</th>
+ <th>b</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td><a href="https://github.com">https://github.com</a> <a href="http://www.github.com">www.github.com</a></td>
+ <td><a href="http://pokemon.com">http://pokemon.com</a></td>
+ </tr>
+ </tbody>
+ </table>
+ static: |-
+ <table data-sourcepos="1:1-3:58" dir="auto">
+ <thead>
+ <tr data-sourcepos="1:1-1:9">
+ <th data-sourcepos="1:2-1:4">a</th>
+ <th data-sourcepos="1:6-1:8">b</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr data-sourcepos="3:1-3:58">
+ <td data-sourcepos="3:2-3:36">
+ <a href="https://github.com" rel="nofollow noreferrer noopener" target="_blank">https://github.com</a> <a href="http://www.github.com" rel="nofollow noreferrer noopener" target="_blank">www.github.com</a>
+ </td>
+ <td data-sourcepos="3:38-3:57"><a href="http://pokemon.com" rel="nofollow noreferrer noopener" target="_blank">http://pokemon.com</a></td>
+ </tr>
+ </tbody>
+ </table>
+ wysiwyg: |-
+ <table><tbody><tr><th colspan="1" rowspan="1"><p dir="auto">a</p></th><th colspan="1" rowspan="1"><p dir="auto">b</p></th></tr><tr><td colspan="1" rowspan="1"><p dir="auto"><a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com">https://github.com</a><a target="_blank" rel="noopener noreferrer nofollow" href="http://www.github.com">www.github.com</a></p></td><td colspan="1" rowspan="1"><p dir="auto"><a target="_blank" rel="noopener noreferrer nofollow" href="http://pokemon.com">http://pokemon.com</a></p></td></tr></tbody></table>
+09_04_00__gfm_undocumented_extensions_and_more_robust_test__task_lists__001:
+ canonical: |
+ <ul>
+ <li><input type="checkbox" disabled="" /> foo</li>
+ <li><input type="checkbox" checked="" disabled="" /> bar</li>
+ </ul>
+ static: |-
+ <ul data-sourcepos="1:1-2:9" class="task-list" dir="auto">
+ <li data-sourcepos="1:1-1:9" class="task-list-item">
+ <task-button></task-button><input type="checkbox" class="task-list-item-checkbox" disabled> foo</li>
+ <li data-sourcepos="2:1-2:9" class="task-list-item">
+ <task-button></task-button><input type="checkbox" class="task-list-item-checkbox" checked disabled> bar</li>
+ </ul>
+ wysiwyg: |-
+ <ul dir="auto" start="1" parens="false" data-type="taskList"><li dir="auto" data-checked="false" data-type="taskItem"><label><input type="checkbox"><span></span></label><div><p dir="auto">foo</p></div></li><li dir="auto" data-checked="true" data-type="taskItem"><label><input type="checkbox" checked="checked"><span></span></label><div><p dir="auto">bar</p></div></li></ul>
+09_04_00__gfm_undocumented_extensions_and_more_robust_test__task_lists__002:
+ canonical: |
+ <ul>
+ <li><input type="checkbox" checked="" disabled="" /> foo
+ <ul>
+ <li><input type="checkbox" disabled="" /> bar</li>
+ <li><input type="checkbox" checked="" disabled="" /> baz</li>
+ </ul>
+ </li>
+ <li><input type="checkbox" disabled="" /> bim</li>
+ </ul>
+ <p>Show a regular (non task) list to show that it has the same structure</p>
+ <ul>
+ <li>[@] foo
+ <ul>
+ <li>[@] bar</li>
+ <li>[@] baz</li>
+ </ul>
+ </li>
+ <li>[@] bim</li>
+ </ul>
+ static: |-
+ <ul data-sourcepos="1:1-5:0" class="task-list" dir="auto">
+ <li data-sourcepos="1:1-3:11" class="task-list-item">
+ <task-button></task-button><input type="checkbox" class="task-list-item-checkbox" checked disabled> foo
+ <ul data-sourcepos="2:3-3:11" class="task-list">
+ <li data-sourcepos="2:3-2:11" class="task-list-item">
+ <task-button></task-button><input type="checkbox" class="task-list-item-checkbox" disabled> bar</li>
+ <li data-sourcepos="3:3-3:11" class="task-list-item">
+ <task-button></task-button><input type="checkbox" class="task-list-item-checkbox" checked disabled> baz</li>
+ </ul>
+ </li>
+ <li data-sourcepos="4:1-5:0" class="task-list-item">
+ <task-button></task-button><input type="checkbox" class="task-list-item-checkbox" disabled> bim</li>
+ </ul>
+ <p data-sourcepos="6:1-6:69" dir="auto">Show a regular (non task) list to show that it has the same structure</p>
+ <ul data-sourcepos="7:1-10:9" dir="auto">
+ <li data-sourcepos="7:1-9:11">[@] foo
+ <ul data-sourcepos="8:3-9:11">
+ <li data-sourcepos="8:3-8:11">[@] bar</li>
+ <li data-sourcepos="9:3-9:11">[@] baz</li>
+ </ul>
+ </li>
+ <li data-sourcepos="10:1-10:9">[@] bim</li>
+ </ul>
+ wysiwyg: |-
+ Error - check implementation:
+ Cannot read properties of undefined (reading 'start')
+09_04_00__gfm_undocumented_extensions_and_more_robust_test__task_lists__003:
+ canonical: |
+ <ul>
+ <li><input type="checkbox" checked="" disabled="" /> foo
+ <ul>
+ <li><input type="checkbox" disabled="" /> bar</li>
+ <li><input type="checkbox" checked="" disabled="" /> baz</li>
+ </ul>
+ </li>
+ <li><input type="checkbox" disabled="" /> bim</li>
+ </ul>
+ <p>Show a regular (non task) list to show that it has the same structure</p>
+ <ul>
+ <li>[@] foo
+ <ul>
+ <li>[@] bar</li>
+ <li>[@] baz</li>
+ </ul>
+ </li>
+ <li>[@] bim</li>
+ </ul>
+ static: |-
+ <ul data-sourcepos="1:1-5:0" class="task-list" dir="auto">
+ <li data-sourcepos="1:1-3:13" class="task-list-item">
+ <task-button></task-button><input type="checkbox" class="task-list-item-checkbox" checked disabled> foo
+ <ul data-sourcepos="2:5-3:13" class="task-list">
+ <li data-sourcepos="2:5-2:13" class="task-list-item">
+ <task-button></task-button><input type="checkbox" class="task-list-item-checkbox" disabled> bar</li>
+ <li data-sourcepos="3:5-3:13" class="task-list-item">
+ <task-button></task-button><input type="checkbox" class="task-list-item-checkbox" checked disabled> baz</li>
+ </ul>
+ </li>
+ <li data-sourcepos="4:1-5:0" class="task-list-item">
+ <task-button></task-button><input type="checkbox" class="task-list-item-checkbox" disabled> bim</li>
+ </ul>
+ <p data-sourcepos="6:1-6:69" dir="auto">Show a regular (non task) list to show that it has the same structure</p>
+ <ul data-sourcepos="7:1-10:9" dir="auto">
+ <li data-sourcepos="7:1-9:13">[@] foo
+ <ul data-sourcepos="8:5-9:13">
+ <li data-sourcepos="8:5-8:13">[@] bar</li>
+ <li data-sourcepos="9:5-9:13">[@] baz</li>
+ </ul>
+ </li>
+ <li data-sourcepos="10:1-10:9">[@] bim</li>
+ </ul>
+ wysiwyg: |-
+ Error - check implementation:
+ Cannot read properties of undefined (reading 'start')
diff --git a/glfm_specification/output_example_snapshots/markdown.yml b/glfm_specification/output_example_snapshots/markdown.yml
index bb41e676002..926df94e0f4 100644
--- a/glfm_specification/output_example_snapshots/markdown.yml
+++ b/glfm_specification/output_example_snapshots/markdown.yml
@@ -2557,3 +2557,74 @@
footnote reference tag [^fortytwo]
[^fortytwo]: footnote text
+09_00_00__gfm_undocumented_extensions_and_more_robust_test__footnotes__002: |
+ This is some text![^1]. Other text.[^footnote].
+
+ Here's a thing[^other-note].
+
+ And another thing[^codeblock-note].
+
+ This doesn't have a referent[^nope].
+
+
+ [^other-note]: no code block here (spaces are stripped away)
+
+ [^codeblock-note]:
+ this is now a code block (8 spaces indentation)
+
+ [^1]: Some *bolded* footnote definition.
+
+ Hi!
+
+ [^footnote]:
+ > Blockquotes can be in a footnote.
+
+ as well as code blocks
+
+ or, naturally, simple paragraphs.
+
+ [^unused]: This is unused.
+? 09_01_00__gfm_undocumented_extensions_and_more_robust_test__when_a_footnote_is_used_multiple_times,_we_insert_multiple_backrefs.__001
+: |
+ This is some text. It has a footnote[^a-footnote].
+
+ This footnote is referenced[^a-footnote] multiple times, in lots of different places.[^a-footnote]
+
+ [^a-footnote]: This footnote definition should have three backrefs.
+09_02_00__gfm_undocumented_extensions_and_more_robust_test__footnote_reference_labels_are_href_escaped__001: |
+ Hello[^"><script>alert(1)</script>]
+
+ [^"><script>alert(1)</script>]: pwned
+09_03_00__gfm_undocumented_extensions_and_more_robust_test__interop__001: |
+ ~~www.google.com~~
+
+ ~~http://google.com~~
+09_03_00__gfm_undocumented_extensions_and_more_robust_test__interop__002: |
+ | a | b |
+ | --- | --- |
+ | https://github.com www.github.com | http://pokemon.com |
+09_04_00__gfm_undocumented_extensions_and_more_robust_test__task_lists__001: |
+ - [ ] foo
+ - [x] bar
+09_04_00__gfm_undocumented_extensions_and_more_robust_test__task_lists__002: |
+ - [x] foo
+ - [ ] bar
+ - [x] baz
+ - [ ] bim
+
+ Show a regular (non task) list to show that it has the same structure
+ - [@] foo
+ - [@] bar
+ - [@] baz
+ - [@] bim
+09_04_00__gfm_undocumented_extensions_and_more_robust_test__task_lists__003: |
+ - [x] foo
+ - [ ] bar
+ - [x] baz
+ - [ ] bim
+
+ Show a regular (non task) list to show that it has the same structure
+ - [@] foo
+ - [@] bar
+ - [@] baz
+ - [@] bim
diff --git a/glfm_specification/output_example_snapshots/prosemirror_json.yml b/glfm_specification/output_example_snapshots/prosemirror_json.yml
index bc6293b54b2..00c805b6928 100644
--- a/glfm_specification/output_example_snapshots/prosemirror_json.yml
+++ b/glfm_specification/output_example_snapshots/prosemirror_json.yml
@@ -23425,3 +23425,527 @@
}
]
}
+09_00_00__gfm_undocumented_extensions_and_more_robust_test__footnotes__002: |-
+ {
+ "type": "doc",
+ "content": [
+ {
+ "type": "paragraph",
+ "content": [
+ {
+ "type": "text",
+ "text": "This is some text!"
+ },
+ {
+ "type": "footnoteReference",
+ "attrs": {
+ "identifier": "1",
+ "label": "1"
+ }
+ },
+ {
+ "type": "text",
+ "text": ". Other text."
+ },
+ {
+ "type": "footnoteReference",
+ "attrs": {
+ "identifier": "footnote",
+ "label": "footnote"
+ }
+ },
+ {
+ "type": "text",
+ "text": "."
+ }
+ ]
+ },
+ {
+ "type": "paragraph",
+ "content": [
+ {
+ "type": "text",
+ "text": "Here's a thing"
+ },
+ {
+ "type": "footnoteReference",
+ "attrs": {
+ "identifier": "other-note",
+ "label": "other-note"
+ }
+ },
+ {
+ "type": "text",
+ "text": "."
+ }
+ ]
+ },
+ {
+ "type": "paragraph",
+ "content": [
+ {
+ "type": "text",
+ "text": "And another thing"
+ },
+ {
+ "type": "footnoteReference",
+ "attrs": {
+ "identifier": "codeblock-note",
+ "label": "codeblock-note"
+ }
+ },
+ {
+ "type": "text",
+ "text": "."
+ }
+ ]
+ },
+ {
+ "type": "paragraph",
+ "content": [
+ {
+ "type": "text",
+ "text": "This doesn't have a referent[^nope]."
+ }
+ ]
+ },
+ {
+ "type": "footnoteDefinition",
+ "attrs": {
+ "identifier": "other-note",
+ "label": "other-note"
+ },
+ "content": [
+ {
+ "type": "paragraph",
+ "content": [
+ {
+ "type": "text",
+ "text": "no code block here (spaces are stripped away)"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "footnoteDefinition",
+ "attrs": {
+ "identifier": "1",
+ "label": "1"
+ },
+ "content": [
+ {
+ "type": "paragraph",
+ "content": [
+ {
+ "type": "text",
+ "text": "Some "
+ },
+ {
+ "type": "text",
+ "marks": [
+ {
+ "type": "italic"
+ }
+ ],
+ "text": "bolded"
+ },
+ {
+ "type": "text",
+ "text": " footnote definition."
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "paragraph",
+ "content": [
+ {
+ "type": "text",
+ "text": "Hi!"
+ }
+ ]
+ },
+ {
+ "type": "footnoteDefinition",
+ "attrs": {
+ "identifier": "unused",
+ "label": "unused"
+ },
+ "content": [
+ {
+ "type": "paragraph",
+ "content": [
+ {
+ "type": "text",
+ "text": "This is unused."
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+? 09_01_00__gfm_undocumented_extensions_and_more_robust_test__when_a_footnote_is_used_multiple_times,_we_insert_multiple_backrefs.__001
+: |-
+ {
+ "type": "doc",
+ "content": [
+ {
+ "type": "paragraph",
+ "content": [
+ {
+ "type": "text",
+ "text": "This is some text. It has a footnote"
+ },
+ {
+ "type": "footnoteReference",
+ "attrs": {
+ "identifier": "a-footnote",
+ "label": "a-footnote"
+ }
+ },
+ {
+ "type": "text",
+ "text": "."
+ }
+ ]
+ },
+ {
+ "type": "paragraph",
+ "content": [
+ {
+ "type": "text",
+ "text": "This footnote is referenced"
+ },
+ {
+ "type": "footnoteReference",
+ "attrs": {
+ "identifier": "a-footnote",
+ "label": "a-footnote"
+ }
+ },
+ {
+ "type": "text",
+ "text": " multiple times, in lots of different places."
+ },
+ {
+ "type": "footnoteReference",
+ "attrs": {
+ "identifier": "a-footnote",
+ "label": "a-footnote"
+ }
+ }
+ ]
+ },
+ {
+ "type": "footnoteDefinition",
+ "attrs": {
+ "identifier": "a-footnote",
+ "label": "a-footnote"
+ },
+ "content": [
+ {
+ "type": "paragraph",
+ "content": [
+ {
+ "type": "text",
+ "text": "This footnote definition should have three backrefs."
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+09_02_00__gfm_undocumented_extensions_and_more_robust_test__footnote_reference_labels_are_href_escaped__001: |-
+ {
+ "type": "doc",
+ "content": [
+ {
+ "type": "paragraph",
+ "content": [
+ {
+ "type": "text",
+ "text": "Hello"
+ },
+ {
+ "type": "footnoteReference",
+ "attrs": {
+ "identifier": "\"><script>alert(1)</script>",
+ "label": "\"><script>alert(1)</script>"
+ }
+ }
+ ]
+ },
+ {
+ "type": "footnoteDefinition",
+ "attrs": {
+ "identifier": "\"><script>alert(1)</script>",
+ "label": "\"><script>alert(1)</script>"
+ },
+ "content": [
+ {
+ "type": "paragraph",
+ "content": [
+ {
+ "type": "text",
+ "text": "pwned"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+09_03_00__gfm_undocumented_extensions_and_more_robust_test__interop__001: |-
+ {
+ "type": "doc",
+ "content": [
+ {
+ "type": "paragraph",
+ "content": [
+ {
+ "type": "text",
+ "marks": [
+ {
+ "type": "link",
+ "attrs": {
+ "uploading": false,
+ "href": "http://www.google.com",
+ "title": null,
+ "canonicalSrc": "http://www.google.com",
+ "isReference": false
+ }
+ },
+ {
+ "type": "strike"
+ }
+ ],
+ "text": "www.google.com"
+ }
+ ]
+ },
+ {
+ "type": "paragraph",
+ "content": [
+ {
+ "type": "text",
+ "marks": [
+ {
+ "type": "link",
+ "attrs": {
+ "uploading": false,
+ "href": "http://google.com",
+ "title": null,
+ "canonicalSrc": "http://google.com",
+ "isReference": false
+ }
+ },
+ {
+ "type": "strike"
+ }
+ ],
+ "text": "http://google.com"
+ }
+ ]
+ }
+ ]
+ }
+09_03_00__gfm_undocumented_extensions_and_more_robust_test__interop__002: |-
+ {
+ "type": "doc",
+ "content": [
+ {
+ "type": "table",
+ "attrs": {
+ "isMarkdown": null
+ },
+ "content": [
+ {
+ "type": "tableRow",
+ "content": [
+ {
+ "type": "tableHeader",
+ "attrs": {
+ "colspan": 1,
+ "rowspan": 1,
+ "colwidth": null
+ },
+ "content": [
+ {
+ "type": "paragraph",
+ "content": [
+ {
+ "type": "text",
+ "text": "a"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "tableHeader",
+ "attrs": {
+ "colspan": 1,
+ "rowspan": 1,
+ "colwidth": null
+ },
+ "content": [
+ {
+ "type": "paragraph",
+ "content": [
+ {
+ "type": "text",
+ "text": "b"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "tableRow",
+ "content": [
+ {
+ "type": "tableCell",
+ "attrs": {
+ "colspan": 1,
+ "rowspan": 1,
+ "colwidth": null
+ },
+ "content": [
+ {
+ "type": "paragraph",
+ "content": [
+ {
+ "type": "text",
+ "marks": [
+ {
+ "type": "link",
+ "attrs": {
+ "uploading": false,
+ "href": "https://github.com",
+ "title": null,
+ "canonicalSrc": "https://github.com",
+ "isReference": false
+ }
+ }
+ ],
+ "text": "https://github.com"
+ },
+ {
+ "type": "text",
+ "marks": [
+ {
+ "type": "link",
+ "attrs": {
+ "uploading": false,
+ "href": "http://www.github.com",
+ "title": null,
+ "canonicalSrc": "http://www.github.com",
+ "isReference": false
+ }
+ }
+ ],
+ "text": "www.github.com"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "tableCell",
+ "attrs": {
+ "colspan": 1,
+ "rowspan": 1,
+ "colwidth": null
+ },
+ "content": [
+ {
+ "type": "paragraph",
+ "content": [
+ {
+ "type": "text",
+ "marks": [
+ {
+ "type": "link",
+ "attrs": {
+ "uploading": false,
+ "href": "http://pokemon.com",
+ "title": null,
+ "canonicalSrc": "http://pokemon.com",
+ "isReference": false
+ }
+ }
+ ],
+ "text": "http://pokemon.com"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+09_04_00__gfm_undocumented_extensions_and_more_robust_test__task_lists__001: |-
+ {
+ "type": "doc",
+ "content": [
+ {
+ "type": "taskList",
+ "attrs": {
+ "numeric": false,
+ "start": 1,
+ "parens": false
+ },
+ "content": [
+ {
+ "type": "taskItem",
+ "attrs": {
+ "checked": false
+ },
+ "content": [
+ {
+ "type": "paragraph",
+ "content": [
+ {
+ "type": "text",
+ "text": "foo"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "taskItem",
+ "attrs": {
+ "checked": true
+ },
+ "content": [
+ {
+ "type": "paragraph",
+ "content": [
+ {
+ "type": "text",
+ "text": "bar"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+09_04_00__gfm_undocumented_extensions_and_more_robust_test__task_lists__002: |-
+ Error - check implementation:
+ Cannot read properties of undefined (reading 'start')
+09_04_00__gfm_undocumented_extensions_and_more_robust_test__task_lists__003: |-
+ Error - check implementation:
+ Cannot read properties of undefined (reading 'start')
diff --git a/glfm_specification/output_example_snapshots/snapshot_spec.html b/glfm_specification/output_example_snapshots/snapshot_spec.html
index 1fb5350cbe3..5c38f8ff5f0 100644
--- a/glfm_specification/output_example_snapshots/snapshot_spec.html
+++ b/glfm_specification/output_example_snapshots/snapshot_spec.html
@@ -366,6 +366,15 @@
<li><a href="#footnotes-1">Footnotes</a></li>
</ul>
</li>
+<li>
+<a href="#gfm-undocumented-extensions-and-more-robust-test">GFM undocumented extensions and more robust test</a><ul>
+<li><a href="#footnotes-2">Footnotes</a></li>
+<li><a href="#when-a-footnote-is-used-multiple-times-we-insert-multiple-backrefs">When a footnote is used multiple times, we insert multiple backrefs.</a></li>
+<li><a href="#footnote-reference-labels-are-href-escaped">Footnote reference labels are href escaped</a></li>
+<li><a href="#interop">Interop</a></li>
+<li><a href="#task-lists">Task lists</a></li>
+</ul>
+</li>
</ul>
<h1 data-sourcepos="3:1-3:15" dir="auto">
<a id="user-content-preliminaries" class="anchor" href="#preliminaries" aria-hidden="true"></a>Preliminaries</h1>
@@ -13545,6 +13554,269 @@ where it makes sense.</p>
<copy-code></copy-code>
</div>
</div>
+<h1 data-sourcepos="15150:1-15150:50" dir="auto">
+<a id="user-content-gfm-undocumented-extensions-and-more-robust-test" class="anchor" href="#gfm-undocumented-extensions-and-more-robust-test" aria-hidden="true"></a>GFM undocumented extensions and more robust test</h1>
+<p data-sourcepos="15152:1-15154:16" dir="auto">This section contains tests borrowed from <a href="https://github.com/github/cmark-gfm/blob/master/test/extensions.txt" rel="nofollow noreferrer noopener" target="_blank">https://github.com/github/cmark-gfm/blob/master/test/extensions.txt</a>.
+It includes items not found in the official GFM specification, such as footnotes and additional tests for tables,
+task lists, etc.</p>
+<h2 data-sourcepos="15156:1-15156:12" dir="auto">
+<a id="user-content-footnotes-2" class="anchor" href="#footnotes-2" aria-hidden="true"></a>Footnotes</h2>
+<div>
+<div><a href="#example-755">Example 755</a></div>
+<div class="gl-relative markdown-code-block js-markdown-code">
+<pre data-sourcepos="15161:1-15188:32" data-canonical-lang="example" class="code highlight js-syntax-highlight language-plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">This is some text![^1]. Other text.[^footnote].</span>
+<span id="LC2" class="line" lang="plaintext"></span>
+<span id="LC3" class="line" lang="plaintext">Here's a thing[^other-note].</span>
+<span id="LC4" class="line" lang="plaintext"></span>
+<span id="LC5" class="line" lang="plaintext">And another thing[^codeblock-note].</span>
+<span id="LC6" class="line" lang="plaintext"></span>
+<span id="LC7" class="line" lang="plaintext">This doesn't have a referent[^nope].</span>
+<span id="LC8" class="line" lang="plaintext"></span>
+<span id="LC9" class="line" lang="plaintext"></span>
+<span id="LC10" class="line" lang="plaintext">[^other-note]: no code block here (spaces are stripped away)</span>
+<span id="LC11" class="line" lang="plaintext"></span>
+<span id="LC12" class="line" lang="plaintext">[^codeblock-note]:</span>
+<span id="LC13" class="line" lang="plaintext"> this is now a code block (8 spaces indentation)</span>
+<span id="LC14" class="line" lang="plaintext"></span>
+<span id="LC15" class="line" lang="plaintext">[^1]: Some *bolded* footnote definition.</span>
+<span id="LC16" class="line" lang="plaintext"></span>
+<span id="LC17" class="line" lang="plaintext">Hi!</span>
+<span id="LC18" class="line" lang="plaintext"></span>
+<span id="LC19" class="line" lang="plaintext">[^footnote]:</span>
+<span id="LC20" class="line" lang="plaintext"> &gt; Blockquotes can be in a footnote.</span>
+<span id="LC21" class="line" lang="plaintext"></span>
+<span id="LC22" class="line" lang="plaintext"> as well as code blocks</span>
+<span id="LC23" class="line" lang="plaintext"></span>
+<span id="LC24" class="line" lang="plaintext"> or, naturally, simple paragraphs.</span>
+<span id="LC25" class="line" lang="plaintext"></span>
+<span id="LC26" class="line" lang="plaintext">[^unused]: This is unused.</span></code></pre>
+<copy-code></copy-code>
+</div>
+<div class="gl-relative markdown-code-block js-markdown-code">
+<pre data-sourcepos="15190:1-15219:32" class="code highlight js-syntax-highlight language-plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">&lt;p&gt;This is some text!&lt;sup class="footnote-ref"&gt;&lt;a href="#fn-1" id="fnref-1" data-footnote-ref&gt;1&lt;/a&gt;&lt;/sup&gt;. Other text.&lt;sup class="footnote-ref"&gt;&lt;a href="#fn-footnote" id="fnref-footnote" data-footnote-ref&gt;2&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;</span>
+<span id="LC2" class="line" lang="plaintext">&lt;p&gt;Here's a thing&lt;sup class="footnote-ref"&gt;&lt;a href="#fn-other-note" id="fnref-other-note" data-footnote-ref&gt;3&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;</span>
+<span id="LC3" class="line" lang="plaintext">&lt;p&gt;And another thing&lt;sup class="footnote-ref"&gt;&lt;a href="#fn-codeblock-note" id="fnref-codeblock-note" data-footnote-ref&gt;4&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;</span>
+<span id="LC4" class="line" lang="plaintext">&lt;p&gt;This doesn't have a referent[^nope].&lt;/p&gt;</span>
+<span id="LC5" class="line" lang="plaintext">&lt;p&gt;Hi!&lt;/p&gt;</span>
+<span id="LC6" class="line" lang="plaintext">&lt;section class="footnotes" data-footnotes&gt;</span>
+<span id="LC7" class="line" lang="plaintext">&lt;ol&gt;</span>
+<span id="LC8" class="line" lang="plaintext">&lt;li id="fn-1"&gt;</span>
+<span id="LC9" class="line" lang="plaintext">&lt;p&gt;Some &lt;em&gt;bolded&lt;/em&gt; footnote definition. &lt;a href="#fnref-1" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="1" aria-label="Back to reference 1"&gt;↩&lt;/a&gt;&lt;/p&gt;</span>
+<span id="LC10" class="line" lang="plaintext">&lt;/li&gt;</span>
+<span id="LC11" class="line" lang="plaintext">&lt;li id="fn-footnote"&gt;</span>
+<span id="LC12" class="line" lang="plaintext">&lt;blockquote&gt;</span>
+<span id="LC13" class="line" lang="plaintext">&lt;p&gt;Blockquotes can be in a footnote.&lt;/p&gt;</span>
+<span id="LC14" class="line" lang="plaintext">&lt;/blockquote&gt;</span>
+<span id="LC15" class="line" lang="plaintext">&lt;pre&gt;&lt;code&gt;as well as code blocks</span>
+<span id="LC16" class="line" lang="plaintext">&lt;/code&gt;&lt;/pre&gt;</span>
+<span id="LC17" class="line" lang="plaintext">&lt;p&gt;or, naturally, simple paragraphs. &lt;a href="#fnref-footnote" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="2" aria-label="Back to reference 2"&gt;↩&lt;/a&gt;&lt;/p&gt;</span>
+<span id="LC18" class="line" lang="plaintext">&lt;/li&gt;</span>
+<span id="LC19" class="line" lang="plaintext">&lt;li id="fn-other-note"&gt;</span>
+<span id="LC20" class="line" lang="plaintext">&lt;p&gt;no code block here (spaces are stripped away) &lt;a href="#fnref-other-note" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="3" aria-label="Back to reference 3"&gt;↩&lt;/a&gt;&lt;/p&gt;</span>
+<span id="LC21" class="line" lang="plaintext">&lt;/li&gt;</span>
+<span id="LC22" class="line" lang="plaintext">&lt;li id="fn-codeblock-note"&gt;</span>
+<span id="LC23" class="line" lang="plaintext">&lt;pre&gt;&lt;code&gt;this is now a code block (8 spaces indentation)</span>
+<span id="LC24" class="line" lang="plaintext">&lt;/code&gt;&lt;/pre&gt;</span>
+<span id="LC25" class="line" lang="plaintext">&lt;a href="#fnref-codeblock-note" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="4" aria-label="Back to reference 4"&gt;↩&lt;/a&gt;</span>
+<span id="LC26" class="line" lang="plaintext">&lt;/li&gt;</span>
+<span id="LC27" class="line" lang="plaintext">&lt;/ol&gt;</span>
+<span id="LC28" class="line" lang="plaintext">&lt;/section&gt;</span></code></pre>
+<copy-code></copy-code>
+</div>
+</div>
+<h2 data-sourcepos="15222:1-15222:71" dir="auto">
+<a id="user-content-when-a-footnote-is-used-multiple-times-we-insert-multiple-backrefs" class="anchor" href="#when-a-footnote-is-used-multiple-times-we-insert-multiple-backrefs" aria-hidden="true"></a>When a footnote is used multiple times, we insert multiple backrefs.</h2>
+<div>
+<div><a href="#example-756">Example 756</a></div>
+<div class="gl-relative markdown-code-block js-markdown-code">
+<pre data-sourcepos="15227:1-15233:32" data-canonical-lang="example" class="code highlight js-syntax-highlight language-plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">This is some text. It has a footnote[^a-footnote].</span>
+<span id="LC2" class="line" lang="plaintext"></span>
+<span id="LC3" class="line" lang="plaintext">This footnote is referenced[^a-footnote] multiple times, in lots of different places.[^a-footnote]</span>
+<span id="LC4" class="line" lang="plaintext"></span>
+<span id="LC5" class="line" lang="plaintext">[^a-footnote]: This footnote definition should have three backrefs.</span></code></pre>
+<copy-code></copy-code>
+</div>
+<div class="gl-relative markdown-code-block js-markdown-code">
+<pre data-sourcepos="15235:1-15245:32" class="code highlight js-syntax-highlight language-plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">&lt;p&gt;This is some text. It has a footnote&lt;sup class="footnote-ref"&gt;&lt;a href="#fn-a-footnote" id="fnref-a-footnote" data-footnote-ref&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;</span>
+<span id="LC2" class="line" lang="plaintext">&lt;p&gt;This footnote is referenced&lt;sup class="footnote-ref"&gt;&lt;a href="#fn-a-footnote" id="fnref-a-footnote-2" data-footnote-ref&gt;1&lt;/a&gt;&lt;/sup&gt; multiple times, in lots of different places.&lt;sup class="footnote-ref"&gt;&lt;a href="#fn-a-footnote" id="fnref-a-footnote-3" data-footnote-ref&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;</span>
+<span id="LC3" class="line" lang="plaintext">&lt;section class="footnotes" data-footnotes&gt;</span>
+<span id="LC4" class="line" lang="plaintext">&lt;ol&gt;</span>
+<span id="LC5" class="line" lang="plaintext">&lt;li id="fn-a-footnote"&gt;</span>
+<span id="LC6" class="line" lang="plaintext">&lt;p&gt;This footnote definition should have three backrefs. &lt;a href="#fnref-a-footnote" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="1" aria-label="Back to reference 1"&gt;↩&lt;/a&gt; &lt;a href="#fnref-a-footnote-2" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="1-2" aria-label="Back to reference 1-2"&gt;↩&lt;sup class="footnote-ref"&gt;2&lt;/sup&gt;&lt;/a&gt; &lt;a href="#fnref-a-footnote-3" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="1-3" aria-label="Back to reference 1-3"&gt;↩&lt;sup class="footnote-ref"&gt;3&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;</span>
+<span id="LC7" class="line" lang="plaintext">&lt;/li&gt;</span>
+<span id="LC8" class="line" lang="plaintext">&lt;/ol&gt;</span>
+<span id="LC9" class="line" lang="plaintext">&lt;/section&gt;</span></code></pre>
+<copy-code></copy-code>
+</div>
+</div>
+<h2 data-sourcepos="15248:1-15248:45" dir="auto">
+<a id="user-content-footnote-reference-labels-are-href-escaped" class="anchor" href="#footnote-reference-labels-are-href-escaped" aria-hidden="true"></a>Footnote reference labels are href escaped</h2>
+<div>
+<div><a href="#example-757">Example 757</a></div>
+<div class="gl-relative markdown-code-block js-markdown-code">
+<pre data-sourcepos="15253:1-15257:32" data-canonical-lang="example" class="code highlight js-syntax-highlight language-plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">Hello[^"&gt;&lt;script&gt;alert(1)&lt;/script&gt;]</span>
+<span id="LC2" class="line" lang="plaintext"></span>
+<span id="LC3" class="line" lang="plaintext">[^"&gt;&lt;script&gt;alert(1)&lt;/script&gt;]: pwned</span></code></pre>
+<copy-code></copy-code>
+</div>
+<div class="gl-relative markdown-code-block js-markdown-code">
+<pre data-sourcepos="15259:1-15268:32" class="code highlight js-syntax-highlight language-plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">&lt;p&gt;Hello&lt;sup class="footnote-ref"&gt;&lt;a href="#fn-%22%3E%3Cscript%3Ealert(1)%3C/script%3E" id="fnref-%22%3E%3Cscript%3Ealert(1)%3C/script%3E" data-footnote-ref&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;</span>
+<span id="LC2" class="line" lang="plaintext">&lt;section class="footnotes" data-footnotes&gt;</span>
+<span id="LC3" class="line" lang="plaintext">&lt;ol&gt;</span>
+<span id="LC4" class="line" lang="plaintext">&lt;li id="fn-%22%3E%3Cscript%3Ealert(1)%3C/script%3E"&gt;</span>
+<span id="LC5" class="line" lang="plaintext">&lt;p&gt;pwned &lt;a href="#fnref-%22%3E%3Cscript%3Ealert(1)%3C/script%3E" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="1" aria-label="Back to reference 1"&gt;↩&lt;/a&gt;&lt;/p&gt;</span>
+<span id="LC6" class="line" lang="plaintext">&lt;/li&gt;</span>
+<span id="LC7" class="line" lang="plaintext">&lt;/ol&gt;</span>
+<span id="LC8" class="line" lang="plaintext">&lt;/section&gt;</span></code></pre>
+<copy-code></copy-code>
+</div>
+</div>
+<h2 data-sourcepos="15271:1-15271:10" dir="auto">
+<a id="user-content-interop" class="anchor" href="#interop" aria-hidden="true"></a>Interop</h2>
+<p data-sourcepos="15273:1-15273:27" dir="auto">Autolink and strikethrough.</p>
+<div>
+<div><a href="#example-758">Example 758</a></div>
+<div class="gl-relative markdown-code-block js-markdown-code">
+<pre data-sourcepos="15278:1-15282:32" data-canonical-lang="example" class="code highlight js-syntax-highlight language-plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">~~www.google.com~~</span>
+<span id="LC2" class="line" lang="plaintext"></span>
+<span id="LC3" class="line" lang="plaintext">~~http://google.com~~</span></code></pre>
+<copy-code></copy-code>
+</div>
+<div class="gl-relative markdown-code-block js-markdown-code">
+<pre data-sourcepos="15284:1-15287:32" class="code highlight js-syntax-highlight language-plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">&lt;p&gt;&lt;del&gt;&lt;a href="http://www.google.com"&gt;www.google.com&lt;/a&gt;&lt;/del&gt;&lt;/p&gt;</span>
+<span id="LC2" class="line" lang="plaintext">&lt;p&gt;&lt;del&gt;&lt;a href="http://google.com"&gt;http://google.com&lt;/a&gt;&lt;/del&gt;&lt;/p&gt;</span></code></pre>
+<copy-code></copy-code>
+</div>
+</div>
+<p data-sourcepos="15290:1-15290:20" dir="auto">Autolink and tables.</p>
+<div>
+<div><a href="#example-759">Example 759</a></div>
+<div class="gl-relative markdown-code-block js-markdown-code">
+<pre data-sourcepos="15295:1-15299:32" data-canonical-lang="example" class="code highlight js-syntax-highlight language-plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">| a | b |</span>
+<span id="LC2" class="line" lang="plaintext">| --- | --- |</span>
+<span id="LC3" class="line" lang="plaintext">| https://github.com www.github.com | http://pokemon.com |</span></code></pre>
+<copy-code></copy-code>
+</div>
+<div class="gl-relative markdown-code-block js-markdown-code">
+<pre data-sourcepos="15301:1-15316:32" class="code highlight js-syntax-highlight language-plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">&lt;table&gt;</span>
+<span id="LC2" class="line" lang="plaintext">&lt;thead&gt;</span>
+<span id="LC3" class="line" lang="plaintext">&lt;tr&gt;</span>
+<span id="LC4" class="line" lang="plaintext">&lt;th&gt;a&lt;/th&gt;</span>
+<span id="LC5" class="line" lang="plaintext">&lt;th&gt;b&lt;/th&gt;</span>
+<span id="LC6" class="line" lang="plaintext">&lt;/tr&gt;</span>
+<span id="LC7" class="line" lang="plaintext">&lt;/thead&gt;</span>
+<span id="LC8" class="line" lang="plaintext">&lt;tbody&gt;</span>
+<span id="LC9" class="line" lang="plaintext">&lt;tr&gt;</span>
+<span id="LC10" class="line" lang="plaintext">&lt;td&gt;&lt;a href="https://github.com"&gt;https://github.com&lt;/a&gt; &lt;a href="http://www.github.com"&gt;www.github.com&lt;/a&gt;&lt;/td&gt;</span>
+<span id="LC11" class="line" lang="plaintext">&lt;td&gt;&lt;a href="http://pokemon.com"&gt;http://pokemon.com&lt;/a&gt;&lt;/td&gt;</span>
+<span id="LC12" class="line" lang="plaintext">&lt;/tr&gt;</span>
+<span id="LC13" class="line" lang="plaintext">&lt;/tbody&gt;</span>
+<span id="LC14" class="line" lang="plaintext">&lt;/table&gt;</span></code></pre>
+<copy-code></copy-code>
+</div>
+</div>
+<h2 data-sourcepos="15319:1-15319:13" dir="auto">
+<a id="user-content-task-lists" class="anchor" href="#task-lists" aria-hidden="true"></a>Task lists</h2>
+<div>
+<div><a href="#example-760">Example 760</a></div>
+<div class="gl-relative markdown-code-block js-markdown-code">
+<pre data-sourcepos="15324:1-15327:32" data-canonical-lang="example" class="code highlight js-syntax-highlight language-plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">- [ ] foo</span>
+<span id="LC2" class="line" lang="plaintext">- [x] bar</span></code></pre>
+<copy-code></copy-code>
+</div>
+<div class="gl-relative markdown-code-block js-markdown-code">
+<pre data-sourcepos="15329:1-15334:32" class="code highlight js-syntax-highlight language-plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">&lt;ul&gt;</span>
+<span id="LC2" class="line" lang="plaintext">&lt;li&gt;&lt;input type="checkbox" disabled="" /&gt; foo&lt;/li&gt;</span>
+<span id="LC3" class="line" lang="plaintext">&lt;li&gt;&lt;input type="checkbox" checked="" disabled="" /&gt; bar&lt;/li&gt;</span>
+<span id="LC4" class="line" lang="plaintext">&lt;/ul&gt;</span></code></pre>
+<copy-code></copy-code>
+</div>
+</div>
+<p data-sourcepos="15337:1-15342:37" dir="auto">Show that a task list and a regular list get processed the same in
+the way that sublists are created. If something works in a list
+item, then it should work the same way with a task. The only
+difference should be the tasklist marker. So, if we use something
+other than a space or x, it won't be recognized as a task item, and
+so will be treated as a regular item.</p>
+<div>
+<div><a href="#example-761">Example 761</a></div>
+<div class="gl-relative markdown-code-block js-markdown-code">
+<pre data-sourcepos="15347:1-15358:32" data-canonical-lang="example" class="code highlight js-syntax-highlight language-plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">- [x] foo</span>
+<span id="LC2" class="line" lang="plaintext"> - [ ] bar</span>
+<span id="LC3" class="line" lang="plaintext"> - [x] baz</span>
+<span id="LC4" class="line" lang="plaintext">- [ ] bim</span>
+<span id="LC5" class="line" lang="plaintext"></span>
+<span id="LC6" class="line" lang="plaintext">Show a regular (non task) list to show that it has the same structure</span>
+<span id="LC7" class="line" lang="plaintext">- [@] foo</span>
+<span id="LC8" class="line" lang="plaintext"> - [@] bar</span>
+<span id="LC9" class="line" lang="plaintext"> - [@] baz</span>
+<span id="LC10" class="line" lang="plaintext">- [@] bim</span></code></pre>
+<copy-code></copy-code>
+</div>
+<div class="gl-relative markdown-code-block js-markdown-code">
+<pre data-sourcepos="15360:1-15380:32" class="code highlight js-syntax-highlight language-plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">&lt;ul&gt;</span>
+<span id="LC2" class="line" lang="plaintext">&lt;li&gt;&lt;input type="checkbox" checked="" disabled="" /&gt; foo</span>
+<span id="LC3" class="line" lang="plaintext">&lt;ul&gt;</span>
+<span id="LC4" class="line" lang="plaintext">&lt;li&gt;&lt;input type="checkbox" disabled="" /&gt; bar&lt;/li&gt;</span>
+<span id="LC5" class="line" lang="plaintext">&lt;li&gt;&lt;input type="checkbox" checked="" disabled="" /&gt; baz&lt;/li&gt;</span>
+<span id="LC6" class="line" lang="plaintext">&lt;/ul&gt;</span>
+<span id="LC7" class="line" lang="plaintext">&lt;/li&gt;</span>
+<span id="LC8" class="line" lang="plaintext">&lt;li&gt;&lt;input type="checkbox" disabled="" /&gt; bim&lt;/li&gt;</span>
+<span id="LC9" class="line" lang="plaintext">&lt;/ul&gt;</span>
+<span id="LC10" class="line" lang="plaintext">&lt;p&gt;Show a regular (non task) list to show that it has the same structure&lt;/p&gt;</span>
+<span id="LC11" class="line" lang="plaintext">&lt;ul&gt;</span>
+<span id="LC12" class="line" lang="plaintext">&lt;li&gt;[@] foo</span>
+<span id="LC13" class="line" lang="plaintext">&lt;ul&gt;</span>
+<span id="LC14" class="line" lang="plaintext">&lt;li&gt;[@] bar&lt;/li&gt;</span>
+<span id="LC15" class="line" lang="plaintext">&lt;li&gt;[@] baz&lt;/li&gt;</span>
+<span id="LC16" class="line" lang="plaintext">&lt;/ul&gt;</span>
+<span id="LC17" class="line" lang="plaintext">&lt;/li&gt;</span>
+<span id="LC18" class="line" lang="plaintext">&lt;li&gt;[@] bim&lt;/li&gt;</span>
+<span id="LC19" class="line" lang="plaintext">&lt;/ul&gt;</span></code></pre>
+<copy-code></copy-code>
+</div>
+</div>
+Use a larger indent -- a task list and a regular list should produce
+the same structure.
+<div>
+<div><a href="#example-762">Example 762</a></div>
+<div class="gl-relative markdown-code-block js-markdown-code">
+<pre data-sourcepos="15388:1-15399:32" data-canonical-lang="example" class="code highlight js-syntax-highlight language-plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">- [x] foo</span>
+<span id="LC2" class="line" lang="plaintext"> - [ ] bar</span>
+<span id="LC3" class="line" lang="plaintext"> - [x] baz</span>
+<span id="LC4" class="line" lang="plaintext">- [ ] bim</span>
+<span id="LC5" class="line" lang="plaintext"></span>
+<span id="LC6" class="line" lang="plaintext">Show a regular (non task) list to show that it has the same structure</span>
+<span id="LC7" class="line" lang="plaintext">- [@] foo</span>
+<span id="LC8" class="line" lang="plaintext"> - [@] bar</span>
+<span id="LC9" class="line" lang="plaintext"> - [@] baz</span>
+<span id="LC10" class="line" lang="plaintext">- [@] bim</span></code></pre>
+<copy-code></copy-code>
+</div>
+<div class="gl-relative markdown-code-block js-markdown-code">
+<pre data-sourcepos="15401:1-15421:32" class="code highlight js-syntax-highlight language-plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">&lt;ul&gt;</span>
+<span id="LC2" class="line" lang="plaintext">&lt;li&gt;&lt;input type="checkbox" checked="" disabled="" /&gt; foo</span>
+<span id="LC3" class="line" lang="plaintext">&lt;ul&gt;</span>
+<span id="LC4" class="line" lang="plaintext">&lt;li&gt;&lt;input type="checkbox" disabled="" /&gt; bar&lt;/li&gt;</span>
+<span id="LC5" class="line" lang="plaintext">&lt;li&gt;&lt;input type="checkbox" checked="" disabled="" /&gt; baz&lt;/li&gt;</span>
+<span id="LC6" class="line" lang="plaintext">&lt;/ul&gt;</span>
+<span id="LC7" class="line" lang="plaintext">&lt;/li&gt;</span>
+<span id="LC8" class="line" lang="plaintext">&lt;li&gt;&lt;input type="checkbox" disabled="" /&gt; bim&lt;/li&gt;</span>
+<span id="LC9" class="line" lang="plaintext">&lt;/ul&gt;</span>
+<span id="LC10" class="line" lang="plaintext">&lt;p&gt;Show a regular (non task) list to show that it has the same structure&lt;/p&gt;</span>
+<span id="LC11" class="line" lang="plaintext">&lt;ul&gt;</span>
+<span id="LC12" class="line" lang="plaintext">&lt;li&gt;[@] foo</span>
+<span id="LC13" class="line" lang="plaintext">&lt;ul&gt;</span>
+<span id="LC14" class="line" lang="plaintext">&lt;li&gt;[@] bar&lt;/li&gt;</span>
+<span id="LC15" class="line" lang="plaintext">&lt;li&gt;[@] baz&lt;/li&gt;</span>
+<span id="LC16" class="line" lang="plaintext">&lt;/ul&gt;</span>
+<span id="LC17" class="line" lang="plaintext">&lt;/li&gt;</span>
+<span id="LC18" class="line" lang="plaintext">&lt;li&gt;[@] bim&lt;/li&gt;</span>
+<span id="LC19" class="line" lang="plaintext">&lt;/ul&gt;</span></code></pre>
+<copy-code></copy-code>
+</div>
+</div>
+
</body>
</html>
diff --git a/glfm_specification/output_example_snapshots/snapshot_spec.md b/glfm_specification/output_example_snapshots/snapshot_spec.md
index 3b3628032bf..a22d9b41ea7 100644
--- a/glfm_specification/output_example_snapshots/snapshot_spec.md
+++ b/glfm_specification/output_example_snapshots/snapshot_spec.md
@@ -10625,3 +10625,231 @@ footnote text
</section>
````````````````````````````````
+# GFM undocumented extensions and more robust test
+
+This section contains tests borrowed from https://github.com/github/cmark-gfm/blob/master/test/extensions.txt.
+It includes items not found in the official GFM specification, such as footnotes and additional tests for tables,
+task lists, etc.
+
+## Footnotes
+
+```````````````````````````````` example
+This is some text![^1]. Other text.[^footnote].
+
+Here's a thing[^other-note].
+
+And another thing[^codeblock-note].
+
+This doesn't have a referent[^nope].
+
+
+[^other-note]: no code block here (spaces are stripped away)
+
+[^codeblock-note]:
+ this is now a code block (8 spaces indentation)
+
+[^1]: Some *bolded* footnote definition.
+
+Hi!
+
+[^footnote]:
+ > Blockquotes can be in a footnote.
+
+ as well as code blocks
+
+ or, naturally, simple paragraphs.
+
+[^unused]: This is unused.
+.
+<p>This is some text!<sup class="footnote-ref"><a href="#fn-1" id="fnref-1" data-footnote-ref>1</a></sup>. Other text.<sup class="footnote-ref"><a href="#fn-footnote" id="fnref-footnote" data-footnote-ref>2</a></sup>.</p>
+<p>Here's a thing<sup class="footnote-ref"><a href="#fn-other-note" id="fnref-other-note" data-footnote-ref>3</a></sup>.</p>
+<p>And another thing<sup class="footnote-ref"><a href="#fn-codeblock-note" id="fnref-codeblock-note" data-footnote-ref>4</a></sup>.</p>
+<p>This doesn't have a referent[^nope].</p>
+<p>Hi!</p>
+<section class="footnotes" data-footnotes>
+<ol>
+<li id="fn-1">
+<p>Some <em>bolded</em> footnote definition. <a href="#fnref-1" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="1" aria-label="Back to reference 1">↩</a></p>
+</li>
+<li id="fn-footnote">
+<blockquote>
+<p>Blockquotes can be in a footnote.</p>
+</blockquote>
+<pre><code>as well as code blocks
+</code></pre>
+<p>or, naturally, simple paragraphs. <a href="#fnref-footnote" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="2" aria-label="Back to reference 2">↩</a></p>
+</li>
+<li id="fn-other-note">
+<p>no code block here (spaces are stripped away) <a href="#fnref-other-note" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="3" aria-label="Back to reference 3">↩</a></p>
+</li>
+<li id="fn-codeblock-note">
+<pre><code>this is now a code block (8 spaces indentation)
+</code></pre>
+<a href="#fnref-codeblock-note" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="4" aria-label="Back to reference 4">↩</a>
+</li>
+</ol>
+</section>
+````````````````````````````````
+
+## When a footnote is used multiple times, we insert multiple backrefs.
+
+```````````````````````````````` example
+This is some text. It has a footnote[^a-footnote].
+
+This footnote is referenced[^a-footnote] multiple times, in lots of different places.[^a-footnote]
+
+[^a-footnote]: This footnote definition should have three backrefs.
+.
+<p>This is some text. It has a footnote<sup class="footnote-ref"><a href="#fn-a-footnote" id="fnref-a-footnote" data-footnote-ref>1</a></sup>.</p>
+<p>This footnote is referenced<sup class="footnote-ref"><a href="#fn-a-footnote" id="fnref-a-footnote-2" data-footnote-ref>1</a></sup> multiple times, in lots of different places.<sup class="footnote-ref"><a href="#fn-a-footnote" id="fnref-a-footnote-3" data-footnote-ref>1</a></sup></p>
+<section class="footnotes" data-footnotes>
+<ol>
+<li id="fn-a-footnote">
+<p>This footnote definition should have three backrefs. <a href="#fnref-a-footnote" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="1" aria-label="Back to reference 1">↩</a> <a href="#fnref-a-footnote-2" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="1-2" aria-label="Back to reference 1-2">↩<sup class="footnote-ref">2</sup></a> <a href="#fnref-a-footnote-3" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="1-3" aria-label="Back to reference 1-3">↩<sup class="footnote-ref">3</sup></a></p>
+</li>
+</ol>
+</section>
+````````````````````````````````
+
+## Footnote reference labels are href escaped
+
+```````````````````````````````` example
+Hello[^"><script>alert(1)</script>]
+
+[^"><script>alert(1)</script>]: pwned
+.
+<p>Hello<sup class="footnote-ref"><a href="#fn-%22%3E%3Cscript%3Ealert(1)%3C/script%3E" id="fnref-%22%3E%3Cscript%3Ealert(1)%3C/script%3E" data-footnote-ref>1</a></sup></p>
+<section class="footnotes" data-footnotes>
+<ol>
+<li id="fn-%22%3E%3Cscript%3Ealert(1)%3C/script%3E">
+<p>pwned <a href="#fnref-%22%3E%3Cscript%3Ealert(1)%3C/script%3E" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="1" aria-label="Back to reference 1">↩</a></p>
+</li>
+</ol>
+</section>
+````````````````````````````````
+
+## Interop
+
+Autolink and strikethrough.
+
+```````````````````````````````` example
+~~www.google.com~~
+
+~~http://google.com~~
+.
+<p><del><a href="http://www.google.com">www.google.com</a></del></p>
+<p><del><a href="http://google.com">http://google.com</a></del></p>
+````````````````````````````````
+
+Autolink and tables.
+
+```````````````````````````````` example
+| a | b |
+| --- | --- |
+| https://github.com www.github.com | http://pokemon.com |
+.
+<table>
+<thead>
+<tr>
+<th>a</th>
+<th>b</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td><a href="https://github.com">https://github.com</a> <a href="http://www.github.com">www.github.com</a></td>
+<td><a href="http://pokemon.com">http://pokemon.com</a></td>
+</tr>
+</tbody>
+</table>
+````````````````````````````````
+
+## Task lists
+
+```````````````````````````````` example
+- [ ] foo
+- [x] bar
+.
+<ul>
+<li><input type="checkbox" disabled="" /> foo</li>
+<li><input type="checkbox" checked="" disabled="" /> bar</li>
+</ul>
+````````````````````````````````
+
+Show that a task list and a regular list get processed the same in
+the way that sublists are created. If something works in a list
+item, then it should work the same way with a task. The only
+difference should be the tasklist marker. So, if we use something
+other than a space or x, it won't be recognized as a task item, and
+so will be treated as a regular item.
+
+```````````````````````````````` example
+- [x] foo
+ - [ ] bar
+ - [x] baz
+- [ ] bim
+
+Show a regular (non task) list to show that it has the same structure
+- [@] foo
+ - [@] bar
+ - [@] baz
+- [@] bim
+.
+<ul>
+<li><input type="checkbox" checked="" disabled="" /> foo
+<ul>
+<li><input type="checkbox" disabled="" /> bar</li>
+<li><input type="checkbox" checked="" disabled="" /> baz</li>
+</ul>
+</li>
+<li><input type="checkbox" disabled="" /> bim</li>
+</ul>
+<p>Show a regular (non task) list to show that it has the same structure</p>
+<ul>
+<li>[@] foo
+<ul>
+<li>[@] bar</li>
+<li>[@] baz</li>
+</ul>
+</li>
+<li>[@] bim</li>
+</ul>
+````````````````````````````````
+Use a larger indent -- a task list and a regular list should produce
+the same structure.
+
+```````````````````````````````` example
+- [x] foo
+ - [ ] bar
+ - [x] baz
+- [ ] bim
+
+Show a regular (non task) list to show that it has the same structure
+- [@] foo
+ - [@] bar
+ - [@] baz
+- [@] bim
+.
+<ul>
+<li><input type="checkbox" checked="" disabled="" /> foo
+<ul>
+<li><input type="checkbox" disabled="" /> bar</li>
+<li><input type="checkbox" checked="" disabled="" /> baz</li>
+</ul>
+</li>
+<li><input type="checkbox" disabled="" /> bim</li>
+</ul>
+<p>Show a regular (non task) list to show that it has the same structure</p>
+<ul>
+<li>[@] foo
+<ul>
+<li>[@] bar</li>
+<li>[@] baz</li>
+</ul>
+</li>
+<li>[@] bim</li>
+</ul>
+````````````````````````````````
+
+<!-- end of the "GFM undocumented extensions and more robust test" section -->
+
diff --git a/jest.config.base.js b/jest.config.base.js
index 8943383735e..18c13293275 100644
--- a/jest.config.base.js
+++ b/jest.config.base.js
@@ -17,6 +17,9 @@ module.exports = (path, options = {}) => {
moduleNameMapper: extModuleNameMapper = {},
moduleNameMapperEE: extModuleNameMapperEE = {},
moduleNameMapperJH: extModuleNameMapperJH = {},
+ roots: extRoots = [],
+ rootsEE: extRootsEE = [],
+ rootsJH: extRootsJH = [],
} = options;
const reporters = ['default'];
@@ -120,7 +123,6 @@ module.exports = (path, options = {}) => {
'^jest/(.*)$': '<rootDir>/spec/frontend/$1',
'^ee_else_ce_jest/(.*)$': '<rootDir>/spec/frontend/$1',
'^jquery$': '<rootDir>/node_modules/jquery/dist/jquery.slim.js',
- '^@sentry/browser$': '<rootDir>/app/assets/javascripts/sentry/sentry_browser_wrapper.js',
'^dexie$': '<rootDir>/node_modules/dexie/dist/dexie.min.js',
...extModuleNameMapper,
...vueModuleNameMappers,
@@ -267,5 +269,11 @@ module.exports = (path, options = {}) => {
'<rootDir>/spec/frontend/__helpers__/html_string_serializer.js',
'<rootDir>/spec/frontend/__helpers__/clean_html_element_serializer.js',
],
+ roots: [
+ '<rootDir>/app/assets/javascripts/',
+ ...extRoots,
+ ...(IS_EE ? ['<rootDir>/ee/app/assets/javascripts/', ...extRootsEE] : []),
+ ...(IS_JH ? ['<rootDir>/jh/app/assets/javascripts/', ...extRootsJH] : []),
+ ],
};
};
diff --git a/jest.config.contract.js b/jest.config.contract.js
index 224d50f87d6..059a3207088 100644
--- a/jest.config.contract.js
+++ b/jest.config.contract.js
@@ -1,6 +1,6 @@
module.exports = () => {
return {
modulePaths: ['<rootDir>/spec/contracts/consumer/node_modules/'],
- roots: ['spec/contracts/consumer', 'ee/spec/contracts/consumer'],
+ roots: ['spec/contracts/consumer/', 'ee/spec/contracts/consumer/'],
};
};
diff --git a/jest.config.integration.js b/jest.config.integration.js
index 0693a500990..919c299000b 100644
--- a/jest.config.integration.js
+++ b/jest.config.integration.js
@@ -23,6 +23,10 @@ module.exports = {
moduleNameMapperJH: {
'^jh_else_ce_test_helpers(/.*)$': '<rootDir>/jh/spec/frontend_integration/test_helpers$1',
},
+ // We need to include spec/frontend in `roots` for the __mocks__ to be found
+ roots: ['<rootDir>/spec/frontend_integration/', '<rootDir>/spec/frontend/'],
+ rootsEE: ['<rootDir>/ee/spec/frontend_integration/'],
+ rootsJH: ['<rootDir>/jh/spec/frontend_integration/'],
}),
fakeTimers: {
enableGlobally: false,
diff --git a/jest.config.js b/jest.config.js
index 96a62b18d8f..acfaee14fbf 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -9,6 +9,10 @@ if (IS_JH && fs.existsSync('./jh/jest.config.js')) {
module.exports = require('./jh/jest.config');
} else {
module.exports = {
- ...baseConfig('spec/frontend'),
+ ...baseConfig('spec/frontend', {
+ roots: ['<rootDir>/spec/frontend/'],
+ rootsEE: ['<rootDir>/ee/spec/frontend/'],
+ rootsJH: ['<rootDir>/jh/spec/frontend/'],
+ }),
};
}
diff --git a/jest.config.scripts.js b/jest.config.scripts.js
new file mode 100644
index 00000000000..2f4ef3906a1
--- /dev/null
+++ b/jest.config.scripts.js
@@ -0,0 +1,8 @@
+const baseConfig = require('./jest.config.base');
+
+module.exports = {
+ ...baseConfig('spec/frontend', {
+ roots: ['<rootDir>/scripts/lib/', '<rootDir>/spec/frontend/'],
+ }),
+ testMatch: [],
+};
diff --git a/lefthook.yml b/lefthook.yml
index e57a6cce518..7272b64096f 100644
--- a/lefthook.yml
+++ b/lefthook.yml
@@ -107,7 +107,7 @@ pre-push:
tags: backend style
files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD
glob: '*.{rb,rake}'
- run: REVEAL_RUBOCOP_TODO=0 bundle exec rubocop --parallel --force-exclusion {files}
+ run: REVEAL_RUBOCOP_TODO=0 bundle exec rubocop --parallel --force-exclusion --no-server {files}
scripts:
"merge_conflicts":
@@ -124,7 +124,7 @@ pre-commit:
tags: backend style
files: git diff --name-only --diff-filter=d --staged
glob: '*.{rb,rake}'
- run: REVEAL_RUBOCOP_TODO=0 bundle exec rubocop --parallel --force-exclusion {files}
+ run: REVEAL_RUBOCOP_TODO=0 bundle exec rubocop --parallel --force-exclusion --no-server {files}
secrets-detection:
tags: secrets
files: git diff --name-only --diff-filter=d --staged
@@ -152,7 +152,7 @@ auto-fix:
tags: backend style
files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD) --cached
glob: '*.{rb,rake}'
- run: REVEAL_RUBOCOP_TODO=0 bundle exec rubocop --parallel --autocorrect --force-exclusion {files}
+ run: REVEAL_RUBOCOP_TODO=0 bundle exec rubocop --parallel --autocorrect --force-exclusion --no-server {files}
gettext:
tags: backend frontend view haml
files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD --cached | while read file;do git diff --unified=1 $(git merge-base origin/master HEAD)..HEAD $file | grep -Fqe '_(' && echo $file;done; true
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 8a26ae7e6f6..43a21c11dbc 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -387,16 +387,7 @@ module API
mount ::API::Internal::MailRoom
mount ::API::Internal::ContainerRegistry::Migration
mount ::API::Internal::Workhorse
-
- version 'v3', using: :path do
- # Although the following endpoints are kept behind V3 namespace,
- # they're not deprecated neither should be removed when V3 get
- # removed. They're needed as a layer to integrate with Jira
- # Development Panel.
- namespace '/', requirements: ::API::V3::Github::ENDPOINT_REQUIREMENTS do
- mount ::API::V3::Github
- end
- end
+ mount ::API::Internal::Shellhorse
route :any, '*path', feature_category: :not_owned do # rubocop:todo Gitlab/AvoidFeatureCategoryNotOwned
error!('404 Not Found', 404)
diff --git a/lib/api/bulk_imports.rb b/lib/api/bulk_imports.rb
index 9bcc16cf211..9dc0e5bae9b 100644
--- a/lib/api/bulk_imports.rb
+++ b/lib/api/bulk_imports.rb
@@ -214,6 +214,23 @@ module API
get ':import_id/entities/:entity_id' do
present bulk_import_entity, with: Entities::BulkImports::Entity
end
+
+ desc 'Get GitLab Migration entity failures' do
+ detail 'This feature was introduced in GitLab 16.6'
+ success code: 200, model: Entities::BulkImports::EntityFailure
+ failure [
+ { code: 401, message: 'Unauthorized' },
+ { code: 404, message: 'Not found' },
+ { code: 503, message: 'Service unavailable' }
+ ]
+ end
+ params do
+ requires :import_id, type: Integer, desc: "The ID of user's GitLab Migration"
+ requires :entity_id, type: Integer, desc: "The ID of GitLab Migration entity"
+ end
+ get ':import_id/entities/:entity_id/failures' do
+ present paginate(bulk_import_entity.failures), with: Entities::BulkImports::EntityFailure
+ end
end
end
end
diff --git a/lib/api/ci/jobs.rb b/lib/api/ci/jobs.rb
index 6f0a2ff7f62..250fe249489 100644
--- a/lib/api/ci/jobs.rb
+++ b/lib/api/ci/jobs.rb
@@ -57,7 +57,7 @@ module API
builds = filter_builds(builds, params[:scope])
builds = builds.preload(:user, :job_artifacts_archive, :job_artifacts, :runner, :tags, pipeline: :project)
- present paginate_with_strategies(builds, paginator_params: { without_count: true }), with: Entities::Ci::Job
+ present paginate_with_strategies(builds, user_project, paginator_params: { without_count: true }), with: Entities::Ci::Job
end
# rubocop: enable CodeReuse/ActiveRecord
@@ -122,10 +122,10 @@ module API
requires :job_id, type: Integer, desc: 'The ID of a job', documentation: { example: 88 }
end
post ':id/jobs/:job_id/cancel', urgency: :low, feature_category: :continuous_integration do
- authorize_update_builds!
+ authorize_cancel_builds!
build = find_build!(params[:job_id])
- authorize!(:update_build, build)
+ authorize!(:cancel_build, build)
build.cancel
diff --git a/lib/api/ci/pipelines.rb b/lib/api/ci/pipelines.rb
index bd5c04f401b..b5123ab49dc 100644
--- a/lib/api/ci/pipelines.rb
+++ b/lib/api/ci/pipelines.rb
@@ -288,6 +288,33 @@ module API
end
end
+ desc 'Updates pipeline metadata' do
+ detail 'This feature was introduced in GitLab 16.6'
+ success status: 200, model: Entities::Ci::PipelineWithMetadata
+ failure [
+ { code: 400, message: 'Bad request' },
+ { code: 401, message: 'Unauthorized' },
+ { code: 403, message: 'Forbidden' },
+ { code: 404, message: 'Not found' }
+ ]
+ end
+ params do
+ requires :pipeline_id, type: Integer, desc: 'The pipeline ID', documentation: { example: 18 }
+ requires :name, type: String, desc: 'The name of the pipeline', documentation: { example: 'Deployment to production' }
+ end
+ route_setting :authentication, job_token_allowed: true
+ put ':id/pipelines/:pipeline_id/metadata', urgency: :low, feature_category: :continuous_integration do
+ authorize! :update_pipeline, pipeline
+
+ response = ::Ci::Pipelines::UpdateMetadataService.new(pipeline, params.slice(:name)).execute
+
+ if response.success?
+ present response.payload, with: Entities::Ci::PipelineWithMetadata
+ else
+ render_api_error_with_reason!(response.reason, response.message, response.payload.join(', '))
+ end
+ end
+
desc 'Retry builds in the pipeline' do
detail 'This feature was introduced in GitLab 8.11.'
success status: 201, model: Entities::Ci::Pipeline
@@ -325,7 +352,7 @@ module API
requires :pipeline_id, type: Integer, desc: 'The pipeline ID', documentation: { example: 18 }
end
post ':id/pipelines/:pipeline_id/cancel', urgency: :low, feature_category: :continuous_integration do
- authorize! :update_pipeline, pipeline
+ authorize! :cancel_pipeline, pipeline
# TODO: inconsistent behavior: when pipeline is not cancelable we should return an error
::Ci::CancelPipelineService.new(pipeline: pipeline, current_user: current_user).execute
diff --git a/lib/api/ci/runners.rb b/lib/api/ci/runners.rb
index 42817c782f4..17bee275c51 100644
--- a/lib/api/ci/runners.rb
+++ b/lib/api/ci/runners.rb
@@ -24,6 +24,9 @@ module API
desc: 'The status of runners to return'
optional :tag_list, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce,
desc: 'A list of runner tags', documentation: { example: "['macos', 'shell']" }
+ optional :version_prefix, type: String, desc: 'The version prefix of runners to return', documentation: { example: "'15.1.' or '16.'" },
+ regexp: /^[\d+.]+/
+
use :pagination
end
@@ -46,6 +49,7 @@ module API
runners = filter_runners(runners, params[:type], allowed_scopes: ::Ci::Runner::AVAILABLE_TYPES)
runners = filter_runners(runners, params[:status], allowed_scopes: ::Ci::Runner::AVAILABLE_STATUSES)
runners = filter_runners(runners, params[:paused] ? 'paused' : 'active', allowed_scopes: %w[paused active]) if params.include?(:paused)
+ runners = runners.with_version_prefix(params[:version_prefix]) if params[:version_prefix]
runners = runners.tagged_with(params[:tag_list]) if params[:tag_list]
runners
diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb
index acb64cd0d3a..62b2885f955 100644
--- a/lib/api/commit_statuses.rb
+++ b/lib/api/commit_statuses.rb
@@ -94,37 +94,6 @@ module API
# rubocop: enable CodeReuse/ActiveRecord
helpers do
- def commit
- strong_memoize(:commit) do
- user_project.commit(params[:sha])
- end
- end
-
- def all_matching_pipelines
- pipelines = user_project.ci_pipelines.newest_first(sha: commit.sha)
- pipelines = pipelines.for_ref(params[:ref]) if params[:ref]
- pipelines = pipelines.id_in(params[:pipeline_id]) if params[:pipeline_id]
- pipelines
- end
-
- def apply_job_state!(job)
- case params[:state]
- when 'pending'
- job.enqueue!
- when 'running'
- job.enqueue
- job.run!
- when 'success'
- job.success!
- when 'failed'
- job.drop!(:api_failure)
- when 'canceled'
- job.cancel!
- else
- render_api_error!('invalid state', 400)
- end
- end
-
def optional_commit_status_params
updatable_optional_attributes = %w[target_url description coverage]
attributes_for_keys(updatable_optional_attributes)
diff --git a/lib/api/concerns/packages/npm_endpoints.rb b/lib/api/concerns/packages/npm_endpoints.rb
index bfaba5c4d7a..19d63a39242 100644
--- a/lib/api/concerns/packages/npm_endpoints.rb
+++ b/lib/api/concerns/packages/npm_endpoints.rb
@@ -202,7 +202,8 @@ module API
get '*package_name', format: false, requirements: ::API::Helpers::Packages::Npm::NPM_ENDPOINT_REQUIREMENTS do
package_name = params[:package_name]
available_packages =
- if Feature.enabled?(:npm_allow_packages_in_multiple_projects)
+ if endpoint_scope != :project &&
+ Feature.enabled?(:npm_allow_packages_in_multiple_projects, group_or_namespace)
finder_for_endpoint_scope(package_name).execute
else
::Packages::Npm::PackageFinder.new(package_name, project: project_or_nil)
@@ -218,9 +219,8 @@ module API
target: project_or_nil,
package_name: package_name
) do
- if endpoint_scope == :project || Feature.disabled?(:npm_allow_packages_in_multiple_projects)
- authorize_read_package!(project)
- elsif Feature.enabled?(:npm_allow_packages_in_multiple_projects)
+ if endpoint_scope != :project &&
+ Feature.enabled?(:npm_allow_packages_in_multiple_projects, group_or_namespace)
available_packages_to_user = ::Packages::Npm::PackagesForUserFinder.new(
current_user,
group_or_namespace,
@@ -232,6 +232,8 @@ module API
end
available_packages = available_packages_to_user
+ else
+ authorize_read_package!(project)
end
not_found!('Packages') if available_packages.empty?
diff --git a/lib/api/entities/bulk_imports/entity_failure.rb b/lib/api/entities/bulk_imports/entity_failure.rb
index 3e69e7fa2aa..08708a7c961 100644
--- a/lib/api/entities/bulk_imports/entity_failure.rb
+++ b/lib/api/entities/bulk_imports/entity_failure.rb
@@ -4,18 +4,14 @@ module API
module Entities
module BulkImports
class EntityFailure < Grape::Entity
- expose :relation, documentation: { type: 'string', example: 'group' }
- expose :pipeline_step, as: :step, documentation: { type: 'string', example: 'extractor' }
+ expose :relation, documentation: { type: 'string', example: 'label' }
expose :exception_message, documentation: { type: 'string', example: 'error message' } do |failure|
::Projects::ImportErrorFilter.filter_message(failure.exception_message.truncate(72))
end
expose :exception_class, documentation: { type: 'string', example: 'Exception' }
expose :correlation_id_value, documentation: { type: 'string', example: 'dfcf583058ed4508e4c7c617bd7f0edd' }
- expose :created_at, documentation: { type: 'dateTime', example: '2012-05-28T04:42:42-07:00' }
- expose :pipeline_class, documentation: {
- type: 'string', example: 'BulkImports::Groups::Pipelines::GroupPipeline'
- }
- expose :pipeline_step, documentation: { type: 'string', example: 'extractor' }
+ expose :source_url, documentation: { type: 'string', example: 'https://source.gitlab.com/group/-/epics/1' }
+ expose :source_title, documentation: { type: 'string', example: 'title' }
end
end
end
diff --git a/lib/api/entities/commit_signature.rb b/lib/api/entities/commit_signature.rb
index 9c30c3c59ea..cdd63df77f0 100644
--- a/lib/api/entities/commit_signature.rb
+++ b/lib/api/entities/commit_signature.rb
@@ -6,27 +6,24 @@ module API
expose :signature_type, documentation: { type: 'string', example: 'PGP' }
expose :signature, merge: true do |commit, options|
- if commit.signature.is_a?(::CommitSignatures::GpgSignature) || commit.raw_commit_from_rugged?
+ case commit.signature
+ when ::CommitSignatures::GpgSignature
::API::Entities::GpgCommitSignature.represent commit_signature(commit), options
- elsif commit.signature.is_a?(::CommitSignatures::X509CommitSignature)
+ when ::CommitSignatures::X509CommitSignature
::API::Entities::X509Signature.represent commit.signature, options
- elsif commit.signature.is_a?(::CommitSignatures::SshSignature)
+ when ::CommitSignatures::SshSignature
::API::Entities::SshSignature.represent(commit.signature, options)
end
end
- expose :commit_source, documentation: { type: 'string', example: 'gitaly' } do |commit, _|
- commit.raw_commit_from_rugged? ? "rugged" : "gitaly"
+ expose :commit_source, documentation: { type: 'string', example: 'gitaly' } do |_commit, _|
+ "gitaly"
end
private
def commit_signature(commit)
- if commit.raw_commit_from_rugged?
- commit.gpg_commit.signature
- else
- commit.signature
- end
+ commit.signature
end
end
end
diff --git a/lib/api/entities/group.rb b/lib/api/entities/group.rb
index d18a29ce4d4..1a1765c2e0a 100644
--- a/lib/api/entities/group.rb
+++ b/lib/api/entities/group.rb
@@ -10,7 +10,8 @@ module API
expose :project_creation_level_str, as: :project_creation_level
expose :auto_devops_enabled
expose :subgroup_creation_level_str, as: :subgroup_creation_level
- expose :emails_disabled
+ expose(:emails_disabled, documentation: { type: 'boolean' }) { |group, options| group.emails_disabled? }
+ expose :emails_enabled, documentation: { type: 'boolean' }
expose :mentions_disabled
expose :lfs_enabled?, as: :lfs_enabled
expose :default_branch_protection
diff --git a/lib/api/entities/ml/mlflow/model_versions/responses/get.rb b/lib/api/entities/ml/mlflow/model_versions/responses/get.rb
new file mode 100644
index 00000000000..14baae03644
--- /dev/null
+++ b/lib/api/entities/ml/mlflow/model_versions/responses/get.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module Ml
+ module Mlflow
+ module ModelVersions
+ module Responses
+ class Get < Grape::Entity
+ expose :model_version, with: Types::ModelVersion
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/ml/mlflow/model_versions/types/model_version.rb b/lib/api/entities/ml/mlflow/model_versions/types/model_version.rb
new file mode 100644
index 00000000000..407158521f7
--- /dev/null
+++ b/lib/api/entities/ml/mlflow/model_versions/types/model_version.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module Ml
+ module Mlflow
+ module ModelVersions
+ module Types
+ class ModelVersion < Grape::Entity
+ expose :name
+ expose :version
+ expose :creation_timestamp, documentation: { type: Integer }
+ expose :last_updated_timestamp, documentation: { type: Integer }
+ expose :user_id
+ expose :current_stage
+ expose :description
+ expose :source
+ expose :run_id
+ expose :status
+ expose :status_message
+ expose :metadata
+ expose :run_link
+ expose :aliases, documentation: { is_array: true, type: String }
+
+ private
+
+ def name
+ object.model.name
+ end
+
+ def creation_timestamp
+ object.created_at.to_i
+ end
+
+ def last_updated_timestamp
+ object.updated_at.to_i
+ end
+
+ def user_id
+ nil
+ end
+
+ def current_stage
+ "development"
+ end
+
+ def description
+ ""
+ end
+
+ def source
+ model_name = object.model.name
+ "api/v4/projects/(id)/packages/ml_models/#{model_name}/model_version/"
+ end
+
+ def run_id
+ ""
+ end
+
+ def status
+ "READY"
+ end
+
+ def status_message
+ ""
+ end
+
+ def metadata
+ []
+ end
+
+ def run_link
+ ""
+ end
+
+ def aliases
+ []
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/ml/mlflow/model_versions/types/model_version_tag.rb b/lib/api/entities/ml/mlflow/model_versions/types/model_version_tag.rb
new file mode 100644
index 00000000000..f5ad3bf3fb9
--- /dev/null
+++ b/lib/api/entities/ml/mlflow/model_versions/types/model_version_tag.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module Ml
+ module Mlflow
+ module ModelVersions
+ module Types
+ class ModelVersionTag < Grape::Entity
+ expose :key
+ expose :value
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/ml/mlflow/registered_model.rb b/lib/api/entities/ml/mlflow/registered_model.rb
new file mode 100644
index 00000000000..1ff983e1611
--- /dev/null
+++ b/lib/api/entities/ml/mlflow/registered_model.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module Ml
+ module Mlflow
+ class RegisteredModel < Grape::Entity
+ expose :name
+ expose :created_at, as: :creation_timestamp
+ expose :updated_at, as: :last_updated_timestamp
+ expose :description
+ expose(:user_id) { |model| model.user_id.to_s }
+ expose :metadata, as: :tags, using: KeyValue
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/wiki_page.rb b/lib/api/entities/wiki_page.rb
index 0f3fdd586a3..8b2c951ecf2 100644
--- a/lib/api/entities/wiki_page.rb
+++ b/lib/api/entities/wiki_page.rb
@@ -22,6 +22,10 @@ module API
expose :encoding, documentation: { type: 'string', example: 'UTF-8' } do |wiki_page|
wiki_page.content.encoding.name
end
+
+ expose :front_matter, documentation: { type: 'Hash', example: { title: "deploy" } }, if: ->(wiki_page) {
+ ::Feature.enabled?(:wiki_front_matter_title, wiki_page.container)
+ }
end
end
end
diff --git a/lib/api/github/entities.rb b/lib/api/github/entities.rb
deleted file mode 100644
index 125985f0e23..00000000000
--- a/lib/api/github/entities.rb
+++ /dev/null
@@ -1,219 +0,0 @@
-# frozen_string_literal: true
-
-# Simplified version of Github API entities.
-# It's mainly used to mimic Github API and integrate with Jira Development Panel.
-#
-module API
- module Github
- module Entities
- class Repository < Grape::Entity
- expose :id
- expose :owner do |project, options|
- root_namespace = options[:root_namespace] || project.root_namespace
-
- { login: root_namespace.path }
- end
- expose :name do |project, options|
- ::Gitlab::Jira::Dvcs.encode_project_name(project)
- end
- end
-
- class BranchCommit < Grape::Entity
- expose :id, as: :sha
- expose :type do |_|
- 'commit'
- end
- end
-
- class RepoCommit < Grape::Entity
- expose :id, as: :sha
- expose :author do |commit|
- {
- login: commit.author&.username,
- email: commit.author_email
- }
- end
- expose :committer do |commit|
- {
- login: commit.author&.username,
- email: commit.committer_email
- }
- end
- expose :commit do |commit|
- {
- author: {
- name: commit.author_name,
- email: commit.author_email,
- date: commit.authored_date.iso8601,
- type: 'User'
- },
- committer: {
- name: commit.committer_name,
- email: commit.committer_email,
- date: commit.committed_date.iso8601,
- type: 'User'
- },
- message: commit.safe_message
- }
- end
- expose :parents do |commit|
- commit.parent_ids.map { |id| { sha: id } }
- end
- expose :files do |_commit, options|
- options[:diff_files].flat_map do |diff|
- additions = diff.added_lines
- deletions = diff.removed_lines
-
- if diff.new_file?
- {
- status: 'added',
- filename: diff.new_path,
- additions: additions,
- changes: additions
- }
- elsif diff.deleted_file?
- {
- status: 'removed',
- filename: diff.old_path,
- deletions: deletions,
- changes: deletions
- }
- elsif diff.renamed_file?
- [
- {
- status: 'removed',
- filename: diff.old_path,
- deletions: deletions,
- changes: deletions
- },
- {
- status: 'added',
- filename: diff.new_path,
- additions: additions,
- changes: additions
- }
- ]
- else
- {
- status: 'modified',
- filename: diff.new_path,
- additions: additions,
- deletions: deletions,
- changes: (additions + deletions)
- }
- end
- end
- end
- end
-
- class Branch < Grape::Entity
- expose :name
-
- expose :commit, using: BranchCommit do |repo_branch, options|
- options[:project].repository.commit(repo_branch.dereferenced_target)
- end
- end
-
- class User < Grape::Entity
- expose :id
- expose :username, as: :login
- expose :user_url, as: :url
- expose :user_url, as: :html_url
- expose :avatar_url do |user|
- user.avatar_url(only_path: false)
- end
-
- private
-
- def user_url
- Gitlab::Routing.url_helpers.user_url(object)
- end
- end
-
- class NoteableComment < Grape::Entity
- expose :id
- expose :author, as: :user, using: User
- expose :note, as: :body
- expose :created_at
- end
-
- class PullRequest < Grape::Entity
- expose :title
- expose :assignee, using: User do |merge_request|
- merge_request.assignee
- end
- expose :author, as: :user, using: User
- expose :created_at
- expose :description, as: :body
- # Since Jira service requests `/repos/-/jira/pulls` (without project
- # scope), we need to make it work with ID instead IID.
- expose :id, as: :number
- # GitHub doesn't have a "merged" or "closed" state. It's just "open" or
- # "closed".
- expose :state do |merge_request|
- case merge_request.state
- when 'opened', 'locked'
- 'open'
- when 'merged'
- 'closed'
- else
- merge_request.state
- end
- end
- expose :merged?, as: :merged
- expose :merged_at do |merge_request|
- merge_request.metrics&.merged_at
- end
- expose :closed_at do |merge_request|
- merge_request.metrics&.latest_closed_at
- end
- expose :updated_at
- expose :html_url do |merge_request|
- Gitlab::UrlBuilder.build(merge_request)
- end
- expose :head do
- expose :source_branch, as: :label
- expose :source_branch, as: :ref
- expose :source_project, as: :repo, using: Repository
- end
- expose :base do
- expose :target_branch, as: :label
- expose :target_branch, as: :ref
- expose :target_project, as: :repo, using: Repository
- end
- end
-
- class PullRequestPayload < Grape::Entity
- expose :action do |merge_request|
- case merge_request.state
- when 'merged', 'closed'
- 'closed'
- else
- 'opened'
- end
- end
-
- expose :id
- expose :pull_request, using: PullRequest do |merge_request|
- merge_request
- end
- end
-
- class PullRequestEvent < Grape::Entity
- expose :id do |merge_request|
- updated_at = merge_request.updated_at.to_i
- "#{merge_request.id}-#{updated_at}"
- end
- expose :type do |_merge_request|
- 'PullRequestEvent'
- end
- expose :updated_at, as: :created_at
- expose :payload, using: PullRequestPayload do |merge_request|
- # The merge request data is used by PullRequestPayload and PullRequest, so we just provide it
- # here. Otherwise Grape::Entity would try to access a field "payload" on Merge Request.
- merge_request
- end
- end
- end
- end
-end
diff --git a/lib/api/group_packages.rb b/lib/api/group_packages.rb
index c2b4cbf732f..b363f59b7ad 100644
--- a/lib/api/group_packages.rb
+++ b/lib/api/group_packages.rb
@@ -47,6 +47,9 @@ module API
optional :package_name,
type: String,
desc: 'Return packages with this name'
+ optional :package_version,
+ type: String,
+ desc: 'Return packages with this version'
optional :include_versionless,
type: Boolean,
desc: 'Returns packages without a version'
@@ -60,7 +63,8 @@ module API
current_user,
user_group,
declared(params).slice(
- :exclude_subgroups, :order_by, :sort, :package_type, :package_name, :include_versionless, :status
+ :exclude_subgroups, :order_by, :sort, :package_type, :package_name,
+ :package_version, :include_versionless, :status
)
).execute
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index 2efdfe109f7..1ff64cd2ffd 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -254,6 +254,7 @@ module API
group = find_group!(params[:id])
group.preload_shared_group_links
+ mark_throttle! :update_namespace_name, scope: group if params.key?(:name) && params[:name].present?
authorize! :admin_group, group
group.remove_avatar! if params.key?(:avatar) && params[:avatar].nil?
diff --git a/lib/api/helm_packages.rb b/lib/api/helm_packages.rb
index 8260d8a88f8..c811f47cb5b 100644
--- a/lib/api/helm_packages.rb
+++ b/lib/api/helm_packages.rb
@@ -75,6 +75,8 @@ module API
requires :file_name, type: String, desc: 'Helm package file name', documentation: { example: 'mychart' }
end
get ":channel/charts/:file_name.tgz" do
+ not_found!("Format #{params[:format]}") unless params[:format].nil?
+
project = authorized_user_project(action: :read_package)
authorize_read_package!(project)
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 56b157f662a..bb94d5d14d0 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -141,7 +141,7 @@ module API
def find_project(id)
return unless id
- projects = Project.without_deleted.not_hidden
+ projects = find_project_scopes
if id.is_a?(Integer) || id =~ INTEGER_ID_REGEX
projects.find_by(id: id)
@@ -151,6 +151,11 @@ module API
end
# rubocop: enable CodeReuse/ActiveRecord
+ # Can be overriden by API endpoints
+ def find_project_scopes
+ Project.without_deleted.not_hidden
+ end
+
def find_project!(id)
project = find_project(id)
@@ -337,6 +342,12 @@ module API
unauthorized!
end
+ def authenticate_by_gitlab_shell_or_workhorse_token!
+ return require_gitlab_workhorse! unless headers[GITLAB_SHELL_API_HEADER].present?
+
+ authenticate_by_gitlab_shell_token!
+ end
+
def authenticated_with_can_read_all_resources!
authenticate!
forbidden! unless current_user.can_read_all_resources?
@@ -391,6 +402,10 @@ module API
authorize! :update_build, user_project
end
+ def authorize_cancel_builds!
+ authorize! :cancel_build, user_project
+ end
+
def require_repository_enabled!(subject = :global)
not_found!("Repository") unless user_project.feature_available?(:repository, current_user)
end
@@ -758,6 +773,7 @@ module API
finder_params[:id_before] = sanitize_id_param(params[:id_before]) if params[:id_before]
finder_params[:updated_after] = declared_params[:updated_after] if declared_params[:updated_after]
finder_params[:updated_before] = declared_params[:updated_before] if declared_params[:updated_before]
+ finder_params[:include_pending_delete] = declared_params[:include_pending_delete] if declared_params[:include_pending_delete]
finder_params
end
@@ -891,7 +907,7 @@ module API
def project_moved?(id, project)
return false unless Feature.enabled?(:api_redirect_moved_projects)
return false unless id.is_a?(String) && id.include?('/')
- return false if project.blank? || id == project.full_path
+ return false if project.blank? || project.full_path.casecmp?(id)
return false unless params[:id] == id
true
diff --git a/lib/api/helpers/groups_helpers.rb b/lib/api/helpers/groups_helpers.rb
index f7802938d8b..fbe13bfe8f7 100644
--- a/lib/api/helpers/groups_helpers.rb
+++ b/lib/api/helpers/groups_helpers.rb
@@ -18,7 +18,8 @@ module API
optional :project_creation_level, type: String, values: ::Gitlab::Access.project_creation_string_values, desc: 'Determine if developers can create projects in the group', as: :project_creation_level_str
optional :auto_devops_enabled, type: Boolean, desc: 'Default to Auto DevOps pipeline for all projects within this group'
optional :subgroup_creation_level, type: String, values: ::Gitlab::Access.subgroup_creation_string_values, desc: 'Allowed to create subgroups', as: :subgroup_creation_level_str
- optional :emails_disabled, type: Boolean, desc: 'Disable email notifications'
+ optional :emails_disabled, type: Boolean, desc: '_(Deprecated)_ Disable email notifications. Use: emails_enabled'
+ optional :emails_enabled, type: Boolean, desc: 'Enable email notifications'
optional :mentions_disabled, type: Boolean, desc: 'Disable a group from getting mentioned'
optional :lfs_enabled, type: Boolean, desc: 'Enable/disable LFS for the projects in this group'
optional :request_access_enabled, type: Boolean, desc: 'Allow users to request member access'
diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb
index f66f899c98b..0c5b12d48e9 100644
--- a/lib/api/helpers/internal_helpers.rb
+++ b/lib/api/helpers/internal_helpers.rb
@@ -123,6 +123,10 @@ module API
# Defined in EE
end
+ def need_git_audit_event?
+ false
+ end
+
private
def repository_path
diff --git a/lib/api/helpers/kubernetes/agent_helpers.rb b/lib/api/helpers/kubernetes/agent_helpers.rb
index 50a8c2a5aed..aa4f4310e1d 100644
--- a/lib/api/helpers/kubernetes/agent_helpers.rb
+++ b/lib/api/helpers/kubernetes/agent_helpers.rb
@@ -41,7 +41,7 @@ module API
end
def agent_has_access_to_project?(project)
- Guest.can?(:download_code, project) || agent.has_access_to?(project)
+ ::Users::Anonymous.can?(:download_code, project) || agent.has_access_to?(project)
end
def increment_unique_events
diff --git a/lib/api/helpers/packages/npm.rb b/lib/api/helpers/packages/npm.rb
index ef3da055b19..c91eef0c4b0 100644
--- a/lib/api/helpers/packages/npm.rb
+++ b/lib/api/helpers/packages/npm.rb
@@ -64,7 +64,7 @@ module API
package_name = params[:package_name]
namespace =
- if Feature.enabled?(:npm_allow_packages_in_multiple_projects)
+ if Feature.enabled?(:npm_allow_packages_in_multiple_projects, top_namespace_from(package_name))
top_namespace_from(package_name)
else
namespace_path = ::Packages::Npm.scope_of(package_name)
@@ -94,10 +94,12 @@ module API
private
def top_namespace_from(package_name)
- namespace_path = ::Packages::Npm.scope_of(package_name)
- return unless namespace_path
+ strong_memoize_with(:top_namespace_from, package_name) do
+ namespace_path = ::Packages::Npm.scope_of(package_name)
+ next unless namespace_path
- Namespace.top_most.by_path(namespace_path)
+ Namespace.top_most.by_path(namespace_path)
+ end
end
def group
diff --git a/lib/api/helpers/pagination_strategies.rb b/lib/api/helpers/pagination_strategies.rb
index 5fbc3081ee8..4353ba0e99a 100644
--- a/lib/api/helpers/pagination_strategies.rb
+++ b/lib/api/helpers/pagination_strategies.rb
@@ -50,7 +50,7 @@ module API
offset_limit = limit_for_scope(request_scope)
if (Gitlab::Pagination::Keyset.available_for_type?(relation) ||
cursor_based_keyset_pagination_supported?(relation)) &&
- cursor_based_keyset_pagination_enforced?(relation) &&
+ cursor_based_keyset_pagination_enforced?(request_scope, relation) &&
offset_limit_exceeded?(offset_limit)
return error!("Offset pagination has a maximum allowed offset of #{offset_limit} " \
@@ -65,8 +65,8 @@ module API
Gitlab::Pagination::CursorBasedKeyset.available_for_type?(relation)
end
- def cursor_based_keyset_pagination_enforced?(relation)
- Gitlab::Pagination::CursorBasedKeyset.enforced_for_type?(relation)
+ def cursor_based_keyset_pagination_enforced?(request_scope, relation)
+ Gitlab::Pagination::CursorBasedKeyset.enforced_for_type?(request_scope, relation)
end
def keyset_pagination_enabled?
diff --git a/lib/api/helpers/rate_limiter.rb b/lib/api/helpers/rate_limiter.rb
index be92277c25a..39940d86fbf 100644
--- a/lib/api/helpers/rate_limiter.rb
+++ b/lib/api/helpers/rate_limiter.rb
@@ -18,6 +18,10 @@ module API
render_api_error!({ error: _('This endpoint has been requested too many times. Try again later.') }, 429)
end
+
+ def mark_throttle!(key, scope:)
+ Gitlab::ApplicationRateLimiter.throttled?(key, scope: scope)
+ end
end
end
end
diff --git a/lib/api/internal/base.rb b/lib/api/internal/base.rb
index f9dc888fbeb..87b3838fb85 100644
--- a/lib/api/internal/base.rb
+++ b/lib/api/internal/base.rb
@@ -4,12 +4,18 @@ module API
# Internal access API
module Internal
class Base < ::API::Base
+ include Gitlab::RackLoadBalancingHelpers
+
before { authenticate_by_gitlab_shell_token! }
before do
api_endpoint = env['api.endpoint']
feature_category = api_endpoint.options[:for].try(:feature_category_for_app, api_endpoint).to_s
+ if actor.user
+ load_balancer_stick_request(::User, :user, actor.user.id)
+ end
+
Gitlab::ApplicationContext.push(
user: -> { actor&.user },
project: -> { project },
@@ -49,6 +55,11 @@ module API
env = parse_env
Gitlab::Git::HookEnv.set(gl_repository, env) if container
+ # Snapshot repositories have different relative path than the main repository. For access
+ # checks that need quarantined objects the relative path in also sent with Gitaly RPCs
+ # calls as a header.
+ populate_relative_path(params[:relative_path])
+
actor.update_last_used_at!
check_result = access_check_result
@@ -66,7 +77,8 @@ module API
git_config_options: ["uploadpack.allowFilter=true",
"uploadpack.allowAnySHA1InWant=true"],
gitaly: gitaly_payload(params[:action]),
- gl_console_messages: check_result.console_messages
+ gl_console_messages: check_result.console_messages,
+ need_audit: need_git_audit_event?
}.merge!(actor.key_details)
# Custom option for git-receive-pack command
@@ -77,7 +89,9 @@ module API
payload[:git_config_options] << "receive.maxInputSize=#{receive_max_input_size.megabytes}"
end
- send_git_audit_streaming_event(protocol: params[:protocol], action: params[:action])
+ unless Feature.enabled?(:log_git_streaming_audit_events, project)
+ send_git_audit_streaming_event(protocol: params[:protocol], action: params[:action])
+ end
response_with_status(**payload)
when ::Gitlab::GitAccessResult::CustomAction
@@ -88,6 +102,12 @@ module API
end
# rubocop: enable Metrics/AbcSize
+ def populate_relative_path(relative_path)
+ return unless Gitlab::SafeRequestStore.active?
+
+ Gitlab::SafeRequestStore[:gitlab_git_relative_path] = relative_path
+ end
+
def validate_actor(actor)
return 'Could not find the given key' unless actor.key
@@ -112,6 +132,7 @@ module API
# username - user name for Git over SSH in keyless SSH cert mode
# protocol - Git access protocol being used, e.g. HTTP or SSH
# project - project full_path (not path on disk)
+ # relative_path - relative path of repository having access checks performed.
# action - git action (git-upload-pack or git-receive-pack)
# changes - changes as "oldrev newrev ref", see Gitlab::ChangesList
# check_ip - optional, only in EE version, may limit access to
diff --git a/lib/api/internal/shellhorse.rb b/lib/api/internal/shellhorse.rb
new file mode 100644
index 00000000000..89210c8a78a
--- /dev/null
+++ b/lib/api/internal/shellhorse.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+module API
+ module Internal
+ class Shellhorse < ::API::Base
+ before { authenticate_by_gitlab_shell_or_workhorse_token! }
+
+ helpers ::API::Helpers::InternalHelpers
+
+ COMMANDS_TO_AUDIT = %w[git-upload-pack git-receive-pack].freeze
+
+ helpers do
+ def check_clone_or_pull_or_push_verb(params)
+ return 'push' if params[:action] == 'git-receive-pack'
+
+ # we must set the default value for wants/haves because
+ # gitlab shell/workhorse will trim the whole posted params
+ # json key if its value is 0
+ wants = haves = 0
+ if params.key?(:packfile_stats)
+ wants = Integer(params[:packfile_stats][:wants]) if params[:packfile_stats][:wants].present?
+ haves = Integer(params[:packfile_stats][:haves]) if params[:packfile_stats][:haves].present?
+ end
+
+ wants > 0 && haves == 0 ? 'clone' : 'pull'
+ end
+ end
+
+ namespace 'internal' do
+ namespace 'shellhorse' do
+ params do
+ requires :action, type: String
+ requires :protocol, type: String
+ requires :gl_repository, type: String # repository identifier, such as project-7
+ optional :packfile_stats, type: Hash do
+ # wants is the number of objects the client announced it wants.
+ optional :wants, type: Integer
+ # haves is the number of objects the client announced it has.
+ optional :haves, type: Integer
+ end
+ end
+
+ post '/git_audit_event', feature_category: :source_code_management do
+ unless COMMANDS_TO_AUDIT.include?(params[:action])
+ break response_with_status(code: 400, success: false, message: "No valid action specified")
+ end
+
+ check_result = access_check_result
+ break check_result if unsuccessful_response?(check_result)
+
+ unless need_git_audit_event?
+ break response_with_status(code: 200, success: false, message: "No git audit event needed")
+ end
+
+ unless check_result.is_a?(::Gitlab::GitAccessResult::Success)
+ break response_with_status(code: 500, success: false,
+ message: ::API::Helpers::InternalHelpers::UNKNOWN_CHECK_RESULT_ERROR)
+ end
+
+ msg = {
+ protocol: params[:protocol],
+ action: params[:action],
+ verb: check_clone_or_pull_or_push_verb(params)
+ }
+ send_git_audit_streaming_event(msg)
+ response_with_status(message: msg)
+ end
+ end
+ end
+ end
+ end
+end
+
+API::Internal::Shellhorse.prepend_mod_with('API::Internal::Shellhorse')
diff --git a/lib/api/invitations.rb b/lib/api/invitations.rb
index 34f9538b047..d625b2c0fe6 100644
--- a/lib/api/invitations.rb
+++ b/lib/api/invitations.rb
@@ -10,6 +10,12 @@ module API
helpers ::API::Helpers::MembersHelpers
+ helpers do
+ params :invitation_params_ee do
+ # Overriden in EE
+ end
+ end
+
%w[group project].each do |source_type|
params do
requires :id, type: String, desc: "The #{source_type} ID"
@@ -26,6 +32,8 @@ module API
optional :user_id, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, desc: 'The user ID of the new member or multiple IDs separated by commas.'
optional :expires_at, type: DateTime, desc: 'Date string in the format YEAR-MONTH-DAY'
optional :invite_source, type: String, desc: 'Source that triggered the member creation process', default: 'invitations-api'
+
+ use :invitation_params_ee
end
post ":id/invitations", urgency: :low do
::Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/354016')
@@ -34,11 +42,7 @@ module API
source = find_source(source_type, params[:id])
- if ::Feature.enabled?(:admin_group_member, source)
- authorize_admin_source_member!(source_type, source)
- else
- authorize_admin_source!(source_type, source)
- end
+ authorize_admin_source_member!(source_type, source)
create_service_params = params.merge(source: source)
@@ -61,11 +65,7 @@ module API
source = find_source(source_type, params[:id])
query = params[:query]
- if ::Feature.enabled?(:admin_group_member, source)
- authorize_admin_source_member!(source_type, source)
- else
- authorize_admin_source!(source_type, source)
- end
+ authorize_admin_source_member!(source_type, source)
invitations = paginate(retrieve_member_invitations(source, query))
@@ -80,16 +80,14 @@ module API
requires :email, type: String, desc: 'The email address of the invitation'
optional :access_level, type: Integer, values: Gitlab::Access.all_values, desc: 'A valid access level (defaults: `30`, developer access level)'
optional :expires_at, type: DateTime, desc: 'Date string in ISO 8601 format (`YYYY-MM-DDTHH:MM:SSZ`)'
+
+ use :invitation_params_ee
end
put ":id/invitations/:email", requirements: { email: %r{[^/]+} } do
source = find_source(source_type, params.delete(:id))
invite_email = params[:email]
- if ::Feature.enabled?(:admin_group_member, source)
- authorize_admin_source_member!(source_type, source)
- else
- authorize_admin_source!(source_type, source)
- end
+ authorize_admin_source_member!(source_type, source)
invite = retrieve_member_invitations(source, invite_email).first
not_found! unless invite
@@ -127,11 +125,7 @@ module API
source = find_source(source_type, params[:id])
invite_email = params[:email]
- if ::Feature.enabled?(:admin_group_member, source)
- authorize_admin_source_member!(source_type, source)
- else
- authorize_admin_source!(source_type, source)
- end
+ authorize_admin_source_member!(source_type, source)
invite = retrieve_member_invitations(source, invite_email).first
not_found! unless invite
@@ -145,3 +139,5 @@ module API
end
end
end
+
+API::Members.prepend_mod
diff --git a/lib/api/lint.rb b/lib/api/lint.rb
index 26619e6924f..b2f0f54e380 100644
--- a/lib/api/lint.rb
+++ b/lib/api/lint.rb
@@ -29,7 +29,7 @@ module API
not_found! 'Commit' unless user_project.commit(sha).present?
- content = user_project.repository.gitlab_ci_yml_for(sha, user_project.ci_config_path_or_default)
+ content = user_project.repository.blob_data_at(sha, user_project.ci_config_path_or_default)
result = Gitlab::Ci::Lint
.new(project: user_project, current_user: current_user, sha: sha)
.validate(content, dry_run: params[:dry_run], ref: params[:ref] || user_project.default_branch)
diff --git a/lib/api/maven_packages.rb b/lib/api/maven_packages.rb
index 517de98a148..14c3fccee32 100644
--- a/lib/api/maven_packages.rb
+++ b/lib/api/maven_packages.rb
@@ -228,7 +228,7 @@ module API
requires :path, type: String, desc: 'Package path', documentation: { example: 'foo/bar/mypkg/1.0-SNAPSHOT' }
requires :file_name, type: String, desc: 'Package file name', regexp: Gitlab::Regex.maven_file_name_regex, documentation: { example: 'mypkg-1.0-SNAPSHOT.pom' }
end
- route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true
+ route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true, basic_auth_personal_access_token: true
put ':id/packages/maven/*path/:file_name/authorize', requirements: MAVEN_ENDPOINT_REQUIREMENTS do
authorize_upload!
@@ -254,7 +254,7 @@ module API
requires :file_name, type: String, desc: 'Package file name', regexp: Gitlab::Regex.maven_file_name_regex, documentation: { example: 'mypkg-1.0-SNAPSHOT.pom' }
requires :file, type: ::API::Validations::Types::WorkhorseFile, desc: 'The package file to be published (generated by Multipart middleware)', documentation: { type: 'file' }
end
- route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true
+ route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true, basic_auth_personal_access_token: true
put ':id/packages/maven/*path/:file_name', requirements: MAVEN_ENDPOINT_REQUIREMENTS do
unprocessable_entity! if Gitlab::FIPS.enabled? && params[:file].md5
authorize_upload!
diff --git a/lib/api/members.rb b/lib/api/members.rb
index bdbdea70da0..56a15c41e1c 100644
--- a/lib/api/members.rb
+++ b/lib/api/members.rb
@@ -118,11 +118,8 @@ module API
post ":id/members", feature_category: feature_category do
source = find_source(source_type, params[:id])
- if ::Feature.enabled?(:admin_group_member, source)
- authorize_admin_source_member!(source_type, source)
- else
- authorize_admin_source!(source_type, source)
- end
+
+ authorize_admin_source_member!(source_type, source)
create_service_params = params.merge(source: source)
@@ -148,11 +145,7 @@ module API
source = find_source(source_type, params.delete(:id))
member = source_members(source).find_by!(user_id: params[:user_id])
- if ::Feature.enabled?(:admin_group_member, source)
- authorize_update_source_member!(source_type, member)
- else
- authorize_admin_source!(source_type, source)
- end
+ authorize_update_source_member!(source_type, member)
result = ::Members::UpdateService
.new(current_user, declared_params(include_missing: false))
diff --git a/lib/api/merge_request_approvals.rb b/lib/api/merge_request_approvals.rb
index 35fdcfe3ab0..d0c9400039a 100644
--- a/lib/api/merge_request_approvals.rb
+++ b/lib/api/merge_request_approvals.rb
@@ -86,6 +86,10 @@ module API
not_found! unless success
+ ::MergeRequests::UpdateReviewerStateService
+ .new(project: user_project, current_user: current_user)
+ .execute(merge_request, "unreviewed")
+
present_approval(merge_request)
end
diff --git a/lib/api/ml/mlflow/api_helpers.rb b/lib/api/ml/mlflow/api_helpers.rb
index 19ac0dbba1b..aefa156717c 100644
--- a/lib/api/ml/mlflow/api_helpers.rb
+++ b/lib/api/ml/mlflow/api_helpers.rb
@@ -12,6 +12,10 @@ module API
unauthorized! unless can?(current_user, :write_model_experiments, user_project)
end
+ def check_api_model_registry_read!
+ not_found! unless can?(current_user, :read_model_registry, user_project)
+ end
+
def resource_not_found!
render_structured_api_error!({ error_code: 'RESOURCE_DOES_NOT_EXIST' }, 404)
end
@@ -79,6 +83,10 @@ module API
candidate_repository.by_eid(eid) || resource_not_found!
end
+ def find_model(project, name)
+ ::Ml::FindModelService.new(project, name).execute || resource_not_found!
+ end
+
def packages_url
path = api_v4_projects_packages_generic_package_version_path(
id: user_project.id, package_name: '', file_name: ''
diff --git a/lib/api/ml/mlflow/entrypoint.rb b/lib/api/ml/mlflow/entrypoint.rb
index 3e0cb723580..7157d2a03f6 100644
--- a/lib/api/ml/mlflow/entrypoint.rb
+++ b/lib/api/ml/mlflow/entrypoint.rb
@@ -26,9 +26,6 @@ module API
status 200
authenticate!
-
- check_api_read!
- check_api_write! unless request.get? || request.head?
end
rescue_from ActiveRecord::ActiveRecordError do |e|
@@ -44,7 +41,9 @@ module API
end
namespace MLFLOW_API_PREFIX do
mount ::API::Ml::Mlflow::Experiments
+ mount ::API::Ml::Mlflow::ModelVersions
mount ::API::Ml::Mlflow::Runs
+ mount ::API::Ml::Mlflow::RegisteredModels
end
end
end
diff --git a/lib/api/ml/mlflow/experiments.rb b/lib/api/ml/mlflow/experiments.rb
index 614112f703b..1a501291941 100644
--- a/lib/api/ml/mlflow/experiments.rb
+++ b/lib/api/ml/mlflow/experiments.rb
@@ -9,6 +9,11 @@ module API
class Experiments < ::API::Base
feature_category :mlops
+ before do
+ check_api_read!
+ check_api_write! unless request.get? || request.head?
+ end
+
resource :experiments do
desc 'Fetch experiment by experiment_id' do
success Entities::Ml::Mlflow::GetExperiment
diff --git a/lib/api/ml/mlflow/model_versions.rb b/lib/api/ml/mlflow/model_versions.rb
new file mode 100644
index 00000000000..989b79e5774
--- /dev/null
+++ b/lib/api/ml/mlflow/model_versions.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module API
+ module Ml
+ module Mlflow
+ class ModelVersions < ::API::Base
+ feature_category :mlops
+
+ resource :model_versions do
+ desc 'Fetch model version by name and version' do
+ success Entities::Ml::Mlflow::ModelVersions::Responses::Get
+ detail 'https://mlflow.org/docs/2.6.0/rest-api.html#get-modelversion'
+ end
+ params do
+ requires :name, type: String, desc: 'Model version name'
+ requires :version, type: String, desc: 'Model version number'
+ end
+ get 'get', urgency: :low do
+ check_api_model_registry_read!
+ resource_not_found! unless params[:name] && params[:version]
+ model_version = ::Ml::ModelVersions::GetModelVersionService.new(
+ user_project, params[:name], params[:version]
+ ).execute
+ resource_not_found! unless model_version
+ response = { model_version: model_version }
+ present response, with: Entities::Ml::Mlflow::ModelVersions::Responses::Get
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/ml/mlflow/registered_models.rb b/lib/api/ml/mlflow/registered_models.rb
new file mode 100644
index 00000000000..18b705ad214
--- /dev/null
+++ b/lib/api/ml/mlflow/registered_models.rb
@@ -0,0 +1,96 @@
+# frozen_string_literal: true
+
+require 'mime/types'
+
+module API
+ # MLFlow integration API, replicating the Rest API https://www.mlflow.org/docs/latest/rest-api.html#rest-api
+ module Ml
+ module Mlflow
+ class RegisteredModels < ::API::Base
+ feature_category :mlops
+
+ before do
+ check_api_read!
+ check_api_write! unless request.get? || request.head?
+ check_api_model_registry_read!
+ end
+
+ resource 'registered-models' do
+ desc 'Creates a Registered Model.' do
+ success Entities::Ml::Mlflow::RegisteredModel
+ detail 'MLFlow Registered Models map to GitLab Models. https://mlflow.org/docs/2.6.0/rest-api.html#create-registeredmodel'
+ end
+ params do
+ requires :name, type: String,
+ desc: 'Register models under this name.'
+ optional :description, type: String,
+ desc: 'Optional description for registered model.'
+ optional :tags, type: Array, desc: 'Additional metadata for registered model.'
+ end
+ post 'create', urgency: :low do
+ present ::Ml::CreateModelService.new(
+ user_project,
+ params[:name],
+ current_user,
+ params[:description],
+ params[:tags]
+ ).execute,
+ with: Entities::Ml::Mlflow::RegisteredModel,
+ root: :registered_model
+ rescue ActiveRecord::RecordInvalid
+ resource_already_exists!
+ end
+
+ desc 'Fetch a Registered Model by Name' do
+ success Entities::Ml::Mlflow::RegisteredModel
+ detail 'https://www.mlflow.org/docs/1.28.0/rest-api.html#get-registeredmodel'
+ end
+ params do
+ # The name param is actually required, however it is listed as optional here
+ # we can send a custom error response required by MLFlow
+ optional :name, type: String, default: '',
+ desc: 'Registered model unique name identifier, in reference to the project'
+ end
+ get 'get', urgency: :low do
+ present find_model(user_project, params[:name]), with: Entities::Ml::Mlflow::RegisteredModel,
+ root: :registered_model
+ end
+
+ desc 'Update a Registered Model by Name' do
+ success Entities::Ml::Mlflow::RegisteredModel
+ detail 'https://mlflow.org/docs/2.6.0/rest-api.html#update-registeredmodel'
+ end
+ params do
+ # The name param is actually required, however it is listed as optional here
+ # we can send a custom error response required by MLFlow
+ optional :name, type: String,
+ desc: 'Registered model unique name identifier, in reference to the project'
+ optional :description, type: String,
+ desc: 'Optional description for registered model.'
+ end
+ patch 'update', urgency: :low do
+ present ::Ml::UpdateModelService.new(find_model(user_project, params[:name]), params[:description]).execute,
+ with: Entities::Ml::Mlflow::RegisteredModel, root: :registered_model
+ end
+
+ desc 'Fetch the latest Model Version for the given Registered Model Name' do
+ success Entities::Ml::Mlflow::ModelVersions::Types::ModelVersion
+ detail 'https://mlflow.org/docs/2.6.0/rest-api.html#get-latest-modelversions'
+ end
+ params do
+ # The name param is actually required, however it is listed as optional here
+ # we can send a custom error response required by MLFlow
+ optional :name, type: String,
+ desc: 'Registered model unique name identifier, in reference to the project'
+ end
+ post 'get-latest-versions', urgency: :low do
+ model = find_model(user_project, params[:name])
+
+ present [model.latest_version], with: Entities::Ml::Mlflow::ModelVersions::Types::ModelVersion,
+ root: :model_versions
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/ml/mlflow/runs.rb b/lib/api/ml/mlflow/runs.rb
index ac052d8bff5..6716db21407 100644
--- a/lib/api/ml/mlflow/runs.rb
+++ b/lib/api/ml/mlflow/runs.rb
@@ -9,6 +9,11 @@ module API
class Runs < ::API::Base
feature_category :mlops
+ before do
+ check_api_read!
+ check_api_write! unless request.get? || request.head?
+ end
+
resource :runs do
desc 'Creates a Run.' do
success Entities::Ml::Mlflow::Run
diff --git a/lib/api/nuget_project_packages.rb b/lib/api/nuget_project_packages.rb
index 46b388a2fda..b061876b997 100644
--- a/lib/api/nuget_project_packages.rb
+++ b/lib/api/nuget_project_packages.rb
@@ -102,7 +102,7 @@ module API
end
def check_duplicate(file_params, symbol_package)
- return if symbol_package || Feature.disabled?(:nuget_duplicates_option, project_or_group.namespace)
+ return if symbol_package
service_params = file_params.merge(remote_url: params['package.remote_url'])
response = ::Packages::Nuget::CheckDuplicatesService.new(project_or_group, current_user, service_params).execute
diff --git a/lib/api/personal_access_tokens.rb b/lib/api/personal_access_tokens.rb
index 9d234ca0593..de00b66ead3 100644
--- a/lib/api/personal_access_tokens.rb
+++ b/lib/api/personal_access_tokens.rb
@@ -72,11 +72,17 @@ module API
detail 'Roates a personal access token.'
success Entities::PersonalAccessTokenWithToken
end
+ params do
+ optional :expires_at,
+ type: Date,
+ desc: "The expiration date of the token",
+ documentation: { example: '2021-01-31' }
+ end
post ':id/rotate' do
token = PersonalAccessToken.find_by_id(params[:id])
if Ability.allowed?(current_user, :manage_user_personal_access_token, token&.user)
- response = ::PersonalAccessTokens::RotateService.new(current_user, token).execute
+ response = ::PersonalAccessTokens::RotateService.new(current_user, token).execute(declared_params)
if response.success?
status :ok
diff --git a/lib/api/project_packages.rb b/lib/api/project_packages.rb
index 6b2ba41f013..7f531525870 100644
--- a/lib/api/project_packages.rb
+++ b/lib/api/project_packages.rb
@@ -46,6 +46,8 @@ module API
desc: 'Return packages of a certain type'
optional :package_name, type: String,
desc: 'Return packages with this name'
+ optional :package_version, type: String,
+ desc: 'Return packages with this version'
optional :include_versionless, type: Boolean,
desc: 'Returns packages without a version'
optional :status, type: String, values: Packages::Package.statuses.keys,
@@ -55,7 +57,7 @@ module API
get ':id/packages' do
packages = ::Packages::PackagesFinder.new(
user_project,
- declared_params.slice(:order_by, :sort, :package_type, :package_name, :include_versionless, :status)
+ declared_params.slice(:order_by, :sort, :package_type, :package_name, :package_version, :include_versionless, :status)
).execute
present paginate(packages), with: ::API::Entities::Package, user: current_user, namespace: user_project.namespace
diff --git a/lib/api/project_repository_storage_moves.rb b/lib/api/project_repository_storage_moves.rb
index 5777b8754e7..b79348c87bf 100644
--- a/lib/api/project_repository_storage_moves.rb
+++ b/lib/api/project_repository_storage_moves.rb
@@ -8,6 +8,16 @@ module API
feature_category :gitaly
+ helpers do
+ extend ::Gitlab::Utils::Override
+
+ # Allow to move projects in hidden/pending_delete state
+ override :find_project_scopes
+ def find_project_scopes
+ Project
+ end
+ end
+
resource :project_repository_storage_moves do
desc 'Get a list of all project repository storage moves' do
detail 'This feature was introduced in GitLab 13.0.'
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index ac28effea43..3b80fd125ca 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -159,6 +159,7 @@ module API
optional :topic_id, type: Integer, desc: 'Limit results to projects with the assigned topic given by the topic ID'
optional :updated_before, type: DateTime, desc: 'Return projects updated before the specified datetime. Format: ISO 8601 YYYY-MM-DDTHH:MM:SSZ'
optional :updated_after, type: DateTime, desc: 'Return projects updated after the specified datetime. Format: ISO 8601 YYYY-MM-DDTHH:MM:SSZ'
+ optional :include_pending_delete, type: Boolean, desc: 'Include projects in pending delete state. Can only be set by admins'
use :optional_filter_params_ee
end
@@ -470,6 +471,7 @@ module API
optional :description, type: String, desc: 'The description that will be assigned to the fork', documentation: { example: 'Description' }
optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The visibility of the fork'
optional :mr_default_target_self, type: Boolean, desc: 'Merge requests of this forked project targets itself by default'
+ optional :branches, type: String, desc: 'Branches to fork'
end
post ':id/fork', feature_category: :source_code_management do
Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/20759')
@@ -489,6 +491,7 @@ module API
service = ::Projects::ForkService.new(user_project, current_user, fork_params)
+ not_found!('Source Branch') if fork_params[:branches].present? && !service.valid_fork_branch?(fork_params[:branches])
not_found!('Target Namespace') unless service.valid_fork_target?
forked_project = service.execute
@@ -792,7 +795,12 @@ module API
not_found!('Group Link') unless link
destroy_conditionally!(link) do
- ::Projects::GroupLinks::DestroyService.new(user_project, current_user).execute(link)
+ result = ::Projects::GroupLinks::DestroyService.new(user_project, current_user).execute(link)
+
+ if result.error?
+ status = :not_found if result.reason == :not_found
+ render_api_error!(result.message, status)
+ end
end
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/lib/api/pypi_packages.rb b/lib/api/pypi_packages.rb
index 027a11738d3..3313b3a87cd 100644
--- a/lib/api/pypi_packages.rb
+++ b/lib/api/pypi_packages.rb
@@ -280,6 +280,13 @@ module API
optional :requires_python, type: String, documentation: { example: '>=3.7' }
optional :md5_digest, type: String, documentation: { example: '900150983cd24fb0d6963f7d28e17f72' }
optional :sha256_digest, type: String, regexp: Gitlab::Regex.sha256_regex, documentation: { example: 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad' }
+ optional :metadata_version, type: String, documentation: { example: '2.3' }
+ optional :author_email, type: String, documentation: { example: 'cschultz@example.com, snoopy@peanuts.com' }
+ optional :description, type: String
+ optional :description_content_type, type: String,
+ documentation: { example: 'text/markdown; charset=UTF-8; variant=GFM' }
+ optional :summary, type: String, documentation: { example: 'A module for collecting votes from beagles.' }
+ optional :keywords, type: String, documentation: { example: 'dog,puppy,voting,election' }
end
route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth
diff --git a/lib/api/releases.rb b/lib/api/releases.rb
index 5d056ade3da..83085b5b7e3 100644
--- a/lib/api/releases.rb
+++ b/lib/api/releases.rb
@@ -270,8 +270,6 @@ module API
.execute
if result[:status] == :success
- log_release_created_audit_event(result[:release])
-
present result[:release], with: Entities::Release, current_user: current_user
else
render_api_error!(result[:message], result[:http_status])
@@ -317,9 +315,6 @@ module API
.execute
if result[:status] == :success
- log_release_updated_audit_event
- log_release_milestones_updated_audit_event if result[:milestones_updated]
-
present result[:release], with: Entities::Release, current_user: current_user
else
render_api_error!(result[:message], result[:http_status])
@@ -350,8 +345,6 @@ module API
.execute
if result[:status] == :success
- log_release_deleted_audit_event
-
present result[:release], with: Entities::Release, current_user: current_user
else
render_api_error!(result[:message], result[:http_status])
@@ -406,22 +399,6 @@ module API
Rack::Utils.parse_nested_query(@request.query_string)
end
- def log_release_created_audit_event(release)
- # extended in EE
- end
-
- def log_release_updated_audit_event
- # extended in EE
- end
-
- def log_release_deleted_audit_event
- # extended in EE
- end
-
- def log_release_milestones_updated_audit_event
- # extended in EE
- end
-
def release_cli?
request.env['HTTP_USER_AGENT']&.include?(RELEASE_CLI_USER_AGENT) == true
end
diff --git a/lib/api/resource_access_tokens.rb b/lib/api/resource_access_tokens.rb
index 1ad5bc8d421..752feb1455f 100644
--- a/lib/api/resource_access_tokens.rb
+++ b/lib/api/resource_access_tokens.rb
@@ -141,6 +141,10 @@ module API
params do
requires :id, type: String, desc: "The #{source_type} ID"
requires :token_id, type: String, desc: "The ID of the token"
+ optional :expires_at,
+ type: Date,
+ desc: "The expiration date of the token",
+ documentation: { example: '2021-01-31' }
end
post ':id/access_tokens/:token_id/rotate' do
resource = find_source(source_type, params[:id])
@@ -149,7 +153,7 @@ module API
token = find_token(resource, params[:token_id]) if resource_accessible
if token
- response = ::PersonalAccessTokens::RotateService.new(current_user, token).execute
+ response = ::PersonalAccessTokens::RotateService.new(current_user, token).execute(declared_params)
if response.success?
status :ok
diff --git a/lib/api/settings.rb b/lib/api/settings.rb
index 9120421fadf..7ad4ecd88b1 100644
--- a/lib/api/settings.rb
+++ b/lib/api/settings.rb
@@ -204,6 +204,7 @@ module API
optional :floc_enabled, type: Grape::API::Boolean, desc: 'Enable FloC (Federated Learning of Cohorts)'
optional :user_deactivation_emails_enabled, type: Boolean, desc: 'Send emails to users upon account deactivation'
optional :suggest_pipeline_enabled, type: Boolean, desc: 'Enable pipeline suggestion banner'
+ optional :enable_artifact_external_redirect_warning_page, type: Boolean, desc: 'Show the external redirect page that warns you about user-generated content in GitLab Pages'
optional :users_get_by_id_limit, type: Integer, desc: "Maximum number of calls to the /users/:id API per 10 minutes per user. Set to 0 for unlimited requests."
optional :runner_token_expiration_interval, type: Integer, desc: 'Token expiration interval for shared runners, in seconds'
optional :group_runner_token_expiration_interval, type: Integer, desc: 'Token expiration interval for group runners, in seconds'
diff --git a/lib/api/users.rb b/lib/api/users.rb
index dd9cb2ee019..5fa6d50581b 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -34,10 +34,14 @@ module API
helpers do
# rubocop: disable CodeReuse/ActiveRecord
def reorder_users(users)
- if params[:order_by] && params[:sort]
- users.reorder(order_options_with_tie_breaker)
- else
+ # Users#search orders by exact matches and handles pagination,
+ # so we should prioritize that.
+ if params[:search]
users
+ else
+ # Note that params[:order_by] and params[:sort] will always be present and
+ # default to "id" and "desc" as defined in `sort_params`.
+ users.reorder(order_options_with_tie_breaker)
end
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/lib/api/v3/github.rb b/lib/api/v3/github.rb
deleted file mode 100644
index 0ce5cdd06de..00000000000
--- a/lib/api/v3/github.rb
+++ /dev/null
@@ -1,289 +0,0 @@
-# frozen_string_literal: true
-
-# The endpoints by default return `404` in preparation for their removal
-# (also see comment above `#reversible_end_of_life!`).
-# https://gitlab.com/gitlab-org/gitlab/-/issues/362168
-#
-# These endpoints partially mimic Github API behavior in order to successfully
-# integrate with Jira Development Panel.
-module API
- module V3
- class Github < ::API::Base
- NO_SLASH_URL_PART_REGEX = %r{[^/]+}
- ENDPOINT_REQUIREMENTS = {
- namespace: NO_SLASH_URL_PART_REGEX,
- project: NO_SLASH_URL_PART_REGEX,
- username: NO_SLASH_URL_PART_REGEX
- }.freeze
-
- # Used to differentiate Jira Cloud requests from Jira Server requests
- # Jira Cloud user agent format: Jira DVCS Connector Vertigo/version
- # Jira Server user agent format: Jira DVCS Connector/version
- JIRA_DVCS_CLOUD_USER_AGENT = 'Jira DVCS Connector Vertigo'
-
- GITALY_TIMEOUT_CACHE_KEY = 'api:v3:Gitaly-timeout-cache-key'
- GITALY_TIMEOUT_CACHE_EXPIRY = 1.day
-
- include PaginationParams
-
- feature_category :integrations
-
- before do
- authorize_jira_user_agent!(request)
- authenticate!
- reversible_end_of_life!
- end
-
- helpers do
- params :project_full_path do
- requires :namespace, type: String
- requires :project, type: String
- end
-
- # The endpoints in this class have been deprecated since 15.1.
- #
- # Due to uncertainty about the impact of a full removal in 16.0, all endpoints return `404`
- # by default but we allow customers to toggle a flag to reverse this breaking change.
- # See https://gitlab.com/gitlab-org/gitlab/-/issues/362168#note_1347692683.
- #
- # TODO Make the breaking change irreversible https://gitlab.com/gitlab-org/gitlab/-/issues/408148.
- def reversible_end_of_life!
- not_found! unless Feature.enabled?(:jira_dvcs_end_of_life_amnesty)
-
- Gitlab::IntegrationsLogger.info(
- user_id: current_user&.id,
- namespace: params[:namespace],
- project: params[:project],
- message: 'Deprecated Jira DVCS endpoint request'
- )
- end
-
- def authorize_jira_user_agent!(request)
- not_found! unless Gitlab::Jira::Middleware.jira_dvcs_connector?(request.env)
- end
-
- def update_project_feature_usage_for(project)
- # Prevent errors on GitLab Geo not allowing
- # UPDATE statements to happen in GET requests.
- return if Gitlab::Database.read_only?
-
- project.log_jira_dvcs_integration_usage(cloud: jira_cloud?)
- end
-
- def jira_cloud?
- request.env['HTTP_USER_AGENT'].include?(JIRA_DVCS_CLOUD_USER_AGENT)
- end
-
- def find_project_with_access(params)
- project = find_project!(
- ::Gitlab::Jira::Dvcs.restore_full_path(**params.slice(:namespace, :project).symbolize_keys)
- )
- not_found! unless can?(current_user, :read_code, project)
- project
- end
-
- # rubocop: disable CodeReuse/ActiveRecord
- def find_merge_requests
- merge_requests = authorized_merge_requests.reorder(updated_at: :desc)
- paginate(merge_requests)
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
- # rubocop: disable CodeReuse/ActiveRecord
- def find_merge_request_with_access(id, access_level = :read_merge_request)
- merge_request = authorized_merge_requests.find_by(id: id)
- not_found! unless can?(current_user, access_level, merge_request)
- merge_request
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
- def authorized_merge_requests
- MergeRequestsFinder.new(current_user, authorized_only: !current_user.can_read_all_resources?)
- .execute.with_jira_integration_associations
- end
-
- def authorized_merge_requests_for_project(project)
- MergeRequestsFinder
- .new(current_user, authorized_only: !current_user.can_read_all_resources?, project_id: project.id)
- .execute.with_jira_integration_associations
- end
-
- # rubocop: disable CodeReuse/ActiveRecord
- def find_notes(noteable)
- # They're not presented on Jira Dev Panel ATM. A comments count with a
- # redirect link is presented.
- notes = paginate(noteable.notes.user.reorder(nil))
- notes.select { |n| n.readable_by?(current_user) }
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
- # Returns an empty Array instead of the Commit diff files for a period
- # of time after a Gitaly timeout, to mitigate frequent Gitaly timeouts
- # for some Commit diffs.
- def diff_files(commit)
- cache_key = [
- GITALY_TIMEOUT_CACHE_KEY,
- commit.project.id,
- commit.cache_key
- ].join(':')
-
- return [] if Rails.cache.read(cache_key).present?
-
- begin
- commit.diffs.diff_files
- rescue GRPC::DeadlineExceeded => error
- # Gitaly fails to load diffs consistently for some commits. The other information
- # is still valuable for Jira. So we skip the loading and respond with a 200 excluding diffs
- # Remove this when https://gitlab.com/gitlab-org/gitaly/-/issues/3741 is fixed.
- Rails.cache.write(cache_key, 1, expires_in: GITALY_TIMEOUT_CACHE_EXPIRY)
- Gitlab::ErrorTracking.track_exception(error)
- []
- end
- end
- end
-
- resource :orgs do
- get ':namespace/repos' do
- present []
- end
- end
-
- resource :user do
- get :repos do
- present []
- end
- end
-
- resource :users do
- params do
- use :pagination
- end
-
- get ':namespace/repos' do
- namespace = Namespace.find_by_full_path(params[:namespace])
- not_found!('Namespace') unless namespace
-
- projects = current_user.can_read_all_resources? ? Project.all : current_user.authorized_projects
- projects = projects.in_namespace(namespace.self_and_descendants)
-
- projects_cte = Project.wrap_with_cte(projects)
- .eager_load_namespace_and_owner
- .with_route
-
- present paginate(projects_cte),
- with: ::API::Github::Entities::Repository,
- root_namespace: namespace.root_ancestor
- end
-
- get ':username' do
- forbidden! unless can?(current_user, :read_users_list)
- user = UsersFinder.new(current_user, { username: params[:username] }).execute.first
- not_found! unless user
- present user, with: ::API::Github::Entities::User
- end
- end
-
- # Jira dev panel integration weirdly requests for "/-/jira/pulls" instead
- # "/api/v3/repos/<namespace>/<project>/pulls". This forces us into
- # returning _all_ Merge Requests from authorized projects (user is a member),
- # instead just the authorized MRs from a project.
- # Jira handles the filtering, presenting just MRs mentioning the Jira
- # issue ID on the MR title / description.
- resource :repos do
- # Keeping for backwards compatibility with old Jira integration instructions
- # so that users that do not change it will not suddenly have a broken integration
- get '/-/jira/pulls' do
- present find_merge_requests, with: ::API::Github::Entities::PullRequest
- end
-
- get '/-/jira/events' do
- present []
- end
-
- params do
- use :project_full_path
- end
- # TODO Remove the custom Apdex SLO target `urgency: :low` when this endpoint has been optimised.
- # https://gitlab.com/gitlab-org/gitlab/-/issues/337269
- get ':namespace/:project/pulls', urgency: :low do
- user_project = find_project_with_access(params)
-
- merge_requests = authorized_merge_requests_for_project(user_project)
-
- present paginate(merge_requests), with: ::API::Github::Entities::PullRequest
- end
-
- params do
- use :project_full_path
- end
- get ':namespace/:project/pulls/:id' do
- merge_request = find_merge_request_with_access(params[:id])
-
- present merge_request, with: ::API::Github::Entities::PullRequest
- end
-
- # In Github, each Merge Request is automatically also an issue.
- # Therefore we return its comments here.
- # It'll present _just_ the comments counting with a link to GitLab on
- # Jira dev panel, not the actual note content.
- get ':namespace/:project/issues/:id/comments' do
- merge_request = find_merge_request_with_access(params[:id])
-
- present find_notes(merge_request), with: ::API::Github::Entities::NoteableComment
- end
-
- # This refer to "review" comments but Jira dev panel doesn't seem to
- # present it accordingly.
- get ':namespace/:project/pulls/:id/comments' do
- present []
- end
-
- # Commits are not presented within "Pull Requests" modal on Jira dev
- # panel.
- get ':namespace/:project/pulls/:id/commits' do
- present []
- end
-
- # Self-hosted Jira (tested on 7.11.1) requests this endpoint right
- # after fetching branches.
- get ':namespace/:project/events' do
- user_project = find_project_with_access(params)
-
- merge_requests = authorized_merge_requests_for_project(user_project)
-
- present paginate(merge_requests), with: ::API::Github::Entities::PullRequestEvent
- end
-
- params do
- use :project_full_path
- use :pagination
- end
- # TODO Remove the custom Apdex SLO target `urgency: :low` when this endpoint has been optimised.
- # https://gitlab.com/gitlab-org/gitlab/-/issues/337268
- get ':namespace/:project/branches', urgency: :low do
- user_project = find_project_with_access(params)
-
- update_project_feature_usage_for(user_project)
-
- next [] unless user_project.repo_exists?
-
- branches = ::Kaminari.paginate_array(user_project.repository.branches.sort_by(&:name))
-
- present paginate(branches), with: ::API::Github::Entities::Branch, project: user_project
- end
-
- params do
- use :project_full_path
- end
- get ':namespace/:project/commits/:sha' do
- user_project = find_project_with_access(params)
-
- commit = user_project.commit(params[:sha])
- not_found! 'Commit' unless commit
-
- present commit, with: ::API::Github::Entities::RepoCommit, diff_files: diff_files(commit)
- end
- end
- end
- end
-end
diff --git a/lib/api/vs_code/settings/entities/vs_code_setting_reference.rb b/lib/api/vs_code/settings/entities/vs_code_setting_reference.rb
new file mode 100644
index 00000000000..38af85dc0c7
--- /dev/null
+++ b/lib/api/vs_code/settings/entities/vs_code_setting_reference.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module API
+ module VsCode
+ module Settings
+ module Entities
+ class VsCodeSettingReference < Grape::Entity
+ include ::API::Helpers::RelatedResourcesHelpers
+
+ expose :url do |setting|
+ expose_path(api_v4_vscode_settings_sync_v1_resource_path(
+ resource_name: setting[:setting_type],
+ id: setting[:uuid]
+ ))
+ end
+ expose :created do |setting|
+ setting[:updated_at]&.to_i
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/vs_code/settings/vs_code_settings_sync.rb b/lib/api/vs_code/settings/vs_code_settings_sync.rb
index dc22496e380..1e53125a3aa 100644
--- a/lib/api/vs_code/settings/vs_code_settings_sync.rb
+++ b/lib/api/vs_code/settings/vs_code_settings_sync.rb
@@ -8,6 +8,14 @@ module API
feature_category :web_ide
+ helpers do
+ def find_settings
+ return [DEFAULT_MACHINE] if params[:resource_name] == DEFAULT_MACHINE[:setting_type]
+
+ SettingsFinder.new(current_user, [params[:resource_name]]).execute
+ end
+ end
+
before do
authenticate!
@@ -21,6 +29,9 @@ module API
desc 'Get the settings manifest for Settings Sync' do
success [Entities::VsCodeManifest]
+ failure [
+ { code: 401, message: '401 Unauthorized' }
+ ]
tags %w[vscode]
end
get '/v1/manifest' do
@@ -31,44 +42,71 @@ module API
end
desc 'Get a specific setting resource' do
- success [Entities::VsCodeSetting]
+ success [
+ Entities::VsCodeSetting,
+ { code: 204, message: 'No content' }
+ ]
+ failure [
+ { code: 400, message: '400 bad request' },
+ { code: 401, message: '401 Unauthorized' }
+ ]
tags %w[vscode]
end
params do
- requires :resource_name, type: String, desc: 'Name of the resource such as settings'
+ requires :resource_name, type: String, desc: 'Name of the resource such as settings',
+ values: SETTINGS_TYPES
requires :id, type: String, desc: 'ID of the resource to retrieve'
end
get '/v1/resource/:resource_name/:id' do
- authenticate!
-
- setting_name = params[:resource_name]
- setting = nil
+ settings = find_settings
- if params[:resource_name] == 'machines'
- setting = DEFAULT_MACHINE
- else
- settings = SettingsFinder.new(current_user, [setting_name]).execute
- setting = settings.first if settings.present?
- end
-
- if setting.nil?
+ if settings.blank?
status :no_content
header :etag, NO_CONTENT_ETAG
body false
else
+ # This endpoint does not use the :id parameter
+ # because the first iteration of this API only
+ # supports storing a single record of a given setting_type.
+ # We can rely on obtaining the first record of the setting
+ # result.
+ setting = settings.first
header :etag, setting[:uuid]
presenter = VsCodeSettingPresenter.new setting
present presenter, with: Entities::VsCodeSetting
end
end
- desc 'Update a specific setting'
+ desc 'Get a list of references to one or more vscode setting resources' do
+ success [Entities::VsCodeSettingReference]
+ failure [
+ { code: 400, message: '400 bad request' },
+ { code: 401, message: '401 Unauthorized' }
+ ]
+ tags %w[vscode]
+ end
params do
- requires :resource_name, type: String, desc: 'Name of the resource such as settings'
+ requires :resource_name, type: String, desc: 'Name of the resource such as settings',
+ values: SETTINGS_TYPES
end
- post '/v1/resource/:resource_name' do
- authenticate!
+ get '/v1/resource/:resource_name' do
+ settings = find_settings
+ present settings, with: Entities::VsCodeSettingReference
+ end
+
+ desc 'Creates or updates a specific setting' do
+ success [{ code: 200, message: 'OK' }]
+ failure [
+ { code: 400, message: 'Bad request' },
+ { code: 401, message: '401 Unauthorized' }
+ ]
+ end
+ params do
+ requires :resource_name, type: String, desc: 'Name of the resource such as settings',
+ values: SETTINGS_TYPES
+ end
+ post '/v1/resource/:resource_name' do
response = CreateOrUpdateService.new(current_user: current_user, params: {
content: params[:content],
version: params[:version],
@@ -83,6 +121,19 @@ module API
error!(response.message, 400)
end
end
+
+ desc 'Deletes all user vscode setting resources' do
+ success [{ code: 200, message: 'OK' }]
+ failure [
+ { code: 401, message: '401 Unauthorized' }
+ ]
+ tags %w[vscode]
+ end
+ delete '/v1/collection' do
+ DeleteService.new(current_user: current_user).execute
+
+ present "OK"
+ end
end
end
end
diff --git a/lib/api/wikis.rb b/lib/api/wikis.rb
index 2058f5de706..a7408512102 100644
--- a/lib/api/wikis.rb
+++ b/lib/api/wikis.rb
@@ -85,6 +85,9 @@ module API
end
params do
requires :title, type: String, desc: 'Title of a wiki page'
+ optional :front_matter, type: Hash do
+ optional :title, type: String, desc: 'Front matter title of a wiki page'
+ end
requires :content, type: String, desc: 'Content of a wiki page'
use :common_wiki_page_params
end
@@ -112,6 +115,9 @@ module API
end
params do
optional :title, type: String, desc: 'Title of a wiki page'
+ optional :front_matter, type: Hash do
+ optional :title, type: String, desc: 'Front matter title of a wiki page'
+ end
optional :content, type: String, desc: 'Content of a wiki page'
use :common_wiki_page_params
at_least_one_of :content, :title, :format
diff --git a/lib/atlassian/jira_connect/jira_user.rb b/lib/atlassian/jira_connect/jira_user.rb
index 57ceb8fdf13..051165474af 100644
--- a/lib/atlassian/jira_connect/jira_user.rb
+++ b/lib/atlassian/jira_connect/jira_user.rb
@@ -3,15 +3,17 @@
module Atlassian
module JiraConnect
class JiraUser
+ ADMIN_GROUPS = %w[site-admins org-admins].freeze
+
def initialize(data)
@data = data
end
- def site_admin?
+ def jira_admin?
groups = @data.dig('groups', 'items')
return false unless groups
- groups.any? { |g| g['name'] == 'site-admins' }
+ groups.any? { |group| ADMIN_GROUPS.include?(group['name']) }
end
end
end
diff --git a/lib/backup/gitaly_backup.rb b/lib/backup/gitaly_backup.rb
index 5b55c2cbdf7..366151a63b4 100644
--- a/lib/backup/gitaly_backup.rb
+++ b/lib/backup/gitaly_backup.rb
@@ -92,7 +92,7 @@ module Backup
args += ['-id', backup_id] if backup_id
when :restore
args += ['-remove-all-repositories', remove_all_repositories.join(',')] if remove_all_repositories
- args += ['-id', backup_id] if backup_id && server_side?
+ args += ['-id', backup_id] if backup_id
end
args
diff --git a/lib/banzai/filter/asset_proxy_filter.rb b/lib/banzai/filter/asset_proxy_filter.rb
index 512c55381ec..eae69700465 100644
--- a/lib/banzai/filter/asset_proxy_filter.rb
+++ b/lib/banzai/filter/asset_proxy_filter.rb
@@ -23,7 +23,8 @@ module Banzai
begin
uri = URI.parse(original_src)
- next if uri.host.nil? && !original_src.start_with?('///')
+ # Skip URLs like `/path.ext` or `path.ext` which are relative to the current host
+ next if uri.relative? && uri.host.nil? && original_src.match(%r{\A/*})[0].length < 2
next if asset_host_allowed?(uri.host)
rescue StandardError
# Ignored
diff --git a/lib/banzai/filter/math_filter.rb b/lib/banzai/filter/math_filter.rb
index 3161e030194..511da4b6ba5 100644
--- a/lib/banzai/filter/math_filter.rb
+++ b/lib/banzai/filter/math_filter.rb
@@ -93,10 +93,20 @@ module Banzai
end
def render_nodes_limit_reached?(count)
+ return false if wiki?
+ return false if blob?
return false unless settings.math_rendering_limits_enabled?
count >= RENDER_NODES_LIMIT
end
+
+ def wiki?
+ context[:wiki].present?
+ end
+
+ def blob?
+ context[:text_source] == :blob
+ end
end
end
end
diff --git a/lib/banzai/filter/references/user_reference_filter.rb b/lib/banzai/filter/references/user_reference_filter.rb
index a3784004087..3fcb36c4714 100644
--- a/lib/banzai/filter/references/user_reference_filter.rb
+++ b/lib/banzai/filter/references/user_reference_filter.rb
@@ -65,13 +65,10 @@ module Banzai
# The keys of this Hash are the namespace paths, the values the
# corresponding Namespace objects.
def namespaces
- cross_join_issue = "https://gitlab.com/gitlab-org/gitlab/-/issues/417466"
- Gitlab::Database.allow_cross_joins_across_databases(url: cross_join_issue) do
- @namespaces ||= Namespace.eager_load(:owner, :route)
- .where_full_path_in(usernames)
- .index_by(&:full_path)
- .transform_keys(&:downcase)
- end
+ @namespaces ||= Namespace.preload(:owner, :route)
+ .where_full_path_in(usernames)
+ .index_by(&:full_path)
+ .transform_keys(&:downcase)
end
# Returns all usernames referenced in the current document.
diff --git a/lib/banzai/reference_parser/user_parser.rb b/lib/banzai/reference_parser/user_parser.rb
index ec96181e7f1..bba5a7dfd09 100644
--- a/lib/banzai/reference_parser/user_parser.rb
+++ b/lib/banzai/reference_parser/user_parser.rb
@@ -6,9 +6,9 @@ module Banzai
self.reference_type = :user
def referenced_by(nodes, options = {})
- group_ids = []
- user_ids = []
- project_ids = []
+ group_ids = Set.new
+ user_ids = Set.new
+ project_ids = Set.new
nodes.each do |node|
if node.has_attribute?('data-group')
@@ -20,8 +20,10 @@ module Banzai
end
end
- find_users_for_groups(group_ids) | find_users(user_ids) |
- find_users_for_projects(project_ids)
+ user_ids += find_user_ids_for_groups(group_ids)
+ user_ids += find_user_ids_for_projects(project_ids)
+
+ find_users(user_ids)
end
def nodes_visible_to_user(user, nodes)
@@ -49,20 +51,6 @@ module Banzai
visible + super(current_user, remaining)
end
- # Check if project belongs to a group which
- # user can read.
- def can_read_group_reference?(node, user, groups)
- node_group = groups[node]
-
- node_group && can?(user, :read_group, node_group)
- end
-
- def can_read_project_reference?(node)
- node_id = node.attr('data-project').to_i
-
- project_for_node(node)&.id == node_id
- end
-
def nodes_user_can_reference(current_user, nodes)
project_attr = 'data-project'
author_attr = 'data-author'
@@ -88,28 +76,44 @@ module Banzai
end
end
+ private
+
+ # Check if project belongs to a group which
+ # user can read.
+ def can_read_group_reference?(node, user, groups)
+ node_group = groups[node]
+
+ node_group && can?(user, :read_group, node_group)
+ end
+
+ def can_read_project_reference?(node)
+ node_id = node.attr('data-project').to_i
+
+ project_for_node(node)&.id == node_id
+ end
+
def find_users(ids)
return [] if ids.empty?
collection_objects_for_ids(User, ids)
end
- def find_users_for_groups(ids)
- return [] if ids.empty?
+ def find_user_ids_for_groups(group_ids)
+ return [] if group_ids.empty?
- cross_join_issue = "https://gitlab.com/gitlab-org/gitlab/-/issues/417466"
- ::Gitlab::Database.allow_cross_joins_across_databases(url: cross_join_issue) do
- User.joins(:group_members).where(members: {
- source_id: Namespace.where(id: ids).where('mentions_disabled IS NOT TRUE').select(:id)
- }).to_a
- end
+ GroupMember
+ .of_groups(Group.id_in(group_ids).where('mentions_disabled IS NOT TRUE'))
+ .non_request
+ .non_invite
+ .non_minimal_access
+ .distinct
+ .pluck(:user_id)
end
- def find_users_for_projects(ids)
- return [] if ids.empty?
+ def find_user_ids_for_projects(project_ids)
+ return [] if project_ids.empty?
- collection_objects_for_ids(Project, ids)
- .flat_map { |p| p.team.members.to_a }
+ ProjectAuthorization.for_project(project_ids).pluck(:user_id)
end
def can_read_reference?(user, ref_project, node)
diff --git a/lib/bitbucket/representation/pull_request_comment.rb b/lib/bitbucket/representation/pull_request_comment.rb
index 34dbf9ad22d..11ce0c26677 100644
--- a/lib/bitbucket/representation/pull_request_comment.rb
+++ b/lib/bitbucket/representation/pull_request_comment.rb
@@ -31,6 +31,10 @@ module Bitbucket
raw.key?('parent')
end
+ def deleted?
+ raw.fetch('deleted', false)
+ end
+
private
def inline
diff --git a/lib/bitbucket/representation/repo.rb b/lib/bitbucket/representation/repo.rb
index 8d5b15e299a..3764d116a36 100644
--- a/lib/bitbucket/representation/repo.rb
+++ b/lib/bitbucket/representation/repo.rb
@@ -59,6 +59,10 @@ module Bitbucket
end
end
+ def default_branch
+ raw.dig('mainbranch', 'name')
+ end
+
def to_s
full_name
end
diff --git a/lib/bulk_imports/clients/graphql.rb b/lib/bulk_imports/clients/graphql.rb
index 94bbdfaa681..3055c8d24ce 100644
--- a/lib/bulk_imports/clients/graphql.rb
+++ b/lib/bulk_imports/clients/graphql.rb
@@ -4,11 +4,14 @@ module BulkImports
module Clients
class Graphql
class HTTP < Graphlient::Adapters::HTTP::Adapter
+ REQUEST_TIMEOUT = 60
+
def execute(document:, operation_name: nil, variables: {}, context: {})
response = ::Gitlab::HTTP.post(
url,
headers: headers,
follow_redirects: false,
+ timeout: REQUEST_TIMEOUT,
body: {
query: document.to_query_string,
operationName: operation_name,
diff --git a/lib/bulk_imports/common/pipelines/entity_finisher.rb b/lib/bulk_imports/common/pipelines/entity_finisher.rb
index fa09f36fdd6..723359aa438 100644
--- a/lib/bulk_imports/common/pipelines/entity_finisher.rb
+++ b/lib/bulk_imports/common/pipelines/entity_finisher.rb
@@ -30,8 +30,7 @@ module BulkImports
source_full_path: entity.source_full_path,
pipeline_class: self.class.name,
message: "Entity #{entity.status_name}",
- source_version: entity.bulk_import.source_version_info.to_s,
- importer: 'gitlab_migration'
+ source_version: entity.bulk_import.source_version_info.to_s
)
::BulkImports::FinishProjectImportWorker.perform_async(entity.project_id) if entity.project?
@@ -42,7 +41,7 @@ module BulkImports
attr_reader :context, :entity, :trackers
def logger
- @logger ||= Gitlab::Import::Logger.build
+ @logger ||= Logger.build
end
def all_other_trackers_failed?
diff --git a/lib/bulk_imports/logger.rb b/lib/bulk_imports/logger.rb
new file mode 100644
index 00000000000..be15c050770
--- /dev/null
+++ b/lib/bulk_imports/logger.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module BulkImports
+ class Logger < ::Gitlab::Import::Logger
+ IMPORTER_NAME = 'gitlab_migration'
+
+ def default_attributes
+ super.merge(importer: IMPORTER_NAME)
+ end
+ end
+end
diff --git a/lib/bulk_imports/ndjson_pipeline.rb b/lib/bulk_imports/ndjson_pipeline.rb
index 89ae66938af..07118c3b55c 100644
--- a/lib/bulk_imports/ndjson_pipeline.rb
+++ b/lib/bulk_imports/ndjson_pipeline.rb
@@ -135,7 +135,7 @@ module BulkImports
bulk_import_entity_id: tracker.entity.id,
pipeline_class: tracker.pipeline_name,
exception_class: 'RecordInvalid',
- exception_message: record.errors.full_messages.to_sentence.truncate(255),
+ exception_message: record.errors.full_messages.to_sentence,
correlation_id_value: Labkit::Correlation::CorrelationId.current_or_new_id
)
end
diff --git a/lib/bulk_imports/pipeline/runner.rb b/lib/bulk_imports/pipeline/runner.rb
index 666916f8758..e2a14c35e79 100644
--- a/lib/bulk_imports/pipeline/runner.rb
+++ b/lib/bulk_imports/pipeline/runner.rb
@@ -17,7 +17,7 @@ module BulkImports
if extracted_data
extracted_data.each_with_index do |entry, index|
raw_entry = entry.dup
- next if Feature.enabled?(:bulk_import_idempotent_workers) && already_processed?(raw_entry, index)
+ next if already_processed?(raw_entry, index)
transformers.each do |transformer|
entry = run_pipeline_step(:transformer, transformer.class.name) do
@@ -25,11 +25,11 @@ module BulkImports
end
end
- run_pipeline_step(:loader, loader.class.name) do
+ run_pipeline_step(:loader, loader.class.name, entry) do
loader.load(context, entry)
end
- save_processed_entry(raw_entry, index) if Feature.enabled?(:bulk_import_idempotent_workers)
+ save_processed_entry(raw_entry, index)
end
tracker.update!(
@@ -40,6 +40,14 @@ module BulkImports
run_pipeline_step(:after_run) do
after_run(extracted_data)
end
+
+ # For batches, `#on_finish` is called once within `FinishBatchedPipelineWorker`
+ # after all batches have completed.
+ unless tracker.batched?
+ run_pipeline_step(:on_finish) do
+ on_finish
+ end
+ end
end
info(message: 'Pipeline finished')
@@ -47,9 +55,11 @@ module BulkImports
skip!('Skipping pipeline due to failed entity')
end
+ def on_finish; end
+
private # rubocop:disable Lint/UselessAccessModifier
- def run_pipeline_step(step, class_name = nil)
+ def run_pipeline_step(step, class_name = nil, entry = nil)
raise MarkedAsFailedError if context.entity.failed?
info(pipeline_step: step, step_class: class_name)
@@ -65,11 +75,11 @@ module BulkImports
rescue BulkImports::NetworkError => e
raise BulkImports::RetryPipelineError.new(e.message, e.retry_delay) if e.retriable?(context.tracker)
- log_and_fail(e, step)
+ log_and_fail(e, step, entry)
rescue BulkImports::RetryPipelineError
raise
rescue StandardError => e
- log_and_fail(e, step)
+ log_and_fail(e, step, entry)
end
def extracted_data_from
@@ -95,8 +105,8 @@ module BulkImports
run if extracted_data.has_next_page?
end
- def log_and_fail(exception, step)
- log_import_failure(exception, step)
+ def log_and_fail(exception, step, entry = nil)
+ log_import_failure(exception, step, entry)
if abort_on_failure?
tracker.fail_op!
@@ -114,16 +124,21 @@ module BulkImports
tracker.skip!
end
- def log_import_failure(exception, step)
+ def log_import_failure(exception, step, entry)
failure_attributes = {
bulk_import_entity_id: context.entity.id,
pipeline_class: pipeline,
pipeline_step: step,
exception_class: exception.class.to_s,
- exception_message: exception.message.truncate(255),
+ exception_message: exception.message,
correlation_id_value: Labkit::Correlation::CorrelationId.current_or_new_id
}
+ if entry
+ failure_attributes[:source_url] = BulkImports::SourceUrlBuilder.new(context, entry).url
+ failure_attributes[:source_title] = entry.try(:title) || entry.try(:name)
+ end
+
log_exception(
exception,
log_params(
@@ -154,8 +169,7 @@ module BulkImports
source_full_path: context.entity.source_full_path,
pipeline_class: pipeline,
context_extra: context.extra,
- source_version: context.entity.bulk_import.source_version_info.to_s,
- importer: 'gitlab_migration'
+ source_version: context.entity.bulk_import.source_version_info.to_s
}
defaults
@@ -164,7 +178,7 @@ module BulkImports
end
def logger
- @logger ||= Gitlab::Import::Logger.build
+ @logger ||= Logger.build
end
def log_exception(exception, payload)
diff --git a/lib/bulk_imports/pipeline_schema_info.rb b/lib/bulk_imports/pipeline_schema_info.rb
new file mode 100644
index 00000000000..df35a3569d6
--- /dev/null
+++ b/lib/bulk_imports/pipeline_schema_info.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module BulkImports
+ class PipelineSchemaInfo
+ def initialize(pipeline_class, portable_class)
+ @pipeline_class = pipeline_class
+ @portable_class = portable_class
+ end
+
+ def db_schema
+ return unless relation
+ return unless association
+
+ Gitlab::Database::GitlabSchema.tables_to_schema[association.table_name]
+ end
+
+ def db_table
+ return unless relation
+ return unless association
+
+ association.table_name
+ end
+
+ private
+
+ attr_reader :pipeline_class, :portable_class
+
+ def relation
+ @relation ||= pipeline_class.try(:relation)
+ end
+
+ def association
+ @association ||= portable_class.reflect_on_association(relation)
+ end
+ end
+end
diff --git a/lib/bulk_imports/projects/pipelines/merge_requests_pipeline.rb b/lib/bulk_imports/projects/pipelines/merge_requests_pipeline.rb
index 264bda6e654..fe5c61e81a3 100644
--- a/lib/bulk_imports/projects/pipelines/merge_requests_pipeline.rb
+++ b/lib/bulk_imports/projects/pipelines/merge_requests_pipeline.rb
@@ -10,8 +10,8 @@ module BulkImports
extractor ::BulkImports::Common::Extractors::NdjsonExtractor, relation: relation
- def after_run(_)
- context.portable.merge_requests.set_latest_merge_request_diff_ids!
+ def on_finish
+ ::Projects::ImportExport::AfterImportMergeRequestsWorker.perform_async(context.portable.id)
end
end
end
diff --git a/lib/bulk_imports/projects/pipelines/releases_pipeline.rb b/lib/bulk_imports/projects/pipelines/releases_pipeline.rb
index c77e53b9aec..433419f4c5c 100644
--- a/lib/bulk_imports/projects/pipelines/releases_pipeline.rb
+++ b/lib/bulk_imports/projects/pipelines/releases_pipeline.rb
@@ -10,9 +10,7 @@ module BulkImports
extractor ::BulkImports::Common::Extractors::NdjsonExtractor, relation: relation
- def after_run(_context)
- super
-
+ def on_finish
portable.releases.find_each do |release|
create_release_evidence(release)
end
diff --git a/lib/bulk_imports/source_url_builder.rb b/lib/bulk_imports/source_url_builder.rb
new file mode 100644
index 00000000000..875b2eae9f7
--- /dev/null
+++ b/lib/bulk_imports/source_url_builder.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+module BulkImports
+ class SourceUrlBuilder
+ ALLOWED_RELATIONS = %w[
+ issues
+ merge_requests
+ epics
+ milestones
+ ].freeze
+
+ attr_reader :context, :entity, :entry
+
+ # @param [BulkImports::Pipeline::Context] context
+ # @param [ApplicationRecord] entry
+ def initialize(context, entry)
+ @context = context
+ @entity = context.entity
+ @entry = entry
+ end
+
+ # Builds a source URL for the given entry if iid is present
+ def url
+ return unless entry.is_a?(ApplicationRecord)
+ return unless iid
+ return unless ALLOWED_RELATIONS.include?(relation)
+
+ File.join(source_instance_url, group_prefix, source_full_path, '-', relation, iid.to_s)
+ end
+
+ private
+
+ def iid
+ @iid ||= entry.try(:iid)
+ end
+
+ def relation
+ @relation ||= context.tracker.pipeline_class.relation
+ end
+
+ def source_instance_url
+ @source_instance_url ||= context.bulk_import.configuration.url
+ end
+
+ def source_full_path
+ @source_full_path ||= entity.source_full_path
+ end
+
+ # Group milestone (or epic) url is /groups/:group_path/-/milestones/:iid
+ # Project milestone url is /:project_path/-/milestones/:iid
+ def group_prefix
+ return '' if entity.project?
+
+ entity.pluralized_name
+ end
+ end
+end
diff --git a/lib/click_house/migration.rb b/lib/click_house/migration.rb
new file mode 100644
index 00000000000..410a7ec86bc
--- /dev/null
+++ b/lib/click_house/migration.rb
@@ -0,0 +1,89 @@
+# frozen_string_literal: true
+
+module ClickHouse
+ class Migration
+ cattr_accessor :verbose, :client_configuration
+ attr_accessor :name, :version
+
+ class << self
+ attr_accessor :delegate
+ end
+
+ def initialize(name = self.class.name, version = nil)
+ @name = name
+ @version = version
+ end
+
+ self.client_configuration = ClickHouse::Client.configuration
+ self.verbose = true
+ # instantiate the delegate object after initialize is defined
+ self.delegate = new
+
+ MIGRATION_FILENAME_REGEXP = /\A([0-9]+)_([_a-z0-9]*)\.?([_a-z0-9]*)?\.rb\z/
+
+ def database
+ self.class.constants.include?(:SCHEMA) ? self.class.const_get(:SCHEMA, false) : :main
+ end
+
+ def execute(query)
+ ClickHouse::Client.execute(query, database, self.class.client_configuration)
+ end
+
+ def up
+ self.class.delegate = self
+
+ return unless self.class.respond_to?(:up)
+
+ self.class.up
+ end
+
+ def down
+ self.class.delegate = self
+
+ return unless self.class.respond_to?(:down)
+
+ self.class.down
+ end
+
+ # Execute this migration in the named direction
+ def migrate(direction)
+ return unless respond_to?(direction)
+
+ case direction
+ when :up then announce 'migrating'
+ when :down then announce 'reverting'
+ end
+
+ time = Benchmark.measure do
+ exec_migration(direction)
+ end
+
+ case direction
+ when :up then announce format("migrated (%.4fs)", time.real)
+ write
+ when :down then announce format("reverted (%.4fs)", time.real)
+ write
+ end
+ end
+
+ private
+
+ def exec_migration(direction)
+ # noinspection RubyCaseWithoutElseBlockInspection
+ case direction
+ when :up then up
+ when :down then down
+ end
+ end
+
+ def write(text = '')
+ $stdout.puts(text) if verbose
+ end
+
+ def announce(message)
+ text = "#{version} #{name}: #{message}"
+ length = [0, 75 - text.length].max
+ write format('== %s %s', text, '=' * length)
+ end
+ end
+end
diff --git a/lib/click_house/migration_support/migration_context.rb b/lib/click_house/migration_support/migration_context.rb
new file mode 100644
index 00000000000..6e4dd2a97c2
--- /dev/null
+++ b/lib/click_house/migration_support/migration_context.rb
@@ -0,0 +1,94 @@
+# frozen_string_literal: true
+
+module ClickHouse
+ module MigrationSupport
+ # MigrationContext sets the context in which a migration is run.
+ #
+ # A migration context requires the path to the migrations is set
+ # in the +migrations_paths+ parameter. Optionally a +schema_migration+
+ # class can be provided. For most applications, +SchemaMigration+ is
+ # sufficient. Multiple database applications need a +SchemaMigration+
+ # per primary database.
+ class MigrationContext
+ attr_reader :migrations_paths, :schema_migration
+
+ def initialize(migrations_paths, schema_migration)
+ @migrations_paths = migrations_paths
+ @schema_migration = schema_migration
+ end
+
+ def up(target_version = nil, &block)
+ selected_migrations = block ? migrations.select(&block) : migrations
+
+ migrate(:up, selected_migrations, target_version)
+ end
+
+ def down(target_version = nil, &block)
+ selected_migrations = block ? migrations.select(&block) : migrations
+
+ migrate(:down, selected_migrations, target_version)
+ end
+
+ private
+
+ def migrate(direction, selected_migrations, target_version = nil)
+ ClickHouse::MigrationSupport::Migrator.new(
+ direction,
+ selected_migrations,
+ schema_migration,
+ target_version
+ ).migrate
+ end
+
+ def migrations
+ migrations = migration_files.map do |file|
+ version, name, scope = parse_migration_filename(file)
+
+ raise ClickHouse::MigrationSupport::IllegalMigrationNameError, file unless version
+
+ version = version.to_i
+ name = name.camelize
+
+ MigrationProxy.new(name, version, file, scope)
+ end
+
+ migrations.sort_by(&:version)
+ end
+
+ def migration_files
+ paths = Array(migrations_paths)
+ Dir[*paths.flat_map { |path| "#{path}/**/[0-9]*_*.rb" }]
+ end
+
+ def parse_migration_filename(filename)
+ File.basename(filename).scan(ClickHouse::Migration::MIGRATION_FILENAME_REGEXP).first
+ end
+ end
+
+ # MigrationProxy is used to defer loading of the actual migration classes
+ # until they are needed
+ MigrationProxy = Struct.new(:name, :version, :filename, :scope) do
+ def initialize(name, version, filename, scope)
+ super
+ @migration = nil
+ end
+
+ def basename
+ File.basename(filename)
+ end
+
+ delegate :migrate, :announce, :write, :database, to: :migration
+
+ private
+
+ def migration
+ @migration ||= load_migration
+ end
+
+ def load_migration
+ require(File.expand_path(filename))
+ name.constantize.new(name, version)
+ end
+ end
+ end
+end
diff --git a/lib/click_house/migration_support/migration_error.rb b/lib/click_house/migration_support/migration_error.rb
new file mode 100644
index 00000000000..0638d487e37
--- /dev/null
+++ b/lib/click_house/migration_support/migration_error.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+module ClickHouse
+ module MigrationSupport
+ class MigrationError < StandardError
+ def initialize(message = nil)
+ message = "\n\n#{message}\n\n" if message
+ super
+ end
+ end
+
+ class IllegalMigrationNameError < MigrationError
+ def initialize(name = nil)
+ if name
+ super("Illegal name for migration file: #{name}\n\t(only lower case letters, numbers, and '_' allowed).")
+ else
+ super('Illegal name for migration.')
+ end
+ end
+ end
+
+ IrreversibleMigration = Class.new(MigrationError)
+
+ class DuplicateMigrationVersionError < MigrationError
+ def initialize(version = nil)
+ if version
+ super("Multiple migrations have the version number #{version}.")
+ else
+ super('Duplicate migration version error.')
+ end
+ end
+ end
+
+ class DuplicateMigrationNameError < MigrationError
+ def initialize(name = nil)
+ if name
+ super("Multiple migrations have the name #{name}.")
+ else
+ super('Duplicate migration name.')
+ end
+ end
+ end
+
+ class UnknownMigrationVersionError < MigrationError
+ def initialize(version = nil)
+ if version
+ super("No migration with version number #{version}.")
+ else
+ super('Unknown migration version.')
+ end
+ end
+ end
+ end
+end
diff --git a/lib/click_house/migration_support/migrator.rb b/lib/click_house/migration_support/migrator.rb
new file mode 100644
index 00000000000..5c67b3a5ff1
--- /dev/null
+++ b/lib/click_house/migration_support/migrator.rb
@@ -0,0 +1,160 @@
+# frozen_string_literal: true
+
+module ClickHouse
+ module MigrationSupport
+ class Migrator
+ class << self
+ attr_accessor :migrations_paths
+ end
+
+ attr_accessor :logger
+
+ self.migrations_paths = ["db/click_house/migrate"]
+
+ def initialize(direction, migrations, schema_migration, target_version = nil, logger = Gitlab::AppLogger)
+ @direction = direction
+ @target_version = target_version
+ @migrated_versions = {}
+ @migrations = migrations
+ @schema_migration = schema_migration
+ @logger = logger
+
+ validate(@migrations)
+
+ migrations.map(&:database).uniq.each do |database|
+ @schema_migration.create_table(database)
+ end
+ end
+
+ def current_version
+ @migrated_versions.values.flatten.max || 0
+ end
+
+ def current_migration
+ migrations.detect { |m| m.version == current_version }
+ end
+ alias_method :current, :current_migration
+
+ def run
+ run_without_lock
+ end
+
+ def migrate
+ migrate_without_lock
+ end
+
+ def runnable
+ runnable = migrations[start..finish]
+
+ if up?
+ runnable.reject { |m| ran?(m) }
+ else
+ # skip the last migration if we're headed down, but not ALL the way down
+ runnable.pop if target
+ runnable.find_all { |m| ran?(m) }
+ end
+ end
+
+ def migrations
+ down? ? @migrations.reverse : @migrations.sort_by(&:version)
+ end
+
+ def pending_migrations(database)
+ already_migrated = migrated(database)
+
+ migrations.reject { |m| already_migrated.include?(m.version) }
+ end
+
+ def migrated(database)
+ @migrated_versions[database] || load_migrated(database)
+ end
+
+ def load_migrated(database)
+ @migrated_versions[database] = Set.new(@schema_migration.all_versions(database).map(&:to_i))
+ end
+
+ private
+
+ # Used for running a specific migration.
+ def run_without_lock
+ migration = migrations.detect { |m| m.version == @target_version }
+
+ raise ClickHouse::MigrationSupport::UnknownMigrationVersionError, @target_version if migration.nil?
+
+ execute_migration(migration)
+ end
+
+ # Used for running multiple migrations up to or down to a certain value.
+ def migrate_without_lock
+ raise ClickHouse::MigrationSupport::UnknownMigrationVersionError, @target_version if invalid_target?
+
+ runnable.each(&method(:execute_migration)) # rubocop: disable Performance/MethodObjectAsBlock -- Execute through proxy
+ end
+
+ def ran?(migration)
+ migrated(migration.database).include?(migration.version.to_i)
+ end
+
+ # Return true if a valid version is not provided.
+ def invalid_target?
+ return unless @target_version
+ return if @target_version == 0
+
+ !target
+ end
+
+ def execute_migration(migration)
+ database = migration.database
+
+ return if down? && migrated(database).exclude?(migration.version.to_i)
+ return if up? && migrated(database).include?(migration.version.to_i)
+
+ logger.info "Migrating to #{migration.name} (#{migration.version})" if logger
+
+ migration.migrate(@direction)
+ record_version_state_after_migrating(database, migration.version)
+ rescue StandardError => e
+ msg = "An error has occurred, all later migrations canceled:\n\n#{e}"
+ raise StandardError, msg, e.backtrace
+ end
+
+ def target
+ migrations.detect { |m| m.version == @target_version }
+ end
+
+ def finish
+ migrations.index(target) || (migrations.size - 1)
+ end
+
+ def start
+ up? ? 0 : (migrations.index(current) || 0)
+ end
+
+ def validate(migrations)
+ name, = migrations.group_by(&:name).find { |_, v| v.length > 1 }
+ raise ClickHouse::MigrationSupport::DuplicateMigrationNameError, name if name
+
+ version, = migrations.group_by(&:version).find { |_, v| v.length > 1 }
+ raise ClickHouse::MigrationSupport::DuplicateMigrationVersionError, version if version
+ end
+
+ def record_version_state_after_migrating(database, version)
+ if down?
+ migrated(database).delete(version)
+ @schema_migration.create!(database, version: version.to_s, active: 0)
+ else
+ migrated(database) << version
+ @schema_migration.create!(database, version: version.to_s)
+ end
+ end
+
+ def up?
+ @direction == :up
+ end
+
+ def down?
+ @direction == :down
+ end
+ end
+ end
+end
diff --git a/lib/click_house/migration_support/schema_migration.rb b/lib/click_house/migration_support/schema_migration.rb
new file mode 100644
index 00000000000..e82debbad0d
--- /dev/null
+++ b/lib/click_house/migration_support/schema_migration.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+module ClickHouse
+ module MigrationSupport
+ class SchemaMigration
+ class_attribute :table_name_prefix, instance_writer: false, default: ''
+ class_attribute :table_name_suffix, instance_writer: false, default: ''
+ class_attribute :schema_migrations_table_name, instance_accessor: false, default: 'schema_migrations'
+
+ class << self
+ TABLE_EXISTS_QUERY = <<~SQL.squish
+ SELECT 1 FROM system.tables
+ WHERE name = {table_name: String} AND database = {database_name: String}
+ SQL
+
+ def primary_key
+ 'version'
+ end
+
+ def table_name
+ "#{table_name_prefix}#{schema_migrations_table_name}#{table_name_suffix}"
+ end
+
+ def table_exists?(database, configuration = ClickHouse::Migration.client_configuration)
+ database_name = configuration.databases[database]&.database
+ return false unless database_name
+
+ placeholders = { table_name: table_name, database_name: database_name }
+ query = ClickHouse::Client::Query.new(raw_query: TABLE_EXISTS_QUERY, placeholders: placeholders)
+
+ ClickHouse::Client.select(query, database, configuration).any?
+ end
+
+ def create_table(database, configuration = ClickHouse::Migration.client_configuration)
+ return if table_exists?(database, configuration)
+
+ query = <<~SQL
+ CREATE TABLE #{table_name} (
+ version LowCardinality(String),
+ active UInt8 NOT NULL DEFAULT 1,
+ applied_at DateTime64(6, 'UTC') NOT NULL DEFAULT now64()
+ )
+ ENGINE = ReplacingMergeTree(applied_at)
+ PRIMARY KEY(version)
+ ORDER BY (version)
+ SQL
+
+ ClickHouse::Client.execute(query, database, configuration)
+ end
+
+ def all_versions(database)
+ query = <<~SQL
+ SELECT version FROM #{table_name} FINAL
+ WHERE active = 1
+ ORDER BY (version)
+ SQL
+
+ ClickHouse::Client.select(query, database, ClickHouse::Migration.client_configuration).pluck('version')
+ end
+
+ def create!(database, **args)
+ insert_sql = <<~SQL
+ INSERT INTO #{table_name} (#{args.keys.join(',')}) VALUES (#{args.values.join(',')})
+ SQL
+
+ ClickHouse::Client.execute(insert_sql, database, ClickHouse::Migration.client_configuration)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/click_house/models/audit_event.rb b/lib/click_house/models/audit_event.rb
new file mode 100644
index 00000000000..a31b4a45298
--- /dev/null
+++ b/lib/click_house/models/audit_event.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+module ClickHouse
+ module Models
+ class AuditEvent < ClickHouse::Models::BaseModel
+ def self.table_name
+ 'audit_events'
+ end
+
+ def by_entity_type(entity_type)
+ where(entity_type: entity_type)
+ end
+
+ def by_entity_id(entity_id)
+ where(entity_id: entity_id)
+ end
+
+ def by_author_id(author_id)
+ where(author_id: author_id)
+ end
+
+ def by_entity_username(username)
+ where(entity_id: self.class.find_user_id(username))
+ end
+
+ def by_author_username(username)
+ where(author_id: self.class.find_user_id(username))
+ end
+
+ def self.by_entity_type(entity_type)
+ new.by_entity_type(entity_type)
+ end
+
+ def self.by_entity_id(entity_id)
+ new.by_entity_id(entity_id)
+ end
+
+ def self.by_author_id(author_id)
+ new.by_author_id(author_id)
+ end
+
+ def self.by_entity_username(username)
+ new.by_entity_username(username)
+ end
+
+ def self.by_author_username(username)
+ new.by_author_username(username)
+ end
+
+ def self.find_user_id(username)
+ ::User.find_by_username(username)&.id
+ end
+ end
+ end
+end
diff --git a/lib/click_house/models/base_model.rb b/lib/click_house/models/base_model.rb
new file mode 100644
index 00000000000..89624076f15
--- /dev/null
+++ b/lib/click_house/models/base_model.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+# rubocop: disable CodeReuse/ActiveRecord
+module ClickHouse
+ module Models
+ class BaseModel
+ extend Forwardable
+
+ def_delegators :@query_builder, :to_sql
+
+ def initialize(query_builder = ClickHouse::QueryBuilder.new(self.class.table_name))
+ @query_builder = query_builder
+ end
+
+ def self.table_name
+ raise NotImplementedError, "Subclasses must define a `table_name` class method"
+ end
+
+ def where(conditions)
+ self.class.new(@query_builder.where(conditions))
+ end
+
+ def order(field, direction = :asc)
+ self.class.new(@query_builder.order(field, direction))
+ end
+
+ def limit(count)
+ self.class.new(@query_builder.limit(count))
+ end
+
+ def offset(count)
+ self.class.new(@query_builder.offset(count))
+ end
+
+ def select(...)
+ self.class.new(@query_builder.select(...))
+ end
+ end
+ end
+end
+# rubocop: enable CodeReuse/ActiveRecord
diff --git a/lib/container_registry/client.rb b/lib/container_registry/client.rb
index e2a1b8296f6..580ba2bdc0d 100644
--- a/lib/container_registry/client.rb
+++ b/lib/container_registry/client.rb
@@ -78,13 +78,9 @@ module ContainerRegistry
delete_if_exists("/v2/#{name}/manifests/#{reference}")
end
- def delete_repository_tag_by_name(name, reference)
- delete_if_exists("/v2/#{name}/tags/reference/#{reference}")
- end
-
# Check if the registry supports tag deletion. This is only supported by the
# GitLab registry fork. The fastest and safest way to check this is to send
- # an OPTIONS request to /v2/<name>/tags/reference/<tag>, using a random
+ # an OPTIONS request to /v2/<name>/manifests/<tag>, using a random
# repository name and tag (the registry won't check if they exist).
# Registries that support tag deletion will reply with a 200 OK and include
# the DELETE method in the Allow header. Others reply with an 404 Not Found.
@@ -93,7 +89,7 @@ module ContainerRegistry
registry_features = Gitlab::CurrentSettings.container_registry_features || []
next true if ::Gitlab.com_except_jh? && registry_features.include?(REGISTRY_TAG_DELETE_FEATURE)
- response = faraday.run_request(:options, '/v2/name/tags/reference/tag', '', {})
+ response = faraday.run_request(:options, '/v2/name/manifests/tag', '', {})
response.success? && response.headers['allow']&.include?('DELETE')
end
end
diff --git a/lib/container_registry/gitlab_api_client.rb b/lib/container_registry/gitlab_api_client.rb
index bd833ec00af..9b6c37da847 100644
--- a/lib/container_registry/gitlab_api_client.rb
+++ b/lib/container_registry/gitlab_api_client.rb
@@ -103,7 +103,7 @@ module ContainerRegistry
end
end
- # https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs-gitlab/api.md#compliance-check
+ # https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs/spec/gitlab/api.md#compliance-check
def supports_gitlab_api?
strong_memoize(:supports_gitlab_api) do
registry_features = Gitlab::CurrentSettings.container_registry_features || []
@@ -116,19 +116,19 @@ module ContainerRegistry
end
end
- # https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs-gitlab/api.md#import-repository
+ # Deprecated. Will be removed as part of https://gitlab.com/gitlab-org/gitlab/-/issues/409873.
def pre_import_repository(path)
response = start_import_for(path, pre: true)
IMPORT_RESPONSES.fetch(response.status, :error)
end
- # https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs-gitlab/api.md#import-repository
+ # Deprecated. Will be removed as part of https://gitlab.com/gitlab-org/gitlab/-/issues/409873.
def import_repository(path)
response = start_import_for(path, pre: false)
IMPORT_RESPONSES.fetch(response.status, :error)
end
- # https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs-gitlab/api.md#cancel-repository-import
+ # Deprecated. Will be removed as part of https://gitlab.com/gitlab-org/gitlab/-/issues/409873.
def cancel_repository_import(path, force: false)
response = with_import_token_faraday do |faraday_client|
faraday_client.delete(import_url_for(path)) do |req|
@@ -142,7 +142,7 @@ module ContainerRegistry
{ status: status, migration_state: actual_state }
end
- # https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs-gitlab/api.md#get-repository-import-status
+ # Deprecated. Will be removed as part of https://gitlab.com/gitlab-org/gitlab/-/issues/409873.
def import_status(path)
with_import_token_faraday do |faraday_client|
response = faraday_client.get(import_url_for(path))
@@ -156,7 +156,7 @@ module ContainerRegistry
end
end
- # https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs-gitlab/api.md#get-repository-details
+ # https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs/spec/gitlab/api.md#get-repository-details
def repository_details(path, sizing: nil)
with_token_faraday do |faraday_client|
req = faraday_client.get("#{GITLAB_REPOSITORIES_PATH}/#{path}/") do |req|
@@ -169,7 +169,7 @@ module ContainerRegistry
end
end
- # https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs-gitlab/api.md#list-repository-tags
+ # https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs/spec/gitlab/api.md#list-repository-tags
def tags(path, page_size: 100, last: nil, before: nil, name: nil, sort: nil)
limited_page_size = [page_size, MAX_TAGS_PAGE_SIZE].min
with_token_faraday do |faraday_client|
@@ -178,7 +178,7 @@ module ContainerRegistry
req.params['n'] = limited_page_size
req.params['last'] = last if last
req.params['before'] = before if before
- req.params['name'] = name if name
+ req.params['name'] = name if name.present?
req.params['sort'] = sort if sort
end
@@ -202,7 +202,7 @@ module ContainerRegistry
end
end
- # https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs-gitlab/api.md#list-sub-repositories
+ # https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs/spec/gitlab/api.md#list-sub-repositories
def sub_repositories_with_tag(path, page_size: 100, last: nil)
limited_page_size = [page_size, MAX_REPOSITORIES_PAGE_SIZE].min
@@ -235,7 +235,7 @@ module ContainerRegistry
# Given a path 'group/subgroup/project' and name 'newname',
# with a successful rename, it will be 'group/subgroup/newname'
- # https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs-gitlab/api.md#rename-base-repository
+ # https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs/spec/gitlab/api.md#rename-base-repository
def rename_base_repository_path(path, name:, dry_run: false)
with_token_faraday do |faraday_client|
url = "#{GITLAB_REPOSITORIES_PATH}/#{path}/"
diff --git a/lib/container_registry/tag.rb b/lib/container_registry/tag.rb
index bf44b74cf7b..70742e8bd38 100644
--- a/lib/container_registry/tag.rb
+++ b/lib/container_registry/tag.rb
@@ -5,16 +5,25 @@ module ContainerRegistry
include Gitlab::Utils::StrongMemoize
attr_reader :repository, :name, :updated_at
- attr_writer :created_at
+ attr_writer :created_at, :manifest_digest, :revision, :total_size
delegate :registry, :client, to: :repository
- delegate :revision, :short_revision, to: :config_blob, allow_nil: true
def initialize(repository, name)
@repository = repository
@name = name
end
+ def revision
+ @revision || config_blob&.revision
+ end
+
+ def short_revision
+ return unless revision
+
+ revision[0..8]
+ end
+
def valid?
manifest.present?
end
@@ -53,7 +62,7 @@ module ContainerRegistry
def digest
strong_memoize(:digest) do
- client.repository_tag_digest(repository.path, name)
+ @manifest_digest || client.repository_tag_digest(repository.path, name)
end
end
@@ -126,6 +135,8 @@ module ContainerRegistry
# rubocop: disable CodeReuse/ActiveRecord
def total_size
+ return @total_size if @total_size
+
return unless layers
layers.sum(&:size) if v2?
diff --git a/lib/generators/batched_background_migration/templates/queue_batched_background_migration.template b/lib/generators/batched_background_migration/templates/queue_batched_background_migration.template
index 886a3bd3116..df4c5382749 100644
--- a/lib/generators/batched_background_migration/templates/queue_batched_background_migration.template
+++ b/lib/generators/batched_background_migration/templates/queue_batched_background_migration.template
@@ -6,6 +6,8 @@
# Update below commented lines with appropriate values.
class <%= migration_class_name %> < Gitlab::Database::Migration[<%= Gitlab::Database::Migration.current_version %>]
+ milestone '<%= Gitlab.current_milestone %>'
+
MIGRATION = "<%= class_name %>"
# DELAY_INTERVAL = 2.minutes
# BATCH_SIZE = <%= Gitlab::Database::Migrations::BatchedBackgroundMigrationHelpers::BATCH_SIZE %>
diff --git a/lib/generators/gitlab/partitioning/templates/foreign_key_definition.rb.template b/lib/generators/gitlab/partitioning/templates/foreign_key_definition.rb.template
index f2f9acea923..a9cf5d085d1 100644
--- a/lib/generators/gitlab/partitioning/templates/foreign_key_definition.rb.template
+++ b/lib/generators/gitlab/partitioning/templates/foreign_key_definition.rb.template
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class <%= migration_class_name %> < Gitlab::Database::Migration[<%= Gitlab::Database::Migration.current_version %>]
+ milestone '<%= Gitlab.current_milestone %>'
+
disable_ddl_transaction!
SOURCE_TABLE_NAME = :<%= source_table_name %>
diff --git a/lib/generators/gitlab/partitioning/templates/foreign_key_index.rb.template b/lib/generators/gitlab/partitioning/templates/foreign_key_index.rb.template
index 4896d931333..4ca4dd3c842 100644
--- a/lib/generators/gitlab/partitioning/templates/foreign_key_index.rb.template
+++ b/lib/generators/gitlab/partitioning/templates/foreign_key_index.rb.template
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class <%= migration_class_name %> < Gitlab::Database::Migration[<%= Gitlab::Database::Migration.current_version %>]
+ milestone '<%= Gitlab.current_milestone %>'
+
disable_ddl_transaction!
INDEX_NAME = :index_<%= source_table_name -%>_on_<%= partitioning_column -%>_<%= foreign_key_column %>
diff --git a/lib/generators/gitlab/partitioning/templates/foreign_key_removal.rb.template b/lib/generators/gitlab/partitioning/templates/foreign_key_removal.rb.template
index b4e881074ad..16bd2548f18 100644
--- a/lib/generators/gitlab/partitioning/templates/foreign_key_removal.rb.template
+++ b/lib/generators/gitlab/partitioning/templates/foreign_key_removal.rb.template
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class <%= migration_class_name %> < Gitlab::Database::Migration[<%= Gitlab::Database::Migration.current_version %>]
+ milestone '<%= Gitlab.current_milestone %>'
+
disable_ddl_transaction!
SOURCE_TABLE_NAME = :<%= source_table_name %>
diff --git a/lib/generators/gitlab/partitioning/templates/foreign_key_validation.rb.template b/lib/generators/gitlab/partitioning/templates/foreign_key_validation.rb.template
index bad7d17a51b..b065f390863 100644
--- a/lib/generators/gitlab/partitioning/templates/foreign_key_validation.rb.template
+++ b/lib/generators/gitlab/partitioning/templates/foreign_key_validation.rb.template
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class <%= migration_class_name %> < Gitlab::Database::Migration[<%= Gitlab::Database::Migration.current_version %>]
+ milestone '<%= Gitlab.current_milestone %>'
+
disable_ddl_transaction!
TABLE_NAME = :<%= source_table_name %>
diff --git a/lib/generators/gitlab/snowplow_event_definition_generator.rb b/lib/generators/gitlab/snowplow_event_definition_generator.rb
deleted file mode 100644
index b1a31541350..00000000000
--- a/lib/generators/gitlab/snowplow_event_definition_generator.rb
+++ /dev/null
@@ -1,73 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails/generators'
-
-module Gitlab
- class SnowplowEventDefinitionGenerator < Rails::Generators::Base
- CE_DIR = 'config/events'
- EE_DIR = 'ee/config/events'
-
- source_root File.expand_path('../../../generator_templates/snowplow_event_definition', __dir__)
-
- desc 'Generates an event definition yml file'
-
- class_option :ee, type: :boolean, optional: true, default: false, desc: 'Indicates if event is for ee'
- class_option :category, type: :string, optional: false, desc: 'Category of the event'
- class_option :action, type: :string, optional: false, desc: 'Action of the event'
-
- def create_event_file
- raise "Event definition already exists at #{file_path}" if definition_exists?
-
- template "event_definition.yml", file_path, force: false
- end
-
- def distributions
- (ee? ? ['- ee'] : ['- ce', '- ee']).join("\n")
- end
-
- def event_category
- options[:category]
- end
-
- def event_action
- options[:action]
- end
-
- def milestone
- Gitlab::VERSION.match('(\d+\.\d+)').captures.first
- end
-
- def ee?
- options[:ee]
- end
-
- private
-
- def definition_exists?
- File.exist?(ce_file_path) || File.exist?(ee_file_path)
- end
-
- def file_path
- ee? ? ee_file_path : ce_file_path
- end
-
- def ce_file_path
- File.join(CE_DIR, file_name)
- end
-
- def ee_file_path
- File.join(EE_DIR, file_name)
- end
-
- # Example of file name
- # 20230227000018_project_management_issue_title_changed.yml
- def file_name
- name = remove_special_chars("#{Time.now.utc.strftime('%Y%m%d%H%M%S')}_#{event_category}_#{event_action}")
- "#{name[0..95]}.yml" # max 100 chars, see https://gitlab.com/gitlab-com/gl-infra/delivery/-/issues/2030#note_679501200
- end
-
- def remove_special_chars(input)
- input.gsub("::", "__").gsub(/[^A-Za-z0-9_]/, '')
- end
- end
-end
diff --git a/lib/generators/gitlab/usage_metric_definition/redis_hll_generator.rb b/lib/generators/gitlab/usage_metric_definition/redis_hll_generator.rb
index 8cd03978f27..f8a05d3132f 100644
--- a/lib/generators/gitlab/usage_metric_definition/redis_hll_generator.rb
+++ b/lib/generators/gitlab/usage_metric_definition/redis_hll_generator.rb
@@ -1,11 +1,14 @@
# frozen_string_literal: true
+# DEPRECATED. Consider using using Internal Events tracking framework
+# https://docs.gitlab.com/ee/development/internal_analytics/internal_event_instrumentation/quick_start.html
+
require 'rails/generators'
module Gitlab
module UsageMetricDefinition
class RedisHllGenerator < Rails::Generators::Base
- desc 'Generates a metric definition .yml file with defaults for Redis HLL.'
+ desc '[DEPRECATED] Generates a metric definition .yml file with defaults for Redis HLL.'
argument :category, type: :string, desc: "Category name"
argument :events, type: :array, desc: "Unique event names", banner: 'event_one event_two event_three'
diff --git a/lib/generators/gitlab/usage_metric_definition_generator.rb b/lib/generators/gitlab/usage_metric_definition_generator.rb
index d57a6b0b724..c231697e22e 100644
--- a/lib/generators/gitlab/usage_metric_definition_generator.rb
+++ b/lib/generators/gitlab/usage_metric_definition_generator.rb
@@ -1,5 +1,8 @@
# frozen_string_literal: true
+# DEPRECATED. Consider using using Internal Events tracking framework
+# https://docs.gitlab.com/ee/development/internal_analytics/internal_event_instrumentation/quick_start.html
+
require 'rails/generators'
module Gitlab
@@ -30,7 +33,7 @@ module Gitlab
source_root File.expand_path('../../../generator_templates/usage_metric_definition', __dir__)
- desc 'Generates metric definitions yml files'
+ desc '[DEPRECATED] Generates metric definitions yml files'
class_option :ee, type: :boolean, optional: true, default: false, desc: 'Indicates if metric is for ee'
class_option :dir,
@@ -40,6 +43,13 @@ module Gitlab
argument :key_paths, type: :array, desc: 'Unique JSON key paths for the metrics'
def create_metric_file
+ say("This generator is DEPRECATED. Use Internal Events tracking framework instead.")
+ # rubocop: disable Gitlab/DocUrl -- link for developers, not users
+ say("https://docs.gitlab.com/ee/development/internal_analytics/internal_event_instrumentation/quick_start.html")
+ # rubocop: enable Gitlab/DocUrl
+ desc = ask("Would you like to continue anyway? y/N") || 'n'
+ return unless desc.casecmp('y') == 0
+
validate!
key_paths.each do |key_path|
diff --git a/lib/gitlab.rb b/lib/gitlab.rb
index 0875b14f7d0..b98a0207567 100644
--- a/lib/gitlab.rb
+++ b/lib/gitlab.rb
@@ -16,6 +16,11 @@ module Gitlab
Gitlab::VersionInfo.parse(Gitlab::VERSION)
end
+ def self.current_milestone
+ v = version_info
+ "#{v.major}.#{v.minor}"
+ end
+
def self.pre_release?
VERSION.include?('pre')
end
diff --git a/lib/gitlab/analytics/cycle_analytics/request_params.rb b/lib/gitlab/analytics/cycle_analytics/request_params.rb
index cea25ba2db4..0c4a0afa1d5 100644
--- a/lib/gitlab/analytics/cycle_analytics/request_params.rb
+++ b/lib/gitlab/analytics/cycle_analytics/request_params.rb
@@ -203,7 +203,8 @@ module Gitlab
def validate_date_range
return if created_after.nil? || created_before.nil?
- if (created_before - created_after) > MAX_RANGE_DAYS
+ time_period = created_before.at_beginning_of_day - created_after.at_beginning_of_day
+ if time_period > MAX_RANGE_DAYS
errors.add(:created_after, s_('CycleAnalytics|The given date range is larger than 180 days'))
end
end
diff --git a/lib/gitlab/application_rate_limiter.rb b/lib/gitlab/application_rate_limiter.rb
index bf3f5b61825..469927b8a53 100644
--- a/lib/gitlab/application_rate_limiter.rb
+++ b/lib/gitlab/application_rate_limiter.rb
@@ -55,6 +55,7 @@ module Gitlab
phone_verification_send_code: { threshold: 10, interval: 1.hour },
phone_verification_verify_code: { threshold: 10, interval: 10.minutes },
namespace_exists: { threshold: 20, interval: 1.minute },
+ update_namespace_name: { threshold: -> { application_settings.update_namespace_name_rate_limit }, interval: 1.hour },
fetch_google_ip_list: { threshold: 10, interval: 1.minute },
project_fork_sync: { threshold: 10, interval: 30.minutes },
ai_action: { threshold: 160, interval: 8.hours },
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index fc1f7a1583c..578cfb52714 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -60,10 +60,10 @@ module Gitlab
Gitlab.config.omniauth.enabled
end
- def find_for_git_client(login, password, project:, ip:)
- raise "Must provide an IP for rate limiting" if ip.nil?
+ def find_for_git_client(login, password, project:, request:)
+ raise "Must provide an IP for rate limiting" if request.ip.nil?
- rate_limiter = Gitlab::Auth::IpRateLimiter.new(ip)
+ rate_limiter = Gitlab::Auth::IpRateLimiter.new(request.ip)
raise IpBlocked if !skip_rate_limit?(login: login) && rate_limiter.banned?
@@ -80,7 +80,7 @@ module Gitlab
user_with_password_for_git(login, password) ||
Gitlab::Auth::Result::EMPTY
- rate_limit!(rate_limiter, success: result.success?, login: login)
+ rate_limit!(rate_limiter, success: result.success?, login: login, request: request)
look_to_limit_user(result.actor)
return result if result.success? || authenticate_using_internal_or_ldap_password?
@@ -142,7 +142,7 @@ module Gitlab
private
- def rate_limit!(rate_limiter, success:, login:)
+ def rate_limit!(rate_limiter, success:, login:, request:)
return if skip_rate_limit?(login: login)
if success
@@ -155,8 +155,18 @@ module Gitlab
# request from this IP if needed.
# This returns true when the failures are over the threshold and the IP
# is banned.
- Gitlab::AppLogger.info "IP #{rate_limiter.ip} failed to login " \
- "as #{login} but has been temporarily banned from Git auth"
+
+ message = "Rack_Attack: Git auth failures has exceeded the threshold. " \
+ "IP has been temporarily banned from Git auth."
+
+ Gitlab::AuthLogger.error(
+ message: message,
+ env: :blocklist,
+ remote_ip: request.ip,
+ request_method: request.request_method,
+ path: request.fullpath,
+ login: login
+ )
end
end
diff --git a/lib/gitlab/auth/saml/config.rb b/lib/gitlab/auth/saml/config.rb
index 7524d8b9f85..e6c9f04eff5 100644
--- a/lib/gitlab/auth/saml/config.rb
+++ b/lib/gitlab/auth/saml/config.rb
@@ -8,6 +8,21 @@ module Gitlab
def enabled?
::AuthHelper.saml_providers.any?
end
+
+ def default_attribute_statements
+ defaults = OmniAuth::Strategies::SAML.default_options[:attribute_statements].to_hash.deep_symbolize_keys
+ defaults[:nickname] = %w[username nickname]
+ defaults[:name] << 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name'
+ defaults[:name] << 'http://schemas.microsoft.com/ws/2008/06/identity/claims/name'
+ defaults[:email] << 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'
+ defaults[:email] << 'http://schemas.microsoft.com/ws/2008/06/identity/claims/emailaddress'
+ defaults[:first_name] << 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname'
+ defaults[:first_name] << 'http://schemas.microsoft.com/ws/2008/06/identity/claims/givenname'
+ defaults[:last_name] << 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname'
+ defaults[:last_name] << 'http://schemas.microsoft.com/ws/2008/06/identity/claims/surname'
+
+ defaults
+ end
end
DEFAULT_PROVIDER_NAME = 'saml'
diff --git a/lib/gitlab/auth/two_factor_auth_verifier.rb b/lib/gitlab/auth/two_factor_auth_verifier.rb
index fbdfd105ee3..4b66aaf0e6a 100644
--- a/lib/gitlab/auth/two_factor_auth_verifier.rb
+++ b/lib/gitlab/auth/two_factor_auth_verifier.rb
@@ -36,7 +36,7 @@ module Gitlab
return false unless time
- two_factor_grace_period.hours.since(time) < Time.current
+ two_factor_grace_period.hours.since(time).past?
end
def allow_2fa_bypass_for_provider
diff --git a/lib/gitlab/background_migration/backfill_packages_tags_project_id.rb b/lib/gitlab/background_migration/backfill_packages_tags_project_id.rb
new file mode 100644
index 00000000000..04fd09f81f0
--- /dev/null
+++ b/lib/gitlab/background_migration/backfill_packages_tags_project_id.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # This migration populates the new `packages_tags.project_id` column from joining with `packages_packages` table
+ class BackfillPackagesTagsProjectId < BatchedMigrationJob
+ operation_name :update_all # This is used as the key on collecting metrics
+ scope_to ->(relation) { relation.where(project_id: nil) }
+ feature_category :package_registry
+
+ def perform
+ each_sub_batch do |sub_batch|
+ joined = sub_batch
+ .joins('INNER JOIN packages_packages ON packages_tags.package_id = packages_packages.id')
+ .select('packages_tags.id, packages_packages.project_id')
+
+ ApplicationRecord.connection.execute <<~SQL
+ WITH joined_cte(packages_tag_id, project_id) AS MATERIALIZED (
+ #{joined.to_sql}
+ )
+ UPDATE packages_tags
+ SET project_id = joined_cte.project_id
+ FROM joined_cte
+ WHERE id = joined_cte.packages_tag_id
+ SQL
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/batched_migration_job.rb b/lib/gitlab/background_migration/batched_migration_job.rb
index 952e6d01f1a..9e9fc9b98b7 100644
--- a/lib/gitlab/background_migration/batched_migration_job.rb
+++ b/lib/gitlab/background_migration/batched_migration_job.rb
@@ -130,7 +130,7 @@ module Gitlab
end
def base_relation
- define_batchable_model(batch_table, connection: connection)
+ define_batchable_model(batch_table, connection: connection, primary_key: batch_column)
.where(batch_column => start_id..end_id)
end
diff --git a/lib/gitlab/background_migration/delete_invalid_protected_branch_merge_access_levels.rb b/lib/gitlab/background_migration/delete_invalid_protected_branch_merge_access_levels.rb
new file mode 100644
index 00000000000..99bc638532a
--- /dev/null
+++ b/lib/gitlab/background_migration/delete_invalid_protected_branch_merge_access_levels.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # A job to remove protected_branch_merge_access_levels for groups that do not have project_group_links
+ # to the project for the associated protected branch
+ class DeleteInvalidProtectedBranchMergeAccessLevels < BatchedMigrationJob
+ operation_name :delete_invalid_protected_branch_merge_access_levels
+ scope_to ->(relation) { relation.where.not(group_id: nil) }
+ feature_category :source_code_management
+
+ def perform
+ each_sub_batch do |sub_batch|
+ sub_batch
+ .joins('INNER JOIN protected_branches ON protected_branches.id = protected_branch_id')
+ .joins(%(
+ LEFT OUTER JOIN project_group_links pgl
+ ON pgl.group_id = protected_branch_merge_access_levels.group_id
+ AND pgl.project_id = protected_branches.project_id
+ ))
+ .where(%(
+ pgl.id IS NULL
+ )).delete_all
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/delete_invalid_protected_branch_push_access_levels.rb b/lib/gitlab/background_migration/delete_invalid_protected_branch_push_access_levels.rb
new file mode 100644
index 00000000000..a6934cf5adc
--- /dev/null
+++ b/lib/gitlab/background_migration/delete_invalid_protected_branch_push_access_levels.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # A job to remove protected_branch_push_access_levels for groups that do not have project_group_links
+ # to the project for the associated protected branch
+ class DeleteInvalidProtectedBranchPushAccessLevels < BatchedMigrationJob
+ operation_name :delete_invalid_protected_branch_push_access_levels
+ scope_to ->(relation) { relation.where.not(group_id: nil) }
+ feature_category :source_code_management
+
+ def perform
+ each_sub_batch do |sub_batch|
+ sub_batch
+ .joins('INNER JOIN protected_branches ON protected_branches.id = protected_branch_id')
+ .joins(%(
+ LEFT OUTER JOIN project_group_links pgl
+ ON pgl.group_id = protected_branch_push_access_levels.group_id
+ AND pgl.project_id = protected_branches.project_id
+ ))
+ .where(%(
+ pgl.id IS NULL
+ )).delete_all
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/delete_invalid_protected_tag_create_access_levels.rb b/lib/gitlab/background_migration/delete_invalid_protected_tag_create_access_levels.rb
new file mode 100644
index 00000000000..8c59e42a9f6
--- /dev/null
+++ b/lib/gitlab/background_migration/delete_invalid_protected_tag_create_access_levels.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # A job to remove protected_tag_create_access_levels for groups that do not have project_group_links
+ # to the project for the associated protected branch
+ class DeleteInvalidProtectedTagCreateAccessLevels < BatchedMigrationJob
+ operation_name :delete_invalid_protected_tag_create_access_levels
+ scope_to ->(relation) { relation.where.not(group_id: nil) }
+ feature_category :source_code_management
+
+ def perform
+ each_sub_batch do |sub_batch|
+ sub_batch
+ .joins('INNER JOIN protected_tags ON protected_tags.id = protected_tag_id')
+ .joins(%(
+ LEFT OUTER JOIN project_group_links pgl
+ ON pgl.group_id = protected_tag_create_access_levels.group_id
+ AND pgl.project_id = protected_tags.project_id
+ ))
+ .where(%(
+ pgl.id IS NULL
+ )).delete_all
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/base_doorkeeper_controller.rb b/lib/gitlab/base_doorkeeper_controller.rb
index 91994c2fa95..c8520993b8e 100644
--- a/lib/gitlab/base_doorkeeper_controller.rb
+++ b/lib/gitlab/base_doorkeeper_controller.rb
@@ -3,7 +3,8 @@
# This is a base controller for doorkeeper.
# It adds the `can?` helper used in the views.
module Gitlab
- class BaseDoorkeeperController < BaseActionController
+ # rubocop:disable Rails/ApplicationController
+ class BaseDoorkeeperController < ActionController::Base
include Gitlab::Allowable
include EnforcesTwoFactorAuthentication
include SessionsHelper
@@ -12,4 +13,5 @@ module Gitlab
helper_method :can?
end
+ # rubocop:enable Rails/ApplicationController
end
diff --git a/lib/gitlab/bitbucket_import/importers/issue_importer.rb b/lib/gitlab/bitbucket_import/importers/issue_importer.rb
index 2c3be67eabc..d194a311278 100644
--- a/lib/gitlab/bitbucket_import/importers/issue_importer.rb
+++ b/lib/gitlab/bitbucket_import/importers/issue_importer.rb
@@ -40,6 +40,8 @@ module Gitlab
project.issues.create!(attributes)
+ metrics.issues_counter.increment
+
log_info(import_stage: 'import_issue', message: 'finished', iid: object[:iid])
rescue StandardError => e
track_import_failure!(project, exception: e)
diff --git a/lib/gitlab/bitbucket_import/importers/issues_importer.rb b/lib/gitlab/bitbucket_import/importers/issues_importer.rb
index 6162433e701..8ab82ddb0be 100644
--- a/lib/gitlab/bitbucket_import/importers/issues_importer.rb
+++ b/lib/gitlab/bitbucket_import/importers/issues_importer.rb
@@ -7,17 +7,21 @@ module Gitlab
include ParallelScheduling
def execute
+ return job_waiter unless repo.issues_enabled?
+
log_info(import_stage: 'import_issues', message: 'importing issues')
issues = client.issues(project.import_source)
labels = build_labels_hash
- issues.each do |issue|
+ issues.each_with_index do |issue, index|
job_waiter.jobs_remaining += 1
next if already_enqueued?(issue)
+ allocate_issues_internal_id! if index == 0
+
job_delay = calculate_job_delay(job_waiter.jobs_remaining)
issue_hash = issue.to_hash.merge({ issue_type_id: default_issue_type_id, label_id: labels[issue.kind] })
@@ -49,11 +53,23 @@ module Gitlab
::WorkItems::Type.default_issue_type.id
end
+ def allocate_issues_internal_id!
+ last_bitbucket_issue = client.last_issue(repo)
+
+ return unless last_bitbucket_issue
+
+ Issue.track_namespace_iid!(project.project_namespace, last_bitbucket_issue.iid)
+ end
+
def build_labels_hash
labels = {}
project.labels.each { |l| labels[l.title.to_s] = l.id }
labels
end
+
+ def repo
+ @repo ||= client.repo(project.import_source)
+ end
end
end
end
diff --git a/lib/gitlab/bitbucket_import/importers/pull_request_importer.rb b/lib/gitlab/bitbucket_import/importers/pull_request_importer.rb
index a18d50e8fce..f7b1753a9f9 100644
--- a/lib/gitlab/bitbucket_import/importers/pull_request_importer.rb
+++ b/lib/gitlab/bitbucket_import/importers/pull_request_importer.rb
@@ -45,6 +45,8 @@ module Gitlab
merge_request.assignee_ids = [author_id]
merge_request.reviewer_ids = reviewers
merge_request.save!
+
+ metrics.merge_requests_counter.increment
end
log_info(import_stage: 'import_pull_request', message: 'finished', iid: object[:iid])
diff --git a/lib/gitlab/bitbucket_import/importers/pull_request_notes_importer.rb b/lib/gitlab/bitbucket_import/importers/pull_request_notes_importer.rb
index 8ea8b1562f2..934e4ee1720 100644
--- a/lib/gitlab/bitbucket_import/importers/pull_request_notes_importer.rb
+++ b/lib/gitlab/bitbucket_import/importers/pull_request_notes_importer.rb
@@ -4,21 +4,22 @@ module Gitlab
module BitbucketImport
module Importers
class PullRequestNotesImporter
- include Loggable
- include ErrorTracking
+ include ParallelScheduling
def initialize(project, hash)
@project = project
- @importer = Gitlab::BitbucketImport::Importer.new(project)
+ @formatter = Gitlab::ImportFormatter.new
+ @user_finder = UserFinder.new(project)
+ @ref_converter = Gitlab::BitbucketImport::RefConverter.new(project)
@object = hash.with_indifferent_access
+ @position_map = {}
+ @discussion_map = {}
end
def execute
log_info(import_stage: 'import_pull_request_notes', message: 'starting', iid: object[:iid])
- merge_request = project.merge_requests.find_by(iid: object[:iid]) # rubocop: disable CodeReuse/ActiveRecord
-
- importer.import_pull_request_comments(merge_request, merge_request) if merge_request
+ import_pull_request_comments if merge_request
log_info(import_stage: 'import_pull_request_notes', message: 'finished', iid: object[:iid])
rescue StandardError => e
@@ -27,7 +28,116 @@ module Gitlab
private
- attr_reader :object, :project, :importer
+ attr_reader :object, :project, :formatter, :user_finder, :ref_converter, :discussion_map, :position_map
+
+ def import_pull_request_comments
+ inline_comments, pr_comments = comments.partition(&:inline?)
+
+ import_inline_comments(inline_comments)
+ import_standalone_pr_comments(pr_comments)
+ end
+
+ def import_inline_comments(inline_comments)
+ children, parents = inline_comments.partition(&:has_parent?)
+
+ parents.each do |comment|
+ position_map[comment.iid] = build_position(comment)
+
+ import_comment(comment)
+ end
+
+ children.each do |comment|
+ position_map[comment.iid] = position_map.fetch(comment.parent_id, nil)
+
+ import_comment(comment)
+ end
+ end
+
+ def import_comment(comment)
+ position = position_map[comment.iid]
+ discussion_id = discussion_map[comment.parent_id]
+
+ note = create_diff_note(comment, position, discussion_id)
+
+ discussion_map[comment.iid] = note&.discussion_id
+ end
+
+ def create_diff_note(comment, position, discussion_id)
+ attributes = pull_request_comment_attributes(comment)
+ attributes.merge!(position: position, type: 'DiffNote', discussion_id: discussion_id)
+
+ note = merge_request.notes.build(attributes)
+
+ return note if note.save
+
+ # Bitbucket supports the ability to comment on any line, not just the
+ # line in the diff. If we can't add the note as a DiffNote, fallback to creating
+ # a regular note.
+
+ log_info(import_stage: 'create_diff_note', message: 'creating fallback DiffNote', iid: merge_request.iid)
+ create_fallback_diff_note(comment, position)
+ rescue StandardError => e
+ Gitlab::ErrorTracking.log_exception(
+ e,
+ import_stage: 'create_diff_note', comment_id: comment.iid, error: e.message
+ )
+
+ nil
+ end
+
+ def create_fallback_diff_note(comment, position)
+ attributes = pull_request_comment_attributes(comment)
+ note = "*Comment on"
+
+ note += " #{position.old_path}:#{position.old_line} -->" if position&.old_line
+ note += " #{position.new_path}:#{position.new_line}" if position&.new_line
+ note += "*\n\n#{comment.note}"
+
+ attributes[:note] = note
+ merge_request.notes.create!(attributes)
+ end
+
+ def build_position(pr_comment)
+ params = {
+ diff_refs: merge_request.diff_refs,
+ old_path: pr_comment.file_path,
+ new_path: pr_comment.file_path,
+ old_line: pr_comment.old_pos,
+ new_line: pr_comment.new_pos
+ }
+
+ Gitlab::Diff::Position.new(params)
+ end
+
+ def import_standalone_pr_comments(pr_comments)
+ pr_comments.each do |comment|
+ attributes = pull_request_comment_attributes(comment)
+ merge_request.notes.create!(attributes)
+ end
+ end
+
+ def pull_request_comment_attributes(comment)
+ {
+ project: project,
+ author_id: user_finder.gitlab_user_id(project, comment.author),
+ note: comment_note(comment),
+ created_at: comment.created_at,
+ updated_at: comment.updated_at
+ }
+ end
+
+ def comment_note(comment)
+ author = formatter.author_line(comment.author) unless user_finder.find_user_id(comment.author)
+ author.to_s + ref_converter.convert_note(comment.note.to_s)
+ end
+
+ def merge_request
+ @merge_request ||= project.merge_requests.iid_in(object[:iid]).first
+ end
+
+ def comments
+ client.pull_request_comments(project.import_source, merge_request.iid).reject(&:deleted?)
+ end
end
end
end
diff --git a/lib/gitlab/bitbucket_import/importers/repository_importer.rb b/lib/gitlab/bitbucket_import/importers/repository_importer.rb
index b8c0ba69d37..9be7ed99436 100644
--- a/lib/gitlab/bitbucket_import/importers/repository_importer.rb
+++ b/lib/gitlab/bitbucket_import/importers/repository_importer.rb
@@ -19,6 +19,7 @@ module Gitlab
validate_repository_size!
+ set_default_branch
update_clone_time
end
@@ -76,6 +77,16 @@ module Gitlab
def validate_repository_size!
# Defined in EE
end
+
+ def set_default_branch
+ default_branch = client.repo(project.import_source).default_branch
+
+ project.change_head(default_branch) if default_branch
+ end
+
+ def client
+ Bitbucket::Client.new(project.import_data.credentials)
+ end
end
end
end
diff --git a/lib/gitlab/bitbucket_import/loggable.rb b/lib/gitlab/bitbucket_import/loggable.rb
index eda3cc96d4d..aeae993b9eb 100644
--- a/lib/gitlab/bitbucket_import/loggable.rb
+++ b/lib/gitlab/bitbucket_import/loggable.rb
@@ -19,6 +19,10 @@ module Gitlab
logger.error(log_data(messages))
end
+ def metrics
+ Gitlab::Import::Metrics.new(:bitbucket_importer, project)
+ end
+
private
def logger
diff --git a/lib/gitlab/checks/diff_check.rb b/lib/gitlab/checks/diff_check.rb
index 15b38188f13..a359236e150 100644
--- a/lib/gitlab/checks/diff_check.rb
+++ b/lib/gitlab/checks/diff_check.rb
@@ -31,8 +31,7 @@ module Gitlab
def treeish_objects
objects = commits
- return objects unless project.repository.empty? &&
- Feature.enabled?(:verify_push_rules_for_first_commit, project)
+ return objects unless project.repository.empty?
# It's a special case for the push to the empty repository
#
diff --git a/lib/gitlab/checks/file_size_check/any_oversized_blobs.rb b/lib/gitlab/checks/file_size_check/any_oversized_blobs.rb
index 35f969dbb46..b8c6bdee1bb 100644
--- a/lib/gitlab/checks/file_size_check/any_oversized_blobs.rb
+++ b/lib/gitlab/checks/file_size_check/any_oversized_blobs.rb
@@ -6,7 +6,7 @@ module Gitlab
class AnyOversizedBlobs
def initialize(project:, changes:, file_size_limit_megabytes:)
@project = project
- @newrevs = changes.pluck(:newrev).compact # rubocop:disable CodeReuse/ActiveRecord just plucking from an array
+ @newrevs = changes.pluck(:newrev).compact # rubocop:disable CodeReuse/ActiveRecord -- Array#pluck
@file_size_limit_megabytes = file_size_limit_megabytes
end
diff --git a/lib/gitlab/ci/ansi2json/line.rb b/lib/gitlab/ci/ansi2json/line.rb
index 21fc2980cdc..791b8a963e9 100644
--- a/lib/gitlab/ci/ansi2json/line.rb
+++ b/lib/gitlab/ci/ansi2json/line.rb
@@ -35,13 +35,15 @@ module Gitlab
end
attr_reader :offset, :sections, :segments, :current_segment,
- :section_header, :section_duration, :section_options
+ :section_header, :section_footer, :section_duration,
+ :section_options
def initialize(offset:, style:, sections: [])
@offset = offset
@segments = []
@sections = sections
@section_header = false
+ @section_footer = false
@duration = nil
@current_segment = Segment.new(style: style)
end
@@ -79,6 +81,10 @@ module Gitlab
@section_header = true
end
+ def set_as_section_footer
+ @section_footer = true
+ end
+
def set_section_duration(duration_in_seconds)
normalized_duration_in_seconds = duration_in_seconds.to_i.clamp(0, 1.year)
duration = ActiveSupport::Duration.build(normalized_duration_in_seconds)
@@ -103,6 +109,7 @@ module Gitlab
{ offset: offset, content: @segments }.tap do |result|
result[:section] = sections.last if sections.any?
result[:section_header] = true if @section_header
+ result[:section_footer] = true if @section_footer
result[:section_duration] = @section_duration if @section_duration
result[:section_options] = @section_options if @section_options
end
diff --git a/lib/gitlab/ci/ansi2json/state.rb b/lib/gitlab/ci/ansi2json/state.rb
index 3aec1cde1bc..6cf76fbbb51 100644
--- a/lib/gitlab/ci/ansi2json/state.rb
+++ b/lib/gitlab/ci/ansi2json/state.rb
@@ -49,6 +49,7 @@ module Gitlab
duration = timestamp.to_i - @open_sections[section].to_i
@current_line.set_section_duration(duration)
+ @current_line.set_as_section_footer
@open_sections.delete(section)
end
diff --git a/lib/gitlab/ci/build/context/build.rb b/lib/gitlab/ci/build/context/build.rb
index 48b138b0258..bbcdcd7d389 100644
--- a/lib/gitlab/ci/build/context/build.rb
+++ b/lib/gitlab/ci/build/context/build.rb
@@ -33,13 +33,9 @@ module Gitlab
# Assigning tags and needs is slow and they are not needed for rules
# evaluation since we don't use them to compute the variables at this point.
def build_attributes
- if pipeline.reduced_build_attributes_list_for_rules?
- attributes
- .except(:tag_list, :needs_attributes)
- .merge!(pipeline_attributes, ci_stage_attributes)
- else
- attributes.merge(pipeline_attributes, ci_stage_attributes)
- end
+ attributes
+ .except(:tag_list, :needs_attributes)
+ .merge!(pipeline_attributes, ci_stage_attributes)
end
def ci_stage_attributes
diff --git a/lib/gitlab/ci/components/instance_path.rb b/lib/gitlab/ci/components/instance_path.rb
index df2b2a14fc6..50731d54fc0 100644
--- a/lib/gitlab/ci/components/instance_path.rb
+++ b/lib/gitlab/ci/components/instance_path.rb
@@ -18,7 +18,6 @@ module Gitlab
def initialize(address:)
@full_path, @version = address.to_s.split('@', 2)
@host = Settings.gitlab_ci['component_fqdn']
- @component_project = ::Ci::Catalog::ComponentsProject.new(project, sha)
end
def fetch_content!(current_user:)
@@ -27,7 +26,8 @@ module Gitlab
raise Gitlab::Access::AccessDeniedError unless Ability.allowed?(current_user, :download_code, project)
- @component_project.fetch_component(component_name)
+ component_project = ::Ci::Catalog::ComponentsProject.new(project, sha)
+ component_project.fetch_component(component_name)
end
def project
diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb
index bf8a99ef45e..5fcafcba829 100644
--- a/lib/gitlab/ci/config/entry/job.rb
+++ b/lib/gitlab/ci/config/entry/job.rb
@@ -14,7 +14,7 @@ module Gitlab
ALLOWED_KEYS = %i[tags script image services start_in artifacts
cache dependencies before_script after_script hooks
coverage retry parallel interruptible timeout
- release id_tokens publish].freeze
+ release id_tokens publish pages].freeze
validations do
validates :config, allowed_keys: Gitlab::Ci::Config::Entry::Job.allowed_keys + PROCESSABLE_ALLOWED_KEYS
@@ -40,13 +40,19 @@ module Gitlab
if needs_value[:job].nil? && needs_value[:cross_dependency].present?
errors.add(:needs, "corresponding to dependencies must be from the same pipeline")
else
- missing_needs = dependencies - needs_value[:job].pluck(:name) # rubocop:disable CodeReuse/ActiveRecord (Array#pluck)
+ missing_needs = dependencies - needs_value[:job].pluck(:name) # rubocop:disable CodeReuse/ActiveRecord -- Array#pluck
errors.add(:dependencies, "the #{missing_needs.join(", ")} should be part of needs") if missing_needs.any?
end
end
- validates :publish, absence: { message: "can only be used within a `pages` job" }, unless: -> { pages_job? }
+ validates :publish,
+ absence: { message: "can only be used within a `pages` job" },
+ unless: -> { pages_job? }
+
+ validates :pages,
+ absence: { message: "can only be used within a `pages` job" },
+ unless: -> { pages_job? }
end
entry :before_script, Entry::Commands,
@@ -127,10 +133,14 @@ module Gitlab
description: 'Path to be published with Pages',
inherit: false
+ entry :pages, ::Gitlab::Ci::Config::Entry::Pages,
+ inherit: false,
+ description: 'Pages configuration.'
+
attributes :script, :tags, :when, :dependencies,
:needs, :retry, :parallel, :start_in,
- :interruptible, :timeout,
- :release, :allow_failure, :publish
+ :interruptible, :timeout, :release,
+ :allow_failure, :publish, :pages
def self.matching?(name, config)
!name.to_s.start_with?('.') &&
@@ -170,7 +180,8 @@ module Gitlab
needs: needs_defined? ? needs_value : nil,
scheduling_type: needs_defined? ? :dag : :stage,
id_tokens: id_tokens_value,
- publish: publish
+ publish: publish,
+ pages: pages
).compact
end
diff --git a/lib/gitlab/ci/config/entry/pages.rb b/lib/gitlab/ci/config/entry/pages.rb
new file mode 100644
index 00000000000..57d9e944f51
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/pages.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ ##
+ # Entry that represents the pages path prefix
+ # Entry that represents the pages attributes
+ #
+ class Pages < ::Gitlab::Config::Entry::Node
+ ALLOWED_KEYS = %i[path_prefix].freeze
+
+ include ::Gitlab::Config::Entry::Attributable
+ include ::Gitlab::Config::Entry::Validatable
+
+ attributes ALLOWED_KEYS
+
+ validations do
+ validates :config, type: Hash
+ validates :config, allowed_keys: ALLOWED_KEYS
+
+ with_options allow_nil: true do
+ validates :path_prefix, type: String
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/processable.rb b/lib/gitlab/ci/config/entry/processable.rb
index 88734ac1186..d0e9a9afc51 100644
--- a/lib/gitlab/ci/config/entry/processable.rb
+++ b/lib/gitlab/ci/config/entry/processable.rb
@@ -25,6 +25,8 @@ module Gitlab
validates :name, type: Symbol
validates :name, length: { maximum: 255 }
+ validates :config, mutually_exclusive_keys: %i[script trigger]
+
validates :config, disallowed_keys: {
in: %i[only except start_in],
message: 'key may not be used with `rules`',
diff --git a/lib/gitlab/ci/config/header/input.rb b/lib/gitlab/ci/config/header/input.rb
index dcb96006459..08ee70b6290 100644
--- a/lib/gitlab/ci/config/header/input.rb
+++ b/lib/gitlab/ci/config/header/input.rb
@@ -11,17 +11,24 @@ module Gitlab
include ::Gitlab::Config::Entry::Validatable
include ::Gitlab::Config::Entry::Attributable
- ALLOWED_KEYS = %i[default description regex type].freeze
+ ALLOWED_KEYS = %i[default description options regex type].freeze
+ ALLOWED_OPTIONS_LIMIT = 50
attributes ALLOWED_KEYS, prefix: :input
validations do
validates :config, type: Hash, allowed_keys: ALLOWED_KEYS
validates :key, alphanumeric: true
- validates :input_default, alphanumeric: true, allow_nil: true
validates :input_description, alphanumeric: true, allow_nil: true
validates :input_regex, type: String, allow_nil: true
validates :input_type, allow_nil: true, allowed_values: Interpolation::Inputs.input_types
+ validates :input_options, type: Array, allow_nil: true
+
+ validate do
+ if input_options&.size.to_i > ALLOWED_OPTIONS_LIMIT
+ errors.add(:config, "cannot define more than #{ALLOWED_OPTIONS_LIMIT} options")
+ end
+ end
end
end
end
diff --git a/lib/gitlab/ci/config/interpolation/inputs/base_input.rb b/lib/gitlab/ci/config/interpolation/inputs/base_input.rb
index ba519776635..987268b0525 100644
--- a/lib/gitlab/ci/config/interpolation/inputs/base_input.rb
+++ b/lib/gitlab/ci/config/interpolation/inputs/base_input.rb
@@ -20,11 +20,6 @@ module Gitlab
raise NotImplementedError
end
- # Checks whether the provided value is of the given type
- def valid_value?(value)
- raise NotImplementedError
- end
-
attr_reader :errors, :name, :spec, :value
def initialize(name:, spec:, value:)
@@ -54,20 +49,39 @@ module Gitlab
private
def validate!
- return error('required value has not been provided') if required_input? && value.nil?
+ validate_required
+
+ return if errors.present?
- # validate default value
- if !required_input? && !valid_value?(default)
- return error("default value is not a #{self.class.type_name}")
- end
+ run_validations(default, default: true) unless required_input?
- # validate provided value
- return error("provided value is not a #{self.class.type_name}") unless valid_value?(actual_value)
+ run_validations(value) unless value.nil?
+ end
+
+ def validate_required
+ error('required value has not been provided') if required_input? && value.nil?
+ end
- validate_regex!
+ def run_validations(value, default: false)
+ validate_type(value, default)
+ validate_options(value)
+ validate_regex(value, default)
end
- def validate_regex!
+ # Type validations are done separately for different input types.
+ def validate_type(_value, _default)
+ raise NotImplementedError
+ end
+
+ # Options can be either StringInput or NumberInput and are validated accordingly.
+ def validate_options(_value)
+ return unless options
+
+ error('Options can only be used with string and number inputs')
+ end
+
+ # Regex can be only be a StringInput and is validated accordingly.
+ def validate_regex(_value, _default)
return unless spec.key?(:regex)
error('RegEx validation can only be used with string inputs')
@@ -96,6 +110,10 @@ module Gitlab
def default
spec[:default]
end
+
+ def options
+ spec[:options]
+ end
end
end
end
diff --git a/lib/gitlab/ci/config/interpolation/inputs/boolean_input.rb b/lib/gitlab/ci/config/interpolation/inputs/boolean_input.rb
index 0293c01a5a8..4c34f7e7fdd 100644
--- a/lib/gitlab/ci/config/interpolation/inputs/boolean_input.rb
+++ b/lib/gitlab/ci/config/interpolation/inputs/boolean_input.rb
@@ -6,6 +6,8 @@ module Gitlab
module Interpolation
class Inputs
class BooleanInput < BaseInput
+ extend ::Gitlab::Utils::Override
+
def self.matches?(spec)
spec.is_a?(Hash) && spec[:type] == type_name
end
@@ -14,8 +16,11 @@ module Gitlab
'boolean'
end
- def valid_value?(value)
- [true, false].include?(value)
+ override :validate_type
+ def validate_type(value, default)
+ return if [true, false].include?(value)
+
+ error("#{default ? 'default' : 'provided'} value is not a boolean")
end
end
end
diff --git a/lib/gitlab/ci/config/interpolation/inputs/number_input.rb b/lib/gitlab/ci/config/interpolation/inputs/number_input.rb
index 314315d2b6d..59bc057749a 100644
--- a/lib/gitlab/ci/config/interpolation/inputs/number_input.rb
+++ b/lib/gitlab/ci/config/interpolation/inputs/number_input.rb
@@ -6,6 +6,8 @@ module Gitlab
module Interpolation
class Inputs
class NumberInput < BaseInput
+ extend ::Gitlab::Utils::Override
+
def self.matches?(spec)
spec.is_a?(Hash) && spec[:type] == type_name
end
@@ -14,8 +16,19 @@ module Gitlab
'number'
end
- def valid_value?(value)
- value.is_a?(Numeric)
+ override :validate_type
+ def validate_type(value, default)
+ return if value.is_a?(Numeric)
+
+ error("#{default ? 'default' : 'provided'} value is not a number")
+ end
+
+ override :validate_options
+ def validate_options(value)
+ return unless options && value
+ return if options.include?(value)
+
+ error("`#{value}` cannot be used because it is not in the list of the allowed options")
end
end
end
diff --git a/lib/gitlab/ci/config/interpolation/inputs/string_input.rb b/lib/gitlab/ci/config/interpolation/inputs/string_input.rb
index 3f40e851f11..01b9d34a883 100644
--- a/lib/gitlab/ci/config/interpolation/inputs/string_input.rb
+++ b/lib/gitlab/ci/config/interpolation/inputs/string_input.rb
@@ -6,6 +6,8 @@ module Gitlab
module Interpolation
class Inputs
class StringInput < BaseInput
+ extend ::Gitlab::Utils::Override
+
def self.matches?(spec)
# The input spec can be `nil` when using a minimal specification
# and also when `type` is not specified.
@@ -22,24 +24,32 @@ module Gitlab
'string'
end
- def valid_value?(value)
- value.nil? || value.is_a?(String)
+ override :validate_type
+ def validate_type(value, default)
+ return if value.is_a?(String)
+
+ error("#{default ? 'default' : 'provided'} value is not a string")
+ end
+
+ override :validate_options
+ def validate_options(value)
+ return unless options && value
+ return if options.include?(value)
+
+ error("`#{value}` cannot be used because it is not in the list of allowed options")
end
private
- def validate_regex!
+ override :validate_regex
+ def validate_regex(value, default)
return unless spec.key?(:regex)
safe_regex = ::Gitlab::UntrustedRegexp.new(spec[:regex])
- return if safe_regex.match?(actual_value)
+ return if safe_regex.match?(value)
- if value.nil?
- error('default value does not match required RegEx pattern')
- else
- error('provided value does not match required RegEx pattern')
- end
+ error("#{default ? 'default' : 'provided'} value does not match required RegEx pattern")
rescue RegexpError
error('invalid regular expression')
end
diff --git a/lib/gitlab/ci/jwt_v2.rb b/lib/gitlab/ci/jwt_v2.rb
index 29beba4774a..90db9d13d85 100644
--- a/lib/gitlab/ci/jwt_v2.rb
+++ b/lib/gitlab/ci/jwt_v2.rb
@@ -25,13 +25,26 @@ module Gitlab
def reserved_claims
super.merge({
- iss: Settings.gitlab.base_url,
+ iss: Feature.enabled?(:oidc_issuer_url) ? Gitlab.config.gitlab.url : Settings.gitlab.base_url,
sub: "project_path:#{project.full_path}:ref_type:#{ref_type}:ref:#{source_ref}",
- aud: aud,
- user_identities: user_identities
+ aud: aud
}.compact)
end
+ def custom_claims
+ additional_custom_claims = {
+ runner_id: runner&.id,
+ runner_environment: runner_environment,
+ sha: pipeline.sha,
+ project_visibility: Gitlab::VisibilityLevel.string_level(project.visibility_level),
+ user_identities: user_identities
+ }.compact
+
+ mapper = ClaimMapper.new(project_config, pipeline)
+
+ super.merge(additional_custom_claims).merge(mapper.to_h)
+ end
+
def user_identities
return unless user&.pass_user_identities_to_ci_jwt
@@ -43,17 +56,6 @@ module Gitlab
end
end
- def custom_claims
- mapper = ClaimMapper.new(project_config, pipeline)
-
- super.merge({
- runner_id: runner&.id,
- runner_environment: runner_environment,
- sha: pipeline.sha,
- project_visibility: Gitlab::VisibilityLevel.string_level(project.visibility_level)
- }).merge(mapper.to_h)
- end
-
def project_config
Gitlab::Ci::ProjectConfig.new(
project: project,
diff --git a/lib/gitlab/ci/parsers/sbom/cyclonedx_properties.rb b/lib/gitlab/ci/parsers/sbom/cyclonedx_properties.rb
index 3dc73544208..35548358c57 100644
--- a/lib/gitlab/ci/parsers/sbom/cyclonedx_properties.rb
+++ b/lib/gitlab/ci/parsers/sbom/cyclonedx_properties.rb
@@ -15,7 +15,8 @@ module Gitlab
SUPPORTED_SCHEMA_VERSION = '1'
GITLAB_PREFIX = 'gitlab:'
SOURCE_PARSERS = {
- 'dependency_scanning' => ::Gitlab::Ci::Parsers::Sbom::Source::DependencyScanning
+ 'dependency_scanning' => ::Gitlab::Ci::Parsers::Sbom::Source::DependencyScanning,
+ 'container_scanning' => ::Gitlab::Ci::Parsers::Sbom::Source::ContainerScanning
}.freeze
SUPPORTED_PROPERTIES = %w[
meta:schema_version
@@ -24,6 +25,10 @@ module Gitlab
dependency_scanning:source_file:path
dependency_scanning:package_manager:name
dependency_scanning:language:name
+ container_scanning:image:name
+ container_scanning:image:tag
+ container_scanning:operating_system:name
+ container_scanning:operating_system:version
].freeze
def self.parse_source(...)
diff --git a/lib/gitlab/ci/parsers/sbom/source/base_source.rb b/lib/gitlab/ci/parsers/sbom/source/base_source.rb
new file mode 100644
index 00000000000..744555aa25a
--- /dev/null
+++ b/lib/gitlab/ci/parsers/sbom/source/base_source.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Parsers
+ module Sbom
+ module Source
+ class BaseSource
+ REQUIRED_ATTRIBUTES = [].freeze
+
+ def self.source(...)
+ new(...).source
+ end
+
+ def initialize(data)
+ @data = data
+ end
+
+ def source
+ return unless required_attributes_present?
+
+ ::Gitlab::Ci::Reports::Sbom::Source.new(
+ type: type,
+ data: data
+ )
+ end
+
+ private
+
+ attr_reader :data
+
+ # Implement in child class
+ # returns a symbol of the source type
+ def type; end
+
+ def required_attributes_present?
+ self.class::REQUIRED_ATTRIBUTES.all? do |keys|
+ data.dig(*keys).present?
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/parsers/sbom/source/container_scanning.rb b/lib/gitlab/ci/parsers/sbom/source/container_scanning.rb
new file mode 100644
index 00000000000..33f9631c424
--- /dev/null
+++ b/lib/gitlab/ci/parsers/sbom/source/container_scanning.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Parsers
+ module Sbom
+ module Source
+ class ContainerScanning < BaseSource
+ REQUIRED_ATTRIBUTES = [
+ %w[image name],
+ %w[image tag]
+ ].freeze
+
+ OPERATING_SYSTEM_ATTRIBUTES = [
+ %w[operating_system name],
+ %w[operating_system version]
+ ].freeze
+
+ private
+
+ def type
+ :container_scanning
+ end
+
+ def required_attributes_present?
+ operating_system_attributes_valid? && super
+ end
+
+ def operating_system_attributes_valid?
+ return true if data['operating_system'].blank?
+
+ OPERATING_SYSTEM_ATTRIBUTES.all? do |keys|
+ data.dig(*keys).present?
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/parsers/sbom/source/dependency_scanning.rb b/lib/gitlab/ci/parsers/sbom/source/dependency_scanning.rb
index c76a4309779..fc5a7606e39 100644
--- a/lib/gitlab/ci/parsers/sbom/source/dependency_scanning.rb
+++ b/lib/gitlab/ci/parsers/sbom/source/dependency_scanning.rb
@@ -5,36 +5,15 @@ module Gitlab
module Parsers
module Sbom
module Source
- class DependencyScanning
+ class DependencyScanning < BaseSource
REQUIRED_ATTRIBUTES = [
%w[input_file path]
].freeze
- def self.source(...)
- new(...).source
- end
-
- def initialize(data)
- @data = data
- end
-
- def source
- return unless required_attributes_present?
-
- ::Gitlab::Ci::Reports::Sbom::Source.new(
- type: :dependency_scanning,
- data: data
- )
- end
-
private
- attr_reader :data
-
- def required_attributes_present?
- REQUIRED_ATTRIBUTES.all? do |keys|
- data.dig(*keys).present?
- end
+ def type
+ :dependency_scanning
end
end
end
diff --git a/lib/gitlab/ci/parsers/security/common.rb b/lib/gitlab/ci/parsers/security/common.rb
index 9032faa66d4..be6c6c2558b 100644
--- a/lib/gitlab/ci/parsers/security/common.rb
+++ b/lib/gitlab/ci/parsers/security/common.rb
@@ -141,7 +141,7 @@ module Gitlab
project_id: @project.id,
found_by_pipeline: report.pipeline,
vulnerability_finding_signatures_enabled: @signatures_enabled,
- cvss: data['cvss'] || []
+ cvss: data['cvss_vectors'] || []
)
)
end
diff --git a/lib/gitlab/ci/parsers/security/validators/schema_validator.rb b/lib/gitlab/ci/parsers/security/validators/schema_validator.rb
index e39482481c7..e2a8044b708 100644
--- a/lib/gitlab/ci/parsers/security/validators/schema_validator.rb
+++ b/lib/gitlab/ci/parsers/security/validators/schema_validator.rb
@@ -7,14 +7,14 @@ module Gitlab
module Validators
class SchemaValidator
SUPPORTED_VERSIONS = {
- cluster_image_scanning: %w[15.0.0 15.0.1 15.0.2 15.0.4 15.0.5 15.0.6],
- container_scanning: %w[15.0.0 15.0.1 15.0.2 15.0.4 15.0.5 15.0.6],
- coverage_fuzzing: %w[15.0.0 15.0.1 15.0.2 15.0.4 15.0.5 15.0.6],
- dast: %w[15.0.0 15.0.1 15.0.2 15.0.4 15.0.5 15.0.6],
- api_fuzzing: %w[15.0.0 15.0.1 15.0.2 15.0.4 15.0.5 15.0.6],
- dependency_scanning: %w[15.0.0 15.0.1 15.0.2 15.0.4 15.0.5 15.0.6],
- sast: %w[15.0.0 15.0.1 15.0.2 15.0.4 15.0.5 15.0.6],
- secret_detection: %w[15.0.0 15.0.1 15.0.2 15.0.4 15.0.5 15.0.6]
+ cluster_image_scanning: %w[15.0.0 15.0.1 15.0.2 15.0.4 15.0.5 15.0.6 15.0.7],
+ container_scanning: %w[15.0.0 15.0.1 15.0.2 15.0.4 15.0.5 15.0.6 15.0.7],
+ coverage_fuzzing: %w[15.0.0 15.0.1 15.0.2 15.0.4 15.0.5 15.0.6 15.0.7],
+ dast: %w[15.0.0 15.0.1 15.0.2 15.0.4 15.0.5 15.0.6 15.0.7],
+ api_fuzzing: %w[15.0.0 15.0.1 15.0.2 15.0.4 15.0.5 15.0.6 15.0.7],
+ dependency_scanning: %w[15.0.0 15.0.1 15.0.2 15.0.4 15.0.5 15.0.6 15.0.7],
+ sast: %w[15.0.0 15.0.1 15.0.2 15.0.4 15.0.5 15.0.6 15.0.7],
+ secret_detection: %w[15.0.0 15.0.1 15.0.2 15.0.4 15.0.5 15.0.6 15.0.7]
}.freeze
VERSIONS_TO_REMOVE_IN_17_0 = %w[].freeze
diff --git a/lib/gitlab/ci/parsers/security/validators/schemas/15.0.7/cluster-image-scanning-report-format.json b/lib/gitlab/ci/parsers/security/validators/schemas/15.0.7/cluster-image-scanning-report-format.json
new file mode 100644
index 00000000000..e27096d071f
--- /dev/null
+++ b/lib/gitlab/ci/parsers/security/validators/schemas/15.0.7/cluster-image-scanning-report-format.json
@@ -0,0 +1,1085 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/raw/master/dist/cluster-image-scanning-report-format.json",
+ "title": "Report format for GitLab Cluster Image Scanning",
+ "description": "This schema provides the the report format for Cluster Image Scanning (https://docs.gitlab.com/ee/user/application_security/cluster_image_scanning/).",
+ "definitions": {
+ "detail_type": {
+ "oneOf": [
+ {
+ "$ref": "#/definitions/named_list"
+ },
+ {
+ "$ref": "#/definitions/list"
+ },
+ {
+ "$ref": "#/definitions/table"
+ },
+ {
+ "$ref": "#/definitions/text"
+ },
+ {
+ "$ref": "#/definitions/url"
+ },
+ {
+ "$ref": "#/definitions/code"
+ },
+ {
+ "$ref": "#/definitions/value"
+ },
+ {
+ "$ref": "#/definitions/diff"
+ },
+ {
+ "$ref": "#/definitions/markdown"
+ },
+ {
+ "$ref": "#/definitions/commit"
+ },
+ {
+ "$ref": "#/definitions/file_location"
+ },
+ {
+ "$ref": "#/definitions/module_location"
+ }
+ ]
+ },
+ "text_value": {
+ "type": "string"
+ },
+ "named_field": {
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "$ref": "#/definitions/text_value",
+ "type": "string",
+ "minLength": 1
+ },
+ "description": {
+ "$ref": "#/definitions/text_value"
+ }
+ }
+ },
+ "named_list": {
+ "type": "object",
+ "description": "An object with named and typed fields",
+ "required": [
+ "type",
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "named-list"
+ },
+ "items": {
+ "type": "object",
+ "patternProperties": {
+ "^.*$": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/named_field"
+ },
+ {
+ "$ref": "#/definitions/detail_type"
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "list": {
+ "type": "object",
+ "description": "A list of typed fields",
+ "required": [
+ "type",
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "list"
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ }
+ }
+ },
+ "table": {
+ "type": "object",
+ "description": "A table of typed fields",
+ "required": [
+ "type",
+ "rows"
+ ],
+ "properties": {
+ "type": {
+ "const": "table"
+ },
+ "header": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ },
+ "rows": {
+ "type": "array",
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ }
+ }
+ }
+ },
+ "text": {
+ "type": "object",
+ "description": "Raw text",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "text"
+ },
+ "value": {
+ "$ref": "#/definitions/text_value"
+ }
+ }
+ },
+ "url": {
+ "type": "object",
+ "description": "A single URL",
+ "required": [
+ "type",
+ "href"
+ ],
+ "properties": {
+ "type": {
+ "const": "url"
+ },
+ "text": {
+ "$ref": "#/definitions/text_value"
+ },
+ "href": {
+ "type": "string",
+ "minLength": 1,
+ "examples": [
+ "http://mysite.com"
+ ]
+ }
+ }
+ },
+ "code": {
+ "type": "object",
+ "description": "A codeblock",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "code"
+ },
+ "value": {
+ "type": "string"
+ },
+ "lang": {
+ "type": "string",
+ "description": "A programming language"
+ }
+ }
+ },
+ "value": {
+ "type": "object",
+ "description": "A field that can store a range of types of value",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "value"
+ },
+ "value": {
+ "type": [
+ "number",
+ "string",
+ "boolean"
+ ]
+ }
+ }
+ },
+ "diff": {
+ "type": "object",
+ "description": "A diff",
+ "required": [
+ "type",
+ "before",
+ "after"
+ ],
+ "properties": {
+ "type": {
+ "const": "diff"
+ },
+ "before": {
+ "type": "string"
+ },
+ "after": {
+ "type": "string"
+ }
+ }
+ },
+ "markdown": {
+ "type": "object",
+ "description": "GitLab flavoured markdown, see https://docs.gitlab.com/ee/user/markdown.html",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "markdown"
+ },
+ "value": {
+ "$ref": "#/definitions/text_value",
+ "examples": [
+ "Here is markdown `inline code` #1 [test](gitlab.com)\n\n![GitLab Logo](https://about.gitlab.com/images/press/logo/preview/gitlab-logo-white-preview.png)"
+ ]
+ }
+ }
+ },
+ "commit": {
+ "type": "object",
+ "description": "A commit/tag/branch within the GitLab project",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "commit"
+ },
+ "value": {
+ "type": "string",
+ "description": "The commit SHA",
+ "minLength": 1
+ }
+ }
+ },
+ "file_location": {
+ "type": "object",
+ "description": "A location within a file in the project",
+ "required": [
+ "type",
+ "file_name",
+ "line_start"
+ ],
+ "properties": {
+ "type": {
+ "const": "file-location"
+ },
+ "file_name": {
+ "type": "string",
+ "minLength": 1
+ },
+ "line_start": {
+ "type": "integer"
+ },
+ "line_end": {
+ "type": "integer"
+ }
+ }
+ },
+ "module_location": {
+ "type": "object",
+ "description": "A location within a binary module of the form module+relative_offset",
+ "required": [
+ "type",
+ "module_name",
+ "offset"
+ ],
+ "properties": {
+ "type": {
+ "const": "module-location"
+ },
+ "module_name": {
+ "type": "string",
+ "minLength": 1,
+ "examples": [
+ "compiled_binary"
+ ]
+ },
+ "offset": {
+ "type": "integer",
+ "examples": [
+ 100
+ ]
+ }
+ }
+ }
+ },
+ "self": {
+ "version": "15.0.7"
+ },
+ "type": "object",
+ "required": [
+ "scan",
+ "version",
+ "vulnerabilities"
+ ],
+ "additionalProperties": true,
+ "properties": {
+ "scan": {
+ "type": "object",
+ "required": [
+ "analyzer",
+ "end_time",
+ "scanner",
+ "start_time",
+ "status",
+ "type"
+ ],
+ "properties": {
+ "end_time": {
+ "type": "string",
+ "description": "ISO8601 UTC value with format yyyy-mm-ddThh:mm:ss, representing when the scan finished.",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$",
+ "examples": [
+ "2020-01-28T03:26:02"
+ ]
+ },
+ "messages": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Communication intended for the initiator of a scan.",
+ "required": [
+ "level",
+ "value"
+ ],
+ "properties": {
+ "level": {
+ "type": "string",
+ "description": "Describes the severity of the communication. Use info to communicate normal scan behaviour; warn to communicate a potentially recoverable problem, or a partial error; fatal to communicate an issue that causes the scan to halt.",
+ "enum": [
+ "info",
+ "warn",
+ "fatal"
+ ],
+ "examples": [
+ "info"
+ ]
+ },
+ "value": {
+ "type": "string",
+ "description": "The message to communicate.",
+ "minLength": 1,
+ "examples": [
+ "Permission denied, scanning aborted"
+ ]
+ }
+ }
+ }
+ },
+ "options": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "A configuration option used for this scan.",
+ "required": [
+ "name",
+ "value"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The configuration option name.",
+ "maxLength": 255,
+ "minLength": 1,
+ "examples": [
+ "DAST_FF_ENABLE_BAS",
+ "DOCKER_TLS_CERTDIR",
+ "DS_MAX_DEPTH",
+ "SECURE_LOG_LEVEL"
+ ]
+ },
+ "source": {
+ "type": "string",
+ "description": "The source of this option.",
+ "enum": [
+ "argument",
+ "file",
+ "env_variable",
+ "other"
+ ]
+ },
+ "value": {
+ "type": [
+ "boolean",
+ "integer",
+ "null",
+ "string"
+ ],
+ "description": "The value used for this scan.",
+ "examples": [
+ true,
+ 2,
+ null,
+ "fatal",
+ ""
+ ]
+ }
+ }
+ }
+ },
+ "analyzer": {
+ "type": "object",
+ "description": "Object defining the analyzer used to perform the scan. Analyzers typically delegate to an underlying scanner to run the scan.",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "vendor"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique id that identifies the analyzer.",
+ "minLength": 1,
+ "examples": [
+ "gitlab-dast"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "description": "A human readable value that identifies the analyzer, not required to be unique.",
+ "minLength": 1,
+ "examples": [
+ "GitLab DAST"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "pattern": "^https?://.+",
+ "description": "A link to more information about the analyzer.",
+ "examples": [
+ "https://docs.gitlab.com/ee/user/application_security/dast"
+ ]
+ },
+ "vendor": {
+ "description": "The vendor/maintainer of the analyzer.",
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the vendor.",
+ "minLength": 1,
+ "examples": [
+ "GitLab"
+ ]
+ }
+ }
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the analyzer.",
+ "minLength": 1,
+ "examples": [
+ "1.0.2"
+ ]
+ }
+ }
+ },
+ "scanner": {
+ "type": "object",
+ "description": "Object defining the scanner used to perform the scan.",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "vendor"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique id that identifies the scanner.",
+ "minLength": 1,
+ "examples": [
+ "my-sast-scanner"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "description": "A human readable value that identifies the scanner, not required to be unique.",
+ "minLength": 1,
+ "examples": [
+ "My SAST Scanner"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "description": "A link to more information about the scanner.",
+ "examples": [
+ "https://scanner.url"
+ ]
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the scanner.",
+ "minLength": 1,
+ "examples": [
+ "1.0.2"
+ ]
+ },
+ "vendor": {
+ "description": "The vendor/maintainer of the scanner.",
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the vendor.",
+ "minLength": 1,
+ "examples": [
+ "GitLab"
+ ]
+ }
+ }
+ }
+ }
+ },
+ "start_time": {
+ "type": "string",
+ "description": "ISO8601 UTC value with format yyyy-mm-ddThh:mm:ss, representing when the scan started.",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$",
+ "examples": [
+ "2020-02-14T16:01:59"
+ ]
+ },
+ "status": {
+ "type": "string",
+ "description": "Result of the scan.",
+ "enum": [
+ "success",
+ "failure"
+ ]
+ },
+ "type": {
+ "type": "string",
+ "description": "Type of the scan.",
+ "enum": [
+ "cluster_image_scanning"
+ ]
+ },
+ "primary_identifiers": {
+ "type": "array",
+ "description": "An unordered array containing an exhaustive list of primary identifiers for which the analyzer may return results",
+ "items": {
+ "type": "object",
+ "required": [
+ "type",
+ "name",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "for example, cve, cwe, osvdb, usn, or an analyzer-dependent type such as gemnasium).",
+ "minLength": 1
+ },
+ "name": {
+ "type": "string",
+ "description": "Human-readable name of the identifier.",
+ "minLength": 1
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the identifier's documentation.",
+ "pattern": "^(https?|ftp)://.+"
+ },
+ "value": {
+ "type": "string",
+ "description": "Value of the identifier, for matching purpose.",
+ "minLength": 1
+ }
+ }
+ }
+ }
+ }
+ },
+ "schema": {
+ "type": "string",
+ "description": "URI pointing to the validating security report schema.",
+ "pattern": "^https?://.+"
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the schema to which the JSON report conforms.",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ },
+ "vulnerabilities": {
+ "type": "array",
+ "description": "Array of vulnerability objects.",
+ "items": {
+ "type": "object",
+ "description": "Describes the vulnerability using GitLab Flavored Markdown",
+ "required": [
+ "id",
+ "identifiers",
+ "location"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Unique identifier of the vulnerability. This is recommended to be a UUID.",
+ "examples": [
+ "642735a5-1425-428d-8d4e-3c854885a3c9"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "maxLength": 255,
+ "description": "The name of the vulnerability. This must not include the finding's specific information."
+ },
+ "description": {
+ "type": "string",
+ "maxLength": 1048576,
+ "description": "A long text section describing the vulnerability more fully."
+ },
+ "severity": {
+ "type": "string",
+ "description": "How much the vulnerability impacts the software. Possible values are Info, Unknown, Low, Medium, High, or Critical. Note that some analyzers may not report all these possible values.",
+ "enum": [
+ "Info",
+ "Unknown",
+ "Low",
+ "Medium",
+ "High",
+ "Critical"
+ ]
+ },
+ "solution": {
+ "type": "string",
+ "maxLength": 7000,
+ "description": "Explanation of how to fix the vulnerability."
+ },
+ "identifiers": {
+ "type": "array",
+ "minItems": 1,
+ "description": "An ordered array of references that identify a vulnerability on internal or external databases. The first identifier is the Primary Identifier, which has special meaning.",
+ "items": {
+ "type": "object",
+ "required": [
+ "type",
+ "name",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "for example, cve, cwe, osvdb, usn, or an analyzer-dependent type such as gemnasium).",
+ "minLength": 1
+ },
+ "name": {
+ "type": "string",
+ "description": "Human-readable name of the identifier.",
+ "minLength": 1
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the identifier's documentation.",
+ "pattern": "^(https?|ftp)://.+"
+ },
+ "value": {
+ "type": "string",
+ "description": "Value of the identifier, for matching purpose.",
+ "minLength": 1
+ }
+ }
+ }
+ },
+ "cvss_vectors": {
+ "type": "array",
+ "minItems": 1,
+ "maxItems": 10,
+ "description": "An ordered array of CVSS vectors, each issued by a vendor to rate the vulnerability. The first item in the array is used as the primary CVSS vector, and is used to filter and sort the vulnerability.",
+ "items": {
+ "oneOf": [
+ {
+ "type": "object",
+ "properties": {
+ "vendor": {
+ "type": "string",
+ "minLength": 1,
+ "default": "unknown"
+ },
+ "vector": {
+ "type": "string",
+ "minLength": 16,
+ "maxLength": 128,
+ "pattern": "^((AV:[NAL]|AC:[LMH]|Au:[MSN]|[CIA]:[NPC]|E:(U|POC|F|H|ND)|RL:(OF|TF|W|U|ND)|RC:(UC|UR|C|ND)|CDP:(N|L|LM|MH|H|ND)|TD:(N|L|M|H|ND)|[CIA]R:(L|M|H|ND))/)*(AV:[NAL]|AC:[LMH]|Au:[MSN]|[CIA]:[NPC]|E:(U|POC|F|H|ND)|RL:(OF|TF|W|U|ND)|RC:(UC|UR|C|ND)|CDP:(N|L|LM|MH|H|ND)|TD:(N|L|M|H|ND)|[CIA]R:(L|M|H|ND))$"
+ }
+ },
+ "required": [
+ "vendor",
+ "vector"
+ ]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "vendor": {
+ "type": "string",
+ "minLength": 1,
+ "default": "unknown"
+ },
+ "vector": {
+ "type": "string",
+ "minLength": 32,
+ "maxLength": 128,
+ "pattern": "^CVSS:3[.][01]/((AV:[NALP]|AC:[LH]|PR:[NLH]|UI:[NR]|S:[UC]|[CIA]:[NLH]|E:[XUPFH]|RL:[XOTWU]|RC:[XURC]|[CIA]R:[XLMH]|MAV:[XNALP]|MAC:[XLH]|MPR:[XNLH]|MUI:[XNR]|MS:[XUC]|M[CIA]:[XNLH])/)*(AV:[NALP]|AC:[LH]|PR:[NLH]|UI:[NR]|S:[UC]|[CIA]:[NLH]|E:[XUPFH]|RL:[XOTWU]|RC:[XURC]|[CIA]R:[XLMH]|MAV:[XNALP]|MAC:[XLH]|MPR:[XNLH]|MUI:[XNR]|MS:[XUC]|M[CIA]:[XNLH])$"
+ }
+ },
+ "required": [
+ "vendor",
+ "vector"
+ ]
+ }
+ ]
+ }
+ },
+ "links": {
+ "type": "array",
+ "description": "An array of references to external documentation or articles that describe the vulnerability.",
+ "items": {
+ "type": "object",
+ "required": [
+ "url"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Name of the vulnerability details link."
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the vulnerability details document.",
+ "pattern": "^(https?|ftp)://.+"
+ }
+ }
+ }
+ },
+ "details": {
+ "$ref": "#/definitions/named_list/properties/items"
+ },
+ "tracking": {
+ "type": "object",
+ "description": "Describes how this vulnerability should be tracked as the project changes.",
+ "oneOf": [
+ {
+ "description": "Declares that a series of items should be tracked using source-specific tracking methods.",
+ "required": [
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "source"
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "description": "An item that should be tracked using source-specific tracking methods.",
+ "type": "object",
+ "required": [
+ "signatures"
+ ],
+ "properties": {
+ "file": {
+ "type": "string",
+ "description": "Path to the file where the vulnerability is located."
+ },
+ "start_line": {
+ "type": "number",
+ "description": "The first line of the file that includes the vulnerability."
+ },
+ "end_line": {
+ "type": "number",
+ "description": "The last line of the file that includes the vulnerability."
+ },
+ "signatures": {
+ "type": "array",
+ "description": "An array of calculated tracking signatures for this tracking item.",
+ "minItems": 1,
+ "items": {
+ "description": "A calculated tracking signature value and metadata.",
+ "type": "object",
+ "required": [
+ "algorithm",
+ "value"
+ ],
+ "properties": {
+ "algorithm": {
+ "type": "string",
+ "description": "The algorithm used to generate the signature."
+ },
+ "value": {
+ "type": "string",
+ "description": "The result of this signature algorithm."
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "Each tracking type must declare its own type."
+ }
+ }
+ },
+ "flags": {
+ "description": "Flags that can be attached to vulnerabilities.",
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Informational flags identified and assigned to a vulnerability.",
+ "required": [
+ "type",
+ "origin",
+ "description"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Result of the scan.",
+ "enum": [
+ "flagged-as-likely-false-positive"
+ ]
+ },
+ "origin": {
+ "minLength": 1,
+ "description": "Tool that issued the flag.",
+ "type": "string"
+ },
+ "description": {
+ "minLength": 1,
+ "description": "What the flag is about.",
+ "type": "string"
+ }
+ }
+ }
+ },
+ "location": {
+ "type": "object",
+ "description": "Identifies the vulnerability's location.",
+ "required": [
+ "dependency",
+ "image",
+ "kubernetes_resource"
+ ],
+ "properties": {
+ "dependency": {
+ "type": "object",
+ "description": "Describes the dependency of a project where the vulnerability is located.",
+ "required": [
+ "package",
+ "version"
+ ],
+ "properties": {
+ "package": {
+ "type": "object",
+ "description": "Provides information on the package where the vulnerability is located.",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Name of the package where the vulnerability is located."
+ }
+ }
+ },
+ "version": {
+ "type": "string",
+ "description": "Version of the vulnerable package."
+ },
+ "iid": {
+ "description": "ID that identifies the dependency in the scope of a dependency file.",
+ "type": "number"
+ },
+ "direct": {
+ "type": "boolean",
+ "description": "Tells whether this is a direct, top-level dependency of the scanned project."
+ },
+ "dependency_path": {
+ "type": "array",
+ "description": "Ancestors of the dependency, starting from a direct project dependency, and ending with an immediate parent of the dependency. The dependency itself is excluded from the path. Direct dependencies have no path.",
+ "items": {
+ "type": "object",
+ "required": [
+ "iid"
+ ],
+ "properties": {
+ "iid": {
+ "type": "number",
+ "description": "ID that is unique in the scope of a parent object, and specific to the resource type."
+ }
+ }
+ }
+ }
+ }
+ },
+ "operating_system": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "description": "The operating system that contains the vulnerable package."
+ },
+ "image": {
+ "type": "string",
+ "minLength": 1,
+ "description": "The analyzed Docker image.",
+ "examples": [
+ "index.docker.io/library/nginx:1.21"
+ ]
+ },
+ "kubernetes_resource": {
+ "type": "object",
+ "description": "The specific Kubernetes resource that was scanned.",
+ "required": [
+ "namespace",
+ "kind",
+ "name",
+ "container_name"
+ ],
+ "properties": {
+ "namespace": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "description": "The Kubernetes namespace the resource that had its image scanned.",
+ "examples": [
+ "default",
+ "staging",
+ "production"
+ ]
+ },
+ "kind": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "description": "The Kubernetes kind the resource that had its image scanned.",
+ "examples": [
+ "Deployment",
+ "DaemonSet"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "description": "The name of the resource that had its image scanned.",
+ "examples": [
+ "nginx-ingress"
+ ]
+ },
+ "container_name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "description": "The name of the container that had its image scanned.",
+ "examples": [
+ "nginx"
+ ]
+ },
+ "agent_id": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "description": "The GitLab ID of the Kubernetes Agent which performed the scan.",
+ "examples": [
+ "1234"
+ ]
+ },
+ "cluster_id": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "description": "The GitLab ID of the Kubernetes cluster when using cluster integration.",
+ "examples": [
+ "1234"
+ ]
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "remediations": {
+ "type": "array",
+ "description": "An array of objects containing information on available remediations, along with patch diffs to apply.",
+ "items": {
+ "type": "object",
+ "required": [
+ "fixes",
+ "summary",
+ "diff"
+ ],
+ "properties": {
+ "fixes": {
+ "type": "array",
+ "description": "An array of strings that represent references to vulnerabilities fixed by this remediation.",
+ "items": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Unique identifier of the vulnerability. This is recommended to be a UUID.",
+ "examples": [
+ "642735a5-1425-428d-8d4e-3c854885a3c9"
+ ]
+ }
+ }
+ }
+ },
+ "summary": {
+ "type": "string",
+ "minLength": 1,
+ "description": "An overview of how the vulnerabilities were fixed."
+ },
+ "diff": {
+ "type": "string",
+ "minLength": 1,
+ "description": "A base64-encoded remediation code diff, compatible with git apply."
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/lib/gitlab/ci/parsers/security/validators/schemas/15.0.7/container-scanning-report-format.json b/lib/gitlab/ci/parsers/security/validators/schemas/15.0.7/container-scanning-report-format.json
new file mode 100644
index 00000000000..94c3b3fc919
--- /dev/null
+++ b/lib/gitlab/ci/parsers/security/validators/schemas/15.0.7/container-scanning-report-format.json
@@ -0,0 +1,1017 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/raw/master/dist/container-scanning-report-format.json",
+ "title": "Report format for GitLab Container Scanning",
+ "description": "This schema provides the the report format for Container Scanning (https://docs.gitlab.com/ee/user/application_security/container_scanning).",
+ "definitions": {
+ "detail_type": {
+ "oneOf": [
+ {
+ "$ref": "#/definitions/named_list"
+ },
+ {
+ "$ref": "#/definitions/list"
+ },
+ {
+ "$ref": "#/definitions/table"
+ },
+ {
+ "$ref": "#/definitions/text"
+ },
+ {
+ "$ref": "#/definitions/url"
+ },
+ {
+ "$ref": "#/definitions/code"
+ },
+ {
+ "$ref": "#/definitions/value"
+ },
+ {
+ "$ref": "#/definitions/diff"
+ },
+ {
+ "$ref": "#/definitions/markdown"
+ },
+ {
+ "$ref": "#/definitions/commit"
+ },
+ {
+ "$ref": "#/definitions/file_location"
+ },
+ {
+ "$ref": "#/definitions/module_location"
+ }
+ ]
+ },
+ "text_value": {
+ "type": "string"
+ },
+ "named_field": {
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "$ref": "#/definitions/text_value",
+ "type": "string",
+ "minLength": 1
+ },
+ "description": {
+ "$ref": "#/definitions/text_value"
+ }
+ }
+ },
+ "named_list": {
+ "type": "object",
+ "description": "An object with named and typed fields",
+ "required": [
+ "type",
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "named-list"
+ },
+ "items": {
+ "type": "object",
+ "patternProperties": {
+ "^.*$": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/named_field"
+ },
+ {
+ "$ref": "#/definitions/detail_type"
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "list": {
+ "type": "object",
+ "description": "A list of typed fields",
+ "required": [
+ "type",
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "list"
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ }
+ }
+ },
+ "table": {
+ "type": "object",
+ "description": "A table of typed fields",
+ "required": [
+ "type",
+ "rows"
+ ],
+ "properties": {
+ "type": {
+ "const": "table"
+ },
+ "header": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ },
+ "rows": {
+ "type": "array",
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ }
+ }
+ }
+ },
+ "text": {
+ "type": "object",
+ "description": "Raw text",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "text"
+ },
+ "value": {
+ "$ref": "#/definitions/text_value"
+ }
+ }
+ },
+ "url": {
+ "type": "object",
+ "description": "A single URL",
+ "required": [
+ "type",
+ "href"
+ ],
+ "properties": {
+ "type": {
+ "const": "url"
+ },
+ "text": {
+ "$ref": "#/definitions/text_value"
+ },
+ "href": {
+ "type": "string",
+ "minLength": 1,
+ "examples": [
+ "http://mysite.com"
+ ]
+ }
+ }
+ },
+ "code": {
+ "type": "object",
+ "description": "A codeblock",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "code"
+ },
+ "value": {
+ "type": "string"
+ },
+ "lang": {
+ "type": "string",
+ "description": "A programming language"
+ }
+ }
+ },
+ "value": {
+ "type": "object",
+ "description": "A field that can store a range of types of value",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "value"
+ },
+ "value": {
+ "type": [
+ "number",
+ "string",
+ "boolean"
+ ]
+ }
+ }
+ },
+ "diff": {
+ "type": "object",
+ "description": "A diff",
+ "required": [
+ "type",
+ "before",
+ "after"
+ ],
+ "properties": {
+ "type": {
+ "const": "diff"
+ },
+ "before": {
+ "type": "string"
+ },
+ "after": {
+ "type": "string"
+ }
+ }
+ },
+ "markdown": {
+ "type": "object",
+ "description": "GitLab flavoured markdown, see https://docs.gitlab.com/ee/user/markdown.html",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "markdown"
+ },
+ "value": {
+ "$ref": "#/definitions/text_value",
+ "examples": [
+ "Here is markdown `inline code` #1 [test](gitlab.com)\n\n![GitLab Logo](https://about.gitlab.com/images/press/logo/preview/gitlab-logo-white-preview.png)"
+ ]
+ }
+ }
+ },
+ "commit": {
+ "type": "object",
+ "description": "A commit/tag/branch within the GitLab project",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "commit"
+ },
+ "value": {
+ "type": "string",
+ "description": "The commit SHA",
+ "minLength": 1
+ }
+ }
+ },
+ "file_location": {
+ "type": "object",
+ "description": "A location within a file in the project",
+ "required": [
+ "type",
+ "file_name",
+ "line_start"
+ ],
+ "properties": {
+ "type": {
+ "const": "file-location"
+ },
+ "file_name": {
+ "type": "string",
+ "minLength": 1
+ },
+ "line_start": {
+ "type": "integer"
+ },
+ "line_end": {
+ "type": "integer"
+ }
+ }
+ },
+ "module_location": {
+ "type": "object",
+ "description": "A location within a binary module of the form module+relative_offset",
+ "required": [
+ "type",
+ "module_name",
+ "offset"
+ ],
+ "properties": {
+ "type": {
+ "const": "module-location"
+ },
+ "module_name": {
+ "type": "string",
+ "minLength": 1,
+ "examples": [
+ "compiled_binary"
+ ]
+ },
+ "offset": {
+ "type": "integer",
+ "examples": [
+ 100
+ ]
+ }
+ }
+ }
+ },
+ "self": {
+ "version": "15.0.7"
+ },
+ "type": "object",
+ "required": [
+ "scan",
+ "version",
+ "vulnerabilities"
+ ],
+ "additionalProperties": true,
+ "properties": {
+ "scan": {
+ "type": "object",
+ "required": [
+ "analyzer",
+ "end_time",
+ "scanner",
+ "start_time",
+ "status",
+ "type"
+ ],
+ "properties": {
+ "end_time": {
+ "type": "string",
+ "description": "ISO8601 UTC value with format yyyy-mm-ddThh:mm:ss, representing when the scan finished.",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$",
+ "examples": [
+ "2020-01-28T03:26:02"
+ ]
+ },
+ "messages": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Communication intended for the initiator of a scan.",
+ "required": [
+ "level",
+ "value"
+ ],
+ "properties": {
+ "level": {
+ "type": "string",
+ "description": "Describes the severity of the communication. Use info to communicate normal scan behaviour; warn to communicate a potentially recoverable problem, or a partial error; fatal to communicate an issue that causes the scan to halt.",
+ "enum": [
+ "info",
+ "warn",
+ "fatal"
+ ],
+ "examples": [
+ "info"
+ ]
+ },
+ "value": {
+ "type": "string",
+ "description": "The message to communicate.",
+ "minLength": 1,
+ "examples": [
+ "Permission denied, scanning aborted"
+ ]
+ }
+ }
+ }
+ },
+ "options": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "A configuration option used for this scan.",
+ "required": [
+ "name",
+ "value"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The configuration option name.",
+ "maxLength": 255,
+ "minLength": 1,
+ "examples": [
+ "DAST_FF_ENABLE_BAS",
+ "DOCKER_TLS_CERTDIR",
+ "DS_MAX_DEPTH",
+ "SECURE_LOG_LEVEL"
+ ]
+ },
+ "source": {
+ "type": "string",
+ "description": "The source of this option.",
+ "enum": [
+ "argument",
+ "file",
+ "env_variable",
+ "other"
+ ]
+ },
+ "value": {
+ "type": [
+ "boolean",
+ "integer",
+ "null",
+ "string"
+ ],
+ "description": "The value used for this scan.",
+ "examples": [
+ true,
+ 2,
+ null,
+ "fatal",
+ ""
+ ]
+ }
+ }
+ }
+ },
+ "analyzer": {
+ "type": "object",
+ "description": "Object defining the analyzer used to perform the scan. Analyzers typically delegate to an underlying scanner to run the scan.",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "vendor"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique id that identifies the analyzer.",
+ "minLength": 1,
+ "examples": [
+ "gitlab-dast"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "description": "A human readable value that identifies the analyzer, not required to be unique.",
+ "minLength": 1,
+ "examples": [
+ "GitLab DAST"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "pattern": "^https?://.+",
+ "description": "A link to more information about the analyzer.",
+ "examples": [
+ "https://docs.gitlab.com/ee/user/application_security/dast"
+ ]
+ },
+ "vendor": {
+ "description": "The vendor/maintainer of the analyzer.",
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the vendor.",
+ "minLength": 1,
+ "examples": [
+ "GitLab"
+ ]
+ }
+ }
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the analyzer.",
+ "minLength": 1,
+ "examples": [
+ "1.0.2"
+ ]
+ }
+ }
+ },
+ "scanner": {
+ "type": "object",
+ "description": "Object defining the scanner used to perform the scan.",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "vendor"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique id that identifies the scanner.",
+ "minLength": 1,
+ "examples": [
+ "my-sast-scanner"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "description": "A human readable value that identifies the scanner, not required to be unique.",
+ "minLength": 1,
+ "examples": [
+ "My SAST Scanner"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "description": "A link to more information about the scanner.",
+ "examples": [
+ "https://scanner.url"
+ ]
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the scanner.",
+ "minLength": 1,
+ "examples": [
+ "1.0.2"
+ ]
+ },
+ "vendor": {
+ "description": "The vendor/maintainer of the scanner.",
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the vendor.",
+ "minLength": 1,
+ "examples": [
+ "GitLab"
+ ]
+ }
+ }
+ }
+ }
+ },
+ "start_time": {
+ "type": "string",
+ "description": "ISO8601 UTC value with format yyyy-mm-ddThh:mm:ss, representing when the scan started.",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$",
+ "examples": [
+ "2020-02-14T16:01:59"
+ ]
+ },
+ "status": {
+ "type": "string",
+ "description": "Result of the scan.",
+ "enum": [
+ "success",
+ "failure"
+ ]
+ },
+ "type": {
+ "type": "string",
+ "description": "Type of the scan.",
+ "enum": [
+ "container_scanning"
+ ]
+ },
+ "primary_identifiers": {
+ "type": "array",
+ "description": "An unordered array containing an exhaustive list of primary identifiers for which the analyzer may return results",
+ "items": {
+ "type": "object",
+ "required": [
+ "type",
+ "name",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "for example, cve, cwe, osvdb, usn, or an analyzer-dependent type such as gemnasium).",
+ "minLength": 1
+ },
+ "name": {
+ "type": "string",
+ "description": "Human-readable name of the identifier.",
+ "minLength": 1
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the identifier's documentation.",
+ "pattern": "^(https?|ftp)://.+"
+ },
+ "value": {
+ "type": "string",
+ "description": "Value of the identifier, for matching purpose.",
+ "minLength": 1
+ }
+ }
+ }
+ }
+ }
+ },
+ "schema": {
+ "type": "string",
+ "description": "URI pointing to the validating security report schema.",
+ "pattern": "^https?://.+"
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the schema to which the JSON report conforms.",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ },
+ "vulnerabilities": {
+ "type": "array",
+ "description": "Array of vulnerability objects.",
+ "items": {
+ "type": "object",
+ "description": "Describes the vulnerability using GitLab Flavored Markdown",
+ "required": [
+ "id",
+ "identifiers",
+ "location"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Unique identifier of the vulnerability. This is recommended to be a UUID.",
+ "examples": [
+ "642735a5-1425-428d-8d4e-3c854885a3c9"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "maxLength": 255,
+ "description": "The name of the vulnerability. This must not include the finding's specific information."
+ },
+ "description": {
+ "type": "string",
+ "maxLength": 1048576,
+ "description": "A long text section describing the vulnerability more fully."
+ },
+ "severity": {
+ "type": "string",
+ "description": "How much the vulnerability impacts the software. Possible values are Info, Unknown, Low, Medium, High, or Critical. Note that some analyzers may not report all these possible values.",
+ "enum": [
+ "Info",
+ "Unknown",
+ "Low",
+ "Medium",
+ "High",
+ "Critical"
+ ]
+ },
+ "solution": {
+ "type": "string",
+ "maxLength": 7000,
+ "description": "Explanation of how to fix the vulnerability."
+ },
+ "identifiers": {
+ "type": "array",
+ "minItems": 1,
+ "description": "An ordered array of references that identify a vulnerability on internal or external databases. The first identifier is the Primary Identifier, which has special meaning.",
+ "items": {
+ "type": "object",
+ "required": [
+ "type",
+ "name",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "for example, cve, cwe, osvdb, usn, or an analyzer-dependent type such as gemnasium).",
+ "minLength": 1
+ },
+ "name": {
+ "type": "string",
+ "description": "Human-readable name of the identifier.",
+ "minLength": 1
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the identifier's documentation.",
+ "pattern": "^(https?|ftp)://.+"
+ },
+ "value": {
+ "type": "string",
+ "description": "Value of the identifier, for matching purpose.",
+ "minLength": 1
+ }
+ }
+ }
+ },
+ "cvss_vectors": {
+ "type": "array",
+ "minItems": 1,
+ "maxItems": 10,
+ "description": "An ordered array of CVSS vectors, each issued by a vendor to rate the vulnerability. The first item in the array is used as the primary CVSS vector, and is used to filter and sort the vulnerability.",
+ "items": {
+ "oneOf": [
+ {
+ "type": "object",
+ "properties": {
+ "vendor": {
+ "type": "string",
+ "minLength": 1,
+ "default": "unknown"
+ },
+ "vector": {
+ "type": "string",
+ "minLength": 16,
+ "maxLength": 128,
+ "pattern": "^((AV:[NAL]|AC:[LMH]|Au:[MSN]|[CIA]:[NPC]|E:(U|POC|F|H|ND)|RL:(OF|TF|W|U|ND)|RC:(UC|UR|C|ND)|CDP:(N|L|LM|MH|H|ND)|TD:(N|L|M|H|ND)|[CIA]R:(L|M|H|ND))/)*(AV:[NAL]|AC:[LMH]|Au:[MSN]|[CIA]:[NPC]|E:(U|POC|F|H|ND)|RL:(OF|TF|W|U|ND)|RC:(UC|UR|C|ND)|CDP:(N|L|LM|MH|H|ND)|TD:(N|L|M|H|ND)|[CIA]R:(L|M|H|ND))$"
+ }
+ },
+ "required": [
+ "vendor",
+ "vector"
+ ]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "vendor": {
+ "type": "string",
+ "minLength": 1,
+ "default": "unknown"
+ },
+ "vector": {
+ "type": "string",
+ "minLength": 32,
+ "maxLength": 128,
+ "pattern": "^CVSS:3[.][01]/((AV:[NALP]|AC:[LH]|PR:[NLH]|UI:[NR]|S:[UC]|[CIA]:[NLH]|E:[XUPFH]|RL:[XOTWU]|RC:[XURC]|[CIA]R:[XLMH]|MAV:[XNALP]|MAC:[XLH]|MPR:[XNLH]|MUI:[XNR]|MS:[XUC]|M[CIA]:[XNLH])/)*(AV:[NALP]|AC:[LH]|PR:[NLH]|UI:[NR]|S:[UC]|[CIA]:[NLH]|E:[XUPFH]|RL:[XOTWU]|RC:[XURC]|[CIA]R:[XLMH]|MAV:[XNALP]|MAC:[XLH]|MPR:[XNLH]|MUI:[XNR]|MS:[XUC]|M[CIA]:[XNLH])$"
+ }
+ },
+ "required": [
+ "vendor",
+ "vector"
+ ]
+ }
+ ]
+ }
+ },
+ "links": {
+ "type": "array",
+ "description": "An array of references to external documentation or articles that describe the vulnerability.",
+ "items": {
+ "type": "object",
+ "required": [
+ "url"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Name of the vulnerability details link."
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the vulnerability details document.",
+ "pattern": "^(https?|ftp)://.+"
+ }
+ }
+ }
+ },
+ "details": {
+ "$ref": "#/definitions/named_list/properties/items"
+ },
+ "tracking": {
+ "type": "object",
+ "description": "Describes how this vulnerability should be tracked as the project changes.",
+ "oneOf": [
+ {
+ "description": "Declares that a series of items should be tracked using source-specific tracking methods.",
+ "required": [
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "source"
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "description": "An item that should be tracked using source-specific tracking methods.",
+ "type": "object",
+ "required": [
+ "signatures"
+ ],
+ "properties": {
+ "file": {
+ "type": "string",
+ "description": "Path to the file where the vulnerability is located."
+ },
+ "start_line": {
+ "type": "number",
+ "description": "The first line of the file that includes the vulnerability."
+ },
+ "end_line": {
+ "type": "number",
+ "description": "The last line of the file that includes the vulnerability."
+ },
+ "signatures": {
+ "type": "array",
+ "description": "An array of calculated tracking signatures for this tracking item.",
+ "minItems": 1,
+ "items": {
+ "description": "A calculated tracking signature value and metadata.",
+ "type": "object",
+ "required": [
+ "algorithm",
+ "value"
+ ],
+ "properties": {
+ "algorithm": {
+ "type": "string",
+ "description": "The algorithm used to generate the signature."
+ },
+ "value": {
+ "type": "string",
+ "description": "The result of this signature algorithm."
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "Each tracking type must declare its own type."
+ }
+ }
+ },
+ "flags": {
+ "description": "Flags that can be attached to vulnerabilities.",
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Informational flags identified and assigned to a vulnerability.",
+ "required": [
+ "type",
+ "origin",
+ "description"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Result of the scan.",
+ "enum": [
+ "flagged-as-likely-false-positive"
+ ]
+ },
+ "origin": {
+ "minLength": 1,
+ "description": "Tool that issued the flag.",
+ "type": "string"
+ },
+ "description": {
+ "minLength": 1,
+ "description": "What the flag is about.",
+ "type": "string"
+ }
+ }
+ }
+ },
+ "location": {
+ "type": "object",
+ "description": "Identifies the vulnerability's location.",
+ "required": [
+ "dependency",
+ "operating_system",
+ "image"
+ ],
+ "properties": {
+ "dependency": {
+ "type": "object",
+ "description": "Describes the dependency of a project where the vulnerability is located.",
+ "required": [
+ "package",
+ "version"
+ ],
+ "properties": {
+ "package": {
+ "type": "object",
+ "description": "Provides information on the package where the vulnerability is located.",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Name of the package where the vulnerability is located."
+ }
+ }
+ },
+ "version": {
+ "type": "string",
+ "description": "Version of the vulnerable package."
+ },
+ "iid": {
+ "description": "ID that identifies the dependency in the scope of a dependency file.",
+ "type": "number"
+ },
+ "direct": {
+ "type": "boolean",
+ "description": "Tells whether this is a direct, top-level dependency of the scanned project."
+ },
+ "dependency_path": {
+ "type": "array",
+ "description": "Ancestors of the dependency, starting from a direct project dependency, and ending with an immediate parent of the dependency. The dependency itself is excluded from the path. Direct dependencies have no path.",
+ "items": {
+ "type": "object",
+ "required": [
+ "iid"
+ ],
+ "properties": {
+ "iid": {
+ "type": "number",
+ "description": "ID that is unique in the scope of a parent object, and specific to the resource type."
+ }
+ }
+ }
+ }
+ }
+ },
+ "operating_system": {
+ "type": "string",
+ "minLength": 1,
+ "description": "The operating system that contains the vulnerable package."
+ },
+ "image": {
+ "type": "string",
+ "minLength": 1,
+ "description": "The analyzed Docker image."
+ },
+ "default_branch_image": {
+ "type": "string",
+ "maxLength": 255,
+ "description": "The name of the image on the default branch."
+ }
+ }
+ }
+ }
+ }
+ },
+ "remediations": {
+ "type": "array",
+ "description": "An array of objects containing information on available remediations, along with patch diffs to apply.",
+ "items": {
+ "type": "object",
+ "required": [
+ "fixes",
+ "summary",
+ "diff"
+ ],
+ "properties": {
+ "fixes": {
+ "type": "array",
+ "description": "An array of strings that represent references to vulnerabilities fixed by this remediation.",
+ "items": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Unique identifier of the vulnerability. This is recommended to be a UUID.",
+ "examples": [
+ "642735a5-1425-428d-8d4e-3c854885a3c9"
+ ]
+ }
+ }
+ }
+ },
+ "summary": {
+ "type": "string",
+ "minLength": 1,
+ "description": "An overview of how the vulnerabilities were fixed."
+ },
+ "diff": {
+ "type": "string",
+ "minLength": 1,
+ "description": "A base64-encoded remediation code diff, compatible with git apply."
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/lib/gitlab/ci/parsers/security/validators/schemas/15.0.7/coverage-fuzzing-report-format.json b/lib/gitlab/ci/parsers/security/validators/schemas/15.0.7/coverage-fuzzing-report-format.json
new file mode 100644
index 00000000000..e15fbc3ed56
--- /dev/null
+++ b/lib/gitlab/ci/parsers/security/validators/schemas/15.0.7/coverage-fuzzing-report-format.json
@@ -0,0 +1,975 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/raw/master/dist/coverage-fuzzing-report-format.json",
+ "title": "Report format for GitLab Fuzz Testing",
+ "description": "This schema provides the report format for Coverage Guided Fuzz Testing (https://docs.gitlab.com/ee/user/application_security/coverage_fuzzing).",
+ "definitions": {
+ "detail_type": {
+ "oneOf": [
+ {
+ "$ref": "#/definitions/named_list"
+ },
+ {
+ "$ref": "#/definitions/list"
+ },
+ {
+ "$ref": "#/definitions/table"
+ },
+ {
+ "$ref": "#/definitions/text"
+ },
+ {
+ "$ref": "#/definitions/url"
+ },
+ {
+ "$ref": "#/definitions/code"
+ },
+ {
+ "$ref": "#/definitions/value"
+ },
+ {
+ "$ref": "#/definitions/diff"
+ },
+ {
+ "$ref": "#/definitions/markdown"
+ },
+ {
+ "$ref": "#/definitions/commit"
+ },
+ {
+ "$ref": "#/definitions/file_location"
+ },
+ {
+ "$ref": "#/definitions/module_location"
+ }
+ ]
+ },
+ "text_value": {
+ "type": "string"
+ },
+ "named_field": {
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "$ref": "#/definitions/text_value",
+ "type": "string",
+ "minLength": 1
+ },
+ "description": {
+ "$ref": "#/definitions/text_value"
+ }
+ }
+ },
+ "named_list": {
+ "type": "object",
+ "description": "An object with named and typed fields",
+ "required": [
+ "type",
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "named-list"
+ },
+ "items": {
+ "type": "object",
+ "patternProperties": {
+ "^.*$": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/named_field"
+ },
+ {
+ "$ref": "#/definitions/detail_type"
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "list": {
+ "type": "object",
+ "description": "A list of typed fields",
+ "required": [
+ "type",
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "list"
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ }
+ }
+ },
+ "table": {
+ "type": "object",
+ "description": "A table of typed fields",
+ "required": [
+ "type",
+ "rows"
+ ],
+ "properties": {
+ "type": {
+ "const": "table"
+ },
+ "header": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ },
+ "rows": {
+ "type": "array",
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ }
+ }
+ }
+ },
+ "text": {
+ "type": "object",
+ "description": "Raw text",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "text"
+ },
+ "value": {
+ "$ref": "#/definitions/text_value"
+ }
+ }
+ },
+ "url": {
+ "type": "object",
+ "description": "A single URL",
+ "required": [
+ "type",
+ "href"
+ ],
+ "properties": {
+ "type": {
+ "const": "url"
+ },
+ "text": {
+ "$ref": "#/definitions/text_value"
+ },
+ "href": {
+ "type": "string",
+ "minLength": 1,
+ "examples": [
+ "http://mysite.com"
+ ]
+ }
+ }
+ },
+ "code": {
+ "type": "object",
+ "description": "A codeblock",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "code"
+ },
+ "value": {
+ "type": "string"
+ },
+ "lang": {
+ "type": "string",
+ "description": "A programming language"
+ }
+ }
+ },
+ "value": {
+ "type": "object",
+ "description": "A field that can store a range of types of value",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "value"
+ },
+ "value": {
+ "type": [
+ "number",
+ "string",
+ "boolean"
+ ]
+ }
+ }
+ },
+ "diff": {
+ "type": "object",
+ "description": "A diff",
+ "required": [
+ "type",
+ "before",
+ "after"
+ ],
+ "properties": {
+ "type": {
+ "const": "diff"
+ },
+ "before": {
+ "type": "string"
+ },
+ "after": {
+ "type": "string"
+ }
+ }
+ },
+ "markdown": {
+ "type": "object",
+ "description": "GitLab flavoured markdown, see https://docs.gitlab.com/ee/user/markdown.html",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "markdown"
+ },
+ "value": {
+ "$ref": "#/definitions/text_value",
+ "examples": [
+ "Here is markdown `inline code` #1 [test](gitlab.com)\n\n![GitLab Logo](https://about.gitlab.com/images/press/logo/preview/gitlab-logo-white-preview.png)"
+ ]
+ }
+ }
+ },
+ "commit": {
+ "type": "object",
+ "description": "A commit/tag/branch within the GitLab project",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "commit"
+ },
+ "value": {
+ "type": "string",
+ "description": "The commit SHA",
+ "minLength": 1
+ }
+ }
+ },
+ "file_location": {
+ "type": "object",
+ "description": "A location within a file in the project",
+ "required": [
+ "type",
+ "file_name",
+ "line_start"
+ ],
+ "properties": {
+ "type": {
+ "const": "file-location"
+ },
+ "file_name": {
+ "type": "string",
+ "minLength": 1
+ },
+ "line_start": {
+ "type": "integer"
+ },
+ "line_end": {
+ "type": "integer"
+ }
+ }
+ },
+ "module_location": {
+ "type": "object",
+ "description": "A location within a binary module of the form module+relative_offset",
+ "required": [
+ "type",
+ "module_name",
+ "offset"
+ ],
+ "properties": {
+ "type": {
+ "const": "module-location"
+ },
+ "module_name": {
+ "type": "string",
+ "minLength": 1,
+ "examples": [
+ "compiled_binary"
+ ]
+ },
+ "offset": {
+ "type": "integer",
+ "examples": [
+ 100
+ ]
+ }
+ }
+ }
+ },
+ "self": {
+ "version": "15.0.7"
+ },
+ "type": "object",
+ "required": [
+ "scan",
+ "version",
+ "vulnerabilities"
+ ],
+ "additionalProperties": true,
+ "properties": {
+ "scan": {
+ "type": "object",
+ "required": [
+ "analyzer",
+ "end_time",
+ "scanner",
+ "start_time",
+ "status",
+ "type"
+ ],
+ "properties": {
+ "end_time": {
+ "type": "string",
+ "description": "ISO8601 UTC value with format yyyy-mm-ddThh:mm:ss, representing when the scan finished.",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$",
+ "examples": [
+ "2020-01-28T03:26:02"
+ ]
+ },
+ "messages": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Communication intended for the initiator of a scan.",
+ "required": [
+ "level",
+ "value"
+ ],
+ "properties": {
+ "level": {
+ "type": "string",
+ "description": "Describes the severity of the communication. Use info to communicate normal scan behaviour; warn to communicate a potentially recoverable problem, or a partial error; fatal to communicate an issue that causes the scan to halt.",
+ "enum": [
+ "info",
+ "warn",
+ "fatal"
+ ],
+ "examples": [
+ "info"
+ ]
+ },
+ "value": {
+ "type": "string",
+ "description": "The message to communicate.",
+ "minLength": 1,
+ "examples": [
+ "Permission denied, scanning aborted"
+ ]
+ }
+ }
+ }
+ },
+ "options": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "A configuration option used for this scan.",
+ "required": [
+ "name",
+ "value"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The configuration option name.",
+ "maxLength": 255,
+ "minLength": 1,
+ "examples": [
+ "DAST_FF_ENABLE_BAS",
+ "DOCKER_TLS_CERTDIR",
+ "DS_MAX_DEPTH",
+ "SECURE_LOG_LEVEL"
+ ]
+ },
+ "source": {
+ "type": "string",
+ "description": "The source of this option.",
+ "enum": [
+ "argument",
+ "file",
+ "env_variable",
+ "other"
+ ]
+ },
+ "value": {
+ "type": [
+ "boolean",
+ "integer",
+ "null",
+ "string"
+ ],
+ "description": "The value used for this scan.",
+ "examples": [
+ true,
+ 2,
+ null,
+ "fatal",
+ ""
+ ]
+ }
+ }
+ }
+ },
+ "analyzer": {
+ "type": "object",
+ "description": "Object defining the analyzer used to perform the scan. Analyzers typically delegate to an underlying scanner to run the scan.",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "vendor"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique id that identifies the analyzer.",
+ "minLength": 1,
+ "examples": [
+ "gitlab-dast"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "description": "A human readable value that identifies the analyzer, not required to be unique.",
+ "minLength": 1,
+ "examples": [
+ "GitLab DAST"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "pattern": "^https?://.+",
+ "description": "A link to more information about the analyzer.",
+ "examples": [
+ "https://docs.gitlab.com/ee/user/application_security/dast"
+ ]
+ },
+ "vendor": {
+ "description": "The vendor/maintainer of the analyzer.",
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the vendor.",
+ "minLength": 1,
+ "examples": [
+ "GitLab"
+ ]
+ }
+ }
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the analyzer.",
+ "minLength": 1,
+ "examples": [
+ "1.0.2"
+ ]
+ }
+ }
+ },
+ "scanner": {
+ "type": "object",
+ "description": "Object defining the scanner used to perform the scan.",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "vendor"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique id that identifies the scanner.",
+ "minLength": 1,
+ "examples": [
+ "my-sast-scanner"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "description": "A human readable value that identifies the scanner, not required to be unique.",
+ "minLength": 1,
+ "examples": [
+ "My SAST Scanner"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "description": "A link to more information about the scanner.",
+ "examples": [
+ "https://scanner.url"
+ ]
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the scanner.",
+ "minLength": 1,
+ "examples": [
+ "1.0.2"
+ ]
+ },
+ "vendor": {
+ "description": "The vendor/maintainer of the scanner.",
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the vendor.",
+ "minLength": 1,
+ "examples": [
+ "GitLab"
+ ]
+ }
+ }
+ }
+ }
+ },
+ "start_time": {
+ "type": "string",
+ "description": "ISO8601 UTC value with format yyyy-mm-ddThh:mm:ss, representing when the scan started.",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$",
+ "examples": [
+ "2020-02-14T16:01:59"
+ ]
+ },
+ "status": {
+ "type": "string",
+ "description": "Result of the scan.",
+ "enum": [
+ "success",
+ "failure"
+ ]
+ },
+ "type": {
+ "type": "string",
+ "description": "Type of the scan.",
+ "enum": [
+ "coverage_fuzzing"
+ ]
+ },
+ "primary_identifiers": {
+ "type": "array",
+ "description": "An unordered array containing an exhaustive list of primary identifiers for which the analyzer may return results",
+ "items": {
+ "type": "object",
+ "required": [
+ "type",
+ "name",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "for example, cve, cwe, osvdb, usn, or an analyzer-dependent type such as gemnasium).",
+ "minLength": 1
+ },
+ "name": {
+ "type": "string",
+ "description": "Human-readable name of the identifier.",
+ "minLength": 1
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the identifier's documentation.",
+ "pattern": "^(https?|ftp)://.+"
+ },
+ "value": {
+ "type": "string",
+ "description": "Value of the identifier, for matching purpose.",
+ "minLength": 1
+ }
+ }
+ }
+ }
+ }
+ },
+ "schema": {
+ "type": "string",
+ "description": "URI pointing to the validating security report schema.",
+ "pattern": "^https?://.+"
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the schema to which the JSON report conforms.",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ },
+ "vulnerabilities": {
+ "type": "array",
+ "description": "Array of vulnerability objects.",
+ "items": {
+ "type": "object",
+ "description": "Describes the vulnerability using GitLab Flavored Markdown",
+ "required": [
+ "id",
+ "identifiers",
+ "location"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Unique identifier of the vulnerability. This is recommended to be a UUID.",
+ "examples": [
+ "642735a5-1425-428d-8d4e-3c854885a3c9"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "maxLength": 255,
+ "description": "The name of the vulnerability. This must not include the finding's specific information."
+ },
+ "description": {
+ "type": "string",
+ "maxLength": 1048576,
+ "description": "A long text section describing the vulnerability more fully."
+ },
+ "severity": {
+ "type": "string",
+ "description": "How much the vulnerability impacts the software. Possible values are Info, Unknown, Low, Medium, High, or Critical. Note that some analyzers may not report all these possible values.",
+ "enum": [
+ "Info",
+ "Unknown",
+ "Low",
+ "Medium",
+ "High",
+ "Critical"
+ ]
+ },
+ "solution": {
+ "type": "string",
+ "maxLength": 7000,
+ "description": "Explanation of how to fix the vulnerability."
+ },
+ "identifiers": {
+ "type": "array",
+ "minItems": 1,
+ "description": "An ordered array of references that identify a vulnerability on internal or external databases. The first identifier is the Primary Identifier, which has special meaning.",
+ "items": {
+ "type": "object",
+ "required": [
+ "type",
+ "name",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "for example, cve, cwe, osvdb, usn, or an analyzer-dependent type such as gemnasium).",
+ "minLength": 1
+ },
+ "name": {
+ "type": "string",
+ "description": "Human-readable name of the identifier.",
+ "minLength": 1
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the identifier's documentation.",
+ "pattern": "^(https?|ftp)://.+"
+ },
+ "value": {
+ "type": "string",
+ "description": "Value of the identifier, for matching purpose.",
+ "minLength": 1
+ }
+ }
+ }
+ },
+ "cvss_vectors": {
+ "type": "array",
+ "minItems": 1,
+ "maxItems": 10,
+ "description": "An ordered array of CVSS vectors, each issued by a vendor to rate the vulnerability. The first item in the array is used as the primary CVSS vector, and is used to filter and sort the vulnerability.",
+ "items": {
+ "oneOf": [
+ {
+ "type": "object",
+ "properties": {
+ "vendor": {
+ "type": "string",
+ "minLength": 1,
+ "default": "unknown"
+ },
+ "vector": {
+ "type": "string",
+ "minLength": 16,
+ "maxLength": 128,
+ "pattern": "^((AV:[NAL]|AC:[LMH]|Au:[MSN]|[CIA]:[NPC]|E:(U|POC|F|H|ND)|RL:(OF|TF|W|U|ND)|RC:(UC|UR|C|ND)|CDP:(N|L|LM|MH|H|ND)|TD:(N|L|M|H|ND)|[CIA]R:(L|M|H|ND))/)*(AV:[NAL]|AC:[LMH]|Au:[MSN]|[CIA]:[NPC]|E:(U|POC|F|H|ND)|RL:(OF|TF|W|U|ND)|RC:(UC|UR|C|ND)|CDP:(N|L|LM|MH|H|ND)|TD:(N|L|M|H|ND)|[CIA]R:(L|M|H|ND))$"
+ }
+ },
+ "required": [
+ "vendor",
+ "vector"
+ ]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "vendor": {
+ "type": "string",
+ "minLength": 1,
+ "default": "unknown"
+ },
+ "vector": {
+ "type": "string",
+ "minLength": 32,
+ "maxLength": 128,
+ "pattern": "^CVSS:3[.][01]/((AV:[NALP]|AC:[LH]|PR:[NLH]|UI:[NR]|S:[UC]|[CIA]:[NLH]|E:[XUPFH]|RL:[XOTWU]|RC:[XURC]|[CIA]R:[XLMH]|MAV:[XNALP]|MAC:[XLH]|MPR:[XNLH]|MUI:[XNR]|MS:[XUC]|M[CIA]:[XNLH])/)*(AV:[NALP]|AC:[LH]|PR:[NLH]|UI:[NR]|S:[UC]|[CIA]:[NLH]|E:[XUPFH]|RL:[XOTWU]|RC:[XURC]|[CIA]R:[XLMH]|MAV:[XNALP]|MAC:[XLH]|MPR:[XNLH]|MUI:[XNR]|MS:[XUC]|M[CIA]:[XNLH])$"
+ }
+ },
+ "required": [
+ "vendor",
+ "vector"
+ ]
+ }
+ ]
+ }
+ },
+ "links": {
+ "type": "array",
+ "description": "An array of references to external documentation or articles that describe the vulnerability.",
+ "items": {
+ "type": "object",
+ "required": [
+ "url"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Name of the vulnerability details link."
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the vulnerability details document.",
+ "pattern": "^(https?|ftp)://.+"
+ }
+ }
+ }
+ },
+ "details": {
+ "$ref": "#/definitions/named_list/properties/items"
+ },
+ "tracking": {
+ "type": "object",
+ "description": "Describes how this vulnerability should be tracked as the project changes.",
+ "oneOf": [
+ {
+ "description": "Declares that a series of items should be tracked using source-specific tracking methods.",
+ "required": [
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "source"
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "description": "An item that should be tracked using source-specific tracking methods.",
+ "type": "object",
+ "required": [
+ "signatures"
+ ],
+ "properties": {
+ "file": {
+ "type": "string",
+ "description": "Path to the file where the vulnerability is located."
+ },
+ "start_line": {
+ "type": "number",
+ "description": "The first line of the file that includes the vulnerability."
+ },
+ "end_line": {
+ "type": "number",
+ "description": "The last line of the file that includes the vulnerability."
+ },
+ "signatures": {
+ "type": "array",
+ "description": "An array of calculated tracking signatures for this tracking item.",
+ "minItems": 1,
+ "items": {
+ "description": "A calculated tracking signature value and metadata.",
+ "type": "object",
+ "required": [
+ "algorithm",
+ "value"
+ ],
+ "properties": {
+ "algorithm": {
+ "type": "string",
+ "description": "The algorithm used to generate the signature."
+ },
+ "value": {
+ "type": "string",
+ "description": "The result of this signature algorithm."
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "Each tracking type must declare its own type."
+ }
+ }
+ },
+ "flags": {
+ "description": "Flags that can be attached to vulnerabilities.",
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Informational flags identified and assigned to a vulnerability.",
+ "required": [
+ "type",
+ "origin",
+ "description"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Result of the scan.",
+ "enum": [
+ "flagged-as-likely-false-positive"
+ ]
+ },
+ "origin": {
+ "minLength": 1,
+ "description": "Tool that issued the flag.",
+ "type": "string"
+ },
+ "description": {
+ "minLength": 1,
+ "description": "What the flag is about.",
+ "type": "string"
+ }
+ }
+ }
+ },
+ "location": {
+ "description": "The location of the error",
+ "type": "object",
+ "properties": {
+ "crash_address": {
+ "type": "string",
+ "description": "The relative address in memory were the crash occurred.",
+ "examples": [
+ "0xabababab"
+ ]
+ },
+ "stacktrace_snippet": {
+ "type": "string",
+ "description": "The stack trace recorded during fuzzing resulting the crash.",
+ "examples": [
+ "func_a+0xabcd\nfunc_b+0xabcc"
+ ]
+ },
+ "crash_state": {
+ "type": "string",
+ "description": "Minimised and normalized crash stack-trace (called crash_state).",
+ "examples": [
+ "func_a+0xa\nfunc_b+0xb\nfunc_c+0xc"
+ ]
+ },
+ "crash_type": {
+ "type": "string",
+ "description": "Type of the crash.",
+ "examples": [
+ "Heap-Buffer-overflow",
+ "Division-by-zero"
+ ]
+ }
+ }
+ }
+ }
+ }
+ },
+ "remediations": {
+ "type": "array",
+ "description": "An array of objects containing information on available remediations, along with patch diffs to apply.",
+ "items": {
+ "type": "object",
+ "required": [
+ "fixes",
+ "summary",
+ "diff"
+ ],
+ "properties": {
+ "fixes": {
+ "type": "array",
+ "description": "An array of strings that represent references to vulnerabilities fixed by this remediation.",
+ "items": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Unique identifier of the vulnerability. This is recommended to be a UUID.",
+ "examples": [
+ "642735a5-1425-428d-8d4e-3c854885a3c9"
+ ]
+ }
+ }
+ }
+ },
+ "summary": {
+ "type": "string",
+ "minLength": 1,
+ "description": "An overview of how the vulnerabilities were fixed."
+ },
+ "diff": {
+ "type": "string",
+ "minLength": 1,
+ "description": "A base64-encoded remediation code diff, compatible with git apply."
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/lib/gitlab/ci/parsers/security/validators/schemas/15.0.7/dast-report-format.json b/lib/gitlab/ci/parsers/security/validators/schemas/15.0.7/dast-report-format.json
new file mode 100644
index 00000000000..8a9519f442f
--- /dev/null
+++ b/lib/gitlab/ci/parsers/security/validators/schemas/15.0.7/dast-report-format.json
@@ -0,0 +1,1380 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/raw/master/dist/dast-report-format.json",
+ "title": "Report format for GitLab DAST",
+ "description": "This schema provides the the report format for Dynamic Application Security Testing (https://docs.gitlab.com/ee/user/application_security/dast).",
+ "definitions": {
+ "detail_type": {
+ "oneOf": [
+ {
+ "$ref": "#/definitions/named_list"
+ },
+ {
+ "$ref": "#/definitions/list"
+ },
+ {
+ "$ref": "#/definitions/table"
+ },
+ {
+ "$ref": "#/definitions/text"
+ },
+ {
+ "$ref": "#/definitions/url"
+ },
+ {
+ "$ref": "#/definitions/code"
+ },
+ {
+ "$ref": "#/definitions/value"
+ },
+ {
+ "$ref": "#/definitions/diff"
+ },
+ {
+ "$ref": "#/definitions/markdown"
+ },
+ {
+ "$ref": "#/definitions/commit"
+ },
+ {
+ "$ref": "#/definitions/file_location"
+ },
+ {
+ "$ref": "#/definitions/module_location"
+ }
+ ]
+ },
+ "text_value": {
+ "type": "string"
+ },
+ "named_field": {
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "$ref": "#/definitions/text_value",
+ "type": "string",
+ "minLength": 1
+ },
+ "description": {
+ "$ref": "#/definitions/text_value"
+ }
+ }
+ },
+ "named_list": {
+ "type": "object",
+ "description": "An object with named and typed fields",
+ "required": [
+ "type",
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "named-list"
+ },
+ "items": {
+ "type": "object",
+ "patternProperties": {
+ "^.*$": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/named_field"
+ },
+ {
+ "$ref": "#/definitions/detail_type"
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "list": {
+ "type": "object",
+ "description": "A list of typed fields",
+ "required": [
+ "type",
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "list"
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ }
+ }
+ },
+ "table": {
+ "type": "object",
+ "description": "A table of typed fields",
+ "required": [
+ "type",
+ "rows"
+ ],
+ "properties": {
+ "type": {
+ "const": "table"
+ },
+ "header": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ },
+ "rows": {
+ "type": "array",
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ }
+ }
+ }
+ },
+ "text": {
+ "type": "object",
+ "description": "Raw text",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "text"
+ },
+ "value": {
+ "$ref": "#/definitions/text_value"
+ }
+ }
+ },
+ "url": {
+ "type": "object",
+ "description": "A single URL",
+ "required": [
+ "type",
+ "href"
+ ],
+ "properties": {
+ "type": {
+ "const": "url"
+ },
+ "text": {
+ "$ref": "#/definitions/text_value"
+ },
+ "href": {
+ "type": "string",
+ "minLength": 1,
+ "examples": [
+ "http://mysite.com"
+ ]
+ }
+ }
+ },
+ "code": {
+ "type": "object",
+ "description": "A codeblock",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "code"
+ },
+ "value": {
+ "type": "string"
+ },
+ "lang": {
+ "type": "string",
+ "description": "A programming language"
+ }
+ }
+ },
+ "value": {
+ "type": "object",
+ "description": "A field that can store a range of types of value",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "value"
+ },
+ "value": {
+ "type": [
+ "number",
+ "string",
+ "boolean"
+ ]
+ }
+ }
+ },
+ "diff": {
+ "type": "object",
+ "description": "A diff",
+ "required": [
+ "type",
+ "before",
+ "after"
+ ],
+ "properties": {
+ "type": {
+ "const": "diff"
+ },
+ "before": {
+ "type": "string"
+ },
+ "after": {
+ "type": "string"
+ }
+ }
+ },
+ "markdown": {
+ "type": "object",
+ "description": "GitLab flavoured markdown, see https://docs.gitlab.com/ee/user/markdown.html",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "markdown"
+ },
+ "value": {
+ "$ref": "#/definitions/text_value",
+ "examples": [
+ "Here is markdown `inline code` #1 [test](gitlab.com)\n\n![GitLab Logo](https://about.gitlab.com/images/press/logo/preview/gitlab-logo-white-preview.png)"
+ ]
+ }
+ }
+ },
+ "commit": {
+ "type": "object",
+ "description": "A commit/tag/branch within the GitLab project",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "commit"
+ },
+ "value": {
+ "type": "string",
+ "description": "The commit SHA",
+ "minLength": 1
+ }
+ }
+ },
+ "file_location": {
+ "type": "object",
+ "description": "A location within a file in the project",
+ "required": [
+ "type",
+ "file_name",
+ "line_start"
+ ],
+ "properties": {
+ "type": {
+ "const": "file-location"
+ },
+ "file_name": {
+ "type": "string",
+ "minLength": 1
+ },
+ "line_start": {
+ "type": "integer"
+ },
+ "line_end": {
+ "type": "integer"
+ }
+ }
+ },
+ "module_location": {
+ "type": "object",
+ "description": "A location within a binary module of the form module+relative_offset",
+ "required": [
+ "type",
+ "module_name",
+ "offset"
+ ],
+ "properties": {
+ "type": {
+ "const": "module-location"
+ },
+ "module_name": {
+ "type": "string",
+ "minLength": 1,
+ "examples": [
+ "compiled_binary"
+ ]
+ },
+ "offset": {
+ "type": "integer",
+ "examples": [
+ 100
+ ]
+ }
+ }
+ }
+ },
+ "self": {
+ "version": "15.0.7"
+ },
+ "type": "object",
+ "required": [
+ "scan",
+ "version",
+ "vulnerabilities"
+ ],
+ "additionalProperties": true,
+ "properties": {
+ "scan": {
+ "type": "object",
+ "required": [
+ "analyzer",
+ "end_time",
+ "scanned_resources",
+ "scanner",
+ "start_time",
+ "status",
+ "type"
+ ],
+ "properties": {
+ "end_time": {
+ "type": "string",
+ "description": "ISO8601 UTC value with format yyyy-mm-ddThh:mm:ss, representing when the scan finished.",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$",
+ "examples": [
+ "2020-01-28T03:26:02"
+ ]
+ },
+ "messages": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Communication intended for the initiator of a scan.",
+ "required": [
+ "level",
+ "value"
+ ],
+ "properties": {
+ "level": {
+ "type": "string",
+ "description": "Describes the severity of the communication. Use info to communicate normal scan behaviour; warn to communicate a potentially recoverable problem, or a partial error; fatal to communicate an issue that causes the scan to halt.",
+ "enum": [
+ "info",
+ "warn",
+ "fatal"
+ ],
+ "examples": [
+ "info"
+ ]
+ },
+ "value": {
+ "type": "string",
+ "description": "The message to communicate.",
+ "minLength": 1,
+ "examples": [
+ "Permission denied, scanning aborted"
+ ]
+ }
+ }
+ }
+ },
+ "options": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "A configuration option used for this scan.",
+ "required": [
+ "name",
+ "value"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The configuration option name.",
+ "maxLength": 255,
+ "minLength": 1,
+ "examples": [
+ "DAST_FF_ENABLE_BAS",
+ "DOCKER_TLS_CERTDIR",
+ "DS_MAX_DEPTH",
+ "SECURE_LOG_LEVEL"
+ ]
+ },
+ "source": {
+ "type": "string",
+ "description": "The source of this option.",
+ "enum": [
+ "argument",
+ "file",
+ "env_variable",
+ "other"
+ ]
+ },
+ "value": {
+ "type": [
+ "boolean",
+ "integer",
+ "null",
+ "string"
+ ],
+ "description": "The value used for this scan.",
+ "examples": [
+ true,
+ 2,
+ null,
+ "fatal",
+ ""
+ ]
+ }
+ }
+ }
+ },
+ "analyzer": {
+ "type": "object",
+ "description": "Object defining the analyzer used to perform the scan. Analyzers typically delegate to an underlying scanner to run the scan.",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "vendor"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique id that identifies the analyzer.",
+ "minLength": 1,
+ "examples": [
+ "gitlab-dast"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "description": "A human readable value that identifies the analyzer, not required to be unique.",
+ "minLength": 1,
+ "examples": [
+ "GitLab DAST"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "pattern": "^https?://.+",
+ "description": "A link to more information about the analyzer.",
+ "examples": [
+ "https://docs.gitlab.com/ee/user/application_security/dast"
+ ]
+ },
+ "vendor": {
+ "description": "The vendor/maintainer of the analyzer.",
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the vendor.",
+ "minLength": 1,
+ "examples": [
+ "GitLab"
+ ]
+ }
+ }
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the analyzer.",
+ "minLength": 1,
+ "examples": [
+ "1.0.2"
+ ]
+ }
+ }
+ },
+ "scanner": {
+ "type": "object",
+ "description": "Object defining the scanner used to perform the scan.",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "vendor"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique id that identifies the scanner.",
+ "minLength": 1,
+ "examples": [
+ "my-sast-scanner"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "description": "A human readable value that identifies the scanner, not required to be unique.",
+ "minLength": 1,
+ "examples": [
+ "My SAST Scanner"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "description": "A link to more information about the scanner.",
+ "examples": [
+ "https://scanner.url"
+ ]
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the scanner.",
+ "minLength": 1,
+ "examples": [
+ "1.0.2"
+ ]
+ },
+ "vendor": {
+ "description": "The vendor/maintainer of the scanner.",
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the vendor.",
+ "minLength": 1,
+ "examples": [
+ "GitLab"
+ ]
+ }
+ }
+ }
+ }
+ },
+ "start_time": {
+ "type": "string",
+ "description": "ISO8601 UTC value with format yyyy-mm-ddThh:mm:ss, representing when the scan started.",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$",
+ "examples": [
+ "2020-02-14T16:01:59"
+ ]
+ },
+ "status": {
+ "type": "string",
+ "description": "Result of the scan.",
+ "enum": [
+ "success",
+ "failure"
+ ]
+ },
+ "type": {
+ "type": "string",
+ "description": "Type of the scan.",
+ "enum": [
+ "dast",
+ "api_fuzzing"
+ ]
+ },
+ "primary_identifiers": {
+ "type": "array",
+ "description": "An unordered array containing an exhaustive list of primary identifiers for which the analyzer may return results",
+ "items": {
+ "type": "object",
+ "required": [
+ "type",
+ "name",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "for example, cve, cwe, osvdb, usn, or an analyzer-dependent type such as gemnasium).",
+ "minLength": 1
+ },
+ "name": {
+ "type": "string",
+ "description": "Human-readable name of the identifier.",
+ "minLength": 1
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the identifier's documentation.",
+ "pattern": "^(https?|ftp)://.+"
+ },
+ "value": {
+ "type": "string",
+ "description": "Value of the identifier, for matching purpose.",
+ "minLength": 1
+ }
+ }
+ }
+ },
+ "scanned_resources": {
+ "type": "array",
+ "description": "The attack surface scanned by DAST.",
+ "items": {
+ "type": "object",
+ "required": [
+ "method",
+ "url",
+ "type"
+ ],
+ "properties": {
+ "method": {
+ "type": "string",
+ "minLength": 1,
+ "description": "HTTP method of the scanned resource.",
+ "examples": [
+ "GET",
+ "POST",
+ "HEAD"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "minLength": 1,
+ "description": "URL of the scanned resource.",
+ "examples": [
+ "http://my.site.com/a-page"
+ ]
+ },
+ "type": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Type of the scanned resource, for DAST, this must be 'url'.",
+ "examples": [
+ "url"
+ ]
+ }
+ }
+ }
+ }
+ }
+ },
+ "schema": {
+ "type": "string",
+ "description": "URI pointing to the validating security report schema.",
+ "pattern": "^https?://.+"
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the schema to which the JSON report conforms.",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ },
+ "vulnerabilities": {
+ "type": "array",
+ "description": "Array of vulnerability objects.",
+ "items": {
+ "type": "object",
+ "description": "Describes the vulnerability using GitLab Flavored Markdown",
+ "required": [
+ "id",
+ "identifiers",
+ "location"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Unique identifier of the vulnerability. This is recommended to be a UUID.",
+ "examples": [
+ "642735a5-1425-428d-8d4e-3c854885a3c9"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "maxLength": 255,
+ "description": "The name of the vulnerability. This must not include the finding's specific information."
+ },
+ "description": {
+ "type": "string",
+ "maxLength": 1048576,
+ "description": "A long text section describing the vulnerability more fully."
+ },
+ "severity": {
+ "type": "string",
+ "description": "How much the vulnerability impacts the software. Possible values are Info, Unknown, Low, Medium, High, or Critical. Note that some analyzers may not report all these possible values.",
+ "enum": [
+ "Info",
+ "Unknown",
+ "Low",
+ "Medium",
+ "High",
+ "Critical"
+ ]
+ },
+ "solution": {
+ "type": "string",
+ "maxLength": 7000,
+ "description": "Explanation of how to fix the vulnerability."
+ },
+ "identifiers": {
+ "type": "array",
+ "minItems": 1,
+ "description": "An ordered array of references that identify a vulnerability on internal or external databases. The first identifier is the Primary Identifier, which has special meaning.",
+ "items": {
+ "type": "object",
+ "required": [
+ "type",
+ "name",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "for example, cve, cwe, osvdb, usn, or an analyzer-dependent type such as gemnasium).",
+ "minLength": 1
+ },
+ "name": {
+ "type": "string",
+ "description": "Human-readable name of the identifier.",
+ "minLength": 1
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the identifier's documentation.",
+ "pattern": "^(https?|ftp)://.+"
+ },
+ "value": {
+ "type": "string",
+ "description": "Value of the identifier, for matching purpose.",
+ "minLength": 1
+ }
+ }
+ }
+ },
+ "cvss_vectors": {
+ "type": "array",
+ "minItems": 1,
+ "maxItems": 10,
+ "description": "An ordered array of CVSS vectors, each issued by a vendor to rate the vulnerability. The first item in the array is used as the primary CVSS vector, and is used to filter and sort the vulnerability.",
+ "items": {
+ "oneOf": [
+ {
+ "type": "object",
+ "properties": {
+ "vendor": {
+ "type": "string",
+ "minLength": 1,
+ "default": "unknown"
+ },
+ "vector": {
+ "type": "string",
+ "minLength": 16,
+ "maxLength": 128,
+ "pattern": "^((AV:[NAL]|AC:[LMH]|Au:[MSN]|[CIA]:[NPC]|E:(U|POC|F|H|ND)|RL:(OF|TF|W|U|ND)|RC:(UC|UR|C|ND)|CDP:(N|L|LM|MH|H|ND)|TD:(N|L|M|H|ND)|[CIA]R:(L|M|H|ND))/)*(AV:[NAL]|AC:[LMH]|Au:[MSN]|[CIA]:[NPC]|E:(U|POC|F|H|ND)|RL:(OF|TF|W|U|ND)|RC:(UC|UR|C|ND)|CDP:(N|L|LM|MH|H|ND)|TD:(N|L|M|H|ND)|[CIA]R:(L|M|H|ND))$"
+ }
+ },
+ "required": [
+ "vendor",
+ "vector"
+ ]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "vendor": {
+ "type": "string",
+ "minLength": 1,
+ "default": "unknown"
+ },
+ "vector": {
+ "type": "string",
+ "minLength": 32,
+ "maxLength": 128,
+ "pattern": "^CVSS:3[.][01]/((AV:[NALP]|AC:[LH]|PR:[NLH]|UI:[NR]|S:[UC]|[CIA]:[NLH]|E:[XUPFH]|RL:[XOTWU]|RC:[XURC]|[CIA]R:[XLMH]|MAV:[XNALP]|MAC:[XLH]|MPR:[XNLH]|MUI:[XNR]|MS:[XUC]|M[CIA]:[XNLH])/)*(AV:[NALP]|AC:[LH]|PR:[NLH]|UI:[NR]|S:[UC]|[CIA]:[NLH]|E:[XUPFH]|RL:[XOTWU]|RC:[XURC]|[CIA]R:[XLMH]|MAV:[XNALP]|MAC:[XLH]|MPR:[XNLH]|MUI:[XNR]|MS:[XUC]|M[CIA]:[XNLH])$"
+ }
+ },
+ "required": [
+ "vendor",
+ "vector"
+ ]
+ }
+ ]
+ }
+ },
+ "links": {
+ "type": "array",
+ "description": "An array of references to external documentation or articles that describe the vulnerability.",
+ "items": {
+ "type": "object",
+ "required": [
+ "url"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Name of the vulnerability details link."
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the vulnerability details document.",
+ "pattern": "^(https?|ftp)://.+"
+ }
+ }
+ }
+ },
+ "details": {
+ "$ref": "#/definitions/named_list/properties/items"
+ },
+ "tracking": {
+ "type": "object",
+ "description": "Describes how this vulnerability should be tracked as the project changes.",
+ "oneOf": [
+ {
+ "description": "Declares that a series of items should be tracked using source-specific tracking methods.",
+ "required": [
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "source"
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "description": "An item that should be tracked using source-specific tracking methods.",
+ "type": "object",
+ "required": [
+ "signatures"
+ ],
+ "properties": {
+ "file": {
+ "type": "string",
+ "description": "Path to the file where the vulnerability is located."
+ },
+ "start_line": {
+ "type": "number",
+ "description": "The first line of the file that includes the vulnerability."
+ },
+ "end_line": {
+ "type": "number",
+ "description": "The last line of the file that includes the vulnerability."
+ },
+ "signatures": {
+ "type": "array",
+ "description": "An array of calculated tracking signatures for this tracking item.",
+ "minItems": 1,
+ "items": {
+ "description": "A calculated tracking signature value and metadata.",
+ "type": "object",
+ "required": [
+ "algorithm",
+ "value"
+ ],
+ "properties": {
+ "algorithm": {
+ "type": "string",
+ "description": "The algorithm used to generate the signature."
+ },
+ "value": {
+ "type": "string",
+ "description": "The result of this signature algorithm."
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "Each tracking type must declare its own type."
+ }
+ }
+ },
+ "flags": {
+ "description": "Flags that can be attached to vulnerabilities.",
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Informational flags identified and assigned to a vulnerability.",
+ "required": [
+ "type",
+ "origin",
+ "description"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Result of the scan.",
+ "enum": [
+ "flagged-as-likely-false-positive"
+ ]
+ },
+ "origin": {
+ "minLength": 1,
+ "description": "Tool that issued the flag.",
+ "type": "string"
+ },
+ "description": {
+ "minLength": 1,
+ "description": "What the flag is about.",
+ "type": "string"
+ }
+ }
+ }
+ },
+ "evidence": {
+ "type": "object",
+ "properties": {
+ "source": {
+ "type": "object",
+ "description": "Source of evidence",
+ "required": [
+ "id",
+ "name"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Unique source identifier",
+ "examples": [
+ "assert:LogAnalysis",
+ "assert:StatusCode"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Source display name",
+ "examples": [
+ "Log Analysis",
+ "Status Code"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "description": "Link to additional information",
+ "examples": [
+ "https://docs.gitlab.com/ee/development/integrations/secure.html"
+ ]
+ }
+ }
+ },
+ "summary": {
+ "type": "string",
+ "description": "Human readable string containing evidence of the vulnerability.",
+ "examples": [
+ "Credit card 4111111111111111 found",
+ "Server leaked information nginx/1.17.6"
+ ]
+ },
+ "request": {
+ "type": "object",
+ "description": "An HTTP request.",
+ "required": [
+ "headers",
+ "method",
+ "url"
+ ],
+ "properties": {
+ "headers": {
+ "type": "array",
+ "description": "HTTP headers present on the request.",
+ "items": {
+ "type": "object",
+ "required": [
+ "name",
+ "value"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Name of the HTTP header.",
+ "examples": [
+ "Accept",
+ "Content-Length",
+ "Content-Type"
+ ]
+ },
+ "value": {
+ "type": "string",
+ "description": "Value of the HTTP header.",
+ "examples": [
+ "*/*",
+ "560",
+ "application/json; charset=utf-8"
+ ]
+ }
+ }
+ }
+ },
+ "method": {
+ "type": "string",
+ "minLength": 1,
+ "description": "HTTP method used in the request.",
+ "examples": [
+ "GET",
+ "POST"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "minLength": 1,
+ "description": "URL of the request.",
+ "examples": [
+ "http://my.site.com/vulnerable-endpoint?show-credit-card"
+ ]
+ },
+ "body": {
+ "type": "string",
+ "description": "Body of the request for display purposes. Body must be suitable for display (not binary), and truncated to a reasonable size.",
+ "examples": [
+ "user=jsmith&first=%27&last=smith"
+ ]
+ }
+ }
+ },
+ "response": {
+ "type": "object",
+ "description": "An HTTP response.",
+ "required": [
+ "headers",
+ "reason_phrase",
+ "status_code"
+ ],
+ "properties": {
+ "headers": {
+ "type": "array",
+ "description": "HTTP headers present on the request.",
+ "items": {
+ "type": "object",
+ "required": [
+ "name",
+ "value"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Name of the HTTP header.",
+ "examples": [
+ "Accept",
+ "Content-Length",
+ "Content-Type"
+ ]
+ },
+ "value": {
+ "type": "string",
+ "description": "Value of the HTTP header.",
+ "examples": [
+ "*/*",
+ "560",
+ "application/json; charset=utf-8"
+ ]
+ }
+ }
+ }
+ },
+ "reason_phrase": {
+ "type": "string",
+ "description": "HTTP reason phrase of the response.",
+ "examples": [
+ "OK",
+ "Internal Server Error"
+ ]
+ },
+ "status_code": {
+ "type": "integer",
+ "description": "HTTP status code of the response.",
+ "examples": [
+ 200,
+ 500
+ ]
+ },
+ "body": {
+ "type": "string",
+ "description": "Body of the response for display purposes. Body must be suitable for display (not binary), and truncated to a reasonable size.",
+ "examples": [
+ "{\"user_id\": 2}"
+ ]
+ }
+ }
+ },
+ "supporting_messages": {
+ "type": "array",
+ "description": "Array of supporting http messages.",
+ "items": {
+ "type": "object",
+ "description": "A supporting http message.",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Message display name.",
+ "examples": [
+ "Unmodified",
+ "Recorded"
+ ]
+ },
+ "request": {
+ "type": "object",
+ "description": "An HTTP request.",
+ "required": [
+ "headers",
+ "method",
+ "url"
+ ],
+ "properties": {
+ "headers": {
+ "type": "array",
+ "description": "HTTP headers present on the request.",
+ "items": {
+ "type": "object",
+ "required": [
+ "name",
+ "value"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Name of the HTTP header.",
+ "examples": [
+ "Accept",
+ "Content-Length",
+ "Content-Type"
+ ]
+ },
+ "value": {
+ "type": "string",
+ "description": "Value of the HTTP header.",
+ "examples": [
+ "*/*",
+ "560",
+ "application/json; charset=utf-8"
+ ]
+ }
+ }
+ }
+ },
+ "method": {
+ "type": "string",
+ "minLength": 1,
+ "description": "HTTP method used in the request.",
+ "examples": [
+ "GET",
+ "POST"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "minLength": 1,
+ "description": "URL of the request.",
+ "examples": [
+ "http://my.site.com/vulnerable-endpoint?show-credit-card"
+ ]
+ },
+ "body": {
+ "type": "string",
+ "description": "Body of the request for display purposes. Body must be suitable for display (not binary), and truncated to a reasonable size.",
+ "examples": [
+ "user=jsmith&first=%27&last=smith"
+ ]
+ }
+ }
+ },
+ "response": {
+ "type": "object",
+ "description": "An HTTP response.",
+ "required": [
+ "headers",
+ "reason_phrase",
+ "status_code"
+ ],
+ "properties": {
+ "headers": {
+ "type": "array",
+ "description": "HTTP headers present on the request.",
+ "items": {
+ "type": "object",
+ "required": [
+ "name",
+ "value"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Name of the HTTP header.",
+ "examples": [
+ "Accept",
+ "Content-Length",
+ "Content-Type"
+ ]
+ },
+ "value": {
+ "type": "string",
+ "description": "Value of the HTTP header.",
+ "examples": [
+ "*/*",
+ "560",
+ "application/json; charset=utf-8"
+ ]
+ }
+ }
+ }
+ },
+ "reason_phrase": {
+ "type": "string",
+ "description": "HTTP reason phrase of the response.",
+ "examples": [
+ "OK",
+ "Internal Server Error"
+ ]
+ },
+ "status_code": {
+ "type": "integer",
+ "description": "HTTP status code of the response.",
+ "examples": [
+ 200,
+ 500
+ ]
+ },
+ "body": {
+ "type": "string",
+ "description": "Body of the response for display purposes. Body must be suitable for display (not binary), and truncated to a reasonable size.",
+ "examples": [
+ "{\"user_id\": 2}"
+ ]
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "location": {
+ "type": "object",
+ "description": "Identifies the vulnerability's location.",
+ "properties": {
+ "hostname": {
+ "type": "string",
+ "description": "The protocol, domain, and port of the application where the vulnerability was found."
+ },
+ "method": {
+ "type": "string",
+ "description": "The HTTP method that was used to request the URL where the vulnerability was found."
+ },
+ "param": {
+ "type": "string",
+ "description": "A value provided by a vulnerability rule related to the found vulnerability. Examples include a header value, or a parameter used in a HTTP POST."
+ },
+ "path": {
+ "type": "string",
+ "description": "The path of the URL where the vulnerability was found. Typically, this would start with a forward slash."
+ }
+ }
+ },
+ "assets": {
+ "type": "array",
+ "description": "Array of build assets associated with vulnerability.",
+ "items": {
+ "type": "object",
+ "description": "Describes an asset associated with vulnerability.",
+ "required": [
+ "type",
+ "name",
+ "url"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "The type of asset",
+ "enum": [
+ "http_session",
+ "postman"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Display name for asset",
+ "examples": [
+ "HTTP Messages",
+ "Postman Collection"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Link to asset in build artifacts",
+ "examples": [
+ "https://gitlab.com/gitlab-org/security-products/dast/-/jobs/626397001/artifacts/file//output/zap_session.data"
+ ]
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "remediations": {
+ "type": "array",
+ "description": "An array of objects containing information on available remediations, along with patch diffs to apply.",
+ "items": {
+ "type": "object",
+ "required": [
+ "fixes",
+ "summary",
+ "diff"
+ ],
+ "properties": {
+ "fixes": {
+ "type": "array",
+ "description": "An array of strings that represent references to vulnerabilities fixed by this remediation.",
+ "items": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Unique identifier of the vulnerability. This is recommended to be a UUID.",
+ "examples": [
+ "642735a5-1425-428d-8d4e-3c854885a3c9"
+ ]
+ }
+ }
+ }
+ },
+ "summary": {
+ "type": "string",
+ "minLength": 1,
+ "description": "An overview of how the vulnerabilities were fixed."
+ },
+ "diff": {
+ "type": "string",
+ "minLength": 1,
+ "description": "A base64-encoded remediation code diff, compatible with git apply."
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/lib/gitlab/ci/parsers/security/validators/schemas/15.0.7/dependency-scanning-report-format.json b/lib/gitlab/ci/parsers/security/validators/schemas/15.0.7/dependency-scanning-report-format.json
new file mode 100644
index 00000000000..83b3537b5f1
--- /dev/null
+++ b/lib/gitlab/ci/parsers/security/validators/schemas/15.0.7/dependency-scanning-report-format.json
@@ -0,0 +1,1083 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/raw/master/dist/dependency-scanning-report-format.json",
+ "title": "Report format for GitLab Dependency Scanning",
+ "description": "This schema provides the the report format for Dependency Scanning analyzers (https://docs.gitlab.com/ee/user/application_security/dependency_scanning).",
+ "definitions": {
+ "detail_type": {
+ "oneOf": [
+ {
+ "$ref": "#/definitions/named_list"
+ },
+ {
+ "$ref": "#/definitions/list"
+ },
+ {
+ "$ref": "#/definitions/table"
+ },
+ {
+ "$ref": "#/definitions/text"
+ },
+ {
+ "$ref": "#/definitions/url"
+ },
+ {
+ "$ref": "#/definitions/code"
+ },
+ {
+ "$ref": "#/definitions/value"
+ },
+ {
+ "$ref": "#/definitions/diff"
+ },
+ {
+ "$ref": "#/definitions/markdown"
+ },
+ {
+ "$ref": "#/definitions/commit"
+ },
+ {
+ "$ref": "#/definitions/file_location"
+ },
+ {
+ "$ref": "#/definitions/module_location"
+ }
+ ]
+ },
+ "text_value": {
+ "type": "string"
+ },
+ "named_field": {
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "$ref": "#/definitions/text_value",
+ "type": "string",
+ "minLength": 1
+ },
+ "description": {
+ "$ref": "#/definitions/text_value"
+ }
+ }
+ },
+ "named_list": {
+ "type": "object",
+ "description": "An object with named and typed fields",
+ "required": [
+ "type",
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "named-list"
+ },
+ "items": {
+ "type": "object",
+ "patternProperties": {
+ "^.*$": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/named_field"
+ },
+ {
+ "$ref": "#/definitions/detail_type"
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "list": {
+ "type": "object",
+ "description": "A list of typed fields",
+ "required": [
+ "type",
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "list"
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ }
+ }
+ },
+ "table": {
+ "type": "object",
+ "description": "A table of typed fields",
+ "required": [
+ "type",
+ "rows"
+ ],
+ "properties": {
+ "type": {
+ "const": "table"
+ },
+ "header": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ },
+ "rows": {
+ "type": "array",
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ }
+ }
+ }
+ },
+ "text": {
+ "type": "object",
+ "description": "Raw text",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "text"
+ },
+ "value": {
+ "$ref": "#/definitions/text_value"
+ }
+ }
+ },
+ "url": {
+ "type": "object",
+ "description": "A single URL",
+ "required": [
+ "type",
+ "href"
+ ],
+ "properties": {
+ "type": {
+ "const": "url"
+ },
+ "text": {
+ "$ref": "#/definitions/text_value"
+ },
+ "href": {
+ "type": "string",
+ "minLength": 1,
+ "examples": [
+ "http://mysite.com"
+ ]
+ }
+ }
+ },
+ "code": {
+ "type": "object",
+ "description": "A codeblock",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "code"
+ },
+ "value": {
+ "type": "string"
+ },
+ "lang": {
+ "type": "string",
+ "description": "A programming language"
+ }
+ }
+ },
+ "value": {
+ "type": "object",
+ "description": "A field that can store a range of types of value",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "value"
+ },
+ "value": {
+ "type": [
+ "number",
+ "string",
+ "boolean"
+ ]
+ }
+ }
+ },
+ "diff": {
+ "type": "object",
+ "description": "A diff",
+ "required": [
+ "type",
+ "before",
+ "after"
+ ],
+ "properties": {
+ "type": {
+ "const": "diff"
+ },
+ "before": {
+ "type": "string"
+ },
+ "after": {
+ "type": "string"
+ }
+ }
+ },
+ "markdown": {
+ "type": "object",
+ "description": "GitLab flavoured markdown, see https://docs.gitlab.com/ee/user/markdown.html",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "markdown"
+ },
+ "value": {
+ "$ref": "#/definitions/text_value",
+ "examples": [
+ "Here is markdown `inline code` #1 [test](gitlab.com)\n\n![GitLab Logo](https://about.gitlab.com/images/press/logo/preview/gitlab-logo-white-preview.png)"
+ ]
+ }
+ }
+ },
+ "commit": {
+ "type": "object",
+ "description": "A commit/tag/branch within the GitLab project",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "commit"
+ },
+ "value": {
+ "type": "string",
+ "description": "The commit SHA",
+ "minLength": 1
+ }
+ }
+ },
+ "file_location": {
+ "type": "object",
+ "description": "A location within a file in the project",
+ "required": [
+ "type",
+ "file_name",
+ "line_start"
+ ],
+ "properties": {
+ "type": {
+ "const": "file-location"
+ },
+ "file_name": {
+ "type": "string",
+ "minLength": 1
+ },
+ "line_start": {
+ "type": "integer"
+ },
+ "line_end": {
+ "type": "integer"
+ }
+ }
+ },
+ "module_location": {
+ "type": "object",
+ "description": "A location within a binary module of the form module+relative_offset",
+ "required": [
+ "type",
+ "module_name",
+ "offset"
+ ],
+ "properties": {
+ "type": {
+ "const": "module-location"
+ },
+ "module_name": {
+ "type": "string",
+ "minLength": 1,
+ "examples": [
+ "compiled_binary"
+ ]
+ },
+ "offset": {
+ "type": "integer",
+ "examples": [
+ 100
+ ]
+ }
+ }
+ }
+ },
+ "self": {
+ "version": "15.0.7"
+ },
+ "type": "object",
+ "required": [
+ "dependency_files",
+ "scan",
+ "version",
+ "vulnerabilities"
+ ],
+ "additionalProperties": true,
+ "properties": {
+ "scan": {
+ "type": "object",
+ "required": [
+ "analyzer",
+ "end_time",
+ "scanner",
+ "start_time",
+ "status",
+ "type"
+ ],
+ "properties": {
+ "end_time": {
+ "type": "string",
+ "description": "ISO8601 UTC value with format yyyy-mm-ddThh:mm:ss, representing when the scan finished.",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$",
+ "examples": [
+ "2020-01-28T03:26:02"
+ ]
+ },
+ "messages": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Communication intended for the initiator of a scan.",
+ "required": [
+ "level",
+ "value"
+ ],
+ "properties": {
+ "level": {
+ "type": "string",
+ "description": "Describes the severity of the communication. Use info to communicate normal scan behaviour; warn to communicate a potentially recoverable problem, or a partial error; fatal to communicate an issue that causes the scan to halt.",
+ "enum": [
+ "info",
+ "warn",
+ "fatal"
+ ],
+ "examples": [
+ "info"
+ ]
+ },
+ "value": {
+ "type": "string",
+ "description": "The message to communicate.",
+ "minLength": 1,
+ "examples": [
+ "Permission denied, scanning aborted"
+ ]
+ }
+ }
+ }
+ },
+ "options": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "A configuration option used for this scan.",
+ "required": [
+ "name",
+ "value"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The configuration option name.",
+ "maxLength": 255,
+ "minLength": 1,
+ "examples": [
+ "DAST_FF_ENABLE_BAS",
+ "DOCKER_TLS_CERTDIR",
+ "DS_MAX_DEPTH",
+ "SECURE_LOG_LEVEL"
+ ]
+ },
+ "source": {
+ "type": "string",
+ "description": "The source of this option.",
+ "enum": [
+ "argument",
+ "file",
+ "env_variable",
+ "other"
+ ]
+ },
+ "value": {
+ "type": [
+ "boolean",
+ "integer",
+ "null",
+ "string"
+ ],
+ "description": "The value used for this scan.",
+ "examples": [
+ true,
+ 2,
+ null,
+ "fatal",
+ ""
+ ]
+ }
+ }
+ }
+ },
+ "analyzer": {
+ "type": "object",
+ "description": "Object defining the analyzer used to perform the scan. Analyzers typically delegate to an underlying scanner to run the scan.",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "vendor"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique id that identifies the analyzer.",
+ "minLength": 1,
+ "examples": [
+ "gitlab-dast"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "description": "A human readable value that identifies the analyzer, not required to be unique.",
+ "minLength": 1,
+ "examples": [
+ "GitLab DAST"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "pattern": "^https?://.+",
+ "description": "A link to more information about the analyzer.",
+ "examples": [
+ "https://docs.gitlab.com/ee/user/application_security/dast"
+ ]
+ },
+ "vendor": {
+ "description": "The vendor/maintainer of the analyzer.",
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the vendor.",
+ "minLength": 1,
+ "examples": [
+ "GitLab"
+ ]
+ }
+ }
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the analyzer.",
+ "minLength": 1,
+ "examples": [
+ "1.0.2"
+ ]
+ }
+ }
+ },
+ "scanner": {
+ "type": "object",
+ "description": "Object defining the scanner used to perform the scan.",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "vendor"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique id that identifies the scanner.",
+ "minLength": 1,
+ "examples": [
+ "my-sast-scanner"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "description": "A human readable value that identifies the scanner, not required to be unique.",
+ "minLength": 1,
+ "examples": [
+ "My SAST Scanner"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "description": "A link to more information about the scanner.",
+ "examples": [
+ "https://scanner.url"
+ ]
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the scanner.",
+ "minLength": 1,
+ "examples": [
+ "1.0.2"
+ ]
+ },
+ "vendor": {
+ "description": "The vendor/maintainer of the scanner.",
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the vendor.",
+ "minLength": 1,
+ "examples": [
+ "GitLab"
+ ]
+ }
+ }
+ }
+ }
+ },
+ "start_time": {
+ "type": "string",
+ "description": "ISO8601 UTC value with format yyyy-mm-ddThh:mm:ss, representing when the scan started.",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$",
+ "examples": [
+ "2020-02-14T16:01:59"
+ ]
+ },
+ "status": {
+ "type": "string",
+ "description": "Result of the scan.",
+ "enum": [
+ "success",
+ "failure"
+ ]
+ },
+ "type": {
+ "type": "string",
+ "description": "Type of the scan.",
+ "enum": [
+ "dependency_scanning"
+ ]
+ },
+ "primary_identifiers": {
+ "type": "array",
+ "description": "An unordered array containing an exhaustive list of primary identifiers for which the analyzer may return results",
+ "items": {
+ "type": "object",
+ "required": [
+ "type",
+ "name",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "for example, cve, cwe, osvdb, usn, or an analyzer-dependent type such as gemnasium).",
+ "minLength": 1
+ },
+ "name": {
+ "type": "string",
+ "description": "Human-readable name of the identifier.",
+ "minLength": 1
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the identifier's documentation.",
+ "pattern": "^(https?|ftp)://.+"
+ },
+ "value": {
+ "type": "string",
+ "description": "Value of the identifier, for matching purpose.",
+ "minLength": 1
+ }
+ }
+ }
+ }
+ }
+ },
+ "schema": {
+ "type": "string",
+ "description": "URI pointing to the validating security report schema.",
+ "pattern": "^https?://.+"
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the schema to which the JSON report conforms.",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ },
+ "vulnerabilities": {
+ "type": "array",
+ "description": "Array of vulnerability objects.",
+ "items": {
+ "type": "object",
+ "description": "Describes the vulnerability using GitLab Flavored Markdown",
+ "required": [
+ "id",
+ "identifiers",
+ "location"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Unique identifier of the vulnerability. This is recommended to be a UUID.",
+ "examples": [
+ "642735a5-1425-428d-8d4e-3c854885a3c9"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "maxLength": 255,
+ "description": "The name of the vulnerability. This must not include the finding's specific information."
+ },
+ "description": {
+ "type": "string",
+ "maxLength": 1048576,
+ "description": "A long text section describing the vulnerability more fully."
+ },
+ "severity": {
+ "type": "string",
+ "description": "How much the vulnerability impacts the software. Possible values are Info, Unknown, Low, Medium, High, or Critical. Note that some analyzers may not report all these possible values.",
+ "enum": [
+ "Info",
+ "Unknown",
+ "Low",
+ "Medium",
+ "High",
+ "Critical"
+ ]
+ },
+ "solution": {
+ "type": "string",
+ "maxLength": 7000,
+ "description": "Explanation of how to fix the vulnerability."
+ },
+ "identifiers": {
+ "type": "array",
+ "minItems": 1,
+ "description": "An ordered array of references that identify a vulnerability on internal or external databases. The first identifier is the Primary Identifier, which has special meaning.",
+ "items": {
+ "type": "object",
+ "required": [
+ "type",
+ "name",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "for example, cve, cwe, osvdb, usn, or an analyzer-dependent type such as gemnasium).",
+ "minLength": 1
+ },
+ "name": {
+ "type": "string",
+ "description": "Human-readable name of the identifier.",
+ "minLength": 1
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the identifier's documentation.",
+ "pattern": "^(https?|ftp)://.+"
+ },
+ "value": {
+ "type": "string",
+ "description": "Value of the identifier, for matching purpose.",
+ "minLength": 1
+ }
+ }
+ }
+ },
+ "cvss_vectors": {
+ "type": "array",
+ "minItems": 1,
+ "maxItems": 10,
+ "description": "An ordered array of CVSS vectors, each issued by a vendor to rate the vulnerability. The first item in the array is used as the primary CVSS vector, and is used to filter and sort the vulnerability.",
+ "items": {
+ "oneOf": [
+ {
+ "type": "object",
+ "properties": {
+ "vendor": {
+ "type": "string",
+ "minLength": 1,
+ "default": "unknown"
+ },
+ "vector": {
+ "type": "string",
+ "minLength": 16,
+ "maxLength": 128,
+ "pattern": "^((AV:[NAL]|AC:[LMH]|Au:[MSN]|[CIA]:[NPC]|E:(U|POC|F|H|ND)|RL:(OF|TF|W|U|ND)|RC:(UC|UR|C|ND)|CDP:(N|L|LM|MH|H|ND)|TD:(N|L|M|H|ND)|[CIA]R:(L|M|H|ND))/)*(AV:[NAL]|AC:[LMH]|Au:[MSN]|[CIA]:[NPC]|E:(U|POC|F|H|ND)|RL:(OF|TF|W|U|ND)|RC:(UC|UR|C|ND)|CDP:(N|L|LM|MH|H|ND)|TD:(N|L|M|H|ND)|[CIA]R:(L|M|H|ND))$"
+ }
+ },
+ "required": [
+ "vendor",
+ "vector"
+ ]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "vendor": {
+ "type": "string",
+ "minLength": 1,
+ "default": "unknown"
+ },
+ "vector": {
+ "type": "string",
+ "minLength": 32,
+ "maxLength": 128,
+ "pattern": "^CVSS:3[.][01]/((AV:[NALP]|AC:[LH]|PR:[NLH]|UI:[NR]|S:[UC]|[CIA]:[NLH]|E:[XUPFH]|RL:[XOTWU]|RC:[XURC]|[CIA]R:[XLMH]|MAV:[XNALP]|MAC:[XLH]|MPR:[XNLH]|MUI:[XNR]|MS:[XUC]|M[CIA]:[XNLH])/)*(AV:[NALP]|AC:[LH]|PR:[NLH]|UI:[NR]|S:[UC]|[CIA]:[NLH]|E:[XUPFH]|RL:[XOTWU]|RC:[XURC]|[CIA]R:[XLMH]|MAV:[XNALP]|MAC:[XLH]|MPR:[XNLH]|MUI:[XNR]|MS:[XUC]|M[CIA]:[XNLH])$"
+ }
+ },
+ "required": [
+ "vendor",
+ "vector"
+ ]
+ }
+ ]
+ }
+ },
+ "links": {
+ "type": "array",
+ "description": "An array of references to external documentation or articles that describe the vulnerability.",
+ "items": {
+ "type": "object",
+ "required": [
+ "url"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Name of the vulnerability details link."
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the vulnerability details document.",
+ "pattern": "^(https?|ftp)://.+"
+ }
+ }
+ }
+ },
+ "details": {
+ "$ref": "#/definitions/named_list/properties/items"
+ },
+ "tracking": {
+ "type": "object",
+ "description": "Describes how this vulnerability should be tracked as the project changes.",
+ "oneOf": [
+ {
+ "description": "Declares that a series of items should be tracked using source-specific tracking methods.",
+ "required": [
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "source"
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "description": "An item that should be tracked using source-specific tracking methods.",
+ "type": "object",
+ "required": [
+ "signatures"
+ ],
+ "properties": {
+ "file": {
+ "type": "string",
+ "description": "Path to the file where the vulnerability is located."
+ },
+ "start_line": {
+ "type": "number",
+ "description": "The first line of the file that includes the vulnerability."
+ },
+ "end_line": {
+ "type": "number",
+ "description": "The last line of the file that includes the vulnerability."
+ },
+ "signatures": {
+ "type": "array",
+ "description": "An array of calculated tracking signatures for this tracking item.",
+ "minItems": 1,
+ "items": {
+ "description": "A calculated tracking signature value and metadata.",
+ "type": "object",
+ "required": [
+ "algorithm",
+ "value"
+ ],
+ "properties": {
+ "algorithm": {
+ "type": "string",
+ "description": "The algorithm used to generate the signature."
+ },
+ "value": {
+ "type": "string",
+ "description": "The result of this signature algorithm."
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "Each tracking type must declare its own type."
+ }
+ }
+ },
+ "flags": {
+ "description": "Flags that can be attached to vulnerabilities.",
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Informational flags identified and assigned to a vulnerability.",
+ "required": [
+ "type",
+ "origin",
+ "description"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Result of the scan.",
+ "enum": [
+ "flagged-as-likely-false-positive"
+ ]
+ },
+ "origin": {
+ "minLength": 1,
+ "description": "Tool that issued the flag.",
+ "type": "string"
+ },
+ "description": {
+ "minLength": 1,
+ "description": "What the flag is about.",
+ "type": "string"
+ }
+ }
+ }
+ },
+ "location": {
+ "type": "object",
+ "description": "Identifies the vulnerability's location.",
+ "required": [
+ "file",
+ "dependency"
+ ],
+ "properties": {
+ "file": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Path to the manifest or lock file where the dependency is declared (such as yarn.lock)."
+ },
+ "dependency": {
+ "type": "object",
+ "description": "Describes the dependency of a project where the vulnerability is located.",
+ "required": [
+ "package",
+ "version"
+ ],
+ "properties": {
+ "package": {
+ "type": "object",
+ "description": "Provides information on the package where the vulnerability is located.",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Name of the package where the vulnerability is located."
+ }
+ }
+ },
+ "version": {
+ "type": "string",
+ "description": "Version of the vulnerable package."
+ },
+ "iid": {
+ "description": "ID that identifies the dependency in the scope of a dependency file.",
+ "type": "number"
+ },
+ "direct": {
+ "type": "boolean",
+ "description": "Tells whether this is a direct, top-level dependency of the scanned project."
+ },
+ "dependency_path": {
+ "type": "array",
+ "description": "Ancestors of the dependency, starting from a direct project dependency, and ending with an immediate parent of the dependency. The dependency itself is excluded from the path. Direct dependencies have no path.",
+ "items": {
+ "type": "object",
+ "required": [
+ "iid"
+ ],
+ "properties": {
+ "iid": {
+ "type": "number",
+ "description": "ID that is unique in the scope of a parent object, and specific to the resource type."
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "remediations": {
+ "type": "array",
+ "description": "An array of objects containing information on available remediations, along with patch diffs to apply.",
+ "items": {
+ "type": "object",
+ "required": [
+ "fixes",
+ "summary",
+ "diff"
+ ],
+ "properties": {
+ "fixes": {
+ "type": "array",
+ "description": "An array of strings that represent references to vulnerabilities fixed by this remediation.",
+ "items": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Unique identifier of the vulnerability. This is recommended to be a UUID.",
+ "examples": [
+ "642735a5-1425-428d-8d4e-3c854885a3c9"
+ ]
+ }
+ }
+ }
+ },
+ "summary": {
+ "type": "string",
+ "minLength": 1,
+ "description": "An overview of how the vulnerabilities were fixed."
+ },
+ "diff": {
+ "type": "string",
+ "minLength": 1,
+ "description": "A base64-encoded remediation code diff, compatible with git apply."
+ }
+ }
+ }
+ },
+ "dependency_files": {
+ "type": "array",
+ "description": "List of dependency files identified in the project.",
+ "items": {
+ "type": "object",
+ "required": [
+ "path",
+ "package_manager",
+ "dependencies"
+ ],
+ "properties": {
+ "path": {
+ "type": "string",
+ "minLength": 1
+ },
+ "package_manager": {
+ "type": "string",
+ "minLength": 1
+ },
+ "dependencies": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Describes the dependency of a project where the vulnerability is located.",
+ "required": [
+ "package",
+ "version"
+ ],
+ "properties": {
+ "package": {
+ "type": "object",
+ "description": "Provides information on the package where the vulnerability is located.",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Name of the package where the vulnerability is located."
+ }
+ }
+ },
+ "version": {
+ "type": "string",
+ "description": "Version of the vulnerable package."
+ },
+ "iid": {
+ "description": "ID that identifies the dependency in the scope of a dependency file.",
+ "type": "number"
+ },
+ "direct": {
+ "type": "boolean",
+ "description": "Tells whether this is a direct, top-level dependency of the scanned project."
+ },
+ "dependency_path": {
+ "type": "array",
+ "description": "Ancestors of the dependency, starting from a direct project dependency, and ending with an immediate parent of the dependency. The dependency itself is excluded from the path. Direct dependencies have no path.",
+ "items": {
+ "type": "object",
+ "required": [
+ "iid"
+ ],
+ "properties": {
+ "iid": {
+ "type": "number",
+ "description": "ID that is unique in the scope of a parent object, and specific to the resource type."
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/lib/gitlab/ci/parsers/security/validators/schemas/15.0.7/sast-report-format.json b/lib/gitlab/ci/parsers/security/validators/schemas/15.0.7/sast-report-format.json
new file mode 100644
index 00000000000..3597ed169d5
--- /dev/null
+++ b/lib/gitlab/ci/parsers/security/validators/schemas/15.0.7/sast-report-format.json
@@ -0,0 +1,970 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/raw/master/dist/sast-report-format.json",
+ "title": "Report format for GitLab SAST",
+ "description": "This schema provides the report format for Static Application Security Testing analyzers (https://docs.gitlab.com/ee/user/application_security/sast).",
+ "definitions": {
+ "detail_type": {
+ "oneOf": [
+ {
+ "$ref": "#/definitions/named_list"
+ },
+ {
+ "$ref": "#/definitions/list"
+ },
+ {
+ "$ref": "#/definitions/table"
+ },
+ {
+ "$ref": "#/definitions/text"
+ },
+ {
+ "$ref": "#/definitions/url"
+ },
+ {
+ "$ref": "#/definitions/code"
+ },
+ {
+ "$ref": "#/definitions/value"
+ },
+ {
+ "$ref": "#/definitions/diff"
+ },
+ {
+ "$ref": "#/definitions/markdown"
+ },
+ {
+ "$ref": "#/definitions/commit"
+ },
+ {
+ "$ref": "#/definitions/file_location"
+ },
+ {
+ "$ref": "#/definitions/module_location"
+ }
+ ]
+ },
+ "text_value": {
+ "type": "string"
+ },
+ "named_field": {
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "$ref": "#/definitions/text_value",
+ "type": "string",
+ "minLength": 1
+ },
+ "description": {
+ "$ref": "#/definitions/text_value"
+ }
+ }
+ },
+ "named_list": {
+ "type": "object",
+ "description": "An object with named and typed fields",
+ "required": [
+ "type",
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "named-list"
+ },
+ "items": {
+ "type": "object",
+ "patternProperties": {
+ "^.*$": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/named_field"
+ },
+ {
+ "$ref": "#/definitions/detail_type"
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "list": {
+ "type": "object",
+ "description": "A list of typed fields",
+ "required": [
+ "type",
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "list"
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ }
+ }
+ },
+ "table": {
+ "type": "object",
+ "description": "A table of typed fields",
+ "required": [
+ "type",
+ "rows"
+ ],
+ "properties": {
+ "type": {
+ "const": "table"
+ },
+ "header": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ },
+ "rows": {
+ "type": "array",
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ }
+ }
+ }
+ },
+ "text": {
+ "type": "object",
+ "description": "Raw text",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "text"
+ },
+ "value": {
+ "$ref": "#/definitions/text_value"
+ }
+ }
+ },
+ "url": {
+ "type": "object",
+ "description": "A single URL",
+ "required": [
+ "type",
+ "href"
+ ],
+ "properties": {
+ "type": {
+ "const": "url"
+ },
+ "text": {
+ "$ref": "#/definitions/text_value"
+ },
+ "href": {
+ "type": "string",
+ "minLength": 1,
+ "examples": [
+ "http://mysite.com"
+ ]
+ }
+ }
+ },
+ "code": {
+ "type": "object",
+ "description": "A codeblock",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "code"
+ },
+ "value": {
+ "type": "string"
+ },
+ "lang": {
+ "type": "string",
+ "description": "A programming language"
+ }
+ }
+ },
+ "value": {
+ "type": "object",
+ "description": "A field that can store a range of types of value",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "value"
+ },
+ "value": {
+ "type": [
+ "number",
+ "string",
+ "boolean"
+ ]
+ }
+ }
+ },
+ "diff": {
+ "type": "object",
+ "description": "A diff",
+ "required": [
+ "type",
+ "before",
+ "after"
+ ],
+ "properties": {
+ "type": {
+ "const": "diff"
+ },
+ "before": {
+ "type": "string"
+ },
+ "after": {
+ "type": "string"
+ }
+ }
+ },
+ "markdown": {
+ "type": "object",
+ "description": "GitLab flavoured markdown, see https://docs.gitlab.com/ee/user/markdown.html",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "markdown"
+ },
+ "value": {
+ "$ref": "#/definitions/text_value",
+ "examples": [
+ "Here is markdown `inline code` #1 [test](gitlab.com)\n\n![GitLab Logo](https://about.gitlab.com/images/press/logo/preview/gitlab-logo-white-preview.png)"
+ ]
+ }
+ }
+ },
+ "commit": {
+ "type": "object",
+ "description": "A commit/tag/branch within the GitLab project",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "commit"
+ },
+ "value": {
+ "type": "string",
+ "description": "The commit SHA",
+ "minLength": 1
+ }
+ }
+ },
+ "file_location": {
+ "type": "object",
+ "description": "A location within a file in the project",
+ "required": [
+ "type",
+ "file_name",
+ "line_start"
+ ],
+ "properties": {
+ "type": {
+ "const": "file-location"
+ },
+ "file_name": {
+ "type": "string",
+ "minLength": 1
+ },
+ "line_start": {
+ "type": "integer"
+ },
+ "line_end": {
+ "type": "integer"
+ }
+ }
+ },
+ "module_location": {
+ "type": "object",
+ "description": "A location within a binary module of the form module+relative_offset",
+ "required": [
+ "type",
+ "module_name",
+ "offset"
+ ],
+ "properties": {
+ "type": {
+ "const": "module-location"
+ },
+ "module_name": {
+ "type": "string",
+ "minLength": 1,
+ "examples": [
+ "compiled_binary"
+ ]
+ },
+ "offset": {
+ "type": "integer",
+ "examples": [
+ 100
+ ]
+ }
+ }
+ }
+ },
+ "self": {
+ "version": "15.0.7"
+ },
+ "type": "object",
+ "required": [
+ "scan",
+ "version",
+ "vulnerabilities"
+ ],
+ "additionalProperties": true,
+ "properties": {
+ "scan": {
+ "type": "object",
+ "required": [
+ "analyzer",
+ "end_time",
+ "scanner",
+ "start_time",
+ "status",
+ "type"
+ ],
+ "properties": {
+ "end_time": {
+ "type": "string",
+ "description": "ISO8601 UTC value with format yyyy-mm-ddThh:mm:ss, representing when the scan finished.",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$",
+ "examples": [
+ "2020-01-28T03:26:02"
+ ]
+ },
+ "messages": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Communication intended for the initiator of a scan.",
+ "required": [
+ "level",
+ "value"
+ ],
+ "properties": {
+ "level": {
+ "type": "string",
+ "description": "Describes the severity of the communication. Use info to communicate normal scan behaviour; warn to communicate a potentially recoverable problem, or a partial error; fatal to communicate an issue that causes the scan to halt.",
+ "enum": [
+ "info",
+ "warn",
+ "fatal"
+ ],
+ "examples": [
+ "info"
+ ]
+ },
+ "value": {
+ "type": "string",
+ "description": "The message to communicate.",
+ "minLength": 1,
+ "examples": [
+ "Permission denied, scanning aborted"
+ ]
+ }
+ }
+ }
+ },
+ "options": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "A configuration option used for this scan.",
+ "required": [
+ "name",
+ "value"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The configuration option name.",
+ "maxLength": 255,
+ "minLength": 1,
+ "examples": [
+ "DAST_FF_ENABLE_BAS",
+ "DOCKER_TLS_CERTDIR",
+ "DS_MAX_DEPTH",
+ "SECURE_LOG_LEVEL"
+ ]
+ },
+ "source": {
+ "type": "string",
+ "description": "The source of this option.",
+ "enum": [
+ "argument",
+ "file",
+ "env_variable",
+ "other"
+ ]
+ },
+ "value": {
+ "type": [
+ "boolean",
+ "integer",
+ "null",
+ "string"
+ ],
+ "description": "The value used for this scan.",
+ "examples": [
+ true,
+ 2,
+ null,
+ "fatal",
+ ""
+ ]
+ }
+ }
+ }
+ },
+ "analyzer": {
+ "type": "object",
+ "description": "Object defining the analyzer used to perform the scan. Analyzers typically delegate to an underlying scanner to run the scan.",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "vendor"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique id that identifies the analyzer.",
+ "minLength": 1,
+ "examples": [
+ "gitlab-dast"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "description": "A human readable value that identifies the analyzer, not required to be unique.",
+ "minLength": 1,
+ "examples": [
+ "GitLab DAST"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "pattern": "^https?://.+",
+ "description": "A link to more information about the analyzer.",
+ "examples": [
+ "https://docs.gitlab.com/ee/user/application_security/dast"
+ ]
+ },
+ "vendor": {
+ "description": "The vendor/maintainer of the analyzer.",
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the vendor.",
+ "minLength": 1,
+ "examples": [
+ "GitLab"
+ ]
+ }
+ }
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the analyzer.",
+ "minLength": 1,
+ "examples": [
+ "1.0.2"
+ ]
+ }
+ }
+ },
+ "scanner": {
+ "type": "object",
+ "description": "Object defining the scanner used to perform the scan.",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "vendor"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique id that identifies the scanner.",
+ "minLength": 1,
+ "examples": [
+ "my-sast-scanner"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "description": "A human readable value that identifies the scanner, not required to be unique.",
+ "minLength": 1,
+ "examples": [
+ "My SAST Scanner"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "description": "A link to more information about the scanner.",
+ "examples": [
+ "https://scanner.url"
+ ]
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the scanner.",
+ "minLength": 1,
+ "examples": [
+ "1.0.2"
+ ]
+ },
+ "vendor": {
+ "description": "The vendor/maintainer of the scanner.",
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the vendor.",
+ "minLength": 1,
+ "examples": [
+ "GitLab"
+ ]
+ }
+ }
+ }
+ }
+ },
+ "start_time": {
+ "type": "string",
+ "description": "ISO8601 UTC value with format yyyy-mm-ddThh:mm:ss, representing when the scan started.",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$",
+ "examples": [
+ "2020-02-14T16:01:59"
+ ]
+ },
+ "status": {
+ "type": "string",
+ "description": "Result of the scan.",
+ "enum": [
+ "success",
+ "failure"
+ ]
+ },
+ "type": {
+ "type": "string",
+ "description": "Type of the scan.",
+ "enum": [
+ "sast"
+ ]
+ },
+ "primary_identifiers": {
+ "type": "array",
+ "description": "An unordered array containing an exhaustive list of primary identifiers for which the analyzer may return results",
+ "items": {
+ "type": "object",
+ "required": [
+ "type",
+ "name",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "for example, cve, cwe, osvdb, usn, or an analyzer-dependent type such as gemnasium).",
+ "minLength": 1
+ },
+ "name": {
+ "type": "string",
+ "description": "Human-readable name of the identifier.",
+ "minLength": 1
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the identifier's documentation.",
+ "pattern": "^(https?|ftp)://.+"
+ },
+ "value": {
+ "type": "string",
+ "description": "Value of the identifier, for matching purpose.",
+ "minLength": 1
+ }
+ }
+ }
+ }
+ }
+ },
+ "schema": {
+ "type": "string",
+ "description": "URI pointing to the validating security report schema.",
+ "pattern": "^https?://.+"
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the schema to which the JSON report conforms.",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ },
+ "vulnerabilities": {
+ "type": "array",
+ "description": "Array of vulnerability objects.",
+ "items": {
+ "type": "object",
+ "description": "Describes the vulnerability using GitLab Flavored Markdown",
+ "required": [
+ "id",
+ "identifiers",
+ "location"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Unique identifier of the vulnerability. This is recommended to be a UUID.",
+ "examples": [
+ "642735a5-1425-428d-8d4e-3c854885a3c9"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "maxLength": 255,
+ "description": "The name of the vulnerability. This must not include the finding's specific information."
+ },
+ "description": {
+ "type": "string",
+ "maxLength": 1048576,
+ "description": "A long text section describing the vulnerability more fully."
+ },
+ "severity": {
+ "type": "string",
+ "description": "How much the vulnerability impacts the software. Possible values are Info, Unknown, Low, Medium, High, or Critical. Note that some analyzers may not report all these possible values.",
+ "enum": [
+ "Info",
+ "Unknown",
+ "Low",
+ "Medium",
+ "High",
+ "Critical"
+ ]
+ },
+ "solution": {
+ "type": "string",
+ "maxLength": 7000,
+ "description": "Explanation of how to fix the vulnerability."
+ },
+ "identifiers": {
+ "type": "array",
+ "minItems": 1,
+ "description": "An ordered array of references that identify a vulnerability on internal or external databases. The first identifier is the Primary Identifier, which has special meaning.",
+ "items": {
+ "type": "object",
+ "required": [
+ "type",
+ "name",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "for example, cve, cwe, osvdb, usn, or an analyzer-dependent type such as gemnasium).",
+ "minLength": 1
+ },
+ "name": {
+ "type": "string",
+ "description": "Human-readable name of the identifier.",
+ "minLength": 1
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the identifier's documentation.",
+ "pattern": "^(https?|ftp)://.+"
+ },
+ "value": {
+ "type": "string",
+ "description": "Value of the identifier, for matching purpose.",
+ "minLength": 1
+ }
+ }
+ }
+ },
+ "cvss_vectors": {
+ "type": "array",
+ "minItems": 1,
+ "maxItems": 10,
+ "description": "An ordered array of CVSS vectors, each issued by a vendor to rate the vulnerability. The first item in the array is used as the primary CVSS vector, and is used to filter and sort the vulnerability.",
+ "items": {
+ "oneOf": [
+ {
+ "type": "object",
+ "properties": {
+ "vendor": {
+ "type": "string",
+ "minLength": 1,
+ "default": "unknown"
+ },
+ "vector": {
+ "type": "string",
+ "minLength": 16,
+ "maxLength": 128,
+ "pattern": "^((AV:[NAL]|AC:[LMH]|Au:[MSN]|[CIA]:[NPC]|E:(U|POC|F|H|ND)|RL:(OF|TF|W|U|ND)|RC:(UC|UR|C|ND)|CDP:(N|L|LM|MH|H|ND)|TD:(N|L|M|H|ND)|[CIA]R:(L|M|H|ND))/)*(AV:[NAL]|AC:[LMH]|Au:[MSN]|[CIA]:[NPC]|E:(U|POC|F|H|ND)|RL:(OF|TF|W|U|ND)|RC:(UC|UR|C|ND)|CDP:(N|L|LM|MH|H|ND)|TD:(N|L|M|H|ND)|[CIA]R:(L|M|H|ND))$"
+ }
+ },
+ "required": [
+ "vendor",
+ "vector"
+ ]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "vendor": {
+ "type": "string",
+ "minLength": 1,
+ "default": "unknown"
+ },
+ "vector": {
+ "type": "string",
+ "minLength": 32,
+ "maxLength": 128,
+ "pattern": "^CVSS:3[.][01]/((AV:[NALP]|AC:[LH]|PR:[NLH]|UI:[NR]|S:[UC]|[CIA]:[NLH]|E:[XUPFH]|RL:[XOTWU]|RC:[XURC]|[CIA]R:[XLMH]|MAV:[XNALP]|MAC:[XLH]|MPR:[XNLH]|MUI:[XNR]|MS:[XUC]|M[CIA]:[XNLH])/)*(AV:[NALP]|AC:[LH]|PR:[NLH]|UI:[NR]|S:[UC]|[CIA]:[NLH]|E:[XUPFH]|RL:[XOTWU]|RC:[XURC]|[CIA]R:[XLMH]|MAV:[XNALP]|MAC:[XLH]|MPR:[XNLH]|MUI:[XNR]|MS:[XUC]|M[CIA]:[XNLH])$"
+ }
+ },
+ "required": [
+ "vendor",
+ "vector"
+ ]
+ }
+ ]
+ }
+ },
+ "links": {
+ "type": "array",
+ "description": "An array of references to external documentation or articles that describe the vulnerability.",
+ "items": {
+ "type": "object",
+ "required": [
+ "url"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Name of the vulnerability details link."
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the vulnerability details document.",
+ "pattern": "^(https?|ftp)://.+"
+ }
+ }
+ }
+ },
+ "details": {
+ "$ref": "#/definitions/named_list/properties/items"
+ },
+ "tracking": {
+ "type": "object",
+ "description": "Describes how this vulnerability should be tracked as the project changes.",
+ "oneOf": [
+ {
+ "description": "Declares that a series of items should be tracked using source-specific tracking methods.",
+ "required": [
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "source"
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "description": "An item that should be tracked using source-specific tracking methods.",
+ "type": "object",
+ "required": [
+ "signatures"
+ ],
+ "properties": {
+ "file": {
+ "type": "string",
+ "description": "Path to the file where the vulnerability is located."
+ },
+ "start_line": {
+ "type": "number",
+ "description": "The first line of the file that includes the vulnerability."
+ },
+ "end_line": {
+ "type": "number",
+ "description": "The last line of the file that includes the vulnerability."
+ },
+ "signatures": {
+ "type": "array",
+ "description": "An array of calculated tracking signatures for this tracking item.",
+ "minItems": 1,
+ "items": {
+ "description": "A calculated tracking signature value and metadata.",
+ "type": "object",
+ "required": [
+ "algorithm",
+ "value"
+ ],
+ "properties": {
+ "algorithm": {
+ "type": "string",
+ "description": "The algorithm used to generate the signature."
+ },
+ "value": {
+ "type": "string",
+ "description": "The result of this signature algorithm."
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "Each tracking type must declare its own type."
+ }
+ }
+ },
+ "flags": {
+ "description": "Flags that can be attached to vulnerabilities.",
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Informational flags identified and assigned to a vulnerability.",
+ "required": [
+ "type",
+ "origin",
+ "description"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Result of the scan.",
+ "enum": [
+ "flagged-as-likely-false-positive"
+ ]
+ },
+ "origin": {
+ "minLength": 1,
+ "description": "Tool that issued the flag.",
+ "type": "string"
+ },
+ "description": {
+ "minLength": 1,
+ "description": "What the flag is about.",
+ "type": "string"
+ }
+ }
+ }
+ },
+ "location": {
+ "type": "object",
+ "description": "Identifies the vulnerability's location.",
+ "properties": {
+ "file": {
+ "type": "string",
+ "description": "Path to the file where the vulnerability is located."
+ },
+ "start_line": {
+ "type": "number",
+ "description": "The first line of the code affected by the vulnerability."
+ },
+ "end_line": {
+ "type": "number",
+ "description": "The last line of the code affected by the vulnerability."
+ },
+ "class": {
+ "type": "string",
+ "description": "Provides the name of the class where the vulnerability is located."
+ },
+ "method": {
+ "type": "string",
+ "description": "Provides the name of the method where the vulnerability is located."
+ }
+ }
+ },
+ "raw_source_code_extract": {
+ "type": "string",
+ "description": "Provides an unsanitized excerpt of the affected source code."
+ }
+ }
+ }
+ },
+ "remediations": {
+ "type": "array",
+ "description": "An array of objects containing information on available remediations, along with patch diffs to apply.",
+ "items": {
+ "type": "object",
+ "required": [
+ "fixes",
+ "summary",
+ "diff"
+ ],
+ "properties": {
+ "fixes": {
+ "type": "array",
+ "description": "An array of strings that represent references to vulnerabilities fixed by this remediation.",
+ "items": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Unique identifier of the vulnerability. This is recommended to be a UUID.",
+ "examples": [
+ "642735a5-1425-428d-8d4e-3c854885a3c9"
+ ]
+ }
+ }
+ }
+ },
+ "summary": {
+ "type": "string",
+ "minLength": 1,
+ "description": "An overview of how the vulnerabilities were fixed."
+ },
+ "diff": {
+ "type": "string",
+ "minLength": 1,
+ "description": "A base64-encoded remediation code diff, compatible with git apply."
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/lib/gitlab/ci/parsers/security/validators/schemas/15.0.7/secret-detection-report-format.json b/lib/gitlab/ci/parsers/security/validators/schemas/15.0.7/secret-detection-report-format.json
new file mode 100644
index 00000000000..afd80ca916b
--- /dev/null
+++ b/lib/gitlab/ci/parsers/security/validators/schemas/15.0.7/secret-detection-report-format.json
@@ -0,0 +1,994 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/raw/master/dist/secret-detection-report-format.json",
+ "title": "Report format for GitLab Secret Detection",
+ "description": "This schema provides the the report format for the Secret Detection analyzer (https://docs.gitlab.com/ee/user/application_security/secret_detection)",
+ "definitions": {
+ "detail_type": {
+ "oneOf": [
+ {
+ "$ref": "#/definitions/named_list"
+ },
+ {
+ "$ref": "#/definitions/list"
+ },
+ {
+ "$ref": "#/definitions/table"
+ },
+ {
+ "$ref": "#/definitions/text"
+ },
+ {
+ "$ref": "#/definitions/url"
+ },
+ {
+ "$ref": "#/definitions/code"
+ },
+ {
+ "$ref": "#/definitions/value"
+ },
+ {
+ "$ref": "#/definitions/diff"
+ },
+ {
+ "$ref": "#/definitions/markdown"
+ },
+ {
+ "$ref": "#/definitions/commit"
+ },
+ {
+ "$ref": "#/definitions/file_location"
+ },
+ {
+ "$ref": "#/definitions/module_location"
+ }
+ ]
+ },
+ "text_value": {
+ "type": "string"
+ },
+ "named_field": {
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "$ref": "#/definitions/text_value",
+ "type": "string",
+ "minLength": 1
+ },
+ "description": {
+ "$ref": "#/definitions/text_value"
+ }
+ }
+ },
+ "named_list": {
+ "type": "object",
+ "description": "An object with named and typed fields",
+ "required": [
+ "type",
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "named-list"
+ },
+ "items": {
+ "type": "object",
+ "patternProperties": {
+ "^.*$": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/named_field"
+ },
+ {
+ "$ref": "#/definitions/detail_type"
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "list": {
+ "type": "object",
+ "description": "A list of typed fields",
+ "required": [
+ "type",
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "list"
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ }
+ }
+ },
+ "table": {
+ "type": "object",
+ "description": "A table of typed fields",
+ "required": [
+ "type",
+ "rows"
+ ],
+ "properties": {
+ "type": {
+ "const": "table"
+ },
+ "header": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ },
+ "rows": {
+ "type": "array",
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detail_type"
+ }
+ }
+ }
+ }
+ },
+ "text": {
+ "type": "object",
+ "description": "Raw text",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "text"
+ },
+ "value": {
+ "$ref": "#/definitions/text_value"
+ }
+ }
+ },
+ "url": {
+ "type": "object",
+ "description": "A single URL",
+ "required": [
+ "type",
+ "href"
+ ],
+ "properties": {
+ "type": {
+ "const": "url"
+ },
+ "text": {
+ "$ref": "#/definitions/text_value"
+ },
+ "href": {
+ "type": "string",
+ "minLength": 1,
+ "examples": [
+ "http://mysite.com"
+ ]
+ }
+ }
+ },
+ "code": {
+ "type": "object",
+ "description": "A codeblock",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "code"
+ },
+ "value": {
+ "type": "string"
+ },
+ "lang": {
+ "type": "string",
+ "description": "A programming language"
+ }
+ }
+ },
+ "value": {
+ "type": "object",
+ "description": "A field that can store a range of types of value",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "value"
+ },
+ "value": {
+ "type": [
+ "number",
+ "string",
+ "boolean"
+ ]
+ }
+ }
+ },
+ "diff": {
+ "type": "object",
+ "description": "A diff",
+ "required": [
+ "type",
+ "before",
+ "after"
+ ],
+ "properties": {
+ "type": {
+ "const": "diff"
+ },
+ "before": {
+ "type": "string"
+ },
+ "after": {
+ "type": "string"
+ }
+ }
+ },
+ "markdown": {
+ "type": "object",
+ "description": "GitLab flavoured markdown, see https://docs.gitlab.com/ee/user/markdown.html",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "markdown"
+ },
+ "value": {
+ "$ref": "#/definitions/text_value",
+ "examples": [
+ "Here is markdown `inline code` #1 [test](gitlab.com)\n\n![GitLab Logo](https://about.gitlab.com/images/press/logo/preview/gitlab-logo-white-preview.png)"
+ ]
+ }
+ }
+ },
+ "commit": {
+ "type": "object",
+ "description": "A commit/tag/branch within the GitLab project",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "commit"
+ },
+ "value": {
+ "type": "string",
+ "description": "The commit SHA",
+ "minLength": 1
+ }
+ }
+ },
+ "file_location": {
+ "type": "object",
+ "description": "A location within a file in the project",
+ "required": [
+ "type",
+ "file_name",
+ "line_start"
+ ],
+ "properties": {
+ "type": {
+ "const": "file-location"
+ },
+ "file_name": {
+ "type": "string",
+ "minLength": 1
+ },
+ "line_start": {
+ "type": "integer"
+ },
+ "line_end": {
+ "type": "integer"
+ }
+ }
+ },
+ "module_location": {
+ "type": "object",
+ "description": "A location within a binary module of the form module+relative_offset",
+ "required": [
+ "type",
+ "module_name",
+ "offset"
+ ],
+ "properties": {
+ "type": {
+ "const": "module-location"
+ },
+ "module_name": {
+ "type": "string",
+ "minLength": 1,
+ "examples": [
+ "compiled_binary"
+ ]
+ },
+ "offset": {
+ "type": "integer",
+ "examples": [
+ 100
+ ]
+ }
+ }
+ }
+ },
+ "self": {
+ "version": "15.0.7"
+ },
+ "type": "object",
+ "required": [
+ "scan",
+ "version",
+ "vulnerabilities"
+ ],
+ "additionalProperties": true,
+ "properties": {
+ "scan": {
+ "type": "object",
+ "required": [
+ "analyzer",
+ "end_time",
+ "scanner",
+ "start_time",
+ "status",
+ "type"
+ ],
+ "properties": {
+ "end_time": {
+ "type": "string",
+ "description": "ISO8601 UTC value with format yyyy-mm-ddThh:mm:ss, representing when the scan finished.",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$",
+ "examples": [
+ "2020-01-28T03:26:02"
+ ]
+ },
+ "messages": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Communication intended for the initiator of a scan.",
+ "required": [
+ "level",
+ "value"
+ ],
+ "properties": {
+ "level": {
+ "type": "string",
+ "description": "Describes the severity of the communication. Use info to communicate normal scan behaviour; warn to communicate a potentially recoverable problem, or a partial error; fatal to communicate an issue that causes the scan to halt.",
+ "enum": [
+ "info",
+ "warn",
+ "fatal"
+ ],
+ "examples": [
+ "info"
+ ]
+ },
+ "value": {
+ "type": "string",
+ "description": "The message to communicate.",
+ "minLength": 1,
+ "examples": [
+ "Permission denied, scanning aborted"
+ ]
+ }
+ }
+ }
+ },
+ "options": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "A configuration option used for this scan.",
+ "required": [
+ "name",
+ "value"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The configuration option name.",
+ "maxLength": 255,
+ "minLength": 1,
+ "examples": [
+ "DAST_FF_ENABLE_BAS",
+ "DOCKER_TLS_CERTDIR",
+ "DS_MAX_DEPTH",
+ "SECURE_LOG_LEVEL"
+ ]
+ },
+ "source": {
+ "type": "string",
+ "description": "The source of this option.",
+ "enum": [
+ "argument",
+ "file",
+ "env_variable",
+ "other"
+ ]
+ },
+ "value": {
+ "type": [
+ "boolean",
+ "integer",
+ "null",
+ "string"
+ ],
+ "description": "The value used for this scan.",
+ "examples": [
+ true,
+ 2,
+ null,
+ "fatal",
+ ""
+ ]
+ }
+ }
+ }
+ },
+ "analyzer": {
+ "type": "object",
+ "description": "Object defining the analyzer used to perform the scan. Analyzers typically delegate to an underlying scanner to run the scan.",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "vendor"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique id that identifies the analyzer.",
+ "minLength": 1,
+ "examples": [
+ "gitlab-dast"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "description": "A human readable value that identifies the analyzer, not required to be unique.",
+ "minLength": 1,
+ "examples": [
+ "GitLab DAST"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "pattern": "^https?://.+",
+ "description": "A link to more information about the analyzer.",
+ "examples": [
+ "https://docs.gitlab.com/ee/user/application_security/dast"
+ ]
+ },
+ "vendor": {
+ "description": "The vendor/maintainer of the analyzer.",
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the vendor.",
+ "minLength": 1,
+ "examples": [
+ "GitLab"
+ ]
+ }
+ }
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the analyzer.",
+ "minLength": 1,
+ "examples": [
+ "1.0.2"
+ ]
+ }
+ }
+ },
+ "scanner": {
+ "type": "object",
+ "description": "Object defining the scanner used to perform the scan.",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "vendor"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique id that identifies the scanner.",
+ "minLength": 1,
+ "examples": [
+ "my-sast-scanner"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "description": "A human readable value that identifies the scanner, not required to be unique.",
+ "minLength": 1,
+ "examples": [
+ "My SAST Scanner"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "description": "A link to more information about the scanner.",
+ "examples": [
+ "https://scanner.url"
+ ]
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the scanner.",
+ "minLength": 1,
+ "examples": [
+ "1.0.2"
+ ]
+ },
+ "vendor": {
+ "description": "The vendor/maintainer of the scanner.",
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the vendor.",
+ "minLength": 1,
+ "examples": [
+ "GitLab"
+ ]
+ }
+ }
+ }
+ }
+ },
+ "start_time": {
+ "type": "string",
+ "description": "ISO8601 UTC value with format yyyy-mm-ddThh:mm:ss, representing when the scan started.",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$",
+ "examples": [
+ "2020-02-14T16:01:59"
+ ]
+ },
+ "status": {
+ "type": "string",
+ "description": "Result of the scan.",
+ "enum": [
+ "success",
+ "failure"
+ ]
+ },
+ "type": {
+ "type": "string",
+ "description": "Type of the scan.",
+ "enum": [
+ "secret_detection"
+ ]
+ },
+ "primary_identifiers": {
+ "type": "array",
+ "description": "An unordered array containing an exhaustive list of primary identifiers for which the analyzer may return results",
+ "items": {
+ "type": "object",
+ "required": [
+ "type",
+ "name",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "for example, cve, cwe, osvdb, usn, or an analyzer-dependent type such as gemnasium).",
+ "minLength": 1
+ },
+ "name": {
+ "type": "string",
+ "description": "Human-readable name of the identifier.",
+ "minLength": 1
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the identifier's documentation.",
+ "pattern": "^(https?|ftp)://.+"
+ },
+ "value": {
+ "type": "string",
+ "description": "Value of the identifier, for matching purpose.",
+ "minLength": 1
+ }
+ }
+ }
+ }
+ }
+ },
+ "schema": {
+ "type": "string",
+ "description": "URI pointing to the validating security report schema.",
+ "pattern": "^https?://.+"
+ },
+ "version": {
+ "type": "string",
+ "description": "The version of the schema to which the JSON report conforms.",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ },
+ "vulnerabilities": {
+ "type": "array",
+ "description": "Array of vulnerability objects.",
+ "items": {
+ "type": "object",
+ "description": "Describes the vulnerability using GitLab Flavored Markdown",
+ "required": [
+ "id",
+ "identifiers",
+ "location"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Unique identifier of the vulnerability. This is recommended to be a UUID.",
+ "examples": [
+ "642735a5-1425-428d-8d4e-3c854885a3c9"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "maxLength": 255,
+ "description": "The name of the vulnerability. This must not include the finding's specific information."
+ },
+ "description": {
+ "type": "string",
+ "maxLength": 1048576,
+ "description": "A long text section describing the vulnerability more fully."
+ },
+ "severity": {
+ "type": "string",
+ "description": "How much the vulnerability impacts the software. Possible values are Info, Unknown, Low, Medium, High, or Critical. Note that some analyzers may not report all these possible values.",
+ "enum": [
+ "Info",
+ "Unknown",
+ "Low",
+ "Medium",
+ "High",
+ "Critical"
+ ]
+ },
+ "solution": {
+ "type": "string",
+ "maxLength": 7000,
+ "description": "Explanation of how to fix the vulnerability."
+ },
+ "identifiers": {
+ "type": "array",
+ "minItems": 1,
+ "description": "An ordered array of references that identify a vulnerability on internal or external databases. The first identifier is the Primary Identifier, which has special meaning.",
+ "items": {
+ "type": "object",
+ "required": [
+ "type",
+ "name",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "for example, cve, cwe, osvdb, usn, or an analyzer-dependent type such as gemnasium).",
+ "minLength": 1
+ },
+ "name": {
+ "type": "string",
+ "description": "Human-readable name of the identifier.",
+ "minLength": 1
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the identifier's documentation.",
+ "pattern": "^(https?|ftp)://.+"
+ },
+ "value": {
+ "type": "string",
+ "description": "Value of the identifier, for matching purpose.",
+ "minLength": 1
+ }
+ }
+ }
+ },
+ "cvss_vectors": {
+ "type": "array",
+ "minItems": 1,
+ "maxItems": 10,
+ "description": "An ordered array of CVSS vectors, each issued by a vendor to rate the vulnerability. The first item in the array is used as the primary CVSS vector, and is used to filter and sort the vulnerability.",
+ "items": {
+ "oneOf": [
+ {
+ "type": "object",
+ "properties": {
+ "vendor": {
+ "type": "string",
+ "minLength": 1,
+ "default": "unknown"
+ },
+ "vector": {
+ "type": "string",
+ "minLength": 16,
+ "maxLength": 128,
+ "pattern": "^((AV:[NAL]|AC:[LMH]|Au:[MSN]|[CIA]:[NPC]|E:(U|POC|F|H|ND)|RL:(OF|TF|W|U|ND)|RC:(UC|UR|C|ND)|CDP:(N|L|LM|MH|H|ND)|TD:(N|L|M|H|ND)|[CIA]R:(L|M|H|ND))/)*(AV:[NAL]|AC:[LMH]|Au:[MSN]|[CIA]:[NPC]|E:(U|POC|F|H|ND)|RL:(OF|TF|W|U|ND)|RC:(UC|UR|C|ND)|CDP:(N|L|LM|MH|H|ND)|TD:(N|L|M|H|ND)|[CIA]R:(L|M|H|ND))$"
+ }
+ },
+ "required": [
+ "vendor",
+ "vector"
+ ]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "vendor": {
+ "type": "string",
+ "minLength": 1,
+ "default": "unknown"
+ },
+ "vector": {
+ "type": "string",
+ "minLength": 32,
+ "maxLength": 128,
+ "pattern": "^CVSS:3[.][01]/((AV:[NALP]|AC:[LH]|PR:[NLH]|UI:[NR]|S:[UC]|[CIA]:[NLH]|E:[XUPFH]|RL:[XOTWU]|RC:[XURC]|[CIA]R:[XLMH]|MAV:[XNALP]|MAC:[XLH]|MPR:[XNLH]|MUI:[XNR]|MS:[XUC]|M[CIA]:[XNLH])/)*(AV:[NALP]|AC:[LH]|PR:[NLH]|UI:[NR]|S:[UC]|[CIA]:[NLH]|E:[XUPFH]|RL:[XOTWU]|RC:[XURC]|[CIA]R:[XLMH]|MAV:[XNALP]|MAC:[XLH]|MPR:[XNLH]|MUI:[XNR]|MS:[XUC]|M[CIA]:[XNLH])$"
+ }
+ },
+ "required": [
+ "vendor",
+ "vector"
+ ]
+ }
+ ]
+ }
+ },
+ "links": {
+ "type": "array",
+ "description": "An array of references to external documentation or articles that describe the vulnerability.",
+ "items": {
+ "type": "object",
+ "required": [
+ "url"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Name of the vulnerability details link."
+ },
+ "url": {
+ "type": "string",
+ "description": "URL of the vulnerability details document.",
+ "pattern": "^(https?|ftp)://.+"
+ }
+ }
+ }
+ },
+ "details": {
+ "$ref": "#/definitions/named_list/properties/items"
+ },
+ "tracking": {
+ "type": "object",
+ "description": "Describes how this vulnerability should be tracked as the project changes.",
+ "oneOf": [
+ {
+ "description": "Declares that a series of items should be tracked using source-specific tracking methods.",
+ "required": [
+ "items"
+ ],
+ "properties": {
+ "type": {
+ "const": "source"
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "description": "An item that should be tracked using source-specific tracking methods.",
+ "type": "object",
+ "required": [
+ "signatures"
+ ],
+ "properties": {
+ "file": {
+ "type": "string",
+ "description": "Path to the file where the vulnerability is located."
+ },
+ "start_line": {
+ "type": "number",
+ "description": "The first line of the file that includes the vulnerability."
+ },
+ "end_line": {
+ "type": "number",
+ "description": "The last line of the file that includes the vulnerability."
+ },
+ "signatures": {
+ "type": "array",
+ "description": "An array of calculated tracking signatures for this tracking item.",
+ "minItems": 1,
+ "items": {
+ "description": "A calculated tracking signature value and metadata.",
+ "type": "object",
+ "required": [
+ "algorithm",
+ "value"
+ ],
+ "properties": {
+ "algorithm": {
+ "type": "string",
+ "description": "The algorithm used to generate the signature."
+ },
+ "value": {
+ "type": "string",
+ "description": "The result of this signature algorithm."
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "Each tracking type must declare its own type."
+ }
+ }
+ },
+ "flags": {
+ "description": "Flags that can be attached to vulnerabilities.",
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Informational flags identified and assigned to a vulnerability.",
+ "required": [
+ "type",
+ "origin",
+ "description"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Result of the scan.",
+ "enum": [
+ "flagged-as-likely-false-positive"
+ ]
+ },
+ "origin": {
+ "minLength": 1,
+ "description": "Tool that issued the flag.",
+ "type": "string"
+ },
+ "description": {
+ "minLength": 1,
+ "description": "What the flag is about.",
+ "type": "string"
+ }
+ }
+ }
+ },
+ "location": {
+ "required": [
+ "commit"
+ ],
+ "type": "object",
+ "properties": {
+ "file": {
+ "type": "string",
+ "description": "Path to the file where the vulnerability is located"
+ },
+ "commit": {
+ "type": "object",
+ "description": "Represents the commit in which the vulnerability was detected",
+ "required": [
+ "sha"
+ ],
+ "properties": {
+ "author": {
+ "type": "string"
+ },
+ "date": {
+ "type": "string"
+ },
+ "message": {
+ "type": "string"
+ },
+ "sha": {
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ },
+ "start_line": {
+ "type": "number",
+ "description": "The first line of the code affected by the vulnerability"
+ },
+ "end_line": {
+ "type": "number",
+ "description": "The last line of the code affected by the vulnerability"
+ },
+ "class": {
+ "type": "string",
+ "description": "Provides the name of the class where the vulnerability is located"
+ },
+ "method": {
+ "type": "string",
+ "description": "Provides the name of the method where the vulnerability is located"
+ }
+ }
+ },
+ "raw_source_code_extract": {
+ "type": "string",
+ "description": "Provides an unsanitized excerpt of the affected source code."
+ }
+ }
+ }
+ },
+ "remediations": {
+ "type": "array",
+ "description": "An array of objects containing information on available remediations, along with patch diffs to apply.",
+ "items": {
+ "type": "object",
+ "required": [
+ "fixes",
+ "summary",
+ "diff"
+ ],
+ "properties": {
+ "fixes": {
+ "type": "array",
+ "description": "An array of strings that represent references to vulnerabilities fixed by this remediation.",
+ "items": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Unique identifier of the vulnerability. This is recommended to be a UUID.",
+ "examples": [
+ "642735a5-1425-428d-8d4e-3c854885a3c9"
+ ]
+ }
+ }
+ }
+ },
+ "summary": {
+ "type": "string",
+ "minLength": 1,
+ "description": "An overview of how the vulnerabilities were fixed."
+ },
+ "diff": {
+ "type": "string",
+ "minLength": 1,
+ "description": "A base64-encoded remediation code diff, compatible with git apply."
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/lib/gitlab/ci/status/build/cancelable.rb b/lib/gitlab/ci/status/build/cancelable.rb
index 43fb5cdbbe6..b8c8cfa802c 100644
--- a/lib/gitlab/ci/status/build/cancelable.rb
+++ b/lib/gitlab/ci/status/build/cancelable.rb
@@ -6,7 +6,7 @@ module Gitlab
module Build
class Cancelable < Status::Extended
def has_action?
- can?(user, :update_build, subject)
+ can?(user, :cancel_build, subject)
end
def action_icon
diff --git a/lib/gitlab/ci/status/composite.rb b/lib/gitlab/ci/status/composite.rb
index 1ba78b357e5..fe4f6db9549 100644
--- a/lib/gitlab/ci/status/composite.rb
+++ b/lib/gitlab/ci/status/composite.rb
@@ -61,6 +61,8 @@ module Gitlab
'running'
elsif any_of?(:waiting_for_resource)
'waiting_for_resource'
+ elsif any_of?(:waiting_for_callback)
+ 'waiting_for_callback'
elsif any_of?(:manual)
'manual'
elsif any_of?(:scheduled)
diff --git a/lib/gitlab/ci/status/waiting_for_callback.rb b/lib/gitlab/ci/status/waiting_for_callback.rb
new file mode 100644
index 00000000000..0184a910ede
--- /dev/null
+++ b/lib/gitlab/ci/status/waiting_for_callback.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Status
+ class WaitingForCallback < Status::Core
+ def text
+ s_('CiStatusText|Waiting')
+ end
+
+ def label
+ s_('CiStatusLabel|waiting for callback')
+ end
+
+ def icon
+ 'status_pending'
+ end
+
+ def favicon
+ 'favicon_status_pending'
+ end
+
+ def group
+ 'waiting-for-callback'
+ end
+
+ def details_path
+ nil
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/templates/Cosign.gitlab-ci.yml b/lib/gitlab/ci/templates/Cosign.gitlab-ci.yml
index 356062c734e..324128678de 100644
--- a/lib/gitlab/ci/templates/Cosign.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Cosign.gitlab-ci.yml
@@ -12,9 +12,9 @@ include:
docker-build:
variables:
- COSIGN_YES: "true" # Used by Cosign to skip confirmation prompts for non-destructive operations
+ COSIGN_YES: "true" # Used by Cosign to skip confirmation prompts for non-destructive operations
id_tokens:
- SIGSTORE_ID_TOKEN: # Used by Cosign to get certificate from Fulcio
+ SIGSTORE_ID_TOKEN: # Used by Cosign to get certificate from Fulcio
aud: sigstore
after_script:
- apk add --update cosign
diff --git a/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml
index 2d04c97b32e..6898923bc53 100644
--- a/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml
@@ -1,5 +1,5 @@
variables:
- AUTO_BUILD_IMAGE_VERSION: 'v1.44.0'
+ AUTO_BUILD_IMAGE_VERSION: 'v1.49.0'
build:
stage: build
diff --git a/lib/gitlab/ci/templates/Jobs/Build.latest.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Build.latest.gitlab-ci.yml
index 2d04c97b32e..6898923bc53 100644
--- a/lib/gitlab/ci/templates/Jobs/Build.latest.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Build.latest.gitlab-ci.yml
@@ -1,5 +1,5 @@
variables:
- AUTO_BUILD_IMAGE_VERSION: 'v1.44.0'
+ AUTO_BUILD_IMAGE_VERSION: 'v1.49.0'
build:
stage: build
diff --git a/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml
index 4d53b92763a..7d923245d79 100644
--- a/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml
@@ -1,5 +1,5 @@
variables:
- DAST_AUTO_DEPLOY_IMAGE_VERSION: 'v2.59.1'
+ DAST_AUTO_DEPLOY_IMAGE_VERSION: 'v2.60.0'
.dast-auto-deploy:
image: "${CI_TEMPLATE_REGISTRY_HOST}/gitlab-org/cluster-integration/auto-deploy-image:${DAST_AUTO_DEPLOY_IMAGE_VERSION}"
diff --git a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
index 390824e8e49..0f8d5bf6d8f 100644
--- a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
@@ -1,5 +1,5 @@
variables:
- AUTO_DEPLOY_IMAGE_VERSION: 'v2.59.1'
+ AUTO_DEPLOY_IMAGE_VERSION: 'v2.60.0'
.auto-deploy:
image: "${CI_TEMPLATE_REGISTRY_HOST}/gitlab-org/cluster-integration/auto-deploy-image:${AUTO_DEPLOY_IMAGE_VERSION}"
diff --git a/lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml
index a9681c0f927..e29d18ea45a 100644
--- a/lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml
@@ -1,5 +1,5 @@
variables:
- AUTO_DEPLOY_IMAGE_VERSION: 'v2.59.1'
+ AUTO_DEPLOY_IMAGE_VERSION: 'v2.60.0'
.auto-deploy:
image: "${CI_TEMPLATE_REGISTRY_HOST}/gitlab-org/cluster-integration/auto-deploy-image:${AUTO_DEPLOY_IMAGE_VERSION}"
diff --git a/lib/gitlab/ci/templates/Kaniko.gitlab-ci.yml b/lib/gitlab/ci/templates/Kaniko.gitlab-ci.yml
index d7a6104082d..4c89497fa97 100644
--- a/lib/gitlab/ci/templates/Kaniko.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Kaniko.gitlab-ci.yml
@@ -46,13 +46,30 @@ kaniko-build:
# Write credentials to access Gitlab Container Registry within the runner/ci
- echo "{\"auths\":{\"$CI_REGISTRY\":{\"auth\":\"$(echo -n ${CI_REGISTRY_USER}:${CI_REGISTRY_PASSWORD} | base64 | tr -d '\n')\"}}}" > /kaniko/.docker/config.json
# Build and push the container. To disable push add --no-push
- - DOCKERFILE_PATH=${DOCKERFILE_PATH:-"$KANIKO_BUILD_CONTEXT/Dockerfile"}
+ # Both Dockerfile and Containerfile are supported. For retrocompatibility, if both files are present, Dockerfile will be used.
+ - |
+ if [ -z "$DOCKERFILE_PATH" ]; then
+ if [ -f "$KANIKO_BUILD_CONTEXT/Dockerfile" ]; then
+ DOCKERFILE_PATH="$KANIKO_BUILD_CONTEXT/Dockerfile"
+ elif [ -n "$CONTAINERFILE_PATH" ]; then
+ DOCKERFILE_PATH="$CONTAINERFILE_PATH"
+ elif [ -f "$KANIKO_BUILD_CONTEXT/Containerfile" ]; then
+ DOCKERFILE_PATH="$KANIKO_BUILD_CONTEXT/Containerfile"
+ else \
+ echo "No suitable configuration for the build context have been found. Please check your configuration."
+ exit 1
+ fi
+ fi
+ - echo $DOCKERFILE_PATH
- /kaniko/executor --context $KANIKO_BUILD_CONTEXT --dockerfile $DOCKERFILE_PATH --destination $IMAGE_TAG $KANIKO_ARGS
- # Run this job in a branch/tag where a Dockerfile exists
+ # Run this job in a branch/tag where a Containerfile/Dockerfile exists
rules:
- exists:
+ - Containerfile
- Dockerfile
- # custom Dockerfile path
+ # custom Containerfile/Dockerfile path
+ # If both variables are set, DOCKERFILE_PATH will be used
- if: $DOCKERFILE_PATH
+ - if: $CONTAINERFILE_PATH
# custom build context without an explicit Dockerfile path
- if: $KANIKO_BUILD_CONTEXT != $CI_PROJECT_DIR
diff --git a/lib/gitlab/ci/templates/Terraform/Base.latest.gitlab-ci.yml b/lib/gitlab/ci/templates/Terraform/Base.latest.gitlab-ci.yml
index d2b929cf995..0ba4f9715c5 100644
--- a/lib/gitlab/ci/templates/Terraform/Base.latest.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Terraform/Base.latest.gitlab-ci.yml
@@ -50,7 +50,11 @@ variables:
- gitlab-terraform plan-json
resource_group: ${TF_STATE_NAME}
artifacts:
- # The next line, which disables public access to pipeline artifacts, may not be available everywhere.
+ # Terraform's cache files can include secrets which can be accidentally exposed.
+ # Please exercise caution when utilizing secrets in your Terraform infrastructure and
+ # consider limiting access to artifacts or take other security measures to protect sensitive information.
+ #
+ # The next line, which disables public access to pipeline artifacts, is not available on GitLab.com.
# See: https://docs.gitlab.com/ee/ci/yaml/#artifactspublic
public: false
paths:
diff --git a/lib/gitlab/ci/templates/Verify/Browser-Performance.gitlab-ci.yml b/lib/gitlab/ci/templates/Verify/Browser-Performance.gitlab-ci.yml
index c1a90955f7f..8c9e0a329dd 100644
--- a/lib/gitlab/ci/templates/Verify/Browser-Performance.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Verify/Browser-Performance.gitlab-ci.yml
@@ -19,6 +19,7 @@ browser_performance:
SITESPEED_IMAGE: sitespeedio/sitespeed.io
SITESPEED_VERSION: 26.1.0
SITESPEED_OPTIONS: ''
+ SITESPEED_DOCKER_OPTIONS: ''
services:
- docker:dind
script:
@@ -48,7 +49,7 @@ browser_performance:
HTTP_PROXY \
NO_PROXY \
) \
- --shm-size=1g --rm -v "$(pwd)":/sitespeed.io $SITESPEED_IMAGE:$SITESPEED_VERSION --plugins.add ./gitlab-exporter --cpu --outputFolder sitespeed-results $URL $SITESPEED_OPTIONS
+ $SITESPEED_DOCKER_OPTIONS --shm-size=1g --rm -v "$(pwd)":/sitespeed.io $SITESPEED_IMAGE:$SITESPEED_VERSION --plugins.add ./gitlab-exporter --cpu --outputFolder sitespeed-results $URL $SITESPEED_OPTIONS
- mv sitespeed-results/data/performance.json browser-performance.json
artifacts:
paths:
diff --git a/lib/gitlab/ci/templates/Verify/Browser-Performance.latest.gitlab-ci.yml b/lib/gitlab/ci/templates/Verify/Browser-Performance.latest.gitlab-ci.yml
index adc92fde5ae..3f4c0c53850 100644
--- a/lib/gitlab/ci/templates/Verify/Browser-Performance.latest.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Verify/Browser-Performance.latest.gitlab-ci.yml
@@ -19,6 +19,7 @@ browser_performance:
SITESPEED_IMAGE: sitespeedio/sitespeed.io
SITESPEED_VERSION: latest
SITESPEED_OPTIONS: ''
+ SITESPEED_DOCKER_OPTIONS: ''
services:
- docker:dind
script:
@@ -48,7 +49,7 @@ browser_performance:
HTTP_PROXY \
NO_PROXY \
) \
- --shm-size=1g --rm -v "$(pwd)":/sitespeed.io $SITESPEED_IMAGE:$SITESPEED_VERSION --plugins.add ./gitlab-exporter --cpu --outputFolder sitespeed-results $URL $SITESPEED_OPTIONS
+ $SITESPEED_DOCKER_OPTIONS --shm-size=1g --rm -v "$(pwd)":/sitespeed.io $SITESPEED_IMAGE:$SITESPEED_VERSION --plugins.add ./gitlab-exporter --cpu --outputFolder sitespeed-results $URL $SITESPEED_OPTIONS
- mv sitespeed-results/data/performance.json browser-performance.json
artifacts:
paths:
diff --git a/lib/gitlab/ci/yaml_processor/dag.rb b/lib/gitlab/ci/yaml_processor/dag.rb
index 4a122c73e80..d3047385c99 100644
--- a/lib/gitlab/ci/yaml_processor/dag.rb
+++ b/lib/gitlab/ci/yaml_processor/dag.rb
@@ -17,13 +17,15 @@ module Gitlab
def self.check_circular_dependencies!(jobs)
new(jobs).tsort
- rescue TSort::Cyclic
- raise ValidationError, 'The pipeline has circular dependencies'
+ rescue TSort::Cyclic => e
+ raise ValidationError, "The pipeline has circular dependencies: #{e.message}"
end
def tsort_each_child(node, &block)
return unless @nodes[node]
+ raise TSort::Cyclic, "self-dependency: #{node}" if @nodes[node].include?(node)
+
@nodes[node].each(&block)
end
diff --git a/lib/gitlab/ci/yaml_processor/result.rb b/lib/gitlab/ci/yaml_processor/result.rb
index 6207b595fc6..2435d128bf2 100644
--- a/lib/gitlab/ci/yaml_processor/result.rb
+++ b/lib/gitlab/ci/yaml_processor/result.rb
@@ -8,8 +8,7 @@ module Gitlab
class Result
attr_reader :errors, :warnings,
:root_variables, :root_variables_with_prefill_data,
- :stages, :jobs,
- :workflow_rules, :workflow_name
+ :stages, :jobs, :workflow_rules, :workflow_name
def initialize(ci_config: nil, errors: [], warnings: [])
@ci_config = ci_config
@@ -124,7 +123,8 @@ module Gitlab
trigger: job[:trigger],
bridge_needs: job.dig(:needs, :bridge)&.first,
release: job[:release],
- publish: job[:publish]
+ publish: job[:publish],
+ pages: job[:pages]
}.compact }.compact
end
diff --git a/lib/gitlab/composer/version_index.rb b/lib/gitlab/composer/version_index.rb
index bfa3112b795..0834fda9cf9 100644
--- a/lib/gitlab/composer/version_index.rb
+++ b/lib/gitlab/composer/version_index.rb
@@ -48,8 +48,7 @@ module Gitlab
end
def package_source(package)
- use_http_url = package.project.public? || Feature.disabled?(:composer_use_ssh_source_urls, package.project)
- git_url = use_http_url ? package.project.http_url_to_repo : package.project.ssh_url_to_repo
+ git_url = package.project.public? ? package.project.http_url_to_repo : package.project.ssh_url_to_repo
{
'type' => 'git',
diff --git a/lib/gitlab/config/entry/validators.rb b/lib/gitlab/config/entry/validators.rb
index 87b7cab3f6d..c7dd11b0432 100644
--- a/lib/gitlab/config/entry/validators.rb
+++ b/lib/gitlab/config/entry/validators.rb
@@ -56,8 +56,7 @@ module Gitlab
mutually_exclusive_keys = value.try(:keys).to_a & options[:in]
if mutually_exclusive_keys.length > 1
- record.errors.add(attribute, "please use only one of the following keys: " +
- mutually_exclusive_keys.join(', '))
+ record.errors.add(attribute, "these keys cannot be used together: #{mutually_exclusive_keys.join(', ')}")
end
end
end
diff --git a/lib/gitlab/config_checker/puma_rugged_checker.rb b/lib/gitlab/config_checker/puma_rugged_checker.rb
deleted file mode 100644
index 82c59f3328b..00000000000
--- a/lib/gitlab/config_checker/puma_rugged_checker.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module ConfigChecker
- module PumaRuggedChecker
- extend self
- extend Gitlab::Git::RuggedImpl::UseRugged
-
- def check
- notices = []
-
- if running_puma_with_multiple_threads? && rugged_enabled_through_feature_flag?
- link_start = '<a href="https://docs.gitlab.com/ee/administration/operations/puma.html#performance-caveat-when-using-puma-with-rugged">'
- link_end = '</a>'
- notices << {
- type: 'warning',
- message: _('Puma is running with a thread count above 1 and the Rugged '\
- 'service is enabled. This may decrease performance in some environments. '\
- 'See our %{link_start}documentation%{link_end} '\
- 'for details of this issue.') % { link_start: link_start, link_end: link_end }
- }
- end
-
- notices
- end
- end
- end
-end
diff --git a/lib/gitlab/database/dictionary.rb b/lib/gitlab/database/dictionary.rb
new file mode 100644
index 00000000000..7b0c8560a26
--- /dev/null
+++ b/lib/gitlab/database/dictionary.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ class Dictionary
+ def initialize(file_path)
+ @file_path = file_path
+ @data = YAML.load_file(file_path)
+ end
+
+ def name_and_schema
+ [key_name, gitlab_schema.to_sym]
+ end
+
+ def table_name
+ data['table_name']
+ end
+
+ def view_name
+ data['view_name']
+ end
+
+ def milestone
+ data['milestone']
+ end
+
+ def gitlab_schema
+ data['gitlab_schema']
+ end
+
+ def schema?(schema_name)
+ gitlab_schema == schema_name.to_s
+ end
+
+ def key_name
+ table_name || view_name
+ end
+
+ def validate!
+ return true unless gitlab_schema.nil?
+
+ raise(
+ GitlabSchema::UnknownSchemaError,
+ "#{file_path} must specify a valid gitlab_schema for #{key_name}. " \
+ "See #{help_page_url}"
+ )
+ end
+
+ private
+
+ attr_reader :file_path, :data
+
+ def help_page_url
+ # rubocop:disable Gitlab/DocUrl -- link directly to docs.gitlab.com, always
+ 'https://docs.gitlab.com/ee/development/database/database_dictionary.html'
+ # rubocop:enable Gitlab/DocUrl
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/dynamic_model_helpers.rb b/lib/gitlab/database/dynamic_model_helpers.rb
index 83edf77f37e..18854530278 100644
--- a/lib/gitlab/database/dynamic_model_helpers.rb
+++ b/lib/gitlab/database/dynamic_model_helpers.rb
@@ -5,7 +5,7 @@ module Gitlab
module DynamicModelHelpers
BATCH_SIZE = 1_000
- def define_batchable_model(table_name, connection:)
+ def define_batchable_model(table_name, connection:, primary_key: nil)
klass = Class.new(ActiveRecord::Base) do
include EachBatch
@@ -13,6 +13,7 @@ module Gitlab
self.inheritance_column = :_type_disabled
end
+ klass.primary_key = primary_key if connection.primary_keys(table_name).length > 1
klass.connection = connection
klass
end
diff --git a/lib/gitlab/database/gitlab_schema.rb b/lib/gitlab/database/gitlab_schema.rb
index 31ceb898eee..ecb45622061 100644
--- a/lib/gitlab/database/gitlab_schema.rb
+++ b/lib/gitlab/database/gitlab_schema.rb
@@ -31,6 +31,7 @@ module Gitlab
'_test_gitlab_main_cell_' => :gitlab_main_cell,
'_test_gitlab_main_' => :gitlab_main,
'_test_gitlab_ci_' => :gitlab_ci,
+ '_test_gitlab_jh_' => :gitlab_jh,
'_test_gitlab_embedding_' => :gitlab_embedding,
'_test_gitlab_geo_' => :gitlab_geo,
'_test_gitlab_pm_' => :gitlab_pm,
@@ -138,19 +139,19 @@ module Gitlab
end
def self.deleted_tables_to_schema
- @deleted_tables_to_schema ||= self.build_dictionary('deleted_tables').to_h
+ @deleted_tables_to_schema ||= self.build_dictionary('deleted_tables').map(&:name_and_schema).to_h
end
def self.deleted_views_to_schema
- @deleted_views_to_schema ||= self.build_dictionary('deleted_views').to_h
+ @deleted_views_to_schema ||= self.build_dictionary('deleted_views').map(&:name_and_schema).to_h
end
def self.tables_to_schema
- @tables_to_schema ||= self.build_dictionary('').to_h
+ @tables_to_schema ||= self.build_dictionary('').map(&:name_and_schema).to_h
end
def self.views_to_schema
- @views_to_schema ||= self.build_dictionary('views').to_h
+ @views_to_schema ||= self.build_dictionary('views').map(&:name_and_schema).to_h
end
def self.schema_names
@@ -159,21 +160,9 @@ module Gitlab
def self.build_dictionary(scope)
Dir.glob(dictionary_path_globs(scope)).map do |file_path|
- data = YAML.load_file(file_path)
-
- key_name = data['table_name'] || data['view_name']
-
- # rubocop:disable Gitlab/DocUrl
- if data['gitlab_schema'].nil?
- raise(
- UnknownSchemaError,
- "#{file_path} must specify a valid gitlab_schema for #{key_name}. " \
- "See https://docs.gitlab.com/ee/development/database/database_dictionary.html"
- )
- end
- # rubocop:enable Gitlab/DocUrl
-
- [key_name, data['gitlab_schema'].to_sym]
+ dictionary = Dictionary.new(file_path)
+ dictionary.validate!
+ dictionary
end
end
end
diff --git a/lib/gitlab/database/migration.rb b/lib/gitlab/database/migration.rb
index 41044816de9..1d17c2ca608 100644
--- a/lib/gitlab/database/migration.rb
+++ b/lib/gitlab/database/migration.rb
@@ -56,6 +56,10 @@ module Gitlab
include Gitlab::Database::Migrations::RunnerBackoff::MigrationHelpers
end
+ class V2_2 < V2_1
+ include Gitlab::Database::Migrations::MilestoneMixin
+ end
+
def self.[](version)
version = version.to_s
name = "V#{version.tr('.', '_')}"
@@ -66,7 +70,7 @@ module Gitlab
# The current version to be used in new migrations
def self.current_version
- 2.1
+ 2.2
end
end
end
diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb
index efcceafda90..a57bce789c7 100644
--- a/lib/gitlab/database/migration_helpers.rb
+++ b/lib/gitlab/database/migration_helpers.rb
@@ -18,8 +18,8 @@ module Gitlab
include AsyncConstraints::MigrationHelpers
include WraparoundVacuumHelpers
- def define_batchable_model(table_name, connection: self.connection)
- super(table_name, connection: connection)
+ def define_batchable_model(table_name, connection: self.connection, primary_key: nil)
+ super(table_name, connection: connection, primary_key: primary_key)
end
def each_batch(table_name, connection: self.connection, **kwargs)
@@ -821,6 +821,7 @@ module Gitlab
primary_key: :id,
batch_size: 20_000,
sub_batch_size: 1000,
+ pause_ms: 100,
interval: 2.minutes
)
@@ -848,6 +849,7 @@ module Gitlab
conversions.keys,
conversions.values,
job_interval: interval,
+ pause_ms: pause_ms,
batch_size: batch_size,
sub_batch_size: sub_batch_size)
end
diff --git a/lib/gitlab/database/migration_helpers/convert_to_bigint.rb b/lib/gitlab/database/migration_helpers/convert_to_bigint.rb
index 11f1e62e8b9..d1edb739b85 100644
--- a/lib/gitlab/database/migration_helpers/convert_to_bigint.rb
+++ b/lib/gitlab/database/migration_helpers/convert_to_bigint.rb
@@ -4,12 +4,16 @@ module Gitlab
module Database
module MigrationHelpers
module ConvertToBigint
- # This helper is extracted for the purpose of
- # https://gitlab.com/gitlab-org/gitlab/-/issues/392815
- # so that we can test all combinations just once,
- # and simplify migration tests.
- #
- # Once we are done with the PK conversions we can remove this.
+ INDEX_OPTIONS_MAP = {
+ unique: :unique,
+ order: :orders,
+ opclass: :opclasses,
+ where: :where,
+ type: :type,
+ using: :using,
+ comment: :comment
+ }.freeze
+
def com_or_dev_or_test_but_not_jh?
return true if Gitlab.dev_or_test_env?
@@ -29,6 +33,78 @@ module Gitlab
column.sql_type == 'bigint' && temp_column.sql_type == 'integer'
end
+
+ def add_bigint_column_indexes(table_name, int_column_name)
+ bigint_column_name = convert_to_bigint_column(int_column_name)
+
+ unless column_exists?(table_name.to_s, bigint_column_name)
+ raise "Bigint column '#{bigint_column_name}' does not exist on #{table_name}"
+ end
+
+ indexes(table_name).each do |i|
+ next unless Array(i.columns).join(' ').match?(/\b#{int_column_name}\b/)
+
+ create_bigint_index(table_name, i, int_column_name, bigint_column_name)
+ end
+ end
+
+ # default 'index_name' method is not used because this method can be reused while swapping/dropping the indexes
+ def bigint_index_name(int_column_index_name)
+ # First 20 digits of the hash is chosen to make sure it fits the 63 chars limit
+ digest = Digest::SHA256.hexdigest(int_column_index_name).first(20)
+ "bigint_idx_#{digest}"
+ end
+
+ private
+
+ def create_bigint_index(table_name, index_definition, int_column_name, bigint_column_name)
+ index_attributes = index_definition.as_json
+ index_options = INDEX_OPTIONS_MAP
+ .transform_values { |key| index_attributes[key.to_s] }
+ .select { |_, v| v.present? }
+
+ bigint_index_options = create_bigint_options(
+ index_options,
+ index_definition.name,
+ int_column_name,
+ bigint_column_name
+ )
+
+ add_concurrent_index(
+ table_name,
+ bigint_index_columns(int_column_name, bigint_column_name, index_definition.columns),
+ name: bigint_index_options.delete(:name),
+ ** bigint_index_options
+ )
+ end
+
+ def bigint_index_columns(int_column_name, bigint_column_name, int_index_columns)
+ if int_index_columns.is_a?(String)
+ int_index_columns.gsub(/\b#{int_column_name}\b/, bigint_column_name)
+ else
+ int_index_columns.map do |column|
+ column == int_column_name.to_s ? bigint_column_name : column
+ end
+ end
+ end
+
+ def create_bigint_options(index_options, int_index_name, int_column_name, bigint_column_name)
+ index_options[:name] = bigint_index_name(int_index_name)
+ index_options[:where]&.gsub!(/\b#{int_column_name}\b/, bigint_column_name)
+
+ # ordering on multiple columns will return a Hash instead of string
+ index_options[:order] =
+ if index_options[:order].is_a?(Hash)
+ index_options[:order].to_h do |column, order|
+ column = bigint_column_name if column == int_column_name
+ [column, order]
+ end
+ else
+ index_options[:order]&.gsub(/\b#{int_column_name}\b/, bigint_column_name)
+ end
+
+ index_options.select { |_, v| v.present? }
+ end
end
end
end
diff --git a/lib/gitlab/database/migration_helpers/wraparound_autovacuum.rb b/lib/gitlab/database/migration_helpers/wraparound_autovacuum.rb
index 555efb58606..7f215bc0db7 100644
--- a/lib/gitlab/database/migration_helpers/wraparound_autovacuum.rb
+++ b/lib/gitlab/database/migration_helpers/wraparound_autovacuum.rb
@@ -13,7 +13,7 @@ module Gitlab
# 3. Introduce the migration again for self-managed.
#
def can_execute_on?(*tables)
- return false unless Gitlab.com? || Gitlab.dev_or_test_env?
+ return false unless Gitlab.com_except_jh? || Gitlab.dev_or_test_env?
if wraparound_prevention_on_tables?(tables)
Gitlab::AppLogger.info(message: "Wraparound prevention vacuum detected", class: self.class)
diff --git a/lib/gitlab/database/migrations/batched_background_migration_helpers.rb b/lib/gitlab/database/migrations/batched_background_migration_helpers.rb
index 64cde273a59..3d4ac113bf6 100644
--- a/lib/gitlab/database/migrations/batched_background_migration_helpers.rb
+++ b/lib/gitlab/database/migrations/batched_background_migration_helpers.rb
@@ -72,6 +72,7 @@ module Gitlab
batch_max_value: nil,
batch_class_name: BATCH_CLASS_NAME,
batch_size: BATCH_SIZE,
+ pause_ms: 100,
max_batch_size: nil,
sub_batch_size: SUB_BATCH_SIZE,
gitlab_schema: nil
@@ -105,6 +106,7 @@ module Gitlab
column_name: batch_column_name,
job_arguments: job_arguments,
interval: job_interval,
+ pause_ms: pause_ms,
min_value: batch_min_value,
max_value: batch_max_value,
batch_class_name: batch_class_name,
diff --git a/lib/gitlab/database/migrations/milestone_mixin.rb b/lib/gitlab/database/migrations/milestone_mixin.rb
index 10bc0c192e7..7d78f74d237 100644
--- a/lib/gitlab/database/migrations/milestone_mixin.rb
+++ b/lib/gitlab/database/migrations/milestone_mixin.rb
@@ -19,11 +19,10 @@ module Gitlab
end
end
- def initialize(name = class_name, version = nil, type = nil)
- raise MilestoneNotSetError, "Milestone is not set for #{self.class.name}" if milestone.nil?
+ def initialize(name = self.class.name, version = nil, _type = nil)
+ raise MilestoneNotSetError, "Milestone is not set for #{name}" if milestone.nil?
super(name, version)
- @version = Gitlab::Database::Migrations::Version.new(version, milestone, type)
end
def milestone # rubocop:disable Lint/DuplicateMethods
diff --git a/lib/gitlab/database/partitioning/partition_manager.rb b/lib/gitlab/database/partitioning/partition_manager.rb
index bb70d052e3e..83cd446534c 100644
--- a/lib/gitlab/database/partitioning/partition_manager.rb
+++ b/lib/gitlab/database/partitioning/partition_manager.rb
@@ -89,6 +89,8 @@ module Gitlab
Gitlab::AppLogger.info(message: "Created partition",
partition_name: partition.partition_name,
table_name: partition.table)
+
+ lock_partitions_for_writes(partition) if should_lock_for_writes?
end
model.partitioning_strategy.after_adding_partitions
@@ -205,6 +207,23 @@ module Gitlab
end
end
end
+
+ def should_lock_for_writes?
+ Feature.enabled?(:automatic_lock_writes_on_partition_tables, type: :ops) &&
+ Gitlab::Database.database_mode == Gitlab::Database::MODE_MULTIPLE_DATABASES &&
+ connection != model.connection
+ end
+ strong_memoize_attr :should_lock_for_writes?
+
+ def lock_partitions_for_writes(partition)
+ table_name = "#{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}.#{partition.partition_name}"
+ Gitlab::Database::LockWritesManager.new(
+ table_name: table_name,
+ connection: connection,
+ database_name: @connection_name,
+ with_retries: !connection.transaction_open?
+ ).lock_writes
+ end
end
end
end
diff --git a/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb b/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb
index 1ce0a44e37f..b486ddb8e76 100644
--- a/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb
+++ b/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb
@@ -8,7 +8,8 @@ module Gitlab
include ::Gitlab::Database::MigrationHelpers
include ::Gitlab::Database::MigrationHelpers::LooseForeignKeyHelpers
- ALLOWED_TABLES = %w[audit_events web_hook_logs].freeze
+ ALLOWED_TABLES = %w[audit_events web_hook_logs merge_request_diff_files merge_request_diff_commits].freeze
+
ERROR_SCOPE = 'table partitioning'
MIGRATION_CLASS_NAME = "::#{module_parent_name}::BackfillPartitionedTable"
@@ -16,6 +17,60 @@ module Gitlab
BATCH_INTERVAL = 2.minutes.freeze
BATCH_SIZE = 50_000
SUB_BATCH_SIZE = 2_500
+ PARTITION_BUFFER = 6
+ MIN_ID = 1
+
+ # Creates a partitioned copy of an existing table, using a RANGE partitioning strategy on a int/bigint column.
+ # One partition is created per partition_size between 1 and MAX(column_name). Also installs a trigger on
+ # the original table to copy writes into the partitioned table. To copy over historic data from before creation
+ # of the partitioned table, use the `enqueue_partitioning_data_migration` helper in a post-deploy migration.
+ # Note: If the original table is empty the system creates 6 partitions in the new table.
+ #
+ # A copy of the original table is required as PG currently does not support partitioning existing tables.
+ #
+ # Example:
+ #
+ # partition_table_by_int_range :merge_request_diff_commits, :merge_request_diff_id, partition_size: 500, primary_key: ['merge_request_diff_id', 'relative_order']
+ #
+ # Options are:
+ # :partition_size - a int specifying the partition size
+ # :primary_key - a array specifying the primary query of the new table
+ #
+ # Note: The system always adds a buffer of 6 partitions.
+ def partition_table_by_int_range(table_name, column_name, partition_size:, primary_key:)
+ Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas.require_ddl_mode!
+
+ assert_table_is_allowed(table_name)
+
+ assert_not_in_transaction_block(scope: ERROR_SCOPE)
+
+ current_primary_key = Array.wrap(connection.primary_key(table_name))
+ raise "primary key not defined for #{table_name}" if current_primary_key.blank?
+
+ partition_column = find_column_definition(table_name, column_name)
+ raise "partition column #{column_name} does not exist on #{table_name}" if partition_column.nil?
+
+ primary_key = Array.wrap(primary_key).map(&:to_s)
+ raise "the partition column must be part of the primary key" unless primary_key.include?(column_name.to_s)
+
+ primary_key_objects = connection.columns(table_name).select { |column| primary_key.include?(column.name) }
+
+ raise 'partition_size must be greater than 1' unless partition_size > 1
+
+ max_id = Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas.with_suppressed do
+ Gitlab::Database::QueryAnalyzers::GitlabSchemasValidateConnection.with_suppressed do
+ define_batchable_model(table_name, connection: connection).maximum(column_name) || partition_size * PARTITION_BUFFER
+ end
+ end
+
+ partitioned_table_name = make_partitioned_table_name(table_name)
+
+ with_lock_retries do
+ create_range_id_partitioned_copy(table_name, partitioned_table_name, partition_column, primary_key_objects)
+ create_int_range_partitions(partitioned_table_name, partition_size, MIN_ID, max_id)
+ create_trigger_to_sync_tables(table_name, partitioned_table_name, current_primary_key)
+ end
+ end
# Creates a partitioned copy of an existing table, using a RANGE partitioning strategy on a timestamp column.
# One partition is created per month between the given `min_date` and `max_date`. Also installs a trigger on
@@ -332,6 +387,34 @@ module Gitlab
connection.columns(table).find { |c| c.name == column.to_s }
end
+ def create_range_id_partitioned_copy(source_table_name, partitioned_table_name, partition_column, primary_keys)
+ if table_exists?(partitioned_table_name)
+ Gitlab::AppLogger.warn "Partitioned table not created because it already exists" \
+ " (this may be due to an aborted migration or similar): table_name: #{partitioned_table_name} "
+ return
+ end
+
+ tmp_partitioning_column_name = "#{partition_column.name}_tmp"
+
+ temporary_columns = primary_keys.map { |key| "#{key.name}_tmp" }.join(", ")
+ temporary_columns_statement = build_temporary_columns_statement(primary_keys)
+
+ transaction do
+ execute(<<~SQL)
+ CREATE TABLE #{partitioned_table_name} (
+ LIKE #{source_table_name} INCLUDING ALL EXCLUDING INDEXES,
+ #{temporary_columns_statement},
+ PRIMARY KEY (#{temporary_columns})
+ ) PARTITION BY RANGE (#{tmp_partitioning_column_name})
+ SQL
+
+ primary_keys.each do |key|
+ remove_column(partitioned_table_name, key.name)
+ rename_column(partitioned_table_name, "#{key.name}_tmp", key.name)
+ end
+ end
+ end
+
def create_range_partitioned_copy(source_table_name, partitioned_table_name, partition_column, primary_key)
if table_exists?(partitioned_table_name)
Gitlab::AppLogger.warn "Partitioned table not created because it already exists" \
@@ -382,6 +465,20 @@ module Gitlab
end
end
+ def create_int_range_partitions(table_name, partition_size, min_id, max_id)
+ lower_bound = min_id
+ upper_bound = min_id + partition_size
+
+ end_id = max_id + PARTITION_BUFFER * partition_size # Adds a buffer of 6 partitions
+
+ while lower_bound < end_id
+ create_range_partition_safely("#{table_name}_#{lower_bound}", table_name, lower_bound, upper_bound)
+
+ lower_bound += partition_size
+ upper_bound += partition_size
+ end
+ end
+
def to_sql_date_literal(date)
connection.quote(date.strftime('%Y-%m-%d'))
end
@@ -411,19 +508,23 @@ module Gitlab
return
end
+ unique_key = Array.wrap(unique_key)
+
delimiter = ",\n "
column_names = connection.columns(partitioned_table_name).map(&:name)
set_statements = build_set_statements(column_names, unique_key)
insert_values = column_names.map { |name| "NEW.#{name}" }
+ delete_where_statement = unique_key.map { |unique_key| "#{unique_key} = OLD.#{unique_key}" }.join(' AND ')
+ update_where_statement = unique_key.map { |unique_key| "#{partitioned_table_name}.#{unique_key} = NEW.#{unique_key}" }.join(' AND ')
create_trigger_function(name, replace: false) do
<<~SQL
IF (TG_OP = 'DELETE') THEN
- DELETE FROM #{partitioned_table_name} where #{unique_key} = OLD.#{unique_key};
+ DELETE FROM #{partitioned_table_name} where #{delete_where_statement};
ELSIF (TG_OP = 'UPDATE') THEN
UPDATE #{partitioned_table_name}
SET #{set_statements.join(delimiter)}
- WHERE #{partitioned_table_name}.#{unique_key} = NEW.#{unique_key};
+ WHERE #{update_where_statement};
ELSIF (TG_OP = 'INSERT') THEN
INSERT INTO #{partitioned_table_name} (#{column_names.join(delimiter)})
VALUES (#{insert_values.join(delimiter)});
@@ -433,8 +534,16 @@ module Gitlab
end
end
+ def build_temporary_columns_statement(columns)
+ columns.map do |column|
+ type = column.name == 'id' || column.name.end_with?('_id') ? 'bigint' : column.sql_type
+
+ "#{column.name}_tmp #{type} NOT NULL"
+ end.join(", ")
+ end
+
def build_set_statements(column_names, unique_key)
- column_names.reject { |name| name == unique_key }.map { |name| "#{name} = NEW.#{name}" }
+ column_names.reject { |name| unique_key.include?(name) }.map { |name| "#{name} = NEW.#{name}" }
end
def create_sync_trigger(table_name, trigger_name, function_name)
diff --git a/lib/gitlab/database/query_analyzers/ci/partitioning_routing_analyzer.rb b/lib/gitlab/database/query_analyzers/ci/partitioning_routing_analyzer.rb
index eb55ebc7619..c2f94b7b0e6 100644
--- a/lib/gitlab/database/query_analyzers/ci/partitioning_routing_analyzer.rb
+++ b/lib/gitlab/database/query_analyzers/ci/partitioning_routing_analyzer.rb
@@ -8,7 +8,7 @@ module Gitlab
class PartitioningRoutingAnalyzer < Database::QueryAnalyzers::Base
RoutingTableNotUsedError = Class.new(QueryAnalyzerError)
- ENABLED_TABLES = %w[ci_builds_metadata].freeze
+ ENABLED_TABLES = %w[ci_builds ci_builds_metadata].freeze
class << self
def enabled?
diff --git a/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch.rb b/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch.rb
new file mode 100644
index 00000000000..583aceba098
--- /dev/null
+++ b/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ module QueryAnalyzers
+ class PreventSetOperatorMismatch < Base
+ SetOperatorStarError = Class.new(QueryAnalyzerError)
+
+ DETECT_REGEX = /.*SELECT.+(UNION|EXCEPT|INTERSECT)/i
+
+ class << self
+ def enabled?
+ ::Feature::FlipperFeature.table_exists? &&
+ Feature.enabled?(:query_analyzer_gitlab_schema_metrics, type: :ops)
+ end
+
+ def analyze(parsed)
+ return unless requires_detection?(parsed.sql)
+
+ # Only handle SELECT queries.
+ parsed.pg.tree.stmts.each do |stmt|
+ select_stmt = next_select_stmt(stmt)
+ next unless select_stmt
+
+ types = SelectStmt.new(select_stmt).types
+
+ raise SetOperatorStarError if types.any?(Type::INVALID)
+ end
+ end
+
+ private
+
+ def next_select_stmt(node)
+ return unless node.stmt.respond_to?(:select_stmt)
+
+ node.stmt.select_stmt
+ end
+
+ # This not entirely correct and will run true on `SELECT union_station, ...`
+ def requires_detection?(sql)
+ sql.match DETECT_REGEX
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/columns.rb b/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/columns.rb
new file mode 100644
index 00000000000..87120b8ffce
--- /dev/null
+++ b/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/columns.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ module QueryAnalyzers
+ class PreventSetOperatorMismatch
+ # Columns refer to table columns produced by queries and parts of queries.
+ # If we have `SELECT namespaces.id` then `id` is a column. But, we can also have
+ # `WHERE namespaces.id > 10` and `id` is also a column.
+ #
+ # In static analysis of a SQL query a column source can be ambiguous.
+ # Such as in `SELECT id FROM users, namespaces. In such cases we assume `id` could come from either `users` or
+ # `namespaces`.
+ class Columns
+ class << self
+ # Determine the type of each column in the select statement.
+ # Returns a Set object containing a Types enum.
+ # When an error is found parsing will return immediately.
+ def types(select_stmt)
+ # Forward through any errors when the column refers to a part of the SQL query that is known to include
+ # errors. For example, the column may refer to a column from a CTE that was invalid.
+ return Set.new([Type::INVALID]) if References.errors?(select_stmt.all_references)
+
+ types = Set.new
+
+ # Resolve the type of reference for each target in the select statement.
+ target_list = select_stmt.node.target_list
+ targets = target_list.map(&:res_target)
+ targets.each do |target|
+ target_type = get_target_type(target, select_stmt)
+
+ # A NULL target is of the form:
+ # SELECT NULL::namespaces FROM namespaces
+ types += if Targets.null?(target)
+ # Maintain any errors but otherwise ignore this target.
+ target_type & [Type::INVALID]
+ else
+ target_type
+ end
+ end
+
+ types
+ end
+
+ private
+
+ def get_target_type(target, select_stmt)
+ target_ref_names = Targets.reference_names(target, select_stmt)
+
+ resolved_refs = References.resolved(select_stmt.all_references)
+
+ # Cross reference column references with resolved references.
+ # A resolved reference is part of a SQL query that we were able to analyze already.
+ # A CTE or sub-query would be such a case. The only non-resolvable reference is a table.
+ all_resolved = (target_ref_names - resolved_refs.keys).empty?
+
+ # Is this target `*` such as `SELECT *`.
+ a_star = Targets.a_star?(target)
+
+ if all_resolved
+ # Defer to the reference source types.
+ col_refs = resolved_refs.slice(*target_ref_names)
+ .values
+ .reduce(:union) || Set.new
+
+ if a_star
+ # When * the target forwards through the types of the references.
+ col_refs
+ else
+ # When not * the column is static, but we also forward through any nested errors.
+ (col_refs.to_a & [Type::INVALID]) << Type::STATIC
+ end
+ elsif a_star
+ # This is a * on a table. The * lookup occurs dynamically during query runtime and will
+ # change when the table schema changes.
+ [Type::DYNAMIC]
+ else
+ # This references a column on a table or intermediate result set such as:
+ # SELECT namespaces.id FROM namespaces
+ #
+ # or:
+ # WITH some_cte AS ( ... ) SELECT some_cte.id FROM some_cte
+ [Type::STATIC]
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/common_table_expressions.rb b/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/common_table_expressions.rb
new file mode 100644
index 00000000000..0ab58ff7c6f
--- /dev/null
+++ b/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/common_table_expressions.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+# The CTE in a SELECT can reference CTEs defined by the current scope, but also CTEs defined by earlier scopes.
+# With the following query as an example:
+#
+# WITH some_cte AS (select 1)
+# SELECT *
+# FROM (SELECT * FROM some_cte) subquery
+#
+# The CTE some_cte is visible from within the subquery scope.
+module Gitlab
+ module Database
+ module QueryAnalyzers
+ class PreventSetOperatorMismatch
+ class CommonTableExpressions
+ class << self
+ # Convert CTEs available within this SELECT statement into a set of References.
+ #
+ # @param [PgQuery::Node] node The PgQuery SELECT statement node containing the CTEs.
+ # @param [References] cte_refs Inherited CTEs from scopes that wrap this SELECT statement.
+ def references(node, cte_refs)
+ return cte_refs if node&.with_clause.nil?
+
+ refs = cte_refs.dup
+
+ node.with_clause.ctes.each do |cte|
+ cte_name = name(cte)
+ cte_select_stmt = select_stmt(cte)
+
+ # Resolve the CTE type to dynamic/static/error.
+ refs[cte_name] = if node.with_clause.recursive
+ # Recursive CTEs need special handling to avoid infinite loops.
+ recursive_refs(cte_refs, cte_name, cte_select_stmt)
+ else
+ SelectStmt.new(cte_select_stmt, cte_refs).types
+ end
+ end
+
+ refs
+ end
+
+ private
+
+ def name(cte)
+ cte.common_table_expr.ctename
+ end
+
+ def select_stmt(cte)
+ cte.common_table_expr.ctequery.select_stmt
+ end
+
+ # Return whether the recursive CTE is dynamic/static/error.
+ def recursive_refs(cte_refs, cte_name, select_stmt)
+ # Resolve the non-recursive term before the recursive term.
+ larg_select_stmt = SelectStmt.new(select_stmt.larg, cte_refs)
+ larg_type = larg_select_stmt.types
+ new_cte_refs = cte_refs.merge({ cte_name => larg_type })
+
+ # Now we can resolve the recursive side.
+ rarg_type = SelectStmt.new(select_stmt.rarg, new_cte_refs).types
+
+ final_type = larg_type | rarg_type
+ if final_type.count > 1
+ final_type | [Type::INVALID]
+ else
+ final_type
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/froms.rb b/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/froms.rb
new file mode 100644
index 00000000000..c205243694a
--- /dev/null
+++ b/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/froms.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ module QueryAnalyzers
+ class PreventSetOperatorMismatch
+ class Froms
+ class << self
+ # Parse the FROM part of the SELECT. Construct a mapping of FROM names to their PgQuery node. Recurse any
+ # sub-queries and resolve to a Set of dynamic/static/error.
+ #
+ # Whenever a node is aliased, use the alias name as it's reference and ignore it's original name.
+ #
+ # For example, given:
+ #
+ # SELECT id
+ # FROM namespaces ns
+ #
+ # Return a Hash of { 'ns' => NodeObject }
+ #
+ # @param [PgQuery::Node] node The PgQuery SELECT statement node containing the CTEs.
+ # @param [References] cte_refs Inherited CTEs from scopes that wrap this SELECT statement.
+ #
+ # @return [Hash] name of from references mapped to the node that defines their value, or Set if already
+ # resolved.
+ def references(node, cte_refs)
+ refs = {}
+
+ return refs unless node
+
+ node.from_clause.each do |from|
+ range_var = Node.dig(from, :range_var)
+ range_sq = Node.dig(from, :range_subselect)
+
+ if range_var
+ # FROM some_table
+ # FROM some_table some_alias
+ refs.merge!(range_var_reference(range_var, cte_refs))
+ elsif Node.dig(from, :join_expr)
+ # FROM some_table INNER JOIN other_table
+ range_vars = Node.locate_descendants(from, :range_var)
+ range_vars.each do |range_var|
+ refs.merge!(range_var_reference(range_var, cte_refs))
+ end
+ elsif range_sq
+ # FROM (SELECT ...) some_alias
+ select_stmt = Node.dig(range_sq, :subquery, :select_stmt)
+ refs[range_sq.alias.aliasname] = SelectStmt.new(select_stmt, cte_refs).types
+ end
+ end
+
+ refs
+ end
+
+ private
+
+ def range_var_reference(range_var, cte_refs)
+ relname = Node.dig(range_var, :alias, :aliasname) || range_var.relname
+ reference = cte_refs[range_var.relname] || range_var
+
+ { relname => reference }
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/node.rb b/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/node.rb
new file mode 100644
index 00000000000..ee41eaa9d3a
--- /dev/null
+++ b/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/node.rb
@@ -0,0 +1,118 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ module QueryAnalyzers
+ class PreventSetOperatorMismatch
+ # The Node class allows us to traverse PgQuery nodes with tree like semantics.
+ #
+ # This class balances convenience and performance. The PgQuery nodes are Google::Protobuf::MessageExts which
+ # contain a dynamic set of attributes known as fields. Accessing these fields can cause performance problems
+ # due to the large volume of iterable fields.
+ #
+ # When possible use #dig over the *descendant* methods.
+ #
+ # The filter available to each method reduces the traversed attributes. The default filter only traverses nodes
+ # required to parse for set operator mismatches.
+ class Node
+ class << self
+ include Gitlab::Utils::StrongMemoize
+
+ # The default nodes help speed up traversal. Traversal of other nodes can greatly affect performance.
+ DEFAULT_NODES = %i[
+ a_star
+ alias
+ args
+ column_ref
+ fields
+ func_call
+ join_expr
+ larg
+ range_subselect
+ range_var
+ rarg
+ res_target
+ subquery
+ val
+ ].freeze
+ DEFAULT_FIELD_FILTER = ->(field) { field.is_a?(Integer) || DEFAULT_NODES.include?(field) }.freeze
+
+ # Recurse through children.
+ # The block will yield the child node and the name of that node.
+ # Calling without a block will return an Enumerator.
+ def descendants(node, filter: DEFAULT_FIELD_FILTER, &blk)
+ if blk
+ children(node, filter: filter) do |child_node, child_field|
+ yield(child_node, child_field)
+
+ descendants(child_node, filter: filter, &blk)
+ end
+ nil
+ else
+ enum_for(:descendants, node, filter: filter, &blk)
+ end
+ end
+
+ # Return the first node that matches the field.
+ def locate_descendant(node, field, filter: DEFAULT_FIELD_FILTER)
+ descendants(node, filter: filter).find { |_, child_field| child_field == field }&.first
+ end
+
+ # Return all nodes that match the field.
+ def locate_descendants(node, field, filter: DEFAULT_FIELD_FILTER)
+ descendants(node, filter: filter).select { |_, child_field| child_field == field }.map(&:first)
+ end
+
+ # Like Hash#dig, traverse attributes in sequential order and return the final value.
+ # Return nil if any of the fields are not available.
+ def dig(node, *attrs)
+ obj = node
+ attrs.each do |attr|
+ if obj.respond_to?(attr)
+ obj = obj.public_send(attr) # rubocop:disable GitlabSecurity/PublicSend
+ else
+ obj = nil
+ break
+ end
+ end
+ obj
+ end
+
+ private
+
+ # Interface with a PgQuery result as though it was a tree node.
+ # All elements in a PgQuery result are ancestors of Google::Protobuf::AbstractMessage
+ #
+ # Based off PgQuery's treewalker https://github.com/pganalyze/pg_query/blob/main/lib/pg_query/treewalker.rb
+ def children(node, filter: DEFAULT_FIELD_FILTER, &_blk)
+ attributes = case node
+ when Google::Protobuf::MessageExts
+ descriptor_fields(node.class.descriptor)
+ when Google::Protobuf::RepeatedField
+ node.count.times.to_a
+ end
+
+ attributes.select(&filter).each do |attr|
+ attr_key = attr.is_a?(Symbol) ? attr.to_s : attr
+ child = node[attr_key]
+ next if child.nil?
+
+ yield(child, attr)
+ end
+ end
+
+ def descriptor_fields(descriptor)
+ strong_memoize_with(:descriptor_fields, descriptor) do
+ keys = []
+ descriptor.each do |field|
+ keys << field.name.to_sym
+ end
+ keys
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/references.rb b/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/references.rb
new file mode 100644
index 00000000000..ba6e9752905
--- /dev/null
+++ b/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/references.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+# References form the base data structure of the PreventSetOperatorMismatch query analyzer.
+#
+# A reference refers to a table, CTE, or other named entity in a SQL query. References are a set of mappings between the
+# name of the reference and the PgQuery node that represents that reference in the parsed tree.
+#
+# Given the SQL:
+#
+# WITH some_cte AS (SELECT 1)
+# SELECT *
+# FROM some_cte, users, namespace ns
+#
+# The reference names would be `some_cte`, `users`, `ns`. The reference values are the nodes in the parse tree that
+# represent that reference:
+# - some_cte: the common table expression node
+# - users: nil, being a table
+# - ns: nil, being a table, but importantly we use the alias name
+#
+# A reference can be "resolved". A resolved reference value is a Set of Types. The reference value was a select
+# statement that has since been parsed.
+module Gitlab
+ module Database
+ module QueryAnalyzers
+ class PreventSetOperatorMismatch
+ class References
+ class << self
+ # All references that have already been parsed to determine static/dynamic/error state.
+ # @param [Hash] refs A Hash of reference names mapped to the parse tree node or resolved Set of Types.
+ def resolved(refs)
+ refs.select { |_name, ref| ref.is_a?(Set) }
+ end
+
+ # All references that have not been parsed to determine static/dynamic/error state.
+ # @param [Hash] refs A Hash of reference names mapped to the parse tree node or resolved Set of Types.
+ def unresolved(refs)
+ refs.select { |_name, ref| unresolved?(ref) }
+ end
+
+ # Whether any currently resolved references have resulted in an error state.
+ # @param [Hash] refs A Hash of reference names mapped to the parse tree node or resolved Set of Types.
+ def errors?(refs)
+ resolved(refs).any? { |_, values| values.include?(Type::INVALID) }
+ end
+
+ private
+
+ def resolved?(ref)
+ ref.is_a?(Set)
+ end
+
+ def unresolved?(ref)
+ !resolved?(ref) && table?(ref)
+ end
+
+ def table?(ref)
+ !ref.is_a?(PgQuery::RangeVar)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/select_stmt.rb b/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/select_stmt.rb
new file mode 100644
index 00000000000..bdbcc49f63f
--- /dev/null
+++ b/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/select_stmt.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ module QueryAnalyzers
+ class PreventSetOperatorMismatch
+ class SelectStmt
+ include Gitlab::Utils::StrongMemoize
+
+ attr_reader :node, :cte_references, :all_references
+
+ # @param [PgQuery::SelectStmt] node The PgQuery node of the select statement.
+ # @param [Hash] inherited_cte_references CTE References available to the select statement.
+ def initialize(node, inherited_cte_references = {})
+ @node = node
+ @cte_references = CommonTableExpressions.references(node, inherited_cte_references)
+ from_references = Froms.references(node, cte_references)
+ @all_references = from_references.merge(cte_references)
+ end
+
+ # returns Set of Types.
+ #
+ # STATIC - queries that don't require a database schema lookup. E.g. `SELECT users.id FROM users`
+ # DYNAMIC - queries that require a database schema lookup. E.g. `SELECT users.* FROM users`
+ # INVALID - set operator queries that mix static and dynamic queries.
+ def types
+ if set_operator?
+ resolve_set_operator_select_types
+ else
+ resolve_normal_select_types
+ end
+ end
+
+ private
+
+ # Standard SELECT, not a set operator (UNION/INTERSECT/EXCEPT)
+ def resolve_normal_select_types
+ # Cross reference resolved sources with what is requested by the SELECT.
+ types = Columns.types(self)
+
+ # Mixed dynamic and static queries can be normalized to simply dynamic queries for the purposes of
+ # detecting mismatched set operator parts.
+ types.delete(Type::STATIC) if types.include?(Type::DYNAMIC)
+
+ types
+ end
+
+ # Set operator (UNION/INTERSECT/EXCEPT)
+ def resolve_set_operator_select_types
+ types = Set.new
+
+ # Recurse each set operator part as a SELECT statement.
+ # select statement part => type
+ set_operator_parts do |part|
+ types += SelectStmt.new(part, cte_references).types
+ end
+
+ types << Type::INVALID if types.count > 1
+
+ types
+ end
+
+ def set_operator?
+ !(node.respond_to?(:op) && node.op == :SETOP_NONE)
+ end
+
+ SET_OPERATOR_PART_LOCATIONS = %i[larg rarg].freeze
+ private_constant :SET_OPERATOR_PART_LOCATIONS
+
+ def set_operator_parts(&_blk)
+ return unless node
+
+ yield node if node.op == :SETOP_NONE
+ yield node.larg if node.larg && node.larg.op == :SETOP_NONE
+ yield node.rarg if node.rarg && node.rarg.op == :SETOP_NONE
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/targets.rb b/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/targets.rb
new file mode 100644
index 00000000000..99db368efcb
--- /dev/null
+++ b/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/targets.rb
@@ -0,0 +1,97 @@
+# frozen_string_literal: true
+
+# Targets refer to SELECT columns but also JOIN fields, etc.
+# A target can have a qualifying reference to some other entity like a table or CTE.
+module Gitlab
+ module Database
+ module QueryAnalyzers
+ class PreventSetOperatorMismatch
+ class Targets
+ class << self
+ # Return the reference names used by the given target.
+ #
+ # For example:
+ # `SELECT users.id` would return ['users']
+ # `SELECT * FROM users, namespaces` would return ['users', 'namespaces']
+ def reference_names(target, select_stmt)
+ # Parse all targets to determine what is referenced.
+ fields = fields(target)
+ case fields.count
+ when 0
+ literal_ref_names(target, select_stmt)
+ when 1
+ unqualified_ref_names(fields, select_stmt)
+ else
+ # The target is qualified such as SELECT reference.id
+ field_ref = fields[fields.count - 2]
+ [field_ref.string.sval]
+ end
+ end
+
+ # True when `SELECT *`
+ def a_star?(target)
+ Node.locate_descendant(target, :a_star)
+ end
+
+ # Null targets are used to produce "polymorphic" query result sets that can be aggregated through a UNION
+ # without having to worry about mismatched columns.
+ #
+ # A null target would be something like:
+ # SELECT NULL::namespaces FROM namespaces
+ def null?(target)
+ target&.val&.type_cast&.arg&.a_const&.isnull
+ end
+
+ private
+
+ def literal_ref_names(target, select_stmt)
+ # The target is unqualified and is not part of a column_ref, such as in `SELECT 1`.
+ # These include targets like literals, functions, and subselects.
+ sub_select_stmt = subselect_select_stmt(target)
+ if sub_select_stmt
+ name = (target.name.presence || "loc_#{target.location}")
+ # The select is anonymous, so we provide a name.
+ k = "#{name}_subselect"
+ # Force parsing of the select.
+ # We don't care about the static/dynamic nature in this case, but we do need to parse for
+ # any nested error states.
+ sub_select = SelectStmt.new(sub_select_stmt, select_stmt.cte_references)
+ select_stmt.all_references[k] = sub_select.types
+ [k]
+ else
+ # TODO we need to parse function references. Assuming no sources for now.
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/428102
+ []
+ end
+ end
+
+ def unqualified_ref_names(fields, select_stmt)
+ # The target is unqualified, but is part of a column_ref.
+ # E.g. `SELECT id FROM namespaces` or `SELECT namespaces FROM namespaces`
+
+ # Otherwise, check all FROM/JOIN/CTE entries.
+ field = fields[0]
+ field_sval = field&.string&.sval
+ if field_sval && select_stmt.all_references.key?(field_sval)
+ # SELECT some_table_name
+ [field.string.sval]
+ else
+ # SELECT *
+ # SELECT some_column
+ select_stmt.all_references.keys
+ end
+ end
+
+ def fields(target)
+ Node.locate_descendants(target, :fields).flatten
+ end
+
+ def subselect_select_stmt(target)
+ Node.dig(target, :val, :sub_link, :subselect, :select_stmt)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/type.rb b/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/type.rb
new file mode 100644
index 00000000000..5988f963827
--- /dev/null
+++ b/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/type.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ module QueryAnalyzers
+ class PreventSetOperatorMismatch
+ # An enumerated set of constants that represent the state of the parse.
+ module Type
+ STATIC = :static
+ DYNAMIC = :dynamic
+ INVALID = :invalid
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/schema_cache_with_renamed_table.rb b/lib/gitlab/database/schema_cache_with_renamed_table.rb
index 6da76803f7c..e110fc44b7b 100644
--- a/lib/gitlab/database/schema_cache_with_renamed_table.rb
+++ b/lib/gitlab/database/schema_cache_with_renamed_table.rb
@@ -11,26 +11,26 @@ module Gitlab
clear_renamed_tables_cache!
end
- def clear_data_source_cache!(name)
- super(name)
+ def clear_data_source_cache!(connection, table_name)
+ super(connection, table_name)
clear_renamed_tables_cache!
end
- def primary_keys(table_name)
- super(underlying_table(table_name))
+ def primary_keys(connection, table_name)
+ super(connection, underlying_table(table_name))
end
- def columns(table_name)
- super(underlying_table(table_name))
+ def columns(connection, table_name)
+ super(connection, underlying_table(table_name))
end
- def columns_hash(table_name)
- super(underlying_table(table_name))
+ def columns_hash(connection, table_name)
+ super(connection, underlying_table(table_name))
end
- def indexes(table_name)
- super(underlying_table(table_name))
+ def indexes(connection, table_name)
+ super(connection, underlying_table(table_name))
end
private
@@ -40,7 +40,7 @@ module Gitlab
end
def renamed_tables_cache
- @renamed_tables ||= Gitlab::Database::TABLES_TO_BE_RENAMED.select do |old_name, new_name|
+ @renamed_tables ||= Gitlab::Database::TABLES_TO_BE_RENAMED.select do |old_name, _new_name|
connection.view_exists?(old_name)
end
end
diff --git a/lib/gitlab/database/schema_cache_with_renamed_table_legacy.rb b/lib/gitlab/database/schema_cache_with_renamed_table_legacy.rb
new file mode 100644
index 00000000000..acc9bbd0aff
--- /dev/null
+++ b/lib/gitlab/database/schema_cache_with_renamed_table_legacy.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ # This is a legacy extension targeted at Rails versions prior to 7.1
+ # In Rails 7.1, the method parameters have been changed to (connection, table_name)
+ module SchemaCacheWithRenamedTableLegacy
+ # Override methods in ActiveRecord::ConnectionAdapters::SchemaCache
+
+ def clear!
+ super
+
+ clear_renamed_tables_cache!
+ end
+
+ def clear_data_source_cache!(name)
+ super(name)
+
+ clear_renamed_tables_cache!
+ end
+
+ def primary_keys(table_name)
+ super(underlying_table(table_name))
+ end
+
+ def columns(table_name)
+ super(underlying_table(table_name))
+ end
+
+ def columns_hash(table_name)
+ super(underlying_table(table_name))
+ end
+
+ def indexes(table_name)
+ super(underlying_table(table_name))
+ end
+
+ private
+
+ def underlying_table(table_name)
+ renamed_tables_cache.fetch(table_name, table_name)
+ end
+
+ def renamed_tables_cache
+ @renamed_tables ||= Gitlab::Database::TABLES_TO_BE_RENAMED.select do |old_name, _new_name|
+ connection.view_exists?(old_name)
+ end
+ end
+
+ def clear_renamed_tables_cache!
+ @renamed_tables = nil # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/tables_locker.rb b/lib/gitlab/database/tables_locker.rb
index aa880b709fe..608dea9e3c5 100644
--- a/lib/gitlab/database/tables_locker.rb
+++ b/lib/gitlab/database/tables_locker.rb
@@ -3,7 +3,7 @@
module Gitlab
module Database
class TablesLocker
- GITLAB_SCHEMAS_TO_IGNORE = %i[gitlab_embedding gitlab_geo].freeze
+ GITLAB_SCHEMAS_TO_IGNORE = %i[gitlab_embedding gitlab_geo gitlab_jh].freeze
def initialize(logger: nil, dry_run: false, include_partitions: true)
@logger = logger
diff --git a/lib/gitlab/database/tables_truncate.rb b/lib/gitlab/database/tables_truncate.rb
index f91146fff3d..5394dee6fec 100644
--- a/lib/gitlab/database/tables_truncate.rb
+++ b/lib/gitlab/database/tables_truncate.rb
@@ -3,7 +3,7 @@
module Gitlab
module Database
class TablesTruncate
- GITLAB_SCHEMAS_TO_IGNORE = %i[gitlab_geo gitlab_embedding].freeze
+ GITLAB_SCHEMAS_TO_IGNORE = %i[gitlab_geo gitlab_embedding gitlab_jh].freeze
def initialize(database_name:, min_batch_size: 5, logger: nil, until_table: nil, dry_run: false)
@database_name = database_name
diff --git a/lib/gitlab/discussions_diff/file_collection.rb b/lib/gitlab/discussions_diff/file_collection.rb
index 60b3a1738f1..3d1f7ab86b3 100644
--- a/lib/gitlab/discussions_diff/file_collection.rb
+++ b/lib/gitlab/discussions_diff/file_collection.rb
@@ -25,8 +25,9 @@ module Gitlab
#
# - Highlight cache is written just for uncached diff files
# - The cache content is not updated (there's no need to do so)
- def load_highlight
- ids = highlightable_collection_ids
+ # - Load only the related diff note ids
+ def load_highlight(diff_note_ids: nil)
+ ids = highlightable_collection_ids(diff_note_ids)
return if ids.empty?
cached_content = read_cache(ids)
@@ -47,8 +48,13 @@ module Gitlab
private
- def highlightable_collection_ids
- each.with_object([]) { |file, memo| memo << file.id unless file.resolved_at }
+ def highlightable_collection_ids(diff_note_ids)
+ each.with_object([]) do |file, memo|
+ # We ignore if file is resolved, or not part of the highlight requested notes
+ next if file.resolved_at || (diff_note_ids.present? && diff_note_ids.exclude?(file.diff_note_id))
+
+ memo << file.id
+ end
end
def read_cache(ids)
diff --git a/lib/gitlab/email/handler/service_desk_handler.rb b/lib/gitlab/email/handler/service_desk_handler.rb
index ebc4e9c2c8c..e3249b143c8 100644
--- a/lib/gitlab/email/handler/service_desk_handler.rb
+++ b/lib/gitlab/email/handler/service_desk_handler.rb
@@ -38,7 +38,7 @@ module Gitlab
create_issue_or_note
if from_address
- add_email_participant
+ add_email_participants
send_thank_you_email unless reply_email?
end
end
@@ -215,6 +215,10 @@ module Gitlab
end
strong_memoize_attr :to_address
+ def cc_addresses
+ mail.cc || []
+ end
+
def can_handle_legacy_format?
project_path && project_path.include?('/') && !mail_key.include?('+')
end
@@ -223,11 +227,33 @@ module Gitlab
Users::Internal.support_bot
end
- def add_email_participant
+ def add_email_participants
return if reply_email? && !Feature.enabled?(:issue_email_participants, @issue.project)
@issue.issue_email_participants.create(email: from_address)
+
+ add_external_participants_from_cc
+ end
+
+ def add_external_participants_from_cc
+ return if project.service_desk_setting.nil?
+ return unless project.service_desk_setting.add_external_participants_from_cc?
+
+ cc_addresses.each do |email|
+ next if service_desk_addresses.include?(email)
+
+ @issue.issue_email_participants.create!(email: email)
+ end
+ end
+
+ def service_desk_addresses
+ [
+ project.service_desk_incoming_address,
+ project.service_desk_alias_address,
+ project.service_desk_custom_address
+ ].compact
end
+ strong_memoize_attr :service_desk_addresses
end
end
end
diff --git a/lib/gitlab/emoji.rb b/lib/gitlab/emoji.rb
index 7d47bfe88fe..1a7a2fba2f3 100644
--- a/lib/gitlab/emoji.rb
+++ b/lib/gitlab/emoji.rb
@@ -6,7 +6,7 @@ module Gitlab
# When updating emoji assets increase the version below
# and update the version number in `app/assets/javascripts/emoji/index.js`
- EMOJI_VERSION = 2
+ EMOJI_VERSION = 3
# Return a Pathname to emoji's current versioned folder
#
diff --git a/lib/gitlab/encrypted_command_base.rb b/lib/gitlab/encrypted_command_base.rb
index b35c28b85cd..679d9d8e31a 100644
--- a/lib/gitlab/encrypted_command_base.rb
+++ b/lib/gitlab/encrypted_command_base.rb
@@ -7,12 +7,12 @@ module Gitlab
EDIT_COMMAND_NAME = "base"
class << self
- def encrypted_secrets
+ def encrypted_secrets(**args)
raise NotImplementedError
end
- def write(contents)
- encrypted = encrypted_secrets
+ def write(contents, args: {})
+ encrypted = encrypted_secrets(**args)
return unless validate_config(encrypted)
validate_contents(contents)
@@ -25,8 +25,8 @@ module Gitlab
warn "Couldn't decrypt #{encrypted.content_path}. Perhaps you passed the wrong key?"
end
- def edit
- encrypted = encrypted_secrets
+ def edit(args: {})
+ encrypted = encrypted_secrets(**args)
return unless validate_config(encrypted)
if ENV["EDITOR"].blank?
@@ -58,8 +58,8 @@ module Gitlab
temp_file&.unlink
end
- def show
- encrypted = encrypted_secrets
+ def show(args: {})
+ encrypted = encrypted_secrets(**args)
return unless validate_config(encrypted)
puts encrypted.read.presence || "File '#{encrypted.content_path}' does not exist. Use `gitlab-rake #{self::EDIT_COMMAND_NAME}` to change that."
diff --git a/lib/gitlab/encrypted_configuration.rb b/lib/gitlab/encrypted_configuration.rb
index 6b64281e631..5ead57e17fd 100644
--- a/lib/gitlab/encrypted_configuration.rb
+++ b/lib/gitlab/encrypted_configuration.rb
@@ -30,7 +30,7 @@ module Gitlab
end
def initialize(content_path: nil, base_key: nil, previous_keys: [])
- @content_path = Pathname.new(content_path).yield_self { |path| path.symlink? ? path.realpath : path } if content_path
+ @content_path = Pathname.new(content_path).then { |path| path.symlink? ? path.realpath : path } if content_path
@key = self.class.generate_key(base_key) if base_key
@previous_keys = previous_keys
end
diff --git a/lib/gitlab/encrypted_ldap_command.rb b/lib/gitlab/encrypted_ldap_command.rb
index 5e1eabe7ec6..442c675f19e 100644
--- a/lib/gitlab/encrypted_ldap_command.rb
+++ b/lib/gitlab/encrypted_ldap_command.rb
@@ -1,6 +1,5 @@
# frozen_string_literal: true
-# rubocop:disable Rails/Output
module Gitlab
class EncryptedLdapCommand < EncryptedCommandBase
DISPLAY_NAME = "LDAP"
@@ -21,4 +20,3 @@ module Gitlab
end
end
end
-# rubocop:enable Rails/Output
diff --git a/lib/gitlab/encrypted_redis_command.rb b/lib/gitlab/encrypted_redis_command.rb
new file mode 100644
index 00000000000..608edcdb950
--- /dev/null
+++ b/lib/gitlab/encrypted_redis_command.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+# rubocop:disable Rails/Output
+module Gitlab
+ class EncryptedRedisCommand < EncryptedCommandBase
+ DISPLAY_NAME = "Redis"
+ EDIT_COMMAND_NAME = "gitlab:redis:secret:edit"
+
+ class << self
+ def all_redis_instance_class_names
+ Gitlab::Redis::ALL_CLASSES.map do |c|
+ normalized_instance_name(c)
+ end
+ end
+
+ def normalized_instance_name(instance)
+ if instance.is_a?(Class)
+ # Gitlab::Redis::SharedState => sharedstate
+ instance.name.demodulize.to_s.downcase
+ else
+ # Drop all hyphens, underscores, and spaces from the name
+ # eg.: shared_state => sharedstate
+ instance.gsub(/[-_ ]/, '').downcase
+ end
+ end
+
+ def encrypted_secrets(**args)
+ if args[:instance_name]
+ instance_class = Gitlab::Redis::ALL_CLASSES.find do |instance|
+ normalized_instance_name(instance) == normalized_instance_name(args[:instance_name])
+ end
+
+ unless instance_class
+ error_message = <<~MSG
+ Specified instance name #{args[:instance_name]} does not exist.
+ The available instances are #{all_redis_instance_class_names.join(', ')}."
+ MSG
+
+ raise error_message
+ end
+ else
+ instance_class = Gitlab::Redis::Cache
+ end
+
+ instance_class.encrypted_secrets
+ end
+
+ def encrypted_file_template
+ <<~YAML
+ # password: '123'
+ YAML
+ end
+ end
+ end
+end
+# rubocop:enable Rails/Output
diff --git a/lib/gitlab/file_detector.rb b/lib/gitlab/file_detector.rb
index 13959f6aa68..ef8f2d4d61b 100644
--- a/lib/gitlab/file_detector.rb
+++ b/lib/gitlab/file_detector.rb
@@ -21,7 +21,7 @@ module Gitlab
# Configuration files
gitignore: '.gitignore',
- gitlab_ci: '.gitlab-ci.yml',
+ gitlab_ci: ::Ci::Pipeline::DEFAULT_CONFIG_PATH,
route_map: '.gitlab/route-map.yml',
# Dependency files
diff --git a/lib/gitlab/git/blame.rb b/lib/gitlab/git/blame.rb
index 3d2bde6f0a7..e134fb31879 100644
--- a/lib/gitlab/git/blame.rb
+++ b/lib/gitlab/git/blame.rb
@@ -4,7 +4,6 @@ module Gitlab
module Git
class Blame
include Gitlab::EncodingHelper
- include Gitlab::Git::WrapsGitalyErrors
attr_reader :lines, :blames, :range
@@ -35,11 +34,9 @@ module Gitlab
end
def fetch_raw_blame
- wrapped_gitaly_errors do
- @repo.gitaly_commit_client.raw_blame(@sha, @path, range: range_spec)
- end
- # Return empty result when blame range is out-of-range
+ @repo.gitaly_commit_client.raw_blame(@sha, @path, range: range_spec)
rescue ArgumentError
+ # Return an empty result when the blame range is out-of-range or path is not found
""
end
diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb
index ae90291c0a3..3744c81f51d 100644
--- a/lib/gitlab/git/blob.rb
+++ b/lib/gitlab/git/blob.rb
@@ -230,5 +230,3 @@ module Gitlab
end
end
end
-
-Gitlab::Git::Blob.singleton_class.prepend Gitlab::Git::RuggedImpl::Blob::ClassMethods
diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb
index 571dde6fcfc..1086ea45a7a 100644
--- a/lib/gitlab/git/commit.rb
+++ b/lib/gitlab/git/commit.rb
@@ -5,7 +5,6 @@ module Gitlab
module Git
class Commit
include Gitlab::EncodingHelper
- prepend Gitlab::Git::RuggedImpl::Commit
extend Gitlab::Git::WrapsGitalyErrors
include Gitlab::Utils::StrongMemoize
@@ -502,5 +501,3 @@ module Gitlab
end
end
end
-
-Gitlab::Git::Commit.singleton_class.prepend Gitlab::Git::RuggedImpl::Commit::ClassMethods
diff --git a/lib/gitlab/git/ref.rb b/lib/gitlab/git/ref.rb
index 4a09f866db4..205dd5be35a 100644
--- a/lib/gitlab/git/ref.rb
+++ b/lib/gitlab/git/ref.rb
@@ -4,7 +4,6 @@ module Gitlab
module Git
class Ref
include Gitlab::EncodingHelper
- include Gitlab::Git::RuggedImpl::Ref
# Branch or tag name
# without "refs/tags|heads" prefix
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index a98cf95edf4..db6e6b4d00b 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -11,7 +11,6 @@ module Gitlab
include Gitlab::Git::WrapsGitalyErrors
include Gitlab::EncodingHelper
include Gitlab::Utils::StrongMemoize
- prepend Gitlab::Git::RuggedImpl::Repository
SEARCH_CONTEXT_LINES = 3
REV_LIST_COMMIT_LIMIT = 2_000
diff --git a/lib/gitlab/git/rugged_impl/blob.rb b/lib/gitlab/git/rugged_impl/blob.rb
deleted file mode 100644
index dc869ff5279..00000000000
--- a/lib/gitlab/git/rugged_impl/blob.rb
+++ /dev/null
@@ -1,107 +0,0 @@
-# frozen_string_literal: true
-
-# NOTE: This code is legacy. Do not add/modify code here unless you have
-# discussed with the Gitaly team. See
-# https://docs.gitlab.com/ee/development/gitaly.html#legacy-rugged-code
-# for more details.
-
-module Gitlab
- module Git
- module RuggedImpl
- module Blob
- module ClassMethods
- extend ::Gitlab::Utils::Override
- include Gitlab::Git::RuggedImpl::UseRugged
-
- override :tree_entry
- def tree_entry(repository, sha, path, limit)
- if use_rugged?(repository, :rugged_tree_entry)
- execute_rugged_call(:rugged_tree_entry, repository, sha, path, limit)
- else
- super
- end
- end
-
- private
-
- def rugged_tree_entry(repository, sha, path, limit)
- return unless path
-
- # Strip any leading / characters from the path
- path = path.sub(%r{\A/*}, '')
-
- rugged_commit = repository.lookup(sha)
- root_tree = rugged_commit.tree
-
- blob_entry = find_entry_by_path(repository, root_tree.oid, *path.split('/'))
-
- return unless blob_entry
-
- if blob_entry[:type] == :commit
- submodule_blob(blob_entry, path, sha)
- else
- blob = repository.lookup(blob_entry[:oid])
-
- if blob
- new(
- id: blob.oid,
- name: blob_entry[:name],
- size: blob.size,
- # Rugged::Blob#content is expensive; don't call it if we don't have to.
- data: limit == 0 ? '' : blob.content(limit),
- mode: blob_entry[:filemode].to_s(8),
- path: path,
- commit_id: sha,
- binary: blob.binary?
- )
- end
- end
- rescue Rugged::ReferenceError
- nil
- end
-
- # Recursive search of blob id by path
- #
- # Ex.
- # blog/ # oid: 1a
- # app/ # oid: 2a
- # models/ # oid: 3a
- # file.rb # oid: 4a
- #
- #
- # Blob.find_entry_by_path(repo, '1a', 'blog', 'app', 'file.rb') # => '4a'
- #
- def find_entry_by_path(repository, root_id, *path_parts)
- root_tree = repository.lookup(root_id)
-
- entry = root_tree.find do |entry|
- entry[:name] == path_parts[0]
- end
-
- return unless entry
-
- if path_parts.size > 1
- return unless entry[:type] == :tree
-
- path_parts.shift
- find_entry_by_path(repository, entry[:oid], *path_parts)
- else
- [:blob, :commit].include?(entry[:type]) ? entry : nil
- end
- end
-
- def submodule_blob(blob_entry, path, sha)
- new(
- id: blob_entry[:oid],
- name: blob_entry[:name],
- size: 0,
- data: '',
- path: path,
- commit_id: sha
- )
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/git/rugged_impl/commit.rb b/lib/gitlab/git/rugged_impl/commit.rb
deleted file mode 100644
index cf547414b0d..00000000000
--- a/lib/gitlab/git/rugged_impl/commit.rb
+++ /dev/null
@@ -1,115 +0,0 @@
-# frozen_string_literal: true
-
-# NOTE: This code is legacy. Do not add/modify code here unless you have
-# discussed with the Gitaly team. See
-# https://docs.gitlab.com/ee/development/gitaly.html#legacy-rugged-code
-# for more details.
-
-# rubocop:disable Gitlab/ModuleWithInstanceVariables
-module Gitlab
- module Git
- module RuggedImpl
- module Commit
- module ClassMethods
- extend ::Gitlab::Utils::Override
- include Gitlab::Git::RuggedImpl::UseRugged
-
- def rugged_find(repo, commit_id)
- obj = repo.rev_parse_target(commit_id)
-
- obj.is_a?(::Rugged::Commit) ? obj : nil
- rescue ::Rugged::Error
- nil
- end
-
- # This needs to return an array of Gitlab::Git:Commit objects
- # instead of Rugged::Commit objects to ensure upstream models
- # operate on a consistent interface. Unlike
- # Gitlab::Git::Commit.find, Gitlab::Git::Commit.batch_by_oid
- # doesn't attempt to decorate the result.
- def rugged_batch_by_oid(repo, oids)
- oids.map { |oid| rugged_find(repo, oid) }
- .compact
- .map { |commit| decorate(repo, commit) }
- # Match Gitaly's list_commits_by_oid behavior
- rescue ::Gitlab::Git::Repository::NoRepository
- []
- end
-
- override :find_commit
- def find_commit(repo, commit_id)
- if use_rugged?(repo, :rugged_find_commit)
- execute_rugged_call(:rugged_find, repo, commit_id)
- else
- super
- end
- end
-
- override :batch_by_oid
- def batch_by_oid(repo, oids)
- if use_rugged?(repo, :rugged_list_commits_by_oid)
- execute_rugged_call(:rugged_batch_by_oid, repo, oids)
- else
- super
- end
- end
- end
-
- extend ::Gitlab::Utils::Override
- include Gitlab::Git::RuggedImpl::UseRugged
-
- override :init_commit
- def init_commit(raw_commit)
- case raw_commit
- when ::Rugged::Commit
- init_from_rugged(raw_commit)
- else
- super
- end
- end
-
- override :commit_tree_entry
- def commit_tree_entry(path)
- if use_rugged?(@repository, :rugged_commit_tree_entry)
- execute_rugged_call(:rugged_tree_entry, path)
- else
- super
- end
- end
-
- # Is this the same as Blob.find_entry_by_path ?
- def rugged_tree_entry(path)
- rugged_commit.tree.path(path)
- rescue Rugged::TreeError
- nil
- end
-
- def rugged_commit
- @rugged_commit ||= if raw_commit.is_a?(Rugged::Commit)
- raw_commit
- else
- @repository.rev_parse_target(id)
- end
- end
-
- def init_from_rugged(commit)
- author = commit.author
- committer = commit.committer
-
- @raw_commit = commit
- @id = commit.oid
- @message = commit.message
- @authored_date = author[:time]
- @committed_date = committer[:time]
- @author_name = author[:name]
- @author_email = author[:email]
- @committer_name = committer[:name]
- @committer_email = committer[:email]
- @parent_ids = commit.parents.map(&:oid)
- @trailers = Hash[commit.trailers]
- end
- end
- end
- end
-end
-# rubocop:enable Gitlab/ModuleWithInstanceVariables
diff --git a/lib/gitlab/git/rugged_impl/ref.rb b/lib/gitlab/git/rugged_impl/ref.rb
deleted file mode 100644
index b553e82dc47..00000000000
--- a/lib/gitlab/git/rugged_impl/ref.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-# frozen_string_literal: true
-
-# NOTE: This code is legacy. Do not add/modify code here unless you have
-# discussed with the Gitaly team. See
-# https://docs.gitlab.com/ee/development/gitaly.html#legacy-rugged-code
-# for more details.
-
-module Gitlab
- module Git
- module RuggedImpl
- module Ref
- def self.dereference_object(object)
- object = object.target while object.is_a?(::Rugged::Tag::Annotation)
-
- object
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/git/rugged_impl/repository.rb b/lib/gitlab/git/rugged_impl/repository.rb
deleted file mode 100644
index cd4eefa158e..00000000000
--- a/lib/gitlab/git/rugged_impl/repository.rb
+++ /dev/null
@@ -1,79 +0,0 @@
-# frozen_string_literal: true
-
-# NOTE: This code is legacy. Do not add/modify code here unless you have
-# discussed with the Gitaly team. See
-# https://docs.gitlab.com/ee/development/gitaly.html#legacy-rugged-code
-# for more details.
-
-# rubocop:disable Gitlab/ModuleWithInstanceVariables
-module Gitlab
- module Git
- module RuggedImpl
- module Repository
- extend ::Gitlab::Utils::Override
- include Gitlab::Git::RuggedImpl::UseRugged
-
- FEATURE_FLAGS = %i[rugged_find_commit rugged_tree_entries rugged_tree_entry rugged_commit_is_ancestor rugged_commit_tree_entry rugged_list_commits_by_oid].freeze
-
- def alternate_object_directories
- relative_object_directories.map { |d| File.join(path, d) }
- end
-
- ALLOWED_OBJECT_RELATIVE_DIRECTORIES_VARIABLES = %w[
- GIT_OBJECT_DIRECTORY_RELATIVE
- GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE
- ].freeze
-
- def relative_object_directories
- Gitlab::Git::HookEnv.all(gl_repository).values_at(*ALLOWED_OBJECT_RELATIVE_DIRECTORIES_VARIABLES).flatten.compact
- end
-
- def rugged
- @rugged ||= ::Rugged::Repository.new(path, alternates: alternate_object_directories)
- rescue ::Rugged::RepositoryError, ::Rugged::OSError
- raise ::Gitlab::Git::Repository::NoRepository, 'no repository for such path'
- end
-
- def cleanup
- @rugged&.close
- end
-
- # Return the object that +revspec+ points to. If +revspec+ is an
- # annotated tag, then return the tag's target instead.
- def rev_parse_target(revspec)
- obj = rugged.rev_parse(revspec)
- Ref.dereference_object(obj)
- end
-
- override :ancestor?
- def ancestor?(from, to)
- if use_rugged?(self, :rugged_commit_is_ancestor)
- execute_rugged_call(:rugged_is_ancestor?, from, to)
- else
- super
- end
- end
-
- def rugged_is_ancestor?(ancestor_id, descendant_id)
- return false if ancestor_id.nil? || descendant_id.nil?
-
- rugged_merge_base(ancestor_id, descendant_id) == ancestor_id
- rescue Rugged::OdbError
- false
- end
-
- def rugged_merge_base(from, to)
- rugged.merge_base(from, to)
- rescue Rugged::ReferenceError
- nil
- end
-
- # Lookup for rugged object by oid or ref name
- def lookup(oid_or_ref_name)
- rev_parse_target(oid_or_ref_name)
- end
- end
- end
- end
-end
-# rubocop:enable Gitlab/ModuleWithInstanceVariables
diff --git a/lib/gitlab/git/rugged_impl/tree.rb b/lib/gitlab/git/rugged_impl/tree.rb
deleted file mode 100644
index bc3ff01e1e2..00000000000
--- a/lib/gitlab/git/rugged_impl/tree.rb
+++ /dev/null
@@ -1,147 +0,0 @@
-# frozen_string_literal: true
-
-# NOTE: This code is legacy. Do not add/modify code here unless you have
-# discussed with the Gitaly team. See
-# https://docs.gitlab.com/ee/development/gitaly.html#legacy-rugged-code
-# for more details.
-
-module Gitlab
- module Git
- module RuggedImpl
- module Tree
- module ClassMethods
- extend ::Gitlab::Utils::Override
- include Gitlab::Git::RuggedImpl::UseRugged
-
- TREE_SORT_ORDER = { tree: 0, blob: 1, commit: 2 }.freeze
-
- override :tree_entries
- def tree_entries(repository, sha, path, recursive, skip_flat_paths, rescue_not_found, pagination_params = nil)
- if use_rugged?(repository, :rugged_tree_entries)
- entries = execute_rugged_call(
- :tree_entries_with_flat_path_from_rugged, repository, sha, path, recursive, skip_flat_paths)
-
- if pagination_params
- paginated_response(entries, pagination_params[:limit], pagination_params[:page_token].to_s)
- else
- [entries, nil]
- end
- else
- super
- end
- end
-
- # Rugged version of TreePagination in Go: https://gitlab.com/gitlab-org/gitaly/-/merge_requests/3611
- def paginated_response(entries, limit, token)
- total_entries = entries.count
-
- return [[], nil] if limit == 0 || limit.blank?
-
- entries = Gitlab::Utils.stable_sort_by(entries) { |x| TREE_SORT_ORDER[x.type] }
-
- if token.blank?
- index = 0
- else
- index = entries.index { |entry| entry.id == token }
-
- raise Gitlab::Git::CommandError, "could not find starting OID: #{token}" if index.nil?
-
- index += 1
- end
-
- return [entries[index..], nil] if limit < 0
-
- last_index = index + limit
- result = entries[index...last_index]
-
- if last_index < total_entries
- cursor = Gitaly::PaginationCursor.new(next_cursor: result.last.id)
- end
-
- [result, cursor]
- end
-
- def tree_entries_with_flat_path_from_rugged(repository, sha, path, recursive, skip_flat_paths)
- tree_entries_from_rugged(repository, sha, path, recursive).tap do |entries|
- # This was an optimization to reduce N+1 queries for Gitaly
- # (https://gitlab.com/gitlab-org/gitaly/issues/530).
- rugged_populate_flat_path(repository, sha, path, entries) unless skip_flat_paths
- end
- end
-
- def tree_entries_from_rugged(repository, sha, path, recursive)
- current_path_entries = get_tree_entries_from_rugged(repository, sha, path)
- ordered_entries = []
-
- current_path_entries.each do |entry|
- ordered_entries << entry
-
- if recursive && entry.dir?
- ordered_entries.concat(tree_entries_from_rugged(repository, sha, entry.path, true))
- end
- end
-
- ordered_entries
- end
-
- def rugged_populate_flat_path(repository, sha, path, entries)
- entries.each do |entry|
- entry.flat_path = entry.path
-
- next unless entry.dir?
-
- entry.flat_path =
- if path
- File.join(path, rugged_flatten_tree(repository, sha, entry, path))
- else
- rugged_flatten_tree(repository, sha, entry, path)
- end
- end
- end
-
- # Returns the relative path of the first subdir that doesn't have only one directory descendant
- def rugged_flatten_tree(repository, sha, tree, root_path)
- subtree = tree_entries_from_rugged(repository, sha, tree.path, false)
-
- if subtree.count == 1 && subtree.first.dir?
- File.join(tree.name, rugged_flatten_tree(repository, sha, subtree.first, root_path))
- else
- tree.name
- end
- end
-
- def get_tree_entries_from_rugged(repository, sha, path)
- commit = repository.lookup(sha)
- root_tree = commit.tree
-
- tree = if path
- id = find_id_by_path(repository, root_tree.oid, path)
- if id
- repository.lookup(id)
- else
- []
- end
- else
- root_tree
- end
-
- tree.map do |entry|
- current_path = path ? File.join(path, entry[:name]) : entry[:name]
-
- new(
- id: entry[:oid],
- name: entry[:name],
- type: entry[:type],
- mode: entry[:filemode].to_s(8),
- path: current_path,
- commit_id: sha
- )
- end
- rescue Rugged::ReferenceError
- []
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/git/rugged_impl/use_rugged.rb b/lib/gitlab/git/rugged_impl/use_rugged.rb
deleted file mode 100644
index 57cced97d02..00000000000
--- a/lib/gitlab/git/rugged_impl/use_rugged.rb
+++ /dev/null
@@ -1,50 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Git
- module RuggedImpl
- module UseRugged
- def use_rugged?(_, _)
- false
- end
-
- def execute_rugged_call(method_name, *args)
- Gitlab::GitalyClient::StorageSettings.allow_disk_access do
- start = Gitlab::Metrics::System.monotonic_time
-
- result = send(method_name, *args) # rubocop:disable GitlabSecurity/PublicSend
-
- duration = Gitlab::Metrics::System.monotonic_time - start
-
- if Gitlab::RuggedInstrumentation.active?
- Gitlab::RuggedInstrumentation.increment_query_count
- Gitlab::RuggedInstrumentation.add_query_time(duration)
-
- Gitlab::RuggedInstrumentation.add_call_details(
- feature: method_name,
- args: args,
- duration: duration,
- backtrace: Gitlab::BacktraceCleaner.clean_backtrace(caller))
- end
-
- result
- end
- end
-
- def running_puma_with_multiple_threads?
- return false unless Gitlab::Runtime.puma?
-
- ::Puma.respond_to?(:cli_config) && ::Puma.cli_config.options[:max_threads] > 1
- end
-
- def rugged_feature_keys
- Gitlab::Git::RuggedImpl::Repository::FEATURE_FLAGS
- end
-
- def rugged_enabled_through_feature_flag?
- false
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/git/tree.rb b/lib/gitlab/git/tree.rb
index 6e97e412b91..4747ab55c63 100644
--- a/lib/gitlab/git/tree.rb
+++ b/lib/gitlab/git/tree.rb
@@ -12,9 +12,6 @@ module Gitlab
class << self
# Get list of tree objects
# for repository based on commit sha and path
- # Uses rugged for raw objects
- #
- # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/320
def where(
repository, sha, path = nil, recursive = false, skip_flat_paths = true, rescue_not_found = true,
pagination_params = nil)
@@ -110,5 +107,3 @@ module Gitlab
end
end
end
-
-Gitlab::Git::Tree.singleton_class.prepend Gitlab::Git::RuggedImpl::Tree::ClassMethods
diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb
index 45283d51b1b..72016aa1183 100644
--- a/lib/gitlab/git_access.rb
+++ b/lib/gitlab/git_access.rb
@@ -101,7 +101,7 @@ module Gitlab
end
def guest_can_download?
- Guest.can?(download_ability, container)
+ ::Users::Anonymous.can?(download_ability, container)
end
def deploy_key_can_download_code?
@@ -395,7 +395,7 @@ module Gitlab
user.can?(:read_project, project)
elsif ci?
false
- end || Guest.can?(:read_project, project)
+ end || ::Users::Anonymous.can?(:read_project, project)
end
def http?
diff --git a/lib/gitlab/git_access_project.rb b/lib/gitlab/git_access_project.rb
index 732e0e14257..b007a957348 100644
--- a/lib/gitlab/git_access_project.rb
+++ b/lib/gitlab/git_access_project.rb
@@ -47,7 +47,7 @@ module Gitlab
end
def repository_path_match
- strong_memoize(:repository_path_match) { repository_path.match(Gitlab::PathRegex.full_project_git_path_regex) || {} }
+ strong_memoize(:repository_path_match) { repository_path&.match(Gitlab::PathRegex.full_project_git_path_regex) || {} }
end
def ensure_project_on_push!
diff --git a/lib/gitlab/git_audit_event.rb b/lib/gitlab/git_audit_event.rb
deleted file mode 100644
index b8365bdf41f..00000000000
--- a/lib/gitlab/git_audit_event.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- class GitAuditEvent # rubocop:disable Gitlab/NamespacedClass
- attr_reader :project, :user, :author
-
- def initialize(player, project)
- @project = project
- @author = player.is_a?(::API::Support::GitAccessActor) ? player.deploy_key_or_user : player
- @user = player.is_a?(::API::Support::GitAccessActor) ? player.user : player
- end
-
- def send_audit_event(msg)
- return if user.blank? || project.blank?
-
- audit_context = {
- name: 'repository_git_operation',
- stream_only: true,
- author: author,
- scope: project,
- target: project,
- message: msg
- }
-
- ::Gitlab::Audit::Auditor.audit(audit_context)
- end
- end
-end
diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb
index 5ec58fc4f44..da38c11ebca 100644
--- a/lib/gitlab/gitaly_client.rb
+++ b/lib/gitlab/gitaly_client.rb
@@ -328,6 +328,8 @@ module Gitlab
'client_name' => CLIENT_NAME
}
+ relative_path = fetch_relative_path
+
context_data = Gitlab::ApplicationContext.current
feature_stack = Thread.current[:gitaly_feature_stack]
@@ -339,6 +341,7 @@ module Gitlab
metadata['username'] = context_data['meta.user'] if context_data&.fetch('meta.user', nil)
metadata['user_id'] = context_data['meta.user_id'].to_s if context_data&.fetch('meta.user_id', nil)
metadata['remote_ip'] = context_data['meta.remote_ip'] if context_data&.fetch('meta.remote_ip', nil)
+ metadata['relative-path-bin'] = relative_path if relative_path
metadata.merge!(Feature::Gitaly.server_feature_flags(**feature_flag_actors))
metadata.merge!(route_to_primary)
@@ -348,6 +351,17 @@ module Gitlab
{ metadata: metadata, deadline: deadline_info[:deadline] }
end
+ # The GitLab `internal/allowed/` API sets the :gitlab_git_relative_path
+ # variable. This provides the repository relative path which can be used to
+ # locate snapshot repositories in Gitaly which act as a quarantine repository
+ # until a transaction is committed.
+ def self.fetch_relative_path
+ return unless Gitlab::SafeRequestStore.active?
+ return if Gitlab::SafeRequestStore[:gitlab_git_relative_path].blank?
+
+ Gitlab::SafeRequestStore.fetch(:gitlab_git_relative_path)
+ end
+
# Gitlab::Git::HookEnv will set the :gitlab_git_env variable in case we're
# running in the context of a Gitaly hook call, which may make use of
# quarantined object directories. We thus need to pass along the path of
diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb
index 1ef5b0f96c2..3949e8e6416 100644
--- a/lib/gitlab/gitaly_client/commit_service.rb
+++ b/lib/gitlab/gitaly_client/commit_service.rb
@@ -418,6 +418,15 @@ module Gitlab
response = gitaly_client_call(@repository.storage, :commit_service, :raw_blame, request, timeout: GitalyClient.medium_timeout)
response.reduce([]) { |memo, msg| memo << msg.data }.join
+ rescue GRPC::BadStatus => e
+ detailed_error = GitalyClient.decode_detailed_error(e)
+
+ case detailed_error.try(:error)
+ when :out_of_range, :path_not_found
+ raise ArgumentError, e.details
+ else
+ raise e
+ end
end
def find_commit(revision)
diff --git a/lib/gitlab/gitaly_client/conflict_files_stitcher.rb b/lib/gitlab/gitaly_client/conflict_files_stitcher.rb
index b1278e3bfac..a6912547ce9 100644
--- a/lib/gitlab/gitaly_client/conflict_files_stitcher.rb
+++ b/lib/gitlab/gitaly_client/conflict_files_stitcher.rb
@@ -43,11 +43,15 @@ module Gitlab
def conflict_from_gitaly_file_header(header)
{
- ancestor: { path: header.ancestor_path },
- ours: { path: header.our_path, mode: header.our_mode },
- theirs: { path: header.their_path }
+ ancestor: { path: encode_path(header.ancestor_path) },
+ ours: { path: encode_path(header.our_path), mode: header.our_mode },
+ theirs: { path: encode_path(header.their_path) }
}
end
+
+ def encode_path(path)
+ Gitlab::EncodingHelper.encode_utf8(path)
+ end
end
end
end
diff --git a/lib/gitlab/gitaly_client/repository_service.rb b/lib/gitlab/gitaly_client/repository_service.rb
index d92bf5263f1..457380615f7 100644
--- a/lib/gitlab/gitaly_client/repository_service.rb
+++ b/lib/gitlab/gitaly_client/repository_service.rb
@@ -136,10 +136,13 @@ module Gitlab
response.base.presence
end
- def fork_repository(source_repository)
+ def fork_repository(source_repository, branch = nil)
+ revision = branch.present? ? "refs/heads/#{branch}" : ""
+
request = Gitaly::CreateForkRequest.new(
repository: @gitaly_repo,
- source_repository: source_repository.gitaly_repository
+ source_repository: source_repository.gitaly_repository,
+ revision: revision
)
gitaly_client_call(
diff --git a/lib/gitlab/gitaly_client/storage_settings.rb b/lib/gitlab/gitaly_client/storage_settings.rb
index 4cc0269673f..adf0c811274 100644
--- a/lib/gitlab/gitaly_client/storage_settings.rb
+++ b/lib/gitlab/gitaly_client/storage_settings.rb
@@ -31,19 +31,11 @@ module Gitlab
end
def self.disk_access_denied?
- return false if rugged_enabled?
-
!temporarily_allowed?(ALLOW_KEY)
rescue StandardError
false # Err on the side of caution, don't break gitlab for people
end
- def self.rugged_enabled?
- Gitlab::Git::RuggedImpl::Repository::FEATURE_FLAGS.any? do |flag|
- Feature.enabled?(flag)
- end
- end
-
def initialize(storage)
raise InvalidConfigurationError, "expected a Hash, got a #{storage.class.name}" unless storage.is_a?(Hash)
raise InvalidConfigurationError, INVALID_STORAGE_MESSAGE unless storage.has_key?('path')
diff --git a/lib/gitlab/github_import/attachments_downloader.rb b/lib/gitlab/github_import/attachments_downloader.rb
index 4db55a6aabb..df9c6c8342d 100644
--- a/lib/gitlab/github_import/attachments_downloader.rb
+++ b/lib/gitlab/github_import/attachments_downloader.rb
@@ -29,8 +29,8 @@ module Gitlab
validate_content_length
validate_filepath
- redirection_url = get_assets_download_redirection_url
- file = download_from(redirection_url)
+ download_url = get_assets_download_redirection_url
+ file = download_from(download_url)
validate_symlink
file
@@ -60,16 +60,16 @@ module Gitlab
options[:follow_redirects] = false
response = Gitlab::HTTP.perform_request(Net::HTTP::Get, file_url, options)
- raise_error("expected a redirect response, got #{response.code}") unless response.redirection?
- redirection_url = response.headers[:location]
- filename = URI.parse(redirection_url).path
+ download_url = if response.redirection?
+ response.headers[:location]
+ else
+ file_url
+ end
- unless Gitlab::GithubImport::Markdown::Attachment::MEDIA_TYPES.any? { |type| filename.ends_with?(type) }
- raise UnsupportedAttachmentError
- end
+ file_type_valid?(URI.parse(download_url).path)
- redirection_url
+ download_url
end
def github_assets_url_regex
@@ -89,6 +89,12 @@ module Gitlab
File.join(dir, filename)
end
end
+
+ def file_type_valid?(file_url)
+ return if Gitlab::GithubImport::Markdown::Attachment::MEDIA_TYPES.any? { |type| file_url.ends_with?(type) }
+
+ raise UnsupportedAttachmentError
+ end
end
end
end
diff --git a/lib/gitlab/github_import/client.rb b/lib/gitlab/github_import/client.rb
index 5a0ae680ab8..33e74c90115 100644
--- a/lib/gitlab/github_import/client.rb
+++ b/lib/gitlab/github_import/client.rb
@@ -182,12 +182,12 @@ module Gitlab
request_count_counter.increment
- raise_or_wait_for_rate_limit unless requests_remaining?
+ raise_or_wait_for_rate_limit('Internal threshold reached') unless requests_remaining?
begin
with_retry { yield }
- rescue ::Octokit::TooManyRequests
- raise_or_wait_for_rate_limit
+ rescue ::Octokit::TooManyRequests => e
+ raise_or_wait_for_rate_limit(e.response_body)
# This retry will only happen when running in sequential mode as we'll
# raise an error in parallel mode.
@@ -213,11 +213,11 @@ module Gitlab
octokit.rate_limit.limit
end
- def raise_or_wait_for_rate_limit
+ def raise_or_wait_for_rate_limit(message)
rate_limit_counter.increment
if parallel?
- raise RateLimitError
+ raise RateLimitError, message
else
sleep(rate_limit_resets_in)
end
diff --git a/lib/gitlab/github_import/issuable_finder.rb b/lib/gitlab/github_import/issuable_finder.rb
index b960df581e4..0780ba6119f 100644
--- a/lib/gitlab/github_import/issuable_finder.rb
+++ b/lib/gitlab/github_import/issuable_finder.rb
@@ -11,6 +11,7 @@ module Gitlab
# The base cache key to use for storing/retrieving issuable IDs.
CACHE_KEY = 'github-import/issuable-finder/%{project}/%{type}/%{iid}'
+ CACHE_OBJECT_NOT_FOUND = -1
# project - An instance of `Project`.
# object - The object to look up or set a database ID for.
@@ -23,9 +24,18 @@ module Gitlab
#
# This method will return `nil` if no ID could be found.
def database_id
- val = Gitlab::Cache::Import::Caching.read(cache_key, timeout: timeout)
+ val = Gitlab::Cache::Import::Caching.read_integer(cache_key, timeout: timeout)
- val.to_i if val.present?
+ return val if Feature.disabled?(:import_fallback_to_db_empty_cache, project)
+
+ return if val == CACHE_OBJECT_NOT_FOUND
+ return val if val.present?
+
+ object_id = cache_key_type.safe_constantize&.find_by(project_id: project.id, iid: cache_key_iid)&.id ||
+ CACHE_OBJECT_NOT_FOUND
+
+ cache_database_id(object_id)
+ object_id == CACHE_OBJECT_NOT_FOUND ? nil : object_id
end
# Associates the given database ID with the current object.
diff --git a/lib/gitlab/github_import/job_delay_calculator.rb b/lib/gitlab/github_import/job_delay_calculator.rb
index 52b211c92d6..077a27df16c 100644
--- a/lib/gitlab/github_import/job_delay_calculator.rb
+++ b/lib/gitlab/github_import/job_delay_calculator.rb
@@ -15,7 +15,7 @@ module Gitlab
def calculate_job_delay(job_index)
multiplier = (job_index / parallel_import_batch[:size])
- (multiplier * parallel_import_batch[:delay]) + 1.second
+ (multiplier * parallel_import_batch[:delay]).to_i + 1
end
end
end
diff --git a/lib/gitlab/github_import/label_finder.rb b/lib/gitlab/github_import/label_finder.rb
index 39e669dbba4..d0bbd2bc7cf 100644
--- a/lib/gitlab/github_import/label_finder.rb
+++ b/lib/gitlab/github_import/label_finder.rb
@@ -7,6 +7,7 @@ module Gitlab
# The base cache key to use for storing/retrieving label IDs.
CACHE_KEY = 'github-import/label-finder/%{project}/%{name}'
+ CACHE_OBJECT_NOT_FOUND = -1
# project - An instance of `Project`.
def initialize(project)
@@ -15,7 +16,18 @@ module Gitlab
# Returns the label ID for the given name.
def id_for(name)
- Gitlab::Cache::Import::Caching.read_integer(cache_key_for(name))
+ cache_key = cache_key_for(name)
+ val = Gitlab::Cache::Import::Caching.read_integer(cache_key)
+
+ return val if Feature.disabled?(:import_fallback_to_db_empty_cache, project)
+
+ return if val == CACHE_OBJECT_NOT_FOUND
+ return val if val.present?
+
+ object_id = project.labels.with_title(name).pick(:id) || CACHE_OBJECT_NOT_FOUND
+
+ Gitlab::Cache::Import::Caching.write(cache_key, object_id)
+ object_id == CACHE_OBJECT_NOT_FOUND ? nil : object_id
end
# rubocop: disable CodeReuse/ActiveRecord
@@ -32,7 +44,7 @@ module Gitlab
# rubocop: enable CodeReuse/ActiveRecord
def cache_key_for(name)
- CACHE_KEY % { project: project.id, name: name }
+ format(CACHE_KEY, project: project.id, name: name)
end
end
end
diff --git a/lib/gitlab/github_import/milestone_finder.rb b/lib/gitlab/github_import/milestone_finder.rb
index d9290e36ea1..dcb679fda6d 100644
--- a/lib/gitlab/github_import/milestone_finder.rb
+++ b/lib/gitlab/github_import/milestone_finder.rb
@@ -7,6 +7,7 @@ module Gitlab
# The base cache key to use for storing/retrieving milestone IDs.
CACHE_KEY = 'github-import/milestone-finder/%{project}/%{iid}'
+ CACHE_OBJECT_NOT_FOUND = -1
# project - An instance of `Project`
def initialize(project)
@@ -18,7 +19,20 @@ module Gitlab
def id_for(issuable)
return unless issuable.milestone_number
- Gitlab::Cache::Import::Caching.read_integer(cache_key_for(issuable.milestone_number))
+ milestone_iid = issuable.milestone_number
+ cache_key = cache_key_for(milestone_iid)
+
+ val = Gitlab::Cache::Import::Caching.read_integer(cache_key)
+
+ return val if Feature.disabled?(:import_fallback_to_db_empty_cache, project)
+
+ return if val == CACHE_OBJECT_NOT_FOUND
+ return val if val.present?
+
+ object_id = project.milestones.by_iid(milestone_iid).pick(:id) || CACHE_OBJECT_NOT_FOUND
+
+ Gitlab::Cache::Import::Caching.write(cache_key, object_id)
+ object_id == CACHE_OBJECT_NOT_FOUND ? nil : object_id
end
# rubocop: disable CodeReuse/ActiveRecord
@@ -35,7 +49,7 @@ module Gitlab
# rubocop: enable CodeReuse/ActiveRecord
def cache_key_for(iid)
- CACHE_KEY % { project: project.id, iid: iid }
+ format(CACHE_KEY, project: project.id, iid: iid)
end
end
end
diff --git a/lib/gitlab/github_import/object_counter.rb b/lib/gitlab/github_import/object_counter.rb
index 88e91800cee..5618cfc6044 100644
--- a/lib/gitlab/github_import/object_counter.rb
+++ b/lib/gitlab/github_import/object_counter.rb
@@ -52,7 +52,7 @@ module Gitlab
.sort
.each do |counter|
object_type = counter.split('/').last
- result[operation][object_type] = CACHING.read_integer(counter) || 0
+ result[operation][object_type] = CACHING.read_integer(counter, timeout: IMPORT_CACHING_TIMEOUT) || 0
end
end
end
diff --git a/lib/gitlab/github_import/parallel_scheduling.rb b/lib/gitlab/github_import/parallel_scheduling.rb
index cccd99f48b1..ce93b5203df 100644
--- a/lib/gitlab/github_import/parallel_scheduling.rb
+++ b/lib/gitlab/github_import/parallel_scheduling.rb
@@ -6,7 +6,7 @@ module Gitlab
include JobDelayCalculator
attr_reader :project, :client, :page_counter, :already_imported_cache_key,
- :job_waiter_cache_key, :job_waiter_remaining_cache_key
+ :job_waiter_cache_key, :job_waiter_remaining_cache_key
# The base cache key to use for tracking already imported objects.
ALREADY_IMPORTED_CACHE_KEY =
@@ -26,12 +26,11 @@ module Gitlab
@client = client
@parallel = parallel
@page_counter = PageCounter.new(project, collection_method)
- @already_imported_cache_key = ALREADY_IMPORTED_CACHE_KEY %
- { project: project.id, collection: collection_method }
- @job_waiter_cache_key = JOB_WAITER_CACHE_KEY %
- { project: project.id, collection: collection_method }
- @job_waiter_remaining_cache_key = JOB_WAITER_REMAINING_CACHE_KEY %
- { project: project.id, collection: collection_method }
+ @already_imported_cache_key = format(ALREADY_IMPORTED_CACHE_KEY, project: project.id,
+ collection: collection_method)
+ @job_waiter_cache_key = format(JOB_WAITER_CACHE_KEY, project: project.id, collection: collection_method)
+ @job_waiter_remaining_cache_key = format(JOB_WAITER_REMAINING_CACHE_KEY, project: project.id,
+ collection: collection_method)
end
def parallel?
@@ -57,7 +56,8 @@ module Gitlab
# still scheduling duplicates while. Since all work has already been
# completed those jobs will just cycle through any remaining pages while
# not scheduling anything.
- Gitlab::Cache::Import::Caching.expire(already_imported_cache_key, Gitlab::Cache::Import::Caching::SHORTER_TIMEOUT)
+ Gitlab::Cache::Import::Caching.expire(already_imported_cache_key,
+ Gitlab::Cache::Import::Caching::SHORTER_TIMEOUT)
info(project.id, message: "importer finished")
retval
@@ -97,7 +97,7 @@ module Gitlab
repr = object_representation(object)
job_delay = calculate_job_delay(enqueued_job_counter)
- sidekiq_worker_class.perform_in(job_delay, project.id, repr.to_hash, job_waiter.key)
+ sidekiq_worker_class.perform_in(job_delay, project.id, repr.to_hash.deep_stringify_keys, job_waiter.key.to_s)
enqueued_job_counter += 1
job_waiter.jobs_remaining = Gitlab::Cache::Import::Caching.increment(job_waiter_remaining_cache_key)
diff --git a/lib/gitlab/github_import/representation/to_hash.rb b/lib/gitlab/github_import/representation/to_hash.rb
index 4a0f36ab8f0..54faa51a787 100644
--- a/lib/gitlab/github_import/representation/to_hash.rb
+++ b/lib/gitlab/github_import/representation/to_hash.rb
@@ -16,11 +16,15 @@ module Gitlab
hash
end
+ # This method allow objects to be safely passed directly to Sidekiq without errors.
+ # It returns JSON datatypes: string, integer, float, boolean, null(nil), array and hash.
def convert_value_for_to_hash(value)
if value.is_a?(Array)
value.map { |v| convert_value_for_to_hash(v) }
elsif value.respond_to?(:to_hash)
value.to_hash
+ elsif value.respond_to?(:strftime) || value.is_a?(Symbol)
+ value.to_s
else
value
end
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index e057b4bb6f1..59813e4f5a0 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -50,6 +50,7 @@ module Gitlab
gon.suggested_label_colors = LabelsHelper.suggested_colors
gon.first_day_of_week = current_user&.first_day_of_week || Gitlab::CurrentSettings.first_day_of_week
gon.time_display_relative = true
+ gon.time_display_format = 0
gon.ee = Gitlab.ee?
gon.jh = Gitlab.jh?
gon.dot_com = Gitlab.com?
@@ -67,6 +68,7 @@ module Gitlab
gon.current_user_fullname = current_user.name
gon.current_user_avatar_url = current_user.avatar_url
gon.time_display_relative = current_user.time_display_relative
+ gon.time_display_format = current_user.time_display_format
end
# Initialize gon.features with any flags that should be
@@ -75,7 +77,6 @@ module Gitlab
push_frontend_feature_flag(:security_auto_fix)
push_frontend_feature_flag(:source_editor_toolbar)
push_frontend_feature_flag(:vscode_web_ide, current_user)
- push_frontend_feature_flag(:unbatch_graphql_queries, current_user)
# To be removed with https://gitlab.com/gitlab-org/gitlab/-/issues/399248
push_frontend_feature_flag(:remove_monitor_metrics)
push_frontend_feature_flag(:custom_emoji)
diff --git a/lib/gitlab/graphql/tracers/timer_tracer.rb b/lib/gitlab/graphql/tracers/timer_tracer.rb
index 8e058621110..2cf06086a3c 100644
--- a/lib/gitlab/graphql/tracers/timer_tracer.rb
+++ b/lib/gitlab/graphql/tracers/timer_tracer.rb
@@ -15,11 +15,11 @@ module Gitlab
end
def trace(key, data)
- start_time = Gitlab::Metrics::System.monotonic_time
+ start_time = ::Gitlab::Metrics::System.monotonic_time
yield
ensure
- data[:duration_s] = Gitlab::Metrics::System.monotonic_time - start_time
+ data[:duration_s] = ::Gitlab::Metrics::System.monotonic_time - start_time
end
end
end
diff --git a/lib/gitlab/group_search_results.rb b/lib/gitlab/group_search_results.rb
index 8ca88859b22..6fe7a0030f0 100644
--- a/lib/gitlab/group_search_results.rb
+++ b/lib/gitlab/group_search_results.rb
@@ -13,7 +13,7 @@ module Gitlab
# rubocop:disable CodeReuse/ActiveRecord
def users
groups = group.self_and_hierarchy_intersecting_with_user_groups(current_user)
- groups = groups.allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/417455")
+ groups = groups.allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/427108")
members = GroupMember.where(group: groups).non_invite
users = super
diff --git a/lib/gitlab/identifier.rb b/lib/gitlab/identifier.rb
index 08d44184bb6..720f8748cba 100644
--- a/lib/gitlab/identifier.rb
+++ b/lib/gitlab/identifier.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-# Detect user based on identifier like
+# Detect user or keys based on identifier like
# key-13 or user-36
module Gitlab
module Identifier
@@ -35,6 +35,13 @@ module Gitlab
end
end
+ # Tries to identify a deploy key using a SSH key identifier (e.g. "key-123").
+ def identify_using_deploy_key(identifier)
+ key_id = identifier.gsub("key-", "")
+
+ DeployKey.find_by_id(key_id)
+ end
+
def identify_with_cache(category, key)
if identification_cache[category].key?(key)
identification_cache[category][key]
diff --git a/lib/gitlab/import_export/command_line_util.rb b/lib/gitlab/import_export/command_line_util.rb
index ea91b01afdb..523df1f9d5e 100644
--- a/lib/gitlab/import_export/command_line_util.rb
+++ b/lib/gitlab/import_export/command_line_util.rb
@@ -40,13 +40,12 @@ module Gitlab
cmd = %W[gzip #{filepath}]
cmd << "-#{options}" if options
- _, status = Gitlab::Popen.popen(cmd)
+ output, status = Gitlab::Popen.popen(cmd)
- if status == 0
- status
- else
- raise Gitlab::ImportExport::Error.file_compression_error
- end
+ return status if status == 0
+
+ message = cmd_error_message(output, status)
+ raise Gitlab::ImportExport::Error.file_compression_error(message)
end
def mkdir_p(path)
@@ -104,9 +103,7 @@ module Gitlab
return true if status == 0
- output = output&.strip
- message = "command exited with error code #{status}"
- message += ": #{output}" if output.present?
+ message = cmd_error_message(output, status)
if @shared.respond_to?(:error)
@shared.error(Gitlab::ImportExport::Error.new(message))
@@ -149,6 +146,12 @@ module Gitlab
FileUtils.remove_dir(dir)
raise
end
+
+ def cmd_error_message(output, status)
+ message = "Command exited with error code #{status}"
+ message << ": #{output.strip}" unless output.blank?
+ message
+ end
end
end
end
diff --git a/lib/gitlab/import_export/error.rb b/lib/gitlab/import_export/error.rb
index fa179f584eb..9b8e6374b5a 100644
--- a/lib/gitlab/import_export/error.rb
+++ b/lib/gitlab/import_export/error.rb
@@ -14,8 +14,8 @@ module Gitlab
self.new('Unknown object type')
end
- def self.file_compression_error
- self.new('File compression/decompression failed')
+ def self.file_compression_error(error)
+ self.new(format('File compression or decompression failed. %{error}', error: error))
end
def self.incompatible_import_file_error
diff --git a/lib/gitlab/import_export/project/sample/date_calculator.rb b/lib/gitlab/import_export/project/sample/date_calculator.rb
index 543fd25d883..0cb0eb32a23 100644
--- a/lib/gitlab/import_export/project/sample/date_calculator.rb
+++ b/lib/gitlab/import_export/project/sample/date_calculator.rb
@@ -25,7 +25,7 @@ module Gitlab
end
def calculate_by_closest_date_to_average(date)
- return date unless closest_date_to_average && closest_date_to_average < Time.current
+ return date unless closest_date_to_average && closest_date_to_average.past?
date + (Time.current - closest_date_to_average).seconds
end
diff --git a/lib/gitlab/instrumentation/redis_base.rb b/lib/gitlab/instrumentation/redis_base.rb
index e39bbb36680..88991495a10 100644
--- a/lib/gitlab/instrumentation/redis_base.rb
+++ b/lib/gitlab/instrumentation/redis_base.rb
@@ -90,7 +90,7 @@ module Gitlab
result = ::Gitlab::Instrumentation::RedisClusterValidator.validate(commands)
return true if result.nil?
- if !result[:valid] && !result[:allowed] && (Rails.env.development? || Rails.env.test?)
+ if !result[:valid] && !result[:allowed] && raise_cross_slot_validation_errors?
raise RedisClusterValidator::CrossSlotError, "Redis command #{result[:command_name]} arguments hash to different slots. See https://docs.gitlab.com/ee/development/redis.html#multi-key-commands"
end
@@ -189,6 +189,10 @@ module Gitlab
redirection_type, _, target_node_key = err_msg.split
{ redirection_type: redirection_type, target_node_key: target_node_key }
end
+
+ def raise_cross_slot_validation_errors?
+ Rails.env.development? || Rails.env.test?
+ end
end
end
end
diff --git a/lib/gitlab/instrumentation/redis_interceptor.rb b/lib/gitlab/instrumentation/redis_interceptor.rb
index 20ba1ab82a7..5934204bd0f 100644
--- a/lib/gitlab/instrumentation/redis_interceptor.rb
+++ b/lib/gitlab/instrumentation/redis_interceptor.rb
@@ -31,7 +31,7 @@ module Gitlab
private
def instrument_call(commands, pipelined = false)
- start = Gitlab::Metrics::System.monotonic_time # must come first so that 'start' is always defined
+ start = ::Gitlab::Metrics::System.monotonic_time # must come first so that 'start' is always defined
instrumentation_class.instance_count_request(commands.size)
instrumentation_class.instance_count_pipelined_request(commands.size) if pipelined
@@ -50,7 +50,7 @@ module Gitlab
instrumentation_class.log_exception(ex)
raise ex
ensure
- duration = Gitlab::Metrics::System.monotonic_time - start
+ duration = ::Gitlab::Metrics::System.monotonic_time - start
unless exclude_from_apdex?(commands)
commands.each { instrumentation_class.instance_observe_duration(duration / commands.size) }
diff --git a/lib/gitlab/instrumentation_helper.rb b/lib/gitlab/instrumentation_helper.rb
index 2a3c4db5ffa..49078a7ccd0 100644
--- a/lib/gitlab/instrumentation_helper.rb
+++ b/lib/gitlab/instrumentation_helper.rb
@@ -12,7 +12,6 @@ module Gitlab
def add_instrumentation_data(payload)
instrument_gitaly(payload)
- instrument_rugged(payload)
instrument_redis(payload)
instrument_elasticsearch(payload)
instrument_zoekt(payload)
@@ -40,15 +39,6 @@ module Gitlab
payload[:gitaly_duration_s] = Gitlab::GitalyClient.query_time
end
- def instrument_rugged(payload)
- rugged_calls = Gitlab::RuggedInstrumentation.query_count
-
- return if rugged_calls == 0
-
- payload[:rugged_calls] = rugged_calls
- payload[:rugged_duration_s] = Gitlab::RuggedInstrumentation.query_time
- end
-
def instrument_redis(payload)
payload.merge! ::Gitlab::Instrumentation::Redis.payload
end
diff --git a/lib/gitlab/internal_events.rb b/lib/gitlab/internal_events.rb
index 2790bc8ee24..e2e4ea75dbf 100644
--- a/lib/gitlab/internal_events.rb
+++ b/lib/gitlab/internal_events.rb
@@ -23,8 +23,6 @@ module Gitlab
private
def increase_total_counter(event_name)
- return unless ::ServicePing::ServicePingSettings.enabled?
-
redis_counter_key =
Gitlab::Usage::Metrics::Instrumentations::TotalCountMetric.redis_key(event_name)
Gitlab::Redis::SharedState.with { |redis| redis.incr(redis_counter_key) }
diff --git a/lib/gitlab/issues/rebalancing/state.rb b/lib/gitlab/issues/rebalancing/state.rb
index 12cc5f6e5dd..c60dac6f571 100644
--- a/lib/gitlab/issues/rebalancing/state.rb
+++ b/lib/gitlab/issues/rebalancing/state.rb
@@ -100,7 +100,7 @@ module Gitlab
def refresh_keys_expiration
with_redis do |redis|
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
- redis.pipelined do |pipeline|
+ Gitlab::Redis::CrossSlot::Pipeline.new(redis).pipelined do |pipeline|
pipeline.expire(issue_ids_key, REDIS_EXPIRY_TIME)
pipeline.expire(current_index_key, REDIS_EXPIRY_TIME)
pipeline.expire(current_project_key, REDIS_EXPIRY_TIME)
diff --git a/lib/gitlab/jira/http_client.rb b/lib/gitlab/jira/http_client.rb
index 7abfe8e38e8..2b8b01e2023 100644
--- a/lib/gitlab/jira/http_client.rb
+++ b/lib/gitlab/jira/http_client.rb
@@ -34,6 +34,17 @@ module Gitlab
request_params[:headers][:Cookie] = get_cookies if options[:use_cookies]
request_params[:base_uri] = uri.to_s
request_params.merge!(auth_params)
+ # Setting defaults here so we can also set `timeout` which prevents setting defaults in the HTTP gem's code
+ request_params[:open_timeout] = options[:open_timeout] || default_timeout_for(:open_timeout)
+ request_params[:read_timeout] = options[:read_timeout] || default_timeout_for(:read_timeout)
+ request_params[:write_timeout] = options[:write_timeout] || default_timeout_for(:write_timeout)
+ # Global timeout. Needs to be at least as high as the maximum defined in other timeouts
+ request_params[:timeout] = [
+ Gitlab::HTTP::DEFAULT_READ_TOTAL_TIMEOUT,
+ request_params[:open_timeout],
+ request_params[:read_timeout],
+ request_params[:write_timeout]
+ ].max
result = Gitlab::HTTP.public_send(http_method, path, **request_params) # rubocop:disable GitlabSecurity/PublicSend
@authenticated = result.response.is_a?(Net::HTTPOK)
@@ -52,6 +63,10 @@ module Gitlab
private
+ def default_timeout_for(param)
+ Gitlab::HTTP::DEFAULT_TIMEOUT_OPTIONS[param]
+ end
+
def auth_params
return {} unless @options[:username] && @options[:password]
diff --git a/lib/gitlab/jira/middleware.rb b/lib/gitlab/jira/middleware.rb
deleted file mode 100644
index 8a74729da49..00000000000
--- a/lib/gitlab/jira/middleware.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Jira
- class Middleware
- def self.jira_dvcs_connector?(env)
- env['HTTP_USER_AGENT']&.downcase&.start_with?('jira dvcs connector')
- end
-
- def initialize(app)
- @app = app
- end
-
- def call(env)
- if self.class.jira_dvcs_connector?(env)
- env['HTTP_AUTHORIZATION'] = env['HTTP_AUTHORIZATION']&.sub('token', 'Bearer')
- end
-
- @app.call(env)
- end
- end
- end
-end
diff --git a/lib/gitlab/jira_import/base_importer.rb b/lib/gitlab/jira_import/base_importer.rb
index 2b83f0492cb..04ef1a0ef68 100644
--- a/lib/gitlab/jira_import/base_importer.rb
+++ b/lib/gitlab/jira_import/base_importer.rb
@@ -5,7 +5,7 @@ module Gitlab
class BaseImporter
attr_reader :project, :client, :formatter, :jira_project_key, :running_import
- def initialize(project)
+ def initialize(project, client = nil)
Gitlab::JiraImport.validate_project_settings!(project)
@running_import = project.latest_jira_import
@@ -14,7 +14,7 @@ module Gitlab
raise Projects::ImportService::Error, _('Unable to find Jira project to import data from.') unless @jira_project_key
@project = project
- @client = project.jira_integration.client
+ @client = client || project.jira_integration.client
@formatter = Gitlab::ImportFormatter.new
end
diff --git a/lib/gitlab/jira_import/issues_importer.rb b/lib/gitlab/jira_import/issues_importer.rb
index 458f7c3f470..54ececc4938 100644
--- a/lib/gitlab/jira_import/issues_importer.rb
+++ b/lib/gitlab/jira_import/issues_importer.rb
@@ -10,7 +10,7 @@ module Gitlab
attr_reader :imported_items_cache_key, :start_at, :job_waiter
- def initialize(project)
+ def initialize(project, client = nil)
super
# get cached start_at value, or zero if not cached yet
@start_at = Gitlab::JiraImport.get_issues_next_start_at(project.id)
diff --git a/lib/gitlab/job_waiter.rb b/lib/gitlab/job_waiter.rb
index e53bfb40654..7b491b3e14d 100644
--- a/lib/gitlab/job_waiter.rb
+++ b/lib/gitlab/job_waiter.rb
@@ -19,9 +19,6 @@ module Gitlab
class JobWaiter
KEY_PREFIX = "gitlab:job_waiter"
- STARTED_METRIC = :gitlab_job_waiter_started_total
- TIMEOUTS_METRIC = :gitlab_job_waiter_timeouts_total
-
# This TTL needs to be long enough to allow whichever Sidekiq job calls
# JobWaiter#wait to reach BLPOP.
DEFAULT_TTL = 6.hours.to_i
@@ -48,16 +45,15 @@ module Gitlab
Gitlab::Redis::SharedState.with { |redis| redis.del(key) } if key?(key)
end
- attr_reader :key, :finished, :worker_label
+ attr_reader :key, :finished
attr_accessor :jobs_remaining
# jobs_remaining - the number of jobs left to wait for
# key - The key of this waiter.
- def initialize(jobs_remaining = 0, key = "#{KEY_PREFIX}:#{SecureRandom.uuid}", worker_label: nil)
+ def initialize(jobs_remaining = 0, key = "#{KEY_PREFIX}:#{SecureRandom.uuid}")
@key = key
@jobs_remaining = jobs_remaining
@finished = []
- @worker_label = worker_label
end
# Waits for all the jobs to be completed.
@@ -67,7 +63,6 @@ module Gitlab
# long to process, or is never processed.
def wait(timeout = 10)
deadline = Time.now.utc + timeout
- increment_counter(STARTED_METRIC)
Gitlab::Redis::SharedState.with do |redis|
while jobs_remaining > 0
@@ -81,10 +76,7 @@ module Gitlab
list, jid = redis.blpop(key, timeout: seconds_left)
# timed out
- unless list && jid
- increment_counter(TIMEOUTS_METRIC)
- break
- end
+ break unless list && jid
@finished << jid
@jobs_remaining -= 1
@@ -93,20 +85,5 @@ module Gitlab
finished
end
-
- private
-
- def increment_counter(metric)
- return unless worker_label
-
- metrics[metric].increment(worker: worker_label)
- end
-
- def metrics
- @metrics ||= {
- STARTED_METRIC => Gitlab::Metrics.counter(STARTED_METRIC, 'JobWaiter attempts started'),
- TIMEOUTS_METRIC => Gitlab::Metrics.counter(TIMEOUTS_METRIC, 'JobWaiter attempts timed out')
- }
- end
end
end
diff --git a/lib/gitlab/kubernetes/kubeconfig/template.rb b/lib/gitlab/kubernetes/kubeconfig/template.rb
index d40b9ce117e..844472f9c8e 100644
--- a/lib/gitlab/kubernetes/kubeconfig/template.rb
+++ b/lib/gitlab/kubernetes/kubeconfig/template.rb
@@ -44,7 +44,7 @@ module Gitlab
)
end
kubeconfig_yaml[:clusters].each do |cluster|
- ca_pem = cluster.dig(:cluster, :'certificate-authority-data')&.yield_self do |data|
+ ca_pem = cluster.dig(:cluster, :'certificate-authority-data')&.then do |data|
Base64.strict_decode64(data)
end
diff --git a/lib/gitlab/legacy_http.rb b/lib/gitlab/legacy_http.rb
index f38b2819c15..cf6ab80d37f 100644
--- a/lib/gitlab/legacy_http.rb
+++ b/lib/gitlab/legacy_http.rb
@@ -35,8 +35,8 @@ module Gitlab
read_total_timeout = options.fetch(:timeout, Gitlab::HTTP::DEFAULT_READ_TOTAL_TIMEOUT)
httparty_perform_request(http_method, path, options_with_timeouts) do |fragment|
- start_time ||= Gitlab::Metrics::System.monotonic_time
- elapsed = Gitlab::Metrics::System.monotonic_time - start_time
+ start_time ||= ::Gitlab::Metrics::System.monotonic_time
+ elapsed = ::Gitlab::Metrics::System.monotonic_time - start_time
if elapsed > read_total_timeout
raise Gitlab::HTTP::ReadTotalTimeout, "Request timed out after #{elapsed} seconds"
diff --git a/lib/gitlab/memory/reporter.rb b/lib/gitlab/memory/reporter.rb
index db0fd24983b..8d32745ac34 100644
--- a/lib/gitlab/memory/reporter.rb
+++ b/lib/gitlab/memory/reporter.rb
@@ -26,13 +26,13 @@ module Gitlab
perf_report: report.name
))
- start_monotonic_time = Gitlab::Metrics::System.monotonic_time
- start_thread_cpu_time = Gitlab::Metrics::System.thread_cpu_time
+ start_monotonic_time = ::Gitlab::Metrics::System.monotonic_time
+ start_thread_cpu_time = ::Gitlab::Metrics::System.thread_cpu_time
report_file = store_report(report)
- cpu_s = Gitlab::Metrics::System.thread_cpu_duration(start_thread_cpu_time)
- duration_s = Gitlab::Metrics::System.monotonic_time - start_monotonic_time
+ cpu_s = ::Gitlab::Metrics::System.thread_cpu_duration(start_thread_cpu_time)
+ duration_s = ::Gitlab::Metrics::System.monotonic_time - start_monotonic_time
@logger.info(
log_labels(
diff --git a/lib/gitlab/memory/reports_uploader.rb b/lib/gitlab/memory/reports_uploader.rb
index 76c3e0862e2..17230414a6a 100644
--- a/lib/gitlab/memory/reports_uploader.rb
+++ b/lib/gitlab/memory/reports_uploader.rb
@@ -13,11 +13,11 @@ module Gitlab
def upload(path)
log_upload_requested(path)
- start_monotonic_time = Gitlab::Metrics::System.monotonic_time
+ start_monotonic_time = ::Gitlab::Metrics::System.monotonic_time
File.open(path.to_s) { |file| fog.put_object(gcs_bucket, File.basename(path), file) }
- duration_s = Gitlab::Metrics::System.monotonic_time - start_monotonic_time
+ duration_s = ::Gitlab::Metrics::System.monotonic_time - start_monotonic_time
log_upload_success(path, duration_s)
rescue StandardError, Errno::ENOENT => error
log_exception(error)
diff --git a/lib/gitlab/merge_requests/mergeability/check_result.rb b/lib/gitlab/merge_requests/mergeability/check_result.rb
index e18909d8f17..075a897478b 100644
--- a/lib/gitlab/merge_requests/mergeability/check_result.rb
+++ b/lib/gitlab/merge_requests/mergeability/check_result.rb
@@ -5,6 +5,7 @@ module Gitlab
class CheckResult
SUCCESS_STATUS = :success
FAILED_STATUS = :failed
+ INACTIVE_STATUS = :inactive
attr_reader :status, :payload
@@ -20,6 +21,10 @@ module Gitlab
new(status: FAILED_STATUS, payload: default_payload.merge(**payload))
end
+ def self.inactive(payload: {})
+ new(status: INACTIVE_STATUS, payload: default_payload.merge(**payload))
+ end
+
def self.from_hash(data)
new(
status: data.fetch(:status).to_sym,
diff --git a/lib/gitlab/metrics/exporter/metrics_middleware.rb b/lib/gitlab/metrics/exporter/metrics_middleware.rb
index 258b655229e..b80a8c503e8 100644
--- a/lib/gitlab/metrics/exporter/metrics_middleware.rb
+++ b/lib/gitlab/metrics/exporter/metrics_middleware.rb
@@ -9,10 +9,10 @@ module Gitlab
default_labels = {
pid: pid
}
- @requests_total = Gitlab::Metrics.counter(
+ @requests_total = ::Gitlab::Metrics.counter(
:exporter_http_requests_total, 'Total number of HTTP requests', default_labels
)
- @request_durations = Gitlab::Metrics.histogram(
+ @request_durations = ::Gitlab::Metrics.histogram(
:exporter_http_request_duration_seconds,
'HTTP request duration histogram (seconds)',
default_labels,
@@ -21,9 +21,9 @@ module Gitlab
end
def call(env)
- start = Gitlab::Metrics::System.monotonic_time
+ start = ::Gitlab::Metrics::System.monotonic_time
@app.call(env).tap do |response|
- duration = Gitlab::Metrics::System.monotonic_time - start
+ duration = ::Gitlab::Metrics::System.monotonic_time - start
labels = {
method: env['REQUEST_METHOD'].downcase,
diff --git a/lib/gitlab/middleware/go.rb b/lib/gitlab/middleware/go.rb
index d2336ec4bb2..5a0612be88e 100644
--- a/lib/gitlab/middleware/go.rb
+++ b/lib/gitlab/middleware/go.rb
@@ -141,7 +141,7 @@ module Gitlab
return empty_result unless has_basic_credentials?(request)
login, password = user_name_and_password(request)
- auth_result = Gitlab::Auth.find_for_git_client(login, password, project: project, ip: request.ip)
+ auth_result = Gitlab::Auth.find_for_git_client(login, password, project: project, request: request)
return empty_result unless auth_result.success?
return empty_result unless auth_result.can?(:access_git)
diff --git a/lib/gitlab/middleware/path_traversal_check.rb b/lib/gitlab/middleware/path_traversal_check.rb
index 79465f3cb30..6fef247b708 100644
--- a/lib/gitlab/middleware/path_traversal_check.rb
+++ b/lib/gitlab/middleware/path_traversal_check.rb
@@ -5,6 +5,28 @@ module Gitlab
class PathTraversalCheck
PATH_TRAVERSAL_MESSAGE = 'Potential path traversal attempt detected'
+ EXCLUDED_EXACT_PATHS = %w[/search].freeze
+ EXCLUDED_PATH_PREFIXES = %w[/search/].freeze
+
+ EXCLUDED_API_PATHS = %w[/search].freeze
+ EXCLUDED_PROJECT_API_PATHS = %w[/search].freeze
+ EXCLUDED_GROUP_API_PATHS = %w[/search].freeze
+
+ API_PREFIX = %r{/api/[^/]+}
+ API_SUFFIX = %r{(?:\.[^/]+)?}
+
+ EXCLUDED_API_PATHS_REGEX = [
+ EXCLUDED_API_PATHS.map do |path|
+ %r{\A#{API_PREFIX}#{path}#{API_SUFFIX}\z}
+ end.freeze,
+ EXCLUDED_PROJECT_API_PATHS.map do |path|
+ %r{\A#{API_PREFIX}/projects/[^/]+(?:/-)?#{path}#{API_SUFFIX}\z}
+ end.freeze,
+ EXCLUDED_GROUP_API_PATHS.map do |path|
+ %r{\A#{API_PREFIX}/groups/[^/]+(?:/-)?#{path}#{API_SUFFIX}\z}
+ end.freeze
+ ].flatten.freeze
+
def initialize(app)
@app = app
end
@@ -14,7 +36,8 @@ module Gitlab
log_params = {}
execution_time = measure_execution_time do
- check(env, log_params)
+ request = ::Rack::Request.new(env.dup)
+ check(request, log_params) unless excluded?(request)
end
log_params[:duration_ms] = execution_time.round(5) if execution_time
@@ -37,17 +60,25 @@ module Gitlab
end
end
- def check(env, log_params)
- request = ::Rack::Request.new(env)
- fullpath = request.fullpath
- decoded_fullpath = CGI.unescape(fullpath)
+ def check(request, log_params)
+ decoded_fullpath = CGI.unescape(request.fullpath)
::Gitlab::PathTraversal.check_path_traversal!(decoded_fullpath, skip_decoding: true)
-
rescue ::Gitlab::PathTraversal::PathTraversalAttackError
- log_params[:fullpath] = fullpath
+ log_params[:method] = request.request_method
+ log_params[:fullpath] = request.fullpath
log_params[:message] = PATH_TRAVERSAL_MESSAGE
end
+ def excluded?(request)
+ path = request.path
+
+ return true if path.in?(EXCLUDED_EXACT_PATHS)
+ return true if EXCLUDED_PATH_PREFIXES.any? { |p| path.start_with?(p) }
+ return true if EXCLUDED_API_PATHS_REGEX.any? { |r| path.match?(r) }
+
+ false
+ end
+
def log(payload)
Gitlab::AppLogger.warn(
payload.merge(class_name: self.class.name)
diff --git a/lib/gitlab/omniauth_initializer.rb b/lib/gitlab/omniauth_initializer.rb
index 81ad7a7f9e1..0bcd5b1196a 100644
--- a/lib/gitlab/omniauth_initializer.rb
+++ b/lib/gitlab/omniauth_initializer.rb
@@ -29,6 +29,8 @@ module Gitlab
{
authorize_params: { gl_auth_type: 'login' }
}
+ when ->(provider_name) { AuthHelper.saml_providers.include?(provider_name.to_sym) }
+ { attribute_statements: ::Gitlab::Auth::Saml::Config.default_attribute_statements }
else
{}
end
@@ -61,7 +63,7 @@ module Gitlab
provider_arguments.concat arguments
provider_arguments << defaults unless defaults.empty?
when Hash, GitlabSettings::Options
- hash_arguments = arguments.deep_symbolize_keys.deep_merge(defaults)
+ hash_arguments = merge_hash_defaults_and_args(defaults, arguments)
normalized = normalize_hash_arguments(hash_arguments)
# A Hash from the configuration will be passed as is.
@@ -80,6 +82,13 @@ module Gitlab
provider_arguments
end
+ def merge_hash_defaults_and_args(defaults, arguments)
+ return arguments.to_hash if defaults.empty?
+ return defaults.deep_merge(arguments.deep_symbolize_keys) if Feature.enabled?(:invert_omniauth_args_merging)
+
+ arguments.to_hash.deep_symbolize_keys.deep_merge(defaults)
+ end
+
def normalize_hash_arguments(args)
args.deep_symbolize_keys!
diff --git a/lib/gitlab/optimistic_locking.rb b/lib/gitlab/optimistic_locking.rb
index 3c8ac55f70b..adc417f287c 100644
--- a/lib/gitlab/optimistic_locking.rb
+++ b/lib/gitlab/optimistic_locking.rb
@@ -7,7 +7,7 @@ module Gitlab
module_function
def retry_lock(subject, max_retries = MAX_RETRIES, name:, &block)
- start_time = Gitlab::Metrics::System.monotonic_time
+ start_time = ::Gitlab::Metrics::System.monotonic_time
retry_attempts = 0
# prevent scope override, see https://gitlab.com/gitlab-org/gitlab/-/issues/391186
@@ -39,7 +39,7 @@ module Gitlab
def log_optimistic_lock_retries(name:, retry_attempts:, start_time:)
return unless retry_attempts > 0
- elapsed_time = Gitlab::Metrics::System.monotonic_time - start_time
+ elapsed_time = ::Gitlab::Metrics::System.monotonic_time - start_time
retry_lock_logger.info(
message: "Optimistic Lock released with retries",
diff --git a/lib/gitlab/pages/deployment_update.rb b/lib/gitlab/pages/deployment_update.rb
index 6845f5d88ec..bf6ac3a056d 100644
--- a/lib/gitlab/pages/deployment_update.rb
+++ b/lib/gitlab/pages/deployment_update.rb
@@ -89,14 +89,10 @@ module Gitlab
project.actual_limits.pages_file_entries
end
+ # If a newer pipeline already build a PagesDeployment
def validate_outdated_sha
return if latest?
-
- # use pipeline_id in case the build is retried
- last_deployed_pipeline_id = project.pages_metadatum&.pages_deployment&.ci_build&.pipeline_id
-
- return unless last_deployed_pipeline_id
- return if last_deployed_pipeline_id <= build.pipeline_id
+ return if latest_pipeline_id <= build.pipeline_id
errors.add(:base, 'build SHA is outdated for this ref')
end
@@ -111,6 +107,13 @@ module Gitlab
def sha
build.sha
end
+
+ def latest_pipeline_id
+ project
+ .active_pages_deployments
+ .with_path_prefix(build.pages&.dig(:path_prefix))
+ .latest_pipeline_id
+ end
end
end
end
diff --git a/lib/gitlab/pagination/cursor_based_keyset.rb b/lib/gitlab/pagination/cursor_based_keyset.rb
index 81dcc54ff35..9e8c0c530a9 100644
--- a/lib/gitlab/pagination/cursor_based_keyset.rb
+++ b/lib/gitlab/pagination/cursor_based_keyset.rb
@@ -34,8 +34,10 @@ module Gitlab
order_satisfied?(relation, cursor_based_request_context)
end
- def self.enforced_for_type?(relation)
- ENFORCED_TYPES.include?(relation.klass)
+ def self.enforced_for_type?(request_scope, relation)
+ enforced = ENFORCED_TYPES
+ enforced += [::Ci::Build] if ::Feature.enabled?(:enforce_ci_builds_pagination_limit, request_scope, type: :ops)
+ enforced.include?(relation.klass)
end
def self.order_satisfied?(relation, cursor_based_request_context)
diff --git a/lib/gitlab/patch/sidekiq_cron_poller.rb b/lib/gitlab/patch/sidekiq_cron_poller.rb
index c9eae2f899f..8f1fbf53161 100644
--- a/lib/gitlab/patch/sidekiq_cron_poller.rb
+++ b/lib/gitlab/patch/sidekiq_cron_poller.rb
@@ -7,7 +7,7 @@
require 'sidekiq/version'
require 'sidekiq/cron/version'
-if Gem::Version.new(Sidekiq::VERSION) != Gem::Version.new('6.5.7')
+if Gem::Version.new(Sidekiq::VERSION) != Gem::Version.new('6.5.12')
raise 'New version of sidekiq detected, please remove or update this patch'
end
diff --git a/lib/gitlab/patch/sidekiq_scheduled_enq.rb b/lib/gitlab/patch/sidekiq_scheduled_enq.rb
index de0e8465f97..b5a40c19923 100644
--- a/lib/gitlab/patch/sidekiq_scheduled_enq.rb
+++ b/lib/gitlab/patch/sidekiq_scheduled_enq.rb
@@ -15,10 +15,8 @@ module Gitlab
# this portion swaps out Sidekiq.redis for Gitlab::Redis::Queues
Gitlab::Redis::Queues.with do |conn| # rubocop:disable Cop/RedisQueueUsage
sorted_sets.each do |sorted_set|
- # adds namespace if `super` polls with a non-namespaced Sidekiq.redis
- if Gitlab::Utils.to_boolean(ENV['SIDEKIQ_ENQUEUE_NON_NAMESPACED'])
- sorted_set = "#{Gitlab::Redis::Queues::SIDEKIQ_NAMESPACE}:#{sorted_set}" # rubocop:disable Cop/RedisQueueUsage
- end
+ # adds namespace since `super` polls with a non-namespaced Sidekiq.redis
+ sorted_set = "#{Gitlab::Redis::Queues::SIDEKIQ_NAMESPACE}:#{sorted_set}" # rubocop:disable Cop/RedisQueueUsage
while !@done && (job = zpopbyscore(conn, keys: [sorted_set], argv: [Time.now.to_f.to_s])) # rubocop:disable Gitlab/ModuleWithInstanceVariables, Lint/AssignmentInCondition
Sidekiq::Client.push(Sidekiq.load_json(job)) # rubocop:disable Cop/SidekiqApiUsage
@@ -28,7 +26,6 @@ module Gitlab
end
end
- # calls original enqueue_jobs which may or may not be namespaced depending on SIDEKIQ_ENQUEUE_NON_NAMESPACED
super
end
end
diff --git a/lib/gitlab/project_template.rb b/lib/gitlab/project_template.rb
index c9ed4720e83..5f2084ce011 100644
--- a/lib/gitlab/project_template.rb
+++ b/lib/gitlab/project_template.rb
@@ -60,14 +60,14 @@ module Gitlab
ProjectTemplate.new('dotnetcore', '.NET Core', _('A .NET Core console application template, customizable for any .NET Core project'), 'https://gitlab.com/gitlab-org/project-templates/dotnetcore', 'illustrations/third-party-logos/dotnet.svg'),
ProjectTemplate.new('android', 'Android', _('A ready-to-go template for use with Android apps'), 'https://gitlab.com/gitlab-org/project-templates/android', 'illustrations/logos/android.svg'),
ProjectTemplate.new('gomicro', 'Go Micro', _('Go Micro is a framework for micro service development'), 'https://gitlab.com/gitlab-org/project-templates/go-micro', 'illustrations/logos/gomicro.svg'),
- ProjectTemplate.new('bridgetown', 'Pages/Bridgetown', _('Everything you need to create a GitLab Pages site using Bridgetown'), 'https://gitlab.com/gitlab-org/project-templates/bridgetown'),
+ ProjectTemplate.new('bridgetown', 'Pages/Bridgetown', _('Everything you need to create a GitLab Pages site using Bridgetown'), 'https://gitlab.com/pages/bridgetown'),
ProjectTemplate.new('gatsby', 'Pages/Gatsby', _('Everything you need to create a GitLab Pages site using Gatsby'), 'https://gitlab.com/pages/gatsby', 'illustrations/third-party-logos/gatsby.svg'),
ProjectTemplate.new('hugo', 'Pages/Hugo', _('Everything you need to create a GitLab Pages site using Hugo'), 'https://gitlab.com/pages/hugo', 'illustrations/logos/hugo.svg'),
ProjectTemplate.new('pelican', 'Pages/Pelican', _('Everything you need to create a GitLab Pages site using Pelican'), 'https://gitlab.com/pages/pelican', 'illustrations/third-party-logos/pelican.svg'),
ProjectTemplate.new('jekyll', 'Pages/Jekyll', _('Everything you need to create a GitLab Pages site using Jekyll'), 'https://gitlab.com/pages/jekyll', 'illustrations/logos/jekyll.svg'),
ProjectTemplate.new('plainhtml', 'Pages/Plain HTML', _('Everything you need to create a GitLab Pages site using plain HTML'), 'https://gitlab.com/pages/plain-html'),
ProjectTemplate.new('hexo', 'Pages/Hexo', _('Everything you need to create a GitLab Pages site using Hexo'), 'https://gitlab.com/pages/hexo', 'illustrations/logos/hexo.svg'),
- ProjectTemplate.new('middleman', 'Pages/Middleman', _('Everything you need to create a GitLab Pages site using Middleman'), 'https://gitlab.com/gitlab-org/project-templates/middleman', 'illustrations/logos/middleman.svg'),
+ ProjectTemplate.new('middleman', 'Pages/Middleman', _('Everything you need to create a GitLab Pages site using Middleman'), 'https://gitlab.com/pages/middleman', 'illustrations/logos/middleman.svg'),
ProjectTemplate.new('gitpod_spring_petclinic', 'Gitpod/Spring Petclinic', _('A Gitpod configured Webapplication in Spring and Java'), 'https://gitlab.com/gitlab-org/project-templates/gitpod-spring-petclinic', 'illustrations/logos/gitpod.svg'),
ProjectTemplate.new('nfhugo', 'Netlify/Hugo', _('A Hugo site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features'), 'https://gitlab.com/pages/nfhugo', 'illustrations/logos/netlify.svg'),
ProjectTemplate.new('nfjekyll', 'Netlify/Jekyll', _('A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features'), 'https://gitlab.com/pages/nfjekyll', 'illustrations/logos/netlify.svg'),
@@ -81,7 +81,8 @@ module Gitlab
ProjectTemplate.new('cluster_management', 'GitLab Cluster Management', _('An example project for managing Kubernetes clusters integrated with GitLab'), 'https://gitlab.com/gitlab-org/project-templates/cluster-management'),
ProjectTemplate.new('kotlin_native_linux', 'Kotlin Native Linux', _('A basic template for developing Linux programs using Kotlin Native'), 'https://gitlab.com/gitlab-org/project-templates/kotlin-native-linux'),
ProjectTemplate.new('typo3_distribution', 'TYPO3 Distribution', _('A template for starting a new TYPO3 project'), 'https://gitlab.com/gitlab-org/project-templates/typo3-distribution', 'illustrations/logos/typo3.svg'),
- ProjectTemplate.new('laravel', 'Laravel Framework', _('A basic folder structure of a Laravel application, to help you get started.'), 'https://gitlab.com/gitlab-org/project-templates/laravel', 'illustrations/logos/laravel.svg')
+ ProjectTemplate.new('laravel', 'Laravel Framework', _('A basic folder structure of a Laravel application, to help you get started.'), 'https://gitlab.com/gitlab-org/project-templates/laravel', 'illustrations/logos/laravel.svg'),
+ ProjectTemplate.new('astro_tailwind', 'Astro Tailwind', _('A basic folder structure of Astro Starter Kit, to help you get started.'), 'https://gitlab.com/gitlab-org/project-templates/astro-tailwind')
]
end
# rubocop:enable Metrics/AbcSize
diff --git a/lib/gitlab/push_options.rb b/lib/gitlab/push_options.rb
index 4471d21b9ac..e817f2130f4 100644
--- a/lib/gitlab/push_options.rb
+++ b/lib/gitlab/push_options.rb
@@ -14,6 +14,7 @@ module Gitlab
:milestone,
:remove_source_branch,
:target,
+ :target_project,
:title,
:unassign,
:unlabel
diff --git a/lib/gitlab/quick_actions/merge_request_actions.rb b/lib/gitlab/quick_actions/merge_request_actions.rb
index 9798b0eca2c..72bec159226 100644
--- a/lib/gitlab/quick_actions/merge_request_actions.rb
+++ b/lib/gitlab/quick_actions/merge_request_actions.rb
@@ -172,6 +172,25 @@ module Gitlab
end
end
+ desc { _('Request changes') }
+ explanation { _('Request changes to the current merge request.') }
+ types MergeRequest
+ condition do
+ Feature.enabled?(:mr_request_changes, current_user) &&
+ quick_action_target.persisted? &&
+ quick_action_target.find_reviewer(current_user)
+ end
+ command :request_changes do
+ result = ::MergeRequests::UpdateReviewerStateService.new(project: quick_action_target.project, current_user: current_user)
+ .execute(quick_action_target, "requested_changes")
+
+ @execution_message[:request_changes] = if result[:status] == :success
+ _('Changes requested to the current merge request.')
+ else
+ result[:message]
+ end
+ end
+
desc { _('Approve a merge request') }
explanation { _('Approve the current merge request.') }
types MergeRequest
@@ -197,6 +216,10 @@ module Gitlab
next unless success
+ ::MergeRequests::UpdateReviewerStateService
+ .new(project: quick_action_target.project, current_user: current_user)
+ .execute(quick_action_target, "unreviewed")
+
@execution_message[:unapprove] = _('Unapproved the current merge request.')
end
diff --git a/lib/gitlab/redis.rb b/lib/gitlab/redis.rb
index 89ec996488f..9f7599d2500 100644
--- a/lib/gitlab/redis.rb
+++ b/lib/gitlab/redis.rb
@@ -14,7 +14,6 @@ module Gitlab
Gitlab::Redis::FeatureFlag,
Gitlab::Redis::Queues,
Gitlab::Redis::QueuesMetadata,
- Gitlab::Redis::Pubsub,
Gitlab::Redis::RateLimiting,
Gitlab::Redis::RepositoryCache,
Gitlab::Redis::Sessions,
diff --git a/lib/gitlab/redis/cluster_util.rb b/lib/gitlab/redis/cluster_util.rb
index 5f1f39b5237..9e307940de3 100644
--- a/lib/gitlab/redis/cluster_util.rb
+++ b/lib/gitlab/redis/cluster_util.rb
@@ -26,6 +26,15 @@ module Gitlab
end
expired_count
end
+
+ # Redis cluster alternative to mget
+ def batch_get(keys, redis)
+ keys.each_slice(1000).flat_map do |subset|
+ Gitlab::Redis::CrossSlot::Pipeline.new(redis).pipelined do |pipeline|
+ subset.map { |key| pipeline.get(key) }
+ end
+ end
+ end
end
end
end
diff --git a/lib/gitlab/redis/multi_store.rb b/lib/gitlab/redis/multi_store.rb
index bbe5a8add4b..6acbf83df24 100644
--- a/lib/gitlab/redis/multi_store.rb
+++ b/lib/gitlab/redis/multi_store.rb
@@ -63,8 +63,12 @@ module Gitlab
hlen
hmget
hscan_each
+ llen
+ lrange
mapped_hmget
mget
+ pfcount
+ pttl
scan
scan_each
scard
@@ -72,20 +76,32 @@ module Gitlab
smembers
sscan
sscan_each
+ strlen
ttl
+ type
+ zcard
+ zcount
+ zrange
+ zrangebyscore
+ zrevrange
zscan_each
+ zscore
].freeze
WRITE_COMMANDS = %i[
+ decr
del
eval
expire
flushdb
hdel
+ hincrby
hset
incr
incrby
mapped_hmset
+ pfadd
+ pfmerge
publish
rpush
sadd
@@ -93,8 +109,15 @@ module Gitlab
set
setex
setnx
+ spop
srem
+ srem?
unlink
+ zadd
+ zpopmin
+ zrem
+ zremrangebyrank
+ zremrangebyscore
memory
].freeze
@@ -254,11 +277,27 @@ module Gitlab
#
# Let's define it explicitly instead of propagating it to method_missing
def close
- if use_primary_and_secondary_stores?
- [primary_store, secondary_store].map(&:close).first
+ if same_redis_store?
+ # if same_redis_store?, `use_primary_store_as_default?` returns false
+ # but we should avoid a feature-flag check in `.close` to avoid checking out
+ # an ActiveRecord connection during clean up.
+ secondary_store.close
else
- default_store.close
+ [primary_store, secondary_store].map(&:close).first
+ end
+ end
+
+ # blpop blocks until an element to be popped exist in the list or after a timeout.
+ def blpop(*args)
+ result = default_store.blpop(*args)
+ if !!result && use_primary_and_secondary_stores?
+ # special case to accommodate Gitlab::JobWaiter as blpop is only used in JobWaiter
+ # 1s should be sufficient wait time to account for delays between 1st and 2nd lpush
+ # https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/2520#note_1630893702
+ non_default_store.blpop(args.first, timeout: 1)
end
+
+ result
end
private
@@ -380,7 +419,7 @@ module Gitlab
end
def redis_store?(store)
- store.is_a?(::Redis) || store.is_a?(::Redis::Namespace)
+ store.is_a?(::Redis)
end
def validate_stores!
diff --git a/lib/gitlab/redis/pubsub.rb b/lib/gitlab/redis/pubsub.rb
deleted file mode 100644
index b5022f467a2..00000000000
--- a/lib/gitlab/redis/pubsub.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Redis
- class Pubsub < ::Gitlab::Redis::Wrapper
- class << self
- def config_fallback
- SharedState
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/redis/shared_state.rb b/lib/gitlab/redis/shared_state.rb
index fb3a143121b..d12d3e8c6aa 100644
--- a/lib/gitlab/redis/shared_state.rb
+++ b/lib/gitlab/redis/shared_state.rb
@@ -3,6 +3,12 @@
module Gitlab
module Redis
class SharedState < ::Gitlab::Redis::Wrapper
+ def self.redis
+ primary_store = ::Redis.new(ClusterSharedState.params)
+ secondary_store = ::Redis.new(params)
+
+ MultiStore.new(primary_store, secondary_store, store_name)
+ end
end
end
end
diff --git a/lib/gitlab/redis/wrapper.rb b/lib/gitlab/redis/wrapper.rb
index 2bcf4769b5a..d5470bc0016 100644
--- a/lib/gitlab/redis/wrapper.rb
+++ b/lib/gitlab/redis/wrapper.rb
@@ -19,7 +19,7 @@ module Gitlab
InvalidPathError = Class.new(StandardError)
class << self
- delegate :params, :url, :store, to: :new
+ delegate :params, :url, :store, :encrypted_secrets, to: :new
def with
pool.with { |redis| yield redis }
@@ -110,6 +110,14 @@ module Gitlab
raw_config_hash[:sentinels]
end
+ def secret_file
+ if raw_config_hash[:secret_file].blank?
+ File.join(Settings.encrypted_settings['path'], 'redis.yaml.enc')
+ else
+ Settings.absolute(raw_config_hash[:secret_file])
+ end
+ end
+
def sentinels?
sentinels && !sentinels.empty?
end
@@ -118,22 +126,44 @@ module Gitlab
::Redis::Store::Factory.create(redis_store_options.merge(extras))
end
+ def encrypted_secrets
+ # In rake tasks, we have to populate the encrypted_secrets even if the
+ # file does not exist, as it is the job of one of those tasks to create
+ # the file. In other cases, like when being loaded as part of spinning
+ # up test environment via `scripts/setup-test-env`, we should gate on
+ # the presence of the specified secret file so that
+ # `Settings.encrypted`, which might not be loadable does not gets
+ # called.
+ Settings.encrypted(secret_file) if File.exist?(secret_file) || ::Gitlab::Runtime.rake?
+ end
+
private
def redis_store_options
config = raw_config_hash
config[:instrumentation_class] ||= self.class.instrumentation_class
- result = if config[:cluster].present?
- config[:db] = 0 # Redis Cluster only supports db 0
- config
+ decrypted_config = parse_encrypted_config(config)
+
+ result = if decrypted_config[:cluster].present?
+ decrypted_config[:db] = 0 # Redis Cluster only supports db 0
+ decrypted_config
else
- parse_redis_url(config)
+ parse_redis_url(decrypted_config)
end
parse_client_tls_options(result)
end
+ def parse_encrypted_config(encrypted_config)
+ encrypted_config.delete(:secret_file)
+
+ decrypted_secrets = encrypted_secrets&.config
+ encrypted_config.merge!(decrypted_secrets) if decrypted_secrets
+
+ encrypted_config
+ end
+
def parse_redis_url(config)
redis_url = config.delete(:url)
redis_uri = URI.parse(redis_url)
diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb
index 2fd9dc9fa09..6ac37986d5c 100644
--- a/lib/gitlab/regex.rb
+++ b/lib/gitlab/regex.rb
@@ -5,6 +5,7 @@ module Gitlab
extend self
extend MergeRequests
extend Packages
+ extend Packages::Protection::Rules
def project_name_regex
# The character range \p{Alnum} overlaps with \u{00A9}-\u{1f9ff}
diff --git a/lib/gitlab/regex/packages.rb b/lib/gitlab/regex/packages.rb
index 6b178933a25..a0038d39318 100644
--- a/lib/gitlab/regex/packages.rb
+++ b/lib/gitlab/regex/packages.rb
@@ -3,6 +3,8 @@
module Gitlab
module Regex
module Packages
+ include ::Gitlab::Utils::StrongMemoize
+
CONAN_RECIPE_FILES = %w[conanfile.py conanmanifest.txt conan_sources.tgz conan_export.tgz].freeze
CONAN_PACKAGE_FILES = %w[conaninfo.txt conanmanifest.txt conan_package.tgz].freeze
@@ -74,8 +76,10 @@ module Gitlab
maven_app_name_regex
end
- def npm_package_name_regex
- @npm_package_name_regex ||= %r{\A(?:@(#{Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX})/)?[-+\.\_a-zA-Z0-9]+\z}o
+ def npm_package_name_regex(other_accepted_chars = nil)
+ strong_memoize_with(:npm_package_name_regex, other_accepted_chars) do
+ %r{\A(?:@(#{Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX})/)?[-+\.\_a-zA-Z0-9#{other_accepted_chars}]+\z}
+ end
end
def npm_package_name_regex_message
diff --git a/lib/gitlab/regex/packages/protection/rules.rb b/lib/gitlab/regex/packages/protection/rules.rb
new file mode 100644
index 00000000000..383f26fe92d
--- /dev/null
+++ b/lib/gitlab/regex/packages/protection/rules.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Regex
+ module Packages
+ module Protection
+ module Rules
+ def protection_rules_npm_package_name_pattern_regex
+ @protection_rules_npm_package_name_pattern_regex ||= npm_package_name_regex('*')
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/request_forgery_protection.rb b/lib/gitlab/request_forgery_protection.rb
index 3a389d3363f..d5e80053772 100644
--- a/lib/gitlab/request_forgery_protection.rb
+++ b/lib/gitlab/request_forgery_protection.rb
@@ -6,7 +6,8 @@
module Gitlab
module RequestForgeryProtection
- class Controller < BaseActionController
+ # rubocop:disable Rails/ApplicationController
+ class Controller < ActionController::Base
protect_from_forgery with: :exception, prepend: true
def initialize
@@ -39,5 +40,6 @@ module Gitlab
rescue ActionController::InvalidAuthenticityToken
false
end
+ # rubocop:enable Rails/ApplicationController
end
end
diff --git a/lib/gitlab/rugged_instrumentation.rb b/lib/gitlab/rugged_instrumentation.rb
deleted file mode 100644
index 36a3a491de6..00000000000
--- a/lib/gitlab/rugged_instrumentation.rb
+++ /dev/null
@@ -1,45 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module RuggedInstrumentation
- def self.query_time
- query_time = SafeRequestStore[:rugged_query_time] || 0
- query_time.round(Gitlab::InstrumentationHelper::DURATION_PRECISION)
- end
-
- def self.add_query_time(duration)
- SafeRequestStore[:rugged_query_time] ||= 0
- SafeRequestStore[:rugged_query_time] += duration
- end
-
- def self.query_time_ms
- (self.query_time * 1000).round(2)
- end
-
- def self.query_count
- SafeRequestStore[:rugged_call_count] ||= 0
- end
-
- def self.increment_query_count
- SafeRequestStore[:rugged_call_count] ||= 0
- SafeRequestStore[:rugged_call_count] += 1
- end
-
- def self.active?
- SafeRequestStore.active?
- end
-
- def self.add_call_details(details)
- return unless Gitlab::PerformanceBar.enabled_for_request?
-
- Gitlab::SafeRequestStore[:rugged_call_details] ||= []
- Gitlab::SafeRequestStore[:rugged_call_details] << details
- end
-
- def self.list_call_details
- return [] unless Gitlab::PerformanceBar.enabled_for_request?
-
- Gitlab::SafeRequestStore[:rugged_call_details] || []
- end
- end
-end
diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb
index d06f414bd9a..fada3b84401 100644
--- a/lib/gitlab/search_results.rb
+++ b/lib/gitlab/search_results.rb
@@ -191,9 +191,7 @@ module Gitlab
unless default_project_filter
project_ids = project_ids_relation
- if Feature.enabled?(:search_issues_hide_archived_projects, current_user) && !filters[:include_archived]
- project_ids = project_ids.non_archived
- end
+ project_ids = project_ids.non_archived unless filters[:include_archived]
issues = issues.in_projects(project_ids)
.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/420046')
@@ -218,9 +216,7 @@ module Gitlab
unless default_project_filter
project_ids = project_ids_relation
- if Feature.enabled?(:search_merge_requests_hide_archived_projects, current_user) && !filters[:include_archived]
- project_ids = project_ids.non_archived
- end
+ project_ids = project_ids.non_archived unless filters[:include_archived]
merge_requests = merge_requests.of_projects(project_ids)
end
diff --git a/lib/gitlab/seeders/ci/catalog/resource_seeder.rb b/lib/gitlab/seeders/ci/catalog/resource_seeder.rb
new file mode 100644
index 00000000000..2971dabe044
--- /dev/null
+++ b/lib/gitlab/seeders/ci/catalog/resource_seeder.rb
@@ -0,0 +1,114 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Seeders
+ module Ci
+ module Catalog
+ class ResourceSeeder
+ # This is currently disabled until it gets fixed: https://gitlab.com/gitlab-org/gitlab/-/issues/429649
+ # Initializes the class
+ #
+ # @param [String] Path of the group to find
+ # @param [Integer] Number of resources to create
+ def initialize(group_path:, seed_count:)
+ @group = Group.find_by_full_path(group_path)
+ @seed_count = seed_count
+ @current_user = @group&.first_owner
+ end
+
+ def seed
+ if @group.nil?
+ warn 'ERROR: Group was not found.'
+ return
+ end
+
+ @seed_count.times do |i|
+ create_ci_catalog_resource(i)
+ end
+ end
+
+ private
+
+ def create_project(name, index)
+ project = ::Projects::CreateService.new(
+ @current_user,
+ description: "This is Catalog resource ##{index}",
+ name: name,
+ namespace_id: @group.id,
+ path: name,
+ visibility_level: @group.visibility_level
+ ).execute
+
+ if project.saved?
+ project
+ else
+ warn project.errors.full_messages.to_sentence
+ nil
+ end
+ end
+
+ def create_template_yml(project)
+ template_content = <<~YAML
+ spec:
+ inputs:
+ stage:
+ default: test
+ ---
+ component-job:
+ script: echo job 1
+ stage: $[[ inputs.stage ]]
+ YAML
+
+ project.repository.create_file(
+ @current_user,
+ 'template.yml',
+ template_content,
+ message: 'Add template.yml',
+ branch_name: project.default_branch_or_main
+ )
+ end
+
+ def create_readme(project, index)
+ project.repository.create_file(
+ @current_user,
+ '/README.md',
+ "## Component stuff #{index}",
+ message: 'Add README.md',
+ branch_name: project.default_branch_or_main
+ )
+ end
+
+ def create_ci_catalog(project)
+ result = ::Ci::Catalog::Resources::CreateService.new(project, @current_user).execute
+ if result.success?
+ result.payload
+ else
+ warn "Project '#{project.name}' could not be converted to a Catalog resource"
+ nil
+ end
+ end
+
+ def create_ci_catalog_resource(index)
+ name = "ci_seed_resource_#{index}"
+
+ if Project.find_by_name(name).present?
+ warn "Project '#{name}' already exists!"
+ return
+ end
+
+ project = create_project(name, index)
+
+ return unless project
+
+ create_readme(project, index)
+ create_template_yml(project)
+
+ return unless create_ci_catalog(project)
+
+ warn "Project '#{name}' was saved successfully!"
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb b/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb
index a1363e7b6b2..10a69acc037 100644
--- a/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb
+++ b/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb
@@ -21,6 +21,7 @@ module Gitlab
include Gitlab::Utils::StrongMemoize
DEFAULT_DUPLICATE_KEY_TTL = 6.hours
+ SHORT_DUPLICATE_KEY_TTL = 10.minutes
DEFAULT_STRATEGY = :until_executing
STRATEGY_NONE = :none
@@ -134,7 +135,7 @@ module Gitlab
jid != existing_jid
end
- def set_deduplicated_flag!(expiry = duplicate_key_ttl)
+ def set_deduplicated_flag!
return unless reschedulable?
with_redis { |redis| redis.eval(DEDUPLICATED_SCRIPT, keys: [cookie_key]) }
@@ -173,7 +174,7 @@ module Gitlab
end
def duplicate_key_ttl
- options[:ttl] || DEFAULT_DUPLICATE_KEY_TTL
+ options[:ttl] || default_duplicate_key_ttl
end
private
@@ -182,6 +183,12 @@ module Gitlab
attr_reader :queue_name, :job
attr_writer :existing_jid
+ def default_duplicate_key_ttl
+ return SHORT_DUPLICATE_KEY_TTL if Feature.enabled?(:reduce_duplicate_job_key_ttl)
+
+ DEFAULT_DUPLICATE_KEY_TTL
+ end
+
def worker_klass
@worker_klass ||= worker_class_name.to_s.safe_constantize
end
diff --git a/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies/deduplicates_when_scheduling.rb b/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies/deduplicates_when_scheduling.rb
index b065190f656..e7ce837de29 100644
--- a/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies/deduplicates_when_scheduling.rb
+++ b/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies/deduplicates_when_scheduling.rb
@@ -20,7 +20,7 @@ module Gitlab
if duplicate_job.idempotent?
duplicate_job.update_latest_wal_location!
- duplicate_job.set_deduplicated_flag!(expiry)
+ duplicate_job.set_deduplicated_flag!
Gitlab::SidekiqLogging::DeduplicationLogger.instance.deduplicated_log(
job, strategy_name, duplicate_job.options)
diff --git a/lib/gitlab/sidekiq_middleware/server_metrics.rb b/lib/gitlab/sidekiq_middleware/server_metrics.rb
index a8b3683e09f..37a9ed37891 100644
--- a/lib/gitlab/sidekiq_middleware/server_metrics.rb
+++ b/lib/gitlab/sidekiq_middleware/server_metrics.rb
@@ -25,11 +25,6 @@ module Gitlab
def metrics
metrics = {
- sidekiq_jobs_cpu_seconds: ::Gitlab::Metrics.histogram(:sidekiq_jobs_cpu_seconds, 'Seconds this Sidekiq job spent on the CPU', {}, SIDEKIQ_LATENCY_BUCKETS),
- sidekiq_jobs_db_seconds: ::Gitlab::Metrics.histogram(:sidekiq_jobs_db_seconds, 'Seconds of database time to run Sidekiq job', {}, SIDEKIQ_LATENCY_BUCKETS),
- sidekiq_jobs_gitaly_seconds: ::Gitlab::Metrics.histogram(:sidekiq_jobs_gitaly_seconds, 'Seconds of Gitaly time to run Sidekiq job', {}, SIDEKIQ_LATENCY_BUCKETS),
- sidekiq_redis_requests_duration_seconds: ::Gitlab::Metrics.histogram(:sidekiq_redis_requests_duration_seconds, 'Duration in seconds that a Sidekiq job spent requests a Redis server', {}, Gitlab::Instrumentation::Redis::QUERY_TIME_BUCKETS),
- sidekiq_elasticsearch_requests_duration_seconds: ::Gitlab::Metrics.histogram(:sidekiq_elasticsearch_requests_duration_seconds, 'Duration in seconds that a Sidekiq job spent in requests to an Elasticsearch server', {}, SIDEKIQ_LATENCY_BUCKETS),
sidekiq_jobs_retried_total: ::Gitlab::Metrics.counter(:sidekiq_jobs_retried_total, 'Sidekiq jobs retried'),
sidekiq_jobs_interrupted_total: ::Gitlab::Metrics.counter(:sidekiq_jobs_interrupted_total, 'Sidekiq jobs interrupted'),
sidekiq_redis_requests_total: ::Gitlab::Metrics.counter(:sidekiq_redis_requests_total, 'Redis requests during a Sidekiq job execution'),
@@ -43,9 +38,24 @@ module Gitlab
metrics[:sidekiq_jobs_completion_seconds] = ::Gitlab::Metrics.histogram(:sidekiq_jobs_completion_seconds, 'Seconds to complete Sidekiq job', {}, SIDEKIQ_JOB_DURATION_BUCKETS)
metrics[:sidekiq_jobs_queue_duration_seconds] = ::Gitlab::Metrics.histogram(:sidekiq_jobs_queue_duration_seconds, 'Duration in seconds that a Sidekiq job was queued before being executed', {}, SIDEKIQ_QUEUE_DURATION_BUCKETS)
metrics[:sidekiq_jobs_failed_total] = ::Gitlab::Metrics.counter(:sidekiq_jobs_failed_total, 'Sidekiq jobs failed')
+
+ # resource usage
+ metrics[:sidekiq_jobs_cpu_seconds] = ::Gitlab::Metrics.histogram(:sidekiq_jobs_cpu_seconds, 'Seconds this Sidekiq job spent on the CPU', {}, SIDEKIQ_LATENCY_BUCKETS)
+ metrics[:sidekiq_jobs_db_seconds] = ::Gitlab::Metrics.histogram(:sidekiq_jobs_db_seconds, 'Seconds of database time to run Sidekiq job', {}, SIDEKIQ_LATENCY_BUCKETS)
+ metrics[:sidekiq_jobs_gitaly_seconds] = ::Gitlab::Metrics.histogram(:sidekiq_jobs_gitaly_seconds, 'Seconds of Gitaly time to run Sidekiq job', {}, SIDEKIQ_LATENCY_BUCKETS)
+ metrics[:sidekiq_redis_requests_duration_seconds] = ::Gitlab::Metrics.histogram(:sidekiq_redis_requests_duration_seconds, 'Duration in seconds that a Sidekiq job spent in requests to a Redis server', {}, Gitlab::Instrumentation::Redis::QUERY_TIME_BUCKETS)
+ metrics[:sidekiq_elasticsearch_requests_duration_seconds] = ::Gitlab::Metrics.histogram(:sidekiq_elasticsearch_requests_duration_seconds, 'Duration in seconds that a Sidekiq job spent in requests to an Elasticsearch server', {}, SIDEKIQ_LATENCY_BUCKETS)
else
- # The sum metric is still used in GitLab.com for dashboards
+ # These metrics are used in GitLab.com dashboards
metrics[:sidekiq_jobs_completion_seconds_sum] = ::Gitlab::Metrics.counter(:sidekiq_jobs_completion_seconds_sum, 'Total of seconds to complete Sidekiq job')
+ metrics[:sidekiq_jobs_completion_count] = ::Gitlab::Metrics.counter(:sidekiq_jobs_completion_count, 'Number of Sidekiq jobs completed')
+
+ # resource usage sums
+ metrics[:sidekiq_jobs_cpu_seconds_sum] = ::Gitlab::Metrics.counter(:sidekiq_jobs_cpu_seconds_sum, 'Total seconds this Sidekiq job spent on the CPU')
+ metrics[:sidekiq_jobs_db_seconds_sum] = ::Gitlab::Metrics.counter(:sidekiq_jobs_db_seconds_sum, 'Total seconds of database time to run Sidekiq job')
+ metrics[:sidekiq_jobs_gitaly_seconds_sum] = ::Gitlab::Metrics.counter(:sidekiq_jobs_gitaly_seconds_sum, 'Total seconds Gitaly time to run Sidekiq job')
+ metrics[:sidekiq_redis_requests_duration_seconds_sum] = ::Gitlab::Metrics.counter(:sidekiq_redis_requests_duration_seconds_sum, 'Total duration in seconds that a Sidekiq job spent in requests to a Redis server')
+ metrics[:sidekiq_elasticsearch_requests_duration_seconds_sum] = ::Gitlab::Metrics.counter(:sidekiq_elasticsearch_requests_duration_seconds_sum, 'Total duration in seconds that a Sidekiq job spent in requests to an Elasticsearch server')
end
metrics
@@ -89,8 +99,9 @@ module Gitlab
# in metrics and can use them in the `ThreadsSampler` for setting a label
Thread.current.name ||= Gitlab::Metrics::Samplers::ThreadsSampler::SIDEKIQ_WORKER_THREAD_NAME
- labels = create_labels(worker.class, queue, job)
- instrument(job, labels) do
+ @job = job
+ @labels = create_labels(worker.class, queue, job)
+ instrument do
yield
end
end
@@ -99,8 +110,8 @@ module Gitlab
attr_reader :metrics
- def instrument(job, labels)
- queue_duration = ::Gitlab::InstrumentationHelper.queue_duration_for_job(job)
+ def instrument
+ @queue_duration = ::Gitlab::InstrumentationHelper.queue_duration_for_job(job)
@metrics[:sidekiq_jobs_queue_duration_seconds]&.observe(labels, queue_duration) if queue_duration
@@ -114,43 +125,33 @@ module Gitlab
@metrics[:sidekiq_jobs_interrupted_total].increment(labels, 1)
end
- job_succeeded = false
+ @job_succeeded = false
monotonic_time_start = Gitlab::Metrics::System.monotonic_time
job_thread_cputime_start = get_thread_cputime
begin
transaction = Gitlab::Metrics::BackgroundTransaction.new
transaction.run { yield }
- job_succeeded = true
+ @job_succeeded = true
ensure
monotonic_time_end = Gitlab::Metrics::System.monotonic_time
job_thread_cputime_end = get_thread_cputime
- monotonic_time = monotonic_time_end - monotonic_time_start
- job_thread_cputime = job_thread_cputime_end - job_thread_cputime_start
+ @monotonic_time = monotonic_time_end - monotonic_time_start
+ @job_thread_cputime = job_thread_cputime_end - job_thread_cputime_start
- # sidekiq_running_jobs, sidekiq_jobs_failed_total should not include the job_status label
@metrics[:sidekiq_running_jobs].increment(labels, -1)
- if Feature.enabled?(:emit_sidekiq_histogram_metrics, type: :ops)
- @metrics[:sidekiq_jobs_failed_total].increment(labels, 1) unless job_succeeded
- else
- # we don't need job_status label here
- @metrics[:sidekiq_jobs_completion_seconds_sum].increment(labels, monotonic_time)
- end
+ @instrumentation = job[:instrumentation] || {}
+
+ record_resource_usage_counters
# job_status: done, fail match the job_status attribute in structured logging
labels[:job_status] = job_succeeded ? "done" : "fail"
- instrumentation = job[:instrumentation] || {}
- @metrics[:sidekiq_jobs_cpu_seconds].observe(labels, job_thread_cputime)
- @metrics[:sidekiq_jobs_completion_seconds]&.observe(labels, monotonic_time)
+ record_histograms
- @metrics[:sidekiq_jobs_db_seconds].observe(labels, ActiveRecord::LogSubscriber.runtime / 1000)
- @metrics[:sidekiq_jobs_gitaly_seconds].observe(labels, get_gitaly_time(instrumentation))
@metrics[:sidekiq_redis_requests_total].increment(labels, get_redis_calls(instrumentation))
- @metrics[:sidekiq_redis_requests_duration_seconds].observe(labels, get_redis_time(instrumentation))
@metrics[:sidekiq_elasticsearch_requests_total].increment(labels, get_elasticsearch_calls(instrumentation))
- @metrics[:sidekiq_elasticsearch_requests_duration_seconds].observe(labels, get_elasticsearch_time(instrumentation))
@metrics[:sidekiq_mem_total_bytes].set(labels, get_thread_memory_total_allocations(instrumentation))
with_load_balancing_settings(job) do |settings|
@@ -162,15 +163,50 @@ module Gitlab
@metrics[:sidekiq_load_balancing_count].increment(labels.merge(load_balancing_labels), 1)
end
- sli_labels = labels.slice(*SIDEKIQ_SLI_LABELS)
- Gitlab::Metrics::SidekiqSlis.record_execution_apdex(sli_labels, monotonic_time) if job_succeeded
- Gitlab::Metrics::SidekiqSlis.record_execution_error(sli_labels, !job_succeeded)
- Gitlab::Metrics::SidekiqSlis.record_queueing_apdex(sli_labels, queue_duration) if queue_duration
+ @sli_labels = labels.slice(*SIDEKIQ_SLI_LABELS)
+ record_execution_sli
+ record_queueing_sli
end
end
private
+ attr_reader :labels, :job, :queue_duration, :job_succeeded, :monotonic_time, :job_thread_cputime, :instrumentation, :sli_labels
+
+ def record_resource_usage_counters
+ if Feature.enabled?(:emit_sidekiq_histogram_metrics, type: :ops)
+ @metrics[:sidekiq_jobs_failed_total].increment(labels, 1) unless job_succeeded
+ else
+ @metrics[:sidekiq_jobs_completion_seconds_sum].increment(labels, monotonic_time)
+ @metrics[:sidekiq_jobs_completion_count].increment(labels, 1)
+ @metrics[:sidekiq_jobs_cpu_seconds_sum].increment(labels, job_thread_cputime)
+ @metrics[:sidekiq_jobs_db_seconds_sum].increment(labels, ActiveRecord::LogSubscriber.runtime / 1000)
+ @metrics[:sidekiq_jobs_gitaly_seconds_sum].increment(labels, get_gitaly_time(instrumentation))
+ @metrics[:sidekiq_redis_requests_duration_seconds_sum].increment(labels, get_redis_time(instrumentation))
+ @metrics[:sidekiq_elasticsearch_requests_duration_seconds_sum].increment(labels, get_elasticsearch_time(instrumentation))
+ end
+ end
+
+ def record_histograms
+ @metrics[:sidekiq_jobs_cpu_seconds]&.observe(labels, job_thread_cputime)
+
+ @metrics[:sidekiq_jobs_completion_seconds]&.observe(labels, monotonic_time)
+
+ @metrics[:sidekiq_jobs_db_seconds]&.observe(labels, ActiveRecord::LogSubscriber.runtime / 1000)
+ @metrics[:sidekiq_jobs_gitaly_seconds]&.observe(labels, get_gitaly_time(instrumentation))
+ @metrics[:sidekiq_redis_requests_duration_seconds]&.observe(labels, get_redis_time(instrumentation))
+ @metrics[:sidekiq_elasticsearch_requests_duration_seconds]&.observe(labels, get_elasticsearch_time(instrumentation))
+ end
+
+ def record_queueing_sli
+ Gitlab::Metrics::SidekiqSlis.record_queueing_apdex(sli_labels, queue_duration) if queue_duration
+ end
+
+ def record_execution_sli
+ Gitlab::Metrics::SidekiqSlis.record_execution_apdex(sli_labels, monotonic_time) if job_succeeded
+ Gitlab::Metrics::SidekiqSlis.record_execution_error(sli_labels, !job_succeeded)
+ end
+
def with_load_balancing_settings(job)
keys = %w[load_balancing_strategy worker_data_consistency]
return unless keys.all? { |k| job.key?(k) }
diff --git a/lib/gitlab/sidekiq_middleware/skip_jobs.rb b/lib/gitlab/sidekiq_middleware/skip_jobs.rb
index 34ad843e8ee..56b150116a3 100644
--- a/lib/gitlab/sidekiq_middleware/skip_jobs.rb
+++ b/lib/gitlab/sidekiq_middleware/skip_jobs.rb
@@ -80,13 +80,20 @@ module Gitlab
end
health_check_attrs = worker_class.database_health_check_attrs
- job_base_model = Gitlab::Database.schemas_to_base_models[health_check_attrs[:gitlab_schema]].first
+
+ tables, schema = health_check_attrs.values_at(:tables, :gitlab_schema)
+
+ if health_check_attrs[:block].respond_to?(:call)
+ schema, tables = health_check_attrs[:block].call(job['args'], schema, tables)
+ end
+
+ job_base_model = Gitlab::Database.schemas_to_base_models[schema].first
health_context = Gitlab::Database::HealthStatus::Context.new(
DatabaseHealthStatusChecker.new(job['jid'], worker_class.name),
job_base_model.connection,
- health_check_attrs[:tables],
- health_check_attrs[:gitlab_schema]
+ tables,
+ schema
)
Gitlab::Database::HealthStatus.evaluate(health_context).any?(&:stop?)
diff --git a/lib/gitlab/sidekiq_status.rb b/lib/gitlab/sidekiq_status.rb
index 778d278146d..ae4aca7ff92 100644
--- a/lib/gitlab/sidekiq_status.rb
+++ b/lib/gitlab/sidekiq_status.rb
@@ -94,8 +94,17 @@ module Gitlab
keys = job_ids.map { |jid| key_for(jid) }
- with_redis { |redis| redis.mget(*keys) }
- .map { |result| !result.nil? }
+ status = with_redis do |redis|
+ Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
+ if Gitlab::Redis::ClusterUtil.cluster?(redis)
+ Gitlab::Redis::ClusterUtil.batch_get(keys, redis)
+ else
+ redis.mget(*keys)
+ end
+ end
+ end
+
+ status.map { |result| !result.nil? }
end
# Returns the JIDs that are completed
diff --git a/lib/gitlab/tracking.rb b/lib/gitlab/tracking.rb
index f127e14243c..3bbcd59f45e 100644
--- a/lib/gitlab/tracking.rb
+++ b/lib/gitlab/tracking.rb
@@ -1,5 +1,8 @@
# frozen_string_literal: true
+# WARNING: This module has been deprecated and will be removed in the future
+# Use InternalEvents.track_event instead https://docs.gitlab.com/ee/development/internal_analytics/internal_event_instrumentation/index.html
+
module Gitlab
module Tracking
class << self
diff --git a/lib/gitlab/url_builder.rb b/lib/gitlab/url_builder.rb
index 1b7dcaa5cf4..a9b8dc313d0 100644
--- a/lib/gitlab/url_builder.rb
+++ b/lib/gitlab/url_builder.rb
@@ -40,6 +40,8 @@ module Gitlab
note_url(object, **options)
when Release
instance.release_url(object, **options)
+ when Organizations::Organization
+ instance.organization_url(object, **options)
when Project
instance.project_url(object, **options)
when Snippet
diff --git a/lib/gitlab/usage/metric_definition.rb b/lib/gitlab/usage/metric_definition.rb
index 7252283d1b9..941c2f793c4 100644
--- a/lib/gitlab/usage/metric_definition.rb
+++ b/lib/gitlab/usage/metric_definition.rb
@@ -3,7 +3,7 @@
module Gitlab
module Usage
class MetricDefinition
- METRIC_SCHEMA_PATH = Rails.root.join('config', 'metrics', 'schema.json')
+ METRIC_SCHEMA_PATH = Rails.root.join('config', 'metrics', 'schema', '**', '*.json')
AVAILABLE_STATUSES = %w[active broken].to_set.freeze
VALID_SERVICE_PING_STATUSES = %w[active broken].to_set.freeze
@@ -52,7 +52,7 @@ module Gitlab
end
def validate!
- self.class.schemer.validate(attributes.deep_stringify_keys).each do |error|
+ errors.each do |error|
error_message = <<~ERROR_MSG
Error type: #{error['type']}
Data: #{error['data']}
@@ -104,8 +104,10 @@ module Gitlab
definitions[key_path]&.to_context
end
- def schemer
- @schemer ||= ::JSONSchemer.schema(Pathname.new(METRIC_SCHEMA_PATH))
+ def schemers
+ @schemers ||= Dir[METRIC_SCHEMA_PATH].map do |path|
+ ::JSONSchemer.schema(Pathname.new(path))
+ end
end
def dump_metrics_yaml
@@ -145,6 +147,19 @@ module Gitlab
private
+ def errors
+ result = []
+
+ self.class.schemers.each do |schemer|
+ # schemer.validate returns an Enumerator object
+ schemer.validate(attributes.deep_stringify_keys).each do |error|
+ result << error
+ end
+ end
+
+ result
+ end
+
def method_missing(method, *args)
attributes[method] || super
end
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index b2027791e9d..5f819f060e4 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -174,7 +174,6 @@ module Gitlab
prometheus_enabled: alt_usage_data(fallback: nil) { Gitlab::Prometheus::Internal.prometheus_enabled? },
prometheus_metrics_enabled: alt_usage_data(fallback: nil) { Gitlab::Metrics.prometheus_metrics_enabled? },
reply_by_email_enabled: alt_usage_data(fallback: nil) { Gitlab::Email::IncomingEmail.enabled? },
- web_ide_clientside_preview_enabled: alt_usage_data(fallback: nil) { false },
signup_enabled: alt_usage_data(fallback: nil) { Gitlab::CurrentSettings.allow_signup? },
grafana_link_enabled: alt_usage_data(fallback: nil) { Gitlab::CurrentSettings.grafana_enabled? },
gitpod_enabled: alt_usage_data(fallback: nil) { Gitlab::CurrentSettings.gitpod_enabled? }
diff --git a/lib/gitlab/usage_data_counters/hll_redis_counter.rb b/lib/gitlab/usage_data_counters/hll_redis_counter.rb
index f9dc8bd8a3c..185b49d4a68 100644
--- a/lib/gitlab/usage_data_counters/hll_redis_counter.rb
+++ b/lib/gitlab/usage_data_counters/hll_redis_counter.rb
@@ -1,5 +1,8 @@
# frozen_string_literal: true
+# WARNING: This module has been deprecated and will be removed in the future
+# Use InternalEvents.track_event instead https://docs.gitlab.com/ee/development/internal_analytics/internal_event_instrumentation/index.html
+
module Gitlab
module UsageDataCounters
module HLLRedisCounter
@@ -53,8 +56,6 @@ module Gitlab
private
def track(values, event_name, time: Time.zone.now)
- return unless ::ServicePing::ServicePingSettings.enabled?
-
event = event_for(event_name)
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(UnknownEvent.new("Unknown event #{event_name}")) unless event.present?
diff --git a/lib/gitlab/usage_data_counters/redis_counter.rb b/lib/gitlab/usage_data_counters/redis_counter.rb
index 591e431c871..3f16681b642 100644
--- a/lib/gitlab/usage_data_counters/redis_counter.rb
+++ b/lib/gitlab/usage_data_counters/redis_counter.rb
@@ -1,17 +1,16 @@
# frozen_string_literal: true
+# WARNING: This module has been deprecated and will be removed in the future
+# Use InternalEvents.track_event instead https://docs.gitlab.com/ee/development/internal_analytics/internal_event_instrumentation/index.html
+
module Gitlab
module UsageDataCounters
module RedisCounter
def increment(redis_counter_key)
- return unless ::ServicePing::ServicePingSettings.enabled?
-
Gitlab::Redis::SharedState.with { |redis| redis.incr(redis_counter_key) }
end
def increment_by(redis_counter_key, incr)
- return unless ::ServicePing::ServicePingSettings.enabled?
-
Gitlab::Redis::SharedState.with { |redis| redis.incrby(redis_counter_key, incr) }
end
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index f2db7e3c9b9..057e89a2a97 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -20,7 +20,7 @@ module Gitlab
include JwtAuthenticatable
class << self
- def git_http_ok(repository, repo_type, user, action, show_all_refs: false)
+ def git_http_ok(repository, repo_type, user, action, show_all_refs: false, need_audit: false)
raise "Unsupported action: #{action}" unless ALLOWED_GIT_HTTP_ACTIONS.include?(action.to_s)
attrs = {
@@ -28,6 +28,7 @@ module Gitlab
GL_REPOSITORY: repo_type.identifier_for_container(repository.container),
GL_USERNAME: user&.username,
ShowAllRefs: show_all_refs,
+ NeedAudit: need_audit,
Repository: repository.gitaly_repository.to_h,
GitConfigOptions: [],
GitalyServer: {
diff --git a/lib/peek/views/rugged.rb b/lib/peek/views/rugged.rb
deleted file mode 100644
index 3ed54a010f8..00000000000
--- a/lib/peek/views/rugged.rb
+++ /dev/null
@@ -1,46 +0,0 @@
-# frozen_string_literal: true
-
-module Peek
- module Views
- class Rugged < DetailedView
- def results
- return {} unless calls > 0
-
- super
- end
-
- private
-
- def duration
- ::Gitlab::RuggedInstrumentation.query_time_ms
- end
-
- def calls
- ::Gitlab::RuggedInstrumentation.query_count
- end
-
- def call_details
- ::Gitlab::RuggedInstrumentation.list_call_details
- end
-
- def format_call_details(call)
- super.merge(args: format_args(call[:args]))
- end
-
- def format_args(args)
- args.map do |arg|
- # ActiveSupport::JSON recursively calls as_json on all
- # instance variables, and if that instance variable points to
- # something that refers back to the same instance, we can wind
- # up in an infinite loop. Currently this only seems to happen with
- # Gitlab::Git::Repository and ::Repository.
- if arg.instance_variables.present?
- arg.to_s
- else
- arg
- end
- end
- end
- end
- end
-end
diff --git a/lib/sbom/purl_type/converter.rb b/lib/sbom/purl_type/converter.rb
index bfcfb414180..bc08083fdae 100644
--- a/lib/sbom/purl_type/converter.rb
+++ b/lib/sbom/purl_type/converter.rb
@@ -18,6 +18,7 @@ module Sbom
'nuget' => 'nuget',
'pip' => 'pypi',
'pipenv' => 'pypi',
+ 'poetry' => 'pypi',
'setuptools' => 'pypi',
'python-pkg' => 'pypi' # this package manager is generated by trivy
}.with_indifferent_access.freeze
diff --git a/lib/sidebars/admin/menus/admin_settings_menu.rb b/lib/sidebars/admin/menus/admin_settings_menu.rb
index 4656e0f33e2..4d2d19c60f7 100644
--- a/lib/sidebars/admin/menus/admin_settings_menu.rb
+++ b/lib/sidebars/admin/menus/admin_settings_menu.rb
@@ -12,7 +12,6 @@ module Sidebars
add_item(ci_cd_menu_item)
add_item(reporting_menu_item)
add_item(metrics_and_profiling_menu_item)
- add_item(service_usage_data_menu_item)
add_item(network_settings_menu_item)
add_item(appearance_menu_item)
add_item(preferences_menu_item)
@@ -102,15 +101,6 @@ module Sidebars
)
end
- def service_usage_data_menu_item
- ::Sidebars::MenuItem.new(
- title: _('Service usage data'),
- link: service_usage_data_admin_application_settings_path,
- active_routes: { path: 'admin/application_settings#service_usage_data' },
- item_id: :admin_service_usage
- )
- end
-
def network_settings_menu_item
::Sidebars::MenuItem.new(
title: _('Network'),
diff --git a/lib/sidebars/explore/menus/catalog_menu.rb b/lib/sidebars/explore/menus/catalog_menu.rb
new file mode 100644
index 00000000000..2d8e8bba08b
--- /dev/null
+++ b/lib/sidebars/explore/menus/catalog_menu.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module Sidebars
+ module Explore
+ module Menus
+ class CatalogMenu < ::Sidebars::Menu
+ override :link
+ def link
+ explore_catalog_index_path
+ end
+
+ override :title
+ def title
+ _('CI/CD Catalog')
+ end
+
+ override :sprite_icon
+ def sprite_icon
+ 'catalog-checkmark'
+ end
+
+ override :render?
+ def render?
+ Feature.enabled?(:global_ci_catalog, current_user)
+ end
+
+ override :active_routes
+ def active_routes
+ { controller: ['explore/catalog'] }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/sidebars/explore/panel.rb b/lib/sidebars/explore/panel.rb
index 6260df6bb5f..3559f7d9627 100644
--- a/lib/sidebars/explore/panel.rb
+++ b/lib/sidebars/explore/panel.rb
@@ -28,6 +28,7 @@ module Sidebars
def add_menus
add_menu(Sidebars::Explore::Menus::ProjectsMenu.new(context))
add_menu(Sidebars::Explore::Menus::GroupsMenu.new(context))
+ add_menu(Sidebars::Explore::Menus::CatalogMenu.new(context))
add_menu(Sidebars::Explore::Menus::TopicsMenu.new(context))
add_menu(Sidebars::Explore::Menus::SnippetsMenu.new(context))
end
diff --git a/lib/sidebars/menu.rb b/lib/sidebars/menu.rb
index ee02429baf3..8fcb373c9dc 100644
--- a/lib/sidebars/menu.rb
+++ b/lib/sidebars/menu.rb
@@ -80,6 +80,7 @@ module Sidebars
is_active = @context.route_is_active.call(active_routes) || items.any? { |item| item[:is_active] }
{
+ id: self.class.name.demodulize.underscore,
title: title,
icon: sprite_icon,
avatar: avatar,
diff --git a/lib/sidebars/organizations/menus/manage_menu.rb b/lib/sidebars/organizations/menus/manage_menu.rb
index 0df716cdd3f..7c342002c31 100644
--- a/lib/sidebars/organizations/menus/manage_menu.rb
+++ b/lib/sidebars/organizations/menus/manage_menu.rb
@@ -30,6 +30,15 @@ module Sidebars
item_id: :organization_groups_and_projects
)
)
+ add_item(
+ ::Sidebars::MenuItem.new(
+ title: _('Users'),
+ link: users_organization_path(context.container),
+ super_sidebar_parent: ::Sidebars::Organizations::Menus::ManageMenu,
+ active_routes: { path: 'organizations/organizations#users' },
+ item_id: :organization_users
+ )
+ )
end
end
end
diff --git a/lib/sidebars/projects/menus/ci_cd_menu.rb b/lib/sidebars/projects/menus/ci_cd_menu.rb
index 02596b16cfa..c77e8e996b0 100644
--- a/lib/sidebars/projects/menus/ci_cd_menu.rb
+++ b/lib/sidebars/projects/menus/ci_cd_menu.rb
@@ -18,7 +18,7 @@ module Sidebars
override :extra_container_html_options
def extra_container_html_options
{
- class: 'shortcuts-pipelines rspec-link-pipelines'
+ class: 'shortcuts-pipelines'
}
end
diff --git a/lib/sidebars/projects/menus/infrastructure_menu.rb b/lib/sidebars/projects/menus/infrastructure_menu.rb
index b08845a37e6..d3c9f3a6466 100644
--- a/lib/sidebars/projects/menus/infrastructure_menu.rb
+++ b/lib/sidebars/projects/menus/infrastructure_menu.rb
@@ -70,7 +70,7 @@ module Sidebars
highlight: Users::CalloutsHelper::GKE_CLUSTER_INTEGRATION,
highlight_priority: Users::Callout.feature_names[:GKE_CLUSTER_INTEGRATION],
dismiss_endpoint: callouts_path,
- auto_devops_help_path: help_page_path('topics/autodevops/index.md') } }
+ auto_devops_help_path: help_page_path('topics/autodevops/index') } }
end
def terraform_states_menu_item
diff --git a/lib/sidebars/projects/menus/scope_menu.rb b/lib/sidebars/projects/menus/scope_menu.rb
index f388c814bd7..d03abfdfb7e 100644
--- a/lib/sidebars/projects/menus/scope_menu.rb
+++ b/lib/sidebars/projects/menus/scope_menu.rb
@@ -22,7 +22,7 @@ module Sidebars
override :extra_container_html_options
def extra_container_html_options
{
- class: 'shortcuts-project rspec-project-link'
+ class: 'shortcuts-project'
}
end
diff --git a/lib/sidebars/projects/menus/settings_menu.rb b/lib/sidebars/projects/menus/settings_menu.rb
index 8fed1c46425..077eebf58b9 100644
--- a/lib/sidebars/projects/menus/settings_menu.rb
+++ b/lib/sidebars/projects/menus/settings_menu.rb
@@ -57,10 +57,6 @@ module Sidebars
monitor_menu_item,
usage_quotas_menu_item
]
- elsif context.current_user && can?(context.current_user, :manage_resource_access_tokens, context.project)
- [
- access_tokens_menu_item
- ]
else
[]
end
diff --git a/lib/sidebars/projects/super_sidebar_menus/monitor_menu.rb b/lib/sidebars/projects/super_sidebar_menus/monitor_menu.rb
index 0441d3b4a03..d4ecf132c44 100644
--- a/lib/sidebars/projects/super_sidebar_menus/monitor_menu.rb
+++ b/lib/sidebars/projects/super_sidebar_menus/monitor_menu.rb
@@ -18,6 +18,7 @@ module Sidebars
def configure_menu_items
[
:tracing,
+ :metrics,
:error_tracking,
:alert_management,
:incidents,
diff --git a/lib/sidebars/user_settings/menus/comment_templates_menu.rb b/lib/sidebars/user_settings/menus/comment_templates_menu.rb
index da37c42bbd4..1e9aea8ec9a 100644
--- a/lib/sidebars/user_settings/menus/comment_templates_menu.rb
+++ b/lib/sidebars/user_settings/menus/comment_templates_menu.rb
@@ -23,7 +23,7 @@ module Sidebars
override :render?
def render?
- !!context.current_user && saved_replies_enabled?
+ !!context.current_user
end
override :active_routes
diff --git a/lib/tasks/gitlab/bulk_add_permission.rake b/lib/tasks/gitlab/bulk_add_permission.rake
index 240b808baf3..917fce42762 100644
--- a/lib/tasks/gitlab/bulk_add_permission.rake
+++ b/lib/tasks/gitlab/bulk_add_permission.rake
@@ -3,7 +3,7 @@
namespace :gitlab do
namespace :import do
desc "GitLab | Import | Add all users to all projects (admin users are added as maintainers)"
- task all_users_to_all_projects: :environment do |t, args|
+ task all_users_to_all_projects: :environment do |t, args|
user_ids = User.where(admin: false).pluck(:id)
admin_ids = User.where(admin: true).pluck(:id)
projects = Project.all
diff --git a/lib/tasks/gitlab/click_house/migration.rake b/lib/tasks/gitlab/click_house/migration.rake
new file mode 100644
index 00000000000..ddac81ec98f
--- /dev/null
+++ b/lib/tasks/gitlab/click_house/migration.rake
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+namespace :gitlab do
+ namespace :clickhouse do
+ task :prepare_schema_migration_table, [:database] => :environment do |_t, args|
+ require_relative '../../../../lib/click_house/migration_support/schema_migration'
+
+ ClickHouse::MigrationSupport::SchemaMigration.create_table(args.database&.to_sym || :main)
+ end
+
+ desc 'GitLab | ClickHouse | Migrate'
+ task migrate: [:prepare_schema_migration_table] do
+ migrate(:up)
+ end
+
+ desc 'GitLab | ClickHouse | Rollback'
+ task rollback: [:prepare_schema_migration_table] do
+ migrate(:down)
+ end
+
+ private
+
+ def check_target_version
+ return unless target_version
+
+ version = ENV['VERSION']
+
+ return if ClickHouse::Migration::MIGRATION_FILENAME_REGEXP.match?(version) || /\A\d+\z/.match?(version)
+
+ raise "Invalid format of target version: `VERSION=#{version}`"
+ end
+
+ def target_version
+ ENV['VERSION'].to_i if ENV['VERSION'] && !ENV['VERSION'].empty?
+ end
+
+ def migrate(direction)
+ require_relative '../../../../lib/click_house/migration_support/schema_migration'
+ require_relative '../../../../lib/click_house/migration_support/migration_context'
+ require_relative '../../../../lib/click_house/migration_support/migrator'
+
+ check_target_version
+
+ scope = ENV['SCOPE']
+ verbose_was = ClickHouse::Migration.verbose
+ ClickHouse::Migration.verbose = ENV['VERBOSE'] ? ENV['VERBOSE'] != 'false' : true
+
+ migrations_paths = ClickHouse::MigrationSupport::Migrator.migrations_paths
+ schema_migration = ClickHouse::MigrationSupport::SchemaMigration
+ migration_context = ClickHouse::MigrationSupport::MigrationContext.new(migrations_paths, schema_migration)
+ migrations_ran = migration_context.public_send(direction, target_version) do |migration|
+ scope.blank? || scope == migration.scope
+ end
+
+ puts('No migrations ran.') unless migrations_ran&.any?
+ ensure
+ ClickHouse::Migration.verbose = verbose_was
+ end
+ end
+end
diff --git a/lib/tasks/gitlab/db.rake b/lib/tasks/gitlab/db.rake
index cf52a219e83..d89ab548419 100644
--- a/lib/tasks/gitlab/db.rake
+++ b/lib/tasks/gitlab/db.rake
@@ -107,7 +107,10 @@ namespace :gitlab do
end
end
- Rake::Task['db:seed_fu'].invoke if databases_loaded.present? && databases_loaded.all?
+ if databases_loaded.present? && databases_loaded.all?
+ Rake::Task["gitlab:db:lock_writes"].invoke
+ Rake::Task['db:seed_fu'].invoke
+ end
end
def configure_database(connection, database_name: nil)
@@ -454,7 +457,12 @@ namespace :gitlab do
ActiveRecord::Base.establish_connection(config) # rubocop: disable Database/EstablishConnection
Gitlab::Database.check_for_non_superuser
- Rake::Task['db:migrate'].invoke
+
+ if Rake::Task.task_defined?("db:migrate:#{db_config.name}")
+ Rake::Task["db:migrate:#{db_config.name}"].invoke
+ else
+ Rake::Task["db:migrate"].invoke
+ end
end
end
diff --git a/lib/tasks/gitlab/features.rake b/lib/tasks/gitlab/features.rake
deleted file mode 100644
index e44328e0de1..00000000000
--- a/lib/tasks/gitlab/features.rake
+++ /dev/null
@@ -1,34 +0,0 @@
-# frozen_string_literal: true
-
-namespace :gitlab do
- namespace :features do
- desc 'GitLab | Features | Enable direct Git access via Rugged for NFS'
- task enable_rugged: :environment do
- set_rugged_feature_flags(true)
- puts 'All Rugged feature flags were enabled.'
- end
-
- task disable_rugged: :environment do
- set_rugged_feature_flags(false)
- puts 'All Rugged feature flags were disabled.'
- end
-
- task unset_rugged: :environment do
- set_rugged_feature_flags(nil)
- puts 'All Rugged feature flags were unset.'
- end
- end
-
- def set_rugged_feature_flags(status)
- Gitlab::Git::RuggedImpl::Repository::FEATURE_FLAGS.each do |flag|
- case status
- when nil
- Feature.remove(flag)
- when true
- Feature.enable(flag)
- when false
- Feature.disable(flag)
- end
- end
- end
-end
diff --git a/lib/tasks/gitlab/redis.rake b/lib/tasks/gitlab/redis.rake
new file mode 100644
index 00000000000..6983c5fc318
--- /dev/null
+++ b/lib/tasks/gitlab/redis.rake
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+namespace :gitlab do
+ namespace :redis do
+ namespace :secret do
+ desc "GitLab | Redis | Secret | Show Redis secret"
+ task :show, [:instance_name] => [:environment] do |_t, args|
+ Gitlab::EncryptedRedisCommand.show(args: args)
+ end
+
+ desc "GitLab | Redis | Secret | Edit Redis secret"
+ task :edit, [:instance_name] => [:environment] do |_t, args|
+ Gitlab::EncryptedRedisCommand.edit(args: args)
+ end
+
+ desc "GitLab | Redis | Secret | Write Redis secret"
+ task :write, [:instance_name] => [:environment] do |_t, args|
+ content = $stdin.tty? ? $stdin.gets : $stdin.read
+ Gitlab::EncryptedRedisCommand.write(content, args: args)
+ end
+ end
+ end
+end
diff --git a/lib/tasks/gitlab/seed/ci_catalog_resources.rake b/lib/tasks/gitlab/seed/ci_catalog_resources.rake
new file mode 100644
index 00000000000..1db995aa801
--- /dev/null
+++ b/lib/tasks/gitlab/seed/ci_catalog_resources.rake
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+# This task should be enabled when the seeder gets fixed:
+# https://gitlab.com/gitlab-org/gitlab/-/issues/429649
+#
+# Seed CI/CD catalog resources
+#
+# @param group_path - Group name under which to create the projects
+# @param seed_count - Total number of Catalog resources to create (default: 30)
+#
+# @example
+# bundle exec rake "gitlab:seed:ci_catalog_resources[root, 50]"
+#
+# namespace :gitlab do
+# namespace :seed do
+# desc 'Seed CI Catalog resources'
+# task :ci_catalog_resources,
+# [:group_path, :seed_count] => :gitlab_environment do |_t, args|
+# Gitlab::Seeders::Ci::Catalog::ResourceSeeder.new(
+# group_path: args.group_path,
+# seed_count: args.seed_count&.to_i
+# ).seed
+# puts "Task finished!"
+# end
+# end
+# end
diff --git a/lib/tasks/gitlab/tw/codeowners.rake b/lib/tasks/gitlab/tw/codeowners.rake
index 495d7a339b8..de1401feb8a 100644
--- a/lib/tasks/gitlab/tw/codeowners.rake
+++ b/lib/tasks/gitlab/tw/codeowners.rake
@@ -26,32 +26,31 @@ namespace :tw do
CodeOwnerRule.new('Analytics Instrumentation', '@lciutacu'),
CodeOwnerRule.new('Anti-Abuse', '@phillipwells'),
CodeOwnerRule.new('Cloud Connector', '@jglassman1'),
- CodeOwnerRule.new('Authentication and Authorization', '@jglassman1'),
+ CodeOwnerRule.new('Authentication', '@jglassman1'),
+ CodeOwnerRule.new('Authorization', '@jglassman1'),
# CodeOwnerRule.new('Billing and Subscription Management', ''),
CodeOwnerRule.new('Code Creation', '@jglassman1'),
CodeOwnerRule.new('Code Review', '@aqualls'),
CodeOwnerRule.new('Compliance', '@eread'),
CodeOwnerRule.new('Composition Analysis', '@rdickenson'),
- CodeOwnerRule.new('Environments', '@phillipwells'),
CodeOwnerRule.new('Container Registry', '@marcel.amirault'),
CodeOwnerRule.new('Contributor Experience', '@eread'),
CodeOwnerRule.new('Database', '@aqualls'),
CodeOwnerRule.new('DataOps', '@sselhorn'),
# CodeOwnerRule.new('Delivery', ''),
- CodeOwnerRule.new('Development', '@sselhorn'),
CodeOwnerRule.new('Distribution', '@axil'),
CodeOwnerRule.new('Distribution (Charts)', '@axil'),
CodeOwnerRule.new('Distribution (Omnibus)', '@eread'),
- CodeOwnerRule.new('Documentation Guidelines', '@sselhorn'),
CodeOwnerRule.new('Duo Chat', '@sselhorn'),
CodeOwnerRule.new('Dynamic Analysis', '@rdickenson'),
CodeOwnerRule.new('Editor Extensions', '@aqualls'),
+ CodeOwnerRule.new('Environments', '@phillipwells'),
CodeOwnerRule.new('Foundations', '@sselhorn'),
# CodeOwnerRule.new('Fulfillment Platform', ''),
CodeOwnerRule.new('Fuzz Testing', '@rdickenson'),
CodeOwnerRule.new('Geo', '@axil'),
CodeOwnerRule.new('Gitaly', '@eread'),
- # CodeOwnerRule.new('GitLab Dedicated', ''),
+ CodeOwnerRule.new('GitLab Dedicated', '@lyspin'),
CodeOwnerRule.new('Global Search', '@ashrafkhamis'),
CodeOwnerRule.new('IDE', '@ashrafkhamis'),
CodeOwnerRule.new('Import and Integrate', '@eread @ashrafkhamis'),
@@ -75,9 +74,9 @@ namespace :tw do
CodeOwnerRule.new('Runner', '@fneill'),
CodeOwnerRule.new('Runner SaaS', '@fneill'),
CodeOwnerRule.new('Security Policies', '@rdickenson'),
+ CodeOwnerRule.new('Solutions Architecture', '@jfullam @brianwald @Darwinjs'),
CodeOwnerRule.new('Source Code', '@msedlakjakubowski'),
CodeOwnerRule.new('Static Analysis', '@rdickenson'),
- CodeOwnerRule.new('Style Guide', '@sselhorn'),
CodeOwnerRule.new('Tenant Scale', '@lciutacu'),
CodeOwnerRule.new('Testing', '@eread'),
CodeOwnerRule.new('Threat Insights', '@rdickenson'),
@@ -87,6 +86,33 @@ namespace :tw do
# CodeOwnerRule.new('Vulnerability Research', '')
].freeze
+ CONTRIBUTOR_DOCS_PATH = '/doc/development/'
+ CONTRIBUTOR_DOCS_CODE_OWNER_RULES = [
+ CodeOwnerRule.new('Analytics Instrumentation',
+ '@gitlab-org/analytics-section/product-analytics/engineers/frontend ' \
+ '@gitlab-org/analytics-section/analytics-instrumentation/engineers'),
+ CodeOwnerRule.new('Authentication', '@gitlab-org/govern/authentication/approvers'),
+ CodeOwnerRule.new('Authorization', '@gitlab-org/govern/authorization/approvers'),
+ CodeOwnerRule.new('Compliance',
+ '@gitlab-org/govern/security-policies-frontend @gitlab-org/govern/threat-insights-frontend-team ' \
+ '@gitlab-org/govern/threat-insights-backend-team'),
+ CodeOwnerRule.new('Composition Analysis',
+ '@gitlab-org/secure/composition-analysis-be @gitlab-org/secure/static-analysis'),
+ CodeOwnerRule.new('Distribution', '@gitlab-org/distribution'),
+ CodeOwnerRule.new('Documentation Guidelines', '@sselhorn'),
+ CodeOwnerRule.new('Engineering Productivity', '@gl-quality/eng-prod'),
+ CodeOwnerRule.new('Foundations', '@gitlab-org/manage/foundations/engineering'),
+ CodeOwnerRule.new('Gitaly', '@proglottis @toon'),
+ CodeOwnerRule.new('Global Search', '@gitlab-org/search-team/migration-maintainers'),
+ CodeOwnerRule.new('IDE',
+ '@gitlab-org/maintainers/remote-development/backend @gitlab-org/maintainers/remote-development/frontend'),
+ CodeOwnerRule.new('Pipeline Authoring', '@gitlab-org/maintainers/cicd-verify'),
+ CodeOwnerRule.new('Pipeline Execution', '@gitlab-org/maintainers/cicd-verify'),
+ CodeOwnerRule.new('Product Analytics', '@gitlab-org/analytics-section/product-analytics/engineers/frontend'),
+ CodeOwnerRule.new('Tenant Scale', '@abdwdd @alexpooley @manojmj'),
+ CodeOwnerRule.new('Threat Insights', '@gitlab-org/govern/threat-insights-frontend-team')
+ ].freeze
+
ERRORS_EXCLUDED_FILES = [
'/doc/architecture'
].freeze
@@ -105,7 +131,8 @@ namespace :tw do
end
def self.writer_for_group(category, path)
- writer = CODE_OWNER_RULES.find { |rule| rule.category == category }&.writer
+ rules = path.start_with?(CONTRIBUTOR_DOCS_PATH) ? CONTRIBUTOR_DOCS_CODE_OWNER_RULES : CODE_OWNER_RULES
+ writer = rules.find { |rule| rule.category == category }&.writer
if writer.is_a?(String) || writer.nil?
writer
diff --git a/lib/tasks/tanuki_emoji.rake b/lib/tasks/tanuki_emoji.rake
index b3099853434..aec7d3c1bf6 100644
--- a/lib/tasks/tanuki_emoji.rake
+++ b/lib/tasks/tanuki_emoji.rake
@@ -30,9 +30,9 @@ namespace :tanuki_emoji do
require 'digest/sha2'
digest_emoji_map = {}
- emojis_map = {}
+ emojis_array = []
- TanukiEmoji.index.all.each do |emoji|
+ TanukiEmoji.index.all.sort_by(&:sort_key).each do |emoji|
emoji_path = Gitlab::Emoji.emoji_public_absolute_path.join("#{emoji.name}.png")
digest_entry = {
@@ -47,13 +47,14 @@ namespace :tanuki_emoji do
# Our new map is only characters to make the json substantially smaller
emoji_entry = {
+ n: emoji.name,
c: emoji.category,
e: emoji.codepoints,
d: emoji.description,
u: emoji.unicode_version
}
- emojis_map[emoji.name] = emoji_entry
+ emojis_array << emoji_entry
end
digests_json = File.join(Rails.root, 'fixtures', 'emojis', 'digests.json')
@@ -63,7 +64,7 @@ namespace :tanuki_emoji do
emojis_json = Gitlab::Emoji.emoji_public_absolute_path.join('emojis.json')
File.open(emojis_json, 'w') do |handle|
- handle.write(Gitlab::Json.pretty_generate(emojis_map))
+ handle.write(Gitlab::Json.pretty_generate(emojis_array))
end
end
diff --git a/lib/unnested_in_filters/rewriter.rb b/lib/unnested_in_filters/rewriter.rb
index 9eb1c0b8273..2e334eb147b 100644
--- a/lib/unnested_in_filters/rewriter.rb
+++ b/lib/unnested_in_filters/rewriter.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-# rubocop:disable CodeReuse/ActiveRecord (This module is generating ActiveRecord relations therefore using AR methods is necessary)
+# rubocop:disable CodeReuse/ActiveRecord -- This module is generating ActiveRecord relations therefore using AR methods is necessary
module UnnestedInFilters
class Rewriter
include Gitlab::Utils::StrongMemoize
@@ -295,3 +295,4 @@ module UnnestedInFilters
end
end
end
+# rubocop:enable CodeReuse/ActiveRecord
diff --git a/lib/vs_code/settings.rb b/lib/vs_code/settings.rb
index 30b91ebb16f..0cc2245eae1 100644
--- a/lib/vs_code/settings.rb
+++ b/lib/vs_code/settings.rb
@@ -15,7 +15,7 @@ module VsCode
}
]
}.freeze
- SETTINGS_TYPES = %w[settings extensions globalState machines keybindings snippets tasks].freeze
+ SETTINGS_TYPES = %w[settings extensions globalState machines keybindings snippets tasks profiles].freeze
DEFAULT_SESSION = "1"
NO_CONTENT_ETAG = "0"
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index c45287f3f90..f95890d9ccf 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -237,6 +237,11 @@ msgid_plural "%d completed issues"
msgstr[0] ""
msgstr[1] ""
+msgid "%d compliance framework selected"
+msgid_plural "%d compliance frameworks selected"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%d contribution"
msgid_plural "%d contributions"
msgstr[0] ""
@@ -741,9 +746,6 @@ msgstr ""
msgid "%{emailPrefix}@company.com"
msgstr ""
-msgid "%{enable_service_ping_link_start}Enable%{enable_service_ping_link_end} or %{generate_manually_link_start}generate%{generate_manually_link_end} Service Ping to preview and download service usage data payload."
-msgstr ""
-
msgid "%{extra} more downstream pipelines"
msgstr ""
@@ -795,12 +797,6 @@ msgstr ""
msgid "%{integrations_link_start}Integrations%{link_end} enable you to make third-party applications part of your GitLab workflow. If the available integrations don't meet your needs, consider using a %{webhooks_link_start}webhook%{link_end}."
msgstr ""
-msgid "%{issuableDisplayName} locked."
-msgstr ""
-
-msgid "%{issuableDisplayName} unlocked."
-msgstr ""
-
msgid "%{issuableType} will be removed! Are you sure?"
msgstr ""
@@ -933,9 +929,6 @@ msgstr ""
msgid "%{lock_path} is locked by GitLab User %{lock_user_id}"
msgstr ""
-msgid "%{locked} created %{timeago}"
-msgstr ""
-
msgid "%{mergeLength}/%{usersLength} can merge"
msgstr ""
@@ -1228,6 +1221,9 @@ msgid_plural "%{strong_start}%{errors}%{strong_end} %{prefix} findings"
msgstr[0] ""
msgstr[1] ""
+msgid "%{strong_start}%{human_size}%{strong_end} Forked Project"
+msgstr ""
+
msgid "%{strong_start}%{human_size}%{strong_end} Project Storage"
msgstr ""
@@ -1378,6 +1374,12 @@ msgstr ""
msgid "'%{value}' days of inactivity must be greater than or equal to 90"
msgstr ""
+msgid "'allow: %{allow}' must be a string"
+msgstr ""
+
+msgid "'except: %{except}' must be an array of string"
+msgstr ""
+
msgid "'projects' is not yet supported"
msgstr ""
@@ -1768,6 +1770,9 @@ msgstr ""
msgid "A Work Item can be a parent or a child, but not both."
msgstr ""
+msgid "A basic folder structure of Astro Starter Kit, to help you get started."
+msgstr ""
+
msgid "A basic folder structure of a Laravel application, to help you get started."
msgstr ""
@@ -1816,6 +1821,9 @@ msgstr ""
msgid "A management, operational, or technical control (that is, safeguard or countermeasure) employed by an organization that provides equivalent or comparable protection for an information system."
msgstr ""
+msgid "A maximum of %{limit} projects can be searched for at one time."
+msgstr ""
+
msgid "A member of the abuse team will review your report as soon as possible."
msgstr ""
@@ -1912,9 +1920,6 @@ msgstr ""
msgid "AISummary|View summary"
msgstr ""
-msgid "AI| %{link_start}How is my data used?%{link_end}"
-msgstr ""
-
msgid "AI|%{tool} is %{transition} an answer"
msgstr ""
@@ -1957,9 +1962,6 @@ msgstr ""
msgid "AI|Explain your rating to help us improve! (optional)"
msgstr ""
-msgid "AI|Features that use third-party AI services require transmission of data, including personal data."
-msgstr ""
-
msgid "AI|For example: Organizations should be able to forecast into the future by using value stream analytics charts. This feature would help them understand how their metrics are trending."
msgstr ""
@@ -2034,18 +2036,12 @@ msgstr ""
msgid "AI|There is too much text in the chat. Please try again with a shorter text."
msgstr ""
-msgid "AI|Third-party AI services"
-msgstr ""
-
msgid "AI|To help improve the quality of the content, send your feedback to GitLab team members."
msgstr ""
msgid "AI|Unhelpful"
msgstr ""
-msgid "AI|Use third-party AI services"
-msgstr ""
-
msgid "AI|What does the selected code mean?"
msgstr ""
@@ -3051,9 +3047,6 @@ msgstr ""
msgid "Add topics to projects to help users find them."
msgstr ""
-msgid "Add variable"
-msgstr ""
-
msgid "Add vulnerability finding"
msgstr ""
@@ -3417,6 +3410,9 @@ msgstr ""
msgid "AdminProjects|Delete Project %{projectName}?"
msgstr ""
+msgid "AdminSettings|%{generate_manually_link_start}Generate%{generate_manually_link_end} Service Ping to preview and download service usage data payload."
+msgstr ""
+
msgid "AdminSettings|%{setting_name} value used by both Rails and Browser JavaScript SDKs."
msgstr ""
@@ -3495,6 +3491,9 @@ msgstr ""
msgid "AdminSettings|Domain verification is an essential security measure for public GitLab sites. Users are required to demonstrate they control a domain before it is enabled. %{link_start}Learn more.%{link_end}"
msgstr ""
+msgid "AdminSettings|Download payload"
+msgstr ""
+
msgid "AdminSettings|Elasticsearch indexing"
msgstr ""
@@ -3531,6 +3530,9 @@ msgstr ""
msgid "AdminSettings|Enable smartcn custom analyzer: Search"
msgstr ""
+msgid "AdminSettings|Enable the external redirect warning page for job artifacts"
+msgstr ""
+
msgid "AdminSettings|Enabled"
msgstr ""
@@ -3693,6 +3695,9 @@ msgstr ""
msgid "AdminSettings|Send warning email"
msgstr ""
+msgid "AdminSettings|Service Ping payload not found in the application cache"
+msgstr ""
+
msgid "AdminSettings|Service ping is disabled in your configuration file, and cannot be enabled through this form. For more information, see the documentation on %{link_start}deactivating service ping%{link_end}."
msgstr ""
@@ -3729,6 +3734,9 @@ msgstr ""
msgid "AdminSettings|Setting must be greater than 0."
msgstr ""
+msgid "AdminSettings|Show a redirect page that warns you about user-generated content in GitLab Pages."
+msgstr ""
+
msgid "AdminSettings|Size and domain settings for Pages static sites."
msgstr ""
@@ -4053,6 +4061,9 @@ msgstr ""
msgid "AdminUsers|Projects, issues, merge requests, and comments of this user are hidden from other users."
msgstr ""
+msgid "AdminUsers|Re-enable spam monitoring for %{username}?"
+msgstr ""
+
msgid "AdminUsers|Reactivating a user will:"
msgstr ""
@@ -4092,9 +4103,15 @@ msgstr ""
msgid "AdminUsers|Sort by"
msgstr ""
+msgid "AdminUsers|Stop monitoring %{username} for possible spam?"
+msgstr ""
+
msgid "AdminUsers|The maximum compute minutes that jobs in this namespace can use on shared runners each month. Set 0 for unlimited. Set empty to inherit the global setting of %{minutes}"
msgstr ""
+msgid "AdminUsers|The user can create issues, notes, snippets, and merge requests that appear to be spam without being blocked."
+msgstr ""
+
msgid "AdminUsers|The user can't access git repositories."
msgstr ""
@@ -4125,6 +4142,12 @@ msgstr ""
msgid "AdminUsers|To confirm, type %{username}."
msgstr ""
+msgid "AdminUsers|Trust user"
+msgstr ""
+
+msgid "AdminUsers|Trusted"
+msgstr ""
+
msgid "AdminUsers|Unban user"
msgstr ""
@@ -4140,6 +4163,9 @@ msgstr ""
msgid "AdminUsers|Unlock user %{username}?"
msgstr ""
+msgid "AdminUsers|Untrust user"
+msgstr ""
+
msgid "AdminUsers|User administration"
msgstr ""
@@ -4173,6 +4199,9 @@ msgstr ""
msgid "AdminUsers|When banned:"
msgstr ""
+msgid "AdminUsers|When not being monitored for spam:"
+msgstr ""
+
msgid "AdminUsers|When the user logs back in, their account will reactivate as a fully active account"
msgstr ""
@@ -4203,9 +4232,15 @@ msgstr ""
msgid "AdminUsers|You can ban their account in the future if necessary."
msgstr ""
+msgid "AdminUsers|You can trust this user in the future if necessary."
+msgstr ""
+
msgid "AdminUsers|You can unban their account in the future. Their data remains intact."
msgstr ""
+msgid "AdminUsers|You can untrust this user in the future."
+msgstr ""
+
msgid "AdminUsers|You cannot remove your own administrator access."
msgstr ""
@@ -4761,6 +4796,12 @@ msgstr ""
msgid "All environments"
msgstr ""
+msgid "All frameworks selected"
+msgstr ""
+
+msgid "All groups"
+msgstr ""
+
msgid "All groups and projects"
msgstr ""
@@ -4794,6 +4835,9 @@ msgstr ""
msgid "All protected branches"
msgstr ""
+msgid "All required approvals must be given."
+msgstr ""
+
msgid "All threads resolved!"
msgstr ""
@@ -5073,6 +5117,9 @@ msgstr ""
msgid "An error occurred while fetching codequality mr diff reports."
msgstr ""
+msgid "An error occurred while fetching comments, please try again."
+msgstr ""
+
msgid "An error occurred while fetching commit data."
msgstr ""
@@ -5151,6 +5198,9 @@ msgstr ""
msgid "An error occurred while fetching this tab."
msgstr ""
+msgid "An error occurred while fetching. Please try again."
+msgstr ""
+
msgid "An error occurred while getting files for - %{branchId}"
msgstr ""
@@ -5386,9 +5436,6 @@ msgstr ""
msgid "An unexpected error occurred while communicating with the Web Terminal."
msgstr ""
-msgid "An unexpected error occurred while loading the Sast diff."
-msgstr ""
-
msgid "An unexpected error occurred while loading the code quality diff."
msgstr ""
@@ -5497,6 +5544,12 @@ msgstr ""
msgid "Analytics|Custom events"
msgstr ""
+msgid "Analytics|Dashboard description"
+msgstr ""
+
+msgid "Analytics|Dashboard description (optional)"
+msgstr ""
+
msgid "Analytics|Dashboard not found"
msgstr ""
@@ -5527,6 +5580,12 @@ msgstr ""
msgid "Analytics|Edit your dashboard"
msgstr ""
+msgid "Analytics|Element ID"
+msgstr ""
+
+msgid "Analytics|Enter a dashboard description"
+msgstr ""
+
msgid "Analytics|Enter a dashboard title"
msgstr ""
@@ -5560,6 +5619,9 @@ msgstr ""
msgid "Analytics|Line chart"
msgstr ""
+msgid "Analytics|Link clicks"
+msgstr ""
+
msgid "Analytics|New dashboard"
msgstr ""
@@ -5635,6 +5697,9 @@ msgstr ""
msgid "Analytics|Tables"
msgstr ""
+msgid "Analytics|Target URL"
+msgstr ""
+
msgid "Analytics|To create your own dashboards, first configure a project to store your dashboards."
msgstr ""
@@ -6037,6 +6102,9 @@ msgstr ""
msgid "Approval options"
msgstr ""
+msgid "Approval rejected."
+msgstr ""
+
msgid "Approval rules"
msgstr ""
@@ -6198,7 +6266,7 @@ msgstr ""
msgid "ApprovalSettings|Remove approvals by Code Owners if their files changed"
msgstr ""
-msgid "ApprovalSettings|Require user password to approve"
+msgid "ApprovalSettings|Require user re-authentication (password or SAML) to approve"
msgstr ""
msgid "ApprovalSettings|There was an error loading merge request approval settings."
@@ -6375,6 +6443,9 @@ msgstr ""
msgid "Are you sure you want to close this blocked issue?"
msgstr ""
+msgid "Are you sure you want to continue?"
+msgstr ""
+
msgid "Are you sure you want to delete %{name}?"
msgstr ""
@@ -6831,12 +6902,27 @@ msgstr[1] ""
msgid "AuditStreams|A header with this name already exists."
msgstr ""
+msgid "AuditStreams|AKIA1231dsdsdsdsds23"
+msgstr ""
+
+msgid "AuditStreams|AWS Region"
+msgstr ""
+
+msgid "AuditStreams|AWS S3"
+msgstr ""
+
+msgid "AuditStreams|Access Key Xid"
+msgstr ""
+
msgid "AuditStreams|Active"
msgstr ""
msgid "AuditStreams|Add a new private key"
msgstr ""
+msgid "AuditStreams|Add a new secret access key"
+msgstr ""
+
msgid "AuditStreams|Add an HTTP endpoint to manage audit logs in third-party systems."
msgstr ""
@@ -6867,6 +6953,9 @@ msgstr ""
msgid "AuditStreams|Are you sure about deleting this destination?"
msgstr ""
+msgid "AuditStreams|Bucket Name"
+msgstr ""
+
msgid "AuditStreams|Cancel editing"
msgstr ""
@@ -6930,6 +7019,9 @@ msgstr ""
msgid "AuditStreams|Save external stream destination"
msgstr ""
+msgid "AuditStreams|Secret Access Key"
+msgstr ""
+
msgid "AuditStreams|Select events"
msgstr ""
@@ -6954,6 +7046,9 @@ msgstr ""
msgid "AuditStreams|This is great for keeping everything one place."
msgstr ""
+msgid "AuditStreams|Use the AWS console to view the secret access key. To change the secret access key, replace it with a new secret access key."
+msgstr ""
+
msgid "AuditStreams|Use the Google Cloud console to view the private key. To change the private key, replace it with a new private key."
msgstr ""
@@ -6966,6 +7061,9 @@ msgstr ""
msgid "AuditStreams|audit-events"
msgstr ""
+msgid "AuditStreams|bucket-name"
+msgstr ""
+
msgid "AuditStreams|ex: 1000"
msgstr ""
@@ -6981,6 +7079,9 @@ msgstr ""
msgid "AuditStreams|my-google-project"
msgstr ""
+msgid "AuditStreams|us-east-1"
+msgstr ""
+
msgid "Aug"
msgstr ""
@@ -8573,6 +8674,9 @@ msgstr ""
msgid "Branches"
msgstr ""
+msgid "Branches matching this string are retargeted. Wildcards are supported, and names are case-sensitive."
+msgstr ""
+
msgid "Branches: %{source_branch} to %{target_branch}"
msgstr ""
@@ -9104,6 +9208,9 @@ msgstr ""
msgid "CI/CD Analytics"
msgstr ""
+msgid "CI/CD Catalog"
+msgstr ""
+
msgid "CI/CD Settings"
msgstr ""
@@ -9496,6 +9603,9 @@ msgstr ""
msgid "Cannot merge"
msgstr ""
+msgid "Cannot merge the source into the target branch, due to a conflict."
+msgstr ""
+
msgid "Cannot modify %{profile_name} referenced in security policy"
msgstr ""
@@ -9706,6 +9816,9 @@ msgstr ""
msgid "Changes"
msgstr ""
+msgid "Changes requested to the current merge request."
+msgstr ""
+
msgid "Changes saved."
msgstr ""
@@ -9718,6 +9831,9 @@ msgstr ""
msgid "Changes to the title have not been saved"
msgstr ""
+msgid "Changes will not affect existing token expiration dates. %{link_start}How will this affect expiration dates?%{link_end}"
+msgstr ""
+
msgid "Changes:"
msgstr ""
@@ -10157,6 +10273,9 @@ msgstr ""
msgid "Ci config already present"
msgstr ""
+msgid "CiCatalogComponent|Component details not available"
+msgstr ""
+
msgid "CiCatalogComponent|Default Value"
msgstr ""
@@ -10172,15 +10291,18 @@ msgstr ""
msgid "CiCatalogComponent|There was an error fetching this resource's components"
msgstr ""
-msgid "CiCatalog|Back to the CI/CD Catalog"
+msgid "CiCatalogComponent|This tab displays auto-collected information about the components in the repository, but no information was found."
msgstr ""
-msgid "CiCatalog|CI/CD Catalog"
+msgid "CiCatalog|Back to the CI/CD Catalog"
msgstr ""
msgid "CiCatalog|CI/CD Catalog resource"
msgstr ""
+msgid "CiCatalog|CI/CD catalog resource"
+msgstr ""
+
msgid "CiCatalog|Component ID not found, or you do not have permission to access component."
msgstr ""
@@ -10190,6 +10312,9 @@ msgstr ""
msgid "CiCatalog|Create a pipeline component repository and make reusing pipeline configurations faster and easier."
msgstr ""
+msgid "CiCatalog|Discover CI configuration resources for a seamless CI/CD experience."
+msgstr ""
+
msgid "CiCatalog|Get started with the CI/CD Catalog"
msgstr ""
@@ -10283,6 +10408,9 @@ msgstr ""
msgid "CiStatusLabel|skipped"
msgstr ""
+msgid "CiStatusLabel|waiting for callback"
+msgstr ""
+
msgid "CiStatusLabel|waiting for delayed job"
msgstr ""
@@ -10723,6 +10851,9 @@ msgstr ""
msgid "Closing %{issuableType}..."
msgstr ""
+msgid "Closing %{workItemType}"
+msgstr ""
+
msgid "Cloud Run"
msgstr ""
@@ -11766,7 +11897,7 @@ msgstr ""
msgid "CodeSuggestions|Projects in this group can use Code Suggestions"
msgstr ""
-msgid "CodeSuggestions|Subject to the %{terms_link_start}Testing Terms of Use%{link_end}. Code Suggestions currently uses third-party AI services unless those are %{third_party_features_link_start}disabled%{link_end}."
+msgid "CodeSuggestions|Subject to the %{terms_link_start}Testing Terms of Use%{link_end}. Code Suggestions uses third-party AI services."
msgstr ""
msgid "CodeownersValidation|An error occurred while loading the validation errors. Please try again later."
@@ -12109,6 +12240,9 @@ msgstr ""
msgid "Compare %{oldCommitId}...%{newCommitId}"
msgstr ""
+msgid "Compare Branches"
+msgstr ""
+
msgid "Compare GitLab editions"
msgstr ""
@@ -12238,6 +12372,18 @@ msgstr ""
msgid "Compliance framework"
msgstr ""
+msgid "ComplianceFrameworksReport|Associated Projects"
+msgstr ""
+
+msgid "ComplianceFrameworksReport|Default"
+msgstr ""
+
+msgid "ComplianceFrameworksReport|Description"
+msgstr ""
+
+msgid "ComplianceFrameworksReport|Edit framework"
+msgstr ""
+
msgid "ComplianceFrameworks|Active compliance frameworks"
msgstr ""
@@ -12433,9 +12579,6 @@ msgstr ""
msgid "ComplianceReport|Remove framework from selected projects"
msgstr ""
-msgid "ComplianceReport|Retrieving the compliance framework report failed. Refresh the page and try again."
-msgstr ""
-
msgid "ComplianceReport|Search target branch"
msgstr ""
@@ -12469,6 +12612,9 @@ msgstr ""
msgid "ComplianceStandardsAdherence|At least two approvals"
msgstr ""
+msgid "ComplianceStandardsAdherence|Check"
+msgstr ""
+
msgid "ComplianceStandardsAdherence|Failure reason"
msgstr ""
@@ -12484,9 +12630,15 @@ msgstr ""
msgid "ComplianceStandardsAdherence|How to fix"
msgstr ""
+msgid "ComplianceStandardsAdherence|Last Scanned"
+msgstr ""
+
msgid "ComplianceStandardsAdherence|Merge request approval rules"
msgstr ""
+msgid "ComplianceStandardsAdherence|More Information"
+msgstr ""
+
msgid "ComplianceStandardsAdherence|No projects with standards adherence checks found"
msgstr ""
@@ -12505,9 +12657,18 @@ msgstr ""
msgid "ComplianceStandardsAdherence|Prevent committers as approvers"
msgstr ""
+msgid "ComplianceStandardsAdherence|Project"
+msgstr ""
+
msgid "ComplianceStandardsAdherence|Requirement"
msgstr ""
+msgid "ComplianceStandardsAdherence|Standard"
+msgstr ""
+
+msgid "ComplianceStandardsAdherence|Status"
+msgstr ""
+
msgid "ComplianceStandardsAdherence|Success reason"
msgstr ""
@@ -12523,6 +12684,9 @@ msgstr ""
msgid "ComplianceStandardsAdherence|View details"
msgstr ""
+msgid "ComplianceStandardsAdherence|View details (fix available)"
+msgstr ""
+
msgid "ComplianceViolations|Compliance Violations Export"
msgstr ""
@@ -13594,6 +13758,12 @@ msgstr ""
msgid "Copy audio URL"
msgstr ""
+msgid "Copy autocomplete description"
+msgstr ""
+
+msgid "Copy autocomplete usage hint"
+msgstr ""
+
msgid "Copy branch name"
msgstr ""
@@ -13612,6 +13782,12 @@ msgstr ""
msgid "Copy commit SHA"
msgstr ""
+msgid "Copy customize name"
+msgstr ""
+
+msgid "Copy descriptive label"
+msgstr ""
+
msgid "Copy diagram URL"
msgstr ""
@@ -13747,6 +13923,9 @@ msgstr ""
msgid "CorpusManagement|Total Size: %{totalSize}"
msgstr ""
+msgid "Correlation ID"
+msgstr ""
+
msgid "Cost Factor Settings"
msgstr ""
@@ -13999,6 +14178,9 @@ msgstr ""
msgid "Create group label"
msgstr ""
+msgid "Create identity verification exemption"
+msgstr ""
+
msgid "Create issue"
msgstr ""
@@ -14062,9 +14244,6 @@ msgstr ""
msgid "Create or import your first project"
msgstr ""
-msgid "Create phone verification exemption"
-msgstr ""
-
msgid "Create pipeline trigger token"
msgstr ""
@@ -14383,6 +14562,9 @@ msgstr ""
msgid "Creating epic"
msgstr ""
+msgid "Creation of member role is allowed only for root groups"
+msgstr ""
+
msgid "Creator"
msgstr ""
@@ -14407,6 +14589,9 @@ msgstr ""
msgid "Credit card required to be on file in order to create a pipeline"
msgstr ""
+msgid "Credit card validation record saved"
+msgstr ""
+
msgid "Credit card:"
msgstr ""
@@ -14852,9 +15037,6 @@ msgstr ""
msgid "DORA4Metrics|All labels"
msgstr ""
-msgid "DORA4Metrics|Analytics Dashboards"
-msgstr ""
-
msgid "DORA4Metrics|Average (last %{days}d)"
msgstr ""
@@ -14873,9 +15055,6 @@ msgstr ""
msgid "DORA4Metrics|Change failure rate (percentage)"
msgstr ""
-msgid "DORA4Metrics|Closed issues"
-msgstr ""
-
msgid "DORA4Metrics|Critical Vulnerabilities over time"
msgstr ""
@@ -14947,6 +15126,12 @@ msgstr ""
msgid "DORA4Metrics|High Vulnerabilities over time"
msgstr ""
+msgid "DORA4Metrics|Issues closed"
+msgstr ""
+
+msgid "DORA4Metrics|Issues created"
+msgstr ""
+
msgid "DORA4Metrics|Lead Time for Changes"
msgstr ""
@@ -15001,9 +15186,6 @@ msgstr ""
msgid "DORA4Metrics|Month to date"
msgstr ""
-msgid "DORA4Metrics|New issues"
-msgstr ""
-
msgid "DORA4Metrics|No data available for Namespace: %{fullPath}"
msgstr ""
@@ -15103,10 +15285,10 @@ msgstr ""
msgid "DORA4Metrics|Took more than 7 days to restore service when a service incident or a defect that impacts users occurs."
msgstr ""
-msgid "DORA4Metrics|Total projects (%{count}) by DORA performers score for %{groupName} group"
+msgid "DORA4Metrics|Total projects (%{count}) with DORA performers score for %{groupName} group"
msgstr ""
-msgid "DORA4Metrics|Total projects by DORA performers score"
+msgid "DORA4Metrics|Total projects with DORA performers score"
msgstr ""
msgid "DORA4Metrics|Value Streams Dashboard"
@@ -15984,10 +16166,13 @@ msgstr ""
msgid "Delete user list"
msgstr ""
-msgid "Delete variable"
+msgid "Delete video"
msgstr ""
-msgid "Delete video"
+msgid "DeleteProject|Couldn't remove the project. A project repository storage move is in progress. Try again when it's complete."
+msgstr ""
+
+msgid "DeleteProject|Couldn't remove the project. A related snippet repository storage move is in progress. Try again when it's complete."
msgstr ""
msgid "DeleteProject|Failed to remove design repository. Please try again or contact administrator."
@@ -16085,6 +16270,9 @@ msgstr ""
msgid "Denied authorization of chat nickname %{user_name}."
msgstr ""
+msgid "Denied licenses must be removed or approved."
+msgstr ""
+
msgid "Deny"
msgstr ""
@@ -16750,27 +16938,6 @@ msgstr ""
msgid "Deployment|Waiting"
msgstr ""
-msgid "Deployment|blocked"
-msgstr ""
-
-msgid "Deployment|canceled"
-msgstr ""
-
-msgid "Deployment|created"
-msgstr ""
-
-msgid "Deployment|failed"
-msgstr ""
-
-msgid "Deployment|running"
-msgstr ""
-
-msgid "Deployment|skipped"
-msgstr ""
-
-msgid "Deployment|success"
-msgstr ""
-
msgid "Deprecated API rate limits"
msgstr ""
@@ -17391,9 +17558,15 @@ msgstr ""
msgid "Discuss a specific suggestion or question."
msgstr ""
+msgid "Discussion locked."
+msgstr ""
+
msgid "Discussion to reply to cannot be found"
msgstr ""
+msgid "Discussion unlocked."
+msgstr ""
+
msgid "Disk Usage"
msgstr ""
@@ -17434,6 +17607,9 @@ msgstr ""
msgid "Display as:"
msgstr ""
+msgid "Display blame info"
+msgstr ""
+
msgid "Display milestones"
msgstr ""
@@ -17605,9 +17781,6 @@ msgstr ""
msgid "Download image"
msgstr ""
-msgid "Download payload"
-msgstr ""
-
msgid "Download raw data (.csv)"
msgstr ""
@@ -18325,6 +18498,9 @@ msgstr ""
msgid "Enforce two-factor authentication for all user sign-ins."
msgstr ""
+msgid "Engine"
+msgstr ""
+
msgid "Enhance security by storing service account keys in secret managers - learn more about %{docLinkStart}secret management with GitLab%{docLinkEnd}"
msgstr ""
@@ -18400,6 +18576,9 @@ msgstr ""
msgid "Enter the username for password-protected Elasticsearch servers."
msgstr ""
+msgid "Enter values to populate the .gitlab-ci.yml configuration file."
+msgstr ""
+
msgid "Enter verification code"
msgstr ""
@@ -18946,6 +19125,9 @@ msgstr ""
msgid "Error creating vulnerability finding: %{errors}"
msgstr ""
+msgid "Error deleting the value stream"
+msgstr ""
+
msgid "Error fetching branches"
msgstr ""
@@ -19051,6 +19233,9 @@ msgstr ""
msgid "Error occurred. User was not unlocked"
msgstr ""
+msgid "Error occurred. User was not updated"
+msgstr ""
+
msgid "Error parsing CSV file. Please make sure it has"
msgstr ""
@@ -19060,6 +19245,9 @@ msgstr ""
msgid "Error rendering Markdown preview"
msgstr ""
+msgid "Error saving credit card validation record"
+msgstr ""
+
msgid "Error saving label update."
msgstr ""
@@ -19533,6 +19721,11 @@ msgstr ""
msgid "Exceptions"
msgstr ""
+msgid "Excluding 1 project with no DORA metrics"
+msgid_plural "Excluding %d projects with no DORA metrics"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "Excluding USB security keys, you should include the browser name together with the device name."
msgstr ""
@@ -19599,9 +19792,6 @@ msgstr ""
msgid "Expand sidebar"
msgstr ""
-msgid "Expand variable reference"
-msgstr ""
-
msgid "Expected documents: %{expected_documents}"
msgstr ""
@@ -19719,9 +19909,6 @@ msgstr ""
msgid "Export this project with all its related data in order to move it to a new GitLab instance. When the exported file is ready, you can download it from this page or from the download link in the email notification you will receive. You can then import it when creating a new project. %{link_start}Learn more.%{link_end}"
msgstr ""
-msgid "Export variable to pipelines running on protected branches and tags only."
-msgstr ""
-
msgid "Exported requirements"
msgstr ""
@@ -19918,6 +20105,9 @@ msgstr ""
msgid "Failed to create import label for jira import."
msgstr ""
+msgid "Failed to create organization"
+msgstr ""
+
msgid "Failed to create repository"
msgstr ""
@@ -20541,16 +20731,16 @@ msgstr ""
msgid "FindFile|Switch branch/tag"
msgstr ""
-msgid "FindingsDrawer|Category:"
+msgid "FindingsDrawer|Code Quality"
msgstr ""
-msgid "FindingsDrawer|Engine:"
+msgid "FindingsDrawer|Code Quality Finding"
msgstr ""
-msgid "FindingsDrawer|Other locations:"
+msgid "FindingsDrawer|Detected in pipeline"
msgstr ""
-msgid "FindingsDrawer|Severity:"
+msgid "FindingsDrawer|SAST Finding"
msgstr ""
msgid "Fingerprint (MD5)"
@@ -20715,9 +20905,6 @@ msgstr ""
msgid "For the GitLab Team to keep your subscription data up to date, this is a reminder to report your license usage on a monthly basis, or at the cadence set in your agreement with GitLab. This allows us to simplify the billing process for overages and renewals. To report your usage data, export your license usage file and email it to %{renewal_service_email}. If you need an updated license, GitLab will send the license to the email address registered in the %{customers_dot}, and you can upload this license to your instance."
msgstr ""
-msgid "For the next few releases, you can go to your avatar at any time to turn the new navigation on and off."
-msgstr ""
-
msgid "Forbidden"
msgstr ""
@@ -20742,9 +20929,15 @@ msgstr ""
msgid "ForkProject|A fork is a copy of a project."
msgstr ""
+msgid "ForkProject|All branches"
+msgstr ""
+
msgid "ForkProject|An error occurred while forking the project. Please try again."
msgstr ""
+msgid "ForkProject|Branches to include"
+msgstr ""
+
msgid "ForkProject|Cancel"
msgstr ""
@@ -20760,6 +20953,9 @@ msgstr ""
msgid "ForkProject|Internal"
msgstr ""
+msgid "ForkProject|Only the default branch %{defaultBranch}"
+msgstr ""
+
msgid "ForkProject|Please select a namespace"
msgstr ""
@@ -20885,6 +21081,36 @@ msgstr ""
msgid "Free trial will expire in %{days}"
msgstr ""
+msgid "FreeUserCap|Action required: %{namespace_name} group has been placed into a read-only state"
+msgstr ""
+
+msgid "FreeUserCap|Explore paid plans"
+msgstr ""
+
+msgid "FreeUserCap|Explore paid plans:"
+msgstr ""
+
+msgid "FreeUserCap|Manage members"
+msgstr ""
+
+msgid "FreeUserCap|Manage members:"
+msgstr ""
+
+msgid "FreeUserCap|Start a trial:"
+msgstr ""
+
+msgid "FreeUserCap|To remove the %{link_start}read-only%{link_end} state and regain write access, you can reduce the number of users in your top-level group to %{free_user_limit} users or less. You can also %{upgrade_start}upgrade%{upgrade_end} to a paid tier, which do not have user limits. If you need additional time, you can %{trial_start}start a free 30-day trial%{trial_end} which includes unlimited users."
+msgstr ""
+
+msgid "FreeUserCap|Upgrade:"
+msgstr ""
+
+msgid "FreeUserCap|You have exceeded your limit of %{free_user_limit} users for %{namespace_name} group because users were added to a group inherited by a group or project in the %{namespace_name} group."
+msgstr ""
+
+msgid "FreeUserCap|You've exceeded your user limit"
+msgstr ""
+
msgid "Freeze end"
msgstr ""
@@ -21159,9 +21385,6 @@ msgstr ""
msgid "Geo|Filter by name"
msgstr ""
-msgid "Geo|Filter by status"
-msgstr ""
-
msgid "Geo|Geo Settings"
msgstr ""
@@ -21291,15 +21514,9 @@ msgstr ""
msgid "Geo|Remove %{siteType} site"
msgstr ""
-msgid "Geo|Remove entry"
-msgstr ""
-
msgid "Geo|Remove site"
msgstr ""
-msgid "Geo|Remove tracking database entry"
-msgstr ""
-
msgid "Geo|Removing a Geo site stops the synchronization to and from that site. Are you sure?"
msgstr ""
@@ -21471,9 +21688,6 @@ msgstr ""
msgid "Geo|Time in seconds"
msgstr ""
-msgid "Geo|Tracking database entry will be removed. Are you sure?"
-msgstr ""
-
msgid "Geo|Tuning settings"
msgstr ""
@@ -21483,9 +21697,6 @@ msgstr ""
msgid "Geo|URL must be a valid url (ex: https://gitlab.com)"
msgstr ""
-msgid "Geo|Undefined"
-msgstr ""
-
msgid "Geo|Unhealthy"
msgstr ""
@@ -21669,6 +21880,9 @@ msgstr ""
msgid "GitLab Community Edition"
msgstr ""
+msgid "GitLab Duo didn't respond. Try again? If it fails again, your request might be too large."
+msgstr ""
+
msgid "GitLab Enterprise Edition"
msgstr ""
@@ -21726,6 +21940,9 @@ msgstr ""
msgid "GitLab group: %{source_link}"
msgstr ""
+msgid "GitLab has redesigned the left sidebar to address customer feedback. View details in %{blog_link_start}this blog post%{link_end}. Here's how to %{issues_link_start}file an issue%{link_end} with the GitLab product team."
+msgstr ""
+
msgid "GitLab informs you if a new version is available. %{link_start}What information does GitLab Inc. collect?%{link_end}"
msgstr ""
@@ -21897,7 +22114,7 @@ msgstr ""
msgid "GitLabPages|Your Pages site is not configured yet. See the %{docs_link_start}GitLab Pages documentation%{link_end} to learn how to upload your static site and have GitLab serve it. You can also take some inspiration from the %{samples_link_start}sample Pages projects%{link_end}."
msgstr ""
-msgid "GitLabPages|Your Project has been configured for Pages. Now we have to wait for the Pipeline to succeed for the first time."
+msgid "GitLabPages|Your project is configured for GitLab Pages and the pipeline is running..."
msgstr ""
msgid "Gitaly Servers"
@@ -23115,6 +23332,12 @@ msgstr ""
msgid "GroupSettings|Select the project containing your custom Insights file."
msgstr ""
+msgid "GroupSettings|Service access tokens expiration enforced setting was not saved"
+msgstr ""
+
+msgid "GroupSettings|Service account token expiration"
+msgstr ""
+
msgid "GroupSettings|Set a size limit for all content in each Pages site in this group. %{link_start}Learn more.%{link_end}"
msgstr ""
@@ -23910,6 +24133,9 @@ msgstr ""
msgid "I accept the %{terms_link}"
msgstr ""
+msgid "I am sorry, I am unable to find what you are looking for."
+msgstr ""
+
msgid "I forgot my password"
msgstr ""
@@ -23985,6 +24211,12 @@ msgstr ""
msgid "INFO: Your SSH key is expiring soon. Please generate a new key."
msgstr ""
+msgid "IP '%{value}' is not a valid CIDR: %{message}"
+msgstr ""
+
+msgid "IP '%{value}' is not a valid CIDR: IP should be followed by a slash followed by an integer subnet mask (for example: '192.168.1.0/24')"
+msgstr ""
+
msgid "IP Address"
msgstr ""
@@ -24015,6 +24247,15 @@ msgstr ""
msgid "Identities"
msgstr ""
+msgid "Identity verification exemption"
+msgstr ""
+
+msgid "Identity verification exemption has been created."
+msgstr ""
+
+msgid "Identity verification exemption has been removed."
+msgstr ""
+
msgid "IdentityVerification|%d country found"
msgid_plural "IdentityVerification|%d countries found"
msgstr[0] ""
@@ -24593,6 +24834,9 @@ msgstr ""
msgid "Import|GitHub import details"
msgstr ""
+msgid "Import|GitLab Migration details"
+msgstr ""
+
msgid "Import|Maximum decompressed file size for archives from imports (MiB)"
msgstr ""
@@ -24668,12 +24912,21 @@ msgstr ""
msgid "InProductMarketing|%{strong_start}GitLab Inc.%{strong_end} 268 Bush Street, #350, San Francisco, CA 94104, USA"
msgstr ""
+msgid "InProductMarketing|%{upper_start}Start your 30-day free trial of%{upper_end} %{lower_start}GitLab Ultimate%{lower_end}"
+msgstr ""
+
+msgid "InProductMarketing|Accelerate your digital transform"
+msgstr ""
+
msgid "InProductMarketing|Blog"
msgstr ""
msgid "InProductMarketing|Built-in security"
msgstr ""
+msgid "InProductMarketing|Deliver software faster"
+msgstr ""
+
msgid "InProductMarketing|Ensure compliance"
msgstr ""
@@ -24689,12 +24942,18 @@ msgstr ""
msgid "InProductMarketing|If you no longer wish to receive marketing emails from us,"
msgstr ""
+msgid "InProductMarketing|Improve collaboration and visibility"
+msgstr ""
+
msgid "InProductMarketing|Invite unlimited colleagues"
msgstr ""
msgid "InProductMarketing|No credit card required"
msgstr ""
+msgid "InProductMarketing|No credit card required."
+msgstr ""
+
msgid "InProductMarketing|Start a Self-Managed trial"
msgstr ""
@@ -25727,6 +25986,9 @@ msgstr ""
msgid "InviteMembersModal|Add unlimited members with your trial"
msgstr ""
+msgid "InviteMembersModal|Administrators can %{linkStart}add new users by email manually%{linkEnd}. After they've been added, you can invite them to this group with their username."
+msgstr ""
+
msgid "InviteMembersModal|Cancel"
msgstr ""
@@ -25760,6 +26022,9 @@ msgstr ""
msgid "InviteMembersModal|Inviting a group %{linkStart}adds its members to your project%{linkEnd}, including members who join after the invite. This might put your group over the free %{count} user limit."
msgstr ""
+msgid "InviteMembersModal|Inviting users by email is disabled"
+msgstr ""
+
msgid "InviteMembersModal|Manage members"
msgstr ""
@@ -25781,6 +26046,9 @@ msgstr ""
msgid "InviteMembersModal|Select a role"
msgstr ""
+msgid "InviteMembersModal|Select members"
+msgstr ""
+
msgid "InviteMembersModal|Select members or type email addresses"
msgstr ""
@@ -25807,6 +26075,9 @@ msgstr ""
msgid "InviteMembersModal|To invite new users to this top-level group, you must remove existing users. You can still add existing users from the top-level group, including any subgroups and projects."
msgstr ""
+msgid "InviteMembersModal|Username"
+msgstr ""
+
msgid "InviteMembersModal|Username or email address"
msgstr ""
@@ -26625,7 +26896,7 @@ msgstr ""
msgid "JiraConnect|Groups are the GitLab groups and subgroups you link to this Jira instance."
msgstr ""
-msgid "JiraConnect|In order to complete the set up, you’ll need to complete a few steps in GitLab."
+msgid "JiraConnect|In order to complete the set up, you’ll need to complete a few steps in GitLab:"
msgstr ""
msgid "JiraConnect|Jira Connect Application ID"
@@ -26655,6 +26926,15 @@ msgstr ""
msgid "JiraConnect|Not seeing your groups? Only groups you have at least the Maintainer role for appear here."
msgstr ""
+msgid "JiraConnect|Prerequisites"
+msgstr ""
+
+msgid "JiraConnect|Set up OAuth authentication"
+msgstr ""
+
+msgid "JiraConnect|Set up your instance"
+msgstr ""
+
msgid "JiraConnect|Setting up this integration is only possible if you're a GitLab administrator."
msgstr ""
@@ -26673,7 +26953,7 @@ msgstr ""
msgid "JiraConnect|Tell us what you think!"
msgstr ""
-msgid "JiraConnect|The Jira user is not a site administrator. Check the permissions in Jira and try again."
+msgid "JiraConnect|The Jira user is not a site or organization administrator. Check the permissions in Jira and try again."
msgstr ""
msgid "JiraConnect|We would love to learn more about your experience with the GitLab for Jira Cloud App."
@@ -26757,7 +27037,7 @@ msgstr ""
msgid "JiraService|Basic"
msgstr ""
-msgid "JiraService|Define the type of Jira issue to create from a vulnerability."
+msgid "JiraService|Create Jira issues of this type from vulnerabilities."
msgstr ""
msgid "JiraService|Displaying Jira issues while leaving GitLab issues also enabled might be confusing. Consider %{gitlab_issues_link_start}disabling GitLab issues%{link_end} if they won't otherwise be used."
@@ -26820,6 +27100,9 @@ msgstr ""
msgid "JiraService|Jira issue regex"
msgstr ""
+msgid "JiraService|Jira issue type"
+msgstr ""
+
msgid "JiraService|Jira issues"
msgstr ""
@@ -27288,7 +27571,7 @@ msgstr ""
msgid "Job|manual"
msgstr ""
-msgid "Job|triggered"
+msgid "Job|trigger token"
msgstr ""
msgid "Join GitLab today! You and your team can plan, build, and ship secure code all in one application. Get started here for free!"
@@ -27769,6 +28052,11 @@ msgstr ""
msgid "Learn more: %{url}"
msgstr ""
+msgid "LearnGitLab|%d task to go"
+msgid_plural "LearnGitLab|%d tasks to go"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "LearnGitLab|%{percentage}%{percentSymbol} completed"
msgstr ""
@@ -27877,6 +28165,9 @@ msgstr ""
msgid "LearnGitLab|Use your new GitLab workflow to deploy your application, monitor its health, and keep it secure:"
msgstr ""
+msgid "LearnGitLab|You completed all tasks!"
+msgstr ""
+
msgid "LearnGitLab|Your team is growing! You've successfully invited new team members to the %{projectName} project."
msgstr ""
@@ -28353,15 +28644,15 @@ msgstr ""
msgid "Lock"
msgstr ""
-msgid "Lock %{issuableDisplayName}"
-msgstr ""
-
msgid "Lock %{issuableType}"
msgstr ""
msgid "Lock File?"
msgstr ""
+msgid "Lock discussion"
+msgstr ""
+
msgid "Lock label after a merge request is merged"
msgstr ""
@@ -28383,7 +28674,7 @@ msgstr ""
msgid "Lock the discussion"
msgstr ""
-msgid "Lock this %{issuableDisplayName}? Only %{strongStart}project members%{strongEnd} will be able to comment."
+msgid "Lock this discussion? Only %{strongStart}project members%{strongEnd} will be able to comment."
msgstr ""
msgid "Lock to current projects"
@@ -28401,7 +28692,7 @@ msgstr ""
msgid "Locked the discussion."
msgstr ""
-msgid "Locking %{issuableDisplayName}"
+msgid "Locking discussion"
msgstr ""
msgid "Locks the discussion."
@@ -28770,12 +29061,6 @@ msgstr ""
msgid "Marks to do as done."
msgstr ""
-msgid "Mask this variable in job logs if it meets %{linkStart}regular expression requirements%{linkEnd}."
-msgstr ""
-
-msgid "Mask variable"
-msgstr ""
-
msgid "Match not found; try refining your search query."
msgstr ""
@@ -28986,10 +29271,10 @@ msgstr ""
msgid "Maximum number of %{name} (%{count}) exceeded"
msgstr ""
-msgid "Maximum number of changes (branches or tags) in a single push above which a bulk push event is created (default is `3`). Setting to `0` does not disable throttling."
+msgid "Maximum number of changes (branches or tags) in a single push above which a bulk push event is created (default is 3). Setting to 0 does not disable throttling."
msgstr ""
-msgid "Maximum number of changes (branches or tags) in a single push above which webhooks and integrations are not triggered (default is `3`). Setting to `0` does not disable throttling."
+msgid "Maximum number of changes (branches or tags) in a single push above which webhooks and integrations are not triggered (default is 3). Setting to 0 does not disable throttling."
msgstr ""
msgid "Maximum number of comments exceeded"
@@ -29010,7 +29295,7 @@ msgstr ""
msgid "Maximum number of requests per minute for an unauthenticated IP address"
msgstr ""
-msgid "Maximum number of requests per minute for each raw path (default is `300`). Set to `0` to disable throttling."
+msgid "Maximum number of requests per minute for each raw path (default is 300). Set to 0 to disable throttling."
msgstr ""
msgid "Maximum number of stages per value stream exceeded"
@@ -29121,6 +29406,9 @@ msgstr ""
msgid "Medium vulnerabilities present"
msgstr ""
+msgid "Member"
+msgstr ""
+
msgid "Member since"
msgstr ""
@@ -29142,21 +29430,6 @@ msgstr ""
msgid "MemberRoles|Add new role"
msgstr ""
-msgid "MemberRoles|Admin vulnerability"
-msgstr ""
-
-msgid "MemberRoles|Allows admin access to the vulnerability reports. Select 'Read vulnerability' for this to take effect."
-msgstr ""
-
-msgid "MemberRoles|Allows manage access to the project access tokens. Select 'Manage Project Access Tokens' for this to take effect."
-msgstr ""
-
-msgid "MemberRoles|Allows read-only access to the source code."
-msgstr ""
-
-msgid "MemberRoles|Allows read-only access to the vulnerability reports."
-msgstr ""
-
msgid "MemberRoles|Are you sure you want to delete this role?"
msgstr ""
@@ -29166,12 +29439,18 @@ msgstr ""
msgid "MemberRoles|Base role to use as template"
msgstr ""
+msgid "MemberRoles|Could not fetch available permissions: %{message}"
+msgstr ""
+
msgid "MemberRoles|Create new role"
msgstr ""
msgid "MemberRoles|Custom roles"
msgstr ""
+msgid "MemberRoles|Custom roles based on %{accessLevel}"
+msgstr ""
+
msgid "MemberRoles|Delete role"
msgstr ""
@@ -29199,9 +29478,6 @@ msgstr ""
msgid "MemberRoles|Make sure the group is in the Ultimate tier."
msgstr ""
-msgid "MemberRoles|Manage Project Access Tokens"
-msgstr ""
-
msgid "MemberRoles|Name"
msgstr ""
@@ -29211,12 +29487,6 @@ msgstr ""
msgid "MemberRoles|Permissions"
msgstr ""
-msgid "MemberRoles|Read code"
-msgstr ""
-
-msgid "MemberRoles|Read vulnerability"
-msgstr ""
-
msgid "MemberRoles|Role name"
msgstr ""
@@ -29229,6 +29499,9 @@ msgstr ""
msgid "MemberRoles|Select a standard role to add permissions."
msgstr ""
+msgid "MemberRoles|Standard roles"
+msgstr ""
+
msgid "MemberRoles|To add a new role select 'Add new role'."
msgstr ""
@@ -29241,9 +29514,6 @@ msgstr ""
msgid "MemberRole|%{requirement} has to be enabled in order to enable %{permission}."
msgstr ""
-msgid "MemberRole|%{role} - custom"
-msgstr ""
-
msgid "MemberRole|can't be changed"
msgstr ""
@@ -29407,6 +29677,9 @@ msgstr ""
msgid "Members|Membership"
msgstr ""
+msgid "Members|Private group information is only accessible to its members."
+msgstr ""
+
msgid "Members|Remove \"%{groupName}\""
msgstr ""
@@ -29481,7 +29754,7 @@ msgstr ""
msgid "Merge automatically (%{strategy})"
msgstr ""
-msgid "Merge blocked: all merge request dependencies must be merged."
+msgid "Merge blocked: Merge all open dependent merge requests, and remove all closed dependencies."
msgstr ""
msgid "Merge blocked: pipeline must succeed. It's waiting for a manual job to continue."
@@ -29493,6 +29766,9 @@ msgstr ""
msgid "Merge conflicts"
msgstr ""
+msgid "Merge conflicts must be resolved."
+msgstr ""
+
msgid "Merge date & time could not be determined"
msgstr ""
@@ -29508,6 +29784,12 @@ msgstr ""
msgid "Merge in progress"
msgstr ""
+msgid "Merge now and don't restart train"
+msgstr ""
+
+msgid "Merge now and restart train"
+msgstr ""
+
msgid "Merge options"
msgstr ""
@@ -29544,6 +29826,18 @@ msgstr ""
msgid "Merge request events"
msgstr ""
+msgid "Merge request is blocked by another merge request."
+msgstr ""
+
+msgid "Merge request must be open."
+msgstr ""
+
+msgid "Merge request must be rebased, because a fast-forward merge is not possible."
+msgstr ""
+
+msgid "Merge request must not be draft."
+msgstr ""
+
msgid "Merge request not merged"
msgstr ""
@@ -29577,6 +29871,9 @@ msgstr ""
msgid "Merge requests can't be merged if the status checks did not succeed or are still running."
msgstr ""
+msgid "Merge train pipelines continue without the merged changes."
+msgstr ""
+
msgid "Merge trains"
msgstr ""
@@ -29931,9 +30228,15 @@ msgstr ""
msgid "Metrics|Delete metric?"
msgstr ""
+msgid "Metrics|Description"
+msgstr ""
+
msgid "Metrics|Edit metric"
msgstr ""
+msgid "Metrics|Failed to load metrics."
+msgstr ""
+
msgid "Metrics|For grouping similar metrics"
msgstr ""
@@ -29943,9 +30246,15 @@ msgstr ""
msgid "Metrics|Legend label (optional)"
msgstr ""
+msgid "Metrics|Metrics"
+msgstr ""
+
msgid "Metrics|Must be a valid PromQL query."
msgstr ""
+msgid "Metrics|Name"
+msgstr ""
+
msgid "Metrics|New metric"
msgstr ""
@@ -29958,6 +30267,9 @@ msgstr ""
msgid "Metrics|There was an error trying to validate your query"
msgstr ""
+msgid "Metrics|Type"
+msgstr ""
+
msgid "Metrics|Unit label"
msgstr ""
@@ -30062,27 +30374,15 @@ msgstr ""
msgid "Milestone(s) not found: %{milestones}"
msgstr ""
-msgid "MilestoneCombobox|An error occurred while searching for milestones"
-msgstr ""
-
msgid "MilestoneCombobox|Group milestones"
msgstr ""
-msgid "MilestoneCombobox|Milestone"
-msgstr ""
-
-msgid "MilestoneCombobox|No matching results"
-msgstr ""
-
msgid "MilestoneCombobox|No milestone"
msgstr ""
msgid "MilestoneCombobox|Project milestones"
msgstr ""
-msgid "MilestoneCombobox|Search Milestones"
-msgstr ""
-
msgid "MilestoneCombobox|Select milestone"
msgstr ""
@@ -30404,6 +30704,9 @@ msgstr ""
msgid "MlExperimentTracking|Model performance"
msgstr ""
+msgid "MlExperimentTracking|Model removed"
+msgstr ""
+
msgid "MlExperimentTracking|Name"
msgstr ""
@@ -30437,11 +30740,22 @@ msgstr ""
msgid "MlExperimentTracking|Triggered by"
msgstr ""
-msgid "MlModelRegistry|%{version} · No other versions"
-msgid_plural "MlModelRegistry|%{version} · %{versionCount} versions"
+msgid "MlModelRegistry|%d model"
+msgid_plural "MlModelRegistry|%d models"
msgstr[0] ""
msgstr[1] ""
+msgid "MlModelRegistry|%d version"
+msgid_plural "MlModelRegistry|%d versions"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "MlModelRegistry|Details"
+msgstr ""
+
+msgid "MlModelRegistry|Latest version"
+msgstr ""
+
msgid "MlModelRegistry|Model registry"
msgstr ""
@@ -30451,6 +30765,20 @@ msgstr ""
msgid "MlModelRegistry|No registered versions"
msgstr ""
+msgid "MlModelRegistry|This model has no versions"
+msgstr ""
+
+msgid "MlModelRegistry|Version candidates"
+msgstr ""
+
+msgid "MlModelRegistry|Versions"
+msgstr ""
+
+msgid "MlModelRegistry|· No other versions"
+msgid_plural "MlModelRegistry|· %d versions"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "Modal updated"
msgstr ""
@@ -31008,6 +31336,12 @@ msgstr ""
msgid "NavigationTheme|Red"
msgstr ""
+msgid "Navigation|%{title} added to pinned items"
+msgstr ""
+
+msgid "Navigation|%{title} removed from pinned items"
+msgstr ""
+
msgid "Navigation|Admin Area"
msgstr ""
@@ -31071,9 +31405,15 @@ msgstr ""
msgid "Navigation|Plan"
msgstr ""
+msgid "Navigation|Preferences"
+msgstr ""
+
msgid "Navigation|Primary navigation"
msgstr ""
+msgid "Navigation|Profile"
+msgstr ""
+
msgid "Navigation|Projects you visit often will appear here."
msgstr ""
@@ -31409,6 +31749,9 @@ msgstr ""
msgid "No available branches"
msgstr ""
+msgid "No branch selected"
+msgstr ""
+
msgid "No branches found"
msgstr ""
@@ -32388,9 +32731,6 @@ msgstr ""
msgid "Number of employees"
msgstr ""
-msgid "Number of events"
-msgstr ""
-
msgid "Number of files touched"
msgstr ""
@@ -32430,6 +32770,24 @@ msgstr ""
msgid "Objective"
msgstr ""
+msgid "ObservabilityMetrics|Metrics"
+msgstr ""
+
+msgid "Observability|Enable"
+msgstr ""
+
+msgid "Observability|Error: Failed to enable GitLab Observability. Please retry later."
+msgstr ""
+
+msgid "Observability|Error: Failed to load page. Try reloading the page."
+msgstr ""
+
+msgid "Observability|Get started with GitLab Observability"
+msgstr ""
+
+msgid "Observability|Monitor your applications with GitLab Observability."
+msgstr ""
+
msgid "Oct"
msgstr ""
@@ -33073,6 +33431,9 @@ msgstr ""
msgid "Options"
msgstr ""
+msgid "Or create your own GitLab account:"
+msgstr ""
+
msgid "Ordered list"
msgstr ""
@@ -33094,12 +33455,21 @@ msgstr ""
msgid "Organization|An error occurred loading the groups. Please refresh the page to try again."
msgstr ""
+msgid "Organization|An error occurred loading the organization users. Please refresh the page to try again."
+msgstr ""
+
msgid "Organization|An error occurred loading the projects. Please refresh the page to try again."
msgstr ""
msgid "Organization|An error occurred loading user organizations. Please refresh the page to try again."
msgstr ""
+msgid "Organization|An error occurred updating your organization. Please try again."
+msgstr ""
+
+msgid "Organization|Choose what organization you want to see by default."
+msgstr ""
+
msgid "Organization|Copy organization ID"
msgstr ""
@@ -33118,6 +33488,9 @@ msgstr ""
msgid "Organization|Get started with organizations"
msgstr ""
+msgid "Organization|Home organization"
+msgstr ""
+
msgid "Organization|Manage"
msgstr ""
@@ -33133,12 +33506,18 @@ msgstr ""
msgid "Organization|Org ID"
msgstr ""
+msgid "Organization|Organization ID"
+msgstr ""
+
msgid "Organization|Organization URL"
msgstr ""
msgid "Organization|Organization URL is required."
msgstr ""
+msgid "Organization|Organization URL must be a minimum of two characters."
+msgstr ""
+
msgid "Organization|Organization name"
msgstr ""
@@ -33151,18 +33530,36 @@ msgstr ""
msgid "Organization|Organization overview"
msgstr ""
+msgid "Organization|Organization settings"
+msgstr ""
+
msgid "Organization|Organization successfully created."
msgstr ""
+msgid "Organization|Organization was successfully updated."
+msgstr ""
+
msgid "Organization|Organizations"
msgstr ""
msgid "Organization|Public - The organization can be accessed without any authentication."
msgstr ""
+msgid "Organization|Search for an organization"
+msgstr ""
+
msgid "Organization|Search or filter list"
msgstr ""
+msgid "Organization|Select an organization"
+msgstr ""
+
+msgid "Organization|Unable to fetch organizations. Reload the page to try again."
+msgstr ""
+
+msgid "Organization|Update your organization name, description, and avatar."
+msgstr ""
+
msgid "Organization|View all"
msgstr ""
@@ -34164,9 +34561,6 @@ msgstr ""
msgid "PerformanceBar|Redis calls"
msgstr ""
-msgid "PerformanceBar|Rugged calls"
-msgstr ""
-
msgid "PerformanceBar|SQL queries"
msgstr ""
@@ -34230,9 +34624,6 @@ msgstr ""
msgid "Personal access token"
msgstr ""
-msgid "Personal project creation is not allowed. Please contact your administrator with questions"
-msgstr ""
-
msgid "Personal projects"
msgstr ""
@@ -34254,15 +34645,6 @@ msgstr ""
msgid "Phone"
msgstr ""
-msgid "Phone verification exemption"
-msgstr ""
-
-msgid "Phone verification exemption has been created."
-msgstr ""
-
-msgid "Phone verification exemption has been removed."
-msgstr ""
-
msgid "PhoneVerification|Enter a valid code."
msgstr ""
@@ -34326,6 +34708,9 @@ msgstr ""
msgid "Pipeline editor"
msgstr ""
+msgid "Pipeline must succeed."
+msgstr ""
+
msgid "Pipeline ran in fork of project"
msgstr ""
@@ -34677,9 +35062,6 @@ msgstr ""
msgid "PipelineStatusTooltip|Pipeline: %{ciStatus}"
msgstr ""
-msgid "PipelineStatusTooltip|Pipeline: %{ci_status}"
-msgstr ""
-
msgid "PipelineWizardDefaultCommitMessage|Add %{filename}"
msgstr ""
@@ -34749,30 +35131,15 @@ msgstr ""
msgid "Pipelines|\"Hello world\" with GitLab CI"
msgstr ""
-msgid "Pipelines|1. Set up a runner"
-msgstr ""
-
-msgid "Pipelines|2. Configure deployment pipeline"
-msgstr ""
-
-msgid "Pipelines|API"
-msgstr ""
-
msgid "Pipelines|Are you sure you want to run this pipeline?"
msgstr ""
msgid "Pipelines|Auto DevOps"
msgstr ""
-msgid "Pipelines|Based on your project, we recommend this template:"
-msgstr ""
-
msgid "Pipelines|Build with confidence"
msgstr ""
-msgid "Pipelines|Building for iOS?"
-msgstr ""
-
msgid "Pipelines|By revoking a trigger you will break any processes making use of it. Are you sure?"
msgstr ""
@@ -34788,9 +35155,6 @@ msgstr ""
msgid "Pipelines|Clear runner caches"
msgstr ""
-msgid "Pipelines|Configure pipeline"
-msgstr ""
-
msgid "Pipelines|Continuous integration and deployment template to test and deploy your %{name} project."
msgstr ""
@@ -34806,9 +35170,6 @@ msgstr ""
msgid "Pipelines|Description"
msgstr ""
-msgid "Pipelines|Don't need a guide? Jump in right away with a template."
-msgstr ""
-
msgid "Pipelines|Edit"
msgstr ""
@@ -34818,9 +35179,6 @@ msgstr ""
msgid "Pipelines|Failed to update. Please reload page to update the list of artifacts."
msgstr ""
-msgid "Pipelines|Follow these instructions to install GitLab Runner on macOS."
-msgstr ""
-
msgid "Pipelines|Full configuration"
msgstr ""
@@ -34836,9 +35194,6 @@ msgstr ""
msgid "Pipelines|GitLab CI/CD can automatically build, test, and deploy your code. Let GitLab take care of time consuming tasks, so you can spend more time creating."
msgstr ""
-msgid "Pipelines|GitLab Runner is an application that works with GitLab CI/CD to run jobs in a pipeline."
-msgstr ""
-
msgid "Pipelines|Go to the pipeline editor"
msgstr ""
@@ -34857,9 +35212,6 @@ msgstr ""
msgid "Pipelines|Learn the basics of pipelines and .yml files"
msgstr ""
-msgid "Pipelines|Let's get that runner set up! %{emojiStart}tada%{emojiEnd}"
-msgstr ""
-
msgid "Pipelines|Lint"
msgstr ""
@@ -34872,15 +35224,9 @@ msgstr ""
msgid "Pipelines|More Information"
msgstr ""
-msgid "Pipelines|Need more information to set up your runner? %{linkStart}Check out our documentation%{linkEnd}."
-msgstr ""
-
msgid "Pipelines|No triggers have been created yet. Add one using the form above."
msgstr ""
-msgid "Pipelines|Not building for iOS or not what you're looking for? %{linkStart}See what else%{linkEnd} GitLab CI/CD has to offer."
-msgstr ""
-
msgid "Pipelines|Owner"
msgstr ""
@@ -34908,9 +35254,6 @@ msgstr ""
msgid "Pipelines|Scheduled"
msgstr ""
-msgid "Pipelines|Set up a runner"
-msgstr ""
-
msgid "Pipelines|Something went wrong while cleaning runners cache."
msgstr ""
@@ -34956,10 +35299,13 @@ msgstr ""
msgid "Pipelines|This pipeline is stuck"
msgstr ""
-msgid "Pipelines|This pipeline ran on the contents of this merge request combined with the contents of all other merge requests queued for merging into the target branch."
+msgid "Pipelines|This pipeline ran on the contents of the merge request combined with the contents of all other merge requests queued for merging into the target branch."
+msgstr ""
+
+msgid "Pipelines|This pipeline ran on the contents of the merge request combined with the contents of the target branch."
msgstr ""
-msgid "Pipelines|This pipeline ran on the contents of this merge request's source branch, not the target branch."
+msgid "Pipelines|This pipeline ran on the contents of the merge request's source branch, not the target branch."
msgstr ""
msgid "Pipelines|This pipeline will run code originating from a forked project merge request. This means that the code can potentially have security considerations like exposing CI variables."
@@ -35013,15 +35359,6 @@ msgstr ""
msgid "Pipelines|We'll continuously validate your pipeline configuration. The validation results will appear here."
msgstr ""
-msgid "Pipelines|We'll guide you through a simple pipeline set-up."
-msgstr ""
-
-msgid "Pipelines|We'll walk you through how to deploy to iOS in two easy steps."
-msgstr ""
-
-msgid "Pipelines|You have runners available to run your job now. No need to do anything else."
-msgstr ""
-
msgid "Pipelines|You should review the code thoroughly before running this pipeline with the parent project's CI/CD resources."
msgstr ""
@@ -35052,6 +35389,9 @@ msgstr ""
msgid "Pipelines|merge train"
msgstr ""
+msgid "Pipelines|merged results"
+msgstr ""
+
msgid "Pipelines|stuck"
msgstr ""
@@ -35226,6 +35566,9 @@ msgstr ""
msgid "Pipeline|You're about to stop pipeline #%{pipelineId}."
msgstr ""
+msgid "Pipeline|api"
+msgstr ""
+
msgid "Pipeline|for"
msgstr ""
@@ -35769,6 +36112,9 @@ msgstr ""
msgid "Preferences|This feature is experimental and translations are not yet complete."
msgstr ""
+msgid "Preferences|Time format"
+msgstr ""
+
msgid "Preferences|Time preferences"
msgstr ""
@@ -35913,6 +36259,9 @@ msgstr ""
msgid "Product analytics"
msgstr ""
+msgid "Product analytics requires Experiment and Beta features to be enabled."
+msgstr ""
+
msgid "ProductAnalytics|1. Add the NPM package to your package.json using your preferred package manager"
msgstr ""
@@ -35934,25 +36283,28 @@ msgstr ""
msgid "ProductAnalytics|After your application has been instrumented and data is being collected, you can visualize and monitor behaviors in your %{linkStart}analytics dashboards%{linkEnd}."
msgstr ""
-msgid "ProductAnalytics|All Clicks Compared"
+msgid "ProductAnalytics|All Events Compared"
msgstr ""
-msgid "ProductAnalytics|All Events Compared"
+msgid "ProductAnalytics|All Link Clicks"
msgstr ""
msgid "ProductAnalytics|All Pages"
msgstr ""
+msgid "ProductAnalytics|All Returning Users Compared"
+msgstr ""
+
msgid "ProductAnalytics|All Sessions Compared"
msgstr ""
msgid "ProductAnalytics|An error occurred while fetching data. Refresh the page to try again."
msgstr ""
-msgid "ProductAnalytics|Analyze your product with Product Analytics"
+msgid "ProductAnalytics|Analytics events by month"
msgstr ""
-msgid "ProductAnalytics|Any Click on elements"
+msgid "ProductAnalytics|Analyze your product with Product Analytics"
msgstr ""
msgid "ProductAnalytics|Audience"
@@ -35970,16 +36322,16 @@ msgstr ""
msgid "ProductAnalytics|Back to dashboards"
msgstr ""
-msgid "ProductAnalytics|Click Events"
+msgid "ProductAnalytics|Compares all events against each other"
msgstr ""
-msgid "ProductAnalytics|Compares all events against each other"
+msgid "ProductAnalytics|Compares all returning users against each other"
msgstr ""
msgid "ProductAnalytics|Compares all user sessions against each other"
msgstr ""
-msgid "ProductAnalytics|Compares click events against each other"
+msgid "ProductAnalytics|Compares link click events against each other"
msgstr ""
msgid "ProductAnalytics|Compares page views of all pages against each other"
@@ -35991,6 +36343,9 @@ msgstr ""
msgid "ProductAnalytics|Cube API key"
msgstr ""
+msgid "ProductAnalytics|Current month to date"
+msgstr ""
+
msgid "ProductAnalytics|Details on how to configure product analytics to collect data."
msgstr ""
@@ -36015,24 +36370,42 @@ msgstr ""
msgid "ProductAnalytics|For the product analytics dashboard to start showing you some data, you need to add the analytics tracking code to your project."
msgstr ""
+msgid "ProductAnalytics|Get started with product analytics"
+msgstr ""
+
msgid "ProductAnalytics|How many sessions a user has"
msgstr ""
-msgid "ProductAnalytics|How often sessions are repeated"
+msgid "ProductAnalytics|How often users returned compared to all sessions"
msgstr ""
msgid "ProductAnalytics|Instrument your application"
msgstr ""
+msgid "ProductAnalytics|Learn how to enable product analytics"
+msgstr ""
+
+msgid "ProductAnalytics|Learn how to onboard projects"
+msgstr ""
+
+msgid "ProductAnalytics|Link Click Events"
+msgstr ""
+
msgid "ProductAnalytics|Loading instance"
msgstr ""
msgid "ProductAnalytics|Measure All tracked Events"
msgstr ""
+msgid "ProductAnalytics|Measure all link click events"
+msgstr ""
+
msgid "ProductAnalytics|Measure all or specific Page Views"
msgstr ""
+msgid "ProductAnalytics|Measure all returning users"
+msgstr ""
+
msgid "ProductAnalytics|Measure all sessions"
msgstr ""
@@ -36042,16 +36415,40 @@ msgstr ""
msgid "ProductAnalytics|Measuring"
msgstr ""
+msgid "ProductAnalytics|Month"
+msgstr ""
+
+msgid "ProductAnalytics|No projects found"
+msgstr ""
+
msgid "ProductAnalytics|On what do you want to get insights?"
msgstr ""
msgid "ProductAnalytics|Page Views"
msgstr ""
+msgid "ProductAnalytics|Percentage of Users Returning"
+msgstr ""
+
+msgid "ProductAnalytics|Previous month"
+msgstr ""
+
+msgid "ProductAnalytics|Product Analytics"
+msgstr ""
+
msgid "ProductAnalytics|Product analytics onboarding"
msgstr ""
-msgid "ProductAnalytics|Repeat Visit Percentage"
+msgid "ProductAnalytics|Product analytics usage is calculated based on the total number of events received from projects within the group. %{linkStart}Learn more%{linkEnd}."
+msgstr ""
+
+msgid "ProductAnalytics|Projects"
+msgstr ""
+
+msgid "ProductAnalytics|Projects (%{maxProjects} of %{totalProjects} shown)"
+msgstr ""
+
+msgid "ProductAnalytics|Returning Users"
msgstr ""
msgid "ProductAnalytics|SDK application ID"
@@ -36078,6 +36475,12 @@ msgstr ""
msgid "ProductAnalytics|Snowplow configurator connection string"
msgstr ""
+msgid "ProductAnalytics|Something went wrong while loading product analytics usage data. Refresh the page to try again."
+msgstr ""
+
+msgid "ProductAnalytics|Store, query, and visualize quantitative data to get insights into user value."
+msgstr ""
+
msgid "ProductAnalytics|The connection string for your Snowplow configurator instance."
msgstr ""
@@ -36087,18 +36490,33 @@ msgstr ""
msgid "ProductAnalytics|The sender of tracking events"
msgstr ""
-msgid "ProductAnalytics|There is no data for this type of chart currently. Please see the Setup tab if you have not configured the product analytics tool already."
+msgid "ProductAnalytics|This group has no projects with product analytics onboarded in the current period."
msgstr ""
msgid "ProductAnalytics|This might take a while, feel free to navigate away from this page and come back later."
msgstr ""
+msgid "ProductAnalytics|This table excludes projects that do not have product analytics onboarded."
+msgstr ""
+
msgid "ProductAnalytics|To instrument your application, select one of the options below. After an option has been instrumented and data is being collected, this page will progress to the next step."
msgstr ""
+msgid "ProductAnalytics|Track your product's performance, and optimize your product and development processes."
+msgstr ""
+
msgid "ProductAnalytics|Unique Users"
msgstr ""
+msgid "ProductAnalytics|Usage by month"
+msgstr ""
+
+msgid "ProductAnalytics|Usage by project"
+msgstr ""
+
+msgid "ProductAnalytics|Use product analytics"
+msgstr ""
+
msgid "ProductAnalytics|Used to retrieve dashboard data from the Cube instance."
msgstr ""
@@ -36732,6 +37150,9 @@ msgstr ""
msgid "Project export started. A download link will be sent by email and made available on this page."
msgstr ""
+msgid "Project groups"
+msgstr ""
+
msgid "Project has too many %{label_for_message} to search"
msgstr ""
@@ -36831,6 +37252,11 @@ msgstr ""
msgid "ProjectCreationLevel|Roles allowed to create projects"
msgstr ""
+msgid "ProjectExceededSize|Here is the project exceeding the storage quota:%{projects_list}"
+msgid_plural "ProjectExceededSize|From the %{repository_size_excess_project_count} projects exceeding the quota, below are the projects using the most storage:%{projects_list}"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "ProjectFileTree|Name"
msgstr ""
@@ -37602,6 +38028,9 @@ msgstr ""
msgid "ProjectTemplates|Android"
msgstr ""
+msgid "ProjectTemplates|Astro Tailwind"
+msgstr ""
+
msgid "ProjectTemplates|GitLab Cluster Management"
msgstr ""
@@ -38199,9 +38628,6 @@ msgstr ""
msgid "Protect a tag"
msgstr ""
-msgid "Protect variable"
-msgstr ""
-
msgid "Protected"
msgstr ""
@@ -38232,7 +38658,7 @@ msgstr ""
msgid "Protected tags"
msgstr ""
-msgid "ProtectedBranch|%{wildcards_link_start}Wildcards%{wildcards_link_end} such as %{code_tag_start}*-stable%{code_tag_end} or %{code_tag_start}production/*%{code_tag_end} are supported."
+msgid "ProtectedBranch|%{wildcards_link_start}Wildcards%{wildcards_link_end} such as %{code_tag_start}*-stable%{code_tag_end} or %{code_tag_start}production/*%{code_tag_end} are supported. %{case_sensitive_link_start}Branch names are case-sensitive.%{case_sensitive_link_end}"
msgstr ""
msgid "ProtectedBranch|After you configure a protected branch, merge request approval, or status check, it appears here."
@@ -38316,7 +38742,7 @@ msgstr ""
msgid "ProtectedBranch|No tags are protected."
msgstr ""
-msgid "ProtectedBranch|Only %{wildcards_link_start}Wildcards%{wildcards_link_end} such as %{code_tag_start}*-stable%{code_tag_end} or %{code_tag_start}production/*%{code_tag_end} are supported."
+msgid "ProtectedBranch|Only %{wildcards_link_start}Wildcards%{wildcards_link_end} such as %{code_tag_start}*-stable%{code_tag_end} or %{code_tag_start}production/*%{code_tag_end} are supported. %{case_sensitive_link_start}Branch names are case-sensitive.%{case_sensitive_link_end}"
msgstr ""
msgid "ProtectedBranch|Protect"
@@ -38635,9 +39061,6 @@ msgstr ""
msgid "Pull requests from fork are not supported"
msgstr ""
-msgid "Puma is running with a thread count above 1 and the Rugged service is enabled. This may decrease performance in some environments. See our %{link_start}documentation%{link_end} for details of this issue."
-msgstr ""
-
msgid "PumbleIntegration|Send notifications about project events to Pumble."
msgstr ""
@@ -38905,9 +39328,6 @@ msgstr ""
msgid "Read more about GitLab at %{link_to_promo}."
msgstr ""
-msgid "Read more about the %{changes_link_start}changes%{link_end}, the %{vision_link_start}vision%{link_end}, and the %{design_link_start}design%{link_end}."
-msgstr ""
-
msgid "Read the documentation before applying changes."
msgstr ""
@@ -39383,6 +39803,9 @@ msgstr ""
msgid "Remove icon"
msgstr ""
+msgid "Remove identity verification exemption"
+msgstr ""
+
msgid "Remove issue reference"
msgstr ""
@@ -39422,9 +39845,6 @@ msgstr ""
msgid "Remove parent epic from an epic"
msgstr ""
-msgid "Remove phone verification exemption"
-msgstr ""
-
msgid "Remove priority"
msgstr ""
@@ -39605,6 +40025,9 @@ msgstr ""
msgid "Reopening %{issuableType}..."
msgstr ""
+msgid "Reopening %{workItemType}"
+msgstr ""
+
msgid "Reopening..."
msgstr ""
@@ -39659,6 +40082,9 @@ msgstr ""
msgid "Reply to this email directly or %{view_it_on_gitlab}."
msgstr ""
+msgid "Reply..."
+msgstr ""
+
msgid "Reply…"
msgstr ""
@@ -40049,6 +40475,12 @@ msgstr ""
msgid "Request a new one"
msgstr ""
+msgid "Request changes"
+msgstr ""
+
+msgid "Request changes to the current merge request."
+msgstr ""
+
msgid "Request data is too large"
msgstr ""
@@ -40091,6 +40523,9 @@ msgstr ""
msgid "Require additional authentication for administrative tasks."
msgstr ""
+msgid "Require expiration date"
+msgstr ""
+
msgid "Required approvals (%{approvals_given} given)"
msgstr ""
@@ -40281,6 +40716,9 @@ msgstr ""
msgid "Restart Terminal"
msgstr ""
+msgid "Restart merge train pipelines with the merged changes."
+msgstr ""
+
msgid "Restore"
msgstr ""
@@ -40596,9 +41034,6 @@ msgstr ""
msgid "Runners|Add tags to specify jobs that the runner can run. %{helpLinkStart}Learn more.%{helpLinkEnd}"
msgstr ""
-msgid "Runners|Add your feedback to this issue"
-msgstr ""
-
msgid "Runners|Admin area › Runners"
msgstr ""
@@ -40725,7 +41160,10 @@ msgstr ""
msgid "Runners|Created %{timeAgo}"
msgstr ""
-msgid "Runners|Created %{timeAgo} by %{avatar}"
+msgid "Runners|Created by %{user}"
+msgstr ""
+
+msgid "Runners|Created by %{user} %{timeAgo}"
msgstr ""
msgid "Runners|Delete"
@@ -41241,10 +41679,10 @@ msgstr ""
msgid "Runners|Tags control which type of jobs a runner can handle. By tagging a runner, you make sure shared runners only handle the jobs they are equipped to run."
msgstr ""
-msgid "Runners|The %{boldStart}runner token%{boldEnd} %{token} displays %{boldStart}only for a short time%{boldEnd}, and is stored in the %{codeStart}config.toml%{codeEnd} after you register the runner. It will not be visible once the runner is registered."
+msgid "Runners|The %{boldStart}runner authentication token%{boldEnd} %{token} displays here %{boldStart}for a short time only%{boldEnd}. After you register the runner, this token is stored in the %{codeStart}config.toml%{codeEnd} and cannot be accessed again from the UI."
msgstr ""
-msgid "Runners|The %{boldStart}runner token%{boldEnd} is no longer visible, it is stored in the %{codeStart}config.toml%{codeEnd} if you have registered the runner."
+msgid "Runners|The %{boldStart}runner authentication token%{boldEnd} is no longer visible, it is stored in the %{codeStart}config.toml%{codeEnd} if you have registered the runner."
msgstr ""
msgid "Runners|The project, group or instance where the runner was registered. Instance runners are always owned by Administrator."
@@ -41365,22 +41803,22 @@ msgstr ""
msgid "Runners|Version %{version}"
msgstr ""
-msgid "Runners|View installation instructions"
+msgid "Runners|Version starts with"
msgstr ""
-msgid "Runners|View metrics"
+msgid "Runners|View installation instructions"
msgstr ""
-msgid "Runners|Wait time (secs)"
+msgid "Runners|View metrics"
msgstr ""
-msgid "Runners|Wait time to pick a job"
+msgid "Runners|View runners list"
msgstr ""
-msgid "Runners|We've been making improvements to how you register runners so that it's more secure and efficient. Tell us how we're doing."
+msgid "Runners|Wait time (secs)"
msgstr ""
-msgid "Runners|We've made some changes and want your feedback"
+msgid "Runners|Wait time to pick a job"
msgstr ""
msgid "Runners|Windows 2019 Shell with manual scaling and optional scheduling. %{percentage} spot."
@@ -41476,6 +41914,9 @@ msgstr ""
msgid "SAML|Your organization's SSO has been connected to your GitLab account"
msgstr ""
+msgid "SAST"
+msgstr ""
+
msgid "SBOMs last updated"
msgstr ""
@@ -41758,6 +42199,9 @@ msgstr ""
msgid "ScanResultPolicy|Fix available is only applicable to container and dependency scanning"
msgstr ""
+msgid "ScanResultPolicy|If an MR receives all necessary approvals to merge, but then a new commit is added, new approvals are required. This ensures new commits that may include vulnerabilities cannot be introduced."
+msgstr ""
+
msgid "ScanResultPolicy|If selected, the following choices will overwrite %{linkStart}project settings%{linkEnd} but only affect the branches selected in the policy."
msgstr ""
@@ -41794,6 +42238,9 @@ msgstr ""
msgid "ScanResultPolicy|Newly Detected"
msgstr ""
+msgid "ScanResultPolicy|No settings available for this policy"
+msgstr ""
+
msgid "ScanResultPolicy|Only 1 age criteria is allowed"
msgstr ""
@@ -41806,10 +42253,16 @@ msgstr ""
msgid "ScanResultPolicy|Override project approval settings"
msgstr ""
+msgid "ScanResultPolicy|Password confirmation on approvals provides an additional level of security. Enabling this enforces the setting on all projects targeted by this policy."
+msgstr ""
+
msgid "ScanResultPolicy|Pre-existing"
msgstr ""
-msgid "ScanResultPolicy|Prevent approval by anyone who added a commit"
+msgid "ScanResultPolicy|Prevent a user from removing a branch from the protected branches list or from deleting a protected branch."
+msgstr ""
+
+msgid "ScanResultPolicy|Prevent approval by commit author"
msgstr ""
msgid "ScanResultPolicy|Prevent approval by merge request's author"
@@ -41818,13 +42271,19 @@ msgstr ""
msgid "ScanResultPolicy|Prevent branch protection modification"
msgstr ""
+msgid "ScanResultPolicy|Prevent pushing and force pushing"
+msgstr ""
+
+msgid "ScanResultPolicy|Prevent pushing and force pushing to a protected branch."
+msgstr ""
+
msgid "ScanResultPolicy|Protected branch settings"
msgstr ""
msgid "ScanResultPolicy|Recommended setting"
msgstr ""
-msgid "ScanResultPolicy|Remove all approvals when commit is added"
+msgid "ScanResultPolicy|Remove all approvals with new commit"
msgstr ""
msgid "ScanResultPolicy|Require the user's password to approve"
@@ -41848,9 +42307,15 @@ msgstr ""
msgid "ScanResultPolicy|Status is:"
msgstr ""
+msgid "ScanResultPolicy|The merge request author cannot approve their own merge request."
+msgstr ""
+
msgid "ScanResultPolicy|Unknown"
msgstr ""
+msgid "ScanResultPolicy|Users who have contributed code to the MR are ineligible for approval, ensuring code committers cannot introduce vulnerabilities and approve code to merge."
+msgstr ""
+
msgid "ScanResultPolicy|When %{scanType} %{scanners} runs against the %{branches} %{branchExceptions} and find(s) %{vulnerabilitiesNumber} %{boldDescription} of the following criteria:"
msgstr ""
@@ -41863,9 +42328,6 @@ msgstr ""
msgid "ScanResultPolicy|When %{scanners} find scanner specified conditions in an open merge request targeting the %{branches} %{branchExceptions} and match %{boldDescription} of the following criteria"
msgstr ""
-msgid "ScanResultPolicy|When enabled, two person approval will be required on all MRs as merge request authors cannot approve their own MRs and merge them unilaterally"
-msgstr ""
-
msgid "ScanResultPolicy|You have selected any protected branch option as a condition. To better protect your project, it is recommended to enable the protect branch settings. %{linkStart}Learn more.%{linkEnd}"
msgstr ""
@@ -42218,6 +42680,9 @@ msgstr ""
msgid "SecretDetection|This description appears to have a token in it. Are you sure you want to add it?"
msgstr ""
+msgid "Secrets"
+msgstr ""
+
msgid "Secure Code Warrior"
msgstr ""
@@ -42530,6 +42995,12 @@ msgstr ""
msgid "SecurityOrchestration|Any merge request"
msgstr ""
+msgid "SecurityOrchestration|Apply this policy to all projects %{projectScopeType} %{exceptionType} %{projectSelector}"
+msgstr ""
+
+msgid "SecurityOrchestration|Apply this policy to all projects %{projectScopeType} named %{frameworkSelector}"
+msgstr ""
+
msgid "SecurityOrchestration|Are you sure you want to delete this policy? This action cannot be undone."
msgstr ""
@@ -42539,12 +43010,18 @@ msgstr ""
msgid "SecurityOrchestration|Branch types don't match any existing branches."
msgstr ""
+msgid "SecurityOrchestration|Cannot create an empty policy"
+msgstr ""
+
msgid "SecurityOrchestration|Choose a project"
msgstr ""
msgid "SecurityOrchestration|Choose approver type"
msgstr ""
+msgid "SecurityOrchestration|Choose framework labels"
+msgstr ""
+
msgid "SecurityOrchestration|Choose specific role"
msgstr ""
@@ -42554,6 +43031,9 @@ msgstr ""
msgid "SecurityOrchestration|Create more robust vulnerability rules and apply them to all your projects."
msgstr ""
+msgid "SecurityOrchestration|Create new framework label"
+msgstr ""
+
msgid "SecurityOrchestration|Create policy"
msgstr ""
@@ -42611,9 +43091,18 @@ msgstr ""
msgid "SecurityOrchestration|Failed to load cluster agents."
msgstr ""
+msgid "SecurityOrchestration|Failed to load compliance frameworks"
+msgstr ""
+
+msgid "SecurityOrchestration|Failed to load group projects"
+msgstr ""
+
msgid "SecurityOrchestration|Failed to load images."
msgstr ""
+msgid "SecurityOrchestration|For any MR that matches this policy's rules, only the override project approval settings apply. No additional approvals are required."
+msgstr ""
+
msgid "SecurityOrchestration|For any merge request on %{branches}%{commitType}%{branchExceptionsString}"
msgstr ""
@@ -42668,6 +43157,9 @@ msgstr ""
msgid "SecurityOrchestration|No actions defined - policy will not run."
msgstr ""
+msgid "SecurityOrchestration|No compliance frameworks"
+msgstr ""
+
msgid "SecurityOrchestration|No description"
msgstr ""
@@ -42694,9 +43186,15 @@ msgid_plural "SecurityOrchestration|On runners with the tags:"
msgstr[0] ""
msgstr[1] ""
+msgid "SecurityOrchestration|Only overriding settings will take effect"
+msgstr ""
+
msgid "SecurityOrchestration|Only owners can update Security Policy Project"
msgstr ""
+msgid "SecurityOrchestration|Override the following project settings:"
+msgstr ""
+
msgid "SecurityOrchestration|Policies"
msgstr ""
@@ -42718,6 +43216,9 @@ msgstr ""
msgid "SecurityOrchestration|Policy editor"
msgstr ""
+msgid "SecurityOrchestration|Policy scope"
+msgstr ""
+
msgid "SecurityOrchestration|Policy status"
msgstr ""
@@ -42799,6 +43300,9 @@ msgstr ""
msgid "SecurityOrchestration|Select exception branches"
msgstr ""
+msgid "SecurityOrchestration|Select frameworks"
+msgstr ""
+
msgid "SecurityOrchestration|Select groups"
msgstr ""
@@ -42859,6 +43363,9 @@ msgstr ""
msgid "SecurityOrchestration|This is a project-level policy"
msgstr ""
+msgid "SecurityOrchestration|This policy doesn't contain any actions or override project approval settings. You cannot create an empty policy."
+msgstr ""
+
msgid "SecurityOrchestration|This policy is inherited"
msgstr ""
@@ -42931,12 +43438,18 @@ msgstr ""
msgid "SecurityOrchestration|You already have the maximum %{maximumAllowed} %{policyType} policies."
msgstr ""
+msgid "SecurityOrchestration|You can't unprotect this branch because its protection is enforced by one or more %{security_policies_link_start}security policies%{security_policies_link_end}. %{learn_more_link_start}Learn more%{learn_more_link_end}."
+msgstr ""
+
msgid "SecurityOrchestration|You don't have any security policies yet"
msgstr ""
msgid "SecurityOrchestration|all namespaces"
msgstr ""
+msgid "SecurityOrchestration|all projects in this group"
+msgstr ""
+
msgid "SecurityOrchestration|any"
msgstr ""
@@ -42967,6 +43480,9 @@ msgstr ""
msgid "SecurityOrchestration|by the agent named %{agents} %{cadence}%{branchExceptionsString}"
msgstr ""
+msgid "SecurityOrchestration|except projects"
+msgstr ""
+
msgid "SecurityOrchestration|group level branch input"
msgstr ""
@@ -42985,12 +43501,18 @@ msgstr ""
msgid "SecurityOrchestration|or from:"
msgstr ""
+msgid "SecurityOrchestration|projects with compliance frameworks"
+msgstr ""
+
msgid "SecurityOrchestration|scanner finds"
msgstr ""
msgid "SecurityOrchestration|scanners find"
msgstr ""
+msgid "SecurityOrchestration|specific projects"
+msgstr ""
+
msgid "SecurityOrchestration|targeting %{branchTypeText}"
msgstr ""
@@ -43018,6 +43540,9 @@ msgstr ""
msgid "SecurityOrchestration|with %{exceptionType} on %{branchSelector}"
msgstr ""
+msgid "SecurityOrchestration|without exceptions"
+msgstr ""
+
msgid "SecurityPolicies|Invalid or empty policy"
msgstr ""
@@ -43207,6 +43732,12 @@ msgstr ""
msgid "SecurityReports|Investigate this vulnerability by creating an issue"
msgstr ""
+msgid "SecurityReports|Is available"
+msgstr ""
+
+msgid "SecurityReports|Is not available"
+msgstr ""
+
msgid "SecurityReports|Issue"
msgstr ""
@@ -43323,6 +43854,9 @@ msgid_plural "SecurityReports|Show %d items"
msgstr[0] ""
msgstr[1] ""
+msgid "SecurityReports|Solution available"
+msgstr ""
+
msgid "SecurityReports|Sometimes a scanner can't determine a finding's severity. Those findings may still be a potential source of risk though. Please review these manually."
msgstr ""
@@ -43497,6 +44031,9 @@ msgstr ""
msgid "Select a label"
msgstr ""
+msgid "Select a merge moment"
+msgstr ""
+
msgid "Select a milestone"
msgstr ""
@@ -43761,7 +44298,7 @@ msgstr ""
msgid "Service Desk allows people to create issues in your GitLab instance without their own user account. It provides a unique email address for end users to create issues in a project. Replies can be sent either through the GitLab interface or by email. End users only see threads through email."
msgstr ""
-msgid "Service Ping payload not found in the application cache"
+msgid "Service access token expiration"
msgstr ""
msgid "Service account"
@@ -43770,10 +44307,10 @@ msgstr ""
msgid "Service account generated successfully"
msgstr ""
-msgid "Service accounts"
+msgid "Service account token expiration"
msgstr ""
-msgid "Service usage data"
+msgid "Service accounts"
msgstr ""
msgid "ServiceAccount|No more seats are available to create Service Account User"
@@ -43794,6 +44331,15 @@ msgstr ""
msgid "ServiceDesk|A verification email has been sent to a sub-address of your custom email address. This can take up to 30 minutes. The screen refreshes automatically."
msgstr ""
+msgid "ServiceDesk|Add email addresses in the %{codeStart}Cc%{codeEnd} header of Service Desk emails to the issue."
+msgstr ""
+
+msgid "ServiceDesk|Add external participants from the %{codeStart}Cc%{codeEnd} header"
+msgstr ""
+
+msgid "ServiceDesk|CRAM-MD5"
+msgstr ""
+
msgid "ServiceDesk|Cannot create custom email"
msgstr ""
@@ -43869,6 +44415,15 @@ msgstr ""
msgid "ServiceDesk|Keep custom email"
msgstr ""
+msgid "ServiceDesk|Let GitLab select a server-supported method (recommended)"
+msgstr ""
+
+msgid "ServiceDesk|Like the author, external participants receive Service Desk emails and can participate in the discussion."
+msgstr ""
+
+msgid "ServiceDesk|Login"
+msgstr ""
+
msgid "ServiceDesk|Minimum 8 characters long."
msgstr ""
@@ -43878,6 +44433,9 @@ msgstr ""
msgid "ServiceDesk|Parameters missing"
msgstr ""
+msgid "ServiceDesk|Plain"
+msgstr ""
+
msgid "ServiceDesk|Please share your feedback on this feature in the %{linkStart}feedback issue%{linkEnd}"
msgstr ""
@@ -43896,6 +44454,9 @@ msgstr ""
msgid "ServiceDesk|SMTP address is required and must be resolvable."
msgstr ""
+msgid "ServiceDesk|SMTP authentication method"
+msgstr ""
+
msgid "ServiceDesk|SMTP host"
msgstr ""
@@ -44007,6 +44568,9 @@ msgstr ""
msgid "Session duration (minutes)"
msgstr ""
+msgid "Session|There was a error loading the user verification challenge. Refresh to try again."
+msgstr ""
+
msgid "Set %{epic_ref} as the parent epic."
msgstr ""
@@ -44019,9 +44583,6 @@ msgstr ""
msgid "Set a password on your account to pull or push via %{protocol}."
msgstr ""
-msgid "Set any rate limit to %{code_open}0%{code_close} to disable the limit."
-msgstr ""
-
msgid "Set due date"
msgstr ""
@@ -44127,6 +44688,12 @@ msgstr ""
msgid "Set to 0 for no size limit."
msgstr ""
+msgid "Set to 0 to disable the limit."
+msgstr ""
+
+msgid "Set to 0 to disable the limits."
+msgstr ""
+
msgid "Set to 0 to disable timeout."
msgstr ""
@@ -44614,6 +45181,9 @@ msgstr ""
msgid "Sign up"
msgstr ""
+msgid "Sign up for your free trial with:"
+msgstr ""
+
msgid "Sign up was successful! Please confirm your email to sign in."
msgstr ""
@@ -45070,7 +45640,7 @@ msgstr ""
msgid "Something went wrong"
msgstr ""
-msgid "Something went wrong fetching the CodeQuality Findings. Please try again!"
+msgid "Something went wrong fetching the Scanner Findings. Please try again."
msgstr ""
msgid "Something went wrong on our end"
@@ -45085,6 +45655,9 @@ msgstr ""
msgid "Something went wrong on our end. Please try again."
msgstr ""
+msgid "Something went wrong trying to change the locked state of the discussion"
+msgstr ""
+
msgid "Something went wrong trying to change the locked state of this %{issuableDisplayName}"
msgstr ""
@@ -45265,13 +45838,10 @@ msgstr ""
msgid "Something went wrong. Please try again."
msgstr ""
-msgid "Something went wrong. Try again later."
-msgstr ""
-
-msgid "Something went wrong. Unable to create phone exemption."
+msgid "Something went wrong. Unable to create identity verification exemption."
msgstr ""
-msgid "Something went wrong. Unable to remove phone exemption."
+msgid "Something went wrong. Unable to remove identity verification exemption."
msgstr ""
msgid "Sorry, no projects matched your search"
@@ -45499,6 +46069,9 @@ msgstr ""
msgid "Source project cannot be found."
msgstr ""
+msgid "Source-Branch"
+msgstr ""
+
msgid "SourceEditor|\"el\" parameter is required for createInstance()"
msgstr ""
@@ -45712,9 +46285,6 @@ msgstr ""
msgid "Start free trial"
msgstr ""
-msgid "Start inputting changes and we will generate a YAML-file for you to add to your repository"
-msgstr ""
-
msgid "Start internal thread"
msgstr ""
@@ -45790,6 +46360,9 @@ msgstr ""
msgid "Status (optional)"
msgstr ""
+msgid "Status checks must pass."
+msgstr ""
+
msgid "Status was retried."
msgstr ""
@@ -45868,6 +46441,9 @@ msgstr ""
msgid "StatusCheck|Target branch"
msgstr ""
+msgid "StatusCheck|URL parameters are hidden for security reasons. For details of URL parameters, see the configuration for the status check service."
+msgstr ""
+
msgid "StatusCheck|Update status check"
msgstr ""
@@ -46030,6 +46606,15 @@ msgstr ""
msgid "Submit feedback"
msgstr ""
+msgid "Submit feedback and approve these changes."
+msgstr ""
+
+msgid "Submit feedback that should be addressed before merging."
+msgstr ""
+
+msgid "Submit general feedback without explicit approval."
+msgstr ""
+
msgid "Submit review"
msgstr ""
@@ -46279,6 +46864,9 @@ msgstr ""
msgid "Successfully synced %{synced_timeago}."
msgstr ""
+msgid "Successfully trusted"
+msgstr ""
+
msgid "Successfully unbanned"
msgstr ""
@@ -46291,6 +46879,9 @@ msgstr ""
msgid "Successfully unlocked"
msgstr ""
+msgid "Successfully untrusted"
+msgstr ""
+
msgid "Successfully updated %{last_updated_timeago}."
msgstr ""
@@ -47478,6 +48069,9 @@ msgstr ""
msgid "The branch or tag does not exist"
msgstr ""
+msgid "The branch to merge into."
+msgstr ""
+
msgid "The character highlighter helps you keep the subject line to %{titleLength} characters and wrap the body at %{bodyLength} so they are readable in git."
msgstr ""
@@ -47562,6 +48156,18 @@ msgstr ""
msgid "The directory has been successfully created."
msgstr ""
+msgid "The discussion in this %{issuableDisplayName} is locked. Only project members can comment."
+msgstr ""
+
+msgid "The discussion in this %{issuable} is locked. Only project members can comment."
+msgstr ""
+
+msgid "The discussion in this %{noteableTypeText} is locked."
+msgstr ""
+
+msgid "The discussion in this merge request is locked."
+msgstr ""
+
msgid "The domain you entered is misformatted."
msgstr ""
@@ -47821,6 +48427,9 @@ msgstr ""
msgid "The project was successfully imported."
msgstr ""
+msgid "The project-group link could not be removed."
+msgstr ""
+
msgid "The related CI build failed."
msgstr ""
@@ -48349,9 +48958,6 @@ msgstr ""
msgid "Third Party Advisory Link"
msgstr ""
-msgid "Third party AI settings not allowed."
-msgstr ""
-
msgid "This %{issuableDisplayName} is locked. Only project members can comment."
msgstr ""
@@ -48364,16 +48970,10 @@ msgstr ""
msgid "This %{issuable} is locked. Only %{strong_open}project members%{strong_close} can comment."
msgstr ""
-msgid "This %{issuable} is locked. Only project members can comment."
-msgstr ""
-
msgid "This %{issuable} would exceed the maximum number of linked %{issuables} (%{limit})."
msgstr ""
-msgid "This %{noteableTypeText} is %{confidentialLinkStart}confidential%{confidentialLinkEnd} and %{lockedLinkStart}locked%{lockedLinkEnd}."
-msgstr ""
-
-msgid "This %{noteableTypeText} is locked."
+msgid "This %{noteableTypeText} is %{confidentialLinkStart}confidential%{confidentialLinkEnd} and its %{lockedLinkStart}discussion is locked%{lockedLinkEnd}."
msgstr ""
msgid "This %{viewer} could not be displayed because %{reason}. You can %{options} instead."
@@ -48571,6 +49171,9 @@ msgstr ""
msgid "This epic does not exist or you don't have sufficient permission."
msgstr ""
+msgid "This feature is only allowed in groups that enable this feature."
+msgstr ""
+
msgid "This feature requires local storage to be enabled"
msgstr ""
@@ -48799,6 +49402,9 @@ msgstr ""
msgid "This link points to external content"
msgstr ""
+msgid "This link will redirect you to %{url}. If this URL looks wrong, please go back or close this window. Do you want to continue?"
+msgstr ""
+
msgid "This may expose confidential information as the selected fork is in another namespace that can have other members."
msgstr ""
@@ -48829,9 +49435,6 @@ msgstr ""
msgid "This merge request is from an internal project to a public project."
msgstr ""
-msgid "This merge request is locked."
-msgstr ""
-
msgid "This merge request was merged. To apply this suggestion, edit this file directly."
msgstr ""
@@ -48856,6 +49459,12 @@ msgstr ""
msgid "This pipeline was created by a schedule."
msgstr ""
+msgid "This pipeline was created by an API call authenticated with a trigger token"
+msgstr ""
+
+msgid "This pipeline was triggered using the api"
+msgstr ""
+
msgid "This process deletes the project repository and all related resources."
msgstr ""
@@ -48991,7 +49600,7 @@ msgstr ""
msgid "This user has the %{access} role in the %{name} project."
msgstr ""
-msgid "This user is currently exempt from phone verification. Remove the exemption using the button below."
+msgid "This user is currently exempt from identity verification. Remove the exemption using the button below."
msgstr ""
msgid "This user is the author of this %{noteable}."
@@ -49000,9 +49609,6 @@ msgstr ""
msgid "This user is the author of this %{workItemType}."
msgstr ""
-msgid "This variable value does not meet the masking requirements."
-msgstr ""
-
msgid "This vulnerability was automatically resolved because its vulnerability type was disabled in this project or removed from GitLab's default ruleset. For details about SAST rule changes, see https://docs.gitlab.com/ee/user/application_security/sast/rules#important-rule-changes."
msgstr ""
@@ -49063,6 +49669,15 @@ msgstr ""
msgid "Time (in hours) that users are allowed to skip forced configuration of two-factor authentication."
msgstr ""
+msgid "Time Display|12-hour: 2:34 PM"
+msgstr ""
+
+msgid "Time Display|24-hour: 14:34"
+msgstr ""
+
+msgid "Time Display|System"
+msgstr ""
+
msgid "Time based: Yes"
msgstr ""
@@ -49451,6 +50066,9 @@ msgstr ""
msgid "To add the entry manually, provide the following details to the application on your phone."
msgstr ""
+msgid "To allow the user to confirm their identity by only confirming an email address and skip phone number and/or credit card verification, create an identity verification exemption using the button below."
+msgstr ""
+
msgid "To approve this merge request, please enter your password. This project requires all approvals to be authenticated."
msgstr ""
@@ -49567,9 +50185,6 @@ msgstr ""
msgid "To remove the %{link_start}read-only%{link_end} state and regain write access, you can reduce the number of users in your top-level group to %{free_limit} users or less. You can also upgrade to a paid tier, which do not have user limits. If you need additional time, you can start a free 30-day trial which includes unlimited users."
msgstr ""
-msgid "To replace phone verification with credit card verification, create a phone verification exemption using the button below."
-msgstr ""
-
msgid "To resolve this, try to:"
msgstr ""
@@ -49870,6 +50485,9 @@ msgstr ""
msgid "Too many users found. Quick actions are limited to at most %{max_count} users"
msgstr ""
+msgid "Tool"
+msgstr ""
+
msgid "TopNav|Explore"
msgstr ""
@@ -49935,9 +50553,6 @@ msgstr ""
msgid "Total Score"
msgstr ""
-msgid "Total Spans"
-msgstr ""
-
msgid "Total cores (CPUs)"
msgstr ""
@@ -49947,6 +50562,9 @@ msgstr ""
msgid "Total memory (GB)"
msgstr ""
+msgid "Total spans"
+msgstr ""
+
msgid "Total test time for all commits/merges"
msgstr ""
@@ -49968,7 +50586,7 @@ msgstr ""
msgid "Trace Details"
msgstr ""
-msgid "Trace Start"
+msgid "Trace start"
msgstr ""
msgid "Tracing"
@@ -49977,6 +50595,12 @@ msgstr ""
msgid "Tracing|%{ms} ms"
msgstr ""
+msgid "Tracing|Attribute"
+msgstr ""
+
+msgid "Tracing|Attributes"
+msgstr ""
+
msgid "Tracing|Check again"
msgstr ""
@@ -49989,25 +50613,19 @@ msgstr ""
msgid "Tracing|Duration (ms)"
msgstr ""
-msgid "Tracing|Enable"
+msgid "Tracing|Error: Failed to load trace details. Try reloading the page."
msgstr ""
-msgid "Tracing|Failed to enable tracing."
+msgid "Tracing|Error: Something went wrong while fetching the operations. Try again."
msgstr ""
-msgid "Tracing|Failed to load page."
-msgstr ""
-
-msgid "Tracing|Failed to load trace details."
+msgid "Tracing|Error: Something went wrong while fetching the services. Try again."
msgstr ""
msgid "Tracing|Failed to load traces."
msgstr ""
-msgid "Tracing|Filter Traces"
-msgstr ""
-
-msgid "Tracing|Get started with Tracing"
+msgid "Tracing|Filter traces"
msgstr ""
msgid "Tracing|Last 1 hour"
@@ -50040,7 +50658,7 @@ msgstr ""
msgid "Tracing|Last 7 days"
msgstr ""
-msgid "Tracing|Monitor your applications with GitLab Distributed Tracing."
+msgid "Tracing|Metadata"
msgstr ""
msgid "Tracing|No traces to display."
@@ -50049,31 +50667,22 @@ msgstr ""
msgid "Tracing|Operation"
msgstr ""
-msgid "Tracing|Select a service to load suggestions"
-msgstr ""
-
-msgid "Tracing|Service"
-msgstr ""
-
-msgid "Tracing|Something went wrong while fetching the operations"
-msgstr ""
-
-msgid "Tracing|Something went wrong while fetching the services"
+msgid "Tracing|Resource attributes"
msgstr ""
-msgid "Tracing|Span Details"
+msgid "Tracing|Select a service to load suggestions"
msgstr ""
-msgid "Tracing|Span ID"
+msgid "Tracing|Service"
msgstr ""
-msgid "Tracing|Status Code"
+msgid "Tracing|Time range"
msgstr ""
-msgid "Tracing|Time range"
+msgid "Tracing|Timestamp"
msgstr ""
-msgid "Tracing|Toggle children spans"
+msgid "Tracing|Toggle child spans"
msgstr ""
msgid "Tracing|Trace ID"
@@ -50085,9 +50694,15 @@ msgstr ""
msgid "Tracing|longer than"
msgstr ""
+msgid "Tracing|name"
+msgstr ""
+
msgid "Tracing|shorter than"
msgstr ""
+msgid "Tracing|value"
+msgstr ""
+
msgid "Track groups of issues that share a theme, across projects and milestones"
msgstr ""
@@ -50324,6 +50939,9 @@ msgstr ""
msgid "Trigger|Trigger description"
msgstr ""
+msgid "Trust user"
+msgstr ""
+
msgid "Trusted"
msgstr ""
@@ -50474,12 +51092,6 @@ msgstr ""
msgid "URL or request ID"
msgstr ""
-msgid "USER %{user_name} WILL BE REMOVED! Are you sure?"
-msgstr ""
-
-msgid "USER WILL BE BLOCKED! Are you sure?"
-msgstr ""
-
msgid "UTC"
msgstr ""
@@ -50633,12 +51245,18 @@ msgstr ""
msgid "Unauthorized to access the cluster agent in this project"
msgstr ""
+msgid "Unauthorized to create a container registry protection rule"
+msgstr ""
+
msgid "Unauthorized to create a package protection rule"
msgstr ""
msgid "Unauthorized to create an environment"
msgstr ""
+msgid "Unauthorized to delete a package protection rule"
+msgstr ""
+
msgid "Unauthorized to update the environment"
msgstr ""
@@ -50738,10 +51356,10 @@ msgstr ""
msgid "Unlock"
msgstr ""
-msgid "Unlock %{issuableDisplayName}"
+msgid "Unlock account"
msgstr ""
-msgid "Unlock account"
+msgid "Unlock discussion"
msgstr ""
msgid "Unlock more features with GitLab Ultimate"
@@ -50750,7 +51368,7 @@ msgstr ""
msgid "Unlock the discussion"
msgstr ""
-msgid "Unlock this %{issuableDisplayName}? %{strongStart}Everyone%{strongEnd} will be able to comment."
+msgid "Unlock this discussion? %{strongStart}Everyone%{strongEnd} will be able to comment."
msgstr ""
msgid "Unlocked"
@@ -50759,7 +51377,7 @@ msgstr ""
msgid "Unlocked the discussion."
msgstr ""
-msgid "Unlocking %{issuableDisplayName}"
+msgid "Unlocking discussion"
msgstr ""
msgid "Unlocks the discussion."
@@ -50780,9 +51398,15 @@ msgstr ""
msgid "Unresolved"
msgstr ""
+msgid "Unresolved discussions must be resolved."
+msgstr ""
+
msgid "Unschedule job"
msgstr ""
+msgid "Unselect"
+msgstr ""
+
msgid "Unselect \"Expand variable reference\" if you want to use the variable value as a raw string."
msgstr ""
@@ -50828,6 +51452,9 @@ msgstr ""
msgid "Untitled"
msgstr ""
+msgid "Untrust user"
+msgstr ""
+
msgid "Unused"
msgstr ""
@@ -50885,9 +51512,6 @@ msgstr ""
msgid "Update username"
msgstr ""
-msgid "Update variable"
-msgstr ""
-
msgid "Update your bookmarked URLs as filtered/sorted branches URL has been changed."
msgstr ""
@@ -50936,6 +51560,9 @@ msgstr ""
msgid "UpdateProject|Pruning unreachable objects can lead to repository corruption."
msgstr ""
+msgid "UpdateProject|Updating default branch is blocked by security policy"
+msgstr ""
+
msgid "UpdateRepositoryStorage|Timeout waiting for %{type} repository pushes"
msgstr ""
@@ -51050,6 +51677,9 @@ msgstr ""
msgid "UsageQuota|%{storage_limit_link_start}A namespace storage limit%{link_end} will soon be enforced for the %{strong_start}%{namespace_name}%{strong_end} namespace. %{extra_message}"
msgstr ""
+msgid "UsageQuota|Any additional purchased storage will be displayed here."
+msgstr ""
+
msgid "UsageQuota|Audio samples, videos, datasets, and graphics."
msgstr ""
@@ -51101,6 +51731,9 @@ msgstr ""
msgid "UsageQuota|Group settings %{gt} Usage quotas"
msgstr ""
+msgid "UsageQuota|How are limits applied?"
+msgstr ""
+
msgid "UsageQuota|Included in %{planName} subscription"
msgstr ""
@@ -51155,7 +51788,13 @@ msgstr ""
msgid "UsageQuota|Precise calculation of Container Registry storage size is delayed because it is too large for synchronous estimation. Precise evaluation will be scheduled within 24 hours."
msgstr ""
-msgid "UsageQuota|Projects under this namespace have %{planLimit} of storage. %{linkStart}How are limits applied?%{linkEnd}"
+msgid "UsageQuota|Product analytics"
+msgstr ""
+
+msgid "UsageQuota|Projects under this namespace have %{planLimit} of storage."
+msgstr ""
+
+msgid "UsageQuota|Purchased storage"
msgstr ""
msgid "UsageQuota|Recalculate repository usage"
@@ -51212,7 +51851,7 @@ msgstr ""
msgid "UsageQuota|The namespace is currently using %{strong_start}%{used_storage}%{strong_end} of namespace storage. View and manage your usage from %{strong_start}%{usage_quotas_nav_instruction}%{strong_end}. %{docs_link_start}Learn more%{link_end} about how to reduce your storage."
msgstr ""
-msgid "UsageQuota|This namespace has %{planLimit} of storage. %{linkStart}How are limits applied?%{linkEnd}"
+msgid "UsageQuota|This namespace has %{planLimit} of storage."
msgstr ""
msgid "UsageQuota|This namespace has no projects which used shared runners in the current period"
@@ -51499,6 +52138,9 @@ msgstr ""
msgid "User %{current_user_username} has started impersonating %{username}"
msgstr ""
+msgid "User %{user_name} will be removed! Are you sure?"
+msgstr ""
+
msgid "User %{username} was successfully removed."
msgstr ""
@@ -51607,6 +52249,15 @@ msgstr ""
msgid "User was successfully updated."
msgstr ""
+msgid "User will be allowed to create possible spam! Are you sure?"
+msgstr ""
+
+msgid "User will be blocked! Are you sure?"
+msgstr ""
+
+msgid "User will not be allowed to create possible spam! Are you sure?"
+msgstr ""
+
msgid "User-based escalation rules must have a user with access to the project"
msgstr ""
@@ -51721,9 +52372,6 @@ msgstr ""
msgid "UserProfile|Contributed projects"
msgstr ""
-msgid "UserProfile|Copy user ID"
-msgstr ""
-
msgid "UserProfile|Copy user ID: %{id}"
msgstr ""
@@ -51826,9 +52474,6 @@ msgstr ""
msgid "UserProfile|User ID copied to clipboard"
msgstr ""
-msgid "UserProfile|User ID: %{id}"
-msgstr ""
-
msgid "UserProfile|User profile navigation"
msgstr ""
@@ -52051,12 +52696,6 @@ msgstr ""
msgid "Value Streams Dashboard | DORA"
msgstr ""
-msgid "Value might contain a variable reference"
-msgstr ""
-
-msgid "Value must meet regular expression requirements to be masked."
-msgstr ""
-
msgid "Value stream"
msgstr ""
@@ -52201,9 +52840,6 @@ msgstr ""
msgid "Variable name '%{variable}' must not start with '%{prefix}'"
msgstr ""
-msgid "Variable value will be evaluated as raw string."
-msgstr ""
-
msgid "Variables"
msgstr ""
@@ -52586,6 +53222,9 @@ msgstr ""
msgid "VulnerabilityExport|CVE"
msgstr ""
+msgid "VulnerabilityExport|CVSS Vectors"
+msgstr ""
+
msgid "VulnerabilityExport|CWE"
msgstr ""
@@ -52661,15 +53300,15 @@ msgstr ""
msgid "VulnerabilityManagement|An unverified non-confirmed finding"
msgstr ""
-msgid "VulnerabilityManagement|Change status"
-msgstr ""
-
msgid "VulnerabilityManagement|Could not process %{issueReference}: %{errorMessage}."
msgstr ""
msgid "VulnerabilityManagement|Create Jira issue"
msgstr ""
+msgid "VulnerabilityManagement|Dismiss as..."
+msgstr ""
+
msgid "VulnerabilityManagement|Enter a name"
msgstr ""
@@ -53468,9 +54107,6 @@ msgstr ""
msgid "Welcome to GitLab,%{br_tag}%{name}!"
msgstr ""
-msgid "Welcome to a new navigation experience"
-msgstr ""
-
msgid "Welcome, %{name}!"
msgstr ""
@@ -53762,9 +54398,6 @@ msgstr ""
msgid "Wiki|Create New Page"
msgstr ""
-msgid "Wiki|Created date"
-msgstr ""
-
msgid "Wiki|Edit Page"
msgstr ""
@@ -53783,9 +54416,6 @@ msgstr ""
msgid "Wiki|The sidebar failed to load. You can reload the page to try again."
msgstr ""
-msgid "Wiki|Title"
-msgstr ""
-
msgid "Wiki|View All Pages"
msgstr ""
@@ -53950,6 +54580,9 @@ msgstr ""
msgid "WorkItem|Due date"
msgstr ""
+msgid "WorkItem|Epic"
+msgstr ""
+
msgid "WorkItem|Existing task"
msgstr ""
@@ -54049,12 +54682,15 @@ msgstr ""
msgid "WorkItem|Save and overwrite"
msgstr ""
-msgid "WorkItem|Search existing %{workItemType}s"
+msgid "WorkItem|Search existing items"
msgstr ""
msgid "WorkItem|Select type"
msgstr ""
+msgid "WorkItem|Show labels"
+msgstr ""
+
msgid "WorkItem|Someone edited the description at the same time you did. If you save it will overwrite their changes. Please confirm you'd like to save your edits."
msgstr ""
@@ -54109,6 +54745,9 @@ msgstr ""
msgid "WorkItem|Something went wrong while fetching milestones. Please try again."
msgstr ""
+msgid "WorkItem|Something went wrong while fetching the %{workItemType}. Please try again."
+msgstr ""
+
msgid "WorkItem|Something went wrong while fetching work item award emojis. Please try again."
msgstr ""
@@ -54163,6 +54802,9 @@ msgstr ""
msgid "WorkItem|This work item is not available. It either doesn't exist or you don't have permission to view it."
msgstr ""
+msgid "WorkItem|Title cannot have more than %{WORK_ITEM_TITLE_MAX_LENGTH} characters."
+msgstr ""
+
msgid "WorkItem|Turn off confidentiality"
msgstr ""
@@ -54693,6 +55335,12 @@ msgstr ""
msgid "You cannot combine replace_ids with add_ids or remove_ids"
msgstr ""
+msgid "You cannot create new projects in your personal namespace because you have reached your personal project limit."
+msgstr ""
+
+msgid "You cannot create projects in your personal namespace. Contact your GitLab administrator."
+msgstr ""
+
msgid "You cannot impersonate a blocked user"
msgstr ""
@@ -54741,6 +55389,9 @@ msgstr ""
msgid "You do not belong to any projects yet."
msgstr ""
+msgid "You do not have access to AI features."
+msgstr ""
+
msgid "You do not have access to any projects for creating incidents."
msgstr ""
@@ -54866,6 +55517,9 @@ msgstr ""
msgid "You have insufficient permissions to create an on-call schedule for this project"
msgstr ""
+msgid "You have insufficient permissions to create organizations"
+msgstr ""
+
msgid "You have insufficient permissions to delete a target branch rule"
msgstr ""
@@ -55040,9 +55694,6 @@ msgstr ""
msgid "You will receive notifications only for comments in which you were @mentioned"
msgstr ""
-msgid "You won't be able to create new projects because you have reached your project limit."
-msgstr ""
-
msgid "You'll be charged for %{true_up_start}users over license%{true_up_end} on a quarterly or annual basis, depending on the terms of your agreement."
msgstr ""
@@ -55052,6 +55703,9 @@ msgstr ""
msgid "You'll need to use different branch names to get a valid comparison."
msgstr ""
+msgid "You're about to leave GitLab"
+msgstr ""
+
msgid "You're about to reduce the visibility of the project %{strong_start}%{project_name}%{strong_end} in %{strong_start}%{group_name}%{strong_end}."
msgstr ""
@@ -55112,6 +55766,9 @@ msgstr ""
msgid "You've already enabled two-factor authentication using one time password authenticators. In order to register a different device, you must first disable two-factor authentication."
msgstr ""
+msgid "You've reached your limit of %{limit} projects created. Contact your GitLab administrator."
+msgstr ""
+
msgid "You've rejected %{user}"
msgstr ""
@@ -55123,6 +55780,9 @@ msgstr[1] ""
msgid "YouTube"
msgstr ""
+msgid "Your %{changes_link} have been committed successfully."
+msgstr ""
+
msgid "Your %{group} membership will now expire in %{days}."
msgstr ""
@@ -55404,9 +56064,6 @@ msgstr ""
msgid "Your profile"
msgstr ""
-msgid "Your project limit is %{limit} projects! Please contact your administrator to increase it"
-msgstr ""
-
msgid "Your projects"
msgstr ""
@@ -55621,6 +56278,9 @@ msgstr ""
msgid "and"
msgstr ""
+msgid "any-approver for the group already exists"
+msgstr ""
+
msgid "any-approver for the merge request already exists"
msgstr ""
@@ -55685,6 +56345,9 @@ msgstr[1] ""
msgid "branch name"
msgstr ""
+msgid "branches"
+msgstr ""
+
msgid "builds"
msgstr ""
@@ -55822,6 +56485,9 @@ msgid_plural "changes"
msgstr[0] ""
msgstr[1] ""
+msgid "changes"
+msgstr ""
+
msgid "check"
msgid_plural "checks"
msgstr[0] ""
@@ -56584,6 +57250,9 @@ msgstr ""
msgid "is not in the group enforcing Group Managed Account"
msgstr ""
+msgid "is not linked to a SAML account or has an inactive SCIM identity. For information on how to resolve this error, see the %{troubleshoot_link_start}troubleshooting SCIM documentation%{troubleshoot_link_end}."
+msgstr ""
+
msgid "is not one of"
msgstr ""
@@ -56895,6 +57564,12 @@ msgstr ""
msgid "mrWidget|Approve additionally"
msgstr ""
+msgid "mrWidget|Approve additionally with SAML"
+msgstr ""
+
+msgid "mrWidget|Approve with SAML"
+msgstr ""
+
msgid "mrWidget|Approved by"
msgstr ""
@@ -56913,6 +57588,9 @@ msgstr ""
msgid "mrWidget|Assign yourself to this issue"
msgstr ""
+msgid "mrWidget|Auto-merge enabled"
+msgstr ""
+
msgid "mrWidget|Cancel auto-merge"
msgstr ""
@@ -56986,6 +57664,12 @@ msgstr ""
msgid "mrWidget|Merged by"
msgstr ""
+msgid "mrWidget|Merging immediately is not recommended. The merged changes could cause pipeline failures on the target branch, and the changes will not be validated against the commits being added by the merge requests currently in the merge train. Read the %{linkStart}documentation%{linkEnd} for more information."
+msgstr ""
+
+msgid "mrWidget|Merging immediately isn't recommended as it may negatively impact the existing merge train. Read the %{linkStart}documentation%{linkEnd} for more information."
+msgstr ""
+
msgid "mrWidget|Please restore it or use a different %{type} branch."
msgstr ""
@@ -57094,6 +57778,18 @@ msgstr ""
msgid "must be after start"
msgstr ""
+msgid "must be an array"
+msgstr ""
+
+msgid "must be an array of CIDR values"
+msgstr ""
+
+msgid "must be an array of hash"
+msgstr ""
+
+msgid "must be an array of hash containing 'allow' attribute of type string"
+msgstr ""
+
msgid "must be an email you have verified"
msgstr ""
@@ -57148,6 +57844,9 @@ msgstr ""
msgid "must contain only a discord user ID."
msgstr ""
+msgid "must contain only a mastodon username."
+msgstr ""
+
msgid "must have a repository"
msgstr ""
@@ -57287,9 +57986,6 @@ msgstr ""
msgid "personal access tokens"
msgstr ""
-msgid "pipeline"
-msgstr ""
-
msgid "pipelineEditorWalkthrough|Let's do this!"
msgstr ""
@@ -57441,6 +58137,9 @@ msgstr ""
msgid "scan-execution-policy: policy not applied, %{policy_path} file is missing"
msgstr ""
+msgid "scheduled"
+msgstr ""
+
msgid "seat"
msgid_plural "seats"
msgstr[0] ""
@@ -57485,6 +58184,9 @@ msgstr ""
msgid "severity|Unknown"
msgstr ""
+msgid "should be a valid NPM package name with optional wildcard characters."
+msgstr ""
+
msgid "should be an array of %{object_name} objects"
msgstr ""
@@ -57509,6 +58211,9 @@ msgstr ""
msgid "sign in"
msgstr ""
+msgid "site"
+msgstr ""
+
msgid "smartcn custom analyzer"
msgstr ""
@@ -57623,6 +58328,9 @@ msgstr ""
msgid "total must be less than or equal to %{size}"
msgstr ""
+msgid "trigger token"
+msgstr ""
+
msgid "triggered"
msgstr ""
diff --git a/metrics_server/metrics_server.rb b/metrics_server/metrics_server.rb
index 7d4968f930c..873489b444e 100644
--- a/metrics_server/metrics_server.rb
+++ b/metrics_server/metrics_server.rb
@@ -88,7 +88,7 @@ class MetricsServer # rubocop:disable Gitlab/NamespacedClass
end
def ensure_valid_target!(target)
- raise "Target must be one of [puma,sidekiq]" unless %w(puma sidekiq).include?(target)
+ raise "Target must be one of [puma,sidekiq]" unless %w[puma sidekiq].include?(target)
end
end
diff --git a/package.json b/package.json
index 3af8e8853e2..773afdd1112 100644
--- a/package.json
+++ b/package.json
@@ -3,9 +3,8 @@
"scripts": {
"check-dependencies": "scripts/frontend/check_dependencies.sh",
"block-dependencies": "node scripts/frontend/block_dependencies.js",
- "check:startup_css": "scripts/frontend/startup_css/startup_css_changed.sh",
"clean": "rm -rf public/assets tmp/cache/*-loader",
- "dev-server": "NODE_OPTIONS=\"--max-old-space-size=5120\" node scripts/frontend/webpack_dev_server.js",
+ "dev-server": "NODE_OPTIONS=\"${NODE_OPTIONS:=--max-old-space-size=5120}\" node scripts/frontend/webpack_dev_server.js",
"file-coverage": "scripts/frontend/file_test_coverage.js",
"lint-docs": "scripts/lint-doc.sh",
"internal:eslint": "eslint --cache --max-warnings 0 --report-unused-disable-directives --ext .js,.vue,.graphql",
@@ -17,6 +16,7 @@
"jest:ci:predictive": "jest --config jest.config.js --ci --coverage --findRelatedTests $(cat $RSPEC_CHANGED_FILES_PATH) $(cat $RSPEC_MATCHING_JS_FILES_PATH) --passWithNoTests --testSequencer ./scripts/frontend/parallel_ci_sequencer.js",
"jest:contract": "PACT_DO_NOT_TRACK=true jest --config jest.config.contract.js --runInBand",
"jest:integration": "jest --config jest.config.integration.js",
+ "jest:scripts": "jest --config jest.config.scripts.js",
"jest:quarantine": "grep -r 'quarantine:' spec/frontend ee/spec/frontend",
"lint:eslint": "node scripts/frontend/eslint.js",
"lint:eslint:fix": "node scripts/frontend/eslint.js --fix",
@@ -33,8 +33,6 @@
"lint:stylelint:fix": "yarn run lint:stylelint --fix",
"lint:stylelint:staged": "scripts/frontend/execute-on-staged-files.sh stylelint '(css|scss)' -q",
"lint:stylelint:staged:fix": "yarn run lint:stylelint:staged --fix",
- "generate:startup_css": "scripts/frontend/startup_css/setup.sh && node scripts/frontend/startup_css/main.js",
- "generate:startup_css:full": "scripts/frontend/startup_css/setup.sh force && node scripts/frontend/startup_css/main.js",
"markdownlint": "markdownlint --config .markdownlint.yml",
"markdownlint:no-trailing-spaces": "markdownlint --config doc/.markdownlint/markdownlint-no-trailing-spaces.yml",
"markdownlint:no-trailing-spaces:fix": "yarn run markdownlint:no-trailing-spaces --fix",
@@ -44,28 +42,27 @@
"storybook:build": "yarn --cwd ./storybook build --quiet",
"storybook:start": "./scripts/frontend/start_storybook.sh",
"swagger:validate": "swagger-cli validate",
- "webpack": "NODE_OPTIONS=\"--max-old-space-size=5120\" webpack --config config/webpack.config.js",
- "webpack-vendor": "NODE_OPTIONS=\"--max-old-space-size=5120\" webpack --config config/webpack.vendor.config.js",
- "webpack-prod": "NODE_OPTIONS=\"--max-old-space-size=5120\" NODE_ENV=production webpack --config config/webpack.config.js"
+ "webpack": "NODE_OPTIONS=\"${NODE_OPTIONS:=--max-old-space-size=5120}\" webpack --config config/webpack.config.js",
+ "webpack-vendor": "NODE_OPTIONS=\"${NODE_OPTIONS:=--max-old-space-size=5120}\" webpack --config config/webpack.vendor.config.js",
+ "webpack-prod": "NODE_OPTIONS=\"${NODE_OPTIONS:=--max-old-space-size=5120}\" NODE_ENV=production webpack --config config/webpack.config.js"
},
"dependencies": {
"@apollo/client": "^3.5.10",
"@babel/core": "^7.18.5",
"@babel/preset-env": "^7.18.2",
- "@cubejs-client/core": "^0.34.0",
- "@cubejs-client/vue": "^0.34.1",
+ "@cubejs-client/core": "^0.34.9",
+ "@cubejs-client/vue": "^0.34.9",
"@floating-ui/dom": "^1.2.9",
- "@gitlab/application-sdk-browser": "^0.2.8",
+ "@gitlab/application-sdk-browser": "^0.2.10",
"@gitlab/at.js": "1.5.7",
- "@gitlab/cluster-client": "^2.0.0",
+ "@gitlab/cluster-client": "^2.1.0",
"@gitlab/favicon-overlay": "2.0.0",
"@gitlab/fonts": "^1.3.0",
- "@gitlab/svgs": "3.66.0",
- "@gitlab/ui": "66.33.0",
+ "@gitlab/svgs": "3.69.0",
+ "@gitlab/ui": "68.2.1",
"@gitlab/visual-review-tools": "1.7.3",
"@gitlab/web-ide": "0.0.1-dev-20231004090414",
"@mattiasbuelens/web-streams-adapter": "^0.1.0",
- "@popperjs/core": "^2.11.2",
"@rails/actioncable": "7.0.8",
"@rails/ujs": "7.0.8",
"@snowplow/browser-plugin-client-hints": "^3.9.0",
@@ -76,41 +73,41 @@
"@snowplow/browser-plugin-timezone": "^3.9.0",
"@snowplow/browser-tracker": "^3.9.0",
"@sourcegraph/code-host-integration": "0.0.91",
- "@tiptap/core": "^2.0.3",
- "@tiptap/extension-blockquote": "^2.0.3",
- "@tiptap/extension-bold": "^2.0.3",
- "@tiptap/extension-bubble-menu": "2.0.3",
- "@tiptap/extension-bullet-list": "^2.0.3",
- "@tiptap/extension-code": "^2.0.3",
- "@tiptap/extension-code-block": "^2.0.3",
- "@tiptap/extension-code-block-lowlight": "2.0.3",
- "@tiptap/extension-document": "^2.0.3",
- "@tiptap/extension-dropcursor": "^2.0.3",
- "@tiptap/extension-gapcursor": "^2.0.3",
- "@tiptap/extension-hard-break": "^2.0.3",
- "@tiptap/extension-heading": "^2.0.3",
- "@tiptap/extension-highlight": "^2.0.3",
- "@tiptap/extension-history": "^2.0.3",
- "@tiptap/extension-horizontal-rule": "^2.0.3",
- "@tiptap/extension-image": "^2.0.3",
- "@tiptap/extension-italic": "^2.0.3",
- "@tiptap/extension-link": "^2.0.3",
- "@tiptap/extension-list-item": "^2.0.3",
- "@tiptap/extension-ordered-list": "^2.0.3",
- "@tiptap/extension-paragraph": "^2.0.3",
- "@tiptap/extension-strike": "^2.0.3",
- "@tiptap/extension-subscript": "^2.0.3",
- "@tiptap/extension-superscript": "^2.0.3",
- "@tiptap/extension-table": "^2.0.3",
- "@tiptap/extension-table-cell": "^2.0.3",
- "@tiptap/extension-table-header": "^2.0.3",
- "@tiptap/extension-table-row": "^2.0.3",
- "@tiptap/extension-task-item": "^2.0.3",
- "@tiptap/extension-task-list": "^2.0.3",
- "@tiptap/extension-text": "^2.0.3",
- "@tiptap/pm": "^2.0.3",
- "@tiptap/suggestion": "^2.0.3",
- "@tiptap/vue-2": "2.0.3",
+ "@tiptap/core": "^2.1.12",
+ "@tiptap/extension-blockquote": "^2.1.12",
+ "@tiptap/extension-bold": "^2.1.12",
+ "@tiptap/extension-bubble-menu": "^2.1.12",
+ "@tiptap/extension-bullet-list": "^2.1.12",
+ "@tiptap/extension-code": "^2.1.12",
+ "@tiptap/extension-code-block": "^2.1.12",
+ "@tiptap/extension-code-block-lowlight": "^2.1.12",
+ "@tiptap/extension-document": "^2.1.12",
+ "@tiptap/extension-dropcursor": "^2.1.12",
+ "@tiptap/extension-gapcursor": "^2.1.12",
+ "@tiptap/extension-hard-break": "^2.1.12",
+ "@tiptap/extension-heading": "^2.1.12",
+ "@tiptap/extension-highlight": "^2.1.12",
+ "@tiptap/extension-history": "^2.1.12",
+ "@tiptap/extension-horizontal-rule": "^2.1.12",
+ "@tiptap/extension-image": "^2.1.12",
+ "@tiptap/extension-italic": "^2.1.12",
+ "@tiptap/extension-link": "^2.1.12",
+ "@tiptap/extension-list-item": "^2.1.12",
+ "@tiptap/extension-ordered-list": "^2.1.12",
+ "@tiptap/extension-paragraph": "^2.1.12",
+ "@tiptap/extension-strike": "^2.1.12",
+ "@tiptap/extension-subscript": "^2.1.12",
+ "@tiptap/extension-superscript": "^2.1.12",
+ "@tiptap/extension-table": "^2.1.12",
+ "@tiptap/extension-table-cell": "^2.1.12",
+ "@tiptap/extension-table-header": "^2.1.12",
+ "@tiptap/extension-table-row": "^2.1.12",
+ "@tiptap/extension-task-item": "^2.1.12",
+ "@tiptap/extension-task-list": "^2.1.12",
+ "@tiptap/extension-text": "^2.1.12",
+ "@tiptap/pm": "^2.1.12",
+ "@tiptap/suggestion": "^2.1.12",
+ "@tiptap/vue-2": "^2.1.12",
"@vue/apollo-components": "^4.0.0-beta.4",
"@vue/apollo-option": "^4.0.0-beta.4",
"apollo-upload-client": "15.0.0",
@@ -126,7 +123,7 @@
"clipboard": "^2.0.8",
"compression-webpack-plugin": "^5.0.2",
"copy-webpack-plugin": "^6.4.1",
- "core-js": "^3.32.2",
+ "core-js": "^3.33.2",
"cron-validator": "^1.1.1",
"cronstrue": "^1.122.0",
"cropper": "^2.3.0",
@@ -148,7 +145,7 @@
"gettext-parser": "^6.0.0",
"graphql": "^15.7.2",
"graphql-tag": "^2.11.0",
- "gridstack": "^9.3.0",
+ "gridstack": "^9.5.0",
"highlight.js": "^11.8.0",
"immer": "^9.0.15",
"ipaddr.js": "^1.9.1",
@@ -167,7 +164,7 @@
"marked-bidi": "^1.0.3",
"mathjax": "3",
"mdurl": "^1.0.1",
- "mermaid": "10.5.0",
+ "mermaid": "10.6.0",
"micromatch": "^4.0.5",
"minimatch": "^3.0.4",
"monaco-editor": "^0.30.1",
@@ -193,12 +190,12 @@
"remark-rehype": "^10.1.0",
"scrollparent": "^2.0.1",
"semver": "^7.3.4",
- "sentrybrowser": "npm:@sentry/browser@7.73.0",
+ "sentrybrowser": "npm:@sentry/browser@7.79.0",
"sentrybrowser5": "npm:@sentry/browser@5.30.0",
"sortablejs": "^1.10.2",
"string-hash": "1.1.3",
"style-loader": "^2.0.0",
- "swagger-ui-dist": "4.12.0",
+ "swagger-ui-dist": "5.9.1",
"thread-loader": "^3.0.4",
"three": "^0.143.0",
"timeago.js": "^4.0.2",
@@ -254,13 +251,12 @@
"axios-mock-adapter": "^1.15.0",
"babel-jest": "^28.1.3",
"chalk": "^2.4.1",
- "cheerio": "^1.0.0-rc.9",
"commander": "^2.20.3",
"custom-jquery-matchers": "^2.1.0",
- "eslint": "8.51.0",
+ "eslint": "8.53.0",
"eslint-import-resolver-jest": "3.0.2",
- "eslint-import-resolver-webpack": "0.13.7",
- "eslint-plugin-import": "^2.28.1",
+ "eslint-import-resolver-webpack": "0.13.8",
+ "eslint-plugin-import": "^2.29.0",
"eslint-plugin-no-jquery": "2.7.0",
"eslint-plugin-no-unsanitized": "^4.0.2",
"fake-indexeddb": "^4.0.1",
@@ -284,15 +280,12 @@
"nodemon": "^2.0.19",
"prettier": "2.2.1",
"prosemirror-test-builder": "^1.1.1",
- "purgecss": "^4.0.3",
- "purgecss-from-html": "^4.0.3",
"sass": "^1.69.0",
"stylelint": "^15.10.2",
"swagger-cli": "^4.0.4",
"timezone-mock": "^1.0.8",
"vite": "^4.4.9",
"vite-plugin-ruby": "^3.2.2",
- "vite-svg-loader": "^3.6.0",
"vue-loader-vue3": "npm:vue-loader@17",
"vue-test-utils-compat": "0.0.14",
"vuex-mock-store": "^0.1.0",
diff --git a/patches/leaflet+1.9.4.patch b/patches/leaflet+1.9.4.patch
new file mode 100644
index 00000000000..5bf2e5448cb
--- /dev/null
+++ b/patches/leaflet+1.9.4.patch
@@ -0,0 +1,28 @@
+diff --git a/node_modules/leaflet/dist/leaflet.css b/node_modules/leaflet/dist/leaflet.css
+index 2961b76..50d36dc 100644
+--- a/node_modules/leaflet/dist/leaflet.css
++++ b/node_modules/leaflet/dist/leaflet.css
+@@ -356,12 +356,12 @@ svg.leaflet-image-layer.leaflet-interactive path {
+ border-radius: 5px;
+ }
+ .leaflet-control-layers-toggle {
+- background-image: url(images/layers.png);
++ background-image: url(./images/layers.png);
+ width: 36px;
+ height: 36px;
+ }
+ .leaflet-retina .leaflet-control-layers-toggle {
+- background-image: url(images/layers-2x.png);
++ background-image: url(./images/layers-2x.png);
+ background-size: 26px 26px;
+ }
+ .leaflet-touch .leaflet-control-layers-toggle {
+@@ -404,7 +404,7 @@ svg.leaflet-image-layer.leaflet-interactive path {
+
+ /* Default icon URLs */
+ .leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */
+- background-image: url(images/marker-icon.png);
++ background-image: url(./images/marker-icon.png);
+ }
+
+
diff --git a/public/-/emojis/3/100.png b/public/-/emojis/3/100.png
new file mode 100644
index 00000000000..3501136c22d
--- /dev/null
+++ b/public/-/emojis/3/100.png
Binary files differ
diff --git a/public/-/emojis/3/1234.png b/public/-/emojis/3/1234.png
new file mode 100644
index 00000000000..8ce28681ebc
--- /dev/null
+++ b/public/-/emojis/3/1234.png
Binary files differ
diff --git a/public/-/emojis/3/8ball.png b/public/-/emojis/3/8ball.png
new file mode 100644
index 00000000000..48207741211
--- /dev/null
+++ b/public/-/emojis/3/8ball.png
Binary files differ
diff --git a/public/-/emojis/3/a.png b/public/-/emojis/3/a.png
new file mode 100644
index 00000000000..6c4875bce2f
--- /dev/null
+++ b/public/-/emojis/3/a.png
Binary files differ
diff --git a/public/-/emojis/3/ab.png b/public/-/emojis/3/ab.png
new file mode 100644
index 00000000000..40c566e63eb
--- /dev/null
+++ b/public/-/emojis/3/ab.png
Binary files differ
diff --git a/public/-/emojis/3/abc.png b/public/-/emojis/3/abc.png
new file mode 100644
index 00000000000..c9a0764b9d6
--- /dev/null
+++ b/public/-/emojis/3/abc.png
Binary files differ
diff --git a/public/-/emojis/3/abcd.png b/public/-/emojis/3/abcd.png
new file mode 100644
index 00000000000..e22fcb79e5a
--- /dev/null
+++ b/public/-/emojis/3/abcd.png
Binary files differ
diff --git a/public/-/emojis/3/accept.png b/public/-/emojis/3/accept.png
new file mode 100644
index 00000000000..0348dded3d8
--- /dev/null
+++ b/public/-/emojis/3/accept.png
Binary files differ
diff --git a/public/-/emojis/3/aerial_tramway.png b/public/-/emojis/3/aerial_tramway.png
new file mode 100644
index 00000000000..ec1cf64f019
--- /dev/null
+++ b/public/-/emojis/3/aerial_tramway.png
Binary files differ
diff --git a/public/-/emojis/3/airplane.png b/public/-/emojis/3/airplane.png
new file mode 100644
index 00000000000..ba6dda4fae3
--- /dev/null
+++ b/public/-/emojis/3/airplane.png
Binary files differ
diff --git a/public/-/emojis/3/airplane_arriving.png b/public/-/emojis/3/airplane_arriving.png
new file mode 100644
index 00000000000..cc83ea662d3
--- /dev/null
+++ b/public/-/emojis/3/airplane_arriving.png
Binary files differ
diff --git a/public/-/emojis/3/airplane_departure.png b/public/-/emojis/3/airplane_departure.png
new file mode 100644
index 00000000000..d72e051c383
--- /dev/null
+++ b/public/-/emojis/3/airplane_departure.png
Binary files differ
diff --git a/public/-/emojis/3/airplane_small.png b/public/-/emojis/3/airplane_small.png
new file mode 100644
index 00000000000..1dc72e20096
--- /dev/null
+++ b/public/-/emojis/3/airplane_small.png
Binary files differ
diff --git a/public/-/emojis/3/alarm_clock.png b/public/-/emojis/3/alarm_clock.png
new file mode 100644
index 00000000000..0509bb4226c
--- /dev/null
+++ b/public/-/emojis/3/alarm_clock.png
Binary files differ
diff --git a/public/-/emojis/3/alembic.png b/public/-/emojis/3/alembic.png
new file mode 100644
index 00000000000..56801316030
--- /dev/null
+++ b/public/-/emojis/3/alembic.png
Binary files differ
diff --git a/public/-/emojis/3/alien.png b/public/-/emojis/3/alien.png
new file mode 100644
index 00000000000..f22de392f55
--- /dev/null
+++ b/public/-/emojis/3/alien.png
Binary files differ
diff --git a/public/-/emojis/3/ambulance.png b/public/-/emojis/3/ambulance.png
new file mode 100644
index 00000000000..fa27a6298b9
--- /dev/null
+++ b/public/-/emojis/3/ambulance.png
Binary files differ
diff --git a/public/-/emojis/3/amphora.png b/public/-/emojis/3/amphora.png
new file mode 100644
index 00000000000..fb0c91f90b3
--- /dev/null
+++ b/public/-/emojis/3/amphora.png
Binary files differ
diff --git a/public/-/emojis/3/anchor.png b/public/-/emojis/3/anchor.png
new file mode 100644
index 00000000000..87123bfda86
--- /dev/null
+++ b/public/-/emojis/3/anchor.png
Binary files differ
diff --git a/public/-/emojis/3/angel.png b/public/-/emojis/3/angel.png
new file mode 100644
index 00000000000..172bab3a431
--- /dev/null
+++ b/public/-/emojis/3/angel.png
Binary files differ
diff --git a/public/-/emojis/3/angel_tone1.png b/public/-/emojis/3/angel_tone1.png
new file mode 100644
index 00000000000..e158cf2577d
--- /dev/null
+++ b/public/-/emojis/3/angel_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/angel_tone2.png b/public/-/emojis/3/angel_tone2.png
new file mode 100644
index 00000000000..4f034d25d47
--- /dev/null
+++ b/public/-/emojis/3/angel_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/angel_tone3.png b/public/-/emojis/3/angel_tone3.png
new file mode 100644
index 00000000000..a5cc52bd6cc
--- /dev/null
+++ b/public/-/emojis/3/angel_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/angel_tone4.png b/public/-/emojis/3/angel_tone4.png
new file mode 100644
index 00000000000..fe7f467844e
--- /dev/null
+++ b/public/-/emojis/3/angel_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/angel_tone5.png b/public/-/emojis/3/angel_tone5.png
new file mode 100644
index 00000000000..8b3c0affb91
--- /dev/null
+++ b/public/-/emojis/3/angel_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/anger.png b/public/-/emojis/3/anger.png
new file mode 100644
index 00000000000..b0466432996
--- /dev/null
+++ b/public/-/emojis/3/anger.png
Binary files differ
diff --git a/public/-/emojis/3/anger_right.png b/public/-/emojis/3/anger_right.png
new file mode 100644
index 00000000000..f79728df54d
--- /dev/null
+++ b/public/-/emojis/3/anger_right.png
Binary files differ
diff --git a/public/-/emojis/3/angry.png b/public/-/emojis/3/angry.png
new file mode 100644
index 00000000000..6004c26ecaf
--- /dev/null
+++ b/public/-/emojis/3/angry.png
Binary files differ
diff --git a/public/-/emojis/3/anguished.png b/public/-/emojis/3/anguished.png
new file mode 100644
index 00000000000..a19ec81e6e5
--- /dev/null
+++ b/public/-/emojis/3/anguished.png
Binary files differ
diff --git a/public/-/emojis/3/ant.png b/public/-/emojis/3/ant.png
new file mode 100644
index 00000000000..74dc0dc8464
--- /dev/null
+++ b/public/-/emojis/3/ant.png
Binary files differ
diff --git a/public/-/emojis/3/apple.png b/public/-/emojis/3/apple.png
new file mode 100644
index 00000000000..4b985bde351
--- /dev/null
+++ b/public/-/emojis/3/apple.png
Binary files differ
diff --git a/public/-/emojis/3/aquarius.png b/public/-/emojis/3/aquarius.png
new file mode 100644
index 00000000000..ea054a8a9e7
--- /dev/null
+++ b/public/-/emojis/3/aquarius.png
Binary files differ
diff --git a/public/-/emojis/3/aries.png b/public/-/emojis/3/aries.png
new file mode 100644
index 00000000000..8b6c08a7552
--- /dev/null
+++ b/public/-/emojis/3/aries.png
Binary files differ
diff --git a/public/-/emojis/3/arrow_backward.png b/public/-/emojis/3/arrow_backward.png
new file mode 100644
index 00000000000..6843100777b
--- /dev/null
+++ b/public/-/emojis/3/arrow_backward.png
Binary files differ
diff --git a/public/-/emojis/3/arrow_double_down.png b/public/-/emojis/3/arrow_double_down.png
new file mode 100644
index 00000000000..d9d05a7bbc7
--- /dev/null
+++ b/public/-/emojis/3/arrow_double_down.png
Binary files differ
diff --git a/public/-/emojis/3/arrow_double_up.png b/public/-/emojis/3/arrow_double_up.png
new file mode 100644
index 00000000000..c351d36485d
--- /dev/null
+++ b/public/-/emojis/3/arrow_double_up.png
Binary files differ
diff --git a/public/-/emojis/3/arrow_down.png b/public/-/emojis/3/arrow_down.png
new file mode 100644
index 00000000000..68a4aea4def
--- /dev/null
+++ b/public/-/emojis/3/arrow_down.png
Binary files differ
diff --git a/public/-/emojis/3/arrow_down_small.png b/public/-/emojis/3/arrow_down_small.png
new file mode 100644
index 00000000000..2c0cf94e73d
--- /dev/null
+++ b/public/-/emojis/3/arrow_down_small.png
Binary files differ
diff --git a/public/-/emojis/3/arrow_forward.png b/public/-/emojis/3/arrow_forward.png
new file mode 100644
index 00000000000..fa4e5e37673
--- /dev/null
+++ b/public/-/emojis/3/arrow_forward.png
Binary files differ
diff --git a/public/-/emojis/3/arrow_heading_down.png b/public/-/emojis/3/arrow_heading_down.png
new file mode 100644
index 00000000000..59717864566
--- /dev/null
+++ b/public/-/emojis/3/arrow_heading_down.png
Binary files differ
diff --git a/public/-/emojis/3/arrow_heading_up.png b/public/-/emojis/3/arrow_heading_up.png
new file mode 100644
index 00000000000..6961a4b3f9c
--- /dev/null
+++ b/public/-/emojis/3/arrow_heading_up.png
Binary files differ
diff --git a/public/-/emojis/3/arrow_left.png b/public/-/emojis/3/arrow_left.png
new file mode 100644
index 00000000000..4fab1e0a87c
--- /dev/null
+++ b/public/-/emojis/3/arrow_left.png
Binary files differ
diff --git a/public/-/emojis/3/arrow_lower_left.png b/public/-/emojis/3/arrow_lower_left.png
new file mode 100644
index 00000000000..7ac710061c7
--- /dev/null
+++ b/public/-/emojis/3/arrow_lower_left.png
Binary files differ
diff --git a/public/-/emojis/3/arrow_lower_right.png b/public/-/emojis/3/arrow_lower_right.png
new file mode 100644
index 00000000000..61dae63f729
--- /dev/null
+++ b/public/-/emojis/3/arrow_lower_right.png
Binary files differ
diff --git a/public/-/emojis/3/arrow_right.png b/public/-/emojis/3/arrow_right.png
new file mode 100644
index 00000000000..778a3575616
--- /dev/null
+++ b/public/-/emojis/3/arrow_right.png
Binary files differ
diff --git a/public/-/emojis/3/arrow_right_hook.png b/public/-/emojis/3/arrow_right_hook.png
new file mode 100644
index 00000000000..bd34a4f5d0f
--- /dev/null
+++ b/public/-/emojis/3/arrow_right_hook.png
Binary files differ
diff --git a/public/-/emojis/3/arrow_up.png b/public/-/emojis/3/arrow_up.png
new file mode 100644
index 00000000000..699e54e296d
--- /dev/null
+++ b/public/-/emojis/3/arrow_up.png
Binary files differ
diff --git a/public/-/emojis/3/arrow_up_down.png b/public/-/emojis/3/arrow_up_down.png
new file mode 100644
index 00000000000..cf355615344
--- /dev/null
+++ b/public/-/emojis/3/arrow_up_down.png
Binary files differ
diff --git a/public/-/emojis/3/arrow_up_small.png b/public/-/emojis/3/arrow_up_small.png
new file mode 100644
index 00000000000..1f0d4e2ee1b
--- /dev/null
+++ b/public/-/emojis/3/arrow_up_small.png
Binary files differ
diff --git a/public/-/emojis/3/arrow_upper_left.png b/public/-/emojis/3/arrow_upper_left.png
new file mode 100644
index 00000000000..4fefca9d4ff
--- /dev/null
+++ b/public/-/emojis/3/arrow_upper_left.png
Binary files differ
diff --git a/public/-/emojis/3/arrow_upper_right.png b/public/-/emojis/3/arrow_upper_right.png
new file mode 100644
index 00000000000..c1439f52943
--- /dev/null
+++ b/public/-/emojis/3/arrow_upper_right.png
Binary files differ
diff --git a/public/-/emojis/3/arrows_clockwise.png b/public/-/emojis/3/arrows_clockwise.png
new file mode 100644
index 00000000000..12bc8184d65
--- /dev/null
+++ b/public/-/emojis/3/arrows_clockwise.png
Binary files differ
diff --git a/public/-/emojis/3/arrows_counterclockwise.png b/public/-/emojis/3/arrows_counterclockwise.png
new file mode 100644
index 00000000000..2a65f636b2b
--- /dev/null
+++ b/public/-/emojis/3/arrows_counterclockwise.png
Binary files differ
diff --git a/public/-/emojis/3/art.png b/public/-/emojis/3/art.png
new file mode 100644
index 00000000000..170e1b2ff97
--- /dev/null
+++ b/public/-/emojis/3/art.png
Binary files differ
diff --git a/public/-/emojis/3/articulated_lorry.png b/public/-/emojis/3/articulated_lorry.png
new file mode 100644
index 00000000000..3a0f4daa029
--- /dev/null
+++ b/public/-/emojis/3/articulated_lorry.png
Binary files differ
diff --git a/public/-/emojis/3/asterisk.png b/public/-/emojis/3/asterisk.png
new file mode 100644
index 00000000000..2f379e50373
--- /dev/null
+++ b/public/-/emojis/3/asterisk.png
Binary files differ
diff --git a/public/-/emojis/3/astonished.png b/public/-/emojis/3/astonished.png
new file mode 100644
index 00000000000..1755a8e51b1
--- /dev/null
+++ b/public/-/emojis/3/astonished.png
Binary files differ
diff --git a/public/-/emojis/3/athletic_shoe.png b/public/-/emojis/3/athletic_shoe.png
new file mode 100644
index 00000000000..3fd62e6d275
--- /dev/null
+++ b/public/-/emojis/3/athletic_shoe.png
Binary files differ
diff --git a/public/-/emojis/3/atm.png b/public/-/emojis/3/atm.png
new file mode 100644
index 00000000000..241d8d13e89
--- /dev/null
+++ b/public/-/emojis/3/atm.png
Binary files differ
diff --git a/public/-/emojis/3/atom.png b/public/-/emojis/3/atom.png
new file mode 100644
index 00000000000..5905490208f
--- /dev/null
+++ b/public/-/emojis/3/atom.png
Binary files differ
diff --git a/public/-/emojis/3/avocado.png b/public/-/emojis/3/avocado.png
new file mode 100644
index 00000000000..9649361f901
--- /dev/null
+++ b/public/-/emojis/3/avocado.png
Binary files differ
diff --git a/public/-/emojis/3/b.png b/public/-/emojis/3/b.png
new file mode 100644
index 00000000000..d72ba8aecf8
--- /dev/null
+++ b/public/-/emojis/3/b.png
Binary files differ
diff --git a/public/-/emojis/3/baby.png b/public/-/emojis/3/baby.png
new file mode 100644
index 00000000000..65ec5c9e920
--- /dev/null
+++ b/public/-/emojis/3/baby.png
Binary files differ
diff --git a/public/-/emojis/3/baby_bottle.png b/public/-/emojis/3/baby_bottle.png
new file mode 100644
index 00000000000..71fa119f5bb
--- /dev/null
+++ b/public/-/emojis/3/baby_bottle.png
Binary files differ
diff --git a/public/-/emojis/3/baby_chick.png b/public/-/emojis/3/baby_chick.png
new file mode 100644
index 00000000000..6a59d81132d
--- /dev/null
+++ b/public/-/emojis/3/baby_chick.png
Binary files differ
diff --git a/public/-/emojis/3/baby_symbol.png b/public/-/emojis/3/baby_symbol.png
new file mode 100644
index 00000000000..e031a83c006
--- /dev/null
+++ b/public/-/emojis/3/baby_symbol.png
Binary files differ
diff --git a/public/-/emojis/3/baby_tone1.png b/public/-/emojis/3/baby_tone1.png
new file mode 100644
index 00000000000..b1b44d162a2
--- /dev/null
+++ b/public/-/emojis/3/baby_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/baby_tone2.png b/public/-/emojis/3/baby_tone2.png
new file mode 100644
index 00000000000..f886dfdd55b
--- /dev/null
+++ b/public/-/emojis/3/baby_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/baby_tone3.png b/public/-/emojis/3/baby_tone3.png
new file mode 100644
index 00000000000..dceede5f0b2
--- /dev/null
+++ b/public/-/emojis/3/baby_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/baby_tone4.png b/public/-/emojis/3/baby_tone4.png
new file mode 100644
index 00000000000..76855978285
--- /dev/null
+++ b/public/-/emojis/3/baby_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/baby_tone5.png b/public/-/emojis/3/baby_tone5.png
new file mode 100644
index 00000000000..9b7a8f3bc04
--- /dev/null
+++ b/public/-/emojis/3/baby_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/back.png b/public/-/emojis/3/back.png
new file mode 100644
index 00000000000..b284d7d9d21
--- /dev/null
+++ b/public/-/emojis/3/back.png
Binary files differ
diff --git a/public/-/emojis/3/bacon.png b/public/-/emojis/3/bacon.png
new file mode 100644
index 00000000000..c3fc3f71ec9
--- /dev/null
+++ b/public/-/emojis/3/bacon.png
Binary files differ
diff --git a/public/-/emojis/3/badminton.png b/public/-/emojis/3/badminton.png
new file mode 100644
index 00000000000..60cdd61a0b0
--- /dev/null
+++ b/public/-/emojis/3/badminton.png
Binary files differ
diff --git a/public/-/emojis/3/baggage_claim.png b/public/-/emojis/3/baggage_claim.png
new file mode 100644
index 00000000000..86138e53281
--- /dev/null
+++ b/public/-/emojis/3/baggage_claim.png
Binary files differ
diff --git a/public/-/emojis/3/balloon.png b/public/-/emojis/3/balloon.png
new file mode 100644
index 00000000000..6baaa25d595
--- /dev/null
+++ b/public/-/emojis/3/balloon.png
Binary files differ
diff --git a/public/-/emojis/3/ballot_box.png b/public/-/emojis/3/ballot_box.png
new file mode 100644
index 00000000000..296a69bea76
--- /dev/null
+++ b/public/-/emojis/3/ballot_box.png
Binary files differ
diff --git a/public/-/emojis/3/ballot_box_with_check.png b/public/-/emojis/3/ballot_box_with_check.png
new file mode 100644
index 00000000000..cd2cfc0c3bf
--- /dev/null
+++ b/public/-/emojis/3/ballot_box_with_check.png
Binary files differ
diff --git a/public/-/emojis/3/bamboo.png b/public/-/emojis/3/bamboo.png
new file mode 100644
index 00000000000..0a49e68d188
--- /dev/null
+++ b/public/-/emojis/3/bamboo.png
Binary files differ
diff --git a/public/-/emojis/3/banana.png b/public/-/emojis/3/banana.png
new file mode 100644
index 00000000000..1ba957e89b6
--- /dev/null
+++ b/public/-/emojis/3/banana.png
Binary files differ
diff --git a/public/-/emojis/3/bangbang.png b/public/-/emojis/3/bangbang.png
new file mode 100644
index 00000000000..005e832fd16
--- /dev/null
+++ b/public/-/emojis/3/bangbang.png
Binary files differ
diff --git a/public/-/emojis/3/bank.png b/public/-/emojis/3/bank.png
new file mode 100644
index 00000000000..9e056f23ca6
--- /dev/null
+++ b/public/-/emojis/3/bank.png
Binary files differ
diff --git a/public/-/emojis/3/bar_chart.png b/public/-/emojis/3/bar_chart.png
new file mode 100644
index 00000000000..9f2638751f0
--- /dev/null
+++ b/public/-/emojis/3/bar_chart.png
Binary files differ
diff --git a/public/-/emojis/3/barber.png b/public/-/emojis/3/barber.png
new file mode 100644
index 00000000000..2bbc3511668
--- /dev/null
+++ b/public/-/emojis/3/barber.png
Binary files differ
diff --git a/public/-/emojis/3/baseball.png b/public/-/emojis/3/baseball.png
new file mode 100644
index 00000000000..a5c0d615728
--- /dev/null
+++ b/public/-/emojis/3/baseball.png
Binary files differ
diff --git a/public/-/emojis/3/basketball.png b/public/-/emojis/3/basketball.png
new file mode 100644
index 00000000000..b91e88f87d4
--- /dev/null
+++ b/public/-/emojis/3/basketball.png
Binary files differ
diff --git a/public/-/emojis/3/basketball_player.png b/public/-/emojis/3/basketball_player.png
new file mode 100644
index 00000000000..8fabefd1d22
--- /dev/null
+++ b/public/-/emojis/3/basketball_player.png
Binary files differ
diff --git a/public/-/emojis/3/basketball_player_tone1.png b/public/-/emojis/3/basketball_player_tone1.png
new file mode 100644
index 00000000000..870f251e92d
--- /dev/null
+++ b/public/-/emojis/3/basketball_player_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/basketball_player_tone2.png b/public/-/emojis/3/basketball_player_tone2.png
new file mode 100644
index 00000000000..644cb8ce6f2
--- /dev/null
+++ b/public/-/emojis/3/basketball_player_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/basketball_player_tone3.png b/public/-/emojis/3/basketball_player_tone3.png
new file mode 100644
index 00000000000..913e7089c7a
--- /dev/null
+++ b/public/-/emojis/3/basketball_player_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/basketball_player_tone4.png b/public/-/emojis/3/basketball_player_tone4.png
new file mode 100644
index 00000000000..330790f88ea
--- /dev/null
+++ b/public/-/emojis/3/basketball_player_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/basketball_player_tone5.png b/public/-/emojis/3/basketball_player_tone5.png
new file mode 100644
index 00000000000..ff79e0bd2e6
--- /dev/null
+++ b/public/-/emojis/3/basketball_player_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/bat.png b/public/-/emojis/3/bat.png
new file mode 100644
index 00000000000..e63cc1cac1f
--- /dev/null
+++ b/public/-/emojis/3/bat.png
Binary files differ
diff --git a/public/-/emojis/3/bath.png b/public/-/emojis/3/bath.png
new file mode 100644
index 00000000000..a3bfe7f1cf9
--- /dev/null
+++ b/public/-/emojis/3/bath.png
Binary files differ
diff --git a/public/-/emojis/3/bath_tone1.png b/public/-/emojis/3/bath_tone1.png
new file mode 100644
index 00000000000..595211ac853
--- /dev/null
+++ b/public/-/emojis/3/bath_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/bath_tone2.png b/public/-/emojis/3/bath_tone2.png
new file mode 100644
index 00000000000..f9441853388
--- /dev/null
+++ b/public/-/emojis/3/bath_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/bath_tone3.png b/public/-/emojis/3/bath_tone3.png
new file mode 100644
index 00000000000..eb70bfc776d
--- /dev/null
+++ b/public/-/emojis/3/bath_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/bath_tone4.png b/public/-/emojis/3/bath_tone4.png
new file mode 100644
index 00000000000..09f6deccbca
--- /dev/null
+++ b/public/-/emojis/3/bath_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/bath_tone5.png b/public/-/emojis/3/bath_tone5.png
new file mode 100644
index 00000000000..60b16f61659
--- /dev/null
+++ b/public/-/emojis/3/bath_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/bathtub.png b/public/-/emojis/3/bathtub.png
new file mode 100644
index 00000000000..501ccae15a7
--- /dev/null
+++ b/public/-/emojis/3/bathtub.png
Binary files differ
diff --git a/public/-/emojis/3/battery.png b/public/-/emojis/3/battery.png
new file mode 100644
index 00000000000..12843f1c95e
--- /dev/null
+++ b/public/-/emojis/3/battery.png
Binary files differ
diff --git a/public/-/emojis/3/beach.png b/public/-/emojis/3/beach.png
new file mode 100644
index 00000000000..a9226e600d3
--- /dev/null
+++ b/public/-/emojis/3/beach.png
Binary files differ
diff --git a/public/-/emojis/3/beach_umbrella.png b/public/-/emojis/3/beach_umbrella.png
new file mode 100644
index 00000000000..5e624c39828
--- /dev/null
+++ b/public/-/emojis/3/beach_umbrella.png
Binary files differ
diff --git a/public/-/emojis/3/bear.png b/public/-/emojis/3/bear.png
new file mode 100644
index 00000000000..acc09b24874
--- /dev/null
+++ b/public/-/emojis/3/bear.png
Binary files differ
diff --git a/public/-/emojis/3/bed.png b/public/-/emojis/3/bed.png
new file mode 100644
index 00000000000..26ced7e343e
--- /dev/null
+++ b/public/-/emojis/3/bed.png
Binary files differ
diff --git a/public/-/emojis/3/bee.png b/public/-/emojis/3/bee.png
new file mode 100644
index 00000000000..969e84a62d8
--- /dev/null
+++ b/public/-/emojis/3/bee.png
Binary files differ
diff --git a/public/-/emojis/3/beer.png b/public/-/emojis/3/beer.png
new file mode 100644
index 00000000000..84b9b596b07
--- /dev/null
+++ b/public/-/emojis/3/beer.png
Binary files differ
diff --git a/public/-/emojis/3/beers.png b/public/-/emojis/3/beers.png
new file mode 100644
index 00000000000..b9f281aa043
--- /dev/null
+++ b/public/-/emojis/3/beers.png
Binary files differ
diff --git a/public/-/emojis/3/beetle.png b/public/-/emojis/3/beetle.png
new file mode 100644
index 00000000000..48a4be009f8
--- /dev/null
+++ b/public/-/emojis/3/beetle.png
Binary files differ
diff --git a/public/-/emojis/3/beginner.png b/public/-/emojis/3/beginner.png
new file mode 100644
index 00000000000..f96db16b96a
--- /dev/null
+++ b/public/-/emojis/3/beginner.png
Binary files differ
diff --git a/public/-/emojis/3/bell.png b/public/-/emojis/3/bell.png
new file mode 100644
index 00000000000..b46ebdb6288
--- /dev/null
+++ b/public/-/emojis/3/bell.png
Binary files differ
diff --git a/public/-/emojis/3/bellhop.png b/public/-/emojis/3/bellhop.png
new file mode 100644
index 00000000000..dbe77931480
--- /dev/null
+++ b/public/-/emojis/3/bellhop.png
Binary files differ
diff --git a/public/-/emojis/3/bento.png b/public/-/emojis/3/bento.png
new file mode 100644
index 00000000000..f68dcb2ea68
--- /dev/null
+++ b/public/-/emojis/3/bento.png
Binary files differ
diff --git a/public/-/emojis/3/bicyclist.png b/public/-/emojis/3/bicyclist.png
new file mode 100644
index 00000000000..b17c6711cda
--- /dev/null
+++ b/public/-/emojis/3/bicyclist.png
Binary files differ
diff --git a/public/-/emojis/3/bicyclist_tone1.png b/public/-/emojis/3/bicyclist_tone1.png
new file mode 100644
index 00000000000..843080335ce
--- /dev/null
+++ b/public/-/emojis/3/bicyclist_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/bicyclist_tone2.png b/public/-/emojis/3/bicyclist_tone2.png
new file mode 100644
index 00000000000..a51965af04f
--- /dev/null
+++ b/public/-/emojis/3/bicyclist_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/bicyclist_tone3.png b/public/-/emojis/3/bicyclist_tone3.png
new file mode 100644
index 00000000000..3a168648f36
--- /dev/null
+++ b/public/-/emojis/3/bicyclist_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/bicyclist_tone4.png b/public/-/emojis/3/bicyclist_tone4.png
new file mode 100644
index 00000000000..cccf1d9daf0
--- /dev/null
+++ b/public/-/emojis/3/bicyclist_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/bicyclist_tone5.png b/public/-/emojis/3/bicyclist_tone5.png
new file mode 100644
index 00000000000..b4ace73d9c5
--- /dev/null
+++ b/public/-/emojis/3/bicyclist_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/bike.png b/public/-/emojis/3/bike.png
new file mode 100644
index 00000000000..93f96307aca
--- /dev/null
+++ b/public/-/emojis/3/bike.png
Binary files differ
diff --git a/public/-/emojis/3/bikini.png b/public/-/emojis/3/bikini.png
new file mode 100644
index 00000000000..e3ec48ceccd
--- /dev/null
+++ b/public/-/emojis/3/bikini.png
Binary files differ
diff --git a/public/-/emojis/3/biohazard.png b/public/-/emojis/3/biohazard.png
new file mode 100644
index 00000000000..38efca6e9be
--- /dev/null
+++ b/public/-/emojis/3/biohazard.png
Binary files differ
diff --git a/public/-/emojis/3/bird.png b/public/-/emojis/3/bird.png
new file mode 100644
index 00000000000..3c24adad7e9
--- /dev/null
+++ b/public/-/emojis/3/bird.png
Binary files differ
diff --git a/public/-/emojis/3/birthday.png b/public/-/emojis/3/birthday.png
new file mode 100644
index 00000000000..079f6b4f6a6
--- /dev/null
+++ b/public/-/emojis/3/birthday.png
Binary files differ
diff --git a/public/-/emojis/3/black_circle.png b/public/-/emojis/3/black_circle.png
new file mode 100644
index 00000000000..aeb256fad50
--- /dev/null
+++ b/public/-/emojis/3/black_circle.png
Binary files differ
diff --git a/public/-/emojis/3/black_heart.png b/public/-/emojis/3/black_heart.png
new file mode 100644
index 00000000000..e03590a7e1b
--- /dev/null
+++ b/public/-/emojis/3/black_heart.png
Binary files differ
diff --git a/public/-/emojis/3/black_joker.png b/public/-/emojis/3/black_joker.png
new file mode 100644
index 00000000000..198f3bf92d4
--- /dev/null
+++ b/public/-/emojis/3/black_joker.png
Binary files differ
diff --git a/public/-/emojis/3/black_large_square.png b/public/-/emojis/3/black_large_square.png
new file mode 100644
index 00000000000..59885c1decf
--- /dev/null
+++ b/public/-/emojis/3/black_large_square.png
Binary files differ
diff --git a/public/-/emojis/3/black_medium_small_square.png b/public/-/emojis/3/black_medium_small_square.png
new file mode 100644
index 00000000000..de0e9a53fd7
--- /dev/null
+++ b/public/-/emojis/3/black_medium_small_square.png
Binary files differ
diff --git a/public/-/emojis/3/black_medium_square.png b/public/-/emojis/3/black_medium_square.png
new file mode 100644
index 00000000000..41e0821fd2c
--- /dev/null
+++ b/public/-/emojis/3/black_medium_square.png
Binary files differ
diff --git a/public/-/emojis/3/black_nib.png b/public/-/emojis/3/black_nib.png
new file mode 100644
index 00000000000..b7142a9af5b
--- /dev/null
+++ b/public/-/emojis/3/black_nib.png
Binary files differ
diff --git a/public/-/emojis/3/black_small_square.png b/public/-/emojis/3/black_small_square.png
new file mode 100644
index 00000000000..f812d0b647f
--- /dev/null
+++ b/public/-/emojis/3/black_small_square.png
Binary files differ
diff --git a/public/-/emojis/3/black_square_button.png b/public/-/emojis/3/black_square_button.png
new file mode 100644
index 00000000000..633c32431cf
--- /dev/null
+++ b/public/-/emojis/3/black_square_button.png
Binary files differ
diff --git a/public/-/emojis/3/blossom.png b/public/-/emojis/3/blossom.png
new file mode 100644
index 00000000000..251f15cb23b
--- /dev/null
+++ b/public/-/emojis/3/blossom.png
Binary files differ
diff --git a/public/-/emojis/3/blowfish.png b/public/-/emojis/3/blowfish.png
new file mode 100644
index 00000000000..4cab2fc8fc6
--- /dev/null
+++ b/public/-/emojis/3/blowfish.png
Binary files differ
diff --git a/public/-/emojis/3/blue_book.png b/public/-/emojis/3/blue_book.png
new file mode 100644
index 00000000000..ca10b2dbf6c
--- /dev/null
+++ b/public/-/emojis/3/blue_book.png
Binary files differ
diff --git a/public/-/emojis/3/blue_car.png b/public/-/emojis/3/blue_car.png
new file mode 100644
index 00000000000..eb63d4d2853
--- /dev/null
+++ b/public/-/emojis/3/blue_car.png
Binary files differ
diff --git a/public/-/emojis/3/blue_heart.png b/public/-/emojis/3/blue_heart.png
new file mode 100644
index 00000000000..9ed47aa4206
--- /dev/null
+++ b/public/-/emojis/3/blue_heart.png
Binary files differ
diff --git a/public/-/emojis/3/blush.png b/public/-/emojis/3/blush.png
new file mode 100644
index 00000000000..2ef1e69832e
--- /dev/null
+++ b/public/-/emojis/3/blush.png
Binary files differ
diff --git a/public/-/emojis/3/boar.png b/public/-/emojis/3/boar.png
new file mode 100644
index 00000000000..584788c3a83
--- /dev/null
+++ b/public/-/emojis/3/boar.png
Binary files differ
diff --git a/public/-/emojis/3/bomb.png b/public/-/emojis/3/bomb.png
new file mode 100644
index 00000000000..c1658f35bd9
--- /dev/null
+++ b/public/-/emojis/3/bomb.png
Binary files differ
diff --git a/public/-/emojis/3/book.png b/public/-/emojis/3/book.png
new file mode 100644
index 00000000000..2ee15e7db43
--- /dev/null
+++ b/public/-/emojis/3/book.png
Binary files differ
diff --git a/public/-/emojis/3/bookmark.png b/public/-/emojis/3/bookmark.png
new file mode 100644
index 00000000000..c80383f64ce
--- /dev/null
+++ b/public/-/emojis/3/bookmark.png
Binary files differ
diff --git a/public/-/emojis/3/bookmark_tabs.png b/public/-/emojis/3/bookmark_tabs.png
new file mode 100644
index 00000000000..fe19b388148
--- /dev/null
+++ b/public/-/emojis/3/bookmark_tabs.png
Binary files differ
diff --git a/public/-/emojis/3/books.png b/public/-/emojis/3/books.png
new file mode 100644
index 00000000000..374a30cfdc6
--- /dev/null
+++ b/public/-/emojis/3/books.png
Binary files differ
diff --git a/public/-/emojis/3/boom.png b/public/-/emojis/3/boom.png
new file mode 100644
index 00000000000..e900ada8694
--- /dev/null
+++ b/public/-/emojis/3/boom.png
Binary files differ
diff --git a/public/-/emojis/3/boot.png b/public/-/emojis/3/boot.png
new file mode 100644
index 00000000000..53ef9c9bf6e
--- /dev/null
+++ b/public/-/emojis/3/boot.png
Binary files differ
diff --git a/public/-/emojis/3/bouquet.png b/public/-/emojis/3/bouquet.png
new file mode 100644
index 00000000000..363c3e4d5b9
--- /dev/null
+++ b/public/-/emojis/3/bouquet.png
Binary files differ
diff --git a/public/-/emojis/3/bow.png b/public/-/emojis/3/bow.png
new file mode 100644
index 00000000000..668e9f32996
--- /dev/null
+++ b/public/-/emojis/3/bow.png
Binary files differ
diff --git a/public/-/emojis/3/bow_and_arrow.png b/public/-/emojis/3/bow_and_arrow.png
new file mode 100644
index 00000000000..9219625875f
--- /dev/null
+++ b/public/-/emojis/3/bow_and_arrow.png
Binary files differ
diff --git a/public/-/emojis/3/bow_tone1.png b/public/-/emojis/3/bow_tone1.png
new file mode 100644
index 00000000000..53d7d5c5ba3
--- /dev/null
+++ b/public/-/emojis/3/bow_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/bow_tone2.png b/public/-/emojis/3/bow_tone2.png
new file mode 100644
index 00000000000..0af738819ae
--- /dev/null
+++ b/public/-/emojis/3/bow_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/bow_tone3.png b/public/-/emojis/3/bow_tone3.png
new file mode 100644
index 00000000000..fbccdadd4b2
--- /dev/null
+++ b/public/-/emojis/3/bow_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/bow_tone4.png b/public/-/emojis/3/bow_tone4.png
new file mode 100644
index 00000000000..18dc42597e4
--- /dev/null
+++ b/public/-/emojis/3/bow_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/bow_tone5.png b/public/-/emojis/3/bow_tone5.png
new file mode 100644
index 00000000000..61c7f3d3771
--- /dev/null
+++ b/public/-/emojis/3/bow_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/bowling.png b/public/-/emojis/3/bowling.png
new file mode 100644
index 00000000000..a0bff897502
--- /dev/null
+++ b/public/-/emojis/3/bowling.png
Binary files differ
diff --git a/public/-/emojis/3/boxing_glove.png b/public/-/emojis/3/boxing_glove.png
new file mode 100644
index 00000000000..cf7ddc9b5d8
--- /dev/null
+++ b/public/-/emojis/3/boxing_glove.png
Binary files differ
diff --git a/public/-/emojis/3/boy.png b/public/-/emojis/3/boy.png
new file mode 100644
index 00000000000..28175e3d73f
--- /dev/null
+++ b/public/-/emojis/3/boy.png
Binary files differ
diff --git a/public/-/emojis/3/boy_tone1.png b/public/-/emojis/3/boy_tone1.png
new file mode 100644
index 00000000000..aa38328d79c
--- /dev/null
+++ b/public/-/emojis/3/boy_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/boy_tone2.png b/public/-/emojis/3/boy_tone2.png
new file mode 100644
index 00000000000..a2f1487c5d4
--- /dev/null
+++ b/public/-/emojis/3/boy_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/boy_tone3.png b/public/-/emojis/3/boy_tone3.png
new file mode 100644
index 00000000000..b69d9ae87dc
--- /dev/null
+++ b/public/-/emojis/3/boy_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/boy_tone4.png b/public/-/emojis/3/boy_tone4.png
new file mode 100644
index 00000000000..ed000edbe8d
--- /dev/null
+++ b/public/-/emojis/3/boy_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/boy_tone5.png b/public/-/emojis/3/boy_tone5.png
new file mode 100644
index 00000000000..664e31af90b
--- /dev/null
+++ b/public/-/emojis/3/boy_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/bread.png b/public/-/emojis/3/bread.png
new file mode 100644
index 00000000000..38b1d37af13
--- /dev/null
+++ b/public/-/emojis/3/bread.png
Binary files differ
diff --git a/public/-/emojis/3/bride_with_veil.png b/public/-/emojis/3/bride_with_veil.png
new file mode 100644
index 00000000000..89048717a9d
--- /dev/null
+++ b/public/-/emojis/3/bride_with_veil.png
Binary files differ
diff --git a/public/-/emojis/3/bride_with_veil_tone1.png b/public/-/emojis/3/bride_with_veil_tone1.png
new file mode 100644
index 00000000000..da3c67c8db1
--- /dev/null
+++ b/public/-/emojis/3/bride_with_veil_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/bride_with_veil_tone2.png b/public/-/emojis/3/bride_with_veil_tone2.png
new file mode 100644
index 00000000000..6b2582a50a5
--- /dev/null
+++ b/public/-/emojis/3/bride_with_veil_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/bride_with_veil_tone3.png b/public/-/emojis/3/bride_with_veil_tone3.png
new file mode 100644
index 00000000000..8162fdb3704
--- /dev/null
+++ b/public/-/emojis/3/bride_with_veil_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/bride_with_veil_tone4.png b/public/-/emojis/3/bride_with_veil_tone4.png
new file mode 100644
index 00000000000..50dfe4f2d8e
--- /dev/null
+++ b/public/-/emojis/3/bride_with_veil_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/bride_with_veil_tone5.png b/public/-/emojis/3/bride_with_veil_tone5.png
new file mode 100644
index 00000000000..5010deb0e7b
--- /dev/null
+++ b/public/-/emojis/3/bride_with_veil_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/bridge_at_night.png b/public/-/emojis/3/bridge_at_night.png
new file mode 100644
index 00000000000..a01e15a9124
--- /dev/null
+++ b/public/-/emojis/3/bridge_at_night.png
Binary files differ
diff --git a/public/-/emojis/3/briefcase.png b/public/-/emojis/3/briefcase.png
new file mode 100644
index 00000000000..9a74a2e42aa
--- /dev/null
+++ b/public/-/emojis/3/briefcase.png
Binary files differ
diff --git a/public/-/emojis/3/broken_heart.png b/public/-/emojis/3/broken_heart.png
new file mode 100644
index 00000000000..b64559dc2e3
--- /dev/null
+++ b/public/-/emojis/3/broken_heart.png
Binary files differ
diff --git a/public/-/emojis/3/bug.png b/public/-/emojis/3/bug.png
new file mode 100644
index 00000000000..921525fa773
--- /dev/null
+++ b/public/-/emojis/3/bug.png
Binary files differ
diff --git a/public/-/emojis/3/bulb.png b/public/-/emojis/3/bulb.png
new file mode 100644
index 00000000000..8581f5fe1ac
--- /dev/null
+++ b/public/-/emojis/3/bulb.png
Binary files differ
diff --git a/public/-/emojis/3/bullettrain_front.png b/public/-/emojis/3/bullettrain_front.png
new file mode 100644
index 00000000000..0367a8331ab
--- /dev/null
+++ b/public/-/emojis/3/bullettrain_front.png
Binary files differ
diff --git a/public/-/emojis/3/bullettrain_side.png b/public/-/emojis/3/bullettrain_side.png
new file mode 100644
index 00000000000..84893313804
--- /dev/null
+++ b/public/-/emojis/3/bullettrain_side.png
Binary files differ
diff --git a/public/-/emojis/3/burrito.png b/public/-/emojis/3/burrito.png
new file mode 100644
index 00000000000..7da1b837e6f
--- /dev/null
+++ b/public/-/emojis/3/burrito.png
Binary files differ
diff --git a/public/-/emojis/3/bus.png b/public/-/emojis/3/bus.png
new file mode 100644
index 00000000000..f0ac86d53c6
--- /dev/null
+++ b/public/-/emojis/3/bus.png
Binary files differ
diff --git a/public/-/emojis/3/busstop.png b/public/-/emojis/3/busstop.png
new file mode 100644
index 00000000000..c92076b7360
--- /dev/null
+++ b/public/-/emojis/3/busstop.png
Binary files differ
diff --git a/public/-/emojis/3/bust_in_silhouette.png b/public/-/emojis/3/bust_in_silhouette.png
new file mode 100644
index 00000000000..da9befccb0f
--- /dev/null
+++ b/public/-/emojis/3/bust_in_silhouette.png
Binary files differ
diff --git a/public/-/emojis/3/busts_in_silhouette.png b/public/-/emojis/3/busts_in_silhouette.png
new file mode 100644
index 00000000000..4e7041a45d4
--- /dev/null
+++ b/public/-/emojis/3/busts_in_silhouette.png
Binary files differ
diff --git a/public/-/emojis/3/butterfly.png b/public/-/emojis/3/butterfly.png
new file mode 100644
index 00000000000..3bc61589ef0
--- /dev/null
+++ b/public/-/emojis/3/butterfly.png
Binary files differ
diff --git a/public/-/emojis/3/cactus.png b/public/-/emojis/3/cactus.png
new file mode 100644
index 00000000000..991fcbf4353
--- /dev/null
+++ b/public/-/emojis/3/cactus.png
Binary files differ
diff --git a/public/-/emojis/3/cake.png b/public/-/emojis/3/cake.png
new file mode 100644
index 00000000000..0301bdea583
--- /dev/null
+++ b/public/-/emojis/3/cake.png
Binary files differ
diff --git a/public/-/emojis/3/calendar.png b/public/-/emojis/3/calendar.png
new file mode 100644
index 00000000000..7627d5c1fca
--- /dev/null
+++ b/public/-/emojis/3/calendar.png
Binary files differ
diff --git a/public/-/emojis/3/calendar_spiral.png b/public/-/emojis/3/calendar_spiral.png
new file mode 100644
index 00000000000..a13a4a5c9d9
--- /dev/null
+++ b/public/-/emojis/3/calendar_spiral.png
Binary files differ
diff --git a/public/-/emojis/3/call_me.png b/public/-/emojis/3/call_me.png
new file mode 100644
index 00000000000..f4feced19fd
--- /dev/null
+++ b/public/-/emojis/3/call_me.png
Binary files differ
diff --git a/public/-/emojis/3/call_me_tone1.png b/public/-/emojis/3/call_me_tone1.png
new file mode 100644
index 00000000000..cbb9b56e70a
--- /dev/null
+++ b/public/-/emojis/3/call_me_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/call_me_tone2.png b/public/-/emojis/3/call_me_tone2.png
new file mode 100644
index 00000000000..da166ecb517
--- /dev/null
+++ b/public/-/emojis/3/call_me_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/call_me_tone3.png b/public/-/emojis/3/call_me_tone3.png
new file mode 100644
index 00000000000..093361f982d
--- /dev/null
+++ b/public/-/emojis/3/call_me_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/call_me_tone4.png b/public/-/emojis/3/call_me_tone4.png
new file mode 100644
index 00000000000..9df57caccf8
--- /dev/null
+++ b/public/-/emojis/3/call_me_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/call_me_tone5.png b/public/-/emojis/3/call_me_tone5.png
new file mode 100644
index 00000000000..acbf2664558
--- /dev/null
+++ b/public/-/emojis/3/call_me_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/calling.png b/public/-/emojis/3/calling.png
new file mode 100644
index 00000000000..d0359eb84da
--- /dev/null
+++ b/public/-/emojis/3/calling.png
Binary files differ
diff --git a/public/-/emojis/3/camel.png b/public/-/emojis/3/camel.png
new file mode 100644
index 00000000000..27169a82dec
--- /dev/null
+++ b/public/-/emojis/3/camel.png
Binary files differ
diff --git a/public/-/emojis/3/camera.png b/public/-/emojis/3/camera.png
new file mode 100644
index 00000000000..8730993c939
--- /dev/null
+++ b/public/-/emojis/3/camera.png
Binary files differ
diff --git a/public/-/emojis/3/camera_with_flash.png b/public/-/emojis/3/camera_with_flash.png
new file mode 100644
index 00000000000..0faf75e29be
--- /dev/null
+++ b/public/-/emojis/3/camera_with_flash.png
Binary files differ
diff --git a/public/-/emojis/3/camping.png b/public/-/emojis/3/camping.png
new file mode 100644
index 00000000000..d69f19211fe
--- /dev/null
+++ b/public/-/emojis/3/camping.png
Binary files differ
diff --git a/public/-/emojis/3/cancer.png b/public/-/emojis/3/cancer.png
new file mode 100644
index 00000000000..77e14405cbe
--- /dev/null
+++ b/public/-/emojis/3/cancer.png
Binary files differ
diff --git a/public/-/emojis/3/candle.png b/public/-/emojis/3/candle.png
new file mode 100644
index 00000000000..b70c3ef6a89
--- /dev/null
+++ b/public/-/emojis/3/candle.png
Binary files differ
diff --git a/public/-/emojis/3/candy.png b/public/-/emojis/3/candy.png
new file mode 100644
index 00000000000..6e7ed35a242
--- /dev/null
+++ b/public/-/emojis/3/candy.png
Binary files differ
diff --git a/public/-/emojis/3/canoe.png b/public/-/emojis/3/canoe.png
new file mode 100644
index 00000000000..848d3575b68
--- /dev/null
+++ b/public/-/emojis/3/canoe.png
Binary files differ
diff --git a/public/-/emojis/3/capital_abcd.png b/public/-/emojis/3/capital_abcd.png
new file mode 100644
index 00000000000..13dc90f6873
--- /dev/null
+++ b/public/-/emojis/3/capital_abcd.png
Binary files differ
diff --git a/public/-/emojis/3/capricorn.png b/public/-/emojis/3/capricorn.png
new file mode 100644
index 00000000000..7f6317677da
--- /dev/null
+++ b/public/-/emojis/3/capricorn.png
Binary files differ
diff --git a/public/-/emojis/3/card_box.png b/public/-/emojis/3/card_box.png
new file mode 100644
index 00000000000..722d7429b6d
--- /dev/null
+++ b/public/-/emojis/3/card_box.png
Binary files differ
diff --git a/public/-/emojis/3/card_index.png b/public/-/emojis/3/card_index.png
new file mode 100644
index 00000000000..c160c49cf02
--- /dev/null
+++ b/public/-/emojis/3/card_index.png
Binary files differ
diff --git a/public/-/emojis/3/carousel_horse.png b/public/-/emojis/3/carousel_horse.png
new file mode 100644
index 00000000000..86d2eeba49b
--- /dev/null
+++ b/public/-/emojis/3/carousel_horse.png
Binary files differ
diff --git a/public/-/emojis/3/carrot.png b/public/-/emojis/3/carrot.png
new file mode 100644
index 00000000000..752f4bbcd1c
--- /dev/null
+++ b/public/-/emojis/3/carrot.png
Binary files differ
diff --git a/public/-/emojis/3/cartwheel.png b/public/-/emojis/3/cartwheel.png
new file mode 100644
index 00000000000..3fbaf1e9ca2
--- /dev/null
+++ b/public/-/emojis/3/cartwheel.png
Binary files differ
diff --git a/public/-/emojis/3/cartwheel_tone1.png b/public/-/emojis/3/cartwheel_tone1.png
new file mode 100644
index 00000000000..297c90ea301
--- /dev/null
+++ b/public/-/emojis/3/cartwheel_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/cartwheel_tone2.png b/public/-/emojis/3/cartwheel_tone2.png
new file mode 100644
index 00000000000..6350424f699
--- /dev/null
+++ b/public/-/emojis/3/cartwheel_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/cartwheel_tone3.png b/public/-/emojis/3/cartwheel_tone3.png
new file mode 100644
index 00000000000..ebb273532f6
--- /dev/null
+++ b/public/-/emojis/3/cartwheel_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/cartwheel_tone4.png b/public/-/emojis/3/cartwheel_tone4.png
new file mode 100644
index 00000000000..57775ff5268
--- /dev/null
+++ b/public/-/emojis/3/cartwheel_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/cartwheel_tone5.png b/public/-/emojis/3/cartwheel_tone5.png
new file mode 100644
index 00000000000..4b0a0ba5a0d
--- /dev/null
+++ b/public/-/emojis/3/cartwheel_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/cat.png b/public/-/emojis/3/cat.png
new file mode 100644
index 00000000000..d0a60fec3db
--- /dev/null
+++ b/public/-/emojis/3/cat.png
Binary files differ
diff --git a/public/-/emojis/3/cat2.png b/public/-/emojis/3/cat2.png
new file mode 100644
index 00000000000..610076eccd7
--- /dev/null
+++ b/public/-/emojis/3/cat2.png
Binary files differ
diff --git a/public/-/emojis/3/cd.png b/public/-/emojis/3/cd.png
new file mode 100644
index 00000000000..db14c01a3c5
--- /dev/null
+++ b/public/-/emojis/3/cd.png
Binary files differ
diff --git a/public/-/emojis/3/chains.png b/public/-/emojis/3/chains.png
new file mode 100644
index 00000000000..8f14d9bebaf
--- /dev/null
+++ b/public/-/emojis/3/chains.png
Binary files differ
diff --git a/public/-/emojis/3/champagne.png b/public/-/emojis/3/champagne.png
new file mode 100644
index 00000000000..fd57382281a
--- /dev/null
+++ b/public/-/emojis/3/champagne.png
Binary files differ
diff --git a/public/-/emojis/3/champagne_glass.png b/public/-/emojis/3/champagne_glass.png
new file mode 100644
index 00000000000..ba9e60d75eb
--- /dev/null
+++ b/public/-/emojis/3/champagne_glass.png
Binary files differ
diff --git a/public/-/emojis/3/chart.png b/public/-/emojis/3/chart.png
new file mode 100644
index 00000000000..c6085e85433
--- /dev/null
+++ b/public/-/emojis/3/chart.png
Binary files differ
diff --git a/public/-/emojis/3/chart_with_downwards_trend.png b/public/-/emojis/3/chart_with_downwards_trend.png
new file mode 100644
index 00000000000..6670585553d
--- /dev/null
+++ b/public/-/emojis/3/chart_with_downwards_trend.png
Binary files differ
diff --git a/public/-/emojis/3/chart_with_upwards_trend.png b/public/-/emojis/3/chart_with_upwards_trend.png
new file mode 100644
index 00000000000..4dd69a0b9e6
--- /dev/null
+++ b/public/-/emojis/3/chart_with_upwards_trend.png
Binary files differ
diff --git a/public/-/emojis/3/checkered_flag.png b/public/-/emojis/3/checkered_flag.png
new file mode 100644
index 00000000000..59f2594ddc5
--- /dev/null
+++ b/public/-/emojis/3/checkered_flag.png
Binary files differ
diff --git a/public/-/emojis/3/cheese.png b/public/-/emojis/3/cheese.png
new file mode 100644
index 00000000000..f5d6b3a7c4c
--- /dev/null
+++ b/public/-/emojis/3/cheese.png
Binary files differ
diff --git a/public/-/emojis/3/cherries.png b/public/-/emojis/3/cherries.png
new file mode 100644
index 00000000000..db09bb5e4b9
--- /dev/null
+++ b/public/-/emojis/3/cherries.png
Binary files differ
diff --git a/public/-/emojis/3/cherry_blossom.png b/public/-/emojis/3/cherry_blossom.png
new file mode 100644
index 00000000000..7f89052791b
--- /dev/null
+++ b/public/-/emojis/3/cherry_blossom.png
Binary files differ
diff --git a/public/-/emojis/3/chestnut.png b/public/-/emojis/3/chestnut.png
new file mode 100644
index 00000000000..04aa25a86c3
--- /dev/null
+++ b/public/-/emojis/3/chestnut.png
Binary files differ
diff --git a/public/-/emojis/3/chicken.png b/public/-/emojis/3/chicken.png
new file mode 100644
index 00000000000..f9aa50248c1
--- /dev/null
+++ b/public/-/emojis/3/chicken.png
Binary files differ
diff --git a/public/-/emojis/3/children_crossing.png b/public/-/emojis/3/children_crossing.png
new file mode 100644
index 00000000000..8549b6ddaad
--- /dev/null
+++ b/public/-/emojis/3/children_crossing.png
Binary files differ
diff --git a/public/-/emojis/3/chipmunk.png b/public/-/emojis/3/chipmunk.png
new file mode 100644
index 00000000000..308550d9e96
--- /dev/null
+++ b/public/-/emojis/3/chipmunk.png
Binary files differ
diff --git a/public/-/emojis/3/chocolate_bar.png b/public/-/emojis/3/chocolate_bar.png
new file mode 100644
index 00000000000..37aac73574b
--- /dev/null
+++ b/public/-/emojis/3/chocolate_bar.png
Binary files differ
diff --git a/public/-/emojis/3/christmas_tree.png b/public/-/emojis/3/christmas_tree.png
new file mode 100644
index 00000000000..3cf4ca07d41
--- /dev/null
+++ b/public/-/emojis/3/christmas_tree.png
Binary files differ
diff --git a/public/-/emojis/3/church.png b/public/-/emojis/3/church.png
new file mode 100644
index 00000000000..a12aabbd66f
--- /dev/null
+++ b/public/-/emojis/3/church.png
Binary files differ
diff --git a/public/-/emojis/3/cinema.png b/public/-/emojis/3/cinema.png
new file mode 100644
index 00000000000..59934f466df
--- /dev/null
+++ b/public/-/emojis/3/cinema.png
Binary files differ
diff --git a/public/-/emojis/3/circus_tent.png b/public/-/emojis/3/circus_tent.png
new file mode 100644
index 00000000000..6c9d9a219e7
--- /dev/null
+++ b/public/-/emojis/3/circus_tent.png
Binary files differ
diff --git a/public/-/emojis/3/city_dusk.png b/public/-/emojis/3/city_dusk.png
new file mode 100644
index 00000000000..8fe2f6e5bca
--- /dev/null
+++ b/public/-/emojis/3/city_dusk.png
Binary files differ
diff --git a/public/-/emojis/3/city_sunset.png b/public/-/emojis/3/city_sunset.png
new file mode 100644
index 00000000000..fcbc2854169
--- /dev/null
+++ b/public/-/emojis/3/city_sunset.png
Binary files differ
diff --git a/public/-/emojis/3/cityscape.png b/public/-/emojis/3/cityscape.png
new file mode 100644
index 00000000000..e9029b2eb2c
--- /dev/null
+++ b/public/-/emojis/3/cityscape.png
Binary files differ
diff --git a/public/-/emojis/3/cl.png b/public/-/emojis/3/cl.png
new file mode 100644
index 00000000000..915f8de08fd
--- /dev/null
+++ b/public/-/emojis/3/cl.png
Binary files differ
diff --git a/public/-/emojis/3/clap.png b/public/-/emojis/3/clap.png
new file mode 100644
index 00000000000..9833ca8f2c6
--- /dev/null
+++ b/public/-/emojis/3/clap.png
Binary files differ
diff --git a/public/-/emojis/3/clap_tone1.png b/public/-/emojis/3/clap_tone1.png
new file mode 100644
index 00000000000..0be85e1c20e
--- /dev/null
+++ b/public/-/emojis/3/clap_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/clap_tone2.png b/public/-/emojis/3/clap_tone2.png
new file mode 100644
index 00000000000..46c31a511a1
--- /dev/null
+++ b/public/-/emojis/3/clap_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/clap_tone3.png b/public/-/emojis/3/clap_tone3.png
new file mode 100644
index 00000000000..1a30dc96fce
--- /dev/null
+++ b/public/-/emojis/3/clap_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/clap_tone4.png b/public/-/emojis/3/clap_tone4.png
new file mode 100644
index 00000000000..de6406e8938
--- /dev/null
+++ b/public/-/emojis/3/clap_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/clap_tone5.png b/public/-/emojis/3/clap_tone5.png
new file mode 100644
index 00000000000..25dd231d55a
--- /dev/null
+++ b/public/-/emojis/3/clap_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/clapper.png b/public/-/emojis/3/clapper.png
new file mode 100644
index 00000000000..09d06f708a7
--- /dev/null
+++ b/public/-/emojis/3/clapper.png
Binary files differ
diff --git a/public/-/emojis/3/classical_building.png b/public/-/emojis/3/classical_building.png
new file mode 100644
index 00000000000..0e6c9bd6e83
--- /dev/null
+++ b/public/-/emojis/3/classical_building.png
Binary files differ
diff --git a/public/-/emojis/3/clipboard.png b/public/-/emojis/3/clipboard.png
new file mode 100644
index 00000000000..e622014de0d
--- /dev/null
+++ b/public/-/emojis/3/clipboard.png
Binary files differ
diff --git a/public/-/emojis/3/clock.png b/public/-/emojis/3/clock.png
new file mode 100644
index 00000000000..a4df2685212
--- /dev/null
+++ b/public/-/emojis/3/clock.png
Binary files differ
diff --git a/public/-/emojis/3/clock1.png b/public/-/emojis/3/clock1.png
new file mode 100644
index 00000000000..b91e15f8816
--- /dev/null
+++ b/public/-/emojis/3/clock1.png
Binary files differ
diff --git a/public/-/emojis/3/clock10.png b/public/-/emojis/3/clock10.png
new file mode 100644
index 00000000000..388f7ba7057
--- /dev/null
+++ b/public/-/emojis/3/clock10.png
Binary files differ
diff --git a/public/-/emojis/3/clock1030.png b/public/-/emojis/3/clock1030.png
new file mode 100644
index 00000000000..f8fd18dd956
--- /dev/null
+++ b/public/-/emojis/3/clock1030.png
Binary files differ
diff --git a/public/-/emojis/3/clock11.png b/public/-/emojis/3/clock11.png
new file mode 100644
index 00000000000..6f468e2a3ce
--- /dev/null
+++ b/public/-/emojis/3/clock11.png
Binary files differ
diff --git a/public/-/emojis/3/clock1130.png b/public/-/emojis/3/clock1130.png
new file mode 100644
index 00000000000..5b02c922ab1
--- /dev/null
+++ b/public/-/emojis/3/clock1130.png
Binary files differ
diff --git a/public/-/emojis/3/clock12.png b/public/-/emojis/3/clock12.png
new file mode 100644
index 00000000000..7580b0e8c33
--- /dev/null
+++ b/public/-/emojis/3/clock12.png
Binary files differ
diff --git a/public/-/emojis/3/clock1230.png b/public/-/emojis/3/clock1230.png
new file mode 100644
index 00000000000..72c8fbeca23
--- /dev/null
+++ b/public/-/emojis/3/clock1230.png
Binary files differ
diff --git a/public/-/emojis/3/clock130.png b/public/-/emojis/3/clock130.png
new file mode 100644
index 00000000000..bf581dfeae6
--- /dev/null
+++ b/public/-/emojis/3/clock130.png
Binary files differ
diff --git a/public/-/emojis/3/clock2.png b/public/-/emojis/3/clock2.png
new file mode 100644
index 00000000000..47dd28d2b00
--- /dev/null
+++ b/public/-/emojis/3/clock2.png
Binary files differ
diff --git a/public/-/emojis/3/clock230.png b/public/-/emojis/3/clock230.png
new file mode 100644
index 00000000000..46f53970329
--- /dev/null
+++ b/public/-/emojis/3/clock230.png
Binary files differ
diff --git a/public/-/emojis/3/clock3.png b/public/-/emojis/3/clock3.png
new file mode 100644
index 00000000000..5ef2bdcd3d2
--- /dev/null
+++ b/public/-/emojis/3/clock3.png
Binary files differ
diff --git a/public/-/emojis/3/clock330.png b/public/-/emojis/3/clock330.png
new file mode 100644
index 00000000000..e2d04b8867d
--- /dev/null
+++ b/public/-/emojis/3/clock330.png
Binary files differ
diff --git a/public/-/emojis/3/clock4.png b/public/-/emojis/3/clock4.png
new file mode 100644
index 00000000000..492b7387c6c
--- /dev/null
+++ b/public/-/emojis/3/clock4.png
Binary files differ
diff --git a/public/-/emojis/3/clock430.png b/public/-/emojis/3/clock430.png
new file mode 100644
index 00000000000..d133e54e43d
--- /dev/null
+++ b/public/-/emojis/3/clock430.png
Binary files differ
diff --git a/public/-/emojis/3/clock5.png b/public/-/emojis/3/clock5.png
new file mode 100644
index 00000000000..56ef3041df2
--- /dev/null
+++ b/public/-/emojis/3/clock5.png
Binary files differ
diff --git a/public/-/emojis/3/clock530.png b/public/-/emojis/3/clock530.png
new file mode 100644
index 00000000000..dc53cc1c261
--- /dev/null
+++ b/public/-/emojis/3/clock530.png
Binary files differ
diff --git a/public/-/emojis/3/clock6.png b/public/-/emojis/3/clock6.png
new file mode 100644
index 00000000000..49641e97294
--- /dev/null
+++ b/public/-/emojis/3/clock6.png
Binary files differ
diff --git a/public/-/emojis/3/clock630.png b/public/-/emojis/3/clock630.png
new file mode 100644
index 00000000000..0f4fd735880
--- /dev/null
+++ b/public/-/emojis/3/clock630.png
Binary files differ
diff --git a/public/-/emojis/3/clock7.png b/public/-/emojis/3/clock7.png
new file mode 100644
index 00000000000..65b554c7b28
--- /dev/null
+++ b/public/-/emojis/3/clock7.png
Binary files differ
diff --git a/public/-/emojis/3/clock730.png b/public/-/emojis/3/clock730.png
new file mode 100644
index 00000000000..ce46b94451f
--- /dev/null
+++ b/public/-/emojis/3/clock730.png
Binary files differ
diff --git a/public/-/emojis/3/clock8.png b/public/-/emojis/3/clock8.png
new file mode 100644
index 00000000000..a838ef2f034
--- /dev/null
+++ b/public/-/emojis/3/clock8.png
Binary files differ
diff --git a/public/-/emojis/3/clock830.png b/public/-/emojis/3/clock830.png
new file mode 100644
index 00000000000..790d19a639f
--- /dev/null
+++ b/public/-/emojis/3/clock830.png
Binary files differ
diff --git a/public/-/emojis/3/clock9.png b/public/-/emojis/3/clock9.png
new file mode 100644
index 00000000000..5b08c8dbd95
--- /dev/null
+++ b/public/-/emojis/3/clock9.png
Binary files differ
diff --git a/public/-/emojis/3/clock930.png b/public/-/emojis/3/clock930.png
new file mode 100644
index 00000000000..ededcf92cba
--- /dev/null
+++ b/public/-/emojis/3/clock930.png
Binary files differ
diff --git a/public/-/emojis/3/closed_book.png b/public/-/emojis/3/closed_book.png
new file mode 100644
index 00000000000..30e95d2d52d
--- /dev/null
+++ b/public/-/emojis/3/closed_book.png
Binary files differ
diff --git a/public/-/emojis/3/closed_lock_with_key.png b/public/-/emojis/3/closed_lock_with_key.png
new file mode 100644
index 00000000000..3a034974455
--- /dev/null
+++ b/public/-/emojis/3/closed_lock_with_key.png
Binary files differ
diff --git a/public/-/emojis/3/closed_umbrella.png b/public/-/emojis/3/closed_umbrella.png
new file mode 100644
index 00000000000..4ae00f07df9
--- /dev/null
+++ b/public/-/emojis/3/closed_umbrella.png
Binary files differ
diff --git a/public/-/emojis/3/cloud.png b/public/-/emojis/3/cloud.png
new file mode 100644
index 00000000000..7f63fa4bf10
--- /dev/null
+++ b/public/-/emojis/3/cloud.png
Binary files differ
diff --git a/public/-/emojis/3/cloud_lightning.png b/public/-/emojis/3/cloud_lightning.png
new file mode 100644
index 00000000000..739d7491473
--- /dev/null
+++ b/public/-/emojis/3/cloud_lightning.png
Binary files differ
diff --git a/public/-/emojis/3/cloud_rain.png b/public/-/emojis/3/cloud_rain.png
new file mode 100644
index 00000000000..ce4aaad1522
--- /dev/null
+++ b/public/-/emojis/3/cloud_rain.png
Binary files differ
diff --git a/public/-/emojis/3/cloud_snow.png b/public/-/emojis/3/cloud_snow.png
new file mode 100644
index 00000000000..6af161f3c5f
--- /dev/null
+++ b/public/-/emojis/3/cloud_snow.png
Binary files differ
diff --git a/public/-/emojis/3/cloud_tornado.png b/public/-/emojis/3/cloud_tornado.png
new file mode 100644
index 00000000000..1480b092e2e
--- /dev/null
+++ b/public/-/emojis/3/cloud_tornado.png
Binary files differ
diff --git a/public/-/emojis/3/clown.png b/public/-/emojis/3/clown.png
new file mode 100644
index 00000000000..311febd6a6b
--- /dev/null
+++ b/public/-/emojis/3/clown.png
Binary files differ
diff --git a/public/-/emojis/3/clubs.png b/public/-/emojis/3/clubs.png
new file mode 100644
index 00000000000..7860b5c6356
--- /dev/null
+++ b/public/-/emojis/3/clubs.png
Binary files differ
diff --git a/public/-/emojis/3/cocktail.png b/public/-/emojis/3/cocktail.png
new file mode 100644
index 00000000000..02680697988
--- /dev/null
+++ b/public/-/emojis/3/cocktail.png
Binary files differ
diff --git a/public/-/emojis/3/coffee.png b/public/-/emojis/3/coffee.png
new file mode 100644
index 00000000000..9117f8a03a5
--- /dev/null
+++ b/public/-/emojis/3/coffee.png
Binary files differ
diff --git a/public/-/emojis/3/coffin.png b/public/-/emojis/3/coffin.png
new file mode 100644
index 00000000000..83f74b23081
--- /dev/null
+++ b/public/-/emojis/3/coffin.png
Binary files differ
diff --git a/public/-/emojis/3/cold_sweat.png b/public/-/emojis/3/cold_sweat.png
new file mode 100644
index 00000000000..81aaf65d30f
--- /dev/null
+++ b/public/-/emojis/3/cold_sweat.png
Binary files differ
diff --git a/public/-/emojis/3/comet.png b/public/-/emojis/3/comet.png
new file mode 100644
index 00000000000..69ec4ecd43a
--- /dev/null
+++ b/public/-/emojis/3/comet.png
Binary files differ
diff --git a/public/-/emojis/3/compression.png b/public/-/emojis/3/compression.png
new file mode 100644
index 00000000000..697af61af91
--- /dev/null
+++ b/public/-/emojis/3/compression.png
Binary files differ
diff --git a/public/-/emojis/3/computer.png b/public/-/emojis/3/computer.png
new file mode 100644
index 00000000000..7f475f39726
--- /dev/null
+++ b/public/-/emojis/3/computer.png
Binary files differ
diff --git a/public/-/emojis/3/confetti_ball.png b/public/-/emojis/3/confetti_ball.png
new file mode 100644
index 00000000000..2bf3de8938a
--- /dev/null
+++ b/public/-/emojis/3/confetti_ball.png
Binary files differ
diff --git a/public/-/emojis/3/confounded.png b/public/-/emojis/3/confounded.png
new file mode 100644
index 00000000000..9410a26ea24
--- /dev/null
+++ b/public/-/emojis/3/confounded.png
Binary files differ
diff --git a/public/-/emojis/3/confused.png b/public/-/emojis/3/confused.png
new file mode 100644
index 00000000000..4dedc86f13d
--- /dev/null
+++ b/public/-/emojis/3/confused.png
Binary files differ
diff --git a/public/-/emojis/3/congratulations.png b/public/-/emojis/3/congratulations.png
new file mode 100644
index 00000000000..6a15c4bc107
--- /dev/null
+++ b/public/-/emojis/3/congratulations.png
Binary files differ
diff --git a/public/-/emojis/3/construction.png b/public/-/emojis/3/construction.png
new file mode 100644
index 00000000000..8b15db797a2
--- /dev/null
+++ b/public/-/emojis/3/construction.png
Binary files differ
diff --git a/public/-/emojis/3/construction_site.png b/public/-/emojis/3/construction_site.png
new file mode 100644
index 00000000000..5d2a18d9f86
--- /dev/null
+++ b/public/-/emojis/3/construction_site.png
Binary files differ
diff --git a/public/-/emojis/3/construction_worker.png b/public/-/emojis/3/construction_worker.png
new file mode 100644
index 00000000000..4ae3426de3a
--- /dev/null
+++ b/public/-/emojis/3/construction_worker.png
Binary files differ
diff --git a/public/-/emojis/3/construction_worker_tone1.png b/public/-/emojis/3/construction_worker_tone1.png
new file mode 100644
index 00000000000..b248134628b
--- /dev/null
+++ b/public/-/emojis/3/construction_worker_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/construction_worker_tone2.png b/public/-/emojis/3/construction_worker_tone2.png
new file mode 100644
index 00000000000..00ea3920275
--- /dev/null
+++ b/public/-/emojis/3/construction_worker_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/construction_worker_tone3.png b/public/-/emojis/3/construction_worker_tone3.png
new file mode 100644
index 00000000000..2dda18a3726
--- /dev/null
+++ b/public/-/emojis/3/construction_worker_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/construction_worker_tone4.png b/public/-/emojis/3/construction_worker_tone4.png
new file mode 100644
index 00000000000..c1eaf70a9f1
--- /dev/null
+++ b/public/-/emojis/3/construction_worker_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/construction_worker_tone5.png b/public/-/emojis/3/construction_worker_tone5.png
new file mode 100644
index 00000000000..9109d5cc838
--- /dev/null
+++ b/public/-/emojis/3/construction_worker_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/control_knobs.png b/public/-/emojis/3/control_knobs.png
new file mode 100644
index 00000000000..fc77d837cfd
--- /dev/null
+++ b/public/-/emojis/3/control_knobs.png
Binary files differ
diff --git a/public/-/emojis/3/convenience_store.png b/public/-/emojis/3/convenience_store.png
new file mode 100644
index 00000000000..687cebbc56d
--- /dev/null
+++ b/public/-/emojis/3/convenience_store.png
Binary files differ
diff --git a/public/-/emojis/3/cookie.png b/public/-/emojis/3/cookie.png
new file mode 100644
index 00000000000..5c562e3d8c3
--- /dev/null
+++ b/public/-/emojis/3/cookie.png
Binary files differ
diff --git a/public/-/emojis/3/cooking.png b/public/-/emojis/3/cooking.png
new file mode 100644
index 00000000000..062ebf83853
--- /dev/null
+++ b/public/-/emojis/3/cooking.png
Binary files differ
diff --git a/public/-/emojis/3/cool.png b/public/-/emojis/3/cool.png
new file mode 100644
index 00000000000..0942e466588
--- /dev/null
+++ b/public/-/emojis/3/cool.png
Binary files differ
diff --git a/public/-/emojis/3/cop.png b/public/-/emojis/3/cop.png
new file mode 100644
index 00000000000..ed4250c9045
--- /dev/null
+++ b/public/-/emojis/3/cop.png
Binary files differ
diff --git a/public/-/emojis/3/cop_tone1.png b/public/-/emojis/3/cop_tone1.png
new file mode 100644
index 00000000000..fd73888b037
--- /dev/null
+++ b/public/-/emojis/3/cop_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/cop_tone2.png b/public/-/emojis/3/cop_tone2.png
new file mode 100644
index 00000000000..e8f401197ef
--- /dev/null
+++ b/public/-/emojis/3/cop_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/cop_tone3.png b/public/-/emojis/3/cop_tone3.png
new file mode 100644
index 00000000000..7f1c923ec1b
--- /dev/null
+++ b/public/-/emojis/3/cop_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/cop_tone4.png b/public/-/emojis/3/cop_tone4.png
new file mode 100644
index 00000000000..f4ca694083d
--- /dev/null
+++ b/public/-/emojis/3/cop_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/cop_tone5.png b/public/-/emojis/3/cop_tone5.png
new file mode 100644
index 00000000000..f9be2181d40
--- /dev/null
+++ b/public/-/emojis/3/cop_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/copyright.png b/public/-/emojis/3/copyright.png
new file mode 100644
index 00000000000..1533c4c61eb
--- /dev/null
+++ b/public/-/emojis/3/copyright.png
Binary files differ
diff --git a/public/-/emojis/3/corn.png b/public/-/emojis/3/corn.png
new file mode 100644
index 00000000000..a2c5befc508
--- /dev/null
+++ b/public/-/emojis/3/corn.png
Binary files differ
diff --git a/public/-/emojis/3/couch.png b/public/-/emojis/3/couch.png
new file mode 100644
index 00000000000..e5f64c537d2
--- /dev/null
+++ b/public/-/emojis/3/couch.png
Binary files differ
diff --git a/public/-/emojis/3/couple.png b/public/-/emojis/3/couple.png
new file mode 100644
index 00000000000..2ed7eb3262a
--- /dev/null
+++ b/public/-/emojis/3/couple.png
Binary files differ
diff --git a/public/-/emojis/3/couple_mm.png b/public/-/emojis/3/couple_mm.png
new file mode 100644
index 00000000000..1adebaa7ff9
--- /dev/null
+++ b/public/-/emojis/3/couple_mm.png
Binary files differ
diff --git a/public/-/emojis/3/couple_with_heart.png b/public/-/emojis/3/couple_with_heart.png
new file mode 100644
index 00000000000..338ffadef7a
--- /dev/null
+++ b/public/-/emojis/3/couple_with_heart.png
Binary files differ
diff --git a/public/-/emojis/3/couple_ww.png b/public/-/emojis/3/couple_ww.png
new file mode 100644
index 00000000000..2bcc1aa527b
--- /dev/null
+++ b/public/-/emojis/3/couple_ww.png
Binary files differ
diff --git a/public/-/emojis/3/couplekiss.png b/public/-/emojis/3/couplekiss.png
new file mode 100644
index 00000000000..1fa3de69028
--- /dev/null
+++ b/public/-/emojis/3/couplekiss.png
Binary files differ
diff --git a/public/-/emojis/3/cow.png b/public/-/emojis/3/cow.png
new file mode 100644
index 00000000000..bb41c5b8327
--- /dev/null
+++ b/public/-/emojis/3/cow.png
Binary files differ
diff --git a/public/-/emojis/3/cow2.png b/public/-/emojis/3/cow2.png
new file mode 100644
index 00000000000..7b22fbf57a2
--- /dev/null
+++ b/public/-/emojis/3/cow2.png
Binary files differ
diff --git a/public/-/emojis/3/cowboy.png b/public/-/emojis/3/cowboy.png
new file mode 100644
index 00000000000..77a8a8f91bc
--- /dev/null
+++ b/public/-/emojis/3/cowboy.png
Binary files differ
diff --git a/public/-/emojis/3/crab.png b/public/-/emojis/3/crab.png
new file mode 100644
index 00000000000..4006a1fac1b
--- /dev/null
+++ b/public/-/emojis/3/crab.png
Binary files differ
diff --git a/public/-/emojis/3/crayon.png b/public/-/emojis/3/crayon.png
new file mode 100644
index 00000000000..a708705dc38
--- /dev/null
+++ b/public/-/emojis/3/crayon.png
Binary files differ
diff --git a/public/-/emojis/3/credit_card.png b/public/-/emojis/3/credit_card.png
new file mode 100644
index 00000000000..a30cf4cea3b
--- /dev/null
+++ b/public/-/emojis/3/credit_card.png
Binary files differ
diff --git a/public/-/emojis/3/crescent_moon.png b/public/-/emojis/3/crescent_moon.png
new file mode 100644
index 00000000000..c53b11e2bfb
--- /dev/null
+++ b/public/-/emojis/3/crescent_moon.png
Binary files differ
diff --git a/public/-/emojis/3/cricket.png b/public/-/emojis/3/cricket.png
new file mode 100644
index 00000000000..e83078dd6f7
--- /dev/null
+++ b/public/-/emojis/3/cricket.png
Binary files differ
diff --git a/public/-/emojis/3/crocodile.png b/public/-/emojis/3/crocodile.png
new file mode 100644
index 00000000000..45eae24995f
--- /dev/null
+++ b/public/-/emojis/3/crocodile.png
Binary files differ
diff --git a/public/-/emojis/3/croissant.png b/public/-/emojis/3/croissant.png
new file mode 100644
index 00000000000..cac52a57ec6
--- /dev/null
+++ b/public/-/emojis/3/croissant.png
Binary files differ
diff --git a/public/-/emojis/3/cross.png b/public/-/emojis/3/cross.png
new file mode 100644
index 00000000000..dcaf713d86a
--- /dev/null
+++ b/public/-/emojis/3/cross.png
Binary files differ
diff --git a/public/-/emojis/3/crossed_flags.png b/public/-/emojis/3/crossed_flags.png
new file mode 100644
index 00000000000..5f8a42cf7ae
--- /dev/null
+++ b/public/-/emojis/3/crossed_flags.png
Binary files differ
diff --git a/public/-/emojis/3/crossed_swords.png b/public/-/emojis/3/crossed_swords.png
new file mode 100644
index 00000000000..849a32fc979
--- /dev/null
+++ b/public/-/emojis/3/crossed_swords.png
Binary files differ
diff --git a/public/-/emojis/3/crown.png b/public/-/emojis/3/crown.png
new file mode 100644
index 00000000000..805b0b48603
--- /dev/null
+++ b/public/-/emojis/3/crown.png
Binary files differ
diff --git a/public/-/emojis/3/cruise_ship.png b/public/-/emojis/3/cruise_ship.png
new file mode 100644
index 00000000000..7c393baf077
--- /dev/null
+++ b/public/-/emojis/3/cruise_ship.png
Binary files differ
diff --git a/public/-/emojis/3/cry.png b/public/-/emojis/3/cry.png
new file mode 100644
index 00000000000..8fcecb30d34
--- /dev/null
+++ b/public/-/emojis/3/cry.png
Binary files differ
diff --git a/public/-/emojis/3/crying_cat_face.png b/public/-/emojis/3/crying_cat_face.png
new file mode 100644
index 00000000000..4ad78aa8ed7
--- /dev/null
+++ b/public/-/emojis/3/crying_cat_face.png
Binary files differ
diff --git a/public/-/emojis/3/crystal_ball.png b/public/-/emojis/3/crystal_ball.png
new file mode 100644
index 00000000000..cf68dfb6704
--- /dev/null
+++ b/public/-/emojis/3/crystal_ball.png
Binary files differ
diff --git a/public/-/emojis/3/cucumber.png b/public/-/emojis/3/cucumber.png
new file mode 100644
index 00000000000..c4f95a74ecb
--- /dev/null
+++ b/public/-/emojis/3/cucumber.png
Binary files differ
diff --git a/public/-/emojis/3/cupid.png b/public/-/emojis/3/cupid.png
new file mode 100644
index 00000000000..a46c194c899
--- /dev/null
+++ b/public/-/emojis/3/cupid.png
Binary files differ
diff --git a/public/-/emojis/3/curly_loop.png b/public/-/emojis/3/curly_loop.png
new file mode 100644
index 00000000000..3d71b69637c
--- /dev/null
+++ b/public/-/emojis/3/curly_loop.png
Binary files differ
diff --git a/public/-/emojis/3/currency_exchange.png b/public/-/emojis/3/currency_exchange.png
new file mode 100644
index 00000000000..bf63ee89a62
--- /dev/null
+++ b/public/-/emojis/3/currency_exchange.png
Binary files differ
diff --git a/public/-/emojis/3/curry.png b/public/-/emojis/3/curry.png
new file mode 100644
index 00000000000..94cb3cea86a
--- /dev/null
+++ b/public/-/emojis/3/curry.png
Binary files differ
diff --git a/public/-/emojis/3/custard.png b/public/-/emojis/3/custard.png
new file mode 100644
index 00000000000..07fc9250fe5
--- /dev/null
+++ b/public/-/emojis/3/custard.png
Binary files differ
diff --git a/public/-/emojis/3/customs.png b/public/-/emojis/3/customs.png
new file mode 100644
index 00000000000..de8b8525889
--- /dev/null
+++ b/public/-/emojis/3/customs.png
Binary files differ
diff --git a/public/-/emojis/3/cyclone.png b/public/-/emojis/3/cyclone.png
new file mode 100644
index 00000000000..10a1ee67512
--- /dev/null
+++ b/public/-/emojis/3/cyclone.png
Binary files differ
diff --git a/public/-/emojis/3/dagger.png b/public/-/emojis/3/dagger.png
new file mode 100644
index 00000000000..7a65ce095d2
--- /dev/null
+++ b/public/-/emojis/3/dagger.png
Binary files differ
diff --git a/public/-/emojis/3/dancer.png b/public/-/emojis/3/dancer.png
new file mode 100644
index 00000000000..701d5dc6a83
--- /dev/null
+++ b/public/-/emojis/3/dancer.png
Binary files differ
diff --git a/public/-/emojis/3/dancer_tone1.png b/public/-/emojis/3/dancer_tone1.png
new file mode 100644
index 00000000000..ad63b590f06
--- /dev/null
+++ b/public/-/emojis/3/dancer_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/dancer_tone2.png b/public/-/emojis/3/dancer_tone2.png
new file mode 100644
index 00000000000..1a28580ead5
--- /dev/null
+++ b/public/-/emojis/3/dancer_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/dancer_tone3.png b/public/-/emojis/3/dancer_tone3.png
new file mode 100644
index 00000000000..e3c35797fef
--- /dev/null
+++ b/public/-/emojis/3/dancer_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/dancer_tone4.png b/public/-/emojis/3/dancer_tone4.png
new file mode 100644
index 00000000000..053a498d4cd
--- /dev/null
+++ b/public/-/emojis/3/dancer_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/dancer_tone5.png b/public/-/emojis/3/dancer_tone5.png
new file mode 100644
index 00000000000..af4758d40c4
--- /dev/null
+++ b/public/-/emojis/3/dancer_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/dancers.png b/public/-/emojis/3/dancers.png
new file mode 100644
index 00000000000..e68ca59ed70
--- /dev/null
+++ b/public/-/emojis/3/dancers.png
Binary files differ
diff --git a/public/-/emojis/3/dango.png b/public/-/emojis/3/dango.png
new file mode 100644
index 00000000000..6a5ed27f7f7
--- /dev/null
+++ b/public/-/emojis/3/dango.png
Binary files differ
diff --git a/public/-/emojis/3/dark_sunglasses.png b/public/-/emojis/3/dark_sunglasses.png
new file mode 100644
index 00000000000..b3c3cbf16fb
--- /dev/null
+++ b/public/-/emojis/3/dark_sunglasses.png
Binary files differ
diff --git a/public/-/emojis/3/dart.png b/public/-/emojis/3/dart.png
new file mode 100644
index 00000000000..297c50e37e6
--- /dev/null
+++ b/public/-/emojis/3/dart.png
Binary files differ
diff --git a/public/-/emojis/3/dash.png b/public/-/emojis/3/dash.png
new file mode 100644
index 00000000000..da03bbdac9d
--- /dev/null
+++ b/public/-/emojis/3/dash.png
Binary files differ
diff --git a/public/-/emojis/3/date.png b/public/-/emojis/3/date.png
new file mode 100644
index 00000000000..9cd8e92e497
--- /dev/null
+++ b/public/-/emojis/3/date.png
Binary files differ
diff --git a/public/-/emojis/3/deciduous_tree.png b/public/-/emojis/3/deciduous_tree.png
new file mode 100644
index 00000000000..c61ab068531
--- /dev/null
+++ b/public/-/emojis/3/deciduous_tree.png
Binary files differ
diff --git a/public/-/emojis/3/deer.png b/public/-/emojis/3/deer.png
new file mode 100644
index 00000000000..8c7cd98546f
--- /dev/null
+++ b/public/-/emojis/3/deer.png
Binary files differ
diff --git a/public/-/emojis/3/department_store.png b/public/-/emojis/3/department_store.png
new file mode 100644
index 00000000000..548cba49523
--- /dev/null
+++ b/public/-/emojis/3/department_store.png
Binary files differ
diff --git a/public/-/emojis/3/desert.png b/public/-/emojis/3/desert.png
new file mode 100644
index 00000000000..35f2adbafb4
--- /dev/null
+++ b/public/-/emojis/3/desert.png
Binary files differ
diff --git a/public/-/emojis/3/desktop.png b/public/-/emojis/3/desktop.png
new file mode 100644
index 00000000000..8de01a76ecb
--- /dev/null
+++ b/public/-/emojis/3/desktop.png
Binary files differ
diff --git a/public/-/emojis/3/diamond_shape_with_a_dot_inside.png b/public/-/emojis/3/diamond_shape_with_a_dot_inside.png
new file mode 100644
index 00000000000..33616cd496d
--- /dev/null
+++ b/public/-/emojis/3/diamond_shape_with_a_dot_inside.png
Binary files differ
diff --git a/public/-/emojis/3/diamonds.png b/public/-/emojis/3/diamonds.png
new file mode 100644
index 00000000000..9c1d0fb2551
--- /dev/null
+++ b/public/-/emojis/3/diamonds.png
Binary files differ
diff --git a/public/-/emojis/3/disappointed.png b/public/-/emojis/3/disappointed.png
new file mode 100644
index 00000000000..58f85314d73
--- /dev/null
+++ b/public/-/emojis/3/disappointed.png
Binary files differ
diff --git a/public/-/emojis/3/disappointed_relieved.png b/public/-/emojis/3/disappointed_relieved.png
new file mode 100644
index 00000000000..eec72338eef
--- /dev/null
+++ b/public/-/emojis/3/disappointed_relieved.png
Binary files differ
diff --git a/public/-/emojis/3/dividers.png b/public/-/emojis/3/dividers.png
new file mode 100644
index 00000000000..a90c2fd8ff6
--- /dev/null
+++ b/public/-/emojis/3/dividers.png
Binary files differ
diff --git a/public/-/emojis/3/dizzy.png b/public/-/emojis/3/dizzy.png
new file mode 100644
index 00000000000..6bdcd9e5584
--- /dev/null
+++ b/public/-/emojis/3/dizzy.png
Binary files differ
diff --git a/public/-/emojis/3/dizzy_face.png b/public/-/emojis/3/dizzy_face.png
new file mode 100644
index 00000000000..8a8836963da
--- /dev/null
+++ b/public/-/emojis/3/dizzy_face.png
Binary files differ
diff --git a/public/-/emojis/3/do_not_litter.png b/public/-/emojis/3/do_not_litter.png
new file mode 100644
index 00000000000..1582c789df8
--- /dev/null
+++ b/public/-/emojis/3/do_not_litter.png
Binary files differ
diff --git a/public/-/emojis/3/dog.png b/public/-/emojis/3/dog.png
new file mode 100644
index 00000000000..261ee82429d
--- /dev/null
+++ b/public/-/emojis/3/dog.png
Binary files differ
diff --git a/public/-/emojis/3/dog2.png b/public/-/emojis/3/dog2.png
new file mode 100644
index 00000000000..c1c9bd911a3
--- /dev/null
+++ b/public/-/emojis/3/dog2.png
Binary files differ
diff --git a/public/-/emojis/3/dollar.png b/public/-/emojis/3/dollar.png
new file mode 100644
index 00000000000..dc0ffc7580e
--- /dev/null
+++ b/public/-/emojis/3/dollar.png
Binary files differ
diff --git a/public/-/emojis/3/dolls.png b/public/-/emojis/3/dolls.png
new file mode 100644
index 00000000000..7e0ac5e0ff5
--- /dev/null
+++ b/public/-/emojis/3/dolls.png
Binary files differ
diff --git a/public/-/emojis/3/dolphin.png b/public/-/emojis/3/dolphin.png
new file mode 100644
index 00000000000..135c1ff635f
--- /dev/null
+++ b/public/-/emojis/3/dolphin.png
Binary files differ
diff --git a/public/-/emojis/3/door.png b/public/-/emojis/3/door.png
new file mode 100644
index 00000000000..9f97e75007f
--- /dev/null
+++ b/public/-/emojis/3/door.png
Binary files differ
diff --git a/public/-/emojis/3/doughnut.png b/public/-/emojis/3/doughnut.png
new file mode 100644
index 00000000000..12fff3c0d52
--- /dev/null
+++ b/public/-/emojis/3/doughnut.png
Binary files differ
diff --git a/public/-/emojis/3/dove.png b/public/-/emojis/3/dove.png
new file mode 100644
index 00000000000..2a4392ec711
--- /dev/null
+++ b/public/-/emojis/3/dove.png
Binary files differ
diff --git a/public/-/emojis/3/dragon.png b/public/-/emojis/3/dragon.png
new file mode 100644
index 00000000000..0373699122e
--- /dev/null
+++ b/public/-/emojis/3/dragon.png
Binary files differ
diff --git a/public/-/emojis/3/dragon_face.png b/public/-/emojis/3/dragon_face.png
new file mode 100644
index 00000000000..1184d6f1a10
--- /dev/null
+++ b/public/-/emojis/3/dragon_face.png
Binary files differ
diff --git a/public/-/emojis/3/dress.png b/public/-/emojis/3/dress.png
new file mode 100644
index 00000000000..b8eacb7d177
--- /dev/null
+++ b/public/-/emojis/3/dress.png
Binary files differ
diff --git a/public/-/emojis/3/dromedary_camel.png b/public/-/emojis/3/dromedary_camel.png
new file mode 100644
index 00000000000..bad07051604
--- /dev/null
+++ b/public/-/emojis/3/dromedary_camel.png
Binary files differ
diff --git a/public/-/emojis/3/drooling_face.png b/public/-/emojis/3/drooling_face.png
new file mode 100644
index 00000000000..ad883534d30
--- /dev/null
+++ b/public/-/emojis/3/drooling_face.png
Binary files differ
diff --git a/public/-/emojis/3/droplet.png b/public/-/emojis/3/droplet.png
new file mode 100644
index 00000000000..5cfce842955
--- /dev/null
+++ b/public/-/emojis/3/droplet.png
Binary files differ
diff --git a/public/-/emojis/3/drum.png b/public/-/emojis/3/drum.png
new file mode 100644
index 00000000000..43a3b0cb1ce
--- /dev/null
+++ b/public/-/emojis/3/drum.png
Binary files differ
diff --git a/public/-/emojis/3/duck.png b/public/-/emojis/3/duck.png
new file mode 100644
index 00000000000..1126d456cb0
--- /dev/null
+++ b/public/-/emojis/3/duck.png
Binary files differ
diff --git a/public/-/emojis/3/dvd.png b/public/-/emojis/3/dvd.png
new file mode 100644
index 00000000000..b3b00ecfdcf
--- /dev/null
+++ b/public/-/emojis/3/dvd.png
Binary files differ
diff --git a/public/-/emojis/3/e-mail.png b/public/-/emojis/3/e-mail.png
new file mode 100644
index 00000000000..ce99dd7a163
--- /dev/null
+++ b/public/-/emojis/3/e-mail.png
Binary files differ
diff --git a/public/-/emojis/3/eagle.png b/public/-/emojis/3/eagle.png
new file mode 100644
index 00000000000..9e799f596a5
--- /dev/null
+++ b/public/-/emojis/3/eagle.png
Binary files differ
diff --git a/public/-/emojis/3/ear.png b/public/-/emojis/3/ear.png
new file mode 100644
index 00000000000..29aaf9ea164
--- /dev/null
+++ b/public/-/emojis/3/ear.png
Binary files differ
diff --git a/public/-/emojis/3/ear_of_rice.png b/public/-/emojis/3/ear_of_rice.png
new file mode 100644
index 00000000000..5424d157bc3
--- /dev/null
+++ b/public/-/emojis/3/ear_of_rice.png
Binary files differ
diff --git a/public/-/emojis/3/ear_tone1.png b/public/-/emojis/3/ear_tone1.png
new file mode 100644
index 00000000000..dee81b5f6ce
--- /dev/null
+++ b/public/-/emojis/3/ear_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/ear_tone2.png b/public/-/emojis/3/ear_tone2.png
new file mode 100644
index 00000000000..06b9835ea65
--- /dev/null
+++ b/public/-/emojis/3/ear_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/ear_tone3.png b/public/-/emojis/3/ear_tone3.png
new file mode 100644
index 00000000000..3e0ac0befd3
--- /dev/null
+++ b/public/-/emojis/3/ear_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/ear_tone4.png b/public/-/emojis/3/ear_tone4.png
new file mode 100644
index 00000000000..42398620022
--- /dev/null
+++ b/public/-/emojis/3/ear_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/ear_tone5.png b/public/-/emojis/3/ear_tone5.png
new file mode 100644
index 00000000000..de97241e30c
--- /dev/null
+++ b/public/-/emojis/3/ear_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/earth_africa.png b/public/-/emojis/3/earth_africa.png
new file mode 100644
index 00000000000..11b57568561
--- /dev/null
+++ b/public/-/emojis/3/earth_africa.png
Binary files differ
diff --git a/public/-/emojis/3/earth_americas.png b/public/-/emojis/3/earth_americas.png
new file mode 100644
index 00000000000..9f80abddcf7
--- /dev/null
+++ b/public/-/emojis/3/earth_americas.png
Binary files differ
diff --git a/public/-/emojis/3/earth_asia.png b/public/-/emojis/3/earth_asia.png
new file mode 100644
index 00000000000..199705875b0
--- /dev/null
+++ b/public/-/emojis/3/earth_asia.png
Binary files differ
diff --git a/public/-/emojis/3/egg.png b/public/-/emojis/3/egg.png
new file mode 100644
index 00000000000..201929d6119
--- /dev/null
+++ b/public/-/emojis/3/egg.png
Binary files differ
diff --git a/public/-/emojis/3/eggplant.png b/public/-/emojis/3/eggplant.png
new file mode 100644
index 00000000000..bb21a8e6f2e
--- /dev/null
+++ b/public/-/emojis/3/eggplant.png
Binary files differ
diff --git a/public/-/emojis/3/eight.png b/public/-/emojis/3/eight.png
new file mode 100644
index 00000000000..d220764a928
--- /dev/null
+++ b/public/-/emojis/3/eight.png
Binary files differ
diff --git a/public/-/emojis/3/eight_pointed_black_star.png b/public/-/emojis/3/eight_pointed_black_star.png
new file mode 100644
index 00000000000..78ad05b8bed
--- /dev/null
+++ b/public/-/emojis/3/eight_pointed_black_star.png
Binary files differ
diff --git a/public/-/emojis/3/eight_spoked_asterisk.png b/public/-/emojis/3/eight_spoked_asterisk.png
new file mode 100644
index 00000000000..fd95e20ba88
--- /dev/null
+++ b/public/-/emojis/3/eight_spoked_asterisk.png
Binary files differ
diff --git a/public/-/emojis/3/eject.png b/public/-/emojis/3/eject.png
new file mode 100644
index 00000000000..9037b30f5fd
--- /dev/null
+++ b/public/-/emojis/3/eject.png
Binary files differ
diff --git a/public/-/emojis/3/electric_plug.png b/public/-/emojis/3/electric_plug.png
new file mode 100644
index 00000000000..8942ea456ea
--- /dev/null
+++ b/public/-/emojis/3/electric_plug.png
Binary files differ
diff --git a/public/-/emojis/3/elephant.png b/public/-/emojis/3/elephant.png
new file mode 100644
index 00000000000..d25e5e96fb5
--- /dev/null
+++ b/public/-/emojis/3/elephant.png
Binary files differ
diff --git a/public/-/emojis/3/emojis.json b/public/-/emojis/3/emojis.json
new file mode 100644
index 00000000000..4f2bb51d303
--- /dev/null
+++ b/public/-/emojis/3/emojis.json
@@ -0,0 +1,12560 @@
+[
+ {
+ "n": "grinning",
+ "c": "people",
+ "e": "😀",
+ "d": "grinning face",
+ "u": "6.1"
+ },
+ {
+ "n": "smiley",
+ "c": "people",
+ "e": "😃",
+ "d": "smiling face with open mouth",
+ "u": "6.0"
+ },
+ {
+ "n": "smile",
+ "c": "people",
+ "e": "😄",
+ "d": "smiling face with open mouth and smiling eyes",
+ "u": "6.0"
+ },
+ {
+ "n": "grin",
+ "c": "people",
+ "e": "😁",
+ "d": "grinning face with smiling eyes",
+ "u": "6.0"
+ },
+ {
+ "n": "laughing",
+ "c": "people",
+ "e": "😆",
+ "d": "smiling face with open mouth and tightly-closed ey",
+ "u": "6.0"
+ },
+ {
+ "n": "sweat_smile",
+ "c": "people",
+ "e": "😅",
+ "d": "smiling face with open mouth and cold sweat",
+ "u": "6.0"
+ },
+ {
+ "n": "rofl",
+ "c": "people",
+ "e": "🤣",
+ "d": "rolling on the floor laughing",
+ "u": "9.0"
+ },
+ {
+ "n": "joy",
+ "c": "people",
+ "e": "😂",
+ "d": "face with tears of joy",
+ "u": "6.0"
+ },
+ {
+ "n": "slight_smile",
+ "c": "people",
+ "e": "🙂",
+ "d": "slightly smiling face",
+ "u": "7.0"
+ },
+ {
+ "n": "upside_down",
+ "c": "people",
+ "e": "🙃",
+ "d": "upside-down face",
+ "u": "8.0"
+ },
+ {
+ "n": "wink",
+ "c": "people",
+ "e": "😉",
+ "d": "winking face",
+ "u": "6.0"
+ },
+ {
+ "n": "blush",
+ "c": "people",
+ "e": "😊",
+ "d": "smiling face with smiling eyes",
+ "u": "6.0"
+ },
+ {
+ "n": "innocent",
+ "c": "people",
+ "e": "😇",
+ "d": "smiling face with halo",
+ "u": "6.0"
+ },
+ {
+ "n": "heart_eyes",
+ "c": "people",
+ "e": "😍",
+ "d": "smiling face with heart-shaped eyes",
+ "u": "6.0"
+ },
+ {
+ "n": "kissing_heart",
+ "c": "people",
+ "e": "😘",
+ "d": "face throwing a kiss",
+ "u": "6.0"
+ },
+ {
+ "n": "kissing",
+ "c": "people",
+ "e": "😗",
+ "d": "kissing face",
+ "u": "6.1"
+ },
+ {
+ "n": "relaxed",
+ "c": "people",
+ "e": "☺",
+ "d": "white smiling face",
+ "u": "1.1"
+ },
+ {
+ "n": "kissing_closed_eyes",
+ "c": "people",
+ "e": "😚",
+ "d": "kissing face with closed eyes",
+ "u": "6.0"
+ },
+ {
+ "n": "kissing_smiling_eyes",
+ "c": "people",
+ "e": "😙",
+ "d": "kissing face with smiling eyes",
+ "u": "6.1"
+ },
+ {
+ "n": "yum",
+ "c": "people",
+ "e": "😋",
+ "d": "face savouring delicious food",
+ "u": "6.0"
+ },
+ {
+ "n": "stuck_out_tongue",
+ "c": "people",
+ "e": "😛",
+ "d": "face with stuck-out tongue",
+ "u": "6.1"
+ },
+ {
+ "n": "stuck_out_tongue_winking_eye",
+ "c": "people",
+ "e": "😜",
+ "d": "face with stuck-out tongue and winking eye",
+ "u": "6.0"
+ },
+ {
+ "n": "stuck_out_tongue_closed_eyes",
+ "c": "people",
+ "e": "😝",
+ "d": "face with stuck-out tongue and tightly-closed eyes",
+ "u": "6.0"
+ },
+ {
+ "n": "money_mouth",
+ "c": "people",
+ "e": "🤑",
+ "d": "money-mouth face",
+ "u": "8.0"
+ },
+ {
+ "n": "hugging",
+ "c": "people",
+ "e": "🤗",
+ "d": "hugging face",
+ "u": "8.0"
+ },
+ {
+ "n": "thinking",
+ "c": "people",
+ "e": "🤔",
+ "d": "thinking face",
+ "u": "8.0"
+ },
+ {
+ "n": "zipper_mouth",
+ "c": "people",
+ "e": "🤐",
+ "d": "zipper-mouth face",
+ "u": "8.0"
+ },
+ {
+ "n": "neutral_face",
+ "c": "people",
+ "e": "😐",
+ "d": "neutral face",
+ "u": "6.0"
+ },
+ {
+ "n": "expressionless",
+ "c": "people",
+ "e": "😑",
+ "d": "expressionless face",
+ "u": "6.1"
+ },
+ {
+ "n": "no_mouth",
+ "c": "people",
+ "e": "😶",
+ "d": "face without mouth",
+ "u": "6.0"
+ },
+ {
+ "n": "smirk",
+ "c": "people",
+ "e": "😏",
+ "d": "smirking face",
+ "u": "6.0"
+ },
+ {
+ "n": "unamused",
+ "c": "people",
+ "e": "😒",
+ "d": "unamused face",
+ "u": "6.0"
+ },
+ {
+ "n": "rolling_eyes",
+ "c": "people",
+ "e": "🙄",
+ "d": "face with rolling eyes",
+ "u": "8.0"
+ },
+ {
+ "n": "grimacing",
+ "c": "people",
+ "e": "😬",
+ "d": "grimacing face",
+ "u": "6.1"
+ },
+ {
+ "n": "lying_face",
+ "c": "people",
+ "e": "🤥",
+ "d": "lying face",
+ "u": "9.0"
+ },
+ {
+ "n": "relieved",
+ "c": "people",
+ "e": "😌",
+ "d": "relieved face",
+ "u": "6.0"
+ },
+ {
+ "n": "pensive",
+ "c": "people",
+ "e": "😔",
+ "d": "pensive face",
+ "u": "6.0"
+ },
+ {
+ "n": "sleepy",
+ "c": "people",
+ "e": "😪",
+ "d": "sleepy face",
+ "u": "6.0"
+ },
+ {
+ "n": "drooling_face",
+ "c": "people",
+ "e": "🤤",
+ "d": "drooling face",
+ "u": "9.0"
+ },
+ {
+ "n": "sleeping",
+ "c": "people",
+ "e": "😴",
+ "d": "sleeping face",
+ "u": "6.1"
+ },
+ {
+ "n": "mask",
+ "c": "people",
+ "e": "😷",
+ "d": "face with medical mask",
+ "u": "6.0"
+ },
+ {
+ "n": "thermometer_face",
+ "c": "people",
+ "e": "🤒",
+ "d": "face with thermometer",
+ "u": "8.0"
+ },
+ {
+ "n": "head_bandage",
+ "c": "people",
+ "e": "🤕",
+ "d": "face with head-bandage",
+ "u": "8.0"
+ },
+ {
+ "n": "nauseated_face",
+ "c": "people",
+ "e": "🤢",
+ "d": "nauseated face",
+ "u": "9.0"
+ },
+ {
+ "n": "sneezing_face",
+ "c": "people",
+ "e": "🤧",
+ "d": "sneezing face",
+ "u": "9.0"
+ },
+ {
+ "n": "dizzy_face",
+ "c": "people",
+ "e": "😵",
+ "d": "dizzy face",
+ "u": "6.0"
+ },
+ {
+ "n": "cowboy",
+ "c": "people",
+ "e": "🤠",
+ "d": "face with cowboy hat",
+ "u": "9.0"
+ },
+ {
+ "n": "sunglasses",
+ "c": "people",
+ "e": "😎",
+ "d": "smiling face with sunglasses",
+ "u": "6.0"
+ },
+ {
+ "n": "nerd",
+ "c": "people",
+ "e": "🤓",
+ "d": "nerd face",
+ "u": "8.0"
+ },
+ {
+ "n": "confused",
+ "c": "people",
+ "e": "😕",
+ "d": "confused face",
+ "u": "6.1"
+ },
+ {
+ "n": "worried",
+ "c": "people",
+ "e": "😟",
+ "d": "worried face",
+ "u": "6.1"
+ },
+ {
+ "n": "slight_frown",
+ "c": "people",
+ "e": "🙁",
+ "d": "slightly frowning face",
+ "u": "7.0"
+ },
+ {
+ "n": "frowning2",
+ "c": "people",
+ "e": "☹",
+ "d": "white frowning face",
+ "u": "1.1"
+ },
+ {
+ "n": "open_mouth",
+ "c": "people",
+ "e": "😮",
+ "d": "face with open mouth",
+ "u": "6.1"
+ },
+ {
+ "n": "hushed",
+ "c": "people",
+ "e": "😯",
+ "d": "hushed face",
+ "u": "6.1"
+ },
+ {
+ "n": "astonished",
+ "c": "people",
+ "e": "😲",
+ "d": "astonished face",
+ "u": "6.0"
+ },
+ {
+ "n": "flushed",
+ "c": "people",
+ "e": "😳",
+ "d": "flushed face",
+ "u": "6.0"
+ },
+ {
+ "n": "frowning",
+ "c": "people",
+ "e": "😦",
+ "d": "frowning face with open mouth",
+ "u": "6.1"
+ },
+ {
+ "n": "anguished",
+ "c": "people",
+ "e": "😧",
+ "d": "anguished face",
+ "u": "6.1"
+ },
+ {
+ "n": "fearful",
+ "c": "people",
+ "e": "😨",
+ "d": "fearful face",
+ "u": "6.0"
+ },
+ {
+ "n": "cold_sweat",
+ "c": "people",
+ "e": "😰",
+ "d": "face with open mouth and cold sweat",
+ "u": "6.0"
+ },
+ {
+ "n": "disappointed_relieved",
+ "c": "people",
+ "e": "😥",
+ "d": "disappointed but relieved face",
+ "u": "6.0"
+ },
+ {
+ "n": "cry",
+ "c": "people",
+ "e": "😢",
+ "d": "crying face",
+ "u": "6.0"
+ },
+ {
+ "n": "sob",
+ "c": "people",
+ "e": "😭",
+ "d": "loudly crying face",
+ "u": "6.0"
+ },
+ {
+ "n": "scream",
+ "c": "people",
+ "e": "😱",
+ "d": "face screaming in fear",
+ "u": "6.0"
+ },
+ {
+ "n": "confounded",
+ "c": "people",
+ "e": "😖",
+ "d": "confounded face",
+ "u": "6.0"
+ },
+ {
+ "n": "persevere",
+ "c": "people",
+ "e": "😣",
+ "d": "persevering face",
+ "u": "6.0"
+ },
+ {
+ "n": "disappointed",
+ "c": "people",
+ "e": "😞",
+ "d": "disappointed face",
+ "u": "6.0"
+ },
+ {
+ "n": "sweat",
+ "c": "people",
+ "e": "😓",
+ "d": "face with cold sweat",
+ "u": "6.0"
+ },
+ {
+ "n": "weary",
+ "c": "people",
+ "e": "😩",
+ "d": "weary face",
+ "u": "6.0"
+ },
+ {
+ "n": "tired_face",
+ "c": "people",
+ "e": "😫",
+ "d": "tired face",
+ "u": "6.0"
+ },
+ {
+ "n": "triumph",
+ "c": "people",
+ "e": "😤",
+ "d": "face with look of triumph",
+ "u": "6.0"
+ },
+ {
+ "n": "rage",
+ "c": "people",
+ "e": "😡",
+ "d": "pouting face",
+ "u": "6.0"
+ },
+ {
+ "n": "angry",
+ "c": "people",
+ "e": "😠",
+ "d": "angry face",
+ "u": "6.0"
+ },
+ {
+ "n": "smiling_imp",
+ "c": "people",
+ "e": "😈",
+ "d": "smiling face with horns",
+ "u": "6.0"
+ },
+ {
+ "n": "imp",
+ "c": "people",
+ "e": "👿",
+ "d": "imp",
+ "u": "6.0"
+ },
+ {
+ "n": "skull",
+ "c": "people",
+ "e": "💀",
+ "d": "skull",
+ "u": "6.0"
+ },
+ {
+ "n": "skull_crossbones",
+ "c": "objects",
+ "e": "☠",
+ "d": "skull and crossbones",
+ "u": "1.1"
+ },
+ {
+ "n": "poop",
+ "c": "people",
+ "e": "💩",
+ "d": "pile of poo",
+ "u": "6.0"
+ },
+ {
+ "n": "clown",
+ "c": "people",
+ "e": "🤡",
+ "d": "clown face",
+ "u": "9.0"
+ },
+ {
+ "n": "japanese_ogre",
+ "c": "people",
+ "e": "👹",
+ "d": "japanese ogre",
+ "u": "6.0"
+ },
+ {
+ "n": "japanese_goblin",
+ "c": "people",
+ "e": "👺",
+ "d": "japanese goblin",
+ "u": "6.0"
+ },
+ {
+ "n": "ghost",
+ "c": "people",
+ "e": "👻",
+ "d": "ghost",
+ "u": "6.0"
+ },
+ {
+ "n": "alien",
+ "c": "people",
+ "e": "👽",
+ "d": "extraterrestrial alien",
+ "u": "6.0"
+ },
+ {
+ "n": "space_invader",
+ "c": "activity",
+ "e": "👾",
+ "d": "alien monster",
+ "u": "6.0"
+ },
+ {
+ "n": "robot",
+ "c": "people",
+ "e": "🤖",
+ "d": "robot face",
+ "u": "8.0"
+ },
+ {
+ "n": "smiley_cat",
+ "c": "people",
+ "e": "😺",
+ "d": "smiling cat face with open mouth",
+ "u": "6.0"
+ },
+ {
+ "n": "smile_cat",
+ "c": "people",
+ "e": "😸",
+ "d": "grinning cat face with smiling eyes",
+ "u": "6.0"
+ },
+ {
+ "n": "joy_cat",
+ "c": "people",
+ "e": "😹",
+ "d": "cat face with tears of joy",
+ "u": "6.0"
+ },
+ {
+ "n": "heart_eyes_cat",
+ "c": "people",
+ "e": "😻",
+ "d": "smiling cat face with heart-shaped eyes",
+ "u": "6.0"
+ },
+ {
+ "n": "smirk_cat",
+ "c": "people",
+ "e": "😼",
+ "d": "cat face with wry smile",
+ "u": "6.0"
+ },
+ {
+ "n": "kissing_cat",
+ "c": "people",
+ "e": "😽",
+ "d": "kissing cat face with closed eyes",
+ "u": "6.0"
+ },
+ {
+ "n": "scream_cat",
+ "c": "people",
+ "e": "🙀",
+ "d": "weary cat face",
+ "u": "6.0"
+ },
+ {
+ "n": "crying_cat_face",
+ "c": "people",
+ "e": "😿",
+ "d": "crying cat face",
+ "u": "6.0"
+ },
+ {
+ "n": "pouting_cat",
+ "c": "people",
+ "e": "😾",
+ "d": "pouting cat face",
+ "u": "6.0"
+ },
+ {
+ "n": "see_no_evil",
+ "c": "nature",
+ "e": "🙈",
+ "d": "see-no-evil monkey",
+ "u": "6.0"
+ },
+ {
+ "n": "hear_no_evil",
+ "c": "nature",
+ "e": "🙉",
+ "d": "hear-no-evil monkey",
+ "u": "6.0"
+ },
+ {
+ "n": "speak_no_evil",
+ "c": "nature",
+ "e": "🙊",
+ "d": "speak-no-evil monkey",
+ "u": "6.0"
+ },
+ {
+ "n": "love_letter",
+ "c": "objects",
+ "e": "💌",
+ "d": "love letter",
+ "u": "6.0"
+ },
+ {
+ "n": "cupid",
+ "c": "symbols",
+ "e": "💘",
+ "d": "heart with arrow",
+ "u": "6.0"
+ },
+ {
+ "n": "gift_heart",
+ "c": "symbols",
+ "e": "💝",
+ "d": "heart with ribbon",
+ "u": "6.0"
+ },
+ {
+ "n": "sparkling_heart",
+ "c": "symbols",
+ "e": "💖",
+ "d": "sparkling heart",
+ "u": "6.0"
+ },
+ {
+ "n": "heartpulse",
+ "c": "symbols",
+ "e": "💗",
+ "d": "growing heart",
+ "u": "6.0"
+ },
+ {
+ "n": "heartbeat",
+ "c": "symbols",
+ "e": "💓",
+ "d": "beating heart",
+ "u": "6.0"
+ },
+ {
+ "n": "revolving_hearts",
+ "c": "symbols",
+ "e": "💞",
+ "d": "revolving hearts",
+ "u": "6.0"
+ },
+ {
+ "n": "two_hearts",
+ "c": "symbols",
+ "e": "💕",
+ "d": "two hearts",
+ "u": "6.0"
+ },
+ {
+ "n": "heart_decoration",
+ "c": "symbols",
+ "e": "💟",
+ "d": "heart decoration",
+ "u": "6.0"
+ },
+ {
+ "n": "heart_exclamation",
+ "c": "symbols",
+ "e": "❣",
+ "d": "heavy heart exclamation mark ornament",
+ "u": "1.1"
+ },
+ {
+ "n": "broken_heart",
+ "c": "symbols",
+ "e": "💔",
+ "d": "broken heart",
+ "u": "6.0"
+ },
+ {
+ "n": "heart",
+ "c": "symbols",
+ "e": "❤",
+ "d": "heavy black heart",
+ "u": "1.1"
+ },
+ {
+ "n": "yellow_heart",
+ "c": "symbols",
+ "e": "💛",
+ "d": "yellow heart",
+ "u": "6.0"
+ },
+ {
+ "n": "green_heart",
+ "c": "symbols",
+ "e": "💚",
+ "d": "green heart",
+ "u": "6.0"
+ },
+ {
+ "n": "blue_heart",
+ "c": "symbols",
+ "e": "💙",
+ "d": "blue heart",
+ "u": "6.0"
+ },
+ {
+ "n": "purple_heart",
+ "c": "symbols",
+ "e": "💜",
+ "d": "purple heart",
+ "u": "6.0"
+ },
+ {
+ "n": "black_heart",
+ "c": "symbols",
+ "e": "🖤",
+ "d": "black heart",
+ "u": "9.0"
+ },
+ {
+ "n": "kiss",
+ "c": "people",
+ "e": "💋",
+ "d": "kiss mark",
+ "u": "6.0"
+ },
+ {
+ "n": "100",
+ "c": "symbols",
+ "e": "💯",
+ "d": "hundred points symbol",
+ "u": "6.0"
+ },
+ {
+ "n": "anger",
+ "c": "symbols",
+ "e": "💢",
+ "d": "anger symbol",
+ "u": "6.0"
+ },
+ {
+ "n": "boom",
+ "c": "nature",
+ "e": "💥",
+ "d": "collision symbol",
+ "u": "6.0"
+ },
+ {
+ "n": "dizzy",
+ "c": "nature",
+ "e": "💫",
+ "d": "dizzy symbol",
+ "u": "6.0"
+ },
+ {
+ "n": "sweat_drops",
+ "c": "nature",
+ "e": "💦",
+ "d": "splashing sweat symbol",
+ "u": "6.0"
+ },
+ {
+ "n": "dash",
+ "c": "nature",
+ "e": "💨",
+ "d": "dash symbol",
+ "u": "6.0"
+ },
+ {
+ "n": "hole",
+ "c": "objects",
+ "e": "🕳",
+ "d": "hole",
+ "u": "7.0"
+ },
+ {
+ "n": "speech_balloon",
+ "c": "symbols",
+ "e": "💬",
+ "d": "speech balloon",
+ "u": "6.0"
+ },
+ {
+ "n": "eye_in_speech_bubble",
+ "c": "symbols",
+ "e": "👁‍🗨",
+ "d": "eye in speech bubble",
+ "u": "7.0"
+ },
+ {
+ "n": "speech_left",
+ "c": "symbols",
+ "e": "🗨",
+ "d": "left speech bubble",
+ "u": "7.0"
+ },
+ {
+ "n": "anger_right",
+ "c": "symbols",
+ "e": "🗯",
+ "d": "right anger bubble",
+ "u": "7.0"
+ },
+ {
+ "n": "thought_balloon",
+ "c": "symbols",
+ "e": "💭",
+ "d": "thought balloon",
+ "u": "6.0"
+ },
+ {
+ "n": "zzz",
+ "c": "people",
+ "e": "💤",
+ "d": "sleeping symbol",
+ "u": "6.0"
+ },
+ {
+ "n": "wave",
+ "c": "people",
+ "e": "👋",
+ "d": "waving hand sign",
+ "u": "6.0"
+ },
+ {
+ "n": "wave_tone1",
+ "c": "people",
+ "e": "👋🏻",
+ "d": "waving hand sign tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "wave_tone2",
+ "c": "people",
+ "e": "👋🏼",
+ "d": "waving hand sign tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "wave_tone3",
+ "c": "people",
+ "e": "👋🏽",
+ "d": "waving hand sign tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "wave_tone4",
+ "c": "people",
+ "e": "👋🏾",
+ "d": "waving hand sign tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "wave_tone5",
+ "c": "people",
+ "e": "👋🏿",
+ "d": "waving hand sign tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "raised_back_of_hand",
+ "c": "people",
+ "e": "🤚",
+ "d": "raised back of hand",
+ "u": "9.0"
+ },
+ {
+ "n": "raised_back_of_hand_tone1",
+ "c": "people",
+ "e": "🤚🏻",
+ "d": "raised back of hand tone 1",
+ "u": "9.0"
+ },
+ {
+ "n": "raised_back_of_hand_tone2",
+ "c": "people",
+ "e": "🤚🏼",
+ "d": "raised back of hand tone 2",
+ "u": "9.0"
+ },
+ {
+ "n": "raised_back_of_hand_tone3",
+ "c": "people",
+ "e": "🤚🏽",
+ "d": "raised back of hand tone 3",
+ "u": "9.0"
+ },
+ {
+ "n": "raised_back_of_hand_tone4",
+ "c": "people",
+ "e": "🤚🏾",
+ "d": "raised back of hand tone 4",
+ "u": "9.0"
+ },
+ {
+ "n": "raised_back_of_hand_tone5",
+ "c": "people",
+ "e": "🤚🏿",
+ "d": "raised back of hand tone 5",
+ "u": "9.0"
+ },
+ {
+ "n": "hand_splayed",
+ "c": "people",
+ "e": "🖐",
+ "d": "raised hand with fingers splayed",
+ "u": "7.0"
+ },
+ {
+ "n": "hand_splayed_tone1",
+ "c": "people",
+ "e": "🖐🏻",
+ "d": "raised hand with fingers splayed tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "hand_splayed_tone2",
+ "c": "people",
+ "e": "🖐🏼",
+ "d": "raised hand with fingers splayed tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "hand_splayed_tone3",
+ "c": "people",
+ "e": "🖐🏽",
+ "d": "raised hand with fingers splayed tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "hand_splayed_tone4",
+ "c": "people",
+ "e": "🖐🏾",
+ "d": "raised hand with fingers splayed tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "hand_splayed_tone5",
+ "c": "people",
+ "e": "🖐🏿",
+ "d": "raised hand with fingers splayed tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "raised_hand",
+ "c": "people",
+ "e": "✋",
+ "d": "raised hand",
+ "u": "6.0"
+ },
+ {
+ "n": "raised_hand_tone1",
+ "c": "people",
+ "e": "✋🏻",
+ "d": "raised hand tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "raised_hand_tone2",
+ "c": "people",
+ "e": "✋🏼",
+ "d": "raised hand tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "raised_hand_tone3",
+ "c": "people",
+ "e": "✋🏽",
+ "d": "raised hand tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "raised_hand_tone4",
+ "c": "people",
+ "e": "✋🏾",
+ "d": "raised hand tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "raised_hand_tone5",
+ "c": "people",
+ "e": "✋🏿",
+ "d": "raised hand tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "vulcan",
+ "c": "people",
+ "e": "🖖",
+ "d": "raised hand with part between middle and ring fingers",
+ "u": "7.0"
+ },
+ {
+ "n": "vulcan_tone1",
+ "c": "people",
+ "e": "🖖🏻",
+ "d": "raised hand with part between middle and ring fingers tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "vulcan_tone2",
+ "c": "people",
+ "e": "🖖🏼",
+ "d": "raised hand with part between middle and ring fingers tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "vulcan_tone3",
+ "c": "people",
+ "e": "🖖🏽",
+ "d": "raised hand with part between middle and ring fingers tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "vulcan_tone4",
+ "c": "people",
+ "e": "🖖🏾",
+ "d": "raised hand with part between middle and ring fingers tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "vulcan_tone5",
+ "c": "people",
+ "e": "🖖🏿",
+ "d": "raised hand with part between middle and ring fingers tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "ok_hand",
+ "c": "people",
+ "e": "👌",
+ "d": "ok hand sign",
+ "u": "6.0"
+ },
+ {
+ "n": "ok_hand_tone1",
+ "c": "people",
+ "e": "👌🏻",
+ "d": "ok hand sign tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "ok_hand_tone2",
+ "c": "people",
+ "e": "👌🏼",
+ "d": "ok hand sign tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "ok_hand_tone3",
+ "c": "people",
+ "e": "👌🏽",
+ "d": "ok hand sign tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "ok_hand_tone4",
+ "c": "people",
+ "e": "👌🏾",
+ "d": "ok hand sign tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "ok_hand_tone5",
+ "c": "people",
+ "e": "👌🏿",
+ "d": "ok hand sign tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "v",
+ "c": "people",
+ "e": "✌",
+ "d": "victory hand",
+ "u": "1.1"
+ },
+ {
+ "n": "v_tone1",
+ "c": "people",
+ "e": "✌🏻",
+ "d": "victory hand tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "v_tone2",
+ "c": "people",
+ "e": "✌🏼",
+ "d": "victory hand tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "v_tone3",
+ "c": "people",
+ "e": "✌🏽",
+ "d": "victory hand tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "v_tone4",
+ "c": "people",
+ "e": "✌🏾",
+ "d": "victory hand tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "v_tone5",
+ "c": "people",
+ "e": "✌🏿",
+ "d": "victory hand tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "fingers_crossed",
+ "c": "people",
+ "e": "🤞",
+ "d": "hand with first and index finger crossed",
+ "u": "9.0"
+ },
+ {
+ "n": "fingers_crossed_tone1",
+ "c": "people",
+ "e": "🤞🏻",
+ "d": "hand with index and middle fingers crossed tone 1",
+ "u": "9.0"
+ },
+ {
+ "n": "fingers_crossed_tone2",
+ "c": "people",
+ "e": "🤞🏼",
+ "d": "hand with index and middle fingers crossed tone 2",
+ "u": "9.0"
+ },
+ {
+ "n": "fingers_crossed_tone3",
+ "c": "people",
+ "e": "🤞🏽",
+ "d": "hand with index and middle fingers crossed tone 3",
+ "u": "9.0"
+ },
+ {
+ "n": "fingers_crossed_tone4",
+ "c": "people",
+ "e": "🤞🏾",
+ "d": "hand with index and middle fingers crossed tone 4",
+ "u": "9.0"
+ },
+ {
+ "n": "fingers_crossed_tone5",
+ "c": "people",
+ "e": "🤞🏿",
+ "d": "hand with index and middle fingers crossed tone 5",
+ "u": "9.0"
+ },
+ {
+ "n": "metal",
+ "c": "people",
+ "e": "🤘",
+ "d": "sign of the horns",
+ "u": "8.0"
+ },
+ {
+ "n": "metal_tone1",
+ "c": "people",
+ "e": "🤘🏻",
+ "d": "sign of the horns tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "metal_tone2",
+ "c": "people",
+ "e": "🤘🏼",
+ "d": "sign of the horns tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "metal_tone3",
+ "c": "people",
+ "e": "🤘🏽",
+ "d": "sign of the horns tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "metal_tone4",
+ "c": "people",
+ "e": "🤘🏾",
+ "d": "sign of the horns tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "metal_tone5",
+ "c": "people",
+ "e": "🤘🏿",
+ "d": "sign of the horns tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "call_me",
+ "c": "people",
+ "e": "🤙",
+ "d": "call me hand",
+ "u": "9.0"
+ },
+ {
+ "n": "call_me_tone1",
+ "c": "people",
+ "e": "🤙🏻",
+ "d": "call me hand tone 1",
+ "u": "9.0"
+ },
+ {
+ "n": "call_me_tone2",
+ "c": "people",
+ "e": "🤙🏼",
+ "d": "call me hand tone 2",
+ "u": "9.0"
+ },
+ {
+ "n": "call_me_tone3",
+ "c": "people",
+ "e": "🤙🏽",
+ "d": "call me hand tone 3",
+ "u": "9.0"
+ },
+ {
+ "n": "call_me_tone4",
+ "c": "people",
+ "e": "🤙🏾",
+ "d": "call me hand tone 4",
+ "u": "9.0"
+ },
+ {
+ "n": "call_me_tone5",
+ "c": "people",
+ "e": "🤙🏿",
+ "d": "call me hand tone 5",
+ "u": "9.0"
+ },
+ {
+ "n": "point_left",
+ "c": "people",
+ "e": "👈",
+ "d": "white left pointing backhand index",
+ "u": "6.0"
+ },
+ {
+ "n": "point_left_tone1",
+ "c": "people",
+ "e": "👈🏻",
+ "d": "white left pointing backhand index tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "point_left_tone2",
+ "c": "people",
+ "e": "👈🏼",
+ "d": "white left pointing backhand index tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "point_left_tone3",
+ "c": "people",
+ "e": "👈🏽",
+ "d": "white left pointing backhand index tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "point_left_tone4",
+ "c": "people",
+ "e": "👈🏾",
+ "d": "white left pointing backhand index tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "point_left_tone5",
+ "c": "people",
+ "e": "👈🏿",
+ "d": "white left pointing backhand index tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "point_right",
+ "c": "people",
+ "e": "👉",
+ "d": "white right pointing backhand index",
+ "u": "6.0"
+ },
+ {
+ "n": "point_right_tone1",
+ "c": "people",
+ "e": "👉🏻",
+ "d": "white right pointing backhand index tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "point_right_tone2",
+ "c": "people",
+ "e": "👉🏼",
+ "d": "white right pointing backhand index tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "point_right_tone3",
+ "c": "people",
+ "e": "👉🏽",
+ "d": "white right pointing backhand index tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "point_right_tone4",
+ "c": "people",
+ "e": "👉🏾",
+ "d": "white right pointing backhand index tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "point_right_tone5",
+ "c": "people",
+ "e": "👉🏿",
+ "d": "white right pointing backhand index tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "point_up_2",
+ "c": "people",
+ "e": "👆",
+ "d": "white up pointing backhand index",
+ "u": "6.0"
+ },
+ {
+ "n": "point_up_2_tone1",
+ "c": "people",
+ "e": "👆🏻",
+ "d": "white up pointing backhand index tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "point_up_2_tone2",
+ "c": "people",
+ "e": "👆🏼",
+ "d": "white up pointing backhand index tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "point_up_2_tone3",
+ "c": "people",
+ "e": "👆🏽",
+ "d": "white up pointing backhand index tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "point_up_2_tone4",
+ "c": "people",
+ "e": "👆🏾",
+ "d": "white up pointing backhand index tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "point_up_2_tone5",
+ "c": "people",
+ "e": "👆🏿",
+ "d": "white up pointing backhand index tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "middle_finger",
+ "c": "people",
+ "e": "🖕",
+ "d": "reversed hand with middle finger extended",
+ "u": "7.0"
+ },
+ {
+ "n": "middle_finger_tone1",
+ "c": "people",
+ "e": "🖕🏻",
+ "d": "reversed hand with middle finger extended tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "middle_finger_tone2",
+ "c": "people",
+ "e": "🖕🏼",
+ "d": "reversed hand with middle finger extended tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "middle_finger_tone3",
+ "c": "people",
+ "e": "🖕🏽",
+ "d": "reversed hand with middle finger extended tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "middle_finger_tone4",
+ "c": "people",
+ "e": "🖕🏾",
+ "d": "reversed hand with middle finger extended tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "middle_finger_tone5",
+ "c": "people",
+ "e": "🖕🏿",
+ "d": "reversed hand with middle finger extended tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "point_down",
+ "c": "people",
+ "e": "👇",
+ "d": "white down pointing backhand index",
+ "u": "6.0"
+ },
+ {
+ "n": "point_down_tone1",
+ "c": "people",
+ "e": "👇🏻",
+ "d": "white down pointing backhand index tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "point_down_tone2",
+ "c": "people",
+ "e": "👇🏼",
+ "d": "white down pointing backhand index tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "point_down_tone3",
+ "c": "people",
+ "e": "👇🏽",
+ "d": "white down pointing backhand index tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "point_down_tone4",
+ "c": "people",
+ "e": "👇🏾",
+ "d": "white down pointing backhand index tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "point_down_tone5",
+ "c": "people",
+ "e": "👇🏿",
+ "d": "white down pointing backhand index tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "point_up",
+ "c": "people",
+ "e": "☝",
+ "d": "white up pointing index",
+ "u": "1.1"
+ },
+ {
+ "n": "point_up_tone1",
+ "c": "people",
+ "e": "☝🏻",
+ "d": "white up pointing index tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "point_up_tone2",
+ "c": "people",
+ "e": "☝🏼",
+ "d": "white up pointing index tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "point_up_tone3",
+ "c": "people",
+ "e": "☝🏽",
+ "d": "white up pointing index tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "point_up_tone4",
+ "c": "people",
+ "e": "☝🏾",
+ "d": "white up pointing index tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "point_up_tone5",
+ "c": "people",
+ "e": "☝🏿",
+ "d": "white up pointing index tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "thumbsup",
+ "c": "people",
+ "e": "👍",
+ "d": "thumbs up sign",
+ "u": "6.0"
+ },
+ {
+ "n": "thumbsup_tone1",
+ "c": "people",
+ "e": "👍🏻",
+ "d": "thumbs up sign tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "thumbsup_tone2",
+ "c": "people",
+ "e": "👍🏼",
+ "d": "thumbs up sign tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "thumbsup_tone3",
+ "c": "people",
+ "e": "👍🏽",
+ "d": "thumbs up sign tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "thumbsup_tone4",
+ "c": "people",
+ "e": "👍🏾",
+ "d": "thumbs up sign tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "thumbsup_tone5",
+ "c": "people",
+ "e": "👍🏿",
+ "d": "thumbs up sign tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "thumbsdown",
+ "c": "people",
+ "e": "👎",
+ "d": "thumbs down sign",
+ "u": "6.0"
+ },
+ {
+ "n": "thumbsdown_tone1",
+ "c": "people",
+ "e": "👎🏻",
+ "d": "thumbs down sign tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "thumbsdown_tone2",
+ "c": "people",
+ "e": "👎🏼",
+ "d": "thumbs down sign tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "thumbsdown_tone3",
+ "c": "people",
+ "e": "👎🏽",
+ "d": "thumbs down sign tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "thumbsdown_tone4",
+ "c": "people",
+ "e": "👎🏾",
+ "d": "thumbs down sign tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "thumbsdown_tone5",
+ "c": "people",
+ "e": "👎🏿",
+ "d": "thumbs down sign tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "fist",
+ "c": "people",
+ "e": "✊",
+ "d": "raised fist",
+ "u": "6.0"
+ },
+ {
+ "n": "fist_tone1",
+ "c": "people",
+ "e": "✊🏻",
+ "d": "raised fist tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "fist_tone2",
+ "c": "people",
+ "e": "✊🏼",
+ "d": "raised fist tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "fist_tone3",
+ "c": "people",
+ "e": "✊🏽",
+ "d": "raised fist tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "fist_tone4",
+ "c": "people",
+ "e": "✊🏾",
+ "d": "raised fist tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "fist_tone5",
+ "c": "people",
+ "e": "✊🏿",
+ "d": "raised fist tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "punch",
+ "c": "people",
+ "e": "👊",
+ "d": "fisted hand sign",
+ "u": "6.0"
+ },
+ {
+ "n": "punch_tone1",
+ "c": "people",
+ "e": "👊🏻",
+ "d": "fisted hand sign tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "punch_tone2",
+ "c": "people",
+ "e": "👊🏼",
+ "d": "fisted hand sign tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "punch_tone3",
+ "c": "people",
+ "e": "👊🏽",
+ "d": "fisted hand sign tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "punch_tone4",
+ "c": "people",
+ "e": "👊🏾",
+ "d": "fisted hand sign tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "punch_tone5",
+ "c": "people",
+ "e": "👊🏿",
+ "d": "fisted hand sign tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "left_facing_fist",
+ "c": "people",
+ "e": "🤛",
+ "d": "left-facing fist",
+ "u": "9.0"
+ },
+ {
+ "n": "left_facing_fist_tone1",
+ "c": "people",
+ "e": "🤛🏻",
+ "d": "left facing fist tone 1",
+ "u": "9.0"
+ },
+ {
+ "n": "left_facing_fist_tone2",
+ "c": "people",
+ "e": "🤛🏼",
+ "d": "left facing fist tone 2",
+ "u": "9.0"
+ },
+ {
+ "n": "left_facing_fist_tone3",
+ "c": "people",
+ "e": "🤛🏽",
+ "d": "left facing fist tone 3",
+ "u": "9.0"
+ },
+ {
+ "n": "left_facing_fist_tone4",
+ "c": "people",
+ "e": "🤛🏾",
+ "d": "left facing fist tone 4",
+ "u": "9.0"
+ },
+ {
+ "n": "left_facing_fist_tone5",
+ "c": "people",
+ "e": "🤛🏿",
+ "d": "left facing fist tone 5",
+ "u": "9.0"
+ },
+ {
+ "n": "right_facing_fist",
+ "c": "people",
+ "e": "🤜",
+ "d": "right-facing fist",
+ "u": "9.0"
+ },
+ {
+ "n": "right_facing_fist_tone1",
+ "c": "people",
+ "e": "🤜🏻",
+ "d": "right facing fist tone 1",
+ "u": "9.0"
+ },
+ {
+ "n": "right_facing_fist_tone2",
+ "c": "people",
+ "e": "🤜🏼",
+ "d": "right facing fist tone 2",
+ "u": "9.0"
+ },
+ {
+ "n": "right_facing_fist_tone3",
+ "c": "people",
+ "e": "🤜🏽",
+ "d": "right facing fist tone 3",
+ "u": "9.0"
+ },
+ {
+ "n": "right_facing_fist_tone4",
+ "c": "people",
+ "e": "🤜🏾",
+ "d": "right facing fist tone 4",
+ "u": "9.0"
+ },
+ {
+ "n": "right_facing_fist_tone5",
+ "c": "people",
+ "e": "🤜🏿",
+ "d": "right facing fist tone 5",
+ "u": "9.0"
+ },
+ {
+ "n": "clap",
+ "c": "people",
+ "e": "👏",
+ "d": "clapping hands sign",
+ "u": "6.0"
+ },
+ {
+ "n": "clap_tone1",
+ "c": "people",
+ "e": "👏🏻",
+ "d": "clapping hands sign tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "clap_tone2",
+ "c": "people",
+ "e": "👏🏼",
+ "d": "clapping hands sign tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "clap_tone3",
+ "c": "people",
+ "e": "👏🏽",
+ "d": "clapping hands sign tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "clap_tone4",
+ "c": "people",
+ "e": "👏🏾",
+ "d": "clapping hands sign tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "clap_tone5",
+ "c": "people",
+ "e": "👏🏿",
+ "d": "clapping hands sign tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "raised_hands",
+ "c": "people",
+ "e": "🙌",
+ "d": "person raising both hands in celebration",
+ "u": "6.0"
+ },
+ {
+ "n": "raised_hands_tone1",
+ "c": "people",
+ "e": "🙌🏻",
+ "d": "person raising both hands in celebration tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "raised_hands_tone2",
+ "c": "people",
+ "e": "🙌🏼",
+ "d": "person raising both hands in celebration tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "raised_hands_tone3",
+ "c": "people",
+ "e": "🙌🏽",
+ "d": "person raising both hands in celebration tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "raised_hands_tone4",
+ "c": "people",
+ "e": "🙌🏾",
+ "d": "person raising both hands in celebration tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "raised_hands_tone5",
+ "c": "people",
+ "e": "🙌🏿",
+ "d": "person raising both hands in celebration tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "open_hands",
+ "c": "people",
+ "e": "👐",
+ "d": "open hands sign",
+ "u": "6.0"
+ },
+ {
+ "n": "open_hands_tone1",
+ "c": "people",
+ "e": "👐🏻",
+ "d": "open hands sign tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "open_hands_tone2",
+ "c": "people",
+ "e": "👐🏼",
+ "d": "open hands sign tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "open_hands_tone3",
+ "c": "people",
+ "e": "👐🏽",
+ "d": "open hands sign tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "open_hands_tone4",
+ "c": "people",
+ "e": "👐🏾",
+ "d": "open hands sign tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "open_hands_tone5",
+ "c": "people",
+ "e": "👐🏿",
+ "d": "open hands sign tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "handshake",
+ "c": "people",
+ "e": "🤝",
+ "d": "handshake",
+ "u": "9.0"
+ },
+ {
+ "n": "handshake_tone1",
+ "c": "people",
+ "e": "🤝🏻",
+ "d": "handshake tone 1",
+ "u": "9.0"
+ },
+ {
+ "n": "handshake_tone2",
+ "c": "people",
+ "e": "🤝🏼",
+ "d": "handshake tone 2",
+ "u": "9.0"
+ },
+ {
+ "n": "handshake_tone3",
+ "c": "people",
+ "e": "🤝🏽",
+ "d": "handshake tone 3",
+ "u": "9.0"
+ },
+ {
+ "n": "handshake_tone4",
+ "c": "people",
+ "e": "🤝🏾",
+ "d": "handshake tone 4",
+ "u": "9.0"
+ },
+ {
+ "n": "handshake_tone5",
+ "c": "people",
+ "e": "🤝🏿",
+ "d": "handshake tone 5",
+ "u": "9.0"
+ },
+ {
+ "n": "pray",
+ "c": "people",
+ "e": "🙏",
+ "d": "person with folded hands",
+ "u": "6.0"
+ },
+ {
+ "n": "pray_tone1",
+ "c": "people",
+ "e": "🙏🏻",
+ "d": "person with folded hands tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "pray_tone2",
+ "c": "people",
+ "e": "🙏🏼",
+ "d": "person with folded hands tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "pray_tone3",
+ "c": "people",
+ "e": "🙏🏽",
+ "d": "person with folded hands tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "pray_tone4",
+ "c": "people",
+ "e": "🙏🏾",
+ "d": "person with folded hands tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "pray_tone5",
+ "c": "people",
+ "e": "🙏🏿",
+ "d": "person with folded hands tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "writing_hand",
+ "c": "people",
+ "e": "✍",
+ "d": "writing hand",
+ "u": "1.1"
+ },
+ {
+ "n": "writing_hand_tone1",
+ "c": "people",
+ "e": "✍🏻",
+ "d": "writing hand tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "writing_hand_tone2",
+ "c": "people",
+ "e": "✍🏼",
+ "d": "writing hand tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "writing_hand_tone3",
+ "c": "people",
+ "e": "✍🏽",
+ "d": "writing hand tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "writing_hand_tone4",
+ "c": "people",
+ "e": "✍🏾",
+ "d": "writing hand tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "writing_hand_tone5",
+ "c": "people",
+ "e": "✍🏿",
+ "d": "writing hand tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "nail_care",
+ "c": "people",
+ "e": "💅",
+ "d": "nail polish",
+ "u": "6.0"
+ },
+ {
+ "n": "nail_care_tone1",
+ "c": "people",
+ "e": "💅🏻",
+ "d": "nail polish tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "nail_care_tone2",
+ "c": "people",
+ "e": "💅🏼",
+ "d": "nail polish tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "nail_care_tone3",
+ "c": "people",
+ "e": "💅🏽",
+ "d": "nail polish tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "nail_care_tone4",
+ "c": "people",
+ "e": "💅🏾",
+ "d": "nail polish tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "nail_care_tone5",
+ "c": "people",
+ "e": "💅🏿",
+ "d": "nail polish tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "selfie",
+ "c": "people",
+ "e": "🤳",
+ "d": "selfie",
+ "u": "9.0"
+ },
+ {
+ "n": "selfie_tone1",
+ "c": "people",
+ "e": "🤳🏻",
+ "d": "selfie tone 1",
+ "u": "9.0"
+ },
+ {
+ "n": "selfie_tone2",
+ "c": "people",
+ "e": "🤳🏼",
+ "d": "selfie tone 2",
+ "u": "9.0"
+ },
+ {
+ "n": "selfie_tone3",
+ "c": "people",
+ "e": "🤳🏽",
+ "d": "selfie tone 3",
+ "u": "9.0"
+ },
+ {
+ "n": "selfie_tone4",
+ "c": "people",
+ "e": "🤳🏾",
+ "d": "selfie tone 4",
+ "u": "9.0"
+ },
+ {
+ "n": "selfie_tone5",
+ "c": "people",
+ "e": "🤳🏿",
+ "d": "selfie tone 5",
+ "u": "9.0"
+ },
+ {
+ "n": "muscle",
+ "c": "people",
+ "e": "💪",
+ "d": "flexed biceps",
+ "u": "6.0"
+ },
+ {
+ "n": "muscle_tone1",
+ "c": "people",
+ "e": "💪🏻",
+ "d": "flexed biceps tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "muscle_tone2",
+ "c": "people",
+ "e": "💪🏼",
+ "d": "flexed biceps tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "muscle_tone3",
+ "c": "people",
+ "e": "💪🏽",
+ "d": "flexed biceps tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "muscle_tone4",
+ "c": "people",
+ "e": "💪🏾",
+ "d": "flexed biceps tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "muscle_tone5",
+ "c": "people",
+ "e": "💪🏿",
+ "d": "flexed biceps tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "ear",
+ "c": "people",
+ "e": "👂",
+ "d": "ear",
+ "u": "6.0"
+ },
+ {
+ "n": "ear_tone1",
+ "c": "people",
+ "e": "👂🏻",
+ "d": "ear tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "ear_tone2",
+ "c": "people",
+ "e": "👂🏼",
+ "d": "ear tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "ear_tone3",
+ "c": "people",
+ "e": "👂🏽",
+ "d": "ear tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "ear_tone4",
+ "c": "people",
+ "e": "👂🏾",
+ "d": "ear tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "ear_tone5",
+ "c": "people",
+ "e": "👂🏿",
+ "d": "ear tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "nose",
+ "c": "people",
+ "e": "👃",
+ "d": "nose",
+ "u": "6.0"
+ },
+ {
+ "n": "nose_tone1",
+ "c": "people",
+ "e": "👃🏻",
+ "d": "nose tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "nose_tone2",
+ "c": "people",
+ "e": "👃🏼",
+ "d": "nose tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "nose_tone3",
+ "c": "people",
+ "e": "👃🏽",
+ "d": "nose tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "nose_tone4",
+ "c": "people",
+ "e": "👃🏾",
+ "d": "nose tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "nose_tone5",
+ "c": "people",
+ "e": "👃🏿",
+ "d": "nose tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "eyes",
+ "c": "people",
+ "e": "👀",
+ "d": "eyes",
+ "u": "6.0"
+ },
+ {
+ "n": "eye",
+ "c": "people",
+ "e": "👁",
+ "d": "eye",
+ "u": "7.0"
+ },
+ {
+ "n": "tongue",
+ "c": "people",
+ "e": "👅",
+ "d": "tongue",
+ "u": "6.0"
+ },
+ {
+ "n": "lips",
+ "c": "people",
+ "e": "👄",
+ "d": "mouth",
+ "u": "6.0"
+ },
+ {
+ "n": "baby",
+ "c": "people",
+ "e": "👶",
+ "d": "baby",
+ "u": "6.0"
+ },
+ {
+ "n": "baby_tone1",
+ "c": "people",
+ "e": "👶🏻",
+ "d": "baby tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "baby_tone2",
+ "c": "people",
+ "e": "👶🏼",
+ "d": "baby tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "baby_tone3",
+ "c": "people",
+ "e": "👶🏽",
+ "d": "baby tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "baby_tone4",
+ "c": "people",
+ "e": "👶🏾",
+ "d": "baby tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "baby_tone5",
+ "c": "people",
+ "e": "👶🏿",
+ "d": "baby tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "boy",
+ "c": "people",
+ "e": "👦",
+ "d": "boy",
+ "u": "6.0"
+ },
+ {
+ "n": "boy_tone1",
+ "c": "people",
+ "e": "👦🏻",
+ "d": "boy tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "boy_tone2",
+ "c": "people",
+ "e": "👦🏼",
+ "d": "boy tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "boy_tone3",
+ "c": "people",
+ "e": "👦🏽",
+ "d": "boy tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "boy_tone4",
+ "c": "people",
+ "e": "👦🏾",
+ "d": "boy tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "boy_tone5",
+ "c": "people",
+ "e": "👦🏿",
+ "d": "boy tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "girl",
+ "c": "people",
+ "e": "👧",
+ "d": "girl",
+ "u": "6.0"
+ },
+ {
+ "n": "girl_tone1",
+ "c": "people",
+ "e": "👧🏻",
+ "d": "girl tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "girl_tone2",
+ "c": "people",
+ "e": "👧🏼",
+ "d": "girl tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "girl_tone3",
+ "c": "people",
+ "e": "👧🏽",
+ "d": "girl tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "girl_tone4",
+ "c": "people",
+ "e": "👧🏾",
+ "d": "girl tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "girl_tone5",
+ "c": "people",
+ "e": "👧🏿",
+ "d": "girl tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "person_with_blond_hair",
+ "c": "people",
+ "e": "👱",
+ "d": "person with blond hair",
+ "u": "6.0"
+ },
+ {
+ "n": "person_with_blond_hair_tone1",
+ "c": "people",
+ "e": "👱🏻",
+ "d": "person with blond hair tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "person_with_blond_hair_tone2",
+ "c": "people",
+ "e": "👱🏼",
+ "d": "person with blond hair tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "person_with_blond_hair_tone3",
+ "c": "people",
+ "e": "👱🏽",
+ "d": "person with blond hair tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "person_with_blond_hair_tone4",
+ "c": "people",
+ "e": "👱🏾",
+ "d": "person with blond hair tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "person_with_blond_hair_tone5",
+ "c": "people",
+ "e": "👱🏿",
+ "d": "person with blond hair tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "man",
+ "c": "people",
+ "e": "👨",
+ "d": "man",
+ "u": "6.0"
+ },
+ {
+ "n": "man_tone1",
+ "c": "people",
+ "e": "👨🏻",
+ "d": "man tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "man_tone2",
+ "c": "people",
+ "e": "👨🏼",
+ "d": "man tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "man_tone3",
+ "c": "people",
+ "e": "👨🏽",
+ "d": "man tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "man_tone4",
+ "c": "people",
+ "e": "👨🏾",
+ "d": "man tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "man_tone5",
+ "c": "people",
+ "e": "👨🏿",
+ "d": "man tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "woman",
+ "c": "people",
+ "e": "👩",
+ "d": "woman",
+ "u": "6.0"
+ },
+ {
+ "n": "woman_tone1",
+ "c": "people",
+ "e": "👩🏻",
+ "d": "woman tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "woman_tone2",
+ "c": "people",
+ "e": "👩🏼",
+ "d": "woman tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "woman_tone3",
+ "c": "people",
+ "e": "👩🏽",
+ "d": "woman tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "woman_tone4",
+ "c": "people",
+ "e": "👩🏾",
+ "d": "woman tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "woman_tone5",
+ "c": "people",
+ "e": "👩🏿",
+ "d": "woman tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "older_man",
+ "c": "people",
+ "e": "👴",
+ "d": "older man",
+ "u": "6.0"
+ },
+ {
+ "n": "older_man_tone1",
+ "c": "people",
+ "e": "👴🏻",
+ "d": "older man tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "older_man_tone2",
+ "c": "people",
+ "e": "👴🏼",
+ "d": "older man tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "older_man_tone3",
+ "c": "people",
+ "e": "👴🏽",
+ "d": "older man tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "older_man_tone4",
+ "c": "people",
+ "e": "👴🏾",
+ "d": "older man tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "older_man_tone5",
+ "c": "people",
+ "e": "👴🏿",
+ "d": "older man tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "older_woman",
+ "c": "people",
+ "e": "👵",
+ "d": "older woman",
+ "u": "6.0"
+ },
+ {
+ "n": "older_woman_tone1",
+ "c": "people",
+ "e": "👵🏻",
+ "d": "older woman tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "older_woman_tone2",
+ "c": "people",
+ "e": "👵🏼",
+ "d": "older woman tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "older_woman_tone3",
+ "c": "people",
+ "e": "👵🏽",
+ "d": "older woman tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "older_woman_tone4",
+ "c": "people",
+ "e": "👵🏾",
+ "d": "older woman tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "older_woman_tone5",
+ "c": "people",
+ "e": "👵🏿",
+ "d": "older woman tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "person_frowning",
+ "c": "people",
+ "e": "🙍",
+ "d": "person frowning",
+ "u": "6.0"
+ },
+ {
+ "n": "person_frowning_tone1",
+ "c": "people",
+ "e": "🙍🏻",
+ "d": "person frowning tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "person_frowning_tone2",
+ "c": "people",
+ "e": "🙍🏼",
+ "d": "person frowning tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "person_frowning_tone3",
+ "c": "people",
+ "e": "🙍🏽",
+ "d": "person frowning tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "person_frowning_tone4",
+ "c": "people",
+ "e": "🙍🏾",
+ "d": "person frowning tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "person_frowning_tone5",
+ "c": "people",
+ "e": "🙍🏿",
+ "d": "person frowning tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "person_with_pouting_face",
+ "c": "people",
+ "e": "🙎",
+ "d": "person with pouting face",
+ "u": "6.0"
+ },
+ {
+ "n": "person_with_pouting_face_tone1",
+ "c": "people",
+ "e": "🙎🏻",
+ "d": "person with pouting face tone1",
+ "u": "8.0"
+ },
+ {
+ "n": "person_with_pouting_face_tone2",
+ "c": "people",
+ "e": "🙎🏼",
+ "d": "person with pouting face tone2",
+ "u": "8.0"
+ },
+ {
+ "n": "person_with_pouting_face_tone3",
+ "c": "people",
+ "e": "🙎🏽",
+ "d": "person with pouting face tone3",
+ "u": "8.0"
+ },
+ {
+ "n": "person_with_pouting_face_tone4",
+ "c": "people",
+ "e": "🙎🏾",
+ "d": "person with pouting face tone4",
+ "u": "8.0"
+ },
+ {
+ "n": "person_with_pouting_face_tone5",
+ "c": "people",
+ "e": "🙎🏿",
+ "d": "person with pouting face tone5",
+ "u": "8.0"
+ },
+ {
+ "n": "no_good",
+ "c": "people",
+ "e": "🙅",
+ "d": "face with no good gesture",
+ "u": "6.0"
+ },
+ {
+ "n": "no_good_tone1",
+ "c": "people",
+ "e": "🙅🏻",
+ "d": "face with no good gesture tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "no_good_tone2",
+ "c": "people",
+ "e": "🙅🏼",
+ "d": "face with no good gesture tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "no_good_tone3",
+ "c": "people",
+ "e": "🙅🏽",
+ "d": "face with no good gesture tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "no_good_tone4",
+ "c": "people",
+ "e": "🙅🏾",
+ "d": "face with no good gesture tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "no_good_tone5",
+ "c": "people",
+ "e": "🙅🏿",
+ "d": "face with no good gesture tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "ok_woman",
+ "c": "people",
+ "e": "🙆",
+ "d": "face with ok gesture",
+ "u": "6.0"
+ },
+ {
+ "n": "ok_woman_tone1",
+ "c": "people",
+ "e": "🙆🏻",
+ "d": "face with ok gesture tone1",
+ "u": "8.0"
+ },
+ {
+ "n": "ok_woman_tone2",
+ "c": "people",
+ "e": "🙆🏼",
+ "d": "face with ok gesture tone2",
+ "u": "8.0"
+ },
+ {
+ "n": "ok_woman_tone3",
+ "c": "people",
+ "e": "🙆🏽",
+ "d": "face with ok gesture tone3",
+ "u": "8.0"
+ },
+ {
+ "n": "ok_woman_tone4",
+ "c": "people",
+ "e": "🙆🏾",
+ "d": "face with ok gesture tone4",
+ "u": "8.0"
+ },
+ {
+ "n": "ok_woman_tone5",
+ "c": "people",
+ "e": "🙆🏿",
+ "d": "face with ok gesture tone5",
+ "u": "8.0"
+ },
+ {
+ "n": "information_desk_person",
+ "c": "people",
+ "e": "💁",
+ "d": "information desk person",
+ "u": "6.0"
+ },
+ {
+ "n": "information_desk_person_tone1",
+ "c": "people",
+ "e": "💁🏻",
+ "d": "information desk person tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "information_desk_person_tone2",
+ "c": "people",
+ "e": "💁🏼",
+ "d": "information desk person tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "information_desk_person_tone3",
+ "c": "people",
+ "e": "💁🏽",
+ "d": "information desk person tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "information_desk_person_tone4",
+ "c": "people",
+ "e": "💁🏾",
+ "d": "information desk person tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "information_desk_person_tone5",
+ "c": "people",
+ "e": "💁🏿",
+ "d": "information desk person tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "raising_hand",
+ "c": "people",
+ "e": "🙋",
+ "d": "happy person raising one hand",
+ "u": "6.0"
+ },
+ {
+ "n": "raising_hand_tone1",
+ "c": "people",
+ "e": "🙋🏻",
+ "d": "happy person raising one hand tone1",
+ "u": "8.0"
+ },
+ {
+ "n": "raising_hand_tone2",
+ "c": "people",
+ "e": "🙋🏼",
+ "d": "happy person raising one hand tone2",
+ "u": "8.0"
+ },
+ {
+ "n": "raising_hand_tone3",
+ "c": "people",
+ "e": "🙋🏽",
+ "d": "happy person raising one hand tone3",
+ "u": "8.0"
+ },
+ {
+ "n": "raising_hand_tone4",
+ "c": "people",
+ "e": "🙋🏾",
+ "d": "happy person raising one hand tone4",
+ "u": "8.0"
+ },
+ {
+ "n": "raising_hand_tone5",
+ "c": "people",
+ "e": "🙋🏿",
+ "d": "happy person raising one hand tone5",
+ "u": "8.0"
+ },
+ {
+ "n": "bow",
+ "c": "people",
+ "e": "🙇",
+ "d": "person bowing deeply",
+ "u": "6.0"
+ },
+ {
+ "n": "bow_tone1",
+ "c": "people",
+ "e": "🙇🏻",
+ "d": "person bowing deeply tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "bow_tone2",
+ "c": "people",
+ "e": "🙇🏼",
+ "d": "person bowing deeply tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "bow_tone3",
+ "c": "people",
+ "e": "🙇🏽",
+ "d": "person bowing deeply tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "bow_tone4",
+ "c": "people",
+ "e": "🙇🏾",
+ "d": "person bowing deeply tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "bow_tone5",
+ "c": "people",
+ "e": "🙇🏿",
+ "d": "person bowing deeply tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "face_palm",
+ "c": "people",
+ "e": "🤦",
+ "d": "face palm",
+ "u": "9.0"
+ },
+ {
+ "n": "face_palm_tone1",
+ "c": "people",
+ "e": "🤦🏻",
+ "d": "face palm tone 1",
+ "u": "9.0"
+ },
+ {
+ "n": "face_palm_tone2",
+ "c": "people",
+ "e": "🤦🏼",
+ "d": "face palm tone 2",
+ "u": "9.0"
+ },
+ {
+ "n": "face_palm_tone3",
+ "c": "people",
+ "e": "🤦🏽",
+ "d": "face palm tone 3",
+ "u": "9.0"
+ },
+ {
+ "n": "face_palm_tone4",
+ "c": "people",
+ "e": "🤦🏾",
+ "d": "face palm tone 4",
+ "u": "9.0"
+ },
+ {
+ "n": "face_palm_tone5",
+ "c": "people",
+ "e": "🤦🏿",
+ "d": "face palm tone 5",
+ "u": "9.0"
+ },
+ {
+ "n": "shrug",
+ "c": "people",
+ "e": "🤷",
+ "d": "shrug",
+ "u": "9.0"
+ },
+ {
+ "n": "shrug_tone1",
+ "c": "people",
+ "e": "🤷🏻",
+ "d": "shrug tone 1",
+ "u": "9.0"
+ },
+ {
+ "n": "shrug_tone2",
+ "c": "people",
+ "e": "🤷🏼",
+ "d": "shrug tone 2",
+ "u": "9.0"
+ },
+ {
+ "n": "shrug_tone3",
+ "c": "people",
+ "e": "🤷🏽",
+ "d": "shrug tone 3",
+ "u": "9.0"
+ },
+ {
+ "n": "shrug_tone4",
+ "c": "people",
+ "e": "🤷🏾",
+ "d": "shrug tone 4",
+ "u": "9.0"
+ },
+ {
+ "n": "shrug_tone5",
+ "c": "people",
+ "e": "🤷🏿",
+ "d": "shrug tone 5",
+ "u": "9.0"
+ },
+ {
+ "n": "cop",
+ "c": "people",
+ "e": "👮",
+ "d": "police officer",
+ "u": "6.0"
+ },
+ {
+ "n": "cop_tone1",
+ "c": "people",
+ "e": "👮🏻",
+ "d": "police officer tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "cop_tone2",
+ "c": "people",
+ "e": "👮🏼",
+ "d": "police officer tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "cop_tone3",
+ "c": "people",
+ "e": "👮🏽",
+ "d": "police officer tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "cop_tone4",
+ "c": "people",
+ "e": "👮🏾",
+ "d": "police officer tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "cop_tone5",
+ "c": "people",
+ "e": "👮🏿",
+ "d": "police officer tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "spy",
+ "c": "people",
+ "e": "🕵",
+ "d": "sleuth or spy",
+ "u": "7.0"
+ },
+ {
+ "n": "spy_tone1",
+ "c": "people",
+ "e": "🕵🏻",
+ "d": "sleuth or spy tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "spy_tone2",
+ "c": "people",
+ "e": "🕵🏼",
+ "d": "sleuth or spy tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "spy_tone3",
+ "c": "people",
+ "e": "🕵🏽",
+ "d": "sleuth or spy tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "spy_tone4",
+ "c": "people",
+ "e": "🕵🏾",
+ "d": "sleuth or spy tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "spy_tone5",
+ "c": "people",
+ "e": "🕵🏿",
+ "d": "sleuth or spy tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "guardsman",
+ "c": "people",
+ "e": "💂",
+ "d": "guardsman",
+ "u": "6.0"
+ },
+ {
+ "n": "guardsman_tone1",
+ "c": "people",
+ "e": "💂🏻",
+ "d": "guardsman tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "guardsman_tone2",
+ "c": "people",
+ "e": "💂🏼",
+ "d": "guardsman tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "guardsman_tone3",
+ "c": "people",
+ "e": "💂🏽",
+ "d": "guardsman tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "guardsman_tone4",
+ "c": "people",
+ "e": "💂🏾",
+ "d": "guardsman tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "guardsman_tone5",
+ "c": "people",
+ "e": "💂🏿",
+ "d": "guardsman tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "construction_worker",
+ "c": "people",
+ "e": "👷",
+ "d": "construction worker",
+ "u": "6.0"
+ },
+ {
+ "n": "construction_worker_tone1",
+ "c": "people",
+ "e": "👷🏻",
+ "d": "construction worker tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "construction_worker_tone2",
+ "c": "people",
+ "e": "👷🏼",
+ "d": "construction worker tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "construction_worker_tone3",
+ "c": "people",
+ "e": "👷🏽",
+ "d": "construction worker tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "construction_worker_tone4",
+ "c": "people",
+ "e": "👷🏾",
+ "d": "construction worker tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "construction_worker_tone5",
+ "c": "people",
+ "e": "👷🏿",
+ "d": "construction worker tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "prince",
+ "c": "people",
+ "e": "🤴",
+ "d": "prince",
+ "u": "9.0"
+ },
+ {
+ "n": "prince_tone1",
+ "c": "people",
+ "e": "🤴🏻",
+ "d": "prince tone 1",
+ "u": "9.0"
+ },
+ {
+ "n": "prince_tone2",
+ "c": "people",
+ "e": "🤴🏼",
+ "d": "prince tone 2",
+ "u": "9.0"
+ },
+ {
+ "n": "prince_tone3",
+ "c": "people",
+ "e": "🤴🏽",
+ "d": "prince tone 3",
+ "u": "9.0"
+ },
+ {
+ "n": "prince_tone4",
+ "c": "people",
+ "e": "🤴🏾",
+ "d": "prince tone 4",
+ "u": "9.0"
+ },
+ {
+ "n": "prince_tone5",
+ "c": "people",
+ "e": "🤴🏿",
+ "d": "prince tone 5",
+ "u": "9.0"
+ },
+ {
+ "n": "princess",
+ "c": "people",
+ "e": "👸",
+ "d": "princess",
+ "u": "6.0"
+ },
+ {
+ "n": "princess_tone1",
+ "c": "people",
+ "e": "👸🏻",
+ "d": "princess tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "princess_tone2",
+ "c": "people",
+ "e": "👸🏼",
+ "d": "princess tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "princess_tone3",
+ "c": "people",
+ "e": "👸🏽",
+ "d": "princess tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "princess_tone4",
+ "c": "people",
+ "e": "👸🏾",
+ "d": "princess tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "princess_tone5",
+ "c": "people",
+ "e": "👸🏿",
+ "d": "princess tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "man_with_turban",
+ "c": "people",
+ "e": "👳",
+ "d": "man with turban",
+ "u": "6.0"
+ },
+ {
+ "n": "man_with_turban_tone1",
+ "c": "people",
+ "e": "👳🏻",
+ "d": "man with turban tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "man_with_turban_tone2",
+ "c": "people",
+ "e": "👳🏼",
+ "d": "man with turban tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "man_with_turban_tone3",
+ "c": "people",
+ "e": "👳🏽",
+ "d": "man with turban tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "man_with_turban_tone4",
+ "c": "people",
+ "e": "👳🏾",
+ "d": "man with turban tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "man_with_turban_tone5",
+ "c": "people",
+ "e": "👳🏿",
+ "d": "man with turban tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "man_with_gua_pi_mao",
+ "c": "people",
+ "e": "👲",
+ "d": "man with gua pi mao",
+ "u": "6.0"
+ },
+ {
+ "n": "man_with_gua_pi_mao_tone1",
+ "c": "people",
+ "e": "👲🏻",
+ "d": "man with gua pi mao tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "man_with_gua_pi_mao_tone2",
+ "c": "people",
+ "e": "👲🏼",
+ "d": "man with gua pi mao tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "man_with_gua_pi_mao_tone3",
+ "c": "people",
+ "e": "👲🏽",
+ "d": "man with gua pi mao tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "man_with_gua_pi_mao_tone4",
+ "c": "people",
+ "e": "👲🏾",
+ "d": "man with gua pi mao tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "man_with_gua_pi_mao_tone5",
+ "c": "people",
+ "e": "👲🏿",
+ "d": "man with gua pi mao tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "man_in_tuxedo",
+ "c": "people",
+ "e": "🤵",
+ "d": "man in tuxedo",
+ "u": "9.0"
+ },
+ {
+ "n": "man_in_tuxedo_tone1",
+ "c": "people",
+ "e": "🤵🏻",
+ "d": "man in tuxedo tone 1",
+ "u": "9.0"
+ },
+ {
+ "n": "man_in_tuxedo_tone2",
+ "c": "people",
+ "e": "🤵🏼",
+ "d": "man in tuxedo tone 2",
+ "u": "9.0"
+ },
+ {
+ "n": "man_in_tuxedo_tone3",
+ "c": "people",
+ "e": "🤵🏽",
+ "d": "man in tuxedo tone 3",
+ "u": "9.0"
+ },
+ {
+ "n": "man_in_tuxedo_tone4",
+ "c": "people",
+ "e": "🤵🏾",
+ "d": "man in tuxedo tone 4",
+ "u": "9.0"
+ },
+ {
+ "n": "man_in_tuxedo_tone5",
+ "c": "people",
+ "e": "🤵🏿",
+ "d": "man in tuxedo tone 5",
+ "u": "9.0"
+ },
+ {
+ "n": "bride_with_veil",
+ "c": "people",
+ "e": "👰",
+ "d": "bride with veil",
+ "u": "6.0"
+ },
+ {
+ "n": "bride_with_veil_tone1",
+ "c": "people",
+ "e": "👰🏻",
+ "d": "bride with veil tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "bride_with_veil_tone2",
+ "c": "people",
+ "e": "👰🏼",
+ "d": "bride with veil tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "bride_with_veil_tone3",
+ "c": "people",
+ "e": "👰🏽",
+ "d": "bride with veil tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "bride_with_veil_tone4",
+ "c": "people",
+ "e": "👰🏾",
+ "d": "bride with veil tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "bride_with_veil_tone5",
+ "c": "people",
+ "e": "👰🏿",
+ "d": "bride with veil tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "pregnant_woman",
+ "c": "people",
+ "e": "🤰",
+ "d": "pregnant woman",
+ "u": "9.0"
+ },
+ {
+ "n": "pregnant_woman_tone1",
+ "c": "people",
+ "e": "🤰🏻",
+ "d": "pregnant woman tone 1",
+ "u": "9.0"
+ },
+ {
+ "n": "pregnant_woman_tone2",
+ "c": "people",
+ "e": "🤰🏼",
+ "d": "pregnant woman tone 2",
+ "u": "9.0"
+ },
+ {
+ "n": "pregnant_woman_tone3",
+ "c": "people",
+ "e": "🤰🏽",
+ "d": "pregnant woman tone 3",
+ "u": "9.0"
+ },
+ {
+ "n": "pregnant_woman_tone4",
+ "c": "people",
+ "e": "🤰🏾",
+ "d": "pregnant woman tone 4",
+ "u": "9.0"
+ },
+ {
+ "n": "pregnant_woman_tone5",
+ "c": "people",
+ "e": "🤰🏿",
+ "d": "pregnant woman tone 5",
+ "u": "9.0"
+ },
+ {
+ "n": "angel",
+ "c": "people",
+ "e": "👼",
+ "d": "baby angel",
+ "u": "6.0"
+ },
+ {
+ "n": "angel_tone1",
+ "c": "people",
+ "e": "👼🏻",
+ "d": "baby angel tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "angel_tone2",
+ "c": "people",
+ "e": "👼🏼",
+ "d": "baby angel tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "angel_tone3",
+ "c": "people",
+ "e": "👼🏽",
+ "d": "baby angel tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "angel_tone4",
+ "c": "people",
+ "e": "👼🏾",
+ "d": "baby angel tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "angel_tone5",
+ "c": "people",
+ "e": "👼🏿",
+ "d": "baby angel tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "santa",
+ "c": "people",
+ "e": "🎅",
+ "d": "father christmas",
+ "u": "6.0"
+ },
+ {
+ "n": "santa_tone1",
+ "c": "people",
+ "e": "🎅🏻",
+ "d": "father christmas tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "santa_tone2",
+ "c": "people",
+ "e": "🎅🏼",
+ "d": "father christmas tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "santa_tone3",
+ "c": "people",
+ "e": "🎅🏽",
+ "d": "father christmas tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "santa_tone4",
+ "c": "people",
+ "e": "🎅🏾",
+ "d": "father christmas tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "santa_tone5",
+ "c": "people",
+ "e": "🎅🏿",
+ "d": "father christmas tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "mrs_claus",
+ "c": "people",
+ "e": "🤶",
+ "d": "mother christmas",
+ "u": "9.0"
+ },
+ {
+ "n": "mrs_claus_tone1",
+ "c": "people",
+ "e": "🤶🏻",
+ "d": "mother christmas tone 1",
+ "u": "9.0"
+ },
+ {
+ "n": "mrs_claus_tone2",
+ "c": "people",
+ "e": "🤶🏼",
+ "d": "mother christmas tone 2",
+ "u": "9.0"
+ },
+ {
+ "n": "mrs_claus_tone3",
+ "c": "people",
+ "e": "🤶🏽",
+ "d": "mother christmas tone 3",
+ "u": "9.0"
+ },
+ {
+ "n": "mrs_claus_tone4",
+ "c": "people",
+ "e": "🤶🏾",
+ "d": "mother christmas tone 4",
+ "u": "9.0"
+ },
+ {
+ "n": "mrs_claus_tone5",
+ "c": "people",
+ "e": "🤶🏿",
+ "d": "mother christmas tone 5",
+ "u": "9.0"
+ },
+ {
+ "n": "massage",
+ "c": "people",
+ "e": "💆",
+ "d": "face massage",
+ "u": "6.0"
+ },
+ {
+ "n": "massage_tone1",
+ "c": "people",
+ "e": "💆🏻",
+ "d": "face massage tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "massage_tone2",
+ "c": "people",
+ "e": "💆🏼",
+ "d": "face massage tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "massage_tone3",
+ "c": "people",
+ "e": "💆🏽",
+ "d": "face massage tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "massage_tone4",
+ "c": "people",
+ "e": "💆🏾",
+ "d": "face massage tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "massage_tone5",
+ "c": "people",
+ "e": "💆🏿",
+ "d": "face massage tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "haircut",
+ "c": "people",
+ "e": "💇",
+ "d": "haircut",
+ "u": "6.0"
+ },
+ {
+ "n": "haircut_tone1",
+ "c": "people",
+ "e": "💇🏻",
+ "d": "haircut tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "haircut_tone2",
+ "c": "people",
+ "e": "💇🏼",
+ "d": "haircut tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "haircut_tone3",
+ "c": "people",
+ "e": "💇🏽",
+ "d": "haircut tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "haircut_tone4",
+ "c": "people",
+ "e": "💇🏾",
+ "d": "haircut tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "haircut_tone5",
+ "c": "people",
+ "e": "💇🏿",
+ "d": "haircut tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "walking",
+ "c": "people",
+ "e": "🚶",
+ "d": "pedestrian",
+ "u": "6.0"
+ },
+ {
+ "n": "walking_tone1",
+ "c": "people",
+ "e": "🚶🏻",
+ "d": "pedestrian tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "walking_tone2",
+ "c": "people",
+ "e": "🚶🏼",
+ "d": "pedestrian tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "walking_tone3",
+ "c": "people",
+ "e": "🚶🏽",
+ "d": "pedestrian tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "walking_tone4",
+ "c": "people",
+ "e": "🚶🏾",
+ "d": "pedestrian tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "walking_tone5",
+ "c": "people",
+ "e": "🚶🏿",
+ "d": "pedestrian tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "runner",
+ "c": "people",
+ "e": "🏃",
+ "d": "runner",
+ "u": "6.0"
+ },
+ {
+ "n": "runner_tone1",
+ "c": "people",
+ "e": "🏃🏻",
+ "d": "runner tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "runner_tone2",
+ "c": "people",
+ "e": "🏃🏼",
+ "d": "runner tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "runner_tone3",
+ "c": "people",
+ "e": "🏃🏽",
+ "d": "runner tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "runner_tone4",
+ "c": "people",
+ "e": "🏃🏾",
+ "d": "runner tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "runner_tone5",
+ "c": "people",
+ "e": "🏃🏿",
+ "d": "runner tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "dancer",
+ "c": "people",
+ "e": "💃",
+ "d": "dancer",
+ "u": "6.0"
+ },
+ {
+ "n": "dancer_tone1",
+ "c": "people",
+ "e": "💃🏻",
+ "d": "dancer tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "dancer_tone2",
+ "c": "people",
+ "e": "💃🏼",
+ "d": "dancer tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "dancer_tone3",
+ "c": "people",
+ "e": "💃🏽",
+ "d": "dancer tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "dancer_tone4",
+ "c": "people",
+ "e": "💃🏾",
+ "d": "dancer tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "dancer_tone5",
+ "c": "people",
+ "e": "💃🏿",
+ "d": "dancer tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "man_dancing",
+ "c": "people",
+ "e": "🕺",
+ "d": "man dancing",
+ "u": "9.0"
+ },
+ {
+ "n": "man_dancing_tone1",
+ "c": "activity",
+ "e": "🕺🏻",
+ "d": "man dancing tone 1",
+ "u": "9.0"
+ },
+ {
+ "n": "man_dancing_tone2",
+ "c": "activity",
+ "e": "🕺🏼",
+ "d": "man dancing tone 2",
+ "u": "9.0"
+ },
+ {
+ "n": "man_dancing_tone3",
+ "c": "activity",
+ "e": "🕺🏽",
+ "d": "man dancing tone 3",
+ "u": "9.0"
+ },
+ {
+ "n": "man_dancing_tone4",
+ "c": "activity",
+ "e": "🕺🏾",
+ "d": "man dancing tone 4",
+ "u": "9.0"
+ },
+ {
+ "n": "man_dancing_tone5",
+ "c": "activity",
+ "e": "🕺🏿",
+ "d": "man dancing tone 5",
+ "u": "9.0"
+ },
+ {
+ "n": "levitate",
+ "c": "activity",
+ "e": "🕴",
+ "d": "man in business suit levitating",
+ "u": "7.0"
+ },
+ {
+ "n": "dancers",
+ "c": "people",
+ "e": "👯",
+ "d": "woman with bunny ears",
+ "u": "6.0"
+ },
+ {
+ "n": "fencer",
+ "c": "activity",
+ "e": "🤺",
+ "d": "fencer",
+ "u": "9.0"
+ },
+ {
+ "n": "horse_racing",
+ "c": "activity",
+ "e": "🏇",
+ "d": "horse racing",
+ "u": "6.0"
+ },
+ {
+ "n": "horse_racing_tone1",
+ "c": "activity",
+ "e": "🏇🏻",
+ "d": "horse racing tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "horse_racing_tone2",
+ "c": "activity",
+ "e": "🏇🏼",
+ "d": "horse racing tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "horse_racing_tone3",
+ "c": "activity",
+ "e": "🏇🏽",
+ "d": "horse racing tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "horse_racing_tone4",
+ "c": "activity",
+ "e": "🏇🏾",
+ "d": "horse racing tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "horse_racing_tone5",
+ "c": "activity",
+ "e": "🏇🏿",
+ "d": "horse racing tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "skier",
+ "c": "activity",
+ "e": "⛷",
+ "d": "skier",
+ "u": "5.2"
+ },
+ {
+ "n": "snowboarder",
+ "c": "activity",
+ "e": "🏂",
+ "d": "snowboarder",
+ "u": "6.0"
+ },
+ {
+ "n": "golfer",
+ "c": "activity",
+ "e": "🏌",
+ "d": "golfer",
+ "u": "7.0"
+ },
+ {
+ "n": "surfer",
+ "c": "activity",
+ "e": "🏄",
+ "d": "surfer",
+ "u": "6.0"
+ },
+ {
+ "n": "surfer_tone1",
+ "c": "activity",
+ "e": "🏄🏻",
+ "d": "surfer tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "surfer_tone2",
+ "c": "activity",
+ "e": "🏄🏼",
+ "d": "surfer tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "surfer_tone3",
+ "c": "activity",
+ "e": "🏄🏽",
+ "d": "surfer tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "surfer_tone4",
+ "c": "activity",
+ "e": "🏄🏾",
+ "d": "surfer tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "surfer_tone5",
+ "c": "activity",
+ "e": "🏄🏿",
+ "d": "surfer tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "rowboat",
+ "c": "activity",
+ "e": "🚣",
+ "d": "rowboat",
+ "u": "6.0"
+ },
+ {
+ "n": "rowboat_tone1",
+ "c": "activity",
+ "e": "🚣🏻",
+ "d": "rowboat tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "rowboat_tone2",
+ "c": "activity",
+ "e": "🚣🏼",
+ "d": "rowboat tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "rowboat_tone3",
+ "c": "activity",
+ "e": "🚣🏽",
+ "d": "rowboat tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "rowboat_tone4",
+ "c": "activity",
+ "e": "🚣🏾",
+ "d": "rowboat tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "rowboat_tone5",
+ "c": "activity",
+ "e": "🚣🏿",
+ "d": "rowboat tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "swimmer",
+ "c": "activity",
+ "e": "🏊",
+ "d": "swimmer",
+ "u": "6.0"
+ },
+ {
+ "n": "swimmer_tone1",
+ "c": "activity",
+ "e": "🏊🏻",
+ "d": "swimmer tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "swimmer_tone2",
+ "c": "activity",
+ "e": "🏊🏼",
+ "d": "swimmer tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "swimmer_tone3",
+ "c": "activity",
+ "e": "🏊🏽",
+ "d": "swimmer tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "swimmer_tone4",
+ "c": "activity",
+ "e": "🏊🏾",
+ "d": "swimmer tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "swimmer_tone5",
+ "c": "activity",
+ "e": "🏊🏿",
+ "d": "swimmer tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "basketball_player",
+ "c": "activity",
+ "e": "⛹",
+ "d": "person with ball",
+ "u": "5.2"
+ },
+ {
+ "n": "basketball_player_tone1",
+ "c": "activity",
+ "e": "⛹🏻",
+ "d": "person with ball tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "basketball_player_tone2",
+ "c": "activity",
+ "e": "⛹🏼",
+ "d": "person with ball tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "basketball_player_tone3",
+ "c": "activity",
+ "e": "⛹🏽",
+ "d": "person with ball tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "basketball_player_tone4",
+ "c": "activity",
+ "e": "⛹🏾",
+ "d": "person with ball tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "basketball_player_tone5",
+ "c": "activity",
+ "e": "⛹🏿",
+ "d": "person with ball tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "lifter",
+ "c": "activity",
+ "e": "🏋",
+ "d": "weight lifter",
+ "u": "7.0"
+ },
+ {
+ "n": "lifter_tone1",
+ "c": "activity",
+ "e": "🏋🏻",
+ "d": "weight lifter tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "lifter_tone2",
+ "c": "activity",
+ "e": "🏋🏼",
+ "d": "weight lifter tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "lifter_tone3",
+ "c": "activity",
+ "e": "🏋🏽",
+ "d": "weight lifter tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "lifter_tone4",
+ "c": "activity",
+ "e": "🏋🏾",
+ "d": "weight lifter tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "lifter_tone5",
+ "c": "activity",
+ "e": "🏋🏿",
+ "d": "weight lifter tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "bicyclist",
+ "c": "activity",
+ "e": "🚴",
+ "d": "bicyclist",
+ "u": "6.0"
+ },
+ {
+ "n": "bicyclist_tone1",
+ "c": "activity",
+ "e": "🚴🏻",
+ "d": "bicyclist tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "bicyclist_tone2",
+ "c": "activity",
+ "e": "🚴🏼",
+ "d": "bicyclist tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "bicyclist_tone3",
+ "c": "activity",
+ "e": "🚴🏽",
+ "d": "bicyclist tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "bicyclist_tone4",
+ "c": "activity",
+ "e": "🚴🏾",
+ "d": "bicyclist tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "bicyclist_tone5",
+ "c": "activity",
+ "e": "🚴🏿",
+ "d": "bicyclist tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "mountain_bicyclist",
+ "c": "activity",
+ "e": "🚵",
+ "d": "mountain bicyclist",
+ "u": "6.0"
+ },
+ {
+ "n": "mountain_bicyclist_tone1",
+ "c": "activity",
+ "e": "🚵🏻",
+ "d": "mountain bicyclist tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "mountain_bicyclist_tone2",
+ "c": "activity",
+ "e": "🚵🏼",
+ "d": "mountain bicyclist tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "mountain_bicyclist_tone3",
+ "c": "activity",
+ "e": "🚵🏽",
+ "d": "mountain bicyclist tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "mountain_bicyclist_tone4",
+ "c": "activity",
+ "e": "🚵🏾",
+ "d": "mountain bicyclist tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "mountain_bicyclist_tone5",
+ "c": "activity",
+ "e": "🚵🏿",
+ "d": "mountain bicyclist tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "cartwheel",
+ "c": "activity",
+ "e": "🤸",
+ "d": "person doing cartwheel",
+ "u": "9.0"
+ },
+ {
+ "n": "cartwheel_tone1",
+ "c": "activity",
+ "e": "🤸🏻",
+ "d": "person doing cartwheel tone 1",
+ "u": "9.0"
+ },
+ {
+ "n": "cartwheel_tone2",
+ "c": "activity",
+ "e": "🤸🏼",
+ "d": "person doing cartwheel tone 2",
+ "u": "9.0"
+ },
+ {
+ "n": "cartwheel_tone3",
+ "c": "activity",
+ "e": "🤸🏽",
+ "d": "person doing cartwheel tone 3",
+ "u": "9.0"
+ },
+ {
+ "n": "cartwheel_tone4",
+ "c": "activity",
+ "e": "🤸🏾",
+ "d": "person doing cartwheel tone 4",
+ "u": "9.0"
+ },
+ {
+ "n": "cartwheel_tone5",
+ "c": "activity",
+ "e": "🤸🏿",
+ "d": "person doing cartwheel tone 5",
+ "u": "9.0"
+ },
+ {
+ "n": "wrestlers",
+ "c": "activity",
+ "e": "🤼",
+ "d": "wrestlers",
+ "u": "9.0"
+ },
+ {
+ "n": "wrestlers_tone1",
+ "c": "activity",
+ "e": "🤼🏻",
+ "d": "wrestlers tone 1",
+ "u": "9.0"
+ },
+ {
+ "n": "wrestlers_tone2",
+ "c": "activity",
+ "e": "🤼🏼",
+ "d": "wrestlers tone 2",
+ "u": "9.0"
+ },
+ {
+ "n": "wrestlers_tone3",
+ "c": "activity",
+ "e": "🤼🏽",
+ "d": "wrestlers tone 3",
+ "u": "9.0"
+ },
+ {
+ "n": "wrestlers_tone4",
+ "c": "activity",
+ "e": "🤼🏾",
+ "d": "wrestlers tone 4",
+ "u": "9.0"
+ },
+ {
+ "n": "wrestlers_tone5",
+ "c": "activity",
+ "e": "🤼🏿",
+ "d": "wrestlers tone 5",
+ "u": "9.0"
+ },
+ {
+ "n": "water_polo",
+ "c": "activity",
+ "e": "🤽",
+ "d": "water polo",
+ "u": "9.0"
+ },
+ {
+ "n": "water_polo_tone1",
+ "c": "activity",
+ "e": "🤽🏻",
+ "d": "water polo tone 1",
+ "u": "9.0"
+ },
+ {
+ "n": "water_polo_tone2",
+ "c": "activity",
+ "e": "🤽🏼",
+ "d": "water polo tone 2",
+ "u": "9.0"
+ },
+ {
+ "n": "water_polo_tone3",
+ "c": "activity",
+ "e": "🤽🏽",
+ "d": "water polo tone 3",
+ "u": "9.0"
+ },
+ {
+ "n": "water_polo_tone4",
+ "c": "activity",
+ "e": "🤽🏾",
+ "d": "water polo tone 4",
+ "u": "9.0"
+ },
+ {
+ "n": "water_polo_tone5",
+ "c": "activity",
+ "e": "🤽🏿",
+ "d": "water polo tone 5",
+ "u": "9.0"
+ },
+ {
+ "n": "handball",
+ "c": "activity",
+ "e": "🤾",
+ "d": "handball",
+ "u": "9.0"
+ },
+ {
+ "n": "handball_tone1",
+ "c": "activity",
+ "e": "🤾🏻",
+ "d": "handball tone 1",
+ "u": "9.0"
+ },
+ {
+ "n": "handball_tone2",
+ "c": "activity",
+ "e": "🤾🏼",
+ "d": "handball tone 2",
+ "u": "9.0"
+ },
+ {
+ "n": "handball_tone3",
+ "c": "activity",
+ "e": "🤾🏽",
+ "d": "handball tone 3",
+ "u": "9.0"
+ },
+ {
+ "n": "handball_tone4",
+ "c": "activity",
+ "e": "🤾🏾",
+ "d": "handball tone 4",
+ "u": "9.0"
+ },
+ {
+ "n": "handball_tone5",
+ "c": "activity",
+ "e": "🤾🏿",
+ "d": "handball tone 5",
+ "u": "9.0"
+ },
+ {
+ "n": "juggling",
+ "c": "activity",
+ "e": "🤹",
+ "d": "juggling",
+ "u": "9.0"
+ },
+ {
+ "n": "juggling_tone1",
+ "c": "activity",
+ "e": "🤹🏻",
+ "d": "juggling tone 1",
+ "u": "9.0"
+ },
+ {
+ "n": "juggling_tone2",
+ "c": "activity",
+ "e": "🤹🏼",
+ "d": "juggling tone 2",
+ "u": "9.0"
+ },
+ {
+ "n": "juggling_tone3",
+ "c": "activity",
+ "e": "🤹🏽",
+ "d": "juggling tone 3",
+ "u": "9.0"
+ },
+ {
+ "n": "juggling_tone4",
+ "c": "activity",
+ "e": "🤹🏾",
+ "d": "juggling tone 4",
+ "u": "9.0"
+ },
+ {
+ "n": "juggling_tone5",
+ "c": "activity",
+ "e": "🤹🏿",
+ "d": "juggling tone 5",
+ "u": "9.0"
+ },
+ {
+ "n": "bath",
+ "c": "activity",
+ "e": "🛀",
+ "d": "bath",
+ "u": "6.0"
+ },
+ {
+ "n": "bath_tone1",
+ "c": "activity",
+ "e": "🛀🏻",
+ "d": "bath tone 1",
+ "u": "8.0"
+ },
+ {
+ "n": "bath_tone2",
+ "c": "activity",
+ "e": "🛀🏼",
+ "d": "bath tone 2",
+ "u": "8.0"
+ },
+ {
+ "n": "bath_tone3",
+ "c": "activity",
+ "e": "🛀🏽",
+ "d": "bath tone 3",
+ "u": "8.0"
+ },
+ {
+ "n": "bath_tone4",
+ "c": "activity",
+ "e": "🛀🏾",
+ "d": "bath tone 4",
+ "u": "8.0"
+ },
+ {
+ "n": "bath_tone5",
+ "c": "activity",
+ "e": "🛀🏿",
+ "d": "bath tone 5",
+ "u": "8.0"
+ },
+ {
+ "n": "sleeping_accommodation",
+ "c": "objects",
+ "e": "🛌",
+ "d": "sleeping accommodation",
+ "u": "7.0"
+ },
+ {
+ "n": "two_women_holding_hands",
+ "c": "people",
+ "e": "👭",
+ "d": "two women holding hands",
+ "u": "6.0"
+ },
+ {
+ "n": "couple",
+ "c": "people",
+ "e": "👫",
+ "d": "man and woman holding hands",
+ "u": "6.0"
+ },
+ {
+ "n": "two_men_holding_hands",
+ "c": "people",
+ "e": "👬",
+ "d": "two men holding hands",
+ "u": "6.0"
+ },
+ {
+ "n": "couplekiss",
+ "c": "people",
+ "e": "💏",
+ "d": "kiss",
+ "u": "6.0"
+ },
+ {
+ "n": "kiss_mm",
+ "c": "people",
+ "e": "👨‍❤️‍💋‍👨",
+ "d": "kiss (man,man)",
+ "u": "6.0"
+ },
+ {
+ "n": "kiss_ww",
+ "c": "people",
+ "e": "👩‍❤️‍💋‍👩",
+ "d": "kiss (woman,woman)",
+ "u": "6.0"
+ },
+ {
+ "n": "couple_with_heart",
+ "c": "people",
+ "e": "💑",
+ "d": "couple with heart",
+ "u": "6.0"
+ },
+ {
+ "n": "couple_mm",
+ "c": "people",
+ "e": "👨‍❤️‍👨",
+ "d": "couple (man,man)",
+ "u": "6.0"
+ },
+ {
+ "n": "couple_ww",
+ "c": "people",
+ "e": "👩‍❤️‍👩",
+ "d": "couple (woman,woman)",
+ "u": "6.0"
+ },
+ {
+ "n": "family_mwg",
+ "c": "people",
+ "e": "👨‍👩‍👧",
+ "d": "family (man,woman,girl)",
+ "u": "6.0"
+ },
+ {
+ "n": "family_mwgb",
+ "c": "people",
+ "e": "👨‍👩‍👧‍👦",
+ "d": "family (man,woman,girl,boy)",
+ "u": "6.0"
+ },
+ {
+ "n": "family_mwbb",
+ "c": "people",
+ "e": "👨‍👩‍👦‍👦",
+ "d": "family (man,woman,boy,boy)",
+ "u": "6.0"
+ },
+ {
+ "n": "family_mwgg",
+ "c": "people",
+ "e": "👨‍👩‍👧‍👧",
+ "d": "family (man,woman,girl,girl)",
+ "u": "6.0"
+ },
+ {
+ "n": "family_mmb",
+ "c": "people",
+ "e": "👨‍👨‍👦",
+ "d": "family (man,man,boy)",
+ "u": "6.0"
+ },
+ {
+ "n": "family_mmg",
+ "c": "people",
+ "e": "👨‍👨‍👧",
+ "d": "family (man,man,girl)",
+ "u": "6.0"
+ },
+ {
+ "n": "family_mmgb",
+ "c": "people",
+ "e": "👨‍👨‍👧‍👦",
+ "d": "family (man,man,girl,boy)",
+ "u": "6.0"
+ },
+ {
+ "n": "family_mmbb",
+ "c": "people",
+ "e": "👨‍👨‍👦‍👦",
+ "d": "family (man,man,boy,boy)",
+ "u": "6.0"
+ },
+ {
+ "n": "family_mmgg",
+ "c": "people",
+ "e": "👨‍👨‍👧‍👧",
+ "d": "family (man,man,girl,girl)",
+ "u": "6.0"
+ },
+ {
+ "n": "family_wwb",
+ "c": "people",
+ "e": "👩‍👩‍👦",
+ "d": "family (woman,woman,boy)",
+ "u": "6.0"
+ },
+ {
+ "n": "family_wwg",
+ "c": "people",
+ "e": "👩‍👩‍👧",
+ "d": "family (woman,woman,girl)",
+ "u": "6.0"
+ },
+ {
+ "n": "family_wwgb",
+ "c": "people",
+ "e": "👩‍👩‍👧‍👦",
+ "d": "family (woman,woman,girl,boy)",
+ "u": "6.0"
+ },
+ {
+ "n": "family_wwbb",
+ "c": "people",
+ "e": "👩‍👩‍👦‍👦",
+ "d": "family (woman,woman,boy,boy)",
+ "u": "6.0"
+ },
+ {
+ "n": "family_wwgg",
+ "c": "people",
+ "e": "👩‍👩‍👧‍👧",
+ "d": "family (woman,woman,girl,girl)",
+ "u": "6.0"
+ },
+ {
+ "n": "speaking_head",
+ "c": "people",
+ "e": "🗣",
+ "d": "speaking head in silhouette",
+ "u": "7.0"
+ },
+ {
+ "n": "bust_in_silhouette",
+ "c": "people",
+ "e": "👤",
+ "d": "bust in silhouette",
+ "u": "6.0"
+ },
+ {
+ "n": "busts_in_silhouette",
+ "c": "people",
+ "e": "👥",
+ "d": "busts in silhouette",
+ "u": "6.0"
+ },
+ {
+ "n": "family",
+ "c": "people",
+ "e": "👪",
+ "d": "family",
+ "u": "6.0"
+ },
+ {
+ "n": "footprints",
+ "c": "people",
+ "e": "👣",
+ "d": "footprints",
+ "u": "6.0"
+ },
+ {
+ "n": "tone1",
+ "c": "modifier",
+ "e": "🏻",
+ "d": "emoji modifier Fitzpatrick type-1-2",
+ "u": "8.0"
+ },
+ {
+ "n": "tone2",
+ "c": "modifier",
+ "e": "🏼",
+ "d": "emoji modifier Fitzpatrick type-3",
+ "u": "8.0"
+ },
+ {
+ "n": "tone3",
+ "c": "modifier",
+ "e": "🏽",
+ "d": "emoji modifier Fitzpatrick type-4",
+ "u": "8.0"
+ },
+ {
+ "n": "tone4",
+ "c": "modifier",
+ "e": "🏾",
+ "d": "emoji modifier Fitzpatrick type-5",
+ "u": "8.0"
+ },
+ {
+ "n": "tone5",
+ "c": "modifier",
+ "e": "🏿",
+ "d": "emoji modifier Fitzpatrick type-6",
+ "u": "8.0"
+ },
+ {
+ "n": "monkey_face",
+ "c": "nature",
+ "e": "🐵",
+ "d": "monkey face",
+ "u": "6.0"
+ },
+ {
+ "n": "monkey",
+ "c": "nature",
+ "e": "🐒",
+ "d": "monkey",
+ "u": "6.0"
+ },
+ {
+ "n": "gorilla",
+ "c": "nature",
+ "e": "🦍",
+ "d": "gorilla",
+ "u": "9.0"
+ },
+ {
+ "n": "dog",
+ "c": "nature",
+ "e": "🐶",
+ "d": "dog face",
+ "u": "6.0"
+ },
+ {
+ "n": "dog2",
+ "c": "nature",
+ "e": "🐕",
+ "d": "dog",
+ "u": "6.0"
+ },
+ {
+ "n": "poodle",
+ "c": "nature",
+ "e": "🐩",
+ "d": "poodle",
+ "u": "6.0"
+ },
+ {
+ "n": "wolf",
+ "c": "nature",
+ "e": "🐺",
+ "d": "wolf face",
+ "u": "6.0"
+ },
+ {
+ "n": "fox",
+ "c": "nature",
+ "e": "🦊",
+ "d": "fox face",
+ "u": "9.0"
+ },
+ {
+ "n": "cat",
+ "c": "nature",
+ "e": "🐱",
+ "d": "cat face",
+ "u": "6.0"
+ },
+ {
+ "n": "cat2",
+ "c": "nature",
+ "e": "🐈",
+ "d": "cat",
+ "u": "6.0"
+ },
+ {
+ "n": "lion_face",
+ "c": "nature",
+ "e": "🦁",
+ "d": "lion face",
+ "u": "8.0"
+ },
+ {
+ "n": "tiger",
+ "c": "nature",
+ "e": "🐯",
+ "d": "tiger face",
+ "u": "6.0"
+ },
+ {
+ "n": "tiger2",
+ "c": "nature",
+ "e": "🐅",
+ "d": "tiger",
+ "u": "6.0"
+ },
+ {
+ "n": "leopard",
+ "c": "nature",
+ "e": "🐆",
+ "d": "leopard",
+ "u": "6.0"
+ },
+ {
+ "n": "horse",
+ "c": "nature",
+ "e": "🐴",
+ "d": "horse face",
+ "u": "6.0"
+ },
+ {
+ "n": "racehorse",
+ "c": "nature",
+ "e": "🐎",
+ "d": "horse",
+ "u": "6.0"
+ },
+ {
+ "n": "unicorn",
+ "c": "nature",
+ "e": "🦄",
+ "d": "unicorn face",
+ "u": "8.0"
+ },
+ {
+ "n": "deer",
+ "c": "nature",
+ "e": "🦌",
+ "d": "deer",
+ "u": "9.0"
+ },
+ {
+ "n": "cow",
+ "c": "nature",
+ "e": "🐮",
+ "d": "cow face",
+ "u": "6.0"
+ },
+ {
+ "n": "ox",
+ "c": "nature",
+ "e": "🐂",
+ "d": "ox",
+ "u": "6.0"
+ },
+ {
+ "n": "water_buffalo",
+ "c": "nature",
+ "e": "🐃",
+ "d": "water buffalo",
+ "u": "6.0"
+ },
+ {
+ "n": "cow2",
+ "c": "nature",
+ "e": "🐄",
+ "d": "cow",
+ "u": "6.0"
+ },
+ {
+ "n": "pig",
+ "c": "nature",
+ "e": "🐷",
+ "d": "pig face",
+ "u": "6.0"
+ },
+ {
+ "n": "pig2",
+ "c": "nature",
+ "e": "🐖",
+ "d": "pig",
+ "u": "6.0"
+ },
+ {
+ "n": "boar",
+ "c": "nature",
+ "e": "🐗",
+ "d": "boar",
+ "u": "6.0"
+ },
+ {
+ "n": "pig_nose",
+ "c": "nature",
+ "e": "🐽",
+ "d": "pig nose",
+ "u": "6.0"
+ },
+ {
+ "n": "ram",
+ "c": "nature",
+ "e": "🐏",
+ "d": "ram",
+ "u": "6.0"
+ },
+ {
+ "n": "sheep",
+ "c": "nature",
+ "e": "🐑",
+ "d": "sheep",
+ "u": "6.0"
+ },
+ {
+ "n": "goat",
+ "c": "nature",
+ "e": "🐐",
+ "d": "goat",
+ "u": "6.0"
+ },
+ {
+ "n": "dromedary_camel",
+ "c": "nature",
+ "e": "🐪",
+ "d": "dromedary camel",
+ "u": "6.0"
+ },
+ {
+ "n": "camel",
+ "c": "nature",
+ "e": "🐫",
+ "d": "bactrian camel",
+ "u": "6.0"
+ },
+ {
+ "n": "elephant",
+ "c": "nature",
+ "e": "🐘",
+ "d": "elephant",
+ "u": "6.0"
+ },
+ {
+ "n": "rhino",
+ "c": "nature",
+ "e": "🦏",
+ "d": "rhinoceros",
+ "u": "9.0"
+ },
+ {
+ "n": "mouse",
+ "c": "nature",
+ "e": "🐭",
+ "d": "mouse face",
+ "u": "6.0"
+ },
+ {
+ "n": "mouse2",
+ "c": "nature",
+ "e": "🐁",
+ "d": "mouse",
+ "u": "6.0"
+ },
+ {
+ "n": "rat",
+ "c": "nature",
+ "e": "🐀",
+ "d": "rat",
+ "u": "6.0"
+ },
+ {
+ "n": "hamster",
+ "c": "nature",
+ "e": "🐹",
+ "d": "hamster face",
+ "u": "6.0"
+ },
+ {
+ "n": "rabbit",
+ "c": "nature",
+ "e": "🐰",
+ "d": "rabbit face",
+ "u": "6.0"
+ },
+ {
+ "n": "rabbit2",
+ "c": "nature",
+ "e": "🐇",
+ "d": "rabbit",
+ "u": "6.0"
+ },
+ {
+ "n": "chipmunk",
+ "c": "nature",
+ "e": "🐿",
+ "d": "chipmunk",
+ "u": "7.0"
+ },
+ {
+ "n": "bat",
+ "c": "nature",
+ "e": "🦇",
+ "d": "bat",
+ "u": "9.0"
+ },
+ {
+ "n": "bear",
+ "c": "nature",
+ "e": "🐻",
+ "d": "bear face",
+ "u": "6.0"
+ },
+ {
+ "n": "koala",
+ "c": "nature",
+ "e": "🐨",
+ "d": "koala",
+ "u": "6.0"
+ },
+ {
+ "n": "panda_face",
+ "c": "nature",
+ "e": "🐼",
+ "d": "panda face",
+ "u": "6.0"
+ },
+ {
+ "n": "feet",
+ "c": "nature",
+ "e": "🐾",
+ "d": "paw prints",
+ "u": "6.0"
+ },
+ {
+ "n": "turkey",
+ "c": "nature",
+ "e": "🦃",
+ "d": "turkey",
+ "u": "8.0"
+ },
+ {
+ "n": "chicken",
+ "c": "nature",
+ "e": "🐔",
+ "d": "chicken",
+ "u": "6.0"
+ },
+ {
+ "n": "rooster",
+ "c": "nature",
+ "e": "🐓",
+ "d": "rooster",
+ "u": "6.0"
+ },
+ {
+ "n": "hatching_chick",
+ "c": "nature",
+ "e": "🐣",
+ "d": "hatching chick",
+ "u": "6.0"
+ },
+ {
+ "n": "baby_chick",
+ "c": "nature",
+ "e": "🐤",
+ "d": "baby chick",
+ "u": "6.0"
+ },
+ {
+ "n": "hatched_chick",
+ "c": "nature",
+ "e": "🐥",
+ "d": "front-facing baby chick",
+ "u": "6.0"
+ },
+ {
+ "n": "bird",
+ "c": "nature",
+ "e": "🐦",
+ "d": "bird",
+ "u": "6.0"
+ },
+ {
+ "n": "penguin",
+ "c": "nature",
+ "e": "🐧",
+ "d": "penguin",
+ "u": "6.0"
+ },
+ {
+ "n": "dove",
+ "c": "nature",
+ "e": "🕊",
+ "d": "dove of peace",
+ "u": "7.0"
+ },
+ {
+ "n": "eagle",
+ "c": "nature",
+ "e": "🦅",
+ "d": "eagle",
+ "u": "9.0"
+ },
+ {
+ "n": "duck",
+ "c": "nature",
+ "e": "🦆",
+ "d": "duck",
+ "u": "9.0"
+ },
+ {
+ "n": "owl",
+ "c": "nature",
+ "e": "🦉",
+ "d": "owl",
+ "u": "9.0"
+ },
+ {
+ "n": "frog",
+ "c": "nature",
+ "e": "🐸",
+ "d": "frog face",
+ "u": "6.0"
+ },
+ {
+ "n": "crocodile",
+ "c": "nature",
+ "e": "🐊",
+ "d": "crocodile",
+ "u": "6.0"
+ },
+ {
+ "n": "turtle",
+ "c": "nature",
+ "e": "🐢",
+ "d": "turtle",
+ "u": "6.0"
+ },
+ {
+ "n": "lizard",
+ "c": "nature",
+ "e": "🦎",
+ "d": "lizard",
+ "u": "9.0"
+ },
+ {
+ "n": "snake",
+ "c": "nature",
+ "e": "🐍",
+ "d": "snake",
+ "u": "6.0"
+ },
+ {
+ "n": "dragon_face",
+ "c": "nature",
+ "e": "🐲",
+ "d": "dragon face",
+ "u": "6.0"
+ },
+ {
+ "n": "dragon",
+ "c": "nature",
+ "e": "🐉",
+ "d": "dragon",
+ "u": "6.0"
+ },
+ {
+ "n": "whale",
+ "c": "nature",
+ "e": "🐳",
+ "d": "spouting whale",
+ "u": "6.0"
+ },
+ {
+ "n": "whale2",
+ "c": "nature",
+ "e": "🐋",
+ "d": "whale",
+ "u": "6.0"
+ },
+ {
+ "n": "dolphin",
+ "c": "nature",
+ "e": "🐬",
+ "d": "dolphin",
+ "u": "6.0"
+ },
+ {
+ "n": "fish",
+ "c": "nature",
+ "e": "🐟",
+ "d": "fish",
+ "u": "6.0"
+ },
+ {
+ "n": "tropical_fish",
+ "c": "nature",
+ "e": "🐠",
+ "d": "tropical fish",
+ "u": "6.0"
+ },
+ {
+ "n": "blowfish",
+ "c": "nature",
+ "e": "🐡",
+ "d": "blowfish",
+ "u": "6.0"
+ },
+ {
+ "n": "shark",
+ "c": "nature",
+ "e": "🦈",
+ "d": "shark",
+ "u": "9.0"
+ },
+ {
+ "n": "octopus",
+ "c": "nature",
+ "e": "🐙",
+ "d": "octopus",
+ "u": "6.0"
+ },
+ {
+ "n": "shell",
+ "c": "nature",
+ "e": "🐚",
+ "d": "spiral shell",
+ "u": "6.0"
+ },
+ {
+ "n": "snail",
+ "c": "nature",
+ "e": "🐌",
+ "d": "snail",
+ "u": "6.0"
+ },
+ {
+ "n": "butterfly",
+ "c": "nature",
+ "e": "🦋",
+ "d": "butterfly",
+ "u": "9.0"
+ },
+ {
+ "n": "bug",
+ "c": "nature",
+ "e": "🐛",
+ "d": "bug",
+ "u": "6.0"
+ },
+ {
+ "n": "ant",
+ "c": "nature",
+ "e": "🐜",
+ "d": "ant",
+ "u": "6.0"
+ },
+ {
+ "n": "bee",
+ "c": "nature",
+ "e": "🐝",
+ "d": "honeybee",
+ "u": "6.0"
+ },
+ {
+ "n": "beetle",
+ "c": "nature",
+ "e": "🐞",
+ "d": "lady beetle",
+ "u": "6.0"
+ },
+ {
+ "n": "spider",
+ "c": "nature",
+ "e": "🕷",
+ "d": "spider",
+ "u": "7.0"
+ },
+ {
+ "n": "spider_web",
+ "c": "nature",
+ "e": "🕸",
+ "d": "spider web",
+ "u": "7.0"
+ },
+ {
+ "n": "scorpion",
+ "c": "nature",
+ "e": "🦂",
+ "d": "scorpion",
+ "u": "8.0"
+ },
+ {
+ "n": "bouquet",
+ "c": "nature",
+ "e": "💐",
+ "d": "bouquet",
+ "u": "6.0"
+ },
+ {
+ "n": "cherry_blossom",
+ "c": "nature",
+ "e": "🌸",
+ "d": "cherry blossom",
+ "u": "6.0"
+ },
+ {
+ "n": "white_flower",
+ "c": "symbols",
+ "e": "💮",
+ "d": "white flower",
+ "u": "6.0"
+ },
+ {
+ "n": "rosette",
+ "c": "activity",
+ "e": "🏵",
+ "d": "rosette",
+ "u": "7.0"
+ },
+ {
+ "n": "rose",
+ "c": "nature",
+ "e": "🌹",
+ "d": "rose",
+ "u": "6.0"
+ },
+ {
+ "n": "wilted_rose",
+ "c": "nature",
+ "e": "🥀",
+ "d": "wilted flower",
+ "u": "9.0"
+ },
+ {
+ "n": "hibiscus",
+ "c": "nature",
+ "e": "🌺",
+ "d": "hibiscus",
+ "u": "6.0"
+ },
+ {
+ "n": "sunflower",
+ "c": "nature",
+ "e": "🌻",
+ "d": "sunflower",
+ "u": "6.0"
+ },
+ {
+ "n": "blossom",
+ "c": "nature",
+ "e": "🌼",
+ "d": "blossom",
+ "u": "6.0"
+ },
+ {
+ "n": "tulip",
+ "c": "nature",
+ "e": "🌷",
+ "d": "tulip",
+ "u": "6.0"
+ },
+ {
+ "n": "seedling",
+ "c": "nature",
+ "e": "🌱",
+ "d": "seedling",
+ "u": "6.0"
+ },
+ {
+ "n": "evergreen_tree",
+ "c": "nature",
+ "e": "🌲",
+ "d": "evergreen tree",
+ "u": "6.0"
+ },
+ {
+ "n": "deciduous_tree",
+ "c": "nature",
+ "e": "🌳",
+ "d": "deciduous tree",
+ "u": "6.0"
+ },
+ {
+ "n": "palm_tree",
+ "c": "nature",
+ "e": "🌴",
+ "d": "palm tree",
+ "u": "6.0"
+ },
+ {
+ "n": "cactus",
+ "c": "nature",
+ "e": "🌵",
+ "d": "cactus",
+ "u": "6.0"
+ },
+ {
+ "n": "ear_of_rice",
+ "c": "nature",
+ "e": "🌾",
+ "d": "ear of rice",
+ "u": "6.0"
+ },
+ {
+ "n": "herb",
+ "c": "nature",
+ "e": "🌿",
+ "d": "herb",
+ "u": "6.0"
+ },
+ {
+ "n": "shamrock",
+ "c": "nature",
+ "e": "☘",
+ "d": "shamrock",
+ "u": "4.1"
+ },
+ {
+ "n": "four_leaf_clover",
+ "c": "nature",
+ "e": "🍀",
+ "d": "four leaf clover",
+ "u": "6.0"
+ },
+ {
+ "n": "maple_leaf",
+ "c": "nature",
+ "e": "🍁",
+ "d": "maple leaf",
+ "u": "6.0"
+ },
+ {
+ "n": "fallen_leaf",
+ "c": "nature",
+ "e": "🍂",
+ "d": "fallen leaf",
+ "u": "6.0"
+ },
+ {
+ "n": "leaves",
+ "c": "nature",
+ "e": "🍃",
+ "d": "leaf fluttering in wind",
+ "u": "6.0"
+ },
+ {
+ "n": "mushroom",
+ "c": "nature",
+ "e": "🍄",
+ "d": "mushroom",
+ "u": "6.0"
+ },
+ {
+ "n": "grapes",
+ "c": "food",
+ "e": "🍇",
+ "d": "grapes",
+ "u": "6.0"
+ },
+ {
+ "n": "melon",
+ "c": "food",
+ "e": "🍈",
+ "d": "melon",
+ "u": "6.0"
+ },
+ {
+ "n": "watermelon",
+ "c": "food",
+ "e": "🍉",
+ "d": "watermelon",
+ "u": "6.0"
+ },
+ {
+ "n": "tangerine",
+ "c": "food",
+ "e": "🍊",
+ "d": "tangerine",
+ "u": "6.0"
+ },
+ {
+ "n": "lemon",
+ "c": "food",
+ "e": "🍋",
+ "d": "lemon",
+ "u": "6.0"
+ },
+ {
+ "n": "banana",
+ "c": "food",
+ "e": "🍌",
+ "d": "banana",
+ "u": "6.0"
+ },
+ {
+ "n": "pineapple",
+ "c": "food",
+ "e": "🍍",
+ "d": "pineapple",
+ "u": "6.0"
+ },
+ {
+ "n": "apple",
+ "c": "food",
+ "e": "🍎",
+ "d": "red apple",
+ "u": "6.0"
+ },
+ {
+ "n": "green_apple",
+ "c": "food",
+ "e": "🍏",
+ "d": "green apple",
+ "u": "6.0"
+ },
+ {
+ "n": "pear",
+ "c": "food",
+ "e": "🍐",
+ "d": "pear",
+ "u": "6.0"
+ },
+ {
+ "n": "peach",
+ "c": "food",
+ "e": "🍑",
+ "d": "peach",
+ "u": "6.0"
+ },
+ {
+ "n": "cherries",
+ "c": "food",
+ "e": "🍒",
+ "d": "cherries",
+ "u": "6.0"
+ },
+ {
+ "n": "strawberry",
+ "c": "food",
+ "e": "🍓",
+ "d": "strawberry",
+ "u": "6.0"
+ },
+ {
+ "n": "kiwi",
+ "c": "food",
+ "e": "🥝",
+ "d": "kiwifruit",
+ "u": "9.0"
+ },
+ {
+ "n": "tomato",
+ "c": "food",
+ "e": "🍅",
+ "d": "tomato",
+ "u": "6.0"
+ },
+ {
+ "n": "avocado",
+ "c": "food",
+ "e": "🥑",
+ "d": "avocado",
+ "u": "9.0"
+ },
+ {
+ "n": "eggplant",
+ "c": "food",
+ "e": "🍆",
+ "d": "aubergine",
+ "u": "6.0"
+ },
+ {
+ "n": "potato",
+ "c": "food",
+ "e": "🥔",
+ "d": "potato",
+ "u": "9.0"
+ },
+ {
+ "n": "carrot",
+ "c": "food",
+ "e": "🥕",
+ "d": "carrot",
+ "u": "9.0"
+ },
+ {
+ "n": "corn",
+ "c": "food",
+ "e": "🌽",
+ "d": "ear of maize",
+ "u": "6.0"
+ },
+ {
+ "n": "hot_pepper",
+ "c": "food",
+ "e": "🌶",
+ "d": "hot pepper",
+ "u": "7.0"
+ },
+ {
+ "n": "cucumber",
+ "c": "food",
+ "e": "🥒",
+ "d": "cucumber",
+ "u": "9.0"
+ },
+ {
+ "n": "peanuts",
+ "c": "food",
+ "e": "🥜",
+ "d": "peanuts",
+ "u": "9.0"
+ },
+ {
+ "n": "chestnut",
+ "c": "nature",
+ "e": "🌰",
+ "d": "chestnut",
+ "u": "6.0"
+ },
+ {
+ "n": "bread",
+ "c": "food",
+ "e": "🍞",
+ "d": "bread",
+ "u": "6.0"
+ },
+ {
+ "n": "croissant",
+ "c": "food",
+ "e": "🥐",
+ "d": "croissant",
+ "u": "9.0"
+ },
+ {
+ "n": "french_bread",
+ "c": "food",
+ "e": "🥖",
+ "d": "baguette bread",
+ "u": "9.0"
+ },
+ {
+ "n": "pancakes",
+ "c": "food",
+ "e": "🥞",
+ "d": "pancakes",
+ "u": "9.0"
+ },
+ {
+ "n": "cheese",
+ "c": "food",
+ "e": "🧀",
+ "d": "cheese wedge",
+ "u": "8.0"
+ },
+ {
+ "n": "meat_on_bone",
+ "c": "food",
+ "e": "🍖",
+ "d": "meat on bone",
+ "u": "6.0"
+ },
+ {
+ "n": "poultry_leg",
+ "c": "food",
+ "e": "🍗",
+ "d": "poultry leg",
+ "u": "6.0"
+ },
+ {
+ "n": "bacon",
+ "c": "food",
+ "e": "🥓",
+ "d": "bacon",
+ "u": "9.0"
+ },
+ {
+ "n": "hamburger",
+ "c": "food",
+ "e": "🍔",
+ "d": "hamburger",
+ "u": "6.0"
+ },
+ {
+ "n": "fries",
+ "c": "food",
+ "e": "🍟",
+ "d": "french fries",
+ "u": "6.0"
+ },
+ {
+ "n": "pizza",
+ "c": "food",
+ "e": "🍕",
+ "d": "slice of pizza",
+ "u": "6.0"
+ },
+ {
+ "n": "hotdog",
+ "c": "food",
+ "e": "🌭",
+ "d": "hot dog",
+ "u": "8.0"
+ },
+ {
+ "n": "taco",
+ "c": "food",
+ "e": "🌮",
+ "d": "taco",
+ "u": "8.0"
+ },
+ {
+ "n": "burrito",
+ "c": "food",
+ "e": "🌯",
+ "d": "burrito",
+ "u": "8.0"
+ },
+ {
+ "n": "stuffed_flatbread",
+ "c": "food",
+ "e": "🥙",
+ "d": "stuffed flatbread",
+ "u": "9.0"
+ },
+ {
+ "n": "egg",
+ "c": "food",
+ "e": "🥚",
+ "d": "egg",
+ "u": "9.0"
+ },
+ {
+ "n": "cooking",
+ "c": "food",
+ "e": "🍳",
+ "d": "cooking",
+ "u": "6.0"
+ },
+ {
+ "n": "shallow_pan_of_food",
+ "c": "food",
+ "e": "🥘",
+ "d": "shallow pan of food",
+ "u": "9.0"
+ },
+ {
+ "n": "stew",
+ "c": "food",
+ "e": "🍲",
+ "d": "pot of food",
+ "u": "6.0"
+ },
+ {
+ "n": "salad",
+ "c": "food",
+ "e": "🥗",
+ "d": "green salad",
+ "u": "9.0"
+ },
+ {
+ "n": "popcorn",
+ "c": "food",
+ "e": "🍿",
+ "d": "popcorn",
+ "u": "8.0"
+ },
+ {
+ "n": "bento",
+ "c": "food",
+ "e": "🍱",
+ "d": "bento box",
+ "u": "6.0"
+ },
+ {
+ "n": "rice_cracker",
+ "c": "food",
+ "e": "🍘",
+ "d": "rice cracker",
+ "u": "6.0"
+ },
+ {
+ "n": "rice_ball",
+ "c": "food",
+ "e": "🍙",
+ "d": "rice ball",
+ "u": "6.0"
+ },
+ {
+ "n": "rice",
+ "c": "food",
+ "e": "🍚",
+ "d": "cooked rice",
+ "u": "6.0"
+ },
+ {
+ "n": "curry",
+ "c": "food",
+ "e": "🍛",
+ "d": "curry and rice",
+ "u": "6.0"
+ },
+ {
+ "n": "ramen",
+ "c": "food",
+ "e": "🍜",
+ "d": "steaming bowl",
+ "u": "6.0"
+ },
+ {
+ "n": "spaghetti",
+ "c": "food",
+ "e": "🍝",
+ "d": "spaghetti",
+ "u": "6.0"
+ },
+ {
+ "n": "sweet_potato",
+ "c": "food",
+ "e": "🍠",
+ "d": "roasted sweet potato",
+ "u": "6.0"
+ },
+ {
+ "n": "oden",
+ "c": "food",
+ "e": "🍢",
+ "d": "oden",
+ "u": "6.0"
+ },
+ {
+ "n": "sushi",
+ "c": "food",
+ "e": "🍣",
+ "d": "sushi",
+ "u": "6.0"
+ },
+ {
+ "n": "fried_shrimp",
+ "c": "food",
+ "e": "🍤",
+ "d": "fried shrimp",
+ "u": "6.0"
+ },
+ {
+ "n": "fish_cake",
+ "c": "food",
+ "e": "🍥",
+ "d": "fish cake with swirl design",
+ "u": "6.0"
+ },
+ {
+ "n": "dango",
+ "c": "food",
+ "e": "🍡",
+ "d": "dango",
+ "u": "6.0"
+ },
+ {
+ "n": "crab",
+ "c": "nature",
+ "e": "🦀",
+ "d": "crab",
+ "u": "8.0"
+ },
+ {
+ "n": "shrimp",
+ "c": "nature",
+ "e": "🦐",
+ "d": "shrimp",
+ "u": "9.0"
+ },
+ {
+ "n": "squid",
+ "c": "nature",
+ "e": "🦑",
+ "d": "squid",
+ "u": "9.0"
+ },
+ {
+ "n": "icecream",
+ "c": "food",
+ "e": "🍦",
+ "d": "soft ice cream",
+ "u": "6.0"
+ },
+ {
+ "n": "shaved_ice",
+ "c": "food",
+ "e": "🍧",
+ "d": "shaved ice",
+ "u": "6.0"
+ },
+ {
+ "n": "ice_cream",
+ "c": "food",
+ "e": "🍨",
+ "d": "ice cream",
+ "u": "6.0"
+ },
+ {
+ "n": "doughnut",
+ "c": "food",
+ "e": "🍩",
+ "d": "doughnut",
+ "u": "6.0"
+ },
+ {
+ "n": "cookie",
+ "c": "food",
+ "e": "🍪",
+ "d": "cookie",
+ "u": "6.0"
+ },
+ {
+ "n": "birthday",
+ "c": "food",
+ "e": "🎂",
+ "d": "birthday cake",
+ "u": "6.0"
+ },
+ {
+ "n": "cake",
+ "c": "food",
+ "e": "🍰",
+ "d": "shortcake",
+ "u": "6.0"
+ },
+ {
+ "n": "chocolate_bar",
+ "c": "food",
+ "e": "🍫",
+ "d": "chocolate bar",
+ "u": "6.0"
+ },
+ {
+ "n": "candy",
+ "c": "food",
+ "e": "🍬",
+ "d": "candy",
+ "u": "6.0"
+ },
+ {
+ "n": "lollipop",
+ "c": "food",
+ "e": "🍭",
+ "d": "lollipop",
+ "u": "6.0"
+ },
+ {
+ "n": "custard",
+ "c": "food",
+ "e": "🍮",
+ "d": "custard",
+ "u": "6.0"
+ },
+ {
+ "n": "honey_pot",
+ "c": "food",
+ "e": "🍯",
+ "d": "honey pot",
+ "u": "6.0"
+ },
+ {
+ "n": "baby_bottle",
+ "c": "food",
+ "e": "🍼",
+ "d": "baby bottle",
+ "u": "6.0"
+ },
+ {
+ "n": "milk",
+ "c": "food",
+ "e": "🥛",
+ "d": "glass of milk",
+ "u": "9.0"
+ },
+ {
+ "n": "coffee",
+ "c": "food",
+ "e": "☕",
+ "d": "hot beverage",
+ "u": "4.0"
+ },
+ {
+ "n": "tea",
+ "c": "food",
+ "e": "🍵",
+ "d": "teacup without handle",
+ "u": "6.0"
+ },
+ {
+ "n": "sake",
+ "c": "food",
+ "e": "🍶",
+ "d": "sake bottle and cup",
+ "u": "6.0"
+ },
+ {
+ "n": "champagne",
+ "c": "food",
+ "e": "🍾",
+ "d": "bottle with popping cork",
+ "u": "8.0"
+ },
+ {
+ "n": "wine_glass",
+ "c": "food",
+ "e": "🍷",
+ "d": "wine glass",
+ "u": "6.0"
+ },
+ {
+ "n": "cocktail",
+ "c": "food",
+ "e": "🍸",
+ "d": "cocktail glass",
+ "u": "6.0"
+ },
+ {
+ "n": "tropical_drink",
+ "c": "food",
+ "e": "🍹",
+ "d": "tropical drink",
+ "u": "6.0"
+ },
+ {
+ "n": "beer",
+ "c": "food",
+ "e": "🍺",
+ "d": "beer mug",
+ "u": "6.0"
+ },
+ {
+ "n": "beers",
+ "c": "food",
+ "e": "🍻",
+ "d": "clinking beer mugs",
+ "u": "6.0"
+ },
+ {
+ "n": "champagne_glass",
+ "c": "food",
+ "e": "🥂",
+ "d": "clinking glasses",
+ "u": "9.0"
+ },
+ {
+ "n": "tumbler_glass",
+ "c": "food",
+ "e": "🥃",
+ "d": "tumbler glass",
+ "u": "9.0"
+ },
+ {
+ "n": "fork_knife_plate",
+ "c": "food",
+ "e": "🍽",
+ "d": "fork and knife with plate",
+ "u": "7.0"
+ },
+ {
+ "n": "fork_and_knife",
+ "c": "food",
+ "e": "🍴",
+ "d": "fork and knife",
+ "u": "6.0"
+ },
+ {
+ "n": "spoon",
+ "c": "food",
+ "e": "🥄",
+ "d": "spoon",
+ "u": "9.0"
+ },
+ {
+ "n": "knife",
+ "c": "objects",
+ "e": "🔪",
+ "d": "hocho",
+ "u": "6.0"
+ },
+ {
+ "n": "amphora",
+ "c": "objects",
+ "e": "🏺",
+ "d": "amphora",
+ "u": "8.0"
+ },
+ {
+ "n": "earth_africa",
+ "c": "nature",
+ "e": "🌍",
+ "d": "earth globe europe-africa",
+ "u": "6.0"
+ },
+ {
+ "n": "earth_americas",
+ "c": "nature",
+ "e": "🌎",
+ "d": "earth globe americas",
+ "u": "6.0"
+ },
+ {
+ "n": "earth_asia",
+ "c": "nature",
+ "e": "🌏",
+ "d": "earth globe asia-australia",
+ "u": "6.0"
+ },
+ {
+ "n": "globe_with_meridians",
+ "c": "symbols",
+ "e": "🌐",
+ "d": "globe with meridians",
+ "u": "6.0"
+ },
+ {
+ "n": "map",
+ "c": "objects",
+ "e": "🗺",
+ "d": "world map",
+ "u": "7.0"
+ },
+ {
+ "n": "japan",
+ "c": "travel",
+ "e": "🗾",
+ "d": "silhouette of japan",
+ "u": "6.0"
+ },
+ {
+ "n": "mountain_snow",
+ "c": "travel",
+ "e": "🏔",
+ "d": "snow capped mountain",
+ "u": "7.0"
+ },
+ {
+ "n": "mountain",
+ "c": "travel",
+ "e": "⛰",
+ "d": "mountain",
+ "u": "5.2"
+ },
+ {
+ "n": "volcano",
+ "c": "travel",
+ "e": "🌋",
+ "d": "volcano",
+ "u": "6.0"
+ },
+ {
+ "n": "mount_fuji",
+ "c": "travel",
+ "e": "🗻",
+ "d": "mount fuji",
+ "u": "6.0"
+ },
+ {
+ "n": "camping",
+ "c": "travel",
+ "e": "🏕",
+ "d": "camping",
+ "u": "7.0"
+ },
+ {
+ "n": "beach",
+ "c": "travel",
+ "e": "🏖",
+ "d": "beach with umbrella",
+ "u": "7.0"
+ },
+ {
+ "n": "desert",
+ "c": "travel",
+ "e": "🏜",
+ "d": "desert",
+ "u": "7.0"
+ },
+ {
+ "n": "island",
+ "c": "travel",
+ "e": "🏝",
+ "d": "desert island",
+ "u": "7.0"
+ },
+ {
+ "n": "park",
+ "c": "travel",
+ "e": "🏞",
+ "d": "national park",
+ "u": "7.0"
+ },
+ {
+ "n": "stadium",
+ "c": "travel",
+ "e": "🏟",
+ "d": "stadium",
+ "u": "7.0"
+ },
+ {
+ "n": "classical_building",
+ "c": "travel",
+ "e": "🏛",
+ "d": "classical building",
+ "u": "7.0"
+ },
+ {
+ "n": "construction_site",
+ "c": "travel",
+ "e": "🏗",
+ "d": "building construction",
+ "u": "7.0"
+ },
+ {
+ "n": "homes",
+ "c": "travel",
+ "e": "🏘",
+ "d": "house buildings",
+ "u": "7.0"
+ },
+ {
+ "n": "house_abandoned",
+ "c": "travel",
+ "e": "🏚",
+ "d": "derelict house building",
+ "u": "7.0"
+ },
+ {
+ "n": "house",
+ "c": "travel",
+ "e": "🏠",
+ "d": "house building",
+ "u": "6.0"
+ },
+ {
+ "n": "house_with_garden",
+ "c": "travel",
+ "e": "🏡",
+ "d": "house with garden",
+ "u": "6.0"
+ },
+ {
+ "n": "office",
+ "c": "travel",
+ "e": "🏢",
+ "d": "office building",
+ "u": "6.0"
+ },
+ {
+ "n": "post_office",
+ "c": "travel",
+ "e": "🏣",
+ "d": "japanese post office",
+ "u": "6.0"
+ },
+ {
+ "n": "european_post_office",
+ "c": "travel",
+ "e": "🏤",
+ "d": "european post office",
+ "u": "6.0"
+ },
+ {
+ "n": "hospital",
+ "c": "travel",
+ "e": "🏥",
+ "d": "hospital",
+ "u": "6.0"
+ },
+ {
+ "n": "bank",
+ "c": "travel",
+ "e": "🏦",
+ "d": "bank",
+ "u": "6.0"
+ },
+ {
+ "n": "hotel",
+ "c": "travel",
+ "e": "🏨",
+ "d": "hotel",
+ "u": "6.0"
+ },
+ {
+ "n": "love_hotel",
+ "c": "travel",
+ "e": "🏩",
+ "d": "love hotel",
+ "u": "6.0"
+ },
+ {
+ "n": "convenience_store",
+ "c": "travel",
+ "e": "🏪",
+ "d": "convenience store",
+ "u": "6.0"
+ },
+ {
+ "n": "school",
+ "c": "travel",
+ "e": "🏫",
+ "d": "school",
+ "u": "6.0"
+ },
+ {
+ "n": "department_store",
+ "c": "travel",
+ "e": "🏬",
+ "d": "department store",
+ "u": "6.0"
+ },
+ {
+ "n": "factory",
+ "c": "travel",
+ "e": "🏭",
+ "d": "factory",
+ "u": "6.0"
+ },
+ {
+ "n": "japanese_castle",
+ "c": "travel",
+ "e": "🏯",
+ "d": "japanese castle",
+ "u": "6.0"
+ },
+ {
+ "n": "european_castle",
+ "c": "travel",
+ "e": "🏰",
+ "d": "european castle",
+ "u": "6.0"
+ },
+ {
+ "n": "wedding",
+ "c": "travel",
+ "e": "💒",
+ "d": "wedding",
+ "u": "6.0"
+ },
+ {
+ "n": "tokyo_tower",
+ "c": "travel",
+ "e": "🗼",
+ "d": "tokyo tower",
+ "u": "6.0"
+ },
+ {
+ "n": "statue_of_liberty",
+ "c": "travel",
+ "e": "🗽",
+ "d": "statue of liberty",
+ "u": "6.0"
+ },
+ {
+ "n": "church",
+ "c": "travel",
+ "e": "⛪",
+ "d": "church",
+ "u": "5.2"
+ },
+ {
+ "n": "mosque",
+ "c": "travel",
+ "e": "🕌",
+ "d": "mosque",
+ "u": "8.0"
+ },
+ {
+ "n": "synagogue",
+ "c": "travel",
+ "e": "🕍",
+ "d": "synagogue",
+ "u": "8.0"
+ },
+ {
+ "n": "shinto_shrine",
+ "c": "travel",
+ "e": "⛩",
+ "d": "shinto shrine",
+ "u": "5.2"
+ },
+ {
+ "n": "kaaba",
+ "c": "travel",
+ "e": "🕋",
+ "d": "kaaba",
+ "u": "8.0"
+ },
+ {
+ "n": "fountain",
+ "c": "travel",
+ "e": "⛲",
+ "d": "fountain",
+ "u": "5.2"
+ },
+ {
+ "n": "tent",
+ "c": "travel",
+ "e": "⛺",
+ "d": "tent",
+ "u": "5.2"
+ },
+ {
+ "n": "foggy",
+ "c": "travel",
+ "e": "🌁",
+ "d": "foggy",
+ "u": "6.0"
+ },
+ {
+ "n": "night_with_stars",
+ "c": "travel",
+ "e": "🌃",
+ "d": "night with stars",
+ "u": "6.0"
+ },
+ {
+ "n": "cityscape",
+ "c": "travel",
+ "e": "🏙",
+ "d": "cityscape",
+ "u": "7.0"
+ },
+ {
+ "n": "sunrise_over_mountains",
+ "c": "travel",
+ "e": "🌄",
+ "d": "sunrise over mountains",
+ "u": "6.0"
+ },
+ {
+ "n": "sunrise",
+ "c": "travel",
+ "e": "🌅",
+ "d": "sunrise",
+ "u": "6.0"
+ },
+ {
+ "n": "city_dusk",
+ "c": "travel",
+ "e": "🌆",
+ "d": "cityscape at dusk",
+ "u": "6.0"
+ },
+ {
+ "n": "city_sunset",
+ "c": "travel",
+ "e": "🌇",
+ "d": "sunset over buildings",
+ "u": "6.0"
+ },
+ {
+ "n": "bridge_at_night",
+ "c": "travel",
+ "e": "🌉",
+ "d": "bridge at night",
+ "u": "6.0"
+ },
+ {
+ "n": "hotsprings",
+ "c": "symbols",
+ "e": "♨",
+ "d": "hot springs",
+ "u": "1.1"
+ },
+ {
+ "n": "carousel_horse",
+ "c": "travel",
+ "e": "🎠",
+ "d": "carousel horse",
+ "u": "6.0"
+ },
+ {
+ "n": "ferris_wheel",
+ "c": "travel",
+ "e": "🎡",
+ "d": "ferris wheel",
+ "u": "6.0"
+ },
+ {
+ "n": "roller_coaster",
+ "c": "travel",
+ "e": "🎢",
+ "d": "roller coaster",
+ "u": "6.0"
+ },
+ {
+ "n": "barber",
+ "c": "objects",
+ "e": "💈",
+ "d": "barber pole",
+ "u": "6.0"
+ },
+ {
+ "n": "circus_tent",
+ "c": "activity",
+ "e": "🎪",
+ "d": "circus tent",
+ "u": "6.0"
+ },
+ {
+ "n": "steam_locomotive",
+ "c": "travel",
+ "e": "🚂",
+ "d": "steam locomotive",
+ "u": "6.0"
+ },
+ {
+ "n": "railway_car",
+ "c": "travel",
+ "e": "🚃",
+ "d": "railway car",
+ "u": "6.0"
+ },
+ {
+ "n": "bullettrain_side",
+ "c": "travel",
+ "e": "🚄",
+ "d": "high-speed train",
+ "u": "6.0"
+ },
+ {
+ "n": "bullettrain_front",
+ "c": "travel",
+ "e": "🚅",
+ "d": "high-speed train with bullet nose",
+ "u": "6.0"
+ },
+ {
+ "n": "train2",
+ "c": "travel",
+ "e": "🚆",
+ "d": "train",
+ "u": "6.0"
+ },
+ {
+ "n": "metro",
+ "c": "travel",
+ "e": "🚇",
+ "d": "metro",
+ "u": "6.0"
+ },
+ {
+ "n": "light_rail",
+ "c": "travel",
+ "e": "🚈",
+ "d": "light rail",
+ "u": "6.0"
+ },
+ {
+ "n": "station",
+ "c": "travel",
+ "e": "🚉",
+ "d": "station",
+ "u": "6.0"
+ },
+ {
+ "n": "tram",
+ "c": "travel",
+ "e": "🚊",
+ "d": "tram",
+ "u": "6.0"
+ },
+ {
+ "n": "monorail",
+ "c": "travel",
+ "e": "🚝",
+ "d": "monorail",
+ "u": "6.0"
+ },
+ {
+ "n": "mountain_railway",
+ "c": "travel",
+ "e": "🚞",
+ "d": "mountain railway",
+ "u": "6.0"
+ },
+ {
+ "n": "train",
+ "c": "travel",
+ "e": "🚋",
+ "d": "Tram Car",
+ "u": "6.0"
+ },
+ {
+ "n": "bus",
+ "c": "travel",
+ "e": "🚌",
+ "d": "bus",
+ "u": "6.0"
+ },
+ {
+ "n": "oncoming_bus",
+ "c": "travel",
+ "e": "🚍",
+ "d": "oncoming bus",
+ "u": "6.0"
+ },
+ {
+ "n": "trolleybus",
+ "c": "travel",
+ "e": "🚎",
+ "d": "trolleybus",
+ "u": "6.0"
+ },
+ {
+ "n": "minibus",
+ "c": "travel",
+ "e": "🚐",
+ "d": "minibus",
+ "u": "6.0"
+ },
+ {
+ "n": "ambulance",
+ "c": "travel",
+ "e": "🚑",
+ "d": "ambulance",
+ "u": "6.0"
+ },
+ {
+ "n": "fire_engine",
+ "c": "travel",
+ "e": "🚒",
+ "d": "fire engine",
+ "u": "6.0"
+ },
+ {
+ "n": "police_car",
+ "c": "travel",
+ "e": "🚓",
+ "d": "police car",
+ "u": "6.0"
+ },
+ {
+ "n": "oncoming_police_car",
+ "c": "travel",
+ "e": "🚔",
+ "d": "oncoming police car",
+ "u": "6.0"
+ },
+ {
+ "n": "taxi",
+ "c": "travel",
+ "e": "🚕",
+ "d": "taxi",
+ "u": "6.0"
+ },
+ {
+ "n": "oncoming_taxi",
+ "c": "travel",
+ "e": "🚖",
+ "d": "oncoming taxi",
+ "u": "6.0"
+ },
+ {
+ "n": "red_car",
+ "c": "travel",
+ "e": "🚗",
+ "d": "automobile",
+ "u": "6.0"
+ },
+ {
+ "n": "oncoming_automobile",
+ "c": "travel",
+ "e": "🚘",
+ "d": "oncoming automobile",
+ "u": "6.0"
+ },
+ {
+ "n": "blue_car",
+ "c": "travel",
+ "e": "🚙",
+ "d": "recreational vehicle",
+ "u": "6.0"
+ },
+ {
+ "n": "truck",
+ "c": "travel",
+ "e": "🚚",
+ "d": "delivery truck",
+ "u": "6.0"
+ },
+ {
+ "n": "articulated_lorry",
+ "c": "travel",
+ "e": "🚛",
+ "d": "articulated lorry",
+ "u": "6.0"
+ },
+ {
+ "n": "tractor",
+ "c": "travel",
+ "e": "🚜",
+ "d": "tractor",
+ "u": "6.0"
+ },
+ {
+ "n": "race_car",
+ "c": "travel",
+ "e": "🏎",
+ "d": "racing car",
+ "u": "7.0"
+ },
+ {
+ "n": "motorcycle",
+ "c": "travel",
+ "e": "🏍",
+ "d": "racing motorcycle",
+ "u": "7.0"
+ },
+ {
+ "n": "motor_scooter",
+ "c": "travel",
+ "e": "🛵",
+ "d": "motor scooter",
+ "u": "9.0"
+ },
+ {
+ "n": "bike",
+ "c": "travel",
+ "e": "🚲",
+ "d": "bicycle",
+ "u": "6.0"
+ },
+ {
+ "n": "scooter",
+ "c": "travel",
+ "e": "🛴",
+ "d": "scooter",
+ "u": "9.0"
+ },
+ {
+ "n": "busstop",
+ "c": "travel",
+ "e": "🚏",
+ "d": "bus stop",
+ "u": "6.0"
+ },
+ {
+ "n": "motorway",
+ "c": "travel",
+ "e": "🛣",
+ "d": "motorway",
+ "u": "7.0"
+ },
+ {
+ "n": "railway_track",
+ "c": "travel",
+ "e": "🛤",
+ "d": "railway track",
+ "u": "7.0"
+ },
+ {
+ "n": "oil",
+ "c": "objects",
+ "e": "🛢",
+ "d": "oil drum",
+ "u": "7.0"
+ },
+ {
+ "n": "fuelpump",
+ "c": "travel",
+ "e": "⛽",
+ "d": "fuel pump",
+ "u": "5.2"
+ },
+ {
+ "n": "rotating_light",
+ "c": "travel",
+ "e": "🚨",
+ "d": "police cars revolving light",
+ "u": "6.0"
+ },
+ {
+ "n": "traffic_light",
+ "c": "travel",
+ "e": "🚥",
+ "d": "horizontal traffic light",
+ "u": "6.0"
+ },
+ {
+ "n": "vertical_traffic_light",
+ "c": "travel",
+ "e": "🚦",
+ "d": "vertical traffic light",
+ "u": "6.0"
+ },
+ {
+ "n": "octagonal_sign",
+ "c": "symbols",
+ "e": "🛑",
+ "d": "octagonal sign",
+ "u": "9.0"
+ },
+ {
+ "n": "construction",
+ "c": "travel",
+ "e": "🚧",
+ "d": "construction sign",
+ "u": "6.0"
+ },
+ {
+ "n": "anchor",
+ "c": "travel",
+ "e": "⚓",
+ "d": "anchor",
+ "u": "4.1"
+ },
+ {
+ "n": "sailboat",
+ "c": "travel",
+ "e": "⛵",
+ "d": "sailboat",
+ "u": "5.2"
+ },
+ {
+ "n": "canoe",
+ "c": "travel",
+ "e": "🛶",
+ "d": "canoe",
+ "u": "9.0"
+ },
+ {
+ "n": "speedboat",
+ "c": "travel",
+ "e": "🚤",
+ "d": "speedboat",
+ "u": "6.0"
+ },
+ {
+ "n": "cruise_ship",
+ "c": "travel",
+ "e": "🛳",
+ "d": "passenger ship",
+ "u": "7.0"
+ },
+ {
+ "n": "ferry",
+ "c": "travel",
+ "e": "⛴",
+ "d": "ferry",
+ "u": "5.2"
+ },
+ {
+ "n": "motorboat",
+ "c": "travel",
+ "e": "🛥",
+ "d": "motorboat",
+ "u": "7.0"
+ },
+ {
+ "n": "ship",
+ "c": "travel",
+ "e": "🚢",
+ "d": "ship",
+ "u": "6.0"
+ },
+ {
+ "n": "airplane",
+ "c": "travel",
+ "e": "✈",
+ "d": "airplane",
+ "u": "1.1"
+ },
+ {
+ "n": "airplane_small",
+ "c": "travel",
+ "e": "🛩",
+ "d": "small airplane",
+ "u": "7.0"
+ },
+ {
+ "n": "airplane_departure",
+ "c": "travel",
+ "e": "🛫",
+ "d": "airplane departure",
+ "u": "7.0"
+ },
+ {
+ "n": "airplane_arriving",
+ "c": "travel",
+ "e": "🛬",
+ "d": "airplane arriving",
+ "u": "7.0"
+ },
+ {
+ "n": "seat",
+ "c": "travel",
+ "e": "💺",
+ "d": "seat",
+ "u": "6.0"
+ },
+ {
+ "n": "helicopter",
+ "c": "travel",
+ "e": "🚁",
+ "d": "helicopter",
+ "u": "6.0"
+ },
+ {
+ "n": "suspension_railway",
+ "c": "travel",
+ "e": "🚟",
+ "d": "suspension railway",
+ "u": "6.0"
+ },
+ {
+ "n": "mountain_cableway",
+ "c": "travel",
+ "e": "🚠",
+ "d": "mountain cableway",
+ "u": "6.0"
+ },
+ {
+ "n": "aerial_tramway",
+ "c": "travel",
+ "e": "🚡",
+ "d": "aerial tramway",
+ "u": "6.0"
+ },
+ {
+ "n": "satellite_orbital",
+ "c": "travel",
+ "e": "🛰",
+ "d": "satellite",
+ "u": "7.0"
+ },
+ {
+ "n": "rocket",
+ "c": "travel",
+ "e": "🚀",
+ "d": "rocket",
+ "u": "6.0"
+ },
+ {
+ "n": "bellhop",
+ "c": "objects",
+ "e": "🛎",
+ "d": "bellhop bell",
+ "u": "7.0"
+ },
+ {
+ "n": "hourglass",
+ "c": "objects",
+ "e": "⌛",
+ "d": "hourglass",
+ "u": "1.1"
+ },
+ {
+ "n": "hourglass_flowing_sand",
+ "c": "objects",
+ "e": "⏳",
+ "d": "hourglass with flowing sand",
+ "u": "6.0"
+ },
+ {
+ "n": "watch",
+ "c": "objects",
+ "e": "⌚",
+ "d": "watch",
+ "u": "1.1"
+ },
+ {
+ "n": "alarm_clock",
+ "c": "objects",
+ "e": "⏰",
+ "d": "alarm clock",
+ "u": "6.0"
+ },
+ {
+ "n": "stopwatch",
+ "c": "objects",
+ "e": "⏱",
+ "d": "stopwatch",
+ "u": "6.0"
+ },
+ {
+ "n": "timer",
+ "c": "objects",
+ "e": "⏲",
+ "d": "timer clock",
+ "u": "6.0"
+ },
+ {
+ "n": "clock",
+ "c": "objects",
+ "e": "🕰",
+ "d": "mantlepiece clock",
+ "u": "7.0"
+ },
+ {
+ "n": "clock12",
+ "c": "symbols",
+ "e": "🕛",
+ "d": "clock face twelve oclock",
+ "u": "6.0"
+ },
+ {
+ "n": "clock1230",
+ "c": "symbols",
+ "e": "🕧",
+ "d": "clock face twelve-thirty",
+ "u": "6.0"
+ },
+ {
+ "n": "clock1",
+ "c": "symbols",
+ "e": "🕐",
+ "d": "clock face one oclock",
+ "u": "6.0"
+ },
+ {
+ "n": "clock130",
+ "c": "symbols",
+ "e": "🕜",
+ "d": "clock face one-thirty",
+ "u": "6.0"
+ },
+ {
+ "n": "clock2",
+ "c": "symbols",
+ "e": "🕑",
+ "d": "clock face two oclock",
+ "u": "6.0"
+ },
+ {
+ "n": "clock230",
+ "c": "symbols",
+ "e": "🕝",
+ "d": "clock face two-thirty",
+ "u": "6.0"
+ },
+ {
+ "n": "clock3",
+ "c": "symbols",
+ "e": "🕒",
+ "d": "clock face three oclock",
+ "u": "6.0"
+ },
+ {
+ "n": "clock330",
+ "c": "symbols",
+ "e": "🕞",
+ "d": "clock face three-thirty",
+ "u": "6.0"
+ },
+ {
+ "n": "clock4",
+ "c": "symbols",
+ "e": "🕓",
+ "d": "clock face four oclock",
+ "u": "6.0"
+ },
+ {
+ "n": "clock430",
+ "c": "symbols",
+ "e": "🕟",
+ "d": "clock face four-thirty",
+ "u": "6.0"
+ },
+ {
+ "n": "clock5",
+ "c": "symbols",
+ "e": "🕔",
+ "d": "clock face five oclock",
+ "u": "6.0"
+ },
+ {
+ "n": "clock530",
+ "c": "symbols",
+ "e": "🕠",
+ "d": "clock face five-thirty",
+ "u": "6.0"
+ },
+ {
+ "n": "clock6",
+ "c": "symbols",
+ "e": "🕕",
+ "d": "clock face six oclock",
+ "u": "6.0"
+ },
+ {
+ "n": "clock630",
+ "c": "symbols",
+ "e": "🕡",
+ "d": "clock face six-thirty",
+ "u": "6.0"
+ },
+ {
+ "n": "clock7",
+ "c": "symbols",
+ "e": "🕖",
+ "d": "clock face seven oclock",
+ "u": "6.0"
+ },
+ {
+ "n": "clock730",
+ "c": "symbols",
+ "e": "🕢",
+ "d": "clock face seven-thirty",
+ "u": "6.0"
+ },
+ {
+ "n": "clock8",
+ "c": "symbols",
+ "e": "🕗",
+ "d": "clock face eight oclock",
+ "u": "6.0"
+ },
+ {
+ "n": "clock830",
+ "c": "symbols",
+ "e": "🕣",
+ "d": "clock face eight-thirty",
+ "u": "6.0"
+ },
+ {
+ "n": "clock9",
+ "c": "symbols",
+ "e": "🕘",
+ "d": "clock face nine oclock",
+ "u": "6.0"
+ },
+ {
+ "n": "clock930",
+ "c": "symbols",
+ "e": "🕤",
+ "d": "clock face nine-thirty",
+ "u": "6.0"
+ },
+ {
+ "n": "clock10",
+ "c": "symbols",
+ "e": "🕙",
+ "d": "clock face ten oclock",
+ "u": "6.0"
+ },
+ {
+ "n": "clock1030",
+ "c": "symbols",
+ "e": "🕥",
+ "d": "clock face ten-thirty",
+ "u": "6.0"
+ },
+ {
+ "n": "clock11",
+ "c": "symbols",
+ "e": "🕚",
+ "d": "clock face eleven oclock",
+ "u": "6.0"
+ },
+ {
+ "n": "clock1130",
+ "c": "symbols",
+ "e": "🕦",
+ "d": "clock face eleven-thirty",
+ "u": "6.0"
+ },
+ {
+ "n": "new_moon",
+ "c": "nature",
+ "e": "🌑",
+ "d": "new moon symbol",
+ "u": "6.0"
+ },
+ {
+ "n": "waxing_crescent_moon",
+ "c": "nature",
+ "e": "🌒",
+ "d": "waxing crescent moon symbol",
+ "u": "6.0"
+ },
+ {
+ "n": "first_quarter_moon",
+ "c": "nature",
+ "e": "🌓",
+ "d": "first quarter moon symbol",
+ "u": "6.0"
+ },
+ {
+ "n": "waxing_gibbous_moon",
+ "c": "nature",
+ "e": "🌔",
+ "d": "waxing gibbous moon symbol",
+ "u": "6.0"
+ },
+ {
+ "n": "full_moon",
+ "c": "nature",
+ "e": "🌕",
+ "d": "full moon symbol",
+ "u": "6.0"
+ },
+ {
+ "n": "waning_gibbous_moon",
+ "c": "nature",
+ "e": "🌖",
+ "d": "waning gibbous moon symbol",
+ "u": "6.0"
+ },
+ {
+ "n": "last_quarter_moon",
+ "c": "nature",
+ "e": "🌗",
+ "d": "last quarter moon symbol",
+ "u": "6.0"
+ },
+ {
+ "n": "waning_crescent_moon",
+ "c": "nature",
+ "e": "🌘",
+ "d": "waning crescent moon symbol",
+ "u": "6.0"
+ },
+ {
+ "n": "crescent_moon",
+ "c": "nature",
+ "e": "🌙",
+ "d": "crescent moon",
+ "u": "6.0"
+ },
+ {
+ "n": "new_moon_with_face",
+ "c": "nature",
+ "e": "🌚",
+ "d": "new moon with face",
+ "u": "6.0"
+ },
+ {
+ "n": "first_quarter_moon_with_face",
+ "c": "nature",
+ "e": "🌛",
+ "d": "first quarter moon with face",
+ "u": "6.0"
+ },
+ {
+ "n": "last_quarter_moon_with_face",
+ "c": "nature",
+ "e": "🌜",
+ "d": "last quarter moon with face",
+ "u": "6.0"
+ },
+ {
+ "n": "thermometer",
+ "c": "objects",
+ "e": "🌡",
+ "d": "thermometer",
+ "u": "7.0"
+ },
+ {
+ "n": "sunny",
+ "c": "nature",
+ "e": "☀",
+ "d": "black sun with rays",
+ "u": "1.1"
+ },
+ {
+ "n": "full_moon_with_face",
+ "c": "nature",
+ "e": "🌝",
+ "d": "full moon with face",
+ "u": "6.0"
+ },
+ {
+ "n": "sun_with_face",
+ "c": "nature",
+ "e": "🌞",
+ "d": "sun with face",
+ "u": "6.0"
+ },
+ {
+ "n": "star",
+ "c": "nature",
+ "e": "⭐",
+ "d": "white medium star",
+ "u": "5.1"
+ },
+ {
+ "n": "star2",
+ "c": "nature",
+ "e": "🌟",
+ "d": "glowing star",
+ "u": "6.0"
+ },
+ {
+ "n": "stars",
+ "c": "travel",
+ "e": "🌠",
+ "d": "shooting star",
+ "u": "6.0"
+ },
+ {
+ "n": "milky_way",
+ "c": "travel",
+ "e": "🌌",
+ "d": "milky way",
+ "u": "6.0"
+ },
+ {
+ "n": "cloud",
+ "c": "nature",
+ "e": "☁",
+ "d": "cloud",
+ "u": "1.1"
+ },
+ {
+ "n": "partly_sunny",
+ "c": "nature",
+ "e": "⛅",
+ "d": "sun behind cloud",
+ "u": "5.2"
+ },
+ {
+ "n": "thunder_cloud_rain",
+ "c": "nature",
+ "e": "⛈",
+ "d": "thunder cloud and rain",
+ "u": "5.2"
+ },
+ {
+ "n": "white_sun_small_cloud",
+ "c": "nature",
+ "e": "🌤",
+ "d": "white sun with small cloud",
+ "u": "7.0"
+ },
+ {
+ "n": "white_sun_cloud",
+ "c": "nature",
+ "e": "🌥",
+ "d": "white sun behind cloud",
+ "u": "7.0"
+ },
+ {
+ "n": "white_sun_rain_cloud",
+ "c": "nature",
+ "e": "🌦",
+ "d": "white sun behind cloud with rain",
+ "u": "7.0"
+ },
+ {
+ "n": "cloud_rain",
+ "c": "nature",
+ "e": "🌧",
+ "d": "cloud with rain",
+ "u": "7.0"
+ },
+ {
+ "n": "cloud_snow",
+ "c": "nature",
+ "e": "🌨",
+ "d": "cloud with snow",
+ "u": "7.0"
+ },
+ {
+ "n": "cloud_lightning",
+ "c": "nature",
+ "e": "🌩",
+ "d": "cloud with lightning",
+ "u": "7.0"
+ },
+ {
+ "n": "cloud_tornado",
+ "c": "nature",
+ "e": "🌪",
+ "d": "cloud with tornado",
+ "u": "7.0"
+ },
+ {
+ "n": "fog",
+ "c": "nature",
+ "e": "🌫",
+ "d": "fog",
+ "u": "7.0"
+ },
+ {
+ "n": "wind_blowing_face",
+ "c": "nature",
+ "e": "🌬",
+ "d": "wind blowing face",
+ "u": "7.0"
+ },
+ {
+ "n": "cyclone",
+ "c": "symbols",
+ "e": "🌀",
+ "d": "cyclone",
+ "u": "6.0"
+ },
+ {
+ "n": "rainbow",
+ "c": "travel",
+ "e": "🌈",
+ "d": "rainbow",
+ "u": "6.0"
+ },
+ {
+ "n": "closed_umbrella",
+ "c": "people",
+ "e": "🌂",
+ "d": "closed umbrella",
+ "u": "6.0"
+ },
+ {
+ "n": "umbrella2",
+ "c": "nature",
+ "e": "☂",
+ "d": "umbrella",
+ "u": "1.1"
+ },
+ {
+ "n": "umbrella",
+ "c": "nature",
+ "e": "☔",
+ "d": "umbrella with rain drops",
+ "u": "4.0"
+ },
+ {
+ "n": "beach_umbrella",
+ "c": "objects",
+ "e": "⛱",
+ "d": "umbrella on ground",
+ "u": "5.2"
+ },
+ {
+ "n": "zap",
+ "c": "nature",
+ "e": "⚡",
+ "d": "high voltage sign",
+ "u": "4.0"
+ },
+ {
+ "n": "snowflake",
+ "c": "nature",
+ "e": "❄",
+ "d": "snowflake",
+ "u": "1.1"
+ },
+ {
+ "n": "snowman2",
+ "c": "nature",
+ "e": "☃",
+ "d": "snowman",
+ "u": "1.1"
+ },
+ {
+ "n": "snowman",
+ "c": "nature",
+ "e": "⛄",
+ "d": "snowman without snow",
+ "u": "5.2"
+ },
+ {
+ "n": "comet",
+ "c": "nature",
+ "e": "☄",
+ "d": "comet",
+ "u": "1.1"
+ },
+ {
+ "n": "fire",
+ "c": "nature",
+ "e": "🔥",
+ "d": "fire",
+ "u": "6.0"
+ },
+ {
+ "n": "droplet",
+ "c": "nature",
+ "e": "💧",
+ "d": "droplet",
+ "u": "6.0"
+ },
+ {
+ "n": "ocean",
+ "c": "nature",
+ "e": "🌊",
+ "d": "water wave",
+ "u": "6.0"
+ },
+ {
+ "n": "jack_o_lantern",
+ "c": "nature",
+ "e": "🎃",
+ "d": "jack-o-lantern",
+ "u": "6.0"
+ },
+ {
+ "n": "christmas_tree",
+ "c": "nature",
+ "e": "🎄",
+ "d": "christmas tree",
+ "u": "6.0"
+ },
+ {
+ "n": "fireworks",
+ "c": "travel",
+ "e": "🎆",
+ "d": "fireworks",
+ "u": "6.0"
+ },
+ {
+ "n": "sparkler",
+ "c": "travel",
+ "e": "🎇",
+ "d": "firework sparkler",
+ "u": "6.0"
+ },
+ {
+ "n": "sparkles",
+ "c": "nature",
+ "e": "✨",
+ "d": "sparkles",
+ "u": "6.0"
+ },
+ {
+ "n": "balloon",
+ "c": "objects",
+ "e": "🎈",
+ "d": "balloon",
+ "u": "6.0"
+ },
+ {
+ "n": "tada",
+ "c": "objects",
+ "e": "🎉",
+ "d": "party popper",
+ "u": "6.0"
+ },
+ {
+ "n": "confetti_ball",
+ "c": "objects",
+ "e": "🎊",
+ "d": "confetti ball",
+ "u": "6.0"
+ },
+ {
+ "n": "tanabata_tree",
+ "c": "nature",
+ "e": "🎋",
+ "d": "tanabata tree",
+ "u": "6.0"
+ },
+ {
+ "n": "bamboo",
+ "c": "nature",
+ "e": "🎍",
+ "d": "pine decoration",
+ "u": "6.0"
+ },
+ {
+ "n": "dolls",
+ "c": "objects",
+ "e": "🎎",
+ "d": "japanese dolls",
+ "u": "6.0"
+ },
+ {
+ "n": "flags",
+ "c": "objects",
+ "e": "🎏",
+ "d": "carp streamer",
+ "u": "6.0"
+ },
+ {
+ "n": "wind_chime",
+ "c": "objects",
+ "e": "🎐",
+ "d": "wind chime",
+ "u": "6.0"
+ },
+ {
+ "n": "rice_scene",
+ "c": "travel",
+ "e": "🎑",
+ "d": "moon viewing ceremony",
+ "u": "6.0"
+ },
+ {
+ "n": "ribbon",
+ "c": "objects",
+ "e": "🎀",
+ "d": "ribbon",
+ "u": "6.0"
+ },
+ {
+ "n": "gift",
+ "c": "objects",
+ "e": "🎁",
+ "d": "wrapped present",
+ "u": "6.0"
+ },
+ {
+ "n": "reminder_ribbon",
+ "c": "activity",
+ "e": "🎗",
+ "d": "reminder ribbon",
+ "u": "7.0"
+ },
+ {
+ "n": "tickets",
+ "c": "activity",
+ "e": "🎟",
+ "d": "admission tickets",
+ "u": "7.0"
+ },
+ {
+ "n": "ticket",
+ "c": "activity",
+ "e": "🎫",
+ "d": "ticket",
+ "u": "6.0"
+ },
+ {
+ "n": "military_medal",
+ "c": "activity",
+ "e": "🎖",
+ "d": "military medal",
+ "u": "7.0"
+ },
+ {
+ "n": "trophy",
+ "c": "activity",
+ "e": "🏆",
+ "d": "trophy",
+ "u": "6.0"
+ },
+ {
+ "n": "medal",
+ "c": "activity",
+ "e": "🏅",
+ "d": "sports medal",
+ "u": "7.0"
+ },
+ {
+ "n": "first_place",
+ "c": "activity",
+ "e": "🥇",
+ "d": "first place medal",
+ "u": "9.0"
+ },
+ {
+ "n": "second_place",
+ "c": "activity",
+ "e": "🥈",
+ "d": "second place medal",
+ "u": "9.0"
+ },
+ {
+ "n": "third_place",
+ "c": "activity",
+ "e": "🥉",
+ "d": "third place medal",
+ "u": "9.0"
+ },
+ {
+ "n": "soccer",
+ "c": "activity",
+ "e": "⚽",
+ "d": "soccer ball",
+ "u": "5.2"
+ },
+ {
+ "n": "baseball",
+ "c": "activity",
+ "e": "⚾",
+ "d": "baseball",
+ "u": "5.2"
+ },
+ {
+ "n": "basketball",
+ "c": "activity",
+ "e": "🏀",
+ "d": "basketball and hoop",
+ "u": "6.0"
+ },
+ {
+ "n": "volleyball",
+ "c": "activity",
+ "e": "🏐",
+ "d": "volleyball",
+ "u": "8.0"
+ },
+ {
+ "n": "football",
+ "c": "activity",
+ "e": "🏈",
+ "d": "american football",
+ "u": "6.0"
+ },
+ {
+ "n": "rugby_football",
+ "c": "activity",
+ "e": "🏉",
+ "d": "rugby football",
+ "u": "6.0"
+ },
+ {
+ "n": "tennis",
+ "c": "activity",
+ "e": "🎾",
+ "d": "tennis racquet and ball",
+ "u": "6.0"
+ },
+ {
+ "n": "bowling",
+ "c": "activity",
+ "e": "🎳",
+ "d": "bowling",
+ "u": "6.0"
+ },
+ {
+ "n": "cricket",
+ "c": "activity",
+ "e": "🏏",
+ "d": "cricket bat and ball",
+ "u": "10.0"
+ },
+ {
+ "n": "field_hockey",
+ "c": "activity",
+ "e": "🏑",
+ "d": "field hockey stick and ball",
+ "u": "8.0"
+ },
+ {
+ "n": "hockey",
+ "c": "activity",
+ "e": "🏒",
+ "d": "ice hockey stick and puck",
+ "u": "8.0"
+ },
+ {
+ "n": "ping_pong",
+ "c": "activity",
+ "e": "🏓",
+ "d": "table tennis paddle and ball",
+ "u": "8.0"
+ },
+ {
+ "n": "badminton",
+ "c": "activity",
+ "e": "🏸",
+ "d": "badminton racquet",
+ "u": "8.0"
+ },
+ {
+ "n": "boxing_glove",
+ "c": "activity",
+ "e": "🥊",
+ "d": "boxing glove",
+ "u": "9.0"
+ },
+ {
+ "n": "martial_arts_uniform",
+ "c": "activity",
+ "e": "🥋",
+ "d": "martial arts uniform",
+ "u": "9.0"
+ },
+ {
+ "n": "goal",
+ "c": "activity",
+ "e": "🥅",
+ "d": "goal net",
+ "u": "9.0"
+ },
+ {
+ "n": "golf",
+ "c": "activity",
+ "e": "⛳",
+ "d": "flag in hole",
+ "u": "5.2"
+ },
+ {
+ "n": "ice_skate",
+ "c": "activity",
+ "e": "⛸",
+ "d": "ice skate",
+ "u": "5.2"
+ },
+ {
+ "n": "fishing_pole_and_fish",
+ "c": "activity",
+ "e": "🎣",
+ "d": "fishing pole and fish",
+ "u": "6.0"
+ },
+ {
+ "n": "running_shirt_with_sash",
+ "c": "activity",
+ "e": "🎽",
+ "d": "running shirt with sash",
+ "u": "6.0"
+ },
+ {
+ "n": "ski",
+ "c": "activity",
+ "e": "🎿",
+ "d": "ski and ski boot",
+ "u": "6.0"
+ },
+ {
+ "n": "dart",
+ "c": "activity",
+ "e": "🎯",
+ "d": "direct hit",
+ "u": "6.0"
+ },
+ {
+ "n": "gun",
+ "c": "objects",
+ "e": "🔫",
+ "d": "pistol",
+ "u": "6.0"
+ },
+ {
+ "n": "8ball",
+ "c": "activity",
+ "e": "🎱",
+ "d": "billiards",
+ "u": "6.0"
+ },
+ {
+ "n": "crystal_ball",
+ "c": "objects",
+ "e": "🔮",
+ "d": "crystal ball",
+ "u": "6.0"
+ },
+ {
+ "n": "video_game",
+ "c": "activity",
+ "e": "🎮",
+ "d": "video game",
+ "u": "6.0"
+ },
+ {
+ "n": "joystick",
+ "c": "objects",
+ "e": "🕹",
+ "d": "joystick",
+ "u": "7.0"
+ },
+ {
+ "n": "slot_machine",
+ "c": "activity",
+ "e": "🎰",
+ "d": "slot machine",
+ "u": "6.0"
+ },
+ {
+ "n": "game_die",
+ "c": "activity",
+ "e": "🎲",
+ "d": "game die",
+ "u": "6.0"
+ },
+ {
+ "n": "spades",
+ "c": "symbols",
+ "e": "♠",
+ "d": "black spade suit",
+ "u": "1.1"
+ },
+ {
+ "n": "hearts",
+ "c": "symbols",
+ "e": "♥",
+ "d": "black heart suit",
+ "u": "1.1"
+ },
+ {
+ "n": "diamonds",
+ "c": "symbols",
+ "e": "♦",
+ "d": "black diamond suit",
+ "u": "1.1"
+ },
+ {
+ "n": "clubs",
+ "c": "symbols",
+ "e": "♣",
+ "d": "black club suit",
+ "u": "1.1"
+ },
+ {
+ "n": "black_joker",
+ "c": "symbols",
+ "e": "🃏",
+ "d": "playing card black joker",
+ "u": "6.0"
+ },
+ {
+ "n": "mahjong",
+ "c": "symbols",
+ "e": "🀄",
+ "d": "mahjong tile red dragon",
+ "u": "5.1"
+ },
+ {
+ "n": "flower_playing_cards",
+ "c": "symbols",
+ "e": "🎴",
+ "d": "flower playing cards",
+ "u": "6.0"
+ },
+ {
+ "n": "performing_arts",
+ "c": "activity",
+ "e": "🎭",
+ "d": "performing arts",
+ "u": "6.0"
+ },
+ {
+ "n": "frame_photo",
+ "c": "objects",
+ "e": "🖼",
+ "d": "frame with picture",
+ "u": "7.0"
+ },
+ {
+ "n": "art",
+ "c": "activity",
+ "e": "🎨",
+ "d": "artist palette",
+ "u": "6.0"
+ },
+ {
+ "n": "eyeglasses",
+ "c": "people",
+ "e": "👓",
+ "d": "eyeglasses",
+ "u": "6.0"
+ },
+ {
+ "n": "dark_sunglasses",
+ "c": "people",
+ "e": "🕶",
+ "d": "dark sunglasses",
+ "u": "7.0"
+ },
+ {
+ "n": "necktie",
+ "c": "people",
+ "e": "👔",
+ "d": "necktie",
+ "u": "6.0"
+ },
+ {
+ "n": "shirt",
+ "c": "people",
+ "e": "👕",
+ "d": "t-shirt",
+ "u": "6.0"
+ },
+ {
+ "n": "jeans",
+ "c": "people",
+ "e": "👖",
+ "d": "jeans",
+ "u": "6.0"
+ },
+ {
+ "n": "dress",
+ "c": "people",
+ "e": "👗",
+ "d": "dress",
+ "u": "6.0"
+ },
+ {
+ "n": "kimono",
+ "c": "people",
+ "e": "👘",
+ "d": "kimono",
+ "u": "6.0"
+ },
+ {
+ "n": "bikini",
+ "c": "people",
+ "e": "👙",
+ "d": "bikini",
+ "u": "6.0"
+ },
+ {
+ "n": "womans_clothes",
+ "c": "people",
+ "e": "👚",
+ "d": "womans clothes",
+ "u": "6.0"
+ },
+ {
+ "n": "purse",
+ "c": "people",
+ "e": "👛",
+ "d": "purse",
+ "u": "6.0"
+ },
+ {
+ "n": "handbag",
+ "c": "people",
+ "e": "👜",
+ "d": "handbag",
+ "u": "6.0"
+ },
+ {
+ "n": "pouch",
+ "c": "people",
+ "e": "👝",
+ "d": "pouch",
+ "u": "6.0"
+ },
+ {
+ "n": "shopping_bags",
+ "c": "objects",
+ "e": "🛍",
+ "d": "shopping bags",
+ "u": "7.0"
+ },
+ {
+ "n": "school_satchel",
+ "c": "people",
+ "e": "🎒",
+ "d": "school satchel",
+ "u": "6.0"
+ },
+ {
+ "n": "mans_shoe",
+ "c": "people",
+ "e": "👞",
+ "d": "mans shoe",
+ "u": "6.0"
+ },
+ {
+ "n": "athletic_shoe",
+ "c": "people",
+ "e": "👟",
+ "d": "athletic shoe",
+ "u": "6.0"
+ },
+ {
+ "n": "high_heel",
+ "c": "people",
+ "e": "👠",
+ "d": "high-heeled shoe",
+ "u": "6.0"
+ },
+ {
+ "n": "sandal",
+ "c": "people",
+ "e": "👡",
+ "d": "womans sandal",
+ "u": "6.0"
+ },
+ {
+ "n": "boot",
+ "c": "people",
+ "e": "👢",
+ "d": "womans boots",
+ "u": "6.0"
+ },
+ {
+ "n": "crown",
+ "c": "people",
+ "e": "👑",
+ "d": "crown",
+ "u": "6.0"
+ },
+ {
+ "n": "womans_hat",
+ "c": "people",
+ "e": "👒",
+ "d": "womans hat",
+ "u": "6.0"
+ },
+ {
+ "n": "tophat",
+ "c": "people",
+ "e": "🎩",
+ "d": "top hat",
+ "u": "6.0"
+ },
+ {
+ "n": "mortar_board",
+ "c": "people",
+ "e": "🎓",
+ "d": "graduation cap",
+ "u": "6.0"
+ },
+ {
+ "n": "helmet_with_cross",
+ "c": "people",
+ "e": "⛑",
+ "d": "helmet with white cross",
+ "u": "5.2"
+ },
+ {
+ "n": "prayer_beads",
+ "c": "objects",
+ "e": "📿",
+ "d": "prayer beads",
+ "u": "8.0"
+ },
+ {
+ "n": "lipstick",
+ "c": "people",
+ "e": "💄",
+ "d": "lipstick",
+ "u": "6.0"
+ },
+ {
+ "n": "ring",
+ "c": "people",
+ "e": "💍",
+ "d": "ring",
+ "u": "6.0"
+ },
+ {
+ "n": "gem",
+ "c": "objects",
+ "e": "💎",
+ "d": "gem stone",
+ "u": "6.0"
+ },
+ {
+ "n": "mute",
+ "c": "symbols",
+ "e": "🔇",
+ "d": "speaker with cancellation stroke",
+ "u": "6.0"
+ },
+ {
+ "n": "speaker",
+ "c": "symbols",
+ "e": "🔈",
+ "d": "speaker",
+ "u": "6.0"
+ },
+ {
+ "n": "sound",
+ "c": "symbols",
+ "e": "🔉",
+ "d": "speaker with one sound wave",
+ "u": "6.0"
+ },
+ {
+ "n": "loud_sound",
+ "c": "symbols",
+ "e": "🔊",
+ "d": "speaker with three sound waves",
+ "u": "6.0"
+ },
+ {
+ "n": "loudspeaker",
+ "c": "symbols",
+ "e": "📢",
+ "d": "public address loudspeaker",
+ "u": "6.0"
+ },
+ {
+ "n": "mega",
+ "c": "symbols",
+ "e": "📣",
+ "d": "cheering megaphone",
+ "u": "6.0"
+ },
+ {
+ "n": "postal_horn",
+ "c": "objects",
+ "e": "📯",
+ "d": "postal horn",
+ "u": "6.0"
+ },
+ {
+ "n": "bell",
+ "c": "symbols",
+ "e": "🔔",
+ "d": "bell",
+ "u": "6.0"
+ },
+ {
+ "n": "no_bell",
+ "c": "symbols",
+ "e": "🔕",
+ "d": "bell with cancellation stroke",
+ "u": "6.0"
+ },
+ {
+ "n": "musical_score",
+ "c": "activity",
+ "e": "🎼",
+ "d": "musical score",
+ "u": "6.0"
+ },
+ {
+ "n": "musical_note",
+ "c": "symbols",
+ "e": "🎵",
+ "d": "musical note",
+ "u": "6.0"
+ },
+ {
+ "n": "notes",
+ "c": "symbols",
+ "e": "🎶",
+ "d": "multiple musical notes",
+ "u": "6.0"
+ },
+ {
+ "n": "microphone2",
+ "c": "objects",
+ "e": "🎙",
+ "d": "studio microphone",
+ "u": "7.0"
+ },
+ {
+ "n": "level_slider",
+ "c": "objects",
+ "e": "🎚",
+ "d": "level slider",
+ "u": "7.0"
+ },
+ {
+ "n": "control_knobs",
+ "c": "objects",
+ "e": "🎛",
+ "d": "control knobs",
+ "u": "7.0"
+ },
+ {
+ "n": "microphone",
+ "c": "activity",
+ "e": "🎤",
+ "d": "microphone",
+ "u": "6.0"
+ },
+ {
+ "n": "headphones",
+ "c": "activity",
+ "e": "🎧",
+ "d": "headphone",
+ "u": "6.0"
+ },
+ {
+ "n": "radio",
+ "c": "objects",
+ "e": "📻",
+ "d": "radio",
+ "u": "6.0"
+ },
+ {
+ "n": "saxophone",
+ "c": "activity",
+ "e": "🎷",
+ "d": "saxophone",
+ "u": "6.0"
+ },
+ {
+ "n": "guitar",
+ "c": "activity",
+ "e": "🎸",
+ "d": "guitar",
+ "u": "6.0"
+ },
+ {
+ "n": "musical_keyboard",
+ "c": "activity",
+ "e": "🎹",
+ "d": "musical keyboard",
+ "u": "6.0"
+ },
+ {
+ "n": "trumpet",
+ "c": "activity",
+ "e": "🎺",
+ "d": "trumpet",
+ "u": "6.0"
+ },
+ {
+ "n": "violin",
+ "c": "activity",
+ "e": "🎻",
+ "d": "violin",
+ "u": "6.0"
+ },
+ {
+ "n": "drum",
+ "c": "activity",
+ "e": "🥁",
+ "d": "drum with drumsticks",
+ "u": "9.0"
+ },
+ {
+ "n": "iphone",
+ "c": "objects",
+ "e": "📱",
+ "d": "mobile phone",
+ "u": "6.0"
+ },
+ {
+ "n": "calling",
+ "c": "objects",
+ "e": "📲",
+ "d": "mobile phone with rightwards arrow at left",
+ "u": "6.0"
+ },
+ {
+ "n": "telephone",
+ "c": "objects",
+ "e": "☎",
+ "d": "black telephone",
+ "u": "1.1"
+ },
+ {
+ "n": "telephone_receiver",
+ "c": "objects",
+ "e": "📞",
+ "d": "telephone receiver",
+ "u": "6.0"
+ },
+ {
+ "n": "pager",
+ "c": "objects",
+ "e": "📟",
+ "d": "pager",
+ "u": "6.0"
+ },
+ {
+ "n": "fax",
+ "c": "objects",
+ "e": "📠",
+ "d": "fax machine",
+ "u": "6.0"
+ },
+ {
+ "n": "battery",
+ "c": "objects",
+ "e": "🔋",
+ "d": "battery",
+ "u": "6.0"
+ },
+ {
+ "n": "electric_plug",
+ "c": "objects",
+ "e": "🔌",
+ "d": "electric plug",
+ "u": "6.0"
+ },
+ {
+ "n": "computer",
+ "c": "objects",
+ "e": "💻",
+ "d": "personal computer",
+ "u": "6.0"
+ },
+ {
+ "n": "desktop",
+ "c": "objects",
+ "e": "🖥",
+ "d": "desktop computer",
+ "u": "7.0"
+ },
+ {
+ "n": "printer",
+ "c": "objects",
+ "e": "🖨",
+ "d": "printer",
+ "u": "7.0"
+ },
+ {
+ "n": "keyboard",
+ "c": "objects",
+ "e": "⌨",
+ "d": "keyboard",
+ "u": "1.1"
+ },
+ {
+ "n": "mouse_three_button",
+ "c": "objects",
+ "e": "🖱",
+ "d": "three button mouse",
+ "u": "7.0"
+ },
+ {
+ "n": "trackball",
+ "c": "objects",
+ "e": "🖲",
+ "d": "trackball",
+ "u": "7.0"
+ },
+ {
+ "n": "minidisc",
+ "c": "objects",
+ "e": "💽",
+ "d": "minidisc",
+ "u": "6.0"
+ },
+ {
+ "n": "floppy_disk",
+ "c": "objects",
+ "e": "💾",
+ "d": "floppy disk",
+ "u": "6.0"
+ },
+ {
+ "n": "cd",
+ "c": "objects",
+ "e": "💿",
+ "d": "optical disc",
+ "u": "6.0"
+ },
+ {
+ "n": "dvd",
+ "c": "objects",
+ "e": "📀",
+ "d": "dvd",
+ "u": "6.0"
+ },
+ {
+ "n": "movie_camera",
+ "c": "objects",
+ "e": "🎥",
+ "d": "movie camera",
+ "u": "6.0"
+ },
+ {
+ "n": "film_frames",
+ "c": "objects",
+ "e": "🎞",
+ "d": "film frames",
+ "u": "7.0"
+ },
+ {
+ "n": "projector",
+ "c": "objects",
+ "e": "📽",
+ "d": "film projector",
+ "u": "7.0"
+ },
+ {
+ "n": "clapper",
+ "c": "activity",
+ "e": "🎬",
+ "d": "clapper board",
+ "u": "6.0"
+ },
+ {
+ "n": "tv",
+ "c": "objects",
+ "e": "📺",
+ "d": "television",
+ "u": "6.0"
+ },
+ {
+ "n": "camera",
+ "c": "objects",
+ "e": "📷",
+ "d": "camera",
+ "u": "6.0"
+ },
+ {
+ "n": "camera_with_flash",
+ "c": "objects",
+ "e": "📸",
+ "d": "camera with flash",
+ "u": "7.0"
+ },
+ {
+ "n": "video_camera",
+ "c": "objects",
+ "e": "📹",
+ "d": "video camera",
+ "u": "6.0"
+ },
+ {
+ "n": "vhs",
+ "c": "objects",
+ "e": "📼",
+ "d": "videocassette",
+ "u": "6.0"
+ },
+ {
+ "n": "mag",
+ "c": "objects",
+ "e": "🔍",
+ "d": "left-pointing magnifying glass",
+ "u": "6.0"
+ },
+ {
+ "n": "mag_right",
+ "c": "objects",
+ "e": "🔎",
+ "d": "right-pointing magnifying glass",
+ "u": "6.0"
+ },
+ {
+ "n": "candle",
+ "c": "objects",
+ "e": "🕯",
+ "d": "candle",
+ "u": "7.0"
+ },
+ {
+ "n": "bulb",
+ "c": "objects",
+ "e": "💡",
+ "d": "electric light bulb",
+ "u": "6.0"
+ },
+ {
+ "n": "flashlight",
+ "c": "objects",
+ "e": "🔦",
+ "d": "electric torch",
+ "u": "6.0"
+ },
+ {
+ "n": "izakaya_lantern",
+ "c": "objects",
+ "e": "🏮",
+ "d": "izakaya lantern",
+ "u": "6.0"
+ },
+ {
+ "n": "notebook_with_decorative_cover",
+ "c": "objects",
+ "e": "📔",
+ "d": "notebook with decorative cover",
+ "u": "6.0"
+ },
+ {
+ "n": "closed_book",
+ "c": "objects",
+ "e": "📕",
+ "d": "closed book",
+ "u": "6.0"
+ },
+ {
+ "n": "book",
+ "c": "objects",
+ "e": "📖",
+ "d": "open book",
+ "u": "6.0"
+ },
+ {
+ "n": "green_book",
+ "c": "objects",
+ "e": "📗",
+ "d": "green book",
+ "u": "6.0"
+ },
+ {
+ "n": "blue_book",
+ "c": "objects",
+ "e": "📘",
+ "d": "blue book",
+ "u": "6.0"
+ },
+ {
+ "n": "orange_book",
+ "c": "objects",
+ "e": "📙",
+ "d": "orange book",
+ "u": "6.0"
+ },
+ {
+ "n": "books",
+ "c": "objects",
+ "e": "📚",
+ "d": "books",
+ "u": "6.0"
+ },
+ {
+ "n": "notebook",
+ "c": "objects",
+ "e": "📓",
+ "d": "notebook",
+ "u": "6.0"
+ },
+ {
+ "n": "ledger",
+ "c": "objects",
+ "e": "📒",
+ "d": "ledger",
+ "u": "6.0"
+ },
+ {
+ "n": "page_with_curl",
+ "c": "objects",
+ "e": "📃",
+ "d": "page with curl",
+ "u": "6.0"
+ },
+ {
+ "n": "scroll",
+ "c": "objects",
+ "e": "📜",
+ "d": "scroll",
+ "u": "6.0"
+ },
+ {
+ "n": "page_facing_up",
+ "c": "objects",
+ "e": "📄",
+ "d": "page facing up",
+ "u": "6.0"
+ },
+ {
+ "n": "newspaper",
+ "c": "objects",
+ "e": "📰",
+ "d": "newspaper",
+ "u": "6.0"
+ },
+ {
+ "n": "newspaper2",
+ "c": "objects",
+ "e": "🗞",
+ "d": "rolled-up newspaper",
+ "u": "7.0"
+ },
+ {
+ "n": "bookmark_tabs",
+ "c": "objects",
+ "e": "📑",
+ "d": "bookmark tabs",
+ "u": "6.0"
+ },
+ {
+ "n": "bookmark",
+ "c": "objects",
+ "e": "🔖",
+ "d": "bookmark",
+ "u": "6.0"
+ },
+ {
+ "n": "label",
+ "c": "objects",
+ "e": "🏷",
+ "d": "label",
+ "u": "7.0"
+ },
+ {
+ "n": "moneybag",
+ "c": "objects",
+ "e": "💰",
+ "d": "money bag",
+ "u": "6.0"
+ },
+ {
+ "n": "yen",
+ "c": "objects",
+ "e": "💴",
+ "d": "banknote with yen sign",
+ "u": "6.0"
+ },
+ {
+ "n": "dollar",
+ "c": "objects",
+ "e": "💵",
+ "d": "banknote with dollar sign",
+ "u": "6.0"
+ },
+ {
+ "n": "euro",
+ "c": "objects",
+ "e": "💶",
+ "d": "banknote with euro sign",
+ "u": "6.0"
+ },
+ {
+ "n": "pound",
+ "c": "objects",
+ "e": "💷",
+ "d": "banknote with pound sign",
+ "u": "6.0"
+ },
+ {
+ "n": "money_with_wings",
+ "c": "objects",
+ "e": "💸",
+ "d": "money with wings",
+ "u": "6.0"
+ },
+ {
+ "n": "credit_card",
+ "c": "objects",
+ "e": "💳",
+ "d": "credit card",
+ "u": "6.0"
+ },
+ {
+ "n": "chart",
+ "c": "symbols",
+ "e": "💹",
+ "d": "chart with upwards trend and yen sign",
+ "u": "6.0"
+ },
+ {
+ "n": "envelope",
+ "c": "objects",
+ "e": "✉",
+ "d": "envelope",
+ "u": "1.1"
+ },
+ {
+ "n": "e-mail",
+ "c": "objects",
+ "e": "📧",
+ "d": "e-mail symbol",
+ "u": "6.0"
+ },
+ {
+ "n": "incoming_envelope",
+ "c": "objects",
+ "e": "📨",
+ "d": "incoming envelope",
+ "u": "6.0"
+ },
+ {
+ "n": "envelope_with_arrow",
+ "c": "objects",
+ "e": "📩",
+ "d": "envelope with downwards arrow above",
+ "u": "6.0"
+ },
+ {
+ "n": "outbox_tray",
+ "c": "objects",
+ "e": "📤",
+ "d": "outbox tray",
+ "u": "6.0"
+ },
+ {
+ "n": "inbox_tray",
+ "c": "objects",
+ "e": "📥",
+ "d": "inbox tray",
+ "u": "6.0"
+ },
+ {
+ "n": "package",
+ "c": "objects",
+ "e": "📦",
+ "d": "package",
+ "u": "6.0"
+ },
+ {
+ "n": "mailbox",
+ "c": "objects",
+ "e": "📫",
+ "d": "closed mailbox with raised flag",
+ "u": "6.0"
+ },
+ {
+ "n": "mailbox_closed",
+ "c": "objects",
+ "e": "📪",
+ "d": "closed mailbox with lowered flag",
+ "u": "6.0"
+ },
+ {
+ "n": "mailbox_with_mail",
+ "c": "objects",
+ "e": "📬",
+ "d": "open mailbox with raised flag",
+ "u": "6.0"
+ },
+ {
+ "n": "mailbox_with_no_mail",
+ "c": "objects",
+ "e": "📭",
+ "d": "open mailbox with lowered flag",
+ "u": "6.0"
+ },
+ {
+ "n": "postbox",
+ "c": "objects",
+ "e": "📮",
+ "d": "postbox",
+ "u": "6.0"
+ },
+ {
+ "n": "ballot_box",
+ "c": "objects",
+ "e": "🗳",
+ "d": "ballot box with ballot",
+ "u": "7.0"
+ },
+ {
+ "n": "pencil2",
+ "c": "objects",
+ "e": "✏",
+ "d": "pencil",
+ "u": "1.1"
+ },
+ {
+ "n": "black_nib",
+ "c": "objects",
+ "e": "✒",
+ "d": "black nib",
+ "u": "1.1"
+ },
+ {
+ "n": "pen_fountain",
+ "c": "objects",
+ "e": "🖋",
+ "d": "lower left fountain pen",
+ "u": "7.0"
+ },
+ {
+ "n": "pen_ballpoint",
+ "c": "objects",
+ "e": "🖊",
+ "d": "lower left ballpoint pen",
+ "u": "7.0"
+ },
+ {
+ "n": "paintbrush",
+ "c": "objects",
+ "e": "🖌",
+ "d": "lower left paintbrush",
+ "u": "7.0"
+ },
+ {
+ "n": "crayon",
+ "c": "objects",
+ "e": "🖍",
+ "d": "lower left crayon",
+ "u": "7.0"
+ },
+ {
+ "n": "pencil",
+ "c": "objects",
+ "e": "📝",
+ "d": "memo",
+ "u": "6.0"
+ },
+ {
+ "n": "briefcase",
+ "c": "people",
+ "e": "💼",
+ "d": "briefcase",
+ "u": "6.0"
+ },
+ {
+ "n": "file_folder",
+ "c": "objects",
+ "e": "📁",
+ "d": "file folder",
+ "u": "6.0"
+ },
+ {
+ "n": "open_file_folder",
+ "c": "objects",
+ "e": "📂",
+ "d": "open file folder",
+ "u": "6.0"
+ },
+ {
+ "n": "dividers",
+ "c": "objects",
+ "e": "🗂",
+ "d": "card index dividers",
+ "u": "7.0"
+ },
+ {
+ "n": "date",
+ "c": "objects",
+ "e": "📅",
+ "d": "calendar",
+ "u": "6.0"
+ },
+ {
+ "n": "calendar",
+ "c": "objects",
+ "e": "📆",
+ "d": "tear-off calendar",
+ "u": "6.0"
+ },
+ {
+ "n": "notepad_spiral",
+ "c": "objects",
+ "e": "🗒",
+ "d": "spiral note pad",
+ "u": "7.0"
+ },
+ {
+ "n": "calendar_spiral",
+ "c": "objects",
+ "e": "🗓",
+ "d": "spiral calendar pad",
+ "u": "7.0"
+ },
+ {
+ "n": "card_index",
+ "c": "objects",
+ "e": "📇",
+ "d": "card index",
+ "u": "6.0"
+ },
+ {
+ "n": "chart_with_upwards_trend",
+ "c": "objects",
+ "e": "📈",
+ "d": "chart with upwards trend",
+ "u": "6.0"
+ },
+ {
+ "n": "chart_with_downwards_trend",
+ "c": "objects",
+ "e": "📉",
+ "d": "chart with downwards trend",
+ "u": "6.0"
+ },
+ {
+ "n": "bar_chart",
+ "c": "objects",
+ "e": "📊",
+ "d": "bar chart",
+ "u": "6.0"
+ },
+ {
+ "n": "clipboard",
+ "c": "objects",
+ "e": "📋",
+ "d": "clipboard",
+ "u": "6.0"
+ },
+ {
+ "n": "pushpin",
+ "c": "objects",
+ "e": "📌",
+ "d": "pushpin",
+ "u": "6.0"
+ },
+ {
+ "n": "round_pushpin",
+ "c": "objects",
+ "e": "📍",
+ "d": "round pushpin",
+ "u": "6.0"
+ },
+ {
+ "n": "paperclip",
+ "c": "objects",
+ "e": "📎",
+ "d": "paperclip",
+ "u": "6.0"
+ },
+ {
+ "n": "paperclips",
+ "c": "objects",
+ "e": "🖇",
+ "d": "linked paperclips",
+ "u": "7.0"
+ },
+ {
+ "n": "straight_ruler",
+ "c": "objects",
+ "e": "📏",
+ "d": "straight ruler",
+ "u": "6.0"
+ },
+ {
+ "n": "triangular_ruler",
+ "c": "objects",
+ "e": "📐",
+ "d": "triangular ruler",
+ "u": "6.0"
+ },
+ {
+ "n": "scissors",
+ "c": "objects",
+ "e": "✂",
+ "d": "black scissors",
+ "u": "1.1"
+ },
+ {
+ "n": "card_box",
+ "c": "objects",
+ "e": "🗃",
+ "d": "card file box",
+ "u": "7.0"
+ },
+ {
+ "n": "file_cabinet",
+ "c": "objects",
+ "e": "🗄",
+ "d": "file cabinet",
+ "u": "7.0"
+ },
+ {
+ "n": "wastebasket",
+ "c": "objects",
+ "e": "🗑",
+ "d": "wastebasket",
+ "u": "7.0"
+ },
+ {
+ "n": "lock",
+ "c": "objects",
+ "e": "🔒",
+ "d": "lock",
+ "u": "6.0"
+ },
+ {
+ "n": "unlock",
+ "c": "objects",
+ "e": "🔓",
+ "d": "open lock",
+ "u": "6.0"
+ },
+ {
+ "n": "lock_with_ink_pen",
+ "c": "objects",
+ "e": "🔏",
+ "d": "lock with ink pen",
+ "u": "6.0"
+ },
+ {
+ "n": "closed_lock_with_key",
+ "c": "objects",
+ "e": "🔐",
+ "d": "closed lock with key",
+ "u": "6.0"
+ },
+ {
+ "n": "key",
+ "c": "objects",
+ "e": "🔑",
+ "d": "key",
+ "u": "6.0"
+ },
+ {
+ "n": "key2",
+ "c": "objects",
+ "e": "🗝",
+ "d": "old key",
+ "u": "7.0"
+ },
+ {
+ "n": "hammer",
+ "c": "objects",
+ "e": "🔨",
+ "d": "hammer",
+ "u": "6.0"
+ },
+ {
+ "n": "pick",
+ "c": "objects",
+ "e": "⛏",
+ "d": "pick",
+ "u": "5.2"
+ },
+ {
+ "n": "hammer_pick",
+ "c": "objects",
+ "e": "⚒",
+ "d": "hammer and pick",
+ "u": "4.1"
+ },
+ {
+ "n": "tools",
+ "c": "objects",
+ "e": "🛠",
+ "d": "hammer and wrench",
+ "u": "7.0"
+ },
+ {
+ "n": "dagger",
+ "c": "objects",
+ "e": "🗡",
+ "d": "dagger knife",
+ "u": "7.0"
+ },
+ {
+ "n": "crossed_swords",
+ "c": "objects",
+ "e": "⚔",
+ "d": "crossed swords",
+ "u": "4.1"
+ },
+ {
+ "n": "bomb",
+ "c": "objects",
+ "e": "💣",
+ "d": "bomb",
+ "u": "6.0"
+ },
+ {
+ "n": "bow_and_arrow",
+ "c": "activity",
+ "e": "🏹",
+ "d": "bow and arrow",
+ "u": "8.0"
+ },
+ {
+ "n": "shield",
+ "c": "objects",
+ "e": "🛡",
+ "d": "shield",
+ "u": "7.0"
+ },
+ {
+ "n": "wrench",
+ "c": "objects",
+ "e": "🔧",
+ "d": "wrench",
+ "u": "6.0"
+ },
+ {
+ "n": "nut_and_bolt",
+ "c": "objects",
+ "e": "🔩",
+ "d": "nut and bolt",
+ "u": "6.0"
+ },
+ {
+ "n": "gear",
+ "c": "objects",
+ "e": "⚙",
+ "d": "gear",
+ "u": "4.1"
+ },
+ {
+ "n": "compression",
+ "c": "objects",
+ "e": "🗜",
+ "d": "compression",
+ "u": "7.0"
+ },
+ {
+ "n": "scales",
+ "c": "objects",
+ "e": "⚖",
+ "d": "scales",
+ "u": "4.1"
+ },
+ {
+ "n": "link",
+ "c": "objects",
+ "e": "🔗",
+ "d": "link symbol",
+ "u": "6.0"
+ },
+ {
+ "n": "chains",
+ "c": "objects",
+ "e": "⛓",
+ "d": "chains",
+ "u": "5.2"
+ },
+ {
+ "n": "alembic",
+ "c": "objects",
+ "e": "⚗",
+ "d": "alembic",
+ "u": "4.1"
+ },
+ {
+ "n": "microscope",
+ "c": "objects",
+ "e": "🔬",
+ "d": "microscope",
+ "u": "6.0"
+ },
+ {
+ "n": "telescope",
+ "c": "objects",
+ "e": "🔭",
+ "d": "telescope",
+ "u": "6.0"
+ },
+ {
+ "n": "satellite",
+ "c": "objects",
+ "e": "📡",
+ "d": "satellite antenna",
+ "u": "6.0"
+ },
+ {
+ "n": "syringe",
+ "c": "objects",
+ "e": "💉",
+ "d": "syringe",
+ "u": "6.0"
+ },
+ {
+ "n": "pill",
+ "c": "objects",
+ "e": "💊",
+ "d": "pill",
+ "u": "6.0"
+ },
+ {
+ "n": "door",
+ "c": "objects",
+ "e": "🚪",
+ "d": "door",
+ "u": "6.0"
+ },
+ {
+ "n": "bed",
+ "c": "objects",
+ "e": "🛏",
+ "d": "bed",
+ "u": "7.0"
+ },
+ {
+ "n": "couch",
+ "c": "objects",
+ "e": "🛋",
+ "d": "couch and lamp",
+ "u": "7.0"
+ },
+ {
+ "n": "toilet",
+ "c": "objects",
+ "e": "🚽",
+ "d": "toilet",
+ "u": "6.0"
+ },
+ {
+ "n": "shower",
+ "c": "objects",
+ "e": "🚿",
+ "d": "shower",
+ "u": "6.0"
+ },
+ {
+ "n": "bathtub",
+ "c": "objects",
+ "e": "🛁",
+ "d": "bathtub",
+ "u": "6.0"
+ },
+ {
+ "n": "shopping_cart",
+ "c": "objects",
+ "e": "🛒",
+ "d": "shopping trolley",
+ "u": "9.0"
+ },
+ {
+ "n": "smoking",
+ "c": "objects",
+ "e": "🚬",
+ "d": "smoking symbol",
+ "u": "6.0"
+ },
+ {
+ "n": "coffin",
+ "c": "objects",
+ "e": "⚰",
+ "d": "coffin",
+ "u": "4.1"
+ },
+ {
+ "n": "urn",
+ "c": "objects",
+ "e": "⚱",
+ "d": "funeral urn",
+ "u": "4.1"
+ },
+ {
+ "n": "moyai",
+ "c": "objects",
+ "e": "🗿",
+ "d": "moyai",
+ "u": "6.0"
+ },
+ {
+ "n": "atm",
+ "c": "symbols",
+ "e": "🏧",
+ "d": "automated teller machine",
+ "u": "6.0"
+ },
+ {
+ "n": "put_litter_in_its_place",
+ "c": "symbols",
+ "e": "🚮",
+ "d": "put litter in its place symbol",
+ "u": "6.0"
+ },
+ {
+ "n": "potable_water",
+ "c": "symbols",
+ "e": "🚰",
+ "d": "potable water symbol",
+ "u": "6.0"
+ },
+ {
+ "n": "wheelchair",
+ "c": "symbols",
+ "e": "♿",
+ "d": "wheelchair symbol",
+ "u": "4.1"
+ },
+ {
+ "n": "mens",
+ "c": "symbols",
+ "e": "🚹",
+ "d": "mens symbol",
+ "u": "6.0"
+ },
+ {
+ "n": "womens",
+ "c": "symbols",
+ "e": "🚺",
+ "d": "womens symbol",
+ "u": "6.0"
+ },
+ {
+ "n": "restroom",
+ "c": "symbols",
+ "e": "🚻",
+ "d": "restroom",
+ "u": "6.0"
+ },
+ {
+ "n": "baby_symbol",
+ "c": "symbols",
+ "e": "🚼",
+ "d": "baby symbol",
+ "u": "6.0"
+ },
+ {
+ "n": "wc",
+ "c": "symbols",
+ "e": "🚾",
+ "d": "water closet",
+ "u": "6.0"
+ },
+ {
+ "n": "passport_control",
+ "c": "symbols",
+ "e": "🛂",
+ "d": "passport control",
+ "u": "6.0"
+ },
+ {
+ "n": "customs",
+ "c": "symbols",
+ "e": "🛃",
+ "d": "customs",
+ "u": "6.0"
+ },
+ {
+ "n": "baggage_claim",
+ "c": "symbols",
+ "e": "🛄",
+ "d": "baggage claim",
+ "u": "6.0"
+ },
+ {
+ "n": "left_luggage",
+ "c": "symbols",
+ "e": "🛅",
+ "d": "left luggage",
+ "u": "6.0"
+ },
+ {
+ "n": "warning",
+ "c": "symbols",
+ "e": "⚠",
+ "d": "warning sign",
+ "u": "4.0"
+ },
+ {
+ "n": "children_crossing",
+ "c": "symbols",
+ "e": "🚸",
+ "d": "children crossing",
+ "u": "6.0"
+ },
+ {
+ "n": "no_entry",
+ "c": "symbols",
+ "e": "⛔",
+ "d": "no entry",
+ "u": "5.2"
+ },
+ {
+ "n": "no_entry_sign",
+ "c": "symbols",
+ "e": "🚫",
+ "d": "no entry sign",
+ "u": "6.0"
+ },
+ {
+ "n": "no_bicycles",
+ "c": "symbols",
+ "e": "🚳",
+ "d": "no bicycles",
+ "u": "6.0"
+ },
+ {
+ "n": "no_smoking",
+ "c": "symbols",
+ "e": "🚭",
+ "d": "no smoking symbol",
+ "u": "6.0"
+ },
+ {
+ "n": "do_not_litter",
+ "c": "symbols",
+ "e": "🚯",
+ "d": "do not litter symbol",
+ "u": "6.0"
+ },
+ {
+ "n": "non-potable_water",
+ "c": "symbols",
+ "e": "🚱",
+ "d": "non-potable water symbol",
+ "u": "6.0"
+ },
+ {
+ "n": "no_pedestrians",
+ "c": "symbols",
+ "e": "🚷",
+ "d": "no pedestrians",
+ "u": "6.0"
+ },
+ {
+ "n": "no_mobile_phones",
+ "c": "symbols",
+ "e": "📵",
+ "d": "no mobile phones",
+ "u": "6.0"
+ },
+ {
+ "n": "underage",
+ "c": "symbols",
+ "e": "🔞",
+ "d": "no one under eighteen symbol",
+ "u": "6.0"
+ },
+ {
+ "n": "radioactive",
+ "c": "symbols",
+ "e": "☢",
+ "d": "radioactive sign",
+ "u": "1.1"
+ },
+ {
+ "n": "biohazard",
+ "c": "symbols",
+ "e": "☣",
+ "d": "biohazard sign",
+ "u": "1.1"
+ },
+ {
+ "n": "arrow_up",
+ "c": "symbols",
+ "e": "⬆",
+ "d": "upwards black arrow",
+ "u": "4.0"
+ },
+ {
+ "n": "arrow_upper_right",
+ "c": "symbols",
+ "e": "↗",
+ "d": "north east arrow",
+ "u": "1.1"
+ },
+ {
+ "n": "arrow_right",
+ "c": "symbols",
+ "e": "➡",
+ "d": "black rightwards arrow",
+ "u": "1.1"
+ },
+ {
+ "n": "arrow_lower_right",
+ "c": "symbols",
+ "e": "↘",
+ "d": "south east arrow",
+ "u": "1.1"
+ },
+ {
+ "n": "arrow_down",
+ "c": "symbols",
+ "e": "⬇",
+ "d": "downwards black arrow",
+ "u": "4.0"
+ },
+ {
+ "n": "arrow_lower_left",
+ "c": "symbols",
+ "e": "↙",
+ "d": "south west arrow",
+ "u": "1.1"
+ },
+ {
+ "n": "arrow_left",
+ "c": "symbols",
+ "e": "⬅",
+ "d": "leftwards black arrow",
+ "u": "4.0"
+ },
+ {
+ "n": "arrow_upper_left",
+ "c": "symbols",
+ "e": "↖",
+ "d": "north west arrow",
+ "u": "1.1"
+ },
+ {
+ "n": "arrow_up_down",
+ "c": "symbols",
+ "e": "↕",
+ "d": "up down arrow",
+ "u": "1.1"
+ },
+ {
+ "n": "left_right_arrow",
+ "c": "symbols",
+ "e": "↔",
+ "d": "left right arrow",
+ "u": "1.1"
+ },
+ {
+ "n": "leftwards_arrow_with_hook",
+ "c": "symbols",
+ "e": "↩",
+ "d": "leftwards arrow with hook",
+ "u": "1.1"
+ },
+ {
+ "n": "arrow_right_hook",
+ "c": "symbols",
+ "e": "↪",
+ "d": "rightwards arrow with hook",
+ "u": "1.1"
+ },
+ {
+ "n": "arrow_heading_up",
+ "c": "symbols",
+ "e": "⤴",
+ "d": "arrow pointing rightwards then curving upwards",
+ "u": "3.2"
+ },
+ {
+ "n": "arrow_heading_down",
+ "c": "symbols",
+ "e": "⤵",
+ "d": "arrow pointing rightwards then curving downwards",
+ "u": "3.2"
+ },
+ {
+ "n": "arrows_clockwise",
+ "c": "symbols",
+ "e": "🔃",
+ "d": "clockwise downwards and upwards open circle arrows",
+ "u": "6.0"
+ },
+ {
+ "n": "arrows_counterclockwise",
+ "c": "symbols",
+ "e": "🔄",
+ "d": "anticlockwise downwards and upwards open circle ar",
+ "u": "6.0"
+ },
+ {
+ "n": "back",
+ "c": "symbols",
+ "e": "🔙",
+ "d": "back with leftwards arrow above",
+ "u": "6.0"
+ },
+ {
+ "n": "end",
+ "c": "symbols",
+ "e": "🔚",
+ "d": "end with leftwards arrow above",
+ "u": "6.0"
+ },
+ {
+ "n": "on",
+ "c": "symbols",
+ "e": "🔛",
+ "d": "on with exclamation mark with left right arrow abo",
+ "u": "6.0"
+ },
+ {
+ "n": "soon",
+ "c": "symbols",
+ "e": "🔜",
+ "d": "soon with rightwards arrow above",
+ "u": "6.0"
+ },
+ {
+ "n": "top",
+ "c": "symbols",
+ "e": "🔝",
+ "d": "top with upwards arrow above",
+ "u": "6.0"
+ },
+ {
+ "n": "place_of_worship",
+ "c": "symbols",
+ "e": "🛐",
+ "d": "place of worship",
+ "u": "8.0"
+ },
+ {
+ "n": "atom",
+ "c": "symbols",
+ "e": "⚛",
+ "d": "atom symbol",
+ "u": "4.1"
+ },
+ {
+ "n": "om_symbol",
+ "c": "symbols",
+ "e": "🕉",
+ "d": "om symbol",
+ "u": "7.0"
+ },
+ {
+ "n": "star_of_david",
+ "c": "symbols",
+ "e": "✡",
+ "d": "star of david",
+ "u": "1.1"
+ },
+ {
+ "n": "wheel_of_dharma",
+ "c": "symbols",
+ "e": "☸",
+ "d": "wheel of dharma",
+ "u": "1.1"
+ },
+ {
+ "n": "yin_yang",
+ "c": "symbols",
+ "e": "☯",
+ "d": "yin yang",
+ "u": "1.1"
+ },
+ {
+ "n": "cross",
+ "c": "symbols",
+ "e": "✝",
+ "d": "latin cross",
+ "u": "1.1"
+ },
+ {
+ "n": "orthodox_cross",
+ "c": "symbols",
+ "e": "☦",
+ "d": "orthodox cross",
+ "u": "1.1"
+ },
+ {
+ "n": "star_and_crescent",
+ "c": "symbols",
+ "e": "☪",
+ "d": "star and crescent",
+ "u": "1.1"
+ },
+ {
+ "n": "peace",
+ "c": "symbols",
+ "e": "☮",
+ "d": "peace symbol",
+ "u": "1.1"
+ },
+ {
+ "n": "menorah",
+ "c": "symbols",
+ "e": "🕎",
+ "d": "menorah with nine branches",
+ "u": "8.0"
+ },
+ {
+ "n": "six_pointed_star",
+ "c": "symbols",
+ "e": "🔯",
+ "d": "six pointed star with middle dot",
+ "u": "6.0"
+ },
+ {
+ "n": "aries",
+ "c": "symbols",
+ "e": "♈",
+ "d": "aries",
+ "u": "1.1"
+ },
+ {
+ "n": "taurus",
+ "c": "symbols",
+ "e": "♉",
+ "d": "taurus",
+ "u": "1.1"
+ },
+ {
+ "n": "gemini",
+ "c": "symbols",
+ "e": "♊",
+ "d": "gemini",
+ "u": "1.1"
+ },
+ {
+ "n": "cancer",
+ "c": "symbols",
+ "e": "♋",
+ "d": "cancer",
+ "u": "1.1"
+ },
+ {
+ "n": "leo",
+ "c": "symbols",
+ "e": "♌",
+ "d": "leo",
+ "u": "1.1"
+ },
+ {
+ "n": "virgo",
+ "c": "symbols",
+ "e": "♍",
+ "d": "virgo",
+ "u": "1.1"
+ },
+ {
+ "n": "libra",
+ "c": "symbols",
+ "e": "♎",
+ "d": "libra",
+ "u": "1.1"
+ },
+ {
+ "n": "scorpius",
+ "c": "symbols",
+ "e": "♏",
+ "d": "scorpius",
+ "u": "1.1"
+ },
+ {
+ "n": "sagittarius",
+ "c": "symbols",
+ "e": "♐",
+ "d": "sagittarius",
+ "u": "1.1"
+ },
+ {
+ "n": "capricorn",
+ "c": "symbols",
+ "e": "♑",
+ "d": "capricorn",
+ "u": "1.1"
+ },
+ {
+ "n": "aquarius",
+ "c": "symbols",
+ "e": "♒",
+ "d": "aquarius",
+ "u": "1.1"
+ },
+ {
+ "n": "pisces",
+ "c": "symbols",
+ "e": "♓",
+ "d": "pisces",
+ "u": "1.1"
+ },
+ {
+ "n": "ophiuchus",
+ "c": "symbols",
+ "e": "⛎",
+ "d": "ophiuchus",
+ "u": "6.0"
+ },
+ {
+ "n": "twisted_rightwards_arrows",
+ "c": "symbols",
+ "e": "🔀",
+ "d": "twisted rightwards arrows",
+ "u": "6.0"
+ },
+ {
+ "n": "repeat",
+ "c": "symbols",
+ "e": "🔁",
+ "d": "clockwise rightwards and leftwards open circle arr",
+ "u": "6.0"
+ },
+ {
+ "n": "repeat_one",
+ "c": "symbols",
+ "e": "🔂",
+ "d": "clockwise rightwards and leftwards open circle arr",
+ "u": "6.0"
+ },
+ {
+ "n": "arrow_forward",
+ "c": "symbols",
+ "e": "▶",
+ "d": "black right-pointing triangle",
+ "u": "1.1"
+ },
+ {
+ "n": "fast_forward",
+ "c": "symbols",
+ "e": "⏩",
+ "d": "black right-pointing double triangle",
+ "u": "6.0"
+ },
+ {
+ "n": "track_next",
+ "c": "symbols",
+ "e": "⏭",
+ "d": "black right-pointing double triangle with vertical bar",
+ "u": "6.0"
+ },
+ {
+ "n": "play_pause",
+ "c": "symbols",
+ "e": "⏯",
+ "d": "black right-pointing double triangle with double vertical bar",
+ "u": "6.0"
+ },
+ {
+ "n": "arrow_backward",
+ "c": "symbols",
+ "e": "◀",
+ "d": "black left-pointing triangle",
+ "u": "1.1"
+ },
+ {
+ "n": "rewind",
+ "c": "symbols",
+ "e": "⏪",
+ "d": "black left-pointing double triangle",
+ "u": "6.0"
+ },
+ {
+ "n": "track_previous",
+ "c": "symbols",
+ "e": "⏮",
+ "d": "black left-pointing double triangle with vertical bar",
+ "u": "6.0"
+ },
+ {
+ "n": "arrow_up_small",
+ "c": "symbols",
+ "e": "🔼",
+ "d": "up-pointing small red triangle",
+ "u": "6.0"
+ },
+ {
+ "n": "arrow_double_up",
+ "c": "symbols",
+ "e": "⏫",
+ "d": "black up-pointing double triangle",
+ "u": "6.0"
+ },
+ {
+ "n": "arrow_down_small",
+ "c": "symbols",
+ "e": "🔽",
+ "d": "down-pointing small red triangle",
+ "u": "6.0"
+ },
+ {
+ "n": "arrow_double_down",
+ "c": "symbols",
+ "e": "⏬",
+ "d": "black down-pointing double triangle",
+ "u": "6.0"
+ },
+ {
+ "n": "pause_button",
+ "c": "symbols",
+ "e": "⏸",
+ "d": "double vertical bar",
+ "u": "7.0"
+ },
+ {
+ "n": "stop_button",
+ "c": "symbols",
+ "e": "⏹",
+ "d": "black square for stop",
+ "u": "7.0"
+ },
+ {
+ "n": "record_button",
+ "c": "symbols",
+ "e": "⏺",
+ "d": "black circle for record",
+ "u": "7.0"
+ },
+ {
+ "n": "eject",
+ "c": "symbols",
+ "e": "⏏",
+ "d": "eject symbol",
+ "u": "4.0"
+ },
+ {
+ "n": "cinema",
+ "c": "symbols",
+ "e": "🎦",
+ "d": "cinema",
+ "u": "6.0"
+ },
+ {
+ "n": "low_brightness",
+ "c": "symbols",
+ "e": "🔅",
+ "d": "low brightness symbol",
+ "u": "6.0"
+ },
+ {
+ "n": "high_brightness",
+ "c": "symbols",
+ "e": "🔆",
+ "d": "high brightness symbol",
+ "u": "6.0"
+ },
+ {
+ "n": "signal_strength",
+ "c": "symbols",
+ "e": "📶",
+ "d": "antenna with bars",
+ "u": "6.0"
+ },
+ {
+ "n": "vibration_mode",
+ "c": "symbols",
+ "e": "📳",
+ "d": "vibration mode",
+ "u": "6.0"
+ },
+ {
+ "n": "mobile_phone_off",
+ "c": "symbols",
+ "e": "📴",
+ "d": "mobile phone off",
+ "u": "6.0"
+ },
+ {
+ "n": "heavy_multiplication_x",
+ "c": "symbols",
+ "e": "✖",
+ "d": "heavy multiplication x",
+ "u": "1.1"
+ },
+ {
+ "n": "heavy_plus_sign",
+ "c": "symbols",
+ "e": "➕",
+ "d": "heavy plus sign",
+ "u": "6.0"
+ },
+ {
+ "n": "heavy_minus_sign",
+ "c": "symbols",
+ "e": "➖",
+ "d": "heavy minus sign",
+ "u": "6.0"
+ },
+ {
+ "n": "heavy_division_sign",
+ "c": "symbols",
+ "e": "➗",
+ "d": "heavy division sign",
+ "u": "6.0"
+ },
+ {
+ "n": "bangbang",
+ "c": "symbols",
+ "e": "‼",
+ "d": "double exclamation mark",
+ "u": "1.1"
+ },
+ {
+ "n": "interrobang",
+ "c": "symbols",
+ "e": "⁉",
+ "d": "exclamation question mark",
+ "u": "3.0"
+ },
+ {
+ "n": "question",
+ "c": "symbols",
+ "e": "❓",
+ "d": "black question mark ornament",
+ "u": "6.0"
+ },
+ {
+ "n": "grey_question",
+ "c": "symbols",
+ "e": "❔",
+ "d": "white question mark ornament",
+ "u": "6.0"
+ },
+ {
+ "n": "grey_exclamation",
+ "c": "symbols",
+ "e": "❕",
+ "d": "white exclamation mark ornament",
+ "u": "6.0"
+ },
+ {
+ "n": "exclamation",
+ "c": "symbols",
+ "e": "❗",
+ "d": "heavy exclamation mark symbol",
+ "u": "5.2"
+ },
+ {
+ "n": "wavy_dash",
+ "c": "symbols",
+ "e": "〰",
+ "d": "wavy dash",
+ "u": "1.1"
+ },
+ {
+ "n": "currency_exchange",
+ "c": "symbols",
+ "e": "💱",
+ "d": "currency exchange",
+ "u": "6.0"
+ },
+ {
+ "n": "heavy_dollar_sign",
+ "c": "symbols",
+ "e": "💲",
+ "d": "heavy dollar sign",
+ "u": "6.0"
+ },
+ {
+ "n": "recycle",
+ "c": "symbols",
+ "e": "♻",
+ "d": "black universal recycling symbol",
+ "u": "3.2"
+ },
+ {
+ "n": "fleur-de-lis",
+ "c": "symbols",
+ "e": "⚜",
+ "d": "fleur-de-lis",
+ "u": "4.1"
+ },
+ {
+ "n": "trident",
+ "c": "symbols",
+ "e": "🔱",
+ "d": "trident emblem",
+ "u": "6.0"
+ },
+ {
+ "n": "name_badge",
+ "c": "symbols",
+ "e": "📛",
+ "d": "name badge",
+ "u": "6.0"
+ },
+ {
+ "n": "beginner",
+ "c": "symbols",
+ "e": "🔰",
+ "d": "japanese symbol for beginner",
+ "u": "6.0"
+ },
+ {
+ "n": "o",
+ "c": "symbols",
+ "e": "⭕",
+ "d": "heavy large circle",
+ "u": "5.2"
+ },
+ {
+ "n": "white_check_mark",
+ "c": "symbols",
+ "e": "✅",
+ "d": "white heavy check mark",
+ "u": "6.0"
+ },
+ {
+ "n": "ballot_box_with_check",
+ "c": "symbols",
+ "e": "☑",
+ "d": "ballot box with check",
+ "u": "1.1"
+ },
+ {
+ "n": "heavy_check_mark",
+ "c": "symbols",
+ "e": "✔",
+ "d": "heavy check mark",
+ "u": "1.1"
+ },
+ {
+ "n": "x",
+ "c": "symbols",
+ "e": "❌",
+ "d": "cross mark",
+ "u": "6.0"
+ },
+ {
+ "n": "negative_squared_cross_mark",
+ "c": "symbols",
+ "e": "❎",
+ "d": "negative squared cross mark",
+ "u": "6.0"
+ },
+ {
+ "n": "curly_loop",
+ "c": "symbols",
+ "e": "➰",
+ "d": "curly loop",
+ "u": "6.0"
+ },
+ {
+ "n": "loop",
+ "c": "symbols",
+ "e": "➿",
+ "d": "double curly loop",
+ "u": "6.0"
+ },
+ {
+ "n": "part_alternation_mark",
+ "c": "symbols",
+ "e": "〽",
+ "d": "part alternation mark",
+ "u": "3.2"
+ },
+ {
+ "n": "eight_spoked_asterisk",
+ "c": "symbols",
+ "e": "✳",
+ "d": "eight spoked asterisk",
+ "u": "1.1"
+ },
+ {
+ "n": "eight_pointed_black_star",
+ "c": "symbols",
+ "e": "✴",
+ "d": "eight pointed black star",
+ "u": "1.1"
+ },
+ {
+ "n": "sparkle",
+ "c": "symbols",
+ "e": "❇",
+ "d": "sparkle",
+ "u": "1.1"
+ },
+ {
+ "n": "copyright",
+ "c": "symbols",
+ "e": "©️",
+ "d": "copyright sign",
+ "u": "1.1"
+ },
+ {
+ "n": "registered",
+ "c": "symbols",
+ "e": "®️",
+ "d": "registered sign",
+ "u": "1.1"
+ },
+ {
+ "n": "tm",
+ "c": "symbols",
+ "e": "™️",
+ "d": "trade mark sign",
+ "u": "1.1"
+ },
+ {
+ "n": "hash",
+ "c": "symbols",
+ "e": "#⃣",
+ "d": "number sign",
+ "u": "3.0"
+ },
+ {
+ "n": "asterisk",
+ "c": "symbols",
+ "e": "*⃣",
+ "d": "keycap asterisk",
+ "u": "3.0"
+ },
+ {
+ "n": "zero",
+ "c": "symbols",
+ "e": "0️⃣",
+ "d": "keycap digit zero",
+ "u": "3.0"
+ },
+ {
+ "n": "one",
+ "c": "symbols",
+ "e": "1️⃣",
+ "d": "keycap digit one",
+ "u": "3.0"
+ },
+ {
+ "n": "two",
+ "c": "symbols",
+ "e": "2️⃣",
+ "d": "keycap digit two",
+ "u": "3.0"
+ },
+ {
+ "n": "three",
+ "c": "symbols",
+ "e": "3️⃣",
+ "d": "keycap digit three",
+ "u": "3.0"
+ },
+ {
+ "n": "four",
+ "c": "symbols",
+ "e": "4️⃣",
+ "d": "keycap digit four",
+ "u": "3.0"
+ },
+ {
+ "n": "five",
+ "c": "symbols",
+ "e": "5️⃣",
+ "d": "keycap digit five",
+ "u": "3.0"
+ },
+ {
+ "n": "six",
+ "c": "symbols",
+ "e": "6️⃣",
+ "d": "keycap digit six",
+ "u": "3.0"
+ },
+ {
+ "n": "seven",
+ "c": "symbols",
+ "e": "7️⃣",
+ "d": "keycap digit seven",
+ "u": "3.0"
+ },
+ {
+ "n": "eight",
+ "c": "symbols",
+ "e": "8️⃣",
+ "d": "keycap digit eight",
+ "u": "3.0"
+ },
+ {
+ "n": "nine",
+ "c": "symbols",
+ "e": "9️⃣",
+ "d": "keycap digit nine",
+ "u": "3.0"
+ },
+ {
+ "n": "ten",
+ "c": "symbols",
+ "e": "🔟",
+ "d": "keycap ten",
+ "u": "6.0"
+ },
+ {
+ "n": "capital_abcd",
+ "c": "symbols",
+ "e": "🔠",
+ "d": "input symbol for latin capital letters",
+ "u": "6.0"
+ },
+ {
+ "n": "abcd",
+ "c": "symbols",
+ "e": "🔡",
+ "d": "input symbol for latin small letters",
+ "u": "6.0"
+ },
+ {
+ "n": "1234",
+ "c": "symbols",
+ "e": "🔢",
+ "d": "input symbol for numbers",
+ "u": "6.0"
+ },
+ {
+ "n": "symbols",
+ "c": "symbols",
+ "e": "🔣",
+ "d": "input symbol for symbols",
+ "u": "6.0"
+ },
+ {
+ "n": "abc",
+ "c": "symbols",
+ "e": "🔤",
+ "d": "input symbol for latin letters",
+ "u": "6.0"
+ },
+ {
+ "n": "a",
+ "c": "symbols",
+ "e": "🅰",
+ "d": "negative squared latin capital letter a",
+ "u": "6.0"
+ },
+ {
+ "n": "ab",
+ "c": "symbols",
+ "e": "🆎",
+ "d": "negative squared ab",
+ "u": "6.0"
+ },
+ {
+ "n": "b",
+ "c": "symbols",
+ "e": "🅱",
+ "d": "negative squared latin capital letter b",
+ "u": "6.0"
+ },
+ {
+ "n": "cl",
+ "c": "symbols",
+ "e": "🆑",
+ "d": "squared cl",
+ "u": "6.0"
+ },
+ {
+ "n": "cool",
+ "c": "symbols",
+ "e": "🆒",
+ "d": "squared cool",
+ "u": "6.0"
+ },
+ {
+ "n": "free",
+ "c": "symbols",
+ "e": "🆓",
+ "d": "squared free",
+ "u": "6.0"
+ },
+ {
+ "n": "information_source",
+ "c": "symbols",
+ "e": "ℹ",
+ "d": "information source",
+ "u": "3.0"
+ },
+ {
+ "n": "id",
+ "c": "symbols",
+ "e": "🆔",
+ "d": "squared id",
+ "u": "6.0"
+ },
+ {
+ "n": "m",
+ "c": "symbols",
+ "e": "Ⓜ",
+ "d": "circled latin capital letter m",
+ "u": "1.1"
+ },
+ {
+ "n": "new",
+ "c": "symbols",
+ "e": "🆕",
+ "d": "squared new",
+ "u": "6.0"
+ },
+ {
+ "n": "ng",
+ "c": "symbols",
+ "e": "🆖",
+ "d": "squared ng",
+ "u": "6.0"
+ },
+ {
+ "n": "o2",
+ "c": "symbols",
+ "e": "🅾",
+ "d": "negative squared latin capital letter o",
+ "u": "6.0"
+ },
+ {
+ "n": "ok",
+ "c": "symbols",
+ "e": "🆗",
+ "d": "squared ok",
+ "u": "6.0"
+ },
+ {
+ "n": "parking",
+ "c": "symbols",
+ "e": "🅿",
+ "d": "negative squared latin capital letter p",
+ "u": "5.2"
+ },
+ {
+ "n": "sos",
+ "c": "symbols",
+ "e": "🆘",
+ "d": "squared sos",
+ "u": "6.0"
+ },
+ {
+ "n": "up",
+ "c": "symbols",
+ "e": "🆙",
+ "d": "squared up with exclamation mark",
+ "u": "6.0"
+ },
+ {
+ "n": "vs",
+ "c": "symbols",
+ "e": "🆚",
+ "d": "squared vs",
+ "u": "6.0"
+ },
+ {
+ "n": "koko",
+ "c": "symbols",
+ "e": "🈁",
+ "d": "squared katakana koko",
+ "u": "6.0"
+ },
+ {
+ "n": "sa",
+ "c": "symbols",
+ "e": "🈂",
+ "d": "squared katakana sa",
+ "u": "6.0"
+ },
+ {
+ "n": "u6708",
+ "c": "symbols",
+ "e": "🈷",
+ "d": "squared cjk unified ideograph-6708",
+ "u": "6.0"
+ },
+ {
+ "n": "u6709",
+ "c": "symbols",
+ "e": "🈶",
+ "d": "squared cjk unified ideograph-6709",
+ "u": "6.0"
+ },
+ {
+ "n": "u6307",
+ "c": "symbols",
+ "e": "🈯",
+ "d": "squared cjk unified ideograph-6307",
+ "u": "5.2"
+ },
+ {
+ "n": "ideograph_advantage",
+ "c": "symbols",
+ "e": "🉐",
+ "d": "circled ideograph advantage",
+ "u": "6.0"
+ },
+ {
+ "n": "u5272",
+ "c": "symbols",
+ "e": "🈹",
+ "d": "squared cjk unified ideograph-5272",
+ "u": "6.0"
+ },
+ {
+ "n": "u7121",
+ "c": "symbols",
+ "e": "🈚",
+ "d": "squared cjk unified ideograph-7121",
+ "u": "5.2"
+ },
+ {
+ "n": "u7981",
+ "c": "symbols",
+ "e": "🈲",
+ "d": "squared cjk unified ideograph-7981",
+ "u": "6.0"
+ },
+ {
+ "n": "accept",
+ "c": "symbols",
+ "e": "🉑",
+ "d": "circled ideograph accept",
+ "u": "6.0"
+ },
+ {
+ "n": "u7533",
+ "c": "symbols",
+ "e": "🈸",
+ "d": "squared cjk unified ideograph-7533",
+ "u": "6.0"
+ },
+ {
+ "n": "u5408",
+ "c": "symbols",
+ "e": "🈴",
+ "d": "squared cjk unified ideograph-5408",
+ "u": "6.0"
+ },
+ {
+ "n": "u7a7a",
+ "c": "symbols",
+ "e": "🈳",
+ "d": "squared cjk unified ideograph-7a7a",
+ "u": "6.0"
+ },
+ {
+ "n": "congratulations",
+ "c": "symbols",
+ "e": "㊗",
+ "d": "circled ideograph congratulation",
+ "u": "1.1"
+ },
+ {
+ "n": "secret",
+ "c": "symbols",
+ "e": "㊙",
+ "d": "circled ideograph secret",
+ "u": "1.1"
+ },
+ {
+ "n": "u55b6",
+ "c": "symbols",
+ "e": "🈺",
+ "d": "squared cjk unified ideograph-55b6",
+ "u": "6.0"
+ },
+ {
+ "n": "u6e80",
+ "c": "symbols",
+ "e": "🈵",
+ "d": "squared cjk unified ideograph-6e80",
+ "u": "6.0"
+ },
+ {
+ "n": "red_circle",
+ "c": "symbols",
+ "e": "🔴",
+ "d": "large red circle",
+ "u": "6.0"
+ },
+ {
+ "n": "large_blue_circle",
+ "c": "symbols",
+ "e": "🔵",
+ "d": "large blue circle",
+ "u": "6.0"
+ },
+ {
+ "n": "black_circle",
+ "c": "symbols",
+ "e": "⚫",
+ "d": "medium black circle",
+ "u": "4.1"
+ },
+ {
+ "n": "white_circle",
+ "c": "symbols",
+ "e": "⚪",
+ "d": "medium white circle",
+ "u": "4.1"
+ },
+ {
+ "n": "black_large_square",
+ "c": "symbols",
+ "e": "⬛",
+ "d": "black large square",
+ "u": "5.1"
+ },
+ {
+ "n": "white_large_square",
+ "c": "symbols",
+ "e": "⬜",
+ "d": "white large square",
+ "u": "5.1"
+ },
+ {
+ "n": "black_medium_square",
+ "c": "symbols",
+ "e": "◼",
+ "d": "black medium square",
+ "u": "3.2"
+ },
+ {
+ "n": "white_medium_square",
+ "c": "symbols",
+ "e": "◻",
+ "d": "white medium square",
+ "u": "3.2"
+ },
+ {
+ "n": "black_medium_small_square",
+ "c": "symbols",
+ "e": "◾",
+ "d": "black medium small square",
+ "u": "3.2"
+ },
+ {
+ "n": "white_medium_small_square",
+ "c": "symbols",
+ "e": "◽",
+ "d": "white medium small square",
+ "u": "3.2"
+ },
+ {
+ "n": "black_small_square",
+ "c": "symbols",
+ "e": "▪",
+ "d": "black small square",
+ "u": "1.1"
+ },
+ {
+ "n": "white_small_square",
+ "c": "symbols",
+ "e": "▫",
+ "d": "white small square",
+ "u": "1.1"
+ },
+ {
+ "n": "large_orange_diamond",
+ "c": "symbols",
+ "e": "🔶",
+ "d": "large orange diamond",
+ "u": "6.0"
+ },
+ {
+ "n": "large_blue_diamond",
+ "c": "symbols",
+ "e": "🔷",
+ "d": "large blue diamond",
+ "u": "6.0"
+ },
+ {
+ "n": "small_orange_diamond",
+ "c": "symbols",
+ "e": "🔸",
+ "d": "small orange diamond",
+ "u": "6.0"
+ },
+ {
+ "n": "small_blue_diamond",
+ "c": "symbols",
+ "e": "🔹",
+ "d": "small blue diamond",
+ "u": "6.0"
+ },
+ {
+ "n": "small_red_triangle",
+ "c": "symbols",
+ "e": "🔺",
+ "d": "up-pointing red triangle",
+ "u": "6.0"
+ },
+ {
+ "n": "small_red_triangle_down",
+ "c": "symbols",
+ "e": "🔻",
+ "d": "down-pointing red triangle",
+ "u": "6.0"
+ },
+ {
+ "n": "diamond_shape_with_a_dot_inside",
+ "c": "symbols",
+ "e": "💠",
+ "d": "diamond shape with a dot inside",
+ "u": "6.0"
+ },
+ {
+ "n": "radio_button",
+ "c": "symbols",
+ "e": "🔘",
+ "d": "radio button",
+ "u": "6.0"
+ },
+ {
+ "n": "white_square_button",
+ "c": "symbols",
+ "e": "🔳",
+ "d": "white square button",
+ "u": "6.0"
+ },
+ {
+ "n": "black_square_button",
+ "c": "symbols",
+ "e": "🔲",
+ "d": "black square button",
+ "u": "6.0"
+ },
+ {
+ "n": "checkered_flag",
+ "c": "travel",
+ "e": "🏁",
+ "d": "chequered flag",
+ "u": "6.0"
+ },
+ {
+ "n": "triangular_flag_on_post",
+ "c": "objects",
+ "e": "🚩",
+ "d": "triangular flag on post",
+ "u": "6.0"
+ },
+ {
+ "n": "crossed_flags",
+ "c": "objects",
+ "e": "🎌",
+ "d": "crossed flags",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_black",
+ "c": "objects",
+ "e": "🏴",
+ "d": "waving black flag",
+ "u": "7.0"
+ },
+ {
+ "n": "flag_white",
+ "c": "objects",
+ "e": "🏳",
+ "d": "waving white flag",
+ "u": "7.0"
+ },
+ {
+ "n": "gay_pride_flag",
+ "c": "flags",
+ "e": "🏳️‍🌈",
+ "d": "gay_pride_flag",
+ "u": "7.0"
+ },
+ {
+ "n": "flag_ac",
+ "c": "flags",
+ "e": "🇦🇨",
+ "d": "ascension",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_ad",
+ "c": "flags",
+ "e": "🇦🇩",
+ "d": "andorra",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_ae",
+ "c": "flags",
+ "e": "🇦🇪",
+ "d": "the united arab emirates",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_af",
+ "c": "flags",
+ "e": "🇦🇫",
+ "d": "afghanistan",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_ag",
+ "c": "flags",
+ "e": "🇦🇬",
+ "d": "antigua and barbuda",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_ai",
+ "c": "flags",
+ "e": "🇦🇮",
+ "d": "anguilla",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_al",
+ "c": "flags",
+ "e": "🇦🇱",
+ "d": "albania",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_am",
+ "c": "flags",
+ "e": "🇦🇲",
+ "d": "armenia",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_ao",
+ "c": "flags",
+ "e": "🇦🇴",
+ "d": "angola",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_aq",
+ "c": "flags",
+ "e": "🇦🇶",
+ "d": "antarctica",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_ar",
+ "c": "flags",
+ "e": "🇦🇷",
+ "d": "argentina",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_as",
+ "c": "flags",
+ "e": "🇦🇸",
+ "d": "american samoa",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_at",
+ "c": "flags",
+ "e": "🇦🇹",
+ "d": "austria",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_au",
+ "c": "flags",
+ "e": "🇦🇺",
+ "d": "australia",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_aw",
+ "c": "flags",
+ "e": "🇦🇼",
+ "d": "aruba",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_ax",
+ "c": "flags",
+ "e": "🇦🇽",
+ "d": "åland islands",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_az",
+ "c": "flags",
+ "e": "🇦🇿",
+ "d": "azerbaijan",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_ba",
+ "c": "flags",
+ "e": "🇧🇦",
+ "d": "bosnia and herzegovina",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_bb",
+ "c": "flags",
+ "e": "🇧🇧",
+ "d": "barbados",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_bd",
+ "c": "flags",
+ "e": "🇧🇩",
+ "d": "bangladesh",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_be",
+ "c": "flags",
+ "e": "🇧🇪",
+ "d": "belgium",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_bf",
+ "c": "flags",
+ "e": "🇧🇫",
+ "d": "burkina faso",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_bg",
+ "c": "flags",
+ "e": "🇧🇬",
+ "d": "bulgaria",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_bh",
+ "c": "flags",
+ "e": "🇧🇭",
+ "d": "bahrain",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_bi",
+ "c": "flags",
+ "e": "🇧🇮",
+ "d": "burundi",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_bj",
+ "c": "flags",
+ "e": "🇧🇯",
+ "d": "benin",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_bl",
+ "c": "flags",
+ "e": "🇧🇱",
+ "d": "saint barthélemy",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_bm",
+ "c": "flags",
+ "e": "🇧🇲",
+ "d": "bermuda",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_bn",
+ "c": "flags",
+ "e": "🇧🇳",
+ "d": "brunei",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_bo",
+ "c": "flags",
+ "e": "🇧🇴",
+ "d": "bolivia",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_bq",
+ "c": "flags",
+ "e": "🇧🇶",
+ "d": "caribbean netherlands",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_br",
+ "c": "flags",
+ "e": "🇧🇷",
+ "d": "brazil",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_bs",
+ "c": "flags",
+ "e": "🇧🇸",
+ "d": "the bahamas",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_bt",
+ "c": "flags",
+ "e": "🇧🇹",
+ "d": "bhutan",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_bv",
+ "c": "flags",
+ "e": "🇧🇻",
+ "d": "bouvet island",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_bw",
+ "c": "flags",
+ "e": "🇧🇼",
+ "d": "botswana",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_by",
+ "c": "flags",
+ "e": "🇧🇾",
+ "d": "belarus",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_bz",
+ "c": "flags",
+ "e": "🇧🇿",
+ "d": "belize",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_ca",
+ "c": "flags",
+ "e": "🇨🇦",
+ "d": "canada",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_cc",
+ "c": "flags",
+ "e": "🇨🇨",
+ "d": "cocos (keeling) islands",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_cd",
+ "c": "flags",
+ "e": "🇨🇩",
+ "d": "the democratic republic of the congo",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_cf",
+ "c": "flags",
+ "e": "🇨🇫",
+ "d": "central african republic",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_cg",
+ "c": "flags",
+ "e": "🇨🇬",
+ "d": "the republic of the congo",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_ch",
+ "c": "flags",
+ "e": "🇨🇭",
+ "d": "switzerland",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_ci",
+ "c": "flags",
+ "e": "🇨🇮",
+ "d": "cote d'ivoire",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_ck",
+ "c": "flags",
+ "e": "🇨🇰",
+ "d": "cook islands",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_cl",
+ "c": "flags",
+ "e": "🇨🇱",
+ "d": "chile",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_cm",
+ "c": "flags",
+ "e": "🇨🇲",
+ "d": "cameroon",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_cn",
+ "c": "flags",
+ "e": "🇨🇳",
+ "d": "china",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_co",
+ "c": "flags",
+ "e": "🇨🇴",
+ "d": "colombia",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_cp",
+ "c": "flags",
+ "e": "🇨🇵",
+ "d": "clipperton island",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_cr",
+ "c": "flags",
+ "e": "🇨🇷",
+ "d": "costa rica",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_cu",
+ "c": "flags",
+ "e": "🇨🇺",
+ "d": "cuba",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_cv",
+ "c": "flags",
+ "e": "🇨🇻",
+ "d": "cape verde",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_cw",
+ "c": "flags",
+ "e": "🇨🇼",
+ "d": "curaçao",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_cx",
+ "c": "flags",
+ "e": "🇨🇽",
+ "d": "christmas island",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_cy",
+ "c": "flags",
+ "e": "🇨🇾",
+ "d": "cyprus",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_cz",
+ "c": "flags",
+ "e": "🇨🇿",
+ "d": "the czech republic",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_de",
+ "c": "flags",
+ "e": "🇩🇪",
+ "d": "germany",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_dg",
+ "c": "flags",
+ "e": "🇩🇬",
+ "d": "diego garcia",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_dj",
+ "c": "flags",
+ "e": "🇩🇯",
+ "d": "djibouti",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_dk",
+ "c": "flags",
+ "e": "🇩🇰",
+ "d": "denmark",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_dm",
+ "c": "flags",
+ "e": "🇩🇲",
+ "d": "dominica",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_do",
+ "c": "flags",
+ "e": "🇩🇴",
+ "d": "the dominican republic",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_dz",
+ "c": "flags",
+ "e": "🇩🇿",
+ "d": "algeria",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_ea",
+ "c": "flags",
+ "e": "🇪🇦",
+ "d": "ceuta, melilla",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_ec",
+ "c": "flags",
+ "e": "🇪🇨",
+ "d": "ecuador",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_ee",
+ "c": "flags",
+ "e": "🇪🇪",
+ "d": "estonia",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_eg",
+ "c": "flags",
+ "e": "🇪🇬",
+ "d": "egypt",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_eh",
+ "c": "flags",
+ "e": "🇪🇭",
+ "d": "western sahara",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_er",
+ "c": "flags",
+ "e": "🇪🇷",
+ "d": "eritrea",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_es",
+ "c": "flags",
+ "e": "🇪🇸",
+ "d": "spain",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_et",
+ "c": "flags",
+ "e": "🇪🇹",
+ "d": "ethiopia",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_eu",
+ "c": "flags",
+ "e": "🇪🇺",
+ "d": "european union",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_fi",
+ "c": "flags",
+ "e": "🇫🇮",
+ "d": "finland",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_fj",
+ "c": "flags",
+ "e": "🇫🇯",
+ "d": "fiji",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_fk",
+ "c": "flags",
+ "e": "🇫🇰",
+ "d": "falkland islands",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_fm",
+ "c": "flags",
+ "e": "🇫🇲",
+ "d": "micronesia",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_fo",
+ "c": "flags",
+ "e": "🇫🇴",
+ "d": "faroe islands",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_fr",
+ "c": "flags",
+ "e": "🇫🇷",
+ "d": "france",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_ga",
+ "c": "flags",
+ "e": "🇬🇦",
+ "d": "gabon",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_gb",
+ "c": "flags",
+ "e": "🇬🇧",
+ "d": "great britain",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_gd",
+ "c": "flags",
+ "e": "🇬🇩",
+ "d": "grenada",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_ge",
+ "c": "flags",
+ "e": "🇬🇪",
+ "d": "georgia",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_gf",
+ "c": "flags",
+ "e": "🇬🇫",
+ "d": "french guiana",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_gg",
+ "c": "flags",
+ "e": "🇬🇬",
+ "d": "guernsey",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_gh",
+ "c": "flags",
+ "e": "🇬🇭",
+ "d": "ghana",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_gi",
+ "c": "flags",
+ "e": "🇬🇮",
+ "d": "gibraltar",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_gl",
+ "c": "flags",
+ "e": "🇬🇱",
+ "d": "greenland",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_gm",
+ "c": "flags",
+ "e": "🇬🇲",
+ "d": "the gambia",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_gn",
+ "c": "flags",
+ "e": "🇬🇳",
+ "d": "guinea",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_gp",
+ "c": "flags",
+ "e": "🇬🇵",
+ "d": "guadeloupe",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_gq",
+ "c": "flags",
+ "e": "🇬🇶",
+ "d": "equatorial guinea",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_gr",
+ "c": "flags",
+ "e": "🇬🇷",
+ "d": "greece",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_gs",
+ "c": "flags",
+ "e": "🇬🇸",
+ "d": "south georgia",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_gt",
+ "c": "flags",
+ "e": "🇬🇹",
+ "d": "guatemala",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_gu",
+ "c": "flags",
+ "e": "🇬🇺",
+ "d": "guam",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_gw",
+ "c": "flags",
+ "e": "🇬🇼",
+ "d": "guinea-bissau",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_gy",
+ "c": "flags",
+ "e": "🇬🇾",
+ "d": "guyana",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_hk",
+ "c": "flags",
+ "e": "🇭🇰",
+ "d": "hong kong",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_hm",
+ "c": "flags",
+ "e": "🇭🇲",
+ "d": "heard island and mcdonald islands",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_hn",
+ "c": "flags",
+ "e": "🇭🇳",
+ "d": "honduras",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_hr",
+ "c": "flags",
+ "e": "🇭🇷",
+ "d": "croatia",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_ht",
+ "c": "flags",
+ "e": "🇭🇹",
+ "d": "haiti",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_hu",
+ "c": "flags",
+ "e": "🇭🇺",
+ "d": "hungary",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_ic",
+ "c": "flags",
+ "e": "🇮🇨",
+ "d": "canary islands",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_id",
+ "c": "flags",
+ "e": "🇮🇩",
+ "d": "indonesia",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_ie",
+ "c": "flags",
+ "e": "🇮🇪",
+ "d": "ireland",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_il",
+ "c": "flags",
+ "e": "🇮🇱",
+ "d": "israel",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_im",
+ "c": "flags",
+ "e": "🇮🇲",
+ "d": "isle of man",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_in",
+ "c": "flags",
+ "e": "🇮🇳",
+ "d": "india",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_io",
+ "c": "flags",
+ "e": "🇮🇴",
+ "d": "british indian ocean territory",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_iq",
+ "c": "flags",
+ "e": "🇮🇶",
+ "d": "iraq",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_ir",
+ "c": "flags",
+ "e": "🇮🇷",
+ "d": "iran",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_is",
+ "c": "flags",
+ "e": "🇮🇸",
+ "d": "iceland",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_it",
+ "c": "flags",
+ "e": "🇮🇹",
+ "d": "italy",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_je",
+ "c": "flags",
+ "e": "🇯🇪",
+ "d": "jersey",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_jm",
+ "c": "flags",
+ "e": "🇯🇲",
+ "d": "jamaica",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_jo",
+ "c": "flags",
+ "e": "🇯🇴",
+ "d": "jordan",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_jp",
+ "c": "flags",
+ "e": "🇯🇵",
+ "d": "japan",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_ke",
+ "c": "flags",
+ "e": "🇰🇪",
+ "d": "kenya",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_kg",
+ "c": "flags",
+ "e": "🇰🇬",
+ "d": "kyrgyzstan",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_kh",
+ "c": "flags",
+ "e": "🇰🇭",
+ "d": "cambodia",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_ki",
+ "c": "flags",
+ "e": "🇰🇮",
+ "d": "kiribati",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_km",
+ "c": "flags",
+ "e": "🇰🇲",
+ "d": "the comoros",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_kn",
+ "c": "flags",
+ "e": "🇰🇳",
+ "d": "saint kitts and nevis",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_kp",
+ "c": "flags",
+ "e": "🇰🇵",
+ "d": "north korea",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_kr",
+ "c": "flags",
+ "e": "🇰🇷",
+ "d": "korea",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_kw",
+ "c": "flags",
+ "e": "🇰🇼",
+ "d": "kuwait",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_ky",
+ "c": "flags",
+ "e": "🇰🇾",
+ "d": "cayman islands",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_kz",
+ "c": "flags",
+ "e": "🇰🇿",
+ "d": "kazakhstan",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_la",
+ "c": "flags",
+ "e": "🇱🇦",
+ "d": "laos",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_lb",
+ "c": "flags",
+ "e": "🇱🇧",
+ "d": "lebanon",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_lc",
+ "c": "flags",
+ "e": "🇱🇨",
+ "d": "saint lucia",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_li",
+ "c": "flags",
+ "e": "🇱🇮",
+ "d": "liechtenstein",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_lk",
+ "c": "flags",
+ "e": "🇱🇰",
+ "d": "sri lanka",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_lr",
+ "c": "flags",
+ "e": "🇱🇷",
+ "d": "liberia",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_ls",
+ "c": "flags",
+ "e": "🇱🇸",
+ "d": "lesotho",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_lt",
+ "c": "flags",
+ "e": "🇱🇹",
+ "d": "lithuania",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_lu",
+ "c": "flags",
+ "e": "🇱🇺",
+ "d": "luxembourg",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_lv",
+ "c": "flags",
+ "e": "🇱🇻",
+ "d": "latvia",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_ly",
+ "c": "flags",
+ "e": "🇱🇾",
+ "d": "libya",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_ma",
+ "c": "flags",
+ "e": "🇲🇦",
+ "d": "morocco",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_mc",
+ "c": "flags",
+ "e": "🇲🇨",
+ "d": "monaco",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_md",
+ "c": "flags",
+ "e": "🇲🇩",
+ "d": "moldova",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_me",
+ "c": "flags",
+ "e": "🇲🇪",
+ "d": "montenegro",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_mf",
+ "c": "flags",
+ "e": "🇲🇫",
+ "d": "saint martin",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_mg",
+ "c": "flags",
+ "e": "🇲🇬",
+ "d": "madagascar",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_mh",
+ "c": "flags",
+ "e": "🇲🇭",
+ "d": "the marshall islands",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_mk",
+ "c": "flags",
+ "e": "🇲🇰",
+ "d": "macedonia",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_ml",
+ "c": "flags",
+ "e": "🇲🇱",
+ "d": "mali",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_mm",
+ "c": "flags",
+ "e": "🇲🇲",
+ "d": "myanmar",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_mn",
+ "c": "flags",
+ "e": "🇲🇳",
+ "d": "mongolia",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_mo",
+ "c": "flags",
+ "e": "🇲🇴",
+ "d": "macau",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_mp",
+ "c": "flags",
+ "e": "🇲🇵",
+ "d": "northern mariana islands",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_mq",
+ "c": "flags",
+ "e": "🇲🇶",
+ "d": "martinique",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_mr",
+ "c": "flags",
+ "e": "🇲🇷",
+ "d": "mauritania",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_ms",
+ "c": "flags",
+ "e": "🇲🇸",
+ "d": "montserrat",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_mt",
+ "c": "flags",
+ "e": "🇲🇹",
+ "d": "malta",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_mu",
+ "c": "flags",
+ "e": "🇲🇺",
+ "d": "mauritius",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_mv",
+ "c": "flags",
+ "e": "🇲🇻",
+ "d": "maldives",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_mw",
+ "c": "flags",
+ "e": "🇲🇼",
+ "d": "malawi",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_mx",
+ "c": "flags",
+ "e": "🇲🇽",
+ "d": "mexico",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_my",
+ "c": "flags",
+ "e": "🇲🇾",
+ "d": "malaysia",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_mz",
+ "c": "flags",
+ "e": "🇲🇿",
+ "d": "mozambique",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_na",
+ "c": "flags",
+ "e": "🇳🇦",
+ "d": "namibia",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_nc",
+ "c": "flags",
+ "e": "🇳🇨",
+ "d": "new caledonia",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_ne",
+ "c": "flags",
+ "e": "🇳🇪",
+ "d": "niger",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_nf",
+ "c": "flags",
+ "e": "🇳🇫",
+ "d": "norfolk island",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_ng",
+ "c": "flags",
+ "e": "🇳🇬",
+ "d": "nigeria",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_ni",
+ "c": "flags",
+ "e": "🇳🇮",
+ "d": "nicaragua",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_nl",
+ "c": "flags",
+ "e": "🇳🇱",
+ "d": "the netherlands",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_no",
+ "c": "flags",
+ "e": "🇳🇴",
+ "d": "norway",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_np",
+ "c": "flags",
+ "e": "🇳🇵",
+ "d": "nepal",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_nr",
+ "c": "flags",
+ "e": "🇳🇷",
+ "d": "nauru",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_nu",
+ "c": "flags",
+ "e": "🇳🇺",
+ "d": "niue",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_nz",
+ "c": "flags",
+ "e": "🇳🇿",
+ "d": "new zealand",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_om",
+ "c": "flags",
+ "e": "🇴🇲",
+ "d": "oman",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_pa",
+ "c": "flags",
+ "e": "🇵🇦",
+ "d": "panama",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_pe",
+ "c": "flags",
+ "e": "🇵🇪",
+ "d": "peru",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_pf",
+ "c": "flags",
+ "e": "🇵🇫",
+ "d": "french polynesia",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_pg",
+ "c": "flags",
+ "e": "🇵🇬",
+ "d": "papua new guinea",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_ph",
+ "c": "flags",
+ "e": "🇵🇭",
+ "d": "the philippines",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_pk",
+ "c": "flags",
+ "e": "🇵🇰",
+ "d": "pakistan",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_pl",
+ "c": "flags",
+ "e": "🇵🇱",
+ "d": "poland",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_pm",
+ "c": "flags",
+ "e": "🇵🇲",
+ "d": "saint pierre and miquelon",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_pn",
+ "c": "flags",
+ "e": "🇵🇳",
+ "d": "pitcairn",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_pr",
+ "c": "flags",
+ "e": "🇵🇷",
+ "d": "puerto rico",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_ps",
+ "c": "flags",
+ "e": "🇵🇸",
+ "d": "palestinian authority",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_pt",
+ "c": "flags",
+ "e": "🇵🇹",
+ "d": "portugal",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_pw",
+ "c": "flags",
+ "e": "🇵🇼",
+ "d": "palau",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_py",
+ "c": "flags",
+ "e": "🇵🇾",
+ "d": "paraguay",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_qa",
+ "c": "flags",
+ "e": "🇶🇦",
+ "d": "qatar",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_re",
+ "c": "flags",
+ "e": "🇷🇪",
+ "d": "réunion",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_ro",
+ "c": "flags",
+ "e": "🇷🇴",
+ "d": "romania",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_rs",
+ "c": "flags",
+ "e": "🇷🇸",
+ "d": "serbia",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_ru",
+ "c": "flags",
+ "e": "🇷🇺",
+ "d": "russia",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_rw",
+ "c": "flags",
+ "e": "🇷🇼",
+ "d": "rwanda",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_sa",
+ "c": "flags",
+ "e": "🇸🇦",
+ "d": "saudi arabia",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_sb",
+ "c": "flags",
+ "e": "🇸🇧",
+ "d": "the solomon islands",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_sc",
+ "c": "flags",
+ "e": "🇸🇨",
+ "d": "the seychelles",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_sd",
+ "c": "flags",
+ "e": "🇸🇩",
+ "d": "sudan",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_se",
+ "c": "flags",
+ "e": "🇸🇪",
+ "d": "sweden",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_sg",
+ "c": "flags",
+ "e": "🇸🇬",
+ "d": "singapore",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_sh",
+ "c": "flags",
+ "e": "🇸🇭",
+ "d": "saint helena",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_si",
+ "c": "flags",
+ "e": "🇸🇮",
+ "d": "slovenia",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_sj",
+ "c": "flags",
+ "e": "🇸🇯",
+ "d": "svalbard and jan mayen",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_sk",
+ "c": "flags",
+ "e": "🇸🇰",
+ "d": "slovakia",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_sl",
+ "c": "flags",
+ "e": "🇸🇱",
+ "d": "sierra leone",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_sm",
+ "c": "flags",
+ "e": "🇸🇲",
+ "d": "san marino",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_sn",
+ "c": "flags",
+ "e": "🇸🇳",
+ "d": "senegal",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_so",
+ "c": "flags",
+ "e": "🇸🇴",
+ "d": "somalia",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_sr",
+ "c": "flags",
+ "e": "🇸🇷",
+ "d": "suriname",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_ss",
+ "c": "flags",
+ "e": "🇸🇸",
+ "d": "south sudan",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_st",
+ "c": "flags",
+ "e": "🇸🇹",
+ "d": "sao tome and principe",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_sv",
+ "c": "flags",
+ "e": "🇸🇻",
+ "d": "el salvador",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_sx",
+ "c": "flags",
+ "e": "🇸🇽",
+ "d": "sint maarten",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_sy",
+ "c": "flags",
+ "e": "🇸🇾",
+ "d": "syria",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_sz",
+ "c": "flags",
+ "e": "🇸🇿",
+ "d": "swaziland",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_ta",
+ "c": "flags",
+ "e": "🇹🇦",
+ "d": "tristan da cunha",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_tc",
+ "c": "flags",
+ "e": "🇹🇨",
+ "d": "turks and caicos islands",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_td",
+ "c": "flags",
+ "e": "🇹🇩",
+ "d": "chad",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_tf",
+ "c": "flags",
+ "e": "🇹🇫",
+ "d": "french southern territories",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_tg",
+ "c": "flags",
+ "e": "🇹🇬",
+ "d": "togo",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_th",
+ "c": "flags",
+ "e": "🇹🇭",
+ "d": "thailand",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_tj",
+ "c": "flags",
+ "e": "🇹🇯",
+ "d": "tajikistan",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_tk",
+ "c": "flags",
+ "e": "🇹🇰",
+ "d": "tokelau",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_tl",
+ "c": "flags",
+ "e": "🇹🇱",
+ "d": "east timor",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_tm",
+ "c": "flags",
+ "e": "🇹🇲",
+ "d": "turkmenistan",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_tn",
+ "c": "flags",
+ "e": "🇹🇳",
+ "d": "tunisia",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_to",
+ "c": "flags",
+ "e": "🇹🇴",
+ "d": "tonga",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_tr",
+ "c": "flags",
+ "e": "🇹🇷",
+ "d": "turkey",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_tt",
+ "c": "flags",
+ "e": "🇹🇹",
+ "d": "trinidad and tobago",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_tv",
+ "c": "flags",
+ "e": "🇹🇻",
+ "d": "tuvalu",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_tw",
+ "c": "flags",
+ "e": "🇹🇼",
+ "d": "the republic of china",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_tz",
+ "c": "flags",
+ "e": "🇹🇿",
+ "d": "tanzania",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_ua",
+ "c": "flags",
+ "e": "🇺🇦",
+ "d": "ukraine",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_ug",
+ "c": "flags",
+ "e": "🇺🇬",
+ "d": "uganda",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_um",
+ "c": "flags",
+ "e": "🇺🇲",
+ "d": "united states minor outlying islands",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_us",
+ "c": "flags",
+ "e": "🇺🇸",
+ "d": "united states",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_uy",
+ "c": "flags",
+ "e": "🇺🇾",
+ "d": "uruguay",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_uz",
+ "c": "flags",
+ "e": "🇺🇿",
+ "d": "uzbekistan",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_va",
+ "c": "flags",
+ "e": "🇻🇦",
+ "d": "the vatican city",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_vc",
+ "c": "flags",
+ "e": "🇻🇨",
+ "d": "saint vincent and the grenadines",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_ve",
+ "c": "flags",
+ "e": "🇻🇪",
+ "d": "venezuela",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_vg",
+ "c": "flags",
+ "e": "🇻🇬",
+ "d": "british virgin islands",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_vi",
+ "c": "flags",
+ "e": "🇻🇮",
+ "d": "u.s. virgin islands",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_vn",
+ "c": "flags",
+ "e": "🇻🇳",
+ "d": "vietnam",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_vu",
+ "c": "flags",
+ "e": "🇻🇺",
+ "d": "vanuatu",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_wf",
+ "c": "flags",
+ "e": "🇼🇫",
+ "d": "wallis and futuna",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_ws",
+ "c": "flags",
+ "e": "🇼🇸",
+ "d": "samoa",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_xk",
+ "c": "flags",
+ "e": "🇽🇰",
+ "d": "kosovo",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_ye",
+ "c": "flags",
+ "e": "🇾🇪",
+ "d": "yemen",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_yt",
+ "c": "flags",
+ "e": "🇾🇹",
+ "d": "mayotte",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_za",
+ "c": "flags",
+ "e": "🇿🇦",
+ "d": "south africa",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_zm",
+ "c": "flags",
+ "e": "🇿🇲",
+ "d": "zambia",
+ "u": "6.0"
+ },
+ {
+ "n": "flag_zw",
+ "c": "flags",
+ "e": "🇿🇼",
+ "d": "zimbabwe",
+ "u": "6.0"
+ }
+]
diff --git a/public/-/emojis/3/end.png b/public/-/emojis/3/end.png
new file mode 100644
index 00000000000..17c319dc03d
--- /dev/null
+++ b/public/-/emojis/3/end.png
Binary files differ
diff --git a/public/-/emojis/3/envelope.png b/public/-/emojis/3/envelope.png
new file mode 100644
index 00000000000..aed381de949
--- /dev/null
+++ b/public/-/emojis/3/envelope.png
Binary files differ
diff --git a/public/-/emojis/3/envelope_with_arrow.png b/public/-/emojis/3/envelope_with_arrow.png
new file mode 100644
index 00000000000..54b89817587
--- /dev/null
+++ b/public/-/emojis/3/envelope_with_arrow.png
Binary files differ
diff --git a/public/-/emojis/3/euro.png b/public/-/emojis/3/euro.png
new file mode 100644
index 00000000000..2094ed7692f
--- /dev/null
+++ b/public/-/emojis/3/euro.png
Binary files differ
diff --git a/public/-/emojis/3/european_castle.png b/public/-/emojis/3/european_castle.png
new file mode 100644
index 00000000000..5da56216c1e
--- /dev/null
+++ b/public/-/emojis/3/european_castle.png
Binary files differ
diff --git a/public/-/emojis/3/european_post_office.png b/public/-/emojis/3/european_post_office.png
new file mode 100644
index 00000000000..05537ad7702
--- /dev/null
+++ b/public/-/emojis/3/european_post_office.png
Binary files differ
diff --git a/public/-/emojis/3/evergreen_tree.png b/public/-/emojis/3/evergreen_tree.png
new file mode 100644
index 00000000000..1004726d8a8
--- /dev/null
+++ b/public/-/emojis/3/evergreen_tree.png
Binary files differ
diff --git a/public/-/emojis/3/exclamation.png b/public/-/emojis/3/exclamation.png
new file mode 100644
index 00000000000..b5685afaf63
--- /dev/null
+++ b/public/-/emojis/3/exclamation.png
Binary files differ
diff --git a/public/-/emojis/3/expressionless.png b/public/-/emojis/3/expressionless.png
new file mode 100644
index 00000000000..9343ff25768
--- /dev/null
+++ b/public/-/emojis/3/expressionless.png
Binary files differ
diff --git a/public/-/emojis/3/eye.png b/public/-/emojis/3/eye.png
new file mode 100644
index 00000000000..3ed81334987
--- /dev/null
+++ b/public/-/emojis/3/eye.png
Binary files differ
diff --git a/public/-/emojis/3/eye_in_speech_bubble.png b/public/-/emojis/3/eye_in_speech_bubble.png
new file mode 100644
index 00000000000..17517147011
--- /dev/null
+++ b/public/-/emojis/3/eye_in_speech_bubble.png
Binary files differ
diff --git a/public/-/emojis/3/eyeglasses.png b/public/-/emojis/3/eyeglasses.png
new file mode 100644
index 00000000000..bb40227139e
--- /dev/null
+++ b/public/-/emojis/3/eyeglasses.png
Binary files differ
diff --git a/public/-/emojis/3/eyes.png b/public/-/emojis/3/eyes.png
new file mode 100644
index 00000000000..61359d39629
--- /dev/null
+++ b/public/-/emojis/3/eyes.png
Binary files differ
diff --git a/public/-/emojis/3/face_palm.png b/public/-/emojis/3/face_palm.png
new file mode 100644
index 00000000000..002c60b3622
--- /dev/null
+++ b/public/-/emojis/3/face_palm.png
Binary files differ
diff --git a/public/-/emojis/3/face_palm_tone1.png b/public/-/emojis/3/face_palm_tone1.png
new file mode 100644
index 00000000000..fb126e9e89f
--- /dev/null
+++ b/public/-/emojis/3/face_palm_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/face_palm_tone2.png b/public/-/emojis/3/face_palm_tone2.png
new file mode 100644
index 00000000000..84a0fba7993
--- /dev/null
+++ b/public/-/emojis/3/face_palm_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/face_palm_tone3.png b/public/-/emojis/3/face_palm_tone3.png
new file mode 100644
index 00000000000..fe5fe356641
--- /dev/null
+++ b/public/-/emojis/3/face_palm_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/face_palm_tone4.png b/public/-/emojis/3/face_palm_tone4.png
new file mode 100644
index 00000000000..6e9e22338cf
--- /dev/null
+++ b/public/-/emojis/3/face_palm_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/face_palm_tone5.png b/public/-/emojis/3/face_palm_tone5.png
new file mode 100644
index 00000000000..ddac1343cb2
--- /dev/null
+++ b/public/-/emojis/3/face_palm_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/factory.png b/public/-/emojis/3/factory.png
new file mode 100644
index 00000000000..cabe180b278
--- /dev/null
+++ b/public/-/emojis/3/factory.png
Binary files differ
diff --git a/public/-/emojis/3/fallen_leaf.png b/public/-/emojis/3/fallen_leaf.png
new file mode 100644
index 00000000000..931532c6603
--- /dev/null
+++ b/public/-/emojis/3/fallen_leaf.png
Binary files differ
diff --git a/public/-/emojis/3/family.png b/public/-/emojis/3/family.png
new file mode 100644
index 00000000000..4b0ba60c626
--- /dev/null
+++ b/public/-/emojis/3/family.png
Binary files differ
diff --git a/public/-/emojis/3/family_mmb.png b/public/-/emojis/3/family_mmb.png
new file mode 100644
index 00000000000..98d7c8ca41c
--- /dev/null
+++ b/public/-/emojis/3/family_mmb.png
Binary files differ
diff --git a/public/-/emojis/3/family_mmbb.png b/public/-/emojis/3/family_mmbb.png
new file mode 100644
index 00000000000..bc7f57d9ff1
--- /dev/null
+++ b/public/-/emojis/3/family_mmbb.png
Binary files differ
diff --git a/public/-/emojis/3/family_mmg.png b/public/-/emojis/3/family_mmg.png
new file mode 100644
index 00000000000..7c810606d79
--- /dev/null
+++ b/public/-/emojis/3/family_mmg.png
Binary files differ
diff --git a/public/-/emojis/3/family_mmgb.png b/public/-/emojis/3/family_mmgb.png
new file mode 100644
index 00000000000..7b41b66f57e
--- /dev/null
+++ b/public/-/emojis/3/family_mmgb.png
Binary files differ
diff --git a/public/-/emojis/3/family_mmgg.png b/public/-/emojis/3/family_mmgg.png
new file mode 100644
index 00000000000..ce89a085800
--- /dev/null
+++ b/public/-/emojis/3/family_mmgg.png
Binary files differ
diff --git a/public/-/emojis/3/family_mwbb.png b/public/-/emojis/3/family_mwbb.png
new file mode 100644
index 00000000000..b8679e572f2
--- /dev/null
+++ b/public/-/emojis/3/family_mwbb.png
Binary files differ
diff --git a/public/-/emojis/3/family_mwg.png b/public/-/emojis/3/family_mwg.png
new file mode 100644
index 00000000000..affc7206f23
--- /dev/null
+++ b/public/-/emojis/3/family_mwg.png
Binary files differ
diff --git a/public/-/emojis/3/family_mwgb.png b/public/-/emojis/3/family_mwgb.png
new file mode 100644
index 00000000000..5951cf11be3
--- /dev/null
+++ b/public/-/emojis/3/family_mwgb.png
Binary files differ
diff --git a/public/-/emojis/3/family_mwgg.png b/public/-/emojis/3/family_mwgg.png
new file mode 100644
index 00000000000..7aaec104e31
--- /dev/null
+++ b/public/-/emojis/3/family_mwgg.png
Binary files differ
diff --git a/public/-/emojis/3/family_wwb.png b/public/-/emojis/3/family_wwb.png
new file mode 100644
index 00000000000..fb40fefe84a
--- /dev/null
+++ b/public/-/emojis/3/family_wwb.png
Binary files differ
diff --git a/public/-/emojis/3/family_wwbb.png b/public/-/emojis/3/family_wwbb.png
new file mode 100644
index 00000000000..1b6f6ae2434
--- /dev/null
+++ b/public/-/emojis/3/family_wwbb.png
Binary files differ
diff --git a/public/-/emojis/3/family_wwg.png b/public/-/emojis/3/family_wwg.png
new file mode 100644
index 00000000000..6e17b1e8ed6
--- /dev/null
+++ b/public/-/emojis/3/family_wwg.png
Binary files differ
diff --git a/public/-/emojis/3/family_wwgb.png b/public/-/emojis/3/family_wwgb.png
new file mode 100644
index 00000000000..23eeaad269d
--- /dev/null
+++ b/public/-/emojis/3/family_wwgb.png
Binary files differ
diff --git a/public/-/emojis/3/family_wwgg.png b/public/-/emojis/3/family_wwgg.png
new file mode 100644
index 00000000000..c2c19736a96
--- /dev/null
+++ b/public/-/emojis/3/family_wwgg.png
Binary files differ
diff --git a/public/-/emojis/3/fast_forward.png b/public/-/emojis/3/fast_forward.png
new file mode 100644
index 00000000000..307199414d4
--- /dev/null
+++ b/public/-/emojis/3/fast_forward.png
Binary files differ
diff --git a/public/-/emojis/3/fax.png b/public/-/emojis/3/fax.png
new file mode 100644
index 00000000000..ff4ab54244e
--- /dev/null
+++ b/public/-/emojis/3/fax.png
Binary files differ
diff --git a/public/-/emojis/3/fearful.png b/public/-/emojis/3/fearful.png
new file mode 100644
index 00000000000..c6dc3d8f19a
--- /dev/null
+++ b/public/-/emojis/3/fearful.png
Binary files differ
diff --git a/public/-/emojis/3/feet.png b/public/-/emojis/3/feet.png
new file mode 100644
index 00000000000..622a3f4d7f5
--- /dev/null
+++ b/public/-/emojis/3/feet.png
Binary files differ
diff --git a/public/-/emojis/3/fencer.png b/public/-/emojis/3/fencer.png
new file mode 100644
index 00000000000..39ee19e1e3c
--- /dev/null
+++ b/public/-/emojis/3/fencer.png
Binary files differ
diff --git a/public/-/emojis/3/ferris_wheel.png b/public/-/emojis/3/ferris_wheel.png
new file mode 100644
index 00000000000..a2f6c2d9415
--- /dev/null
+++ b/public/-/emojis/3/ferris_wheel.png
Binary files differ
diff --git a/public/-/emojis/3/ferry.png b/public/-/emojis/3/ferry.png
new file mode 100644
index 00000000000..60e738626ea
--- /dev/null
+++ b/public/-/emojis/3/ferry.png
Binary files differ
diff --git a/public/-/emojis/3/field_hockey.png b/public/-/emojis/3/field_hockey.png
new file mode 100644
index 00000000000..758e09349a2
--- /dev/null
+++ b/public/-/emojis/3/field_hockey.png
Binary files differ
diff --git a/public/-/emojis/3/file_cabinet.png b/public/-/emojis/3/file_cabinet.png
new file mode 100644
index 00000000000..88531539f2e
--- /dev/null
+++ b/public/-/emojis/3/file_cabinet.png
Binary files differ
diff --git a/public/-/emojis/3/file_folder.png b/public/-/emojis/3/file_folder.png
new file mode 100644
index 00000000000..47a14a2836a
--- /dev/null
+++ b/public/-/emojis/3/file_folder.png
Binary files differ
diff --git a/public/-/emojis/3/film_frames.png b/public/-/emojis/3/film_frames.png
new file mode 100644
index 00000000000..528cf55a933
--- /dev/null
+++ b/public/-/emojis/3/film_frames.png
Binary files differ
diff --git a/public/-/emojis/3/fingers_crossed.png b/public/-/emojis/3/fingers_crossed.png
new file mode 100644
index 00000000000..2c065dcd557
--- /dev/null
+++ b/public/-/emojis/3/fingers_crossed.png
Binary files differ
diff --git a/public/-/emojis/3/fingers_crossed_tone1.png b/public/-/emojis/3/fingers_crossed_tone1.png
new file mode 100644
index 00000000000..c3864ac868f
--- /dev/null
+++ b/public/-/emojis/3/fingers_crossed_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/fingers_crossed_tone2.png b/public/-/emojis/3/fingers_crossed_tone2.png
new file mode 100644
index 00000000000..82f3b9ce4fe
--- /dev/null
+++ b/public/-/emojis/3/fingers_crossed_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/fingers_crossed_tone3.png b/public/-/emojis/3/fingers_crossed_tone3.png
new file mode 100644
index 00000000000..199fe0cf68d
--- /dev/null
+++ b/public/-/emojis/3/fingers_crossed_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/fingers_crossed_tone4.png b/public/-/emojis/3/fingers_crossed_tone4.png
new file mode 100644
index 00000000000..12d6b8d2963
--- /dev/null
+++ b/public/-/emojis/3/fingers_crossed_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/fingers_crossed_tone5.png b/public/-/emojis/3/fingers_crossed_tone5.png
new file mode 100644
index 00000000000..9a62704dd92
--- /dev/null
+++ b/public/-/emojis/3/fingers_crossed_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/fire.png b/public/-/emojis/3/fire.png
new file mode 100644
index 00000000000..83f34701807
--- /dev/null
+++ b/public/-/emojis/3/fire.png
Binary files differ
diff --git a/public/-/emojis/3/fire_engine.png b/public/-/emojis/3/fire_engine.png
new file mode 100644
index 00000000000..271141fd1b9
--- /dev/null
+++ b/public/-/emojis/3/fire_engine.png
Binary files differ
diff --git a/public/-/emojis/3/fireworks.png b/public/-/emojis/3/fireworks.png
new file mode 100644
index 00000000000..dcfff099ad3
--- /dev/null
+++ b/public/-/emojis/3/fireworks.png
Binary files differ
diff --git a/public/-/emojis/3/first_place.png b/public/-/emojis/3/first_place.png
new file mode 100644
index 00000000000..bf22f19b833
--- /dev/null
+++ b/public/-/emojis/3/first_place.png
Binary files differ
diff --git a/public/-/emojis/3/first_quarter_moon.png b/public/-/emojis/3/first_quarter_moon.png
new file mode 100644
index 00000000000..aeb729f4ce1
--- /dev/null
+++ b/public/-/emojis/3/first_quarter_moon.png
Binary files differ
diff --git a/public/-/emojis/3/first_quarter_moon_with_face.png b/public/-/emojis/3/first_quarter_moon_with_face.png
new file mode 100644
index 00000000000..8042d82544a
--- /dev/null
+++ b/public/-/emojis/3/first_quarter_moon_with_face.png
Binary files differ
diff --git a/public/-/emojis/3/fish.png b/public/-/emojis/3/fish.png
new file mode 100644
index 00000000000..945e72ce9f6
--- /dev/null
+++ b/public/-/emojis/3/fish.png
Binary files differ
diff --git a/public/-/emojis/3/fish_cake.png b/public/-/emojis/3/fish_cake.png
new file mode 100644
index 00000000000..31bb39b908b
--- /dev/null
+++ b/public/-/emojis/3/fish_cake.png
Binary files differ
diff --git a/public/-/emojis/3/fishing_pole_and_fish.png b/public/-/emojis/3/fishing_pole_and_fish.png
new file mode 100644
index 00000000000..5f0fd69a69c
--- /dev/null
+++ b/public/-/emojis/3/fishing_pole_and_fish.png
Binary files differ
diff --git a/public/-/emojis/3/fist.png b/public/-/emojis/3/fist.png
new file mode 100644
index 00000000000..0750351373d
--- /dev/null
+++ b/public/-/emojis/3/fist.png
Binary files differ
diff --git a/public/-/emojis/3/fist_tone1.png b/public/-/emojis/3/fist_tone1.png
new file mode 100644
index 00000000000..bdd0e44d659
--- /dev/null
+++ b/public/-/emojis/3/fist_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/fist_tone2.png b/public/-/emojis/3/fist_tone2.png
new file mode 100644
index 00000000000..7189dba83f2
--- /dev/null
+++ b/public/-/emojis/3/fist_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/fist_tone3.png b/public/-/emojis/3/fist_tone3.png
new file mode 100644
index 00000000000..e0d6ec2ffc3
--- /dev/null
+++ b/public/-/emojis/3/fist_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/fist_tone4.png b/public/-/emojis/3/fist_tone4.png
new file mode 100644
index 00000000000..3d2c974bc00
--- /dev/null
+++ b/public/-/emojis/3/fist_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/fist_tone5.png b/public/-/emojis/3/fist_tone5.png
new file mode 100644
index 00000000000..84d1f68bf4d
--- /dev/null
+++ b/public/-/emojis/3/fist_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/five.png b/public/-/emojis/3/five.png
new file mode 100644
index 00000000000..e7e387b889e
--- /dev/null
+++ b/public/-/emojis/3/five.png
Binary files differ
diff --git a/public/-/emojis/3/flag_ac.png b/public/-/emojis/3/flag_ac.png
new file mode 100644
index 00000000000..e7e7bcaad81
--- /dev/null
+++ b/public/-/emojis/3/flag_ac.png
Binary files differ
diff --git a/public/-/emojis/3/flag_ad.png b/public/-/emojis/3/flag_ad.png
new file mode 100644
index 00000000000..105439cc07e
--- /dev/null
+++ b/public/-/emojis/3/flag_ad.png
Binary files differ
diff --git a/public/-/emojis/3/flag_ae.png b/public/-/emojis/3/flag_ae.png
new file mode 100644
index 00000000000..33b21809a0c
--- /dev/null
+++ b/public/-/emojis/3/flag_ae.png
Binary files differ
diff --git a/public/-/emojis/3/flag_af.png b/public/-/emojis/3/flag_af.png
new file mode 100644
index 00000000000..ab0fe0e4f74
--- /dev/null
+++ b/public/-/emojis/3/flag_af.png
Binary files differ
diff --git a/public/-/emojis/3/flag_ag.png b/public/-/emojis/3/flag_ag.png
new file mode 100644
index 00000000000..2c73fdf1525
--- /dev/null
+++ b/public/-/emojis/3/flag_ag.png
Binary files differ
diff --git a/public/-/emojis/3/flag_ai.png b/public/-/emojis/3/flag_ai.png
new file mode 100644
index 00000000000..065ab03e0ce
--- /dev/null
+++ b/public/-/emojis/3/flag_ai.png
Binary files differ
diff --git a/public/-/emojis/3/flag_al.png b/public/-/emojis/3/flag_al.png
new file mode 100644
index 00000000000..dc0a90eef32
--- /dev/null
+++ b/public/-/emojis/3/flag_al.png
Binary files differ
diff --git a/public/-/emojis/3/flag_am.png b/public/-/emojis/3/flag_am.png
new file mode 100644
index 00000000000..065d25b7153
--- /dev/null
+++ b/public/-/emojis/3/flag_am.png
Binary files differ
diff --git a/public/-/emojis/3/flag_ao.png b/public/-/emojis/3/flag_ao.png
new file mode 100644
index 00000000000..dd957f9ef1d
--- /dev/null
+++ b/public/-/emojis/3/flag_ao.png
Binary files differ
diff --git a/public/-/emojis/3/flag_aq.png b/public/-/emojis/3/flag_aq.png
new file mode 100644
index 00000000000..930e8e3a726
--- /dev/null
+++ b/public/-/emojis/3/flag_aq.png
Binary files differ
diff --git a/public/-/emojis/3/flag_ar.png b/public/-/emojis/3/flag_ar.png
new file mode 100644
index 00000000000..db8209c85c2
--- /dev/null
+++ b/public/-/emojis/3/flag_ar.png
Binary files differ
diff --git a/public/-/emojis/3/flag_as.png b/public/-/emojis/3/flag_as.png
new file mode 100644
index 00000000000..11213f5704a
--- /dev/null
+++ b/public/-/emojis/3/flag_as.png
Binary files differ
diff --git a/public/-/emojis/3/flag_at.png b/public/-/emojis/3/flag_at.png
new file mode 100644
index 00000000000..b33df6aa245
--- /dev/null
+++ b/public/-/emojis/3/flag_at.png
Binary files differ
diff --git a/public/-/emojis/3/flag_au.png b/public/-/emojis/3/flag_au.png
new file mode 100644
index 00000000000..411488897f8
--- /dev/null
+++ b/public/-/emojis/3/flag_au.png
Binary files differ
diff --git a/public/-/emojis/3/flag_aw.png b/public/-/emojis/3/flag_aw.png
new file mode 100644
index 00000000000..3973babb781
--- /dev/null
+++ b/public/-/emojis/3/flag_aw.png
Binary files differ
diff --git a/public/-/emojis/3/flag_ax.png b/public/-/emojis/3/flag_ax.png
new file mode 100644
index 00000000000..11920409024
--- /dev/null
+++ b/public/-/emojis/3/flag_ax.png
Binary files differ
diff --git a/public/-/emojis/3/flag_az.png b/public/-/emojis/3/flag_az.png
new file mode 100644
index 00000000000..e59359684bc
--- /dev/null
+++ b/public/-/emojis/3/flag_az.png
Binary files differ
diff --git a/public/-/emojis/3/flag_ba.png b/public/-/emojis/3/flag_ba.png
new file mode 100644
index 00000000000..d810071c62f
--- /dev/null
+++ b/public/-/emojis/3/flag_ba.png
Binary files differ
diff --git a/public/-/emojis/3/flag_bb.png b/public/-/emojis/3/flag_bb.png
new file mode 100644
index 00000000000..0f8881e39b7
--- /dev/null
+++ b/public/-/emojis/3/flag_bb.png
Binary files differ
diff --git a/public/-/emojis/3/flag_bd.png b/public/-/emojis/3/flag_bd.png
new file mode 100644
index 00000000000..17bcfe491ec
--- /dev/null
+++ b/public/-/emojis/3/flag_bd.png
Binary files differ
diff --git a/public/-/emojis/3/flag_be.png b/public/-/emojis/3/flag_be.png
new file mode 100644
index 00000000000..105dd2bfba7
--- /dev/null
+++ b/public/-/emojis/3/flag_be.png
Binary files differ
diff --git a/public/-/emojis/3/flag_bf.png b/public/-/emojis/3/flag_bf.png
new file mode 100644
index 00000000000..ea075a3b880
--- /dev/null
+++ b/public/-/emojis/3/flag_bf.png
Binary files differ
diff --git a/public/-/emojis/3/flag_bg.png b/public/-/emojis/3/flag_bg.png
new file mode 100644
index 00000000000..9210245917c
--- /dev/null
+++ b/public/-/emojis/3/flag_bg.png
Binary files differ
diff --git a/public/-/emojis/3/flag_bh.png b/public/-/emojis/3/flag_bh.png
new file mode 100644
index 00000000000..2ed0ea1c7a0
--- /dev/null
+++ b/public/-/emojis/3/flag_bh.png
Binary files differ
diff --git a/public/-/emojis/3/flag_bi.png b/public/-/emojis/3/flag_bi.png
new file mode 100644
index 00000000000..98839afcf3a
--- /dev/null
+++ b/public/-/emojis/3/flag_bi.png
Binary files differ
diff --git a/public/-/emojis/3/flag_bj.png b/public/-/emojis/3/flag_bj.png
new file mode 100644
index 00000000000..dffe78e8e0e
--- /dev/null
+++ b/public/-/emojis/3/flag_bj.png
Binary files differ
diff --git a/public/-/emojis/3/flag_bl.png b/public/-/emojis/3/flag_bl.png
new file mode 100644
index 00000000000..267a479dc37
--- /dev/null
+++ b/public/-/emojis/3/flag_bl.png
Binary files differ
diff --git a/public/-/emojis/3/flag_black.png b/public/-/emojis/3/flag_black.png
new file mode 100644
index 00000000000..60c40762a30
--- /dev/null
+++ b/public/-/emojis/3/flag_black.png
Binary files differ
diff --git a/public/-/emojis/3/flag_bm.png b/public/-/emojis/3/flag_bm.png
new file mode 100644
index 00000000000..6c1c56313a2
--- /dev/null
+++ b/public/-/emojis/3/flag_bm.png
Binary files differ
diff --git a/public/-/emojis/3/flag_bn.png b/public/-/emojis/3/flag_bn.png
new file mode 100644
index 00000000000..8e698042cd4
--- /dev/null
+++ b/public/-/emojis/3/flag_bn.png
Binary files differ
diff --git a/public/-/emojis/3/flag_bo.png b/public/-/emojis/3/flag_bo.png
new file mode 100644
index 00000000000..45714cc5f96
--- /dev/null
+++ b/public/-/emojis/3/flag_bo.png
Binary files differ
diff --git a/public/-/emojis/3/flag_bq.png b/public/-/emojis/3/flag_bq.png
new file mode 100644
index 00000000000..52bf03c5017
--- /dev/null
+++ b/public/-/emojis/3/flag_bq.png
Binary files differ
diff --git a/public/-/emojis/3/flag_br.png b/public/-/emojis/3/flag_br.png
new file mode 100644
index 00000000000..1d32b466587
--- /dev/null
+++ b/public/-/emojis/3/flag_br.png
Binary files differ
diff --git a/public/-/emojis/3/flag_bs.png b/public/-/emojis/3/flag_bs.png
new file mode 100644
index 00000000000..3d7cadc9506
--- /dev/null
+++ b/public/-/emojis/3/flag_bs.png
Binary files differ
diff --git a/public/-/emojis/3/flag_bt.png b/public/-/emojis/3/flag_bt.png
new file mode 100644
index 00000000000..bb7219420d3
--- /dev/null
+++ b/public/-/emojis/3/flag_bt.png
Binary files differ
diff --git a/public/-/emojis/3/flag_bv.png b/public/-/emojis/3/flag_bv.png
new file mode 100644
index 00000000000..d94e9a72179
--- /dev/null
+++ b/public/-/emojis/3/flag_bv.png
Binary files differ
diff --git a/public/-/emojis/3/flag_bw.png b/public/-/emojis/3/flag_bw.png
new file mode 100644
index 00000000000..9cbeaa67eea
--- /dev/null
+++ b/public/-/emojis/3/flag_bw.png
Binary files differ
diff --git a/public/-/emojis/3/flag_by.png b/public/-/emojis/3/flag_by.png
new file mode 100644
index 00000000000..8470eaf9b1e
--- /dev/null
+++ b/public/-/emojis/3/flag_by.png
Binary files differ
diff --git a/public/-/emojis/3/flag_bz.png b/public/-/emojis/3/flag_bz.png
new file mode 100644
index 00000000000..2e26a0d6ef8
--- /dev/null
+++ b/public/-/emojis/3/flag_bz.png
Binary files differ
diff --git a/public/-/emojis/3/flag_ca.png b/public/-/emojis/3/flag_ca.png
new file mode 100644
index 00000000000..044edd36311
--- /dev/null
+++ b/public/-/emojis/3/flag_ca.png
Binary files differ
diff --git a/public/-/emojis/3/flag_cc.png b/public/-/emojis/3/flag_cc.png
new file mode 100644
index 00000000000..54e4972ec1d
--- /dev/null
+++ b/public/-/emojis/3/flag_cc.png
Binary files differ
diff --git a/public/-/emojis/3/flag_cd.png b/public/-/emojis/3/flag_cd.png
new file mode 100644
index 00000000000..53fbf77e757
--- /dev/null
+++ b/public/-/emojis/3/flag_cd.png
Binary files differ
diff --git a/public/-/emojis/3/flag_cf.png b/public/-/emojis/3/flag_cf.png
new file mode 100644
index 00000000000..d1db72b656e
--- /dev/null
+++ b/public/-/emojis/3/flag_cf.png
Binary files differ
diff --git a/public/-/emojis/3/flag_cg.png b/public/-/emojis/3/flag_cg.png
new file mode 100644
index 00000000000..c1145ed54b5
--- /dev/null
+++ b/public/-/emojis/3/flag_cg.png
Binary files differ
diff --git a/public/-/emojis/3/flag_ch.png b/public/-/emojis/3/flag_ch.png
new file mode 100644
index 00000000000..700c7fde14d
--- /dev/null
+++ b/public/-/emojis/3/flag_ch.png
Binary files differ
diff --git a/public/-/emojis/3/flag_ci.png b/public/-/emojis/3/flag_ci.png
new file mode 100644
index 00000000000..6629b379c97
--- /dev/null
+++ b/public/-/emojis/3/flag_ci.png
Binary files differ
diff --git a/public/-/emojis/3/flag_ck.png b/public/-/emojis/3/flag_ck.png
new file mode 100644
index 00000000000..11be975c5b0
--- /dev/null
+++ b/public/-/emojis/3/flag_ck.png
Binary files differ
diff --git a/public/-/emojis/3/flag_cl.png b/public/-/emojis/3/flag_cl.png
new file mode 100644
index 00000000000..3c23b255425
--- /dev/null
+++ b/public/-/emojis/3/flag_cl.png
Binary files differ
diff --git a/public/-/emojis/3/flag_cm.png b/public/-/emojis/3/flag_cm.png
new file mode 100644
index 00000000000..6a0bd3cea0c
--- /dev/null
+++ b/public/-/emojis/3/flag_cm.png
Binary files differ
diff --git a/public/-/emojis/3/flag_cn.png b/public/-/emojis/3/flag_cn.png
new file mode 100644
index 00000000000..327aa7dd7bf
--- /dev/null
+++ b/public/-/emojis/3/flag_cn.png
Binary files differ
diff --git a/public/-/emojis/3/flag_co.png b/public/-/emojis/3/flag_co.png
new file mode 100644
index 00000000000..3d8cd9e41f2
--- /dev/null
+++ b/public/-/emojis/3/flag_co.png
Binary files differ
diff --git a/public/-/emojis/3/flag_cp.png b/public/-/emojis/3/flag_cp.png
new file mode 100644
index 00000000000..98039a24e6b
--- /dev/null
+++ b/public/-/emojis/3/flag_cp.png
Binary files differ
diff --git a/public/-/emojis/3/flag_cr.png b/public/-/emojis/3/flag_cr.png
new file mode 100644
index 00000000000..78941a89143
--- /dev/null
+++ b/public/-/emojis/3/flag_cr.png
Binary files differ
diff --git a/public/-/emojis/3/flag_cu.png b/public/-/emojis/3/flag_cu.png
new file mode 100644
index 00000000000..67faa51a5d2
--- /dev/null
+++ b/public/-/emojis/3/flag_cu.png
Binary files differ
diff --git a/public/-/emojis/3/flag_cv.png b/public/-/emojis/3/flag_cv.png
new file mode 100644
index 00000000000..6f481eae77e
--- /dev/null
+++ b/public/-/emojis/3/flag_cv.png
Binary files differ
diff --git a/public/-/emojis/3/flag_cw.png b/public/-/emojis/3/flag_cw.png
new file mode 100644
index 00000000000..afb10f14b60
--- /dev/null
+++ b/public/-/emojis/3/flag_cw.png
Binary files differ
diff --git a/public/-/emojis/3/flag_cx.png b/public/-/emojis/3/flag_cx.png
new file mode 100644
index 00000000000..4fec7f6220f
--- /dev/null
+++ b/public/-/emojis/3/flag_cx.png
Binary files differ
diff --git a/public/-/emojis/3/flag_cy.png b/public/-/emojis/3/flag_cy.png
new file mode 100644
index 00000000000..6d14cf405ff
--- /dev/null
+++ b/public/-/emojis/3/flag_cy.png
Binary files differ
diff --git a/public/-/emojis/3/flag_cz.png b/public/-/emojis/3/flag_cz.png
new file mode 100644
index 00000000000..0182b611410
--- /dev/null
+++ b/public/-/emojis/3/flag_cz.png
Binary files differ
diff --git a/public/-/emojis/3/flag_de.png b/public/-/emojis/3/flag_de.png
new file mode 100644
index 00000000000..3298cfccacd
--- /dev/null
+++ b/public/-/emojis/3/flag_de.png
Binary files differ
diff --git a/public/-/emojis/3/flag_dg.png b/public/-/emojis/3/flag_dg.png
new file mode 100644
index 00000000000..520adc653cc
--- /dev/null
+++ b/public/-/emojis/3/flag_dg.png
Binary files differ
diff --git a/public/-/emojis/3/flag_dj.png b/public/-/emojis/3/flag_dj.png
new file mode 100644
index 00000000000..eef897f1734
--- /dev/null
+++ b/public/-/emojis/3/flag_dj.png
Binary files differ
diff --git a/public/-/emojis/3/flag_dk.png b/public/-/emojis/3/flag_dk.png
new file mode 100644
index 00000000000..120a8fd7138
--- /dev/null
+++ b/public/-/emojis/3/flag_dk.png
Binary files differ
diff --git a/public/-/emojis/3/flag_dm.png b/public/-/emojis/3/flag_dm.png
new file mode 100644
index 00000000000..5c5999be098
--- /dev/null
+++ b/public/-/emojis/3/flag_dm.png
Binary files differ
diff --git a/public/-/emojis/3/flag_do.png b/public/-/emojis/3/flag_do.png
new file mode 100644
index 00000000000..d59ec7e54ef
--- /dev/null
+++ b/public/-/emojis/3/flag_do.png
Binary files differ
diff --git a/public/-/emojis/3/flag_dz.png b/public/-/emojis/3/flag_dz.png
new file mode 100644
index 00000000000..c7d8acc9c66
--- /dev/null
+++ b/public/-/emojis/3/flag_dz.png
Binary files differ
diff --git a/public/-/emojis/3/flag_ea.png b/public/-/emojis/3/flag_ea.png
new file mode 100644
index 00000000000..d48640c85d8
--- /dev/null
+++ b/public/-/emojis/3/flag_ea.png
Binary files differ
diff --git a/public/-/emojis/3/flag_ec.png b/public/-/emojis/3/flag_ec.png
new file mode 100644
index 00000000000..61dddf35f10
--- /dev/null
+++ b/public/-/emojis/3/flag_ec.png
Binary files differ
diff --git a/public/-/emojis/3/flag_ee.png b/public/-/emojis/3/flag_ee.png
new file mode 100644
index 00000000000..620f80e4206
--- /dev/null
+++ b/public/-/emojis/3/flag_ee.png
Binary files differ
diff --git a/public/-/emojis/3/flag_eg.png b/public/-/emojis/3/flag_eg.png
new file mode 100644
index 00000000000..8799f624c2d
--- /dev/null
+++ b/public/-/emojis/3/flag_eg.png
Binary files differ
diff --git a/public/-/emojis/3/flag_eh.png b/public/-/emojis/3/flag_eh.png
new file mode 100644
index 00000000000..829595e4c2c
--- /dev/null
+++ b/public/-/emojis/3/flag_eh.png
Binary files differ
diff --git a/public/-/emojis/3/flag_er.png b/public/-/emojis/3/flag_er.png
new file mode 100644
index 00000000000..a29c0662463
--- /dev/null
+++ b/public/-/emojis/3/flag_er.png
Binary files differ
diff --git a/public/-/emojis/3/flag_es.png b/public/-/emojis/3/flag_es.png
new file mode 100644
index 00000000000..d48640c85d8
--- /dev/null
+++ b/public/-/emojis/3/flag_es.png
Binary files differ
diff --git a/public/-/emojis/3/flag_et.png b/public/-/emojis/3/flag_et.png
new file mode 100644
index 00000000000..a848b8cd19f
--- /dev/null
+++ b/public/-/emojis/3/flag_et.png
Binary files differ
diff --git a/public/-/emojis/3/flag_eu.png b/public/-/emojis/3/flag_eu.png
new file mode 100644
index 00000000000..940d25cbfa1
--- /dev/null
+++ b/public/-/emojis/3/flag_eu.png
Binary files differ
diff --git a/public/-/emojis/3/flag_fi.png b/public/-/emojis/3/flag_fi.png
new file mode 100644
index 00000000000..525b45b11ad
--- /dev/null
+++ b/public/-/emojis/3/flag_fi.png
Binary files differ
diff --git a/public/-/emojis/3/flag_fj.png b/public/-/emojis/3/flag_fj.png
new file mode 100644
index 00000000000..eff4c31d825
--- /dev/null
+++ b/public/-/emojis/3/flag_fj.png
Binary files differ
diff --git a/public/-/emojis/3/flag_fk.png b/public/-/emojis/3/flag_fk.png
new file mode 100644
index 00000000000..32d298878c0
--- /dev/null
+++ b/public/-/emojis/3/flag_fk.png
Binary files differ
diff --git a/public/-/emojis/3/flag_fm.png b/public/-/emojis/3/flag_fm.png
new file mode 100644
index 00000000000..e60b16cc45a
--- /dev/null
+++ b/public/-/emojis/3/flag_fm.png
Binary files differ
diff --git a/public/-/emojis/3/flag_fo.png b/public/-/emojis/3/flag_fo.png
new file mode 100644
index 00000000000..5a518772352
--- /dev/null
+++ b/public/-/emojis/3/flag_fo.png
Binary files differ
diff --git a/public/-/emojis/3/flag_fr.png b/public/-/emojis/3/flag_fr.png
new file mode 100644
index 00000000000..98039a24e6b
--- /dev/null
+++ b/public/-/emojis/3/flag_fr.png
Binary files differ
diff --git a/public/-/emojis/3/flag_ga.png b/public/-/emojis/3/flag_ga.png
new file mode 100644
index 00000000000..28c24da1100
--- /dev/null
+++ b/public/-/emojis/3/flag_ga.png
Binary files differ
diff --git a/public/-/emojis/3/flag_gb.png b/public/-/emojis/3/flag_gb.png
new file mode 100644
index 00000000000..0ca6ffbdf69
--- /dev/null
+++ b/public/-/emojis/3/flag_gb.png
Binary files differ
diff --git a/public/-/emojis/3/flag_gd.png b/public/-/emojis/3/flag_gd.png
new file mode 100644
index 00000000000..d0d4abc000e
--- /dev/null
+++ b/public/-/emojis/3/flag_gd.png
Binary files differ
diff --git a/public/-/emojis/3/flag_ge.png b/public/-/emojis/3/flag_ge.png
new file mode 100644
index 00000000000..f9fab204a42
--- /dev/null
+++ b/public/-/emojis/3/flag_ge.png
Binary files differ
diff --git a/public/-/emojis/3/flag_gf.png b/public/-/emojis/3/flag_gf.png
new file mode 100644
index 00000000000..a762e4f4303
--- /dev/null
+++ b/public/-/emojis/3/flag_gf.png
Binary files differ
diff --git a/public/-/emojis/3/flag_gg.png b/public/-/emojis/3/flag_gg.png
new file mode 100644
index 00000000000..d90a4d5fb95
--- /dev/null
+++ b/public/-/emojis/3/flag_gg.png
Binary files differ
diff --git a/public/-/emojis/3/flag_gh.png b/public/-/emojis/3/flag_gh.png
new file mode 100644
index 00000000000..82a2f41e187
--- /dev/null
+++ b/public/-/emojis/3/flag_gh.png
Binary files differ
diff --git a/public/-/emojis/3/flag_gi.png b/public/-/emojis/3/flag_gi.png
new file mode 100644
index 00000000000..c2d84ddcb38
--- /dev/null
+++ b/public/-/emojis/3/flag_gi.png
Binary files differ
diff --git a/public/-/emojis/3/flag_gl.png b/public/-/emojis/3/flag_gl.png
new file mode 100644
index 00000000000..6f38711a78c
--- /dev/null
+++ b/public/-/emojis/3/flag_gl.png
Binary files differ
diff --git a/public/-/emojis/3/flag_gm.png b/public/-/emojis/3/flag_gm.png
new file mode 100644
index 00000000000..9b5b726373d
--- /dev/null
+++ b/public/-/emojis/3/flag_gm.png
Binary files differ
diff --git a/public/-/emojis/3/flag_gn.png b/public/-/emojis/3/flag_gn.png
new file mode 100644
index 00000000000..8821ec9bd42
--- /dev/null
+++ b/public/-/emojis/3/flag_gn.png
Binary files differ
diff --git a/public/-/emojis/3/flag_gp.png b/public/-/emojis/3/flag_gp.png
new file mode 100644
index 00000000000..a647fced59b
--- /dev/null
+++ b/public/-/emojis/3/flag_gp.png
Binary files differ
diff --git a/public/-/emojis/3/flag_gq.png b/public/-/emojis/3/flag_gq.png
new file mode 100644
index 00000000000..11cc7efcc09
--- /dev/null
+++ b/public/-/emojis/3/flag_gq.png
Binary files differ
diff --git a/public/-/emojis/3/flag_gr.png b/public/-/emojis/3/flag_gr.png
new file mode 100644
index 00000000000..d21b9090c46
--- /dev/null
+++ b/public/-/emojis/3/flag_gr.png
Binary files differ
diff --git a/public/-/emojis/3/flag_gs.png b/public/-/emojis/3/flag_gs.png
new file mode 100644
index 00000000000..6134e4d008a
--- /dev/null
+++ b/public/-/emojis/3/flag_gs.png
Binary files differ
diff --git a/public/-/emojis/3/flag_gt.png b/public/-/emojis/3/flag_gt.png
new file mode 100644
index 00000000000..f6164936b1e
--- /dev/null
+++ b/public/-/emojis/3/flag_gt.png
Binary files differ
diff --git a/public/-/emojis/3/flag_gu.png b/public/-/emojis/3/flag_gu.png
new file mode 100644
index 00000000000..a98170e8398
--- /dev/null
+++ b/public/-/emojis/3/flag_gu.png
Binary files differ
diff --git a/public/-/emojis/3/flag_gw.png b/public/-/emojis/3/flag_gw.png
new file mode 100644
index 00000000000..e378bef4372
--- /dev/null
+++ b/public/-/emojis/3/flag_gw.png
Binary files differ
diff --git a/public/-/emojis/3/flag_gy.png b/public/-/emojis/3/flag_gy.png
new file mode 100644
index 00000000000..ecda480554c
--- /dev/null
+++ b/public/-/emojis/3/flag_gy.png
Binary files differ
diff --git a/public/-/emojis/3/flag_hk.png b/public/-/emojis/3/flag_hk.png
new file mode 100644
index 00000000000..a10bb088089
--- /dev/null
+++ b/public/-/emojis/3/flag_hk.png
Binary files differ
diff --git a/public/-/emojis/3/flag_hm.png b/public/-/emojis/3/flag_hm.png
new file mode 100644
index 00000000000..411488897f8
--- /dev/null
+++ b/public/-/emojis/3/flag_hm.png
Binary files differ
diff --git a/public/-/emojis/3/flag_hn.png b/public/-/emojis/3/flag_hn.png
new file mode 100644
index 00000000000..d0022feac8e
--- /dev/null
+++ b/public/-/emojis/3/flag_hn.png
Binary files differ
diff --git a/public/-/emojis/3/flag_hr.png b/public/-/emojis/3/flag_hr.png
new file mode 100644
index 00000000000..e802cb38946
--- /dev/null
+++ b/public/-/emojis/3/flag_hr.png
Binary files differ
diff --git a/public/-/emojis/3/flag_ht.png b/public/-/emojis/3/flag_ht.png
new file mode 100644
index 00000000000..f82286d8814
--- /dev/null
+++ b/public/-/emojis/3/flag_ht.png
Binary files differ
diff --git a/public/-/emojis/3/flag_hu.png b/public/-/emojis/3/flag_hu.png
new file mode 100644
index 00000000000..e32ae6719f6
--- /dev/null
+++ b/public/-/emojis/3/flag_hu.png
Binary files differ
diff --git a/public/-/emojis/3/flag_ic.png b/public/-/emojis/3/flag_ic.png
new file mode 100644
index 00000000000..e99d9d3f245
--- /dev/null
+++ b/public/-/emojis/3/flag_ic.png
Binary files differ
diff --git a/public/-/emojis/3/flag_id.png b/public/-/emojis/3/flag_id.png
new file mode 100644
index 00000000000..3cfc3fa45c8
--- /dev/null
+++ b/public/-/emojis/3/flag_id.png
Binary files differ
diff --git a/public/-/emojis/3/flag_ie.png b/public/-/emojis/3/flag_ie.png
new file mode 100644
index 00000000000..0dd86179495
--- /dev/null
+++ b/public/-/emojis/3/flag_ie.png
Binary files differ
diff --git a/public/-/emojis/3/flag_il.png b/public/-/emojis/3/flag_il.png
new file mode 100644
index 00000000000..439ced78d3b
--- /dev/null
+++ b/public/-/emojis/3/flag_il.png
Binary files differ
diff --git a/public/-/emojis/3/flag_im.png b/public/-/emojis/3/flag_im.png
new file mode 100644
index 00000000000..69fc5a30ed9
--- /dev/null
+++ b/public/-/emojis/3/flag_im.png
Binary files differ
diff --git a/public/-/emojis/3/flag_in.png b/public/-/emojis/3/flag_in.png
new file mode 100644
index 00000000000..8ad7e9447d1
--- /dev/null
+++ b/public/-/emojis/3/flag_in.png
Binary files differ
diff --git a/public/-/emojis/3/flag_io.png b/public/-/emojis/3/flag_io.png
new file mode 100644
index 00000000000..520adc653cc
--- /dev/null
+++ b/public/-/emojis/3/flag_io.png
Binary files differ
diff --git a/public/-/emojis/3/flag_iq.png b/public/-/emojis/3/flag_iq.png
new file mode 100644
index 00000000000..3af377f252d
--- /dev/null
+++ b/public/-/emojis/3/flag_iq.png
Binary files differ
diff --git a/public/-/emojis/3/flag_ir.png b/public/-/emojis/3/flag_ir.png
new file mode 100644
index 00000000000..66c91de1b35
--- /dev/null
+++ b/public/-/emojis/3/flag_ir.png
Binary files differ
diff --git a/public/-/emojis/3/flag_is.png b/public/-/emojis/3/flag_is.png
new file mode 100644
index 00000000000..0b0379b6993
--- /dev/null
+++ b/public/-/emojis/3/flag_is.png
Binary files differ
diff --git a/public/-/emojis/3/flag_it.png b/public/-/emojis/3/flag_it.png
new file mode 100644
index 00000000000..b18c50adea5
--- /dev/null
+++ b/public/-/emojis/3/flag_it.png
Binary files differ
diff --git a/public/-/emojis/3/flag_je.png b/public/-/emojis/3/flag_je.png
new file mode 100644
index 00000000000..c514594f15e
--- /dev/null
+++ b/public/-/emojis/3/flag_je.png
Binary files differ
diff --git a/public/-/emojis/3/flag_jm.png b/public/-/emojis/3/flag_jm.png
new file mode 100644
index 00000000000..96cba7996cb
--- /dev/null
+++ b/public/-/emojis/3/flag_jm.png
Binary files differ
diff --git a/public/-/emojis/3/flag_jo.png b/public/-/emojis/3/flag_jo.png
new file mode 100644
index 00000000000..3d96d761668
--- /dev/null
+++ b/public/-/emojis/3/flag_jo.png
Binary files differ
diff --git a/public/-/emojis/3/flag_jp.png b/public/-/emojis/3/flag_jp.png
new file mode 100644
index 00000000000..e5c1c471846
--- /dev/null
+++ b/public/-/emojis/3/flag_jp.png
Binary files differ
diff --git a/public/-/emojis/3/flag_ke.png b/public/-/emojis/3/flag_ke.png
new file mode 100644
index 00000000000..bf3ee8acde3
--- /dev/null
+++ b/public/-/emojis/3/flag_ke.png
Binary files differ
diff --git a/public/-/emojis/3/flag_kg.png b/public/-/emojis/3/flag_kg.png
new file mode 100644
index 00000000000..683c1837886
--- /dev/null
+++ b/public/-/emojis/3/flag_kg.png
Binary files differ
diff --git a/public/-/emojis/3/flag_kh.png b/public/-/emojis/3/flag_kh.png
new file mode 100644
index 00000000000..c5ebaee91d5
--- /dev/null
+++ b/public/-/emojis/3/flag_kh.png
Binary files differ
diff --git a/public/-/emojis/3/flag_ki.png b/public/-/emojis/3/flag_ki.png
new file mode 100644
index 00000000000..1e99c52b4c0
--- /dev/null
+++ b/public/-/emojis/3/flag_ki.png
Binary files differ
diff --git a/public/-/emojis/3/flag_km.png b/public/-/emojis/3/flag_km.png
new file mode 100644
index 00000000000..c394f1c75f5
--- /dev/null
+++ b/public/-/emojis/3/flag_km.png
Binary files differ
diff --git a/public/-/emojis/3/flag_kn.png b/public/-/emojis/3/flag_kn.png
new file mode 100644
index 00000000000..c8a4e42d97b
--- /dev/null
+++ b/public/-/emojis/3/flag_kn.png
Binary files differ
diff --git a/public/-/emojis/3/flag_kp.png b/public/-/emojis/3/flag_kp.png
new file mode 100644
index 00000000000..aa0ac4f6473
--- /dev/null
+++ b/public/-/emojis/3/flag_kp.png
Binary files differ
diff --git a/public/-/emojis/3/flag_kr.png b/public/-/emojis/3/flag_kr.png
new file mode 100644
index 00000000000..356d312e961
--- /dev/null
+++ b/public/-/emojis/3/flag_kr.png
Binary files differ
diff --git a/public/-/emojis/3/flag_kw.png b/public/-/emojis/3/flag_kw.png
new file mode 100644
index 00000000000..fc30e8ea8cb
--- /dev/null
+++ b/public/-/emojis/3/flag_kw.png
Binary files differ
diff --git a/public/-/emojis/3/flag_ky.png b/public/-/emojis/3/flag_ky.png
new file mode 100644
index 00000000000..ec4b2a48d93
--- /dev/null
+++ b/public/-/emojis/3/flag_ky.png
Binary files differ
diff --git a/public/-/emojis/3/flag_kz.png b/public/-/emojis/3/flag_kz.png
new file mode 100644
index 00000000000..7f3c5e4eb70
--- /dev/null
+++ b/public/-/emojis/3/flag_kz.png
Binary files differ
diff --git a/public/-/emojis/3/flag_la.png b/public/-/emojis/3/flag_la.png
new file mode 100644
index 00000000000..f61309db084
--- /dev/null
+++ b/public/-/emojis/3/flag_la.png
Binary files differ
diff --git a/public/-/emojis/3/flag_lb.png b/public/-/emojis/3/flag_lb.png
new file mode 100644
index 00000000000..83bbd534f1d
--- /dev/null
+++ b/public/-/emojis/3/flag_lb.png
Binary files differ
diff --git a/public/-/emojis/3/flag_lc.png b/public/-/emojis/3/flag_lc.png
new file mode 100644
index 00000000000..42b49d2059c
--- /dev/null
+++ b/public/-/emojis/3/flag_lc.png
Binary files differ
diff --git a/public/-/emojis/3/flag_li.png b/public/-/emojis/3/flag_li.png
new file mode 100644
index 00000000000..851a5e23c7c
--- /dev/null
+++ b/public/-/emojis/3/flag_li.png
Binary files differ
diff --git a/public/-/emojis/3/flag_lk.png b/public/-/emojis/3/flag_lk.png
new file mode 100644
index 00000000000..94148a42a6f
--- /dev/null
+++ b/public/-/emojis/3/flag_lk.png
Binary files differ
diff --git a/public/-/emojis/3/flag_lr.png b/public/-/emojis/3/flag_lr.png
new file mode 100644
index 00000000000..316e623afe0
--- /dev/null
+++ b/public/-/emojis/3/flag_lr.png
Binary files differ
diff --git a/public/-/emojis/3/flag_ls.png b/public/-/emojis/3/flag_ls.png
new file mode 100644
index 00000000000..0935837ec98
--- /dev/null
+++ b/public/-/emojis/3/flag_ls.png
Binary files differ
diff --git a/public/-/emojis/3/flag_lt.png b/public/-/emojis/3/flag_lt.png
new file mode 100644
index 00000000000..2b4bc32fd91
--- /dev/null
+++ b/public/-/emojis/3/flag_lt.png
Binary files differ
diff --git a/public/-/emojis/3/flag_lu.png b/public/-/emojis/3/flag_lu.png
new file mode 100644
index 00000000000..ebdcc7741fd
--- /dev/null
+++ b/public/-/emojis/3/flag_lu.png
Binary files differ
diff --git a/public/-/emojis/3/flag_lv.png b/public/-/emojis/3/flag_lv.png
new file mode 100644
index 00000000000..1c9eca324cd
--- /dev/null
+++ b/public/-/emojis/3/flag_lv.png
Binary files differ
diff --git a/public/-/emojis/3/flag_ly.png b/public/-/emojis/3/flag_ly.png
new file mode 100644
index 00000000000..7a592633b01
--- /dev/null
+++ b/public/-/emojis/3/flag_ly.png
Binary files differ
diff --git a/public/-/emojis/3/flag_ma.png b/public/-/emojis/3/flag_ma.png
new file mode 100644
index 00000000000..e10789036e4
--- /dev/null
+++ b/public/-/emojis/3/flag_ma.png
Binary files differ
diff --git a/public/-/emojis/3/flag_mc.png b/public/-/emojis/3/flag_mc.png
new file mode 100644
index 00000000000..4ce51af1b03
--- /dev/null
+++ b/public/-/emojis/3/flag_mc.png
Binary files differ
diff --git a/public/-/emojis/3/flag_md.png b/public/-/emojis/3/flag_md.png
new file mode 100644
index 00000000000..f2397e452ae
--- /dev/null
+++ b/public/-/emojis/3/flag_md.png
Binary files differ
diff --git a/public/-/emojis/3/flag_me.png b/public/-/emojis/3/flag_me.png
new file mode 100644
index 00000000000..90d099d845f
--- /dev/null
+++ b/public/-/emojis/3/flag_me.png
Binary files differ
diff --git a/public/-/emojis/3/flag_mf.png b/public/-/emojis/3/flag_mf.png
new file mode 100644
index 00000000000..98039a24e6b
--- /dev/null
+++ b/public/-/emojis/3/flag_mf.png
Binary files differ
diff --git a/public/-/emojis/3/flag_mg.png b/public/-/emojis/3/flag_mg.png
new file mode 100644
index 00000000000..e4735ae04bc
--- /dev/null
+++ b/public/-/emojis/3/flag_mg.png
Binary files differ
diff --git a/public/-/emojis/3/flag_mh.png b/public/-/emojis/3/flag_mh.png
new file mode 100644
index 00000000000..9c092055bd9
--- /dev/null
+++ b/public/-/emojis/3/flag_mh.png
Binary files differ
diff --git a/public/-/emojis/3/flag_mk.png b/public/-/emojis/3/flag_mk.png
new file mode 100644
index 00000000000..a19baac59dd
--- /dev/null
+++ b/public/-/emojis/3/flag_mk.png
Binary files differ
diff --git a/public/-/emojis/3/flag_ml.png b/public/-/emojis/3/flag_ml.png
new file mode 100644
index 00000000000..5ffff4573f0
--- /dev/null
+++ b/public/-/emojis/3/flag_ml.png
Binary files differ
diff --git a/public/-/emojis/3/flag_mm.png b/public/-/emojis/3/flag_mm.png
new file mode 100644
index 00000000000..31e81f3eb82
--- /dev/null
+++ b/public/-/emojis/3/flag_mm.png
Binary files differ
diff --git a/public/-/emojis/3/flag_mn.png b/public/-/emojis/3/flag_mn.png
new file mode 100644
index 00000000000..52e6d949f8b
--- /dev/null
+++ b/public/-/emojis/3/flag_mn.png
Binary files differ
diff --git a/public/-/emojis/3/flag_mo.png b/public/-/emojis/3/flag_mo.png
new file mode 100644
index 00000000000..a46519c4452
--- /dev/null
+++ b/public/-/emojis/3/flag_mo.png
Binary files differ
diff --git a/public/-/emojis/3/flag_mp.png b/public/-/emojis/3/flag_mp.png
new file mode 100644
index 00000000000..726df4037b9
--- /dev/null
+++ b/public/-/emojis/3/flag_mp.png
Binary files differ
diff --git a/public/-/emojis/3/flag_mq.png b/public/-/emojis/3/flag_mq.png
new file mode 100644
index 00000000000..f88d5d97735
--- /dev/null
+++ b/public/-/emojis/3/flag_mq.png
Binary files differ
diff --git a/public/-/emojis/3/flag_mr.png b/public/-/emojis/3/flag_mr.png
new file mode 100644
index 00000000000..b2b01eb272d
--- /dev/null
+++ b/public/-/emojis/3/flag_mr.png
Binary files differ
diff --git a/public/-/emojis/3/flag_ms.png b/public/-/emojis/3/flag_ms.png
new file mode 100644
index 00000000000..5f202c492c3
--- /dev/null
+++ b/public/-/emojis/3/flag_ms.png
Binary files differ
diff --git a/public/-/emojis/3/flag_mt.png b/public/-/emojis/3/flag_mt.png
new file mode 100644
index 00000000000..33eae3cab25
--- /dev/null
+++ b/public/-/emojis/3/flag_mt.png
Binary files differ
diff --git a/public/-/emojis/3/flag_mu.png b/public/-/emojis/3/flag_mu.png
new file mode 100644
index 00000000000..6404dbf9a34
--- /dev/null
+++ b/public/-/emojis/3/flag_mu.png
Binary files differ
diff --git a/public/-/emojis/3/flag_mv.png b/public/-/emojis/3/flag_mv.png
new file mode 100644
index 00000000000..2027b0e826c
--- /dev/null
+++ b/public/-/emojis/3/flag_mv.png
Binary files differ
diff --git a/public/-/emojis/3/flag_mw.png b/public/-/emojis/3/flag_mw.png
new file mode 100644
index 00000000000..2dec6561580
--- /dev/null
+++ b/public/-/emojis/3/flag_mw.png
Binary files differ
diff --git a/public/-/emojis/3/flag_mx.png b/public/-/emojis/3/flag_mx.png
new file mode 100644
index 00000000000..f24df18d199
--- /dev/null
+++ b/public/-/emojis/3/flag_mx.png
Binary files differ
diff --git a/public/-/emojis/3/flag_my.png b/public/-/emojis/3/flag_my.png
new file mode 100644
index 00000000000..4de9b8de086
--- /dev/null
+++ b/public/-/emojis/3/flag_my.png
Binary files differ
diff --git a/public/-/emojis/3/flag_mz.png b/public/-/emojis/3/flag_mz.png
new file mode 100644
index 00000000000..b7d7ca87a8e
--- /dev/null
+++ b/public/-/emojis/3/flag_mz.png
Binary files differ
diff --git a/public/-/emojis/3/flag_na.png b/public/-/emojis/3/flag_na.png
new file mode 100644
index 00000000000..04316824cc9
--- /dev/null
+++ b/public/-/emojis/3/flag_na.png
Binary files differ
diff --git a/public/-/emojis/3/flag_nc.png b/public/-/emojis/3/flag_nc.png
new file mode 100644
index 00000000000..52ee45e79d9
--- /dev/null
+++ b/public/-/emojis/3/flag_nc.png
Binary files differ
diff --git a/public/-/emojis/3/flag_ne.png b/public/-/emojis/3/flag_ne.png
new file mode 100644
index 00000000000..f0a0c09a522
--- /dev/null
+++ b/public/-/emojis/3/flag_ne.png
Binary files differ
diff --git a/public/-/emojis/3/flag_nf.png b/public/-/emojis/3/flag_nf.png
new file mode 100644
index 00000000000..5819a858430
--- /dev/null
+++ b/public/-/emojis/3/flag_nf.png
Binary files differ
diff --git a/public/-/emojis/3/flag_ng.png b/public/-/emojis/3/flag_ng.png
new file mode 100644
index 00000000000..944486f9691
--- /dev/null
+++ b/public/-/emojis/3/flag_ng.png
Binary files differ
diff --git a/public/-/emojis/3/flag_ni.png b/public/-/emojis/3/flag_ni.png
new file mode 100644
index 00000000000..5ccdc5a94b1
--- /dev/null
+++ b/public/-/emojis/3/flag_ni.png
Binary files differ
diff --git a/public/-/emojis/3/flag_nl.png b/public/-/emojis/3/flag_nl.png
new file mode 100644
index 00000000000..3ea08f39858
--- /dev/null
+++ b/public/-/emojis/3/flag_nl.png
Binary files differ
diff --git a/public/-/emojis/3/flag_no.png b/public/-/emojis/3/flag_no.png
new file mode 100644
index 00000000000..d94e9a72179
--- /dev/null
+++ b/public/-/emojis/3/flag_no.png
Binary files differ
diff --git a/public/-/emojis/3/flag_np.png b/public/-/emojis/3/flag_np.png
new file mode 100644
index 00000000000..8105052636b
--- /dev/null
+++ b/public/-/emojis/3/flag_np.png
Binary files differ
diff --git a/public/-/emojis/3/flag_nr.png b/public/-/emojis/3/flag_nr.png
new file mode 100644
index 00000000000..15295d0df43
--- /dev/null
+++ b/public/-/emojis/3/flag_nr.png
Binary files differ
diff --git a/public/-/emojis/3/flag_nu.png b/public/-/emojis/3/flag_nu.png
new file mode 100644
index 00000000000..6b5afc0bca6
--- /dev/null
+++ b/public/-/emojis/3/flag_nu.png
Binary files differ
diff --git a/public/-/emojis/3/flag_nz.png b/public/-/emojis/3/flag_nz.png
new file mode 100644
index 00000000000..ea6f2368d20
--- /dev/null
+++ b/public/-/emojis/3/flag_nz.png
Binary files differ
diff --git a/public/-/emojis/3/flag_om.png b/public/-/emojis/3/flag_om.png
new file mode 100644
index 00000000000..57c88d19d01
--- /dev/null
+++ b/public/-/emojis/3/flag_om.png
Binary files differ
diff --git a/public/-/emojis/3/flag_pa.png b/public/-/emojis/3/flag_pa.png
new file mode 100644
index 00000000000..d9d880b5b87
--- /dev/null
+++ b/public/-/emojis/3/flag_pa.png
Binary files differ
diff --git a/public/-/emojis/3/flag_pe.png b/public/-/emojis/3/flag_pe.png
new file mode 100644
index 00000000000..7d41ec53fd3
--- /dev/null
+++ b/public/-/emojis/3/flag_pe.png
Binary files differ
diff --git a/public/-/emojis/3/flag_pf.png b/public/-/emojis/3/flag_pf.png
new file mode 100644
index 00000000000..2a14a5e1e4f
--- /dev/null
+++ b/public/-/emojis/3/flag_pf.png
Binary files differ
diff --git a/public/-/emojis/3/flag_pg.png b/public/-/emojis/3/flag_pg.png
new file mode 100644
index 00000000000..95acf5da44f
--- /dev/null
+++ b/public/-/emojis/3/flag_pg.png
Binary files differ
diff --git a/public/-/emojis/3/flag_ph.png b/public/-/emojis/3/flag_ph.png
new file mode 100644
index 00000000000..e84f6d51761
--- /dev/null
+++ b/public/-/emojis/3/flag_ph.png
Binary files differ
diff --git a/public/-/emojis/3/flag_pk.png b/public/-/emojis/3/flag_pk.png
new file mode 100644
index 00000000000..d4ad31d7270
--- /dev/null
+++ b/public/-/emojis/3/flag_pk.png
Binary files differ
diff --git a/public/-/emojis/3/flag_pl.png b/public/-/emojis/3/flag_pl.png
new file mode 100644
index 00000000000..0f1839943b4
--- /dev/null
+++ b/public/-/emojis/3/flag_pl.png
Binary files differ
diff --git a/public/-/emojis/3/flag_pm.png b/public/-/emojis/3/flag_pm.png
new file mode 100644
index 00000000000..6034f084a6d
--- /dev/null
+++ b/public/-/emojis/3/flag_pm.png
Binary files differ
diff --git a/public/-/emojis/3/flag_pn.png b/public/-/emojis/3/flag_pn.png
new file mode 100644
index 00000000000..b7098d1c8fe
--- /dev/null
+++ b/public/-/emojis/3/flag_pn.png
Binary files differ
diff --git a/public/-/emojis/3/flag_pr.png b/public/-/emojis/3/flag_pr.png
new file mode 100644
index 00000000000..2fc5e5dc3cf
--- /dev/null
+++ b/public/-/emojis/3/flag_pr.png
Binary files differ
diff --git a/public/-/emojis/3/flag_ps.png b/public/-/emojis/3/flag_ps.png
new file mode 100644
index 00000000000..f85277da227
--- /dev/null
+++ b/public/-/emojis/3/flag_ps.png
Binary files differ
diff --git a/public/-/emojis/3/flag_pt.png b/public/-/emojis/3/flag_pt.png
new file mode 100644
index 00000000000..db518e255d1
--- /dev/null
+++ b/public/-/emojis/3/flag_pt.png
Binary files differ
diff --git a/public/-/emojis/3/flag_pw.png b/public/-/emojis/3/flag_pw.png
new file mode 100644
index 00000000000..dc72d2877a1
--- /dev/null
+++ b/public/-/emojis/3/flag_pw.png
Binary files differ
diff --git a/public/-/emojis/3/flag_py.png b/public/-/emojis/3/flag_py.png
new file mode 100644
index 00000000000..32d09073d15
--- /dev/null
+++ b/public/-/emojis/3/flag_py.png
Binary files differ
diff --git a/public/-/emojis/3/flag_qa.png b/public/-/emojis/3/flag_qa.png
new file mode 100644
index 00000000000..02bebd76678
--- /dev/null
+++ b/public/-/emojis/3/flag_qa.png
Binary files differ
diff --git a/public/-/emojis/3/flag_re.png b/public/-/emojis/3/flag_re.png
new file mode 100644
index 00000000000..76ab2085f47
--- /dev/null
+++ b/public/-/emojis/3/flag_re.png
Binary files differ
diff --git a/public/-/emojis/3/flag_ro.png b/public/-/emojis/3/flag_ro.png
new file mode 100644
index 00000000000..ed9cdd874be
--- /dev/null
+++ b/public/-/emojis/3/flag_ro.png
Binary files differ
diff --git a/public/-/emojis/3/flag_rs.png b/public/-/emojis/3/flag_rs.png
new file mode 100644
index 00000000000..767a22acb64
--- /dev/null
+++ b/public/-/emojis/3/flag_rs.png
Binary files differ
diff --git a/public/-/emojis/3/flag_ru.png b/public/-/emojis/3/flag_ru.png
new file mode 100644
index 00000000000..4ef7edfa177
--- /dev/null
+++ b/public/-/emojis/3/flag_ru.png
Binary files differ
diff --git a/public/-/emojis/3/flag_rw.png b/public/-/emojis/3/flag_rw.png
new file mode 100644
index 00000000000..15c38302d80
--- /dev/null
+++ b/public/-/emojis/3/flag_rw.png
Binary files differ
diff --git a/public/-/emojis/3/flag_sa.png b/public/-/emojis/3/flag_sa.png
new file mode 100644
index 00000000000..f9845634a24
--- /dev/null
+++ b/public/-/emojis/3/flag_sa.png
Binary files differ
diff --git a/public/-/emojis/3/flag_sb.png b/public/-/emojis/3/flag_sb.png
new file mode 100644
index 00000000000..dda0b3cae0c
--- /dev/null
+++ b/public/-/emojis/3/flag_sb.png
Binary files differ
diff --git a/public/-/emojis/3/flag_sc.png b/public/-/emojis/3/flag_sc.png
new file mode 100644
index 00000000000..097f3f8f851
--- /dev/null
+++ b/public/-/emojis/3/flag_sc.png
Binary files differ
diff --git a/public/-/emojis/3/flag_sd.png b/public/-/emojis/3/flag_sd.png
new file mode 100644
index 00000000000..70a3a95a8ee
--- /dev/null
+++ b/public/-/emojis/3/flag_sd.png
Binary files differ
diff --git a/public/-/emojis/3/flag_se.png b/public/-/emojis/3/flag_se.png
new file mode 100644
index 00000000000..4904b2cba39
--- /dev/null
+++ b/public/-/emojis/3/flag_se.png
Binary files differ
diff --git a/public/-/emojis/3/flag_sg.png b/public/-/emojis/3/flag_sg.png
new file mode 100644
index 00000000000..f4f9416c467
--- /dev/null
+++ b/public/-/emojis/3/flag_sg.png
Binary files differ
diff --git a/public/-/emojis/3/flag_sh.png b/public/-/emojis/3/flag_sh.png
new file mode 100644
index 00000000000..a0122711407
--- /dev/null
+++ b/public/-/emojis/3/flag_sh.png
Binary files differ
diff --git a/public/-/emojis/3/flag_si.png b/public/-/emojis/3/flag_si.png
new file mode 100644
index 00000000000..36736281a49
--- /dev/null
+++ b/public/-/emojis/3/flag_si.png
Binary files differ
diff --git a/public/-/emojis/3/flag_sj.png b/public/-/emojis/3/flag_sj.png
new file mode 100644
index 00000000000..d94e9a72179
--- /dev/null
+++ b/public/-/emojis/3/flag_sj.png
Binary files differ
diff --git a/public/-/emojis/3/flag_sk.png b/public/-/emojis/3/flag_sk.png
new file mode 100644
index 00000000000..46e027dea8b
--- /dev/null
+++ b/public/-/emojis/3/flag_sk.png
Binary files differ
diff --git a/public/-/emojis/3/flag_sl.png b/public/-/emojis/3/flag_sl.png
new file mode 100644
index 00000000000..3f2e4f191b0
--- /dev/null
+++ b/public/-/emojis/3/flag_sl.png
Binary files differ
diff --git a/public/-/emojis/3/flag_sm.png b/public/-/emojis/3/flag_sm.png
new file mode 100644
index 00000000000..261b25e45f4
--- /dev/null
+++ b/public/-/emojis/3/flag_sm.png
Binary files differ
diff --git a/public/-/emojis/3/flag_sn.png b/public/-/emojis/3/flag_sn.png
new file mode 100644
index 00000000000..97e2fbe745b
--- /dev/null
+++ b/public/-/emojis/3/flag_sn.png
Binary files differ
diff --git a/public/-/emojis/3/flag_so.png b/public/-/emojis/3/flag_so.png
new file mode 100644
index 00000000000..aaecb5fb5a0
--- /dev/null
+++ b/public/-/emojis/3/flag_so.png
Binary files differ
diff --git a/public/-/emojis/3/flag_sr.png b/public/-/emojis/3/flag_sr.png
new file mode 100644
index 00000000000..b2c03cf5b3c
--- /dev/null
+++ b/public/-/emojis/3/flag_sr.png
Binary files differ
diff --git a/public/-/emojis/3/flag_ss.png b/public/-/emojis/3/flag_ss.png
new file mode 100644
index 00000000000..65b217b70e3
--- /dev/null
+++ b/public/-/emojis/3/flag_ss.png
Binary files differ
diff --git a/public/-/emojis/3/flag_st.png b/public/-/emojis/3/flag_st.png
new file mode 100644
index 00000000000..9cb7c0287b7
--- /dev/null
+++ b/public/-/emojis/3/flag_st.png
Binary files differ
diff --git a/public/-/emojis/3/flag_sv.png b/public/-/emojis/3/flag_sv.png
new file mode 100644
index 00000000000..006ac33d5a5
--- /dev/null
+++ b/public/-/emojis/3/flag_sv.png
Binary files differ
diff --git a/public/-/emojis/3/flag_sx.png b/public/-/emojis/3/flag_sx.png
new file mode 100644
index 00000000000..52228e067ba
--- /dev/null
+++ b/public/-/emojis/3/flag_sx.png
Binary files differ
diff --git a/public/-/emojis/3/flag_sy.png b/public/-/emojis/3/flag_sy.png
new file mode 100644
index 00000000000..314495da4b3
--- /dev/null
+++ b/public/-/emojis/3/flag_sy.png
Binary files differ
diff --git a/public/-/emojis/3/flag_sz.png b/public/-/emojis/3/flag_sz.png
new file mode 100644
index 00000000000..ef94dd551d0
--- /dev/null
+++ b/public/-/emojis/3/flag_sz.png
Binary files differ
diff --git a/public/-/emojis/3/flag_ta.png b/public/-/emojis/3/flag_ta.png
new file mode 100644
index 00000000000..e1bbc026b65
--- /dev/null
+++ b/public/-/emojis/3/flag_ta.png
Binary files differ
diff --git a/public/-/emojis/3/flag_tc.png b/public/-/emojis/3/flag_tc.png
new file mode 100644
index 00000000000..c244969df1f
--- /dev/null
+++ b/public/-/emojis/3/flag_tc.png
Binary files differ
diff --git a/public/-/emojis/3/flag_td.png b/public/-/emojis/3/flag_td.png
new file mode 100644
index 00000000000..ad7c61a2b4e
--- /dev/null
+++ b/public/-/emojis/3/flag_td.png
Binary files differ
diff --git a/public/-/emojis/3/flag_tf.png b/public/-/emojis/3/flag_tf.png
new file mode 100644
index 00000000000..720ce002eb8
--- /dev/null
+++ b/public/-/emojis/3/flag_tf.png
Binary files differ
diff --git a/public/-/emojis/3/flag_tg.png b/public/-/emojis/3/flag_tg.png
new file mode 100644
index 00000000000..42e20707c30
--- /dev/null
+++ b/public/-/emojis/3/flag_tg.png
Binary files differ
diff --git a/public/-/emojis/3/flag_th.png b/public/-/emojis/3/flag_th.png
new file mode 100644
index 00000000000..6022f2645f1
--- /dev/null
+++ b/public/-/emojis/3/flag_th.png
Binary files differ
diff --git a/public/-/emojis/3/flag_tj.png b/public/-/emojis/3/flag_tj.png
new file mode 100644
index 00000000000..78a207210eb
--- /dev/null
+++ b/public/-/emojis/3/flag_tj.png
Binary files differ
diff --git a/public/-/emojis/3/flag_tk.png b/public/-/emojis/3/flag_tk.png
new file mode 100644
index 00000000000..84bc1155fd3
--- /dev/null
+++ b/public/-/emojis/3/flag_tk.png
Binary files differ
diff --git a/public/-/emojis/3/flag_tl.png b/public/-/emojis/3/flag_tl.png
new file mode 100644
index 00000000000..c6a53e5b5e7
--- /dev/null
+++ b/public/-/emojis/3/flag_tl.png
Binary files differ
diff --git a/public/-/emojis/3/flag_tm.png b/public/-/emojis/3/flag_tm.png
new file mode 100644
index 00000000000..207ec41373a
--- /dev/null
+++ b/public/-/emojis/3/flag_tm.png
Binary files differ
diff --git a/public/-/emojis/3/flag_tn.png b/public/-/emojis/3/flag_tn.png
new file mode 100644
index 00000000000..c196de6565c
--- /dev/null
+++ b/public/-/emojis/3/flag_tn.png
Binary files differ
diff --git a/public/-/emojis/3/flag_to.png b/public/-/emojis/3/flag_to.png
new file mode 100644
index 00000000000..1e334470bf6
--- /dev/null
+++ b/public/-/emojis/3/flag_to.png
Binary files differ
diff --git a/public/-/emojis/3/flag_tr.png b/public/-/emojis/3/flag_tr.png
new file mode 100644
index 00000000000..c32816cc587
--- /dev/null
+++ b/public/-/emojis/3/flag_tr.png
Binary files differ
diff --git a/public/-/emojis/3/flag_tt.png b/public/-/emojis/3/flag_tt.png
new file mode 100644
index 00000000000..7dc4f5b6654
--- /dev/null
+++ b/public/-/emojis/3/flag_tt.png
Binary files differ
diff --git a/public/-/emojis/3/flag_tv.png b/public/-/emojis/3/flag_tv.png
new file mode 100644
index 00000000000..1b1fcbf4f46
--- /dev/null
+++ b/public/-/emojis/3/flag_tv.png
Binary files differ
diff --git a/public/-/emojis/3/flag_tw.png b/public/-/emojis/3/flag_tw.png
new file mode 100644
index 00000000000..81f18e8aede
--- /dev/null
+++ b/public/-/emojis/3/flag_tw.png
Binary files differ
diff --git a/public/-/emojis/3/flag_tz.png b/public/-/emojis/3/flag_tz.png
new file mode 100644
index 00000000000..15953ebb6f5
--- /dev/null
+++ b/public/-/emojis/3/flag_tz.png
Binary files differ
diff --git a/public/-/emojis/3/flag_ua.png b/public/-/emojis/3/flag_ua.png
new file mode 100644
index 00000000000..23deec9379c
--- /dev/null
+++ b/public/-/emojis/3/flag_ua.png
Binary files differ
diff --git a/public/-/emojis/3/flag_ug.png b/public/-/emojis/3/flag_ug.png
new file mode 100644
index 00000000000..ee0ab96e776
--- /dev/null
+++ b/public/-/emojis/3/flag_ug.png
Binary files differ
diff --git a/public/-/emojis/3/flag_um.png b/public/-/emojis/3/flag_um.png
new file mode 100644
index 00000000000..ca311bd16fd
--- /dev/null
+++ b/public/-/emojis/3/flag_um.png
Binary files differ
diff --git a/public/-/emojis/3/flag_us.png b/public/-/emojis/3/flag_us.png
new file mode 100644
index 00000000000..ca311bd16fd
--- /dev/null
+++ b/public/-/emojis/3/flag_us.png
Binary files differ
diff --git a/public/-/emojis/3/flag_uy.png b/public/-/emojis/3/flag_uy.png
new file mode 100644
index 00000000000..f9c064087b5
--- /dev/null
+++ b/public/-/emojis/3/flag_uy.png
Binary files differ
diff --git a/public/-/emojis/3/flag_uz.png b/public/-/emojis/3/flag_uz.png
new file mode 100644
index 00000000000..aac61d58c4f
--- /dev/null
+++ b/public/-/emojis/3/flag_uz.png
Binary files differ
diff --git a/public/-/emojis/3/flag_va.png b/public/-/emojis/3/flag_va.png
new file mode 100644
index 00000000000..acfd70c51fa
--- /dev/null
+++ b/public/-/emojis/3/flag_va.png
Binary files differ
diff --git a/public/-/emojis/3/flag_vc.png b/public/-/emojis/3/flag_vc.png
new file mode 100644
index 00000000000..8863ec1ba37
--- /dev/null
+++ b/public/-/emojis/3/flag_vc.png
Binary files differ
diff --git a/public/-/emojis/3/flag_ve.png b/public/-/emojis/3/flag_ve.png
new file mode 100644
index 00000000000..602657e337a
--- /dev/null
+++ b/public/-/emojis/3/flag_ve.png
Binary files differ
diff --git a/public/-/emojis/3/flag_vg.png b/public/-/emojis/3/flag_vg.png
new file mode 100644
index 00000000000..3fa10bf2fad
--- /dev/null
+++ b/public/-/emojis/3/flag_vg.png
Binary files differ
diff --git a/public/-/emojis/3/flag_vi.png b/public/-/emojis/3/flag_vi.png
new file mode 100644
index 00000000000..fcf5cbec7d8
--- /dev/null
+++ b/public/-/emojis/3/flag_vi.png
Binary files differ
diff --git a/public/-/emojis/3/flag_vn.png b/public/-/emojis/3/flag_vn.png
new file mode 100644
index 00000000000..9e603f6b167
--- /dev/null
+++ b/public/-/emojis/3/flag_vn.png
Binary files differ
diff --git a/public/-/emojis/3/flag_vu.png b/public/-/emojis/3/flag_vu.png
new file mode 100644
index 00000000000..0d066d309e8
--- /dev/null
+++ b/public/-/emojis/3/flag_vu.png
Binary files differ
diff --git a/public/-/emojis/3/flag_wf.png b/public/-/emojis/3/flag_wf.png
new file mode 100644
index 00000000000..5fa64fa8d0d
--- /dev/null
+++ b/public/-/emojis/3/flag_wf.png
Binary files differ
diff --git a/public/-/emojis/3/flag_white.png b/public/-/emojis/3/flag_white.png
new file mode 100644
index 00000000000..d61c3e954dd
--- /dev/null
+++ b/public/-/emojis/3/flag_white.png
Binary files differ
diff --git a/public/-/emojis/3/flag_ws.png b/public/-/emojis/3/flag_ws.png
new file mode 100644
index 00000000000..243d1cc2e64
--- /dev/null
+++ b/public/-/emojis/3/flag_ws.png
Binary files differ
diff --git a/public/-/emojis/3/flag_xk.png b/public/-/emojis/3/flag_xk.png
new file mode 100644
index 00000000000..387fe660587
--- /dev/null
+++ b/public/-/emojis/3/flag_xk.png
Binary files differ
diff --git a/public/-/emojis/3/flag_ye.png b/public/-/emojis/3/flag_ye.png
new file mode 100644
index 00000000000..7506fb2de99
--- /dev/null
+++ b/public/-/emojis/3/flag_ye.png
Binary files differ
diff --git a/public/-/emojis/3/flag_yt.png b/public/-/emojis/3/flag_yt.png
new file mode 100644
index 00000000000..00adfaacda6
--- /dev/null
+++ b/public/-/emojis/3/flag_yt.png
Binary files differ
diff --git a/public/-/emojis/3/flag_za.png b/public/-/emojis/3/flag_za.png
new file mode 100644
index 00000000000..96236a0d34b
--- /dev/null
+++ b/public/-/emojis/3/flag_za.png
Binary files differ
diff --git a/public/-/emojis/3/flag_zm.png b/public/-/emojis/3/flag_zm.png
new file mode 100644
index 00000000000..71807f86941
--- /dev/null
+++ b/public/-/emojis/3/flag_zm.png
Binary files differ
diff --git a/public/-/emojis/3/flag_zw.png b/public/-/emojis/3/flag_zw.png
new file mode 100644
index 00000000000..cf2c5b5a930
--- /dev/null
+++ b/public/-/emojis/3/flag_zw.png
Binary files differ
diff --git a/public/-/emojis/3/flags.png b/public/-/emojis/3/flags.png
new file mode 100644
index 00000000000..b2d6ab4b89c
--- /dev/null
+++ b/public/-/emojis/3/flags.png
Binary files differ
diff --git a/public/-/emojis/3/flashlight.png b/public/-/emojis/3/flashlight.png
new file mode 100644
index 00000000000..bb6933f0095
--- /dev/null
+++ b/public/-/emojis/3/flashlight.png
Binary files differ
diff --git a/public/-/emojis/3/fleur-de-lis.png b/public/-/emojis/3/fleur-de-lis.png
new file mode 100644
index 00000000000..44130044b8a
--- /dev/null
+++ b/public/-/emojis/3/fleur-de-lis.png
Binary files differ
diff --git a/public/-/emojis/3/floppy_disk.png b/public/-/emojis/3/floppy_disk.png
new file mode 100644
index 00000000000..c94373d19f5
--- /dev/null
+++ b/public/-/emojis/3/floppy_disk.png
Binary files differ
diff --git a/public/-/emojis/3/flower_playing_cards.png b/public/-/emojis/3/flower_playing_cards.png
new file mode 100644
index 00000000000..988dd11cb22
--- /dev/null
+++ b/public/-/emojis/3/flower_playing_cards.png
Binary files differ
diff --git a/public/-/emojis/3/flushed.png b/public/-/emojis/3/flushed.png
new file mode 100644
index 00000000000..d0e141206d3
--- /dev/null
+++ b/public/-/emojis/3/flushed.png
Binary files differ
diff --git a/public/-/emojis/3/fog.png b/public/-/emojis/3/fog.png
new file mode 100644
index 00000000000..81d7c57aa16
--- /dev/null
+++ b/public/-/emojis/3/fog.png
Binary files differ
diff --git a/public/-/emojis/3/foggy.png b/public/-/emojis/3/foggy.png
new file mode 100644
index 00000000000..d616d716e6b
--- /dev/null
+++ b/public/-/emojis/3/foggy.png
Binary files differ
diff --git a/public/-/emojis/3/football.png b/public/-/emojis/3/football.png
new file mode 100644
index 00000000000..75bf17b1328
--- /dev/null
+++ b/public/-/emojis/3/football.png
Binary files differ
diff --git a/public/-/emojis/3/footprints.png b/public/-/emojis/3/footprints.png
new file mode 100644
index 00000000000..378c0aaa56d
--- /dev/null
+++ b/public/-/emojis/3/footprints.png
Binary files differ
diff --git a/public/-/emojis/3/fork_and_knife.png b/public/-/emojis/3/fork_and_knife.png
new file mode 100644
index 00000000000..d526471d29a
--- /dev/null
+++ b/public/-/emojis/3/fork_and_knife.png
Binary files differ
diff --git a/public/-/emojis/3/fork_knife_plate.png b/public/-/emojis/3/fork_knife_plate.png
new file mode 100644
index 00000000000..5ad662327b1
--- /dev/null
+++ b/public/-/emojis/3/fork_knife_plate.png
Binary files differ
diff --git a/public/-/emojis/3/fountain.png b/public/-/emojis/3/fountain.png
new file mode 100644
index 00000000000..96c1352516f
--- /dev/null
+++ b/public/-/emojis/3/fountain.png
Binary files differ
diff --git a/public/-/emojis/3/four.png b/public/-/emojis/3/four.png
new file mode 100644
index 00000000000..075389303ec
--- /dev/null
+++ b/public/-/emojis/3/four.png
Binary files differ
diff --git a/public/-/emojis/3/four_leaf_clover.png b/public/-/emojis/3/four_leaf_clover.png
new file mode 100644
index 00000000000..7c37aba7146
--- /dev/null
+++ b/public/-/emojis/3/four_leaf_clover.png
Binary files differ
diff --git a/public/-/emojis/3/fox.png b/public/-/emojis/3/fox.png
new file mode 100644
index 00000000000..9ae868c7d70
--- /dev/null
+++ b/public/-/emojis/3/fox.png
Binary files differ
diff --git a/public/-/emojis/3/frame_photo.png b/public/-/emojis/3/frame_photo.png
new file mode 100644
index 00000000000..87da6c848f3
--- /dev/null
+++ b/public/-/emojis/3/frame_photo.png
Binary files differ
diff --git a/public/-/emojis/3/free.png b/public/-/emojis/3/free.png
new file mode 100644
index 00000000000..c3f8ecb2188
--- /dev/null
+++ b/public/-/emojis/3/free.png
Binary files differ
diff --git a/public/-/emojis/3/french_bread.png b/public/-/emojis/3/french_bread.png
new file mode 100644
index 00000000000..1c71c123d5f
--- /dev/null
+++ b/public/-/emojis/3/french_bread.png
Binary files differ
diff --git a/public/-/emojis/3/fried_shrimp.png b/public/-/emojis/3/fried_shrimp.png
new file mode 100644
index 00000000000..56c72d63710
--- /dev/null
+++ b/public/-/emojis/3/fried_shrimp.png
Binary files differ
diff --git a/public/-/emojis/3/fries.png b/public/-/emojis/3/fries.png
new file mode 100644
index 00000000000..c48b1734dff
--- /dev/null
+++ b/public/-/emojis/3/fries.png
Binary files differ
diff --git a/public/-/emojis/3/frog.png b/public/-/emojis/3/frog.png
new file mode 100644
index 00000000000..5a2f4c668bf
--- /dev/null
+++ b/public/-/emojis/3/frog.png
Binary files differ
diff --git a/public/-/emojis/3/frowning.png b/public/-/emojis/3/frowning.png
new file mode 100644
index 00000000000..58e0c67a2bb
--- /dev/null
+++ b/public/-/emojis/3/frowning.png
Binary files differ
diff --git a/public/-/emojis/3/frowning2.png b/public/-/emojis/3/frowning2.png
new file mode 100644
index 00000000000..a30f4f36066
--- /dev/null
+++ b/public/-/emojis/3/frowning2.png
Binary files differ
diff --git a/public/-/emojis/3/fuelpump.png b/public/-/emojis/3/fuelpump.png
new file mode 100644
index 00000000000..941d5ec7433
--- /dev/null
+++ b/public/-/emojis/3/fuelpump.png
Binary files differ
diff --git a/public/-/emojis/3/full_moon.png b/public/-/emojis/3/full_moon.png
new file mode 100644
index 00000000000..8c73fc2ecf6
--- /dev/null
+++ b/public/-/emojis/3/full_moon.png
Binary files differ
diff --git a/public/-/emojis/3/full_moon_with_face.png b/public/-/emojis/3/full_moon_with_face.png
new file mode 100644
index 00000000000..a9aedae2395
--- /dev/null
+++ b/public/-/emojis/3/full_moon_with_face.png
Binary files differ
diff --git a/public/-/emojis/3/game_die.png b/public/-/emojis/3/game_die.png
new file mode 100644
index 00000000000..d3754f16ead
--- /dev/null
+++ b/public/-/emojis/3/game_die.png
Binary files differ
diff --git a/public/-/emojis/3/gay_pride_flag.png b/public/-/emojis/3/gay_pride_flag.png
new file mode 100644
index 00000000000..9a6d2add84d
--- /dev/null
+++ b/public/-/emojis/3/gay_pride_flag.png
Binary files differ
diff --git a/public/-/emojis/3/gear.png b/public/-/emojis/3/gear.png
new file mode 100644
index 00000000000..d75eb3bcb25
--- /dev/null
+++ b/public/-/emojis/3/gear.png
Binary files differ
diff --git a/public/-/emojis/3/gem.png b/public/-/emojis/3/gem.png
new file mode 100644
index 00000000000..bd5ddcc0cf2
--- /dev/null
+++ b/public/-/emojis/3/gem.png
Binary files differ
diff --git a/public/-/emojis/3/gemini.png b/public/-/emojis/3/gemini.png
new file mode 100644
index 00000000000..e06658b1aa3
--- /dev/null
+++ b/public/-/emojis/3/gemini.png
Binary files differ
diff --git a/public/-/emojis/3/ghost.png b/public/-/emojis/3/ghost.png
new file mode 100644
index 00000000000..a32f5c9c076
--- /dev/null
+++ b/public/-/emojis/3/ghost.png
Binary files differ
diff --git a/public/-/emojis/3/gift.png b/public/-/emojis/3/gift.png
new file mode 100644
index 00000000000..92d11ff1be9
--- /dev/null
+++ b/public/-/emojis/3/gift.png
Binary files differ
diff --git a/public/-/emojis/3/gift_heart.png b/public/-/emojis/3/gift_heart.png
new file mode 100644
index 00000000000..317073b749b
--- /dev/null
+++ b/public/-/emojis/3/gift_heart.png
Binary files differ
diff --git a/public/-/emojis/3/girl.png b/public/-/emojis/3/girl.png
new file mode 100644
index 00000000000..8a66d436ddb
--- /dev/null
+++ b/public/-/emojis/3/girl.png
Binary files differ
diff --git a/public/-/emojis/3/girl_tone1.png b/public/-/emojis/3/girl_tone1.png
new file mode 100644
index 00000000000..1a260068a96
--- /dev/null
+++ b/public/-/emojis/3/girl_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/girl_tone2.png b/public/-/emojis/3/girl_tone2.png
new file mode 100644
index 00000000000..43b91b8551c
--- /dev/null
+++ b/public/-/emojis/3/girl_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/girl_tone3.png b/public/-/emojis/3/girl_tone3.png
new file mode 100644
index 00000000000..bff227055c7
--- /dev/null
+++ b/public/-/emojis/3/girl_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/girl_tone4.png b/public/-/emojis/3/girl_tone4.png
new file mode 100644
index 00000000000..6b258a8edc9
--- /dev/null
+++ b/public/-/emojis/3/girl_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/girl_tone5.png b/public/-/emojis/3/girl_tone5.png
new file mode 100644
index 00000000000..efdec2df0f9
--- /dev/null
+++ b/public/-/emojis/3/girl_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/globe_with_meridians.png b/public/-/emojis/3/globe_with_meridians.png
new file mode 100644
index 00000000000..beca893eda6
--- /dev/null
+++ b/public/-/emojis/3/globe_with_meridians.png
Binary files differ
diff --git a/public/-/emojis/3/goal.png b/public/-/emojis/3/goal.png
new file mode 100644
index 00000000000..3b5b2c65250
--- /dev/null
+++ b/public/-/emojis/3/goal.png
Binary files differ
diff --git a/public/-/emojis/3/goat.png b/public/-/emojis/3/goat.png
new file mode 100644
index 00000000000..d88463d768d
--- /dev/null
+++ b/public/-/emojis/3/goat.png
Binary files differ
diff --git a/public/-/emojis/3/golf.png b/public/-/emojis/3/golf.png
new file mode 100644
index 00000000000..16910ca79fe
--- /dev/null
+++ b/public/-/emojis/3/golf.png
Binary files differ
diff --git a/public/-/emojis/3/golfer.png b/public/-/emojis/3/golfer.png
new file mode 100644
index 00000000000..554558e68e3
--- /dev/null
+++ b/public/-/emojis/3/golfer.png
Binary files differ
diff --git a/public/-/emojis/3/gorilla.png b/public/-/emojis/3/gorilla.png
new file mode 100644
index 00000000000..08c10f889a7
--- /dev/null
+++ b/public/-/emojis/3/gorilla.png
Binary files differ
diff --git a/public/-/emojis/3/grapes.png b/public/-/emojis/3/grapes.png
new file mode 100644
index 00000000000..dc1c8a42eca
--- /dev/null
+++ b/public/-/emojis/3/grapes.png
Binary files differ
diff --git a/public/-/emojis/3/green_apple.png b/public/-/emojis/3/green_apple.png
new file mode 100644
index 00000000000..df297a575f0
--- /dev/null
+++ b/public/-/emojis/3/green_apple.png
Binary files differ
diff --git a/public/-/emojis/3/green_book.png b/public/-/emojis/3/green_book.png
new file mode 100644
index 00000000000..da6ab19b789
--- /dev/null
+++ b/public/-/emojis/3/green_book.png
Binary files differ
diff --git a/public/-/emojis/3/green_heart.png b/public/-/emojis/3/green_heart.png
new file mode 100644
index 00000000000..3e9dde55a42
--- /dev/null
+++ b/public/-/emojis/3/green_heart.png
Binary files differ
diff --git a/public/-/emojis/3/grey_exclamation.png b/public/-/emojis/3/grey_exclamation.png
new file mode 100644
index 00000000000..b7cd2b00626
--- /dev/null
+++ b/public/-/emojis/3/grey_exclamation.png
Binary files differ
diff --git a/public/-/emojis/3/grey_question.png b/public/-/emojis/3/grey_question.png
new file mode 100644
index 00000000000..e43e9ef5b4b
--- /dev/null
+++ b/public/-/emojis/3/grey_question.png
Binary files differ
diff --git a/public/-/emojis/3/grimacing.png b/public/-/emojis/3/grimacing.png
new file mode 100644
index 00000000000..ab12d7edd9d
--- /dev/null
+++ b/public/-/emojis/3/grimacing.png
Binary files differ
diff --git a/public/-/emojis/3/grin.png b/public/-/emojis/3/grin.png
new file mode 100644
index 00000000000..fc82c6bd4ae
--- /dev/null
+++ b/public/-/emojis/3/grin.png
Binary files differ
diff --git a/public/-/emojis/3/grinning.png b/public/-/emojis/3/grinning.png
new file mode 100644
index 00000000000..d9d24fc834f
--- /dev/null
+++ b/public/-/emojis/3/grinning.png
Binary files differ
diff --git a/public/-/emojis/3/guardsman.png b/public/-/emojis/3/guardsman.png
new file mode 100644
index 00000000000..cb6ef760163
--- /dev/null
+++ b/public/-/emojis/3/guardsman.png
Binary files differ
diff --git a/public/-/emojis/3/guardsman_tone1.png b/public/-/emojis/3/guardsman_tone1.png
new file mode 100644
index 00000000000..485242cdf8a
--- /dev/null
+++ b/public/-/emojis/3/guardsman_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/guardsman_tone2.png b/public/-/emojis/3/guardsman_tone2.png
new file mode 100644
index 00000000000..cbba6179e67
--- /dev/null
+++ b/public/-/emojis/3/guardsman_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/guardsman_tone3.png b/public/-/emojis/3/guardsman_tone3.png
new file mode 100644
index 00000000000..94698df5ebd
--- /dev/null
+++ b/public/-/emojis/3/guardsman_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/guardsman_tone4.png b/public/-/emojis/3/guardsman_tone4.png
new file mode 100644
index 00000000000..df3593b97b7
--- /dev/null
+++ b/public/-/emojis/3/guardsman_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/guardsman_tone5.png b/public/-/emojis/3/guardsman_tone5.png
new file mode 100644
index 00000000000..fe449dd7326
--- /dev/null
+++ b/public/-/emojis/3/guardsman_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/guitar.png b/public/-/emojis/3/guitar.png
new file mode 100644
index 00000000000..afccf1c4170
--- /dev/null
+++ b/public/-/emojis/3/guitar.png
Binary files differ
diff --git a/public/-/emojis/3/gun.png b/public/-/emojis/3/gun.png
new file mode 100644
index 00000000000..fa713cca903
--- /dev/null
+++ b/public/-/emojis/3/gun.png
Binary files differ
diff --git a/public/-/emojis/3/haircut.png b/public/-/emojis/3/haircut.png
new file mode 100644
index 00000000000..6bc3eb96c84
--- /dev/null
+++ b/public/-/emojis/3/haircut.png
Binary files differ
diff --git a/public/-/emojis/3/haircut_tone1.png b/public/-/emojis/3/haircut_tone1.png
new file mode 100644
index 00000000000..b862983e467
--- /dev/null
+++ b/public/-/emojis/3/haircut_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/haircut_tone2.png b/public/-/emojis/3/haircut_tone2.png
new file mode 100644
index 00000000000..5f0588d23dd
--- /dev/null
+++ b/public/-/emojis/3/haircut_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/haircut_tone3.png b/public/-/emojis/3/haircut_tone3.png
new file mode 100644
index 00000000000..a820a84770f
--- /dev/null
+++ b/public/-/emojis/3/haircut_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/haircut_tone4.png b/public/-/emojis/3/haircut_tone4.png
new file mode 100644
index 00000000000..80c1fb49ce1
--- /dev/null
+++ b/public/-/emojis/3/haircut_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/haircut_tone5.png b/public/-/emojis/3/haircut_tone5.png
new file mode 100644
index 00000000000..85ef6faa508
--- /dev/null
+++ b/public/-/emojis/3/haircut_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/hamburger.png b/public/-/emojis/3/hamburger.png
new file mode 100644
index 00000000000..4f41f0e500b
--- /dev/null
+++ b/public/-/emojis/3/hamburger.png
Binary files differ
diff --git a/public/-/emojis/3/hammer.png b/public/-/emojis/3/hammer.png
new file mode 100644
index 00000000000..16709adc8e9
--- /dev/null
+++ b/public/-/emojis/3/hammer.png
Binary files differ
diff --git a/public/-/emojis/3/hammer_pick.png b/public/-/emojis/3/hammer_pick.png
new file mode 100644
index 00000000000..0a84f3da67e
--- /dev/null
+++ b/public/-/emojis/3/hammer_pick.png
Binary files differ
diff --git a/public/-/emojis/3/hamster.png b/public/-/emojis/3/hamster.png
new file mode 100644
index 00000000000..e6c8747fb6c
--- /dev/null
+++ b/public/-/emojis/3/hamster.png
Binary files differ
diff --git a/public/-/emojis/3/hand_splayed.png b/public/-/emojis/3/hand_splayed.png
new file mode 100644
index 00000000000..1ef0aeb3220
--- /dev/null
+++ b/public/-/emojis/3/hand_splayed.png
Binary files differ
diff --git a/public/-/emojis/3/hand_splayed_tone1.png b/public/-/emojis/3/hand_splayed_tone1.png
new file mode 100644
index 00000000000..acdf53b8c38
--- /dev/null
+++ b/public/-/emojis/3/hand_splayed_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/hand_splayed_tone2.png b/public/-/emojis/3/hand_splayed_tone2.png
new file mode 100644
index 00000000000..0849a7a9236
--- /dev/null
+++ b/public/-/emojis/3/hand_splayed_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/hand_splayed_tone3.png b/public/-/emojis/3/hand_splayed_tone3.png
new file mode 100644
index 00000000000..33050a2d739
--- /dev/null
+++ b/public/-/emojis/3/hand_splayed_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/hand_splayed_tone4.png b/public/-/emojis/3/hand_splayed_tone4.png
new file mode 100644
index 00000000000..6f3d80fc1f3
--- /dev/null
+++ b/public/-/emojis/3/hand_splayed_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/hand_splayed_tone5.png b/public/-/emojis/3/hand_splayed_tone5.png
new file mode 100644
index 00000000000..e6aee746215
--- /dev/null
+++ b/public/-/emojis/3/hand_splayed_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/handbag.png b/public/-/emojis/3/handbag.png
new file mode 100644
index 00000000000..bbc9b950acc
--- /dev/null
+++ b/public/-/emojis/3/handbag.png
Binary files differ
diff --git a/public/-/emojis/3/handball.png b/public/-/emojis/3/handball.png
new file mode 100644
index 00000000000..590339f8837
--- /dev/null
+++ b/public/-/emojis/3/handball.png
Binary files differ
diff --git a/public/-/emojis/3/handball_tone1.png b/public/-/emojis/3/handball_tone1.png
new file mode 100644
index 00000000000..3083c727c00
--- /dev/null
+++ b/public/-/emojis/3/handball_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/handball_tone2.png b/public/-/emojis/3/handball_tone2.png
new file mode 100644
index 00000000000..95387a56726
--- /dev/null
+++ b/public/-/emojis/3/handball_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/handball_tone3.png b/public/-/emojis/3/handball_tone3.png
new file mode 100644
index 00000000000..14e7ff24344
--- /dev/null
+++ b/public/-/emojis/3/handball_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/handball_tone4.png b/public/-/emojis/3/handball_tone4.png
new file mode 100644
index 00000000000..ee9becaa748
--- /dev/null
+++ b/public/-/emojis/3/handball_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/handball_tone5.png b/public/-/emojis/3/handball_tone5.png
new file mode 100644
index 00000000000..2826267bf2e
--- /dev/null
+++ b/public/-/emojis/3/handball_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/handshake.png b/public/-/emojis/3/handshake.png
new file mode 100644
index 00000000000..08274235735
--- /dev/null
+++ b/public/-/emojis/3/handshake.png
Binary files differ
diff --git a/public/-/emojis/3/handshake_tone1.png b/public/-/emojis/3/handshake_tone1.png
new file mode 100644
index 00000000000..b80629e7a75
--- /dev/null
+++ b/public/-/emojis/3/handshake_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/handshake_tone2.png b/public/-/emojis/3/handshake_tone2.png
new file mode 100644
index 00000000000..cb8864e27ae
--- /dev/null
+++ b/public/-/emojis/3/handshake_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/handshake_tone3.png b/public/-/emojis/3/handshake_tone3.png
new file mode 100644
index 00000000000..52817cb5f43
--- /dev/null
+++ b/public/-/emojis/3/handshake_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/handshake_tone4.png b/public/-/emojis/3/handshake_tone4.png
new file mode 100644
index 00000000000..7b0cc45d77b
--- /dev/null
+++ b/public/-/emojis/3/handshake_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/handshake_tone5.png b/public/-/emojis/3/handshake_tone5.png
new file mode 100644
index 00000000000..d9254fe6125
--- /dev/null
+++ b/public/-/emojis/3/handshake_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/hash.png b/public/-/emojis/3/hash.png
new file mode 100644
index 00000000000..ebd7b7047b3
--- /dev/null
+++ b/public/-/emojis/3/hash.png
Binary files differ
diff --git a/public/-/emojis/3/hatched_chick.png b/public/-/emojis/3/hatched_chick.png
new file mode 100644
index 00000000000..0867f68a397
--- /dev/null
+++ b/public/-/emojis/3/hatched_chick.png
Binary files differ
diff --git a/public/-/emojis/3/hatching_chick.png b/public/-/emojis/3/hatching_chick.png
new file mode 100644
index 00000000000..b7f68d7b6a5
--- /dev/null
+++ b/public/-/emojis/3/hatching_chick.png
Binary files differ
diff --git a/public/-/emojis/3/head_bandage.png b/public/-/emojis/3/head_bandage.png
new file mode 100644
index 00000000000..96a8b1511be
--- /dev/null
+++ b/public/-/emojis/3/head_bandage.png
Binary files differ
diff --git a/public/-/emojis/3/headphones.png b/public/-/emojis/3/headphones.png
new file mode 100644
index 00000000000..8dd7ef6e07e
--- /dev/null
+++ b/public/-/emojis/3/headphones.png
Binary files differ
diff --git a/public/-/emojis/3/hear_no_evil.png b/public/-/emojis/3/hear_no_evil.png
new file mode 100644
index 00000000000..489b210e41a
--- /dev/null
+++ b/public/-/emojis/3/hear_no_evil.png
Binary files differ
diff --git a/public/-/emojis/3/heart.png b/public/-/emojis/3/heart.png
new file mode 100644
index 00000000000..c462b4318ad
--- /dev/null
+++ b/public/-/emojis/3/heart.png
Binary files differ
diff --git a/public/-/emojis/3/heart_decoration.png b/public/-/emojis/3/heart_decoration.png
new file mode 100644
index 00000000000..f3db4864f40
--- /dev/null
+++ b/public/-/emojis/3/heart_decoration.png
Binary files differ
diff --git a/public/-/emojis/3/heart_exclamation.png b/public/-/emojis/3/heart_exclamation.png
new file mode 100644
index 00000000000..afe34a69a56
--- /dev/null
+++ b/public/-/emojis/3/heart_exclamation.png
Binary files differ
diff --git a/public/-/emojis/3/heart_eyes.png b/public/-/emojis/3/heart_eyes.png
new file mode 100644
index 00000000000..e3bb7806e12
--- /dev/null
+++ b/public/-/emojis/3/heart_eyes.png
Binary files differ
diff --git a/public/-/emojis/3/heart_eyes_cat.png b/public/-/emojis/3/heart_eyes_cat.png
new file mode 100644
index 00000000000..74b72d63f79
--- /dev/null
+++ b/public/-/emojis/3/heart_eyes_cat.png
Binary files differ
diff --git a/public/-/emojis/3/heartbeat.png b/public/-/emojis/3/heartbeat.png
new file mode 100644
index 00000000000..168dbdc2d36
--- /dev/null
+++ b/public/-/emojis/3/heartbeat.png
Binary files differ
diff --git a/public/-/emojis/3/heartpulse.png b/public/-/emojis/3/heartpulse.png
new file mode 100644
index 00000000000..79258086f4b
--- /dev/null
+++ b/public/-/emojis/3/heartpulse.png
Binary files differ
diff --git a/public/-/emojis/3/hearts.png b/public/-/emojis/3/hearts.png
new file mode 100644
index 00000000000..723226d77a4
--- /dev/null
+++ b/public/-/emojis/3/hearts.png
Binary files differ
diff --git a/public/-/emojis/3/heavy_check_mark.png b/public/-/emojis/3/heavy_check_mark.png
new file mode 100644
index 00000000000..9dfde579082
--- /dev/null
+++ b/public/-/emojis/3/heavy_check_mark.png
Binary files differ
diff --git a/public/-/emojis/3/heavy_division_sign.png b/public/-/emojis/3/heavy_division_sign.png
new file mode 100644
index 00000000000..a86eebd442b
--- /dev/null
+++ b/public/-/emojis/3/heavy_division_sign.png
Binary files differ
diff --git a/public/-/emojis/3/heavy_dollar_sign.png b/public/-/emojis/3/heavy_dollar_sign.png
new file mode 100644
index 00000000000..4e58e372cbd
--- /dev/null
+++ b/public/-/emojis/3/heavy_dollar_sign.png
Binary files differ
diff --git a/public/-/emojis/3/heavy_minus_sign.png b/public/-/emojis/3/heavy_minus_sign.png
new file mode 100644
index 00000000000..493523b02af
--- /dev/null
+++ b/public/-/emojis/3/heavy_minus_sign.png
Binary files differ
diff --git a/public/-/emojis/3/heavy_multiplication_x.png b/public/-/emojis/3/heavy_multiplication_x.png
new file mode 100644
index 00000000000..5a0e0198c61
--- /dev/null
+++ b/public/-/emojis/3/heavy_multiplication_x.png
Binary files differ
diff --git a/public/-/emojis/3/heavy_plus_sign.png b/public/-/emojis/3/heavy_plus_sign.png
new file mode 100644
index 00000000000..b3791a2c3fe
--- /dev/null
+++ b/public/-/emojis/3/heavy_plus_sign.png
Binary files differ
diff --git a/public/-/emojis/3/helicopter.png b/public/-/emojis/3/helicopter.png
new file mode 100644
index 00000000000..18843853ec4
--- /dev/null
+++ b/public/-/emojis/3/helicopter.png
Binary files differ
diff --git a/public/-/emojis/3/helmet_with_cross.png b/public/-/emojis/3/helmet_with_cross.png
new file mode 100644
index 00000000000..6119e2e6498
--- /dev/null
+++ b/public/-/emojis/3/helmet_with_cross.png
Binary files differ
diff --git a/public/-/emojis/3/herb.png b/public/-/emojis/3/herb.png
new file mode 100644
index 00000000000..7b1ff5815f0
--- /dev/null
+++ b/public/-/emojis/3/herb.png
Binary files differ
diff --git a/public/-/emojis/3/hibiscus.png b/public/-/emojis/3/hibiscus.png
new file mode 100644
index 00000000000..5400fdd7a9b
--- /dev/null
+++ b/public/-/emojis/3/hibiscus.png
Binary files differ
diff --git a/public/-/emojis/3/high_brightness.png b/public/-/emojis/3/high_brightness.png
new file mode 100644
index 00000000000..fde91f298a3
--- /dev/null
+++ b/public/-/emojis/3/high_brightness.png
Binary files differ
diff --git a/public/-/emojis/3/high_heel.png b/public/-/emojis/3/high_heel.png
new file mode 100644
index 00000000000..4a6e62180e9
--- /dev/null
+++ b/public/-/emojis/3/high_heel.png
Binary files differ
diff --git a/public/-/emojis/3/hockey.png b/public/-/emojis/3/hockey.png
new file mode 100644
index 00000000000..3353eb01657
--- /dev/null
+++ b/public/-/emojis/3/hockey.png
Binary files differ
diff --git a/public/-/emojis/3/hole.png b/public/-/emojis/3/hole.png
new file mode 100644
index 00000000000..bf1ad759bd6
--- /dev/null
+++ b/public/-/emojis/3/hole.png
Binary files differ
diff --git a/public/-/emojis/3/homes.png b/public/-/emojis/3/homes.png
new file mode 100644
index 00000000000..32b16a807d0
--- /dev/null
+++ b/public/-/emojis/3/homes.png
Binary files differ
diff --git a/public/-/emojis/3/honey_pot.png b/public/-/emojis/3/honey_pot.png
new file mode 100644
index 00000000000..85294a979fa
--- /dev/null
+++ b/public/-/emojis/3/honey_pot.png
Binary files differ
diff --git a/public/-/emojis/3/horse.png b/public/-/emojis/3/horse.png
new file mode 100644
index 00000000000..35112aca1ae
--- /dev/null
+++ b/public/-/emojis/3/horse.png
Binary files differ
diff --git a/public/-/emojis/3/horse_racing.png b/public/-/emojis/3/horse_racing.png
new file mode 100644
index 00000000000..84f92487654
--- /dev/null
+++ b/public/-/emojis/3/horse_racing.png
Binary files differ
diff --git a/public/-/emojis/3/horse_racing_tone1.png b/public/-/emojis/3/horse_racing_tone1.png
new file mode 100644
index 00000000000..7d2e8729e12
--- /dev/null
+++ b/public/-/emojis/3/horse_racing_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/horse_racing_tone2.png b/public/-/emojis/3/horse_racing_tone2.png
new file mode 100644
index 00000000000..ff29cd640de
--- /dev/null
+++ b/public/-/emojis/3/horse_racing_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/horse_racing_tone3.png b/public/-/emojis/3/horse_racing_tone3.png
new file mode 100644
index 00000000000..a72be537729
--- /dev/null
+++ b/public/-/emojis/3/horse_racing_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/horse_racing_tone4.png b/public/-/emojis/3/horse_racing_tone4.png
new file mode 100644
index 00000000000..169955f1d7e
--- /dev/null
+++ b/public/-/emojis/3/horse_racing_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/horse_racing_tone5.png b/public/-/emojis/3/horse_racing_tone5.png
new file mode 100644
index 00000000000..09aeeb940e1
--- /dev/null
+++ b/public/-/emojis/3/horse_racing_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/hospital.png b/public/-/emojis/3/hospital.png
new file mode 100644
index 00000000000..2251e567729
--- /dev/null
+++ b/public/-/emojis/3/hospital.png
Binary files differ
diff --git a/public/-/emojis/3/hot_pepper.png b/public/-/emojis/3/hot_pepper.png
new file mode 100644
index 00000000000..934bf6daf87
--- /dev/null
+++ b/public/-/emojis/3/hot_pepper.png
Binary files differ
diff --git a/public/-/emojis/3/hotdog.png b/public/-/emojis/3/hotdog.png
new file mode 100644
index 00000000000..a1738700e26
--- /dev/null
+++ b/public/-/emojis/3/hotdog.png
Binary files differ
diff --git a/public/-/emojis/3/hotel.png b/public/-/emojis/3/hotel.png
new file mode 100644
index 00000000000..2291ece74a2
--- /dev/null
+++ b/public/-/emojis/3/hotel.png
Binary files differ
diff --git a/public/-/emojis/3/hotsprings.png b/public/-/emojis/3/hotsprings.png
new file mode 100644
index 00000000000..a8e12e196de
--- /dev/null
+++ b/public/-/emojis/3/hotsprings.png
Binary files differ
diff --git a/public/-/emojis/3/hourglass.png b/public/-/emojis/3/hourglass.png
new file mode 100644
index 00000000000..ae55c33d237
--- /dev/null
+++ b/public/-/emojis/3/hourglass.png
Binary files differ
diff --git a/public/-/emojis/3/hourglass_flowing_sand.png b/public/-/emojis/3/hourglass_flowing_sand.png
new file mode 100644
index 00000000000..961f1769d32
--- /dev/null
+++ b/public/-/emojis/3/hourglass_flowing_sand.png
Binary files differ
diff --git a/public/-/emojis/3/house.png b/public/-/emojis/3/house.png
new file mode 100644
index 00000000000..a23121dad9e
--- /dev/null
+++ b/public/-/emojis/3/house.png
Binary files differ
diff --git a/public/-/emojis/3/house_abandoned.png b/public/-/emojis/3/house_abandoned.png
new file mode 100644
index 00000000000..147f8ddb361
--- /dev/null
+++ b/public/-/emojis/3/house_abandoned.png
Binary files differ
diff --git a/public/-/emojis/3/house_with_garden.png b/public/-/emojis/3/house_with_garden.png
new file mode 100644
index 00000000000..728d9791d33
--- /dev/null
+++ b/public/-/emojis/3/house_with_garden.png
Binary files differ
diff --git a/public/-/emojis/3/hugging.png b/public/-/emojis/3/hugging.png
new file mode 100644
index 00000000000..06afc6c7350
--- /dev/null
+++ b/public/-/emojis/3/hugging.png
Binary files differ
diff --git a/public/-/emojis/3/hushed.png b/public/-/emojis/3/hushed.png
new file mode 100644
index 00000000000..9d02a806052
--- /dev/null
+++ b/public/-/emojis/3/hushed.png
Binary files differ
diff --git a/public/-/emojis/3/ice_cream.png b/public/-/emojis/3/ice_cream.png
new file mode 100644
index 00000000000..15888c9e9d9
--- /dev/null
+++ b/public/-/emojis/3/ice_cream.png
Binary files differ
diff --git a/public/-/emojis/3/ice_skate.png b/public/-/emojis/3/ice_skate.png
new file mode 100644
index 00000000000..60b7dcba437
--- /dev/null
+++ b/public/-/emojis/3/ice_skate.png
Binary files differ
diff --git a/public/-/emojis/3/icecream.png b/public/-/emojis/3/icecream.png
new file mode 100644
index 00000000000..a63c4b900da
--- /dev/null
+++ b/public/-/emojis/3/icecream.png
Binary files differ
diff --git a/public/-/emojis/3/id.png b/public/-/emojis/3/id.png
new file mode 100644
index 00000000000..29d43ac5eb4
--- /dev/null
+++ b/public/-/emojis/3/id.png
Binary files differ
diff --git a/public/-/emojis/3/ideograph_advantage.png b/public/-/emojis/3/ideograph_advantage.png
new file mode 100644
index 00000000000..95c62036a17
--- /dev/null
+++ b/public/-/emojis/3/ideograph_advantage.png
Binary files differ
diff --git a/public/-/emojis/3/imp.png b/public/-/emojis/3/imp.png
new file mode 100644
index 00000000000..9d6c71af9b0
--- /dev/null
+++ b/public/-/emojis/3/imp.png
Binary files differ
diff --git a/public/-/emojis/3/inbox_tray.png b/public/-/emojis/3/inbox_tray.png
new file mode 100644
index 00000000000..2a34335455d
--- /dev/null
+++ b/public/-/emojis/3/inbox_tray.png
Binary files differ
diff --git a/public/-/emojis/3/incoming_envelope.png b/public/-/emojis/3/incoming_envelope.png
new file mode 100644
index 00000000000..7febff3bfe2
--- /dev/null
+++ b/public/-/emojis/3/incoming_envelope.png
Binary files differ
diff --git a/public/-/emojis/3/information_desk_person.png b/public/-/emojis/3/information_desk_person.png
new file mode 100644
index 00000000000..396f4f0a489
--- /dev/null
+++ b/public/-/emojis/3/information_desk_person.png
Binary files differ
diff --git a/public/-/emojis/3/information_desk_person_tone1.png b/public/-/emojis/3/information_desk_person_tone1.png
new file mode 100644
index 00000000000..fec768939e2
--- /dev/null
+++ b/public/-/emojis/3/information_desk_person_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/information_desk_person_tone2.png b/public/-/emojis/3/information_desk_person_tone2.png
new file mode 100644
index 00000000000..bd086a61d48
--- /dev/null
+++ b/public/-/emojis/3/information_desk_person_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/information_desk_person_tone3.png b/public/-/emojis/3/information_desk_person_tone3.png
new file mode 100644
index 00000000000..13557e8c32e
--- /dev/null
+++ b/public/-/emojis/3/information_desk_person_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/information_desk_person_tone4.png b/public/-/emojis/3/information_desk_person_tone4.png
new file mode 100644
index 00000000000..a91d0b5835e
--- /dev/null
+++ b/public/-/emojis/3/information_desk_person_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/information_desk_person_tone5.png b/public/-/emojis/3/information_desk_person_tone5.png
new file mode 100644
index 00000000000..4acb6992b4c
--- /dev/null
+++ b/public/-/emojis/3/information_desk_person_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/information_source.png b/public/-/emojis/3/information_source.png
new file mode 100644
index 00000000000..0b9dc75c950
--- /dev/null
+++ b/public/-/emojis/3/information_source.png
Binary files differ
diff --git a/public/-/emojis/3/innocent.png b/public/-/emojis/3/innocent.png
new file mode 100644
index 00000000000..297404a8b92
--- /dev/null
+++ b/public/-/emojis/3/innocent.png
Binary files differ
diff --git a/public/-/emojis/3/interrobang.png b/public/-/emojis/3/interrobang.png
new file mode 100644
index 00000000000..9b2875cea93
--- /dev/null
+++ b/public/-/emojis/3/interrobang.png
Binary files differ
diff --git a/public/-/emojis/3/iphone.png b/public/-/emojis/3/iphone.png
new file mode 100644
index 00000000000..2a333235c71
--- /dev/null
+++ b/public/-/emojis/3/iphone.png
Binary files differ
diff --git a/public/-/emojis/3/island.png b/public/-/emojis/3/island.png
new file mode 100644
index 00000000000..87d13274924
--- /dev/null
+++ b/public/-/emojis/3/island.png
Binary files differ
diff --git a/public/-/emojis/3/izakaya_lantern.png b/public/-/emojis/3/izakaya_lantern.png
new file mode 100644
index 00000000000..c180a2ff957
--- /dev/null
+++ b/public/-/emojis/3/izakaya_lantern.png
Binary files differ
diff --git a/public/-/emojis/3/jack_o_lantern.png b/public/-/emojis/3/jack_o_lantern.png
new file mode 100644
index 00000000000..6b4b5fd3360
--- /dev/null
+++ b/public/-/emojis/3/jack_o_lantern.png
Binary files differ
diff --git a/public/-/emojis/3/japan.png b/public/-/emojis/3/japan.png
new file mode 100644
index 00000000000..98dcb48b15d
--- /dev/null
+++ b/public/-/emojis/3/japan.png
Binary files differ
diff --git a/public/-/emojis/3/japanese_castle.png b/public/-/emojis/3/japanese_castle.png
new file mode 100644
index 00000000000..6b133365e3b
--- /dev/null
+++ b/public/-/emojis/3/japanese_castle.png
Binary files differ
diff --git a/public/-/emojis/3/japanese_goblin.png b/public/-/emojis/3/japanese_goblin.png
new file mode 100644
index 00000000000..1c0a10aa2fc
--- /dev/null
+++ b/public/-/emojis/3/japanese_goblin.png
Binary files differ
diff --git a/public/-/emojis/3/japanese_ogre.png b/public/-/emojis/3/japanese_ogre.png
new file mode 100644
index 00000000000..6d4f4aa7f45
--- /dev/null
+++ b/public/-/emojis/3/japanese_ogre.png
Binary files differ
diff --git a/public/-/emojis/3/jeans.png b/public/-/emojis/3/jeans.png
new file mode 100644
index 00000000000..2bd8122c90f
--- /dev/null
+++ b/public/-/emojis/3/jeans.png
Binary files differ
diff --git a/public/-/emojis/3/joy.png b/public/-/emojis/3/joy.png
new file mode 100644
index 00000000000..3a1103803c4
--- /dev/null
+++ b/public/-/emojis/3/joy.png
Binary files differ
diff --git a/public/-/emojis/3/joy_cat.png b/public/-/emojis/3/joy_cat.png
new file mode 100644
index 00000000000..0c3c1be3b6d
--- /dev/null
+++ b/public/-/emojis/3/joy_cat.png
Binary files differ
diff --git a/public/-/emojis/3/joystick.png b/public/-/emojis/3/joystick.png
new file mode 100644
index 00000000000..ace46cb751f
--- /dev/null
+++ b/public/-/emojis/3/joystick.png
Binary files differ
diff --git a/public/-/emojis/3/juggling.png b/public/-/emojis/3/juggling.png
new file mode 100644
index 00000000000..50f2275aac0
--- /dev/null
+++ b/public/-/emojis/3/juggling.png
Binary files differ
diff --git a/public/-/emojis/3/juggling_tone1.png b/public/-/emojis/3/juggling_tone1.png
new file mode 100644
index 00000000000..acb9056a2a4
--- /dev/null
+++ b/public/-/emojis/3/juggling_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/juggling_tone2.png b/public/-/emojis/3/juggling_tone2.png
new file mode 100644
index 00000000000..c2809447a80
--- /dev/null
+++ b/public/-/emojis/3/juggling_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/juggling_tone3.png b/public/-/emojis/3/juggling_tone3.png
new file mode 100644
index 00000000000..625e0d90506
--- /dev/null
+++ b/public/-/emojis/3/juggling_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/juggling_tone4.png b/public/-/emojis/3/juggling_tone4.png
new file mode 100644
index 00000000000..ceb628ff36e
--- /dev/null
+++ b/public/-/emojis/3/juggling_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/juggling_tone5.png b/public/-/emojis/3/juggling_tone5.png
new file mode 100644
index 00000000000..fc1ff36a024
--- /dev/null
+++ b/public/-/emojis/3/juggling_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/kaaba.png b/public/-/emojis/3/kaaba.png
new file mode 100644
index 00000000000..c49eb875d09
--- /dev/null
+++ b/public/-/emojis/3/kaaba.png
Binary files differ
diff --git a/public/-/emojis/3/key.png b/public/-/emojis/3/key.png
new file mode 100644
index 00000000000..26ef5c33cdf
--- /dev/null
+++ b/public/-/emojis/3/key.png
Binary files differ
diff --git a/public/-/emojis/3/key2.png b/public/-/emojis/3/key2.png
new file mode 100644
index 00000000000..abd66dbc688
--- /dev/null
+++ b/public/-/emojis/3/key2.png
Binary files differ
diff --git a/public/-/emojis/3/keyboard.png b/public/-/emojis/3/keyboard.png
new file mode 100644
index 00000000000..f1f9d350b39
--- /dev/null
+++ b/public/-/emojis/3/keyboard.png
Binary files differ
diff --git a/public/-/emojis/3/kimono.png b/public/-/emojis/3/kimono.png
new file mode 100644
index 00000000000..c04d88fc1ce
--- /dev/null
+++ b/public/-/emojis/3/kimono.png
Binary files differ
diff --git a/public/-/emojis/3/kiss.png b/public/-/emojis/3/kiss.png
new file mode 100644
index 00000000000..4d30dff10d9
--- /dev/null
+++ b/public/-/emojis/3/kiss.png
Binary files differ
diff --git a/public/-/emojis/3/kiss_mm.png b/public/-/emojis/3/kiss_mm.png
new file mode 100644
index 00000000000..9b49c4b409d
--- /dev/null
+++ b/public/-/emojis/3/kiss_mm.png
Binary files differ
diff --git a/public/-/emojis/3/kiss_ww.png b/public/-/emojis/3/kiss_ww.png
new file mode 100644
index 00000000000..678857c5109
--- /dev/null
+++ b/public/-/emojis/3/kiss_ww.png
Binary files differ
diff --git a/public/-/emojis/3/kissing.png b/public/-/emojis/3/kissing.png
new file mode 100644
index 00000000000..bb176638d8d
--- /dev/null
+++ b/public/-/emojis/3/kissing.png
Binary files differ
diff --git a/public/-/emojis/3/kissing_cat.png b/public/-/emojis/3/kissing_cat.png
new file mode 100644
index 00000000000..cc47ec15537
--- /dev/null
+++ b/public/-/emojis/3/kissing_cat.png
Binary files differ
diff --git a/public/-/emojis/3/kissing_closed_eyes.png b/public/-/emojis/3/kissing_closed_eyes.png
new file mode 100644
index 00000000000..3422c63253b
--- /dev/null
+++ b/public/-/emojis/3/kissing_closed_eyes.png
Binary files differ
diff --git a/public/-/emojis/3/kissing_heart.png b/public/-/emojis/3/kissing_heart.png
new file mode 100644
index 00000000000..0427b706194
--- /dev/null
+++ b/public/-/emojis/3/kissing_heart.png
Binary files differ
diff --git a/public/-/emojis/3/kissing_smiling_eyes.png b/public/-/emojis/3/kissing_smiling_eyes.png
new file mode 100644
index 00000000000..6b00b2fbe17
--- /dev/null
+++ b/public/-/emojis/3/kissing_smiling_eyes.png
Binary files differ
diff --git a/public/-/emojis/3/kiwi.png b/public/-/emojis/3/kiwi.png
new file mode 100644
index 00000000000..d9cabbf03fe
--- /dev/null
+++ b/public/-/emojis/3/kiwi.png
Binary files differ
diff --git a/public/-/emojis/3/knife.png b/public/-/emojis/3/knife.png
new file mode 100644
index 00000000000..86c5a80ea04
--- /dev/null
+++ b/public/-/emojis/3/knife.png
Binary files differ
diff --git a/public/-/emojis/3/koala.png b/public/-/emojis/3/koala.png
new file mode 100644
index 00000000000..88c4947e513
--- /dev/null
+++ b/public/-/emojis/3/koala.png
Binary files differ
diff --git a/public/-/emojis/3/koko.png b/public/-/emojis/3/koko.png
new file mode 100644
index 00000000000..835d1351c13
--- /dev/null
+++ b/public/-/emojis/3/koko.png
Binary files differ
diff --git a/public/-/emojis/3/label.png b/public/-/emojis/3/label.png
new file mode 100644
index 00000000000..ff9ebf30690
--- /dev/null
+++ b/public/-/emojis/3/label.png
Binary files differ
diff --git a/public/-/emojis/3/large_blue_circle.png b/public/-/emojis/3/large_blue_circle.png
new file mode 100644
index 00000000000..13aca4ff995
--- /dev/null
+++ b/public/-/emojis/3/large_blue_circle.png
Binary files differ
diff --git a/public/-/emojis/3/large_blue_diamond.png b/public/-/emojis/3/large_blue_diamond.png
new file mode 100644
index 00000000000..501b85dedc0
--- /dev/null
+++ b/public/-/emojis/3/large_blue_diamond.png
Binary files differ
diff --git a/public/-/emojis/3/large_orange_diamond.png b/public/-/emojis/3/large_orange_diamond.png
new file mode 100644
index 00000000000..929f9132687
--- /dev/null
+++ b/public/-/emojis/3/large_orange_diamond.png
Binary files differ
diff --git a/public/-/emojis/3/last_quarter_moon.png b/public/-/emojis/3/last_quarter_moon.png
new file mode 100644
index 00000000000..a5e5b7ff320
--- /dev/null
+++ b/public/-/emojis/3/last_quarter_moon.png
Binary files differ
diff --git a/public/-/emojis/3/last_quarter_moon_with_face.png b/public/-/emojis/3/last_quarter_moon_with_face.png
new file mode 100644
index 00000000000..2f2ed5fd8f5
--- /dev/null
+++ b/public/-/emojis/3/last_quarter_moon_with_face.png
Binary files differ
diff --git a/public/-/emojis/3/laughing.png b/public/-/emojis/3/laughing.png
new file mode 100644
index 00000000000..56ace6c7f8d
--- /dev/null
+++ b/public/-/emojis/3/laughing.png
Binary files differ
diff --git a/public/-/emojis/3/leaves.png b/public/-/emojis/3/leaves.png
new file mode 100644
index 00000000000..ed2dcc893ea
--- /dev/null
+++ b/public/-/emojis/3/leaves.png
Binary files differ
diff --git a/public/-/emojis/3/ledger.png b/public/-/emojis/3/ledger.png
new file mode 100644
index 00000000000..bf8f09fb37f
--- /dev/null
+++ b/public/-/emojis/3/ledger.png
Binary files differ
diff --git a/public/-/emojis/3/left_facing_fist.png b/public/-/emojis/3/left_facing_fist.png
new file mode 100644
index 00000000000..9c906df503b
--- /dev/null
+++ b/public/-/emojis/3/left_facing_fist.png
Binary files differ
diff --git a/public/-/emojis/3/left_facing_fist_tone1.png b/public/-/emojis/3/left_facing_fist_tone1.png
new file mode 100644
index 00000000000..1135adf2b05
--- /dev/null
+++ b/public/-/emojis/3/left_facing_fist_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/left_facing_fist_tone2.png b/public/-/emojis/3/left_facing_fist_tone2.png
new file mode 100644
index 00000000000..b1632483ce9
--- /dev/null
+++ b/public/-/emojis/3/left_facing_fist_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/left_facing_fist_tone3.png b/public/-/emojis/3/left_facing_fist_tone3.png
new file mode 100644
index 00000000000..2209797d48c
--- /dev/null
+++ b/public/-/emojis/3/left_facing_fist_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/left_facing_fist_tone4.png b/public/-/emojis/3/left_facing_fist_tone4.png
new file mode 100644
index 00000000000..a5e3c48f821
--- /dev/null
+++ b/public/-/emojis/3/left_facing_fist_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/left_facing_fist_tone5.png b/public/-/emojis/3/left_facing_fist_tone5.png
new file mode 100644
index 00000000000..f9d58061e6f
--- /dev/null
+++ b/public/-/emojis/3/left_facing_fist_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/left_luggage.png b/public/-/emojis/3/left_luggage.png
new file mode 100644
index 00000000000..5a4c8014113
--- /dev/null
+++ b/public/-/emojis/3/left_luggage.png
Binary files differ
diff --git a/public/-/emojis/3/left_right_arrow.png b/public/-/emojis/3/left_right_arrow.png
new file mode 100644
index 00000000000..900ff12cc99
--- /dev/null
+++ b/public/-/emojis/3/left_right_arrow.png
Binary files differ
diff --git a/public/-/emojis/3/leftwards_arrow_with_hook.png b/public/-/emojis/3/leftwards_arrow_with_hook.png
new file mode 100644
index 00000000000..22b51a690a9
--- /dev/null
+++ b/public/-/emojis/3/leftwards_arrow_with_hook.png
Binary files differ
diff --git a/public/-/emojis/3/lemon.png b/public/-/emojis/3/lemon.png
new file mode 100644
index 00000000000..e0bab2a069d
--- /dev/null
+++ b/public/-/emojis/3/lemon.png
Binary files differ
diff --git a/public/-/emojis/3/leo.png b/public/-/emojis/3/leo.png
new file mode 100644
index 00000000000..a42fe8e6aa3
--- /dev/null
+++ b/public/-/emojis/3/leo.png
Binary files differ
diff --git a/public/-/emojis/3/leopard.png b/public/-/emojis/3/leopard.png
new file mode 100644
index 00000000000..37ea6bb759b
--- /dev/null
+++ b/public/-/emojis/3/leopard.png
Binary files differ
diff --git a/public/-/emojis/3/level_slider.png b/public/-/emojis/3/level_slider.png
new file mode 100644
index 00000000000..84ccab6e7d6
--- /dev/null
+++ b/public/-/emojis/3/level_slider.png
Binary files differ
diff --git a/public/-/emojis/3/levitate.png b/public/-/emojis/3/levitate.png
new file mode 100644
index 00000000000..bbbd63ec40f
--- /dev/null
+++ b/public/-/emojis/3/levitate.png
Binary files differ
diff --git a/public/-/emojis/3/libra.png b/public/-/emojis/3/libra.png
new file mode 100644
index 00000000000..791d13f8a45
--- /dev/null
+++ b/public/-/emojis/3/libra.png
Binary files differ
diff --git a/public/-/emojis/3/lifter.png b/public/-/emojis/3/lifter.png
new file mode 100644
index 00000000000..de24a15a5d9
--- /dev/null
+++ b/public/-/emojis/3/lifter.png
Binary files differ
diff --git a/public/-/emojis/3/lifter_tone1.png b/public/-/emojis/3/lifter_tone1.png
new file mode 100644
index 00000000000..fb3f3233ad1
--- /dev/null
+++ b/public/-/emojis/3/lifter_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/lifter_tone2.png b/public/-/emojis/3/lifter_tone2.png
new file mode 100644
index 00000000000..0355652ba15
--- /dev/null
+++ b/public/-/emojis/3/lifter_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/lifter_tone3.png b/public/-/emojis/3/lifter_tone3.png
new file mode 100644
index 00000000000..a0d8c97ee19
--- /dev/null
+++ b/public/-/emojis/3/lifter_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/lifter_tone4.png b/public/-/emojis/3/lifter_tone4.png
new file mode 100644
index 00000000000..2f6cc33e461
--- /dev/null
+++ b/public/-/emojis/3/lifter_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/lifter_tone5.png b/public/-/emojis/3/lifter_tone5.png
new file mode 100644
index 00000000000..77888ef91d1
--- /dev/null
+++ b/public/-/emojis/3/lifter_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/light_rail.png b/public/-/emojis/3/light_rail.png
new file mode 100644
index 00000000000..ae337d7ce54
--- /dev/null
+++ b/public/-/emojis/3/light_rail.png
Binary files differ
diff --git a/public/-/emojis/3/link.png b/public/-/emojis/3/link.png
new file mode 100644
index 00000000000..f1a7c11e9f0
--- /dev/null
+++ b/public/-/emojis/3/link.png
Binary files differ
diff --git a/public/-/emojis/3/lion_face.png b/public/-/emojis/3/lion_face.png
new file mode 100644
index 00000000000..42eac6464b3
--- /dev/null
+++ b/public/-/emojis/3/lion_face.png
Binary files differ
diff --git a/public/-/emojis/3/lips.png b/public/-/emojis/3/lips.png
new file mode 100644
index 00000000000..542c12a5594
--- /dev/null
+++ b/public/-/emojis/3/lips.png
Binary files differ
diff --git a/public/-/emojis/3/lipstick.png b/public/-/emojis/3/lipstick.png
new file mode 100644
index 00000000000..a58667d0c70
--- /dev/null
+++ b/public/-/emojis/3/lipstick.png
Binary files differ
diff --git a/public/-/emojis/3/lizard.png b/public/-/emojis/3/lizard.png
new file mode 100644
index 00000000000..1712def5e90
--- /dev/null
+++ b/public/-/emojis/3/lizard.png
Binary files differ
diff --git a/public/-/emojis/3/lock.png b/public/-/emojis/3/lock.png
new file mode 100644
index 00000000000..1d63bc87919
--- /dev/null
+++ b/public/-/emojis/3/lock.png
Binary files differ
diff --git a/public/-/emojis/3/lock_with_ink_pen.png b/public/-/emojis/3/lock_with_ink_pen.png
new file mode 100644
index 00000000000..fa9cf62c0f1
--- /dev/null
+++ b/public/-/emojis/3/lock_with_ink_pen.png
Binary files differ
diff --git a/public/-/emojis/3/lollipop.png b/public/-/emojis/3/lollipop.png
new file mode 100644
index 00000000000..0948101801d
--- /dev/null
+++ b/public/-/emojis/3/lollipop.png
Binary files differ
diff --git a/public/-/emojis/3/loop.png b/public/-/emojis/3/loop.png
new file mode 100644
index 00000000000..353fce5994e
--- /dev/null
+++ b/public/-/emojis/3/loop.png
Binary files differ
diff --git a/public/-/emojis/3/loud_sound.png b/public/-/emojis/3/loud_sound.png
new file mode 100644
index 00000000000..8150c3ae6ab
--- /dev/null
+++ b/public/-/emojis/3/loud_sound.png
Binary files differ
diff --git a/public/-/emojis/3/loudspeaker.png b/public/-/emojis/3/loudspeaker.png
new file mode 100644
index 00000000000..b73054a5cb9
--- /dev/null
+++ b/public/-/emojis/3/loudspeaker.png
Binary files differ
diff --git a/public/-/emojis/3/love_hotel.png b/public/-/emojis/3/love_hotel.png
new file mode 100644
index 00000000000..b0bc9b9150c
--- /dev/null
+++ b/public/-/emojis/3/love_hotel.png
Binary files differ
diff --git a/public/-/emojis/3/love_letter.png b/public/-/emojis/3/love_letter.png
new file mode 100644
index 00000000000..135bcf05119
--- /dev/null
+++ b/public/-/emojis/3/love_letter.png
Binary files differ
diff --git a/public/-/emojis/3/low_brightness.png b/public/-/emojis/3/low_brightness.png
new file mode 100644
index 00000000000..f444013da6e
--- /dev/null
+++ b/public/-/emojis/3/low_brightness.png
Binary files differ
diff --git a/public/-/emojis/3/lying_face.png b/public/-/emojis/3/lying_face.png
new file mode 100644
index 00000000000..97432b50d57
--- /dev/null
+++ b/public/-/emojis/3/lying_face.png
Binary files differ
diff --git a/public/-/emojis/3/m.png b/public/-/emojis/3/m.png
new file mode 100644
index 00000000000..3ee9e9fbc9c
--- /dev/null
+++ b/public/-/emojis/3/m.png
Binary files differ
diff --git a/public/-/emojis/3/mag.png b/public/-/emojis/3/mag.png
new file mode 100644
index 00000000000..2363ea1bb02
--- /dev/null
+++ b/public/-/emojis/3/mag.png
Binary files differ
diff --git a/public/-/emojis/3/mag_right.png b/public/-/emojis/3/mag_right.png
new file mode 100644
index 00000000000..9284b384810
--- /dev/null
+++ b/public/-/emojis/3/mag_right.png
Binary files differ
diff --git a/public/-/emojis/3/mahjong.png b/public/-/emojis/3/mahjong.png
new file mode 100644
index 00000000000..6e86f916fc4
--- /dev/null
+++ b/public/-/emojis/3/mahjong.png
Binary files differ
diff --git a/public/-/emojis/3/mailbox.png b/public/-/emojis/3/mailbox.png
new file mode 100644
index 00000000000..e6a6f24ddfa
--- /dev/null
+++ b/public/-/emojis/3/mailbox.png
Binary files differ
diff --git a/public/-/emojis/3/mailbox_closed.png b/public/-/emojis/3/mailbox_closed.png
new file mode 100644
index 00000000000..712b644754c
--- /dev/null
+++ b/public/-/emojis/3/mailbox_closed.png
Binary files differ
diff --git a/public/-/emojis/3/mailbox_with_mail.png b/public/-/emojis/3/mailbox_with_mail.png
new file mode 100644
index 00000000000..5b3acde9ed3
--- /dev/null
+++ b/public/-/emojis/3/mailbox_with_mail.png
Binary files differ
diff --git a/public/-/emojis/3/mailbox_with_no_mail.png b/public/-/emojis/3/mailbox_with_no_mail.png
new file mode 100644
index 00000000000..e91113c5b7d
--- /dev/null
+++ b/public/-/emojis/3/mailbox_with_no_mail.png
Binary files differ
diff --git a/public/-/emojis/3/man.png b/public/-/emojis/3/man.png
new file mode 100644
index 00000000000..1471371a28d
--- /dev/null
+++ b/public/-/emojis/3/man.png
Binary files differ
diff --git a/public/-/emojis/3/man_dancing.png b/public/-/emojis/3/man_dancing.png
new file mode 100644
index 00000000000..506607c93f1
--- /dev/null
+++ b/public/-/emojis/3/man_dancing.png
Binary files differ
diff --git a/public/-/emojis/3/man_dancing_tone1.png b/public/-/emojis/3/man_dancing_tone1.png
new file mode 100644
index 00000000000..42f547529c2
--- /dev/null
+++ b/public/-/emojis/3/man_dancing_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/man_dancing_tone2.png b/public/-/emojis/3/man_dancing_tone2.png
new file mode 100644
index 00000000000..2c3daaa08e2
--- /dev/null
+++ b/public/-/emojis/3/man_dancing_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/man_dancing_tone3.png b/public/-/emojis/3/man_dancing_tone3.png
new file mode 100644
index 00000000000..85c5092a13a
--- /dev/null
+++ b/public/-/emojis/3/man_dancing_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/man_dancing_tone4.png b/public/-/emojis/3/man_dancing_tone4.png
new file mode 100644
index 00000000000..98897f3aa8c
--- /dev/null
+++ b/public/-/emojis/3/man_dancing_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/man_dancing_tone5.png b/public/-/emojis/3/man_dancing_tone5.png
new file mode 100644
index 00000000000..16da682d8e3
--- /dev/null
+++ b/public/-/emojis/3/man_dancing_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/man_in_tuxedo.png b/public/-/emojis/3/man_in_tuxedo.png
new file mode 100644
index 00000000000..15cfc08f5c2
--- /dev/null
+++ b/public/-/emojis/3/man_in_tuxedo.png
Binary files differ
diff --git a/public/-/emojis/3/man_in_tuxedo_tone1.png b/public/-/emojis/3/man_in_tuxedo_tone1.png
new file mode 100644
index 00000000000..18a24c9571f
--- /dev/null
+++ b/public/-/emojis/3/man_in_tuxedo_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/man_in_tuxedo_tone2.png b/public/-/emojis/3/man_in_tuxedo_tone2.png
new file mode 100644
index 00000000000..2de8e0a8aa2
--- /dev/null
+++ b/public/-/emojis/3/man_in_tuxedo_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/man_in_tuxedo_tone3.png b/public/-/emojis/3/man_in_tuxedo_tone3.png
new file mode 100644
index 00000000000..35b10680ac3
--- /dev/null
+++ b/public/-/emojis/3/man_in_tuxedo_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/man_in_tuxedo_tone4.png b/public/-/emojis/3/man_in_tuxedo_tone4.png
new file mode 100644
index 00000000000..569bf7c6928
--- /dev/null
+++ b/public/-/emojis/3/man_in_tuxedo_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/man_in_tuxedo_tone5.png b/public/-/emojis/3/man_in_tuxedo_tone5.png
new file mode 100644
index 00000000000..d3f0518b4b7
--- /dev/null
+++ b/public/-/emojis/3/man_in_tuxedo_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/man_tone1.png b/public/-/emojis/3/man_tone1.png
new file mode 100644
index 00000000000..d7b3233ccfd
--- /dev/null
+++ b/public/-/emojis/3/man_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/man_tone2.png b/public/-/emojis/3/man_tone2.png
new file mode 100644
index 00000000000..4713c4e89a7
--- /dev/null
+++ b/public/-/emojis/3/man_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/man_tone3.png b/public/-/emojis/3/man_tone3.png
new file mode 100644
index 00000000000..4c2b28426be
--- /dev/null
+++ b/public/-/emojis/3/man_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/man_tone4.png b/public/-/emojis/3/man_tone4.png
new file mode 100644
index 00000000000..91f19b75c5e
--- /dev/null
+++ b/public/-/emojis/3/man_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/man_tone5.png b/public/-/emojis/3/man_tone5.png
new file mode 100644
index 00000000000..5fe2741efb8
--- /dev/null
+++ b/public/-/emojis/3/man_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/man_with_gua_pi_mao.png b/public/-/emojis/3/man_with_gua_pi_mao.png
new file mode 100644
index 00000000000..986a82d38cb
--- /dev/null
+++ b/public/-/emojis/3/man_with_gua_pi_mao.png
Binary files differ
diff --git a/public/-/emojis/3/man_with_gua_pi_mao_tone1.png b/public/-/emojis/3/man_with_gua_pi_mao_tone1.png
new file mode 100644
index 00000000000..6657368cc73
--- /dev/null
+++ b/public/-/emojis/3/man_with_gua_pi_mao_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/man_with_gua_pi_mao_tone2.png b/public/-/emojis/3/man_with_gua_pi_mao_tone2.png
new file mode 100644
index 00000000000..822a6b18309
--- /dev/null
+++ b/public/-/emojis/3/man_with_gua_pi_mao_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/man_with_gua_pi_mao_tone3.png b/public/-/emojis/3/man_with_gua_pi_mao_tone3.png
new file mode 100644
index 00000000000..ec53dc12c14
--- /dev/null
+++ b/public/-/emojis/3/man_with_gua_pi_mao_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/man_with_gua_pi_mao_tone4.png b/public/-/emojis/3/man_with_gua_pi_mao_tone4.png
new file mode 100644
index 00000000000..e454dcbf25f
--- /dev/null
+++ b/public/-/emojis/3/man_with_gua_pi_mao_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/man_with_gua_pi_mao_tone5.png b/public/-/emojis/3/man_with_gua_pi_mao_tone5.png
new file mode 100644
index 00000000000..15e5d9238e8
--- /dev/null
+++ b/public/-/emojis/3/man_with_gua_pi_mao_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/man_with_turban.png b/public/-/emojis/3/man_with_turban.png
new file mode 100644
index 00000000000..4fdf66b58be
--- /dev/null
+++ b/public/-/emojis/3/man_with_turban.png
Binary files differ
diff --git a/public/-/emojis/3/man_with_turban_tone1.png b/public/-/emojis/3/man_with_turban_tone1.png
new file mode 100644
index 00000000000..e018f241027
--- /dev/null
+++ b/public/-/emojis/3/man_with_turban_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/man_with_turban_tone2.png b/public/-/emojis/3/man_with_turban_tone2.png
new file mode 100644
index 00000000000..84fdcdeeeef
--- /dev/null
+++ b/public/-/emojis/3/man_with_turban_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/man_with_turban_tone3.png b/public/-/emojis/3/man_with_turban_tone3.png
new file mode 100644
index 00000000000..f3bb33a49c3
--- /dev/null
+++ b/public/-/emojis/3/man_with_turban_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/man_with_turban_tone4.png b/public/-/emojis/3/man_with_turban_tone4.png
new file mode 100644
index 00000000000..e718321068f
--- /dev/null
+++ b/public/-/emojis/3/man_with_turban_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/man_with_turban_tone5.png b/public/-/emojis/3/man_with_turban_tone5.png
new file mode 100644
index 00000000000..0c7c9c80150
--- /dev/null
+++ b/public/-/emojis/3/man_with_turban_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/mans_shoe.png b/public/-/emojis/3/mans_shoe.png
new file mode 100644
index 00000000000..e8bc1d237c0
--- /dev/null
+++ b/public/-/emojis/3/mans_shoe.png
Binary files differ
diff --git a/public/-/emojis/3/map.png b/public/-/emojis/3/map.png
new file mode 100644
index 00000000000..609a7da92d5
--- /dev/null
+++ b/public/-/emojis/3/map.png
Binary files differ
diff --git a/public/-/emojis/3/maple_leaf.png b/public/-/emojis/3/maple_leaf.png
new file mode 100644
index 00000000000..b79b9d4b8f8
--- /dev/null
+++ b/public/-/emojis/3/maple_leaf.png
Binary files differ
diff --git a/public/-/emojis/3/martial_arts_uniform.png b/public/-/emojis/3/martial_arts_uniform.png
new file mode 100644
index 00000000000..813c890ec96
--- /dev/null
+++ b/public/-/emojis/3/martial_arts_uniform.png
Binary files differ
diff --git a/public/-/emojis/3/mask.png b/public/-/emojis/3/mask.png
new file mode 100644
index 00000000000..fa1ef6f7191
--- /dev/null
+++ b/public/-/emojis/3/mask.png
Binary files differ
diff --git a/public/-/emojis/3/massage.png b/public/-/emojis/3/massage.png
new file mode 100644
index 00000000000..e2592063d1e
--- /dev/null
+++ b/public/-/emojis/3/massage.png
Binary files differ
diff --git a/public/-/emojis/3/massage_tone1.png b/public/-/emojis/3/massage_tone1.png
new file mode 100644
index 00000000000..cc9dcfea166
--- /dev/null
+++ b/public/-/emojis/3/massage_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/massage_tone2.png b/public/-/emojis/3/massage_tone2.png
new file mode 100644
index 00000000000..108603a7f79
--- /dev/null
+++ b/public/-/emojis/3/massage_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/massage_tone3.png b/public/-/emojis/3/massage_tone3.png
new file mode 100644
index 00000000000..5c81192b6ee
--- /dev/null
+++ b/public/-/emojis/3/massage_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/massage_tone4.png b/public/-/emojis/3/massage_tone4.png
new file mode 100644
index 00000000000..64f3bbb8086
--- /dev/null
+++ b/public/-/emojis/3/massage_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/massage_tone5.png b/public/-/emojis/3/massage_tone5.png
new file mode 100644
index 00000000000..738fde080ab
--- /dev/null
+++ b/public/-/emojis/3/massage_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/meat_on_bone.png b/public/-/emojis/3/meat_on_bone.png
new file mode 100644
index 00000000000..38e9b730868
--- /dev/null
+++ b/public/-/emojis/3/meat_on_bone.png
Binary files differ
diff --git a/public/-/emojis/3/medal.png b/public/-/emojis/3/medal.png
new file mode 100644
index 00000000000..0b8a681f8a3
--- /dev/null
+++ b/public/-/emojis/3/medal.png
Binary files differ
diff --git a/public/-/emojis/3/mega.png b/public/-/emojis/3/mega.png
new file mode 100644
index 00000000000..729f1d6062e
--- /dev/null
+++ b/public/-/emojis/3/mega.png
Binary files differ
diff --git a/public/-/emojis/3/melon.png b/public/-/emojis/3/melon.png
new file mode 100644
index 00000000000..4bf92d03233
--- /dev/null
+++ b/public/-/emojis/3/melon.png
Binary files differ
diff --git a/public/-/emojis/3/menorah.png b/public/-/emojis/3/menorah.png
new file mode 100644
index 00000000000..6acfdd9dfdd
--- /dev/null
+++ b/public/-/emojis/3/menorah.png
Binary files differ
diff --git a/public/-/emojis/3/mens.png b/public/-/emojis/3/mens.png
new file mode 100644
index 00000000000..fcdeea081d2
--- /dev/null
+++ b/public/-/emojis/3/mens.png
Binary files differ
diff --git a/public/-/emojis/3/metal.png b/public/-/emojis/3/metal.png
new file mode 100644
index 00000000000..aa16a05280f
--- /dev/null
+++ b/public/-/emojis/3/metal.png
Binary files differ
diff --git a/public/-/emojis/3/metal_tone1.png b/public/-/emojis/3/metal_tone1.png
new file mode 100644
index 00000000000..e21cf74fb44
--- /dev/null
+++ b/public/-/emojis/3/metal_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/metal_tone2.png b/public/-/emojis/3/metal_tone2.png
new file mode 100644
index 00000000000..8f3e405c249
--- /dev/null
+++ b/public/-/emojis/3/metal_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/metal_tone3.png b/public/-/emojis/3/metal_tone3.png
new file mode 100644
index 00000000000..17c7e61b1a1
--- /dev/null
+++ b/public/-/emojis/3/metal_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/metal_tone4.png b/public/-/emojis/3/metal_tone4.png
new file mode 100644
index 00000000000..377ee61c6b7
--- /dev/null
+++ b/public/-/emojis/3/metal_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/metal_tone5.png b/public/-/emojis/3/metal_tone5.png
new file mode 100644
index 00000000000..3538e4a0634
--- /dev/null
+++ b/public/-/emojis/3/metal_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/metro.png b/public/-/emojis/3/metro.png
new file mode 100644
index 00000000000..5b46fff041c
--- /dev/null
+++ b/public/-/emojis/3/metro.png
Binary files differ
diff --git a/public/-/emojis/3/microphone.png b/public/-/emojis/3/microphone.png
new file mode 100644
index 00000000000..2fa4b55cce5
--- /dev/null
+++ b/public/-/emojis/3/microphone.png
Binary files differ
diff --git a/public/-/emojis/3/microphone2.png b/public/-/emojis/3/microphone2.png
new file mode 100644
index 00000000000..8de74839976
--- /dev/null
+++ b/public/-/emojis/3/microphone2.png
Binary files differ
diff --git a/public/-/emojis/3/microscope.png b/public/-/emojis/3/microscope.png
new file mode 100644
index 00000000000..a43f3f862c3
--- /dev/null
+++ b/public/-/emojis/3/microscope.png
Binary files differ
diff --git a/public/-/emojis/3/middle_finger.png b/public/-/emojis/3/middle_finger.png
new file mode 100644
index 00000000000..cadd7de4b1e
--- /dev/null
+++ b/public/-/emojis/3/middle_finger.png
Binary files differ
diff --git a/public/-/emojis/3/middle_finger_tone1.png b/public/-/emojis/3/middle_finger_tone1.png
new file mode 100644
index 00000000000..2c2b63095c7
--- /dev/null
+++ b/public/-/emojis/3/middle_finger_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/middle_finger_tone2.png b/public/-/emojis/3/middle_finger_tone2.png
new file mode 100644
index 00000000000..33a47b9bd7f
--- /dev/null
+++ b/public/-/emojis/3/middle_finger_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/middle_finger_tone3.png b/public/-/emojis/3/middle_finger_tone3.png
new file mode 100644
index 00000000000..209224ab4e4
--- /dev/null
+++ b/public/-/emojis/3/middle_finger_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/middle_finger_tone4.png b/public/-/emojis/3/middle_finger_tone4.png
new file mode 100644
index 00000000000..438793f2ee0
--- /dev/null
+++ b/public/-/emojis/3/middle_finger_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/middle_finger_tone5.png b/public/-/emojis/3/middle_finger_tone5.png
new file mode 100644
index 00000000000..91eb77d1857
--- /dev/null
+++ b/public/-/emojis/3/middle_finger_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/military_medal.png b/public/-/emojis/3/military_medal.png
new file mode 100644
index 00000000000..e78c6fa87e0
--- /dev/null
+++ b/public/-/emojis/3/military_medal.png
Binary files differ
diff --git a/public/-/emojis/3/milk.png b/public/-/emojis/3/milk.png
new file mode 100644
index 00000000000..ea6fae69ed7
--- /dev/null
+++ b/public/-/emojis/3/milk.png
Binary files differ
diff --git a/public/-/emojis/3/milky_way.png b/public/-/emojis/3/milky_way.png
new file mode 100644
index 00000000000..b769796a96c
--- /dev/null
+++ b/public/-/emojis/3/milky_way.png
Binary files differ
diff --git a/public/-/emojis/3/minibus.png b/public/-/emojis/3/minibus.png
new file mode 100644
index 00000000000..799a2eeac72
--- /dev/null
+++ b/public/-/emojis/3/minibus.png
Binary files differ
diff --git a/public/-/emojis/3/minidisc.png b/public/-/emojis/3/minidisc.png
new file mode 100644
index 00000000000..7ee33d5be5e
--- /dev/null
+++ b/public/-/emojis/3/minidisc.png
Binary files differ
diff --git a/public/-/emojis/3/mobile_phone_off.png b/public/-/emojis/3/mobile_phone_off.png
new file mode 100644
index 00000000000..9fe93dffcb9
--- /dev/null
+++ b/public/-/emojis/3/mobile_phone_off.png
Binary files differ
diff --git a/public/-/emojis/3/money_mouth.png b/public/-/emojis/3/money_mouth.png
new file mode 100644
index 00000000000..9ec050c9c62
--- /dev/null
+++ b/public/-/emojis/3/money_mouth.png
Binary files differ
diff --git a/public/-/emojis/3/money_with_wings.png b/public/-/emojis/3/money_with_wings.png
new file mode 100644
index 00000000000..245b84584b2
--- /dev/null
+++ b/public/-/emojis/3/money_with_wings.png
Binary files differ
diff --git a/public/-/emojis/3/moneybag.png b/public/-/emojis/3/moneybag.png
new file mode 100644
index 00000000000..1d7a8cd2a02
--- /dev/null
+++ b/public/-/emojis/3/moneybag.png
Binary files differ
diff --git a/public/-/emojis/3/monkey.png b/public/-/emojis/3/monkey.png
new file mode 100644
index 00000000000..f8b4d6a6ccf
--- /dev/null
+++ b/public/-/emojis/3/monkey.png
Binary files differ
diff --git a/public/-/emojis/3/monkey_face.png b/public/-/emojis/3/monkey_face.png
new file mode 100644
index 00000000000..cbab205225d
--- /dev/null
+++ b/public/-/emojis/3/monkey_face.png
Binary files differ
diff --git a/public/-/emojis/3/monorail.png b/public/-/emojis/3/monorail.png
new file mode 100644
index 00000000000..71b9947f085
--- /dev/null
+++ b/public/-/emojis/3/monorail.png
Binary files differ
diff --git a/public/-/emojis/3/mortar_board.png b/public/-/emojis/3/mortar_board.png
new file mode 100644
index 00000000000..150e02378a6
--- /dev/null
+++ b/public/-/emojis/3/mortar_board.png
Binary files differ
diff --git a/public/-/emojis/3/mosque.png b/public/-/emojis/3/mosque.png
new file mode 100644
index 00000000000..cd8e6fac4cd
--- /dev/null
+++ b/public/-/emojis/3/mosque.png
Binary files differ
diff --git a/public/-/emojis/3/motor_scooter.png b/public/-/emojis/3/motor_scooter.png
new file mode 100644
index 00000000000..831d6ea9800
--- /dev/null
+++ b/public/-/emojis/3/motor_scooter.png
Binary files differ
diff --git a/public/-/emojis/3/motorboat.png b/public/-/emojis/3/motorboat.png
new file mode 100644
index 00000000000..9e8a605b814
--- /dev/null
+++ b/public/-/emojis/3/motorboat.png
Binary files differ
diff --git a/public/-/emojis/3/motorcycle.png b/public/-/emojis/3/motorcycle.png
new file mode 100644
index 00000000000..669a050d5b4
--- /dev/null
+++ b/public/-/emojis/3/motorcycle.png
Binary files differ
diff --git a/public/-/emojis/3/motorway.png b/public/-/emojis/3/motorway.png
new file mode 100644
index 00000000000..abb88ad9418
--- /dev/null
+++ b/public/-/emojis/3/motorway.png
Binary files differ
diff --git a/public/-/emojis/3/mount_fuji.png b/public/-/emojis/3/mount_fuji.png
new file mode 100644
index 00000000000..696499ed762
--- /dev/null
+++ b/public/-/emojis/3/mount_fuji.png
Binary files differ
diff --git a/public/-/emojis/3/mountain.png b/public/-/emojis/3/mountain.png
new file mode 100644
index 00000000000..3156894f243
--- /dev/null
+++ b/public/-/emojis/3/mountain.png
Binary files differ
diff --git a/public/-/emojis/3/mountain_bicyclist.png b/public/-/emojis/3/mountain_bicyclist.png
new file mode 100644
index 00000000000..724f811a817
--- /dev/null
+++ b/public/-/emojis/3/mountain_bicyclist.png
Binary files differ
diff --git a/public/-/emojis/3/mountain_bicyclist_tone1.png b/public/-/emojis/3/mountain_bicyclist_tone1.png
new file mode 100644
index 00000000000..c36a951c8b3
--- /dev/null
+++ b/public/-/emojis/3/mountain_bicyclist_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/mountain_bicyclist_tone2.png b/public/-/emojis/3/mountain_bicyclist_tone2.png
new file mode 100644
index 00000000000..e60847a02f0
--- /dev/null
+++ b/public/-/emojis/3/mountain_bicyclist_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/mountain_bicyclist_tone3.png b/public/-/emojis/3/mountain_bicyclist_tone3.png
new file mode 100644
index 00000000000..1f4e071bc5a
--- /dev/null
+++ b/public/-/emojis/3/mountain_bicyclist_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/mountain_bicyclist_tone4.png b/public/-/emojis/3/mountain_bicyclist_tone4.png
new file mode 100644
index 00000000000..b9698a9e76b
--- /dev/null
+++ b/public/-/emojis/3/mountain_bicyclist_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/mountain_bicyclist_tone5.png b/public/-/emojis/3/mountain_bicyclist_tone5.png
new file mode 100644
index 00000000000..79fe186755c
--- /dev/null
+++ b/public/-/emojis/3/mountain_bicyclist_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/mountain_cableway.png b/public/-/emojis/3/mountain_cableway.png
new file mode 100644
index 00000000000..6a23c7d3903
--- /dev/null
+++ b/public/-/emojis/3/mountain_cableway.png
Binary files differ
diff --git a/public/-/emojis/3/mountain_railway.png b/public/-/emojis/3/mountain_railway.png
new file mode 100644
index 00000000000..369b2e347da
--- /dev/null
+++ b/public/-/emojis/3/mountain_railway.png
Binary files differ
diff --git a/public/-/emojis/3/mountain_snow.png b/public/-/emojis/3/mountain_snow.png
new file mode 100644
index 00000000000..93a2a5f4157
--- /dev/null
+++ b/public/-/emojis/3/mountain_snow.png
Binary files differ
diff --git a/public/-/emojis/3/mouse.png b/public/-/emojis/3/mouse.png
new file mode 100644
index 00000000000..9250e474b02
--- /dev/null
+++ b/public/-/emojis/3/mouse.png
Binary files differ
diff --git a/public/-/emojis/3/mouse2.png b/public/-/emojis/3/mouse2.png
new file mode 100644
index 00000000000..bea98407263
--- /dev/null
+++ b/public/-/emojis/3/mouse2.png
Binary files differ
diff --git a/public/-/emojis/3/mouse_three_button.png b/public/-/emojis/3/mouse_three_button.png
new file mode 100644
index 00000000000..fe51db1fb37
--- /dev/null
+++ b/public/-/emojis/3/mouse_three_button.png
Binary files differ
diff --git a/public/-/emojis/3/movie_camera.png b/public/-/emojis/3/movie_camera.png
new file mode 100644
index 00000000000..7dff4477ade
--- /dev/null
+++ b/public/-/emojis/3/movie_camera.png
Binary files differ
diff --git a/public/-/emojis/3/moyai.png b/public/-/emojis/3/moyai.png
new file mode 100644
index 00000000000..1ab5cec7a08
--- /dev/null
+++ b/public/-/emojis/3/moyai.png
Binary files differ
diff --git a/public/-/emojis/3/mrs_claus.png b/public/-/emojis/3/mrs_claus.png
new file mode 100644
index 00000000000..f1680c9fb33
--- /dev/null
+++ b/public/-/emojis/3/mrs_claus.png
Binary files differ
diff --git a/public/-/emojis/3/mrs_claus_tone1.png b/public/-/emojis/3/mrs_claus_tone1.png
new file mode 100644
index 00000000000..26c7f27e75f
--- /dev/null
+++ b/public/-/emojis/3/mrs_claus_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/mrs_claus_tone2.png b/public/-/emojis/3/mrs_claus_tone2.png
new file mode 100644
index 00000000000..e9e0908ca0a
--- /dev/null
+++ b/public/-/emojis/3/mrs_claus_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/mrs_claus_tone3.png b/public/-/emojis/3/mrs_claus_tone3.png
new file mode 100644
index 00000000000..b464380985c
--- /dev/null
+++ b/public/-/emojis/3/mrs_claus_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/mrs_claus_tone4.png b/public/-/emojis/3/mrs_claus_tone4.png
new file mode 100644
index 00000000000..ac8a014dbd3
--- /dev/null
+++ b/public/-/emojis/3/mrs_claus_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/mrs_claus_tone5.png b/public/-/emojis/3/mrs_claus_tone5.png
new file mode 100644
index 00000000000..2d998498375
--- /dev/null
+++ b/public/-/emojis/3/mrs_claus_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/muscle.png b/public/-/emojis/3/muscle.png
new file mode 100644
index 00000000000..b40eccccba6
--- /dev/null
+++ b/public/-/emojis/3/muscle.png
Binary files differ
diff --git a/public/-/emojis/3/muscle_tone1.png b/public/-/emojis/3/muscle_tone1.png
new file mode 100644
index 00000000000..d971d331bfc
--- /dev/null
+++ b/public/-/emojis/3/muscle_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/muscle_tone2.png b/public/-/emojis/3/muscle_tone2.png
new file mode 100644
index 00000000000..dfb2c9697d3
--- /dev/null
+++ b/public/-/emojis/3/muscle_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/muscle_tone3.png b/public/-/emojis/3/muscle_tone3.png
new file mode 100644
index 00000000000..a528d2a27c7
--- /dev/null
+++ b/public/-/emojis/3/muscle_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/muscle_tone4.png b/public/-/emojis/3/muscle_tone4.png
new file mode 100644
index 00000000000..b3b5cd54e8b
--- /dev/null
+++ b/public/-/emojis/3/muscle_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/muscle_tone5.png b/public/-/emojis/3/muscle_tone5.png
new file mode 100644
index 00000000000..b98fa1b2af3
--- /dev/null
+++ b/public/-/emojis/3/muscle_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/mushroom.png b/public/-/emojis/3/mushroom.png
new file mode 100644
index 00000000000..989372ef8ce
--- /dev/null
+++ b/public/-/emojis/3/mushroom.png
Binary files differ
diff --git a/public/-/emojis/3/musical_keyboard.png b/public/-/emojis/3/musical_keyboard.png
new file mode 100644
index 00000000000..3b5bb2f0af2
--- /dev/null
+++ b/public/-/emojis/3/musical_keyboard.png
Binary files differ
diff --git a/public/-/emojis/3/musical_note.png b/public/-/emojis/3/musical_note.png
new file mode 100644
index 00000000000..7316152de60
--- /dev/null
+++ b/public/-/emojis/3/musical_note.png
Binary files differ
diff --git a/public/-/emojis/3/musical_score.png b/public/-/emojis/3/musical_score.png
new file mode 100644
index 00000000000..d3ec8b4a9cb
--- /dev/null
+++ b/public/-/emojis/3/musical_score.png
Binary files differ
diff --git a/public/-/emojis/3/mute.png b/public/-/emojis/3/mute.png
new file mode 100644
index 00000000000..62b1ab9dbbb
--- /dev/null
+++ b/public/-/emojis/3/mute.png
Binary files differ
diff --git a/public/-/emojis/3/nail_care.png b/public/-/emojis/3/nail_care.png
new file mode 100644
index 00000000000..58af69b130e
--- /dev/null
+++ b/public/-/emojis/3/nail_care.png
Binary files differ
diff --git a/public/-/emojis/3/nail_care_tone1.png b/public/-/emojis/3/nail_care_tone1.png
new file mode 100644
index 00000000000..127e7311df1
--- /dev/null
+++ b/public/-/emojis/3/nail_care_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/nail_care_tone2.png b/public/-/emojis/3/nail_care_tone2.png
new file mode 100644
index 00000000000..7463b7f36f7
--- /dev/null
+++ b/public/-/emojis/3/nail_care_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/nail_care_tone3.png b/public/-/emojis/3/nail_care_tone3.png
new file mode 100644
index 00000000000..335c23402e1
--- /dev/null
+++ b/public/-/emojis/3/nail_care_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/nail_care_tone4.png b/public/-/emojis/3/nail_care_tone4.png
new file mode 100644
index 00000000000..516732c6e6c
--- /dev/null
+++ b/public/-/emojis/3/nail_care_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/nail_care_tone5.png b/public/-/emojis/3/nail_care_tone5.png
new file mode 100644
index 00000000000..40beb2c2bf5
--- /dev/null
+++ b/public/-/emojis/3/nail_care_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/name_badge.png b/public/-/emojis/3/name_badge.png
new file mode 100644
index 00000000000..7a6314f3470
--- /dev/null
+++ b/public/-/emojis/3/name_badge.png
Binary files differ
diff --git a/public/-/emojis/3/nauseated_face.png b/public/-/emojis/3/nauseated_face.png
new file mode 100644
index 00000000000..aff0287a9bf
--- /dev/null
+++ b/public/-/emojis/3/nauseated_face.png
Binary files differ
diff --git a/public/-/emojis/3/necktie.png b/public/-/emojis/3/necktie.png
new file mode 100644
index 00000000000..e2fcfe233a1
--- /dev/null
+++ b/public/-/emojis/3/necktie.png
Binary files differ
diff --git a/public/-/emojis/3/negative_squared_cross_mark.png b/public/-/emojis/3/negative_squared_cross_mark.png
new file mode 100644
index 00000000000..9165fb916e5
--- /dev/null
+++ b/public/-/emojis/3/negative_squared_cross_mark.png
Binary files differ
diff --git a/public/-/emojis/3/nerd.png b/public/-/emojis/3/nerd.png
new file mode 100644
index 00000000000..f0b8d4807a6
--- /dev/null
+++ b/public/-/emojis/3/nerd.png
Binary files differ
diff --git a/public/-/emojis/3/neutral_face.png b/public/-/emojis/3/neutral_face.png
new file mode 100644
index 00000000000..4dca04fdfc4
--- /dev/null
+++ b/public/-/emojis/3/neutral_face.png
Binary files differ
diff --git a/public/-/emojis/3/new.png b/public/-/emojis/3/new.png
new file mode 100644
index 00000000000..fc11fea7251
--- /dev/null
+++ b/public/-/emojis/3/new.png
Binary files differ
diff --git a/public/-/emojis/3/new_moon.png b/public/-/emojis/3/new_moon.png
new file mode 100644
index 00000000000..699e2283aaa
--- /dev/null
+++ b/public/-/emojis/3/new_moon.png
Binary files differ
diff --git a/public/-/emojis/3/new_moon_with_face.png b/public/-/emojis/3/new_moon_with_face.png
new file mode 100644
index 00000000000..5863644ce97
--- /dev/null
+++ b/public/-/emojis/3/new_moon_with_face.png
Binary files differ
diff --git a/public/-/emojis/3/newspaper.png b/public/-/emojis/3/newspaper.png
new file mode 100644
index 00000000000..1b72b5592a4
--- /dev/null
+++ b/public/-/emojis/3/newspaper.png
Binary files differ
diff --git a/public/-/emojis/3/newspaper2.png b/public/-/emojis/3/newspaper2.png
new file mode 100644
index 00000000000..13e9f54d42b
--- /dev/null
+++ b/public/-/emojis/3/newspaper2.png
Binary files differ
diff --git a/public/-/emojis/3/ng.png b/public/-/emojis/3/ng.png
new file mode 100644
index 00000000000..4ccf0e104c1
--- /dev/null
+++ b/public/-/emojis/3/ng.png
Binary files differ
diff --git a/public/-/emojis/3/night_with_stars.png b/public/-/emojis/3/night_with_stars.png
new file mode 100644
index 00000000000..3c44cb2308e
--- /dev/null
+++ b/public/-/emojis/3/night_with_stars.png
Binary files differ
diff --git a/public/-/emojis/3/nine.png b/public/-/emojis/3/nine.png
new file mode 100644
index 00000000000..178adebc883
--- /dev/null
+++ b/public/-/emojis/3/nine.png
Binary files differ
diff --git a/public/-/emojis/3/no_bell.png b/public/-/emojis/3/no_bell.png
new file mode 100644
index 00000000000..70800056224
--- /dev/null
+++ b/public/-/emojis/3/no_bell.png
Binary files differ
diff --git a/public/-/emojis/3/no_bicycles.png b/public/-/emojis/3/no_bicycles.png
new file mode 100644
index 00000000000..e77283eec67
--- /dev/null
+++ b/public/-/emojis/3/no_bicycles.png
Binary files differ
diff --git a/public/-/emojis/3/no_entry.png b/public/-/emojis/3/no_entry.png
new file mode 100644
index 00000000000..5fe52435ba3
--- /dev/null
+++ b/public/-/emojis/3/no_entry.png
Binary files differ
diff --git a/public/-/emojis/3/no_entry_sign.png b/public/-/emojis/3/no_entry_sign.png
new file mode 100644
index 00000000000..d36a461aaef
--- /dev/null
+++ b/public/-/emojis/3/no_entry_sign.png
Binary files differ
diff --git a/public/-/emojis/3/no_good.png b/public/-/emojis/3/no_good.png
new file mode 100644
index 00000000000..0e7a9ca57c5
--- /dev/null
+++ b/public/-/emojis/3/no_good.png
Binary files differ
diff --git a/public/-/emojis/3/no_good_tone1.png b/public/-/emojis/3/no_good_tone1.png
new file mode 100644
index 00000000000..054522577a9
--- /dev/null
+++ b/public/-/emojis/3/no_good_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/no_good_tone2.png b/public/-/emojis/3/no_good_tone2.png
new file mode 100644
index 00000000000..7b002ccddd9
--- /dev/null
+++ b/public/-/emojis/3/no_good_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/no_good_tone3.png b/public/-/emojis/3/no_good_tone3.png
new file mode 100644
index 00000000000..39bb63f75e4
--- /dev/null
+++ b/public/-/emojis/3/no_good_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/no_good_tone4.png b/public/-/emojis/3/no_good_tone4.png
new file mode 100644
index 00000000000..e0d16713730
--- /dev/null
+++ b/public/-/emojis/3/no_good_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/no_good_tone5.png b/public/-/emojis/3/no_good_tone5.png
new file mode 100644
index 00000000000..cc7c1829b96
--- /dev/null
+++ b/public/-/emojis/3/no_good_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/no_mobile_phones.png b/public/-/emojis/3/no_mobile_phones.png
new file mode 100644
index 00000000000..8e375d76df0
--- /dev/null
+++ b/public/-/emojis/3/no_mobile_phones.png
Binary files differ
diff --git a/public/-/emojis/3/no_mouth.png b/public/-/emojis/3/no_mouth.png
new file mode 100644
index 00000000000..1e67ba3e048
--- /dev/null
+++ b/public/-/emojis/3/no_mouth.png
Binary files differ
diff --git a/public/-/emojis/3/no_pedestrians.png b/public/-/emojis/3/no_pedestrians.png
new file mode 100644
index 00000000000..3673e73d99c
--- /dev/null
+++ b/public/-/emojis/3/no_pedestrians.png
Binary files differ
diff --git a/public/-/emojis/3/no_smoking.png b/public/-/emojis/3/no_smoking.png
new file mode 100644
index 00000000000..28df4476795
--- /dev/null
+++ b/public/-/emojis/3/no_smoking.png
Binary files differ
diff --git a/public/-/emojis/3/non-potable_water.png b/public/-/emojis/3/non-potable_water.png
new file mode 100644
index 00000000000..c3a838989df
--- /dev/null
+++ b/public/-/emojis/3/non-potable_water.png
Binary files differ
diff --git a/public/-/emojis/3/nose.png b/public/-/emojis/3/nose.png
new file mode 100644
index 00000000000..d554d8266de
--- /dev/null
+++ b/public/-/emojis/3/nose.png
Binary files differ
diff --git a/public/-/emojis/3/nose_tone1.png b/public/-/emojis/3/nose_tone1.png
new file mode 100644
index 00000000000..bb9b0e32c0e
--- /dev/null
+++ b/public/-/emojis/3/nose_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/nose_tone2.png b/public/-/emojis/3/nose_tone2.png
new file mode 100644
index 00000000000..ab33c2f7856
--- /dev/null
+++ b/public/-/emojis/3/nose_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/nose_tone3.png b/public/-/emojis/3/nose_tone3.png
new file mode 100644
index 00000000000..5e21cce930d
--- /dev/null
+++ b/public/-/emojis/3/nose_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/nose_tone4.png b/public/-/emojis/3/nose_tone4.png
new file mode 100644
index 00000000000..edff6c9b76f
--- /dev/null
+++ b/public/-/emojis/3/nose_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/nose_tone5.png b/public/-/emojis/3/nose_tone5.png
new file mode 100644
index 00000000000..be4fb9dd7aa
--- /dev/null
+++ b/public/-/emojis/3/nose_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/notebook.png b/public/-/emojis/3/notebook.png
new file mode 100644
index 00000000000..71ea5926049
--- /dev/null
+++ b/public/-/emojis/3/notebook.png
Binary files differ
diff --git a/public/-/emojis/3/notebook_with_decorative_cover.png b/public/-/emojis/3/notebook_with_decorative_cover.png
new file mode 100644
index 00000000000..9c19f162a5f
--- /dev/null
+++ b/public/-/emojis/3/notebook_with_decorative_cover.png
Binary files differ
diff --git a/public/-/emojis/3/notepad_spiral.png b/public/-/emojis/3/notepad_spiral.png
new file mode 100644
index 00000000000..cd157e1f075
--- /dev/null
+++ b/public/-/emojis/3/notepad_spiral.png
Binary files differ
diff --git a/public/-/emojis/3/notes.png b/public/-/emojis/3/notes.png
new file mode 100644
index 00000000000..b2ccdb4fdfe
--- /dev/null
+++ b/public/-/emojis/3/notes.png
Binary files differ
diff --git a/public/-/emojis/3/nut_and_bolt.png b/public/-/emojis/3/nut_and_bolt.png
new file mode 100644
index 00000000000..12f315b42dc
--- /dev/null
+++ b/public/-/emojis/3/nut_and_bolt.png
Binary files differ
diff --git a/public/-/emojis/3/o.png b/public/-/emojis/3/o.png
new file mode 100644
index 00000000000..60d3a67b7a1
--- /dev/null
+++ b/public/-/emojis/3/o.png
Binary files differ
diff --git a/public/-/emojis/3/o2.png b/public/-/emojis/3/o2.png
new file mode 100644
index 00000000000..396f10bc7a1
--- /dev/null
+++ b/public/-/emojis/3/o2.png
Binary files differ
diff --git a/public/-/emojis/3/ocean.png b/public/-/emojis/3/ocean.png
new file mode 100644
index 00000000000..326120f06ae
--- /dev/null
+++ b/public/-/emojis/3/ocean.png
Binary files differ
diff --git a/public/-/emojis/3/octagonal_sign.png b/public/-/emojis/3/octagonal_sign.png
new file mode 100644
index 00000000000..909917c79be
--- /dev/null
+++ b/public/-/emojis/3/octagonal_sign.png
Binary files differ
diff --git a/public/-/emojis/3/octopus.png b/public/-/emojis/3/octopus.png
new file mode 100644
index 00000000000..6e02be6bc65
--- /dev/null
+++ b/public/-/emojis/3/octopus.png
Binary files differ
diff --git a/public/-/emojis/3/oden.png b/public/-/emojis/3/oden.png
new file mode 100644
index 00000000000..8705452827f
--- /dev/null
+++ b/public/-/emojis/3/oden.png
Binary files differ
diff --git a/public/-/emojis/3/office.png b/public/-/emojis/3/office.png
new file mode 100644
index 00000000000..efc1b7fe17a
--- /dev/null
+++ b/public/-/emojis/3/office.png
Binary files differ
diff --git a/public/-/emojis/3/oil.png b/public/-/emojis/3/oil.png
new file mode 100644
index 00000000000..8141f0f4d77
--- /dev/null
+++ b/public/-/emojis/3/oil.png
Binary files differ
diff --git a/public/-/emojis/3/ok.png b/public/-/emojis/3/ok.png
new file mode 100644
index 00000000000..7ac40b38704
--- /dev/null
+++ b/public/-/emojis/3/ok.png
Binary files differ
diff --git a/public/-/emojis/3/ok_hand.png b/public/-/emojis/3/ok_hand.png
new file mode 100644
index 00000000000..cefc5583e4b
--- /dev/null
+++ b/public/-/emojis/3/ok_hand.png
Binary files differ
diff --git a/public/-/emojis/3/ok_hand_tone1.png b/public/-/emojis/3/ok_hand_tone1.png
new file mode 100644
index 00000000000..6ab2c4a0b30
--- /dev/null
+++ b/public/-/emojis/3/ok_hand_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/ok_hand_tone2.png b/public/-/emojis/3/ok_hand_tone2.png
new file mode 100644
index 00000000000..66d1f9e18a1
--- /dev/null
+++ b/public/-/emojis/3/ok_hand_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/ok_hand_tone3.png b/public/-/emojis/3/ok_hand_tone3.png
new file mode 100644
index 00000000000..d621900f510
--- /dev/null
+++ b/public/-/emojis/3/ok_hand_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/ok_hand_tone4.png b/public/-/emojis/3/ok_hand_tone4.png
new file mode 100644
index 00000000000..e5a04d44ff5
--- /dev/null
+++ b/public/-/emojis/3/ok_hand_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/ok_hand_tone5.png b/public/-/emojis/3/ok_hand_tone5.png
new file mode 100644
index 00000000000..2f8ee905c60
--- /dev/null
+++ b/public/-/emojis/3/ok_hand_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/ok_woman.png b/public/-/emojis/3/ok_woman.png
new file mode 100644
index 00000000000..13ba0e57483
--- /dev/null
+++ b/public/-/emojis/3/ok_woman.png
Binary files differ
diff --git a/public/-/emojis/3/ok_woman_tone1.png b/public/-/emojis/3/ok_woman_tone1.png
new file mode 100644
index 00000000000..5ed4b830368
--- /dev/null
+++ b/public/-/emojis/3/ok_woman_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/ok_woman_tone2.png b/public/-/emojis/3/ok_woman_tone2.png
new file mode 100644
index 00000000000..9ccd187d96f
--- /dev/null
+++ b/public/-/emojis/3/ok_woman_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/ok_woman_tone3.png b/public/-/emojis/3/ok_woman_tone3.png
new file mode 100644
index 00000000000..fa03cb6d59a
--- /dev/null
+++ b/public/-/emojis/3/ok_woman_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/ok_woman_tone4.png b/public/-/emojis/3/ok_woman_tone4.png
new file mode 100644
index 00000000000..d0dde591096
--- /dev/null
+++ b/public/-/emojis/3/ok_woman_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/ok_woman_tone5.png b/public/-/emojis/3/ok_woman_tone5.png
new file mode 100644
index 00000000000..7bba2590a87
--- /dev/null
+++ b/public/-/emojis/3/ok_woman_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/older_man.png b/public/-/emojis/3/older_man.png
new file mode 100644
index 00000000000..fdb37b455b0
--- /dev/null
+++ b/public/-/emojis/3/older_man.png
Binary files differ
diff --git a/public/-/emojis/3/older_man_tone1.png b/public/-/emojis/3/older_man_tone1.png
new file mode 100644
index 00000000000..282992bb73e
--- /dev/null
+++ b/public/-/emojis/3/older_man_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/older_man_tone2.png b/public/-/emojis/3/older_man_tone2.png
new file mode 100644
index 00000000000..28c013c40f8
--- /dev/null
+++ b/public/-/emojis/3/older_man_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/older_man_tone3.png b/public/-/emojis/3/older_man_tone3.png
new file mode 100644
index 00000000000..7af90cd6ea8
--- /dev/null
+++ b/public/-/emojis/3/older_man_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/older_man_tone4.png b/public/-/emojis/3/older_man_tone4.png
new file mode 100644
index 00000000000..c66b2711cf8
--- /dev/null
+++ b/public/-/emojis/3/older_man_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/older_man_tone5.png b/public/-/emojis/3/older_man_tone5.png
new file mode 100644
index 00000000000..86b09738a44
--- /dev/null
+++ b/public/-/emojis/3/older_man_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/older_woman.png b/public/-/emojis/3/older_woman.png
new file mode 100644
index 00000000000..f1d13148730
--- /dev/null
+++ b/public/-/emojis/3/older_woman.png
Binary files differ
diff --git a/public/-/emojis/3/older_woman_tone1.png b/public/-/emojis/3/older_woman_tone1.png
new file mode 100644
index 00000000000..bfb73101f61
--- /dev/null
+++ b/public/-/emojis/3/older_woman_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/older_woman_tone2.png b/public/-/emojis/3/older_woman_tone2.png
new file mode 100644
index 00000000000..c977015582d
--- /dev/null
+++ b/public/-/emojis/3/older_woman_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/older_woman_tone3.png b/public/-/emojis/3/older_woman_tone3.png
new file mode 100644
index 00000000000..cc7922077a4
--- /dev/null
+++ b/public/-/emojis/3/older_woman_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/older_woman_tone4.png b/public/-/emojis/3/older_woman_tone4.png
new file mode 100644
index 00000000000..77111bf20a6
--- /dev/null
+++ b/public/-/emojis/3/older_woman_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/older_woman_tone5.png b/public/-/emojis/3/older_woman_tone5.png
new file mode 100644
index 00000000000..26673b3df96
--- /dev/null
+++ b/public/-/emojis/3/older_woman_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/om_symbol.png b/public/-/emojis/3/om_symbol.png
new file mode 100644
index 00000000000..ce778fd4457
--- /dev/null
+++ b/public/-/emojis/3/om_symbol.png
Binary files differ
diff --git a/public/-/emojis/3/on.png b/public/-/emojis/3/on.png
new file mode 100644
index 00000000000..74fd6c584b8
--- /dev/null
+++ b/public/-/emojis/3/on.png
Binary files differ
diff --git a/public/-/emojis/3/oncoming_automobile.png b/public/-/emojis/3/oncoming_automobile.png
new file mode 100644
index 00000000000..8579ad86c3a
--- /dev/null
+++ b/public/-/emojis/3/oncoming_automobile.png
Binary files differ
diff --git a/public/-/emojis/3/oncoming_bus.png b/public/-/emojis/3/oncoming_bus.png
new file mode 100644
index 00000000000..0b9dc0544e8
--- /dev/null
+++ b/public/-/emojis/3/oncoming_bus.png
Binary files differ
diff --git a/public/-/emojis/3/oncoming_police_car.png b/public/-/emojis/3/oncoming_police_car.png
new file mode 100644
index 00000000000..b4e95c64bba
--- /dev/null
+++ b/public/-/emojis/3/oncoming_police_car.png
Binary files differ
diff --git a/public/-/emojis/3/oncoming_taxi.png b/public/-/emojis/3/oncoming_taxi.png
new file mode 100644
index 00000000000..9211b81d8d1
--- /dev/null
+++ b/public/-/emojis/3/oncoming_taxi.png
Binary files differ
diff --git a/public/-/emojis/3/one.png b/public/-/emojis/3/one.png
new file mode 100644
index 00000000000..2532afb6fcf
--- /dev/null
+++ b/public/-/emojis/3/one.png
Binary files differ
diff --git a/public/-/emojis/3/open_file_folder.png b/public/-/emojis/3/open_file_folder.png
new file mode 100644
index 00000000000..d33f2f40c9a
--- /dev/null
+++ b/public/-/emojis/3/open_file_folder.png
Binary files differ
diff --git a/public/-/emojis/3/open_hands.png b/public/-/emojis/3/open_hands.png
new file mode 100644
index 00000000000..d4bdccbe0b2
--- /dev/null
+++ b/public/-/emojis/3/open_hands.png
Binary files differ
diff --git a/public/-/emojis/3/open_hands_tone1.png b/public/-/emojis/3/open_hands_tone1.png
new file mode 100644
index 00000000000..1ba31eeb590
--- /dev/null
+++ b/public/-/emojis/3/open_hands_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/open_hands_tone2.png b/public/-/emojis/3/open_hands_tone2.png
new file mode 100644
index 00000000000..a74b5b4d5bf
--- /dev/null
+++ b/public/-/emojis/3/open_hands_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/open_hands_tone3.png b/public/-/emojis/3/open_hands_tone3.png
new file mode 100644
index 00000000000..5dc472da57e
--- /dev/null
+++ b/public/-/emojis/3/open_hands_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/open_hands_tone4.png b/public/-/emojis/3/open_hands_tone4.png
new file mode 100644
index 00000000000..435544a1f26
--- /dev/null
+++ b/public/-/emojis/3/open_hands_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/open_hands_tone5.png b/public/-/emojis/3/open_hands_tone5.png
new file mode 100644
index 00000000000..a5d516049ac
--- /dev/null
+++ b/public/-/emojis/3/open_hands_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/open_mouth.png b/public/-/emojis/3/open_mouth.png
new file mode 100644
index 00000000000..17fbef0c183
--- /dev/null
+++ b/public/-/emojis/3/open_mouth.png
Binary files differ
diff --git a/public/-/emojis/3/ophiuchus.png b/public/-/emojis/3/ophiuchus.png
new file mode 100644
index 00000000000..5e9a128b9af
--- /dev/null
+++ b/public/-/emojis/3/ophiuchus.png
Binary files differ
diff --git a/public/-/emojis/3/orange_book.png b/public/-/emojis/3/orange_book.png
new file mode 100644
index 00000000000..d673bc9aec5
--- /dev/null
+++ b/public/-/emojis/3/orange_book.png
Binary files differ
diff --git a/public/-/emojis/3/orthodox_cross.png b/public/-/emojis/3/orthodox_cross.png
new file mode 100644
index 00000000000..7ae9fcb33d1
--- /dev/null
+++ b/public/-/emojis/3/orthodox_cross.png
Binary files differ
diff --git a/public/-/emojis/3/outbox_tray.png b/public/-/emojis/3/outbox_tray.png
new file mode 100644
index 00000000000..0d0afbfeccb
--- /dev/null
+++ b/public/-/emojis/3/outbox_tray.png
Binary files differ
diff --git a/public/-/emojis/3/owl.png b/public/-/emojis/3/owl.png
new file mode 100644
index 00000000000..d559d46d75c
--- /dev/null
+++ b/public/-/emojis/3/owl.png
Binary files differ
diff --git a/public/-/emojis/3/ox.png b/public/-/emojis/3/ox.png
new file mode 100644
index 00000000000..539f49b8a2c
--- /dev/null
+++ b/public/-/emojis/3/ox.png
Binary files differ
diff --git a/public/-/emojis/3/package.png b/public/-/emojis/3/package.png
new file mode 100644
index 00000000000..cee076870bb
--- /dev/null
+++ b/public/-/emojis/3/package.png
Binary files differ
diff --git a/public/-/emojis/3/page_facing_up.png b/public/-/emojis/3/page_facing_up.png
new file mode 100644
index 00000000000..31e199da290
--- /dev/null
+++ b/public/-/emojis/3/page_facing_up.png
Binary files differ
diff --git a/public/-/emojis/3/page_with_curl.png b/public/-/emojis/3/page_with_curl.png
new file mode 100644
index 00000000000..b252e563dcf
--- /dev/null
+++ b/public/-/emojis/3/page_with_curl.png
Binary files differ
diff --git a/public/-/emojis/3/pager.png b/public/-/emojis/3/pager.png
new file mode 100644
index 00000000000..3fe3355c646
--- /dev/null
+++ b/public/-/emojis/3/pager.png
Binary files differ
diff --git a/public/-/emojis/3/paintbrush.png b/public/-/emojis/3/paintbrush.png
new file mode 100644
index 00000000000..b429269f18c
--- /dev/null
+++ b/public/-/emojis/3/paintbrush.png
Binary files differ
diff --git a/public/-/emojis/3/palm_tree.png b/public/-/emojis/3/palm_tree.png
new file mode 100644
index 00000000000..e26bc83032b
--- /dev/null
+++ b/public/-/emojis/3/palm_tree.png
Binary files differ
diff --git a/public/-/emojis/3/pancakes.png b/public/-/emojis/3/pancakes.png
new file mode 100644
index 00000000000..4906a19fcee
--- /dev/null
+++ b/public/-/emojis/3/pancakes.png
Binary files differ
diff --git a/public/-/emojis/3/panda_face.png b/public/-/emojis/3/panda_face.png
new file mode 100644
index 00000000000..edfc15d1db3
--- /dev/null
+++ b/public/-/emojis/3/panda_face.png
Binary files differ
diff --git a/public/-/emojis/3/paperclip.png b/public/-/emojis/3/paperclip.png
new file mode 100644
index 00000000000..a307e0efc87
--- /dev/null
+++ b/public/-/emojis/3/paperclip.png
Binary files differ
diff --git a/public/-/emojis/3/paperclips.png b/public/-/emojis/3/paperclips.png
new file mode 100644
index 00000000000..01010d13145
--- /dev/null
+++ b/public/-/emojis/3/paperclips.png
Binary files differ
diff --git a/public/-/emojis/3/park.png b/public/-/emojis/3/park.png
new file mode 100644
index 00000000000..d3fba154924
--- /dev/null
+++ b/public/-/emojis/3/park.png
Binary files differ
diff --git a/public/-/emojis/3/parking.png b/public/-/emojis/3/parking.png
new file mode 100644
index 00000000000..8b547b76816
--- /dev/null
+++ b/public/-/emojis/3/parking.png
Binary files differ
diff --git a/public/-/emojis/3/part_alternation_mark.png b/public/-/emojis/3/part_alternation_mark.png
new file mode 100644
index 00000000000..0432e380f23
--- /dev/null
+++ b/public/-/emojis/3/part_alternation_mark.png
Binary files differ
diff --git a/public/-/emojis/3/partly_sunny.png b/public/-/emojis/3/partly_sunny.png
new file mode 100644
index 00000000000..dcb0aa10133
--- /dev/null
+++ b/public/-/emojis/3/partly_sunny.png
Binary files differ
diff --git a/public/-/emojis/3/passport_control.png b/public/-/emojis/3/passport_control.png
new file mode 100644
index 00000000000..13d436b025c
--- /dev/null
+++ b/public/-/emojis/3/passport_control.png
Binary files differ
diff --git a/public/-/emojis/3/pause_button.png b/public/-/emojis/3/pause_button.png
new file mode 100644
index 00000000000..cf62af3cbf8
--- /dev/null
+++ b/public/-/emojis/3/pause_button.png
Binary files differ
diff --git a/public/-/emojis/3/peace.png b/public/-/emojis/3/peace.png
new file mode 100644
index 00000000000..331e82e5f4a
--- /dev/null
+++ b/public/-/emojis/3/peace.png
Binary files differ
diff --git a/public/-/emojis/3/peach.png b/public/-/emojis/3/peach.png
new file mode 100644
index 00000000000..a432bcbcdb7
--- /dev/null
+++ b/public/-/emojis/3/peach.png
Binary files differ
diff --git a/public/-/emojis/3/peanuts.png b/public/-/emojis/3/peanuts.png
new file mode 100644
index 00000000000..dbcb5e3f13c
--- /dev/null
+++ b/public/-/emojis/3/peanuts.png
Binary files differ
diff --git a/public/-/emojis/3/pear.png b/public/-/emojis/3/pear.png
new file mode 100644
index 00000000000..89f121d93ef
--- /dev/null
+++ b/public/-/emojis/3/pear.png
Binary files differ
diff --git a/public/-/emojis/3/pen_ballpoint.png b/public/-/emojis/3/pen_ballpoint.png
new file mode 100644
index 00000000000..b28ecf7a4de
--- /dev/null
+++ b/public/-/emojis/3/pen_ballpoint.png
Binary files differ
diff --git a/public/-/emojis/3/pen_fountain.png b/public/-/emojis/3/pen_fountain.png
new file mode 100644
index 00000000000..8610ec1eb35
--- /dev/null
+++ b/public/-/emojis/3/pen_fountain.png
Binary files differ
diff --git a/public/-/emojis/3/pencil.png b/public/-/emojis/3/pencil.png
new file mode 100644
index 00000000000..69795a5c331
--- /dev/null
+++ b/public/-/emojis/3/pencil.png
Binary files differ
diff --git a/public/-/emojis/3/pencil2.png b/public/-/emojis/3/pencil2.png
new file mode 100644
index 00000000000..c0b04c7d34e
--- /dev/null
+++ b/public/-/emojis/3/pencil2.png
Binary files differ
diff --git a/public/-/emojis/3/penguin.png b/public/-/emojis/3/penguin.png
new file mode 100644
index 00000000000..3787ce64f36
--- /dev/null
+++ b/public/-/emojis/3/penguin.png
Binary files differ
diff --git a/public/-/emojis/3/pensive.png b/public/-/emojis/3/pensive.png
new file mode 100644
index 00000000000..1ca0d392746
--- /dev/null
+++ b/public/-/emojis/3/pensive.png
Binary files differ
diff --git a/public/-/emojis/3/performing_arts.png b/public/-/emojis/3/performing_arts.png
new file mode 100644
index 00000000000..817aaac81fa
--- /dev/null
+++ b/public/-/emojis/3/performing_arts.png
Binary files differ
diff --git a/public/-/emojis/3/persevere.png b/public/-/emojis/3/persevere.png
new file mode 100644
index 00000000000..0300cecd2b1
--- /dev/null
+++ b/public/-/emojis/3/persevere.png
Binary files differ
diff --git a/public/-/emojis/3/person_frowning.png b/public/-/emojis/3/person_frowning.png
new file mode 100644
index 00000000000..ef83e19282e
--- /dev/null
+++ b/public/-/emojis/3/person_frowning.png
Binary files differ
diff --git a/public/-/emojis/3/person_frowning_tone1.png b/public/-/emojis/3/person_frowning_tone1.png
new file mode 100644
index 00000000000..1c1d5f031ca
--- /dev/null
+++ b/public/-/emojis/3/person_frowning_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/person_frowning_tone2.png b/public/-/emojis/3/person_frowning_tone2.png
new file mode 100644
index 00000000000..669f500c408
--- /dev/null
+++ b/public/-/emojis/3/person_frowning_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/person_frowning_tone3.png b/public/-/emojis/3/person_frowning_tone3.png
new file mode 100644
index 00000000000..467477ab715
--- /dev/null
+++ b/public/-/emojis/3/person_frowning_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/person_frowning_tone4.png b/public/-/emojis/3/person_frowning_tone4.png
new file mode 100644
index 00000000000..594954e686a
--- /dev/null
+++ b/public/-/emojis/3/person_frowning_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/person_frowning_tone5.png b/public/-/emojis/3/person_frowning_tone5.png
new file mode 100644
index 00000000000..f3f923ce7c4
--- /dev/null
+++ b/public/-/emojis/3/person_frowning_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/person_with_blond_hair.png b/public/-/emojis/3/person_with_blond_hair.png
new file mode 100644
index 00000000000..a55c1fe3bba
--- /dev/null
+++ b/public/-/emojis/3/person_with_blond_hair.png
Binary files differ
diff --git a/public/-/emojis/3/person_with_blond_hair_tone1.png b/public/-/emojis/3/person_with_blond_hair_tone1.png
new file mode 100644
index 00000000000..f5c55e083f2
--- /dev/null
+++ b/public/-/emojis/3/person_with_blond_hair_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/person_with_blond_hair_tone2.png b/public/-/emojis/3/person_with_blond_hair_tone2.png
new file mode 100644
index 00000000000..a520269d964
--- /dev/null
+++ b/public/-/emojis/3/person_with_blond_hair_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/person_with_blond_hair_tone3.png b/public/-/emojis/3/person_with_blond_hair_tone3.png
new file mode 100644
index 00000000000..6da4018043b
--- /dev/null
+++ b/public/-/emojis/3/person_with_blond_hair_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/person_with_blond_hair_tone4.png b/public/-/emojis/3/person_with_blond_hair_tone4.png
new file mode 100644
index 00000000000..b680160dd1a
--- /dev/null
+++ b/public/-/emojis/3/person_with_blond_hair_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/person_with_blond_hair_tone5.png b/public/-/emojis/3/person_with_blond_hair_tone5.png
new file mode 100644
index 00000000000..798f5c6a3d1
--- /dev/null
+++ b/public/-/emojis/3/person_with_blond_hair_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/person_with_pouting_face.png b/public/-/emojis/3/person_with_pouting_face.png
new file mode 100644
index 00000000000..7a4b7892d7d
--- /dev/null
+++ b/public/-/emojis/3/person_with_pouting_face.png
Binary files differ
diff --git a/public/-/emojis/3/person_with_pouting_face_tone1.png b/public/-/emojis/3/person_with_pouting_face_tone1.png
new file mode 100644
index 00000000000..41700c179b9
--- /dev/null
+++ b/public/-/emojis/3/person_with_pouting_face_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/person_with_pouting_face_tone2.png b/public/-/emojis/3/person_with_pouting_face_tone2.png
new file mode 100644
index 00000000000..14d6be7d79a
--- /dev/null
+++ b/public/-/emojis/3/person_with_pouting_face_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/person_with_pouting_face_tone3.png b/public/-/emojis/3/person_with_pouting_face_tone3.png
new file mode 100644
index 00000000000..541061e5935
--- /dev/null
+++ b/public/-/emojis/3/person_with_pouting_face_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/person_with_pouting_face_tone4.png b/public/-/emojis/3/person_with_pouting_face_tone4.png
new file mode 100644
index 00000000000..e797d3f2532
--- /dev/null
+++ b/public/-/emojis/3/person_with_pouting_face_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/person_with_pouting_face_tone5.png b/public/-/emojis/3/person_with_pouting_face_tone5.png
new file mode 100644
index 00000000000..d245c8b7e35
--- /dev/null
+++ b/public/-/emojis/3/person_with_pouting_face_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/pick.png b/public/-/emojis/3/pick.png
new file mode 100644
index 00000000000..a3b573f104f
--- /dev/null
+++ b/public/-/emojis/3/pick.png
Binary files differ
diff --git a/public/-/emojis/3/pig.png b/public/-/emojis/3/pig.png
new file mode 100644
index 00000000000..c8f36d0b6bf
--- /dev/null
+++ b/public/-/emojis/3/pig.png
Binary files differ
diff --git a/public/-/emojis/3/pig2.png b/public/-/emojis/3/pig2.png
new file mode 100644
index 00000000000..e3313b515e6
--- /dev/null
+++ b/public/-/emojis/3/pig2.png
Binary files differ
diff --git a/public/-/emojis/3/pig_nose.png b/public/-/emojis/3/pig_nose.png
new file mode 100644
index 00000000000..34eb7ca5e74
--- /dev/null
+++ b/public/-/emojis/3/pig_nose.png
Binary files differ
diff --git a/public/-/emojis/3/pill.png b/public/-/emojis/3/pill.png
new file mode 100644
index 00000000000..7a926596862
--- /dev/null
+++ b/public/-/emojis/3/pill.png
Binary files differ
diff --git a/public/-/emojis/3/pineapple.png b/public/-/emojis/3/pineapple.png
new file mode 100644
index 00000000000..e1cef9c1106
--- /dev/null
+++ b/public/-/emojis/3/pineapple.png
Binary files differ
diff --git a/public/-/emojis/3/ping_pong.png b/public/-/emojis/3/ping_pong.png
new file mode 100644
index 00000000000..fc1de71206a
--- /dev/null
+++ b/public/-/emojis/3/ping_pong.png
Binary files differ
diff --git a/public/-/emojis/3/pisces.png b/public/-/emojis/3/pisces.png
new file mode 100644
index 00000000000..5b748a7422a
--- /dev/null
+++ b/public/-/emojis/3/pisces.png
Binary files differ
diff --git a/public/-/emojis/3/pizza.png b/public/-/emojis/3/pizza.png
new file mode 100644
index 00000000000..522a6a88b1c
--- /dev/null
+++ b/public/-/emojis/3/pizza.png
Binary files differ
diff --git a/public/-/emojis/3/place_of_worship.png b/public/-/emojis/3/place_of_worship.png
new file mode 100644
index 00000000000..e36b7b7222e
--- /dev/null
+++ b/public/-/emojis/3/place_of_worship.png
Binary files differ
diff --git a/public/-/emojis/3/play_pause.png b/public/-/emojis/3/play_pause.png
new file mode 100644
index 00000000000..1955922e2f6
--- /dev/null
+++ b/public/-/emojis/3/play_pause.png
Binary files differ
diff --git a/public/-/emojis/3/point_down.png b/public/-/emojis/3/point_down.png
new file mode 100644
index 00000000000..0f1ff30678b
--- /dev/null
+++ b/public/-/emojis/3/point_down.png
Binary files differ
diff --git a/public/-/emojis/3/point_down_tone1.png b/public/-/emojis/3/point_down_tone1.png
new file mode 100644
index 00000000000..1940b678983
--- /dev/null
+++ b/public/-/emojis/3/point_down_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/point_down_tone2.png b/public/-/emojis/3/point_down_tone2.png
new file mode 100644
index 00000000000..f5818c7ff48
--- /dev/null
+++ b/public/-/emojis/3/point_down_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/point_down_tone3.png b/public/-/emojis/3/point_down_tone3.png
new file mode 100644
index 00000000000..80ad44feeb5
--- /dev/null
+++ b/public/-/emojis/3/point_down_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/point_down_tone4.png b/public/-/emojis/3/point_down_tone4.png
new file mode 100644
index 00000000000..dd6ade5547e
--- /dev/null
+++ b/public/-/emojis/3/point_down_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/point_down_tone5.png b/public/-/emojis/3/point_down_tone5.png
new file mode 100644
index 00000000000..b1f9d1f5fde
--- /dev/null
+++ b/public/-/emojis/3/point_down_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/point_left.png b/public/-/emojis/3/point_left.png
new file mode 100644
index 00000000000..1330a37c03c
--- /dev/null
+++ b/public/-/emojis/3/point_left.png
Binary files differ
diff --git a/public/-/emojis/3/point_left_tone1.png b/public/-/emojis/3/point_left_tone1.png
new file mode 100644
index 00000000000..70870fe06a5
--- /dev/null
+++ b/public/-/emojis/3/point_left_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/point_left_tone2.png b/public/-/emojis/3/point_left_tone2.png
new file mode 100644
index 00000000000..7a44d8abe73
--- /dev/null
+++ b/public/-/emojis/3/point_left_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/point_left_tone3.png b/public/-/emojis/3/point_left_tone3.png
new file mode 100644
index 00000000000..493c1cf043f
--- /dev/null
+++ b/public/-/emojis/3/point_left_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/point_left_tone4.png b/public/-/emojis/3/point_left_tone4.png
new file mode 100644
index 00000000000..96ba4c680e4
--- /dev/null
+++ b/public/-/emojis/3/point_left_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/point_left_tone5.png b/public/-/emojis/3/point_left_tone5.png
new file mode 100644
index 00000000000..4be3b09c8fa
--- /dev/null
+++ b/public/-/emojis/3/point_left_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/point_right.png b/public/-/emojis/3/point_right.png
new file mode 100644
index 00000000000..4ff5710e09c
--- /dev/null
+++ b/public/-/emojis/3/point_right.png
Binary files differ
diff --git a/public/-/emojis/3/point_right_tone1.png b/public/-/emojis/3/point_right_tone1.png
new file mode 100644
index 00000000000..b44895c5b51
--- /dev/null
+++ b/public/-/emojis/3/point_right_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/point_right_tone2.png b/public/-/emojis/3/point_right_tone2.png
new file mode 100644
index 00000000000..5b8fe2514fb
--- /dev/null
+++ b/public/-/emojis/3/point_right_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/point_right_tone3.png b/public/-/emojis/3/point_right_tone3.png
new file mode 100644
index 00000000000..93c05c4c6a7
--- /dev/null
+++ b/public/-/emojis/3/point_right_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/point_right_tone4.png b/public/-/emojis/3/point_right_tone4.png
new file mode 100644
index 00000000000..02e05e05607
--- /dev/null
+++ b/public/-/emojis/3/point_right_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/point_right_tone5.png b/public/-/emojis/3/point_right_tone5.png
new file mode 100644
index 00000000000..abf9ed1b55b
--- /dev/null
+++ b/public/-/emojis/3/point_right_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/point_up.png b/public/-/emojis/3/point_up.png
new file mode 100644
index 00000000000..459c347afc2
--- /dev/null
+++ b/public/-/emojis/3/point_up.png
Binary files differ
diff --git a/public/-/emojis/3/point_up_2.png b/public/-/emojis/3/point_up_2.png
new file mode 100644
index 00000000000..40588555177
--- /dev/null
+++ b/public/-/emojis/3/point_up_2.png
Binary files differ
diff --git a/public/-/emojis/3/point_up_2_tone1.png b/public/-/emojis/3/point_up_2_tone1.png
new file mode 100644
index 00000000000..5d7b820ebf1
--- /dev/null
+++ b/public/-/emojis/3/point_up_2_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/point_up_2_tone2.png b/public/-/emojis/3/point_up_2_tone2.png
new file mode 100644
index 00000000000..46ff4c2f95b
--- /dev/null
+++ b/public/-/emojis/3/point_up_2_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/point_up_2_tone3.png b/public/-/emojis/3/point_up_2_tone3.png
new file mode 100644
index 00000000000..39d3622b96a
--- /dev/null
+++ b/public/-/emojis/3/point_up_2_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/point_up_2_tone4.png b/public/-/emojis/3/point_up_2_tone4.png
new file mode 100644
index 00000000000..00eb6198fc4
--- /dev/null
+++ b/public/-/emojis/3/point_up_2_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/point_up_2_tone5.png b/public/-/emojis/3/point_up_2_tone5.png
new file mode 100644
index 00000000000..e1614d1e2b5
--- /dev/null
+++ b/public/-/emojis/3/point_up_2_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/point_up_tone1.png b/public/-/emojis/3/point_up_tone1.png
new file mode 100644
index 00000000000..74eb90e3ec4
--- /dev/null
+++ b/public/-/emojis/3/point_up_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/point_up_tone2.png b/public/-/emojis/3/point_up_tone2.png
new file mode 100644
index 00000000000..17e77dd605e
--- /dev/null
+++ b/public/-/emojis/3/point_up_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/point_up_tone3.png b/public/-/emojis/3/point_up_tone3.png
new file mode 100644
index 00000000000..3998e0b3517
--- /dev/null
+++ b/public/-/emojis/3/point_up_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/point_up_tone4.png b/public/-/emojis/3/point_up_tone4.png
new file mode 100644
index 00000000000..39cb4748ba0
--- /dev/null
+++ b/public/-/emojis/3/point_up_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/point_up_tone5.png b/public/-/emojis/3/point_up_tone5.png
new file mode 100644
index 00000000000..83ba921d16a
--- /dev/null
+++ b/public/-/emojis/3/point_up_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/police_car.png b/public/-/emojis/3/police_car.png
new file mode 100644
index 00000000000..9ac6495da1b
--- /dev/null
+++ b/public/-/emojis/3/police_car.png
Binary files differ
diff --git a/public/-/emojis/3/poodle.png b/public/-/emojis/3/poodle.png
new file mode 100644
index 00000000000..0a2794d990c
--- /dev/null
+++ b/public/-/emojis/3/poodle.png
Binary files differ
diff --git a/public/-/emojis/3/poop.png b/public/-/emojis/3/poop.png
new file mode 100644
index 00000000000..c5361b89319
--- /dev/null
+++ b/public/-/emojis/3/poop.png
Binary files differ
diff --git a/public/-/emojis/3/popcorn.png b/public/-/emojis/3/popcorn.png
new file mode 100644
index 00000000000..f83b38506f2
--- /dev/null
+++ b/public/-/emojis/3/popcorn.png
Binary files differ
diff --git a/public/-/emojis/3/post_office.png b/public/-/emojis/3/post_office.png
new file mode 100644
index 00000000000..35c62a68ea0
--- /dev/null
+++ b/public/-/emojis/3/post_office.png
Binary files differ
diff --git a/public/-/emojis/3/postal_horn.png b/public/-/emojis/3/postal_horn.png
new file mode 100644
index 00000000000..b76d1b2b350
--- /dev/null
+++ b/public/-/emojis/3/postal_horn.png
Binary files differ
diff --git a/public/-/emojis/3/postbox.png b/public/-/emojis/3/postbox.png
new file mode 100644
index 00000000000..9390b8a6870
--- /dev/null
+++ b/public/-/emojis/3/postbox.png
Binary files differ
diff --git a/public/-/emojis/3/potable_water.png b/public/-/emojis/3/potable_water.png
new file mode 100644
index 00000000000..ba2253dc45f
--- /dev/null
+++ b/public/-/emojis/3/potable_water.png
Binary files differ
diff --git a/public/-/emojis/3/potato.png b/public/-/emojis/3/potato.png
new file mode 100644
index 00000000000..915781de25e
--- /dev/null
+++ b/public/-/emojis/3/potato.png
Binary files differ
diff --git a/public/-/emojis/3/pouch.png b/public/-/emojis/3/pouch.png
new file mode 100644
index 00000000000..53781258d34
--- /dev/null
+++ b/public/-/emojis/3/pouch.png
Binary files differ
diff --git a/public/-/emojis/3/poultry_leg.png b/public/-/emojis/3/poultry_leg.png
new file mode 100644
index 00000000000..73226f25c54
--- /dev/null
+++ b/public/-/emojis/3/poultry_leg.png
Binary files differ
diff --git a/public/-/emojis/3/pound.png b/public/-/emojis/3/pound.png
new file mode 100644
index 00000000000..2e2e1e4910f
--- /dev/null
+++ b/public/-/emojis/3/pound.png
Binary files differ
diff --git a/public/-/emojis/3/pouting_cat.png b/public/-/emojis/3/pouting_cat.png
new file mode 100644
index 00000000000..3849bcb3b4b
--- /dev/null
+++ b/public/-/emojis/3/pouting_cat.png
Binary files differ
diff --git a/public/-/emojis/3/pray.png b/public/-/emojis/3/pray.png
new file mode 100644
index 00000000000..50665c75185
--- /dev/null
+++ b/public/-/emojis/3/pray.png
Binary files differ
diff --git a/public/-/emojis/3/pray_tone1.png b/public/-/emojis/3/pray_tone1.png
new file mode 100644
index 00000000000..b984a73ddeb
--- /dev/null
+++ b/public/-/emojis/3/pray_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/pray_tone2.png b/public/-/emojis/3/pray_tone2.png
new file mode 100644
index 00000000000..c407a8b3660
--- /dev/null
+++ b/public/-/emojis/3/pray_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/pray_tone3.png b/public/-/emojis/3/pray_tone3.png
new file mode 100644
index 00000000000..4e3793a7763
--- /dev/null
+++ b/public/-/emojis/3/pray_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/pray_tone4.png b/public/-/emojis/3/pray_tone4.png
new file mode 100644
index 00000000000..2fbfd040536
--- /dev/null
+++ b/public/-/emojis/3/pray_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/pray_tone5.png b/public/-/emojis/3/pray_tone5.png
new file mode 100644
index 00000000000..a135b50bb7b
--- /dev/null
+++ b/public/-/emojis/3/pray_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/prayer_beads.png b/public/-/emojis/3/prayer_beads.png
new file mode 100644
index 00000000000..dc0b65983d2
--- /dev/null
+++ b/public/-/emojis/3/prayer_beads.png
Binary files differ
diff --git a/public/-/emojis/3/pregnant_woman.png b/public/-/emojis/3/pregnant_woman.png
new file mode 100644
index 00000000000..7643f388f21
--- /dev/null
+++ b/public/-/emojis/3/pregnant_woman.png
Binary files differ
diff --git a/public/-/emojis/3/pregnant_woman_tone1.png b/public/-/emojis/3/pregnant_woman_tone1.png
new file mode 100644
index 00000000000..8600bfdb3d3
--- /dev/null
+++ b/public/-/emojis/3/pregnant_woman_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/pregnant_woman_tone2.png b/public/-/emojis/3/pregnant_woman_tone2.png
new file mode 100644
index 00000000000..bb4e83036c6
--- /dev/null
+++ b/public/-/emojis/3/pregnant_woman_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/pregnant_woman_tone3.png b/public/-/emojis/3/pregnant_woman_tone3.png
new file mode 100644
index 00000000000..317409a05d4
--- /dev/null
+++ b/public/-/emojis/3/pregnant_woman_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/pregnant_woman_tone4.png b/public/-/emojis/3/pregnant_woman_tone4.png
new file mode 100644
index 00000000000..bf7446637bd
--- /dev/null
+++ b/public/-/emojis/3/pregnant_woman_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/pregnant_woman_tone5.png b/public/-/emojis/3/pregnant_woman_tone5.png
new file mode 100644
index 00000000000..622d4309648
--- /dev/null
+++ b/public/-/emojis/3/pregnant_woman_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/prince.png b/public/-/emojis/3/prince.png
new file mode 100644
index 00000000000..89917fcb358
--- /dev/null
+++ b/public/-/emojis/3/prince.png
Binary files differ
diff --git a/public/-/emojis/3/prince_tone1.png b/public/-/emojis/3/prince_tone1.png
new file mode 100644
index 00000000000..04801cdd4ad
--- /dev/null
+++ b/public/-/emojis/3/prince_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/prince_tone2.png b/public/-/emojis/3/prince_tone2.png
new file mode 100644
index 00000000000..2fb88ccbf6f
--- /dev/null
+++ b/public/-/emojis/3/prince_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/prince_tone3.png b/public/-/emojis/3/prince_tone3.png
new file mode 100644
index 00000000000..232f5df4923
--- /dev/null
+++ b/public/-/emojis/3/prince_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/prince_tone4.png b/public/-/emojis/3/prince_tone4.png
new file mode 100644
index 00000000000..d867a211d9b
--- /dev/null
+++ b/public/-/emojis/3/prince_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/prince_tone5.png b/public/-/emojis/3/prince_tone5.png
new file mode 100644
index 00000000000..32985896d79
--- /dev/null
+++ b/public/-/emojis/3/prince_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/princess.png b/public/-/emojis/3/princess.png
new file mode 100644
index 00000000000..7060e9857bf
--- /dev/null
+++ b/public/-/emojis/3/princess.png
Binary files differ
diff --git a/public/-/emojis/3/princess_tone1.png b/public/-/emojis/3/princess_tone1.png
new file mode 100644
index 00000000000..ff3f587f348
--- /dev/null
+++ b/public/-/emojis/3/princess_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/princess_tone2.png b/public/-/emojis/3/princess_tone2.png
new file mode 100644
index 00000000000..4efdc45b19c
--- /dev/null
+++ b/public/-/emojis/3/princess_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/princess_tone3.png b/public/-/emojis/3/princess_tone3.png
new file mode 100644
index 00000000000..2ad3726ba78
--- /dev/null
+++ b/public/-/emojis/3/princess_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/princess_tone4.png b/public/-/emojis/3/princess_tone4.png
new file mode 100644
index 00000000000..5073b327fd5
--- /dev/null
+++ b/public/-/emojis/3/princess_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/princess_tone5.png b/public/-/emojis/3/princess_tone5.png
new file mode 100644
index 00000000000..1ad4d672027
--- /dev/null
+++ b/public/-/emojis/3/princess_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/printer.png b/public/-/emojis/3/printer.png
new file mode 100644
index 00000000000..788641e1801
--- /dev/null
+++ b/public/-/emojis/3/printer.png
Binary files differ
diff --git a/public/-/emojis/3/projector.png b/public/-/emojis/3/projector.png
new file mode 100644
index 00000000000..125cf081a54
--- /dev/null
+++ b/public/-/emojis/3/projector.png
Binary files differ
diff --git a/public/-/emojis/3/punch.png b/public/-/emojis/3/punch.png
new file mode 100644
index 00000000000..1f8b6839cd9
--- /dev/null
+++ b/public/-/emojis/3/punch.png
Binary files differ
diff --git a/public/-/emojis/3/punch_tone1.png b/public/-/emojis/3/punch_tone1.png
new file mode 100644
index 00000000000..f5413ee12c3
--- /dev/null
+++ b/public/-/emojis/3/punch_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/punch_tone2.png b/public/-/emojis/3/punch_tone2.png
new file mode 100644
index 00000000000..3808a169bc2
--- /dev/null
+++ b/public/-/emojis/3/punch_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/punch_tone3.png b/public/-/emojis/3/punch_tone3.png
new file mode 100644
index 00000000000..2eca7fc6d97
--- /dev/null
+++ b/public/-/emojis/3/punch_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/punch_tone4.png b/public/-/emojis/3/punch_tone4.png
new file mode 100644
index 00000000000..8e3a6afb208
--- /dev/null
+++ b/public/-/emojis/3/punch_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/punch_tone5.png b/public/-/emojis/3/punch_tone5.png
new file mode 100644
index 00000000000..7108cd27092
--- /dev/null
+++ b/public/-/emojis/3/punch_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/purple_heart.png b/public/-/emojis/3/purple_heart.png
new file mode 100644
index 00000000000..1056c2fba01
--- /dev/null
+++ b/public/-/emojis/3/purple_heart.png
Binary files differ
diff --git a/public/-/emojis/3/purse.png b/public/-/emojis/3/purse.png
new file mode 100644
index 00000000000..7a492707535
--- /dev/null
+++ b/public/-/emojis/3/purse.png
Binary files differ
diff --git a/public/-/emojis/3/pushpin.png b/public/-/emojis/3/pushpin.png
new file mode 100644
index 00000000000..b42d77e0573
--- /dev/null
+++ b/public/-/emojis/3/pushpin.png
Binary files differ
diff --git a/public/-/emojis/3/put_litter_in_its_place.png b/public/-/emojis/3/put_litter_in_its_place.png
new file mode 100644
index 00000000000..50338046ec6
--- /dev/null
+++ b/public/-/emojis/3/put_litter_in_its_place.png
Binary files differ
diff --git a/public/-/emojis/3/question.png b/public/-/emojis/3/question.png
new file mode 100644
index 00000000000..e224bd99b83
--- /dev/null
+++ b/public/-/emojis/3/question.png
Binary files differ
diff --git a/public/-/emojis/3/rabbit.png b/public/-/emojis/3/rabbit.png
new file mode 100644
index 00000000000..ee77df1edd4
--- /dev/null
+++ b/public/-/emojis/3/rabbit.png
Binary files differ
diff --git a/public/-/emojis/3/rabbit2.png b/public/-/emojis/3/rabbit2.png
new file mode 100644
index 00000000000..6d098ccf322
--- /dev/null
+++ b/public/-/emojis/3/rabbit2.png
Binary files differ
diff --git a/public/-/emojis/3/race_car.png b/public/-/emojis/3/race_car.png
new file mode 100644
index 00000000000..d6c180cab6d
--- /dev/null
+++ b/public/-/emojis/3/race_car.png
Binary files differ
diff --git a/public/-/emojis/3/racehorse.png b/public/-/emojis/3/racehorse.png
new file mode 100644
index 00000000000..7fde8bdee57
--- /dev/null
+++ b/public/-/emojis/3/racehorse.png
Binary files differ
diff --git a/public/-/emojis/3/radio.png b/public/-/emojis/3/radio.png
new file mode 100644
index 00000000000..eaf4b74a5b9
--- /dev/null
+++ b/public/-/emojis/3/radio.png
Binary files differ
diff --git a/public/-/emojis/3/radio_button.png b/public/-/emojis/3/radio_button.png
new file mode 100644
index 00000000000..021cbe4a69c
--- /dev/null
+++ b/public/-/emojis/3/radio_button.png
Binary files differ
diff --git a/public/-/emojis/3/radioactive.png b/public/-/emojis/3/radioactive.png
new file mode 100644
index 00000000000..af922f09b2b
--- /dev/null
+++ b/public/-/emojis/3/radioactive.png
Binary files differ
diff --git a/public/-/emojis/3/rage.png b/public/-/emojis/3/rage.png
new file mode 100644
index 00000000000..68d461fd0fd
--- /dev/null
+++ b/public/-/emojis/3/rage.png
Binary files differ
diff --git a/public/-/emojis/3/railway_car.png b/public/-/emojis/3/railway_car.png
new file mode 100644
index 00000000000..7df2c4c0058
--- /dev/null
+++ b/public/-/emojis/3/railway_car.png
Binary files differ
diff --git a/public/-/emojis/3/railway_track.png b/public/-/emojis/3/railway_track.png
new file mode 100644
index 00000000000..e37328e72ef
--- /dev/null
+++ b/public/-/emojis/3/railway_track.png
Binary files differ
diff --git a/public/-/emojis/3/rainbow.png b/public/-/emojis/3/rainbow.png
new file mode 100644
index 00000000000..ad03c746f94
--- /dev/null
+++ b/public/-/emojis/3/rainbow.png
Binary files differ
diff --git a/public/-/emojis/3/raised_back_of_hand.png b/public/-/emojis/3/raised_back_of_hand.png
new file mode 100644
index 00000000000..e5d2399b3a0
--- /dev/null
+++ b/public/-/emojis/3/raised_back_of_hand.png
Binary files differ
diff --git a/public/-/emojis/3/raised_back_of_hand_tone1.png b/public/-/emojis/3/raised_back_of_hand_tone1.png
new file mode 100644
index 00000000000..08e8ebfc163
--- /dev/null
+++ b/public/-/emojis/3/raised_back_of_hand_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/raised_back_of_hand_tone2.png b/public/-/emojis/3/raised_back_of_hand_tone2.png
new file mode 100644
index 00000000000..7123e2dab44
--- /dev/null
+++ b/public/-/emojis/3/raised_back_of_hand_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/raised_back_of_hand_tone3.png b/public/-/emojis/3/raised_back_of_hand_tone3.png
new file mode 100644
index 00000000000..7289a913c85
--- /dev/null
+++ b/public/-/emojis/3/raised_back_of_hand_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/raised_back_of_hand_tone4.png b/public/-/emojis/3/raised_back_of_hand_tone4.png
new file mode 100644
index 00000000000..67a77a92b65
--- /dev/null
+++ b/public/-/emojis/3/raised_back_of_hand_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/raised_back_of_hand_tone5.png b/public/-/emojis/3/raised_back_of_hand_tone5.png
new file mode 100644
index 00000000000..5e067c1681c
--- /dev/null
+++ b/public/-/emojis/3/raised_back_of_hand_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/raised_hand.png b/public/-/emojis/3/raised_hand.png
new file mode 100644
index 00000000000..e9bdf7e5a5e
--- /dev/null
+++ b/public/-/emojis/3/raised_hand.png
Binary files differ
diff --git a/public/-/emojis/3/raised_hand_tone1.png b/public/-/emojis/3/raised_hand_tone1.png
new file mode 100644
index 00000000000..1f241453f0d
--- /dev/null
+++ b/public/-/emojis/3/raised_hand_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/raised_hand_tone2.png b/public/-/emojis/3/raised_hand_tone2.png
new file mode 100644
index 00000000000..36128fff984
--- /dev/null
+++ b/public/-/emojis/3/raised_hand_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/raised_hand_tone3.png b/public/-/emojis/3/raised_hand_tone3.png
new file mode 100644
index 00000000000..81569a49088
--- /dev/null
+++ b/public/-/emojis/3/raised_hand_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/raised_hand_tone4.png b/public/-/emojis/3/raised_hand_tone4.png
new file mode 100644
index 00000000000..ee7c304811b
--- /dev/null
+++ b/public/-/emojis/3/raised_hand_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/raised_hand_tone5.png b/public/-/emojis/3/raised_hand_tone5.png
new file mode 100644
index 00000000000..29cf0754048
--- /dev/null
+++ b/public/-/emojis/3/raised_hand_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/raised_hands.png b/public/-/emojis/3/raised_hands.png
new file mode 100644
index 00000000000..d60ecbfed71
--- /dev/null
+++ b/public/-/emojis/3/raised_hands.png
Binary files differ
diff --git a/public/-/emojis/3/raised_hands_tone1.png b/public/-/emojis/3/raised_hands_tone1.png
new file mode 100644
index 00000000000..ed9b563956d
--- /dev/null
+++ b/public/-/emojis/3/raised_hands_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/raised_hands_tone2.png b/public/-/emojis/3/raised_hands_tone2.png
new file mode 100644
index 00000000000..dd6fdab1b91
--- /dev/null
+++ b/public/-/emojis/3/raised_hands_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/raised_hands_tone3.png b/public/-/emojis/3/raised_hands_tone3.png
new file mode 100644
index 00000000000..6d60cf17a02
--- /dev/null
+++ b/public/-/emojis/3/raised_hands_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/raised_hands_tone4.png b/public/-/emojis/3/raised_hands_tone4.png
new file mode 100644
index 00000000000..1af063fa865
--- /dev/null
+++ b/public/-/emojis/3/raised_hands_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/raised_hands_tone5.png b/public/-/emojis/3/raised_hands_tone5.png
new file mode 100644
index 00000000000..bae4d1c0e96
--- /dev/null
+++ b/public/-/emojis/3/raised_hands_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/raising_hand.png b/public/-/emojis/3/raising_hand.png
new file mode 100644
index 00000000000..d1e1b7d3cd6
--- /dev/null
+++ b/public/-/emojis/3/raising_hand.png
Binary files differ
diff --git a/public/-/emojis/3/raising_hand_tone1.png b/public/-/emojis/3/raising_hand_tone1.png
new file mode 100644
index 00000000000..47bd7568b49
--- /dev/null
+++ b/public/-/emojis/3/raising_hand_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/raising_hand_tone2.png b/public/-/emojis/3/raising_hand_tone2.png
new file mode 100644
index 00000000000..4701e2834b2
--- /dev/null
+++ b/public/-/emojis/3/raising_hand_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/raising_hand_tone3.png b/public/-/emojis/3/raising_hand_tone3.png
new file mode 100644
index 00000000000..6dcb187d3b4
--- /dev/null
+++ b/public/-/emojis/3/raising_hand_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/raising_hand_tone4.png b/public/-/emojis/3/raising_hand_tone4.png
new file mode 100644
index 00000000000..6c66cf724a2
--- /dev/null
+++ b/public/-/emojis/3/raising_hand_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/raising_hand_tone5.png b/public/-/emojis/3/raising_hand_tone5.png
new file mode 100644
index 00000000000..e4649bef629
--- /dev/null
+++ b/public/-/emojis/3/raising_hand_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/ram.png b/public/-/emojis/3/ram.png
new file mode 100644
index 00000000000..ed720a34104
--- /dev/null
+++ b/public/-/emojis/3/ram.png
Binary files differ
diff --git a/public/-/emojis/3/ramen.png b/public/-/emojis/3/ramen.png
new file mode 100644
index 00000000000..cc394a81082
--- /dev/null
+++ b/public/-/emojis/3/ramen.png
Binary files differ
diff --git a/public/-/emojis/3/rat.png b/public/-/emojis/3/rat.png
new file mode 100644
index 00000000000..43525c25090
--- /dev/null
+++ b/public/-/emojis/3/rat.png
Binary files differ
diff --git a/public/-/emojis/3/record_button.png b/public/-/emojis/3/record_button.png
new file mode 100644
index 00000000000..de602cd4b2a
--- /dev/null
+++ b/public/-/emojis/3/record_button.png
Binary files differ
diff --git a/public/-/emojis/3/recycle.png b/public/-/emojis/3/recycle.png
new file mode 100644
index 00000000000..43ceec54afb
--- /dev/null
+++ b/public/-/emojis/3/recycle.png
Binary files differ
diff --git a/public/-/emojis/3/red_car.png b/public/-/emojis/3/red_car.png
new file mode 100644
index 00000000000..e03036bdfc9
--- /dev/null
+++ b/public/-/emojis/3/red_car.png
Binary files differ
diff --git a/public/-/emojis/3/red_circle.png b/public/-/emojis/3/red_circle.png
new file mode 100644
index 00000000000..2599e084dfc
--- /dev/null
+++ b/public/-/emojis/3/red_circle.png
Binary files differ
diff --git a/public/-/emojis/3/registered.png b/public/-/emojis/3/registered.png
new file mode 100644
index 00000000000..e0d3d28cff2
--- /dev/null
+++ b/public/-/emojis/3/registered.png
Binary files differ
diff --git a/public/-/emojis/3/relaxed.png b/public/-/emojis/3/relaxed.png
new file mode 100644
index 00000000000..aeababb5735
--- /dev/null
+++ b/public/-/emojis/3/relaxed.png
Binary files differ
diff --git a/public/-/emojis/3/relieved.png b/public/-/emojis/3/relieved.png
new file mode 100644
index 00000000000..5aab9acec08
--- /dev/null
+++ b/public/-/emojis/3/relieved.png
Binary files differ
diff --git a/public/-/emojis/3/reminder_ribbon.png b/public/-/emojis/3/reminder_ribbon.png
new file mode 100644
index 00000000000..a2b4e44f3b0
--- /dev/null
+++ b/public/-/emojis/3/reminder_ribbon.png
Binary files differ
diff --git a/public/-/emojis/3/repeat.png b/public/-/emojis/3/repeat.png
new file mode 100644
index 00000000000..b23951091eb
--- /dev/null
+++ b/public/-/emojis/3/repeat.png
Binary files differ
diff --git a/public/-/emojis/3/repeat_one.png b/public/-/emojis/3/repeat_one.png
new file mode 100644
index 00000000000..054653bc56e
--- /dev/null
+++ b/public/-/emojis/3/repeat_one.png
Binary files differ
diff --git a/public/-/emojis/3/restroom.png b/public/-/emojis/3/restroom.png
new file mode 100644
index 00000000000..6617f52373c
--- /dev/null
+++ b/public/-/emojis/3/restroom.png
Binary files differ
diff --git a/public/-/emojis/3/revolving_hearts.png b/public/-/emojis/3/revolving_hearts.png
new file mode 100644
index 00000000000..dac2c409365
--- /dev/null
+++ b/public/-/emojis/3/revolving_hearts.png
Binary files differ
diff --git a/public/-/emojis/3/rewind.png b/public/-/emojis/3/rewind.png
new file mode 100644
index 00000000000..46bf1722918
--- /dev/null
+++ b/public/-/emojis/3/rewind.png
Binary files differ
diff --git a/public/-/emojis/3/rhino.png b/public/-/emojis/3/rhino.png
new file mode 100644
index 00000000000..668a446803d
--- /dev/null
+++ b/public/-/emojis/3/rhino.png
Binary files differ
diff --git a/public/-/emojis/3/ribbon.png b/public/-/emojis/3/ribbon.png
new file mode 100644
index 00000000000..4d1f568c1d9
--- /dev/null
+++ b/public/-/emojis/3/ribbon.png
Binary files differ
diff --git a/public/-/emojis/3/rice.png b/public/-/emojis/3/rice.png
new file mode 100644
index 00000000000..0b1ddf99281
--- /dev/null
+++ b/public/-/emojis/3/rice.png
Binary files differ
diff --git a/public/-/emojis/3/rice_ball.png b/public/-/emojis/3/rice_ball.png
new file mode 100644
index 00000000000..c06515c8e08
--- /dev/null
+++ b/public/-/emojis/3/rice_ball.png
Binary files differ
diff --git a/public/-/emojis/3/rice_cracker.png b/public/-/emojis/3/rice_cracker.png
new file mode 100644
index 00000000000..025b0557fc0
--- /dev/null
+++ b/public/-/emojis/3/rice_cracker.png
Binary files differ
diff --git a/public/-/emojis/3/rice_scene.png b/public/-/emojis/3/rice_scene.png
new file mode 100644
index 00000000000..243e7ce2ac0
--- /dev/null
+++ b/public/-/emojis/3/rice_scene.png
Binary files differ
diff --git a/public/-/emojis/3/right_facing_fist.png b/public/-/emojis/3/right_facing_fist.png
new file mode 100644
index 00000000000..70a51237341
--- /dev/null
+++ b/public/-/emojis/3/right_facing_fist.png
Binary files differ
diff --git a/public/-/emojis/3/right_facing_fist_tone1.png b/public/-/emojis/3/right_facing_fist_tone1.png
new file mode 100644
index 00000000000..bda21235986
--- /dev/null
+++ b/public/-/emojis/3/right_facing_fist_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/right_facing_fist_tone2.png b/public/-/emojis/3/right_facing_fist_tone2.png
new file mode 100644
index 00000000000..2b5d2514d8d
--- /dev/null
+++ b/public/-/emojis/3/right_facing_fist_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/right_facing_fist_tone3.png b/public/-/emojis/3/right_facing_fist_tone3.png
new file mode 100644
index 00000000000..40f330a55c1
--- /dev/null
+++ b/public/-/emojis/3/right_facing_fist_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/right_facing_fist_tone4.png b/public/-/emojis/3/right_facing_fist_tone4.png
new file mode 100644
index 00000000000..5f1c1293194
--- /dev/null
+++ b/public/-/emojis/3/right_facing_fist_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/right_facing_fist_tone5.png b/public/-/emojis/3/right_facing_fist_tone5.png
new file mode 100644
index 00000000000..3a93306f255
--- /dev/null
+++ b/public/-/emojis/3/right_facing_fist_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/ring.png b/public/-/emojis/3/ring.png
new file mode 100644
index 00000000000..3fc92c8efa1
--- /dev/null
+++ b/public/-/emojis/3/ring.png
Binary files differ
diff --git a/public/-/emojis/3/robot.png b/public/-/emojis/3/robot.png
new file mode 100644
index 00000000000..ddf197b52ea
--- /dev/null
+++ b/public/-/emojis/3/robot.png
Binary files differ
diff --git a/public/-/emojis/3/rocket.png b/public/-/emojis/3/rocket.png
new file mode 100644
index 00000000000..06a8c008b68
--- /dev/null
+++ b/public/-/emojis/3/rocket.png
Binary files differ
diff --git a/public/-/emojis/3/rofl.png b/public/-/emojis/3/rofl.png
new file mode 100644
index 00000000000..53673cef6cc
--- /dev/null
+++ b/public/-/emojis/3/rofl.png
Binary files differ
diff --git a/public/-/emojis/3/roller_coaster.png b/public/-/emojis/3/roller_coaster.png
new file mode 100644
index 00000000000..5b511bf6de2
--- /dev/null
+++ b/public/-/emojis/3/roller_coaster.png
Binary files differ
diff --git a/public/-/emojis/3/rolling_eyes.png b/public/-/emojis/3/rolling_eyes.png
new file mode 100644
index 00000000000..8470e15ff1b
--- /dev/null
+++ b/public/-/emojis/3/rolling_eyes.png
Binary files differ
diff --git a/public/-/emojis/3/rooster.png b/public/-/emojis/3/rooster.png
new file mode 100644
index 00000000000..9b464ad2831
--- /dev/null
+++ b/public/-/emojis/3/rooster.png
Binary files differ
diff --git a/public/-/emojis/3/rose.png b/public/-/emojis/3/rose.png
new file mode 100644
index 00000000000..3b74c1e6901
--- /dev/null
+++ b/public/-/emojis/3/rose.png
Binary files differ
diff --git a/public/-/emojis/3/rosette.png b/public/-/emojis/3/rosette.png
new file mode 100644
index 00000000000..9607481278a
--- /dev/null
+++ b/public/-/emojis/3/rosette.png
Binary files differ
diff --git a/public/-/emojis/3/rotating_light.png b/public/-/emojis/3/rotating_light.png
new file mode 100644
index 00000000000..d868b247cd0
--- /dev/null
+++ b/public/-/emojis/3/rotating_light.png
Binary files differ
diff --git a/public/-/emojis/3/round_pushpin.png b/public/-/emojis/3/round_pushpin.png
new file mode 100644
index 00000000000..8b3b49c3ec1
--- /dev/null
+++ b/public/-/emojis/3/round_pushpin.png
Binary files differ
diff --git a/public/-/emojis/3/rowboat.png b/public/-/emojis/3/rowboat.png
new file mode 100644
index 00000000000..40705e7e2ad
--- /dev/null
+++ b/public/-/emojis/3/rowboat.png
Binary files differ
diff --git a/public/-/emojis/3/rowboat_tone1.png b/public/-/emojis/3/rowboat_tone1.png
new file mode 100644
index 00000000000..9014b2f8960
--- /dev/null
+++ b/public/-/emojis/3/rowboat_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/rowboat_tone2.png b/public/-/emojis/3/rowboat_tone2.png
new file mode 100644
index 00000000000..70dbf641c14
--- /dev/null
+++ b/public/-/emojis/3/rowboat_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/rowboat_tone3.png b/public/-/emojis/3/rowboat_tone3.png
new file mode 100644
index 00000000000..12b23c9b54a
--- /dev/null
+++ b/public/-/emojis/3/rowboat_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/rowboat_tone4.png b/public/-/emojis/3/rowboat_tone4.png
new file mode 100644
index 00000000000..67b741783a9
--- /dev/null
+++ b/public/-/emojis/3/rowboat_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/rowboat_tone5.png b/public/-/emojis/3/rowboat_tone5.png
new file mode 100644
index 00000000000..01ed0805197
--- /dev/null
+++ b/public/-/emojis/3/rowboat_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/rugby_football.png b/public/-/emojis/3/rugby_football.png
new file mode 100644
index 00000000000..eb6aece5982
--- /dev/null
+++ b/public/-/emojis/3/rugby_football.png
Binary files differ
diff --git a/public/-/emojis/3/runner.png b/public/-/emojis/3/runner.png
new file mode 100644
index 00000000000..ac4142a0d74
--- /dev/null
+++ b/public/-/emojis/3/runner.png
Binary files differ
diff --git a/public/-/emojis/3/runner_tone1.png b/public/-/emojis/3/runner_tone1.png
new file mode 100644
index 00000000000..91eedefb220
--- /dev/null
+++ b/public/-/emojis/3/runner_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/runner_tone2.png b/public/-/emojis/3/runner_tone2.png
new file mode 100644
index 00000000000..671eebc851c
--- /dev/null
+++ b/public/-/emojis/3/runner_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/runner_tone3.png b/public/-/emojis/3/runner_tone3.png
new file mode 100644
index 00000000000..c878f4bea7c
--- /dev/null
+++ b/public/-/emojis/3/runner_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/runner_tone4.png b/public/-/emojis/3/runner_tone4.png
new file mode 100644
index 00000000000..e411c49c4c8
--- /dev/null
+++ b/public/-/emojis/3/runner_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/runner_tone5.png b/public/-/emojis/3/runner_tone5.png
new file mode 100644
index 00000000000..481c43d92c8
--- /dev/null
+++ b/public/-/emojis/3/runner_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/running_shirt_with_sash.png b/public/-/emojis/3/running_shirt_with_sash.png
new file mode 100644
index 00000000000..354b0f86fab
--- /dev/null
+++ b/public/-/emojis/3/running_shirt_with_sash.png
Binary files differ
diff --git a/public/-/emojis/3/sa.png b/public/-/emojis/3/sa.png
new file mode 100644
index 00000000000..efda8e0ef41
--- /dev/null
+++ b/public/-/emojis/3/sa.png
Binary files differ
diff --git a/public/-/emojis/3/sagittarius.png b/public/-/emojis/3/sagittarius.png
new file mode 100644
index 00000000000..638df61a64f
--- /dev/null
+++ b/public/-/emojis/3/sagittarius.png
Binary files differ
diff --git a/public/-/emojis/3/sailboat.png b/public/-/emojis/3/sailboat.png
new file mode 100644
index 00000000000..a4affb08500
--- /dev/null
+++ b/public/-/emojis/3/sailboat.png
Binary files differ
diff --git a/public/-/emojis/3/sake.png b/public/-/emojis/3/sake.png
new file mode 100644
index 00000000000..32c31b2b19b
--- /dev/null
+++ b/public/-/emojis/3/sake.png
Binary files differ
diff --git a/public/-/emojis/3/salad.png b/public/-/emojis/3/salad.png
new file mode 100644
index 00000000000..e199bd390e3
--- /dev/null
+++ b/public/-/emojis/3/salad.png
Binary files differ
diff --git a/public/-/emojis/3/sandal.png b/public/-/emojis/3/sandal.png
new file mode 100644
index 00000000000..8f641221862
--- /dev/null
+++ b/public/-/emojis/3/sandal.png
Binary files differ
diff --git a/public/-/emojis/3/santa.png b/public/-/emojis/3/santa.png
new file mode 100644
index 00000000000..dbea807e355
--- /dev/null
+++ b/public/-/emojis/3/santa.png
Binary files differ
diff --git a/public/-/emojis/3/santa_tone1.png b/public/-/emojis/3/santa_tone1.png
new file mode 100644
index 00000000000..c53dafd0dd2
--- /dev/null
+++ b/public/-/emojis/3/santa_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/santa_tone2.png b/public/-/emojis/3/santa_tone2.png
new file mode 100644
index 00000000000..8dcc1470f1e
--- /dev/null
+++ b/public/-/emojis/3/santa_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/santa_tone3.png b/public/-/emojis/3/santa_tone3.png
new file mode 100644
index 00000000000..32c58d37b01
--- /dev/null
+++ b/public/-/emojis/3/santa_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/santa_tone4.png b/public/-/emojis/3/santa_tone4.png
new file mode 100644
index 00000000000..434846b95a7
--- /dev/null
+++ b/public/-/emojis/3/santa_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/santa_tone5.png b/public/-/emojis/3/santa_tone5.png
new file mode 100644
index 00000000000..9920f21d2bb
--- /dev/null
+++ b/public/-/emojis/3/santa_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/satellite.png b/public/-/emojis/3/satellite.png
new file mode 100644
index 00000000000..5c559370b45
--- /dev/null
+++ b/public/-/emojis/3/satellite.png
Binary files differ
diff --git a/public/-/emojis/3/satellite_orbital.png b/public/-/emojis/3/satellite_orbital.png
new file mode 100644
index 00000000000..a08470226f2
--- /dev/null
+++ b/public/-/emojis/3/satellite_orbital.png
Binary files differ
diff --git a/public/-/emojis/3/saxophone.png b/public/-/emojis/3/saxophone.png
new file mode 100644
index 00000000000..1a9fa8f0592
--- /dev/null
+++ b/public/-/emojis/3/saxophone.png
Binary files differ
diff --git a/public/-/emojis/3/scales.png b/public/-/emojis/3/scales.png
new file mode 100644
index 00000000000..60485a6d431
--- /dev/null
+++ b/public/-/emojis/3/scales.png
Binary files differ
diff --git a/public/-/emojis/3/school.png b/public/-/emojis/3/school.png
new file mode 100644
index 00000000000..34b8e4cb70e
--- /dev/null
+++ b/public/-/emojis/3/school.png
Binary files differ
diff --git a/public/-/emojis/3/school_satchel.png b/public/-/emojis/3/school_satchel.png
new file mode 100644
index 00000000000..f3d84f31a0e
--- /dev/null
+++ b/public/-/emojis/3/school_satchel.png
Binary files differ
diff --git a/public/-/emojis/3/scissors.png b/public/-/emojis/3/scissors.png
new file mode 100644
index 00000000000..f1fab8c74dd
--- /dev/null
+++ b/public/-/emojis/3/scissors.png
Binary files differ
diff --git a/public/-/emojis/3/scooter.png b/public/-/emojis/3/scooter.png
new file mode 100644
index 00000000000..cae1aa50482
--- /dev/null
+++ b/public/-/emojis/3/scooter.png
Binary files differ
diff --git a/public/-/emojis/3/scorpion.png b/public/-/emojis/3/scorpion.png
new file mode 100644
index 00000000000..8d8c145857b
--- /dev/null
+++ b/public/-/emojis/3/scorpion.png
Binary files differ
diff --git a/public/-/emojis/3/scorpius.png b/public/-/emojis/3/scorpius.png
new file mode 100644
index 00000000000..059b45a80de
--- /dev/null
+++ b/public/-/emojis/3/scorpius.png
Binary files differ
diff --git a/public/-/emojis/3/scream.png b/public/-/emojis/3/scream.png
new file mode 100644
index 00000000000..81467f32ba3
--- /dev/null
+++ b/public/-/emojis/3/scream.png
Binary files differ
diff --git a/public/-/emojis/3/scream_cat.png b/public/-/emojis/3/scream_cat.png
new file mode 100644
index 00000000000..c682fe7654a
--- /dev/null
+++ b/public/-/emojis/3/scream_cat.png
Binary files differ
diff --git a/public/-/emojis/3/scroll.png b/public/-/emojis/3/scroll.png
new file mode 100644
index 00000000000..ddec9deb6f9
--- /dev/null
+++ b/public/-/emojis/3/scroll.png
Binary files differ
diff --git a/public/-/emojis/3/seat.png b/public/-/emojis/3/seat.png
new file mode 100644
index 00000000000..4cf5b744f1e
--- /dev/null
+++ b/public/-/emojis/3/seat.png
Binary files differ
diff --git a/public/-/emojis/3/second_place.png b/public/-/emojis/3/second_place.png
new file mode 100644
index 00000000000..478e95852ea
--- /dev/null
+++ b/public/-/emojis/3/second_place.png
Binary files differ
diff --git a/public/-/emojis/3/secret.png b/public/-/emojis/3/secret.png
new file mode 100644
index 00000000000..1f0ef02fa53
--- /dev/null
+++ b/public/-/emojis/3/secret.png
Binary files differ
diff --git a/public/-/emojis/3/see_no_evil.png b/public/-/emojis/3/see_no_evil.png
new file mode 100644
index 00000000000..7e500f70664
--- /dev/null
+++ b/public/-/emojis/3/see_no_evil.png
Binary files differ
diff --git a/public/-/emojis/3/seedling.png b/public/-/emojis/3/seedling.png
new file mode 100644
index 00000000000..9ce4e16a3d7
--- /dev/null
+++ b/public/-/emojis/3/seedling.png
Binary files differ
diff --git a/public/-/emojis/3/selfie.png b/public/-/emojis/3/selfie.png
new file mode 100644
index 00000000000..f66a0726e03
--- /dev/null
+++ b/public/-/emojis/3/selfie.png
Binary files differ
diff --git a/public/-/emojis/3/selfie_tone1.png b/public/-/emojis/3/selfie_tone1.png
new file mode 100644
index 00000000000..ff8497da02f
--- /dev/null
+++ b/public/-/emojis/3/selfie_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/selfie_tone2.png b/public/-/emojis/3/selfie_tone2.png
new file mode 100644
index 00000000000..cda07827962
--- /dev/null
+++ b/public/-/emojis/3/selfie_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/selfie_tone3.png b/public/-/emojis/3/selfie_tone3.png
new file mode 100644
index 00000000000..c40fdff8972
--- /dev/null
+++ b/public/-/emojis/3/selfie_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/selfie_tone4.png b/public/-/emojis/3/selfie_tone4.png
new file mode 100644
index 00000000000..c60858f1006
--- /dev/null
+++ b/public/-/emojis/3/selfie_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/selfie_tone5.png b/public/-/emojis/3/selfie_tone5.png
new file mode 100644
index 00000000000..4718b7c8494
--- /dev/null
+++ b/public/-/emojis/3/selfie_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/seven.png b/public/-/emojis/3/seven.png
new file mode 100644
index 00000000000..e254ef6fdd4
--- /dev/null
+++ b/public/-/emojis/3/seven.png
Binary files differ
diff --git a/public/-/emojis/3/shallow_pan_of_food.png b/public/-/emojis/3/shallow_pan_of_food.png
new file mode 100644
index 00000000000..0fbf77d45b1
--- /dev/null
+++ b/public/-/emojis/3/shallow_pan_of_food.png
Binary files differ
diff --git a/public/-/emojis/3/shamrock.png b/public/-/emojis/3/shamrock.png
new file mode 100644
index 00000000000..7fed4712809
--- /dev/null
+++ b/public/-/emojis/3/shamrock.png
Binary files differ
diff --git a/public/-/emojis/3/shark.png b/public/-/emojis/3/shark.png
new file mode 100644
index 00000000000..97d2f93a79d
--- /dev/null
+++ b/public/-/emojis/3/shark.png
Binary files differ
diff --git a/public/-/emojis/3/shaved_ice.png b/public/-/emojis/3/shaved_ice.png
new file mode 100644
index 00000000000..a8483b92c9c
--- /dev/null
+++ b/public/-/emojis/3/shaved_ice.png
Binary files differ
diff --git a/public/-/emojis/3/sheep.png b/public/-/emojis/3/sheep.png
new file mode 100644
index 00000000000..31168c85de2
--- /dev/null
+++ b/public/-/emojis/3/sheep.png
Binary files differ
diff --git a/public/-/emojis/3/shell.png b/public/-/emojis/3/shell.png
new file mode 100644
index 00000000000..500130bf8bb
--- /dev/null
+++ b/public/-/emojis/3/shell.png
Binary files differ
diff --git a/public/-/emojis/3/shield.png b/public/-/emojis/3/shield.png
new file mode 100644
index 00000000000..1ef6a7e057a
--- /dev/null
+++ b/public/-/emojis/3/shield.png
Binary files differ
diff --git a/public/-/emojis/3/shinto_shrine.png b/public/-/emojis/3/shinto_shrine.png
new file mode 100644
index 00000000000..037d405f70e
--- /dev/null
+++ b/public/-/emojis/3/shinto_shrine.png
Binary files differ
diff --git a/public/-/emojis/3/ship.png b/public/-/emojis/3/ship.png
new file mode 100644
index 00000000000..5863070b294
--- /dev/null
+++ b/public/-/emojis/3/ship.png
Binary files differ
diff --git a/public/-/emojis/3/shirt.png b/public/-/emojis/3/shirt.png
new file mode 100644
index 00000000000..b86a2dc6d26
--- /dev/null
+++ b/public/-/emojis/3/shirt.png
Binary files differ
diff --git a/public/-/emojis/3/shopping_bags.png b/public/-/emojis/3/shopping_bags.png
new file mode 100644
index 00000000000..b37726047d3
--- /dev/null
+++ b/public/-/emojis/3/shopping_bags.png
Binary files differ
diff --git a/public/-/emojis/3/shopping_cart.png b/public/-/emojis/3/shopping_cart.png
new file mode 100644
index 00000000000..037e4edb689
--- /dev/null
+++ b/public/-/emojis/3/shopping_cart.png
Binary files differ
diff --git a/public/-/emojis/3/shower.png b/public/-/emojis/3/shower.png
new file mode 100644
index 00000000000..223abf3322c
--- /dev/null
+++ b/public/-/emojis/3/shower.png
Binary files differ
diff --git a/public/-/emojis/3/shrimp.png b/public/-/emojis/3/shrimp.png
new file mode 100644
index 00000000000..7a3667cc1ff
--- /dev/null
+++ b/public/-/emojis/3/shrimp.png
Binary files differ
diff --git a/public/-/emojis/3/shrug.png b/public/-/emojis/3/shrug.png
new file mode 100644
index 00000000000..cfc3d1471b3
--- /dev/null
+++ b/public/-/emojis/3/shrug.png
Binary files differ
diff --git a/public/-/emojis/3/shrug_tone1.png b/public/-/emojis/3/shrug_tone1.png
new file mode 100644
index 00000000000..e2f3d24f8b9
--- /dev/null
+++ b/public/-/emojis/3/shrug_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/shrug_tone2.png b/public/-/emojis/3/shrug_tone2.png
new file mode 100644
index 00000000000..7e359abee83
--- /dev/null
+++ b/public/-/emojis/3/shrug_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/shrug_tone3.png b/public/-/emojis/3/shrug_tone3.png
new file mode 100644
index 00000000000..301b4922f47
--- /dev/null
+++ b/public/-/emojis/3/shrug_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/shrug_tone4.png b/public/-/emojis/3/shrug_tone4.png
new file mode 100644
index 00000000000..7e95a0e23b6
--- /dev/null
+++ b/public/-/emojis/3/shrug_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/shrug_tone5.png b/public/-/emojis/3/shrug_tone5.png
new file mode 100644
index 00000000000..771f7611e2d
--- /dev/null
+++ b/public/-/emojis/3/shrug_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/signal_strength.png b/public/-/emojis/3/signal_strength.png
new file mode 100644
index 00000000000..9a98a83072e
--- /dev/null
+++ b/public/-/emojis/3/signal_strength.png
Binary files differ
diff --git a/public/-/emojis/3/six.png b/public/-/emojis/3/six.png
new file mode 100644
index 00000000000..fc4482573a7
--- /dev/null
+++ b/public/-/emojis/3/six.png
Binary files differ
diff --git a/public/-/emojis/3/six_pointed_star.png b/public/-/emojis/3/six_pointed_star.png
new file mode 100644
index 00000000000..762094850fa
--- /dev/null
+++ b/public/-/emojis/3/six_pointed_star.png
Binary files differ
diff --git a/public/-/emojis/3/ski.png b/public/-/emojis/3/ski.png
new file mode 100644
index 00000000000..4bbb1966368
--- /dev/null
+++ b/public/-/emojis/3/ski.png
Binary files differ
diff --git a/public/-/emojis/3/skier.png b/public/-/emojis/3/skier.png
new file mode 100644
index 00000000000..3a50c6f49ff
--- /dev/null
+++ b/public/-/emojis/3/skier.png
Binary files differ
diff --git a/public/-/emojis/3/skull.png b/public/-/emojis/3/skull.png
new file mode 100644
index 00000000000..de8357e5fde
--- /dev/null
+++ b/public/-/emojis/3/skull.png
Binary files differ
diff --git a/public/-/emojis/3/skull_crossbones.png b/public/-/emojis/3/skull_crossbones.png
new file mode 100644
index 00000000000..0840efc4b90
--- /dev/null
+++ b/public/-/emojis/3/skull_crossbones.png
Binary files differ
diff --git a/public/-/emojis/3/sleeping.png b/public/-/emojis/3/sleeping.png
new file mode 100644
index 00000000000..7d2315f4c04
--- /dev/null
+++ b/public/-/emojis/3/sleeping.png
Binary files differ
diff --git a/public/-/emojis/3/sleeping_accommodation.png b/public/-/emojis/3/sleeping_accommodation.png
new file mode 100644
index 00000000000..a4cfd898944
--- /dev/null
+++ b/public/-/emojis/3/sleeping_accommodation.png
Binary files differ
diff --git a/public/-/emojis/3/sleepy.png b/public/-/emojis/3/sleepy.png
new file mode 100644
index 00000000000..ad0fb9b87bf
--- /dev/null
+++ b/public/-/emojis/3/sleepy.png
Binary files differ
diff --git a/public/-/emojis/3/slight_frown.png b/public/-/emojis/3/slight_frown.png
new file mode 100644
index 00000000000..adb072c7fa5
--- /dev/null
+++ b/public/-/emojis/3/slight_frown.png
Binary files differ
diff --git a/public/-/emojis/3/slight_smile.png b/public/-/emojis/3/slight_smile.png
new file mode 100644
index 00000000000..e38c9ba1b92
--- /dev/null
+++ b/public/-/emojis/3/slight_smile.png
Binary files differ
diff --git a/public/-/emojis/3/slot_machine.png b/public/-/emojis/3/slot_machine.png
new file mode 100644
index 00000000000..37bfb6b2260
--- /dev/null
+++ b/public/-/emojis/3/slot_machine.png
Binary files differ
diff --git a/public/-/emojis/3/small_blue_diamond.png b/public/-/emojis/3/small_blue_diamond.png
new file mode 100644
index 00000000000..b83d4e1129e
--- /dev/null
+++ b/public/-/emojis/3/small_blue_diamond.png
Binary files differ
diff --git a/public/-/emojis/3/small_orange_diamond.png b/public/-/emojis/3/small_orange_diamond.png
new file mode 100644
index 00000000000..d15c9971d4a
--- /dev/null
+++ b/public/-/emojis/3/small_orange_diamond.png
Binary files differ
diff --git a/public/-/emojis/3/small_red_triangle.png b/public/-/emojis/3/small_red_triangle.png
new file mode 100644
index 00000000000..d4b36bec645
--- /dev/null
+++ b/public/-/emojis/3/small_red_triangle.png
Binary files differ
diff --git a/public/-/emojis/3/small_red_triangle_down.png b/public/-/emojis/3/small_red_triangle_down.png
new file mode 100644
index 00000000000..f5934bc46c6
--- /dev/null
+++ b/public/-/emojis/3/small_red_triangle_down.png
Binary files differ
diff --git a/public/-/emojis/3/smile.png b/public/-/emojis/3/smile.png
new file mode 100644
index 00000000000..020856fbfa0
--- /dev/null
+++ b/public/-/emojis/3/smile.png
Binary files differ
diff --git a/public/-/emojis/3/smile_cat.png b/public/-/emojis/3/smile_cat.png
new file mode 100644
index 00000000000..7bd86d9ad43
--- /dev/null
+++ b/public/-/emojis/3/smile_cat.png
Binary files differ
diff --git a/public/-/emojis/3/smiley.png b/public/-/emojis/3/smiley.png
new file mode 100644
index 00000000000..25db0d4936d
--- /dev/null
+++ b/public/-/emojis/3/smiley.png
Binary files differ
diff --git a/public/-/emojis/3/smiley_cat.png b/public/-/emojis/3/smiley_cat.png
new file mode 100644
index 00000000000..4b56e4fca6d
--- /dev/null
+++ b/public/-/emojis/3/smiley_cat.png
Binary files differ
diff --git a/public/-/emojis/3/smiling_imp.png b/public/-/emojis/3/smiling_imp.png
new file mode 100644
index 00000000000..1c23402e37b
--- /dev/null
+++ b/public/-/emojis/3/smiling_imp.png
Binary files differ
diff --git a/public/-/emojis/3/smirk.png b/public/-/emojis/3/smirk.png
new file mode 100644
index 00000000000..efb6db0393b
--- /dev/null
+++ b/public/-/emojis/3/smirk.png
Binary files differ
diff --git a/public/-/emojis/3/smirk_cat.png b/public/-/emojis/3/smirk_cat.png
new file mode 100644
index 00000000000..cb2f3f03d42
--- /dev/null
+++ b/public/-/emojis/3/smirk_cat.png
Binary files differ
diff --git a/public/-/emojis/3/smoking.png b/public/-/emojis/3/smoking.png
new file mode 100644
index 00000000000..017dff2df8a
--- /dev/null
+++ b/public/-/emojis/3/smoking.png
Binary files differ
diff --git a/public/-/emojis/3/snail.png b/public/-/emojis/3/snail.png
new file mode 100644
index 00000000000..28a31806c82
--- /dev/null
+++ b/public/-/emojis/3/snail.png
Binary files differ
diff --git a/public/-/emojis/3/snake.png b/public/-/emojis/3/snake.png
new file mode 100644
index 00000000000..11680226a2f
--- /dev/null
+++ b/public/-/emojis/3/snake.png
Binary files differ
diff --git a/public/-/emojis/3/sneezing_face.png b/public/-/emojis/3/sneezing_face.png
new file mode 100644
index 00000000000..f807fe90a10
--- /dev/null
+++ b/public/-/emojis/3/sneezing_face.png
Binary files differ
diff --git a/public/-/emojis/3/snowboarder.png b/public/-/emojis/3/snowboarder.png
new file mode 100644
index 00000000000..fd70915bfc6
--- /dev/null
+++ b/public/-/emojis/3/snowboarder.png
Binary files differ
diff --git a/public/-/emojis/3/snowflake.png b/public/-/emojis/3/snowflake.png
new file mode 100644
index 00000000000..9eaf9ede6e2
--- /dev/null
+++ b/public/-/emojis/3/snowflake.png
Binary files differ
diff --git a/public/-/emojis/3/snowman.png b/public/-/emojis/3/snowman.png
new file mode 100644
index 00000000000..a1c8bbd32ac
--- /dev/null
+++ b/public/-/emojis/3/snowman.png
Binary files differ
diff --git a/public/-/emojis/3/snowman2.png b/public/-/emojis/3/snowman2.png
new file mode 100644
index 00000000000..67c7572ca8d
--- /dev/null
+++ b/public/-/emojis/3/snowman2.png
Binary files differ
diff --git a/public/-/emojis/3/sob.png b/public/-/emojis/3/sob.png
new file mode 100644
index 00000000000..5e386c89b4e
--- /dev/null
+++ b/public/-/emojis/3/sob.png
Binary files differ
diff --git a/public/-/emojis/3/soccer.png b/public/-/emojis/3/soccer.png
new file mode 100644
index 00000000000..a850058d657
--- /dev/null
+++ b/public/-/emojis/3/soccer.png
Binary files differ
diff --git a/public/-/emojis/3/soon.png b/public/-/emojis/3/soon.png
new file mode 100644
index 00000000000..1a2c8507203
--- /dev/null
+++ b/public/-/emojis/3/soon.png
Binary files differ
diff --git a/public/-/emojis/3/sos.png b/public/-/emojis/3/sos.png
new file mode 100644
index 00000000000..5d014eb40a0
--- /dev/null
+++ b/public/-/emojis/3/sos.png
Binary files differ
diff --git a/public/-/emojis/3/sound.png b/public/-/emojis/3/sound.png
new file mode 100644
index 00000000000..8745dcbfea6
--- /dev/null
+++ b/public/-/emojis/3/sound.png
Binary files differ
diff --git a/public/-/emojis/3/space_invader.png b/public/-/emojis/3/space_invader.png
new file mode 100644
index 00000000000..20b6a7dc292
--- /dev/null
+++ b/public/-/emojis/3/space_invader.png
Binary files differ
diff --git a/public/-/emojis/3/spades.png b/public/-/emojis/3/spades.png
new file mode 100644
index 00000000000..391970ee89f
--- /dev/null
+++ b/public/-/emojis/3/spades.png
Binary files differ
diff --git a/public/-/emojis/3/spaghetti.png b/public/-/emojis/3/spaghetti.png
new file mode 100644
index 00000000000..7b090699d8c
--- /dev/null
+++ b/public/-/emojis/3/spaghetti.png
Binary files differ
diff --git a/public/-/emojis/3/sparkle.png b/public/-/emojis/3/sparkle.png
new file mode 100644
index 00000000000..0afb772cea3
--- /dev/null
+++ b/public/-/emojis/3/sparkle.png
Binary files differ
diff --git a/public/-/emojis/3/sparkler.png b/public/-/emojis/3/sparkler.png
new file mode 100644
index 00000000000..d161be56b5e
--- /dev/null
+++ b/public/-/emojis/3/sparkler.png
Binary files differ
diff --git a/public/-/emojis/3/sparkles.png b/public/-/emojis/3/sparkles.png
new file mode 100644
index 00000000000..bb495239cfb
--- /dev/null
+++ b/public/-/emojis/3/sparkles.png
Binary files differ
diff --git a/public/-/emojis/3/sparkling_heart.png b/public/-/emojis/3/sparkling_heart.png
new file mode 100644
index 00000000000..32dcf9aa915
--- /dev/null
+++ b/public/-/emojis/3/sparkling_heart.png
Binary files differ
diff --git a/public/-/emojis/3/speak_no_evil.png b/public/-/emojis/3/speak_no_evil.png
new file mode 100644
index 00000000000..e9bfeec5f42
--- /dev/null
+++ b/public/-/emojis/3/speak_no_evil.png
Binary files differ
diff --git a/public/-/emojis/3/speaker.png b/public/-/emojis/3/speaker.png
new file mode 100644
index 00000000000..53a81d34d85
--- /dev/null
+++ b/public/-/emojis/3/speaker.png
Binary files differ
diff --git a/public/-/emojis/3/speaking_head.png b/public/-/emojis/3/speaking_head.png
new file mode 100644
index 00000000000..98d13b4655f
--- /dev/null
+++ b/public/-/emojis/3/speaking_head.png
Binary files differ
diff --git a/public/-/emojis/3/speech_balloon.png b/public/-/emojis/3/speech_balloon.png
new file mode 100644
index 00000000000..68931bd98bd
--- /dev/null
+++ b/public/-/emojis/3/speech_balloon.png
Binary files differ
diff --git a/public/-/emojis/3/speech_left.png b/public/-/emojis/3/speech_left.png
new file mode 100644
index 00000000000..752e6968ef7
--- /dev/null
+++ b/public/-/emojis/3/speech_left.png
Binary files differ
diff --git a/public/-/emojis/3/speedboat.png b/public/-/emojis/3/speedboat.png
new file mode 100644
index 00000000000..0ea80037c32
--- /dev/null
+++ b/public/-/emojis/3/speedboat.png
Binary files differ
diff --git a/public/-/emojis/3/spider.png b/public/-/emojis/3/spider.png
new file mode 100644
index 00000000000..afabcf2cea4
--- /dev/null
+++ b/public/-/emojis/3/spider.png
Binary files differ
diff --git a/public/-/emojis/3/spider_web.png b/public/-/emojis/3/spider_web.png
new file mode 100644
index 00000000000..145f80430af
--- /dev/null
+++ b/public/-/emojis/3/spider_web.png
Binary files differ
diff --git a/public/-/emojis/3/spoon.png b/public/-/emojis/3/spoon.png
new file mode 100644
index 00000000000..e1b338a307c
--- /dev/null
+++ b/public/-/emojis/3/spoon.png
Binary files differ
diff --git a/public/-/emojis/3/spy.png b/public/-/emojis/3/spy.png
new file mode 100644
index 00000000000..7fe4c2b0066
--- /dev/null
+++ b/public/-/emojis/3/spy.png
Binary files differ
diff --git a/public/-/emojis/3/spy_tone1.png b/public/-/emojis/3/spy_tone1.png
new file mode 100644
index 00000000000..019fe0e53b2
--- /dev/null
+++ b/public/-/emojis/3/spy_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/spy_tone2.png b/public/-/emojis/3/spy_tone2.png
new file mode 100644
index 00000000000..cadc40ed53c
--- /dev/null
+++ b/public/-/emojis/3/spy_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/spy_tone3.png b/public/-/emojis/3/spy_tone3.png
new file mode 100644
index 00000000000..d994a09ecf4
--- /dev/null
+++ b/public/-/emojis/3/spy_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/spy_tone4.png b/public/-/emojis/3/spy_tone4.png
new file mode 100644
index 00000000000..92695bc1074
--- /dev/null
+++ b/public/-/emojis/3/spy_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/spy_tone5.png b/public/-/emojis/3/spy_tone5.png
new file mode 100644
index 00000000000..78f55b54ada
--- /dev/null
+++ b/public/-/emojis/3/spy_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/squid.png b/public/-/emojis/3/squid.png
new file mode 100644
index 00000000000..c6a4e665b74
--- /dev/null
+++ b/public/-/emojis/3/squid.png
Binary files differ
diff --git a/public/-/emojis/3/stadium.png b/public/-/emojis/3/stadium.png
new file mode 100644
index 00000000000..facc7d4fc36
--- /dev/null
+++ b/public/-/emojis/3/stadium.png
Binary files differ
diff --git a/public/-/emojis/3/star.png b/public/-/emojis/3/star.png
new file mode 100644
index 00000000000..aac6c64711d
--- /dev/null
+++ b/public/-/emojis/3/star.png
Binary files differ
diff --git a/public/-/emojis/3/star2.png b/public/-/emojis/3/star2.png
new file mode 100644
index 00000000000..adaf11e6e39
--- /dev/null
+++ b/public/-/emojis/3/star2.png
Binary files differ
diff --git a/public/-/emojis/3/star_and_crescent.png b/public/-/emojis/3/star_and_crescent.png
new file mode 100644
index 00000000000..5ab5193e942
--- /dev/null
+++ b/public/-/emojis/3/star_and_crescent.png
Binary files differ
diff --git a/public/-/emojis/3/star_of_david.png b/public/-/emojis/3/star_of_david.png
new file mode 100644
index 00000000000..88e957492c2
--- /dev/null
+++ b/public/-/emojis/3/star_of_david.png
Binary files differ
diff --git a/public/-/emojis/3/stars.png b/public/-/emojis/3/stars.png
new file mode 100644
index 00000000000..4567af681a5
--- /dev/null
+++ b/public/-/emojis/3/stars.png
Binary files differ
diff --git a/public/-/emojis/3/station.png b/public/-/emojis/3/station.png
new file mode 100644
index 00000000000..d8a43944883
--- /dev/null
+++ b/public/-/emojis/3/station.png
Binary files differ
diff --git a/public/-/emojis/3/statue_of_liberty.png b/public/-/emojis/3/statue_of_liberty.png
new file mode 100644
index 00000000000..6a6a59c078d
--- /dev/null
+++ b/public/-/emojis/3/statue_of_liberty.png
Binary files differ
diff --git a/public/-/emojis/3/steam_locomotive.png b/public/-/emojis/3/steam_locomotive.png
new file mode 100644
index 00000000000..95a570c62bd
--- /dev/null
+++ b/public/-/emojis/3/steam_locomotive.png
Binary files differ
diff --git a/public/-/emojis/3/stew.png b/public/-/emojis/3/stew.png
new file mode 100644
index 00000000000..369f9c6fca5
--- /dev/null
+++ b/public/-/emojis/3/stew.png
Binary files differ
diff --git a/public/-/emojis/3/stop_button.png b/public/-/emojis/3/stop_button.png
new file mode 100644
index 00000000000..5df39ab4adb
--- /dev/null
+++ b/public/-/emojis/3/stop_button.png
Binary files differ
diff --git a/public/-/emojis/3/stopwatch.png b/public/-/emojis/3/stopwatch.png
new file mode 100644
index 00000000000..1610ba9e6fc
--- /dev/null
+++ b/public/-/emojis/3/stopwatch.png
Binary files differ
diff --git a/public/-/emojis/3/straight_ruler.png b/public/-/emojis/3/straight_ruler.png
new file mode 100644
index 00000000000..3479fb1c6bd
--- /dev/null
+++ b/public/-/emojis/3/straight_ruler.png
Binary files differ
diff --git a/public/-/emojis/3/strawberry.png b/public/-/emojis/3/strawberry.png
new file mode 100644
index 00000000000..726356e830d
--- /dev/null
+++ b/public/-/emojis/3/strawberry.png
Binary files differ
diff --git a/public/-/emojis/3/stuck_out_tongue.png b/public/-/emojis/3/stuck_out_tongue.png
new file mode 100644
index 00000000000..0d48279a13e
--- /dev/null
+++ b/public/-/emojis/3/stuck_out_tongue.png
Binary files differ
diff --git a/public/-/emojis/3/stuck_out_tongue_closed_eyes.png b/public/-/emojis/3/stuck_out_tongue_closed_eyes.png
new file mode 100644
index 00000000000..6c2d8152d3d
--- /dev/null
+++ b/public/-/emojis/3/stuck_out_tongue_closed_eyes.png
Binary files differ
diff --git a/public/-/emojis/3/stuck_out_tongue_winking_eye.png b/public/-/emojis/3/stuck_out_tongue_winking_eye.png
new file mode 100644
index 00000000000..0d939158bac
--- /dev/null
+++ b/public/-/emojis/3/stuck_out_tongue_winking_eye.png
Binary files differ
diff --git a/public/-/emojis/3/stuffed_flatbread.png b/public/-/emojis/3/stuffed_flatbread.png
new file mode 100644
index 00000000000..fab6f0cf60e
--- /dev/null
+++ b/public/-/emojis/3/stuffed_flatbread.png
Binary files differ
diff --git a/public/-/emojis/3/sun_with_face.png b/public/-/emojis/3/sun_with_face.png
new file mode 100644
index 00000000000..09ca3548bda
--- /dev/null
+++ b/public/-/emojis/3/sun_with_face.png
Binary files differ
diff --git a/public/-/emojis/3/sunflower.png b/public/-/emojis/3/sunflower.png
new file mode 100644
index 00000000000..470cff7ab65
--- /dev/null
+++ b/public/-/emojis/3/sunflower.png
Binary files differ
diff --git a/public/-/emojis/3/sunglasses.png b/public/-/emojis/3/sunglasses.png
new file mode 100644
index 00000000000..c5e7e6e5287
--- /dev/null
+++ b/public/-/emojis/3/sunglasses.png
Binary files differ
diff --git a/public/-/emojis/3/sunny.png b/public/-/emojis/3/sunny.png
new file mode 100644
index 00000000000..a746a886798
--- /dev/null
+++ b/public/-/emojis/3/sunny.png
Binary files differ
diff --git a/public/-/emojis/3/sunrise.png b/public/-/emojis/3/sunrise.png
new file mode 100644
index 00000000000..369d13fc5a7
--- /dev/null
+++ b/public/-/emojis/3/sunrise.png
Binary files differ
diff --git a/public/-/emojis/3/sunrise_over_mountains.png b/public/-/emojis/3/sunrise_over_mountains.png
new file mode 100644
index 00000000000..7ef6d6dfc1f
--- /dev/null
+++ b/public/-/emojis/3/sunrise_over_mountains.png
Binary files differ
diff --git a/public/-/emojis/3/surfer.png b/public/-/emojis/3/surfer.png
new file mode 100644
index 00000000000..b4dee78ca5d
--- /dev/null
+++ b/public/-/emojis/3/surfer.png
Binary files differ
diff --git a/public/-/emojis/3/surfer_tone1.png b/public/-/emojis/3/surfer_tone1.png
new file mode 100644
index 00000000000..12f91117845
--- /dev/null
+++ b/public/-/emojis/3/surfer_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/surfer_tone2.png b/public/-/emojis/3/surfer_tone2.png
new file mode 100644
index 00000000000..2ce3ec7b545
--- /dev/null
+++ b/public/-/emojis/3/surfer_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/surfer_tone3.png b/public/-/emojis/3/surfer_tone3.png
new file mode 100644
index 00000000000..46d81855192
--- /dev/null
+++ b/public/-/emojis/3/surfer_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/surfer_tone4.png b/public/-/emojis/3/surfer_tone4.png
new file mode 100644
index 00000000000..a45e877308e
--- /dev/null
+++ b/public/-/emojis/3/surfer_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/surfer_tone5.png b/public/-/emojis/3/surfer_tone5.png
new file mode 100644
index 00000000000..f76eed71b82
--- /dev/null
+++ b/public/-/emojis/3/surfer_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/sushi.png b/public/-/emojis/3/sushi.png
new file mode 100644
index 00000000000..6c4b87ec2f6
--- /dev/null
+++ b/public/-/emojis/3/sushi.png
Binary files differ
diff --git a/public/-/emojis/3/suspension_railway.png b/public/-/emojis/3/suspension_railway.png
new file mode 100644
index 00000000000..e1aa1a18873
--- /dev/null
+++ b/public/-/emojis/3/suspension_railway.png
Binary files differ
diff --git a/public/-/emojis/3/sweat.png b/public/-/emojis/3/sweat.png
new file mode 100644
index 00000000000..2122850839b
--- /dev/null
+++ b/public/-/emojis/3/sweat.png
Binary files differ
diff --git a/public/-/emojis/3/sweat_drops.png b/public/-/emojis/3/sweat_drops.png
new file mode 100644
index 00000000000..392c04cc7ba
--- /dev/null
+++ b/public/-/emojis/3/sweat_drops.png
Binary files differ
diff --git a/public/-/emojis/3/sweat_smile.png b/public/-/emojis/3/sweat_smile.png
new file mode 100644
index 00000000000..f632526bdf8
--- /dev/null
+++ b/public/-/emojis/3/sweat_smile.png
Binary files differ
diff --git a/public/-/emojis/3/sweet_potato.png b/public/-/emojis/3/sweet_potato.png
new file mode 100644
index 00000000000..abfbe70a528
--- /dev/null
+++ b/public/-/emojis/3/sweet_potato.png
Binary files differ
diff --git a/public/-/emojis/3/swimmer.png b/public/-/emojis/3/swimmer.png
new file mode 100644
index 00000000000..985910c928c
--- /dev/null
+++ b/public/-/emojis/3/swimmer.png
Binary files differ
diff --git a/public/-/emojis/3/swimmer_tone1.png b/public/-/emojis/3/swimmer_tone1.png
new file mode 100644
index 00000000000..7735655aaab
--- /dev/null
+++ b/public/-/emojis/3/swimmer_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/swimmer_tone2.png b/public/-/emojis/3/swimmer_tone2.png
new file mode 100644
index 00000000000..e3d8f4ef2c8
--- /dev/null
+++ b/public/-/emojis/3/swimmer_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/swimmer_tone3.png b/public/-/emojis/3/swimmer_tone3.png
new file mode 100644
index 00000000000..4d187d1f13e
--- /dev/null
+++ b/public/-/emojis/3/swimmer_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/swimmer_tone4.png b/public/-/emojis/3/swimmer_tone4.png
new file mode 100644
index 00000000000..e19f0fcd07f
--- /dev/null
+++ b/public/-/emojis/3/swimmer_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/swimmer_tone5.png b/public/-/emojis/3/swimmer_tone5.png
new file mode 100644
index 00000000000..bc485bebe87
--- /dev/null
+++ b/public/-/emojis/3/swimmer_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/symbols.png b/public/-/emojis/3/symbols.png
new file mode 100644
index 00000000000..dcd10763aec
--- /dev/null
+++ b/public/-/emojis/3/symbols.png
Binary files differ
diff --git a/public/-/emojis/3/synagogue.png b/public/-/emojis/3/synagogue.png
new file mode 100644
index 00000000000..bf42bf77742
--- /dev/null
+++ b/public/-/emojis/3/synagogue.png
Binary files differ
diff --git a/public/-/emojis/3/syringe.png b/public/-/emojis/3/syringe.png
new file mode 100644
index 00000000000..6bff9419b63
--- /dev/null
+++ b/public/-/emojis/3/syringe.png
Binary files differ
diff --git a/public/-/emojis/3/taco.png b/public/-/emojis/3/taco.png
new file mode 100644
index 00000000000..6760f289b87
--- /dev/null
+++ b/public/-/emojis/3/taco.png
Binary files differ
diff --git a/public/-/emojis/3/tada.png b/public/-/emojis/3/tada.png
new file mode 100644
index 00000000000..29822f25ad1
--- /dev/null
+++ b/public/-/emojis/3/tada.png
Binary files differ
diff --git a/public/-/emojis/3/tanabata_tree.png b/public/-/emojis/3/tanabata_tree.png
new file mode 100644
index 00000000000..b944198d57a
--- /dev/null
+++ b/public/-/emojis/3/tanabata_tree.png
Binary files differ
diff --git a/public/-/emojis/3/tangerine.png b/public/-/emojis/3/tangerine.png
new file mode 100644
index 00000000000..90ae7bec598
--- /dev/null
+++ b/public/-/emojis/3/tangerine.png
Binary files differ
diff --git a/public/-/emojis/3/taurus.png b/public/-/emojis/3/taurus.png
new file mode 100644
index 00000000000..f6cac21ebe0
--- /dev/null
+++ b/public/-/emojis/3/taurus.png
Binary files differ
diff --git a/public/-/emojis/3/taxi.png b/public/-/emojis/3/taxi.png
new file mode 100644
index 00000000000..fc1b5c2c7ef
--- /dev/null
+++ b/public/-/emojis/3/taxi.png
Binary files differ
diff --git a/public/-/emojis/3/tea.png b/public/-/emojis/3/tea.png
new file mode 100644
index 00000000000..1ce3af7dcae
--- /dev/null
+++ b/public/-/emojis/3/tea.png
Binary files differ
diff --git a/public/-/emojis/3/telephone.png b/public/-/emojis/3/telephone.png
new file mode 100644
index 00000000000..715668433bd
--- /dev/null
+++ b/public/-/emojis/3/telephone.png
Binary files differ
diff --git a/public/-/emojis/3/telephone_receiver.png b/public/-/emojis/3/telephone_receiver.png
new file mode 100644
index 00000000000..310596e7c22
--- /dev/null
+++ b/public/-/emojis/3/telephone_receiver.png
Binary files differ
diff --git a/public/-/emojis/3/telescope.png b/public/-/emojis/3/telescope.png
new file mode 100644
index 00000000000..c2a5539f619
--- /dev/null
+++ b/public/-/emojis/3/telescope.png
Binary files differ
diff --git a/public/-/emojis/3/ten.png b/public/-/emojis/3/ten.png
new file mode 100644
index 00000000000..095b845f60f
--- /dev/null
+++ b/public/-/emojis/3/ten.png
Binary files differ
diff --git a/public/-/emojis/3/tennis.png b/public/-/emojis/3/tennis.png
new file mode 100644
index 00000000000..5e17e45d876
--- /dev/null
+++ b/public/-/emojis/3/tennis.png
Binary files differ
diff --git a/public/-/emojis/3/tent.png b/public/-/emojis/3/tent.png
new file mode 100644
index 00000000000..154bf32a14c
--- /dev/null
+++ b/public/-/emojis/3/tent.png
Binary files differ
diff --git a/public/-/emojis/3/thermometer.png b/public/-/emojis/3/thermometer.png
new file mode 100644
index 00000000000..59948a90828
--- /dev/null
+++ b/public/-/emojis/3/thermometer.png
Binary files differ
diff --git a/public/-/emojis/3/thermometer_face.png b/public/-/emojis/3/thermometer_face.png
new file mode 100644
index 00000000000..7f66be79dc2
--- /dev/null
+++ b/public/-/emojis/3/thermometer_face.png
Binary files differ
diff --git a/public/-/emojis/3/thinking.png b/public/-/emojis/3/thinking.png
new file mode 100644
index 00000000000..9d2d2239434
--- /dev/null
+++ b/public/-/emojis/3/thinking.png
Binary files differ
diff --git a/public/-/emojis/3/third_place.png b/public/-/emojis/3/third_place.png
new file mode 100644
index 00000000000..8735d669da5
--- /dev/null
+++ b/public/-/emojis/3/third_place.png
Binary files differ
diff --git a/public/-/emojis/3/thought_balloon.png b/public/-/emojis/3/thought_balloon.png
new file mode 100644
index 00000000000..d03489c53c2
--- /dev/null
+++ b/public/-/emojis/3/thought_balloon.png
Binary files differ
diff --git a/public/-/emojis/3/three.png b/public/-/emojis/3/three.png
new file mode 100644
index 00000000000..ff06694841c
--- /dev/null
+++ b/public/-/emojis/3/three.png
Binary files differ
diff --git a/public/-/emojis/3/thumbsdown.png b/public/-/emojis/3/thumbsdown.png
new file mode 100644
index 00000000000..5f46c1e086d
--- /dev/null
+++ b/public/-/emojis/3/thumbsdown.png
Binary files differ
diff --git a/public/-/emojis/3/thumbsdown_tone1.png b/public/-/emojis/3/thumbsdown_tone1.png
new file mode 100644
index 00000000000..a1a4f42c338
--- /dev/null
+++ b/public/-/emojis/3/thumbsdown_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/thumbsdown_tone2.png b/public/-/emojis/3/thumbsdown_tone2.png
new file mode 100644
index 00000000000..eae40534e95
--- /dev/null
+++ b/public/-/emojis/3/thumbsdown_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/thumbsdown_tone3.png b/public/-/emojis/3/thumbsdown_tone3.png
new file mode 100644
index 00000000000..d795af13536
--- /dev/null
+++ b/public/-/emojis/3/thumbsdown_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/thumbsdown_tone4.png b/public/-/emojis/3/thumbsdown_tone4.png
new file mode 100644
index 00000000000..07d1afac9c5
--- /dev/null
+++ b/public/-/emojis/3/thumbsdown_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/thumbsdown_tone5.png b/public/-/emojis/3/thumbsdown_tone5.png
new file mode 100644
index 00000000000..847ce3a82ce
--- /dev/null
+++ b/public/-/emojis/3/thumbsdown_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/thumbsup.png b/public/-/emojis/3/thumbsup.png
new file mode 100644
index 00000000000..5cebe8a7787
--- /dev/null
+++ b/public/-/emojis/3/thumbsup.png
Binary files differ
diff --git a/public/-/emojis/3/thumbsup_tone1.png b/public/-/emojis/3/thumbsup_tone1.png
new file mode 100644
index 00000000000..76b734a2e32
--- /dev/null
+++ b/public/-/emojis/3/thumbsup_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/thumbsup_tone2.png b/public/-/emojis/3/thumbsup_tone2.png
new file mode 100644
index 00000000000..c7d900d86c5
--- /dev/null
+++ b/public/-/emojis/3/thumbsup_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/thumbsup_tone3.png b/public/-/emojis/3/thumbsup_tone3.png
new file mode 100644
index 00000000000..5bd66fd212d
--- /dev/null
+++ b/public/-/emojis/3/thumbsup_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/thumbsup_tone4.png b/public/-/emojis/3/thumbsup_tone4.png
new file mode 100644
index 00000000000..37b2d15fd7b
--- /dev/null
+++ b/public/-/emojis/3/thumbsup_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/thumbsup_tone5.png b/public/-/emojis/3/thumbsup_tone5.png
new file mode 100644
index 00000000000..c5d208280c3
--- /dev/null
+++ b/public/-/emojis/3/thumbsup_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/thunder_cloud_rain.png b/public/-/emojis/3/thunder_cloud_rain.png
new file mode 100644
index 00000000000..c6e07334eb9
--- /dev/null
+++ b/public/-/emojis/3/thunder_cloud_rain.png
Binary files differ
diff --git a/public/-/emojis/3/ticket.png b/public/-/emojis/3/ticket.png
new file mode 100644
index 00000000000..b8949469865
--- /dev/null
+++ b/public/-/emojis/3/ticket.png
Binary files differ
diff --git a/public/-/emojis/3/tickets.png b/public/-/emojis/3/tickets.png
new file mode 100644
index 00000000000..1b240c3b83e
--- /dev/null
+++ b/public/-/emojis/3/tickets.png
Binary files differ
diff --git a/public/-/emojis/3/tiger.png b/public/-/emojis/3/tiger.png
new file mode 100644
index 00000000000..398db6aceeb
--- /dev/null
+++ b/public/-/emojis/3/tiger.png
Binary files differ
diff --git a/public/-/emojis/3/tiger2.png b/public/-/emojis/3/tiger2.png
new file mode 100644
index 00000000000..e5409e67fb4
--- /dev/null
+++ b/public/-/emojis/3/tiger2.png
Binary files differ
diff --git a/public/-/emojis/3/timer.png b/public/-/emojis/3/timer.png
new file mode 100644
index 00000000000..dcff365af63
--- /dev/null
+++ b/public/-/emojis/3/timer.png
Binary files differ
diff --git a/public/-/emojis/3/tired_face.png b/public/-/emojis/3/tired_face.png
new file mode 100644
index 00000000000..823b8aaeba0
--- /dev/null
+++ b/public/-/emojis/3/tired_face.png
Binary files differ
diff --git a/public/-/emojis/3/tm.png b/public/-/emojis/3/tm.png
new file mode 100644
index 00000000000..d353953a40c
--- /dev/null
+++ b/public/-/emojis/3/tm.png
Binary files differ
diff --git a/public/-/emojis/3/toilet.png b/public/-/emojis/3/toilet.png
new file mode 100644
index 00000000000..828879db5ec
--- /dev/null
+++ b/public/-/emojis/3/toilet.png
Binary files differ
diff --git a/public/-/emojis/3/tokyo_tower.png b/public/-/emojis/3/tokyo_tower.png
new file mode 100644
index 00000000000..4366f3413a6
--- /dev/null
+++ b/public/-/emojis/3/tokyo_tower.png
Binary files differ
diff --git a/public/-/emojis/3/tomato.png b/public/-/emojis/3/tomato.png
new file mode 100644
index 00000000000..74ba9aa90a8
--- /dev/null
+++ b/public/-/emojis/3/tomato.png
Binary files differ
diff --git a/public/-/emojis/3/tone1.png b/public/-/emojis/3/tone1.png
new file mode 100644
index 00000000000..d36f06f2763
--- /dev/null
+++ b/public/-/emojis/3/tone1.png
Binary files differ
diff --git a/public/-/emojis/3/tone2.png b/public/-/emojis/3/tone2.png
new file mode 100644
index 00000000000..8c08edd81cb
--- /dev/null
+++ b/public/-/emojis/3/tone2.png
Binary files differ
diff --git a/public/-/emojis/3/tone3.png b/public/-/emojis/3/tone3.png
new file mode 100644
index 00000000000..af925abe698
--- /dev/null
+++ b/public/-/emojis/3/tone3.png
Binary files differ
diff --git a/public/-/emojis/3/tone4.png b/public/-/emojis/3/tone4.png
new file mode 100644
index 00000000000..fb59d7b4419
--- /dev/null
+++ b/public/-/emojis/3/tone4.png
Binary files differ
diff --git a/public/-/emojis/3/tone5.png b/public/-/emojis/3/tone5.png
new file mode 100644
index 00000000000..b504a423322
--- /dev/null
+++ b/public/-/emojis/3/tone5.png
Binary files differ
diff --git a/public/-/emojis/3/tongue.png b/public/-/emojis/3/tongue.png
new file mode 100644
index 00000000000..d0d9d44b2ec
--- /dev/null
+++ b/public/-/emojis/3/tongue.png
Binary files differ
diff --git a/public/-/emojis/3/tools.png b/public/-/emojis/3/tools.png
new file mode 100644
index 00000000000..6a1936bc74e
--- /dev/null
+++ b/public/-/emojis/3/tools.png
Binary files differ
diff --git a/public/-/emojis/3/top.png b/public/-/emojis/3/top.png
new file mode 100644
index 00000000000..25dab3dea95
--- /dev/null
+++ b/public/-/emojis/3/top.png
Binary files differ
diff --git a/public/-/emojis/3/tophat.png b/public/-/emojis/3/tophat.png
new file mode 100644
index 00000000000..9f783e522cb
--- /dev/null
+++ b/public/-/emojis/3/tophat.png
Binary files differ
diff --git a/public/-/emojis/3/track_next.png b/public/-/emojis/3/track_next.png
new file mode 100644
index 00000000000..f6d79d955d9
--- /dev/null
+++ b/public/-/emojis/3/track_next.png
Binary files differ
diff --git a/public/-/emojis/3/track_previous.png b/public/-/emojis/3/track_previous.png
new file mode 100644
index 00000000000..ec520b9356d
--- /dev/null
+++ b/public/-/emojis/3/track_previous.png
Binary files differ
diff --git a/public/-/emojis/3/trackball.png b/public/-/emojis/3/trackball.png
new file mode 100644
index 00000000000..b3e7df4c87b
--- /dev/null
+++ b/public/-/emojis/3/trackball.png
Binary files differ
diff --git a/public/-/emojis/3/tractor.png b/public/-/emojis/3/tractor.png
new file mode 100644
index 00000000000..63c75e8845b
--- /dev/null
+++ b/public/-/emojis/3/tractor.png
Binary files differ
diff --git a/public/-/emojis/3/traffic_light.png b/public/-/emojis/3/traffic_light.png
new file mode 100644
index 00000000000..d93af70b1f5
--- /dev/null
+++ b/public/-/emojis/3/traffic_light.png
Binary files differ
diff --git a/public/-/emojis/3/train.png b/public/-/emojis/3/train.png
new file mode 100644
index 00000000000..f96833196a9
--- /dev/null
+++ b/public/-/emojis/3/train.png
Binary files differ
diff --git a/public/-/emojis/3/train2.png b/public/-/emojis/3/train2.png
new file mode 100644
index 00000000000..5062b323b46
--- /dev/null
+++ b/public/-/emojis/3/train2.png
Binary files differ
diff --git a/public/-/emojis/3/tram.png b/public/-/emojis/3/tram.png
new file mode 100644
index 00000000000..c544d624f42
--- /dev/null
+++ b/public/-/emojis/3/tram.png
Binary files differ
diff --git a/public/-/emojis/3/triangular_flag_on_post.png b/public/-/emojis/3/triangular_flag_on_post.png
new file mode 100644
index 00000000000..f1d2b3b3ee7
--- /dev/null
+++ b/public/-/emojis/3/triangular_flag_on_post.png
Binary files differ
diff --git a/public/-/emojis/3/triangular_ruler.png b/public/-/emojis/3/triangular_ruler.png
new file mode 100644
index 00000000000..06d1b219853
--- /dev/null
+++ b/public/-/emojis/3/triangular_ruler.png
Binary files differ
diff --git a/public/-/emojis/3/trident.png b/public/-/emojis/3/trident.png
new file mode 100644
index 00000000000..00bbaf72b0e
--- /dev/null
+++ b/public/-/emojis/3/trident.png
Binary files differ
diff --git a/public/-/emojis/3/triumph.png b/public/-/emojis/3/triumph.png
new file mode 100644
index 00000000000..950ce46587f
--- /dev/null
+++ b/public/-/emojis/3/triumph.png
Binary files differ
diff --git a/public/-/emojis/3/trolleybus.png b/public/-/emojis/3/trolleybus.png
new file mode 100644
index 00000000000..9c28d71c413
--- /dev/null
+++ b/public/-/emojis/3/trolleybus.png
Binary files differ
diff --git a/public/-/emojis/3/trophy.png b/public/-/emojis/3/trophy.png
new file mode 100644
index 00000000000..b2ad4f41106
--- /dev/null
+++ b/public/-/emojis/3/trophy.png
Binary files differ
diff --git a/public/-/emojis/3/tropical_drink.png b/public/-/emojis/3/tropical_drink.png
new file mode 100644
index 00000000000..2d65920c251
--- /dev/null
+++ b/public/-/emojis/3/tropical_drink.png
Binary files differ
diff --git a/public/-/emojis/3/tropical_fish.png b/public/-/emojis/3/tropical_fish.png
new file mode 100644
index 00000000000..d45b9c27f09
--- /dev/null
+++ b/public/-/emojis/3/tropical_fish.png
Binary files differ
diff --git a/public/-/emojis/3/truck.png b/public/-/emojis/3/truck.png
new file mode 100644
index 00000000000..ebd86859411
--- /dev/null
+++ b/public/-/emojis/3/truck.png
Binary files differ
diff --git a/public/-/emojis/3/trumpet.png b/public/-/emojis/3/trumpet.png
new file mode 100644
index 00000000000..c3d1f597f99
--- /dev/null
+++ b/public/-/emojis/3/trumpet.png
Binary files differ
diff --git a/public/-/emojis/3/tulip.png b/public/-/emojis/3/tulip.png
new file mode 100644
index 00000000000..8645096f26c
--- /dev/null
+++ b/public/-/emojis/3/tulip.png
Binary files differ
diff --git a/public/-/emojis/3/tumbler_glass.png b/public/-/emojis/3/tumbler_glass.png
new file mode 100644
index 00000000000..82d941552da
--- /dev/null
+++ b/public/-/emojis/3/tumbler_glass.png
Binary files differ
diff --git a/public/-/emojis/3/turkey.png b/public/-/emojis/3/turkey.png
new file mode 100644
index 00000000000..9de58f0f129
--- /dev/null
+++ b/public/-/emojis/3/turkey.png
Binary files differ
diff --git a/public/-/emojis/3/turtle.png b/public/-/emojis/3/turtle.png
new file mode 100644
index 00000000000..c4b54451a92
--- /dev/null
+++ b/public/-/emojis/3/turtle.png
Binary files differ
diff --git a/public/-/emojis/3/tv.png b/public/-/emojis/3/tv.png
new file mode 100644
index 00000000000..3f07ce033de
--- /dev/null
+++ b/public/-/emojis/3/tv.png
Binary files differ
diff --git a/public/-/emojis/3/twisted_rightwards_arrows.png b/public/-/emojis/3/twisted_rightwards_arrows.png
new file mode 100644
index 00000000000..4fe9d76f52d
--- /dev/null
+++ b/public/-/emojis/3/twisted_rightwards_arrows.png
Binary files differ
diff --git a/public/-/emojis/3/two.png b/public/-/emojis/3/two.png
new file mode 100644
index 00000000000..1cce3c264ac
--- /dev/null
+++ b/public/-/emojis/3/two.png
Binary files differ
diff --git a/public/-/emojis/3/two_hearts.png b/public/-/emojis/3/two_hearts.png
new file mode 100644
index 00000000000..6208168d076
--- /dev/null
+++ b/public/-/emojis/3/two_hearts.png
Binary files differ
diff --git a/public/-/emojis/3/two_men_holding_hands.png b/public/-/emojis/3/two_men_holding_hands.png
new file mode 100644
index 00000000000..3cacda77c25
--- /dev/null
+++ b/public/-/emojis/3/two_men_holding_hands.png
Binary files differ
diff --git a/public/-/emojis/3/two_women_holding_hands.png b/public/-/emojis/3/two_women_holding_hands.png
new file mode 100644
index 00000000000..f141a967450
--- /dev/null
+++ b/public/-/emojis/3/two_women_holding_hands.png
Binary files differ
diff --git a/public/-/emojis/3/u5272.png b/public/-/emojis/3/u5272.png
new file mode 100644
index 00000000000..af2bdaf6f67
--- /dev/null
+++ b/public/-/emojis/3/u5272.png
Binary files differ
diff --git a/public/-/emojis/3/u5408.png b/public/-/emojis/3/u5408.png
new file mode 100644
index 00000000000..f5b45188612
--- /dev/null
+++ b/public/-/emojis/3/u5408.png
Binary files differ
diff --git a/public/-/emojis/3/u55b6.png b/public/-/emojis/3/u55b6.png
new file mode 100644
index 00000000000..3e092ffecc2
--- /dev/null
+++ b/public/-/emojis/3/u55b6.png
Binary files differ
diff --git a/public/-/emojis/3/u6307.png b/public/-/emojis/3/u6307.png
new file mode 100644
index 00000000000..2a0e7a8a65f
--- /dev/null
+++ b/public/-/emojis/3/u6307.png
Binary files differ
diff --git a/public/-/emojis/3/u6708.png b/public/-/emojis/3/u6708.png
new file mode 100644
index 00000000000..d039b4606f1
--- /dev/null
+++ b/public/-/emojis/3/u6708.png
Binary files differ
diff --git a/public/-/emojis/3/u6709.png b/public/-/emojis/3/u6709.png
new file mode 100644
index 00000000000..b789aab56f4
--- /dev/null
+++ b/public/-/emojis/3/u6709.png
Binary files differ
diff --git a/public/-/emojis/3/u6e80.png b/public/-/emojis/3/u6e80.png
new file mode 100644
index 00000000000..8289f0fec7f
--- /dev/null
+++ b/public/-/emojis/3/u6e80.png
Binary files differ
diff --git a/public/-/emojis/3/u7121.png b/public/-/emojis/3/u7121.png
new file mode 100644
index 00000000000..eb2c66415b3
--- /dev/null
+++ b/public/-/emojis/3/u7121.png
Binary files differ
diff --git a/public/-/emojis/3/u7533.png b/public/-/emojis/3/u7533.png
new file mode 100644
index 00000000000..5829da3fe8e
--- /dev/null
+++ b/public/-/emojis/3/u7533.png
Binary files differ
diff --git a/public/-/emojis/3/u7981.png b/public/-/emojis/3/u7981.png
new file mode 100644
index 00000000000..d455dab4329
--- /dev/null
+++ b/public/-/emojis/3/u7981.png
Binary files differ
diff --git a/public/-/emojis/3/u7a7a.png b/public/-/emojis/3/u7a7a.png
new file mode 100644
index 00000000000..1d3d13041d6
--- /dev/null
+++ b/public/-/emojis/3/u7a7a.png
Binary files differ
diff --git a/public/-/emojis/3/umbrella.png b/public/-/emojis/3/umbrella.png
new file mode 100644
index 00000000000..56a6e5eb923
--- /dev/null
+++ b/public/-/emojis/3/umbrella.png
Binary files differ
diff --git a/public/-/emojis/3/umbrella2.png b/public/-/emojis/3/umbrella2.png
new file mode 100644
index 00000000000..7ce97ac53df
--- /dev/null
+++ b/public/-/emojis/3/umbrella2.png
Binary files differ
diff --git a/public/-/emojis/3/unamused.png b/public/-/emojis/3/unamused.png
new file mode 100644
index 00000000000..0e4cc248916
--- /dev/null
+++ b/public/-/emojis/3/unamused.png
Binary files differ
diff --git a/public/-/emojis/3/underage.png b/public/-/emojis/3/underage.png
new file mode 100644
index 00000000000..9921bd26260
--- /dev/null
+++ b/public/-/emojis/3/underage.png
Binary files differ
diff --git a/public/-/emojis/3/unicorn.png b/public/-/emojis/3/unicorn.png
new file mode 100644
index 00000000000..d37e6849fa3
--- /dev/null
+++ b/public/-/emojis/3/unicorn.png
Binary files differ
diff --git a/public/-/emojis/3/unlock.png b/public/-/emojis/3/unlock.png
new file mode 100644
index 00000000000..a6c556e31d2
--- /dev/null
+++ b/public/-/emojis/3/unlock.png
Binary files differ
diff --git a/public/-/emojis/3/up.png b/public/-/emojis/3/up.png
new file mode 100644
index 00000000000..4f72637fcaf
--- /dev/null
+++ b/public/-/emojis/3/up.png
Binary files differ
diff --git a/public/-/emojis/3/upside_down.png b/public/-/emojis/3/upside_down.png
new file mode 100644
index 00000000000..c993b35aa95
--- /dev/null
+++ b/public/-/emojis/3/upside_down.png
Binary files differ
diff --git a/public/-/emojis/3/urn.png b/public/-/emojis/3/urn.png
new file mode 100644
index 00000000000..340954510da
--- /dev/null
+++ b/public/-/emojis/3/urn.png
Binary files differ
diff --git a/public/-/emojis/3/v.png b/public/-/emojis/3/v.png
new file mode 100644
index 00000000000..55b99f99a95
--- /dev/null
+++ b/public/-/emojis/3/v.png
Binary files differ
diff --git a/public/-/emojis/3/v_tone1.png b/public/-/emojis/3/v_tone1.png
new file mode 100644
index 00000000000..f05ddd7132e
--- /dev/null
+++ b/public/-/emojis/3/v_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/v_tone2.png b/public/-/emojis/3/v_tone2.png
new file mode 100644
index 00000000000..3dc8e215ec6
--- /dev/null
+++ b/public/-/emojis/3/v_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/v_tone3.png b/public/-/emojis/3/v_tone3.png
new file mode 100644
index 00000000000..ac5033f39f1
--- /dev/null
+++ b/public/-/emojis/3/v_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/v_tone4.png b/public/-/emojis/3/v_tone4.png
new file mode 100644
index 00000000000..495c1a2c84b
--- /dev/null
+++ b/public/-/emojis/3/v_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/v_tone5.png b/public/-/emojis/3/v_tone5.png
new file mode 100644
index 00000000000..581e075bf78
--- /dev/null
+++ b/public/-/emojis/3/v_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/vertical_traffic_light.png b/public/-/emojis/3/vertical_traffic_light.png
new file mode 100644
index 00000000000..fc3148c2ab8
--- /dev/null
+++ b/public/-/emojis/3/vertical_traffic_light.png
Binary files differ
diff --git a/public/-/emojis/3/vhs.png b/public/-/emojis/3/vhs.png
new file mode 100644
index 00000000000..11c6e63b4a3
--- /dev/null
+++ b/public/-/emojis/3/vhs.png
Binary files differ
diff --git a/public/-/emojis/3/vibration_mode.png b/public/-/emojis/3/vibration_mode.png
new file mode 100644
index 00000000000..fe2699ba676
--- /dev/null
+++ b/public/-/emojis/3/vibration_mode.png
Binary files differ
diff --git a/public/-/emojis/3/video_camera.png b/public/-/emojis/3/video_camera.png
new file mode 100644
index 00000000000..89d9725d50e
--- /dev/null
+++ b/public/-/emojis/3/video_camera.png
Binary files differ
diff --git a/public/-/emojis/3/video_game.png b/public/-/emojis/3/video_game.png
new file mode 100644
index 00000000000..552c18f8ca5
--- /dev/null
+++ b/public/-/emojis/3/video_game.png
Binary files differ
diff --git a/public/-/emojis/3/violin.png b/public/-/emojis/3/violin.png
new file mode 100644
index 00000000000..578d1c7b2b9
--- /dev/null
+++ b/public/-/emojis/3/violin.png
Binary files differ
diff --git a/public/-/emojis/3/virgo.png b/public/-/emojis/3/virgo.png
new file mode 100644
index 00000000000..2e7254a45e7
--- /dev/null
+++ b/public/-/emojis/3/virgo.png
Binary files differ
diff --git a/public/-/emojis/3/volcano.png b/public/-/emojis/3/volcano.png
new file mode 100644
index 00000000000..bc210edcb1c
--- /dev/null
+++ b/public/-/emojis/3/volcano.png
Binary files differ
diff --git a/public/-/emojis/3/volleyball.png b/public/-/emojis/3/volleyball.png
new file mode 100644
index 00000000000..8eeef790dcc
--- /dev/null
+++ b/public/-/emojis/3/volleyball.png
Binary files differ
diff --git a/public/-/emojis/3/vs.png b/public/-/emojis/3/vs.png
new file mode 100644
index 00000000000..c55dba709a0
--- /dev/null
+++ b/public/-/emojis/3/vs.png
Binary files differ
diff --git a/public/-/emojis/3/vulcan.png b/public/-/emojis/3/vulcan.png
new file mode 100644
index 00000000000..91bedcdb15a
--- /dev/null
+++ b/public/-/emojis/3/vulcan.png
Binary files differ
diff --git a/public/-/emojis/3/vulcan_tone1.png b/public/-/emojis/3/vulcan_tone1.png
new file mode 100644
index 00000000000..ec1fa548399
--- /dev/null
+++ b/public/-/emojis/3/vulcan_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/vulcan_tone2.png b/public/-/emojis/3/vulcan_tone2.png
new file mode 100644
index 00000000000..85c89abfb4e
--- /dev/null
+++ b/public/-/emojis/3/vulcan_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/vulcan_tone3.png b/public/-/emojis/3/vulcan_tone3.png
new file mode 100644
index 00000000000..b0dde6562b8
--- /dev/null
+++ b/public/-/emojis/3/vulcan_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/vulcan_tone4.png b/public/-/emojis/3/vulcan_tone4.png
new file mode 100644
index 00000000000..b1c7ff8f608
--- /dev/null
+++ b/public/-/emojis/3/vulcan_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/vulcan_tone5.png b/public/-/emojis/3/vulcan_tone5.png
new file mode 100644
index 00000000000..6d80c03bd82
--- /dev/null
+++ b/public/-/emojis/3/vulcan_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/walking.png b/public/-/emojis/3/walking.png
new file mode 100644
index 00000000000..d682c0a05b0
--- /dev/null
+++ b/public/-/emojis/3/walking.png
Binary files differ
diff --git a/public/-/emojis/3/walking_tone1.png b/public/-/emojis/3/walking_tone1.png
new file mode 100644
index 00000000000..0012e8bf9be
--- /dev/null
+++ b/public/-/emojis/3/walking_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/walking_tone2.png b/public/-/emojis/3/walking_tone2.png
new file mode 100644
index 00000000000..6e4315de104
--- /dev/null
+++ b/public/-/emojis/3/walking_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/walking_tone3.png b/public/-/emojis/3/walking_tone3.png
new file mode 100644
index 00000000000..23db8941f74
--- /dev/null
+++ b/public/-/emojis/3/walking_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/walking_tone4.png b/public/-/emojis/3/walking_tone4.png
new file mode 100644
index 00000000000..09ed954c93b
--- /dev/null
+++ b/public/-/emojis/3/walking_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/walking_tone5.png b/public/-/emojis/3/walking_tone5.png
new file mode 100644
index 00000000000..c4d8d3b5ef4
--- /dev/null
+++ b/public/-/emojis/3/walking_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/waning_crescent_moon.png b/public/-/emojis/3/waning_crescent_moon.png
new file mode 100644
index 00000000000..7b8237edbb3
--- /dev/null
+++ b/public/-/emojis/3/waning_crescent_moon.png
Binary files differ
diff --git a/public/-/emojis/3/waning_gibbous_moon.png b/public/-/emojis/3/waning_gibbous_moon.png
new file mode 100644
index 00000000000..59b7f2cb950
--- /dev/null
+++ b/public/-/emojis/3/waning_gibbous_moon.png
Binary files differ
diff --git a/public/-/emojis/3/warning.png b/public/-/emojis/3/warning.png
new file mode 100644
index 00000000000..57784fe8def
--- /dev/null
+++ b/public/-/emojis/3/warning.png
Binary files differ
diff --git a/public/-/emojis/3/wastebasket.png b/public/-/emojis/3/wastebasket.png
new file mode 100644
index 00000000000..cd8057de563
--- /dev/null
+++ b/public/-/emojis/3/wastebasket.png
Binary files differ
diff --git a/public/-/emojis/3/watch.png b/public/-/emojis/3/watch.png
new file mode 100644
index 00000000000..e11a17bb054
--- /dev/null
+++ b/public/-/emojis/3/watch.png
Binary files differ
diff --git a/public/-/emojis/3/water_buffalo.png b/public/-/emojis/3/water_buffalo.png
new file mode 100644
index 00000000000..bd32406dae1
--- /dev/null
+++ b/public/-/emojis/3/water_buffalo.png
Binary files differ
diff --git a/public/-/emojis/3/water_polo.png b/public/-/emojis/3/water_polo.png
new file mode 100644
index 00000000000..90206e62917
--- /dev/null
+++ b/public/-/emojis/3/water_polo.png
Binary files differ
diff --git a/public/-/emojis/3/water_polo_tone1.png b/public/-/emojis/3/water_polo_tone1.png
new file mode 100644
index 00000000000..f463506ac69
--- /dev/null
+++ b/public/-/emojis/3/water_polo_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/water_polo_tone2.png b/public/-/emojis/3/water_polo_tone2.png
new file mode 100644
index 00000000000..a02eaaa6b3f
--- /dev/null
+++ b/public/-/emojis/3/water_polo_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/water_polo_tone3.png b/public/-/emojis/3/water_polo_tone3.png
new file mode 100644
index 00000000000..f4f80b9946c
--- /dev/null
+++ b/public/-/emojis/3/water_polo_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/water_polo_tone4.png b/public/-/emojis/3/water_polo_tone4.png
new file mode 100644
index 00000000000..260b27791c2
--- /dev/null
+++ b/public/-/emojis/3/water_polo_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/water_polo_tone5.png b/public/-/emojis/3/water_polo_tone5.png
new file mode 100644
index 00000000000..73cb325ba51
--- /dev/null
+++ b/public/-/emojis/3/water_polo_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/watermelon.png b/public/-/emojis/3/watermelon.png
new file mode 100644
index 00000000000..93448a8f2c4
--- /dev/null
+++ b/public/-/emojis/3/watermelon.png
Binary files differ
diff --git a/public/-/emojis/3/wave.png b/public/-/emojis/3/wave.png
new file mode 100644
index 00000000000..3161c2a6faf
--- /dev/null
+++ b/public/-/emojis/3/wave.png
Binary files differ
diff --git a/public/-/emojis/3/wave_tone1.png b/public/-/emojis/3/wave_tone1.png
new file mode 100644
index 00000000000..a45cc13862c
--- /dev/null
+++ b/public/-/emojis/3/wave_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/wave_tone2.png b/public/-/emojis/3/wave_tone2.png
new file mode 100644
index 00000000000..af3f1ae0e4a
--- /dev/null
+++ b/public/-/emojis/3/wave_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/wave_tone3.png b/public/-/emojis/3/wave_tone3.png
new file mode 100644
index 00000000000..7d1f9c475cc
--- /dev/null
+++ b/public/-/emojis/3/wave_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/wave_tone4.png b/public/-/emojis/3/wave_tone4.png
new file mode 100644
index 00000000000..c4e29a1ea8d
--- /dev/null
+++ b/public/-/emojis/3/wave_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/wave_tone5.png b/public/-/emojis/3/wave_tone5.png
new file mode 100644
index 00000000000..91925afcb28
--- /dev/null
+++ b/public/-/emojis/3/wave_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/wavy_dash.png b/public/-/emojis/3/wavy_dash.png
new file mode 100644
index 00000000000..54b7c2eff3f
--- /dev/null
+++ b/public/-/emojis/3/wavy_dash.png
Binary files differ
diff --git a/public/-/emojis/3/waxing_crescent_moon.png b/public/-/emojis/3/waxing_crescent_moon.png
new file mode 100644
index 00000000000..ae26131838d
--- /dev/null
+++ b/public/-/emojis/3/waxing_crescent_moon.png
Binary files differ
diff --git a/public/-/emojis/3/waxing_gibbous_moon.png b/public/-/emojis/3/waxing_gibbous_moon.png
new file mode 100644
index 00000000000..3c50dd6b778
--- /dev/null
+++ b/public/-/emojis/3/waxing_gibbous_moon.png
Binary files differ
diff --git a/public/-/emojis/3/wc.png b/public/-/emojis/3/wc.png
new file mode 100644
index 00000000000..be3d80ab1ac
--- /dev/null
+++ b/public/-/emojis/3/wc.png
Binary files differ
diff --git a/public/-/emojis/3/weary.png b/public/-/emojis/3/weary.png
new file mode 100644
index 00000000000..1492622b838
--- /dev/null
+++ b/public/-/emojis/3/weary.png
Binary files differ
diff --git a/public/-/emojis/3/wedding.png b/public/-/emojis/3/wedding.png
new file mode 100644
index 00000000000..ac86a232e67
--- /dev/null
+++ b/public/-/emojis/3/wedding.png
Binary files differ
diff --git a/public/-/emojis/3/whale.png b/public/-/emojis/3/whale.png
new file mode 100644
index 00000000000..d6326af1a15
--- /dev/null
+++ b/public/-/emojis/3/whale.png
Binary files differ
diff --git a/public/-/emojis/3/whale2.png b/public/-/emojis/3/whale2.png
new file mode 100644
index 00000000000..7b586e17b52
--- /dev/null
+++ b/public/-/emojis/3/whale2.png
Binary files differ
diff --git a/public/-/emojis/3/wheel_of_dharma.png b/public/-/emojis/3/wheel_of_dharma.png
new file mode 100644
index 00000000000..3fff7c63b61
--- /dev/null
+++ b/public/-/emojis/3/wheel_of_dharma.png
Binary files differ
diff --git a/public/-/emojis/3/wheelchair.png b/public/-/emojis/3/wheelchair.png
new file mode 100644
index 00000000000..260c05e88bd
--- /dev/null
+++ b/public/-/emojis/3/wheelchair.png
Binary files differ
diff --git a/public/-/emojis/3/white_check_mark.png b/public/-/emojis/3/white_check_mark.png
new file mode 100644
index 00000000000..82035d48bc0
--- /dev/null
+++ b/public/-/emojis/3/white_check_mark.png
Binary files differ
diff --git a/public/-/emojis/3/white_circle.png b/public/-/emojis/3/white_circle.png
new file mode 100644
index 00000000000..e5aa5bb4e70
--- /dev/null
+++ b/public/-/emojis/3/white_circle.png
Binary files differ
diff --git a/public/-/emojis/3/white_flower.png b/public/-/emojis/3/white_flower.png
new file mode 100644
index 00000000000..e165fe1910a
--- /dev/null
+++ b/public/-/emojis/3/white_flower.png
Binary files differ
diff --git a/public/-/emojis/3/white_large_square.png b/public/-/emojis/3/white_large_square.png
new file mode 100644
index 00000000000..2e216c74f19
--- /dev/null
+++ b/public/-/emojis/3/white_large_square.png
Binary files differ
diff --git a/public/-/emojis/3/white_medium_small_square.png b/public/-/emojis/3/white_medium_small_square.png
new file mode 100644
index 00000000000..eb79781b466
--- /dev/null
+++ b/public/-/emojis/3/white_medium_small_square.png
Binary files differ
diff --git a/public/-/emojis/3/white_medium_square.png b/public/-/emojis/3/white_medium_square.png
new file mode 100644
index 00000000000..6b7c6585b32
--- /dev/null
+++ b/public/-/emojis/3/white_medium_square.png
Binary files differ
diff --git a/public/-/emojis/3/white_small_square.png b/public/-/emojis/3/white_small_square.png
new file mode 100644
index 00000000000..09f2f1d1bbf
--- /dev/null
+++ b/public/-/emojis/3/white_small_square.png
Binary files differ
diff --git a/public/-/emojis/3/white_square_button.png b/public/-/emojis/3/white_square_button.png
new file mode 100644
index 00000000000..1eb1af7a77b
--- /dev/null
+++ b/public/-/emojis/3/white_square_button.png
Binary files differ
diff --git a/public/-/emojis/3/white_sun_cloud.png b/public/-/emojis/3/white_sun_cloud.png
new file mode 100644
index 00000000000..a6982814fa4
--- /dev/null
+++ b/public/-/emojis/3/white_sun_cloud.png
Binary files differ
diff --git a/public/-/emojis/3/white_sun_rain_cloud.png b/public/-/emojis/3/white_sun_rain_cloud.png
new file mode 100644
index 00000000000..a4b13e900a4
--- /dev/null
+++ b/public/-/emojis/3/white_sun_rain_cloud.png
Binary files differ
diff --git a/public/-/emojis/3/white_sun_small_cloud.png b/public/-/emojis/3/white_sun_small_cloud.png
new file mode 100644
index 00000000000..6c00833eb19
--- /dev/null
+++ b/public/-/emojis/3/white_sun_small_cloud.png
Binary files differ
diff --git a/public/-/emojis/3/wilted_rose.png b/public/-/emojis/3/wilted_rose.png
new file mode 100644
index 00000000000..03919a12094
--- /dev/null
+++ b/public/-/emojis/3/wilted_rose.png
Binary files differ
diff --git a/public/-/emojis/3/wind_blowing_face.png b/public/-/emojis/3/wind_blowing_face.png
new file mode 100644
index 00000000000..32e658bcbec
--- /dev/null
+++ b/public/-/emojis/3/wind_blowing_face.png
Binary files differ
diff --git a/public/-/emojis/3/wind_chime.png b/public/-/emojis/3/wind_chime.png
new file mode 100644
index 00000000000..5f5add3cba5
--- /dev/null
+++ b/public/-/emojis/3/wind_chime.png
Binary files differ
diff --git a/public/-/emojis/3/wine_glass.png b/public/-/emojis/3/wine_glass.png
new file mode 100644
index 00000000000..63c94e0cf6c
--- /dev/null
+++ b/public/-/emojis/3/wine_glass.png
Binary files differ
diff --git a/public/-/emojis/3/wink.png b/public/-/emojis/3/wink.png
new file mode 100644
index 00000000000..c605d9c2c78
--- /dev/null
+++ b/public/-/emojis/3/wink.png
Binary files differ
diff --git a/public/-/emojis/3/wolf.png b/public/-/emojis/3/wolf.png
new file mode 100644
index 00000000000..b5b06c27786
--- /dev/null
+++ b/public/-/emojis/3/wolf.png
Binary files differ
diff --git a/public/-/emojis/3/woman.png b/public/-/emojis/3/woman.png
new file mode 100644
index 00000000000..75533da530f
--- /dev/null
+++ b/public/-/emojis/3/woman.png
Binary files differ
diff --git a/public/-/emojis/3/woman_tone1.png b/public/-/emojis/3/woman_tone1.png
new file mode 100644
index 00000000000..a41f4f2f555
--- /dev/null
+++ b/public/-/emojis/3/woman_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/woman_tone2.png b/public/-/emojis/3/woman_tone2.png
new file mode 100644
index 00000000000..6ec6de75016
--- /dev/null
+++ b/public/-/emojis/3/woman_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/woman_tone3.png b/public/-/emojis/3/woman_tone3.png
new file mode 100644
index 00000000000..9c08adebd65
--- /dev/null
+++ b/public/-/emojis/3/woman_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/woman_tone4.png b/public/-/emojis/3/woman_tone4.png
new file mode 100644
index 00000000000..88cbdbc798a
--- /dev/null
+++ b/public/-/emojis/3/woman_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/woman_tone5.png b/public/-/emojis/3/woman_tone5.png
new file mode 100644
index 00000000000..b02ec4b4af1
--- /dev/null
+++ b/public/-/emojis/3/woman_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/womans_clothes.png b/public/-/emojis/3/womans_clothes.png
new file mode 100644
index 00000000000..86f89f19f37
--- /dev/null
+++ b/public/-/emojis/3/womans_clothes.png
Binary files differ
diff --git a/public/-/emojis/3/womans_hat.png b/public/-/emojis/3/womans_hat.png
new file mode 100644
index 00000000000..b69fcad7e49
--- /dev/null
+++ b/public/-/emojis/3/womans_hat.png
Binary files differ
diff --git a/public/-/emojis/3/womens.png b/public/-/emojis/3/womens.png
new file mode 100644
index 00000000000..ea38f1a854d
--- /dev/null
+++ b/public/-/emojis/3/womens.png
Binary files differ
diff --git a/public/-/emojis/3/worried.png b/public/-/emojis/3/worried.png
new file mode 100644
index 00000000000..b85910fb297
--- /dev/null
+++ b/public/-/emojis/3/worried.png
Binary files differ
diff --git a/public/-/emojis/3/wrench.png b/public/-/emojis/3/wrench.png
new file mode 100644
index 00000000000..feff25c2113
--- /dev/null
+++ b/public/-/emojis/3/wrench.png
Binary files differ
diff --git a/public/-/emojis/3/wrestlers.png b/public/-/emojis/3/wrestlers.png
new file mode 100644
index 00000000000..58175c6ecbe
--- /dev/null
+++ b/public/-/emojis/3/wrestlers.png
Binary files differ
diff --git a/public/-/emojis/3/wrestlers_tone1.png b/public/-/emojis/3/wrestlers_tone1.png
new file mode 100644
index 00000000000..55c8bb3fcc2
--- /dev/null
+++ b/public/-/emojis/3/wrestlers_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/wrestlers_tone2.png b/public/-/emojis/3/wrestlers_tone2.png
new file mode 100644
index 00000000000..2ae6989956d
--- /dev/null
+++ b/public/-/emojis/3/wrestlers_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/wrestlers_tone3.png b/public/-/emojis/3/wrestlers_tone3.png
new file mode 100644
index 00000000000..bff6020b931
--- /dev/null
+++ b/public/-/emojis/3/wrestlers_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/wrestlers_tone4.png b/public/-/emojis/3/wrestlers_tone4.png
new file mode 100644
index 00000000000..d09cf19262b
--- /dev/null
+++ b/public/-/emojis/3/wrestlers_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/wrestlers_tone5.png b/public/-/emojis/3/wrestlers_tone5.png
new file mode 100644
index 00000000000..0850e7d1e42
--- /dev/null
+++ b/public/-/emojis/3/wrestlers_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/writing_hand.png b/public/-/emojis/3/writing_hand.png
new file mode 100644
index 00000000000..9a2691a0dc4
--- /dev/null
+++ b/public/-/emojis/3/writing_hand.png
Binary files differ
diff --git a/public/-/emojis/3/writing_hand_tone1.png b/public/-/emojis/3/writing_hand_tone1.png
new file mode 100644
index 00000000000..bf821143858
--- /dev/null
+++ b/public/-/emojis/3/writing_hand_tone1.png
Binary files differ
diff --git a/public/-/emojis/3/writing_hand_tone2.png b/public/-/emojis/3/writing_hand_tone2.png
new file mode 100644
index 00000000000..c7fa4523983
--- /dev/null
+++ b/public/-/emojis/3/writing_hand_tone2.png
Binary files differ
diff --git a/public/-/emojis/3/writing_hand_tone3.png b/public/-/emojis/3/writing_hand_tone3.png
new file mode 100644
index 00000000000..c41e68bd2bc
--- /dev/null
+++ b/public/-/emojis/3/writing_hand_tone3.png
Binary files differ
diff --git a/public/-/emojis/3/writing_hand_tone4.png b/public/-/emojis/3/writing_hand_tone4.png
new file mode 100644
index 00000000000..448d3507e9c
--- /dev/null
+++ b/public/-/emojis/3/writing_hand_tone4.png
Binary files differ
diff --git a/public/-/emojis/3/writing_hand_tone5.png b/public/-/emojis/3/writing_hand_tone5.png
new file mode 100644
index 00000000000..7959873d60d
--- /dev/null
+++ b/public/-/emojis/3/writing_hand_tone5.png
Binary files differ
diff --git a/public/-/emojis/3/x.png b/public/-/emojis/3/x.png
new file mode 100644
index 00000000000..9406c87c5f0
--- /dev/null
+++ b/public/-/emojis/3/x.png
Binary files differ
diff --git a/public/-/emojis/3/yellow_heart.png b/public/-/emojis/3/yellow_heart.png
new file mode 100644
index 00000000000..8067e9413e8
--- /dev/null
+++ b/public/-/emojis/3/yellow_heart.png
Binary files differ
diff --git a/public/-/emojis/3/yen.png b/public/-/emojis/3/yen.png
new file mode 100644
index 00000000000..72258005f08
--- /dev/null
+++ b/public/-/emojis/3/yen.png
Binary files differ
diff --git a/public/-/emojis/3/yin_yang.png b/public/-/emojis/3/yin_yang.png
new file mode 100644
index 00000000000..35ae33c4326
--- /dev/null
+++ b/public/-/emojis/3/yin_yang.png
Binary files differ
diff --git a/public/-/emojis/3/yum.png b/public/-/emojis/3/yum.png
new file mode 100644
index 00000000000..ead526319f4
--- /dev/null
+++ b/public/-/emojis/3/yum.png
Binary files differ
diff --git a/public/-/emojis/3/zap.png b/public/-/emojis/3/zap.png
new file mode 100644
index 00000000000..7271a50bcba
--- /dev/null
+++ b/public/-/emojis/3/zap.png
Binary files differ
diff --git a/public/-/emojis/3/zero.png b/public/-/emojis/3/zero.png
new file mode 100644
index 00000000000..db3f638a02c
--- /dev/null
+++ b/public/-/emojis/3/zero.png
Binary files differ
diff --git a/public/-/emojis/3/zipper_mouth.png b/public/-/emojis/3/zipper_mouth.png
new file mode 100644
index 00000000000..58915b98f57
--- /dev/null
+++ b/public/-/emojis/3/zipper_mouth.png
Binary files differ
diff --git a/public/-/emojis/3/zzz.png b/public/-/emojis/3/zzz.png
new file mode 100644
index 00000000000..4b146f52bde
--- /dev/null
+++ b/public/-/emojis/3/zzz.png
Binary files differ
diff --git a/qa/Gemfile b/qa/Gemfile
index a52385d314b..3b0e8fa888c 100644
--- a/qa/Gemfile
+++ b/qa/Gemfile
@@ -2,14 +2,14 @@
source 'https://rubygems.org'
-gem 'gitlab-qa', '~> 12', '>= 12.4.1', require: 'gitlab/qa'
+gem 'gitlab-qa', '~> 12', '>= 12.5.0', require: 'gitlab/qa'
gem 'gitlab_quality-test_tooling', '~> 0.9.3', require: false
gem 'gitlab-utils', path: '../gems/gitlab-utils'
gem 'activesupport', '~> 7.0.8' # This should stay in sync with the root's Gemfile
gem 'allure-rspec', '~> 2.20.0'
gem 'capybara', '~> 3.39.2'
gem 'capybara-screenshot', '~> 1.0.26'
-gem 'rake', '~> 13', '>= 13.0.6'
+gem 'rake', '~> 13', '>= 13.1.0'
gem 'rspec', '~> 3.12'
gem 'selenium-webdriver', '= 4.14.0'
gem 'airborne', '~> 0.3.7', require: false # airborne is messing with rspec sandboxed mode so not requiring by default
diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock
index 30fd3691521..a1563a7351e 100644
--- a/qa/Gemfile.lock
+++ b/qa/Gemfile.lock
@@ -121,7 +121,7 @@ GEM
gitlab (4.19.0)
httparty (~> 0.20)
terminal-table (>= 1.5.1)
- gitlab-qa (12.4.1)
+ gitlab-qa (12.5.0)
activesupport (>= 6.1, < 7.1)
gitlab (~> 4.19)
http (~> 5.0)
@@ -246,7 +246,7 @@ GEM
rails-html-sanitizer (1.5.0)
loofah (~> 2.19, >= 2.19.1)
rainbow (3.1.1)
- rake (13.0.6)
+ rake (13.1.0)
regexp_parser (2.1.1)
representable (3.2.0)
declarative (< 0.1.0)
@@ -351,7 +351,7 @@ DEPENDENCIES
faraday-retry (~> 2.2)
fog-core (= 2.1.0)
fog-google (~> 1.19)
- gitlab-qa (~> 12, >= 12.4.1)
+ gitlab-qa (~> 12, >= 12.5.0)
gitlab-utils!
gitlab_quality-test_tooling (~> 0.9.3)
influxdb-client (~> 2.9)
@@ -362,7 +362,7 @@ DEPENDENCIES
parallel_tests (~> 4.2, >= 4.2.1)
pry-byebug (~> 3.10.1)
rainbow (~> 3.1.1)
- rake (~> 13, >= 13.0.6)
+ rake (~> 13, >= 13.1.0)
rest-client (~> 2.1.0)
rotp (~> 6.3.0)
rspec (~> 3.12)
@@ -377,4 +377,4 @@ DEPENDENCIES
zeitwerk (~> 2.6, >= 2.6.8)
BUNDLED WITH
- 2.4.20
+ 2.4.21
diff --git a/qa/Rakefile b/qa/Rakefile
index 6f94c63b4de..a8336e087c7 100644
--- a/qa/Rakefile
+++ b/qa/Rakefile
@@ -83,10 +83,27 @@ end
desc "Deletes user's projects"
task :delete_user_projects, [:delete_before, :dry_run] do |_, args|
- QA::Tools::DeleteUserProjects.new(args).run
+ args.with_defaults(delete_before: (Date.today - 1).to_s, dry_run: false)
+ QA::Tools::DeleteUserProjects.new(
+ delete_before: args[:delete_before],
+ dry_run: !!(args[:dry_run] =~ /true|1|y/i)).run
end
desc "Revokes user's personal access tokens"
task :revoke_user_pats, [:revoke_before, :dry_run] do |_, args|
QA::Tools::RevokeUserPersonalAccessTokens.new(args).run
end
+
+desc "Generate group with multiple projects for direct transfer test"
+task :generate_direct_transfer_test_group, [:project_tar_paths, :group_path, :project_copies] do |_, args|
+ QA::Support::GitlabAddress.define_gitlab_address_attribute!
+ QA::Runtime::Browser.configure!
+ QA::Runtime::Scenario.from_env(QA::Runtime::Env.runtime_scenario_attributes)
+
+ numeric_args = { project_copies: Integer(args[:project_copies], exception: false) }.compact
+ string_args = args.to_h
+ .slice(:project_tar_paths, :group_path)
+ .compact_blank
+
+ QA::Tools::GenerateImportTestGroup.new(**string_args, **numeric_args).generate
+end
diff --git a/qa/gdk/Dockerfile.gdk b/qa/gdk/Dockerfile.gdk
index 9326beba93f..bc3c1c4317d 100644
--- a/qa/gdk/Dockerfile.gdk
+++ b/qa/gdk/Dockerfile.gdk
@@ -1,11 +1,11 @@
-FROM registry.gitlab.com/gitlab-org/gitlab-development-kit/asdf-bootstrapped-verify:main@sha256:fb833ce8f838e38104ee3b499a99f86e7a5844a4d4d8fdeeababd2d717504f3e as base
+FROM registry.gitlab.com/gitlab-org/gitlab-development-kit/asdf-bootstrapped-verify/main:d843a4d237bbb9c2f04d2cbddc89fd6dadeb86cf as base
ENV GITLAB_LICENSE_MODE=test \
GDK_KILL_CONFIRM=true
# Clone GDK at specific sha and bootstrap packages
#
-ARG GDK_SHA=65cf4576208b9f79c54c0042c44024c0008deafc
+ARG GDK_SHA=e2e32c98ef2874a3bd13af00e0085f8299ff3288
RUN set -eux; \
git clone --depth 1 https://gitlab.com/gitlab-org/gitlab-development-kit.git && cd gitlab-development-kit; \
git fetch --depth 1 origin ${GDK_SHA} && git -c advice.detachedHead=false checkout ${GDK_SHA}; \
diff --git a/qa/lib/gitlab/page/admin/subscription.rb b/qa/lib/gitlab/page/admin/subscription.rb
index e19f0580007..6decdeeb3f7 100644
--- a/qa/lib/gitlab/page/admin/subscription.rb
+++ b/qa/lib/gitlab/page/admin/subscription.rb
@@ -39,7 +39,7 @@ module Gitlab
# @option plan [Hash] Support::Helpers::PREMIUM_SELF_MANAGED
# @option plan [Hash] Support::Helpers::ULTIMATE
# @option plan [Hash] Support::Helpers::ULTIMATE_SELF_MANAGED
- # @option plan [Hash] Support::Helpers::CI_MINUTES
+ # @option plan [Hash] Support::Helpers::COMPUTE_MINUTES
# @option plan [Hash] Support::Helpers::STORAGE
# @param users_in_license [Integer] Number of users in license
# @param license_type [Hash] Type of the license
diff --git a/qa/lib/gitlab/page/group/settings/usage_quotas.rb b/qa/lib/gitlab/page/group/settings/usage_quotas.rb
index 8ae9e8fd25a..eab40ab8941 100644
--- a/qa/lib/gitlab/page/group/settings/usage_quotas.rb
+++ b/qa/lib/gitlab/page/group/settings/usage_quotas.rb
@@ -13,9 +13,9 @@ module Gitlab
# Pipelines section
link :pipelines_tab
- link :buy_ci_minutes
- div :plan_ci_minutes
- div :additional_ci_minutes
+ link :buy_compute_minutes
+ div :plan_compute_minutes
+ div :additional_compute_minutes
div :ci_purchase_successful_alert, text: /You have successfully purchased CI minutes/
# Storage section
@@ -39,17 +39,17 @@ module Gitlab
button :confirm_member_approval, text: /^OK$/
def plan_ci_limits
- plan_ci_minutes[/(\d+){2}/]
+ plan_compute_minutes[/(\d+){2}/]
end
def additional_ci_limits
- additional_ci_minutes[/(\d+){2}/]
+ additional_compute_minutes[/(\d+){2}/]
end
- def additional_ci_minutes_added?
+ def additional_compute_minutes_added?
# When opening the Usage quotas page, Seats quota tab is opened briefly even when url is to a different tab
::QA::Support::WaitForRequests.wait_for_requests
- additional_ci_minutes?
+ additional_compute_minutes?
end
# Returns total purchased storage value once it's ready on page
@@ -61,29 +61,29 @@ module Gitlab
storage_purchased[/(\d+){2}.\d+/].to_f
end
- # Waits for additional CI minutes to be available on the page
- def wait_for_additional_ci_minutes_available
+ # Waits for additional compute minutes to be available on the page
+ def wait_for_additional_compute_minutes_available
::QA::Support::Waiter.wait_until(
max_duration: ::QA::Support::Helpers::Zuora::ZUORA_TIMEOUT,
sleep_interval: 2,
reload_page: Chemlab.configuration.browser.session,
- message: 'Expected additional CI minutes but they did not appear.'
+ message: 'Expected additional compute minutes but they did not appear.'
) do
- additional_ci_minutes_added?
+ additional_compute_minutes_added?
end
end
- # Waits for additional CI minutes amount to match the expected number of minutes
+ # Waits for additional compute minutes amount to match the expected number of minutes
#
# @param [String] minutes
- def wait_for_additional_ci_minute_limits(minutes)
- wait_for_additional_ci_minutes_available
+ def wait_for_additional_compute_minute_limits(minutes)
+ wait_for_additional_compute_minutes_available
::QA::Support::Waiter.wait_until(
max_duration: ::QA::Support::Helpers::Zuora::ZUORA_TIMEOUT,
sleep_interval: 2,
reload_page: Chemlab.configuration.browser.session,
- message: "Expected additional CI minutes to equal #{minutes}"
+ message: "Expected additional compute minutes to equal #{minutes}"
) do
additional_ci_limits == minutes
end
diff --git a/qa/lib/gitlab/page/group/settings/usage_quotas.stub.rb b/qa/lib/gitlab/page/group/settings/usage_quotas.stub.rb
index 748c9a82d59..0c473123340 100644
--- a/qa/lib/gitlab/page/group/settings/usage_quotas.stub.rb
+++ b/qa/lib/gitlab/page/group/settings/usage_quotas.stub.rb
@@ -125,75 +125,75 @@ module Gitlab
# This is a stub, used for indexing. The method is dynamically generated.
end
- # @note Defined as +link :buy_ci_minutes+
- # Clicks +buy_ci_minutes+
- def buy_ci_minutes
+ # @note Defined as +link :buy_compute_minutes+
+ # Clicks +buy_compute_minutes+
+ def buy_compute_minutes
# This is a stub, used for indexing. The method is dynamically generated.
end
# @example
# Gitlab::Page::Group::Settings::UsageQuotas.perform do |usage_quotas|
- # expect(usage_quotas.buy_ci_minutes_element).to exist
+ # expect(usage_quotas.buy_compute_minutes_element).to exist
# end
# @return [Watir::Link] The raw +Link+ element
- def buy_ci_minutes_element
+ def buy_compute_minutes_element
# This is a stub, used for indexing. The method is dynamically generated.
end
# @example
# Gitlab::Page::Group::Settings::UsageQuotas.perform do |usage_quotas|
- # expect(usage_quotas).to be_buy_ci_minutes
+ # expect(usage_quotas).to be_buy_compute_minutes
# end
- # @return [Boolean] true if the +buy_ci_minutes+ element is present on the page
- def buy_ci_minutes?
+ # @return [Boolean] true if the +buy_compute_minutes+ element is present on the page
+ def buy_compute_minutes?
# This is a stub, used for indexing. The method is dynamically generated.
end
- # @note Defined as +div :plan_ci_minutes+
- # @return [String] The text content or value of +plan_ci_minutes+
- def plan_ci_minutes
+ # @note Defined as +div :plan_compute_minutes+
+ # @return [String] The text content or value of +plan_compute_minutes+
+ def plan_compute_minutes
# This is a stub, used for indexing. The method is dynamically generated.
end
# @example
# Gitlab::Page::Group::Settings::UsageQuotas.perform do |usage_quotas|
- # expect(usage_quotas.plan_ci_minutes_element).to exist
+ # expect(usage_quotas.plan_compute_minutes_element).to exist
# end
# @return [Watir::Div] The raw +Div+ element
- def plan_ci_minutes_element
+ def plan_compute_minutes_element
# This is a stub, used for indexing. The method is dynamically generated.
end
# @example
# Gitlab::Page::Group::Settings::UsageQuotas.perform do |usage_quotas|
- # expect(usage_quotas).to be_plan_ci_minutes
+ # expect(usage_quotas).to be_plan_compute_minutes
# end
- # @return [Boolean] true if the +plan_ci_minutes+ element is present on the page
- def plan_ci_minutes?
+ # @return [Boolean] true if the +plan_compute_minutes+ element is present on the page
+ def plan_compute_minutes?
# This is a stub, used for indexing. The method is dynamically generated.
end
- # @note Defined as +div :additional_ci_minutes+
- # @return [String] The text content or value of +additional_ci_minutes+
- def additional_ci_minutes
+ # @note Defined as +div :additional_compute_minutes+
+ # @return [String] The text content or value of +additional_compute_minutes+
+ def additional_compute_minutes
# This is a stub, used for indexing. The method is dynamically generated.
end
# @example
# Gitlab::Page::Group::Settings::UsageQuotas.perform do |usage_quotas|
- # expect(usage_quotas.additional_ci_minutes_element).to exist
+ # expect(usage_quotas.additional_compute_minutes_element).to exist
# end
# @return [Watir::Div] The raw +Div+ element
- def additional_ci_minutes_element
+ def additional_compute_minutes_element
# This is a stub, used for indexing. The method is dynamically generated.
end
# @example
# Gitlab::Page::Group::Settings::UsageQuotas.perform do |usage_quotas|
- # expect(usage_quotas).to be_additional_ci_minutes
+ # expect(usage_quotas).to be_additional_compute_minutes
# end
- # @return [Boolean] true if the +additional_ci_minutes+ element is present on the page
- def additional_ci_minutes?
+ # @return [Boolean] true if the +additional_compute_minutes+ element is present on the page
+ def additional_compute_minutes?
# This is a stub, used for indexing. The method is dynamically generated.
end
diff --git a/qa/lib/gitlab/page/subscriptions/new.rb b/qa/lib/gitlab/page/subscriptions/new.rb
index 95e5028f985..739efeed898 100644
--- a/qa/lib/gitlab/page/subscriptions/new.rb
+++ b/qa/lib/gitlab/page/subscriptions/new.rb
@@ -40,6 +40,21 @@ module Gitlab
# Order Summary
div :selected_plan
div :total_amount
+
+ # Alerts
+ div :lock_competition_error, text: /Operation failed due to a lock competition, please retry later./
+
+ def purchase
+ ::QA::Support::Retrier.retry_until(
+ max_duration: 60,
+ sleep_interval: 10,
+ message: 'Expected no Zuora lock competition error'
+ ) do
+ confirm_purchase
+ ::QA::Support::WaitForRequests.wait_for_requests
+ !lock_competition_error?
+ end
+ end
end
end
end
diff --git a/qa/lib/gitlab/page/subscriptions/new.stub.rb b/qa/lib/gitlab/page/subscriptions/new.stub.rb
index a7f5d689838..e660d198478 100644
--- a/qa/lib/gitlab/page/subscriptions/new.stub.rb
+++ b/qa/lib/gitlab/page/subscriptions/new.stub.rb
@@ -621,6 +621,30 @@ module Gitlab
def total_amount?
# This is a stub, used for indexing. The method is dynamically generated.
end
+
+ # @note Defined as +div :lock_competition_error+
+ # @return [String] The text content or value of +lock_competition_error+
+ def lock_competition_error
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Subscriptions::New.perform do |new|
+ # expect(new.lock_competition_error_element).to exist
+ # end
+ # @return [Watir::Div] The raw +Div+ element
+ def lock_competition_error_element
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Subscriptions::New.perform do |new|
+ # expect(new).to be_lock_competition_error
+ # end
+ # @return [Boolean] true if the +lock_competition_error+ element is present on the page
+ def lock_competition_error?
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
end
end
end
diff --git a/qa/qa/factories/deploy_tokens.rb b/qa/qa/factories/deploy_tokens.rb
new file mode 100644
index 00000000000..fb804172177
--- /dev/null
+++ b/qa/qa/factories/deploy_tokens.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module QA
+ FactoryBot.define do
+ # https://docs.gitlab.com/ee/api/deploy_tokens.html#create-a-project-deploy-token
+ factory :project_deploy_token, class: 'QA::Resource::ProjectDeployToken'
+
+ # https://docs.gitlab.com/ee/api/deploy_tokens.html#create-a-group-deploy-token
+ factory :group_deploy_token, class: 'QA::Resource::GroupDeployToken'
+ end
+end
diff --git a/qa/qa/factories/designs.rb b/qa/qa/factories/designs.rb
new file mode 100644
index 00000000000..1475d8c3e0d
--- /dev/null
+++ b/qa/qa/factories/designs.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module QA
+ FactoryBot.define do
+ factory :design, class: 'QA::Resource::Design'
+ end
+end
diff --git a/qa/qa/factories/issues.rb b/qa/qa/factories/issues.rb
index 2931a41e347..c7f6ac44861 100644
--- a/qa/qa/factories/issues.rb
+++ b/qa/qa/factories/issues.rb
@@ -12,6 +12,12 @@ module QA
trait :confidential do
confidential { true }
end
+
+ trait :incident do
+ issue_type { 'incident' }
+ end
+
+ factory :incident, traits: [:incident]
end
end
end
diff --git a/qa/qa/factories/packages.rb b/qa/qa/factories/packages.rb
new file mode 100644
index 00000000000..5e4b11e8f8e
--- /dev/null
+++ b/qa/qa/factories/packages.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+module QA
+ FactoryBot.define do
+ # https://docs.gitlab.com/ee/api/packages.html
+ factory :package, class: 'QA::Resource::Package'
+ end
+end
diff --git a/qa/qa/fixtures/mocks/import/github.yml b/qa/qa/fixtures/mocks/import/github.yml
index 16d038ed091..0abd255bf0d 100644
--- a/qa/qa/fixtures/mocks/import/github.yml
+++ b/qa/qa/fixtures/mocks/import/github.yml
@@ -9,6 +9,101 @@
# follow_redirect: true
# keep_host: true
+# - request:
+# method: "POST"
+# headers:
+# Host: "api.github.com"
+# proxy:
+# host: "https://api.github.com"
+# follow_redirect: true
+# keep_host: true
+
+# Mocked response definition
+#
+- request:
+ path: /graphql
+ method: POST
+ headers:
+ Host: api.github.com
+ response:
+ status: 200
+ headers:
+ Content-Type: application/json; charset=utf-8
+ X-Ratelimit-Limit: '5000'
+ X-Ratelimit-Remaining: '5000'
+ body: |
+ {
+ "data": {
+ "search": {
+ "nodes": [
+ {
+ "__typename": "Repository",
+ "full_name": "gitlab-qa-github/import-test",
+ "id": 466994992,
+ "name": "import-test",
+ "owner": {
+ "login": "gitlab-qa-github"
+ }
+ }
+ ],
+ "pageInfo": {
+ "endCursor": "Y3Vyc29yOjQ=",
+ "hasNextPage": false,
+ "hasPreviousPage": false,
+ "startCursor": "Y3Vyc29yOjE="
+ },
+ "repositoryCount": 1
+ }
+ }
+ }
+
+- request:
+ path: /user
+ method: GET
+ headers:
+ Host: api.github.com
+ response:
+ status: 200
+ headers:
+ Content-Type: application/json; charset=utf-8
+ X-Ratelimit-Limit: '5000'
+ X-Ratelimit-Remaining: '5000'
+ body: |
+ {
+ "avatar_url": "https://avatars.githubusercontent.com/u/59606922?v=4",
+ "bio": null,
+ "blog": "",
+ "company": null,
+ "created_at": "2020-01-07T11:47:11Z",
+ "email": null,
+ "events_url": "https://api.github.com/users/gitlab-qa-github/events{/privacy}",
+ "followers": 0,
+ "followers_url": "https://api.github.com/users/gitlab-qa-github/followers",
+ "following": 0,
+ "following_url": "https://api.github.com/users/gitlab-qa-github/following{/other_user}",
+ "gists_url": "https://api.github.com/users/gitlab-qa-github/gists{/gist_id}",
+ "gravatar_id": "",
+ "hireable": null,
+ "html_url": "https://github.com/gitlab-qa-github",
+ "id": 59606922,
+ "location": null,
+ "login": "gitlab-qa-github",
+ "name": null,
+ "node_id": "MDQ6VXNlcjU5NjA2OTIy",
+ "organizations_url": "https://api.github.com/users/gitlab-qa-github/orgs",
+ "public_gists": 0,
+ "public_repos": 4,
+ "received_events_url": "https://api.github.com/users/gitlab-qa-github/received_events",
+ "repos_url": "https://api.github.com/users/gitlab-qa-github/repos",
+ "site_admin": false,
+ "starred_url": "https://api.github.com/users/gitlab-qa-github/starred{/owner}{/repo}",
+ "subscriptions_url": "https://api.github.com/users/gitlab-qa-github/subscriptions",
+ "twitter_username": null,
+ "type": "User",
+ "updated_at": "2023-05-11T09:17:43Z",
+ "url": "https://api.github.com/users/gitlab-qa-github"
+ }
+
- request:
path: /rate_limit
method: GET
@@ -217,9 +312,6 @@
- request:
path: /user/orgs
method: GET
- query_params:
- page: '1'
- per_page: '25'
headers:
Host: api.github.com
response:
diff --git a/qa/qa/flow/login.rb b/qa/qa/flow/login.rb
index 2702b52f2ef..807419983bc 100644
--- a/qa/qa/flow/login.rb
+++ b/qa/qa/flow/login.rb
@@ -19,13 +19,9 @@ module QA
end
def sign_in(as: nil, address: :gitlab, skip_page_validation: false, admin: false)
- Page::Main::Login.perform { |p| p.redirect_to_login_page(address) }
-
- if !Page::Main::Login.perform(&:on_login_page?) && Page::Main::Menu.perform(&:signed_in?)
- Page::Main::Menu.perform(&:sign_out)
- end
-
Page::Main::Login.perform do |login|
+ login.redirect_to_login_page(address)
+
if admin
login.sign_in_using_admin_credentials
else
diff --git a/qa/qa/flow/purchase.rb b/qa/qa/flow/purchase.rb
index 2eee15b874c..a412539d3bd 100644
--- a/qa/qa/flow/purchase.rb
+++ b/qa/qa/flow/purchase.rb
@@ -19,25 +19,25 @@ module QA
fill_in_customer_info
fill_in_payment_info
- new_subscription.confirm_purchase
+ new_subscription.purchase
end
end
- def purchase_ci_minutes(quantity: 1)
+ def purchase_compute_minutes(quantity: 1)
Page::Group::Menu.perform(&:go_to_usage_quotas)
Gitlab::Page::Group::Settings::UsageQuotas.perform do |usage_quota|
usage_quota.pipelines_tab
- usage_quota.buy_ci_minutes
+ usage_quota.buy_compute_minutes
end
- Gitlab::Page::Subscriptions::New.perform do |ci_minutes|
- ci_minutes.quantity = quantity
- ci_minutes.continue_to_billing
+ Gitlab::Page::Subscriptions::New.perform do |compute_minutes|
+ compute_minutes.quantity = quantity
+ compute_minutes.continue_to_billing
fill_in_customer_info
fill_in_payment_info
- ci_minutes.confirm_purchase
+ compute_minutes.purchase
end
end
@@ -59,7 +59,7 @@ module QA
fill_in_customer_info
fill_in_payment_info
- storage.confirm_purchase
+ storage.purchase
end
end
diff --git a/qa/qa/flow/user_onboarding.rb b/qa/qa/flow/user_onboarding.rb
index ee595cae338..3753dc7c8d7 100644
--- a/qa/qa/flow/user_onboarding.rb
+++ b/qa/qa/flow/user_onboarding.rb
@@ -5,7 +5,11 @@ module QA
module UserOnboarding
extend self
- def onboard_user(wait: Capybara.default_max_wait_time)
+ def onboard_user
+ # Implemented in EE only
+ end
+
+ def create_initial_project
# Implemented in EE only
end
end
diff --git a/qa/qa/page/admin/menu.rb b/qa/qa/page/admin/menu.rb
index 563650bbdf2..fea44a0ebca 100644
--- a/qa/qa/page/admin/menu.rb
+++ b/qa/qa/page/admin/menu.rb
@@ -21,7 +21,7 @@ module QA
end
def go_to_applications
- click_element(:nav_item_link, submenu_item: 'Applications')
+ click_element('nav-item-link', submenu_item: 'Applications')
end
private
diff --git a/qa/qa/page/admin/overview/users/index.rb b/qa/qa/page/admin/overview/users/index.rb
index c444b728f5a..fb1a7c29008 100644
--- a/qa/qa/page/admin/overview/users/index.rb
+++ b/qa/qa/page/admin/overview/users/index.rb
@@ -11,7 +11,7 @@ module QA
element 'pending-approval-tab'
end
- view 'app/assets/javascripts/admin/users/components/users_table.vue' do
+ view 'app/assets/javascripts/vue_shared/components/users_table/users_table.vue' do
element 'user-row-content'
end
diff --git a/qa/qa/page/admin/settings/metrics_and_profiling.rb b/qa/qa/page/admin/settings/metrics_and_profiling.rb
index aa2399ba810..2e919dd171a 100644
--- a/qa/qa/page/admin/settings/metrics_and_profiling.rb
+++ b/qa/qa/page/admin/settings/metrics_and_profiling.rb
@@ -9,7 +9,7 @@ module QA
view 'app/views/admin/application_settings/metrics_and_profiling.html.haml' do
element 'performance-bar-settings-content'
- element :usage_statistics_settings_content
+ element 'usage-statistics-settings-content'
end
def expand_performance_bar(&block)
@@ -19,7 +19,7 @@ module QA
end
def expand_usage_statistics(&block)
- expand_content(:usage_statistics_settings_content) do
+ expand_content('usage-statistics-settings-content') do
Component::UsageStatistics.perform(&block)
end
end
diff --git a/qa/qa/page/component/ci_badge_link.rb b/qa/qa/page/component/ci_badge_link.rb
deleted file mode 100644
index d0a40b4bfbe..00000000000
--- a/qa/qa/page/component/ci_badge_link.rb
+++ /dev/null
@@ -1,52 +0,0 @@
-# frozen_string_literal: true
-
-module QA
- module Page
- module Component
- module CiBadgeLink
- extend QA::Page::PageConcern
-
- COMPLETED_STATUSES = %w[Passed Failed Canceled Blocked Skipped Manual].freeze # excludes Created, Pending, Running
- INCOMPLETE_STATUSES = %w[Pending Created Running].freeze
-
- # e.g. def passed?(timeout: nil); status_badge == 'Passed'; end
- COMPLETED_STATUSES.map do |status|
- define_method "#{status.downcase}?" do |timeout: nil|
- timeout ? completed?(timeout: timeout) : completed?
- status_badge == status
- end
-
- # has_passed? => passed?
- # has_failed? => failed?
- alias_method :"has_#{status.downcase}?", :"#{status.downcase}?"
- end
-
- # e.g. def pending?; status_badge == 'Pending'; end
- INCOMPLETE_STATUSES.map do |status|
- define_method "#{status.downcase}?" do
- status_badge == status
- end
- end
-
- def self.included(base)
- super
-
- base.view 'app/assets/javascripts/vue_shared/components/ci_badge_link.vue' do
- element 'ci-badge-text'
- end
- end
-
- def status_badge
- # There are more than 1 on job details page
- all_elements('ci-badge-text', minimum: 1).first.text
- end
-
- def completed?(timeout: 60)
- wait_until(reload: false, sleep_interval: 3.0, max_duration: timeout) do
- COMPLETED_STATUSES.include?(status_badge)
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/page/component/ci_icon.rb b/qa/qa/page/component/ci_icon.rb
new file mode 100644
index 00000000000..1ddcc810f95
--- /dev/null
+++ b/qa/qa/page/component/ci_icon.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+module QA
+ module Page
+ module Component
+ module CiIcon
+ extend QA::Page::PageConcern
+
+ # rubocop:disable Layout/LineLength
+ COMPLETED_STATUSES = %w[Passed Failed Canceled Blocked Skipped Manual].freeze # excludes Created, Pending, Running
+ # rubocop:enable Layout/LineLength
+ INCOMPLETE_STATUSES = %w[Pending Created Running].freeze
+
+ # e.g. def passed?(timeout: nil); status_badge == 'Passed'; end
+ COMPLETED_STATUSES.map do |status|
+ define_method "#{status.downcase}?" do |timeout: nil|
+ timeout ? completed?(timeout: timeout) : completed?
+ status_badge == status
+ end
+
+ # has_passed? => passed?
+ # has_failed? => failed?
+ alias_method :"has_#{status.downcase}?", :"#{status.downcase}?"
+ end
+
+ # e.g. def pending?; status_badge == 'Pending'; end
+ INCOMPLETE_STATUSES.map do |status|
+ define_method "#{status.downcase}?" do
+ status_badge == status
+ end
+ end
+
+ def self.included(base)
+ super
+
+ base.view 'app/assets/javascripts/vue_shared/components/ci_icon.vue' do
+ element 'ci-icon-text'
+ end
+ end
+
+ def status_badge
+ # There are more than 1 on job details page
+ all_elements('ci-icon-text', minimum: 1).first.text
+ end
+
+ def completed?(timeout: 60)
+ wait_until(reload: false, sleep_interval: 3.0, max_duration: timeout) do
+ COMPLETED_STATUSES.include?(status_badge)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/component/commit_modal.rb b/qa/qa/page/component/commit_modal.rb
index 7192e8bafb5..4f0618c6f3d 100644
--- a/qa/qa/page/component/commit_modal.rb
+++ b/qa/qa/page/component/commit_modal.rb
@@ -5,7 +5,7 @@ module QA
module Component
class CommitModal < Page::Base
view 'app/assets/javascripts/projects/commit/components/form_modal.vue' do
- element :submit_commit_button, required: true
+ element 'submit-commit', required: true
end
end
end
diff --git a/qa/qa/page/component/deploy_token.rb b/qa/qa/page/component/deploy_token.rb
new file mode 100644
index 00000000000..71501391db1
--- /dev/null
+++ b/qa/qa/page/component/deploy_token.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+module QA
+ module Page
+ module Component
+ module DeployToken
+ extend QA::Page::PageConcern
+
+ def self.included(base)
+ super
+
+ base.view 'app/views/shared/deploy_tokens/_form.html.haml' do
+ element 'deploy-token-name-field'
+ element 'deploy-token-expires-at-field'
+ element 'deploy-token-read-repository-checkbox'
+ element 'deploy-token-read-package-registry-checkbox'
+ element 'deploy-token-write-package-registry-checkbox'
+ element 'deploy-token-read-registry-checkbox'
+ element 'deploy-token-write-registry-checkbox'
+ element 'create-deploy-token-button'
+ end
+
+ base.view 'app/views/shared/deploy_tokens/_new_deploy_token.html.haml' do
+ element 'created-deploy-token-container'
+ element 'deploy-token-user-field'
+ element 'deploy-token-field'
+ end
+ end
+
+ def fill_token_name(name)
+ fill_element('deploy-token-name-field', name)
+ end
+
+ def fill_token_expires_at(expires_at)
+ fill_element('deploy-token-expires-at-field', "#{expires_at}\n")
+ end
+
+ def fill_scopes(scopes)
+ check_element('deploy-token-read-repository-checkbox', true) if scopes.include? :read_repository
+ check_element('deploy-token-read-package-registry-checkbox', true) if scopes.include? :read_package_registry
+ check_element('deploy-token-write-package-registry-checkbox', true) if scopes.include? :write_package_registry
+ check_element('deploy-token-read-registry-checkbox', true) if scopes.include? :read_registry
+ check_element('deploy-token-write-registry-checkbox', true) if scopes.include? :write_registry
+ end
+
+ def add_token
+ click_element('create-deploy-token-button')
+ end
+
+ def token_username
+ within_new_project_deploy_token do
+ find_element('deploy-token-user-field').value
+ end
+ end
+
+ def token_password
+ within_new_project_deploy_token do
+ find_element('deploy-token-field').value
+ end
+ end
+
+ private
+
+ def within_new_project_deploy_token(&block)
+ has_element?('created-deploy-token-container', wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME)
+
+ within_element('created-deploy-token-container', &block)
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/component/design_management.rb b/qa/qa/page/component/design_management.rb
index 4caa5169c5f..a43c9f95ae4 100644
--- a/qa/qa/page/component/design_management.rb
+++ b/qa/qa/page/component/design_management.rb
@@ -11,49 +11,49 @@ module QA
base.class_eval do
view 'app/assets/javascripts/design_management/components/design_notes/design_discussion.vue' do
- element :design_discussion_content
+ element 'design-discussion-content'
end
view 'app/assets/javascripts/design_management/components/design_notes/design_note.vue' do
- element :note_content
+ element 'note-text'
end
view 'app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue' do
- element :note_textarea
- element :save_comment_button
+ element 'note-textarea'
+ element 'save-comment-button'
end
view 'app/assets/javascripts/design_management/components/design_overlay.vue' do
- element :design_image_button
+ element 'design-image-button'
end
view 'app/assets/javascripts/design_management/components/list/item.vue' do
- element :design_file_name
- element :design_image
- element :design_status_icon
+ element 'design-file-name'
+ element 'design-image'
+ element 'design-status-icon'
end
view 'app/assets/javascripts/design_management/pages/index.vue' do
- element :archive_button
- element :design_checkbox
- element :design_dropzone_content
+ element 'archive-button'
+ element 'design-checkbox'
+ element 'design-dropzone-content'
end
view 'app/assets/javascripts/design_management/components/delete_button.vue' do
- element :confirm_archiving_button
+ element 'confirm-archiving-button'
end
end
end
def add_annotation(note)
- click_element(:design_image_button)
- fill_element(:note_textarea, note)
- click_element(:save_comment_button)
+ click_element('design-image-button')
+ fill_element('note-textarea', note)
+ click_element('save-comment-button')
# It takes a moment for the annotation to be saved.
# We'll check for the annotation in a test, but here we'll at least
# wait for the "Save comment" button to disappear
- saved = has_no_element?(:save_comment_button)
+ saved = has_no_element?('save-comment-button')
return if saved
raise RSpec::Expectations::ExpectationNotMetError, %q(There was a problem while adding the annotation)
@@ -64,16 +64,16 @@ module QA
# It accepts a `class:` option, but that only works for class attributes
# It doesn't work as a CSS selector.
# So instead we use the name attribute as a locator
- within_element(:design_dropzone_content) do
+ within_element('design-dropzone-content') do
page.attach_file("upload_file", design_file_path, make_visible: { display: 'block' })
end
filename = ::File.basename(design_file_path)
wait_until(reload: false, sleep_interval: 1, message: "Design upload") do
- image = find_element(:design_image, filename: filename)
+ image = find_element('design-image', filename: filename).find('img')
- has_element?(:design_file_name, text: filename) && image["complete"] && image["naturalWidth"].to_i > 0
+ has_element?('design-file-name', text: filename) && image["complete"] && image["naturalWidth"].to_i > 0
end
end
@@ -83,38 +83,38 @@ module QA
end
def click_design(filename)
- click_element(:design_file_name, text: filename)
+ click_element('design-file-name', text: filename)
end
def select_design(filename)
- click_element(:design_checkbox, design: filename)
+ click_element('design-checkbox', design: filename)
end
def archive_selected_designs
- click_element(:archive_button)
- click_element(:confirm_archiving_button)
+ click_element('archive-button')
+ click_element('confirm-archiving-button')
end
def has_annotation?(note)
- within_element_by_index(:design_discussion_content, 0) do
- has_element?(:note_content, text: note)
+ within_element_by_index('design-discussion-content', 0) do
+ has_element?('note-text', text: note)
end
end
def has_design?(filename)
- has_element?(:design_file_name, text: filename)
+ has_element?('design-file-name', text: filename)
end
def has_no_design?(filename)
- has_no_element?(:design_file_name, text: filename)
+ has_no_element?('design-file-name', text: filename)
end
def has_created_icon?
- has_element?(:design_status_icon, status: 'file-addition-solid')
+ has_element?('design-status-icon', status: 'file-addition-solid')
end
def has_modified_icon?
- has_element?(:design_status_icon, status: 'file-modified-solid')
+ has_element?('design-status-icon', status: 'file-modified-solid')
end
end
end
diff --git a/qa/qa/page/component/import/gitlab.rb b/qa/qa/page/component/import/gitlab.rb
index 1cb8a099a70..6c5516dd2e2 100644
--- a/qa/qa/page/component/import/gitlab.rb
+++ b/qa/qa/page/component/import/gitlab.rb
@@ -11,17 +11,16 @@ module QA
super
base.view 'app/views/import/gitlab_projects/new.html.haml' do
- element :import_project_button
+ element 'import-project-button'
end
base.view 'app/views/import/shared/_new_project_form.html.haml' do
- element :project_name_field
- element :project_slug_field
+ element 'project-name-field'
end
end
def set_imported_project_name(name)
- fill_element(:project_name_field, name)
+ fill_element('project-name-field', name)
end
def attach_exported_file(path)
@@ -29,7 +28,7 @@ module QA
end
def click_import_gitlab_project
- click_element(:import_project_button)
+ click_element('import-project-button')
wait_until(reload: false) do
has_notice?("The project was successfully imported.") || has_element?('project-name-content')
diff --git a/qa/qa/page/component/import/selection.rb b/qa/qa/page/component/import/selection.rb
index db2ff74e0f8..bd3268c3b16 100644
--- a/qa/qa/page/component/import/selection.rb
+++ b/qa/qa/page/component/import/selection.rb
@@ -9,16 +9,16 @@ module QA
super
base.view 'app/views/projects/_import_project_pane.html.haml' do
- element :gitlab_import_button
+ element 'gitlab-import-button'
end
end
def click_gitlab
retry_until(reload: true, max_attempts: 10, message: 'Waiting for import source to be enabled') do
- has_element?(:gitlab_import_button)
+ has_element?('gitlab-import-button')
end
- click_element(:gitlab_import_button)
+ click_element('gitlab-import-button')
end
end
end
diff --git a/qa/qa/page/dashboard/todos.rb b/qa/qa/page/dashboard/todos.rb
index 94fd20b80ab..40b8498d6bf 100644
--- a/qa/qa/page/dashboard/todos.rb
+++ b/qa/qa/page/dashboard/todos.rb
@@ -7,18 +7,18 @@ module QA
include Page::Component::Snippet
view 'app/views/dashboard/todos/index.html.haml' do
- element :todos_list_container, required: true
+ element 'todos-list-container', required: true
element 'group-dropdown'
end
view 'app/views/dashboard/todos/_todo.html.haml' do
element 'todo-item-container'
- element :todo_action_name_content
+ element 'todo-action-name-content'
element 'todo-author-name-content'
end
view 'app/helpers/dropdowns_helper.rb' do
- element :dropdown_input_field
+ element 'dropdown-input-field'
element 'dropdown-list-content'
end
@@ -33,7 +33,7 @@ module QA
def filter_todos_by_group(group)
click_element 'group-dropdown'
- fill_element(:dropdown_input_field, group.path)
+ fill_element('dropdown-input-field', group.path)
within_element('dropdown-list-content') do
click_on group.path
@@ -54,9 +54,9 @@ module QA
private
def has_latest_todo_with_content?(action, **kwargs)
- within_element(:todos_list_container) do
+ within_element('todos-list-container') do
within_element_by_index('todo-item-container', 0) do
- has_element?(:todo_action_name_content, text: action) &&
+ has_element?('todo-action-name-content', text: action) &&
has_element?(kwargs[:selector], text: kwargs[:text])
end
end
diff --git a/qa/qa/page/file/edit.rb b/qa/qa/page/file/edit.rb
index 665fef0d794..e7f243390ba 100644
--- a/qa/qa/page/file/edit.rb
+++ b/qa/qa/page/file/edit.rb
@@ -9,7 +9,7 @@ module QA
include Shared::Editor
def has_markdown_preview?(component, content)
- within_element(:source_editor_preview_container) do
+ within_element('source-editor-preview-container') do
has_css?(component, exact_text: content)
end
end
diff --git a/qa/qa/page/file/form.rb b/qa/qa/page/file/form.rb
index cfc689daa32..30cd4f11bb4 100644
--- a/qa/qa/page/file/form.rb
+++ b/qa/qa/page/file/form.rb
@@ -11,7 +11,7 @@ module QA
include Shared::Editor
view 'app/views/projects/blob/_editor.html.haml' do
- element :file_name_field
+ element 'file-name-field'
end
view 'app/assets/javascripts/blob/filepath_form/components/template_selector.vue' do
@@ -19,7 +19,7 @@ module QA
end
def add_name(name)
- fill_element(:file_name_field, name)
+ fill_element('file-name-field', name)
end
def add_custom_name(template_name)
diff --git a/qa/qa/page/file/shared/commit_message.rb b/qa/qa/page/file/shared/commit_message.rb
index 9d90400f42f..32e1abf5590 100644
--- a/qa/qa/page/file/shared/commit_message.rb
+++ b/qa/qa/page/file/shared/commit_message.rb
@@ -11,28 +11,28 @@ module QA
super
base.view 'app/assets/javascripts/repository/components/delete_blob_modal.vue' do
- element :commit_message_field
+ element 'commit-message-field'
end
base.view 'app/assets/javascripts/repository/components/commit_info.vue' do
- element :commit_content
+ element 'commit-content'
end
base.view 'app/views/shared/_commit_message_container.html.haml' do
- element :commit_message_field
+ element 'commit-message-field'
end
base.view 'app/views/projects/commits/_commit.html.haml' do
- element :commit_content
+ element 'commit-content'
end
end
def add_commit_message(message)
- fill_element(:commit_message_field, message)
+ fill_element('commit-message-field', message)
end
def has_commit_message?(text)
- has_element?(:commit_content, text: text)
+ has_element?('commit-content', text: text)
end
end
end
diff --git a/qa/qa/page/file/shared/editor.rb b/qa/qa/page/file/shared/editor.rb
index 2e28c158ea1..86d044bb4fa 100644
--- a/qa/qa/page/file/shared/editor.rb
+++ b/qa/qa/page/file/shared/editor.rb
@@ -11,7 +11,7 @@ module QA
super
base.view 'app/views/projects/blob/_editor.html.haml' do
- element :source_editor_preview_container
+ element 'source-editor-preview-container'
end
end
@@ -30,7 +30,7 @@ module QA
private
def text_area
- within_element :source_editor_preview_container do
+ within_element 'source-editor-preview-container' do
find('textarea', visible: false)
end
end
diff --git a/qa/qa/page/file/show.rb b/qa/qa/page/file/show.rb
index 284fab58d5d..e52b1838392 100644
--- a/qa/qa/page/file/show.rb
+++ b/qa/qa/page/file/show.rb
@@ -9,18 +9,18 @@ module QA
include Page::Component::BlobContent
view 'app/assets/javascripts/repository/components/blob_button_group.vue' do
- element :lock_button
+ element 'lock-button'
end
view 'app/assets/javascripts/vue_shared/components/web_ide_link.vue' do
- element :action_dropdown
- element :edit_menu_item, ':data-qa-selector="`${action.key}_menu_item`"' # rubocop:disable QA/ElementWithPattern
- element :webide_menu_item, ':data-qa-selector="`${action.key}_menu_item`"' # rubocop:disable QA/ElementWithPattern
+ element 'action-dropdown'
+ element 'edit-menu-item', ':data-testid="`${action.key}-menu-item`"' # rubocop:disable QA/ElementWithPattern
+ element 'webide-menu-item', ':data-testid="`${action.key}-menu-item`"' # rubocop:disable QA/ElementWithPattern
end
def click_edit
- click_element(:action_dropdown)
- click_element(:edit_menu_item)
+ click_element('action-dropdown')
+ click_element('edit-menu-item')
end
def click_delete
diff --git a/qa/qa/page/group/settings/group_deploy_tokens.rb b/qa/qa/page/group/settings/group_deploy_tokens.rb
index c1c3303113b..4a44787d26d 100644
--- a/qa/qa/page/group/settings/group_deploy_tokens.rb
+++ b/qa/qa/page/group/settings/group_deploy_tokens.rb
@@ -5,60 +5,7 @@ module QA
module Group
module Settings
class GroupDeployTokens < Page::Base
- view 'app/views/shared/deploy_tokens/_form.html.haml' do
- element :deploy_token_name_field
- element :deploy_token_expires_at_field
- element :deploy_token_read_repository_checkbox
- element :deploy_token_read_package_registry_checkbox
- element :deploy_token_read_registry_checkbox
- element :deploy_token_write_package_registry_checkbox
- element :create_deploy_token_button
- end
-
- view 'app/views/shared/deploy_tokens/_new_deploy_token.html.haml' do
- element :created_deploy_token_container
- element :deploy_token_user_field
- element :deploy_token_field
- end
-
- def fill_token_name(name)
- fill_element(:deploy_token_name_field, name)
- end
-
- def fill_token_expires_at(expires_at)
- fill_element(:deploy_token_expires_at_field, expires_at.to_s + "\n")
- end
-
- def fill_scopes(read_repository: false, read_registry: false, read_package_registry: false, write_package_registry: false )
- check_element(:deploy_token_read_repository_checkbox, true) if read_repository
- check_element(:deploy_token_read_package_registry_checkbox, true) if read_package_registry
- check_element(:deploy_token_read_registry_checkbox, true) if read_registry
- check_element(:deploy_token_write_package_registry_checkbox, true) if write_package_registry
- end
-
- def add_token
- click_element(:create_deploy_token_button)
- end
-
- def token_username
- within_new_project_deploy_token do
- find_element(:deploy_token_user_field).value
- end
- end
-
- def token_password
- within_new_project_deploy_token do
- find_element(:deploy_token_field).value
- end
- end
-
- private
-
- def within_new_project_deploy_token(&block)
- has_element?(:created_deploy_token_container, wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME)
-
- within_element(:created_deploy_token_container, &block)
- end
+ include Page::Component::DeployToken
end
end
end
diff --git a/qa/qa/page/group/settings/repository.rb b/qa/qa/page/group/settings/repository.rb
index 2cc80ef26c6..c160102a5ab 100644
--- a/qa/qa/page/group/settings/repository.rb
+++ b/qa/qa/page/group/settings/repository.rb
@@ -8,11 +8,11 @@ module QA
include QA::Page::Settings::Common
view 'app/views/shared/deploy_tokens/_index.html.haml' do
- element :deploy_tokens_settings_content
+ element 'deploy-tokens-settings-content'
end
def expand_deploy_tokens(&block)
- expand_content(:deploy_tokens_settings_content) do
+ expand_content('deploy-tokens-settings-content') do
Settings::GroupDeployTokens.perform(&block)
end
end
diff --git a/qa/qa/page/group/sub_menus/main.rb b/qa/qa/page/group/sub_menus/main.rb
index a9f2e11c532..b7b694335f9 100644
--- a/qa/qa/page/group/sub_menus/main.rb
+++ b/qa/qa/page/group/sub_menus/main.rb
@@ -16,7 +16,7 @@ module QA
end
def go_to_group_overview
- click_element(:nav_item_link, submenu_item: 'group-overview')
+ click_element('nav-item-link', submenu_item: 'group-overview')
end
end
end
diff --git a/qa/qa/page/main/login.rb b/qa/qa/page/main/login.rb
index 1fd0b5b453c..97cd9fc443a 100644
--- a/qa/qa/page/main/login.rb
+++ b/qa/qa/page/main/login.rb
@@ -5,6 +5,7 @@ module QA
module Main
class Login < Page::Base
include Layout::Flash
+ include Runtime::Canary
view 'app/views/devise/passwords/edit.html.haml' do
element :password_field
@@ -250,7 +251,11 @@ module QA
wait_for_gitlab_to_respond
- Page::Main::Menu.validate_elements_present! unless skip_page_validation
+ return if skip_page_validation
+
+ Page::Main::Menu.validate_elements_present!
+
+ validate_canary!
end
def fill_in_credential(user)
diff --git a/qa/qa/page/main/menu.rb b/qa/qa/page/main/menu.rb
index 2413166e120..0a023c757ee 100644
--- a/qa/qa/page/main/menu.rb
+++ b/qa/qa/page/main/menu.rb
@@ -14,6 +14,10 @@ module QA
element :navbar, required: true # TODO: rename to sidebar once it's default implementation
end
+ view 'app/assets/javascripts/super_sidebar/components/user_bar.vue' do
+ element 'canary-badge-link'
+ end
+
view 'app/assets/javascripts/super_sidebar/components/user_menu.vue' do
element 'user-dropdown', required: !Runtime::Env.phone_layout?
element :user_avatar_content, required: !Runtime::Env.phone_layout?
@@ -78,23 +82,23 @@ module QA
end
def go_to_projects
- click_element(:nav_item_link, submenu_item: 'Projects')
+ click_element('nav-item-link', submenu_item: 'Projects')
end
def go_to_groups
# This needs to be fixed in the tests themselves. Fullfillment tests try to go to groups view from the
# group. Instead of having a global hack, explicit test should navigate to correct view first.
# see: https://gitlab.com/gitlab-org/gitlab/-/issues/403589#note_1383040061
- go_to_your_work unless has_element?(:nav_item_link, submenu_item: 'Groups', wait: 0)
- click_element(:nav_item_link, submenu_item: 'Groups')
+ go_to_your_work unless has_element?('nav-item-link', submenu_item: 'Groups', wait: 0)
+ click_element('nav-item-link', submenu_item: 'Groups')
end
def go_to_snippets
- click_element(:nav_item_link, submenu_item: 'Snippets')
+ click_element('nav-item-link', submenu_item: 'Snippets')
end
def go_to_workspaces
- click_element(:nav_item_link, submenu_item: 'Workspaces')
+ click_element('nav-item-link', submenu_item: 'Workspaces')
end
def go_to_menu_dropdown_option(option_name)
@@ -112,7 +116,7 @@ module QA
end
def signed_in_as_user?(user)
- return false unless has_personal_area?
+ return false unless signed_in?
within_user_menu do
has_element?('user-profile-link', text: /#{user.username}/)
@@ -176,13 +180,13 @@ module QA
end
# To verify whether the user has been directed to a canary web node
- # @return [Boolean] result of checking existence of :canary_badge_link element
+ # @return [Boolean] result of checking existence of 'canary-badge-link' element
# @example:
# Menu.perform do |menu|
# expect(menu.canary?).to be(true)
# end
def canary?
- has_element?(:canary_badge_link)
+ has_element?('canary-badge-link')
end
private
diff --git a/qa/qa/page/merge_request/show.rb b/qa/qa/page/merge_request/show.rb
index a51c65a18c6..8129567c079 100644
--- a/qa/qa/page/merge_request/show.rb
+++ b/qa/qa/page/merge_request/show.rb
@@ -473,7 +473,7 @@ module QA
def cherry_pick!
click_element('cherry-pick-button', Page::Component::CommitModal)
- click_element(:submit_commit_button)
+ click_element('submit-commit')
end
def revert_change!
@@ -482,7 +482,7 @@ module QA
retry_on_exception(reload: true) do
click_element('revert-button', Page::Component::CommitModal)
end
- click_element(:submit_commit_button)
+ click_element('submit-commit')
end
def mr_widget_text
diff --git a/qa/qa/page/profile/menu.rb b/qa/qa/page/profile/menu.rb
index d3b08944914..37128571b7f 100644
--- a/qa/qa/page/profile/menu.rb
+++ b/qa/qa/page/profile/menu.rb
@@ -7,23 +7,23 @@ module QA
include SubMenus::CreateNewMenu
def click_ssh_keys
- click_element(:nav_item_link, submenu_item: 'SSH Keys')
+ click_element('nav-item-link', submenu_item: 'SSH Keys')
end
def click_account
- click_element(:nav_item_link, submenu_item: 'Account')
+ click_element('nav-item-link', submenu_item: 'Account')
end
def click_emails
- click_element(:nav_item_link, submenu_item: 'Emails')
+ click_element('nav-item-link', submenu_item: 'Emails')
end
def click_password
- click_element(:nav_item_link, submenu_item: 'Password')
+ click_element('nav-item-link', submenu_item: 'Password')
end
def click_access_tokens
- click_element(:nav_item_link, submenu_item: 'Access Tokens')
+ click_element('nav-item-link', submenu_item: 'Access Tokens')
end
end
end
diff --git a/qa/qa/page/project/branches/show.rb b/qa/qa/page/project/branches/show.rb
index bbe0f91abf6..4e0d2e0265a 100644
--- a/qa/qa/page/project/branches/show.rb
+++ b/qa/qa/page/project/branches/show.rb
@@ -6,36 +6,36 @@ module QA
module Branches
class Show < Page::Base
view 'app/assets/javascripts/branches/components/branch_more_actions.vue' do
- element :delete_branch_button
+ element 'delete-branch-button'
end
view 'app/assets/javascripts/branches/components/delete_branch_modal.vue' do
- element :delete_branch_confirmation_button
+ element 'delete-branch-confirmation-button'
end
view 'app/views/projects/branches/_branch.html.haml' do
- element :branch_container
- element :branch_link
+ element 'branch-container'
+ element 'branch-link'
end
view 'app/views/projects/branches/_panel.html.haml' do
- element :all_branches_container
+ element 'all-branches-container'
end
def delete_branch(branch_name)
- within_element(:branch_container, name: branch_name) do
- click_element(:delete_branch_button)
+ within_element('branch-container', name: branch_name) do
+ click_element('delete-branch-button')
end
- click_element(:delete_branch_confirmation_button)
+ click_element('delete-branch-confirmation-button')
finished_loading?
end
def has_no_branch?(branch_name, reload: false)
wait_until(reload: reload) do
- within_element(:all_branches_container) do
- has_no_element?(:branch_link, text: branch_name)
+ within_element('all-branches-container') do
+ has_no_element?('branch-link', text: branch_name)
end
end
end
diff --git a/qa/qa/page/project/commit/show.rb b/qa/qa/page/project/commit/show.rb
index bc44a4e5e72..1c876f450c5 100644
--- a/qa/qa/page/project/commit/show.rb
+++ b/qa/qa/page/project/commit/show.rb
@@ -6,41 +6,41 @@ module QA
module Commit
class Show < Page::Base
view 'app/views/projects/commit/_commit_box.html.haml' do
- element :commit_sha_content
+ element 'commit-sha-content'
end
view 'app/assets/javascripts/projects/commit/components/commit_options_dropdown.vue' do
- element :options_button
- element :revert_button
- element :cherry_pick_button
- element :email_patches
- element :plain_diff
+ element 'commit-options-dropdown'
+ element 'revert-link'
+ element 'cherry-pick-link'
+ element 'email-patches-link'
+ element 'plain-diff-link'
end
def revert_commit
- click_element(:options_button)
- click_element(:revert_button, Page::Component::CommitModal)
- click_element(:submit_commit_button)
+ click_element('commit-options-dropdown')
+ click_element('revert-link', Page::Component::CommitModal)
+ click_element('submit-commit')
end
def cherry_pick_commit
- click_element(:options_button)
- click_element(:cherry_pick_button, Page::Component::CommitModal)
- click_element(:submit_commit_button)
+ click_element('commit-options-dropdown')
+ click_element('cherry-pick-link', Page::Component::CommitModal)
+ click_element('submit-commit')
end
def select_email_patches
- click_element :options_button
- visit_link_in_element :email_patches
+ click_element 'commit-options-dropdown'
+ visit_link_in_element 'email-patches-link'
end
def select_plain_diff
- click_element :options_button
- visit_link_in_element :plain_diff
+ click_element 'commit-options-dropdown'
+ visit_link_in_element 'plain-diff-link'
end
def commit_sha
- find_element(:commit_sha_content).text
+ find_element('commit-sha-content').text
end
end
end
diff --git a/qa/qa/page/project/import/github.rb b/qa/qa/page/project/import/github.rb
index 08400042028..128ed9682d4 100644
--- a/qa/qa/page/project/import/github.rb
+++ b/qa/qa/page/project/import/github.rb
@@ -6,17 +6,20 @@ module QA
module Import
class Github < Page::Base
view 'app/views/import/github/new.html.haml' do
- element :personal_access_token_field
- element :authenticate_button
+ element 'personal-access-token-field'
+ element 'authenticate-button'
+ end
+
+ view 'app/assets/javascripts/import_entities/import_projects/components/advanced_settings.vue' do
+ element 'advanced-settings-checkbox'
end
view 'app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue' do
- element :project_import_row
- element :project_path_field
- element :import_button
- element :project_path_content
- element :go_to_project_link
- element :import_status_indicator
+ element 'project-import-row'
+ element 'project-path-field'
+ element 'import-button'
+ element 'go-to-project-link'
+ element 'import-status-indicator'
end
view "app/assets/javascripts/import_entities/components/import_target_dropdown.vue" do
@@ -30,12 +33,12 @@ module QA
def add_personal_access_token(personal_access_token)
# If for some reasons this process is retried, user cannot re-enter github token in the same group
# In this case skip this step and proceed to import project row
- return unless has_element?(:personal_access_token_field)
+ return unless has_element?('personal-access-token-field')
raise ArgumentError, "No personal access token was provided" if personal_access_token.empty?
- fill_element(:personal_access_token_field, personal_access_token)
- click_element(:authenticate_button)
+ fill_element('personal-access-token-field', personal_access_token)
+ click_element('authenticate-button')
finished_loading?
end
@@ -45,15 +48,15 @@ module QA
# @param [String] target_group_path
# @return [void]
def import!(gh_project_name, target_group_path, project_name)
- within_element(:project_import_row, source_project: gh_project_name) do
+ within_element('project-import-row', source_project: gh_project_name) do
click_element('target-namespace-dropdown')
click_element("listbox-item-#{target_group_path}", wait: 10)
- fill_element(:project_path_field, project_name)
+ fill_element('project-path-field', project_name)
retry_until do
- click_element(:import_button)
+ click_element('import-button')
# Make sure import started before waiting for completion
- has_no_element?(:import_status_indicator, text: "Not started", wait: 1)
+ has_no_element?('import-status-indicator', text: "Not started", wait: 1)
end
end
end
@@ -63,8 +66,8 @@ module QA
# @param [String] gh_project_name
# @return [Boolean]
def has_go_to_project_link?(gh_project_name)
- within_element(:project_import_row, source_project: gh_project_name) do
- has_element?(:go_to_project_link)
+ within_element('project-import-row', source_project: gh_project_name) do
+ has_element?('go-to-project-link')
end
end
@@ -78,14 +81,14 @@ module QA
wait: QA::Support::WaitForRequests::DEFAULT_MAX_WAIT_TIME,
allow_partial_import: false
)
- within_element(:project_import_row, source_project: gh_project_name, skip_finished_loading_check: true) do
+ within_element('project-import-row', source_project: gh_project_name, skip_finished_loading_check: true) do
wait_until(
max_duration: wait,
sleep_interval: 5,
reload: false,
skip_finished_loading_check_on_refresh: true
) do
- status_selector = 'import_status_indicator'
+ status_selector = 'import-status-indicator'
next has_element?(status_selector, text: "Complete", wait: 1) unless allow_partial_import
@@ -102,7 +105,7 @@ module QA
# @param [Symbol] option_name
# @return [void]
def select_advanced_option(option_name)
- check_element(:advanced_settings_checkbox, true, option_name: option_name)
+ check_element('advanced-settings-checkbox', true, option_name: option_name)
end
end
end
diff --git a/qa/qa/page/project/issue/show.rb b/qa/qa/page/project/issue/show.rb
index e75682aee57..7451a05daec 100644
--- a/qa/qa/page/project/issue/show.rb
+++ b/qa/qa/page/project/issue/show.rb
@@ -89,6 +89,7 @@ module QA
def open_actions_dropdown
# We use find here because these are gitlab-ui elements
+ wait_for_requests
find('[data-testid="desktop-dropdown"] > button').click
end
end
diff --git a/qa/qa/page/project/job/show.rb b/qa/qa/page/project/job/show.rb
index a1ad3a50be7..c1b2eca9fc2 100644
--- a/qa/qa/page/project/job/show.rb
+++ b/qa/qa/page/project/job/show.rb
@@ -5,7 +5,7 @@ module QA
module Project
module Job
class Show < QA::Page::Base
- include Component::CiBadgeLink
+ include Component::CiIcon
view 'app/assets/javascripts/ci/job_details/components/log/log.vue' do
element 'job-log-content'
@@ -68,12 +68,14 @@ module QA
end
end
- def has_locked_artifact?
- has_element?('artifacts-locked-message-content')
+ def has_locked_artifact?(wait: 240)
+ wait_until(reload: true, max_duration: wait, sleep_interval: 1) do
+ has_element?('artifacts-locked-message-content')
+ end
end
# Artifact unlock is async and depends on queue size on target env
- def has_unlocked_artifact?(wait: 120)
+ def has_unlocked_artifact?(wait: 240)
wait_until(reload: true, max_duration: wait, sleep_interval: 1) do
has_element?('artifacts-unlocked-message-content')
end
diff --git a/qa/qa/page/project/monitor/incidents/show.rb b/qa/qa/page/project/monitor/incidents/show.rb
new file mode 100644
index 00000000000..224b0c8917c
--- /dev/null
+++ b/qa/qa/page/project/monitor/incidents/show.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module QA
+ module Page
+ module Project
+ module Monitor
+ module Incidents
+ class Show < Page::Base
+ include Page::Component::Note
+ include Page::Component::Issuable::Sidebar
+
+ view 'app/assets/javascripts/sidebar/components/severity/sidebar_severity_widget.vue' do
+ element 'incident-severity'
+ element 'severity-block-container'
+ end
+
+ def has_severity?(severity)
+ wait_severity_block_finish_loading do
+ has_element?('incident-severity', text: severity)
+ end
+ end
+
+ private
+
+ def wait_severity_block_finish_loading
+ within_element('severity-block-container') do
+ wait_until(reload: false, max_duration: 10, sleep_interval: 1) do
+ finished_loading_block?
+ yield
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
+
+QA::Page::Project::Monitor::Incidents::Show.prepend_mod_with('Page::Project::Monitor::Incidents::Show', namespace: QA)
diff --git a/qa/qa/page/project/pipeline/index.rb b/qa/qa/page/project/pipeline/index.rb
index 5a050eaa8bb..bd5d934cc50 100644
--- a/qa/qa/page/project/pipeline/index.rb
+++ b/qa/qa/page/project/pipeline/index.rb
@@ -5,7 +5,7 @@ module QA
module Project
module Pipeline
class Index < QA::Page::Base
- include Component::CiBadgeLink
+ include Component::CiIcon
view 'app/assets/javascripts/ci/pipelines_page/components/pipeline_url.vue' do
element 'pipeline-url-link'
@@ -25,7 +25,7 @@ module QA
def latest_pipeline_status
within(latest_pipeline) do
- find_element('ci-badge-text')
+ find_element('ci-icon-text')
end.text
end
diff --git a/qa/qa/page/project/pipeline/show.rb b/qa/qa/page/project/pipeline/show.rb
index 0caf373fffd..151df85af3d 100644
--- a/qa/qa/page/project/pipeline/show.rb
+++ b/qa/qa/page/project/pipeline/show.rb
@@ -5,7 +5,7 @@ module QA
module Project
module Pipeline
class Show < QA::Page::Base
- include Component::CiBadgeLink
+ include Component::CiIcon
view 'app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue' do
element 'pipeline-details-header', required: true
@@ -43,7 +43,7 @@ module QA
def has_build?(name, status: :success, wait: nil)
if status
within_element('job-item-container', text: name) do
- has_selector?(".ci-status-icon-#{status}", **{ wait: wait }.compact)
+ has_selector?("[data-testid='status_#{status}_borderless-icon']", **{ wait: wait }.compact)
end
else
has_element?('job-item-container', text: name)
diff --git a/qa/qa/page/project/settings/branch_rules.rb b/qa/qa/page/project/settings/branch_rules.rb
index b01f493addf..401d75fd1ce 100644
--- a/qa/qa/page/project/settings/branch_rules.rb
+++ b/qa/qa/page/project/settings/branch_rules.rb
@@ -6,22 +6,22 @@ module QA
module Settings
class BranchRules < Page::Base
view 'app/assets/javascripts/projects/settings/repository/branch_rules/app.vue' do
- element :add_branch_rule_button
+ element 'add-branch-rule-button'
end
view 'app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue' do
- element :branch_content
- element :details_button
+ element 'branch-content'
+ element 'details-button'
end
def click_add_branch_rule
- click_element(:add_branch_rule_button)
+ click_element('add-branch-rule-button')
click_button('Create protected branch')
end
def navigate_to_branch_rules_details(branch_name)
- within_element(:branch_content, branch_name: branch_name) do
- click_element(:details_button)
+ within_element('branch-content', branch_name: branch_name) do
+ click_element('details-button')
end
end
end
diff --git a/qa/qa/page/project/settings/branch_rules_details.rb b/qa/qa/page/project/settings/branch_rules_details.rb
index f6806a30efa..4f8de1c3431 100644
--- a/qa/qa/page/project/settings/branch_rules_details.rb
+++ b/qa/qa/page/project/settings/branch_rules_details.rb
@@ -6,23 +6,23 @@ module QA
module Settings
class BranchRulesDetails < Page::Base
view 'app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue' do
- element :allowed_to_push_content
- element :allowed_to_merge_content
+ element 'allowed-to-push-content'
+ element 'allowed-to-merge-content'
end
view 'app/assets/javascripts/projects/settings/branch_rules/components/view/protection_row.vue' do
- element :access_level_content
+ element 'access-level'
end
def has_allowed_to_push?(role)
- within_element(:allowed_to_push_content) do
- has_element?(:access_level_content, role: role)
+ within_element('allowed-to-push-content') do
+ has_element?('access-level', role: role)
end
end
def has_allowed_to_merge?(role)
- within_element(:allowed_to_merge_content) do
- has_element?(:access_level_content, role: role)
+ within_element('allowed-to-merge-content') do
+ has_element?('access-level', role: role)
end
end
end
diff --git a/qa/qa/page/project/settings/ci_variables.rb b/qa/qa/page/project/settings/ci_variables.rb
index 8057dfdf2dd..4d5bf06f95b 100644
--- a/qa/qa/page/project/settings/ci_variables.rb
+++ b/qa/qa/page/project/settings/ci_variables.rb
@@ -7,41 +7,34 @@ module QA
class CiVariables < Page::Base
include QA::Page::Settings::Common
- # TODO: remove this when the ci_variable_drawer feature flag is enabled by default
- view 'app/assets/javascripts/ci/ci_variable_list/components/ci_variable_modal.vue' do
- element :ci_variable_key_field
- element :ci_variable_value_field
- element :ci_variable_save_button
- end
-
view 'app/assets/javascripts/ci/ci_variable_list/components/ci_variable_drawer.vue' do
- element :ci_variable_key_field
- element :ci_variable_value_field
- element :ci_variable_save_button
+ element 'ci-variable-key'
+ element 'ci-variable-value'
+ element 'ci-variable-confirm-button'
end
def fill_variable(key, value, masked = false)
- within_element(:ci_variable_key_field) { find('input').set key }
- fill_element :ci_variable_value_field, value
+ within_element('ci-variable-key') { find('input').set key }
+ fill_element 'ci-variable-value', value
click_ci_variable_save_button
wait_until(reload: false) do
- within_element('ci-variable-table') { has_element?(:edit_ci_variable_button) }
+ within_element('ci-variable-table') { has_element?('edit-ci-variable-button') }
end
end
def click_add_variable
- click_element :add_ci_variable_button
+ click_element 'add-ci-variable-button'
end
def click_edit_ci_variable
within_element('ci-variable-table') do
- click_element :edit_ci_variable_button
+ click_element 'edit-ci-variable-button'
end
end
def click_ci_variable_save_button
- click_element :ci_variable_save_button
+ click_element 'ci-variable-confirm-button'
end
end
end
diff --git a/qa/qa/page/project/settings/default_branch.rb b/qa/qa/page/project/settings/default_branch.rb
deleted file mode 100644
index a59158966c1..00000000000
--- a/qa/qa/page/project/settings/default_branch.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-# frozen_string_literal: true
-
-module QA
- module Page
- module Project
- module Settings
- class DefaultBranch < Page::Base
- include ::QA::Page::Component::Dropdown
-
- view 'app/views/projects/branch_defaults/_show.html.haml' do
- element :save_changes_button
- end
-
- view 'app/assets/javascripts/projects/settings/components/default_branch_selector.vue' do
- element :default_branch_dropdown
- end
-
- def set_default_branch(branch)
- expand_select_list
- search_and_select(branch)
- end
-
- def click_save_changes_button
- find('.btn-confirm').click
- end
- end
- end
- end
- end
-end
-
-QA::Page::Project::Settings::DefaultBranch.prepend_mod_with('Page::Project::Settings::DefaultBranch', namespace: QA)
diff --git a/qa/qa/page/project/settings/deploy_tokens.rb b/qa/qa/page/project/settings/deploy_tokens.rb
deleted file mode 100644
index cf25f4a0568..00000000000
--- a/qa/qa/page/project/settings/deploy_tokens.rb
+++ /dev/null
@@ -1,84 +0,0 @@
-# frozen_string_literal: true
-
-module QA
- module Page
- module Project
- module Settings
- class DeployTokens < Page::Base
- view 'app/views/shared/deploy_tokens/_form.html.haml' do
- element :deploy_token_name_field
- element :deploy_token_expires_at_field
- element :deploy_token_read_repository_checkbox
- element :deploy_token_read_package_registry_checkbox
- element :deploy_token_write_package_registry_checkbox
- element :deploy_token_read_registry_checkbox
- element :deploy_token_write_registry_checkbox
- element :create_deploy_token_button
- end
-
- view 'app/views/shared/deploy_tokens/_new_deploy_token.html.haml' do
- element :created_deploy_token_container
- element :deploy_token_user_field
- element :deploy_token_field
- end
-
- def fill_token_name(name)
- fill_element(:deploy_token_name_field, name)
- end
-
- def fill_token_expires_at(expires_at)
- fill_element(:deploy_token_expires_at_field, expires_at.to_s + "\n")
- end
-
- def fill_scopes(scopes)
- if scopes.include? :read_repository
- check_element(:deploy_token_read_repository_checkbox, true)
- end
-
- if scopes.include? :read_package_registry
- check_element(:deploy_token_read_package_registry_checkbox, true)
- end
-
- if scopes.include? :write_package_registry
- check_element(:deploy_token_write_package_registry_checkbox, true)
- end
-
- if scopes.include? :read_registry
- check_element(:deploy_token_read_registry_checkbox, true)
- end
-
- if scopes.include? :write_registry
- check_element(:deploy_token_write_registry_checkbox, true)
- end
- end
-
- def add_token
- click_element(:create_deploy_token_button)
- end
-
- def token_username
- within_new_project_deploy_token do
- find_element(:deploy_token_user_field).value
- end
- end
-
- def token_password
- within_new_project_deploy_token do
- find_element(:deploy_token_field).value
- end
- end
-
- private
-
- def within_new_project_deploy_token
- has_element?(:created_deploy_token_container, wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME)
-
- within_element(:created_deploy_token_container) do
- yield
- end
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/page/project/settings/merge_request.rb b/qa/qa/page/project/settings/merge_request.rb
index f3d9c763159..84a8f9cbdf0 100644
--- a/qa/qa/page/project/settings/merge_request.rb
+++ b/qa/qa/page/project/settings/merge_request.rb
@@ -8,23 +8,19 @@ module QA
include QA::Page::Settings::Common
view 'app/views/projects/settings/merge_requests/show.html.haml' do
- element :save_merge_request_changes_button
+ element 'save-merge-request-changes-button'
end
view 'app/views/projects/settings/merge_requests/_merge_request_merge_method_settings.html.haml' do
- element :merge_ff_radio
- end
-
- view 'app/views/projects/settings/merge_requests/_merge_request_pipelines_and_threads_options.html.haml' do
- element :only_allow_merge_if_all_discussions_are_resolved_checkbox
+ element 'merge-ff-radio'
end
def click_save_changes
- click_element(:save_merge_request_changes_button)
+ click_element('save-merge-request-changes-button')
end
def enable_ff_only
- choose_element(:merge_ff_radio, true)
+ choose_element('merge-ff-radio', true)
click_save_changes
end
end
diff --git a/qa/qa/page/project/settings/mirroring_repositories.rb b/qa/qa/page/project/settings/mirroring_repositories.rb
index 404f9184290..ddd8bf8b337 100644
--- a/qa/qa/page/project/settings/mirroring_repositories.rb
+++ b/qa/qa/page/project/settings/mirroring_repositories.rb
@@ -6,41 +6,41 @@ module QA
module Settings
class MirroringRepositories < Page::Base
view 'app/views/projects/mirrors/_authentication_method.html.haml' do
- element :authentication_method_field
+ element 'authentication-method-field'
element 'username-field'
element 'password-field'
end
view 'app/views/projects/mirrors/_mirror_repos.html.haml' do
- element :mirror_repository_url_field
- element :mirror_repository_button
+ element 'mirror-repository-url-field'
+ element 'mirror-repository-button'
element 'add-new-mirror'
end
view 'app/views/projects/mirrors/_mirror_repos_list.html.haml' do
- element :mirror_repository_url_content
- element :mirror_last_update_at_content
- element :mirror_error_badge_content
- element :mirrored_repository_row_container
- element :copy_public_key_button
+ element 'mirror-repository-url-content'
+ element 'mirror-last-update-at-content'
+ element 'mirror-error-badge-content'
+ element 'mirrored-repository-row-container'
+ element 'copy-public-key-button'
end
view 'app/views/projects/mirrors/_mirror_repos_form.html.haml' do
- element :mirror_direction_field
+ element 'mirror-direction-field'
end
view 'app/views/shared/_remote_mirror_update_button.html.haml' do
- element :update_now_button
+ element 'update-now-button'
end
view 'app/views/projects/mirrors/_ssh_host_keys.html.haml' do
- element :detect_host_keys
- element :fingerprints_list
+ element 'detect-host-keys'
+ element 'fingerprints-list'
end
def repository_url=(value)
click_element 'add-new-mirror'
- fill_element :mirror_repository_url_field, value
+ fill_element 'mirror-repository-url-field', value
end
def username=(value)
@@ -54,12 +54,12 @@ module QA
def mirror_direction=(value)
raise ArgumentError, "Mirror direction must be 'Push' or 'Pull'" unless %w[Push Pull].include?(value)
- select_element(:mirror_direction_field, value)
+ select_element('mirror-direction-field', value)
# Changing the mirror direction causes the fields below to change,
# and that change is animated, so we need to wait for the animation
# to complete otherwise changes to those fields could fail
- wait_for_animated_element :authentication_method_field
+ wait_for_animated_element 'authentication-method-field'
end
def authentication_method=(value)
@@ -67,35 +67,35 @@ module QA
raise ArgumentError, "Authentication method must be 'SSH public key', 'Password', or 'None'"
end
- select_element(:authentication_method_field, value)
+ select_element('authentication-method-field', value)
end
def public_key(url)
row_index = find_repository_row_index url
- within_element_by_index(:mirrored_repository_row_container, row_index) do
- find_element(:copy_public_key_button)['data-clipboard-text']
+ within_element_by_index('mirrored-repository-row-container', row_index) do
+ find_element('copy-public-key-button')['data-clipboard-text']
end
end
def detect_host_keys
- click_element :detect_host_keys
+ click_element 'detect-host-keys'
# The host key detection process is interrupted if we navigate away
# from the page before the fingerprint appears.
- find_element(:fingerprints_list, text: /.*/, wait: 60)
+ find_element('fingerprints-list', text: /.*/, wait: 60)
end
def mirror_repository
- click_element :mirror_repository_button
+ click_element 'mirror-repository-button'
end
def update(url)
row_index = find_repository_row_index(url)
- within_element_by_index(:mirrored_repository_row_container, row_index) do
+ within_element_by_index('mirrored-repository-row-container', row_index) do
# When a repository is first mirrored, the update process might
# already be started, so the button is already "clicked"
- click_element :update_now_button if has_element?(:update_now_button, wait: 0)
+ click_element 'update-now-button' if has_element?('update-now-button', wait: 0)
end
end
@@ -105,16 +105,16 @@ module QA
row_index = find_repository_row_index(url)
wait_until(sleep_interval: 1) do
- within_element_by_index(:mirrored_repository_row_container, row_index) do
- last_update = find_element(:mirror_last_update_at_content, wait: 0)
+ within_element_by_index('mirrored-repository-row-container', row_index) do
+ last_update = find_element('mirror-last-update-at-content', wait: 0)
last_update.has_text?('just now') || last_update.has_text?('seconds')
end
end
# Fail early if the page still shows that there has been no update
- within_element_by_index(:mirrored_repository_row_container, row_index) do
- find_element(:mirror_last_update_at_content, wait: 0).assert_no_text('Never')
- assert_no_element(:mirror_error_badge_content)
+ within_element_by_index('mirrored-repository-row-container', row_index) do
+ find_element('mirror-last-update-at-content', wait: 0).assert_no_text('Never')
+ assert_no_element('mirror-error-badge-content')
end
end
@@ -122,7 +122,7 @@ module QA
def find_repository_row_index(target_url)
wait_until(max_duration: 5, reload: false) do
- all_elements(:mirror_repository_url_content, minimum: 1).index do |url|
+ all_elements('mirror-repository-url-content', minimum: 1).index do |url|
# The url might be a sanitized url but the target_url won't be so
# we compare just the paths instead of the full url
# We also must remove any badges from the url (e.g. All Branches)
diff --git a/qa/qa/page/project/settings/pages.rb b/qa/qa/page/project/settings/pages.rb
index c5b8560ba9a..64990dc2991 100644
--- a/qa/qa/page/project/settings/pages.rb
+++ b/qa/qa/page/project/settings/pages.rb
@@ -8,11 +8,11 @@ module QA
include QA::Page::Settings::Common
view 'app/views/projects/pages/_access.html.haml' do
- element :access_page_container
+ element 'access-page-container'
end
def go_to_access_page
- within_element(:access_page_container) do
+ within_element('access-page-container') do
find('a').click
page.driver.browser.switch_to.window(page.driver.browser.window_handles.last)
end
diff --git a/qa/qa/page/project/settings/project_deploy_tokens.rb b/qa/qa/page/project/settings/project_deploy_tokens.rb
new file mode 100644
index 00000000000..61b44a5e546
--- /dev/null
+++ b/qa/qa/page/project/settings/project_deploy_tokens.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module QA
+ module Page
+ module Project
+ module Settings
+ class ProjectDeployTokens < Page::Base
+ include Page::Component::DeployToken
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/project/settings/protected_branches.rb b/qa/qa/page/project/settings/protected_branches.rb
index 8358a4ae33e..07b17faa9bd 100644
--- a/qa/qa/page/project/settings/protected_branches.rb
+++ b/qa/qa/page/project/settings/protected_branches.rb
@@ -22,7 +22,7 @@ module QA
end
view 'app/views/protected_branches/shared/_create_protected_branch.html.haml' do
- element :protect_button
+ element 'protect-button'
end
def select_branch(branch_name)
@@ -43,7 +43,7 @@ module QA
end
def protect_branch
- click_element(:protect_button, wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME)
+ click_element('protect-button', wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME)
wait_for_requests
end
diff --git a/qa/qa/page/project/settings/protected_tags.rb b/qa/qa/page/project/settings/protected_tags.rb
index 5923bc7dc78..c23ade31fb1 100644
--- a/qa/qa/page/project/settings/protected_tags.rb
+++ b/qa/qa/page/project/settings/protected_tags.rb
@@ -8,36 +8,36 @@ module QA
include Page::Component::DropdownFilter
view 'app/views/projects/protected_tags/shared/_dropdown.html.haml' do
- element :tags_dropdown
+ element 'tags-dropdown'
end
view 'app/assets/javascripts/protected_tags/protected_tag_create.js' do
- element :allowed_to_create_dropdown
+ element 'allowed-to-create-dropdown'
end
view 'app/views/projects/protected_tags/shared/_create_protected_tag.html.haml' do
- element :protect_tag_button
+ element 'protect-tag-button'
end
def set_tag(tag_name)
click_button 'Add tag'
- click_element :tags_dropdown
+ click_element 'tags-dropdown'
filter_and_select(tag_name)
end
def choose_access_level_role(role)
- return if find_element(:allowed_to_create_dropdown).text == role
+ return if find_element('allowed-to-create-dropdown').text == role
- click_element :allowed_to_create_dropdown
- within_element :allowed_to_create_dropdown do
+ click_element 'allowed-to-create-dropdown'
+ within_element 'allowed-to-create-dropdown' do
click_on role
end
# confirm selection and remove dropdown
- click_element :allowed_to_create_dropdown
+ click_element 'allowed-to-create-dropdown'
end
def click_protect_tag_button
- click_element :protect_tag_button
+ click_element 'protect-tag-button'
end
end
end
diff --git a/qa/qa/page/project/settings/repository.rb b/qa/qa/page/project/settings/repository.rb
index 4e53ea1aee9..d68784c09aa 100644
--- a/qa/qa/page/project/settings/repository.rb
+++ b/qa/qa/page/project/settings/repository.rb
@@ -8,15 +8,15 @@ module QA
include QA::Page::Settings::Common
view 'app/views/protected_branches/shared/_index.html.haml' do
- element :protected_branches_settings_content
+ element 'protected-branches-settings-content'
end
view 'app/views/projects/mirrors/_mirror_repos.html.haml' do
- element :mirroring_repositories_settings_content
+ element 'mirroring-repositories-settings-content'
end
view 'app/views/shared/deploy_tokens/_index.html.haml' do
- element :deploy_tokens_settings_content
+ element 'deploy-tokens-settings-content'
end
view 'app/views/shared/deploy_keys/_index.html.haml' do
@@ -24,16 +24,16 @@ module QA
end
view 'app/views/projects/protected_tags/shared/_index.html.haml' do
- element :protected_tag_settings_content
+ element 'protected-tag-settings-content'
end
view 'app/views/projects/branch_rules/_show.html.haml' do
- element :branch_rules_content
+ element 'branch-rules-content'
end
def expand_deploy_tokens(&block)
- expand_content(:deploy_tokens_settings_content) do
- Settings::DeployTokens.perform(&block)
+ expand_content('deploy-tokens-settings-content') do
+ Settings::ProjectDeployTokens.perform(&block)
end
end
@@ -44,33 +44,25 @@ module QA
end
def expand_protected_branches(&block)
- expand_content(:protected_branches_settings_content) do
+ expand_content('protected-branches-settings-content') do
ProtectedBranches.perform(&block)
end
end
def expand_mirroring_repositories(&block)
- expand_content(:mirroring_repositories_settings_content) do
+ expand_content('mirroring-repositories-settings-content') do
MirroringRepositories.perform(&block)
end
end
def expand_protected_tags(&block)
- expand_content(:protected_tag_settings_content) do
+ expand_content('protected-tag-settings-content') do
ProtectedTags.perform(&block)
end
end
def expand_branch_rules
- expand_content(:branch_rules_content)
- end
-
- def expand_default_branch(&block)
- within('#branch-defaults-settings') do
- find('.btn-default').click do
- DefaultBranch.perform(&block)
- end
- end
+ expand_content('branch-rules-content')
end
end
end
diff --git a/qa/qa/page/project/show.rb b/qa/qa/page/project/show.rb
index acc0f4cc293..3223b2b488d 100644
--- a/qa/qa/page/project/show.rb
+++ b/qa/qa/page/project/show.rb
@@ -149,8 +149,8 @@ module QA
end
def open_web_ide!
- click_element(:action_dropdown)
- click_element(:webide_menu_item)
+ click_element('action-dropdown')
+ click_element('webide-menu-item')
page.driver.browser.switch_to.window(page.driver.browser.window_handles.last)
end
@@ -160,8 +160,8 @@ module QA
end
def has_edit_fork_button?
- click_element(:action_dropdown)
- has_element?(:webide_menu_item, text: 'Edit fork in Web IDE')
+ click_element('action-dropdown')
+ has_element?('webide-menu-item', text: 'Edit fork in Web IDE')
end
def project_name
diff --git a/qa/qa/page/project/sub_menus/main.rb b/qa/qa/page/project/sub_menus/main.rb
index a147f28eef0..193d58a2603 100644
--- a/qa/qa/page/project/sub_menus/main.rb
+++ b/qa/qa/page/project/sub_menus/main.rb
@@ -8,7 +8,7 @@ module QA
extend QA::Page::PageConcern
def click_project
- click_element(:nav_item_link, submenu_item: 'project-overview')
+ click_element('nav-item-link', submenu_item: 'project-overview')
end
end
end
diff --git a/qa/qa/page/project/web_ide/vscode.rb b/qa/qa/page/project/web_ide/vscode.rb
index 74194b85ebe..63dc75b523f 100644
--- a/qa/qa/page/project/web_ide/vscode.rb
+++ b/qa/qa/page/project/web_ide/vscode.rb
@@ -40,6 +40,10 @@ module QA
click_element('span[aria-label="New Folder..."]')
end
+ def has_committed_and_pushed_successfully?
+ page.has_css?('.span[title="Success! Your changes have been committed."]')
+ end
+
def click_upload_menu_item
click_element('span[aria-label="Upload..."]')
end
@@ -88,6 +92,10 @@ module QA
click_element('.monaco-button[title="Create new branch"]')
end
+ def click_continue_with_existing_branch
+ page.find('.monaco-button[title="Continue"]').click
+ end
+
def has_branch_input_field?
has_element?('input[aria-label="input"]')
end
@@ -103,6 +111,32 @@ module QA
page.within_frame(iframe, &block)
end
+ def click_new_file_menu_item
+ page.find('[aria-label="New File..."]').click
+ end
+
+ def switch_to_original_window
+ page.driver.browser.switch_to.window(page.driver.browser.window_handles.first)
+ end
+
+ def create_new_file_from_template(filename, template)
+ within_vscode_editor do
+ Support::Waiter.wait_until(max_duration: 20, retry_on_exception: true) do
+ click_new_file_menu_item
+ enter_new_file_text_input(filename)
+ page.within('div.editor-container') do
+ page.find('textarea.inputarea.monaco-mouse-cursor-text').send_keys(template)
+ end
+ page.has_content?(filename)
+ end
+ end
+ end
+
+ def enter_new_file_text_input(name)
+ page.find('.explorer-item-edited', visible: true)
+ send_keys(name, :enter)
+ end
+
# Used for stablility, due to feature_caching of vscode_web_ide
def wait_for_ide_to_load
page.driver.browser.switch_to.window(page.driver.browser.window_handles.last)
@@ -113,7 +147,7 @@ module QA
end
end
- wait_for_requests
+ Support::WaitForRequests.wait_for_requests(finish_loading_wait: 30)
Support::Waiter.wait_until(max_duration: 10, reload_page: page, retry_on_exception: true) do
within_vscode_editor do
# Check for webide file_explorer element
@@ -157,6 +191,13 @@ module QA
end
end
+ def push_to_existing_branch
+ within_vscode_editor do
+ click_continue_with_existing_branch
+ has_committed_and_pushed_successfully?
+ end
+ end
+
def push_to_new_branch
within_vscode_editor do
click_new_branch
@@ -191,8 +232,9 @@ module QA
end
end
- def add_file_content(prompt_data)
+ def add_prompt_into_a_file(file_name, prompt_data)
within_vscode_editor do
+ open_file_from_explorer(file_name)
click_inside_editor_frame
within_file_editor do
send_keys(:enter, :enter, prompt_data)
diff --git a/qa/qa/page/registration/sign_up.rb b/qa/qa/page/registration/sign_up.rb
index ab3f15bb857..8e303c3d425 100644
--- a/qa/qa/page/registration/sign_up.rb
+++ b/qa/qa/page/registration/sign_up.rb
@@ -4,7 +4,7 @@ module QA
module Page
module Registration
class SignUp < Page::Base
- view 'app/views/devise/shared/_signup_box.html.haml' do
+ view 'app/views/devise/shared/_signup_box_form.html.haml' do
element 'new-user-first-name-field'
element 'new-user-last-name-field'
element 'new-user-email-field'
diff --git a/qa/qa/page/search/results.rb b/qa/qa/page/search/results.rb
index 9e56d000070..f9e5e97b96f 100644
--- a/qa/qa/page/search/results.rb
+++ b/qa/qa/page/search/results.rb
@@ -20,21 +20,26 @@ module QA
end
def switch_to_code
- switch_to_tab(:code_tab)
+ click_element(:nav_item_link, submenu_item: 'Code')
end
def switch_to_projects
switch_to_tab(:projects_tab)
end
- def has_file_in_project?(file_name, project_name)
- has_element?(:result_item_content, text: "#{project_name}: #{file_name}")
+ def has_project_in_search_result?(project_name)
+ has_element?(:result_item_content, text: project_name)
end
- def has_file_with_content?(file_name, file_text)
- within_element_by_index(:result_item_content, 0) do
- break false unless has_element?(:file_title_content, text: file_name)
+ def has_file_in_project?(file_name, project_name)
+ within_element(:result_item_content, text: project_name) do
+ has_element?(:file_title_content, text: file_name)
+ end
+ end
+ def has_file_in_project_with_content?(file_text, file_path)
+ within_element(:result_item_content,
+ text: file_path) do
has_element?(:file_text_content, text: file_text)
end
end
diff --git a/qa/qa/page/settings/common.rb b/qa/qa/page/settings/common.rb
index f63c987c3b4..2fb5f17458b 100644
--- a/qa/qa/page/settings/common.rb
+++ b/qa/qa/page/settings/common.rb
@@ -6,7 +6,7 @@ module QA
module Common
# Click the Expand button present in the specified section
#
- # @param [Symbol] element_name `element` name defined in a `view` block
+ # @param [Symbol|String] element_name `element` name defined in a `view` block
def expand_content(element_name)
within_element(element_name) do
# Because it is possible to click the button before the JS toggle code is bound
diff --git a/qa/qa/page/sub_menus/common.rb b/qa/qa/page/sub_menus/common.rb
index dc878674877..4cfd4f04cbe 100644
--- a/qa/qa/page/sub_menus/common.rb
+++ b/qa/qa/page/sub_menus/common.rb
@@ -46,7 +46,7 @@ module QA
end
within_element(:menu_section, section_name: parent_menu_name) do
- click_element(:nav_item_link, submenu_item: sub_menu)
+ click_element('nav-item-link', submenu_item: sub_menu)
end
end
end
diff --git a/qa/qa/page/sub_menus/main.rb b/qa/qa/page/sub_menus/main.rb
index c8d7d27d930..25732672dd4 100644
--- a/qa/qa/page/sub_menus/main.rb
+++ b/qa/qa/page/sub_menus/main.rb
@@ -7,11 +7,11 @@ module QA
extend QA::Page::PageConcern
def go_to_issues
- click_element(:nav_item_link, submenu_item: 'Issues')
+ click_element('nav-item-link', submenu_item: 'Issues')
end
def go_to_merge_requests
- click_element(:nav_item_link, submenu_item: 'Merge requests')
+ click_element('nav-item-link', submenu_item: 'Merge requests')
end
end
end
diff --git a/qa/qa/page/user/show.rb b/qa/qa/page/user/show.rb
index 62e08dc8c63..aba79256340 100644
--- a/qa/qa/page/user/show.rb
+++ b/qa/qa/page/user/show.rb
@@ -21,7 +21,7 @@ module QA
end
def click_following_tab
- click_element(:nav_item_link, submenu_item: 'Following')
+ click_element('nav-item-link', submenu_item: 'Following')
end
def click_user_link(username)
diff --git a/qa/qa/resource/group_base.rb b/qa/qa/resource/group_base.rb
index 263c2ca2aeb..0972bc32966 100644
--- a/qa/qa/resource/group_base.rb
+++ b/qa/qa/resource/group_base.rb
@@ -22,8 +22,14 @@ module QA
# Get group projects
#
# @return [Array<QA::Resource::Project>]
- def projects
- parse_body(api_get_from("#{api_get_path}/projects")).map do |project|
+ def projects(auto_paginate: false)
+ response = if auto_paginate
+ auto_paginated_response(request_url("#{api_get_path}/projects", per_page: '100'))
+ else
+ parse_body(api_get_from("#{api_get_path}/projects"))
+ end
+
+ response.map do |project|
Project.init do |resource|
resource.add_name_uuid = false
resource.api_client = api_client
@@ -39,8 +45,14 @@ module QA
# Get group labels
#
# @return [Array<QA::Resource::GroupLabel>]
- def labels
- parse_body(api_get_from("#{api_get_path}/labels")).map do |label|
+ def labels(auto_paginate: false)
+ response = if auto_paginate
+ auto_paginated_response(request_url("#{api_get_path}/labels", per_page: '100'))
+ else
+ parse_body(api_get_from("#{api_get_path}/labels"))
+ end
+
+ response.map do |label|
GroupLabel.init do |resource|
resource.api_client = api_client
resource.group = self
@@ -55,8 +67,14 @@ module QA
# Get group milestones
#
# @return [Array<QA::Resource::GroupMilestone>]
- def milestones
- parse_body(api_get_from("#{api_get_path}/milestones")).map do |milestone|
+ def milestones(auto_paginate: false)
+ response = if auto_paginate
+ auto_paginated_response(request_url("#{api_get_path}/milestones", per_page: '100'))
+ else
+ parse_body(api_get_from("#{api_get_path}/milestones"))
+ end
+
+ response.map do |milestone|
GroupMilestone.init do |resource|
resource.api_client = api_client
resource.group = self
@@ -71,8 +89,14 @@ module QA
# Get group badges
#
# @return [Array<QA::Resource::GroupBadge>]
- def badges
- parse_body(api_get_from("#{api_get_path}/badges")).map do |badge|
+ def badges(auto_paginate: false)
+ response = if auto_paginate
+ auto_paginated_response(request_url("#{api_get_path}/badges", per_page: '100'))
+ else
+ parse_body(api_get_from("#{api_get_path}/badges"))
+ end
+
+ response.map do |badge|
GroupBadge.init do |resource|
resource.api_client = api_client
resource.group = self
diff --git a/qa/qa/resource/group_deploy_token.rb b/qa/qa/resource/group_deploy_token.rb
index 4c9b296ece1..3a110fcbdc8 100644
--- a/qa/qa/resource/group_deploy_token.rb
+++ b/qa/qa/resource/group_deploy_token.rb
@@ -51,7 +51,7 @@ module QA
setting.expand_deploy_tokens do |page|
page.fill_token_name(name)
page.fill_token_expires_at(expires_at)
- page.fill_scopes(read_repository: true, read_package_registry: true, write_package_registry: true)
+ page.fill_scopes(@scopes)
page.add_token
end
diff --git a/qa/qa/resource/import_project.rb b/qa/qa/resource/import_project.rb
index 1709a9eb989..631c23bc53c 100644
--- a/qa/qa/resource/import_project.rb
+++ b/qa/qa/resource/import_project.rb
@@ -3,15 +3,16 @@
module QA
module Resource
class ImportProject < Resource::Project
- attr_writer :file_path
+ attr_accessor :file_path, :overwrite
def initialize
@name = "ImportedProject-#{SecureRandom.hex(8)}"
@file_path = Runtime::Path.fixture('export.tar.gz')
+ @import = true
+ @overwrite = false
end
def fabricate!
- self.import = true
super
group.visit!
@@ -27,8 +28,29 @@ module QA
end
end
- def fabricate_via_api!
- raise NotImplementedError
+ def api_post_path
+ "/projects/import"
+ end
+
+ def api_post_body
+ {
+ file: ::File.new(file_path),
+ path: name,
+ namespace: personal_namespace || group.full_path,
+ overwrite: overwrite
+ }
+ end
+
+ private
+
+ def transform_api_resource(api_resource)
+ api_resource
+ end
+
+ def resource_web_url(resource)
+ super
+ rescue ResourceURLMissingError
+ # this particular resource does not expose a web_url property
end
end
end
diff --git a/qa/qa/resource/issue.rb b/qa/qa/resource/issue.rb
index 72b57801053..5f9e14fb2e5 100644
--- a/qa/qa/resource/issue.rb
+++ b/qa/qa/resource/issue.rb
@@ -24,6 +24,10 @@ module QA
false
end
+ attribute :issue_type do
+ 'issue'
+ end
+
def initialize
@assignee_ids = []
@labels = []
@@ -62,7 +66,8 @@ module QA
assignee_ids: assignee_ids,
labels: labels,
title: title,
- confidential: confidential
+ confidential: confidential,
+ issue_type: issue_type
}.tap do |hash|
hash[:milestone_id] = @milestone.id if @milestone
hash[:weight] = @weight if @weight
diff --git a/qa/qa/resource/personal_access_token.rb b/qa/qa/resource/personal_access_token.rb
index 9152e28e515..b1d38b2b8d0 100644
--- a/qa/qa/resource/personal_access_token.rb
+++ b/qa/qa/resource/personal_access_token.rb
@@ -11,8 +11,11 @@ module QA
# This *could* be different than the api_client.user or the api_user provided by the QA::Resource::ApiFabricator
attr_writer :user
- attribute :id
- attribute :token
+ attributes :id, :token
+
+ attribute :expires_at do
+ Time.now.utc.to_date + 2
+ end
# Only Admins can create PAT via the API.
# If Runtime::Env.admin_personal_access_token is provided, fabricate via the API,
@@ -49,7 +52,7 @@ module QA
api_client = Runtime::API::Client.new(:gitlab,
is_new_session: false,
user: user,
- personal_access_token: self.token)
+ personal_access_token: token)
request_url = Runtime::API::Request.new(api_client,
"/personal_access_tokens?user_id=#{user.id}",
per_page: '100').url
@@ -88,12 +91,7 @@ module QA
end
def cache_token
- QA::Resource::PersonalAccessTokenCache.set_token_for_username(user.username, self.token) if @user && self.token
- end
-
- # Expire in 2 days just in case the token is created just before midnight
- def expires_at
- @expires_at || Time.now.utc.to_date + 2
+ QA::Resource::PersonalAccessTokenCache.set_token_for_username(user.username, token) if @user && token
end
def fabricate!
@@ -115,7 +113,7 @@ module QA
cache_token
- self.token
+ token
end
end
end
diff --git a/qa/qa/resource/project.rb b/qa/qa/resource/project.rb
index 25fbd01f879..14ee09541f4 100644
--- a/qa/qa/resource/project.rb
+++ b/qa/qa/resource/project.rb
@@ -14,7 +14,8 @@ module QA
:github_personal_access_token,
:github_repository_path,
:gitlab_repository_path,
- :personal_namespace
+ :personal_namespace,
+ :import_wait_duration
attr_reader :repository_storage
@@ -65,6 +66,7 @@ module QA
@template_name = nil
@personal_namespace = nil
@import = false
+ @import_wait_duration = 60
self.name = "the_awesome_project"
end
@@ -123,13 +125,13 @@ module QA
resource_web_url(api_get)
rescue ResourceNotFoundError
response = super
- return response unless template_name || import
+ return response unless @template_name || import
# If a project is being imported, wait until it completes before we let the test continue.
# Otherwise we see Git repository errors
# See https://gitlab.com/gitlab-org/gitlab/-/issues/356101
Support::Retrier.retry_until(
- max_duration: 60,
+ max_duration: import_wait_duration,
sleep_interval: 5,
retry_on_exception: true,
message: "Wait for project to be imported"
diff --git a/qa/qa/runtime/canary.rb b/qa/qa/runtime/canary.rb
new file mode 100644
index 00000000000..9a6b8c0dc8d
--- /dev/null
+++ b/qa/qa/runtime/canary.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module QA
+ module Runtime
+ module Canary
+ CanaryValidationError = Class.new(StandardError)
+
+ def validate_canary!
+ return unless QA::Runtime::Env.qa_cookies.to_s.include?("gitlab_canary=true")
+
+ canary_cookie = Capybara.current_session.driver.browser.manage.all_cookies.find do |cookie|
+ cookie[:name] == 'gitlab_canary'
+ end
+
+ unless canary_cookie && canary_cookie[:value] == 'true'
+ raise Canary::CanaryValidationError,
+ "gitlab_canary=true cookie was expected but not set in browser. QA_COOKIES: #{QA::Runtime::Env.qa_cookies}"
+ end
+
+ return if Page::Main::Menu.perform(&:canary?)
+
+ raise Canary::CanaryValidationError,
+ "gitlab_canary=true cookie was set in browser but 'Next' badge was not shown on UI"
+ end
+ end
+ end
+end
diff --git a/qa/qa/runtime/path.rb b/qa/qa/runtime/path.rb
index ae1b26ca84a..d122240225c 100644
--- a/qa/qa/runtime/path.rb
+++ b/qa/qa/runtime/path.rb
@@ -15,6 +15,10 @@ module QA
def fixture(*args)
::File.join(fixtures_path, *args)
end
+
+ def qa_tmp(*args)
+ ::File.join([qa_root, 'tmp', *args].compact)
+ end
end
end
end
diff --git a/qa/qa/service/docker_run/base.rb b/qa/qa/service/docker_run/base.rb
index 3bd7912958f..bcddfc4b3a9 100644
--- a/qa/qa/service/docker_run/base.rb
+++ b/qa/qa/service/docker_run/base.rb
@@ -120,6 +120,14 @@ module QA
# If the host could not be resolved, fallback on localhost
'127.0.0.1'
end
+
+ # Copy files to/from the Docker container and the host
+ #
+ # @param from the source path to copy files from
+ # @param to the destination path to copy files to
+ def copy(from:, to:)
+ shell("docker cp #{from} #{to}")
+ end
end
end
end
diff --git a/qa/qa/service/docker_run/gitlab.rb b/qa/qa/service/docker_run/gitlab.rb
index ce8ab17f2b5..c39f4c22865 100644
--- a/qa/qa/service/docker_run/gitlab.rb
+++ b/qa/qa/service/docker_run/gitlab.rb
@@ -32,6 +32,11 @@ module QA
CMD
end
+ # Copy logs for GitLab services from the Docker container to the test framework's tmp folder
+ def extract_service_logs
+ copy(from: "#{@name}:/var/log/gitlab", to: Runtime::Path.qa_tmp(@name))
+ end
+
private
def release_variables_available?
diff --git a/qa/qa/specs/features/api/10_govern/group_access_token_spec.rb b/qa/qa/specs/features/api/10_govern/group_access_token_spec.rb
index 40c8adc8822..a3b14566153 100644
--- a/qa/qa/specs/features/api/10_govern/group_access_token_spec.rb
+++ b/qa/qa/specs/features/api/10_govern/group_access_token_spec.rb
@@ -2,7 +2,7 @@
module QA
RSpec.describe 'Govern' do
- describe 'Group access token', product_group: :authentication_and_authorization do
+ describe 'Group access token', product_group: :authentication do
let(:group_access_token) { create(:group_access_token) }
let(:api_client) { Runtime::API::Client.new(:gitlab, personal_access_token: group_access_token.token) }
let(:project) do
diff --git a/qa/qa/specs/features/api/10_govern/project_access_token_spec.rb b/qa/qa/specs/features/api/10_govern/project_access_token_spec.rb
index 686eb5968c7..cb7e4849d0f 100644
--- a/qa/qa/specs/features/api/10_govern/project_access_token_spec.rb
+++ b/qa/qa/specs/features/api/10_govern/project_access_token_spec.rb
@@ -2,7 +2,7 @@
module QA
RSpec.describe 'Govern' do
- describe 'Project access token', product_group: :authentication_and_authorization do
+ describe 'Project access token', product_group: :authentication do
let!(:project) { create(:project, name: "project-to-test-project-access-token-#{SecureRandom.hex(4)}") }
let!(:project_access_token) { create(:project_access_token, project: project) }
let!(:user_api_client) { Runtime::API::Client.new(:gitlab, personal_access_token: project_access_token.token) }
diff --git a/qa/qa/specs/features/api/1_manage/import/import_github_repo_spec.rb b/qa/qa/specs/features/api/1_manage/import/import_github_repo_spec.rb
index 1c335231515..9d0d81bdd91 100644
--- a/qa/qa/specs/features/api/1_manage/import/import_github_repo_spec.rb
+++ b/qa/qa/specs/features/api/1_manage/import/import_github_repo_spec.rb
@@ -136,11 +136,11 @@ module QA
def verify_merge_requests_import
merge_requests = imported_project.merge_requests
- merge_request = Resource::MergeRequest.init do |mr|
- mr.project = imported_project
- mr.iid = merge_requests.first[:iid]
- mr.api_client = user_api_client
- end.reload!
+ merge_request = build(:merge_request,
+ project: imported_project,
+ iid: merge_requests.first[:iid],
+ api_client: user_api_client).reload!
+
comments, events = fetch_events_and_comments(merge_request)
expect(merge_requests.length).to eq(1)
diff --git a/qa/qa/specs/features/api/1_manage/import/import_large_github_repo_spec.rb b/qa/qa/specs/features/api/1_manage/import/import_large_github_repo_spec.rb
index 02b3d4cf32b..9c0720aa185 100644
--- a/qa/qa/specs/features/api/1_manage/import/import_large_github_repo_spec.rb
+++ b/qa/qa/specs/features/api/1_manage/import/import_large_github_repo_spec.rb
@@ -3,6 +3,12 @@
require "etc"
# Lifesize project import test executed from https://gitlab.com/gitlab-org/manage/import/import-metrics
+#
+# This test is executed using different size live projects on GitHub.
+# Due to projects being active, there can be a lag between when test is fetching data from GitHub and
+# when importer is fetching data. It can create extra objects in imported project compared to test expectation.
+# Because of this, all expectation check for inclusion rather than exact match to avoid failures if extra issues,
+# comments, events got created while import was running.
# rubocop:disable Rails/Pluck
module QA
@@ -12,6 +18,21 @@ module QA
tags: { import_type: ENV["QA_IMPORT_TYPE"], import_repo: ENV["QA_LARGE_IMPORT_REPO"] || "rspec/rspec-core" }
} do
describe 'Project import', product_group: :import_and_integrate do # rubocop:disable RSpec/MultipleMemoizedHelpers
+ let!(:api_client) { Runtime::API::Client.as_admin }
+ let!(:user) { create(:user, api_client: api_client) }
+ let!(:user_api_client) do
+ Runtime::API::Client.new(
+ user: user,
+ is_new_session: false,
+ personal_access_token: Resource::PersonalAccessToken.fabricate_via_api! do |pat|
+ pat.user = user
+ # importing very large project can take multiple days
+ # token must not expire while we still poll for import result
+ pat.expires_at = (Time.now.to_date + 5)
+ end.token
+ )
+ end
+
# Full object comparison is a fairly heavy operation
# Importer itself returns counts of objects it fetched and counts it imported
# We can use that for a lightweight comparison for very large projects
@@ -21,7 +42,6 @@ module QA
let(:api_parallel_threads) { ENV['QA_LARGE_IMPORT_API_PARALLEL']&.to_i || Etc.nprocessors }
let(:logger) { Runtime::Logger.logger }
- let(:differ) { RSpec::Support::Differ.new(color: true) }
let(:gitlab_address) { QA::Runtime::Scenario.gitlab_address.chomp("/") }
let(:dummy_url) { "https://example.com" } # this is used to replace all dynamic urls in descriptions and comments
let(:api_request_params) { { auto_paginate: true, attempts: 2 } }
@@ -98,16 +118,17 @@ module QA
]
end
- let(:api_client) { Runtime::API::Client.as_admin }
-
- let(:user) { create(:user, api_client: api_client) }
-
let(:github_client) do
Octokit::Client.new(
access_token: ENV['QA_LARGE_IMPORT_GH_TOKEN'] || Runtime::Env.github_access_token,
per_page: 100,
middleware: Faraday::RackBuilder.new do |builder|
- builder.use(Faraday::Retry::Middleware, exceptions: [Octokit::InternalServerError, Octokit::ServerError])
+ builder.use(Faraday::Retry::Middleware,
+ max: 3,
+ interval: 1,
+ retry_block: ->(exception:, **) { logger.warn("Request to GitHub failed: '#{exception}', retrying") },
+ exceptions: [Octokit::InternalServerError, Octokit::ServerError]
+ )
builder.use(Faraday::Response::RaiseError) # faraday retry swallows errors, so it needs to be re-raised
end
)
@@ -145,52 +166,33 @@ module QA
end
let(:gh_issues) do
- issues = gh_all_issues.reject(&:pull_request).each_with_object({}) do |issue, hash|
+ gh_all_issues.reject(&:pull_request).each_with_object({}) do |issue, hash|
id = issue.number
+ logger.debug("- Fetching comments and events for issue #{id} -")
hash[id] = {
url: issue.html_url,
title: issue.title,
body: issue.body || '',
- comments: gh_issue_comments[id]
+ comments: fetch_issuable_comments(id, "issue"),
+ events: fetch_issuable_events(id)
}
end
-
- fetch_github_events(issues, "issue")
end
let(:gh_prs) do
- prs = gh_all_issues.select(&:pull_request).each_with_object({}) do |pr, hash|
+ gh_all_issues.select(&:pull_request).each_with_object({}) do |pr, hash|
id = pr.number
+ logger.debug("- Fetching comments and events for pr #{id} -")
hash[id] = {
url: pr.html_url,
title: pr.title,
body: pr.body || '',
- comments: [*gh_pr_comments[id], *gh_issue_comments[id]].compact
+ comments: fetch_issuable_comments(id, "pr"),
+ events: fetch_issuable_events(id)
}
end
-
- fetch_github_events(prs, "pr")
- end
-
- # rubocop:disable Layout/LineLength
- let(:gh_issue_comments) do
- logger.info("- Fetching issue comments -")
- with_paginated_request { github_client.issues_comments(github_repo) }.each_with_object(Hash.new { |h, k| h[k] = [] }) do |c, hash|
- hash[id_from_url(c.html_url)] << c.body&.gsub(gh_link_pattern, dummy_url)
- end
end
- let(:gh_pr_comments) do
- logger.info("- Fetching pr comments -")
- with_paginated_request { github_client.pull_requests_comments(github_repo) }.each_with_object(Hash.new { |h, k| h[k] = [] }) do |c, hash|
- hash[id_from_url(c.html_url)] << c.body
- # some suggestions can contain extra whitespaces which gitlab will remove
- &.gsub(/suggestion\s+\r/, "suggestion\r")
- &.gsub(gh_link_pattern, dummy_url)
- end
- end
- # rubocop:enable Layout/LineLength
-
let(:imported_project) do
Resource::ProjectImportedFromGithub.fabricate_via_api! do |project|
project.add_name_uuid = false
@@ -253,7 +255,6 @@ module QA
}
},
target: {
- project_name: imported_project.path_with_namespace,
data: {
branches: gl_branches.length,
commits: gl_commits.length,
@@ -267,7 +268,7 @@ module QA
issue_events: gl_issues.sum { |_k, v| v[:events].length }
}
},
- not_imported: {
+ diff: {
mrs: @mr_diff,
issues: @issue_diff
}
@@ -291,7 +292,7 @@ module QA
fetch_github_objects unless only_stats_comparison
import_status = -> {
- imported_project.project_import_status.yield_self do |status|
+ imported_project.project_import_status.then do |status|
@stats = status[:stats]&.slice(:fetched, :imported)
# fail fast if import explicitly failed
@@ -333,7 +334,8 @@ module QA
},
target: {
name: "GitLab",
- address: gitlab_address
+ address: gitlab_address,
+ project_name: imported_project.full_path
}
}.deep_merge(additional_data)
end
@@ -359,10 +361,8 @@ module QA
def verify_repository_import
logger.info("== Verifying repository import ==")
expect(imported_project.description).to eq(gh_repo.description)
- # check via include, importer creates more branches
- # https://gitlab.com/gitlab-org/gitlab/-/issues/332711
expect(gl_branches).to include(*gh_branches)
- expect(gl_commits).to match_array(gh_commits)
+ expect(gl_commits).to include(*gh_commits)
end
# Verify imported labels
@@ -370,7 +370,6 @@ module QA
# @return [void]
def verify_labels_import
logger.info("== Verifying label import ==")
- # check via include, additional labels can be inherited from parent group
expect(gl_labels).to include(*gh_labels)
end
@@ -379,7 +378,7 @@ module QA
# @return [void]
def verify_milestones_import
logger.info("== Verifying milestones import ==")
- expect(gl_milestones).to match_array(gh_milestones)
+ expect(gl_milestones).to include(*gh_milestones)
end
# Verify imported merge requests and mr issues
@@ -402,42 +401,59 @@ module QA
#
private
- # Fetch github events and add to issue object
+ # Fetch issuable object comments
#
- # @param [Hash] issuables
+ # @param [Integer] id
# @param [String] type
- # @return [Hash]
- def fetch_github_events(issuables, type)
- logger.info("- Fetching #{type} events -")
- issuables.to_h do |id, issuable|
- logger.debug("Fetching events for #{type} !#{id}")
- events = with_paginated_request { github_client.issue_events(github_repo, id) }
- .map { |event| event[:event] }
- .reject { |event| unsupported_events.include?(event) }
-
- [id, issuable.merge({ events: events })]
- end
+ # @return [Array]
+ def fetch_issuable_comments(id, type)
+ pr = type == "pr"
+ comments = []
+ # every pr is also an issue, so when fetching pr comments, issue endpoint has to be used as well
+ comments.push(*with_paginated_request { github_client.issue_comments(github_repo, id) })
+ comments.push(*with_paginated_request { github_client.pull_request_comments(github_repo, id) }) if pr
+ comments.map! { |comment| comment.body&.gsub(gh_link_pattern, dummy_url) }
+ return comments unless pr
+
+ # some suggestions can contain extra whitespaces which gitlab will remove
+ comments.map { |comment| comment.gsub(/suggestion\s+\r/, "suggestion\r") }
+ end
+
+ # Fetch issuable object events
+ #
+ # @param [Integer] id
+ # @return [Array]
+ def fetch_issuable_events(id)
+ with_paginated_request { github_client.issue_events(github_repo, id) }
+ .map { |event| event[:event] }
+ .reject { |event| unsupported_events.include?(event) }
end
- # Verify imported mrs or issues and return missing items
+ # Verify imported mrs or issues and return content diff
#
# @param [String] type verification object, 'mrs' or 'issues'
# @return [Hash]
def verify_mrs_or_issues(type)
# Compare length to have easy to read overview how many objects are missing
#
- expected = type == 'mr' ? mrs : gl_issues
- actual = type == 'mr' ? gh_prs : gh_issues
- count_msg = "Expected to contain same amount of #{type}s. Gitlab: #{expected.length}, Github: #{actual.length}"
- expect(expected.length).to eq(actual.length), count_msg
+ expected = type == 'mr' ? gh_prs : gh_issues
+ actual = type == 'mr' ? mrs : gl_issues
- missing_objects = (actual.keys - expected.keys).map { |it| actual[it].slice(:title, :url) }
- missing_content = verify_comments_and_events(type, actual, expected)
+ missing_objects = (expected.keys - actual.keys).map { |it| expected[it].slice(:title, :url) }
+ extra_objects = (actual.keys - expected.keys).map { |it| actual[it].slice(:title, :url) }
+ count_msg = <<~MSG
+ Expected to contain all of GitHub's #{type}s. Gitlab: #{actual.length}, Github: #{expected.length}.
+ Missing: #{missing_objects.map { |it| it[:url] }}
+ MSG
+ expect(expected.length <= actual.length).to be_truthy, count_msg
+
+ content_diff = verify_comments_and_events(type, actual, expected)
{
- "#{type}s": missing_objects.empty? ? nil : missing_objects,
- "#{type}_content": missing_content.empty? ? nil : missing_content
- }.compact
+ "extra_#{type}s": extra_objects,
+ "missing_#{type}s": missing_objects,
+ "#{type}_content_diff": content_diff
+ }.compact_blank
end
# Verify imported comments and events
@@ -447,7 +463,7 @@ module QA
# @param [Hash] expected
# @return [Hash]
def verify_comments_and_events(type, actual, expected)
- actual.each_with_object([]) do |(key, actual_item), missing_content|
+ actual.each_with_object([]) do |(key, actual_item), content_diff|
expected_item = expected[key]
title = actual_item[:title]
msg = "expected #{type} with iid '#{key}' to have"
@@ -461,42 +477,43 @@ module QA
#
expected_body = expected_item[:body]
actual_body = actual_item[:body]
- body_msg = <<~MSG
- #{msg} same description. diff:\n#{differ.diff(expected_body, actual_body)}
- MSG
+ body_msg = "#{msg} same description"
expect(expected_body).to eq(actual_body), body_msg
# Print amount difference first
#
expected_comments = expected_item[:comments]
actual_comments = actual_item[:comments]
- comment_count_msg = <<~MSG
- #{msg} same amount of comments. Gitlab: #{expected_comments.length}, Github: #{actual_comments.length}
+ comment_count_msg = <<~MSG.strip
+ #{msg} same comments. GitHub: #{expected_comments.length}, GitLab: #{actual_comments.length}
MSG
- expect(expected_comments.length).to eq(actual_comments.length), comment_count_msg
- expect(expected_comments).to match_array(actual_comments)
+ expect(actual_comments).to include(*expected_comments), comment_count_msg
expected_events = expected_item[:events]
actual_events = actual_item[:events]
- event_count_msg = <<~MSG
- #{msg} same amount of events. Gitlab: #{expected_events.length}, Github: #{actual_events.length}
+ event_count_msg = <<~MSG.strip
+ #{msg} same events. GitHub: #{expected_events.length}, GitLab: #{actual_events.length}.
+ Missing event: #{expected_events - actual_events}
MSG
- expect(expected_events.length).to eq(actual_events.length), event_count_msg
- expect(expected_events).to match_array(actual_events)
+ expect(actual_events).to include(*expected_events), event_count_msg
- # Save missing comments and events
+ # Save comment and event diff
#
- comment_diff = actual_comments - expected_comments
- event_diff = actual_events - expected_events
- next if comment_diff.empty? && event_diff.empty?
+ missing_comments = expected_comments - actual_comments
+ extra_comments = actual_comments - expected_comments
+ missing_events = expected_events - actual_events
+ extra_events = actual_events - expected_events
+ next if [missing_comments, missing_events, extra_comments, extra_events].all?(&:empty?)
- missing_content << {
+ content_diff << {
title: title,
- github_url: actual_item[:url],
- gitlab_url: expected_item[:url],
- missing_comments: comment_diff.empty? ? nil : comment_diff,
- missing_events: event_diff.empty? ? nil : event_diff
- }.compact
+ github_url: expected_item[:url],
+ gitlab_url: actual_item[:url],
+ missing_comments: missing_comments,
+ extra_comments: extra_comments,
+ missing_events: missing_events,
+ extra_events: extra_events
+ }.compact_blank
end
end
@@ -550,11 +567,7 @@ module QA
logger.debug("- Fetching merge request comments #{api_parallel_threads} parallel threads -")
Parallel.map(imported_mrs, in_threads: api_parallel_threads) do |mr|
- resource = Resource::MergeRequest.init do |resource|
- resource.project = imported_project
- resource.iid = mr[:iid]
- resource.api_client = api_client
- end
+ resource = build(:merge_request, project: imported_project, iid: mr[:iid], api_client: api_client)
comments = resource.comments(**api_request_params)
label_events = resource.label_events(**api_request_params)
@@ -661,16 +674,6 @@ module QA
File.open("tmp/github-import-data.json", "w") { |file| file.write(JSON.pretty_generate(json)) }
end
- # Extract id number from web url of issue or pull request
- #
- # Some endpoints don't return object id as separate parameter so web url can be used as a workaround
- #
- # @param [String] url
- # @return [Integer]
- def id_from_url(url)
- url.match(%r{(?<type>issues|pull)/(?<id>\d+)})&.named_captures&.fetch("id", nil).to_i
- end
-
# Custom pagination for github requests
#
# Default autopagination doesn't work correctly with rate limit
diff --git a/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_issue_spec.rb b/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_issue_spec.rb
index ddf0c39e4c4..04f3bf1cb03 100644
--- a/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_issue_spec.rb
+++ b/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_issue_spec.rb
@@ -74,10 +74,7 @@ module QA
end
let(:imported_design) do
- Resource::Design.init do |design|
- design.api_client = api_client
- design.issue = imported_issue.reload!
- end.reload!
+ build(:design, api_client: api_client, issue: imported_issue.reload!).reload!
end
it(
diff --git a/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_large_project_spec.rb b/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_large_project_spec.rb
index 5e453043ead..6ea2047fed7 100644
--- a/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_large_project_spec.rb
+++ b/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_large_project_spec.rb
@@ -34,23 +34,36 @@ module QA
let!(:source_api_client) { source_admin_api_client }
let!(:source_group) do
- Resource::Sandbox.fabricate_via_api! do |group|
- group.api_client = source_api_client
- group.path = gitlab_source_group
- end
+ paths = gitlab_source_group.split("/")
+ sandbox = build(:sandbox, api_client: source_api_client, path: paths.first).reload!
+ next sandbox if paths.size == 1
+
+ paths[1..].each_with_object([sandbox]) do |path, arr|
+ arr << build(:group, api_client: source_api_client, sandbox: arr.last, path: path).reload!
+ end.last
end
# generate unique target group because source group has a static name
let!(:target_sandbox) do
- Resource::Sandbox.fabricate_via_api! do |group|
- group.api_client = admin_api_client
- group.path = "qa-sandbox-#{SecureRandom.hex(4)}"
- end
+ create(:sandbox, api_client: admin_api_client, path: "qa-sandbox-#{SecureRandom.hex(4)}")
+ end
+
+ let!(:api_client) do
+ Runtime::API::Client.new(
+ user: user,
+ is_new_session: false,
+ personal_access_token: Resource::PersonalAccessToken.fabricate_via_api! do |pat|
+ pat.user = user
+ # importing very large project can take multiple days
+ # token must not expire while we still poll for import result
+ pat.expires_at = (Time.now.to_date + 5)
+ end.token
+ )
end
# Source objects
#
- let(:source_project) { source_group.projects.find { |project| project.name == gitlab_source_project }.reload! }
+ let(:source_project) { source_group.projects(auto_paginate: true).find { |project| project.name == gitlab_source_project }.reload! }
let(:source_branches) { source_project.repository_branches(auto_paginate: true).map { |b| b[:name] } }
let(:source_commits) { source_project.commits(auto_paginate: true).map { |c| c[:id] } }
let(:source_labels) { source_project.labels(auto_paginate: true).map { |l| l.except(:id) } }
@@ -72,7 +85,7 @@ module QA
# Imported objects
#
- let(:imported_project) { imported_group.projects.find { |project| project.name == gitlab_source_project }.reload! }
+ let(:imported_project) { imported_group.projects(auto_paginate: true).find { |project| project.name == gitlab_source_project }.reload! }
let(:branches) { imported_project.repository_branches(auto_paginate: true).map { |b| b[:name] } }
let(:commits) { imported_project.commits(auto_paginate: true).map { |c| c[:id] } }
let(:labels) { imported_project.labels(auto_paginate: true).map { |l| l.except(:id) } }
@@ -343,11 +356,7 @@ module QA
imported_mrs = project.merge_requests(auto_paginate: true, attempts: 2)
Parallel.map(imported_mrs, in_threads: api_parallel_threads) do |mr|
- resource = Resource::MergeRequest.init do |resource|
- resource.project = project
- resource.iid = mr[:iid]
- resource.api_client = client
- end
+ resource = build(:merge_request, project: project, iid: mr[:iid], api_client: client)
[mr[:iid], {
url: mr[:web_url],
diff --git a/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_mr_spec.rb b/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_mr_spec.rb
index 6469e7ab92b..a32da1f5880 100644
--- a/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_mr_spec.rb
+++ b/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_mr_spec.rb
@@ -37,11 +37,10 @@ module QA
let(:imported_mrs) { imported_project.merge_requests }
let(:imported_mr) do
- Resource::MergeRequest.init do |mr|
- mr.project = imported_project
- mr.iid = imported_project.merge_requests.first[:iid]
- mr.api_client = api_client
- end
+ build(:merge_request,
+ project: imported_project,
+ iid: imported_project.merge_requests.first[:iid],
+ api_client: api_client)
end
let(:imported_mr_comments) do
diff --git a/qa/qa/specs/features/api/3_create/repository/default_branch_name_setting_spec.rb b/qa/qa/specs/features/api/3_create/repository/default_branch_name_setting_spec.rb
index a512bb76560..3a6874cd587 100644
--- a/qa/qa/specs/features/api/3_create/repository/default_branch_name_setting_spec.rb
+++ b/qa/qa/specs/features/api/3_create/repository/default_branch_name_setting_spec.rb
@@ -41,11 +41,7 @@ module QA
repository.push_changes('trunk')
end
- project = Resource::Project.fabricate_via_api! do |project|
- project.add_name_uuid = false
- project.name = project_name
- project.group = group
- end
+ project = create(:project, add_name_uuid: false, name: project_name, group: group)
expect(project.default_branch).to eq('trunk')
expect(project).to have_file('README.md')
diff --git a/qa/qa/specs/features/api/4_verify/api_variable_inheritance_with_forward_pipeline_variables_spec.rb b/qa/qa/specs/features/api/4_verify/api_variable_inheritance_with_forward_pipeline_variables_spec.rb
index 0042daaaf3f..64272ce2951 100644
--- a/qa/qa/specs/features/api/4_verify/api_variable_inheritance_with_forward_pipeline_variables_spec.rb
+++ b/qa/qa/specs/features/api/4_verify/api_variable_inheritance_with_forward_pipeline_variables_spec.rb
@@ -42,6 +42,7 @@ module QA
def upstream_ci_file
{
+ action: 'create',
file_path: '.gitlab-ci.yml',
content: <<~YAML
child1_trigger:
diff --git a/qa/qa/specs/features/api/4_verify/file_variable_downstream_pipeline_spec.rb b/qa/qa/specs/features/api/4_verify/file_variable_downstream_pipeline_spec.rb
index c95a1f9fcd0..f0707420b3b 100644
--- a/qa/qa/specs/features/api/4_verify/file_variable_downstream_pipeline_spec.rb
+++ b/qa/qa/specs/features/api/4_verify/file_variable_downstream_pipeline_spec.rb
@@ -170,12 +170,7 @@ module QA
'TEST_PROJECT_FILE' => "hello, this is test\n",
'DOCKER_CA_CERT' => "This is secret\n"
}.each do |file_name, content|
- Resource::CiVariable.fabricate_via_api! do |ci_variable|
- ci_variable.project = upstream_project
- ci_variable.key = file_name
- ci_variable.value = content
- ci_variable.variable_type = 'file'
- end
+ create(:ci_variable, project: upstream_project, key: file_name, value: content, variable_type: 'file')
end
end
diff --git a/qa/qa/specs/features/api/4_verify/file_variable_spec.rb b/qa/qa/specs/features/api/4_verify/file_variable_spec.rb
index 4dcff9d270e..f0f7bb3f500 100644
--- a/qa/qa/specs/features/api/4_verify/file_variable_spec.rb
+++ b/qa/qa/specs/features/api/4_verify/file_variable_spec.rb
@@ -66,7 +66,7 @@ module QA
end
it(
- 'does not expose file variable content with echo',
+ 'does not expose file variable content with echo', :reliable,
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/370791'
) do
job = create(:job, project: project, id: project.job_by_name('job_echo')[:id])
@@ -81,7 +81,7 @@ module QA
end
it(
- 'can read file variable content with cat',
+ 'can read file variable content with cat', :reliable,
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/386409'
) do
job = job = create(:job, project: project, id: project.job_by_name('job_cat')[:id])
@@ -96,12 +96,7 @@ module QA
private
def add_file_variable_to_project(key, value)
- Resource::CiVariable.fabricate_via_api! do |ci_variable|
- ci_variable.project = project
- ci_variable.key = key
- ci_variable.value = value
- ci_variable.variable_type = 'file'
- end
+ create(:ci_variable, project: project, key: key, value: value, variable_type: 'file')
end
def trigger_pipeline
diff --git a/qa/qa/specs/features/api/4_verify/job_downloads_artifacts_spec.rb b/qa/qa/specs/features/api/4_verify/job_downloads_artifacts_spec.rb
index d666bcea6e9..ecb7d94d56e 100644
--- a/qa/qa/specs/features/api/4_verify/job_downloads_artifacts_spec.rb
+++ b/qa/qa/specs/features/api/4_verify/job_downloads_artifacts_spec.rb
@@ -9,39 +9,36 @@ module QA
let!(:runner) { create(:project_runner, project: project, name: executor, tags: [executor]) }
let!(:add_ci_file) do
- Resource::Repository::Commit.fabricate_via_api! do |commit|
- commit.project = project
- commit.commit_message = 'Add CI file for job artifacts test'
- commit.add_files(
- [
- file_path: '.gitlab-ci.yml',
- content: <<~YAML
- default:
- tags: ["#{executor}"]
+ create(:commit, project: project, commit_message: 'Add CI file for job artifacts test', actions: [
+ {
+ action: 'create',
+ file_path: '.gitlab-ci.yml',
+ content: <<~YAML
+ default:
+ tags: ["#{executor}"]
- stages:
- - build
- - test
+ stages:
+ - build
+ - test
- job_creates_artifacts:
- stage: build
- script: mkdir tmp; echo #{random_test_string} > tmp/output.xml
- artifacts:
- paths:
- - tmp
+ job_creates_artifacts:
+ stage: build
+ script: mkdir tmp; echo #{random_test_string} > tmp/output.xml
+ artifacts:
+ paths:
+ - tmp
- job_with_default_settings:
- stage: test
- script: cat $CI_PROJECT_DIR/tmp/output.xml
+ job_with_default_settings:
+ stage: test
+ script: cat $CI_PROJECT_DIR/tmp/output.xml
- job_with_empty_dependencies:
- stage: test
- dependencies: []
- script: cat $CI_PROJECT_DIR/tmp/output.xml
- YAML
- ]
- )
- end
+ job_with_empty_dependencies:
+ stage: test
+ dependencies: []
+ script: cat $CI_PROJECT_DIR/tmp/output.xml
+ YAML
+ }
+ ])
end
let(:job_with_default_settings) do
@@ -68,7 +65,7 @@ module QA
runner.remove_via_api!
end
- it 'are not downloaded when dependencies array is set to empty',
+ it 'are not downloaded when dependencies array is set to empty', :reliable,
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/424958' do
# If this job fails, the 'failed' status of pipeline is no longer helpful
# We should exit the test case here
diff --git a/qa/qa/specs/features/browser_ui/10_govern/group/group_access_token_spec.rb b/qa/qa/specs/features/browser_ui/10_govern/group/group_access_token_spec.rb
index 0c57bb7fa3f..525b22c8a7d 100644
--- a/qa/qa/specs/features/browser_ui/10_govern/group/group_access_token_spec.rb
+++ b/qa/qa/specs/features/browser_ui/10_govern/group/group_access_token_spec.rb
@@ -2,7 +2,7 @@
module QA
RSpec.describe 'Govern' do
- describe 'Group access tokens', product_group: :authentication_and_authorization do
+ describe 'Group access tokens', product_group: :authentication do
let(:group_access_token) { QA::Resource::GroupAccessToken.fabricate_via_browser_ui! }
it(
diff --git a/qa/qa/specs/features/browser_ui/10_govern/login/2fa_recovery_spec.rb b/qa/qa/specs/features/browser_ui/10_govern/login/2fa_recovery_spec.rb
index e1afdf823b8..f5defaf75a8 100644
--- a/qa/qa/specs/features/browser_ui/10_govern/login/2fa_recovery_spec.rb
+++ b/qa/qa/specs/features/browser_ui/10_govern/login/2fa_recovery_spec.rb
@@ -2,7 +2,7 @@
module QA
RSpec.describe 'Govern', :requires_admin, :skip_live_env, :reliable do
- describe '2FA', product_group: :authentication_and_authorization do
+ describe '2FA', product_group: :authentication do
let(:owner_user) { create(:user, api_client: admin_api_client) }
let(:developer_user) { create(:user, api_client: admin_api_client) }
diff --git a/qa/qa/specs/features/browser_ui/10_govern/login/2fa_ssh_recovery_spec.rb b/qa/qa/specs/features/browser_ui/10_govern/login/2fa_ssh_recovery_spec.rb
index 1570ca24c96..8c9bb170925 100644
--- a/qa/qa/specs/features/browser_ui/10_govern/login/2fa_ssh_recovery_spec.rb
+++ b/qa/qa/specs/features/browser_ui/10_govern/login/2fa_ssh_recovery_spec.rb
@@ -2,7 +2,7 @@
module QA
RSpec.describe 'Govern', :reliable, :requires_admin, :skip_live_env,
- product_group: :authentication_and_authorization do
+ product_group: :authentication do
describe '2FA' do
let!(:user) { create(:user) }
let!(:user_api_client) { Runtime::API::Client.new(:gitlab, user: user) }
diff --git a/qa/qa/specs/features/browser_ui/10_govern/login/log_in_spec.rb b/qa/qa/specs/features/browser_ui/10_govern/login/log_in_spec.rb
index ab7cbc0743c..c289852d2cc 100644
--- a/qa/qa/specs/features/browser_ui/10_govern/login/log_in_spec.rb
+++ b/qa/qa/specs/features/browser_ui/10_govern/login/log_in_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
- RSpec.describe 'Govern', :smoke, :mobile, product_group: :authentication_and_authorization do
+ RSpec.describe 'Govern', :smoke, :mobile, product_group: :authentication do
describe 'basic user login' do
it 'user logs in using basic credentials and logs out',
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347880' do
diff --git a/qa/qa/specs/features/browser_ui/10_govern/login/log_in_with_2fa_spec.rb b/qa/qa/specs/features/browser_ui/10_govern/login/log_in_with_2fa_spec.rb
index 20570300d4f..b0e8c367924 100644
--- a/qa/qa/specs/features/browser_ui/10_govern/login/log_in_with_2fa_spec.rb
+++ b/qa/qa/specs/features/browser_ui/10_govern/login/log_in_with_2fa_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
- RSpec.describe 'Govern', :requires_admin, product_group: :authentication_and_authorization do
+ RSpec.describe 'Govern', :requires_admin, product_group: :authentication do
describe '2FA' do
let(:admin_api_client) { Runtime::API::Client.as_admin }
let(:owner_api_client) { Runtime::API::Client.new(:gitlab, user: owner_user) }
diff --git a/qa/qa/specs/features/browser_ui/10_govern/login/log_into_gitlab_via_ldap_spec.rb b/qa/qa/specs/features/browser_ui/10_govern/login/log_into_gitlab_via_ldap_spec.rb
index f9b9dbb0f2b..c5cd11cd8a0 100644
--- a/qa/qa/specs/features/browser_ui/10_govern/login/log_into_gitlab_via_ldap_spec.rb
+++ b/qa/qa/specs/features/browser_ui/10_govern/login/log_into_gitlab_via_ldap_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
- RSpec.describe 'Govern', :orchestrated, :ldap_no_tls, :ldap_tls, product_group: :authentication_and_authorization do
+ RSpec.describe 'Govern', :orchestrated, :ldap_no_tls, :ldap_tls, product_group: :authentication do
describe 'LDAP login' do
it 'user logs into GitLab using LDAP credentials',
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347892' do
diff --git a/qa/qa/specs/features/browser_ui/10_govern/login/log_into_mattermost_via_gitlab_spec.rb b/qa/qa/specs/features/browser_ui/10_govern/login/log_into_mattermost_via_gitlab_spec.rb
index 276e502e19c..ea7bad5205a 100644
--- a/qa/qa/specs/features/browser_ui/10_govern/login/log_into_mattermost_via_gitlab_spec.rb
+++ b/qa/qa/specs/features/browser_ui/10_govern/login/log_into_mattermost_via_gitlab_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
- RSpec.describe 'Govern', :orchestrated, :mattermost, product_group: :authentication_and_authorization do
+ RSpec.describe 'Govern', :orchestrated, :mattermost, product_group: :authentication do
describe 'Mattermost login' do
it 'user logs into Mattermost using GitLab OAuth',
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347891' do
diff --git a/qa/qa/specs/features/browser_ui/10_govern/login/login_via_instance_wide_saml_sso_spec.rb b/qa/qa/specs/features/browser_ui/10_govern/login/login_via_instance_wide_saml_sso_spec.rb
index 5528e733852..df2ea63c650 100644
--- a/qa/qa/specs/features/browser_ui/10_govern/login/login_via_instance_wide_saml_sso_spec.rb
+++ b/qa/qa/specs/features/browser_ui/10_govern/login/login_via_instance_wide_saml_sso_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
- RSpec.describe 'Govern', :orchestrated, :instance_saml, product_group: :authentication_and_authorization do
+ RSpec.describe 'Govern', :orchestrated, :instance_saml, product_group: :authentication do
describe 'Instance wide SAML SSO' do
it(
'user logs in to gitlab with SAML SSO',
diff --git a/qa/qa/specs/features/browser_ui/10_govern/login/login_via_oauth_and_oidc_with_gitlab_as_idp_spec.rb b/qa/qa/specs/features/browser_ui/10_govern/login/login_via_oauth_and_oidc_with_gitlab_as_idp_spec.rb
index 5907f7654a0..6d5a2aef76c 100644
--- a/qa/qa/specs/features/browser_ui/10_govern/login/login_via_oauth_and_oidc_with_gitlab_as_idp_spec.rb
+++ b/qa/qa/specs/features/browser_ui/10_govern/login/login_via_oauth_and_oidc_with_gitlab_as_idp_spec.rb
@@ -2,7 +2,7 @@
module QA
RSpec.describe 'Govern', :skip_live_env, requires_admin: 'creates users and instance OAuth application',
- product_group: :authentication_and_authorization do
+ product_group: :authentication do
let!(:user) { create(:user) }
let(:consumer_host) { "http://#{consumer_name}.#{Runtime::Env.running_in_ci? ? 'test' : 'bridge'}" }
let(:instance_oauth_app) do
@@ -14,6 +14,7 @@ module QA
after do
instance_oauth_app.remove_via_api!
+ save_gitlab_logs(consumer_name)
remove_gitlab_service(consumer_name)
end
@@ -28,6 +29,11 @@ module QA
end
end
+ # Copy GitLab logs from inside the named Docker container running the GitLab OAuth instance
+ def save_gitlab_logs(name)
+ Service::DockerRun::Gitlab.new(name: name).extract_service_logs
+ end
+
def remove_gitlab_service(name)
Service::DockerRun::Gitlab.new(name: name).remove!
end
@@ -71,7 +77,10 @@ module QA
end
end
- describe 'OIDC' do
+ describe 'OIDC', quarantine: {
+ issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/429723',
+ type: :flaky
+ } do
let(:consumer_name) { 'gitlab-oidc-consumer' }
let(:redirect_uri) { "#{consumer_host}/users/auth/openid_connect/callback" }
let(:scopes) { %w[openid profile email] }
diff --git a/qa/qa/specs/features/browser_ui/10_govern/login/oauth_login_with_facebook_spec.rb b/qa/qa/specs/features/browser_ui/10_govern/login/oauth_login_with_facebook_spec.rb
index 361d87ac72c..717c5d6d935 100644
--- a/qa/qa/specs/features/browser_ui/10_govern/login/oauth_login_with_facebook_spec.rb
+++ b/qa/qa/specs/features/browser_ui/10_govern/login/oauth_login_with_facebook_spec.rb
@@ -1,10 +1,14 @@
# frozen_string_literal: true
module QA
- RSpec.describe 'Govern', :orchestrated, :oauth, product_group: :authentication_and_authorization do
+ RSpec.describe 'Govern', :orchestrated, :oauth, product_group: :authentication do
describe 'OAuth' do
it 'logs in with Facebook credentials',
- testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/417115' do
+ testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/417115',
+ quarantine: {
+ type: :waiting_on,
+ issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/431392'
+ } do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.perform(&:sign_in_with_facebook)
diff --git a/qa/qa/specs/features/browser_ui/10_govern/login/oauth_login_with_github_spec.rb b/qa/qa/specs/features/browser_ui/10_govern/login/oauth_login_with_github_spec.rb
index 872b7a7d87f..4fa3201d7fa 100644
--- a/qa/qa/specs/features/browser_ui/10_govern/login/oauth_login_with_github_spec.rb
+++ b/qa/qa/specs/features/browser_ui/10_govern/login/oauth_login_with_github_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
- RSpec.describe 'Govern', :orchestrated, :oauth, product_group: :authentication_and_authorization do
+ RSpec.describe 'Govern', :orchestrated, :oauth, product_group: :authentication do
describe 'OAuth' do
it 'connects and logs in with GitHub OAuth',
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/402405' do
diff --git a/qa/qa/specs/features/browser_ui/10_govern/login/register_spec.rb b/qa/qa/specs/features/browser_ui/10_govern/login/register_spec.rb
index 1fca9f490f7..7ff6e484b1e 100644
--- a/qa/qa/specs/features/browser_ui/10_govern/login/register_spec.rb
+++ b/qa/qa/specs/features/browser_ui/10_govern/login/register_spec.rb
@@ -15,7 +15,7 @@ module QA
end
end
- RSpec.describe 'Govern', :skip_signup_disabled, :requires_admin, product_group: :authentication_and_authorization do
+ RSpec.describe 'Govern', :skip_signup_disabled, :requires_admin, product_group: :authentication do
describe 'while LDAP is enabled', :orchestrated, :ldap_no_tls,
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347934',
quarantine: {
@@ -157,11 +157,11 @@ module QA
Flow::Login.sign_in(as: user, skip_page_validation: true)
Flow::UserOnboarding.onboard_user
-
- # In development env and .com the user is asked to create a group and a project which can be skipped for
- # the purpose of this test
+ # In development env and .com the user is asked to create a group and a project
+ Flow::UserOnboarding.create_initial_project if page.has_text?("Create or import your first project", wait: 0)
Runtime::Browser.visit(:gitlab, Page::Dashboard::Welcome)
- Page::Main::Menu.perform(&:has_personal_area?)
+
+ expect(Page::Main::Menu.perform(&:has_personal_area?)).to be_truthy
end
end
end
diff --git a/qa/qa/specs/features/browser_ui/10_govern/project/project_access_token_spec.rb b/qa/qa/specs/features/browser_ui/10_govern/project/project_access_token_spec.rb
index e61ab7cb9c6..aeae42b2a4a 100644
--- a/qa/qa/specs/features/browser_ui/10_govern/project/project_access_token_spec.rb
+++ b/qa/qa/specs/features/browser_ui/10_govern/project/project_access_token_spec.rb
@@ -2,7 +2,7 @@
module QA
RSpec.describe 'Govern' do
- describe 'Project access tokens', :reliable, product_group: :authentication_and_authorization do
+ describe 'Project access tokens', :reliable, product_group: :authentication do
let(:project_access_token) { QA::Resource::ProjectAccessToken.fabricate_via_browser_ui! }
after do
diff --git a/qa/qa/specs/features/browser_ui/10_govern/user/impersonation_token_spec.rb b/qa/qa/specs/features/browser_ui/10_govern/user/impersonation_token_spec.rb
index 5f175508bbd..142d4857d10 100644
--- a/qa/qa/specs/features/browser_ui/10_govern/user/impersonation_token_spec.rb
+++ b/qa/qa/specs/features/browser_ui/10_govern/user/impersonation_token_spec.rb
@@ -2,7 +2,7 @@
module QA
RSpec.describe 'Govern' do
- describe 'Impersonation tokens', :requires_admin, product_group: :authentication_and_authorization do
+ describe 'Impersonation tokens', :requires_admin, product_group: :authentication do
let(:admin_api_client) { Runtime::API::Client.as_admin }
let!(:user) { create(:user, :hard_delete, api_client: admin_api_client) }
diff --git a/qa/qa/specs/features/browser_ui/10_govern/user/user_access_termination_spec.rb b/qa/qa/specs/features/browser_ui/10_govern/user/user_access_termination_spec.rb
index 895d577e9bd..6636a2c6220 100644
--- a/qa/qa/specs/features/browser_ui/10_govern/user/user_access_termination_spec.rb
+++ b/qa/qa/specs/features/browser_ui/10_govern/user/user_access_termination_spec.rb
@@ -3,7 +3,7 @@
module QA
RSpec.describe 'Govern' do
# TODO: `:reliable` should be added back once https://gitlab.com/gitlab-org/gitlab/-/issues/359278 is resolved
- describe 'User', :requires_admin, product_group: :authentication_and_authorization do
+ describe 'User', :requires_admin, product_group: :authentication do
# rubocop:disable RSpec/InstanceVariable
before(:all) do
admin_api_client = Runtime::API::Client.as_admin
diff --git a/qa/qa/specs/features/browser_ui/2_plan/pages/new_static_page_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/pages/new_static_page_spec.rb
index 582270c7940..579b6e43533 100644
--- a/qa/qa/specs/features/browser_ui/2_plan/pages/new_static_page_spec.rb
+++ b/qa/qa/specs/features/browser_ui/2_plan/pages/new_static_page_spec.rb
@@ -40,7 +40,7 @@ module QA
end
Page::Project::Menu.perform(&:go_to_pages_settings)
- Page::Project::Deployments::Pages.perform(&:go_to_access_page)
+ Page::Project::Settings::Pages.perform(&:go_to_access_page)
Support::Waiter.wait_until(
sleep_interval: 2,
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/file/edit_file_via_web_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/file/edit_file_via_web_spec.rb
index ac70165f107..155ebeef840 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/file/edit_file_via_web_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/file/edit_file_via_web_spec.rb
@@ -25,7 +25,7 @@ module QA
Page::File::Show.perform do |file|
aggregate_failures 'file details' do
- expect(file).to have_notice('Your changes have been successfully committed.')
+ expect(file).to have_notice('Your changes have been committed successfully.')
expect(file).to have_file_content(updated_file_content)
expect(file).to have_commit_message(commit_message_for_update)
end
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb
index 41ef38d2d66..0fce6d5bcf4 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb
@@ -9,10 +9,7 @@ module QA
access_token = Resource::PersonalAccessToken.fabricate!.token
- user = Resource::User.init do |user|
- user.username = Runtime::User.username
- user.password = access_token
- end
+ user = build(:user, username: Runtime::User.username, password: access_token)
push = Resource::Repository::ProjectPush.fabricate! do |push|
push.user = user
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_lfs_over_http_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_lfs_over_http_spec.rb
index 25a289f4e19..f794edb1b7a 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_lfs_over_http_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_lfs_over_http_spec.rb
@@ -3,7 +3,13 @@
module QA
RSpec.describe 'Create', product_group: :source_code do
describe 'Push mirror a repository over HTTP' do
- it 'configures and syncs LFS objects for a (push) mirrored repository', :aggregate_failures, testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347847' do
+ it 'configures and syncs LFS objects for a (push) mirrored repository', :aggregate_failures,
+ testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347847',
+ quarantine: {
+ only: { condition: -> { ENV['QA_RUN_TYPE'] == 'e2e-package-and-test-ce' } },
+ issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/412268',
+ type: :investigating
+ } do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.perform(&:sign_in_using_credentials)
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_over_http_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_over_http_spec.rb
index e430781412d..6704c05ead8 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_over_http_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_over_http_spec.rb
@@ -3,7 +3,14 @@
module QA
RSpec.describe 'Create' do
describe 'Push mirror a repository over HTTP', product_group: :source_code do
- it 'configures and syncs a (push) mirrored repository', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347741' do
+ it('configures and syncs a (push) mirrored repository',
+ testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347741',
+ quarantine: {
+ only: { condition: -> { ENV['QA_RUN_TYPE'] == 'e2e-package-and-test-ce' } },
+ issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/412611',
+ type: :investigating
+ }
+ ) do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.perform(&:sign_in_using_credentials)
diff --git a/qa/qa/specs/features/browser_ui/3_create/source_editor/source_editor_toolbar_spec.rb b/qa/qa/specs/features/browser_ui/3_create/source_editor/source_editor_toolbar_spec.rb
index bd98cb17332..2a763d08276 100644
--- a/qa/qa/specs/features/browser_ui/3_create/source_editor/source_editor_toolbar_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/source_editor/source_editor_toolbar_spec.rb
@@ -29,7 +29,7 @@ module QA
Page::File::Show.perform do |file|
aggregate_failures 'file details' do
- expect(file).to have_notice('Your changes have been successfully committed.')
+ expect(file).to have_notice('Your changes have been committed successfully.')
expect(file).to have_file_content(edited_readme_content)
end
end
diff --git a/qa/qa/specs/features/browser_ui/3_create/web_ide/add_new_directory_in_web_ide_spec.rb b/qa/qa/specs/features/browser_ui/3_create/web_ide/add_new_directory_in_web_ide_spec.rb
index 4d1cad4c4f9..fafa47a4a4f 100644
--- a/qa/qa/specs/features/browser_ui/3_create/web_ide/add_new_directory_in_web_ide_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/web_ide/add_new_directory_in_web_ide_spec.rb
@@ -1,12 +1,7 @@
# frozen_string_literal: true
module QA
- RSpec.describe 'Create', product_group: :ide,
- quarantine: {
- only: { job: 'slow-network' },
- issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/387609',
- type: :flaky
- } do
+ RSpec.describe 'Create', product_group: :ide do
describe 'Add a directory in Web IDE' do
let(:project) { create(:project, :with_readme, name: 'webide-add-directory-project') }
diff --git a/qa/qa/specs/features/browser_ui/3_create/web_ide/upload_new_file_in_web_ide_spec.rb b/qa/qa/specs/features/browser_ui/3_create/web_ide/upload_new_file_in_web_ide_spec.rb
index 2049ef78962..c038a6d8a87 100644
--- a/qa/qa/specs/features/browser_ui/3_create/web_ide/upload_new_file_in_web_ide_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/web_ide/upload_new_file_in_web_ide_spec.rb
@@ -1,12 +1,7 @@
# frozen_string_literal: true
module QA
- RSpec.describe 'Create', product_group: :ide,
- quarantine: {
- only: { job: 'slow-network' },
- issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/387609',
- type: :flaky
- } do
+ RSpec.describe 'Create', product_group: :ide do
describe 'Upload a file in Web IDE' do
let(:file_path) { File.absolute_path(File.join('qa', 'fixtures', 'web_ide', file_name)) }
let(:project) { create(:project, :with_readme, name: 'webide-upload-file-project') }
diff --git a/qa/qa/specs/features/browser_ui/3_create/web_ide_old/upload_new_file_in_web_ide_spec.rb b/qa/qa/specs/features/browser_ui/3_create/web_ide_old/upload_new_file_in_web_ide_spec.rb
deleted file mode 100644
index ab035d3b52c..00000000000
--- a/qa/qa/specs/features/browser_ui/3_create/web_ide_old/upload_new_file_in_web_ide_spec.rb
+++ /dev/null
@@ -1,91 +0,0 @@
-# frozen_string_literal: true
-
-# TODO: remove this test when coverage is replaced or deemed irrelevant
-module QA
- RSpec.describe 'Create', :skip_live_env, product_group: :ide do
- before do
- skip("Skipped but kept as reference. https://gitlab.com/gitlab-org/gitlab/-/merge_requests/115741#note_1330720944")
- end
-
- describe 'Upload a file in Web IDE' do
- let(:file_path) { Runtime::Path.fixture('web_ide', file_name) }
- let(:project) { create(:project, :with_readme, name: 'upload-file-project') }
-
- before do
- Flow::Login.sign_in
-
- project.visit!
- Page::Project::Show.perform(&:open_web_ide!)
- end
-
- context 'when a file with the same name already exists' do
- let(:file_name) { 'README.md' }
-
- it 'throws an error', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347850' do
- Page::Project::WebIDE::Edit.perform do |ide|
- ide.wait_until_ide_loads
- ide.upload_file(file_path)
- end
-
- expect(page).to have_content('The name "README.md" is already taken in this directory.')
- end
- end
-
- context 'when the file is a text file' do
- let(:file_name) { 'text_file.txt' }
-
- it 'shows the Edit tab with the text',
- testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347852' do
- Page::Project::WebIDE::Edit.perform do |ide|
- ide.wait_until_ide_loads
- ide.upload_file(file_path)
-
- expect(ide).to have_file(file_name)
- expect(ide).to have_file_addition_icon(file_name)
- expect(ide).to have_text('Simple text')
-
- ide.commit_changes
-
- expect(ide).to have_file(file_name)
- end
- end
- end
-
- context 'when the file is binary' do
- let(:file_name) { 'logo_sample.svg' }
-
- it 'shows a Download button', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347851' do
- Page::Project::WebIDE::Edit.perform do |ide|
- ide.upload_file(file_path)
-
- expect(ide).to have_file(file_name)
- expect(ide).to have_file_addition_icon(file_name)
- expect(ide).to have_download_button(file_name)
-
- ide.commit_changes
-
- expect(ide).to have_file(file_name)
- end
- end
- end
-
- context 'when the file is an image' do
- let(:file_name) { 'dk.png' }
-
- it 'shows an image viewer', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347853' do
- Page::Project::WebIDE::Edit.perform do |ide|
- ide.upload_file(file_path)
-
- expect(ide).to have_file(file_name)
- expect(ide).to have_file_addition_icon(file_name)
- expect(ide).to have_image_viewer(file_name)
-
- ide.commit_changes
-
- expect(ide).to have_file(file_name)
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/specs/features/browser_ui/4_verify/ci_job_artifacts/expose_job_artifacts_in_mr_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/ci_job_artifacts/expose_job_artifacts_in_mr_spec.rb
index dd7e8832c29..5786b15508a 100644
--- a/qa/qa/specs/features/browser_ui/4_verify/ci_job_artifacts/expose_job_artifacts_in_mr_spec.rb
+++ b/qa/qa/specs/features/browser_ui/4_verify/ci_job_artifacts/expose_job_artifacts_in_mr_spec.rb
@@ -10,16 +10,9 @@ module QA
let!(:runner) { create(:project_runner, project: project, name: executor, tags: [executor]) }
let!(:commit_ci_file) do
- Resource::Repository::Commit.fabricate_via_api! do |commit|
- commit.project = project
- commit.commit_message = 'Add .gitlab-ci.yml'
- commit.add_files(
- [
- file_path: '.gitlab-ci.yml',
- content: content
- ]
- )
- end
+ create(:commit, project: project, commit_message: 'Add .gitlab-ci.yml', actions: [
+ { action: 'create', file_path: '.gitlab-ci.yml', content: content }
+ ])
end
let(:merge_request) do
diff --git a/qa/qa/specs/features/browser_ui/4_verify/ci_job_artifacts/unlocking_job_artifacts_across_parent_child_pipelines_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/ci_job_artifacts/unlocking_job_artifacts_across_parent_child_pipelines_spec.rb
index bde817eccd3..7004f608d9e 100644
--- a/qa/qa/specs/features/browser_ui/4_verify/ci_job_artifacts/unlocking_job_artifacts_across_parent_child_pipelines_spec.rb
+++ b/qa/qa/specs/features/browser_ui/4_verify/ci_job_artifacts/unlocking_job_artifacts_across_parent_child_pipelines_spec.rb
@@ -1,7 +1,11 @@
# frozen_string_literal: true
module QA
- RSpec.describe 'Verify', :runner, product_group: :pipeline_security do
+ RSpec.describe 'Verify', :runner, product_group: :pipeline_security,
+ quarantine: {
+ issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/422863',
+ type: :flaky
+ } do
describe 'Unlocking job artifacts across parent-child pipelines' do
let(:executor) { "qa-runner-#{Faker::Alphanumeric.alphanumeric(number: 8)}" }
let(:project) { create(:project, name: 'unlock-job-artifacts-parent-child-project') }
diff --git a/qa/qa/specs/features/browser_ui/4_verify/ci_job_artifacts/unlocking_job_artifacts_across_pipelines_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/ci_job_artifacts/unlocking_job_artifacts_across_pipelines_spec.rb
index f9b8f7dcd1b..642e941adc8 100644
--- a/qa/qa/specs/features/browser_ui/4_verify/ci_job_artifacts/unlocking_job_artifacts_across_pipelines_spec.rb
+++ b/qa/qa/specs/features/browser_ui/4_verify/ci_job_artifacts/unlocking_job_artifacts_across_pipelines_spec.rb
@@ -2,7 +2,8 @@
module QA
RSpec.describe 'Verify', :runner, product_group: :pipeline_security do
- describe "Unlocking job artifacts across pipelines" do
+ describe "Unlocking job artifacts across pipelines", feature_flag: { name: 'ci_unlock_non_successful_pipelines,
+ scope: :project' } do
let(:executor) { "qa-runner-#{Faker::Alphanumeric.alphanumeric(number: 8)}" }
let(:project) { create(:project, name: 'unlock-job-artifacts-project') }
@@ -15,11 +16,13 @@ module QA
end
before do
+ Runtime::Feature.enable(:ci_unlock_non_successful_pipelines, project: project)
Flow::Login.sign_in
project.visit!
end
after do
+ Runtime::Feature.disable(:ci_unlock_non_successful_pipelines, project: project)
runner.remove_via_api!
end
@@ -59,11 +62,7 @@ module QA
end
it 'keeps job artifacts from latest failed pipelines and from latest successful pipeline',
- testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/394808',
- quarantine: {
- issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/266958',
- type: :bug
- } do
+ testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/394808' do
update_ci_file(job_name: 'failed_job_1', script: 'exit 1')
Flow::Pipeline.wait_for_latest_pipeline(status: 'Failed')
@@ -94,11 +93,7 @@ module QA
end
it 'keeps job artifacts from the latest blocked pipeline and from latest successful pipeline',
- testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/395511',
- quarantine: {
- issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/387087',
- type: :bug
- } do
+ testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/395511' do
update_ci_with_manual_job(job_name: 'successful_job_with_manual_1', script: 'echo test')
Flow::Pipeline.wait_for_latest_pipeline(status: 'Blocked')
diff --git a/qa/qa/specs/features/browser_ui/4_verify/ci_project_artifacts/user_can_bulk_delete_artifacts_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/ci_project_artifacts/user_can_bulk_delete_artifacts_spec.rb
index 27969759adf..34a6abdd267 100644
--- a/qa/qa/specs/features/browser_ui/4_verify/ci_project_artifacts/user_can_bulk_delete_artifacts_spec.rb
+++ b/qa/qa/specs/features/browser_ui/4_verify/ci_project_artifacts/user_can_bulk_delete_artifacts_spec.rb
@@ -51,18 +51,9 @@ module QA
end
def commit_ci_file
- Resource::Repository::Commit.fabricate_via_api! do |commit|
- commit.project = project
- commit.commit_message = 'Add .gitlab-ci.yml'
- commit.add_files(
- [
- {
- file_path: '.gitlab-ci.yml',
- content: content
- }
- ]
- )
- end
+ create(:commit, project: project, commit_message: 'Add .gitlab-ci.yml', actions: [
+ { action: 'create', file_path: '.gitlab-ci.yml', content: content }
+ ])
end
def content
diff --git a/qa/qa/specs/features/browser_ui/4_verify/ci_variable/pipeline_with_protected_variable_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/pipeline_with_protected_variable_spec.rb
index 2c18d288ccb..d24485280c1 100644
--- a/qa/qa/specs/features/browser_ui/4_verify/ci_variable/pipeline_with_protected_variable_spec.rb
+++ b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/pipeline_with_protected_variable_spec.rb
@@ -50,7 +50,8 @@ module QA
runner.remove_via_api!
end
- it 'exposes variable on protected branch', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348005' do
+ it 'exposes variable on protected branch', :reliable,
+ testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348005' do
create_protected_branch
[developer, maintainer].each do |user|
@@ -63,7 +64,8 @@ module QA
end
end
- it 'does not expose variable on unprotected branch', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347664' do
+ it 'does not expose variable on unprotected branch', :reliable,
+ testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347664' do
[developer, maintainer].each do |user|
create_merge_request(Runtime::API::Client.new(:gitlab, user: user))
go_to_pipeline_job(user)
diff --git a/qa/qa/specs/features/browser_ui/4_verify/ci_variable/prefill_variables_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/prefill_variables_spec.rb
index 31b95cb97ae..e80c969a68d 100644
--- a/qa/qa/specs/features/browser_ui/4_verify/ci_variable/prefill_variables_spec.rb
+++ b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/prefill_variables_spec.rb
@@ -46,7 +46,7 @@ module QA
Page::Project::Pipeline::Index.perform(&:click_run_pipeline_button)
end
- it 'shows only variables with description as prefill variables on the run pipeline page',
+ it 'shows only variables with description as prefill variables on the run pipeline page', :reliable,
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/378977' do
Page::Project::Pipeline::New.perform do |new|
aggregate_failures do
@@ -67,7 +67,7 @@ module QA
end
end
- it 'shows dropdown for variables with description, value, and options defined',
+ it 'shows dropdown for variables with description, value, and options defined', :reliable,
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/383820' do
Page::Project::Pipeline::New.perform do |new|
aggregate_failures do
diff --git a/qa/qa/specs/features/browser_ui/4_verify/ci_variable/raw_variables_defined_in_yaml_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/raw_variables_defined_in_yaml_spec.rb
index e5e5852b5f2..c6d9fdb08b3 100644
--- a/qa/qa/specs/features/browser_ui/4_verify/ci_variable/raw_variables_defined_in_yaml_spec.rb
+++ b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/raw_variables_defined_in_yaml_spec.rb
@@ -77,7 +77,7 @@ module QA
end
it(
- 'expands variables according to expand: true/false',
+ 'expands variables according to expand: true/false', :reliable,
:aggregate_failures,
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/381487'
) do
diff --git a/qa/qa/specs/features/browser_ui/4_verify/ci_variable/ui_variable_inheritable_when_forward_pipeline_variables_true_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/ui_variable_inheritable_when_forward_pipeline_variables_true_spec.rb
index 12c29ac2363..dd0ab16cf24 100644
--- a/qa/qa/specs/features/browser_ui/4_verify/ci_variable/ui_variable_inheritable_when_forward_pipeline_variables_true_spec.rb
+++ b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/ui_variable_inheritable_when_forward_pipeline_variables_true_spec.rb
@@ -29,6 +29,7 @@ module QA
def upstream_ci_file
{
+ action: 'create',
file_path: '.gitlab-ci.yml',
content: <<~YAML
child1_trigger:
diff --git a/qa/qa/specs/features/browser_ui/4_verify/ci_variable/ui_variable_non_inheritable_when_forward_pipeline_variables_false_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/ui_variable_non_inheritable_when_forward_pipeline_variables_false_spec.rb
index 1d354daaa5b..125ca024607 100644
--- a/qa/qa/specs/features/browser_ui/4_verify/ci_variable/ui_variable_non_inheritable_when_forward_pipeline_variables_false_spec.rb
+++ b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/ui_variable_non_inheritable_when_forward_pipeline_variables_false_spec.rb
@@ -44,6 +44,7 @@ module QA
def upstream_ci_file
{
+ action: 'create',
file_path: '.gitlab-ci.yml',
content: <<~YAML
child1_trigger:
diff --git a/qa/qa/specs/features/browser_ui/4_verify/pipeline/include_multiple_files_from_a_project_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/pipeline/include_multiple_files_from_a_project_spec.rb
index adf397907da..74fed9b6df2 100644
--- a/qa/qa/specs/features/browser_ui/4_verify/pipeline/include_multiple_files_from_a_project_spec.rb
+++ b/qa/qa/specs/features/browser_ui/4_verify/pipeline/include_multiple_files_from_a_project_spec.rb
@@ -30,7 +30,8 @@ module QA
runner.remove_via_api!
end
- it 'runs the pipeline with composed config', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348087' do
+ it 'runs the pipeline with composed config', :reliable,
+ testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348087' do
Page::Project::Pipeline::Show.perform do |pipeline|
aggregate_failures 'pipeline has all expected jobs' do
expect(pipeline).to have_job('build')
diff --git a/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_with_image_pull_policy_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_with_image_pull_policy_spec.rb
index 126c0bd5d9c..776a2605674 100644
--- a/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_with_image_pull_policy_spec.rb
+++ b/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_with_image_pull_policy_spec.rb
@@ -84,7 +84,7 @@ module QA
let(:text2) { 'is not one of the allowed_pull_policies ([never])' }
it(
- 'fails job with policy not allowed message',
+ 'fails job with policy not allowed message', :reliable,
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/368853'
) do
visit_job
diff --git a/qa/qa/specs/features/browser_ui/4_verify/pipeline/run_pipeline_with_manual_jobs_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/pipeline/run_pipeline_with_manual_jobs_spec.rb
index b5ebcb9e48a..afb3ae3cbfd 100644
--- a/qa/qa/specs/features/browser_ui/4_verify/pipeline/run_pipeline_with_manual_jobs_spec.rb
+++ b/qa/qa/specs/features/browser_ui/4_verify/pipeline/run_pipeline_with_manual_jobs_spec.rb
@@ -70,7 +70,7 @@ module QA
end
it(
- 'does not leave any job in skipped state',
+ 'does not leave any job in skipped state', :reliable,
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/349158'
) do
Page::Project::Pipeline::Show.perform do |show|
diff --git a/qa/qa/specs/features/browser_ui/4_verify/runner/fleet_management/group_runner_counts_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/runner/fleet_management/group_runner_counts_spec.rb
index 0173f8bd132..97869f26f36 100644
--- a/qa/qa/specs/features/browser_ui/4_verify/runner/fleet_management/group_runner_counts_spec.rb
+++ b/qa/qa/specs/features/browser_ui/4_verify/runner/fleet_management/group_runner_counts_spec.rb
@@ -18,7 +18,7 @@ module QA
end
it(
- 'shows group runner counts',
+ 'shows group runner counts', :reliable,
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/421256'
) do
Flow::Login.sign_in
diff --git a/qa/qa/specs/features/browser_ui/5_package/package_registry/composer_registry_spec.rb b/qa/qa/specs/features/browser_ui/5_package/package_registry/composer_registry_spec.rb
index 0d6f0faa8c1..a8aa10fc35c 100644
--- a/qa/qa/specs/features/browser_ui/5_package/package_registry/composer_registry_spec.rb
+++ b/qa/qa/specs/features/browser_ui/5_package/package_registry/composer_registry_spec.rb
@@ -5,21 +5,15 @@ module QA
describe 'Composer Repository', :external_api_calls do
include Runtime::Fixtures
- let(:project) { create(:project, :privtae, name: 'composer-package-project') }
- let(:package) do
- Resource::Package.init do |package|
- package.name = "my_package-#{SecureRandom.hex(4)}"
- package.project = project
- end
- end
+ let(:project) { create(:project, :private, name: 'composer-package-project') }
+ let(:package) { build(:package, name: "my_package-#{SecureRandom.hex(4)}", project: project) }
let!(:runner) do
- Resource::ProjectRunner.fabricate! do |runner|
- runner.name = "qa-runner-#{Time.now.to_i}"
- runner.tags = ["runner-for-#{project.name}"]
- runner.executor = :docker
- runner.project = project
- end
+ create(:project_runner,
+ name: "qa-runner-#{Time.now.to_i}",
+ tags: ["runner-for-#{project.name}"],
+ executor: :docker,
+ project: project)
end
let(:gitlab_host_with_port) { Support::GitlabAddress.host_with_port }
diff --git a/qa/qa/specs/features/browser_ui/5_package/package_registry/conan_repository_spec.rb b/qa/qa/specs/features/browser_ui/5_package/package_registry/conan_repository_spec.rb
index 7e70d73e339..7c9c3869fdf 100644
--- a/qa/qa/specs/features/browser_ui/5_package/package_registry/conan_repository_spec.rb
+++ b/qa/qa/specs/features/browser_ui/5_package/package_registry/conan_repository_spec.rb
@@ -10,20 +10,14 @@ module QA
include Runtime::Fixtures
let(:project) { create(:project, :private, name: 'conan-package-project') }
- let(:package) do
- Resource::Package.init do |package|
- package.name = "conantest-#{SecureRandom.hex(8)}"
- package.project = project
- end
- end
+ let(:package) { build(:package, name: "conantest-#{SecureRandom.hex(8)}", project: project) }
let!(:runner) do
- Resource::ProjectRunner.fabricate! do |runner|
- runner.name = "qa-runner-#{Time.now.to_i}"
- runner.tags = ["runner-for-#{project.name}"]
- runner.executor = :docker
- runner.project = project
- end
+ create(:project_runner,
+ name: "qa-runner-#{Time.now.to_i}",
+ tags: ["runner-for-#{project.name}"],
+ executor: :docker,
+ project: project)
end
let(:gitlab_address_with_port) do
diff --git a/qa/qa/specs/features/browser_ui/5_package/package_registry/generic_repository_spec.rb b/qa/qa/specs/features/browser_ui/5_package/package_registry/generic_repository_spec.rb
index 1baa70a2a65..2163d73614d 100644
--- a/qa/qa/specs/features/browser_ui/5_package/package_registry/generic_repository_spec.rb
+++ b/qa/qa/specs/features/browser_ui/5_package/package_registry/generic_repository_spec.rb
@@ -6,20 +6,14 @@ module QA
include Runtime::Fixtures
let(:project) { create(:project, :private, name: 'generic-package-project') }
- let(:package) do
- Resource::Package.init do |package|
- package.name = "my_package-#{SecureRandom.hex(8)}"
- package.project = project
- end
- end
+ let(:package) { build(:package, name: "my_package-#{SecureRandom.hex(8)}", project: project) }
let!(:runner) do
- Resource::ProjectRunner.fabricate! do |runner|
- runner.name = "qa-runner-#{Time.now.to_i}"
- runner.tags = ["runner-for-#{project.name}"]
- runner.executor = :docker
- runner.project = project
- end
+ create(:project_runner,
+ name: "qa-runner-#{Time.now.to_i}",
+ tags: ["runner-for-#{project.name}"],
+ executor: :docker,
+ project: project)
end
let(:file_txt) do
diff --git a/qa/qa/specs/features/browser_ui/5_package/package_registry/helm_registry_spec.rb b/qa/qa/specs/features/browser_ui/5_package/package_registry/helm_registry_spec.rb
index 42635a9e59f..91a25e68f00 100644
--- a/qa/qa/specs/features/browser_ui/5_package/package_registry/helm_registry_spec.rb
+++ b/qa/qa/specs/features/browser_ui/5_package/package_registry/helm_registry_spec.rb
@@ -46,19 +46,14 @@ module QA
end
it "pushes and pulls a helm chart", testcase: params[:testcase] do
+ helm_upload_yaml = ERB.new(read_fixture('package_managers/helm', 'helm_upload_package.yaml.erb')).result(binding)
+ helm_chart_yaml = ERB.new(read_fixture('package_managers/helm', 'Chart.yaml.erb')).result(binding)
+
Support::Retrier.retry_on_exception(max_attempts: 3, sleep_interval: 2) do
- Resource::Repository::Commit.fabricate_via_api! do |commit|
- helm_upload_yaml = ERB.new(read_fixture('package_managers/helm', 'helm_upload_package.yaml.erb')).result(binding)
- helm_chart_yaml = ERB.new(read_fixture('package_managers/helm', 'Chart.yaml.erb')).result(binding)
-
- commit.project = package_project
- commit.commit_message = 'Add .gitlab-ci.yml'
- commit.add_files(
- [
- { file_path: '.gitlab-ci.yml', content: helm_upload_yaml },
- { file_path: 'Chart.yaml', content: helm_chart_yaml }
- ])
- end
+ create(:commit, project: package_project, commit_message: 'Add .gitlab-ci.yml', actions: [
+ { action: 'create', file_path: '.gitlab-ci.yml', content: helm_upload_yaml },
+ { action: 'create', file_path: 'Chart.yaml', content: helm_chart_yaml }
+ ])
end
package_project.visit!
@@ -85,14 +80,12 @@ module QA
expect(show).to have_package_info(package_name, package_version)
end
- Support::Retrier.retry_on_exception(max_attempts: 3, sleep_interval: 2) do
- Resource::Repository::Commit.fabricate_via_api! do |commit|
- helm_install_yaml = ERB.new(read_fixture('package_managers/helm', 'helm_install_package.yaml.erb')).result(binding)
+ helm_install_yaml = ERB.new(read_fixture('package_managers/helm', 'helm_install_package.yaml.erb')).result(binding)
- commit.project = client_project
- commit.commit_message = 'Add .gitlab-ci.yml'
- commit.add_files([{ file_path: '.gitlab-ci.yml', content: helm_install_yaml }])
- end
+ Support::Retrier.retry_on_exception(max_attempts: 3, sleep_interval: 2) do
+ create(:commit, project: client_project, commit_message: 'Add .gitlab-ci.yml', actions: [
+ { action: 'create', file_path: '.gitlab-ci.yml', content: helm_install_yaml }
+ ])
end
client_project.visit!
diff --git a/qa/qa/specs/features/browser_ui/5_package/package_registry/maven/maven_group_level_spec.rb b/qa/qa/specs/features/browser_ui/5_package/package_registry/maven/maven_group_level_spec.rb
index 3cbc78ab806..f781ad0df2f 100644
--- a/qa/qa/specs/features/browser_ui/5_package/package_registry/maven/maven_group_level_spec.rb
+++ b/qa/qa/specs/features/browser_ui/5_package/package_registry/maven/maven_group_level_spec.rb
@@ -63,21 +63,16 @@ module QA
end
it 'pushes and pulls a maven package', testcase: params[:testcase] do
+ gitlab_ci_yaml = ERB.new(read_fixture('package_managers/maven/group/producer', 'gitlab_ci.yaml.erb')).result(binding)
+ pom_xml = ERB.new(read_fixture('package_managers/maven/group/producer', 'pom.xml.erb')).result(binding)
+ settings_xml = ERB.new(read_fixture('package_managers/maven/group/producer', 'settings.xml.erb')).result(binding)
+
Support::Retrier.retry_on_exception(max_attempts: 3, sleep_interval: 2) do
- Resource::Repository::Commit.fabricate_via_api! do |commit|
- gitlab_ci_yaml = ERB.new(read_fixture('package_managers/maven/group/producer', 'gitlab_ci.yaml.erb')).result(binding)
- pom_xml = ERB.new(read_fixture('package_managers/maven/group/producer', 'pom.xml.erb')).result(binding)
- settings_xml = ERB.new(read_fixture('package_managers/maven/group/producer', 'settings.xml.erb')).result(binding)
-
- commit.project = package_project
- commit.commit_message = 'Add files'
- commit.add_files(
- [
- { file_path: '.gitlab-ci.yml', content: gitlab_ci_yaml },
- { file_path: 'pom.xml', content: pom_xml },
- { file_path: 'settings.xml', content: settings_xml }
- ])
- end
+ create(:commit, project: package_project, actions: [
+ { action: 'create', file_path: '.gitlab-ci.yml', content: gitlab_ci_yaml },
+ { action: 'create', file_path: 'pom.xml', content: pom_xml },
+ { action: 'create', file_path: 'settings.xml', content: settings_xml }
+ ])
end
package_project.visit!
@@ -104,21 +99,16 @@ module QA
expect(show).to have_package_info(package_name, package_version)
end
+ gitlab_ci_yaml = ERB.new(read_fixture('package_managers/maven/group/consumer', 'gitlab_ci.yaml.erb')).result(binding)
+ pom_xml = ERB.new(read_fixture('package_managers/maven/group/consumer', 'pom.xml.erb')).result(binding)
+ settings_xml = ERB.new(read_fixture('package_managers/maven/group/consumer', 'settings.xml.erb')).result(binding)
+
Support::Retrier.retry_on_exception(max_attempts: 3, sleep_interval: 2) do
- Resource::Repository::Commit.fabricate_via_api! do |commit|
- gitlab_ci_yaml = ERB.new(read_fixture('package_managers/maven/group/consumer', 'gitlab_ci.yaml.erb')).result(binding)
- pom_xml = ERB.new(read_fixture('package_managers/maven/group/consumer', 'pom.xml.erb')).result(binding)
- settings_xml = ERB.new(read_fixture('package_managers/maven/group/consumer', 'settings.xml.erb')).result(binding)
-
- commit.project = client_project
- commit.commit_message = 'Add files'
- commit.add_files(
- [
- { file_path: '.gitlab-ci.yml', content: gitlab_ci_yaml },
- { file_path: 'pom.xml', content: pom_xml },
- { file_path: 'settings.xml', content: settings_xml }
- ])
- end
+ create(:commit, project: client_project, actions: [
+ { action: 'create', file_path: '.gitlab-ci.yml', content: gitlab_ci_yaml },
+ { action: 'create', file_path: 'pom.xml', content: pom_xml },
+ { action: 'create', file_path: 'settings.xml', content: settings_xml }
+ ])
end
client_project.visit!
@@ -192,21 +182,16 @@ module QA
end
def create_package(project)
+ gitlab_ci_yaml = ERB.new(read_fixture('package_managers/maven/group/producer', 'gitlab_ci.yaml.erb')).result(binding)
+ pom_xml = ERB.new(read_fixture('package_managers/maven/group/producer', 'pom.xml.erb')).result(binding)
+ settings_xml_with_pat = ERB.new(read_fixture('package_managers/maven/group', 'settings_with_pat.xml.erb')).result(binding)
+
Support::Retrier.retry_on_exception(max_attempts: 3, sleep_interval: 2) do
- Resource::Repository::Commit.fabricate_via_api! do |commit|
- gitlab_ci_yaml = ERB.new(read_fixture('package_managers/maven/group/producer', 'gitlab_ci.yaml.erb')).result(binding)
- pom_xml = ERB.new(read_fixture('package_managers/maven/group/producer', 'pom.xml.erb')).result(binding)
- settings_xml_with_pat = ERB.new(read_fixture('package_managers/maven/group', 'settings_with_pat.xml.erb')).result(binding)
-
- commit.project = project
- commit.commit_message = 'Add .gitlab-ci.yml'
- commit.add_files(
- [
- { file_path: '.gitlab-ci.yml', content: gitlab_ci_yaml },
- { file_path: 'pom.xml', content: pom_xml },
- { file_path: 'settings.xml', content: settings_xml_with_pat }
- ])
- end
+ create(:commit, project: project, commit_message: 'Add .gitlab-ci.yml', actions: [
+ { action: 'create', file_path: '.gitlab-ci.yml', content: gitlab_ci_yaml },
+ { action: 'create', file_path: 'pom.xml', content: pom_xml },
+ { action: 'create', file_path: 'settings.xml', content: settings_xml_with_pat }
+ ])
end
end
diff --git a/qa/qa/specs/features/browser_ui/5_package/package_registry/maven/maven_project_level_spec.rb b/qa/qa/specs/features/browser_ui/5_package/package_registry/maven/maven_project_level_spec.rb
index d1663f075e0..98a7e03181f 100644
--- a/qa/qa/specs/features/browser_ui/5_package/package_registry/maven/maven_project_level_spec.rb
+++ b/qa/qa/specs/features/browser_ui/5_package/package_registry/maven/maven_project_level_spec.rb
@@ -13,20 +13,14 @@ module QA
let(:package_type) { 'maven' }
let(:personal_access_token) { Runtime::Env.personal_access_token }
let(:package_project) { create(:project, :with_readme, :private, name: "#{package_type}_package_project") }
- let(:package) do
- Resource::Package.init do |package|
- package.name = package_name
- package.project = package_project
- end
- end
+ let(:package) { build(:package, name: package_name, project: package_project) }
let(:runner) do
- Resource::ProjectRunner.fabricate! do |runner|
- runner.name = "qa-runner-#{Time.now.to_i}"
- runner.tags = ["runner-for-#{package_project.name}"]
- runner.executor = :docker
- runner.project = package_project
- end
+ create(:project_runner,
+ name: "qa-runner-#{Time.now.to_i}",
+ tags: ["runner-for-#{package_project.name}"],
+ executor: :docker,
+ project: package_project)
end
let(:gitlab_address_with_port) do
@@ -34,15 +28,14 @@ module QA
end
let(:project_deploy_token) do
- Resource::ProjectDeployToken.fabricate_via_api! do |deploy_token|
- deploy_token.name = 'package-deploy-token'
- deploy_token.project = package_project
- deploy_token.scopes = %w[
+ create(:project_deploy_token,
+ name: 'package-deploy-token',
+ project: package_project,
+ scopes: %w[
read_repository
read_package_registry
write_package_registry
- ]
- end
+ ])
end
before do
@@ -89,24 +82,19 @@ module QA
end
it 'pushes and pulls a maven package via maven', testcase: params[:testcase] do
+ gitlab_ci_yaml = ERB.new(read_fixture('package_managers/maven/project', 'gitlab_ci.yaml.erb'))
+ .result(binding)
+ pom_xml = ERB.new(read_fixture('package_managers/maven/project', 'pom.xml.erb'))
+ .result(binding)
+ settings_xml = ERB.new(read_fixture('package_managers/maven/project', 'settings.xml.erb'))
+ .result(binding)
+
Support::Retrier.retry_on_exception(max_attempts: 3, sleep_interval: 2) do
- Resource::Repository::Commit.fabricate_via_api! do |commit|
- gitlab_ci_yaml = ERB.new(read_fixture('package_managers/maven/project', 'gitlab_ci.yaml.erb'))
- .result(binding)
- pom_xml = ERB.new(read_fixture('package_managers/maven/project', 'pom.xml.erb'))
- .result(binding)
- settings_xml = ERB.new(read_fixture('package_managers/maven/project', 'settings.xml.erb'))
- .result(binding)
-
- commit.project = package_project
- commit.commit_message = 'Add files'
- commit.add_files(
- [
- { file_path: '.gitlab-ci.yml', content: gitlab_ci_yaml },
- { file_path: 'pom.xml', content: pom_xml },
- { file_path: 'settings.xml', content: settings_xml }
- ])
- end
+ create(:commit, project: package_project, actions: [
+ { action: 'create', file_path: '.gitlab-ci.yml', content: gitlab_ci_yaml },
+ { action: 'create', file_path: 'pom.xml', content: pom_xml },
+ { action: 'create', file_path: 'settings.xml', content: settings_xml }
+ ])
end
package_project.visit!
diff --git a/qa/qa/specs/features/browser_ui/5_package/package_registry/maven_gradle_repository_spec.rb b/qa/qa/specs/features/browser_ui/5_package/package_registry/maven_gradle_repository_spec.rb
index 88cbe34b858..c7ba83677c7 100644
--- a/qa/qa/specs/features/browser_ui/5_package/package_registry/maven_gradle_repository_spec.rb
+++ b/qa/qa/specs/features/browser_ui/5_package/package_registry/maven_gradle_repository_spec.rb
@@ -76,18 +76,13 @@ module QA
it 'pushes and pulls a maven package via gradle', testcase: params[:testcase] do
Support::Retrier.retry_on_exception(max_attempts: 3, sleep_interval: 2) do
- Resource::Repository::Commit.fabricate_via_api! do |commit|
- gradle_publish_install_yaml = ERB.new(read_fixture('package_managers/maven/gradle', 'gradle_upload_install_package.yaml.erb')).result(binding)
- build_gradle = ERB.new(read_fixture('package_managers/maven/gradle', 'build.gradle.erb')).result(binding)
-
- commit.project = project
- commit.commit_message = 'Add .gitlab-ci.yml'
- commit.add_files(
- [
- { file_path: '.gitlab-ci.yml', content: gradle_publish_install_yaml },
- { file_path: 'build.gradle', content: build_gradle }
- ])
- end
+ gradle_publish_install_yaml = ERB.new(read_fixture('package_managers/maven/gradle', 'gradle_upload_install_package.yaml.erb')).result(binding)
+ build_gradle = ERB.new(read_fixture('package_managers/maven/gradle', 'build.gradle.erb')).result(binding)
+
+ create(:commit, project: project, commit_message: 'Add .gitlab-ci.yml', actions: [
+ { action: 'create', file_path: '.gitlab-ci.yml', content: gradle_publish_install_yaml },
+ { action: 'create', file_path: 'build.gradle', content: build_gradle }
+ ])
end
project.visit!
diff --git a/qa/qa/specs/features/browser_ui/5_package/package_registry/npm/npm_group_level_spec.rb b/qa/qa/specs/features/browser_ui/5_package/package_registry/npm/npm_group_level_spec.rb
index 0550c3373da..e58622d377c 100644
--- a/qa/qa/specs/features/browser_ui/5_package/package_registry/npm/npm_group_level_spec.rb
+++ b/qa/qa/specs/features/browser_ui/5_package/package_registry/npm/npm_group_level_spec.rb
@@ -16,15 +16,14 @@ module QA
end
let(:project_deploy_token) do
- Resource::ProjectDeployToken.fabricate_via_api! do |deploy_token|
- deploy_token.name = 'npm-deploy-token'
- deploy_token.project = project
- deploy_token.scopes = %w[
+ create(:project_deploy_token,
+ name: 'npm-deploy-token',
+ project: project,
+ scopes: %w[
read_repository
read_package_registry
write_package_registry
- ]
- end
+ ])
end
let(:gitlab_address_without_port) { Support::GitlabAddress.address_with_port(with_default_port: false) }
@@ -32,19 +31,15 @@ module QA
let!(:project) { create(:project, name: 'npm-group-level-publish') }
let!(:another_project) { create(:project, name: 'npm-group-level-install', group: project.group) }
let!(:runner) do
- Resource::GroupRunner.fabricate! do |runner|
- runner.name = "qa-runner-#{Time.now.to_i}"
- runner.tags = ["runner-for-#{project.group.name}"]
- runner.executor = :docker
- runner.group = project.group
- end
+ create(:group_runner,
+ name: "qa-runner-#{Time.now.to_i}",
+ tags: ["runner-for-#{project.group.name}"],
+ executor: :docker,
+ group: project.group)
end
let(:package) do
- Resource::Package.init do |package|
- package.name = "@#{registry_scope}/#{project.name}-#{SecureRandom.hex(8)}"
- package.project = project
- end
+ build(:package, name: "@#{registry_scope}/#{project.name}-#{SecureRandom.hex(8)}", project: project)
end
after do
@@ -75,25 +70,23 @@ module QA
end
it 'push and pull a npm package via CI', testcase: params[:testcase] do
+ npm_upload_yaml = ERB.new(read_fixture('package_managers/npm',
+ 'npm_upload_package_group.yaml.erb')).result(binding)
+ package_json = ERB.new(read_fixture('package_managers/npm', 'package.json.erb')).result(binding)
+
Support::Retrier.retry_on_exception(max_attempts: 3, sleep_interval: 2) do
- npm_upload_yaml = ERB.new(read_fixture('package_managers/npm',
- 'npm_upload_package_group.yaml.erb')).result(binding)
- package_json = ERB.new(read_fixture('package_managers/npm', 'package.json.erb')).result(binding)
-
- Resource::Repository::Commit.fabricate_via_api! do |commit|
- commit.project = project
- commit.commit_message = 'Add files'
- commit.add_files([
- {
- file_path: '.gitlab-ci.yml',
- content: npm_upload_yaml
- },
- {
- file_path: 'package.json',
- content: package_json
- }
- ])
- end
+ create(:commit, project: project, actions: [
+ {
+ action: 'create',
+ file_path: '.gitlab-ci.yml',
+ content: npm_upload_yaml
+ },
+ {
+ action: 'create',
+ file_path: 'package.json',
+ content: package_json
+ }
+ ])
end
project.visit!
@@ -107,20 +100,17 @@ module QA
expect(job).to be_successful(timeout: 800)
end
+ npm_install_yaml = ERB.new(read_fixture('package_managers/npm',
+ 'npm_install_package_group.yaml.erb')).result(binding)
+
Support::Retrier.retry_on_exception(max_attempts: 3, sleep_interval: 2) do
- Resource::Repository::Commit.fabricate_via_api! do |commit|
- npm_install_yaml = ERB.new(read_fixture('package_managers/npm',
- 'npm_install_package_group.yaml.erb')).result(binding)
-
- commit.project = another_project
- commit.commit_message = 'Add .gitlab-ci.yml'
- commit.add_files([
- {
- file_path: '.gitlab-ci.yml',
- content: npm_install_yaml
- }
- ])
- end
+ create(:commit, project: another_project, commit_message: 'Add .gitlab-ci.yml', actions: [
+ {
+ action: 'create',
+ file_path: '.gitlab-ci.yml',
+ content: npm_install_yaml
+ }
+ ])
end
another_project.visit!
diff --git a/qa/qa/specs/features/browser_ui/5_package/package_registry/npm/npm_instance_level_spec.rb b/qa/qa/specs/features/browser_ui/5_package/package_registry/npm/npm_instance_level_spec.rb
index f8e526a01b0..c24e37e0d1e 100644
--- a/qa/qa/specs/features/browser_ui/5_package/package_registry/npm/npm_instance_level_spec.rb
+++ b/qa/qa/specs/features/browser_ui/5_package/package_registry/npm/npm_instance_level_spec.rb
@@ -16,15 +16,14 @@ module QA
end
let(:project_deploy_token) do
- Resource::ProjectDeployToken.fabricate_via_api! do |deploy_token|
- deploy_token.name = 'npm-deploy-token'
- deploy_token.project = project
- deploy_token.scopes = %w[
+ create(:project_deploy_token,
+ name: 'npm-deploy-token',
+ project: project,
+ scopes: %w[
read_repository
read_package_registry
write_package_registry
- ]
- end
+ ])
end
let(:gitlab_address_without_port) { Support::GitlabAddress.address_with_port(with_default_port: false) }
@@ -32,19 +31,15 @@ module QA
let!(:project) { create(:project, name: 'npm-instance-level-publish') }
let!(:another_project) { create(:project, name: 'npm-instance-level-install', group: project.group) }
let!(:runner) do
- Resource::GroupRunner.fabricate! do |runner|
- runner.name = "qa-runner-#{Time.now.to_i}"
- runner.tags = ["runner-for-#{project.group.name}"]
- runner.executor = :docker
- runner.group = project.group
- end
+ create(:group_runner,
+ name: "qa-runner-#{Time.now.to_i}",
+ tags: ["runner-for-#{project.group.name}"],
+ executor: :docker,
+ group: project.group)
end
let(:package) do
- Resource::Package.init do |package|
- package.name = "@#{registry_scope}/#{project.name}-#{SecureRandom.hex(8)}"
- package.project = project
- end
+ build(:package, name: "@#{registry_scope}/#{project.name}-#{SecureRandom.hex(8)}", project: project)
end
after do
@@ -75,24 +70,22 @@ module QA
end
it 'push and pull a npm package via CI', testcase: params[:testcase] do
+ npm_upload_yaml = ERB.new(read_fixture('package_managers/npm', 'npm_upload_package_instance.yaml.erb')).result(binding)
+ package_json = ERB.new(read_fixture('package_managers/npm', 'package.json.erb')).result(binding)
+
Support::Retrier.retry_on_exception(max_attempts: 3, sleep_interval: 2) do
- npm_upload_yaml = ERB.new(read_fixture('package_managers/npm', 'npm_upload_package_instance.yaml.erb')).result(binding)
- package_json = ERB.new(read_fixture('package_managers/npm', 'package.json.erb')).result(binding)
-
- Resource::Repository::Commit.fabricate_via_api! do |commit|
- commit.project = project
- commit.commit_message = 'Add files'
- commit.add_files([
- {
- file_path: '.gitlab-ci.yml',
- content: npm_upload_yaml
- },
- {
- file_path: 'package.json',
- content: package_json
- }
- ])
- end
+ create(:commit, project: project, actions: [
+ {
+ action: 'create',
+ file_path: '.gitlab-ci.yml',
+ content: npm_upload_yaml
+ },
+ {
+ action: 'create',
+ file_path: 'package.json',
+ content: package_json
+ }
+ ])
end
project.visit!
@@ -106,19 +99,12 @@ module QA
expect(job).to be_successful(timeout: 800)
end
+ npm_install_yaml = ERB.new(read_fixture('package_managers/npm', 'npm_install_package_instance.yaml.erb')).result(binding)
+
Support::Retrier.retry_on_exception(max_attempts: 3, sleep_interval: 2) do
- Resource::Repository::Commit.fabricate_via_api! do |commit|
- npm_install_yaml = ERB.new(read_fixture('package_managers/npm', 'npm_install_package_instance.yaml.erb')).result(binding)
-
- commit.project = another_project
- commit.commit_message = 'Add .gitlab-ci.yml'
- commit.add_files([
- {
- file_path: '.gitlab-ci.yml',
- content: npm_install_yaml
- }
- ])
- end
+ create(:commit, project: another_project, commit_message: 'Add .gitlab-ci.yml', actions: [
+ { action: 'create', file_path: '.gitlab-ci.yml', content: npm_install_yaml }
+ ])
end
another_project.visit!
diff --git a/qa/qa/specs/features/browser_ui/5_package/package_registry/npm/npm_project_level_spec.rb b/qa/qa/specs/features/browser_ui/5_package/package_registry/npm/npm_project_level_spec.rb
index 11df6dcb303..cfa6b62cdbe 100644
--- a/qa/qa/specs/features/browser_ui/5_package/package_registry/npm/npm_project_level_spec.rb
+++ b/qa/qa/specs/features/browser_ui/5_package/package_registry/npm/npm_project_level_spec.rb
@@ -16,35 +16,28 @@ module QA
end
let(:project_deploy_token) do
- Resource::ProjectDeployToken.fabricate_via_api! do |deploy_token|
- deploy_token.name = 'npm-deploy-token'
- deploy_token.project = project
- deploy_token.scopes = %w[
+ create(:project_deploy_token,
+ name: 'npm-deploy-token',
+ project: project,
+ scopes: %w[
read_repository
read_package_registry
write_package_registry
- ]
- end
+ ])
end
let(:gitlab_address_without_port) { Support::GitlabAddress.address_with_port(with_default_port: false) }
let(:gitlab_host_without_port) { Support::GitlabAddress.host_with_port(with_default_port: false) }
let!(:project) { create(:project, :private, name: 'npm-project-level') }
let!(:runner) do
- Resource::ProjectRunner.fabricate! do |runner|
- runner.name = "qa-runner-#{Time.now.to_i}"
- runner.tags = ["runner-for-#{project.name}"]
- runner.executor = :docker
- runner.project = project
- end
+ create(:project_runner,
+ name: "qa-runner-#{Time.now.to_i}",
+ tags: ["runner-for-#{project.name}"],
+ executor: :docker,
+ project: project)
end
- let(:package) do
- Resource::Package.init do |package|
- package.name = "@#{registry_scope}/mypackage-#{SecureRandom.hex(8)}"
- package.project = project
- end
- end
+ let(:package) { build(:package, name: "@#{registry_scope}/mypackage-#{SecureRandom.hex(8)}", project: project) }
after do
package.remove_via_api!
@@ -71,23 +64,13 @@ module QA
end
it 'push and pull a npm package via CI', testcase: params[:testcase] do
- Resource::Repository::Commit.fabricate_via_api! do |commit|
- npm_upload_install_yaml = ERB.new(read_fixture('package_managers/npm', 'npm_upload_install_package_project.yaml.erb')).result(binding)
- package_json = ERB.new(read_fixture('package_managers/npm', 'package.json.erb')).result(binding)
-
- commit.project = project
- commit.commit_message = 'Add .gitlab-ci.yml'
- commit.add_files([
- {
- file_path: '.gitlab-ci.yml',
- content: npm_upload_install_yaml
- },
- {
- file_path: 'package.json',
- content: package_json
- }
- ])
- end
+ npm_upload_install_yaml = ERB.new(read_fixture('package_managers/npm', 'npm_upload_install_package_project.yaml.erb')).result(binding)
+ package_json = ERB.new(read_fixture('package_managers/npm', 'package.json.erb')).result(binding)
+
+ create(:commit, project: project, commit_message: 'Add .gitlab-ci.yml', actions: [
+ { action: 'create', file_path: '.gitlab-ci.yml', content: npm_upload_install_yaml },
+ { action: 'create', file_path: 'package.json', content: package_json }
+ ])
project.visit!
Flow::Pipeline.visit_latest_pipeline
diff --git a/qa/qa/specs/features/browser_ui/5_package/package_registry/nuget/nuget_group_level_spec.rb b/qa/qa/specs/features/browser_ui/5_package/package_registry/nuget/nuget_group_level_spec.rb
index c6504ed3b18..a9eadb52c72 100644
--- a/qa/qa/specs/features/browser_ui/5_package/package_registry/nuget/nuget_group_level_spec.rb
+++ b/qa/qa/specs/features/browser_ui/5_package/package_registry/nuget/nuget_group_level_spec.rb
@@ -17,23 +17,17 @@ module QA
end
let(:group_deploy_token) do
- Resource::GroupDeployToken.fabricate_via_api! do |deploy_token|
- deploy_token.name = 'nuget-group-deploy-token'
- deploy_token.group = project.group
- deploy_token.scopes = %w[
+ create(:group_deploy_token,
+ name: 'nuget-group-deploy-token',
+ group: project.group,
+ scopes: %w[
read_repository
read_package_registry
write_package_registry
- ]
- end
+ ])
end
- let(:package) do
- Resource::Package.init do |package|
- package.name = "dotnetcore-#{SecureRandom.hex(8)}"
- package.project = project
- end
- end
+ let(:package) { build(:package, name: "dotnetcore-#{SecureRandom.hex(8)}", project: project) }
let(:another_project) { create(:project, name: 'nuget-package-install-project', template_name: 'dotnetcore', group: project.group) }
let(:package_project_inbound_job_token_disabled) do
@@ -51,12 +45,11 @@ module QA
end
let!(:runner) do
- Resource::GroupRunner.fabricate! do |runner|
- runner.name = "qa-runner-#{Time.now.to_i}"
- runner.tags = ["runner-for-#{project.group.name}"]
- runner.executor = :docker
- runner.group = project.group
- end
+ create(:group_runner,
+ name: "qa-runner-#{Time.now.to_i}",
+ tags: ["runner-for-#{project.group.name}"],
+ executor: :docker,
+ group: project.group)
end
after do
@@ -102,13 +95,12 @@ module QA
it 'publishes a nuget package at the project endpoint and installs it from the group endpoint', testcase: params[:testcase] do
Flow::Login.sign_in
+ nuget_upload_yaml = ERB.new(read_fixture('package_managers/nuget', 'nuget_upload_package.yaml.erb')).result(binding)
+
Support::Retrier.retry_on_exception(max_attempts: 3, sleep_interval: 2) do
- Resource::Repository::Commit.fabricate_via_api! do |commit|
- nuget_upload_yaml = ERB.new(read_fixture('package_managers/nuget', 'nuget_upload_package.yaml.erb')).result(binding)
- commit.project = project
- commit.commit_message = 'Add .gitlab-ci.yml'
- commit.update_files([{ file_path: '.gitlab-ci.yml', content: nuget_upload_yaml }])
- end
+ create(:commit, project: project, commit_message: 'Add .gitlab-ci.yml', actions: [
+ { action: 'update', file_path: '.gitlab-ci.yml', content: nuget_upload_yaml }
+ ])
end
project.visit!
@@ -124,31 +116,24 @@ module QA
another_project.visit!
+ nuget_install_yaml = ERB.new(read_fixture('package_managers/nuget', 'nuget_install_package.yaml.erb')).result(binding)
+
Support::Retrier.retry_on_exception(max_attempts: 3, sleep_interval: 2) do
- Resource::Repository::Commit.fabricate_via_api! do |commit|
- nuget_install_yaml = ERB.new(read_fixture('package_managers/nuget', 'nuget_install_package.yaml.erb')).result(binding)
-
- commit.project = another_project
- commit.commit_message = 'Add new csproj file'
- commit.add_files(
- [
- {
- file_path: 'otherdotnet.csproj',
- content: <<~EOF
- <Project Sdk="Microsoft.NET.Sdk">
-
- <PropertyGroup>
- <OutputType>Exe</OutputType>
- <TargetFramework>net7.0</TargetFramework>
- </PropertyGroup>
-
- </Project>
- EOF
- }
- ]
- )
- commit.update_files([{ file_path: '.gitlab-ci.yml', content: nuget_install_yaml }])
- end
+ create(:commit, project: another_project, commit_message: 'Add new csproj file', actions: [
+ {
+ action: 'create',
+ file_path: 'otherdotnet.csproj',
+ content: <<~XML
+ <Project Sdk="Microsoft.NET.Sdk">
+ <PropertyGroup>
+ <OutputType>Exe</OutputType>
+ <TargetFramework>net7.0</TargetFramework>
+ </PropertyGroup>
+ </Project>
+ XML
+ },
+ { action: 'update', file_path: '.gitlab-ci.yml', content: nuget_install_yaml }
+ ])
end
Flow::Pipeline.visit_latest_pipeline
diff --git a/qa/qa/specs/features/browser_ui/5_package/package_registry/nuget/nuget_project_level_spec.rb b/qa/qa/specs/features/browser_ui/5_package/package_registry/nuget/nuget_project_level_spec.rb
index b4cac8af1dc..634d7ab3cd6 100644
--- a/qa/qa/specs/features/browser_ui/5_package/package_registry/nuget/nuget_project_level_spec.rb
+++ b/qa/qa/specs/features/browser_ui/5_package/package_registry/nuget/nuget_project_level_spec.rb
@@ -8,31 +8,24 @@ module QA
let(:project) { create(:project, :private, name: 'nuget-package-project', template_name: 'dotnetcore') }
let(:personal_access_token) { Resource::PersonalAccessToken.fabricate! }
let(:project_deploy_token) do
- Resource::ProjectDeployToken.fabricate_via_api! do |deploy_token|
- deploy_token.name = 'package-deploy-token'
- deploy_token.project = project
- deploy_token.scopes = %w[
+ create(:project_deploy_token,
+ name: 'package-deploy-token',
+ project: project,
+ scopes: %w[
read_repository
read_package_registry
write_package_registry
- ]
- end
+ ])
end
- let(:package) do
- Resource::Package.init do |package|
- package.name = "dotnetcore-#{SecureRandom.hex(8)}"
- package.project = project
- end
- end
+ let(:package) { build(:package, name: "dotnetcore-#{SecureRandom.hex(8)}", project: project) }
let!(:runner) do
- Resource::ProjectRunner.fabricate! do |runner|
- runner.name = "qa-runner-#{Time.now.to_i}"
- runner.tags = ["runner-for-#{project.name}"]
- runner.executor = :docker
- runner.project = project
- end
+ create(:project_runner,
+ name: "qa-runner-#{Time.now.to_i}",
+ tags: ["runner-for-#{project.name}"],
+ executor: :docker,
+ project: project)
end
after do
@@ -88,60 +81,54 @@ module QA
Flow::Login.sign_in
Support::Retrier.retry_on_exception(max_attempts: 3, sleep_interval: 2) do
- Resource::Repository::Commit.fabricate_via_api! do |commit|
- commit.project = project
- commit.commit_message = 'Add files'
- commit.update_files(
- [
- {
- file_path: '.gitlab-ci.yml',
- content: <<~YAML
- stages:
- - deploy
- - install
-
- deploy:
- stage: deploy
- image: mcr.microsoft.com/dotnet/sdk:5.0
- script:
- - dotnet restore -p:Configuration=Release
- - dotnet build -c Release
- - dotnet pack -c Release -p:PackageID=#{package.name}
- - dotnet nuget add source "$CI_SERVER_URL/api/v4/projects/$CI_PROJECT_ID/packages/nuget/index.json" --name gitlab --username #{auth_token_username} --password #{auth_token_password} --store-password-in-clear-text
- - dotnet nuget push "bin/Release/*.nupkg" --source gitlab
- rules:
- - if: '$CI_COMMIT_BRANCH == "#{project.default_branch}"'
- tags:
- - "runner-for-#{project.name}"
-
- install:
- stage: install
- image: mcr.microsoft.com/dotnet/sdk:5.0
- script:
- - dotnet nuget add source "$CI_SERVER_URL/api/v4/projects/$CI_PROJECT_ID/packages/nuget/index.json" --name gitlab --username #{auth_token_username} --password #{auth_token_password} --store-password-in-clear-text
- - "dotnet add dotnetcore.csproj package #{package.name} --version 1.0.0"
- rules:
- - if: '$CI_COMMIT_BRANCH == "#{project.default_branch}"'
- tags:
- - "runner-for-#{project.name}"
- YAML
- },
- {
- file_path: 'dotnetcore.csproj',
- content: <<~EOF
- <Project Sdk="Microsoft.NET.Sdk">
-
- <PropertyGroup>
- <OutputType>Exe</OutputType>
- <TargetFramework>net5.0</TargetFramework>
- </PropertyGroup>
-
- </Project>
- EOF
- }
- ]
- )
- end
+ create(:commit, project: project, actions: [
+ {
+ action: 'update',
+ file_path: '.gitlab-ci.yml',
+ content: <<~YAML
+ stages:
+ - deploy
+ - install
+
+ deploy:
+ stage: deploy
+ image: mcr.microsoft.com/dotnet/sdk:5.0
+ script:
+ - dotnet restore -p:Configuration=Release
+ - dotnet build -c Release
+ - dotnet pack -c Release -p:PackageID=#{package.name}
+ - dotnet nuget add source "$CI_SERVER_URL/api/v4/projects/$CI_PROJECT_ID/packages/nuget/index.json" --name gitlab --username #{auth_token_username} --password #{auth_token_password} --store-password-in-clear-text
+ - dotnet nuget push "bin/Release/*.nupkg" --source gitlab
+ rules:
+ - if: '$CI_COMMIT_BRANCH == "#{project.default_branch}"'
+ tags:
+ - "runner-for-#{project.name}"
+
+ install:
+ stage: install
+ image: mcr.microsoft.com/dotnet/sdk:5.0
+ script:
+ - dotnet nuget add source "$CI_SERVER_URL/api/v4/projects/$CI_PROJECT_ID/packages/nuget/index.json" --name gitlab --username #{auth_token_username} --password #{auth_token_password} --store-password-in-clear-text
+ - "dotnet add dotnetcore.csproj package #{package.name} --version 1.0.0"
+ rules:
+ - if: '$CI_COMMIT_BRANCH == "#{project.default_branch}"'
+ tags:
+ - "runner-for-#{project.name}"
+ YAML
+ },
+ {
+ action: 'update',
+ file_path: 'dotnetcore.csproj',
+ content: <<~XML
+ <Project Sdk="Microsoft.NET.Sdk">
+ <PropertyGroup>
+ <OutputType>Exe</OutputType>
+ <TargetFramework>net5.0</TargetFramework>
+ </PropertyGroup>
+ </Project>
+ XML
+ }
+ ])
end
project.visit!
diff --git a/qa/qa/specs/features/browser_ui/5_package/package_registry/pypi_repository_spec.rb b/qa/qa/specs/features/browser_ui/5_package/package_registry/pypi_repository_spec.rb
index 80439501299..18eaddf2e0d 100644
--- a/qa/qa/specs/features/browser_ui/5_package/package_registry/pypi_repository_spec.rb
+++ b/qa/qa/specs/features/browser_ui/5_package/package_registry/pypi_repository_spec.rb
@@ -7,20 +7,14 @@ module QA
include Support::Helpers::MaskToken
let(:project) { create(:project, :private, name: 'pypi-package-project') }
- let(:package) do
- Resource::Package.init do |package|
- package.name = "mypypipackage-#{SecureRandom.hex(8)}"
- package.project = project
- end
- end
+ let(:package) { build(:package, name: "mypypipackage-#{SecureRandom.hex(8)}", project: project) }
let!(:runner) do
- Resource::ProjectRunner.fabricate! do |runner|
- runner.name = "qa-runner-#{Time.now.to_i}"
- runner.tags = ["runner-for-#{project.name}"]
- runner.executor = :docker
- runner.project = project
- end
+ create(:project_runner,
+ name: "qa-runner-#{Time.now.to_i}",
+ tags: ["runner-for-#{project.name}"],
+ executor: :docker,
+ project: project)
end
let(:uri) { URI.parse(Runtime::Scenario.gitlab_address) }
@@ -36,21 +30,21 @@ module QA
Flow::Login.sign_in
Support::Retrier.retry_on_exception(max_attempts: 3, sleep_interval: 2) do
- Resource::Repository::Commit.fabricate_via_api! do |commit|
- pypi_yaml = ERB.new(read_fixture('package_managers/pypi', 'pypi_upload_install_package.yaml.erb')).result(binding)
- pypi_setup_file = ERB.new(read_fixture('package_managers/pypi', 'setup.py.erb')).result(binding)
-
- commit.project = project
- commit.commit_message = 'Add files'
- commit.add_files([{
- file_path: '.gitlab-ci.yml',
- content: pypi_yaml
- },
- {
- file_path: 'setup.py',
- content: pypi_setup_file
- }])
- end
+ pypi_yaml = ERB.new(read_fixture('package_managers/pypi', 'pypi_upload_install_package.yaml.erb')).result(binding)
+ pypi_setup_file = ERB.new(read_fixture('package_managers/pypi', 'setup.py.erb')).result(binding)
+
+ create(:commit, project: project, actions: [
+ {
+ action: 'create',
+ file_path: '.gitlab-ci.yml',
+ content: pypi_yaml
+ },
+ {
+ action: 'create',
+ file_path: 'setup.py',
+ content: pypi_setup_file
+ }
+ ])
end
project.visit!
diff --git a/qa/qa/specs/features/browser_ui/5_package/package_registry/rubygems_registry_spec.rb b/qa/qa/specs/features/browser_ui/5_package/package_registry/rubygems_registry_spec.rb
index c77bb9b1b4b..3502022a616 100644
--- a/qa/qa/specs/features/browser_ui/5_package/package_registry/rubygems_registry_spec.rb
+++ b/qa/qa/specs/features/browser_ui/5_package/package_registry/rubygems_registry_spec.rb
@@ -7,20 +7,14 @@ module QA
include Runtime::Fixtures
let(:project) { create(:project, :private, name: 'rubygems-package-project') }
- let(:package) do
- Resource::Package.init do |package|
- package.name = "mygem-#{SecureRandom.hex(8)}"
- package.project = project
- end
- end
+ let(:package) { build(:package, name: "mygem-#{SecureRandom.hex(8)}", project: project) }
let!(:runner) do
- Resource::ProjectRunner.fabricate! do |runner|
- runner.name = "qa-runner-#{Time.now.to_i}"
- runner.tags = ["runner-for-#{project.name}"]
- runner.executor = :docker
- runner.project = project
- end
+ create(:project_runner,
+ name: "qa-runner-#{Time.now.to_i}",
+ tags: ["runner-for-#{project.name}"],
+ executor: :docker,
+ project: project)
end
let(:gitlab_address_with_port) do
@@ -39,36 +33,32 @@ module QA
Flow::Login.sign_in
Support::Retrier.retry_on_exception(max_attempts: 3, sleep_interval: 2) do
- Resource::Repository::Commit.fabricate_via_api! do |commit|
- rubygem_upload_yaml = ERB.new(read_fixture('package_managers/rubygems', 'rubygems_upload_package.yaml.erb')).result(binding)
- rubygem_package_gemspec = ERB.new(read_fixture('package_managers/rubygems', 'package.gemspec.erb')).result(binding)
+ rubygem_upload_yaml = ERB.new(read_fixture('package_managers/rubygems', 'rubygems_upload_package.yaml.erb')).result(binding)
+ rubygem_package_gemspec = ERB.new(read_fixture('package_managers/rubygems', 'package.gemspec.erb')).result(binding)
- commit.project = project
- commit.commit_message = 'Add package files'
- commit.add_files(
- [
- {
- file_path: '.gitlab-ci.yml',
- content: rubygem_upload_yaml
- },
- {
- file_path: 'lib/hello_gem.rb',
- content:
- <<~RUBY
- class HelloWorld
- def self.hi
- puts "Hello world!"
- end
- end
- RUBY
- },
- {
- file_path: "#{package.name}.gemspec",
- content: rubygem_package_gemspec
- }
- ]
- )
- end
+ create(:commit, project: project, commit_message: 'Add package files', actions: [
+ {
+ action: 'create',
+ file_path: '.gitlab-ci.yml',
+ content: rubygem_upload_yaml
+ },
+ {
+ action: 'create',
+ file_path: 'lib/hello_gem.rb',
+ content: <<~RUBY
+ class HelloWorld
+ def self.hi
+ puts "Hello world!"
+ end
+ end
+ RUBY
+ },
+ {
+ action: 'create',
+ file_path: "#{package.name}.gemspec",
+ content: rubygem_package_gemspec
+ }
+ ])
end
project.visit!
diff --git a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb
index a94317d3463..aa7d60e2fe4 100644
--- a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb
+++ b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb
@@ -74,22 +74,17 @@ module QA
def upload_agent_config(project, agent)
Support::Retrier.retry_on_exception(max_attempts: 3, sleep_interval: 2) do
- Resource::Repository::Commit.fabricate_via_api! do |commit|
- commit.project = project
- commit.commit_message = 'Add kubernetes agent configuration'
- commit.add_files(
- [
- {
- file_path: ".gitlab/agents/#{agent}/config.yaml",
- content: <<~YAML
- ci_access:
- projects:
- - id: #{project.path_with_namespace}
- YAML
- }
- ]
- )
- end
+ create(:commit, project: project, commit_message: 'Add k8s agent configuration', actions: [
+ {
+ action: 'create',
+ file_path: ".gitlab/agents/#{agent}/config.yaml",
+ content: <<~YAML
+ ci_access:
+ projects:
+ - id: #{project.path_with_namespace}
+ YAML
+ }
+ ])
end
end
diff --git a/qa/qa/specs/features/browser_ui/9_data_stores/group/transfer_project_spec.rb b/qa/qa/specs/features/browser_ui/9_data_stores/group/transfer_project_spec.rb
index 6de1d08e674..e1995452aa9 100644
--- a/qa/qa/specs/features/browser_ui/9_data_stores/group/transfer_project_spec.rb
+++ b/qa/qa/specs/features/browser_ui/9_data_stores/group/transfer_project_spec.rb
@@ -9,10 +9,9 @@ module QA
let(:readme_content) { 'Here is the edited content.' }
before do
- Resource::Repository::Commit.fabricate_via_api! do |commit|
- commit.project = project
- commit.add_files([{ file_path: 'README.md', content: readme_content }])
- end
+ create(:commit, project: project, actions: [
+ { action: 'create', file_path: 'README.md', content: readme_content }
+ ])
Flow::Login.sign_in
diff --git a/qa/qa/specs/features/browser_ui/9_data_stores/project/create_project_spec.rb b/qa/qa/specs/features/browser_ui/9_data_stores/project/create_project_spec.rb
index 6c791a0ae0a..be4278f82d4 100644
--- a/qa/qa/specs/features/browser_ui/9_data_stores/project/create_project_spec.rb
+++ b/qa/qa/specs/features/browser_ui/9_data_stores/project/create_project_spec.rb
@@ -31,11 +31,7 @@ module QA
context(
'in personal namespace',
- testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347643',
- quarantine: {
- type: :investigating,
- issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/425904'
- }
+ testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347643'
) do
let(:project_name) { "project-in-personal-namespace-#{SecureRandom.hex(8)}" }
let(:project) do
diff --git a/qa/qa/specs/features/shared_contexts/import/gitlab_group_migration_common.rb b/qa/qa/specs/features/shared_contexts/import/gitlab_group_migration_common.rb
index 19d9f9ad94f..b6ea046fd59 100644
--- a/qa/qa/specs/features/shared_contexts/import/gitlab_group_migration_common.rb
+++ b/qa/qa/specs/features/shared_contexts/import/gitlab_group_migration_common.rb
@@ -10,7 +10,6 @@ module QA
let!(:import_wait_duration) { { max_duration: 120, sleep_interval: 2 } }
# source instance objects
- #
let!(:source_gitlab_address) { ENV["QA_IMPORT_SOURCE_URL"] || raise("QA_IMPORT_SOURCE_URL is required!") }
let!(:source_admin_api_client) do
Runtime::API::Client.new(
@@ -19,42 +18,42 @@ module QA
is_new_session: false
)
end
+
let!(:source_bulk_import_enabled) do
Runtime::ApplicationSettings.get_application_settings(api_client: source_admin_api_client)[:bulk_import_enabled]
end
+
let!(:source_admin_user) do
create(:user,
:set_public_email,
api_client: source_admin_api_client,
username: Runtime::Env.admin_username || 'root')
end
+
let!(:source_group) do
- Resource::Sandbox.fabricate_via_api! do |group|
- group.api_client = source_admin_api_client
- group.path = "source-group-for-import-#{SecureRandom.hex(4)}"
- group.avatar = File.new(Runtime::Path.fixture('designs', 'tanuki.jpg'), "r")
- end
+ create(:sandbox,
+ api_client: source_admin_api_client,
+ path: "source-group-for-import-#{SecureRandom.hex(4)}",
+ avatar: File.new(Runtime::Path.fixture('designs', 'tanuki.jpg'), "r"))
end
# target instance objects
- #
let!(:admin_api_client) { Runtime::API::Client.as_admin }
+
let!(:target_bulk_import_enabled) do
Runtime::ApplicationSettings.get_application_settings(api_client: admin_api_client)[:bulk_import_enabled]
end
+
let!(:admin_user) do
create(:user,
:set_public_email,
api_client: admin_api_client,
username: Runtime::Env.admin_username || 'root')
end
+
let!(:user) { create(:user, api_client: admin_api_client, username: "target-user-#{SecureRandom.hex(6)}") }
let!(:api_client) { Runtime::API::Client.new(user: user) }
- let!(:target_sandbox) do
- Resource::Sandbox.fabricate_via_api! do |group|
- group.api_client = admin_api_client
- end
- end
+ let!(:target_sandbox) { create(:sandbox, api_client: admin_api_client) }
let(:destination_group_path) { source_group.path }
let(:imported_group) do
diff --git a/qa/qa/specs/features/shared_contexts/packages_registry_shared_context.rb b/qa/qa/specs/features/shared_contexts/packages_registry_shared_context.rb
index 21e4d906043..2f7816341f1 100644
--- a/qa/qa/specs/features/shared_contexts/packages_registry_shared_context.rb
+++ b/qa/qa/specs/features/shared_contexts/packages_registry_shared_context.rb
@@ -25,19 +25,15 @@ module QA
end
let(:package) do
- Resource::Package.init do |package|
- package.name = package_name
- package.project = package_project
- end
+ build(:package, name: package_name, project: package_project)
end
let(:runner) do
- Resource::GroupRunner.fabricate! do |runner|
- runner.name = "qa-runner-#{Time.now.to_i}"
- runner.tags = ["runner-for-#{package_project.group.name}"]
- runner.executor = :docker
- runner.group = package_project.group
- end
+ create(:group_runner,
+ name: "qa-runner-#{Time.now.to_i}",
+ tags: ["runner-for-#{package_project.group.name}"],
+ executor: :docker,
+ group: package_project.group)
end
let(:gitlab_address_with_port) do
@@ -45,15 +41,14 @@ module QA
end
let(:project_deploy_token) do
- Resource::ProjectDeployToken.fabricate_via_api! do |deploy_token|
- deploy_token.name = 'package-deploy-token'
- deploy_token.project = package_project
- deploy_token.scopes = %w[
+ create(:project_deploy_token,
+ name: 'package-deploy-token',
+ project: package_project,
+ scopes: %w[
read_repository
read_package_registry
write_package_registry
- ]
- end
+ ])
end
before do
diff --git a/qa/qa/specs/features/shared_contexts/variable_inheritance_shared_context.rb b/qa/qa/specs/features/shared_contexts/variable_inheritance_shared_context.rb
index fb91364a1fc..1738e7b43d7 100644
--- a/qa/qa/specs/features/shared_contexts/variable_inheritance_shared_context.rb
+++ b/qa/qa/specs/features/shared_contexts/variable_inheritance_shared_context.rb
@@ -62,11 +62,7 @@ module QA
end
def add_ci_file(project, files)
- Resource::Repository::Commit.fabricate_via_api! do |commit|
- commit.project = project
- commit.commit_message = 'Add CI config file'
- commit.add_files(files)
- end
+ create(:commit, project: project, commit_message: 'Add CI config file', actions: files)
end
def visit_job_page(pipeline_title, job_name)
@@ -100,56 +96,60 @@ module QA
def upstream_child1_ci_file
{
+ action: 'create',
file_path: '.child1-ci.yml',
content: <<~YAML
- child1_job:
- stage: test
- tags: ["#{random_string}"]
- script:
- - echo $TEST_VAR
- - echo Done!
+ child1_job:
+ stage: test
+ tags: ["#{random_string}"]
+ script:
+ - echo $TEST_VAR
+ - echo Done!
YAML
}
end
def upstream_child2_ci_file
{
+ action: 'create',
file_path: '.child2-ci.yml',
content: <<~YAML
- child2_job:
- stage: test
- tags: ["#{random_string}"]
- script:
- - echo $TEST_VAR
- - echo Done!
+ child2_job:
+ stage: test
+ tags: ["#{random_string}"]
+ script:
+ - echo $TEST_VAR
+ - echo Done!
YAML
}
end
def downstream1_ci_file
{
+ action: 'create',
file_path: '.gitlab-ci.yml',
content: <<~YAML
- downstream1_job:
- stage: deploy
- tags: ["#{random_string}"]
- script:
- - echo $TEST_VAR
- - echo Done!
+ downstream1_job:
+ stage: deploy
+ tags: ["#{random_string}"]
+ script:
+ - echo $TEST_VAR
+ - echo Done!
YAML
}
end
def downstream2_ci_file
{
+ action: 'create',
file_path: '.gitlab-ci.yml',
content: <<~YAML
- downstream2_job:
- stage: deploy
- tags: ["#{random_string}"]
- script:
- - echo $TEST_VAR
- - echo Done!
+ downstream2_job:
+ stage: deploy
+ tags: ["#{random_string}"]
+ script:
+ - echo $TEST_VAR
+ - echo Done!
YAML
}
end
diff --git a/qa/qa/specs/features/shared_examples/merge_with_code_owner_shared_examples.rb b/qa/qa/specs/features/shared_examples/merge_with_code_owner_shared_examples.rb
index 01b229192cc..255294476a1 100644
--- a/qa/qa/specs/features/shared_examples/merge_with_code_owner_shared_examples.rb
+++ b/qa/qa/specs/features/shared_examples/merge_with_code_owner_shared_examples.rb
@@ -13,20 +13,15 @@ module QA
settings.set_default_number_of_approvals_required(1)
end
- Resource::Repository::Commit.fabricate_via_api! do |commit|
- commit.project = project
- commit.commit_message = 'Add CODEOWNERS'
- commit.add_files(
- [
- {
- file_path: 'CODEOWNERS',
- content: <<~CONTENT
- README.md @#{codeowner}
- CONTENT
- }
- ]
- )
- end
+ create(:commit, project: project, commit_message: 'Add CODEOWNERS', actions: [
+ {
+ action: 'create',
+ file_path: 'CODEOWNERS',
+ content: <<~CONTENT
+ README.md @#{codeowner}
+ CONTENT
+ }
+ ])
# Require approval from code owners on the default branch
protected_branch = Resource::ProtectedBranch.fabricate_via_api! do |branch|
diff --git a/qa/qa/support/helpers/plan.rb b/qa/qa/support/helpers/plan.rb
index c3867b4a1b8..c53c099b2bd 100644
--- a/qa/qa/support/helpers/plan.rb
+++ b/qa/qa/support/helpers/plan.rb
@@ -4,7 +4,7 @@ module QA
module Support
module Helpers
module Plan
- FREE = { name: 'free', price: 0, yearly_price: 0, ci_minutes: 400 }.freeze
+ FREE = { name: 'free', price: 0, yearly_price: 0, compute_minutes: 400 }.freeze
PREMIUM = {
plan_id: '2c92a00d76f0d5060176f2fb0a5029ff',
@@ -12,7 +12,7 @@ module QA
name: 'premium',
price: 19,
yearly_price: 228,
- ci_minutes: 10000
+ compute_minutes: 10000
}.freeze
PREMIUM_SELF_MANAGED = {
@@ -29,7 +29,7 @@ module QA
name: 'ultimate',
price: 99,
yearly_price: 1188,
- ci_minutes: 50000
+ compute_minutes: 50000
}.freeze
ULTIMATE_SELF_MANAGED = {
@@ -40,12 +40,12 @@ module QA
yearly_price: 1188
}.freeze
- CI_MINUTES = {
+ COMPUTE_MINUTES = {
plan_id: '2c92a0086a07f4a8016a2c0a1f7b4b4c',
rate_charge_id: '2c92a0fd6a07f4c6016a2c0af07c3f21',
- name: 'ci_minutes',
+ name: 'compute_minutes',
price: 10,
- ci_minutes: 1000
+ compute_minutes: 1000
}.freeze
STORAGE = {
diff --git a/qa/qa/support/matchers/have_matcher.rb b/qa/qa/support/matchers/have_matcher.rb
index 52650edab5f..b8c63166068 100644
--- a/qa/qa/support/matchers/have_matcher.rb
+++ b/qa/qa/support/matchers/have_matcher.rb
@@ -5,33 +5,34 @@ module QA
module Matchers
module HaveMatcher
PREDICATE_TARGETS = %w[
- auto_devops_container
- element
- file_content
- file_name
+ alert_with_title
assignee
+ auto_devops_container
child_pipeline
- linked_pipeline
content
+ delete_issue_button
design
+ element
file
+ file_content
+ file_name
+ framework
+ incident
issue
job
+ label
+ linked_pipeline
+ linked_resource
package
pipeline
related_issue_item
sast_status
security_configuration_history_link
+ skipped_job_in_group
snippet_description
+ system_note
tag
- label
variable
- system_note
- alert_with_title
- incident
- framework
- delete_issue_button
- skipped_job_in_group
].each do |predicate|
RSpec::Matchers.define "have_#{predicate}" do |*args, **kwargs|
match do |page_object|
diff --git a/qa/qa/tools/ci/qa_changes.rb b/qa/qa/tools/ci/qa_changes.rb
index 1e3ef9e4816..91c7760933f 100644
--- a/qa/qa/tools/ci/qa_changes.rb
+++ b/qa/qa/tools/ci/qa_changes.rb
@@ -29,10 +29,16 @@ module QA
# @return [String]
def qa_tests
return if mr_diff.empty? || dependency_changes
+ return if only_spec_changes? && mr_diff.all? { |change| change[:deleted_file] }
- # make paths relative to qa directory
- return changed_files&.map { |path| path.delete_prefix("qa/") }&.join(" ") if only_spec_changes?
- return qa_spec_directories_for_devops_stage&.join(" ") if non_qa_changes? && mr_labels.any?
+ if only_spec_changes?
+ return mr_diff
+ .reject { |change| change[:deleted_file] }
+ .map { |change| change[:path].delete_prefix("qa/") } # make paths relative to qa directory
+ .join(" ")
+ end
+
+ qa_spec_directories_for_devops_stage&.join(" ") if non_qa_changes? && mr_labels.any?
end
# Qa framework changes
diff --git a/qa/qa/tools/delete_user_projects.rb b/qa/qa/tools/delete_user_projects.rb
index 9c031e352b4..5c3ae4d4ef2 100644
--- a/qa/qa/tools/delete_user_projects.rb
+++ b/qa/qa/tools/delete_user_projects.rb
@@ -29,6 +29,15 @@ module QA
personal_access_token: ENV['GITLAB_QA_ACCESS_TOKEN'])
end
+ # @example
+ # GITLAB_ADDRESS=<address> \
+ # GITLAB_QA_ACCESS_TOKEN=<token> \
+ # USER_ID=<id> bundle exec "delete_user_projects[2023-01-01,true]"
+ #
+ # @example
+ # GITLAB_ADDRESS=<address> \
+ # GITLAB_QA_ACCESS_TOKEN=<token> \
+ # USER_ID=<id> bundle exec "delete_user_projects[,true]"
def run
$stdout.puts 'Running...'
diff --git a/qa/qa/tools/generate_import_test_group.rb b/qa/qa/tools/generate_import_test_group.rb
new file mode 100644
index 00000000000..f2b5f79c3ec
--- /dev/null
+++ b/qa/qa/tools/generate_import_test_group.rb
@@ -0,0 +1,123 @@
+# frozen_string_literal: true
+
+require "etc"
+
+module QA
+ module Tools
+ # Helper to generate group with projects for Direct Transfer testing
+ #
+ # Should be used with care as it can trigger a lot of project imports
+ #
+ class GenerateImportTestGroup
+ # Generate test group
+ #
+ # @param [String] project_tar_paths exported project tar.gz file path, optionally several separated by ';'
+ # @param [String] group_path path of group where projects will be generated
+ # @param [Integer] project_copies number of projects to create in a group
+ def initialize(
+ project_tar_paths: Runtime::Path.fixture('export.tar.gz'),
+ group_path: "import-test",
+ project_copies: 10
+ )
+ @project_tar_paths = project_tar_paths
+ @group_path = group_path
+ @project_copies = project_copies
+ @logger = Runtime::Logger.logger
+ end
+
+ # Generate group with projects
+ #
+ # @return [void]
+ def generate
+ check_access_token
+ raise("Project pool has no valid archive files") if project_pool.empty?
+
+ logger.info("Creating '#{group_path}' group with #{project_copies} copies of exported projects")
+ create_group
+
+ (1..project_copies).each do
+ name = "imported-project-#{SecureRandom.hex(8)}"
+ tar = project_pool[rand(0..project_pool.size - 1)]
+
+ logger.info("Fabricating copy of '#{tar.basename}' with name '#{name}'")
+ Resource::ImportProject.fabricate_via_api! do |project|
+ project.file_path = tar.to_s
+ project.api_client = api_client
+ project.name = name
+ project.group = group
+ # we mark project as not import so it doesn't wait for import to finish
+ # when generating large projects, it can take a long time
+ project.import = false
+ end
+
+ sleep(10) # add pause to not trigger 'too many requests error'
+ rescue StandardError => e
+ logger.error("Failed to fabricate project '#{name}', error: #{e}")
+ end
+ end
+
+ private
+
+ attr_reader :project_tar_paths, :group_path, :project_copies, :logger
+
+ # Gitlab access token
+ #
+ # @return [String]
+ def access_token
+ @access_token ||= ENV['GITLAB_QA_ACCESS_TOKEN'] || raise("GITLAB_QA_ACCESS_TOKEN required")
+ end
+ alias_method :check_access_token, :access_token
+
+ # API client
+ #
+ # @return [Runtime::API::Client]
+ def api_client
+ @api_client ||= Runtime::API::Client.new(:gitlab, personal_access_token: access_token)
+ end
+
+ # Pool of project tar files
+ #
+ # @return [Array<Pathname>]
+ def project_pool
+ @project_pool ||= project_tar_paths.split(";").filter_map do |f|
+ path = Pathname.new(f)
+ next logger.warn("#{f} is not a valid path!") && nil unless path.exist?
+
+ path
+ end
+ end
+
+ # Create group with all subgroups
+ #
+ # @return [<Resource::Sandbox, Resource::Group>]
+ def group
+ return @group if defined?(@group)
+
+ paths = group_path.split("/")
+ sandbox = create(:sandbox, path: paths.first)
+ return @group = sandbox if paths.size == 1
+
+ @group = paths[1..].each_with_object([sandbox]) do |path, arr|
+ arr << create(:group, parent: arr.last, path: path)
+ end.last
+ end
+ alias_method :create_group, :group
+
+ # Create group resource
+ #
+ # @param [Symbol] type
+ # @param [String] path
+ # @param [<Resource::Sandbox, Resource::Group>] sandbox
+ # @return [<Resource::Sandbox, Resource::Group>]
+ def create(type, path:, parent: nil)
+ resource_class = type == :sandbox ? Resource::Sandbox : Resource::Group
+
+ resource_class.fabricate_via_api! do |resource|
+ resource.api_client = api_client
+ resource.sandbox = parent unless type == :sandbox
+ resource.path = path
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/tools/long_running_spec_reporter.rb b/qa/qa/tools/long_running_spec_reporter.rb
index 865b16f1d41..82015d024fb 100644
--- a/qa/qa/tools/long_running_spec_reporter.rb
+++ b/qa/qa/tools/long_running_spec_reporter.rb
@@ -41,7 +41,7 @@ module QA
def mean_runtime
@mean_runtime ||= latest_report.values
.select { |v| v < RUNTIME_THRESHOLD }
- .yield_self { |runtimes| runtimes.sum(0.0) / runtimes.length }
+ .then { |runtimes| runtimes.sum(0.0) / runtimes.length }
end
# Spec files exceeding runtime threshold
diff --git a/rubocop/batched_background_migrations.rb b/rubocop/batched_background_migrations.rb
deleted file mode 100644
index ce7115e5cd5..00000000000
--- a/rubocop/batched_background_migrations.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-# frozen_string_literal: true
-
-module RuboCop
- class BatchedBackgroundMigrations
- DICTIONARY_BASE_DIR = 'db/docs/batched_background_migrations'
-
- attr_reader :queued_migration_version
-
- class << self
- def dictionary_data
- @dictionary_data ||= Dir.glob("*.yml", base: DICTIONARY_BASE_DIR).each_with_object({}) do |file_name, data|
- dictionary = YAML.load_file(File.join(DICTIONARY_BASE_DIR, file_name))
-
- next unless dictionary['queued_migration_version'].present?
-
- data[dictionary['queued_migration_version'].to_s] = {
- finalize_after: dictionary['finalize_after'],
- finalized_by: dictionary['finalized_by'].to_s
- }
- end
- end
- end
-
- def initialize(queued_migration_version)
- @queued_migration_version = queued_migration_version
- end
-
- def finalized_by
- self.class.dictionary_data.dig(queued_migration_version.to_s, :finalized_by)
- end
- end
-end
diff --git a/rubocop/batched_background_migrations_dictionary.rb b/rubocop/batched_background_migrations_dictionary.rb
new file mode 100644
index 00000000000..286f0a57bad
--- /dev/null
+++ b/rubocop/batched_background_migrations_dictionary.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+module RuboCop
+ class BatchedBackgroundMigrationsDictionary
+ DICTIONARY_BASE_DIR = 'db/docs/batched_background_migrations'
+
+ attr_reader :queued_migration_version
+
+ class << self
+ def dictionary_data
+ @dictionary_data ||= Dir.glob("*.yml", base: DICTIONARY_BASE_DIR).each_with_object({}) do |file_name, data|
+ dictionary = YAML.load_file(File.join(DICTIONARY_BASE_DIR, file_name))
+
+ next unless dictionary['queued_migration_version'].present?
+
+ data[dictionary['queued_migration_version'].to_s] = {
+ introduced_by_url: dictionary['introduced_by_url'],
+ finalize_after: dictionary['finalize_after'],
+ finalized_by: dictionary['finalized_by'].to_s
+ }
+ end
+ end
+ end
+
+ def initialize(queued_migration_version)
+ @queued_migration_version = queued_migration_version
+ end
+
+ def finalized_by
+ dictionary_data&.dig(:finalized_by)
+ end
+
+ def finalize_after
+ dictionary_data&.dig(:finalize_after)
+ end
+
+ def introduced_by_url
+ dictionary_data&.dig(:introduced_by_url)
+ end
+
+ private
+
+ def dictionary_data
+ @dictionary_data ||= self.class.dictionary_data[queued_migration_version.to_s]
+ end
+ end
+end
diff --git a/rubocop/cop/background_migration/dictionary_file.rb b/rubocop/cop/background_migration/dictionary_file.rb
new file mode 100644
index 00000000000..9ae47260e79
--- /dev/null
+++ b/rubocop/cop/background_migration/dictionary_file.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+require_relative '../../migration_helpers'
+require_relative '../../batched_background_migrations_dictionary'
+
+module RuboCop
+ module Cop
+ module BackgroundMigration
+ # Checks the batched background migration has the corresponding dictionary file
+ class DictionaryFile < RuboCop::Cop::Base
+ include MigrationHelpers
+
+ MSG = {
+ missing_key: "Mandatory key '%{key}' is missing from the dictionary. Please add with an appropriate value.",
+ missing_dictionary: <<-MESSAGE.delete("\n").squeeze(' ').strip
+ Missing %{file_name}.
+ Use the generator 'batched_background_migration' to create dictionary files automatically.
+ For more details refer: https://docs.gitlab.com/ee/development/database/batched_background_migrations.html#generator
+ MESSAGE
+ }.freeze
+
+ DICTIONARY_DIR = "db/docs/batched_background_migrations"
+
+ def_node_matcher :batched_background_migration_name_node, <<~PATTERN
+ `(send nil? :queue_batched_background_migration $_ ...)
+ PATTERN
+
+ def_node_matcher :migration_constant_value, <<~PATTERN
+ `(casgn nil? %const_name ({sym|str} $_))
+ PATTERN
+
+ def on_class(node)
+ return unless time_enforced?(node) && in_post_deployment_migration?(node)
+
+ migration_name_node = batched_background_migration_name_node(node)
+ return unless migration_name_node
+
+ migration_name = if migration_name_node.const_name.present?
+ migration_constant_value(node, const_name: migration_name_node.const_name.to_sym)
+ else
+ migration_name_node.value
+ end
+
+ error_code, msg_params = validate_dictionary_file(migration_name, node)
+ return unless error_code.present?
+
+ add_offense(node, message: format(MSG[error_code], msg_params))
+ end
+
+ private
+
+ def dictionary_file?(migration_class_name)
+ File.exist?(dictionary_file_path(migration_class_name))
+ end
+
+ def dictionary_file_path(migration_class_name)
+ File.join(rails_root, DICTIONARY_DIR, "#{migration_class_name.underscore}.yml")
+ end
+
+ def validate_dictionary_file(migration_name, node)
+ unless dictionary_file?(migration_name)
+ return [:missing_dictionary, { file_name: dictionary_file_path(migration_name) }]
+ end
+
+ bbm_dictionary = RuboCop::BatchedBackgroundMigrationsDictionary.new(version(node))
+
+ return [:missing_key, { key: :finalize_after }] unless bbm_dictionary.finalize_after.present?
+
+ return [:missing_key, { key: :introduced_by_url }] unless bbm_dictionary.introduced_by_url.present?
+ end
+
+ def rails_root
+ @rails_root ||= File.expand_path('../../..', __dir__)
+ end
+ end
+ end
+ end
+end
diff --git a/rubocop/cop/background_migration/missing_dictionary_file.rb b/rubocop/cop/background_migration/missing_dictionary_file.rb
deleted file mode 100644
index 9158b268bf9..00000000000
--- a/rubocop/cop/background_migration/missing_dictionary_file.rb
+++ /dev/null
@@ -1,59 +0,0 @@
-# frozen_string_literal: true
-
-require_relative '../../migration_helpers'
-
-module RuboCop
- module Cop
- module BackgroundMigration
- # Checks the batched background migration has the corresponding dictionary file
- class MissingDictionaryFile < RuboCop::Cop::Base
- include MigrationHelpers
-
- MSG = "Missing %{file_name}. " \
- "Use the generator 'batched_background_migration' to create dictionary files automatically. " \
- "For more details refer: https://docs.gitlab.com/ee/development/database/batched_background_migrations.html#generator"
-
- DICTIONARY_DIR = "db/docs/batched_background_migrations"
-
- def_node_matcher :batched_background_migration_name_node, <<~PATTERN
- `(send nil? :queue_batched_background_migration $_ ...)
- PATTERN
-
- def_node_matcher :migration_constant_value, <<~PATTERN
- `(casgn nil? %const_name ({sym|str} $_))
- PATTERN
-
- def on_class(node)
- return unless time_enforced?(node) && in_post_deployment_migration?(node)
-
- migration_name_node = batched_background_migration_name_node(node)
- return unless migration_name_node
-
- migration_name = if migration_name_node.const_name.present?
- migration_constant_value(node, const_name: migration_name_node.const_name.to_sym)
- else
- migration_name_node.value
- end
-
- return if dictionary_file?(migration_name)
-
- add_offense(node, message: format(MSG, file_name: dictionary_file_path(migration_name)))
- end
-
- private
-
- def dictionary_file?(migration_class_name)
- File.exist?(dictionary_file_path(migration_class_name))
- end
-
- def dictionary_file_path(migration_class_name)
- File.join(rails_root, DICTIONARY_DIR, "#{migration_class_name.underscore}.yml")
- end
-
- def rails_root
- @rails_root ||= File.expand_path('../../..', __dir__)
- end
- end
- end
- end
-end
diff --git a/rubocop/cop/gitlab/avoid_gitlab_instance_checks.rb b/rubocop/cop/gitlab/avoid_gitlab_instance_checks.rb
index 962a58cfb4a..6aac3649b04 100644
--- a/rubocop/cop/gitlab/avoid_gitlab_instance_checks.rb
+++ b/rubocop/cop/gitlab/avoid_gitlab_instance_checks.rb
@@ -17,7 +17,7 @@ module RuboCop
# end
#
# # good
- # if Gitlab::Saas.feature_available?('purchases/additional_minutes')
+ # if Gitlab::Saas.feature_available?(:purchases_additional_minutes)
# Ci::Runner::FORM_EDITABLE + Ci::Runner::MINUTES_COST_FACTOR_FIELDS
# else
# Ci::Runner::FORM_EDITABLE
diff --git a/rubocop/cop/gitlab/doc_url.rb b/rubocop/cop/gitlab/doc_url.rb
index 41a1c2f8b36..79e25142f26 100644
--- a/rubocop/cop/gitlab/doc_url.rb
+++ b/rubocop/cop/gitlab/doc_url.rb
@@ -19,7 +19,7 @@ module RuboCop
include RangeHelp
MSG = 'Use `#help_page_url` instead of directly including link. ' \
- 'See https://docs.gitlab.com/ee/development/documentation/#linking-to-help-in-ruby.'
+ 'See https://docs.gitlab.com/ee/development/documentation/help#linking-to-help.'
DOCS_URL_REGEXP = %r{https://docs.gitlab.com/ee/[\w#%./-]+}
diff --git a/rubocop/cop/gitlab/mark_used_feature_flags.rb b/rubocop/cop/gitlab/mark_used_feature_flags.rb
index 65a1731fc28..4c6cc6c6778 100644
--- a/rubocop/cop/gitlab/mark_used_feature_flags.rb
+++ b/rubocop/cop/gitlab/mark_used_feature_flags.rb
@@ -16,9 +16,6 @@ module RuboCop
EXPERIMENT_METHODS = %i[
experiment
].freeze
- RUGGED_METHODS = %i[
- use_rugged?
- ].freeze
WORKER_METHODS = %i[
data_consistency
deduplicate
@@ -28,7 +25,7 @@ module RuboCop
push_force_frontend_feature_flag
limit_feature_flag=
limit_feature_flag_for_override=
- ].freeze + EXPERIMENT_METHODS + RUGGED_METHODS + WORKER_METHODS
+ ].freeze + EXPERIMENT_METHODS + WORKER_METHODS
RESTRICT_ON_SEND = FEATURE_METHODS + SELF_METHODS
@@ -119,7 +116,7 @@ module RuboCop
pair.key.value == :feature_flag
end&.value
else
- arg_index = rugged_method?(node) ? 3 : 2
+ arg_index = 2
node.children[arg_index]
end
@@ -156,10 +153,6 @@ module RuboCop
class_caller(node) == "Feature::Gitaly"
end
- def rugged_method?(node)
- RUGGED_METHODS.include?(method_name(node))
- end
-
def feature_method?(node)
FEATURE_METHODS.include?(method_name(node)) && (caller_is_feature?(node) || caller_is_feature_gitaly?(node))
end
diff --git a/rubocop/cop/migration/migration_with_milestone.rb b/rubocop/cop/migration/migration_with_milestone.rb
new file mode 100644
index 00000000000..d06850dee5b
--- /dev/null
+++ b/rubocop/cop/migration/migration_with_milestone.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module RuboCop
+ module Cop
+ module Migration
+ # Cop that checks that any 2.2+ migration is incldued with a call to 'milestone'
+ class MigrationWithMilestone < RuboCop::Cop::Base
+ MSG = 'Version 2.2 migrations must specify a milestone.'
+
+ def_node_matcher :gitlab_migration?, <<-PATTERN
+ (class (const nil? _) (send (const (const (const nil? :Gitlab) :Database) :Migration) :[] (float $_)) ...)
+ PATTERN
+
+ def_node_search :milestone_call?, '(begin <(send nil? :milestone (str $_)) ...>)'
+
+ def on_class(node)
+ version = gitlab_migration?(node)
+ return unless version && version >= 2.2
+
+ body_node = node.body
+ return unless body_node
+
+ add_offense(node, message: MSG) unless milestone_call?(body_node)
+ end
+ end
+ end
+ end
+end
diff --git a/rubocop/cop/migration/prevent_index_creation.rb b/rubocop/cop/migration/prevent_index_creation.rb
index aa0ab7b1e50..0a1dccccbc8 100644
--- a/rubocop/cop/migration/prevent_index_creation.rb
+++ b/rubocop/cop/migration/prevent_index_creation.rb
@@ -8,11 +8,11 @@ module RuboCop
class PreventIndexCreation < RuboCop::Cop::Base
include MigrationHelpers
- FORBIDDEN_TABLES = %i[ci_builds namespaces].freeze
+ FORBIDDEN_TABLES = %i[ci_builds namespaces projects users].freeze
MSG = "Adding new index to #{FORBIDDEN_TABLES.join(", ")} is forbidden. " \
"For `ci_builds` see https://gitlab.com/gitlab-org/gitlab/-/issues/332886, " \
- "for `namespaces` see https://gitlab.com/groups/gitlab-org/-/epics/11543".freeze
+ "for `namespaces`, `projects`, and `users` see https://gitlab.com/groups/gitlab-org/-/epics/11543".freeze
def on_new_investigation
super
diff --git a/rubocop/cop/migration/unfinished_dependencies.rb b/rubocop/cop/migration/unfinished_dependencies.rb
index 1e0741c8411..56ba7d405c5 100644
--- a/rubocop/cop/migration/unfinished_dependencies.rb
+++ b/rubocop/cop/migration/unfinished_dependencies.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require_relative '../../migration_helpers'
-require_relative '../../batched_background_migrations'
+require_relative '../../batched_background_migrations_dictionary'
module RuboCop
module Cop
@@ -43,7 +43,7 @@ module RuboCop
private
def fetch_finalized_by(queued_migration_version)
- BatchedBackgroundMigrations.new(queued_migration_version).finalized_by
+ BatchedBackgroundMigrationsDictionary.new(queued_migration_version).finalized_by
end
end
end
diff --git a/rubocop/cop/migration/with_lock_retries_disallowed_method.rb b/rubocop/cop/migration/with_lock_retries_disallowed_method.rb
index 1b0d5ed9324..e019e5bd0cf 100644
--- a/rubocop/cop/migration/with_lock_retries_disallowed_method.rb
+++ b/rubocop/cop/migration/with_lock_retries_disallowed_method.rb
@@ -29,6 +29,14 @@ module RuboCop
index_exists?
column_exists?
create_trigger_to_sync_tables
+ lock_tables
+ swap_columns
+ swap_columns_default
+ swap_foreign_keys
+ swap_indexes
+ reset_trigger_function
+ cleanup_conversion_of_integer_to_bigint
+ revert_initialize_conversion_of_integer_to_bigint
].sort.freeze
MSG = "The method is not allowed to be called within the `with_lock_retries` block, the only allowed methods are: #{ALLOWED_MIGRATION_METHODS.join(', ')}".freeze
diff --git a/rubocop/cop/style/inline_disable_annotation.rb b/rubocop/cop/style/inline_disable_annotation.rb
new file mode 100644
index 00000000000..c3db541fe82
--- /dev/null
+++ b/rubocop/cop/style/inline_disable_annotation.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+module RuboCop
+ module Cop
+ module Style
+ # rubocop:disable Lint/RedundantCopDisableDirective -- For examples
+ # Checks that rubocop inline disabling is formatted according
+ # to guidelines.
+ # See: https://docs.gitlab.com/ee/development/rubocop_development_guide.html#disabling-rules-inline,
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/428762
+ #
+ # # bad
+ # # rubocop:disable Some/Cop, Another/Cop
+ #
+ # # good
+ # # rubocop:disable Some/Cop, Another/Cop -- Some reason
+ #
+ # rubocop:enable Lint/RedundantCopDisableDirective
+ class InlineDisableAnnotation < RuboCop::Cop::Base
+ include RangeHelp
+
+ COP_DISABLE = '#\s*rubocop\s*:\s*(?:disable|todo)\s+'
+ BAD_DISABLE = %r{\A(?<line>(?<disabling>#{COP_DISABLE}(?:[\w/]+(?:\s*,\s*[\w/]+)*))\s*)(?!.*\s*--\s\S).*}
+ COP_DISABLE_LINE = /\A(?<line>#{COP_DISABLE}.*)\Z/
+ MSG = <<~MESSAGE
+ Inline disabling a cop needs to follow the format of `%{disable} -- Some reason`.
+ See https://docs.gitlab.com/ee/development/rubocop_development_guide.html#disabling-rules-inline.
+ MESSAGE
+
+ def on_new_investigation
+ processed_source.comments.each do |comment|
+ candidate_match = COP_DISABLE_LINE.match(comment.text)
+ # Pre-filter to ensure we are on a comment that is for a rubocop disabling
+ next unless candidate_match
+
+ bad_match = BAD_DISABLE.match(comment.text)
+ # Only the badly formatted lines make it past this.
+ next unless bad_match
+
+ add_offense(
+ source_range(
+ processed_source.buffer, comment.loc.line, comment.loc.column, candidate_match[:line].length
+ ),
+ message: format(MSG, disable: bad_match[:disabling])
+ )
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/rubocop/rubocop-code_reuse.yml b/rubocop/rubocop-code_reuse.yml
index f96de5caf99..2bd3339368d 100644
--- a/rubocop/rubocop-code_reuse.yml
+++ b/rubocop/rubocop-code_reuse.yml
@@ -24,6 +24,7 @@ CodeReuse/ActiveRecord:
- danger/**/*.rb
- lib/backup/**/*.rb
- lib/banzai/**/*.rb
+ - lib/click_house/migration_support/**/*.rb
- lib/gitlab/background_migration/**/*.rb
- lib/gitlab/cycle_analytics/**/*.rb
- lib/gitlab/counters/**/*.rb
diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb
index 42882966b85..708be988f3a 100644
--- a/rubocop/rubocop.rb
+++ b/rubocop/rubocop.rb
@@ -1,7 +1,11 @@
# rubocop:disable Naming/FileName
# frozen_string_literal: true
+# Load ActiveSupport to ensure that core extensions like `Enumerable#exclude?`
+# are available in cop rules like `Performance/CollectionLiteralInLoop`.
+require 'active_support/all'
+
# Auto-require all cops under `rubocop/cop/**/*.rb`
-Dir[File.join(__dir__, 'cop', '**', '*.rb')].sort.each { |file| require file }
+Dir[File.join(__dir__, 'cop', '**', '*.rb')].each { |file| require file }
# rubocop:enable Naming/FileName
diff --git a/scripts/database/query_analyzers.rb b/scripts/database/query_analyzers.rb
new file mode 100644
index 00000000000..390851df81a
--- /dev/null
+++ b/scripts/database/query_analyzers.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+class Database
+ class QueryAnalyzers
+ attr_reader :analyzers
+
+ def initialize
+ @analyzers = ObjectSpace.each_object(::Class).select { |c| c < Base }.map(&:new)
+ end
+
+ def analyze(query)
+ analyzers.each { |analyzer| analyzer.analyze(query) }
+ end
+
+ def save!
+ analyzers.each(&:save!)
+ end
+ end
+end
+
+Dir[File.join(File.expand_path('query_analyzers', __dir__), '*.rb')].each do |plugin|
+ require plugin
+end
diff --git a/scripts/database/query_analyzers/base.rb b/scripts/database/query_analyzers/base.rb
new file mode 100644
index 00000000000..4bf47a32da1
--- /dev/null
+++ b/scripts/database/query_analyzers/base.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require 'json'
+require 'zlib'
+
+class Database
+ class QueryAnalyzers
+ class Base
+ attr_accessor :output
+
+ def initialize
+ @output = {}
+ end
+
+ def filename
+ self.class
+ end
+
+ def analyze(query); end
+
+ def save!
+ Zlib::GzipWriter.open(output_path(filename)) do |file|
+ JSON.dump(output, file)
+ end
+ end
+
+ private
+
+ def output_path(filename)
+ File.join(
+ File.dirname(ENV['RSPEC_AUTO_EXPLAIN_LOG_PATH']),
+ "#{filename}.gz"
+ )
+ end
+ end
+ end
+end
diff --git a/scripts/database/query_analyzers/multiple_partition_scan_detector.rb b/scripts/database/query_analyzers/multiple_partition_scan_detector.rb
new file mode 100644
index 00000000000..3afce51f87a
--- /dev/null
+++ b/scripts/database/query_analyzers/multiple_partition_scan_detector.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require_relative 'base'
+
+class Database
+ class QueryAnalyzers
+ class MultiplePartitionScanDetector < Database::QueryAnalyzers::Base
+ TABLES = %w[
+ p_ci_builds p_ci_builds_metadata p_ci_job_annotations p_ci_runner_machine_builds
+ ].freeze
+
+ def analyze(query)
+ super
+
+ TABLES.each do |table_name|
+ if query['query'].include?(table_name) && query['plan'].to_s.include?('"Subplans Removed"=>0')
+ (output[table_name] ||= []) << query
+ end
+ end
+ end
+
+ def save!
+ TABLES.each do |table_name|
+ next unless output[table_name]
+
+ Zlib::GzipWriter.open(output_path("#{table_name}_multiple_partition_scans.ndjson")) do |file|
+ output[table_name].each do |query|
+ file.puts(JSON.generate(query))
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/scripts/duo_chat/reporter.rb b/scripts/duo_chat/reporter.rb
new file mode 100755
index 00000000000..686a49164a7
--- /dev/null
+++ b/scripts/duo_chat/reporter.rb
@@ -0,0 +1,233 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+require 'gitlab'
+require 'json'
+
+class Reporter
+ IDENTIFIABLE_NOTE_TAG = 'gitlab-org/ai-powered/ai-framework:duo-chat-qa-evaluation-'
+
+ GRADE_TO_EMOJI_MAPPING = {
+ correct: ":white_check_mark:",
+ incorrect: ":x:",
+ unexpected: ":warning:"
+ }.freeze
+
+ def run
+ merge_request_iid = ENV['CI_MERGE_REQUEST_IID']
+ ci_project_id = ENV['CI_PROJECT_ID']
+
+ puts "Saving #{artifact_path}"
+ File.write(artifact_path, report_note)
+
+ # Look for an existing note
+ report_notes = com_gitlab_client
+ .merge_request_notes(ci_project_id, merge_request_iid)
+ .auto_paginate
+ .select do |note|
+ note.body.include? note_identifier_tag
+ end
+
+ note = report_notes.max_by { |note| Time.parse(note.created_at) }
+
+ if note && note.type != 'DiscussionNote'
+ # The latest note has not led to a discussion. Update it.
+ com_gitlab_client.edit_merge_request_note(ci_project_id, merge_request_iid, note.id, report_note)
+
+ puts "Updated comment."
+ else
+ # This is the first note or the latest note has been discussed on the MR.
+ # Don't update, create new note instead.
+ com_gitlab_client.create_merge_request_note(ci_project_id, merge_request_iid, report_note)
+
+ puts "Posted comment."
+ end
+ end
+
+ private
+
+ def report_filename
+ "#{ENV['DUO_RSPEC']}.md"
+ end
+
+ def artifact_path
+ File.join(ENV['CI_PROJECT_DIR'], report_filename)
+ end
+
+ def note_identifier_tag
+ "#{IDENTIFIABLE_NOTE_TAG}#{ENV['DUO_RSPEC']}"
+ end
+
+ def com_gitlab_client
+ @com_gitlab_client ||= Gitlab.client(
+ endpoint: "https://gitlab.com/api/v4",
+ private_token: ENV['PROJECT_TOKEN_FOR_CI_SCRIPTS_API_USAGE']
+ )
+ end
+
+ def report_note
+ report = <<~MARKDOWN
+ <!-- #{note_identifier_tag} -->
+
+ ## GitLab Duo Chat QA evaluation
+
+ Report generated for "#{ENV['CI_JOB_NAME']}". This report is generated and refreshed automatically. Do not edit.
+
+ LLMs have been asked to evaluate GitLab Duo Chat's answers.
+
+ :white_check_mark: : LLM evaluated the answer as `CORRECT`.
+
+ :x: : LLM evaluated the answer as `INCORRECT`.
+
+ :warning: : LLM did not evaluate correctly or the evaluation request might have failed.
+
+ ### Summary
+
+ - The total number of evaluations: #{summary_numbers[:total]}
+
+ - The number of evaluations in which all LLMs graded `CORRECT`: #{summary_numbers[:correct]} (#{summary_numbers[:correct_ratio]}%)
+
+ - Note: if an evaluation request failed or its response was not parsable, it was ignored. For example, :white_check_mark: :warning: would count as `CORRECT`.
+
+ - The number of evaluations in which all LLMs graded `INCORRECT`: #{summary_numbers[:incorrect]} (#{summary_numbers[:incorrect_ratio]}%)
+
+ - Note: if an evaluation request failed or its response was not parsable, it was ignored. For example, :x: :warning: would count as `INCORRECT`.
+
+ - The number of evaluations in which LLMs disagreed: #{summary_numbers[:disagreed]} (#{summary_numbers[:disagreed_ratio]}%)
+
+
+ ### Evaluations
+
+ #{eval_content}
+
+
+ MARKDOWN
+
+ if report.length > 1000000
+ return <<~MARKDOWN
+ <!-- #{note_identifier_tag} -->
+
+ ## GitLab Duo Chat QA evaluation
+
+ Report generated for "#{ENV['CI_JOB_NAME']}". This report is generated and refreshed automatically. Do not edit.
+
+ **:warning: the evaluation report is too long (> `1000000`) and cannot be posted as a note.**
+
+ Please check out the artifact for the CI job "#{ENV['CI_JOB_NAME']}":
+
+ https://gitlab.com/gitlab-org/gitlab/-/jobs/#{ENV['CI_JOB_ID']}/artifacts/file/#{report_filename}
+
+ MARKDOWN
+ end
+
+ report
+ end
+
+ def report_data
+ @report_data ||= Dir[File.join(ENV['CI_PROJECT_DIR'], "tmp/duo_chat/qa*.json")]
+ .map { |file| JSON.parse(File.read(file)) }
+ end
+
+ def eval_content
+ report_data
+ .sort_by { |a| a["question"] }
+ .map do |data|
+ <<~MARKDOWN
+ <details>
+
+ <summary>
+
+ #{correctness_indicator(data)}
+
+ `"#{data['question']}"`
+
+ (context: `#{data['resource']}`)
+
+ </summary>
+
+ #### Resource
+
+ `#{data['resource']}`
+
+ #### Answer
+
+ #{data['answer']}
+
+ #### LLM Evaluation
+
+ Tools used: #{data['tools_used']}
+
+ #{evalutions(data)}
+
+
+ </details>
+
+ MARKDOWN
+ end
+ .join
+ end
+
+ def summary_numbers
+ @graded_evaluations ||= report_data.map { |data| data["evaluations"].map { |eval| parse_grade(eval) } }
+
+ total = @graded_evaluations.size
+ correct = @graded_evaluations.count { |grades| !(grades.include? :incorrect) }
+ incorrect = @graded_evaluations.count { |grades| !(grades.include? :correct) }
+ disagreed = @graded_evaluations.count { |grades| (grades.include? :correct) && (grades.include? :incorrect) }
+
+ {
+ total: total,
+ correct: correct,
+ correct_ratio: (correct.to_f / total * 100).round(1),
+ incorrect: incorrect,
+ incorrect_ratio: (incorrect.to_f / total * 100).round(1),
+ disagreed: disagreed,
+ disagreed_ratio: (disagreed.to_f / total * 100).round(1)
+ }
+ end
+
+ def parse_grade(eval)
+ return :correct if eval["response"].match?(/Grade: CORRECT/i)
+ return :incorrect if eval["response"].match?(/Grade: INCORRECT/i)
+
+ # If the LLM's evaluation includes neither CORRECT nor CORRECT, flag it.
+ :unexpected
+ end
+
+ def correctness_indicator(data)
+ data["evaluations"].map { |eval| parse_grade(eval) }.map { |grade| GRADE_TO_EMOJI_MAPPING[grade] }.join(' ')
+ end
+
+ def evalutions(data)
+ rows = data["evaluations"].map do |eval|
+ grade = parse_grade(eval)
+
+ <<~MARKDOWN
+ <tr>
+ <td>#{eval['model']}</td>
+ <td>
+ #{GRADE_TO_EMOJI_MAPPING[grade]}
+ </td>
+ <td>
+ #{eval['response']}
+ </td
+ </tr>
+
+ MARKDOWN
+ end
+ .join
+
+ <<~MARKDOWN
+ <table>
+ <tr>
+ <td>Model</td>
+ <td>Grade</td>
+ <td>Details</td>
+ </tr>
+ #{rows}
+ </table>
+ MARKDOWN
+ end
+end
+
+Reporter.new.run
diff --git a/scripts/frontend/create_jsconfig.js b/scripts/frontend/create_jsconfig.js
index be95c5cb2d3..28674aacd45 100755
--- a/scripts/frontend/create_jsconfig.js
+++ b/scripts/frontend/create_jsconfig.js
@@ -31,18 +31,8 @@ async function createJsConfig() {
const webpackConfig = require('../../config/webpack.config');
// Aliases
- const paths = {
- // NOTE: Sentry is exposed via a wrapper, which has a limited API.
- '@sentry/browser': [
- path.relative(PATH_PROJECT_ROOT, 'app/assets/javascripts/sentry/sentry_browser_wrapper.js'),
- ],
- };
- const WEBPACK_ALIAS_EXCEPTIONS = [
- 'jquery$',
- '@gitlab/svgs/dist/icons.svg',
- '@apollo/client$',
- '@sentry/browser$',
- ];
+ const paths = {};
+ const WEBPACK_ALIAS_EXCEPTIONS = ['jquery$', '@gitlab/svgs/dist/icons.svg', '@apollo/client$'];
Object.entries(webpackConfig.resolve.alias)
.filter(([key]) => !WEBPACK_ALIAS_EXCEPTIONS.includes(key))
.forEach(([key, value]) => {
diff --git a/scripts/frontend/startup_css/clean_css.js b/scripts/frontend/startup_css/clean_css.js
deleted file mode 100644
index 67a0453e816..00000000000
--- a/scripts/frontend/startup_css/clean_css.js
+++ /dev/null
@@ -1,83 +0,0 @@
-const { memoize, isString, isRegExp } = require('lodash');
-const { parse } = require('postcss');
-const { CSS_TO_REMOVE } = require('./constants');
-
-const getSelectorRemoveTesters = memoize(() =>
- CSS_TO_REMOVE.map((x) => {
- if (isString(x)) {
- return (selector) => x === selector;
- }
- if (isRegExp(x)) {
- return (selector) => x.test(selector);
- }
-
- throw new Error(`Unexpected type in CSS_TO_REMOVE content "${x}". Expected String or RegExp.`);
- }),
-);
-
-const getRemoveTesters = memoize(() => {
- const selectorTesters = getSelectorRemoveTesters();
-
- // These are mostly carried over from the previous project
- // https://gitlab.com/gitlab-org/frontend/gitlab-css-statistics/-/blob/2aa00af25dba08fc71081c77206f45efe817ea4b/lib/gl_startup_extract.js
- return [
- (node) => node.type === 'comment',
- (node) =>
- node.type === 'atrule' &&
- (node.params === 'print' ||
- node.params === 'prefers-reduced-motion: reduce' ||
- node.name === 'keyframe' ||
- node.name === 'charset'),
- (node) => node.selector && node.selectors && !node.selectors.length,
- (node) => node.selector && selectorTesters.some((fn) => fn(node.selector)),
- (node) =>
- node.type === 'decl' &&
- (node.prop === 'transition' ||
- node.prop.indexOf('-webkit-') > -1 ||
- node.prop.indexOf('-ms-') > -1),
- ];
-});
-
-const getNodesToRemove = (nodes) => {
- const removeTesters = getRemoveTesters();
- const remNodes = [];
-
- nodes.forEach((node) => {
- if (removeTesters.some((fn) => fn(node))) {
- remNodes.push(node);
- } else if (node.nodes?.length) {
- remNodes.push(...getNodesToRemove(node.nodes));
- }
- });
-
- return remNodes;
-};
-
-const getEmptyNodesToRemove = (nodes) =>
- nodes
- .filter((node) => node.nodes)
- .reduce((acc, node) => {
- if (node.nodes.length) {
- acc.push(...getEmptyNodesToRemove(node.nodes));
- } else {
- acc.push(node);
- }
-
- return acc;
- }, []);
-
-const cleanCSS = (css) => {
- const cssRoot = parse(css);
-
- getNodesToRemove(cssRoot.nodes).forEach((node) => {
- node.remove();
- });
-
- getEmptyNodesToRemove(cssRoot.nodes).forEach((node) => {
- node.remove();
- });
-
- return cssRoot.toResult().css;
-};
-
-module.exports = { cleanCSS };
diff --git a/scripts/frontend/startup_css/constants.js b/scripts/frontend/startup_css/constants.js
deleted file mode 100644
index bf9774daea5..00000000000
--- a/scripts/frontend/startup_css/constants.js
+++ /dev/null
@@ -1,108 +0,0 @@
-const path = require('path');
-const IS_EE = require('../../../config/helpers/is_ee_env');
-
-// controls --------------------------------------------------------------------
-const HTML_TO_REMOVE = [
- 'style',
- 'script',
- 'link[rel="stylesheet"]',
- '.content-wrapper',
- '#js-peek',
- '.modal',
- '.feature-highlight',
- // The user has to open up the responsive nav, so we don't need it on load
- '.top-nav-responsive',
- // We don't want to capture all the children of a dropdown-menu
- '.dropdown-menu',
-];
-const CSS_TO_REMOVE = [
- '.tooltip',
- '.tooltip.show',
- '.fa',
- '.gl-accessibility:focus',
- '.toasted-container',
- 'body .toasted-container.bottom-left',
- '.popover',
- '.with-performance-bar .navbar-gitlab',
- '.text-secondary',
- /\.feature-highlight-popover-content/,
- /\.commit/,
- /\.md/,
- /\.with-performance-bar/,
-];
-const APPLICATION_CSS_PREFIX = 'application';
-const APPLICATION_DARK_CSS_PREFIX = 'application_dark';
-const UTILITIES_CSS_PREFIX = 'application_utilities';
-const UTILITIES_DARK_CSS_PREFIX = 'application_utilities_dark';
-
-// paths -----------------------------------------------------------------------
-const ROOT = path.resolve(__dirname, '../../..');
-const ROOT_RAILS = IS_EE ? path.join(ROOT, 'ee') : ROOT;
-const FIXTURES_FOLDER_NAME = IS_EE ? 'fixtures-ee' : 'fixtures';
-const FIXTURES_ROOT = path.join(ROOT, 'tmp/tests/frontend', FIXTURES_FOLDER_NAME);
-const PATH_SIGNIN_HTML = path.join(FIXTURES_ROOT, 'startup_css/sign-in.html');
-const PATH_SIGNIN_OLD_HTML = path.join(FIXTURES_ROOT, 'startup_css/sign-in-old.html');
-const PATH_ASSETS = path.join(ROOT, 'tmp/startup_css_assets');
-const PATH_STARTUP_SCSS = path.join(ROOT_RAILS, 'app/assets/stylesheets/startup');
-
-// helpers ---------------------------------------------------------------------
-const createMainOutput = ({ outFile, cssKeys, type }) => ({
- outFile,
- htmlPaths: [
- path.join(FIXTURES_ROOT, `startup_css/project-${type}.html`),
- path.join(FIXTURES_ROOT, `startup_css/project-${type}-signed-out.html`),
- path.join(FIXTURES_ROOT, `startup_css/project-${type}-super-sidebar.html`),
- ],
- cssKeys,
- purgeOptions: {
- safelist: {
- standard: [
- 'page-with-super-sidebar',
- 'page-with-super-sidebar-collapsed',
- 'page-with-icon-sidebar',
- 'sidebar-collapsed-desktop',
- // We want to include the root dropdown-menu style since it should be hidden by default
- 'dropdown-menu',
- ],
- // We want to include the identicon backgrounds
- greedy: [/^bg[0-9]$/],
- },
- },
-});
-
-const OUTPUTS = [
- createMainOutput({
- type: 'general',
- outFile: 'startup-general',
- cssKeys: [APPLICATION_CSS_PREFIX, UTILITIES_CSS_PREFIX],
- }),
- createMainOutput({
- type: 'dark',
- outFile: 'startup-dark',
- cssKeys: [APPLICATION_DARK_CSS_PREFIX, UTILITIES_DARK_CSS_PREFIX],
- }),
- {
- outFile: 'startup-signin',
- htmlPaths: [PATH_SIGNIN_HTML, PATH_SIGNIN_OLD_HTML],
- cssKeys: [APPLICATION_CSS_PREFIX, UTILITIES_CSS_PREFIX],
- purgeOptions: {
- safelist: {
- standard: ['fieldset', 'hidden'],
- deep: [/login-page$/],
- },
- },
- },
-];
-
-module.exports = {
- HTML_TO_REMOVE,
- CSS_TO_REMOVE,
- APPLICATION_CSS_PREFIX,
- APPLICATION_DARK_CSS_PREFIX,
- UTILITIES_CSS_PREFIX,
- UTILITIES_DARK_CSS_PREFIX,
- ROOT,
- PATH_ASSETS,
- PATH_STARTUP_SCSS,
- OUTPUTS,
-};
diff --git a/scripts/frontend/startup_css/get_css_path.js b/scripts/frontend/startup_css/get_css_path.js
deleted file mode 100644
index 54078cf3149..00000000000
--- a/scripts/frontend/startup_css/get_css_path.js
+++ /dev/null
@@ -1,22 +0,0 @@
-const fs = require('fs');
-const path = require('path');
-const { memoize } = require('lodash');
-const { PATH_ASSETS } = require('./constants');
-const { die } = require('./utils');
-
-const listAssetsDir = memoize(() => fs.readdirSync(PATH_ASSETS));
-
-const getCSSPath = (prefix) => {
- const matcher = new RegExp(`^${prefix}-[^-]+\\.css$`);
- const cssPath = listAssetsDir().find((x) => matcher.test(x));
-
- if (!cssPath) {
- die(
- `Could not find the CSS asset matching "${prefix}". Have you run "scripts/frontend/startup_css/setup.sh"?`,
- );
- }
-
- return path.join(PATH_ASSETS, cssPath);
-};
-
-module.exports = { getCSSPath };
diff --git a/scripts/frontend/startup_css/get_startup_css.js b/scripts/frontend/startup_css/get_startup_css.js
deleted file mode 100644
index 2c8c3b4e321..00000000000
--- a/scripts/frontend/startup_css/get_startup_css.js
+++ /dev/null
@@ -1,71 +0,0 @@
-const fs = require('fs');
-const cheerio = require('cheerio');
-const { mergeWith, isArray } = require('lodash');
-const { PurgeCSS } = require('purgecss');
-const purgeHtml = require('purgecss-from-html');
-const { cleanCSS } = require('./clean_css');
-const { HTML_TO_REMOVE } = require('./constants');
-const { die } = require('./utils');
-
-const cleanHtml = (html) => {
- const $ = cheerio.load(html);
-
- HTML_TO_REMOVE.forEach((selector) => {
- $(selector).remove();
- });
-
- return $.html();
-};
-
-const mergePurgeCSSOptions = (...options) =>
- mergeWith(...options, (objValue, srcValue) => {
- if (isArray(objValue)) {
- return objValue.concat(srcValue);
- }
-
- return undefined;
- });
-
-const getStartupCSS = async ({ htmlPaths, cssPaths, purgeOptions }) => {
- const content = htmlPaths.map((htmlPath) => {
- if (!fs.existsSync(htmlPath)) {
- die(
- `Could not find fixture "${htmlPath}". Have you run the fixtures? (bundle exec rspec spec/frontend/fixtures/startup_css.rb)`,
- );
- }
-
- const rawHtml = fs.readFileSync(htmlPath);
- const html = cleanHtml(rawHtml);
-
- return { raw: html, extension: 'html' };
- });
-
- const purgeCSSResult = await new PurgeCSS().purge({
- content,
- css: cssPaths,
- ...mergePurgeCSSOptions(
- {
- fontFace: true,
- variables: true,
- keyframes: true,
- blocklist: [/:hover/, /:focus/, /-webkit-/, /-moz-focusring-/, /-ms-expand/],
- safelist: {
- standard: ['brand-header-logo'],
- },
- // By default, PurgeCSS ignores special characters, but our utilities use "!"
- defaultExtractor: (x) => x.match(/[\w-!]+/g),
- extractors: [
- {
- extractor: purgeHtml,
- extensions: ['html'],
- },
- ],
- },
- purgeOptions,
- ),
- });
-
- return purgeCSSResult.map(({ css }) => cleanCSS(css)).join('\n');
-};
-
-module.exports = { getStartupCSS };
diff --git a/scripts/frontend/startup_css/main.js b/scripts/frontend/startup_css/main.js
deleted file mode 100644
index 1e8dcbebae2..00000000000
--- a/scripts/frontend/startup_css/main.js
+++ /dev/null
@@ -1,60 +0,0 @@
-const { memoize } = require('lodash');
-const { OUTPUTS } = require('./constants');
-const { getCSSPath } = require('./get_css_path');
-const { getStartupCSS } = require('./get_startup_css');
-const { log, die } = require('./utils');
-const { writeStartupSCSS } = require('./write_startup_scss');
-
-const memoizedCSSPath = memoize(getCSSPath);
-
-const runTask = async ({ outFile, htmlPaths, cssKeys, purgeOptions = {} }) => {
- try {
- log(`Generating startup CSS for HTML files: ${htmlPaths}`);
- const generalCSS = await getStartupCSS({
- htmlPaths,
- cssPaths: cssKeys.map(memoizedCSSPath),
- purgeOptions,
- });
-
- log(`Writing to startup CSS...`);
- const startupCSSPath = writeStartupSCSS(outFile, generalCSS);
- log(`Finished writing to ${startupCSSPath}`);
-
- return {
- success: true,
- outFile,
- };
- } catch (e) {
- log(`ERROR! Unexpected error occurred while generating startup CSS for: ${outFile}`);
- log(e);
-
- return {
- success: false,
- outFile,
- };
- }
-};
-
-const main = async () => {
- const result = await Promise.all(OUTPUTS.map(runTask));
- const fullSuccess = result.every((x) => x.success);
-
- log('RESULTS:');
- log('--------');
-
- result.forEach(({ success, outFile }) => {
- const status = success ? '✓' : 'ⅹ';
-
- log(`${status}: ${outFile}`);
- });
-
- log('--------');
-
- if (fullSuccess) {
- log('Done!');
- } else {
- die('Some tasks have failed');
- }
-};
-
-main();
diff --git a/scripts/frontend/startup_css/setup.sh b/scripts/frontend/startup_css/setup.sh
deleted file mode 100755
index 795799bd9fd..00000000000
--- a/scripts/frontend/startup_css/setup.sh
+++ /dev/null
@@ -1,76 +0,0 @@
-path_public_dir="public"
-path_tmp="tmp"
-path_dest="$path_tmp/startup_css_assets"
-glob_css_dest="$path_dest/application*.css"
-glob_css_src="$path_public_dir/assets/application*.css"
-should_clean=false
-
-should_force() {
- $1=="force"
-}
-
-has_dest_already() {
- find $glob_css_dest -quit
-}
-
-has_src_already() {
- find $glob_css_src -quit
-}
-
-compile_assets() {
- # We need to build the same test bundle that is built in CI
- RAILS_ENV=test bundle exec rake rake:assets:precompile
-}
-
-clean_assets() {
- bundle exec rake rake:assets:clobber
-}
-
-copy_assets() {
- rm -rf $path_dest
- mkdir $path_dest
- cp $glob_css_src $path_dest
-}
-
-echo "-----------------------------------------------------------"
-echo "If you are run into any issues with Startup CSS generation,"
-echo "please check out the feedback issue:"
-echo ""
-echo "https://gitlab.com/gitlab-org/gitlab/-/issues/331812"
-echo "-----------------------------------------------------------"
-
-if [ ! -e $path_public_dir ]; then
- echo "Could not find '$path_public_dir/'. This script must be run in the root directory of the gitlab project."
- exit 1
-fi
-
-if [ ! -e $path_tmp ]; then
- echo "Could not find '$path_tmp/'. This script must be run in the root directory of the gitlab project."
- exit 1
-fi
-
-if [ "$1" != "force" ] && has_dest_already; then
- echo "Already found assets for '$glob_css_dest'. Did you want to run this script with 'force' argument?"
- exit 0
-fi
-
-# If we are in CI, don't recompile things...
-if [ -n "$CI" ]; then
- if ! has_src_already; then
- echo "Could not find '$glob_css_src'. Expected these artifacts to be generated by CI pipeline."
- exit 1
- fi
-elif has_src_already; then
- echo "Found '$glob_css_src'. Skipping compile assets..."
-else
- echo "Starting compile assets process..."
- compile_assets
- should_clean=true
-fi
-
-copy_assets
-
-if $should_clean; then
- echo "Starting cleanup..."
- clean_assets
-fi
diff --git a/scripts/frontend/startup_css/startup_css_changed.sh b/scripts/frontend/startup_css/startup_css_changed.sh
deleted file mode 100755
index db6fb575d1d..00000000000
--- a/scripts/frontend/startup_css/startup_css_changed.sh
+++ /dev/null
@@ -1,51 +0,0 @@
-#!/bin/sh
-
-echo "-----------------------------------------------------------"
-echo "If you run into any issues with Startup CSS generation"
-echo "please check out the feedback issue:"
-echo ""
-echo "https://gitlab.com/gitlab-org/gitlab/-/issues/331812"
-echo "-----------------------------------------------------------"
-
-startup_glob="app/assets/stylesheets/startup*"
-
-if ! [ "$FOSS_ONLY" ]
-then
- startup_glob="*${startup_glob}"
-fi
-
-
-echo "Staging changes to '${startup_glob}' so we can check for untracked files..."
-git add "${startup_glob}"
-
-if [ -n "$(git diff HEAD --name-only -- "${startup_glob}")" ]; then
- diff=$(git diff HEAD -- "${startup_glob}")
- cat <<EOF
-
-Startup CSS changes detected!
-
-It looks like there have been recent changes which require
-regenerating the Startup CSS files.
-
-IMPORTANT:
-
- - If you are making changes to any Startup CSS file, it is very likely that
- **both** the CE and EE Startup CSS files will need to be updated.
- - Changing any Startup CSS file will trigger the "as-if-foss" job to also run.
-
-HOW TO FIX:
-
-To fix this job, consider one of the following options:
-
- 1. (Strongly recommended) Copy and apply the diff below:
- 2. Regenerate locally with "yarn run generate:startup_css".
- You may need to set "FOSS_ONLY=1" if you are trying to generate for CE.
-
------ start diff -----
-$diff
-
------ end diff -------
-EOF
-
- exit 1
-fi
diff --git a/scripts/frontend/startup_css/utils.js b/scripts/frontend/startup_css/utils.js
deleted file mode 100644
index 49ad201fb6b..00000000000
--- a/scripts/frontend/startup_css/utils.js
+++ /dev/null
@@ -1,8 +0,0 @@
-const die = (message) => {
- console.log(message);
- process.exit(1);
-};
-
-const log = (message) => console.error(`[gitlab.startup_css] ${message}`);
-
-module.exports = { die, log };
diff --git a/scripts/frontend/startup_css/write_startup_scss.js b/scripts/frontend/startup_css/write_startup_scss.js
deleted file mode 100644
index 245681bada3..00000000000
--- a/scripts/frontend/startup_css/write_startup_scss.js
+++ /dev/null
@@ -1,28 +0,0 @@
-const { writeFileSync } = require('fs');
-const path = require('path');
-const prettier = require('prettier');
-const { PATH_STARTUP_SCSS } = require('./constants');
-
-const buildFinalContent = (raw) => {
- const content = `// DO NOT EDIT! This is auto-generated from "yarn run generate:startup_css"
-// Please see the feedback issue for more details and help:
-// https://gitlab.com/gitlab-org/gitlab/-/issues/331812
-@charset "UTF-8";
-${raw}
-@import 'startup/cloaking';
-@include cloak-startup-scss(none);
-`;
-
- // We run prettier so that there is more determinism with the generated file.
- return prettier.format(content, { parser: 'scss' });
-};
-
-const writeStartupSCSS = (name, raw) => {
- const fullPath = path.join(PATH_STARTUP_SCSS, `${name}.scss`);
-
- writeFileSync(fullPath, buildFinalContent(raw));
-
- return fullPath;
-};
-
-module.exports = { writeStartupSCSS };
diff --git a/scripts/internal_events/monitor.rb b/scripts/internal_events/monitor.rb
index b2ef924eb11..e9ba1dbfbb7 100644
--- a/scripts/internal_events/monitor.rb
+++ b/scripts/internal_events/monitor.rb
@@ -40,7 +40,9 @@ def metric_definitions_from_args
end
def red(text)
- "\e[31m#{text}\e[0m"
+ @pastel ||= Pastel.new
+
+ @pastel.red(text)
end
def snowplow_data
@@ -133,6 +135,26 @@ def generate_metrics_table
)
end
+def render_screen(paused)
+ metrics_table = generate_metrics_table
+ events_table = generate_snowplow_table
+
+ print TTY::Cursor.clear_screen
+ print TTY::Cursor.move_to(0, 0)
+
+ puts "Updated at #{Time.current} #{'[PAUSED]' if paused}"
+ puts "Monitored events: #{ARGV.join(', ')}"
+ puts
+
+ puts metrics_table
+
+ puts events_table
+
+ puts
+ puts "Press \"p\" to toggle refresh. (It makes it easier to select and copy the tables)"
+ puts "Press \"q\" to quit"
+end
+
begin
snowplow_data
rescue Errno::ECONNREFUSED
@@ -142,29 +164,23 @@ rescue Errno::ECONNREFUSED
exit 1
end
-print "\e[?1049h" # Stores the original screen buffer
-print "\e[H" # Moves the cursor home
+reader = TTY::Reader.new
+paused = false
+
begin
loop do
- metrics_table = generate_metrics_table
- events_table = generate_snowplow_table
-
- print "\e[H" # Moves the cursor home
- print "\e[2J" # Clears the screen buffer
-
- puts "Updated at #{Time.current}"
- puts "Monitored events: #{ARGV.join(', ')}"
- puts
-
- puts metrics_table
+ case reader.read_keypress(nonblock: true)
+ when 'p'
+ paused = !paused
+ render_screen(paused)
+ when 'q'
+ break
+ end
- puts events_table
+ render_screen(paused) unless paused
sleep 1
end
rescue Interrupt
# Quietly shut down
-ensure
- print "\e[?1049l" # Restores the original screen buffer
- print "\e[H" # Moves the cursor home
end
diff --git a/scripts/lib/glfm/update_example_snapshots.rb b/scripts/lib/glfm/update_example_snapshots.rb
index 793f7521283..01760c23a68 100644
--- a/scripts/lib/glfm/update_example_snapshots.rb
+++ b/scripts/lib/glfm/update_example_snapshots.rb
@@ -289,7 +289,7 @@ module Glfm
wysiwyg_html_and_json_tempfile_path = Dir::Tmpname.create(WYSIWYG_HTML_AND_JSON_TEMPFILE_BASENAME) {}
ENV['OUTPUT_WYSIWYG_HTML_AND_JSON_TEMPFILE_PATH'] = wysiwyg_html_and_json_tempfile_path
- cmd = "yarn jest --testMatch '**/render_wysiwyg_html_and_json.js' #{__dir__}/render_wysiwyg_html_and_json.js"
+ cmd = "yarn jest:scripts #{__dir__}/render_wysiwyg_html_and_json.js"
run_external_cmd(cmd)
output("Reading generated WYSIWYG HTML and prosemirror JSON from tempfile " \
diff --git a/scripts/lint-doc.sh b/scripts/lint-doc.sh
index b16d9042f75..46d7159d71f 100755
--- a/scripts/lint-doc.sh
+++ b/scripts/lint-doc.sh
@@ -137,33 +137,39 @@ then
MD_DOC_PATH="$@"
# shellcheck disable=2059
printf "${COLOR_GREEN}INFO: List of files specified on command line. Running Markdownlint and Vale for only those files...${COLOR_RESET}\n"
-elif [ -z "${CI_MERGE_REQUEST_TARGET_BRANCH_SHA}" ]
+elif [ -n "${CI_MERGE_REQUEST_IID}" ]
then
- MD_DOC_PATH=${MD_DOC_PATH:-doc}
- # shellcheck disable=2059
- printf "${COLOR_GREEN}INFO: Merge request pipeline (detached) detected. Running Markdownlint and Vale on all files...${COLOR_RESET}\n"
-else
- MERGE_BASE=$(git merge-base "${CI_MERGE_REQUEST_TARGET_BRANCH_SHA}" "${CI_MERGE_REQUEST_SOURCE_BRANCH_SHA}")
- if git diff --diff-filter=d --name-only "${MERGE_BASE}..${CI_MERGE_REQUEST_SOURCE_BRANCH_SHA}" | grep -E "\.vale|\.markdownlint|lint-doc\.sh|docs\.gitlab-ci\.yml"
+ DOC_CHANGES_FILE=$(mktemp)
+ ruby -r './tooling/lib/tooling/find_changes' -e "Tooling::FindChanges.new(
+ from: :api,
+ changed_files_pathname: '${DOC_CHANGES_FILE}',
+ file_filter: ->(file) { !file['deleted_file'] && file['new_path'] =~ %r{doc/.*\.md|lint-doc\.sh|docs\.gitlab-ci\.yml} }
+ ).execute"
+ if grep -E "\.vale|\.markdownlint|lint-doc\.sh|docs\.gitlab-ci\.yml" < $DOC_CHANGES_FILE
then
MD_DOC_PATH=${MD_DOC_PATH:-doc}
# shellcheck disable=2059
printf "${COLOR_GREEN}INFO: Vale, Markdownlint, lint-doc.sh, or pipeline configuration changed. Testing all files.${COLOR_RESET}\n"
else
- MD_DOC_PATH=$(git diff --diff-filter=d --name-only "${MERGE_BASE}..${CI_MERGE_REQUEST_SOURCE_BRANCH_SHA}" -- 'doc/*.md')
+ MD_DOC_PATH=$(cat $DOC_CHANGES_FILE)
if [ -n "${MD_DOC_PATH}" ]
then
# shellcheck disable=2059
- printf "${COLOR_GREEN}INFO: Merged results pipeline detected. Testing only the following files:${COLOR_RESET}\n${MD_DOC_PATH}\n"
+ printf "${COLOR_GREEN}INFO: Merge request pipeline detected. Testing only the following files:${COLOR_RESET}\n${MD_DOC_PATH}\n"
fi
fi
+ rm $DOC_CHANGES_FILE
+else
+ MD_DOC_PATH=${MD_DOC_PATH:-doc}
+ # shellcheck disable=2059
+ printf "${COLOR_GREEN}INFO: No merge request pipeline detected. Running Markdownlint and Vale on all files...${COLOR_RESET}\n"
fi
function run_locally_or_in_container() {
local cmd=$1
local args=$2
local files=$3
- local registry_url="registry.gitlab.com/gitlab-org/gitlab-docs/lint-markdown:alpine-3.16-vale-2.22.0-markdownlint-0.32.2-markdownlint2-0.6.0"
+ local registry_url="registry.gitlab.com/gitlab-org/gitlab-docs/lint-markdown:alpine-3.18-vale-2.29.6-markdownlint-0.37.0-markdownlint2-0.10.0"
if hash "${cmd}" 2>/dev/null
then
@@ -201,7 +207,7 @@ printf "${COLOR_GREEN}INFO: Linting markdown style...${COLOR_RESET}\n"
if [ -z "${MD_DOC_PATH}" ]
then
# shellcheck disable=2059
- printf "${COLOR_GREEN}INFO: Merged results pipeline detected, but no markdown files found. Skipping.${COLOR_RESET}\n"
+ printf "${COLOR_GREEN}INFO: Merge request pipeline detected, but no markdown files found. Skipping.${COLOR_RESET}\n"
else
if ! yarn markdownlint --rules doc/.markdownlint/rules ${MD_DOC_PATH};
then
diff --git a/scripts/lint-rugged b/scripts/lint-rugged
deleted file mode 100755
index 73708b52772..00000000000
--- a/scripts/lint-rugged
+++ /dev/null
@@ -1,50 +0,0 @@
-#!/usr/bin/env ruby
-# frozen_string_literal: true
-
-ALLOWED = [
- # https://gitlab.com/gitlab-org/gitaly/issues/760
- 'lib/elasticsearch/git/repository.rb',
-
- # Needed to avoid using the git binary to validate a branch name
- 'lib/gitlab/git_ref_validator.rb',
-
- # Reverted Rugged calls due to Gitaly atop NFS performance
- # See https://docs.gitlab.com/ee/development/gitaly.html#legacy-rugged-code.
- 'lib/gitlab/git/rugged_impl/',
- 'lib/gitlab/gitaly_client/storage_settings.rb',
-
- # Needed to detect Rugged enabled: https://gitlab.com/gitlab-org/gitlab/issues/35371
- 'lib/gitlab/config_checker/puma_rugged_checker.rb',
-
- # Needed for GPG/X509 commit signature API
- #
- 'app/models/commit.rb',
- 'lib/api/entities/commit_signature.rb',
-
- # Needed for logging
- 'config/initializers/peek.rb',
- 'config/initializers/lograge.rb',
- 'lib/gitlab/grape_logging/loggers/perf_logger.rb',
- 'lib/gitlab/instrumentation_helper.rb',
- 'lib/gitlab/sidekiq_middleware/instrumentation_logger.rb',
- 'lib/gitlab/rugged_instrumentation.rb',
- 'lib/peek/views/rugged.rb'
-].freeze
-
-rugged_lines = IO.popen(%w[git grep -i -n rugged -- app config lib], &:read).lines
-rugged_lines = rugged_lines.select { |l| /^[^:]*\.rb:/ =~ l }
-rugged_lines = rugged_lines.reject { |l| l.start_with?(*ALLOWED) }
-rugged_lines = rugged_lines.reject { |l| /(include|prepend) Gitlab::Git::RuggedImpl/ =~ l }
-rugged_lines = rugged_lines.reject { |l| l.include?('Gitlab::ConfigChecker::PumaRuggedChecker.check') }
-rugged_lines = rugged_lines.reject do |line|
- code, _comment = line.split('# ', 2)
- code !~ /rugged/i
-end
-
-exit if rugged_lines.empty?
-
-puts "Using Rugged is only allowed in test and #{ALLOWED}\n\n"
-
-puts rugged_lines
-
-exit(false)
diff --git a/scripts/lint/ruby-metrics-abc.rb b/scripts/lint/ruby-metrics-abc.rb
new file mode 100755
index 00000000000..bea7be14a1e
--- /dev/null
+++ b/scripts/lint/ruby-metrics-abc.rb
@@ -0,0 +1,62 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+require "rubocop"
+
+# Shows ABC size of methods and blocks for passed files.
+#
+# See https://docs.rubocop.org/rubocop/cops_metrics.html#metricsabcsize
+#
+# Usage: scripts/ruby-metrics-abc.rb <ruby file> ...
+# Example: scripts/ruby-metrics-abc.rb app/models/project.rb app/models/user.rb
+
+module Tooling
+ class MetricsABC
+ extend RuboCop::AST::NodePattern::Macros
+ include RuboCop::AST::Traversal
+
+ def run(source)
+ version = RUBY_VERSION[/^(\d+\.\d+)/, 1].to_f
+ ast = RuboCop::AST::ProcessedSource.new(source, version).ast
+
+ walk(ast)
+ end
+
+ def on_def(node)
+ print_abc("def #{node.method_name}", node)
+ end
+
+ def on_defs(node)
+ print_abc("def self.#{node.method_name}", node)
+ end
+
+ def on_block(node)
+ return unless node.parent&.send_type?
+
+ method_name = node.parent.method_name
+ arguments = node.parent.arguments.select { |n| n.sym_type? || n.str_type? }.map(&:source)
+
+ print_abc("#{method_name}(#{arguments.join(', ')})", node)
+ end
+
+ private
+
+ def print_abc(prefix, node)
+ # https://www.rubydoc.info/gems/rubocop/RuboCop/Cop/Metrics/Utils/AbcSizeCalculator#calculate-instance_method
+ abc_score, abc_vector = RuboCop::Cop::Metrics::Utils::AbcSizeCalculator
+ .calculate(node, discount_repeated_attributes: true) # rubocop:disable CodeReuse/ActiveRecord -- This is not AR
+ puts format(" %d: %s: %.2f %s", node.first_line, prefix, abc_score, abc_vector)
+ end
+ end
+
+ if ARGV.empty?
+ puts "Usage: scripts/ruby-metrics-abc.rb <ruby file> ..."
+ puts " Example: scripts/ruby-metrics-abc.rb app/models/project.rb app/models/user.rb"
+ end
+
+ ARGV.each do |file|
+ puts "Checking #{file}:"
+
+ MetricsABC.new.run(File.read(file))
+ end
+end
diff --git a/scripts/merge-auto-explain-logs b/scripts/merge-auto-explain-logs
index 0dff4fb33f8..114afc580d0 100755
--- a/scripts/merge-auto-explain-logs
+++ b/scripts/merge-auto-explain-logs
@@ -1,14 +1,49 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
-require "set"
-require "json"
+require 'json'
+require 'set'
+require 'zlib'
+
+# Load query analyzers
+require_relative File.expand_path('database/query_analyzers.rb', __dir__)
+
+source = ENV['CI_MERGE_REQUEST_SOURCE_BRANCH_SHA']
+target = ENV['CI_MERGE_REQUEST_TARGET_BRANCH_SHA']
+log_path = ENV['RSPEC_AUTO_EXPLAIN_LOG_PATH']
+logs_path = File.dirname(log_path)
+
+exit(0) unless Dir.exist?(logs_path)
fingerprints = Set.new
+jobs = Set.new
+query_analyzers = Database::QueryAnalyzers.new
+
+JOB_NAME = %r{^(.*)\.\d+\.[^.]+\.ndjson\.gz$}
+
+Zlib::GzipWriter.open(log_path) do |log|
+ Dir[File.join(logs_path, '*.gz')].reject { |p| p == log_path }.each do |file|
+ job_name = File.basename(file)[JOB_NAME, 1]
+ Zlib::GzipReader.open(file) do |gz|
+ gz.each_line do |line|
+ query = JSON.parse(line)
+ fingerprint = query['fingerprint']
-ARGF.each_line do |line|
- fingerprint = JSON.parse(line)['fingerprint']
- $stdout.puts(line) && $stdout.flush if fingerprints.add?(fingerprint)
+ next unless fingerprints.add?(fingerprint)
+
+ query_analyzers.analyze(query)
+
+ jobs << job_name
+ query['job_name'] = job_name
+ log.puts(JSON.generate(query))
+ end
+ end
+
+ File.delete(file)
+ end
end
-warn("auto_explain log contains #{fingerprints.size} entries")
+query_analyzers.save!
+
+warn("auto_explain log contains #{fingerprints.size} entries from: #{jobs.to_a.sort.join(', ')}")
+warn("auto_explain comparison of #{target} to #{source}") if source && target
diff --git a/scripts/prepare_build.sh b/scripts/prepare_build.sh
index 36fe4a010a0..4f644812aa7 100644
--- a/scripts/prepare_build.sh
+++ b/scripts/prepare_build.sh
@@ -21,6 +21,7 @@ sed -i 's|url:.*$|url: redis://redis:6379|g' config/resque.yml
if [[ "$USE_REDIS_CLUSTER" != "false" ]] && [[ "$SETUP_DB" != "false" ]]; then
cp config/redis.yml.example config/redis.yml
sed -i 's|- .*$|- redis://rediscluster:7001|g' config/redis.yml
+ sed -i 's|url:.*$|url: redis://redis:6379|g' config/redis.yml
fi
setup_database_yml
diff --git a/scripts/regenerate-schema b/scripts/regenerate-schema
index f1018403395..75ee0c33bea 100755
--- a/scripts/regenerate-schema
+++ b/scripts/regenerate-schema
@@ -2,6 +2,7 @@
# frozen_string_literal: true
+require 'optparse'
require 'open3'
require 'fileutils'
require 'uri'
@@ -27,16 +28,37 @@ class SchemaRegenerator
# directory when it runs.
SCHEMA_MIGRATIONS_DIR = 'db/schema_migrations/'
+ def initialize(options)
+ @rollback_testing = options.delete(:rollback_testing)
+ end
+
def execute
Dir.chdir(File.expand_path('..', __dir__)) do
+ # Note: `db:drop` must run prior to hiding migrations.
+ #
+ # Executing a Rails DB command e.g., `reset`, `drop`, etc. triggers running the initializers.
+ # During the initialization, the default values for `application_settings` need to be set.
+ # Depending on the presence of migrations, the default values are either faked or inserted.
+ #
+ # 1. If no migration is detected, all the necessary columns are in place from `db/structure.sql`.
+ # The default values can be inserted into `application_settings` table.
+ #
+ # 2. If a migration is detected, at least one column may be missing from `db/structure.sql`
+ # and needs to be added through the detected migration. In this case, the default values are faked.
+ # If not, an error would be raised e.g., "NoMethodError: undefined method `some_setting`"
+ #
+ # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/135085#note_1628210334 for more info.
+ #
+ drop_db
checkout_ref
checkout_clean_schema
hide_migrations
remove_schema_migration_files
stop_spring
- reset_db
+ setup_db
unhide_migrations
migrate
+ rollback if @rollback_testing
ensure
unhide_migrations
end
@@ -156,9 +178,15 @@ class SchemaRegenerator
end
##
- # Run rake task to reset the database.
- def reset_db
- run %q(bin/rails db:reset RAILS_ENV=test)
+ # Run rake task to drop the database.
+ def drop_db
+ run %q(bin/rails db:drop RAILS_ENV=test)
+ end
+
+ ##
+ # Run rake task to setup the database.
+ def setup_db
+ run %q(bin/rails db:setup RAILS_ENV=test)
end
##
@@ -168,6 +196,15 @@ class SchemaRegenerator
end
##
+ # Run rake task to rollback migrations.
+ def rollback
+ (untracked_schema_migrations + committed_schema_migrations).sort.reverse_each do |filename|
+ version = filename[/\d+\Z/]
+ run %(bin/rails db:rollback:main db:rollback:ci RAILS_ENV=test VERSION=#{version})
+ end
+ end
+
+ ##
# Run the given +cmd+.
#
# The command is colored green, and the output of the command is
@@ -224,4 +261,19 @@ class SchemaRegenerator
end
end
-SchemaRegenerator.new.execute
+if $PROGRAM_NAME == __FILE__
+ options = {}
+
+ OptionParser.new do |opts|
+ opts.on("-r", "--rollback-testing", String, "Enable rollback testing") do
+ options[:rollback_testing] = true
+ end
+
+ opts.on("-h", "--help", "Prints this help") do
+ puts opts
+ exit
+ end
+ end.parse!
+
+ SchemaRegenerator.new(options).execute
+end
diff --git a/scripts/review_apps/base-config.yaml b/scripts/review_apps/base-config.yaml
index a425aecc86b..721733f6f68 100644
--- a/scripts/review_apps/base-config.yaml
+++ b/scripts/review_apps/base-config.yaml
@@ -56,10 +56,10 @@ gitlab:
# Based on https://console.cloud.google.com/monitoring/metrics-explorer;duration=P14D?pageState=%7B%22xyChart%22:%7B%22constantLines%22:%5B%5D,%22dataSets%22:%5B%7B%22plotType%22:%22LINE%22,%22targetAxis%22:%22Y1%22,%22timeSeriesFilter%22:%7B%22aggregations%22:%5B%7B%22crossSeriesReducer%22:%22REDUCE_NONE%22,%22groupByFields%22:%5B%5D,%22perSeriesAligner%22:%22ALIGN_RATE%22%7D,%7B%22crossSeriesReducer%22:%22REDUCE_NONE%22,%22groupByFields%22:%5B%5D,%22perSeriesAligner%22:%22ALIGN_MEAN%22%7D%5D,%22apiSource%22:%22DEFAULT_CLOUD%22,%22crossSeriesReducer%22:%22REDUCE_NONE%22,%22filter%22:%22metric.type%3D%5C%22kubernetes.io%2Fcontainer%2Fcpu%2Fcore_usage_time%5C%22%20resource.type%3D%5C%22k8s_container%5C%22%20resource.label.%5C%22container_name%5C%22%3D%5C%22gitlab-shell%5C%22%22,%22groupByFields%22:%5B%5D,%22minAlignmentPeriod%22:%2260s%22,%22perSeriesAligner%22:%22ALIGN_RATE%22,%22secondaryCrossSeriesReducer%22:%22REDUCE_NONE%22,%22secondaryGroupByFields%22:%5B%5D%7D%7D%5D,%22options%22:%7B%22mode%22:%22STATS%22%7D,%22y1Axis%22:%7B%22label%22:%22%22,%22scale%22:%22LINEAR%22%7D%7D%7D&project=gitlab-review-apps
cpu: 12m
# Based on https://console.cloud.google.com/monitoring/metrics-explorer;duration=P14D?pageState=%7B%22xyChart%22:%7B%22constantLines%22:%5B%5D,%22dataSets%22:%5B%7B%22plotType%22:%22LINE%22,%22targetAxis%22:%22Y1%22,%22timeSeriesFilter%22:%7B%22aggregations%22:%5B%7B%22crossSeriesReducer%22:%22REDUCE_NONE%22,%22groupByFields%22:%5B%5D,%22perSeriesAligner%22:%22ALIGN_MEAN%22%7D%5D,%22apiSource%22:%22DEFAULT_CLOUD%22,%22crossSeriesReducer%22:%22REDUCE_NONE%22,%22filter%22:%22metric.type%3D%5C%22kubernetes.io%2Fcontainer%2Fmemory%2Fused_bytes%5C%22%20resource.type%3D%5C%22k8s_container%5C%22%20resource.label.%5C%22container_name%5C%22%3D%5C%22gitlab-shell%5C%22%22,%22groupByFields%22:%5B%5D,%22minAlignmentPeriod%22:%2260s%22,%22perSeriesAligner%22:%22ALIGN_MEAN%22%7D%7D%5D,%22options%22:%7B%22mode%22:%22STATS%22%7D,%22y1Axis%22:%7B%22label%22:%22%22,%22scale%22:%22LINEAR%22%7D%7D%7D&project=gitlab-review-apps
- memory: 20Mi
+ memory: 50Mi
limits:
- cpu: 90m
- memory: 40Mi
+ cpu: 24m
+ memory: 100Mi
minReplicas: 1
maxReplicas: 1
hpa:
diff --git a/scripts/rspec_helpers.sh b/scripts/rspec_helpers.sh
index 46ffbc223eb..6c39f126afa 100644
--- a/scripts/rspec_helpers.sh
+++ b/scripts/rspec_helpers.sh
@@ -486,13 +486,3 @@ function is_rspec_last_run_results_file_missing() {
return 1
fi
}
-
-function merge_auto_explain_logs() {
- local auto_explain_logs_path="$(dirname "${RSPEC_AUTO_EXPLAIN_LOG_PATH}")/"
-
- for file in ${auto_explain_logs_path}*.gz; do
- (gunzip -c "${file}" && rm -f "${file}" || true)
- done | \
- scripts/merge-auto-explain-logs | \
- gzip -c > "${RSPEC_AUTO_EXPLAIN_LOG_PATH}"
-}
diff --git a/scripts/static-analysis b/scripts/static-analysis
index 41583166e04..fa394ac46c4 100755
--- a/scripts/static-analysis
+++ b/scripts/static-analysis
@@ -50,7 +50,6 @@ class StaticAnalysis
Task.new(%w[scripts/lint-conflicts.sh], 1),
Task.new(%w[yarn run block-dependencies], 1),
Task.new(%w[yarn run check-dependencies], 1),
- Task.new(%w[scripts/lint-rugged], 1),
Task.new(%w[scripts/gemfile_lock_changed.sh], 1)
].compact.freeze
diff --git a/spec/click_house/migration_support/migration_context_spec.rb b/spec/click_house/migration_support/migration_context_spec.rb
new file mode 100644
index 00000000000..48ad9d9e3fa
--- /dev/null
+++ b/spec/click_house/migration_support/migration_context_spec.rb
@@ -0,0 +1,233 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require_relative '../../../lib/click_house/migration_support/migration_error'
+
+RSpec.describe ClickHouse::MigrationSupport::MigrationContext,
+ click_house: :without_migrations, feature_category: :database do
+ include ClickHouseTestHelpers
+
+ # We don't need to delete data since we don't modify Postgres data
+ self.use_transactional_tests = false
+
+ let_it_be(:schema_migration) { ClickHouse::MigrationSupport::SchemaMigration }
+
+ let(:migrations_base_dir) { 'click_house/migrations' }
+ let(:migrations_dir) { expand_fixture_path("#{migrations_base_dir}/#{migrations_dirname}") }
+ let(:migration_context) { described_class.new(migrations_dir, schema_migration) }
+ let(:target_version) { nil }
+
+ after do
+ clear_consts(expand_fixture_path(migrations_base_dir))
+ end
+
+ describe 'performs migrations' do
+ subject(:migration) { migrate(target_version, migration_context) }
+
+ describe 'when creating a table' do
+ let(:migrations_dirname) { 'plain_table_creation' }
+
+ it 'creates a table' do
+ expect { migration }.to change { active_schema_migrations_count }.from(0).to(1)
+
+ table_schema = describe_table('some')
+ expect(schema_migrations).to contain_exactly(a_hash_including(version: '1', active: 1))
+ expect(table_schema).to match({
+ id: a_hash_including(type: 'UInt64'),
+ date: a_hash_including(type: 'Date')
+ })
+ end
+ end
+
+ describe 'when dropping a table' do
+ let(:migrations_dirname) { 'drop_table' }
+ let(:target_version) { 2 }
+
+ it 'drops table' do
+ migrate(1, migration_context)
+ expect(table_names).to include('some')
+
+ migration
+ expect(table_names).not_to include('some')
+ end
+ end
+
+ context 'when a migration raises an error' do
+ let(:migrations_dirname) { 'migration_with_error' }
+
+ it 'passes the error to caller as a StandardError' do
+ expect { migration }.to raise_error StandardError,
+ "An error has occurred, all later migrations canceled:\n\nA migration error happened"
+ expect(schema_migrations).to be_empty
+ end
+ end
+
+ context 'when a migration targets an unknown database' do
+ let(:migrations_dirname) { 'plain_table_creation_on_invalid_database' }
+
+ it 'raises ConfigurationError' do
+ expect { migration }.to raise_error ClickHouse::Client::ConfigurationError,
+ "The database 'unknown_database' is not configured"
+ end
+ end
+
+ context 'when migrations target multiple databases' do
+ let_it_be(:config) { ClickHouse::Client::Configuration.new }
+ let_it_be(:main_db_config) { [:main, config] }
+ let_it_be(:another_db_config) { [:another_db, config] }
+ let_it_be(:another_database_name) { 'gitlab_clickhouse_test_2' }
+
+ let(:migrations_dirname) { 'migrations_over_multiple_databases' }
+
+ before(:context) do
+ # Ensure we have a second database to run the test on
+ clone_database_configuration(:main, :another_db, another_database_name, config)
+
+ with_net_connect_allowed do
+ ClickHouse::Client.execute("CREATE DATABASE IF NOT EXISTS #{another_database_name}", :main, config)
+ end
+ end
+
+ after(:context) do
+ with_net_connect_allowed do
+ ClickHouse::Client.execute("DROP DATABASE #{another_database_name}", :another_db, config)
+ end
+ end
+
+ around do |example|
+ clear_db(config)
+
+ previous_config = ClickHouse::Migration.client_configuration
+ ClickHouse::Migration.client_configuration = config
+
+ example.run
+ ensure
+ ClickHouse::Migration.client_configuration = previous_config
+ end
+
+ def clone_database_configuration(source_db_identifier, target_db_identifier, target_db_name, target_config)
+ raw_config = Rails.application.config_for(:click_house)
+ raw_config.each do |database_identifier, db_config|
+ register_database(target_config, database_identifier, db_config)
+ end
+
+ target_db_config = raw_config[source_db_identifier].merge(database: target_db_name)
+ register_database(target_config, target_db_identifier, target_db_config)
+ target_config.http_post_proc = ClickHouse::Client.configuration.http_post_proc
+ target_config.json_parser = ClickHouse::Client.configuration.json_parser
+ target_config.logger = ::Logger.new(IO::NULL)
+ end
+
+ it 'registers migrations on respective database', :aggregate_failures do
+ expect { migrate(2, migration_context) }
+ .to change { active_schema_migrations_count(*main_db_config) }.from(0).to(1)
+ .and change { active_schema_migrations_count(*another_db_config) }.from(0).to(1)
+
+ expect(schema_migrations(*another_db_config)).to contain_exactly(a_hash_including(version: '2', active: 1))
+ expect(table_names(*main_db_config)).not_to include('some_on_another_db')
+ expect(table_names(*another_db_config)).not_to include('some')
+
+ expect(describe_table('some', *main_db_config)).to match({
+ id: a_hash_including(type: 'UInt64'),
+ date: a_hash_including(type: 'Date')
+ })
+ expect(describe_table('some_on_another_db', *another_db_config)).to match({
+ id: a_hash_including(type: 'UInt64'),
+ date: a_hash_including(type: 'Date')
+ })
+
+ expect { migrate(nil, migration_context) }
+ .to change { active_schema_migrations_count(*main_db_config) }.to(2)
+ .and not_change { active_schema_migrations_count(*another_db_config) }
+
+ expect(schema_migrations(*main_db_config)).to match([
+ a_hash_including(version: '1', active: 1),
+ a_hash_including(version: '3', active: 1)
+ ])
+ expect(schema_migrations(*another_db_config)).to match_array(a_hash_including(version: '2', active: 1))
+
+ expect(describe_table('some', *main_db_config)).to match({
+ id: a_hash_including(type: 'UInt64'),
+ timestamp: a_hash_including(type: 'Date')
+ })
+ end
+ end
+
+ context 'when target_version is incorrect' do
+ let(:target_version) { 2 }
+ let(:migrations_dirname) { 'plain_table_creation' }
+
+ it 'raises UnknownMigrationVersionError' do
+ expect { migration }.to raise_error ClickHouse::MigrationSupport::UnknownMigrationVersionError
+
+ expect(active_schema_migrations_count).to eq 0
+ end
+ end
+
+ context 'when migrations with duplicate name exist' do
+ let(:migrations_dirname) { 'duplicate_name' }
+
+ it 'raises DuplicateMigrationNameError' do
+ expect { migration }.to raise_error ClickHouse::MigrationSupport::DuplicateMigrationNameError
+
+ expect(active_schema_migrations_count).to eq 0
+ end
+ end
+
+ context 'when migrations with duplicate version exist' do
+ let(:migrations_dirname) { 'duplicate_version' }
+
+ it 'raises DuplicateMigrationVersionError' do
+ expect { migration }.to raise_error ClickHouse::MigrationSupport::DuplicateMigrationVersionError
+
+ expect(active_schema_migrations_count).to eq 0
+ end
+ end
+ end
+
+ describe 'performs rollbacks' do
+ subject(:migration) { rollback(target_version, migration_context) }
+
+ before do
+ migrate(nil, migration_context)
+ end
+
+ context 'when migrating back all the way to 0' do
+ let(:target_version) { 0 }
+
+ context 'when down method is present' do
+ let(:migrations_dirname) { 'table_creation_with_down_method' }
+
+ it 'removes migration and performs down method' do
+ expect(table_names).to include('some')
+
+ expect { migration }.to change { active_schema_migrations_count }.from(1).to(0)
+
+ expect(table_names).not_to include('some')
+ expect(schema_migrations).to contain_exactly(a_hash_including(version: '1', active: 0))
+ end
+ end
+
+ context 'when down method is missing' do
+ let(:migrations_dirname) { 'plain_table_creation' }
+
+ it 'removes migration ignoring missing down method' do
+ expect { migration }.to change { active_schema_migrations_count }.from(1).to(0)
+ .and not_change { table_names & %w[some] }.from(%w[some])
+ end
+ end
+ end
+
+ context 'when target_version is incorrect' do
+ let(:target_version) { -1 }
+ let(:migrations_dirname) { 'plain_table_creation' }
+
+ it 'raises UnknownMigrationVersionError' do
+ expect { migration }.to raise_error ClickHouse::MigrationSupport::UnknownMigrationVersionError
+
+ expect(active_schema_migrations_count).to eq 1
+ end
+ end
+ end
+end
diff --git a/spec/components/projects/ml/models_index_component_spec.rb b/spec/components/projects/ml/models_index_component_spec.rb
index c42c94d5d01..b662e8c0a08 100644
--- a/spec/components/projects/ml/models_index_component_spec.rb
+++ b/spec/components/projects/ml/models_index_component_spec.rb
@@ -23,7 +23,7 @@ RSpec.describe Projects::Ml::ModelsIndexComponent, type: :component, feature_cat
end
subject(:component) do
- described_class.new(paginator: paginator)
+ described_class.new(model_count: 5, paginator: paginator)
end
describe 'rendered' do
@@ -43,13 +43,15 @@ RSpec.describe Projects::Ml::ModelsIndexComponent, type: :component, feature_cat
{
'name' => model1.name,
'version' => model1.latest_version.version,
- 'path' => "/#{project.full_path}/-/packages/#{model1.latest_version.package_id}",
+ 'versionPackagePath' => "/#{project.full_path}/-/packages/#{model1.latest_version.package_id}",
+ 'versionPath' => "/#{project.full_path}/-/ml/models/#{model1.id}/versions/#{model1.latest_version.id}",
'versionCount' => 1
},
{
'name' => model2.name,
'version' => nil,
- 'path' => nil,
+ 'versionPackagePath' => nil,
+ 'versionPath' => nil,
'versionCount' => 0
}
],
@@ -58,7 +60,8 @@ RSpec.describe Projects::Ml::ModelsIndexComponent, type: :component, feature_cat
'hasPreviousPage' => false,
'startCursor' => 'abcde',
'endCursor' => 'defgh'
- }
+ },
+ 'modelCount' => 5
})
end
end
diff --git a/spec/components/projects/ml/show_ml_model_component_spec.rb b/spec/components/projects/ml/show_ml_model_component_spec.rb
index 7d08b90791b..ec125851d3d 100644
--- a/spec/components/projects/ml/show_ml_model_component_spec.rb
+++ b/spec/components/projects/ml/show_ml_model_component_spec.rb
@@ -22,7 +22,12 @@ RSpec.describe Projects::Ml::ShowMlModelComponent, type: :component, feature_cat
'model' => {
'id' => model1.id,
'name' => model1.name,
- 'path' => "/#{project.full_path}/-/ml/models/#{model1.id}"
+ 'path' => "/#{project.full_path}/-/ml/models/#{model1.id}",
+ 'description' => 'This is a placeholder for the short description',
+ 'latestVersion' => {
+ 'version' => model1.latest_version.version
+ },
+ 'versionCount' => 1
}
})
end
diff --git a/spec/components/projects/ml/show_ml_model_version_component_spec.rb b/spec/components/projects/ml/show_ml_model_version_component_spec.rb
new file mode 100644
index 00000000000..973d8123c45
--- /dev/null
+++ b/spec/components/projects/ml/show_ml_model_version_component_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe Projects::Ml::ShowMlModelVersionComponent, type: :component, feature_category: :mlops do
+ let_it_be(:project) { build_stubbed(:project) }
+ let_it_be(:model) { build_stubbed(:ml_models, project: project) }
+ let_it_be(:version) { build_stubbed(:ml_model_versions, model: model) }
+
+ subject(:component) do
+ described_class.new(model_version: version)
+ end
+
+ describe 'rendered' do
+ before do
+ render_inline component
+ end
+
+ it 'renders element with view_model' do
+ element = page.find("#js-mount-show-ml-model-version")
+
+ expect(Gitlab::Json.parse(element['data-view-model'])).to eq({
+ 'modelVersion' => {
+ 'id' => version.id,
+ 'version' => version.version,
+ 'path' => "/#{project.full_path}/-/ml/models/#{model.id}/versions/#{version.id}",
+ 'model' => {
+ 'name' => model.name,
+ 'path' => "/#{project.full_path}/-/ml/models/#{model.id}"
+ }
+ }
+ })
+ end
+ end
+end
diff --git a/spec/controllers/admin/application_settings_controller_spec.rb b/spec/controllers/admin/application_settings_controller_spec.rb
index 60343c822af..8dbdd8db99b 100644
--- a/spec/controllers/admin/application_settings_controller_spec.rb
+++ b/spec/controllers/admin/application_settings_controller_spec.rb
@@ -66,51 +66,69 @@ RSpec.describe Admin::ApplicationSettingsController, :do_not_mock_admin_mode_set
sign_in(admin)
end
+ context 'when there are NO recent ServicePing reports' do
+ it 'return 404' do
+ get :usage_data, format: :json
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
context 'when there are recent ServicePing reports' do
- it 'attempts to use prerecorded data' do
+ before do
create(:raw_usage_data)
+ end
+ it 'does not trigger ServicePing generation' do
expect(Gitlab::Usage::ServicePingReport).not_to receive(:for)
get :usage_data, format: :json
end
- end
- context 'when there are NO recent ServicePing reports' do
- it 'calculates data on the fly' do
- allow(Gitlab::Usage::ServicePingReport).to receive(:for).and_call_original
+ it 'check cached data if present' do
+ expect(Rails.cache).to receive(:fetch).with(Gitlab::Usage::ServicePingReport::CACHE_KEY).and_return({ test: 1 })
+ expect(::RawUsageData).not_to receive(:for_current_reporting_cycle)
get :usage_data, format: :json
-
- expect(Gitlab::Usage::ServicePingReport).to have_received(:for)
end
- end
- it 'returns HTML data' do
- get :usage_data, format: :html
+ context 'if no cached data available' do
+ before do
+ allow(Rails.cache).to receive(:fetch).and_return(nil)
+ end
- expect(response.body).to start_with('<span')
- expect(response).to have_gitlab_http_status(:ok)
- end
+ it 'returns latest RawUsageData' do
+ expect(::RawUsageData).to receive_message_chain(:for_current_reporting_cycle, :first, :payload)
- it 'returns JSON data' do
- get :usage_data, format: :json
+ get :usage_data, format: :json
+ end
+ end
- body = json_response
- expect(body["version"]).to eq(Gitlab::VERSION)
- expect(body).to include('counts')
- expect(response).to have_gitlab_http_status(:ok)
- end
+ it 'returns HTML data' do
+ get :usage_data, format: :html
- describe 'usage data counter' do
- let(:counter) { Gitlab::UsageDataCounters::ServiceUsageDataCounter }
+ expect(response.body).to start_with('<span')
+ expect(response).to have_gitlab_http_status(:ok)
+ end
- it 'incremented when json generated' do
- expect { get :usage_data, format: :json }.to change { counter.read(:download_payload_click) }.by(1)
+ it 'returns JSON data' do
+ get :usage_data, format: :json
+
+ expect(json_response).to be_present
+ expect(json_response['test']).to include('test')
+ expect(response).to have_gitlab_http_status(:ok)
end
- it 'not incremented when html format requested' do
- expect { get :usage_data }.not_to change { counter.read(:download_payload_click) }
+ describe 'usage data counter' do
+ let(:counter) { Gitlab::UsageDataCounters::ServiceUsageDataCounter }
+
+ it 'incremented when json generated' do
+ expect { get :usage_data, format: :json }.to change { counter.read(:download_payload_click) }.by(1)
+ end
+
+ it 'not incremented when html format requested' do
+ expect { get :usage_data }.not_to change { counter.read(:download_payload_click) }
+ end
end
end
end
@@ -524,35 +542,37 @@ RSpec.describe Admin::ApplicationSettingsController, :do_not_mock_admin_mode_set
end
end
- describe 'GET #service_usage_data', feature_category: :service_ping do
+ describe 'GET #metrics_and_profiling', feature_category: :service_ping do
before do
stub_usage_data_connections
stub_database_flavor_check
sign_in(admin)
end
- it 'assigns truthy value if there are recent ServicePing reports in database' do
+ it 'assigns service_ping_data if there are recent ServicePing reports in database' do
create(:raw_usage_data)
- get :service_usage_data, format: :html
+ get :metrics_and_profiling, format: :html
- expect(assigns(:service_ping_data_present)).to be_truthy
+ expect(assigns(:service_ping_data)).to be_present
expect(response).to have_gitlab_http_status(:ok)
end
- it 'assigns truthy value if there are recent ServicePing reports in cache', :use_clean_rails_memory_store_caching do
- Rails.cache.write('usage_data', true)
+ it 'assigns service_ping_data if there are recent ServicePing reports in cache', :use_clean_rails_memory_store_caching do
+ create(:raw_usage_data)
+ cached_data = { testKey: "testValue" }
+ Rails.cache.write('usage_data', cached_data)
- get :service_usage_data, format: :html
+ get :metrics_and_profiling, format: :html
- expect(assigns(:service_ping_data_present)).to be_truthy
+ expect(assigns(:service_ping_data)).to eq(cached_data)
expect(response).to have_gitlab_http_status(:ok)
end
- it 'assigns falsey value if there are NO recent ServicePing reports' do
- get :service_usage_data, format: :html
+ it 'does not assign service_ping_data value if there are NO recent ServicePing reports' do
+ get :metrics_and_profiling, format: :html
- expect(assigns(:service_ping_data_present)).to be_falsey
+ expect(assigns(:service_ping_data)).not_to be_present
expect(response).to have_gitlab_http_status(:ok)
end
end
diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb
index a66cb4364d7..cbe0319a78d 100644
--- a/spec/controllers/autocomplete_controller_spec.rb
+++ b/spec/controllers/autocomplete_controller_spec.rb
@@ -454,77 +454,85 @@ RSpec.describe AutocompleteController do
end
end
- context 'Get merge_request_target_branches', feature_category: :code_review_workflow do
- let!(:merge_request) { create(:merge_request, source_project: project, target_branch: 'feature') }
+ context 'GET branches', feature_category: :code_review_workflow do
+ let_it_be(:merge_request) do
+ create(:merge_request, source_project: project,
+ source_branch: 'test_source_branch', target_branch: 'test_target_branch')
+ end
- context 'anonymous user' do
- it 'returns empty json' do
- get :merge_request_target_branches, params: { project_id: project.id }
+ shared_examples 'Get merge_request_{}_branches' do |path, expected_result|
+ context 'anonymous user' do
+ it 'returns empty json' do
+ get path, params: { project_id: project.id }
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to be_empty
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to be_empty
+ end
end
- end
- context 'user without any accessible merge requests' do
- it 'returns empty json' do
- sign_in(create(:user))
+ context 'user without any accessible merge requests' do
+ it 'returns empty json' do
+ sign_in(create(:user))
- get :merge_request_target_branches, params: { project_id: project.id }
+ get path, params: { project_id: project.id }
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to be_empty
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to be_empty
+ end
end
- end
- context 'user with an accessible merge request but no scope' do
- where(
- params: [
- {},
- { group_id: ' ' },
- { project_id: ' ' },
- { group_id: ' ', project_id: ' ' }
- ]
- )
-
- with_them do
- it 'returns an error' do
- sign_in(user)
+ context 'user with an accessible merge request but no scope' do
+ where(
+ params: [
+ {},
+ { group_id: ' ' },
+ { project_id: ' ' },
+ { group_id: ' ', project_id: ' ' }
+ ]
+ )
+
+ with_them do
+ it 'returns an error' do
+ sign_in(user)
- get :merge_request_target_branches, params: params
+ get path, params: params
- expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response).to eq({ 'error' => 'At least one of group_id or project_id must be specified' })
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response).to eq({ 'error' => 'At least one of group_id or project_id must be specified' })
+ end
end
end
- end
- context 'user with an accessible merge request by project' do
- it 'returns json' do
- sign_in(user)
+ context 'user with an accessible merge request by project' do
+ it 'returns json' do
+ sign_in(user)
- get :merge_request_target_branches, params: { project_id: project.id }
+ get path, params: { project_id: project.id }
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to contain_exactly({ 'title' => 'feature' })
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to contain_exactly(expected_result)
+ end
end
- end
- context 'user with an accessible merge request by group' do
- let(:group) { create(:group) }
- let(:project) { create(:project, namespace: group) }
- let(:user) { create(:user) }
+ context 'user with an accessible merge request by group' do
+ let(:group) { create(:group) }
+ let(:user) { create(:user) }
- it 'returns json' do
- group.add_owner(user)
+ it 'returns json' do
+ project.update!(namespace: group)
+ group.add_owner(user)
- sign_in(user)
+ sign_in(user)
- get :merge_request_target_branches, params: { group_id: group.id }
+ get path, params: { group_id: group.id }
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to contain_exactly({ 'title' => 'feature' })
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to contain_exactly(expected_result)
+ end
end
end
+
+ it_behaves_like 'Get merge_request_{}_branches', :merge_request_target_branches, { 'title' => 'test_target_branch' }
+ it_behaves_like 'Get merge_request_{}_branches', :merge_request_source_branches, { 'title' => 'test_source_branch' }
end
end
diff --git a/spec/controllers/groups/settings/applications_controller_spec.rb b/spec/controllers/groups/settings/applications_controller_spec.rb
index c398fd044c2..aa50ef9a92c 100644
--- a/spec/controllers/groups/settings/applications_controller_spec.rb
+++ b/spec/controllers/groups/settings/applications_controller_spec.rb
@@ -23,17 +23,55 @@ RSpec.describe Groups::Settings::ApplicationsController do
expect(response).to render_template :index
expect(assigns[:scopes]).to be_kind_of(Doorkeeper::OAuth::Scopes)
end
- end
- context 'when user is not owner' do
- before do
- group.add_maintainer(user)
+ context 'when admin mode is enabled' do
+ let!(:user) { create(:user, :admin) }
+
+ before do
+ Gitlab::Session.with_session(controller.session) do
+ controller.current_user_mode.request_admin_mode!
+ controller.current_user_mode.enable_admin_mode!(password: user.password)
+ end
+ end
+
+ it 'renders the applications page' do
+ get :index, params: { group_id: group }
+
+ expect(response).to render_template :index
+ expect(assigns[:scopes]).to be_kind_of(Doorkeeper::OAuth::Scopes)
+ end
end
+ end
- it 'renders a 404' do
- get :index, params: { group_id: group }
+ %w[guest reporter developer maintainer].each do |role|
+ context "when user is a #{role}" do
+ before do
+ group.send("add_#{role}", user)
+ end
+
+ it 'renders a 404' do
+ get :index, params: { group_id: group }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ context "when admin mode is enabled for the admin user who is a #{role} of a group" do
+ let!(:user) { create(:user, :admin) }
+
+ before do
+ Gitlab::Session.with_session(controller.session) do
+ controller.current_user_mode.request_admin_mode!
+ controller.current_user_mode.enable_admin_mode!(password: user.password)
+ end
+ end
+
+ it 'renders the applications page' do
+ get :index, params: { group_id: group }
- expect(response).to have_gitlab_http_status(:not_found)
+ expect(response).to render_template :index
+ expect(assigns[:scopes]).to be_kind_of(Doorkeeper::OAuth::Scopes)
+ end
+ end
end
end
end
@@ -44,23 +82,61 @@ RSpec.describe Groups::Settings::ApplicationsController do
group.add_owner(user)
end
- it 'renders the application form' do
+ it 'renders the edit application page' do
get :edit, params: { group_id: group, id: application.id }
expect(response).to render_template :edit
expect(assigns[:scopes]).to be_kind_of(Doorkeeper::OAuth::Scopes)
end
- end
- context 'when user is not owner' do
- before do
- group.add_maintainer(user)
+ context 'when admin mode is enabled' do
+ let!(:user) { create(:user, :admin) }
+
+ before do
+ Gitlab::Session.with_session(controller.session) do
+ controller.current_user_mode.request_admin_mode!
+ controller.current_user_mode.enable_admin_mode!(password: user.password)
+ end
+ end
+
+ it 'renders the edit application page' do
+ get :edit, params: { group_id: group, id: application.id }
+
+ expect(response).to render_template :edit
+ expect(assigns[:scopes]).to be_kind_of(Doorkeeper::OAuth::Scopes)
+ end
end
+ end
- it 'renders a 404' do
- get :edit, params: { group_id: group, id: application.id }
+ %w[guest reporter developer maintainer].each do |role|
+ context "when user is a #{role}" do
+ before do
+ group.send("add_#{role}", user)
+ end
+
+ it 'renders a 404' do
+ get :edit, params: { group_id: group, id: application.id }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ context "when admin mode is enabled for the admin user who is a #{role} of a group" do
+ let!(:user) { create(:user, :admin) }
- expect(response).to have_gitlab_http_status(:not_found)
+ before do
+ Gitlab::Session.with_session(controller.session) do
+ controller.current_user_mode.request_admin_mode!
+ controller.current_user_mode.enable_admin_mode!(password: user.password)
+ end
+ end
+
+ it 'renders the edit application page' do
+ get :edit, params: { group_id: group, id: application.id }
+
+ expect(response).to render_template :edit
+ expect(assigns[:scopes]).to be_kind_of(Doorkeeper::OAuth::Scopes)
+ end
+ end
end
end
end
@@ -121,19 +197,71 @@ RSpec.describe Groups::Settings::ApplicationsController do
expect(response).to render_template :index
end
end
- end
- context 'when user is not owner' do
- before do
- group.add_maintainer(user)
+ context 'when admin mode is enabled' do
+ let!(:user) { create(:user, :admin) }
+
+ before do
+ Gitlab::Session.with_session(controller.session) do
+ controller.current_user_mode.request_admin_mode!
+ controller.current_user_mode.enable_admin_mode!(password: user.password)
+ end
+ end
+
+ it 'creates the application' do
+ create_params = attributes_for(:application, trusted: false, confidential: false, scopes: ['api'])
+
+ expect do
+ post :create, params: { group_id: group, doorkeeper_application: create_params }
+ end.to change { Doorkeeper::Application.count }.by(1)
+
+ application = Doorkeeper::Application.last
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template :show
+ expect(application).to have_attributes(create_params.except(:uid, :owner_type))
+ end
end
+ end
+
+ %w[guest reporter developer maintainer].each do |role|
+ context "when user is a #{role}" do
+ let(:create_params) { attributes_for(:application, trusted: true, confidential: false, scopes: ['api']) }
+
+ before do
+ group.send("add_#{role}", user)
+ end
+
+ it 'renders a 404' do
+ post :create, params: { group_id: group, doorkeeper_application: create_params }
- it 'renders a 404' do
- create_params = attributes_for(:application, trusted: true, confidential: false, scopes: ['api'])
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ context "when admin mode is enabled for the admin user who is a #{role} of a group" do
+ let!(:user) { create(:user, :admin) }
+
+ before do
+ Gitlab::Session.with_session(controller.session) do
+ controller.current_user_mode.request_admin_mode!
+ controller.current_user_mode.enable_admin_mode!(password: user.password)
+ end
+ end
+
+ it 'creates the application' do
+ create_params = attributes_for(:application, trusted: false, confidential: false, scopes: ['api'])
- post :create, params: { group_id: group, doorkeeper_application: create_params }
+ expect do
+ post :create, params: { group_id: group, doorkeeper_application: create_params }
+ end.to change { Doorkeeper::Application.count }.by(1)
- expect(response).to have_gitlab_http_status(:not_found)
+ application = Doorkeeper::Application.last
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template :show
+ expect(application).to have_attributes(create_params.except(:uid, :owner_type))
+ end
+ end
end
end
end
@@ -162,6 +290,26 @@ RSpec.describe Groups::Settings::ApplicationsController do
expect(json_response['secret']).not_to be_nil
end
+ context 'when admin mode is enabled' do
+ let!(:user) { create(:user, :admin) }
+
+ before do
+ Gitlab::Session.with_session(controller.session) do
+ controller.current_user_mode.request_admin_mode!
+ controller.current_user_mode.enable_admin_mode!(password: user.password)
+ end
+ end
+
+ it { is_expected.to have_gitlab_http_status(:ok) }
+ it { expect { subject }.to change { application.reload.secret } }
+
+ it 'returns the secret in json format' do
+ subject
+
+ expect(json_response['secret']).not_to be_nil
+ end
+ end
+
context 'when renew fails' do
before do
allow_next_found_instance_of(Doorkeeper::Application) do |application|
@@ -174,21 +322,42 @@ RSpec.describe Groups::Settings::ApplicationsController do
end
end
- context 'when user is not owner' do
- before do
- group.add_maintainer(user)
- end
+ %w[guest reporter developer maintainer].each do |role|
+ context "when user is a #{role}" do
+ let(:oauth_params) do
+ {
+ group_id: group,
+ id: application.id
+ }
+ end
- let(:oauth_params) do
- {
- group_id: group,
- id: application.id
- }
- end
+ before do
+ group.send("add_#{role}", user)
+ end
- it 'renders a 404' do
- put :renew, params: oauth_params
- expect(response).to have_gitlab_http_status(:not_found)
+ it 'renders a 404' do
+ put :renew, params: oauth_params
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ context "when admin mode is enabled for the admin user who is a #{role} of a group" do
+ let!(:user) { create(:user, :admin) }
+
+ before do
+ Gitlab::Session.with_session(controller.session) do
+ controller.current_user_mode.request_admin_mode!
+ controller.current_user_mode.enable_admin_mode!(password: user.password)
+ end
+ end
+
+ it 'returns the secret in json format' do
+ put :renew, params: oauth_params
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['secret']).not_to be_nil
+ end
+ end
end
end
end
@@ -230,19 +399,67 @@ RSpec.describe Groups::Settings::ApplicationsController do
expect(application).to be_confidential
end
end
- end
- context 'when user is not owner' do
- before do
- group.add_maintainer(user)
+ context 'when admin mode is enabled' do
+ let!(:user) { create(:user, :admin) }
+
+ before do
+ Gitlab::Session.with_session(controller.session) do
+ controller.current_user_mode.request_admin_mode!
+ controller.current_user_mode.enable_admin_mode!(password: user.password)
+ end
+ end
+
+ it 'updates the application' do
+ doorkeeper_params = { redirect_uri: 'http://example.com/', trusted: true, confidential: false }
+
+ patch :update, params: { group_id: group, id: application.id, doorkeeper_application: doorkeeper_params }
+
+ application.reload
+
+ expect(response).to redirect_to(group_settings_application_path(group, application))
+ expect(application)
+ .to have_attributes(redirect_uri: 'http://example.com/', trusted: false, confidential: false)
+ end
end
+ end
- it 'renders a 404' do
- doorkeeper_params = { redirect_uri: 'http://example.com/', trusted: true, confidential: false }
+ %w[guest reporter developer maintainer].each do |role|
+ context "when user is a #{role}" do
+ before do
+ group.send("add_#{role}", user)
+ end
- patch :update, params: { group_id: group, id: application.id, doorkeeper_application: doorkeeper_params }
+ it 'renders a 404' do
+ doorkeeper_params = { redirect_uri: 'http://example.com/', trusted: true, confidential: false }
+
+ patch :update, params: { group_id: group, id: application.id, doorkeeper_application: doorkeeper_params }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
- expect(response).to have_gitlab_http_status(:not_found)
+ context "when admin mode is enabled for the admin user who is a #{role} of a group" do
+ let!(:user) { create(:user, :admin) }
+
+ before do
+ Gitlab::Session.with_session(controller.session) do
+ controller.current_user_mode.request_admin_mode!
+ controller.current_user_mode.enable_admin_mode!(password: user.password)
+ end
+ end
+
+ it 'updates the application' do
+ doorkeeper_params = { redirect_uri: 'http://example.com/', trusted: true, confidential: false }
+
+ patch :update, params: { group_id: group, id: application.id, doorkeeper_application: doorkeeper_params }
+
+ application.reload
+
+ expect(response).to redirect_to(group_settings_application_path(group, application))
+ expect(application)
+ .to have_attributes(redirect_uri: 'http://example.com/', trusted: false, confidential: false)
+ end
+ end
end
end
end
@@ -259,17 +476,55 @@ RSpec.describe Groups::Settings::ApplicationsController do
expect(Doorkeeper::Application.exists?(application.id)).to be_falsy
expect(response).to redirect_to(group_settings_applications_url(group))
end
- end
- context 'when user is not owner' do
- before do
- group.add_maintainer(user)
+ context 'when admin mode is enabled' do
+ let!(:user) { create(:user, :admin) }
+
+ before do
+ Gitlab::Session.with_session(controller.session) do
+ controller.current_user_mode.request_admin_mode!
+ controller.current_user_mode.enable_admin_mode!(password: user.password)
+ end
+ end
+
+ it 'deletes the application' do
+ delete :destroy, params: { group_id: group, id: application.id }
+
+ expect(Doorkeeper::Application.exists?(application.id)).to be_falsy
+ expect(response).to redirect_to(group_settings_applications_url(group))
+ end
end
+ end
- it 'renders a 404' do
- delete :destroy, params: { group_id: group, id: application.id }
+ %w[guest reporter developer maintainer].each do |role|
+ context "when user is a #{role}" do
+ before do
+ group.send("add_#{role}", user)
+ end
+
+ it 'renders a 404' do
+ delete :destroy, params: { group_id: group, id: application.id }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ context "when admin mode is enabled for the admin user who is a #{role} of a group" do
+ let!(:user) { create(:user, :admin) }
- expect(response).to have_gitlab_http_status(:not_found)
+ before do
+ Gitlab::Session.with_session(controller.session) do
+ controller.current_user_mode.request_admin_mode!
+ controller.current_user_mode.enable_admin_mode!(password: user.password)
+ end
+ end
+
+ it 'deletes the application' do
+ delete :destroy, params: { group_id: group, id: application.id }
+
+ expect(Doorkeeper::Application.exists?(application.id)).to be_falsy
+ expect(response).to redirect_to(group_settings_applications_url(group))
+ end
+ end
end
end
end
diff --git a/spec/controllers/import/bulk_imports_controller_spec.rb b/spec/controllers/import/bulk_imports_controller_spec.rb
index c5e5aa03669..57c723829e3 100644
--- a/spec/controllers/import/bulk_imports_controller_spec.rb
+++ b/spec/controllers/import/bulk_imports_controller_spec.rb
@@ -300,6 +300,33 @@ RSpec.describe Import::BulkImportsController, feature_category: :importers do
end
end
+ describe 'GET details' do
+ subject(:request) { get :details }
+
+ context 'when bulk_import_details_page feature flag is enabled' do
+ before do
+ stub_feature_flags(bulk_import_details_page: true)
+ request
+ end
+
+ it 'responds with a 200 and shows the template', :aggregate_failures do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template(:details)
+ end
+ end
+
+ context 'when bulk_import_details_page feature flag is disabled' do
+ before do
+ stub_feature_flags(bulk_import_details_page: false)
+ request
+ end
+
+ it 'responds with a 404' do
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
describe 'GET realtime_changes' do
let_it_be(:bulk_import) { create(:bulk_import, :created, user: user) }
diff --git a/spec/controllers/oauth/jira_dvcs/authorizations_controller_spec.rb b/spec/controllers/oauth/jira_dvcs/authorizations_controller_spec.rb
deleted file mode 100644
index 3d271a22f27..00000000000
--- a/spec/controllers/oauth/jira_dvcs/authorizations_controller_spec.rb
+++ /dev/null
@@ -1,71 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Oauth::JiraDvcs::AuthorizationsController, feature_category: :integrations do
- let_it_be(:application) { create(:oauth_application, redirect_uri: 'https://example.com/callback') }
-
- describe 'GET new' do
- it 'redirects to OAuth authorization with correct params' do
- get :new, params: { client_id: application.uid, scope: 'foo', redirect_uri: 'https://example.com/callback' }
-
- expect(response).to redirect_to(oauth_authorization_url(
- client_id: application.uid,
- response_type: 'code',
- scope: 'foo',
- redirect_uri: oauth_jira_dvcs_callback_url))
- end
-
- it 'replaces the GitHub "repo" scope with "api"' do
- get :new, params: { client_id: application.uid, scope: 'repo', redirect_uri: 'https://example.com/callback' }
-
- expect(response).to redirect_to(oauth_authorization_url(
- client_id: application.uid,
- response_type: 'code',
- scope: 'api',
- redirect_uri: oauth_jira_dvcs_callback_url))
- end
-
- it 'returns 404 with an invalid client' do
- get :new, params: { client_id: 'client-123', scope: 'foo', redirect_uri: 'https://example.com/callback' }
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
-
- it 'returns 403 with an incorrect redirect_uri' do
- get :new, params: { client_id: application.uid, scope: 'foo', redirect_uri: 'http://unsafe-website.com/callback' }
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
-
- describe 'GET callback' do
- it 'redirects to redirect_uri on session with code param' do
- session['redirect_uri'] = 'http://example.com'
-
- get :callback, params: { code: 'hash-123' }
-
- expect(response).to redirect_to('http://example.com?code=hash-123')
- end
-
- it 'redirects to redirect_uri on session with code param preserving existing query' do
- session['redirect_uri'] = 'http://example.com?foo=bar'
-
- get :callback, params: { code: 'hash-123' }
-
- expect(response).to redirect_to('http://example.com?foo=bar&code=hash-123')
- end
- end
-
- describe 'POST access_token' do
- it 'returns oauth params in a format Jira expects' do
- expect_any_instance_of(Doorkeeper::Request::AuthorizationCode).to receive(:authorize) do
- double(status: :ok, body: { 'access_token' => 'fake-123', 'scope' => 'foo', 'token_type' => 'bar' })
- end
-
- post :access_token, params: { code: 'code-123', client_id: application.uid, client_secret: 'secret-123' }
-
- expect(response.body).to eq('access_token=fake-123&scope=foo&token_type=bar')
- end
- end
-end
diff --git a/spec/controllers/profiles/notifications_controller_spec.rb b/spec/controllers/profiles/notifications_controller_spec.rb
index 22c0a62a6a1..ef49eb911ba 100644
--- a/spec/controllers/profiles/notifications_controller_spec.rb
+++ b/spec/controllers/profiles/notifications_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Profiles::NotificationsController do
+RSpec.describe Profiles::NotificationsController, feature_category: :team_planning do
let(:user) do
create(:user) do |user|
user.emails.create!(email: 'original@example.com', confirmed_at: Time.current)
diff --git a/spec/controllers/profiles_controller_spec.rb b/spec/controllers/profiles_controller_spec.rb
index 2bcb47f97ab..4f350ddf1ef 100644
--- a/spec/controllers/profiles_controller_spec.rb
+++ b/spec/controllers/profiles_controller_spec.rb
@@ -128,6 +128,16 @@ RSpec.describe ProfilesController, :request_store do
expect(user.reload.discord).to eq(discord_user_id)
expect(response).to have_gitlab_http_status(:found)
end
+
+ it 'allows updating user specified mastodon username', :aggregate_failures do
+ mastodon_username = '@robin@example.com'
+ sign_in(user)
+
+ put :update, params: { user: { mastodon: mastodon_username } }
+
+ expect(user.reload.mastodon).to eq(mastodon_username)
+ expect(response).to have_gitlab_http_status(:found)
+ end
end
describe 'GET audit_log' do
diff --git a/spec/controllers/projects/artifacts_controller_spec.rb b/spec/controllers/projects/artifacts_controller_spec.rb
index 31e6d6ae5e6..a0548e847a0 100644
--- a/spec/controllers/projects/artifacts_controller_spec.rb
+++ b/spec/controllers/projects/artifacts_controller_spec.rb
@@ -324,12 +324,32 @@ RSpec.describe Projects::ArtifactsController, feature_category: :build_artifacts
end
context 'when the file exists' do
- it 'renders the file view' do
- path = 'ci_artifacts.txt'
+ context 'when the external redirect page is enabled' do
+ before do
+ stub_application_setting(enable_artifact_external_redirect_warning_page: true)
+ end
+
+ it 'redirects to the user-generated content warning page' do
+ path = 'ci_artifacts.txt'
- get :file, params: { namespace_id: project.namespace, project_id: project, job_id: job, path: path }
+ get :file, params: { namespace_id: project.namespace, project_id: project, job_id: job, path: path }
+
+ expect(response).to redirect_to(external_file_project_job_artifacts_path(project, job, path: path))
+ end
+ end
- expect(response).to redirect_to(external_file_project_job_artifacts_path(project, job, path: path))
+ context 'when the external redirect page is disabled' do
+ before do
+ stub_application_setting(enable_artifact_external_redirect_warning_page: false)
+ end
+
+ it 'renders the file view' do
+ path = 'ci_artifacts.txt'
+
+ get :file, params: { namespace_id: project.namespace, project_id: project, job_id: job, path: path }
+
+ expect(response).to have_gitlab_http_status(:found)
+ end
end
end
end
diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb
index 49c1935c4a3..7811ef3bade 100644
--- a/spec/controllers/projects/blob_controller_spec.rb
+++ b/spec/controllers/projects/blob_controller_spec.rb
@@ -23,18 +23,12 @@ RSpec.describe Projects::BlobController, feature_category: :source_code_manageme
render_views
context 'with file path' do
+ include_context 'with ambiguous refs for controllers'
+
before do
- expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original
- project.repository.add_tag(project.creator, 'ambiguous_ref', RepoHelpers.sample_commit.id)
- project.repository.add_branch(project.creator, 'ambiguous_ref', RepoHelpers.another_sample_commit.id)
request
end
- after do
- project.repository.rm_tag(project.creator, 'ambiguous_ref')
- project.repository.rm_branch(project.creator, 'ambiguous_ref')
- end
-
context 'when the ref is ambiguous' do
let(:ref) { 'ambiguous_ref' }
let(:path) { 'README.md' }
@@ -43,6 +37,8 @@ RSpec.describe Projects::BlobController, feature_category: :source_code_manageme
context 'and the redirect_with_ref_type flag is disabled' do
let(:redirect_with_ref_type) { false }
+ it_behaves_like '#set_is_ambiguous_ref when ref is ambiguous'
+
context 'and explicitly requesting a branch' do
let(:ref_type) { 'heads' }
@@ -61,16 +57,22 @@ RSpec.describe Projects::BlobController, feature_category: :source_code_manageme
end
context 'and the redirect_with_ref_type flag is enabled' do
- context 'when the ref_type is nil' do
- let(:ref_type) { nil }
+ let(:ref_type) { nil }
- it 'redirects to the tag' do
- expect(response).to redirect_to(project_blob_path(project, id, ref_type: 'tags'))
- end
+ it 'redirects to the tag' do
+ expect(response).to redirect_to(project_blob_path(project, id, ref_type: 'tags'))
end
end
end
+ describe '#set_is_ambiguous_ref with no ambiguous ref' do
+ let(:id) { 'master/invalid-path.rb' }
+ let(:redirect_with_ref_type) { false }
+ let(:ambiguous_ref_modal) { true }
+
+ it_behaves_like '#set_is_ambiguous_ref when ref is not ambiguous'
+ end
+
context "valid branch, valid file" do
let(:id) { 'master/README.md' }
diff --git a/spec/controllers/projects/group_links_controller_spec.rb b/spec/controllers/projects/group_links_controller_spec.rb
index 2075dd3e7a7..4510e9e646e 100644
--- a/spec/controllers/projects/group_links_controller_spec.rb
+++ b/spec/controllers/projects/group_links_controller_spec.rb
@@ -30,24 +30,29 @@ RSpec.describe Projects::GroupLinksController, feature_category: :system_access
end
let(:expiry_date) { 1.month.from_now.to_date }
+ let(:group_access) { Gitlab::Access::GUEST }
- before do
- travel_to Time.now.utc.beginning_of_day
-
+ subject(:update_link) do
put(
:update,
params: {
namespace_id: project.namespace.to_param,
project_id: project.to_param,
id: link.id,
- group_link: { group_access: Gitlab::Access::GUEST, expires_at: expiry_date }
+ group_link: { group_access: group_access, expires_at: expiry_date }
},
format: :json
)
end
+ before do
+ travel_to Time.now.utc.beginning_of_day
+ end
+
context 'when `expires_at` is set' do
it 'returns correct json response' do
+ update_link
+
expect(json_response).to eq({ "expires_in" => controller.helpers.time_ago_with_tooltip(expiry_date), "expires_soon" => false })
end
end
@@ -56,27 +61,41 @@ RSpec.describe Projects::GroupLinksController, feature_category: :system_access
let(:expiry_date) { nil }
it 'returns empty json response' do
+ update_link
+
expect(json_response).to be_empty
end
end
+
+ it "returns an error when link is not updated" do
+ allow(::Projects::GroupLinks::UpdateService).to receive_message_chain(:new, :execute)
+ .and_return(ServiceResponse.error(message: '404 Not Found', reason: :not_found))
+
+ update_link
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq('404 Not Found')
+ end
end
describe '#destroy' do
let(:group_owner) { create(:user) }
+ let(:group_access) { Gitlab::Access::DEVELOPER }
+ let(:format) { :html }
- let(:link) do
- create(:project_group_link, project: project, group: group, group_access: Gitlab::Access::DEVELOPER)
+ let!(:link) do
+ create(:project_group_link, project: project, group: group, group_access: group_access)
end
subject(:destroy_link) do
post(:destroy, params: { namespace_id: project.namespace.to_param,
project_id: project.to_param,
- id: link.id })
+ id: link.id }, format: format)
end
shared_examples 'success response' do
it 'deletes the project group link' do
- destroy_link
+ expect { destroy_link }.to change { project.reload.project_group_links.count }
expect(response).to redirect_to(project_project_members_path(project))
expect(response).to have_gitlab_http_status(:found)
@@ -119,6 +138,27 @@ RSpec.describe Projects::GroupLinksController, feature_category: :system_access
end
it_behaves_like 'success response'
+
+ it "returns an error when link is not destroyed" do
+ allow(::Projects::GroupLinks::DestroyService).to receive_message_chain(:new, :execute)
+ .and_return(ServiceResponse.error(message: 'The error message'))
+
+ expect { destroy_link }.not_to change { project.reload.project_group_links.count }
+ expect(flash[:alert]).to eq('The project-group link could not be removed.')
+ end
+
+ context 'when format is js' do
+ let(:format) { :js }
+
+ it "returns an error when link is not destroyed" do
+ allow(::Projects::GroupLinks::DestroyService).to receive_message_chain(:new, :execute)
+ .and_return(ServiceResponse.error(message: '404 Not Found', reason: :not_found))
+
+ expect { destroy_link }.not_to change { project.reload.project_group_links.count }
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq('404 Not Found')
+ end
+ end
end
context 'when user is not a project maintainer' do
diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb
index 9851153bd39..58da1d37904 100644
--- a/spec/controllers/projects/jobs_controller_spec.rb
+++ b/spec/controllers/projects/jobs_controller_spec.rb
@@ -621,6 +621,64 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state, featu
end
end
+ describe 'GET test_report_summary.json' do
+ let_it_be(:build) { create(:ci_build, :success, :test_reports, project: project) }
+
+ before do
+ sign_in(user)
+ end
+
+ context 'when the user has access' do
+ let(:user) { developer }
+
+ context 'when the summary has been generated' do
+ let!(:report_result) { create(:ci_build_report_result, build: build, project: project) }
+
+ before do
+ get_test_report_summary
+ end
+
+ it 'returns the summary as json' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('job/test_report_summary')
+ end
+ end
+
+ context 'when the summary has not been generated' do
+ before do
+ get_test_report_summary
+ end
+
+ it 'returns a 404 response' do
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ context 'when the user does not have access' do
+ let(:user) { guest }
+
+ before do
+ project.update!(public_builds: false)
+ get_test_report_summary
+ end
+
+ it 'returns not_found status' do
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ def get_test_report_summary
+ get :test_report_summary,
+ params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ id: build.id
+ },
+ format: :json
+ end
+ end
+
describe 'GET trace.json' do
before do
get_trace
diff --git a/spec/controllers/projects/merge_requests/drafts_controller_spec.rb b/spec/controllers/projects/merge_requests/drafts_controller_spec.rb
index 68fbeb00b67..505f9f5b19b 100644
--- a/spec/controllers/projects/merge_requests/drafts_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests/drafts_controller_spec.rb
@@ -4,10 +4,10 @@ require 'spec_helper'
RSpec.describe Projects::MergeRequests::DraftsController, feature_category: :code_review_workflow do
include RepoHelpers
- let(:project) { create(:project, :repository) }
- let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project, author: create(:user)) }
- let(:user) { project.first_owner }
- let(:user2) { create(:user) }
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be_with_reload(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project, author: create(:user)) }
+ let(:user) { project.first_owner }
+ let_it_be(:user2) { create(:user) }
let(:params) do
{
@@ -18,6 +18,8 @@ RSpec.describe Projects::MergeRequests::DraftsController, feature_category: :cod
end
before do
+ create(:merge_request_reviewer, merge_request: merge_request, reviewer: user)
+
sign_in(user)
stub_licensed_features(multiple_merge_request_assignees: true)
stub_commonmark_sourcepos_disabled
@@ -216,9 +218,12 @@ RSpec.describe Projects::MergeRequests::DraftsController, feature_category: :cod
end
context 'without permissions' do
+ before_all do
+ project.add_developer(user2)
+ end
+
before do
sign_in(user2)
- project.add_developer(user2)
end
it 'does not allow editing draft note belonging to someone else' do
@@ -282,7 +287,7 @@ RSpec.describe Projects::MergeRequests::DraftsController, feature_category: :cod
end
context 'when note belongs to someone else' do
- before do
+ before_all do
project.add_developer(user2)
end
@@ -465,6 +470,24 @@ RSpec.describe Projects::MergeRequests::DraftsController, feature_category: :cod
end
end
+ context 'reviewer state' do
+ before do
+ create(:draft_note, merge_request: merge_request, author: user)
+ end
+
+ it 'updates reviewers state' do
+ post :publish, params: params.merge!(reviewer_state: 'requested_changes')
+
+ expect(merge_request.merge_request_reviewers.reload[0].state).to eq('requested_changes')
+ end
+
+ it 'approves merge request' do
+ post :publish, params: params.merge!(reviewer_state: 'approved')
+
+ expect(merge_request.approvals.reload.size).to eq(1)
+ end
+ end
+
context 'approve merge request' do
before do
allow(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
@@ -517,9 +540,12 @@ RSpec.describe Projects::MergeRequests::DraftsController, feature_category: :cod
end
context 'without permissions' do
+ before_all do
+ project.add_developer(user2)
+ end
+
before do
sign_in(user2)
- project.add_developer(user2)
end
it 'does not allow destroying a draft note belonging to someone else' do
@@ -562,9 +588,12 @@ RSpec.describe Projects::MergeRequests::DraftsController, feature_category: :cod
end
context 'without permissions' do
+ before_all do
+ project.add_developer(user2)
+ end
+
before do
sign_in(user2)
- project.add_developer(user2)
end
it 'does not destroys a draft note belonging to someone else' do
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index 92bbffdfde5..539c6d17e0e 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -2305,68 +2305,139 @@ RSpec.describe Projects::MergeRequestsController, feature_category: :code_review
end
context 'highlight preloading' do
- context 'with commit diff notes' do
- let!(:commit_diff_note) do
- create(:diff_note_on_commit, project: merge_request.project)
+ context 'when only_highlight_discussions_requested is false' do
+ before do
+ stub_feature_flags(only_highlight_discussions_requested: false)
end
- it 'preloads notes diffs highlights' do
- expect_next_instance_of(Gitlab::DiscussionsDiff::FileCollection) do |collection|
- note_diff_file = commit_diff_note.note_diff_file
+ context 'with commit diff notes' do
+ let!(:first_commit_diff_note) do
+ create(:diff_note_on_commit, project: merge_request.project)
+ end
+
+ let!(:second_commit_diff_note) do
+ create(:diff_note_on_commit, project: merge_request.project)
+ end
+
+ it 'preloads all of the notes diffs highlights' do
+ expect_next_instance_of(Gitlab::DiscussionsDiff::FileCollection) do |collection|
+ first_note_diff_file = first_commit_diff_note.note_diff_file
+ second_note_diff_file = second_commit_diff_note.note_diff_file
+
+ expect(collection).to receive(:load_highlight).and_call_original
+ expect(collection).to receive(:find_by_id).with(first_note_diff_file.id).and_call_original
+ expect(collection).to receive(:find_by_id).with(second_note_diff_file.id).and_call_original
+ end
- expect(collection).to receive(:load_highlight).and_call_original
- expect(collection).to receive(:find_by_id).with(note_diff_file.id).and_call_original
+ get :discussions, params: { namespace_id: project.namespace, project_id: project, id: merge_request.iid,
+ per_page: 2 }
end
- get :discussions, params: { namespace_id: project.namespace, project_id: project, id: merge_request.iid }
+ it 'preloads all of the notes diffs highlights when per_page is 1' do
+ expect_next_instance_of(Gitlab::DiscussionsDiff::FileCollection) do |collection|
+ first_note_diff_file = first_commit_diff_note.note_diff_file
+ second_note_diff_file = second_commit_diff_note.note_diff_file
+
+ expect(collection).to receive(:load_highlight).and_call_original
+ expect(collection).to receive(:find_by_id).with(first_note_diff_file.id).and_call_original
+ expect(collection).not_to receive(:find_by_id).with(second_note_diff_file.id)
+ end
+
+ get :discussions, params: { namespace_id: project.namespace, project_id: project, id: merge_request.iid,
+ per_page: 1 }
+ end
end
- end
- context 'with diff notes' do
- let!(:diff_note) do
- create(:diff_note_on_merge_request, noteable: merge_request, project: merge_request.project)
+ context 'with diff notes' do
+ let!(:diff_note) do
+ create(:diff_note_on_merge_request, noteable: merge_request, project: merge_request.project)
+ end
+
+ it 'preloads notes diffs highlights' do
+ expect_next_instance_of(Gitlab::DiscussionsDiff::FileCollection) do |collection|
+ note_diff_file = diff_note.note_diff_file
+
+ expect(collection).to receive(:load_highlight).and_call_original
+ expect(collection).to receive(:find_by_id).with(note_diff_file.id).and_call_original
+ end
+
+ get :discussions, params: { namespace_id: project.namespace, project_id: project, id: merge_request.iid }
+ end
end
+ end
- it 'preloads notes diffs highlights' do
- expect_next_instance_of(Gitlab::DiscussionsDiff::FileCollection) do |collection|
- note_diff_file = diff_note.note_diff_file
+ context 'when only_highlight_discussions_requested is true' do
+ context 'with commit diff notes' do
+ let!(:first_commit_diff_note) do
+ create(:diff_note_on_commit, project: merge_request.project)
+ end
- expect(collection).to receive(:load_highlight).and_call_original
- expect(collection).to receive(:find_by_id).with(note_diff_file.id).and_call_original
+ let!(:second_commit_diff_note) do
+ create(:diff_note_on_commit, project: merge_request.project)
end
- get :discussions, params: { namespace_id: project.namespace, project_id: project, id: merge_request.iid }
- end
+ it 'preloads all of the notes diffs highlights' do
+ expect_next_instance_of(Gitlab::DiscussionsDiff::FileCollection) do |collection|
+ first_note_diff_file = first_commit_diff_note.note_diff_file
+ second_note_diff_file = second_commit_diff_note.note_diff_file
+
+ expect(collection).to receive(:load_highlight).with(diff_note_ids: [first_commit_diff_note.id, second_commit_diff_note.id]).and_call_original
+ expect(collection).to receive(:find_by_id).with(first_note_diff_file.id).and_call_original
+ expect(collection).to receive(:find_by_id).with(second_note_diff_file.id).and_call_original
+ end
+
+ get :discussions, params: { namespace_id: project.namespace, project_id: project, id: merge_request.iid,
+ per_page: 2 }
+ end
+
+ it 'preloads all of the notes diffs highlights when per_page is 1' do
+ expect_next_instance_of(Gitlab::DiscussionsDiff::FileCollection) do |collection|
+ first_note_diff_file = first_commit_diff_note.note_diff_file
+ second_note_diff_file = second_commit_diff_note.note_diff_file
- it 'does not preload highlights when diff note is resolved' do
- Notes::ResolveService.new(diff_note.project, user).execute(diff_note)
+ expect(collection).to receive(:load_highlight).with(diff_note_ids: [first_commit_diff_note.id]).and_call_original
+ expect(collection).to receive(:find_by_id).with(first_note_diff_file.id).and_call_original
+ expect(collection).not_to receive(:find_by_id).with(second_note_diff_file.id)
+ end
- expect_next_instance_of(Gitlab::DiscussionsDiff::FileCollection) do |collection|
- note_diff_file = diff_note.note_diff_file
+ get :discussions, params: { namespace_id: project.namespace, project_id: project, id: merge_request.iid,
+ per_page: 1 }
+ end
+ end
- expect(collection).to receive(:load_highlight).and_call_original
- expect(collection).to receive(:find_by_id).with(note_diff_file.id).and_call_original
+ context 'with diff notes' do
+ let!(:diff_note) do
+ create(:diff_note_on_merge_request, noteable: merge_request, project: merge_request.project)
end
- get :discussions, params: { namespace_id: project.namespace, project_id: project, id: merge_request.iid }
+ it 'preloads notes diffs highlights' do
+ expect_next_instance_of(Gitlab::DiscussionsDiff::FileCollection) do |collection|
+ note_diff_file = diff_note.note_diff_file
+
+ expect(collection).to receive(:load_highlight).with(diff_note_ids: [diff_note.id]).and_call_original
+ expect(collection).to receive(:find_by_id).with(note_diff_file.id).and_call_original
+ end
+
+ get :discussions, params: { namespace_id: project.namespace, project_id: project, id: merge_request.iid }
+ end
end
end
end
- end
- context do
- it_behaves_like 'discussions provider' do
- let!(:author) { create(:user) }
- let!(:project) { create(:project) }
+ context do
+ it_behaves_like 'discussions provider' do
+ let!(:author) { create(:user) }
+ let_it_be_with_refind(:project) { create(:project) }
- let!(:merge_request) { create(:merge_request, source_project: project) }
+ let!(:merge_request) { create(:merge_request, source_project: project) }
- let!(:mr_note1) { create(:discussion_note_on_merge_request, noteable: merge_request, project: project) }
- let!(:mr_note2) { create(:discussion_note_on_merge_request, noteable: merge_request, project: project) }
+ let!(:mr_note1) { create(:discussion_note_on_merge_request, noteable: merge_request, project: project) }
+ let!(:mr_note2) { create(:discussion_note_on_merge_request, noteable: merge_request, project: project) }
- let(:requested_iid) { merge_request.iid }
- let(:expected_discussion_count) { 2 }
- let(:expected_discussion_ids) { [mr_note1.discussion_id, mr_note2.discussion_id] }
+ let(:requested_iid) { merge_request.iid }
+ let(:expected_discussion_count) { 2 }
+ let(:expected_discussion_ids) { [mr_note1.discussion_id, mr_note2.discussion_id] }
+ end
end
end
end
diff --git a/spec/controllers/projects/pages_controller_spec.rb b/spec/controllers/projects/pages_controller_spec.rb
index 34ec8d8d575..d94150a37d0 100644
--- a/spec/controllers/projects/pages_controller_spec.rb
+++ b/spec/controllers/projects/pages_controller_spec.rb
@@ -48,7 +48,6 @@ RSpec.describe Projects::PagesController, feature_category: :pages do
context 'when the project does not have onboarding complete' do
before do
- project.pages_metadatum.update_attribute(:deployed, false)
project.pages_metadatum.update_attribute(:onboarding_complete, false)
end
@@ -76,6 +75,17 @@ RSpec.describe Projects::PagesController, feature_category: :pages do
end
end
+ context 'when the project has a deployed pages app' do
+ before do
+ project.pages_metadatum.update_attribute(:onboarding_complete, false)
+ create(:pages_deployment, project: project)
+ end
+
+ it 'does not redirect to #new' do
+ expect(subject).not_to redirect_to(action: 'new')
+ end
+ end
+
context 'when pages is disabled' do
let(:project) { create(:project, :pages_disabled) }
diff --git a/spec/controllers/projects/tree_controller_spec.rb b/spec/controllers/projects/tree_controller_spec.rb
index a409030e359..4e00d58bf17 100644
--- a/spec/controllers/projects/tree_controller_spec.rb
+++ b/spec/controllers/projects/tree_controller_spec.rb
@@ -5,7 +5,6 @@ require 'spec_helper'
RSpec.describe Projects::TreeController, feature_category: :source_code_management do
let_it_be(:project) { create(:project, :repository) }
let(:user) { create(:user) }
- let(:redirect_with_ref_type) { true }
before do
sign_in(user)
@@ -25,20 +24,31 @@ RSpec.describe Projects::TreeController, feature_category: :source_code_manageme
let(:ref_type) { nil }
+ let(:redirect_with_ref_type) { true }
+
# Make sure any errors accessing the tree in our views bubble up to this spec
render_views
- before do
- expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original
- project.repository.add_tag(project.creator, 'ambiguous_ref', RepoHelpers.sample_commit.id)
- project.repository.add_branch(project.creator, 'ambiguous_ref', RepoHelpers.another_sample_commit.id)
+ include_context 'with ambiguous refs for controllers'
- stub_feature_flags(redirect_with_ref_type: redirect_with_ref_type)
- end
+ describe '#set_is_ambiguous_ref before action' do
+ let(:redirect_with_ref_type) { false }
+
+ before do
+ request
+ end
- after do
- project.repository.rm_tag(project.creator, 'ambiguous_ref')
- project.repository.rm_branch(project.creator, 'ambiguous_ref')
+ context 'when ref requested is ambiguous with no ref type' do
+ let(:id) { 'ambiguous_ref' }
+
+ it_behaves_like '#set_is_ambiguous_ref when ref is ambiguous'
+ end
+
+ context 'when ref requested is not ambiguous' do
+ let(:id) { 'master' }
+
+ it_behaves_like '#set_is_ambiguous_ref when ref is not ambiguous'
+ end
end
context 'when the redirect_with_ref_type flag is disabled' do
diff --git a/spec/controllers/repositories/git_http_controller_spec.rb b/spec/controllers/repositories/git_http_controller_spec.rb
index 602c9c0a2ce..0ae44d3654e 100644
--- a/spec/controllers/repositories/git_http_controller_spec.rb
+++ b/spec/controllers/repositories/git_http_controller_spec.rb
@@ -200,4 +200,24 @@ RSpec.describe Repositories::GitHttpController, feature_category: :source_code_m
end
end
end
+
+ describe '#append_info_to_payload' do
+ let(:log_payload) { {} }
+ let(:container) { project.design_management_repository }
+ let(:repository_path) { "#{container.full_path}.git" }
+ let(:params) { { repository_path: repository_path, service: 'git-upload-pack' } }
+ let(:repository_storage) { "default" }
+
+ before do
+ allow(controller).to receive(:append_info_to_payload).and_wrap_original do |method, *|
+ method.call(log_payload)
+ end
+ end
+
+ it 'appends metadata for logging' do
+ post :git_upload_pack, params: params
+ expect(controller).to have_received(:append_info_to_payload)
+ expect(log_payload.dig(:metadata, :repository_storage)).to eq(repository_storage)
+ end
+ end
end
diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb
index 94aedf463e9..9453520341b 100644
--- a/spec/controllers/search_controller_spec.rb
+++ b/spec/controllers/search_controller_spec.rb
@@ -537,6 +537,34 @@ RSpec.describe SearchController, feature_category: :global_search do
expect(response.headers['Cache-Control']).to eq('max-age=60, private')
expect(response.headers['Pragma']).to be_nil
end
+
+ context 'unique users tracking' do
+ before do
+ allow(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event)
+ end
+
+ it_behaves_like 'tracking unique hll events' do
+ subject(:request) { get :autocomplete, params: { term: 'term' } }
+
+ let(:target_event) { 'i_search_total' }
+ let(:expected_value) { instance_of(String) }
+ end
+ end
+
+ it_behaves_like 'Snowplow event tracking with RedisHLL context' do
+ subject { get :autocomplete, params: { group_id: namespace.id, term: 'term' } }
+
+ let(:project) { nil }
+ let(:category) { described_class.to_s }
+ let(:action) { 'autocomplete' }
+ let(:label) { 'redis_hll_counters.search.search_total_unique_counts_monthly' }
+ let(:property) { 'i_search_total' }
+ let(:context) do
+ [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: property).to_context]
+ end
+
+ let(:namespace) { create(:group) }
+ end
end
describe '#append_info_to_payload' do
diff --git a/spec/db/docs_spec.rb b/spec/db/docs_spec.rb
index 8d4cb3ac5ef..19edf3da0d5 100644
--- a/spec/db/docs_spec.rb
+++ b/spec/db/docs_spec.rb
@@ -14,6 +14,7 @@ RSpec.shared_examples 'validate dictionary' do |objects, directory_path, require
introduced_by_url
milestone
gitlab_schema
+ schema_inconsistencies
]
end
@@ -169,7 +170,8 @@ RSpec.shared_examples 'validate dictionary' do |objects, directory_path, require
end
RSpec.describe 'Views documentation', feature_category: :database do
- database_base_models = Gitlab::Database.database_base_models.select { |k, _| k != 'geo' }
+ excluded = %w[geo jh]
+ database_base_models = Gitlab::Database.database_base_models.reject { |k, _| k.in?(excluded) }
views = database_base_models.flat_map { |_, m| m.connection.views }.sort.uniq
directory_path = File.join('db', 'docs', 'views')
required_fields = %i[feature_categories view_name gitlab_schema]
@@ -178,7 +180,8 @@ RSpec.describe 'Views documentation', feature_category: :database do
end
RSpec.describe 'Tables documentation', feature_category: :database do
- database_base_models = Gitlab::Database.database_base_models.select { |k, _| k != 'geo' }
+ excluded = %w[geo jh]
+ database_base_models = Gitlab::Database.database_base_models.reject { |k, _| k.in?(excluded) }
tables = database_base_models.flat_map { |_, m| m.connection.tables }.sort.uniq
directory_path = File.join('db', 'docs')
required_fields = %i[feature_categories table_name gitlab_schema]
diff --git a/spec/db/migration_spec.rb b/spec/db/migration_spec.rb
index b7a4a302290..784f8753893 100644
--- a/spec/db/migration_spec.rb
+++ b/spec/db/migration_spec.rb
@@ -8,7 +8,8 @@ RSpec.describe 'Migrations Validation', feature_category: :database do
# The range describes the timestamps that given migration helper can be used
let(:all_migration_classes) do
{
- 2022_12_01_02_15_00.. => Gitlab::Database::Migration[2.1],
+ 2023_10_10_02_15_00.. => Gitlab::Database::Migration[2.2],
+ 2022_12_01_02_15_00..2023_11_01_02_15_00 => Gitlab::Database::Migration[2.1],
2022_01_26_21_06_58..2023_01_11_12_45_12 => Gitlab::Database::Migration[2.0],
2021_09_01_15_33_24..2022_04_25_12_06_03 => Gitlab::Database::Migration[1.0],
2021_05_31_05_39_16..2021_09_01_15_33_24 => ActiveRecord::Migration[6.1],
diff --git a/spec/experiments/ios_specific_templates_experiment_spec.rb b/spec/experiments/ios_specific_templates_experiment_spec.rb
deleted file mode 100644
index 909ac22b97b..00000000000
--- a/spec/experiments/ios_specific_templates_experiment_spec.rb
+++ /dev/null
@@ -1,62 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe IosSpecificTemplatesExperiment do
- subject do
- described_class.new(actor: user, project: project) do |e|
- e.candidate { true }
- end.run
- end
-
- let_it_be(:user) { create(:user) }
- let_it_be(:project) { create(:project, :auto_devops_disabled) }
-
- let!(:project_setting) { create(:project_setting, project: project, target_platforms: target_platforms) }
- let(:target_platforms) { %w[ios] }
-
- before do
- stub_experiments(ios_specific_templates: :candidate)
- project.add_developer(user) if user
- end
-
- it { is_expected.to be true }
-
- describe 'skipping the experiment' do
- context 'no actor' do
- let_it_be(:user) { nil }
-
- it { is_expected.to be_falsey }
- end
-
- context 'actor cannot create pipelines' do
- before do
- project.add_guest(user)
- end
-
- it { is_expected.to be_falsey }
- end
-
- context 'targeting a non iOS platform' do
- let(:target_platforms) { [] }
-
- it { is_expected.to be_falsey }
- end
-
- context 'project has a ci.yaml file' do
- before do
- allow(project).to receive(:has_ci?).and_return(true)
- end
-
- it { is_expected.to be_falsey }
- end
-
- context 'project has pipelines' do
- before do
- create(:ci_pipeline, project: project)
- end
-
- it { is_expected.to be_falsey }
- end
- end
-end
diff --git a/spec/factories/activity_pub/releases_subscriptions.rb b/spec/factories/activity_pub/releases_subscriptions.rb
new file mode 100644
index 00000000000..b789188528a
--- /dev/null
+++ b/spec/factories/activity_pub/releases_subscriptions.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :activity_pub_releases_subscription, class: 'ActivityPub::ReleasesSubscription' do
+ project
+ subscriber_url { 'https://example.com/actor' }
+ status { :requested }
+ payload do
+ {
+ '@context': 'https://www.w3.org/ns/activitystreams',
+ id: 'https://example.com/actor#follow/1',
+ type: 'Follow',
+ actor: 'https://example.com/actor',
+ object: 'http://localhost/user/project/-/releases'
+ }
+ end
+
+ trait :inbox do
+ subscriber_inbox_url { 'https://example.com/actor/inbox' }
+ end
+
+ trait :shared_inbox do
+ shared_inbox_url { 'https://example.com/shared-inbox' }
+ end
+ end
+end
diff --git a/spec/factories/ai/service_access_tokens.rb b/spec/factories/ai/service_access_tokens.rb
index 61abf4e1144..0598eed52c4 100644
--- a/spec/factories/ai/service_access_tokens.rb
+++ b/spec/factories/ai/service_access_tokens.rb
@@ -4,7 +4,6 @@ FactoryBot.define do
factory :service_access_token, class: 'Ai::ServiceAccessToken' do
token { SecureRandom.alphanumeric(10) }
expires_at { Time.current + 1.day }
- category { :code_suggestions }
trait :active do
expires_at { Time.current + 1.day }
@@ -13,9 +12,5 @@ FactoryBot.define do
trait :expired do
expires_at { Time.current - 1.day }
end
-
- trait :code_suggestions do
- category { :code_suggestions }
- end
end
end
diff --git a/spec/factories/ci/build_trace_chunks.rb b/spec/factories/ci/build_trace_chunks.rb
index 64a297932de..56d8fc23222 100644
--- a/spec/factories/ci/build_trace_chunks.rb
+++ b/spec/factories/ci/build_trace_chunks.rb
@@ -22,6 +22,22 @@ FactoryBot.define do
data_store { :redis }
end
+ trait :redis_trace_chunks_with_data do
+ data_store { :redis_trace_chunks }
+
+ transient do
+ initial_data { 'test data' }
+ end
+
+ after(:create) do |build_trace_chunk, evaluator|
+ Ci::BuildTraceChunks::RedisTraceChunks.new.set_data(build_trace_chunk, evaluator.initial_data)
+ end
+ end
+
+ trait :redis_trace_chunks_without_data do
+ data_store { :redis_trace_chunks }
+ end
+
trait :database_with_data do
data_store { :database }
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index 867db96aaaf..18415a6079f 100644
--- a/spec/factories/ci/builds.rb
+++ b/spec/factories/ci/builds.rb
@@ -117,6 +117,11 @@ FactoryBot.define do
status { 'running' }
end
+ trait :waiting_for_callback do
+ started
+ status { 'waiting_for_callback' }
+ end
+
trait :pending do
with_token
queued_at { 'Di 29. Okt 09:50:59 CET 2013' }
diff --git a/spec/factories/ci/catalog/resources/components.rb b/spec/factories/ci/catalog/resources/components.rb
index 8feecc695bc..843ccb2b461 100644
--- a/spec/factories/ci/catalog/resources/components.rb
+++ b/spec/factories/ci/catalog/resources/components.rb
@@ -5,6 +5,6 @@ FactoryBot.define do
version factory: :ci_catalog_resource_version
catalog_resource { version.catalog_resource }
project { version.project }
- name { catalog_resource.name }
+ name { catalog_resource.project.name }
end
end
diff --git a/spec/factories/ci/pipeline_artifacts.rb b/spec/factories/ci/pipeline_artifacts.rb
index bdd390126dd..77b1ac5a9cc 100644
--- a/spec/factories/ci/pipeline_artifacts.rb
+++ b/spec/factories/ci/pipeline_artifacts.rb
@@ -22,14 +22,6 @@ FactoryBot.define do
locked { :unlocked }
end
- trait :checksummed do
- verification_checksum { 'abc' }
- end
-
- trait :checksum_failure do
- verification_failure { 'Could not calculate the checksum' }
- end
-
trait :expired do
expire_at { Date.yesterday }
end
diff --git a/spec/factories/ci/reports/security/findings.rb b/spec/factories/ci/reports/security/findings.rb
index 202c2789b45..670d833c1f8 100644
--- a/spec/factories/ci/reports/security/findings.rb
+++ b/spec/factories/ci/reports/security/findings.rb
@@ -10,7 +10,7 @@ FactoryBot.define do
metadata_version { 'sast:1.0' }
name { 'Cipher with no integrity' }
report_type { :sast }
- cvss { [{ vendor: "GitLab", vector_string: "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:L/I:L/A:N" }] }
+ cvss { [{ vendor: "GitLab", vector: "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:L/I:L/A:N" }] }
original_data do
{
description: "The cipher does not provide data integrity update 1",
diff --git a/spec/factories/commit_statuses.rb b/spec/factories/commit_statuses.rb
index 7d0176d0683..e0b34bc39d8 100644
--- a/spec/factories/commit_statuses.rb
+++ b/spec/factories/commit_statuses.rb
@@ -32,6 +32,10 @@ FactoryBot.define do
status { 'running' }
end
+ trait :waiting_for_callback do
+ status { 'waiting_for_callback' }
+ end
+
trait :pending do
status { 'pending' }
end
diff --git a/spec/factories/ml/model_metadata.rb b/spec/factories/ml/model_metadata.rb
new file mode 100644
index 00000000000..03ceaffa6bc
--- /dev/null
+++ b/spec/factories/ml/model_metadata.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :ml_model_metadata, class: '::Ml::ModelMetadata' do
+ association :model, factory: :ml_models
+
+ sequence(:name) { |n| "metadata_#{n}" }
+ sequence(:value) { |n| "value#{n}" }
+ end
+end
diff --git a/spec/factories/ml/models.rb b/spec/factories/ml/models.rb
index 158c26499b0..3377a54f265 100644
--- a/spec/factories/ml/models.rb
+++ b/spec/factories/ml/models.rb
@@ -18,5 +18,11 @@ FactoryBot.define do
versions { [version] }
latest_version { version }
end
+
+ trait :with_metadata do
+ after(:create) do |model|
+ model.metadata = FactoryBot.create_list(:ml_model_metadata, 2, model: model) # rubocop:disable StrategyInCallback
+ end
+ end
end
end
diff --git a/spec/factories/packages/npm/metadata_cache.rb b/spec/factories/packages/npm/metadata_cache.rb
index e76ddf3c983..4fe1930d03e 100644
--- a/spec/factories/packages/npm/metadata_cache.rb
+++ b/spec/factories/packages/npm/metadata_cache.rb
@@ -6,5 +6,15 @@ FactoryBot.define do
sequence(:package_name) { |n| "@#{project.root_namespace.path}/package-#{n}" }
file { fixture_file_upload('spec/fixtures/packages/npm/metadata.json') }
size { 401.bytes }
+
+ trait :processing do
+ status { 'processing' }
+ end
+
+ trait :stale do
+ after(:create) do |entry|
+ entry.update_attribute(:project_id, nil)
+ end
+ end
end
end
diff --git a/spec/factories/packages/nuget/symbol.rb b/spec/factories/packages/nuget/symbol.rb
index 7ab1e026cda..665535de939 100644
--- a/spec/factories/packages/nuget/symbol.rb
+++ b/spec/factories/packages/nuget/symbol.rb
@@ -7,5 +7,6 @@ FactoryBot.define do
file_path { 'lib/net7.0/package.pdb' }
size { 100.bytes }
sequence(:signature) { |n| "b91a152048fc4b3883bf3cf73fbc03f#{n}FFFFFFFF" }
+ file_sha256 { 'dd1aaf26c557685cc37f93f53a2b6befb2c2e679f5ace6ec7a26d12086f358be' }
end
end
diff --git a/spec/factories/pages_domains.rb b/spec/factories/pages_domains.rb
index d91037e803f..5b78c83251a 100644
--- a/spec/factories/pages_domains.rb
+++ b/spec/factories/pages_domains.rb
@@ -150,6 +150,87 @@ NVOFBkpdn627G190
end
end
+ trait :with_untrusted_root_ca_in_chain do
+ # This contains
+ # [Intermediate #2 (SHA-2)] 'CloudFlare Origin SSL Certificate Authority'
+ # [Intermediate #1 (SHA-2)] 'CloudFlare Origin Certificate'
+ certificate do
+ '-----BEGIN CERTIFICATE-----
+MIIGCDCCA/CgAwIBAgIQKy5u6tl1NmwUim7bo3yMBzANBgkqhkiG9w0BAQwFADCB
+hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G
+A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV
+BAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTQwMjEy
+MDAwMDAwWhcNMjkwMjExMjM1OTU5WjCBkDELMAkGA1UEBhMCR0IxGzAZBgNVBAgT
+EkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR
+Q09NT0RPIENBIExpbWl0ZWQxNjA0BgNVBAMTLUNPTU9ETyBSU0EgRG9tYWluIFZh
+bGlkYXRpb24gU2VjdXJlIFNlcnZlciBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEP
+ADCCAQoCggEBAI7CAhnhoFmk6zg1jSz9AdDTScBkxwtiBUUWOqigwAwCfx3M28Sh
+bXcDow+G+eMGnD4LgYqbSRutA776S9uMIO3Vzl5ljj4Nr0zCsLdFXlIvNN5IJGS0
+Qa4Al/e+Z96e0HqnU4A7fK31llVvl0cKfIWLIpeNs4TgllfQcBhglo/uLQeTnaG6
+ytHNe+nEKpooIZFNb5JPJaXyejXdJtxGpdCsWTWM/06RQ1A/WZMebFEh7lgUq/51
+UHg+TLAchhP6a5i84DuUHoVS3AOTJBhuyydRReZw3iVDpA3hSqXttn7IzW3uLh0n
+c13cRTCAquOyQQuvvUSH2rnlG51/ruWFgqUCAwEAAaOCAWUwggFhMB8GA1UdIwQY
+MBaAFLuvfgI9+qbxPISOre44mOzZMjLUMB0GA1UdDgQWBBSQr2o6lFoL2JDqElZz
+30O0Oija5zAOBgNVHQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNV
+HSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwGwYDVR0gBBQwEjAGBgRVHSAAMAgG
+BmeBDAECATBMBgNVHR8ERTBDMEGgP6A9hjtodHRwOi8vY3JsLmNvbW9kb2NhLmNv
+bS9DT01PRE9SU0FDZXJ0aWZpY2F0aW9uQXV0aG9yaXR5LmNybDBxBggrBgEFBQcB
+AQRlMGMwOwYIKwYBBQUHMAKGL2h0dHA6Ly9jcnQuY29tb2RvY2EuY29tL0NPTU9E
+T1JTQUFkZFRydXN0Q0EuY3J0MCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5jb21v
+ZG9jYS5jb20wDQYJKoZIhvcNAQEMBQADggIBAE4rdk+SHGI2ibp3wScF9BzWRJ2p
+mj6q1WZmAT7qSeaiNbz69t2Vjpk1mA42GHWx3d1Qcnyu3HeIzg/3kCDKo2cuH1Z/
+e+FE6kKVxF0NAVBGFfKBiVlsit2M8RKhjTpCipj4SzR7JzsItG8kO3KdY3RYPBps
+P0/HEZrIqPW1N+8QRcZs2eBelSaz662jue5/DJpmNXMyYE7l3YphLG5SEXdoltMY
+dVEVABt0iN3hxzgEQyjpFv3ZBdRdRydg1vs4O2xyopT4Qhrf7W8GjEXCBgCq5Ojc
+2bXhc3js9iPc0d1sjhqPpepUfJa3w/5Vjo1JXvxku88+vZbrac2/4EjxYoIQ5QxG
+V/Iz2tDIY+3GH5QFlkoakdH368+PUq4NCNk+qKBR6cGHdNXJ93SrLlP7u3r7l+L4
+HyaPs9Kg4DdbKDsx5Q5XLVq4rXmsXiBmGqW5prU5wfWYQ//u+aen/e7KJD2AFsQX
+j4rBYKEMrltDR5FL1ZoXX/nUh8HCjLfn4g8wGTeGrODcQgPmlKidrv0PJFGUzpII
+0fxQ8ANAe4hZ7Q7drNJ3gjTcBpUC2JD5Leo31Rpg0Gcg19hCC0Wvgmje3WYkN5Ap
+lBlGGSW4gNfL1IYoakRwJiNiqZ+Gb7+6kHDSVneFeO/qJakXzlByjAA6quPbYzSf
++AZxAeKCINT+b72x
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCB
+hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G
+A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV
+BAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMTE5
+MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgT
+EkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR
+Q09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNh
+dGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCR
+6FSS0gpWsawNJN3Fz0RndJkrN6N9I3AAcbxT38T6KhKPS38QVr2fcHK3YX/JSw8X
+pz3jsARh7v8Rl8f0hj4K+j5c+ZPmNHrZFGvnnLOFoIJ6dq9xkNfs/Q36nGz637CC
+9BR++b7Epi9Pf5l/tfxnQ3K9DADWietrLNPtj5gcFKt+5eNu/Nio5JIk2kNrYrhV
+/erBvGy2i/MOjZrkm2xpmfh4SDBF1a3hDTxFYPwyllEnvGfDyi62a+pGx8cgoLEf
+Zd5ICLqkTqnyg0Y3hOvozIFIQ2dOciqbXL1MGyiKXCJ7tKuY2e7gUYPDCUZObT6Z
++pUX2nwzV0E8jVHtC7ZcryxjGt9XyD+86V3Em69FmeKjWiS0uqlWPc9vqv9JWL7w
+qP/0uK3pN/u6uPQLOvnoQ0IeidiEyxPx2bvhiWC4jChWrBQdnArncevPDt09qZah
+SL0896+1DSJMwBGB7FY79tOi4lu3sgQiUpWAk2nojkxl8ZEDLXB0AuqLZxUpaVIC
+u9ffUGpVRr+goyhhf3DQw6KqLCGqR84onAZFdr+CGCe01a60y1Dma/RMhnEw6abf
+Fobg2P9A3fvQQoh/ozM6LlweQRGBY84YcWsr7KaKtzFcOmpH4MN5WdYgGq/yapiq
+crxXStJLnbsQ/LBMQeXtHT1eKJ2czL+zUdqnR+WEUwIDAQABo0IwQDAdBgNVHQ4E
+FgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB
+/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAArx1UaEt65Ru2yyTUEUAJNMnMvl
+wFTPoCWOAvn9sKIN9SCYPBMtrFaisNZ+EZLpLrqeLppysb0ZRGxhNaKatBYSaVqM
+4dc+pBroLwP0rmEdEBsqpIt6xf4FpuHA1sj+nq6PK7o9mfjYcwlYRm6mnPTXJ9OV
+2jeDchzTc+CiR5kDOF3VSXkAKRzH7JsgHAckaVd4sjn8OoSgtZx8jb8uk2Intzna
+FxiuvTwJaP+EmzzV1gsD41eeFPfR60/IvYcjt7ZJQ3mFXLrrkguhxuhoqEwWsRqZ
+CuhTLJK7oQkYdQxlqHvLI7cawiiFwxv/0Cti76R7CZGYZ4wUAc1oBmpjIXUDgIiK
+boHGhfKppC3n9KUkEEeDys30jXlYsQab5xoq2Z0B15R97QNKyvDb6KkBPvVWmcke
+jkk9u+UJueBPSZI9FoJAzMxZxuY67RIuaTxslbH9qh17f4a+Hg4yRvv7E491f0yL
+S0Zj/gA0QHDBw7mh3aZw4gSzQbzpgJHqZJx64SIDqZxubw5lT2yHh17zbqD5daWb
+QOhTsiedSrnAdyGN/4fy3ryM7xfft0kL0fJuMAsaDk527RH89elWsn2/x20Kk4yl
+0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7ILaZRfyHB
+NVOFBkpdn627G190
+-----END CERTIFICATE-----'
+ end
+
+ key do
+ File.read(Rails.root.join('spec/fixtures/', 'origin_cert_key.pem'))
+ end
+ end
+
trait :with_trusted_expired_chain do
# This contains
# Let's Encrypt Authority X3
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index 443bca6030c..1e3ade779af 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -94,6 +94,8 @@ FactoryBot.define do
visibility_level: evaluator.visibility_level
}
+ project_namespace_hash[:id] = evaluator.project_namespace_id.presence
+
project.build_project_namespace(project_namespace_hash)
project.build_project_feature(project_feature_hash)
@@ -256,6 +258,35 @@ FactoryBot.define do
end
end
+ # A catalog resource repository with a file structure set up for ci components.
+ trait :catalog_resource_with_components do
+ small_repo
+ description { 'catalog resource' }
+
+ files do
+ {
+ 'templates/secret-detection.yml' => "spec:\n inputs:\n website:\n---\nimage: alpine_1",
+ 'templates/dast/template.yml' => 'image: alpine_2',
+ 'templates/template.yml' => 'image: alpine_3',
+ 'templates/blank-yaml.yml' => '',
+ 'README.md' => 'readme'
+ }
+ end
+
+ transient do
+ create_tag { nil }
+ end
+
+ after(:create) do |project, evaluator|
+ if evaluator.create_tag
+ project.repository.add_tag(
+ project.creator,
+ evaluator.create_tag,
+ project.repository.commit.sha)
+ end
+ end
+ end
+
# A basic repository with a single file 'test.txt'. It also has the HEAD as the default branch.
trait :small_repo do
custom_repo
@@ -477,7 +508,7 @@ FactoryBot.define do
trait :pages_published do
after(:create) do |project|
project.mark_pages_onboarding_complete
- project.mark_pages_as_deployed
+ create(:pages_deployment, project: project) # rubocop: disable RSpec/FactoryBot/StrategyInCallback
end
end
diff --git a/spec/factories/snippet_repositories.rb b/spec/factories/snippet_repositories.rb
index c3a6bc3ae31..1f9e68514bb 100644
--- a/spec/factories/snippet_repositories.rb
+++ b/spec/factories/snippet_repositories.rb
@@ -8,13 +8,5 @@ FactoryBot.define do
snippet_repository.shard_name = snippet_repository.snippet.repository_storage
snippet_repository.disk_path = snippet_repository.snippet.disk_path
end
-
- trait(:checksummed) do
- verification_checksum { 'abc' }
- end
-
- trait(:checksum_failure) do
- verification_failure { 'Could not calculate the checksum' }
- end
end
end
diff --git a/spec/factories/terraform/state_version.rb b/spec/factories/terraform/state_version.rb
index c6bd08815cf..5386dfa98f2 100644
--- a/spec/factories/terraform/state_version.rb
+++ b/spec/factories/terraform/state_version.rb
@@ -8,13 +8,5 @@ FactoryBot.define do
sequence(:version)
file { fixture_file_upload('spec/fixtures/terraform/terraform.tfstate', 'application/json') }
-
- trait(:checksummed) do
- verification_checksum { 'abc' }
- end
-
- trait(:checksum_failure) do
- verification_failure { 'Could not calculate the checksum' }
- end
end
end
diff --git a/spec/factories/users.rb b/spec/factories/users.rb
index de2b5159fe7..8b42631040e 100644
--- a/spec/factories/users.rb
+++ b/spec/factories/users.rb
@@ -135,10 +135,6 @@ FactoryBot.define do
end
end
- trait :no_super_sidebar do
- use_new_navigation { false }
- end
-
trait :two_factor_via_webauthn do
transient { registrations_count { 5 } }
diff --git a/spec/factories/users/phone_number_validations.rb b/spec/factories/users/phone_number_validations.rb
index da53dda89b4..b7e6e819127 100644
--- a/spec/factories/users/phone_number_validations.rb
+++ b/spec/factories/users/phone_number_validations.rb
@@ -6,5 +6,6 @@ FactoryBot.define do
country { 'US' }
international_dial_code { 1 }
phone_number { '555' }
+ telesign_reference_xid { FFaker::Guid.guid }
end
end
diff --git a/spec/fast_spec_helper.rb b/spec/fast_spec_helper.rb
index 7ffab4554cf..d03f8b18b3e 100644
--- a/spec/fast_spec_helper.rb
+++ b/spec/fast_spec_helper.rb
@@ -8,6 +8,7 @@ end
require_relative '../config/bundler_setup'
+ENV['GITLAB_ENV'] = 'test'
ENV['IN_MEMORY_APPLICATION_SETTINGS'] = 'true'
require './spec/deprecation_warnings'
diff --git a/spec/features/abuse_report_spec.rb b/spec/features/abuse_report_spec.rb
index f1df5c2d6f0..eac29b0b741 100644
--- a/spec/features/abuse_report_spec.rb
+++ b/spec/features/abuse_report_spec.rb
@@ -3,9 +3,9 @@
require 'spec_helper'
RSpec.describe 'Abuse reports', :js, feature_category: :insider_threat do
- let_it_be(:abusive_user) { create(:user, :no_super_sidebar) }
+ let_it_be(:abusive_user) { create(:user) }
- let_it_be(:reporter1) { create(:user, :no_super_sidebar) }
+ let_it_be(:reporter1) { create(:user) }
let_it_be(:project) { create(:project, :public, :repository) }
let_it_be(:issue) { create(:issue, project: project, author: abusive_user) }
@@ -55,103 +55,53 @@ RSpec.describe 'Abuse reports', :js, feature_category: :insider_threat do
it_behaves_like 'reports the user with an abuse category'
end
- describe 'when user_profile_overflow_menu FF turned on' do
- context 'when reporting a user profile for abuse' do
- let_it_be(:reporter2) { create(:user, :no_super_sidebar) }
+ context 'when reporting a user profile for abuse' do
+ let_it_be(:reporter2) { create(:user) }
- before do
- visit user_path(abusive_user)
- find_by_testid('base-dropdown-toggle').click
- end
-
- it_behaves_like 'reports the user with an abuse category'
-
- it 'allows the reporter to report the same user for different abuse categories' do
- visit user_path(abusive_user)
-
- find_by_testid('base-dropdown-toggle').click
- fill_and_submit_abuse_category_form
- fill_and_submit_report_abuse_form
-
- expect(page).to have_content 'Thank you for your report'
-
- visit user_path(abusive_user)
-
- find_by_testid('base-dropdown-toggle').click
- fill_and_submit_abuse_category_form("They're being offensive or abusive.")
- fill_and_submit_report_abuse_form
-
- expect(page).to have_content 'Thank you for your report'
- end
-
- it 'allows multiple users to report the same user' do
- fill_and_submit_abuse_category_form
- fill_and_submit_report_abuse_form
-
- expect(page).to have_content 'Thank you for your report'
-
- gitlab_sign_out
- gitlab_sign_in(reporter2)
-
- visit user_path(abusive_user)
-
- find_by_testid('base-dropdown-toggle').click
- fill_and_submit_abuse_category_form
- fill_and_submit_report_abuse_form
-
- expect(page).to have_content 'Thank you for your report'
- end
-
- it_behaves_like 'cancel report'
+ before do
+ visit user_path(abusive_user)
+ find_by_testid('user-profile-actions').click
end
- end
-
- describe 'when user_profile_overflow_menu FF turned off' do
- context 'when reporting a user profile for abuse' do
- let_it_be(:reporter2) { create(:user, :no_super_sidebar) }
- before do
- stub_feature_flags(user_profile_overflow_menu_vue: false)
- visit user_path(abusive_user)
- end
-
- it_behaves_like 'reports the user with an abuse category'
-
- it 'allows the reporter to report the same user for different abuse categories' do
- visit user_path(abusive_user)
+ it_behaves_like 'reports the user with an abuse category'
- fill_and_submit_abuse_category_form
- fill_and_submit_report_abuse_form
+ it 'allows the reporter to report the same user for different abuse categories' do
+ visit user_path(abusive_user)
- expect(page).to have_content 'Thank you for your report'
+ find_by_testid('user-profile-actions').click
+ fill_and_submit_abuse_category_form
+ fill_and_submit_report_abuse_form
- visit user_path(abusive_user)
+ expect(page).to have_content 'Thank you for your report'
- fill_and_submit_abuse_category_form("They're being offensive or abusive.")
- fill_and_submit_report_abuse_form
+ visit user_path(abusive_user)
- expect(page).to have_content 'Thank you for your report'
- end
+ find_by_testid('user-profile-actions').click
+ fill_and_submit_abuse_category_form("They're being offensive or abusive.")
+ fill_and_submit_report_abuse_form
- it 'allows multiple users to report the same user' do
- fill_and_submit_abuse_category_form
- fill_and_submit_report_abuse_form
+ expect(page).to have_content 'Thank you for your report'
+ end
- expect(page).to have_content 'Thank you for your report'
+ it 'allows multiple users to report the same user', :js do
+ fill_and_submit_abuse_category_form
+ fill_and_submit_report_abuse_form
- gitlab_sign_out
- gitlab_sign_in(reporter2)
+ expect(page).to have_content 'Thank you for your report'
- visit user_path(abusive_user)
+ gitlab_sign_out
+ gitlab_sign_in(reporter2)
- fill_and_submit_abuse_category_form
- fill_and_submit_report_abuse_form
+ visit user_path(abusive_user)
- expect(page).to have_content 'Thank you for your report'
- end
+ find_by_testid('user-profile-actions').click
+ fill_and_submit_abuse_category_form
+ fill_and_submit_report_abuse_form
- it_behaves_like 'cancel report'
+ expect(page).to have_content 'Thank you for your report'
end
+
+ it_behaves_like 'cancel report'
end
context 'when reporting an merge request for abuse' do
@@ -180,10 +130,6 @@ RSpec.describe 'Abuse reports', :js, feature_category: :insider_threat do
end
end
- # TODO: implement tests before the FF "user_profile_overflow_menu_vue" is turned on
- # See: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/122971
- # Related Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/416983
-
private
def fill_and_submit_abuse_category_form(category = "They're posting spam.")
diff --git a/spec/features/admin/admin_browse_spam_logs_spec.rb b/spec/features/admin/admin_browse_spam_logs_spec.rb
index c272a8630b7..f781e2adf07 100644
--- a/spec/features/admin/admin_browse_spam_logs_spec.rb
+++ b/spec/features/admin/admin_browse_spam_logs_spec.rb
@@ -4,9 +4,9 @@ require 'spec_helper'
RSpec.describe 'Admin browse spam logs', feature_category: :shared do
let!(:spam_log) { create(:spam_log, description: 'abcde ' * 20) }
+ let(:admin) { create(:admin) }
before do
- admin = create(:admin)
sign_in(admin)
gitlab_enable_admin_mode_sign_in(admin)
end
@@ -22,6 +22,7 @@ RSpec.describe 'Admin browse spam logs', feature_category: :shared do
expect(page).to have_content("#{spam_log.description[0...97]}...")
expect(page).to have_link('Remove user')
expect(page).to have_link('Block user')
+ expect(page).to have_link('Trust user')
end
it 'does not perform N+1 queries' do
@@ -30,4 +31,15 @@ RSpec.describe 'Admin browse spam logs', feature_category: :shared do
expect { visit admin_spam_logs_path }.not_to exceed_query_limit(control_queries)
end
+
+ context 'when user is trusted' do
+ before do
+ UserCustomAttribute.set_trusted_by(user: spam_log.user, trusted_by: admin)
+ end
+
+ it 'allows admin to untrust the user' do
+ visit admin_spam_logs_path
+ expect(page).to have_link('Untrust user')
+ end
+ end
end
diff --git a/spec/features/admin/admin_deploy_keys_spec.rb b/spec/features/admin/admin_deploy_keys_spec.rb
index f59b4db5cc2..f9510ef296a 100644
--- a/spec/features/admin/admin_deploy_keys_spec.rb
+++ b/spec/features/admin/admin_deploy_keys_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe 'admin deploy keys', :js, feature_category: :system_access do
it 'show all public deploy keys' do
visit admin_deploy_keys_path
- page.within(find('[data-testid="deploy-keys-list"]', match: :first)) do
+ within_testid('deploy-keys-list', match: :first) do
expect(page).to have_content(deploy_key.title)
expect(page).to have_content(another_deploy_key.title)
end
@@ -29,7 +29,7 @@ RSpec.describe 'admin deploy keys', :js, feature_category: :system_access do
visit admin_deploy_keys_path
- page.within(find('[data-testid="deploy-keys-list"]', match: :first)) do
+ within_testid('deploy-keys-list', match: :first) do
expect(page).to have_content(write_key.project.full_name)
end
end
@@ -49,7 +49,7 @@ RSpec.describe 'admin deploy keys', :js, feature_category: :system_access do
expect(page).to have_current_path admin_deploy_keys_path, ignore_query: true
- page.within(find('[data-testid="deploy-keys-list"]', match: :first)) do
+ within_testid('deploy-keys-list', match: :first) do
expect(page).to have_content('laptop')
end
end
@@ -69,7 +69,7 @@ RSpec.describe 'admin deploy keys', :js, feature_category: :system_access do
expect(page).to have_current_path admin_deploy_keys_path, ignore_query: true
- page.within(find('[data-testid="deploy-keys-list"]', match: :first)) do
+ within_testid('deploy-keys-list', match: :first) do
expect(page).to have_content('new-title')
end
end
@@ -88,7 +88,7 @@ RSpec.describe 'admin deploy keys', :js, feature_category: :system_access do
end
expect(page).to have_current_path admin_deploy_keys_path, ignore_query: true
- page.within(find('[data-testid="deploy-keys-list"]', match: :first)) do
+ within_testid('deploy-keys-list', match: :first) do
expect(page).not_to have_content(deploy_key.title)
end
end
diff --git a/spec/features/admin/admin_dev_ops_reports_spec.rb b/spec/features/admin/admin_dev_ops_reports_spec.rb
index f290464b043..99d43e6b0da 100644
--- a/spec/features/admin/admin_dev_ops_reports_spec.rb
+++ b/spec/features/admin/admin_dev_ops_reports_spec.rb
@@ -19,8 +19,8 @@ RSpec.describe 'DevOps Report page', :js, feature_category: :devops_reports do
expect(page).to have_content 'Introducing Your DevOps Report'
- page.within(find('[data-testid="devops-score-container"]')) do
- find('[data-testid="close-icon"]').click
+ within_testid('devops-score-container') do
+ find_by_testid('close-icon').click
end
expect(page).not_to have_content 'Introducing Your DevOps Report'
diff --git a/spec/features/admin/admin_groups_spec.rb b/spec/features/admin/admin_groups_spec.rb
index 1e3dbd7fea4..f071da1835a 100644
--- a/spec/features/admin/admin_groups_spec.rb
+++ b/spec/features/admin/admin_groups_spec.rb
@@ -138,7 +138,7 @@ RSpec.describe 'Admin Groups', feature_category: :groups_and_projects do
it 'shows access requests with link to manage access' do
visit admin_group_path(group)
- page.within '[data-testid="access-requests"]' do
+ within_testid('access-requests') do
expect(page).to have_content access_request.user.name
expect(page).to have_link 'Manage access', href: group_group_members_path(group, tab: 'access_requests')
end
diff --git a/spec/features/admin/admin_hooks_spec.rb b/spec/features/admin/admin_hooks_spec.rb
index a5acba1fe4a..2aec5baf351 100644
--- a/spec/features/admin/admin_hooks_spec.rb
+++ b/spec/features/admin/admin_hooks_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe 'Admin::Hooks', feature_category: :webhooks do
include Spec::Support::Helpers::ModalHelpers
- let_it_be(:user) { create(:admin, :no_super_sidebar) }
+ let_it_be(:user) { create(:admin) }
before do
sign_in(user)
@@ -13,10 +13,10 @@ RSpec.describe 'Admin::Hooks', feature_category: :webhooks do
end
describe 'GET /admin/hooks' do
- it 'is ok' do
+ it 'is ok', :js do
visit admin_root_path
- page.within '.nav-sidebar' do
+ within_testid('super-sidebar') do
click_on 'System Hooks', match: :first
end
diff --git a/spec/features/admin/admin_jobs_spec.rb b/spec/features/admin/admin_jobs_spec.rb
index b125974532b..b3e21d02354 100644
--- a/spec/features/admin/admin_jobs_spec.rb
+++ b/spec/features/admin/admin_jobs_spec.rb
@@ -132,7 +132,7 @@ RSpec.describe 'Admin Jobs', :js, feature_category: :continuous_integration do
within_testid('jobs-table') do
expect(page).to have_selector('[data-testid="jobs-table-row"]', count: 1)
- expect(find_by_testid('ci-badge-text')).to have_content('Failed')
+ expect(find_by_testid('ci-icon-text')).to have_content('Failed')
end
end
end
diff --git a/spec/features/admin/admin_mode/logout_spec.rb b/spec/features/admin/admin_mode/logout_spec.rb
index 5d9106fea02..7a33256e7a8 100644
--- a/spec/features/admin/admin_mode/logout_spec.rb
+++ b/spec/features/admin/admin_mode/logout_spec.rb
@@ -5,9 +5,8 @@ require 'spec_helper'
RSpec.describe 'Admin Mode Logout', :js, feature_category: :system_access do
include TermsHelper
include UserLoginHelper
- include Features::TopNavSpecHelpers
- let(:user) { create(:admin, :no_super_sidebar) }
+ let(:user) { create(:admin) }
before do
# TODO: This used to use gitlab_sign_in, instead of sign_in, but that is buggy. See
@@ -22,11 +21,9 @@ RSpec.describe 'Admin Mode Logout', :js, feature_category: :system_access do
expect(page).to have_current_path root_path, ignore_query: true
- open_top_nav
+ click_button 'Search or go to…'
- within_top_nav do
- expect(page).to have_link(href: new_admin_session_path)
- end
+ expect(page).to have_link(href: new_admin_session_path)
end
it 'disable shows flash notice' do
@@ -45,11 +42,9 @@ RSpec.describe 'Admin Mode Logout', :js, feature_category: :system_access do
expect(page).to have_current_path root_path, ignore_query: true
- open_top_nav
+ click_button 'Search or go to…'
- within_top_nav do
- expect(page).to have_link(href: new_admin_session_path)
- end
+ expect(page).to have_link(href: new_admin_session_path)
end
end
end
diff --git a/spec/features/admin/admin_mode/workers_spec.rb b/spec/features/admin/admin_mode/workers_spec.rb
index 2a862c750d7..124c43eef9d 100644
--- a/spec/features/admin/admin_mode/workers_spec.rb
+++ b/spec/features/admin/admin_mode/workers_spec.rb
@@ -6,8 +6,8 @@ require 'spec_helper'
RSpec.describe 'Admin mode for workers', :request_store, feature_category: :system_access do
include Features::AdminUsersHelpers
- let(:user) { create(:user, :no_super_sidebar) }
- let(:user_to_delete) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
+ let(:user_to_delete) { create(:user) }
before do
sign_in(user)
@@ -22,7 +22,7 @@ RSpec.describe 'Admin mode for workers', :request_store, feature_category: :syst
end
context 'as an admin user' do
- let(:user) { create(:admin, :no_super_sidebar) }
+ let(:user) { create(:admin) }
context 'when admin mode disabled' do
it 'cannot delete user', :js do
diff --git a/spec/features/admin/admin_mode_spec.rb b/spec/features/admin/admin_mode_spec.rb
index edfa58567ad..b1b44ce143f 100644
--- a/spec/features/admin/admin_mode_spec.rb
+++ b/spec/features/admin/admin_mode_spec.rb
@@ -4,10 +4,9 @@ require 'spec_helper'
RSpec.describe 'Admin mode', :js, feature_category: :shared do
include MobileHelpers
- include Features::TopNavSpecHelpers
include StubENV
- let(:admin) { create(:admin, :no_super_sidebar) }
+ let(:admin) { create(:admin) }
before do
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
@@ -21,20 +20,16 @@ RSpec.describe 'Admin mode', :js, feature_category: :shared do
context 'when not in admin mode' do
it 'has no leave admin mode button' do
visit new_admin_session_path
- open_top_nav
+ open_search_modal
- page.within('.navbar-sub-nav') do
- expect(page).not_to have_link(href: destroy_admin_session_path)
- end
+ expect(page).not_to have_link(href: destroy_admin_session_path)
end
it 'can open pages not in admin scope' do
visit new_admin_session_path
- open_top_nav_projects
+ open_search_modal
- within_top_nav do
- click_link('View all projects')
- end
+ click_link('View all my projects')
expect(page).to have_current_path(dashboard_projects_path)
end
@@ -78,29 +73,23 @@ RSpec.describe 'Admin mode', :js, feature_category: :shared do
end
it 'contains link to leave admin mode' do
- open_top_nav
+ open_search_modal
- within_top_nav do
- expect(page).to have_link(href: destroy_admin_session_path)
- end
+ expect(page).to have_link(href: destroy_admin_session_path)
end
it 'can leave admin mode using main dashboard link' do
gitlab_disable_admin_mode
- open_top_nav
+ open_search_modal
- within_top_nav do
- expect(page).to have_link(href: new_admin_session_path)
- end
+ expect(page).to have_link(href: new_admin_session_path)
end
it 'can open pages not in admin scope' do
- open_top_nav_projects
+ open_search_modal
- within_top_nav do
- click_link('View all projects')
- end
+ click_link('View all my projects')
expect(page).to have_current_path(dashboard_projects_path)
end
@@ -108,7 +97,7 @@ RSpec.describe 'Admin mode', :js, feature_category: :shared do
context 'nav bar' do
it 'shows admin dashboard links on bigger screen' do
visit root_dashboard_path
- open_top_nav
+ open_search_modal
expect(page).to have_link(text: 'Admin', href: admin_root_path, visible: true)
expect(page).to have_link(text: 'Leave admin mode', href: destroy_admin_session_path, visible: true)
@@ -123,11 +112,9 @@ RSpec.describe 'Admin mode', :js, feature_category: :shared do
it 'can leave admin mode' do
gitlab_disable_admin_mode
- open_top_nav
+ open_search_modal
- within_top_nav do
- expect(page).to have_link(href: new_admin_session_path)
- end
+ expect(page).to have_link(href: new_admin_session_path)
end
end
end
@@ -141,10 +128,14 @@ RSpec.describe 'Admin mode', :js, feature_category: :shared do
it 'shows no admin mode buttons in navbar' do
visit admin_root_path
- open_top_nav
+ open_search_modal
expect(page).not_to have_link(href: new_admin_session_path)
expect(page).not_to have_link(href: destroy_admin_session_path)
end
end
+
+ def open_search_modal
+ click_button 'Search or go to…'
+ end
end
diff --git a/spec/features/admin/admin_projects_spec.rb b/spec/features/admin/admin_projects_spec.rb
index 3454b7af962..b793299e253 100644
--- a/spec/features/admin/admin_projects_spec.rb
+++ b/spec/features/admin/admin_projects_spec.rb
@@ -95,7 +95,7 @@ RSpec.describe "Admin::Projects", feature_category: :groups_and_projects do
context 'when project has open access requests' do
it 'shows access requests with link to manage access' do
- page.within '[data-testid="access-requests"]' do
+ within_testid('access-requests') do
expect(page).to have_content access_request.user.name
expect(page).to have_link 'Manage access', href: project_project_members_path(project, tab: 'access_requests')
end
diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb
index 9edd970532e..750f5f8d4b9 100644
--- a/spec/features/admin/admin_runners_spec.rb
+++ b/spec/features/admin/admin_runners_spec.rb
@@ -100,7 +100,7 @@ RSpec.describe "Admin Runners", feature_category: :runner_fleet do
visit admin_runners_path
within_runner_row(runner.id) do
- expect(find("[data-testid='job-count']")).to have_content '2'
+ expect(find_by_testid('job-count')).to have_content '2'
end
end
@@ -116,8 +116,8 @@ RSpec.describe "Admin Runners", feature_category: :runner_fleet do
expect(current_url).to match(admin_runner_path(runner))
- expect(find("[data-testid='td-status']")).to have_content "Running"
- expect(find("[data-testid='td-job']")).to have_content "##{job.id}"
+ expect(find_by_testid('td-status')).to have_content "Running"
+ expect(find_by_testid('td-job')).to have_content "##{job.id}"
end
describe 'search' do
@@ -202,6 +202,36 @@ RSpec.describe "Admin Runners", feature_category: :runner_fleet do
end
end
+ describe 'filter by version prefix' do
+ before_all do
+ runner_v15 = create(:ci_runner, :instance, description: 'runner-v15')
+ runner_v14 = create(:ci_runner, :instance, description: 'runner-v14')
+
+ create(:ci_runner_machine, runner: runner_v15, version: '15.0.0')
+ create(:ci_runner_machine, runner: runner_v14, version: '14.0.0')
+ end
+
+ before do
+ visit admin_runners_path
+ end
+
+ it 'shows all runners' do
+ expect(page).to have_link('All 2')
+
+ expect(page).to have_content 'runner-v15'
+ expect(page).to have_content 'runner-v14'
+ end
+
+ it 'shows filtered runner based on supplied prefix' do
+ input_filtered_search_filter_is_only(s_('Runners|Version starts with'), '15.0')
+
+ expect(page).to have_link('All 1')
+
+ expect(page).not_to have_content 'runner-v14'
+ expect(page).to have_content 'runner-v15'
+ end
+ end
+
describe 'filter by status' do
let_it_be(:never_contacted) do
create(:ci_runner, :instance, description: 'runner-never-contacted', contacted_at: nil)
@@ -291,7 +321,7 @@ RSpec.describe "Admin Runners", feature_category: :runner_fleet do
expect(page).to have_link('Group 1')
expect(page).to have_link('Project 1')
- page.within('[data-testid="runner-type-tabs"]') do
+ within_testid('runner-type-tabs') do
expect(page).to have_link('All', class: 'active')
end
end
@@ -302,7 +332,7 @@ RSpec.describe "Admin Runners", feature_category: :runner_fleet do
expect(page).to have_content 'runner-project'
expect(page).to have_content 'runner-group'
- page.within('[data-testid="runner-type-tabs"]') do
+ within_testid('runner-type-tabs') do
click_on('Project')
expect(page).to have_link('Project', class: 'active')
@@ -315,7 +345,7 @@ RSpec.describe "Admin Runners", feature_category: :runner_fleet do
it 'show the same counts after selecting another tab' do
visit admin_runners_path
- page.within('[data-testid="runner-type-tabs"]') do
+ within_testid('runner-type-tabs') do
click_on('Project')
expect(page).to have_link('All 2')
@@ -329,7 +359,7 @@ RSpec.describe "Admin Runners", feature_category: :runner_fleet do
visit admin_runners_path
- page.within('[data-testid="runner-type-tabs"]') do
+ within_testid('runner-type-tabs') do
click_on 'Project'
end
@@ -355,7 +385,7 @@ RSpec.describe "Admin Runners", feature_category: :runner_fleet do
expect(page).to have_content 'runner-group'
expect(page).not_to have_content 'runner-paused-project'
- page.within('[data-testid="runner-type-tabs"]') do
+ within_testid('runner-type-tabs') do
click_on 'Project'
end
@@ -367,7 +397,7 @@ RSpec.describe "Admin Runners", feature_category: :runner_fleet do
context 'when type does not match' do
before do
visit admin_runners_path
- page.within('[data-testid="runner-type-tabs"]') do
+ within_testid('runner-type-tabs') do
click_on 'Instance'
end
end
@@ -440,24 +470,28 @@ RSpec.describe "Admin Runners", feature_category: :runner_fleet do
visit admin_runners_path
- within '[data-testid="runner-list"] tbody tr:nth-child(1)' do
- expect(page).to have_content 'runner-2'
- end
+ within_testid('runner-list') do
+ within('tbody tr:nth-child(1)') do
+ expect(page).to have_content 'runner-2'
+ end
- within '[data-testid="runner-list"] tbody tr:nth-child(2)' do
- expect(page).to have_content 'runner-1'
+ within('tbody tr:nth-child(2)') do
+ expect(page).to have_content 'runner-1'
+ end
end
click_on 'Created date' # Open "sort by" dropdown
click_on 'Last contact'
click_on 'Sort direction: Descending'
- within '[data-testid="runner-list"] tbody tr:nth-child(1)' do
- expect(page).to have_content 'runner-1'
- end
+ within_testid('runner-list') do
+ within('tbody tr:nth-child(1)') do
+ expect(page).to have_content 'runner-1'
+ end
- within '[data-testid="runner-list"] tbody tr:nth-child(2)' do
- expect(page).to have_content 'runner-2'
+ within('tbody tr:nth-child(2)') do
+ expect(page).to have_content 'runner-2'
+ end
end
end
end
@@ -522,8 +556,8 @@ RSpec.describe "Admin Runners", feature_category: :runner_fleet do
describe 'runner show page breadcrumbs' do
it 'contains the current runner id and token' do
- page.within '[data-testid="breadcrumb-links"]' do
- expect(page.find('[data-testid="breadcrumb-current-link"]')).to have_link(
+ within_testid('breadcrumb-links') do
+ expect(find_by_testid('breadcrumb-current-link')).to have_link(
"##{runner.id} (#{runner.short_sha})"
)
end
@@ -555,7 +589,7 @@ RSpec.describe "Admin Runners", feature_category: :runner_fleet do
end
it 'deletes runner and redirects to runner list' do
- expect(page.find('[data-testid="alert-success"]')).to have_content('deleted')
+ expect(find_by_testid('alert-success')).to have_content('deleted')
expect(current_url).to match(admin_runners_path)
end
end
@@ -581,9 +615,9 @@ RSpec.describe "Admin Runners", feature_category: :runner_fleet do
describe 'breadcrumbs' do
it 'contains the current runner id and token' do
- page.within '[data-testid="breadcrumb-links"]' do
+ within_testid('breadcrumb-links') do
expect(page).to have_link("##{project_runner.id} (#{project_runner.short_sha})")
- expect(page.find('[data-testid="breadcrumb-current-link"]')).to have_content("Edit")
+ expect(find_by_testid('breadcrumb-current-link')).to have_content("Edit")
end
end
end
@@ -591,7 +625,7 @@ RSpec.describe "Admin Runners", feature_category: :runner_fleet do
describe 'runner header', :js do
it 'contains the runner status, type and id' do
expect(page).to have_content(
- "##{project_runner.id} (#{project_runner.short_sha}) #{s_('Runners|Never contacted')} Project created"
+ "##{project_runner.id} (#{project_runner.short_sha}) #{s_('Runners|Never contacted')} Project Created"
)
end
end
@@ -604,7 +638,7 @@ RSpec.describe "Admin Runners", feature_category: :runner_fleet do
it 'show success alert and redirects to runner page' do
expect(current_url).to match(admin_runner_path(project_runner))
- expect(page.find('[data-testid="alert-success"]')).to have_content('saved')
+ expect(find_by_testid('alert-success')).to have_content('saved')
end
end
@@ -631,11 +665,13 @@ RSpec.describe "Admin Runners", feature_category: :runner_fleet do
describe 'enable/create' do
shared_examples 'assignable runner' do
it 'enables a runner for a project' do
- within find('[data-testid="unassigned-projects"] tr', text: project2.full_name) do
- click_on 'Enable'
+ within_testid('unassigned-projects') do
+ within('tr', text: project2.full_name) do
+ click_on 'Enable'
+ end
end
- assigned_project = page.find('[data-testid="assigned-projects"]')
+ assigned_project = find_by_testid('assigned-projects')
expect(page).to have_content('Runner assigned to project.')
expect(assigned_project).to have_content(project2.name)
@@ -671,11 +707,11 @@ RSpec.describe "Admin Runners", feature_category: :runner_fleet do
end
it 'removed project runner from project' do
- within '[data-testid="assigned-projects"]' do
+ within_testid('assigned-projects') do
click_on 'Disable'
end
- new_runner_project = page.find('[data-testid="unassigned-projects"]')
+ new_runner_project = find_by_testid('unassigned-projects')
expect(page).to have_content('Runner unassigned from project.')
expect(new_runner_project).to have_content(project1.name)
diff --git a/spec/features/admin/admin_sees_background_migrations_spec.rb b/spec/features/admin/admin_sees_background_migrations_spec.rb
index 7423e74bf3a..ae307b8038c 100644
--- a/spec/features/admin/admin_sees_background_migrations_spec.rb
+++ b/spec/features/admin/admin_sees_background_migrations_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe "Admin > Admin sees background migrations", feature_category: :database do
include ListboxHelpers
- let_it_be(:admin) { create(:admin, :no_super_sidebar) }
+ let_it_be(:admin) { create(:admin) }
let(:job_class) { Gitlab::BackgroundMigration::CopyColumnUsingBackgroundMigrationJob }
let_it_be(:active_migration) { create(:batched_background_migration, :active, table_name: 'active') }
@@ -21,16 +21,18 @@ RSpec.describe "Admin > Admin sees background migrations", feature_category: :da
gitlab_enable_admin_mode_sign_in(admin)
end
- it 'can navigate to background migrations' do
+ it 'can navigate to background migrations', :js do
visit admin_root_path
- within '.nav-sidebar' do
- link = find_link 'Background Migrations'
+ within_testid('super-sidebar') do
+ click_on 'Monitoring'
+ click_on 'Background Migrations'
+ end
- link.click
+ expect(page).to have_current_path(admin_background_migrations_path)
- expect(page).to have_current_path(admin_background_migrations_path)
- expect(link).to have_ancestor(:css, 'li.active')
+ within_testid('super-sidebar') do
+ expect(page).to have_css('a[aria-current="page"]', text: 'Background Migrations')
end
end
diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb
index 1b10ea81333..4e0198b1f2b 100644
--- a/spec/features/admin/admin_settings_spec.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe 'Admin updates settings', feature_category: :shared do
include TermsHelper
include UsageDataHelpers
- let_it_be(:admin) { create(:admin, :no_super_sidebar) }
+ let_it_be(:admin) { create(:admin) }
context 'application setting :admin_mode is enabled', :request_store do
before do
@@ -22,7 +22,7 @@ RSpec.describe 'Admin updates settings', feature_category: :shared do
end
it 'change visibility settings' do
- page.within('[data-testid="admin-visibility-access-settings"]') do
+ within_testid('admin-visibility-access-settings') do
choose "application_setting_default_project_visibility_20"
click_button 'Save changes'
end
@@ -31,19 +31,19 @@ RSpec.describe 'Admin updates settings', feature_category: :shared do
end
it 'uncheck all restricted visibility levels' do
- page.within('[data-testid="restricted-visibility-levels"]') do
+ within_testid('restricted-visibility-levels') do
uncheck s_('VisibilityLevel|Public')
uncheck s_('VisibilityLevel|Internal')
uncheck s_('VisibilityLevel|Private')
end
- page.within('[data-testid="admin-visibility-access-settings"]') do
+ within_testid('admin-visibility-access-settings') do
click_button 'Save changes'
end
expect(page).to have_content "Application settings saved successfully"
- page.within('[data-testid="restricted-visibility-levels"]') do
+ within_testid('restricted-visibility-levels') do
expect(find_field(s_('VisibilityLevel|Public'))).not_to be_checked
expect(find_field(s_('VisibilityLevel|Internal'))).not_to be_checked
expect(find_field(s_('VisibilityLevel|Private'))).not_to be_checked
@@ -53,7 +53,7 @@ RSpec.describe 'Admin updates settings', feature_category: :shared do
it 'modify import sources' do
expect(current_settings.import_sources).to be_empty
- page.within('[data-testid="admin-import-export-settings"]') do
+ within_testid('admin-import-export-settings') do
check "Repository by URL"
click_button 'Save changes'
end
@@ -63,12 +63,12 @@ RSpec.describe 'Admin updates settings', feature_category: :shared do
end
it 'change Visibility and Access Controls' do
- page.within('[data-testid="admin-import-export-settings"]') do
- page.within('[data-testid="project-export"]') do
+ within_testid('admin-import-export-settings') do
+ within_testid('project-export') do
uncheck 'Enabled'
end
- page.within('[data-testid="bulk-import"]') do
+ within_testid('bulk-import') do
check 'Enabled'
end
@@ -81,7 +81,7 @@ RSpec.describe 'Admin updates settings', feature_category: :shared do
end
it 'change Keys settings' do
- page.within('[data-testid="admin-visibility-access-settings"]') do
+ within_testid('admin-visibility-access-settings') do
select 'Are forbidden', from: 'RSA SSH keys'
select 'Are allowed', from: 'DSA SSH keys'
select 'Must be at least 384 bits', from: 'ECDSA SSH keys'
@@ -103,7 +103,7 @@ RSpec.describe 'Admin updates settings', feature_category: :shared do
end
it 'change Account and Limit Settings' do
- page.within(find('[data-testid="account-and-limit-settings-content"]')) do
+ within_testid('account-and-limit-settings-content') do
uncheck 'Gravatar enabled'
click_button 'Save changes'
end
@@ -113,7 +113,7 @@ RSpec.describe 'Admin updates settings', feature_category: :shared do
end
it 'change Maximum export size' do
- page.within(find('[data-testid="admin-import-export-settings"]')) do
+ within_testid('admin-import-export-settings') do
fill_in 'Maximum export size (MiB)', with: 25
click_button 'Save changes'
end
@@ -123,7 +123,7 @@ RSpec.describe 'Admin updates settings', feature_category: :shared do
end
it 'change Maximum import size' do
- page.within(find('[data-testid="admin-import-export-settings"]')) do
+ within_testid('admin-import-export-settings') do
fill_in 'Maximum import size (MiB)', with: 15
click_button 'Save changes'
end
@@ -169,7 +169,7 @@ RSpec.describe 'Admin updates settings', feature_category: :shared do
expect(page).to have_unchecked_field(_('Deactivate dormant users after a period of inactivity'))
expect(current_settings.deactivate_dormant_users).to be_falsey
- page.within(find('[data-testid="account-and-limit-settings-content"]')) do
+ within_testid('account-and-limit-settings-content') do
check _('Deactivate dormant users after a period of inactivity')
click_button _('Save changes')
end
@@ -185,7 +185,7 @@ RSpec.describe 'Admin updates settings', feature_category: :shared do
it 'change dormant users period', :js do
expect(page).to have_field(_('Days of inactivity before deactivation'), disabled: true)
- page.within(find('[data-testid="account-and-limit-settings-content"]')) do
+ within_testid('account-and-limit-settings-content') do
check _('Deactivate dormant users after a period of inactivity')
fill_in _('Days of inactivity before deactivation'), with: '180'
click_button _('Save changes')
@@ -202,7 +202,7 @@ RSpec.describe 'Admin updates settings', feature_category: :shared do
selector = '#application_setting_deactivate_dormant_users_period_error'
expect(page).not_to have_selector(selector, visible: :visible)
- page.within(find('[data-testid="account-and-limit-settings-content"]')) do
+ within_testid('account-and-limit-settings-content') do
check 'application_setting_deactivate_dormant_users'
fill_in _('application_setting_deactivate_dormant_users_period'), with: '30'
click_button 'Save changes'
@@ -666,28 +666,47 @@ RSpec.describe 'Admin updates settings', feature_category: :shared do
expect(find_field('Allow access to members of the following group').value).to be_nil
end
- it 'loads togglable usage ping payload on click', :js do
- allow(Gitlab::Usage::ServicePingReport).to receive(:for).and_return({ uuid: '12345678', hostname: '127.0.0.1' })
+ context 'Service usage data', :with_license do
+ before do
+ stub_usage_data_connections
+ stub_database_flavor_check
+ end
- stub_usage_data_connections
- stub_database_flavor_check
+ context 'when service data cached' do
+ before_all do
+ create(:raw_usage_data)
+ end
- page.within('#js-usage-settings') do
- expected_payload_content = /(?=.*"uuid")(?=.*"hostname")/m
+ it 'loads usage ping payload on click', :js do
+ expected_payload_content = /(?=.*"test")/m
- expect(page).not_to have_content expected_payload_content
+ expect(page).not_to have_content expected_payload_content
- click_button('Preview payload')
+ click_button('Preview payload')
- wait_for_requests
+ wait_for_requests
+
+ expect(page).to have_button 'Hide payload'
+ expect(page).to have_content expected_payload_content
+ end
- expect(page).to have_selector '.js-service-ping-payload'
- expect(page).to have_button 'Hide payload'
- expect(page).to have_content expected_payload_content
+ it 'generates usage ping payload on button click', :js do
+ expect_next_instance_of(Admin::ApplicationSettingsController) do |instance|
+ expect(instance).to receive(:usage_data).and_call_original
+ end
- click_button('Hide payload')
+ click_button('Download payload')
- expect(page).not_to have_content expected_payload_content
+ wait_for_requests
+ end
+ end
+
+ context 'when service data not cached' do
+ it 'renders missing cache information' do
+ visit metrics_and_profiling_admin_application_settings_path
+
+ expect(page).to have_text('Service Ping payload not found in the application cache')
+ end
end
end
end
@@ -799,7 +818,7 @@ RSpec.describe 'Admin updates settings', feature_category: :shared do
it 'changes gitlab shell operation limits settings' do
visit network_admin_application_settings_path
- page.within('[data-testid="gitlab-shell-operation-limits"]') do
+ within_testid('gitlab-shell-operation-limits') do
fill_in 'Maximum number of Git operations per minute', with: 100
click_button 'Save changes'
end
@@ -971,15 +990,14 @@ RSpec.describe 'Admin updates settings', feature_category: :shared do
end
end
- context 'Nav bar' do
+ context 'Nav bar', :js do
it 'shows default help links in nav' do
default_support_url = "https://#{ApplicationHelper.promo_host}/get-help/"
visit root_dashboard_path
- find('.header-help-dropdown-toggle').click
-
- page.within '.header-help' do
+ within_testid('super-sidebar') do
+ click_on 'Help'
expect(page).to have_link(text: 'Help', href: help_path)
expect(page).to have_link(text: 'Support', href: default_support_url)
end
@@ -991,68 +1009,12 @@ RSpec.describe 'Admin updates settings', feature_category: :shared do
visit root_dashboard_path
- find('.header-help-dropdown-toggle').click
-
- page.within '.header-help' do
+ within_testid('super-sidebar') do
+ click_on 'Help'
expect(page).to have_link(text: 'Support', href: new_support_url)
end
end
end
-
- context 'Service usage data page', :with_license do
- before do
- stub_usage_data_connections
- stub_database_flavor_check
- end
-
- context 'when service data cached', :use_clean_rails_memory_store_caching do
- let(:usage_data) { { uuid: "1111", hostname: "localhost", counts: { issue: 0 } }.deep_stringify_keys }
-
- before do
- # We are mocking Gitlab::Usage::ServicePingReport because this dataset generation
- # takes a very long time, and is not what we're testing in this context.
- #
- # See https://gitlab.com/gitlab-org/gitlab/-/issues/414929
- allow(Gitlab::UsageData).to receive(:data).and_return(usage_data)
- allow(Gitlab::Usage::ServicePingReport).to receive(:with_instrumentation_classes)
- .with(usage_data, :with_value).and_return(usage_data)
-
- visit usage_data_admin_application_settings_path
- visit service_usage_data_admin_application_settings_path
- end
-
- it 'loads usage ping payload on click', :js do
- expected_payload_content = /(?=.*"uuid")(?=.*"hostname")/m
-
- expect(page).not_to have_content expected_payload_content
-
- click_button('Preview payload')
-
- wait_for_requests
-
- expect(page).to have_button 'Hide payload'
- expect(page).to have_content expected_payload_content
- end
-
- it 'generates usage ping payload on button click', :js do
- expect_next_instance_of(Admin::ApplicationSettingsController) do |instance|
- expect(instance).to receive(:usage_data).and_call_original
- end
-
- click_button('Download payload')
-
- wait_for_requests
- end
- end
-
- context 'when service data not cached' do
- it 'renders missing cache information' do
- visit service_usage_data_admin_application_settings_path
-
- expect(page).to have_text('Service Ping payload not found in the application cache')
- end
- end
- end
end
context 'application setting :admin_mode is disabled' do
diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb
index ca08bc9e577..9ab5b1fd3bb 100644
--- a/spec/features/admin/admin_users_spec.rb
+++ b/spec/features/admin/admin_users_spec.rb
@@ -82,4 +82,12 @@ RSpec.describe "Admin::Users", feature_category: :user_management do
end
end
end
+
+ it 'does not perform N+1 queries' do
+ control_queries = ActiveRecord::QueryRecorder.new { visit admin_users_path }
+
+ expect { create(:user) }.to change { User.count }.by(1)
+
+ expect { visit admin_users_path }.not_to exceed_query_limit(control_queries)
+ end
end
diff --git a/spec/features/admin/admin_uses_repository_checks_spec.rb b/spec/features/admin/admin_uses_repository_checks_spec.rb
index d9d36ec3bae..05232de35e5 100644
--- a/spec/features/admin/admin_uses_repository_checks_spec.rb
+++ b/spec/features/admin/admin_uses_repository_checks_spec.rb
@@ -47,7 +47,7 @@ RSpec.describe 'Admin uses repository checks', :request_store, feature_category:
)
visit_admin_project_page(project)
- page.within('[data-testid="last-repository-check-failed-alert"]') do
+ within_testid('last-repository-check-failed-alert') do
expect(page.text).to match(/Last repository check \(just now\) failed/)
end
end
diff --git a/spec/features/admin/broadcast_messages_spec.rb b/spec/features/admin/broadcast_messages_spec.rb
index b89ebc34d6a..e4a2e31ee1c 100644
--- a/spec/features/admin/broadcast_messages_spec.rb
+++ b/spec/features/admin/broadcast_messages_spec.rb
@@ -36,12 +36,12 @@ RSpec.describe 'Admin Broadcast Messages', :js, feature_category: :onboarding do
# edit
page.within(first_message_container) do
- find('[data-testid="edit-message"]').click
+ find_by_testid('edit-message').click
end
wait_for_requests
- expect(find('[data-testid="message-input"]').value).to eq('test message')
+ expect(find_by_testid('message-input').value).to eq('test message')
fill_in 'Message', with: 'changed test message'
@@ -61,11 +61,11 @@ RSpec.describe 'Admin Broadcast Messages', :js, feature_category: :onboarding do
end
def preview_container
- find('[data-testid="preview-broadcast-message"]')
+ find_by_testid('preview-broadcast-message')
end
def first_message_container
- find('[data-testid="message-row"]', match: :first)
+ find_by_testid('message-row', match: :first)
end
end
end
diff --git a/spec/features/admin/users/user_spec.rb b/spec/features/admin/users/user_spec.rb
index 7dc329e6909..b8dc725c17f 100644
--- a/spec/features/admin/users/user_spec.rb
+++ b/spec/features/admin/users/user_spec.rb
@@ -6,8 +6,8 @@ RSpec.describe 'Admin::Users::User', feature_category: :user_management do
include Features::AdminUsersHelpers
include Spec::Support::Helpers::ModalHelpers
- let_it_be(:user) { create(:omniauth_user, :no_super_sidebar, provider: 'twitter', extern_uid: '123456') }
- let_it_be(:current_user) { create(:admin, :no_super_sidebar) }
+ let_it_be(:user) { create(:omniauth_user, provider: 'twitter', extern_uid: '123456') }
+ let_it_be(:current_user) { create(:admin) }
before do
sign_in(current_user)
@@ -145,7 +145,7 @@ RSpec.describe 'Admin::Users::User', feature_category: :user_management do
end
describe 'Impersonation' do
- let_it_be(:another_user) { create(:user, :no_super_sidebar) }
+ let_it_be(:another_user) { create(:user) }
context 'before impersonating' do
subject { visit admin_user_path(user_to_visit) }
@@ -156,7 +156,7 @@ RSpec.describe 'Admin::Users::User', feature_category: :user_management do
it 'disables impersonate button' do
subject
- impersonate_btn = find('[data-testid="impersonate-user-link"]')
+ impersonate_btn = find_by_testid('impersonate-user-link')
expect(impersonate_btn).not_to be_nil
expect(impersonate_btn['disabled']).not_to be_nil
@@ -174,7 +174,7 @@ RSpec.describe 'Admin::Users::User', feature_category: :user_management do
subject
expect(page).to have_content('Impersonate')
- impersonate_btn = find('[data-testid="impersonate-user-link"]')
+ impersonate_btn = find_by_testid('impersonate-user-link')
expect(impersonate_btn['disabled']).to be_nil
end
end
@@ -257,15 +257,13 @@ RSpec.describe 'Admin::Users::User', feature_category: :user_management do
visit admin_user_path(another_user)
end
- it 'logs in as the user when impersonate is clicked' do
+ it 'logs in as the user when impersonate is clicked', :js do
subject
- find('[data-testid="user-dropdown"]').click
-
- expect(page.find(:css, '[data-testid="user-profile-link"]')['data-user']).to eql(another_user.username)
+ expect(page).to have_button("#{another_user.name} user’s menu")
end
- it 'sees impersonation log out icon' do
+ it 'sees impersonation log out icon', :js do
subject
icon = first('[data-testid="incognito-icon"]')
@@ -306,8 +304,8 @@ RSpec.describe 'Admin::Users::User', feature_category: :user_management do
end
end
- context 'ending impersonation' do
- subject { find(:css, 'li.impersonation a').click }
+ context 'ending impersonation', :js do
+ subject { click_on 'Stop impersonating' }
before do
visit admin_user_path(another_user)
@@ -317,9 +315,7 @@ RSpec.describe 'Admin::Users::User', feature_category: :user_management do
it 'logs out of impersonated user back to original user' do
subject
- find('[data-testid="user-dropdown"]').click
-
- expect(page.find(:css, '[data-testid="user-profile-link"]')['data-user']).to eq(current_user.username)
+ expect(page).to have_button("#{current_user.name} user’s menu")
end
it 'is redirected back to the impersonated users page in the admin after stopping' do
diff --git a/spec/features/admin/users/users_spec.rb b/spec/features/admin/users/users_spec.rb
index 8ee30c50a7d..4e988674858 100644
--- a/spec/features/admin/users/users_spec.rb
+++ b/spec/features/admin/users/users_spec.rb
@@ -43,7 +43,7 @@ RSpec.describe 'Admin::Users', feature_category: :user_management do
end
it 'clicking edit user takes us to edit page', :aggregate_failures do
- page.within("[data-testid='user-actions-#{user.id}']") do
+ within_testid("user-actions-#{user.id}") do
click_link 'Edit'
end
@@ -71,7 +71,7 @@ RSpec.describe 'Admin::Users', feature_category: :user_management do
it 'displays count of users projects' do
visit admin_users_path
- expect(page.find("[data-testid='user-project-count-#{current_user.id}']").text).to eq("1")
+ expect(find_by_testid("user-project-count-#{current_user.id}").text).to eq("1")
end
end
@@ -321,7 +321,7 @@ RSpec.describe 'Admin::Users', feature_category: :user_management do
click_user_dropdown_toggle(user.id)
- find('[data-testid="approve"]').click
+ find_by_testid('approve').click
expect(page).to have_content("Approve user #{user.name}?")
@@ -378,7 +378,7 @@ RSpec.describe 'Admin::Users', feature_category: :user_management do
wait_for_requests
- expect(page.find("[data-testid='user-group-count-#{current_user.id}']").text).to eq("2")
+ expect(find_by_testid("user-group-count-#{current_user.id}").text).to eq("2")
end
end
end
@@ -542,7 +542,7 @@ RSpec.describe 'Admin::Users', feature_category: :user_management do
it 'allows group membership to be revoked', :js do
page.within(first('.group_member')) do
- find('.btn[data-testid="remove-user"]').click
+ find_by_testid('remove-user').click
end
accept_gl_confirm(button_text: 'Remove')
@@ -587,7 +587,7 @@ RSpec.describe 'Admin::Users', feature_category: :user_management do
end
def check_breadcrumb(content)
- expect(find('[data-testid="breadcrumb-current-link"]')).to have_content(content)
+ expect(find_by_testid('breadcrumb-current-link')).to have_content(content)
end
end
diff --git a/spec/features/admin_variables_spec.rb b/spec/features/admin_variables_spec.rb
index 91e7a46849c..caa94209e50 100644
--- a/spec/features/admin_variables_spec.rb
+++ b/spec/features/admin_variables_spec.rb
@@ -13,13 +13,12 @@ RSpec.describe 'Instance variables', :js, feature_category: :secrets_management
sign_in(admin)
gitlab_enable_admin_mode_sign_in(admin)
- stub_feature_flags(ci_variable_drawer: false)
visit page_path
wait_for_requests
end
context 'when ci_variables_pages FF is enabled' do
- it_behaves_like 'variable list', is_admin: true
+ it_behaves_like 'variable list drawer', is_admin: true
it_behaves_like 'variable list pagination', :ci_instance_variable
end
@@ -28,16 +27,6 @@ RSpec.describe 'Instance variables', :js, feature_category: :secrets_management
stub_feature_flags(ci_variables_pages: false)
end
- it_behaves_like 'variable list', is_admin: true
- end
-
- context 'when ci_variable_drawer FF is enabled' do
- before do
- stub_feature_flags(ci_variable_drawer: true)
- visit page_path
- wait_for_requests
- end
-
it_behaves_like 'variable list drawer', is_admin: true
end
end
diff --git a/spec/features/alert_management/alert_details_spec.rb b/spec/features/alert_management/alert_details_spec.rb
index 66b7a9ca46c..58ce9e68468 100644
--- a/spec/features/alert_management/alert_details_spec.rb
+++ b/spec/features/alert_management/alert_details_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe 'Alert details', :js, feature_category: :incident_management do
let_it_be(:project) { create(:project) }
- let_it_be(:developer) { create(:user, :no_super_sidebar) }
+ let_it_be(:developer) { create(:user) }
let_it_be(:alert) { create(:alert_management_alert, project: project, status: 'triggered', title: 'Alert') }
before_all do
@@ -27,7 +27,7 @@ RSpec.describe 'Alert details', :js, feature_category: :incident_management do
it 'shows the alert tabs' do
page.within('.alert-management-details') do
- alert_tabs = find('[data-testid="alertDetailsTabs"]')
+ alert_tabs = find_by_testid('alertDetailsTabs')
expect(alert_tabs).to have_content('Alert details')
expect(alert_tabs).to have_content('Metrics')
@@ -47,10 +47,10 @@ RSpec.describe 'Alert details', :js, feature_category: :incident_management do
it 'updates the alert todo button from the right sidebar' do
expect(page).to have_selector('[data-testid="alert-todo-button"]')
- todo_button = find('[data-testid="alert-todo-button"]')
+ todo_button = find_by_testid('alert-todo-button')
expect(todo_button).to have_content('Add a to do')
- find('[data-testid="alert-todo-button"]').click
+ find_by_testid('alert-todo-button').click
wait_for_requests
expect(todo_button).to have_content('Mark as done')
@@ -58,7 +58,7 @@ RSpec.describe 'Alert details', :js, feature_category: :incident_management do
it 'updates the alert status from the right sidebar' do
page.within('.alert-status') do
- alert_status = find('[data-testid="status"]')
+ alert_status = find_by_testid('status')
expect(alert_status).to have_content('Triggered')
@@ -77,7 +77,7 @@ RSpec.describe 'Alert details', :js, feature_category: :incident_management do
expect(alert_assignee).to have_content('None - assign yourself')
- find('[data-testid="unassigned-users"]').click
+ find_by_testid('unassigned-users').click
wait_for_requests
diff --git a/spec/features/alert_management/alert_management_list_spec.rb b/spec/features/alert_management/alert_management_list_spec.rb
index cc54af249e1..058447b3be3 100644
--- a/spec/features/alert_management/alert_management_list_spec.rb
+++ b/spec/features/alert_management/alert_management_list_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe 'Alert Management index', :js, feature_category: :incident_management do
let_it_be(:project) { create(:project) }
- let_it_be(:developer) { create(:user, :no_super_sidebar) }
+ let_it_be(:developer) { create(:user) }
before_all do
project.add_developer(developer)
@@ -22,7 +22,7 @@ RSpec.describe 'Alert Management index', :js, feature_category: :incident_manage
expect(page).to have_content('Alerts')
expect(page).to have_content('Surface alerts in GitLab')
expect(page).not_to have_selector('.gl-table')
- page.within('.layout-page') do
+ page.within('.content-wrapper') do
expect(page).not_to have_css('[data-testid="search-icon"]')
end
end
@@ -31,7 +31,7 @@ RSpec.describe 'Alert Management index', :js, feature_category: :incident_manage
it 'renders correctly' do
expect(page).to have_content('Alerts')
expect(page).to have_selector('.gl-table')
- page.within('.layout-page') do
+ page.within('.content-wrapper') do
expect(page).to have_css('[data-testid="search-icon"]')
end
end
diff --git a/spec/features/boards/board_filters_spec.rb b/spec/features/boards/board_filters_spec.rb
index 1ee02de9a66..a6d5d4926ff 100644
--- a/spec/features/boards/board_filters_spec.rb
+++ b/spec/features/boards/board_filters_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe 'Issue board filters', :js, feature_category: :team_planning do
let_it_be(:issue_2) { create(:labeled_issue, project: project, milestone: milestone_2, assignees: [user], labels: [project_label], confidential: true) }
let_it_be(:award_emoji1) { create(:award_emoji, name: 'thumbsup', user: user, awardable: issue_1) }
- let(:filtered_search) { find('[data-testid="issue-board-filtered-search"]') }
+ let(:filtered_search) { find_by_testid('issue-board-filtered-search') }
let(:filter_input) { find('.gl-filtered-search-term-input') }
let(:filter_dropdown) { find('.gl-filtered-search-suggestion-list') }
let(:filter_first_suggestion) { find('.gl-filtered-search-suggestion-list').first('.gl-filtered-search-suggestion') }
@@ -25,7 +25,6 @@ RSpec.describe 'Issue board filters', :js, feature_category: :team_planning do
let_it_be(:board) { create(:board, project: project) }
before do
- stub_feature_flags(apollo_boards: false)
project.add_maintainer(user)
sign_in(user)
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index 85e54c0f451..48b978f7245 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -28,13 +28,12 @@ RSpec.describe 'Project issue boards', :js, feature_category: :team_planning do
let_it_be(:user, reload: true) { create(:user) }
let_it_be(:user2, reload: true) { create(:user) }
- let(:filtered_search) { find('[data-testid="issue-board-filtered-search"]') }
+ let(:filtered_search) { find_by_testid('issue-board-filtered-search') }
let(:filter_input) { find('.gl-filtered-search-term-input') }
let(:filter_submit) { find('.gl-search-box-by-click-search-button') }
context 'signed in user' do
before do
- stub_feature_flags(apollo_boards: false)
project.add_maintainer(user)
project.add_maintainer(user2)
@@ -296,7 +295,7 @@ RSpec.describe 'Project issue boards', :js, feature_category: :team_planning do
it 'shows issue count on the list' do
page.within(find(".board:nth-child(2)")) do
- expect(page.find('[data-testid="board-items-count"]')).to have_text(total_planning_issues)
+ expect(find_by_testid('board-items-count')).to have_text(total_planning_issues)
expect(page).not_to have_selector('.max-issue-size')
end
end
@@ -389,7 +388,7 @@ RSpec.describe 'Project issue boards', :js, feature_category: :team_planning do
wait_for_board_cards(2, 1)
- find('[data-testid="filtered-search-clear-button"]').click
+ find_by_testid('filtered-search-clear-button').click
filter_submit.click
end
@@ -518,7 +517,6 @@ RSpec.describe 'Project issue boards', :js, feature_category: :team_planning do
context 'signed out user' do
before do
- stub_feature_flags(apollo_boards: false)
visit project_board_path(project, board)
wait_for_requests
end
@@ -540,7 +538,6 @@ RSpec.describe 'Project issue boards', :js, feature_category: :team_planning do
let_it_be(:user_guest, reload: true) { create(:user) }
before do
- stub_feature_flags(apollo_boards: false)
project.add_guest(user_guest)
sign_in(user_guest)
visit project_board_path(project, board)
diff --git a/spec/features/boards/issue_ordering_spec.rb b/spec/features/boards/issue_ordering_spec.rb
index 35e387c9d8a..625a8ddad84 100644
--- a/spec/features/boards/issue_ordering_spec.rb
+++ b/spec/features/boards/issue_ordering_spec.rb
@@ -15,7 +15,6 @@ RSpec.describe 'Issue Boards', :js, feature_category: :team_planning do
let!(:issue3) { create(:labeled_issue, project: project, title: 'testing 3', labels: [label], relative_position: 1) }
before do
- stub_feature_flags(apollo_boards: false)
project.add_maintainer(user)
sign_in(user)
@@ -131,7 +130,7 @@ RSpec.describe 'Issue Boards', :js, feature_category: :team_planning do
end
context 'ordering in list using move to position' do
- let(:move_to_position) { find('[data-testid="board-move-to-position"]') }
+ let(:move_to_position) { find_by_testid('board-move-to-position') }
before do
visit project_board_path(project, board)
diff --git a/spec/features/boards/multiple_boards_spec.rb b/spec/features/boards/multiple_boards_spec.rb
index 9d59d3dd02a..e9d34c6f87f 100644
--- a/spec/features/boards/multiple_boards_spec.rb
+++ b/spec/features/boards/multiple_boards_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'Multiple Issue Boards', :js, feature_category: :team_planning do
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public) }
let_it_be(:planning) { create(:label, project: project, name: 'Planning') }
let_it_be(:board) { create(:board, name: 'board1', project: project) }
diff --git a/spec/features/boards/new_issue_spec.rb b/spec/features/boards/new_issue_spec.rb
index 682ccca38bd..1e44e1d35f9 100644
--- a/spec/features/boards/new_issue_spec.rb
+++ b/spec/features/boards/new_issue_spec.rb
@@ -11,11 +11,7 @@ RSpec.describe 'Issue Boards new issue', :js, feature_category: :team_planning d
let_it_be(:existing_issue) { create(:issue, project: project, title: 'other issue', relative_position: 50) }
let(:board_list_header) { first('[data-testid="board-list-header"]') }
- let(:project_select_dropdown) { find('[data-testid="project-select-dropdown"]') }
-
- before do
- stub_feature_flags(apollo_boards: false)
- end
+ let(:project_select_dropdown) { find_by_testid('project-select-dropdown') }
context 'authorized user' do
before do
@@ -100,7 +96,7 @@ RSpec.describe 'Issue Boards new issue', :js, feature_category: :team_planning d
wait_for_requests
- page.within('[data-testid="sidebar-labels"]') do
+ within_testid('sidebar-labels') do
click_button 'Edit'
wait_for_requests
diff --git a/spec/features/boards/reload_boards_on_browser_back_spec.rb b/spec/features/boards/reload_boards_on_browser_back_spec.rb
index 036daee7655..0ca680c5ed5 100644
--- a/spec/features/boards/reload_boards_on_browser_back_spec.rb
+++ b/spec/features/boards/reload_boards_on_browser_back_spec.rb
@@ -9,8 +9,6 @@ RSpec.describe 'Ensure Boards do not show stale data on browser back', :js, feat
context 'authorized user' do
before do
- stub_feature_flags(apollo_boards: false)
-
project.add_maintainer(user)
sign_in(user)
diff --git a/spec/features/boards/sidebar_assignee_spec.rb b/spec/features/boards/sidebar_assignee_spec.rb
index 899ab5863e1..93e45b3e3f8 100644
--- a/spec/features/boards/sidebar_assignee_spec.rb
+++ b/spec/features/boards/sidebar_assignee_spec.rb
@@ -65,7 +65,7 @@ RSpec.describe 'Project issue boards sidebar assignee', :js,
wait_for_requests
page.within('.dropdown-menu-user') do
- find('[data-testid="unassign"]').click
+ find_by_testid('unassign').click
end
expect(page).to have_content('None')
diff --git a/spec/features/boards/sidebar_labels_in_namespaces_spec.rb b/spec/features/boards/sidebar_labels_in_namespaces_spec.rb
index 68c2b2587e7..da3dd6ba071 100644
--- a/spec/features/boards/sidebar_labels_in_namespaces_spec.rb
+++ b/spec/features/boards/sidebar_labels_in_namespaces_spec.rb
@@ -14,8 +14,6 @@ RSpec.describe 'Issue boards sidebar labels select', :js, feature_category: :tea
let_it_be(:group_board) { create(:board, group: group) }
before do
- stub_feature_flags(apollo_boards: false)
-
load_board group_board_path(group, group_board)
end
diff --git a/spec/features/boards/sidebar_labels_spec.rb b/spec/features/boards/sidebar_labels_spec.rb
index 460d0d232b3..0560cbbfae7 100644
--- a/spec/features/boards/sidebar_labels_spec.rb
+++ b/spec/features/boards/sidebar_labels_spec.rb
@@ -20,7 +20,6 @@ RSpec.describe 'Project issue boards sidebar labels', :js, feature_category: :te
let(:card) { find('.board:nth-child(2)').first('.board-card') }
before do
- stub_feature_flags(apollo_boards: false)
project.add_maintainer(user)
sign_in(user)
diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb
index 71cc9a28575..893f1c246a0 100644
--- a/spec/features/boards/sidebar_spec.rb
+++ b/spec/features/boards/sidebar_spec.rb
@@ -15,7 +15,6 @@ RSpec.describe 'Project issue boards sidebar', :js, feature_category: :team_plan
let_it_be(:issue, reload: true) { create(:issue, project: project, relative_position: 1) }
before do
- stub_feature_flags(apollo_boards: false)
project.add_maintainer(user)
sign_in(user)
diff --git a/spec/features/boards/user_adds_lists_to_board_spec.rb b/spec/features/boards/user_adds_lists_to_board_spec.rb
index cc2afca7657..d202c2a1f7d 100644
--- a/spec/features/boards/user_adds_lists_to_board_spec.rb
+++ b/spec/features/boards/user_adds_lists_to_board_spec.rb
@@ -29,7 +29,6 @@ RSpec.describe 'User adds lists', :js, feature_category: :team_planning do
with_them do
before do
- stub_feature_flags(apollo_boards: false)
sign_in(user)
set_cookie('sidebar_collapsed', 'true')
diff --git a/spec/features/boards/user_visits_board_spec.rb b/spec/features/boards/user_visits_board_spec.rb
index 4741f58d883..cf8709b3a76 100644
--- a/spec/features/boards/user_visits_board_spec.rb
+++ b/spec/features/boards/user_visits_board_spec.rb
@@ -44,7 +44,6 @@ RSpec.describe 'User visits issue boards', :js, feature_category: :team_planning
with_them do
before do
- stub_feature_flags(apollo_boards: false)
visit board_path
wait_for_requests
@@ -60,7 +59,6 @@ RSpec.describe 'User visits issue boards', :js, feature_category: :team_planning
end
context "project boards" do
- stub_feature_flags(apollo_boards: false)
let_it_be(:board) { create_default(:board, project: project) }
let(:board_path) { project_boards_path(project, params) }
@@ -69,7 +67,6 @@ RSpec.describe 'User visits issue boards', :js, feature_category: :team_planning
end
context "group boards" do
- stub_feature_flags(apollo_boards: false)
let_it_be(:board) { create_default(:board, group: group) }
let(:board_path) { group_boards_path(group, params) }
diff --git a/spec/features/broadcast_messages_spec.rb b/spec/features/broadcast_messages_spec.rb
index 98f87face15..f887242384c 100644
--- a/spec/features/broadcast_messages_spec.rb
+++ b/spec/features/broadcast_messages_spec.rb
@@ -125,8 +125,8 @@ RSpec.describe 'Broadcast Messages', feature_category: :onboarding do
visit admin_broadcast_messages_path
- page.within('[data-testid="message-row"]', match: :first) do
- find("[data-testid='delete-message-#{message.id}']").click
+ within_testid('message-row', match: :first) do
+ find_by_testid("delete-message-#{message.id}").click
end
accept_gl_confirm(button_text: 'Delete message')
@@ -145,7 +145,7 @@ RSpec.describe 'Broadcast Messages', feature_category: :onboarding do
end
def expect_broadcast_message(text)
- page.within('[data-testid="banner-broadcast-message"]') do
+ within_testid('banner-broadcast-message') do
expect(page).to have_content text
end
end
@@ -157,7 +157,7 @@ RSpec.describe 'Broadcast Messages', feature_category: :onboarding do
end
def expect_to_be_on_explore_projects_page
- page.within('[data-testid="explore-projects-title"]') do
+ within_testid('explore-projects-title') do
expect(page).to have_content 'Explore projects'
end
end
diff --git a/spec/features/calendar_spec.rb b/spec/features/calendar_spec.rb
index e22ae4f51fb..291c40f0f6b 100644
--- a/spec/features/calendar_spec.rb
+++ b/spec/features/calendar_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe 'Contributions Calendar', :js, feature_category: :user_profile do
include MobileHelpers
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
let(:contributed_project) { create(:project, :public, :repository) }
let(:issue_note) { create(:note, project: contributed_project) }
@@ -83,7 +83,6 @@ RSpec.describe 'Contributions Calendar', :js, feature_category: :user_profile do
shared_context 'when user page is visited' do
before do
visit user.username
- page.click_link('Overview')
wait_for_requests
end
end
diff --git a/spec/features/callouts/registration_enabled_spec.rb b/spec/features/callouts/registration_enabled_spec.rb
index 3282a40854d..4cd5cb9a857 100644
--- a/spec/features/callouts/registration_enabled_spec.rb
+++ b/spec/features/callouts/registration_enabled_spec.rb
@@ -43,7 +43,7 @@ RSpec.describe 'Registration enabled callout', feature_category: :system_access
before do
visit admin_root_path
- find('[data-testid="close-registration-enabled-callout"]').click
+ find_by_testid('close-registration-enabled-callout').click
wait_for_requests
diff --git a/spec/features/clusters/create_agent_spec.rb b/spec/features/clusters/create_agent_spec.rb
index d90c43f452c..79eaecdf582 100644
--- a/spec/features/clusters/create_agent_spec.rb
+++ b/spec/features/clusters/create_agent_spec.rb
@@ -27,7 +27,7 @@ RSpec.describe 'Cluster agent registration', :js, feature_category: :deployment_
end
it 'allows the user to select an agent to install, and displays the resulting agent token' do
- find('[data-testid="clusters-default-action-button"]').click
+ find_by_testid('clusters-default-action-button').click
expect(page).to have_content('Register')
diff --git a/spec/features/commit_spec.rb b/spec/features/commit_spec.rb
index 61792ea5a58..739a070f423 100644
--- a/spec/features/commit_spec.rb
+++ b/spec/features/commit_spec.rb
@@ -66,12 +66,4 @@ RSpec.describe 'Commit', feature_category: :source_code_management do
end
it_behaves_like "single commit view"
-
- context "when super sidebar is enabled" do
- before do
- user.update!(use_new_navigation: true)
- end
-
- it_behaves_like "single commit view"
- end
end
diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb
index 5f880af37dc..8f6c1c28872 100644
--- a/spec/features/commits_spec.rb
+++ b/spec/features/commits_spec.rb
@@ -41,13 +41,13 @@ RSpec.describe 'Commits', feature_category: :source_code_management do
end
it 'contains commit short id' do
- page.within('[data-testid="pipeline-details-header"]') do
+ within_testid('pipeline-details-header') do
expect(page).to have_content pipeline.sha[0..7]
end
end
it 'contains generic commit status build' do
- page.within('[data-testid="jobs-tab-table"]') do
+ within_testid('jobs-tab-table') do
expect(page).to have_content "##{status.id}" # build id
expect(page).to have_content 'generic' # build name
end
@@ -81,8 +81,8 @@ RSpec.describe 'Commits', feature_category: :source_code_management do
it 'shows correct build status from default branch' do
page.within("//li[@id='commit-#{pipeline.short_sha}']") do
- expect(page).to have_css("[data-testid='ci-status-badge-legacy']")
- expect(page).to have_css('.ci-status-icon-success')
+ expect(page).to have_css("[data-testid='ci-icon']")
+ expect(page).to have_css('[data-testid="status_success_borderless-icon"]')
end
end
end
@@ -122,7 +122,7 @@ RSpec.describe 'Commits', feature_category: :source_code_management do
describe 'Cancel build' do
it 'cancels build', :js, :sidekiq_might_not_need_inline do
visit pipeline_path(pipeline)
- find('[data-testid="cancel-pipeline"]').click
+ find_by_testid('cancel-pipeline').click
expect(page).to have_content 'Canceled'
end
end
diff --git a/spec/features/contextual_sidebar_spec.rb b/spec/features/contextual_sidebar_spec.rb
index ab322f18240..dffc87c2028 100644
--- a/spec/features/contextual_sidebar_spec.rb
+++ b/spec/features/contextual_sidebar_spec.rb
@@ -4,39 +4,19 @@ require 'spec_helper'
RSpec.describe 'Contextual sidebar', :js, feature_category: :remote_development do
context 'when context is a project' do
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository, namespace: user.namespace) }
before do
sign_in(user)
+ visit project_path(project)
end
- context 'when analyzing the menu' do
- before do
- visit project_path(project)
- end
+ it 'shows flyout menu on other section on hover' do
+ expect(page).not_to have_link('Pipelines', href: project_pipelines_path(project))
- it 'shows flyout navs when collapsed or expanded apart from on the active item when expanded', :aggregate_failures do
- expect(page).not_to have_selector('.js-sidebar-collapsed')
-
- find('.rspec-link-pipelines').hover
-
- expect(page).to have_selector('.is-showing-fly-out')
-
- find('.rspec-project-link').hover
-
- expect(page).not_to have_selector('.is-showing-fly-out')
-
- find('.rspec-toggle-sidebar').click
-
- find('.rspec-link-pipelines').hover
-
- expect(page).to have_selector('.is-showing-fly-out')
-
- find('.rspec-project-link').hover
-
- expect(page).to have_selector('.is-showing-fly-out')
- end
+ find_button('Build').hover
+ expect(page).to have_link('Pipelines', href: project_pipelines_path(project))
end
end
end
diff --git a/spec/features/dashboard/activity_spec.rb b/spec/features/dashboard/activity_spec.rb
index 61631d28aa9..fd1e5a34f05 100644
--- a/spec/features/dashboard/activity_spec.rb
+++ b/spec/features/dashboard/activity_spec.rb
@@ -2,8 +2,8 @@
require 'spec_helper'
-RSpec.describe 'Dashboard > Activity', feature_category: :user_profile do
- let(:user) { create(:user, :no_super_sidebar) }
+RSpec.describe 'Dashboard > Activity', :js, feature_category: :user_profile do
+ let(:user) { create(:user) }
before do
sign_in(user)
@@ -46,7 +46,7 @@ RSpec.describe 'Dashboard > Activity', feature_category: :user_profile do
it_behaves_like "an autodiscoverable RSS feed with current_user's feed token"
end
- context 'event filters', :js do
+ context 'event filters' do
let(:project) { create(:project, :repository) }
let(:merge_request) do
diff --git a/spec/features/dashboard/group_dashboard_with_external_authorization_service_spec.rb b/spec/features/dashboard/group_dashboard_with_external_authorization_service_spec.rb
deleted file mode 100644
index a00666c2376..00000000000
--- a/spec/features/dashboard/group_dashboard_with_external_authorization_service_spec.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'The group dashboard', :js, feature_category: :groups_and_projects do
- include ExternalAuthorizationServiceHelpers
- include Features::TopNavSpecHelpers
-
- let(:user) { create(:user, :no_super_sidebar) }
-
- before do
- sign_in user
- end
-
- describe 'The top navigation' do
- it 'has all the expected links' do
- visit dashboard_groups_path
-
- open_top_nav
-
- within_top_nav do
- expect(page).to have_button('Projects')
- expect(page).to have_button('Groups')
- expect(page).to have_link('Your work')
- expect(page).to have_link('Explore')
- end
- end
-
- it 'hides some links when an external authorization service is enabled' do
- enable_external_authorization_service_check
- visit dashboard_groups_path
-
- open_top_nav
-
- within_top_nav do
- expect(page).to have_button('Projects')
- expect(page).to have_button('Groups')
- expect(page).to have_link('Your work')
- expect(page).to have_link('Explore')
- end
- end
- end
-end
diff --git a/spec/features/dashboard/group_spec.rb b/spec/features/dashboard/group_spec.rb
index ea600758607..7510a92e19b 100644
--- a/spec/features/dashboard/group_spec.rb
+++ b/spec/features/dashboard/group_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe 'Dashboard Group', feature_category: :groups_and_projects do
it 'creates new group', :js do
visit dashboard_groups_path
- find('[data-testid="new-group-button"]').click
+ find_by_testid('new-group-button').click
click_link 'Create group'
new_name = 'Samurai'
diff --git a/spec/features/dashboard/groups_list_spec.rb b/spec/features/dashboard/groups_list_spec.rb
index e1da163cdf5..745e45478d1 100644
--- a/spec/features/dashboard/groups_list_spec.rb
+++ b/spec/features/dashboard/groups_list_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'Dashboard Groups page', :js, feature_category: :groups_and_projects do
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
let(:group) { create(:group) }
let(:nested_group) { create(:group, :nested) }
let(:another_group) { create(:group) }
diff --git a/spec/features/dashboard/issuables_counter_spec.rb b/spec/features/dashboard/issuables_counter_spec.rb
index 501405c5662..d34f8cb3e18 100644
--- a/spec/features/dashboard/issuables_counter_spec.rb
+++ b/spec/features/dashboard/issuables_counter_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'Navigation bar counter', :use_clean_rails_memory_store_caching, feature_category: :team_planning do
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
let(:project) { create(:project, namespace: user.namespace) }
let(:issue) { create(:issue, project: project) }
let(:merge_request) { create(:merge_request, source_project: project) }
@@ -17,33 +17,29 @@ RSpec.describe 'Navigation bar counter', :use_clean_rails_memory_store_caching,
it 'reflects dashboard issues count', :js do
visit issues_path
- expect_counters('issues', '1', n_("%d assigned issue", "%d assigned issues", 1) % 1)
+ expect_issue_count(1)
issue.update!(assignees: [])
- Users::AssignedIssuesCountService.new(current_user: user).delete_cache
+ user.invalidate_cache_counts
- travel_to(3.minutes.from_now) do
- visit issues_path
+ visit issues_path
- expect_counters('issues', '0', n_("%d assigned issue", "%d assigned issues", 0) % 0)
- end
+ expect_issue_count(0)
end
it 'reflects dashboard merge requests count', :js do
visit merge_requests_path
- expect_counters('merge_requests', '1', n_("%d merge request", "%d merge requests", 1) % 1)
+ expect_merge_request_count(1)
merge_request.update!(assignees: [])
user.invalidate_cache_counts
- travel_to(3.minutes.from_now) do
- visit merge_requests_path
+ visit merge_requests_path
- expect_counters('merge_requests', '0', n_("%d merge request", "%d merge requests", 0) % 0)
- end
+ expect_merge_request_count(0)
end
def issues_path
@@ -54,11 +50,21 @@ RSpec.describe 'Navigation bar counter', :use_clean_rails_memory_store_caching,
merge_requests_dashboard_path(assignee_username: user.username)
end
- def expect_counters(issuable_type, count, badge_label)
+ def expect_issue_count(count)
dashboard_count = find('.gl-tabs-nav li a.active')
+ expect(dashboard_count).to have_content(count)
+ within_testid('super-sidebar') do
+ expect(page).to have_link("Issues #{count}")
+ end
+ end
+
+ def expect_merge_request_count(count)
+ dashboard_count = find('.gl-tabs-nav li a.active')
expect(dashboard_count).to have_content(count)
- expect(page).to have_css(".dashboard-shortcuts-#{issuable_type}", visible: :all, text: count)
- expect(page).to have_css("span[aria-label='#{badge_label}']", visible: :all, text: count)
+
+ within_testid('super-sidebar') do
+ expect(page).to have_button("Merge requests #{count}")
+ end
end
end
diff --git a/spec/features/dashboard/issues_spec.rb b/spec/features/dashboard/issues_spec.rb
index 69b32113bba..7008f702622 100644
--- a/spec/features/dashboard/issues_spec.rb
+++ b/spec/features/dashboard/issues_spec.rb
@@ -2,10 +2,10 @@
require 'spec_helper'
-RSpec.describe 'Dashboard Issues', feature_category: :team_planning do
+RSpec.describe 'Dashboard Issues', :js, feature_category: :team_planning do
include FilteredSearchHelpers
- let_it_be(:current_user) { create(:user, :no_super_sidebar) }
+ let_it_be(:current_user) { create(:user) }
let_it_be(:user) { current_user } # Shared examples depend on this being available
let_it_be(:public_project) { create(:project, :public) }
let_it_be(:project) { create(:project) }
@@ -23,7 +23,7 @@ RSpec.describe 'Dashboard Issues', feature_category: :team_planning do
it_behaves_like 'a "Your work" page with sidebar and breadcrumbs', :issues_dashboard_path, :issues
- describe 'issues', :js do
+ describe 'issues' do
it 'shows issues assigned to current user' do
expect(page).to have_content(assigned_issue.title)
expect(page).not_to have_content(authored_issue.title)
@@ -59,23 +59,25 @@ RSpec.describe 'Dashboard Issues', feature_category: :team_planning do
end
describe 'new issue dropdown' do
- it 'shows projects only with issues feature enabled', :js do
+ it 'shows projects only with issues feature enabled' do
click_button _('Select project to create issue')
- page.within('[data-testid="new-resource-dropdown"] [role="menu"]') do
- expect(page).to have_content(project.full_name)
- expect(page).not_to have_content(project_with_issues_disabled.full_name)
+ within_testid('new-resource-dropdown') do
+ within('[role="menu"]') do
+ expect(page).to have_content(project.full_name)
+ expect(page).not_to have_content(project_with_issues_disabled.full_name)
+ end
end
end
- it 'shows the new issue page', :js do
+ it 'shows the new issue page' do
click_button _('Select project to create issue')
wait_for_requests
project_path = "/#{project.full_path}"
- page.within('[data-testid="new-resource-dropdown"]') do
+ within_testid('new-resource-dropdown') do
find_button(project.full_name).click
end
diff --git a/spec/features/dashboard/merge_requests_spec.rb b/spec/features/dashboard/merge_requests_spec.rb
index 4bb04f4ff80..8a7652858b8 100644
--- a/spec/features/dashboard/merge_requests_spec.rb
+++ b/spec/features/dashboard/merge_requests_spec.rb
@@ -2,12 +2,12 @@
require 'spec_helper'
-RSpec.describe 'Dashboard Merge Requests', feature_category: :code_review_workflow do
+RSpec.describe 'Dashboard Merge Requests', :js, feature_category: :code_review_workflow do
include Features::SortingHelpers
include FilteredSearchHelpers
include ProjectForksHelper
- let(:current_user) { create(:user, :no_super_sidebar) }
+ let(:current_user) { create(:user) }
let(:user) { current_user }
let(:project) { create(:project) }
@@ -19,7 +19,21 @@ RSpec.describe 'Dashboard Merge Requests', feature_category: :code_review_workfl
sign_in(current_user)
end
- it_behaves_like 'a "Your work" page with sidebar and breadcrumbs', :merge_requests_dashboard_path, :merge_requests
+ describe 'sidebar' do
+ it 'has nav items for assigned MRs and review requests' do
+ visit merge_requests_dashboard_path(assignee_username: user)
+
+ within('#super-sidebar') do
+ expect(page).to have_css("a[data-track-label='merge_requests_assigned'][aria-current='page']")
+ end
+
+ click_link 'Review requests'
+
+ within('#super-sidebar') do
+ expect(page).to have_css("a[data-track-label='merge_requests_to_review'][aria-current='page']")
+ end
+ end
+ end
it 'disables target branch filter' do
visit merge_requests_dashboard_path
@@ -35,11 +49,11 @@ RSpec.describe 'Dashboard Merge Requests', feature_category: :code_review_workfl
visit merge_requests_dashboard_path
end
- it 'shows projects only with merge requests feature enabled', :js do
+ it 'shows projects only with merge requests feature enabled' do
click_button 'Select project to create merge request'
wait_for_requests
- page.within('[data-testid="new-resource-dropdown"]') do
+ within_testid('new-resource-dropdown') do
expect(page).to have_content(project.full_name)
expect(page).not_to have_content(project_with_disabled_merge_requests.full_name)
@@ -132,14 +146,10 @@ RSpec.describe 'Dashboard Merge Requests', feature_category: :code_review_workfl
end
it 'includes assigned and reviewers in badge' do
- within("span[aria-label='#{n_("%d merge request", "%d merge requests", 3) % 3}']") do
- expect(page).to have_content('3')
+ within('#merge-requests') do
+ expect(page).to have_css("a", text: 'Assigned 2')
+ expect(page).to have_css("a", text: 'Review requests 1')
end
-
- find('.dashboard-shortcuts-merge_requests').click
-
- expect(find('.js-assigned-mr-count')).to have_content('2')
- expect(find('.js-reviewer-mr-count')).to have_content('1')
end
it 'shows assigned merge requests' do
@@ -156,7 +166,7 @@ RSpec.describe 'Dashboard Merge Requests', feature_category: :code_review_workfl
expect(page).not_to have_content(review_requested_merge_request.title)
end
- it 'shows authored merge requests', :js do
+ it 'shows authored merge requests' do
reset_filters
input_filtered_search("author:=#{current_user.to_reference}")
@@ -169,7 +179,7 @@ RSpec.describe 'Dashboard Merge Requests', feature_category: :code_review_workfl
expect(page).not_to have_content(other_merge_request.title)
end
- it 'shows labeled merge requests', :js do
+ it 'shows labeled merge requests' do
reset_filters
input_filtered_search("label:=#{label.name}")
@@ -182,13 +192,13 @@ RSpec.describe 'Dashboard Merge Requests', feature_category: :code_review_workfl
expect(page).not_to have_content(other_merge_request.title)
end
- it 'shows error message without filter', :js do
+ it 'shows error message without filter' do
reset_filters
expect(page).to have_content('Please select at least one filter to see results')
end
- it 'shows sorted merge requests', :js do
+ it 'shows sorted merge requests' do
pajamas_sort_by(s_('SortOptions|Created date'))
visit merge_requests_dashboard_path(assignee_username: current_user.username)
@@ -196,7 +206,7 @@ RSpec.describe 'Dashboard Merge Requests', feature_category: :code_review_workfl
expect(find('.issues-filters')).to have_content('Created date')
end
- it 'keeps sorting merge requests after visiting Projects MR page', :js do
+ it 'keeps sorting merge requests after visiting Projects MR page' do
pajamas_sort_by(s_('SortOptions|Created date'))
visit project_merge_requests_path(project)
@@ -205,7 +215,7 @@ RSpec.describe 'Dashboard Merge Requests', feature_category: :code_review_workfl
end
end
- context 'merge request review', :js do
+ context 'merge request review' do
let_it_be(:author_user) { create(:user) }
let!(:review_requested_merge_request) do
diff --git a/spec/features/dashboard/milestones_spec.rb b/spec/features/dashboard/milestones_spec.rb
index 38637115246..915626de33c 100644
--- a/spec/features/dashboard/milestones_spec.rb
+++ b/spec/features/dashboard/milestones_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Dashboard > Milestones', feature_category: :team_planning do
+RSpec.describe 'Dashboard > Milestones', :js, feature_category: :team_planning do
describe 'as anonymous user' do
before do
visit dashboard_milestones_path
@@ -14,7 +14,7 @@ RSpec.describe 'Dashboard > Milestones', feature_category: :team_planning do
end
describe 'as logged-in user' do
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project) { create(:project, namespace: user.namespace) }
let!(:milestone) { create(:milestone, project: project) }
@@ -35,11 +35,11 @@ RSpec.describe 'Dashboard > Milestones', feature_category: :team_planning do
expect(first('.milestone')).to have_content('Merge requests')
end
- describe 'new milestones dropdown', :js do
- it 'takes user to a new milestone page', :js do
+ describe 'new milestones dropdown' do
+ it 'takes user to a new milestone page' do
click_button 'Select project to create milestone'
- page.within('[data-testid="new-resource-dropdown"]') do
+ within_testid('new-resource-dropdown') do
click_button group.name
click_link "New milestone in #{group.name}"
end
@@ -50,7 +50,7 @@ RSpec.describe 'Dashboard > Milestones', feature_category: :team_planning do
end
describe 'with merge requests disabled' do
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project) { create(:project, :merge_requests_disabled, namespace: user.namespace) }
let!(:milestone) { create(:milestone, project: project) }
diff --git a/spec/features/dashboard/navbar_spec.rb b/spec/features/dashboard/navbar_spec.rb
index 30e7f2d2e4e..2ce9eba309d 100644
--- a/spec/features/dashboard/navbar_spec.rb
+++ b/spec/features/dashboard/navbar_spec.rb
@@ -2,10 +2,10 @@
require 'spec_helper'
-RSpec.describe '"Your work" navbar', feature_category: :navigation do
+RSpec.describe '"Your work" navbar', :js, feature_category: :navigation do
include_context 'dashboard navbar structure'
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
it_behaves_like 'verified navigation bar' do
before do
diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb
index 90ad6fcea25..5379dabc713 100644
--- a/spec/features/dashboard/projects_spec.rb
+++ b/spec/features/dashboard/projects_spec.rb
@@ -2,8 +2,8 @@
require 'spec_helper'
-RSpec.describe 'Dashboard Projects', feature_category: :groups_and_projects do
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+RSpec.describe 'Dashboard Projects', :js, feature_category: :groups_and_projects do
+ let_it_be(:user) { create(:user) }
let_it_be(:project, reload: true) { create(:project, :repository, creator: build(:user)) } # ensure creator != owner to avoid N+1 false-positive
let_it_be(:project2) { create(:project, :public) }
@@ -91,7 +91,7 @@ RSpec.describe 'Dashboard Projects', feature_category: :groups_and_projects do
expect(find('.gl-tabs-nav li:nth-child(1) .badge-pill')).to have_content(1)
end
- it 'shows personal projects on personal projects tab', :js do
+ it 'shows personal projects on personal projects tab' do
project3 = create(:project, namespace: user.namespace)
visit dashboard_projects_path
@@ -111,7 +111,7 @@ RSpec.describe 'Dashboard Projects', feature_category: :groups_and_projects do
end
end
- context 'when on Starred projects tab', :js do
+ context 'when on Starred projects tab' do
it_behaves_like 'a "Your work" page with sidebar and breadcrumbs', :starred_dashboard_projects_path, :projects
it 'shows the empty state when there are no starred projects' do
@@ -153,8 +153,8 @@ RSpec.describe 'Dashboard Projects', feature_category: :groups_and_projects do
page.within('[data-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-status-badge']")
- expect(page).to have_css('.ci-status-icon-success')
+ expect(page).to have_css("[data-testid='ci-icon']")
+ expect(page).to have_css('[data-testid="status_success_borderless-icon"]')
expect(page).to have_link('Pipeline: passed')
end
end
@@ -165,8 +165,8 @@ RSpec.describe 'Dashboard Projects', feature_category: :groups_and_projects do
page.within('[data-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-status-badge']")
- expect(page).not_to have_css('.ci-status-icon-success')
+ expect(page).not_to have_css("[data-testid='ci-icon']")
+ expect(page).not_to have_css('[data-testid="status_success_borderless-icon"]')
expect(page).not_to have_link('Pipeline: passed')
end
end
diff --git a/spec/features/dashboard/shortcuts_spec.rb b/spec/features/dashboard/shortcuts_spec.rb
index c8013d364e3..976dcc5a027 100644
--- a/spec/features/dashboard/shortcuts_spec.rb
+++ b/spec/features/dashboard/shortcuts_spec.rb
@@ -50,7 +50,6 @@ RSpec.describe 'Dashboard shortcuts', :js, feature_category: :shared do
context 'logged out' do
before do
- stub_feature_flags(super_sidebar_logged_out: false)
visit explore_root_path
end
diff --git a/spec/features/dashboard/snippets_spec.rb b/spec/features/dashboard/snippets_spec.rb
index f9284f9479e..5ab5a27171c 100644
--- a/spec/features/dashboard/snippets_spec.rb
+++ b/spec/features/dashboard/snippets_spec.rb
@@ -2,8 +2,8 @@
require 'spec_helper'
-RSpec.describe 'Dashboard snippets', feature_category: :source_code_management do
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+RSpec.describe 'Dashboard snippets', :js, feature_category: :source_code_management do
+ let_it_be(:user) { create(:user) }
it_behaves_like 'a "Your work" page with sidebar and breadcrumbs', :dashboard_snippets_path, :snippets
@@ -32,7 +32,7 @@ RSpec.describe 'Dashboard snippets', feature_category: :source_code_management d
end
end
- context 'when there are no project snippets', :js do
+ context 'when there are no project snippets' do
let(:project) { create(:project, :public, creator: user) }
before do
@@ -55,7 +55,7 @@ RSpec.describe 'Dashboard snippets', feature_category: :source_code_management d
it 'shows documentation button in main comment area' do
parent_element = page.find('.row.empty-state')
- expect(parent_element).to have_link('Documentation', href: help_page_path('user/snippets.md'))
+ expect(parent_element).to have_link('Documentation', href: help_page_path('user/snippets'))
end
end
diff --git a/spec/features/dashboard/todos/todos_spec.rb b/spec/features/dashboard/todos/todos_spec.rb
index ade7da0cb49..59ce873905a 100644
--- a/spec/features/dashboard/todos/todos_spec.rb
+++ b/spec/features/dashboard/todos/todos_spec.rb
@@ -2,13 +2,13 @@
require 'spec_helper'
-RSpec.describe 'Dashboard Todos', feature_category: :team_planning do
+RSpec.describe 'Dashboard Todos', :js, feature_category: :team_planning do
include DesignManagementTestHelpers
- let_it_be(:user) { create(:user, :no_super_sidebar, username: 'john') }
- let_it_be(:user2) { create(:user, :no_super_sidebar, username: 'diane') }
+ let_it_be(:user) { create(:user, username: 'john') }
+ let_it_be(:user2) { create(:user, username: 'diane') }
let_it_be(:user3) { create(:user) }
- let_it_be(:author) { create(:user, :no_super_sidebar) }
+ let_it_be(:author) { create(:user) }
let_it_be(:project) { create(:project, :public) }
let_it_be(:issue) { create(:issue, project: project, due_date: Date.today, title: "Fix bug") }
@@ -73,7 +73,7 @@ RSpec.describe 'Dashboard Todos', feature_category: :team_planning do
end
end
- context 'User has a todo', :js do
+ context 'User has a todo' do
let_it_be(:user_todo) { create(:todo, :mentioned, user: user, project: project, target: issue, author: author) }
before do
@@ -287,7 +287,7 @@ RSpec.describe 'Dashboard Todos', feature_category: :team_planning do
end
end
- context 'User has done todos', :js do
+ context 'User has done todos' do
before do
create(:todo, :mentioned, :done, user: user, project: project, target: issue, author: author)
sign_in(user)
@@ -359,7 +359,7 @@ RSpec.describe 'Dashboard Todos', feature_category: :team_planning do
expect(page).to have_selector('.gl-pagination .js-pagination-page', count: 2)
end
- describe 'mark all as done', :js do
+ describe 'mark all as done' do
before do
visit dashboard_todos_path
find('.js-todos-mark-all').click
@@ -377,7 +377,7 @@ RSpec.describe 'Dashboard Todos', feature_category: :team_planning do
end
end
- describe 'undo mark all as done', :js do
+ describe 'undo mark all as done' do
before do
visit dashboard_todos_path
end
diff --git a/spec/features/explore/catalog_spec.rb b/spec/features/explore/catalog_spec.rb
new file mode 100644
index 00000000000..52ce52e43fe
--- /dev/null
+++ b/spec/features/explore/catalog_spec.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Global Catalog', :js, feature_category: :pipeline_composition do
+ let_it_be(:namespace) { create(:group) }
+ let_it_be(:user) { create(:user) }
+
+ before_all do
+ namespace.add_developer(user)
+ end
+
+ before do
+ sign_in(user)
+ end
+
+ describe 'GET explore/catalog' do
+ let_it_be(:project) { create(:project, :repository, namespace: namespace) }
+ let_it_be(:ci_resource_projects) do
+ create_list(
+ :project,
+ 3,
+ :repository,
+ description: 'A simple component',
+ namespace: namespace
+ )
+ end
+
+ before do
+ ci_resource_projects.each do |current_project|
+ create(:ci_catalog_resource, project: current_project)
+ end
+
+ visit explore_catalog_index_path
+ wait_for_requests
+ end
+
+ it 'shows CI Catalog title and description', :aggregate_failures do
+ expect(page).to have_content('CI/CD Catalog')
+ expect(page).to have_content('Discover CI configuration resources for a seamless CI/CD experience.')
+ end
+
+ it 'renders CI Catalog resources list' do
+ expect(find_all('[data-testid="catalog-resource-item"]').length).to be(3)
+ end
+
+ context 'for a single CI/CD catalog resource' do
+ it 'renders resource details', :aggregate_failures do
+ within_testid('catalog-resource-item', match: :first) do
+ expect(page).to have_content(ci_resource_projects[2].name)
+ expect(page).to have_content(ci_resource_projects[2].description)
+ expect(page).to have_content(namespace.name)
+ end
+ end
+
+ context 'when clicked' do
+ before do
+ find_by_testid('ci-resource-link', match: :first).click
+ end
+
+ it 'navigate to the details page' do
+ expect(page).to have_content('Go to the project')
+ end
+ end
+ end
+ end
+
+ describe 'GET explore/catalog/:id' do
+ let_it_be(:project) { create(:project, :repository, namespace: namespace) }
+ let_it_be(:new_ci_resource) { create(:ci_catalog_resource, project: project) }
+
+ before do
+ visit explore_catalog_path(id: new_ci_resource["id"])
+ end
+
+ it 'navigates to the details page' do
+ expect(page).to have_content('Go to the project')
+ end
+ end
+end
diff --git a/spec/features/explore/navbar_spec.rb b/spec/features/explore/navbar_spec.rb
index 853d66ed4d1..c172760eb2c 100644
--- a/spec/features/explore/navbar_spec.rb
+++ b/spec/features/explore/navbar_spec.rb
@@ -2,13 +2,24 @@
require 'spec_helper'
-RSpec.describe '"Explore" navbar', feature_category: :navigation do
+RSpec.describe '"Explore" navbar', :js, feature_category: :navigation do
include_context '"Explore" navbar structure'
it_behaves_like 'verified navigation bar' do
before do
- stub_feature_flags(super_sidebar_logged_out: false)
+ stub_feature_flags(global_ci_catalog: false)
visit explore_projects_path
end
end
+
+ context "with 'global_ci_catalog' enabled" do
+ include_context '"Explore" navbar structure with global_ci_catalog FF'
+
+ it_behaves_like 'verified navigation bar', global_ci_catalog: true do
+ before do
+ stub_feature_flags(global_ci_catalog: true)
+ visit explore_projects_path
+ end
+ end
+ end
end
diff --git a/spec/features/explore/user_explores_projects_spec.rb b/spec/features/explore/user_explores_projects_spec.rb
index 43d464e0c9f..e1341824bfd 100644
--- a/spec/features/explore/user_explores_projects_spec.rb
+++ b/spec/features/explore/user_explores_projects_spec.rb
@@ -3,20 +3,45 @@
require 'spec_helper'
RSpec.describe 'User explores projects', feature_category: :user_profile do
- before do
- stub_feature_flags(super_sidebar_logged_out: false)
+ shared_examples 'an "Explore > Projects" page with sidebar and breadcrumbs' do |page_path|
+ before do
+ visit send(page_path)
+ end
+
+ describe "sidebar", :js do
+ it 'shows the "Explore" sidebar' do
+ has_testid?('super-sidebar')
+ within_testid('super-sidebar') do
+ expect(page).to have_css('#super-sidebar-context-header', text: 'Explore')
+ end
+ end
+
+ it 'shows the "Projects" menu item as active' do
+ within_testid('super-sidebar') do
+ expect(page).to have_css("[aria-current='page']", text: "Projects")
+ end
+ end
+ end
+
+ describe 'breadcrumbs' do
+ it 'has "Explore" as its root breadcrumb' do
+ within '.breadcrumbs-list li:first' do
+ expect(page).to have_link('Explore', href: explore_root_path)
+ end
+ end
+ end
end
describe '"All" tab' do
- it_behaves_like 'an "Explore" page with sidebar and breadcrumbs', :explore_projects_path, :projects
+ it_behaves_like 'an "Explore > Projects" page with sidebar and breadcrumbs', :explore_projects_path
end
describe '"Most starred" tab' do
- it_behaves_like 'an "Explore" page with sidebar and breadcrumbs', :starred_explore_projects_path, :projects
+ it_behaves_like 'an "Explore > Projects" page with sidebar and breadcrumbs', :starred_explore_projects_path
end
describe '"Trending" tab' do
- it_behaves_like 'an "Explore" page with sidebar and breadcrumbs', :trending_explore_projects_path, :projects
+ it_behaves_like 'an "Explore > Projects" page with sidebar and breadcrumbs', :trending_explore_projects_path
end
context 'when some projects exist' do
diff --git a/spec/features/global_search_spec.rb b/spec/features/global_search_spec.rb
index dfafacf48e2..7d6d1648ff5 100644
--- a/spec/features/global_search_spec.rb
+++ b/spec/features/global_search_spec.rb
@@ -3,9 +3,7 @@
require 'spec_helper'
RSpec.describe 'Global search', :js, feature_category: :global_search do
- include AfterNextHelpers
-
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, namespace: user.namespace) }
before do
@@ -18,17 +16,18 @@ RSpec.describe 'Global search', :js, feature_category: :global_search do
visit dashboard_projects_path
end
- it 'renders updated search bar' do
- expect(page).to have_no_selector('.search-form')
- expect(page).to have_selector('#js-header-search')
+ it 'renders search button' do
+ expect(page).to have_button('Search or go to…')
end
- it 'focuses search input when shortcut "s" is pressed' do
- expect(page).not_to have_selector('#search:focus')
+ it 'opens search modal when shortcut "s" is pressed' do
+ search_selector = 'input[type="search"]:focus'
+
+ expect(page).not_to have_selector(search_selector)
find('body').native.send_key('s')
- expect(page).to have_selector('#search:focus')
+ expect(page).to have_selector(search_selector)
end
end
end
diff --git a/spec/features/group_variables_spec.rb b/spec/features/group_variables_spec.rb
index b4a0678cb5f..841cc1726a0 100644
--- a/spec/features/group_variables_spec.rb
+++ b/spec/features/group_variables_spec.rb
@@ -12,13 +12,13 @@ RSpec.describe 'Group variables', :js, feature_category: :secrets_management do
group.add_owner(user)
gitlab_sign_in(user)
- stub_feature_flags(ci_variable_drawer: false)
visit page_path
wait_for_requests
end
context 'when ci_variables_pages FF is enabled' do
- it_behaves_like 'variable list'
+ it_behaves_like 'variable list drawer'
+ it_behaves_like 'variable list env scope'
it_behaves_like 'variable list pagination', :ci_group_variable
end
@@ -27,16 +27,7 @@ RSpec.describe 'Group variables', :js, feature_category: :secrets_management do
stub_feature_flags(ci_variables_pages: false)
end
- it_behaves_like 'variable list'
- end
-
- context 'when ci_variable_drawer FF is enabled' do
- before do
- stub_feature_flags(ci_variable_drawer: true)
- visit page_path
- wait_for_requests
- end
-
it_behaves_like 'variable list drawer'
+ it_behaves_like 'variable list env scope'
end
end
diff --git a/spec/features/groups/board_sidebar_spec.rb b/spec/features/groups/board_sidebar_spec.rb
index 6a1b7d20a25..3fe520ea2ea 100644
--- a/spec/features/groups/board_sidebar_spec.rb
+++ b/spec/features/groups/board_sidebar_spec.rb
@@ -19,7 +19,6 @@ RSpec.describe 'Group Issue Boards', :js, feature_category: :groups_and_projects
let(:card) { find('.board:nth-child(1)').first('.board-card') }
before do
- stub_feature_flags(apollo_boards: false)
sign_in(user)
visit group_board_path(group, board)
diff --git a/spec/features/groups/board_spec.rb b/spec/features/groups/board_spec.rb
index c2d6b80b4c0..e6dc6055e27 100644
--- a/spec/features/groups/board_spec.rb
+++ b/spec/features/groups/board_spec.rb
@@ -14,8 +14,6 @@ RSpec.describe 'Group Boards', feature_category: :team_planning do
let_it_be(:project) { create(:project_empty_repo, group: group) }
before do
- stub_feature_flags(apollo_boards: false)
-
group.add_maintainer(user)
sign_in(user)
@@ -61,8 +59,6 @@ RSpec.describe 'Group Boards', feature_category: :team_planning do
let_it_be(:issue2) { create(:issue, title: 'issue2', project: project2) }
before do
- stub_feature_flags(apollo_boards: false)
-
project1.add_guest(user)
project2.add_reporter(user)
diff --git a/spec/features/groups/container_registry_spec.rb b/spec/features/groups/container_registry_spec.rb
index 953a8e27547..65edeb0798f 100644
--- a/spec/features/groups/container_registry_spec.rb
+++ b/spec/features/groups/container_registry_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'Container Registry', :js, feature_category: :container_registry do
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project) { create(:project, namespace: group) }
@@ -28,8 +28,8 @@ RSpec.describe 'Container Registry', :js, feature_category: :container_registry
it 'sidebar menu is open' do
visit_container_registry
- sidebar = find('.nav-sidebar')
- expect(sidebar).to have_link _('Container Registry')
+ expect(page).to have_active_navigation('Deploy')
+ expect(page).to have_active_sub_navigation('Container Registry')
end
context 'when there are no image repositories' do
diff --git a/spec/features/groups/dependency_proxy_spec.rb b/spec/features/groups/dependency_proxy_spec.rb
index 2d4f6d4fbf2..12c480a46b0 100644
--- a/spec/features/groups/dependency_proxy_spec.rb
+++ b/spec/features/groups/dependency_proxy_spec.rb
@@ -3,8 +3,8 @@
require 'spec_helper'
RSpec.describe 'Group Dependency Proxy', feature_category: :dependency_proxy do
- let(:owner) { create(:user, :no_super_sidebar) }
- let(:reporter) { create(:user, :no_super_sidebar) }
+ let(:owner) { create(:user) }
+ let(:reporter) { create(:user) }
let(:group) { create(:group) }
let(:path) { group_dependency_proxy_path(group) }
let(:settings_path) { group_settings_packages_and_registries_path(group) }
@@ -36,8 +36,8 @@ RSpec.describe 'Group Dependency Proxy', feature_category: :dependency_proxy do
it 'sidebar menu is open' do
visit path
- sidebar = find('.nav-sidebar')
- expect(sidebar).to have_link _('Dependency Proxy')
+ expect(page).to have_active_navigation('Operate')
+ expect(page).to have_active_sub_navigation('Dependency Proxy')
end
it 'toggles defaults to enabled' do
diff --git a/spec/features/groups/group_page_with_external_authorization_service_spec.rb b/spec/features/groups/group_page_with_external_authorization_service_spec.rb
index 4cc0fe4171d..fe1a685899f 100644
--- a/spec/features/groups/group_page_with_external_authorization_service_spec.rb
+++ b/spec/features/groups/group_page_with_external_authorization_service_spec.rb
@@ -2,10 +2,10 @@
require 'spec_helper'
-RSpec.describe 'The group page', feature_category: :groups_and_projects do
+RSpec.describe 'The group page', :js, feature_category: :groups_and_projects do
include ExternalAuthorizationServiceHelpers
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
let(:group) { create(:group) }
before do
@@ -14,8 +14,9 @@ RSpec.describe 'The group page', feature_category: :groups_and_projects do
end
def expect_all_sidebar_links
- within('.nav-sidebar') do
- expect(page).to have_link('Group information')
+ within('#super-sidebar .contextual-nav') do
+ click_button 'Manage'
+ click_button 'Plan'
expect(page).to have_link('Activity')
expect(page).to have_link('Issues')
expect(page).to have_link('Merge requests')
@@ -42,8 +43,11 @@ RSpec.describe 'The group page', feature_category: :groups_and_projects do
enable_external_authorization_service_check
visit group_path(group)
- within('.nav-sidebar') do
- expect(page).to have_link('Group information')
+ within('#super-sidebar .contextual-nav') do
+ expect(page).not_to have_button('Plan')
+
+ click_button 'Manage'
+
expect(page).not_to have_link('Activity')
expect(page).not_to have_link('Contribution')
diff --git a/spec/features/groups/group_settings_spec.rb b/spec/features/groups/group_settings_spec.rb
index a248a2b471a..0437e5df6e9 100644
--- a/spec/features/groups/group_settings_spec.rb
+++ b/spec/features/groups/group_settings_spec.rb
@@ -329,7 +329,7 @@ RSpec.describe 'Edit group settings', feature_category: :groups_and_projects do
end
def updated_emails_disabled?
- group.reload.clear_memoization(:emails_disabled_memoized)
+ group.reload.clear_memoization(:emails_enabled_memoized)
group.emails_disabled?
end
end
diff --git a/spec/features/groups/members/request_access_spec.rb b/spec/features/groups/members/request_access_spec.rb
index c04b84be90e..2d0b2e483c5 100644
--- a/spec/features/groups/members/request_access_spec.rb
+++ b/spec/features/groups/members/request_access_spec.rb
@@ -3,8 +3,8 @@
require 'spec_helper'
RSpec.describe 'Groups > Members > Request access', feature_category: :groups_and_projects do
- let(:user) { create(:user, :no_super_sidebar) }
- let(:owner) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
+ let(:owner) { create(:user) }
let(:group) { create(:group, :public) }
let!(:project) { create(:project, :private, namespace: group) }
@@ -48,12 +48,15 @@ RSpec.describe 'Groups > Members > Request access', feature_category: :groups_an
expect(page).not_to have_content group.name
end
- it 'user is not listed in the group members page' do
+ it 'user is not listed in the group members page', :js do
click_link 'Request Access'
expect(group.requesters.exists?(user_id: user)).to be_truthy
- first(:link, 'Members').click
+ within_testid 'super-sidebar' do
+ click_button 'Manage'
+ first(:link, 'Members').click
+ end
page.within('.content') do
expect(page).not_to have_content(user.name)
diff --git a/spec/features/groups/members/sort_members_spec.rb b/spec/features/groups/members/sort_members_spec.rb
index fd367b8e763..ea6f3ae1966 100644
--- a/spec/features/groups/members/sort_members_spec.rb
+++ b/spec/features/groups/members/sort_members_spec.rb
@@ -19,7 +19,7 @@ RSpec.describe 'Groups > Members > Sort members', :js, feature_category: :groups
def expect_sort_by(text, sort_direction)
within('[data-testid="members-sort-dropdown"]') do
expect(page).to have_css('button[aria-haspopup="menu"]', text: text)
- expect(page).to have_button("Sorting Direction: #{sort_direction == :asc ? 'Ascending' : 'Descending'}")
+ expect(page).to have_button("Sort direction: #{sort_direction == :asc ? 'Ascending' : 'Descending'}")
end
end
diff --git a/spec/features/groups/merge_requests_spec.rb b/spec/features/groups/merge_requests_spec.rb
index bbb7d322b9a..0a830e6715c 100644
--- a/spec/features/groups/merge_requests_spec.rb
+++ b/spec/features/groups/merge_requests_spec.rb
@@ -26,8 +26,10 @@ RSpec.describe 'Group merge requests page', feature_category: :code_review_workf
expect(page).not_to have_content(issuable_archived.title)
end
- it 'ignores archived merge request count badges in navbar' do
- expect(first(:link, text: 'Merge requests').find('.badge').text).to eq("1")
+ it 'ignores archived merge request count badges in navbar', :js do
+ within_testid('super-sidebar') do
+ expect(find_link(text: 'Merge requests').find('.badge').text).to eq("1")
+ end
end
it 'ignores archived merge request count badges in state-filters' do
diff --git a/spec/features/groups/navbar_spec.rb b/spec/features/groups/navbar_spec.rb
index 76e4e32d138..7d5cc704f9c 100644
--- a/spec/features/groups/navbar_spec.rb
+++ b/spec/features/groups/navbar_spec.rb
@@ -2,19 +2,19 @@
require 'spec_helper'
-RSpec.describe 'Group navbar', :with_license, feature_category: :navigation do
+RSpec.describe 'Group navbar', :with_license, :js, feature_category: :navigation do
include NavbarStructureHelper
include WikiHelpers
include_context 'group navbar structure'
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
let(:group) { create(:group) }
before do
- insert_package_nav(_('Kubernetes'))
- insert_after_nav_item(_('Analytics'), new_nav_item: settings_for_maintainer_nav_item) if Gitlab.ee?
+ create_package_nav(_('Operate'))
+ insert_after_nav_item(_('Analyze'), new_nav_item: settings_for_maintainer_nav_item) if Gitlab.ee?
stub_config(dependency_proxy: { enabled: false })
stub_config(registry: { enabled: false })
@@ -46,9 +46,9 @@ RSpec.describe 'Group navbar', :with_license, feature_category: :navigation do
before do
if Gitlab.ee?
- insert_customer_relations_nav(_('Analytics'))
+ insert_customer_relations_nav(_('Iterations'))
else
- insert_customer_relations_nav(_('Packages and registries'))
+ insert_customer_relations_nav(_('Milestones'))
end
visit group_path(group)
@@ -85,7 +85,7 @@ RSpec.describe 'Group navbar', :with_license, feature_category: :navigation do
before do
group.update!(harbor_integration: harbor_integration)
- insert_harbor_registry_nav(_('Package Registry'))
+ insert_harbor_registry_nav(_('Kubernetes'))
visit group_path(group)
end
diff --git a/spec/features/groups/new_group_page_spec.rb b/spec/features/groups/new_group_page_spec.rb
index e1034f2bb9d..f86430ae617 100644
--- a/spec/features/groups/new_group_page_spec.rb
+++ b/spec/features/groups/new_group_page_spec.rb
@@ -12,42 +12,17 @@ RSpec.describe 'New group page', :js, feature_category: :groups_and_projects do
end
describe 'sidebar' do
- context 'in the current navigation' do
- before do
- user.update!(use_new_navigation: false)
- end
-
- context 'for a new top-level group' do
- it_behaves_like 'a "Your work" page with sidebar and breadcrumbs', :new_group_path, :groups
- end
-
- context 'for a new subgroup' do
- it 'shows the group sidebar of the parent group' do
- visit new_group_path(parent_id: parent_group.id, anchor: 'create-group-pane')
- expect(page).to have_selector(
- ".nav-sidebar[aria-label=\"Group navigation\"] .context-header[title=\"#{parent_group.name}\"]"
- )
- end
+ context 'for a new top-level group' do
+ it 'shows the "Your work" navigation' do
+ visit new_group_path
+ expect(page).to have_selector(".super-sidebar", text: "Your work")
end
end
- context 'in the new navigation' do
- before do
- user.update!(use_new_navigation: true)
- end
-
- context 'for a new top-level group' do
- it 'shows the "Your work" navigation' do
- visit new_group_path
- expect(page).to have_selector(".super-sidebar", text: "Your work")
- end
- end
-
- context 'for a new subgroup' do
- it 'shows the group navigation of the parent group' do
- visit new_group_path(parent_id: parent_group.id, anchor: 'create-group-pane')
- expect(page).to have_selector(".super-sidebar", text: parent_group.name)
- end
+ context 'for a new subgroup' do
+ it 'shows the group navigation of the parent group' do
+ visit new_group_path(parent_id: parent_group.id, anchor: 'create-group-pane')
+ expect(page).to have_selector(".super-sidebar", text: parent_group.name)
end
end
end
diff --git a/spec/features/groups/packages_spec.rb b/spec/features/groups/packages_spec.rb
index 1d9269501be..7819b1f0ab6 100644
--- a/spec/features/groups/packages_spec.rb
+++ b/spec/features/groups/packages_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'Group Packages', feature_category: :package_registry do
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
@@ -32,7 +32,7 @@ RSpec.describe 'Group Packages', feature_category: :package_registry do
end
it 'sidebar menu is open' do
- sidebar = find('.nav-sidebar')
+ sidebar = find_by_testid('super-sidebar')
expect(sidebar).to have_link _('Package Registry')
end
diff --git a/spec/features/groups/settings/packages_and_registries_spec.rb b/spec/features/groups/settings/packages_and_registries_spec.rb
index fa310722860..cbd26441e2b 100644
--- a/spec/features/groups/settings/packages_and_registries_spec.rb
+++ b/spec/features/groups/settings/packages_and_registries_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe 'Group Package and registry settings', feature_category: :package_registry do
include WaitForRequests
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
let(:group) { create(:group) }
let(:sub_group) { create(:group, parent: group) }
@@ -20,12 +20,13 @@ RSpec.describe 'Group Package and registry settings', feature_category: :package
stub_packages_setting(enabled: false)
end
- it 'the menu item is not visible' do
+ it 'the menu item is not visible', :js do
visit group_path(group)
- settings_menu = find_settings_menu
-
- expect(settings_menu).not_to have_content 'Packages and registries'
+ within_testid('super-sidebar') do
+ click_button 'Settings'
+ expect(page).not_to have_content 'Packages and registries'
+ end
end
it 'renders 404 when navigating to page' do
@@ -36,11 +37,13 @@ RSpec.describe 'Group Package and registry settings', feature_category: :package
end
context 'when packages feature is enabled on the group' do
- it 'the menu item is visible' do
+ it 'the menu item is visible', :js do
visit group_path(group)
- settings_menu = find_settings_menu
- expect(settings_menu).to have_content 'Packages and registries'
+ within_testid('super-sidebar') do
+ click_button 'Settings'
+ expect(page).to have_content 'Packages and registries'
+ end
end
it 'has a page title set' do
@@ -49,11 +52,12 @@ RSpec.describe 'Group Package and registry settings', feature_category: :package
expect(page).to have_title _('Packages and registries settings')
end
- it 'sidebar menu is open' do
+ it 'sidebar menu is open', :js do
visit_settings_page
- sidebar = find('.nav-sidebar')
- expect(sidebar).to have_link _('Packages and registries')
+ within_testid('super-sidebar') do
+ expect(page).to have_link _('Packages and registries')
+ end
end
it 'passes axe automated accessibility testing', :js do
@@ -62,7 +66,7 @@ RSpec.describe 'Group Package and registry settings', feature_category: :package
wait_for_requests
expect(page).to be_axe_clean.within('[data-testid="packages-and-registries-group-settings"]')
- .skipping :'link-in-text-block'
+ .skipping :'link-in-text-block', :'heading-order'
end
it 'has a Duplicate packages section', :js do
@@ -124,10 +128,6 @@ RSpec.describe 'Group Package and registry settings', feature_category: :package
end
end
- def find_settings_menu
- find('.shortcuts-settings ul')
- end
-
def visit_settings_page
visit group_settings_packages_and_registries_path(group)
end
diff --git a/spec/features/groups/show_spec.rb b/spec/features/groups/show_spec.rb
index 8450322945c..cf18f3cb4e5 100644
--- a/spec/features/groups/show_spec.rb
+++ b/spec/features/groups/show_spec.rb
@@ -276,7 +276,7 @@ RSpec.describe 'Group show page', feature_category: :groups_and_projects do
end
it 'is disabled if emails are disabled' do
- group.update!(emails_disabled: true)
+ group.update!(emails_enabled: false)
visit path
diff --git a/spec/features/groups/user_sees_package_sidebar_spec.rb b/spec/features/groups/user_sees_package_sidebar_spec.rb
index 4efb9ff7608..8985b602eb7 100644
--- a/spec/features/groups/user_sees_package_sidebar_spec.rb
+++ b/spec/features/groups/user_sees_package_sidebar_spec.rb
@@ -2,8 +2,8 @@
require 'spec_helper'
-RSpec.describe 'Groups > sidebar', feature_category: :groups_and_projects do
- let(:user) { create(:user, :no_super_sidebar) }
+RSpec.describe 'Groups > sidebar', :js, feature_category: :groups_and_projects do
+ let(:user) { create(:user) }
let(:group) { create(:group) }
before do
@@ -19,13 +19,15 @@ RSpec.describe 'Groups > sidebar', feature_category: :groups_and_projects do
end
it 'shows main menu' do
- within '.nav-sidebar' do
- expect(page).to have_link(_('Packages'))
+ within_testid 'super-sidebar' do
+ click_button 'Deploy'
+ expect(page).to have_link(_('Package Registry'))
end
end
it 'has container registry link' do
- within '.nav-sidebar' do
+ within_testid 'super-sidebar' do
+ click_button 'Deploy'
expect(page).to have_link(_('Container Registry'))
end
end
@@ -38,7 +40,8 @@ RSpec.describe 'Groups > sidebar', feature_category: :groups_and_projects do
end
it 'does not have container registry link' do
- within '.nav-sidebar' do
+ within_testid 'super-sidebar' do
+ click_button 'Deploy'
expect(page).not_to have_link(_('Container Registry'))
end
end
diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb
index bcbfdf487ac..578f39181d1 100644
--- a/spec/features/groups_spec.rb
+++ b/spec/features/groups_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'Group', feature_category: :groups_and_projects do
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
before do
sign_in(user)
@@ -441,21 +441,30 @@ RSpec.describe 'Group', feature_category: :groups_and_projects do
expect(page).to have_content(nested_group.name)
expect(page).to have_content(project.name)
- expect(page).to have_link('Group information')
end
- it 'renders subgroup page with the text "Subgroup information"' do
+ it 'renders group page with the text "Group" in the sidebar header' do
+ visit group_path(group)
+
+ within('#super-sidebar-context-header') do
+ expect(page).to have_text('Group')
+ end
+ end
+
+ it 'renders subgroup page with the text "Group" in the sidebar header' do
visit group_path(nested_group)
- wait_for_requests
- expect(page).to have_link('Subgroup information')
+ within('#super-sidebar-context-header') do
+ expect(page).to have_text('Group')
+ end
end
- it 'renders project page with the text "Project information"' do
+ it 'renders project page with the text "Project" in the sidebar header' do
visit project_path(project)
- wait_for_requests
- expect(page).to have_link('Project information')
+ within('#super-sidebar-context-header') do
+ expect(page).to have_text('Project')
+ end
end
end
diff --git a/spec/features/help_dropdown_spec.rb b/spec/features/help_dropdown_spec.rb
index 08d7dba4d79..3e4c0bc55fe 100644
--- a/spec/features/help_dropdown_spec.rb
+++ b/spec/features/help_dropdown_spec.rb
@@ -3,24 +3,19 @@
require 'spec_helper'
RSpec.describe "Help Dropdown", :js, feature_category: :shared do
- let_it_be(:user) { create(:user, :no_super_sidebar) }
- let_it_be(:admin) { create(:admin, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:admin) { create(:admin) }
before do
stub_application_setting(version_check_enabled: true)
end
- context 'when logged in as non-admin' do
- before do
- sign_in(user)
- visit root_path
- end
-
- it 'does not render version data' do
- page.within '.header-help' do
- find('.header-help-dropdown-toggle').click
+ shared_examples 'no version check badge' do
+ it 'does not render version check badge' do
+ within_testid('super-sidebar') do
+ click_on 'Help'
- expect(page).not_to have_text('Your GitLab Version')
+ expect(page).not_to have_text('Your GitLab version')
expect(page).not_to have_text("#{Gitlab.version_info.major}.#{Gitlab.version_info.minor}")
expect(page).not_to have_selector('.version-check-badge')
expect(page).not_to have_text('Up to date')
@@ -28,45 +23,53 @@ RSpec.describe "Help Dropdown", :js, feature_category: :shared do
end
end
- context 'when logged in as admin' do
- before do
- sign_in(admin)
- gitlab_enable_admin_mode_sign_in(admin)
- end
+ shared_examples 'correct version check badge' do |ui_text, severity|
+ context "when severity is #{severity}" do
+ before do
+ sign_in(admin)
+ gitlab_enable_admin_mode_sign_in(admin)
- describe 'does render version data' do
- where(:response, :ui_text) do
- [
- [{ "severity" => "success" }, 'Up to date'],
- [{ "severity" => "warning" }, 'Update available'],
- [{ "severity" => "danger" }, 'Update ASAP']
- ]
+ allow_next_instance_of(VersionCheck) do |instance|
+ allow(instance).to receive(:response).and_return({ "severity" => severity })
+ end
+ visit root_path
end
- with_them do
- before do
- allow_next_instance_of(VersionCheck) do |instance|
- allow(instance).to receive(:response).and_return(response)
- end
- visit root_path
- end
+ it 'renders correct version check badge variant' do
+ within_testid('super-sidebar') do
+ click_on 'Help'
- it 'renders correct version badge variant',
- quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/369850' do
- page.within '.header-help' do
- find('.header-help-dropdown-toggle').click
+ expect(page).to have_text('Your GitLab version')
+ expect(page).to have_text("#{Gitlab.version_info.major}.#{Gitlab.version_info.minor}")
- expect(page).to have_text('Your GitLab Version')
- expect(page).to have_text("#{Gitlab.version_info.major}.#{Gitlab.version_info.minor}")
- expect(page).to have_selector('.version-check-badge')
- expect(page).to have_selector(
- 'a[data-testid="gitlab-version-container"][href="/help/update/index"]'
- )
- expect(page).to have_selector('.version-check-badge[href="/help/update/index"]')
- expect(page).to have_text(ui_text)
+ within page.find_link(href: help_page_path('update/index')) do
+ expect(page).to have_selector(".version-check-badge.badge-#{severity}", text: ui_text)
end
end
end
end
end
+
+ context 'when anonymous user' do
+ before do
+ visit user_path(user)
+ end
+
+ include_examples 'no version check badge'
+ end
+
+ context 'when logged in as non-admin' do
+ before do
+ sign_in(user)
+ visit root_path
+ end
+
+ include_examples 'no version check badge'
+ end
+
+ context 'when logged in as admin' do
+ include_examples 'correct version check badge', 'Up to date', 'success'
+ include_examples 'correct version check badge', 'Update available', 'warning'
+ include_examples 'correct version check badge', 'Update ASAP', 'danger'
+ end
end
diff --git a/spec/features/ide/user_opens_merge_request_spec.rb b/spec/features/ide/user_opens_merge_request_spec.rb
index 1d3cada57db..a8a56ffe310 100644
--- a/spec/features/ide/user_opens_merge_request_spec.rb
+++ b/spec/features/ide/user_opens_merge_request_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'IDE merge request', :js, feature_category: :web_ide do
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public, :repository, namespace: user.namespace) }
let_it_be(:merge_request) { create(:merge_request, :simple, source_project: project) }
@@ -16,7 +16,9 @@ RSpec.describe 'IDE merge request', :js, feature_category: :web_ide do
end
it 'user opens merge request' do
- click_button 'Code'
+ within '.merge-request' do
+ click_button 'Code'
+ end
click_link 'Open in Web IDE'
wait_for_requests
diff --git a/spec/features/invites_spec.rb b/spec/features/invites_spec.rb
index c86d4c260ee..bc6efb63f6f 100644
--- a/spec/features/invites_spec.rb
+++ b/spec/features/invites_spec.rb
@@ -59,7 +59,7 @@ RSpec.describe 'Group or Project invitations', :aggregate_failures, feature_cate
end
context 'when invite is sent before account is created;ldap or service sign in for manual acceptance edge case' do
- let(:user) { create(:user, :no_super_sidebar, email: 'user@example.com') }
+ let(:user) { create(:user, email: 'user@example.com') }
context 'when invite clicked and not signed in' do
before do
@@ -85,7 +85,6 @@ RSpec.describe 'Group or Project invitations', :aggregate_failures, feature_cate
it 'shows message user already a member' do
expect(page).to have_current_path(invite_path(group_invite.raw_invite_token), ignore_query: true)
- expect(page).to have_link(user.name, href: user_path(user))
expect(page).to have_content('You are already a member of this group.')
end
end
diff --git a/spec/features/issuables/shortcuts_issuable_spec.rb b/spec/features/issuables/shortcuts_issuable_spec.rb
index 06387c14ee2..6bb453c34e6 100644
--- a/spec/features/issuables/shortcuts_issuable_spec.rb
+++ b/spec/features/issuables/shortcuts_issuable_spec.rb
@@ -58,14 +58,13 @@ RSpec.describe 'Blob shortcuts', :js, feature_category: :team_planning do
it "opens assignee dropdown for editing" do
find('body').native.send_key('a')
- expect(find('.block.assignee')).to have_selector('.js-sidebar-assignee-data')
+ expect(find('.block.assignee')).to have_selector('.dropdown-menu-user')
end
end
describe 'pressing "a"' do
describe 'On an Issue' do
before do
- stub_feature_flags(issue_assignees_widget: false)
visit project_issue_path(project, issue)
wait_for_requests
end
@@ -75,7 +74,6 @@ RSpec.describe 'Blob shortcuts', :js, feature_category: :team_planning do
describe 'On a Merge Request' do
before do
- stub_feature_flags(issue_assignees_widget: false)
visit project_merge_request_path(project, merge_request)
wait_for_requests
end
diff --git a/spec/features/issues/discussion_lock_spec.rb b/spec/features/issues/discussion_lock_spec.rb
index fb9addff1a2..04d59854ddc 100644
--- a/spec/features/issues/discussion_lock_spec.rb
+++ b/spec/features/issues/discussion_lock_spec.rb
@@ -28,7 +28,7 @@ RSpec.describe 'Discussion Lock', :js, feature_category: :team_planning do
click_button('Lock')
end
- expect(find('#notes')).to have_content('locked this issue')
+ expect(find('#notes')).to have_content('locked the discussion in this issue')
end
end
@@ -46,7 +46,7 @@ RSpec.describe 'Discussion Lock', :js, feature_category: :team_planning do
click_button('Unlock')
end
- expect(find('#notes')).to have_content('unlocked this issue')
+ expect(find('#notes')).to have_content('unlocked the discussion in this issue')
expect(find('.issuable-sidebar')).to have_content('Unlocked')
end
@@ -101,7 +101,7 @@ RSpec.describe 'Discussion Lock', :js, feature_category: :team_planning do
page.within('#notes') do
expect(page).not_to have_selector('js-main-target-form')
expect(page.find('.disabled-comments'))
- .to have_content('This issue is locked. Only project members can comment.')
+ .to have_content('The discussion in this issue is locked. Only project members can comment.')
end
end
end
diff --git a/spec/features/issues/filtered_search/visual_tokens_spec.rb b/spec/features/issues/filtered_search/visual_tokens_spec.rb
index e51c82081ff..ca1a822fd88 100644
--- a/spec/features/issues/filtered_search/visual_tokens_spec.rb
+++ b/spec/features/issues/filtered_search/visual_tokens_spec.rb
@@ -6,8 +6,8 @@ RSpec.describe 'Visual tokens', :js, feature_category: :team_planning do
include FilteredSearchHelpers
let_it_be(:project) { create(:project) }
- let_it_be(:user) { create(:user, :no_super_sidebar, name: 'administrator', username: 'root') }
- let_it_be(:user_rock) { create(:user, :no_super_sidebar, name: 'The Rock', username: 'rock') }
+ let_it_be(:user) { create(:user, name: 'administrator', username: 'root') }
+ let_it_be(:user_rock) { create(:user, name: 'The Rock', username: 'rock') }
let_it_be(:milestone_nine) { create(:milestone, title: '9.0', project: project) }
let_it_be(:milestone_ten) { create(:milestone, title: '10.0', project: project) }
let_it_be(:label) { create(:label, project: project, title: 'abc') }
@@ -41,7 +41,7 @@ RSpec.describe 'Visual tokens', :js, feature_category: :team_planning do
end
it 'ends editing mode when document is clicked' do
- find('.js-navbar').click
+ find('body').click(x: 0, y: 0)
expect_empty_search_term
expect_hidden_suggestions_list
diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb
index 73c53e855b2..2fb30469691 100644
--- a/spec/features/issues/form_spec.rb
+++ b/spec/features/issues/form_spec.rb
@@ -7,9 +7,9 @@ RSpec.describe 'New/edit issue', :js, feature_category: :team_planning do
include ListboxHelpers
let_it_be(:project) { create(:project, :repository) }
- let_it_be(:user) { create(:user, :no_super_sidebar) }
- let_it_be(:user2) { create(:user, :no_super_sidebar) }
- let_it_be(:guest) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:user2) { create(:user) }
+ let_it_be(:guest) { create(:user) }
let_it_be(:milestone) { create(:milestone, project: project) }
let_it_be(:label) { create(:label, project: project) }
let_it_be(:label2) { create(:label, project: project) }
@@ -526,7 +526,7 @@ RSpec.describe 'New/edit issue', :js, feature_category: :team_planning do
find('body').send_keys('e')
- click_link 'Boards'
+ click_link 'Homepage'
expect(page).not_to have_content(expected_content)
end
@@ -539,7 +539,7 @@ RSpec.describe 'New/edit issue', :js, feature_category: :team_planning do
find('body').send_keys('e')
fill_in 'issue-description', with: content
- click_link 'Boards' do
+ click_link 'Homepage' do
page.driver.browser.switch_to.alert.dismiss
end
@@ -554,8 +554,8 @@ RSpec.describe 'New/edit issue', :js, feature_category: :team_planning do
find('body').send_keys('e')
fill_in 'issue-description', with: content
- click_link 'Boards' do
- page.driver.browser.switch_to.alert.accept
+ click_link 'Homepage' do
+ page.driver.browser.switch_to.alert.dismiss
end
expect(page).not_to have_content(content)
@@ -601,14 +601,4 @@ RSpec.describe 'New/edit issue', :js, feature_category: :team_planning do
end
end
end
-
- def before_for_selector(selector)
- js = <<-JS.strip_heredoc
- (function(selector) {
- var el = document.querySelector(selector);
- return window.getComputedStyle(el, '::before').getPropertyValue('content');
- })("#{escape_javascript(selector)}")
- JS
- page.evaluate_script(js)
- end
end
diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb
index a015a83c793..e4df106de07 100644
--- a/spec/features/issues/issue_sidebar_spec.rb
+++ b/spec/features/issues/issue_sidebar_spec.rb
@@ -26,179 +26,66 @@ RSpec.describe 'Issue Sidebar', feature_category: :team_planning do
let(:user2) { create(:user) }
let(:issue2) { create(:issue, project: project, author: user2) }
- context 'when GraphQL assignees widget feature flag is disabled' do
- before do
- stub_feature_flags(issue_assignees_widget: false)
- end
+ include_examples 'issuable invite members' do
+ let(:issuable_path) { project_issue_path(project, issue2) }
+ end
- include_examples 'issuable invite members' do
- let(:issuable_path) { project_issue_path(project, issue2) }
+ context 'when user is a developer' do
+ before do
+ project.add_developer(user)
+ visit_issue(project, issue2)
end
- context 'when user is a developer' do
- before do
- project.add_developer(user)
- visit_issue(project, issue2)
-
- find('.block.assignee .edit-link').click
- wait_for_requests
- end
-
- it 'shows author in assignee dropdown' do
- page.within '.dropdown-menu-user' do
- expect(page).to have_content(user2.name)
- end
- end
-
- it 'shows author when filtering assignee dropdown' do
- page.within '.dropdown-menu-user' do
- find('.dropdown-input-field').set(user2.name)
-
- wait_for_requests
-
- expect(page).to have_content(user2.name)
- end
- end
-
- it 'assigns yourself' do
- find('.block.assignee .dropdown-menu-toggle').click
-
- click_button 'assign yourself'
-
- wait_for_requests
-
- find('.block.assignee .edit-link').click
-
- page.within '.dropdown-menu-user' do
- expect(page.find('.dropdown-header')).to be_visible
- expect(page.find('.dropdown-menu-user-link.is-active')).to have_content(user.name)
- end
- end
-
- it 'keeps your filtered term after filtering and dismissing the dropdown' do
- find('.dropdown-input-field').set(user2.name)
-
- wait_for_requests
-
- page.within '.dropdown-menu-user' do
- expect(page).not_to have_content 'Unassigned'
- click_link user2.name
- end
-
- within '.js-right-sidebar' do
- find('.block.assignee').click(x: 0, y: 0, offset: 0)
- find('.block.assignee .edit-link').click
- end
-
- expect(page.all('.dropdown-menu-user li').length).to eq(6)
- expect(find('.dropdown-input-field').value).to eq('')
- end
-
- it 'shows label text as "Apply" when assignees are changed' do
- project.add_developer(user)
- visit_issue(project, issue2)
-
- find('.block.assignee .edit-link').click
- wait_for_requests
+ it 'shows author in assignee dropdown' do
+ open_assignees_dropdown
- click_on 'Unassigned'
-
- expect(page).to have_link('Apply')
+ page.within '.dropdown-menu-user' do
+ expect(page).to have_content(user2.name)
end
end
- end
-
- context 'when GraphQL assignees widget feature flag is enabled' do
- # TODO: Move to shared examples when feature flag is removed: https://gitlab.com/gitlab-org/gitlab/-/issues/328185
- context 'when a privileged user can invite' do
- it 'shows a link for inviting members and launches invite modal' do
- project.add_maintainer(user)
- visit_issue(project, issue2)
- open_assignees_dropdown
+ it 'shows author when filtering assignee dropdown' do
+ open_assignees_dropdown
- page.within '.dropdown-menu-user' do
- expect(page).to have_link('Invite members')
+ page.within '.dropdown-menu-user' do
+ find('[data-testid="user-search-input"]').set(user2.name)
- click_link 'Invite members'
- end
+ wait_for_requests
- page.within invite_modal_selector do
- expect(page).to have_content("You're inviting members to the #{project.name} project")
- end
+ expect(page).to have_content(user2.name)
end
end
- context 'when user cannot invite members in assignee dropdown' do
- it 'shows author in assignee dropdown and no invite link' do
- project.add_developer(user)
- visit_issue(project, issue2)
-
- open_assignees_dropdown
+ it 'assigns yourself' do
+ click_button 'assign yourself'
+ wait_for_requests
- page.within '.dropdown-menu-user' do
- expect(page).not_to have_link('Invite members')
- end
+ page.within '.assignee' do
+ expect(page).to have_content(user.name)
end
end
- context 'when user is a developer' do
- before do
- project.add_developer(user)
- visit_issue(project, issue2)
- end
+ it 'keeps your filtered term after filtering and dismissing the dropdown' do
+ open_assignees_dropdown
- it 'shows author in assignee dropdown' do
- open_assignees_dropdown
+ find('[data-testid="user-search-input"]').set(user2.name)
+ wait_for_requests
- page.within '.dropdown-menu-user' do
- expect(page).to have_content(user2.name)
- end
+ page.within '.dropdown-menu-user' do
+ expect(page).not_to have_content 'Unassigned'
+ click_button user2.name
end
- it 'shows author when filtering assignee dropdown' do
- open_assignees_dropdown
-
- page.within '.dropdown-menu-user' do
- find('[data-testid="user-search-input"]').set(user2.name)
-
- wait_for_requests
+ find('.participants').click
+ wait_for_requests
- expect(page).to have_content(user2.name)
- end
- end
-
- it 'assigns yourself' do
- click_button 'assign yourself'
- wait_for_requests
+ open_assignees_dropdown
- page.within '.assignee' do
- expect(page).to have_content(user.name)
- end
+ page.within('.assignee') do
+ expect(page.all('[data-testid="selected-participant"]').length).to eq(1)
end
- it 'keeps your filtered term after filtering and dismissing the dropdown' do
- open_assignees_dropdown
-
- find('[data-testid="user-search-input"]').set(user2.name)
- wait_for_requests
-
- page.within '.dropdown-menu-user' do
- expect(page).not_to have_content 'Unassigned'
- click_button user2.name
- end
-
- find('.participants').click
- wait_for_requests
-
- open_assignees_dropdown
-
- page.within('.assignee') do
- expect(page.all('[data-testid="selected-participant"]').length).to eq(1)
- end
-
- expect(find('[data-testid="user-search-input"]').value).to eq(user2.name)
- end
+ expect(find('[data-testid="user-search-input"]').value).to eq(user2.name)
end
end
end
diff --git a/spec/features/issues/issue_state_spec.rb b/spec/features/issues/issue_state_spec.rb
index 3fe49ff7080..125329764c6 100644
--- a/spec/features/issues/issue_state_spec.rb
+++ b/spec/features/issues/issue_state_spec.rb
@@ -51,7 +51,7 @@ RSpec.describe 'issue state', :js, feature_category: :team_planning do
find('#new-actions-header-dropdown > button').click
end
- it_behaves_like 'issue closed', '.dropdown-menu-right'
+ it_behaves_like 'issue closed', '.gl-new-dropdown-contents'
end
context 'when clicking the bottom `Close issue` button', :aggregate_failures do
@@ -74,7 +74,7 @@ RSpec.describe 'issue state', :js, feature_category: :team_planning do
find('#new-actions-header-dropdown > button').click
end
- it_behaves_like 'issue reopened', '.dropdown-menu-right'
+ it_behaves_like 'issue reopened', '.gl-new-dropdown-contents'
end
context 'when clicking the bottom `Reopen issue` button', :aggregate_failures do
diff --git a/spec/features/issues/move_spec.rb b/spec/features/issues/move_spec.rb
index 4a38373db71..eca52a3a2c3 100644
--- a/spec/features/issues/move_spec.rb
+++ b/spec/features/issues/move_spec.rb
@@ -44,7 +44,7 @@ RSpec.describe 'issue move to another project', feature_category: :team_planning
it 'moving issue to another project', :js do
click_button _('Move issue')
wait_for_requests
- all('.gl-dropdown-item')[0].click
+ all('.gl-new-dropdown-item')[0].click
click_button _('Move')
expect(page).to have_content("Text with #{cross_reference}#{mr.to_reference}")
@@ -116,7 +116,7 @@ RSpec.describe 'issue move to another project', feature_category: :team_planning
click_button _('Move issue')
wait_for_requests
- find('.gl-dropdown-item', text: project_title).click
+ find('.gl-new-dropdown-item', text: project_title).click
click_button _('Move')
end
diff --git a/spec/features/issues/service_desk_spec.rb b/spec/features/issues/service_desk_spec.rb
index 8e952a23f05..8662f0f98f5 100644
--- a/spec/features/issues/service_desk_spec.rb
+++ b/spec/features/issues/service_desk_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe 'Service Desk Issue Tracker', :js, feature_category: :service_desk do
let(:project) { create(:project, :private, service_desk_enabled: true) }
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
let_it_be(:support_bot) { Users::Internal.support_bot }
before do
@@ -21,8 +21,10 @@ RSpec.describe 'Service Desk Issue Tracker', :js, feature_category: :service_des
describe 'navigation to service desk' do
before do
visit project_path(project)
- find('.sidebar-top-level-items .shortcuts-issues').click
- find('.sidebar-sub-level-items a', text: 'Service Desk').click
+ find('#menu-section-button-monitor').click
+ within('#monitor') do
+ click_link('Service Desk')
+ end
end
it 'can navigate to the service desk from link in the sidebar' do
diff --git a/spec/features/issues/todo_spec.rb b/spec/features/issues/todo_spec.rb
index 2095453ac29..458e3fac517 100644
--- a/spec/features/issues/todo_spec.rb
+++ b/spec/features/issues/todo_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe 'Manually create a todo item from issue', :js, feature_category: :team_planning do
let!(:project) { create(:project) }
let!(:issue) { create(:issue, project: project) }
- let!(:user) { create(:user, :no_super_sidebar) }
+ let!(:user) { create(:user) }
before do
stub_feature_flags(notifications_todos_buttons: false)
@@ -20,13 +20,13 @@ RSpec.describe 'Manually create a todo item from issue', :js, feature_category:
expect(page).to have_content 'Mark as done'
end
- page.within ".header-content span[aria-label='#{_('Todos count')}']" do
+ within_testid 'todos-shortcut-button' do
expect(page).to have_content '1'
end
visit project_issue_path(project, issue)
- page.within ".header-content span[aria-label='#{_('Todos count')}']" do
+ within_testid 'todos-shortcut-button' do
expect(page).to have_content '1'
end
end
@@ -37,10 +37,10 @@ RSpec.describe 'Manually create a todo item from issue', :js, feature_category:
click_button 'Mark as done'
end
- expect(page).to have_selector(".header-content span[aria-label='#{_('Todos count')}']", visible: false)
+ expect(page).to have_selector("[data-testid='todos-shortcut-button']", text: '')
visit project_issue_path(project, issue)
- expect(page).to have_selector(".header-content span[aria-label='#{_('Todos count')}']", visible: false)
+ expect(page).to have_selector("[data-testid='todos-shortcut-button']", text: '')
end
end
diff --git a/spec/features/issues/user_comments_on_issue_spec.rb b/spec/features/issues/user_comments_on_issue_spec.rb
index a81a99771cc..d27f3ffebe6 100644
--- a/spec/features/issues/user_comments_on_issue_spec.rb
+++ b/spec/features/issues/user_comments_on_issue_spec.rb
@@ -33,7 +33,9 @@ RSpec.describe "User comments on issue", :js, feature_category: :team_planning d
end
end
- it_behaves_like 'edits content using the content editor'
+ # do not test quick actions here since guest users don't have permission
+ # to execute all quick actions
+ it_behaves_like 'edits content using the content editor', { with_quick_actions: false }
it "adds comment with code block" do
code_block_content = "Command [1]: /usr/local/bin/git , see [text](doc/text)"
diff --git a/spec/features/issues/user_creates_issue_spec.rb b/spec/features/issues/user_creates_issue_spec.rb
index 29b44bf165d..a407e7fd112 100644
--- a/spec/features/issues/user_creates_issue_spec.rb
+++ b/spec/features/issues/user_creates_issue_spec.rb
@@ -139,8 +139,6 @@ RSpec.describe "User creates issue", feature_category: :team_planning do
end
end
- it_behaves_like 'edits content using the content editor'
-
context 'dropzone upload file', :js do
before do
visit new_project_issue_path(project)
@@ -308,6 +306,21 @@ RSpec.describe "User creates issue", feature_category: :team_planning do
end
end
+ context 'when signed in as a maintainer', :js do
+ let_it_be(:project) { create(:project) }
+
+ before_all do
+ project.add_maintainer(user)
+ end
+
+ before do
+ sign_in(user)
+ visit(new_project_issue_path(project))
+ end
+
+ it_behaves_like 'edits content using the content editor'
+ end
+
context "when signed in as user with special characters in their name" do
let(:user_special) { create(:user, name: "Jon O'Shea") }
diff --git a/spec/features/issues/user_edits_issue_spec.rb b/spec/features/issues/user_edits_issue_spec.rb
index 7919e8f7ed4..e9bf1ef542b 100644
--- a/spec/features/issues/user_edits_issue_spec.rb
+++ b/spec/features/issues/user_edits_issue_spec.rb
@@ -210,166 +210,79 @@ RSpec.describe "Issues > User edits issue", :js, feature_category: :team_plannin
end
describe 'update assignee' do
- context 'when GraphQL assignees widget feature flag is disabled' do
- before do
- stub_feature_flags(issue_assignees_widget: false)
- end
-
- context 'by authorized user' do
- def close_dropdown_menu_if_visible
- find('.dropdown-menu-toggle', visible: :all).tap do |toggle|
- toggle.click if toggle.visible?
- end
- end
-
- it 'allows user to select unassigned' do
- visit project_issue_path(project, issue)
-
- page.within('.assignee') do
- expect(page).to have_content user.name.to_s
-
- click_link 'Edit'
- click_link 'Unassigned'
-
- close_dropdown_menu_if_visible
-
- expect(page).to have_content 'None - assign yourself'
- end
- end
-
- it 'allows user to select an assignee' do
- issue2 = create(:issue, project: project, author: user)
- visit project_issue_path(project, issue2)
-
- page.within('.assignee') do
- expect(page).to have_content "None"
- end
-
- page.within '.assignee' do
- click_link 'Edit'
- end
-
- page.within '.dropdown-menu-user' do
- click_link user.name
- end
-
- page.within('.assignee') do
- expect(page).to have_content user.name
- end
- end
-
- it 'allows user to unselect themselves' do
- issue2 = create(:issue, project: project, author: user, assignees: [user])
-
- visit project_issue_path(project, issue2)
+ context 'by authorized user' do
+ it 'allows user to select unassigned' do
+ visit project_issue_path(project, issue)
- page.within '.assignee' do
- expect(page).to have_content user.name
+ page.within('.assignee') do
+ expect(page).to have_content user.name.to_s
- click_link 'Edit'
- click_link user.name
+ click_button('Edit')
+ wait_for_requests
- close_dropdown_menu_if_visible
+ find('[data-testid="unassign"]').click
+ find('[data-testid="title"]').click
+ wait_for_requests
- page.within '[data-testid="no-value"]' do
- expect(page).to have_content "None"
- end
- end
+ expect(page).to have_content 'None - assign yourself'
end
end
- context 'by unauthorized user' do
- let(:guest) { create(:user) }
-
- before do
- project.add_guest(guest)
- end
-
- it 'shows assignee text' do
- sign_out(:user)
- sign_in(guest)
+ it 'allows user to select an assignee' do
+ issue2 = create(:issue, project: project, author: user)
+ visit project_issue_path(project, issue2)
- visit project_issue_path(project, issue)
- expect(page).to have_content issue.assignees.first.name
+ page.within('.assignee') do
+ expect(page).to have_content "None"
+ click_button('Edit')
+ wait_for_requests
end
- end
- end
-
- context 'when GraphQL assignees widget feature flag is enabled' do
- context 'by authorized user' do
- it 'allows user to select unassigned' do
- visit project_issue_path(project, issue)
-
- page.within('.assignee') do
- expect(page).to have_content user.name.to_s
-
- click_button('Edit')
- wait_for_requests
-
- find('[data-testid="unassign"]').click
- find('[data-testid="title"]').click
- wait_for_requests
- expect(page).to have_content 'None - assign yourself'
- end
+ page.within '.dropdown-menu-user' do
+ click_button user.name
end
- it 'allows user to select an assignee' do
- issue2 = create(:issue, project: project, author: user)
- visit project_issue_path(project, issue2)
-
- page.within('.assignee') do
- expect(page).to have_content "None"
- click_button('Edit')
- wait_for_requests
- end
-
- page.within '.dropdown-menu-user' do
- click_button user.name
- end
-
- page.within('.assignee') do
- find('[data-testid="title"]').click
- wait_for_requests
+ page.within('.assignee') do
+ find('[data-testid="title"]').click
+ wait_for_requests
- expect(page).to have_content user.name
- end
+ expect(page).to have_content user.name
end
+ end
- it 'allows user to unselect themselves' do
- issue2 = create(:issue, project: project, author: user, assignees: [user])
+ it 'allows user to unselect themselves' do
+ issue2 = create(:issue, project: project, author: user, assignees: [user])
- visit project_issue_path(project, issue2)
+ visit project_issue_path(project, issue2)
- page.within '.assignee' do
- expect(page).to have_content user.name
+ page.within '.assignee' do
+ expect(page).to have_content user.name
- click_button('Edit')
- wait_for_requests
- click_button user.name
+ click_button('Edit')
+ wait_for_requests
+ click_button user.name
- find('[data-testid="title"]').click
- wait_for_requests
+ find('[data-testid="title"]').click
+ wait_for_requests
- expect(page).to have_content "None"
- end
+ expect(page).to have_content "None"
end
end
+ end
- context 'by unauthorized user' do
- let(:guest) { create(:user) }
+ context 'by unauthorized user' do
+ let(:guest) { create(:user) }
- before do
- project.add_guest(guest)
- end
+ before do
+ project.add_guest(guest)
+ end
- it 'shows assignee text' do
- sign_out(:user)
- sign_in(guest)
+ it 'shows assignee text' do
+ sign_out(:user)
+ sign_in(guest)
- visit project_issue_path(project, issue)
- expect(page).to have_content issue.assignees.first.name
- end
+ visit project_issue_path(project, issue)
+ expect(page).to have_content issue.assignees.first.name
end
end
end
diff --git a/spec/features/issues/user_interacts_with_awards_spec.rb b/spec/features/issues/user_interacts_with_awards_spec.rb
index 539e429534e..813fdeea0a1 100644
--- a/spec/features/issues/user_interacts_with_awards_spec.rb
+++ b/spec/features/issues/user_interacts_with_awards_spec.rb
@@ -57,7 +57,7 @@ RSpec.describe 'User interacts with awards', feature_category: :team_planning do
end
page.within('.emoji-picker') do
- emoji_button = page.first('gl-emoji[data-name="8ball"]')
+ emoji_button = page.first('gl-emoji[data-name="grinning"]')
emoji_button.hover
emoji_button.click
end
@@ -65,7 +65,7 @@ RSpec.describe 'User interacts with awards', feature_category: :team_planning do
page.within('.awards') do
expect(page).to have_selector('[data-testid="award-button"]')
expect(page.find('[data-testid="award-button"].selected .js-counter')).to have_content('1')
- expect(page).to have_css('[data-testid="award-button"].selected[title="You reacted with :8ball:"]')
+ expect(page).to have_css('[data-testid="award-button"].selected[title="You reacted with :grinning:"]')
wait_for_requests
@@ -114,17 +114,17 @@ RSpec.describe 'User interacts with awards', feature_category: :team_planning do
context 'User interacts with awards on a note' do
let!(:note) { create(:note, noteable: issue, project: issue.project) }
- let!(:award_emoji) { create(:award_emoji, awardable: note, name: '100') }
+ let!(:award_emoji) { create(:award_emoji, awardable: note, name: 'grinning') }
it 'shows the award on the note' do
page.within('.note-awards') do
- expect(page).to have_emoji('100')
+ expect(page).to have_emoji('grinning')
end
end
it 'allows adding a vote to an award' do
page.within('.note-awards') do
- find('gl-emoji[data-name="100"]').click
+ find('gl-emoji[data-name="grinning"]').click
end
wait_for_requests
@@ -140,11 +140,11 @@ RSpec.describe 'User interacts with awards', feature_category: :team_planning do
# make sure emoji popup is visible
execute_script("window.scrollBy(0, 200)")
- find('gl-emoji[data-name="8ball"]').click
+ find('gl-emoji[data-name="laughing"]').click
wait_for_requests
page.within('.note-awards') do
- expect(page).to have_emoji('8ball')
+ expect(page).to have_emoji('laughing')
end
expect(note.reload.award_emoji.size).to eq(2)
restore_window_size
@@ -165,7 +165,7 @@ RSpec.describe 'User interacts with awards', feature_category: :team_planning do
it 'does not allow toggling existing emoji' do
page.within('.note-awards') do
- find('gl-emoji[data-name="100"]').click
+ find('gl-emoji[data-name="grinning"]').click
end
wait_for_requests
diff --git a/spec/features/issues/user_uses_quick_actions_spec.rb b/spec/features/issues/user_uses_quick_actions_spec.rb
index d3552b87fea..937a0683794 100644
--- a/spec/features/issues/user_uses_quick_actions_spec.rb
+++ b/spec/features/issues/user_uses_quick_actions_spec.rb
@@ -12,8 +12,8 @@ RSpec.describe 'Issues > User uses quick actions', :js, feature_category: :team_
context "issuable common quick actions" do
let(:new_url_opts) { {} }
- let(:maintainer) { create(:user, :no_super_sidebar) }
- let(:project) { create(:project, :public) }
+ let(:maintainer) { create(:user) }
+ let_it_be(:project) { create(:project, :public) }
let!(:label_bug) { create(:label, project: project, title: 'bug') }
let!(:label_feature) { create(:label, project: project, title: 'feature') }
let!(:milestone) { create(:milestone, project: project, title: 'ASAP') }
@@ -25,7 +25,7 @@ RSpec.describe 'Issues > User uses quick actions', :js, feature_category: :team_
end
describe 'issue-only commands' do
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
let(:issue) { create(:issue, project: project, due_date: Date.new(2016, 8, 28)) }
diff --git a/spec/features/jira_connect/branches_spec.rb b/spec/features/jira_connect/branches_spec.rb
index ae1dd551c47..25dc14a1dc9 100644
--- a/spec/features/jira_connect/branches_spec.rb
+++ b/spec/features/jira_connect/branches_spec.rb
@@ -5,8 +5,8 @@ require 'spec_helper'
RSpec.describe 'Create GitLab branches from Jira', :js, feature_category: :integrations do
include ListboxHelpers
- let_it_be(:alice) { create(:user, :no_super_sidebar, name: 'Alice') }
- let_it_be(:bob) { create(:user, :no_super_sidebar, name: 'Bob') }
+ let_it_be(:alice) { create(:user, name: 'Alice') }
+ let_it_be(:bob) { create(:user, name: 'Bob') }
let_it_be(:project1) { create(:project, :repository, namespace: alice.namespace, title: 'foo') }
let_it_be(:project2) { create(:project, :repository, namespace: alice.namespace, title: 'bar') }
diff --git a/spec/features/jira_oauth_provider_authorize_spec.rb b/spec/features/jira_oauth_provider_authorize_spec.rb
deleted file mode 100644
index e873d9c219f..00000000000
--- a/spec/features/jira_oauth_provider_authorize_spec.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'JIRA OAuth Provider', feature_category: :integrations do
- describe 'JIRA DVCS OAuth Authorization' do
- let_it_be(:application) do
- create(:oauth_application, redirect_uri: oauth_jira_dvcs_callback_url, scopes: 'read_user')
- end
-
- let(:authorize_path) do
- oauth_jira_dvcs_authorize_path(client_id: application.uid,
- redirect_uri: oauth_jira_dvcs_callback_url,
- response_type: 'code',
- state: 'my_state',
- scope: 'read_user')
- end
-
- before do
- sign_in(user)
- end
-
- it_behaves_like 'Secure OAuth Authorizations' do
- before do
- visit authorize_path
- end
- end
-
- context 'when the flag is disabled' do
- let_it_be(:user) { create(:user) }
-
- before do
- stub_feature_flags(jira_dvcs_end_of_life_amnesty: false)
- visit authorize_path
- end
-
- it 'presents as an endpoint that does not exist' do
- expect(page).to have_gitlab_http_status(:not_found)
- end
- end
- end
-end
diff --git a/spec/features/labels_hierarchy_spec.rb b/spec/features/labels_hierarchy_spec.rb
index 72f5b46c3ad..c6c7342325b 100644
--- a/spec/features/labels_hierarchy_spec.rb
+++ b/spec/features/labels_hierarchy_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe 'Labels Hierarchy', :js, feature_category: :team_planning do
include FilteredSearchHelpers
- let!(:user) { create(:user, :no_super_sidebar) }
+ let!(:user) { create(:user) }
let!(:grandparent) { create(:group) }
let!(:parent) { create(:group, parent: grandparent) }
let!(:child) { create(:group, parent: parent) }
@@ -179,7 +179,7 @@ RSpec.describe 'Labels Hierarchy', :js, feature_category: :team_planning do
wait_for_requests
end
- find('.btn-confirm').click
+ click_button 'Create issue'
expect(page.find('.issue-details h1.title')).to have_content('new created issue')
expect(page).to have_selector('span.gl-label-text', text: grandparent_group_label.title)
diff --git a/spec/features/markdown/keyboard_shortcuts_spec.rb b/spec/features/markdown/keyboard_shortcuts_spec.rb
index 6f128e16041..ba88278199a 100644
--- a/spec/features/markdown/keyboard_shortcuts_spec.rb
+++ b/spec/features/markdown/keyboard_shortcuts_spec.rb
@@ -97,8 +97,8 @@ RSpec.describe 'Markdown keyboard shortcuts', :js, feature_category: :team_plann
context 'Vue.js markdown editor' do
let(:path_to_visit) { new_project_release_path(project) }
- let(:markdown_field) { find_field('Release notes') }
- let(:non_markdown_field) { find_field('Release title') }
+ let(:markdown_field) { find_field('release-notes') }
+ let(:non_markdown_field) { find_field('release-title') }
it_behaves_like 'keyboard shortcuts'
it_behaves_like 'no side effects'
diff --git a/spec/features/merge_request/maintainer_edits_fork_spec.rb b/spec/features/merge_request/maintainer_edits_fork_spec.rb
index 7603696c60c..8618dca5873 100644
--- a/spec/features/merge_request/maintainer_edits_fork_spec.rb
+++ b/spec/features/merge_request/maintainer_edits_fork_spec.rb
@@ -50,7 +50,10 @@ RSpec.describe 'a maintainer edits files on a source-branch of an MR from a fork
click_button 'Commit changes'
wait_for_requests
- expect(page).to have_content('Your changes have been successfully committed')
+ expect(page).to have_content('Your changes have been committed successfully')
+ page.within '.flash-container' do
+ expect(page).to have_link 'changes'
+ end
expect(page).to have_content(content)
end
end
diff --git a/spec/features/merge_request/merge_request_discussion_lock_spec.rb b/spec/features/merge_request/merge_request_discussion_lock_spec.rb
index 782c4af58ac..7e01063816f 100644
--- a/spec/features/merge_request/merge_request_discussion_lock_spec.rb
+++ b/spec/features/merge_request/merge_request_discussion_lock_spec.rb
@@ -92,7 +92,7 @@ RSpec.describe 'Merge Request Discussion Lock', :js, feature_category: :code_rev
it 'the user can lock the merge_request' do
find('#new-actions-header-dropdown button').click
- expect(page).to have_content('Lock merge request')
+ expect(page).to have_content('Lock discussion')
end
end
@@ -105,7 +105,7 @@ RSpec.describe 'Merge Request Discussion Lock', :js, feature_category: :code_rev
it 'the user can unlock the merge_request' do
find('#new-actions-header-dropdown button').click
- expect(page).to have_content('Unlock merge request')
+ expect(page).to have_content('Unlock discussion')
end
end
end
diff --git a/spec/features/merge_request/user_awards_emoji_spec.rb b/spec/features/merge_request/user_awards_emoji_spec.rb
index f43672942ff..63e1cffed23 100644
--- a/spec/features/merge_request/user_awards_emoji_spec.rb
+++ b/spec/features/merge_request/user_awards_emoji_spec.rb
@@ -50,7 +50,7 @@ RSpec.describe 'Merge request > User awards emoji', :js, feature_category: :code
# make sure emoji popup is visible
execute_script("window.scrollBy(0, 200)")
- find('gl-emoji[data-name="8ball"]').click
+ find('gl-emoji[data-name="grinning"]').click
end
wait_for_requests
diff --git a/spec/features/merge_request/user_edits_assignees_sidebar_spec.rb b/spec/features/merge_request/user_edits_assignees_sidebar_spec.rb
index 2fcbb4e70c3..ae95bc3e11f 100644
--- a/spec/features/merge_request/user_edits_assignees_sidebar_spec.rb
+++ b/spec/features/merge_request/user_edits_assignees_sidebar_spec.rb
@@ -34,202 +34,81 @@ RSpec.describe 'Merge request > User edits assignees sidebar', :js, feature_cate
let(:sidebar_assignee_tooltip) { sidebar_assignee_avatar_link['title'] || '' }
let(:sidebar_assignee_merge_ability) { sidebar_assignee_avatar_link['data-cannot-merge'] || '' }
- context 'when GraphQL assignees widget feature flag is disabled' do
- let(:sidebar_assignee_dropdown_item) do
- sidebar_assignee_block.find(".dropdown-menu li[data-user-id=\"#{assignee.id}\"]")
- end
-
- let(:sidebar_assignee_dropdown_tooltip) { sidebar_assignee_dropdown_item.find('a')['data-title'] || '' }
+ let(:sidebar_assignee_dropdown_item) { sidebar_assignee_block.find(".dropdown-item", text: assignee.username) }
+ let(:sidebar_assignee_dropdown_tooltip) { sidebar_assignee_dropdown_item['title'] }
+ context 'when user is an owner' do
before do
- stub_feature_flags(issue_assignees_widget: false)
- end
+ stub_const('Autocomplete::UsersFinder::LIMIT', users_find_limit)
- context 'when user is an owner' do
- before do
- stub_const('Autocomplete::UsersFinder::LIMIT', users_find_limit)
+ sign_in(owner)
- sign_in(owner)
+ merge_request.assignees << assignee
- merge_request.assignees << assignee
+ visit project_merge_request_path(project, merge_request)
- visit project_merge_request_path(project, merge_request)
+ wait_for_requests
+ end
- wait_for_requests
+ shared_examples 'when assigned' do |expected_tooltip: '', expected_cannot_merge: ''|
+ it 'shows assignee name' do
+ expect(sidebar_assignee_block).to have_text(assignee.name)
end
- shared_examples 'when assigned' do |expected_tooltip: '', expected_cannot_merge: ''|
- it 'shows assignee name' do
- expect(sidebar_assignee_block).to have_text(assignee.name)
- end
+ it "sets data-cannot-merge to '#{expected_cannot_merge}'" do
+ expect(sidebar_assignee_merge_ability).to eql(expected_cannot_merge)
+ end
- it "sets data-cannot-merge to '#{expected_cannot_merge}'" do
- expect(sidebar_assignee_merge_ability).to eql(expected_cannot_merge)
+ context 'when edit is clicked' do
+ before do
+ open_assignees_dropdown
end
- context 'when edit is clicked' do
- before do
- sidebar_assignee_block.click_link('Edit')
-
- wait_for_requests
- end
-
- it "shows assignee tooltip '#{expected_tooltip}" do
- expect(sidebar_assignee_dropdown_tooltip).to eql(expected_tooltip)
- end
+ it "shows assignee tooltip '#{expected_tooltip}" do
+ expect(sidebar_assignee_dropdown_tooltip).to eql(expected_tooltip)
end
end
-
- context 'when assigned to maintainer' do
- let(:assignee) { project_maintainers.last }
-
- it_behaves_like 'when assigned', expected_tooltip: ''
- end
-
- context 'when assigned to developer' do
- let(:assignee) { project_developers.last }
-
- it_behaves_like 'when assigned', expected_tooltip: 'Cannot merge', expected_cannot_merge: 'true'
- end
end
- context 'with members shared into ancestors of the project' do
- before do
- sign_in(owner)
-
- visit project_merge_request_path(project, merge_request)
- wait_for_requests
+ context 'when assigned to maintainer' do
+ let(:assignee) { project_maintainers.last }
- sidebar_assignee_block.click_link('Edit')
- wait_for_requests
- end
-
- it 'contains the members shared into ancestors of the projects' do
- page.within '.dropdown-menu-user' do
- expect(page).to have_content shared_into_ancestor_user.name
- end
- end
+ it_behaves_like 'when assigned', expected_tooltip: ''
end
- context 'with invite members considerations' do
- let_it_be(:user) { create(:user) }
+ context 'when assigned to developer' do
+ let(:assignee) { project_developers.last }
- before do
- sign_in(user)
- end
-
- include_examples 'issuable invite members' do
- let(:issuable_path) { project_merge_request_path(project, merge_request) }
- end
+ it_behaves_like 'when assigned', expected_tooltip: 'Cannot merge', expected_cannot_merge: 'true'
end
end
- context 'when GraphQL assignees widget feature flag is enabled' do
- let(:sidebar_assignee_dropdown_item) { sidebar_assignee_block.find(".dropdown-item", text: assignee.username) }
- let(:sidebar_assignee_dropdown_tooltip) { sidebar_assignee_dropdown_item['title'] }
-
- context 'when user is an owner' do
- before do
- stub_const('Autocomplete::UsersFinder::LIMIT', users_find_limit)
-
- sign_in(owner)
-
- merge_request.assignees << assignee
-
- visit project_merge_request_path(project, merge_request)
-
- wait_for_requests
- end
-
- shared_examples 'when assigned' do |expected_tooltip: '', expected_cannot_merge: ''|
- it 'shows assignee name' do
- expect(sidebar_assignee_block).to have_text(assignee.name)
- end
-
- it "sets data-cannot-merge to '#{expected_cannot_merge}'" do
- expect(sidebar_assignee_merge_ability).to eql(expected_cannot_merge)
- end
-
- context 'when edit is clicked' do
- before do
- open_assignees_dropdown
- end
-
- it "shows assignee tooltip '#{expected_tooltip}" do
- expect(sidebar_assignee_dropdown_tooltip).to eql(expected_tooltip)
- end
- end
- end
-
- context 'when assigned to maintainer' do
- let(:assignee) { project_maintainers.last }
-
- it_behaves_like 'when assigned', expected_tooltip: ''
- end
+ context 'with members shared into ancestors of the project' do
+ before do
+ sign_in(owner)
- context 'when assigned to developer' do
- let(:assignee) { project_developers.last }
+ visit project_merge_request_path(project, merge_request)
+ wait_for_requests
- it_behaves_like 'when assigned', expected_tooltip: 'Cannot merge', expected_cannot_merge: 'true'
- end
+ open_assignees_dropdown
end
- context 'with members shared into ancestors of the project' do
- before do
- sign_in(owner)
-
- visit project_merge_request_path(project, merge_request)
- wait_for_requests
-
- open_assignees_dropdown
- end
-
- it 'contains the members shared into ancestors of the projects' do
- page.within '.dropdown-menu-user' do
- expect(page).to have_content shared_into_ancestor_user.name
- end
+ it 'contains the members shared into ancestors of the projects' do
+ page.within '.dropdown-menu-user' do
+ expect(page).to have_content shared_into_ancestor_user.name
end
end
+ end
- context 'with invite members considerations' do
- let_it_be(:user) { create(:user) }
-
- before do
- sign_in(user)
- end
-
- # TODO: Move to shared examples when feature flag is removed: https://gitlab.com/gitlab-org/gitlab/-/issues/328185
- context 'when a privileged user can invite' do
- it 'shows a link for inviting members and launches invite modal' do
- project.add_maintainer(user)
- visit project_merge_request_path(project, merge_request)
-
- open_assignees_dropdown
-
- page.within '.dropdown-menu-user' do
- expect(page).to have_link('Invite members')
-
- click_link 'Invite members'
- end
-
- page.within invite_modal_selector do
- expect(page).to have_content("You're inviting members to the #{project.name} project")
- end
- end
- end
-
- context 'when user cannot invite members in assignee dropdown' do
- it 'shows author in assignee dropdown and no invite link' do
- project.add_developer(user)
- visit project_merge_request_path(project, merge_request)
+ context 'with invite members considerations' do
+ let_it_be(:user) { create(:user) }
- open_assignees_dropdown
+ before do
+ sign_in(user)
+ end
- page.within '.dropdown-menu-user' do
- expect(page).not_to have_link('Invite members')
- end
- end
- end
+ include_examples 'issuable invite members' do
+ let(:issuable_path) { project_merge_request_path(project, merge_request) }
end
end
diff --git a/spec/features/merge_request/user_locks_discussion_spec.rb b/spec/features/merge_request/user_locks_discussion_spec.rb
index a603a5c1e0b..d4cc6c9410c 100644
--- a/spec/features/merge_request/user_locks_discussion_spec.rb
+++ b/spec/features/merge_request/user_locks_discussion_spec.rb
@@ -43,7 +43,7 @@ RSpec.describe 'Merge request > User locks discussion', :js, feature_category: :
page.within('.js-vue-notes-event') do
expect(page).not_to have_selector('js-main-target-form')
expect(page.find('.issuable-note-warning'))
- .to have_content('This merge request is locked. Only project members can comment.')
+ .to have_content('The discussion in this merge request is locked. Only project members can comment.')
end
end
end
diff --git a/spec/features/merge_request/user_merges_immediately_spec.rb b/spec/features/merge_request/user_merges_immediately_spec.rb
index 71af2045bab..ae229651579 100644
--- a/spec/features/merge_request/user_merges_immediately_spec.rb
+++ b/spec/features/merge_request/user_merges_immediately_spec.rb
@@ -37,7 +37,7 @@ RSpec.describe 'Merge requests > User merges immediately', :js, feature_category
wait_for_requests
page.within '[data-testid="ready_to_merge_state"]' do
- find('.dropdown-toggle').click
+ find('.gl-new-dropdown-toggle').click
Sidekiq::Testing.fake! do
click_button 'Merge immediately'
diff --git a/spec/features/merge_request/user_merges_merge_request_spec.rb b/spec/features/merge_request/user_merges_merge_request_spec.rb
index ede686cc700..111204a7105 100644
--- a/spec/features/merge_request/user_merges_merge_request_spec.rb
+++ b/spec/features/merge_request/user_merges_merge_request_spec.rb
@@ -3,7 +3,7 @@
require "spec_helper"
RSpec.describe "User merges a merge request", :js, feature_category: :code_review_workflow do
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
before do
sign_in(user)
@@ -21,7 +21,8 @@ RSpec.describe "User merges a merge request", :js, feature_category: :code_revie
end
end
- context 'sidebar merge requests counter' do
+ # Pending re-implementation: https://gitlab.com/gitlab-org/gitlab/-/issues/429268
+ xcontext 'sidebar merge requests counter' do
let_it_be(:project) { create(:project, :public, :repository, namespace: user.namespace) }
let!(:merge_request) { create(:merge_request, source_project: project) }
diff --git a/spec/features/merge_request/user_opens_checkout_branch_modal_spec.rb b/spec/features/merge_request/user_opens_checkout_branch_modal_spec.rb
index 230111fe439..111e8574bac 100644
--- a/spec/features/merge_request/user_opens_checkout_branch_modal_spec.rb
+++ b/spec/features/merge_request/user_opens_checkout_branch_modal_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe 'Merge request > User opens checkout branch modal', :js, feature_category: :code_review_workflow do
include ProjectForksHelper
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public, :repository, namespace: user.namespace) }
before do
@@ -13,7 +13,7 @@ RSpec.describe 'Merge request > User opens checkout branch modal', :js, feature_
end
describe 'for fork' do
- let(:author) { create(:user, :no_super_sidebar) }
+ let(:author) { create(:user) }
let(:source_project) { fork_project(project, author, repository: true) }
let(:merge_request) do
@@ -31,8 +31,10 @@ RSpec.describe 'Merge request > User opens checkout branch modal', :js, feature_
it 'shows instructions' do
visit project_merge_request_path(project, merge_request)
- click_button 'Code'
- click_button 'Check out branch'
+ page.within 'main' do
+ click_button 'Code'
+ click_button 'Check out branch'
+ end
expect(page).to have_content(source_project.http_url_to_repo)
end
diff --git a/spec/features/merge_request/user_reverts_merge_request_spec.rb b/spec/features/merge_request/user_reverts_merge_request_spec.rb
index 8c782056aa4..c2f82039f0b 100644
--- a/spec/features/merge_request/user_reverts_merge_request_spec.rb
+++ b/spec/features/merge_request/user_reverts_merge_request_spec.rb
@@ -3,23 +3,28 @@
require 'spec_helper'
RSpec.describe 'User reverts a merge request', :js, feature_category: :code_review_workflow do
+ include Spec::Support::Helpers::ModalHelpers
+
let(:merge_request) { create(:merge_request, :simple, source_project: project) }
let(:project) { create(:project, :public, :repository) }
let(:user) { create(:user) }
before do
- stub_feature_flags(unbatch_graphql_queries: false)
project.add_developer(user)
sign_in(user)
+ set_cookie('new-actions-popover-viewed', 'true')
visit(merge_request_path(merge_request))
page.within('.mr-state-widget') do
click_button 'Merge'
end
- wait_for_requests
+ wait_for_all_requests
+ page.refresh
+
+ wait_for_requests
# do not reload the page by visiting, let javascript update the page as it will validate we have loaded the modal
# code correctly on page update that adds the `revert` button
end
@@ -55,11 +60,11 @@ RSpec.describe 'User reverts a merge request', :js, feature_category: :code_revi
end
def revert_commit(create_merge_request: false)
- click_button('Revert')
+ click_button 'Revert'
- page.within('[data-testid="modal-commit"]') do
+ within_modal do
uncheck('create_merge_request') unless create_merge_request
- click_button('Revert')
+ click_button 'Revert'
end
end
end
diff --git a/spec/features/merge_request/user_sees_check_out_branch_modal_spec.rb b/spec/features/merge_request/user_sees_check_out_branch_modal_spec.rb
index 921c12134a9..da290f59736 100644
--- a/spec/features/merge_request/user_sees_check_out_branch_modal_spec.rb
+++ b/spec/features/merge_request/user_sees_check_out_branch_modal_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'Merge request > User sees check out branch modal', :js, feature_category: :code_review_workflow do
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public, :repository, creator: user) }
let_it_be(:merge_request) { create(:merge_request, source_project: project) }
let(:modal_window_title) { 'Check out, review, and resolve locally' }
@@ -13,8 +13,10 @@ RSpec.describe 'Merge request > User sees check out branch modal', :js, feature_
visit project_merge_request_path(project, merge_request)
wait_for_requests
- click_button 'Code'
- click_button('Check out branch')
+ page.within 'main' do
+ click_button 'Code'
+ click_button('Check out branch')
+ end
end
it 'shows the check out branch modal' do
diff --git a/spec/features/merge_request/user_sees_merge_request_file_tree_sidebar_spec.rb b/spec/features/merge_request/user_sees_merge_request_file_tree_sidebar_spec.rb
index c385def6762..8caa13c6297 100644
--- a/spec/features/merge_request/user_sees_merge_request_file_tree_sidebar_spec.rb
+++ b/spec/features/merge_request/user_sees_merge_request_file_tree_sidebar_spec.rb
@@ -15,48 +15,56 @@ RSpec.describe 'Merge request > User sees merge request file tree sidebar', :js,
sign_in(user)
visit diffs_project_merge_request_path(project, merge_request)
wait_for_requests
- scroll_into_view
end
it 'sees file tree sidebar' do
expect(page).to have_selector('.file-row[role=button]')
end
- # TODO: fix this test
- # For some reason the browser in CI doesn't update the file tree sidebar when review bar is shown
- # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/118378#note_1403906356
- #
- # it 'has last entry visible with discussions enabled' do
- # add_diff_line_draft_comment('foo', find('.line_holder', match: :first))
- # scroll_into_view
- # scroll_to_end
- # button = find_all('.file-row[role=button]').last
- # expect(button.obscured?).to be_falsy
- # end
-
- shared_examples 'shows last visible file in sidebar' do
- it 'shows last file' do
- scroll_to_end
+ shared_examples 'last entry clickable' do
+ specify do
+ sidebar_scroller.execute_script('this.scrollBy(0,99999)')
button = find_all('.file-row[role=button]').last
title = button.find('[data-testid=file-row-name-container]')[:title]
+ expect(button.obscured?).to be_falsy
button.click
expect(page).to have_selector(".file-title-name[title*=\"#{title}\"]")
end
end
- it_behaves_like 'shows last visible file in sidebar'
+ it_behaves_like 'last entry clickable'
+
+ context 'when has started a review' do
+ before do
+ add_diff_line_draft_comment('foo', find('.line_holder', match: :first))
+ # wait for review bar to appear
+ find_by_testid('review_bar_component')
+ # wait for sidebar to adjust
+ sleep(1)
+ end
+
+ it_behaves_like 'last entry clickable'
+
+ context 'when scrolled into full view' do
+ before do
+ sidebar.execute_script("this.scrollIntoView({ block: 'end' })")
+ end
+
+ it_behaves_like 'last entry clickable'
+ end
+ end
context 'when viewing using file-by-file mode' do
let(:user) { create(:user, view_diffs_file_by_file: true) }
- it_behaves_like 'shows last visible file in sidebar'
- end
+ it_behaves_like 'last entry clickable'
- def scroll_into_view
- sidebar.execute_script("this.scrollIntoView({ block: 'end' })")
- end
+ context 'when navigating to the next file' do
+ before do
+ click_link 'Next'
+ end
- def scroll_to_end
- sidebar_scroller.execute_script('this.scrollBy(0,99999)')
+ it_behaves_like 'last entry clickable'
+ end
end
end
diff --git a/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb b/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb
index 69eb6b0dc17..5e683ddf7ba 100644
--- a/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb
+++ b/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb
@@ -86,7 +86,7 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
it 'sees branch pipelines and detached merge request pipelines in correct order' do
page.within('.ci-table') do
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Created', count: 2)
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Created', count: 2)
expect(first('[data-testid="pipeline-url-link"]')).to have_content("##{detached_merge_request_pipeline.id}")
end
end
@@ -122,7 +122,7 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
it 'sees branch pipelines and detached merge request pipelines in correct order' do
page.within('.ci-table') do
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Pending', count: 4)
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Pending', count: 4)
expect(all('[data-testid="pipeline-url-link"]')[0])
.to have_content("##{detached_merge_request_pipeline_2.id}")
@@ -220,7 +220,7 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
it 'sees a branch pipeline in pipeline tab' do
page.within('.ci-table') do
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Created', count: 1)
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Created', count: 1)
expect(first('[data-testid="pipeline-url-link"]')).to have_content("##{push_pipeline.id}")
end
end
@@ -273,7 +273,7 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
it 'sees branch pipelines and detached merge request pipelines in correct order' do
page.within('.ci-table') do
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Pending', count: 2)
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Pending', count: 2)
expect(first('[data-testid="pipeline-url-link"]')).to have_content("##{detached_merge_request_pipeline.id}")
end
end
@@ -289,7 +289,7 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
it 'sees pipeline list in forked project' do
visit project_pipelines_path(forked_project)
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Pending', count: 2)
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Pending', count: 2)
end
context 'when a user updated a merge request from a forked project to the parent project' do
@@ -315,7 +315,7 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
it 'sees branch pipelines and detached merge request pipelines in correct order' do
page.within('.ci-table') do
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Pending', count: 4)
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Pending', count: 4)
expect(all('[data-testid="pipeline-url-link"]')[0])
.to have_content("##{detached_merge_request_pipeline_2.id}")
@@ -358,7 +358,7 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
it 'sees pipeline list in forked project' do
visit project_pipelines_path(forked_project)
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Pending', count: 4)
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Pending', count: 4)
end
end
diff --git a/spec/features/merge_request/user_sees_merge_widget_spec.rb b/spec/features/merge_request/user_sees_merge_widget_spec.rb
index 96cad397441..c18b2c97f96 100644
--- a/spec/features/merge_request/user_sees_merge_widget_spec.rb
+++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb
@@ -53,7 +53,6 @@ RSpec.describe 'Merge request > User sees merge widget', :js, feature_category:
let!(:deployment) { build.deployment }
before do
- stub_feature_flags(unbatch_graphql_queries: false)
merge_request.update!(head_pipeline: pipeline)
deployment.update!(status: :success)
visit project_merge_request_path(project, merge_request)
@@ -84,6 +83,8 @@ RSpec.describe 'Merge request > User sees merge widget', :js, feature_category:
wait_for_requests
+ page.refresh
+
click_button 'Cherry-pick'
page.within(modal_selector) do
@@ -175,7 +176,7 @@ RSpec.describe 'Merge request > User sees merge widget', :js, feature_category:
expect(page).to have_content("Merge blocked")
expect(page).to have_content(
"pipeline must succeed. It's waiting for a manual action to continue.")
- expect(page).to have_css('.ci-status-icon-manual')
+ expect(page).to have_css('[data-testid="status_manual_borderless-icon"]')
end
end
diff --git a/spec/features/merge_request/user_sees_pipelines_spec.rb b/spec/features/merge_request/user_sees_pipelines_spec.rb
index a68b3c444fe..a06d1808b6b 100644
--- a/spec/features/merge_request/user_sees_pipelines_spec.rb
+++ b/spec/features/merge_request/user_sees_pipelines_spec.rb
@@ -49,9 +49,8 @@ RSpec.describe 'Merge request > User sees pipelines', :js, feature_category: :co
wait_for_requests
page.within(find('[data-testid="pipeline-table-row"]', match: :first)) do
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Passed')
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Passed')
expect(page).to have_content(pipeline.id)
- expect(page).to have_content('API')
expect(page).to have_css('[data-testid="pipeline-mini-graph"]')
expect(page).to have_css('[data-testid="pipelines-manual-actions-dropdown"]')
expect(page).to have_css('[data-testid="pipeline-multi-actions-dropdown"]')
diff --git a/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb b/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb
index 654c71c87e0..daa84227adc 100644
--- a/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb
+++ b/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe 'Merge request > User selects branches for new MR', :js, feature_category: :code_review_workflow do
include ListboxHelpers
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public, :repository, namespace: user.namespace) }
def select_source_branch(branch_name)
@@ -64,8 +64,10 @@ RSpec.describe 'Merge request > User selects branches for new MR', :js, feature_
fill_in "merge_request_title", with: "Orphaned MR test"
click_button "Create merge request"
- click_button 'Code'
- click_button "Check out branch"
+ page.within 'main' do
+ click_button 'Code'
+ click_button "Check out branch"
+ end
expect(page).to have_content 'git checkout -b \'orphaned-branch\' \'origin/orphaned-branch\''
end
diff --git a/spec/features/merge_request/user_uses_quick_actions_spec.rb b/spec/features/merge_request/user_uses_quick_actions_spec.rb
index b2cc25f1c34..a89f533c9dd 100644
--- a/spec/features/merge_request/user_uses_quick_actions_spec.rb
+++ b/spec/features/merge_request/user_uses_quick_actions_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe 'Merge request > User uses quick actions', :js, :use_clean_rails_
context "issuable common quick actions" do
let!(:new_url_opts) { { merge_request: { source_branch: 'feature', target_branch: 'master' } } }
- let(:maintainer) { create(:user, :no_super_sidebar) }
+ let(:maintainer) { create(:user) }
let(:project) { create(:project, :public, :repository) }
let!(:label_bug) { create(:label, project: project, title: 'bug') }
let!(:label_feature) { create(:label, project: project, title: 'feature') }
@@ -26,8 +26,8 @@ RSpec.describe 'Merge request > User uses quick actions', :js, :use_clean_rails_
end
describe 'merge-request-only commands' do
- let(:user) { create(:user, :no_super_sidebar) }
- let(:guest) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
+ let(:guest) { create(:user) }
let(:project) { create(:project, :public, :repository) }
let(:merge_request) { create(:merge_request, source_project: project) }
let!(:milestone) { create(:milestone, project: project, title: 'ASAP') }
diff --git a/spec/features/merge_requests/user_filters_by_source_branch_spec.rb b/spec/features/merge_requests/user_filters_by_source_branch_spec.rb
new file mode 100644
index 00000000000..7eade94de2a
--- /dev/null
+++ b/spec/features/merge_requests/user_filters_by_source_branch_spec.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Merge Requests > User filters by source branch', :js, feature_category: :code_review_workflow do
+ include FilteredSearchHelpers
+
+ def create_mr(source_branch, target_branch, status)
+ create(:merge_request, status, source_project: project,
+ target_branch: target_branch, source_branch: source_branch)
+ end
+
+ let_it_be(:project) { create(:project, :public, :repository) }
+ let_it_be(:user) { project.creator }
+
+ let_it_be(:mr1) { create_mr('source1', 'target1', :opened) }
+ let_it_be(:mr2) { create_mr('source2', 'target1', :opened) }
+ let_it_be(:mr3) { create_mr('source1', 'target2', :merged) }
+ let_it_be(:mr4) { create_mr('source1', 'target2', :closed) }
+
+ before do
+ sign_in(user)
+ visit project_merge_requests_path(project)
+ end
+
+ context 'when filtering by source-branch:source1' do
+ it 'applies the filter' do
+ input_filtered_search('source-branch:=source1')
+
+ expect(page).to have_issuable_counts(open: 1, merged: 1, closed: 1, all: 3)
+ expect(page).to have_content mr1.title
+ expect(page).not_to have_content mr2.title
+ end
+ end
+
+ context 'when filtering by source-branch:source2' do
+ it 'applies the filter' do
+ input_filtered_search('source-branch:=source2')
+
+ expect(page).to have_issuable_counts(open: 1, merged: 0, closed: 0, all: 1)
+ expect(page).not_to have_content mr1.title
+ expect(page).to have_content mr2.title
+ end
+ end
+
+ context 'when filtering by source-branch:non-exists-branch' do
+ it 'applies the filter' do
+ input_filtered_search('source-branch:=non-exists-branch')
+
+ expect(page).to have_issuable_counts(open: 0, merged: 0, closed: 0, all: 0)
+ expect(page).not_to have_content mr1.title
+ expect(page).not_to have_content mr2.title
+ end
+ end
+
+ context 'when filtering by source-branch:!=source1' do
+ it 'applies the filter' do
+ input_filtered_search('source-branch:!=source1')
+
+ expect(page).to have_issuable_counts(open: 1, merged: 0, closed: 0, all: 1)
+ expect(page).not_to have_content mr1.title
+ expect(page).to have_content mr2.title
+ end
+ end
+end
diff --git a/spec/features/monitor_sidebar_link_spec.rb b/spec/features/monitor_sidebar_link_spec.rb
index 1d39f749ca7..1855379825b 100644
--- a/spec/features/monitor_sidebar_link_spec.rb
+++ b/spec/features/monitor_sidebar_link_spec.rb
@@ -2,9 +2,9 @@
require 'spec_helper'
-RSpec.describe 'Monitor dropdown sidebar', :aggregate_failures, feature_category: :shared do
+RSpec.describe 'Monitor dropdown sidebar', :js, feature_category: :shared do
let_it_be_with_reload(:project) { create(:project, :internal, :repository) }
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
let(:role) { nil }
@@ -13,7 +13,7 @@ RSpec.describe 'Monitor dropdown sidebar', :aggregate_failures, feature_category
sign_in(user)
end
- shared_examples 'shows Monitor menu based on the access level' do
+ shared_examples 'shows common Monitor menu item based on the access level' do
using RSpec::Parameterized::TableSyntax
let(:enabled) { Featurable::PRIVATE }
@@ -30,10 +30,14 @@ RSpec.describe 'Monitor dropdown sidebar', :aggregate_failures, feature_category
visit project_issues_path(project)
- if render
- expect(page).to have_selector('a.shortcuts-monitor', text: 'Monitor')
- else
- expect(page).not_to have_selector('a.shortcuts-monitor')
+ click_button('Monitor')
+
+ within_testid('super-sidebar') do
+ if render
+ expect(page).to have_link('Incidents')
+ else
+ expect(page).not_to have_link('Incidents', visible: :all)
+ end
end
end
end
@@ -44,32 +48,35 @@ RSpec.describe 'Monitor dropdown sidebar', :aggregate_failures, feature_category
before do
project.project_feature.update_attribute(:monitor_access_level, access_level)
+ visit project_issues_path(project)
+ click_button('Monitor')
end
- it 'has the correct `Monitor` menu items', :aggregate_failures do
- visit project_issues_path(project)
- expect(page).to have_selector('a.shortcuts-monitor', text: 'Monitor')
+ it 'has the correct `Monitor` and `Operate` menu items' do
expect(page).to have_link('Incidents', href: project_incidents_path(project))
+
+ click_button('Operate')
+
expect(page).to have_link('Environments', href: project_environments_path(project))
- expect(page).not_to have_link('Alerts', href: project_alert_management_index_path(project))
- expect(page).not_to have_link('Error Tracking', href: project_error_tracking_index_path(project))
- expect(page).not_to have_link('Kubernetes', href: project_clusters_path(project))
+ expect(page).not_to have_link('Alerts', href: project_alert_management_index_path(project), visible: :all)
+ expect(page).not_to have_link('Error Tracking', href: project_error_tracking_index_path(project), visible: :all)
+ expect(page).not_to have_link('Kubernetes clusters', href: project_clusters_path(project), visible: :all)
end
context 'when monitor project feature is PRIVATE' do
let(:access_level) { ProjectFeature::PRIVATE }
- it 'does not show the `Monitor` menu' do
- expect(page).not_to have_selector('a.shortcuts-monitor')
+ it 'does not show common items of the `Monitor` menu' do
+ expect(page).not_to have_link('Error Tracking', href: project_incidents_path(project), visible: :all)
end
end
context 'when monitor project feature is DISABLED' do
let(:access_level) { ProjectFeature::DISABLED }
- it 'does not show the `Monitor` menu' do
- expect(page).not_to have_selector('a.shortcuts-monitor')
+ it 'does not show the `Incidents` menu' do
+ expect(page).not_to have_link('Error Tracking', href: project_incidents_path(project), visible: :all)
end
end
end
@@ -77,63 +84,86 @@ RSpec.describe 'Monitor dropdown sidebar', :aggregate_failures, feature_category
context 'when user has guest role' do
let(:role) { :guest }
- it 'has the correct `Monitor` menu items' do
+ it 'has the correct `Monitor` and `Operate` menu items' do
visit project_issues_path(project)
- expect(page).to have_selector('a.shortcuts-monitor', text: 'Monitor')
+
+ click_button('Monitor')
+
expect(page).to have_link('Incidents', href: project_incidents_path(project))
+
+ click_button('Operate')
+
expect(page).to have_link('Environments', href: project_environments_path(project))
- expect(page).not_to have_link('Alerts', href: project_alert_management_index_path(project))
- expect(page).not_to have_link('Error Tracking', href: project_error_tracking_index_path(project))
- expect(page).not_to have_link('Kubernetes', href: project_clusters_path(project))
+ expect(page).not_to have_link('Alerts', href: project_alert_management_index_path(project), visible: :all)
+ expect(page).not_to have_link('Error Tracking', href: project_error_tracking_index_path(project), visible: :all)
+ expect(page).not_to have_link('Kubernetes clusters', href: project_clusters_path(project), visible: :all)
end
- it_behaves_like 'shows Monitor menu based on the access level'
+ it_behaves_like 'shows common Monitor menu item based on the access level'
end
context 'when user has reporter role' do
let(:role) { :reporter }
- it 'has the correct `Monitor` menu items' do
+ it 'has the correct `Monitor` and `Operate` menu items' do
visit project_issues_path(project)
+
+ click_button('Monitor')
+
expect(page).to have_link('Incidents', href: project_incidents_path(project))
- expect(page).to have_link('Environments', href: project_environments_path(project))
expect(page).to have_link('Error Tracking', href: project_error_tracking_index_path(project))
- expect(page).not_to have_link('Alerts', href: project_alert_management_index_path(project))
- expect(page).not_to have_link('Kubernetes', href: project_clusters_path(project))
+ click_button('Operate')
+
+ expect(page).to have_link('Environments', href: project_environments_path(project))
+
+ expect(page).not_to have_link('Alerts', href: project_alert_management_index_path(project), visible: :all)
+ expect(page).not_to have_link('Kubernetes clusters', href: project_clusters_path(project), visible: :all)
end
- it_behaves_like 'shows Monitor menu based on the access level'
+ it_behaves_like 'shows common Monitor menu item based on the access level'
end
context 'when user has developer role' do
let(:role) { :developer }
- it 'has the correct `Monitor` menu items' do
+ it 'has the correct `Monitor` and `Operate` menu items' do
visit project_issues_path(project)
+
+ click_button('Monitor')
+
expect(page).to have_link('Alerts', href: project_alert_management_index_path(project))
expect(page).to have_link('Incidents', href: project_incidents_path(project))
- expect(page).to have_link('Environments', href: project_environments_path(project))
expect(page).to have_link('Error Tracking', href: project_error_tracking_index_path(project))
- expect(page).to have_link('Kubernetes', href: project_clusters_path(project))
+
+ click_button('Operate')
+
+ expect(page).to have_link('Environments', href: project_environments_path(project))
+ expect(page).to have_link('Kubernetes clusters', href: project_clusters_path(project))
end
- it_behaves_like 'shows Monitor menu based on the access level'
+ it_behaves_like 'shows common Monitor menu item based on the access level'
end
context 'when user has maintainer role' do
let(:role) { :maintainer }
- it 'has the correct `Monitor` menu items' do
+ it 'has the correct `Monitor` and `Operate` menu items' do
visit project_issues_path(project)
+
+ click_button('Monitor')
+
expect(page).to have_link('Alerts', href: project_alert_management_index_path(project))
expect(page).to have_link('Incidents', href: project_incidents_path(project))
- expect(page).to have_link('Environments', href: project_environments_path(project))
expect(page).to have_link('Error Tracking', href: project_error_tracking_index_path(project))
- expect(page).to have_link('Kubernetes', href: project_clusters_path(project))
+
+ click_button('Operate')
+
+ expect(page).to have_link('Environments', href: project_environments_path(project))
+ expect(page).to have_link('Kubernetes clusters', href: project_clusters_path(project))
end
- it_behaves_like 'shows Monitor menu based on the access level'
+ it_behaves_like 'shows common Monitor menu item based on the access level'
end
end
diff --git a/spec/features/nav/new_nav_callout_spec.rb b/spec/features/nav/new_nav_callout_spec.rb
deleted file mode 100644
index 22e7fd6b9f9..00000000000
--- a/spec/features/nav/new_nav_callout_spec.rb
+++ /dev/null
@@ -1,64 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'new navigation callout', :js, feature_category: :navigation do
- let_it_be(:callout_title) { _('Welcome to a new navigation experience') }
- let(:dot_com) { false }
-
- before do
- allow(Gitlab).to receive(:com?).and_return(dot_com)
- sign_in(user)
- visit root_path
- end
-
- context 'with new navigation toggled on' do
- let_it_be(:user) { create(:user, created_at: Date.new(2023, 6, 1), use_new_navigation: true) }
-
- it 'shows a callout about the new navigation' do
- expect(page).to have_content callout_title
- end
-
- context 'when user dismisses callout' do
- it 'hides callout' do
- expect(page).to have_content callout_title
-
- page.within(find('[data-feature-id="new_navigation_callout"]')) do
- find('[data-testid="close-icon"]').click
- end
-
- wait_for_requests
-
- visit root_path
-
- expect(page).not_to have_content callout_title
- end
- end
- end
-
- context 'when user registered on or after June 2nd 2023' do
- let_it_be(:user) { create(:user, created_at: Date.new(2023, 6, 2), use_new_navigation: true) }
-
- context 'when on GitLab.com' do
- let(:dot_com) { true }
-
- it 'does not show the callout about the new navigation' do
- expect(page).not_to have_content callout_title
- end
- end
-
- context 'when on a self-managed instance' do
- it 'shows the callout about the new navigation' do
- expect(page).to have_content callout_title
- end
- end
- end
-
- context 'with new navigation toggled off' do
- let_it_be(:user) { create(:user, created_at: Date.new(2023, 6, 1), use_new_navigation: false) }
-
- it 'does not show the callout' do
- expect(page).not_to have_content callout_title
- end
- end
-end
diff --git a/spec/features/nav/new_nav_for_everyone_callout_spec.rb b/spec/features/nav/new_nav_for_everyone_callout_spec.rb
new file mode 100644
index 00000000000..ad0b57298d7
--- /dev/null
+++ b/spec/features/nav/new_nav_for_everyone_callout_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'new navigation for everyone callout', :js, feature_category: :navigation do
+ let_it_be(:callout_title) { _('GitLab has redesigned the left sidebar to address customer feedback') }
+
+ before do
+ sign_in(user)
+ visit root_path
+ end
+
+ context 'with new navigation previously toggled on' do
+ let_it_be(:user) { create(:user, use_new_navigation: true) }
+
+ it 'does not show the callout' do
+ expect(page).to have_css('[data-testid="super-sidebar"]')
+ expect(page).not_to have_content callout_title
+ end
+ end
+
+ context 'with new navigation previously toggled off' do
+ let_it_be(:user) { create(:user, use_new_navigation: false) }
+
+ it 'shows a callout about the new navigation now being active for everyone' do
+ expect(page).to have_css('[data-testid="super-sidebar"]')
+ expect(page).to have_content callout_title
+ end
+
+ context 'when user dismisses callout' do
+ it 'hides callout' do
+ expect(page).to have_content callout_title
+
+ page.within(find('[data-feature-id="new_nav_for_everyone_callout"]')) do
+ find_by_testid('close-icon').click
+ end
+
+ wait_for_requests
+
+ visit root_path
+
+ expect(page).not_to have_content callout_title
+ end
+ end
+ end
+
+ context 'with new navigation never toggled on or off' do
+ let_it_be(:user) { create(:user, use_new_navigation: nil) }
+
+ it 'does not show the callout' do
+ expect(page).to have_css('[data-testid="super-sidebar"]')
+ expect(page).not_to have_content callout_title
+ end
+ end
+end
diff --git a/spec/features/nav/new_nav_invite_members_spec.rb b/spec/features/nav/new_nav_invite_members_spec.rb
index 4c37d6b4760..7501745ec55 100644
--- a/spec/features/nav/new_nav_invite_members_spec.rb
+++ b/spec/features/nav/new_nav_invite_members_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe 'new navigation toggle', :js, feature_category: :navigation do
include Features::InviteMembersModalHelpers
- let_it_be(:user) { create(:user, use_new_navigation: true) }
+ let_it_be(:user) { create(:user) }
before do
sign_in(user)
diff --git a/spec/features/nav/new_nav_toggle_spec.rb b/spec/features/nav/new_nav_toggle_spec.rb
deleted file mode 100644
index 6872058be8e..00000000000
--- a/spec/features/nav/new_nav_toggle_spec.rb
+++ /dev/null
@@ -1,59 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'new navigation toggle', :js, feature_category: :navigation do
- let_it_be(:user) { create(:user) }
-
- before do
- user.update!(use_new_navigation: user_preference)
- sign_in(user)
- visit explore_projects_path
- end
-
- context 'when user has new nav disabled' do
- let(:user_preference) { false }
-
- it 'allows to enable new nav', :aggregate_failures do
- within '.js-nav-user-dropdown' do
- find('a[data-toggle="dropdown"]').click
- expect(page).to have_content('Navigation redesign')
-
- toggle = page.find('.gl-toggle:not(.is-checked)')
- toggle.click # reloads the page
- end
-
- wait_for_requests
-
- expect(user.reload.use_new_navigation).to eq true
- end
-
- it 'shows the old navigation' do
- expect(page).to have_selector('.js-navbar')
- expect(page).not_to have_selector('[data-testid="super-sidebar"]')
- end
- end
-
- context 'when user has new nav enabled' do
- let(:user_preference) { true }
-
- it 'allows to disable new nav', :aggregate_failures do
- within '[data-testid="super-sidebar"] [data-testid="user-dropdown"]' do
- click_button "#{user.name} user’s menu"
- expect(page).to have_content('Navigation redesign')
-
- toggle = page.find('.gl-toggle.is-checked')
- toggle.click # reloads the page
- end
-
- wait_for_requests
-
- expect(user.reload.use_new_navigation).to eq false
- end
-
- it 'shows the new navigation' do
- expect(page).not_to have_selector('.js-navbar')
- expect(page).to have_selector('[data-testid="super-sidebar"]')
- end
- end
-end
diff --git a/spec/features/nav/pinned_nav_items_spec.rb b/spec/features/nav/pinned_nav_items_spec.rb
index b4d6464ec50..a2428048a1a 100644
--- a/spec/features/nav/pinned_nav_items_spec.rb
+++ b/spec/features/nav/pinned_nav_items_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'Navigation menu item pinning', :js, feature_category: :navigation do
- let_it_be(:user) { create(:user, use_new_navigation: true) }
+ let_it_be(:user) { create(:user) }
before do
sign_in(user)
diff --git a/spec/features/nav/top_nav_responsive_spec.rb b/spec/features/nav/top_nav_responsive_spec.rb
deleted file mode 100644
index 2a07742c91e..00000000000
--- a/spec/features/nav/top_nav_responsive_spec.rb
+++ /dev/null
@@ -1,101 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'top nav responsive', :js, feature_category: :navigation do
- include MobileHelpers
- include Features::InviteMembersModalHelpers
-
- let_it_be(:user) { create(:user, :no_super_sidebar) }
-
- before do
- sign_in(user)
-
- resize_screen_xs
- end
-
- context 'when outside groups and projects' do
- before do
- visit explore_projects_path
- end
-
- context 'when menu is closed' do
- it 'has page content and hides responsive menu', :aggregate_failures do
- expect(page).to have_css('.page-title', text: 'Explore projects')
- expect(page).to have_link('Homepage', id: 'logo')
-
- expect(page).to have_no_css('.top-nav-responsive')
- end
- end
-
- context 'when menu is opened' do
- before do
- click_button('Menu')
- end
-
- it 'hides everything and shows responsive menu', :aggregate_failures do
- expect(page).to have_no_css('.page-title', text: 'Explore projects')
- expect(page).to have_no_link('Homepage', id: 'logo')
-
- within '.top-nav-responsive' do
- expect(page).to have_link(nil, href: search_path)
- expect(page).to have_button('Projects')
- expect(page).to have_button('Groups')
- expect(page).to have_link('Your work', href: dashboard_projects_path)
- expect(page).to have_link('Explore', href: explore_projects_path)
- end
- end
-
- it 'has new dropdown', :aggregate_failures do
- create_new_button.click
-
- expect(page).to have_link('New project', href: new_project_path)
- expect(page).to have_link('New group', href: new_group_path)
- expect(page).to have_link('New snippet', href: new_snippet_path)
- end
- end
- end
-
- context 'when inside a project' do
- let_it_be(:project) { create(:project).tap { |record| record.add_owner(user) } }
-
- before do
- visit project_path(project)
- end
-
- it 'the add menu contains invite members dropdown option and opens invite modal' do
- invite_members_from_menu
-
- page.within invite_modal_selector do
- expect(page).to have_content("You're inviting members to the #{project.name} project")
- end
- end
- end
-
- context 'when inside a group' do
- let_it_be(:group) { create(:group).tap { |record| record.add_owner(user) } }
-
- before do
- visit group_path(group)
- end
-
- it 'the add menu contains invite members dropdown option and opens invite modal' do
- invite_members_from_menu
-
- page.within invite_modal_selector do
- expect(page).to have_content("You're inviting members to the #{group.name} group")
- end
- end
- end
-
- def invite_members_from_menu
- click_button('Menu')
- create_new_button.click
-
- click_button('Invite members')
- end
-
- def create_new_button
- find('[data-testid="plus-icon"]')
- end
-end
diff --git a/spec/features/nav/top_nav_spec.rb b/spec/features/nav/top_nav_spec.rb
deleted file mode 100644
index bf91897eb26..00000000000
--- a/spec/features/nav/top_nav_spec.rb
+++ /dev/null
@@ -1,51 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'top nav responsive', :js, feature_category: :navigation do
- include Features::InviteMembersModalHelpers
-
- let_it_be(:user) { create(:user, :no_super_sidebar) }
-
- before do
- sign_in(user)
- end
-
- context 'when inside a project' do
- let_it_be(:project) { create(:project).tap { |record| record.add_owner(user) } }
-
- before do
- visit project_path(project)
- end
-
- it 'the add menu contains invite members dropdown option and opens invite modal' do
- invite_members_from_menu
-
- page.within invite_modal_selector do
- expect(page).to have_content("You're inviting members to the #{project.name} project")
- end
- end
- end
-
- context 'when inside a group' do
- let_it_be(:group) { create(:group).tap { |record| record.add_owner(user) } }
-
- before do
- visit group_path(group)
- end
-
- it 'the add menu contains invite members dropdown option and opens invite modal' do
- invite_members_from_menu
-
- page.within invite_modal_selector do
- expect(page).to have_content("You're inviting members to the #{group.name} group")
- end
- end
- end
-
- def invite_members_from_menu
- find('[data-testid="new-menu-toggle"]').click
-
- click_link('Invite members')
- end
-end
diff --git a/spec/features/nav/top_nav_tooltip_spec.rb b/spec/features/nav/top_nav_tooltip_spec.rb
deleted file mode 100644
index 1afd1981a86..00000000000
--- a/spec/features/nav/top_nav_tooltip_spec.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'top nav tooltips', :js, feature_category: :navigation do
- let_it_be(:user) { create(:user) }
-
- before do
- sign_in(user)
- visit explore_projects_path
- end
-
- it 'clicking new dropdown hides tooltip', :aggregate_failures,
- quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/382786' do
- btn = '#js-onboarding-new-project-link'
-
- page.find(btn).hover
-
- expect(page).to have_content('Create new...')
-
- page.find(btn).click
-
- expect(page).not_to have_content('Create new...')
- end
-end
diff --git a/spec/features/profiles/two_factor_auths_spec.rb b/spec/features/profiles/two_factor_auths_spec.rb
index b52f66cfcee..15ab79684d9 100644
--- a/spec/features/profiles/two_factor_auths_spec.rb
+++ b/spec/features/profiles/two_factor_auths_spec.rb
@@ -59,7 +59,7 @@ RSpec.describe 'Two factor auths', feature_category: :user_profile do
fill_in 'pin_code', with: '123'
click_button 'Register with two-factor app'
- expect(page).to have_link('Try the troubleshooting steps here.', href: help_page_path('user/profile/account/two_factor_authentication.md', anchor: 'troubleshooting'))
+ expect(page).to have_link('Try the troubleshooting steps here.', href: help_page_path('user/profile/account/two_factor_authentication', anchor: 'troubleshooting'))
end
end
diff --git a/spec/features/profiles/user_edit_profile_spec.rb b/spec/features/profiles/user_edit_profile_spec.rb
index 697ad4c87f7..439839cfad5 100644
--- a/spec/features/profiles/user_edit_profile_spec.rb
+++ b/spec/features/profiles/user_edit_profile_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe 'User edit profile', feature_category: :user_profile do
include Features::NotesHelpers
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be_with_reload(:user) { create(:user) }
before do
stub_feature_flags(edit_user_profile_vue: false)
@@ -18,17 +18,6 @@ RSpec.describe 'User edit profile', feature_category: :user_profile do
wait_for_requests if respond_to?(:wait_for_requests)
end
- def update_user_email
- fill_in 'user_email', with: 'new-email@example.com'
- click_button 'Update profile settings'
- end
-
- def confirm_password(password)
- fill_in 'password-confirmation', with: password
- click_button 'Confirm password'
- wait_for_requests if respond_to?(:wait_for_requests)
- end
-
def visit_user
visit user_path(user)
wait_for_requests
@@ -119,6 +108,18 @@ RSpec.describe 'User edit profile', feature_category: :user_profile do
end
describe 'when I change my email', :js do
+ def update_user_email
+ fill_in 'user_email', with: '' # Clearing the email field
+ fill_in 'user_email', with: 'new-email@example.com'
+ submit_settings
+ end
+
+ def confirm_password(password)
+ fill_in 'password-confirmation', with: password
+ click_button 'Confirm password'
+ wait_for_requests if respond_to?(:wait_for_requests)
+ end
+
before do
user.send_reset_password_instructions
end
@@ -194,13 +195,20 @@ RSpec.describe 'User edit profile', feature_category: :user_profile do
emoji_button.click
end
+ after do
+ if user.status
+ user.status.destroy!
+ user.reload_status
+ end
+ end
+
context 'profile edit form' do
it 'shows the user status form' do
expect(page).to have_content('Current status')
end
it 'adds emoji to user status' do
- emoji = 'basketball'
+ emoji = 'laughing'
select_emoji(emoji)
submit_settings
@@ -225,7 +233,7 @@ RSpec.describe 'User edit profile', feature_category: :user_profile do
end
it 'adds message and emoji to user status' do
- emoji = '8ball'
+ emoji = 'grinning'
message = 'Playing outside'
select_emoji(emoji)
fill_in s_("SetStatusModal|What's your status?"), with: message
@@ -319,9 +327,9 @@ RSpec.describe 'User edit profile', feature_category: :user_profile do
let(:project) { create(:project) }
def open_modal(button_text)
- find('.header-user-dropdown-toggle').click
+ find_by_testid('user-dropdown').click
- page.within ".header-user" do
+ within_testid('user-dropdown') do
find('.js-set-status-modal-trigger.ready')
click_button button_text
@@ -348,9 +356,9 @@ RSpec.describe 'User edit profile', feature_category: :user_profile do
end
it 'shows the "Set status" menu item in the user menu' do
- find('.header-user-dropdown-toggle').click
+ find_by_testid('user-dropdown').click
- page.within ".header-user" do
+ within_testid('user-dropdown') do
expect(page).to have_content('Set status')
end
end
@@ -359,9 +367,9 @@ RSpec.describe 'User edit profile', feature_category: :user_profile do
user_status = create(:user_status, user: user, message: 'Eating bread', emoji: 'stuffed_flatbread')
visit root_path(user)
- find('.header-user-dropdown-toggle').click
+ find_by_testid('user-dropdown').click
- page.within ".header-user" do
+ within_testid('user-dropdown') do
expect(page).to have_emoji(user_status.emoji)
expect(page).to have_content user_status.message
expect(page).to have_content('Edit status')
@@ -376,7 +384,7 @@ RSpec.describe 'User edit profile', feature_category: :user_profile do
end
it 'adds emoji to user status' do
- emoji = '8ball'
+ emoji = 'grinning'
open_user_status_modal
select_emoji(emoji, true)
set_user_status_in_modal
@@ -407,7 +415,7 @@ RSpec.describe 'User edit profile', feature_category: :user_profile do
it 'opens the emoji modal again after closing it' do
open_user_status_modal
- select_emoji('8ball', true)
+ select_emoji('grinning', true)
find('.emoji-menu-toggle-button').click
@@ -418,7 +426,7 @@ RSpec.describe 'User edit profile', feature_category: :user_profile do
project.add_maintainer(user)
visit(project_issue_path(project, issue))
- emoji = '8ball'
+ emoji = 'grinning'
open_user_status_modal
select_emoji(emoji, true)
@@ -440,7 +448,7 @@ RSpec.describe 'User edit profile', feature_category: :user_profile do
end
it 'adds message and emoji to user status' do
- emoji = '8ball'
+ emoji = 'grinning'
message = 'Playing outside'
open_user_status_modal
select_emoji(emoji, true)
@@ -478,8 +486,6 @@ RSpec.describe 'User edit profile', feature_category: :user_profile do
end
context 'Remove status button' do
- let(:user) { create(:user, :no_super_sidebar) }
-
before do
user.status = UserStatus.new(message: 'Eating bread', emoji: 'stuffed_flatbread')
@@ -504,9 +510,9 @@ RSpec.describe 'User edit profile', feature_category: :user_profile do
it 'shows the "Set status" menu item in the user menu' do
visit root_path(user)
- find('.header-user-dropdown-toggle').click
+ find_by_testid('user-dropdown').click
- page.within ".header-user" do
+ within_testid('user-dropdown') do
expect(page).to have_content('Set status')
end
end
@@ -520,30 +526,30 @@ RSpec.describe 'User edit profile', feature_category: :user_profile do
expect(page).to have_emoji('speech_balloon')
end
end
+ end
- context 'User time preferences', :js do
- let(:issue) { create(:issue, project: project) }
- let(:project) { create(:project) }
+ context 'User time preferences', :js do
+ let(:issue) { create(:issue, project: project) }
+ let(:project) { create(:project) }
- it 'shows the user time preferences form' do
- expect(page).to have_content('Time settings')
- end
+ it 'shows the user time preferences form' do
+ expect(page).to have_content('Time settings')
+ end
- it 'allows the user to select a time zone from a dropdown list of options' do
- expect(page).not_to have_selector('.user-time-preferences [data-testid="base-dropdown-menu"]')
+ it 'allows the user to select a time zone from a dropdown list of options' do
+ expect(page).not_to have_selector('.user-time-preferences [data-testid="base-dropdown-menu"]')
- page.find('.user-time-preferences .gl-new-dropdown-toggle').click
+ page.find('.user-time-preferences .gl-new-dropdown-toggle').click
- expect(page.find('.user-time-preferences [data-testid="base-dropdown-menu"]')).to be_visible
+ expect(page.find('.user-time-preferences [data-testid="base-dropdown-menu"]')).to be_visible
- page.find("li", text: "Arizona").click
+ page.find("li", text: "Arizona").click
- expect(page).to have_field(:user_timezone, with: 'America/Phoenix', type: :hidden)
- end
+ expect(page).to have_field(:user_timezone, with: 'America/Phoenix', type: :hidden)
+ end
- it 'timezone defaults to empty' do
- expect(page).to have_field(:user_timezone, with: '', type: :hidden)
- end
+ it 'timezone defaults to empty' do
+ expect(page).to have_field(:user_timezone, with: '', type: :hidden)
end
end
diff --git a/spec/features/profiles/user_visits_profile_account_page_spec.rb b/spec/features/profiles/user_visits_profile_account_page_spec.rb
deleted file mode 100644
index 8569cefd1f4..00000000000
--- a/spec/features/profiles/user_visits_profile_account_page_spec.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'User visits the profile account page', feature_category: :user_profile do
- let(:user) { create(:user, :no_super_sidebar) }
-
- before do
- sign_in(user)
-
- visit(profile_account_path)
- end
-
- it 'shows correct menu item' do
- expect(page).to have_active_navigation('Account')
- end
-end
diff --git a/spec/features/profiles/user_visits_profile_authentication_log_spec.rb b/spec/features/profiles/user_visits_profile_authentication_log_spec.rb
index f92b8e2e751..c081f4a3ec9 100644
--- a/spec/features/profiles/user_visits_profile_authentication_log_spec.rb
+++ b/spec/features/profiles/user_visits_profile_authentication_log_spec.rb
@@ -3,19 +3,7 @@
require 'spec_helper'
RSpec.describe 'User visits the authentication log', feature_category: :user_profile do
- let(:user) { create(:user, :no_super_sidebar) }
-
- context 'when user signed in' do
- before do
- sign_in(user)
- end
-
- it 'shows correct menu item' do
- visit(audit_log_profile_path)
-
- expect(page).to have_active_navigation('Authentication Log')
- end
- end
+ let(:user) { create(:user) }
context 'when user has activity' do
before do
diff --git a/spec/features/profiles/user_visits_profile_preferences_page_spec.rb b/spec/features/profiles/user_visits_profile_preferences_page_spec.rb
index 4da1a7ba81a..033d69d29b9 100644
--- a/spec/features/profiles/user_visits_profile_preferences_page_spec.rb
+++ b/spec/features/profiles/user_visits_profile_preferences_page_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe 'User visits the profile preferences page', :js, feature_category: :user_profile do
include ListboxHelpers
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
before do
sign_in(user)
@@ -13,10 +13,6 @@ RSpec.describe 'User visits the profile preferences page', :js, feature_category
visit(profile_preferences_path)
end
- it 'shows correct menu item' do
- expect(page).to have_active_navigation('Preferences')
- end
-
describe 'User changes their syntax highlighting theme', :js do
it 'updates their preference' do
choose 'user_color_scheme_id_5'
@@ -44,7 +40,7 @@ RSpec.describe 'User visits the profile preferences page', :js, feature_category
wait_for_requests
- find('#logo').click
+ find('[data-track-label="gitlab_logo_link"]').click
expect(page).to have_content("You don't have starred projects yet")
expect(page).to have_current_path starred_dashboard_projects_path, ignore_query: true
diff --git a/spec/features/profiles/user_visits_profile_spec.rb b/spec/features/profiles/user_visits_profile_spec.rb
index 821c3d5ef2b..37a19ecadb8 100644
--- a/spec/features/profiles/user_visits_profile_spec.rb
+++ b/spec/features/profiles/user_visits_profile_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'User visits their profile', feature_category: :user_profile do
- let_it_be_with_refind(:user) { create(:user, :no_super_sidebar) }
+ let_it_be_with_refind(:user) { create(:user) }
before do
stub_feature_flags(profile_tabs_vue: false)
@@ -11,12 +11,6 @@ RSpec.describe 'User visits their profile', feature_category: :user_profile do
sign_in(user)
end
- it 'shows correct menu item' do
- visit(profile_path)
-
- expect(page).to have_active_navigation('Profile')
- end
-
it 'shows profile info' do
visit(profile_path)
@@ -59,7 +53,7 @@ RSpec.describe 'User visits their profile', feature_category: :user_profile do
expect(page).to have_content user.username
end
- page.within ".content" do
+ within_testid('super-sidebar') do
click_link link
end
diff --git a/spec/features/profiles/user_visits_profile_ssh_keys_page_spec.rb b/spec/features/profiles/user_visits_profile_ssh_keys_page_spec.rb
deleted file mode 100644
index 728fe1a3172..00000000000
--- a/spec/features/profiles/user_visits_profile_ssh_keys_page_spec.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'User visits the profile SSH keys page', feature_category: :user_profile do
- let(:user) { create(:user, :no_super_sidebar) }
-
- before do
- sign_in(user)
-
- visit(profile_keys_path)
- end
-
- it 'shows correct menu item' do
- expect(page).to have_active_navigation('SSH Keys')
- end
-end
diff --git a/spec/features/project_variables_spec.rb b/spec/features/project_variables_spec.rb
index e2fa924af67..9a1aa41b982 100644
--- a/spec/features/project_variables_spec.rb
+++ b/spec/features/project_variables_spec.rb
@@ -13,13 +13,13 @@ RSpec.describe 'Project variables', :js, feature_category: :secrets_management d
project.add_maintainer(user)
project.variables << variable
- stub_feature_flags(ci_variable_drawer: false)
visit page_path
wait_for_requests
end
context 'when ci_variables_pages FF is enabled' do
- it_behaves_like 'variable list'
+ it_behaves_like 'variable list drawer'
+ it_behaves_like 'variable list env scope'
it_behaves_like 'variable list pagination', :ci_variable
end
@@ -28,37 +28,7 @@ RSpec.describe 'Project variables', :js, feature_category: :secrets_management d
stub_feature_flags(ci_variables_pages: false)
end
- it_behaves_like 'variable list'
- end
-
- it 'adds a new variable with an environment scope' do
- click_button('Add variable')
-
- page.within('#add-ci-variable') do
- fill_in 'Key', with: 'akey'
- find('#ci-variable-value').set('akey_value')
-
- click_button('All (default)')
- fill_in 'Search', with: 'review/*'
- find('[data-testid="create-wildcard-button"]').click
-
- click_button('Add variable')
- end
-
- wait_for_requests
-
- page.within('[data-testid="ci-variable-table"]') do
- expect(find('.js-ci-variable-row:first-child [data-label="Environments"]').text).to eq('review/*')
- end
- end
-
- context 'when ci_variable_drawer FF is enabled' do
- before do
- stub_feature_flags(ci_variable_drawer: true)
- visit page_path
- wait_for_requests
- end
-
it_behaves_like 'variable list drawer'
+ it_behaves_like 'variable list env scope'
end
end
diff --git a/spec/features/projects/active_tabs_spec.rb b/spec/features/projects/active_tabs_spec.rb
index 8879636e4dc..199ea638f61 100644
--- a/spec/features/projects/active_tabs_spec.rb
+++ b/spec/features/projects/active_tabs_spec.rb
@@ -2,8 +2,8 @@
require 'spec_helper'
-RSpec.describe 'Project active tab', feature_category: :groups_and_projects do
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+RSpec.describe 'Project active tab', :js, feature_category: :groups_and_projects do
+ let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository, :with_namespace_settings, namespace: user.namespace) }
before do
@@ -11,7 +11,7 @@ RSpec.describe 'Project active tab', feature_category: :groups_and_projects do
end
def click_tab(title)
- page.within '.sidebar-top-level-items > .active' do
+ within_testid('super-sidebar') do
click_link(title)
end
end
@@ -20,66 +20,68 @@ RSpec.describe 'Project active tab', feature_category: :groups_and_projects do
it 'activates Project scope menu' do
visit project_path(project)
- expect(page).to have_selector('.sidebar-top-level-items > li.active', count: 1)
- expect(find('.sidebar-top-level-items > li.active')).to have_content(project.name)
+ expect(page).to have_selector('a[aria-current="page"]', count: 1)
+ expect(find('a[aria-current="page"]')).to have_content(project.name)
end
end
- context 'on Project information' do
- context 'default link' do
- before do
- visit project_path(project)
-
- click_link('Project information', match: :first)
- end
-
- it_behaves_like 'page has active tab', 'Project'
- it_behaves_like 'page has active sub tab', 'Activity'
- end
+ context 'on Project Manage' do
+ %w[Activity Members Labels].each do |sub_menu|
+ context "on project Manage/#{sub_menu}" do
+ before do
+ visit project_path(project)
+ within_testid('super-sidebar') do
+ click_button("Manage")
+ end
+ click_tab(sub_menu)
+ end
- context 'on Project information/Activity' do
- before do
- visit activity_project_path(project)
+ it_behaves_like 'page has active tab', 'Manage'
+ it_behaves_like 'page has active sub tab', sub_menu
end
-
- it_behaves_like 'page has active tab', 'Project'
- it_behaves_like 'page has active sub tab', 'Activity'
end
end
- context 'on project Repository' do
+ context 'on project Code' do
before do
root_ref = project.repository.root_ref
visit project_tree_path(project, root_ref)
+
+ # Enabling Js in here causes more SQL queries to be caught by the query limiter.
+ # We are increasing the limit here so that the tests pass.
+ allow(Gitlab::QueryLimiting::Transaction).to receive(:threshold).and_return(110)
end
- it_behaves_like 'page has active tab', 'Repository'
+ it_behaves_like 'page has active tab', 'Code'
- %w[Files Commits Graph Compare Branches Tags].each do |sub_menu|
- context "on project Repository/#{sub_menu}" do
+ ["Repository", "Branches", "Commits", "Tags", "Repository graph", "Compare revisions"].each do |sub_menu|
+ context "on project Code/#{sub_menu}" do
before do
click_tab(sub_menu)
end
- it_behaves_like 'page has active tab', 'Repository'
+ it_behaves_like 'page has active tab', 'Code'
it_behaves_like 'page has active sub tab', sub_menu
end
end
end
- context 'on project Issues' do
+ context 'on project Plan' do
before do
visit project_issues_path(project)
end
- it_behaves_like 'page has active tab', 'Issues'
+ it_behaves_like 'page has active tab', 'Pinned'
- context "on project Issues/Milestones" do
+ context "on project Code/Milestones" do
before do
+ within_testid('super-sidebar') do
+ click_button("Plan")
+ end
click_tab('Milestones')
end
- it_behaves_like 'page has active tab', 'Issues'
+ it_behaves_like 'page has active tab', 'Plan'
it_behaves_like 'page has active sub tab', 'Milestones'
end
end
@@ -89,7 +91,7 @@ RSpec.describe 'Project active tab', feature_category: :groups_and_projects do
visit project_merge_requests_path(project)
end
- it_behaves_like 'page has active tab', 'Merge requests'
+ it_behaves_like 'page has active tab', 'Pinned'
end
context 'on project Wiki' do
@@ -97,7 +99,8 @@ RSpec.describe 'Project active tab', feature_category: :groups_and_projects do
visit wiki_path(project.wiki)
end
- it_behaves_like 'page has active tab', 'Wiki'
+ it_behaves_like 'page has active tab', 'Plan'
+ it_behaves_like 'page has active sub tab', 'Wiki'
end
context 'on project Members' do
@@ -105,7 +108,8 @@ RSpec.describe 'Project active tab', feature_category: :groups_and_projects do
visit project_project_members_path(project)
end
- it_behaves_like 'page has active tab', 'Members'
+ it_behaves_like 'page has active tab', 'Manage'
+ it_behaves_like 'page has active sub tab', 'Members'
end
context 'on project Settings' do
@@ -132,23 +136,23 @@ RSpec.describe 'Project active tab', feature_category: :groups_and_projects do
end
end
- context 'on project Analytics' do
+ context 'on project Analyze' do
before do
visit project_cycle_analytics_path(project)
end
- context 'on project Analytics/Value stream Analytics' do
- it_behaves_like 'page has active tab', _('Analytics')
+ context 'on project Analyze/Value stream Analyze' do
+ it_behaves_like 'page has active tab', _('Analyze')
it_behaves_like 'page has active sub tab', _('Value stream')
end
- context 'on project Analytics/"CI/CD"' do
+ context 'on project Analyze/"CI/CD"' do
before do
click_tab(_('CI/CD'))
end
- it_behaves_like 'page has active tab', _('Analytics')
- it_behaves_like 'page has active sub tab', _('CI/CD')
+ it_behaves_like 'page has active tab', _('Analyze')
+ it_behaves_like 'page has active sub tab', _('CI/CD analytics')
end
end
@@ -161,7 +165,7 @@ RSpec.describe 'Project active tab', feature_category: :groups_and_projects do
visit project_pipeline_path(project, pipeline)
end
- it_behaves_like 'page has active tab', _('CI/CD')
+ it_behaves_like 'page has active tab', _('Build')
it_behaves_like 'page has active sub tab', _('Pipelines')
end
@@ -170,7 +174,7 @@ RSpec.describe 'Project active tab', feature_category: :groups_and_projects do
visit dag_project_pipeline_path(project, pipeline)
end
- it_behaves_like 'page has active tab', _('CI/CD')
+ it_behaves_like 'page has active tab', _('Build')
it_behaves_like 'page has active sub tab', _('Pipelines')
end
@@ -179,7 +183,7 @@ RSpec.describe 'Project active tab', feature_category: :groups_and_projects do
visit builds_project_pipeline_path(project, pipeline)
end
- it_behaves_like 'page has active tab', _('CI/CD')
+ it_behaves_like 'page has active tab', _('Build')
it_behaves_like 'page has active sub tab', _('Pipelines')
end
@@ -188,7 +192,7 @@ RSpec.describe 'Project active tab', feature_category: :groups_and_projects do
visit failures_project_pipeline_path(project, pipeline)
end
- it_behaves_like 'page has active tab', _('CI/CD')
+ it_behaves_like 'page has active tab', _('Build')
it_behaves_like 'page has active sub tab', _('Pipelines')
end
@@ -197,7 +201,7 @@ RSpec.describe 'Project active tab', feature_category: :groups_and_projects do
visit test_report_project_pipeline_path(project, pipeline)
end
- it_behaves_like 'page has active tab', _('CI/CD')
+ it_behaves_like 'page has active tab', _('Build')
it_behaves_like 'page has active sub tab', _('Pipelines')
end
end
diff --git a/spec/features/projects/blobs/blob_show_spec.rb b/spec/features/projects/blobs/blob_show_spec.rb
index 30a81ccc071..36665f2b77d 100644
--- a/spec/features/projects/blobs/blob_show_spec.rb
+++ b/spec/features/projects/blobs/blob_show_spec.rb
@@ -941,9 +941,7 @@ RSpec.describe 'File blob', :js, feature_category: :groups_and_projects do
it 'shows the realtime pipeline status' do
page.within('.commit-actions') do
- expect(page).to have_css('.ci-status-icon')
- expect(page).to have_css('.ci-status-icon-running')
- expect(page).to have_selector('[data-testid="status_running-icon"]')
+ expect(page).to have_selector('[data-testid="status_running_borderless-icon"]')
end
end
end
diff --git a/spec/features/projects/blobs/edit_spec.rb b/spec/features/projects/blobs/edit_spec.rb
index e8a9edcc0cc..9c4f70a68b8 100644
--- a/spec/features/projects/blobs/edit_spec.rb
+++ b/spec/features/projects/blobs/edit_spec.rb
@@ -120,7 +120,7 @@ RSpec.describe 'Editing file blob', :js, feature_category: :groups_and_projects
it 'updates content' do
edit_and_commit
- expect(page).to have_content 'successfully committed'
+ expect(page).to have_content 'committed successfully.'
expect(page).to have_content 'NextFeature'
end
diff --git a/spec/features/projects/branches/user_creates_branch_spec.rb b/spec/features/projects/branches/user_creates_branch_spec.rb
index eafb75d75ac..8d636dacb75 100644
--- a/spec/features/projects/branches/user_creates_branch_spec.rb
+++ b/spec/features/projects/branches/user_creates_branch_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe 'User creates branch', :js, feature_category: :groups_and_project
include Features::BranchesHelpers
let_it_be(:group) { create(:group, :public) }
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
shared_examples 'creates new branch' do
specify do
diff --git a/spec/features/projects/branches_spec.rb b/spec/features/projects/branches_spec.rb
index 79e9ca7998e..7915f446ee0 100644
--- a/spec/features/projects/branches_spec.rb
+++ b/spec/features/projects/branches_spec.rb
@@ -299,13 +299,13 @@ RSpec.describe 'Branches', feature_category: :groups_and_projects do
it 'shows pipeline status when available' do
page.within first('.all-branches li') do
- expect(page).to have_css 'a.gl-badge .ci-status-icon-success'
+ expect(page).to have_css '[data-testid="status_success_borderless-icon"]'
end
end
it 'displays a placeholder when not available' do
page.all('.all-branches li') do |li|
- expect(li).to have_css '.pipeline-status svg.s16'
+ expect(li).to have_css '.pipeline-status svg.s24'
end
end
end
@@ -317,7 +317,7 @@ RSpec.describe 'Branches', feature_category: :groups_and_projects do
it 'does not show placeholder or pipeline status' do
page.all('.all-branches') do |branches|
- expect(branches).not_to have_css '.pipeline-status svg.s16'
+ expect(branches).not_to have_css '.pipeline-status svg.s24'
end
end
end
diff --git a/spec/features/projects/ci/editor_spec.rb b/spec/features/projects/ci/editor_spec.rb
index adaa5e48967..22cc5c67987 100644
--- a/spec/features/projects/ci/editor_spec.rb
+++ b/spec/features/projects/ci/editor_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe 'Pipeline Editor', :js, feature_category: :pipeline_composition do
include Features::SourceEditorSpecHelpers
+ include ListboxHelpers
let(:project) { create(:project_empty_repo, :public) }
let(:user) { create(:user) }
@@ -216,12 +217,13 @@ RSpec.describe 'Pipeline Editor', :js, feature_category: :pipeline_composition d
def switch_to_branch(branch)
# close button for the popover
find('[data-testid="close-button"]').click
- find('[data-testid="branch-selector"]').click
page.within '[data-testid="branch-selector"]' do
- click_button branch
- wait_for_requests
+ toggle_listbox
+ select_listbox_item(branch, exact_text: true)
end
+
+ wait_for_requests
end
before do
diff --git a/spec/features/projects/clusters/gcp_spec.rb b/spec/features/projects/clusters/gcp_spec.rb
index b16f43a16b6..c223053606b 100644
--- a/spec/features/projects/clusters/gcp_spec.rb
+++ b/spec/features/projects/clusters/gcp_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe 'Gcp Cluster', :js, feature_category: :deployment_management do
include GoogleApi::CloudPlatformHelpers
let(:project) { create(:project) }
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
before do
project.add_maintainer(user)
@@ -54,7 +54,7 @@ RSpec.describe 'Gcp Cluster', :js, feature_category: :deployment_management do
before do
visit project_clusters_path(project)
- click_button(class: 'gl-new-dropdown-toggle')
+ click_button(class: 'gl-new-dropdown-toggle', text: 'Connect a cluster (agent)')
click_link 'Connect a cluster (certificate - deprecated)'
end
diff --git a/spec/features/projects/clusters/user_spec.rb b/spec/features/projects/clusters/user_spec.rb
index 1393cc6db15..e256b44c4dc 100644
--- a/spec/features/projects/clusters/user_spec.rb
+++ b/spec/features/projects/clusters/user_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe 'User Cluster', :js, feature_category: :deployment_management do
include GoogleApi::CloudPlatformHelpers
let(:project) { create(:project) }
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
before do
project.add_maintainer(user)
@@ -25,7 +25,7 @@ RSpec.describe 'User Cluster', :js, feature_category: :deployment_management do
before do
visit project_clusters_path(project)
- click_button(class: 'gl-new-dropdown-toggle')
+ click_button(class: 'gl-new-dropdown-toggle', text: 'Connect a cluster (agent)')
click_link 'Connect a cluster (certificate - deprecated)'
end
diff --git a/spec/features/projects/clusters_spec.rb b/spec/features/projects/clusters_spec.rb
index c5d960f2308..d799fbc49ef 100644
--- a/spec/features/projects/clusters_spec.rb
+++ b/spec/features/projects/clusters_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe 'Clusters', :js, feature_category: :groups_and_projects do
include GoogleApi::CloudPlatformHelpers
let(:project) { create(:project) }
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
before do
project.add_maintainer(user)
@@ -125,12 +125,12 @@ RSpec.describe 'Clusters', :js, feature_category: :groups_and_projects do
def visit_create_cluster_page
visit project_clusters_path(project)
- click_button(class: 'gl-new-dropdown-toggle')
+ click_button(class: 'gl-new-dropdown-toggle', text: 'Connect a cluster (agent)')
click_link 'Create a cluster'
end
def visit_connect_cluster_page
- click_button(class: 'gl-new-dropdown-toggle')
+ click_button(class: 'gl-new-dropdown-toggle', text: 'Connect a cluster (agent)')
click_link 'Connect a cluster (certificate - deprecated)'
end
end
diff --git a/spec/features/projects/commit/comments/user_adds_comment_spec.rb b/spec/features/projects/commit/comments/user_adds_comment_spec.rb
index b0cb57f158d..d9225192f6b 100644
--- a/spec/features/projects/commit/comments/user_adds_comment_spec.rb
+++ b/spec/features/projects/commit/comments/user_adds_comment_spec.rb
@@ -112,7 +112,7 @@ RSpec.describe "User adds a comment on a commit", :js, feature_category: :source
click_button("Comment")
end
- expect(page).to have_button("Reply...").and have_no_css("form.new_note")
+ expect(page).to have_css(".reply-placeholder-text-field").and have_no_css("form.new_note")
end
# A comment should be added and visible.
diff --git a/spec/features/projects/commit/mini_pipeline_graph_spec.rb b/spec/features/projects/commit/mini_pipeline_graph_spec.rb
index 5bb3d1af924..c0a8dabf648 100644
--- a/spec/features/projects/commit/mini_pipeline_graph_spec.rb
+++ b/spec/features/projects/commit/mini_pipeline_graph_spec.rb
@@ -52,7 +52,7 @@ RSpec.describe 'Mini Pipeline Graph in Commit View', :js, feature_category: :sou
end
it 'display icon with status' do
- expect(page).to have_selector('.ci-status-icon-running')
+ expect(page).to have_selector('[data-testid="status_running_borderless-icon"]')
end
it 'displays a mini pipeline graph' do
@@ -63,7 +63,7 @@ RSpec.describe 'Mini Pipeline Graph in Commit View', :js, feature_category: :sou
wait_for_requests
page.within '.js-builds-dropdown-list' do
- expect(page).to have_selector('.ci-status-icon-running')
+ expect(page).to have_selector('[data-testid="status_running_borderless-icon"]')
expect(page).to have_content(build.stage_name)
end
diff --git a/spec/features/projects/commit/user_sees_pipelines_tab_spec.rb b/spec/features/projects/commit/user_sees_pipelines_tab_spec.rb
index 00cb5474ea0..5d722ddbedb 100644
--- a/spec/features/projects/commit/user_sees_pipelines_tab_spec.rb
+++ b/spec/features/projects/commit/user_sees_pipelines_tab_spec.rb
@@ -36,9 +36,8 @@ RSpec.describe 'Commit > Pipelines tab', :js, feature_category: :source_code_man
wait_for_requests
page.within('[data-testid="pipeline-table-row"]') do
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Passed')
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Passed')
expect(page).to have_content(pipeline.id)
- expect(page).to have_content('API')
expect(page).to have_css('[data-testid="pipeline-mini-graph"]')
expect(page).to have_css('[data-testid="pipelines-manual-actions-dropdown"]')
expect(page).to have_css('[data-testid="pipeline-multi-actions-dropdown"]')
diff --git a/spec/features/projects/compare_spec.rb b/spec/features/projects/compare_spec.rb
index eff538513c1..ccf5c6996f1 100644
--- a/spec/features/projects/compare_spec.rb
+++ b/spec/features/projects/compare_spec.rb
@@ -185,13 +185,4 @@ RSpec.describe "Compare", :js, feature_category: :groups_and_projects do
it_behaves_like "compare view of branches"
it_behaves_like "compare view of tags"
-
- context "when super sidebar is enabled" do
- before do
- user.update!(use_new_navigation: true)
- end
-
- it_behaves_like "compare view of branches"
- it_behaves_like "compare view of tags"
- end
end
diff --git a/spec/features/projects/confluence/user_views_confluence_page_spec.rb b/spec/features/projects/confluence/user_views_confluence_page_spec.rb
index 216bea74c09..9c036f35887 100644
--- a/spec/features/projects/confluence/user_views_confluence_page_spec.rb
+++ b/spec/features/projects/confluence/user_views_confluence_page_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'User views the Confluence page', feature_category: :integrations do
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
let(:project) { create(:project, :public) }
@@ -11,12 +11,14 @@ RSpec.describe 'User views the Confluence page', feature_category: :integrations
sign_in(user)
end
- it 'shows the page when the Confluence integration is enabled' do
+ it 'shows the page when the Confluence integration is enabled', :js do
service = create(:confluence_integration, project: project)
visit project_wikis_confluence_path(project)
- expect(page).to have_css('.nav-sidebar li.active', text: 'Confluence', match: :first)
+ within_testid('super-sidebar') do
+ expect(page).to have_css('a[aria-current="page"]', text: 'Confluence')
+ end
element = page.find('.row.empty-state')
diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb
index 3a2c7f0ac7b..0a54f5923f2 100644
--- a/spec/features/projects/environments/environments_spec.rb
+++ b/spec/features/projects/environments/environments_spec.rb
@@ -34,16 +34,16 @@ RSpec.describe 'Environments page', :js, feature_category: :continuous_delivery
describe 'with one available environment' do
let!(:environment) { create(:environment, project: project, state: :available) }
- it 'shows "Available" and "Stopped" tab with links' do
+ it 'shows "Active" and "Stopped" tab with links' do
visit_environments(project)
- expect(page).to have_link(_('Available'))
+ expect(page).to have_link(_('Active'))
expect(page).to have_link(_('Stopped'))
end
- describe 'in available tab page' do
+ describe 'in active tab page' do
it 'shows one environment' do
- visit_environments(project, scope: 'available')
+ visit_environments(project, scope: 'active')
expect(page).to have_link(environment.name, href: project_environment_path(project, environment))
end
@@ -56,7 +56,7 @@ RSpec.describe 'Environments page', :js, feature_category: :continuous_delivery
end
it 'renders second page of pipelines' do
- visit_environments(project, scope: 'available')
+ visit_environments(project, scope: 'active')
find('.page-link.next-page-item').click
wait_for_requests
@@ -85,7 +85,7 @@ RSpec.describe 'Environments page', :js, feature_category: :continuous_delivery
end
it 'shows one environment without error' do
- visit_environments(project, scope: 'available')
+ visit_environments(project, scope: 'active')
expect(page).to have_link(environment.name, href: project_environment_path(project, environment))
end
@@ -95,9 +95,9 @@ RSpec.describe 'Environments page', :js, feature_category: :continuous_delivery
describe 'with one stopped environment' do
let!(:environment) { create(:environment, project: project, state: :stopped) }
- describe 'in available tab page' do
+ describe 'in active tab page' do
it 'shows no environments' do
- visit_environments(project, scope: 'available')
+ visit_environments(project, scope: 'active')
expect(page).to have_content(s_('Environments|Get started with environments'))
end
@@ -122,7 +122,7 @@ RSpec.describe 'Environments page', :js, feature_category: :continuous_delivery
it 'does not show environments and tabs' do
expect(page).to have_content(s_('Environments|Get started with environments'))
- expect(page).not_to have_link(_('Available'))
+ expect(page).not_to have_link(_('Active'))
expect(page).not_to have_link(_('Stopped'))
end
end
@@ -142,7 +142,7 @@ RSpec.describe 'Environments page', :js, feature_category: :continuous_delivery
it 'shows environments names and counters' do
expect(page).to have_link(environment.name, href: project_environment_path(project, environment))
- expect(page).to have_link("#{_('Available')} 1")
+ expect(page).to have_link("#{_('Active')} 1")
expect(page).to have_link("#{_('Stopped')} 0")
end
diff --git a/spec/features/projects/features_visibility_spec.rb b/spec/features/projects/features_visibility_spec.rb
index 8f66b722ead..c6a770cee9e 100644
--- a/spec/features/projects/features_visibility_spec.rb
+++ b/spec/features/projects/features_visibility_spec.rb
@@ -3,10 +3,15 @@
require 'spec_helper'
RSpec.describe 'Edit Project Settings', feature_category: :groups_and_projects do
- let(:member) { create(:user, :no_super_sidebar) }
+ let(:member) { create(:user) }
let!(:project) { create(:project, :public, :repository) }
let!(:issue) { create(:issue, project: project) }
- let(:non_member) { create(:user, :no_super_sidebar) }
+ let(:non_member) { create(:user) }
+
+ # Sidebar nav links are only visible after hovering over or expanding the
+ # section that contains them (if it exists). Finding visible and hidden
+ # nav links allows us to avoid doing that.
+ let(:visibility_all) { { visible: :all } }
describe 'project features visibility selectors', :js do
before do
@@ -14,7 +19,7 @@ RSpec.describe 'Edit Project Settings', feature_category: :groups_and_projects d
sign_in(member)
end
- tools = { builds: "pipelines", issues: "issues", wiki: "wiki", snippets: "snippets", merge_requests: "merge_requests", analytics: "analytics" }
+ tools = { builds: "pipelines", issues: "issues", wiki: "wiki", snippets: "snippets", merge_requests: "merge_requests", analytics: "project-cycle-analytics" }
tools.each do |tool_name, shortcut_name|
describe "feature #{tool_name}" do
@@ -22,20 +27,16 @@ RSpec.describe 'Edit Project Settings', feature_category: :groups_and_projects d
visit edit_project_path(project)
# disable by clicking toggle
- toggle_feature_off("project[project_feature_attributes][#{tool_name}_access_level]")
- page.within('.sharing-permissions') do
- find('[data-testid="project-features-save-button"]').click
- end
+ toggle_feature_off(tool_name)
+ click_save_changes
wait_for_requests
- expect(page).not_to have_selector(".shortcuts-#{shortcut_name}")
+ expect(page).not_to have_selector(".shortcuts-#{shortcut_name}", **visibility_all)
# re-enable by clicking toggle again
- toggle_feature_on("project[project_feature_attributes][#{tool_name}_access_level]")
- page.within('.sharing-permissions') do
- find('[data-testid="project-features-save-button"]').click
- end
+ toggle_feature_on(tool_name)
+ click_save_changes
wait_for_requests
- expect(page).to have_selector(".shortcuts-#{shortcut_name}")
+ expect(page).to have_selector(".shortcuts-#{shortcut_name}", **visibility_all)
end
end
end
@@ -48,8 +49,8 @@ RSpec.describe 'Edit Project Settings', feature_category: :groups_and_projects d
visit project_path(project)
- expect(page).to have_selector('.shortcuts-issues')
- expect(page).not_to have_selector('.shortcuts-labels')
+ expect(page).to have_selector('.shortcuts-issues', **visibility_all)
+ expect(page).not_to have_selector('.shortcuts-labels', **visibility_all)
end
end
@@ -65,8 +66,8 @@ RSpec.describe 'Edit Project Settings', feature_category: :groups_and_projects d
it 'hides issues tab' do
visit project_path(project)
- expect(page).not_to have_selector('.shortcuts-issues')
- expect(page).not_to have_selector('.shortcuts-labels')
+ expect(page).not_to have_selector('.shortcuts-issues', **visibility_all)
+ expect(page).not_to have_selector('.shortcuts-labels', **visibility_all)
end
end
@@ -74,7 +75,7 @@ RSpec.describe 'Edit Project Settings', feature_category: :groups_and_projects d
it "shows builds when enabled" do
visit project_pipelines_path(project)
- expect(page).to have_selector(".shortcuts-builds")
+ expect(page).to have_selector(".shortcuts-builds", **visibility_all)
end
it "hides builds when disabled" do
@@ -83,7 +84,7 @@ RSpec.describe 'Edit Project Settings', feature_category: :groups_and_projects d
visit project_pipelines_path(project)
- expect(page).not_to have_selector(".shortcuts-builds")
+ expect(page).not_to have_selector(".shortcuts-builds", **visibility_all)
end
end
end
@@ -183,23 +184,19 @@ RSpec.describe 'Edit Project Settings', feature_category: :groups_and_projects d
end
it "disables repository related features" do
- toggle_feature_off('project[project_feature_attributes][repository_access_level]')
+ toggle_feature_off('repository')
- page.within('.sharing-permissions') do
- click_button "Save changes"
- end
+ click_save_changes
expect(find(".sharing-permissions")).to have_selector(".gl-toggle.is-disabled", minimum: 3)
end
it "shows empty features project homepage" do
- toggle_feature_off('project[project_feature_attributes][repository_access_level]')
- toggle_feature_off('project[project_feature_attributes][issues_access_level]')
- toggle_feature_off('project[project_feature_attributes][wiki_access_level]')
+ toggle_feature_off('repository')
+ toggle_feature_off('issues')
+ toggle_feature_off('wiki')
- page.within('.sharing-permissions') do
- click_button "Save changes"
- end
+ click_save_changes
wait_for_requests
visit project_path(project)
@@ -208,13 +205,11 @@ RSpec.describe 'Edit Project Settings', feature_category: :groups_and_projects d
end
it "hides project activity tabs" do
- toggle_feature_off('project[project_feature_attributes][repository_access_level]')
- toggle_feature_off('project[project_feature_attributes][issues_access_level]')
- toggle_feature_off('project[project_feature_attributes][wiki_access_level]')
+ toggle_feature_off('repository')
+ toggle_feature_off('issues')
+ toggle_feature_off('wiki')
- page.within('.sharing-permissions') do
- click_button "Save changes"
- end
+ click_save_changes
wait_for_requests
visit activity_project_path(project)
@@ -229,7 +224,7 @@ RSpec.describe 'Edit Project Settings', feature_category: :groups_and_projects d
# Regression spec for https://gitlab.com/gitlab-org/gitlab-foss/issues/25272
it "hides comments activity tab only on disabled issues, merge requests and repository" do
- toggle_feature_off('project[project_feature_attributes][issues_access_level]')
+ toggle_feature_off('issues')
save_changes_and_check_activity_tab do
expect(page).to have_content("Comments")
@@ -237,7 +232,7 @@ RSpec.describe 'Edit Project Settings', feature_category: :groups_and_projects d
visit edit_project_path(project)
- toggle_feature_off('project[project_feature_attributes][merge_requests_access_level]')
+ toggle_feature_off('merge_requests')
save_changes_and_check_activity_tab do
expect(page).to have_content("Comments")
@@ -245,7 +240,7 @@ RSpec.describe 'Edit Project Settings', feature_category: :groups_and_projects d
visit edit_project_path(project)
- toggle_feature_off('project[project_feature_attributes][repository_access_level]')
+ toggle_feature_off('repository')
save_changes_and_check_activity_tab do
expect(page).not_to have_content("Comments")
@@ -255,9 +250,7 @@ RSpec.describe 'Edit Project Settings', feature_category: :groups_and_projects d
end
def save_changes_and_check_activity_tab
- page.within('.sharing-permissions') do
- click_button "Save changes"
- end
+ click_save_changes
wait_for_requests
visit activity_project_path(project)
@@ -284,10 +277,16 @@ RSpec.describe 'Edit Project Settings', feature_category: :groups_and_projects d
end
def toggle_feature_off(feature_name)
- find(".project-feature-controls[data-for=\"#{feature_name}\"] .gl-toggle.is-checked").click
+ find(".project-feature-controls[data-for=\"project[project_feature_attributes][#{feature_name}_access_level]\"] .gl-toggle.is-checked").click
end
def toggle_feature_on(feature_name)
- find(".project-feature-controls[data-for=\"#{feature_name}\"] .gl-toggle:not(.is-checked)").click
+ find(".project-feature-controls[data-for=\"project[project_feature_attributes][#{feature_name}_access_level]\"] .gl-toggle:not(.is-checked)").click
+ end
+
+ def click_save_changes
+ page.within('.sharing-permissions') do
+ click_button 'Save changes'
+ end
end
end
diff --git a/spec/features/projects/files/project_owner_creates_license_file_spec.rb b/spec/features/projects/files/project_owner_creates_license_file_spec.rb
index 595aad0144b..18041bbb00a 100644
--- a/spec/features/projects/files/project_owner_creates_license_file_spec.rb
+++ b/spec/features/projects/files/project_owner_creates_license_file_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'Projects > Files > Project owner creates a license file', :js, feature_category: :groups_and_projects do
- let_it_be(:project_maintainer) { create(:user, :no_super_sidebar) }
+ let_it_be(:project_maintainer) { create(:user) }
let_it_be(:project) { create(:project, :repository, namespace: project_maintainer.namespace) }
before do
@@ -59,7 +59,7 @@ RSpec.describe 'Projects > Files > Project owner creates a license file', :js, f
end
def select_template(template)
- page.within('.gl-new-dropdown') do
+ within_testid('template-selector') do
click_button 'Apply a template'
find('.gl-new-dropdown-contents li', text: template).click
wait_for_requests
diff --git a/spec/features/projects/files/user_edits_files_spec.rb b/spec/features/projects/files/user_edits_files_spec.rb
index 10fa4a21359..5612f6a53b2 100644
--- a/spec/features/projects/files/user_edits_files_spec.rb
+++ b/spec/features/projects/files/user_edits_files_spec.rb
@@ -79,6 +79,25 @@ RSpec.describe 'Projects > Files > User edits files', :js, feature_category: :gr
expect(page).to have_content('*.rbca')
end
+ it 'displays a flash message with a link when an edited file was committed' do
+ click_link('.gitignore')
+ edit_in_single_file_editor
+ find('.file-editor', match: :first)
+
+ editor_set_value('*.rbca')
+ fill_in(:commit_message, with: 'New commit message', visible: true)
+ click_button('Commit changes')
+
+ expect(page).to have_current_path(project_blob_path(project, 'master/.gitignore'), ignore_query: true)
+
+ wait_for_requests
+
+ expect(page).to have_content('Your changes have been committed successfully')
+ page.within '.flash-container' do
+ expect(page).to have_link 'changes'
+ end
+ end
+
it 'commits an edited file to a new branch' do
click_link('.gitignore')
edit_in_single_file_editor
diff --git a/spec/features/projects/files/user_find_file_spec.rb b/spec/features/projects/files/user_find_file_spec.rb
index 005a870bea0..b6e739e8082 100644
--- a/spec/features/projects/files/user_find_file_spec.rb
+++ b/spec/features/projects/files/user_find_file_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe 'User find project file', feature_category: :groups_and_projects do
include ListboxHelpers
- let(:user) { create :user, :no_super_sidebar }
+ let(:user) { create :user }
let(:project) { create :project, :repository }
before do
@@ -15,29 +15,25 @@ RSpec.describe 'User find project file', feature_category: :groups_and_projects
visit project_tree_path(project, project.repository.root_ref)
end
- def active_main_tab
- find('.sidebar-top-level-items > li.active')
- end
-
def find_file(text)
fill_in 'file_find', with: text
end
def ref_selector_dropdown
- find('.gl-button-text')
+ find('.ref-selector .gl-button-text')
end
it 'navigates to find file by shortcut', :js do
find('body').native.send_key('t')
- expect(active_main_tab).to have_content('Repository')
+ expect(page).to have_active_sub_navigation('Repository')
expect(page).to have_selector('.file-finder-holder', count: 1)
end
- it 'navigates to find file' do
+ it 'navigates to find file', :js do
click_link 'Find file'
- expect(active_main_tab).to have_content('Repository')
+ expect(page).to have_active_sub_navigation('Repository')
expect(page).to have_selector('.file-finder-holder', count: 1)
end
diff --git a/spec/features/projects/files/user_reads_pipeline_status_spec.rb b/spec/features/projects/files/user_reads_pipeline_status_spec.rb
index ce3f0541139..24dd673501c 100644
--- a/spec/features/projects/files/user_reads_pipeline_status_spec.rb
+++ b/spec/features/projects/files/user_reads_pipeline_status_spec.rb
@@ -25,7 +25,7 @@ RSpec.describe 'user reads pipeline status', :js, feature_category: :groups_and_
page.within('.commit-detail') do
expect(page).to have_link('', href: project_pipeline_path(project, expected_pipeline))
- expect(page).to have_selector(".ci-status-icon-#{expected_pipeline.status}")
+ expect(page).to have_selector("[data-testid='status_#{expected_pipeline.status}_borderless-icon']")
end
end
end
diff --git a/spec/features/projects/files/user_searches_for_files_spec.rb b/spec/features/projects/files/user_searches_for_files_spec.rb
index 627912df408..030d5a8ec40 100644
--- a/spec/features/projects/files/user_searches_for_files_spec.rb
+++ b/spec/features/projects/files/user_searches_for_files_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'Projects > Files > User searches for files', feature_category: :groups_and_projects do
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository, namespace: user.namespace) }
before do
@@ -18,7 +18,7 @@ RSpec.describe 'Projects > Files > User searches for files', feature_category: :
visit project_path(project)
end
- it 'does not show any result' do
+ it 'does not show any result', :js do
submit_search('coffee')
expect(page).to have_content("We couldn't find any")
@@ -41,7 +41,7 @@ RSpec.describe 'Projects > Files > User searches for files', feature_category: :
visit project_tree_path(project, project.default_branch)
end
- it 'shows found files' do
+ it 'shows found files', :js do
expect(page).to have_selector('.tree-controls .shortcuts-find-file')
submit_search('coffee')
diff --git a/spec/features/projects/forks/fork_list_spec.rb b/spec/features/projects/forks/fork_list_spec.rb
index 86e4e03259e..966147637f5 100644
--- a/spec/features/projects/forks/fork_list_spec.rb
+++ b/spec/features/projects/forks/fork_list_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe 'listing forks of a project', feature_category: :groups_and_proje
let(:source) { create(:project, :public, :repository) }
let!(:fork) { fork_project(source, nil, repository: true) }
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
before do
source.add_maintainer(user)
diff --git a/spec/features/projects/graph_spec.rb b/spec/features/projects/graph_spec.rb
index 9b0803e4b0c..e9c05fd7f7f 100644
--- a/spec/features/projects/graph_spec.rb
+++ b/spec/features/projects/graph_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'Project Graph', :js, feature_category: :groups_and_projects do
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
let(:project) { create(:project, :repository, namespace: user.namespace) }
let(:branch_name) { 'master' }
@@ -59,7 +59,7 @@ RSpec.describe 'Project Graph', :js, feature_category: :groups_and_projects do
it 'HTML escapes branch name' do
expect(page.body).to include("Commit statistics for <strong>#{ERB::Util.html_escape(branch_name)}</strong>")
- expect(page.find('.gl-new-dropdown-button-text')['innerHTML']).to include(ERB::Util.html_escape(branch_name))
+ expect(page).to have_button(branch_name)
end
end
diff --git a/spec/features/projects/integrations/user_activates_issue_tracker_spec.rb b/spec/features/projects/integrations/user_activates_issue_tracker_spec.rb
index 9fc91e03c94..944a2c164d5 100644
--- a/spec/features/projects/integrations/user_activates_issue_tracker_spec.rb
+++ b/spec/features/projects/integrations/user_activates_issue_tracker_spec.rb
@@ -38,7 +38,8 @@ RSpec.describe 'User activates issue tracker', :js, feature_category: :integrati
end
it 'shows the link in the menu' do
- page.within('.nav-sidebar') do
+ within_testid('super-sidebar') do
+ click_button 'Plan'
expect(page).to have_link(tracker, href: url)
end
end
@@ -77,7 +78,8 @@ RSpec.describe 'User activates issue tracker', :js, feature_category: :integrati
end
it 'does not show the external tracker link in the menu' do
- page.within('.nav-sidebar') do
+ within_testid('super-sidebar') do
+ click_button 'Plan'
expect(page).not_to have_link(tracker, href: url)
end
end
diff --git a/spec/features/projects/integrations/user_activates_jira_spec.rb b/spec/features/projects/integrations/user_activates_jira_spec.rb
index 0bd5020e9bf..cc0d4c6f564 100644
--- a/spec/features/projects/integrations/user_activates_jira_spec.rb
+++ b/spec/features/projects/integrations/user_activates_jira_spec.rb
@@ -25,10 +25,11 @@ RSpec.describe 'User activates Jira', :js, feature_category: :integrations do
unless Gitlab.ee?
it 'adds Jira link to sidebar menu' do
- page.within('.nav-sidebar') do
- expect(page).not_to have_link('Jira issues', visible: false)
- expect(page).not_to have_link('Open Jira', href: url, visible: false)
- expect(page).to have_link('Jira', href: url)
+ within_testid('super-sidebar') do
+ click_button 'Plan'
+ expect(page).not_to have_link('Jira issues')
+ expect(page).not_to have_link('Open Jira')
+ expect(page).to have_link(exact_text: 'Jira', href: url)
end
end
end
@@ -76,8 +77,9 @@ RSpec.describe 'User activates Jira', :js, feature_category: :integrations do
end
it 'does not show the Jira link in the menu' do
- page.within('.nav-sidebar') do
- expect(page).not_to have_link('Jira', href: url)
+ within_testid('super-sidebar') do
+ click_button 'Plan'
+ expect(page).not_to have_link('Jira')
end
end
end
diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb
index 9ba4b544191..bc67cdbfad1 100644
--- a/spec/features/projects/issuable_templates_spec.rb
+++ b/spec/features/projects/issuable_templates_spec.rb
@@ -64,6 +64,27 @@ RSpec.describe 'issuable templates', :js, feature_category: :groups_and_projects
end
end
+ context 'user creates an issue template using issuable_template query param' do
+ let(:template_content) { 'this is a test "bug" template' }
+
+ before do
+ project.repository.create_file(
+ user,
+ '.gitlab/issue_templates/bug.md',
+ template_content,
+ message: 'added issue template',
+ branch_name: 'master')
+ end
+
+ it 'applies correctly in the rich text editor' do
+ visit new_project_issue_path project
+ click_button "Switch to rich text editing"
+ visit new_project_issue_path(project, { issuable_template: 'bug' })
+
+ expect(page).to have_content(template_content)
+ end
+ end
+
context 'user creates an issue using templates, with a prior description' do
let(:prior_description) { 'test issue description' }
let(:template_content) { 'this is a test "bug" template' }
diff --git a/spec/features/projects/issues/email_participants_spec.rb b/spec/features/projects/issues/email_participants_spec.rb
index 215c45351c1..e1b8133a10f 100644
--- a/spec/features/projects/issues/email_participants_spec.rb
+++ b/spec/features/projects/issues/email_participants_spec.rb
@@ -68,18 +68,4 @@ RSpec.describe 'viewing an issue', :js, feature_category: :service_desk do
end
end
end
-
- context 'for feature flags' do
- before do
- sign_in(user)
- end
-
- it 'pushes service_desk_new_note_email_native_attachments feature flag to frontend' do
- stub_feature_flags(service_desk_new_note_email_native_attachments: true)
-
- visit project_issue_path(project, issue)
-
- expect(page).to have_pushed_frontend_feature_flags(serviceDeskNewNoteEmailNativeAttachments: true)
- end
- end
end
diff --git a/spec/features/projects/jobs/user_browses_jobs_spec.rb b/spec/features/projects/jobs/user_browses_jobs_spec.rb
index fc67d7dedcc..115b3dda5b2 100644
--- a/spec/features/projects/jobs/user_browses_jobs_spec.rb
+++ b/spec/features/projects/jobs/user_browses_jobs_spec.rb
@@ -72,7 +72,7 @@ RSpec.describe 'User browses jobs', feature_category: :groups_and_projects do
wait_for_requests
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Canceled')
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Canceled')
expect(page).not_to have_selector('[data-testid="jobs-table-error-alert"]')
end
end
@@ -93,7 +93,7 @@ RSpec.describe 'User browses jobs', feature_category: :groups_and_projects do
wait_for_requests
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Pending')
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Pending')
end
end
@@ -133,7 +133,7 @@ RSpec.describe 'User browses jobs', feature_category: :groups_and_projects do
wait_for_requests
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Pending')
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Pending')
end
it 'unschedules a job successfully' do
@@ -141,7 +141,7 @@ RSpec.describe 'User browses jobs', feature_category: :groups_and_projects do
wait_for_requests
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Manual')
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Manual')
end
end
diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb
index 12ed2558712..050ed4e0e4c 100644
--- a/spec/features/projects/jobs_spec.rb
+++ b/spec/features/projects/jobs_spec.rb
@@ -66,7 +66,7 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state, feature_category: :grou
wait_for_requests
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Passed')
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Passed')
end
it 'shows commit`s data', :js do
diff --git a/spec/features/projects/members/sorting_spec.rb b/spec/features/projects/members/sorting_spec.rb
index 9747d499ae9..94c42c0f098 100644
--- a/spec/features/projects/members/sorting_spec.rb
+++ b/spec/features/projects/members/sorting_spec.rb
@@ -149,7 +149,7 @@ RSpec.describe 'Projects > Members > Sorting', :js, feature_category: :groups_an
def expect_sort_by(text, sort_direction)
within('[data-testid="members-sort-dropdown"]') do
expect(page).to have_css('button[aria-haspopup="menu"]', text: text)
- expect(page).to have_button("Sorting Direction: #{sort_direction == :asc ? 'Ascending' : 'Descending'}")
+ expect(page).to have_button("Sort direction: #{sort_direction == :asc ? 'Ascending' : 'Descending'}")
end
end
end
diff --git a/spec/features/projects/members/user_requests_access_spec.rb b/spec/features/projects/members/user_requests_access_spec.rb
index d1e58ba91f0..e7f99a4048c 100644
--- a/spec/features/projects/members/user_requests_access_spec.rb
+++ b/spec/features/projects/members/user_requests_access_spec.rb
@@ -5,8 +5,8 @@ require 'spec_helper'
RSpec.describe 'Projects > Members > User requests access', :js, feature_category: :groups_and_projects do
include Spec::Support::Helpers::ModalHelpers
- let_it_be(:user) { create(:user, :no_super_sidebar) }
- let_it_be(:maintainer) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:maintainer) { create(:user) }
let_it_be(:project) { create(:project, :public, :repository) }
let(:owner) { project.first_owner }
@@ -54,10 +54,9 @@ RSpec.describe 'Projects > Members > User requests access', :js, feature_categor
expect(project.requesters.exists?(user_id: user)).to be_truthy
- click_link 'Project information'
-
- page.within('.nav-sidebar') do
- click_link('Members')
+ within_testid('super-sidebar') do
+ click_button 'Manage'
+ click_link 'Members'
end
page.within('.content') do
diff --git a/spec/features/projects/milestones/user_interacts_with_labels_spec.rb b/spec/features/projects/milestones/user_interacts_with_labels_spec.rb
index 3742c9f19d8..9c3eaff1545 100644
--- a/spec/features/projects/milestones/user_interacts_with_labels_spec.rb
+++ b/spec/features/projects/milestones/user_interacts_with_labels_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'User interacts with labels', feature_category: :team_planning do
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
let(:project) { create(:project, namespace: user.namespace) }
let(:milestone) { create(:milestone, project: project, title: 'v2.2', description: '# Description header') }
let(:issue1) { create(:issue, project: project, title: 'Bugfix1', milestone: milestone) }
@@ -25,9 +25,7 @@ RSpec.describe 'User interacts with labels', feature_category: :team_planning do
it 'shows the list of labels', :js do
click_link('v2.2')
- page.within('.nav-sidebar') do
- page.find(:xpath, "//a[@href='#tab-labels']").click
- end
+ page.find(:xpath, "//a[@href='#tab-labels']").click
expect(page).to have_selector('ul.manage-labels-list')
diff --git a/spec/features/projects/navbar_spec.rb b/spec/features/projects/navbar_spec.rb
index e967c1be3bc..348a661855c 100644
--- a/spec/features/projects/navbar_spec.rb
+++ b/spec/features/projects/navbar_spec.rb
@@ -2,13 +2,13 @@
require 'spec_helper'
-RSpec.describe 'Project navbar', :with_license, feature_category: :groups_and_projects do
+RSpec.describe 'Project navbar', :with_license, :js, feature_category: :groups_and_projects do
include NavbarStructureHelper
include WaitForRequests
include_context 'project navbar structure'
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository, namespace: user.namespace) }
before do
@@ -16,7 +16,7 @@ RSpec.describe 'Project navbar', :with_license, feature_category: :groups_and_pr
stub_config(registry: { enabled: false })
stub_feature_flags(ml_experiment_tracking: false)
- insert_package_nav(_('Deployments'))
+ insert_package_nav
insert_infrastructure_registry_nav
insert_infrastructure_google_cloud_nav
insert_infrastructure_aws_nav
@@ -28,29 +28,13 @@ RSpec.describe 'Project navbar', :with_license, feature_category: :groups_and_pr
end
end
- context 'when value stream is available' do
- before do
- visit project_path(project)
- end
-
- it 'redirects to value stream when Analytics item is clicked' do
- page.within('.sidebar-top-level-items') do
- find('.shortcuts-analytics').click
- end
-
- wait_for_requests
-
- expect(page).to have_current_path(project_cycle_analytics_path(project))
- end
- end
-
context 'when pages are available' do
before do
stub_config(pages: { enabled: true })
insert_after_sub_nav_item(
- _('Releases'),
- within: _('Deployments'),
+ _('Package Registry'),
+ within: _('Deploy'),
new_sub_nav_item_name: _('Pages')
)
@@ -86,7 +70,7 @@ RSpec.describe 'Project navbar', :with_license, feature_category: :groups_and_pr
let_it_be(:harbor_integration) { create(:harbor_integration, project: project) }
before do
- insert_harbor_registry_nav(_('Terraform modules'))
+ insert_harbor_registry_nav(_('AWS'))
visit project_path(project)
end
@@ -98,7 +82,11 @@ RSpec.describe 'Project navbar', :with_license, feature_category: :groups_and_pr
before do
stub_feature_flags(ml_experiment_tracking: true)
- insert_model_experiments_nav(_('Terraform modules'))
+ if Gitlab.ee? # rubocop: disable RSpec/AvoidConditionalStatements
+ insert_model_experiments_nav(_('Merge request analytics'))
+ else
+ insert_model_experiments_nav(_('Repository analytics'))
+ end
visit project_path(project)
end
diff --git a/spec/features/projects/network_graph_spec.rb b/spec/features/projects/network_graph_spec.rb
index eff0335c891..e84bbf382ad 100644
--- a/spec/features/projects/network_graph_spec.rb
+++ b/spec/features/projects/network_graph_spec.rb
@@ -124,12 +124,4 @@ RSpec.describe 'Project Network Graph', :js, feature_category: :groups_and_proje
end
it_behaves_like 'network graph'
-
- context 'when disable_network_graph_notes_count is disabled' do
- before do
- stub_feature_flags(disable_network_graph_notes_count: false)
- end
-
- it_behaves_like 'network graph'
- end
end
diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb
index 926fea24e14..a3cbb86da2c 100644
--- a/spec/features/projects/new_project_spec.rb
+++ b/spec/features/projects/new_project_spec.rb
@@ -9,13 +9,37 @@ RSpec.describe 'New project', :js, feature_category: :groups_and_projects do
stub_application_setting(import_sources: Gitlab::ImportSources.values)
end
+ shared_examples 'shows correct navigation' do
+ context 'for a new top-level project' do
+ it 'shows the "Your work" navigation' do
+ visit new_project_path
+ expect(page).to have_selector(".super-sidebar", text: "Your work")
+ end
+ end
+
+ context 'for a new group project' do
+ let_it_be(:parent_group) { create(:group) }
+
+ before do
+ parent_group.add_owner(user)
+ end
+
+ it 'shows the group sidebar of the parent group' do
+ visit new_project_path(namespace_id: parent_group.id)
+ expect(page).to have_selector(".super-sidebar", text: parent_group.name)
+ end
+ end
+ end
+
context 'as a user' do
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
before do
sign_in(user)
end
+ it_behaves_like 'shows correct navigation'
+
it 'shows the project description field when it should' do
description_label = 'Project description (optional)'
@@ -76,7 +100,9 @@ RSpec.describe 'New project', :js, feature_category: :groups_and_projects do
end
context 'as an admin' do
- let(:user) { create(:admin, :no_super_sidebar) }
+ let(:user) { create(:admin) }
+
+ it_behaves_like 'shows correct navigation'
shared_examples '"New project" page' do
before do
@@ -103,14 +129,6 @@ RSpec.describe 'New project', :js, feature_category: :groups_and_projects do
include_examples '"New project" page'
- context 'when the new navigation is enabled' do
- before do
- user.update!(use_new_navigation: true)
- end
-
- include_examples '"New project" page'
- end
-
shared_examples 'renders importer link' do |params|
context 'with user namespace' do
before do
@@ -566,66 +584,17 @@ RSpec.describe 'New project', :js, feature_category: :groups_and_projects do
let(:provider) { :bitbucket }
context 'as a user' do
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
let(:oauth_config_instructions) { 'To enable importing projects from Bitbucket, ask your GitLab administrator to configure OAuth integration' }
it_behaves_like 'has instructions to enable OAuth'
end
context 'as an admin', :do_not_mock_admin_mode_setting do
- let(:user) { create(:admin, :no_super_sidebar) }
+ let(:user) { create(:admin) }
let(:oauth_config_instructions) { 'To enable importing projects from Bitbucket, as administrator you need to configure OAuth integration' }
it_behaves_like 'has instructions to enable OAuth'
end
end
-
- describe 'sidebar' do
- let_it_be(:user) { create(:user, :no_super_sidebar) }
- let_it_be(:parent_group) { create(:group) }
-
- before do
- parent_group.add_owner(user)
- sign_in(user)
- end
-
- context 'in the current navigation' do
- before do
- user.update!(use_new_navigation: false)
- end
-
- context 'for a new top-level project' do
- it_behaves_like 'a "Your work" page with sidebar and breadcrumbs', :new_project_path, :projects
- end
-
- context 'for a new group project' do
- it 'shows the group sidebar of the parent group' do
- visit new_project_path(namespace_id: parent_group.id)
- expect(page).to have_selector(".nav-sidebar[aria-label=\"Group navigation\"] .context-header[title=\"#{parent_group.name}\"]")
- end
- end
- end
-
- context 'in the new navigation' do
- before do
- parent_group.add_owner(user)
- user.update!(use_new_navigation: true)
- sign_in(user)
- end
-
- context 'for a new top-level project' do
- it 'shows the "Your work" navigation' do
- visit new_project_path
- expect(page).to have_selector(".super-sidebar", text: "Your work")
- end
- end
-
- context 'for a new group project' do
- it 'shows the group sidebar of the parent group' do
- visit new_project_path(namespace_id: parent_group.id)
- expect(page).to have_selector(".super-sidebar", text: parent_group.name)
- end
- end
- end
- end
end
diff --git a/spec/features/projects/pages/user_configures_pages_pipeline_spec.rb b/spec/features/projects/pages/user_configures_pages_pipeline_spec.rb
index baef75ca303..eb7bcb38d38 100644
--- a/spec/features/projects/pages/user_configures_pages_pipeline_spec.rb
+++ b/spec/features/projects/pages/user_configures_pages_pipeline_spec.rb
@@ -15,45 +15,23 @@ RSpec.describe 'Pages edits pages settings', :js, feature_category: :pages do
sign_in(user)
end
- context 'when pipeline wizard feature is enabled' do
- before do
- Feature.enable(:use_pipeline_wizard_for_pages)
- end
-
- context 'when onboarding is not complete' do
- it 'renders onboarding instructions' do
- visit project_pages_path(project)
-
- expect(page).to have_content('Get started with Pages')
- end
- end
-
- context 'when onboarding is complete' do
- before do
- project.mark_pages_onboarding_complete
- end
-
- it 'shows waiting screen' do
- visit project_pages_path(project)
+ context 'when onboarding is not complete' do
+ it 'renders onboarding instructions' do
+ visit project_pages_path(project)
- expect(page).to have_content('Waiting for the Pages Pipeline to complete...')
- end
+ expect(page).to have_content('Get started with GitLab Pages')
end
end
- context 'when pipeline wizard feature is disabled' do
+ context 'when onboarding is complete' do
before do
- Feature.disable(:use_pipeline_wizard_for_pages)
- end
-
- after do
- Feature.enable(:use_pipeline_wizard_for_pages)
+ project.mark_pages_onboarding_complete
end
- it 'shows configure pages instructions' do
+ it 'shows waiting screen' do
visit project_pages_path(project)
- expect(page).to have_content('Configure pages')
+ expect(page).to have_content('Waiting for the Pages Pipeline to complete...')
end
end
end
diff --git a/spec/features/projects/pages/user_edits_settings_spec.rb b/spec/features/projects/pages/user_edits_settings_spec.rb
index 8350214bf99..4ad729a29e1 100644
--- a/spec/features/projects/pages/user_edits_settings_spec.rb
+++ b/spec/features/projects/pages/user_edits_settings_spec.rb
@@ -5,7 +5,7 @@ RSpec.describe 'Pages edits pages settings', :js, feature_category: :pages do
include Spec::Support::Helpers::ModalHelpers
let_it_be_with_reload(:project) { create(:project, :pages_published, pages_https_only: false) }
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
before do
allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
@@ -22,7 +22,7 @@ RSpec.describe 'Pages edits pages settings', :js, feature_category: :pages do
context 'when pages deployed' do
before do
- project.mark_pages_as_deployed
+ create(:pages_deployment, project: project)
end
it 'renders Access pages' do
@@ -85,7 +85,7 @@ RSpec.describe 'Pages edits pages settings', :js, feature_category: :pages do
it 'renders "Pages" tab' do
visit project_pages_path(project)
- page.within '.nav-sidebar' do
+ within_testid 'super-sidebar' do
expect(page).to have_link('Pages')
end
end
@@ -96,7 +96,8 @@ RSpec.describe 'Pages edits pages settings', :js, feature_category: :pages do
it 'renders "Pages" tab' do
visit project_environments_path(project)
- page.within '.nav-sidebar' do
+ within_testid 'super-sidebar' do
+ click_button 'Deploy'
expect(page).to have_link('Pages')
end
end
@@ -110,7 +111,8 @@ RSpec.describe 'Pages edits pages settings', :js, feature_category: :pages do
it 'does not render "Pages" tab' do
visit project_environments_path(project)
- page.within '.nav-sidebar' do
+ within_testid 'super-sidebar' do
+ click_button 'Deploy'
expect(page).not_to have_link('Pages')
end
end
@@ -123,7 +125,7 @@ RSpec.describe 'Pages edits pages settings', :js, feature_category: :pages do
before do
project.namespace.update!(owner: user)
- project.mark_pages_as_deployed
+ create(:pages_deployment, project: project)
end
it 'tries to change the setting' do
@@ -185,7 +187,7 @@ RSpec.describe 'Pages edits pages settings', :js, feature_category: :pages do
describe 'Remove page' do
context 'when pages are deployed' do
before do
- project.mark_pages_as_deployed
+ create(:pages_deployment, project: project)
end
it 'removes the pages', :sidekiq_inline do
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index f042a12884c..fccfe00f593 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -165,7 +165,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
it 'shows a running icon and a cancel action for the running build' do
page.within('#ci-badge-deploy') do
- expect(page).to have_selector('[data-testid="status_running-icon"]')
+ expect(page).to have_selector('[data-testid="status_running_borderless-icon"]')
expect(page).to have_selector('.js-icon-cancel')
expect(page).to have_content('deploy')
end
@@ -187,7 +187,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
it 'shows a preparing icon and a cancel action' do
page.within('#ci-badge-prepare') do
- expect(page).to have_selector('[data-testid="status_preparing-icon"]')
+ expect(page).to have_selector('[data-testid="status_preparing_borderless-icon"]')
expect(page).to have_selector('.js-icon-cancel')
expect(page).to have_content('prepare')
end
@@ -209,7 +209,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
it 'shows the success icon and a retry action for the successful build' do
page.within('#ci-badge-build') do
- expect(page).to have_selector('[data-testid="status_success-icon"]')
+ expect(page).to have_selector('[data-testid="status_success_borderless-icon"]')
expect(page).to have_content('build')
end
@@ -224,7 +224,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
expect(page).not_to have_content('Retry job')
within('[data-testid="pipeline-details-header"]') do
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Running')
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Running')
end
end
end
@@ -238,7 +238,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
it 'shows the scheduled icon and an unschedule action for the delayed job' do
page.within('#ci-badge-delayed-job') do
- expect(page).to have_selector('[data-testid="status_scheduled-icon"]')
+ expect(page).to have_selector('[data-testid="status_scheduled_borderless-icon"]')
expect(page).to have_content('delayed-job')
end
@@ -263,7 +263,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
it 'shows the failed icon and a retry action for the failed build' do
page.within('#ci-badge-test') do
- expect(page).to have_selector('[data-testid="status_failed-icon"]')
+ expect(page).to have_selector('[data-testid="status_failed_borderless-icon"]')
expect(page).to have_content('test')
end
@@ -278,7 +278,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
expect(page).not_to have_content('Retry job')
within('[data-testid="pipeline-details-header"]') do
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Running')
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Running')
end
end
@@ -297,7 +297,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
it 'shows the skipped icon and a play action for the manual build' do
page.within('#ci-badge-manual-build') do
- expect(page).to have_selector('[data-testid="status_manual-icon"]')
+ expect(page).to have_selector('[data-testid="status_manual_borderless-icon"]')
expect(page).to have_content('manual')
end
@@ -312,7 +312,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
expect(page).not_to have_content('Play job')
within('[data-testid="pipeline-details-header"]') do
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Running')
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Running')
end
end
end
@@ -323,7 +323,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
end
it 'shows the success icon and the generic comit status build' do
- expect(page).to have_selector('[data-testid="status_success-icon"]')
+ expect(page).to have_selector('[data-testid="status_success_borderless-icon"]')
expect(page).to have_content('jenkins')
expect(page).to have_link('jenkins', href: 'http://gitlab.com/status')
end
@@ -358,7 +358,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
let(:status) { :success }
it 'does not show the cancel or retry action' do
- expect(page).to have_selector('.ci-status-icon-success')
+ expect(page).to have_selector('[data-testid="status_success_borderless-icon"]')
expect(page).not_to have_selector('button[aria-label="Retry downstream pipeline"]')
expect(page).not_to have_selector('button[aria-label="Cancel downstream pipeline"]')
end
@@ -379,7 +379,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
it 'shows the pipeline as canceled with the retry action' do
expect(page).to have_selector('button[aria-label="Retry downstream pipeline"]')
- expect(page).to have_selector('.ci-status-icon-canceled')
+ expect(page).to have_selector('[data-testid="status_canceled_borderless-icon"]')
end
end
end
@@ -398,7 +398,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
end
it 'shows running pipeline with the cancel action' do
- expect(page).to have_selector('.ci-status-icon-running')
+ expect(page).to have_selector('[data-testid="status_running_borderless-icon"]')
expect(page).to have_selector('button[aria-label="Cancel downstream pipeline"]')
end
end
@@ -418,7 +418,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
end
it 'shows running pipeline with the cancel action' do
- expect(page).to have_selector('.ci-status-icon-running')
+ expect(page).to have_selector('[data-testid="status_running_borderless-icon"]')
expect(page).to have_selector('button[aria-label="Cancel downstream pipeline"]')
end
end
@@ -438,7 +438,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
end
it 'does not show the retry button' do
- expect(page).to have_selector('.ci-status-icon-failed')
+ expect(page).to have_selector('[data-testid="status_failed_borderless-icon"]')
expect(page).not_to have_selector('button[aria-label="Retry downstream pipeline"]')
end
end
@@ -537,7 +537,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
it 'shows running status in pipeline header', :sidekiq_might_not_need_inline do
within('[data-testid="pipeline-details-header"]') do
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Running')
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Running')
end
end
end
@@ -782,8 +782,8 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
expect(page).to have_content('Cancel pipeline')
end
- it 'does not link to job', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/408215' do
- expect(page).not_to have_selector('.js-pipeline-graph-job-link')
+ it 'does link to job' do
+ expect(page).to have_selector('.js-pipeline-graph-job-link')
end
end
end
@@ -900,18 +900,18 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
subject
within('[data-testid="pipeline-details-header"]') do
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Pending')
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Pending')
end
within('.js-pipeline-graph') do
within(all('[data-testid="stage-column"]')[0]) do
expect(page).to have_content('test')
- expect(page).to have_css('.ci-status-icon-pending')
+ expect(page).to have_css('[data-testid="status_pending_borderless-icon"]')
end
within(all('[data-testid="stage-column"]')[1]) do
expect(page).to have_content('deploy')
- expect(page).to have_css('.ci-status-icon-created')
+ expect(page).to have_css('[data-testid="status_created_borderless-icon"]')
end
end
end
@@ -925,18 +925,18 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
subject
within('[data-testid="pipeline-details-header"]') do
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Running')
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Running')
end
within('.js-pipeline-graph') do
within(all('[data-testid="stage-column"]')[0]) do
expect(page).to have_content('test')
- expect(page).to have_css('.ci-status-icon-success')
+ expect(page).to have_css('[data-testid="status_success_borderless-icon"]')
end
within(all('[data-testid="stage-column"]')[1]) do
expect(page).to have_content('deploy')
- expect(page).to have_css('.ci-status-icon-pending')
+ expect(page).to have_css('[data-testid="status_pending_borderless-icon"]')
end
end
end
@@ -954,13 +954,13 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
subject
within('[data-testid="pipeline-details-header"]') do
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Waiting')
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Waiting')
end
within('.js-pipeline-graph') do
within(all('[data-testid="stage-column"]')[1]) do
expect(page).to have_content('deploy')
- expect(page).to have_css('.ci-status-icon-waiting-for-resource')
+ expect(page).to have_css('[data-testid="status_pending_borderless-icon"]')
end
end
end
@@ -974,13 +974,13 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
subject
within('[data-testid="pipeline-details-header"]') do
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Running')
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Running')
end
within('.js-pipeline-graph') do
within(all('[data-testid="stage-column"]')[1]) do
expect(page).to have_content('deploy')
- expect(page).to have_css('.ci-status-icon-pending')
+ expect(page).to have_css('[data-testid="status_pending_borderless-icon"]')
end
end
end
@@ -1002,13 +1002,13 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
subject
within('[data-testid="pipeline-details-header"]') do
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Waiting')
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Waiting')
end
within('.js-pipeline-graph') do
within(all('[data-testid="stage-column"]')[1]) do
expect(page).to have_content('deploy')
- expect(page).to have_css('.ci-status-icon-waiting-for-resource')
+ expect(page).to have_css('[data-testid="status_pending_borderless-icon"]')
end
end
end
diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
index ca3b7f0ad47..30d3303dfbb 100644
--- a/spec/features/projects/pipelines/pipelines_spec.rb
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :continuous_integration do
let(:expected_detached_mr_tag) { 'merge request' }
context 'when user is logged in' do
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
before do
sign_in(user)
@@ -115,7 +115,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :continuous_integration do
it 'indicates that pipeline can be canceled' do
expect(page).to have_selector('.js-pipelines-cancel-button')
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Running')
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Running')
end
context 'when canceling' do
@@ -127,7 +127,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :continuous_integration do
it 'indicated that pipelines was canceled', :sidekiq_might_not_need_inline do
expect(page).not_to have_selector('.js-pipelines-cancel-button')
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Canceled')
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Canceled')
end
end
end
@@ -144,7 +144,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :continuous_integration do
it 'indicates that pipeline can be retried' do
expect(page).to have_selector('.js-pipelines-retry-button')
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Failed')
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Failed')
end
context 'when retrying' do
@@ -155,7 +155,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :continuous_integration do
it 'shows running pipeline that is not retryable' do
expect(page).not_to have_selector('.js-pipelines-retry-button')
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Running')
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Running')
end
end
end
@@ -183,7 +183,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :continuous_integration do
within '.pipeline-tags' do
expect(page).to have_content(expected_detached_mr_tag)
- expect(page).to have_link(merge_request.iid, href: project_merge_request_path(project, merge_request))
+ expect(page).to have_link(merge_request.iid.to_s, href: project_merge_request_path(project, merge_request))
expect(page).not_to have_link(pipeline.ref)
end
@@ -223,7 +223,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :continuous_integration do
within '.pipeline-tags' do
expect(page).not_to have_content(expected_detached_mr_tag)
- expect(page).to have_link(merge_request.iid, href: project_merge_request_path(project, merge_request))
+ expect(page).to have_link(merge_request.iid.to_s, href: project_merge_request_path(project, merge_request))
expect(page).not_to have_link(pipeline.ref)
end
@@ -396,7 +396,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :continuous_integration do
end
it 'shows the pipeline as preparing' do
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Preparing')
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Preparing')
end
end
@@ -417,7 +417,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :continuous_integration do
end
it 'has pipeline running' do
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Running')
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Running')
end
context 'when canceling' do
@@ -428,7 +428,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :continuous_integration do
it 'indicates that pipeline was canceled', :sidekiq_might_not_need_inline do
expect(page).not_to have_selector('.js-pipelines-cancel-button')
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Canceled')
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Canceled')
end
end
end
@@ -450,7 +450,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :continuous_integration do
end
it 'has failed pipeline', :sidekiq_might_not_need_inline do
- expect(page).to have_selector('[data-testid="ci-badge-link"]', text: 'Failed')
+ expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Failed')
end
end
end
@@ -650,7 +650,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :continuous_integration do
# header
expect(page).to have_text("##{pipeline.id}")
- expect(page).to have_link(pipeline.user.name, href: user_path(pipeline.user))
+ expect(page).to have_link(pipeline.user.name, href: /#{user_path(pipeline.user)}$/)
# stages
expect(page).to have_text('build')
@@ -805,29 +805,12 @@ RSpec.describe 'Pipelines', :js, feature_category: :continuous_integration do
describe 'Empty State' do
let(:project) { create(:project, :repository) }
- context 'when `ios_specific_templates` is not enabled' do
- before do
- visit project_pipelines_path(project)
- end
-
- it 'renders empty state' do
- expect(page).to have_content 'Try test template'
- end
+ before do
+ visit project_pipelines_path(project)
end
- describe 'when the `ios_specific_templates` experiment is enabled and the "Set up a runner" button is clicked' do
- before do
- stub_experiments(ios_specific_templates: :candidate)
- project.project_setting.update!(target_platforms: %w[ios])
- visit project_pipelines_path(project)
- click_button 'Set up a runner'
- end
-
- it 'displays a modal with the macOS platform selected and an explanation popover' do
- expect(page).to have_button 'macOS', class: 'selected'
- expect(page).to have_selector('#runner-instructions-modal___BV_modal_content_')
- expect(page).to have_selector('.popover')
- end
+ it 'renders empty state' do
+ expect(page).to have_content 'Try test template'
end
end
end
diff --git a/spec/features/projects/releases/user_creates_release_spec.rb b/spec/features/projects/releases/user_creates_release_spec.rb
index 678c8df666f..319053ddcb5 100644
--- a/spec/features/projects/releases/user_creates_release_spec.rb
+++ b/spec/features/projects/releases/user_creates_release_spec.rb
@@ -148,8 +148,7 @@ RSpec.describe 'User creates release', :js, feature_category: :continuous_delive
fill_release_title(release_title)
- select_milestone(milestone_1.title)
- select_milestone(milestone_2.title)
+ select_milestones(milestone_1.title, milestone_2.title)
fill_release_notes(release_notes)
diff --git a/spec/features/projects/releases/user_views_edit_release_spec.rb b/spec/features/projects/releases/user_views_edit_release_spec.rb
index b3f21a2d328..203b4ce82e3 100644
--- a/spec/features/projects/releases/user_views_edit_release_spec.rb
+++ b/spec/features/projects/releases/user_views_edit_release_spec.rb
@@ -20,8 +20,8 @@ RSpec.describe 'User edits Release', :js, feature_category: :continuous_delivery
end
def fill_out_form_and_click(button_to_click)
- fill_in 'Release title', with: 'Updated Release title'
- fill_in 'Release notes', with: 'Updated Release notes'
+ fill_in 'release-title', with: 'Updated Release title', fill_options: { clear: :backspace }
+ fill_in 'release-notes', with: 'Updated Release notes'
click_link_or_button button_to_click
@@ -44,8 +44,8 @@ RSpec.describe 'User edits Release', :js, feature_category: :continuous_delivery
expect(page).to have_content('Releases are based on Git tags. We recommend tags that use semantic versioning, for example 1.0.0, 2.1.0-pre.')
expect(find_field('Tag name', disabled: true).value).to eq(release.tag)
- expect(find_field('Release title').value).to eq(release.name)
- expect(find_field('Release notes').value).to eq(release.description)
+ expect(find_field('release-title').value).to eq(release.name)
+ expect(find_field('release-notes').value).to eq(release.description)
expect(page).to have_button('Save changes')
expect(page).to have_link('Cancel')
diff --git a/spec/features/projects/settings/monitor_settings_spec.rb b/spec/features/projects/settings/monitor_settings_spec.rb
index c2914c020e3..fca10d9c0b0 100644
--- a/spec/features/projects/settings/monitor_settings_spec.rb
+++ b/spec/features/projects/settings/monitor_settings_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe 'Projects > Settings > For a forked project', :js, feature_category: :groups_and_projects do
include ListboxHelpers
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository, create_templates: :issue, namespace: user.namespace) }
before do
@@ -15,13 +15,10 @@ RSpec.describe 'Projects > Settings > For a forked project', :js, feature_catego
describe 'Sidebar > Monitor' do
it 'renders the menu in the sidebar' do
visit project_path(project)
- wait_for_requests
- expect(page).to have_selector(
- '.sidebar-sub-level-items a[aria-label="Error Tracking"]',
- text: 'Error Tracking',
- visible: :hidden
- )
+ within_testid('super-sidebar') do
+ expect(page).to have_link('Error Tracking', visible: :hidden)
+ end
end
end
diff --git a/spec/features/projects/settings/registry_settings_cleanup_tags_spec.rb b/spec/features/projects/settings/registry_settings_cleanup_tags_spec.rb
index ee54065fdf8..1b53a6222e6 100644
--- a/spec/features/projects/settings/registry_settings_cleanup_tags_spec.rb
+++ b/spec/features/projects/settings/registry_settings_cleanup_tags_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe 'Project > Settings > Packages and registries > Container registry tag expiration policy',
feature_category: :groups_and_projects do
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
let_it_be(:project, reload: true) { create(:project, namespace: user.namespace) }
let(:container_registry_enabled) { true }
@@ -24,9 +24,10 @@ RSpec.describe 'Project > Settings > Packages and registries > Container registr
it 'shows active tab on sidebar' do
subject
- expect(find('.sidebar-top-level-items > li.active')).to have_content('Settings')
- expect(find('.sidebar-sub-level-items > li.active:not(.fly-out-top-item)'))
- .to have_content('Packages and registries')
+ within_testid('super-sidebar') do
+ expect(page).to have_selector('button[aria-expanded="true"]', text: 'Settings')
+ expect(page).to have_selector('[aria-current="page"]', text: 'Packages and registries')
+ end
end
it 'shows available section' do
@@ -44,7 +45,7 @@ RSpec.describe 'Project > Settings > Packages and registries > Container registr
wait_for_requests
expect(page).to be_axe_clean.within('[data-testid="container-expiration-policy-project-settings"]')
- .skipping :'link-in-text-block'
+ .skipping :'link-in-text-block', :'heading-order'
end
it 'saves cleanup policy submit the form' do
diff --git a/spec/features/projects/settings/registry_settings_spec.rb b/spec/features/projects/settings/registry_settings_spec.rb
index 7f0367f47f7..d6e08628721 100644
--- a/spec/features/projects/settings/registry_settings_spec.rb
+++ b/spec/features/projects/settings/registry_settings_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe 'Project > Settings > Packages and registries > Container registry tag expiration policy',
feature_category: :groups_and_projects do
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
let_it_be(:project, reload: true) { create(:project, namespace: user.namespace) }
let(:container_registry_enabled) { true }
@@ -27,15 +27,16 @@ RSpec.describe 'Project > Settings > Packages and registries > Container registr
wait_for_requests
expect(page).to be_axe_clean.within('[data-testid="packages-and-registries-project-settings"]')
- .skipping :'link-in-text-block'
+ .skipping :'link-in-text-block', :'heading-order'
end
it 'shows active tab on sidebar' do
subject
- expect(find('.sidebar-top-level-items > li.active')).to have_content('Settings')
- expect(find('.sidebar-sub-level-items > li.active:not(.fly-out-top-item)'))
- .to have_content('Packages and registries')
+ within_testid('super-sidebar') do
+ expect(page).to have_selector('button[aria-expanded="true"]', text: 'Settings')
+ expect(page).to have_selector('[aria-current="page"]', text: 'Packages and registries')
+ end
end
it 'shows available section' do
diff --git a/spec/features/projects/show/user_sees_collaboration_links_spec.rb b/spec/features/projects/show/user_sees_collaboration_links_spec.rb
index 626d4de7baf..f231b4a591a 100644
--- a/spec/features/projects/show/user_sees_collaboration_links_spec.rb
+++ b/spec/features/projects/show/user_sees_collaboration_links_spec.rb
@@ -6,14 +6,14 @@ RSpec.describe 'Projects > Show > Collaboration links', :js, feature_category: :
using RSpec::Parameterized::TableSyntax
let_it_be(:project) { create(:project, :repository, :public) }
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
before do
sign_in(user)
end
def find_new_menu_toggle
- find('#js-onboarding-new-project-link')
+ find('[data-testid="base-dropdown-toggle"]', text: 'Create new...')
end
context 'with developer user' do
@@ -25,7 +25,7 @@ RSpec.describe 'Projects > Show > Collaboration links', :js, feature_category: :
visit project_path(project)
# The navigation bar
- page.within('.header-new') do
+ within_testid('super-sidebar') do
find_new_menu_toggle.click
aggregate_failures 'dropdown links in the navigation bar' do
@@ -60,7 +60,7 @@ RSpec.describe 'Projects > Show > Collaboration links', :js, feature_category: :
visit project_path(project)
- page.within('.header-new') do
+ within_testid('super-sidebar') do
find_new_menu_toggle.click
aggregate_failures 'dropdown links' do
diff --git a/spec/features/projects/snippets/show_spec.rb b/spec/features/projects/snippets/show_spec.rb
index 12018b4b9d7..e5836739c57 100644
--- a/spec/features/projects/snippets/show_spec.rb
+++ b/spec/features/projects/snippets/show_spec.rb
@@ -3,35 +3,62 @@
require 'spec_helper'
RSpec.describe 'Projects > Snippets > Project snippet', :js, feature_category: :source_code_management do
- let_it_be(:user) { create(:user) }
+ let_it_be(:author) { create(:author) }
let_it_be(:project) do
- create(:project, creator: user).tap do |p|
- p.add_maintainer(user)
+ create(:project, :public, creator: author).tap do |p|
+ p.add_maintainer(author)
end
end
- let_it_be(:snippet) { create(:project_snippet, :repository, project: project, author: user) }
+ let_it_be(:snippet) { create(:project_snippet, :public, :repository, project: project, author: author) }
+ let(:anchor) { nil }
+ let(:file_path) { 'files/ruby/popen.rb' }
+
+ def visit_page
+ visit project_snippet_path(project, snippet, anchor: anchor)
+ end
before do
- sign_in(user)
+ # rubocop: disable RSpec/AnyInstanceOf -- TODO: The usage of let_it_be forces us
+ allow_any_instance_of(Snippet).to receive(:blobs)
+ .and_return([snippet.repository.blob_at('master', file_path)])
+ # rubocop: enable RSpec/AnyInstanceOf
end
- it_behaves_like 'show and render proper snippet blob' do
- let(:anchor) { nil }
+ context 'when signed in' do
+ before do
+ sign_in(user)
+ visit_page
+ end
- subject do
- visit project_snippet_path(project, snippet, anchor: anchor)
+ context 'as project member' do
+ let(:user) { author }
- wait_for_requests
+ it_behaves_like 'show and render proper snippet blob'
+ it_behaves_like 'does show New Snippet button'
end
- end
- # it_behaves_like 'showing user status' do
- # This will be handled in https://gitlab.com/gitlab-org/gitlab/-/issues/262394
+ context 'as external user' do
+ let_it_be(:user) { create(:user, :external) }
+
+ it_behaves_like 'show and render proper snippet blob'
+ it_behaves_like 'does not show New Snippet button'
+ end
- it_behaves_like 'does not show New Snippet button' do
- let(:file_path) { 'files/ruby/popen.rb' }
+ context 'as another user' do
+ let_it_be(:user) { create(:user) }
+
+ it_behaves_like 'show and render proper snippet blob'
+ it_behaves_like 'does not show New Snippet button'
+ end
+ end
+
+ context 'when unauthenticated' do
+ before do
+ visit_page
+ end
- subject { visit project_snippet_path(project, snippet) }
+ it_behaves_like 'show and render proper snippet blob'
+ it_behaves_like 'does not show New Snippet button'
end
end
diff --git a/spec/features/projects/tree/tree_show_spec.rb b/spec/features/projects/tree/tree_show_spec.rb
index 3becc48d450..4a913d82a78 100644
--- a/spec/features/projects/tree/tree_show_spec.rb
+++ b/spec/features/projects/tree/tree_show_spec.rb
@@ -21,6 +21,15 @@ RSpec.describe 'Projects tree', :js, feature_category: :web_ide do
sign_in(user)
end
+ it 'passes axe automated accessibility testing' do
+ visit project_tree_path(project, test_sha)
+ wait_for_requests
+
+ expect(page).to be_axe_clean.within('.project-last-commit')
+ expect(page).to be_axe_clean.within('.nav-block')
+ expect(page).to be_axe_clean.within('.tree-content-holder').skipping :'link-in-text-block'
+ end
+
it 'renders tree table without errors' do
visit project_tree_path(project, test_sha)
wait_for_requests
@@ -111,9 +120,16 @@ RSpec.describe 'Projects tree', :js, feature_category: :web_ide do
end
context 'LFS' do
- it 'renders LFS badge on blob item' do
+ before do
visit project_tree_path(project, File.join('master', 'files/lfs'))
+ wait_for_requests
+ end
+ it 'passes axe automated accessibility testing' do
+ expect(page).to be_axe_clean.within('.tree-content-holder').skipping :'link-in-text-block'
+ end
+
+ it 'renders LFS badge on blob item' do
expect(page).to have_selector('[data-testid="label-lfs"]', text: 'LFS')
end
end
diff --git a/spec/features/projects/user_sees_sidebar_spec.rb b/spec/features/projects/user_sees_sidebar_spec.rb
index 22d00e9a351..61225b45760 100644
--- a/spec/features/projects/user_sees_sidebar_spec.rb
+++ b/spec/features/projects/user_sees_sidebar_spec.rb
@@ -2,8 +2,8 @@
require 'spec_helper'
-RSpec.describe 'Projects > User sees sidebar', feature_category: :groups_and_projects do
- let(:user) { create(:user, :no_super_sidebar) }
+RSpec.describe 'Projects > User sees sidebar', :js, feature_category: :groups_and_projects do
+ let(:user) { create(:user) }
let(:project) { create(:project, :private, public_builds: false, namespace: user.namespace) }
# NOTE: See documented behaviour https://design.gitlab.com/regions/navigation#contextual-navigation
@@ -14,44 +14,25 @@ RSpec.describe 'Projects > User sees sidebar', feature_category: :groups_and_pro
sign_in(user)
end
- shared_examples 'has a expanded nav sidebar' do
- it 'has a expanded desktop nav-sidebar on load' do
- expect(page).to have_content('Collapse sidebar')
- expect(page).not_to have_selector('.sidebar-collapsed-desktop')
- expect(page).not_to have_selector('.sidebar-expanded-mobile')
+ shared_examples 'has an expanded nav sidebar' do
+ it 'has an expanded nav sidebar on load' do
+ expect(page).to have_selector('[data-testid="super-sidebar-collapse-button"]', visible: :visible)
end
- it 'can collapse the nav-sidebar' do
- page.find('.nav-sidebar .js-toggle-sidebar').click
- expect(page).to have_selector('.sidebar-collapsed-desktop')
- expect(page).not_to have_content('Collapse sidebar')
- expect(page).not_to have_selector('.sidebar-expanded-mobile')
+ it 'can collapse the nav sidebar' do
+ find_by_testid('super-sidebar-collapse-button').click
+ expect(page).to have_selector('[data-testid="super-sidebar-collapse-button"]', visible: :hidden)
end
end
shared_examples 'has a collapsed nav sidebar' do
- it 'has a collapsed desktop nav-sidebar on load' do
- expect(page).not_to have_content('Collapse sidebar')
- expect(page).not_to have_selector('.sidebar-expanded-mobile')
+ it 'has a collapsed nav sidebar on load' do
+ expect(page).to have_selector('[data-testid="super-sidebar-collapse-button"]', visible: :hidden)
end
- it 'can expand the nav-sidebar' do
- page.find('.nav-sidebar .js-toggle-sidebar').click
- expect(page).to have_selector('.sidebar-expanded-mobile')
- expect(page).to have_content('Collapse sidebar')
- end
- end
-
- shared_examples 'has a mobile nav-sidebar' do
- it 'has a hidden nav-sidebar on load' do
- expect(page).not_to have_content('.mobile-nav-open')
- expect(page).not_to have_selector('.sidebar-expanded-mobile')
- end
-
- it 'can expand the nav-sidebar' do
- page.find('.toggle-mobile-nav').click
- expect(page).to have_selector('.mobile-nav-open')
- expect(page).to have_selector('.sidebar-expanded-mobile')
+ it 'can expand the nav sidebar' do
+ page.find('.js-super-sidebar-toggle-expand').click
+ expect(page).to have_selector('[data-testid="super-sidebar-collapse-button"]', visible: :visible)
end
end
@@ -59,29 +40,24 @@ RSpec.describe 'Projects > User sees sidebar', feature_category: :groups_and_pro
before do
resize_screen_xs
visit project_path(project)
- expect(page).to have_selector('.nav-sidebar')
- expect(page).to have_selector('.toggle-mobile-nav')
end
- it_behaves_like 'has a mobile nav-sidebar'
+ it_behaves_like 'has a collapsed nav sidebar'
end
context 'with a small size viewport' do
before do
resize_screen_sm
visit project_path(project)
- expect(page).to have_selector('.nav-sidebar')
- expect(page).to have_selector('.toggle-mobile-nav')
end
- it_behaves_like 'has a mobile nav-sidebar'
+ it_behaves_like 'has a collapsed nav sidebar'
end
context 'with medium size viewport' do
before do
resize_window(768, 800)
visit project_path(project)
- expect(page).to have_selector('.nav-sidebar')
end
it_behaves_like 'has a collapsed nav sidebar'
@@ -91,7 +67,6 @@ RSpec.describe 'Projects > User sees sidebar', feature_category: :groups_and_pro
before do
resize_window(1199, 800)
visit project_path(project)
- expect(page).to have_selector('.nav-sidebar')
end
it_behaves_like 'has a collapsed nav sidebar'
@@ -101,10 +76,9 @@ RSpec.describe 'Projects > User sees sidebar', feature_category: :groups_and_pro
before do
resize_window(1200, 800)
visit project_path(project)
- expect(page).to have_selector('.nav-sidebar')
end
- it_behaves_like 'has a expanded nav sidebar'
+ it_behaves_like 'has an expanded nav sidebar'
end
end
@@ -121,8 +95,8 @@ RSpec.describe 'Projects > User sees sidebar', feature_category: :groups_and_pro
it 'does not display a "Snippets" link' do
visit project_path(project)
- within('.nav-sidebar') do
- expect(page).not_to have_content 'Snippets'
+ within_testid('super-sidebar') do
+ expect(page).not_to have_button 'Code'
end
end
end
@@ -182,7 +156,7 @@ RSpec.describe 'Projects > User sees sidebar', feature_category: :groups_and_pro
end
context 'as guest' do
- let(:guest) { create(:user, :no_super_sidebar) }
+ let(:guest) { create(:user) }
let!(:issue) { create(:issue, :opened, project: project, author: guest) }
before do
@@ -194,15 +168,19 @@ RSpec.describe 'Projects > User sees sidebar', feature_category: :groups_and_pro
it 'shows allowed tabs only' do
visit project_path(project)
- within('.nav-sidebar') do
- expect(page).to have_content 'Project'
- expect(page).to have_content 'Issues'
- expect(page).to have_content 'Wiki'
- expect(page).to have_content 'Monitor'
+ within_testid('super-sidebar') do
+ expect(page).to have_button 'Pinned'
+ expect(page).to have_button 'Manage'
+ expect(page).to have_button 'Plan'
+ expect(page).to have_button 'Code'
+ expect(page).to have_button 'Monitor'
+ expect(page).to have_button 'Analyze'
+
+ expect(page).not_to have_button 'Build'
- expect(page).not_to have_content 'Repository'
- expect(page).not_to have_content 'CI/CD'
- expect(page).not_to have_content 'Merge Requests'
+ click_button 'Code'
+ expect(page).not_to have_link 'Repository'
+ expect(page).not_to have_link 'Merge requests'
end
end
@@ -212,8 +190,8 @@ RSpec.describe 'Projects > User sees sidebar', feature_category: :groups_and_pro
visit project_path(project)
- within('.nav-sidebar') do
- expect(page).to have_content 'CI/CD'
+ within_testid('super-sidebar') do
+ expect(page).to have_button 'Build'
end
end
diff --git a/spec/features/projects/user_uses_shortcuts_spec.rb b/spec/features/projects/user_uses_shortcuts_spec.rb
index b7b2093d78a..a000c9e1da8 100644
--- a/spec/features/projects/user_uses_shortcuts_spec.rb
+++ b/spec/features/projects/user_uses_shortcuts_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'User uses shortcuts', :js, feature_category: :groups_and_projects do
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository, namespace: user.namespace) }
before do
@@ -21,14 +21,14 @@ RSpec.describe 'User uses shortcuts', :js, feature_category: :groups_and_project
find('body').native.send_key('g')
find('body').native.send_key('o')
- expect(page).to have_active_navigation(project.name)
+ expect(page).to have_active_sub_navigation(project.name)
end
it 'redirects to the activity page' do
find('body').native.send_key('g')
find('body').native.send_key('v')
- expect(page).to have_active_navigation('Project')
+ expect(page).to have_active_navigation('Manage')
expect(page).to have_active_sub_navigation('Activity')
end
end
@@ -38,31 +38,39 @@ RSpec.describe 'User uses shortcuts', :js, feature_category: :groups_and_project
find('body').native.send_key('g')
find('body').native.send_key('f')
- expect(page).to have_active_navigation('Repository')
- expect(page).to have_active_sub_navigation('Files')
+ expect(page).to have_active_navigation('Code')
+ expect(page).to have_active_sub_navigation('Repository')
end
- it 'redirects to the repository commits page' do
- find('body').native.send_key('g')
- find('body').native.send_key('c')
+ context 'when hitting the commits controller' do
+ # Hitting the commits controller with the super sidebar enabled seems to trigger more SQL
+ # queries, exceeding the 100 limit. We need to increase the limit a bit for these tests to pass.
+ before do
+ allow(Gitlab::QueryLimiting::Transaction).to receive(:threshold).and_return(110)
+ end
+
+ it 'redirects to the repository commits page' do
+ find('body').native.send_key('g')
+ find('body').native.send_key('c')
- expect(page).to have_active_navigation('Repository')
- expect(page).to have_active_sub_navigation('Commits')
+ expect(page).to have_active_navigation('Code')
+ expect(page).to have_active_sub_navigation('Commits')
+ end
end
it 'redirects to the repository graph page' do
find('body').native.send_key('g')
find('body').native.send_key('n')
- expect(page).to have_active_navigation('Repository')
- expect(page).to have_active_sub_navigation('Graph')
+ expect(page).to have_active_navigation('Code')
+ expect(page).to have_active_sub_navigation('Repository graph')
end
it 'redirects to the repository charts page' do
find('body').native.send_key('g')
find('body').native.send_key('d')
- expect(page).to have_active_navigation(_('Analytics'))
+ expect(page).to have_active_navigation(_('Analyze'))
expect(page).to have_active_sub_navigation(_('Repository'))
end
end
@@ -72,16 +80,16 @@ RSpec.describe 'User uses shortcuts', :js, feature_category: :groups_and_project
find('body').native.send_key('g')
find('body').native.send_key('i')
- expect(page).to have_active_navigation('Issues')
- expect(page).to have_active_sub_navigation('List')
+ expect(page).to have_active_navigation('Pinned')
+ expect(page).to have_active_sub_navigation('Issues')
end
it 'redirects to the issue board page' do
find('body').native.send_key('g')
find('body').native.send_key('b')
- expect(page).to have_active_navigation('Issues')
- expect(page).to have_active_sub_navigation('Board')
+ expect(page).to have_active_navigation('Plan')
+ expect(page).to have_active_sub_navigation('Issue boards')
end
it 'redirects to the new issue page' do
@@ -97,7 +105,8 @@ RSpec.describe 'User uses shortcuts', :js, feature_category: :groups_and_project
find('body').native.send_key('g')
find('body').native.send_key('m')
- expect(page).to have_active_navigation('Merge requests')
+ expect(page).to have_active_navigation('Pinned')
+ expect(page).to have_active_sub_navigation('Merge requests')
end
end
@@ -106,7 +115,7 @@ RSpec.describe 'User uses shortcuts', :js, feature_category: :groups_and_project
find('body').native.send_key('g')
find('body').native.send_key('p')
- expect(page).to have_active_navigation('CI/CD')
+ expect(page).to have_active_navigation('Build')
expect(page).to have_active_sub_navigation('Pipelines')
end
@@ -114,7 +123,7 @@ RSpec.describe 'User uses shortcuts', :js, feature_category: :groups_and_project
find('body').native.send_key('g')
find('body').native.send_key('j')
- expect(page).to have_active_navigation('CI/CD')
+ expect(page).to have_active_navigation('Build')
expect(page).to have_active_sub_navigation('Jobs')
end
end
@@ -124,7 +133,7 @@ RSpec.describe 'User uses shortcuts', :js, feature_category: :groups_and_project
find('body').native.send_key('g')
find('body').native.send_key('e')
- expect(page).to have_active_navigation('Deployments')
+ expect(page).to have_active_navigation('Operate')
expect(page).to have_active_sub_navigation('Environments')
end
end
@@ -134,7 +143,7 @@ RSpec.describe 'User uses shortcuts', :js, feature_category: :groups_and_project
find('body').native.send_key('g')
find('body').native.send_key('k')
- expect(page).to have_active_navigation('Infrastructure')
+ expect(page).to have_active_navigation('Operate')
expect(page).to have_active_sub_navigation('Kubernetes')
end
end
@@ -144,7 +153,8 @@ RSpec.describe 'User uses shortcuts', :js, feature_category: :groups_and_project
find('body').native.send_key('g')
find('body').native.send_key('s')
- expect(page).to have_active_navigation('Snippets')
+ expect(page).to have_active_navigation('Code')
+ expect(page).to have_active_sub_navigation('Snippets')
end
end
@@ -153,7 +163,8 @@ RSpec.describe 'User uses shortcuts', :js, feature_category: :groups_and_project
find('body').native.send_key('g')
find('body').native.send_key('w')
- expect(page).to have_active_navigation('Wiki')
+ expect(page).to have_active_navigation('Plan')
+ expect(page).to have_active_sub_navigation('Wiki')
end
end
end
diff --git a/spec/features/projects/wikis_spec.rb b/spec/features/projects/wikis_spec.rb
index 63714954c0c..5d950da6674 100644
--- a/spec/features/projects/wikis_spec.rb
+++ b/spec/features/projects/wikis_spec.rb
@@ -3,7 +3,7 @@
require "spec_helper"
RSpec.describe 'Project wikis', :js, feature_category: :wiki do
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
let(:wiki) { create(:project_wiki, user: user, project: project) }
let(:project) { create(:project, namespace: user.namespace, creator: user) }
diff --git a/spec/features/projects/work_items/linked_work_items_spec.rb b/spec/features/projects/work_items/linked_work_items_spec.rb
index 66016cf8b7b..49f723c3055 100644
--- a/spec/features/projects/work_items/linked_work_items_spec.rb
+++ b/spec/features/projects/work_items/linked_work_items_spec.rb
@@ -102,8 +102,8 @@ RSpec.describe 'Work item linked items', :js, feature_category: :team_planning d
expect(find('.work-items-list')).to have_content('Task 1')
- find_by_testid('links-menu').click
- click_button 'Remove'
+ find_by_testid('links-child').hover
+ find_by_testid('remove-work-item-link').click
wait_for_all_requests
diff --git a/spec/features/projects/work_items/work_item_children_spec.rb b/spec/features/projects/work_items/work_item_children_spec.rb
index 843afb54dec..752ea282fbf 100644
--- a/spec/features/projects/work_items/work_item_children_spec.rb
+++ b/spec/features/projects/work_items/work_item_children_spec.rb
@@ -87,8 +87,8 @@ RSpec.describe 'Work item children', :js, feature_category: :team_planning do
expect(find('[data-testid="links-child"]')).to have_content('Task 1')
expect(find('[data-testid="children-count"]')).to have_content('1')
- find('[data-testid="links-menu"]').click
- click_button 'Remove'
+ find_by_testid('links-child').hover
+ find_by_testid('remove-work-item-link').click
wait_for_all_requests
diff --git a/spec/features/projects/work_items/work_item_spec.rb b/spec/features/projects/work_items/work_item_spec.rb
index 5210d67b78c..33153d21575 100644
--- a/spec/features/projects/work_items/work_item_spec.rb
+++ b/spec/features/projects/work_items/work_item_spec.rb
@@ -3,11 +3,14 @@
require 'spec_helper'
RSpec.describe 'Work item', :js, feature_category: :team_planning do
- let_it_be_with_reload(:user) { create(:user, :no_super_sidebar) }
- let_it_be_with_reload(:user2) { create(:user, :no_super_sidebar, name: 'John') }
+ include ListboxHelpers
+
+ let_it_be_with_reload(:user) { create(:user) }
+ let_it_be_with_reload(:user2) { create(:user, name: 'John') }
let_it_be(:project) { create(:project, :public) }
let_it_be(:work_item) { create(:work_item, project: project) }
+ let_it_be(:task) { create(:work_item, :task, project: project) }
let_it_be(:emoji_upvote) { create(:award_emoji, :upvote, awardable: work_item, user: user2) }
let_it_be(:milestone) { create(:milestone, project: project) }
let_it_be(:milestones) { create_list(:milestone, 25, project: project) }
@@ -18,9 +21,7 @@ RSpec.describe 'Work item', :js, feature_category: :team_planning do
context 'for signed in user' do
before do
project.add_developer(user)
-
sign_in(user)
-
visit work_items_path
end
@@ -37,7 +38,7 @@ RSpec.describe 'Work item', :js, feature_category: :team_planning do
end
it 'actions dropdown is displayed' do
- expect(page).to have_selector('[data-testid="work-item-actions-dropdown"]')
+ expect(page).to have_button _('More actions')
end
it 'reassigns to another user',
@@ -74,9 +75,7 @@ RSpec.describe 'Work item', :js, feature_category: :team_planning do
context 'for signed in owner' do
before do
project.add_owner(user)
-
sign_in(user)
-
visit work_items_path
end
@@ -86,29 +85,37 @@ RSpec.describe 'Work item', :js, feature_category: :team_planning do
context 'for guest users' do
before do
project.add_guest(user)
-
sign_in(user)
-
visit work_items_path
end
it_behaves_like 'work items comment actions for guest users'
end
+ context 'when item is a task' do
+ before do
+ project.add_developer(user)
+
+ sign_in(user)
+
+ visit project_work_item_path(project, task.iid)
+ end
+
+ it_behaves_like 'work items parent', :issue
+ end
+
context 'for user not signed in' do
before do
visit work_items_path
end
it 'todos action is not displayed' do
- expect(page).not_to have_selector('[data-testid="work-item-todos-action"]')
+ expect(page).not_to have_button s_('WorkItem|Add a to do')
end
it 'award button is disabled and add reaction is not displayed' do
- within('[data-testid="work-item-award-list"]') do
- expect(page).not_to have_selector('[data-testid="emoji-picker"]')
- expect(page).to have_selector('[data-testid="award-button"].disabled')
- end
+ expect(page).not_to have_button _('Add reaction')
+ expect(page).to have_selector('[data-testid="award-button"].disabled')
end
it 'assignees input field is disabled' do
diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb
index 7ca9395f669..c6966e47f0a 100644
--- a/spec/features/projects_spec.rb
+++ b/spec/features/projects_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe 'Project', feature_category: :groups_and_projects do
include MobileHelpers
describe 'template' do
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
before do
sign_in user
@@ -78,7 +78,7 @@ RSpec.describe 'Project', feature_category: :groups_and_projects do
end
describe 'shows tip about push to create git command' do
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
before do
sign_in user
@@ -214,7 +214,7 @@ RSpec.describe 'Project', feature_category: :groups_and_projects do
end
describe 'showing information about source of a project fork', :js do
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
let(:base_project) { create(:project, :public, :repository) }
let(:forked_project) { fork_project(base_project, user, repository: true) }
@@ -265,7 +265,7 @@ RSpec.describe 'Project', feature_category: :groups_and_projects do
end
describe 'when the project repository is disabled', :js do
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
let(:project) { create(:project, :repository_disabled, :repository, namespace: user.namespace) }
before do
@@ -282,7 +282,7 @@ RSpec.describe 'Project', feature_category: :groups_and_projects do
end
describe 'removal', :js do
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
let(:project) { create(:project, namespace: user.namespace) }
before do
@@ -307,7 +307,7 @@ RSpec.describe 'Project', feature_category: :groups_and_projects do
end
describe 'tree view (default view is set to Files)', :js do
- let(:user) { create(:user, :no_super_sidebar, project_view: 'files') }
+ let(:user) { create(:user, project_view: 'files') }
let(:project) { create(:forked_project_with_submodules) }
before do
@@ -379,7 +379,7 @@ RSpec.describe 'Project', feature_category: :groups_and_projects do
end
describe 'activity view' do
- let(:user) { create(:user, :no_super_sidebar, project_view: 'activity') }
+ let(:user) { create(:user, project_view: 'activity') }
let(:project) { create(:project, :repository) }
before do
@@ -410,7 +410,7 @@ RSpec.describe 'Project', feature_category: :groups_and_projects do
end
describe 'edit' do
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
let(:project) { create(:project, :public) }
let(:path) { edit_project_path(project) }
@@ -425,9 +425,9 @@ RSpec.describe 'Project', feature_category: :groups_and_projects do
describe 'view for a user without an access to a repo' do
let(:project) { create(:project, :repository) }
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
- it 'does not contain default branch information in its content' do
+ it 'does not contain default branch information in its content', :js do
default_branch = 'merge-commit-analyze-side-branch'
project.add_guest(user)
@@ -436,8 +436,10 @@ RSpec.describe 'Project', feature_category: :groups_and_projects do
sign_in(user)
visit project_path(project)
- lines_with_default_branch = page.html.lines.select { |line| line.include?(default_branch) }
- expect(lines_with_default_branch).to eq([])
+ page.within('#content-body') do
+ lines_with_default_branch = page.html.lines.select { |line| line.include?(default_branch) }
+ expect(lines_with_default_branch).to eq([])
+ end
end
end
diff --git a/spec/features/search/user_searches_for_code_spec.rb b/spec/features/search/user_searches_for_code_spec.rb
index d2847203669..976324a5032 100644
--- a/spec/features/search/user_searches_for_code_spec.rb
+++ b/spec/features/search/user_searches_for_code_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe 'User searches for code', :js, :disable_rate_limiter, feature_cat
using RSpec::Parameterized::TableSyntax
include ListboxHelpers
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
let_it_be_with_reload(:project) { create(:project, :repository, namespace: user.namespace) }
context 'when signed in' do
diff --git a/spec/features/search/user_searches_for_comments_spec.rb b/spec/features/search/user_searches_for_comments_spec.rb
index f47e692c652..f7af1797c71 100644
--- a/spec/features/search/user_searches_for_comments_spec.rb
+++ b/spec/features/search/user_searches_for_comments_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe 'User searches for comments', :js, :disable_rate_limiter, feature_category: :global_search do
let_it_be(:project) { create(:project, :repository) }
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
before do
project.add_reporter(user)
diff --git a/spec/features/search/user_searches_for_commits_spec.rb b/spec/features/search/user_searches_for_commits_spec.rb
index 140d8763813..724daf9277d 100644
--- a/spec/features/search/user_searches_for_commits_spec.rb
+++ b/spec/features/search/user_searches_for_commits_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'User searches for commits', :js, :clean_gitlab_redis_rate_limiting, feature_category: :global_search do
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository) }
let(:sha) { '6d394385cf567f80a8fd85055db1ab4c5295806f' }
diff --git a/spec/features/search/user_searches_for_issues_spec.rb b/spec/features/search/user_searches_for_issues_spec.rb
index d816b393cce..9451e337db1 100644
--- a/spec/features/search/user_searches_for_issues_spec.rb
+++ b/spec/features/search/user_searches_for_issues_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'User searches for issues', :js, :clean_gitlab_redis_rate_limiting, feature_category: :global_search do
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, namespace: user.namespace) }
let!(:issue1) { create(:issue, title: 'issue Foo', project: project, created_at: 1.hour.ago) }
diff --git a/spec/features/search/user_searches_for_merge_requests_spec.rb b/spec/features/search/user_searches_for_merge_requests_spec.rb
index 61af5e86eea..d7b52d9e07a 100644
--- a/spec/features/search/user_searches_for_merge_requests_spec.rb
+++ b/spec/features/search/user_searches_for_merge_requests_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'User searches for merge requests', :js, :clean_gitlab_redis_rate_limiting, feature_category: :global_search do
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, namespace: user.namespace) }
let_it_be(:merge_request1) { create(:merge_request, title: 'Merge Request Foo', source_project: project, target_project: project, created_at: 1.hour.ago) }
let_it_be(:merge_request2) { create(:merge_request, :simple, title: 'Merge Request Bar', source_project: project, target_project: project) }
diff --git a/spec/features/search/user_searches_for_milestones_spec.rb b/spec/features/search/user_searches_for_milestones_spec.rb
index ad62c8eb3da..7ca7958f61b 100644
--- a/spec/features/search/user_searches_for_milestones_spec.rb
+++ b/spec/features/search/user_searches_for_milestones_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe 'User searches for milestones', :js, :clean_gitlab_redis_rate_limiting,
feature_category: :global_search do
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, namespace: user.namespace) }
let_it_be(:milestone1) { create(:milestone, title: 'Foo', project: project) }
let_it_be(:milestone2) { create(:milestone, title: 'Bar', project: project) }
diff --git a/spec/features/search/user_searches_for_projects_spec.rb b/spec/features/search/user_searches_for_projects_spec.rb
index 51e5ad85e2b..48a94161927 100644
--- a/spec/features/search/user_searches_for_projects_spec.rb
+++ b/spec/features/search/user_searches_for_projects_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe 'User searches for projects', :js, :disable_rate_limiter, feature
context 'when signed out' do
context 'when block_anonymous_global_searches is disabled' do
before do
- stub_feature_flags(block_anonymous_global_searches: false, super_sidebar_logged_out: false)
+ stub_feature_flags(block_anonymous_global_searches: false)
end
include_examples 'top right search form'
diff --git a/spec/features/search/user_searches_for_users_spec.rb b/spec/features/search/user_searches_for_users_spec.rb
index b52f6aeba68..e0a07c5103d 100644
--- a/spec/features/search/user_searches_for_users_spec.rb
+++ b/spec/features/search/user_searches_for_users_spec.rb
@@ -3,9 +3,9 @@
require 'spec_helper'
RSpec.describe 'User searches for users', :js, :clean_gitlab_redis_rate_limiting, feature_category: :global_search do
- let_it_be(:user1) { create(:user, :no_super_sidebar, username: 'gob_bluth', name: 'Gob Bluth') }
- let_it_be(:user2) { create(:user, :no_super_sidebar, username: 'michael_bluth', name: 'Michael Bluth') }
- let_it_be(:user3) { create(:user, :no_super_sidebar, username: 'gob_2018', name: 'George Oscar Bluth') }
+ let_it_be(:user1) { create(:user, username: 'gob_bluth', name: 'Gob Bluth') }
+ let_it_be(:user2) { create(:user, username: 'michael_bluth', name: 'Michael Bluth') }
+ let_it_be(:user3) { create(:user, username: 'gob_2018', name: 'George Oscar Bluth') }
before do
sign_in(user1)
diff --git a/spec/features/search/user_searches_for_wiki_pages_spec.rb b/spec/features/search/user_searches_for_wiki_pages_spec.rb
index a5b63243d0b..65f262075f9 100644
--- a/spec/features/search/user_searches_for_wiki_pages_spec.rb
+++ b/spec/features/search/user_searches_for_wiki_pages_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe 'User searches for wiki pages', :js, :clean_gitlab_redis_rate_limiting,
feature_category: :global_search do
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository, :wiki_repo, namespace: user.namespace) }
let_it_be(:wiki_page) do
create(:wiki_page, wiki: project.wiki, title: 'directory/title', content: 'Some Wiki content')
diff --git a/spec/features/search/user_uses_header_search_field_spec.rb b/spec/features/search/user_uses_header_search_field_spec.rb
index 3f2a71b63dc..1ab47f6fd59 100644
--- a/spec/features/search/user_uses_header_search_field_spec.rb
+++ b/spec/features/search/user_uses_header_search_field_spec.rb
@@ -6,8 +6,8 @@ RSpec.describe 'User uses header search field', :js, :disable_rate_limiter, feat
include FilteredSearchHelpers
let_it_be(:project) { create(:project, :repository) }
- let_it_be(:reporter) { create(:user, :no_super_sidebar) }
- let_it_be(:developer) { create(:user, :no_super_sidebar) }
+ let_it_be(:reporter) { create(:user) }
+ let_it_be(:developer) { create(:user) }
let(:user) { reporter }
@@ -31,12 +31,6 @@ RSpec.describe 'User uses header search field', :js, :disable_rate_limiter, feat
submit_search('gitlab')
end
- it 'renders page title' do
- page.within('.page-title') do
- expect(page).to have_content('Search')
- end
- end
-
it 'renders breadcrumbs' do
page.within('.breadcrumbs') do
expect(page).to have_content('Search')
@@ -46,31 +40,34 @@ RSpec.describe 'User uses header search field', :js, :disable_rate_limiter, feat
context 'when using the keyboard shortcut' do
before do
- find('#search')
find('body').native.send_keys('s')
- wait_for_all_requests
end
- it 'shows the category search dropdown', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/250285' do
- expect(page).to have_selector('.dropdown-header', text: /#{scope_name}/i)
+ it 'shows the search modal' do
+ expect(page).to have_selector(search_modal_results, visible: :visible)
end
end
- context 'when clicking the search field' do
+ context 'when clicking the search button' do
before do
- page.find('#search').click
+ within_testid('super-sidebar') do
+ click_button "Search or go to…"
+ end
wait_for_all_requests
end
- it 'shows category search dropdown', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/250285' do
- expect(page).to have_selector('.dropdown-header', text: /#{scope_name}/i)
+ it 'shows search scope badge' do
+ fill_in 'search', with: 'text'
+ within('#super-sidebar-search-modal') do
+ expect(page).to have_selector('.search-scope-help', text: scope_name)
+ end
end
context 'when clicking issues', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/332317' do
let!(:issue) { create(:issue, project: project, author: user, assignees: [user]) }
it 'shows assigned issues' do
- find('[data-testid="header-search-dropdown-menu"]').click_link('Issues assigned to me')
+ find(search_modal_results).click_link('Issues assigned to me')
expect(page).to have_selector('.issues-list .issue')
expect_tokens([assignee_token(user.name)])
@@ -78,7 +75,7 @@ RSpec.describe 'User uses header search field', :js, :disable_rate_limiter, feat
end
it 'shows created issues' do
- find('[data-testid="header-search-dropdown-menu"]').click_link("Issues I've created")
+ find(search_modal_results).click_link("Issues I've created")
expect(page).to have_selector('.issues-list .issue')
expect_tokens([author_token(user.name)])
@@ -90,7 +87,7 @@ RSpec.describe 'User uses header search field', :js, :disable_rate_limiter, feat
let!(:merge_request) { create(:merge_request, source_project: project, author: user, assignees: [user]) }
it 'shows assigned merge requests' do
- find('[data-testid="header-search-dropdown-menu"]').click_link('Merge requests assigned to me')
+ find(search_modal_results).click_link('Merge requests assigned to me')
expect(page).to have_selector('.mr-list .merge-request')
expect_tokens([assignee_token(user.name)])
@@ -98,7 +95,7 @@ RSpec.describe 'User uses header search field', :js, :disable_rate_limiter, feat
end
it 'shows created merge requests' do
- find('[data-testid="header-search-dropdown-menu"]').click_link("Merge requests I've created")
+ find(search_modal_results).click_link("Merge requests I've created")
expect(page).to have_selector('.mr-list .merge-request')
expect_tokens([author_token(user.name)])
@@ -119,7 +116,7 @@ RSpec.describe 'User uses header search field', :js, :disable_rate_limiter, feat
context 'when user is in a global scope' do
include_examples 'search field examples' do
let(:url) { root_path }
- let(:scope_name) { 'All GitLab' }
+ let(:scope_name) { 'in all GitLab' }
end
it 'displays search options', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/251076' do
@@ -136,11 +133,13 @@ RSpec.describe 'User uses header search field', :js, :disable_rate_limiter, feat
end
it 'displays result counts for all categories' do
- expect(page).to have_content('Projects 1')
- expect(page).to have_content('Issues 1')
- expect(page).to have_content('Merge requests 0')
- expect(page).to have_content('Milestones 0')
- expect(page).to have_content('Users 0')
+ within_testid('super-sidebar') do
+ expect(page).to have_link('Projects 1')
+ expect(page).to have_link('Issues 1')
+ expect(page).to have_link('Merge requests 0')
+ expect(page).to have_link('Milestones 0')
+ expect(page).to have_link('Users 0')
+ end
end
end
end
@@ -162,9 +161,8 @@ RSpec.describe 'User uses header search field', :js, :disable_rate_limiter, feat
it 'displays search options' do
fill_in_search('test')
- expect(page).to have_selector(scoped_search_link('test', search_code: true))
expect(page).to have_selector(scoped_search_link('test', group_id: group.id, search_code: true))
- expect(page).to have_selector(scoped_search_link('test', project_id: project.id, group_id: group.id, search_code: true))
+ expect(page).to have_selector(scoped_search_link('test', search_code: true))
end
end
@@ -176,26 +174,25 @@ RSpec.describe 'User uses header search field', :js, :disable_rate_limiter, feat
it 'displays search options' do
fill_in_search('test')
- sleep 0.5
- expect(page).to have_selector(scoped_search_link('test', search_code: true, repository_ref: 'master'))
+
expect(page).not_to have_selector(scoped_search_link('test', search_code: true, group_id: project.namespace_id, repository_ref: 'master'))
- expect(page).to have_selector(scoped_search_link('test', search_code: true, project_id: project.id, repository_ref: 'master'))
+ expect(page).to have_selector(scoped_search_link('test', search_code: true, repository_ref: 'master'))
end
it 'displays a link to project merge requests' do
fill_in_search('Merge')
- within(dashboard_search_options_popup_menu) do
- expect(page).to have_text('Merge requests')
+ within(search_modal_results) do
+ expect(page).to have_link('Merge requests')
end
end
it 'does not display a link to project feature flags' do
fill_in_search('Feature')
- within(dashboard_search_options_popup_menu) do
- expect(page).to have_text('Feature in all GitLab')
- expect(page).to have_no_text('Feature Flags')
+ within(search_modal_results) do
+ expect(page).to have_link('in all GitLab Feature')
+ expect(page).not_to have_link('Feature Flags')
end
end
@@ -205,8 +202,8 @@ RSpec.describe 'User uses header search field', :js, :disable_rate_limiter, feat
it 'displays a link to project feature flags' do
fill_in_search('Feature')
- within(dashboard_search_options_popup_menu) do
- expect(page).to have_text('Feature Flags')
+ within(search_modal_results) do
+ expect(page).to have_link('Feature Flags')
end
end
end
@@ -228,8 +225,8 @@ RSpec.describe 'User uses header search field', :js, :disable_rate_limiter, feat
it 'displays search options' do
fill_in_search('test')
+
expect(page).to have_selector(scoped_search_link('test'))
- expect(page).to have_selector(scoped_search_link('test', group_id: group.id))
expect(page).not_to have_selector(scoped_search_link('test', project_id: project.id))
end
end
@@ -253,7 +250,6 @@ RSpec.describe 'User uses header search field', :js, :disable_rate_limiter, feat
fill_in_search('test')
expect(page).to have_selector(scoped_search_link('test'))
- expect(page).to have_selector(scoped_search_link('test', group_id: subgroup.id))
expect(page).not_to have_selector(scoped_search_link('test', project_id: project.id))
end
end
@@ -268,10 +264,10 @@ RSpec.describe 'User uses header search field', :js, :disable_rate_limiter, feat
href.concat("&search_code=true") if search_code
href.concat("&repository_ref=#{repository_ref}") if repository_ref
- "[data-testid='header-search-dropdown-menu'] a[href='#{href}']"
+ ".global-search-results a[href='#{href}']"
end
- def dashboard_search_options_popup_menu
- "[data-testid='header-search-dropdown-menu'] .header-search-dropdown-content"
+ def search_modal_results
+ ".global-search-results"
end
end
diff --git a/spec/features/snippets/search_snippets_spec.rb b/spec/features/snippets/search_snippets_spec.rb
index 7a07299a14f..cf6f9825932 100644
--- a/spec/features/snippets/search_snippets_spec.rb
+++ b/spec/features/snippets/search_snippets_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe 'Search Snippets', :js, feature_category: :global_search do
it 'user searches for snippets by title' do
- user = create(:user, :no_super_sidebar)
+ user = create(:user)
public_snippet = create(:personal_snippet, :public, title: 'Beginning and Middle')
private_snippet = create(:personal_snippet, :private, title: 'Middle and End', author: user)
diff --git a/spec/features/snippets/show_spec.rb b/spec/features/snippets/show_spec.rb
index bbb120edb80..03f46ea0122 100644
--- a/spec/features/snippets/show_spec.rb
+++ b/spec/features/snippets/show_spec.rb
@@ -3,49 +3,63 @@
require 'spec_helper'
RSpec.describe 'Snippet', :js, feature_category: :source_code_management do
- let_it_be(:user) { create(:user, :no_super_sidebar) }
- let_it_be(:snippet) { create(:personal_snippet, :public, :repository, author: user) }
+ let_it_be(:owner) { create(:user) }
+ let_it_be(:snippet) { create(:personal_snippet, :public, :repository, author: owner) }
+ let(:anchor) { nil }
+ let(:file_path) { 'files/ruby/popen.rb' }
before do
- stub_feature_flags(super_sidebar_logged_out: false)
+ # rubocop: disable RSpec/AnyInstanceOf -- TODO: The usage of let_it_be forces us
+ allow_any_instance_of(Snippet).to receive(:blobs)
+ .and_return([snippet.repository.blob_at('master', file_path)])
+ # rubocop: enable RSpec/AnyInstanceOf
end
- it_behaves_like 'show and render proper snippet blob' do
- let(:anchor) { nil }
-
- subject do
- visit snippet_path(snippet, anchor: anchor)
+ def visit_page
+ visit snippet_path(snippet, anchor: anchor)
+ end
- wait_for_requests
+ context 'when signed in' do
+ before do
+ sign_in(user)
+ visit_page
end
- end
- # it_behaves_like 'showing user status' do
- # This will be handled in https://gitlab.com/gitlab-org/gitlab/-/issues/262394
+ context 'as the snippet owner' do
+ let(:user) { owner }
- it_behaves_like 'does not show New Snippet button' do
- let(:file_path) { 'files/ruby/popen.rb' }
+ it_behaves_like 'show and render proper snippet blob'
+ it_behaves_like 'does show New Snippet button'
+ it_behaves_like 'a "Your work" page with sidebar and breadcrumbs', :dashboard_snippets_path, :snippets
+ end
- subject { visit snippet_path(snippet) }
- end
+ context 'as external user' do
+ let_it_be(:user) { create(:user, :external) }
- it_behaves_like 'a "Your work" page with sidebar and breadcrumbs', :dashboard_snippets_path, :snippets
+ it_behaves_like 'show and render proper snippet blob'
+ it_behaves_like 'does not show New Snippet button'
+ it_behaves_like 'a "Your work" page with sidebar and breadcrumbs', :dashboard_snippets_path, :snippets
+ end
- context 'when unauthenticated' do
- it 'shows the "Explore" sidebar' do
- visit snippet_path(snippet)
+ context 'as another user' do
+ let_it_be(:user) { create(:user) }
- expect(page).to have_css('aside.nav-sidebar[aria-label="Explore"]')
+ it_behaves_like 'show and render proper snippet blob'
+ it_behaves_like 'does show New Snippet button'
+ it_behaves_like 'a "Your work" page with sidebar and breadcrumbs', :dashboard_snippets_path, :snippets
end
end
- context 'when authenticated as a different user' do
- let_it_be(:different_user) { create(:user, :no_super_sidebar) }
-
+ context 'when unauthenticated' do
before do
- sign_in(different_user)
+ visit_page
end
- it_behaves_like 'a "Your work" page with sidebar and breadcrumbs', :dashboard_snippets_path, :snippets
+ it_behaves_like 'show and render proper snippet blob'
+ it_behaves_like 'does not show New Snippet button'
+
+ it 'shows the "Explore" sidebar' do
+ expect(page).to have_css('#super-sidebar-context-header', text: 'Explore')
+ end
end
end
diff --git a/spec/features/snippets/user_creates_snippet_spec.rb b/spec/features/snippets/user_creates_snippet_spec.rb
index 341cc150a64..f1f804786a3 100644
--- a/spec/features/snippets/user_creates_snippet_spec.rb
+++ b/spec/features/snippets/user_creates_snippet_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe 'User creates snippet', :js, feature_category: :source_code_manag
include DropzoneHelper
include Features::SnippetSpecHelpers
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
let(:title) { 'My Snippet Title' }
let(:file_content) { 'Hello World!' }
diff --git a/spec/features/task_lists_spec.rb b/spec/features/task_lists_spec.rb
index 24d63cadf00..c1be2b8e3c7 100644
--- a/spec/features/task_lists_spec.rb
+++ b/spec/features/task_lists_spec.rb
@@ -6,8 +6,8 @@ RSpec.describe 'Task Lists', :js, feature_category: :team_planning do
include Warden::Test::Helpers
let_it_be(:project) { create(:project, :public, :repository) }
- let_it_be(:user) { create(:user, :no_super_sidebar) }
- let_it_be(:user2) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:user2) { create(:user) }
let(:markdown) do
<<-MARKDOWN.strip_heredoc
@@ -44,7 +44,7 @@ RSpec.describe 'Task Lists', :js, feature_category: :team_planning do
end
before do
- login_as(user)
+ sign_in(user)
end
def visit_issue(project, issue)
diff --git a/spec/features/unsubscribe_links_spec.rb b/spec/features/unsubscribe_links_spec.rb
index b78efa65888..77ef3df97f6 100644
--- a/spec/features/unsubscribe_links_spec.rb
+++ b/spec/features/unsubscribe_links_spec.rb
@@ -6,8 +6,8 @@ RSpec.describe 'Unsubscribe links', :sidekiq_inline, feature_category: :shared d
include Warden::Test::Helpers
let_it_be(:project) { create(:project, :public) }
- let_it_be(:author) { create(:user, :no_super_sidebar).tap { |u| project.add_reporter(u) } }
- let_it_be(:recipient) { create(:user, :no_super_sidebar) }
+ let_it_be(:author) { create(:user).tap { |u| project.add_reporter(u) } }
+ let_it_be(:recipient) { create(:user) }
let(:params) { { title: 'A bug!', description: 'Fix it!', assignee_ids: [recipient.id] } }
let(:issue) { Issues::CreateService.new(container: project, current_user: author, params: params).execute[:issue] }
@@ -22,10 +22,6 @@ RSpec.describe 'Unsubscribe links', :sidekiq_inline, feature_category: :shared d
end
context 'when logged out' do
- before do
- stub_feature_flags(super_sidebar_logged_out: false)
- end
-
context 'when visiting the link from the body' do
it 'shows the unsubscribe confirmation page and redirects to root path when confirming' do
visit body_link
diff --git a/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb b/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb
index 5de544e866e..83eb7cb989e 100644
--- a/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb
+++ b/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'User uploads avatar to profile', feature_category: :user_profile do
- let!(:user) { create(:user, :no_super_sidebar) }
+ let!(:user) { create(:user) }
let(:avatar_file_path) { Rails.root.join('spec', 'fixtures', 'dk.png') }
shared_examples 'upload avatar' do
@@ -19,8 +19,7 @@ RSpec.describe 'User uploads avatar to profile', feature_category: :user_profile
wait_for_all_requests
data_uri = find('.avatar-image .gl-avatar')['src']
- expect(page.find('.header-user-avatar')['src']).to eq data_uri
- expect(page.find('[data-testid="sidebar-user-avatar"]')['src']).to eq data_uri
+ within_testid('user-dropdown') { expect(find('.gl-avatar')['src']).to eq data_uri }
visit profile_path
diff --git a/spec/features/usage_stats_consent_spec.rb b/spec/features/usage_stats_consent_spec.rb
index 92f7a944007..ebf1cd9e143 100644
--- a/spec/features/usage_stats_consent_spec.rb
+++ b/spec/features/usage_stats_consent_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe 'Usage stats consent', feature_category: :service_ping do
context 'when signed in' do
- let(:user) { create(:admin, :no_super_sidebar, created_at: 8.days.ago) }
+ let(:user) { create(:admin, created_at: 8.days.ago) }
let(:message) { 'To help improve GitLab, we would like to periodically collect usage information.' }
before do
@@ -22,24 +22,29 @@ RSpec.describe 'Usage stats consent', feature_category: :service_ping do
gitlab_enable_admin_mode_sign_in(user)
end
- it 'hides the banner permanently when sets usage stats' do
- visit root_dashboard_path
+ shared_examples 'dismissible banner' do |button_text|
+ it 'hides the banner permanently when sets usage stats', :js do
+ visit root_dashboard_path
- expect(page).to have_content(message)
+ expect(page).to have_content(message)
- click_link 'Send service data'
+ click_link button_text
- expect(page).not_to have_content(message)
- expect(page).to have_content('Application settings saved successfully')
+ expect(page).not_to have_content(message)
+ expect(page).to have_content('Application settings saved successfully')
- gitlab_sign_out
- gitlab_sign_in(user)
- visit root_dashboard_path
+ gitlab_sign_out
+ gitlab_sign_in(user)
+ visit root_dashboard_path
- expect(page).not_to have_content(message)
+ expect(page).not_to have_content(message)
+ end
end
- it 'shows banner on next session if user did not set usage stats' do
+ it_behaves_like 'dismissible banner', _('Send service data')
+ it_behaves_like 'dismissible banner', _("Don't send service data")
+
+ it 'shows banner on next session if user did not set usage stats', :js do
visit root_dashboard_path
expect(page).to have_content(message)
diff --git a/spec/features/user_can_display_performance_bar_spec.rb b/spec/features/user_can_display_performance_bar_spec.rb
index caf13c4111b..a22418760aa 100644
--- a/spec/features/user_can_display_performance_bar_spec.rb
+++ b/spec/features/user_can_display_performance_bar_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User can display performance bar', :js, feature_category: :application_performance do
+RSpec.describe 'User can display performance bar', :js, feature_category: :cloud_connector do
shared_examples 'performance bar cannot be displayed' do
it 'does not show the performance bar by default' do
expect(page).not_to have_css('#js-peek')
diff --git a/spec/features/user_sees_active_nav_items_spec.rb b/spec/features/user_sees_active_nav_items_spec.rb
new file mode 100644
index 00000000000..966b8491374
--- /dev/null
+++ b/spec/features/user_sees_active_nav_items_spec.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'User sees correct active nav items in the super sidebar', :js, feature_category: :value_stream_management do
+ let_it_be(:current_user) { create(:user) }
+
+ before do
+ sign_in(current_user)
+ end
+
+ describe 'profile pages' do
+ context 'when visiting profile page' do
+ before do
+ visit profile_path
+ end
+
+ it 'renders the side navigation with the correct submenu set as active' do
+ expect(page).to have_active_sub_navigation('Profile')
+ end
+ end
+
+ context 'when visiting preferences page' do
+ before do
+ visit profile_preferences_path
+ end
+
+ it 'renders the side navigation with the correct submenu set as active' do
+ expect(page).to have_active_sub_navigation('Preferences')
+ end
+ end
+
+ context 'when visiting authentication logs' do
+ before do
+ visit audit_log_profile_path
+ end
+
+ it 'renders the side navigation with the correct submenu set as active' do
+ expect(page).to have_active_sub_navigation('Authentication Log')
+ end
+ end
+
+ context 'when visiting SSH keys page' do
+ before do
+ visit profile_keys_path
+ end
+
+ it 'renders the side navigation with the correct submenu set as active' do
+ expect(page).to have_active_sub_navigation('SSH Keys')
+ end
+ end
+
+ context 'when visiting account page' do
+ before do
+ visit profile_account_path
+ end
+
+ it 'renders the side navigation with the correct submenu set as active' do
+ expect(page).to have_active_sub_navigation('Account')
+ end
+ end
+ end
+end
diff --git a/spec/features/user_sees_revert_modal_spec.rb b/spec/features/user_sees_revert_modal_spec.rb
index fdeee6a2808..ebb84a0d87f 100644
--- a/spec/features/user_sees_revert_modal_spec.rb
+++ b/spec/features/user_sees_revert_modal_spec.rb
@@ -12,6 +12,8 @@ RSpec.describe 'Merge request > User sees revert modal', :js, :sidekiq_might_not
it 'shows the revert modal' do
click_button('Revert')
+ wait_for_requests
+
page.within('[data-testid="modal-commit"]') do
expect(page).to have_content 'Revert this merge request'
end
@@ -19,7 +21,6 @@ RSpec.describe 'Merge request > User sees revert modal', :js, :sidekiq_might_not
end
before do
- stub_feature_flags(unbatch_graphql_queries: false)
sign_in(user)
visit(project_merge_request_path(project, merge_request))
@@ -27,6 +28,10 @@ RSpec.describe 'Merge request > User sees revert modal', :js, :sidekiq_might_not
click_button 'Merge'
end
+ wait_for_all_requests
+
+ page.refresh
+
wait_for_requests
end
diff --git a/spec/features/users/active_sessions_spec.rb b/spec/features/users/active_sessions_spec.rb
index 663d2283dbd..8509a8d7356 100644
--- a/spec/features/users/active_sessions_spec.rb
+++ b/spec/features/users/active_sessions_spec.rb
@@ -3,10 +3,10 @@
require 'spec_helper'
RSpec.describe 'Active user sessions', :clean_gitlab_redis_sessions, feature_category: :system_access do
- it 'successful login adds a new active user login' do
- user = create(:user, :no_super_sidebar)
+ it 'successful login adds a new active user login', :js do
+ user = create(:user)
- now = Time.zone.parse('2018-03-12 09:06')
+ now = Time.zone.now.change(usec: 0)
travel_to(now) do
gitlab_sign_in(user)
expect(page).to have_current_path root_path, ignore_query: true
@@ -24,14 +24,14 @@ RSpec.describe 'Active user sessions', :clean_gitlab_redis_sessions, feature_cat
sessions = ActiveSession.list(user)
expect(sessions.first).to have_attributes(
- created_at: Time.zone.parse('2018-03-12 09:06'),
- updated_at: Time.zone.parse('2018-03-12 09:07')
+ created_at: now,
+ updated_at: now + 1.minute
)
end
end
it 'successful login cleans up obsolete entries' do
- user = create(:user, :no_super_sidebar)
+ user = create(:user)
Gitlab::Redis::Sessions.with do |redis|
redis.sadd?("session:lookup:user:gitlab:#{user.id}", '59822c7d9fcdfa03725eff41782ad97d')
@@ -45,7 +45,7 @@ RSpec.describe 'Active user sessions', :clean_gitlab_redis_sessions, feature_cat
end
it 'sessionless login does not clean up obsolete entries' do
- user = create(:user, :no_super_sidebar)
+ user = create(:user)
personal_access_token = create(:personal_access_token, user: user)
Gitlab::Redis::Sessions.with do |redis|
@@ -60,8 +60,8 @@ RSpec.describe 'Active user sessions', :clean_gitlab_redis_sessions, feature_cat
end
end
- it 'logout deletes the active user login' do
- user = create(:user, :no_super_sidebar)
+ it 'logout deletes the active user login', :js do
+ user = create(:user)
gitlab_sign_in(user)
expect(page).to have_current_path root_path, ignore_query: true
diff --git a/spec/features/users/anonymous_sessions_spec.rb b/spec/features/users/anonymous_sessions_spec.rb
index 368f272ba23..81b18b7ca02 100644
--- a/spec/features/users/anonymous_sessions_spec.rb
+++ b/spec/features/users/anonymous_sessions_spec.rb
@@ -5,6 +5,10 @@ require 'spec_helper'
RSpec.describe 'Session TTLs', :clean_gitlab_redis_shared_state, feature_category: :system_access do
include SessionHelpers
+ before do
+ expire_session
+ end
+
it 'creates a session with a short TTL when login fails' do
visit new_user_session_path
# The session key only gets created after a post
@@ -18,10 +22,10 @@ RSpec.describe 'Session TTLs', :clean_gitlab_redis_shared_state, feature_categor
end
it 'increases the TTL when the login succeeds' do
- user = create(:user, :no_super_sidebar)
+ user = create(:user)
gitlab_sign_in(user)
- expect(page).to have_content(user.name)
+ expect(find('.js-super-sidebar')['data-sidebar']).to include(user.name)
expect_single_session_with_authenticated_ttl
end
diff --git a/spec/features/users/email_verification_on_login_spec.rb b/spec/features/users/email_verification_on_login_spec.rb
index d83040efd72..ad62af6ec69 100644
--- a/spec/features/users/email_verification_on_login_spec.rb
+++ b/spec/features/users/email_verification_on_login_spec.rb
@@ -5,8 +5,8 @@ require 'spec_helper'
RSpec.describe 'Email Verification On Login', :clean_gitlab_redis_rate_limiting, :js, feature_category: :system_access do
include EmailHelpers
- let_it_be_with_reload(:user) { create(:user, :no_super_sidebar) }
- let_it_be(:another_user) { create(:user, :no_super_sidebar) }
+ let_it_be_with_reload(:user) { create(:user) }
+ let_it_be(:another_user) { create(:user) }
let_it_be(:new_email) { build_stubbed(:user).email }
let(:require_email_verification_enabled) { user }
@@ -220,7 +220,7 @@ RSpec.describe 'Email Verification On Login', :clean_gitlab_redis_rate_limiting,
shared_examples 'no email verification required when 2fa enabled or ff disabled' do
context 'when 2FA is enabled' do
- let_it_be(:user) { create(:user, :no_super_sidebar, :two_factor) }
+ let_it_be(:user) { create(:user, :two_factor) }
it_behaves_like 'no email verification required', two_factor_auth: true
end
diff --git a/spec/features/users/login_spec.rb b/spec/features/users/login_spec.rb
index 87afcbd416b..0f086af227c 100644
--- a/spec/features/users/login_spec.rb
+++ b/spec/features/users/login_spec.rb
@@ -16,7 +16,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
- user = create(:user, :no_super_sidebar)
+ user = create(:user)
expect(user.reset_password_token).to be_nil
@@ -43,7 +43,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_
# This behavior is dependent on there only being one user
User.delete_all
- user = create(:admin, :no_super_sidebar, password_automatically_set: true)
+ user = create(:admin, password_automatically_set: true)
visit root_path
expect(page).to have_current_path edit_user_password_path, ignore_query: true
@@ -77,7 +77,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_
.and increment(:user_unauthenticated_counter)
.and increment(:user_session_destroyed_counter).twice
- user = create(:user, :no_super_sidebar, :blocked)
+ user = create(:user, :blocked)
gitlab_sign_in(user)
@@ -90,14 +90,14 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_
.and increment(:user_unauthenticated_counter)
.and increment(:user_session_destroyed_counter).twice
- user = create(:user, :no_super_sidebar, :blocked)
+ user = create(:user, :blocked)
expect { gitlab_sign_in(user) }.not_to change { user.reload.sign_in_count }
end
end
describe 'with an unconfirmed email address' do
- let!(:user) { create(:user, :no_super_sidebar, confirmed_at: nil) }
+ let!(:user) { create(:user, confirmed_at: nil) }
let(:grace_period) { 2.days }
let(:alert_title) { 'Please confirm your email address' }
let(:alert_message) { "To continue, you need to select the link in the confirmation email we sent to verify your email address. If you didn't get our email, select Resend confirmation email" }
@@ -141,7 +141,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_
end
context 'when resending the confirmation email' do
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
it 'redirects to the "almost there" page' do
visit new_user_confirmation_path
@@ -154,7 +154,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_
end
describe 'with a disallowed password' do
- let(:user) { create(:user, :no_super_sidebar, :disallowed_password) }
+ let(:user) { create(:user, :disallowed_password) }
before do
expect(authentication_metrics)
@@ -295,7 +295,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_
# Freeze time to prevent failures when time between code being entered and
# validated greater than otp_allowed_drift
context 'with valid username/password', :freeze_time do
- let(:user) { create(:user, :no_super_sidebar, :two_factor) }
+ let(:user) { create(:user, :two_factor) }
before do
gitlab_sign_in(user, remember: true)
@@ -372,13 +372,13 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_
end
context 'when user with TOTP enabled' do
- let(:user) { create(:user, :no_super_sidebar, :two_factor) }
+ let(:user) { create(:user, :two_factor) }
include_examples 'can login with recovery codes'
end
context 'when user with only Webauthn enabled' do
- let(:user) { create(:user, :no_super_sidebar, :two_factor_via_webauthn, registrations_count: 1) }
+ let(:user) { create(:user, :two_factor_via_webauthn, registrations_count: 1) }
include_examples 'can login with recovery codes', only_two_factor_webauthn_enabled: true
end
@@ -494,7 +494,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_
end
context 'with correct username and password' do
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
it 'allows basic login' do
expect(authentication_metrics)
@@ -584,7 +584,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_
end
context 'with correct username and invalid password' do
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
it 'blocks invalid login' do
expect(authentication_metrics)
@@ -601,7 +601,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_
end
describe 'with required two-factor authentication enabled' do
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
# TODO: otp_grace_period_started_at
@@ -639,7 +639,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_
end
context 'after the grace period' do
- let(:user) { create(:user, :no_super_sidebar, otp_grace_period_started_at: 9999.hours.ago) }
+ let(:user) { create(:user, otp_grace_period_started_at: 9999.hours.ago) }
it 'redirects to two-factor configuration page' do
expect(authentication_metrics)
@@ -728,7 +728,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_
end
context 'after the grace period' do
- let(:user) { create(:user, :no_super_sidebar, otp_grace_period_started_at: 9999.hours.ago) }
+ let(:user) { create(:user, otp_grace_period_started_at: 9999.hours.ago) }
it 'redirects to two-factor configuration page' do
expect(authentication_metrics)
@@ -919,7 +919,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_
end
context 'when terms are enforced', :js do
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
before do
enforce_terms
@@ -1090,7 +1090,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_
end
context 'when sending confirmation email and not yet confirmed' do
- let!(:user) { create(:user, :no_super_sidebar, confirmed_at: nil) }
+ let!(:user) { create(:user, confirmed_at: nil) }
let(:grace_period) { 2.days }
let(:alert_title) { 'Please confirm your email address' }
let(:alert_message) { "To continue, you need to select the link in the confirmation email we sent to verify your email address. If you didn't get our email, select Resend confirmation email" }
diff --git a/spec/features/users/logout_spec.rb b/spec/features/users/logout_spec.rb
index d0e5be8dca3..c9839247e7d 100644
--- a/spec/features/users/logout_spec.rb
+++ b/spec/features/users/logout_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'Logout/Sign out', :js, feature_category: :system_access do
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
before do
sign_in(user)
diff --git a/spec/features/users/overview_spec.rb b/spec/features/users/overview_spec.rb
index d1ff60b6069..1da61ecb868 100644
--- a/spec/features/users/overview_spec.rb
+++ b/spec/features/users/overview_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'Overview tab on a user profile', :js, feature_category: :user_profile do
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
let(:contributed_project) { create(:project, :public, :repository) }
def push_code_contribution
@@ -27,8 +27,7 @@ RSpec.describe 'Overview tab on a user profile', :js, feature_category: :user_pr
shared_context 'visit overview tab' do
before do
visit user.username
- page.find('.js-overview-tab a').click
- wait_for_requests
+ click_nav user.name
end
end
@@ -61,15 +60,15 @@ RSpec.describe 'Overview tab on a user profile', :js, feature_category: :user_pr
end
end
- describe 'user has 11 activities' do
+ describe 'user has 15 activities' do
before do
- 11.times { push_code_contribution }
+ 16.times { push_code_contribution }
end
include_context 'visit overview tab'
- it 'displays 10 entries in the list of activities' do
- expect(find('#js-overview')).to have_selector('.event-item', count: 10)
+ it 'displays 15 entries in the list of activities' do
+ expect(find('#js-overview')).to have_selector('.event-item', count: 15)
end
it 'shows a link to the activity list' do
@@ -158,8 +157,7 @@ RSpec.describe 'Overview tab on a user profile', :js, feature_category: :user_pr
describe 'user has no followers' do
before do
visit user.username
- page.find('.js-followers-tab a').click
- wait_for_requests
+ click_nav 'Followers'
end
it 'shows an empty followers list with an info message' do
@@ -177,8 +175,7 @@ RSpec.describe 'Overview tab on a user profile', :js, feature_category: :user_pr
before do
follower.follow(user)
visit user.username
- page.find('.js-followers-tab a').click
- wait_for_requests
+ click_nav 'Followers'
end
it 'shows followers' do
@@ -199,8 +196,7 @@ RSpec.describe 'Overview tab on a user profile', :js, feature_category: :user_pr
end
visit user.username
- page.find('.js-followers-tab a').click
- wait_for_requests
+ click_nav 'Followers'
end
it 'shows paginated followers' do
page.within('#followers') do
@@ -221,8 +217,7 @@ RSpec.describe 'Overview tab on a user profile', :js, feature_category: :user_pr
describe 'user is not following others' do
before do
visit user.username
- page.find('.js-following-tab a').click
- wait_for_requests
+ click_nav 'Following'
end
it 'shows an empty following list with an info message' do
@@ -240,8 +235,7 @@ RSpec.describe 'Overview tab on a user profile', :js, feature_category: :user_pr
before do
user.follow(followee)
visit user.username
- page.find('.js-following-tab a').click
- wait_for_requests
+ click_nav 'Following'
end
it 'shows following user' do
@@ -262,8 +256,7 @@ RSpec.describe 'Overview tab on a user profile', :js, feature_category: :user_pr
end
visit user.username
- page.find('.js-following-tab a').click
- wait_for_requests
+ click_nav 'Following'
end
it 'shows paginated following' do
page.within('#following') do
@@ -286,8 +279,7 @@ RSpec.describe 'Overview tab on a user profile', :js, feature_category: :user_pr
shared_context "visit bot's overview tab" do
before do
visit bot_user.username
- page.find('.js-overview-tab a').click
- wait_for_requests
+ click_nav bot_user.name
end
end
@@ -327,4 +319,13 @@ RSpec.describe 'Overview tab on a user profile', :js, feature_category: :user_pr
end
end
end
+
+ private
+
+ def click_nav(title)
+ within_testid('super-sidebar') do
+ click_link title
+ end
+ wait_for_requests
+ end
end
diff --git a/spec/features/users/rss_spec.rb b/spec/features/users/rss_spec.rb
index 99451ac472d..730c31df899 100644
--- a/spec/features/users/rss_spec.rb
+++ b/spec/features/users/rss_spec.rb
@@ -3,58 +3,35 @@
require 'spec_helper'
RSpec.describe 'User RSS', feature_category: :user_profile do
- let(:user) { create(:user, :no_super_sidebar) }
- let(:path) { user_path(create(:user, :no_super_sidebar)) }
+ let(:user) { create(:user) }
+ let(:path) { user_path(create(:user)) }
- describe 'with "user_profile_overflow_menu_vue" feature flag off' do
+ context 'when signed in' do
before do
- stub_feature_flags(user_profile_overflow_menu_vue: false)
+ sign_in(user)
+ visit path
end
- context 'when signed in' do
- before do
- sign_in(user)
- visit path
+ it 'shows the RSS link with overflow menu', :js do
+ page.within('.user-cover-block') do
+ find_by_testid('base-dropdown-toggle').click
end
- it_behaves_like "it has an RSS button with current_user's feed token"
- end
-
- context 'when signed out' do
- before do
- stub_feature_flags(super_sidebar_logged_out: false)
- visit path
- end
-
- it_behaves_like "it has an RSS button without a feed token"
+ expect(page).to have_link 'Subscribe', href: /feed_token=glft-.*-#{user.id}/
end
end
- describe 'with "user_profile_overflow_menu_vue" feature flag on', :js do
- context 'when signed in' do
- before do
- sign_in(user)
- visit path
- end
-
- it 'shows the RSS link with overflow menu' do
- find('[data-testid="base-dropdown-toggle"').click
-
- expect(page).to have_link 'Subscribe', href: /feed_token=glft-.*-#{user.id}/
- end
+ context 'when signed out' do
+ before do
+ visit path
end
- context 'when signed out' do
- before do
- stub_feature_flags(super_sidebar_logged_out: false)
- visit path
+ it 'has an RSS without a feed token', :js do
+ page.within('.user-cover-block') do
+ find_by_testid('base-dropdown-toggle').click
end
- it 'has an RSS without a feed token' do
- find('[data-testid="base-dropdown-toggle"').click
-
- expect(page).not_to have_link 'Subscribe', href: /feed_token=glft-.*-#{user.id}/
- end
+ expect(page).not_to have_link 'Subscribe', href: /feed_token=glft-.*-#{user.id}/
end
end
end
diff --git a/spec/features/users/show_spec.rb b/spec/features/users/show_spec.rb
index 522eb12f507..2821e8286a4 100644
--- a/spec/features/users/show_spec.rb
+++ b/spec/features/users/show_spec.rb
@@ -7,38 +7,16 @@ RSpec.describe 'User page', feature_category: :user_profile do
let_it_be(:user) { create(:user, bio: '<b>Lorem</b> <i>ipsum</i> dolor sit <a href="https://example.com">amet</a>') }
- before do
- stub_feature_flags(super_sidebar_logged_out: false)
- end
-
subject(:visit_profile) { visit(user_path(user)) }
- context 'with "user_profile_overflow_menu_vue" feature flag enabled', :js do
- it 'does not show the user id in the profile info' do
- subject
-
- expect(page).not_to have_content("User ID: #{user.id}")
- end
-
- it 'shows copy user id action in the dropdown' do
- subject
-
- find('[data-testid="base-dropdown-toggle"').click
-
- expect(page).to have_content("Copy user ID: #{user.id}")
- end
- end
+ it 'shows copy user id action in the dropdown', :js do
+ subject
- context 'with "user_profile_overflow_menu_vue" feature flag disabled', :js do
- before do
- stub_feature_flags(user_profile_overflow_menu_vue: false)
+ page.within('.user-cover-block') do
+ find_by_testid('base-dropdown-toggle').click
end
- it 'shows user id' do
- subject
-
- expect(page).to have_content("User ID: #{user.id}")
- end
+ expect(page).to have_content("Copy user ID: #{user.id}")
end
it 'shows name on breadcrumbs' do
@@ -193,33 +171,37 @@ RSpec.describe 'User page', feature_category: :user_profile do
expect(page).not_to have_button(text: 'Follow', class: 'gl-button')
end
- shared_examples 'follower tabs with count badges' do
- it 'shows 0 followers and 0 following' do
+ shared_examples 'follower links with count badges' do
+ it 'shows no count if no followers / following' do
subject
- expect(page).to have_content('Followers 0')
- expect(page).to have_content('Following 0')
+ within_testid('super-sidebar') do
+ expect(page).to have_link(text: 'Followers')
+ expect(page).to have_link(text: 'Following')
+ end
end
- it 'shows 1 followers and 1 following' do
+ it 'shows count if followers / following' do
follower.follow(user)
user.follow(followee)
subject
- expect(page).to have_content('Followers 1')
- expect(page).to have_content('Following 1')
+ within_testid('super-sidebar') do
+ expect(page).to have_link(text: 'Followers 1')
+ expect(page).to have_link(text: 'Following 1')
+ end
end
end
- it_behaves_like 'follower tabs with count badges'
+ it_behaves_like 'follower links with count badges'
context 'with profile_tabs_vue feature flag disabled' do
before_all do
stub_feature_flags(profile_tabs_vue: false)
end
- it_behaves_like 'follower tabs with count badges'
+ it_behaves_like 'follower links with count badges'
end
it 'does show button to follow' do
diff --git a/spec/features/users/signup_spec.rb b/spec/features/users/signup_spec.rb
index 968308938d1..d873c4846fd 100644
--- a/spec/features/users/signup_spec.rb
+++ b/spec/features/users/signup_spec.rb
@@ -54,7 +54,7 @@ RSpec.shared_examples 'Signup name validation' do |field, max_length, label|
end
end
-RSpec.describe 'Signup', :js, feature_category: :user_profile do
+RSpec.describe 'Signup', :js, feature_category: :user_management do
include TermsHelper
let(:new_user) { build_stubbed(:user) }
diff --git a/spec/features/users/snippets_spec.rb b/spec/features/users/snippets_spec.rb
index 98ac9fa5f92..3a56b371a8c 100644
--- a/spec/features/users/snippets_spec.rb
+++ b/spec/features/users/snippets_spec.rb
@@ -4,10 +4,10 @@ require 'spec_helper'
RSpec.describe 'Snippets tab on a user profile', :js, feature_category: :source_code_management do
context 'when the user has snippets' do
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
before do
- stub_feature_flags(profile_tabs_vue: false, super_sidebar_logged_out: false)
+ stub_feature_flags(profile_tabs_vue: false)
end
context 'pagination' do
@@ -16,7 +16,7 @@ RSpec.describe 'Snippets tab on a user profile', :js, feature_category: :source_
before do
allow(Snippet).to receive(:default_per_page).and_return(1)
visit user_path(user)
- page.within('.user-profile-nav') { click_link 'Snippets' }
+ within_testid('super-sidebar') { click_link 'Snippets' }
wait_for_requests
end
@@ -30,9 +30,9 @@ RSpec.describe 'Snippets tab on a user profile', :js, feature_category: :source_
let!(:other_snippet) { create(:snippet, :public) }
it 'contains only internal and public snippets of a user when a user is logged in' do
- sign_in(create(:user, :no_super_sidebar))
+ sign_in(create(:user))
visit user_path(user)
- page.within('.user-profile-nav') { click_link 'Snippets' }
+ within_testid('super-sidebar') { click_link 'Snippets' }
wait_for_requests
expect(page).to have_selector('.snippet-row', count: 2)
@@ -43,7 +43,7 @@ RSpec.describe 'Snippets tab on a user profile', :js, feature_category: :source_
it 'contains only public snippets of a user when a user is not logged in' do
visit user_path(user)
- page.within('.user-profile-nav') { click_link 'Snippets' }
+ within_testid('super-sidebar') { click_link 'Snippets' }
wait_for_requests
expect(page).to have_selector('.snippet-row', count: 1)
diff --git a/spec/features/users/terms_spec.rb b/spec/features/users/terms_spec.rb
index e51ed3a0e80..28191587572 100644
--- a/spec/features/users/terms_spec.rb
+++ b/spec/features/users/terms_spec.rb
@@ -27,7 +27,7 @@ RSpec.describe 'Users > Terms', :js, feature_category: :user_profile do
end
context 'when user is a project bot' do
- let(:project_bot) { create(:user, :no_super_sidebar, :project_bot) }
+ let(:project_bot) { create(:user, :project_bot) }
before do
enforce_terms
@@ -42,7 +42,7 @@ RSpec.describe 'Users > Terms', :js, feature_category: :user_profile do
end
context 'when user is a service account' do
- let(:service_account) { create(:user, :no_super_sidebar, :service_account) }
+ let(:service_account) { create(:user, :service_account) }
before do
enforce_terms
@@ -57,7 +57,7 @@ RSpec.describe 'Users > Terms', :js, feature_category: :user_profile do
end
context 'when signed in' do
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
before do
sign_in(user)
@@ -115,7 +115,7 @@ RSpec.describe 'Users > Terms', :js, feature_category: :user_profile do
# Application settings are cached for a minute
travel_to 2.minutes.from_now do
- within('.nav-sidebar') do
+ within('.contextual-nav') do
click_link 'Issues'
end
diff --git a/spec/features/users/user_browses_projects_on_user_page_spec.rb b/spec/features/users/user_browses_projects_on_user_page_spec.rb
index 5e047192e7b..039b1bbe5b1 100644
--- a/spec/features/users/user_browses_projects_on_user_page_spec.rb
+++ b/spec/features/users/user_browses_projects_on_user_page_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'Users > User browses projects on user page', :js, feature_category: :groups_and_projects do
- let!(:user) { create(:user, :no_super_sidebar) }
+ let!(:user) { create(:user) }
let!(:private_project) do
create :project, :private, name: 'private', namespace: user.namespace do |project|
project.add_maintainer(user)
@@ -23,13 +23,13 @@ RSpec.describe 'Users > User browses projects on user page', :js, feature_catego
end
def click_nav_link(name)
- page.within '.nav-links' do
+ within_testid('super-sidebar') do
click_link name
end
end
before do
- stub_feature_flags(profile_tabs_vue: false, super_sidebar_logged_out: false)
+ stub_feature_flags(profile_tabs_vue: false)
end
it 'hides loading spinner after load', :js do
@@ -87,7 +87,7 @@ RSpec.describe 'Users > User browses projects on user page', :js, feature_catego
end
context 'when signed in as another user' do
- let(:another_user) { create(:user, :no_super_sidebar) }
+ let(:another_user) { create(:user) }
before do
sign_in(another_user)
diff --git a/spec/features/webauthn_spec.rb b/spec/features/webauthn_spec.rb
index 52e2b375187..72463a0b9ab 100644
--- a/spec/features/webauthn_spec.rb
+++ b/spec/features/webauthn_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe 'Using WebAuthn Devices for Authentication', :js, feature_categor
# TODO: it_behaves_like 'hardware device for 2fa', 'WebAuthn'
describe 'registration' do
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
before do
gitlab_sign_in(user)
@@ -58,7 +58,7 @@ RSpec.describe 'Using WebAuthn Devices for Authentication', :js, feature_categor
gitlab_sign_out
# Second user
- user = create(:user, :no_super_sidebar)
+ user = create(:user)
gitlab_sign_in(user)
visit profile_account_path
enable_two_factor_authentication
@@ -126,7 +126,7 @@ RSpec.describe 'Using WebAuthn Devices for Authentication', :js, feature_categor
it_behaves_like 'hardware device for 2fa', 'WebAuthn'
describe 'registration' do
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
before do
gitlab_sign_in(user)
@@ -161,7 +161,7 @@ RSpec.describe 'Using WebAuthn Devices for Authentication', :js, feature_categor
gitlab_sign_out
# Second user
- user = create(:user, :no_super_sidebar)
+ user = create(:user)
gitlab_sign_in(user)
user.update_attribute(:otp_required_for_login, true)
visit profile_account_path
@@ -227,7 +227,7 @@ RSpec.describe 'Using WebAuthn Devices for Authentication', :js, feature_categor
describe 'authentication' do
let(:otp_required_for_login) { true }
- let(:user) { create(:user, :no_super_sidebar, webauthn_xid: WebAuthn.generate_user_id, otp_required_for_login: otp_required_for_login) }
+ let(:user) { create(:user, webauthn_xid: WebAuthn.generate_user_id, otp_required_for_login: otp_required_for_login) }
let!(:webauthn_device) do
add_webauthn_device(app_id, user)
end
@@ -256,7 +256,7 @@ RSpec.describe 'Using WebAuthn Devices for Authentication', :js, feature_categor
describe 'when a given WebAuthn device has already been registered by another user' do
describe 'but not the current user' do
- let(:other_user) { create(:user, :no_super_sidebar, webauthn_xid: WebAuthn.generate_user_id, otp_required_for_login: otp_required_for_login) }
+ let(:other_user) { create(:user, webauthn_xid: WebAuthn.generate_user_id, otp_required_for_login: otp_required_for_login) }
it 'does not allow logging in with that particular device' do
# Register other user with a different WebAuthn device
@@ -277,7 +277,7 @@ RSpec.describe 'Using WebAuthn Devices for Authentication', :js, feature_categor
it "allows logging in with that particular device" do
pending("support for passing credential options in FakeClient")
# Register current user with the same WebAuthn device
- current_user = create(:user, :no_super_sidebar)
+ current_user = create(:user)
gitlab_sign_in(current_user)
visit profile_account_path
manage_two_factor_authentication
diff --git a/spec/features/whats_new_spec.rb b/spec/features/whats_new_spec.rb
index c8bcf5f6ef0..887994106b6 100644
--- a/spec/features/whats_new_spec.rb
+++ b/spec/features/whats_new_spec.rb
@@ -2,36 +2,28 @@
require "spec_helper"
-RSpec.describe "renders a `whats new` dropdown item", feature_category: :onboarding do
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+RSpec.describe "renders a `whats new` dropdown item", :js, feature_category: :onboarding do
+ let_it_be(:user) { create(:user) }
context 'when not logged in' do
- before do
- stub_feature_flags(super_sidebar_logged_out: false)
- end
-
it 'and on SaaS it renders', :saas do
visit user_path(user)
- page.within '.header-help' do
- find('.header-help-dropdown-toggle').click
+ within_testid('super-sidebar') { click_on 'Help' }
- expect(page).to have_button(text: "What's new")
- end
+ expect(page).to have_button(text: "What's new")
end
it "doesn't render what's new" do
visit user_path(user)
- page.within '.header-help' do
- find('.header-help-dropdown-toggle').click
+ within_testid('super-sidebar') { click_on 'Help' }
- expect(page).not_to have_button(text: "What's new")
- end
+ expect(page).not_to have_button(text: "What's new")
end
end
- context 'when logged in', :js do
+ context 'when logged in' do
before do
sign_in(user)
end
@@ -40,7 +32,7 @@ RSpec.describe "renders a `whats new` dropdown item", feature_category: :onboard
Gitlab::CurrentSettings.update!(whats_new_variant: ApplicationSetting.whats_new_variants[:all_tiers])
visit root_dashboard_path
- find('.header-help-dropdown-toggle').click
+ within_testid('super-sidebar') { click_on 'Help' }
expect(page).to have_button(text: "What's new")
end
@@ -49,7 +41,7 @@ RSpec.describe "renders a `whats new` dropdown item", feature_category: :onboard
Gitlab::CurrentSettings.update!(whats_new_variant: ApplicationSetting.whats_new_variants[:disabled])
visit root_dashboard_path
- find('.header-help-dropdown-toggle').click
+ within_testid('super-sidebar') { click_on 'Help' }
expect(page).not_to have_button(text: "What's new")
end
@@ -57,24 +49,24 @@ RSpec.describe "renders a `whats new` dropdown item", feature_category: :onboard
it 'shows notification dot and count and removes it once viewed' do
visit root_dashboard_path
- page.within '.header-help' do
- expect(page).to have_selector('.notification-dot', visible: true)
+ within_testid('super-sidebar') do
+ click_on 'Help'
+ button = find_button(text: "What's new")
- find('.header-help-dropdown-toggle').click
+ has_testid?('notification-dot', visible: true)
+ expect(button).to have_selector('.badge-pill')
- expect(page).to have_button(text: "What's new")
- expect(page).to have_selector('.js-whats-new-notification-count')
-
- find('button', text: "What's new").click
+ button.click
end
find('.whats-new-drawer .gl-drawer-close-button').click
- find('.header-help-dropdown-toggle').click
- page.within '.header-help' do
- expect(page).not_to have_selector('.notification-dot', visible: true)
- expect(page).to have_button(text: "What's new")
- expect(page).not_to have_selector('.js-whats-new-notification-count')
+ within_testid('super-sidebar') do
+ click_on 'Help'
+ button = find_button(text: "What's new")
+
+ has_testid?('notification-dot', visible: false)
+ expect(button).not_to have_selector('.badge-pill')
end
end
end
diff --git a/spec/finders/ci/catalog/resources/versions_finder_spec.rb b/spec/finders/ci/catalog/resources/versions_finder_spec.rb
new file mode 100644
index 00000000000..b2418aa45dd
--- /dev/null
+++ b/spec/finders/ci/catalog/resources/versions_finder_spec.rb
@@ -0,0 +1,106 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::Catalog::Resources::VersionsFinder, feature_category: :pipeline_composition do
+ include_context 'when there are catalog resources with versions'
+
+ let(:sort) { nil }
+ let(:latest) { nil }
+ let(:params) { { sort: sort, latest: latest }.compact }
+
+ subject(:execute) { described_class.new([resource1, resource2], current_user, params).execute }
+
+ it 'avoids N+1 queries when authorizing multiple catalog resources', :request_store do
+ control_count = ActiveRecord::QueryRecorder.new { execute }
+
+ # A new user is required to avoid a false positive from cached user authorization queries
+ new_user = create(:user)
+
+ expect do
+ described_class.new([resource1, resource2, resource3], new_user, params).execute
+ end.not_to exceed_query_limit(control_count)
+ end
+
+ context 'when the user is not authorized for any catalog resource' do
+ it 'returns empty response' do
+ is_expected.to be_empty
+ end
+ end
+
+ describe 'versions' do
+ before_all do
+ resource1.project.add_guest(current_user)
+ end
+
+ it 'returns the versions of the authorized catalog resource' do
+ expect(execute).to match_array([v1_0, v1_1])
+ end
+
+ context 'with sort parameter' do
+ it 'returns versions ordered by released_at descending by default' do
+ expect(execute).to eq([v1_1, v1_0])
+ end
+
+ context 'when sort is released_at_asc' do
+ let(:sort) { 'released_at_asc' }
+
+ it 'returns versions ordered by released_at ascending' do
+ expect(execute).to eq([v1_0, v1_1])
+ end
+ end
+
+ context 'when sort is created_asc' do
+ let(:sort) { 'created_asc' }
+
+ it 'returns versions ordered by created_at ascending' do
+ expect(execute).to eq([v1_1, v1_0])
+ end
+ end
+
+ context 'when sort is created_desc' do
+ let(:sort) { 'created_desc' }
+
+ it 'returns versions ordered by created_at descending' do
+ expect(execute).to eq([v1_0, v1_1])
+ end
+ end
+ end
+
+ it 'preloads associations' do
+ expect(Ci::Catalog::Resources::Version).to receive(:preloaded).once.and_call_original
+
+ execute
+ end
+ end
+
+ describe 'latest versions' do
+ before_all do
+ resource1.project.add_guest(current_user)
+ resource2.project.add_guest(current_user)
+ end
+
+ let(:latest) { true }
+
+ it 'returns the latest version for each authorized catalog resource' do
+ expect(execute).to match_array([v1_1, v2_1])
+ end
+
+ context 'when one catalog resource does not have versions' do
+ it 'returns the latest version of only the catalog resource with versions' do
+ resource1.versions.delete_all(:delete_all)
+
+ is_expected.to match_array([v2_1])
+ end
+ end
+
+ context 'when no catalog resource has versions' do
+ it 'returns empty response' do
+ resource1.versions.delete_all(:delete_all)
+ resource2.versions.delete_all(:delete_all)
+
+ is_expected.to be_empty
+ end
+ end
+ end
+end
diff --git a/spec/finders/ci/runners_finder_spec.rb b/spec/finders/ci/runners_finder_spec.rb
index 06cca035c6f..7f680f50297 100644
--- a/spec/finders/ci/runners_finder_spec.rb
+++ b/spec/finders/ci/runners_finder_spec.rb
@@ -148,6 +148,22 @@ RSpec.describe Ci::RunnersFinder, feature_category: :runner_fleet do
described_class.new(current_user: admin, params: { tag_name: %w[tag1 tag2] }).execute
end
end
+
+ context 'by creator' do
+ it 'calls the corresponding scope on Ci::Runner' do
+ expect(Ci::Runner).to receive(:with_creator_id).with('1').and_call_original
+
+ described_class.new(current_user: admin, params: { creator_id: '1' }).execute
+ end
+ end
+
+ context 'by version' do
+ it 'calls the corresponding scope on Ci::Runner' do
+ expect(Ci::Runner).to receive(:with_version_prefix).with('15.').and_call_original
+
+ described_class.new(current_user: admin, params: { version_prefix: '15.' }).execute
+ end
+ end
end
context 'sorting' do
@@ -291,6 +307,9 @@ RSpec.describe Ci::RunnersFinder, feature_category: :runner_fleet do
let_it_be(:runner_project_5) { create(:ci_runner, :project, contacted_at: 3.minutes.ago, tag_list: %w[runner_tag], projects: [project_4]) }
let_it_be(:runner_project_6) { create(:ci_runner, :project, contacted_at: 2.minutes.ago, projects: [project_5]) }
let_it_be(:runner_project_7) { create(:ci_runner, :project, contacted_at: 1.minute.ago, projects: [project_6]) }
+ let_it_be(:runner_manager_1) { create(:ci_runner_machine, runner: runner_sub_group_1, version: '15.11.0') }
+ let_it_be(:runner_manager_2) { create(:ci_runner_machine, runner: runner_sub_group_2, version: '15.11.1') }
+ let_it_be(:runner_manager_3) { create(:ci_runner_machine, runner: runner_sub_group_3, version: '15.10.1') }
let(:target_group) { nil }
let(:membership) { nil }
@@ -431,6 +450,32 @@ RSpec.describe Ci::RunnersFinder, feature_category: :runner_fleet do
runner_project_3, runner_project_2, runner_project_1])
end
end
+
+ context 'by version prefix' do
+ context 'search by major version' do
+ let(:extra_params) { { version_prefix: '15.' } }
+
+ it 'returns correct runner' do
+ is_expected.to contain_exactly(runner_sub_group_1, runner_sub_group_2, runner_sub_group_3)
+ end
+ end
+
+ context 'search by minor version' do
+ let(:extra_params) { { version_prefix: '15.11.' } }
+
+ it 'returns correct runner' do
+ is_expected.to contain_exactly(runner_sub_group_1, runner_sub_group_2)
+ end
+ end
+
+ context 'search by patch version' do
+ let(:extra_params) { { version_prefix: '15.11.1' } }
+
+ it 'returns correct runner' do
+ is_expected.to contain_exactly(runner_sub_group_2)
+ end
+ end
+ end
end
end
end
@@ -560,6 +605,7 @@ RSpec.describe Ci::RunnersFinder, feature_category: :runner_fleet do
let_it_be(:runner_project_active) { create(:ci_runner, :project, contacted_at: 5.minutes.ago, active: true, projects: [project]) }
let_it_be(:runner_project_inactive) { create(:ci_runner, :project, contacted_at: 5.minutes.ago, active: false, projects: [project]) }
let_it_be(:runner_other_project_inactive) { create(:ci_runner, :project, contacted_at: 5.minutes.ago, active: false, projects: [other_project]) }
+ let_it_be(:runner_manager) { create(:ci_runner_machine, runner: runner_instance_inactive, version: '15.10.0') }
context 'by search term' do
let_it_be(:runner_project_1) { create(:ci_runner, :project, contacted_at: 5.minutes.ago, description: 'runner_project_search', projects: [project]) }
@@ -608,6 +654,24 @@ RSpec.describe Ci::RunnersFinder, feature_category: :runner_fleet do
expect(subject).to match_array([runner_project_active, runner_project_inactive])
end
end
+
+ context 'by creator' do
+ let_it_be(:runner_creator_1) { create(:ci_runner, creator_id: '1') }
+
+ let(:extra_params) { { creator_id: '1' } }
+
+ it 'returns correct runners' do
+ is_expected.to contain_exactly(runner_creator_1)
+ end
+ end
+
+ context 'by version prefix' do
+ let(:extra_params) { { version_prefix: '15.' } }
+
+ it 'returns correct runners' do
+ is_expected.to contain_exactly(runner_instance_inactive)
+ end
+ end
end
end
diff --git a/spec/finders/data_transfer/mocked_transfer_finder_spec.rb b/spec/finders/data_transfer/mocked_transfer_finder_spec.rb
deleted file mode 100644
index f60bc98f587..00000000000
--- a/spec/finders/data_transfer/mocked_transfer_finder_spec.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe DataTransfer::MockedTransferFinder, feature_category: :source_code_management do
- describe '#execute' do
- subject(:execute) { described_class.new.execute }
-
- it 'returns mock data' do
- expect(execute.first).to include(
- date: '2023-01-01',
- repository_egress: be_a(Integer),
- artifacts_egress: be_a(Integer),
- packages_egress: be_a(Integer),
- registry_egress: be_a(Integer),
- total_egress: be_a(Integer)
- )
-
- expect(execute.size).to eq(12)
- end
- end
-end
diff --git a/spec/finders/milestones_finder_spec.rb b/spec/finders/milestones_finder_spec.rb
index c4c62e21ad9..118679a4911 100644
--- a/spec/finders/milestones_finder_spec.rb
+++ b/spec/finders/milestones_finder_spec.rb
@@ -22,7 +22,7 @@ RSpec.describe MilestonesFinder do
end
it 'returns milestones for groups' do
- result = described_class.new(group_ids: group.id, state: 'all').execute
+ result = described_class.new(group_ids: group.id, state: 'all').execute
expect(result).to contain_exactly(milestone_5, milestone_1, milestone_2)
end
diff --git a/spec/finders/organizations/user_organizations_finder_spec.rb b/spec/finders/organizations/user_organizations_finder_spec.rb
new file mode 100644
index 00000000000..71c9b861831
--- /dev/null
+++ b/spec/finders/organizations/user_organizations_finder_spec.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Organizations::UserOrganizationsFinder, '#execute', feature_category: :cell do
+ let_it_be(:admin) { create(:user, :admin) }
+ let_it_be(:organization_user) { create(:organization_user) }
+ let_it_be(:organization) { organization_user.organization }
+ let_it_be(:another_organization) { create(:organization) }
+ let_it_be(:another_user) { create(:user) }
+
+ let(:current_user) { organization_user.user }
+ let(:target_user) { organization_user.user }
+
+ subject(:finder) { described_class.new(current_user, target_user).execute }
+
+ context 'when the current user has access to the organization' do
+ it { is_expected.to contain_exactly(organization) }
+ end
+
+ context 'when the current user is an admin' do
+ let(:current_user) { admin }
+
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it { is_expected.to contain_exactly(organization) }
+ end
+
+ context 'when admin mode is disabled' do
+ it { is_expected.to be_empty }
+ end
+ end
+
+ context 'when the current user does not access to the organization' do
+ let(:current_user) { another_user }
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'when the current user is nil' do
+ let(:current_user) { nil }
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'when the target user is nil' do
+ let(:target_user) { nil }
+
+ it { is_expected.to be_empty }
+ end
+end
diff --git a/spec/finders/packages/group_packages_finder_spec.rb b/spec/finders/packages/group_packages_finder_spec.rb
index e4a944eb837..a2698bc0153 100644
--- a/spec/finders/packages/group_packages_finder_spec.rb
+++ b/spec/finders/packages/group_packages_finder_spec.rb
@@ -203,7 +203,7 @@ RSpec.describe Packages::GroupPackagesFinder do
end
context 'group has package of all types' do
- package_types.each do |pt| # rubocop:disable RSpec/UselessDynamicDefinition
+ package_types.each do |pt| # rubocop:disable RSpec/UselessDynamicDefinition -- `pt` used in `let`
let_it_be("package_#{pt}") { create("#{pt}_package", project: project) }
end
diff --git a/spec/finders/packages/pypi/packages_finder_spec.rb b/spec/finders/packages/pypi/packages_finder_spec.rb
index 3957eb188da..26cfaa29a0c 100644
--- a/spec/finders/packages/pypi/packages_finder_spec.rb
+++ b/spec/finders/packages/pypi/packages_finder_spec.rb
@@ -64,6 +64,16 @@ RSpec.describe Packages::Pypi::PackagesFinder do
end
it { is_expected.to contain_exactly(package2, package3, package4) }
+
+ context 'when package registry is disabled for one project' do
+ before do
+ project2.project_feature.update!(package_registry_access_level: ProjectFeature::DISABLED)
+ end
+
+ it 'filters the packages from the disabled project' do
+ expect(subject).to contain_exactly(package2, package3)
+ end
+ end
end
end
end
diff --git a/spec/finders/personal_access_tokens_finder_spec.rb b/spec/finders/personal_access_tokens_finder_spec.rb
index d91b2c8f599..d7cc72fe8ed 100644
--- a/spec/finders/personal_access_tokens_finder_spec.rb
+++ b/spec/finders/personal_access_tokens_finder_spec.rb
@@ -2,16 +2,16 @@
require 'spec_helper'
-RSpec.describe PersonalAccessTokensFinder, :enable_admin_mode do
+RSpec.describe PersonalAccessTokensFinder, :enable_admin_mode, feature_category: :system_access do
using RSpec::Parameterized::TableSyntax
describe '#execute' do
- let(:admin) { create(:admin) }
- let(:user) { create(:user) }
- let(:other_user) { create(:user) }
- let(:project_bot) { create(:user, :project_bot) }
+ let_it_be(:admin) { create(:admin) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:other_user) { create(:user) }
+ let_it_be(:project_bot) { create(:user, :project_bot) }
- let!(:tokens) do
+ let_it_be(:tokens) do
{
active: create(:personal_access_token, user: user, name: 'my_pat_1'),
active_other: create(:personal_access_token, user: other_user, name: 'my_pat_2'),
@@ -24,6 +24,8 @@ RSpec.describe PersonalAccessTokensFinder, :enable_admin_mode do
}
end
+ let(:tokens_keys) { tokens.keys }
+
let(:params) { {} }
let(:current_user) { admin }
@@ -89,7 +91,7 @@ RSpec.describe PersonalAccessTokensFinder, :enable_admin_mode do
describe 'by user' do
where(:by_user, :expected_tokens) do
- nil | tokens.keys
+ nil | ref(:tokens_keys)
ref(:user) | [:active, :expired, :revoked, :active_impersonation, :expired_impersonation, :revoked_impersonation]
ref(:other_user) | [:active_other]
ref(:admin) | []
@@ -106,7 +108,7 @@ RSpec.describe PersonalAccessTokensFinder, :enable_admin_mode do
describe 'by users' do
where(:by_users, :expected_tokens) do
- nil | tokens.keys
+ nil | ref(:tokens_keys)
lazy { [user] } | [:active, :expired, :revoked, :active_impersonation, :expired_impersonation, :revoked_impersonation]
lazy { [other_user] } | [:active_other]
lazy { [user, other_user] } | [:active, :active_other, :expired, :revoked, :active_impersonation, :expired_impersonation, :revoked_impersonation]
@@ -124,10 +126,10 @@ RSpec.describe PersonalAccessTokensFinder, :enable_admin_mode do
describe 'by impersonation' do
where(:by_impersonation, :expected_tokens) do
- nil | tokens.keys
+ nil | ref(:tokens_keys)
true | [:active_impersonation, :expired_impersonation, :revoked_impersonation]
false | [:active, :active_other, :expired, :revoked, :bot]
- 'other' | tokens.keys
+ 'other' | ref(:tokens_keys)
end
with_them do
@@ -141,10 +143,10 @@ RSpec.describe PersonalAccessTokensFinder, :enable_admin_mode do
describe 'by state' do
where(:by_state, :expected_tokens) do
- nil | tokens.keys
+ nil | ref(:tokens_keys)
'active' | [:active, :active_other, :active_impersonation, :bot]
'inactive' | [:expired, :revoked, :expired_impersonation, :revoked_impersonation]
- 'other' | tokens.keys
+ 'other' | ref(:tokens_keys)
end
with_them do
@@ -158,9 +160,9 @@ RSpec.describe PersonalAccessTokensFinder, :enable_admin_mode do
describe 'by owner type' do
where(:by_owner_type, :expected_tokens) do
- nil | tokens.keys
+ nil | ref(:tokens_keys)
'human' | [:active, :active_other, :expired, :revoked, :active_impersonation, :expired_impersonation, :revoked_impersonation]
- 'other' | tokens.keys
+ 'other' | ref(:tokens_keys)
end
with_them do
@@ -197,7 +199,7 @@ RSpec.describe PersonalAccessTokensFinder, :enable_admin_mode do
where(:by_created_before, :expected_tokens) do
6.days.ago | []
2.days.ago | [:active_other]
- 2.days.from_now | tokens.keys
+ 2.days.from_now | ref(:tokens_keys)
end
with_them do
@@ -211,7 +213,7 @@ RSpec.describe PersonalAccessTokensFinder, :enable_admin_mode do
describe 'by created after' do
where(:by_created_after, :expected_tokens) do
- 6.days.ago | tokens.keys
+ 6.days.ago | ref(:tokens_keys)
2.days.ago | [:active, :expired, :revoked, :active_impersonation, :expired_impersonation, :revoked_impersonation, :bot]
2.days.from_now | []
end
@@ -236,7 +238,7 @@ RSpec.describe PersonalAccessTokensFinder, :enable_admin_mode do
where(:by_last_used_before, :expected_tokens) do
6.days.ago | []
2.days.ago | [:active_other]
- 2.days.from_now | tokens.keys
+ 2.days.from_now | ref(:tokens_keys)
end
with_them do
@@ -250,7 +252,7 @@ RSpec.describe PersonalAccessTokensFinder, :enable_admin_mode do
describe 'by last used after' do
where(:by_last_used_after, :expected_tokens) do
- 6.days.ago | tokens.keys
+ 6.days.ago | ref(:tokens_keys)
2.days.ago | [:active, :expired, :revoked, :active_impersonation, :expired_impersonation, :revoked_impersonation, :bot]
2.days.from_now | []
end
@@ -267,7 +269,7 @@ RSpec.describe PersonalAccessTokensFinder, :enable_admin_mode do
describe 'by search' do
where(:by_search, :expected_tokens) do
- nil | tokens.keys
+ nil | ref(:tokens_keys)
'my_pat' | [:active, :active_other]
'other' | []
end
@@ -283,10 +285,10 @@ RSpec.describe PersonalAccessTokensFinder, :enable_admin_mode do
describe 'sort' do
where(:sort, :expected_tokens) do
- nil | tokens.keys
+ nil | ref(:tokens_keys)
'id_asc' | [:active, :active_other, :expired, :revoked, :active_impersonation, :expired_impersonation, :revoked_impersonation, :bot]
'id_desc' | [:bot, :revoked_impersonation, :expired_impersonation, :active_impersonation, :revoked, :expired, :active_other, :active]
- 'other' | tokens.keys
+ 'other' | ref(:tokens_keys)
end
with_them do
diff --git a/spec/finders/projects/ml/model_finder_spec.rb b/spec/finders/projects/ml/model_finder_spec.rb
index 1d869e1792d..a2c2836a63d 100644
--- a/spec/finders/projects/ml/model_finder_spec.rb
+++ b/spec/finders/projects/ml/model_finder_spec.rb
@@ -6,24 +6,57 @@ RSpec.describe Projects::Ml::ModelFinder, feature_category: :mlops do
let_it_be(:project) { create(:project) }
let_it_be(:model1) { create(:ml_models, :with_versions, project: project) }
let_it_be(:model2) { create(:ml_models, :with_versions, project: project) }
- let_it_be(:model3) { create(:ml_models) }
+ let_it_be(:model3) { create(:ml_models, name: "#{model1.name}_1", project: project) }
+ let_it_be(:other_model) { create(:ml_models) }
+ let_it_be(:project_models) { [model1, model2, model3] }
- subject(:models) { described_class.new(project).execute.to_a }
+ let(:params) { {} }
- it 'returns models for project' do
- is_expected.to match_array([model1, model2])
- end
+ subject(:models) { described_class.new(project, params).execute.to_a }
+
+ describe 'default params' do
+ it 'returns models for project ordered by id' do
+ is_expected.to eq([model3, model2, model1])
+ end
+
+ it 'including the latest version and project', :aggregate_failures do
+ expect(models[0].association_cached?(:latest_version)).to be(true)
+ expect(models[0].association_cached?(:project)).to be(true)
+ expect(models[1].association_cached?(:latest_version)).to be(true)
+ expect(models[1].association_cached?(:project)).to be(true)
+ end
+
+ it 'does not return models belonging to a different project' do
+ is_expected.not_to include(other_model)
+ end
- it 'including the latest version', :aggregate_failures do
- expect(models[0].association_cached?(:latest_version)).to be(true)
- expect(models[1].association_cached?(:latest_version)).to be(true)
+ it 'includes version count' do
+ expect(models[0].version_count).to be(models[0].versions.count)
+ end
end
- it 'does not return models belonging to a different project' do
- is_expected.not_to include(model3)
+ context 'when name is passed' do
+ let(:params) { { name: model1.name } }
+
+ it 'searches by name' do
+ is_expected.to match_array([model1, model3])
+ end
end
- it 'includes version count' do
- expect(models[0].version_count).to be(models[0].versions.count)
+ describe 'sorting' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:test_case, :order_by, :direction, :expected_order) do
+ 'default params' | nil | nil | [2, 1, 0]
+ 'ascending order' | 'id' | 'ASC' | [0, 1, 2]
+ 'by column' | 'name' | 'ASC' | [0, 2, 1]
+ 'invalid sort' | nil | 'UP' | [2, 1, 0]
+ 'invalid order by' | 'INVALID' | nil | [2, 1, 0]
+ end
+ with_them do
+ let(:params) { { order_by: order_by, sort: direction } }
+
+ it { expect(subject).to eq(project_models.values_at(*expected_order)) }
+ end
end
end
diff --git a/spec/finders/projects_finder_spec.rb b/spec/finders/projects_finder_spec.rb
index a795df4dec6..f7afd96fa09 100644
--- a/spec/finders/projects_finder_spec.rb
+++ b/spec/finders/projects_finder_spec.rb
@@ -66,6 +66,18 @@ RSpec.describe ProjectsFinder, feature_category: :groups_and_projects do
it { is_expected.to eq([internal_project]) }
end
+ describe 'with full_paths' do
+ let_it_be(:second_public_project) do
+ create(:project, :public, :merge_requests_enabled, :issues_disabled, group: group, name: 'second-public', path: 'second-public')
+ end
+
+ context 'only returns projects matching the provided full paths' do
+ let(:params) { { full_paths: [public_project.full_path, second_public_project.full_path] } }
+
+ it { is_expected.to match_array([public_project, second_public_project]) }
+ end
+ end
+
describe 'with id_after' do
context 'only returns projects with a project id greater than given' do
let(:params) { { id_after: internal_project.id } }
@@ -413,13 +425,30 @@ RSpec.describe ProjectsFinder, feature_category: :groups_and_projects do
it { is_expected.to match_array([internal_project]) }
end
- describe 'always filters by without_deleted' do
+ describe 'filters by without_deleted by default' do
let_it_be(:pending_delete_project) { create(:project, :public, pending_delete: true) }
it 'returns projects that are not pending_delete' do
expect(subject).not_to include(pending_delete_project)
expect(subject).to include(public_project, internal_project)
end
+
+ context 'when include_pending_delete param is provided' do
+ let(:params) { { include_pending_delete: true } }
+
+ it 'returns projects that are not pending_delete' do
+ expect(subject).not_to include(pending_delete_project)
+ expect(subject).to include(public_project, internal_project)
+ end
+
+ context 'when user is an admin', :enable_admin_mode do
+ let(:current_user) { create(:admin) }
+
+ it 'also return pending_delete projects' do
+ expect(subject).to include(public_project, internal_project, pending_delete_project)
+ end
+ end
+ end
end
describe 'filter by last_activity_before' do
diff --git a/spec/finders/user_group_notification_settings_finder_spec.rb b/spec/finders/user_group_notification_settings_finder_spec.rb
index ac59a42d813..83d0d343c04 100644
--- a/spec/finders/user_group_notification_settings_finder_spec.rb
+++ b/spec/finders/user_group_notification_settings_finder_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe UserGroupNotificationSettingsFinder do
+RSpec.describe UserGroupNotificationSettingsFinder, feature_category: :team_planning do
let_it_be(:user) { create(:user) }
subject { described_class.new(user, Group.where(id: groups.map(&:id))).execute }
@@ -127,38 +127,38 @@ RSpec.describe UserGroupNotificationSettingsFinder do
expect(result.count).to eq(3)
end
- end
- end
- context 'preloading `emails_disabled`' do
- let_it_be(:root_group) { create(:group) }
- let_it_be(:sub_group) { create(:group, parent: root_group) }
- let_it_be(:sub_sub_group) { create(:group, parent: sub_group) }
+ context 'preloading `emails_enabled`' do
+ let_it_be(:root_group) { create(:group) }
+ let_it_be(:sub_group) { create(:group, parent: root_group) }
+ let_it_be(:sub_sub_group) { create(:group, parent: sub_group) }
- let_it_be(:another_root_group) { create(:group) }
- let_it_be(:sub_group_with_emails_disabled) { create(:group, emails_disabled: true, parent: another_root_group) }
- let_it_be(:another_sub_sub_group) { create(:group, parent: sub_group_with_emails_disabled) }
+ let_it_be(:another_root_group) { create(:group) }
+ let_it_be(:sub_group_with_emails_disabled) { create(:group, emails_enabled: false, parent: another_root_group) }
+ let_it_be(:another_sub_sub_group) { create(:group, parent: sub_group_with_emails_disabled) }
- let_it_be(:root_group_with_emails_disabled) { create(:group, emails_disabled: true) }
- let_it_be(:group) { create(:group, parent: root_group_with_emails_disabled) }
+ let_it_be(:root_group_with_emails_disabled) { create(:group, emails_enabled: false) }
+ let_it_be(:group) { create(:group, parent: root_group_with_emails_disabled) }
- let(:groups) { Group.where(id: [sub_sub_group, another_sub_sub_group, group]) }
+ let(:groups) { Group.where(id: [sub_sub_group, another_sub_sub_group, group]) }
- before do
- described_class.new(user, groups).execute
- end
+ before do
+ described_class.new(user, groups).execute
+ end
- it 'preloads the `group.emails_disabled` method' do
- recorder = ActiveRecord::QueryRecorder.new do
- groups.each(&:emails_disabled?)
- end
+ it 'preloads the `group.emails_enabled` method' do
+ recorder = ActiveRecord::QueryRecorder.new do
+ groups.each(&:emails_enabled?)
+ end
- expect(recorder.count).to eq(0)
- end
+ expect(recorder.count).to eq(0)
+ end
- it 'preloads the `group.emails_disabled` method correctly' do
- groups.each do |group|
- expect(group.emails_disabled?).to eq(Group.find(group.id).emails_disabled?) # compare the memoized and the freshly loaded value
+ it 'preloads the `group.emails_enabled` method correctly' do
+ groups.each do |group|
+ expect(group.emails_enabled?).to eq(Group.find(group.id).emails_enabled?) # compare the memoized and the freshly loaded value
+ end
+ end
end
end
end
diff --git a/spec/finders/vs_code/settings/settings_finder_spec.rb b/spec/finders/vs_code/settings/settings_finder_spec.rb
index b7b4308bbbd..fa24f5d0aec 100644
--- a/spec/finders/vs_code/settings/settings_finder_spec.rb
+++ b/spec/finders/vs_code/settings/settings_finder_spec.rb
@@ -34,8 +34,6 @@ RSpec.describe VsCode::Settings::SettingsFinder, feature_category: :web_ide do
let_it_be(:setting) { create(:vscode_setting, user: user) }
context 'when user has no settings with that type' do
- subject { finder.execute }
-
it 'returns an empty array' do
finder = described_class.new(user, ['profile'])
expect(finder.execute).to eq([])
@@ -43,8 +41,6 @@ RSpec.describe VsCode::Settings::SettingsFinder, feature_category: :web_ide do
end
context 'when user does have settings with the type' do
- subject { finder.execute }
-
it 'returns the record when a single setting exists' do
result = described_class.new(user, ['settings']).execute
expect(result.length).to eq(1)
@@ -53,9 +49,9 @@ RSpec.describe VsCode::Settings::SettingsFinder, feature_category: :web_ide do
end
it 'returns multiple records when more than one setting exists' do
- create(:vscode_setting, user: user, setting_type: 'profile')
+ create(:vscode_setting, user: user, setting_type: 'globalState')
- result = described_class.new(user, %w[settings profile]).execute
+ result = described_class.new(user, %w[settings globalState]).execute
expect(result.length).to eq(2)
end
end
diff --git a/spec/fixtures/api/schemas/entities/admin_users_data_attributes_paths.json b/spec/fixtures/api/schemas/entities/admin_users_data_attributes_paths.json
index 44d8e48a972..61472b273e1 100644
--- a/spec/fixtures/api/schemas/entities/admin_users_data_attributes_paths.json
+++ b/spec/fixtures/api/schemas/entities/admin_users_data_attributes_paths.json
@@ -1,19 +1,51 @@
{
"type": "object",
"properties": {
- "edit": { "type": "string" },
- "approve": { "type": "string" },
- "reject": { "type": "string" },
- "unblock": { "type": "string" },
- "block": { "type": "string" },
- "deactivate": { "type": "string" },
- "activate": { "type": "string" },
- "unlock": { "type": "string" },
- "delete": { "type": "string" },
- "delete_with_contributions": { "type": "string" },
- "admin_user": { "type": "string" },
- "ban": { "type": "string" },
- "unban": { "type": "string" }
+ "edit": {
+ "type": "string"
+ },
+ "approve": {
+ "type": "string"
+ },
+ "reject": {
+ "type": "string"
+ },
+ "unblock": {
+ "type": "string"
+ },
+ "block": {
+ "type": "string"
+ },
+ "deactivate": {
+ "type": "string"
+ },
+ "activate": {
+ "type": "string"
+ },
+ "unlock": {
+ "type": "string"
+ },
+ "delete": {
+ "type": "string"
+ },
+ "delete_with_contributions": {
+ "type": "string"
+ },
+ "admin_user": {
+ "type": "string"
+ },
+ "ban": {
+ "type": "string"
+ },
+ "unban": {
+ "type": "string"
+ },
+ "trust": {
+ "type": "string"
+ },
+ "untrust": {
+ "type": "string"
+ }
},
"required": [
"edit",
@@ -28,7 +60,9 @@
"delete_with_contributions",
"admin_user",
"ban",
- "unban"
+ "unban",
+ "trust",
+ "untrust"
],
"additionalProperties": false
}
diff --git a/spec/fixtures/api/schemas/entities/member.json b/spec/fixtures/api/schemas/entities/member.json
index cd8a4e0519b..38f8a245b49 100644
--- a/spec/fixtures/api/schemas/entities/member.json
+++ b/spec/fixtures/api/schemas/entities/member.json
@@ -11,7 +11,8 @@
"type",
"can_update",
"can_remove",
- "is_direct_member"
+ "is_direct_member",
+ "custom_roles"
],
"properties": {
"id": {
@@ -48,7 +49,8 @@
"type": "object",
"required": [
"integer_value",
- "string_value"
+ "string_value",
+ "member_role_id"
],
"properties": {
"integer_value": {
@@ -56,6 +58,12 @@
},
"string_value": {
"type": "string"
+ },
+ "member_role_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
}
},
"additionalProperties": false
@@ -138,6 +146,26 @@
}
},
"additionalProperties": false
+ },
+ "custom_roles": {
+ "type": "array",
+ "items": [
+ {
+ "type": "object",
+ "properties": {
+ "base_access_level": {
+ "type": "integer"
+ },
+ "member_role_id": {
+ "type": "integer"
+ },
+ "name": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ }
+ ]
}
}
-} \ No newline at end of file
+}
diff --git a/spec/fixtures/api/schemas/entities/merge_request_noteable.json b/spec/fixtures/api/schemas/entities/merge_request_noteable.json
index 4b790a2c34b..6f3c29b16e9 100644
--- a/spec/fixtures/api/schemas/entities/merge_request_noteable.json
+++ b/spec/fixtures/api/schemas/entities/merge_request_noteable.json
@@ -24,13 +24,11 @@
"type": "object",
"required": [
"can_create_note",
- "can_update",
- "can_approve"
+ "can_update"
],
"properties": {
"can_create_note": { "type": "boolean" },
- "can_update": { "type": "boolean" },
- "can_approve": { "type": "boolean" }
+ "can_update": { "type": "boolean" }
},
"additionalProperties": false
},
diff --git a/spec/fixtures/api/schemas/graphql/packages/package_details.json b/spec/fixtures/api/schemas/graphql/packages/package_details.json
index 2e7a950d330..1acc3f6ad7d 100644
--- a/spec/fixtures/api/schemas/graphql/packages/package_details.json
+++ b/spec/fixtures/api/schemas/graphql/packages/package_details.json
@@ -17,7 +17,8 @@
"statusMessage",
"canDestroy",
"lastDownloadedAt",
- "_links"
+ "_links",
+ "userPermissions"
],
"properties": {
"id": {
@@ -272,6 +273,15 @@
]
}
}
+ },
+ "userPermissions": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "destroyPackage": {
+ "type": "boolean"
+ }
+ }
}
}
}
diff --git a/spec/fixtures/api/schemas/graphql/packages/package_pypi_metadata.json b/spec/fixtures/api/schemas/graphql/packages/package_pypi_metadata.json
index cecebe3a0e9..c9b941ed8fa 100644
--- a/spec/fixtures/api/schemas/graphql/packages/package_pypi_metadata.json
+++ b/spec/fixtures/api/schemas/graphql/packages/package_pypi_metadata.json
@@ -1,13 +1,51 @@
{
"type": "object",
"additionalProperties": false,
- "required": ["id"],
+ "required": [
+ "id"
+ ],
"properties": {
"id": {
"type": "string"
},
+ "authorEmail": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "description": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "descriptionContentType": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "keywords": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "metadataVersion": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
"requiredPython": {
"type": "string"
+ },
+ "summary": {
+ "type": [
+ "string",
+ "null"
+ ]
}
}
}
diff --git a/spec/fixtures/api/schemas/group_link/group_link.json b/spec/fixtures/api/schemas/group_link/group_link.json
index 885ed6d18e0..4db38952ecc 100644
--- a/spec/fixtures/api/schemas/group_link/group_link.json
+++ b/spec/fixtures/api/schemas/group_link/group_link.json
@@ -44,6 +44,9 @@
"valid_roles": {
"type": "object"
},
+ "is_shared_with_group_private": {
+ "type": "boolean"
+ },
"shared_with_group": {
"type": "object",
"required": [
@@ -89,4 +92,4 @@
"type": "boolean"
}
}
-} \ No newline at end of file
+}
diff --git a/spec/fixtures/api/schemas/job/test_report_summary.json b/spec/fixtures/api/schemas/job/test_report_summary.json
new file mode 100644
index 00000000000..056a02854d4
--- /dev/null
+++ b/spec/fixtures/api/schemas/job/test_report_summary.json
@@ -0,0 +1,34 @@
+{
+ "type": "object",
+ "properties": {
+ "total": {
+ "type": "object",
+ "properties": {
+ "time": { "type": "number" },
+ "count": { "type": "number" },
+ "success": { "type": "number" },
+ "failed": { "type": "number" },
+ "skipped": { "type": "number" },
+ "error": { "type": "number" },
+ "suite_error": { "type": ["string", "null"] }
+ }
+ },
+ "test_suites": {
+ "type": "array",
+ "items": {
+ "name": { "type": "string" },
+ "total_time": { "type": "number" },
+ "total_count": { "type": "number" },
+ "success_count": { "type": "number" },
+ "failed_count": { "type": "number" },
+ "skipped_count": { "type": "number" },
+ "error_count": { "type": "number" },
+ "build_ids": {
+ "type": "array",
+ "items": { "type": "number" }
+ },
+ "suite_error": { "type": ["string", "null"] }
+ }
+ }
+ }
+}
diff --git a/spec/fixtures/api/schemas/ml/get_latest_versions.json b/spec/fixtures/api/schemas/ml/get_latest_versions.json
new file mode 100644
index 00000000000..cb2308fa637
--- /dev/null
+++ b/spec/fixtures/api/schemas/ml/get_latest_versions.json
@@ -0,0 +1,80 @@
+{
+ "type": "object",
+ "required": [
+ "model_versions"
+ ],
+ "properties": {
+ "model_versions": {
+ "type": "array",
+ "items": [
+ {
+ "type": "object",
+ "required": [
+ "name",
+ "version",
+ "creation_timestamp",
+ "last_updated_timestamp",
+ "user_id",
+ "current_stage",
+ "description",
+ "source",
+ "run_id",
+ "status",
+ "status_message",
+ "metadata",
+ "run_link",
+ "aliases"
+ ],
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "version": {
+ "type": "string"
+ },
+ "creation_timestamp": {
+ "type": "integer"
+ },
+ "last_updated_timestamp": {
+ "type": "integer"
+ },
+ "user_id": {
+ "type": "null"
+ },
+ "current_stage": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "source": {
+ "type": "string"
+ },
+ "run_id": {
+ "type": "string"
+ },
+ "status": {
+ "type": "string"
+ },
+ "status_message": {
+ "type": "string"
+ },
+ "metadata": {
+ "type": "array",
+ "items": {
+ }
+ },
+ "run_link": {
+ "type": "string"
+ },
+ "aliases": {
+ "type": "array",
+ "items": {
+ }
+ }
+ }
+ }
+ ]
+ }
+ }
+}
diff --git a/spec/fixtures/api/schemas/ml/get_model.json b/spec/fixtures/api/schemas/ml/get_model.json
new file mode 100644
index 00000000000..6b7ced6845b
--- /dev/null
+++ b/spec/fixtures/api/schemas/ml/get_model.json
@@ -0,0 +1,51 @@
+{
+ "type": "object",
+ "required": [
+ "registered_model"
+ ],
+ "properties": {
+ "model": {
+ "type": "object",
+ "required": [
+ "name",
+ "description",
+ "user_id"
+ ],
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "user_id": {
+ "type": "integer"
+ },
+ "creation_timestamp": {
+ "type": "string"
+ },
+ "last_updated_timestamp": {
+ "type": "string"
+ },
+ "tags": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "key",
+ "value"
+ ],
+ "properties": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/spec/fixtures/api/schemas/ml/update_model.json b/spec/fixtures/api/schemas/ml/update_model.json
new file mode 100644
index 00000000000..6b7ced6845b
--- /dev/null
+++ b/spec/fixtures/api/schemas/ml/update_model.json
@@ -0,0 +1,51 @@
+{
+ "type": "object",
+ "required": [
+ "registered_model"
+ ],
+ "properties": {
+ "model": {
+ "type": "object",
+ "required": [
+ "name",
+ "description",
+ "user_id"
+ ],
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "user_id": {
+ "type": "integer"
+ },
+ "creation_timestamp": {
+ "type": "string"
+ },
+ "last_updated_timestamp": {
+ "type": "string"
+ },
+ "tags": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "key",
+ "value"
+ ],
+ "properties": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/spec/fixtures/click_house/migrations/drop_table/1_create_some_table.rb b/spec/fixtures/click_house/migrations/drop_table/1_create_some_table.rb
new file mode 100644
index 00000000000..14ef80cbdb7
--- /dev/null
+++ b/spec/fixtures/click_house/migrations/drop_table/1_create_some_table.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+# rubocop: disable Gitlab/NamespacedClass -- Fixtures do not need to be namespaced
+class CreateSomeTable < ClickHouse::Migration
+ def up
+ execute <<~SQL
+ CREATE TABLE some (
+ id UInt64,
+ date Date
+ ) ENGINE = Memory
+ SQL
+ end
+end
+# rubocop: enable Gitlab/NamespacedClass
diff --git a/spec/fixtures/click_house/migrations/drop_table/2_drop_some_table.rb b/spec/fixtures/click_house/migrations/drop_table/2_drop_some_table.rb
new file mode 100644
index 00000000000..82045b08e21
--- /dev/null
+++ b/spec/fixtures/click_house/migrations/drop_table/2_drop_some_table.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+# rubocop: disable Gitlab/NamespacedClass -- Fixtures do not need to be namespaced
+class DropSomeTable < ClickHouse::Migration
+ def up
+ execute <<~SQL
+ DROP TABLE some
+ SQL
+ end
+end
+# rubocop: enable Gitlab/NamespacedClass
diff --git a/spec/fixtures/click_house/migrations/duplicate_name/1_create_some_table.rb b/spec/fixtures/click_house/migrations/duplicate_name/1_create_some_table.rb
new file mode 100644
index 00000000000..14ef80cbdb7
--- /dev/null
+++ b/spec/fixtures/click_house/migrations/duplicate_name/1_create_some_table.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+# rubocop: disable Gitlab/NamespacedClass -- Fixtures do not need to be namespaced
+class CreateSomeTable < ClickHouse::Migration
+ def up
+ execute <<~SQL
+ CREATE TABLE some (
+ id UInt64,
+ date Date
+ ) ENGINE = Memory
+ SQL
+ end
+end
+# rubocop: enable Gitlab/NamespacedClass
diff --git a/spec/fixtures/click_house/migrations/duplicate_name/2_create_some_table.rb b/spec/fixtures/click_house/migrations/duplicate_name/2_create_some_table.rb
new file mode 100644
index 00000000000..be6c1905502
--- /dev/null
+++ b/spec/fixtures/click_house/migrations/duplicate_name/2_create_some_table.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+# rubocop: disable Gitlab/NamespacedClass -- Fixtures do not need to be namespaced
+class CreateSomeTable2 < ClickHouse::Migration
+ def up
+ execute <<~SQL
+ CREATE TABLE some (
+ id UInt64,
+ date Date
+ ) ENGINE = Memory
+ SQL
+ end
+end
+# rubocop: enable Gitlab/NamespacedClass
diff --git a/spec/fixtures/click_house/migrations/duplicate_version/1_create_some_table.rb b/spec/fixtures/click_house/migrations/duplicate_version/1_create_some_table.rb
new file mode 100644
index 00000000000..14ef80cbdb7
--- /dev/null
+++ b/spec/fixtures/click_house/migrations/duplicate_version/1_create_some_table.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+# rubocop: disable Gitlab/NamespacedClass -- Fixtures do not need to be namespaced
+class CreateSomeTable < ClickHouse::Migration
+ def up
+ execute <<~SQL
+ CREATE TABLE some (
+ id UInt64,
+ date Date
+ ) ENGINE = Memory
+ SQL
+ end
+end
+# rubocop: enable Gitlab/NamespacedClass
diff --git a/spec/fixtures/click_house/migrations/duplicate_version/1_drop_some_table.rb b/spec/fixtures/click_house/migrations/duplicate_version/1_drop_some_table.rb
new file mode 100644
index 00000000000..82045b08e21
--- /dev/null
+++ b/spec/fixtures/click_house/migrations/duplicate_version/1_drop_some_table.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+# rubocop: disable Gitlab/NamespacedClass -- Fixtures do not need to be namespaced
+class DropSomeTable < ClickHouse::Migration
+ def up
+ execute <<~SQL
+ DROP TABLE some
+ SQL
+ end
+end
+# rubocop: enable Gitlab/NamespacedClass
diff --git a/spec/fixtures/click_house/migrations/migration_with_error/1_migration_with_error.rb b/spec/fixtures/click_house/migrations/migration_with_error/1_migration_with_error.rb
new file mode 100644
index 00000000000..b8ae3df2085
--- /dev/null
+++ b/spec/fixtures/click_house/migrations/migration_with_error/1_migration_with_error.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+# rubocop: disable Gitlab/NamespacedClass -- Fixtures do not need to be namespaced
+class MigrationWithError < ClickHouse::Migration
+ def up
+ raise ClickHouse::Client::DatabaseError, 'A migration error happened'
+ end
+end
+# rubocop: enable Gitlab/NamespacedClass
diff --git a/spec/fixtures/click_house/migrations/migrations_over_multiple_databases/1_create_some_table_on_main_db.rb b/spec/fixtures/click_house/migrations/migrations_over_multiple_databases/1_create_some_table_on_main_db.rb
new file mode 100644
index 00000000000..98d71d9507b
--- /dev/null
+++ b/spec/fixtures/click_house/migrations/migrations_over_multiple_databases/1_create_some_table_on_main_db.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+# rubocop: disable Gitlab/NamespacedClass -- Fixtures do not need to be namespaced
+class CreateSomeTableOnMainDb < ClickHouse::Migration
+ def up
+ execute <<~SQL
+ CREATE TABLE some (
+ id UInt64,
+ date Date
+ ) ENGINE = MergeTree
+ PRIMARY KEY(id)
+ SQL
+ end
+end
+# rubocop: enable Gitlab/NamespacedClass
diff --git a/spec/fixtures/click_house/migrations/migrations_over_multiple_databases/2_create_some_table_on_another_db.rb b/spec/fixtures/click_house/migrations/migrations_over_multiple_databases/2_create_some_table_on_another_db.rb
new file mode 100644
index 00000000000..b8cd86a67f5
--- /dev/null
+++ b/spec/fixtures/click_house/migrations/migrations_over_multiple_databases/2_create_some_table_on_another_db.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+# rubocop: disable Gitlab/NamespacedClass -- Fixtures do not need to be namespaced
+class CreateSomeTableOnAnotherDb < ClickHouse::Migration
+ SCHEMA = :another_db
+
+ def up
+ execute <<~SQL
+ CREATE TABLE some_on_another_db (
+ id UInt64,
+ date Date
+ ) ENGINE = Memory
+ SQL
+ end
+end
+# rubocop: enable Gitlab/NamespacedClass
diff --git a/spec/fixtures/click_house/migrations/migrations_over_multiple_databases/3_change_some_table_on_main_db.rb b/spec/fixtures/click_house/migrations/migrations_over_multiple_databases/3_change_some_table_on_main_db.rb
new file mode 100644
index 00000000000..9112ab79fc5
--- /dev/null
+++ b/spec/fixtures/click_house/migrations/migrations_over_multiple_databases/3_change_some_table_on_main_db.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+# rubocop: disable Gitlab/NamespacedClass -- Fixtures do not need to be namespaced
+class ChangeSomeTableOnMainDb < ClickHouse::Migration
+ def up
+ execute <<~SQL
+ ALTER TABLE some RENAME COLUMN date to timestamp
+ SQL
+ end
+end
+# rubocop: enable Gitlab/NamespacedClass
diff --git a/spec/fixtures/click_house/migrations/plain_table_creation/1_create_some_table.rb b/spec/fixtures/click_house/migrations/plain_table_creation/1_create_some_table.rb
new file mode 100644
index 00000000000..14ef80cbdb7
--- /dev/null
+++ b/spec/fixtures/click_house/migrations/plain_table_creation/1_create_some_table.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+# rubocop: disable Gitlab/NamespacedClass -- Fixtures do not need to be namespaced
+class CreateSomeTable < ClickHouse::Migration
+ def up
+ execute <<~SQL
+ CREATE TABLE some (
+ id UInt64,
+ date Date
+ ) ENGINE = Memory
+ SQL
+ end
+end
+# rubocop: enable Gitlab/NamespacedClass
diff --git a/spec/fixtures/click_house/migrations/plain_table_creation_on_invalid_database/1_create_some_table.rb b/spec/fixtures/click_house/migrations/plain_table_creation_on_invalid_database/1_create_some_table.rb
new file mode 100644
index 00000000000..ee900ef24c5
--- /dev/null
+++ b/spec/fixtures/click_house/migrations/plain_table_creation_on_invalid_database/1_create_some_table.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+# rubocop: disable Gitlab/NamespacedClass -- Fixtures do not need to be namespaced
+class CreateSomeTable < ClickHouse::Migration
+ SCHEMA = :unknown_database
+
+ def up
+ execute <<~SQL
+ CREATE TABLE some (
+ id UInt64,
+ date Date
+ ) ENGINE = Memory
+ SQL
+ end
+end
+# rubocop: enable Gitlab/NamespacedClass
diff --git a/spec/fixtures/click_house/migrations/table_creation_with_down_method/1_create_some_table.rb b/spec/fixtures/click_house/migrations/table_creation_with_down_method/1_create_some_table.rb
new file mode 100644
index 00000000000..7ac92b9ee38
--- /dev/null
+++ b/spec/fixtures/click_house/migrations/table_creation_with_down_method/1_create_some_table.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+# rubocop: disable Gitlab/NamespacedClass -- Fixtures do not need to be namespaced
+class CreateSomeTable < ClickHouse::Migration
+ def up
+ execute <<~SQL
+ CREATE TABLE some (
+ id UInt64,
+ date Date
+ ) ENGINE = Memory
+ SQL
+ end
+
+ def down
+ execute <<~SQL
+ DROP TABLE some
+ SQL
+ end
+end
+# rubocop: enable Gitlab/NamespacedClass
diff --git a/spec/fixtures/origin_cert_key.pem b/spec/fixtures/origin_cert_key.pem
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/spec/fixtures/origin_cert_key.pem
diff --git a/spec/fixtures/security_reports/master/gl-common-scanning-report.json b/spec/fixtures/security_reports/master/gl-common-scanning-report.json
index 47e2a503b02..35db4779920 100644
--- a/spec/fixtures/security_reports/master/gl-common-scanning-report.json
+++ b/spec/fixtures/security_reports/master/gl-common-scanning-report.json
@@ -12,10 +12,10 @@
"id": "gemnasium",
"name": "Gemnasium"
},
- "cvss": [
+ "cvss_vectors": [
{
"vendor": "GitLab",
- "vector_string": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H"
+ "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H"
}
],
"location": {
diff --git a/spec/fixtures/tooling/danger/rubocop_todo/cop1.yml b/spec/fixtures/tooling/danger/rubocop_todo/cop1.yml
new file mode 100644
index 00000000000..8f240b92682
--- /dev/null
+++ b/spec/fixtures/tooling/danger/rubocop_todo/cop1.yml
@@ -0,0 +1,5 @@
+---
+Cop1:
+ Exclude:
+ - 'app/controllers/application_controller.rb'
+ - 'app/controllers/acme_challenges_controller.rb'
diff --git a/spec/fixtures/tooling/danger/rubocop_todo/cop2.yml b/spec/fixtures/tooling/danger/rubocop_todo/cop2.yml
new file mode 100644
index 00000000000..9ab2c0dabb9
--- /dev/null
+++ b/spec/fixtures/tooling/danger/rubocop_todo/cop2.yml
@@ -0,0 +1,4 @@
+---
+Cop2:
+ Exclude:
+ - 'app/controllers/application_controller.rb'
diff --git a/spec/frontend/__helpers__/dom_shims/clipboard_event.js b/spec/frontend/__helpers__/dom_shims/clipboard_event.js
new file mode 100644
index 00000000000..fbff61503f2
--- /dev/null
+++ b/spec/frontend/__helpers__/dom_shims/clipboard_event.js
@@ -0,0 +1 @@
+window.ClipboardEvent = class ClipboardEvent extends Event {};
diff --git a/spec/frontend/__helpers__/dom_shims/drag_event.js b/spec/frontend/__helpers__/dom_shims/drag_event.js
new file mode 100644
index 00000000000..cbfee0b71ec
--- /dev/null
+++ b/spec/frontend/__helpers__/dom_shims/drag_event.js
@@ -0,0 +1 @@
+window.DragEvent = class DragEvent extends Event {};
diff --git a/spec/frontend/__helpers__/dom_shims/index.js b/spec/frontend/__helpers__/dom_shims/index.js
index 3b41e2ca2a7..f2c34b8ca31 100644
--- a/spec/frontend/__helpers__/dom_shims/index.js
+++ b/spec/frontend/__helpers__/dom_shims/index.js
@@ -1,5 +1,7 @@
import './clipboard';
+import './clipboard_event';
import './create_object_url';
+import './drag_event';
import './element_scroll_into_view';
import './element_scroll_by';
import './element_scroll_to';
diff --git a/spec/frontend/__helpers__/emoji.js b/spec/frontend/__helpers__/emoji.js
index 6c9291bdc8f..1037bd48df6 100644
--- a/spec/frontend/__helpers__/emoji.js
+++ b/spec/frontend/__helpers__/emoji.js
@@ -1,5 +1,5 @@
import { initEmojiMap, EMOJI_VERSION } from '~/emoji';
-import { CACHE_VERSION_KEY, CACHE_KEY } from '~/emoji/constants';
+import { CACHE_KEY } from '~/emoji/constants';
export const validEmoji = {
atom: {
@@ -95,19 +95,18 @@ export const emojiFixtureMap = {
export const mockEmojiData = Object.keys(emojiFixtureMap).reduce((acc, k) => {
const { moji: e, unicodeVersion: u, category: c, description: d } = emojiFixtureMap[k];
- acc[k] = { name: k, e, u, c, d };
+ acc.push({ n: k, e, u, c, d });
return acc;
-}, {});
+}, []);
export function clearEmojiMock() {
localStorage.clear();
initEmojiMap.promise = null;
}
-export async function initEmojiMock(mockData = mockEmojiData) {
+export async function initEmojiMock(data = mockEmojiData) {
clearEmojiMock();
- localStorage.setItem(CACHE_VERSION_KEY, EMOJI_VERSION);
- localStorage.setItem(CACHE_KEY, JSON.stringify(mockData));
+ localStorage.setItem(CACHE_KEY, JSON.stringify({ data, EMOJI_VERSION }));
await initEmojiMap();
}
diff --git a/spec/frontend/__helpers__/local_storage_helper.js b/spec/frontend/__helpers__/local_storage_helper.js
index cf75b0b53fe..367e7ec24ba 100644
--- a/spec/frontend/__helpers__/local_storage_helper.js
+++ b/spec/frontend/__helpers__/local_storage_helper.js
@@ -30,6 +30,9 @@ export const createLocalStorageSpy = () => {
let storage = {};
return {
+ get length() {
+ return Object.keys(storage).length;
+ },
clear: jest.fn(() => {
storage = {};
}),
diff --git a/spec/frontend/__helpers__/mock_observability_client.js b/spec/frontend/__helpers__/mock_observability_client.js
new file mode 100644
index 00000000000..82425aa2842
--- /dev/null
+++ b/spec/frontend/__helpers__/mock_observability_client.js
@@ -0,0 +1,19 @@
+import { buildClient } from '~/observability/client';
+
+export function createMockClient() {
+ const mockClient = buildClient({
+ provisioningUrl: 'provisioning-url',
+ tracingUrl: 'tracing-url',
+ servicesUrl: 'services-url',
+ operationsUrl: 'operations-url',
+ metricsUrl: 'metrics-url',
+ });
+
+ Object.getOwnPropertyNames(mockClient)
+ .filter((item) => typeof mockClient[item] === 'function')
+ .forEach((item) => {
+ mockClient[item] = jest.fn();
+ });
+
+ return mockClient;
+}
diff --git a/spec/frontend/__mocks__/@gitlab/ui.js b/spec/frontend/__mocks__/@gitlab/ui.js
index c51f37db384..04b3215a88a 100644
--- a/spec/frontend/__mocks__/@gitlab/ui.js
+++ b/spec/frontend/__mocks__/@gitlab/ui.js
@@ -1,3 +1,5 @@
+import '~/commons/gitlab_ui';
+
export * from '@gitlab/ui';
/**
diff --git a/spec/frontend/admin/abuse_report/components/abuse_report_app_spec.js b/spec/frontend/admin/abuse_report/components/abuse_report_app_spec.js
index 4340699a7ed..73e3f1eb49a 100644
--- a/spec/frontend/admin/abuse_report/components/abuse_report_app_spec.js
+++ b/spec/frontend/admin/abuse_report/components/abuse_report_app_spec.js
@@ -7,12 +7,14 @@ import ReportDetails from '~/admin/abuse_report/components/report_details.vue';
import ReportedContent from '~/admin/abuse_report/components/reported_content.vue';
import ActivityEventsList from '~/admin/abuse_report/components/activity_events_list.vue';
import ActivityHistoryItem from '~/admin/abuse_report/components/activity_history_item.vue';
+import AbuseReportNotes from '~/admin/abuse_report/components/abuse_report_notes.vue';
+
import { SUCCESS_ALERT } from '~/admin/abuse_report/constants';
import { mockAbuseReport } from '../mock_data';
describe('AbuseReportApp', () => {
let wrapper;
-
+ const mockAbuseReportId = mockAbuseReport.report.globalId;
const { similarOpenReports } = mockAbuseReport.user;
const findAlert = () => wrapper.findComponent(GlAlert);
@@ -27,6 +29,7 @@ describe('AbuseReportApp', () => {
const findActivityList = () => wrapper.findComponent(ActivityEventsList);
const findActivityItem = () => wrapper.findByTestId('activity');
+
const findActivityForSimilarReports = () =>
wrapper.findAllByTestId('activity-similar-open-reports');
const firstActivityForSimilarReports = () =>
@@ -34,6 +37,8 @@ describe('AbuseReportApp', () => {
const findReportDetails = () => wrapper.findComponent(ReportDetails);
+ const findAbuseReportNotes = () => wrapper.findComponent(AbuseReportNotes);
+
const createComponent = (props = {}, provide = {}) => {
wrapper = shallowMountExtended(AbuseReportApp, {
propsData: {
@@ -135,7 +140,7 @@ describe('AbuseReportApp', () => {
it('renders ReportDetails', () => {
createComponent({}, { glFeatures: { abuseReportLabels: true } });
- expect(findReportDetails().props('reportId')).toBe(mockAbuseReport.report.globalId);
+ expect(findReportDetails().props('reportId')).toBe(mockAbuseReportId);
});
});
@@ -162,4 +167,25 @@ describe('AbuseReportApp', () => {
expect(firstActivityForSimilarReports().props('report')).toBe(similarOpenReports[0]);
});
});
+
+ describe('Notes', () => {
+ describe('when abuseReportNotes feature flag is enabled', () => {
+ it('renders abuse report notes', () => {
+ createComponent({}, { glFeatures: { abuseReportNotes: true } });
+
+ expect(findAbuseReportNotes().exists()).toBe(true);
+ expect(findAbuseReportNotes().props()).toMatchObject({
+ abuseReportId: mockAbuseReportId,
+ });
+ });
+ });
+
+ describe('when abuseReportNotes feature flag is disabled', () => {
+ it('does not render ReportDetails', () => {
+ createComponent({}, { glFeatures: { abuseReportNotes: false } });
+
+ expect(findAbuseReportNotes().exists()).toBe(false);
+ });
+ });
+ });
});
diff --git a/spec/frontend/admin/abuse_report/components/abuse_report_notes_spec.js b/spec/frontend/admin/abuse_report/components/abuse_report_notes_spec.js
new file mode 100644
index 00000000000..166c735ffbd
--- /dev/null
+++ b/spec/frontend/admin/abuse_report/components/abuse_report_notes_spec.js
@@ -0,0 +1,98 @@
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { createAlert } from '~/alert';
+import SkeletonLoadingContainer from '~/vue_shared/components/notes/skeleton_note.vue';
+import abuseReportNotesQuery from '~/admin/abuse_report/graphql/notes/abuse_report_notes.query.graphql';
+import AbuseReportNotes from '~/admin/abuse_report/components/abuse_report_notes.vue';
+import AbuseReportDiscussion from '~/admin/abuse_report/components/notes/abuse_report_discussion.vue';
+
+import { mockAbuseReport, mockNotesByIdResponse } from '../mock_data';
+
+jest.mock('~/alert');
+
+describe('Abuse Report Notes', () => {
+ let wrapper;
+
+ Vue.use(VueApollo);
+
+ const mockAbuseReportId = mockAbuseReport.report.globalId;
+
+ const notesQueryHandler = jest.fn().mockResolvedValue(mockNotesByIdResponse);
+
+ const findSkeletonLoaders = () => wrapper.findAllComponents(SkeletonLoadingContainer);
+ const findAbuseReportDiscussions = () => wrapper.findAllComponents(AbuseReportDiscussion);
+
+ const createComponent = ({
+ queryHandler = notesQueryHandler,
+ abuseReportId = mockAbuseReportId,
+ } = {}) => {
+ wrapper = shallowMount(AbuseReportNotes, {
+ apolloProvider: createMockApollo([[abuseReportNotesQuery, queryHandler]]),
+ propsData: {
+ abuseReportId,
+ },
+ });
+ };
+
+ describe('when notes are loading', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('should show the skeleton loaders', () => {
+ expect(findSkeletonLoaders()).toHaveLength(5);
+ });
+ });
+
+ describe('when notes have been loaded', () => {
+ beforeEach(() => {
+ createComponent();
+ return waitForPromises();
+ });
+
+ it('should not render skeleton loader', () => {
+ expect(findSkeletonLoaders()).toHaveLength(0);
+ });
+
+ it('should call the abuse report notes query', () => {
+ expect(notesQueryHandler).toHaveBeenCalledWith({
+ id: mockAbuseReportId,
+ });
+ });
+
+ it('should show notes to the length of the response', () => {
+ expect(findAbuseReportDiscussions()).toHaveLength(2);
+
+ const discussions = mockNotesByIdResponse.data.abuseReport.discussions.nodes;
+
+ expect(findAbuseReportDiscussions().at(0).props()).toMatchObject({
+ abuseReportId: mockAbuseReportId,
+ discussion: discussions[0].notes.nodes,
+ });
+
+ expect(findAbuseReportDiscussions().at(1).props()).toMatchObject({
+ abuseReportId: mockAbuseReportId,
+ discussion: discussions[1].notes.nodes,
+ });
+ });
+ });
+
+ describe('When there is an error fetching the notes', () => {
+ beforeEach(() => {
+ createComponent({
+ queryHandler: jest.fn().mockRejectedValue(new Error()),
+ });
+
+ return waitForPromises();
+ });
+
+ it('should show an error when query fails', () => {
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'An error occurred while fetching comments, please try again.',
+ });
+ });
+ });
+});
diff --git a/spec/frontend/admin/abuse_report/components/labels_select_spec.js b/spec/frontend/admin/abuse_report/components/labels_select_spec.js
index a22dcc18e10..6eabaa33189 100644
--- a/spec/frontend/admin/abuse_report/components/labels_select_spec.js
+++ b/spec/frontend/admin/abuse_report/components/labels_select_spec.js
@@ -9,7 +9,7 @@ import LabelsSelect from '~/admin/abuse_report/components/labels_select.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { stubComponent, RENDER_ALL_SLOTS_TEMPLATE } from 'helpers/stub_component';
-import labelsQuery from '~/admin/abuse_report/components/graphql/abuse_report_labels.query.graphql';
+import labelsQuery from '~/admin/abuse_report/graphql/abuse_report_labels.query.graphql';
import DropdownWidget from '~/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue';
import DropdownValue from '~/sidebar/components/labels/labels_select_widget/dropdown_value.vue';
import DropdownHeader from '~/sidebar/components/labels/labels_select_widget/dropdown_header.vue';
diff --git a/spec/frontend/admin/abuse_report/components/notes/__snapshots__/abuse_report_note_body_spec.js.snap b/spec/frontend/admin/abuse_report/components/notes/__snapshots__/abuse_report_note_body_spec.js.snap
new file mode 100644
index 00000000000..5651a2a3eab
--- /dev/null
+++ b/spec/frontend/admin/abuse_report/components/notes/__snapshots__/abuse_report_note_body_spec.js.snap
@@ -0,0 +1,15 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Abuse Report Note Body should show the note body 1`] = `
+<div
+ class="md note-text"
+ data-testid="abuse-report-note-body"
+>
+ <p
+ data-sourcepos="1:1-1:9"
+ dir="auto"
+ >
+ Comment 1
+ </p>
+</div>
+`;
diff --git a/spec/frontend/admin/abuse_report/components/notes/abuse_report_discussion_spec.js b/spec/frontend/admin/abuse_report/components/notes/abuse_report_discussion_spec.js
new file mode 100644
index 00000000000..86f0939a938
--- /dev/null
+++ b/spec/frontend/admin/abuse_report/components/notes/abuse_report_discussion_spec.js
@@ -0,0 +1,79 @@
+import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import ToggleRepliesWidget from '~/notes/components/toggle_replies_widget.vue';
+import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
+import AbuseReportDiscussion from '~/admin/abuse_report/components/notes/abuse_report_discussion.vue';
+import AbuseReportNote from '~/admin/abuse_report/components/notes/abuse_report_note.vue';
+
+import {
+ mockAbuseReport,
+ mockDiscussionWithNoReplies,
+ mockDiscussionWithReplies,
+} from '../../mock_data';
+
+describe('Abuse Report Discussion', () => {
+ let wrapper;
+ const mockAbuseReportId = mockAbuseReport.report.globalId;
+
+ const findAbuseReportNote = () => wrapper.findComponent(AbuseReportNote);
+ const findAbuseReportNotes = () => wrapper.findAllComponents(AbuseReportNote);
+ const findTimelineEntryItem = () => wrapper.findComponent(TimelineEntryItem);
+ const findToggleRepliesWidget = () => wrapper.findComponent(ToggleRepliesWidget);
+
+ const createComponent = ({
+ discussion = mockDiscussionWithNoReplies,
+ abuseReportId = mockAbuseReportId,
+ } = {}) => {
+ wrapper = shallowMount(AbuseReportDiscussion, {
+ propsData: {
+ discussion,
+ abuseReportId,
+ },
+ });
+ };
+
+ describe('Default', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('should show the abuse report note', () => {
+ expect(findAbuseReportNote().exists()).toBe(true);
+
+ expect(findAbuseReportNote().props()).toMatchObject({
+ abuseReportId: mockAbuseReportId,
+ note: mockDiscussionWithNoReplies[0],
+ });
+ });
+
+ it('should not show timeline entry item component', () => {
+ expect(findTimelineEntryItem().exists()).toBe(false);
+ });
+
+ it('should not show the the toggle replies widget wrapper when no replies', () => {
+ expect(findToggleRepliesWidget().exists()).toBe(false);
+ });
+ });
+
+ describe('When the main comments has replies', () => {
+ beforeEach(() => {
+ createComponent({
+ discussion: mockDiscussionWithReplies,
+ });
+ });
+
+ it('should show the toggle replies widget', () => {
+ expect(findToggleRepliesWidget().exists()).toBe(true);
+ });
+
+ it('the number of replies should be equal to the response length', () => {
+ expect(findAbuseReportNotes()).toHaveLength(3);
+ });
+
+ it('should collapse when we click on toggle replies widget', async () => {
+ findToggleRepliesWidget().vm.$emit('toggle');
+ await nextTick();
+ expect(findAbuseReportNotes()).toHaveLength(1);
+ });
+ });
+});
diff --git a/spec/frontend/admin/abuse_report/components/notes/abuse_report_note_body_spec.js b/spec/frontend/admin/abuse_report/components/notes/abuse_report_note_body_spec.js
new file mode 100644
index 00000000000..25f675b4562
--- /dev/null
+++ b/spec/frontend/admin/abuse_report/components/notes/abuse_report_note_body_spec.js
@@ -0,0 +1,27 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import AbuseReportNoteBody from '~/admin/abuse_report/components/notes/abuse_report_note_body.vue';
+import { mockDiscussionWithNoReplies } from '../../mock_data';
+
+describe('Abuse Report Note Body', () => {
+ let wrapper;
+ const mockNote = mockDiscussionWithNoReplies[0];
+
+ const findNoteBody = () => wrapper.findByTestId('abuse-report-note-body');
+
+ const createComponent = ({ note = mockNote } = {}) => {
+ wrapper = shallowMountExtended(AbuseReportNoteBody, {
+ propsData: {
+ note,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('should show the note body', () => {
+ expect(findNoteBody().exists()).toBe(true);
+ expect(findNoteBody().html()).toMatchSnapshot();
+ });
+});
diff --git a/spec/frontend/admin/abuse_report/components/notes/abuse_report_note_spec.js b/spec/frontend/admin/abuse_report/components/notes/abuse_report_note_spec.js
new file mode 100644
index 00000000000..b6908853e46
--- /dev/null
+++ b/spec/frontend/admin/abuse_report/components/notes/abuse_report_note_spec.js
@@ -0,0 +1,80 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlAvatarLink, GlAvatar } from '@gitlab/ui';
+import AbuseReportNote from '~/admin/abuse_report/components/notes/abuse_report_note.vue';
+import NoteHeader from '~/notes/components/note_header.vue';
+import NoteBody from '~/admin/abuse_report/components/notes/abuse_report_note_body.vue';
+
+import { mockAbuseReport, mockDiscussionWithNoReplies } from '../../mock_data';
+
+describe('Abuse Report Note', () => {
+ let wrapper;
+ const mockAbuseReportId = mockAbuseReport.report.globalId;
+ const mockNote = mockDiscussionWithNoReplies[0];
+
+ const findAvatar = () => wrapper.findComponent(GlAvatar);
+ const findAvatarLink = () => wrapper.findComponent(GlAvatarLink);
+
+ const findNoteHeader = () => wrapper.findComponent(NoteHeader);
+ const findNoteBody = () => wrapper.findComponent(NoteBody);
+
+ const createComponent = ({ note = mockNote, abuseReportId = mockAbuseReportId } = {}) => {
+ wrapper = shallowMount(AbuseReportNote, {
+ propsData: {
+ note,
+ abuseReportId,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ describe('Author', () => {
+ const { author } = mockNote;
+
+ it('should show avatar', () => {
+ const avatar = findAvatar();
+
+ expect(avatar.exists()).toBe(true);
+ expect(avatar.props()).toMatchObject({
+ src: author.avatarUrl,
+ entityName: author.username,
+ alt: author.name,
+ });
+ });
+
+ it('should show avatar link with popover support', () => {
+ const avatarLink = findAvatarLink();
+
+ expect(avatarLink.exists()).toBe(true);
+ expect(avatarLink.classes()).toContain('js-user-link');
+ expect(avatarLink.attributes()).toMatchObject({
+ href: author.webUrl,
+ 'data-user-id': '1',
+ 'data-username': `${author.username}`,
+ });
+ });
+ });
+
+ describe('Header', () => {
+ it('should show note header', () => {
+ expect(findNoteHeader().exists()).toBe(true);
+ expect(findNoteHeader().props()).toMatchObject({
+ author: mockNote.author,
+ createdAt: mockNote.createdAt,
+ noteId: mockNote.id,
+ noteUrl: mockNote.url,
+ });
+ });
+ });
+
+ describe('Body', () => {
+ it('should show note body', () => {
+ expect(findNoteBody().exists()).toBe(true);
+ expect(findNoteBody().props()).toMatchObject({
+ note: mockNote,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/admin/abuse_report/components/report_details_spec.js b/spec/frontend/admin/abuse_report/components/report_details_spec.js
index a5c43dcb82b..a7e732b43b0 100644
--- a/spec/frontend/admin/abuse_report/components/report_details_spec.js
+++ b/spec/frontend/admin/abuse_report/components/report_details_spec.js
@@ -5,7 +5,7 @@ import LabelsSelect from '~/admin/abuse_report/components/labels_select.vue';
import ReportDetails from '~/admin/abuse_report/components/report_details.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import abuseReportQuery from '~/admin/abuse_report/components/graphql/abuse_report.query.graphql';
+import abuseReportQuery from '~/admin/abuse_report/graphql/abuse_report.query.graphql';
import { createAlert } from '~/alert';
import { mockAbuseReport, mockLabel1, mockReportQueryResponse } from '../mock_data';
diff --git a/spec/frontend/admin/abuse_report/mock_data.js b/spec/frontend/admin/abuse_report/mock_data.js
index ee61eabfa66..44c8cbdad7f 100644
--- a/spec/frontend/admin/abuse_report/mock_data.js
+++ b/spec/frontend/admin/abuse_report/mock_data.js
@@ -103,10 +103,14 @@ export const mockLabelsQueryResponse = {
export const mockReportQueryResponse = {
data: {
abuseReport: {
+ id: 'gid://gitlab/AbuseReport/1',
labels: {
nodes: [mockLabel1],
__typename: 'LabelConnection',
},
+ discussions: {
+ nodes: [],
+ },
__typename: 'AbuseReport',
},
},
@@ -128,3 +132,211 @@ export const mockCreateLabelResponse = {
},
},
};
+
+export const mockDiscussionWithNoReplies = [
+ {
+ id: 'gid://gitlab/Note/1',
+ body: 'Comment 1',
+ bodyHtml: '\u003cp data-sourcepos="1:1-1:9" dir="auto"\u003eComment 1\u003c/p\u003e',
+ createdAt: '2023-10-19T06:11:13Z',
+ lastEditedAt: '2023-10-20T02:46:50Z',
+ url: 'http://127.0.0.1:3000/admin/abuse_reports/1#note_1',
+ resolved: false,
+ author: {
+ id: 'gid://gitlab/User/1',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://127.0.0.1:3000/root',
+ __typename: 'UserCore',
+ },
+ lastEditedBy: null,
+ userPermissions: {
+ adminNote: true,
+ __typename: 'NotePermissions',
+ },
+ discussion: {
+ id: 'gid://gitlab/Discussion/055af96ab917175219aec8739c911277b18ea41d',
+ notes: {
+ nodes: [
+ {
+ id: 'gid://gitlab/Note/1',
+ __typename: 'Note',
+ },
+ ],
+ __typename: 'NoteConnection',
+ },
+ __typename: 'Discussion',
+ },
+ __typename: 'Note',
+ },
+];
+export const mockDiscussionWithReplies = [
+ {
+ id: 'gid://gitlab/DiscussionNote/2',
+ body: 'Comment 2',
+ bodyHtml: '\u003cp data-sourcepos="1:1-1:9" dir="auto"\u003eComment 2\u003c/p\u003e',
+ createdAt: '2023-10-20T07:47:21Z',
+ lastEditedAt: '2023-10-20T07:47:42Z',
+ url: 'http://127.0.0.1:3000/admin/abuse_reports/1#note_2',
+ resolved: false,
+ author: {
+ id: 'gid://gitlab/User/1',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://127.0.0.1:3000/root',
+ __typename: 'UserCore',
+ },
+ lastEditedBy: null,
+ userPermissions: {
+ adminNote: true,
+ __typename: 'NotePermissions',
+ },
+ discussion: {
+ id: 'gid://gitlab/Discussion/9c7228e06fb0339a3d1440fcda960acfd8baa43a',
+ notes: {
+ nodes: [
+ {
+ id: 'gid://gitlab/DiscussionNote/2',
+ __typename: 'Note',
+ },
+ {
+ id: 'gid://gitlab/DiscussionNote/3',
+ __typename: 'Note',
+ },
+ {
+ id: 'gid://gitlab/DiscussionNote/4',
+ __typename: 'Note',
+ },
+ ],
+ __typename: 'NoteConnection',
+ },
+ __typename: 'Discussion',
+ },
+ __typename: 'Note',
+ },
+ {
+ id: 'gid://gitlab/DiscussionNote/3',
+ body: 'Reply comment 1',
+ bodyHtml: '\u003cp data-sourcepos="1:1-1:15" dir="auto"\u003eReply comment 1\u003c/p\u003e',
+ createdAt: '2023-10-20T07:47:42Z',
+ lastEditedAt: '2023-10-20T07:47:42Z',
+ url: 'http://127.0.0.1:3000/admin/abuse_reports/1#note_3',
+ resolved: false,
+ author: {
+ id: 'gid://gitlab/User/1',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://127.0.0.1:3000/root',
+ __typename: 'UserCore',
+ },
+ lastEditedBy: null,
+ userPermissions: {
+ adminNote: true,
+ __typename: 'NotePermissions',
+ },
+ discussion: {
+ id: 'gid://gitlab/Discussion/9c7228e06fb0339a3d1440fcda960acfd8baa43a',
+ notes: {
+ nodes: [
+ {
+ id: 'gid://gitlab/DiscussionNote/2',
+ __typename: 'Note',
+ },
+ {
+ id: 'gid://gitlab/DiscussionNote/3',
+ __typename: 'Note',
+ },
+ {
+ id: 'gid://gitlab/DiscussionNote/4',
+ __typename: 'Note',
+ },
+ ],
+ __typename: 'NoteConnection',
+ },
+ __typename: 'Discussion',
+ },
+ __typename: 'Note',
+ },
+ {
+ id: 'gid://gitlab/DiscussionNote/4',
+ body: 'Reply comment 2',
+ bodyHtml: '\u003cp data-sourcepos="1:1-1:15" dir="auto"\u003eReply comment 2\u003c/p\u003e',
+ createdAt: '2023-10-20T08:26:51Z',
+ lastEditedAt: '2023-10-20T08:26:51Z',
+ url: 'http://127.0.0.1:3000/admin/abuse_reports/1#note_4',
+ resolved: false,
+ author: {
+ id: 'gid://gitlab/User/1',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://127.0.0.1:3000/root',
+ __typename: 'UserCore',
+ },
+ lastEditedBy: null,
+ userPermissions: {
+ adminNote: true,
+ __typename: 'NotePermissions',
+ },
+ discussion: {
+ id: 'gid://gitlab/Discussion/9c7228e06fb0339a3d1440fcda960acfd8baa43a',
+ notes: {
+ nodes: [
+ {
+ id: 'gid://gitlab/DiscussionNote/2',
+ __typename: 'Note',
+ },
+ {
+ id: 'gid://gitlab/DiscussionNote/3',
+ __typename: 'Note',
+ },
+ {
+ id: 'gid://gitlab/DiscussionNote/4',
+ __typename: 'Note',
+ },
+ ],
+ __typename: 'NoteConnection',
+ },
+ __typename: 'Discussion',
+ },
+ __typename: 'Note',
+ },
+];
+
+export const mockNotesByIdResponse = {
+ data: {
+ abuseReport: {
+ id: 'gid://gitlab/AbuseReport/1',
+ discussions: {
+ nodes: [
+ {
+ id: 'gid://gitlab/Discussion/055af96ab917175219aec8739c911277b18ea41d',
+ replyId:
+ 'gid://gitlab/IndividualNoteDiscussion/055af96ab917175219aec8739c911277b18ea41d',
+ notes: {
+ nodes: mockDiscussionWithNoReplies,
+ __typename: 'NoteConnection',
+ },
+ },
+ {
+ id: 'gid://gitlab/Discussion/9c7228e06fb0339a3d1440fcda960acfd8baa43a',
+ replyId: 'gid://gitlab/Discussion/9c7228e06fb0339a3d1440fcda960acfd8baa43a',
+ notes: {
+ nodes: mockDiscussionWithReplies,
+ __typename: 'NoteConnection',
+ },
+ },
+ ],
+ __typename: 'DiscussionConnection',
+ },
+ __typename: 'AbuseReport',
+ },
+ },
+};
diff --git a/spec/frontend/admin/users/components/app_spec.js b/spec/frontend/admin/users/components/app_spec.js
index d40089edc82..4b224947303 100644
--- a/spec/frontend/admin/users/components/app_spec.js
+++ b/spec/frontend/admin/users/components/app_spec.js
@@ -1,14 +1,42 @@
-import { shallowMount } from '@vue/test-utils';
-
+import { mount } from '@vue/test-utils';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import AdminUsersApp from '~/admin/users/components/app.vue';
-import AdminUsersTable from '~/admin/users/components/users_table.vue';
-import { users, paths } from '../mock_data';
+import UserActions from '~/admin/users/components/user_actions.vue';
+import getUsersGroupCountsQuery from '~/admin/users/graphql/queries/get_users_group_counts.query.graphql';
+import UsersTable from '~/vue_shared/components/users_table/users_table.vue';
+import { createAlert } from '~/alert';
+import { users, paths, createGroupCountResponse } from '../mock_data';
+
+Vue.use(VueApollo);
+
+jest.mock('~/alert');
describe('AdminUsersApp component', () => {
let wrapper;
+ const user = users[0];
+
+ const mockSuccessData = [{ id: user.id, groupCount: 5 }];
+ const mockParsedGroupCount = { 2177: 5 };
+ const mockError = new Error();
+
+ const createFetchGroupCount = (data) =>
+ jest.fn().mockResolvedValue(createGroupCountResponse(data));
+ const loadingResolver = jest.fn().mockResolvedValue(new Promise(() => {}));
+ const errorResolver = jest.fn().mockRejectedValueOnce(mockError);
+ const successfulResolver = createFetchGroupCount(mockSuccessData);
- const initComponent = (props = {}) => {
- wrapper = shallowMount(AdminUsersApp, {
+ function createMockApolloProvider(resolverMock) {
+ const requestHandlers = [[getUsersGroupCountsQuery, resolverMock]];
+
+ return createMockApollo(requestHandlers);
+ }
+
+ const initComponent = (props = {}, resolverMock = successfulResolver) => {
+ wrapper = mount(AdminUsersApp, {
+ apolloProvider: createMockApolloProvider(resolverMock),
propsData: {
users,
paths,
@@ -17,16 +45,47 @@ describe('AdminUsersApp component', () => {
});
};
- describe('when initialized', () => {
- beforeEach(() => {
+ const findUsersTable = () => wrapper.findComponent(UsersTable);
+ const findAllUserActions = () => wrapper.findAllComponents(UserActions);
+
+ describe.each`
+ description | mockResolver | loading | groupCounts | error
+ ${'when API call is loading'} | ${loadingResolver} | ${true} | ${{}} | ${false}
+ ${'when API returns successful with results'} | ${successfulResolver} | ${false} | ${mockParsedGroupCount} | ${false}
+ ${'when API returns error'} | ${errorResolver} | ${false} | ${{}} | ${true}
+ `('$description', ({ mockResolver, loading, groupCounts, error }) => {
+ beforeEach(async () => {
+ initComponent({}, mockResolver);
+ await waitForPromises();
+ });
+
+ it(`renders the UsersTable with group-counts-loading set to ${loading}`, () => {
+ expect(findUsersTable().props('groupCountsLoading')).toBe(loading);
+ });
+
+ it('renders the UsersTable with the correct group-counts data', () => {
+ expect(findUsersTable().props('groupCounts')).toStrictEqual(groupCounts);
+ });
+
+ it(`does ${error ? '' : 'not '}render an error message`, () => {
+ return error
+ ? expect(createAlert).toHaveBeenCalledWith({
+ message: 'Could not load user group counts. Please refresh the page to try again.',
+ error: mockError,
+ captureError: true,
+ })
+ : expect(createAlert).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('UserActions', () => {
+ beforeEach(async () => {
initComponent();
+ await waitForPromises();
});
- it('renders the admin users table with props', () => {
- expect(wrapper.findComponent(AdminUsersTable).props()).toEqual({
- users,
- paths,
- });
+ it('renders a UserActions component for each user', () => {
+ expect(findAllUserActions().wrappers.map((w) => w.props('user'))).toStrictEqual(users);
});
});
});
diff --git a/spec/frontend/admin/users/components/user_avatar_spec.js b/spec/frontend/admin/users/components/user_avatar_spec.js
deleted file mode 100644
index 02e648d2b77..00000000000
--- a/spec/frontend/admin/users/components/user_avatar_spec.js
+++ /dev/null
@@ -1,121 +0,0 @@
-import { GlAvatarLabeled, GlBadge, GlIcon } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-
-import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-import AdminUserAvatar from '~/admin/users/components/user_avatar.vue';
-import { LENGTH_OF_USER_NOTE_TOOLTIP } from '~/admin/users/constants';
-import { truncate } from '~/lib/utils/text_utility';
-import { users, paths } from '../mock_data';
-
-describe('AdminUserAvatar component', () => {
- let wrapper;
- const user = users[0];
- const adminUserPath = paths.adminUser;
-
- const findNote = () => wrapper.findComponent(GlIcon);
- const findAvatar = () => wrapper.findComponent(GlAvatarLabeled);
- const findUserLink = () => wrapper.find('.js-user-link');
- const findAllBadges = () => wrapper.findAllComponents(GlBadge);
- const findTooltip = () => getBinding(findNote().element, 'gl-tooltip');
-
- const initComponent = (props = {}) => {
- wrapper = shallowMount(AdminUserAvatar, {
- propsData: {
- user,
- adminUserPath,
- ...props,
- },
- directives: {
- GlTooltip: createMockDirective('gl-tooltip'),
- },
- stubs: {
- GlAvatarLabeled,
- },
- });
- };
-
- describe('when initialized', () => {
- beforeEach(() => {
- initComponent();
- });
-
- it('adds a user link hover card', () => {
- expect(findUserLink().attributes()).toMatchObject({
- 'data-user-id': user.id.toString(),
- 'data-username': user.username,
- });
- });
-
- it("renders the user's name with an admin path link", () => {
- const avatar = findAvatar();
-
- expect(avatar.props('label')).toBe(user.name);
- expect(avatar.props('labelLink')).toBe(adminUserPath.replace('id', user.username));
- });
-
- it("renders the user's email with a mailto link", () => {
- const avatar = findAvatar();
-
- expect(avatar.props('subLabel')).toBe(user.email);
- expect(avatar.props('subLabelLink')).toBe(`mailto:${user.email}`);
- });
-
- it("renders the user's avatar image", () => {
- expect(findAvatar().attributes('src')).toBe(user.avatarUrl);
- });
-
- it('renders a user note icon', () => {
- expect(findNote().exists()).toBe(true);
- expect(findNote().props('name')).toBe('document');
- });
-
- it("renders the user's note tooltip", () => {
- const tooltip = findTooltip();
-
- expect(tooltip).toBeDefined();
- expect(tooltip.value).toBe(user.note);
- });
-
- it("renders the user's badges", () => {
- findAllBadges().wrappers.forEach((badge, idx) => {
- expect(badge.text()).toBe(user.badges[idx].text);
- expect(badge.props('variant')).toBe(user.badges[idx].variant);
- });
- });
-
- describe('and the user note is very long', () => {
- const noteText = new Array(LENGTH_OF_USER_NOTE_TOOLTIP + 1).join('a');
-
- beforeEach(() => {
- initComponent({
- user: {
- ...user,
- note: noteText,
- },
- });
- });
-
- it("renders a truncated user's note tooltip", () => {
- const tooltip = findTooltip();
-
- expect(tooltip).toBeDefined();
- expect(tooltip.value).toBe(truncate(noteText, LENGTH_OF_USER_NOTE_TOOLTIP));
- });
- });
-
- describe('and the user does not have a note', () => {
- beforeEach(() => {
- initComponent({
- user: {
- ...user,
- note: null,
- },
- });
- });
-
- it('does not render a user note', () => {
- expect(findNote().exists()).toBe(false);
- });
- });
- });
-});
diff --git a/spec/frontend/admin/users/components/users_table_spec.js b/spec/frontend/admin/users/components/users_table_spec.js
deleted file mode 100644
index 6f658fd2e59..00000000000
--- a/spec/frontend/admin/users/components/users_table_spec.js
+++ /dev/null
@@ -1,141 +0,0 @@
-import { GlTable, GlSkeletonLoader } from '@gitlab/ui';
-import Vue from 'vue';
-import VueApollo from 'vue-apollo';
-
-import createMockApollo from 'helpers/mock_apollo_helper';
-import waitForPromises from 'helpers/wait_for_promises';
-import { mountExtended } from 'helpers/vue_test_utils_helper';
-
-import AdminUserActions from '~/admin/users/components/user_actions.vue';
-import AdminUserAvatar from '~/admin/users/components/user_avatar.vue';
-import AdminUsersTable from '~/admin/users/components/users_table.vue';
-import getUsersGroupCountsQuery from '~/admin/users/graphql/queries/get_users_group_counts.query.graphql';
-import { createAlert } from '~/alert';
-import AdminUserDate from '~/vue_shared/components/user_date.vue';
-
-import { users, paths, createGroupCountResponse } from '../mock_data';
-
-jest.mock('~/alert');
-
-Vue.use(VueApollo);
-
-describe('AdminUsersTable component', () => {
- let wrapper;
- const user = users[0];
-
- const createFetchGroupCount = (data) =>
- jest.fn().mockResolvedValue(createGroupCountResponse(data));
- const fetchGroupCountsLoading = jest.fn().mockResolvedValue(new Promise(() => {}));
- const fetchGroupCountsError = jest.fn().mockRejectedValue(new Error('Network error'));
- const fetchGroupCountsResponse = createFetchGroupCount([{ id: user.id, groupCount: 5 }]);
-
- const findUserGroupCount = (id) => wrapper.findByTestId(`user-group-count-${id}`);
- const findUserGroupCountLoader = (id) => findUserGroupCount(id).findComponent(GlSkeletonLoader);
- const getCellByLabel = (trIdx, label) => {
- return wrapper
- .findComponent(GlTable)
- .find('tbody')
- .findAll('tr')
- .at(trIdx)
- .find(`[data-label="${label}"][role="cell"]`);
- };
-
- function createMockApolloProvider(resolverMock) {
- const requestHandlers = [[getUsersGroupCountsQuery, resolverMock]];
-
- return createMockApollo(requestHandlers);
- }
-
- const initComponent = (props = {}, resolverMock = fetchGroupCountsResponse) => {
- wrapper = mountExtended(AdminUsersTable, {
- apolloProvider: createMockApolloProvider(resolverMock),
- propsData: {
- users,
- paths,
- ...props,
- },
- });
- };
-
- describe('when there are users', () => {
- beforeEach(() => {
- initComponent();
- });
-
- it('renders the projects count', () => {
- expect(getCellByLabel(0, 'Projects').text()).toContain(`${user.projectsCount}`);
- });
-
- it('renders the user actions', () => {
- expect(wrapper.findComponent(AdminUserActions).exists()).toBe(true);
- });
-
- it.each`
- component | label
- ${AdminUserAvatar} | ${'Name'}
- ${AdminUserDate} | ${'Created on'}
- ${AdminUserDate} | ${'Last activity'}
- `('renders the component for column $label', ({ component, label }) => {
- expect(getCellByLabel(0, label).findComponent(component).exists()).toBe(true);
- });
- });
-
- describe('when users is an empty array', () => {
- beforeEach(() => {
- initComponent({ users: [] });
- });
-
- it('renders a "No users found" message', () => {
- expect(wrapper.text()).toContain('No users found');
- });
- });
-
- describe('group counts', () => {
- describe('when fetching the data', () => {
- beforeEach(() => {
- initComponent({}, fetchGroupCountsLoading);
- });
-
- it('renders a loader for each user', () => {
- expect(findUserGroupCountLoader(user.id).exists()).toBe(true);
- });
- });
-
- describe('when the data has been fetched', () => {
- beforeEach(async () => {
- initComponent();
- await waitForPromises();
- });
-
- it("renders the user's group count", () => {
- expect(findUserGroupCount(user.id).text()).toBe('5');
- });
-
- describe("and a user's group count is null", () => {
- beforeEach(async () => {
- initComponent({}, createFetchGroupCount([{ id: user.id, groupCount: null }]));
- await waitForPromises();
- });
-
- it("renders the user's group count as 0", () => {
- expect(findUserGroupCount(user.id).text()).toBe('0');
- });
- });
- });
-
- describe('when there is an error while fetching the data', () => {
- beforeEach(async () => {
- initComponent({}, fetchGroupCountsError);
- await waitForPromises();
- });
-
- it('creates an alert message and captures the error', () => {
- expect(createAlert).toHaveBeenCalledWith({
- message: 'Could not load user group counts. Please refresh the page to try again.',
- captureError: true,
- error: expect.any(Error),
- });
- });
- });
- });
-});
diff --git a/spec/frontend/admin/users/constants.js b/spec/frontend/admin/users/constants.js
index d341eb03b1b..39e8e51f43c 100644
--- a/spec/frontend/admin/users/constants.js
+++ b/spec/frontend/admin/users/constants.js
@@ -9,6 +9,8 @@ const REJECT = 'reject';
const APPROVE = 'approve';
const BAN = 'ban';
const UNBAN = 'unban';
+const TRUST = 'trust';
+const UNTRUST = 'untrust';
export const EDIT = 'edit';
@@ -24,6 +26,8 @@ export const CONFIRMATION_ACTIONS = [
UNBAN,
APPROVE,
REJECT,
+ TRUST,
+ UNTRUST,
];
export const DELETE_ACTIONS = [DELETE, DELETE_WITH_CONTRIBUTIONS];
diff --git a/spec/frontend/alert_spec.js b/spec/frontend/alert_spec.js
index de3093c6c19..71c7dbe0cfd 100644
--- a/spec/frontend/alert_spec.js
+++ b/spec/frontend/alert_spec.js
@@ -1,8 +1,8 @@
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { createAlert, VARIANT_WARNING } from '~/alert';
-jest.mock('@sentry/browser');
+jest.mock('~/sentry/sentry_browser_wrapper');
describe('Flash', () => {
const findTextContent = (containerSelector = '.flash-container') =>
diff --git a/spec/frontend/analytics/cycle_analytics/components/filter_bar_spec.js b/spec/frontend/analytics/cycle_analytics/components/filter_bar_spec.js
index 387d0b453ee..3b2606d494a 100644
--- a/spec/frontend/analytics/cycle_analytics/components/filter_bar_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/components/filter_bar_spec.js
@@ -216,7 +216,7 @@ describe('Filter bar', () => {
urlUtils.mergeUrlParams = jest.fn();
mock = new MockAdapter(axios);
- wrapper = createComponent(storeConfig);
+ wrapper = createComponent(storeConfig());
wrapper.vm.$store.dispatch('filters/setFilters', {
...initialFilterBarState,
diff --git a/spec/frontend/analytics/cycle_analytics/mock_data.js b/spec/frontend/analytics/cycle_analytics/mock_data.js
index 7ad95cab9ad..e0b6f4aa8c4 100644
--- a/spec/frontend/analytics/cycle_analytics/mock_data.js
+++ b/spec/frontend/analytics/cycle_analytics/mock_data.js
@@ -11,7 +11,7 @@ import {
DEFAULT_VALUE_STREAM,
PAGINATION_TYPE,
PAGINATION_SORT_DIRECTION_DESC,
- PAGINATION_SORT_FIELD_END_EVENT,
+ PAGINATION_SORT_FIELD_DURATION,
} from '~/analytics/cycle_analytics/constants';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { getDateInPast } from '~/lib/utils/datetime_utility';
@@ -245,7 +245,7 @@ export const valueStreamStages = rawValueStreamStages.map((s) =>
export const initialPaginationQuery = {
page: 15,
- sort: PAGINATION_SORT_FIELD_END_EVENT,
+ sort: PAGINATION_SORT_FIELD_DURATION,
direction: PAGINATION_SORT_DIRECTION_DESC,
};
@@ -257,7 +257,7 @@ export const initialPaginationState = {
export const basePaginationResult = {
pagination: PAGINATION_TYPE,
- sort: PAGINATION_SORT_FIELD_END_EVENT,
+ sort: PAGINATION_SORT_FIELD_DURATION,
direction: PAGINATION_SORT_DIRECTION_DESC,
page: null,
};
diff --git a/spec/frontend/analytics/cycle_analytics/store/mutations_spec.js b/spec/frontend/analytics/cycle_analytics/store/mutations_spec.js
index 25fed2b1714..a37f37aaaf4 100644
--- a/spec/frontend/analytics/cycle_analytics/store/mutations_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/store/mutations_spec.js
@@ -2,7 +2,7 @@ import { useFakeDate } from 'helpers/fake_date';
import * as types from '~/analytics/cycle_analytics/store/mutation_types';
import mutations from '~/analytics/cycle_analytics/store/mutations';
import {
- PAGINATION_SORT_FIELD_END_EVENT,
+ PAGINATION_SORT_FIELD_DURATION,
PAGINATION_SORT_DIRECTION_DESC,
} from '~/analytics/cycle_analytics/constants';
import {
@@ -99,7 +99,7 @@ describe('Project Value Stream Analytics mutations', () => {
${types.SET_LOADING} | ${true} | ${'isLoading'} | ${true}
${types.SET_LOADING} | ${false} | ${'isLoading'} | ${false}
${types.SET_SELECTED_VALUE_STREAM} | ${selectedValueStream} | ${'selectedValueStream'} | ${selectedValueStream}
- ${types.SET_PAGINATION} | ${pagination} | ${'pagination'} | ${{ ...pagination, sort: PAGINATION_SORT_FIELD_END_EVENT, direction: PAGINATION_SORT_DIRECTION_DESC }}
+ ${types.SET_PAGINATION} | ${pagination} | ${'pagination'} | ${{ ...pagination, sort: PAGINATION_SORT_FIELD_DURATION, direction: PAGINATION_SORT_DIRECTION_DESC }}
${types.SET_PAGINATION} | ${{ ...pagination, sort: 'duration', direction: 'asc' }} | ${'pagination'} | ${{ ...pagination, sort: 'duration', direction: 'asc' }}
${types.SET_SELECTED_STAGE} | ${selectedStage} | ${'selectedStage'} | ${selectedStage}
${types.RECEIVE_VALUE_STREAMS_SUCCESS} | ${[selectedValueStream]} | ${'valueStreams'} | ${[selectedValueStream]}
diff --git a/spec/frontend/analytics/product_analytics/components/activity_chart_spec.js b/spec/frontend/analytics/product_analytics/components/activity_chart_spec.js
deleted file mode 100644
index 4f8126aaacf..00000000000
--- a/spec/frontend/analytics/product_analytics/components/activity_chart_spec.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import { GlColumnChart } from '@gitlab/ui/dist/charts';
-import { shallowMount } from '@vue/test-utils';
-import ActivityChart from '~/analytics/product_analytics/components/activity_chart.vue';
-
-describe('Activity Chart Bundle', () => {
- let wrapper;
- function mountComponent({ provide }) {
- wrapper = shallowMount(ActivityChart, {
- provide: {
- formattedData: {},
- ...provide,
- },
- });
- }
-
- const findChart = () => wrapper.findComponent(GlColumnChart);
- const findNoData = () => wrapper.find('[data-testid="noActivityChartData"]');
-
- describe('Activity Chart', () => {
- it('renders an warning message with no data', () => {
- mountComponent({ provide: { formattedData: {} } });
- expect(findNoData().exists()).toBe(true);
- });
-
- it('renders a chart with data', () => {
- mountComponent({
- provide: { formattedData: { keys: ['key1', 'key2'], values: [5038, 2241] } },
- });
-
- expect(findNoData().exists()).toBe(false);
- expect(findChart().exists()).toBe(true);
- });
- });
-});
diff --git a/spec/frontend/analytics/shared/components/metric_tile_spec.js b/spec/frontend/analytics/shared/components/metric_tile_spec.js
index 9da5ed0fb07..262357a35e4 100644
--- a/spec/frontend/analytics/shared/components/metric_tile_spec.js
+++ b/spec/frontend/analytics/shared/components/metric_tile_spec.js
@@ -32,8 +32,7 @@ describe('MetricTile', () => {
};
wrapper = createComponent({ metric });
- const singleStat = findSingleStat();
- singleStat.vm.$emit('click');
+ findSingleStat().vm.$emit('click');
expect(redirectTo).toHaveBeenCalledWith('foo/bar'); // eslint-disable-line import/no-deprecated
});
@@ -41,27 +40,31 @@ describe('MetricTile', () => {
const metric = { identifier: 'deploys', value: '10', label: 'Deploys' };
wrapper = createComponent({ metric });
- const singleStat = findSingleStat();
- singleStat.vm.$emit('click');
+ findSingleStat().vm.$emit('click');
expect(redirectTo).not.toHaveBeenCalled(); // eslint-disable-line import/no-deprecated
});
});
- describe('decimal places', () => {
+ describe('number formatting', () => {
it(`will render 0 decimal places for an integer value`, () => {
const metric = { identifier: 'deploys', value: '10', label: 'Deploys' };
wrapper = createComponent({ metric });
- const singleStat = findSingleStat();
- expect(singleStat.props('animationDecimalPlaces')).toBe(0);
+ expect(findSingleStat().props('animationDecimalPlaces')).toBe(0);
});
it(`will render 1 decimal place for a float value`, () => {
const metric = { identifier: 'deploys', value: '10.5', label: 'Deploys' };
wrapper = createComponent({ metric });
- const singleStat = findSingleStat();
- expect(singleStat.props('animationDecimalPlaces')).toBe(1);
+ expect(findSingleStat().props('animationDecimalPlaces')).toBe(1);
+ });
+
+ it('will render using delimiters', () => {
+ const metric = { identifier: 'deploys', value: '10000', label: 'Deploys' };
+ wrapper = createComponent({ metric });
+
+ expect(findSingleStat().props('useDelimiters')).toBe(true);
});
});
diff --git a/spec/frontend/authentication/two_factor_auth/components/recovery_codes_spec.js b/spec/frontend/authentication/two_factor_auth/components/recovery_codes_spec.js
index aef06a74fdd..086a4bc1ec0 100644
--- a/spec/frontend/authentication/two_factor_auth/components/recovery_codes_spec.js
+++ b/spec/frontend/authentication/two_factor_auth/components/recovery_codes_spec.js
@@ -21,7 +21,7 @@ describe('RecoveryCodes', () => {
propsData: {
codes,
profileAccountPath,
- ...(options?.propsData || {}),
+ ...options?.propsData,
},
...options,
}),
diff --git a/spec/frontend/awards_handler_spec.js b/spec/frontend/awards_handler_spec.js
index c2b7906d0d6..c2a878e661d 100644
--- a/spec/frontend/awards_handler_spec.js
+++ b/spec/frontend/awards_handler_spec.js
@@ -13,62 +13,71 @@ let awardsHandler = null;
describe('AwardsHandler', () => {
useFakeRequestAnimationFrame();
- const emojiData = {
- '8ball': {
+ const emojiData = [
+ {
+ n: '8ball',
c: 'activity',
e: '🎱',
d: 'billiards',
u: '6.0',
},
- grinning: {
+ {
+ n: 'grinning',
c: 'people',
e: '😀',
d: 'grinning face',
u: '6.1',
},
- angel: {
+ {
+ n: 'angel',
c: 'people',
e: '👼',
d: 'baby angel',
u: '6.0',
},
- anger: {
+ {
+ n: 'anger',
c: 'symbols',
e: '💢',
d: 'anger symbol',
u: '6.0',
},
- alien: {
+ {
+ n: 'alien',
c: 'people',
e: '👽',
d: 'extraterrestrial alien',
u: '6.0',
},
- sunglasses: {
+ {
+ n: 'sunglasses',
c: 'people',
e: '😎',
d: 'smiling face with sunglasses',
u: '6.0',
},
- grey_question: {
+ {
+ n: 'grey_question',
c: 'symbols',
e: '❔',
d: 'white question mark ornament',
u: '6.0',
},
- thumbsup: {
+ {
+ n: 'thumbsup',
c: 'people',
e: '👍',
d: 'thumbs up sign',
u: '6.0',
},
- thumbsdown: {
+ {
+ n: 'thumbsdown',
c: 'people',
e: '👎',
d: 'thumbs down sign',
u: '6.0',
},
- };
+ ];
const openAndWaitForEmojiMenu = (sel = '.js-add-award') => {
$(sel).eq(0).click();
diff --git a/spec/frontend/batch_comments/components/submit_dropdown_spec.js b/spec/frontend/batch_comments/components/submit_dropdown_spec.js
index 19be3fb7d31..9e0b13c7e6e 100644
--- a/spec/frontend/batch_comments/components/submit_dropdown_spec.js
+++ b/spec/frontend/batch_comments/components/submit_dropdown_spec.js
@@ -1,22 +1,51 @@
-import { GlDropdown } from '@gitlab/ui';
+import { GlDisclosureDropdown } from '@gitlab/ui';
import Vue from 'vue';
+import VueApollo from 'vue-apollo';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
+import createMockApollo from 'helpers/mock_apollo_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import SubmitDropdown from '~/batch_comments/components/submit_dropdown.vue';
import { mockTracking } from 'helpers/tracking_helper';
+import userCanApproveQuery from '~/batch_comments/queries/can_approve.query.graphql';
jest.mock('~/autosave');
+Vue.use(VueApollo);
Vue.use(Vuex);
let wrapper;
let publishReview;
let trackingSpy;
-function factory({ canApprove = true, shouldAnimateReviewButton = false } = {}) {
+function factory({
+ canApprove = true,
+ shouldAnimateReviewButton = false,
+ mrRequestChanges = false,
+} = {}) {
publishReview = jest.fn();
trackingSpy = mockTracking(undefined, null, jest.spyOn);
+ const requestHandlers = [
+ [
+ userCanApproveQuery,
+ () =>
+ Promise.resolve({
+ data: {
+ project: {
+ id: 1,
+ mergeRequest: {
+ id: 1,
+ userPermissions: {
+ canApprove,
+ },
+ },
+ },
+ },
+ }),
+ ],
+ ];
+ const apolloProvider = createMockApollo(requestHandlers);
const store = new Vuex.Store({
getters: {
@@ -27,12 +56,17 @@ function factory({ canApprove = true, shouldAnimateReviewButton = false } = {})
getNoteableData: () => ({
id: 1,
preview_note_path: '/preview',
- current_user: { can_approve: canApprove },
}),
noteableType: () => 'merge_request',
getCurrentUserLastNote: () => ({ id: 1 }),
},
modules: {
+ diffs: {
+ namespaced: true,
+ state: {
+ projectPath: 'gitlab-org/gitlab',
+ },
+ },
batchComments: {
namespaced: true,
state: { shouldAnimateReviewButton },
@@ -44,13 +78,17 @@ function factory({ canApprove = true, shouldAnimateReviewButton = false } = {})
});
wrapper = mountExtended(SubmitDropdown, {
store,
+ apolloProvider,
+ provide: {
+ glFeatures: { mrRequestChanges },
+ },
});
}
const findCommentTextarea = () => wrapper.findByTestId('comment-textarea');
const findSubmitButton = () => wrapper.findByTestId('submit-review-button');
const findForm = () => wrapper.findByTestId('submit-gl-form');
-const findSubmitDropdown = () => wrapper.findComponent(GlDropdown);
+const findSubmitDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
describe('Batch comments submit dropdown', () => {
afterEach(() => {
@@ -70,6 +108,7 @@ describe('Batch comments submit dropdown', () => {
note: 'Hello world',
approve: false,
approval_password: '',
+ reviewer_state: 'reviewed',
});
});
@@ -113,11 +152,18 @@ describe('Batch comments submit dropdown', () => {
canApprove | exists | existsText
${true} | ${true} | ${'shows'}
${false} | ${false} | ${'hides'}
- `('$existsText approve checkbox if can_approve is $canApprove', ({ canApprove, exists }) => {
- factory({ canApprove });
+ `(
+ '$existsText approve checkbox if can_approve is $canApprove',
+ async ({ canApprove, exists }) => {
+ factory({ canApprove });
- expect(wrapper.findByTestId('approve_merge_request').exists()).toBe(exists);
- });
+ wrapper.findComponent(GlDisclosureDropdown).vm.$emit('shown');
+
+ await waitForPromises();
+
+ expect(wrapper.findByTestId('approve_merge_request').exists()).toBe(exists);
+ },
+ );
it.each`
shouldAnimateReviewButton | animationClassApplied | classText
@@ -133,4 +179,52 @@ describe('Batch comments submit dropdown', () => {
);
},
);
+
+ describe('when mrRequestChanges feature flag is enabled', () => {
+ it('renders a radio group with review state options', async () => {
+ factory({ mrRequestChanges: true });
+
+ await waitForPromises();
+
+ expect(wrapper.findAll('.gl-form-radio').length).toBe(3);
+ });
+
+ it('renders disabled approve radio button when user can not approve', async () => {
+ factory({ mrRequestChanges: true, canApprove: false });
+
+ wrapper.findComponent(GlDisclosureDropdown).vm.$emit('shown');
+
+ await waitForPromises();
+
+ expect(wrapper.find('.custom-control-input[value="approved"]').attributes('disabled')).toBe(
+ 'disabled',
+ );
+ });
+
+ it.each`
+ value
+ ${'approved'}
+ ${'reviewed'}
+ ${'requested_changes'}
+ `('sends $value review state to api when submitting', async ({ value }) => {
+ factory({ mrRequestChanges: true });
+
+ wrapper.findComponent(GlDisclosureDropdown).vm.$emit('shown');
+
+ await waitForPromises();
+
+ await wrapper.find(`.custom-control-input[value="${value}"]`).trigger('change');
+
+ findForm().vm.$emit('submit', { preventDefault: jest.fn() });
+
+ expect(publishReview).toHaveBeenCalledWith(expect.anything(), {
+ noteable_type: 'merge_request',
+ noteable_id: 1,
+ note: 'Hello world',
+ approve: false,
+ approval_password: '',
+ reviewer_state: value,
+ });
+ });
+ });
});
diff --git a/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js b/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js
index 824b2a296c6..3f8083aa37d 100644
--- a/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js
+++ b/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js
@@ -191,8 +191,6 @@ describe('Batch comments store actions', () => {
return actions.publishReview({ dispatch, commit, getters, rootGetters }).then(() => {
expect(commit.mock.calls[0]).toEqual(['REQUEST_PUBLISH_REVIEW']);
expect(commit.mock.calls[1]).toEqual(['RECEIVE_PUBLISH_REVIEW_SUCCESS']);
-
- expect(dispatch.mock.calls[0]).toEqual(['updateDiscussionsAfterPublish']);
});
});
diff --git a/spec/frontend/behaviors/gl_emoji_spec.js b/spec/frontend/behaviors/gl_emoji_spec.js
index c7f4fce0e4c..ef40179c23b 100644
--- a/spec/frontend/behaviors/gl_emoji_spec.js
+++ b/spec/frontend/behaviors/gl_emoji_spec.js
@@ -15,20 +15,22 @@ jest.mock('~/lib/graphql', () => {
});
describe('gl_emoji', () => {
- const emojiData = {
- grey_question: {
+ const emojiData = [
+ {
+ n: 'grey_question',
c: 'symbols',
e: '❔',
d: 'white question mark ornament',
u: '6.0',
},
- bomb: {
+ {
+ n: 'bomb',
c: 'objects',
e: '💣',
d: 'bomb',
u: '6.0',
},
- };
+ ];
beforeAll(() => {
jest.spyOn(EmojiUnicodeSupport, 'default').mockReturnValue(true);
@@ -119,7 +121,7 @@ describe('gl_emoji', () => {
await waitForPromises();
expect(glEmojiElement.outerHTML).toBe(
- '<gl-emoji data-name="&quot;x=&quot;y&quot; onload=&quot;alert(document.location.href)&quot;" data-unicode-version="x"><img class="emoji" title=":&quot;x=&quot;y&quot; onload=&quot;alert(document.location.href)&quot;:" alt=":&quot;x=&quot;y&quot; onload=&quot;alert(document.location.href)&quot;:" src="/-/emojis/2/grey_question.png" align="absmiddle"></gl-emoji>',
+ '<gl-emoji data-name="&quot;x=&quot;y&quot; onload=&quot;alert(document.location.href)&quot;" data-unicode-version="x"><img class="emoji" title=":&quot;x=&quot;y&quot; onload=&quot;alert(document.location.href)&quot;:" alt=":&quot;x=&quot;y&quot; onload=&quot;alert(document.location.href)&quot;:" src="/-/emojis/3/grey_question.png" align="absmiddle"></gl-emoji>',
);
});
diff --git a/spec/frontend/behaviors/load_startup_css_spec.js b/spec/frontend/behaviors/load_startup_css_spec.js
deleted file mode 100644
index e9e4c06732f..00000000000
--- a/spec/frontend/behaviors/load_startup_css_spec.js
+++ /dev/null
@@ -1,48 +0,0 @@
-import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import { loadStartupCSS } from '~/behaviors/load_startup_css';
-
-describe('behaviors/load_startup_css', () => {
- let loadListener;
-
- const setupListeners = () => {
- document
- .querySelectorAll('link')
- .forEach((x) => x.addEventListener('load', () => loadListener(x)));
- };
-
- beforeEach(() => {
- loadListener = jest.fn();
-
- setHTMLFixture(`
- <meta charset="utf-8" />
- <link media="print" src="./lorem-print.css" />
- <link media="print" src="./ipsum-print.css" />
- <link media="all" src="./dolar-all.css" />
- `);
-
- setupListeners();
-
- loadStartupCSS();
- });
-
- afterEach(() => {
- resetHTMLFixture();
- });
-
- it('does nothing at first', () => {
- expect(loadListener).not.toHaveBeenCalled();
- });
-
- describe('on window load', () => {
- beforeEach(() => {
- window.dispatchEvent(new Event('load'));
- });
-
- it('dispatches load to the print links', () => {
- expect(loadListener.mock.calls.map(([el]) => el.getAttribute('src'))).toEqual([
- './lorem-print.css',
- './ipsum-print.css',
- ]);
- });
- });
-});
diff --git a/spec/frontend/blob/components/blob_header_spec.js b/spec/frontend/blob/components/blob_header_spec.js
index 922d6a0211b..e7b2ee74940 100644
--- a/spec/frontend/blob/components/blob_header_spec.js
+++ b/spec/frontend/blob/components/blob_header_spec.js
@@ -116,13 +116,22 @@ describe('Blob Header Default Actions', () => {
});
});
+ it.each([[{ showBlameToggle: true }], [{ showBlameToggle: false }]])(
+ 'passes the `showBlameToggle` prop to the viewer switcher',
+ (propsData) => {
+ createComponent({ propsData });
+
+ expect(findViewSwitcher().props('showBlameToggle')).toBe(propsData.showBlameToggle);
+ },
+ );
+
it('does not render viewer switcher if the blob has only the simple viewer', () => {
createComponent({
blobProps: {
richViewer: null,
},
});
- expect(findViewSwitcher().exists()).toBe(false);
+ expect(findViewSwitcher().props('showViewerToggles')).toBe(false);
});
it('does not render viewer switcher if a corresponding prop is passed', () => {
diff --git a/spec/frontend/blob/components/blob_header_viewer_switcher_spec.js b/spec/frontend/blob/components/blob_header_viewer_switcher_spec.js
index 2ef87f6664b..25d9642acb0 100644
--- a/spec/frontend/blob/components/blob_header_viewer_switcher_spec.js
+++ b/spec/frontend/blob/components/blob_header_viewer_switcher_spec.js
@@ -1,6 +1,6 @@
import { GlButtonGroup, GlButton } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import BlobHeaderViewerSwitcher from '~/blob/components/blob_header_viewer_switcher.vue';
import {
RICH_BLOB_VIEWER,
@@ -12,14 +12,15 @@ import {
describe('Blob Header Viewer Switcher', () => {
let wrapper;
- function createComponent(propsData = {}) {
- wrapper = mount(BlobHeaderViewerSwitcher, {
+ function createComponent(propsData = { showViewerToggles: true }) {
+ wrapper = mountExtended(BlobHeaderViewerSwitcher, {
propsData,
});
}
const findSimpleViewerButton = () => wrapper.findComponent('[data-viewer="simple"]');
const findRichViewerButton = () => wrapper.findComponent('[data-viewer="rich"]');
+ const findBlameButton = () => wrapper.findByText('Blame');
describe('intiialization', () => {
it('is initialized with simple viewer as active', () => {
@@ -74,7 +75,7 @@ describe('Blob Header Viewer Switcher', () => {
});
it('emits an event when a Simple Viewer button is clicked', async () => {
- createComponent({ value: RICH_BLOB_VIEWER });
+ createComponent({ value: RICH_BLOB_VIEWER, showViewerToggles: true });
findSimpleViewerButton().vm.$emit('click');
await nextTick();
@@ -82,4 +83,28 @@ describe('Blob Header Viewer Switcher', () => {
expect(wrapper.emitted('input')).toEqual([[SIMPLE_BLOB_VIEWER]]);
});
});
+
+ it('does not render simple and rich viewer buttons if `showViewerToggles` is `false`', async () => {
+ createComponent({ showViewerToggles: false });
+ await nextTick();
+
+ expect(findSimpleViewerButton().exists()).toBe(false);
+ expect(findRichViewerButton().exists()).toBe(false);
+ });
+
+ it('does not render a Blame button if `showBlameToggle` is `false`', async () => {
+ createComponent({ showBlameToggle: false });
+ await nextTick();
+
+ expect(findBlameButton().exists()).toBe(false);
+ });
+
+ it('emits an event when the Blame button is clicked', async () => {
+ createComponent({ showBlameToggle: true });
+
+ findBlameButton().trigger('click');
+ await nextTick();
+
+ expect(wrapper.emitted('blame')).toHaveLength(1);
+ });
});
diff --git a/spec/frontend/boards/board_card_inner_spec.js b/spec/frontend/boards/board_card_inner_spec.js
index 8314cbda7a1..c70e461da83 100644
--- a/spec/frontend/boards/board_card_inner_spec.js
+++ b/spec/frontend/boards/board_card_inner_spec.js
@@ -1,4 +1,4 @@
-import { GlLabel, GlLoadingIcon, GlTooltip } from '@gitlab/ui';
+import { GlLabel, GlLoadingIcon } from '@gitlab/ui';
import { range } from 'lodash';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
@@ -47,13 +47,6 @@ describe('Board card component', () => {
const findIssuableBlockedIcon = () => wrapper.findComponent(IssuableBlockedIcon);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const findEpicCountablesTotalTooltip = () => wrapper.findComponent(GlTooltip);
- const findEpicCountables = () => wrapper.findByTestId('epic-countables');
- const findEpicCountablesBadgeIssues = () => wrapper.findByTestId('epic-countables-counts-issues');
- const findEpicCountablesBadgeWeight = () => wrapper.findByTestId('epic-countables-weight-issues');
- const findEpicBadgeProgress = () => wrapper.findByTestId('epic-progress');
- const findEpicCountablesTotalWeight = () => wrapper.findByTestId('epic-countables-total-weight');
- const findEpicProgressTooltip = () => wrapper.findByTestId('epic-progress-tooltip-content');
const findHiddenIssueIcon = () => wrapper.findByTestId('hidden-icon');
const findWorkItemIcon = () => wrapper.findComponent(WorkItemTypeIcon);
@@ -70,7 +63,7 @@ describe('Board card component', () => {
const mockApollo = createMockApollo();
- const createWrapper = ({ props = {}, isEpicBoard = false, isGroupBoard = true } = {}) => {
+ const createWrapper = ({ props = {}, isGroupBoard = true } = {}) => {
mockApollo.clients.defaultClient.cache.writeQuery({
query: isShowingLabelsQuery,
data: {
@@ -97,8 +90,8 @@ describe('Board card component', () => {
provide: {
rootPath: '/',
scopedLabelsAvailable: false,
- isEpicBoard,
- allowSubEpics: isEpicBoard,
+ isEpicBoard: false,
+ allowSubEpics: false,
issuableType: TYPE_ISSUE,
isGroupBoard,
isApolloBoard: false,
@@ -509,117 +502,4 @@ describe('Board card component', () => {
expect(findLoadingIcon().exists()).toBe(true);
});
});
-
- describe('is an epic board', () => {
- const descendantCounts = {
- closedEpics: 0,
- closedIssues: 0,
- openedEpics: 0,
- openedIssues: 0,
- };
-
- const descendantWeightSum = {
- closedIssues: 0,
- openedIssues: 0,
- };
-
- beforeEach(() => {
- createStore();
- });
-
- it('should render if the item has issues', () => {
- createWrapper({
- props: {
- item: {
- ...issue,
- descendantCounts,
- descendantWeightSum,
- hasIssues: true,
- },
- },
- isEpicBoard: true,
- });
-
- expect(findEpicCountables().exists()).toBe(true);
- });
-
- it('should not render if the item does not have issues', () => {
- createWrapper({
- item: {
- ...issue,
- descendantCounts,
- descendantWeightSum,
- hasIssues: false,
- },
- });
-
- expect(findEpicCountablesBadgeIssues().exists()).toBe(false);
- });
-
- it('shows render item countBadge, weights, and progress correctly', () => {
- createWrapper({
- props: {
- item: {
- ...issue,
- descendantCounts: {
- ...descendantCounts,
- openedIssues: 1,
- },
- descendantWeightSum: {
- closedIssues: 10,
- openedIssues: 5,
- },
- hasIssues: true,
- },
- },
- isEpicBoard: true,
- });
-
- expect(findEpicCountablesBadgeIssues().text()).toBe('1');
- expect(findEpicCountablesBadgeWeight().text()).toBe('15');
- expect(findEpicBadgeProgress().text()).toBe('67%');
- });
-
- it('does not render progress when weight is zero', () => {
- createWrapper({
- props: {
- item: {
- ...issue,
- descendantCounts: {
- ...descendantCounts,
- openedIssues: 1,
- },
- descendantWeightSum,
- hasIssues: true,
- },
- },
- isEpicBoard: true,
- });
-
- expect(findEpicBadgeProgress().exists()).toBe(false);
- });
-
- it('renders the tooltip with the correct data', () => {
- createWrapper({
- props: {
- item: {
- ...issue,
- descendantCounts,
- descendantWeightSum: {
- closedIssues: 10,
- openedIssues: 5,
- },
- hasIssues: true,
- },
- },
- isEpicBoard: true,
- });
-
- const tooltip = findEpicCountablesTotalTooltip();
- expect(tooltip).toBeDefined();
-
- expect(findEpicCountablesTotalWeight().text()).toBe('15');
- expect(findEpicProgressTooltip().text()).toBe('10 of 15 weight completed');
- });
- });
});
diff --git a/spec/frontend/boards/cache_updates_spec.js b/spec/frontend/boards/cache_updates_spec.js
index bc661f20451..07f5cef4a36 100644
--- a/spec/frontend/boards/cache_updates_spec.js
+++ b/spec/frontend/boards/cache_updates_spec.js
@@ -1,4 +1,4 @@
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { setError } from '~/boards/graphql/cache_updates';
import { defaultClient } from '~/graphql_shared/issuable_client';
import setErrorMutation from '~/boards/graphql/client/set_error.mutation.graphql';
diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js
index dfc8b18e197..0be17db9450 100644
--- a/spec/frontend/boards/mock_data.js
+++ b/spec/frontend/boards/mock_data.js
@@ -281,6 +281,7 @@ export const rawIssue = {
title: 'Issue 1',
id: 'gid://gitlab/Issue/436',
iid: '27',
+ closedAt: null,
dueDate: null,
timeEstimate: 0,
confidential: false,
@@ -324,6 +325,7 @@ export const mockIssue = {
id: 'gid://gitlab/Issue/436',
iid: '27',
title: 'Issue 1',
+ closedAt: null,
dueDate: null,
timeEstimate: 0,
confidential: false,
@@ -412,6 +414,7 @@ export const mockIssue2 = {
id: 'gid://gitlab/Issue/437',
iid: 28,
title: 'Issue 2',
+ closedAt: null,
dueDate: null,
timeEstimate: 0,
confidential: false,
diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js
index 5b4b79c650a..358cb340802 100644
--- a/spec/frontend/boards/stores/actions_spec.js
+++ b/spec/frontend/boards/stores/actions_spec.js
@@ -1,8 +1,8 @@
-import * as Sentry from '@sentry/browser';
import { cloneDeep } from 'lodash';
import Vue from 'vue';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { inactiveId, ISSUABLE, ListType, DraggableItemTypes } from 'ee_else_ce/boards/constants';
import issueMoveListMutation from 'ee_else_ce/boards/graphql/issue_move_list.mutation.graphql';
import testAction from 'helpers/vuex_action_helper';
diff --git a/spec/frontend/ci/artifacts/components/job_artifacts_table_spec.js b/spec/frontend/ci/artifacts/components/job_artifacts_table_spec.js
index 3628af31aa1..ba77d90f4e2 100644
--- a/spec/frontend/ci/artifacts/components/job_artifacts_table_spec.js
+++ b/spec/frontend/ci/artifacts/components/job_artifacts_table_spec.js
@@ -2,7 +2,7 @@ import { GlLoadingIcon, GlTable, GlLink, GlPagination, GlModal, GlFormCheckbox }
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import getJobArtifactsResponse from 'test_fixtures/graphql/ci/artifacts/graphql/queries/get_job_artifacts.query.graphql.json';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import waitForPromises from 'helpers/wait_for_promises';
import JobArtifactsTable from '~/ci/artifacts/components/job_artifacts_table.vue';
import ArtifactsTableRowDetails from '~/ci/artifacts/components/artifacts_table_row_details.vue';
@@ -51,7 +51,7 @@ describe('JobArtifactsTable component', () => {
const findStatuses = () => wrapper.findAllByTestId('job-artifacts-job-status');
const findSuccessfulJobStatus = () => findStatuses().at(0);
- const findCiBadgeLink = () => findSuccessfulJobStatus().findComponent(CiBadgeLink);
+ const findCiIcon = () => findSuccessfulJobStatus().findComponent(CiIcon);
const findLinks = () => wrapper.findAllComponents(GlLink);
const findJobLink = () => findLinks().at(0);
@@ -201,12 +201,11 @@ describe('JobArtifactsTable component', () => {
});
it('shows the job status as an icon for a successful job', () => {
- expect(findCiBadgeLink().props()).toMatchObject({
+ expect(findCiIcon().props()).toMatchObject({
status: {
group: 'success',
},
- size: 'sm',
- showText: false,
+ showStatusText: false,
});
});
diff --git a/spec/frontend/ci/catalog/components/details/ci_resource_components_spec.js b/spec/frontend/ci/catalog/components/details/ci_resource_components_spec.js
index a41996d20b3..382f8e46203 100644
--- a/spec/frontend/ci/catalog/components/details/ci_resource_components_spec.js
+++ b/spec/frontend/ci/catalog/components/details/ci_resource_components_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlEmptyState, GlLoadingIcon } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { resolvers } from '~/ci/catalog/graphql/settings';
import CiResourceComponents from '~/ci/catalog/components/details/ci_resource_components.vue';
@@ -8,7 +8,7 @@ import getCiCatalogcomponentComponents from '~/ci/catalog/graphql/queries/get_ci
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/alert';
-import { mockComponents } from '../../mock';
+import { mockComponents, mockComponentsEmpty } from '../../mock';
Vue.use(VueApollo);
jest.mock('~/alert');
@@ -37,7 +37,9 @@ describe('CiResourceComponents', () => {
await waitForPromises();
};
+ const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findCopyToClipboardButton = (i) => wrapper.findAllByTestId('copy-to-clipboard').at(i);
const findComponents = () => wrapper.findAllByTestId('component-section');
beforeEach(() => {
@@ -82,30 +84,61 @@ describe('CiResourceComponents', () => {
});
describe('when queries have loaded', () => {
- beforeEach(async () => {
- await createComponent();
- });
+ describe('and there is no metadata', () => {
+ beforeEach(async () => {
+ mockComponentsResponse.mockResolvedValue(mockComponentsEmpty);
+ await createComponent();
+ });
- it('renders every component', () => {
- expect(findComponents()).toHaveLength(components.length);
- });
+ it('renders the empty state', () => {
+ expect(findEmptyState().exists()).toBe(true);
+ expect(findEmptyState().props().title).toBe('Component details not available');
+ });
- it('renders the component name, description and snippet', () => {
- components.forEach((component) => {
- expect(wrapper.text()).toContain(component.name);
- expect(wrapper.text()).toContain(component.description);
- expect(wrapper.text()).toContain(component.path);
+ it('does not render components', () => {
+ expect(findComponents()).toHaveLength(0);
});
});
- describe('inputs', () => {
- it('renders the component parameter attributes', () => {
- const [firstComponent] = components;
+ describe('and there is metadata', () => {
+ beforeEach(async () => {
+ await createComponent();
+ });
+
+ it('does not render the empty state', () => {
+ expect(findEmptyState().exists()).toBe(false);
+ });
+
+ it('renders every component', () => {
+ expect(findComponents()).toHaveLength(components.length);
+ });
+
+ it('renders the component name, description and snippet', () => {
+ components.forEach((component) => {
+ expect(wrapper.text()).toContain(component.name);
+ expect(wrapper.text()).toContain(component.description);
+ expect(wrapper.text()).toContain(component.path);
+ });
+ });
+
+ it('adds a copy-to-clipboard button', () => {
+ components.forEach((component, i) => {
+ const button = findCopyToClipboardButton(i);
+
+ expect(button.props().icon).toBe('copy-to-clipboard');
+ expect(button.attributes('data-clipboard-text')).toContain(component.path);
+ });
+ });
+
+ describe('inputs', () => {
+ it('renders the component parameter attributes', () => {
+ const [firstComponent] = components;
- firstComponent.inputs.nodes.forEach((input) => {
- expect(findComponents().at(0).text()).toContain(input.name);
- expect(findComponents().at(0).text()).toContain(input.defaultValue);
- expect(findComponents().at(0).text()).toContain('Yes');
+ firstComponent.inputs.nodes.forEach((input) => {
+ expect(findComponents().at(0).text()).toContain(input.name);
+ expect(findComponents().at(0).text()).toContain(input.defaultValue);
+ expect(findComponents().at(0).text()).toContain('Yes');
+ });
});
});
});
diff --git a/spec/frontend/ci/catalog/components/details/ci_resource_header_spec.js b/spec/frontend/ci/catalog/components/details/ci_resource_header_spec.js
index 6ab9520508d..c061332ba13 100644
--- a/spec/frontend/ci/catalog/components/details/ci_resource_header_spec.js
+++ b/spec/frontend/ci/catalog/components/details/ci_resource_header_spec.js
@@ -2,8 +2,8 @@ import { GlAvatar, GlAvatarLink, GlBadge } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import CiResourceHeader from '~/ci/catalog/components/details/ci_resource_header.vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import CiResourceAbout from '~/ci/catalog/components/details/ci_resource_about.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { catalogSharedDataMock, catalogAdditionalDetailsMock } from '../../mock';
describe('CiResourceHeader', () => {
@@ -24,7 +24,7 @@ describe('CiResourceHeader', () => {
const findAvatar = () => wrapper.findComponent(GlAvatar);
const findAvatarLink = () => wrapper.findComponent(GlAvatarLink);
const findVersionBadge = () => wrapper.findComponent(GlBadge);
- const findPipelineStatusBadge = () => wrapper.findComponent(CiBadgeLink);
+ const findPipelineStatusBadge = () => wrapper.findComponent(CiIcon);
const createComponent = ({ props = {} } = {}) => {
wrapper = shallowMountExtended(CiResourceHeader, {
@@ -126,8 +126,7 @@ describe('CiResourceHeader', () => {
expect(findPipelineStatusBadge().exists()).toBe(hasPipelineBadge);
if (hasPipelineBadge) {
expect(findPipelineStatusBadge().props()).toEqual({
- showText: true,
- size: 'sm',
+ showStatusText: true,
status: pipelineStatus,
showTooltip: true,
useLink: true,
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 912fd9e1a93..2a5c24d0515 100644
--- a/spec/frontend/ci/catalog/components/list/catalog_header_spec.js
+++ b/spec/frontend/ci/catalog/components/list/catalog_header_spec.js
@@ -10,36 +10,53 @@ describe('CatalogHeader', () => {
let wrapper;
const defaultProps = {};
- const defaultProvide = {
+ const customProvide = {
pageTitle: 'Catalog page',
pageDescription: 'This is a nice catalog page',
};
const findBanner = () => wrapper.findComponent(GlBanner);
const findFeedbackButton = () => findBanner().findComponent(GlButton);
- const findTitle = () => wrapper.findByText(defaultProvide.pageTitle);
- const findDescription = () => wrapper.findByText(defaultProvide.pageDescription);
+ const findTitle = () => wrapper.find('h1');
+ const findDescription = () => wrapper.findByTestId('description');
- const createComponent = ({ props = {}, stubs = {} } = {}) => {
+ const createComponent = ({ props = {}, provide = {}, stubs = {} } = {}) => {
wrapper = shallowMountExtended(CatalogHeader, {
propsData: {
...defaultProps,
...props,
},
- provide: defaultProvide,
+ provide,
stubs: {
...stubs,
},
});
};
- it('renders the Catalog title and description', () => {
- createComponent();
+ describe('title and description', () => {
+ describe('when there are no values provided', () => {
+ beforeEach(() => {
+ createComponent();
+ });
- expect(findTitle().exists()).toBe(true);
- expect(findDescription().exists()).toBe(true);
- });
+ it('renders the default values', () => {
+ expect(findTitle().text()).toBe('CI/CD Catalog');
+ expect(findDescription().text()).toBe(
+ 'Discover CI configuration resources for a seamless CI/CD experience.',
+ );
+ });
+ });
+ describe('when custom values are provided', () => {
+ beforeEach(() => {
+ createComponent({ provide: customProvide });
+ });
+ it('renders the custom values', () => {
+ expect(findTitle().text()).toBe(customProvide.pageTitle);
+ expect(findDescription().text()).toBe(customProvide.pageDescription);
+ });
+ });
+ });
describe('Feedback banner', () => {
describe('when user has never dismissed', () => {
beforeEach(() => {
diff --git a/spec/frontend/ci/catalog/components/list/ci_resources_list_item_spec.js b/spec/frontend/ci/catalog/components/list/ci_resources_list_item_spec.js
index 7f446064366..3862195d8c7 100644
--- a/spec/frontend/ci/catalog/components/list/ci_resources_list_item_spec.js
+++ b/spec/frontend/ci/catalog/components/list/ci_resources_list_item_spec.js
@@ -48,7 +48,6 @@ describe('CiResourcesListItem', () => {
const findUserLink = () => wrapper.findByTestId('user-link');
const findTimeAgoMessage = () => wrapper.findComponent(GlSprintf);
const findFavorites = () => wrapper.findByTestId('stats-favorites');
- const findForks = () => wrapper.findByTestId('stats-forks');
beforeEach(() => {
router = createRouter();
@@ -161,7 +160,6 @@ describe('CiResourcesListItem', () => {
createComponent({
props: {
resource: {
- forksCount: 0,
starCount: 0,
},
},
@@ -172,11 +170,6 @@ describe('CiResourcesListItem', () => {
expect(findFavorites().exists()).toBe(true);
expect(findFavorites().text()).toBe('0');
});
-
- it('render forks as 0', () => {
- expect(findForks().exists()).toBe(true);
- expect(findForks().text()).toBe('0');
- });
});
describe('where there are statistics', () => {
@@ -188,11 +181,6 @@ describe('CiResourcesListItem', () => {
expect(findFavorites().exists()).toBe(true);
expect(findFavorites().text()).toBe(String(defaultProps.resource.starCount));
});
-
- it('render forks', () => {
- expect(findForks().exists()).toBe(true);
- expect(findForks().text()).toBe(String(defaultProps.resource.forksCount));
- });
});
});
});
diff --git a/spec/frontend/ci/catalog/components/pages/ci_resources_page_spec.js b/spec/frontend/ci/catalog/components/pages/ci_resources_page_spec.js
new file mode 100644
index 00000000000..e18b418b155
--- /dev/null
+++ b/spec/frontend/ci/catalog/components/pages/ci_resources_page_spec.js
@@ -0,0 +1,211 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { createAlert } from '~/alert';
+
+import CatalogHeader from '~/ci/catalog/components/list/catalog_header.vue';
+import CiResourcesList from '~/ci/catalog/components/list/ci_resources_list.vue';
+import CatalogListSkeletonLoader from '~/ci/catalog/components/list/catalog_list_skeleton_loader.vue';
+import EmptyState from '~/ci/catalog/components/list/empty_state.vue';
+import { cacheConfig } from '~/ci/catalog/graphql/settings';
+import ciResourcesPage from '~/ci/catalog/components/pages/ci_resources_page.vue';
+
+import getCatalogResources from '~/ci/catalog/graphql/queries/get_ci_catalog_resources.query.graphql';
+
+import { emptyCatalogResponseBody, catalogResponseBody } from '../../mock';
+
+Vue.use(VueApollo);
+jest.mock('~/alert');
+
+describe('CiResourcesPage', () => {
+ let wrapper;
+ let catalogResourcesResponse;
+
+ const createComponent = () => {
+ const handlers = [[getCatalogResources, catalogResourcesResponse]];
+ const mockApollo = createMockApollo(handlers, {}, cacheConfig);
+
+ wrapper = shallowMountExtended(ciResourcesPage, {
+ apolloProvider: mockApollo,
+ });
+
+ return waitForPromises();
+ };
+
+ const findCatalogHeader = () => wrapper.findComponent(CatalogHeader);
+ const findCiResourcesList = () => wrapper.findComponent(CiResourcesList);
+ const findLoadingState = () => wrapper.findComponent(CatalogListSkeletonLoader);
+ const findEmptyState = () => wrapper.findComponent(EmptyState);
+
+ beforeEach(() => {
+ catalogResourcesResponse = jest.fn();
+ });
+
+ describe('when initial queries are loading', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('shows a loading icon and no list', () => {
+ expect(findLoadingState().exists()).toBe(true);
+ expect(findEmptyState().exists()).toBe(false);
+ expect(findCiResourcesList().exists()).toBe(false);
+ });
+ });
+
+ describe('when queries have loaded', () => {
+ it('renders the Catalog Header', async () => {
+ await createComponent();
+
+ expect(findCatalogHeader().exists()).toBe(true);
+ });
+
+ describe('and there are no resources', () => {
+ beforeEach(async () => {
+ catalogResourcesResponse.mockResolvedValue(emptyCatalogResponseBody);
+
+ await createComponent();
+ });
+
+ it('renders the empty state', () => {
+ expect(findLoadingState().exists()).toBe(false);
+ expect(findEmptyState().exists()).toBe(true);
+ expect(findCiResourcesList().exists()).toBe(false);
+ });
+ });
+
+ describe('and there are resources', () => {
+ const { nodes, pageInfo, count } = catalogResponseBody.data.ciCatalogResources;
+
+ beforeEach(async () => {
+ catalogResourcesResponse.mockResolvedValue(catalogResponseBody);
+
+ await createComponent();
+ });
+ it('renders the resources list', () => {
+ expect(findLoadingState().exists()).toBe(false);
+ expect(findEmptyState().exists()).toBe(false);
+ expect(findCiResourcesList().exists()).toBe(true);
+ });
+
+ it('passes down props to the resources list', () => {
+ expect(findCiResourcesList().props()).toMatchObject({
+ currentPage: 1,
+ resources: nodes,
+ pageInfo,
+ totalCount: count,
+ });
+ });
+ });
+ });
+
+ describe('pagination', () => {
+ it.each`
+ eventName
+ ${'onPrevPage'}
+ ${'onNextPage'}
+ `('refetch query with new params when receiving $eventName', async ({ eventName }) => {
+ const { pageInfo } = catalogResponseBody.data.ciCatalogResources;
+
+ catalogResourcesResponse.mockResolvedValue(catalogResponseBody);
+ await createComponent();
+
+ expect(catalogResourcesResponse).toHaveBeenCalledTimes(1);
+
+ await findCiResourcesList().vm.$emit(eventName);
+
+ expect(catalogResourcesResponse).toHaveBeenCalledTimes(2);
+
+ if (eventName === 'onNextPage') {
+ expect(catalogResourcesResponse.mock.calls[1][0]).toEqual({
+ after: pageInfo.endCursor,
+ first: 20,
+ });
+ } else {
+ expect(catalogResourcesResponse.mock.calls[1][0]).toEqual({
+ before: pageInfo.startCursor,
+ last: 20,
+ first: null,
+ });
+ }
+ });
+ });
+
+ describe('pages count', () => {
+ describe('when the fetchMore call suceeds', () => {
+ beforeEach(async () => {
+ catalogResourcesResponse.mockResolvedValue(catalogResponseBody);
+
+ await createComponent();
+ });
+
+ it('increments and drecrements the page count correctly', async () => {
+ expect(findCiResourcesList().props().currentPage).toBe(1);
+
+ findCiResourcesList().vm.$emit('onNextPage');
+ await waitForPromises();
+
+ expect(findCiResourcesList().props().currentPage).toBe(2);
+
+ await findCiResourcesList().vm.$emit('onPrevPage');
+ await waitForPromises();
+
+ expect(findCiResourcesList().props().currentPage).toBe(1);
+ });
+ });
+
+ describe('when the fetchMore call fails', () => {
+ const errorMessage = 'there was an error';
+
+ describe('for next page', () => {
+ beforeEach(async () => {
+ catalogResourcesResponse.mockResolvedValueOnce(catalogResponseBody);
+ catalogResourcesResponse.mockRejectedValue({ message: errorMessage });
+
+ await createComponent();
+ });
+
+ it('does not increment the page and calls createAlert', async () => {
+ expect(findCiResourcesList().props().currentPage).toBe(1);
+
+ findCiResourcesList().vm.$emit('onNextPage');
+ await waitForPromises();
+
+ expect(findCiResourcesList().props().currentPage).toBe(1);
+ expect(createAlert).toHaveBeenCalledWith({ message: errorMessage, variant: 'danger' });
+ });
+ });
+
+ describe('for previous page', () => {
+ beforeEach(async () => {
+ // Initial query
+ catalogResourcesResponse.mockResolvedValueOnce(catalogResponseBody);
+ // When clicking on next
+ catalogResourcesResponse.mockResolvedValueOnce(catalogResponseBody);
+ // when clicking on previous
+ catalogResourcesResponse.mockRejectedValue({ message: errorMessage });
+
+ await createComponent();
+ });
+
+ it('does not decrement the page and calls createAlert', async () => {
+ expect(findCiResourcesList().props().currentPage).toBe(1);
+
+ findCiResourcesList().vm.$emit('onNextPage');
+ await waitForPromises();
+
+ expect(findCiResourcesList().props().currentPage).toBe(2);
+
+ findCiResourcesList().vm.$emit('onPrevPage');
+ await waitForPromises();
+
+ expect(findCiResourcesList().props().currentPage).toBe(2);
+ expect(createAlert).toHaveBeenCalledWith({ message: errorMessage, variant: 'danger' });
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ci/catalog/global_catalog_spec.js b/spec/frontend/ci/catalog/global_catalog_spec.js
new file mode 100644
index 00000000000..fddabf46c0b
--- /dev/null
+++ b/spec/frontend/ci/catalog/global_catalog_spec.js
@@ -0,0 +1,17 @@
+import { shallowMount } from '@vue/test-utils';
+import GlobalCatalog from '~/ci/catalog/global_catalog.vue';
+import CiCatalogHome from '~/ci/catalog/components/ci_catalog_home.vue';
+
+describe('GlobalCatalog', () => {
+ let wrapper;
+
+ const findHomeComponent = () => wrapper.findComponent(CiCatalogHome);
+
+ beforeEach(() => {
+ wrapper = shallowMount(GlobalCatalog);
+ });
+
+ it('renders the catalog home component', () => {
+ expect(findHomeComponent().exists()).toBe(true);
+ });
+});
diff --git a/spec/frontend/ci/catalog/index_spec.js b/spec/frontend/ci/catalog/index_spec.js
new file mode 100644
index 00000000000..01332cfbb3d
--- /dev/null
+++ b/spec/frontend/ci/catalog/index_spec.js
@@ -0,0 +1,48 @@
+import Vue from 'vue';
+import { initCatalog } from '~/ci/catalog/';
+import * as Router from '~/ci/catalog/router';
+import CiResourcesPage from '~/ci/catalog/components/pages/ci_resources_page.vue';
+
+describe('~/ci/catalog/index', () => {
+ describe('initCatalog', () => {
+ const SELECTOR = 'SELECTOR';
+
+ let el;
+ let component;
+ const baseRoute = '/explore/catalog';
+
+ const createElement = () => {
+ el = document.createElement('div');
+ el.id = SELECTOR;
+ el.dataset.ciCatalogPath = baseRoute;
+ document.body.appendChild(el);
+ };
+
+ afterEach(() => {
+ el = null;
+ });
+
+ describe('when the element exists', () => {
+ beforeEach(() => {
+ createElement();
+ jest.spyOn(Router, 'createRouter');
+ component = initCatalog(`#${SELECTOR}`);
+ });
+
+ it('returns a Vue Instance', () => {
+ expect(component).toBeInstanceOf(Vue);
+ });
+
+ it('creates a router with the received base path and component', () => {
+ expect(Router.createRouter).toHaveBeenCalledTimes(1);
+ expect(Router.createRouter).toHaveBeenCalledWith(baseRoute, CiResourcesPage);
+ });
+ });
+
+ describe('When the element does not exist', () => {
+ it('returns `null`', () => {
+ expect(initCatalog('foo')).toBe(null);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ci/catalog/mock.js b/spec/frontend/ci/catalog/mock.js
index 21fed6ac8ec..125f003224c 100644
--- a/spec/frontend/ci/catalog/mock.js
+++ b/spec/frontend/ci/catalog/mock.js
@@ -1,5 +1,23 @@
import { componentsMockData } from '~/ci/catalog/constants';
+export const emptyCatalogResponseBody = {
+ data: {
+ ciCatalogResources: {
+ pageInfo: {
+ startCursor:
+ 'eyJjcmVhdGVkX2F0IjoiMjAxNS0wNy0wMyAxMDowMDowMC4wMDAwMDAwMDAgKzAwMDAiLCJpZCI6IjEyOSJ9',
+ endCursor:
+ 'eyJjcmVhdGVkX2F0IjoiMjAxNS0wNy0wMyAxMDowMDowMC4wMDAwMDAwMDAgKzAwMDAiLCJpZCI6IjExMCJ9',
+ hasNextPage: false,
+ hasPreviousPage: false,
+ __typename: 'PageInfo',
+ },
+ count: 0,
+ nodes: [],
+ },
+ },
+};
+
export const catalogResponseBody = {
data: {
ciCatalogResources: {
@@ -20,7 +38,6 @@ export const catalogResponseBody = {
name: 'Project-42 Name',
description: 'A simple component',
starCount: 0,
- forksCount: 0,
latestVersion: null,
rootNamespace: {
id: 'gid://gitlab/Group/185',
@@ -37,7 +54,6 @@ export const catalogResponseBody = {
name: 'Project-41 Name',
description: 'A simple component',
starCount: 0,
- forksCount: 0,
latestVersion: null,
rootNamespace: {
id: 'gid://gitlab/Group/185',
@@ -54,7 +70,6 @@ export const catalogResponseBody = {
name: 'Project-40 Name',
description: 'A simple component',
starCount: 0,
- forksCount: 0,
latestVersion: null,
rootNamespace: {
id: 'gid://gitlab/Group/185',
@@ -71,7 +86,6 @@ export const catalogResponseBody = {
name: 'Project-39 Name',
description: 'A simple component',
starCount: 0,
- forksCount: 0,
latestVersion: null,
rootNamespace: {
id: 'gid://gitlab/Group/185',
@@ -88,7 +102,6 @@ export const catalogResponseBody = {
name: 'Project-38 Name',
description: 'A simple component',
starCount: 0,
- forksCount: 0,
latestVersion: null,
rootNamespace: {
id: 'gid://gitlab/Group/185',
@@ -105,7 +118,6 @@ export const catalogResponseBody = {
name: 'Project-37 Name',
description: 'A simple component',
starCount: 0,
- forksCount: 0,
latestVersion: null,
rootNamespace: {
id: 'gid://gitlab/Group/185',
@@ -122,7 +134,6 @@ export const catalogResponseBody = {
name: 'Project-36 Name',
description: 'A simple component',
starCount: 0,
- forksCount: 0,
latestVersion: null,
rootNamespace: {
id: 'gid://gitlab/Group/185',
@@ -139,7 +150,6 @@ export const catalogResponseBody = {
name: 'Project-35 Name',
description: 'A simple component',
starCount: 0,
- forksCount: 0,
latestVersion: null,
rootNamespace: {
id: 'gid://gitlab/Group/185',
@@ -156,7 +166,6 @@ export const catalogResponseBody = {
name: 'Project-34 Name',
description: 'A simple component',
starCount: 0,
- forksCount: 0,
latestVersion: null,
rootNamespace: {
id: 'gid://gitlab/Group/185',
@@ -173,7 +182,6 @@ export const catalogResponseBody = {
name: 'Project-33 Name',
description: 'A simple component',
starCount: 0,
- forksCount: 0,
latestVersion: null,
rootNamespace: {
id: 'gid://gitlab/Group/185',
@@ -190,7 +198,6 @@ export const catalogResponseBody = {
name: 'Project-32 Name',
description: 'A simple component',
starCount: 0,
- forksCount: 0,
latestVersion: null,
rootNamespace: {
id: 'gid://gitlab/Group/185',
@@ -207,7 +214,6 @@ export const catalogResponseBody = {
name: 'Project-31 Name',
description: 'A simple component',
starCount: 0,
- forksCount: 0,
latestVersion: null,
rootNamespace: {
id: 'gid://gitlab/Group/185',
@@ -224,7 +230,6 @@ export const catalogResponseBody = {
name: 'Project-30 Name',
description: 'A simple component',
starCount: 0,
- forksCount: 0,
latestVersion: null,
rootNamespace: {
id: 'gid://gitlab/Group/185',
@@ -241,7 +246,6 @@ export const catalogResponseBody = {
name: 'Project-29 Name',
description: 'A simple component',
starCount: 0,
- forksCount: 0,
latestVersion: null,
rootNamespace: {
id: 'gid://gitlab/Group/185',
@@ -258,7 +262,6 @@ export const catalogResponseBody = {
name: 'Project-28 Name',
description: 'A simple component',
starCount: 0,
- forksCount: 0,
latestVersion: null,
rootNamespace: {
id: 'gid://gitlab/Group/185',
@@ -275,7 +278,6 @@ export const catalogResponseBody = {
name: 'Project-27 Name',
description: 'A simple component',
starCount: 0,
- forksCount: 0,
latestVersion: null,
rootNamespace: {
id: 'gid://gitlab/Group/185',
@@ -292,7 +294,6 @@ export const catalogResponseBody = {
name: 'Project-26 Name',
description: 'A simple component',
starCount: 0,
- forksCount: 0,
latestVersion: null,
rootNamespace: {
id: 'gid://gitlab/Group/185',
@@ -309,7 +310,6 @@ export const catalogResponseBody = {
name: 'Project-25 Name',
description: 'A simple component',
starCount: 0,
- forksCount: 0,
latestVersion: null,
rootNamespace: {
id: 'gid://gitlab/Group/185',
@@ -326,7 +326,6 @@ export const catalogResponseBody = {
name: 'Project-24 Name',
description: 'A simple component',
starCount: 0,
- forksCount: 0,
latestVersion: null,
rootNamespace: {
id: 'gid://gitlab/Group/185',
@@ -343,7 +342,6 @@ export const catalogResponseBody = {
name: 'Project-23 Name',
description: 'A simple component',
starCount: 0,
- forksCount: 0,
latestVersion: null,
rootNamespace: {
id: 'gid://gitlab/Group/185',
@@ -380,7 +378,6 @@ export const catalogSinglePageResponse = {
name: 'Project-45 Name',
description: 'A simple component',
starCount: 0,
- forksCount: 0,
latestVersion: null,
rootNamespace: {
id: 'gid://gitlab/Group/185',
@@ -397,7 +394,6 @@ export const catalogSinglePageResponse = {
name: 'Project-44 Name',
description: 'A simple component',
starCount: 0,
- forksCount: 0,
latestVersion: null,
rootNamespace: {
id: 'gid://gitlab/Group/185',
@@ -414,7 +410,6 @@ export const catalogSinglePageResponse = {
name: 'Project-43 Name',
description: 'A simple component',
starCount: 0,
- forksCount: 0,
latestVersion: null,
rootNamespace: {
id: 'gid://gitlab/Group/185',
@@ -441,7 +436,6 @@ export const catalogSharedDataMock = {
name: 'Ruby',
rootNamespace: { id: 1, fullPath: '/group/project', name: 'my-dumb-project' },
starCount: 1,
- forksCount: 2,
latestVersion: {
__typename: 'Release',
id: '3',
@@ -506,7 +500,6 @@ const generateResourcesNodes = (count = 20, startId = 0) => {
__typename: 'CiCatalogResource',
id: `gid://gitlab/CiCatalogResource/${i}`,
description: `This is a component that does a bunch of stuff and is really just a number: ${i}`,
- forksCount: 5,
icon: 'my-icon',
name: `My component #${i}`,
rootNamespace: {
@@ -544,3 +537,13 @@ export const mockComponents = {
},
},
};
+
+export const mockComponentsEmpty = {
+ data: {
+ ciCatalogResource: {
+ __typename: 'CiCatalogResource',
+ id: `gid://gitlab/CiCatalogResource/1`,
+ components: [],
+ },
+ },
+};
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_variable_drawer_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_variable_drawer_spec.js
index 207ea7aa060..610aae3946f 100644
--- a/spec/frontend/ci/ci_variable_list/components/ci_variable_drawer_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_variable_drawer_spec.js
@@ -67,9 +67,9 @@ describe('CI Variable Drawer', () => {
});
};
- const findConfirmBtn = () => wrapper.findByTestId('ci-variable-confirm-btn');
+ const findConfirmBtn = () => wrapper.findByTestId('ci-variable-confirm-button');
const findConfirmDeleteModal = () => wrapper.findComponent(GlModal);
- const findDeleteBtn = () => wrapper.findByTestId('ci-variable-delete-btn');
+ const findDeleteBtn = () => wrapper.findByTestId('ci-variable-delete-button');
const findDisabledEnvironmentScopeDropdown = () => wrapper.findComponent(GlFormInput);
const findDrawer = () => wrapper.findComponent(GlDrawer);
const findEnvironmentScopeDropdown = () => wrapper.findComponent(CiEnvironmentsDropdown);
@@ -350,6 +350,13 @@ describe('CI Variable Drawer', () => {
});
describe('drawer events', () => {
+ it('emits `search-environment-scope` before mounting', () => {
+ createComponent();
+
+ expect(wrapper.emitted('search-environment-scope')).toHaveLength(1);
+ expect(wrapper.emitted('search-environment-scope')).toEqual([['']]);
+ });
+
it('emits `close-form` when closing the drawer', async () => {
createComponent();
@@ -477,7 +484,7 @@ describe('CI Variable Drawer', () => {
it('bubbles up the search event', async () => {
await findEnvironmentScopeDropdown().vm.$emit('search-environment-scope', 'staging');
- expect(wrapper.emitted('search-environment-scope')).toEqual([['staging']]);
+ expect(wrapper.emitted('search-environment-scope')[1]).toEqual(['staging']);
});
});
});
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js
deleted file mode 100644
index 5ba9b3b8c20..00000000000
--- a/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js
+++ /dev/null
@@ -1,576 +0,0 @@
-import { GlButton, GlFormInput, GlSprintf } from '@gitlab/ui';
-import { mockTracking } from 'helpers/tracking_helper';
-import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
-import CiEnvironmentsDropdown from '~/ci/ci_variable_list/components/ci_environments_dropdown.vue';
-import CiVariableModal from '~/ci/ci_variable_list/components/ci_variable_modal.vue';
-import {
- ADD_VARIABLE_ACTION,
- AWS_ACCESS_KEY_ID,
- EDIT_VARIABLE_ACTION,
- EVENT_LABEL,
- EVENT_ACTION,
- ENVIRONMENT_SCOPE_LINK_TITLE,
- AWS_TIP_TITLE,
- AWS_TIP_MESSAGE,
- instanceString,
- variableOptions,
-} from '~/ci/ci_variable_list/constants';
-import { mockVariablesWithScopes } from '../mocks';
-import ModalStub from '../stubs';
-
-describe('Ci variable modal', () => {
- let wrapper;
- let trackingSpy;
-
- const maskableRegex = '^[a-zA-Z0-9_+=/@:.~-]{8,}$';
- const maskableRawRegex = '^\\S{8,}$';
-
- const mockVariables = mockVariablesWithScopes(instanceString);
-
- const defaultProvide = {
- containsVariableReferenceLink: '/reference',
- environmentScopeLink: '/help/environments',
- glFeatures: {
- ciRemoveCharacterLimitationRawMaskedVar: true,
- },
- isProtectedByDefault: false,
- maskedEnvironmentVariablesLink: '/variables-link',
- maskableRawRegex,
- maskableRegex,
- };
-
- const defaultProps = {
- areEnvironmentsLoading: false,
- areScopedVariablesAvailable: true,
- environments: [],
- hideEnvironmentScope: false,
- mode: ADD_VARIABLE_ACTION,
- selectedVariable: {},
- variables: [],
- };
-
- const createComponent = ({ mountFn = shallowMountExtended, props = {}, provide = {} } = {}) => {
- wrapper = mountFn(CiVariableModal, {
- attachTo: document.body,
- provide: { ...defaultProvide, ...provide },
- propsData: {
- ...defaultProps,
- ...props,
- },
- stubs: {
- GlModal: ModalStub,
- },
- });
- };
-
- const findCiEnvironmentsDropdown = () => wrapper.findComponent(CiEnvironmentsDropdown);
- const findReferenceWarning = () => wrapper.findByTestId('contains-variable-reference');
- const findModal = () => wrapper.findComponent(ModalStub);
- const findAWSTip = () => wrapper.findByTestId('aws-guidance-tip');
- const findAddorUpdateButton = () => wrapper.findByTestId('ciUpdateOrAddVariableBtn');
- const deleteVariableButton = () =>
- findModal()
- .findAllComponents(GlButton)
- .wrappers.find((button) => button.props('variant') === 'danger');
- const findExpandedVariableCheckbox = () => wrapper.findByTestId('ci-variable-expanded-checkbox');
- const findProtectedVariableCheckbox = () =>
- wrapper.findByTestId('ci-variable-protected-checkbox');
- const findMaskedVariableCheckbox = () => wrapper.findByTestId('ci-variable-masked-checkbox');
- const findValueField = () => wrapper.find('#ci-variable-value');
- const findEnvScopeLink = () => wrapper.findByTestId('environment-scope-link');
- const findEnvScopeInput = () =>
- wrapper.findByTestId('environment-scope').findComponent(GlFormInput);
- const findRawVarTip = () => wrapper.findByTestId('raw-variable-tip');
- const findVariableTypeDropdown = () => wrapper.find('#ci-variable-type');
- const findEnvironmentScopeText = () => wrapper.findByText('Environment scope');
-
- describe('Adding a variable', () => {
- describe('when no key/value pair are present', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('shows the submit button as disabled', () => {
- expect(findAddorUpdateButton().attributes('disabled')).toBeDefined();
- });
- });
-
- describe('when a key/value pair is present', () => {
- beforeEach(() => {
- createComponent({ props: { selectedVariable: mockVariables[0] } });
- });
-
- it('shows the submit button as enabled', () => {
- expect(findAddorUpdateButton().attributes('disabled')).toBeUndefined();
- });
- });
-
- describe('events', () => {
- const [currentVariable] = mockVariables;
-
- beforeEach(() => {
- createComponent({ props: { selectedVariable: currentVariable } });
- });
-
- it('Dispatches `add-variable` action on submit', () => {
- findAddorUpdateButton().vm.$emit('click');
- expect(wrapper.emitted('add-variable')).toEqual([[currentVariable]]);
- });
-
- it('Dispatches the `close-form` event when dismissing', () => {
- findModal().vm.$emit('hidden');
- expect(wrapper.emitted('close-form')).toEqual([[]]);
- });
- });
- });
-
- describe('when protected by default', () => {
- describe('when adding a new variable', () => {
- beforeEach(() => {
- createComponent({ provide: { isProtectedByDefault: true } });
- findModal().vm.$emit('shown');
- });
-
- it('updates the protected value to true', () => {
- expect(findProtectedVariableCheckbox().attributes('data-is-protected-checked')).toBe(
- 'true',
- );
- });
- });
-
- describe('when editing a variable', () => {
- beforeEach(() => {
- createComponent({
- provide: { isProtectedByDefault: false },
- props: {
- selectedVariable: {},
- mode: EDIT_VARIABLE_ACTION,
- },
- });
- findModal().vm.$emit('shown');
- });
-
- it('keeps the value as false', () => {
- expect(
- findProtectedVariableCheckbox().attributes('data-is-protected-checked'),
- ).toBeUndefined();
- });
- });
- });
-
- describe('Adding a new non-AWS variable', () => {
- beforeEach(() => {
- const [variable] = mockVariables;
- createComponent({ mountFn: mountExtended, props: { selectedVariable: variable } });
- });
-
- it('does not show AWS guidance tip', () => {
- const tip = findAWSTip();
-
- expect(tip.isVisible()).toBe(false);
- });
- });
-
- describe('Adding a new AWS variable', () => {
- beforeEach(() => {
- const [variable] = mockVariables;
- const AWSKeyVariable = {
- ...variable,
- key: AWS_ACCESS_KEY_ID,
- value: 'AKIAIOSFODNN7EXAMPLEjdhy',
- };
- createComponent({
- mountFn: shallowMountExtended,
- props: { selectedVariable: AWSKeyVariable },
- });
- });
-
- it('shows AWS guidance tip', () => {
- const tip = findAWSTip();
-
- expect(tip.isVisible()).toBe(true);
- expect(tip.props('title')).toBe(AWS_TIP_TITLE);
- expect(tip.findComponent(GlSprintf).attributes('message')).toBe(AWS_TIP_MESSAGE);
- });
- });
-
- describe('when expanded', () => {
- describe('with a $ character', () => {
- beforeEach(() => {
- const [variable] = mockVariables;
- const variableWithDollarSign = {
- ...variable,
- value: 'valueWith$',
- };
- createComponent({
- mountFn: mountExtended,
- props: { selectedVariable: variableWithDollarSign },
- });
- });
-
- it(`renders the variable reference warning`, () => {
- expect(findReferenceWarning().exists()).toBe(true);
- });
-
- it(`does not render raw variable tip`, () => {
- expect(findRawVarTip().exists()).toBe(false);
- });
- });
-
- describe('without a $ character', () => {
- beforeEach(() => {
- const [variable] = mockVariables;
- createComponent({
- mountFn: mountExtended,
- props: { selectedVariable: variable },
- });
- });
-
- it(`does not render the variable reference warning`, () => {
- expect(findReferenceWarning().exists()).toBe(false);
- });
-
- it(`does not render raw variable tip`, () => {
- expect(findRawVarTip().exists()).toBe(false);
- });
- });
-
- describe('setting raw value', () => {
- const [variable] = mockVariables;
-
- it('defaults to expanded and raw:false when adding a variable', () => {
- createComponent({ props: { selectedVariable: variable } });
-
- findModal().vm.$emit('shown');
-
- expect(findExpandedVariableCheckbox().attributes('checked')).toBe('true');
-
- findAddorUpdateButton().vm.$emit('click');
-
- expect(wrapper.emitted('add-variable')).toEqual([
- [
- {
- ...variable,
- raw: false,
- },
- ],
- ]);
- });
-
- it('sets correct raw value when editing', async () => {
- createComponent({
- props: {
- selectedVariable: variable,
- mode: EDIT_VARIABLE_ACTION,
- },
- });
-
- findModal().vm.$emit('shown');
- await findExpandedVariableCheckbox().vm.$emit('change');
- await findAddorUpdateButton().vm.$emit('click');
-
- expect(wrapper.emitted('update-variable')).toEqual([
- [
- {
- ...variable,
- raw: true,
- },
- ],
- ]);
- });
- });
- });
-
- describe('when not expanded', () => {
- describe('with a $ character', () => {
- beforeEach(() => {
- const selectedVariable = mockVariables[1];
- createComponent({
- mountFn: mountExtended,
- props: { selectedVariable },
- });
- });
-
- it(`renders raw variable tip`, () => {
- expect(findRawVarTip().exists()).toBe(true);
- });
- });
- });
-
- describe('Editing a variable', () => {
- const [variable] = mockVariables;
-
- beforeEach(() => {
- createComponent({ props: { selectedVariable: variable, mode: EDIT_VARIABLE_ACTION } });
- });
-
- it('button text is Update variable when updating', () => {
- expect(findAddorUpdateButton().text()).toBe('Update variable');
- });
-
- it('Update variable button dispatches updateVariable with correct variable', () => {
- findAddorUpdateButton().vm.$emit('click');
- expect(wrapper.emitted('update-variable')).toEqual([[variable]]);
- });
-
- it('Propagates the `close-form` event', () => {
- findModal().vm.$emit('hidden');
- expect(wrapper.emitted('close-form')).toEqual([[]]);
- });
-
- it('dispatches `delete-variable` with correct variable to delete', () => {
- deleteVariableButton().vm.$emit('click');
- expect(wrapper.emitted('delete-variable')).toEqual([[variable]]);
- });
- });
-
- describe('Environment scope', () => {
- describe('when feature is available', () => {
- describe('and section is not hidden', () => {
- beforeEach(() => {
- createComponent({
- mountFn: mountExtended,
- props: {
- areScopedVariablesAvailable: true,
- hideEnvironmentScope: false,
- },
- });
- });
-
- it('renders the environment dropdown and section title', () => {
- expect(findCiEnvironmentsDropdown().exists()).toBe(true);
- expect(findCiEnvironmentsDropdown().isVisible()).toBe(true);
- expect(findEnvironmentScopeText().exists()).toBe(true);
- });
-
- it('renders a link to documentation on scopes', () => {
- const link = findEnvScopeLink();
-
- expect(link.attributes('title')).toBe(ENVIRONMENT_SCOPE_LINK_TITLE);
- expect(link.attributes('href')).toBe(defaultProvide.environmentScopeLink);
- });
- });
-
- describe('and section is hidden', () => {
- beforeEach(() => {
- createComponent({
- mountFn: mountExtended,
- props: {
- areScopedVariablesAvailable: true,
- hideEnvironmentScope: true,
- },
- });
- });
-
- it('does not renders the environment dropdown and section title', () => {
- expect(findCiEnvironmentsDropdown().exists()).toBe(false);
- expect(findEnvironmentScopeText().exists()).toBe(false);
- });
- });
- });
-
- describe('when feature is not available', () => {
- describe('and section is not hidden', () => {
- beforeEach(() => {
- createComponent({
- mountFn: mountExtended,
- props: {
- areScopedVariablesAvailable: false,
- hideEnvironmentScope: false,
- },
- });
- });
-
- it('disables the dropdown', () => {
- expect(findCiEnvironmentsDropdown().exists()).toBe(false);
- expect(findEnvironmentScopeText().exists()).toBe(true);
- expect(findEnvScopeInput().attributes('readonly')).toBe('readonly');
- });
- });
-
- describe('and section is hidden', () => {
- beforeEach(() => {
- createComponent({
- mountFn: mountExtended,
- props: {
- areScopedVariablesAvailable: false,
- hideEnvironmentScope: true,
- },
- });
- });
-
- it('hides the dropdown', () => {
- expect(findEnvironmentScopeText().exists()).toBe(false);
- expect(findCiEnvironmentsDropdown().exists()).toBe(false);
- });
- });
- });
- });
-
- describe('variable type dropdown', () => {
- describe('default behaviour', () => {
- beforeEach(() => {
- createComponent({ mountFn: mountExtended });
- });
-
- it('adds each option as a dropdown item', () => {
- expect(findVariableTypeDropdown().findAll('option')).toHaveLength(variableOptions.length);
- variableOptions.forEach((v) => {
- expect(findVariableTypeDropdown().text()).toContain(v.text);
- });
- });
- });
- });
-
- describe('Validations', () => {
- const maskError = 'This variable value does not meet the masking requirements.';
- const helpText = 'Value must meet regular expression requirements to be masked.';
-
- describe('when the variable is raw', () => {
- const [variable] = mockVariables;
- const validRawMaskedVariable = {
- ...variable,
- value: 'd$%^asdsadas',
- masked: false,
- raw: true,
- };
-
- beforeEach(() => {
- createComponent({
- mountFn: mountExtended,
- props: { selectedVariable: validRawMaskedVariable },
- });
- });
-
- it('should not show an error with symbols', async () => {
- await findMaskedVariableCheckbox().trigger('click');
-
- expect(findModal().text()).not.toContain(maskError);
- });
-
- it('should not show an error when length is less than 8', async () => {
- await findValueField().vm.$emit('input', 'a');
- await findMaskedVariableCheckbox().trigger('click');
-
- expect(findModal().text()).toContain(maskError);
- });
-
- it('does not show the masked variable help text', () => {
- expect(findModal().text()).not.toContain(helpText);
- });
- });
-
- describe('when the value is empty', () => {
- beforeEach(() => {
- const [variable] = mockVariables;
- const emptyValueVariable = { ...variable, value: '' };
- createComponent({
- mountFn: mountExtended,
- props: { selectedVariable: emptyValueVariable },
- });
- });
-
- it('allows user to submit', () => {
- expect(findAddorUpdateButton().attributes('disabled')).toBeUndefined();
- });
- });
-
- describe('when the mask state is invalid', () => {
- beforeEach(async () => {
- const [variable] = mockVariables;
- const invalidMaskVariable = {
- ...variable,
- value: 'd:;',
- masked: false,
- };
- createComponent({
- mountFn: mountExtended,
- props: { selectedVariable: invalidMaskVariable },
- });
- trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
- await findMaskedVariableCheckbox().trigger('click');
- });
-
- it('disables the submit button', () => {
- expect(findAddorUpdateButton().attributes('disabled')).toBeDefined();
- });
-
- it('shows the correct error text and help text', () => {
- expect(findModal().text()).toContain(maskError);
- expect(findModal().text()).toContain(helpText);
- });
-
- it('sends the correct tracking event', () => {
- expect(trackingSpy).toHaveBeenCalledWith(undefined, EVENT_ACTION, {
- label: EVENT_LABEL,
- property: ';',
- });
- });
- });
-
- describe.each`
- value | masked | eventSent | trackingErrorProperty
- ${'secretValue'} | ${false} | ${0} | ${null}
- ${'short'} | ${true} | ${0} | ${null}
- ${'dollar$ign'} | ${false} | ${1} | ${'$'}
- ${'dollar$ign'} | ${true} | ${1} | ${'$'}
- ${'unsupported|char'} | ${true} | ${1} | ${'|'}
- ${'unsupported|char'} | ${false} | ${0} | ${null}
- `('Adding a new variable', ({ value, masked, eventSent, trackingErrorProperty }) => {
- beforeEach(async () => {
- const [variable] = mockVariables;
- const invalidKeyVariable = {
- ...variable,
- value: '',
- masked: false,
- };
- createComponent({
- mountFn: mountExtended,
- props: { selectedVariable: invalidKeyVariable },
- });
- trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
- await findValueField().vm.$emit('input', value);
- if (masked) {
- await findMaskedVariableCheckbox().trigger('click');
- }
- });
-
- it(`${
- eventSent > 0 ? 'sends the correct' : 'does not send the'
- } variable validation tracking event with ${value}`, () => {
- expect(trackingSpy).toHaveBeenCalledTimes(eventSent);
-
- if (eventSent > 0) {
- expect(trackingSpy).toHaveBeenCalledWith(undefined, EVENT_ACTION, {
- label: EVENT_LABEL,
- property: trackingErrorProperty,
- });
- }
- });
- });
-
- describe('when masked variable has acceptable value', () => {
- beforeEach(() => {
- const [variable] = mockVariables;
- const validMaskandKeyVariable = {
- ...variable,
- key: AWS_ACCESS_KEY_ID,
- value: '12345678',
- masked: true,
- };
- createComponent({
- mountFn: mountExtended,
- props: { selectedVariable: validMaskandKeyVariable },
- });
- });
-
- it('shows the help text', () => {
- expect(findModal().text()).toContain(helpText);
- });
-
- it('does not disable the submit button', () => {
- expect(findAddorUpdateButton().attributes('disabled')).toBeUndefined();
- });
- });
- });
-});
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_variable_settings_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_variable_settings_spec.js
index 04145c2c6aa..01d3cdf504d 100644
--- a/spec/frontend/ci/ci_variable_list/components/ci_variable_settings_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_variable_settings_spec.js
@@ -1,6 +1,5 @@
import { shallowMount } from '@vue/test-utils';
import CiVariableSettings from '~/ci/ci_variable_list/components/ci_variable_settings.vue';
-import CiVariableModal from '~/ci/ci_variable_list/components/ci_variable_modal.vue';
import CiVariableTable from '~/ci/ci_variable_list/components/ci_variable_table.vue';
import CiVariableDrawer from '~/ci/ci_variable_list/components/ci_variable_drawer.vue';
@@ -30,20 +29,13 @@ describe('Ci variable table', () => {
const findCiVariableDrawer = () => wrapper.findComponent(CiVariableDrawer);
const findCiVariableTable = () => wrapper.findComponent(CiVariableTable);
- const findCiVariableModal = () => wrapper.findComponent(CiVariableModal);
- const createComponent = ({ props = {}, featureFlags = {} } = {}) => {
+ const createComponent = ({ props = {} } = {}) => {
wrapper = shallowMount(CiVariableSettings, {
propsData: {
...defaultProps,
...props,
},
- provide: {
- glFeatures: {
- ciVariableDrawer: false,
- ...featureFlags,
- },
- },
});
};
@@ -60,24 +52,8 @@ describe('Ci variable table', () => {
});
});
- it('passes props down correctly to the ci modal', async () => {
- createComponent();
-
- await findCiVariableTable().vm.$emit('set-selected-variable');
-
- expect(findCiVariableModal().props()).toEqual({
- areEnvironmentsLoading: defaultProps.areEnvironmentsLoading,
- areScopedVariablesAvailable: defaultProps.areScopedVariablesAvailable,
- environments: defaultProps.environments,
- hideEnvironmentScope: defaultProps.hideEnvironmentScope,
- variables: defaultProps.variables,
- mode: ADD_VARIABLE_ACTION,
- selectedVariable: {},
- });
- });
-
it('passes props down correctly to the ci drawer', async () => {
- createComponent({ featureFlags: { ciVariableDrawer: true } });
+ createComponent();
await findCiVariableTable().vm.$emit('set-selected-variable');
@@ -92,55 +68,51 @@ describe('Ci variable table', () => {
});
});
- describe.each`
- bool | flagStatus | elementName | findElement
- ${false} | ${'disabled'} | ${'modal'} | ${findCiVariableModal}
- ${true} | ${'enabled'} | ${'drawer'} | ${findCiVariableDrawer}
- `('when ciVariableDrawer feature flag is $flagStatus', ({ bool, elementName, findElement }) => {
+ describe('drawer behavior', () => {
beforeEach(() => {
- createComponent({ featureFlags: { ciVariableDrawer: bool } });
+ createComponent();
});
- it(`${elementName} is hidden by default`, () => {
- expect(findElement().exists()).toBe(false);
+ it(`drawer is hidden by default`, () => {
+ expect(findCiVariableDrawer().exists()).toBe(false);
});
- it(`shows ${elementName} when adding a new variable`, async () => {
+ it(`shows drawer when adding a new variable`, async () => {
await findCiVariableTable().vm.$emit('set-selected-variable');
- expect(findElement().exists()).toBe(true);
+ expect(findCiVariableDrawer().exists()).toBe(true);
});
- it(`shows ${elementName} when updating a variable`, async () => {
+ it(`shows drawer when updating a variable`, async () => {
await findCiVariableTable().vm.$emit('set-selected-variable', newVariable);
- expect(findElement().exists()).toBe(true);
+ expect(findCiVariableDrawer().exists()).toBe(true);
});
- it(`hides ${elementName} when closing the form`, async () => {
+ it(`hides drawer when closing the form`, async () => {
await findCiVariableTable().vm.$emit('set-selected-variable');
- expect(findElement().isVisible()).toBe(true);
+ expect(findCiVariableDrawer().isVisible()).toBe(true);
- await findElement().vm.$emit('close-form');
+ await findCiVariableDrawer().vm.$emit('close-form');
- expect(findElement().exists()).toBe(false);
+ expect(findCiVariableDrawer().exists()).toBe(false);
});
- it(`passes down ADD mode to ${elementName} when receiving an empty variable`, async () => {
+ it(`passes down ADD mode to drawer when receiving an empty variable`, async () => {
await findCiVariableTable().vm.$emit('set-selected-variable');
- expect(findElement().props('mode')).toBe(ADD_VARIABLE_ACTION);
+ expect(findCiVariableDrawer().props('mode')).toBe(ADD_VARIABLE_ACTION);
});
- it(`passes down EDIT mode to ${elementName} when receiving a variable`, async () => {
+ it(`passes down EDIT mode to drawer when receiving a variable`, async () => {
await findCiVariableTable().vm.$emit('set-selected-variable', newVariable);
- expect(findElement().props('mode')).toBe(EDIT_VARIABLE_ACTION);
+ expect(findCiVariableDrawer().props('mode')).toBe(EDIT_VARIABLE_ACTION);
});
});
- describe('variable events for modal', () => {
+ describe('variable events', () => {
beforeEach(() => {
createComponent();
});
@@ -153,25 +125,6 @@ describe('Ci variable table', () => {
`('bubbles up the $eventName event', async ({ eventName }) => {
await findCiVariableTable().vm.$emit('set-selected-variable');
- await findCiVariableModal().vm.$emit(eventName, newVariable);
-
- expect(wrapper.emitted(eventName)).toEqual([[newVariable]]);
- });
- });
-
- describe('variable events for drawer', () => {
- beforeEach(() => {
- createComponent({ featureFlags: { ciVariableDrawer: true } });
- });
-
- it.each`
- eventName
- ${'add-variable'}
- ${'update-variable'}
- ${'delete-variable'}
- `('bubbles up the $eventName event', async ({ eventName }) => {
- await findCiVariableTable().vm.$emit('set-selected-variable');
-
await findCiVariableDrawer().vm.$emit(eventName, newVariable);
expect(wrapper.emitted(eventName)).toEqual([[newVariable]]);
@@ -195,7 +148,7 @@ describe('Ci variable table', () => {
});
});
- describe('environment events for modal', () => {
+ describe('environment events', () => {
beforeEach(() => {
createComponent();
});
@@ -203,20 +156,6 @@ describe('Ci variable table', () => {
it('bubbles up the search event', async () => {
await findCiVariableTable().vm.$emit('set-selected-variable');
- await findCiVariableModal().vm.$emit('search-environment-scope', 'staging');
-
- expect(wrapper.emitted('search-environment-scope')).toEqual([['staging']]);
- });
- });
-
- describe('environment events for drawer', () => {
- beforeEach(() => {
- createComponent({ featureFlags: { ciVariableDrawer: true } });
- });
-
- it('bubbles up the search event', async () => {
- await findCiVariableTable().vm.$emit('set-selected-variable');
-
await findCiVariableDrawer().vm.$emit('search-environment-scope', 'staging');
expect(wrapper.emitted('search-environment-scope')).toEqual([['staging']]);
diff --git a/spec/frontend/ci/common/pipelines_table_spec.js b/spec/frontend/ci/common/pipelines_table_spec.js
index 6cf391d72ca..f6d3121109f 100644
--- a/spec/frontend/ci/common/pipelines_table_spec.js
+++ b/spec/frontend/ci/common/pipelines_table_spec.js
@@ -16,7 +16,7 @@ import {
TRACKING_CATEGORIES,
} from '~/ci/constants';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
describe('Pipelines Table', () => {
let wrapper;
@@ -58,7 +58,7 @@ describe('Pipelines Table', () => {
};
const findGlTableLite = () => wrapper.findComponent(GlTableLite);
- const findCiBadgeLink = () => wrapper.findComponent(CiBadgeLink);
+ const findCiIcon = () => wrapper.findComponent(CiIcon);
const findPipelineInfo = () => wrapper.findComponent(PipelineUrl);
const findTriggerer = () => wrapper.findComponent(PipelineTriggerer);
const findLegacyPipelineMiniGraph = () => wrapper.findComponent(LegacyPipelineMiniGraph);
@@ -96,7 +96,7 @@ describe('Pipelines Table', () => {
describe('status cell', () => {
it('should render a status badge', () => {
- expect(findCiBadgeLink().exists()).toBe(true);
+ expect(findCiIcon().exists()).toBe(true);
});
});
@@ -265,7 +265,7 @@ describe('Pipelines Table', () => {
});
it('tracks status badge click', () => {
- findCiBadgeLink().vm.$emit('ciStatusBadgeClick');
+ findCiIcon().vm.$emit('ciStatusBadgeClick');
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_ci_status_badge', {
label: TRACKING_CATEGORIES.table,
diff --git a/spec/frontend/ci/job_details/components/job_header_spec.js b/spec/frontend/ci/job_details/components/job_header_spec.js
index 609369316f5..d12267807ac 100644
--- a/spec/frontend/ci/job_details/components/job_header_spec.js
+++ b/spec/frontend/ci/job_details/components/job_header_spec.js
@@ -1,7 +1,7 @@
import { GlButton, GlAvatarLink, GlTooltip } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import JobHeader from '~/ci/job_details/components/job_header.vue';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
@@ -29,7 +29,7 @@ describe('Header CI Component', () => {
shouldRenderTriggeredLabel: true,
};
- const findCiBadgeLink = () => wrapper.findComponent(CiBadgeLink);
+ const findCiIcon = () => wrapper.findComponent(CiIcon);
const findTimeAgo = () => wrapper.findComponent(TimeagoTooltip);
const findUserLink = () => wrapper.findComponent(GlAvatarLink);
const findSidebarToggleBtn = () => wrapper.findComponent(GlButton);
@@ -57,7 +57,7 @@ describe('Header CI Component', () => {
});
it('should render status badge', () => {
- expect(findCiBadgeLink().exists()).toBe(true);
+ expect(findCiIcon().exists()).toBe(true);
});
it('should render timeago date', () => {
diff --git a/spec/frontend/ci/job_details/components/log/line_header_spec.js b/spec/frontend/ci/job_details/components/log/line_header_spec.js
index 45296e4b6c2..c75f5fa30d5 100644
--- a/spec/frontend/ci/job_details/components/log/line_header_spec.js
+++ b/spec/frontend/ci/job_details/components/log/line_header_spec.js
@@ -1,3 +1,4 @@
+import { GlIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import setWindowLocation from 'helpers/set_window_location_helper';
@@ -30,6 +31,8 @@ describe('Job Log Header Line', () => {
});
};
+ const findIcon = () => wrapper.findComponent(GlIcon);
+
describe('line', () => {
beforeEach(() => {
createComponent();
@@ -48,23 +51,33 @@ describe('Job Log Header Line', () => {
});
});
- describe('when isCloses is true', () => {
+ describe('when isClosed is true', () => {
beforeEach(() => {
createComponent({ ...defaultProps, isClosed: true });
});
it('sets icon name to be chevron-lg-right', () => {
- expect(wrapper.vm.iconName).toEqual('chevron-lg-right');
+ expect(findIcon().props('name')).toEqual('chevron-lg-right');
});
});
- describe('when isCloses is false', () => {
+ describe('when isClosed is false', () => {
beforeEach(() => {
createComponent({ ...defaultProps, isClosed: false });
});
it('sets icon name to be chevron-lg-down', () => {
- expect(wrapper.vm.iconName).toEqual('chevron-lg-down');
+ expect(findIcon().props('name')).toEqual('chevron-lg-down');
+ });
+ });
+
+ describe('when isClosed is not defined', () => {
+ beforeEach(() => {
+ createComponent({ ...defaultProps, isClosed: undefined });
+ });
+
+ it('sets icon name to be chevron-lg-right', () => {
+ expect(findIcon().props('name')).toEqual('chevron-lg-down');
});
});
diff --git a/spec/frontend/ci/job_details/components/log/mock_data.js b/spec/frontend/ci/job_details/components/log/mock_data.js
index 14669872cc1..d9b1354f475 100644
--- a/spec/frontend/ci/job_details/components/log/mock_data.js
+++ b/spec/frontend/ci/job_details/components/log/mock_data.js
@@ -1,67 +1,73 @@
-export const mockJobLog = [
+export const mockJobLines = [
{
- offset: 1000,
- content: [{ text: 'Running with gitlab-runner 12.1.0 (de7731dd)' }],
+ offset: 0,
+ content: [
+ {
+ text: 'Running with gitlab-runner 12.1.0 (de7731dd)',
+ style: 'term-fg-l-cyan term-bold',
+ },
+ ],
},
{
offset: 1001,
content: [{ text: ' on docker-auto-scale-com 8a6210b8' }],
},
+];
+
+export const mockEmptySection = [
{
offset: 1002,
content: [
{
- text: 'Using Docker executor with image dev.gitlab.org3',
+ text: 'Resolving secrets',
+ style: 'term-fg-l-cyan term-bold',
},
],
- section: 'prepare-executor',
+ section: 'resolve-secrets',
section_header: true,
},
{
offset: 1003,
- content: [{ text: 'Docker executor with image registry.gitlab.com ...' }],
- section: 'prepare-executor',
- },
- {
- offset: 1004,
- content: [{ text: 'Starting service ...', style: 'term-fg-l-green' }],
- section: 'prepare-executor',
- },
- {
- offset: 1005,
content: [],
- section: 'prepare-executor',
- section_duration: '00:09',
+ section: 'resolve-secrets',
+ section_footer: true,
+ section_duration: '00:00',
},
+];
+
+export const mockContentSection = [
{
- offset: 1006,
+ offset: 1004,
content: [
{
- text: 'Getting source from Git repository',
+ text: 'Using Docker executor with image dev.gitlab.org3',
},
],
- section: 'get-sources',
+ section: 'prepare-executor',
section_header: true,
},
{
- offset: 1007,
- content: [{ text: 'Fetching changes with git depth set to 20...' }],
- section: 'get-sources',
+ offset: 1005,
+ content: [{ text: 'Docker executor with image registry.gitlab.com ...' }],
+ section: 'prepare-executor',
},
{
- offset: 1008,
- content: [{ text: 'Initialized empty Git repository', style: 'term-fg-l-green' }],
- section: 'get-sources',
+ offset: 1006,
+ content: [{ text: 'Starting service ...', style: 'term-fg-l-green' }],
+ section: 'prepare-executor',
},
{
- offset: 1009,
+ offset: 1007,
content: [],
- section: 'get-sources',
- section_duration: '00:19',
+ section: 'prepare-executor',
+ section_footer: true,
+ section_duration: '00:09',
},
];
-export const mockJobLogLineCount = 8; // `text` entries in mockJobLog
+export const mockJobLog = [...mockJobLines, ...mockEmptySection, ...mockContentSection];
+
+export const mockJobLogLineCount = 6; // `text` entries in mockJobLog
export const originalTrace = [
{
diff --git a/spec/frontend/ci/job_details/components/sidebar/stages_dropdown_spec.js b/spec/frontend/ci/job_details/components/sidebar/stages_dropdown_spec.js
index e007896c81e..54c5a73f757 100644
--- a/spec/frontend/ci/job_details/components/sidebar/stages_dropdown_spec.js
+++ b/spec/frontend/ci/job_details/components/sidebar/stages_dropdown_spec.js
@@ -3,7 +3,7 @@ import { shallowMount } from '@vue/test-utils';
import { Mousetrap } from '~/lib/mousetrap';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import StagesDropdown from '~/ci/job_details/components/sidebar/stages_dropdown.vue';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import * as copyToClipboard from '~/behaviors/copy_to_clipboard';
import {
mockPipelineWithoutRef,
@@ -15,7 +15,7 @@ import {
describe('Stages Dropdown', () => {
let wrapper;
- const findStatus = () => wrapper.findComponent(CiBadgeLink);
+ const findStatus = () => wrapper.findComponent(CiIcon);
const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
const findSelectedStageText = () => findDropdown().props('toggleText');
@@ -47,7 +47,6 @@ describe('Stages Dropdown', () => {
it('renders pipeline status', () => {
expect(findStatus().props('status')).toBe(mockPipelineWithoutMR.details.status);
- expect(findStatus().props('size')).toBe('sm');
});
it('renders dropdown with stages', () => {
diff --git a/spec/frontend/ci/job_details/job_app_spec.js b/spec/frontend/ci/job_details/job_app_spec.js
index ff84b2d0283..2bd0429ef56 100644
--- a/spec/frontend/ci/job_details/job_app_spec.js
+++ b/spec/frontend/ci/job_details/job_app_spec.js
@@ -311,6 +311,8 @@ describe('Job App', () => {
it('should render job log', () => {
expect(findJobLog().exists()).toBe(true);
+
+ expect(findJobLog().props()).toEqual({ searchResults: [] });
});
});
diff --git a/spec/frontend/ci/job_details/store/actions_spec.js b/spec/frontend/ci/job_details/store/actions_spec.js
index 2799bc9578c..849f55ac444 100644
--- a/spec/frontend/ci/job_details/store/actions_spec.js
+++ b/spec/frontend/ci/job_details/store/actions_spec.js
@@ -284,7 +284,7 @@ describe('Job State actions', () => {
});
});
- describe('error', () => {
+ describe('server error', () => {
beforeEach(() => {
mock.onGet(`${TEST_HOST}/endpoint/trace.json`).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
});
@@ -303,6 +303,28 @@ describe('Job State actions', () => {
);
});
});
+
+ describe('unexpected error', () => {
+ beforeEach(() => {
+ mock.onGet(`${TEST_HOST}/endpoint/trace.json`).reply(() => {
+ throw new Error('an error');
+ });
+ });
+
+ it('dispatches requestJobLog and receiveJobLogError', () => {
+ return testAction(
+ fetchJobLog,
+ null,
+ mockedState,
+ [],
+ [
+ {
+ type: 'receiveJobLogError',
+ },
+ ],
+ );
+ });
+ });
});
describe('startPollingJobLog', () => {
diff --git a/spec/frontend/ci/job_details/store/mutations_spec.js b/spec/frontend/ci/job_details/store/mutations_spec.js
index 78b29efed68..601dff47584 100644
--- a/spec/frontend/ci/job_details/store/mutations_spec.js
+++ b/spec/frontend/ci/job_details/store/mutations_spec.js
@@ -1,6 +1,7 @@
import * as types from '~/ci/job_details/store/mutation_types';
import mutations from '~/ci/job_details/store/mutations';
import state from '~/ci/job_details/store/state';
+import * as utils from '~/ci/job_details/store/utils';
describe('Jobs Store Mutations', () => {
let stateCopy;
@@ -87,50 +88,91 @@ describe('Jobs Store Mutations', () => {
});
describe('with new job log', () => {
+ const mockLog = {
+ append: false,
+ size: 511846,
+ complete: true,
+ lines: [
+ {
+ offset: 1,
+ content: [{ text: 'Line content' }],
+ },
+ ],
+ };
+
+ beforeEach(() => {
+ jest.spyOn(utils, 'logLinesParser');
+ });
+
+ afterEach(() => {
+ utils.logLinesParser.mockRestore();
+ });
+
describe('log.lines', () => {
- describe('when append is true', () => {
+ describe('when it is defined', () => {
it('sets the parsed log', () => {
- mutations[types.RECEIVE_JOB_LOG_SUCCESS](stateCopy, {
- append: true,
- size: 511846,
- complete: true,
- lines: [
- {
- offset: 1,
- content: [{ text: 'Running with gitlab-runner 11.12.1 (5a147c92)' }],
- },
- ],
- });
+ mutations[types.RECEIVE_JOB_LOG_SUCCESS](stateCopy, mockLog);
+
+ expect(utils.logLinesParser).toHaveBeenCalledWith(mockLog.lines, [], '');
expect(stateCopy.jobLog).toEqual([
{
offset: 1,
- content: [{ text: 'Running with gitlab-runner 11.12.1 (5a147c92)' }],
+ content: [{ text: 'Line content' }],
lineNumber: 1,
},
]);
});
});
- describe('when it is defined', () => {
+ describe('when it is defined and location.hash is set', () => {
+ beforeEach(() => {
+ window.location.hash = '#L1';
+ });
+
it('sets the parsed log', () => {
- mutations[types.RECEIVE_JOB_LOG_SUCCESS](stateCopy, {
- append: false,
- size: 511846,
- complete: true,
- lines: [
- { offset: 0, content: [{ text: 'Running with gitlab-runner 11.11.1 (5a147c92)' }] },
- ],
- });
+ mutations[types.RECEIVE_JOB_LOG_SUCCESS](stateCopy, mockLog);
+
+ expect(utils.logLinesParser).toHaveBeenCalledWith(mockLog.lines, [], '#L1');
expect(stateCopy.jobLog).toEqual([
{
- offset: 0,
- content: [{ text: 'Running with gitlab-runner 11.11.1 (5a147c92)' }],
+ offset: 1,
+ content: [{ text: 'Line content' }],
lineNumber: 1,
},
]);
});
+
+ describe('when append is true', () => {
+ it('sets the parsed log', () => {
+ stateCopy.jobLog = [
+ {
+ offset: 0,
+ content: [{ text: 'Previous line content' }],
+ lineNumber: 1,
+ },
+ ];
+
+ mutations[types.RECEIVE_JOB_LOG_SUCCESS](stateCopy, {
+ ...mockLog,
+ append: true,
+ });
+
+ expect(stateCopy.jobLog).toEqual([
+ {
+ offset: 0,
+ content: [{ text: 'Previous line content' }],
+ lineNumber: 1,
+ },
+ {
+ offset: 1,
+ content: [{ text: 'Line content' }],
+ lineNumber: 2,
+ },
+ ]);
+ });
+ });
});
describe('when it is null', () => {
diff --git a/spec/frontend/ci/job_details/store/utils_spec.js b/spec/frontend/ci/job_details/store/utils_spec.js
index 394ce0ab737..8fc4eeb0ca8 100644
--- a/spec/frontend/ci/job_details/store/utils_spec.js
+++ b/spec/frontend/ci/job_details/store/utils_spec.js
@@ -195,11 +195,9 @@ describe('Jobs Store Utils', () => {
expect(result[0].lineNumber).toEqual(1);
expect(result[1].lineNumber).toEqual(2);
expect(result[2].line.lineNumber).toEqual(3);
- expect(result[2].lines[0].lineNumber).toEqual(4);
- expect(result[2].lines[1].lineNumber).toEqual(5);
- expect(result[3].line.lineNumber).toEqual(6);
- expect(result[3].lines[0].lineNumber).toEqual(7);
- expect(result[3].lines[1].lineNumber).toEqual(8);
+ expect(result[3].line.lineNumber).toEqual(4);
+ expect(result[3].lines[0].lineNumber).toEqual(5);
+ expect(result[3].lines[1].lineNumber).toEqual(6);
});
});
@@ -215,16 +213,16 @@ describe('Jobs Store Utils', () => {
});
it('creates a lines array property with the content of the collapsible section', () => {
- expect(result[2].lines.length).toEqual(2);
- expect(result[2].lines[0].content).toEqual(mockJobLog[3].content);
- expect(result[2].lines[1].content).toEqual(mockJobLog[4].content);
+ expect(result[3].lines.length).toEqual(2);
+ expect(result[3].lines[0].content).toEqual(mockJobLog[5].content);
+ expect(result[3].lines[1].content).toEqual(mockJobLog[6].content);
});
});
describe('section duration', () => {
it('adds the section information to the header section', () => {
- expect(result[2].line.section_duration).toEqual(mockJobLog[5].section_duration);
- expect(result[3].line.section_duration).toEqual(mockJobLog[9].section_duration);
+ expect(result[2].line.section_duration).toEqual(mockJobLog[3].section_duration);
+ expect(result[3].line.section_duration).toEqual(mockJobLog[7].section_duration);
});
it('does not add section duration as a line', () => {
diff --git a/spec/frontend/ci/jobs_page/components/job_cells/job_cell_spec.js b/spec/frontend/ci/jobs_page/components/job_cells/job_cell_spec.js
index bb44d970bd7..68ff2403b30 100644
--- a/spec/frontend/ci/jobs_page/components/job_cells/job_cell_spec.js
+++ b/spec/frontend/ci/jobs_page/components/job_cells/job_cell_spec.js
@@ -107,11 +107,11 @@ describe('Job Cell', () => {
});
it.each`
- testId | text
- ${'manual-job-badge'} | ${'manual'}
- ${'triggered-job-badge'} | ${'triggered'}
- ${'fail-job-badge'} | ${'allowed to fail'}
- ${'delayed-job-badge'} | ${'delayed'}
+ testId | text
+ ${'manual-job-badge'} | ${'manual'}
+ ${'trigger-token-job-badge'} | ${'trigger token'}
+ ${'fail-job-badge'} | ${'allowed to fail'}
+ ${'delayed-job-badge'} | ${'delayed'}
`('displays the static $text badge', ({ testId, text }) => {
createComponent({
manualJob: true,
diff --git a/spec/frontend/ci/jobs_page/components/jobs_table_spec.js b/spec/frontend/ci/jobs_page/components/jobs_table_spec.js
index d4e0ce92bc2..d14afe7dd3e 100644
--- a/spec/frontend/ci/jobs_page/components/jobs_table_spec.js
+++ b/spec/frontend/ci/jobs_page/components/jobs_table_spec.js
@@ -3,7 +3,7 @@ import { mount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import JobsTable from '~/ci/jobs_page/components/jobs_table.vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { DEFAULT_FIELDS_ADMIN } from '~/ci/admin/jobs_table/constants';
import ProjectCell from '~/ci/admin/jobs_table/components/cells/project_cell.vue';
import RunnerCell from '~/ci/admin/jobs_table/components/cells/runner_cell.vue';
@@ -13,7 +13,7 @@ describe('Jobs Table', () => {
let wrapper;
const findTable = () => wrapper.findComponent(GlTable);
- const findCiBadgeLink = () => wrapper.findComponent(CiBadgeLink);
+ const findCiIcon = () => wrapper.findComponent(CiIcon);
const findTableRows = () => wrapper.findAllByTestId('jobs-table-row');
const findJobStage = () => wrapper.findByTestId('job-stage-name');
const findJobName = () => wrapper.findByTestId('job-name');
@@ -45,7 +45,7 @@ describe('Jobs Table', () => {
});
it('displays job status', () => {
- expect(findCiBadgeLink().exists()).toBe(true);
+ expect(findCiIcon().exists()).toBe(true);
});
it('displays the job stage, id and name', () => {
diff --git a/spec/frontend/ci/pipeline_details/graph/components/graph_component_spec.js b/spec/frontend/ci/pipeline_details/graph/components/graph_component_spec.js
index a98e79c69fe..c3f22749978 100644
--- a/spec/frontend/ci/pipeline_details/graph/components/graph_component_spec.js
+++ b/spec/frontend/ci/pipeline_details/graph/components/graph_component_spec.js
@@ -19,6 +19,10 @@ describe('graph component', () => {
const findLinksLayer = () => wrapper.findComponent(LinksLayer);
const findStageColumns = () => wrapper.findAllComponents(StageColumnComponent);
const findStageNameInJob = () => wrapper.findByTestId('stage-name-in-job');
+ const findPipelineContainer = () => wrapper.findByTestId('pipeline-container');
+ const findRootGraphLayout = () => wrapper.findByTestId('stage-column');
+ const findStageColumnTitle = () => wrapper.findByTestId('stage-column-title');
+ const findJobItem = () => wrapper.findComponent(JobItem);
const defaultProps = {
pipeline: generateResponse(mockPipelineResponse, 'root/fungi-xoxo'),
@@ -42,6 +46,9 @@ describe('graph component', () => {
mountFn = shallowMount,
props = {},
stubOverride = {},
+ glFeatures = {
+ newPipelineGraph: false,
+ },
} = {}) => {
wrapper = mountFn(PipelineGraph, {
propsData: {
@@ -61,6 +68,9 @@ describe('graph component', () => {
'job-group-dropdown': true,
...stubOverride,
},
+ provide: {
+ glFeatures,
+ },
});
};
@@ -112,9 +122,8 @@ describe('graph component', () => {
});
it('dims unrelated jobs', () => {
- const unrelatedJob = wrapper.findComponent(JobItem);
expect(findLinksLayer().emitted().highlightedJobsChange).toHaveLength(1);
- expect(unrelatedJob.classes('gl-opacity-3')).toBe(true);
+ expect(findJobItem().classes('gl-opacity-3')).toBe(true);
});
});
});
@@ -179,4 +188,82 @@ describe('graph component', () => {
expect(findDownstreamColumn().props().linkedPipelines).toHaveLength(1);
});
});
+
+ describe.each`
+ name | value | state
+ ${'disabled'} | ${false} | ${'should not'}
+ ${'enabled'} | ${true} | ${'should'}
+ `('With feature flag newPipelineGraph $name', ({ value, state }) => {
+ beforeEach(() => {
+ createComponent({
+ mountFn: mountExtended,
+ stubOverride: { 'job-item': false, StageColumnComponent },
+ glFeatures: {
+ newPipelineGraph: value,
+ },
+ stubs: {
+ StageColumnComponent,
+ },
+ });
+ });
+
+ it(`${state} add class pipeline-graph-container on wrapper`, () => {
+ expect(findPipelineContainer().classes('pipeline-graph-container')).toBe(value);
+ });
+
+ it(`${state} add class is-stage-view on rootGraphLayout`, () => {
+ expect(findRootGraphLayout().classes('is-stage-view')).toBe(value);
+ });
+
+ it(`${state} add titleClasses on stageColumnTitle`, () => {
+ const titleClasses = [
+ 'gl-font-weight-bold',
+ 'gl-pipeline-job-width',
+ 'gl-text-truncate',
+ 'gl-line-height-36',
+ 'gl-pl-4',
+ 'gl-mb-n2',
+ ];
+ const legacyTitleClasses = [
+ 'gl-font-weight-bold',
+ 'gl-pipeline-job-width',
+ 'gl-text-truncate',
+ 'gl-line-height-36',
+ 'gl-pl-3',
+ ];
+ const checkClasses = value ? titleClasses : legacyTitleClasses;
+
+ expect(findStageColumnTitle().classes()).toEqual(expect.arrayContaining(checkClasses));
+ });
+
+ it(`${state} add jobClasses on findJobItem`, () => {
+ const jobClasses = [
+ 'gl-p-3',
+ 'gl-border-0',
+ 'gl-bg-transparent',
+ 'gl-rounded-base',
+ 'gl-hover-bg-gray-50',
+ 'gl-focus-bg-gray-50',
+ 'gl-hover-text-gray-900',
+ 'gl-focus-text-gray-900',
+ ];
+ const legacyJobClasses = [
+ 'gl-p-3',
+ 'gl-border-gray-100',
+ 'gl-border-solid',
+ 'gl-border-1',
+ 'gl-bg-white',
+ 'gl-rounded-7',
+ 'gl-hover-bg-gray-50',
+ 'gl-focus-bg-gray-50',
+ 'gl-hover-text-gray-900',
+ 'gl-focus-text-gray-900',
+ 'gl-hover-border-gray-200',
+ 'gl-focus-border-gray-200',
+ ];
+ const checkClasses = value ? jobClasses : legacyJobClasses;
+
+ expect(findJobItem().props('cssClassJobName')).toEqual(expect.arrayContaining(checkClasses));
+ });
+ });
});
diff --git a/spec/frontend/ci/pipeline_details/graph/components/job_item_spec.js b/spec/frontend/ci/pipeline_details/graph/components/job_item_spec.js
index de9ee8a16bf..10db7f398fe 100644
--- a/spec/frontend/ci/pipeline_details/graph/components/job_item_spec.js
+++ b/spec/frontend/ci/pipeline_details/graph/components/job_item_spec.js
@@ -5,7 +5,7 @@ import JobItem from '~/ci/pipeline_details/graph/components/job_item.vue';
import axios from '~/lib/utils/axios_utils';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import ActionComponent from '~/ci/common/private/job_action_component.vue';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import {
@@ -31,7 +31,7 @@ describe('pipeline graph job item', () => {
const findActionComponent = () => wrapper.findByTestId('ci-action-button');
const findBadge = () => wrapper.findByTestId('job-bridge-badge');
const findJobLink = () => wrapper.findByTestId('job-with-link');
- const findJobCiBadge = () => wrapper.findComponent(CiBadgeLink);
+ const findJobCiIcon = () => wrapper.findComponent(CiIcon);
const findModal = () => wrapper.findComponent(GlModal);
const clickOnModalPrimaryBtn = () => findModal().vm.$emit('primary');
@@ -60,7 +60,7 @@ describe('pipeline graph job item', () => {
...mocks,
},
stubs: {
- CiBadgeLink,
+ CiIcon,
},
});
};
@@ -86,8 +86,10 @@ describe('pipeline graph job item', () => {
expect(link.attributes('title')).toBe(`${mockJob.name} - ${mockJob.status.label}`);
- expect(findJobCiBadge().exists()).toBe(true);
- expect(findJobCiBadge().find('.ci-status-icon-success').exists()).toBe(true);
+ expect(findJobCiIcon().exists()).toBe(true);
+ expect(findJobCiIcon().find('[data-testid="status_success_borderless-icon"]').exists()).toBe(
+ true,
+ );
expect(wrapper.text()).toBe(mockJob.name);
});
@@ -105,8 +107,10 @@ describe('pipeline graph job item', () => {
});
it('should render status and name', () => {
- expect(findJobCiBadge().exists()).toBe(true);
- expect(findJobCiBadge().find('.ci-status-icon-success').exists()).toBe(true);
+ expect(findJobCiIcon().exists()).toBe(true);
+ expect(findJobCiIcon().find('[data-testid="status_success_borderless-icon"]').exists()).toBe(
+ true,
+ );
expect(findJobLink().exists()).toBe(false);
expect(wrapper.text()).toBe(mockJobWithoutDetails.name);
@@ -117,12 +121,12 @@ describe('pipeline graph job item', () => {
});
});
- describe('CiBadgeLink', () => {
+ describe('CiIcon', () => {
it('should not render a link', () => {
createWrapper();
- expect(findJobCiBadge().exists()).toBe(true);
- expect(findJobCiBadge().props('useLink')).toBe(false);
+ expect(findJobCiIcon().exists()).toBe(true);
+ expect(findJobCiIcon().props('useLink')).toBe(false);
});
});
diff --git a/spec/frontend/ci/pipeline_details/graph/components/job_name_component_spec.js b/spec/frontend/ci/pipeline_details/graph/components/job_name_component_spec.js
index ca201aee648..1da85ad9f78 100644
--- a/spec/frontend/ci/pipeline_details/graph/components/job_name_component_spec.js
+++ b/spec/frontend/ci/pipeline_details/graph/components/job_name_component_spec.js
@@ -25,6 +25,6 @@ describe('job name component', () => {
it('should render an icon with the provided status', () => {
expect(wrapper.findComponent(CiIcon).exists()).toBe(true);
- expect(wrapper.find('.ci-status-icon-success').exists()).toBe(true);
+ expect(wrapper.find('[data-testid="status_success_borderless-icon"]').exists()).toBe(true);
});
});
diff --git a/spec/frontend/ci/pipeline_details/graph/components/linked_pipeline_spec.js b/spec/frontend/ci/pipeline_details/graph/components/linked_pipeline_spec.js
index 5fe8581e81b..72be51575d7 100644
--- a/spec/frontend/ci/pipeline_details/graph/components/linked_pipeline_spec.js
+++ b/spec/frontend/ci/pipeline_details/graph/components/linked_pipeline_spec.js
@@ -93,7 +93,7 @@ describe('Linked pipeline', () => {
});
it('should render the pipeline status icon svg', () => {
- expect(wrapper.find('.ci-status-icon-success svg').exists()).toBe(true);
+ expect(wrapper.findByTestId('status_success_borderless-icon').exists()).toBe(true);
});
it('should have a ci-status child component', () => {
diff --git a/spec/frontend/ci/pipeline_details/header/pipeline_details_header_spec.js b/spec/frontend/ci/pipeline_details/header/pipeline_details_header_spec.js
index 6e13658a773..e8e178ed148 100644
--- a/spec/frontend/ci/pipeline_details/header/pipeline_details_header_spec.js
+++ b/spec/frontend/ci/pipeline_details/header/pipeline_details_header_spec.js
@@ -7,7 +7,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import PipelineDetailsHeader from '~/ci/pipeline_details/header/pipeline_details_header.vue';
import { BUTTON_TOOLTIP_RETRY, BUTTON_TOOLTIP_CANCEL } from '~/ci/constants';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import cancelPipelineMutation from '~/ci/pipeline_details/graphql/mutations/cancel_pipeline.mutation.graphql';
import deletePipelineMutation from '~/ci/pipeline_details/graphql/mutations/delete_pipeline.mutation.graphql';
import retryPipelineMutation from '~/ci/pipeline_details/graphql/mutations/retry_pipeline.mutation.graphql';
@@ -56,7 +56,7 @@ describe('Pipeline details header', () => {
.mockResolvedValue(pipelineDeleteMutationResponseFailed);
const findAlert = () => wrapper.findComponent(GlAlert);
- const findStatus = () => wrapper.findComponent(CiBadgeLink);
+ const findStatus = () => wrapper.findComponent(CiIcon);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findAllBadges = () => wrapper.findAllComponents(GlBadge);
const findDeleteModal = () => wrapper.findComponent(GlModal);
@@ -94,9 +94,11 @@ describe('Pipeline details header', () => {
failureReason: 'pipeline failed',
badges: {
schedule: true,
+ trigger: false,
child: false,
latest: true,
mergeTrainPipeline: false,
+ mergedResultsPipeline: false,
invalid: false,
failed: false,
autoDevops: false,
@@ -178,6 +180,7 @@ describe('Pipeline details header', () => {
expect(findAllBadges()).toHaveLength(2);
expect(wrapper.findByText('latest').exists()).toBe(true);
expect(wrapper.findByText('Scheduled').exists()).toBe(true);
+ expect(wrapper.findByText('trigger token').exists()).toBe(false);
});
it('displays ref text', () => {
@@ -202,6 +205,21 @@ describe('Pipeline details header', () => {
});
});
+ describe('with triggered pipeline', () => {
+ beforeEach(async () => {
+ createComponent(defaultHandlers, {
+ ...defaultProps,
+ badges: { ...defaultProps.badges, trigger: true },
+ });
+
+ await waitForPromises();
+ });
+
+ it('displays triggered badge', () => {
+ expect(wrapper.findByText('trigger token').exists()).toBe(true);
+ });
+ });
+
describe('without pipeline name', () => {
it('displays commit title', async () => {
createComponent(defaultHandlers, { ...defaultProps, name: '' });
diff --git a/spec/frontend/ci/pipeline_editor/components/file-nav/branch_switcher_spec.js b/spec/frontend/ci/pipeline_editor/components/file-nav/branch_switcher_spec.js
index 4057759b9b9..d38226fedb2 100644
--- a/spec/frontend/ci/pipeline_editor/components/file-nav/branch_switcher_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/file-nav/branch_switcher_spec.js
@@ -1,12 +1,6 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
-import {
- GlDropdown,
- GlDropdownItem,
- GlInfiniteScroll,
- GlLoadingIcon,
- GlSearchBoxByType,
-} from '@gitlab/ui';
+import { GlCollapsibleListbox, GlListboxItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -76,17 +70,15 @@ describe('Pipeline editor branch switcher', () => {
totalBranches: mockTotalBranches,
},
apolloProvider: mockApollo,
+ stubs: { GlCollapsibleListbox },
});
return waitForPromises();
};
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
- const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
- const findInfiniteScroll = () => wrapper.findComponent(GlInfiniteScroll);
- const defaultBranchInDropdown = () => findDropdownItems().at(0);
+ const findGlCollapsibleListbox = () => wrapper.findComponent(GlCollapsibleListbox);
+ const findGlListboxItems = () => wrapper.findAllComponents(GlListboxItem);
+ const defaultBranchInDropdown = () => findGlListboxItems().at(0);
const setAvailableBranchesMock = (availableBranches) => {
mockAvailableBranchQuery.mockResolvedValue(availableBranches);
@@ -112,11 +104,7 @@ describe('Pipeline editor branch switcher', () => {
});
it('disables the dropdown', () => {
- expect(findDropdown().props('disabled')).toBe(true);
- });
-
- it('shows loading icon', () => {
- expect(findLoadingIcon().exists()).toBe(true);
+ expect(findGlCollapsibleListbox().props('disabled')).toBe(true);
});
});
@@ -126,29 +114,25 @@ describe('Pipeline editor branch switcher', () => {
await createComponent();
});
- it('does not render the loading icon', () => {
- expect(findLoadingIcon().exists()).toBe(false);
- });
-
it('renders search box', () => {
- expect(findSearchBox().exists()).toBe(true);
+ expect(findGlCollapsibleListbox().props().searchable).toBe(true);
});
it('renders list of branches', () => {
- expect(findDropdown().exists()).toBe(true);
- expect(findDropdownItems()).toHaveLength(mockTotalBranchResults);
+ expect(findGlCollapsibleListbox().exists()).toBe(true);
+ expect(findGlListboxItems()).toHaveLength(mockTotalBranchResults);
});
it('renders current branch with a check mark', () => {
expect(defaultBranchInDropdown().text()).toBe(mockDefaultBranch);
- expect(defaultBranchInDropdown().props('isChecked')).toBe(true);
+ expect(defaultBranchInDropdown().props('isSelected')).toBe(true);
});
it('does not render check mark for other branches', () => {
- const nonDefaultBranch = findDropdownItems().at(1);
+ const nonDefaultBranch = findGlListboxItems().at(1);
expect(nonDefaultBranch.text()).not.toBe(mockDefaultBranch);
- expect(nonDefaultBranch.props('isChecked')).toBe(false);
+ expect(nonDefaultBranch.props('isSelected')).toBe(false);
});
});
@@ -159,7 +143,7 @@ describe('Pipeline editor branch switcher', () => {
});
it('does not render dropdown', () => {
- expect(findDropdown().props('disabled')).toBe(true);
+ expect(findGlCollapsibleListbox().props('disabled')).toBe(true);
});
it('shows an error message', () => {
@@ -175,8 +159,8 @@ describe('Pipeline editor branch switcher', () => {
});
it('updates session history when selecting a different branch', async () => {
- const branch = findDropdownItems().at(1);
- branch.vm.$emit('click');
+ const branch = findGlListboxItems().at(1);
+ findGlCollapsibleListbox().vm.$emit('select', branch.text());
await waitForPromises();
expect(window.history.pushState).toHaveBeenCalled();
@@ -184,7 +168,7 @@ describe('Pipeline editor branch switcher', () => {
});
it('does not update session history when selecting current branch', async () => {
- const branch = findDropdownItems().at(0);
+ const branch = findGlListboxItems().at(0);
branch.vm.$emit('click');
await waitForPromises();
@@ -192,21 +176,21 @@ describe('Pipeline editor branch switcher', () => {
expect(window.history.pushState).not.toHaveBeenCalled();
});
- it('emits the refetchContent event when selecting a different branch', async () => {
- const branch = findDropdownItems().at(1);
+ it('emits the `refetchContent` event when selecting a different branch', async () => {
+ const branch = findGlListboxItems().at(1);
expect(branch.text()).not.toBe(mockDefaultBranch);
expect(wrapper.emitted('refetchContent')).toBeUndefined();
- branch.vm.$emit('click');
+ findGlCollapsibleListbox().vm.$emit('select', branch.text());
await waitForPromises();
expect(wrapper.emitted('refetchContent')).toBeDefined();
expect(wrapper.emitted('refetchContent')).toHaveLength(1);
});
- it('does not emit the refetchContent event when selecting the current branch', async () => {
- const branch = findDropdownItems().at(0);
+ it('does not emit the `refetchContent` event when selecting the current branch', async () => {
+ const branch = findGlListboxItems().at(0);
expect(branch.text()).toBe(mockDefaultBranch);
expect(wrapper.emitted('refetchContent')).toBeUndefined();
@@ -223,11 +207,11 @@ describe('Pipeline editor branch switcher', () => {
await waitForPromises();
});
- it('emits `select-branch` event and does not switch branch', async () => {
+ it('emits `select-branch` event and does not switch branch', () => {
expect(wrapper.emitted('select-branch')).toBeUndefined();
- const branch = findDropdownItems().at(1);
- await branch.vm.$emit('click');
+ const branch = findGlListboxItems().at(1);
+ findGlCollapsibleListbox().vm.$emit('select', branch.text());
expect(wrapper.emitted('select-branch')).toEqual([[branch.text()]]);
expect(wrapper.emitted('refetchContent')).toBeUndefined();
@@ -248,7 +232,7 @@ describe('Pipeline editor branch switcher', () => {
it('shows error message on fetch error', async () => {
mockAvailableBranchQuery.mockResolvedValue(new Error());
- findSearchBox().vm.$emit('input', 'te');
+ findGlCollapsibleListbox().vm.$emit('search', 'te');
await waitForPromises();
testErrorHandling();
@@ -260,7 +244,8 @@ describe('Pipeline editor branch switcher', () => {
});
it('calls query with correct variables', async () => {
- findSearchBox().vm.$emit('input', 'te');
+ findGlCollapsibleListbox().vm.$emit('search', 'te');
+
await waitForPromises();
expect(mockAvailableBranchQuery).toHaveBeenCalledWith({
@@ -272,35 +257,35 @@ describe('Pipeline editor branch switcher', () => {
});
it('fetches new list of branches', async () => {
- expect(findDropdownItems()).toHaveLength(mockTotalBranchResults);
+ expect(findGlListboxItems()).toHaveLength(mockTotalBranchResults);
- findSearchBox().vm.$emit('input', 'te');
+ findGlCollapsibleListbox().vm.$emit('search', 'te');
await waitForPromises();
- expect(findDropdownItems()).toHaveLength(mockTotalSearchResults);
+ expect(findGlListboxItems()).toHaveLength(mockTotalSearchResults);
});
it('does not hide dropdown when search result is empty', async () => {
mockAvailableBranchQuery.mockResolvedValue(mockEmptySearchBranches);
- findSearchBox().vm.$emit('input', 'aaaaa');
+ findGlCollapsibleListbox().vm.$emit('search', 'aaaa');
await waitForPromises();
- expect(findDropdown().exists()).toBe(true);
- expect(findDropdownItems()).toHaveLength(0);
+ expect(findGlCollapsibleListbox().exists()).toBe(true);
+ expect(findGlListboxItems()).toHaveLength(0);
});
});
describe('without a search term', () => {
beforeEach(async () => {
mockAvailableBranchQuery.mockResolvedValue(mockSearchBranches);
- findSearchBox().vm.$emit('input', 'te');
+ findGlCollapsibleListbox().vm.$emit('search', 'te');
await waitForPromises();
mockAvailableBranchQuery.mockResolvedValue(generateMockProjectBranches());
});
it('calls query with correct variables', async () => {
- findSearchBox().vm.$emit('input', '');
+ findGlCollapsibleListbox().vm.$emit('search', '');
await waitForPromises();
expect(mockAvailableBranchQuery).toHaveBeenCalledWith({
@@ -312,71 +297,34 @@ describe('Pipeline editor branch switcher', () => {
});
it('fetches new list of branches', async () => {
- expect(findDropdownItems()).toHaveLength(mockTotalSearchResults);
+ expect(findGlListboxItems()).toHaveLength(mockTotalSearchResults);
- findSearchBox().vm.$emit('input', '');
+ findGlCollapsibleListbox().vm.$emit('search', '');
await waitForPromises();
- expect(findDropdownItems()).toHaveLength(mockTotalBranchResults);
+ expect(findGlListboxItems()).toHaveLength(mockTotalBranchResults);
});
});
});
describe('when scrolling to the bottom of the list', () => {
beforeEach(async () => {
- setAvailableBranchesMock(generateMockProjectBranches());
- await createComponent();
+ createComponent();
+ await waitForPromises();
});
afterEach(() => {
mockAvailableBranchQuery.mockClear();
});
- describe('when search term is empty', () => {
- it('fetches more branches', async () => {
- expect(mockAvailableBranchQuery).toHaveBeenCalledTimes(1);
-
- setAvailableBranchesMock(generateMockProjectBranches('new-'));
- findInfiniteScroll().vm.$emit('bottomReached');
- await waitForPromises();
-
- expect(mockAvailableBranchQuery).toHaveBeenCalledTimes(2);
- });
-
- it('calls the query with the correct variables', async () => {
- setAvailableBranchesMock(generateMockProjectBranches('new-'));
- findInfiniteScroll().vm.$emit('bottomReached');
- await waitForPromises();
-
- expect(mockAvailableBranchQuery).toHaveBeenCalledWith({
- limit: mockBranchPaginationLimit,
- offset: mockBranchPaginationLimit, // offset changed
- projectFullPath: mockProjectFullPath,
- searchPattern: '*',
- });
- });
-
- it('shows error message on fetch error', async () => {
- mockAvailableBranchQuery.mockResolvedValue(new Error());
-
- findInfiniteScroll().vm.$emit('bottomReached');
- await waitForPromises();
-
- testErrorHandling();
- });
- });
-
describe('when search term exists', () => {
it('does not fetch more branches', async () => {
- findSearchBox().vm.$emit('input', 'te');
+ findGlCollapsibleListbox().vm.$emit('search', 'new');
await waitForPromises();
expect(mockAvailableBranchQuery).toHaveBeenCalledTimes(2);
mockAvailableBranchQuery.mockClear();
- findInfiniteScroll().vm.$emit('bottomReached');
- await waitForPromises();
-
expect(mockAvailableBranchQuery).not.toHaveBeenCalled();
});
});
diff --git a/spec/frontend/ci/pipeline_mini_graph/legacy_pipeline_stage_spec.js b/spec/frontend/ci/pipeline_mini_graph/legacy_pipeline_stage_spec.js
index 4b357a9fc7c..87df7676bf1 100644
--- a/spec/frontend/ci/pipeline_mini_graph/legacy_pipeline_stage_spec.js
+++ b/spec/frontend/ci/pipeline_mini_graph/legacy_pipeline_stage_spec.js
@@ -2,7 +2,7 @@ import { GlDropdown } from '@gitlab/ui';
import { nextTick } from 'vue';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import LegacyPipelineStage from '~/ci/pipeline_mini_graph/legacy_pipeline_stage.vue';
@@ -52,7 +52,7 @@ describe('Pipelines stage component', () => {
});
const findCiActionBtn = () => wrapper.find('.js-ci-action');
- const findCiIcon = () => wrapper.findComponent(CiBadgeLink);
+ const findCiIcon = () => wrapper.findComponent(CiIcon);
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findDropdownToggle = () => wrapper.find('button.dropdown-toggle');
const findDropdownMenu = () =>
diff --git a/spec/frontend/ci/pipeline_mini_graph/linked_pipelines_mini_list_spec.js b/spec/frontend/ci/pipeline_mini_graph/linked_pipelines_mini_list_spec.js
index 3c9d235bfcc..55ce3c79039 100644
--- a/spec/frontend/ci/pipeline_mini_graph/linked_pipelines_mini_list_spec.js
+++ b/spec/frontend/ci/pipeline_mini_graph/linked_pipelines_mini_list_spec.js
@@ -51,7 +51,7 @@ describe('Linked pipeline mini list', () => {
});
it('should render the correct ci status icon', () => {
- expect(findCiIcon().classes('ci-status-icon-running')).toBe(true);
+ expect(wrapper.find('[data-testid="status_running_borderless-icon"]').exists()).toBe(true);
});
it('should have an activated tooltip', () => {
@@ -95,7 +95,7 @@ describe('Linked pipeline mini list', () => {
});
it('should render the correct ci status icon', () => {
- expect(findCiIcon().classes('ci-status-icon-running')).toBe(true);
+ expect(wrapper.find('[data-testid="status_running_borderless-icon"]').exists()).toBe(true);
});
it('should have an activated tooltip', () => {
diff --git a/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline_spec.js b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline_spec.js
index ae069145292..b79e7c6e251 100644
--- a/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline_spec.js
@@ -1,5 +1,5 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import PipelineScheduleLastPipeline from '~/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue';
import { mockPipelineScheduleNodes } from '../../../mock_data';
@@ -18,16 +18,14 @@ describe('Pipeline schedule last pipeline', () => {
});
};
- const findCIBadgeLink = () => wrapper.findComponent(CiBadgeLink);
+ const findCiIcon = () => wrapper.findComponent(CiIcon);
const findStatusText = () => wrapper.findByTestId('pipeline-schedule-status-text');
it('displays pipeline status', () => {
createComponent();
- expect(findCIBadgeLink().exists()).toBe(true);
- expect(findCIBadgeLink().props('status')).toBe(
- defaultProps.schedule.lastPipeline.detailedStatus,
- );
+ expect(findCiIcon().exists()).toBe(true);
+ expect(findCiIcon().props('status')).toBe(defaultProps.schedule.lastPipeline.detailedStatus);
expect(findStatusText().exists()).toBe(false);
});
@@ -35,6 +33,6 @@ describe('Pipeline schedule last pipeline', () => {
createComponent({ schedule: mockPipelineScheduleNodes[0] });
expect(findStatusText().text()).toBe('None');
- expect(findCIBadgeLink().exists()).toBe(false);
+ expect(findCiIcon().exists()).toBe(false);
});
});
diff --git a/spec/frontend/ci/pipelines_page/components/empty_state/ios_templates_spec.js b/spec/frontend/ci/pipelines_page/components/empty_state/ios_templates_spec.js
deleted file mode 100644
index 8620d41886e..00000000000
--- a/spec/frontend/ci/pipelines_page/components/empty_state/ios_templates_spec.js
+++ /dev/null
@@ -1,133 +0,0 @@
-import '~/commons';
-import { nextTick } from 'vue';
-import { GlPopover, GlButton } from '@gitlab/ui';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue';
-import IosTemplates from '~/ci/pipelines_page/components/empty_state/ios_templates.vue';
-import CiTemplates from '~/ci/pipelines_page/components/empty_state/ci_templates.vue';
-
-const pipelineEditorPath = '/-/ci/editor';
-const registrationToken = 'SECRET_TOKEN';
-const iOSTemplateName = 'iOS-Fastlane';
-
-describe('iOS Templates', () => {
- let wrapper;
-
- const createWrapper = (providedPropsData = {}) => {
- return shallowMountExtended(IosTemplates, {
- provide: {
- pipelineEditorPath,
- iosRunnersAvailable: true,
- ...providedPropsData,
- },
- propsData: {
- registrationToken,
- },
- stubs: {
- GlButton,
- },
- });
- };
-
- const findIosTemplate = () => wrapper.findComponent(CiTemplates);
- const findRunnerInstructionsModal = () => wrapper.findComponent(RunnerInstructionsModal);
- const findRunnerInstructionsPopover = () => wrapper.findComponent(GlPopover);
- const findRunnerSetupTodoEmoji = () => wrapper.findByTestId('runner-setup-marked-todo');
- const findRunnerSetupCompletedEmoji = () => wrapper.findByTestId('runner-setup-marked-completed');
- const findSetupRunnerLink = () => wrapper.findByText('Set up a runner');
- const configurePipelineLink = () => wrapper.findByTestId('configure-pipeline-link');
-
- describe('when ios runners are not available', () => {
- beforeEach(() => {
- wrapper = createWrapper({ iosRunnersAvailable: false });
- });
-
- describe('the runner setup section', () => {
- it('marks the section as todo', () => {
- expect(findRunnerSetupTodoEmoji().isVisible()).toBe(true);
- expect(findRunnerSetupCompletedEmoji().isVisible()).toBe(false);
- });
-
- it('renders the setup runner link', () => {
- expect(findSetupRunnerLink().exists()).toBe(true);
- });
-
- it('renders the runner instructions modal with a popover once clicked', async () => {
- findSetupRunnerLink().element.parentElement.click();
-
- await nextTick();
-
- expect(findRunnerInstructionsModal().exists()).toBe(true);
- expect(findRunnerInstructionsModal().props('registrationToken')).toBe(registrationToken);
- expect(findRunnerInstructionsModal().props('defaultPlatformName')).toBe('osx');
-
- findRunnerInstructionsModal().vm.$emit('shown');
-
- await nextTick();
-
- expect(findRunnerInstructionsPopover().exists()).toBe(true);
- });
- });
-
- describe('the configure pipeline section', () => {
- it('has a disabled link button', () => {
- expect(configurePipelineLink().props('disabled')).toBe(true);
- });
- });
-
- describe('the ios-Fastlane template', () => {
- it('renders the template', () => {
- expect(findIosTemplate().props('filterTemplates')).toStrictEqual([iOSTemplateName]);
- });
-
- it('has a disabled link button', () => {
- expect(findIosTemplate().props('disabled')).toBe(true);
- });
- });
- });
-
- describe('when ios runners are available', () => {
- beforeEach(() => {
- wrapper = createWrapper();
- });
-
- describe('the runner setup section', () => {
- it('marks the section as completed', () => {
- expect(findRunnerSetupTodoEmoji().isVisible()).toBe(false);
- expect(findRunnerSetupCompletedEmoji().isVisible()).toBe(true);
- });
-
- it('does not render the setup runner link', () => {
- expect(findSetupRunnerLink().exists()).toBe(false);
- });
- });
-
- describe('the configure pipeline section', () => {
- it('has an enabled link button', () => {
- expect(configurePipelineLink().props('disabled')).toBe(false);
- });
-
- it('links to the pipeline editor with the right template', () => {
- expect(configurePipelineLink().attributes('href')).toBe(
- `${pipelineEditorPath}?template=${iOSTemplateName}`,
- );
- });
- });
-
- describe('the ios-Fastlane template', () => {
- it('renders the template', () => {
- expect(findIosTemplate().props('filterTemplates')).toStrictEqual([iOSTemplateName]);
- });
-
- it('has an enabled link button', () => {
- expect(findIosTemplate().props('disabled')).toBe(false);
- });
-
- it('links to the pipeline editor with the right template', () => {
- expect(configurePipelineLink().attributes('href')).toBe(
- `${pipelineEditorPath}?template=${iOSTemplateName}`,
- );
- });
- });
- });
-});
diff --git a/spec/frontend/ci/pipelines_page/components/empty_state/no_ci_empty_state_spec.js b/spec/frontend/ci/pipelines_page/components/empty_state/no_ci_empty_state_spec.js
index 0c42723f753..ea47edb6842 100644
--- a/spec/frontend/ci/pipelines_page/components/empty_state/no_ci_empty_state_spec.js
+++ b/spec/frontend/ci/pipelines_page/components/empty_state/no_ci_empty_state_spec.js
@@ -1,11 +1,8 @@
import '~/commons';
import { shallowMount } from '@vue/test-utils';
import { GlEmptyState } from '@gitlab/ui';
-import { stubExperiments } from 'helpers/experimentation_helper';
import EmptyState from '~/ci/pipelines_page/components/empty_state/no_ci_empty_state.vue';
-import GitlabExperiment from '~/experimentation/components/gitlab_experiment.vue';
import PipelinesCiTemplates from '~/ci/pipelines_page/components/empty_state/pipelines_ci_templates.vue';
-import IosTemplates from '~/ci/pipelines_page/components/empty_state/ios_templates.vue';
describe('Pipelines Empty State', () => {
let wrapper;
@@ -13,7 +10,6 @@ describe('Pipelines Empty State', () => {
const findIllustration = () => wrapper.find('img');
const findButton = () => wrapper.find('a');
const pipelinesCiTemplates = () => wrapper.findComponent(PipelinesCiTemplates);
- const iosTemplates = () => wrapper.findComponent(IosTemplates);
const createWrapper = (props = {}) => {
wrapper = shallowMount(EmptyState, {
@@ -30,40 +26,17 @@ describe('Pipelines Empty State', () => {
},
stubs: {
GlEmptyState,
- GitlabExperiment,
},
});
};
describe('when user can configure CI', () => {
- describe('when the ios_specific_templates experiment is active', () => {
- beforeEach(() => {
- stubExperiments({ ios_specific_templates: 'candidate' });
- createWrapper();
- });
-
- it('should render the iOS templates', () => {
- expect(iosTemplates().exists()).toBe(true);
- });
-
- it('should not render the CI/CD templates', () => {
- expect(pipelinesCiTemplates().exists()).toBe(false);
- });
+ beforeEach(() => {
+ createWrapper();
});
- describe('when the ios_specific_templates experiment is inactive', () => {
- beforeEach(() => {
- stubExperiments({ ios_specific_templates: 'control' });
- createWrapper();
- });
-
- it('should render the CI/CD templates', () => {
- expect(pipelinesCiTemplates().exists()).toBe(true);
- });
-
- it('should not render the iOS templates', () => {
- expect(iosTemplates().exists()).toBe(false);
- });
+ it('should render the CI/CD templates', () => {
+ expect(pipelinesCiTemplates().exists()).toBe(true);
});
});
diff --git a/spec/frontend/ci/pipelines_page/components/pipeline_labels_spec.js b/spec/frontend/ci/pipelines_page/components/pipeline_labels_spec.js
index 6b0d5b18f7d..a660994ac8b 100644
--- a/spec/frontend/ci/pipelines_page/components/pipeline_labels_spec.js
+++ b/spec/frontend/ci/pipelines_page/components/pipeline_labels_spec.js
@@ -2,6 +2,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { trimText } from 'helpers/text_helper';
import PipelineLabelsComponent from '~/ci/pipelines_page/components/pipeline_labels.vue';
import { mockPipeline } from 'jest/ci/pipeline_details/mock_data';
+import { SCHEDULE_ORIGIN, API_ORIGIN } from '~/ci/pipelines_page/constants';
const projectPath = 'test/test';
@@ -9,6 +10,7 @@ describe('Pipeline label component', () => {
let wrapper;
const findScheduledTag = () => wrapper.findByTestId('pipeline-url-scheduled');
+ const findTriggeredTag = () => wrapper.findByTestId('pipeline-url-triggered');
const findLatestTag = () => wrapper.findByTestId('pipeline-url-latest');
const findYamlTag = () => wrapper.findByTestId('pipeline-url-yaml');
const findStuckTag = () => wrapper.findByTestId('pipeline-url-stuck');
@@ -19,6 +21,7 @@ describe('Pipeline label component', () => {
const findFailureTag = () => wrapper.findByTestId('pipeline-url-failure');
const findForkTag = () => wrapper.findByTestId('pipeline-url-fork');
const findTrainTag = () => wrapper.findByTestId('pipeline-url-train');
+ const findApiTag = () => wrapper.findByTestId('pipeline-api-badge');
const defaultProps = mockPipeline(projectPath);
@@ -41,6 +44,7 @@ describe('Pipeline label component', () => {
expect(findAutoDevopsTag().exists()).toBe(false);
expect(findFailureTag().exists()).toBe(false);
expect(findScheduledTag().exists()).toBe(false);
+ expect(findTriggeredTag().exists()).toBe(false);
expect(findForkTag().exists()).toBe(false);
expect(findTrainTag().exists()).toBe(false);
expect(findMergedResultsTag().exists()).toBe(false);
@@ -121,14 +125,28 @@ describe('Pipeline label component', () => {
it('should render scheduled badge when pipeline was triggered by a schedule', () => {
const scheduledPipeline = defaultProps.pipeline;
- scheduledPipeline.source = 'schedule';
+ scheduledPipeline.source = SCHEDULE_ORIGIN;
createComponent({
...scheduledPipeline,
});
expect(findScheduledTag().exists()).toBe(true);
- expect(findScheduledTag().text()).toContain('Scheduled');
+ expect(findScheduledTag().text()).toContain('scheduled');
+ });
+
+ it('should render triggered badge when pipeline was triggered by a trigger', () => {
+ const triggeredPipeline = {
+ ...defaultProps.pipeline,
+ source: 'trigger',
+ };
+
+ createComponent({
+ pipeline: triggeredPipeline,
+ });
+
+ expect(findTriggeredTag().exists()).toBe(true);
+ expect(findTriggeredTag().text()).toBe('trigger token');
});
it('should render the fork badge when the pipeline was run in a fork', () => {
@@ -201,4 +219,26 @@ describe('Pipeline label component', () => {
expect(findMergedResultsTag().exists()).toBe(false);
});
+
+ it.each`
+ display | source
+ ${true} | ${API_ORIGIN}
+ ${false} | ${SCHEDULE_ORIGIN}
+ `(
+ 'should display the api badge: $display, when the pipeline has a source of $source',
+ ({ display, source }) => {
+ const apiPipeline = defaultProps.pipeline;
+ apiPipeline.source = source;
+
+ createComponent({
+ ...apiPipeline,
+ });
+
+ if (display) {
+ expect(findApiTag().text()).toBe(API_ORIGIN);
+ } else {
+ expect(findApiTag().exists()).toBe(false);
+ }
+ },
+ );
});
diff --git a/spec/frontend/ci/pipelines_page/components/pipeline_triggerer_spec.js b/spec/frontend/ci/pipelines_page/components/pipeline_triggerer_spec.js
index cb04171f031..a4780cddc3c 100644
--- a/spec/frontend/ci/pipelines_page/components/pipeline_triggerer_spec.js
+++ b/spec/frontend/ci/pipelines_page/components/pipeline_triggerer_spec.js
@@ -29,7 +29,6 @@ describe('Pipelines Triggerer', () => {
const findAvatarLink = () => wrapper.findComponent(GlAvatarLink);
const findAvatar = () => wrapper.findComponent(GlAvatar);
- const findTriggerer = () => wrapper.findByText('API');
describe('when user was a triggerer', () => {
beforeEach(() => {
@@ -42,7 +41,6 @@ describe('Pipelines Triggerer', () => {
it('should render only user avatar', () => {
expect(findAvatarLink().exists()).toBe(true);
- expect(findTriggerer().exists()).toBe(false);
});
it('should set correct props on avatar link component', () => {
@@ -62,15 +60,4 @@ describe('Pipelines Triggerer', () => {
expect(findAvatar().attributes().src).toBe(mockData.pipeline.user.avatar_url);
});
});
-
- describe('when API was a triggerer', () => {
- beforeEach(() => {
- createComponent({ pipeline: {} });
- });
-
- it('should render label only', () => {
- expect(findAvatarLink().exists()).toBe(false);
- expect(findTriggerer().exists()).toBe(true);
- });
- });
});
diff --git a/spec/frontend/ci/pipelines_page/pipelines_spec.js b/spec/frontend/ci/pipelines_page/pipelines_spec.js
index fd95f98e7f8..97192058ff6 100644
--- a/spec/frontend/ci/pipelines_page/pipelines_spec.js
+++ b/spec/frontend/ci/pipelines_page/pipelines_spec.js
@@ -7,11 +7,11 @@ import {
GlPagination,
GlCollapsibleListbox,
} from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { chunk } from 'lodash';
-import { nextTick } from 'vue';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
import mockPipelinesResponse from 'test_fixtures/pipelines/pipelines.json';
import setWindowLocation from 'helpers/set_window_location_helper';
import { TEST_HOST } from 'helpers/test_constants';
@@ -19,6 +19,7 @@ import { mockTracking } from 'helpers/tracking_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createMockApollo from 'helpers/mock_apollo_helper';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import Api from '~/api';
import { createAlert, VARIANT_WARNING } from '~/alert';
import setSortPreferenceMutation from '~/issues/list/queries/set_sort_preference.mutation.graphql';
@@ -28,7 +29,12 @@ import NavigationControls from '~/ci/pipelines_page/components/nav_controls.vue'
import PipelinesComponent from '~/ci/pipelines_page/pipelines.vue';
import PipelinesCiTemplates from '~/ci/pipelines_page/components/empty_state/pipelines_ci_templates.vue';
import PipelinesTableComponent from '~/ci/common/pipelines_table.vue';
-import { PIPELINE_IID_KEY, RAW_TEXT_WARNING, TRACKING_CATEGORIES } from '~/ci/constants';
+import {
+ PIPELINE_ID_KEY,
+ PIPELINE_IID_KEY,
+ RAW_TEXT_WARNING,
+ TRACKING_CATEGORIES,
+} from '~/ci/constants';
import Store from '~/ci/pipeline_details/stores/pipelines_store';
import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
@@ -36,11 +42,12 @@ import {
setIdTypePreferenceMutationResponse,
setIdTypePreferenceMutationResponseWithErrors,
} from 'jest/issues/list/mock_data';
-
import { stageReply } from 'jest/ci/pipeline_mini_graph/mock_data';
import { users, mockSearch, branches } from '../pipeline_details/mock_data';
-jest.mock('@sentry/browser');
+Vue.use(VueApollo);
+
+jest.mock('~/sentry/sentry_browser_wrapper');
jest.mock('~/alert');
const mockProjectPath = 'twitter/flight';
@@ -372,6 +379,8 @@ describe('Pipelines', () => {
beforeEach(() => {
gon.current_user_id = 1;
+
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
});
it('should change the text to Show Pipeline IID', async () => {
@@ -384,6 +393,25 @@ describe('Pipelines', () => {
expect(findPipelineUrlLinks().at(0).text()).toBe(`#${mockFilteredPipeline.iid}`);
});
+ it('tracks the iid usage of the ID/IID dropdown', async () => {
+ findPipelineKeyCollapsibleBox().vm.$emit('select', PIPELINE_IID_KEY);
+
+ await waitForPromises();
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'pipelines_display_options', {
+ label: TRACKING_CATEGORIES.listbox,
+ property: 'iid',
+ });
+ });
+
+ it('does not track the id usage of the ID/IID dropdown', async () => {
+ findPipelineKeyCollapsibleBox().vm.$emit('select', PIPELINE_ID_KEY);
+
+ await waitForPromises();
+
+ expect(trackingSpy).not.toHaveBeenCalled();
+ });
+
it('calls mutation to save idType preference', () => {
mutationMock = jest.fn().mockResolvedValue(setIdTypePreferenceMutationResponse);
createComponent();
diff --git a/spec/frontend/ci/runner/admin_new_runner_app/admin_new_runner_app_spec.js b/spec/frontend/ci/runner/admin_new_runner_app/admin_new_runner_app_spec.js
index 75bca68b888..03958381d75 100644
--- a/spec/frontend/ci/runner/admin_new_runner_app/admin_new_runner_app_spec.js
+++ b/spec/frontend/ci/runner/admin_new_runner_app/admin_new_runner_app_spec.js
@@ -6,7 +6,6 @@ import { createAlert, VARIANT_SUCCESS } from '~/alert';
import AdminNewRunnerApp from '~/ci/runner/admin_new_runner/admin_new_runner_app.vue';
import RegistrationCompatibilityAlert from '~/ci/runner/components/registration/registration_compatibility_alert.vue';
-import RegistrationFeedbackBanner from '~/ci/runner/components/registration/registration_feedback_banner.vue';
import { saveAlertToLocalStorage } from '~/ci/runner/local_storage_alert/save_alert_to_local_storage';
import RunnerPlatformsRadioGroup from '~/ci/runner/components/runner_platforms_radio_group.vue';
import {
@@ -32,7 +31,6 @@ describe('AdminNewRunnerApp', () => {
let wrapper;
const findRunnerPlatformsRadioGroup = () => wrapper.findComponent(RunnerPlatformsRadioGroup);
- const findRegistrationFeedbackBanner = () => wrapper.findComponent(RegistrationFeedbackBanner);
const findRegistrationCompatibilityAlert = () =>
wrapper.findComponent(RegistrationCompatibilityAlert);
const findRunnerCreateForm = () => wrapper.findComponent(RunnerCreateForm);
@@ -49,10 +47,6 @@ describe('AdminNewRunnerApp', () => {
createComponent();
});
- it('shows a registration feedback banner', () => {
- expect(findRegistrationFeedbackBanner().exists()).toBe(true);
- });
-
it('shows a registration compatibility alert', () => {
expect(findRegistrationCompatibilityAlert().props('alertKey')).toBe(INSTANCE_TYPE);
});
diff --git a/spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js b/spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js
index bc28147db27..4f5f9c43cb4 100644
--- a/spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js
+++ b/spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js
@@ -43,6 +43,7 @@ import {
PARAM_KEY_PAUSED,
PARAM_KEY_STATUS,
PARAM_KEY_TAG,
+ PARAM_KEY_VERSION,
STATUS_ONLINE,
DEFAULT_MEMBERSHIP,
RUNNER_PAGE_SIZE,
@@ -255,6 +256,10 @@ describe('AdminRunnersApp', () => {
options: expect.any(Array),
}),
expect.objectContaining({
+ type: PARAM_KEY_VERSION,
+ title: 'Version starts with',
+ }),
+ expect.objectContaining({
type: PARAM_KEY_TAG,
recentSuggestionsStorageKey: `${ADMIN_FILTERED_SEARCH_NAMESPACE}-recent-tags`,
}),
diff --git a/spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js b/spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js
index bc77b7b89dd..27fb288c462 100644
--- a/spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js
+++ b/spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js
@@ -1,21 +1,15 @@
import { GlSprintf } from '@gitlab/ui';
-import { __, sprintf } from '~/locale';
+import { __ } from '~/locale';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import RunnerSummaryCell from '~/ci/runner/components/cells/runner_summary_cell.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
+import RunnerCreatedAt from '~/ci/runner/components/runner_created_at.vue';
import RunnerManagersBadge from '~/ci/runner/components/runner_managers_badge.vue';
import RunnerTags from '~/ci/runner/components/runner_tags.vue';
import RunnerSummaryField from '~/ci/runner/components/cells/runner_summary_field.vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import {
- INSTANCE_TYPE,
- I18N_INSTANCE_TYPE,
- PROJECT_TYPE,
- I18N_CREATED_AT_LABEL,
- I18N_CREATED_AT_BY_LABEL,
-} from '~/ci/runner/constants';
+import { INSTANCE_TYPE, I18N_INSTANCE_TYPE, PROJECT_TYPE } from '~/ci/runner/constants';
import { allRunnersWithCreatorData } from '../../mock_data';
@@ -182,45 +176,11 @@ describe('RunnerTypeCell', () => {
expect(findRunnerSummaryField('pipeline').text()).toContain('1,000+');
});
- describe('Displays creation info', () => {
- const findCreatedTime = () => findRunnerSummaryField('calendar').findComponent(TimeAgo);
-
- it('Displays created at ...', () => {
- createComponent({
- runner: { createdBy: null },
- });
-
- expect(findRunnerSummaryField('calendar').text()).toMatchInterpolatedText(
- sprintf(I18N_CREATED_AT_LABEL, {
- timeAgo: findCreatedTime().text(),
- }),
- );
- expect(findCreatedTime().props('time')).toBe(mockRunner.createdAt);
- });
-
- it('Displays created at ... by ...', () => {
- createComponent({ mountFn: mountExtended });
-
- expect(findRunnerSummaryField('calendar').text()).toMatchInterpolatedText(
- sprintf(I18N_CREATED_AT_BY_LABEL, {
- timeAgo: findCreatedTime().text(),
- avatar: mockRunner.createdBy.username,
- }),
- );
-
- expect(findCreatedTime().props('time')).toBe(mockRunner.createdAt);
- });
-
- it('Displays creator avatar', () => {
- const { name, avatarUrl, webUrl, username } = mockRunner.createdBy;
+ it('Displays creation info', () => {
+ createComponent();
- expect(wrapper.findComponent(UserAvatarLink).props()).toMatchObject({
- imgAlt: expect.stringContaining(name),
- imgSrc: avatarUrl,
- linkHref: webUrl,
- tooltipText: username,
- });
- });
+ const createdAt = findRunnerSummaryField('calendar').findComponent(RunnerCreatedAt);
+ expect(createdAt.props('runner')).toEqual(mockRunner);
});
it('Displays tag list', () => {
diff --git a/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js b/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js
index 3fb845b186a..f8ef0bdad51 100644
--- a/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js
+++ b/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js
@@ -178,7 +178,7 @@ describe('RegistrationDropdown', () => {
mountExtended,
);
- expect(findRegistrationTokenInput().element.type).toBe('password');
+ expect(findRegistrationTokenInput().classes()).toContain('input-copy-show-disc');
});
});
diff --git a/spec/frontend/ci/runner/components/registration/registration_feedback_banner_spec.js b/spec/frontend/ci/runner/components/registration/registration_feedback_banner_spec.js
deleted file mode 100644
index fa6b7ad7c63..00000000000
--- a/spec/frontend/ci/runner/components/registration/registration_feedback_banner_spec.js
+++ /dev/null
@@ -1,52 +0,0 @@
-import { GlBanner } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import RegistrationFeedbackBanner from '~/ci/runner/components/registration/registration_feedback_banner.vue';
-import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
-import { makeMockUserCalloutDismisser } from 'helpers/mock_user_callout_dismisser';
-
-describe('Runner registration feeback banner', () => {
- let wrapper;
- let userCalloutDismissSpy;
-
- const findUserCalloutDismisser = () => wrapper.findComponent(UserCalloutDismisser);
- const findBanner = () => wrapper.findComponent(GlBanner);
-
- const createComponent = ({ shouldShowCallout = true } = {}) => {
- userCalloutDismissSpy = jest.fn();
-
- wrapper = shallowMount(RegistrationFeedbackBanner, {
- stubs: {
- UserCalloutDismisser: makeMockUserCalloutDismisser({
- dismiss: userCalloutDismissSpy,
- shouldShowCallout,
- }),
- },
- });
- };
-
- it('banner is shown', () => {
- createComponent();
-
- expect(findBanner().exists()).toBe(true);
- });
-
- it('dismisses the callout when closed', () => {
- createComponent();
-
- findBanner().vm.$emit('close');
-
- expect(userCalloutDismissSpy).toHaveBeenCalled();
- });
-
- it('sets feature name to create_runner_workflow_banner', () => {
- createComponent();
-
- expect(findUserCalloutDismisser().props('featureName')).toBe('create_runner_workflow_banner');
- });
-
- it('is not displayed once it has been dismissed', () => {
- createComponent({ shouldShowCallout: false });
-
- expect(findBanner().exists()).toBe(false);
- });
-});
diff --git a/spec/frontend/ci/runner/components/registration/registration_token_spec.js b/spec/frontend/ci/runner/components/registration/registration_token_spec.js
index eccfe43b47f..af9dbe89eb6 100644
--- a/spec/frontend/ci/runner/components/registration/registration_token_spec.js
+++ b/spec/frontend/ci/runner/components/registration/registration_token_spec.js
@@ -55,7 +55,7 @@ describe('RegistrationToken', () => {
mountFn: mountExtended,
});
- expect(wrapper.find('input').element.type).toBe('password');
+ expect(wrapper.find('input').classes()).toContain('input-copy-show-disc');
});
describe('When the copy to clipboard button is clicked', () => {
diff --git a/spec/frontend/ci/runner/components/runner_created_at_spec.js b/spec/frontend/ci/runner/components/runner_created_at_spec.js
new file mode 100644
index 00000000000..19d1e55c72d
--- /dev/null
+++ b/spec/frontend/ci/runner/components/runner_created_at_spec.js
@@ -0,0 +1,97 @@
+import { GlSprintf, GlLink } from '@gitlab/ui';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
+import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+
+import RunnerCreatedAt from '~/ci/runner/components/runner_created_at.vue';
+
+import { runnerData } from '../mock_data';
+
+const mockRunner = runnerData.data.runner;
+
+describe('RunnerCreatedAt', () => {
+ let wrapper;
+
+ const createWrapper = ({ runner = {} } = {}) => {
+ wrapper = mountExtended(RunnerCreatedAt, {
+ propsData: {
+ runner: {
+ ...mockRunner,
+ ...runner,
+ },
+ },
+ stubs: {
+ GlSprintf,
+ TimeAgo,
+ UserAvatarLink,
+ },
+ });
+ };
+
+ const findTimeAgo = () => wrapper.findComponent(TimeAgo);
+ const findLink = () => wrapper.findComponent(GlLink);
+
+ const expectUserLink = (createdBy) => {
+ const { id, name, avatarUrl, webUrl, username } = createdBy;
+
+ expect(findLink().text()).toBe(name);
+ expect(findLink().attributes('href')).toBe(webUrl);
+ expect({ ...findLink().element.dataset }).toEqual({
+ avatarUrl,
+ name,
+ userId: `${getIdFromGraphQLId(id)}`,
+ username,
+ });
+ };
+
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ it('shows creation time and creator', () => {
+ expect(wrapper.text()).toMatchInterpolatedText(
+ `Created by ${mockRunner.createdBy.name} ${findTimeAgo().text()}`,
+ );
+
+ expectUserLink(mockRunner.createdBy);
+ expect(findTimeAgo().props('time')).toBe(mockRunner.createdAt);
+ });
+
+ it('shows creation time with no creator', () => {
+ createWrapper({
+ runner: {
+ createdBy: null,
+ },
+ });
+
+ expect(wrapper.text()).toMatchInterpolatedText(`Created ${findTimeAgo().text()}`);
+
+ expect(findLink().exists()).toBe(false);
+ expect(findTimeAgo().props('time')).toBe(mockRunner.createdAt);
+ });
+
+ it('shows creator with no creation time', () => {
+ createWrapper({
+ runner: {
+ createdAt: null,
+ },
+ });
+
+ expect(wrapper.text()).toMatchInterpolatedText(`Created by ${mockRunner.createdBy.name}`);
+
+ expectUserLink(mockRunner.createdBy);
+ expect(findTimeAgo().exists()).toBe(false);
+ });
+
+ it('shows no creation information', () => {
+ createWrapper({
+ runner: {
+ createdBy: null,
+ createdAt: null,
+ },
+ });
+
+ expect(wrapper.html()).toBe('');
+ });
+});
diff --git a/spec/frontend/ci/runner/components/runner_filtered_search_bar_spec.js b/spec/frontend/ci/runner/components/runner_filtered_search_bar_spec.js
index 7572122a5f3..ffc19d66cac 100644
--- a/spec/frontend/ci/runner/components/runner_filtered_search_bar_spec.js
+++ b/spec/frontend/ci/runner/components/runner_filtered_search_bar_spec.js
@@ -1,4 +1,5 @@
import { GlFilteredSearch, GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { assertProps } from 'helpers/assert_props';
import RunnerFilteredSearchBar from '~/ci/runner/components/runner_filtered_search_bar.vue';
@@ -35,8 +36,8 @@ describe('RunnerList', () => {
const mockOtherSort = CONTACTED_DESC;
const mockFilters = [
- { type: PARAM_KEY_STATUS, value: { data: STATUS_ONLINE, operator: '=' } },
- { type: FILTERED_SEARCH_TERM, value: { data: '' } },
+ { id: 1, type: PARAM_KEY_STATUS, value: { data: STATUS_ONLINE, operator: '=' } },
+ { id: 2, type: FILTERED_SEARCH_TERM, value: { data: '' } },
];
const expectToHaveLastEmittedInput = (value) => {
@@ -148,10 +149,12 @@ describe('RunnerList', () => {
).toEqual('Last contact');
});
- it('when the user sets a filter, the "search" preserves the other filters', () => {
+ it('when the user sets a filter, the "search" preserves the other filters', async () => {
findGlFilteredSearch().vm.$emit('input', mockFilters);
findGlFilteredSearch().vm.$emit('submit');
+ await nextTick();
+
expectToHaveLastEmittedInput({
runnerType: INSTANCE_TYPE,
membership: DEFAULT_MEMBERSHIP,
@@ -162,10 +165,12 @@ describe('RunnerList', () => {
});
});
- it('when the user sets a filter, the "search" is emitted with filters', () => {
+ it('when the user sets a filter, the "search" is emitted with filters', async () => {
findGlFilteredSearch().vm.$emit('input', mockFilters);
findGlFilteredSearch().vm.$emit('submit');
+ await nextTick();
+
expectToHaveLastEmittedInput({
runnerType: null,
membership: DEFAULT_MEMBERSHIP,
diff --git a/spec/frontend/ci/runner/components/runner_header_spec.js b/spec/frontend/ci/runner/components/runner_header_spec.js
index f5091226eaa..c7d95c49d81 100644
--- a/spec/frontend/ci/runner/components/runner_header_spec.js
+++ b/spec/frontend/ci/runner/components/runner_header_spec.js
@@ -11,6 +11,7 @@ import { convertToGraphQLId } from '~/graphql_shared/utils';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import RunnerHeader from '~/ci/runner/components/runner_header.vue';
+import RunnerCreatedAt from '~/ci/runner/components/runner_created_at.vue';
import RunnerTypeBadge from '~/ci/runner/components/runner_type_badge.vue';
import RunnerStatusBadge from '~/ci/runner/components/runner_status_badge.vue';
@@ -25,7 +26,6 @@ describe('RunnerHeader', () => {
const findRunnerTypeBadge = () => wrapper.findComponent(RunnerTypeBadge);
const findRunnerStatusBadge = () => wrapper.findComponent(RunnerStatusBadge);
const findRunnerLockedIcon = () => wrapper.findByTestId('lock-icon');
- const findTimeAgo = () => wrapper.findComponent(TimeAgo);
const createComponent = ({ runner = {}, options = {}, mountFn = shallowMountExtended } = {}) => {
wrapper = mountFn(RunnerHeader, {
@@ -86,24 +86,10 @@ describe('RunnerHeader', () => {
expect(findRunnerLockedIcon().exists()).toBe(true);
});
- it('displays the runner creation time', () => {
+ it('displays the runner creation data', () => {
createComponent();
- expect(wrapper.text()).toMatch(/created .+/);
- expect(findTimeAgo().props('time')).toBe(mockRunner.createdAt);
- });
-
- it('does not display runner creation time if "createdAt" is missing', () => {
- createComponent({
- runner: {
- id: convertToGraphQLId(TYPENAME_CI_RUNNER, 99),
- createdAt: null,
- },
- });
-
- expect(wrapper.text()).toContain(`#99 (${mockRunnerSha})`);
- expect(wrapper.text()).not.toMatch(/created .+/);
- expect(findTimeAgo().exists()).toBe(false);
+ expect(wrapper.findComponent(RunnerCreatedAt).props('runner')).toEqual(mockRunner);
});
it('displays actions in a slot', () => {
diff --git a/spec/frontend/ci/runner/components/runner_list_header_spec.js b/spec/frontend/ci/runner/components/runner_list_header_spec.js
new file mode 100644
index 00000000000..45823fa6813
--- /dev/null
+++ b/spec/frontend/ci/runner/components/runner_list_header_spec.js
@@ -0,0 +1,31 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import RunnerListHeader from '~/ci/runner/components/runner_list_header.vue';
+
+describe('RunnerListHeader', () => {
+ let wrapper;
+ const createWrapper = (options) => {
+ wrapper = shallowMountExtended(RunnerListHeader, {
+ ...options,
+ });
+ };
+
+ it('shows title', () => {
+ createWrapper({
+ scopedSlots: {
+ title: () => 'My title',
+ },
+ });
+
+ expect(wrapper.find('h1').text()).toBe('My title');
+ });
+
+ it('shows actions', () => {
+ createWrapper({
+ scopedSlots: {
+ actions: () => 'My actions',
+ },
+ });
+
+ expect(wrapper.text()).toContain('My actions');
+ });
+});
diff --git a/spec/frontend/ci/runner/components/runner_update_form_spec.js b/spec/frontend/ci/runner/components/runner_update_form_spec.js
index 2ba1c31fe52..a1ecedc6846 100644
--- a/spec/frontend/ci/runner/components/runner_update_form_spec.js
+++ b/spec/frontend/ci/runner/components/runner_update_form_spec.js
@@ -137,7 +137,15 @@ describe('RunnerUpdateForm', () => {
await submitFormAndWait();
// Some read-only fields are not submitted
- const { __typename, shortSha, runnerType, createdAt, status, ...submitted } = mockRunner;
+ const {
+ __typename,
+ shortSha,
+ runnerType,
+ createdAt,
+ createdBy,
+ status,
+ ...submitted
+ } = mockRunner;
expectToHaveSubmittedRunnerContaining(submitted);
});
diff --git a/spec/frontend/ci/runner/group_new_runner_app/group_new_runner_app_spec.js b/spec/frontend/ci/runner/group_new_runner_app/group_new_runner_app_spec.js
index 177fd9bcd9a..623a8f1c5a1 100644
--- a/spec/frontend/ci/runner/group_new_runner_app/group_new_runner_app_spec.js
+++ b/spec/frontend/ci/runner/group_new_runner_app/group_new_runner_app_spec.js
@@ -6,7 +6,6 @@ import { createAlert, VARIANT_SUCCESS } from '~/alert';
import GroupRunnerRunnerApp from '~/ci/runner/group_new_runner/group_new_runner_app.vue';
import RegistrationCompatibilityAlert from '~/ci/runner/components/registration/registration_compatibility_alert.vue';
-import RegistrationFeedbackBanner from '~/ci/runner/components/registration/registration_feedback_banner.vue';
import { saveAlertToLocalStorage } from '~/ci/runner/local_storage_alert/save_alert_to_local_storage';
import RunnerPlatformsRadioGroup from '~/ci/runner/components/runner_platforms_radio_group.vue';
import {
@@ -34,7 +33,6 @@ describe('GroupRunnerRunnerApp', () => {
let wrapper;
const findRunnerPlatformsRadioGroup = () => wrapper.findComponent(RunnerPlatformsRadioGroup);
- const findRegistrationFeedbackBanner = () => wrapper.findComponent(RegistrationFeedbackBanner);
const findRegistrationCompatibilityAlert = () =>
wrapper.findComponent(RegistrationCompatibilityAlert);
const findRunnerCreateForm = () => wrapper.findComponent(RunnerCreateForm);
@@ -54,10 +52,6 @@ describe('GroupRunnerRunnerApp', () => {
createComponent();
});
- it('shows a registration feedback banner', () => {
- expect(findRegistrationFeedbackBanner().exists()).toBe(true);
- });
-
it('shows a registration compatibility alert', () => {
expect(findRegistrationCompatibilityAlert().props('alertKey')).toBe(mockGroupId);
});
diff --git a/spec/frontend/ci/runner/mock_data.js b/spec/frontend/ci/runner/mock_data.js
index b8eb9f0ba1b..51556650c32 100644
--- a/spec/frontend/ci/runner/mock_data.js
+++ b/spec/frontend/ci/runner/mock_data.js
@@ -310,6 +310,23 @@ export const mockSearchExamples = [
first: RUNNER_PAGE_SIZE,
},
},
+ {
+ name: 'version prefix',
+ urlQuery: '?version_prefix[]=16.',
+ search: {
+ runnerType: null,
+ membership: DEFAULT_MEMBERSHIP,
+ filters: [{ type: 'version_prefix', value: { data: '16.', operator: '=' } }],
+ pagination: {},
+ sort: CREATED_DESC,
+ },
+ graphqlVariables: {
+ versionPrefix: '16.',
+ membership: DEFAULT_MEMBERSHIP,
+ sort: CREATED_DESC,
+ first: RUNNER_PAGE_SIZE,
+ },
+ },
];
export const onlineContactTimeoutSecs = 2 * 60 * 60;
diff --git a/spec/frontend/ci/runner/project_new_runner_app/project_new_runner_app_spec.js b/spec/frontend/ci/runner/project_new_runner_app/project_new_runner_app_spec.js
index 22d8e243f7b..3e12f3911a0 100644
--- a/spec/frontend/ci/runner/project_new_runner_app/project_new_runner_app_spec.js
+++ b/spec/frontend/ci/runner/project_new_runner_app/project_new_runner_app_spec.js
@@ -6,7 +6,6 @@ import { createAlert, VARIANT_SUCCESS } from '~/alert';
import ProjectRunnerRunnerApp from '~/ci/runner/project_new_runner/project_new_runner_app.vue';
import RegistrationCompatibilityAlert from '~/ci/runner/components/registration/registration_compatibility_alert.vue';
-import RegistrationFeedbackBanner from '~/ci/runner/components/registration/registration_feedback_banner.vue';
import { saveAlertToLocalStorage } from '~/ci/runner/local_storage_alert/save_alert_to_local_storage';
import RunnerPlatformsRadioGroup from '~/ci/runner/components/runner_platforms_radio_group.vue';
import {
@@ -34,7 +33,6 @@ describe('ProjectRunnerRunnerApp', () => {
let wrapper;
const findRunnerPlatformsRadioGroup = () => wrapper.findComponent(RunnerPlatformsRadioGroup);
- const findRegistrationFeedbackBanner = () => wrapper.findComponent(RegistrationFeedbackBanner);
const findRegistrationCompatibilityAlert = () =>
wrapper.findComponent(RegistrationCompatibilityAlert);
const findRunnerCreateForm = () => wrapper.findComponent(RunnerCreateForm);
@@ -55,10 +53,6 @@ describe('ProjectRunnerRunnerApp', () => {
createComponent();
});
- it('shows a registration feedback banner', () => {
- expect(findRegistrationFeedbackBanner().exists()).toBe(true);
- });
-
it('shows a registration compatibility alert', () => {
expect(findRegistrationCompatibilityAlert().props('alertKey')).toBe(mockProjectId);
});
diff --git a/spec/frontend/ci/runner/runner_edit/runner_edit_app_spec.js b/spec/frontend/ci/runner/runner_edit/runner_edit_app_spec.js
index ee4bd9ccc92..6ceecd79163 100644
--- a/spec/frontend/ci/runner/runner_edit/runner_edit_app_spec.js
+++ b/spec/frontend/ci/runner/runner_edit/runner_edit_app_spec.js
@@ -64,7 +64,7 @@ describe('RunnerEditApp', () => {
await createComponentWithApollo({ mountFn: mount });
expect(findRunnerHeader().text()).toContain(`#${mockRunnerId} (${mockRunnerSha})`);
- expect(findRunnerHeader().text()).toContain('created');
+ expect(findRunnerHeader().text()).toContain('Created');
});
it('displays the runner type and status', async () => {
diff --git a/spec/frontend/ci/runner/sentry_utils_spec.js b/spec/frontend/ci/runner/sentry_utils_spec.js
index 59d386a5899..bb291557288 100644
--- a/spec/frontend/ci/runner/sentry_utils_spec.js
+++ b/spec/frontend/ci/runner/sentry_utils_spec.js
@@ -1,7 +1,7 @@
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { captureException } from '~/ci/runner/sentry_utils';
-jest.mock('@sentry/browser');
+jest.mock('~/sentry/sentry_browser_wrapper');
describe('~/ci/runner/sentry_utils', () => {
describe('captureException', () => {
diff --git a/spec/frontend/clusters_list/components/clusters_spec.js b/spec/frontend/clusters_list/components/clusters_spec.js
index d4474b1c643..a7ec32c4f32 100644
--- a/spec/frontend/clusters_list/components/clusters_spec.js
+++ b/spec/frontend/clusters_list/components/clusters_spec.js
@@ -1,8 +1,8 @@
import { GlLoadingIcon, GlPagination, GlSkeletonLoader, GlTableLite } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import Clusters from '~/clusters_list/components/clusters.vue';
import ClustersEmptyState from '~/clusters_list/components/clusters_empty_state.vue';
import ClusterStore from '~/clusters_list/store';
@@ -15,7 +15,7 @@ import {
} from '~/clusters_list/store/mutation_types';
import { apiData } from '../mock_data';
-jest.mock('@sentry/browser');
+jest.mock('~/sentry/sentry_browser_wrapper');
describe('Clusters', () => {
let mock;
diff --git a/spec/frontend/clusters_list/store/actions_spec.js b/spec/frontend/clusters_list/store/actions_spec.js
index 9e6da595a75..fda3fde6baa 100644
--- a/spec/frontend/clusters_list/store/actions_spec.js
+++ b/spec/frontend/clusters_list/store/actions_spec.js
@@ -1,5 +1,5 @@
-import * as Sentry from '@sentry/browser';
import MockAdapter from 'axios-mock-adapter';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import testAction from 'helpers/vuex_action_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { MAX_REQUESTS } from '~/clusters_list/constants';
diff --git a/spec/frontend/commit/components/commit_box_pipeline_status_spec.js b/spec/frontend/commit/components/commit_box_pipeline_status_spec.js
index 844a2d81832..008a1b2c068 100644
--- a/spec/frontend/commit/components/commit_box_pipeline_status_spec.js
+++ b/spec/frontend/commit/components/commit_box_pipeline_status_spec.js
@@ -5,7 +5,7 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/alert';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import CommitBoxPipelineStatus from '~/projects/commit_box/info/components/commit_box_pipeline_status.vue';
import {
COMMIT_BOX_POLL_INTERVAL,
@@ -32,7 +32,7 @@ describe('Commit box pipeline status', () => {
const failedHandler = jest.fn().mockRejectedValue(new Error('GraphQL error'));
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const findCiBadgeLink = () => wrapper.findComponent(CiBadgeLink);
+ const findCiIcon = () => wrapper.findComponent(CiIcon);
const advanceToNextFetch = () => {
jest.advanceTimersByTime(COMMIT_BOX_POLL_INTERVAL);
@@ -50,7 +50,7 @@ describe('Commit box pipeline status', () => {
...mockProvide,
},
stubs: {
- CiBadgeLink,
+ CiIcon,
},
apolloProvider: createMockApolloProvider(handler),
});
@@ -61,7 +61,7 @@ describe('Commit box pipeline status', () => {
createComponent();
expect(findLoadingIcon().exists()).toBe(true);
- expect(findCiBadgeLink().exists()).toBe(false);
+ expect(findCiIcon().exists()).toBe(false);
});
});
@@ -73,7 +73,7 @@ describe('Commit box pipeline status', () => {
});
it('should display pipeline status after the query is resolved successfully', () => {
- expect(findCiBadgeLink().exists()).toBe(true);
+ expect(findCiIcon().exists()).toBe(true);
expect(findLoadingIcon().exists()).toBe(false);
expect(createAlert).toHaveBeenCalledTimes(0);
@@ -90,7 +90,7 @@ describe('Commit box pipeline status', () => {
},
} = mockPipelineStatusResponse;
- expect(findCiBadgeLink().attributes('href')).toBe(detailsPath);
+ expect(findCiIcon().attributes('href')).toBe(detailsPath);
});
});
diff --git a/spec/frontend/content_editor/extensions/code_block_highlight_spec.js b/spec/frontend/content_editor/extensions/code_block_highlight_spec.js
index fc8460c7f84..6f0c0ee6ed5 100644
--- a/spec/frontend/content_editor/extensions/code_block_highlight_spec.js
+++ b/spec/frontend/content_editor/extensions/code_block_highlight_spec.js
@@ -61,6 +61,18 @@ describe('content_editor/extensions/code_block_highlight', () => {
expect(editorHtmlOutput.classList.toString()).toContain('content-editor-code-block');
});
+
+ it('includes the lowlight plugin', () => {
+ expect(tiptapEditor.state.plugins).toContainEqual(
+ expect.objectContaining({ key: expect.stringContaining('lowlight') }),
+ );
+ });
+
+ it('does not include the VSCode paste plugin', () => {
+ expect(tiptapEditor.state.plugins).not.toContainEqual(
+ expect.objectContaining({ key: expect.stringContaining('VSCode') }),
+ );
+ });
});
describe.each`
diff --git a/spec/frontend/content_editor/extensions/copy_paste_spec.js b/spec/frontend/content_editor/extensions/copy_paste_spec.js
index 4729b1c1223..e290b4e5137 100644
--- a/spec/frontend/content_editor/extensions/copy_paste_spec.js
+++ b/spec/frontend/content_editor/extensions/copy_paste_spec.js
@@ -1,24 +1,24 @@
import CopyPaste from '~/content_editor/extensions/copy_paste';
import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight';
-import Loading from '~/content_editor/extensions/loading';
+import Loading, { findAllLoaders } from '~/content_editor/extensions/loading';
import Diagram from '~/content_editor/extensions/diagram';
import Frontmatter from '~/content_editor/extensions/frontmatter';
+import Selection from '~/content_editor/extensions/selection';
import Heading from '~/content_editor/extensions/heading';
import Bold from '~/content_editor/extensions/bold';
import BulletList from '~/content_editor/extensions/bullet_list';
import ListItem from '~/content_editor/extensions/list_item';
import Italic from '~/content_editor/extensions/italic';
+import Table from '~/content_editor/extensions/table';
+import TableCell from '~/content_editor/extensions/table_cell';
+import TableRow from '~/content_editor/extensions/table_row';
+import TableHeader from '~/content_editor/extensions/table_header';
import { VARIANT_DANGER } from '~/alert';
import eventHubFactory from '~/helpers/event_hub_factory';
import { ALERT_EVENT } from '~/content_editor/constants';
import waitForPromises from 'helpers/wait_for_promises';
import MarkdownSerializer from '~/content_editor/services/markdown_serializer';
-import {
- createTestEditor,
- createDocBuilder,
- waitUntilNextDocTransaction,
- sleep,
-} from '../test_utils';
+import { createTestEditor, createDocBuilder, waitUntilNextDocTransaction } from '../test_utils';
const CODE_BLOCK_HTML = '<pre class="js-syntax-highlight" lang="javascript">var a = 2;</pre>';
const CODE_SUGGESTION_HTML =
@@ -35,13 +35,11 @@ describe('content_editor/extensions/copy_paste', () => {
let p;
let bold;
let italic;
- let loading;
let heading;
let codeBlock;
let bulletList;
let listItem;
let renderMarkdown;
- let resolveRenderMarkdownPromise;
let resolveRenderMarkdownPromiseAndWait;
let eventHub;
@@ -52,7 +50,6 @@ describe('content_editor/extensions/copy_paste', () => {
renderMarkdown = jest.fn().mockImplementation(
() =>
new Promise((resolve) => {
- resolveRenderMarkdownPromise = resolve;
resolveRenderMarkdownPromiseAndWait = (data) =>
waitUntilNextDocTransaction({ tiptapEditor, action: () => resolve(data) });
}),
@@ -65,28 +62,34 @@ describe('content_editor/extensions/copy_paste', () => {
Bold,
Italic,
Loading,
+ Selection,
CodeBlockHighlight,
Diagram,
Frontmatter,
Heading,
BulletList,
ListItem,
+ Table,
+ TableCell,
+ TableRow,
+ TableHeader,
CopyPaste.configure({ renderMarkdown, eventHub, serializer: new MarkdownSerializer() }),
],
});
({
- builders: { doc, p, bold, italic, heading, loading, codeBlock, bulletList, listItem },
+ builders: { doc, p, bold, italic, heading, codeBlock, bulletList, listItem },
} = createDocBuilder({
tiptapEditor,
names: {
bold: { markType: Bold.name },
italic: { markType: Italic.name },
- loading: { nodeType: Loading.name },
heading: { nodeType: Heading.name },
bulletList: { nodeType: BulletList.name },
listItem: { nodeType: ListItem.name },
codeBlock: { nodeType: CodeBlockHighlight.name },
+ diagram: { nodeType: Diagram.name },
+ frontmatter: { nodeType: Frontmatter.name },
},
}));
});
@@ -110,17 +113,6 @@ describe('content_editor/extensions/copy_paste', () => {
});
};
- const triggerPasteEventHandlerAndWaitForTransaction = (event) => {
- return waitUntilNextDocTransaction({
- tiptapEditor,
- action: () => {
- tiptapEditor.view.someProp('handlePaste', (eventHandler) => {
- return eventHandler(tiptapEditor.view, event);
- });
- },
- });
- };
-
it.each`
types | data | formatDesc
${['text/plain']} | ${{}} | ${'plain text'}
@@ -183,37 +175,57 @@ describe('content_editor/extensions/copy_paste', () => {
});
});
+ describe('when copying content with a single table cell', () => {
+ it('sets the clipboard data properly', () => {
+ const event = buildClipboardEvent({ eventName: 'copy' });
+
+ tiptapEditor.commands.insertContent('<table><tr><td>Cell 1</td></tr></table>');
+ tiptapEditor.commands.selectAll();
+ tiptapEditor.view.dispatchEvent(event);
+
+ expect(event.clipboardData.setData).toHaveBeenCalledWith('text/x-gfm', 'Cell 1');
+ });
+ });
+
+ describe('when copying content with a table with multiple cells', () => {
+ it('sets the clipboard data properly', () => {
+ const event = buildClipboardEvent({ eventName: 'copy' });
+
+ tiptapEditor.commands.insertContent('<table><tr><td>Cell 1</td><td>Cell 2</td></tr></table>');
+ tiptapEditor.commands.selectAll();
+ tiptapEditor.view.dispatchEvent(event);
+
+ expect(event.clipboardData.setData).toHaveBeenCalledWith(
+ 'text/x-gfm',
+ `<table>
+<tr>
+<td>Cell 1</td>
+<td>Cell 2</td>
+</tr>
+</table>
+
+`,
+ );
+ });
+ });
+
describe('when pasting raw markdown source', () => {
it('shows a loading indicator while markdown is being processed', async () => {
- const expectedDoc = doc(p(loading({ id: expect.any(String) })));
-
- await triggerPasteEventHandlerAndWaitForTransaction(buildClipboardEvent());
+ await triggerPasteEventHandler(buildClipboardEvent());
- expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
+ expect(findAllLoaders(tiptapEditor.state)).toHaveLength(1);
});
it('pastes in the correct position if some content is added before the markdown is processed', async () => {
const expectedDoc = doc(p(bold('some markdown'), 'some content'));
const resolvedValue = '<strong>some markdown</strong>';
- await triggerPasteEventHandlerAndWaitForTransaction(buildClipboardEvent());
+ await triggerPasteEventHandler(buildClipboardEvent());
tiptapEditor.commands.insertContent('some content');
- await resolveRenderMarkdownPromiseAndWait(resolvedValue);
-
- expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
- expect(tiptapEditor.state.selection.from).toEqual(26); // end of the document
- });
-
- it('does not paste anything if the loading indicator is deleted before the markdown is processed', async () => {
- const expectedDoc = doc(p());
- await triggerPasteEventHandlerAndWaitForTransaction(buildClipboardEvent());
- tiptapEditor.chain().selectAll().deleteSelection().run();
- resolveRenderMarkdownPromise('some markdown');
+ await resolveRenderMarkdownPromiseAndWait(resolvedValue);
- // wait some time to be sure no transaction happened
- await sleep();
expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
});
@@ -227,7 +239,7 @@ describe('content_editor/extensions/copy_paste', () => {
it('transforms pasted text into a prosemirror node', async () => {
const expectedDoc = doc(p(bold('bold text')));
- await triggerPasteEventHandlerAndWaitForTransaction(buildClipboardEvent());
+ await triggerPasteEventHandler(buildClipboardEvent());
await resolveRenderMarkdownPromiseAndWait(resolvedValue);
expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
@@ -239,7 +251,7 @@ describe('content_editor/extensions/copy_paste', () => {
tiptapEditor.commands.setContent('Initial text and ');
- await triggerPasteEventHandlerAndWaitForTransaction(buildClipboardEvent());
+ await triggerPasteEventHandler(buildClipboardEvent());
await resolveRenderMarkdownPromiseAndWait(resolvedValue);
expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
@@ -253,7 +265,7 @@ describe('content_editor/extensions/copy_paste', () => {
tiptapEditor.commands.setContent('Initial text and ');
tiptapEditor.commands.setTextSelection({ from: 13, to: 17 });
- await triggerPasteEventHandlerAndWaitForTransaction(buildClipboardEvent());
+ await triggerPasteEventHandler(buildClipboardEvent());
await resolveRenderMarkdownPromiseAndWait(resolvedValue);
expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
@@ -274,7 +286,7 @@ describe('content_editor/extensions/copy_paste', () => {
tiptapEditor.commands.setContent('Initial text and ');
- await triggerPasteEventHandlerAndWaitForTransaction(buildClipboardEvent());
+ await triggerPasteEventHandler(buildClipboardEvent());
await resolveRenderMarkdownPromiseAndWait(resolvedValue);
expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
@@ -289,7 +301,7 @@ describe('content_editor/extensions/copy_paste', () => {
const expectedDoc = doc(p(bold('bold text')), p('some code'));
- await triggerPasteEventHandlerAndWaitForTransaction(
+ await triggerPasteEventHandler(
buildClipboardEvent({
types: ['text/html'],
data: {
@@ -309,7 +321,7 @@ describe('content_editor/extensions/copy_paste', () => {
const resolvedValue = '<strong>bold text</strong>';
const expectedDoc = doc(p(bold('bold text')));
- await triggerPasteEventHandlerAndWaitForTransaction(
+ await triggerPasteEventHandler(
buildClipboardEvent({
types: ['text/x-gfm', 'text/plain', 'text/html'],
data: {
@@ -332,7 +344,7 @@ describe('content_editor/extensions/copy_paste', () => {
bulletList(listItem(p('Cat')), listItem(p('Dog')), listItem(p('Turtle'))),
);
- await triggerPasteEventHandlerAndWaitForTransaction(
+ await triggerPasteEventHandler(
buildClipboardEvent({
types: ['text/plain', 'text/html'],
data: {
@@ -359,7 +371,7 @@ describe('content_editor/extensions/copy_paste', () => {
),
);
- await triggerPasteEventHandlerAndWaitForTransaction(
+ await triggerPasteEventHandler(
buildClipboardEvent({
types: ['vscode-editor-data', 'text/plain', 'text/html'],
data: {
@@ -380,7 +392,7 @@ describe('content_editor/extensions/copy_paste', () => {
const expectedDoc = doc(p(bold('bold text')));
- await triggerPasteEventHandlerAndWaitForTransaction(
+ await triggerPasteEventHandler(
buildClipboardEvent({
types: ['vscode-editor-data', 'text/plain', 'text/html'],
data: {
diff --git a/spec/frontend/content_editor/extensions/horizontal_rule_spec.js b/spec/frontend/content_editor/extensions/horizontal_rule_spec.js
index 322c04a42e1..14241ae9cc6 100644
--- a/spec/frontend/content_editor/extensions/horizontal_rule_spec.js
+++ b/spec/frontend/content_editor/extensions/horizontal_rule_spec.js
@@ -22,7 +22,7 @@ describe('content_editor/extensions/horizontal_rule', () => {
it.each`
input | insertedNodes
- ${'---'} | ${() => [p(), horizontalRule()]}
+ ${'---'} | ${() => [horizontalRule(), p()]}
${'--'} | ${() => [p()]}
${'---x'} | ${() => [p()]}
${' ---x'} | ${() => [p()]}
diff --git a/spec/frontend/content_editor/extensions/html_marks_spec.js b/spec/frontend/content_editor/extensions/html_marks_spec.js
new file mode 100644
index 00000000000..3757962ce52
--- /dev/null
+++ b/spec/frontend/content_editor/extensions/html_marks_spec.js
@@ -0,0 +1,89 @@
+import HTMLMarks from '~/content_editor/extensions/html_marks';
+import { createTestEditor, createDocBuilder } from '../test_utils';
+
+describe('content_editor/extensions/html_marks', () => {
+ let tiptapEditor;
+ let doc;
+ let ins;
+ let abbr;
+ let bdo;
+ let cite;
+ let dfn;
+ let small;
+ let span;
+ let time;
+ let kbd;
+ let q;
+ let p;
+ let samp;
+ let varMark;
+ let ruby;
+ let rp;
+ let rt;
+
+ beforeEach(() => {
+ tiptapEditor = createTestEditor({ extensions: [...HTMLMarks] });
+
+ ({
+ builders: {
+ doc,
+ ins,
+ abbr,
+ bdo,
+ cite,
+ dfn,
+ small,
+ span,
+ time,
+ kbd,
+ q,
+ samp,
+ var: varMark,
+ ruby,
+ rp,
+ rt,
+ p,
+ },
+ } = createDocBuilder({
+ tiptapEditor,
+ names: {
+ ...HTMLMarks.reduce(
+ (builders, htmlMark) => ({
+ ...builders,
+ [htmlMark.name]: { markType: htmlMark.name },
+ }),
+ {},
+ ),
+ },
+ }));
+ });
+
+ it.each`
+ input | expectedContent
+ ${'<ins>inserted</ins>'} | ${() => ins('inserted')}
+ ${'<abbr title="abbr">abbreviation</abbr>'} | ${() => abbr({ title: 'abbr' }, 'abbreviation')}
+ ${'<bdo dir="rtl">bdo</bdo>'} | ${() => bdo({ dir: 'rtl' }, 'bdo')}
+ ${'<cite>citation</cite>'} | ${() => cite('citation')}
+ ${'<dfn>definition</dfn>'} | ${() => dfn('definition')}
+ ${'<small>small text</small>'} | ${() => small('small text')}
+ ${'<span dir="rtl">span text</span>'} | ${() => span({ dir: 'rtl' }, 'span text')}
+ ${'<time datetime="2023-11-02">November 2, 2023</time>'} | ${() => time({ datetime: '2023-11-02' }, 'November 2, 2023')}
+ ${'<kbd>keyboard</kbd>'} | ${() => kbd('keyboard')}
+ ${'<q>quote</q>'} | ${() => q('quote')}
+ ${'<samp>sample</samp>'} | ${() => samp('sample')}
+ ${'<var>variable</var>'} | ${() => varMark('variable')}
+ ${'<ruby>base<rp>(</rp><rt>ruby</rt><rp>)</rp></ruby>'} | ${() => ruby('base', rp('('), rt('ruby'), rp(')'))}
+ `('parses and creates marks for $input', ({ input, expectedContent }) => {
+ tiptapEditor.commands.setContent(input);
+ expect(tiptapEditor.getJSON()).toEqual(doc(p(expectedContent())).toJSON());
+ expect(tiptapEditor.getHTML()).toContain(input);
+ });
+
+ it('does not parse an element with a data-escaped-char attribute', () => {
+ const input = '<span data-escaped-char>#</span> not a heading';
+ const expectedDoc = doc(p('# not a heading'));
+ tiptapEditor.commands.setContent(input);
+ expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
+ expect(tiptapEditor.getHTML()).not.toContain('<span');
+ });
+});
diff --git a/spec/frontend/content_editor/extensions/word_break_spec.js b/spec/frontend/content_editor/extensions/word_break_spec.js
index 23167269d7d..5caca218702 100644
--- a/spec/frontend/content_editor/extensions/word_break_spec.js
+++ b/spec/frontend/content_editor/extensions/word_break_spec.js
@@ -22,11 +22,11 @@ describe('content_editor/extensions/word_break', () => {
it.each`
input | insertedNode
- ${'<wbr>'} | ${() => p(wordBreak())}
- ${'<wbr'} | ${() => p()}
- ${'wbr>'} | ${() => p()}
+ ${'<wbr>'} | ${() => [p(wordBreak()), p()]}
+ ${'<wbr'} | ${() => [p()]}
+ ${'wbr>'} | ${() => [p()]}
`('with input=$input, then should insert a $insertedNode', ({ input, insertedNode }) => {
- const expectedDoc = doc(insertedNode());
+ const expectedDoc = doc(...insertedNode());
triggerNodeInputRule({ tiptapEditor, inputRuleText: input });
diff --git a/spec/frontend/contribution_events/components/contribution_event/contribution_event_base_spec.js b/spec/frontend/contribution_events/components/contribution_event/contribution_event_base_spec.js
index 310966243d1..f5850401b8e 100644
--- a/spec/frontend/contribution_events/components/contribution_event/contribution_event_base_spec.js
+++ b/spec/frontend/contribution_events/components/contribution_event/contribution_event_base_spec.js
@@ -38,7 +38,7 @@ describe('ContributionEventBase', () => {
expect(avatarLink.attributes('href')).toBe(defaultPropsData.event.author.web_url);
expect(avatarLabeled.attributes()).toMatchObject({
src: defaultPropsData.event.author.avatar_url,
- size: '32',
+ size: '24',
});
expect(avatarLabeled.props()).toMatchObject({
label: defaultPropsData.event.author.name,
diff --git a/spec/frontend/crm/organization_form_wrapper_spec.js b/spec/frontend/crm/organization_form_wrapper_spec.js
index f15fcac71d5..4e84180b22b 100644
--- a/spec/frontend/crm/organization_form_wrapper_spec.js
+++ b/spec/frontend/crm/organization_form_wrapper_spec.js
@@ -2,8 +2,8 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import OrganizationFormWrapper from '~/crm/organizations/components/organization_form_wrapper.vue';
import CrmForm from '~/crm/components/crm_form.vue';
import getGroupOrganizationsQuery from '~/crm/organizations/components/graphql/get_group_organizations.query.graphql';
-import createOrganizationMutation from '~/crm/organizations/components/graphql/create_customer_relations_organization.mutation.graphql';
-import updateOrganizationMutation from '~/crm/organizations/components/graphql/update_organization.mutation.graphql';
+import createCustomerRelationsOrganizationMutation from '~/crm/organizations/components/graphql/create_customer_relations_organization.mutation.graphql';
+import updateCustomerRelationsOrganizationMutation from '~/crm/organizations/components/graphql/update_customer_relations_organization.mutation.graphql';
describe('Customer relations organization form wrapper', () => {
let wrapper;
@@ -48,7 +48,7 @@ describe('Customer relations organization form wrapper', () => {
expect(organizationForm.props('fields')).toHaveLength(4);
expect(organizationForm.props('title')).toBe('Edit organization');
expect(organizationForm.props('successMessage')).toBe('Organization has been updated.');
- expect(organizationForm.props('mutation')).toBe(updateOrganizationMutation);
+ expect(organizationForm.props('mutation')).toBe(updateCustomerRelationsOrganizationMutation);
expect(organizationForm.props('getQuery')).toMatchObject({
query: getGroupOrganizationsQuery,
variables: { groupFullPath: 'flightjs' },
@@ -69,7 +69,7 @@ describe('Customer relations organization form wrapper', () => {
expect(organizationForm.props('fields')).toHaveLength(3);
expect(organizationForm.props('title')).toBe('New organization');
expect(organizationForm.props('successMessage')).toBe('Organization has been added.');
- expect(organizationForm.props('mutation')).toBe(createOrganizationMutation);
+ expect(organizationForm.props('mutation')).toBe(createCustomerRelationsOrganizationMutation);
expect(organizationForm.props('getQuery')).toMatchObject({
query: getGroupOrganizationsQuery,
variables: { groupFullPath: 'flightjs' },
diff --git a/spec/frontend/custom_emoji/components/delete_item_spec.js b/spec/frontend/custom_emoji/components/delete_item_spec.js
index 06c4ca8d54b..2d5594b8a65 100644
--- a/spec/frontend/custom_emoji/components/delete_item_spec.js
+++ b/spec/frontend/custom_emoji/components/delete_item_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
-import * as Sentry from '@sentry/browser';
import { GlModal } from '@gitlab/ui';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -11,7 +11,7 @@ import deleteCustomEmojiMutation from '~/custom_emoji/queries/delete_custom_emoj
import { CUSTOM_EMOJI } from '../mock_data';
jest.mock('~/alert');
-jest.mock('@sentry/browser');
+jest.mock('~/sentry/sentry_browser_wrapper');
let wrapper;
let deleteMutationSpy;
diff --git a/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_signed_out_spec.js.snap b/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_signed_out_spec.js.snap
index 8bc9bce7e80..f77f1a791f9 100644
--- a/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_signed_out_spec.js.snap
+++ b/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_signed_out_spec.js.snap
@@ -2,7 +2,7 @@
exports[`DesignNoteSignedOut renders message containing register and sign-in links while user wants to reply to a discussion 1`] = `
<div
- class="disabled-comment text-center"
+ class="disabled-comment gl-text-center gl-text-secondary"
>
Please
<gl-link-stub
@@ -22,7 +22,7 @@ exports[`DesignNoteSignedOut renders message containing register and sign-in lin
exports[`DesignNoteSignedOut renders message containing register and sign-in links while user wants to start a new discussion 1`] = `
<div
- class="disabled-comment text-center"
+ class="disabled-comment gl-text-center gl-text-secondary"
>
Please
<gl-link-stub
diff --git a/spec/frontend/design_management/components/design_notes/__snapshots__/design_reply_form_spec.js.snap b/spec/frontend/design_management/components/design_notes/__snapshots__/design_reply_form_spec.js.snap
index 206187c3530..696b04868f7 100644
--- a/spec/frontend/design_management/components/design_notes/__snapshots__/design_reply_form_spec.js.snap
+++ b/spec/frontend/design_management/components/design_notes/__snapshots__/design_reply_form_spec.js.snap
@@ -3,7 +3,7 @@
exports[`Design reply form component renders button text as "Comment" when creating a comment 1`] = `
<button
class="btn btn-confirm btn-md disabled gl-button gl-mr-3 gl-w-auto!"
- data-qa-selector="save_comment_button"
+ data-testid="save-comment-button"
data-track-action="click_button"
disabled=""
type="submit"
@@ -19,7 +19,7 @@ exports[`Design reply form component renders button text as "Comment" when creat
exports[`Design reply form component renders button text as "Save comment" when creating a comment 1`] = `
<button
class="btn btn-confirm btn-md disabled gl-button gl-mr-3 gl-w-auto!"
- data-qa-selector="save_comment_button"
+ data-testid="save-comment-button"
data-track-action="click_button"
disabled=""
type="submit"
diff --git a/spec/frontend/design_management/components/design_notes/design_note_spec.js b/spec/frontend/design_management/components/design_notes/design_note_spec.js
index 8795b089551..28b264cede9 100644
--- a/spec/frontend/design_management/components/design_notes/design_note_spec.js
+++ b/spec/frontend/design_management/components/design_notes/design_note_spec.js
@@ -1,7 +1,7 @@
import { ApolloMutation } from 'vue-apollo';
import { nextTick } from 'vue';
import { GlAvatar, GlAvatarLink, GlDisclosureDropdown, GlDisclosureDropdownItem } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -51,13 +51,19 @@ describe('Design note component', () => {
const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
const findDropdownItems = () => findDropdown().findAllComponents(GlDisclosureDropdownItem);
const findEditDropdownItem = () => findDropdownItems().at(0);
- const findDeleteDropdownItem = () => findDropdownItems().at(1);
+ const findCopyLinkDropdownItem = () => findDropdownItems().at(1);
+ const findDeleteDropdownItem = () => findDropdownItems().at(2);
+
+ const showToast = jest.fn();
function createComponent({
props = {},
data = { isEditing: false },
mountFn = mountExtended,
mocks = {
+ $toast: {
+ show: showToast,
+ },
$route,
$apollo: {
mutate: jest.fn().mockResolvedValue({ data: { updateNote: {} } }),
@@ -239,6 +245,7 @@ describe('Design note component', () => {
expect(findDropdown().exists()).toBe(true);
expect(findEditDropdownItem().exists()).toBe(true);
+ expect(findCopyLinkDropdownItem().exists()).toBe(true);
expect(findDeleteDropdownItem().exists()).toBe(true);
expect(findDropdown().props('items')[0].extraAttrs.class).toBe('gl-sm-display-none!');
});
@@ -266,6 +273,40 @@ describe('Design note component', () => {
expect(wrapper.emitted()).toEqual({ 'delete-note': [[{ ...payload }]] });
});
+ it('shows a success toast after copying the url to the clipboard', () => {
+ createComponent({
+ props: {
+ note: {
+ ...note,
+ userPermissions: {
+ adminNote: true,
+ },
+ },
+ },
+ });
+
+ findCopyLinkDropdownItem().find('button').trigger('click');
+
+ expect(showToast).toHaveBeenCalledWith('Link copied to clipboard.');
+ });
+
+ it('has data-clipboard-text set to the correct url', () => {
+ createComponent({
+ props: {
+ note: {
+ ...note,
+ userPermissions: {
+ adminNote: true,
+ },
+ },
+ },
+ });
+
+ expect(findCopyLinkDropdownItem().props('item').extraAttrs['data-clipboard-text']).toBe(
+ 'http://test.host/#note_123',
+ );
+ });
+
describe('when user has award emoji permissions', () => {
const findEmojiPicker = () => wrapper.findComponent(EmojiPicker);
const propsData = {
diff --git a/spec/frontend/design_management/components/design_overlay_spec.js b/spec/frontend/design_management/components/design_overlay_spec.js
index 3eb47fdb97e..6b08b44c39e 100644
--- a/spec/frontend/design_management/components/design_overlay_spec.js
+++ b/spec/frontend/design_management/components/design_overlay_spec.js
@@ -86,7 +86,7 @@ describe('Design overlay component', () => {
};
wrapper
- .find('[data-qa-selector="design_image_button"]')
+ .find('[data-testid="design-image-button"]')
.trigger('mouseup', { offsetX: newCoordinates.x, offsetY: newCoordinates.y });
expect(wrapper.emitted('openCommentForm')).toEqual([
diff --git a/spec/frontend/design_management/components/design_presentation_spec.js b/spec/frontend/design_management/components/design_presentation_spec.js
index e64dec14461..96c2b4da2c6 100644
--- a/spec/frontend/design_management/components/design_presentation_spec.js
+++ b/spec/frontend/design_management/components/design_presentation_spec.js
@@ -46,7 +46,7 @@ describe('Design management design presentation component', () => {
wrapper.element.scrollTo = jest.fn();
}
- const findOverlayCommentButton = () => wrapper.find('[data-qa-selector="design_image_button"]');
+ const findOverlayCommentButton = () => wrapper.find('[data-testid="design-image-button"]');
/**
* Spy on $refs and mock given values
diff --git a/spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap b/spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap
index 53359b02b4c..a05b3baecd3 100644
--- a/spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap
+++ b/spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap
@@ -17,12 +17,12 @@ exports[`Design management list item component with notes renders item with mult
>
<gl-intersection-observer-stub
class="gl-flex-grow-1"
+ data-qa-filename="test"
+ data-testid="design-image"
>
<img
alt="test"
class="design-img gl-display-block gl-max-h-full gl-max-w-full gl-mx-auto gl-w-auto"
- data-qa-filename="test"
- data-qa-selector="design_image"
data-testid="design-img-1"
src="null"
/>
@@ -33,10 +33,10 @@ exports[`Design management list item component with notes renders item with mult
>
<div
class="gl-display-flex gl-flex-direction-column str-truncated-100"
+ data-testid="design-file-name"
>
<span
class="gl-font-weight-semibold str-truncated-100"
- data-qa-selector="design_file_name"
data-testid="design-img-filename-1"
title="test"
>
@@ -82,12 +82,12 @@ exports[`Design management list item component with notes renders item with sing
>
<gl-intersection-observer-stub
class="gl-flex-grow-1"
+ data-qa-filename="test"
+ data-testid="design-image"
>
<img
alt="test"
class="design-img gl-display-block gl-max-h-full gl-max-w-full gl-mx-auto gl-w-auto"
- data-qa-filename="test"
- data-qa-selector="design_image"
data-testid="design-img-1"
src="null"
/>
@@ -98,10 +98,10 @@ exports[`Design management list item component with notes renders item with sing
>
<div
class="gl-display-flex gl-flex-direction-column str-truncated-100"
+ data-testid="design-file-name"
>
<span
class="gl-font-weight-semibold str-truncated-100"
- data-qa-selector="design_file_name"
data-testid="design-img-filename-1"
title="test"
>
diff --git a/spec/frontend/diffs/components/app_spec.js b/spec/frontend/diffs/components/app_spec.js
index 212def72b90..63d9a2471b6 100644
--- a/spec/frontend/diffs/components/app_spec.js
+++ b/spec/frontend/diffs/components/app_spec.js
@@ -2,8 +2,11 @@ import { GlLoadingIcon, GlPagination } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
+import getMRCodequalityAndSecurityReports from '~/diffs/components/graphql/get_mr_codequality_and_security_reports.query.graphql';
+import createMockApollo from 'helpers/mock_apollo_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
import { TEST_HOST } from 'spec/test_constants';
@@ -19,11 +22,14 @@ import CollapsedFilesWarning from '~/diffs/components/collapsed_files_warning.vu
import HiddenFilesWarning from '~/diffs/components/hidden_files_warning.vue';
import eventHub from '~/diffs/event_hub';
+import { EVT_DISCUSSIONS_ASSIGNED } from '~/diffs/constants';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { Mousetrap } from '~/lib/mousetrap';
import * as urlUtils from '~/lib/utils/url_utility';
+import * as commonUtils from '~/lib/utils/common_utils';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { stubPerformanceWebAPI } from 'helpers/performance';
import createDiffsStore from '../create_diffs_store';
import diffsMockData from '../mock_data/merge_request_diffs';
@@ -34,6 +40,7 @@ const COMMIT_URL = `${TEST_HOST}/COMMIT/OLD`;
const UPDATED_COMMIT_URL = `${TEST_HOST}/COMMIT/NEW`;
Vue.use(Vuex);
+Vue.use(VueApollo);
Vue.config.ignoredElements = ['copy-code'];
@@ -46,12 +53,19 @@ describe('diffs/components/app', () => {
let store;
let wrapper;
let mock;
+ let fakeApollo;
+
+ const codeQualityAndSastQueryHandlerSuccess = jest.fn().mockResolvedValue({});
function createComponent(props = {}, extendStore = () => {}, provisions = {}, baseConfig = {}) {
+ fakeApollo = createMockApollo([
+ [getMRCodequalityAndSecurityReports, codeQualityAndSastQueryHandlerSuccess],
+ ]);
+
const provide = {
...provisions,
glFeatures: {
- ...(provisions.glFeatures || {}),
+ ...provisions.glFeatures,
},
};
@@ -74,10 +88,11 @@ describe('diffs/components/app', () => {
});
wrapper = shallowMount(App, {
+ apolloProvider: fakeApollo,
propsData: {
endpointCoverage: `${TEST_HOST}/diff/endpointCoverage`,
endpointCodequality: '',
- endpointSast: '',
+ sastReportAvailable: false,
projectPath: 'namespace/project',
currentUser: {},
changesEmptyStateIllustration: '',
@@ -120,8 +135,6 @@ describe('diffs/components/app', () => {
jest.spyOn(wrapper.vm, 'fetchDiffFilesBatch').mockImplementation(fetchResolver);
jest.spyOn(wrapper.vm, 'fetchCoverageFiles').mockImplementation(fetchResolver);
jest.spyOn(wrapper.vm, 'setDiscussions').mockImplementation(() => {});
- jest.spyOn(wrapper.vm, 'unwatchDiscussions').mockImplementation(() => {});
- jest.spyOn(wrapper.vm, 'unwatchRetrievingBatches').mockImplementation(() => {});
store.state.diffs.retrievingBatches = true;
store.state.diffs.diffFiles = [];
return nextTick();
@@ -136,9 +149,7 @@ describe('diffs/components/app', () => {
expect(wrapper.vm.fetchDiffFilesMeta).toHaveBeenCalled();
expect(wrapper.vm.fetchDiffFilesBatch).toHaveBeenCalled();
expect(wrapper.vm.fetchCoverageFiles).toHaveBeenCalled();
- expect(wrapper.vm.unwatchDiscussions).toHaveBeenCalled();
expect(wrapper.vm.diffFilesLength).toBe(100);
- expect(wrapper.vm.unwatchRetrievingBatches).toHaveBeenCalled();
});
it('calls batch methods if diffsBatchLoad is enabled, and latest version', async () => {
@@ -150,9 +161,7 @@ describe('diffs/components/app', () => {
expect(wrapper.vm.fetchDiffFilesMeta).toHaveBeenCalled();
expect(wrapper.vm.fetchDiffFilesBatch).toHaveBeenCalled();
expect(wrapper.vm.fetchCoverageFiles).toHaveBeenCalled();
- expect(wrapper.vm.unwatchDiscussions).toHaveBeenCalled();
expect(wrapper.vm.diffFilesLength).toBe(100);
- expect(wrapper.vm.unwatchRetrievingBatches).toHaveBeenCalled();
});
});
@@ -187,16 +196,14 @@ describe('diffs/components/app', () => {
wrapper.vm.fetchData(false);
expect(wrapper.vm.fetchCodequality).not.toHaveBeenCalled();
+ expect(codeQualityAndSastQueryHandlerSuccess).not.toHaveBeenCalled();
});
});
describe('SAST diff', () => {
it('does not fetch Sast data on FOSS', () => {
createComponent();
- jest.spyOn(wrapper.vm, 'fetchSast');
- wrapper.vm.fetchData(false);
-
- expect(wrapper.vm.fetchSast).not.toHaveBeenCalled();
+ expect(codeQualityAndSastQueryHandlerSuccess).not.toHaveBeenCalled();
});
});
@@ -652,6 +659,12 @@ describe('diffs/components/app', () => {
});
describe('file-by-file', () => {
+ let hashSpy;
+
+ beforeEach(() => {
+ hashSpy = jest.spyOn(commonUtils, 'handleLocationHash');
+ });
+
it('renders a single diff', async () => {
createComponent(
undefined,
@@ -671,6 +684,48 @@ describe('diffs/components/app', () => {
expect(wrapper.findAllComponents(DiffFile).length).toBe(1);
});
+ describe('rechecking the url hash for scrolling', () => {
+ const advanceAndCheckCalls = (count = 0) => {
+ jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
+ expect(hashSpy).toHaveBeenCalledTimes(count);
+ };
+
+ it('re-checks one time after the file finishes loading', () => {
+ createComponent(
+ undefined,
+ ({ state }) => {
+ state.diffs.diffFiles = [{ isLoadingFullFile: true }];
+ },
+ undefined,
+ { viewDiffsFileByFile: true },
+ );
+
+ // The hash check is not called if the file is still marked as loading
+ expect(hashSpy).toHaveBeenCalledTimes(0);
+ eventHub.$emit(EVT_DISCUSSIONS_ASSIGNED);
+ advanceAndCheckCalls();
+ eventHub.$emit(EVT_DISCUSSIONS_ASSIGNED);
+ advanceAndCheckCalls();
+ // Once the file has finished loading, it calls through to check the hash
+ store.state.diffs.diffFiles[0].isLoadingFullFile = false;
+ eventHub.$emit(EVT_DISCUSSIONS_ASSIGNED);
+ advanceAndCheckCalls(1);
+ // No further scrolls happen after one hash check / scroll
+ eventHub.$emit(EVT_DISCUSSIONS_ASSIGNED);
+ advanceAndCheckCalls(1);
+ eventHub.$emit(EVT_DISCUSSIONS_ASSIGNED);
+ advanceAndCheckCalls(1);
+ });
+
+ it('does not re-check when not in single-file mode', () => {
+ createComponent();
+
+ eventHub.$emit(EVT_DISCUSSIONS_ASSIGNED);
+
+ expect(hashSpy).not.toHaveBeenCalled();
+ });
+ });
+
describe('pagination', () => {
const fileByFileNav = () => wrapper.find('[data-testid="file-by-file-navigation"]');
const paginator = () => fileByFileNav().findComponent(GlPagination);
@@ -769,6 +824,15 @@ describe('diffs/components/app', () => {
beforeEach(() => {
createComponent();
+
+ store.state.diffs.diffFiles = [
+ {
+ file_hash: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a',
+ highlighted_diff_lines: [],
+ viewer: { manuallyCollapsed: true },
+ },
+ ];
+
loadSpy = jest.spyOn(wrapper.vm, 'loadCollapsedDiff').mockResolvedValue('resolved');
});
@@ -787,5 +851,14 @@ describe('diffs/components/app', () => {
expect(loadSpy).toHaveBeenCalledWith({ file: store.state.diffs.diffFiles[0] });
});
+
+ it('does nothing when file is not collapsed', () => {
+ store.state.diffs.diffFiles[0].viewer.manuallyCollapsed = false;
+ window.location.hash = '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_0_1';
+
+ eventHub.$emit('doneLoadingBatches');
+
+ expect(loadSpy).not.toHaveBeenCalledWith({ file: store.state.diffs.diffFiles[0] });
+ });
});
});
diff --git a/spec/frontend/diffs/components/diff_file_header_spec.js b/spec/frontend/diffs/components/diff_file_header_spec.js
index b0d98e0e4a6..d6539a5bffa 100644
--- a/spec/frontend/diffs/components/diff_file_header_spec.js
+++ b/spec/frontend/diffs/components/diff_file_header_spec.js
@@ -103,7 +103,7 @@ describe('DiffFileHeader component', () => {
const createComponent = ({ props, options = {} } = {}) => {
mockStoreConfig = cloneDeep(defaultMockStoreConfig);
- const store = new Vuex.Store({ ...mockStoreConfig, ...(options.store || {}) });
+ const store = new Vuex.Store({ ...mockStoreConfig, ...options.store });
wrapper = shallowMount(DiffFileHeader, {
propsData: {
diff --git a/spec/frontend/diffs/components/diff_file_spec.js b/spec/frontend/diffs/components/diff_file_spec.js
index 13efd3584b4..34af3d72b04 100644
--- a/spec/frontend/diffs/components/diff_file_spec.js
+++ b/spec/frontend/diffs/components/diff_file_spec.js
@@ -13,6 +13,7 @@ import DiffFileComponent from '~/diffs/components/diff_file.vue';
import DiffFileHeaderComponent from '~/diffs/components/diff_file_header.vue';
import {
+ EVT_DISCUSSIONS_ASSIGNED,
EVT_EXPAND_ALL_FILES,
EVT_PERF_MARK_DIFF_FILES_END,
EVT_PERF_MARK_FIRST_DIFF_FILE_SHOWN,
@@ -271,9 +272,10 @@ describe('DiffFile', () => {
await nextTick(); // Wait for the idleCallback
await nextTick(); // Wait for nextTick inside postRender
- expect(eventHub.$emit).toHaveBeenCalledTimes(2);
+ expect(eventHub.$emit).toHaveBeenCalledTimes(3);
expect(eventHub.$emit).toHaveBeenCalledWith(EVT_PERF_MARK_FIRST_DIFF_FILE_SHOWN);
expect(eventHub.$emit).toHaveBeenCalledWith(EVT_PERF_MARK_DIFF_FILES_END);
+ expect(eventHub.$emit).toHaveBeenCalledWith(EVT_DISCUSSIONS_ASSIGNED);
});
});
});
diff --git a/spec/frontend/diffs/components/shared/__snapshots__/findings_drawer_spec.js.snap b/spec/frontend/diffs/components/shared/__snapshots__/findings_drawer_spec.js.snap
index afa2a7d9678..cfc34bd2f25 100644
--- a/spec/frontend/diffs/components/shared/__snapshots__/findings_drawer_spec.js.snap
+++ b/spec/frontend/diffs/components/shared/__snapshots__/findings_drawer_spec.js.snap
@@ -1,111 +1,220 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FindingsDrawer matches the snapshot 1`] = `
-<gl-drawer-stub
+<transition-stub
class="findings-drawer"
- headerheight=""
- open="true"
- variant="default"
- zindex="252"
+ name="gl-drawer"
>
- <h2
- class="gl-font-size-h2 gl-mb-0 gl-mt-0"
- data-testid="findings-drawer-heading"
+ <aside
+ class="gl-drawer gl-drawer-default"
+ style="top: 0px; z-index: 252;"
>
- Unused method argument - \`c\`. If it's necessary, use \`_\` or \`_c\` as an argument name to indicate that it won't be used.
- </h2>
- <ul
- class="gl-border-b-initial gl-list-style-none gl-mb-0 gl-pb-0!"
- >
- <li
- class="gl-mb-4"
- data-testid="findings-drawer-severity"
- >
- <span
- class="gl-font-weight-bold"
- >
- Severity:
- </span>
- <gl-icon-stub
- class="gl-text-orange-300 inline-findings-severity-icon"
- data-testid="findings-drawer-severity-icon"
- name="severity-low"
- size="12"
- />
- minor
- </li>
- <li
- class="gl-mb-4"
- data-testid="findings-drawer-engine"
- >
- <span
- class="gl-font-weight-bold"
- >
- Engine:
- </span>
- testengine name
- </li>
- <li
- class="gl-mb-4"
- data-testid="findings-drawer-category"
+ <div
+ class="gl-drawer-header"
>
- <span
- class="gl-font-weight-bold"
+ <div
+ class="gl-drawer-title"
>
- Category:
- </span>
- testcategory 1
- </li>
- <li
- class="gl-mb-4"
- data-testid="findings-drawer-other-locations"
+ <h2
+ class="drawer-heading gl-font-base gl-mb-0 gl-mt-0"
+ >
+ <svg
+ aria-hidden="true"
+ class="gl-icon gl-text-orange-300 gl-vertical-align-baseline! inline-findings-severity-icon s12"
+ data-testid="severity-low-icon"
+ role="img"
+ >
+ <use
+ href="file-mock#severity-low"
+ />
+ </svg>
+ <span
+ class="drawer-heading-severity"
+ >
+ low
+ </span>
+ SAST Finding
+ </h2>
+ <button
+ aria-label="Close drawer"
+ class="btn btn-default btn-default-tertiary btn-icon btn-sm gl-button gl-drawer-close-button"
+ type="button"
+ >
+ <svg
+ aria-hidden="true"
+ class="gl-button-icon gl-icon s16"
+ data-testid="close-icon"
+ role="img"
+ >
+ <use
+ href="file-mock#close"
+ />
+ </svg>
+ </button>
+ </div>
+ </div>
+ <div
+ class="gl-drawer-body gl-drawer-body-scrim"
>
- <span
- class="gl-display-block gl-font-weight-bold gl-mb-3"
- >
- Other locations:
- </span>
<ul
- class="gl-pl-6"
+ class="gl-border-b-initial gl-list-style-none gl-mb-0 gl-pb-0!"
>
<li
- class="gl-mb-1"
+ class="gl-mb-4"
+ >
+ <p
+ class="gl-line-height-20"
+ >
+ <span
+ class="gl-display-block gl-font-weight-bold gl-mb-1"
+ data-testid="findings-drawer-item-description"
+ >
+ Name
+ </span>
+ <span
+ data-testid="findings-drawer-item-value-prop"
+ >
+ mockedtitle
+ </span>
+ </p>
+ </li>
+ <li
+ class="gl-mb-4"
+ >
+ <p
+ class="gl-line-height-20"
+ >
+ <span
+ class="gl-display-block gl-font-weight-bold gl-mb-1"
+ data-testid="findings-drawer-item-description"
+ >
+ Status
+ </span>
+ <span
+ class="badge badge-pill badge-warning gl-badge md text-capitalize"
+ >
+ detected
+ </span>
+ </p>
+ </li>
+ <li
+ class="gl-mb-4"
+ >
+ <p
+ class="gl-line-height-20"
+ >
+ <span
+ class="gl-display-block gl-font-weight-bold gl-mb-1"
+ data-testid="findings-drawer-item-description"
+ >
+ Description
+ </span>
+ <span
+ data-testid="findings-drawer-item-value-prop"
+ >
+ fakedesc
+ </span>
+ </p>
+ </li>
+ <li
+ class="gl-mb-4"
+ >
+ <p
+ class="gl-line-height-20"
+ >
+ <span
+ class="gl-display-block gl-font-weight-bold gl-mb-1"
+ data-testid="findings-drawer-item-description"
+ >
+ Project
+ </span>
+ <a
+ class="gl-link"
+ href="/testpath"
+ >
+ testname
+ </a>
+ </p>
+ </li>
+ <li
+ class="gl-mb-4"
+ >
+ <p
+ class="gl-line-height-20"
+ >
+ <span
+ class="gl-display-block gl-font-weight-bold gl-mb-1"
+ data-testid="findings-drawer-item-description"
+ >
+ File
+ </span>
+ <span
+ data-testid="findings-drawer-item-value-prop"
+ />
+ </p>
+ </li>
+ <li
+ class="gl-mb-4"
>
- <gl-link-stub
- data-testid="findings-drawer-other-locations-link"
- href="http://testlink.com"
+ <p
+ class="gl-line-height-20"
>
- testpath
- </gl-link-stub>
+ <span
+ class="gl-display-block gl-font-weight-bold gl-mb-1"
+ data-testid="findings-drawer-item-description"
+ >
+ Identifiers
+ </span>
+ <span>
+ <a
+ class="gl-link"
+ href="https://semgrep.dev/r/gitlab.eslint.detect-disable-mustache-escape"
+ >
+ eslint.detect-disable-mustache-escape
+ </a>
+ </span>
+ </p>
</li>
<li
- class="gl-mb-1"
+ class="gl-mb-4"
>
- <gl-link-stub
- data-testid="findings-drawer-other-locations-link"
- href="http://testlink.com"
+ <p
+ class="gl-line-height-20"
>
- testpath 1
- </gl-link-stub>
+ <span
+ class="gl-display-block gl-font-weight-bold gl-mb-1"
+ data-testid="findings-drawer-item-description"
+ >
+ Tool
+ </span>
+ <span
+ data-testid="findings-drawer-item-value-prop"
+ >
+ SAST
+ </span>
+ </p>
</li>
<li
- class="gl-mb-1"
+ class="gl-mb-4"
>
- <gl-link-stub
- data-testid="findings-drawer-other-locations-link"
- href="http://testlink.com"
+ <p
+ class="gl-line-height-20"
>
- testpath2
- </gl-link-stub>
+ <span
+ class="gl-display-block gl-font-weight-bold gl-mb-1"
+ data-testid="findings-drawer-item-description"
+ >
+ Engine
+ </span>
+ <span
+ data-testid="findings-drawer-item-value-prop"
+ >
+ testengine name
+ </span>
+ </p>
</li>
</ul>
- </li>
- </ul>
- <span
- class="drawer-body gl-display-block gl-px-3 gl-py-0!"
- data-testid="findings-drawer-body"
- >
- Duplicated Code Duplicated code
- </span>
-</gl-drawer-stub>
+ </div>
+ </aside>
+</transition-stub>
`;
diff --git a/spec/frontend/diffs/components/shared/findings_drawer_item_spec.js b/spec/frontend/diffs/components/shared/findings_drawer_item_spec.js
new file mode 100644
index 00000000000..80087ea66a2
--- /dev/null
+++ b/spec/frontend/diffs/components/shared/findings_drawer_item_spec.js
@@ -0,0 +1,54 @@
+import FindingsDrawerItem from '~/diffs/components/shared/findings_drawer_item.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+let wrapper;
+
+const mockDescription = 'testDescription';
+const slotTestId = 'findings-drawer-item-value-slot';
+const mockValue = 'testValue';
+const mockSlot = `<span data-testid="${slotTestId}">mockSlot</span>`;
+const mockSlotText = 'mockSlot';
+
+describe('FindingsDrawerItem', () => {
+ const description = () => wrapper.findByTestId('findings-drawer-item-description');
+
+ const valueSlot = () => wrapper.findByTestId(slotTestId);
+ const valueProp = () => wrapper.findByTestId('findings-drawer-item-value-prop');
+
+ const createWrapper = (props = {}, slots = {}) => {
+ return shallowMountExtended(FindingsDrawerItem, {
+ propsData: {
+ ...props,
+ },
+ slots: {
+ ...slots,
+ },
+ });
+ };
+
+ it('renders with default values', () => {
+ wrapper = createWrapper();
+ expect(description().text()).toContain('');
+ expect(valueProp().text()).toContain('');
+ });
+
+ it('renders description and value props correctly', () => {
+ wrapper = createWrapper({ description: mockDescription, value: mockValue });
+ expect(description().text()).toContain(mockDescription);
+ expect(valueProp().text()).toContain(mockValue);
+ });
+
+ describe('when slot content is passed', () => {
+ it('renders slot content', () => {
+ wrapper = createWrapper({}, { value: mockSlot });
+ expect(valueSlot().text()).toContain(mockSlotText);
+ });
+
+ describe('when value prop is passed', () => {
+ it('does not render value prop', () => {
+ wrapper = createWrapper({ value: mockValue }, { value: mockSlot });
+ expect(valueProp().exists()).toBe(false);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/diffs/components/shared/findings_drawer_spec.js b/spec/frontend/diffs/components/shared/findings_drawer_spec.js
index 0af6e0f0e96..62d875ed9b7 100644
--- a/spec/frontend/diffs/components/shared/findings_drawer_spec.js
+++ b/spec/frontend/diffs/components/shared/findings_drawer_spec.js
@@ -1,16 +1,33 @@
+import { GlDrawer } from '@gitlab/ui';
import FindingsDrawer from '~/diffs/components/shared/findings_drawer.vue';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import mockFinding from '../../mock_data/findings_drawer';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { mockFinding, mockProject } from '../../mock_data/findings_drawer';
let wrapper;
+const getDrawer = () => wrapper.findComponent(GlDrawer);
+const closeEvent = 'close';
+
+const createWrapper = () => {
+ return mountExtended(FindingsDrawer, {
+ propsData: {
+ drawer: mockFinding,
+ project: mockProject,
+ },
+ });
+};
+
describe('FindingsDrawer', () => {
- const createWrapper = () => {
- return shallowMountExtended(FindingsDrawer, {
- propsData: {
- drawer: mockFinding,
- },
- });
- };
+ it('renders without errors', () => {
+ wrapper = createWrapper();
+ expect(wrapper.exists()).toBe(true);
+ });
+
+ it('emits close event when gl-drawer emits close event', () => {
+ wrapper = createWrapper();
+
+ getDrawer().vm.$emit(closeEvent);
+ expect(wrapper.emitted(closeEvent)).toHaveLength(1);
+ });
it('matches the snapshot', () => {
wrapper = createWrapper();
diff --git a/spec/frontend/diffs/mock_data/findings_drawer.js b/spec/frontend/diffs/mock_data/findings_drawer.js
index d7e7e957c83..4823a18b267 100644
--- a/spec/frontend/diffs/mock_data/findings_drawer.js
+++ b/spec/frontend/diffs/mock_data/findings_drawer.js
@@ -1,21 +1,28 @@
-export default {
+export const mockFinding = {
+ title: 'mockedtitle',
+ state: 'detected',
+ scale: 'sast',
line: 7,
- description:
- "Unused method argument - `c`. If it's necessary, use `_` or `_c` as an argument name to indicate that it won't be used.",
- severity: 'minor',
+ description: 'fakedesc',
+ severity: 'low',
engineName: 'testengine name',
categories: ['testcategory 1', 'testcategory 2'],
content: {
body: 'Duplicated Code Duplicated code',
},
- location: {
- path: 'workhorse/config_test.go',
- lines: { begin: 221, end: 284 },
- },
- otherLocations: [
- { path: 'testpath', href: 'http://testlink.com' },
- { path: 'testpath 1', href: 'http://testlink.com' },
- { path: 'testpath2', href: 'http://testlink.com' },
+ webUrl: {},
+ identifiers: [
+ {
+ __typename: 'VulnerabilityIdentifier',
+ externalId: 'eslint.detect-disable-mustache-escape',
+ externalType: 'semgrep_id',
+ name: 'eslint.detect-disable-mustache-escape',
+ url: 'https://semgrep.dev/r/gitlab.eslint.detect-disable-mustache-escape',
+ },
],
- type: 'issue',
+};
+
+export const mockProject = {
+ nameWithNamespace: 'testname',
+ fullPath: 'testpath',
};
diff --git a/spec/frontend/diffs/store/actions_spec.js b/spec/frontend/diffs/store/actions_spec.js
index 18e81232b5c..8cf376b13e3 100644
--- a/spec/frontend/diffs/store/actions_spec.js
+++ b/spec/frontend/diffs/store/actions_spec.js
@@ -521,7 +521,7 @@ describe('DiffsStoreActions', () => {
return testAction(
diffActions.fetchDiffFilesBatch,
{},
- { endpointBatch, diffViewType: 'inline', diffFiles: [] },
+ { endpointBatch, diffViewType: 'inline', diffFiles: [], perPage: 5 },
[
{ type: types.SET_BATCH_LOADING_STATE, payload: 'loading' },
{ type: types.SET_RETRIEVING_BATCHES, payload: true },
@@ -841,7 +841,7 @@ describe('DiffsStoreActions', () => {
};
const singleDiscussion = {
id: '1',
- file_hash: 'ABC',
+ diff_file: { file_hash: 'ABC' },
line_code: 'ABC_1_1',
};
diff --git a/spec/frontend/diffs/store/utils_spec.js b/spec/frontend/diffs/store/utils_spec.js
index 720b72f4965..6331269d6e8 100644
--- a/spec/frontend/diffs/store/utils_spec.js
+++ b/spec/frontend/diffs/store/utils_spec.js
@@ -330,7 +330,7 @@ describe('DiffsStoreUtils', () => {
old_line: 5,
new_line: 5,
rich_text: '<p>rich</p>', // Note no leading space
- discussionsExpanded: true,
+ discussionsExpanded: false,
discussions: [],
hasForm: false,
text: undefined,
diff --git a/spec/frontend/diffs/utils/sort_errors_by_file_spec.js b/spec/frontend/diffs/utils/sort_errors_by_file_spec.js
deleted file mode 100644
index ca8a8ec3516..00000000000
--- a/spec/frontend/diffs/utils/sort_errors_by_file_spec.js
+++ /dev/null
@@ -1,52 +0,0 @@
-import { sortFindingsByFile } from '~/diffs/utils/sort_findings_by_file';
-
-describe('sort_findings_by_file utilities', () => {
- const mockDescription = 'mockDescription';
- const mockSeverity = 'mockseverity';
- const mockLine = '00';
- const mockFile1 = 'file1.js';
- const mockFile2 = 'file2.rb';
- const emptyResponse = {
- files: {},
- };
-
- const unsortedFindings = [
- {
- severity: mockSeverity,
- filePath: mockFile1,
- line: mockLine,
- description: mockDescription,
- },
- {
- severity: mockSeverity,
- filePath: mockFile2,
- line: mockLine,
- description: mockDescription,
- },
- ];
- const sortedFindings = {
- files: {
- [mockFile1]: [
- {
- line: mockLine,
- description: mockDescription,
- severity: mockSeverity,
- },
- ],
- [mockFile2]: [
- {
- line: mockLine,
- description: mockDescription,
- severity: mockSeverity,
- },
- ],
- },
- };
-
- it('sorts Findings correctly', () => {
- expect(sortFindingsByFile(unsortedFindings)).toEqual(sortedFindings);
- });
- it('does not throw error when given no input', () => {
- expect(sortFindingsByFile()).toEqual(emptyResponse);
- });
-});
diff --git a/spec/frontend/diffs/utils/sort_findings_by_file_spec.js b/spec/frontend/diffs/utils/sort_findings_by_file_spec.js
new file mode 100644
index 00000000000..8dc4f57d98c
--- /dev/null
+++ b/spec/frontend/diffs/utils/sort_findings_by_file_spec.js
@@ -0,0 +1,66 @@
+import { sortFindingsByFile } from '~/diffs/utils/sort_findings_by_file';
+
+describe('sort_findings_by_file utilities', () => {
+ const mockDescription = 'mockDescription';
+ const mockSeverity = 'mockseverity';
+ const mockLine = '00';
+ const mockFile1 = 'file1.js';
+ const mockFile2 = 'file2.rb';
+ const webUrl1 = 'http://example.com/file1.js';
+ const webUrl2 = 'http://example.com/file2.rb';
+ const engineName1 = 'engineName1';
+ const engineName2 = 'engineName2';
+ const emptyResponse = {
+ files: {},
+ };
+
+ const unsortedFindings = [
+ {
+ severity: mockSeverity,
+ filePath: mockFile1,
+ line: mockLine,
+ description: mockDescription,
+ webUrl: webUrl1,
+ engineName: engineName1,
+ },
+ {
+ severity: mockSeverity,
+ filePath: mockFile2,
+ line: mockLine,
+ description: mockDescription,
+ webUrl: webUrl2,
+ engineName: engineName2,
+ },
+ ];
+ const sortedFindings = {
+ files: {
+ [mockFile1]: [
+ {
+ line: mockLine,
+ filePath: mockFile1,
+ description: mockDescription,
+ severity: mockSeverity,
+ webUrl: webUrl1,
+ engineName: engineName1,
+ },
+ ],
+ [mockFile2]: [
+ {
+ line: mockLine,
+ filePath: mockFile2,
+ description: mockDescription,
+ severity: mockSeverity,
+ webUrl: webUrl2,
+ engineName: engineName2,
+ },
+ ],
+ },
+ };
+
+ it('sorts Findings correctly', () => {
+ expect(sortFindingsByFile(unsortedFindings)).toEqual(sortedFindings);
+ });
+ it('does not throw error when given no input', () => {
+ expect(sortFindingsByFile()).toEqual(emptyResponse);
+ });
+});
diff --git a/spec/frontend/emoji/awards_app/store/actions_spec.js b/spec/frontend/emoji/awards_app/store/actions_spec.js
index 65f2e813f19..23aa7bd5ad7 100644
--- a/spec/frontend/emoji/awards_app/store/actions_spec.js
+++ b/spec/frontend/emoji/awards_app/store/actions_spec.js
@@ -1,11 +1,11 @@
-import * as Sentry from '@sentry/browser';
import MockAdapter from 'axios-mock-adapter';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import testAction from 'helpers/vuex_action_helper';
import * as actions from '~/emoji/awards_app/store/actions';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
-jest.mock('@sentry/browser');
+jest.mock('~/sentry/sentry_browser_wrapper');
jest.mock('~/vue_shared/plugins/global_toast');
describe('Awards app actions', () => {
diff --git a/spec/frontend/emoji/index_spec.js b/spec/frontend/emoji/index_spec.js
index 1a12bd303f1..7d6a45fbf30 100644
--- a/spec/frontend/emoji/index_spec.js
+++ b/spec/frontend/emoji/index_spec.js
@@ -1,10 +1,11 @@
+import MockAdapter from 'axios-mock-adapter';
import {
emojiFixtureMap,
- mockEmojiData,
initEmojiMock,
validEmoji,
invalidEmoji,
clearEmojiMock,
+ mockEmojiData,
} from 'helpers/emoji';
import { trimText } from 'helpers/text_helper';
import { createMockClient } from 'helpers/mock_apollo_helper';
@@ -14,9 +15,10 @@ import {
getEmojiInfo,
sortEmoji,
initEmojiMap,
- getAllEmoji,
+ getEmojiMap,
emojiFallbackImageSrc,
loadCustomEmojiWithNames,
+ EMOJI_VERSION,
} from '~/emoji';
import isEmojiUnicodeSupported, {
@@ -27,8 +29,11 @@ import isEmojiUnicodeSupported, {
isHorceRacingSkinToneComboEmoji,
isPersonZwjEmoji,
} from '~/emoji/support/is_emoji_unicode_supported';
-import { NEUTRAL_INTENT_MULTIPLIER } from '~/emoji/constants';
+import { CACHE_KEY, CACHE_VERSION_KEY, NEUTRAL_INTENT_MULTIPLIER } from '~/emoji/constants';
import customEmojiQuery from '~/emoji/queries/custom_emoji.query.graphql';
+import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
+import { useLocalStorageSpy } from 'jest/__helpers__/local_storage_helper';
let mockClient;
jest.mock('~/lib/graphql', () => {
@@ -75,6 +80,195 @@ function createMockEmojiClient() {
document.body.dataset.groupFullPath = 'test-group';
}
+describe('retrieval of emojis.json', () => {
+ useLocalStorageSpy();
+
+ let mock;
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ mock.onGet(/emojis\.json$/).reply(HTTP_STATUS_OK, mockEmojiData);
+ initEmojiMap.promise = null;
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ const assertCorrectLocalStorage = () => {
+ expect(localStorage.length).toBe(1);
+ expect(localStorage.getItem(CACHE_KEY)).toBe(
+ JSON.stringify({ data: mockEmojiData, EMOJI_VERSION }),
+ );
+ };
+
+ const assertEmojiBeingLoadedCorrectly = () => {
+ expect(Object.keys(getEmojiMap())).toEqual(Object.keys(validEmoji));
+ };
+
+ it('should remove the old `CACHE_VERSION_KEY`', async () => {
+ localStorage.setItem(CACHE_VERSION_KEY, EMOJI_VERSION);
+
+ await initEmojiMap();
+
+ expect(localStorage.getItem(CACHE_VERSION_KEY)).toBe(null);
+ });
+
+ describe('when the localStorage is empty', () => {
+ it('should call the API and store results in localStorage', async () => {
+ await initEmojiMap();
+
+ assertEmojiBeingLoadedCorrectly();
+ expect(mock.history.get.length).toBe(1);
+ assertCorrectLocalStorage();
+ });
+ });
+
+ describe('when the localStorage stores the correct version', () => {
+ beforeEach(async () => {
+ localStorage.setItem(CACHE_KEY, JSON.stringify({ data: mockEmojiData, EMOJI_VERSION }));
+ localStorage.setItem.mockClear();
+ await initEmojiMap();
+ });
+
+ it('should not call the API and not mutate the localStorage', () => {
+ assertEmojiBeingLoadedCorrectly();
+ expect(mock.history.get.length).toBe(0);
+ expect(localStorage.setItem).not.toHaveBeenCalled();
+ assertCorrectLocalStorage();
+ });
+ });
+
+ describe('when the localStorage stores an incorrect version', () => {
+ beforeEach(async () => {
+ localStorage.setItem(
+ CACHE_KEY,
+ JSON.stringify({ data: mockEmojiData, EMOJI_VERSION: `${EMOJI_VERSION}-different` }),
+ );
+ localStorage.setItem.mockClear();
+ await initEmojiMap();
+ });
+
+ it('should call the API and store results in localStorage', () => {
+ assertEmojiBeingLoadedCorrectly();
+ expect(mock.history.get.length).toBe(1);
+ assertCorrectLocalStorage();
+ });
+ });
+
+ describe('when the localStorage stores corrupted data', () => {
+ beforeEach(async () => {
+ localStorage.setItem(CACHE_KEY, "[invalid: 'INVALID_JSON");
+ localStorage.setItem.mockClear();
+ await initEmojiMap();
+ });
+
+ it('should call the API and store results in localStorage', () => {
+ assertEmojiBeingLoadedCorrectly();
+ expect(mock.history.get.length).toBe(1);
+ assertCorrectLocalStorage();
+ });
+ });
+
+ describe('when the localStorage stores data in a different format', () => {
+ beforeEach(async () => {
+ localStorage.setItem(CACHE_KEY, JSON.stringify([]));
+ localStorage.setItem.mockClear();
+ await initEmojiMap();
+ });
+
+ it('should call the API and store results in localStorage', () => {
+ assertEmojiBeingLoadedCorrectly();
+ expect(mock.history.get.length).toBe(1);
+ assertCorrectLocalStorage();
+ });
+ });
+
+ describe('when the localStorage is full', () => {
+ beforeEach(async () => {
+ const oldSetItem = localStorage.setItem;
+ localStorage.setItem = jest.fn().mockImplementationOnce((key, value) => {
+ if (key === CACHE_KEY) {
+ throw new Error('Storage Full');
+ }
+ oldSetItem(key, value);
+ });
+ await initEmojiMap();
+ });
+
+ it('should call API but not store the results', () => {
+ assertEmojiBeingLoadedCorrectly();
+ expect(mock.history.get.length).toBe(1);
+ expect(localStorage.length).toBe(0);
+ expect(localStorage.setItem).toHaveBeenCalledTimes(1);
+ expect(localStorage.setItem).toHaveBeenCalledWith(
+ CACHE_KEY,
+ JSON.stringify({ data: mockEmojiData, EMOJI_VERSION }),
+ );
+ });
+ });
+
+ describe('backwards compatibility', () => {
+ // As per: https://gitlab.com/gitlab-org/gitlab/-/blob/62b66abd3bb7801e7c85b4e42a1bbd51fbb37c1b/app/assets/javascripts/emoji/index.js#L27-52
+ async function prevImplementation() {
+ if (
+ window.localStorage.getItem(CACHE_VERSION_KEY) === EMOJI_VERSION &&
+ window.localStorage.getItem(CACHE_KEY)
+ ) {
+ return JSON.parse(window.localStorage.getItem(CACHE_KEY));
+ }
+
+ // We load the JSON file direct from the server
+ // because it can't be loaded from a CDN due to
+ // cross domain problems with JSON
+ const { data } = await axios.get(
+ `${gon.relative_url_root || ''}/-/emojis/${EMOJI_VERSION}/emojis.json`,
+ );
+
+ try {
+ window.localStorage.setItem(CACHE_VERSION_KEY, EMOJI_VERSION);
+ window.localStorage.setItem(CACHE_KEY, JSON.stringify(data));
+ } catch {
+ // Setting data in localstorage may fail when storage quota is exceeded.
+ // We should continue even when this fails.
+ }
+
+ return data;
+ }
+
+ it('Old -> New -> Old should not break', async () => {
+ // The follow steps simulate a multi-version deployment. e.g.
+ // Hitting a page on "regular" .com, then canary, and then "regular" again
+
+ // Load emoji the old way to pre-populate the cache
+ let res = await prevImplementation();
+ expect(res).toEqual(mockEmojiData);
+ expect(mock.history.get.length).toBe(1);
+ localStorage.setItem.mockClear();
+
+ // Load emoji the new way
+ await initEmojiMap();
+ expect(mock.history.get.length).toBe(2);
+ assertEmojiBeingLoadedCorrectly();
+ assertCorrectLocalStorage();
+ localStorage.setItem.mockClear();
+
+ // Load emoji the old way to pre-populate the cache
+ res = await prevImplementation();
+ expect(res).toEqual(mockEmojiData);
+ expect(mock.history.get.length).toBe(3);
+ expect(localStorage.setItem.mock.calls).toEqual([
+ [CACHE_VERSION_KEY, EMOJI_VERSION],
+ [CACHE_KEY, JSON.stringify(mockEmojiData)],
+ ]);
+
+ // Load emoji the old way should work again (and be taken from the cache)
+ res = await prevImplementation();
+ expect(res).toEqual(mockEmojiData);
+ expect(mock.history.get.length).toBe(3);
+ });
+ });
+});
+
describe('emoji', () => {
beforeEach(async () => {
await initEmojiMock();
@@ -90,7 +284,7 @@ describe('emoji', () => {
it('should contain valid emoji', async () => {
await initEmojiMap();
- const allEmoji = Object.keys(getAllEmoji());
+ const allEmoji = Object.keys(getEmojiMap());
Object.keys(validEmoji).forEach((key) => {
expect(allEmoji.includes(key)).toBe(true);
});
@@ -99,34 +293,11 @@ describe('emoji', () => {
it('should not contain invalid emoji', async () => {
await initEmojiMap();
- const allEmoji = Object.keys(getAllEmoji());
+ const allEmoji = Object.keys(getEmojiMap());
Object.keys(invalidEmoji).forEach((key) => {
expect(allEmoji.includes(key)).toBe(false);
});
});
-
- it('fixes broken pride emoji', async () => {
- clearEmojiMock();
- await initEmojiMock({
- gay_pride_flag: {
- c: 'flags',
- // Without a zero-width joiner
- e: '🏳🌈',
- name: 'gay_pride_flag',
- u: '6.0',
- },
- });
-
- expect(getAllEmoji()).toEqual({
- gay_pride_flag: {
- c: 'flags',
- // With a zero-width joiner
- e: '🏳️‍🌈',
- name: 'gay_pride_flag',
- u: '6.0',
- },
- });
- });
});
describe('glEmojiTag', () => {
@@ -448,11 +619,11 @@ describe('emoji', () => {
describe('getEmojiInfo', () => {
it.each(['atom', 'five', 'black_heart'])("should return a correct emoji for '%s'", (name) => {
- expect(getEmojiInfo(name)).toEqual(mockEmojiData[name]);
+ expect(getEmojiInfo(name)).toEqual(getEmojiMap()[name]);
});
it('should return fallback emoji by default', () => {
- expect(getEmojiInfo('atjs')).toEqual(mockEmojiData.grey_question);
+ expect(getEmojiInfo('atjs')).toEqual(getEmojiMap().grey_question);
});
it('should return null when fallback is false', () => {
@@ -461,7 +632,7 @@ describe('emoji', () => {
describe('when query is undefined', () => {
it('should return fallback emoji by default', () => {
- expect(getEmojiInfo()).toEqual(mockEmojiData.grey_question);
+ expect(getEmojiInfo()).toEqual(getEmojiMap().grey_question);
});
it('should return null when fallback is false', () => {
@@ -489,9 +660,9 @@ describe('emoji', () => {
}
return {
- emoji: mockEmojiData[name],
+ emoji: getEmojiMap()[name],
field: 'd',
- fieldValue: mockEmojiData[name].d,
+ fieldValue: getEmojiMap()[name].d,
score,
};
})
@@ -542,7 +713,7 @@ describe('emoji', () => {
const { field, score, fieldValue, name } = item;
return {
- emoji: mockEmojiData[name],
+ emoji: getEmojiMap()[name],
field,
fieldValue,
score,
@@ -669,9 +840,9 @@ describe('emoji', () => {
const { field, score, name } = item;
return {
- emoji: mockEmojiData[name],
+ emoji: getEmojiMap()[name],
field,
- fieldValue: mockEmojiData[name][field],
+ fieldValue: getEmojiMap()[name][field],
score,
};
});
@@ -737,7 +908,7 @@ describe('emoji', () => {
it.each`
emoji | src
- ${'thumbsup'} | ${'/-/emojis/2/thumbsup.png'}
+ ${'thumbsup'} | ${'/-/emojis/3/thumbsup.png'}
${'parrot'} | ${'parrot.gif'}
`('returns $src for emoji with name $emoji', ({ emoji, src }) => {
expect(emojiFallbackImageSrc(emoji)).toBe(src);
@@ -757,7 +928,7 @@ describe('emoji', () => {
it('returns empty object', async () => {
const result = await loadCustomEmojiWithNames();
- expect(result).toEqual({});
+ expect(result).toEqual({ emojis: {}, names: [] });
});
});
@@ -769,7 +940,7 @@ describe('emoji', () => {
it('returns empty object', async () => {
const result = await loadCustomEmojiWithNames();
- expect(result).toEqual({});
+ expect(result).toEqual({ emojis: {}, names: [] });
});
});
@@ -778,14 +949,17 @@ describe('emoji', () => {
const result = await loadCustomEmojiWithNames();
expect(result).toEqual({
- parrot: {
- c: 'custom',
- d: 'parrot',
- e: undefined,
- name: 'parrot',
- src: 'parrot.gif',
- u: 'custom',
+ emojis: {
+ parrot: {
+ c: 'custom',
+ d: 'parrot',
+ e: undefined,
+ name: 'parrot',
+ src: 'parrot.gif',
+ u: 'custom',
+ },
},
+ names: ['parrot'],
});
});
});
diff --git a/spec/frontend/environments/environment_form_spec.js b/spec/frontend/environments/environment_form_spec.js
index 5888b22aece..478ac8d6e0e 100644
--- a/spec/frontend/environments/environment_form_spec.js
+++ b/spec/frontend/environments/environment_form_spec.js
@@ -31,6 +31,7 @@ const configuration = {
headers: {
'GitLab-Agent-Id': 2,
'Content-Type': 'application/json',
+ Accept: 'application/json',
},
credentials: 'include',
};
diff --git a/spec/frontend/environments/environments_app_spec.js b/spec/frontend/environments/environments_app_spec.js
index dc450eb2aa7..5ac949e77b6 100644
--- a/spec/frontend/environments/environments_app_spec.js
+++ b/spec/frontend/environments/environments_app_spec.js
@@ -71,7 +71,7 @@ describe('~/environments/components/environments_app.vue', () => {
previousPage: 1,
__typename: 'LocalPageInfo',
},
- location = '?scope=available&page=2&search=prod',
+ location = '?scope=active&page=2&search=prod',
}) => {
setWindowLocation(location);
environmentAppMock.mockReturnValue(environmentsApp);
@@ -96,7 +96,7 @@ describe('~/environments/components/environments_app.vue', () => {
paginationMock = jest.fn();
});
- it('should request available environments if the scope is invalid', async () => {
+ it('should request active environments if the scope is invalid', async () => {
await createWrapperWithMocked({
environmentsApp: resolvedEnvironmentsApp,
folder: resolvedFolder,
@@ -105,7 +105,7 @@ describe('~/environments/components/environments_app.vue', () => {
expect(environmentAppMock).toHaveBeenCalledWith(
expect.anything(),
- expect.objectContaining({ scope: 'available', page: 2 }),
+ expect.objectContaining({ scope: 'active', page: 2 }),
expect.anything(),
expect.anything(),
);
@@ -174,18 +174,25 @@ describe('~/environments/components/environments_app.vue', () => {
expect(button.exists()).toBe(true);
});
- it('should not show a button to open the review app modal if review apps are configured', async () => {
- await createWrapperWithMocked({
- environmentsApp: {
- ...resolvedEnvironmentsApp,
- reviewApp: { canSetupReviewApp: false },
- },
- folder: resolvedFolder,
- });
+ it.each`
+ canSetupReviewApp | hasReviewApp
+ ${false} | ${true}
+ ${true} | ${true}
+ `(
+ 'should not show button to open the review app modal',
+ async ({ canSetupReviewApp, hasReviewApp }) => {
+ await createWrapperWithMocked({
+ environmentsApp: {
+ ...resolvedEnvironmentsApp,
+ reviewApp: { canSetupReviewApp, hasReviewApp },
+ },
+ folder: resolvedFolder,
+ });
- const button = wrapper.findByRole('button', { name: s__('Environments|Enable review apps') });
- expect(button.exists()).toBe(false);
- });
+ const button = wrapper.findByRole('button', { name: s__('Environments|Enable review apps') });
+ expect(button.exists()).toBe(false);
+ },
+ );
it('should not show a button to clean up environments if the user has no permissions', async () => {
await createWrapperWithMocked({
@@ -218,16 +225,16 @@ describe('~/environments/components/environments_app.vue', () => {
});
describe('tabs', () => {
- it('should show tabs for available and stopped environmets', async () => {
+ it('should show tabs for active and stopped environmets', async () => {
await createWrapperWithMocked({
environmentsApp: resolvedEnvironmentsApp,
folder: resolvedFolder,
});
- const [available, stopped] = wrapper.findAllByRole('tab').wrappers;
+ const [active, stopped] = wrapper.findAllByRole('tab').wrappers;
- expect(available.text()).toContain(__('Available'));
- expect(available.text()).toContain(resolvedEnvironmentsApp.availableCount.toString());
+ expect(active.text()).toContain(__('Active'));
+ expect(active.text()).toContain(resolvedEnvironmentsApp.activeCount.toString());
expect(stopped.text()).toContain(__('Stopped'));
expect(stopped.text()).toContain(resolvedEnvironmentsApp.stoppedCount.toString());
});
@@ -372,7 +379,7 @@ describe('~/environments/components/environments_app.vue', () => {
next.trigger('click');
await nextTick();
- expect(window.location.search).toBe('?scope=available&page=3&search=prod');
+ expect(window.location.search).toBe('?scope=active&page=3&search=prod');
});
});
@@ -399,7 +406,7 @@ describe('~/environments/components/environments_app.vue', () => {
await waitForDebounce();
- expect(window.location.search).toBe('?scope=available&page=1&search=hello');
+ expect(window.location.search).toBe('?scope=active&page=1&search=hello');
});
it('should query for the entered parameter', async () => {
diff --git a/spec/frontend/environments/graphql/mock_data.js b/spec/frontend/environments/graphql/mock_data.js
index fd97f19a6ab..7d354566761 100644
--- a/spec/frontend/environments/graphql/mock_data.js
+++ b/spec/frontend/environments/graphql/mock_data.js
@@ -262,16 +262,17 @@ export const environmentsApp = {
review_app: {
can_setup_review_app: true,
all_clusters_empty: true,
+ has_review_app: false,
review_snippet:
'{"deploy_review"=>{"stage"=>"deploy", "script"=>["echo \\"Deploy a review app\\""], "environment"=>{"name"=>"review/$CI_COMMIT_REF_NAME", "url"=>"https://$CI_ENVIRONMENT_SLUG.example.com"}, "only"=>["branches"]}}',
},
can_stop_stale_environments: true,
- available_count: 4,
+ active_count: 4,
stopped_count: 0,
};
export const resolvedEnvironmentsApp = {
- availableCount: 4,
+ activeCount: 4,
environments: [
{
name: 'review',
@@ -471,6 +472,7 @@ export const resolvedEnvironmentsApp = {
reviewApp: {
canSetupReviewApp: true,
allClustersEmpty: true,
+ hasReviewApp: false,
reviewSnippet:
'{"deploy_review"=>{"stage"=>"deploy", "script"=>["echo \\"Deploy a review app\\""], "environment"=>{"name"=>"review/$CI_COMMIT_REF_NAME", "url"=>"https://$CI_ENVIRONMENT_SLUG.example.com"}, "only"=>["branches"]}}',
__typename: 'ReviewApp',
@@ -533,7 +535,7 @@ export const folder = {
has_opened_alert: false,
},
],
- available_count: 2,
+ active_count: 2,
stopped_count: 0,
};
@@ -702,7 +704,7 @@ export const resolvedEnvironment = {
};
export const resolvedFolder = {
- availableCount: 2,
+ activeCount: 2,
environments: [
{
id: 42,
diff --git a/spec/frontend/environments/graphql/resolvers/flux_spec.js b/spec/frontend/environments/graphql/resolvers/flux_spec.js
index aa6f9e120f0..526c98b55b3 100644
--- a/spec/frontend/environments/graphql/resolvers/flux_spec.js
+++ b/spec/frontend/environments/graphql/resolvers/flux_spec.js
@@ -1,4 +1,5 @@
import MockAdapter from 'axios-mock-adapter';
+import { WatchApi } from '@gitlab/cluster-client';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_OK, HTTP_STATUS_UNAUTHORIZED } from '~/lib/utils/http_status';
import { resolvers } from '~/environments/graphql/resolvers';
@@ -14,8 +15,6 @@ describe('~/frontend/environments/graphql/resolvers', () => {
headers: { 'GitLab-Agent-Id': '1' },
},
};
- const namespace = 'default';
- const environmentName = 'my-environment';
beforeEach(() => {
mockResolvers = resolvers();
@@ -27,114 +26,260 @@ describe('~/frontend/environments/graphql/resolvers', () => {
});
describe('fluxKustomizationStatus', () => {
- const endpoint = `${configuration.basePath}/apis/kustomize.toolkit.fluxcd.io/v1beta1/namespaces/${namespace}/kustomizations/${environmentName}`;
+ const client = { writeQuery: jest.fn() };
const fluxResourcePath =
'kustomize.toolkit.fluxcd.io/v1beta1/namespaces/my-namespace/kustomizations/app';
- const endpointWithFluxResourcePath = `${configuration.basePath}/apis/${fluxResourcePath}`;
+ const endpoint = `${configuration.basePath}/apis/${fluxResourcePath}`;
- it('should request Flux Kustomizations for the provided namespace via the Kubernetes API if the fluxResourcePath is not specified', async () => {
- mock
- .onGet(endpoint, { withCredentials: true, headers: configuration.baseOptions.headers })
- .reply(HTTP_STATUS_OK, {
- status: { conditions: fluxKustomizationsMock },
- });
+ describe('when k8sWatchApi feature is disabled', () => {
+ it('should request Flux Kustomization for the provided fluxResourcePath via the Kubernetes API', async () => {
+ mock
+ .onGet(endpoint, {
+ withCredentials: true,
+ headers: configuration.baseOptions.headers,
+ })
+ .reply(HTTP_STATUS_OK, {
+ status: { conditions: fluxKustomizationsMock },
+ });
+
+ const fluxKustomizationStatus = await mockResolvers.Query.fluxKustomizationStatus(
+ null,
+ {
+ configuration,
+ fluxResourcePath,
+ },
+ { client },
+ );
- const fluxKustomizationStatus = await mockResolvers.Query.fluxKustomizationStatus(null, {
- configuration,
- namespace,
- environmentName,
+ expect(fluxKustomizationStatus).toEqual(fluxKustomizationsMock);
});
+ it('should throw an error if the API call fails', async () => {
+ const apiError = 'Invalid credentials';
+ mock
+ .onGet(endpoint, { withCredentials: true, headers: configuration.base })
+ .reply(HTTP_STATUS_UNAUTHORIZED, { message: apiError });
+
+ const fluxKustomizationsError = mockResolvers.Query.fluxKustomizationStatus(
+ null,
+ {
+ configuration,
+ fluxResourcePath,
+ },
+ { client },
+ );
- expect(fluxKustomizationStatus).toEqual(fluxKustomizationsMock);
+ await expect(fluxKustomizationsError).rejects.toThrow(apiError);
+ });
});
- it('should request Flux Kustomization for the provided fluxResourcePath via the Kubernetes API', async () => {
- mock
- .onGet(endpointWithFluxResourcePath, {
- withCredentials: true,
- headers: configuration.baseOptions.headers,
- })
- .reply(HTTP_STATUS_OK, {
- status: { conditions: fluxKustomizationsMock },
- });
- const fluxKustomizationStatus = await mockResolvers.Query.fluxKustomizationStatus(null, {
- configuration,
- namespace,
- environmentName,
- fluxResourcePath,
+ describe('when k8sWatchApi feature is enabled', () => {
+ const mockWatcher = WatchApi.prototype;
+ const mockKustomizationStatusFn = jest.fn().mockImplementation(() => {
+ return Promise.resolve(mockWatcher);
});
+ const mockOnDataFn = jest.fn().mockImplementation((eventName, callback) => {
+ if (eventName === 'data') {
+ callback(fluxKustomizationsMock);
+ }
+ });
+ const resourceName = 'custom-resource';
+ const resourceNamespace = 'custom-namespace';
+ const apiVersion = 'kustomize.toolkit.fluxcd.io/v1beta1';
- expect(fluxKustomizationStatus).toEqual(fluxKustomizationsMock);
- });
- it('should throw an error if the API call fails', async () => {
- const apiError = 'Invalid credentials';
- mock
- .onGet(endpoint, { withCredentials: true, headers: configuration.base })
- .reply(HTTP_STATUS_UNAUTHORIZED, { message: apiError });
-
- const fluxKustomizationsError = mockResolvers.Query.fluxKustomizationStatus(null, {
- configuration,
- namespace,
- environmentName,
+ beforeEach(() => {
+ gon.features = { k8sWatchApi: true };
+ jest.spyOn(mockWatcher, 'subscribeToStream').mockImplementation(mockKustomizationStatusFn);
+ jest.spyOn(mockWatcher, 'on').mockImplementation(mockOnDataFn);
+ });
+
+ describe('when the Kustomization data is present', () => {
+ beforeEach(() => {
+ mock
+ .onGet(endpoint, { withCredentials: true, headers: configuration.baseOptions.headers })
+ .reply(HTTP_STATUS_OK, {
+ apiVersion,
+ metadata: { name: resourceName, namespace: resourceNamespace },
+ status: { conditions: fluxKustomizationsMock },
+ });
+ });
+ it('should watch Kustomization by the metadata name from the cluster_client library when the data is present', async () => {
+ await mockResolvers.Query.fluxKustomizationStatus(
+ null,
+ {
+ configuration,
+ fluxResourcePath,
+ },
+ { client },
+ );
+
+ expect(mockKustomizationStatusFn).toHaveBeenCalledWith(
+ `/apis/${apiVersion}/namespaces/${resourceNamespace}/kustomizations`,
+ {
+ watch: true,
+ fieldSelector: `metadata.name=${decodeURIComponent(resourceName)}`,
+ },
+ );
+ });
+
+ it('should return data when received from the library', async () => {
+ const kustomizationStatus = await mockResolvers.Query.fluxKustomizationStatus(
+ null,
+ {
+ configuration,
+ fluxResourcePath,
+ },
+ { client },
+ );
+
+ expect(kustomizationStatus).toEqual(fluxKustomizationsMock);
+ });
});
- await expect(fluxKustomizationsError).rejects.toThrow(apiError);
+ it('should not watch Kustomization by the metadata name from the cluster_client library when the data is not present', async () => {
+ mock
+ .onGet(endpoint, { withCredentials: true, headers: configuration.baseOptions.headers })
+ .reply(HTTP_STATUS_OK, {});
+
+ await mockResolvers.Query.fluxKustomizationStatus(
+ null,
+ {
+ configuration,
+ fluxResourcePath,
+ },
+ { client },
+ );
+
+ expect(mockKustomizationStatusFn).not.toHaveBeenCalled();
+ });
});
});
describe('fluxHelmReleaseStatus', () => {
- const endpoint = `${configuration.basePath}/apis/helm.toolkit.fluxcd.io/v2beta1/namespaces/${namespace}/helmreleases/${environmentName}`;
+ const client = { writeQuery: jest.fn() };
const fluxResourcePath =
'helm.toolkit.fluxcd.io/v2beta1/namespaces/my-namespace/helmreleases/app';
- const endpointWithFluxResourcePath = `${configuration.basePath}/apis/${fluxResourcePath}`;
+ const endpoint = `${configuration.basePath}/apis/${fluxResourcePath}`;
- it('should request Flux Helm Releases via the Kubernetes API', async () => {
- mock
- .onGet(endpoint, { withCredentials: true, headers: configuration.baseOptions.headers })
- .reply(HTTP_STATUS_OK, {
- status: { conditions: fluxKustomizationsMock },
- });
+ describe('when k8sWatchApi feature is disabled', () => {
+ it('should request Flux HelmRelease for the provided fluxResourcePath via the Kubernetes API', async () => {
+ mock
+ .onGet(endpoint, {
+ withCredentials: true,
+ headers: configuration.baseOptions.headers,
+ })
+ .reply(HTTP_STATUS_OK, {
+ status: { conditions: fluxKustomizationsMock },
+ });
+
+ const fluxHelmReleaseStatus = await mockResolvers.Query.fluxHelmReleaseStatus(
+ null,
+ {
+ configuration,
+ fluxResourcePath,
+ },
+ { client },
+ );
- const fluxHelmReleaseStatus = await mockResolvers.Query.fluxHelmReleaseStatus(null, {
- configuration,
- namespace,
- environmentName,
+ expect(fluxHelmReleaseStatus).toEqual(fluxKustomizationsMock);
});
+ it('should throw an error if the API call fails', async () => {
+ const apiError = 'Invalid credentials';
+ mock
+ .onGet(endpoint, { withCredentials: true, headers: configuration.base })
+ .reply(HTTP_STATUS_UNAUTHORIZED, { message: apiError });
+
+ const fluxHelmReleasesError = mockResolvers.Query.fluxHelmReleaseStatus(
+ null,
+ {
+ configuration,
+ fluxResourcePath,
+ },
+ { client },
+ );
- expect(fluxHelmReleaseStatus).toEqual(fluxKustomizationsMock);
+ await expect(fluxHelmReleasesError).rejects.toThrow(apiError);
+ });
});
- it('should request Flux HelmRelease for the provided fluxResourcePath via the Kubernetes API', async () => {
- mock
- .onGet(endpointWithFluxResourcePath, {
- withCredentials: true,
- headers: configuration.baseOptions.headers,
- })
- .reply(HTTP_STATUS_OK, {
- status: { conditions: fluxKustomizationsMock },
- });
- const fluxHelmReleaseStatus = await mockResolvers.Query.fluxHelmReleaseStatus(null, {
- configuration,
- namespace,
- environmentName,
- fluxResourcePath,
+ describe('when k8sWatchApi feature is enabled', () => {
+ const mockWatcher = WatchApi.prototype;
+ const mockHelmReleaseStatusFn = jest.fn().mockImplementation(() => {
+ return Promise.resolve(mockWatcher);
});
+ const mockOnDataFn = jest.fn().mockImplementation((eventName, callback) => {
+ if (eventName === 'data') {
+ callback(fluxKustomizationsMock);
+ }
+ });
+ const resourceName = 'custom-resource';
+ const resourceNamespace = 'custom-namespace';
+ const apiVersion = 'helm.toolkit.fluxcd.io/v2beta1';
- expect(fluxHelmReleaseStatus).toEqual(fluxKustomizationsMock);
- });
- it('should throw an error if the API call fails', async () => {
- const apiError = 'Invalid credentials';
- mock
- .onGet(endpoint, { withCredentials: true, headers: configuration.base })
- .reply(HTTP_STATUS_UNAUTHORIZED, { message: apiError });
-
- const fluxHelmReleasesError = mockResolvers.Query.fluxHelmReleaseStatus(null, {
- configuration,
- namespace,
- environmentName,
+ beforeEach(() => {
+ gon.features = { k8sWatchApi: true };
+ jest.spyOn(mockWatcher, 'subscribeToStream').mockImplementation(mockHelmReleaseStatusFn);
+ jest.spyOn(mockWatcher, 'on').mockImplementation(mockOnDataFn);
+ });
+
+ describe('when the HelmRelease data is present', () => {
+ beforeEach(() => {
+ mock
+ .onGet(endpoint, { withCredentials: true, headers: configuration.baseOptions.headers })
+ .reply(HTTP_STATUS_OK, {
+ apiVersion,
+ metadata: { name: resourceName, namespace: resourceNamespace },
+ status: { conditions: fluxKustomizationsMock },
+ });
+ });
+ it('should watch HelmRelease by the metadata name from the cluster_client library when the data is present', async () => {
+ await mockResolvers.Query.fluxHelmReleaseStatus(
+ null,
+ {
+ configuration,
+ fluxResourcePath,
+ },
+ { client },
+ );
+
+ expect(mockHelmReleaseStatusFn).toHaveBeenCalledWith(
+ `/apis/${apiVersion}/namespaces/${resourceNamespace}/helmreleases`,
+ {
+ watch: true,
+ fieldSelector: `metadata.name=${decodeURIComponent(resourceName)}`,
+ },
+ );
+ });
+
+ it('should return data when received from the library', async () => {
+ const fluxHelmReleaseStatus = await mockResolvers.Query.fluxHelmReleaseStatus(
+ null,
+ {
+ configuration,
+ fluxResourcePath,
+ },
+ { client },
+ );
+
+ expect(fluxHelmReleaseStatus).toEqual(fluxKustomizationsMock);
+ });
});
- await expect(fluxHelmReleasesError).rejects.toThrow(apiError);
+ it('should not watch Kustomization by the metadata name from the cluster_client library when the data is not present', async () => {
+ mock
+ .onGet(endpoint, { withCredentials: true, headers: configuration.baseOptions.headers })
+ .reply(HTTP_STATUS_OK, {});
+
+ await mockResolvers.Query.fluxHelmReleaseStatus(
+ null,
+ {
+ configuration,
+ fluxResourcePath,
+ },
+ { client },
+ );
+
+ expect(mockHelmReleaseStatusFn).not.toHaveBeenCalled();
+ });
});
});
});
diff --git a/spec/frontend/environments/graphql/resolvers/kubernetes_spec.js b/spec/frontend/environments/graphql/resolvers/kubernetes_spec.js
index ed15c66f4c6..f244ddb01b5 100644
--- a/spec/frontend/environments/graphql/resolvers/kubernetes_spec.js
+++ b/spec/frontend/environments/graphql/resolvers/kubernetes_spec.js
@@ -1,8 +1,9 @@
import MockAdapter from 'axios-mock-adapter';
-import { CoreV1Api, AppsV1Api, BatchV1Api } from '@gitlab/cluster-client';
+import { CoreV1Api, AppsV1Api, BatchV1Api, WatchApi } from '@gitlab/cluster-client';
import axios from '~/lib/utils/axios_utils';
import { resolvers } from '~/environments/graphql/resolvers';
import { CLUSTER_AGENT_ERROR_MESSAGES } from '~/environments/constants';
+import k8sPodsQuery from '~/environments/graphql/queries/k8s_pods.query.graphql';
import { k8sPodsMock, k8sServicesMock, k8sNamespacesMock } from '../mock_data';
describe('~/frontend/environments/graphql/resolvers', () => {
@@ -27,6 +28,7 @@ describe('~/frontend/environments/graphql/resolvers', () => {
});
describe('k8sPods', () => {
+ const client = { writeQuery: jest.fn() };
const mockPodsListFn = jest.fn().mockImplementation(() => {
return Promise.resolve({
items: k8sPodsMock,
@@ -36,39 +38,122 @@ describe('~/frontend/environments/graphql/resolvers', () => {
const mockNamespacedPodsListFn = jest.fn().mockImplementation(mockPodsListFn);
const mockAllPodsListFn = jest.fn().mockImplementation(mockPodsListFn);
- beforeEach(() => {
- jest
- .spyOn(CoreV1Api.prototype, 'listCoreV1NamespacedPod')
- .mockImplementation(mockNamespacedPodsListFn);
- jest
- .spyOn(CoreV1Api.prototype, 'listCoreV1PodForAllNamespaces')
- .mockImplementation(mockAllPodsListFn);
- });
+ describe('when k8sWatchApi feature is disabled', () => {
+ beforeEach(() => {
+ jest
+ .spyOn(CoreV1Api.prototype, 'listCoreV1NamespacedPod')
+ .mockImplementation(mockNamespacedPodsListFn);
+ jest
+ .spyOn(CoreV1Api.prototype, 'listCoreV1PodForAllNamespaces')
+ .mockImplementation(mockAllPodsListFn);
+ });
+
+ it('should request namespaced pods from the cluster_client library if namespace is specified', async () => {
+ const pods = await mockResolvers.Query.k8sPods(
+ null,
+ {
+ configuration,
+ namespace,
+ },
+ { client },
+ );
- it('should request namespaced pods from the cluster_client library if namespace is specified', async () => {
- const pods = await mockResolvers.Query.k8sPods(null, { configuration, namespace });
+ expect(mockNamespacedPodsListFn).toHaveBeenCalledWith({ namespace });
+ expect(mockAllPodsListFn).not.toHaveBeenCalled();
- expect(mockNamespacedPodsListFn).toHaveBeenCalledWith({ namespace });
- expect(mockAllPodsListFn).not.toHaveBeenCalled();
+ expect(pods).toEqual(k8sPodsMock);
+ });
+ it('should request all pods from the cluster_client library if namespace is not specified', async () => {
+ const pods = await mockResolvers.Query.k8sPods(
+ null,
+ {
+ configuration,
+ namespace: '',
+ },
+ { client },
+ );
- expect(pods).toEqual(k8sPodsMock);
- });
- it('should request all pods from the cluster_client library if namespace is not specified', async () => {
- const pods = await mockResolvers.Query.k8sPods(null, { configuration, namespace: '' });
+ expect(mockAllPodsListFn).toHaveBeenCalled();
+ expect(mockNamespacedPodsListFn).not.toHaveBeenCalled();
- expect(mockAllPodsListFn).toHaveBeenCalled();
- expect(mockNamespacedPodsListFn).not.toHaveBeenCalled();
+ expect(pods).toEqual(k8sPodsMock);
+ });
+ it('should throw an error if the API call fails', async () => {
+ jest
+ .spyOn(CoreV1Api.prototype, 'listCoreV1PodForAllNamespaces')
+ .mockRejectedValue(new Error('API error'));
- expect(pods).toEqual(k8sPodsMock);
+ await expect(
+ mockResolvers.Query.k8sPods(null, { configuration }, { client }),
+ ).rejects.toThrow('API error');
+ });
});
- it('should throw an error if the API call fails', async () => {
- jest
- .spyOn(CoreV1Api.prototype, 'listCoreV1PodForAllNamespaces')
- .mockRejectedValue(new Error('API error'));
- await expect(mockResolvers.Query.k8sPods(null, { configuration })).rejects.toThrow(
- 'API error',
- );
+ describe('when k8sWatchApi feature is enabled', () => {
+ const mockWatcher = WatchApi.prototype;
+ const mockPodsListWatcherFn = jest.fn().mockImplementation(() => {
+ return Promise.resolve(mockWatcher);
+ });
+
+ const mockOnDataFn = jest.fn().mockImplementation((eventName, callback) => {
+ if (eventName === 'data') {
+ callback([]);
+ }
+ });
+
+ describe('when the pods data is present', () => {
+ beforeEach(() => {
+ gon.features = { k8sWatchApi: true };
+
+ jest
+ .spyOn(CoreV1Api.prototype, 'listCoreV1NamespacedPod')
+ .mockImplementation(mockNamespacedPodsListFn);
+ jest
+ .spyOn(CoreV1Api.prototype, 'listCoreV1PodForAllNamespaces')
+ .mockImplementation(mockAllPodsListFn);
+ jest.spyOn(mockWatcher, 'subscribeToStream').mockImplementation(mockPodsListWatcherFn);
+ jest.spyOn(mockWatcher, 'on').mockImplementation(mockOnDataFn);
+ });
+
+ it('should request namespaced pods from the cluster_client library if namespace is specified', async () => {
+ await mockResolvers.Query.k8sPods(null, { configuration, namespace }, { client });
+
+ expect(mockPodsListWatcherFn).toHaveBeenCalledWith(
+ `/api/v1/namespaces/${namespace}/pods`,
+ {
+ watch: true,
+ },
+ );
+ });
+ it('should request all pods from the cluster_client library if namespace is not specified', async () => {
+ await mockResolvers.Query.k8sPods(null, { configuration, namespace: '' }, { client });
+
+ expect(mockPodsListWatcherFn).toHaveBeenCalledWith(`/api/v1/pods`, { watch: true });
+ });
+ it('should update cache with the new data when received from the library', async () => {
+ await mockResolvers.Query.k8sPods(null, { configuration, namespace: '' }, { client });
+
+ expect(client.writeQuery).toHaveBeenCalledWith({
+ query: k8sPodsQuery,
+ variables: { configuration, namespace: '' },
+ data: { k8sPods: [] },
+ });
+ });
+ });
+
+ it('should not watch pods from the cluster_client library when the pods data is not present', async () => {
+ jest.spyOn(CoreV1Api.prototype, 'listCoreV1NamespacedPod').mockImplementation(
+ jest.fn().mockImplementation(() => {
+ return Promise.resolve({
+ items: [],
+ });
+ }),
+ );
+
+ await mockResolvers.Query.k8sPods(null, { configuration, namespace }, { client });
+
+ expect(mockPodsListWatcherFn).not.toHaveBeenCalled();
+ });
});
});
describe('k8sServices', () => {
diff --git a/spec/frontend/environments/kubernetes_overview_spec.js b/spec/frontend/environments/kubernetes_overview_spec.js
index 2b810aac653..12689df586f 100644
--- a/spec/frontend/environments/kubernetes_overview_spec.js
+++ b/spec/frontend/environments/kubernetes_overview_spec.js
@@ -30,6 +30,7 @@ const configuration = {
headers: {
'GitLab-Agent-Id': '1',
'Content-Type': 'application/json',
+ Accept: 'application/json',
},
credentials: 'include',
};
@@ -121,7 +122,6 @@ describe('~/environments/components/kubernetes_overview.vue', () => {
expect(findKubernetesStatusBar().props()).toEqual({
clusterHealthStatus: 'success',
configuration,
- namespace: kubernetesNamespace,
environmentName: resolvedEnvironment.name,
fluxResourcePath: fluxResourcePathMock,
});
diff --git a/spec/frontend/environments/kubernetes_status_bar_spec.js b/spec/frontend/environments/kubernetes_status_bar_spec.js
index 5dec7ca5aac..dcd628354e1 100644
--- a/spec/frontend/environments/kubernetes_status_bar_spec.js
+++ b/spec/frontend/environments/kubernetes_status_bar_spec.js
@@ -49,7 +49,6 @@ describe('~/environments/components/kubernetes_status_bar.vue', () => {
const createWrapper = ({
apolloProvider = createApolloProvider(),
clusterHealthStatus = '',
- namespace = '',
fluxResourcePath = '',
} = {}) => {
wrapper = shallowMountExtended(KubernetesStatusBar, {
@@ -57,7 +56,6 @@ describe('~/environments/components/kubernetes_status_bar.vue', () => {
clusterHealthStatus,
configuration,
environmentName,
- namespace,
fluxResourcePath,
},
apolloProvider,
@@ -88,7 +86,7 @@ describe('~/environments/components/kubernetes_status_bar.vue', () => {
});
describe('sync badge', () => {
- describe('when no namespace is provided', () => {
+ describe('when no flux resource path is provided', () => {
beforeEach(() => {
createWrapper();
});
@@ -104,7 +102,6 @@ describe('~/environments/components/kubernetes_status_bar.vue', () => {
});
describe('when flux resource path is provided', () => {
- const namespace = 'my-namespace';
let fluxResourcePath;
describe('if the provided resource is a Kustomization', () => {
@@ -112,7 +109,7 @@ describe('~/environments/components/kubernetes_status_bar.vue', () => {
fluxResourcePath =
'kustomize.toolkit.fluxcd.io/v1beta1/namespaces/my-namespace/kustomizations/app';
- createWrapper({ namespace, fluxResourcePath });
+ createWrapper({ fluxResourcePath });
});
it('requests the Kustomization resource status', () => {
@@ -120,8 +117,6 @@ describe('~/environments/components/kubernetes_status_bar.vue', () => {
{},
expect.objectContaining({
configuration,
- namespace,
- environmentName,
fluxResourcePath,
}),
expect.any(Object),
@@ -139,7 +134,7 @@ describe('~/environments/components/kubernetes_status_bar.vue', () => {
fluxResourcePath =
'helm.toolkit.fluxcd.io/v2beta1/namespaces/my-namespace/helmreleases/app';
- createWrapper({ namespace, fluxResourcePath });
+ createWrapper({ fluxResourcePath });
});
it('requests the HelmRelease resource status', () => {
@@ -147,8 +142,6 @@ describe('~/environments/components/kubernetes_status_bar.vue', () => {
{},
expect.objectContaining({
configuration,
- namespace,
- environmentName,
fluxResourcePath,
}),
expect.any(Object),
@@ -160,30 +153,6 @@ describe('~/environments/components/kubernetes_status_bar.vue', () => {
expect(fluxKustomizationStatusQuery).not.toHaveBeenCalled();
});
});
- });
-
- describe('when namespace is provided', () => {
- describe('with no Flux resources found', () => {
- beforeEach(() => {
- createWrapper({ namespace: 'my-namespace' });
- });
-
- it('requests Kustomizations', () => {
- expect(fluxKustomizationStatusQuery).toHaveBeenCalled();
- });
-
- it('requests HelmReleases when there were no Kustomizations found', async () => {
- await waitForPromises();
-
- expect(fluxHelmReleaseStatusQuery).toHaveBeenCalled();
- });
-
- it('renders sync status as Unavailable when no Kustomizations and HelmReleases found', async () => {
- await waitForPromises();
-
- expect(findSyncBadge().text()).toBe(s__('Deployment|Unavailable'));
- });
- });
describe('with Flux Kustomizations available', () => {
const createApolloProviderWithKustomizations = ({
@@ -202,63 +171,11 @@ describe('~/environments/components/kubernetes_status_bar.vue', () => {
it("doesn't request HelmReleases when the Kustomizations were found", async () => {
createWrapper({
apolloProvider: createApolloProviderWithKustomizations(),
- namespace: 'my-namespace',
});
await waitForPromises();
expect(fluxHelmReleaseStatusQuery).not.toHaveBeenCalled();
});
-
- it.each`
- status | type | badgeType
- ${'True'} | ${'Stalled'} | ${'stalled'}
- ${'True'} | ${'Reconciling'} | ${'reconciling'}
- ${'True'} | ${'Ready'} | ${'reconciled'}
- ${'False'} | ${'Ready'} | ${'failed'}
- ${'True'} | ${'Unknown'} | ${'unknown'}
- `(
- 'renders $badgeType when status is $status and type is $type',
- async ({ status, type, badgeType }) => {
- createWrapper({
- apolloProvider: createApolloProviderWithKustomizations({
- result: { status, type, message: '' },
- }),
- namespace: 'my-namespace',
- });
- await waitForPromises();
-
- const badge = SYNC_STATUS_BADGES[badgeType];
-
- expect(findSyncBadge().text()).toBe(badge.text);
- expect(findSyncBadge().props()).toMatchObject({
- icon: badge.icon,
- variant: badge.variant,
- });
- },
- );
-
- it.each`
- status | type | message | popoverTitle | popoverText
- ${'True'} | ${'Stalled'} | ${'stalled reason'} | ${s__('Deployment|Flux sync stalled')} | ${'stalled reason'}
- ${'True'} | ${'Reconciling'} | ${''} | ${undefined} | ${s__('Deployment|Flux sync reconciling')}
- ${'True'} | ${'Ready'} | ${''} | ${undefined} | ${s__('Deployment|Flux sync reconciled successfully')}
- ${'False'} | ${'Ready'} | ${'failed reason'} | ${s__('Deployment|Flux sync failed')} | ${'failed reason'}
- ${'True'} | ${'Unknown'} | ${''} | ${s__('Deployment|Flux sync status is unknown')} | ${s__('Deployment|Unable to detect state. %{linkStart}How are states detected?%{linkEnd}')}
- `(
- 'renders correct popover text when status is $status and type is $type',
- async ({ status, type, message, popoverTitle, popoverText }) => {
- createWrapper({
- apolloProvider: createApolloProviderWithKustomizations({
- result: { status, type, message },
- }),
- namespace: 'my-namespace',
- });
- await waitForPromises();
-
- expect(findPopover().text()).toMatchInterpolatedText(popoverText);
- expect(findPopover().props('title')).toBe(popoverTitle);
- },
- );
});
describe('when Flux API errored', () => {
@@ -277,7 +194,8 @@ describe('~/environments/components/kubernetes_status_bar.vue', () => {
beforeEach(async () => {
createWrapper({
apolloProvider: createApolloProviderWithErrors(),
- namespace: 'my-namespace',
+ fluxResourcePath:
+ 'kustomize.toolkit.fluxcd.io/v1beta1/namespaces/my-namespace/kustomizations/app',
});
await waitForPromises();
});
diff --git a/spec/frontend/environments/kubernetes_summary_spec.js b/spec/frontend/environments/kubernetes_summary_spec.js
index fdcf32e7d01..457d1a37c1d 100644
--- a/spec/frontend/environments/kubernetes_summary_spec.js
+++ b/spec/frontend/environments/kubernetes_summary_spec.js
@@ -16,7 +16,11 @@ describe('~/environments/components/kubernetes_summary.vue', () => {
const namespace = 'my-kubernetes-namespace';
const configuration = {
basePath: mockKasTunnelUrl,
- headers: { 'GitLab-Agent-Id': '1', 'Content-Type': 'application/json' },
+ headers: {
+ 'GitLab-Agent-Id': '1',
+ 'Content-Type': 'application/json',
+ Accept: 'application/json',
+ },
};
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
diff --git a/spec/frontend/error_tracking/components/error_details_spec.js b/spec/frontend/error_tracking/components/error_details_spec.js
index d6cf12587b9..977e0a55a99 100644
--- a/spec/frontend/error_tracking/components/error_details_spec.js
+++ b/spec/frontend/error_tracking/components/error_details_spec.js
@@ -190,7 +190,7 @@ describe('ErrorDetails', () => {
});
describe('unsafe chars for culprit field', () => {
- const findReportedText = () => wrapper.find('[data-qa-selector="reported_text"]');
+ const findReportedText = () => wrapper.find('[data-testid="reported-text"]');
const culprit = '<script>console.log("surprise!")</script>';
beforeEach(() => {
store.state.details.loadingStacktrace = false;
@@ -350,7 +350,7 @@ describe('ErrorDetails', () => {
it('should submit the form', () => {
window.HTMLFormElement.prototype.submit = () => {};
const submitSpy = jest.spyOn(wrapper.vm.$refs.sentryIssueForm, 'submit');
- wrapper.find('[data-qa-selector="create_issue_button"]').vm.$emit('click');
+ wrapper.find('[data-testid="create-issue-button"]').vm.$emit('click');
expect(submitSpy).toHaveBeenCalled();
submitSpy.mockRestore();
});
@@ -462,7 +462,7 @@ describe('ErrorDetails', () => {
describe('GitLab issue link', () => {
const gitlabIssuePath = 'https://gitlab.example.com/issues/1';
const findGitLabLink = () => wrapper.find(`[href="${gitlabIssuePath}"]`);
- const findCreateIssueButton = () => wrapper.find('[data-qa-selector="create_issue_button"]');
+ const findCreateIssueButton = () => wrapper.find('[data-testid="create-issue-button"]');
const findViewIssueButton = () => wrapper.find('[data-qa-selector="view_issue_button"]');
describe('is present', () => {
@@ -562,7 +562,7 @@ describe('ErrorDetails', () => {
});
it('should track create issue button click', async () => {
- await wrapper.find('[data-qa-selector="create_issue_button"]').vm.$emit('click');
+ await wrapper.find('[data-testid="create-issue-button"]').vm.$emit('click');
expect(Tracking.event).toHaveBeenCalledWith(category, 'click_create_issue_from_error', {
extra: {
variant: integrated ? 'integrated' : 'external',
diff --git a/spec/frontend/fixtures/issues.rb b/spec/frontend/fixtures/issues.rb
index 90aa0544526..6d81d9ca1d2 100644
--- a/spec/frontend/fixtures/issues.rb
+++ b/spec/frontend/fixtures/issues.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Projects::IssuesController, '(JavaScript fixtures)', :with_license, type: :controller do
include JavaScriptFixturesHelpers
- let(:user) { create(:user, :no_super_sidebar, feed_token: 'feedtoken:coldfeed') }
+ let(:user) { create(:user, feed_token: 'feedtoken:coldfeed') }
let(:namespace) { create(:namespace, name: 'frontend-fixtures') }
let(:project) { create(:project_empty_repo, namespace: namespace, path: 'issues-project') }
diff --git a/spec/frontend/fixtures/merge_requests.rb b/spec/frontend/fixtures/merge_requests.rb
index a1896a6470b..e8272a1f93a 100644
--- a/spec/frontend/fixtures/merge_requests.rb
+++ b/spec/frontend/fixtures/merge_requests.rb
@@ -2,7 +2,13 @@
require 'spec_helper'
-RSpec.describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :controller do
+RSpec
+ .describe(
+ Projects::MergeRequestsController,
+ '(JavaScript fixtures)',
+ type: :controller,
+ feature_category: :code_review_workflow
+ ) do
include JavaScriptFixturesHelpers
let(:namespace) { create(:namespace, name: 'frontend-fixtures') }
diff --git a/spec/frontend/fixtures/snippet.rb b/spec/frontend/fixtures/snippet.rb
index 23df89a244c..a96b7a57106 100644
--- a/spec/frontend/fixtures/snippet.rb
+++ b/spec/frontend/fixtures/snippet.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe SnippetsController, '(JavaScript fixtures)', type: :controller do
include JavaScriptFixturesHelpers
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
let(:namespace) { create(:namespace, name: 'frontend-fixtures', owner: user) }
let(:project) { create(:project, :repository, namespace: namespace, path: 'branches-project') }
let(:snippet) { create(:personal_snippet, :public, title: 'snippet.md', content: '# snippet', file_name: 'snippet.md', author: user) }
diff --git a/spec/frontend/fixtures/startup_css.rb b/spec/frontend/fixtures/startup_css.rb
deleted file mode 100644
index 83e02470321..00000000000
--- a/spec/frontend/fixtures/startup_css.rb
+++ /dev/null
@@ -1,89 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'Startup CSS fixtures', type: :controller do
- include JavaScriptFixturesHelpers
-
- let(:use_full_html) { true }
-
- render_views
-
- shared_examples 'startup css project fixtures' do |type|
- let(:user) { create(:user, :admin) }
- let(:project) { create(:project, :public, :repository, description: 'Code and stuff', creator: user) }
-
- before do
- # We want vNext badge to be included and com/canary don't remove/hide any other elements.
- # This is why we're turning com and canary on by default for now.
- allow(Gitlab).to receive(:canary?).and_return(true)
- sign_in(user)
- end
-
- it "startup_css/project-#{type}.html" do
- get :show, params: {
- namespace_id: project.namespace.to_param,
- id: project
- }
-
- expect(response).to be_successful
- end
-
- it "startup_css/project-#{type}-signed-out.html" do
- sign_out(user)
-
- get :show, params: {
- namespace_id: project.namespace.to_param,
- id: project
- }
-
- expect(response).to be_successful
- end
-
- # This ensures that the correct css is generated for super sidebar
- it "startup_css/project-#{type}-super-sidebar.html" do
- user.update!(use_new_navigation: true)
-
- get :show, params: {
- namespace_id: project.namespace.to_param,
- id: project
- }
-
- expect(response).to be_successful
- end
- end
-
- describe ProjectsController, '(Startup CSS fixtures)', :saas, type: :controller do
- it_behaves_like 'startup css project fixtures', 'general'
- end
-
- describe ProjectsController, '(Startup CSS fixtures)', :saas, type: :controller do
- before do
- user.update!(theme_id: 11)
- end
-
- it_behaves_like 'startup css project fixtures', 'dark'
- end
-
- describe SessionsController, '(Startup CSS fixtures)', type: :controller do
- include DeviseHelpers
-
- before do
- set_devise_mapping(context: request)
- end
-
- it 'startup_css/sign-in.html' do
- get :new
-
- expect(response).to be_successful
- end
-
- it 'startup_css/sign-in-old.html' do
- stub_feature_flags(restyle_login_page: false)
-
- get :new
-
- expect(response).to be_successful
- end
- end
-end
diff --git a/spec/frontend/groups/components/overview_tabs_spec.js b/spec/frontend/groups/components/overview_tabs_spec.js
index 8db69295ac4..6bed744685f 100644
--- a/spec/frontend/groups/components/overview_tabs_spec.js
+++ b/spec/frontend/groups/components/overview_tabs_spec.js
@@ -362,9 +362,7 @@ describe('OverviewTabs', () => {
describe('when sort direction is changed', () => {
beforeEach(async () => {
await setup();
- await wrapper
- .findByRole('button', { name: 'Sorting Direction: Ascending' })
- .trigger('click');
+ await wrapper.findByRole('button', { name: 'Sort direction: Ascending' }).trigger('click');
});
it('updates query string with `sort` key', () => {
diff --git a/spec/frontend/groups/members/utils_spec.js b/spec/frontend/groups/members/utils_spec.js
index 0912e66e3e8..c7874b8b896 100644
--- a/spec/frontend/groups/members/utils_spec.js
+++ b/spec/frontend/groups/members/utils_spec.js
@@ -8,7 +8,19 @@ describe('group member utils', () => {
accessLevel: 50,
expires_at: '2020-10-16',
}),
- ).toEqual({ group_member: { access_level: 50, expires_at: '2020-10-16' } });
+ ).toEqual({
+ group_member: { access_level: 50, expires_at: '2020-10-16', member_role_id: null },
+ });
+
+ expect(
+ groupMemberRequestFormatter({
+ accessLevel: 50,
+ expires_at: '2020-10-16',
+ memberRoleId: 80,
+ }),
+ ).toEqual({
+ group_member: { access_level: 50, expires_at: '2020-10-16', member_role_id: 80 },
+ });
});
});
});
diff --git a/spec/frontend/header_spec.js b/spec/frontend/header_spec.js
index 4907dc09a3c..13c11863443 100644
--- a/spec/frontend/header_spec.js
+++ b/spec/frontend/header_spec.js
@@ -3,7 +3,14 @@ import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import initTodoToggle, { initNavUserDropdownTracking } from '~/header';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-describe('Header', () => {
+// TODO: Remove this with the removal of the old navigation.
+// See https://gitlab.com/groups/gitlab-org/-/epics/11875.
+//
+// This and ~/header will be removed. These tests no longer work due to the
+// corresponding fixtures changing for
+// https://gitlab.com/gitlab-org/gitlab/-/issues/420121.
+// eslint-disable-next-line jest/no-disabled-tests
+describe.skip('Header', () => {
describe('Todos notification', () => {
const todosPendingCount = '.js-todos-count';
diff --git a/spec/frontend/helpers/help_page_helper_spec.js b/spec/frontend/helpers/help_page_helper_spec.js
index 09c1a113a96..44a7300097f 100644
--- a/spec/frontend/helpers/help_page_helper_spec.js
+++ b/spec/frontend/helpers/help_page_helper_spec.js
@@ -3,6 +3,7 @@ import { helpPagePath } from '~/helpers/help_page_helper';
describe('help page helper', () => {
it.each`
relative_url_root | path | anchor | expected
+ ${undefined} | ${undefined} | ${undefined} | ${'/help'}
${undefined} | ${'administration/index'} | ${undefined} | ${'/help/administration/index'}
${''} | ${'administration/index'} | ${undefined} | ${'/help/administration/index'}
${'/'} | ${'administration/index'} | ${undefined} | ${'/help/administration/index'}
diff --git a/spec/frontend/helpers/init_simple_app_helper_spec.js b/spec/frontend/helpers/init_simple_app_helper_spec.js
index 7938e3851d0..de39a6f9d70 100644
--- a/spec/frontend/helpers/init_simple_app_helper_spec.js
+++ b/spec/frontend/helpers/init_simple_app_helper_spec.js
@@ -1,7 +1,9 @@
import { createWrapper } from '@vue/test-utils';
import Vue from 'vue';
+import VueApollo from 'vue-apollo';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { initSimpleApp } from '~/helpers/init_simple_app_helper';
+import createDefaultClient from '~/lib/graphql';
const MockComponent = Vue.component('MockComponent', {
props: {
@@ -25,10 +27,10 @@ const findMock = () => wrapper.findComponent(MockComponent);
const didCreateApp = () => wrapper !== undefined;
-const initMock = (html, props = {}) => {
+const initMock = (html, options = {}) => {
setHTMLFixture(html);
- const app = initSimpleApp('#mount-here', MockComponent, { props });
+ const app = initSimpleApp('#mount-here', MockComponent, options);
wrapper = app ? createWrapper(app) : undefined;
};
@@ -58,4 +60,35 @@ describe('helpers/init_simple_app_helper/initSimpleApp', () => {
count: 123,
});
});
+
+ describe('options', () => {
+ describe('withApolloProvider', () => {
+ describe('if not true or not VueApollo', () => {
+ it('apolloProvider not created', () => {
+ initMock('<div id="mount-here"></div>', { withApolloProvider: false });
+
+ expect(wrapper.vm.$apollo).toBeUndefined();
+ });
+ });
+
+ describe('if true, creates default provider', () => {
+ it('creates a default apolloProvider', () => {
+ initMock('<div id="mount-here"></div>', { withApolloProvider: true });
+
+ expect(wrapper.vm.$apollo).not.toBeUndefined();
+ });
+ });
+
+ describe('if VueApollo, sets as default provider', () => {
+ it('uses the provided apolloClient', () => {
+ Vue.use(VueApollo);
+ const apolloProvider = new VueApollo({ defaultClient: createDefaultClient() });
+
+ initMock('<div id="mount-here"></div>', { withApolloProvider: apolloProvider });
+
+ expect(wrapper.vm.$apolloProvider).toBe(apolloProvider);
+ });
+ });
+ });
+ });
});
diff --git a/spec/frontend/ide/components/commit_sidebar/new_merge_request_option_spec.js b/spec/frontend/ide/components/commit_sidebar/new_merge_request_option_spec.js
index c5e540c3ea7..26c709e6951 100644
--- a/spec/frontend/ide/components/commit_sidebar/new_merge_request_option_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/new_merge_request_option_spec.js
@@ -4,7 +4,7 @@ import Vuex from 'vuex';
import { GlFormCheckbox } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import NewMergeRequestOption from '~/ide/components/commit_sidebar/new_merge_request_option.vue';
-import { createStore } from '~/ide/stores';
+import { createStoreOptions } from '~/ide/stores';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
Vue.use(Vuex);
@@ -22,17 +22,25 @@ describe('NewMergeRequestOption component', () => {
shouldDisableNewMrOption = false,
shouldCreateMR = false,
} = {}) => {
- store = createStore();
-
- wrapper = shallowMountExtended(NewMergeRequestOption, {
- store: {
- ...store,
- getters: {
- 'commit/shouldHideNewMrOption': shouldHideNewMrOption,
- 'commit/shouldDisableNewMrOption': shouldDisableNewMrOption,
- 'commit/shouldCreateMR': shouldCreateMR,
+ const storeOptions = createStoreOptions();
+
+ store = new Vuex.Store({
+ ...storeOptions,
+ modules: {
+ ...storeOptions.modules,
+ commit: {
+ ...storeOptions.modules.commit,
+ getters: {
+ shouldHideNewMrOption: () => shouldHideNewMrOption,
+ shouldDisableNewMrOption: () => shouldDisableNewMrOption,
+ shouldCreateMR: () => shouldCreateMR,
+ },
},
},
+ });
+
+ wrapper = shallowMountExtended(NewMergeRequestOption, {
+ store,
directives: {
GlTooltip: createMockDirective('gl-tooltip'),
},
diff --git a/spec/frontend/ide/components/jobs/detail/description_spec.js b/spec/frontend/ide/components/jobs/detail/description_spec.js
index 2bb0f3fccf4..909bd1f7c90 100644
--- a/spec/frontend/ide/components/jobs/detail/description_spec.js
+++ b/spec/frontend/ide/components/jobs/detail/description_spec.js
@@ -20,11 +20,8 @@ describe('IDE job description', () => {
});
it('renders CI icon', () => {
- expect(wrapper.find('.ci-status-icon').findComponent(GlIcon).exists()).toBe(true);
- });
-
- it('renders a borderless CI icon', () => {
- expect(wrapper.find('.borderless').findComponent(GlIcon).exists()).toBe(true);
+ expect(wrapper.find('[data-testid="ci-icon"]').findComponent(GlIcon).exists()).toBe(true);
+ expect(wrapper.find('[data-testid="status_success_borderless-icon"]').exists()).toBe(true);
});
it('renders bridge job details without the job link', () => {
diff --git a/spec/frontend/ide/components/jobs/item_spec.js b/spec/frontend/ide/components/jobs/item_spec.js
index ab442a27817..aa6fc5531dd 100644
--- a/spec/frontend/ide/components/jobs/item_spec.js
+++ b/spec/frontend/ide/components/jobs/item_spec.js
@@ -18,7 +18,7 @@ describe('IDE jobs item', () => {
});
it('renders CI icon', () => {
- expect(wrapper.find('[data-testid="status_success_borderless-icon"]').exists()).toBe(true);
+ expect(wrapper.find('[data-testid="ci-icon"]').exists()).toBe(true);
});
it('does not render view logs button if not started', async () => {
diff --git a/spec/frontend/ide/components/repo_editor_spec.js b/spec/frontend/ide/components/repo_editor_spec.js
index 33fa5bc799f..6f53aaed655 100644
--- a/spec/frontend/ide/components/repo_editor_spec.js
+++ b/spec/frontend/ide/components/repo_editor_spec.js
@@ -8,17 +8,14 @@ import { shallowMount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
import { stubPerformanceWebAPI } from 'helpers/performance';
import { exampleConfigs, exampleFiles } from 'jest/ide/lib/editorconfig/mock_data';
-import {
- EDITOR_CODE_INSTANCE_FN,
- EDITOR_DIFF_INSTANCE_FN,
- EXTENSION_CI_SCHEMA_FILE_NAME_MATCH,
-} from '~/editor/constants';
+import { EDITOR_CODE_INSTANCE_FN, EDITOR_DIFF_INSTANCE_FN } from '~/editor/constants';
import { EditorMarkdownExtension } from '~/editor/extensions/source_editor_markdown_ext';
import { EditorMarkdownPreviewExtension } from '~/editor/extensions/source_editor_markdown_livepreview_ext';
import { CiSchemaExtension } from '~/editor/extensions/source_editor_ci_schema_ext';
import SourceEditor from '~/editor/source_editor';
import RepoEditor from '~/ide/components/repo_editor.vue';
import { leftSidebarViews, FILE_VIEW_MODE_PREVIEW, viewerTypes } from '~/ide/constants';
+import { DEFAULT_CI_CONFIG_PATH } from '~/lib/utils/constants';
import ModelManager from '~/ide/lib/common/model_manager';
import service from '~/ide/services';
import { createStoreOptions } from '~/ide/stores';
@@ -56,7 +53,7 @@ const dummyFile = {
active: true,
},
ciConfig: {
- ...file(EXTENSION_CI_SCHEMA_FILE_NAME_MATCH),
+ ...file(DEFAULT_CI_CONFIG_PATH),
content: '',
tempFile: true,
active: true,
diff --git a/spec/frontend/import/details/components/bulk_import_details_app_spec.js b/spec/frontend/import/details/components/bulk_import_details_app_spec.js
new file mode 100644
index 00000000000..d32afb7ddcb
--- /dev/null
+++ b/spec/frontend/import/details/components/bulk_import_details_app_spec.js
@@ -0,0 +1,18 @@
+import { shallowMount } from '@vue/test-utils';
+import BulkImportDetailsApp from '~/import/details/components/bulk_import_details_app.vue';
+
+describe('Bulk import details app', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMount(BulkImportDetailsApp);
+ };
+
+ describe('template', () => {
+ it('renders heading', () => {
+ createComponent();
+
+ expect(wrapper.find('h1').text()).toBe('GitLab Migration details');
+ });
+ });
+});
diff --git a/spec/frontend/import/details/components/import_details_app_spec.js b/spec/frontend/import/details/components/import_details_app_spec.js
index 6e748a57a1d..cc3d9dd5e5e 100644
--- a/spec/frontend/import/details/components/import_details_app_spec.js
+++ b/spec/frontend/import/details/components/import_details_app_spec.js
@@ -12,7 +12,7 @@ describe('Import details app', () => {
it('renders heading', () => {
createComponent();
- expect(wrapper.find('h1').text()).toBe(ImportDetailsApp.i18n.pageTitle);
+ expect(wrapper.find('h1').text()).toBe('GitHub import details');
});
});
});
diff --git a/spec/frontend/import/details/components/import_details_table_spec.js b/spec/frontend/import/details/components/import_details_table_spec.js
index aee8573eb02..e2ba0ddad17 100644
--- a/spec/frontend/import/details/components/import_details_table_spec.js
+++ b/spec/frontend/import/details/components/import_details_table_spec.js
@@ -2,6 +2,7 @@ import { mount, shallowMount } from '@vue/test-utils';
import { GlEmptyState, GlLoadingIcon, GlTable } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
+import { getParameterValues } from '~/lib/utils/url_utility';
import { HTTP_STATUS_OK, HTTP_STATUS_INTERNAL_SERVER_ERROR } from '~/lib/utils/http_status';
import { createAlert } from '~/alert';
import waitForPromises from 'helpers/wait_for_promises';
@@ -11,13 +12,32 @@ import ImportDetailsTable from '~/import/details/components/import_details_table
import { mockImportFailures, mockHeaders } from '../mock_data';
jest.mock('~/alert');
+jest.mock('~/lib/utils/url_utility', () => ({
+ ...jest.requireActual('~/lib/utils/url_utility'),
+ getParameterValues: jest.fn().mockReturnValue([]),
+}));
describe('Import details table', () => {
let wrapper;
let mock;
- const createComponent = ({ mountFn = shallowMount, provide = {} } = {}) => {
+ const mockFields = [
+ {
+ key: 'type',
+ label: 'Type',
+ },
+ {
+ key: 'title',
+ label: 'Title',
+ },
+ ];
+
+ const createComponent = ({ mountFn = shallowMount, props = {}, provide = {} } = {}) => {
wrapper = mountFn(ImportDetailsTable, {
+ propsData: {
+ fields: mockFields,
+ ...props,
+ },
provide,
});
};
@@ -109,5 +129,49 @@ describe('Import details table', () => {
});
});
});
+
+ describe('when bulk_import is true', () => {
+ const mockId = 144;
+ const mockEntityId = 68;
+
+ beforeEach(() => {
+ gon.api_version = 'v4';
+ getParameterValues.mockReturnValueOnce([mockId]);
+ getParameterValues.mockReturnValueOnce([mockEntityId]);
+
+ mock
+ .onGet(`/api/v4/bulk_imports/${mockId}/entities/${mockEntityId}/failures`)
+ .reply(HTTP_STATUS_OK, mockImportFailures, mockHeaders);
+
+ createComponent({
+ mountFn: mount,
+ props: {
+ bulkImport: true,
+ },
+ });
+ });
+
+ it('renders loading icon', () => {
+ expect(findGlLoadingIcon().exists()).toBe(true);
+ });
+
+ it('does not render loading icon after fetch', async () => {
+ await waitForPromises();
+
+ expect(findGlLoadingIcon().exists()).toBe(false);
+ });
+
+ it('sets items and pagination info', async () => {
+ await waitForPromises();
+
+ expect(findGlTableRows().length).toBe(mockImportFailures.length);
+ expect(findPaginationBar().props('pageInfo')).toMatchObject({
+ page: mockHeaders['x-page'],
+ perPage: mockHeaders['x-per-page'],
+ total: mockHeaders['x-total'],
+ totalPages: mockHeaders['x-total-pages'],
+ });
+ });
+ });
});
});
diff --git a/spec/frontend/import_entities/import_groups/components/import_status_spec.js b/spec/frontend/import_entities/import_groups/components/import_status_spec.js
new file mode 100644
index 00000000000..8d055d45dd8
--- /dev/null
+++ b/spec/frontend/import_entities/import_groups/components/import_status_spec.js
@@ -0,0 +1,99 @@
+import { GlBadge, GlLink } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+
+import ImportStatus from '~/import_entities/import_groups/components/import_status.vue';
+import { STATUSES, STATUS_ICON_MAP } from '~/import_entities/constants';
+
+describe('Group import status component', () => {
+ let wrapper;
+
+ const defaultProps = {
+ status: STATUSES.FINISHED,
+ };
+
+ const mockDetailsPath = '/details';
+
+ const createComponent = ({ props } = {}) => {
+ wrapper = shallowMount(ImportStatus, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ provide: {
+ detailsPath: mockDetailsPath,
+ },
+ });
+ };
+
+ const findGlBadge = () => wrapper.findComponent(GlBadge);
+ const findGlLink = () => wrapper.findComponent(GlLink);
+
+ describe('status badge text', () => {
+ describe('when import is partial', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ status: STATUSES.FINISHED,
+ hasFailures: true,
+ },
+ });
+ });
+
+ it('renders warning badge with text', () => {
+ expect(findGlBadge().props()).toMatchObject({
+ icon: 'status-alert',
+ variant: 'warning',
+ });
+ expect(findGlBadge().text()).toBe('Partially completed');
+ });
+ });
+
+ describe.each([
+ STATUSES.CREATED,
+ STATUSES.FAILED,
+ STATUSES.FINISHED,
+ STATUSES.STARTED,
+ STATUSES.TIMEOUT,
+ ])(`when import is %s`, (status) => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ status,
+ },
+ });
+ });
+
+ it('renders badge with text', () => {
+ const expectedStatus = STATUS_ICON_MAP[status];
+
+ expect(findGlBadge().props()).toMatchObject({
+ icon: expectedStatus.icon,
+ variant: expectedStatus.variant,
+ });
+ expect(findGlBadge().text()).toBe(expectedStatus.text);
+ });
+ });
+ });
+
+ describe('details link', () => {
+ it('does not render by default', () => {
+ createComponent();
+
+ expect(findGlLink().exists()).toBe(false);
+ });
+
+ it('renders with correct link when import is partial', () => {
+ createComponent({
+ props: {
+ id: 2,
+ entityId: 11,
+ hasFailures: true,
+ showDetailsLink: true,
+ status: STATUSES.FINISHED,
+ },
+ });
+
+ expect(findGlLink().attributes('href')).toBe('/details?id=2&entity_id=11');
+ });
+ });
+});
diff --git a/spec/frontend/import_entities/import_projects/components/github_organizations_box_spec.js b/spec/frontend/import_entities/import_projects/components/github_organizations_box_spec.js
index b6f96cd6a23..7b3758cbd25 100644
--- a/spec/frontend/import_entities/import_projects/components/github_organizations_box_spec.js
+++ b/spec/frontend/import_entities/import_projects/components/github_organizations_box_spec.js
@@ -1,8 +1,8 @@
import { GlCollapsibleListbox } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import { mount } from '@vue/test-utils';
-import { captureException } from '@sentry/browser';
import { nextTick } from 'vue';
+import { captureException } from '~/sentry/sentry_browser_wrapper';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
@@ -10,7 +10,7 @@ import { createAlert } from '~/alert';
import GithubOrganizationsBox from '~/import_entities/import_projects/components/github_organizations_box.vue';
-jest.mock('@sentry/browser');
+jest.mock('~/sentry/sentry_browser_wrapper');
jest.mock('~/alert');
const MOCK_RESPONSE = {
diff --git a/spec/frontend/incidents/components/incidents_list_spec.js b/spec/frontend/incidents/components/incidents_list_spec.js
index 470d63e7c2a..ff413c3feac 100644
--- a/spec/frontend/incidents/components/incidents_list_spec.js
+++ b/spec/frontend/incidents/components/incidents_list_spec.js
@@ -278,7 +278,7 @@ describe('Incidents List', () => {
${'severity'} | ${TH_SEVERITY_TEST_ID} | ${noneSort} | ${descSort} | ${ascSort}
${'status'} | ${TH_ESCALATION_STATUS_TEST_ID} | ${noneSort} | ${descSort} | ${ascSort}
${'publish date'} | ${TH_PUBLISHED_TEST_ID} | ${noneSort} | ${descSort} | ${ascSort}
- ${'due date'} | ${TH_INCIDENT_SLA_TEST_ID} | ${noneSort} | ${ascSort} | ${descSort}
+ ${'due date'} | ${TH_INCIDENT_SLA_TEST_ID} | ${noneSort} | ${descSort} | ${ascSort}
`(
'updates sort with new direction when sorting by $description',
async ({ selector, initialSort, firstSort, nextSort }) => {
diff --git a/spec/frontend/integrations/edit/components/dynamic_field_spec.js b/spec/frontend/integrations/edit/components/dynamic_field_spec.js
index e1d9aef752f..95d15eb2c00 100644
--- a/spec/frontend/integrations/edit/components/dynamic_field_spec.js
+++ b/spec/frontend/integrations/edit/components/dynamic_field_spec.js
@@ -1,16 +1,22 @@
import { GlFormGroup, GlFormCheckbox, GlFormInput, GlFormSelect, GlFormTextarea } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
+import Vue from 'vue';
+// eslint-disable-next-line no-restricted-imports
+import Vuex from 'vuex';
+
import DynamicField from '~/integrations/edit/components/dynamic_field.vue';
import { mockField } from '../mock_data';
+Vue.use(Vuex);
+
describe('DynamicField', () => {
let wrapper;
+ let store;
const createComponent = (props, isInheriting = false, editable = true) => {
- wrapper = mount(DynamicField, {
- propsData: { ...mockField, ...props },
- computed: {
+ store = new Vuex.Store({
+ getters: {
isInheriting: () => isInheriting,
propsSource: () => {
return {
@@ -19,6 +25,11 @@ describe('DynamicField', () => {
},
},
});
+
+ wrapper = mount(DynamicField, {
+ propsData: { ...mockField, ...props },
+ store,
+ });
};
const findGlFormGroup = () => wrapper.findComponent(GlFormGroup);
@@ -29,11 +40,11 @@ describe('DynamicField', () => {
describe('template', () => {
describe.each`
- isInheriting | editable | disabled | readonly | checkboxLabel
- ${true} | ${true} | ${'disabled'} | ${'readonly'} | ${undefined}
- ${false} | ${true} | ${undefined} | ${undefined} | ${'Custom checkbox label'}
- ${true} | ${false} | ${'disabled'} | ${'readonly'} | ${undefined}
- ${false} | ${false} | ${'disabled'} | ${undefined} | ${'Custom checkbox label'}
+ isInheriting | editable | disabled | readonly | checkboxLabel
+ ${true} | ${true} | ${'disabled'} | ${true} | ${undefined}
+ ${false} | ${true} | ${undefined} | ${false} | ${'Custom checkbox label'}
+ ${true} | ${false} | ${'disabled'} | ${true} | ${undefined}
+ ${false} | ${false} | ${'disabled'} | ${false} | ${'Custom checkbox label'}
`(
'dynamic field, when isInheriting = `$isInheriting` and editable = `$editable`',
({ isInheriting, editable, disabled, readonly, checkboxLabel }) => {
@@ -108,7 +119,7 @@ describe('DynamicField', () => {
it(`renders GlFormTextarea, which ${isInheriting ? 'is' : 'is not'} readonly`, () => {
expect(findGlFormTextarea().exists()).toBe(true);
- expect(findGlFormTextarea().find('textarea').attributes('readonly')).toBe(readonly);
+ expect('readonly' in findGlFormTextarea().find('textarea').attributes()).toBe(readonly);
});
it('does not render other types of input', () => {
@@ -132,7 +143,7 @@ describe('DynamicField', () => {
it(`renders GlFormInput, which ${isInheriting ? 'is' : 'is not'} readonly`, () => {
expect(findGlFormInput().exists()).toBe(true);
expect(findGlFormInput().attributes('type')).toBe('password');
- expect(findGlFormInput().attributes('readonly')).toBe(readonly);
+ expect('readonly' in findGlFormInput().attributes()).toBe(readonly);
});
it('does not render other types of input', () => {
@@ -161,9 +172,9 @@ describe('DynamicField', () => {
id: 'service_project_url',
name: 'service[project_url]',
placeholder: mockField.placeholder,
- required: 'required',
+ required: expect.any(String),
});
- expect(findGlFormInput().attributes('readonly')).toBe(readonly);
+ expect('readonly' in findGlFormInput().attributes()).toBe(readonly);
});
it('does not render other types of input', () => {
diff --git a/spec/frontend/integrations/edit/components/integration_form_spec.js b/spec/frontend/integrations/edit/components/integration_form_spec.js
index 5aa3ee35379..cef8fb0b720 100644
--- a/spec/frontend/integrations/edit/components/integration_form_spec.js
+++ b/spec/frontend/integrations/edit/components/integration_form_spec.js
@@ -2,7 +2,7 @@ import { GlAlert, GlForm } from '@gitlab/ui';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { setHTMLFixture } from 'helpers/fixtures';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -29,7 +29,7 @@ import {
mockSectionJiraIssues,
} from '../mock_data';
-jest.mock('@sentry/browser');
+jest.mock('~/sentry/sentry_browser_wrapper');
jest.mock('~/lib/utils/url_utility');
describe('IntegrationForm', () => {
diff --git a/spec/frontend/integrations/overrides/components/integration_overrides_spec.js b/spec/frontend/integrations/overrides/components/integration_overrides_spec.js
index 9e863eaecfd..ac67d53e00d 100644
--- a/spec/frontend/integrations/overrides/components/integration_overrides_spec.js
+++ b/spec/frontend/integrations/overrides/components/integration_overrides_spec.js
@@ -1,7 +1,7 @@
import { GlTable, GlLink, GlPagination, GlAlert } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
import { shallowMount, mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import waitForPromises from 'helpers/wait_for_promises';
import { DEFAULT_PER_PAGE } from '~/api';
import IntegrationOverrides from '~/integrations/overrides/components/integration_overrides.vue';
diff --git a/spec/frontend/invite_members/components/invite_members_modal_spec.js b/spec/frontend/invite_members/components/invite_members_modal_spec.js
index cfc2fd65cc1..19b7fad5fc8 100644
--- a/spec/frontend/invite_members/components/invite_members_modal_spec.js
+++ b/spec/frontend/invite_members/components/invite_members_modal_spec.js
@@ -1,4 +1,4 @@
-import { GlModal, GlSprintf, GlFormGroup, GlCollapse, GlIcon } from '@gitlab/ui';
+import { GlLink, GlModal, GlSprintf, GlFormGroup, GlCollapse, GlIcon } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import { stubComponent } from 'helpers/stub_component';
@@ -60,6 +60,7 @@ describe('InviteMembersModal', () => {
let mock;
let trackingSpy;
const showToast = jest.fn();
+ const newUsersUrl = '/new/users/url';
const expectTracking = (action, label = undefined, property = undefined) =>
expect(trackingSpy).toHaveBeenCalledWith(INVITE_MEMBER_MODAL_TRACKING_CATEGORY, action, {
@@ -68,11 +69,13 @@ describe('InviteMembersModal', () => {
property,
});
- const createComponent = (props = {}, stubs = {}) => {
+ const createComponent = (props = {}, stubs = {}, provide = {}) => {
wrapper = shallowMountExtended(InviteMembersModal, {
provide: {
newProjectPath,
name: propsData.name,
+ newUsersUrl,
+ ...provide,
},
propsData: {
usersLimitDataset: {},
@@ -129,6 +132,7 @@ describe('InviteMembersModal', () => {
const findEmptyInvitesAlert = () => wrapper.findByTestId('empty-invites-alert');
const findMemberErrorAlert = () => wrapper.findByTestId('alert-member-error');
const findMoreInviteErrorsButton = () => wrapper.findByTestId('accordion-button');
+ const findEmailSignupDisabledAlert = () => wrapper.findByTestId('email-signup-disabled-alert');
const findUserLimitAlert = () => wrapper.findComponent(UserLimitNotification);
const findAccordion = () => wrapper.findComponent(GlCollapse);
const findErrorsIcon = () => wrapper.findComponent(GlIcon);
@@ -759,6 +763,58 @@ describe('InviteMembersModal', () => {
expect(findMemberErrorAlert().exists()).toBe(false);
});
});
+
+ describe('when email signup is not allowed', () => {
+ beforeEach(() => {
+ createComponent({}, {}, { isEmailSignupEnabled: false });
+ });
+
+ it('shows the correct form description', () => {
+ expect(membersFormGroupDescription()).toBe('Select members');
+ });
+
+ it('shows an alert', () => {
+ expect(findEmailSignupDisabledAlert().text()).toBe(
+ "Administrators can add new users by email manually. After they've been added, you can invite them to this group with their username.",
+ );
+ });
+
+ it('does not render a link', () => {
+ expect(findEmailSignupDisabledAlert().findComponent(GlLink).exists()).toBe(false);
+ });
+
+ describe('when the current user is an admin', () => {
+ beforeEach(() => {
+ createComponent({}, {}, { isCurrentUserAdmin: true, isEmailSignupEnabled: false });
+ });
+
+ it('shows an alert', () => {
+ expect(findEmailSignupDisabledAlert().text()).toBe(
+ "Administrators can add new users by email manually. After they've been added, you can invite them to this group with their username.",
+ );
+ });
+
+ it('renders a link', () => {
+ expect(findEmailSignupDisabledAlert().findComponent(GlLink).attributes('href')).toBe(
+ newUsersUrl,
+ );
+ });
+
+ describe('when no new users url is provided', () => {
+ beforeEach(() => {
+ createComponent(
+ {},
+ {},
+ { isCurrentUserAdmin: true, isEmailSignupEnabled: false, newUsersUrl: '' },
+ );
+ });
+
+ it('does not render a link', () => {
+ expect(findEmailSignupDisabledAlert().findComponent(GlLink).exists()).toBe(false);
+ });
+ });
+ });
+ });
});
describe('when inviting members and non-members in same click', () => {
diff --git a/spec/frontend/invite_members/components/members_token_select_spec.js b/spec/frontend/invite_members/components/members_token_select_spec.js
index 925534edd7c..a4b8a8b0197 100644
--- a/spec/frontend/invite_members/components/members_token_select_spec.js
+++ b/spec/frontend/invite_members/components/members_token_select_spec.js
@@ -157,6 +157,21 @@ describe('MembersTokenSelect', () => {
expect(tokenSelector.props('allowUserDefinedTokens')).toBe(result);
});
+
+ describe('when cannot use email token', () => {
+ beforeEach(() => {
+ wrapper = createComponent({ canUseEmailToken: false });
+ tokenSelector = findTokenSelector();
+
+ tokenSelector.vm.$emit('text-input', 'foo@bar.com');
+
+ return nextTick();
+ });
+
+ it('does not allow user defined tokens', () => {
+ expect(tokenSelector.props('allowUserDefinedTokens')).toBe(false);
+ });
+ });
});
});
diff --git a/spec/frontend/invite_members/utils/member_utils_spec.js b/spec/frontend/invite_members/utils/member_utils_spec.js
index abae43c3dbb..4d71a35ff99 100644
--- a/spec/frontend/invite_members/utils/member_utils_spec.js
+++ b/spec/frontend/invite_members/utils/member_utils_spec.js
@@ -1,4 +1,8 @@
-import { memberName, triggerExternalAlert } from '~/invite_members/utils/member_utils';
+import {
+ memberName,
+ triggerExternalAlert,
+ inviteMembersTrackingOptions,
+} from '~/invite_members/utils/member_utils';
jest.mock('~/lib/utils/url_utility');
@@ -18,3 +22,13 @@ describe('Trigger External Alert', () => {
expect(triggerExternalAlert()).toBe(false);
});
});
+
+describe('inviteMembersTrackingOptions', () => {
+ it('returns options with a label', () => {
+ expect(inviteMembersTrackingOptions({ label: '_label_' })).toEqual({ label: '_label_' });
+ });
+
+ it('handles options that has no label', () => {
+ expect(inviteMembersTrackingOptions({})).toEqual({ label: undefined });
+ });
+});
diff --git a/spec/frontend/issuable/components/locked_badge_spec.js b/spec/frontend/issuable/components/locked_badge_spec.js
index 73ab6e36ba1..46143d16712 100644
--- a/spec/frontend/issuable/components/locked_badge_spec.js
+++ b/spec/frontend/issuable/components/locked_badge_spec.js
@@ -39,7 +39,7 @@ describe('LockedBadge component', () => {
it('has title', () => {
expect(findBadge().attributes('title')).toBe(
- 'This issue is locked. Only project members can comment.',
+ 'The discussion in this issue is locked. Only project members can comment.',
);
});
});
diff --git a/spec/frontend/issuable/components/status_badge_spec.js b/spec/frontend/issuable/components/status_badge_spec.js
index cdc848626c7..9ab5b4f7149 100644
--- a/spec/frontend/issuable/components/status_badge_spec.js
+++ b/spec/frontend/issuable/components/status_badge_spec.js
@@ -16,10 +16,10 @@ describe('StatusBadge component', () => {
${'merge_request'} | ${'Open'} | ${'opened'} | ${'success'} | ${'merge-request-open'}
${'merge_request'} | ${'Closed'} | ${'closed'} | ${'danger'} | ${'merge-request-close'}
${'merge_request'} | ${'Merged'} | ${'merged'} | ${'info'} | ${'merge'}
- ${'issue'} | ${'Open'} | ${'opened'} | ${'success'} | ${'issues'}
- ${'issue'} | ${'Closed'} | ${'closed'} | ${'info'} | ${'issue-closed'}
- ${'epic'} | ${'Open'} | ${'opened'} | ${'success'} | ${'epic'}
- ${'epic'} | ${'Closed'} | ${'closed'} | ${'info'} | ${'epic-closed'}
+ ${'issue'} | ${'Open'} | ${'opened'} | ${'success'} | ${'issue-open-m'}
+ ${'issue'} | ${'Closed'} | ${'closed'} | ${'info'} | ${'issue-close'}
+ ${'epic'} | ${'Open'} | ${'opened'} | ${'success'} | ${'issue-open-m'}
+ ${'epic'} | ${'Closed'} | ${'closed'} | ${'info'} | ${'issue-close'}
`(
'when issuableType=$issuableType and state=$state',
({ issuableType, badgeText, state, badgeVariant, badgeIcon }) => {
diff --git a/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js b/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js
index f6c9fab76d1..35699568793 100644
--- a/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js
+++ b/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js
@@ -1,9 +1,9 @@
import { GlDisclosureDropdown, GlEmptyState } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
import AxiosMockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { cloneDeep } from 'lodash';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import getIssuesQuery from 'ee_else_ce/issues/dashboard/queries/get_issues.query.graphql';
import IssueCardStatistics from 'ee_else_ce/issues/list/components/issue_card_statistics.vue';
import IssueCardTimeInfo from 'ee_else_ce/issues/list/components/issue_card_time_info.vue';
@@ -45,7 +45,7 @@ import {
issuesQueryResponse,
} from '../mock_data';
-jest.mock('@sentry/browser');
+jest.mock('~/sentry/sentry_browser_wrapper');
jest.mock('~/lib/utils/scroll_utils', () => ({ scrollUp: jest.fn() }));
describe('IssuesDashboardApp component', () => {
diff --git a/spec/frontend/issues/issue_spec.js b/spec/frontend/issues/issue_spec.js
index bf2ca42f71f..b976a051f7a 100644
--- a/spec/frontend/issues/issue_spec.js
+++ b/spec/frontend/issues/issue_spec.js
@@ -58,7 +58,17 @@ describe('Issue', () => {
);
});
- it('updates issueCounter text', () => {
+ // TODO: Remove this with the removal of the old navigation.
+ // See https://gitlab.com/groups/gitlab-org/-/epics/11875.
+ // See also https://gitlab.com/gitlab-org/gitlab/-/issues/429678 about
+ // reimplementing this in the new navigation.
+ //
+ // Since this entire suite only tests the issue count updating, removing
+ // this test would mean removing the entire suite. But, ~/issues/issue.js
+ // does more than just that. Tests should be written to cover those other
+ // features. So we're just skipping this for now.
+ // eslint-disable-next-line jest/no-disabled-tests
+ it.skip('updates issueCounter text', () => {
expect(testContext.issueCounter).toBeVisible();
expect(testContext.issueCounter).toHaveText(expectedCounterText);
});
diff --git a/spec/frontend/issues/list/components/issues_list_app_spec.js b/spec/frontend/issues/list/components/issues_list_app_spec.js
index f830168ce5d..6bd952cd215 100644
--- a/spec/frontend/issues/list/components/issues_list_app_spec.js
+++ b/spec/frontend/issues/list/components/issues_list_app_spec.js
@@ -1,11 +1,11 @@
import { GlButton, GlDisclosureDropdown, GlDrawer } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
import { mount, shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import { cloneDeep } from 'lodash';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import VueRouter from 'vue-router';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import getIssuesQuery from 'ee_else_ce/issues/list/queries/get_issues.query.graphql';
import getIssuesCountsQuery from 'ee_else_ce/issues/list/queries/get_issues_counts.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
@@ -87,7 +87,7 @@ import {
import('~/issuable');
import('~/users_select');
-jest.mock('@sentry/browser');
+jest.mock('~/sentry/sentry_browser_wrapper');
jest.mock('~/alert');
jest.mock('~/lib/utils/scroll_utils', () => ({ scrollUp: jest.fn() }));
diff --git a/spec/frontend/issues/service_desk/components/service_desk_list_app_spec.js b/spec/frontend/issues/service_desk/components/service_desk_list_app_spec.js
index d28b4f2fe76..bb388cefa95 100644
--- a/spec/frontend/issues/service_desk/components/service_desk_list_app_spec.js
+++ b/spec/frontend/issues/service_desk/components/service_desk_list_app_spec.js
@@ -3,8 +3,8 @@ import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { cloneDeep } from 'lodash';
import VueRouter from 'vue-router';
-import * as Sentry from '@sentry/browser';
import AxiosMockAdapter from 'axios-mock-adapter';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import axios from '~/lib/utils/axios_utils';
import createMockApollo from 'helpers/mock_apollo_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
@@ -55,7 +55,7 @@ import {
locationSearch,
} from '../mock_data';
-jest.mock('@sentry/browser');
+jest.mock('~/sentry/sentry_browser_wrapper');
jest.mock('~/alert');
jest.mock('~/lib/utils/scroll_utils', () => ({ scrollUp: jest.fn() }));
diff --git a/spec/frontend/issues/show/components/header_actions_spec.js b/spec/frontend/issues/show/components/header_actions_spec.js
index e508045eff3..d0c2a1a5f1b 100644
--- a/spec/frontend/issues/show/components/header_actions_spec.js
+++ b/spec/frontend/issues/show/components/header_actions_spec.js
@@ -1,9 +1,16 @@
import Vue, { nextTick } from 'vue';
-import { GlDropdown, GlDropdownItem, GlLink, GlModal, GlButton } from '@gitlab/ui';
+import {
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
+ GlLink,
+ GlModal,
+ GlButton,
+} from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import VueApollo from 'vue-apollo';
+import { stubComponent } from 'helpers/stub_component';
import waitForPromises from 'helpers/wait_for_promises';
import { mockTracking } from 'helpers/tracking_helper';
import { createAlert, VARIANT_SUCCESS } from '~/alert';
@@ -120,8 +127,10 @@ describe('HeaderActions component', () => {
const findDropdownBy = (dataTestId) => wrapper.find(`[data-testid="${dataTestId}"]`);
const findMobileDropdown = () => findDropdownBy('mobile-dropdown');
const findDesktopDropdown = () => findDropdownBy('desktop-dropdown');
- const findMobileDropdownItems = () => findMobileDropdown().findAllComponents(GlDropdownItem);
- const findDesktopDropdownItems = () => findDesktopDropdown().findAllComponents(GlDropdownItem);
+ const findMobileDropdownItems = () =>
+ findMobileDropdown().findAllComponents(GlDisclosureDropdownItem);
+ const findDesktopDropdownItems = () =>
+ findDesktopDropdown().findAllComponents(GlDisclosureDropdownItem);
const findAbuseCategorySelector = () => wrapper.findComponent(AbuseCategorySelector);
const findReportAbuseButton = () => wrapper.find(`[data-testid="report-abuse-item"]`);
const findNotificationWidget = () => wrapper.find(`[data-testid="notification-toggle"]`);
@@ -179,6 +188,11 @@ describe('HeaderActions component', () => {
},
stubs: {
GlButton,
+ GlDisclosureDropdown: stubComponent(GlDisclosureDropdown, {
+ methods: {
+ close: jest.fn(),
+ },
+ }),
},
});
};
@@ -217,7 +231,7 @@ describe('HeaderActions component', () => {
});
it('calls apollo mutation', () => {
- findToggleIssueStateButton().vm.$emit('click');
+ findToggleIssueStateButton().vm.$emit('action');
expect(updateIssueMutationResponseHandler).toHaveBeenCalledWith({
input: {
@@ -229,7 +243,7 @@ describe('HeaderActions component', () => {
});
it('dispatches a custom event to update the issue page', async () => {
- findToggleIssueStateButton().vm.$emit('click');
+ findToggleIssueStateButton().vm.$emit('action');
await waitForPromises();
@@ -286,7 +300,11 @@ describe('HeaderActions component', () => {
it(`${isItemVisible ? 'shows' : 'hides'} "${itemText}" item`, () => {
expect(
findDropdownItems()
- .filter((item) => item.text() === itemText)
+ .filter((item) => {
+ return item.props('item')
+ ? item.props('item').text === itemText
+ : item.text() === itemText;
+ })
.exists(),
).toBe(isItemVisible);
});
@@ -313,7 +331,7 @@ describe('HeaderActions component', () => {
it('should trigger "open.form" event when clicked', async () => {
expect(issuesEventHub.$emit).not.toHaveBeenCalled();
- await findEditButton().trigger('click');
+ await findEditButton().vm.$emit('click');
expect(issuesEventHub.$emit).toHaveBeenCalledWith('open.form');
});
});
@@ -328,7 +346,7 @@ describe('HeaderActions component', () => {
});
it('tracks clicking on button', () => {
- findDesktopDropdownItems().at(4).vm.$emit('click');
+ findDesktopDropdownItems().at(4).vm.$emit('action');
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_dropdown', {
label: 'delete_issue',
@@ -345,7 +363,7 @@ describe('HeaderActions component', () => {
promoteToEpicHandler: promoteToEpicMutationSuccessResponseHandler,
});
- wrapper.find('[data-testid="promote-button"]').vm.$emit('click');
+ wrapper.find('[data-testid="promote-button"]').vm.$emit('action');
await waitForPromises();
});
@@ -381,7 +399,7 @@ describe('HeaderActions component', () => {
promoteToEpicHandler: promoteToEpicMutationErrorHandler,
});
- wrapper.find('[data-testid="promote-button"]').vm.$emit('click');
+ wrapper.find('[data-testid="promote-button"]').vm.$emit('action');
await waitForPromises();
});
@@ -483,7 +501,7 @@ describe('HeaderActions component', () => {
});
it('opens the abuse category drawer', async () => {
- findReportAbuseButton().vm.$emit('click');
+ findReportAbuseButton().vm.$emit('action');
await nextTick();
@@ -491,7 +509,7 @@ describe('HeaderActions component', () => {
});
it('closes the abuse category drawer', async () => {
- await findReportAbuseButton().vm.$emit('click');
+ await findReportAbuseButton().vm.$emit('action');
expect(findAbuseCategorySelector().exists()).toEqual(true);
await findAbuseCategorySelector().vm.$emit('close-drawer');
@@ -603,7 +621,7 @@ describe('HeaderActions component', () => {
});
it('shows toast message', () => {
- findCopyRefenceDropdownItem().vm.$emit('click');
+ findCopyRefenceDropdownItem().vm.$emit('action');
expect(toast).toHaveBeenCalledWith('Reference copied');
});
@@ -652,7 +670,7 @@ describe('HeaderActions component', () => {
});
it('shows toast message', () => {
- findCopyEmailItem().vm.$emit('click');
+ findCopyEmailItem().vm.$emit('action');
expect(toast).toHaveBeenCalledWith('Email address copied');
});
@@ -710,11 +728,13 @@ describe('HeaderActions component', () => {
props: { issueType, issuableEmailAddress: 'mock-email-address' },
});
- expect(wrapper.findComponent(GlDropdown).attributes('text')).toBe(
+ expect(wrapper.findComponent(GlDisclosureDropdown).props('toggleText')).toBe(
`${capitalizeFirstCharacter(expectedText)} actions`,
);
expect(findDropdownBy('copy-email').text()).toBe(`Copy ${expectedText} email address`);
- expect(findDesktopDropdownItems().at(1).text()).toBe(`New related ${expectedText}`);
+ expect(findDesktopDropdownItems().at(1).props('item').text).toBe(
+ `New related ${expectedText}`,
+ );
});
});
});
diff --git a/spec/frontend/issues/show/components/issue_header_spec.js b/spec/frontend/issues/show/components/issue_header_spec.js
index 6acc7004576..6c4e357d722 100644
--- a/spec/frontend/issues/show/components/issue_header_spec.js
+++ b/spec/frontend/issues/show/components/issue_header_spec.js
@@ -47,7 +47,7 @@ describe('IssueHeader component', () => {
issuableType: 'issue',
serviceDeskReplyTo: '',
showWorkItemTypeIcon: true,
- statusIcon: 'issues',
+ statusIcon: 'issue-open-m',
workspaceType: 'project',
});
});
@@ -63,7 +63,7 @@ describe('IssueHeader component', () => {
});
it('renders correct icon', () => {
- expect(findIssuableHeader().props('statusIcon')).toBe('issues');
+ expect(findIssuableHeader().props('statusIcon')).toBe('issue-open-m');
});
});
@@ -77,7 +77,7 @@ describe('IssueHeader component', () => {
});
it('renders correct icon', () => {
- expect(findIssuableHeader().props('statusIcon')).toBe('issue-closed');
+ expect(findIssuableHeader().props('statusIcon')).toBe('issue-close');
});
describe('when issue is marked as duplicate', () => {
diff --git a/spec/frontend/issues/show/components/sticky_header_spec.js b/spec/frontend/issues/show/components/sticky_header_spec.js
index a909084956f..43d96f398b6 100644
--- a/spec/frontend/issues/show/components/sticky_header_spec.js
+++ b/spec/frontend/issues/show/components/sticky_header_spec.js
@@ -36,12 +36,12 @@ describe('StickyHeader component', () => {
it.each`
issuableType | issuableStatus | statusIcon
- ${TYPE_INCIDENT} | ${STATUS_OPEN} | ${'issues'}
- ${TYPE_INCIDENT} | ${STATUS_CLOSED} | ${'issue-closed'}
- ${TYPE_ISSUE} | ${STATUS_OPEN} | ${'issues'}
- ${TYPE_ISSUE} | ${STATUS_CLOSED} | ${'issue-closed'}
- ${TYPE_EPIC} | ${STATUS_OPEN} | ${'epic'}
- ${TYPE_EPIC} | ${STATUS_CLOSED} | ${'epic-closed'}
+ ${TYPE_INCIDENT} | ${STATUS_OPEN} | ${'issue-open-m'}
+ ${TYPE_INCIDENT} | ${STATUS_CLOSED} | ${'issue-close'}
+ ${TYPE_ISSUE} | ${STATUS_OPEN} | ${'issue-open-m'}
+ ${TYPE_ISSUE} | ${STATUS_CLOSED} | ${'issue-close'}
+ ${TYPE_EPIC} | ${STATUS_OPEN} | ${'issue-open-m'}
+ ${TYPE_EPIC} | ${STATUS_CLOSED} | ${'issue-close'}
`(
'shows with state icon "$statusIcon" for $issuableType when status is $issuableStatus',
({ issuableType, issuableStatus, statusIcon }) => {
diff --git a/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions_spec.js b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions_spec.js
index 40ea6058c70..efe89100e90 100644
--- a/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions_spec.js
@@ -1,15 +1,23 @@
+import { nextTick } from 'vue';
import { shallowMount } from '@vue/test-utils';
-import { GlButton, GlLink } from '@gitlab/ui';
+import { GlButton, GlFormCheckbox, GlLink } from '@gitlab/ui';
-import { OAUTH_SELF_MANAGED_DOC_LINK } from '~/jira_connect/subscriptions/constants';
+import {
+ PREREQUISITES_DOC_LINK,
+ OAUTH_SELF_MANAGED_DOC_LINK,
+ SET_UP_INSTANCE_DOC_LINK,
+} from '~/jira_connect/subscriptions/constants';
import SetupInstructions from '~/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions.vue';
describe('SetupInstructions', () => {
let wrapper;
- const findGlLink = () => wrapper.findComponent(GlLink);
+ const findPrerequisitesGlLink = () => wrapper.findAllComponents(GlLink).at(0);
+ const findOAuthGlLink = () => wrapper.findAllComponents(GlLink).at(1);
+ const findSetUpInstanceGlLink = () => wrapper.findAllComponents(GlLink).at(2);
const findBackButton = () => wrapper.findAllComponents(GlButton).at(0);
const findNextButton = () => wrapper.findAllComponents(GlButton).at(1);
+ const findCheckboxAtIndex = (index) => wrapper.findAllComponents(GlFormCheckbox).at(index);
const createComponent = () => {
wrapper = shallowMount(SetupInstructions);
@@ -20,8 +28,34 @@ describe('SetupInstructions', () => {
createComponent();
});
- it('renders "Learn more" link to documentation', () => {
- expect(findGlLink().attributes('href')).toBe(OAUTH_SELF_MANAGED_DOC_LINK);
+ it('renders "Prerequisites" link to documentation', () => {
+ expect(findPrerequisitesGlLink().attributes('href')).toBe(PREREQUISITES_DOC_LINK);
+ });
+
+ it('renders "Set up OAuth authentication" link to documentation', () => {
+ expect(findOAuthGlLink().attributes('href')).toBe(OAUTH_SELF_MANAGED_DOC_LINK);
+ });
+
+ it('renders "Set up your instance" link to documentation', () => {
+ expect(findSetUpInstanceGlLink().attributes('href')).toBe(SET_UP_INSTANCE_DOC_LINK);
+ });
+
+ describe('NextButton', () => {
+ it('emits next event when clicked and all steps checked', async () => {
+ createComponent();
+
+ findCheckboxAtIndex(0).vm.$emit('input', true);
+ findCheckboxAtIndex(1).vm.$emit('input', true);
+ findCheckboxAtIndex(2).vm.$emit('input', true);
+
+ await nextTick();
+
+ expect(findNextButton().attributes('disabled')).toBeUndefined();
+ });
+
+ it('disables button when not all steps are checked', () => {
+ expect(findNextButton().attributes('disabled')).toBe('true');
+ });
});
describe('when "Next" button is clicked', () => {
diff --git a/spec/frontend/lib/utils/color_utils_spec.js b/spec/frontend/lib/utils/color_utils_spec.js
index 92ac66c19f0..aac50a2c850 100644
--- a/spec/frontend/lib/utils/color_utils_spec.js
+++ b/spec/frontend/lib/utils/color_utils_spec.js
@@ -18,15 +18,17 @@ describe('Color utils', () => {
describe('darkModeEnabled', () => {
it.each`
- page | bodyClass | ideTheme | expected
+ page | rootClass | ideTheme | expected
${'ide:index'} | ${'gl-dark'} | ${'monokai-light'} | ${false}
${'ide:index'} | ${'ui-light'} | ${'monokai'} | ${true}
${'groups:issues:index'} | ${'ui-light'} | ${'monokai'} | ${false}
${'groups:issues:index'} | ${'gl-dark'} | ${'monokai-light'} | ${true}
`(
- 'is $expected on $page with $bodyClass body class and $ideTheme IDE theme',
- ({ page, bodyClass, ideTheme, expected }) => {
- document.body.outerHTML = `<body class="${bodyClass}" data-page="${page}"></body>`;
+ 'is $expected on $page with $rootClass root class and $ideTheme IDE theme',
+ ({ page, rootClass, ideTheme, expected }) => {
+ document.documentElement.className = rootClass;
+ document.body.outerHTML = `<body data-page="${page}"></body>`;
+
window.gon = {
user_color_scheme: ideTheme,
};
diff --git a/spec/frontend/lib/utils/common_utils_spec.js b/spec/frontend/lib/utils/common_utils_spec.js
index 8697249ebf5..6295914b127 100644
--- a/spec/frontend/lib/utils/common_utils_spec.js
+++ b/spec/frontend/lib/utils/common_utils_spec.js
@@ -1213,4 +1213,28 @@ describe('common_utils', () => {
expect(cloned.ref === ref).toBe(false);
});
});
+
+ describe('isDefaultCiConfig', () => {
+ it('returns true when the path is the default CI config path', () => {
+ expect(commonUtils.isDefaultCiConfig('.gitlab-ci.yml')).toBe(true);
+ });
+
+ it('returns false when the path is not the default CI config path', () => {
+ expect(commonUtils.isDefaultCiConfig('some/other/path.yml')).toBe(false);
+ });
+ });
+
+ describe('hasCiConfigExtension', () => {
+ it('returns true when the path is the default CI config path', () => {
+ expect(commonUtils.hasCiConfigExtension('.gitlab-ci.yml')).toBe(true);
+ });
+
+ it('returns true when the path has a CI config extension', () => {
+ expect(commonUtils.hasCiConfigExtension('some/path.gitlab-ci.yml')).toBe(true);
+ });
+
+ it('returns false when the path does not have a CI config extension', () => {
+ expect(commonUtils.hasCiConfigExtension('some/other/path.yml')).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/lib/utils/datetime/timeago_utility_spec.js b/spec/frontend/lib/utils/datetime/timeago_utility_spec.js
index 74ce8175357..44db4cf88a2 100644
--- a/spec/frontend/lib/utils/datetime/timeago_utility_spec.js
+++ b/spec/frontend/lib/utils/datetime/timeago_utility_spec.js
@@ -160,5 +160,24 @@ describe('TimeAgo utils', () => {
);
},
);
+
+ describe('With User Setting Time Format', () => {
+ it.each`
+ timeDisplayFormat | display | text
+ ${0} | ${'System'} | ${'Feb 18, 2020, 10:22 PM'}
+ ${1} | ${'12-hour'} | ${'Feb 18, 2020, 10:22 PM'}
+ ${2} | ${'24-hour'} | ${'Feb 18, 2020, 22:22'}
+ `(`'$display' renders as '$text'`, ({ timeDisplayFormat, text }) => {
+ gon.time_display_relative = false;
+ gon.time_display_format = timeDisplayFormat;
+
+ const element = document.querySelector('time');
+ localTimeAgo([element]);
+
+ jest.runAllTimers();
+
+ expect(element.innerText).toBe(text);
+ });
+ });
});
});
diff --git a/spec/frontend/lib/utils/forms_spec.js b/spec/frontend/lib/utils/forms_spec.js
index b97f5bf3c51..88b1f9afdf5 100644
--- a/spec/frontend/lib/utils/forms_spec.js
+++ b/spec/frontend/lib/utils/forms_spec.js
@@ -6,7 +6,8 @@ import {
hasMinimumLength,
isParseableAsInteger,
isIntegerGreaterThan,
- isEmail,
+ isServiceDeskSettingEmail,
+ isUserEmail,
parseRailsFormFields,
} from '~/lib/utils/forms';
@@ -202,7 +203,7 @@ describe('lib/utils/forms', () => {
);
});
- describe('isEmail', () => {
+ describe('isServiceDeskSettingEmail', () => {
it.each`
input | returnValue
${'user-with_special-chars@example.com'} | ${true}
@@ -219,7 +220,28 @@ describe('lib/utils/forms', () => {
${' '} | ${false}
${'12'} | ${false}
`('returns $returnValue for value $input', ({ input, returnValue }) => {
- expect(isEmail(input)).toBe(returnValue);
+ expect(isServiceDeskSettingEmail(input)).toBe(returnValue);
+ });
+ });
+
+ describe('isUserEmail', () => {
+ it.each`
+ input | returnValue
+ ${'user-with_special-chars@example.com'} | ${true}
+ ${'user@subdomain.example.com'} | ${true}
+ ${'user@example.com'} | ${true}
+ ${'user@example.co'} | ${true}
+ ${'user@example.c'} | ${true}
+ ${'user@example'} | ${true}
+ ${''} | ${false}
+ ${[]} | ${false}
+ ${null} | ${false}
+ ${undefined} | ${false}
+ ${'hello'} | ${false}
+ ${' '} | ${false}
+ ${'12'} | ${false}
+ `('returns $returnValue for value $input', ({ input, returnValue }) => {
+ expect(isUserEmail(input)).toBe(returnValue);
});
});
diff --git a/spec/frontend/members/components/avatars/group_avatar_spec.js b/spec/frontend/members/components/avatars/group_avatar_spec.js
index 8e4263f88fe..1463aa5ae59 100644
--- a/spec/frontend/members/components/avatars/group_avatar_spec.js
+++ b/spec/frontend/members/components/avatars/group_avatar_spec.js
@@ -1,8 +1,9 @@
-import { GlAvatarLink } from '@gitlab/ui';
+import { GlAvatarLabeled, GlAvatarLink } from '@gitlab/ui';
import { getByText as getByTextHelper } from '@testing-library/dom';
import { mount, createWrapper } from '@vue/test-utils';
import GroupAvatar from '~/members/components/avatars/group_avatar.vue';
-import { group as member } from '../../mock_data';
+import PrivateIcon from '~/members/components/icons/private_icon.vue';
+import { group as member, privateGroup as privateMember } from '../../mock_data';
describe('MemberList', () => {
let wrapper;
@@ -21,11 +22,9 @@ describe('MemberList', () => {
const getByText = (text, options) =>
createWrapper(getByTextHelper(wrapper.element, text, options));
- beforeEach(() => {
+ it('renders link to group', () => {
createComponent();
- });
- it('renders link to group', () => {
const link = wrapper.findComponent(GlAvatarLink);
expect(link.exists()).toBe(true);
@@ -33,10 +32,26 @@ describe('MemberList', () => {
});
it("renders group's full name", () => {
+ createComponent();
+
expect(getByText(group.fullName).exists()).toBe(true);
});
it("renders group's avatar", () => {
+ createComponent();
+
expect(wrapper.find('img').attributes('src')).toBe(group.avatarUrl);
});
+
+ describe('when group is private', () => {
+ beforeEach(() => {
+ createComponent({ member: privateMember });
+ });
+
+ it('renders private avatar with icon', () => {
+ expect(wrapper.findComponent(GlAvatarLink).exists()).toBe(false);
+ expect(wrapper.findComponent(GlAvatarLabeled).props('label')).toBe('Private');
+ expect(wrapper.findComponent(PrivateIcon).exists()).toBe(true);
+ });
+ });
});
diff --git a/spec/frontend/members/components/icons/private_icon_spec.js b/spec/frontend/members/components/icons/private_icon_spec.js
new file mode 100644
index 00000000000..ea2b65e3307
--- /dev/null
+++ b/spec/frontend/members/components/icons/private_icon_spec.js
@@ -0,0 +1,30 @@
+import { GlIcon } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import PrivateIcon from '~/members/components/icons/private_icon.vue';
+
+describe('PrivateIcon', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = mountExtended(PrivateIcon, {
+ directives: {
+ GlTooltip: createMockDirective('gl-tooltip'),
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders private icon with tooltip', () => {
+ const icon = wrapper.findComponent(GlIcon);
+ const tooltipDirective = getBinding(icon.element, 'gl-tooltip');
+
+ expect(icon.props('name')).toBe('eye-slash');
+ expect(tooltipDirective.value).toBe(
+ 'Private group information is only accessible to its members.',
+ );
+ });
+});
diff --git a/spec/frontend/members/components/table/member_source_spec.js b/spec/frontend/members/components/table/member_source_spec.js
index bbfbb19fd92..16b8d239944 100644
--- a/spec/frontend/members/components/table/member_source_spec.js
+++ b/spec/frontend/members/components/table/member_source_spec.js
@@ -1,6 +1,7 @@
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import MemberSource from '~/members/components/table/member_source.vue';
+import PrivateIcon from '~/members/components/icons/private_icon.vue';
describe('MemberSource', () => {
let wrapper;
@@ -30,6 +31,20 @@ describe('MemberSource', () => {
const getTooltipDirective = (elementWrapper) => getBinding(elementWrapper.element, 'gl-tooltip');
+ describe('when source is private', () => {
+ beforeEach(() => {
+ createComponent({
+ isSharedWithGroupPrivate: true,
+ isDirectMember: false,
+ });
+ });
+
+ it('displays private with icon', () => {
+ expect(wrapper.findByText('Private').exists()).toBe(true);
+ expect(wrapper.findComponent(PrivateIcon).exists()).toBe(true);
+ });
+ });
+
describe('direct member', () => {
describe('when created by is available', () => {
it('displays "Direct member by <user name>"', () => {
diff --git a/spec/frontend/members/components/table/members_table_spec.js b/spec/frontend/members/components/table/members_table_spec.js
index 4539478bf9a..791155fcd1b 100644
--- a/spec/frontend/members/components/table/members_table_spec.js
+++ b/spec/frontend/members/components/table/members_table_spec.js
@@ -27,6 +27,7 @@ import {
directMember,
invite,
accessRequest,
+ privateGroup,
pagination,
} from '../../mock_data';
@@ -245,6 +246,24 @@ describe('MembersTable', () => {
});
});
});
+
+ describe('Source field', () => {
+ beforeEach(() => {
+ createComponent({
+ members: [privateGroup],
+ tableFields: ['source'],
+ });
+ });
+
+ it('passes correct props to `MemberSource` component', () => {
+ expect(wrapper.findComponent(MemberSource).props()).toMatchObject({
+ memberSource: {},
+ isDirectMember: true,
+ isSharedWithGroupPrivate: true,
+ createdBy: null,
+ });
+ });
+ });
});
describe('when `members` is an empty array', () => {
diff --git a/spec/frontend/members/components/table/role_dropdown_spec.js b/spec/frontend/members/components/table/role_dropdown_spec.js
index 5204ac2fdbe..62275a05dc5 100644
--- a/spec/frontend/members/components/table/role_dropdown_spec.js
+++ b/spec/frontend/members/components/table/role_dropdown_spec.js
@@ -1,10 +1,10 @@
import { GlCollapsibleListbox, GlListboxItem } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
-import * as Sentry from '@sentry/browser';
import { mount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import waitForPromises from 'helpers/wait_for_promises';
import RoleDropdown from '~/members/components/table/role_dropdown.vue';
import { MEMBER_TYPES } from '~/members/constants';
@@ -13,7 +13,7 @@ import { member } from '../../mock_data';
Vue.use(Vuex);
jest.mock('ee_else_ce/members/guest_overage_confirm_action');
-jest.mock('@sentry/browser');
+jest.mock('~/sentry/sentry_browser_wrapper');
describe('RoleDropdown', () => {
let wrapper;
@@ -71,9 +71,7 @@ describe('RoleDropdown', () => {
it('has items prop with all valid roles', () => {
createComponent();
- const roles = findListbox()
- .props('items')
- .map((item) => item.text);
+ const roles = findListboxItems().wrappers.map((item) => item.text());
expect(roles).toEqual(Object.keys(member.validRoles));
});
@@ -102,7 +100,7 @@ describe('RoleDropdown', () => {
expect(actions.updateMemberRole).toHaveBeenCalledWith(expect.any(Object), {
memberId: member.id,
- accessLevel: { integerValue: 30, stringValue: 'Developer' },
+ accessLevel: { integerValue: 30, memberRoleId: null },
});
});
@@ -247,7 +245,7 @@ describe('RoleDropdown', () => {
});
it('resets selected dropdown item', () => {
- expect(findListbox().props('selected')).toBe(member.validRoles.Owner);
+ expect(findListbox().props('selected')).toMatch(/role-static-\d+/);
});
});
});
diff --git a/spec/frontend/members/mock_data.js b/spec/frontend/members/mock_data.js
index 161e96c0c48..e0dc765b9e4 100644
--- a/spec/frontend/members/mock_data.js
+++ b/spec/frontend/members/mock_data.js
@@ -41,7 +41,7 @@ export const member = {
usingLicense: false,
groupSso: false,
groupManagedAccount: false,
- provisionedByThisGroup: false,
+ enterpriseUserOfThisGroup: false,
validRoles: {
Guest: 10,
Reporter: 20,
@@ -50,6 +50,7 @@ export const member = {
Owner: 50,
'Minimal access': 5,
},
+ customRoles: [],
};
export const group = {
@@ -69,6 +70,19 @@ export const group = {
validRoles: { Guest: 10, Reporter: 20, Developer: 30, Maintainer: 40, Owner: 50 },
};
+export const privateGroup = {
+ accessLevel: { integerValue: 10, stringValue: 'Guest' },
+ isSharedWithGroupPrivate: true,
+ sharedWithGroup: {
+ id: 24,
+ },
+ id: 3,
+ isDirectMember: true,
+ createdAt: '2020-08-06T15:31:07.662Z',
+ expiresAt: null,
+ validRoles: { Guest: 10, Reporter: 20, Developer: 30, Maintainer: 40, Owner: 50 },
+};
+
export const modalData = {
isAccessRequest: true,
isInvite: true,
diff --git a/spec/frontend/members/store/actions_spec.js b/spec/frontend/members/store/actions_spec.js
index 38214048b23..3df3d85c4f1 100644
--- a/spec/frontend/members/store/actions_spec.js
+++ b/spec/frontend/members/store/actions_spec.js
@@ -15,6 +15,8 @@ import {
} from '~/members/store/actions';
import * as types from '~/members/store/mutation_types';
+const mockedRequestFormatter = jest.fn().mockImplementation(noop);
+
describe('Vuex members actions', () => {
describe('update member actions', () => {
let mock;
@@ -22,7 +24,7 @@ describe('Vuex members actions', () => {
const state = {
members,
memberPath: '/groups/foo-bar/-/group_members/:id',
- requestFormatter: noop,
+ requestFormatter: mockedRequestFormatter,
};
beforeEach(() => {
@@ -35,7 +37,7 @@ describe('Vuex members actions', () => {
describe('updateMemberRole', () => {
const memberId = members[0].id;
- const accessLevel = { integerValue: 30, stringValue: 'Developer' };
+ const accessLevel = { integerValue: 30, memberRoleId: 90 };
const payload = {
memberId,
@@ -54,6 +56,10 @@ describe('Vuex members actions', () => {
]);
expect(mock.history.put[0].url).toBe('/groups/foo-bar/-/group_members/238');
+ expect(mockedRequestFormatter).toHaveBeenCalledWith({
+ accessLevel: accessLevel.integerValue,
+ memberRoleId: accessLevel.memberRoleId,
+ });
});
});
diff --git a/spec/frontend/members/utils_spec.js b/spec/frontend/members/utils_spec.js
index c4357e9c1f0..54f5433c9c9 100644
--- a/spec/frontend/members/utils_spec.js
+++ b/spec/frontend/members/utils_spec.js
@@ -22,6 +22,8 @@ import {
buildSortHref,
parseDataAttributes,
groupLinkRequestFormatter,
+ roleDropdownItems,
+ initialSelectedRole,
} from '~/members/utils';
import {
member as memberMock,
@@ -35,6 +37,8 @@ import {
dataAttribute,
} from './mock_data';
+jest.mock('lodash/uniqueId', () => (prefix) => `${prefix}0`);
+
const IS_CURRENT_USER_ID = 123;
const IS_NOT_CURRENT_USER_ID = 124;
const URL_HOST = 'https://localhost/';
@@ -317,7 +321,46 @@ describe('Members Utils', () => {
accessLevel: 50,
expires_at: '2020-10-16',
}),
- ).toEqual({ group_link: { group_access: 50, expires_at: '2020-10-16' } });
+ ).toEqual({
+ group_link: { group_access: 50, expires_at: '2020-10-16', member_role_id: null },
+ });
+
+ expect(
+ groupLinkRequestFormatter({
+ accessLevel: 50,
+ expires_at: '2020-10-16',
+ memberRoleId: 80,
+ }),
+ ).toEqual({
+ group_link: { group_access: 50, expires_at: '2020-10-16', member_role_id: 80 },
+ });
+ });
+ });
+
+ describe('roleDropdownItems', () => {
+ it('returns properly flatten and formatted dropdowns', () => {
+ const { flatten, formatted } = roleDropdownItems(members[0]);
+
+ expect(flatten).toEqual(formatted);
+ expect(flatten[0]).toMatchObject({
+ text: 'Guest',
+ value: 'role-static-0',
+ accessLevel: 10,
+ memberRoleId: null,
+ });
+ });
+ });
+
+ describe('initialSelectedRole', () => {
+ it('find and return correct value', () => {
+ expect(
+ initialSelectedRole(
+ [{ accessLevel: 10, memberRoleId: null, text: 'Guest', value: 'role-static-0' }],
+ {
+ accessLevel: { integerValue: 10 },
+ },
+ ),
+ ).toBe('role-static-0');
});
});
});
diff --git a/spec/frontend/merge_requests/components/sticky_header_spec.js b/spec/frontend/merge_requests/components/sticky_header_spec.js
new file mode 100644
index 00000000000..9fc265cd9ad
--- /dev/null
+++ b/spec/frontend/merge_requests/components/sticky_header_spec.js
@@ -0,0 +1,48 @@
+import Vue from 'vue';
+// eslint-disable-next-line no-restricted-imports
+import Vuex from 'vuex';
+import { GlSprintf } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import StickyHeader from '~/merge_requests/components/sticky_header.vue';
+
+Vue.use(Vuex);
+
+let wrapper;
+
+function createComponent(provide = {}) {
+ const store = new Vuex.Store({
+ state: {
+ page: { activeTab: 'overview' },
+ notes: { notes: { doneFetchingBatchDiscussions: true } },
+ },
+ getters: {
+ getNoteableData: () => ({
+ id: 1,
+ source_branch: 'source-branch',
+ target_branch: 'main',
+ }),
+ discussionTabCounter: () => 1,
+ },
+ });
+
+ wrapper = shallowMountExtended(StickyHeader, {
+ store,
+ provide,
+ stubs: {
+ GlSprintf,
+ },
+ });
+}
+
+describe('Merge requests sticky header component', () => {
+ describe('forked project', () => {
+ it('renders source branch with source project path', () => {
+ createComponent({
+ projectPath: 'gitlab-org/gitlab',
+ sourceProjectPath: 'root/gitlab',
+ });
+
+ expect(wrapper.findByTestId('source-branch').text()).toBe('root/gitlab:source-branch');
+ });
+ });
+});
diff --git a/spec/frontend/milestones/components/milestone_combobox_spec.js b/spec/frontend/milestones/components/milestone_combobox_spec.js
index 53abf6dc544..3e2cd354d53 100644
--- a/spec/frontend/milestones/components/milestone_combobox_spec.js
+++ b/spec/frontend/milestones/components/milestone_combobox_spec.js
@@ -1,12 +1,11 @@
-import { GlLoadingIcon, GlSearchBoxByType, GlDropdownItem } from '@gitlab/ui';
+import { GlLoadingIcon, GlCollapsibleListbox, GlListboxItem } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
-import Vue, { nextTick } from 'vue';
+import Vue from 'vue';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
-import { ENTER_KEY } from '~/lib/utils/keys';
import MilestoneCombobox from '~/milestones/components/milestone_combobox.vue';
import createStore from '~/milestones/stores/';
import { projectMilestones, groupMilestones } from '../mock_data';
@@ -52,9 +51,6 @@ describe('Milestone combobox component', () => {
wrapper.setProps({ value: selectedMilestone });
},
},
- stubs: {
- GlSearchBoxByType: true,
- },
store: createStore(),
});
};
@@ -89,57 +85,25 @@ describe('Milestone combobox component', () => {
//
// Finders
//
- const findButtonContent = () => wrapper.find('[data-testid="milestone-combobox-button-content"]');
-
- const findNoResults = () => wrapper.find('[data-testid="milestone-combobox-no-results"]');
-
+ const findGlCollapsibleListbox = () => wrapper.findComponent(GlCollapsibleListbox);
+ const findButtonContent = () => wrapper.find('[data-testid="base-dropdown-toggle"]');
+ const findNoResults = () => wrapper.find('[data-testid="listbox-no-results-text"]');
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
-
- const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
-
const findProjectMilestonesSection = () =>
- wrapper.find('[data-testid="project-milestones-section"]');
- const findProjectMilestonesDropdownItems = () =>
- findProjectMilestonesSection().findAllComponents(GlDropdownItem);
- const findFirstProjectMilestonesDropdownItem = () => findProjectMilestonesDropdownItems().at(0);
-
- const findGroupMilestonesSection = () => wrapper.find('[data-testid="group-milestones-section"]');
- const findGroupMilestonesDropdownItems = () =>
- findGroupMilestonesSection().findAllComponents(GlDropdownItem);
- const findFirstGroupMilestonesDropdownItem = () => findGroupMilestonesDropdownItems().at(0);
-
- //
- // Expecters
- //
- const projectMilestoneSectionContainsErrorMessage = () => {
- const projectMilestoneSection = findProjectMilestonesSection();
-
- return projectMilestoneSection
- .text()
- .includes('An error occurred while searching for milestones');
- };
-
- const groupMilestoneSectionContainsErrorMessage = () => {
- const groupMilestoneSection = findGroupMilestonesSection();
-
- return groupMilestoneSection
- .text()
- .includes('An error occurred while searching for milestones');
- };
+ findGlCollapsibleListbox().find('[data-testid="project-milestones-section"]');
+ const findGroupMilestonesSection = () =>
+ findGlCollapsibleListbox().find('[data-testid="group-milestones-section"]');
+ const findDropdownItems = () => findGlCollapsibleListbox().findAllComponents(GlListboxItem);
//
// Convenience methods
//
const updateQuery = (newQuery) => {
- findSearchBox().vm.$emit('input', newQuery);
+ findGlCollapsibleListbox().vm.$emit('search', newQuery);
};
- const selectFirstProjectMilestone = () => {
- findFirstProjectMilestonesDropdownItem().vm.$emit('click');
- };
-
- const selectFirstGroupMilestone = () => {
- findFirstGroupMilestonesDropdownItem().vm.$emit('click');
+ const selectItem = (item) => {
+ findGlCollapsibleListbox().vm.$emit('select', item);
};
const waitForRequests = async ({ andClearMocks } = { andClearMocks: false }) => {
@@ -224,22 +188,6 @@ describe('Milestone combobox component', () => {
});
});
- describe('when the Enter is pressed', () => {
- beforeEach(() => {
- createComponent();
-
- return waitForRequests({ andClearMocks: true });
- });
-
- it('requeries the search when Enter is pressed', () => {
- findSearchBox().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY }));
-
- return waitForRequests().then(() => {
- expect(searchApiCallSpy).toHaveBeenCalledTimes(1);
- });
- });
- });
-
describe('when no results are found', () => {
beforeEach(() => {
projectMilestonesApiCallSpy = jest
@@ -257,7 +205,7 @@ describe('Milestone combobox component', () => {
describe('when the search query is empty', () => {
it('renders a "no results" message', () => {
- expect(findNoResults().text()).toBe('No matching results');
+ expect(findNoResults().text()).toBe('No results found');
});
});
});
@@ -275,22 +223,14 @@ describe('Milestone combobox component', () => {
});
it('renders the "Project milestones" heading with a total number indicator', () => {
- expect(
- findProjectMilestonesSection()
- .find('[data-testid="milestone-results-section-header"]')
- .text(),
- ).toBe('Project milestones 6');
- });
-
- it("does not render an error message in the project milestone section's body", () => {
- expect(projectMilestoneSectionContainsErrorMessage()).toBe(false);
+ expect(findProjectMilestonesSection().text()).toBe('Project milestones 6');
});
it('renders each project milestones as a selectable item', () => {
- const dropdownItems = findProjectMilestonesDropdownItems();
+ const dropdownItems = findDropdownItems();
- projectMilestones.forEach((milestone, i) => {
- expect(dropdownItems.at(i).text()).toBe(milestone.title);
+ projectMilestones.forEach((milestone) => {
+ expect(dropdownItems.filter((x) => x.text() === milestone.title).exists()).toBe(true);
});
});
});
@@ -323,12 +263,8 @@ describe('Milestone combobox component', () => {
return waitForRequests();
});
- it('renders the project milestones section in the dropdown', () => {
- expect(findProjectMilestonesSection().exists()).toBe(true);
- });
-
- it("renders an error message in the project milestones section's body", () => {
- expect(projectMilestoneSectionContainsErrorMessage()).toBe(true);
+ it('does not render the project milestones section in the dropdown', () => {
+ expect(findProjectMilestonesSection().exists()).toBe(false);
});
});
@@ -339,52 +275,24 @@ describe('Milestone combobox component', () => {
return waitForRequests();
});
- it('renders a checkmark by the selected item', async () => {
- selectFirstProjectMilestone();
-
- await nextTick();
-
- expect(
- findFirstProjectMilestonesDropdownItem()
- .find('svg')
- .classes('gl-dropdown-item-check-icon'),
- ).toBe(true);
-
- selectFirstProjectMilestone();
-
- await nextTick();
-
- expect(
- findFirstProjectMilestonesDropdownItem().find('svg').classes('gl-visibility-hidden'),
- ).toBe(true);
- });
+ describe('when a project milestone is selected', () => {
+ const item = 'v1.0';
- describe('when a project milestones is selected', () => {
beforeEach(() => {
createComponent();
projectMilestonesApiCallSpy = jest
.fn()
.mockReturnValue([HTTP_STATUS_OK, [{ title: 'v1.0' }], { [X_TOTAL_HEADER]: '1' }]);
+ selectItem([item]);
return waitForRequests();
});
- it("displays the project milestones name in the dropdown's button", async () => {
- selectFirstProjectMilestone();
- await nextTick();
-
- expect(findButtonContent().text()).toBe('v1.0');
-
- selectFirstProjectMilestone();
- await nextTick();
-
- expect(findButtonContent().text()).toBe('No milestone');
+ it("displays the project milestones name in the dropdown's button", () => {
+ expect(findButtonContent().text()).toBe(item);
});
- it('updates the v-model binding with the project milestone title', async () => {
- selectFirstProjectMilestone();
- await nextTick();
-
+ it('updates the v-model binding with the project milestone title', () => {
expect(wrapper.emitted().input[0][0]).toStrictEqual(['v1.0']);
});
});
@@ -404,22 +312,14 @@ describe('Milestone combobox component', () => {
});
it('renders the "Group milestones" heading with a total number indicator', () => {
- expect(
- findGroupMilestonesSection()
- .find('[data-testid="milestone-results-section-header"]')
- .text(),
- ).toBe('Group milestones 6');
- });
-
- it("does not render an error message in the group milestone section's body", () => {
- expect(groupMilestoneSectionContainsErrorMessage()).toBe(false);
+ expect(findGroupMilestonesSection().text()).toBe('Group milestones 6');
});
it('renders each group milestones as a selectable item', () => {
- const dropdownItems = findGroupMilestonesDropdownItems();
+ const dropdownItems = findDropdownItems();
- groupMilestones.forEach((milestone, i) => {
- expect(dropdownItems.at(i).text()).toBe(milestone.title);
+ groupMilestones.forEach((milestone) => {
+ expect(dropdownItems.filter((x) => x.text() === milestone.title).exists()).toBe(true);
});
});
});
@@ -452,74 +352,8 @@ describe('Milestone combobox component', () => {
return waitForRequests();
});
- it('renders the group milestones section in the dropdown', () => {
- expect(findGroupMilestonesSection().exists()).toBe(true);
- });
-
- it("renders an error message in the group milestones section's body", () => {
- expect(groupMilestoneSectionContainsErrorMessage()).toBe(true);
- });
- });
-
- describe('selection', () => {
- beforeEach(() => {
- createComponent();
-
- return waitForRequests();
- });
-
- it('renders a checkmark by the selected item', async () => {
- selectFirstGroupMilestone();
-
- await nextTick();
-
- expect(
- findFirstGroupMilestonesDropdownItem()
- .find('svg')
- .classes('gl-dropdown-item-check-icon'),
- ).toBe(true);
-
- selectFirstGroupMilestone();
-
- await nextTick();
-
- expect(
- findFirstGroupMilestonesDropdownItem().find('svg').classes('gl-visibility-hidden'),
- ).toBe(true);
- });
-
- describe('when a group milestones is selected', () => {
- beforeEach(() => {
- createComponent();
- groupMilestonesApiCallSpy = jest
- .fn()
- .mockReturnValue([
- HTTP_STATUS_OK,
- [{ title: 'group-v1.0' }],
- { [X_TOTAL_HEADER]: '1' },
- ]);
-
- return waitForRequests();
- });
-
- it("displays the group milestones name in the dropdown's button", async () => {
- selectFirstGroupMilestone();
- await nextTick();
-
- expect(findButtonContent().text()).toBe('group-v1.0');
-
- selectFirstGroupMilestone();
- await nextTick();
-
- expect(findButtonContent().text()).toBe('No milestone');
- });
-
- it('updates the v-model binding with the group milestone title', async () => {
- selectFirstGroupMilestone();
- await nextTick();
-
- expect(wrapper.emitted().input[0][0]).toStrictEqual(['group-v1.0']);
- });
+ it('does not render the group milestones section', () => {
+ expect(findGroupMilestonesSection().exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/milestones/stores/mutations_spec.js b/spec/frontend/milestones/stores/mutations_spec.js
index a53d6ca5de1..e3a5e4d00f4 100644
--- a/spec/frontend/milestones/stores/mutations_spec.js
+++ b/spec/frontend/milestones/stores/mutations_spec.js
@@ -163,10 +163,12 @@ describe('Milestones combobox Vuex store mutations', () => {
expect(state.matches.projectMilestones).toEqual({
list: [
{
- title: 'v0.1',
+ text: 'v0.1',
+ value: 'v0.1',
},
{
- title: 'v0.2',
+ text: 'v0.2',
+ value: 'v0.2',
},
],
error: null,
@@ -192,10 +194,12 @@ describe('Milestones combobox Vuex store mutations', () => {
expect(state.matches.projectMilestones).toEqual({
list: [
{
- title: 'v0.1',
+ text: 'v0.1',
+ value: 'v0.1',
},
{
- title: 'v0.2',
+ text: 'v0.2',
+ value: 'v0.2',
},
],
error: null,
@@ -245,10 +249,12 @@ describe('Milestones combobox Vuex store mutations', () => {
expect(state.matches.groupMilestones).toEqual({
list: [
{
- title: 'group-0.1',
+ text: 'group-0.1',
+ value: 'group-0.1',
},
{
- title: 'group-0.2',
+ text: 'group-0.2',
+ value: 'group-0.2',
},
],
error: null,
@@ -274,10 +280,12 @@ describe('Milestones combobox Vuex store mutations', () => {
expect(state.matches.groupMilestones).toEqual({
list: [
{
- title: 'group-0.1',
+ text: 'group-0.1',
+ value: 'group-0.1',
},
{
- title: 'group-0.2',
+ text: 'group-0.2',
+ value: 'group-0.2',
},
],
error: null,
diff --git a/spec/frontend/ml/model_registry/apps/index_ml_models_spec.js b/spec/frontend/ml/model_registry/apps/index_ml_models_spec.js
new file mode 100644
index 00000000000..6e0ab2ebe2d
--- /dev/null
+++ b/spec/frontend/ml/model_registry/apps/index_ml_models_spec.js
@@ -0,0 +1,83 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { IndexMlModels } from '~/ml/model_registry/apps';
+import ModelRow from '~/ml/model_registry/components/model_row.vue';
+import { TITLE_LABEL, NO_MODELS_LABEL } from '~/ml/model_registry/translations';
+import Pagination from '~/vue_shared/components/incubation/pagination.vue';
+import SearchBar from '~/ml/model_registry/components/search_bar.vue';
+import { BASE_SORT_FIELDS } from '~/ml/model_registry/constants';
+import TitleArea from '~/vue_shared/components/registry/title_area.vue';
+import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
+import { mockModels, startCursor, defaultPageInfo } from '../mock_data';
+
+let wrapper;
+const createWrapper = (
+ propsData = { models: mockModels, pageInfo: defaultPageInfo, modelCount: 2 },
+) => {
+ wrapper = shallowMountExtended(IndexMlModels, { propsData });
+};
+
+const findModelRow = (index) => wrapper.findAllComponents(ModelRow).at(index);
+const findPagination = () => wrapper.findComponent(Pagination);
+const findEmptyLabel = () => wrapper.findByText(NO_MODELS_LABEL);
+const findSearchBar = () => wrapper.findComponent(SearchBar);
+const findTitleArea = () => wrapper.findComponent(TitleArea);
+const findModelCountMetadataItem = () => findTitleArea().findComponent(MetadataItem);
+
+describe('MlModelsIndex', () => {
+ describe('empty state', () => {
+ beforeEach(() => createWrapper({ models: [], pageInfo: defaultPageInfo }));
+
+ it('displays empty state when no experiment', () => {
+ expect(findEmptyLabel().exists()).toBe(true);
+ });
+
+ it('does not show pagination', () => {
+ expect(findPagination().exists()).toBe(false);
+ });
+
+ it('does not show search bar', () => {
+ expect(findSearchBar().exists()).toBe(false);
+ });
+ });
+
+ describe('with data', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ it('does not show empty state', () => {
+ expect(findEmptyLabel().exists()).toBe(false);
+ });
+
+ describe('header', () => {
+ it('displays the title', () => {
+ expect(findTitleArea().props('title')).toBe(TITLE_LABEL);
+ });
+
+ it('sets model metadata item to model count', () => {
+ expect(findModelCountMetadataItem().props('text')).toBe(`2 models`);
+ });
+ });
+
+ it('adds a search bar', () => {
+ expect(findSearchBar().props()).toMatchObject({ sortableFields: BASE_SORT_FIELDS });
+ });
+
+ describe('model list', () => {
+ it('displays the models', () => {
+ expect(findModelRow(0).props('model')).toMatchObject(mockModels[0]);
+ expect(findModelRow(1).props('model')).toMatchObject(mockModels[1]);
+ });
+ });
+
+ describe('pagination', () => {
+ it('should show', () => {
+ expect(findPagination().exists()).toBe(true);
+ });
+
+ it('passes pagination to pagination component', () => {
+ expect(findPagination().props('startCursor')).toBe(startCursor);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ml/model_registry/apps/show_ml_model_spec.js b/spec/frontend/ml/model_registry/apps/show_ml_model_spec.js
index 57a5a5f003f..bc4770976a9 100644
--- a/spec/frontend/ml/model_registry/apps/show_ml_model_spec.js
+++ b/spec/frontend/ml/model_registry/apps/show_ml_model_spec.js
@@ -1,15 +1,78 @@
+import { GlBadge, GlTab } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { ShowMlModel } from '~/ml/model_registry/apps';
-import { MODEL } from '../mock_data';
+import TitleArea from '~/vue_shared/components/registry/title_area.vue';
+import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
+import { NO_VERSIONS_LABEL } from '~/ml/model_registry/translations';
+import { MODEL, makeModel } from '../mock_data';
let wrapper;
-const createWrapper = () => {
- wrapper = shallowMount(ShowMlModel, { propsData: { model: MODEL } });
+const createWrapper = (model = MODEL) => {
+ wrapper = shallowMount(ShowMlModel, { propsData: { model } });
};
+const findDetailTab = () => wrapper.findAllComponents(GlTab).at(0);
+const findVersionsTab = () => wrapper.findAllComponents(GlTab).at(1);
+const findVersionsCountBadge = () => findVersionsTab().findComponent(GlBadge);
+const findCandidateTab = () => wrapper.findAllComponents(GlTab).at(2);
+const findCandidatesCountBadge = () => findCandidateTab().findComponent(GlBadge);
+const findTitleArea = () => wrapper.findComponent(TitleArea);
+const findVersionCountMetadataItem = () => findTitleArea().findComponent(MetadataItem);
+
describe('ShowMlModel', () => {
- beforeEach(() => createWrapper());
- it('renders the app', () => {
- expect(wrapper.text()).toContain(MODEL.name);
+ describe('Title', () => {
+ beforeEach(() => createWrapper());
+
+ it('title is set to model name', () => {
+ expect(findTitleArea().props('title')).toBe(MODEL.name);
+ });
+
+ it('subheader is set to description', () => {
+ expect(findTitleArea().text()).toContain(MODEL.description);
+ });
+
+ it('sets version metadata item to version count', () => {
+ expect(findVersionCountMetadataItem().props('text')).toBe(`${MODEL.versionCount} versions`);
+ });
+ });
+
+ describe('Details', () => {
+ beforeEach(() => createWrapper());
+
+ it('has a details tab', () => {
+ expect(findDetailTab().attributes('title')).toBe('Details');
+ });
+
+ describe('when it has latest version', () => {
+ it('displays the version', () => {
+ expect(findDetailTab().text()).toContain(MODEL.latestVersion.version);
+ });
+ });
+
+ describe('when it does not have latest version', () => {
+ beforeEach(() => {
+ createWrapper(makeModel({ latestVersion: null }));
+ });
+
+ it('shows no version message', () => {
+ expect(findDetailTab().text()).toContain(NO_VERSIONS_LABEL);
+ });
+ });
+ });
+
+ describe('Versions tab', () => {
+ beforeEach(() => createWrapper());
+
+ it('shows the number of versions in the tab', () => {
+ expect(findVersionsCountBadge().text()).toBe(MODEL.versionCount.toString());
+ });
+ });
+
+ describe('Candidates tab', () => {
+ beforeEach(() => createWrapper());
+
+ it('shows the number of candidates in the tab', () => {
+ expect(findCandidatesCountBadge().text()).toBe(MODEL.candidateCount.toString());
+ });
});
});
diff --git a/spec/frontend/ml/model_registry/apps/show_ml_model_version_spec.js b/spec/frontend/ml/model_registry/apps/show_ml_model_version_spec.js
new file mode 100644
index 00000000000..77fca53c00e
--- /dev/null
+++ b/spec/frontend/ml/model_registry/apps/show_ml_model_version_spec.js
@@ -0,0 +1,15 @@
+import { shallowMount } from '@vue/test-utils';
+import { ShowMlModelVersion } from '~/ml/model_registry/apps';
+import { MODEL_VERSION } from '../mock_data';
+
+let wrapper;
+const createWrapper = () => {
+ wrapper = shallowMount(ShowMlModelVersion, { propsData: { modelVersion: MODEL_VERSION } });
+};
+
+describe('ShowMlModelVersion', () => {
+ beforeEach(() => createWrapper());
+ it('renders the app', () => {
+ expect(wrapper.text()).toContain(`${MODEL_VERSION.model.name} - ${MODEL_VERSION.version}`);
+ });
+});
diff --git a/spec/frontend/ml/model_registry/components/model_row_spec.js b/spec/frontend/ml/model_registry/components/model_row_spec.js
new file mode 100644
index 00000000000..9d16ce5c466
--- /dev/null
+++ b/spec/frontend/ml/model_registry/components/model_row_spec.js
@@ -0,0 +1,45 @@
+import { GlLink } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ModelRow from '~/ml/model_registry/components/model_row.vue';
+import { mockModels, modelWithoutVersion } from '../mock_data';
+
+let wrapper;
+const createWrapper = (model = mockModels[0]) => {
+ wrapper = shallowMountExtended(ModelRow, { propsData: { model } });
+};
+
+const findTitleLink = () => wrapper.findAllComponents(GlLink).at(0);
+const findVersionLink = () => wrapper.findAllComponents(GlLink).at(1);
+const findMessage = (message) => wrapper.findByText(message);
+
+describe('ModelRow', () => {
+ it('Has a link to the model', () => {
+ createWrapper();
+
+ expect(findTitleLink().text()).toBe(mockModels[0].name);
+ expect(findTitleLink().attributes('href')).toBe(mockModels[0].path);
+ });
+
+ it('Shows the latest version and the version count', () => {
+ createWrapper();
+
+ expect(findVersionLink().text()).toBe(mockModels[0].version);
+ expect(findVersionLink().attributes('href')).toBe(mockModels[0].versionPath);
+ expect(findMessage('· 3 versions').exists()).toBe(true);
+ });
+
+ it('Shows the latest version and no version count if it has only 1 version', () => {
+ createWrapper(mockModels[1]);
+
+ expect(findVersionLink().text()).toBe(mockModels[1].version);
+ expect(findVersionLink().attributes('href')).toBe(mockModels[1].versionPath);
+
+ expect(findMessage('· No other versions').exists()).toBe(true);
+ });
+
+ it('Shows no version message if model has no versions', () => {
+ createWrapper(modelWithoutVersion);
+
+ expect(findMessage('No registered versions').exists()).toBe(true);
+ });
+});
diff --git a/spec/frontend/ml/model_registry/components/search_bar_spec.js b/spec/frontend/ml/model_registry/components/search_bar_spec.js
new file mode 100644
index 00000000000..f9e18486434
--- /dev/null
+++ b/spec/frontend/ml/model_registry/components/search_bar_spec.js
@@ -0,0 +1,86 @@
+import { shallowMount } from '@vue/test-utils';
+import setWindowLocation from 'helpers/set_window_location_helper';
+import * as urlHelpers from '~/lib/utils/url_utility';
+import SearchBar from '~/ml/model_registry/components/search_bar.vue';
+import { BASE_SORT_FIELDS } from '~/ml/model_registry/constants';
+import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
+
+let wrapper;
+
+const makeUrl = ({ filter = 'query', orderBy = 'name', sort = 'asc' } = {}) =>
+ `https://blah.com/?name=${filter}&orderBy=${orderBy}&sort=${sort}`;
+
+const createWrapper = () => {
+ wrapper = shallowMount(SearchBar, { propsData: { sortableFields: BASE_SORT_FIELDS } });
+};
+
+const findRegistrySearch = () => wrapper.findComponent(RegistrySearch);
+
+describe('SearchBar', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ it('passes default filter and sort by to registry search', () => {
+ expect(findRegistrySearch().props()).toMatchObject({
+ filters: [],
+ sorting: {
+ orderBy: 'created_at',
+ sort: 'desc',
+ },
+ sortableFields: BASE_SORT_FIELDS,
+ });
+ });
+
+ it('sets the component filters based on the querystring', () => {
+ const filter = 'A';
+ setWindowLocation(makeUrl({ filter }));
+
+ createWrapper();
+
+ expect(findRegistrySearch().props('filters')).toMatchObject([{ value: { data: filter } }]);
+ });
+
+ it('sets the registry search sort based on the querystring', () => {
+ const orderBy = 'B';
+ const sort = 'C';
+
+ setWindowLocation(makeUrl({ orderBy, sort }));
+
+ createWrapper();
+
+ expect(findRegistrySearch().props('sorting')).toMatchObject({ orderBy, sort: 'c' });
+ });
+
+ describe('Search submit', () => {
+ beforeEach(() => {
+ setWindowLocation(makeUrl());
+ jest.spyOn(urlHelpers, 'visitUrl').mockImplementation(() => {});
+
+ createWrapper();
+ });
+
+ it('On submit, resets the cursor and reloads to correct page', () => {
+ findRegistrySearch().vm.$emit('filter:submit');
+
+ expect(urlHelpers.visitUrl).toHaveBeenCalledTimes(1);
+ expect(urlHelpers.visitUrl).toHaveBeenCalledWith(makeUrl());
+ });
+
+ it('On sorting changed, resets cursor and reloads to correct page', () => {
+ const orderBy = 'created_at';
+ findRegistrySearch().vm.$emit('sorting:changed', { orderBy });
+
+ expect(urlHelpers.visitUrl).toHaveBeenCalledTimes(1);
+ expect(urlHelpers.visitUrl).toHaveBeenCalledWith(makeUrl({ orderBy }));
+ });
+
+ it('On direction changed, reloads to correct page', () => {
+ const sort = 'asc';
+ findRegistrySearch().vm.$emit('sorting:changed', { sort });
+
+ expect(urlHelpers.visitUrl).toHaveBeenCalledTimes(1);
+ expect(urlHelpers.visitUrl).toHaveBeenCalledWith(makeUrl({ sort }));
+ });
+ });
+});
diff --git a/spec/frontend/ml/model_registry/mock_data.js b/spec/frontend/ml/model_registry/mock_data.js
index 18b2b32e069..a820c323103 100644
--- a/spec/frontend/ml/model_registry/mock_data.js
+++ b/spec/frontend/ml/model_registry/mock_data.js
@@ -1 +1,48 @@
-export const MODEL = { name: 'blah' };
+const LATEST_VERSION = {
+ version: '1.2.3',
+};
+
+export const makeModel = ({ latestVersion } = { latestVersion: LATEST_VERSION }) => ({
+ id: 1234,
+ name: 'blah',
+ path: 'path/to/blah',
+ description: 'Description of the model',
+ latestVersion,
+ versionCount: 2,
+ candidateCount: 1,
+});
+
+export const MODEL = makeModel();
+
+export const MODEL_VERSION = { version: '1.2.3', model: MODEL };
+
+export const mockModels = [
+ {
+ name: 'model_1',
+ version: '1.0',
+ versionPath: 'path/to/version',
+ path: 'path/to/model_1',
+ versionCount: 3,
+ },
+ {
+ name: 'model_2',
+ version: '1.1',
+ path: 'path/to/model_2',
+ versionCount: 1,
+ },
+];
+
+export const modelWithoutVersion = {
+ name: 'model_without_version',
+ path: 'path/to/model_without_version',
+ versionCount: 0,
+};
+
+export const startCursor = 'eyJpZCI6IjE2In0';
+
+export const defaultPageInfo = Object.freeze({
+ startCursor,
+ endCursor: 'eyJpZCI6IjIifQ',
+ hasNextPage: true,
+ hasPreviousPage: true,
+});
diff --git a/spec/frontend/ml/model_registry/routes/models/index/components/ml_models_index_spec.js b/spec/frontend/ml/model_registry/routes/models/index/components/ml_models_index_spec.js
deleted file mode 100644
index c1b9aef9634..00000000000
--- a/spec/frontend/ml/model_registry/routes/models/index/components/ml_models_index_spec.js
+++ /dev/null
@@ -1,63 +0,0 @@
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import MlModelsIndexApp from '~/ml/model_registry/routes/models/index';
-import ModelRow from '~/ml/model_registry/routes/models/index/components/model_row.vue';
-import { TITLE_LABEL, NO_MODELS_LABEL } from '~/ml/model_registry/routes/models/index/translations';
-import Pagination from '~/vue_shared/components/incubation/pagination.vue';
-import { mockModels, startCursor, defaultPageInfo } from './mock_data';
-
-let wrapper;
-const createWrapper = (propsData = { models: mockModels, pageInfo: defaultPageInfo }) => {
- wrapper = shallowMountExtended(MlModelsIndexApp, { propsData });
-};
-
-const findModelRow = (index) => wrapper.findAllComponents(ModelRow).at(index);
-const findPagination = () => wrapper.findComponent(Pagination);
-const findTitle = () => wrapper.findByText(TITLE_LABEL);
-const findEmptyLabel = () => wrapper.findByText(NO_MODELS_LABEL);
-
-describe('MlModelsIndex', () => {
- describe('empty state', () => {
- beforeEach(() => createWrapper({ models: [], pageInfo: defaultPageInfo }));
-
- it('displays empty state when no experiment', () => {
- expect(findEmptyLabel().exists()).toBe(true);
- });
-
- it('does not show pagination', () => {
- expect(findPagination().exists()).toBe(false);
- });
- });
-
- describe('with data', () => {
- beforeEach(() => {
- createWrapper();
- });
-
- it('does not show empty state', () => {
- expect(findEmptyLabel().exists()).toBe(false);
- });
-
- describe('header', () => {
- it('displays the title', () => {
- expect(findTitle().exists()).toBe(true);
- });
- });
-
- describe('model list', () => {
- it('displays the models', () => {
- expect(findModelRow(0).props('model')).toMatchObject(mockModels[0]);
- expect(findModelRow(1).props('model')).toMatchObject(mockModels[1]);
- });
- });
-
- describe('pagination', () => {
- it('should show', () => {
- expect(findPagination().exists()).toBe(true);
- });
-
- it('passes pagination to pagination component', () => {
- expect(findPagination().props('startCursor')).toBe(startCursor);
- });
- });
- });
-});
diff --git a/spec/frontend/ml/model_registry/routes/models/index/components/model_row_spec.js b/spec/frontend/ml/model_registry/routes/models/index/components/model_row_spec.js
deleted file mode 100644
index 7600288f560..00000000000
--- a/spec/frontend/ml/model_registry/routes/models/index/components/model_row_spec.js
+++ /dev/null
@@ -1,42 +0,0 @@
-import { GlLink } from '@gitlab/ui';
-import {
- mockModels,
- modelWithoutVersion,
-} from 'jest/ml/model_registry/routes/models/index/components/mock_data';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import ModelRow from '~/ml/model_registry/routes/models/index/components/model_row.vue';
-
-let wrapper;
-const createWrapper = (model = mockModels[0]) => {
- wrapper = shallowMountExtended(ModelRow, { propsData: { model } });
-};
-
-const findLink = () => wrapper.findComponent(GlLink);
-const findMessage = (message) => wrapper.findByText(message);
-
-describe('ModelRow', () => {
- beforeEach(() => {
- createWrapper();
- });
-
- it('Has a link to the model', () => {
- expect(findLink().text()).toBe(mockModels[0].name);
- expect(findLink().attributes('href')).toBe(mockModels[0].path);
- });
-
- it('Shows the latest version and the version count', () => {
- expect(findMessage('1.0 · 3 versions').exists()).toBe(true);
- });
-
- it('Shows the latest version and no version count if it has only 1 version', () => {
- createWrapper(mockModels[1]);
-
- expect(findMessage('1.1 · No other versions').exists()).toBe(true);
- });
-
- it('Shows no version message if model has no versions', () => {
- createWrapper(modelWithoutVersion);
-
- expect(findMessage('No registered versions').exists()).toBe(true);
- });
-});
diff --git a/spec/frontend/notes/components/comment_field_layout_spec.js b/spec/frontend/notes/components/comment_field_layout_spec.js
index 93b54f95021..b55019ed525 100644
--- a/spec/frontend/notes/components/comment_field_layout_spec.js
+++ b/spec/frontend/notes/components/comment_field_layout_spec.js
@@ -31,19 +31,13 @@ describe('Comment Field Layout Component', () => {
const findAttachmentsWarning = () => wrapper.findComponent(AttachmentsWarning);
const findErrorAlert = () => wrapper.findByTestId('comment-field-alert-container');
- const createWrapper = (props = {}, provide = {}) => {
+ const createWrapper = (props = {}) => {
wrapper = extendedWrapper(
shallowMount(CommentFieldLayout, {
propsData: {
noteableData: noteableDataMock,
...props,
},
- provide: {
- glFeatures: {
- serviceDeskNewNoteEmailNativeAttachments: true,
- },
- ...provide,
- },
}),
);
};
@@ -160,22 +154,4 @@ describe('Comment Field Layout Component', () => {
expect(findEmailParticipantsWarning().exists()).toBe(false);
});
});
-
- describe('serviceDeskNewNoteEmailNativeAttachments flag', () => {
- it('shows warning message when flag is enabled', () => {
- createWrapper(commentFieldWithAttachmentData, {
- glFeatures: { serviceDeskNewNoteEmailNativeAttachments: true },
- });
-
- expect(findAttachmentsWarning().exists()).toBe(true);
- });
-
- it('shows warning message when flag is disables', () => {
- createWrapper(commentFieldWithAttachmentData, {
- glFeatures: { serviceDeskNewNoteEmailNativeAttachments: false },
- });
-
- expect(findAttachmentsWarning().exists()).toBe(false);
- });
- });
});
diff --git a/spec/frontend/observability/client_spec.js b/spec/frontend/observability/client_spec.js
index 68a53131539..b41b303f57d 100644
--- a/spec/frontend/observability/client_spec.js
+++ b/spec/frontend/observability/client_spec.js
@@ -1,10 +1,13 @@
import MockAdapter from 'axios-mock-adapter';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { buildClient } from '~/observability/client';
import axios from '~/lib/utils/axios_utils';
+import { logError } from '~/lib/logger';
+import { DEFAULT_SORTING_OPTION, SORTING_OPTIONS } from '~/observability/constants';
jest.mock('~/lib/utils/axios_utils');
-jest.mock('@sentry/browser');
+jest.mock('~/sentry/sentry_browser_wrapper');
+jest.mock('~/lib/logger');
describe('buildClient', () => {
let client;
@@ -14,52 +17,58 @@ describe('buildClient', () => {
const provisioningUrl = 'https://example.com/provisioning';
const servicesUrl = 'https://example.com/services';
const operationsUrl = 'https://example.com/services/$SERVICE_NAME$/operations';
+ const metricsUrl = 'https://example.com/metrics';
const FETCHING_TRACES_ERROR = 'traces are missing/invalid in the response';
+ const apiConfig = {
+ tracingUrl,
+ provisioningUrl,
+ servicesUrl,
+ operationsUrl,
+ metricsUrl,
+ };
+
+ const getQueryParam = () => decodeURIComponent(axios.get.mock.calls[0][1].params.toString());
+
beforeEach(() => {
axiosMock = new MockAdapter(axios);
jest.spyOn(axios, 'get');
- client = buildClient({
- tracingUrl,
- provisioningUrl,
- servicesUrl,
- operationsUrl,
- });
+ client = buildClient(apiConfig);
});
afterEach(() => {
axiosMock.restore();
});
+ const expectErrorToBeReported = (e) => {
+ expect(Sentry.captureException).toHaveBeenCalledWith(e);
+ expect(logError).toHaveBeenCalledWith(e);
+ };
+
describe('buildClient', () => {
- it('rejects if params are missing', () => {
- const e = new Error(
- 'missing required params. provisioningUrl, tracingUrl, servicesUrl, operationsUrl are required',
- );
- expect(() =>
- buildClient({ tracingUrl: 'test', servicesUrl: 'test', operationsUrl: 'test' }),
- ).toThrow(e);
- expect(() =>
- buildClient({ provisioningUrl: 'test', servicesUrl: 'test', operationsUrl: 'test' }),
- ).toThrow(e);
- expect(() =>
- buildClient({ provisioningUrl: 'test', tracingUrl: 'test', operationsUrl: 'test' }),
- ).toThrow(e);
+ it('throws is option is missing', () => {
+ expect(() => buildClient()).toThrow(new Error('No options object provided'));
+ });
+ it.each(Object.keys(apiConfig))('throws if %s is missing', (param) => {
+ const e = new Error(`${param} param must be a string`);
+
expect(() =>
- buildClient({ provisioningUrl: 'test', tracingUrl: 'test', servicesUrl: 'test' }),
+ buildClient({
+ ...apiConfig,
+ [param]: undefined,
+ }),
).toThrow(e);
- expect(() => buildClient({})).toThrow(e);
});
});
- describe('isTracingEnabled', () => {
+ describe('isObservabilityEnabled', () => {
it('returns true if requests succeedes', async () => {
axiosMock.onGet(provisioningUrl).reply(200, {
status: 'ready',
});
- const enabled = await client.isTracingEnabled();
+ const enabled = await client.isObservabilityEnabled();
expect(enabled).toBe(true);
});
@@ -67,7 +76,7 @@ describe('buildClient', () => {
it('returns false if response is 404', async () => {
axiosMock.onGet(provisioningUrl).reply(404);
- const enabled = await client.isTracingEnabled();
+ const enabled = await client.isObservabilityEnabled();
expect(enabled).toBe(false);
});
@@ -79,7 +88,7 @@ describe('buildClient', () => {
status: 'not ready',
});
- const enabled = await client.isTracingEnabled();
+ const enabled = await client.isObservabilityEnabled();
expect(enabled).toBe(true);
});
@@ -88,20 +97,20 @@ describe('buildClient', () => {
axiosMock.onGet(provisioningUrl).reply(500);
const e = 'Request failed with status code 500';
- await expect(client.isTracingEnabled()).rejects.toThrow(e);
- expect(Sentry.captureException).toHaveBeenCalledWith(new Error(e));
+ await expect(client.isObservabilityEnabled()).rejects.toThrow(e);
+ expectErrorToBeReported(new Error(e));
});
it('throws in case of unexpected response', async () => {
axiosMock.onGet(provisioningUrl).reply(200, {});
const e = 'Failed to check provisioning';
- await expect(client.isTracingEnabled()).rejects.toThrow(e);
- expect(Sentry.captureException).toHaveBeenCalledWith(new Error(e));
+ await expect(client.isObservabilityEnabled()).rejects.toThrow(e);
+ expectErrorToBeReported(new Error(e));
});
});
- describe('enableTraces', () => {
+ describe('enableObservability', () => {
it('makes a PUT request to the provisioning URL', async () => {
let putConfig;
axiosMock.onPut(provisioningUrl).reply((config) => {
@@ -109,7 +118,7 @@ describe('buildClient', () => {
return [200];
});
- await client.enableTraces();
+ await client.enableObservability();
expect(putConfig.withCredentials).toBe(true);
});
@@ -119,52 +128,32 @@ describe('buildClient', () => {
const e = 'Request failed with status code 401';
- await expect(client.enableTraces()).rejects.toThrow(e);
- expect(Sentry.captureException).toHaveBeenCalledWith(new Error(e));
+ await expect(client.enableObservability()).rejects.toThrow(e);
+ expectErrorToBeReported(new Error(e));
});
});
describe('fetchTrace', () => {
it('fetches the trace from the tracing URL', async () => {
- const mockTraces = [
- {
- trace_id: 'trace-1',
- duration_nano: 3000,
- spans: [{ duration_nano: 1000 }, { duration_nano: 2000 }],
- },
- ];
-
- axiosMock.onGet(tracingUrl).reply(200, {
- traces: mockTraces,
- });
+ const mockTrace = {
+ trace_id: 'trace-1',
+ duration_nano: 3000,
+ spans: [{ duration_nano: 1000 }, { duration_nano: 2000 }],
+ };
+ axiosMock.onGet(`${tracingUrl}/trace-1`).reply(200, mockTrace);
const result = await client.fetchTrace('trace-1');
expect(axios.get).toHaveBeenCalledTimes(1);
- expect(axios.get).toHaveBeenCalledWith(tracingUrl, {
+ expect(axios.get).toHaveBeenCalledWith(`${tracingUrl}/trace-1`, {
withCredentials: true,
- params: { trace_id: 'trace-1' },
});
- expect(result).toEqual(mockTraces[0]);
+ expect(result).toEqual(mockTrace);
});
it('rejects if trace id is missing', () => {
return expect(client.fetchTrace()).rejects.toThrow('traceId is required.');
});
-
- it('rejects if traces are empty', async () => {
- axiosMock.onGet(tracingUrl).reply(200, { traces: [] });
-
- await expect(client.fetchTrace('trace-1')).rejects.toThrow(FETCHING_TRACES_ERROR);
- expect(Sentry.captureException).toHaveBeenCalledWith(new Error(FETCHING_TRACES_ERROR));
- });
-
- it('rejects if traces are invalid', async () => {
- axiosMock.onGet(tracingUrl).reply(200, { traces: 'invalid' });
-
- await expect(client.fetchTraces()).rejects.toThrow(FETCHING_TRACES_ERROR);
- expect(Sentry.captureException).toHaveBeenCalledWith(new Error(FETCHING_TRACES_ERROR));
- });
});
describe('fetchTraces', () => {
@@ -187,7 +176,7 @@ describe('buildClient', () => {
expect(axios.get).toHaveBeenCalledTimes(1);
expect(axios.get).toHaveBeenCalledWith(tracingUrl, {
withCredentials: true,
- params: new URLSearchParams(),
+ params: expect.any(URLSearchParams),
});
expect(result).toEqual(mockResponse);
});
@@ -196,41 +185,64 @@ describe('buildClient', () => {
axiosMock.onGet(tracingUrl).reply(200, {});
await expect(client.fetchTraces()).rejects.toThrow(FETCHING_TRACES_ERROR);
- expect(Sentry.captureException).toHaveBeenCalledWith(new Error(FETCHING_TRACES_ERROR));
+ expectErrorToBeReported(new Error(FETCHING_TRACES_ERROR));
});
it('rejects if traces are invalid', async () => {
axiosMock.onGet(tracingUrl).reply(200, { traces: 'invalid' });
await expect(client.fetchTraces()).rejects.toThrow(FETCHING_TRACES_ERROR);
- expect(Sentry.captureException).toHaveBeenCalledWith(new Error(FETCHING_TRACES_ERROR));
+ expectErrorToBeReported(new Error(FETCHING_TRACES_ERROR));
});
- describe('query filter', () => {
+ describe('sort order', () => {
beforeEach(() => {
axiosMock.onGet(tracingUrl).reply(200, {
traces: [],
});
});
+ it('appends sort param if specified', async () => {
+ await client.fetchTraces({ sortBy: SORTING_OPTIONS.DURATION_DESC });
+
+ expect(getQueryParam()).toBe(`sort=${SORTING_OPTIONS.DURATION_DESC}`);
+ });
+
+ it('defaults to DEFAULT_SORTING_OPTION if no sortBy param is specified', async () => {
+ await client.fetchTraces();
+
+ expect(getQueryParam()).toBe(`sort=${DEFAULT_SORTING_OPTION}`);
+ });
- const getQueryParam = () => decodeURIComponent(axios.get.mock.calls[0][1].params.toString());
+ it('defaults to timestamp_desc if sortBy param is not an accepted value', async () => {
+ await client.fetchTraces({ sortBy: 'foo-bar' });
+
+ expect(getQueryParam()).toBe(`sort=${SORTING_OPTIONS.TIMESTAMP_DESC}`);
+ });
+ });
+
+ describe('query filter', () => {
+ beforeEach(() => {
+ axiosMock.onGet(tracingUrl).reply(200, {
+ traces: [],
+ });
+ });
it('does not set any query param without filters', async () => {
await client.fetchTraces();
- expect(getQueryParam()).toBe('');
+ expect(getQueryParam()).toBe(`sort=${SORTING_OPTIONS.TIMESTAMP_DESC}`);
});
it('appends page_token if specified', async () => {
await client.fetchTraces({ pageToken: 'page-token' });
- expect(getQueryParam()).toBe('page_token=page-token');
+ expect(getQueryParam()).toContain('page_token=page-token');
});
it('appends page_size if specified', async () => {
await client.fetchTraces({ pageSize: 10 });
- expect(getQueryParam()).toBe('page_size=10');
+ expect(getQueryParam()).toContain('page_size=10');
});
it('converts filter to proper query params', async () => {
@@ -244,7 +256,7 @@ describe('buildClient', () => {
{ operator: '=', value: 'op' },
{ operator: '!=', value: 'not-op' },
],
- serviceName: [
+ service: [
{ operator: '=', value: 'service' },
{ operator: '!=', value: 'not-service' },
],
@@ -253,14 +265,16 @@ describe('buildClient', () => {
{ operator: '=', value: 'trace-id' },
{ operator: '!=', value: 'not-trace-id' },
],
+ attribute: [{ operator: '=', value: 'name1=value1' }],
},
});
- expect(getQueryParam()).toBe(
+ expect(getQueryParam()).toContain(
'gt[duration_nano]=100000000&lt[duration_nano]=1000000000' +
'&operation=op&not[operation]=not-op' +
'&service_name=service&not[service_name]=not-service' +
'&period=5m' +
- '&trace_id=trace-id&not[trace_id]=not-trace-id',
+ '&trace_id=trace-id&not[trace_id]=not-trace-id' +
+ '&attr_name=name1&attr_value=value1',
);
});
@@ -273,7 +287,7 @@ describe('buildClient', () => {
],
},
});
- expect(getQueryParam()).toBe('operation=op&operation=op2');
+ expect(getQueryParam()).toContain('operation=op&operation=op2');
});
it('ignores unsupported filters', async () => {
@@ -283,7 +297,7 @@ describe('buildClient', () => {
},
});
- expect(getQueryParam()).toBe('');
+ expect(getQueryParam()).toBe(`sort=${SORTING_OPTIONS.TIMESTAMP_DESC}`);
});
it('ignores empty filters', async () => {
@@ -294,7 +308,7 @@ describe('buildClient', () => {
},
});
- expect(getQueryParam()).toBe('');
+ expect(getQueryParam()).toBe(`sort=${SORTING_OPTIONS.TIMESTAMP_DESC}`);
});
it('ignores unsupported operators', async () => {
@@ -309,7 +323,7 @@ describe('buildClient', () => {
{ operator: '>', value: 'foo' },
{ operator: '<', value: 'foo' },
],
- serviceName: [
+ service: [
{ operator: '>', value: 'foo' },
{ operator: '<', value: 'foo' },
],
@@ -321,7 +335,7 @@ describe('buildClient', () => {
},
});
- expect(getQueryParam()).toBe('');
+ expect(getQueryParam()).toBe(`sort=${SORTING_OPTIONS.TIMESTAMP_DESC}`);
});
});
});
@@ -348,7 +362,7 @@ describe('buildClient', () => {
const e = 'failed to fetch services. invalid response';
await expect(client.fetchServices()).rejects.toThrow(e);
- expect(Sentry.captureException).toHaveBeenCalledWith(new Error(e));
+ expectErrorToBeReported(new Error(e));
});
});
@@ -375,19 +389,17 @@ describe('buildClient', () => {
it('rejects if serviceName is missing', async () => {
const e = 'fetchOperations() - serviceName is required.';
await expect(client.fetchOperations()).rejects.toThrow(e);
- expect(Sentry.captureException).toHaveBeenCalledWith(new Error(e));
+ expectErrorToBeReported(new Error(e));
});
it('rejects if operationUrl does not contain $SERVICE_NAME$', async () => {
client = buildClient({
- tracingUrl,
- provisioningUrl,
- servicesUrl,
+ ...apiConfig,
operationsUrl: 'something',
});
const e = 'fetchOperations() - operationsUrl must contain $SERVICE_NAME$';
await expect(client.fetchOperations(serviceName)).rejects.toThrow(e);
- expect(Sentry.captureException).toHaveBeenCalledWith(new Error(e));
+ expectErrorToBeReported(new Error(e));
});
it('rejects if operations are missing', async () => {
@@ -395,7 +407,44 @@ describe('buildClient', () => {
const e = 'failed to fetch operations. invalid response';
await expect(client.fetchOperations(serviceName)).rejects.toThrow(e);
- expect(Sentry.captureException).toHaveBeenCalledWith(new Error(e));
+ expectErrorToBeReported(new Error(e));
+ });
+ });
+
+ describe('fetchMetrics', () => {
+ const FETCHING_METRICS_ERROR = 'metrics are missing/invalid in the response';
+
+ it('fetches metrics from the metrics URL', async () => {
+ const mockResponse = {
+ metrics: [
+ { name: 'metric A', description: 'a counter metric called A', type: 'COUNTER' },
+ { name: 'metric B', description: 'a gauge metric called B', type: 'GAUGE' },
+ ],
+ };
+
+ axiosMock.onGet(metricsUrl).reply(200, mockResponse);
+
+ const result = await client.fetchMetrics();
+
+ expect(axios.get).toHaveBeenCalledTimes(1);
+ expect(axios.get).toHaveBeenCalledWith(metricsUrl, {
+ withCredentials: true,
+ });
+ expect(result).toEqual(mockResponse);
+ });
+
+ it('rejects if metrics are missing', async () => {
+ axiosMock.onGet(metricsUrl).reply(200, {});
+
+ await expect(client.fetchMetrics()).rejects.toThrow(FETCHING_METRICS_ERROR);
+ expectErrorToBeReported(new Error(FETCHING_METRICS_ERROR));
+ });
+
+ it('rejects if metrics are invalid', async () => {
+ axiosMock.onGet(metricsUrl).reply(200, { traces: 'invalid' });
+
+ await expect(client.fetchMetrics()).rejects.toThrow(FETCHING_METRICS_ERROR);
+ expectErrorToBeReported(new Error(FETCHING_METRICS_ERROR));
});
});
});
diff --git a/spec/frontend/observability/loader_spec.js b/spec/frontend/observability/loader_spec.js
new file mode 100644
index 00000000000..abd1e6f3fe0
--- /dev/null
+++ b/spec/frontend/observability/loader_spec.js
@@ -0,0 +1,103 @@
+import { nextTick } from 'vue';
+import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import Loader from '~/observability/components/loader/index.vue';
+import { DEFAULT_TIMERS, CONTENT_STATE } from '~/observability/components/loader/constants';
+
+describe('Loader component', () => {
+ let wrapper;
+
+ const findSpinner = () => wrapper.findComponent(GlLoadingIcon);
+
+ const findContentWrapper = () => wrapper.findByTestId('content-wrapper');
+
+ const findAlert = () => wrapper.findComponent(GlAlert);
+
+ const mountComponent = ({ ...props } = {}) => {
+ wrapper = shallowMountExtended(Loader, {
+ propsData: props,
+ });
+ };
+
+ describe('on mount', () => {
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ describe('showing content', () => {
+ it('shows the loader if content is not loaded within CONTENT_WAIT_MS', async () => {
+ expect(findSpinner().exists()).toBe(false);
+ expect(findContentWrapper().exists()).toBe(false);
+
+ jest.advanceTimersByTime(DEFAULT_TIMERS.CONTENT_WAIT_MS);
+
+ await nextTick();
+
+ expect(findSpinner().exists()).toBe(true);
+ expect(findContentWrapper().exists()).toBe(false);
+ });
+
+ it('does not show the loader if content loads within CONTENT_WAIT_MS', async () => {
+ expect(findSpinner().exists()).toBe(false);
+ expect(findContentWrapper().exists()).toBe(false);
+
+ await wrapper.setProps({ contentState: CONTENT_STATE.LOADED });
+
+ expect(findContentWrapper().exists()).toBe(true);
+ expect(findSpinner().exists()).toBe(false);
+
+ jest.advanceTimersByTime(DEFAULT_TIMERS.CONTENT_WAIT_MS);
+
+ await nextTick();
+
+ expect(findContentWrapper().exists()).toBe(true);
+ expect(findSpinner().exists()).toBe(false);
+ });
+
+ it('hides the loader after content loads', async () => {
+ jest.advanceTimersByTime(DEFAULT_TIMERS.CONTENT_WAIT_MS);
+
+ await nextTick();
+
+ expect(findSpinner().exists()).toBe(true);
+ expect(findContentWrapper().exists()).toBe(false);
+
+ await wrapper.setProps({ contentState: CONTENT_STATE.LOADED });
+
+ expect(findContentWrapper().exists()).toBe(true);
+ expect(findSpinner().exists()).toBe(false);
+ });
+ });
+
+ describe('error handling', () => {
+ it('shows the error dialog if content has not loaded within TIMEOUT_MS', async () => {
+ expect(findAlert().exists()).toBe(false);
+ jest.advanceTimersByTime(DEFAULT_TIMERS.TIMEOUT_MS);
+
+ await nextTick();
+
+ expect(findAlert().exists()).toBe(true);
+ expect(findContentWrapper().exists()).toBe(false);
+ });
+
+ it('shows the error dialog if content fails to load', async () => {
+ expect(findAlert().exists()).toBe(false);
+
+ await wrapper.setProps({ contentState: 'error' });
+
+ expect(findAlert().exists()).toBe(true);
+ expect(findContentWrapper().exists()).toBe(false);
+ });
+
+ it('does not show the error dialog if content has loaded within TIMEOUT_MS', async () => {
+ wrapper.setProps({ contentState: CONTENT_STATE.LOADED });
+ jest.advanceTimersByTime(DEFAULT_TIMERS.TIMEOUT_MS);
+
+ await nextTick();
+
+ expect(findAlert().exists()).toBe(false);
+ expect(findContentWrapper().exists()).toBe(true);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/observability/observability_container_spec.js b/spec/frontend/observability/observability_container_spec.js
index 5d838756308..41906b2f45d 100644
--- a/spec/frontend/observability/observability_container_spec.js
+++ b/spec/frontend/observability/observability_container_spec.js
@@ -1,41 +1,43 @@
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { stubComponent } from 'helpers/stub_component';
import ObservabilityContainer from '~/observability/components/observability_container.vue';
-import ObservabilitySkeleton from '~/observability/components/skeleton/index.vue';
+import ObservabilityLoader from '~/observability/components/loader/index.vue';
+import { CONTENT_STATE } from '~/observability/components/loader/constants';
import { buildClient } from '~/observability/client';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
+import { logError } from '~/lib/logger';
jest.mock('~/observability/client');
+jest.mock('~/sentry/sentry_browser_wrapper');
+jest.mock('~/lib/logger');
describe('ObservabilityContainer', () => {
let wrapper;
- const mockSkeletonOnContentLoaded = jest.fn();
- const mockSkeletonOnError = jest.fn();
-
const OAUTH_URL = 'https://example.com/oauth';
const TRACING_URL = 'https://example.com/tracing';
const PROVISIONING_URL = 'https://example.com/provisioning';
const SERVICES_URL = 'https://example.com/services';
const OPERATIONS_URL = 'https://example.com/operations';
+ const METRICS_URL = 'https://example.com/metrics';
+
+ const mockClient = { mock: 'client' };
beforeEach(() => {
jest.spyOn(console, 'error').mockImplementation();
- buildClient.mockReturnValue({});
+ buildClient.mockReturnValue(mockClient);
wrapper = shallowMountExtended(ObservabilityContainer, {
propsData: {
- oauthUrl: OAUTH_URL,
- tracingUrl: TRACING_URL,
- provisioningUrl: PROVISIONING_URL,
- servicesUrl: SERVICES_URL,
- operationsUrl: OPERATIONS_URL,
- },
- stubs: {
- ObservabilitySkeleton: stubComponent(ObservabilitySkeleton, {
- methods: { onContentLoaded: mockSkeletonOnContentLoaded, onError: mockSkeletonOnError },
- }),
+ apiConfig: {
+ oauthUrl: OAUTH_URL,
+ tracingUrl: TRACING_URL,
+ provisioningUrl: PROVISIONING_URL,
+ servicesUrl: SERVICES_URL,
+ operationsUrl: OPERATIONS_URL,
+ metricssUrl: METRICS_URL,
+ },
},
slots: {
default: {
@@ -43,12 +45,6 @@ describe('ObservabilityContainer', () => {
h(`<div>mockedComponent</div>`);
},
name: 'MockComponent',
- props: {
- observabilityClient: {
- type: Object,
- required: true,
- },
- },
},
},
});
@@ -60,6 +56,8 @@ describe('ObservabilityContainer', () => {
data: {
type: 'AUTH_COMPLETION',
status,
+ message: 'test-message',
+ statusCode: 'test-code',
},
origin: origin ?? new URL(OAUTH_URL).origin,
}),
@@ -67,6 +65,7 @@ describe('ObservabilityContainer', () => {
const findIframe = () => wrapper.findByTestId('observability-oauth-iframe');
const findSlotComponent = () => wrapper.findComponent({ name: 'MockComponent' });
+ const findLoader = () => wrapper.findComponent(ObservabilityLoader);
it('should render the oauth iframe', () => {
const iframe = findIframe();
@@ -76,56 +75,87 @@ describe('ObservabilityContainer', () => {
expect(iframe.attributes('sandbox')).toBe('allow-same-origin allow-forms allow-scripts');
});
- it('should render the ObservabilitySkeleton', () => {
- const skeleton = wrapper.findComponent(ObservabilitySkeleton);
- expect(skeleton.exists()).toBe(true);
+ it('should render the ObservabilityLoader', () => {
+ expect(findLoader().exists()).toBe(true);
});
it('should not render the default slot', () => {
expect(findSlotComponent().exists()).toBe(false);
});
- it('renders the slot content and removes the iframe on oauth success message', async () => {
- dispatchMessageEvent('success');
+ it('should not emit observability-client-ready', () => {
+ expect(wrapper.emitted('observability-client-ready')).toBeUndefined();
+ });
- await nextTick();
+ describe('on oauth success message', () => {
+ beforeEach(async () => {
+ dispatchMessageEvent('success');
- expect(mockSkeletonOnContentLoaded).toHaveBeenCalledTimes(1);
+ await nextTick();
+ });
+
+ it('sets the loader contentState to LOADED', () => {
+ expect(findLoader().props('contentState')).toBe(CONTENT_STATE.LOADED);
+ });
+
+ it('renders the slot content', () => {
+ const slotComponent = findSlotComponent();
+ expect(slotComponent.exists()).toBe(true);
+ });
+
+ it('build the observability client', () => {
+ expect(buildClient).toHaveBeenCalledWith(wrapper.props('apiConfig'));
+ });
- const slotComponent = findSlotComponent();
- expect(slotComponent.exists()).toBe(true);
- expect(buildClient).toHaveBeenCalledWith({
- provisioningUrl: PROVISIONING_URL,
- tracingUrl: TRACING_URL,
- servicesUrl: SERVICES_URL,
- operationsUrl: OPERATIONS_URL,
+ it('emits observability-client-ready', () => {
+ expect(wrapper.emitted('observability-client-ready')).toEqual([[mockClient]]);
});
- expect(findIframe().exists()).toBe(false);
});
- it('does not render the slot content and removes the iframe on oauth error message', async () => {
- dispatchMessageEvent('error');
+ describe('on oauth error message', () => {
+ beforeEach(async () => {
+ dispatchMessageEvent('error');
- await nextTick();
+ await nextTick();
+ });
- expect(mockSkeletonOnError).toHaveBeenCalledTimes(1);
+ it('set the loader contentState to ERROR', () => {
+ expect(findLoader().props('contentState')).toBe(CONTENT_STATE.ERROR);
+ });
- expect(findSlotComponent().exists()).toBe(false);
- expect(findIframe().exists()).toBe(false);
- expect(buildClient).not.toHaveBeenCalled();
+ it('does not renders the slot content', () => {
+ expect(findSlotComponent().exists()).toBe(false);
+ });
+
+ it('does not build the observability client', () => {
+ expect(buildClient).not.toHaveBeenCalled();
+ });
+
+ it('does not emit observability-client-ready', () => {
+ expect(wrapper.emitted('observability-client-ready')).toBeUndefined();
+ });
+
+ it('reports the error', () => {
+ const e = new Error('GOB auth failed with error: test-message - status: test-code');
+ expect(Sentry.captureException).toHaveBeenCalledWith(e);
+ expect(logError).toHaveBeenCalledWith(e);
+ });
});
- it('handles oauth message only once', () => {
- dispatchMessageEvent('success');
+ it('handles oauth message only once', async () => {
dispatchMessageEvent('success');
+ dispatchMessageEvent('error');
+
+ await nextTick();
- expect(mockSkeletonOnContentLoaded).toHaveBeenCalledTimes(1);
+ expect(buildClient).toHaveBeenCalledTimes(1);
+ expect(findLoader().props('contentState')).toBe(CONTENT_STATE.LOADED);
});
it('only handles messages from the oauth url', () => {
dispatchMessageEvent('success', 'www.fake-url.com');
- expect(mockSkeletonOnContentLoaded).toHaveBeenCalledTimes(0);
+ expect(findLoader().props('contentState')).toBe(null);
expect(findSlotComponent().exists()).toBe(false);
expect(findIframe().exists()).toBe(true);
});
@@ -135,6 +165,6 @@ describe('ObservabilityContainer', () => {
dispatchMessageEvent('success');
- expect(mockSkeletonOnContentLoaded).toHaveBeenCalledTimes(0);
+ expect(findLoader().props('contentState')).toBe(null);
});
});
diff --git a/spec/frontend/observability/observability_empty_state_spec.js b/spec/frontend/observability/observability_empty_state_spec.js
new file mode 100644
index 00000000000..ce420dd076d
--- /dev/null
+++ b/spec/frontend/observability/observability_empty_state_spec.js
@@ -0,0 +1,36 @@
+import { GlButton, GlEmptyState } from '@gitlab/ui';
+import ObservabilityEmptyState from '~/observability/components/observability_empty_state.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+describe('ObservabilityEmptyState', () => {
+ let wrapper;
+
+ const findEnableButton = () => wrapper.findComponent(GlButton);
+
+ beforeEach(() => {
+ wrapper = shallowMountExtended(ObservabilityEmptyState);
+ });
+
+ it('passes the correct title', () => {
+ expect(wrapper.findComponent(GlEmptyState).props('title')).toBe(
+ 'Get started with GitLab Observability',
+ );
+ });
+
+ it('displays the correct description', () => {
+ const description = wrapper.find('span').text();
+ expect(description).toBe('Monitor your applications with GitLab Observability.');
+ });
+
+ it('displays the enable button', () => {
+ const enableButton = findEnableButton();
+ expect(enableButton.exists()).toBe(true);
+ expect(enableButton.text()).toBe('Enable');
+ });
+
+ it('emits enable-tracing when enable button is clicked', () => {
+ findEnableButton().vm.$emit('click');
+
+ expect(wrapper.emitted('enable-observability')).toHaveLength(1);
+ });
+});
diff --git a/spec/frontend/observability/provisioned_observability_container_spec.js b/spec/frontend/observability/provisioned_observability_container_spec.js
new file mode 100644
index 00000000000..a2e8b60dc9f
--- /dev/null
+++ b/spec/frontend/observability/provisioned_observability_container_spec.js
@@ -0,0 +1,156 @@
+import { GlLoadingIcon } from '@gitlab/ui';
+import ProvisionedObservabilityContainer from '~/observability/components/provisioned_observability_container.vue';
+import ObservabilityContainer from '~/observability/components/observability_container.vue';
+import ObservabilityEmptyState from '~/observability/components/observability_empty_state.vue';
+import waitForPromises from 'helpers/wait_for_promises';
+
+import { createAlert } from '~/alert';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+jest.mock('~/alert');
+
+describe('ProvisionedObservabilityContainer', () => {
+ let wrapper;
+ let mockClient;
+
+ const mockClientReady = async () => {
+ await wrapper
+ .findComponent(ObservabilityContainer)
+ .vm.$emit('observability-client-ready', mockClient);
+ };
+
+ const mockClientReadyAndWait = async () => {
+ await wrapper
+ .findComponent(ObservabilityContainer)
+ .vm.$emit('observability-client-ready', mockClient);
+ await waitForPromises();
+ };
+
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findEmptyState = () => wrapper.findComponent(ObservabilityEmptyState);
+ const findSlotComponent = () => wrapper.findComponent({ name: 'MockComponent' });
+
+ const props = {
+ apiConfig: {
+ oauthUrl: 'https://example.com/oauth',
+ tracingUrl: 'https://example.com/tracing',
+ servicesUrl: 'https://example.com/services',
+ provisioningUrl: 'https://example.com/provisioning',
+ operationsUrl: 'https://example.com/operations',
+ metricsUrl: 'https://example.com/metrics',
+ },
+ };
+
+ beforeEach(() => {
+ mockClient = {
+ isObservabilityEnabled: jest.fn().mockResolvedValue(true),
+ enableObservability: jest.fn().mockResolvedValue(true),
+ };
+ wrapper = shallowMountExtended(ProvisionedObservabilityContainer, {
+ propsData: props,
+ slots: {
+ default: {
+ render(h) {
+ h(`<div>mockedComponent</div>`);
+ },
+ name: 'MockComponent',
+ },
+ },
+ });
+ });
+
+ it('renders the observability-container', () => {
+ const observabilityContainer = wrapper.findComponent(ObservabilityContainer);
+ expect(observabilityContainer.exists()).toBe(true);
+ expect(observabilityContainer.props('apiConfig')).toStrictEqual(props.apiConfig);
+ });
+
+ describe('when the client is ready', () => {
+ it('renders the loading indicator while checking if observability is enabled', async () => {
+ await mockClientReady();
+
+ expect(findLoadingIcon().exists()).toBe(true);
+ expect(findEmptyState().exists()).toBe(false);
+ expect(findSlotComponent().exists()).toBe(false);
+ expect(mockClient.isObservabilityEnabled).toHaveBeenCalledTimes(1);
+ });
+
+ describe('if observability is enabled', () => {
+ beforeEach(async () => {
+ mockClient.isObservabilityEnabled.mockResolvedValue(true);
+ await mockClientReadyAndWait();
+ });
+
+ it('renders the content slot', () => {
+ expect(findLoadingIcon().exists()).toBe(false);
+ expect(findEmptyState().exists()).toBe(false);
+ expect(findSlotComponent().exists()).toBe(true);
+ });
+ });
+
+ describe('if observability is not enabled', () => {
+ beforeEach(async () => {
+ mockClient.isObservabilityEnabled.mockResolvedValue(false);
+ await mockClientReadyAndWait();
+ });
+
+ it('renders the empty state', () => {
+ expect(findLoadingIcon().exists()).toBe(false);
+ expect(findEmptyState().exists()).toBe(true);
+ expect(findSlotComponent().exists()).toBe(false);
+ });
+
+ describe('when empty-state emits enable-observability', () => {
+ it('shows the loading icon', async () => {
+ await findEmptyState().vm.$emit('enable-observability');
+
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+
+ it('enable observability', async () => {
+ await findEmptyState().vm.$emit('enable-observability');
+
+ expect(mockClient.enableObservability).toHaveBeenCalledTimes(1);
+ });
+
+ it('shows the content slot', async () => {
+ await findEmptyState().vm.$emit('enable-observability');
+ await waitForPromises();
+
+ expect(findLoadingIcon().exists()).toBe(false);
+ expect(findEmptyState().exists()).toBe(false);
+ expect(findSlotComponent().exists()).toBe(true);
+ });
+ });
+ });
+ });
+
+ describe('error handling', () => {
+ it('shows an alert if checking if observability is enabled fails', async () => {
+ mockClient.isObservabilityEnabled.mockRejectedValue(new Error('error'));
+
+ await mockClientReadyAndWait();
+
+ expect(findLoadingIcon().exists()).toBe(false);
+ expect(findEmptyState().exists()).toBe(false);
+ expect(findSlotComponent().exists()).toBe(false);
+ expect(createAlert).toHaveBeenLastCalledWith({
+ message: 'Error: Failed to load page. Try reloading the page.',
+ });
+ });
+
+ it('shows an alert when checking if observability is enabled fails', async () => {
+ mockClient.isObservabilityEnabled.mockResolvedValue(false);
+ mockClient.enableObservability.mockRejectedValue(new Error('error'));
+
+ await mockClientReadyAndWait();
+
+ await findEmptyState().vm.$emit('enable-observability');
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenLastCalledWith({
+ message: 'Error: Failed to enable GitLab Observability. Please retry later.',
+ });
+ });
+ });
+});
diff --git a/spec/frontend/observability/skeleton_spec.js b/spec/frontend/observability/skeleton_spec.js
deleted file mode 100644
index 5501fa117e0..00000000000
--- a/spec/frontend/observability/skeleton_spec.js
+++ /dev/null
@@ -1,144 +0,0 @@
-import { nextTick } from 'vue';
-import { GlSkeletonLoader, GlAlert, GlLoadingIcon } from '@gitlab/ui';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-
-import Skeleton from '~/observability/components/skeleton/index.vue';
-
-import { DEFAULT_TIMERS } from '~/observability/constants';
-
-describe('Skeleton component', () => {
- let wrapper;
-
- const findSpinner = () => wrapper.findComponent(GlLoadingIcon);
-
- const findContentWrapper = () => wrapper.findByTestId('content-wrapper');
-
- const findAlert = () => wrapper.findComponent(GlAlert);
-
- const mountComponent = ({ ...props } = {}) => {
- wrapper = shallowMountExtended(Skeleton, {
- propsData: props,
- });
- };
-
- describe('on mount', () => {
- beforeEach(() => {
- mountComponent({ variant: 'spinner' });
- });
-
- describe('showing content', () => {
- it('shows the skeleton if content is not loaded within CONTENT_WAIT_MS', async () => {
- expect(findSpinner().exists()).toBe(false);
- expect(findContentWrapper().exists()).toBe(false);
-
- jest.advanceTimersByTime(DEFAULT_TIMERS.CONTENT_WAIT_MS);
-
- await nextTick();
-
- expect(findSpinner().exists()).toBe(true);
- expect(findContentWrapper().exists()).toBe(false);
- });
-
- it('does not show the skeleton if content loads within CONTENT_WAIT_MS', async () => {
- expect(findSpinner().exists()).toBe(false);
- expect(findContentWrapper().exists()).toBe(false);
-
- wrapper.vm.onContentLoaded();
-
- await nextTick();
-
- expect(findContentWrapper().exists()).toBe(true);
- expect(findSpinner().exists()).toBe(false);
-
- jest.advanceTimersByTime(DEFAULT_TIMERS.CONTENT_WAIT_MS);
-
- await nextTick();
-
- expect(findContentWrapper().exists()).toBe(true);
- expect(findSpinner().exists()).toBe(false);
- });
-
- it('hides the skeleton after content loads', async () => {
- jest.advanceTimersByTime(DEFAULT_TIMERS.CONTENT_WAIT_MS);
-
- await nextTick();
-
- expect(findSpinner().exists()).toBe(true);
- expect(findContentWrapper().exists()).toBe(false);
-
- wrapper.vm.onContentLoaded();
-
- await nextTick();
-
- expect(findContentWrapper().exists()).toBe(true);
- expect(findSpinner().exists()).toBe(false);
- });
- });
-
- describe('error handling', () => {
- it('shows the error dialog if content has not loaded within TIMEOUT_MS', async () => {
- expect(findAlert().exists()).toBe(false);
- jest.advanceTimersByTime(DEFAULT_TIMERS.TIMEOUT_MS);
-
- await nextTick();
-
- expect(findAlert().exists()).toBe(true);
- expect(findContentWrapper().exists()).toBe(false);
- });
-
- it('shows the error dialog if content fails to load', async () => {
- expect(findAlert().exists()).toBe(false);
-
- wrapper.vm.onError();
-
- await nextTick();
-
- expect(findAlert().exists()).toBe(true);
- expect(findContentWrapper().exists()).toBe(false);
- });
-
- it('does not show the error dialog if content has loaded within TIMEOUT_MS', async () => {
- wrapper.vm.onContentLoaded();
- jest.advanceTimersByTime(DEFAULT_TIMERS.TIMEOUT_MS);
-
- await nextTick();
-
- expect(findAlert().exists()).toBe(false);
- expect(findContentWrapper().exists()).toBe(true);
- });
- });
- });
-
- describe('skeleton variant', () => {
- it('shows only the spinner variant when variant is spinner', async () => {
- mountComponent({ variant: 'spinner' });
- jest.advanceTimersByTime(DEFAULT_TIMERS.CONTENT_WAIT_MS);
- await nextTick();
-
- expect(findSpinner().exists()).toBe(true);
- expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(false);
- });
-
- it('shows only the default variant when variant is not spinner', async () => {
- mountComponent({ variant: 'unknown' });
- jest.advanceTimersByTime(DEFAULT_TIMERS.CONTENT_WAIT_MS);
- await nextTick();
-
- expect(findSpinner().exists()).toBe(false);
- expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(true);
- });
- });
-
- describe('on destroy', () => {
- it('should clear init timer and timeout timer', () => {
- jest.spyOn(global, 'clearTimeout');
- mountComponent();
- wrapper.destroy();
- expect(clearTimeout).toHaveBeenCalledTimes(2);
- expect(clearTimeout.mock.calls).toEqual([
- [wrapper.vm.loadingTimeout], // First call
- [wrapper.vm.errorTimeout], // Second call
- ]);
- });
- });
-});
diff --git a/spec/frontend/organizations/new/components/app_spec.js b/spec/frontend/organizations/new/components/app_spec.js
index 06d30ad6b12..4f31baedbf6 100644
--- a/spec/frontend/organizations/new/components/app_spec.js
+++ b/spec/frontend/organizations/new/components/app_spec.js
@@ -3,16 +3,19 @@ import Vue, { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import App from '~/organizations/new/components/app.vue';
-import resolvers from '~/organizations/shared/graphql/resolvers';
+import organizationCreateMutation from '~/organizations/new/graphql/mutations/organization_create.mutation.graphql';
import NewEditForm from '~/organizations/shared/components/new_edit_form.vue';
import { visitUrlWithAlerts } from '~/lib/utils/url_utility';
-import { createOrganizationResponse } from '~/organizations/mock_data';
+import FormErrorsAlert from '~/vue_shared/components/form/errors_alert.vue';
+import {
+ organizationCreateResponse,
+ organizationCreateResponseWithErrors,
+} from '~/organizations/mock_data';
import { createAlert } from '~/alert';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
Vue.use(VueApollo);
-jest.useFakeTimers();
jest.mock('~/lib/utils/url_utility');
jest.mock('~/alert');
@@ -21,8 +24,12 @@ describe('OrganizationNewApp', () => {
let wrapper;
let mockApollo;
- const createComponent = ({ mockResolvers = resolvers } = {}) => {
- mockApollo = createMockApollo([], mockResolvers);
+ const createComponent = ({
+ handlers = [
+ [organizationCreateMutation, jest.fn().mockResolvedValue(organizationCreateResponse)],
+ ],
+ } = {}) => {
+ mockApollo = createMockApollo(handlers);
wrapper = shallowMountExtended(App, { apolloProvider: mockApollo });
};
@@ -46,13 +53,11 @@ describe('OrganizationNewApp', () => {
describe('when form is submitted', () => {
describe('when API is loading', () => {
beforeEach(async () => {
- const mockResolvers = {
- Mutation: {
- createOrganization: jest.fn().mockReturnValueOnce(new Promise(() => {})),
- },
- };
-
- createComponent({ mockResolvers });
+ createComponent({
+ handlers: [
+ [organizationCreateMutation, jest.fn().mockReturnValueOnce(new Promise(() => {}))],
+ ],
+ });
await submitForm();
});
@@ -66,13 +71,12 @@ describe('OrganizationNewApp', () => {
beforeEach(async () => {
createComponent();
await submitForm();
- jest.runAllTimers();
await waitForPromises();
});
- it('redirects user to organization path', () => {
+ it('redirects user to organization web url', () => {
expect(visitUrlWithAlerts).toHaveBeenCalledWith(
- createOrganizationResponse.organization.path,
+ organizationCreateResponse.data.organizationCreate.organization.webUrl,
[
{
id: 'organization-successfully-created',
@@ -86,26 +90,44 @@ describe('OrganizationNewApp', () => {
});
describe('when API request is not successful', () => {
- const error = new Error();
-
- beforeEach(async () => {
- const mockResolvers = {
- Mutation: {
- createOrganization: jest.fn().mockRejectedValueOnce(error),
- },
- };
+ describe('when there is a network error', () => {
+ const error = new Error();
+
+ beforeEach(async () => {
+ createComponent({
+ handlers: [[organizationCreateMutation, jest.fn().mockRejectedValue(error)]],
+ });
+ await submitForm();
+ await waitForPromises();
+ });
- createComponent({ mockResolvers });
- await submitForm();
- jest.runAllTimers();
- await waitForPromises();
+ it('displays error alert', () => {
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'An error occurred creating an organization. Please try again.',
+ error,
+ captureError: true,
+ });
+ });
});
- it('displays error alert', () => {
- expect(createAlert).toHaveBeenCalledWith({
- message: 'An error occurred creating an organization. Please try again.',
- error,
- captureError: true,
+ describe('when there are GraphQL errors', () => {
+ beforeEach(async () => {
+ createComponent({
+ handlers: [
+ [
+ organizationCreateMutation,
+ jest.fn().mockResolvedValue(organizationCreateResponseWithErrors),
+ ],
+ ],
+ });
+ await submitForm();
+ await waitForPromises();
+ });
+
+ it('displays form errors alert', () => {
+ expect(wrapper.findComponent(FormErrorsAlert).props('errors')).toEqual(
+ organizationCreateResponseWithErrors.data.organizationCreate.errors,
+ );
});
});
});
diff --git a/spec/frontend/organizations/settings/general/components/app_spec.js b/spec/frontend/organizations/settings/general/components/app_spec.js
new file mode 100644
index 00000000000..6d75f8a9949
--- /dev/null
+++ b/spec/frontend/organizations/settings/general/components/app_spec.js
@@ -0,0 +1,19 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import OrganizationSettings from '~/organizations/settings/general/components/organization_settings.vue';
+import App from '~/organizations/settings/general/components/app.vue';
+
+describe('OrganizationSettings', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(App);
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders `Organization settings` section', () => {
+ expect(wrapper.findComponent(OrganizationSettings).exists()).toBe(true);
+ });
+});
diff --git a/spec/frontend/organizations/settings/general/components/organization_settings_spec.js b/spec/frontend/organizations/settings/general/components/organization_settings_spec.js
new file mode 100644
index 00000000000..7645b41e3bd
--- /dev/null
+++ b/spec/frontend/organizations/settings/general/components/organization_settings_spec.js
@@ -0,0 +1,126 @@
+import VueApollo from 'vue-apollo';
+import Vue, { nextTick } from 'vue';
+
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import OrganizationSettings from '~/organizations/settings/general/components/organization_settings.vue';
+import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue';
+import NewEditForm from '~/organizations/shared/components/new_edit_form.vue';
+import { FORM_FIELD_NAME, FORM_FIELD_ID } from '~/organizations/shared/constants';
+import resolvers from '~/organizations/shared/graphql/resolvers';
+import { createAlert, VARIANT_INFO } from '~/alert';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+
+Vue.use(VueApollo);
+jest.useFakeTimers();
+jest.mock('~/alert');
+
+describe('OrganizationSettings', () => {
+ let wrapper;
+ let mockApollo;
+
+ const defaultProvide = {
+ organization: {
+ id: 1,
+ name: 'GitLab',
+ },
+ };
+
+ const createComponent = ({ mockResolvers = resolvers } = {}) => {
+ mockApollo = createMockApollo([], mockResolvers);
+
+ wrapper = shallowMountExtended(OrganizationSettings, {
+ provide: defaultProvide,
+ apolloProvider: mockApollo,
+ });
+ };
+
+ const findForm = () => wrapper.findComponent(NewEditForm);
+ const submitForm = async () => {
+ findForm().vm.$emit('submit', { name: 'Foo bar', path: 'foo-bar' });
+ await nextTick();
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ mockApollo = null;
+ });
+
+ it('renders settings block', () => {
+ expect(wrapper.findComponent(SettingsBlock).exists()).toBe(true);
+ });
+
+ it('renders form with correct props', () => {
+ createComponent();
+
+ expect(findForm().props()).toMatchObject({
+ loading: false,
+ initialFormValues: defaultProvide.organization,
+ fieldsToRender: [FORM_FIELD_NAME, FORM_FIELD_ID],
+ });
+ });
+
+ describe('when form is submitted', () => {
+ describe('when API is loading', () => {
+ beforeEach(async () => {
+ const mockResolvers = {
+ Mutation: {
+ updateOrganization: jest.fn().mockReturnValueOnce(new Promise(() => {})),
+ },
+ };
+
+ createComponent({ mockResolvers });
+
+ await submitForm();
+ });
+
+ it('sets form `loading` prop to `true`', () => {
+ expect(findForm().props('loading')).toBe(true);
+ });
+ });
+
+ describe('when API request is successful', () => {
+ beforeEach(async () => {
+ createComponent();
+ await submitForm();
+ jest.runAllTimers();
+ await waitForPromises();
+ });
+
+ it('displays info alert', () => {
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'Organization was successfully updated.',
+ variant: VARIANT_INFO,
+ });
+ });
+ });
+
+ describe('when API request is not successful', () => {
+ const error = new Error();
+
+ beforeEach(async () => {
+ const mockResolvers = {
+ Mutation: {
+ updateOrganization: jest.fn().mockRejectedValueOnce(error),
+ },
+ };
+
+ createComponent({ mockResolvers });
+ await submitForm();
+ jest.runAllTimers();
+ await waitForPromises();
+ });
+
+ it('displays error alert', () => {
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'An error occurred updating your organization. Please try again.',
+ error,
+ captureError: true,
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/organizations/shared/components/new_edit_form_spec.js b/spec/frontend/organizations/shared/components/new_edit_form_spec.js
index 43c099fbb1c..93f022a3259 100644
--- a/spec/frontend/organizations/shared/components/new_edit_form_spec.js
+++ b/spec/frontend/organizations/shared/components/new_edit_form_spec.js
@@ -1,6 +1,7 @@
import { GlButton, GlInputGroupText, GlTruncate } from '@gitlab/ui';
import NewEditForm from '~/organizations/shared/components/new_edit_form.vue';
+import { FORM_FIELD_NAME, FORM_FIELD_ID, FORM_FIELD_PATH } from '~/organizations/shared/constants';
import { mountExtended } from 'helpers/vue_test_utils_helper';
describe('NewEditForm', () => {
@@ -27,6 +28,7 @@ describe('NewEditForm', () => {
};
const findNameField = () => wrapper.findByLabelText('Organization name');
+ const findIdField = () => wrapper.findByLabelText('Organization ID');
const findUrlField = () => wrapper.findByLabelText('Organization URL');
const submitForm = async () => {
await wrapper.findByRole('button', { name: 'Create organization' }).trigger('click');
@@ -47,6 +49,56 @@ describe('NewEditForm', () => {
expect(findUrlField().exists()).toBe(true);
});
+ it('requires `Organization URL` field to be a minimum of two characters', async () => {
+ createComponent();
+
+ await findUrlField().setValue('f');
+ await submitForm();
+
+ expect(
+ wrapper.findByText('Organization URL must be a minimum of two characters.').exists(),
+ ).toBe(true);
+ });
+
+ describe('when `fieldsToRender` prop is set', () => {
+ beforeEach(() => {
+ createComponent({ propsData: { fieldsToRender: [FORM_FIELD_ID] } });
+ });
+
+ it('only renders provided fields', () => {
+ expect(findNameField().exists()).toBe(false);
+ expect(findIdField().exists()).toBe(true);
+ expect(findUrlField().exists()).toBe(false);
+ });
+ });
+
+ describe('when `initialFormValues` prop is set', () => {
+ beforeEach(() => {
+ createComponent({
+ propsData: {
+ fieldsToRender: [FORM_FIELD_NAME, FORM_FIELD_ID, FORM_FIELD_PATH],
+ initialFormValues: {
+ [FORM_FIELD_NAME]: 'Foo bar',
+ [FORM_FIELD_ID]: 1,
+ [FORM_FIELD_PATH]: 'foo-bar',
+ },
+ },
+ });
+ });
+
+ it('sets initial values for fields', () => {
+ expect(findNameField().element.value).toBe('Foo bar');
+ expect(findIdField().element.value).toBe('1');
+ expect(findUrlField().element.value).toBe('foo-bar');
+ });
+ });
+
+ it('renders `Organization ID` field as disabled', () => {
+ createComponent({ propsData: { fieldsToRender: [FORM_FIELD_ID] } });
+
+ expect(findIdField().attributes('disabled')).toBe('disabled');
+ });
+
describe('when form is submitted without filling in required fields', () => {
beforeEach(async () => {
createComponent();
@@ -100,6 +152,30 @@ describe('NewEditForm', () => {
});
});
+ describe('when `Organization URL` field is not rendered', () => {
+ beforeEach(async () => {
+ createComponent({
+ propsData: {
+ fieldsToRender: [FORM_FIELD_NAME, FORM_FIELD_ID],
+ initialFormValues: {
+ [FORM_FIELD_NAME]: 'Foo bar',
+ [FORM_FIELD_ID]: 1,
+ [FORM_FIELD_PATH]: 'foo-bar',
+ },
+ },
+ });
+
+ await findNameField().setValue('Foo bar baz');
+ await submitForm();
+ });
+
+ it('does not modify `Organization URL` when typing in `Organization name`', () => {
+ expect(wrapper.emitted('submit')).toEqual([
+ [{ name: 'Foo bar baz', id: 1, path: 'foo-bar' }],
+ ]);
+ });
+ });
+
describe('when `loading` prop is `true`', () => {
beforeEach(() => {
createComponent({ propsData: { loading: true } });
@@ -109,4 +185,46 @@ describe('NewEditForm', () => {
expect(wrapper.findComponent(GlButton).props('loading')).toBe(true);
});
});
+
+ describe('when `showCancelButton` prop is `false`', () => {
+ beforeEach(() => {
+ createComponent({ propsData: { showCancelButton: false } });
+ });
+
+ it('does not show cancel button', () => {
+ expect(wrapper.findByRole('link', { name: 'Cancel' }).exists()).toBe(false);
+ });
+ });
+
+ describe('when `showCancelButton` prop is `true`', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('shows cancel button', () => {
+ expect(wrapper.findByRole('link', { name: 'Cancel' }).attributes('href')).toBe(
+ defaultProvide.organizationsPath,
+ );
+ });
+ });
+
+ describe('when `submitButtonText` prop is not set', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('defaults to `Create organization`', () => {
+ expect(wrapper.findByRole('button', { name: 'Create organization' }).exists()).toBe(true);
+ });
+ });
+
+ describe('when `submitButtonText` prop is set', () => {
+ beforeEach(() => {
+ createComponent({ propsData: { submitButtonText: 'Save changes' } });
+ });
+
+ it('uses it for submit button', () => {
+ expect(wrapper.findByRole('button', { name: 'Save changes' }).exists()).toBe(true);
+ });
+ });
});
diff --git a/spec/frontend/organizations/users/components/app_spec.js b/spec/frontend/organizations/users/components/app_spec.js
new file mode 100644
index 00000000000..b30fd984099
--- /dev/null
+++ b/spec/frontend/organizations/users/components/app_spec.js
@@ -0,0 +1,81 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { createAlert } from '~/alert';
+import organizationUsersQuery from '~/organizations/users/graphql/organization_users.query.graphql';
+import OrganizationsUsersApp from '~/organizations/users/components/app.vue';
+import { MOCK_ORGANIZATION_GID, MOCK_USERS } from '../mock_data';
+
+jest.mock('~/alert');
+
+Vue.use(VueApollo);
+
+const mockError = new Error();
+
+const loadingResolver = jest.fn().mockReturnValue(new Promise(() => {}));
+const successfulResolver = (nodes) =>
+ jest.fn().mockResolvedValue({
+ data: { organization: { id: 1, organizationUsers: { nodes } } },
+ });
+const errorResolver = jest.fn().mockRejectedValueOnce(mockError);
+
+describe('OrganizationsUsersApp', () => {
+ let wrapper;
+ let mockApollo;
+
+ const createComponent = (mockResolvers = successfulResolver(MOCK_USERS)) => {
+ mockApollo = createMockApollo([[organizationUsersQuery, mockResolvers]]);
+
+ wrapper = shallowMountExtended(OrganizationsUsersApp, {
+ apolloProvider: mockApollo,
+ provide: {
+ organizationGid: MOCK_ORGANIZATION_GID,
+ },
+ });
+ };
+
+ afterEach(() => {
+ mockApollo = null;
+ });
+
+ const findOrganizationUsersLoading = () => wrapper.findByText('Loading');
+ const findOrganizationUsers = () => wrapper.findByTestId('organization-users');
+
+ describe.each`
+ description | mockResolver | loading | userData | error
+ ${'when API call is loading'} | ${loadingResolver} | ${true} | ${[]} | ${false}
+ ${'when API returns successful with results'} | ${successfulResolver(MOCK_USERS)} | ${false} | ${MOCK_USERS} | ${false}
+ ${'when API returns successful without results'} | ${successfulResolver([])} | ${false} | ${[]} | ${false}
+ ${'when API returns error'} | ${errorResolver} | ${false} | ${[]} | ${true}
+ `('$description', ({ mockResolver, loading, userData, error }) => {
+ beforeEach(async () => {
+ createComponent(mockResolver);
+ await waitForPromises();
+ });
+
+ it(`does ${
+ loading ? '' : 'not '
+ }render the organization users view with loading placeholder`, () => {
+ expect(findOrganizationUsersLoading().exists()).toBe(loading);
+ });
+
+ it(`renders the organization users view with ${
+ userData.length ? 'correct' : 'empty'
+ } users array raw data`, () => {
+ expect(JSON.parse(findOrganizationUsers().text())).toStrictEqual(userData);
+ });
+
+ it(`does ${error ? '' : 'not '}render an error message`, () => {
+ return error
+ ? expect(createAlert).toHaveBeenCalledWith({
+ message:
+ 'An error occurred loading the organization users. Please refresh the page to try again.',
+ error: mockError,
+ captureError: true,
+ })
+ : expect(createAlert).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/organizations/users/mock_data.js b/spec/frontend/organizations/users/mock_data.js
new file mode 100644
index 00000000000..4f159c70c2c
--- /dev/null
+++ b/spec/frontend/organizations/users/mock_data.js
@@ -0,0 +1,22 @@
+export const MOCK_ORGANIZATION_GID = 'gid://gitlab/Organizations::Organization/1';
+
+export const MOCK_USERS = [
+ {
+ badges: [],
+ id: 'gid://gitlab/Organizations::OrganizationUser/3',
+ user: { id: 'gid://gitlab/User/3' },
+ },
+ {
+ badges: [],
+ id: 'gid://gitlab/Organizations::OrganizationUser/2',
+ user: { id: 'gid://gitlab/User/2' },
+ },
+ {
+ badges: [
+ { text: 'Admin', variant: 'success' },
+ { text: "It's you!", variant: 'muted' },
+ ],
+ id: 'gid://gitlab/Organizations::OrganizationUser/1',
+ user: { id: 'gid://gitlab/User/1' },
+ },
+];
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap
index 17acf7381c0..d6c3d98efa3 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap
@@ -177,7 +177,7 @@ exports[`PypiInstallation renders all the messages 1`] = `
</div>
</div>
<small
- class="form-text text-muted"
+ class="form-text text-gl-muted"
id="reference-8"
tabindex="-1"
>
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/additional_metadata_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/additional_metadata_spec.js
index 2e59c27cc1b..133941bbb2e 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/additional_metadata_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/additional_metadata_spec.js
@@ -1,7 +1,7 @@
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { GlAlert } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import {
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js
index 5ba4b1f687e..7823b146aba 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js
@@ -6,9 +6,9 @@ import {
GlModal,
GlKeysetPagination,
} from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { stubComponent } from 'helpers/stub_component';
import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js
index ed470f63b8a..d83d571872c 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { stubComponent } from 'helpers/stub_component';
import createMockApollo from 'helpers/mock_apollo_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js
index e9f2a2c5095..8e22e9a3b0c 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { GlAlert } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import { stubComponent } from 'helpers/stub_component';
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/publish_method_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/publish_method_spec.js.snap
index f202635d717..89cf5acdef3 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/publish_method_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/publish_method_spec.js.snap
@@ -21,7 +21,7 @@ exports[`publish_method renders 1`] = `
size="16"
/>
<gl-link-stub
- class="gl-mr-2"
+ class="gl-mr-2 gl-text-decoration-underline"
data-testid="pipeline-sha"
href="/namespace14/project14/-/commit/b83d6e391c22777fca1ed3012fce84f633d7fed0"
>
diff --git a/spec/frontend/packages_and_registries/package_registry/mock_data.js b/spec/frontend/packages_and_registries/package_registry/mock_data.js
index 91dc02f8f39..6c03f91b73d 100644
--- a/spec/frontend/packages_and_registries/package_registry/mock_data.js
+++ b/spec/frontend/packages_and_registries/package_registry/mock_data.js
@@ -308,7 +308,7 @@ export const packageMetadataQuery = (packageType) => {
id: 'gid://gitlab/Packages::Package/111',
packageType,
metadata: {
- ...(packageTypeMetadataQueryMapping[packageType]?.() ?? {}),
+ ...packageTypeMetadataQueryMapping[packageType]?.(),
},
__typename: 'PackageDetailsType',
},
diff --git a/spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js b/spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js
index 49e76cfbae0..bf7abd353b6 100644
--- a/spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js
+++ b/spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js
@@ -60,13 +60,17 @@ describe('Packages Settings', () => {
const findDescription = () => wrapper.findByTestId('description');
const findMavenSettings = () => wrapper.findByTestId('maven-settings');
const findGenericSettings = () => wrapper.findByTestId('generic-settings');
+ const findNugetSettings = () => wrapper.findByTestId('nuget-settings');
const findMavenDuplicatedSettingsToggle = () => findMavenSettings().findComponent(GlToggle);
const findGenericDuplicatedSettingsToggle = () => findGenericSettings().findComponent(GlToggle);
+ const findNugetDuplicatedSettingsToggle = () => findNugetSettings().findComponent(GlToggle);
const findMavenDuplicatedSettingsExceptionsInput = () =>
findMavenSettings().findComponent(ExceptionsInput);
const findGenericDuplicatedSettingsExceptionsInput = () =>
findGenericSettings().findComponent(ExceptionsInput);
+ const findNugetDuplicatedSettingsExceptionsInput = () =>
+ findNugetSettings().findComponent(ExceptionsInput);
const fillApolloCache = () => {
apolloProvider.defaultClient.cache.writeQuery({
@@ -208,6 +212,58 @@ describe('Packages Settings', () => {
});
});
+ describe('nuget settings', () => {
+ it('exists', () => {
+ mountComponent({ mountFn: mountExtended });
+
+ expect(findNugetSettings().find('td').text()).toBe('NuGet');
+ });
+
+ it('renders toggle', () => {
+ mountComponent({ mountFn: mountExtended });
+
+ const { nugetDuplicatesAllowed } = packageSettings;
+
+ expect(findNugetDuplicatedSettingsToggle().exists()).toBe(true);
+ expect(findNugetDuplicatedSettingsToggle().props()).toMatchObject({
+ label: DUPLICATES_TOGGLE_LABEL,
+ value: nugetDuplicatesAllowed,
+ disabled: false,
+ labelPosition: 'hidden',
+ });
+ });
+
+ it('renders ExceptionsInput and assigns duplication allowness and exception props', () => {
+ mountComponent({ mountFn: mountExtended });
+
+ const { nugetDuplicatesAllowed, nugetDuplicateExceptionRegex } = packageSettings;
+
+ expect(findNugetDuplicatedSettingsExceptionsInput().props()).toMatchObject({
+ duplicatesAllowed: nugetDuplicatesAllowed,
+ duplicateExceptionRegex: nugetDuplicateExceptionRegex,
+ duplicateExceptionRegexError: '',
+ loading: false,
+ name: 'nugetDuplicateExceptionRegex',
+ id: 'nuget-duplicated-settings-regex-input',
+ });
+ });
+
+ it('on update event calls the mutation', () => {
+ const mutationResolver = jest.fn().mockResolvedValue(groupPackageSettingsMutationMock());
+ mountComponent({ mountFn: mountExtended, mutationResolver });
+
+ fillApolloCache();
+
+ findNugetDuplicatedSettingsExceptionsInput().vm.$emit('update', {
+ nugetDuplicateExceptionRegex: ')',
+ });
+
+ expect(mutationResolver).toHaveBeenCalledWith({
+ input: { nugetDuplicateExceptionRegex: ')', namespacePath: 'foo_group_path' },
+ });
+ });
+ });
+
describe('settings update', () => {
describe('success state', () => {
beforeEach(() => {
diff --git a/spec/frontend/packages_and_registries/settings/group/mock_data.js b/spec/frontend/packages_and_registries/settings/group/mock_data.js
index 1ca9dc6daeb..c68b0b8e23f 100644
--- a/spec/frontend/packages_and_registries/settings/group/mock_data.js
+++ b/spec/frontend/packages_and_registries/settings/group/mock_data.js
@@ -3,6 +3,8 @@ const packageDuplicateSettings = {
mavenDuplicateExceptionRegex: '',
genericDuplicatesAllowed: true,
genericDuplicateExceptionRegex: '',
+ nugetDuplicatesAllowed: true,
+ nugetDuplicateExceptionRegex: '',
};
export const packageForwardingSettings = {
diff --git a/spec/frontend/pages/admin/application_settings/appearances/preview_sign_in/index_spec.js b/spec/frontend/pages/admin/application_settings/appearances/preview_sign_in/index_spec.js
new file mode 100644
index 00000000000..2fec65ad4c8
--- /dev/null
+++ b/spec/frontend/pages/admin/application_settings/appearances/preview_sign_in/index_spec.js
@@ -0,0 +1,10 @@
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
+
+jest.mock('~/behaviors/markdown/render_gfm');
+
+describe('Preview sign in', () => {
+ it('calls `renderGFM` to ensure that all gitlab-flavoured markdown is rendered on the preview sign in page', async () => {
+ await import('~/pages/sessions/new/index');
+ expect(renderGFM).toHaveBeenCalledWith(document.body);
+ });
+});
diff --git a/spec/frontend/pages/groups/sso/index_spec.js b/spec/frontend/pages/groups/sso/index_spec.js
new file mode 100644
index 00000000000..3166c4aa743
--- /dev/null
+++ b/spec/frontend/pages/groups/sso/index_spec.js
@@ -0,0 +1,10 @@
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
+
+jest.mock('~/behaviors/markdown/render_gfm');
+
+describe('SAML single sign-on page', () => {
+ it('calls `renderGFM` to ensure that all gitlab-flavoured markdown is rendered on the SAML single sign-on page', async () => {
+ await import('~/pages/sessions/new/index');
+ expect(renderGFM).toHaveBeenCalledWith(document.body);
+ });
+});
diff --git a/spec/frontend/pages/import/bulk_imports/details/index_spec.js b/spec/frontend/pages/import/bulk_imports/details/index_spec.js
new file mode 100644
index 00000000000..0fefa3017f7
--- /dev/null
+++ b/spec/frontend/pages/import/bulk_imports/details/index_spec.js
@@ -0,0 +1,37 @@
+import Vue from 'vue';
+import { initBulkImportDetails } from '~/pages/import/bulk_imports/details/index';
+
+jest.mock('~/import/details/components/bulk_import_details_app.vue');
+
+describe('initBulkImportDetails', () => {
+ let appRoot;
+
+ const createAppRoot = () => {
+ appRoot = document.createElement('div');
+ appRoot.setAttribute('class', 'js-bulk-import-details');
+ document.body.appendChild(appRoot);
+ };
+
+ afterEach(() => {
+ if (appRoot) {
+ appRoot.remove();
+ appRoot = null;
+ }
+ });
+
+ describe('when there is no app root', () => {
+ it('returns null', () => {
+ expect(initBulkImportDetails()).toBeNull();
+ });
+ });
+
+ describe('when there is an app root', () => {
+ beforeEach(() => {
+ createAppRoot();
+ });
+
+ it('returns a Vue instance', () => {
+ expect(initBulkImportDetails()).toBeInstanceOf(Vue);
+ });
+ });
+});
diff --git a/spec/frontend/pages/passwords/new/index_spec.js b/spec/frontend/pages/passwords/new/index_spec.js
new file mode 100644
index 00000000000..084fb317f4e
--- /dev/null
+++ b/spec/frontend/pages/passwords/new/index_spec.js
@@ -0,0 +1,10 @@
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
+
+jest.mock('~/behaviors/markdown/render_gfm');
+
+describe('Password page', () => {
+ it('calls `renderGFM` to ensure that all gitlab-flavoured markdown is rendered on the password page', async () => {
+ await import('~/pages/sessions/new/index');
+ expect(renderGFM).toHaveBeenCalledWith(document.body);
+ });
+});
diff --git a/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js b/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js
index 7bc4cd4d541..b0bfa4620c6 100644
--- a/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js
+++ b/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js
@@ -1,4 +1,11 @@
-import { GlFormInputGroup, GlFormInput, GlForm, GlFormRadioGroup, GlFormRadio } from '@gitlab/ui';
+import {
+ GlFormInputGroup,
+ GlFormInput,
+ GlForm,
+ GlFormRadioGroup,
+ GlFormRadio,
+ GlSprintf,
+} from '@gitlab/ui';
import { getByRole } from '@testing-library/dom';
import { mount, shallowMount } from '@vue/test-utils';
import axios from 'axios';
@@ -41,6 +48,7 @@ describe('ForkForm component', () => {
projectPath: 'project-name',
projectDescription: 'some project description',
projectVisibility: 'private',
+ projectDefaultBranch: 'main',
restrictedVisibilityLevels: [],
};
@@ -96,6 +104,7 @@ describe('ForkForm component', () => {
GlFormInput,
GlFormRadioGroup,
GlFormRadio,
+ GlSprintf,
},
});
};
@@ -118,13 +127,13 @@ describe('ForkForm component', () => {
const findInternalRadio = () => wrapper.find('[data-testid="radio-internal"]');
const findPublicRadio = () => wrapper.find('[data-testid="radio-public"]');
const findForkNameInput = () => wrapper.find('[data-testid="fork-name-input"]');
- const findGlFormRadioGroup = () => wrapper.findComponent(GlFormRadioGroup);
const findForkUrlInput = () => wrapper.findComponent(ProjectNamespace);
const findForkSlugInput = () => wrapper.find('[data-testid="fork-slug-input"]');
const findForkDescriptionTextarea = () =>
wrapper.find('[data-testid="fork-description-textarea"]');
const findVisibilityRadioGroup = () =>
wrapper.find('[data-testid="fork-visibility-radio-group"]');
+ const findBranchesRadioGroup = () => wrapper.find('[data-testid="fork-branches-radio-group"]');
it('will go to cancelPath when click cancel button', () => {
createComponent();
@@ -203,11 +212,25 @@ describe('ForkForm component', () => {
});
});
+ describe('branches options', () => {
+ const formRadios = () => findBranchesRadioGroup().findAllComponents(GlFormRadio);
+ it('displays 2 branches options', () => {
+ createComponent();
+ expect(formRadios()).toHaveLength(2);
+ });
+
+ it('displays the correct description for each option', () => {
+ createComponent();
+ expect(formRadios().at(0).text()).toBe('All branches');
+ expect(formRadios().at(1).text()).toMatchInterpolatedText('Only the default branch main');
+ });
+ });
+
describe('visibility level', () => {
it('displays the correct description', () => {
createComponent();
- const formRadios = wrapper.findAllComponents(GlFormRadio);
+ const formRadios = findVisibilityRadioGroup().findAllComponents(GlFormRadio);
Object.keys(PROJECT_VISIBILITY_TYPE).forEach((visibilityType, index) => {
expect(formRadios.at(index).text()).toBe(PROJECT_VISIBILITY_TYPE[visibilityType]);
@@ -217,7 +240,7 @@ describe('ForkForm component', () => {
it('displays all 3 visibility levels', () => {
createComponent();
- expect(wrapper.findAllComponents(GlFormRadio)).toHaveLength(3);
+ expect(findVisibilityRadioGroup().findAllComponents(GlFormRadio)).toHaveLength(3);
});
describe('when the namespace is changed', () => {
@@ -236,7 +259,7 @@ describe('ForkForm component', () => {
it('resets the visibility to max allowed below current level', async () => {
createFullComponent({ projectVisibility: 'public' }, { namespaces });
- expect(findGlFormRadioGroup().vm.$attrs.checked).toBe('public');
+ expect(findVisibilityRadioGroup().vm.$attrs.checked).toBe('public');
fillForm({
name: 'one',
@@ -251,7 +274,7 @@ describe('ForkForm component', () => {
it('does not reset the visibility when current level is allowed', async () => {
createFullComponent({ projectVisibility: 'public' }, { namespaces });
- expect(findGlFormRadioGroup().vm.$attrs.checked).toBe('public');
+ expect(findVisibilityRadioGroup().vm.$attrs.checked).toBe('public');
fillForm({
name: 'two',
@@ -266,7 +289,7 @@ describe('ForkForm component', () => {
it('does not reset the visibility when visibility cap is increased', async () => {
createFullComponent({ projectVisibility: 'public' }, { namespaces });
- expect(findGlFormRadioGroup().vm.$attrs.checked).toBe('public');
+ expect(findVisibilityRadioGroup().vm.$attrs.checked).toBe('public');
fillForm({
name: 'three',
@@ -291,7 +314,7 @@ describe('ForkForm component', () => {
{ namespaces },
);
- await findGlFormRadioGroup().vm.$emit('input', 'internal');
+ await findVisibilityRadioGroup().vm.$emit('input', 'internal');
fillForm({
name: 'five',
id: 5,
@@ -469,7 +492,7 @@ describe('ForkForm component', () => {
jest.spyOn(axios, 'post');
setupComponent();
- await findGlFormRadioGroup().vm.$emit('input', null);
+ await findVisibilityRadioGroup().vm.$emit('input', null);
await nextTick();
@@ -533,6 +556,7 @@ describe('ForkForm component', () => {
const url = `/api/${GON_API_VERSION}/projects/${projectId}/fork`;
const project = {
+ branches: '',
description: projectDescription,
id: projectId,
name: projectName,
diff --git a/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js b/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js
index 50d09481b93..f6ecee4cd53 100644
--- a/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js
+++ b/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js
@@ -51,7 +51,7 @@ describe('Interval Pattern Input Component', () => {
beforeEach(() => {
oldWindowGl = window.gl;
window.gl = {
- ...(window.gl || {}),
+ ...window.gl,
pipelineScheduleFieldErrors: {
updateFormValidityState: jest.fn(),
},
diff --git a/spec/frontend/pages/registrations/new/index_spec.js b/spec/frontend/pages/registrations/new/index_spec.js
new file mode 100644
index 00000000000..4d30e5c9352
--- /dev/null
+++ b/spec/frontend/pages/registrations/new/index_spec.js
@@ -0,0 +1,10 @@
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
+
+jest.mock('~/behaviors/markdown/render_gfm');
+
+describe('Sign up page', () => {
+ it('calls `renderGFM` to ensure that all gitlab-flavoured markdown is rendered on the sign up page', async () => {
+ await import('~/pages/sessions/new/index');
+ expect(renderGFM).toHaveBeenCalledWith(document.body);
+ });
+});
diff --git a/spec/frontend/pages/sessions/new/index_spec.js b/spec/frontend/pages/sessions/new/index_spec.js
new file mode 100644
index 00000000000..13aff16b3a9
--- /dev/null
+++ b/spec/frontend/pages/sessions/new/index_spec.js
@@ -0,0 +1,10 @@
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
+
+jest.mock('~/behaviors/markdown/render_gfm');
+
+describe('Sign in page', () => {
+ it('calls `renderGFM` to ensure that all gitlab-flavoured markdown is rendered on the sign in page', async () => {
+ await import('~/pages/sessions/new/index');
+ expect(renderGFM).toHaveBeenCalledWith(document.body);
+ });
+});
diff --git a/spec/frontend/pipeline_wizard/templates/pages_spec.js b/spec/frontend/pipeline_wizard/templates/pages_spec.js
index f89e8f05475..72b3b1ca852 100644
--- a/spec/frontend/pipeline_wizard/templates/pages_spec.js
+++ b/spec/frontend/pipeline_wizard/templates/pages_spec.js
@@ -39,7 +39,11 @@ describe('Pages Template', () => {
}),
expect.objectContaining({
widget: 'checklist',
- title: 'Before we begin, please check:',
+ items: [
+ expect.objectContaining({
+ text: 'The application files are in the `public` folder',
+ }),
+ ],
}),
],
template: expect.stringContaining(VAR_BUILD_IMAGE),
diff --git a/spec/frontend/projects/members/utils_spec.js b/spec/frontend/projects/members/utils_spec.js
index 813e8455e85..2624851d9d8 100644
--- a/spec/frontend/projects/members/utils_spec.js
+++ b/spec/frontend/projects/members/utils_spec.js
@@ -8,7 +8,19 @@ describe('project member utils', () => {
accessLevel: 50,
expires_at: '2020-10-16',
}),
- ).toEqual({ project_member: { access_level: 50, expires_at: '2020-10-16' } });
+ ).toEqual({
+ project_member: { access_level: 50, expires_at: '2020-10-16', member_role_id: null },
+ });
+
+ expect(
+ projectMemberRequestFormatter({
+ accessLevel: 50,
+ expires_at: '2020-10-16',
+ memberRoleId: 80,
+ }),
+ ).toEqual({
+ project_member: { access_level: 50, expires_at: '2020-10-16', member_role_id: 80 },
+ });
});
});
});
diff --git a/spec/frontend/projects/pipelines/charts/components/__snapshots__/statistics_list_spec.js.snap b/spec/frontend/projects/pipelines/charts/components/__snapshots__/statistics_list_spec.js.snap
index 16d291804cc..a2877a9f17c 100644
--- a/spec/frontend/projects/pipelines/charts/components/__snapshots__/statistics_list_spec.js.snap
+++ b/spec/frontend/projects/pipelines/charts/components/__snapshots__/statistics_list_spec.js.snap
@@ -7,7 +7,7 @@ exports[`StatisticsList displays the counts data with labels 1`] = `
Total:
</span>
<strong>
- 4 pipelines
+ 40,000 pipelines
</strong>
</li>
<li>
@@ -15,7 +15,7 @@ exports[`StatisticsList displays the counts data with labels 1`] = `
Successful:
</span>
<strong>
- 2 pipelines
+ 20,000 pipelines
</strong>
</li>
<li>
@@ -25,7 +25,7 @@ exports[`StatisticsList displays the counts data with labels 1`] = `
<gl-link-stub
href="/flightjs/Flight/-/pipelines?page=1&scope=all&status=failed"
>
- 2 pipelines
+ 20,000 pipelines
</gl-link-stub>
</li>
<li>
diff --git a/spec/frontend/projects/pipelines/charts/mock_data.js b/spec/frontend/projects/pipelines/charts/mock_data.js
index 04971b5b20e..0e3e43835d0 100644
--- a/spec/frontend/projects/pipelines/charts/mock_data.js
+++ b/spec/frontend/projects/pipelines/charts/mock_data.js
@@ -1,7 +1,7 @@
export const counts = {
- failed: 2,
- success: 2,
- total: 4,
+ failed: 20000,
+ success: 20000,
+ total: 40000,
successRatio: 50,
totalDuration: 116158,
};
diff --git a/spec/frontend/projects/settings_service_desk/components/custom_email_form_spec.js b/spec/frontend/projects/settings_service_desk/components/custom_email_form_spec.js
index 9b012995ea4..efefcdb20df 100644
--- a/spec/frontend/projects/settings_service_desk/components/custom_email_form_spec.js
+++ b/spec/frontend/projects/settings_service_desk/components/custom_email_form_spec.js
@@ -1,11 +1,17 @@
import { mount } from '@vue/test-utils';
-import { GlLink } from '@gitlab/ui';
+import { GlFormSelect, GlLink } from '@gitlab/ui';
import { nextTick } from 'vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { helpPagePath } from '~/helpers/help_page_helper';
import CustomEmailForm from '~/projects/settings_service_desk/components/custom_email_form.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-import { I18N_FORM_FORWARDING_CLIPBOARD_BUTTON_TITLE } from '~/projects/settings_service_desk/custom_email_constants';
+import {
+ I18N_FORM_FORWARDING_CLIPBOARD_BUTTON_TITLE,
+ I18N_FORM_SMTP_AUTHENTICATION_NONE,
+ I18N_FORM_SMTP_AUTHENTICATION_PLAIN,
+ I18N_FORM_SMTP_AUTHENTICATION_LOGIN,
+ I18N_FORM_SMTP_AUTHENTICATION_CRAM_MD5,
+} from '~/projects/settings_service_desk/custom_email_constants';
describe('CustomEmailForm', () => {
let wrapper;
@@ -24,6 +30,7 @@ describe('CustomEmailForm', () => {
const findSmtpPortInput = () => findInputByTestId('form-smtp-port');
const findSmtpUsernameInput = () => findInputByTestId('form-smtp-username');
const findSmtpPasswordInput = () => findInputByTestId('form-smtp-password');
+ const findSmtpAuthenticationSelect = () => wrapper.findComponent(GlFormSelect).find('select');
const findSubmit = () => wrapper.findByTestId('form-submit');
const clickButtonAndExpectNoSubmitEvent = async () => {
@@ -60,6 +67,23 @@ describe('CustomEmailForm', () => {
);
});
+ it('renders correct translations for options for SMTP authentication', () => {
+ createWrapper();
+
+ const translationStrings = [
+ I18N_FORM_SMTP_AUTHENTICATION_NONE,
+ I18N_FORM_SMTP_AUTHENTICATION_PLAIN,
+ I18N_FORM_SMTP_AUTHENTICATION_LOGIN,
+ I18N_FORM_SMTP_AUTHENTICATION_CRAM_MD5,
+ ];
+
+ findSmtpAuthenticationSelect()
+ .findAll('option')
+ .wrappers.forEach((item, index) => {
+ expect(item.text()).toEqual(translationStrings[index]);
+ });
+ });
+
it('form inputs are disabled when submitting', () => {
createWrapper({ isSubmitting: true });
@@ -68,6 +92,7 @@ describe('CustomEmailForm', () => {
expect(findSmtpPortInput().attributes('disabled')).toBeDefined();
expect(findSmtpUsernameInput().attributes('disabled')).toBeDefined();
expect(findSmtpPasswordInput().attributes('disabled')).toBeDefined();
+ expect(findSmtpAuthenticationSelect().attributes('disabled')).toBeDefined();
expect(findSubmit().props('loading')).toBe(true);
});
@@ -99,6 +124,8 @@ describe('CustomEmailForm', () => {
findSmtpPasswordInput().setValue('supersecret');
findSmtpPasswordInput().trigger('change');
+
+ findSmtpAuthenticationSelect().setValue('login');
});
it('is invalid when malformed email provided', async () => {
@@ -200,9 +227,10 @@ describe('CustomEmailForm', () => {
{
custom_email: 'user@example.com',
smtp_address: 'smtp.example.com',
+ smtp_username: 'user@example.com',
smtp_password: 'supersecret',
smtp_port: '587',
- smtp_username: 'user@example.com',
+ smtp_authentication: 'login',
},
],
]);
diff --git a/spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js b/spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js
index 8655845d1b7..0eec981b67d 100644
--- a/spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js
+++ b/spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js
@@ -22,6 +22,7 @@ describe('ServiceDeskRoot', () => {
isIssueTrackerEnabled: true,
outgoingName: 'GitLab Support Bot',
projectKey: 'key',
+ addExternalParticipantsFromCc: true,
selectedTemplate: 'Bug',
selectedFileTemplateProjectId: 42,
templates: ['Bug', 'Documentation'],
@@ -62,6 +63,7 @@ describe('ServiceDeskRoot', () => {
incomingEmail: provideData.initialIncomingEmail,
initialOutgoingName: provideData.outgoingName,
initialProjectKey: provideData.projectKey,
+ initialAddExternalParticipantsFromCc: provideData.addExternalParticipantsFromCc,
initialSelectedTemplate: provideData.selectedTemplate,
initialSelectedFileTemplateProjectId: provideData.selectedFileTemplateProjectId,
isEnabled: provideData.initialIsEnabled,
@@ -147,6 +149,7 @@ describe('ServiceDeskRoot', () => {
selectedTemplate: 'Bug',
outgoingName: 'GitLab Support Bot',
projectKey: 'key',
+ addExternalParticipantsFromCc: true,
};
wrapper.findComponent(ServiceDeskSetting).vm.$emit('save', payload);
@@ -160,6 +163,7 @@ describe('ServiceDeskRoot', () => {
outgoing_name: 'GitLab Support Bot',
project_key: 'key',
service_desk_enabled: true,
+ add_external_participants_from_cc: true,
});
});
@@ -178,6 +182,7 @@ describe('ServiceDeskRoot', () => {
selectedTemplate: 'Bug',
outgoingName: 'GitLab Support Bot',
projectKey: 'key',
+ addExternalParticipantsFromCc: true,
};
wrapper.findComponent(ServiceDeskSetting).vm.$emit('save', payload);
diff --git a/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js b/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js
index 260fd200f03..6449f9bb68e 100644
--- a/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js
+++ b/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js
@@ -1,4 +1,4 @@
-import { GlButton, GlDropdown, GlLoadingIcon, GlToggle, GlAlert } from '@gitlab/ui';
+import { GlButton, GlDropdown, GlFormCheckbox, GlLoadingIcon, GlToggle, GlAlert } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
@@ -19,8 +19,9 @@ describe('ServiceDeskSetting', () => {
const findSuffixFormGroup = () => wrapper.findByTestId('suffix-form-group');
const findIssueTrackerInfo = () => wrapper.findComponent(GlAlert);
const findIssueHelpLink = () => wrapper.findByTestId('issue-help-page');
+ const findAddExternalParticipantsFromCcCheckbox = () => wrapper.findComponent(GlFormCheckbox);
- const createComponent = ({ props = {} } = {}) =>
+ const createComponent = ({ props = {}, provide = {} } = {}) =>
extendedWrapper(
mount(ServiceDeskSetting, {
propsData: {
@@ -28,6 +29,12 @@ describe('ServiceDeskSetting', () => {
isIssueTrackerEnabled: true,
...props,
},
+ provide: {
+ glFeatures: {
+ issueEmailParticipants: true,
+ },
+ ...provide,
+ },
}),
);
@@ -205,6 +212,25 @@ describe('ServiceDeskSetting', () => {
});
});
+ describe('add external participants from cc checkbox', () => {
+ it('is rendered', () => {
+ wrapper = createComponent();
+ expect(findAddExternalParticipantsFromCcCheckbox().exists()).toBe(true);
+ });
+
+ it('forwards the initial value to the checkbox', () => {
+ wrapper = createComponent({ props: { initialAddExternalParticipantsFromCc: true } });
+ expect(findAddExternalParticipantsFromCcCheckbox().find('input').element.checked).toBe(true);
+ });
+
+ describe('when feature flag issue_email_participants is disabled', () => {
+ it('is not rendered', () => {
+ wrapper = createComponent({ provide: { glFeatures: { issueEmailParticipants: false } } });
+ expect(findAddExternalParticipantsFromCcCheckbox().exists()).toBe(false);
+ });
+ });
+ });
+
describe('save button', () => {
it('renders a save button to save a template', () => {
wrapper = createComponent();
@@ -223,6 +249,7 @@ describe('ServiceDeskSetting', () => {
initialSelectedFileTemplateProjectId: 42,
initialOutgoingName: 'GitLab Support Bot',
initialProjectKey: 'key',
+ initialAddExternalParticipantsFromCc: false,
},
});
@@ -235,6 +262,7 @@ describe('ServiceDeskSetting', () => {
fileTemplateProjectId: 42,
outgoingName: 'GitLab Support Bot',
projectKey: 'key',
+ addExternalParticipantsFromCc: false,
};
expect(wrapper.emitted('save')[0]).toEqual([payload]);
@@ -260,6 +288,10 @@ describe('ServiceDeskSetting', () => {
expect(findButton().exists()).toBe(false);
});
+ it('does not render add external participants from cc checkbox', () => {
+ expect(findAddExternalParticipantsFromCcCheckbox().exists()).toBe(false);
+ });
+
it('emits an event to turn on Service Desk when the toggle is clicked', async () => {
findToggle().vm.$emit('change', false);
diff --git a/spec/frontend/protected_branches/protected_branch_edit_spec.js b/spec/frontend/protected_branches/protected_branch_edit_spec.js
index 6422856ba22..301b0e8e157 100644
--- a/spec/frontend/protected_branches/protected_branch_edit_spec.js
+++ b/spec/frontend/protected_branches/protected_branch_edit_spec.js
@@ -6,37 +6,96 @@ import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import ProtectedBranchEdit from '~/protected_branches/protected_branch_edit';
+import waitForPromises from 'helpers/wait_for_promises';
jest.mock('~/alert');
const TEST_URL = `${TEST_HOST}/url`;
+
+const response = {
+ project_id: 2,
+ name: 'release/*',
+ id: 30,
+ created_at: '2023-09-21T03:06:27.532Z',
+ updated_at: '2023-10-31T21:37:50.126Z',
+ code_owner_approval_required: false,
+ allow_force_push: false,
+ namespace_id: null,
+ merge_access_levels: [
+ {
+ id: 37,
+ protected_branch_id: 30,
+ access_level: 40,
+ created_at: '2023-10-31T22:44:15.545Z',
+ updated_at: '2023-10-31T22:44:15.545Z',
+ user_id: null,
+ group_id: null,
+ },
+ ],
+ push_access_levels: [
+ {
+ id: 38,
+ protected_branch_id: 30,
+ access_level: 40,
+ created_at: '2023-10-31T22:43:53.105Z',
+ updated_at: '2023-10-31T22:43:53.105Z',
+ user_id: null,
+ group_id: null,
+ deploy_key_id: null,
+ },
+ ],
+};
+
+// Toggles
const FORCE_PUSH_TOGGLE_TESTID = 'force-push-toggle';
const CODE_OWNER_TOGGLE_TESTID = 'code-owner-toggle';
const IS_CHECKED_CLASS = 'is-checked';
const IS_DISABLED_CLASS = 'is-disabled';
const IS_LOADING_SELECTOR = '.toggle-loading';
+// Dropdowns
+const MERGE_DROPDOWN_TESTID = 'protected-branch-allowed-to-merge';
+const PUSH_DROPDOWN_TESTID = 'protected-branch-allowed-to-push';
+const INIT_MERGE_DATA_TESTID = 'js-allowed-to-merge';
+const INIT_PUSH_DATA_TESTID = 'js-allowed-to-push';
+
describe('ProtectedBranchEdit', () => {
let mock;
- beforeEach(() => {
- jest.spyOn(ProtectedBranchEdit.prototype, 'initDropdowns').mockImplementation();
-
- mock = new MockAdapter(axios);
- });
-
const findForcePushToggle = () =>
document.querySelector(`div[data-testid="${FORCE_PUSH_TOGGLE_TESTID}"] button`);
const findCodeOwnerToggle = () =>
document.querySelector(`div[data-testid="${CODE_OWNER_TOGGLE_TESTID}"] button`);
+ const findMergeDropdown = () =>
+ document.querySelector(`div[data-testid="${MERGE_DROPDOWN_TESTID}"]`);
+ const findPushDropdown = () =>
+ document.querySelector(`div[data-testid="${PUSH_DROPDOWN_TESTID}"]`);
const create = ({
forcePushToggleChecked = false,
codeOwnerToggleChecked = false,
+ mergeClass = INIT_MERGE_DATA_TESTID,
+ mergeDisabled = false,
+ mergePreselected = [],
+ pushClass = INIT_PUSH_DATA_TESTID,
+ pushDisabled = false,
+ pushPreselected = [],
hasLicense = true,
} = {}) => {
setHTMLFixture(`<div id="wrap" data-url="${TEST_URL}">
<span
+ class="${mergeClass}"
+ data-label="Dropdown allowed to merge"
+ data-disabled="${mergeDisabled}"
+ data-preselected-items='${mergePreselected}'
+ data-testid="${MERGE_DROPDOWN_TESTID}"></span>
+ <span
+ class="${pushClass}"
+ data-label="Dropdown allowed to push"
+ data-disabled="${pushDisabled}"
+ data-preselected-items='${pushPreselected}'
+ data-testid="${PUSH_DROPDOWN_TESTID}"></span>
+ <span
class="js-force-push-toggle"
data-label="Toggle allowed to force push"
data-is-checked="${forcePushToggleChecked}"
@@ -51,108 +110,261 @@ describe('ProtectedBranchEdit', () => {
return new ProtectedBranchEdit({ $wrap: $('#wrap'), hasLicense });
};
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
afterEach(() => {
mock.restore();
resetHTMLFixture();
});
- describe('when license supports code owner approvals', () => {
+ describe('dropdowns', () => {
+ const accessLevels = [
+ {
+ id: 40,
+ text: 'Maintainers',
+ before_divider: true,
+ },
+ {
+ id: 30,
+ text: 'Developers + Maintainers',
+ before_divider: true,
+ },
+ ];
+
beforeEach(() => {
- create();
- });
+ window.gon = {
+ current_project_id: 1,
+ merge_access_levels: { roles: accessLevels },
+ push_access_levels: { roles: accessLevels },
+ };
- it('instantiates the code owner toggle', () => {
- expect(findCodeOwnerToggle()).not.toBe(null);
+ jest.spyOn(ProtectedBranchEdit.prototype, 'initToggles').mockImplementation();
});
- });
- describe('when license does not support code owner approvals', () => {
- beforeEach(() => {
- create({ hasLicense: false });
- });
+ describe('rendering', () => {
+ describe('merge dropdown', () => {
+ it('builds the merge dropdown when it has the proper class', () => {
+ create();
+ expect(findMergeDropdown()).not.toBe(null);
+ });
- it('does not instantiate the code owner toggle', () => {
- expect(findCodeOwnerToggle()).toBe(null);
- });
- });
+ it('does not build the merge dropdown when it does not have the proper class', () => {
+ create({ mergeClass: 'invalid-class' });
+ expect(findMergeDropdown()).toBe(null);
+ });
+ });
- describe('when toggles are not available in the DOM on page load', () => {
- beforeEach(() => {
- create({ hasLicense: true });
- setHTMLFixture('');
- });
+ describe('push dropdown', () => {
+ it('builds the push dropdown when it has the proper class', () => {
+ create();
+ expect(findPushDropdown()).not.toBe(null);
+ });
- afterEach(() => {
- resetHTMLFixture();
+ it('does not build the push dropdown when it does not have the proper class', () => {
+ create({ pushClass: 'invalid-class' });
+ expect(findPushDropdown()).toBe(null);
+ });
+ });
});
- it('does not instantiate the force push toggle', () => {
- expect(findForcePushToggle()).toBe(null);
+ describe('preselected item', () => {
+ beforeEach(() => {
+ mock.onPatch(TEST_URL).reply(HTTP_STATUS_OK, response);
+ });
+
+ it('sets selected item on load', () => {
+ const preselected = [{ id: 38, access_level: 40, type: 'role' }];
+ const ProtectedBranchEditInstance = create({
+ pushPreselected: JSON.stringify(preselected),
+ });
+ const dropdown = ProtectedBranchEditInstance.push_access_levels_dropdown;
+ expect(dropdown.preselected).toEqual(preselected);
+ });
+
+ it('updates selected item on save for enabled dropdowns', async () => {
+ const selectedValue = [{ access_level: 40 }];
+ const ProtectedBranchEditInstance = create({});
+ const dropdown = ProtectedBranchEditInstance.push_access_levels_dropdown;
+ dropdown.$emit('select', selectedValue);
+ dropdown.$emit('hidden');
+ await waitForPromises();
+ expect(dropdown.preselected[0].id).toBe(response.push_access_levels[0].id);
+ });
+
+ it('does not update selected item on save for disabled dropdowns', async () => {
+ const selectedValue = [{ access_level: 40 }];
+ const ProtectedBranchEditInstance = create({ pushDisabled: '' });
+ const dropdown = ProtectedBranchEditInstance.push_access_levels_dropdown;
+ dropdown.$emit('select', selectedValue);
+ dropdown.$emit('hidden');
+ await waitForPromises();
+ expect(dropdown.preselected).toEqual([]);
+ });
});
- it('does not instantiate the code owner toggle', () => {
- expect(findCodeOwnerToggle()).toBe(null);
+ describe('on hidden', () => {
+ beforeEach(() => {
+ mock.onPatch(TEST_URL).reply(HTTP_STATUS_OK, {});
+ });
+
+ it('does not update permissions on hidden if there are no changes', () => {
+ const ProtectedBranchEditInstance = create();
+ const dropdown = ProtectedBranchEditInstance.merge_access_levels_dropdown;
+ dropdown.$emit('hidden');
+ expect(mock.history.patch).toHaveLength(0);
+ });
+
+ it('updates permissions on hidden for enabled dropdowns with changes', async () => {
+ const preselectedData = { id: 38, access_level: 40 };
+ const preselected = [{ ...preselectedData, type: 'role' }];
+ const selectedValue = [{ access_level: 30 }];
+ const ProtectedBranchEditInstance = create({
+ pushPreselected: JSON.stringify(preselected),
+ });
+ const dropdown = ProtectedBranchEditInstance.merge_access_levels_dropdown;
+ dropdown.$emit('select', selectedValue);
+ dropdown.$emit('hidden');
+ await waitForPromises();
+ expect(mock.history.patch).toHaveLength(1);
+ expect(mock.history.patch[0].data).toEqual(
+ JSON.stringify({
+ protected_branch: {
+ merge_access_levels_attributes: selectedValue,
+ push_access_levels_attributes: [preselectedData],
+ },
+ }),
+ );
+ });
+
+ it('does not update permissions on hidden for disabled dropdowns', async () => {
+ const preselected = [{ id: 38, access_level: 0, type: 'role' }];
+ const selectedValue = [{ access_level: 30 }];
+ const ProtectedBranchEditInstance = create({
+ mergeDisabled: '',
+ mergePreselected: JSON.stringify(preselected),
+ });
+ const dropdown = ProtectedBranchEditInstance.push_access_levels_dropdown;
+ dropdown.$emit('select', selectedValue);
+ dropdown.$emit('hidden');
+ await waitForPromises();
+ expect(mock.history.patch).toHaveLength(1);
+ expect(mock.history.patch[0].data).toEqual(
+ JSON.stringify({
+ protected_branch: {
+ merge_access_levels_attributes: [],
+ push_access_levels_attributes: selectedValue,
+ },
+ }),
+ );
+ });
});
});
- describe.each`
- description | checkedOption | patchParam | finder
- ${'force push'} | ${'forcePushToggleChecked'} | ${'allow_force_push'} | ${findForcePushToggle}
- ${'code owner'} | ${'codeOwnerToggleChecked'} | ${'code_owner_approval_required'} | ${findCodeOwnerToggle}
- `('when unchecked $description toggle button', ({ checkedOption, patchParam, finder }) => {
- let toggle;
-
+ describe('toggles', () => {
beforeEach(() => {
- create({ [checkedOption]: false });
+ jest.spyOn(ProtectedBranchEdit.prototype, 'initDropdowns').mockImplementation();
+ });
- toggle = finder();
+ describe('when license supports code owner approvals', () => {
+ beforeEach(() => {
+ create();
+ });
+
+ it('instantiates the code owner toggle', () => {
+ expect(findCodeOwnerToggle()).not.toBe(null);
+ });
});
- it('is not changed', () => {
- expect(toggle).not.toHaveClass(IS_CHECKED_CLASS);
- expect(toggle.querySelector(IS_LOADING_SELECTOR)).toBe(null);
- expect(toggle).not.toHaveClass(IS_DISABLED_CLASS);
+ describe('when license does not support code owner approvals', () => {
+ beforeEach(() => {
+ create({ hasLicense: false });
+ });
+
+ it('does not instantiate the code owner toggle', () => {
+ expect(findCodeOwnerToggle()).toBe(null);
+ });
});
- describe('when clicked', () => {
+ describe('when toggles are not available in the DOM on page load', () => {
beforeEach(() => {
- mock
- .onPatch(TEST_URL, { protected_branch: { [patchParam]: true } })
- .replyOnce(HTTP_STATUS_OK, {});
+ create({ hasLicense: true });
+ setHTMLFixture('');
});
- it('checks and disables button', async () => {
- await toggle.click();
+ afterEach(() => {
+ resetHTMLFixture();
+ });
- expect(toggle).toHaveClass(IS_CHECKED_CLASS);
- expect(toggle.querySelector(IS_LOADING_SELECTOR)).not.toBe(null);
- expect(toggle).toHaveClass(IS_DISABLED_CLASS);
+ it('does not instantiate the force push toggle', () => {
+ expect(findForcePushToggle()).toBe(null);
});
- it('sends update to BE', async () => {
- await toggle.click();
+ it('does not instantiate the code owner toggle', () => {
+ expect(findCodeOwnerToggle()).toBe(null);
+ });
+ });
- await axios.waitForAll();
+ describe.each`
+ description | checkedOption | patchParam | finder
+ ${'force push'} | ${'forcePushToggleChecked'} | ${'allow_force_push'} | ${findForcePushToggle}
+ ${'code owner'} | ${'codeOwnerToggleChecked'} | ${'code_owner_approval_required'} | ${findCodeOwnerToggle}
+ `('when unchecked $description toggle button', ({ checkedOption, patchParam, finder }) => {
+ let toggle;
- // Args are asserted in the `.onPatch` call
- expect(mock.history.patch).toHaveLength(1);
+ beforeEach(() => {
+ create({ [checkedOption]: false });
- expect(toggle).not.toHaveClass(IS_DISABLED_CLASS);
+ toggle = finder();
+ });
+
+ it('is not changed', () => {
+ expect(toggle).not.toHaveClass(IS_CHECKED_CLASS);
expect(toggle.querySelector(IS_LOADING_SELECTOR)).toBe(null);
- expect(createAlert).not.toHaveBeenCalled();
+ expect(toggle).not.toHaveClass(IS_DISABLED_CLASS);
});
- });
- describe('when clicked and BE error', () => {
- beforeEach(() => {
- mock.onPatch(TEST_URL).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
- toggle.click();
+ describe('when clicked', () => {
+ beforeEach(() => {
+ mock
+ .onPatch(TEST_URL, { protected_branch: { [patchParam]: true } })
+ .replyOnce(HTTP_STATUS_OK, {});
+ });
+
+ it('checks and disables button', async () => {
+ await toggle.click();
+
+ expect(toggle).toHaveClass(IS_CHECKED_CLASS);
+ expect(toggle.querySelector(IS_LOADING_SELECTOR)).not.toBe(null);
+ expect(toggle).toHaveClass(IS_DISABLED_CLASS);
+ });
+
+ it('sends update to BE', async () => {
+ await toggle.click();
+
+ await axios.waitForAll();
+
+ // Args are asserted in the `.onPatch` call
+ expect(mock.history.patch).toHaveLength(1);
+
+ expect(toggle).not.toHaveClass(IS_DISABLED_CLASS);
+ expect(toggle.querySelector(IS_LOADING_SELECTOR)).toBe(null);
+ expect(createAlert).not.toHaveBeenCalled();
+ });
});
- it('alerts error', async () => {
- await axios.waitForAll();
+ describe('when clicked and BE error', () => {
+ beforeEach(() => {
+ mock.onPatch(TEST_URL).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
+ toggle.click();
+ });
+
+ it('alerts error', async () => {
+ await axios.waitForAll();
- expect(createAlert).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalled();
+ });
});
});
});
diff --git a/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap b/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap
index 1a5301c5525..99cfc4f418f 100644
--- a/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap
+++ b/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap
@@ -8,14 +8,12 @@ exports[`Repository last commit component renders commit widget 1`] = `
class="commit-actions gl-align-items-center gl-display-flex gl-flex-align gl-flex-direction-row"
>
<div
- class="ci-status-link"
+ class="gl-ml-5"
>
- <ci-badge-link-stub
+ <ci-icon-stub
aria-label="Pipeline: failed"
class="js-commit-pipeline"
- details-path="https://test.com/pipeline"
showtooltip="true"
- size="md"
status="[object Object]"
uselink="true"
/>
diff --git a/spec/frontend/repository/components/blob_content_viewer_spec.js b/spec/frontend/repository/components/blob_content_viewer_spec.js
index cc077e20e0b..e0d2984893b 100644
--- a/spec/frontend/repository/components/blob_content_viewer_spec.js
+++ b/spec/frontend/repository/components/blob_content_viewer_spec.js
@@ -18,6 +18,7 @@ import { loadViewer } from '~/repository/components/blob_viewers';
import DownloadViewer from '~/repository/components/blob_viewers/download_viewer.vue';
import EmptyViewer from '~/repository/components/blob_viewers/empty_viewer.vue';
import SourceViewer from '~/vue_shared/components/source_viewer/source_viewer.vue';
+import SourceViewerNew from '~/vue_shared/components/source_viewer/source_viewer_new.vue';
import blobInfoQuery from 'shared_queries/repository/blob_info.query.graphql';
import projectInfoQuery from '~/repository/queries/project_info.query.graphql';
import CodeIntelligence from '~/code_navigation/components/app.vue';
@@ -137,6 +138,7 @@ const createComponent = async (mockData = {}, mountFn = shallowMount, mockRoute
...inject,
glFeatures: {
highlightJsWorker: false,
+ blobBlameInfo: true,
},
},
}),
@@ -157,6 +159,7 @@ describe('Blob content viewer component', () => {
const findForkSuggestion = () => wrapper.findComponent(ForkSuggestion);
const findCodeIntelligence = () => wrapper.findComponent(CodeIntelligence);
const findSourceViewer = () => wrapper.findComponent(SourceViewer);
+ const findSourceViewerNew = () => wrapper.findComponent(SourceViewerNew);
beforeEach(() => {
jest.spyOn(window, 'requestIdleCallback').mockImplementation(execImmediately);
@@ -179,14 +182,43 @@ describe('Blob content viewer component', () => {
expect(findBlobHeader().props('activeViewerType')).toEqual(SIMPLE_BLOB_VIEWER);
expect(findBlobHeader().props('hasRenderError')).toEqual(false);
- expect(findBlobHeader().props('hideViewerSwitcher')).toEqual(true);
+ expect(findBlobHeader().props('hideViewerSwitcher')).toEqual(false);
expect(findBlobHeader().props('blob')).toEqual(simpleViewerMock);
expect(findBlobHeader().props('showForkSuggestion')).toEqual(false);
+ expect(findBlobHeader().props('showBlameToggle')).toEqual(false);
expect(findBlobHeader().props('projectPath')).toEqual(propsMock.projectPath);
expect(findBlobHeader().props('projectId')).toEqual(projectMock.id);
expect(mockRouterPush).not.toHaveBeenCalled();
});
+ describe('blame toggle', () => {
+ const triggerBlame = async () => {
+ findBlobHeader().vm.$emit('blame');
+ await nextTick();
+ };
+
+ it('renders a blame toggle for JSON files', async () => {
+ await createComponent({ blob: { ...simpleViewerMock, language: 'json' } });
+
+ expect(findBlobHeader().props('showBlameToggle')).toEqual(true);
+ });
+
+ it('adds blame param to the URL and passes `showBlame` to the SourceViewer', async () => {
+ loadViewer.mockReturnValueOnce(SourceViewerNew);
+ await createComponent({ blob: { ...simpleViewerMock, language: 'json' } });
+
+ await triggerBlame();
+
+ expect(mockRouterPush).toHaveBeenCalledWith({ query: { blame: '1' } });
+ expect(findSourceViewerNew().props('showBlame')).toBe(true);
+
+ await triggerBlame();
+
+ expect(mockRouterPush).toHaveBeenCalledWith({ query: { blame: '0' } });
+ expect(findSourceViewerNew().props('showBlame')).toBe(false);
+ });
+ });
+
it('creates an alert when the BlobHeader component emits an error', async () => {
await createComponent();
diff --git a/spec/frontend/repository/components/commit_info_spec.js b/spec/frontend/repository/components/commit_info_spec.js
index 34e941aa858..4e570346d97 100644
--- a/spec/frontend/repository/components/commit_info_spec.js
+++ b/spec/frontend/repository/components/commit_info_spec.js
@@ -21,9 +21,9 @@ const findAuthorName = () => wrapper.findByText(`${commit.authorName} authored`)
const findCommitRowDescription = () => wrapper.find('pre');
const findTitleHtml = () => wrapper.findByText(commit.titleHtml);
-const createComponent = async ({ commitMock = {} } = {}) => {
+const createComponent = async ({ commitMock = {}, prevBlameLink } = {}) => {
wrapper = shallowMountExtended(CommitInfo, {
- propsData: { commit: { ...commit, ...commitMock } },
+ propsData: { commit: { ...commit, ...commitMock }, prevBlameLink },
});
await nextTick();
@@ -35,6 +35,7 @@ describe('Repository last commit component', () => {
expect(findUserLink().exists()).toBe(true);
expect(findUserAvatarLink().exists()).toBe(true);
+ expect(findUserAvatarLink().props('imgAlt')).toBe("Test authorName's avatar");
});
it('hides author component when author does not exist', () => {
@@ -79,6 +80,22 @@ describe('Repository last commit component', () => {
});
});
+ describe('previous blame link', () => {
+ const prevBlameLink = '<a>Previous blame link</a>';
+
+ it('renders a previous blame link when it is present', () => {
+ createComponent({ prevBlameLink });
+
+ expect(wrapper.html()).toContain(prevBlameLink);
+ });
+
+ it('does not render a previous blame link when it is not present', () => {
+ createComponent({ prevBlameLink: null });
+
+ expect(wrapper.html()).not.toContain(prevBlameLink);
+ });
+ });
+
it('sets correct CSS class if the commit message is empty', () => {
createComponent({ commitMock: { message: '' } });
diff --git a/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap b/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap
index af7eca6a52d..e14f41e2ed2 100644
--- a/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap
+++ b/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap
@@ -34,15 +34,15 @@ exports[`Repository table row component renders a symlink table row 1`] = `
/>
</td>
<td
- class="cursor-default d-none d-sm-table-cell gl-text-secondary tree-commit"
+ class="cursor-default d-none d-sm-table-cell tree-commit"
>
<gl-link-stub
- class="gl-text-secondary str-truncated-100 tree-commit-link"
+ class="gl-text-gray-600 str-truncated-100 tree-commit-link"
/>
<gl-intersection-observer-stub />
</td>
<td
- class="cursor-default gl-text-secondary text-right tree-time-ago"
+ class="cursor-default gl-text-gray-600 text-right tree-time-ago"
>
<gl-intersection-observer-stub>
<timeago-tooltip-stub
@@ -90,15 +90,15 @@ exports[`Repository table row component renders table row 1`] = `
/>
</td>
<td
- class="cursor-default d-none d-sm-table-cell gl-text-secondary tree-commit"
+ class="cursor-default d-none d-sm-table-cell tree-commit"
>
<gl-link-stub
- class="gl-text-secondary str-truncated-100 tree-commit-link"
+ class="gl-text-gray-600 str-truncated-100 tree-commit-link"
/>
<gl-intersection-observer-stub />
</td>
<td
- class="cursor-default gl-text-secondary text-right tree-time-ago"
+ class="cursor-default gl-text-gray-600 text-right tree-time-ago"
>
<gl-intersection-observer-stub>
<timeago-tooltip-stub
@@ -146,15 +146,15 @@ exports[`Repository table row component renders table row for path with special
/>
</td>
<td
- class="cursor-default d-none d-sm-table-cell gl-text-secondary tree-commit"
+ class="cursor-default d-none d-sm-table-cell tree-commit"
>
<gl-link-stub
- class="gl-text-secondary str-truncated-100 tree-commit-link"
+ class="gl-text-gray-600 str-truncated-100 tree-commit-link"
/>
<gl-intersection-observer-stub />
</td>
<td
- class="cursor-default gl-text-secondary text-right tree-time-ago"
+ class="cursor-default gl-text-gray-600 text-right tree-time-ago"
>
<gl-intersection-observer-stub>
<timeago-tooltip-stub
diff --git a/spec/frontend/search/sidebar/components/app_spec.js b/spec/frontend/search/sidebar/components/app_spec.js
index d8d2492209e..c2d88493d71 100644
--- a/spec/frontend/search/sidebar/components/app_spec.js
+++ b/spec/frontend/search/sidebar/components/app_spec.js
@@ -17,6 +17,7 @@ import ProjectsFilters from '~/search/sidebar/components/projects_filters.vue';
import NotesFilters from '~/search/sidebar/components/notes_filters.vue';
import CommitsFilters from '~/search/sidebar/components/commits_filters.vue';
import MilestonesFilters from '~/search/sidebar/components/milestones_filters.vue';
+import WikiBlobsFilters from '~/search/sidebar/components/wiki_blobs_filters.vue';
import ScopeLegacyNavigation from '~/search/sidebar/components/scope_legacy_navigation.vue';
import SmallScreenDrawerNavigation from '~/search/sidebar/components/small_screen_drawer_navigation.vue';
import ScopeSidebarNavigation from '~/search/sidebar/components/scope_sidebar_navigation.vue';
@@ -46,9 +47,7 @@ describe('GlobalSearchSidebar', () => {
store,
provide: {
glFeatures: {
- searchNotesHideArchivedProjects: true,
- searchCommitsHideArchivedProjects: true,
- searchMilestonesHideArchivedProjects: true,
+ searchProjectWikisHideArchivedProjects: true,
},
},
});
@@ -62,6 +61,7 @@ describe('GlobalSearchSidebar', () => {
const findNotesFilters = () => wrapper.findComponent(NotesFilters);
const findCommitsFilters = () => wrapper.findComponent(CommitsFilters);
const findMilestonesFilters = () => wrapper.findComponent(MilestonesFilters);
+ const findWikiBlobsFilters = () => wrapper.findComponent(WikiBlobsFilters);
const findScopeLegacyNavigation = () => wrapper.findComponent(ScopeLegacyNavigation);
const findSmallScreenDrawerNavigation = () => wrapper.findComponent(SmallScreenDrawerNavigation);
const findScopeSidebarNavigation = () => wrapper.findComponent(ScopeSidebarNavigation);
@@ -92,6 +92,8 @@ describe('GlobalSearchSidebar', () => {
${'commits'} | ${findCommitsFilters} | ${SEARCH_TYPE_ADVANCED} | ${true}
${'milestones'} | ${findMilestonesFilters} | ${SEARCH_TYPE_BASIC} | ${true}
${'milestones'} | ${findMilestonesFilters} | ${SEARCH_TYPE_ADVANCED} | ${true}
+ ${'wiki_blobs'} | ${findWikiBlobsFilters} | ${SEARCH_TYPE_BASIC} | ${true}
+ ${'wiki_blobs'} | ${findWikiBlobsFilters} | ${SEARCH_TYPE_ADVANCED} | ${true}
`('with sidebar $scope scope:', ({ scope, filter, searchType, isShown }) => {
beforeEach(() => {
getterSpies.currentScope = jest.fn(() => scope);
diff --git a/spec/frontend/search/sidebar/components/blobs_filters_spec.js b/spec/frontend/search/sidebar/components/blobs_filters_spec.js
index 729fae44c19..245ddb8f8bb 100644
--- a/spec/frontend/search/sidebar/components/blobs_filters_spec.js
+++ b/spec/frontend/search/sidebar/components/blobs_filters_spec.js
@@ -17,7 +17,7 @@ describe('GlobalSearch BlobsFilters', () => {
currentScope: () => 'blobs',
};
- const createComponent = ({ initialState = {}, searchBlobsHideArchivedProjects = true } = {}) => {
+ const createComponent = ({ initialState = {} } = {}) => {
const store = new Vuex.Store({
state: {
urlQuery: MOCK_QUERY,
@@ -30,11 +30,6 @@ describe('GlobalSearch BlobsFilters', () => {
wrapper = shallowMount(BlobsFilters, {
store,
- provide: {
- glFeatures: {
- searchBlobsHideArchivedProjects,
- },
- },
});
};
@@ -42,29 +37,20 @@ describe('GlobalSearch BlobsFilters', () => {
const findArchivedFilter = () => wrapper.findComponent(ArchivedFilter);
const findDividers = () => wrapper.findAll('hr');
- describe.each`
- description | searchBlobsHideArchivedProjects
- ${'Renders correctly with Archived Filter enabled'} | ${true}
- ${'Renders correctly with Archived Filter disabled'} | ${false}
- `('$description', ({ searchBlobsHideArchivedProjects }) => {
- beforeEach(() => {
- createComponent({
- searchBlobsHideArchivedProjects,
- });
- });
+ beforeEach(() => {
+ createComponent({});
+ });
- it('renders LanguageFilter', () => {
- expect(findLanguageFilter().exists()).toBe(true);
- });
+ it('renders LanguageFilter', () => {
+ expect(findLanguageFilter().exists()).toBe(true);
+ });
- it(`renders correctly ArchivedFilter when searchBlobsHideArchivedProjects is ${searchBlobsHideArchivedProjects}`, () => {
- expect(findArchivedFilter().exists()).toBe(searchBlobsHideArchivedProjects);
- });
+ it('renders ArchivedFilter', () => {
+ expect(findArchivedFilter().exists()).toBe(true);
+ });
- it('renders divider correctly', () => {
- const dividersCount = searchBlobsHideArchivedProjects ? 1 : 0;
- expect(findDividers()).toHaveLength(dividersCount);
- });
+ it('renders divider correctly', () => {
+ expect(findDividers()).toHaveLength(1);
});
describe('Renders correctly in new nav', () => {
@@ -74,7 +60,6 @@ describe('GlobalSearch BlobsFilters', () => {
searchType: SEARCH_TYPE_ADVANCED,
useSidebarNavigation: true,
},
- searchBlobsHideArchivedProjects: true,
});
});
diff --git a/spec/frontend/search/sidebar/components/issues_filters_spec.js b/spec/frontend/search/sidebar/components/issues_filters_spec.js
index c3b3a93e362..860c5c147a6 100644
--- a/spec/frontend/search/sidebar/components/issues_filters_spec.js
+++ b/spec/frontend/search/sidebar/components/issues_filters_spec.js
@@ -19,11 +19,7 @@ describe('GlobalSearch IssuesFilters', () => {
currentScope: () => 'issues',
};
- const createComponent = ({
- initialState = {},
- searchIssueLabelAggregation = true,
- searchIssuesHideArchivedProjects = true,
- } = {}) => {
+ const createComponent = ({ initialState = {}, searchIssueLabelAggregation = true } = {}) => {
const store = new Vuex.Store({
state: {
urlQuery: MOCK_QUERY,
@@ -39,7 +35,6 @@ describe('GlobalSearch IssuesFilters', () => {
provide: {
glFeatures: {
searchIssueLabelAggregation,
- searchIssuesHideArchivedProjects,
},
},
});
@@ -52,16 +47,13 @@ describe('GlobalSearch IssuesFilters', () => {
const findDividers = () => wrapper.findAll('hr');
describe.each`
- description | searchIssueLabelAggregation | searchIssuesHideArchivedProjects
- ${'Renders correctly with Label Filter disabled'} | ${false} | ${true}
- ${'Renders correctly with Archived Filter disabled'} | ${true} | ${false}
- ${'Renders correctly with Archived Filter and Label Filter disabled'} | ${false} | ${false}
- ${'Renders correctly with Archived Filter and Label Filter enabled'} | ${true} | ${true}
- `('$description', ({ searchIssueLabelAggregation, searchIssuesHideArchivedProjects }) => {
+ description | searchIssueLabelAggregation
+ ${'Renders correctly with Label Filter disabled'} | ${false}
+ ${'Renders correctly with Label Filter enabled'} | ${true}
+ `('$description', ({ searchIssueLabelAggregation }) => {
beforeEach(() => {
createComponent({
searchIssueLabelAggregation,
- searchIssuesHideArchivedProjects,
});
});
@@ -73,23 +65,20 @@ describe('GlobalSearch IssuesFilters', () => {
expect(findConfidentialityFilter().exists()).toBe(true);
});
- it(`renders correctly LabelFilter when searchIssueLabelAggregation is ${searchIssueLabelAggregation}`, () => {
- expect(findLabelFilter().exists()).toBe(searchIssueLabelAggregation);
+ it('renders correctly ArchivedFilter', () => {
+ expect(findArchivedFilter().exists()).toBe(true);
});
- it(`renders correctly ArchivedFilter when searchIssuesHideArchivedProjects is ${searchIssuesHideArchivedProjects}`, () => {
- expect(findArchivedFilter().exists()).toBe(searchIssuesHideArchivedProjects);
+ it(`renders correctly LabelFilter when searchIssueLabelAggregation is ${searchIssueLabelAggregation}`, () => {
+ expect(findLabelFilter().exists()).toBe(searchIssueLabelAggregation);
});
it('renders divider correctly', () => {
- // one divider can't be disabled
- let dividersCount = 1;
+ // two dividers can't be disabled
+ let dividersCount = 2;
if (searchIssueLabelAggregation) {
dividersCount += 1;
}
- if (searchIssuesHideArchivedProjects) {
- dividersCount += 1;
- }
expect(findDividers()).toHaveLength(dividersCount);
});
});
@@ -127,7 +116,6 @@ describe('GlobalSearch IssuesFilters', () => {
useSidebarNavigation: true,
},
searchIssueLabelAggregation: true,
- searchIssuesHideArchivedProjects: true,
});
});
it('renders StatusFilter', () => {
diff --git a/spec/frontend/search/sidebar/components/label_filter_spec.js b/spec/frontend/search/sidebar/components/label_filter_spec.js
index 07b2e176610..9d2a0c5e739 100644
--- a/spec/frontend/search/sidebar/components/label_filter_spec.js
+++ b/spec/frontend/search/sidebar/components/label_filter_spec.js
@@ -13,7 +13,11 @@ import Vue from 'vue';
import Vuex from 'vuex';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
-import { MOCK_QUERY, MOCK_LABEL_AGGREGATIONS } from 'jest/search/mock_data';
+import {
+ MOCK_QUERY,
+ MOCK_LABEL_AGGREGATIONS,
+ MOCK_FILTERED_UNSELECTED_LABELS,
+} from 'jest/search/mock_data';
import LabelFilter from '~/search/sidebar/components/label_filter/index.vue';
import LabelDropdownItems from '~/search/sidebar/components/label_filter/label_dropdown_items.vue';
@@ -52,8 +56,15 @@ describe('GlobalSearchSidebarLabelFilter', () => {
let trackingSpy;
let config;
let store;
+ let state;
+
+ const createComponent = (initialState, gettersStubs) => {
+ state = createState({
+ query: MOCK_QUERY,
+ aggregations: MOCK_LABEL_AGGREGATIONS,
+ ...initialState,
+ });
- const createComponent = (initialState) => {
config = {
actions: {
...actions,
@@ -62,13 +73,12 @@ describe('GlobalSearchSidebarLabelFilter', () => {
setLabelFilterSearch: actionSpies.setLabelFilterSearch,
setQuery: actionSpies.setQuery,
},
- getters,
+ state,
+ getters: {
+ ...getters,
+ ...gettersStubs,
+ },
mutations,
- state: createState({
- query: MOCK_QUERY,
- aggregations: MOCK_LABEL_AGGREGATIONS,
- ...initialState,
- }),
};
store = new Vuex.Store(config);
@@ -95,6 +105,10 @@ describe('GlobalSearchSidebarLabelFilter', () => {
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findNoLabelsFoundMessage = () => wrapper.findComponentByTestId('no-labels-found-message');
+ const findLabelPills = () => wrapper.findAllComponentsByTestId('label');
+ const findSelectedUappliedLavelPills = () => wrapper.findAllComponentsByTestId('unapplied-label');
+ const findClosedUnappliedPills = () => wrapper.findAllComponentsByTestId('unselected-label');
+
describe('Renders correctly closed', () => {
beforeEach(async () => {
createComponent();
@@ -349,5 +363,42 @@ describe('GlobalSearchSidebarLabelFilter', () => {
});
});
});
+
+ describe('newly selected and unapplied labels show as pills above dropdown', () => {
+ beforeEach(() => {
+ const mockGetters = { unappliedNewLabels: jest.fn(() => MOCK_FILTERED_UNSELECTED_LABELS) };
+ createComponent({}, mockGetters);
+ });
+
+ it('has correct pills', () => {
+ expect(findSelectedUappliedLavelPills()).toHaveLength(2);
+ });
+ });
+
+ describe('applied labels show as pills above dropdown', () => {
+ beforeEach(() => {
+ const mockGetters = {
+ appliedSelectedLabels: jest.fn(() => MOCK_FILTERED_UNSELECTED_LABELS),
+ };
+ createComponent({}, mockGetters);
+ });
+
+ it('has correct pills', () => {
+ expect(findLabelPills()).toHaveLength(2);
+ });
+ });
+
+ describe('closed unapplied labels show as pills above dropdown', () => {
+ beforeEach(() => {
+ const mockGetters = {
+ unselectedLabels: jest.fn(() => MOCK_FILTERED_UNSELECTED_LABELS),
+ };
+ createComponent({}, mockGetters);
+ });
+
+ it('has correct pills', () => {
+ expect(findClosedUnappliedPills()).toHaveLength(2);
+ });
+ });
});
});
diff --git a/spec/frontend/search/sidebar/components/merge_requests_filters_spec.js b/spec/frontend/search/sidebar/components/merge_requests_filters_spec.js
index 278249c2660..b02228a418f 100644
--- a/spec/frontend/search/sidebar/components/merge_requests_filters_spec.js
+++ b/spec/frontend/search/sidebar/components/merge_requests_filters_spec.js
@@ -17,10 +17,7 @@ describe('GlobalSearch MergeRequestsFilters', () => {
currentScope: () => 'merge_requests',
};
- const createComponent = ({
- initialState = {},
- searchMergeRequestsHideArchivedProjects = true,
- } = {}) => {
+ const createComponent = (initialState = {}) => {
const store = new Vuex.Store({
state: {
urlQuery: MOCK_QUERY,
@@ -33,11 +30,6 @@ describe('GlobalSearch MergeRequestsFilters', () => {
wrapper = shallowMount(MergeRequestsFilters, {
store,
- provide: {
- glFeatures: {
- searchMergeRequestsHideArchivedProjects,
- },
- },
});
};
@@ -45,34 +37,23 @@ describe('GlobalSearch MergeRequestsFilters', () => {
const findArchivedFilter = () => wrapper.findComponent(ArchivedFilter);
const findDividers = () => wrapper.findAll('hr');
- describe.each`
- description | searchMergeRequestsHideArchivedProjects
- ${'Renders correctly with Archived Filter disabled'} | ${false}
- ${'Renders correctly with Archived Filter enabled'} | ${true}
- `('$description', ({ searchMergeRequestsHideArchivedProjects }) => {
+ describe('Renders correctly with Archived Filter', () => {
beforeEach(() => {
- createComponent({
- searchMergeRequestsHideArchivedProjects,
- });
+ createComponent();
});
it('renders StatusFilter', () => {
expect(findStatusFilter().exists()).toBe(true);
});
- it(`renders correctly ArchivedFilter when searchMergeRequestsHideArchivedProjects is ${searchMergeRequestsHideArchivedProjects}`, () => {
- expect(findArchivedFilter().exists()).toBe(searchMergeRequestsHideArchivedProjects);
- });
-
it('renders divider correctly', () => {
- const dividersCount = searchMergeRequestsHideArchivedProjects ? 1 : 0;
- expect(findDividers()).toHaveLength(dividersCount);
+ expect(findDividers()).toHaveLength(1);
});
});
describe('Renders correctly with basic search', () => {
beforeEach(() => {
- createComponent({ initialState: { searchType: SEARCH_TYPE_BASIC } });
+ createComponent({ searchType: SEARCH_TYPE_BASIC });
});
it('renders StatusFilter', () => {
@@ -91,11 +72,8 @@ describe('GlobalSearch MergeRequestsFilters', () => {
describe('Renders correctly in new nav', () => {
beforeEach(() => {
createComponent({
- initialState: {
- searchType: SEARCH_TYPE_ADVANCED,
- useSidebarNavigation: true,
- },
- searchMergeRequestsHideArchivedProjects: true,
+ searchType: SEARCH_TYPE_ADVANCED,
+ useSidebarNavigation: true,
});
});
it('renders StatusFilter', () => {
diff --git a/spec/frontend/search/store/getters_spec.js b/spec/frontend/search/store/getters_spec.js
index 571525bd025..8e988ce5c4a 100644
--- a/spec/frontend/search/store/getters_spec.js
+++ b/spec/frontend/search/store/getters_spec.js
@@ -134,4 +134,23 @@ describe('Global Search Store Getters', () => {
]);
});
});
+
+ describe('unselectedLabels', () => {
+ it('returns all labels that are not selected', () => {
+ state.query.labels = ['60'];
+ expect(getters.unselectedLabels(state)).toStrictEqual([MOCK_LABEL_SEARCH_RESULT]);
+ });
+ });
+
+ describe('unappliedNewLabels', () => {
+ it('returns all labels that are selected but not applied', () => {
+ // Applied labels
+ state.urlQuery.labels = ['37', '60'];
+ // Applied and selected labels
+ state.query.labels = ['37', '6', '73', '60'];
+ // Selected but unapplied labels
+ // expect(getters.unappliedNewLabels(state)).toStrictEqual(MOCK_FILTERED_UNSELECTED_LABELS);
+ expect(getters.unappliedNewLabels(state).map(({ key }) => key)).toStrictEqual(['6', '73']);
+ });
+ });
});
diff --git a/spec/frontend/security_configuration/components/training_provider_list_spec.js b/spec/frontend/security_configuration/components/training_provider_list_spec.js
index 2982cef7c74..5b2b3f46df6 100644
--- a/spec/frontend/security_configuration/components/training_provider_list_spec.js
+++ b/spec/frontend/security_configuration/components/training_provider_list_spec.js
@@ -1,4 +1,3 @@
-import * as Sentry from '@sentry/browser';
import {
GlAlert,
GlLink,
@@ -10,6 +9,7 @@ import {
} from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
diff --git a/spec/frontend/sentry/init_sentry_spec.js b/spec/frontend/sentry/init_sentry_spec.js
index fb0dba35759..118a48cc1de 100644
--- a/spec/frontend/sentry/init_sentry_spec.js
+++ b/spec/frontend/sentry/init_sentry_spec.js
@@ -87,7 +87,7 @@ describe('SentryConfig', () => {
expect(mockBrowserClient).toHaveBeenCalledWith(
expect.objectContaining({
dsn: mockDsn,
- release: mockVersion,
+ release: mockRevision,
allowUrls: [mockGitlabUrl, 'webpack-internal://'],
environment: mockEnvironment,
tracesSampleRate: mockSentryClientsideTracesSampleRate,
@@ -115,7 +115,7 @@ describe('SentryConfig', () => {
expect(mockSetTags).toHaveBeenCalledTimes(1);
expect(mockSetTags).toHaveBeenCalledWith({
page: mockPage,
- revision: mockRevision,
+ version: mockVersion,
feature_category: mockFeatureCategory,
});
});
diff --git a/spec/frontend/sentry/sentry_browser_wrapper_spec.js b/spec/frontend/sentry/sentry_browser_wrapper_spec.js
index d98286e1371..60c441fe83c 100644
--- a/spec/frontend/sentry/sentry_browser_wrapper_spec.js
+++ b/spec/frontend/sentry/sentry_browser_wrapper_spec.js
@@ -1,18 +1,39 @@
+/* eslint-disable no-console */
+
import * as Sentry from '~/sentry/sentry_browser_wrapper';
const mockError = new Error('error!');
describe('SentryBrowserWrapper', () => {
+ beforeAll(() => {
+ process.env.NODE_ENV = 'development';
+ });
+
+ afterAll(() => {
+ process.env.NODE_ENV = 'test';
+ });
+
+ beforeEach(() => {
+ jest.spyOn(console, 'error').mockImplementation();
+ });
+
afterEach(() => {
+ console.error.mockRestore();
+
// eslint-disable-next-line no-underscore-dangle
delete window._Sentry;
});
describe('when _Sentry is not defined', () => {
- it('methods fail silently', () => {
- expect(() => {
- Sentry.captureException(mockError);
- }).not.toThrow();
+ it('captureException will report to console instead', () => {
+ Sentry.captureException(mockError);
+
+ expect(console.error).toHaveBeenCalledTimes(1);
+ expect(console.error).toHaveBeenCalledWith(
+ '[Sentry stub]',
+ 'captureException(...) called with:',
+ { 0: mockError },
+ );
});
});
diff --git a/spec/frontend/shortcuts_spec.js b/spec/frontend/shortcuts_spec.js
index 88ad9204d08..ca72426cb44 100644
--- a/spec/frontend/shortcuts_spec.js
+++ b/spec/frontend/shortcuts_spec.js
@@ -7,23 +7,26 @@ import Shortcuts, { LOCAL_MOUSETRAP_DATA_KEY } from '~/behaviors/shortcuts/short
import MarkdownPreview from '~/behaviors/preview_markdown';
describe('Shortcuts', () => {
- const createEvent = (type, target) =>
- $.Event(type, {
- target,
- });
let shortcuts;
beforeAll(() => {
shortcuts = new Shortcuts();
});
+ const mockSuperSidebarSearchButton = () => {
+ const button = document.createElement('button');
+ button.id = 'super-sidebar-search';
+ return button;
+ };
+
beforeEach(() => {
setHTMLFixture(htmlSnippetsShow);
+ document.body.appendChild(mockSuperSidebarSearchButton());
new Shortcuts(); // eslint-disable-line no-new
new MarkdownPreview(); // eslint-disable-line no-new
- jest.spyOn(document.querySelector('#search'), 'focus');
+ jest.spyOn(HTMLElement.prototype, 'click');
jest.spyOn(Mousetrap.prototype, 'stopCallback');
jest.spyOn(Mousetrap.prototype, 'bind').mockImplementation();
@@ -100,21 +103,22 @@ describe('Shortcuts', () => {
});
describe('focusSearch', () => {
- describe('when super sidebar is NOT enabled', () => {
- let originalGon;
- beforeEach(() => {
- originalGon = window.gon;
- window.gon = { use_new_navigation: false };
- });
+ let event;
- afterEach(() => {
- window.gon = originalGon;
- });
+ beforeEach(() => {
+ window.gon.use_new_navigation = true;
+ event = new KeyboardEvent('keydown', { cancelable: true });
+ Shortcuts.focusSearch(event);
+ });
- it('focuses the search bar', () => {
- Shortcuts.focusSearch(createEvent('KeyboardEvent'));
- expect(document.querySelector('#search').focus).toHaveBeenCalled();
- });
+ it('clicks the super sidebar search button', () => {
+ expect(HTMLElement.prototype.click).toHaveBeenCalled();
+ const thisArg = HTMLElement.prototype.click.mock.contexts[0];
+ expect(thisArg.id).toBe('super-sidebar-search');
+ });
+
+ it('cancels the default behaviour of the event', () => {
+ expect(event.defaultPrevented).toBe(true);
});
});
diff --git a/spec/frontend/sidebar/components/lock/__snapshots__/edit_form_spec.js.snap b/spec/frontend/sidebar/components/lock/__snapshots__/edit_form_spec.js.snap
index d5bbd3bb3c9..48ba23ac0a1 100644
--- a/spec/frontend/sidebar/components/lock/__snapshots__/edit_form_spec.js.snap
+++ b/spec/frontend/sidebar/components/lock/__snapshots__/edit_form_spec.js.snap
@@ -9,7 +9,7 @@ exports[`Edit Form Dropdown In issue page when locked the appropriate warning te
class="text"
>
<gl-sprintf-stub
- message="Unlock this %{issuableDisplayName}? %{strongStart}Everyone%{strongEnd} will be able to comment."
+ message="Unlock this discussion? %{strongStart}Everyone%{strongEnd} will be able to comment."
/>
</p>
<edit-form-buttons-stub
@@ -28,7 +28,7 @@ exports[`Edit Form Dropdown In issue page when unlocked the appropriate warning
class="text"
>
<gl-sprintf-stub
- message="Lock this %{issuableDisplayName}? Only %{strongStart}project members%{strongEnd} will be able to comment."
+ message="Lock this discussion? Only %{strongStart}project members%{strongEnd} will be able to comment."
/>
</p>
<edit-form-buttons-stub
@@ -46,7 +46,7 @@ exports[`Edit Form Dropdown In merge request page when locked the appropriate wa
class="text"
>
<gl-sprintf-stub
- message="Unlock this %{issuableDisplayName}? %{strongStart}Everyone%{strongEnd} will be able to comment."
+ message="Unlock this discussion? %{strongStart}Everyone%{strongEnd} will be able to comment."
/>
</p>
<edit-form-buttons-stub
@@ -65,7 +65,7 @@ exports[`Edit Form Dropdown In merge request page when unlocked the appropriate
class="text"
>
<gl-sprintf-stub
- message="Lock this %{issuableDisplayName}? Only %{strongStart}project members%{strongEnd} will be able to comment."
+ message="Lock this discussion? Only %{strongStart}project members%{strongEnd} will be able to comment."
/>
</p>
<edit-form-buttons-stub
diff --git a/spec/frontend/sidebar/components/lock/issuable_lock_form_spec.js b/spec/frontend/sidebar/components/lock/issuable_lock_form_spec.js
index e1c41fb8b46..69531af6e3a 100644
--- a/spec/frontend/sidebar/components/lock/issuable_lock_form_spec.js
+++ b/spec/frontend/sidebar/components/lock/issuable_lock_form_spec.js
@@ -176,8 +176,8 @@ describe('IssuableLockForm', () => {
it.each`
locked | message
- ${true} | ${'Merge request locked.'}
- ${false} | ${'Merge request unlocked.'}
+ ${true} | ${'Discussion locked.'}
+ ${false} | ${'Discussion unlocked.'}
`('displays $message when merge request is $locked', async ({ locked, message }) => {
initStore(locked);
diff --git a/spec/frontend/sidebar/components/move/issuable_move_dropdown_spec.js b/spec/frontend/sidebar/components/move/issuable_move_dropdown_spec.js
index 56c915c4cae..f049001ba45 100644
--- a/spec/frontend/sidebar/components/move/issuable_move_dropdown_spec.js
+++ b/spec/frontend/sidebar/components/move/issuable_move_dropdown_spec.js
@@ -1,19 +1,9 @@
-import { nextTick } from 'vue';
-import {
- GlIcon,
- GlLoadingIcon,
- GlDropdown,
- GlDropdownForm,
- GlDropdownItem,
- GlSearchBoxByType,
- GlButton,
-} from '@gitlab/ui';
+import { GlCollapsibleListbox, GlListboxItem } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import axios from '~/lib/utils/axios_utils';
import IssuableMoveDropdown from '~/sidebar/components/move/issuable_move_dropdown.vue';
-import { stubComponent } from 'helpers/stub_component';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
const mockProjects = [
@@ -42,318 +32,136 @@ const mockProps = {
disabled: false,
};
-const mockEvent = {
- stopPropagation: jest.fn(),
- preventDefault: jest.fn(),
-};
-
-const focusInputMock = jest.fn();
-const hideMock = jest.fn();
-
describe('IssuableMoveDropdown', () => {
let mock;
let wrapper;
- const createComponent = (propsData = mockProps) => {
- wrapper = shallowMountExtended(IssuableMoveDropdown, {
- propsData,
- stubs: {
- GlDropdown: stubComponent(GlDropdown, {
- methods: {
- hide: hideMock,
- },
- }),
- GlSearchBoxByType: stubComponent(GlSearchBoxByType, {
- methods: {
- focusInput: focusInputMock,
- },
- }),
- },
- });
+ const createComponent = (propsData = {}) => {
+ wrapper = mountExtended(IssuableMoveDropdown, { propsData: { ...mockProps, ...propsData } });
};
beforeEach(() => {
mock = new MockAdapter(axios);
mock.onGet(mockProps.projectsFetchPath).reply(HTTP_STATUS_OK, mockProjects);
-
- createComponent();
});
afterEach(() => {
mock.restore();
});
- const findCollapsedEl = () => wrapper.findByTestId('move-collapsed');
- const findFooter = () => wrapper.findByTestId('footer');
- const findHeader = () => wrapper.findByTestId('header');
- const findFailedLoadResults = () => wrapper.findByTestId('failed-load-results');
- const findDropdownContent = () => wrapper.findByTestId('content');
- const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
- const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const findDropdownEl = () => wrapper.findComponent(GlDropdown);
- const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
-
- describe('watch', () => {
- describe('searchKey', () => {
- it('calls `fetchProjects` with value of the prop', async () => {
- jest.spyOn(axios, 'get');
- findSearchBox().vm.$emit('input', 'foo');
-
- await waitForPromises();
-
- expect(axios.get).toHaveBeenCalledWith('/-/autocomplete/projects?project_id=1', {
- params: { search: 'foo' },
- });
- });
- });
- });
-
- describe('methods', () => {
- describe('fetchProjects', () => {
- it('sets projectsListLoading to true and projectsListLoadFailed to false', async () => {
- findDropdownEl().vm.$emit('shown');
- await nextTick();
-
- expect(findLoadingIcon().exists()).toBe(true);
- expect(findFailedLoadResults().exists()).toBe(false);
- });
-
- it('calls `axios.get` with `projectsFetchPath` and query param `search`', async () => {
- jest.spyOn(axios, 'get');
-
- findSearchBox().vm.$emit('input', 'foo');
- await waitForPromises();
-
- expect(axios.get).toHaveBeenCalledWith(
- mockProps.projectsFetchPath,
- expect.objectContaining({
- params: {
- search: 'foo',
- },
- }),
- );
- });
-
- it('sets response to `projects` and focuses on searchInput when request is successful', async () => {
- jest.spyOn(axios, 'get');
-
- findSearchBox().vm.$emit('input', 'foo');
- await waitForPromises();
+ const findDropdownButton = () => wrapper.findByTestId('dropdown-button');
+ const findDropdown = () => wrapper.findComponent(GlCollapsibleListbox);
+ const findDropdownMoveButton = () => wrapper.findByTestId('dropdown-move-button');
+ const findDropdownItemsText = () =>
+ wrapper.findAllComponents(GlListboxItem).wrappers.map((item) => item.text());
- expect(findAllDropdownItems()).toHaveLength(mockProjects.length);
- expect(focusInputMock).toHaveBeenCalled();
- });
-
- it('sets projectsListLoadFailed to true when request fails', async () => {
- jest.spyOn(axios, 'get').mockRejectedValue({});
-
- findSearchBox().vm.$emit('input', 'foo');
- await waitForPromises();
-
- expect(findFailedLoadResults().exists()).toBe(true);
- });
-
- it('sets projectsListLoading to false when request completes', async () => {
- jest.spyOn(axios, 'get');
-
- findDropdownEl().vm.$emit('shown');
- await waitForPromises();
-
- expect(findLoadingIcon().exists()).toBe(false);
- });
- });
-
- describe('isSelectedProject', () => {
- it.each`
- projectIndex | selectedProjectIndex | title | returnValue
- ${0} | ${0} | ${'are same projects'} | ${true}
- ${0} | ${1} | ${'are different projects'} | ${false}
- `(
- 'returns $returnValue when selectedProject and provided project param $title',
- async ({ projectIndex, selectedProjectIndex, returnValue }) => {
- findDropdownEl().vm.$emit('shown');
- await waitForPromises();
-
- findAllDropdownItems().at(selectedProjectIndex).vm.$emit('click', mockEvent);
-
- await nextTick();
-
- expect(findAllDropdownItems().at(projectIndex).props('isChecked')).toBe(returnValue);
- },
- );
-
- it('returns false when selectedProject is null', async () => {
- findDropdownEl().vm.$emit('shown');
- await waitForPromises();
+ it('renders a dropdown button with provided title and header', () => {
+ createComponent();
- expect(findAllDropdownItems().at(0).props('isChecked')).toBe(false);
- });
- });
+ expect(findDropdownButton().text()).toBe(mockProps.dropdownButtonTitle);
+ expect(findDropdown().props('headerText')).toBe(mockProps.dropdownHeaderTitle);
});
- describe('template', () => {
- it('renders collapsed state element with icon', () => {
- const collapsedEl = findCollapsedEl();
-
- expect(collapsedEl.exists()).toBe(true);
- expect(collapsedEl.attributes('title')).toBe(mockProps.dropdownButtonTitle);
- expect(collapsedEl.findComponent(GlIcon).exists()).toBe(true);
- expect(collapsedEl.findComponent(GlIcon).props('name')).toBe('arrow-right');
- });
-
- describe('gl-dropdown component', () => {
- it('renders component container element', () => {
- expect(findDropdownEl().exists()).toBe(true);
- expect(findDropdownEl().props('block')).toBe(true);
- });
+ it('renders the dropdown button as disabled when disabled prop is true', () => {
+ createComponent({ disabled: true });
- it('renders gl-dropdown-form component', () => {
- expect(findDropdownEl().findComponent(GlDropdownForm).exists()).toBe(true);
- });
-
- it('renders disabled dropdown when `disabled` is true', () => {
- createComponent({ ...mockProps, disabled: true });
- expect(findDropdownEl().props('disabled')).toBe(true);
- });
-
- it('renders header element', () => {
- const headerEl = findHeader();
-
- expect(headerEl.exists()).toBe(true);
- expect(headerEl.find('span').text()).toBe(mockProps.dropdownHeaderTitle);
- expect(headerEl.findComponent(GlButton).props('icon')).toBe('close');
- });
-
- it('renders gl-search-box-by-type component', () => {
- const searchEl = findDropdownEl().findComponent(GlSearchBoxByType);
-
- expect(searchEl.exists()).toBe(true);
- expect(searchEl.attributes()).toMatchObject({
- placeholder: 'Search project',
- debounce: '300',
- });
- });
-
- it('renders gl-loading-icon component when projectsListLoading prop is true', async () => {
- findDropdownEl().vm.$emit('shown');
- await nextTick();
-
- expect(findLoadingIcon().exists()).toBe(true);
- });
-
- it('renders gl-dropdown-item components for available projects', async () => {
- findDropdownEl().vm.$emit('shown');
- await waitForPromises();
-
- findAllDropdownItems().at(0).vm.$emit('click', mockEvent);
- await nextTick();
-
- expect(findAllDropdownItems()).toHaveLength(mockProjects.length);
- expect(findAllDropdownItems().at(0).props()).toMatchObject({
- isCheckItem: true,
- isChecked: true,
- });
- expect(findAllDropdownItems().at(0).text()).toBe(mockProjects[0].name_with_namespace);
- });
-
- it('renders string "No matching results" when search does not yield any matches', async () => {
- mock.onGet(mockProps.projectsFetchPath).reply(HTTP_STATUS_OK, []);
+ expect(findDropdownButton().props('disabled')).toBe(true);
+ });
- findSearchBox().vm.$emit('input', 'foo');
- await waitForPromises();
+ it('triggers a project search when dropdown button is clicked', async () => {
+ createComponent();
- expect(findDropdownContent().text()).toContain('No matching results');
- });
+ await findDropdownButton().trigger('click');
+ await waitForPromises();
- it('renders string "Failed to load projects" when loading projects list fails', async () => {
- mock.onGet(mockProps.projectsFetchPath).reply(HTTP_STATUS_OK, []);
- jest.spyOn(axios, 'get').mockRejectedValue({});
+ expect(mock.history.get).toHaveLength(1);
- findDropdownEl().vm.$emit('shown');
- await waitForPromises();
+ expect(findDropdownItemsText()).toEqual([
+ 'Gitlab Org / Gitlab Shell',
+ 'Gnuwget / Wget2',
+ 'Commit451 / Lab Coat',
+ ]);
+ });
- expect(findDropdownContent().text()).toContain('Failed to load projects');
- });
+ it('shows "No matching results" when no projects are found', async () => {
+ createComponent();
- it('renders gl-button within footer', async () => {
- const moveButtonEl = findFooter().findComponent(GlButton);
+ mock.onGet(mockProps.projectsFetchPath).reply(HTTP_STATUS_OK, []);
- expect(moveButtonEl.text()).toBe('Move');
- expect(moveButtonEl.attributes('disabled')).toBeDefined();
+ await findDropdown().vm.$emit('search', 'foobar');
+ await waitForPromises();
- findDropdownEl().vm.$emit('shown');
- await waitForPromises();
+ expect(findDropdown().text()).toContain('No matching results');
+ expect(findDropdownItemsText()).toEqual([]);
+ });
- findAllDropdownItems().at(0).vm.$emit('click', mockEvent);
- await nextTick();
+ it('shows "Failed to load projects" when request fails', async () => {
+ createComponent();
- expect(findFooter().findComponent(GlButton).attributes('disabled')).not.toBeDefined();
- });
- });
+ mock.onGet(mockProps.projectsFetchPath).networkError();
- describe('events', () => {
- it('collapsed state element emits `toggle-collapse` event on component when clicked', () => {
- findCollapsedEl().trigger('click');
+ await findDropdown().vm.$emit('search', 'foobar');
+ await waitForPromises();
- expect(wrapper.emitted('toggle-collapse')).toHaveLength(1);
- });
+ expect(findDropdown().text()).toContain('Failed to load projects');
+ expect(findDropdownItemsText()).toEqual([]);
+ });
- it('gl-dropdown component calls `fetchProjects` on `shown` event', () => {
- jest.spyOn(axios, 'get');
+ it('disables the Move issuable button if no project is selected', async () => {
+ createComponent();
- findDropdownEl().vm.$emit('shown');
+ await findDropdownButton().trigger('click');
+ await waitForPromises();
- expect(axios.get).toHaveBeenCalled();
- });
+ expect(findDropdownMoveButton().props('disabled')).toBe(true);
+ });
- it('gl-dropdown component prevents dropdown body from closing on `hide` event when `projectItemClick` prop is true', async () => {
- findDropdownEl().vm.$emit('shown');
- await waitForPromises();
+ it('shows search results when search is successful', async () => {
+ createComponent();
- findAllDropdownItems().at(0).vm.$emit('click', mockEvent);
- await nextTick();
+ mock.onGet(mockProps.projectsFetchPath).reply(HTTP_STATUS_OK, [
+ {
+ id: 2,
+ name_with_namespace: 'Gitlab Org / Gitlab Shell',
+ full_path: 'gitlab-org/gitlab-shell',
+ },
+ ]);
- findDropdownEl().vm.$emit('hide', mockEvent);
+ await findDropdown().vm.$emit('search', 'shell');
+ await waitForPromises();
- expect(mockEvent.preventDefault).toHaveBeenCalled();
- });
+ expect(findDropdownItemsText()).toEqual(['Gitlab Org / Gitlab Shell']);
+ });
- it('gl-dropdown component emits `dropdown-close` event on component from `hide` event', () => {
- findDropdownEl().vm.$emit('hide');
+ it('emits "move-issuable" event when Move issuable button is clicked', async () => {
+ createComponent();
- expect(wrapper.emitted('dropdown-close')).toHaveLength(1);
- });
+ await findDropdownButton().trigger('click');
+ await waitForPromises();
- it('close icon in dropdown header closes the dropdown when clicked', async () => {
- findHeader().findComponent(GlButton).vm.$emit('click', mockEvent);
+ await wrapper.findAllComponents(GlListboxItem).wrappers[0].trigger('click');
+ await findDropdownMoveButton().trigger('click');
- await nextTick();
- expect(hideMock).toHaveBeenCalled();
- });
+ expect(wrapper.emitted('move-issuable')).toEqual([[mockProjects[0]]]);
+ });
- it('sets project for clicked gl-dropdown-item to selectedProject', async () => {
- findDropdownEl().vm.$emit('shown');
- await waitForPromises();
+ it('disables the Move issuable button when moveInProgress prop is true', async () => {
+ createComponent({ moveInProgress: true });
- findAllDropdownItems().at(0).vm.$emit('click', mockEvent);
- await nextTick();
+ await findDropdownButton().trigger('click');
+ await waitForPromises();
- expect(findAllDropdownItems().at(0).props('isChecked')).toBe(true);
- });
+ expect(findDropdownMoveButton().props('disabled')).toBe(true);
+ });
- it('hides dropdown and emits `move-issuable` event when move button is clicked', async () => {
- findDropdownEl().vm.$emit('shown');
- await waitForPromises();
+ it('emits "dropdown-close" event when dropdown is hidden', async () => {
+ createComponent();
- findAllDropdownItems().at(0).vm.$emit('click', mockEvent);
- await nextTick();
+ await findDropdownButton().trigger('click');
+ await waitForPromises();
- findFooter().findComponent(GlButton).vm.$emit('click');
+ await findDropdown().vm.$emit('hidden');
- expect(hideMock).toHaveBeenCalled();
- expect(wrapper.emitted('move-issuable')).toHaveLength(1);
- expect(wrapper.emitted('move-issuable')[0]).toEqual([mockProjects[0]]);
- });
- });
+ expect(wrapper.emitted('dropdown-close')).toHaveLength(1);
});
});
diff --git a/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js b/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js
index 27ab347775a..c1c3c1fea91 100644
--- a/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js
+++ b/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js
@@ -1,8 +1,8 @@
import { GlLink, GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
import { shallowMount, mount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import createMockApollo from 'helpers/mock_apollo_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
diff --git a/spec/frontend/silent_mode_settings/components/app_spec.js b/spec/frontend/silent_mode_settings/components/app_spec.js
index 5997bfd1b5f..dfa2b1bfcbb 100644
--- a/spec/frontend/silent_mode_settings/components/app_spec.js
+++ b/spec/frontend/silent_mode_settings/components/app_spec.js
@@ -1,4 +1,4 @@
-import { GlToggle, GlBadge } from '@gitlab/ui';
+import { GlToggle } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/alert';
@@ -29,19 +29,8 @@ describe('SilentModeSettingsApp', () => {
};
const findGlToggle = () => wrapper.findComponent(GlToggle);
- const findGlBadge = () => wrapper.findComponent(GlBadge);
describe('template', () => {
- describe('experiment badge', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('renders properly', () => {
- expect(findGlBadge().exists()).toBe(true);
- });
- });
-
describe('when silent mode is already enabled', () => {
beforeEach(() => {
createComponent({ isSilentModeEnabled: true });
diff --git a/spec/frontend/snippets/components/snippet_header_spec.js b/spec/frontend/snippets/components/snippet_header_spec.js
index 3932675aa52..1eb5de70e4b 100644
--- a/spec/frontend/snippets/components/snippet_header_spec.js
+++ b/spec/frontend/snippets/components/snippet_header_spec.js
@@ -80,6 +80,7 @@ describe('Snippet header component', () => {
const findAuthorEmoji = () => wrapper.findComponent(GlEmoji);
const findAuthoredMessage = () => wrapper.find('[data-testid="authored-message"]').text();
+ const findAuthorUsername = () => wrapper.find('[data-testid="authored-username"]');
const findButtons = () => wrapper.findAllComponents(GlButton);
const findButtonsAsModel = () =>
findButtons().wrappers.map((x) => ({
@@ -116,6 +117,7 @@ describe('Snippet header component', () => {
project: null,
author: {
name: 'Thor Odinson',
+ username: null,
status: null,
},
blobs: [Blob],
@@ -135,12 +137,24 @@ describe('Snippet header component', () => {
expect(wrapper.find('.detail-page-header').exists()).toBe(true);
});
- it('renders a message showing snippet creation date and author', () => {
+ it('renders a message showing snippet creation date and author full name, without username when not available', () => {
createComponent();
const text = findAuthoredMessage();
expect(text).toContain('Authored 1 month ago by');
expect(text).toContain('Thor Odinson');
+ expect(findAuthorUsername().exists()).toBe(false);
+ });
+
+ it('renders a message showing snippet creation date, author full name and username', () => {
+ snippet.author.username = 'todinson';
+ createComponent();
+
+ const text = findAuthoredMessage();
+ expect(text).toContain('Authored 1 month ago by');
+ expect(text).toContain('Thor Odinson');
+ expect(text).toContain('@todinson');
+ expect(findAuthorUsername().exists()).toBe(true);
});
describe('author status', () => {
diff --git a/spec/frontend/super_sidebar/components/global_search/components/global_search_default_places_spec.js b/spec/frontend/super_sidebar/components/global_search/components/global_search_default_places_spec.js
index f91c8034fe9..d1bec8f8662 100644
--- a/spec/frontend/super_sidebar/components/global_search/components/global_search_default_places_spec.js
+++ b/spec/frontend/super_sidebar/components/global_search/components/global_search_default_places_spec.js
@@ -88,6 +88,19 @@ describe('GlobalSearchDefaultPlaces', () => {
'data-qa-places-item': 'Admin area',
},
},
+ {
+ text: 'Leave admin mode',
+ href: '/admin/session/destroy',
+ extraAttrs: {
+ 'data-track-action': 'click_command_palette_item',
+ 'data-track-extra': '{"title":"Leave admin mode"}',
+ 'data-track-label': 'item_without_id',
+ 'data-track-property': 'nav_panel_unknown',
+ 'data-testid': 'places-item-link',
+ 'data-qa-places-item': 'Leave admin mode',
+ 'data-method': 'post',
+ },
+ },
]);
});
});
diff --git a/spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js b/spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js
index 038c7a96adc..c1258294110 100644
--- a/spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js
+++ b/spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js
@@ -25,7 +25,14 @@ import {
} from '~/super_sidebar/components/global_search/constants';
import { truncate } from '~/lib/utils/text_utility';
import { visitUrl } from '~/lib/utils/url_utility';
-import { ENTER_KEY } from '~/lib/utils/keys';
+import {
+ ENTER_KEY,
+ ARROW_DOWN_KEY,
+ ARROW_UP_KEY,
+ END_KEY,
+ HOME_KEY,
+ NUMPAD_ENTER_KEY,
+} from '~/lib/utils/keys';
import {
MOCK_SEARCH,
MOCK_SEARCH_QUERY,
@@ -415,7 +422,7 @@ describe('GlobalSearchModal', () => {
class="gl-new-dropdown-item"
tabindex="0"
:data-testid="'test-result-' + n"
- >Result {{ n }}</li>
+ ><a href="#">Result {{ n }}</a></li>
</ul>`,
},
},
@@ -429,26 +436,26 @@ describe('GlobalSearchModal', () => {
});
it('Home key keeps focus in input', () => {
- const event = triggerKeydownEvent(findSearchInput().element, 'Home');
+ const event = triggerKeydownEvent(findSearchInput().element, HOME_KEY);
expect(document.activeElement).toBe(findSearchInput().element);
expect(event.defaultPrevented).toBe(false);
});
it('End key keeps focus on input', () => {
- const event = triggerKeydownEvent(findSearchInput().element, 'End');
- findSearchInput().trigger('keydown', { code: 'End' });
+ const event = triggerKeydownEvent(findSearchInput().element, END_KEY);
+ findSearchInput().trigger('keydown', { code: END_KEY });
expect(document.activeElement).toBe(findSearchInput().element);
expect(event.defaultPrevented).toBe(false);
});
it('ArrowUp keeps focus on input', () => {
- const event = triggerKeydownEvent(findSearchInput().element, 'ArrowUp');
+ const event = triggerKeydownEvent(findSearchInput().element, ARROW_UP_KEY);
expect(document.activeElement).toBe(findSearchInput().element);
expect(event.defaultPrevented).toBe(false);
});
it('ArrowDown focuses the first item', () => {
- const event = triggerKeydownEvent(findSearchInput().element, 'ArrowDown');
+ const event = triggerKeydownEvent(findSearchInput().element, ARROW_DOWN_KEY);
expect(document.activeElement).toBe(wrapper.findByTestId('test-result-1').element);
expect(event.defaultPrevented).toBe(true);
});
@@ -460,32 +467,44 @@ describe('GlobalSearchModal', () => {
});
it('Home key focuses first item', () => {
- const event = triggerKeydownEvent(document.activeElement, 'Home');
+ const event = triggerKeydownEvent(document.activeElement, HOME_KEY);
expect(document.activeElement).toBe(wrapper.findByTestId('test-result-1').element);
expect(event.defaultPrevented).toBe(true);
});
it('End key focuses last item', () => {
- const event = triggerKeydownEvent(document.activeElement, 'End');
+ const event = triggerKeydownEvent(document.activeElement, END_KEY);
expect(document.activeElement).toBe(wrapper.findByTestId('test-result-5').element);
expect(event.defaultPrevented).toBe(true);
});
it('ArrowUp focuses previous item if any, else input', () => {
- let event = triggerKeydownEvent(document.activeElement, 'ArrowUp');
+ let event = triggerKeydownEvent(document.activeElement, ARROW_UP_KEY);
expect(document.activeElement).toBe(wrapper.findByTestId('test-result-1').element);
expect(event.defaultPrevented).toBe(true);
- event = triggerKeydownEvent(document.activeElement, 'ArrowUp');
+ event = triggerKeydownEvent(document.activeElement, ARROW_UP_KEY);
expect(document.activeElement).toBe(findSearchInput().element);
expect(event.defaultPrevented).toBe(true);
});
it('ArrowDown focuses next item', () => {
- const event = triggerKeydownEvent(document.activeElement, 'ArrowDown');
+ const event = triggerKeydownEvent(document.activeElement, ARROW_DOWN_KEY);
expect(document.activeElement).toBe(wrapper.findByTestId('test-result-3').element);
expect(event.defaultPrevented).toBe(true);
});
+
+ it('NumpadEnter clicks the current item child', () => {
+ const focusedElement = document.activeElement;
+ const focusedElementChild = focusedElement.firstChild;
+
+ const clickMock = jest.fn();
+ focusedElementChild.click = clickMock;
+
+ const event = triggerKeydownEvent(focusedElement, NUMPAD_ENTER_KEY);
+ expect(clickMock).toHaveBeenCalled();
+ expect(event.defaultPrevented).toBe(true);
+ });
});
});
});
diff --git a/spec/frontend/super_sidebar/components/nav_item_spec.js b/spec/frontend/super_sidebar/components/nav_item_spec.js
index e6de9b1de22..94eb47887c3 100644
--- a/spec/frontend/super_sidebar/components/nav_item_spec.js
+++ b/spec/frontend/super_sidebar/components/nav_item_spec.js
@@ -90,6 +90,19 @@ describe('NavItem component', () => {
expect(findPill().text()).toBe(initialPillValue);
});
});
+
+ describe('async updating pill prop', () => {
+ it('re-renders item with when prop pill_count changes', async () => {
+ createWrapper({ item: { title: 'Foo', pill_count: 0 } });
+
+ expect(findPill().text()).toBe('0');
+
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/428246
+ // This is testing specific async behaviour that was before missed
+ await wrapper.setProps({ item: { title: 'Foo', pill_count: 10 } });
+ expect(findPill().text()).toBe('10');
+ });
+ });
});
describe('destroyed', () => {
diff --git a/spec/frontend/super_sidebar/components/user_bar_spec.js b/spec/frontend/super_sidebar/components/user_bar_spec.js
index b58b65f09f5..27d65f27007 100644
--- a/spec/frontend/super_sidebar/components/user_bar_spec.js
+++ b/spec/frontend/super_sidebar/components/user_bar_spec.js
@@ -49,7 +49,6 @@ describe('UserBar component', () => {
sidebarData,
},
provide: {
- toggleNewNavEndpoint: '/-/profile/preferences',
isImpersonating: false,
...provideOverrides,
},
diff --git a/spec/frontend/super_sidebar/components/user_menu_spec.js b/spec/frontend/super_sidebar/components/user_menu_spec.js
index 79a31492f3f..45a60fce00a 100644
--- a/spec/frontend/super_sidebar/components/user_menu_spec.js
+++ b/spec/frontend/super_sidebar/components/user_menu_spec.js
@@ -3,8 +3,6 @@ import { mountExtended } from 'helpers/vue_test_utils_helper';
import { stubComponent } from 'helpers/stub_component';
import UserMenu from '~/super_sidebar/components/user_menu.vue';
import UserMenuProfileItem from '~/super_sidebar/components/user_menu_profile_item.vue';
-import NewNavToggle from '~/nav/components/new_nav_toggle.vue';
-import invalidUrl from '~/lib/utils/invalid_url';
import { mockTracking } from 'helpers/tracking_helper';
import PersistentUserCallout from '~/persistent_user_callout';
import { userMenuMockData, userMenuMockStatus, userMenuMockPipelineMinutes } from '../mock_data';
@@ -14,7 +12,6 @@ describe('UserMenu component', () => {
let trackingSpy;
const GlEmoji = { template: '<img/>' };
- const toggleNewNavEndpoint = invalidUrl;
const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
const showDropdown = () => findDropdown().vm.$emit('shown');
@@ -34,7 +31,6 @@ describe('UserMenu component', () => {
...stubs,
},
provide: {
- toggleNewNavEndpoint,
isImpersonating: false,
...provide,
},
@@ -459,15 +455,6 @@ describe('UserMenu component', () => {
});
});
- describe('New navigation toggle item', () => {
- it('should render menu item with new navigation toggle', () => {
- createWrapper();
- const toggleItem = wrapper.findComponent(NewNavToggle);
- expect(toggleItem.exists()).toBe(true);
- expect(toggleItem.props('endpoint')).toBe(toggleNewNavEndpoint);
- });
- });
-
describe('Sign out group', () => {
const findSignOutGroup = () => wrapper.findByTestId('sign-out-group');
diff --git a/spec/frontend/super_sidebar/mock_data.js b/spec/frontend/super_sidebar/mock_data.js
index d464ce372ed..d2d2faedbf8 100644
--- a/spec/frontend/super_sidebar/mock_data.js
+++ b/spec/frontend/super_sidebar/mock_data.js
@@ -74,6 +74,7 @@ export const mergeRequestMenuGroup = [
export const contextSwitcherLinks = [
{ title: 'Explore', link: '/explore', icon: 'compass', link_classes: 'persistent-link-class' },
{ title: 'Admin area', link: '/admin', icon: 'admin' },
+ { title: 'Leave admin mode', link: '/admin/session/destroy', data_method: 'post' },
];
export const sidebarData = {
diff --git a/spec/frontend/super_sidebar/utils_spec.js b/spec/frontend/super_sidebar/utils_spec.js
index 85c13a4c892..43eb82f5928 100644
--- a/spec/frontend/super_sidebar/utils_spec.js
+++ b/spec/frontend/super_sidebar/utils_spec.js
@@ -1,5 +1,5 @@
-import * as Sentry from '@sentry/browser';
import MockAdapter from 'axios-mock-adapter';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import {
getTopFrequentItems,
trackContextAccess,
@@ -16,7 +16,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import { unsortedFrequentItems, sortedFrequentItems } from '../frequent_items/mock_data';
import { cachedFrequentProjects } from './mock_data';
-jest.mock('@sentry/browser');
+jest.mock('~/sentry/sentry_browser_wrapper');
useLocalStorageSpy();
diff --git a/spec/frontend/terraform/components/init_command_modal_spec.js b/spec/frontend/terraform/components/init_command_modal_spec.js
index 4015482b81b..cdd25e90318 100644
--- a/spec/frontend/terraform/components/init_command_modal_spec.js
+++ b/spec/frontend/terraform/components/init_command_modal_spec.js
@@ -8,13 +8,13 @@ const terraformApiUrl = 'https://gitlab.com/api/v4/projects/1';
const username = 'username';
const modalId = 'fake-modal-id';
const stateName = 'aws/eu-central-1';
-const stateNamePlaceholder = '<YOUR-STATE-NAME>';
const stateNameEncoded = encodeURIComponent(stateName);
const modalInfoCopyStr = `export GITLAB_ACCESS_TOKEN=<YOUR-ACCESS-TOKEN>
+export TF_STATE_NAME=${stateNameEncoded}
terraform init \\
- -backend-config="address=${terraformApiUrl}/${stateNameEncoded}" \\
- -backend-config="lock_address=${terraformApiUrl}/${stateNameEncoded}/lock" \\
- -backend-config="unlock_address=${terraformApiUrl}/${stateNameEncoded}/lock" \\
+ -backend-config="address=${terraformApiUrl}/$TF_STATE_NAME" \\
+ -backend-config="lock_address=${terraformApiUrl}/$TF_STATE_NAME/lock" \\
+ -backend-config="unlock_address=${terraformApiUrl}/$TF_STATE_NAME/lock" \\
-backend-config="username=${username}" \\
-backend-config="password=$GITLAB_ACCESS_TOKEN" \\
-backend-config="lock_method=POST" \\
@@ -67,7 +67,7 @@ describe('InitCommandModal', () => {
describe('init command', () => {
it('includes correct address', () => {
expect(findInitCommand().text()).toContain(
- `-backend-config="address=${terraformApiUrl}/${stateNameEncoded}"`,
+ `-backend-config="address=${terraformApiUrl}/$TF_STATE_NAME"`,
);
});
it('includes correct username', () => {
@@ -94,7 +94,7 @@ describe('InitCommandModal', () => {
describe('on rendering', () => {
it('includes correct address', () => {
expect(findInitCommand().text()).toContain(
- `-backend-config="address=${terraformApiUrl}/${stateNamePlaceholder}"`,
+ `-backend-config="address=${terraformApiUrl}/$TF_STATE_NAME"`,
);
});
});
diff --git a/spec/frontend/time_tracking/components/timelogs_app_spec.js b/spec/frontend/time_tracking/components/timelogs_app_spec.js
index 13188f3b937..4cc719ee09f 100644
--- a/spec/frontend/time_tracking/components/timelogs_app_spec.js
+++ b/spec/frontend/time_tracking/components/timelogs_app_spec.js
@@ -1,10 +1,10 @@
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
-import * as Sentry from '@sentry/browser';
import { GlDatepicker, GlLoadingIcon, GlKeysetPagination } from '@gitlab/ui';
import getTimelogsEmptyResponse from 'test_fixtures/graphql/get_timelogs_empty_response.json';
import getPaginatedTimelogsResponse from 'test_fixtures/graphql/get_paginated_timelogs_response.json';
import getNonPaginatedTimelogsResponse from 'test_fixtures/graphql/get_non_paginated_timelogs_response.json';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { createAlert } from '~/alert';
import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
@@ -14,7 +14,7 @@ import TimelogsApp from '~/time_tracking/components/timelogs_app.vue';
import TimelogsTable from '~/time_tracking/components/timelogs_table.vue';
jest.mock('~/alert');
-jest.mock('@sentry/browser');
+jest.mock('~/sentry/sentry_browser_wrapper');
describe('Timelogs app', () => {
Vue.use(VueApollo);
diff --git a/spec/frontend/token_access/token_projects_table_spec.js b/spec/frontend/token_access/token_projects_table_spec.js
index 7654aa09b0a..7a78befa0d7 100644
--- a/spec/frontend/token_access/token_projects_table_spec.js
+++ b/spec/frontend/token_access/token_projects_table_spec.js
@@ -28,7 +28,6 @@ describe('Token projects table', () => {
const findAllDeleteProjectBtn = () => wrapper.findAllComponents(GlButton);
const findAllTableRows = () => wrapper.findAllByTestId('projects-token-table-row');
const findProjectNameCell = () => wrapper.findByTestId('token-access-project-name');
- const findProjectNamespaceCell = () => wrapper.findByTestId('token-access-project-namespace');
it('displays a table', () => {
createComponent();
@@ -57,25 +56,9 @@ describe('Token projects table', () => {
expect(findAllDeleteProjectBtn()).toHaveLength(1);
});
- it('displays project and namespace cells', () => {
+ it('displays project fullpath', () => {
createComponent();
- expect(findProjectNameCell().text()).toBe('merge-train-stuff');
- expect(findProjectNamespaceCell().text()).toBe('root');
- });
-
- it('displays empty string for namespace when namespace is null', () => {
- const nullNamespace = {
- id: '1',
- name: 'merge-train-stuff',
- namespace: null,
- fullPath: 'root/merge-train-stuff',
- isLocked: false,
- __typename: 'Project',
- };
-
- createComponent({ projects: [nullNamespace] });
-
- expect(findProjectNamespaceCell().text()).toBe('');
+ expect(findProjectNameCell().text()).toBe('root/merge-train-stuff');
});
});
diff --git a/spec/frontend/tracking/dispatch_snowplow_event_spec.js b/spec/frontend/tracking/dispatch_snowplow_event_spec.js
index 5f4d065d504..8297a7088f2 100644
--- a/spec/frontend/tracking/dispatch_snowplow_event_spec.js
+++ b/spec/frontend/tracking/dispatch_snowplow_event_spec.js
@@ -1,10 +1,10 @@
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { dispatchSnowplowEvent } from '~/tracking/dispatch_snowplow_event';
import getStandardContext from '~/tracking/get_standard_context';
import { extraContext, servicePingContext } from './mock_data';
-jest.mock('@sentry/browser');
+jest.mock('~/sentry/sentry_browser_wrapper');
jest.mock('~/tracking/get_standard_context');
const category = 'Incident Management';
diff --git a/spec/frontend/tracking/tracking_initialization_spec.js b/spec/frontend/tracking/tracking_initialization_spec.js
index 2dc3c6ab41c..adaac7441f0 100644
--- a/spec/frontend/tracking/tracking_initialization_spec.js
+++ b/spec/frontend/tracking/tracking_initialization_spec.js
@@ -1,6 +1,7 @@
import { TRACKING_CONTEXT_SCHEMA } from '~/experimentation/constants';
import { getExperimentData, getAllExperimentContexts } from '~/experimentation/utils';
import Tracking, { initUserTracking, initDefaultTrackers, InternalEvents } from '~/tracking';
+import { MAX_LOCAL_STORAGE_QUEUE_SIZE } from '~/tracking/constants';
import getStandardContext from '~/tracking/get_standard_context';
jest.mock('~/experimentation/utils', () => ({
@@ -65,6 +66,7 @@ describe('Tracking', () => {
fields: { allow: [] },
forms: { allow: [] },
},
+ maxLocalStorageQueueSize: MAX_LOCAL_STORAGE_QUEUE_SIZE,
});
});
});
diff --git a/spec/frontend/users/profile/components/report_abuse_button_spec.js b/spec/frontend/users/profile/components/report_abuse_button_spec.js
deleted file mode 100644
index 1ca944dce12..00000000000
--- a/spec/frontend/users/profile/components/report_abuse_button_spec.js
+++ /dev/null
@@ -1,79 +0,0 @@
-import { GlButton } from '@gitlab/ui';
-import { createWrapper } from '@vue/test-utils';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
-import ReportAbuseButton from '~/users/profile/components/report_abuse_button.vue';
-import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
-
-describe('ReportAbuseButton', () => {
- let wrapper;
-
- const ACTION_PATH = '/abuse_reports/add_category';
- const USER_ID = 1;
- const REPORTED_FROM_URL = 'http://example.com';
-
- const createComponent = (props) => {
- wrapper = shallowMountExtended(ReportAbuseButton, {
- propsData: {
- ...props,
- },
- provide: {
- reportAbusePath: ACTION_PATH,
- reportedUserId: USER_ID,
- reportedFromUrl: REPORTED_FROM_URL,
- },
- });
- };
-
- beforeEach(() => {
- createComponent();
- });
-
- const findReportAbuseButton = () => wrapper.findComponent(GlButton);
- const findAbuseCategorySelector = () => wrapper.findComponent(AbuseCategorySelector);
-
- it('renders report abuse button', () => {
- expect(findReportAbuseButton().exists()).toBe(true);
-
- expect(findReportAbuseButton().props()).toMatchObject({
- category: 'primary',
- icon: 'error',
- });
-
- expect(findReportAbuseButton().attributes('aria-label')).toBe(
- ReportAbuseButton.i18n.reportAbuse,
- );
- });
-
- it('renders abuse category selector with the drawer initially closed', () => {
- expect(findAbuseCategorySelector().exists()).toBe(true);
-
- expect(findAbuseCategorySelector().props('showDrawer')).toBe(false);
- });
-
- describe('when button is clicked', () => {
- beforeEach(async () => {
- await findReportAbuseButton().vm.$emit('click');
- });
-
- it('opens the abuse category selector', () => {
- expect(findAbuseCategorySelector().props('showDrawer')).toBe(true);
- });
-
- it('closes the abuse category selector', async () => {
- await findAbuseCategorySelector().vm.$emit('close-drawer');
-
- expect(findAbuseCategorySelector().props('showDrawer')).toBe(false);
- });
- });
-
- describe('when user hovers out of the button', () => {
- it(`should emit ${BV_HIDE_TOOLTIP} to close the tooltip`, () => {
- const rootWrapper = createWrapper(wrapper.vm.$root);
-
- findReportAbuseButton().vm.$emit('mouseout');
-
- expect(rootWrapper.emitted(BV_HIDE_TOOLTIP)).toHaveLength(1);
- });
- });
-});
diff --git a/spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js
index 2aed037be6f..c81f4328d2a 100644
--- a/spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js
@@ -4,6 +4,7 @@ import { GlButton, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { createMockSubscription as createMockApolloSubscription } from 'mock-apollo-client';
import approvedByCurrentUser from 'test_fixtures/graphql/merge_requests/approvals/approvals.query.graphql.json';
+import { visitUrl } from '~/lib/utils/url_utility';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
@@ -28,6 +29,10 @@ jest.mock('~/alert', () => ({
dismiss: mockAlertDismiss,
})),
}));
+jest.mock('~/lib/utils/url_utility', () => ({
+ ...jest.requireActual('~/lib/utils/url_utility'),
+ visitUrl: jest.fn(),
+}));
const TEST_HELP_PATH = 'help/path';
const testApprovedBy = () => [1, 7, 10].map((id) => ({ id }));
@@ -113,6 +118,7 @@ describe('MRWidget approvals', () => {
targetProjectFullPath: 'gitlab-org/gitlab',
id: 1,
iid: '1',
+ requireSamlAuthToApprove: false,
};
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
@@ -172,6 +178,22 @@ describe('MRWidget approvals', () => {
category: 'primary',
});
});
+
+ describe('with SAML auth requried for approval', () => {
+ beforeEach(async () => {
+ const response = createCanApproveResponse();
+ mr.requireSamlAuthToApprove = true;
+ createComponent({}, { query: response });
+ await waitForPromises();
+ });
+ it('approve action is rendered with correct text', () => {
+ expect(findActionData()).toEqual({
+ variant: 'confirm',
+ text: 'Approve with SAML',
+ category: 'primary',
+ });
+ });
+ });
});
describe('and MR is approved', () => {
@@ -194,6 +216,25 @@ describe('MRWidget approvals', () => {
});
});
+ describe('with approvers, with SAML auth requried for approval', () => {
+ beforeEach(async () => {
+ canApproveResponse.data.project.mergeRequest.approvedBy.nodes =
+ approvedByCurrentUser.data.project.mergeRequest.approvedBy.nodes;
+ canApproveResponse.data.project.mergeRequest.approvedBy.nodes[0].id = 69;
+ mr.requireSamlAuthToApprove = true;
+ createComponent({}, { query: canApproveResponse });
+ await waitForPromises();
+ });
+
+ it('approve additionally action is rendered with correct text', () => {
+ expect(findActionData()).toEqual({
+ variant: 'confirm',
+ text: 'Approve additionally with SAML',
+ category: 'secondary',
+ });
+ });
+ });
+
describe('with approvers', () => {
beforeEach(async () => {
canApproveResponse.data.project.mergeRequest.approvedBy.nodes =
@@ -215,6 +256,25 @@ describe('MRWidget approvals', () => {
});
});
+ describe('when SAML auth is required and user clicks Approve with SAML', () => {
+ const fakeGroupSamlPath = '/example_group_saml';
+
+ beforeEach(async () => {
+ mr.requireSamlAuthToApprove = true;
+ mr.samlApprovalPath = fakeGroupSamlPath;
+
+ createComponent({}, { query: createCanApproveResponse() });
+ await waitForPromises();
+ });
+
+ it('redirects the user to the group SAML path', async () => {
+ const action = findAction();
+ action.vm.$emit('click');
+ await nextTick();
+ expect(visitUrl).toHaveBeenCalledWith(fakeGroupSamlPath);
+ });
+ });
+
describe('when approve action is clicked', () => {
beforeEach(async () => {
createComponent({}, { query: canApproveResponse });
diff --git a/spec/frontend/vue_merge_request_widget/components/checks/conflicts_spec.js b/spec/frontend/vue_merge_request_widget/components/checks/conflicts_spec.js
index 57dcd2fd819..ad7c14ddae6 100644
--- a/spec/frontend/vue_merge_request_widget/components/checks/conflicts_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/checks/conflicts_spec.js
@@ -12,7 +12,7 @@ let wrapper;
let apolloProvider;
function factory({
- result = 'passed',
+ status = 'success',
canMerge = true,
pushToSourceBranch = true,
shouldBeRebased = false,
@@ -42,7 +42,7 @@ function factory({
apolloProvider,
propsData: {
mr,
- check: { result, failureReason: 'Conflicts message' },
+ check: { status, identifier: 'CONFLICT' },
},
});
}
@@ -55,7 +55,7 @@ describe('Merge request merge checks conflicts component', () => {
it('renders failure reason text', () => {
factory();
- expect(wrapper.text()).toEqual('Conflicts message');
+ expect(wrapper.text()).toEqual('Merge conflicts must be resolved.');
});
it.each`
@@ -74,7 +74,12 @@ describe('Merge request merge checks conflicts component', () => {
sourceBranchProtected,
rendersConflictButton,
}) => {
- factory({ mr: { conflictResolutionPath }, pushToSourceBranch, sourceBranchProtected });
+ factory({
+ status: 'FAILED',
+ mr: { conflictResolutionPath },
+ pushToSourceBranch,
+ sourceBranchProtected,
+ });
await waitForPromises();
diff --git a/spec/frontend/vue_merge_request_widget/components/checks/message_spec.js b/spec/frontend/vue_merge_request_widget/components/checks/message_spec.js
index 4446eb7324b..aeea34c29ce 100644
--- a/spec/frontend/vue_merge_request_widget/components/checks/message_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/checks/message_spec.js
@@ -12,18 +12,18 @@ function factory(propsData = {}) {
describe('Merge request merge checks message component', () => {
it('renders failure reason text', () => {
- factory({ check: { result: 'passed', failureReason: 'Failed message' } });
+ factory({ check: { status: 'success', identifier: 'discussions_not_resolved' } });
- expect(wrapper.text()).toEqual('Failed message');
+ expect(wrapper.text()).toEqual('Unresolved discussions must be resolved.');
});
it.each`
- result | icon
- ${'passed'} | ${'success'}
- ${'failed'} | ${'failed'}
- ${'allowed_to_fail'} | ${'neutral'}
- `('renders $icon icon for $result result', ({ result, icon }) => {
- factory({ check: { result, failureReason: 'Failed message' } });
+ status | icon
+ ${'success'} | ${'success'}
+ ${'failed'} | ${'failed'}
+ ${'inactive'} | ${'neutral'}
+ `('renders $icon icon for $status result', ({ status, icon }) => {
+ factory({ check: { status, identifier: 'discussions_not_resolved' } });
expect(wrapper.findComponent(StatusIcon).props('iconName')).toBe(icon);
});
diff --git a/spec/frontend/vue_merge_request_widget/components/checks/rebase_spec.js b/spec/frontend/vue_merge_request_widget/components/checks/rebase_spec.js
new file mode 100644
index 00000000000..d6c01aee3b1
--- /dev/null
+++ b/spec/frontend/vue_merge_request_widget/components/checks/rebase_spec.js
@@ -0,0 +1,323 @@
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlModal } from '@gitlab/ui';
+import MergeChecksRebase from '~/vue_merge_request_widget/components/checks/rebase.vue';
+import rebaseQuery from '~/vue_merge_request_widget/queries/states/rebase.query.graphql';
+import eventHub from '~/vue_merge_request_widget/event_hub';
+import toast from '~/vue_shared/plugins/global_toast';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { stubComponent } from 'helpers/stub_component';
+
+jest.mock('~/vue_shared/plugins/global_toast');
+
+let wrapper;
+const showMock = jest.fn();
+
+const mockPipelineNodes = [
+ {
+ id: '1',
+ project: {
+ id: '2',
+ fullPath: 'user/forked',
+ },
+ },
+];
+
+const mockQueryHandler = ({
+ rebaseInProgress = false,
+ targetBranch = '',
+ pushToSourceBranch = false,
+ nodes = mockPipelineNodes,
+} = {}) =>
+ jest.fn().mockResolvedValue({
+ data: {
+ project: {
+ id: '1',
+ mergeRequest: {
+ id: '2',
+ rebaseInProgress,
+ targetBranch,
+ userPermissions: {
+ pushToSourceBranch,
+ },
+ pipelines: {
+ nodes,
+ },
+ },
+ },
+ },
+ });
+
+const createMockApolloProvider = (handler) => {
+ Vue.use(VueApollo);
+
+ return createMockApollo([[rebaseQuery, handler]]);
+};
+
+function createWrapper({ propsData = {}, provideData = {}, handler = mockQueryHandler() } = {}) {
+ wrapper = mountExtended(MergeChecksRebase, {
+ apolloProvider: createMockApolloProvider(handler),
+ provide: {
+ ...provideData,
+ },
+ propsData: {
+ mr: {},
+ service: {},
+ check: {
+ identifier: 'need_rebase',
+ status: 'FAILED',
+ },
+ ...propsData,
+ },
+ stubs: {
+ GlModal: stubComponent(GlModal, {
+ methods: {
+ show: showMock,
+ },
+ }),
+ },
+ });
+}
+
+describe('Merge request merge checks rebase component', () => {
+ const findStandardRebaseButton = () => wrapper.findByTestId('standard-rebase-button');
+ const findRebaseWithoutCiButton = () => wrapper.findByTestId('rebase-without-ci-button');
+ const findModal = () => wrapper.findComponent(GlModal);
+
+ describe('with permissions', () => {
+ const rebaseMock = jest.fn().mockResolvedValue();
+ const pollMock = jest.fn().mockResolvedValue({});
+
+ describe('Rebase buttons', () => {
+ it('renders both buttons', async () => {
+ createWrapper({
+ handler: mockQueryHandler({ pushToSourceBranch: true }),
+ });
+
+ await waitForPromises();
+
+ expect(findRebaseWithoutCiButton().exists()).toBe(true);
+ expect(findStandardRebaseButton().exists()).toBe(true);
+ });
+
+ it('starts the rebase when clicking', async () => {
+ createWrapper({
+ propsData: {
+ service: {
+ rebase: rebaseMock,
+ poll: pollMock,
+ },
+ },
+ handler: mockQueryHandler({ pushToSourceBranch: true }),
+ });
+
+ await waitForPromises();
+
+ findStandardRebaseButton().vm.$emit('click');
+
+ expect(rebaseMock).toHaveBeenCalledWith({ skipCi: false });
+ });
+
+ it('starts the CI-skipping rebase when clicking on "Rebase without CI"', async () => {
+ createWrapper({
+ propsData: {
+ service: {
+ rebase: rebaseMock,
+ poll: pollMock,
+ },
+ },
+ handler: mockQueryHandler({ pushToSourceBranch: true }),
+ });
+
+ await waitForPromises();
+
+ findRebaseWithoutCiButton().vm.$emit('click');
+
+ expect(rebaseMock).toHaveBeenCalledWith({ skipCi: true });
+ });
+ });
+
+ describe('Rebase when pipelines must succeed is enabled', () => {
+ beforeEach(async () => {
+ createWrapper({
+ propsData: {
+ mr: {
+ onlyAllowMergeIfPipelineSucceeds: true,
+ },
+ service: {
+ rebase: rebaseMock,
+ poll: pollMock,
+ },
+ },
+ handler: mockQueryHandler({ pushToSourceBranch: true }),
+ });
+
+ await waitForPromises();
+ });
+
+ it('renders only the rebase button', () => {
+ expect(findRebaseWithoutCiButton().exists()).toBe(false);
+ expect(findStandardRebaseButton().exists()).toBe(true);
+ });
+
+ it('starts the rebase when clicking', async () => {
+ findStandardRebaseButton().vm.$emit('click');
+
+ await nextTick();
+
+ expect(rebaseMock).toHaveBeenCalledWith({ skipCi: false });
+ });
+ });
+
+ describe('Rebase when pipelines must succeed and skipped pipelines are considered successful are enabled', () => {
+ beforeEach(async () => {
+ createWrapper({
+ propsData: {
+ mr: {
+ onlyAllowMergeIfPipelineSucceeds: true,
+ allowMergeOnSkippedPipeline: true,
+ },
+ service: {
+ rebase: rebaseMock,
+ poll: pollMock,
+ },
+ },
+ handler: mockQueryHandler({ pushToSourceBranch: true }),
+ });
+
+ await waitForPromises();
+ });
+
+ it('renders both rebase buttons', () => {
+ expect(findRebaseWithoutCiButton().exists()).toBe(true);
+ expect(findStandardRebaseButton().exists()).toBe(true);
+ });
+
+ it('starts the rebase when clicking', async () => {
+ findStandardRebaseButton().vm.$emit('click');
+
+ await nextTick();
+
+ expect(rebaseMock).toHaveBeenCalledWith({ skipCi: false });
+ });
+
+ it('starts the CI-skipping rebase when clicking on "Rebase without CI"', async () => {
+ findRebaseWithoutCiButton().vm.$emit('click');
+
+ await nextTick();
+
+ expect(rebaseMock).toHaveBeenCalledWith({ skipCi: true });
+ });
+ });
+
+ describe('security modal', () => {
+ it('displays modal and rebases after confirming', async () => {
+ createWrapper({
+ propsData: {
+ mr: {
+ sourceProjectFullPath: 'user/forked',
+ targetProjectFullPath: 'root/original',
+ },
+ service: {
+ rebase: rebaseMock,
+ poll: pollMock,
+ },
+ },
+ provideData: { canCreatePipelineInTargetProject: true },
+ handler: mockQueryHandler({ pushToSourceBranch: true }),
+ });
+
+ await waitForPromises();
+
+ findStandardRebaseButton().vm.$emit('click');
+ expect(showMock).toHaveBeenCalled();
+
+ findModal().vm.$emit('primary');
+
+ expect(rebaseMock).toHaveBeenCalled();
+ });
+
+ it('does not display modal', async () => {
+ createWrapper({
+ propsData: {
+ mr: {
+ sourceProjectFullPath: 'user/forked',
+ targetProjectFullPath: 'root/original',
+ },
+ service: {
+ rebase: rebaseMock,
+ poll: pollMock,
+ },
+ },
+ provideData: { canCreatePipelineInTargetProject: false },
+ handler: mockQueryHandler({ pushToSourceBranch: true }),
+ });
+
+ await waitForPromises();
+
+ findStandardRebaseButton().vm.$emit('click');
+
+ expect(showMock).not.toHaveBeenCalled();
+ expect(rebaseMock).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('without permissions', () => {
+ const exampleTargetBranch = 'fake-branch-to-test-with';
+
+ it('does render the "Rebase without pipeline" button', async () => {
+ createWrapper({
+ handler: mockQueryHandler({
+ rebaseInProgress: false,
+ pushToSourceBranch: false,
+ targetBranch: exampleTargetBranch,
+ }),
+ });
+
+ await waitForPromises();
+
+ expect(findRebaseWithoutCiButton().exists()).toBe(true);
+ });
+ });
+
+ describe('methods', () => {
+ it('checkRebaseStatus', async () => {
+ jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
+ createWrapper({
+ propsData: {
+ service: {
+ rebase() {
+ return Promise.resolve();
+ },
+ poll() {
+ return Promise.resolve({
+ data: {
+ rebase_in_progress: false,
+ should_be_rebased: false,
+ merge_error: null,
+ },
+ });
+ },
+ },
+ },
+ });
+
+ await waitForPromises();
+
+ findRebaseWithoutCiButton().vm.$emit('click');
+
+ // Wait for the rebase request
+ await nextTick();
+ // Wait for the polling request
+ await nextTick();
+ // Wait for the eventHub to be called
+ await nextTick();
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetRebaseSuccess');
+ expect(toast).toHaveBeenCalledWith('Rebase completed');
+ });
+ });
+});
diff --git a/spec/frontend/vue_merge_request_widget/components/checks/unresolved_discussions_spec.js b/spec/frontend/vue_merge_request_widget/components/checks/unresolved_discussions_spec.js
new file mode 100644
index 00000000000..fc83901b318
--- /dev/null
+++ b/spec/frontend/vue_merge_request_widget/components/checks/unresolved_discussions_spec.js
@@ -0,0 +1,49 @@
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import notesEventHub from '~/notes/event_hub';
+import MergeChecksUnresolvedDiscussions from '~/vue_merge_request_widget/components/checks/unresolved_discussions.vue';
+import MergeChecksMessage from '~/vue_merge_request_widget/components/checks/message.vue';
+
+describe('MergeChecksUnresolvedDiscussions component', () => {
+ let wrapper;
+
+ function createComponent(
+ propsData = {
+ check: {
+ status: 'FAILED',
+ failureReason: 'Failed message',
+ identifier: 'discussions_not_resolved',
+ },
+ },
+ ) {
+ wrapper = mountExtended(MergeChecksUnresolvedDiscussions, {
+ propsData,
+ });
+ }
+
+ it('passes check down to the MergeChecksMessage', () => {
+ const check = {
+ status: 'failed',
+ failureReason: 'Unresolved discussions',
+ identifier: 'discussions_not_resolved',
+ };
+ createComponent({ check });
+
+ expect(wrapper.findComponent(MergeChecksMessage).props('check')).toEqual(check);
+ });
+
+ it('does not show go to first unresolved discussion button with passed state', () => {
+ createComponent({ check: { status: 'success', identifier: 'discussions_not_resolved' } });
+ const button = wrapper.findByRole('button', { name: 'Go to first unresolved thread' });
+ expect(button.exists()).toBe(false);
+ });
+
+ it('triggers go to first discussion action', () => {
+ const callback = jest.fn();
+ notesEventHub.$on('jumpToFirstUnresolvedDiscussion', callback);
+ createComponent();
+
+ wrapper.findByRole('button', { name: 'Go to first unresolved thread' }).trigger('click');
+
+ expect(callback).toHaveBeenCalled();
+ });
+});
diff --git a/spec/frontend/vue_merge_request_widget/components/merge_checks_spec.js b/spec/frontend/vue_merge_request_widget/components/merge_checks_spec.js
index c86fe6d0a10..d39098b27c2 100644
--- a/spec/frontend/vue_merge_request_widget/components/merge_checks_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/merge_checks_spec.js
@@ -1,18 +1,22 @@
import VueApollo from 'vue-apollo';
import Vue from 'vue';
-import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import MergeChecksComponent from '~/vue_merge_request_widget/components/merge_checks.vue';
import mergeChecksQuery from '~/vue_merge_request_widget/queries/merge_checks.query.graphql';
import StatusIcon from '~/vue_merge_request_widget/components/extensions/status_icon.vue';
+import StateContainer from '~/vue_merge_request_widget/components/state_container.vue';
+import { COMPONENTS } from '~/vue_merge_request_widget/components/checks/constants';
+import conflictsStateQuery from '~/vue_merge_request_widget/queries/states/conflicts.query.graphql';
+import rebaseStateQuery from '~/vue_merge_request_widget/queries/states/rebase.query.graphql';
Vue.use(VueApollo);
let wrapper;
let apolloProvider;
-function factory({ canMerge = true, mergeChecks = [] } = {}) {
+function factory(mountFn, { canMerge = true, mergeabilityChecks = [] } = {}) {
apolloProvider = createMockApollo([
[
mergeChecksQuery,
@@ -20,28 +24,79 @@ function factory({ canMerge = true, mergeChecks = [] } = {}) {
data: {
project: {
id: 1,
- mergeRequest: { id: 1, userPermissions: { canMerge }, mergeChecks },
+ mergeRequest: { id: 1, userPermissions: { canMerge }, mergeabilityChecks },
},
},
}),
],
+ [
+ conflictsStateQuery,
+ () =>
+ Promise.resolve({
+ data: {
+ project: {
+ id: 1,
+ mergeRequest: {
+ id: 1,
+ shouldBeRebased: false,
+ sourceBranchProtected: false,
+ userPermissions: { pushToSourceBranch: true },
+ },
+ },
+ },
+ }),
+ ],
+ [
+ rebaseStateQuery,
+ () =>
+ Promise.resolve({
+ data: {
+ project: {
+ id: '1',
+ mergeRequest: {
+ id: '2',
+ rebaseInProgress: false,
+ targetBranch: 'main',
+ userPermissions: {
+ pushToSourceBranch: true,
+ },
+ pipelines: {
+ nodes: [
+ {
+ id: '1',
+ project: {
+ id: '2',
+ fullPath: 'gitlab/gitlab',
+ },
+ },
+ ],
+ },
+ },
+ },
+ },
+ }),
+ ],
]);
- wrapper = mountExtended(MergeChecksComponent, {
+ wrapper = mountFn(MergeChecksComponent, {
apolloProvider,
propsData: {
mr: {},
+ service: {},
},
});
}
+const mountComponent = factory.bind(null, mountExtended);
+const shallowMountComponent = factory.bind(null, shallowMountExtended);
+
describe('Merge request merge checks component', () => {
afterEach(() => {
apolloProvider = null;
});
it('renders ready to merge text if user can merge', async () => {
- factory({ canMerge: true });
+ mountComponent({ canMerge: true });
await waitForPromises();
@@ -49,7 +104,7 @@ describe('Merge request merge checks component', () => {
});
it('renders ready to merge by members text if user can not merge', async () => {
- factory({ canMerge: false });
+ mountComponent({ canMerge: false });
await waitForPromises();
@@ -57,11 +112,11 @@ describe('Merge request merge checks component', () => {
});
it.each`
- mergeChecks | text
- ${[{ identifier: 'discussions', result: 'failed' }]} | ${'Merge blocked: 1 check failed'}
- ${[{ identifier: 'discussions', result: 'failed' }, { identifier: 'rebase', result: 'failed' }]} | ${'Merge blocked: 2 checks failed'}
- `('renders $text for $mergeChecks', async ({ mergeChecks, text }) => {
- factory({ mergeChecks });
+ mergeabilityChecks | text
+ ${[{ identifier: 'discussions', status: 'failed' }]} | ${'Merge blocked: 1 check failed'}
+ ${[{ identifier: 'discussions', status: 'failed' }, { identifier: 'rebase', status: 'failed' }]} | ${'Merge blocked: 2 checks failed'}
+ `('renders $text for $mergeabilityChecks', async ({ mergeabilityChecks, text }) => {
+ mountComponent({ mergeabilityChecks });
await waitForPromises();
@@ -69,19 +124,37 @@ describe('Merge request merge checks component', () => {
});
it.each`
- result | statusIcon
+ status | statusIcon
${'failed'} | ${'failed'}
${'passed'} | ${'success'}
- `('renders $statusIcon for $result result', async ({ result, statusIcon }) => {
- factory({ mergeChecks: [{ result, identifier: 'discussions' }] });
+ `('renders $statusIcon for $status result', async ({ status, statusIcon }) => {
+ mountComponent({ mergeabilityChecks: [{ status, identifier: 'discussions' }] });
await waitForPromises();
expect(wrapper.findComponent(StatusIcon).props('iconName')).toBe(statusIcon);
});
+ it.each`
+ identifier
+ ${'conflict'}
+ ${'unresolved_discussions'}
+ ${'need_rebase'}
+ ${'default'}
+ `('renders $identifier merge check', async ({ identifier }) => {
+ shallowMountComponent({ mergeabilityChecks: [{ status: 'failed', identifier }] });
+
+ wrapper.findComponent(StateContainer).vm.$emit('toggle');
+
+ await waitForPromises();
+
+ const { default: component } = await COMPONENTS[identifier]();
+
+ expect(wrapper.findComponent(component).exists()).toBe(true);
+ });
+
it('expands collapsed area', async () => {
- factory();
+ mountComponent();
await waitForPromises();
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js
index 48b86d879ad..9239807ae71 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js
@@ -831,4 +831,16 @@ describe('ReadyToMerge', () => {
});
});
});
+
+ describe('merge details', () => {
+ it('shows auto-merge hint when auto merge is set and some checks have failed', () => {
+ createComponent({ mr: { state: 'mergeChecksFailed', autoMergeEnabled: true } });
+ expect(wrapper.text()).toContain('Auto-merge enabled');
+ });
+
+ it("doesn't show auto-merge hint when auto merge is not set", () => {
+ createComponent({ mr: { autoMergeEnabled: false } });
+ expect(wrapper.text()).not.toContain('Auto-merge enabled');
+ });
+ });
});
diff --git a/spec/frontend/vue_merge_request_widget/components/widget/app_spec.js b/spec/frontend/vue_merge_request_widget/components/widget/app_spec.js
index 205824c3edd..1fc3b0c84ee 100644
--- a/spec/frontend/vue_merge_request_widget/components/widget/app_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/widget/app_spec.js
@@ -5,6 +5,7 @@ import MrSecurityWidgetCE from '~/vue_merge_request_widget/extensions/security_r
import MrTestReportWidget from '~/vue_merge_request_widget/extensions/test_report/index.vue';
import MrTerraformWidget from '~/vue_merge_request_widget/extensions/terraform/index.vue';
import MrCodeQualityWidget from '~/vue_merge_request_widget/extensions/code_quality/index.vue';
+import MrAccessibilityWidget from '~/vue_merge_request_widget/extensions/accessibility/index.vue';
describe('MR Widget App', () => {
let wrapper;
@@ -38,10 +39,11 @@ describe('MR Widget App', () => {
});
describe.each`
- widgetName | widget | endpoint
- ${'testReportWidget'} | ${MrTestReportWidget} | ${'testResultsPath'}
- ${'terraformPlansWidget'} | ${MrTerraformWidget} | ${'terraformReportsPath'}
- ${'codeQualityWidget'} | ${MrCodeQualityWidget} | ${'codequalityReportsPath'}
+ widgetName | widget | endpoint
+ ${'testReportWidget'} | ${MrTestReportWidget} | ${'testResultsPath'}
+ ${'terraformPlansWidget'} | ${MrTerraformWidget} | ${'terraformReportsPath'}
+ ${'codeQualityWidget'} | ${MrCodeQualityWidget} | ${'codequalityReportsPath'}
+ ${'accessibilityWidget'} | ${MrAccessibilityWidget} | ${'accessibilityReportPath'}
`('$widgetName', ({ widget, endpoint }) => {
it(`is mounted when ${endpoint} is defined`, async () => {
createComponent({ mr: { [endpoint]: `path/to/${endpoint}` } });
diff --git a/spec/frontend/vue_merge_request_widget/components/widget/dynamic_content_spec.js b/spec/frontend/vue_merge_request_widget/components/widget/dynamic_content_spec.js
index 16751bcc0f0..213959fe4e2 100644
--- a/spec/frontend/vue_merge_request_widget/components/widget/dynamic_content_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/widget/dynamic_content_spec.js
@@ -2,6 +2,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { EXTENSION_ICONS } from '~/vue_merge_request_widget/constants';
import DynamicContent from '~/vue_merge_request_widget/components/widget/dynamic_content.vue';
import ContentRow from '~/vue_merge_request_widget/components/widget/widget_content_row.vue';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
describe('~/vue_merge_request_widget/components/widget/dynamic_content.vue', () => {
let wrapper;
@@ -16,10 +17,13 @@ describe('~/vue_merge_request_widget/components/widget/dynamic_content.vue', ()
DynamicContent,
ContentRow,
},
+ directives: {
+ GlTooltip: createMockDirective('gl-tooltip'),
+ },
});
};
- it('renders given data', () => {
+ beforeEach(() => {
createComponent({
propsData: {
data: {
@@ -49,10 +53,23 @@ describe('~/vue_merge_request_widget/components/widget/dynamic_content.vue', ()
text: 'This is recursive. It will be listed in level 3.',
},
],
+ tooltipText: 'Tooltip text',
},
},
});
+ });
+ it('renders given data', () => {
expect(wrapper.html()).toMatchSnapshot();
});
+
+ it('has a tooltip on the row text', () => {
+ const text = wrapper.findByText('Main text for the row');
+ const tooltip = getBinding(text.element, 'gl-tooltip');
+
+ expect(tooltip.value).toMatchObject({
+ title: 'Tooltip text',
+ boundary: 'viewport',
+ });
+ });
});
diff --git a/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js b/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js
index 18fdba32f52..87c1ad7947e 100644
--- a/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js
@@ -1,5 +1,5 @@
import { nextTick } from 'vue';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
import HelpPopover from '~/vue_shared/components/help_popover.vue';
import waitForPromises from 'helpers/wait_for_promises';
diff --git a/spec/frontend/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports_spec.js b/spec/frontend/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports_spec.js
index e23cd92f53e..b277a9f6716 100644
--- a/spec/frontend/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports_spec.js
+++ b/spec/frontend/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports_spec.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import { GlDropdown } from '@gitlab/ui';
+import { GlDisclosureDropdown } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import MRSecurityWidget from '~/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports.vue';
import Widget from '~/vue_merge_request_widget/components/widget/widget.vue';
@@ -27,8 +27,7 @@ describe('vue_merge_request_widget/extensions/security_reports/mr_widget_securit
};
const findWidget = () => wrapper.findComponent(Widget);
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findDropdownItem = (name) => wrapper.findByTestId(name);
+ const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
describe('with data', () => {
beforeEach(async () => {
@@ -55,24 +54,52 @@ describe('vue_merge_request_widget/extensions/security_reports/mr_widget_securit
});
it.each`
- artifactName | exists | downloadPath
- ${'sam_scan'} | ${true} | ${'/root/security-reports/-/jobs/14/artifacts/download?file_type=sast'}
- ${'sast-spotbugs'} | ${true} | ${'/root/security-reports/-/jobs/11/artifacts/download?file_type=sast'}
- ${'sast-sobelow'} | ${false} | ${''}
- ${'sast-pmd-apex'} | ${false} | ${''}
- ${'sast-eslint'} | ${true} | ${'/root/security-reports/-/jobs/8/artifacts/download?file_type=sast'}
- ${'secrets'} | ${true} | ${'/root/security-reports/-/jobs/7/artifacts/download?file_type=secret_detection'}
+ artifactName | downloadPath
+ ${'sam_scan'} | ${'/root/security-reports/-/jobs/14/artifacts/download?file_type=sast'}
+ ${'sast-spotbugs'} | ${'/root/security-reports/-/jobs/11/artifacts/download?file_type=sast'}
+ ${'sast-eslint'} | ${'/root/security-reports/-/jobs/8/artifacts/download?file_type=sast'}
+ ${'secrets'} | ${'/root/security-reports/-/jobs/7/artifacts/download?file_type=secret_detection'}
`(
- 'has a dropdown to download $artifactName artifacts',
- ({ artifactName, exists, downloadPath }) => {
+ 'has a dropdown item to download $artifactName artifacts',
+ ({ artifactName, downloadPath }) => {
expect(findDropdown().exists()).toBe(true);
- expect(wrapper.findByText(`Download ${artifactName}`).exists()).toBe(exists);
- if (exists) {
- const dropdownItem = findDropdownItem(`download-${artifactName}`);
- expect(dropdownItem.attributes('download')).toBe('');
- expect(dropdownItem.attributes('href')).toBe(downloadPath);
- }
+ expect(findDropdown().props('items')).toEqual(
+ expect.arrayContaining([
+ {
+ href: downloadPath,
+ text: `Download ${artifactName}`,
+ extraAttrs: {
+ download: '',
+ rel: 'nofollow',
+ },
+ },
+ ]),
+ );
+ },
+ );
+
+ it.each`
+ artifactName | downloadPath
+ ${'sast-sobelow'} | ${''}
+ ${'sast-pmd-apex'} | ${''}
+ `(
+ 'does not have a dropdown item to download $artifactName artifacts',
+ ({ artifactName, downloadPath }) => {
+ expect(findDropdown().exists()).toBe(true);
+
+ expect(findDropdown().props('items')).not.toEqual(
+ expect.arrayContaining([
+ {
+ href: downloadPath,
+ text: `Download ${artifactName}`,
+ extraAttrs: {
+ download: '',
+ rel: 'nofollow',
+ },
+ },
+ ]),
+ );
},
);
});
diff --git a/spec/frontend/vue_merge_request_widget/extentions/accessibility/index_spec.js b/spec/frontend/vue_merge_request_widget/extentions/accessibility/index_spec.js
index 9b1e694d9c4..baeab1641d2 100644
--- a/spec/frontend/vue_merge_request_widget/extentions/accessibility/index_spec.js
+++ b/spec/frontend/vue_merge_request_widget/extentions/accessibility/index_spec.js
@@ -3,29 +3,25 @@ import { mountExtended } from 'helpers/vue_test_utils_helper';
import { trimText } from 'helpers/text_helper';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
-import extensionsContainer from '~/vue_merge_request_widget/components/extensions/container';
-import { registerExtension } from '~/vue_merge_request_widget/components/extensions';
-import accessibilityExtension from '~/vue_merge_request_widget/extensions/accessibility';
+import AccessibilityWidget from '~/vue_merge_request_widget/extensions/accessibility/index.vue';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { accessibilityReportResponseErrors, accessibilityReportResponseSuccess } from './mock_data';
-describe('Accessibility extension', () => {
+describe('Accessibility widget', () => {
let wrapper;
let mock;
- registerExtension(accessibilityExtension);
-
const endpoint = '/root/repo/-/merge_requests/4/accessibility_reports.json';
const mockApi = (statusCode, data) => {
- mock.onGet(endpoint).reply(statusCode, data);
+ mock.onGet(endpoint).reply(statusCode, data, {});
};
const findToggleCollapsedButton = () => wrapper.findByTestId('toggle-button');
const findAllExtensionListItems = () => wrapper.findAllByTestId('extension-list-item');
const createComponent = () => {
- wrapper = mountExtended(extensionsContainer, {
+ wrapper = mountExtended(AccessibilityWidget, {
propsData: {
mr: {
accessibilityReportPath: endpoint,
diff --git a/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js b/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js
index eb3d624dc04..9296e548081 100644
--- a/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js
+++ b/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js
@@ -3,13 +3,13 @@ import MockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { createMockSubscription as createMockApolloSubscription } from 'mock-apollo-client';
-import * as Sentry from '@sentry/browser';
import approvedByCurrentUser from 'test_fixtures/graphql/merge_requests/approvals/approvals.query.graphql.json';
import getStateQueryResponse from 'test_fixtures/graphql/merge_requests/get_state.query.graphql.json';
import readyToMergeResponse from 'test_fixtures/graphql/merge_requests/states/ready_to_merge.query.graphql.json';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import api from '~/api';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_OK, HTTP_STATUS_NO_CONTENT } from '~/lib/utils/http_status';
@@ -63,8 +63,8 @@ jest.mock('~/smart_interval');
jest.mock('~/lib/utils/favicon');
-jest.mock('@sentry/browser', () => ({
- ...jest.requireActual('@sentry/browser'),
+jest.mock('~/sentry/sentry_browser_wrapper', () => ({
+ ...jest.requireActual('~/sentry/sentry_browser_wrapper'),
captureException: jest.fn(),
}));
diff --git a/spec/frontend/vue_shared/components/ci_badge_link_spec.js b/spec/frontend/vue_shared/components/ci_badge_link_spec.js
deleted file mode 100644
index e1660225a5c..00000000000
--- a/spec/frontend/vue_shared/components/ci_badge_link_spec.js
+++ /dev/null
@@ -1,158 +0,0 @@
-import { GlBadge } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
-
-jest.mock('~/lib/utils/url_utility', () => ({
- visitUrl: jest.fn(),
-}));
-
-describe('CI Badge Link Component', () => {
- let wrapper;
-
- const statuses = {
- canceled: {
- text: 'canceled',
- label: 'canceled',
- group: 'canceled',
- icon: 'status_canceled',
- details_path: 'status/canceled',
- },
- created: {
- text: 'created',
- label: 'created',
- group: 'created',
- icon: 'status_created',
- details_path: 'status/created',
- },
- failed: {
- text: 'failed',
- label: 'failed',
- group: 'failed',
- icon: 'status_failed',
- details_path: 'status/failed',
- },
- manual: {
- text: 'manual',
- label: 'manual action',
- group: 'manual',
- icon: 'status_manual',
- details_path: 'status/manual',
- },
- pending: {
- text: 'pending',
- label: 'pending',
- group: 'pending',
- icon: 'status_pending',
- details_path: 'status/pending',
- },
- preparing: {
- text: 'preparing',
- label: 'preparing',
- group: 'preparing',
- icon: 'status_preparing',
- details_path: 'status/preparing',
- },
- running: {
- text: 'running',
- label: 'running',
- group: 'running',
- icon: 'status_running',
- details_path: 'status/running',
- },
- scheduled: {
- text: 'scheduled',
- label: 'scheduled',
- group: 'scheduled',
- icon: 'status_scheduled',
- details_path: 'status/scheduled',
- },
- skipped: {
- text: 'skipped',
- label: 'skipped',
- group: 'skipped',
- icon: 'status_skipped',
- details_path: 'status/skipped',
- },
- success_warining: {
- text: 'warning',
- label: 'passed with warnings',
- group: 'success-with-warnings',
- icon: 'status_warning',
- details_path: 'status/warning',
- },
- success: {
- text: 'passed',
- label: 'passed',
- group: 'passed',
- icon: 'status_success',
- details_path: 'status/passed',
- },
- };
-
- const findIcon = () => wrapper.findComponent(CiIcon);
- const findBadge = () => wrapper.findComponent(GlBadge);
- const findBadgeText = () => wrapper.find('[data-testid="ci-badge-text"');
-
- const createComponent = (propsData) => {
- wrapper = shallowMount(CiBadgeLink, { propsData });
- };
-
- it.each(Object.keys(statuses))('should render badge for status: %s', (status) => {
- createComponent({ status: statuses[status] });
-
- expect(wrapper.attributes('href')).toBe(statuses[status].details_path);
- expect(wrapper.text()).toBe(statuses[status].text);
- expect(findBadge().props('size')).toBe('md');
- expect(findIcon().exists()).toBe(true);
- });
-
- it.each`
- status | textColor | variant
- ${statuses.success} | ${'gl-text-green-700'} | ${'success'}
- ${statuses.success_warining} | ${'gl-text-orange-700'} | ${'warning'}
- ${statuses.failed} | ${'gl-text-red-700'} | ${'danger'}
- ${statuses.running} | ${'gl-text-blue-700'} | ${'info'}
- ${statuses.pending} | ${'gl-text-orange-700'} | ${'warning'}
- ${statuses.preparing} | ${'gl-text-gray-600'} | ${'muted'}
- ${statuses.canceled} | ${'gl-text-gray-700'} | ${'neutral'}
- ${statuses.scheduled} | ${'gl-text-gray-600'} | ${'muted'}
- ${statuses.skipped} | ${'gl-text-gray-600'} | ${'muted'}
- ${statuses.manual} | ${'gl-text-gray-700'} | ${'neutral'}
- ${statuses.created} | ${'gl-text-gray-600'} | ${'muted'}
- `(
- 'should contain correct badge class and variant for status: $status.text',
- ({ status, textColor, variant }) => {
- createComponent({ status });
-
- expect(findBadgeText().classes()).toContain(textColor);
- expect(findBadge().props('variant')).toBe(variant);
- },
- );
-
- it('should not render label', () => {
- createComponent({ status: statuses.canceled, showText: false });
-
- expect(wrapper.text()).toBe('');
- });
-
- it('should emit ciStatusBadgeClick event', () => {
- createComponent({ status: statuses.success });
-
- findBadge().vm.$emit('click');
-
- expect(wrapper.emitted('ciStatusBadgeClick')).toEqual([[]]);
- });
-
- it('should render dynamic badge size', () => {
- createComponent({ status: statuses.success, size: 'lg' });
-
- expect(findBadge().props('size')).toBe('lg');
- });
-
- it('should have class `gl-px-2` when `showText` is false', () => {
- createComponent({ status: statuses.success, size: 'md', showText: false });
-
- expect(findBadge().classes()).toContain('gl-px-2');
- });
-});
diff --git a/spec/frontend/vue_shared/components/ci_icon_spec.js b/spec/frontend/vue_shared/components/ci_icon_spec.js
index c907b776b91..cbb725bf9e6 100644
--- a/spec/frontend/vue_shared/components/ci_icon_spec.js
+++ b/spec/frontend/vue_shared/components/ci_icon_spec.js
@@ -2,92 +2,135 @@ import { GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
+const mockStatus = {
+ group: 'success',
+ icon: 'status_success',
+ text: 'Success',
+};
+
describe('CI Icon component', () => {
let wrapper;
- const createComponent = (props) => {
+ const createComponent = ({ props = {} } = {}) => {
wrapper = shallowMount(CiIcon, {
propsData: {
+ status: mockStatus,
...props,
},
});
};
- it('should render a span element with an svg', () => {
- createComponent({
- status: {
- group: 'success',
- icon: 'status_success',
- },
- });
+ const findIcon = () => wrapper.findComponent(GlIcon);
- expect(wrapper.find('span').exists()).toBe(true);
- expect(wrapper.findComponent(GlIcon).exists()).toBe(true);
+ it('should render a span element and an icon', () => {
+ createComponent();
+
+ expect(wrapper.attributes('size')).toBe('md');
+ expect(findIcon().exists()).toBe(true);
});
describe.each`
- isActive
- ${true}
- ${false}
- `('when isActive is $isActive', ({ isActive }) => {
- it(`"active" class is ${isActive ? 'not ' : ''}added`, () => {
- wrapper = shallowMount(CiIcon, {
- propsData: {
+ showStatusText | showTooltip | expectedText | expectedTooltip | expectedAriaLabel
+ ${true} | ${true} | ${'Success'} | ${undefined} | ${undefined}
+ ${true} | ${false} | ${'Success'} | ${undefined} | ${undefined}
+ ${false} | ${true} | ${''} | ${'Success'} | ${'Success'}
+ ${false} | ${false} | ${''} | ${undefined} | ${'Success'}
+ `(
+ 'when showStatusText is %{showStatusText} and showTooltip is %{showTooltip}',
+ ({ showStatusText, showTooltip, expectedText, expectedTooltip, expectedAriaLabel }) => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ showStatusText,
+ showTooltip,
+ },
+ });
+ });
+
+ it(`aria-label is ${expectedAriaLabel}`, () => {
+ expect(wrapper.attributes('aria-label')).toBe(expectedAriaLabel);
+ });
+
+ it(`text shown is ${expectedAriaLabel}`, () => {
+ expect(wrapper.text()).toBe(expectedText);
+ });
+
+ it(`tooltip shown is ${expectedAriaLabel}`, () => {
+ expect(wrapper.attributes('title')).toBe(expectedTooltip);
+ });
+ },
+ );
+
+ describe('when appearing as a link', () => {
+ it('shows a GraphQL path', () => {
+ createComponent({
+ props: {
status: {
- group: 'success',
- icon: 'status_success',
+ ...mockStatus,
+ detailsPath: '/path',
},
- isActive,
+ useLink: true,
},
});
- expect(wrapper.classes('active')).toBe(isActive);
+ expect(wrapper.attributes('href')).toBe('/path');
});
- });
- describe.each`
- isInteractive
- ${true}
- ${false}
- `('when isInteractive is $isInteractive', ({ isInteractive }) => {
- it(`"interactive" class is ${isInteractive ? 'not ' : ''}added`, () => {
- wrapper = shallowMount(CiIcon, {
- propsData: {
+ it('shows a REST API path', () => {
+ createComponent({
+ props: {
status: {
- group: 'success',
- icon: 'status_success',
+ ...mockStatus,
+ details_path: '/path',
},
- isInteractive,
+ useLink: true,
+ },
+ });
+
+ expect(wrapper.attributes('href')).toBe('/path');
+ });
+
+ it('shows no path', () => {
+ createComponent({
+ status: {
+ detailsPath: '/path',
+ details_path: '/path',
+ },
+ props: {
+ useLink: false,
},
});
- expect(wrapper.classes('interactive')).toBe(isInteractive);
+ expect(wrapper.attributes('href')).toBe(undefined);
});
});
- describe('rendering a status', () => {
+ describe('rendering a status icon and class', () => {
it.each`
- icon | group | cssClass
- ${'status_success'} | ${'success'} | ${'ci-status-icon-success'}
- ${'status_failed'} | ${'failed'} | ${'ci-status-icon-failed'}
- ${'status_warning'} | ${'warning'} | ${'ci-status-icon-warning'}
- ${'status_pending'} | ${'pending'} | ${'ci-status-icon-pending'}
- ${'status_running'} | ${'running'} | ${'ci-status-icon-running'}
- ${'status_created'} | ${'created'} | ${'ci-status-icon-created'}
- ${'status_skipped'} | ${'skipped'} | ${'ci-status-icon-skipped'}
- ${'status_canceled'} | ${'canceled'} | ${'ci-status-icon-canceled'}
- ${'status_manual'} | ${'manual'} | ${'ci-status-icon-manual'}
- `('should render a $group status', ({ icon, group, cssClass }) => {
- wrapper = shallowMount(CiIcon, {
- propsData: {
+ icon | variant
+ ${'status_success'} | ${'success'}
+ ${'status_warning'} | ${'warning'}
+ ${'status_pending'} | ${'warning'}
+ ${'status_failed'} | ${'danger'}
+ ${'status_running'} | ${'info'}
+ ${'status_created'} | ${'neutral'}
+ ${'status_skipped'} | ${'neutral'}
+ ${'status_canceled'} | ${'neutral'}
+ ${'status_manual'} | ${'neutral'}
+ `('should render a $group status', ({ icon, variant }) => {
+ createComponent({
+ props: {
status: {
+ ...mockStatus,
icon,
- group,
},
+ showStatusText: true,
},
});
+ expect(wrapper.attributes('variant')).toBe(variant);
+ expect(wrapper.classes(`ci-icon-variant-${variant}`)).toBe(true);
- expect(wrapper.classes()).toContain(cssClass);
+ expect(findIcon().props('name')).toBe(`${icon}_borderless`);
});
});
});
diff --git a/spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js b/spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js
index 40232eb367a..810269257b6 100644
--- a/spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js
+++ b/spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js
@@ -1,15 +1,16 @@
import {
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
GlSprintf,
- GlDropdown,
- GlDropdownItem,
- GlDropdownText,
GlSearchBoxByType,
+ GlIcon,
} from '@gitlab/ui';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { nextTick } from 'vue';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import { stubComponent } from 'helpers/stub_component';
import DiffStatsDropdown, { i18n } from '~/vue_shared/components/diff_stats_dropdown.vue';
+import { ARROW_DOWN_KEY } from '~/lib/utils/keys';
jest.mock('fuzzaldrin-plus', () => ({
filter: jest.fn().mockReturnValue([]),
@@ -42,7 +43,7 @@ describe('Diff Stats Dropdown', () => {
const focusInputMock = jest.fn();
const createComponent = ({ changed = 0, added = 0, deleted = 0, files = [] } = {}) => {
- wrapper = shallowMountExtended(DiffStatsDropdown, {
+ wrapper = mountExtended(DiffStatsDropdown, {
propsData: {
changed,
added,
@@ -51,7 +52,6 @@ describe('Diff Stats Dropdown', () => {
},
stubs: {
GlSprintf,
- GlDropdown,
GlSearchBoxByType: stubComponent(GlSearchBoxByType, {
methods: { focusInput: focusInputMock },
}),
@@ -59,9 +59,8 @@ describe('Diff Stats Dropdown', () => {
});
};
- const findChanged = () => wrapper.findComponent(GlDropdown);
- const findChangedFiles = () => findChanged().findAllComponents(GlDropdownItem);
- const findNoFilesText = () => findChanged().findComponent(GlDropdownText);
+ const findChanged = () => wrapper.findComponent(GlDisclosureDropdown);
+ const findChangedFiles = () => findChanged().findAllComponents(GlDisclosureDropdownItem);
const findCollapsed = () => wrapper.findByTestId('diff-stats-additions-deletions-expanded');
const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
@@ -79,15 +78,14 @@ describe('Diff Stats Dropdown', () => {
const fileText = findChangedFiles().at(1).text();
expect(fileText).toContain(mockFiles[1].name);
expect(fileText).toContain(mockFiles[1].path);
- expect(fileData.props()).toMatchObject({
- iconName: mockFiles[1].icon,
- iconColor: mockFiles[1].iconColor,
- });
+ expect(fileData.findComponent(GlIcon).props('name')).toEqual(mockFiles[1].icon);
+ expect(fileData.findComponent(GlIcon).classes()).toContain('gl-text-red-500');
+ expect(fileData.find('a').attributes('href')).toEqual(mockFiles[1].href);
});
it('when no files changed', () => {
createComponent({ files: [] });
- expect(findNoFilesText().text()).toContain(i18n.noFilesFound);
+ expect(findChanged().text()).toContain(i18n.noFilesFound);
});
});
@@ -108,7 +106,7 @@ describe('Diff Stats Dropdown', () => {
});
it(`dropdown header should be '${expectedDropdownHeader}'`, () => {
- expect(findChanged().props('text')).toBe(expectedDropdownHeader);
+ expect(findChanged().props('toggleText')).toBe(expectedDropdownHeader);
});
it(`added and deleted count in collapsed section should be '${expectedAddedDeletedCollapsed}'`, () => {
@@ -137,27 +135,27 @@ describe('Diff Stats Dropdown', () => {
});
});
- describe('selecting file dropdown item', () => {
+ describe('on dropdown open', () => {
beforeEach(() => {
- createComponent({ files: mockFiles });
+ createComponent();
});
- it('updates the URL', () => {
- findChangedFiles().at(0).vm.$emit('click');
- expect(window.location.hash).toBe(mockFiles[0].href);
- findChangedFiles().at(1).vm.$emit('click');
- expect(window.location.hash).toBe(mockFiles[1].href);
+ it('should set the search input focus', () => {
+ findChanged().vm.$emit('shown');
+ expect(focusInputMock).toHaveBeenCalled();
});
});
- describe('on dropdown open', () => {
+ describe('keyboard nav', () => {
beforeEach(() => {
- createComponent();
+ createComponent({ files: mockFiles });
});
- it('should set the search input focus', () => {
- findChanged().vm.$emit('shown');
- expect(focusInputMock).toHaveBeenCalled();
+ it('focuses the first item when pressing the down key within the search box', () => {
+ const spy = jest.spyOn(wrapper.vm, 'focusFirstItem');
+ findSearchBox().vm.$emit('keydown', new KeyboardEvent({ key: ARROW_DOWN_KEY }));
+
+ expect(spy).toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/vue_shared/components/ensure_data_spec.js b/spec/frontend/vue_shared/components/ensure_data_spec.js
index 217e795bc64..399fe19ea3f 100644
--- a/spec/frontend/vue_shared/components/ensure_data_spec.js
+++ b/spec/frontend/vue_shared/components/ensure_data_spec.js
@@ -1,6 +1,6 @@
import { GlEmptyState } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
import { mount } from '@vue/test-utils';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import ensureData from '~/ensure_data';
const mockData = { message: 'Hello there' };
diff --git a/spec/frontend/vue_shared/components/entity_select/entity_select_spec.js b/spec/frontend/vue_shared/components/entity_select/entity_select_spec.js
index 36772ad03fe..1376133ec37 100644
--- a/spec/frontend/vue_shared/components/entity_select/entity_select_spec.js
+++ b/spec/frontend/vue_shared/components/entity_select/entity_select_spec.js
@@ -23,10 +23,13 @@ describe('EntitySelect', () => {
// Props
const label = 'label';
+ const description = 'description';
const inputName = 'inputName';
const inputId = 'inputId';
const headerText = 'headerText';
const defaultToggleText = 'defaultToggleText';
+ const toggleClass = 'foo-bar';
+ const block = true;
// Finders
const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
@@ -37,11 +40,14 @@ describe('EntitySelect', () => {
wrapper = shallowMountExtended(EntitySelect, {
propsData: {
label,
+ description,
inputName,
inputId,
headerText,
defaultToggleText,
fetchItems: fetchItemsMock,
+ toggleClass,
+ block,
...props,
},
stubs: {
@@ -65,6 +71,21 @@ describe('EntitySelect', () => {
fetchItemsMock = jest.fn().mockImplementation(() => ({ items: [itemMock], totalPages: 1 }));
});
+ describe('GlCollapsibleListbox props', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it.each`
+ prop | expectedValue
+ ${'block'} | ${block}
+ ${'toggleClass'} | ${toggleClass}
+ ${'headerText'} | ${headerText}
+ `('passes the $prop prop to GlCollapsibleListbox', ({ prop, expectedValue }) => {
+ expect(findListbox().props(prop)).toBe(expectedValue);
+ });
+ });
+
describe('on mount', () => {
it('calls the fetch function when the listbox is opened', async () => {
createComponent();
@@ -114,6 +135,12 @@ describe('EntitySelect', () => {
expect(wrapper.findByTestId(testid).exists()).toBe(true);
});
+ it('passes description prop to GlFormGroup', () => {
+ createComponent();
+
+ expect(wrapper.findComponent(GlFormGroup).attributes('description')).toBe(description);
+ });
+
describe('selection', () => {
it('uses the default toggle text while no group is selected', () => {
createComponent();
diff --git a/spec/frontend/vue_shared/components/entity_select/organization_select_spec.js b/spec/frontend/vue_shared/components/entity_select/organization_select_spec.js
new file mode 100644
index 00000000000..ea029ba4f27
--- /dev/null
+++ b/spec/frontend/vue_shared/components/entity_select/organization_select_spec.js
@@ -0,0 +1,179 @@
+import VueApollo from 'vue-apollo';
+import Vue, { nextTick } from 'vue';
+import { GlCollapsibleListbox } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import OrganizationSelect from '~/vue_shared/components/entity_select/organization_select.vue';
+import EntitySelect from '~/vue_shared/components/entity_select/entity_select.vue';
+import {
+ ORGANIZATION_TOGGLE_TEXT,
+ ORGANIZATION_HEADER_TEXT,
+ FETCH_ORGANIZATIONS_ERROR,
+ FETCH_ORGANIZATION_ERROR,
+} from '~/vue_shared/components/entity_select/constants';
+import resolvers from '~/organizations/shared/graphql/resolvers';
+import organizationsQuery from '~/organizations/index/graphql/organizations.query.graphql';
+import { organizations as organizationsMock } from '~/organizations/mock_data';
+import waitForPromises from 'helpers/wait_for_promises';
+import createMockApollo from 'helpers/mock_apollo_helper';
+
+Vue.use(VueApollo);
+
+jest.useFakeTimers();
+
+describe('OrganizationSelect', () => {
+ let wrapper;
+ let mockApollo;
+
+ // Mocks
+ const [organizationMock] = organizationsMock;
+
+ // Stubs
+ const GlAlert = {
+ template: '<div><slot /></div>',
+ };
+
+ // Props
+ const label = 'label';
+ const description = 'description';
+ const inputName = 'inputName';
+ const inputId = 'inputId';
+ const toggleClass = 'foo-bar';
+
+ // Finders
+ const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
+ const findEntitySelect = () => wrapper.findComponent(EntitySelect);
+ const findAlert = () => wrapper.findComponent(GlAlert);
+
+ const handleInput = jest.fn();
+
+ // Helpers
+ const createComponent = ({ props = {}, mockResolvers = resolvers, handlers } = {}) => {
+ mockApollo = createMockApollo(
+ handlers || [
+ [
+ organizationsQuery,
+ jest.fn().mockResolvedValueOnce({
+ data: { currentUser: { id: 1, organizations: { nodes: organizationsMock } } },
+ }),
+ ],
+ ],
+ mockResolvers,
+ );
+
+ wrapper = shallowMountExtended(OrganizationSelect, {
+ apolloProvider: mockApollo,
+ propsData: {
+ label,
+ description,
+ inputName,
+ inputId,
+ toggleClass,
+ ...props,
+ },
+ stubs: {
+ GlAlert,
+ EntitySelect,
+ },
+ listeners: {
+ input: handleInput,
+ },
+ });
+ };
+ const openListbox = () => findListbox().vm.$emit('shown');
+
+ afterEach(() => {
+ mockApollo = null;
+ });
+
+ describe('entity_select props', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it.each`
+ prop | expectedValue
+ ${'label'} | ${label}
+ ${'description'} | ${description}
+ ${'inputName'} | ${inputName}
+ ${'inputId'} | ${inputId}
+ ${'defaultToggleText'} | ${ORGANIZATION_TOGGLE_TEXT}
+ ${'headerText'} | ${ORGANIZATION_HEADER_TEXT}
+ ${'toggleClass'} | ${toggleClass}
+ `('passes the $prop prop to entity-select', ({ prop, expectedValue }) => {
+ expect(findEntitySelect().props(prop)).toBe(expectedValue);
+ });
+ });
+
+ describe('on mount', () => {
+ it('fetches organizations when the listbox is opened', async () => {
+ createComponent();
+ await nextTick();
+ jest.runAllTimers();
+ await waitForPromises();
+
+ openListbox();
+ jest.runAllTimers();
+ await waitForPromises();
+ expect(findListbox().props('items')).toEqual([
+ { text: organizationsMock[0].name, value: 1 },
+ { text: organizationsMock[1].name, value: 2 },
+ { text: organizationsMock[2].name, value: 3 },
+ ]);
+ });
+
+ describe('with an initial selection', () => {
+ it("fetches the initially selected value's name", async () => {
+ createComponent({ props: { initialSelection: organizationMock.id } });
+ await nextTick();
+ jest.runAllTimers();
+ await waitForPromises();
+
+ expect(findListbox().props('toggleText')).toBe(organizationMock.name);
+ });
+
+ it('show an error if fetching initially selected fails', async () => {
+ const mockResolvers = {
+ Query: {
+ organization: jest.fn().mockRejectedValueOnce(new Error()),
+ },
+ };
+
+ createComponent({ props: { initialSelection: organizationMock.id }, mockResolvers });
+ await nextTick();
+ jest.runAllTimers();
+
+ expect(findAlert().exists()).toBe(false);
+
+ await waitForPromises();
+
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlert().text()).toBe(FETCH_ORGANIZATION_ERROR);
+ });
+ });
+ });
+
+ it('shows an error when fetching organizations fails', async () => {
+ createComponent({
+ handlers: [[organizationsQuery, jest.fn().mockRejectedValueOnce(new Error())]],
+ });
+ await nextTick();
+ jest.runAllTimers();
+ await waitForPromises();
+
+ openListbox();
+ expect(findAlert().exists()).toBe(false);
+
+ jest.runAllTimers();
+ await waitForPromises();
+
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlert().text()).toBe(FETCH_ORGANIZATIONS_ERROR);
+ });
+
+ it('forwards events to the parent scope via `v-on="$listeners"`', () => {
+ createComponent();
+ findEntitySelect().vm.$emit('input');
+
+ expect(handleInput).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js
index 00a412d9de8..bb612a13209 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js
@@ -297,27 +297,31 @@ describe('FilteredSearchBarRoot', () => {
await nextTick();
});
- it('calls `uniqueTokens` on `filterValue` prop to remove duplicates', () => {
- wrapper.vm.handleFilterSubmit();
+ it('calls `uniqueTokens` on `filterValue` prop to remove duplicates', async () => {
+ findGlFilteredSearch().vm.$emit('submit');
+ await nextTick();
expect(uniqueTokens).toHaveBeenCalledWith(wrapper.vm.filterValue);
});
- it('calls `recentSearchesStore.addRecentSearch` with serialized value of provided `filters` param', () => {
+ it('calls `recentSearchesStore.addRecentSearch` with serialized value of provided `filters` param', async () => {
jest.spyOn(wrapper.vm.recentSearchesStore, 'addRecentSearch');
- wrapper.vm.handleFilterSubmit();
+ findGlFilteredSearch().vm.$emit('submit');
+ await nextTick();
return wrapper.vm.recentSearchesPromise.then(() => {
expect(wrapper.vm.recentSearchesStore.addRecentSearch).toHaveBeenCalledWith(mockFilters);
});
});
- it('calls `recentSearchesService.save` with array of searches', () => {
+ it('calls `recentSearchesService.save` with array of searches', async () => {
jest.spyOn(wrapper.vm.recentSearchesService, 'save');
wrapper.vm.handleFilterSubmit();
+ await nextTick();
+
return wrapper.vm.recentSearchesPromise.then(() => {
expect(wrapper.vm.recentSearchesService.save).toHaveBeenCalledWith([mockFilters]);
});
@@ -336,15 +340,16 @@ describe('FilteredSearchBarRoot', () => {
it('calls `blurSearchInput` method to remove focus from filter input field', () => {
jest.spyOn(wrapper.vm, 'blurSearchInput');
- wrapper.findComponent(GlFilteredSearch).vm.$emit('submit', mockFilters);
+ findGlFilteredSearch().vm.$emit('submit', mockFilters);
expect(wrapper.vm.blurSearchInput).toHaveBeenCalled();
});
- it('emits component event `onFilter` with provided filters param', () => {
+ it('emits component event `onFilter` with provided filters param', async () => {
jest.spyOn(wrapper.vm, 'removeQuotesEnclosure');
- wrapper.vm.handleFilterSubmit();
+ findGlFilteredSearch().vm.$emit('submit');
+ await nextTick();
expect(wrapper.emitted('onFilter')[0]).toEqual([mockFilters]);
expect(wrapper.vm.removeQuotesEnclosure).toHaveBeenCalledWith(mockFilters);
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js
index 72e3475df75..88618de6979 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js
@@ -88,6 +88,7 @@ function createComponent({
slots = defaultSlots,
scopedSlots = defaultScopedSlots,
mountFn = mount,
+ groupMultiSelectTokens = false,
} = {}) {
return mountFn(BaseToken, {
propsData: {
@@ -95,6 +96,9 @@ function createComponent({
...props,
},
provide: {
+ glFeatures: {
+ groupMultiSelectTokens,
+ },
portalName: 'fake target',
alignSuggestions: jest.fn(),
suggestionsListClass: () => 'custom-class',
@@ -148,6 +152,24 @@ describe('BaseToken', () => {
`"${mockRegularLabel.title}"`,
);
});
+
+ it('uses last item in list when value is an array', () => {
+ const mockGetActiveTokenValue = jest.fn();
+
+ wrapper = createComponent({
+ props: {
+ value: { data: mockLabels.map((l) => l.title) },
+ suggestions: mockLabels,
+ getActiveTokenValue: mockGetActiveTokenValue,
+ },
+ groupMultiSelectTokens: true,
+ });
+
+ const lastTitle = mockLabels[mockLabels.length - 1].title;
+
+ expect(mockGetActiveTokenValue).toHaveBeenCalledTimes(1);
+ expect(mockGetActiveTokenValue).toHaveBeenCalledWith(mockLabels, lastTitle);
+ });
});
});
@@ -385,6 +407,28 @@ describe('BaseToken', () => {
expect(setTokenValueToRecentlyUsed).not.toHaveBeenCalled();
});
+
+ it('emits token-selected event when groupMultiSelectTokens: true', () => {
+ wrapper = createComponent({
+ props: { suggestions: mockLabels },
+ groupMultiSelectTokens: true,
+ });
+
+ findGlFilteredSearchToken().vm.$emit('select', mockTokenValue.title);
+
+ expect(wrapper.emitted('token-selected')).toEqual([[mockTokenValue.title]]);
+ });
+
+ it('does not emit token-selected event when groupMultiSelectTokens: true', () => {
+ wrapper = createComponent({
+ props: { suggestions: mockLabels },
+ groupMultiSelectTokens: false,
+ });
+
+ findGlFilteredSearchToken().vm.$emit('select', mockTokenValue.title);
+
+ expect(wrapper.emitted('token-selected')).toBeUndefined();
+ });
});
});
@@ -476,6 +520,14 @@ describe('BaseToken', () => {
expect(wrapper.emitted('fetch-suggestions')[2]).toEqual(['foo']);
});
});
+
+ it('does not emit `fetch-suggestions` when value is array', () => {
+ expect(wrapper.emitted('fetch-suggestions')).toEqual([[''], ['']]);
+
+ findGlFilteredSearchToken().vm.$emit('input', { data: ['first item'] });
+
+ expect(wrapper.emitted('fetch-suggestions')).toEqual([[''], ['']]);
+ });
});
});
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/user_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/user_token_spec.js
index 0229d00eb91..4462d1bfaf5 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/user_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/user_token_spec.js
@@ -3,6 +3,7 @@ import {
GlFilteredSearchSuggestion,
GlDropdownDivider,
GlAvatar,
+ GlIcon,
} from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
@@ -53,6 +54,7 @@ function createComponent(options = {}) {
stubs = defaultStubs,
data = {},
listeners = {},
+ groupMultiSelectTokens = false,
} = options;
return mount(UserToken, {
apolloProvider: mockApollo,
@@ -63,6 +65,9 @@ function createComponent(options = {}) {
cursorPosition: 'start',
},
provide: {
+ glFeatures: {
+ groupMultiSelectTokens,
+ },
portalName: 'fake target',
alignSuggestions: function fakeAlignSuggestions() {},
suggestionsListClass: () => 'custom-class',
@@ -82,6 +87,8 @@ describe('UserToken', () => {
let wrapper;
const findBaseToken = () => wrapper.findComponent(BaseToken);
+ const findSuggestions = () => wrapper.findAllComponents(GlFilteredSearchSuggestion);
+ const findIconAtSuggestion = (index) => findSuggestions().at(index).findComponent(GlIcon);
beforeEach(() => {
mock = new MockAdapter(axios);
@@ -303,6 +310,110 @@ describe('UserToken', () => {
expect(mockInput).toHaveBeenLastCalledWith([{ data: 'mockData', operator: '=' }]);
});
+ describe('multiSelect', () => {
+ it('renders check icons in suggestions when multiSelect is true', async () => {
+ wrapper = createComponent({
+ value: { data: [mockUsers[0].username, mockUsers[1].username], operator: '=' },
+ data: {
+ users: mockUsers,
+ },
+ config: { ...mockAuthorToken, multiSelect: true, initialUsers: mockUsers },
+ active: true,
+ stubs: { Portal: true },
+ groupMultiSelectTokens: true,
+ });
+
+ await activateSuggestionsList();
+
+ const suggestions = wrapper.findAllComponents(GlFilteredSearchSuggestion);
+
+ expect(findIconAtSuggestion(1).exists()).toBe(false);
+ expect(findIconAtSuggestion(2).props('name')).toBe('check');
+ expect(findIconAtSuggestion(3).props('name')).toBe('check');
+
+ // test for left padding on unchecked items (so alignment is correct)
+ expect(findIconAtSuggestion(4).exists()).toBe(false);
+ expect(suggestions.at(4).find('.gl-pl-6').exists()).toBe(true);
+ });
+
+ it('renders multiple users when multiSelect is true', async () => {
+ wrapper = createComponent({
+ value: { data: [mockUsers[0].username, mockUsers[1].username], operator: '=' },
+ data: {
+ users: mockUsers,
+ },
+ config: { ...mockAuthorToken, multiSelect: true, initialUsers: mockUsers },
+ groupMultiSelectTokens: true,
+ });
+
+ await nextTick();
+ const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment);
+
+ expect(tokenSegments).toHaveLength(3); // Author, =, "Administrator"
+
+ const tokenValue = tokenSegments.at(2);
+
+ const [user1, user2] = mockUsers;
+
+ expect(tokenValue.findAllComponents(GlAvatar).at(1).props('src')).toBe(
+ mockUsers[1].avatar_url,
+ );
+ expect(tokenValue.text()).toBe(`${user1.name},${user2.name}`);
+ });
+
+ it('adds new user to multi-select-values', () => {
+ wrapper = createComponent({
+ value: { data: [mockUsers[0].username], operator: '=' },
+ data: {
+ users: mockUsers,
+ },
+ config: { ...mockAuthorToken, multiSelect: true, initialUsers: mockUsers },
+ active: true,
+ groupMultiSelectTokens: true,
+ });
+
+ findBaseToken().vm.$emit('token-selected', mockUsers[1].username);
+
+ expect(findBaseToken().props().multiSelectValues).toEqual([
+ mockUsers[0].username,
+ mockUsers[1].username,
+ ]);
+ });
+
+ it('removes existing user from array', () => {
+ const initialUsers = [mockUsers[0].username, mockUsers[1].username];
+ wrapper = createComponent({
+ value: { data: initialUsers, operator: '=' },
+ data: {
+ users: mockUsers,
+ },
+ config: { ...mockAuthorToken, multiSelect: true, initialUsers: mockUsers },
+ active: true,
+ groupMultiSelectTokens: true,
+ });
+
+ findBaseToken().vm.$emit('token-selected', mockUsers[0].username);
+
+ expect(findBaseToken().props().multiSelectValues).toEqual([mockUsers[1].username]);
+ });
+
+ it('clears input field after token selected', () => {
+ wrapper = createComponent({
+ value: { data: [mockUsers[0].username, mockUsers[1].username], operator: '=' },
+ data: {
+ users: mockUsers,
+ },
+ config: { ...mockAuthorToken, multiSelect: true, initialUsers: mockUsers },
+ active: true,
+ groupMultiSelectTokens: true,
+ });
+
+ findBaseToken().vm.$emit('token-selected', 'test');
+
+ expect(wrapper.emitted('input')).toEqual([[{ operator: '=', data: '' }]]);
+ });
+ });
+
describe('when loading', () => {
beforeEach(() => {
wrapper = createComponent({
diff --git a/spec/frontend/vue_shared/components/form/errors_alert_spec.js b/spec/frontend/vue_shared/components/form/errors_alert_spec.js
new file mode 100644
index 00000000000..b7efe19378d
--- /dev/null
+++ b/spec/frontend/vue_shared/components/form/errors_alert_spec.js
@@ -0,0 +1,60 @@
+import { GlAlert } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import FormErrorsAlert from '~/vue_shared/components/form/errors_alert.vue';
+
+describe('FormErrorsAlert', () => {
+ let wrapper;
+
+ const defaultPropsData = {
+ errors: ['Foo', 'Bar', 'Baz'],
+ };
+
+ function createComponent({ propsData = {} } = {}) {
+ wrapper = shallowMount(FormErrorsAlert, {
+ propsData: {
+ ...defaultPropsData,
+ ...propsData,
+ },
+ });
+ }
+
+ const findAlert = () => wrapper.findComponent(GlAlert);
+
+ describe('when there are no errors', () => {
+ it('renders nothing', () => {
+ createComponent({ propsData: { errors: [] } });
+
+ expect(wrapper.html()).toBe('');
+ });
+ });
+
+ describe('when there is one error', () => {
+ it('renders correct title and message', () => {
+ createComponent({ propsData: { errors: ['Foo'] } });
+
+ expect(findAlert().props('title')).toBe('The form contains the following error:');
+ expect(findAlert().text()).toContain('Foo');
+ });
+ });
+
+ describe('when there are multiple errors', () => {
+ it('renders correct title and message', () => {
+ createComponent();
+
+ expect(findAlert().props('title')).toBe('The form contains the following errors:');
+ expect(findAlert().text()).toContain('Foo');
+ expect(findAlert().text()).toContain('Bar');
+ expect(findAlert().text()).toContain('Baz');
+ });
+ });
+
+ describe('when alert is dismissed', () => {
+ it('emits input event with empty array as payload', () => {
+ createComponent();
+
+ findAlert().vm.$emit('dismiss');
+
+ expect(wrapper.emitted('input')).toEqual([[[]]]);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js b/spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js
index 72a0eb98a07..b57643a1359 100644
--- a/spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js
+++ b/spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js
@@ -44,11 +44,11 @@ describe('InputCopyToggleVisibility', () => {
};
function expectInputToBeMasked() {
- expect(findFormInput().element.type).toBe('password');
+ expect(findFormInput().classes()).toContain('input-copy-show-disc');
}
function expectInputToBeRevealed() {
- expect(findFormInput().element.type).toBe('text');
+ expect(findFormInput().classes()).not.toContain('input-copy-show-disc');
expect(findFormInput().element.value).toBe(valueProp);
}
diff --git a/spec/frontend/vue_shared/components/list_selector/group_item_spec.js b/spec/frontend/vue_shared/components/list_selector/group_item_spec.js
new file mode 100644
index 00000000000..b59e4c734c1
--- /dev/null
+++ b/spec/frontend/vue_shared/components/list_selector/group_item_spec.js
@@ -0,0 +1,55 @@
+import { GlAvatar } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import GroupItem from '~/vue_shared/components/list_selector/group_item.vue';
+
+describe('GroupItem spec', () => {
+ let wrapper;
+
+ const MOCK_GROUP = { fullName: 'Group 1', name: 'group1', avatarUrl: 'some/avatar.jpg' };
+
+ const createComponent = (props) => {
+ wrapper = mountExtended(GroupItem, {
+ propsData: {
+ data: MOCK_GROUP,
+ ...props,
+ },
+ });
+ };
+
+ const findAvatar = () => wrapper.findComponent(GlAvatar);
+ const findDeleteButton = () => wrapper.findByRole('button', { fullName: 'Delete Group 1' });
+
+ beforeEach(() => createComponent());
+
+ it('renders an Avatar component', () => {
+ expect(findAvatar().props('size')).toBe(32);
+ expect(findAvatar().attributes()).toMatchObject({
+ src: MOCK_GROUP.avatarUrl,
+ alt: MOCK_GROUP.fullName,
+ });
+ });
+
+ it('renders a fullName and name', () => {
+ expect(wrapper.text()).toContain('Group 1');
+ expect(wrapper.text()).toContain('@group1');
+ });
+
+ it('does not render a delete button by default', () => {
+ expect(findDeleteButton().exists()).toBe(false);
+ });
+
+ describe('Delete button', () => {
+ beforeEach(() => createComponent({ canDelete: true }));
+
+ it('renders a delete button', () => {
+ expect(findDeleteButton().exists()).toBe(true);
+ expect(findDeleteButton().props('icon')).toBe('remove');
+ });
+
+ it('emits a delete event if the delete button is clicked', () => {
+ findDeleteButton().trigger('click');
+
+ expect(wrapper.emitted('delete')).toEqual([[MOCK_GROUP.name]]);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/list_selector/index_spec.js b/spec/frontend/vue_shared/components/list_selector/index_spec.js
new file mode 100644
index 00000000000..11e64a91eb0
--- /dev/null
+++ b/spec/frontend/vue_shared/components/list_selector/index_spec.js
@@ -0,0 +1,257 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlCard, GlIcon, GlCollapsibleListbox, GlSearchBoxByType } from '@gitlab/ui';
+import Api from '~/api';
+import { createAlert } from '~/alert';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import ListSelector from '~/vue_shared/components/list_selector/index.vue';
+import UserItem from '~/vue_shared/components/list_selector/user_item.vue';
+import GroupItem from '~/vue_shared/components/list_selector/group_item.vue';
+import groupsAutocompleteQuery from '~/graphql_shared/queries/groups_autocomplete.query.graphql';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { USERS_RESPONSE_MOCK, GROUPS_RESPONSE_MOCK } from './mock_data';
+
+jest.mock('~/alert');
+Vue.use(VueApollo);
+
+describe('List Selector spec', () => {
+ let wrapper;
+ let fakeApollo;
+
+ const USERS_MOCK_PROPS = {
+ title: 'Users',
+ projectPath: 'some/project/path',
+ groupPath: 'some/group/path',
+ type: 'users',
+ };
+
+ const GROUPS_MOCK_PROPS = {
+ title: 'Groups',
+ projectPath: 'some/project/path',
+ type: 'groups',
+ };
+
+ const groupsAutocompleteQuerySuccess = jest.fn().mockResolvedValue(GROUPS_RESPONSE_MOCK);
+
+ const createComponent = async (props) => {
+ fakeApollo = createMockApollo([[groupsAutocompleteQuery, groupsAutocompleteQuerySuccess]]);
+
+ wrapper = mountExtended(ListSelector, {
+ apolloProvider: fakeApollo,
+ propsData: {
+ ...props,
+ },
+ });
+
+ await waitForPromises();
+ };
+
+ const findCard = () => wrapper.findComponent(GlCard);
+ const findTitle = () => findCard().find('[data-testid="list-selector-title"]');
+ const findIcon = () => wrapper.findComponent(GlIcon);
+ const findAllListBoxComponents = () => wrapper.findAllComponents(GlCollapsibleListbox);
+ const findSearchResultsDropdown = () => findAllListBoxComponents().at(0);
+ const findNamespaceDropdown = () => findAllListBoxComponents().at(1);
+ const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
+ const findAllUserComponents = () => wrapper.findAllComponents(UserItem);
+ const findAllGroupComponents = () => wrapper.findAllComponents(GroupItem);
+
+ beforeEach(() => {
+ jest.spyOn(Api, 'projectUsers').mockResolvedValue(USERS_RESPONSE_MOCK);
+ jest.spyOn(Api, 'groupMembers').mockResolvedValue({ data: USERS_RESPONSE_MOCK });
+ });
+
+ describe('Users type', () => {
+ const search = 'foo';
+
+ beforeEach(() => createComponent(USERS_MOCK_PROPS));
+
+ it('renders a Card component', () => {
+ expect(findCard().exists()).toBe(true);
+ });
+
+ it('renders a correct title', () => {
+ expect(findTitle().exists()).toBe(true);
+ expect(findTitle().text()).toContain('Users');
+ });
+
+ it('renders the correct icon', () => {
+ expect(findIcon().props('name')).toBe('user');
+ });
+
+ it('renders a Search box component', () => {
+ expect(findSearchBox().exists()).toBe(true);
+ });
+
+ it('renders two namespace dropdown items', () => {
+ expect(findNamespaceDropdown().props('items').length).toBe(2);
+ });
+
+ it('does not call query when search box has not received an input', () => {
+ expect(Api.projectUsers).not.toHaveBeenCalled();
+ expect(Api.groupMembers).not.toHaveBeenCalled();
+ expect(findAllUserComponents().length).toBe(0);
+ });
+
+ describe.each`
+ dropdownItemValue | apiMethod | apiParams | searchResponse
+ ${'false'} | ${'groupMembers'} | ${[USERS_MOCK_PROPS.groupPath, { query: search }]} | ${USERS_RESPONSE_MOCK}
+ ${'true'} | ${'projectUsers'} | ${[USERS_MOCK_PROPS.projectPath, search]} | ${USERS_RESPONSE_MOCK}
+ `(
+ 'searching based on namespace dropdown selection',
+ ({ dropdownItemValue, apiMethod, apiParams, searchResponse }) => {
+ const emitSearchInput = async () => {
+ findSearchBox().vm.$emit('input', search);
+ await waitForPromises();
+ };
+
+ beforeEach(async () => {
+ findNamespaceDropdown().vm.$emit('select', dropdownItemValue);
+ await emitSearchInput();
+ });
+
+ it('shows error alert when API fails', async () => {
+ jest.spyOn(Api, apiMethod).mockRejectedValueOnce();
+ await emitSearchInput();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'An error occurred while fetching. Please try again.',
+ });
+ });
+
+ it('calls query with correct variables when Search box receives an input', () => {
+ expect(Api[apiMethod]).toHaveBeenCalledWith(...apiParams);
+ });
+
+ it('renders a List box component with the correct props', () => {
+ expect(findSearchResultsDropdown().props()).toMatchObject({
+ multiple: true,
+ items: searchResponse,
+ });
+ });
+
+ it('renders a user component for each search result', () => {
+ expect(findAllUserComponents().length).toBe(searchResponse.length);
+ });
+
+ it('emits an event when a search result is selected', () => {
+ const firstSearchResult = searchResponse[0];
+ findAllUserComponents().at(0).vm.$emit('select', firstSearchResult.username);
+
+ expect(wrapper.emitted('select')).toEqual([
+ [{ ...firstSearchResult, text: 'Administrator', value: 'root' }],
+ ]);
+ });
+ },
+ );
+
+ describe('selected items', () => {
+ const selectedUser = { username: 'root' };
+ const selectedItems = [selectedUser];
+ beforeEach(() => createComponent({ ...USERS_MOCK_PROPS, selectedItems }));
+
+ it('renders a heading with the total selected items', () => {
+ expect(findTitle().text()).toContain('Users');
+ expect(findTitle().text()).toContain('1');
+ });
+
+ it('renders a user component for each selected item', () => {
+ expect(findAllUserComponents().length).toBe(selectedItems.length);
+ expect(findAllUserComponents().at(0).props()).toMatchObject({
+ data: selectedUser,
+ canDelete: true,
+ });
+ });
+
+ it('emits a delete event when a delete event is emitted from the user component', () => {
+ const username = 'root';
+ findAllUserComponents().at(0).vm.$emit('delete', username);
+
+ expect(wrapper.emitted('delete')).toEqual([[username]]);
+ });
+ });
+ });
+
+ describe('Groups type', () => {
+ beforeEach(() => createComponent(GROUPS_MOCK_PROPS));
+
+ it('renders a correct title', () => {
+ expect(findTitle().exists()).toBe(true);
+ expect(findTitle().text()).toContain('Groups');
+ });
+
+ it('renders the correct icon', () => {
+ expect(findIcon().props('name')).toBe('group');
+ });
+
+ it('does not call query when search box has not received an input', () => {
+ expect(groupsAutocompleteQuerySuccess).not.toHaveBeenCalled();
+ expect(findAllGroupComponents().length).toBe(0);
+ });
+
+ describe('searching', () => {
+ const searchResponse = GROUPS_RESPONSE_MOCK.data.groups.nodes;
+ const search = 'foo';
+
+ const emitSearchInput = async () => {
+ findSearchBox().vm.$emit('input', search);
+ await waitForPromises();
+ };
+
+ beforeEach(() => emitSearchInput());
+
+ it('calls query with correct variables when Search box receives an input', () => {
+ expect(groupsAutocompleteQuerySuccess).toHaveBeenCalledWith({
+ search,
+ });
+ });
+
+ it('renders a dropdown for the search results', () => {
+ expect(findSearchResultsDropdown().props()).toMatchObject({
+ multiple: true,
+ items: searchResponse,
+ });
+ });
+
+ it('renders a group component for each search result', () => {
+ expect(findAllGroupComponents().length).toBe(searchResponse.length);
+ });
+
+ it('emits an event when a search result is selected', () => {
+ const firstSearchResult = searchResponse[0];
+ findAllGroupComponents().at(0).vm.$emit('select', firstSearchResult.name);
+
+ expect(wrapper.emitted('select')).toEqual([
+ [{ ...firstSearchResult, text: 'Flightjs', value: 'Flightjs' }],
+ ]);
+ });
+ });
+
+ describe('selected items', () => {
+ const selectedGroup = { name: 'Flightjs' };
+ const selectedItems = [selectedGroup];
+ beforeEach(() => createComponent({ ...GROUPS_MOCK_PROPS, selectedItems }));
+
+ it('renders a heading with the total selected items', () => {
+ expect(findTitle().text()).toContain('Groups');
+ expect(findTitle().text()).toContain('1');
+ });
+
+ it('renders a group component for each selected item', () => {
+ expect(findAllGroupComponents().length).toBe(selectedItems.length);
+ expect(findAllGroupComponents().at(0).props()).toMatchObject({
+ data: selectedGroup,
+ canDelete: true,
+ });
+ });
+
+ it('emits a delete event when a delete event is emitted from the group component', () => {
+ const name = 'Flightjs';
+ findAllGroupComponents().at(0).vm.$emit('delete', name);
+
+ expect(wrapper.emitted('delete')).toEqual([[name]]);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/list_selector/mock_data.js b/spec/frontend/vue_shared/components/list_selector/mock_data.js
new file mode 100644
index 00000000000..5b44a0c2a83
--- /dev/null
+++ b/spec/frontend/vue_shared/components/list_selector/mock_data.js
@@ -0,0 +1,41 @@
+export const USERS_RESPONSE_MOCK = [
+ {
+ id: 'gid://gitlab/User/1',
+ avatarUrl: '/uploads/-/system/user/avatar/1/avatar.png',
+ name: 'Administrator',
+ username: 'root',
+ __typename: 'AutocompletedUser',
+ },
+ {
+ id: 'gid://gitlab/User/15',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/c4ab964b90c3049c47882b319d3c5cc0?s=80\u0026d=identicon',
+ name: 'Corrine Rath',
+ username: 'laronda.graham',
+ __typename: 'AutocompletedUser',
+ },
+];
+
+export const GROUPS_RESPONSE_MOCK = {
+ data: {
+ groups: {
+ nodes: [
+ {
+ id: 'gid://gitlab/Group/33',
+ name: 'Flightjs',
+ fullName: 'Flightjs',
+ avatarUrl: null,
+ __typename: 'Group',
+ },
+ {
+ id: 'gid://gitlab/Group/34',
+ name: 'Flight 2',
+ fullName: 'Flight2',
+ avatarUrl: null,
+ __typename: 'Group',
+ },
+ ],
+ __typename: 'GroupConnection',
+ },
+ },
+};
diff --git a/spec/frontend/vue_shared/components/list_selector/user_item_spec.js b/spec/frontend/vue_shared/components/list_selector/user_item_spec.js
new file mode 100644
index 00000000000..d84a29c67e0
--- /dev/null
+++ b/spec/frontend/vue_shared/components/list_selector/user_item_spec.js
@@ -0,0 +1,55 @@
+import { GlAvatar } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import UserItem from '~/vue_shared/components/list_selector/user_item.vue';
+
+describe('UserItem spec', () => {
+ let wrapper;
+
+ const MOCK_USER = { name: 'Admin', username: 'root', avatarUrl: 'some/avatar.jpg' };
+
+ const createComponent = (props) => {
+ wrapper = mountExtended(UserItem, {
+ propsData: {
+ data: MOCK_USER,
+ ...props,
+ },
+ });
+ };
+
+ const findAvatar = () => wrapper.findComponent(GlAvatar);
+ const findDeleteButton = () => wrapper.findByRole('button', { name: 'Delete Admin' });
+
+ beforeEach(() => createComponent());
+
+ it('renders an Avatar component', () => {
+ expect(findAvatar().props('size')).toBe(32);
+ expect(findAvatar().attributes()).toMatchObject({
+ src: MOCK_USER.avatarUrl,
+ alt: MOCK_USER.name,
+ });
+ });
+
+ it('renders a name and username', () => {
+ expect(wrapper.text()).toContain('Admin');
+ expect(wrapper.text()).toContain('@root');
+ });
+
+ it('does not render a delete button by default', () => {
+ expect(findDeleteButton().exists()).toBe(false);
+ });
+
+ describe('Delete button', () => {
+ beforeEach(() => createComponent({ canDelete: true }));
+
+ it('renders a delete button', () => {
+ expect(findDeleteButton().exists()).toBe(true);
+ expect(findDeleteButton().props('icon')).toBe('remove');
+ });
+
+ it('emits a delete event if the delete button is clicked', () => {
+ findDeleteButton().trigger('click');
+
+ expect(wrapper.emitted('delete')).toEqual([[MOCK_USER.username]]);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
index b4c90fe49d1..edb11bd581b 100644
--- a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
@@ -137,7 +137,26 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
await enableContentEditor();
expect(mock.history.post).toHaveLength(1);
- expect(mock.history.post[0].url).toContain(`render_quick_actions=true`);
+ expect(mock.history.post[0].url).toBe(
+ `${window.location.origin}/api/markdown?render_quick_actions=true`,
+ );
+ });
+
+ describe('if gitlab is installed under a relative url', () => {
+ beforeEach(() => {
+ window.gon = { relative_url_root: '/gitlab' };
+ });
+
+ it('passes render_quick_actions param to renderMarkdownPath if quick actions are enabled', async () => {
+ buildWrapper({ propsData: { supportsQuickActions: true } });
+
+ await enableContentEditor();
+
+ expect(mock.history.post).toHaveLength(1);
+ expect(mock.history.post[0].url).toBe(
+ `${window.location.origin}/gitlab/api/markdown?render_quick_actions=true`,
+ );
+ });
});
it('does not pass render_quick_actions param to renderMarkdownPath if quick actions are disabled', async () => {
@@ -146,7 +165,9 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
await enableContentEditor();
expect(mock.history.post).toHaveLength(1);
- expect(mock.history.post[0].url).toContain(`render_quick_actions=false`);
+ expect(mock.history.post[0].url).toBe(
+ `${window.location.origin}/api/markdown?render_quick_actions=false`,
+ );
});
it('enables content editor switcher when contentEditorEnabled prop is true', () => {
diff --git a/spec/frontend/vue_shared/components/notes/__snapshots__/noteable_warning_spec.js.snap b/spec/frontend/vue_shared/components/notes/__snapshots__/noteable_warning_spec.js.snap
index 891b0c95f0e..ad0e260ad70 100644
--- a/spec/frontend/vue_shared/components/notes/__snapshots__/noteable_warning_spec.js.snap
+++ b/spec/frontend/vue_shared/components/notes/__snapshots__/noteable_warning_spec.js.snap
@@ -2,7 +2,7 @@
exports[`Issue Warning Component when issue is locked but not confidential renders information about locked issue 1`] = `
<span>
- This issue is locked. Only project members can comment.
+ The discussion in this issue is locked. Only project members can comment.
<gl-link-stub
href="locked-path"
target="_blank"
@@ -34,12 +34,12 @@ exports[`Issue Warning Component when noteable is locked and confidential render
>
confidential
</gl-link-stub>
- and
+ and its
<gl-link-stub
href=""
target="_blank"
>
- locked
+ discussion is locked
</gl-link-stub>
.
</span>
diff --git a/spec/frontend/vue_shared/components/notes/noteable_warning_spec.js b/spec/frontend/vue_shared/components/notes/noteable_warning_spec.js
index d7fcb9a25d4..d73356e00da 100644
--- a/spec/frontend/vue_shared/components/notes/noteable_warning_spec.js
+++ b/spec/frontend/vue_shared/components/notes/noteable_warning_spec.js
@@ -126,12 +126,14 @@ describe('Issue Warning Component', () => {
});
it('renders confidential & locked messages with noteable "issue"', () => {
- expect(findLockedBlock(wrapperLocked).text()).toContain('This issue is locked.');
+ expect(findLockedBlock(wrapperLocked).text()).toContain(
+ 'The discussion in this issue is locked.',
+ );
expect(findConfidentialBlock(wrapperConfidential).text()).toContain(
'This is a confidential issue.',
);
expect(findLockedAndConfidentialBlock(wrapperLockedAndConfidential).text()).toContain(
- 'This issue is confidential and locked.',
+ 'This issue is confidential and its discussion is locked.',
);
});
@@ -147,7 +149,9 @@ describe('Issue Warning Component', () => {
});
await nextTick();
- expect(findLockedBlock(wrapperLocked).text()).toContain('This epic is locked.');
+ expect(findLockedBlock(wrapperLocked).text()).toContain(
+ 'The discussion in this epic is locked.',
+ );
await nextTick();
expect(findConfidentialBlock(wrapperConfidential).text()).toContain(
@@ -156,7 +160,7 @@ describe('Issue Warning Component', () => {
await nextTick();
expect(findLockedAndConfidentialBlock(wrapperLockedAndConfidential).text()).toContain(
- 'This epic is confidential and locked.',
+ 'This epic is confidential and its discussion is locked.',
);
});
@@ -172,7 +176,9 @@ describe('Issue Warning Component', () => {
});
await nextTick();
- expect(findLockedBlock(wrapperLocked).text()).toContain('This merge request is locked.');
+ expect(findLockedBlock(wrapperLocked).text()).toContain(
+ 'The discussion in this merge request is locked.',
+ );
await nextTick();
expect(findConfidentialBlock(wrapperConfidential).text()).toContain(
@@ -181,7 +187,7 @@ describe('Issue Warning Component', () => {
await nextTick();
expect(findLockedAndConfidentialBlock(wrapperLockedAndConfidential).text()).toContain(
- 'This merge request is confidential and locked.',
+ 'This merge request is confidential and its discussion is locked.',
);
});
});
diff --git a/spec/frontend/vue_shared/components/notes/system_note_spec.js b/spec/frontend/vue_shared/components/notes/system_note_spec.js
index 7f3912dcadb..ade97290004 100644
--- a/spec/frontend/vue_shared/components/notes/system_note_spec.js
+++ b/spec/frontend/vue_shared/components/notes/system_note_spec.js
@@ -61,10 +61,16 @@ describe('system note component', () => {
expect(vm.classes()).toContain('target');
});
- it('should render svg icon', () => {
+ it('should render svg icon only for certain icons', () => {
+ const ALLOWED_ICONS = ['check', 'merge', 'merge-request-close', 'issue-close'];
createComponent(props);
- expect(vm.find('[data-testid="timeline-icon"]').exists()).toBe(true);
+ expect(vm.find('[data-testid="timeline-icon"]').exists()).toBe(false);
+
+ ALLOWED_ICONS.forEach((icon) => {
+ createComponent({ note: { ...props.note, system_note_icon_name: icon } });
+ expect(vm.find('[data-testid="timeline-icon"]').exists()).toBe(true);
+ });
});
// Redcarpet Markdown renderer wraps text in `<p>` tags
diff --git a/spec/frontend/vue_shared/components/source_viewer/components/blame_info_spec.js b/spec/frontend/vue_shared/components/source_viewer/components/blame_info_spec.js
index ff8b2be9634..121bc691041 100644
--- a/spec/frontend/vue_shared/components/source_viewer/components/blame_info_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/components/blame_info_spec.js
@@ -1,42 +1,35 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { setHTMLFixture } from 'helpers/fixtures';
import CommitInfo from '~/repository/components/commit_info.vue';
import BlameInfo from '~/vue_shared/components/source_viewer/components/blame_info.vue';
-import * as utils from '~/vue_shared/components/source_viewer/utils';
-import { SOURCE_CODE_CONTENT_MOCK, BLAME_DATA_MOCK } from '../mock_data';
+import { BLAME_DATA_MOCK } from '../mock_data';
describe('BlameInfo component', () => {
let wrapper;
const createComponent = () => {
wrapper = shallowMountExtended(BlameInfo, {
- propsData: { blameData: BLAME_DATA_MOCK },
+ propsData: { blameInfo: BLAME_DATA_MOCK },
});
};
beforeEach(() => {
- setHTMLFixture(SOURCE_CODE_CONTENT_MOCK);
- jest.spyOn(utils, 'toggleBlameClasses');
createComponent();
});
const findCommitInfoComponents = () => wrapper.findAllComponents(CommitInfo);
- it('adds the necessary classes to the DOM', () => {
- expect(utils.toggleBlameClasses).toHaveBeenCalledWith(BLAME_DATA_MOCK, true);
- });
-
it('renders a CommitInfo component for each blame entry', () => {
expect(findCommitInfoComponents().length).toBe(BLAME_DATA_MOCK.length);
});
it.each(BLAME_DATA_MOCK)(
'sets the correct data and positioning for the commitInfo',
- ({ lineno, commit, index }) => {
+ ({ commit, commitData, index, blameOffset }) => {
const commitInfoComponent = findCommitInfoComponents().at(index);
expect(commitInfoComponent.props('commit')).toEqual(commit);
- expect(commitInfoComponent.element.style.top).toBe(utils.calculateBlameOffset(lineno));
+ expect(commitInfoComponent.props('prevBlameLink')).toBe(commitData?.projectBlameLink || null);
+ expect(commitInfoComponent.element.style.top).toBe(blameOffset);
},
);
@@ -52,12 +45,4 @@ describe('BlameInfo component', () => {
expect(findCommitInfoComponents().at(2).element.classList).toContain(borderTopClassName);
});
});
-
- describe('when component is destroyed', () => {
- beforeEach(() => wrapper.destroy());
-
- it('resets the DOM to its original state', () => {
- expect(utils.toggleBlameClasses).toHaveBeenCalledWith(BLAME_DATA_MOCK, false);
- });
- });
});
diff --git a/spec/frontend/vue_shared/components/source_viewer/components/chunk_new_spec.js b/spec/frontend/vue_shared/components/source_viewer/components/chunk_new_spec.js
index 852598b13dc..c7b2363026a 100644
--- a/spec/frontend/vue_shared/components/source_viewer/components/chunk_new_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/components/chunk_new_spec.js
@@ -6,7 +6,6 @@ import { CHUNK_1, CHUNK_2 } from '../mock_data';
describe('Chunk component', () => {
let wrapper;
- let idleCallbackSpy;
const createComponent = (props = {}) => {
wrapper = shallowMountExtended(Chunk, {
@@ -19,7 +18,6 @@ describe('Chunk component', () => {
const findContent = () => wrapper.findByTestId('content');
beforeEach(() => {
- idleCallbackSpy = jest.spyOn(window, 'requestIdleCallback').mockImplementation((fn) => fn());
createComponent();
});
@@ -40,19 +38,6 @@ describe('Chunk component', () => {
});
describe('rendering', () => {
- it('does not register window.requestIdleCallback for the first chunk, renders content immediately', () => {
- expect(window.requestIdleCallback).not.toHaveBeenCalled();
- expect(findContent().text()).toBe(CHUNK_1.highlightedContent);
- });
-
- it('does not render content if browser is not in idle state', () => {
- idleCallbackSpy.mockRestore();
- createComponent({ chunkIndex: 1, ...CHUNK_2 });
-
- expect(findLineNumbers()).toHaveLength(0);
- expect(findContent().exists()).toBe(false);
- });
-
describe('isHighlighted is false', () => {
beforeEach(() => createComponent(CHUNK_2));
diff --git a/spec/frontend/vue_shared/components/source_viewer/mock_data.js b/spec/frontend/vue_shared/components/source_viewer/mock_data.js
index b3516f7ed72..cfff3a15b77 100644
--- a/spec/frontend/vue_shared/components/source_viewer/mock_data.js
+++ b/spec/frontend/vue_shared/components/source_viewer/mock_data.js
@@ -24,22 +24,74 @@ export const CHUNK_2 = {
};
export const SOURCE_CODE_CONTENT_MOCK = `
-<div class="content">
- <div>
- <div id="L1">1</div>
- <div id="L2">2</div>
- <div id="L3">3</div>
- </div>
+<div class="file-holder">
+ <div class="blob-viewer">
+ <div class="content">
+ <div>
+ <div id="L1">1</div>
+ <div id="L2">2</div>
+ <div id="L3">3</div>
+ </div>
- <div>
- <div id="LC1">Content 1</div>
- <div id="LC2">Content 2</div>
- <div id="LC3">Content 3</div>
+ <div>
+ <div id="LC1">Content 1</div>
+ <div id="LC2">Content 2</div>
+ <div id="LC3">Content 3</div>
+ </div>
+ </div>
</div>
</div>`;
+const COMMIT_DATA_MOCK = { projectBlameLink: 'project/blame/link' };
+
export const BLAME_DATA_MOCK = [
- { lineno: 1, commit: { author: 'Peter' }, index: 0 },
- { lineno: 2, commit: { author: 'Sarah' }, index: 1 },
- { lineno: 3, commit: { author: 'Peter' }, index: 2 },
+ {
+ lineno: 1,
+ commit: { author: 'Peter', sha: 'abc' },
+ index: 0,
+ blameOffset: '0px',
+ commitData: COMMIT_DATA_MOCK,
+ },
+ { lineno: 2, commit: { author: 'Sarah', sha: 'def' }, index: 1, blameOffset: '1px' },
+ { lineno: 3, commit: { author: 'Peter', sha: 'ghi' }, index: 2, blameOffset: '2px' },
];
+
+export const BLAME_DATA_QUERY_RESPONSE_MOCK = {
+ data: {
+ project: {
+ id: 'gid://gitlab/Project/278964',
+ __typename: 'Project',
+ repository: {
+ __typename: 'Repository',
+ blobs: {
+ __typename: 'BlobConnection',
+ nodes: [
+ {
+ id: 'gid://gitlab/Blob/f0c77e4b621df72719ce2b500ea6228559f6bc09',
+ blame: {
+ firstLine: '1',
+ groups: [
+ {
+ lineno: 1,
+ span: 3,
+ commit: {
+ id: 'gid://gitlab/CommitPresenter/13b0aca4142d1d55931577f69289a792f216f805',
+ titleHtml: 'Upload New File',
+ message: 'Upload New File',
+ authoredDate: '2022-10-31T10:38:30+00:00',
+ authorGravatar: 'path/to/gravatar',
+ webPath: '/commit/1234',
+ author: {},
+ sha: '13b0aca4142d1d55931577f69289a792f216f805',
+ },
+ commitData: COMMIT_DATA_MOCK,
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ },
+ },
+};
diff --git a/spec/frontend/vue_shared/components/source_viewer/source_viewer_new_spec.js b/spec/frontend/vue_shared/components/source_viewer/source_viewer_new_spec.js
index 1a498d0c5b1..ee7164515f6 100644
--- a/spec/frontend/vue_shared/components/source_viewer/source_viewer_new_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/source_viewer_new_spec.js
@@ -1,11 +1,29 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { setHTMLFixture } from 'helpers/fixtures';
import SourceViewer from '~/vue_shared/components/source_viewer/source_viewer_new.vue';
import Chunk from '~/vue_shared/components/source_viewer/components/chunk_new.vue';
import { EVENT_ACTION, EVENT_LABEL_VIEWER } from '~/vue_shared/components/source_viewer/constants';
import Tracking from '~/tracking';
import LineHighlighter from '~/blob/line_highlighter';
import addBlobLinksTracking from '~/blob/blob_links_tracking';
-import { BLOB_DATA_MOCK, CHUNK_1, CHUNK_2, LANGUAGE_MOCK } from './mock_data';
+import waitForPromises from 'helpers/wait_for_promises';
+import blameDataQuery from '~/vue_shared/components/source_viewer/queries/blame_data.query.graphql';
+import Blame from '~/vue_shared/components/source_viewer/components/blame_info.vue';
+import * as utils from '~/vue_shared/components/source_viewer/utils';
+
+import {
+ BLOB_DATA_MOCK,
+ CHUNK_1,
+ CHUNK_2,
+ LANGUAGE_MOCK,
+ BLAME_DATA_QUERY_RESPONSE_MOCK,
+ SOURCE_CODE_CONTENT_MOCK,
+} from './mock_data';
+
+Vue.use(VueApollo);
const lineHighlighter = new LineHighlighter();
jest.mock('~/blob/line_highlighter', () =>
@@ -17,17 +35,35 @@ jest.mock('~/blob/blob_links_tracking');
describe('Source Viewer component', () => {
let wrapper;
+ let fakeApollo;
const CHUNKS_MOCK = [CHUNK_1, CHUNK_2];
const hash = '#L142';
- const createComponent = () => {
+ const blameDataQueryHandlerSuccess = jest.fn().mockResolvedValue(BLAME_DATA_QUERY_RESPONSE_MOCK);
+ const blameInfo =
+ BLAME_DATA_QUERY_RESPONSE_MOCK.data.project.repository.blobs.nodes[0].blame.groups;
+
+ const createComponent = ({ showBlame = true } = {}) => {
+ fakeApollo = createMockApollo([[blameDataQuery, blameDataQueryHandlerSuccess]]);
+
wrapper = shallowMountExtended(SourceViewer, {
+ apolloProvider: fakeApollo,
mocks: { $route: { hash } },
- propsData: { blob: BLOB_DATA_MOCK, chunks: CHUNKS_MOCK },
+ propsData: {
+ blob: BLOB_DATA_MOCK,
+ chunks: CHUNKS_MOCK,
+ projectPath: 'test',
+ showBlame,
+ },
});
};
const findChunks = () => wrapper.findAllComponents(Chunk);
+ const findBlameComponents = () => wrapper.findAllComponents(Blame);
+ const triggerChunkAppear = async (chunkIndex = 0) => {
+ findChunks().at(chunkIndex).vm.$emit('appear');
+ await waitForPromises();
+ };
beforeEach(() => {
jest.spyOn(Tracking, 'event');
@@ -50,6 +86,65 @@ describe('Source Viewer component', () => {
});
describe('rendering', () => {
+ it('does not render a Blame component if the respective chunk for the blame has not appeared', async () => {
+ await waitForPromises();
+ expect(findBlameComponents()).toHaveLength(0);
+ });
+
+ describe('DOM updates', () => {
+ it('adds the necessary classes to the DOM', async () => {
+ setHTMLFixture(SOURCE_CODE_CONTENT_MOCK);
+ jest.spyOn(utils, 'toggleBlameClasses');
+ createComponent();
+ await triggerChunkAppear();
+
+ expect(utils.toggleBlameClasses).toHaveBeenCalledWith(blameInfo, true);
+ });
+ });
+
+ describe('Blame information', () => {
+ it('renders a Blame component when a chunk appears', async () => {
+ await triggerChunkAppear();
+
+ expect(findBlameComponents().at(0).exists()).toBe(true);
+ expect(findBlameComponents().at(0).props()).toMatchObject({ blameInfo });
+ });
+
+ it('calls the query only once per chunk', async () => {
+ jest.spyOn(wrapper.vm.$apollo, 'query');
+
+ // We trigger the `appear` event multiple times here in order to simulate the user scrolling past the chunk more than once.
+ // In this scenario we only want to query the backend once.
+ await triggerChunkAppear();
+ await triggerChunkAppear();
+
+ expect(wrapper.vm.$apollo.query).toHaveBeenCalledTimes(1);
+ });
+
+ it('requests blame information for overlapping chunk', async () => {
+ jest.spyOn(wrapper.vm.$apollo, 'query');
+
+ await triggerChunkAppear(1);
+
+ expect(wrapper.vm.$apollo.query).toHaveBeenCalledTimes(2);
+ expect(blameDataQueryHandlerSuccess).toHaveBeenCalledWith(
+ expect.objectContaining({ fromLine: 71, toLine: 110 }),
+ );
+ expect(blameDataQueryHandlerSuccess).toHaveBeenCalledWith(
+ expect.objectContaining({ fromLine: 1, toLine: 70 }),
+ );
+
+ expect(findChunks().at(0).props('isHighlighted')).toBe(true);
+ });
+
+ it('does not render a Blame component when `showBlame: false`', async () => {
+ createComponent({ showBlame: false });
+ await triggerChunkAppear();
+
+ expect(findBlameComponents()).toHaveLength(0);
+ });
+ });
+
it('renders a Chunk component for each chunk', () => {
expect(findChunks().at(0).props()).toMatchObject(CHUNK_1);
expect(findChunks().at(1).props()).toMatchObject(CHUNK_2);
@@ -58,8 +153,7 @@ describe('Source Viewer component', () => {
describe('hash highlighting', () => {
it('calls highlightHash with expected parameter', () => {
- const scrollEnabled = false;
- expect(lineHighlighter.highlightHash).toHaveBeenCalledWith(hash, scrollEnabled);
+ expect(lineHighlighter.highlightHash).toHaveBeenCalledWith(hash);
});
});
});
diff --git a/spec/frontend/vue_shared/components/source_viewer/utils_spec.js b/spec/frontend/vue_shared/components/source_viewer/utils_spec.js
index 0ac72aa9afb..a656b06068b 100644
--- a/spec/frontend/vue_shared/components/source_viewer/utils_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/utils_spec.js
@@ -1,6 +1,7 @@
import { setHTMLFixture } from 'helpers/fixtures';
import {
calculateBlameOffset,
+ shouldRender,
toggleBlameClasses,
} from '~/vue_shared/components/source_viewer/utils';
import { SOURCE_CODE_CONTENT_MOCK, BLAME_DATA_MOCK } from './mock_data';
@@ -21,6 +22,19 @@ describe('SourceViewer utils', () => {
});
});
+ describe('shouldRender', () => {
+ const commit = { sha: 'abc' };
+ const identicalSha = [{ commit }, { commit }];
+
+ it.each`
+ data | index | result
+ ${identicalSha} | ${0} | ${true}
+ ${identicalSha} | ${1} | ${false}
+ `('returns $result', ({ data, index, result }) => {
+ expect(shouldRender(data, index)).toBe(result);
+ });
+ });
+
describe('toggleBlameClasses', () => {
it('adds classes', () => {
toggleBlameClasses(BLAME_DATA_MOCK, true);
diff --git a/spec/frontend/vue_shared/components/users_table/mock_data.js b/spec/frontend/vue_shared/components/users_table/mock_data.js
new file mode 100644
index 00000000000..c763ca2ca9b
--- /dev/null
+++ b/spec/frontend/vue_shared/components/users_table/mock_data.js
@@ -0,0 +1,23 @@
+export const MOCK_USERS = [
+ {
+ id: 2177,
+ name: 'Nikki',
+ createdAt: '2020-11-13T12:26:54.177Z',
+ email: 'nikki@example.com',
+ username: 'nikki',
+ lastActivityOn: '2020-12-09',
+ avatarUrl:
+ 'https://secure.gravatar.com/avatar/054f062d8b1a42b123f17e13a173cda8?s=80\\u0026d=identicon',
+ badges: [
+ { text: 'Admin', variant: 'success' },
+ { text: "It's you!", variant: 'muted' },
+ ],
+ projectsCount: 0,
+ actions: [],
+ note: 'Create per issue #999',
+ },
+];
+
+export const MOCK_ADMIN_USER_PATH = 'admin/users/:id';
+
+export const MOCK_GROUP_COUNTS = { 2177: 5 };
diff --git a/spec/frontend/vue_shared/components/users_table/user_avatar_spec.js b/spec/frontend/vue_shared/components/users_table/user_avatar_spec.js
new file mode 100644
index 00000000000..035778530af
--- /dev/null
+++ b/spec/frontend/vue_shared/components/users_table/user_avatar_spec.js
@@ -0,0 +1,139 @@
+import { GlAvatarLabeled, GlBadge, GlIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import AdminUserAvatar from '~/vue_shared/components/users_table/user_avatar.vue';
+import { LENGTH_OF_USER_NOTE_TOOLTIP } from '~/vue_shared/components/users_table/constants';
+import { truncate } from '~/lib/utils/text_utility';
+import { MOCK_USERS, MOCK_ADMIN_USER_PATH } from './mock_data';
+
+describe('AdminUserAvatar component', () => {
+ let wrapper;
+ const user = MOCK_USERS[0];
+
+ const findNote = () => wrapper.findComponent(GlIcon);
+ const findAvatar = () => wrapper.findComponent(GlAvatarLabeled);
+ const findUserLink = () => wrapper.find('.js-user-link');
+ const findAllBadges = () => wrapper.findAllComponents(GlBadge);
+ const findTooltip = () => getBinding(findNote().element, 'gl-tooltip');
+
+ const initComponent = (props = {}) => {
+ wrapper = shallowMount(AdminUserAvatar, {
+ propsData: {
+ user,
+ adminUserPath: MOCK_ADMIN_USER_PATH,
+ ...props,
+ },
+ directives: {
+ GlTooltip: createMockDirective('gl-tooltip'),
+ },
+ stubs: {
+ GlAvatarLabeled,
+ },
+ });
+ };
+
+ describe('when initialized', () => {
+ beforeEach(() => {
+ initComponent();
+ });
+
+ it('adds a user link hover card', () => {
+ expect(findUserLink().attributes()).toMatchObject({
+ 'data-user-id': user.id.toString(),
+ 'data-username': user.username,
+ });
+ });
+
+ it("renders the user's name with an admin path link", () => {
+ const avatar = findAvatar();
+
+ expect(avatar.props('label')).toBe(user.name);
+ expect(avatar.props('labelLink')).toBe(MOCK_ADMIN_USER_PATH.replace('id', user.username));
+ });
+
+ it("renders the user's avatar image", () => {
+ expect(findAvatar().attributes('src')).toBe(user.avatarUrl);
+ });
+
+ it('renders a user note icon', () => {
+ expect(findNote().exists()).toBe(true);
+ expect(findNote().props('name')).toBe('document');
+ });
+
+ it("renders the user's note tooltip", () => {
+ const tooltip = findTooltip();
+
+ expect(tooltip).toBeDefined();
+ expect(tooltip.value).toBe(user.note);
+ });
+
+ it("renders the user's badges", () => {
+ findAllBadges().wrappers.forEach((badge, idx) => {
+ expect(badge.text()).toBe(user.badges[idx].text);
+ expect(badge.props('variant')).toBe(user.badges[idx].variant);
+ });
+ });
+
+ describe('and the user note is very long', () => {
+ const noteText = new Array(LENGTH_OF_USER_NOTE_TOOLTIP + 1).join('a');
+
+ beforeEach(() => {
+ initComponent({
+ user: {
+ ...user,
+ note: noteText,
+ },
+ });
+ });
+
+ it("renders a truncated user's note tooltip", () => {
+ const tooltip = findTooltip();
+
+ expect(tooltip).toBeDefined();
+ expect(tooltip.value).toBe(truncate(noteText, LENGTH_OF_USER_NOTE_TOOLTIP));
+ });
+ });
+
+ describe('and the user does not have a note', () => {
+ beforeEach(() => {
+ initComponent({
+ user: {
+ ...user,
+ note: null,
+ },
+ });
+ });
+
+ it('does not render a user note', () => {
+ expect(findNote().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('when user has an email address', () => {
+ beforeEach(() => {
+ initComponent();
+ });
+
+ it("renders the user's email with a mailto link", () => {
+ const avatar = findAvatar();
+
+ expect(avatar.props('subLabel')).toBe(user.email);
+ expect(avatar.props('subLabelLink')).toBe(`mailto:${user.email}`);
+ });
+ });
+
+ describe('when user does not have an email address', () => {
+ beforeEach(() => {
+ initComponent({ user: { ...MOCK_USERS[0], email: null } });
+ });
+
+ it("renders the user's username without a link", () => {
+ const avatar = findAvatar();
+
+ expect(avatar.props('subLabel')).toBe(`@${user.username}`);
+ expect(avatar.props('subLabelLink')).toBe('');
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/users_table/users_table_spec.js b/spec/frontend/vue_shared/components/users_table/users_table_spec.js
new file mode 100644
index 00000000000..45d1d291d47
--- /dev/null
+++ b/spec/frontend/vue_shared/components/users_table/users_table_spec.js
@@ -0,0 +1,95 @@
+import { GlTable, GlSkeletonLoader } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import UsersTable from '~/vue_shared/components/users_table/users_table.vue';
+import UserAvatar from '~/vue_shared/components/users_table/user_avatar.vue';
+import UserDate from '~/vue_shared/components/user_date.vue';
+import { MOCK_USERS, MOCK_ADMIN_USER_PATH, MOCK_GROUP_COUNTS } from './mock_data';
+
+describe('UsersTable component', () => {
+ let wrapper;
+ const user = MOCK_USERS[0];
+
+ const findUserGroupCount = (id) => wrapper.findByTestId(`user-group-count-${id}`);
+ const findUserGroupCountLoader = (id) => findUserGroupCount(id).findComponent(GlSkeletonLoader);
+ const getCellByLabel = (trIdx, label) => {
+ return wrapper
+ .findComponent(GlTable)
+ .find('tbody')
+ .findAll('tr')
+ .at(trIdx)
+ .find(`[data-label="${label}"][role="cell"]`);
+ };
+
+ const initComponent = (props = {}) => {
+ wrapper = mountExtended(UsersTable, {
+ propsData: {
+ users: MOCK_USERS,
+ adminUserPath: MOCK_ADMIN_USER_PATH,
+ groupCounts: MOCK_GROUP_COUNTS,
+ groupCountsLoading: false,
+ ...props,
+ },
+ });
+ };
+
+ describe('when there are users', () => {
+ beforeEach(() => {
+ initComponent();
+ });
+
+ it('renders the projects count', () => {
+ expect(getCellByLabel(0, 'Projects').text()).toContain(`${user.projectsCount}`);
+ });
+
+ it.each`
+ component | label
+ ${UserAvatar} | ${'Name'}
+ ${UserDate} | ${'Created on'}
+ ${UserDate} | ${'Last activity'}
+ `('renders the component for column $label', ({ component, label }) => {
+ expect(getCellByLabel(0, label).findComponent(component).exists()).toBe(true);
+ });
+ });
+
+ describe('when users is an empty array', () => {
+ beforeEach(() => {
+ initComponent({ users: [] });
+ });
+
+ it('renders a "No users found" message', () => {
+ expect(wrapper.text()).toContain('No users found');
+ });
+ });
+
+ describe('group counts', () => {
+ describe('when groupCountsLoading is true', () => {
+ beforeEach(() => {
+ initComponent({ groupCountsLoading: true });
+ });
+
+ it('renders a loader for each user', () => {
+ expect(findUserGroupCountLoader(user.id).exists()).toBe(true);
+ });
+ });
+
+ describe('when groupCounts has data', () => {
+ beforeEach(() => {
+ initComponent();
+ });
+
+ it("renders the user's group count", () => {
+ expect(findUserGroupCount(user.id).text()).toBe('5');
+ });
+ });
+
+ describe('when groupCounts has no data', () => {
+ beforeEach(() => {
+ initComponent({ groupCounts: {} });
+ });
+
+ it("renders the user's group count as 0", () => {
+ expect(findUserGroupCount(user.id).text()).toBe('0');
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/web_ide_link_spec.js b/spec/frontend/vue_shared/components/web_ide_link_spec.js
index 56d89d428f7..84ecbb431ac 100644
--- a/spec/frontend/vue_shared/components/web_ide_link_spec.js
+++ b/spec/frontend/vue_shared/components/web_ide_link_spec.js
@@ -35,7 +35,7 @@ const ACTION_EDIT = {
text: 'Edit single file',
secondaryText: 'Edit this file only.',
attrs: {
- 'data-qa-selector': 'edit_menu_item',
+ 'data-testid': 'edit-menu-item',
},
tracking: {
action: 'click_consolidated_edit',
@@ -51,7 +51,7 @@ const ACTION_WEB_IDE = {
secondaryText: i18n.webIdeText,
text: 'Web IDE',
attrs: {
- 'data-qa-selector': 'webide_menu_item',
+ 'data-testid': 'webide-menu-item',
},
href: undefined,
handle: expect.any(Function),
@@ -71,7 +71,7 @@ const ACTION_GITPOD = {
secondaryText: 'Launch a ready-to-code development environment for your project.',
text: 'Gitpod',
attrs: {
- 'data-qa-selector': 'gitpod_menu_item',
+ 'data-testid': 'gitpod-menu-item',
},
tracking: {
action: 'click_consolidated_edit',
@@ -88,7 +88,7 @@ const ACTION_PIPELINE_EDITOR = {
secondaryText: 'Edit, lint, and visualize your pipeline.',
text: 'Edit in pipeline editor',
attrs: {
- 'data-qa-selector': 'pipeline_editor_menu_item',
+ 'data-testid': 'pipeline_editor-menu-item',
},
tracking: {
action: 'click_consolidated_edit',
@@ -149,7 +149,7 @@ describe('vue_shared/components/web_ide_link', () => {
href: props.item.href,
handle: props.item.handle,
attrs: {
- 'data-qa-selector': attributes['data-qa-selector'],
+ 'data-testid': attributes['data-testid'],
},
};
});
diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js
index 02e729a00bd..71ff5275063 100644
--- a/spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js
+++ b/spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js
@@ -133,6 +133,7 @@ describe('IssuableBody', () => {
issuable: issuableBodyProps.issuable,
statusIcon: issuableBodyProps.statusIcon,
enableEdit: issuableBodyProps.enableEdit,
+ workspaceType: issuableBodyProps.workspaceType,
});
});
diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js
index ad7afefff12..6d1d3773643 100644
--- a/spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js
+++ b/spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js
@@ -52,6 +52,7 @@ describe('IssuableShowRoot', () => {
descriptionPreviewPath,
descriptionHelpPath,
taskCompletionStatus,
+ workspaceType,
} = mockIssuableShowProps;
const { state, blocked, confidential, createdAt, author } = mockIssuable;
@@ -92,6 +93,7 @@ describe('IssuableShowRoot', () => {
editFormVisible,
descriptionPreviewPath,
descriptionHelpPath,
+ workspaceType,
});
});
diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_title_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_title_spec.js
index eefc9142064..0ea69bc27e5 100644
--- a/spec/frontend/vue_shared/issuable/show/components/issuable_title_spec.js
+++ b/spec/frontend/vue_shared/issuable/show/components/issuable_title_spec.js
@@ -4,6 +4,7 @@ import { nextTick } from 'vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import IssuableTitle from '~/vue_shared/issuable/show/components/issuable_title.vue';
+import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
import { mockIssuableShowProps, mockIssuable } from '../mock_data';
@@ -86,19 +87,39 @@ describe('IssuableTitle', () => {
expect(tooltip).toBeDefined();
});
- it('renders sticky header when `stickyTitleVisible` prop is true', async () => {
- wrapper.findComponent(GlIntersectionObserver).vm.$emit('disappear');
- await nextTick();
+ describe('sticky header', () => {
+ it('renders when `stickyTitleVisible` prop is true', async () => {
+ wrapper.findComponent(GlIntersectionObserver).vm.$emit('disappear');
+ await nextTick();
- const stickyHeaderEl = findStickyHeader();
+ const stickyHeaderEl = findStickyHeader();
- expect(stickyHeaderEl.exists()).toBe(true);
- expect(stickyHeaderEl.findComponent(GlBadge).props('variant')).toBe('success');
- expect(stickyHeaderEl.findComponent(GlIcon).props('name')).toBe(
- issuableTitleProps.statusIcon,
- );
- expect(stickyHeaderEl.text()).toContain('Open');
- expect(stickyHeaderEl.text()).toContain(issuableTitleProps.issuable.title);
+ expect(stickyHeaderEl.exists()).toBe(true);
+ expect(stickyHeaderEl.findComponent(GlBadge).props('variant')).toBe('success');
+ expect(stickyHeaderEl.findComponent(GlIcon).props('name')).toBe(
+ issuableTitleProps.statusIcon,
+ );
+ expect(stickyHeaderEl.text()).toContain('Open');
+ expect(stickyHeaderEl.findComponent(ConfidentialityBadge).exists()).toBe(false);
+ expect(stickyHeaderEl.text()).toContain(issuableTitleProps.issuable.title);
+ });
+
+ it('renders ConfidentialityBadge when issuable is confidential', async () => {
+ wrapper = createComponent({
+ ...mockIssuableShowProps,
+ issuable: {
+ ...mockIssuable,
+ confidential: true,
+ },
+ });
+
+ wrapper.findComponent(GlIntersectionObserver).vm.$emit('disappear');
+ await nextTick();
+
+ const stickyHeaderEl = findStickyHeader();
+
+ expect(stickyHeaderEl.findComponent(ConfidentialityBadge).exists()).toBe(true);
+ });
});
});
});
diff --git a/spec/frontend/vue_shared/mixins/gl_feature_flags_mixin_spec.js b/spec/frontend/vue_shared/mixins/gl_feature_flags_mixin_spec.js
index 3ce12caf95a..1f579a1e945 100644
--- a/spec/frontend/vue_shared/mixins/gl_feature_flags_mixin_spec.js
+++ b/spec/frontend/vue_shared/mixins/gl_feature_flags_mixin_spec.js
@@ -19,7 +19,7 @@ describe('GitLab Feature Flags Mixin', () => {
wrapper = shallowMount(component, {
provide: {
- glFeatures: { ...(gon.features || {}) },
+ glFeatures: { ...gon.features },
},
});
});
diff --git a/spec/frontend/webhooks/components/__snapshots__/push_events_spec.js.snap b/spec/frontend/webhooks/components/__snapshots__/push_events_spec.js.snap
index 4150bd75c16..56f8bf0a5e3 100644
--- a/spec/frontend/webhooks/components/__snapshots__/push_events_spec.js.snap
+++ b/spec/frontend/webhooks/components/__snapshots__/push_events_spec.js.snap
@@ -15,9 +15,7 @@ exports[`Webhook push events form editor component Different push events rules w
data-testid="rule_all_branches"
value="all_branches"
>
- <div
- data-qa-selector="strategy_radio_all"
- >
+ <div>
All branches
</div>
</gl-form-radio-stub>
@@ -26,9 +24,7 @@ exports[`Webhook push events form editor component Different push events rules w
data-testid="rule_wildcard"
value="wildcard"
>
- <div
- data-qa-selector="strategy_radio_wildcard"
- >
+ <div>
Wildcard pattern
</div>
</gl-form-radio-stub>
@@ -40,9 +36,7 @@ exports[`Webhook push events form editor component Different push events rules w
data-testid="rule_regex"
value="regex"
>
- <div
- data-qa-selector="strategy_radio_regex"
- >
+ <div>
Regular expression
</div>
</gl-form-radio-stub>
@@ -67,9 +61,7 @@ exports[`Webhook push events form editor component Different push events rules w
data-testid="rule_all_branches"
value="all_branches"
>
- <div
- data-qa-selector="strategy_radio_all"
- >
+ <div>
All branches
</div>
</gl-form-radio-stub>
@@ -78,9 +70,7 @@ exports[`Webhook push events form editor component Different push events rules w
data-testid="rule_wildcard"
value="wildcard"
>
- <div
- data-qa-selector="strategy_radio_wildcard"
- >
+ <div>
Wildcard pattern
</div>
</gl-form-radio-stub>
@@ -92,9 +82,7 @@ exports[`Webhook push events form editor component Different push events rules w
data-testid="rule_regex"
value="regex"
>
- <div
- data-qa-selector="strategy_radio_regex"
- >
+ <div>
Regular expression
</div>
</gl-form-radio-stub>
@@ -102,7 +90,6 @@ exports[`Webhook push events form editor component Different push events rules w
class="gl-ml-6"
>
<gl-form-input-stub
- data-qa-selector="webhook_branch_filter_field"
data-testid="webhook_branch_filter_field"
name="hook[push_events_branch_filter]"
value="foo"
@@ -133,9 +120,7 @@ exports[`Webhook push events form editor component Different push events rules w
data-testid="rule_all_branches"
value="all_branches"
>
- <div
- data-qa-selector="strategy_radio_all"
- >
+ <div>
All branches
</div>
</gl-form-radio-stub>
@@ -144,9 +129,7 @@ exports[`Webhook push events form editor component Different push events rules w
data-testid="rule_wildcard"
value="wildcard"
>
- <div
- data-qa-selector="strategy_radio_wildcard"
- >
+ <div>
Wildcard pattern
</div>
</gl-form-radio-stub>
@@ -154,7 +137,6 @@ exports[`Webhook push events form editor component Different push events rules w
class="gl-ml-6"
>
<gl-form-input-stub
- data-qa-selector="webhook_branch_filter_field"
data-testid="webhook_branch_filter_field"
name="hook[push_events_branch_filter]"
value="foo"
@@ -172,9 +154,7 @@ exports[`Webhook push events form editor component Different push events rules w
data-testid="rule_regex"
value="regex"
>
- <div
- data-qa-selector="strategy_radio_regex"
- >
+ <div>
Regular expression
</div>
</gl-form-radio-stub>
@@ -199,9 +179,7 @@ exports[`Webhook push events form editor component Different push events rules w
data-testid="rule_all_branches"
value="all_branches"
>
- <div
- data-qa-selector="strategy_radio_all"
- >
+ <div>
All branches
</div>
</gl-form-radio-stub>
@@ -210,9 +188,7 @@ exports[`Webhook push events form editor component Different push events rules w
data-testid="rule_wildcard"
value="wildcard"
>
- <div
- data-qa-selector="strategy_radio_wildcard"
- >
+ <div>
Wildcard pattern
</div>
</gl-form-radio-stub>
@@ -224,9 +200,7 @@ exports[`Webhook push events form editor component Different push events rules w
data-testid="rule_regex"
value="regex"
>
- <div
- data-qa-selector="strategy_radio_regex"
- >
+ <div>
Regular expression
</div>
</gl-form-radio-stub>
@@ -251,9 +225,7 @@ exports[`Webhook push events form editor component Different push events rules w
data-testid="rule_all_branches"
value="all_branches"
>
- <div
- data-qa-selector="strategy_radio_all"
- >
+ <div>
All branches
</div>
</gl-form-radio-stub>
@@ -262,9 +234,7 @@ exports[`Webhook push events form editor component Different push events rules w
data-testid="rule_wildcard"
value="wildcard"
>
- <div
- data-qa-selector="strategy_radio_wildcard"
- >
+ <div>
Wildcard pattern
</div>
</gl-form-radio-stub>
@@ -276,9 +246,7 @@ exports[`Webhook push events form editor component Different push events rules w
data-testid="rule_regex"
value="regex"
>
- <div
- data-qa-selector="strategy_radio_regex"
- >
+ <div>
Regular expression
</div>
</gl-form-radio-stub>
@@ -286,7 +254,6 @@ exports[`Webhook push events form editor component Different push events rules w
class="gl-ml-6"
>
<gl-form-input-stub
- data-qa-selector="webhook_branch_filter_field"
data-testid="webhook_branch_filter_field"
name="hook[push_events_branch_filter]"
value=""
@@ -317,9 +284,7 @@ exports[`Webhook push events form editor component Different push events rules w
data-testid="rule_all_branches"
value="all_branches"
>
- <div
- data-qa-selector="strategy_radio_all"
- >
+ <div>
All branches
</div>
</gl-form-radio-stub>
@@ -328,9 +293,7 @@ exports[`Webhook push events form editor component Different push events rules w
data-testid="rule_wildcard"
value="wildcard"
>
- <div
- data-qa-selector="strategy_radio_wildcard"
- >
+ <div>
Wildcard pattern
</div>
</gl-form-radio-stub>
@@ -338,7 +301,6 @@ exports[`Webhook push events form editor component Different push events rules w
class="gl-ml-6"
>
<gl-form-input-stub
- data-qa-selector="webhook_branch_filter_field"
data-testid="webhook_branch_filter_field"
name="hook[push_events_branch_filter]"
value=""
@@ -356,9 +318,7 @@ exports[`Webhook push events form editor component Different push events rules w
data-testid="rule_regex"
value="regex"
>
- <div
- data-qa-selector="strategy_radio_regex"
- >
+ <div>
Regular expression
</div>
</gl-form-radio-stub>
diff --git a/spec/frontend/work_items/components/notes/system_note_spec.js b/spec/frontend/work_items/components/notes/system_note_spec.js
index 03f1aa356ad..69bc0961240 100644
--- a/spec/frontend/work_items/components/notes/system_note_spec.js
+++ b/spec/frontend/work_items/components/notes/system_note_spec.js
@@ -40,8 +40,14 @@ describe('Work Items system note component', () => {
);
});
- it('should render svg icon', () => {
- expect(findTimelineIcon().exists()).toBe(true);
+ it('should render svg icon only for allowed icons', () => {
+ expect(findTimelineIcon().exists()).toBe(false);
+
+ const ALLOWED_ICONS = ['issue-close'];
+ ALLOWED_ICONS.forEach((icon) => {
+ createComponent({ note: { ...workItemSystemNoteWithMetadata, systemNoteIconName: icon } });
+ expect(findTimelineIcon().exists()).toBe(true);
+ });
});
it('should not show compare previous version for FOSS', () => {
diff --git a/spec/frontend/work_items/components/notes/work_item_comment_form_spec.js b/spec/frontend/work_items/components/notes/work_item_comment_form_spec.js
index ee2b434bd75..fe89c525fea 100644
--- a/spec/frontend/work_items/components/notes/work_item_comment_form_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_comment_form_spec.js
@@ -10,7 +10,7 @@ import { STATE_OPEN } from '~/work_items/constants';
import * as confirmViaGlModal from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import WorkItemCommentForm from '~/work_items/components/notes/work_item_comment_form.vue';
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
-import WorkItemStateToggleButton from '~/work_items/components/work_item_state_toggle_button.vue';
+import WorkItemStateToggle from '~/work_items/components/work_item_state_toggle.vue';
Vue.use(VueApollo);
@@ -37,7 +37,7 @@ describe('Work item comment form component', () => {
const findConfirmButton = () => wrapper.find('[data-testid="confirm-button"]');
const findInternalNoteCheckbox = () => wrapper.findComponent(GlFormCheckbox);
const findInternalNoteTooltipIcon = () => wrapper.findComponent(GlIcon);
- const findWorkItemToggleStateButton = () => wrapper.findComponent(WorkItemStateToggleButton);
+ const findWorkItemToggleStateButton = () => wrapper.findComponent(WorkItemStateToggle);
const createComponent = ({
isSubmitting = false,
diff --git a/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js b/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js
index 6a24987b737..596283a9590 100644
--- a/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js
@@ -1,4 +1,4 @@
-import { GlDisclosureDropdown } from '@gitlab/ui';
+import { GlButton, GlDisclosureDropdown } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
@@ -17,7 +17,7 @@ describe('Work Item Note Actions', () => {
const showSpy = jest.fn();
const findReplyButton = () => wrapper.findComponent(ReplyButton);
- const findEditButton = () => wrapper.findByTestId('edit-work-item-note');
+ const findEditButton = () => wrapper.findComponent(GlButton);
const findEmojiButton = () => wrapper.findByTestId('note-emoji-button');
const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
const findDeleteNoteButton = () => wrapper.findByTestId('delete-note-action');
diff --git a/spec/frontend/work_items/components/shared/work_item_link_child_contents_spec.js b/spec/frontend/work_items/components/shared/work_item_link_child_contents_spec.js
index 2e1a7983dec..a40e860d9fe 100644
--- a/spec/frontend/work_items/components/shared/work_item_link_child_contents_spec.js
+++ b/spec/frontend/work_items/components/shared/work_item_link_child_contents_spec.js
@@ -1,4 +1,4 @@
-import { GlLabel, GlIcon, GlLink } from '@gitlab/ui';
+import { GlLabel, GlIcon, GlLink, GlButton } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
@@ -9,7 +9,6 @@ import { createAlert } from '~/alert';
import RichTimestampTooltip from '~/vue_shared/components/rich_timestamp_tooltip.vue';
import WorkItemLinkChildContents from '~/work_items/components/shared/work_item_link_child_contents.vue';
-import WorkItemLinksMenu from '~/work_items/components/shared/work_item_links_menu.vue';
import { WORK_ITEM_TYPE_VALUE_OBJECTIVE } from '~/work_items/constants';
import {
@@ -39,13 +38,18 @@ describe('WorkItemLinkChildContents', () => {
const findAllLabels = () => wrapper.findAllComponents(GlLabel);
const findRegularLabel = () => findAllLabels().at(0);
const findScopedLabel = () => findAllLabels().at(1);
- const findLinksMenuComponent = () => wrapper.findComponent(WorkItemLinksMenu);
+ const findRemoveButton = () => wrapper.findComponent(GlButton);
- const createComponent = ({ canUpdate = true, childItem = workItemTask } = {}) => {
+ const createComponent = ({
+ canUpdate = true,
+ childItem = workItemTask,
+ showLabels = true,
+ } = {}) => {
wrapper = shallowMountExtended(WorkItemLinkChildContents, {
propsData: {
canUpdate,
childItem,
+ showLabels,
},
});
};
@@ -129,19 +133,6 @@ describe('WorkItemLinkChildContents', () => {
expect(findMetadataComponent().exists()).toBe(false);
});
-
- it('renders labels', () => {
- const mockLabel = mockLabels[0];
-
- expect(findAllLabels()).toHaveLength(mockLabels.length);
- expect(findRegularLabel().props()).toMatchObject({
- title: mockLabel.title,
- backgroundColor: mockLabel.color,
- description: mockLabel.description,
- scoped: false,
- });
- expect(findScopedLabel().props('scoped')).toBe(true); // Second label is scoped
- });
});
describe('item menu', () => {
@@ -149,20 +140,47 @@ describe('WorkItemLinkChildContents', () => {
createComponent();
});
- it('renders work-item-links-menu', () => {
- expect(findLinksMenuComponent().exists()).toBe(true);
+ it('renders remove button', () => {
+ expect(findRemoveButton().exists()).toBe(true);
});
it('does not render work-item-links-menu when canUpdate is false', () => {
createComponent({ canUpdate: false });
- expect(findLinksMenuComponent().exists()).toBe(false);
+ expect(findRemoveButton().exists()).toBe(false);
});
it('removeChild event on menu triggers `click-remove-child` event', () => {
- findLinksMenuComponent().vm.$emit('removeChild');
+ findRemoveButton().vm.$emit('click');
expect(wrapper.emitted('removeChild')).toEqual([[workItemTask]]);
});
});
+
+ describe('item labels', () => {
+ it('renders normal and scoped label', () => {
+ createComponent({ childItem: workItemObjectiveWithChild });
+
+ const mockLabel = mockLabels[0];
+
+ expect(findAllLabels()).toHaveLength(mockLabels.length);
+ expect(findRegularLabel().props()).toMatchObject({
+ title: mockLabel.title,
+ backgroundColor: mockLabel.color,
+ description: mockLabel.description,
+ scoped: false,
+ });
+ expect(findScopedLabel().props('scoped')).toBe(true); // Second label is scoped
+ });
+
+ it.each`
+ expectedAssertion | showLabels
+ ${'does not render labels'} | ${true}
+ ${'renders label'} | ${false}
+ `('$expectedAssertion when showLabels is $showLabels', ({ showLabels }) => {
+ createComponent({ showLabels, childItem: workItemObjectiveWithChild });
+
+ expect(findAllLabels().exists()).toBe(showLabels);
+ });
+ });
});
diff --git a/spec/frontend/work_items/components/shared/work_item_links_menu_spec.js b/spec/frontend/work_items/components/shared/work_item_links_menu_spec.js
deleted file mode 100644
index 338a70feae4..00000000000
--- a/spec/frontend/work_items/components/shared/work_item_links_menu_spec.js
+++ /dev/null
@@ -1,30 +0,0 @@
-import { GlDisclosureDropdown, GlDisclosureDropdownItem } from '@gitlab/ui';
-
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import WorkItemLinksMenu from '~/work_items/components/shared/work_item_links_menu.vue';
-
-describe('WorkItemLinksMenu', () => {
- let wrapper;
-
- const createComponent = () => {
- wrapper = shallowMountExtended(WorkItemLinksMenu);
- };
-
- const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
- const findRemoveDropdownItem = () => wrapper.findComponent(GlDisclosureDropdownItem);
-
- beforeEach(() => {
- createComponent();
- });
-
- it('renders dropdown and dropdown items', () => {
- expect(findDropdown().exists()).toBe(true);
- expect(findRemoveDropdownItem().exists()).toBe(true);
- });
-
- it('emits removeChild event on click Remove', () => {
- findRemoveDropdownItem().vm.$emit('action');
-
- expect(wrapper.emitted('removeChild')).toHaveLength(1);
- });
-});
diff --git a/spec/frontend/work_items/components/shared/work_item_token_input_spec.js b/spec/frontend/work_items/components/shared/work_item_token_input_spec.js
index 075b69415cf..5726aaaa2d0 100644
--- a/spec/frontend/work_items/components/shared/work_item_token_input_spec.js
+++ b/spec/frontend/work_items/components/shared/work_item_token_input_spec.js
@@ -1,13 +1,19 @@
-import Vue from 'vue';
-import { GlTokenSelector } from '@gitlab/ui';
+import Vue, { nextTick } from 'vue';
+import { GlTokenSelector, GlAlert } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import WorkItemTokenInput from '~/work_items/components/shared/work_item_token_input.vue';
import { WORK_ITEM_TYPE_ENUM_TASK } from '~/work_items/constants';
+import groupWorkItemsQuery from '~/work_items/graphql/group_work_items.query.graphql';
import projectWorkItemsQuery from '~/work_items/graphql/project_work_items.query.graphql';
-import { availableWorkItemsResponse, searchedWorkItemsResponse } from '../../mock_data';
+import {
+ availableWorkItemsResponse,
+ searchWorkItemsTextResponse,
+ searchWorkItemsIidResponse,
+ searchWorkItemsTextIidResponse,
+} from '../../mock_data';
Vue.use(VueApollo);
@@ -15,17 +21,27 @@ describe('WorkItemTokenInput', () => {
let wrapper;
const availableWorkItemsResolver = jest.fn().mockResolvedValue(availableWorkItemsResponse);
- const searchedWorkItemResolver = jest.fn().mockResolvedValue(searchedWorkItemsResponse);
+ const groupSearchedWorkItemResolver = jest.fn().mockResolvedValue(searchWorkItemsTextResponse);
+ const searchWorkItemTextResolver = jest.fn().mockResolvedValue(searchWorkItemsTextResponse);
+ const searchWorkItemIidResolver = jest.fn().mockResolvedValue(searchWorkItemsIidResponse);
+ const searchWorkItemTextIidResolver = jest.fn().mockResolvedValue(searchWorkItemsTextIidResponse);
const createComponent = async ({
workItemsToAdd = [],
parentConfidential = false,
childrenType = WORK_ITEM_TYPE_ENUM_TASK,
areWorkItemsToAddValid = true,
- workItemsResolver = searchedWorkItemResolver,
+ workItemsResolver = searchWorkItemTextResolver,
+ isGroup = false,
} = {}) => {
wrapper = shallowMountExtended(WorkItemTokenInput, {
- apolloProvider: createMockApollo([[projectWorkItemsQuery, workItemsResolver]]),
+ apolloProvider: createMockApollo([
+ [projectWorkItemsQuery, workItemsResolver],
+ [groupWorkItemsQuery, groupSearchedWorkItemResolver],
+ ]),
+ provide: {
+ isGroup,
+ },
propsData: {
value: workItemsToAdd,
childrenType,
@@ -41,6 +57,7 @@ describe('WorkItemTokenInput', () => {
};
const findTokenSelector = () => wrapper.findComponent(GlTokenSelector);
+ const findGlAlert = () => wrapper.findComponent(GlAlert);
it('searches for available work items on focus', async () => {
createComponent({ workItemsResolver: availableWorkItemsResolver });
@@ -52,24 +69,34 @@ describe('WorkItemTokenInput', () => {
searchTerm: '',
types: [WORK_ITEM_TYPE_ENUM_TASK],
in: undefined,
+ iid: null,
+ isNumber: false,
});
expect(findTokenSelector().props('dropdownItems')).toHaveLength(3);
});
- it('searches for available work items when typing in input', async () => {
- createComponent({ workItemsResolver: searchedWorkItemResolver });
- findTokenSelector().vm.$emit('focus');
- findTokenSelector().vm.$emit('text-input', 'Task 2');
- await waitForPromises();
+ it.each`
+ inputType | input | resolver | searchTerm | iid | isNumber | length
+ ${'iid'} | ${'101'} | ${searchWorkItemIidResolver} | ${'101'} | ${'101'} | ${true} | ${1}
+ ${'text'} | ${'Task 2'} | ${searchWorkItemTextResolver} | ${'Task 2'} | ${null} | ${false} | ${1}
+ ${'iid and text'} | ${'123'} | ${searchWorkItemTextIidResolver} | ${'123'} | ${'123'} | ${true} | ${2}
+ `(
+ 'searches by $inputType for available work items when typing in input',
+ async ({ input, resolver, searchTerm, iid, isNumber, length }) => {
+ createComponent({ workItemsResolver: resolver });
+ findTokenSelector().vm.$emit('focus');
+ findTokenSelector().vm.$emit('text-input', input);
+ await waitForPromises();
- expect(searchedWorkItemResolver).toHaveBeenCalledWith({
- fullPath: 'test-project-path',
- searchTerm: 'Task 2',
- types: [WORK_ITEM_TYPE_ENUM_TASK],
- in: 'TITLE',
- });
- expect(findTokenSelector().props('dropdownItems')).toHaveLength(1);
- });
+ expect(resolver).toHaveBeenCalledWith({
+ searchTerm,
+ in: 'TITLE',
+ iid,
+ isNumber,
+ });
+ expect(findTokenSelector().props('dropdownItems')).toHaveLength(length);
+ },
+ );
it('renders red border around token selector input when work item is not valid', () => {
createComponent({
@@ -78,4 +105,58 @@ describe('WorkItemTokenInput', () => {
expect(findTokenSelector().props('containerClass')).toBe('gl-inset-border-1-red-500!');
});
+
+ describe('when project context', () => {
+ beforeEach(() => {
+ createComponent();
+ findTokenSelector().vm.$emit('focus');
+ });
+
+ it('calls the project work items query', () => {
+ expect(searchWorkItemTextResolver).toHaveBeenCalled();
+ });
+
+ it('skips calling the group work items query', () => {
+ expect(groupSearchedWorkItemResolver).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when group context', () => {
+ beforeEach(() => {
+ createComponent({ isGroup: true });
+ findTokenSelector().vm.$emit('focus');
+ });
+
+ it('skips calling the project work items query', () => {
+ expect(searchWorkItemTextResolver).not.toHaveBeenCalled();
+ });
+
+ it('calls the group work items query', () => {
+ expect(groupSearchedWorkItemResolver).toHaveBeenCalled();
+ });
+ });
+
+ describe('when project work items query fails', () => {
+ beforeEach(() => {
+ createComponent({
+ workItemsResolver: jest
+ .fn()
+ .mockRejectedValue('Something went wrong while fetching the results'),
+ });
+ findTokenSelector().vm.$emit('focus');
+ });
+
+ it('shows error and allows error alert to be closed', async () => {
+ await waitForPromises();
+ expect(findGlAlert().exists()).toBe(true);
+ expect(findGlAlert().text()).toBe(
+ 'Something went wrong while fetching the task. Please try again.',
+ );
+
+ findGlAlert().vm.$emit('dismiss');
+ await nextTick();
+
+ expect(findGlAlert().exists()).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/work_items/components/work_item_actions_spec.js b/spec/frontend/work_items/components/work_item_actions_spec.js
index 15c33bf5b1e..6cbb3c4384e 100644
--- a/spec/frontend/work_items/components/work_item_actions_spec.js
+++ b/spec/frontend/work_items/components/work_item_actions_spec.js
@@ -1,4 +1,10 @@
-import { GlDisclosureDropdown, GlDropdownDivider, GlModal, GlToggle } from '@gitlab/ui';
+import {
+ GlDisclosureDropdown,
+ GlDropdownDivider,
+ GlModal,
+ GlToggle,
+ GlDisclosureDropdownItem,
+} from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
@@ -10,14 +16,16 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { isLoggedIn } from '~/lib/utils/common_utils';
import toast from '~/vue_shared/plugins/global_toast';
import WorkItemActions from '~/work_items/components/work_item_actions.vue';
+import WorkItemStateToggle from '~/work_items/components/work_item_state_toggle.vue';
import {
+ STATE_OPEN,
TEST_ID_CONFIDENTIALITY_TOGGLE_ACTION,
- TEST_ID_NOTIFICATIONS_TOGGLE_ACTION,
TEST_ID_NOTIFICATIONS_TOGGLE_FORM,
TEST_ID_DELETE_ACTION,
TEST_ID_PROMOTE_ACTION,
TEST_ID_COPY_REFERENCE_ACTION,
TEST_ID_COPY_CREATE_NOTE_EMAIL_ACTION,
+ TEST_ID_TOGGLE_ACTION,
} from '~/work_items/constants';
import updateWorkItemNotificationsMutation from '~/work_items/graphql/update_work_item_notifications.mutation.graphql';
import projectWorkItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql';
@@ -44,11 +52,10 @@ describe('WorkItemActions component', () => {
const findModal = () => wrapper.findComponent(GlModal);
const findConfidentialityToggleButton = () =>
wrapper.findByTestId(TEST_ID_CONFIDENTIALITY_TOGGLE_ACTION);
- const findNotificationsToggleButton = () =>
- wrapper.findByTestId(TEST_ID_NOTIFICATIONS_TOGGLE_ACTION);
const findDeleteButton = () => wrapper.findByTestId(TEST_ID_DELETE_ACTION);
const findPromoteButton = () => wrapper.findByTestId(TEST_ID_PROMOTE_ACTION);
const findCopyReferenceButton = () => wrapper.findByTestId(TEST_ID_COPY_REFERENCE_ACTION);
+ const findWorkItemToggleOption = () => wrapper.findComponent(WorkItemStateToggle);
const findCopyCreateNoteEmailButton = () =>
wrapper.findByTestId(TEST_ID_COPY_CREATE_NOTE_EMAIL_ACTION);
const findDropdownItems = () => wrapper.findAll('[data-testid="work-item-actions-dropdown"] > *');
@@ -108,6 +115,7 @@ describe('WorkItemActions component', () => {
[updateWorkItemNotificationsMutation, notificationsMutationHandler],
]),
propsData: {
+ workItemState: STATE_OPEN,
fullPath: 'gitlab-org/gitlab-test',
workItemId: 'gid://gitlab/WorkItem/1',
canUpdate,
@@ -132,6 +140,7 @@ describe('WorkItemActions component', () => {
show: jest.fn(),
},
}),
+ GlDisclosureDropdownItem,
GlDisclosureDropdown: stubComponent(GlDisclosureDropdown, {
methods: {
close: modalShowSpy,
@@ -167,6 +176,10 @@ describe('WorkItemActions component', () => {
text: 'Turn on confidentiality',
},
{
+ testId: TEST_ID_TOGGLE_ACTION,
+ text: '',
+ },
+ {
testId: TEST_ID_COPY_REFERENCE_ACTION,
text: 'Copy reference',
},
@@ -248,7 +261,7 @@ describe('WorkItemActions component', () => {
});
it('renders toggle button', () => {
- expect(findNotificationsToggleButton().exists()).toBe(true);
+ expect(findNotificationsToggle().exists()).toBe(true);
});
it.each`
@@ -366,4 +379,10 @@ describe('WorkItemActions component', () => {
expect(toast).toHaveBeenCalledWith('Email address copied');
});
});
+
+ it('renders the toggle status button', () => {
+ createComponent();
+
+ expect(findWorkItemToggleOption().exists()).toBe(true);
+ });
});
diff --git a/spec/frontend/work_items/components/work_item_award_emoji_spec.js b/spec/frontend/work_items/components/work_item_award_emoji_spec.js
index f8c5f8edc4c..a756bfa6889 100644
--- a/spec/frontend/work_items/components/work_item_award_emoji_spec.js
+++ b/spec/frontend/work_items/components/work_item_award_emoji_spec.js
@@ -9,7 +9,8 @@ import { isLoggedIn } from '~/lib/utils/common_utils';
import AwardList from '~/vue_shared/components/awards_list.vue';
import WorkItemAwardEmoji from '~/work_items/components/work_item_award_emoji.vue';
import updateAwardEmojiMutation from '~/work_items/graphql/update_award_emoji.mutation.graphql';
-import workItemAwardEmojiQuery from '~/work_items/graphql/award_emoji.query.graphql';
+import groupWorkItemAwardEmojiQuery from '~/work_items/graphql/group_award_emoji.query.graphql';
+import projectWorkItemAwardEmojiQuery from '~/work_items/graphql/award_emoji.query.graphql';
import {
EMOJI_THUMBSUP,
EMOJI_THUMBSDOWN,
@@ -42,6 +43,7 @@ describe('WorkItemAwardEmoji component', () => {
const workItemQueryResponse = workItemByIidResponseFactory();
const mockWorkItem = workItemQueryResponse.data.workspace.workItems.nodes[0];
+ const groupAwardEmojiQuerySuccessHandler = jest.fn().mockResolvedValue(workItemQueryResponse);
const awardEmojiQuerySuccessHandler = jest.fn().mockResolvedValue(workItemQueryResponse);
const awardEmojiQueryEmptyHandler = jest.fn().mockResolvedValue(
workItemByIidResponseFactory({
@@ -83,10 +85,12 @@ describe('WorkItemAwardEmoji component', () => {
awardEmojiQueryHandler = awardEmojiQuerySuccessHandler,
awardEmojiMutationHandler = awardEmojiAddSuccessHandler,
workItemIid = '1',
+ isGroup = false,
} = {}) => {
mockApolloProvider = createMockApollo(
[
- [workItemAwardEmojiQuery, awardEmojiQueryHandler],
+ [projectWorkItemAwardEmojiQuery, awardEmojiQueryHandler],
+ [groupWorkItemAwardEmojiQuery, groupAwardEmojiQuerySuccessHandler],
[updateAwardEmojiMutation, awardEmojiMutationHandler],
],
{},
@@ -108,6 +112,9 @@ describe('WorkItemAwardEmoji component', () => {
wrapper = shallowMount(WorkItemAwardEmoji, {
isLoggedIn: isLoggedIn(),
apolloProvider: mockApolloProvider,
+ provide: {
+ isGroup,
+ },
propsData: {
workItemId: 'gid://gitlab/WorkItem/1',
workItemFullpath: 'test-project-path',
@@ -270,7 +277,7 @@ describe('WorkItemAwardEmoji component', () => {
};
});
- it('calls mutation succesfully and adds the award emoji with proper user details', async () => {
+ it('calls mutation successfully and adds the award emoji with proper user details', async () => {
createComponent({
awardEmojiMutationHandler: awardEmojiAddSuccessHandler,
});
@@ -345,4 +352,18 @@ describe('WorkItemAwardEmoji component', () => {
});
});
});
+
+ describe('group award emoji query', () => {
+ it('is not called in a project context', () => {
+ createComponent();
+
+ expect(groupAwardEmojiQuerySuccessHandler).not.toHaveBeenCalled();
+ });
+
+ it('is called in a group context', () => {
+ createComponent({ isGroup: true });
+
+ expect(groupAwardEmojiQuerySuccessHandler).toHaveBeenCalled();
+ });
+ });
});
diff --git a/spec/frontend/work_items/components/work_item_description_spec.js b/spec/frontend/work_items/components/work_item_description_spec.js
index de2895591dd..1d25bb74986 100644
--- a/spec/frontend/work_items/components/work_item_description_spec.js
+++ b/spec/frontend/work_items/components/work_item_description_spec.js
@@ -92,7 +92,7 @@ describe('WorkItemDescription', () => {
it('passes correct autocompletion data and preview markdown sources and enables quick actions', async () => {
const {
iid,
- project: { fullPath },
+ namespace: { fullPath },
} = workItemQueryResponse.data.workItem;
await createComponent({ isEditing: true });
diff --git a/spec/frontend/work_items/components/work_item_detail_spec.js b/spec/frontend/work_items/components/work_item_detail_spec.js
index 28826748cb0..acfe4571cd2 100644
--- a/spec/frontend/work_items/components/work_item_detail_spec.js
+++ b/spec/frontend/work_items/components/work_item_detail_spec.js
@@ -23,8 +23,6 @@ import WorkItemTree from '~/work_items/components/work_item_links/work_item_tree
import WorkItemRelationships from '~/work_items/components/work_item_relationships/work_item_relationships.vue';
import WorkItemNotes from '~/work_items/components/work_item_notes.vue';
import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
-import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
-import WorkItemStateToggleButton from '~/work_items/components/work_item_state_toggle_button.vue';
import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
import WorkItemTodos from '~/work_items/components/work_item_todos.vue';
import { i18n } from '~/work_items/constants';
@@ -55,10 +53,6 @@ describe('WorkItemDetail component', () => {
canUpdate: true,
canDelete: true,
});
- const workItemQueryResponseWithCannotUpdate = workItemByIidResponseFactory({
- canUpdate: false,
- canDelete: false,
- });
const workItemQueryResponseWithoutParent = workItemByIidResponseFactory({
parent: null,
canUpdate: true,
@@ -95,8 +89,6 @@ describe('WorkItemDetail component', () => {
const findWorkItemTwoColumnViewContainer = () => wrapper.findByTestId('work-item-overview');
const findRightSidebar = () => wrapper.findByTestId('work-item-overview-right-sidebar');
const triggerPageScroll = () => findIntersectionObserver().vm.$emit('disappear');
- const findWorkItemStateToggleButton = () => wrapper.findComponent(WorkItemStateToggleButton);
- const findWorkItemTypeIcon = () => wrapper.findComponent(WorkItemTypeIcon);
const createComponent = ({
isGroup = false,
@@ -212,25 +204,6 @@ describe('WorkItemDetail component', () => {
});
});
- describe('work item state toggle button', () => {
- describe.each`
- description | canUpdate
- ${'when user cannot update'} | ${false}
- ${'when user can update'} | ${true}
- `('$description', ({ canUpdate }) => {
- it(`${canUpdate ? 'is rendered' : 'is not rendered'}`, async () => {
- createComponent({
- handler: canUpdate
- ? jest.fn().mockResolvedValue(workItemQueryResponse)
- : jest.fn().mockResolvedValue(workItemQueryResponseWithCannotUpdate),
- });
- await waitForPromises();
-
- expect(findWorkItemStateToggleButton().exists()).toBe(canUpdate);
- });
- });
- });
-
describe('close button', () => {
describe('when isModal prop is false', () => {
it('does not render', async () => {
@@ -408,12 +381,11 @@ describe('WorkItemDetail component', () => {
expect(findParent().exists()).toBe(false);
});
- it('shows work item type with reference when there is no a parent', async () => {
+ it('shows title in the header when there is no parent', async () => {
createComponent({ handler: jest.fn().mockResolvedValue(workItemQueryResponseWithoutParent) });
await waitForPromises();
- expect(findWorkItemTypeIcon().props('showText')).toBe(true);
- expect(findWorkItemType().text()).toBe('#1');
+ expect(findWorkItemType().classes()).toEqual(['gl-w-full']);
});
describe('with parent', () => {
@@ -428,10 +400,6 @@ describe('WorkItemDetail component', () => {
expect(findParent().exists()).toBe(true);
});
- it('does not show work item type', () => {
- expect(findWorkItemType().exists()).toBe(false);
- });
-
it('shows parent breadcrumb icon', () => {
expect(findParentButton().props('icon')).toBe(mockParent.parent.workItemType.iconName);
});
@@ -468,6 +436,10 @@ describe('WorkItemDetail component', () => {
const { iid } = workItemQueryResponse.data.workspace.workItems.nodes[0];
expect(findParent().text()).toContain(`#${iid}`);
});
+
+ it('does not show title in the header when parent exists', () => {
+ expect(findWorkItemType().classes()).toEqual(['gl-sm-display-none!']);
+ });
});
});
diff --git a/spec/frontend/work_items/components/work_item_labels_spec.js b/spec/frontend/work_items/components/work_item_labels_spec.js
index 28aa7ffa1be..d7bebac6dbd 100644
--- a/spec/frontend/work_items/components/work_item_labels_spec.js
+++ b/spec/frontend/work_items/components/work_item_labels_spec.js
@@ -5,7 +5,8 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
-import labelSearchQuery from '~/sidebar/components/labels/labels_select_widget/graphql/project_labels.query.graphql';
+import groupLabelsQuery from '~/sidebar/components/labels/labels_select_widget/graphql/group_labels.query.graphql';
+import projectLabelsQuery from '~/sidebar/components/labels/labels_select_widget/graphql/project_labels.query.graphql';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import groupWorkItemByIidQuery from '~/work_items/graphql/group_work_item_by_iid.query.graphql';
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
@@ -37,7 +38,8 @@ describe('WorkItemLabels component', () => {
const groupWorkItemQuerySuccess = jest
.fn()
.mockResolvedValue(groupWorkItemByIidResponseFactory({ labels: null }));
- const successSearchQueryHandler = jest.fn().mockResolvedValue(projectLabelsResponse);
+ const projectLabelsQueryHandler = jest.fn().mockResolvedValue(projectLabelsResponse);
+ const groupLabelsQueryHandler = jest.fn().mockResolvedValue(projectLabelsResponse);
const successUpdateWorkItemMutationHandler = jest
.fn()
.mockResolvedValue(updateWorkItemMutationResponse);
@@ -47,7 +49,7 @@ describe('WorkItemLabels component', () => {
canUpdate = true,
isGroup = false,
workItemQueryHandler = workItemQuerySuccess,
- searchQueryHandler = successSearchQueryHandler,
+ searchQueryHandler = projectLabelsQueryHandler,
updateWorkItemMutationHandler = successUpdateWorkItemMutationHandler,
workItemIid = '1',
} = {}) => {
@@ -55,7 +57,8 @@ describe('WorkItemLabels component', () => {
apolloProvider: createMockApollo([
[workItemByIidQuery, workItemQueryHandler],
[groupWorkItemByIidQuery, groupWorkItemQuerySuccess],
- [labelSearchQuery, searchQueryHandler],
+ [projectLabelsQuery, searchQueryHandler],
+ [groupLabelsQuery, groupLabelsQueryHandler],
[updateWorkItemMutation, updateWorkItemMutationHandler],
]),
provide: {
@@ -179,7 +182,7 @@ describe('WorkItemLabels component', () => {
findTokenSelector().vm.$emit('text-input', searchKey);
await waitForPromises();
- expect(successSearchQueryHandler).toHaveBeenCalledWith(
+ expect(projectLabelsQueryHandler).toHaveBeenCalledWith(
expect.objectContaining({ searchTerm: searchKey }),
);
});
@@ -273,6 +276,16 @@ describe('WorkItemLabels component', () => {
expect(workItemQuerySuccess).not.toHaveBeenCalled();
});
+
+ it('calls the project labels query on search', async () => {
+ createComponent();
+
+ findTokenSelector().vm.$emit('focus');
+ findTokenSelector().vm.$emit('text-input', 'hello');
+ await waitForPromises();
+
+ expect(projectLabelsQueryHandler).toHaveBeenCalled();
+ });
});
describe('when group context', () => {
@@ -296,5 +309,15 @@ describe('WorkItemLabels component', () => {
expect(groupWorkItemQuerySuccess).not.toHaveBeenCalled();
});
+
+ it('calls the group labels query on search', async () => {
+ createComponent({ isGroup: true });
+
+ findTokenSelector().vm.$emit('focus');
+ findTokenSelector().vm.$emit('text-input', 'hello');
+ await waitForPromises();
+
+ expect(groupLabelsQueryHandler).toHaveBeenCalled();
+ });
});
});
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js
index 9addf6c3450..36af0c5b3c8 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js
@@ -91,6 +91,7 @@ describe('WorkItemLinkChild', () => {
childItem: workItemObjectiveWithChild,
canUpdate: true,
showTaskIcon: false,
+ showLabels: true,
});
});
});
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js
index 0b88b3ff5b4..f8b2736c0f8 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js
@@ -1,5 +1,6 @@
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
+import { GlToggle } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -93,6 +94,7 @@ describe('WorkItemLinks', () => {
const findWorkItemDetailModal = () => wrapper.findComponent(WorkItemDetailModal);
const findAbuseCategorySelector = () => wrapper.findComponent(AbuseCategorySelector);
const findWorkItemLinkChildrenWrapper = () => wrapper.findComponent(WorkItemChildrenWrapper);
+ const findShowLabelsToggle = () => wrapper.findComponent(GlToggle);
afterEach(() => {
mockApollo = null;
@@ -278,4 +280,21 @@ describe('WorkItemLinks', () => {
expect(groupResponseWithAddChildPermission).toHaveBeenCalled();
});
});
+
+ it.each`
+ toggleValue
+ ${true}
+ ${false}
+ `(
+ 'passes showLabels as $toggleValue to child items when toggle is $toggleValue',
+ async ({ toggleValue }) => {
+ await createComponent();
+
+ findShowLabelsToggle().vm.$emit('change', toggleValue);
+
+ await nextTick();
+
+ expect(findWorkItemLinkChildrenWrapper().props('showLabels')).toBe(toggleValue);
+ },
+ );
});
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js
index f30fded0b45..6c1d1035c3d 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js
@@ -1,4 +1,5 @@
import { nextTick } from 'vue';
+import { GlToggle } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import WidgetWrapper from '~/work_items/components/widget_wrapper.vue';
import WorkItemTree from '~/work_items/components/work_item_links/work_item_tree.vue';
@@ -20,6 +21,7 @@ describe('WorkItemTree', () => {
const findForm = () => wrapper.findComponent(WorkItemLinksForm);
const findWidgetWrapper = () => wrapper.findComponent(WidgetWrapper);
const findWorkItemLinkChildrenWrapper = () => wrapper.findComponent(WorkItemChildrenWrapper);
+ const findShowLabelsToggle = () => wrapper.findComponent(GlToggle);
const createComponent = ({
workItemType = 'Objective',
@@ -126,4 +128,21 @@ describe('WorkItemTree', () => {
expect(wrapper.emitted('addChild')).toEqual([[]]);
});
+
+ it.each`
+ toggleValue
+ ${true}
+ ${false}
+ `(
+ 'passes showLabels as $toggleValue to child items when toggle is $toggleValue',
+ async ({ toggleValue }) => {
+ createComponent();
+
+ findShowLabelsToggle().vm.$emit('change', toggleValue);
+
+ await nextTick();
+
+ expect(findWorkItemLinkChildrenWrapper().props('showLabels')).toBe(toggleValue);
+ },
+ );
});
diff --git a/spec/frontend/work_items/components/work_item_milestone_spec.js b/spec/frontend/work_items/components/work_item_milestone_spec.js
index e303ad4b481..fc2c5eb2af2 100644
--- a/spec/frontend/work_items/components/work_item_milestone_spec.js
+++ b/spec/frontend/work_items/components/work_item_milestone_spec.js
@@ -1,14 +1,8 @@
-import {
- GlDropdown,
- GlDropdownItem,
- GlSearchBoxByType,
- GlSkeletonLoader,
- GlFormGroup,
- GlDropdownText,
-} from '@gitlab/ui';
+import { GlCollapsibleListbox, GlListboxItem, GlSkeletonLoader, GlFormGroup } from '@gitlab/ui';
+
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
-import WorkItemMilestone from '~/work_items/components/work_item_milestone.vue';
+import WorkItemMilestone, { noMilestoneId } from '~/work_items/components/work_item_milestone.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mockTracking } from 'helpers/tracking_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
@@ -32,17 +26,13 @@ describe('WorkItemMilestone component', () => {
const workItemId = 'gid://gitlab/WorkItem/1';
const workItemType = 'Task';
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
+ const findDropdown = () => wrapper.findComponent(GlCollapsibleListbox);
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
- const findNoMilestoneDropdownItem = () => wrapper.findByTestId('no-milestone');
- const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
- const findFirstDropdownItem = () => findDropdownItems().at(0);
- const findDropdownTexts = () => wrapper.findAllComponents(GlDropdownText);
- const findDropdownItemAtIndex = (index) => findDropdownItems().at(index);
+ const findNoMilestoneDropdownItem = () => wrapper.findByTestId('listbox-item-no-milestone-id');
+ const findDropdownItems = () => wrapper.findAllComponents(GlListboxItem);
const findDisabledTextSpan = () => wrapper.findByTestId('disabled-text');
- const findDropdownTextAtIndex = (index) => findDropdownTexts().at(index);
const findInputGroup = () => wrapper.findComponent(GlFormGroup);
+ const findNoResultsText = () => wrapper.findByTestId('no-results-text');
const successSearchQueryHandler = jest.fn().mockResolvedValue(projectMilestonesResponse);
const successSearchWithNoMatchingMilestones = jest
@@ -74,8 +64,7 @@ describe('WorkItemMilestone component', () => {
workItemType,
},
stubs: {
- GlDropdown,
- GlSearchBoxByType,
+ GlCollapsibleListbox,
},
});
};
@@ -106,7 +95,7 @@ describe('WorkItemMilestone component', () => {
it(`has a value of "Add to milestone"`, () => {
createComponent({ canUpdate: true, milestone: null });
- expect(findDropdown().props('text')).toBe(WorkItemMilestone.i18n.MILESTONE_PLACEHOLDER);
+ expect(findDropdown().props('toggleText')).toBe(WorkItemMilestone.i18n.MILESTONE_PLACEHOLDER);
});
});
@@ -114,7 +103,7 @@ describe('WorkItemMilestone component', () => {
it('has the search box', () => {
createComponent();
- expect(findSearchBox().exists()).toBe(true);
+ expect(findDropdown().props('searchable')).toBe(true);
});
it('shows no matching results when no items', () => {
@@ -122,9 +111,8 @@ describe('WorkItemMilestone component', () => {
searchQueryHandler: successSearchWithNoMatchingMilestones,
});
- expect(findDropdownTextAtIndex(0).text()).toBe(WorkItemMilestone.i18n.NO_MATCHING_RESULTS);
+ expect(findNoResultsText().text()).toBe(WorkItemMilestone.i18n.NO_MATCHING_RESULTS);
expect(findDropdownItems()).toHaveLength(1);
- expect(findDropdownTexts()).toHaveLength(1);
});
});
@@ -165,16 +153,18 @@ describe('WorkItemMilestone component', () => {
it('changes the milestone to null when clicked on no milestone', async () => {
showDropdown();
- findFirstDropdownItem().vm.$emit('click');
+ findDropdown().vm.$emit('select', noMilestoneId);
hideDropdown();
await nextTick();
expect(findDropdown().props('loading')).toBe(true);
await waitForPromises();
-
- expect(findDropdown().props('loading')).toBe(false);
- expect(findDropdown().props('text')).toBe(WorkItemMilestone.i18n.MILESTONE_PLACEHOLDER);
+ expect(findDropdown().props()).toMatchObject({
+ loading: false,
+ toggleText: WorkItemMilestone.i18n.MILESTONE_PLACEHOLDER,
+ toggleClass: expect.arrayContaining(['gl-text-gray-500!']),
+ });
});
it('changes the milestone to the selected milestone', async () => {
@@ -182,15 +172,16 @@ describe('WorkItemMilestone component', () => {
/** the index is -1 since no matching results is also a dropdown item */
const milestoneAtIndex =
projectMilestonesResponse.data.workspace.attributes.nodes[milestoneIndex - 1];
+
showDropdown();
await waitForPromises();
- findDropdownItemAtIndex(milestoneIndex).vm.$emit('click');
+ findDropdown().vm.$emit('select', milestoneAtIndex.id);
hideDropdown();
await waitForPromises();
- expect(findDropdown().props('text')).toBe(milestoneAtIndex.title);
+ expect(findDropdown().props('toggleText')).toBe(milestoneAtIndex.title);
});
});
@@ -208,7 +199,7 @@ describe('WorkItemMilestone component', () => {
});
showDropdown();
- findFirstDropdownItem().vm.$emit('click');
+ findDropdown().vm.$emit('select', noMilestoneId);
hideDropdown();
await waitForPromises();
@@ -224,7 +215,7 @@ describe('WorkItemMilestone component', () => {
createComponent({ canUpdate: true });
showDropdown();
- findFirstDropdownItem().vm.$emit('click');
+ findDropdown().vm.$emit('select', noMilestoneId);
hideDropdown();
await waitForPromises();
diff --git a/spec/frontend/work_items/components/work_item_parent_spec.js b/spec/frontend/work_items/components/work_item_parent_spec.js
index a72eeabc43c..11fe6dffbfa 100644
--- a/spec/frontend/work_items/components/work_item_parent_spec.js
+++ b/spec/frontend/work_items/components/work_item_parent_spec.js
@@ -1,14 +1,15 @@
-import * as Sentry from '@sentry/browser';
import { GlCollapsibleListbox, GlFormGroup } from '@gitlab/ui';
-
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import waitForPromises from 'helpers/wait_for_promises';
import createMockApollo from 'helpers/mock_apollo_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import WorkItemParent from '~/work_items/components/work_item_parent.vue';
+import { removeHierarchyChild } from '~/work_items/graphql/cache_utils';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
+import groupWorkItemsQuery from '~/work_items/graphql/group_work_items.query.graphql';
import projectWorkItemsQuery from '~/work_items/graphql/project_work_items.query.graphql';
import { WORK_ITEM_TYPE_ENUM_OBJECTIVE } from '~/work_items/constants';
@@ -20,7 +21,10 @@ import {
updateWorkItemMutationErrorResponse,
} from '../mock_data';
-jest.mock('@sentry/browser');
+jest.mock('~/sentry/sentry_browser_wrapper');
+jest.mock('~/work_items/graphql/cache_utils', () => ({
+ removeHierarchyChild: jest.fn(),
+}));
describe('WorkItemParent component', () => {
Vue.use(VueApollo);
@@ -29,7 +33,9 @@ describe('WorkItemParent component', () => {
const workItemId = 'gid://gitlab/WorkItem/1';
const workItemType = 'Objective';
+ const mockFullPath = 'full-path';
+ const groupWorkItemsSuccessHandler = jest.fn().mockResolvedValue(availableObjectivesResponse);
const availableWorkItemsSuccessHandler = jest.fn().mockResolvedValue(availableObjectivesResponse);
const availableWorkItemsFailureHandler = jest.fn().mockRejectedValue(new Error());
@@ -42,14 +48,17 @@ describe('WorkItemParent component', () => {
parent = null,
searchQueryHandler = availableWorkItemsSuccessHandler,
mutationHandler = successUpdateWorkItemMutationHandler,
+ isGroup = false,
} = {}) => {
wrapper = shallowMountExtended(WorkItemParent, {
apolloProvider: createMockApollo([
[projectWorkItemsQuery, searchQueryHandler],
+ [groupWorkItemsQuery, groupWorkItemsSuccessHandler],
[updateWorkItemMutation, mutationHandler],
]),
provide: {
- fullPath: 'full-path',
+ fullPath: mockFullPath,
+ isGroup,
},
propsData: {
canUpdate,
@@ -81,7 +90,6 @@ describe('WorkItemParent component', () => {
headerText: 'Assign parent',
category: 'tertiary',
loading: false,
- noCaret: true,
isCheckCentered: true,
searchable: true,
searching: false,
@@ -90,7 +98,6 @@ describe('WorkItemParent component', () => {
toggleText: 'None',
searchPlaceholder: 'Search',
resetButtonLabel: 'Unassign',
- block: true,
});
});
@@ -98,14 +105,12 @@ describe('WorkItemParent component', () => {
createComponent({ canUpdate: false, parent: mockParentWidgetResponse });
expect(findCollapsibleListbox().exists()).toBe(false);
- expect(findParentText().exists()).toBe(true);
expect(findParentText().text()).toBe('Objective 101');
});
it('shows loading while searching', async () => {
await findCollapsibleListbox().vm.$emit('shown');
expect(findCollapsibleListbox().props('searching')).toBe(true);
- expect(findCollapsibleListbox().props('no-caret')).toBeUndefined();
});
});
@@ -143,15 +148,27 @@ describe('WorkItemParent component', () => {
});
await findCollapsibleListbox().vm.$emit('shown');
- await findCollapsibleListbox().vm.$emit('search', 'Objective 101');
await waitForPromises();
expect(searchedItemQueryHandler).toHaveBeenCalledWith({
fullPath: 'full-path',
+ searchTerm: '',
+ types: [WORK_ITEM_TYPE_ENUM_OBJECTIVE],
+ in: undefined,
+ iid: null,
+ isNumber: false,
+ });
+
+ await findCollapsibleListbox().vm.$emit('search', 'Objective 101');
+
+ expect(searchedItemQueryHandler).toHaveBeenCalledWith({
+ fullPath: 'full-path',
searchTerm: 'Objective 101',
types: [WORK_ITEM_TYPE_ENUM_OBJECTIVE],
in: 'TITLE',
+ iid: null,
+ isNumber: false,
});
await nextTick();
@@ -164,7 +181,6 @@ describe('WorkItemParent component', () => {
describe('listbox', () => {
const selectWorkItem = async (workItem) => {
- await findCollapsibleListbox().vm.$emit('shown');
await findCollapsibleListbox().vm.$emit('select', workItem);
};
@@ -181,6 +197,14 @@ describe('WorkItemParent component', () => {
},
},
});
+
+ expect(removeHierarchyChild).toHaveBeenCalledWith({
+ cache: expect.anything(Object),
+ fullPath: mockFullPath,
+ iid: undefined,
+ isGroup: false,
+ workItem: { id: 'gid://gitlab/WorkItem/1' },
+ });
});
it('calls mutation when item is unassigned', async () => {
@@ -188,6 +212,9 @@ describe('WorkItemParent component', () => {
.fn()
.mockResolvedValue(updateWorkItemMutationResponseFactory({ parent: null }));
createComponent({
+ parent: {
+ iid: '1',
+ },
mutationHandler: unAssignParentWorkItemMutationHandler,
});
@@ -203,6 +230,13 @@ describe('WorkItemParent component', () => {
},
},
});
+ expect(removeHierarchyChild).toHaveBeenCalledWith({
+ cache: expect.anything(Object),
+ fullPath: mockFullPath,
+ iid: '1',
+ isGroup: false,
+ workItem: { id: 'gid://gitlab/WorkItem/1' },
+ });
});
it('emits error when mutation fails', async () => {
@@ -233,4 +267,34 @@ describe('WorkItemParent component', () => {
expect(Sentry.captureException).toHaveBeenCalledWith(error);
});
});
+
+ describe('when project context', () => {
+ beforeEach(() => {
+ createComponent();
+ findCollapsibleListbox().vm.$emit('shown');
+ });
+
+ it('calls the project work items query', () => {
+ expect(availableWorkItemsSuccessHandler).toHaveBeenCalled();
+ });
+
+ it('skips calling the group work items query', () => {
+ expect(groupWorkItemsSuccessHandler).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when group context', () => {
+ beforeEach(() => {
+ createComponent({ isGroup: true });
+ findCollapsibleListbox().vm.$emit('shown');
+ });
+
+ it('skips calling the project work items query', () => {
+ expect(availableWorkItemsSuccessHandler).not.toHaveBeenCalled();
+ });
+
+ it('calls the group work items query', () => {
+ expect(groupWorkItemsSuccessHandler).toHaveBeenCalled();
+ });
+ });
});
diff --git a/spec/frontend/work_items/components/work_item_relationships/__snapshots__/work_item_relationship_list_spec.js.snap b/spec/frontend/work_items/components/work_item_relationships/__snapshots__/work_item_relationship_list_spec.js.snap
index bbc19a011a5..20af8584e37 100644
--- a/spec/frontend/work_items/components/work_item_relationships/__snapshots__/work_item_relationship_list_spec.js.snap
+++ b/spec/frontend/work_items/components/work_item_relationships/__snapshots__/work_item_relationship_list_spec.js.snap
@@ -22,6 +22,7 @@ exports[`WorkItemRelationshipList renders linked item list 1`] = `
<work-item-link-child-contents-stub
canupdate="true"
childitem="[object Object]"
+ showlabels="true"
showtaskicon="true"
/>
</li>
diff --git a/spec/frontend/work_items/components/work_item_relationships/work_item_add_relationship_form_spec.js b/spec/frontend/work_items/components/work_item_relationships/work_item_add_relationship_form_spec.js
index d7b3ced2ff9..520cf5f7ea4 100644
--- a/spec/frontend/work_items/components/work_item_relationships/work_item_add_relationship_form_spec.js
+++ b/spec/frontend/work_items/components/work_item_relationships/work_item_add_relationship_form_spec.js
@@ -34,6 +34,9 @@ describe('WorkItemAddRelationshipForm', () => {
wrapper = shallowMountExtended(WorkItemAddRelationshipForm, {
apolloProvider: mockApolloProvider,
+ provide: {
+ isGroup: false,
+ },
propsData: {
workItemId,
workItemIid,
diff --git a/spec/frontend/work_items/components/work_item_relationships/work_item_relationships_spec.js b/spec/frontend/work_items/components/work_item_relationships/work_item_relationships_spec.js
index 7178fa1aae7..0faea0e4862 100644
--- a/spec/frontend/work_items/components/work_item_relationships/work_item_relationships_spec.js
+++ b/spec/frontend/work_items/components/work_item_relationships/work_item_relationships_spec.js
@@ -1,6 +1,6 @@
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlLoadingIcon, GlToggle } from '@gitlab/ui';
import createMockApollo from 'helpers/mock_apollo_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
@@ -82,6 +82,7 @@ describe('WorkItemRelationships', () => {
wrapper.findAllComponents(WorkItemRelationshipList);
const findAddButton = () => wrapper.findByTestId('link-item-add-button');
const findWorkItemRelationshipForm = () => wrapper.findComponent(WorkItemAddRelationshipForm);
+ const findShowLabelsToggle = () => wrapper.findComponent(GlToggle);
it('shows loading icon when query is not processed', () => {
createComponent();
@@ -99,6 +100,11 @@ describe('WorkItemRelationships', () => {
expect(findLinkedItemsHelpLink().attributes('href')).toBe(
'/help/user/okrs.md#linked-items-in-okrs',
);
+ expect(findShowLabelsToggle().props()).toMatchObject({
+ value: true,
+ labelPosition: 'left',
+ label: 'Show labels',
+ });
});
it('renders blocking linked item lists', async () => {
@@ -153,6 +159,29 @@ describe('WorkItemRelationships', () => {
expect(findWorkItemRelationshipForm().exists()).toBe(false);
});
+ it.each`
+ toggleValue
+ ${true}
+ ${false}
+ `(
+ 'passes showLabels as $toggleValue to child items when toggle is $toggleValue',
+ async ({ toggleValue }) => {
+ await createComponent({
+ workItemQueryHandler: jest
+ .fn()
+ .mockResolvedValue(workItemByIidResponseFactory({ linkedItems: mockLinkedItems })),
+ });
+
+ findShowLabelsToggle().vm.$emit('change', toggleValue);
+
+ await nextTick();
+
+ expect(findAllWorkItemRelationshipListComponents().at(0).props('showLabels')).toBe(
+ toggleValue,
+ );
+ },
+ );
+
describe('when project context', () => {
it('calls the project work item query', () => {
createComponent();
diff --git a/spec/frontend/work_items/components/work_item_state_toggle_button_spec.js b/spec/frontend/work_items/components/work_item_state_toggle_button_spec.js
index c0b206e5da4..a210bd50422 100644
--- a/spec/frontend/work_items/components/work_item_state_toggle_button_spec.js
+++ b/spec/frontend/work_items/components/work_item_state_toggle_button_spec.js
@@ -5,7 +5,7 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mockTracking } from 'helpers/tracking_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import WorkItemStateToggleButton from '~/work_items/components/work_item_state_toggle_button.vue';
+import WorkItemStateToggle from '~/work_items/components/work_item_state_toggle.vue';
import {
STATE_OPEN,
STATE_CLOSED,
@@ -33,7 +33,7 @@ describe('Work Item State toggle button component', () => {
workItemState = STATE_OPEN,
workItemType = 'Task',
} = {}) => {
- wrapper = shallowMount(WorkItemStateToggleButton, {
+ wrapper = shallowMount(WorkItemStateToggle, {
apolloProvider: createMockApollo([[updateWorkItemMutation, mutationHandler]]),
propsData: {
workItemId: id,
diff --git a/spec/frontend/work_items/components/work_item_title_spec.js b/spec/frontend/work_items/components/work_item_title_spec.js
index 34391b74cf7..0f466bcf691 100644
--- a/spec/frontend/work_items/components/work_item_title_spec.js
+++ b/spec/frontend/work_items/components/work_item_title_spec.js
@@ -131,5 +131,25 @@ describe('WorkItemTitle component', () => {
property: 'type_Task',
});
});
+
+ describe('when title has more than 255 characters', () => {
+ const title = new Array(257).join('a');
+
+ it('does not call a mutation', () => {
+ createComponent();
+
+ findItemTitle().vm.$emit('title-changed', title);
+
+ expect(mutationSuccessHandler).not.toHaveBeenCalled();
+ });
+
+ it('emits an error message', () => {
+ createComponent();
+
+ findItemTitle().vm.$emit('title-changed', title);
+
+ expect(wrapper.emitted('error')).toEqual([['Title cannot have more than 255 characters.']]);
+ });
+ });
});
});
diff --git a/spec/frontend/work_items/components/work_item_todos_spec.js b/spec/frontend/work_items/components/work_item_todos_spec.js
index c76cdbcee53..d67d84e75b5 100644
--- a/spec/frontend/work_items/components/work_item_todos_spec.js
+++ b/spec/frontend/work_items/components/work_item_todos_spec.js
@@ -34,7 +34,7 @@ describe('WorkItemTodo component', () => {
const workItemQueryResponse = workItemResponseFactory({ canUpdate: true });
const mockWorkItemId = workItemQueryResponse.data.workItem.id;
const mockWorkItemIid = workItemQueryResponse.data.workItem.iid;
- const mockWorkItemFullpath = workItemQueryResponse.data.workItem.project.fullPath;
+ const mockWorkItemFullpath = workItemQueryResponse.data.workItem.namespace.fullPath;
const createTodoSuccessHandler = jest
.fn()
diff --git a/spec/frontend/work_items/list/components/work_items_list_app_spec.js b/spec/frontend/work_items/list/components/work_items_list_app_spec.js
index 96083478e77..401d7dcbbdb 100644
--- a/spec/frontend/work_items/list/components/work_items_list_app_spec.js
+++ b/spec/frontend/work_items/list/components/work_items_list_app_spec.js
@@ -1,7 +1,7 @@
-import * as Sentry from '@sentry/browser';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import IssueCardStatistics from 'ee_else_ce/issues/list/components/issue_card_statistics.vue';
import IssueCardTimeInfo from 'ee_else_ce/issues/list/components/issue_card_time_info.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
@@ -12,7 +12,7 @@ import WorkItemsListApp from '~/work_items/list/components/work_items_list_app.v
import getWorkItemsQuery from '~/work_items/list/queries/get_work_items.query.graphql';
import { groupWorkItemsQueryResponse } from '../../mock_data';
-jest.mock('@sentry/browser');
+jest.mock('~/sentry/sentry_browser_wrapper');
describe('WorkItemsListApp component', () => {
let wrapper;
diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js
index 9eb604c81cb..41e8a01de36 100644
--- a/spec/frontend/work_items/mock_data.js
+++ b/spec/frontend/work_items/mock_data.js
@@ -112,6 +112,7 @@ export const workItemQueryResponse = {
__typename: 'WorkItem',
id: 'gid://gitlab/WorkItem/1',
iid: '1',
+ archived: false,
title: 'Test',
state: 'OPEN',
description: 'description',
@@ -127,11 +128,10 @@ export const workItemQueryResponse = {
webUrl: 'http://127.0.0.1:3000/root',
__typename: 'UserCore',
},
- project: {
+ namespace: {
__typename: 'Project',
id: '1',
fullPath: 'test-project-path',
- archived: false,
name: 'Project name',
},
workItemType: {
@@ -224,6 +224,7 @@ export const updateWorkItemMutationResponse = {
__typename: 'WorkItem',
id: 'gid://gitlab/WorkItem/1',
iid: '1',
+ archived: false,
title: 'Updated title',
state: 'OPEN',
description: 'description',
@@ -234,11 +235,10 @@ export const updateWorkItemMutationResponse = {
author: {
...mockAssignees[0],
},
- project: {
+ namespace: {
__typename: 'Project',
id: '1',
fullPath: 'test-project-path',
- archived: false,
name: 'Project name',
},
workItemType: {
@@ -335,6 +335,7 @@ export const convertWorkItemMutationResponse = {
__typename: 'WorkItem',
id: 'gid://gitlab/WorkItem/1',
iid: '1',
+ archived: false,
title: 'Updated title',
state: 'OPEN',
description: 'description',
@@ -345,11 +346,10 @@ export const convertWorkItemMutationResponse = {
author: {
...mockAssignees[0],
},
- project: {
+ namespace: {
__typename: 'Project',
id: '1',
fullPath: 'test-project-path',
- archived: false,
name: 'Project name',
},
workItemType: {
@@ -626,6 +626,7 @@ export const workItemResponseFactory = ({
__typename: 'WorkItem',
id: 'gid://gitlab/WorkItem/1',
iid,
+ archived: false,
title: 'Updated title',
state,
description: 'description',
@@ -634,11 +635,10 @@ export const workItemResponseFactory = ({
updatedAt,
closedAt: null,
author,
- project: {
+ namespace: {
__typename: 'Project',
id: '1',
fullPath: 'test-project-path',
- archived: false,
name: 'Project name',
},
workItemType,
@@ -901,6 +901,7 @@ export const createWorkItemMutationResponse = {
__typename: 'WorkItem',
id: 'gid://gitlab/WorkItem/1',
iid: '1',
+ archived: false,
title: 'Updated title',
state: 'OPEN',
description: 'description',
@@ -911,11 +912,10 @@ export const createWorkItemMutationResponse = {
author: {
...mockAssignees[0],
},
- project: {
+ namespace: {
__typename: 'Project',
id: '1',
fullPath: 'test-project-path',
- archived: false,
name: 'Project name',
},
workItemType: {
@@ -987,6 +987,7 @@ export const workItemHierarchyEmptyResponse = {
{
id: 'gid://gitlab/WorkItem/1',
iid: '1',
+ archived: false,
state: 'OPEN',
workItemType: {
id: 'gid://gitlab/WorkItems::Type/1',
@@ -1000,11 +1001,10 @@ export const workItemHierarchyEmptyResponse = {
updatedAt: null,
closedAt: null,
author: mockAssignees[0],
- project: {
+ namespace: {
__typename: 'Project',
id: '1',
fullPath: 'test-project-path',
- archived: false,
name: 'Project name',
},
userPermissions: {
@@ -1045,6 +1045,7 @@ export const workItemHierarchyNoUpdatePermissionResponse = {
workItem: {
id: 'gid://gitlab/WorkItem/1',
iid: '1',
+ archived: false,
state: 'OPEN',
workItemType: {
id: 'gid://gitlab/WorkItems::Type/6',
@@ -1067,11 +1068,10 @@ export const workItemHierarchyNoUpdatePermissionResponse = {
adminWorkItemLink: true,
__typename: 'WorkItemPermissions',
},
- project: {
+ namespace: {
__typename: 'Project',
id: '1',
fullPath: 'test-project-path',
- archived: false,
name: 'Project name',
},
confidential: false,
@@ -1207,6 +1207,7 @@ export const workItemHierarchyResponse = {
{
id: 'gid://gitlab/WorkItem/1',
iid: '1',
+ archived: false,
workItemType: {
id: 'gid://gitlab/WorkItems::Type/1',
name: 'Issue',
@@ -1227,11 +1228,10 @@ export const workItemHierarchyResponse = {
...mockAssignees[0],
},
confidential: false,
- project: {
+ namespace: {
__typename: 'Project',
id: '1',
fullPath: 'test-project-path',
- archived: false,
name: 'Project name',
},
description: 'Issue description',
@@ -1303,17 +1303,17 @@ export const workItemObjectiveMetadataWidgets = {
export const workItemObjectiveWithChild = {
id: 'gid://gitlab/WorkItem/12',
iid: '12',
+ archived: false,
workItemType: {
id: 'gid://gitlab/WorkItems::Type/2411',
name: 'Objective',
iconName: 'issue-type-objective',
__typename: 'WorkItemType',
},
- project: {
+ namespace: {
__typename: 'Project',
id: '1',
fullPath: 'test-project-path',
- archived: false,
name: 'Project name',
},
userPermissions: {
@@ -1381,6 +1381,7 @@ export const workItemHierarchyTreeResponse = {
workItem: {
id: 'gid://gitlab/WorkItem/2',
iid: '2',
+ archived: false,
workItemType: {
id: 'gid://gitlab/WorkItems::Type/2411',
name: 'Objective',
@@ -1398,11 +1399,10 @@ export const workItemHierarchyTreeResponse = {
__typename: 'WorkItemPermissions',
},
confidential: false,
- project: {
+ namespace: {
__typename: 'Project',
id: '1',
fullPath: 'test-project-path',
- archived: false,
name: 'Project name',
},
widgets: [
@@ -1483,6 +1483,7 @@ export const changeIndirectWorkItemParentMutationResponse = {
description: null,
id: 'gid://gitlab/WorkItem/13',
iid: '13',
+ archived: false,
state: 'OPEN',
title: 'Objective 2',
confidential: false,
@@ -1492,11 +1493,10 @@ export const changeIndirectWorkItemParentMutationResponse = {
author: {
...mockAssignees[0],
},
- project: {
+ namespace: {
__typename: 'Project',
id: '1',
fullPath: 'test-project-path',
- archived: false,
name: 'Project name',
},
reference: 'test-project-path#13',
@@ -1552,6 +1552,7 @@ export const changeWorkItemParentMutationResponse = {
description: null,
id: 'gid://gitlab/WorkItem/2',
iid: '2',
+ archived: false,
state: 'OPEN',
title: 'Foo',
confidential: false,
@@ -1561,11 +1562,10 @@ export const changeWorkItemParentMutationResponse = {
author: {
...mockAssignees[0],
},
- project: {
+ namespace: {
__typename: 'Project',
id: '1',
fullPath: 'test-project-path',
- archived: false,
name: 'Project name',
},
reference: 'test-project-path#2',
@@ -1600,27 +1600,18 @@ export const availableWorkItemsResponse = {
id: 'gid://gitlab/WorkItem/458',
iid: '2',
title: 'Task 1',
- state: 'OPEN',
- createdAt: '2022-08-03T12:41:54Z',
- confidential: false,
__typename: 'WorkItem',
},
{
id: 'gid://gitlab/WorkItem/459',
iid: '3',
title: 'Task 2',
- state: 'OPEN',
- createdAt: '2022-08-03T12:41:54Z',
- confidential: false,
__typename: 'WorkItem',
},
{
id: 'gid://gitlab/WorkItem/460',
iid: '4',
title: 'Task 3',
- state: 'OPEN',
- createdAt: '2022-08-03T12:41:54Z',
- confidential: true,
__typename: 'WorkItem',
},
],
@@ -1640,24 +1631,18 @@ export const availableObjectivesResponse = {
id: 'gid://gitlab/WorkItem/716',
iid: '122',
title: 'Objective 101',
- state: 'OPEN',
- confidential: false,
__typename: 'WorkItem',
},
{
id: 'gid://gitlab/WorkItem/712',
iid: '118',
title: 'Objective 103',
- state: 'OPEN',
- confidential: false,
__typename: 'WorkItem',
},
{
id: 'gid://gitlab/WorkItem/711',
iid: '117',
title: 'Objective 102',
- state: 'OPEN',
- confidential: false,
__typename: 'WorkItem',
},
],
@@ -1677,8 +1662,6 @@ export const searchedObjectiveResponse = {
id: 'gid://gitlab/WorkItem/716',
iid: '122',
title: 'Objective 101',
- state: 'OPEN',
- confidential: false,
__typename: 'WorkItem',
},
],
@@ -1687,7 +1670,7 @@ export const searchedObjectiveResponse = {
},
};
-export const searchedWorkItemsResponse = {
+export const searchWorkItemsTextResponse = {
data: {
workspace: {
__typename: 'Project',
@@ -1698,9 +1681,57 @@ export const searchedWorkItemsResponse = {
id: 'gid://gitlab/WorkItem/459',
iid: '3',
title: 'Task 2',
- state: 'OPEN',
- createdAt: '2022-08-03T12:41:54Z',
- confidential: false,
+ __typename: 'WorkItem',
+ },
+ ],
+ },
+ },
+ },
+};
+
+export const searchWorkItemsIidResponse = {
+ data: {
+ workspace: {
+ __typename: 'Project',
+ id: 'gid://gitlab/Project/2',
+ workItems: {
+ nodes: [],
+ },
+ workItemsByIid: {
+ nodes: [
+ {
+ id: 'gid://gitlab/WorkItem/460',
+ iid: '101',
+ title: 'Task 3',
+ __typename: 'WorkItem',
+ },
+ ],
+ },
+ },
+ },
+};
+
+export const searchWorkItemsTextIidResponse = {
+ data: {
+ workspace: {
+ __typename: 'Project',
+ id: 'gid://gitlab/Project/2',
+ workItems: {
+ nodes: [
+ {
+ id: 'gid://gitlab/WorkItem/459',
+ iid: '3',
+ title: 'Task 123',
+ __typename: 'WorkItem',
+ },
+ ],
+ },
+ workItemsByIid: {
+ nodes: [
+ {
+ id: 'gid://gitlab/WorkItem/460',
+ iid: '123',
+ title: 'Task 2',
__typename: 'WorkItem',
},
],
diff --git a/spec/frontend_integration/snippets/snippets_notes_spec.js b/spec/frontend_integration/snippets/snippets_notes_spec.js
index 27be7793ce6..b7d2c8924f6 100644
--- a/spec/frontend_integration/snippets/snippets_notes_spec.js
+++ b/spec/frontend_integration/snippets/snippets_notes_spec.js
@@ -51,7 +51,7 @@ describe('Integration Snippets notes', () => {
'circled latin capital letter m',
],
],
- [':', ['100', '1234', '8ball', 'a', 'ab']],
+ [':', ['grinning', 'smiley', 'smile', 'grin', 'laughing']],
// We do not want the search to start with space https://gitlab.com/gitlab-org/gitlab/-/issues/322548
[': ', []],
// We want to preserve that we can have space INSIDE the search
diff --git a/spec/frontend_integration/test_helpers/mock_server/routes/emojis.js b/spec/frontend_integration/test_helpers/mock_server/routes/emojis.js
index 83991ad5af9..72953b94552 100644
--- a/spec/frontend_integration/test_helpers/mock_server/routes/emojis.js
+++ b/spec/frontend_integration/test_helpers/mock_server/routes/emojis.js
@@ -1,5 +1,5 @@
import { Response } from 'miragejs';
-import emojis from 'public/-/emojis/2/emojis.json';
+import emojis from 'public/-/emojis/3/emojis.json';
import { EMOJI_VERSION } from '~/emoji';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
diff --git a/spec/frontend_integration/test_helpers/mock_server/routes/graphql.js b/spec/frontend_integration/test_helpers/mock_server/routes/graphql.js
index a22763dcb45..d9b8f8aaeb5 100644
--- a/spec/frontend_integration/test_helpers/mock_server/routes/graphql.js
+++ b/spec/frontend_integration/test_helpers/mock_server/routes/graphql.js
@@ -2,10 +2,8 @@ import { graphqlQuery } from '../graphql';
export default (server) => {
server.post('/api/graphql', (schema, request) => {
- const batches = JSON.parse(request.requestBody);
+ const { query, variables } = JSON.parse(request.requestBody);
- return Promise.all(
- batches.map(({ query, variables }) => graphqlQuery(query, variables, schema)),
- );
+ return graphqlQuery(query, variables, schema);
});
};
diff --git a/spec/graphql/mutations/base_mutation_spec.rb b/spec/graphql/mutations/base_mutation_spec.rb
deleted file mode 100644
index a73d914f48f..00000000000
--- a/spec/graphql/mutations/base_mutation_spec.rb
+++ /dev/null
@@ -1,58 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe ::Mutations::BaseMutation, feature_category: :api do
- include GraphqlHelpers
-
- describe 'argument nullability' do
- let_it_be(:user) { create(:user) }
- let_it_be(:context) { { current_user: user } }
-
- subject(:mutation) { mutation_class.new(object: nil, context: context, field: nil) }
-
- describe 'when using a mutation with correct argument declarations' do
- context 'when argument is nullable and required' do
- let(:mutation_class) do
- Class.new(described_class) do
- graphql_name 'BaseMutation'
- argument :foo, GraphQL::Types::String, required: :nullable
- end
- end
-
- specify do
- expect { subject.ready? }.to raise_error(ArgumentError, /must be provided: foo/)
- end
-
- specify do
- expect { subject.ready?(foo: nil) }.not_to raise_error
- end
-
- specify do
- expect { subject.ready?(foo: "bar") }.not_to raise_error
- end
- end
-
- context 'when argument is required and NOT nullable' do
- let(:mutation_class) do
- Class.new(described_class) do
- graphql_name 'BaseMutation'
- argument :foo, GraphQL::Types::String, required: true
- end
- end
-
- specify do
- expect { subject.ready? }.to raise_error(ArgumentError, /must be provided/)
- end
-
- specify do
- expect { subject.ready?(foo: nil) }.to raise_error(ArgumentError, /must be provided/)
- end
-
- specify do
- expect { subject.ready?(foo: "bar") }.not_to raise_error
- end
- end
- end
- end
-end
diff --git a/spec/graphql/mutations/ci/runner/delete_spec.rb b/spec/graphql/mutations/ci/runner/delete_spec.rb
index f19fa7c34a9..beff18e1dfd 100644
--- a/spec/graphql/mutations/ci/runner/delete_spec.rb
+++ b/spec/graphql/mutations/ci/runner/delete_spec.rb
@@ -48,7 +48,7 @@ RSpec.describe Mutations::Ci::Runner::Delete, feature_category: :runner_fleet do
let(:mutation_params) { {} }
it 'raises an error' do
- expect { subject }.to raise_error(ArgumentError, "Arguments must be provided: id")
+ expect { subject }.to raise_error(ArgumentError, "missing keyword: :id")
end
end
diff --git a/spec/graphql/mutations/ci/runner/update_spec.rb b/spec/graphql/mutations/ci/runner/update_spec.rb
index 02bb7ee2170..03bfd4d738b 100644
--- a/spec/graphql/mutations/ci/runner/update_spec.rb
+++ b/spec/graphql/mutations/ci/runner/update_spec.rb
@@ -42,7 +42,7 @@ RSpec.describe Mutations::Ci::Runner::Update, feature_category: :runner_fleet do
let(:mutation_params) { {} }
it 'raises an error' do
- expect { response }.to raise_error(ArgumentError, "Arguments must be provided: id")
+ expect { response }.to raise_error(ArgumentError, "missing keyword: :id")
end
end
diff --git a/spec/graphql/mutations/container_repositories/destroy_tags_spec.rb b/spec/graphql/mutations/container_repositories/destroy_tags_spec.rb
index 96dd1754155..9a06eb81903 100644
--- a/spec/graphql/mutations/container_repositories/destroy_tags_spec.rb
+++ b/spec/graphql/mutations/container_repositories/destroy_tags_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Mutations::ContainerRepositories::DestroyTags do
+RSpec.describe Mutations::ContainerRepositories::DestroyTags, feature_category: :container_registry do
include GraphqlHelpers
include_context 'container repository delete tags service shared context'
@@ -23,7 +23,7 @@ RSpec.describe Mutations::ContainerRepositories::DestroyTags do
shared_examples 'destroying container repository tags' do
before do
stub_delete_reference_requests(tags)
- expect_delete_tag_by_names(tags)
+ expect_delete_tags(tags)
allow_next_instance_of(ContainerRegistry::Client) do |client|
allow(client).to receive(:supports_tag_delete?).and_return(true)
end
diff --git a/spec/graphql/mutations/merge_requests/update_spec.rb b/spec/graphql/mutations/merge_requests/update_spec.rb
index 6ced71c5f4c..c34ec939d12 100644
--- a/spec/graphql/mutations/merge_requests/update_spec.rb
+++ b/spec/graphql/mutations/merge_requests/update_spec.rb
@@ -153,20 +153,6 @@ RSpec.describe Mutations::MergeRequests::Update, feature_category: :team_plannin
subject(:ready) { mutation.ready?(**arguments) }
- context 'when required arguments are not provided' do
- let(:arguments) { {} }
-
- it 'raises an argument error' do
- expect { subject }.to raise_error(ArgumentError, 'Arguments must be provided: projectPath, iid')
- end
- end
-
- context 'when required arguments are provided' do
- it 'returns true' do
- expect(subject).to eq(true)
- end
- end
-
context 'when timeEstimate is provided' do
let(:extra_args) { { time_estimate: time_estimate } }
diff --git a/spec/graphql/mutations/namespace/package_settings/update_spec.rb b/spec/graphql/mutations/namespace/package_settings/update_spec.rb
index b7f9eac3755..f4e79481d44 100644
--- a/spec/graphql/mutations/namespace/package_settings/update_spec.rb
+++ b/spec/graphql/mutations/namespace/package_settings/update_spec.rb
@@ -73,18 +73,6 @@ RSpec.describe Mutations::Namespace::PackageSettings::Update, feature_category:
)
end
end
-
- context 'when nuget_duplicates_option FF is disabled' do
- let_it_be(:params) { { namespace_path: namespace.full_path, nuget_duplicates_allowed: false } }
-
- before do
- stub_feature_flags(nuget_duplicates_option: false)
- end
-
- it 'raises an error' do
- expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable, /feature flag is disabled/)
- end
- end
end
RSpec.shared_examples 'denying access to namespace package setting' do
diff --git a/spec/graphql/mutations/saved_replies/create_spec.rb b/spec/graphql/mutations/saved_replies/create_spec.rb
index 9423ba2b354..9db23ab5345 100644
--- a/spec/graphql/mutations/saved_replies/create_spec.rb
+++ b/spec/graphql/mutations/saved_replies/create_spec.rb
@@ -14,33 +14,17 @@ RSpec.describe Mutations::SavedReplies::Create do
mutation.resolve(**mutation_arguments)
end
- context 'when feature is disabled' do
- before do
- stub_feature_flags(saved_replies: false)
- end
-
- it 'raises Gitlab::Graphql::Errors::ResourceNotAvailable' do
- expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable, 'Feature disabled')
- end
- end
-
- context 'when feature is enabled for current user' do
- before do
- stub_feature_flags(saved_replies: current_user)
- end
+ context 'when service fails to create a new saved reply' do
+ let(:mutation_arguments) { { name: '', content: '' } }
- context 'when service fails to create a new saved reply' do
- let(:mutation_arguments) { { name: '', content: '' } }
-
- it { expect(subject[:saved_reply]).to be_nil }
- it { expect(subject[:errors]).to match_array(["Content can't be blank", "Name can't be blank"]) }
- end
+ it { expect(subject[:saved_reply]).to be_nil }
+ it { expect(subject[:errors]).to match_array(["Content can't be blank", "Name can't be blank"]) }
+ end
- context 'when service successfully creates a new saved reply' do
- it { expect(subject[:saved_reply].name).to eq(mutation_arguments[:name]) }
- it { expect(subject[:saved_reply].content).to eq(mutation_arguments[:content]) }
- it { expect(subject[:errors]).to be_empty }
- end
+ context 'when service successfully creates a new saved reply' do
+ it { expect(subject[:saved_reply].name).to eq(mutation_arguments[:name]) }
+ it { expect(subject[:saved_reply].content).to eq(mutation_arguments[:content]) }
+ it { expect(subject[:errors]).to be_empty }
end
end
end
diff --git a/spec/graphql/mutations/saved_replies/destroy_spec.rb b/spec/graphql/mutations/saved_replies/destroy_spec.rb
index 6cff28ec0b2..84efd9ee0b8 100644
--- a/spec/graphql/mutations/saved_replies/destroy_spec.rb
+++ b/spec/graphql/mutations/saved_replies/destroy_spec.rb
@@ -13,34 +13,18 @@ RSpec.describe Mutations::SavedReplies::Destroy do
mutation.resolve(id: saved_reply.to_global_id)
end
- context 'when feature is disabled' do
+ context 'when service fails to delete a new saved reply' do
before do
- stub_feature_flags(saved_replies: false)
+ saved_reply.destroy!
end
it 'raises Gitlab::Graphql::Errors::ResourceNotAvailable' do
- expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable, 'Feature disabled')
+ expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
- context 'when feature is enabled for current user' do
- before do
- stub_feature_flags(saved_replies: current_user)
- end
-
- context 'when service fails to delete a new saved reply' do
- before do
- saved_reply.destroy!
- end
-
- it 'raises Gitlab::Graphql::Errors::ResourceNotAvailable' do
- expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
- end
- end
-
- context 'when service successfully deletes the saved reply' do
- it { expect(subject[:errors]).to be_empty }
- end
+ context 'when service successfully deletes the saved reply' do
+ it { expect(subject[:errors]).to be_empty }
end
end
end
diff --git a/spec/graphql/mutations/saved_replies/update_spec.rb b/spec/graphql/mutations/saved_replies/update_spec.rb
index 9b0e90b7b41..cc358d946a5 100644
--- a/spec/graphql/mutations/saved_replies/update_spec.rb
+++ b/spec/graphql/mutations/saved_replies/update_spec.rb
@@ -15,33 +15,17 @@ RSpec.describe Mutations::SavedReplies::Update do
mutation.resolve(id: saved_reply.to_global_id, **mutation_arguments)
end
- context 'when feature is disabled' do
- before do
- stub_feature_flags(saved_replies: false)
- end
-
- it 'raises Gitlab::Graphql::Errors::ResourceNotAvailable' do
- expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable, 'Feature disabled')
- end
- end
-
- context 'when feature is enabled for current user' do
- before do
- stub_feature_flags(saved_replies: current_user)
- end
+ context 'when service fails to update a new saved reply' do
+ let(:mutation_arguments) { { name: '', content: '' } }
- context 'when service fails to update a new saved reply' do
- let(:mutation_arguments) { { name: '', content: '' } }
-
- it { expect(subject[:saved_reply]).to be_nil }
- it { expect(subject[:errors]).to match_array(["Content can't be blank", "Name can't be blank"]) }
- end
+ it { expect(subject[:saved_reply]).to be_nil }
+ it { expect(subject[:errors]).to match_array(["Content can't be blank", "Name can't be blank"]) }
+ end
- context 'when service successfully updates the saved reply' do
- it { expect(subject[:saved_reply].name).to eq(mutation_arguments[:name]) }
- it { expect(subject[:saved_reply].content).to eq(mutation_arguments[:content]) }
- it { expect(subject[:errors]).to be_empty }
- end
+ context 'when service successfully updates the saved reply' do
+ it { expect(subject[:saved_reply].name).to eq(mutation_arguments[:name]) }
+ it { expect(subject[:saved_reply].content).to eq(mutation_arguments[:content]) }
+ it { expect(subject[:errors]).to be_empty }
end
end
end
diff --git a/spec/graphql/resolvers/ci/catalog/resource_resolver_spec.rb b/spec/graphql/resolvers/ci/catalog/resource_resolver_spec.rb
new file mode 100644
index 00000000000..19fc0c7fc4c
--- /dev/null
+++ b/spec/graphql/resolvers/ci/catalog/resource_resolver_spec.rb
@@ -0,0 +1,123 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Resolvers::Ci::Catalog::ResourceResolver, feature_category: :pipeline_composition do
+ include GraphqlHelpers
+
+ let_it_be(:namespace) { create(:group) }
+ let_it_be(:project) { create(:project, :private, namespace: namespace) }
+ let_it_be(:resource) { create(:ci_catalog_resource, project: project) }
+ let_it_be(:user) { create(:user) }
+
+ describe '#resolve' do
+ context 'when id argument is provided' do
+ context 'when the user is authorised to view the resource' do
+ before_all do
+ namespace.add_developer(user)
+ end
+
+ context 'when resource is found' do
+ it 'returns a single CI/CD Catalog resource' do
+ result = resolve(described_class, ctx: { current_user: user },
+ args: { id: resource.to_global_id.to_s })
+
+ expect(result.id).to eq(resource.id)
+ expect(result.class).to eq(Ci::Catalog::Resource)
+ end
+ end
+
+ context 'when resource is not found' do
+ it 'raises ResourceNotAvailable error' do
+ result = resolve(described_class, ctx: { current_user: user },
+ args: { id: "gid://gitlab/Ci::Catalog::Resource/not-a-real-id" })
+
+ expect(result).to be_a(::Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
+ end
+ end
+
+ context 'when user is not authorised to view the resource' do
+ it 'raises ResourceNotAvailable error' do
+ result = resolve(described_class, ctx: { current_user: user },
+ args: { id: resource.to_global_id.to_s })
+
+ expect(result).to be_a(::Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
+ end
+ end
+
+ context 'when full_path argument is provided' do
+ context 'when the user is authorised to view the resource' do
+ before_all do
+ namespace.add_developer(user)
+ end
+
+ context 'when resource is found' do
+ it 'returns a single CI/CD Catalog resource' do
+ result = resolve(described_class, ctx: { current_user: user },
+ args: { full_path: resource.project.full_path })
+
+ expect(result.id).to eq(resource.id)
+ expect(result.class).to eq(Ci::Catalog::Resource)
+ end
+ end
+
+ context 'when resource is not found' do
+ it 'raises ResourceNotAvailable error' do
+ result = resolve(described_class, ctx: { current_user: user },
+ args: { full_path: "project/non_exisitng_resource" })
+
+ expect(result).to be_a(::Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
+ end
+
+ context 'when project is not a catalog resource' do
+ let_it_be(:project) { create(:project, :private, namespace: namespace) }
+
+ it 'raises ResourceNotAvailable error' do
+ result = resolve(described_class, ctx: { current_user: user }, args: { full_path: project.full_path })
+
+ expect(result).to be_a(::Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
+ end
+ end
+
+ context 'when user is not authorised to view the resource' do
+ it 'raises ResourceNotAvailable error' do
+ result = resolve(described_class, ctx: { current_user: user },
+ args: { full_path: resource.project.full_path })
+
+ expect(result).to be_a(::Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
+ end
+ end
+
+ context 'when neither id nor full_path argument is provided' do
+ before_all do
+ namespace.add_developer(user)
+ end
+ it 'raises ArgumentError' do
+ expect_graphql_error_to_be_created(::Gitlab::Graphql::Errors::ArgumentError,
+ "Exactly one of 'id' or 'full_path' arguments is required.") do
+ resolve(described_class, ctx: { current_user: user },
+ args: {})
+ end
+ end
+ end
+
+ context 'when both full_path and id arguments are provided' do
+ before_all do
+ namespace.add_developer(user)
+ end
+
+ it 'raises ArgumentError' do
+ expect_graphql_error_to_be_created(::Gitlab::Graphql::Errors::ArgumentError,
+ "Exactly one of 'id' or 'full_path' arguments is required.") do
+ resolve(described_class, ctx: { current_user: user },
+ args: { full_path: resource.project.full_path, id: resource.to_global_id.to_s })
+ end
+ end
+ end
+ end
+end
diff --git a/spec/graphql/resolvers/ci/catalog/resources_resolver_spec.rb b/spec/graphql/resolvers/ci/catalog/resources_resolver_spec.rb
new file mode 100644
index 00000000000..97105db686f
--- /dev/null
+++ b/spec/graphql/resolvers/ci/catalog/resources_resolver_spec.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Resolvers::Ci::Catalog::ResourcesResolver, feature_category: :pipeline_composition do
+ include GraphqlHelpers
+
+ let_it_be(:namespace) { create(:group) }
+ let_it_be(:project_1) { create(:project, name: 'Z', namespace: namespace) }
+ let_it_be(:project_2) { create(:project, name: 'A_Test', namespace: namespace) }
+ let_it_be(:project_3) { create(:project, name: 'L', description: 'Test', namespace: namespace) }
+ let_it_be(:resource_1) { create(:ci_catalog_resource, project: project_1) }
+ let_it_be(:resource_2) { create(:ci_catalog_resource, project: project_2) }
+ let_it_be(:resource_3) { create(:ci_catalog_resource, project: project_3) }
+ let_it_be(:user) { create(:user) }
+
+ let(:ctx) { { current_user: user } }
+ let(:search) { nil }
+ let(:sort) { nil }
+
+ let(:args) do
+ {
+ project_path: project_1.full_path,
+ sort: sort,
+ search: search
+ }.compact
+ end
+
+ subject(:result) { resolve(described_class, ctx: ctx, args: args) }
+
+ describe '#resolve' do
+ context 'with an authorized user' do
+ before_all do
+ namespace.add_owner(user)
+ end
+
+ it 'returns all catalog resources visible to the current user in the namespace' do
+ expect(result.items.count).to be(3)
+ expect(result.items.pluck(:name)).to contain_exactly('Z', 'A_Test', 'L')
+ end
+
+ context 'when the sort parameter is not provided' do
+ it 'returns all catalog resources sorted by descending created date' do
+ expect(result.items.pluck(:name)).to eq(%w[L A_Test Z])
+ end
+ end
+
+ context 'when the sort parameter is provided' do
+ let(:sort) { 'NAME_DESC' }
+
+ it 'returns all catalog resources sorted by descending name' do
+ expect(result.items.pluck(:name)).to eq(%w[Z L A_Test])
+ end
+ end
+
+ context 'when the search parameter is provided' do
+ let(:search) { 'test' }
+
+ it 'returns the catalog resources that match the search term' do
+ expect(result.items.pluck(:name)).to contain_exactly('A_Test', 'L')
+ end
+ end
+ end
+
+ context 'when the current user cannot read the namespace catalog' do
+ it 'returns empty response' do
+ expect(result).to be_empty
+ end
+ end
+ end
+end
diff --git a/spec/graphql/resolvers/ci/catalog/versions_resolver_spec.rb b/spec/graphql/resolvers/ci/catalog/versions_resolver_spec.rb
new file mode 100644
index 00000000000..02fb3dfaee4
--- /dev/null
+++ b/spec/graphql/resolvers/ci/catalog/versions_resolver_spec.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+# In this context, a `version` is equivalent to a `release`
+RSpec.describe Resolvers::Ci::Catalog::VersionsResolver, feature_category: :pipeline_composition do
+ include GraphqlHelpers
+
+ let_it_be(:today) { Time.now }
+ let_it_be(:yesterday) { today - 1.day }
+ let_it_be(:tomorrow) { today + 1.day }
+
+ let_it_be(:project) { create(:project, :private) }
+ # rubocop: disable Layout/LineLength
+ let_it_be(:version1) { create(:release, project: project, tag: 'v1.0.0', released_at: yesterday, created_at: tomorrow) }
+ let_it_be(:version2) { create(:release, project: project, tag: 'v2.0.0', released_at: today, created_at: yesterday) }
+ let_it_be(:version3) { create(:release, project: project, tag: 'v3.0.0', released_at: tomorrow, created_at: today) }
+ # rubocop: enable Layout/LineLength
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:public_user) { create(:user) }
+
+ let(:args) { { sort: :released_at_desc } }
+ let(:all_releases) { [version1, version2, version3] }
+
+ before_all do
+ project.add_developer(developer)
+ end
+
+ describe '#resolve' do
+ it_behaves_like 'releases and group releases resolver'
+
+ describe 'when order_by is created_at' do
+ let(:current_user) { developer }
+
+ context 'with sort: desc' do
+ let(:args) { { sort: :created_desc } }
+
+ it 'returns the releases ordered by created_at in descending order' do
+ expect(resolve_releases.to_a)
+ .to match_array(all_releases)
+ .and be_sorted(:created_at, :desc)
+ end
+ end
+
+ context 'with sort: asc' do
+ let(:args) { { sort: :created_asc } }
+
+ it 'returns the releases ordered by created_at in ascending order' do
+ expect(resolve_releases.to_a)
+ .to match_array(all_releases)
+ .and be_sorted(:created_at, :asc)
+ end
+ end
+ end
+ end
+
+ private
+
+ def resolve_versions
+ context = { current_user: current_user }
+ resolve(described_class, obj: project, args: args, ctx: context, arg_style: :internal)
+ end
+
+ # Required for shared examples
+ alias_method :resolve_releases, :resolve_versions
+end
diff --git a/spec/graphql/resolvers/ci/runners_resolver_spec.rb b/spec/graphql/resolvers/ci/runners_resolver_spec.rb
index c164393d605..7d37d13366c 100644
--- a/spec/graphql/resolvers/ci/runners_resolver_spec.rb
+++ b/spec/graphql/resolvers/ci/runners_resolver_spec.rb
@@ -85,7 +85,9 @@ RSpec.describe Resolvers::Ci::RunnersResolver, feature_category: :runner_fleet d
type: :instance_type,
tag_list: ['active_runner'],
search: 'abc',
- sort: :contacted_asc
+ sort: :contacted_asc,
+ creator_id: 'gid://gitlab/User/1',
+ version_prefix: '15.'
}
end
@@ -98,7 +100,9 @@ RSpec.describe Resolvers::Ci::RunnersResolver, feature_category: :runner_fleet d
tag_name: ['active_runner'],
preload: false,
search: 'abc',
- sort: 'contacted_asc'
+ sort: 'contacted_asc',
+ creator_id: '1',
+ version_prefix: '15.'
}
end
@@ -169,6 +173,26 @@ RSpec.describe Resolvers::Ci::RunnersResolver, feature_category: :runner_fleet d
expect(resolve_scope.items.to_a).to contain_exactly :execute_return_value
end
end
+
+ context 'with an invalid version filter parameter' do
+ let(:args) do
+ { version_prefix: 'a.b' }
+ end
+
+ let(:expected_params) do
+ {
+ preload: false,
+ version_prefix: 'a.b'
+ }
+ end
+
+ it 'ignores the parameter and returns runners' do
+ expect(::Ci::RunnersFinder).to receive(:new).with(current_user: user, params: expected_params).once.and_return(finder)
+ allow(finder).to receive(:execute).once.and_return([:execute_return_value])
+
+ expect(resolve_scope.items.to_a).to contain_exactly :execute_return_value
+ end
+ end
end
end
end
diff --git a/spec/graphql/resolvers/container_repository_tags_resolver_spec.rb b/spec/graphql/resolvers/container_repository_tags_resolver_spec.rb
index 0408357e8f2..48be1c29184 100644
--- a/spec/graphql/resolvers/container_repository_tags_resolver_spec.rb
+++ b/spec/graphql/resolvers/container_repository_tags_resolver_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Resolvers::ContainerRepositoryTagsResolver do
+RSpec.describe Resolvers::ContainerRepositoryTagsResolver, feature_category: :container_registry do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
@@ -12,51 +12,135 @@ RSpec.describe Resolvers::ContainerRepositoryTagsResolver do
let(:args) { { sort: nil } }
describe '#resolve' do
- let(:resolver) do
- resolve(
- described_class,
- ctx: { current_user: user },
- obj: repository,
- args: args,
- arg_style: :internal
- )
+ shared_examples 'fetching via tags and filter in place' do
+ context 'by name' do
+ subject { resolver(args).map(&:name) }
+
+ before do
+ stub_container_registry_tags(repository: repository.path, tags: %w[aaa bab bbb ccc 123], with_manifest: false)
+ end
+
+ context 'without sort' do
+ # order is not guaranteed
+ it { is_expected.to contain_exactly('aaa', 'bab', 'bbb', 'ccc', '123') }
+ end
+
+ context 'with sorting and filtering' do
+ context 'name_asc' do
+ let(:args) { { sort: 'NAME_ASC' } }
+
+ it { is_expected.to eq(%w[123 aaa bab bbb ccc]) }
+ end
+
+ context 'name_desc' do
+ let(:args) { { sort: 'NAME_DESC' } }
+
+ it { is_expected.to eq(%w[ccc bbb bab aaa 123]) }
+ end
+
+ context 'filter by name' do
+ let(:args) { { sort: 'NAME_DESC', name: 'b' } }
+
+ it { is_expected.to eq(%w[bbb bab]) }
+ end
+ end
+ end
end
before do
stub_container_registry_config(enabled: true)
end
- context 'by name' do
- subject { resolver.map(&:name) }
-
+ context 'when Gitlab API is supported', :saas do
before do
- stub_container_registry_tags(repository: repository.path, tags: %w[aaa bab bbb ccc 123], with_manifest: false)
+ allow(repository).to receive(:tags_page).and_return({
+ tags: [],
+ pagination: {
+ previous: { uri: URI('/test?before=prev-cursor') },
+ next: { uri: URI('/test?last=next-cursor') }
+ }
+ })
+
+ allow(ContainerRegistry::GitlabApiClient).to receive(:supports_gitlab_api?).and_return(true)
end
- context 'without sort' do
- # order is not guaranteed
- it { is_expected.to contain_exactly('aaa', 'bab', 'bbb', 'ccc', '123') }
+ context 'get the page size based on first and last param' do
+ it 'sends the page size based on first if next page is asked' do
+ args = { sort: 'NAME_ASC', first: 10 }
+ expect(repository).to receive(:tags_page).with(hash_including(page_size: args[:first]))
+
+ resolver(args)
+ end
+
+ it 'sends the page size based on last if prev page is asked' do
+ args = { sort: 'NAME_ASC', last: 10 }
+ expect(repository).to receive(:tags_page).with(hash_including(page_size: args[:last]))
+
+ resolver(args)
+ end
end
- context 'with sorting and filtering' do
- context "name_asc" do
- let(:args) { { sort: :name_asc } }
+ context 'with parameters' do
+ using RSpec::Parameterized::TableSyntax
- it { is_expected.to eq(%w[123 aaa bab bbb ccc]) }
+ where(:before, :after, :sort, :name, :first, :last, :sort_value) do
+ nil | nil | 'NAME_DESC' | '' | 10 | nil | '-name'
+ 'bb' | nil | 'NAME_ASC' | 'a' | nil | 5 | 'name'
+ nil | 'aa' | 'NAME_DESC' | 'a' | 10 | nil | '-name'
end
- context "name_desc" do
- let(:args) { { sort: :name_desc } }
+ with_them do
+ let(:args) do
+ { before: before, after: after, sort: sort, name: name, first: first, last: last }.compact
+ end
+
+ it 'calls ContainerRepository#tags_page with correct parameters' do
+ expect(repository).to receive(:tags_page).with(
+ before: before,
+ last: after,
+ sort: sort_value,
+ name: name,
+ page_size: [first, last].map(&:to_i).max
+ )
- it { is_expected.to eq(%w[ccc bbb bab aaa 123]) }
+ resolver(args)
+ end
end
+ end
+
+ it 'returns an ExternallyPaginatedArray' do
+ expect(Gitlab::Graphql::ExternallyPaginatedArray)
+ .to receive(:new).with('prev-cursor', 'next-cursor')
- context 'filter by name' do
- let(:args) { { sort: :name_desc, name: 'b' } }
+ expect(resolver(args)).is_a? Gitlab::Graphql::ExternallyPaginatedArray
+ end
- it { is_expected.to eq(%w[bbb bab]) }
+ context 'when feature use_repository_list_tags_on_graphql is disabled' do
+ before do
+ stub_feature_flags(use_repository_list_tags_on_graphql: false)
end
+
+ it_behaves_like 'fetching via tags and filter in place'
end
end
+
+ context 'when Gitlab API is not supported' do
+ before do
+ allow(ContainerRegistry::GitlabApiClient).to receive(:supports_gitlab_api?).and_return(false)
+ end
+
+ it_behaves_like 'fetching via tags and filter in place'
+ end
+
+ def resolver(args, opts = {})
+ field_options = {
+ owner: resolver_parent,
+ resolver: described_class,
+ connection_extension: Gitlab::Graphql::Extensions::ExternallyPaginatedArrayExtension
+ }.merge(opts)
+
+ field = ::Types::BaseField.from_options('field_value', **field_options)
+ resolve_field(field, repository, args: args, object_type: resolver_parent)
+ end
end
end
diff --git a/spec/graphql/resolvers/data_transfer/group_data_transfer_resolver_spec.rb b/spec/graphql/resolvers/data_transfer/group_data_transfer_resolver_spec.rb
index 4ea3d287454..b5bffbc8803 100644
--- a/spec/graphql/resolvers/data_transfer/group_data_transfer_resolver_spec.rb
+++ b/spec/graphql/resolvers/data_transfer/group_data_transfer_resolver_spec.rb
@@ -10,6 +10,7 @@ RSpec.describe Resolvers::DataTransfer::GroupDataTransferResolver, feature_categ
let(:from) { Date.new(2022, 1, 1) }
let(:to) { Date.new(2023, 1, 1) }
+ let(:finder) { instance_double(::DataTransfer::GroupDataTransferFinder) }
let(:finder_results) do
[
build(:project_data_transfer, date: to, repository_egress: 250000)
@@ -41,21 +42,12 @@ RSpec.describe Resolvers::DataTransfer::GroupDataTransferResolver, feature_categ
include_examples 'Data transfer resolver'
- context 'when data_transfer_monitoring_mock_data is disabled' do
- let(:finder) { instance_double(::DataTransfer::GroupDataTransferFinder) }
+ it 'calls GroupDataTransferFinder with expected arguments' do
+ expect(::DataTransfer::GroupDataTransferFinder).to receive(:new).with(
+ group: group, from: from, to: to, user: current_user).once.and_return(finder)
+ allow(finder).to receive(:execute).once.and_return(finder_results)
- before do
- stub_feature_flags(data_transfer_monitoring_mock_data: false)
- end
-
- it 'calls GroupDataTransferFinder with expected arguments' do
- expect(::DataTransfer::GroupDataTransferFinder).to receive(:new).with(
- group: group, from: from, to: to, user: current_user
- ).once.and_return(finder)
- allow(finder).to receive(:execute).once.and_return(finder_results)
-
- expect(resolve_egress).to eq({ egress_nodes: finder_results.map(&:attributes) })
- end
+ expect(resolve_egress).to eq({ egress_nodes: finder_results.map(&:attributes) })
end
end
diff --git a/spec/graphql/resolvers/data_transfer/project_data_transfer_resolver_spec.rb b/spec/graphql/resolvers/data_transfer/project_data_transfer_resolver_spec.rb
index 7307c1a54a9..25ff02218cf 100644
--- a/spec/graphql/resolvers/data_transfer/project_data_transfer_resolver_spec.rb
+++ b/spec/graphql/resolvers/data_transfer/project_data_transfer_resolver_spec.rb
@@ -10,6 +10,9 @@ RSpec.describe Resolvers::DataTransfer::ProjectDataTransferResolver, feature_cat
let(:from) { Date.new(2022, 1, 1) }
let(:to) { Date.new(2023, 1, 1) }
+
+ let(:finder) { instance_double(::DataTransfer::ProjectDataTransferFinder) }
+
let(:finder_results) do
[
{
@@ -44,21 +47,12 @@ RSpec.describe Resolvers::DataTransfer::ProjectDataTransferResolver, feature_cat
include_examples 'Data transfer resolver'
- context 'when data_transfer_monitoring_mock_data is disabled' do
- let(:finder) { instance_double(::DataTransfer::ProjectDataTransferFinder) }
-
- before do
- stub_feature_flags(data_transfer_monitoring_mock_data: false)
- end
-
- it 'calls ProjectDataTransferFinder with expected arguments' do
- expect(::DataTransfer::ProjectDataTransferFinder).to receive(:new).with(
- project: project, from: from, to: to, user: current_user
- ).once.and_return(finder)
- allow(finder).to receive(:execute).once.and_return(finder_results)
+ it 'calls ProjectDataTransferFinder with expected arguments' do
+ expect(::DataTransfer::ProjectDataTransferFinder).to receive(:new).with(
+ project: project, from: from, to: to, user: current_user).once.and_return(finder)
+ allow(finder).to receive(:execute).once.and_return(finder_results)
- expect(resolve_egress).to eq({ egress_nodes: finder_results })
- end
+ expect(resolve_egress).to eq({ egress_nodes: finder_results })
end
end
diff --git a/spec/graphql/resolvers/projects_resolver_spec.rb b/spec/graphql/resolvers/projects_resolver_spec.rb
index 058d46a5e86..d9c1527af86 100644
--- a/spec/graphql/resolvers/projects_resolver_spec.rb
+++ b/spec/graphql/resolvers/projects_resolver_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Resolvers::ProjectsResolver do
+RSpec.describe Resolvers::ProjectsResolver, feature_category: :source_code_management do
include GraphqlHelpers
describe '#resolve' do
diff --git a/spec/graphql/resolvers/saved_reply_resolver_spec.rb b/spec/graphql/resolvers/saved_reply_resolver_spec.rb
index f1cb0ca5214..d6457e8c21a 100644
--- a/spec/graphql/resolvers/saved_reply_resolver_spec.rb
+++ b/spec/graphql/resolvers/saved_reply_resolver_spec.rb
@@ -8,30 +8,18 @@ RSpec.describe Resolvers::SavedReplyResolver, feature_category: :user_profile do
let_it_be(:current_user) { create(:user) }
let_it_be(:saved_reply) { create(:saved_reply, user: current_user) }
- describe 'feature flag disabled' do
- before do
- stub_feature_flags(saved_replies: false)
- end
-
- it 'does not return saved reply' do
- expect(resolve_saved_reply).to be_nil
- end
+ it 'returns users saved reply' do
+ expect(resolve_saved_reply).to eq(saved_reply)
end
- describe 'feature flag enabled' do
- it 'returns users saved reply' do
- expect(resolve_saved_reply).to eq(saved_reply)
- end
-
- it 'returns nil when saved reply is not found' do
- expect(resolve_saved_reply({ id: 'gid://gitlab/Users::SavedReply/100' })).to be_nil
- end
+ it 'returns nil when saved reply is not found' do
+ expect(resolve_saved_reply({ id: 'gid://gitlab/Users::SavedReply/100' })).to be_nil
+ end
- it 'returns nil when saved reply is another users' do
- other_users_saved_reply = create(:saved_reply, user: create(:user))
+ it 'returns nil when saved reply is another users' do
+ other_users_saved_reply = create(:saved_reply, user: create(:user))
- expect(resolve_saved_reply({ id: other_users_saved_reply.to_global_id })).to be_nil
- end
+ expect(resolve_saved_reply({ id: other_users_saved_reply.to_global_id })).to be_nil
end
def resolve_saved_reply(args = { id: saved_reply.to_global_id })
diff --git a/spec/graphql/resolvers/users/frecent_groups_resolver_spec.rb b/spec/graphql/resolvers/users/frecent_groups_resolver_spec.rb
new file mode 100644
index 00000000000..8836ba73110
--- /dev/null
+++ b/spec/graphql/resolvers/users/frecent_groups_resolver_spec.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Resolvers::Users::FrecentGroupsResolver, feature_category: :navigation do
+ it_behaves_like 'namespace visits resolver'
+end
diff --git a/spec/graphql/resolvers/users/frecent_projects_resolver_spec.rb b/spec/graphql/resolvers/users/frecent_projects_resolver_spec.rb
new file mode 100644
index 00000000000..30a0a7d93b3
--- /dev/null
+++ b/spec/graphql/resolvers/users/frecent_projects_resolver_spec.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Resolvers::Users::FrecentProjectsResolver, feature_category: :navigation do
+ it_behaves_like 'namespace visits resolver'
+end
diff --git a/spec/graphql/types/analytics/cycle_analytics/value_stream_type_spec.rb b/spec/graphql/types/analytics/cycle_analytics/value_stream_type_spec.rb
new file mode 100644
index 00000000000..5e2638210d3
--- /dev/null
+++ b/spec/graphql/types/analytics/cycle_analytics/value_stream_type_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::Analytics::CycleAnalytics::ValueStreamType, feature_category: :value_stream_management do
+ specify { expect(described_class.graphql_name).to eq('ValueStream') }
+
+ specify { expect(described_class).to require_graphql_authorizations(:read_cycle_analytics) }
+
+ specify { expect(described_class).to have_graphql_fields(:id, :name, :namespace, :project) }
+end
diff --git a/spec/graphql/types/base_argument_spec.rb b/spec/graphql/types/base_argument_spec.rb
index 0ce6aa3667d..99154c8c9a5 100644
--- a/spec/graphql/types/base_argument_spec.rb
+++ b/spec/graphql/types/base_argument_spec.rb
@@ -3,41 +3,14 @@
require 'spec_helper'
RSpec.describe Types::BaseArgument, feature_category: :api do
- let_it_be(:field) do
- Types::BaseField.new(name: 'field', type: String, null: true)
- end
-
- let(:base_args) { { name: 'test', type: String, required: false, owner: field } }
-
- def subject(args = {})
- described_class.new(**base_args.merge(args))
- end
-
- include_examples 'Gitlab-style deprecations'
-
- describe 'required argument declarations' do
- it 'accepts nullable, required arguments' do
- arguments = base_args.merge({ required: :nullable })
-
- expect { subject(arguments) }.not_to raise_error
+ include_examples 'Gitlab-style deprecations' do
+ let_it_be(:field) do
+ Types::BaseField.new(name: 'field', type: String, null: true)
end
- it 'accepts required, non-nullable arguments' do
- arguments = base_args.merge({ required: true })
-
- expect { subject(arguments) }.not_to raise_error
- end
-
- it 'accepts non-required arguments' do
- arguments = base_args.merge({ required: false })
-
- expect { subject(arguments) }.not_to raise_error
- end
-
- it 'accepts no required argument declaration' do
- arguments = base_args
-
- expect { subject(arguments) }.not_to raise_error
+ def subject(args = {})
+ base_args = { name: 'test', type: String, required: false, owner: field }
+ described_class.new(**base_args.merge(args))
end
end
end
diff --git a/spec/graphql/types/ci/catalog/resource_sort_enum_spec.rb b/spec/graphql/types/ci/catalog/resource_sort_enum_spec.rb
new file mode 100644
index 00000000000..1de324b6652
--- /dev/null
+++ b/spec/graphql/types/ci/catalog/resource_sort_enum_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['CiCatalogResourceSort'], feature_category: :pipeline_composition do
+ it { expect(described_class.graphql_name).to eq('CiCatalogResourceSort') }
+
+ it 'exposes all the existing catalog resource sort orders' do
+ expect(described_class.values.keys).to include(
+ *%w[NAME_ASC NAME_DESC LATEST_RELEASED_AT_ASC LATEST_RELEASED_AT_DESC CREATED_ASC CREATED_DESC]
+ )
+ end
+end
diff --git a/spec/graphql/types/ci/catalog/resource_type_spec.rb b/spec/graphql/types/ci/catalog/resource_type_spec.rb
new file mode 100644
index 00000000000..5f5732c5237
--- /dev/null
+++ b/spec/graphql/types/ci/catalog/resource_type_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::Ci::Catalog::ResourceType, feature_category: :pipeline_composition do
+ specify { expect(described_class.graphql_name).to eq('CiCatalogResource') }
+
+ it 'exposes the expected fields' do
+ expected_fields = %i[
+ id
+ name
+ description
+ icon
+ web_path
+ versions
+ latest_version
+ latest_released_at
+ star_count
+ forks_count
+ root_namespace
+ readme_html
+ open_issues_count
+ open_merge_requests_count
+ ]
+
+ expect(described_class).to have_graphql_fields(*expected_fields)
+ end
+end
diff --git a/spec/graphql/types/container_registry/protection/rule_access_level_enum_spec.rb b/spec/graphql/types/container_registry/protection/rule_access_level_enum_spec.rb
new file mode 100644
index 00000000000..295401f89f9
--- /dev/null
+++ b/spec/graphql/types/container_registry/protection/rule_access_level_enum_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['ContainerRegistryProtectionRuleAccessLevel'], feature_category: :container_registry do
+ it 'exposes all options' do
+ expect(described_class.values.keys).to match_array(%w[DEVELOPER MAINTAINER OWNER])
+ end
+end
diff --git a/spec/graphql/types/container_registry/protection/rule_type_spec.rb b/spec/graphql/types/container_registry/protection/rule_type_spec.rb
new file mode 100644
index 00000000000..58b53af80fb
--- /dev/null
+++ b/spec/graphql/types/container_registry/protection/rule_type_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['ContainerRegistryProtectionRule'], feature_category: :container_registry do
+ specify { expect(described_class.graphql_name).to eq('ContainerRegistryProtectionRule') }
+
+ specify { expect(described_class.description).to be_present }
+
+ specify { expect(described_class).to require_graphql_authorizations(:admin_container_image) }
+
+ describe 'id' do
+ subject { described_class.fields['id'] }
+
+ it { is_expected.to have_non_null_graphql_type(::Types::GlobalIDType[::ContainerRegistry::Protection::Rule]) }
+ end
+
+ describe 'container_path_pattern' do
+ subject { described_class.fields['containerPathPattern'] }
+
+ it { is_expected.to have_non_null_graphql_type(GraphQL::Types::String) }
+ end
+
+ describe 'push_protected_up_to_access_level' do
+ subject { described_class.fields['pushProtectedUpToAccessLevel'] }
+
+ it { is_expected.to have_non_null_graphql_type(Types::ContainerRegistry::Protection::RuleAccessLevelEnum) }
+ end
+
+ describe 'delete_protected_up_to_access_level' do
+ subject { described_class.fields['deleteProtectedUpToAccessLevel'] }
+
+ it { is_expected.to have_non_null_graphql_type(Types::ContainerRegistry::Protection::RuleAccessLevelEnum) }
+ end
+end
diff --git a/spec/graphql/types/data_transfer/project_data_transfer_type_spec.rb b/spec/graphql/types/data_transfer/project_data_transfer_type_spec.rb
index a93da279b7f..80ead81650e 100644
--- a/spec/graphql/types/data_transfer/project_data_transfer_type_spec.rb
+++ b/spec/graphql/types/data_transfer/project_data_transfer_type_spec.rb
@@ -14,25 +14,15 @@ RSpec.describe GitlabSchema.types['ProjectDataTransfer'], feature_category: :sou
let_it_be(:project) { create(:project) }
let(:from) { Date.new(2022, 1, 1) }
let(:to) { Date.new(2023, 1, 1) }
- let(:finder_result) { 40_000_000 }
+ let(:relation) { instance_double(ActiveRecord::Relation) }
- it 'returns mock data' do
- expect(resolve_field(:total_egress, { from: from, to: to }, extras: { parent: project },
- arg_style: :internal)).to eq(finder_result)
+ before do
+ allow(relation).to receive(:sum).and_return(10)
end
- context 'when data_transfer_monitoring_mock_data is disabled' do
- let(:relation) { instance_double(ActiveRecord::Relation) }
-
- before do
- allow(relation).to receive(:sum).and_return(10)
- stub_feature_flags(data_transfer_monitoring_mock_data: false)
- end
-
- it 'calls sum on active record relation' do
- expect(resolve_field(:total_egress, { egress_nodes: relation }, extras: { parent: project },
- arg_style: :internal)).to eq(10)
- end
+ it 'calls sum on active record relation' do
+ expect(resolve_field(:total_egress, { egress_nodes: relation }, extras: { parent: project },
+ arg_style: :internal)).to eq(10)
end
end
end
diff --git a/spec/graphql/types/merge_request_review_state_enum_spec.rb b/spec/graphql/types/merge_request_review_state_enum_spec.rb
index 486e1c4f502..d8de3fcd1d1 100644
--- a/spec/graphql/types/merge_request_review_state_enum_spec.rb
+++ b/spec/graphql/types/merge_request_review_state_enum_spec.rb
@@ -6,12 +6,16 @@ RSpec.describe GitlabSchema.types['MergeRequestReviewState'] do
it 'the correct enum members' do
expect(described_class.values).to match(
'REVIEWED' => have_attributes(
- description: 'The merge request is reviewed.',
+ description: 'Merge request reviewer has reviewed.',
value: 'reviewed'
),
'UNREVIEWED' => have_attributes(
- description: 'The merge request is unreviewed.',
+ description: 'Awaiting review from merge request reviewer.',
value: 'unreviewed'
+ ),
+ 'REQUESTED_CHANGES' => have_attributes(
+ description: 'Merge request reviewer has requested changes.',
+ value: 'requested_changes'
)
)
end
diff --git a/spec/graphql/types/notes/noteable_interface_spec.rb b/spec/graphql/types/notes/noteable_interface_spec.rb
index e11dece60b8..c88cfe18b81 100644
--- a/spec/graphql/types/notes/noteable_interface_spec.rb
+++ b/spec/graphql/types/notes/noteable_interface_spec.rb
@@ -19,6 +19,7 @@ RSpec.describe Types::Notes::NoteableInterface do
expect(described_class.resolve_type(build(:merge_request), {})).to eq(Types::MergeRequestType)
expect(described_class.resolve_type(build(:design), {})).to eq(Types::DesignManagement::DesignType)
expect(described_class.resolve_type(build(:alert_management_alert), {})).to eq(Types::AlertManagement::AlertType)
+ expect(described_class.resolve_type(build(:abuse_report), {})).to eq(Types::AbuseReportType)
end
end
end
diff --git a/spec/graphql/types/organizations/organization_type_spec.rb b/spec/graphql/types/organizations/organization_type_spec.rb
index 26d7c10a715..62787ad220d 100644
--- a/spec/graphql/types/organizations/organization_type_spec.rb
+++ b/spec/graphql/types/organizations/organization_type_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe GitlabSchema.types['Organization'], feature_category: :cell do
- let(:expected_fields) { %w[groups id name organization_users path] }
+ let(:expected_fields) { %w[groups id name organization_users path web_url] }
specify { expect(described_class.graphql_name).to eq('Organization') }
specify { expect(described_class).to require_graphql_authorizations(:read_organization) }
diff --git a/spec/graphql/types/organizations/organization_user_badge_type_spec.rb b/spec/graphql/types/organizations/organization_user_badge_type_spec.rb
new file mode 100644
index 00000000000..1ea9b3ad1df
--- /dev/null
+++ b/spec/graphql/types/organizations/organization_user_badge_type_spec.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['OrganizationUserBadge'], feature_category: :cell do
+ let(:expected_fields) { %w[text variant] }
+
+ specify { expect(described_class.graphql_name).to eq('OrganizationUserBadge') }
+ specify { expect(described_class).to have_graphql_fields(*expected_fields) }
+end
diff --git a/spec/graphql/types/packages/package_base_type_spec.rb b/spec/graphql/types/packages/package_base_type_spec.rb
index ebe29da0539..6b568f4ae7f 100644
--- a/spec/graphql/types/packages/package_base_type_spec.rb
+++ b/spec/graphql/types/packages/package_base_type_spec.rb
@@ -4,8 +4,8 @@ require 'spec_helper'
RSpec.describe GitlabSchema.types['PackageBase'], feature_category: :package_registry do
specify { expect(described_class.description).to eq('Represents a package in the Package Registry') }
-
specify { expect(described_class).to require_graphql_authorizations(:read_package) }
+ specify { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Package) }
it 'includes all expected fields' do
expected_fields = %w[
@@ -14,6 +14,7 @@ RSpec.describe GitlabSchema.types['PackageBase'], feature_category: :package_reg
project
tags metadata
status status_message can_destroy
+ user_permissions
]
expect(described_class).to include_graphql_fields(*expected_fields)
diff --git a/spec/graphql/types/packages/package_details_type_spec.rb b/spec/graphql/types/packages/package_details_type_spec.rb
index e4fe53f7660..464e81c0a8c 100644
--- a/spec/graphql/types/packages/package_details_type_spec.rb
+++ b/spec/graphql/types/packages/package_details_type_spec.rb
@@ -2,17 +2,17 @@
require 'spec_helper'
-RSpec.describe GitlabSchema.types['PackageDetailsType'] do
+RSpec.describe GitlabSchema.types['PackageDetailsType'], feature_category: :package_registry do
specify { expect(described_class.description).to eq('Represents a package details in the Package Registry') }
-
specify { expect(described_class).to require_graphql_authorizations(:read_package) }
+ specify { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Package) }
it 'includes all the package fields' do
expected_fields = %w[
id name version created_at updated_at package_type tags project
pipelines versions package_files dependency_links public_package
npm_url maven_url conan_url nuget_url pypi_url pypi_setup_url
- composer_url composer_config_repository_url
+ composer_url composer_config_repository_url user_permissions
]
expect(described_class).to include_graphql_fields(*expected_fields)
diff --git a/spec/graphql/types/packages/package_type_spec.rb b/spec/graphql/types/packages/package_type_spec.rb
index df8135ed87e..dc1cc6a8bad 100644
--- a/spec/graphql/types/packages/package_type_spec.rb
+++ b/spec/graphql/types/packages/package_type_spec.rb
@@ -2,10 +2,10 @@
require 'spec_helper'
-RSpec.describe GitlabSchema.types['Package'] do
+RSpec.describe GitlabSchema.types['Package'], feature_category: :package_registry do
specify { expect(described_class.description).to eq('Represents a package with pipelines in the Package Registry') }
-
specify { expect(described_class).to require_graphql_authorizations(:read_package) }
+ specify { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Package) }
it 'includes all the package fields and pipelines' do
expected_fields = %w[
@@ -14,6 +14,7 @@ RSpec.describe GitlabSchema.types['Package'] do
project
tags pipelines metadata
status can_destroy
+ user_permissions
]
expect(described_class).to include_graphql_fields(*expected_fields)
diff --git a/spec/graphql/types/packages/protection/rule_type_spec.rb b/spec/graphql/types/packages/protection/rule_type_spec.rb
index a4a458d3568..bc5a052796d 100644
--- a/spec/graphql/types/packages/protection/rule_type_spec.rb
+++ b/spec/graphql/types/packages/protection/rule_type_spec.rb
@@ -9,6 +9,12 @@ RSpec.describe GitlabSchema.types['PackagesProtectionRule'], feature_category: :
specify { expect(described_class).to require_graphql_authorizations(:admin_package) }
+ describe 'id' do
+ subject { described_class.fields['id'] }
+
+ it { is_expected.to have_non_null_graphql_type(::Types::GlobalIDType[::Packages::Protection::Rule]) }
+ end
+
describe 'package_name_pattern' do
subject { described_class.fields['packageNamePattern'] }
diff --git a/spec/graphql/types/packages/pypi/metadatum_type_spec.rb b/spec/graphql/types/packages/pypi/metadatum_type_spec.rb
index 16fb3ef2098..831307490a9 100644
--- a/spec/graphql/types/packages/pypi/metadatum_type_spec.rb
+++ b/spec/graphql/types/packages/pypi/metadatum_type_spec.rb
@@ -5,7 +5,14 @@ require 'spec_helper'
RSpec.describe GitlabSchema.types['PypiMetadata'] do
it 'includes pypi metadatum fields' do
expected_fields = %w[
- id required_python
+ author_email
+ description
+ description_content_type
+ id
+ keywords
+ metadata_version
+ required_python
+ summary
]
expect(described_class).to include_graphql_fields(*expected_fields)
diff --git a/spec/graphql/types/permission_types/abuse_report_spec.rb b/spec/graphql/types/permission_types/abuse_report_spec.rb
new file mode 100644
index 00000000000..399df137a78
--- /dev/null
+++ b/spec/graphql/types/permission_types/abuse_report_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::PermissionTypes::AbuseReport, feature_category: :insider_threat do
+ it do
+ expected_permissions = [
+ :read_abuse_report, :create_note
+ ]
+
+ expected_permissions.each do |permission|
+ expect(described_class).to have_graphql_field(permission)
+ end
+ end
+end
diff --git a/spec/graphql/types/permission_types/ci/job_spec.rb b/spec/graphql/types/permission_types/ci/job_spec.rb
index e4bc5419070..238f086c7ee 100644
--- a/spec/graphql/types/permission_types/ci/job_spec.rb
+++ b/spec/graphql/types/permission_types/ci/job_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Types::PermissionTypes::Ci::Job do
it 'has expected permission fields' do
expected_permissions = [
- :read_job_artifacts, :read_build, :update_build
+ :read_job_artifacts, :read_build, :update_build, :cancel_build
]
expect(described_class).to have_graphql_fields(expected_permissions).only
diff --git a/spec/graphql/types/permission_types/ci/pipeline_spec.rb b/spec/graphql/types/permission_types/ci/pipeline_spec.rb
new file mode 100644
index 00000000000..6830b659b12
--- /dev/null
+++ b/spec/graphql/types/permission_types/ci/pipeline_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::PermissionTypes::Ci::Pipeline, feature_category: :continuous_integration do
+ it 'has expected permission fields' do
+ expected_permissions = [
+ :admin_pipeline, :destroy_pipeline, :update_pipeline, :cancel_pipeline
+ ]
+
+ expect(described_class).to have_graphql_fields(expected_permissions).only
+ end
+end
diff --git a/spec/graphql/types/permission_types/package_spec.rb b/spec/graphql/types/permission_types/package_spec.rb
new file mode 100644
index 00000000000..3de37438234
--- /dev/null
+++ b/spec/graphql/types/permission_types/package_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['PackagePermissions'], feature_category: :package_registry do
+ it 'has the expected fields' do
+ expected_permissions = [:destroy_package]
+
+ expect(described_class).to have_graphql_fields(expected_permissions).only
+ end
+end
diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb
index e295014a2a6..7b4bcf4b1b0 100644
--- a/spec/graphql/types/project_type_spec.rb
+++ b/spec/graphql/types/project_type_spec.rb
@@ -39,7 +39,7 @@ RSpec.describe GitlabSchema.types['Project'] do
recent_issue_boards ci_config_path_or_default packages_cleanup_policy ci_variables
timelog_categories fork_targets branch_rules ci_config_variables pipeline_schedules languages
incident_management_timeline_event_tags visible_forks inherited_ci_variables autocomplete_users
- ci_cd_settings
+ ci_cd_settings detailed_import_status
]
expect(described_class).to include_graphql_fields(*expected_fields)
@@ -932,4 +932,93 @@ RSpec.describe GitlabSchema.types['Project'] do
end
end
end
+
+ describe 'detailed_import_status' do
+ let_it_be_with_reload(:project) { create(:project, :with_import_url) }
+
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ detailedImportStatus {
+ status
+ url
+ lastError
+ }
+ }
+ }
+ )
+ end
+
+ subject { GitlabSchema.execute(query, context: { current_user: current_user }).as_json }
+
+ let(:detailed_import_status) do
+ subject.dig('data', 'project', 'detailedImportStatus')
+ end
+
+ context 'when project is not imported' do
+ let(:current_user) { create(:user) }
+
+ before do
+ project.add_developer(current_user)
+ project.import_state.destroy!
+ end
+
+ it 'returns nil' do
+ expect(detailed_import_status).to be_nil
+ end
+ end
+
+ context 'when current_user is not set' do
+ let(:current_user) { nil }
+
+ it 'returns nil' do
+ expect(detailed_import_status).to be_nil
+ end
+ end
+
+ context 'when current_user has no permission' do
+ let(:current_user) { create(:user) }
+
+ it 'returns nil' do
+ expect(detailed_import_status).to be_nil
+ end
+ end
+
+ context 'when current_user has limited permission' do
+ let(:current_user) { create(:user) }
+
+ before do
+ project.add_developer(current_user)
+ project.import_state.last_error = 'Some error'
+ project.import_state.save!
+ end
+
+ it 'returns detailed information' do
+ expect(detailed_import_status).to include(
+ 'status' => project.import_state.status,
+ 'url' => project.safe_import_url,
+ 'lastError' => nil
+ )
+ end
+ end
+
+ context 'when current_user has permission' do
+ let(:current_user) { create(:user) }
+
+ before do
+ project.add_maintainer(current_user)
+ project.import_state.last_error = 'Some error'
+ project.import_state.save!
+ end
+
+ it 'returns detailed information' do
+ expect(detailed_import_status).to include(
+ 'status' => project.import_state.status,
+ 'url' => project.safe_import_url,
+ 'lastError' => 'Some error'
+ )
+ end
+ end
+ end
end
diff --git a/spec/graphql/types/projects/detailed_import_status_type_spec.rb b/spec/graphql/types/projects/detailed_import_status_type_spec.rb
new file mode 100644
index 00000000000..cfdf06e4ab5
--- /dev/null
+++ b/spec/graphql/types/projects/detailed_import_status_type_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['DetailedImportStatus'], feature_category: :importers do
+ include GraphqlHelpers
+
+ let(:fields) do
+ %w[
+ id
+ status
+ url
+ last_error
+ last_update_at
+ last_update_started_at
+ last_successful_update_at
+ ]
+ end
+
+ it { expect(described_class.graphql_name).to eq('DetailedImportStatus') }
+ it { expect(described_class).to have_graphql_fields(fields) }
+ it { expect(described_class).to require_graphql_authorizations(:read_project) }
+end
diff --git a/spec/graphql/types/security/codequality_reports_comparer/report_generation_status_enum_spec.rb b/spec/graphql/types/security/codequality_reports_comparer/report_generation_status_enum_spec.rb
new file mode 100644
index 00000000000..16a115c37e0
--- /dev/null
+++ b/spec/graphql/types/security/codequality_reports_comparer/report_generation_status_enum_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['CodequalityReportsComparerReportGenerationStatus'], feature_category: :code_quality do
+ specify { expect(described_class.graphql_name).to eq('CodequalityReportsComparerReportGenerationStatus') }
+
+ it 'exposes all codequality report status values' do
+ expect(described_class.values.keys).to contain_exactly('PARSED', 'PARSING', 'ERROR')
+ end
+end
diff --git a/spec/graphql/types/security/codequality_reports_comparer/status_enum_spec.rb b/spec/graphql/types/security/codequality_reports_comparer/status_enum_spec.rb
index 6e5bdd1e91d..626873deffe 100644
--- a/spec/graphql/types/security/codequality_reports_comparer/status_enum_spec.rb
+++ b/spec/graphql/types/security/codequality_reports_comparer/status_enum_spec.rb
@@ -2,8 +2,8 @@
require 'spec_helper'
-RSpec.describe GitlabSchema.types['CodequalityReportsComparerReportStatus'], feature_category: :code_quality do
- specify { expect(described_class.graphql_name).to eq('CodequalityReportsComparerReportStatus') }
+RSpec.describe GitlabSchema.types['CodequalityReportsComparerStatus'], feature_category: :code_quality do
+ specify { expect(described_class.graphql_name).to eq('CodequalityReportsComparerStatus') }
it 'exposes all codequality report status values' do
expect(described_class.values.keys).to contain_exactly('SUCCESS', 'FAILED', 'NOT_FOUND')
diff --git a/spec/graphql/types/security/codequality_reports_comparer_type_spec.rb b/spec/graphql/types/security/codequality_reports_comparer_type_spec.rb
index 02f7a9d6925..fad43845b58 100644
--- a/spec/graphql/types/security/codequality_reports_comparer_type_spec.rb
+++ b/spec/graphql/types/security/codequality_reports_comparer_type_spec.rb
@@ -6,6 +6,6 @@ RSpec.describe GitlabSchema.types['CodequalityReportsComparer'], feature_categor
specify { expect(described_class.graphql_name).to eq('CodequalityReportsComparer') }
it 'has expected fields' do
- expect(described_class).to have_graphql_fields(:report)
+ expect(described_class).to have_graphql_fields(:status, :report)
end
end
diff --git a/spec/graphql/types/user_type_spec.rb b/spec/graphql/types/user_type_spec.rb
index af0f8a86b6c..457127f5bed 100644
--- a/spec/graphql/types/user_type_spec.rb
+++ b/spec/graphql/types/user_type_spec.rb
@@ -55,6 +55,7 @@ RSpec.describe GitlabSchema.types['User'], feature_category: :user_profile do
organization
jobTitle
createdAt
+ lastActivityOn
pronouns
ide
]
diff --git a/spec/graphql/types/work_items/linked_item_type_spec.rb b/spec/graphql/types/work_items/linked_item_type_spec.rb
index 7d7fda45ce4..8dc8a742790 100644
--- a/spec/graphql/types/work_items/linked_item_type_spec.rb
+++ b/spec/graphql/types/work_items/linked_item_type_spec.rb
@@ -10,4 +10,10 @@ RSpec.describe Types::WorkItems::LinkedItemType, feature_category: :portfolio_ma
expect(described_class).to have_graphql_fields(*expected_fields)
end
+
+ describe 'work_item' do
+ subject { described_class.fields['workItem'] }
+
+ it { is_expected.to have_nullable_graphql_type(Types::WorkItemType) }
+ end
end
diff --git a/spec/helpers/admin/components_helper_spec.rb b/spec/helpers/admin/components_helper_spec.rb
index bb590d003ad..b2107f99a34 100644
--- a/spec/helpers/admin/components_helper_spec.rb
+++ b/spec/helpers/admin/components_helper_spec.rb
@@ -11,6 +11,7 @@ RSpec.describe Admin::ComponentsHelper, feature_category: :database do
}
main[:ci] = { adapter_name: 'PostgreSQL', version: expected_version } if Gitlab::Database.has_config?(:ci)
main[:geo] = { adapter_name: 'PostgreSQL', version: expected_version } if Gitlab::Database.has_config?(:geo)
+ main[:jh] = { adapter_name: 'PostgreSQL', version: expected_version } if Gitlab::Database.has_config?(:jh)
main
end
diff --git a/spec/helpers/admin/user_actions_helper_spec.rb b/spec/helpers/admin/user_actions_helper_spec.rb
index 87d2308690c..abfdabf3413 100644
--- a/spec/helpers/admin/user_actions_helper_spec.rb
+++ b/spec/helpers/admin/user_actions_helper_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe Admin::UserActionsHelper do
+RSpec.describe Admin::UserActionsHelper, feature_category: :user_management do
describe '#admin_actions' do
let_it_be(:current_user) { build(:user) }
@@ -29,13 +29,33 @@ RSpec.describe Admin::UserActionsHelper do
context 'the user is a standard user' do
let_it_be(:user) { create(:user) }
- it { is_expected.to contain_exactly("edit", "block", "ban", "deactivate", "delete", "delete_with_contributions") }
+ it do
+ is_expected.to contain_exactly(
+ "edit",
+ "block",
+ "ban",
+ "deactivate",
+ "delete",
+ "delete_with_contributions",
+ "trust"
+ )
+ end
end
context 'the user is an admin user' do
let_it_be(:user) { create(:user, :admin) }
- it { is_expected.to contain_exactly("edit", "block", "ban", "deactivate", "delete", "delete_with_contributions") }
+ it do
+ is_expected.to contain_exactly(
+ "edit",
+ "block",
+ "ban",
+ "deactivate",
+ "delete",
+ "delete_with_contributions",
+ "trust"
+ )
+ end
end
context 'the user is blocked by LDAP' do
@@ -59,7 +79,16 @@ RSpec.describe Admin::UserActionsHelper do
context 'the user is deactivated' do
let_it_be(:user) { create(:user, :deactivated) }
- it { is_expected.to contain_exactly("edit", "block", "ban", "activate", "delete", "delete_with_contributions") }
+ it do
+ is_expected.to contain_exactly(
+ "edit",
+ "block",
+ "ban",
+ "activate",
+ "delete",
+ "delete_with_contributions"
+ )
+ end
end
context 'the user is locked' do
@@ -77,7 +106,8 @@ RSpec.describe Admin::UserActionsHelper do
"deactivate",
"unlock",
"delete",
- "delete_with_contributions"
+ "delete_with_contributions",
+ "trust"
)
}
end
@@ -88,6 +118,21 @@ RSpec.describe Admin::UserActionsHelper do
it { is_expected.to contain_exactly("edit", "unban", "delete", "delete_with_contributions") }
end
+ context 'the user is trusted' do
+ let_it_be(:user) { create(:user, :trusted) }
+
+ it do
+ is_expected.to contain_exactly("edit",
+ "block",
+ "deactivate",
+ "ban",
+ "delete",
+ "delete_with_contributions",
+ "untrust"
+ )
+ end
+ end
+
context 'the current_user does not have permission to delete the user' do
let_it_be(:user) { build(:user) }
@@ -95,7 +140,7 @@ RSpec.describe Admin::UserActionsHelper do
allow(helper).to receive(:can?).with(current_user, :destroy_user, user).and_return(false)
end
- it { is_expected.to contain_exactly("edit", "block", "ban", "deactivate") }
+ it { is_expected.to contain_exactly("edit", "block", "ban", "deactivate", "trust") }
end
context 'the user is a sole owner of a group' do
@@ -106,7 +151,7 @@ RSpec.describe Admin::UserActionsHelper do
group.add_owner(user)
end
- it { is_expected.to contain_exactly("edit", "block", "ban", "deactivate", "delete_with_contributions") }
+ it { is_expected.to contain_exactly("edit", "block", "ban", "deactivate", "delete_with_contributions", "trust") }
end
context 'the user is a bot' do
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index 7cf64c6e049..d67d07a4f1e 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -637,6 +637,21 @@ RSpec.describe ApplicationHelper do
expect(discord).to eq('https://discord.com/users/1234567890123456789')
end
end
+
+ context 'when mastodon is set' do
+ let_it_be(:user) { build(:user) }
+ let(:mastodon) { mastodon_url(user) }
+
+ it 'returns an empty string if mastodon username is not set' do
+ expect(mastodon).to eq('')
+ end
+
+ it 'returns mastodon url when mastodon username is set' do
+ user.mastodon = '@robin@example.com'
+
+ expect(mastodon).to eq(external_redirect_path(url: 'https://example.com/@robin'))
+ end
+ end
end
describe '#gitlab_ui_form_for' do
@@ -740,14 +755,6 @@ RSpec.describe ApplicationHelper do
it { is_expected.not_to include('with-top-bar') }
end
end
-
- describe 'logged-out-marketing-header' do
- before do
- allow(helper).to receive(:current_user).and_return(nil)
- end
-
- it { is_expected.not_to include('logged-out-marketing-header') }
- end
end
describe '#dispensable_render' do
diff --git a/spec/helpers/auth_helper_spec.rb b/spec/helpers/auth_helper_spec.rb
index 40798b4c038..264137add8a 100644
--- a/spec/helpers/auth_helper_spec.rb
+++ b/spec/helpers/auth_helper_spec.rb
@@ -2,7 +2,9 @@
require "spec_helper"
-RSpec.describe AuthHelper do
+RSpec.describe AuthHelper, feature_category: :system_access do
+ include LoginHelpers
+
describe "button_based_providers" do
it 'returns all enabled providers from devise' do
allow(helper).to receive(:auth_providers) { [:twitter, :github] }
@@ -310,88 +312,16 @@ RSpec.describe AuthHelper do
end
end
- describe '#auth_strategy_class' do
- subject(:auth_strategy_class) { helper.auth_strategy_class(name) }
-
- context 'when configuration specifies no provider' do
- let(:name) { 'does_not_exist' }
-
- before do
- allow(Gitlab.config.omniauth).to receive(:providers).and_return([])
- end
-
- it 'returns false' do
- expect(auth_strategy_class).to be_falsey
- end
- end
-
- context 'when configuration specifies a provider with args but without strategy_class' do
- let(:name) { 'google_oauth2' }
- let(:provider) do
- Struct.new(:name, :args).new(
- name,
- 'app_id' => 'YOUR_APP_ID'
- )
- end
-
- before do
- allow(Gitlab.config.omniauth).to receive(:providers).and_return([provider])
- end
-
- it 'returns false' do
- expect(auth_strategy_class).to be_falsey
- end
- end
-
- context 'when configuration specifies a provider with args and strategy_class' do
- let(:name) { 'provider1' }
- let(:strategy) { 'OmniAuth::Strategies::LDAP' }
- let(:provider) do
- Struct.new(:name, :args).new(
- name,
- 'strategy_class' => strategy
- )
- end
-
- before do
- allow(Gitlab.config.omniauth).to receive(:providers).and_return([provider])
- end
-
- it 'returns the class' do
- expect(auth_strategy_class).to eq(strategy)
- end
- end
-
- context 'when configuration specifies another provider with args and another strategy_class' do
- let(:name) { 'provider1' }
- let(:strategy) { 'OmniAuth::Strategies::LDAP' }
- let(:provider) do
- Struct.new(:name, :args).new(
- 'another_name',
- 'strategy_class' => strategy
- )
- end
-
- before do
- allow(Gitlab.config.omniauth).to receive(:providers).and_return([provider])
- end
-
- it 'returns false' do
- expect(auth_strategy_class).to be_falsey
- end
- end
- end
-
describe '#saml_providers' do
subject(:saml_providers) { helper.saml_providers }
let(:saml_strategy) { 'OmniAuth::Strategies::SAML' }
- let(:saml_provider_1_name) { 'saml_provider_1' }
+ let(:saml_provider_1_name) { 'saml' }
let(:saml_provider_1) do
Struct.new(:name, :args).new(
saml_provider_1_name,
- 'strategy_class' => saml_strategy
+ {}
)
end
@@ -422,7 +352,7 @@ RSpec.describe AuthHelper do
context 'when SAML is enabled without specifying a strategy class' do
before do
- allow(Gitlab::Auth::OAuth::Provider).to receive(:providers).and_return([:saml])
+ stub_omniauth_config(providers: [saml_provider_1])
end
it 'returns the saml provider' do
@@ -432,8 +362,7 @@ RSpec.describe AuthHelper do
context 'when configuration specifies no provider' do
before do
- allow(Devise).to receive(:omniauth_providers).and_return([])
- allow(Gitlab.config.omniauth).to receive(:providers).and_return([])
+ stub_omniauth_config(providers: [])
end
it 'returns an empty list' do
@@ -443,30 +372,27 @@ RSpec.describe AuthHelper do
context 'when configuration specifies a provider with a SAML strategy_class' do
before do
- allow(Devise).to receive(:omniauth_providers).and_return([saml_provider_1_name])
- allow(Gitlab.config.omniauth).to receive(:providers).and_return([saml_provider_1])
+ stub_omniauth_config(providers: [saml_provider_1])
end
it 'returns the provider' do
- expect(saml_providers).to match_array([saml_provider_1_name])
+ expect(saml_providers).to match_array([saml_provider_1_name.to_sym])
end
end
context 'when configuration specifies two providers with a SAML strategy_class' do
before do
- allow(Devise).to receive(:omniauth_providers).and_return([saml_provider_1_name, saml_provider_2_name])
- allow(Gitlab.config.omniauth).to receive(:providers).and_return([saml_provider_1, saml_provider_2])
+ stub_omniauth_config(providers: [saml_provider_1, saml_provider_2])
end
it 'returns the provider' do
- expect(saml_providers).to match_array([saml_provider_1_name, saml_provider_2_name])
+ expect(saml_providers).to match_array([saml_provider_1_name.to_sym, saml_provider_2_name.to_sym])
end
end
context 'when configuration specifies a provider with a non-SAML strategy_class' do
before do
- allow(Devise).to receive(:omniauth_providers).and_return([ldap_provider_name])
- allow(Gitlab.config.omniauth).to receive(:providers).and_return([ldap_provider])
+ stub_omniauth_config(providers: [ldap_provider])
end
it 'returns an empty list' do
@@ -476,12 +402,11 @@ RSpec.describe AuthHelper do
context 'when configuration specifies four providers but only two with SAML strategy_class' do
before do
- allow(Devise).to receive(:omniauth_providers).and_return([saml_provider_1_name, ldap_provider_name, saml_provider_2_name, google_oauth2_provider_name])
- allow(Gitlab.config.omniauth).to receive(:providers).and_return([saml_provider_1, ldap_provider, saml_provider_2, google_oauth2_provider])
+ stub_omniauth_config(providers: [saml_provider_1, ldap_provider, saml_provider_2, google_oauth2_provider])
end
it 'returns the provider' do
- expect(saml_providers).to match_array([saml_provider_1_name, saml_provider_2_name])
+ expect(saml_providers).to match_array([saml_provider_1_name.to_sym, saml_provider_2_name.to_sym])
end
end
end
diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb
index e832fa2718a..d09e01c959c 100644
--- a/spec/helpers/blob_helper_spec.rb
+++ b/spec/helpers/blob_helper_spec.rb
@@ -221,7 +221,7 @@ RSpec.describe BlobHelper do
context 'when file is a pipeline config file' do
let(:data) { File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) }
- let(:blob) { fake_blob(path: Gitlab::FileDetector::PATTERNS[:gitlab_ci], data: data) }
+ let(:blob) { fake_blob(path: project.ci_config_path_or_default, data: data) }
it 'is true' do
expect(helper.show_suggest_pipeline_creation_celebration?).to be_truthy
diff --git a/spec/helpers/ci/catalog/resources_helper_spec.rb b/spec/helpers/ci/catalog/resources_helper_spec.rb
index 3b29e6f292b..5c5d02ce6d8 100644
--- a/spec/helpers/ci/catalog/resources_helper_spec.rb
+++ b/spec/helpers/ci/catalog/resources_helper_spec.rb
@@ -5,17 +5,34 @@ require 'spec_helper'
RSpec.describe Ci::Catalog::ResourcesHelper, feature_category: :pipeline_composition do
include Devise::Test::ControllerHelpers
- let_it_be(:project) { build(:project) }
+ let_it_be(:project) { create_default(:project) }
+ let_it_be(:user) { create_default(:user) }
+
+ before do
+ allow(helper).to receive(:current_user).and_return(user)
+ end
describe '#can_add_catalog_resource?' do
subject { helper.can_add_catalog_resource?(project) }
- before do
- stub_licensed_features(ci_namespace_catalog: false)
+ context 'when user is not an owner' do
+ before_all do
+ project.add_maintainer(user)
+ end
+
+ it 'returns false' do
+ expect(subject).to be false
+ end
end
- it 'user cannot add a catalog resource in CE regardless of permissions' do
- expect(subject).to be false
+ context 'when user is an owner' do
+ before_all do
+ project.add_owner(user)
+ end
+
+ it 'returns true' do
+ expect(subject).to be true
+ end
end
end
diff --git a/spec/helpers/ci/jobs_helper_spec.rb b/spec/helpers/ci/jobs_helper_spec.rb
index 884fe7a018e..af369f7d420 100644
--- a/spec/helpers/ci/jobs_helper_spec.rb
+++ b/spec/helpers/ci/jobs_helper_spec.rb
@@ -43,6 +43,7 @@ RSpec.describe Ci::JobsHelper, feature_category: :continuous_integration do
"scheduled" => "SCHEDULED",
"skipped" => "SKIPPED",
"success" => "SUCCESS",
+ "waiting_for_callback" => "WAITING_FOR_CALLBACK",
"waiting_for_resource" => "WAITING_FOR_RESOURCE"
})
end
diff --git a/spec/helpers/ci/pipeline_editor_helper_spec.rb b/spec/helpers/ci/pipeline_editor_helper_spec.rb
index f411f533b25..dd7d602e2a5 100644
--- a/spec/helpers/ci/pipeline_editor_helper_spec.rb
+++ b/spec/helpers/ci/pipeline_editor_helper_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::PipelineEditorHelper do
+RSpec.describe Ci::PipelineEditorHelper, feature_category: :pipeline_composition do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
diff --git a/spec/helpers/ci/pipelines_helper_spec.rb b/spec/helpers/ci/pipelines_helper_spec.rb
index 477c07bf3e3..1a5c036b4f1 100644
--- a/spec/helpers/ci/pipelines_helper_spec.rb
+++ b/spec/helpers/ci/pipelines_helper_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::PipelinesHelper do
+RSpec.describe Ci::PipelinesHelper, feature_category: :continuous_integration do
include Devise::Test::ControllerHelpers
describe 'pipeline_warnings' do
@@ -99,75 +99,6 @@ RSpec.describe Ci::PipelinesHelper do
:full_path,
:visibility_pipeline_id_type])
end
-
- describe 'when the project is eligible for the `ios_specific_templates` experiment' do
- let_it_be(:project) { create(:project, :auto_devops_disabled, shared_runners_enabled: false) }
- let_it_be(:user) { create(:user) }
-
- before do
- allow(helper).to receive(:current_user).and_return(user)
- project.add_developer(user)
- create(:project_setting, project: project, target_platforms: %w[ios])
- end
-
- describe 'the `registration_token` attribute' do
- subject { data[:registration_token] }
-
- context 'when the `ios_specific_templates` experiment variant is control' do
- before do
- stub_experiments(ios_specific_templates: :control)
- end
-
- it { is_expected.to be_nil }
- end
-
- context 'when the `ios_specific_templates` experiment variant is candidate' do
- before do
- stub_experiments(ios_specific_templates: :candidate)
- end
-
- context 'when the user cannot register project runners' do
- before do
- allow(helper).to receive(:can?).with(user, :register_project_runners, project).and_return(false)
- end
-
- it { is_expected.to be_nil }
- end
-
- context 'when the user can register project runners' do
- it { is_expected.to eq(project.runners_token) }
- end
- end
- end
-
- describe 'the `ios_runners_available` attribute', :saas do
- subject { data[:ios_runners_available] }
-
- context 'when the `ios_specific_templates` experiment variant is control' do
- before do
- stub_experiments(ios_specific_templates: :control)
- end
-
- it { is_expected.to be_nil }
- end
-
- context 'when the `ios_specific_templates` experiment variant is candidate' do
- before do
- stub_experiments(ios_specific_templates: :candidate)
- end
-
- context 'when shared runners are not enabled' do
- it { is_expected.to eq('false') }
- end
-
- context 'when shared runners are enabled' do
- let_it_be(:project) { create(:project, :auto_devops_disabled, shared_runners_enabled: true) }
-
- it { is_expected.to eq('true') }
- end
- end
- end
- end
end
describe '#visibility_pipeline_id_type' do
diff --git a/spec/helpers/ci/status_helper_spec.rb b/spec/helpers/ci/status_helper_spec.rb
index 17fe474b360..502a535e102 100644
--- a/spec/helpers/ci/status_helper_spec.rb
+++ b/spec/helpers/ci/status_helper_spec.rb
@@ -8,18 +8,6 @@ RSpec.describe Ci::StatusHelper do
let(:success_commit) { double("Ci::Pipeline", status: 'success') }
let(:failed_commit) { double("Ci::Pipeline", status: 'failed') }
- describe '#ci_icon_for_status' do
- it 'renders to correct svg on success' do
- expect(helper.ci_icon_for_status('success').to_s)
- .to include 'status_success'
- end
-
- it 'renders the correct svg on failure' do
- expect(helper.ci_icon_for_status('failed').to_s)
- .to include 'status_failed'
- end
- end
-
describe "#pipeline_status_cache_key" do
it "builds a cache key for pipeline status" do
pipeline_status = Gitlab::Cache::Ci::ProjectPipelineStatus.new(
@@ -33,23 +21,19 @@ RSpec.describe Ci::StatusHelper do
end
end
- describe "#render_status_with_link" do
- subject { helper.render_status_with_link("success") }
-
- it "renders a passed status icon" do
- is_expected.to include("<span class=\"ci-status-link ci-status-icon-success d-inline-flex")
- end
+ describe "#render_ci_icon" do
+ subject { helper.render_ci_icon("success") }
it "has 'Pipeline' as the status type in the title" do
is_expected.to include("title=\"Pipeline: passed\"")
end
it "has the success status icon" do
- is_expected.to include("ci-status-icon-success")
+ is_expected.to include("ci-icon-variant-success")
end
context "when pipeline has commit path" do
- subject { helper.render_status_with_link("success", "/commit-path") }
+ subject { helper.render_ci_icon("success", "/commit-path") }
it "links to commit" do
is_expected.to include("href=\"/commit-path\"")
@@ -60,53 +44,40 @@ RSpec.describe Ci::StatusHelper do
end
it "has the correct status icon" do
- is_expected.to include("ci-status-icon-success")
+ is_expected.to include("ci-icon-variant-success")
end
end
- context "when different type than pipeline is provided" do
- subject { helper.render_status_with_link("success", type: "commit") }
+ context "when showing status text" do
+ subject do
+ detailed_status = Gitlab::Ci::Status::Success.new(build(:ci_build, :success), build(:user))
+ helper.render_ci_icon(detailed_status, show_status_text: true)
+ end
- it "has the provided type in the title" do
- is_expected.to include("title=\"Commit: passed\"")
+ it "contains status text" do
+ is_expected.to include("data-testid=\"ci-icon-text\"")
+ is_expected.to include("passed")
end
end
context "when tooltip_placement is provided" do
- subject { helper.render_status_with_link("success", tooltip_placement: "right") }
+ subject { helper.render_ci_icon("success", tooltip_placement: "right") }
it "has the provided tooltip placement" do
is_expected.to include("data-placement=\"right\"")
end
end
- context "when additional CSS classes are provided" do
- subject { helper.render_status_with_link("success", cssclass: "extra-class") }
-
- it "has appended extra class to icon classes" do
- is_expected.to include('class="ci-status-link ci-status-icon-success d-inline-flex ' \
- 'gl-line-height-1 extra-class"')
- end
- end
-
context "when container is provided" do
- subject { helper.render_status_with_link("success", container: "my-container") }
+ subject { helper.render_ci_icon("success", container: "my-container") }
it "has the provided container in data" do
is_expected.to include("data-container=\"my-container\"")
end
end
- context "when icon_size is provided" do
- subject { helper.render_status_with_link("success", icon_size: 24) }
-
- it "has the svg class to change size" do
- is_expected.to include("<svg class=\"s24\"")
- end
- end
-
context "when status is success-with-warnings" do
- subject { helper.render_status_with_link("success-with-warnings") }
+ subject { helper.render_ci_icon("success-with-warnings") }
it "renders warning variant of gl-badge" do
is_expected.to include('gl-badge badge badge-pill badge-warning')
@@ -114,33 +85,41 @@ RSpec.describe Ci::StatusHelper do
end
context "when status is manual" do
- subject { helper.render_status_with_link("manual") }
+ subject { helper.render_ci_icon("manual") }
it "renders neutral variant of gl-badge" do
is_expected.to include('gl-badge badge badge-pill badge-neutral')
end
end
- end
- describe '#badge_variant' do
- using RSpec::Parameterized::TableSyntax
-
- where(:status, :expected_badge_variant_class) do
- 'success' | 'badge-success'
- 'success-with-warnings' | 'badge-warning'
- 'pending' | 'badge-warning'
- 'failed' | 'badge-danger'
- 'running' | 'badge-info'
- 'canceled' | 'badge-neutral'
- 'manual' | 'badge-neutral'
- 'other-status' | 'badge-muted'
- end
+ describe 'badge and icon appearance' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:status, :icon, :badge_variant) do
+ 'success' | 'status_success_borderless' | 'success'
+ 'success-with-warnings' | 'status_warning_borderless' | 'warning'
+ 'pending' | 'status_pending_borderless' | 'warning'
+ 'waiting-for-resource' | 'status_pending_borderless' | 'warning'
+ 'failed' | 'status_failed_borderless' | 'danger'
+ 'running' | 'status_running_borderless' | 'info'
+ 'preparing' | 'status_preparing_borderless' | 'neutral'
+ 'canceled' | 'status_canceled_borderless' | 'neutral'
+ 'created' | 'status_created_borderless' | 'neutral'
+ 'scheduled' | 'status_scheduled_borderless' | 'neutral'
+ 'play' | 'play' | 'neutral'
+ 'skipped' | 'status_skipped_borderless' | 'neutral'
+ 'manual' | 'status_manual_borderless' | 'neutral'
+ 'other-status' | 'status_canceled_borderless' | 'neutral'
+ end
- with_them do
- subject { helper.render_status_with_link(status) }
+ with_them do
+ subject { helper.render_ci_icon(status) }
- it 'uses the correct badge variant classes for gl-badge' do
- is_expected.to include("gl-badge badge badge-pill #{expected_badge_variant_class}")
+ it 'uses the correct variant and icon for status' do
+ is_expected.to include("gl-badge badge badge-pill badge-#{badge_variant}")
+ is_expected.to include("ci-icon-variant-#{badge_variant}")
+ is_expected.to include("data-testid=\"#{icon}-icon\"")
+ end
end
end
end
diff --git a/spec/helpers/colors_helper_spec.rb b/spec/helpers/colors_helper_spec.rb
index ca5cafb7ebe..328796ef1ae 100644
--- a/spec/helpers/colors_helper_spec.rb
+++ b/spec/helpers/colors_helper_spec.rb
@@ -39,51 +39,4 @@ RSpec.describe ColorsHelper do
end
end
end
-
- describe '#rgb_array_to_hex_color' do
- context 'valid RGB array' do
- where(:rgb_array, :hex_color) do
- [0, 0, 0] | '#000000'
- [0, 0, 255] | '#0000ff'
- [0, 255, 0] | '#00ff00'
- [255, 0, 0] | '#ff0000'
- [12, 34, 56] | '#0c2238'
- [222, 111, 88] | '#de6f58'
- [255, 255, 255] | '#ffffff'
- end
-
- with_them do
- it 'returns correct hex color' do
- expect(helper.rgb_array_to_hex_color(rgb_array)).to eq(hex_color)
- end
- end
- end
-
- context 'invalid RGB array' do
- where(:rgb_array) do
- [
- '',
- '#000000',
- 0,
- nil,
- [],
- [0],
- [0, 0],
- [0, 0, 0, 0],
- [-1, 0, 0],
- [0, -1, 0],
- [0, 0, -1],
- [256, 0, 0],
- [0, 256, 0],
- [0, 0, 256]
- ]
- end
-
- with_them do
- it 'raise ArgumentError' do
- expect { helper.rgb_array_to_hex_color(rgb_array) }.to raise_error(ArgumentError)
- end
- end
- end
- end
end
diff --git a/spec/helpers/environment_helper_spec.rb b/spec/helpers/environment_helper_spec.rb
index 1383bf34881..0c04417fca6 100644
--- a/spec/helpers/environment_helper_spec.rb
+++ b/spec/helpers/environment_helper_spec.rb
@@ -3,45 +3,6 @@
require 'spec_helper'
RSpec.describe EnvironmentHelper, feature_category: :environment_management do
- describe '#render_deployment_status' do
- context 'when using a manual deployment' do
- it 'renders a span tag' do
- deploy = build(:deployment, deployable: nil, status: :success)
- html = helper.render_deployment_status(deploy)
-
- expect(html).to have_css('span.ci-status.ci-success')
- end
- end
-
- context 'when using a deployment from a build' do
- it 'renders a link tag' do
- deploy = build(:deployment, status: :success)
- html = helper.render_deployment_status(deploy)
-
- expect(html).to have_css('a.ci-status.ci-success')
- end
- end
-
- context 'when deploying from a bridge' do
- it 'renders a span tag' do
- deploy = build(:deployment, deployable: create(:ci_bridge), status: :success)
- html = helper.render_deployment_status(deploy)
-
- expect(html).to have_css('span.ci-status.ci-success')
- end
- end
-
- context 'for a blocked deployment' do
- subject { helper.render_deployment_status(deployment) }
-
- let(:deployment) { build(:deployment, :blocked) }
-
- it 'indicates the status' do
- expect(subject).to have_text('blocked')
- end
- end
- end
-
describe '#environments_detail_data_json' do
subject { helper.environments_detail_data_json(user, project, environment) }
diff --git a/spec/helpers/environments_helper_spec.rb b/spec/helpers/environments_helper_spec.rb
index 6624404bc49..14f99f144b2 100644
--- a/spec/helpers/environments_helper_spec.rb
+++ b/spec/helpers/environments_helper_spec.rb
@@ -23,8 +23,8 @@ RSpec.describe EnvironmentsHelper, feature_category: :environment_management do
'settings_path' => edit_project_settings_integration_path(project, 'prometheus'),
'clusters_path' => project_clusters_path(project),
'current_environment_name' => environment.name,
- 'documentation_path' => help_page_path('administration/monitoring/prometheus/index.md'),
- 'add_dashboard_documentation_path' => help_page_path('operations/metrics/dashboards/index.md', anchor: 'add-a-new-dashboard-to-your-project'),
+ 'documentation_path' => help_page_path('administration/monitoring/prometheus/index'),
+ 'add_dashboard_documentation_path' => help_page_path('operations/metrics/dashboards/index', anchor: 'add-a-new-dashboard-to-your-project'),
'empty_getting_started_svg_path' => match_asset_path('/assets/illustrations/monitoring/getting_started.svg'),
'empty_loading_svg_path' => match_asset_path('/assets/illustrations/monitoring/loading.svg'),
'empty_no_data_svg_path' => match_asset_path('/assets/illustrations/monitoring/no_data.svg'),
@@ -95,17 +95,4 @@ RSpec.describe EnvironmentsHelper, feature_category: :environment_management do
expect(subject).to eq(true)
end
end
-
- describe '#environment_logs_data' do
- it 'returns logs data' do
- expected_data = {
- "environment_name": environment.name,
- "environments_path": api_v4_projects_environments_path(id: project.id),
- "environment_id": environment.id,
- "clusters_path": project_clusters_path(project, format: :json)
- }
-
- expect(helper.environment_logs_data(project, environment)).to eq(expected_data)
- end
- end
end
diff --git a/spec/helpers/events_helper_spec.rb b/spec/helpers/events_helper_spec.rb
index 6ffca876361..d19df3d1395 100644
--- a/spec/helpers/events_helper_spec.rb
+++ b/spec/helpers/events_helper_spec.rb
@@ -22,7 +22,8 @@ RSpec.describe EventsHelper, factory_default: :keep, feature_category: :user_pro
it 'returns a link to the author' do
name = user.name
- expect(helper.link_to_author(event)).to eq(link_to(name, user_path(user.username), title: name))
+ expect(helper.link_to_author(event)).to eq(link_to(name, user_path(user.username), title: name,
+ data: { user_id: user.id, username: user.username }, class: 'js-user-link'))
end
it 'returns the author name if the author is not present' do
@@ -35,7 +36,71 @@ RSpec.describe EventsHelper, factory_default: :keep, feature_category: :user_pro
allow(helper).to receive(:current_user).and_return(user)
name = _('You')
- expect(helper.link_to_author(event, self_added: true)).to eq(link_to(name, user_path(user.username), title: name))
+ expect(helper.link_to_author(event, self_added: true)).to eq(link_to(name, user_path(user.username), title: name,
+ data: { user_id: user.id, username: user.username }, class: 'js-user-link'))
+ end
+ end
+
+ describe '#icon_for_profile_event' do
+ let(:event) { build(:event, :joined) }
+ let(:users_activity_page?) { true }
+
+ before do
+ allow(helper).to receive(:current_path?).and_call_original
+ allow(helper).to receive(:current_path?).with('users#activity').and_return(users_activity_page?)
+ end
+
+ context 'when on users activity page' do
+ it 'gives an icon with specialized classes' do
+ result = helper.icon_for_profile_event(event)
+
+ expect(result).to include('joined-icon')
+ expect(result).to include('<svg')
+ end
+
+ context 'with an unsupported event action_name' do
+ let(:event) { build(:event, :expired) }
+
+ it 'does not have an icon' do
+ result = helper.icon_for_profile_event(event)
+
+ expect(result).not_to include('<svg')
+ end
+ end
+ end
+
+ context 'when not on users activity page' do
+ let(:users_activity_page?) { false }
+
+ it 'gives an icon with specialized classes' do
+ result = helper.icon_for_profile_event(event)
+
+ expect(result).not_to include('joined-icon')
+ expect(result).not_to include('<svg')
+ expect(result).to include('<img')
+ end
+ end
+ end
+
+ describe '#event_user_info' do
+ let(:event) { build(:event) }
+ let(:users_activity_page?) { true }
+
+ before do
+ allow(helper).to receive(:current_path?).and_call_original
+ allow(helper).to receive(:current_path?).with('users#activity').and_return(users_activity_page?)
+ end
+
+ subject { helper.event_user_info(event) }
+
+ context 'when on users activity page' do
+ it { is_expected.to be_nil }
+ end
+
+ context 'when not on users activity page' do
+ let(:users_activity_page?) { false }
+
+ it { is_expected.to include('<div') }
end
end
@@ -268,12 +333,26 @@ RSpec.describe EventsHelper, factory_default: :keep, feature_category: :user_pro
describe '#event_wiki_title_html' do
let(:event) { create(:wiki_page_event) }
+ let(:url) { helper.event_wiki_page_target_url(event) }
+ let(:title) { event.target_title }
it 'produces a suitable title chunk' do
- url = helper.event_wiki_page_target_url(event)
- title = event.target_title
html = [
- "<span class=\"event-target-type gl-mr-2\">wiki page</span>",
+ "<span class=\"event-target-type gl-mr-2 \">wiki page</span>",
+ "<a title=\"#{title}\" class=\"has-tooltip event-target-link gl-mr-2\" href=\"#{url}\">",
+ title,
+ "</a>"
+ ].join
+
+ expect(helper.event_wiki_title_html(event)).to eq(html)
+ end
+
+ it 'produces a suitable title chunk on the user profile' do
+ allow(helper).to receive(:user_profile_activity_classes).and_return(
+ 'gl-font-weight-semibold gl-text-black-normal')
+
+ html = [
+ "<span class=\"event-target-type gl-mr-2 gl-font-weight-semibold gl-text-black-normal\">wiki page</span>",
"<a title=\"#{title}\" class=\"has-tooltip event-target-link gl-mr-2\" href=\"#{url}\">",
title,
"</a>"
@@ -441,5 +520,28 @@ RSpec.describe EventsHelper, factory_default: :keep, feature_category: :user_pro
end
end
end
+
+ describe '#user_profile_activity_classes' do
+ let(:users_activity_page?) { true }
+
+ before do
+ allow(helper).to receive(:current_path?).and_call_original
+ allow(helper).to receive(:current_path?).with('users#activity').and_return(users_activity_page?)
+ end
+
+ context 'when on the user activity page' do
+ it 'returns the expected class names' do
+ expect(helper.user_profile_activity_classes).to eq(' gl-font-weight-semibold gl-text-black-normal')
+ end
+ end
+
+ context 'when not on the user activity page' do
+ let(:users_activity_page?) { false }
+
+ it 'returns an empty string' do
+ expect(helper.user_profile_activity_classes).to eq('')
+ end
+ end
+ end
end
# rubocop:enable RSpec/FactoryBot/AvoidCreate
diff --git a/spec/helpers/groups/group_members_helper_spec.rb b/spec/helpers/groups/group_members_helper_spec.rb
index a9c6822e2c1..3a7c852b683 100644
--- a/spec/helpers/groups/group_members_helper_spec.rb
+++ b/spec/helpers/groups/group_members_helper_spec.rb
@@ -19,14 +19,9 @@ RSpec.describe Groups::GroupMembersHelper do
let(:members_collection) { members }
before do
- allow(helper).to receive(:can?).with(current_user, :export_group_memberships, group).and_return(false)
- allow(helper).to receive(:can?).with(current_user, :owner_access, group).and_return(true)
allow(helper).to receive(:current_user).and_return(current_user)
- allow(helper).to receive(:can?).with(current_user, :export_group_memberships, shared_group).and_return(true)
allow(helper).to receive(:group_group_member_path).with(shared_group, ':id').and_return('/groups/foo-bar/-/group_members/:id')
allow(helper).to receive(:group_group_link_path).with(shared_group, ':id').and_return('/groups/foo-bar/-/group_links/:id')
- allow(helper).to receive(:can?).with(current_user, :admin_group_member, shared_group).and_return(true)
- allow(helper).to receive(:can?).with(current_user, :admin_member_access_request, shared_group).and_return(true)
end
subject do
@@ -54,8 +49,8 @@ RSpec.describe Groups::GroupMembersHelper do
it 'returns expected json' do
expected = {
source_id: shared_group.id,
- can_manage_members: true,
- can_manage_access_requests: true,
+ can_manage_members: be_in([true, false]),
+ can_manage_access_requests: be_in([true, false]),
group_name: shared_group.name,
group_path: shared_group.full_path
}
@@ -102,9 +97,6 @@ RSpec.describe Groups::GroupMembersHelper do
before do
allow(helper).to receive(:group_group_member_path).with(sub_shared_group, ':id').and_return('/groups/foo-bar/-/group_members/:id')
allow(helper).to receive(:group_group_link_path).with(sub_shared_group, ':id').and_return('/groups/foo-bar/-/group_links/:id')
- allow(helper).to receive(:can?).with(current_user, :admin_group_member, sub_shared_group).and_return(true)
- allow(helper).to receive(:can?).with(current_user, :admin_member_access_request, sub_shared_group).and_return(true)
- allow(helper).to receive(:can?).with(current_user, :export_group_memberships, sub_shared_group).and_return(true)
end
subject do
diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb
index 0db15541b99..f5cadfc2761 100644
--- a/spec/helpers/groups_helper_spec.rb
+++ b/spec/helpers/groups_helper_spec.rb
@@ -288,13 +288,13 @@ RSpec.describe GroupsHelper, feature_category: :groups_and_projects do
end
it 'returns false if parent group is disabling emails' do
- allow(group).to receive(:emails_disabled?).and_return(true)
+ allow(group).to receive(:emails_enabled?).and_return(false)
expect(helper.can_disable_group_emails?(subgroup)).to be_falsey
end
it 'returns true if parent group is not disabling emails' do
- allow(group).to receive(:emails_disabled?).and_return(false)
+ allow(group).to receive(:emails_enabled?).and_return(true)
expect(helper.can_disable_group_emails?(subgroup)).to be_truthy
end
diff --git a/spec/helpers/ide_helper_spec.rb b/spec/helpers/ide_helper_spec.rb
index 47500b8e21e..d5d7f8f72b3 100644
--- a/spec/helpers/ide_helper_spec.rb
+++ b/spec/helpers/ide_helper_spec.rb
@@ -101,7 +101,7 @@ RSpec.describe IdeHelper, feature_category: :web_ide do
'user-preferences-path' => profile_preferences_path,
'sign-in-path' => 'test-sign-in-path',
'new-web-ide-help-page-path' =>
- help_page_path('user/project/web_ide/index.md', anchor: 'vscode-reimplementation'),
+ help_page_path('user/project/web_ide/index', anchor: 'vscode-reimplementation'),
'csp-nonce' => 'test-csp-nonce',
'ide-remote-path' => ide_remote_path(remote_host: ':remote_host', remote_path: ':remote_path')
}
diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb
index 0faea5629e8..6abce4c5983 100644
--- a/spec/helpers/issuables_helper_spec.rb
+++ b/spec/helpers/issuables_helper_spec.rb
@@ -109,10 +109,14 @@ RSpec.describe IssuablesHelper, feature_category: :team_planning do
allow(helper).to receive(:current_user).and_return(user)
end
- context 'when assigned issues count is over 100' do
- let_it_be(:issues) { create_list(:issue, 101, project: project, assignees: [user]) }
+ context 'when assigned issues count is over MAX_LIMIT_FOR_ASSIGNEED_ISSUES_COUNT' do
+ before do
+ stub_const('User::MAX_LIMIT_FOR_ASSIGNEED_ISSUES_COUNT', 2)
+ end
+
+ let_it_be(:issues) { create_list(:issue, 3, project: project, assignees: [user]) }
- it { is_expected.to eq 100 }
+ it { is_expected.to eq 2 }
end
end
end
@@ -127,10 +131,14 @@ RSpec.describe IssuablesHelper, feature_category: :team_planning do
allow(helper).to receive(:current_user).and_return(user)
end
- context 'when assigned issues count is over 99' do
- let_it_be(:issues) { create_list(:issue, 100, project: project, assignees: [user]) }
+ context 'when assigned issues count is over MAX_LIMIT_FOR_ASSIGNEED_ISSUES_COUNT' do
+ before do
+ stub_const('User::MAX_LIMIT_FOR_ASSIGNEED_ISSUES_COUNT', 2)
+ end
+
+ let_it_be(:issues) { create_list(:issue, 3, project: project, assignees: [user]) }
- it { is_expected.to eq '99+' }
+ it { is_expected.to eq '1+' }
end
end
diff --git a/spec/helpers/members_helper_spec.rb b/spec/helpers/members_helper_spec.rb
index 68a12d8dbf7..d2d758cdc7c 100644
--- a/spec/helpers/members_helper_spec.rb
+++ b/spec/helpers/members_helper_spec.rb
@@ -45,21 +45,6 @@ RSpec.describe MembersHelper do
end
end
- describe '#remove_member_title' do
- let(:requester) { create(:user) }
- let(:project) { create(:project, :public) }
- let(:project_member) { build(:project_member, project: project) }
- let(:project_member_request) { project.request_access(requester) }
- let(:group) { create(:group) }
- let(:group_member) { build(:group_member, group: group) }
- let(:group_member_request) { group.request_access(requester) }
-
- it { expect(remove_member_title(project_member)).to eq 'Remove user from project' }
- it { expect(remove_member_title(project_member_request)).to eq 'Deny access request from project' }
- it { expect(remove_member_title(group_member)).to eq 'Remove user from group and any subresources' }
- it { expect(remove_member_title(group_member_request)).to eq 'Deny access request from group' }
- end
-
describe '#leave_confirmation_message' do
let(:project) { build_stubbed(:project) }
let(:group) { build_stubbed(:group) }
diff --git a/spec/helpers/nav/new_dropdown_helper_spec.rb b/spec/helpers/nav/new_dropdown_helper_spec.rb
index 47f23c4fa21..4252e10c922 100644
--- a/spec/helpers/nav/new_dropdown_helper_spec.rb
+++ b/spec/helpers/nav/new_dropdown_helper_spec.rb
@@ -11,6 +11,7 @@ RSpec.describe Nav::NewDropdownHelper, feature_category: :navigation do
let(:with_can_create_project) { false }
let(:with_can_create_group) { false }
let(:with_can_create_snippet) { false }
+ let(:with_can_create_organization) { false }
let(:title) { 'Create new...' }
subject(:view_model) do
@@ -24,6 +25,7 @@ RSpec.describe Nav::NewDropdownHelper, feature_category: :navigation do
allow(user).to receive(:can_create_group?) { with_can_create_group }
allow(user).to receive(:can?).and_call_original
allow(user).to receive(:can?).with(:create_snippet) { with_can_create_snippet }
+ allow(user).to receive(:can?).with(:create_organization) { with_can_create_organization }
end
shared_examples 'invite member item' do |partial|
@@ -135,6 +137,39 @@ RSpec.describe Nav::NewDropdownHelper, feature_category: :navigation do
)
end
end
+
+ context 'when can create organization' do
+ let(:with_can_create_organization) { true }
+
+ it 'has new organization menu item' do
+ expect(view_model[:menu_sections]).to eq(
+ expected_menu_section(
+ title: _('In GitLab'),
+ menu_item: ::Gitlab::Nav::TopNavMenuItem.build(
+ id: 'general_new_organization',
+ title: s_('Organization|New organization'),
+ href: '/-/organizations/new',
+ data: {
+ track_action: 'click_link_new_organization_parent',
+ track_label: 'plus_menu_dropdown',
+ track_property: 'navigation_top',
+ testid: 'global_new_organization_link'
+ }
+ )
+ )
+ )
+ end
+
+ context 'when ui_for_organizations feature flag is disabled' do
+ before do
+ stub_feature_flags(ui_for_organizations: false)
+ end
+
+ it 'does not have new organization menu item' do
+ expect(view_model[:menu_sections]).to match_array([])
+ end
+ end
+ end
end
context 'with persisted group' do
diff --git a/spec/helpers/nav_helper_spec.rb b/spec/helpers/nav_helper_spec.rb
index 12ab7ca93c0..9a0f72838fb 100644
--- a/spec/helpers/nav_helper_spec.rb
+++ b/spec/helpers/nav_helper_spec.rb
@@ -145,20 +145,12 @@ RSpec.describe NavHelper, feature_category: :navigation do
let(:user_preference) { nil }
specify { expect(subject).to eq true }
-
- context 'when the user was not enrolled into the new nav via a special feature flag' do
- before do
- stub_feature_flags(super_sidebar_nav_enrolled: false)
- end
-
- specify { expect(subject).to eq false }
- end
end
context 'when user has new nav disabled' do
let(:user_preference) { false }
- specify { expect(subject).to eq false }
+ specify { expect(subject).to eq true }
end
context 'when user has new nav enabled' do
@@ -168,24 +160,6 @@ RSpec.describe NavHelper, feature_category: :navigation do
end
end
- shared_examples 'anonymous show_super_sidebar is supposed to' do
- before do
- stub_feature_flags(super_sidebar_logged_out: feature_flag)
- end
-
- context 'when super_sidebar_logged_out feature flag is disabled' do
- let(:feature_flag) { false }
-
- specify { expect(subject).to eq false }
- end
-
- context 'when super_sidebar_logged_out feature flag is enabled' do
- let(:feature_flag) { true }
-
- specify { expect(subject).to eq true }
- end
- end
-
context 'without a user' do
context 'with current_user (nil) as a default' do
before do
@@ -194,13 +168,13 @@ RSpec.describe NavHelper, feature_category: :navigation do
subject { helper.show_super_sidebar? }
- it_behaves_like 'anonymous show_super_sidebar is supposed to'
+ specify { expect(subject).to eq true }
end
context 'with nil provided as an argument' do
subject { helper.show_super_sidebar?(nil) }
- it_behaves_like 'anonymous show_super_sidebar is supposed to'
+ specify { expect(subject).to eq true }
end
end
diff --git a/spec/helpers/operations_helper_spec.rb b/spec/helpers/operations_helper_spec.rb
index 9e50712a386..e018ab41a83 100644
--- a/spec/helpers/operations_helper_spec.rb
+++ b/spec/helpers/operations_helper_spec.rb
@@ -30,7 +30,7 @@ RSpec.describe OperationsHelper do
it 'returns the correct values' do
expect(subject).to eq(
- 'alerts_setup_url' => help_page_path('operations/incident_management/integrations.md', anchor: 'configuration'),
+ 'alerts_setup_url' => help_page_path('operations/incident_management/integrations', anchor: 'configuration'),
'alerts_usage_url' => project_alert_management_index_path(project),
'prometheus_form_path' => project_settings_integration_path(project, prometheus_integration),
'prometheus_reset_key_path' => reset_alerting_token_project_settings_operations_path(project),
diff --git a/spec/helpers/organizations/organization_helper_spec.rb b/spec/helpers/organizations/organization_helper_spec.rb
index cf8ae358e49..9d55d2a84f8 100644
--- a/spec/helpers/organizations/organization_helper_spec.rb
+++ b/spec/helpers/organizations/organization_helper_spec.rb
@@ -91,4 +91,40 @@ RSpec.describe Organizations::OrganizationHelper, feature_category: :cell do
)
end
end
+
+ describe '#home_organization_setting_app_data' do
+ it 'returns expected json' do
+ expect(Gitlab::Json.parse(helper.home_organization_setting_app_data)).to eq(
+ {
+ 'initial_selection' => 1
+ }
+ )
+ end
+ end
+
+ describe '#organization_settings_general_app_data' do
+ it 'returns expected json' do
+ expect(Gitlab::Json.parse(helper.organization_settings_general_app_data(organization))).to eq(
+ {
+ 'organization' => {
+ 'id' => organization.id,
+ 'name' => organization.name,
+ 'path' => organization.path
+ },
+ 'organizations_path' => organizations_path,
+ 'root_url' => root_url
+ }
+ )
+ end
+ end
+
+ describe '#organization_user_app_data' do
+ it 'returns expected data object' do
+ expect(helper.organization_user_app_data(organization)).to eq(
+ {
+ organization_gid: organization.to_global_id
+ }
+ )
+ end
+ end
end
diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb
index 9d1564dfef1..0bd8792ae83 100644
--- a/spec/helpers/preferences_helper_spec.rb
+++ b/spec/helpers/preferences_helper_spec.rb
@@ -71,6 +71,16 @@ RSpec.describe PreferencesHelper do
end
end
+ describe '#time_display_format_choices_with_default' do
+ it 'returns choices' do
+ expect(helper.time_display_format_choices).to eq({
+ "12-hour: 2:34 PM" => 1,
+ "24-hour: 14:34" => 2,
+ "System" => 0
+ })
+ end
+ end
+
describe '#user_application_theme' do
context 'with a user' do
it "returns user's theme's css_class" do
diff --git a/spec/helpers/projects/pipeline_helper_spec.rb b/spec/helpers/projects/pipeline_helper_spec.rb
index 16c9b8a85ec..7e117fe0cce 100644
--- a/spec/helpers/projects/pipeline_helper_spec.rb
+++ b/spec/helpers/projects/pipeline_helper_spec.rb
@@ -54,6 +54,7 @@ RSpec.describe Projects::PipelineHelper do
failure_reason: pipeline.failure_reason,
triggered_by_path: '',
schedule: pipeline.schedule?.to_s,
+ trigger: pipeline.trigger?.to_s,
child: pipeline.child?.to_s,
latest: pipeline.latest?.to_s,
merge_train_pipeline: pipeline.merge_train_pipeline?.to_s,
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index 90d998e17c3..1e05cf6a7ac 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -128,6 +128,24 @@ RSpec.describe ProjectsHelper, feature_category: :source_code_management do
end
end
+ describe '#load_catalog_resources' do
+ before_all do
+ create_list(:project, 2)
+ end
+
+ let_it_be(:projects) { Project.all.to_a }
+
+ it 'does not execute a database query when project.catalog_resource is accessed' do
+ helper.load_catalog_resources(projects)
+
+ queries = ActiveRecord::QueryRecorder.new do
+ projects.each(&:catalog_resource)
+ end
+
+ expect(queries).not_to exceed_query_limit(0)
+ end
+ end
+
describe '#last_pipeline_from_status_cache' do
before do
# clear cross-example caches
diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb
index 1ff7e48abfc..e1c0aafc3c3 100644
--- a/spec/helpers/search_helper_spec.rb
+++ b/spec/helpers/search_helper_spec.rb
@@ -797,20 +797,24 @@ RSpec.describe SearchHelper, feature_category: :global_search do
allow(self).to receive(:current_user).and_return(:the_current_user)
end
- where(:input, :expected) do
- '0' | false
- '1' | true
- 'yes' | true
- 'no' | false
- 'true' | true
- 'false' | false
- true | true
- false | false
+ shared_context 'with inputs' do
+ where(:input, :expected) do
+ '0' | false
+ '1' | true
+ 'yes' | true
+ 'no' | false
+ 'true' | true
+ 'false' | false
+ true | true
+ false | false
+ end
end
describe 'for confidential' do
let(:params) { { confidential: input } }
+ include_context 'with inputs'
+
with_them do
it 'transforms param' do
expect(::SearchService).to receive(:new).with(:the_current_user, { confidential: expected })
@@ -823,6 +827,8 @@ RSpec.describe SearchHelper, feature_category: :global_search do
describe 'for include_archived' do
let(:params) { { include_archived: input } }
+ include_context 'with inputs'
+
with_them do
it 'transforms param' do
expect(::SearchService).to receive(:new).with(:the_current_user, { include_archived: expected })
diff --git a/spec/helpers/sidebars_helper_spec.rb b/spec/helpers/sidebars_helper_spec.rb
index 3e5ee714b32..c9131ca518f 100644
--- a/spec/helpers/sidebars_helper_spec.rb
+++ b/spec/helpers/sidebars_helper_spec.rb
@@ -287,6 +287,9 @@ RSpec.describe SidebarsHelper, feature_category: :navigation do
{ href: "/groups/new", text: "New group",
component: nil,
extraAttrs: extra_attrs.call("general_new_group") },
+ { href: "/-/organizations/new", text: s_('Organization|New organization'),
+ component: nil,
+ extraAttrs: extra_attrs.call("general_new_organization") },
{ href: "/-/snippets/new", text: "New snippet",
component: nil,
extraAttrs: extra_attrs.call("general_new_snippet") }
@@ -403,15 +406,15 @@ RSpec.describe SidebarsHelper, feature_category: :navigation do
describe 'context switcher persistent links' do
let_it_be(:public_link) do
- [
- { title: s_('Navigation|Explore'), link: '/explore', icon: 'compass' }
- ]
+ { title: s_('Navigation|Explore'), link: '/explore', icon: 'compass' }
end
let_it_be(:public_links_for_user) do
[
{ title: s_('Navigation|Your work'), link: '/', icon: 'work' },
- *public_link
+ public_link,
+ { title: s_('Navigation|Profile'), link: '/-/profile', icon: 'profile' },
+ { title: s_('Navigation|Preferences'), link: '/-/profile/preferences', icon: 'preferences' }
]
end
@@ -436,7 +439,7 @@ RSpec.describe SidebarsHelper, feature_category: :navigation do
let(:user) { nil }
it 'returns only the public links for an anonymous user' do
- expect(subject[:context_switcher_links]).to eq(public_link)
+ expect(subject[:context_switcher_links]).to eq([public_link])
end
end
diff --git a/spec/helpers/sorting_helper_spec.rb b/spec/helpers/sorting_helper_spec.rb
index 0f53cc98415..dccea889d55 100644
--- a/spec/helpers/sorting_helper_spec.rb
+++ b/spec/helpers/sorting_helper_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe SortingHelper do
+RSpec.describe SortingHelper, feature_category: :shared do
include ApplicationHelper
include IconsHelper
include ExploreHelper
diff --git a/spec/helpers/users/callouts_helper_spec.rb b/spec/helpers/users/callouts_helper_spec.rb
index 10f021330db..5ec06507088 100644
--- a/spec/helpers/users/callouts_helper_spec.rb
+++ b/spec/helpers/users/callouts_helper_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe Users::CalloutsHelper do
+RSpec.describe Users::CalloutsHelper, feature_category: :navigation do
let_it_be(:user, refind: true) { create(:user) }
before do
@@ -165,26 +165,6 @@ RSpec.describe Users::CalloutsHelper do
end
end
- describe '.show_pages_menu_callout?' do
- subject { helper.show_pages_menu_callout? }
-
- before do
- allow(helper).to receive(:user_dismissed?).with(described_class::PAGES_MOVED_CALLOUT) { dismissed }
- end
-
- context 'when user has not dismissed' do
- let(:dismissed) { false }
-
- it { is_expected.to be true }
- end
-
- context 'when user dismissed' do
- let(:dismissed) { true }
-
- it { is_expected.to be false }
- end
- end
-
describe '#web_hook_disabled_dismissed?', feature_category: :webhooks do
context 'without a project' do
it 'is false' do
diff --git a/spec/helpers/users_helper_spec.rb b/spec/helpers/users_helper_spec.rb
index 20b5452d2d4..a3d77d76591 100644
--- a/spec/helpers/users_helper_spec.rb
+++ b/spec/helpers/users_helper_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe UsersHelper do
+RSpec.describe UsersHelper, feature_category: :user_management do
include TermsHelper
let_it_be(:user) { create(:user, timezone: ActiveSupport::TimeZone::MAPPING['UTC']) }
diff --git a/spec/helpers/visibility_level_helper_spec.rb b/spec/helpers/visibility_level_helper_spec.rb
index 8f37bf29a4b..4af7fae400e 100644
--- a/spec/helpers/visibility_level_helper_spec.rb
+++ b/spec/helpers/visibility_level_helper_spec.rb
@@ -323,34 +323,4 @@ RSpec.describe VisibilityLevelHelper, feature_category: :system_access do
it { is_expected.to eq(expected) }
end
end
-
- describe '#visibility_level_options' do
- let(:user) { build(:user) }
-
- before do
- allow(helper).to receive(:current_user).and_return(user)
- end
-
- it 'returns the desired mapping' do
- expected_options = [
- {
- level: 0,
- label: 'Private',
- description: 'The group and its projects can only be viewed by members.'
- },
- {
- level: 10,
- label: 'Internal',
- description: 'The group and any internal projects can be viewed by any logged in user except external users.'
- },
- {
- level: 20,
- label: 'Public',
- description: 'The group and any public projects can be viewed without any authentication.'
- }
- ]
-
- expect(helper.visibility_level_options(group)).to eq expected_options
- end
- end
end
diff --git a/spec/helpers/vite_helper_spec.rb b/spec/helpers/vite_helper_spec.rb
deleted file mode 100644
index edb5650ab1a..00000000000
--- a/spec/helpers/vite_helper_spec.rb
+++ /dev/null
@@ -1,59 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe ViteHelper, feature_category: :tooling do
- let(:source) { 'foo.js' }
- let(:vite_source) { 'vite/foo.js' }
- let(:vite_tag) { '<tag src="vite/foo"></tag>' }
- let(:webpack_source) { 'webpack/foo.js' }
- let(:webpack_tag) { '<tag src="webpack/foo"></tag>' }
-
- context 'when vite enabled' do
- before do
- stub_rails_env('development')
- stub_feature_flags(vite: true)
-
- allow(helper).to receive(:vite_javascript_tag).and_return(vite_tag)
- allow(helper).to receive(:vite_asset_path).and_return(vite_source)
- allow(helper).to receive(:vite_stylesheet_tag).and_return(vite_tag)
- allow(helper).to receive(:vite_asset_url).and_return(vite_source)
- allow(helper).to receive(:vite_running).and_return(true)
- end
-
- describe '#universal_javascript_include_tag' do
- it 'returns vite javascript tag' do
- expect(helper.universal_javascript_include_tag(source)).to eq(vite_tag)
- end
- end
-
- describe '#universal_asset_path' do
- it 'returns vite asset path' do
- expect(helper.universal_asset_path(source)).to eq(vite_source)
- end
- end
- end
-
- context 'when vite disabled' do
- before do
- stub_feature_flags(vite: false)
-
- allow(helper).to receive(:javascript_include_tag).and_return(webpack_tag)
- allow(helper).to receive(:asset_path).and_return(webpack_source)
- allow(helper).to receive(:stylesheet_link_tag).and_return(webpack_tag)
- allow(helper).to receive(:path_to_stylesheet).and_return(webpack_source)
- end
-
- describe '#universal_javascript_include_tag' do
- it 'returns webpack javascript tag' do
- expect(helper.universal_javascript_include_tag(source)).to eq(webpack_tag)
- end
- end
-
- describe '#universal_asset_path' do
- it 'returns ActionView asset path' do
- expect(helper.universal_asset_path(source)).to eq(webpack_source)
- end
- end
- end
-end
diff --git a/spec/helpers/wiki_helper_spec.rb b/spec/helpers/wiki_helper_spec.rb
index 6eaa603a43d..31bbd394ac1 100644
--- a/spec/helpers/wiki_helper_spec.rb
+++ b/spec/helpers/wiki_helper_spec.rb
@@ -115,17 +115,6 @@ RSpec.describe WikiHelper, feature_category: :wiki do
end
end
- describe '#wiki_sort_title' do
- it 'returns a title corresponding to a key' do
- expect(helper.wiki_sort_title('created_at')).to eq('Created date')
- expect(helper.wiki_sort_title('title')).to eq('Title')
- end
-
- it 'defaults to Title if a key is unknown' do
- expect(helper.wiki_sort_title('unknown')).to eq('Title')
- end
- end
-
describe '#wiki_page_tracking_context' do
let_it_be(:page) { create(:wiki_page, title: 'path/to/page 💩', content: '💩', format: :markdown) }
diff --git a/spec/initializers/action_cable_subscription_adapter_identifier_spec.rb b/spec/initializers/action_cable_subscription_adapter_identifier_spec.rb
index dd2bf298611..cf82fd751dd 100644
--- a/spec/initializers/action_cable_subscription_adapter_identifier_spec.rb
+++ b/spec/initializers/action_cable_subscription_adapter_identifier_spec.rb
@@ -27,8 +27,7 @@ RSpec.describe 'ActionCableSubscriptionAdapterIdentifier override' do
sub = ActionCable.server.pubsub.send(:redis_connection)
- expect(sub.is_a?(::Gitlab::Redis::MultiStore)).to eq(true)
- expect(sub.secondary_store.connection[:id]).to eq('unix:///home/localuser/redis/redis.socket/0')
+ expect(sub.connection[:id]).to eq('unix:///home/localuser/redis/redis.socket/0')
expect(ActionCable.server.config.cable[:id]).to be_nil
end
end
diff --git a/spec/initializers/active_record_transaction_observer_spec.rb b/spec/initializers/active_record_transaction_observer_spec.rb
index a834037dce5..124bfe2eb48 100644
--- a/spec/initializers/active_record_transaction_observer_spec.rb
+++ b/spec/initializers/active_record_transaction_observer_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'ActiveRecord Transaction Observer', feature_category: :application_performance do
+RSpec.describe 'ActiveRecord Transaction Observer', feature_category: :cloud_connector do
def load_initializer
load Rails.root.join('config/initializers/active_record_transaction_observer.rb')
end
diff --git a/spec/initializers/diagnostic_reports_spec.rb b/spec/initializers/diagnostic_reports_spec.rb
index dc989efe809..ce184ecee29 100644
--- a/spec/initializers/diagnostic_reports_spec.rb
+++ b/spec/initializers/diagnostic_reports_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'diagnostic reports', :aggregate_failures, feature_category: :application_performance do
+RSpec.describe 'diagnostic reports', :aggregate_failures, feature_category: :cloud_connector do
subject(:load_initializer) do
load Rails.root.join('config/initializers/diagnostic_reports.rb')
end
diff --git a/spec/initializers/google_cloud_profiler_spec.rb b/spec/initializers/google_cloud_profiler_spec.rb
index 493d1e0bea5..d9b871fd1f0 100644
--- a/spec/initializers/google_cloud_profiler_spec.rb
+++ b/spec/initializers/google_cloud_profiler_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'google cloud profiler', :aggregate_failures, feature_category: :application_performance do
+RSpec.describe 'google cloud profiler', :aggregate_failures, feature_category: :cloud_connector do
subject(:load_initializer) do
load rails_root_join('config/initializers/google_cloud_profiler.rb')
end
diff --git a/spec/initializers/memory_watchdog_spec.rb b/spec/initializers/memory_watchdog_spec.rb
index ef24da0071b..d1dfb198818 100644
--- a/spec/initializers/memory_watchdog_spec.rb
+++ b/spec/initializers/memory_watchdog_spec.rb
@@ -2,7 +2,7 @@
require 'fast_spec_helper'
-RSpec.describe 'memory watchdog', feature_category: :application_performance do
+RSpec.describe 'memory watchdog', feature_category: :cloud_connector do
shared_examples 'starts configured watchdog' do |configure_monitor_method|
shared_examples 'configures and starts watchdog' do
it "correctly configures and starts watchdog", :aggregate_failures do
diff --git a/spec/lib/api/entities/bulk_imports/entity_failure_spec.rb b/spec/lib/api/entities/bulk_imports/entity_failure_spec.rb
index 0132102b117..217e6c11630 100644
--- a/spec/lib/api/entities/bulk_imports/entity_failure_spec.rb
+++ b/spec/lib/api/entities/bulk_imports/entity_failure_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Entities::BulkImports::EntityFailure do
+RSpec.describe API::Entities::BulkImports::EntityFailure, feature_category: :importers do
let_it_be(:failure) { create(:bulk_import_failure) }
subject { described_class.new(failure).as_json }
@@ -10,11 +10,11 @@ RSpec.describe API::Entities::BulkImports::EntityFailure do
it 'has the correct attributes' do
expect(subject).to include(
:relation,
- :step,
- :exception_class,
:exception_message,
+ :exception_class,
:correlation_id_value,
- :created_at
+ :source_url,
+ :source_title
)
end
diff --git a/spec/lib/api/entities/wiki_page_spec.rb b/spec/lib/api/entities/wiki_page_spec.rb
index a3566293c5c..da75ade997b 100644
--- a/spec/lib/api/entities/wiki_page_spec.rb
+++ b/spec/lib/api/entities/wiki_page_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe API::Entities::WikiPage do
context "with front matter content" do
let(:wiki_page) { create(:wiki_page) }
- let(:content_with_front_matter) { "---\nxxx: abc\n---\nHome Page" }
+ let(:content_with_front_matter) { "---\ntitle: abc\n---\nHome Page" }
before do
wiki_page.update(content: content_with_front_matter) # rubocop:disable Rails/SaveBang
@@ -33,6 +33,10 @@ RSpec.describe API::Entities::WikiPage do
it 'returns the raw wiki page content' do
expect(subject[:content]).to eq content_with_front_matter
end
+
+ it 'return the front matter title' do
+ expect(subject[:front_matter]).to eq({ title: "abc" })
+ end
end
context 'when render_html param is passed' do
diff --git a/spec/lib/api/github/entities_spec.rb b/spec/lib/api/github/entities_spec.rb
deleted file mode 100644
index 63c54b259a2..00000000000
--- a/spec/lib/api/github/entities_spec.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe API::Github::Entities do
- describe API::Github::Entities::User do
- let(:user) { create(:user, username: username) }
- let(:username) { 'name_of_user' }
- let(:gitlab_protocol_and_host) { "#{Gitlab.config.gitlab.protocol}://#{Gitlab.config.gitlab.host}" }
- let(:expected_user_url) { "#{gitlab_protocol_and_host}/#{username}" }
- let(:entity) { described_class.new(user) }
-
- subject { entity.as_json }
-
- specify :aggregate_failures do
- expect(subject[:id]).to eq user.id
- expect(subject[:login]).to eq 'name_of_user'
- expect(subject[:url]).to eq expected_user_url
- expect(subject[:html_url]).to eq expected_user_url
- expect(subject[:avatar_url]).to include('https://www.gravatar.com/avatar')
- end
-
- context 'with avatar' do
- let(:user) { create(:user, :with_avatar, username: username) }
-
- specify do
- expect(subject[:avatar_url]).to include("#{gitlab_protocol_and_host}/uploads/-/system/user/avatar/")
- end
- end
- end
-end
diff --git a/spec/lib/api/helpers/rate_limiter_spec.rb b/spec/lib/api/helpers/rate_limiter_spec.rb
index 531140a32a3..101b1c97184 100644
--- a/spec/lib/api/helpers/rate_limiter_spec.rb
+++ b/spec/lib/api/helpers/rate_limiter_spec.rb
@@ -69,4 +69,12 @@ RSpec.describe API::Helpers::RateLimiter do
end
end
end
+
+ describe '#mark_throttle!' do
+ it 'calls ApplicationRateLimiter#throttle?' do
+ expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(key, scope: scope).and_return(false)
+
+ subject.mark_throttle!(key, scope: scope)
+ end
+ end
end
diff --git a/spec/lib/api/helpers_spec.rb b/spec/lib/api/helpers_spec.rb
index 5d343ec2777..21b3b8e6927 100644
--- a/spec/lib/api/helpers_spec.rb
+++ b/spec/lib/api/helpers_spec.rb
@@ -1327,4 +1327,79 @@ RSpec.describe API::Helpers, feature_category: :shared do
end
end
end
+
+ describe '#authenticate_by_gitlab_shell_or_workhorse_token!' do
+ include GitlabShellHelpers
+ include WorkhorseHelpers
+
+ include_context 'workhorse headers'
+
+ let(:headers) { {} }
+ let(:params) { {} }
+
+ context 'when request from gitlab shell' do
+ let(:valid_secret_token) { 'valid' }
+ let(:invalid_secret_token) { 'invalid' }
+
+ before do
+ allow(helper).to receive_messages(headers: headers)
+ end
+
+ context 'with invalid token' do
+ let(:headers) { gitlab_shell_internal_api_request_header(secret_token: invalid_secret_token) }
+
+ it 'unauthorized' do
+ expect(helper).to receive(:unauthorized!)
+
+ helper.authenticate_by_gitlab_shell_or_workhorse_token!
+ end
+ end
+
+ context 'with valid token' do
+ let(:headers) { gitlab_shell_internal_api_request_header }
+
+ it 'authorized' do
+ expect(helper).not_to receive(:unauthorized!)
+
+ helper.authenticate_by_gitlab_shell_or_workhorse_token!
+ end
+ end
+ end
+
+ context 'when request from gitlab workhorse' do
+ let(:env) { {} }
+ let(:request) { ActionDispatch::Request.new(env) }
+
+ before do
+ allow_any_instance_of(ActionDispatch::Request).to receive(:headers).and_return(headers)
+ allow(helper).to receive(:request).and_return(request)
+ allow(helper).to receive_messages(params: params, headers: headers, env: env)
+ end
+
+ context 'with invalid token' do
+ let(:headers) { { Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => JWT.encode({ 'iss' => 'gitlab-workhorse' }, 'wrongkey', 'HS256') } }
+
+ before do
+ allow(JWT).to receive(:decode).and_return([{ 'iss' => 'gitlab-workhorse' }])
+ end
+
+ it 'unauthorized' do
+ expect(helper).to receive(:forbidden!)
+
+ helper.authenticate_by_gitlab_shell_or_workhorse_token!
+ end
+ end
+
+ context 'with valid token' do
+ let(:headers) { workhorse_headers }
+ let(:env) { { 'HTTP_GITLAB_WORKHORSE' => 1 } }
+
+ it 'authorized' do
+ expect(helper).not_to receive(:forbidden!)
+
+ helper.authenticate_by_gitlab_shell_or_workhorse_token!
+ end
+ end
+ end
+ end
end
diff --git a/spec/lib/atlassian/jira_connect/client_spec.rb b/spec/lib/atlassian/jira_connect/client_spec.rb
index f7597579e7a..a692d76da77 100644
--- a/spec/lib/atlassian/jira_connect/client_spec.rb
+++ b/spec/lib/atlassian/jira_connect/client_spec.rb
@@ -447,38 +447,77 @@ RSpec.describe Atlassian::JiraConnect::Client, feature_category: :integrations d
end
describe '#user_info' do
- let(:account_id) { '12345' }
- let(:response_body) do
- {
- groups: {
- items: [
- { name: 'site-admins' }
- ]
- }
- }.to_json
- end
+ context 'when user is a site administrator' do
+ let(:account_id) { '12345' }
+ let(:response_body) do
+ {
+ groups: {
+ items: [
+ { name: 'site-admins' }
+ ]
+ }
+ }.to_json
+ end
- before do
- stub_full_request("https://gitlab-test.atlassian.net/rest/api/3/user?accountId=#{account_id}&expand=groups")
- .to_return(status: response_status, body: response_body, headers: { 'Content-Type': 'application/json' })
- end
+ before do
+ stub_full_request("https://gitlab-test.atlassian.net/rest/api/3/user?accountId=#{account_id}&expand=groups")
+ .to_return(status: response_status, body: response_body, headers: { 'Content-Type': 'application/json' })
+ end
+
+ context 'with a successful response' do
+ let(:response_status) { 200 }
- context 'with a successful response' do
- let(:response_status) { 200 }
+ it 'returns a JiraUser instance' do
+ jira_user = client.user_info(account_id)
- it 'returns a JiraUser instance' do
- jira_user = client.user_info(account_id)
+ expect(jira_user).to be_a(Atlassian::JiraConnect::JiraUser)
+ expect(jira_user).to be_jira_admin
+ end
+ end
+
+ context 'with a failed response' do
+ let(:response_status) { 401 }
- expect(jira_user).to be_a(Atlassian::JiraConnect::JiraUser)
- expect(jira_user).to be_site_admin
+ it 'returns nil' do
+ expect(client.user_info(account_id)).to be_nil
+ end
end
end
- context 'with a failed response' do
- let(:response_status) { 401 }
+ context 'when user is an organization administrator' do
+ let(:account_id) { '12345' }
+ let(:response_body) do
+ {
+ groups: {
+ items: [
+ { name: 'org-admins' }
+ ]
+ }
+ }.to_json
+ end
+
+ before do
+ stub_full_request("https://gitlab-test.atlassian.net/rest/api/3/user?accountId=#{account_id}&expand=groups")
+ .to_return(status: response_status, body: response_body, headers: { 'Content-Type': 'application/json' })
+ end
+
+ context 'with a successful response' do
+ let(:response_status) { 200 }
+
+ it 'returns a JiraUser instance' do
+ jira_user = client.user_info(account_id)
+
+ expect(jira_user).to be_a(Atlassian::JiraConnect::JiraUser)
+ expect(jira_user).to be_jira_admin
+ end
+ end
+
+ context 'with a failed response' do
+ let(:response_status) { 401 }
- it 'returns nil' do
- expect(client.user_info(account_id)).to be_nil
+ it 'returns nil' do
+ expect(client.user_info(account_id)).to be_nil
+ end
end
end
end
diff --git a/spec/lib/backup/gitaly_backup_spec.rb b/spec/lib/backup/gitaly_backup_spec.rb
index 6c2656b1c48..058c7f12f63 100644
--- a/spec/lib/backup/gitaly_backup_spec.rb
+++ b/spec/lib/backup/gitaly_backup_spec.rb
@@ -166,25 +166,40 @@ RSpec.describe Backup::GitalyBackup, feature_category: :backup_restore do
let_it_be(:personal_snippet) { create(:personal_snippet, author: project.first_owner) }
let_it_be(:project_snippet) { create(:project_snippet, project: project, author: project.first_owner) }
- def copy_fixture_to_backup_path(backup_name, repo_disk_path)
- FileUtils.mkdir_p(File.join(Gitlab.config.backup.path, 'repositories', File.dirname(repo_disk_path)))
+ def create_repo_backup(backup_name, repo)
+ repo_backup_root = File.join(Gitlab.config.backup.path, 'repositories')
+
+ FileUtils.mkdir_p(File.join(repo_backup_root, 'manifests', repo.storage, repo.relative_path))
+ FileUtils.mkdir_p(File.join(repo_backup_root, repo.relative_path))
%w[.bundle .refs].each do |filetype|
FileUtils.cp(
Rails.root.join('spec/fixtures/lib/backup', backup_name + filetype),
- File.join(Gitlab.config.backup.path, 'repositories', repo_disk_path + filetype)
+ File.join(repo_backup_root, repo.relative_path + filetype)
)
end
+
+ manifest = <<-TOML
+ object_format = 'sha1'
+ head_references = 'heads/refs/master'
+
+ [[steps]]
+ bundle_path = '#{repo.relative_path}.bundle'
+ ref_path = '#{repo.relative_path}.refs'
+ custom_hooks_path = '#{repo.relative_path}.custom_hooks.tar'
+ TOML
+
+ File.write(File.join(repo_backup_root, 'manifests', repo.storage, repo.relative_path, backup_id + '.toml'), manifest)
end
it 'restores from repository bundles', :aggregate_failures do
- copy_fixture_to_backup_path('project_repo', project.disk_path)
- copy_fixture_to_backup_path('wiki_repo', project.wiki.disk_path)
- copy_fixture_to_backup_path('design_repo', project.design_repository.disk_path)
- copy_fixture_to_backup_path('personal_snippet_repo', personal_snippet.disk_path)
- copy_fixture_to_backup_path('project_snippet_repo', project_snippet.disk_path)
+ create_repo_backup('project_repo', project.repository.raw)
+ create_repo_backup('wiki_repo', project.wiki.repository)
+ create_repo_backup('design_repo', project.design_repository)
+ create_repo_backup('personal_snippet_repo', personal_snippet.repository)
+ create_repo_backup('project_snippet_repo', project_snippet.repository)
- expect(Open3).to receive(:popen2).with(expected_env, anything, 'restore', '-path', anything, '-layout', 'pointer').and_call_original
+ expect(Open3).to receive(:popen2).with(expected_env, anything, 'restore', '-path', anything, '-layout', 'pointer', '-id', backup_id).and_call_original
subject.start(:restore, destination, backup_id: backup_id)
subject.enqueue(project, Gitlab::GlRepository::PROJECT)
@@ -204,9 +219,9 @@ RSpec.describe Backup::GitalyBackup, feature_category: :backup_restore do
end
it 'clears specified storages when remove_all_repositories is set' do
- expect(Open3).to receive(:popen2).with(expected_env, anything, 'restore', '-path', anything, '-layout', 'pointer', '-remove-all-repositories', 'default').and_call_original
+ expect(Open3).to receive(:popen2).with(expected_env, anything, 'restore', '-path', anything, '-layout', 'pointer', '-remove-all-repositories', 'default', '-id', backup_id).and_call_original
- copy_fixture_to_backup_path('project_repo', project.disk_path)
+ create_repo_backup('project_repo', project.repository.raw)
subject.start(:restore, destination, backup_id: backup_id, remove_all_repositories: %w[default])
subject.enqueue(project, Gitlab::GlRepository::PROJECT)
subject.finish!
@@ -216,7 +231,7 @@ RSpec.describe Backup::GitalyBackup, feature_category: :backup_restore do
let(:max_parallelism) { 3 }
it 'passes parallel option through' do
- expect(Open3).to receive(:popen2).with(expected_env, anything, 'restore', '-path', anything, '-layout', 'pointer', '-parallel', '3').and_call_original
+ expect(Open3).to receive(:popen2).with(expected_env, anything, 'restore', '-path', anything, '-layout', 'pointer', '-parallel', '3', '-id', backup_id).and_call_original
subject.start(:restore, destination, backup_id: backup_id)
subject.finish!
@@ -227,7 +242,7 @@ RSpec.describe Backup::GitalyBackup, feature_category: :backup_restore do
let(:storage_parallelism) { 3 }
it 'passes parallel option through' do
- expect(Open3).to receive(:popen2).with(expected_env, anything, 'restore', '-path', anything, '-layout', 'pointer', '-parallel-storage', '3').and_call_original
+ expect(Open3).to receive(:popen2).with(expected_env, anything, 'restore', '-path', anything, '-layout', 'pointer', '-parallel-storage', '3', '-id', backup_id).and_call_original
subject.start(:restore, destination, backup_id: backup_id)
subject.finish!
diff --git a/spec/lib/banzai/filter/asset_proxy_filter_spec.rb b/spec/lib/banzai/filter/asset_proxy_filter_spec.rb
index baa22e08971..cb696e21e65 100644
--- a/spec/lib/banzai/filter/asset_proxy_filter_spec.rb
+++ b/spec/lib/banzai/filter/asset_proxy_filter_spec.rb
@@ -62,6 +62,8 @@ RSpec.describe Banzai::Filter::AssetProxyFilter, feature_category: :team_plannin
end
context 'when properly configured' do
+ using RSpec::Parameterized::TableSyntax
+
before do
stub_asset_proxy_setting(enabled: true)
stub_asset_proxy_setting(secret_key: 'shared-secret')
@@ -71,84 +73,33 @@ RSpec.describe Banzai::Filter::AssetProxyFilter, feature_category: :team_plannin
@context = described_class.transform_context({})
end
- it 'replaces img src' do
- src = 'http://example.com/test.png'
- new_src = 'https://assets.example.com/08df250eeeef1a8cf2c761475ac74c5065105612/687474703a2f2f6578616d706c652e636f6d2f746573742e706e67'
- doc = filter(image(src), @context)
-
- expect(doc.at_css('img')['src']).to eq new_src
- expect(doc.at_css('img')['data-canonical-src']).to eq src
- end
-
- it 'replaces invalid URLs' do
- src = '///example.com/test.png'
- new_src = 'https://assets.example.com/3368d2c7b9bed775bdd1e811f36a4b80a0dcd8ab/2f2f2f6578616d706c652e636f6d2f746573742e706e67'
- doc = filter(image(src), @context)
-
- expect(doc.at_css('img')['src']).to eq new_src
- expect(doc.at_css('img')['data-canonical-src']).to eq src
- end
-
- it 'replaces by default, even strings that do not look like URLs' do
- src = 'oigjsie8787%$**(#(%0'
- new_src = 'https://assets.example.com/1b893f9a71d66c99437f27e19b9a061a6f5d9391/6f69676a7369653837383725242a2a2823282530'
- doc = filter(image(src), @context)
-
- expect(doc.at_css('img')['src']).to eq new_src
- expect(doc.at_css('img')['data-canonical-src']).to eq src
- end
-
- it 'replaces URL with non-ASCII characters' do
- src = 'https://example.com/x?¬'
- new_src = 'https://assets.example.com/2f29a8c7f13f3ae14dc18c154dbbd657d703e75f/68747470733a2f2f6578616d706c652e636f6d2f783fc2ac'
- doc = filter(image(src), @context)
-
- expect(doc.at_css('img')['src']).to eq new_src
- expect(doc.at_css('img')['data-canonical-src']).to eq src
- end
-
- it 'replaces out-of-spec URL that would still be rendered in the browser' do
- src = 'https://example.com/##'
- new_src = 'https://assets.example.com/d7d0c845cc553d9430804c07e9456545ef3e6fe6/68747470733a2f2f6578616d706c652e636f6d2f2323'
- doc = filter(image(src), @context)
-
- expect(doc.at_css('img')['src']).to eq new_src
- expect(doc.at_css('img')['data-canonical-src']).to eq src
+ where(:data_canonical_src, :src) do
+ 'http://example.com/test.png' | 'https://assets.example.com/08df250eeeef1a8cf2c761475ac74c5065105612/687474703a2f2f6578616d706c652e636f6d2f746573742e706e67'
+ '///example.com/test.png' | 'https://assets.example.com/3368d2c7b9bed775bdd1e811f36a4b80a0dcd8ab/2f2f2f6578616d706c652e636f6d2f746573742e706e67'
+ '//example.com/test.png' | 'https://assets.example.com/a2e9aa56319e31bbd05be72e633f2864ff08becb/2f2f6578616d706c652e636f6d2f746573742e706e67'
+ # If it can't be parsed, default to use asset proxy
+ 'oigjsie8787%$**(#(%0' | 'https://assets.example.com/1b893f9a71d66c99437f27e19b9a061a6f5d9391/6f69676a7369653837383725242a2a2823282530'
+ 'https://example.com/x?¬' | 'https://assets.example.com/2f29a8c7f13f3ae14dc18c154dbbd657d703e75f/68747470733a2f2f6578616d706c652e636f6d2f783fc2ac'
+ # The browser loads this as if it was a valid URL
+ 'http:example.com' | 'https://assets.example.com/bcefecd18484ec2850887d6730273e5e70f5ed1a/687474703a6578616d706c652e636f6d'
+ 'https:example.com' | 'https://assets.example.com/648e074361143780357db0b5cf73d4438d5484d3/68747470733a6578616d706c652e636f6d'
+ 'https://example.com/##' | 'https://assets.example.com/d7d0c845cc553d9430804c07e9456545ef3e6fe6/68747470733a2f2f6578616d706c652e636f6d2f2323'
+ nil | "test.png"
+ nil | "/test.png"
+ nil | "#{Gitlab.config.gitlab.url}/test.png"
+ nil | 'http://gitlab.com/test.png'
+ nil | 'http://gitlab.com/test.png?url=http://example.com/test.png'
+ nil | 'http://images.mydomain.com/test.png'
end
- it 'skips internal images' do
- src = "#{Gitlab.config.gitlab.url}/test.png"
- doc = filter(image(src), @context)
+ with_them do
+ it 'correctly modifies the img tag' do
+ original_url = data_canonical_src || src
+ doc = filter(image(original_url), @context)
- expect(doc.at_css('img')['src']).to eq src
- end
-
- it 'skip relative urls' do
- src = "/test.png"
- doc = filter(image(src), @context)
-
- expect(doc.at_css('img')['src']).to eq src
- end
-
- it 'skips single domain' do
- src = "http://gitlab.com/test.png"
- doc = filter(image(src), @context)
-
- expect(doc.at_css('img')['src']).to eq src
- end
-
- it 'skips single domain and ignores url in query string' do
- src = "http://gitlab.com/test.png?url=http://example.com/test.png"
- doc = filter(image(src), @context)
-
- expect(doc.at_css('img')['src']).to eq src
- end
-
- it 'skips wildcarded domain' do
- src = "http://images.mydomain.com/test.png"
- doc = filter(image(src), @context)
-
- expect(doc.at_css('img')['src']).to eq src
+ expect(doc.at_css('img')['src']).to eq src
+ expect(doc.at_css('img')['data-canonical-src']).to eq data_canonical_src
+ end
end
end
end
diff --git a/spec/lib/banzai/filter/math_filter_spec.rb b/spec/lib/banzai/filter/math_filter_spec.rb
index 3fa0f9028e8..9e9e4110b44 100644
--- a/spec/lib/banzai/filter/math_filter_spec.rb
+++ b/spec/lib/banzai/filter/math_filter_spec.rb
@@ -210,18 +210,31 @@ RSpec.describe Banzai::Filter::MathFilter, feature_category: :team_planning do
context 'when limiting how many elements can be marked as math' do
subject { pipeline_filter('$`2+2`$ + $3+3$ + $$4+4$$') }
- it 'enforces limits by default' do
+ before do
stub_const('Banzai::Filter::MathFilter::RENDER_NODES_LIMIT', 2)
+ end
+ it 'enforces limits by default' do
expect(subject.search('.js-render-math').count).to eq(2)
end
it 'does not limit when math_rendering_limits_enabled is false' do
stub_application_setting(math_rendering_limits_enabled: false)
- stub_const('Banzai::Filter::MathFilter::RENDER_NODES_LIMIT', 2)
expect(subject.search('.js-render-math').count).to eq(3)
end
+
+ it 'does not limit for the wiki' do
+ doc = pipeline_filter('$`2+2`$ + $3+3$ + $$4+4$$', { wiki: true })
+
+ expect(doc.search('.js-render-math').count).to eq(3)
+ end
+
+ it 'does not limit for blobs' do
+ doc = pipeline_filter('$`2+2`$ + $3+3$ + $$4+4$$', { text_source: :blob })
+
+ expect(doc.search('.js-render-math').count).to eq(3)
+ end
end
it 'protects against malicious backtracking' do
@@ -232,14 +245,14 @@ RSpec.describe Banzai::Filter::MathFilter, feature_category: :team_planning do
end.not_to raise_error
end
- def pipeline_filter(text)
- context = { project: nil, no_sourcepos: true }
+ def pipeline_filter(text, context = {})
+ context = { project: nil, no_sourcepos: true }.merge(context)
doc = Banzai::Pipeline::PreProcessPipeline.call(text, {})
doc = Banzai::Pipeline::PlainMarkdownPipeline.call(doc[:output], context)
doc = Banzai::Filter::CodeLanguageFilter.call(doc[:output], context, nil)
doc = Banzai::Filter::SanitizationFilter.call(doc, context, nil)
- filter(doc)
+ filter(doc, context)
end
end
diff --git a/spec/lib/banzai/filter/references/user_reference_filter_spec.rb b/spec/lib/banzai/filter/references/user_reference_filter_spec.rb
index 7a11ff3ac3d..b4f9d1a22cf 100644
--- a/spec/lib/banzai/filter/references/user_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/references/user_reference_filter_spec.rb
@@ -231,16 +231,17 @@ RSpec.describe Banzai::Filter::References::UserReferenceFilter, feature_category
it 'does not have N+1 per multiple user references', :use_sql_query_cache do
markdown = reference.to_s
+ reference_filter(markdown) # warm up
control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
reference_filter(markdown)
- end.count
+ end
markdown = "#{reference} @qwertyuiopzx @wertyuio @ertyu @rtyui #{reference2} #{reference3}"
expect do
reference_filter(markdown)
- end.not_to exceed_all_query_limit(control_count)
+ end.to issue_same_number_of_queries_as(control_count)
end
end
end
diff --git a/spec/lib/banzai/reference_parser/user_parser_spec.rb b/spec/lib/banzai/reference_parser/user_parser_spec.rb
index 179e6e73fa3..096f1d3792b 100644
--- a/spec/lib/banzai/reference_parser/user_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/user_parser_spec.rb
@@ -25,8 +25,14 @@ RSpec.describe Banzai::ReferenceParser::UserParser, feature_category: :team_plan
context 'when group has members' do
let!(:group_member) { create(:group_member, group: group, user: user) }
-
- it 'returns the users of the group' do
+ let!(:user2) { create(:user) }
+ let!(:user3) { create(:user) }
+ let!(:user4) { create(:user) }
+ let!(:group_member2) { create(:group_member, :minimal_access, group: group, user: user2) }
+ let!(:group_member3) { create(:group_member, :access_request, group: group, user: user3) }
+ let!(:group_member4) { create(:group_member, :invited, group: group, user: user4) }
+
+ it 'returns the relevant users of the group with enough access' do
expect(subject.referenced_by([link])).to eq([user])
end
diff --git a/spec/lib/bitbucket/representation/pull_request_comment_spec.rb b/spec/lib/bitbucket/representation/pull_request_comment_spec.rb
index e748cd7b955..6fdd1dfa561 100644
--- a/spec/lib/bitbucket/representation/pull_request_comment_spec.rb
+++ b/spec/lib/bitbucket/representation/pull_request_comment_spec.rb
@@ -2,7 +2,7 @@
require 'fast_spec_helper'
-RSpec.describe Bitbucket::Representation::PullRequestComment do
+RSpec.describe Bitbucket::Representation::PullRequestComment, feature_category: :importers do
describe '#iid' do
it { expect(described_class.new('id' => 1).iid).to eq(1) }
end
@@ -33,4 +33,10 @@ RSpec.describe Bitbucket::Representation::PullRequestComment do
it { expect(described_class.new('parent' => {}).has_parent?).to be_truthy }
it { expect(described_class.new({}).has_parent?).to be_falsey }
end
+
+ describe '#deleted?' do
+ it { expect(described_class.new('deleted' => true).deleted?).to be_truthy }
+ it { expect(described_class.new('deleted' => false).deleted?).to be_falsey }
+ it { expect(described_class.new({}).deleted?).to be_falsey }
+ end
end
diff --git a/spec/lib/bitbucket/representation/repo_spec.rb b/spec/lib/bitbucket/representation/repo_spec.rb
index ba5a3306d07..ac6095dedd1 100644
--- a/spec/lib/bitbucket/representation/repo_spec.rb
+++ b/spec/lib/bitbucket/representation/repo_spec.rb
@@ -2,7 +2,7 @@
require 'fast_spec_helper'
-RSpec.describe Bitbucket::Representation::Repo do
+RSpec.describe Bitbucket::Representation::Repo, feature_category: :importers do
describe '#has_wiki?' do
it { expect(described_class.new({ 'has_wiki' => false }).has_wiki?).to be_falsey }
it { expect(described_class.new({ 'has_wiki' => true }).has_wiki?).to be_truthy }
@@ -42,6 +42,11 @@ RSpec.describe Bitbucket::Representation::Repo do
it { expect(described_class.new({ 'full_name' => 'ben/test' }).slug).to eq('test') }
end
+ describe '#default_branch' do
+ it { expect(described_class.new({ 'mainbranch' => { 'name' => 'master' } }).default_branch).to eq('master') }
+ it { expect(described_class.new({}).default_branch).to eq(nil) }
+ end
+
describe '#clone_url' do
it 'builds url' do
data = { 'links' => { 'clone' => [{ 'name' => 'https', 'href' => 'https://bibucket.org/test/test.git' }] } }
diff --git a/spec/lib/bulk_imports/clients/graphql_spec.rb b/spec/lib/bulk_imports/clients/graphql_spec.rb
index 9bb37a7c438..16f98ed462e 100644
--- a/spec/lib/bulk_imports/clients/graphql_spec.rb
+++ b/spec/lib/bulk_imports/clients/graphql_spec.rb
@@ -8,13 +8,13 @@ RSpec.describe BulkImports::Clients::Graphql, feature_category: :importers do
subject { described_class.new(url: config.url, token: config.access_token) }
describe '#execute' do
- let(:graphql_client_double) { double }
let(:response_double) { double }
describe 'network errors' do
before do
allow(Gitlab::HTTP)
.to receive(:post)
+ .with(an_instance_of(String), a_hash_including(timeout: 60))
.and_return(response_double)
end
diff --git a/spec/lib/bulk_imports/common/pipelines/badges_pipeline_spec.rb b/spec/lib/bulk_imports/common/pipelines/badges_pipeline_spec.rb
index a18d26bedf3..aeb48bed314 100644
--- a/spec/lib/bulk_imports/common/pipelines/badges_pipeline_spec.rb
+++ b/spec/lib/bulk_imports/common/pipelines/badges_pipeline_spec.rb
@@ -39,18 +39,6 @@ RSpec.describe BulkImports::Common::Pipelines::BadgesPipeline do
expect { pipeline.run }.to not_change(Badge, :count)
end
- context 'with FF bulk_import_idempotent_workers disabled' do
- before do
- stub_feature_flags(bulk_import_idempotent_workers: false)
- end
-
- it 'creates duplicated badges' do
- expect { pipeline.run }.to change(Badge, :count).by(2)
-
- expect { pipeline.run }.to change(Badge, :count)
- end
- end
-
context 'when project entity' do
let(:first_page) { extracted_data(has_next_page: true) }
let(:last_page) { extracted_data(name: 'badge2', kind: 'project') }
diff --git a/spec/lib/bulk_imports/common/pipelines/entity_finisher_spec.rb b/spec/lib/bulk_imports/common/pipelines/entity_finisher_spec.rb
index 8ca74565788..b96ea20c676 100644
--- a/spec/lib/bulk_imports/common/pipelines/entity_finisher_spec.rb
+++ b/spec/lib/bulk_imports/common/pipelines/entity_finisher_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe BulkImports::Common::Pipelines::EntityFinisher, feature_category:
context = BulkImports::Pipeline::Context.new(pipeline_tracker)
subject = described_class.new(context)
- expect_next_instance_of(Gitlab::Import::Logger) do |logger|
+ expect_next_instance_of(BulkImports::Logger) do |logger|
expect(logger)
.to receive(:info)
.with(
@@ -19,8 +19,7 @@ RSpec.describe BulkImports::Common::Pipelines::EntityFinisher, feature_category:
source_full_path: entity.source_full_path,
pipeline_class: described_class.name,
message: 'Entity finished',
- source_version: entity.bulk_import.source_version_info.to_s,
- importer: 'gitlab_migration'
+ source_version: entity.bulk_import.source_version_info.to_s
)
end
diff --git a/spec/lib/bulk_imports/pipeline/runner_spec.rb b/spec/lib/bulk_imports/pipeline/runner_spec.rb
index 8d48606633a..4540408990c 100644
--- a/spec/lib/bulk_imports/pipeline/runner_spec.rb
+++ b/spec/lib/bulk_imports/pipeline/runner_spec.rb
@@ -43,7 +43,9 @@ RSpec.describe BulkImports::Pipeline::Runner, feature_category: :importers do
stub_const('BulkImports::MyPipeline', pipeline)
end
- let_it_be_with_reload(:entity) { create(:bulk_import_entity) }
+ let_it_be(:bulk_import) { create(:bulk_import) }
+ let_it_be(:configuration) { create(:bulk_import_configuration, bulk_import: bulk_import) }
+ let_it_be_with_reload(:entity) { create(:bulk_import_entity, bulk_import: bulk_import) }
let(:tracker) { create(:bulk_import_tracker, entity: entity) }
let(:context) { BulkImports::Pipeline::Context.new(tracker, extra: :data) }
@@ -52,7 +54,7 @@ RSpec.describe BulkImports::Pipeline::Runner, feature_category: :importers do
shared_examples 'failed pipeline' do |exception_class, exception_message|
it 'logs import failure' do
- expect_next_instance_of(Gitlab::Import::Logger) do |logger|
+ expect_next_instance_of(BulkImports::Logger) do |logger|
expect(logger).to receive(:error)
.with(
a_hash_including(
@@ -67,7 +69,6 @@ RSpec.describe BulkImports::Pipeline::Runner, feature_category: :importers do
'correlation_id' => anything,
'class' => 'BulkImports::MyPipeline',
'message' => 'An object of a pipeline failed to import',
- 'importer' => 'gitlab_migration',
'exception.backtrace' => anything,
'source_version' => entity.bulk_import.source_version_info.to_s
)
@@ -92,14 +93,13 @@ RSpec.describe BulkImports::Pipeline::Runner, feature_category: :importers do
end
it 'logs a warn message and marks entity and tracker as failed' do
- expect_next_instance_of(Gitlab::Import::Logger) do |logger|
+ expect_next_instance_of(BulkImports::Logger) do |logger|
expect(logger).to receive(:warn)
.with(
log_params(
context,
message: 'Aborting entity migration due to pipeline failure',
- pipeline_class: 'BulkImports::MyPipeline',
- importer: 'gitlab_migration'
+ pipeline_class: 'BulkImports::MyPipeline'
)
)
end
@@ -119,6 +119,56 @@ RSpec.describe BulkImports::Pipeline::Runner, feature_category: :importers do
expect(entity.failed?).to eq(false)
end
end
+
+ context 'when failure happens during loader' do
+ before do
+ allow(tracker).to receive(:pipeline_class).and_return(BulkImports::MyPipeline)
+ allow(BulkImports::MyPipeline).to receive(:relation).and_return(relation)
+
+ allow_next_instance_of(BulkImports::Extractor) do |extractor|
+ allow(extractor).to receive(:extract).with(context).and_return(extracted_data)
+ end
+
+ allow_next_instance_of(BulkImports::Transformer) do |transformer|
+ allow(transformer).to receive(:transform).with(context, extracted_data.data.first).and_return(entry)
+ end
+
+ allow_next_instance_of(BulkImports::Loader) do |loader|
+ allow(loader).to receive(:load).with(context, entry).and_raise(StandardError, 'Error!')
+ end
+ end
+
+ context 'when entry has title' do
+ let(:relation) { 'issues' }
+ let(:entry) { Issue.new(iid: 1, title: 'hello world') }
+
+ it 'creates failure record with source url and title' do
+ subject.run
+
+ failure = entity.failures.first
+ expected_source_url = File.join(configuration.url, 'groups', entity.source_full_path, '-', 'issues', '1')
+
+ expect(failure).to be_present
+ expect(failure.source_url).to eq(expected_source_url)
+ expect(failure.source_title).to eq('hello world')
+ end
+ end
+
+ context 'when entry has name' do
+ let(:relation) { 'boards' }
+ let(:entry) { Board.new(name: 'hello world') }
+
+ it 'creates failure record with name' do
+ subject.run
+
+ failure = entity.failures.first
+
+ expect(failure).to be_present
+ expect(failure.source_url).to be_nil
+ expect(failure.source_title).to eq('hello world')
+ end
+ end
+ end
end
describe 'pipeline runner' do
@@ -144,6 +194,8 @@ RSpec.describe BulkImports::Pipeline::Runner, feature_category: :importers do
.with(context, extracted_data.data.first)
end
+ expect(subject).to receive(:on_finish)
+
expect_next_instance_of(Gitlab::Import::Logger) do |logger|
expect(logger).to receive(:info)
.with(
@@ -185,6 +237,14 @@ RSpec.describe BulkImports::Pipeline::Runner, feature_category: :importers do
log_params(
context,
pipeline_class: 'BulkImports::MyPipeline',
+ pipeline_step: :on_finish
+ )
+ )
+ expect(logger).to receive(:info)
+ .with(
+ log_params(
+ context,
+ pipeline_class: 'BulkImports::MyPipeline',
pipeline_step: :after_run
)
)
@@ -201,6 +261,28 @@ RSpec.describe BulkImports::Pipeline::Runner, feature_category: :importers do
subject.run
end
+ context 'when the pipeline is batched' do
+ let(:tracker) { create(:bulk_import_tracker, :batched, entity: entity) }
+
+ before do
+ allow_next_instance_of(BulkImports::Extractor) do |extractor|
+ allow(extractor).to receive(:extract).and_return(extracted_data)
+ end
+ end
+
+ it 'calls after_run' do
+ expect(subject).to receive(:after_run)
+
+ subject.run
+ end
+
+ it 'does not call on_finish' do
+ expect(subject).not_to receive(:on_finish)
+
+ subject.run
+ end
+ end
+
context 'when extracted data has multiple pages' do
it 'updates tracker information and runs pipeline again' do
first_page = extracted_data(has_next_page: true)
@@ -299,34 +381,6 @@ RSpec.describe BulkImports::Pipeline::Runner, feature_category: :importers do
subject.run
end
-
- context 'with FF bulk_import_idempotent_workers disabled' do
- before do
- stub_feature_flags(bulk_import_idempotent_workers: false)
- end
-
- it 'does not touch the cache' do
- expect_next_instance_of(BulkImports::Extractor) do |extractor|
- expect(extractor)
- .to receive(:extract)
- .with(context)
- .and_return(extracted_data)
- end
-
- expect_next_instance_of(BulkImports::Transformer) do |transformer|
- expect(transformer)
- .to receive(:transform)
- .with(context, extracted_data.data.first)
- .and_return(extracted_data.data.first)
- end
-
- expect_next_instance_of(BulkImports::MyPipeline) do |klass|
- expect(klass).not_to receive(:save_processed_entry)
- end
-
- subject.run
- end
- end
end
context 'when the entry is already processed' do
@@ -356,43 +410,13 @@ RSpec.describe BulkImports::Pipeline::Runner, feature_category: :importers do
subject.run
end
-
- context 'with FF bulk_import_idempotent_workers disabled' do
- before do
- stub_feature_flags(bulk_import_idempotent_workers: false)
- end
-
- it 'calls extractor, transformer, and loader' do
- expect_next_instance_of(BulkImports::Extractor) do |extractor|
- expect(extractor)
- .to receive(:extract)
- .with(context)
- .and_return(extracted_data)
- end
-
- expect_next_instance_of(BulkImports::Transformer) do |transformer|
- expect(transformer)
- .to receive(:transform)
- .with(context, extracted_data.data.first)
- .and_return(extracted_data.data.first)
- end
-
- expect_next_instance_of(BulkImports::Loader) do |loader|
- expect(loader)
- .to receive(:load)
- .with(context, extracted_data.data.first)
- end
-
- subject.run
- end
- end
end
context 'when entity is marked as failed' do
it 'logs and returns without execution' do
entity.fail_op!
- expect_next_instance_of(Gitlab::Import::Logger) do |logger|
+ expect_next_instance_of(BulkImports::Logger) do |logger|
expect(logger).to receive(:warn)
.with(
log_params(
@@ -414,14 +438,17 @@ RSpec.describe BulkImports::Pipeline::Runner, feature_category: :importers do
bulk_import_entity_type: context.entity.source_type,
source_full_path: entity.source_full_path,
source_version: context.entity.bulk_import.source_version_info.to_s,
- importer: 'gitlab_migration',
context_extra: context.extra
}.merge(extra)
end
def extracted_data(has_next_page: false)
BulkImports::Pipeline::ExtractedData.new(
- data: { foo: :bar },
+ data: {
+ 'foo' => 'bar',
+ 'title' => 'hello world',
+ 'iid' => 1
+ },
page_info: {
'has_next_page' => has_next_page,
'next_page' => has_next_page ? 'cursor' : nil
diff --git a/spec/lib/bulk_imports/pipeline_schema_info_spec.rb b/spec/lib/bulk_imports/pipeline_schema_info_spec.rb
new file mode 100644
index 00000000000..45dd92ca26d
--- /dev/null
+++ b/spec/lib/bulk_imports/pipeline_schema_info_spec.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::PipelineSchemaInfo, feature_category: :importers do
+ let(:entity) { build(:bulk_import_entity, :project_entity) }
+ let(:tracker) { build(:bulk_import_tracker, entity: entity, pipeline_name: pipeline_name) }
+
+ let(:pipeline_name) { BulkImports::Common::Pipelines::LabelsPipeline.to_s }
+
+ subject { described_class.new(tracker.pipeline_class, tracker.entity.portable_class) }
+
+ describe '#db_schema' do
+ context 'when pipeline defines a relation name which is an association' do
+ it 'returns the schema name of the table used by the association' do
+ expect(subject.db_schema).to eq(:gitlab_main_cell)
+ end
+ end
+
+ context 'when pipeline does not define a relation name' do
+ let(:pipeline_name) { BulkImports::Common::Pipelines::EntityFinisher.to_s }
+
+ it 'returns nil' do
+ expect(subject.db_schema).to eq(nil)
+ end
+ end
+
+ context 'when pipeline relation name is not an association' do
+ let(:pipeline_name) { BulkImports::Projects::Pipelines::CommitNotesPipeline.to_s }
+
+ it 'returns nil' do
+ expect(subject.db_schema).to eq(nil)
+ end
+ end
+ end
+
+ describe '#db_table' do
+ context 'when pipeline defines a relation name which is an association' do
+ it 'returns the name of the table used by the association' do
+ expect(subject.db_table).to eq('labels')
+ end
+ end
+
+ context 'when pipeline does not define a relation name' do
+ let(:pipeline_name) { BulkImports::Common::Pipelines::EntityFinisher.to_s }
+
+ it 'returns nil' do
+ expect(subject.db_table).to eq(nil)
+ end
+ end
+
+ context 'when pipeline relation name is not an association' do
+ let(:pipeline_name) { BulkImports::Projects::Pipelines::CommitNotesPipeline.to_s }
+
+ it 'returns nil' do
+ expect(subject.db_table).to eq(nil)
+ end
+ end
+ end
+end
diff --git a/spec/lib/bulk_imports/projects/pipelines/container_expiration_policy_pipeline_spec.rb b/spec/lib/bulk_imports/projects/pipelines/container_expiration_policy_pipeline_spec.rb
index 9dac8e45ef9..334c2004b59 100644
--- a/spec/lib/bulk_imports/projects/pipelines/container_expiration_policy_pipeline_spec.rb
+++ b/spec/lib/bulk_imports/projects/pipelines/container_expiration_policy_pipeline_spec.rb
@@ -2,10 +2,10 @@
require 'spec_helper'
-RSpec.describe BulkImports::Projects::Pipelines::ContainerExpirationPolicyPipeline do
+RSpec.describe BulkImports::Projects::Pipelines::ContainerExpirationPolicyPipeline, feature_category: :importers do
let_it_be(:project) { create(:project) }
let_it_be(:entity) { create(:bulk_import_entity, :project_entity, project: project) }
- let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity) }
+ let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity, pipeline_name: described_class) }
let_it_be(:context) { BulkImports::Pipeline::Context.new(tracker) }
let_it_be(:policy) do
diff --git a/spec/lib/bulk_imports/projects/pipelines/external_pull_requests_pipeline_spec.rb b/spec/lib/bulk_imports/projects/pipelines/external_pull_requests_pipeline_spec.rb
index b7197814f9c..f00da47d9f5 100644
--- a/spec/lib/bulk_imports/projects/pipelines/external_pull_requests_pipeline_spec.rb
+++ b/spec/lib/bulk_imports/projects/pipelines/external_pull_requests_pipeline_spec.rb
@@ -2,11 +2,11 @@
require 'spec_helper'
-RSpec.describe BulkImports::Projects::Pipelines::ExternalPullRequestsPipeline do
+RSpec.describe BulkImports::Projects::Pipelines::ExternalPullRequestsPipeline, feature_category: :importers do
let_it_be(:project) { create(:project) }
let_it_be(:bulk_import) { create(:bulk_import) }
let_it_be(:entity) { create(:bulk_import_entity, :project_entity, project: project, bulk_import: bulk_import) }
- let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity) }
+ let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity, pipeline_name: described_class) }
let_it_be(:context) { BulkImports::Pipeline::Context.new(tracker) }
let(:attributes) { {} }
diff --git a/spec/lib/bulk_imports/projects/pipelines/merge_requests_pipeline_spec.rb b/spec/lib/bulk_imports/projects/pipelines/merge_requests_pipeline_spec.rb
index 3fb7e28036e..b9e424f4a7d 100644
--- a/spec/lib/bulk_imports/projects/pipelines/merge_requests_pipeline_spec.rb
+++ b/spec/lib/bulk_imports/projects/pipelines/merge_requests_pipeline_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BulkImports::Projects::Pipelines::MergeRequestsPipeline do
+RSpec.describe BulkImports::Projects::Pipelines::MergeRequestsPipeline, feature_category: :importers do
let_it_be(:user) { create(:user) }
let_it_be(:another_user) { create(:user) }
let_it_be(:group) { create(:group) }
@@ -43,6 +43,7 @@ RSpec.describe BulkImports::Projects::Pipelines::MergeRequestsPipeline do
'base_commit_sha' => 'ae73cb07c9eeaf35924a10f713b364d32b2dd34f',
'head_commit_sha' => 'a97f74ddaa848b707bea65441c903ae4bf5d844d',
'start_commit_sha' => '9eea46b5c72ead701c22f516474b95049c9d9462',
+ 'diff_type' => 1,
'merge_request_diff_commits' => [
{
'sha' => 'COMMIT1',
@@ -99,6 +100,8 @@ RSpec.describe BulkImports::Projects::Pipelines::MergeRequestsPipeline do
allow(project.repository).to receive(:branch_exists?).and_return(false)
allow(project.repository).to receive(:create_branch)
+ allow(::Projects::ImportExport::AfterImportMergeRequestsWorker).to receive(:perform_async)
+
pipeline.run
end
@@ -244,8 +247,10 @@ RSpec.describe BulkImports::Projects::Pipelines::MergeRequestsPipeline do
expect(imported_mr.merge_request_diff).to be_present
end
- it 'has the correct data for merge request latest_merge_request_diff' do
- expect(imported_mr.latest_merge_request_diff_id).to eq(imported_mr.merge_request_diffs.maximum(:id))
+ it 'enqueues AfterImportMergeRequestsWorker worker' do
+ expect(::Projects::ImportExport::AfterImportMergeRequestsWorker)
+ .to have_received(:perform_async)
+ .with(project.id)
end
it 'imports diff files' do
diff --git a/spec/lib/bulk_imports/projects/pipelines/releases_pipeline_spec.rb b/spec/lib/bulk_imports/projects/pipelines/releases_pipeline_spec.rb
index 9e0b5af6bfe..fa85e24189c 100644
--- a/spec/lib/bulk_imports/projects/pipelines/releases_pipeline_spec.rb
+++ b/spec/lib/bulk_imports/projects/pipelines/releases_pipeline_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BulkImports::Projects::Pipelines::ReleasesPipeline do
+RSpec.describe BulkImports::Projects::Pipelines::ReleasesPipeline, feature_category: :importers do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
diff --git a/spec/lib/bulk_imports/source_url_builder_spec.rb b/spec/lib/bulk_imports/source_url_builder_spec.rb
new file mode 100644
index 00000000000..2c0e042314b
--- /dev/null
+++ b/spec/lib/bulk_imports/source_url_builder_spec.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::SourceUrlBuilder, feature_category: :importers do
+ let_it_be(:bulk_import) { create(:bulk_import) }
+ let_it_be(:configuration) { create(:bulk_import_configuration, bulk_import: bulk_import) }
+
+ let(:entity) { create(:bulk_import_entity, bulk_import: bulk_import) }
+ let(:tracker) { create(:bulk_import_tracker, entity: entity) }
+ let(:context) { BulkImports::Pipeline::Context.new(tracker) }
+ let(:entry) { Issue.new(iid: 1, title: 'hello world') }
+
+ describe '#url' do
+ subject { described_class.new(context, entry) }
+
+ before do
+ allow(subject).to receive(:relation).and_return('issues')
+ end
+
+ context 'when relation is allowed' do
+ context 'when entity is a group' do
+ it 'returns the url specific to groups' do
+ expected_url = File.join(
+ configuration.url,
+ 'groups',
+ entity.source_full_path,
+ '-',
+ 'issues',
+ '1'
+ )
+
+ expect(subject.url).to eq(expected_url)
+ end
+ end
+
+ context 'when entity is a project' do
+ let(:entity) { create(:bulk_import_entity, :project_entity, bulk_import: bulk_import) }
+
+ it 'returns the url' do
+ expected_url = File.join(
+ configuration.url,
+ entity.source_full_path,
+ '-',
+ 'issues',
+ '1'
+ )
+
+ expect(subject.url).to eq(expected_url)
+ end
+ end
+ end
+
+ context 'when entry is not an ApplicationRecord' do
+ let(:entry) { 'not an ApplicationRecord' }
+
+ it 'returns nil' do
+ expect(subject.url).to be_nil
+ end
+ end
+
+ context 'when relation is not allowed' do
+ it 'returns nil' do
+ allow(subject).to receive(:relation).and_return('not_allowed')
+
+ expect(subject.url).to be_nil
+ end
+ end
+
+ context 'when entry has no iid' do
+ let(:entry) { Issue.new }
+
+ it 'returns nil' do
+ expect(subject.url).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/lib/click_house/models/audit_event_spec.rb b/spec/lib/click_house/models/audit_event_spec.rb
new file mode 100644
index 00000000000..ea3f1a6cbd4
--- /dev/null
+++ b/spec/lib/click_house/models/audit_event_spec.rb
@@ -0,0 +1,132 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ClickHouse::Models::AuditEvent, feature_category: :audit_events do
+ let(:instance) { described_class.new }
+
+ describe '#by_entity_type' do
+ it 'builds the correct SQL' do
+ expected_sql = <<~SQL
+ SELECT * FROM "audit_events" WHERE "audit_events"."entity_type" = 'Project'
+ SQL
+
+ result_sql = instance.by_entity_type("Project").to_sql
+
+ expect(result_sql.strip).to eq(expected_sql.strip)
+ end
+ end
+
+ describe '#by_entity_id' do
+ it 'builds the correct SQL' do
+ expected_sql = <<~SQL
+ SELECT * FROM "audit_events" WHERE "audit_events"."entity_id" = 42
+ SQL
+
+ result_sql = instance.by_entity_id(42).to_sql
+
+ expect(result_sql.strip).to eq(expected_sql.strip)
+ end
+ end
+
+ describe '#by_author_id' do
+ it 'builds the correct SQL' do
+ expected_sql = <<~SQL
+ SELECT * FROM "audit_events" WHERE "audit_events"."author_id" = 5
+ SQL
+
+ result_sql = instance.by_author_id(5).to_sql
+
+ expect(result_sql.strip).to eq(expected_sql.strip)
+ end
+ end
+
+ describe '#by_entity_username' do
+ let_it_be(:user) { create(:user, username: 'Dummy') }
+
+ it 'builds the correct SQL' do
+ expected_sql = <<~SQL
+ SELECT * FROM "audit_events" WHERE "audit_events"."entity_id" = #{user.id}
+ SQL
+
+ result_sql = instance.by_entity_username('Dummy').to_sql
+
+ expect(result_sql.strip).to eq(expected_sql.strip)
+ end
+ end
+
+ describe '#by_author_username' do
+ let_it_be(:user) { create(:user, username: 'Dummy') }
+
+ it 'builds the correct SQL' do
+ expected_sql = <<~SQL
+ SELECT * FROM "audit_events" WHERE "audit_events"."author_id" = #{user.id}
+ SQL
+
+ result_sql = instance.by_author_username('Dummy').to_sql
+
+ expect(result_sql.strip).to eq(expected_sql.strip)
+ end
+ end
+
+ describe 'class methods' do
+ before do
+ allow(described_class).to receive(:new).and_return(instance)
+ end
+
+ describe '.by_entity_type' do
+ it 'calls the corresponding instance method' do
+ expect(instance).to receive(:by_entity_type).with("Project")
+
+ described_class.by_entity_type("Project")
+ end
+ end
+
+ describe '.by_entity_id' do
+ it 'calls the corresponding instance method' do
+ expect(instance).to receive(:by_entity_id).with(42)
+
+ described_class.by_entity_id(42)
+ end
+ end
+
+ describe '.by_author_id' do
+ it 'calls the corresponding instance method' do
+ expect(instance).to receive(:by_author_id).with(5)
+
+ described_class.by_author_id(5)
+ end
+ end
+
+ describe '.by_entity_username' do
+ it 'calls the corresponding instance method' do
+ expect(instance).to receive(:by_entity_username).with('Dummy')
+
+ described_class.by_entity_username('Dummy')
+ end
+ end
+
+ describe '.by_author_username' do
+ it 'calls the corresponding instance method' do
+ expect(instance).to receive(:by_author_username).with('Dummy')
+
+ described_class.by_author_username('Dummy')
+ end
+ end
+ end
+
+ describe 'method chaining' do
+ it 'builds the correct SQL with chained methods' do
+ expected_sql = <<~SQL.lines(chomp: true).join(' ')
+ SELECT * FROM "audit_events"
+ WHERE "audit_events"."entity_type" = 'Project'
+ AND "audit_events"."author_id" = 1
+ SQL
+
+ instance = described_class.new
+ result_sql = instance.by_entity_type("Project").by_author_id(1).to_sql
+
+ expect(result_sql.strip).to eq(expected_sql.strip)
+ end
+ end
+end
diff --git a/spec/lib/click_house/models/base_model_spec.rb b/spec/lib/click_house/models/base_model_spec.rb
new file mode 100644
index 00000000000..376300d7781
--- /dev/null
+++ b/spec/lib/click_house/models/base_model_spec.rb
@@ -0,0 +1,117 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ClickHouse::Models::BaseModel, feature_category: :database do
+ let(:table_name) { "dummy_table" }
+ let(:query_builder) { instance_double("ClickHouse::QueryBuilder") }
+ let(:updated_query_builder) { instance_double("ClickHouse::QueryBuilder") }
+
+ let(:dummy_class) do
+ Class.new(described_class) do
+ def self.table_name
+ "dummy_table"
+ end
+ end
+ end
+
+ describe '#to_sql' do
+ it 'delegates to the query builder' do
+ expect(query_builder).to receive(:to_sql).and_return("SELECT * FROM dummy_table")
+
+ dummy_instance = dummy_class.new(query_builder)
+
+ expect(dummy_instance.to_sql).to eq("SELECT * FROM dummy_table")
+ end
+ end
+
+ describe '#where' do
+ it 'returns a new instance with refined query' do
+ dummy_instance = dummy_class.new(query_builder)
+
+ expect(query_builder).to receive(:where).with({ foo: "bar" }).and_return(updated_query_builder)
+
+ new_instance = dummy_instance.where(foo: "bar")
+
+ expect(new_instance).to be_a(dummy_class)
+ expect(new_instance).not_to eq(dummy_instance)
+ end
+ end
+
+ describe '#order' do
+ it 'returns a new instance with an order clause' do
+ dummy_instance = dummy_class.new(query_builder)
+
+ expect(query_builder).to receive(:order).with(:created_at, :asc).and_return(updated_query_builder)
+
+ new_instance = dummy_instance.order(:created_at)
+
+ expect(new_instance).to be_a(dummy_class)
+ expect(new_instance).not_to eq(dummy_instance)
+ end
+
+ context "when direction is also passed" do
+ it 'returns a new instance with an order clause' do
+ dummy_instance = dummy_class.new(query_builder)
+
+ expect(query_builder).to receive(:order).with(:created_at, :desc).and_return(updated_query_builder)
+
+ new_instance = dummy_instance.order(:created_at, :desc)
+
+ expect(new_instance).to be_a(dummy_class)
+ expect(new_instance).not_to eq(dummy_instance)
+ end
+ end
+ end
+
+ describe '#limit' do
+ it 'returns a new instance with a limit clause' do
+ dummy_instance = dummy_class.new(query_builder)
+
+ expect(query_builder).to receive(:limit).with(10).and_return(updated_query_builder)
+
+ new_instance = dummy_instance.limit(10)
+
+ expect(new_instance).to be_a(dummy_class)
+ expect(new_instance).not_to eq(dummy_instance)
+ end
+ end
+
+ describe '#offset' do
+ it 'returns a new instance with an offset clause' do
+ dummy_instance = dummy_class.new(query_builder)
+
+ expect(query_builder).to receive(:offset).with(5).and_return(updated_query_builder)
+
+ new_instance = dummy_instance.offset(5)
+
+ expect(new_instance).to be_a(dummy_class)
+ expect(new_instance).not_to eq(dummy_instance)
+ end
+ end
+
+ describe '#select' do
+ it 'returns a new instance with selected fields' do
+ dummy_instance = dummy_class.new(query_builder)
+
+ expect(query_builder).to receive(:select).with(:id, :name).and_return(updated_query_builder)
+
+ new_instance = dummy_instance.select(:id, :name)
+
+ expect(new_instance).to be_a(dummy_class)
+ expect(new_instance).not_to eq(dummy_instance)
+ end
+ end
+
+ describe '.table_name' do
+ it 'raises a NotImplementedError for the base model' do
+ expect do
+ described_class.table_name
+ end.to raise_error(NotImplementedError, "Subclasses must define a `table_name` class method")
+ end
+
+ it 'does not raise an error for the subclass' do
+ expect(dummy_class.table_name).to eq(table_name)
+ end
+ end
+end
diff --git a/spec/lib/container_registry/client_spec.rb b/spec/lib/container_registry/client_spec.rb
index 39409cf8d3a..37161119744 100644
--- a/spec/lib/container_registry/client_spec.rb
+++ b/spec/lib/container_registry/client_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ContainerRegistry::Client do
+RSpec.describe ContainerRegistry::Client, feature_category: :container_registry do
using RSpec::Parameterized::TableSyntax
include_context 'container registry client'
@@ -307,12 +307,12 @@ RSpec.describe ContainerRegistry::Client do
end
end
- describe '#delete_repository_tag_by_name' do
- subject { client.delete_repository_tag_by_name('group/test', 'a') }
+ describe '#delete_repository_tag_by_digest' do
+ subject { client.delete_repository_tag_by_digest('group/test', 'a') }
context 'when the tag exists' do
before do
- stub_request(:delete, "http://container-registry/v2/group/test/tags/reference/a")
+ stub_request(:delete, "http://container-registry/v2/group/test/manifests/a")
.with(headers: headers_with_accept_types)
.to_return(status: 200, body: "")
end
@@ -322,7 +322,7 @@ RSpec.describe ContainerRegistry::Client do
context 'when the tag does not exist' do
before do
- stub_request(:delete, "http://container-registry/v2/group/test/tags/reference/a")
+ stub_request(:delete, "http://container-registry/v2/group/test/manifests/a")
.with(headers: headers_with_accept_types)
.to_return(status: 404, body: "")
end
@@ -332,7 +332,7 @@ RSpec.describe ContainerRegistry::Client do
context 'when an error occurs' do
before do
- stub_request(:delete, "http://container-registry/v2/group/test/tags/reference/a")
+ stub_request(:delete, "http://container-registry/v2/group/test/manifests/a")
.with(headers: headers_with_accept_types)
.to_return(status: 500, body: "")
end
@@ -485,7 +485,7 @@ RSpec.describe ContainerRegistry::Client do
def stub_registry_tags_support(supported = true)
status_code = supported ? 200 : 404
- stub_request(:options, "#{registry_api_url}/v2/name/tags/reference/tag")
+ stub_request(:options, "#{registry_api_url}/v2/name/manifests/tag")
.to_return(
status: status_code,
body: '',
diff --git a/spec/lib/container_registry/gitlab_api_client_spec.rb b/spec/lib/container_registry/gitlab_api_client_spec.rb
index 86675ba27f6..3c87af3a1c8 100644
--- a/spec/lib/container_registry/gitlab_api_client_spec.rb
+++ b/spec/lib/container_registry/gitlab_api_client_spec.rb
@@ -220,6 +220,7 @@ RSpec.describe ContainerRegistry::GitlabApiClient, feature_category: :container_
{
name: '0.1.0',
digest: 'sha256:1234567890',
+ config_digest: 'sha256:13828381121',
media_type: 'application/vnd.oci.image.manifest.v1+json',
size_bytes: 1234567890,
created_at: 5.minutes.ago
@@ -227,6 +228,7 @@ RSpec.describe ContainerRegistry::GitlabApiClient, feature_category: :container_
{
name: 'latest',
digest: 'sha256:1234567892',
+ config_digest: 'sha256:33139438113',
media_type: 'application/vnd.oci.image.manifest.v1+json',
size_bytes: 1234567892,
created_at: 10.minutes.ago
diff --git a/spec/lib/container_registry/tag_spec.rb b/spec/lib/container_registry/tag_spec.rb
index cb5c6a60e1d..8f9308f2127 100644
--- a/spec/lib/container_registry/tag_spec.rb
+++ b/spec/lib/container_registry/tag_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ContainerRegistry::Tag do
+RSpec.describe ContainerRegistry::Tag, feature_category: :container_registry do
let(:group) { create(:group, name: 'group') }
let(:project) { create(:project, path: 'test', group: group) }
@@ -74,6 +74,77 @@ RSpec.describe ContainerRegistry::Tag do
end
end
+ describe '#total_size' do
+ context 'when total_size is set' do
+ before do
+ tag.total_size = 1000
+ end
+
+ it 'returns the set size' do
+ expect(tag.total_size).to eq(1000)
+ end
+ end
+ end
+
+ describe '#revision' do
+ context 'when revision is set' do
+ before do
+ tag.revision = 'xyz789'
+ end
+
+ it 'returns the set revision' do
+ expect(tag.revision).to eq('xyz789')
+ end
+ end
+
+ context 'when revision is not set' do
+ context 'when config_blob is not nil' do
+ let(:blob) { ContainerRegistry::Blob.new(repository, {}) }
+
+ before do
+ allow(tag).to receive(:config_blob).and_return(blob)
+ allow(blob).to receive(:revision).and_return('abc123')
+ end
+
+ it 'returns the revision from config_blob' do
+ expect(tag.revision).to eq('abc123')
+ end
+ end
+
+ context 'when config_blob is nil' do
+ before do
+ allow(tag).to receive(:config_blob).and_return(nil)
+ end
+
+ it 'returns nil' do
+ expect(tag.revision).to be_nil
+ end
+ end
+ end
+ end
+
+ describe '#short_revision' do
+ context 'when revision is not nil' do
+ before do
+ allow(tag).to receive(:revision).and_return('abcdef1234567890')
+ end
+
+ it 'returns the first 9 characters of the revision' do
+ expect(tag.short_revision).to eq('abcdef123')
+ end
+ end
+
+ context 'when revision is nil' do
+ before do
+ allow(tag).to receive(:revision).and_return(nil)
+ end
+
+ it 'returns nil' do
+ expect(tag.short_revision).to be_nil
+ end
+ end
+ end
+
context 'schema v1' do
before do
stub_request(:get, 'http://registry.gitlab/v2/group/test/manifests/tag')
@@ -277,6 +348,16 @@ RSpec.describe ContainerRegistry::Tag do
end
describe '#digest' do
+ context 'when manifest_digest is set' do
+ before do
+ tag.manifest_digest = 'sha256:manifestdigest'
+ end
+
+ it 'returns the set manifest_digest' do
+ expect(tag.digest).to eq('sha256:manifestdigest')
+ end
+ end
+
it 'returns a correct tag digest' do
expect(tag.digest).to eq 'sha256:digest'
end
diff --git a/spec/lib/generators/batched_background_migration/batched_background_migration_generator_spec.rb b/spec/lib/generators/batched_background_migration/batched_background_migration_generator_spec.rb
index 2d48b83be4c..893cf976074 100644
--- a/spec/lib/generators/batched_background_migration/batched_background_migration_generator_spec.rb
+++ b/spec/lib/generators/batched_background_migration/batched_background_migration_generator_spec.rb
@@ -14,6 +14,7 @@ RSpec.describe BatchedBackgroundMigration::BatchedBackgroundMigrationGenerator,
before do
prepare_destination
+ allow(Gitlab).to receive(:current_milestone).and_return('16.6')
end
after do
diff --git a/spec/lib/generators/batched_background_migration/expected_files/queue_my_batched_migration.txt b/spec/lib/generators/batched_background_migration/expected_files/queue_my_batched_migration.txt
index aa79062422b..36f7885b591 100644
--- a/spec/lib/generators/batched_background_migration/expected_files/queue_my_batched_migration.txt
+++ b/spec/lib/generators/batched_background_migration/expected_files/queue_my_batched_migration.txt
@@ -5,7 +5,9 @@
# Update below commented lines with appropriate values.
-class QueueMyBatchedMigration < Gitlab::Database::Migration[2.1]
+class QueueMyBatchedMigration < Gitlab::Database::Migration[2.2]
+ milestone '16.6'
+
MIGRATION = "MyBatchedMigration"
# DELAY_INTERVAL = 2.minutes
# BATCH_SIZE = 1000
diff --git a/spec/lib/generators/gitlab/snowplow_event_definition_generator_spec.rb b/spec/lib/generators/gitlab/snowplow_event_definition_generator_spec.rb
deleted file mode 100644
index 740cfa767e4..00000000000
--- a/spec/lib/generators/gitlab/snowplow_event_definition_generator_spec.rb
+++ /dev/null
@@ -1,95 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::SnowplowEventDefinitionGenerator, :silence_stdout, feature_category: :product_analytics_data_management do
- let(:ce_temp_dir) { Dir.mktmpdir }
- let(:ee_temp_dir) { Dir.mktmpdir }
- let(:timestamp) { Time.now.utc.strftime('%Y%m%d%H%M%S') }
-
- let(:generator_options) do
- { 'category' => 'Projects::Pipelines::EmailCampaignsController', 'action' => 'click' }
- end
-
- before do
- stub_const("#{described_class}::CE_DIR", ce_temp_dir)
- stub_const("#{described_class}::EE_DIR", ee_temp_dir)
- end
-
- around do |example|
- freeze_time { example.run }
- end
-
- after do
- FileUtils.rm_rf([ce_temp_dir, ee_temp_dir])
- end
-
- describe 'Creating event definition file' do
- before do
- stub_const('Gitlab::VERSION', '13.11.0-pre')
- end
-
- let(:sample_event_dir) { 'lib/generators/gitlab/snowplow_event_definition_generator' }
- let(:file_name) { Dir.children(ce_temp_dir).first }
-
- it 'creates CE event definition file using the template' do
- sample_event = ::Gitlab::Config::Loader::Yaml
- .new(fixture_file(File.join(sample_event_dir, 'sample_event.yml'))).load_raw!
-
- described_class.new([], generator_options).invoke_all
-
- event_definition_path = File.join(ce_temp_dir, file_name)
- expect(::Gitlab::Config::Loader::Yaml.new(File.read(event_definition_path)).load_raw!).to eq(sample_event)
- end
-
- describe 'generated filename' do
- it 'includes timestamp' do
- described_class.new([], generator_options).invoke_all
-
- expect(file_name).to include(timestamp.to_s)
- end
-
- it 'removes special characters' do
- generator_options = { 'category' => '"`ui:[mavenpackages | t5%348()-=@ ]`"', 'action' => 'click' }
-
- described_class.new([], generator_options).invoke_all
-
- expect(file_name).to include('uimavenpackagest')
- end
-
- it 'cuts name if longer than 100 characters' do
- generator_options = { 'category' => 'a' * 100, 'action' => 'click' }
-
- described_class.new([], generator_options).invoke_all
-
- expect(file_name.length).to eq(100)
- end
- end
-
- context 'when event definition with same file name already exists' do
- before do
- stub_const('Gitlab::VERSION', '12.11.0-pre')
- described_class.new([], generator_options).invoke_all
- end
-
- it 'raises error' do
- expect { described_class.new([], generator_options.merge('force' => false)).invoke_all }
- .to raise_error(StandardError, /Event definition already exists at/)
- end
- end
-
- describe 'EE' do
- let(:file_name) { Dir.children(ee_temp_dir).first }
-
- it 'creates EE event definition file using the template' do
- sample_event = ::Gitlab::Config::Loader::Yaml
- .new(fixture_file(File.join(sample_event_dir, 'sample_event_ee.yml'))).load_raw!
-
- described_class.new([], generator_options.merge('ee' => true)).invoke_all
-
- event_definition_path = File.join(ee_temp_dir, file_name)
- expect(::Gitlab::Config::Loader::Yaml.new(File.read(event_definition_path)).load_raw!).to eq(sample_event)
- end
- end
- end
-end
diff --git a/spec/lib/generators/gitlab/usage_metric_definition/redis_hll_generator_spec.rb b/spec/lib/generators/gitlab/usage_metric_definition/redis_hll_generator_spec.rb
index b6e1d59f6c0..5265b608ab4 100644
--- a/spec/lib/generators/gitlab/usage_metric_definition/redis_hll_generator_spec.rb
+++ b/spec/lib/generators/gitlab/usage_metric_definition/redis_hll_generator_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::UsageMetricDefinition::RedisHllGenerator, :silence_stdout do
+RSpec.describe Gitlab::UsageMetricDefinition::RedisHllGenerator, :silence_stdout, feature_category: :service_ping do
include UsageDataHelpers
let(:category) { 'test_category' }
@@ -16,6 +16,10 @@ RSpec.describe Gitlab::UsageMetricDefinition::RedisHllGenerator, :silence_stdout
stub_const("#{Gitlab::UsageMetricDefinitionGenerator}::TOP_LEVEL_DIR", temp_dir)
# Stub Prometheus requests from Gitlab::Utils::UsageData
stub_prometheus_queries
+
+ allow_next_instance_of(Gitlab::UsageMetricDefinitionGenerator) do |instance|
+ allow(instance).to receive(:ask).and_return('y') # confirm deprecation warning
+ end
end
after do
diff --git a/spec/lib/generators/gitlab/usage_metric_definition_generator_spec.rb b/spec/lib/generators/gitlab/usage_metric_definition_generator_spec.rb
index f7a4bac39d7..e0cb74d8559 100644
--- a/spec/lib/generators/gitlab/usage_metric_definition_generator_spec.rb
+++ b/spec/lib/generators/gitlab/usage_metric_definition_generator_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::UsageMetricDefinitionGenerator, :silence_stdout do
+RSpec.describe Gitlab::UsageMetricDefinitionGenerator, :silence_stdout, feature_category: :service_ping do
include UsageDataHelpers
let(:key_path) { 'counts_weekly.test_metric' }
@@ -14,6 +14,10 @@ RSpec.describe Gitlab::UsageMetricDefinitionGenerator, :silence_stdout do
stub_const("#{described_class}::TOP_LEVEL_DIR", temp_dir)
# Stub Prometheus requests from Gitlab::Utils::UsageData
stub_prometheus_queries
+
+ allow_next_instance_of(described_class) do |instance|
+ allow(instance).to receive(:ask).and_return('y') # confirm deprecation warning
+ end
end
after do
@@ -100,4 +104,19 @@ RSpec.describe Gitlab::UsageMetricDefinitionGenerator, :silence_stdout do
expect(files.count).to eq(2)
end
end
+
+ ['n', 'N', 'random word', nil].each do |answer|
+ context "when user agreed with deprecation warning by typing: #{answer}" do
+ it 'does not create definition file' do
+ allow_next_instance_of(described_class) do |instance|
+ allow(instance).to receive(:ask).and_return(answer)
+ end
+
+ described_class.new([key_path], { 'dir' => dir, 'class_name' => class_name }).invoke_all
+ files = Dir.glob(File.join(temp_dir, 'metrics/counts_7d/*_metric.yml'))
+
+ expect(files.count).to eq(0)
+ end
+ end
+ end
end
diff --git a/spec/lib/generators/model/mocks/migration_file.txt b/spec/lib/generators/model/mocks/migration_file.txt
index c9e51e51863..d0a5c71ffc3 100644
--- a/spec/lib/generators/model/mocks/migration_file.txt
+++ b/spec/lib/generators/model/mocks/migration_file.txt
@@ -3,7 +3,7 @@
# See https://docs.gitlab.com/ee/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
-class CreateModelGeneratorTestFoos < Gitlab::Database::Migration[2.1]
+class CreateModelGeneratorTestFoos < Gitlab::Database::Migration[2.2]
# When using the methods "add_concurrent_index" or "remove_concurrent_index"
# you must disable the use of transactions
# as these methods can not run in an existing transaction.
@@ -16,6 +16,7 @@ class CreateModelGeneratorTestFoos < Gitlab::Database::Migration[2.1]
# To disable transactions uncomment the following line and remove these
# comments:
# disable_ddl_transaction!
+ milestone '16.5'
# Add dependent 'batched_background_migrations.queued_migration_version' values.
# DEPENDENT_BATCHED_BACKGROUND_MIGRATIONS = []
diff --git a/spec/lib/generators/model/model_generator_spec.rb b/spec/lib/generators/model/model_generator_spec.rb
index 0e770190d25..7284fc8b28a 100644
--- a/spec/lib/generators/model/model_generator_spec.rb
+++ b/spec/lib/generators/model/model_generator_spec.rb
@@ -17,6 +17,10 @@ RSpec.describe Model::ModelGenerator do
FileUtils.rm_rf(temp_dir)
end
+ before do
+ allow(Gitlab).to receive(:current_milestone).and_return('16.5')
+ end
+
it 'creates the model file with the right content' do
subject.invoke_all
diff --git a/spec/lib/gitlab/alert_management/payload/base_spec.rb b/spec/lib/gitlab/alert_management/payload/base_spec.rb
index 3e8d71ac673..bfde0a69f98 100644
--- a/spec/lib/gitlab/alert_management/payload/base_spec.rb
+++ b/spec/lib/gitlab/alert_management/payload/base_spec.rb
@@ -32,7 +32,7 @@ RSpec.describe Gitlab::AlertManagement::Payload::Base do
context 'with multiple paths provided' do
let(:payload_class) do
Class.new(described_class) do
- attribute :test, paths: [['test'], %w(alt test)]
+ attribute :test, paths: [['test'], %w[alt test]]
end
end
@@ -204,8 +204,8 @@ RSpec.describe Gitlab::AlertManagement::Payload::Base do
end
context 'with too-long hosts array' do
- let(:hosts) { %w(abc def ghij) }
- let(:shortened_hosts) { %w(abc def ghi) }
+ let(:hosts) { %w[abc def ghij] }
+ let(:shortened_hosts) { %w[abc def ghi] }
before do
stub_const('::AlertManagement::Alert::HOSTS_MAX_LENGTH', 9)
@@ -215,15 +215,15 @@ RSpec.describe Gitlab::AlertManagement::Payload::Base do
it { is_expected.to eq(hosts: shortened_hosts, project_id: project.id) }
context 'with host cut off between elements' do
- let(:hosts) { %w(abcde fghij) }
- let(:shortened_hosts) { %w(abcde fghi) }
+ let(:hosts) { %w[abcde fghij] }
+ let(:shortened_hosts) { %w[abcde fghi] }
it { is_expected.to eq({ hosts: shortened_hosts, project_id: project.id }) }
end
context 'with nested hosts' do
let(:hosts) { ['abc', ['de', 'f'], 'g', 'hij'] } # rubocop:disable Style/WordArray
- let(:shortened_hosts) { %w(abc de f g hi) }
+ let(:shortened_hosts) { %w[abc de f g hi] }
it { is_expected.to eq({ hosts: shortened_hosts, project_id: project.id }) }
end
diff --git a/spec/lib/gitlab/analytics/cycle_analytics/average_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/average_spec.rb
index 261d587506f..b2a267d42ec 100644
--- a/spec/lib/gitlab/analytics/cycle_analytics/average_spec.rb
+++ b/spec/lib/gitlab/analytics/cycle_analytics/average_spec.rb
@@ -4,7 +4,6 @@ require 'spec_helper'
RSpec.describe Gitlab::Analytics::CycleAnalytics::Average, feature_category: :value_stream_management do
let_it_be(:project) { create(:project) }
-
let_it_be(:issue_1) do
# Duration: 10 days
create(:issue, project: project, created_at: 20.days.ago).tap do |issue|
@@ -30,8 +29,12 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::Average, feature_category: :va
let(:query) { Issue.joins(:metrics).in_projects(project.id) }
- around do |example|
- freeze_time { example.run }
+ before_all do
+ freeze_time
+ end
+
+ after :all do
+ unfreeze_time
end
subject(:average) { described_class.new(stage: stage, query: query) }
@@ -45,8 +48,7 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::Average, feature_category: :va
it { is_expected.to eq(nil) }
end
- context 'returns the average duration in seconds',
- quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/413223' do
+ context 'returns the average duration in seconds' do
it { is_expected.to be_within(0.5).of(7.5.days.to_f) }
end
end
diff --git a/spec/lib/gitlab/asset_proxy_spec.rb b/spec/lib/gitlab/asset_proxy_spec.rb
index 7d7952d5741..af8721739a0 100644
--- a/spec/lib/gitlab/asset_proxy_spec.rb
+++ b/spec/lib/gitlab/asset_proxy_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe Gitlab::AssetProxy do
context 'when asset proxy is enabled' do
before do
- stub_asset_proxy_setting(allowlist: %w(gitlab.com *.mydomain.com))
+ stub_asset_proxy_setting(allowlist: %w[gitlab.com *.mydomain.com])
stub_asset_proxy_setting(
enabled: true,
url: 'https://assets.example.com',
diff --git a/spec/lib/gitlab/auth/ldap/auth_hash_spec.rb b/spec/lib/gitlab/auth/ldap/auth_hash_spec.rb
index c19d890a703..0208255d24d 100644
--- a/spec/lib/gitlab/auth/ldap/auth_hash_spec.rb
+++ b/spec/lib/gitlab/auth/ldap/auth_hash_spec.rb
@@ -52,7 +52,7 @@ RSpec.describe Gitlab::Auth::Ldap::AuthHash do
let(:attributes) do
{
- 'username' => %w(mail email),
+ 'username' => %w[mail email],
'name' => 'fullName'
}
end
diff --git a/spec/lib/gitlab/auth/ldap/config_spec.rb b/spec/lib/gitlab/auth/ldap/config_spec.rb
index 48039b58216..f97b16254e7 100644
--- a/spec/lib/gitlab/auth/ldap/config_spec.rb
+++ b/spec/lib/gitlab/auth/ldap/config_spec.rb
@@ -90,7 +90,7 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK
end
it 'returns one provider' do
- expect(described_class.available_providers).to match_array(%w(ldapmain))
+ expect(described_class.available_providers).to match_array(%w[ldapmain])
end
end
@@ -552,15 +552,15 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK
stub_ldap_config(
options: {
'attributes' => {
- 'username' => %w(sAMAccountName),
- 'email' => %w(userPrincipalName)
+ 'username' => %w[sAMAccountName],
+ 'email' => %w[userPrincipalName]
}
}
)
expect(config.attributes).to include({
- 'username' => %w(sAMAccountName),
- 'email' => %w(userPrincipalName),
+ 'username' => %w[sAMAccountName],
+ 'email' => %w[userPrincipalName],
'name' => 'cn'
})
end
diff --git a/spec/lib/gitlab/auth/ldap/person_spec.rb b/spec/lib/gitlab/auth/ldap/person_spec.rb
index f8268bb1666..b5fd44d4aa9 100644
--- a/spec/lib/gitlab/auth/ldap/person_spec.rb
+++ b/spec/lib/gitlab/auth/ldap/person_spec.rb
@@ -13,13 +13,13 @@ RSpec.describe Gitlab::Auth::Ldap::Person do
'uid' => 'uid',
'attributes' => {
'name' => 'cn',
- 'email' => %w(mail email userPrincipalName),
+ 'email' => %w[mail email userPrincipalName],
'username' => username_attribute
}
}
)
end
- let(:username_attribute) { %w(uid sAMAccountName userid) }
+ let(:username_attribute) { %w[uid sAMAccountName userid] }
describe '.normalize_dn' do
subject { described_class.normalize_dn(given) }
@@ -57,7 +57,7 @@ RSpec.describe Gitlab::Auth::Ldap::Person do
'attributes' => {
'name' => 'cn',
'email' => 'mail',
- 'username' => %w(uid mail),
+ 'username' => %w[uid mail],
'first_name' => ''
}
}
diff --git a/spec/lib/gitlab/auth/o_auth/user_spec.rb b/spec/lib/gitlab/auth/o_auth/user_spec.rb
index 8a9182f6457..c137ca88589 100644
--- a/spec/lib/gitlab/auth/o_auth/user_spec.rb
+++ b/spec/lib/gitlab/auth/o_auth/user_spec.rb
@@ -369,7 +369,7 @@ RSpec.describe Gitlab::Auth::OAuth::User, feature_category: :system_access do
context "and at least one LDAP provider is defined" do
before do
- stub_ldap_config(providers: %w(ldapmain))
+ stub_ldap_config(providers: %w[ldapmain])
end
context "and a corresponding LDAP person" do
@@ -570,7 +570,7 @@ RSpec.describe Gitlab::Auth::OAuth::User, feature_category: :system_access do
before do
allow(ldap_user).to receive(:uid) { uid }
allow(ldap_user).to receive(:username) { 'johndoe@example.com' }
- allow(ldap_user).to receive(:email) { %w(johndoe@example.com john2@example.com) }
+ allow(ldap_user).to receive(:email) { %w[johndoe@example.com john2@example.com] }
allow(ldap_user).to receive(:dn) { dn }
end
@@ -605,7 +605,7 @@ RSpec.describe Gitlab::Auth::OAuth::User, feature_category: :system_access do
context "and at least one LDAP provider is defined" do
before do
- stub_ldap_config(providers: %w(ldapmain))
+ stub_ldap_config(providers: %w[ldapmain])
end
context "and a corresponding LDAP person" do
@@ -1055,7 +1055,7 @@ RSpec.describe Gitlab::Auth::OAuth::User, feature_category: :system_access do
context "update only requested info" do
before do
stub_omniauth_setting(sync_profile_from_provider: ['my-provider'])
- stub_omniauth_setting(sync_profile_attributes: %w(name location))
+ stub_omniauth_setting(sync_profile_attributes: %w[name location])
end
it "updates the user name" do
diff --git a/spec/lib/gitlab/auth/saml/auth_hash_spec.rb b/spec/lib/gitlab/auth/saml/auth_hash_spec.rb
index 5286e22abc9..e37b9b10834 100644
--- a/spec/lib/gitlab/auth/saml/auth_hash_spec.rb
+++ b/spec/lib/gitlab/auth/saml/auth_hash_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Gitlab::Auth::Saml::AuthHash do
include LoginHelpers
- let(:raw_info_attr) { { 'groups' => %w(Developers Freelancers) } }
+ let(:raw_info_attr) { { 'groups' => %w[Developers Freelancers] } }
subject(:saml_auth_hash) { described_class.new(omniauth_auth_hash) }
let(:info_hash) do
@@ -23,12 +23,12 @@ RSpec.describe Gitlab::Auth::Saml::AuthHash do
end
before do
- stub_saml_group_config(%w(Developers Freelancers Designers))
+ stub_saml_group_config(%w[Developers Freelancers Designers])
end
describe '#groups' do
it 'returns array of groups' do
- expect(saml_auth_hash.groups).to eq(%w(Developers Freelancers))
+ expect(saml_auth_hash.groups).to eq(%w[Developers Freelancers])
end
context 'raw info hash attributes empty' do
diff --git a/spec/lib/gitlab/auth/saml/config_spec.rb b/spec/lib/gitlab/auth/saml/config_spec.rb
index d657622c9f2..c19171bb6f8 100644
--- a/spec/lib/gitlab/auth/saml/config_spec.rb
+++ b/spec/lib/gitlab/auth/saml/config_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe Gitlab::Auth::Saml::Config do
+ include LoginHelpers
+
describe '.enabled?' do
subject { described_class.enabled? }
@@ -10,13 +12,48 @@ RSpec.describe Gitlab::Auth::Saml::Config do
context 'when SAML is enabled' do
before do
- allow(Gitlab::Auth::OAuth::Provider).to receive(:providers).and_return([:saml])
+ stub_basic_saml_config
end
it { is_expected.to eq(true) }
end
end
+ describe '.default_attribute_statements' do
+ it 'includes upstream defaults, nickname and Microsoft values' do
+ expect(described_class.default_attribute_statements).to match_array(
+ {
+ nickname: %w[username nickname],
+ name: [
+ 'name',
+ 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name',
+ 'http://schemas.microsoft.com/ws/2008/06/identity/claims/name'
+ ],
+ email: [
+ 'email',
+ 'mail',
+ 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress',
+ 'http://schemas.microsoft.com/ws/2008/06/identity/claims/emailaddress'
+ ],
+ first_name: [
+ 'first_name',
+ 'firstname',
+ 'firstName',
+ 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname',
+ 'http://schemas.microsoft.com/ws/2008/06/identity/claims/givenname'
+ ],
+ last_name: [
+ 'last_name',
+ 'lastname',
+ 'lastName',
+ 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname',
+ 'http://schemas.microsoft.com/ws/2008/06/identity/claims/surname'
+ ]
+ }
+ )
+ end
+ end
+
describe '#external_groups' do
let(:config_1) { described_class.new('saml1') }
diff --git a/spec/lib/gitlab/auth/saml/user_spec.rb b/spec/lib/gitlab/auth/saml/user_spec.rb
index a8a5d8ae5df..034d1a69a0b 100644
--- a/spec/lib/gitlab/auth/saml/user_spec.rb
+++ b/spec/lib/gitlab/auth/saml/user_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe Gitlab::Auth::Saml::User do
let(:uid) { 'my-uid' }
let(:dn) { 'uid=user1,ou=people,dc=example' }
let(:provider) { 'saml' }
- let(:raw_info_attr) { { 'groups' => %w(Developers Freelancers Designers) } }
+ let(:raw_info_attr) { { 'groups' => %w[Developers Freelancers Designers] } }
let(:auth_hash) { OmniAuth::AuthHash.new(uid: uid, provider: provider, info: info_hash, extra: { raw_info: OneLogin::RubySaml::Attributes.new(raw_info_attr) }) }
let(:info_hash) do
{
@@ -47,12 +47,12 @@ RSpec.describe Gitlab::Auth::Saml::User do
context 'external groups' do
before do
- stub_saml_group_config(%w(Interns))
+ stub_saml_group_config(%w[Interns])
end
context 'are defined' do
it 'marks the user as external' do
- stub_saml_group_config(%w(Freelancers))
+ stub_saml_group_config(%w[Freelancers])
saml_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
expect(gl_user.external).to be_truthy
@@ -119,7 +119,7 @@ RSpec.describe Gitlab::Auth::Saml::User do
context 'external groups' do
context 'are defined' do
it 'marks the user as external' do
- stub_saml_group_config(%w(Freelancers))
+ stub_saml_group_config(%w[Freelancers])
saml_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
expect(gl_user.external).to be_truthy
@@ -128,7 +128,7 @@ RSpec.describe Gitlab::Auth::Saml::User do
context 'are defined but the user does not belong there' do
it 'does not mark the user as external' do
- stub_saml_group_config(%w(Interns))
+ stub_saml_group_config(%w[Interns])
saml_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
expect(gl_user.external).to be_falsey
@@ -151,7 +151,7 @@ RSpec.describe Gitlab::Auth::Saml::User do
context 'and at least one LDAP provider is defined' do
before do
- stub_ldap_config(providers: %w(ldapmain))
+ stub_ldap_config(providers: %w[ldapmain])
end
context 'and a corresponding LDAP person' do
@@ -160,7 +160,7 @@ RSpec.describe Gitlab::Auth::Saml::User do
before do
allow(ldap_user).to receive(:uid) { uid }
allow(ldap_user).to receive(:username) { uid }
- allow(ldap_user).to receive(:email) { %w(john@mail.com john2@example.com) }
+ allow(ldap_user).to receive(:email) { %w[john@mail.com john2@example.com] }
allow(ldap_user).to receive(:dn) { dn }
allow(Gitlab::Auth::Ldap::Adapter).to receive(:new).and_return(adapter)
allow(Gitlab::Auth::Ldap::Person).to receive(:find_by_uid).with(uid, adapter).and_return(ldap_user)
@@ -190,14 +190,14 @@ RSpec.describe Gitlab::Auth::Saml::User do
info: info_hash,
extra: {
raw_info: OneLogin::RubySaml::Attributes.new(
- { 'groups' => %w(Developers Freelancers Designers) }
+ { 'groups' => %w[Developers Freelancers Designers] }
)
}
}
end
let(:auth_hash) { OmniAuth::AuthHash.new(auth_hash_base_attributes) }
- let(:uid_types) { %w(uid dn email) }
+ let(:uid_types) { %w[uid dn email] }
before do
create(:omniauth_user,
@@ -410,7 +410,7 @@ RSpec.describe Gitlab::Auth::Saml::User do
let(:raw_info_attr) { {} }
it 'does not mark user as external' do
- stub_saml_group_config(%w(Freelancers))
+ stub_saml_group_config(%w[Freelancers])
expect(saml_user.find_user.external).to be_falsy
end
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index f5b9555916c..020089b3880 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -8,6 +8,8 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
let(:auth_failure) { { actor: nil, project: nil, type: nil, authentication_abilities: nil } }
let(:gl_auth) { described_class }
+ let(:request) { instance_double(ActionDispatch::Request, ip: 'ip') }
+
describe 'constants' do
it 'API_SCOPES contains all scopes for API access' do
expect(subject::API_SCOPES).to match_array %i[api read_user read_api create_runner k8s_proxy]
@@ -202,7 +204,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
end
context 'when IP is already banned' do
- subject { gl_auth.find_for_git_client('username-does-not-matter', 'password-does-not-matter', project: nil, ip: 'ip') }
+ subject { gl_auth.find_for_git_client('username-does-not-matter', 'password-does-not-matter', project: nil, request: request) }
before do
expect_next_instance_of(Gitlab::Auth::IpRateLimiter) do |rate_limiter|
@@ -223,7 +225,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
expect(rate_limiter).not_to receive(:reset!)
end
- gl_auth.find_for_git_client('gitlab-ci-token', build.token, project: build.project, ip: 'ip')
+ gl_auth.find_for_git_client('gitlab-ci-token', build.token, project: build.project, request: request)
end
it 'skips rate limiting for failed auth' do
@@ -231,7 +233,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
expect(rate_limiter).not_to receive(:register_fail!)
end
- gl_auth.find_for_git_client('gitlab-ci-token', 'wrong_token', project: build.project, ip: 'ip')
+ gl_auth.find_for_git_client('gitlab-ci-token', 'wrong_token', project: build.project, request: request)
end
end
@@ -243,7 +245,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
expect(rate_limiter).to receive(:reset!)
end
- gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip')
+ gl_auth.find_for_git_client(user.username, user.password, project: nil, request: request)
end
it 'rate limits a user by unique IPs' do
@@ -252,7 +254,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
end
expect(Gitlab::Auth::UniqueIpsLimiter).to receive(:limit_user!).twice.and_call_original
- gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip')
+ gl_auth.find_for_git_client(user.username, user.password, project: nil, request: request)
end
it 'registers failure for failed auth' do
@@ -260,13 +262,36 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
expect(rate_limiter).to receive(:register_fail!)
end
- gl_auth.find_for_git_client(user.username, 'wrong_password', project: nil, ip: 'ip')
+ gl_auth.find_for_git_client(user.username, 'wrong_password', project: nil, request: request)
+ end
+
+ context 'when failure goes over threshold' do
+ let(:request) { instance_double(ActionDispatch::Request, fullpath: '/some/project.git/info/refs', request_method: 'GET', ip: 'ip') }
+
+ before do
+ expect_next_instance_of(Gitlab::Auth::IpRateLimiter) do |rate_limiter|
+ expect(rate_limiter).to receive(:register_fail!).and_return(true)
+ end
+ end
+
+ it 'logs a message' do
+ expect(Gitlab::AuthLogger).to receive(:error).with(
+ message: include('IP has been temporarily banned from Git auth'),
+ env: :blocklist,
+ remote_ip: request.ip,
+ request_method: request.request_method,
+ path: request.fullpath,
+ login: user.username
+ )
+
+ gl_auth.find_for_git_client(user.username, 'wrong_password', project: nil, request: request)
+ end
end
end
end
context 'build token' do
- subject { gl_auth.find_for_git_client(username, build.token, project: project, ip: 'ip') }
+ subject { gl_auth.find_for_git_client(username, build.token, project: project, request: request) }
let(:username) { 'gitlab-ci-token' }
@@ -344,20 +369,20 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
project.create_drone_ci_integration(active: true)
project.drone_ci_integration.update!(token: 'token', drone_url: generate(:url))
- expect(gl_auth.find_for_git_client('drone-ci-token', 'token', project: project, ip: 'ip')).to have_attributes(actor: nil, project: project, type: :ci, authentication_abilities: described_class.build_authentication_abilities)
+ expect(gl_auth.find_for_git_client('drone-ci-token', 'token', project: project, request: request)).to have_attributes(actor: nil, project: project, type: :ci, authentication_abilities: described_class.build_authentication_abilities)
end
it 'recognizes master passwords' do
user = create(:user)
- expect(gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip')).to have_attributes(actor: user, project: nil, type: :gitlab_or_ldap, authentication_abilities: described_class.full_authentication_abilities)
+ expect(gl_auth.find_for_git_client(user.username, user.password, project: nil, request: request)).to have_attributes(actor: user, project: nil, type: :gitlab_or_ldap, authentication_abilities: described_class.full_authentication_abilities)
end
include_examples 'user login operation with unique ip limit' do
let(:user) { create(:user) }
def operation
- expect(gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip')).to have_attributes(actor: user, project: nil, type: :gitlab_or_ldap, authentication_abilities: described_class.full_authentication_abilities)
+ expect(gl_auth.find_for_git_client(user.username, user.password, project: nil, request: request)).to have_attributes(actor: user, project: nil, type: :gitlab_or_ldap, authentication_abilities: described_class.full_authentication_abilities)
end
end
@@ -366,14 +391,14 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
user = create(:user)
token = Gitlab::LfsToken.new(user).token
- expect(gl_auth.find_for_git_client(user.username, token, project: nil, ip: 'ip')).to have_attributes(actor: user, project: nil, type: :lfs_token, authentication_abilities: described_class.read_write_project_authentication_abilities)
+ expect(gl_auth.find_for_git_client(user.username, token, project: nil, request: request)).to have_attributes(actor: user, project: nil, type: :lfs_token, authentication_abilities: described_class.read_write_project_authentication_abilities)
end
it 'recognizes deploy key lfs tokens' do
key = create(:deploy_key)
token = Gitlab::LfsToken.new(key).token
- expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: nil, ip: 'ip')).to have_attributes(actor: key, project: nil, type: :lfs_deploy_token, authentication_abilities: described_class.read_only_authentication_abilities)
+ expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: nil, request: request)).to have_attributes(actor: key, project: nil, type: :lfs_deploy_token, authentication_abilities: described_class.read_only_authentication_abilities)
end
it 'does not try password auth before oauth' do
@@ -382,7 +407,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
expect(gl_auth).not_to receive(:find_with_user_password)
- gl_auth.find_for_git_client(user.username, token, project: nil, ip: 'ip')
+ gl_auth.find_for_git_client(user.username, token, project: nil, request: request)
end
it 'grants deploy key write permissions' do
@@ -390,14 +415,14 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
create(:deploy_keys_project, :write_access, deploy_key: key, project: project)
token = Gitlab::LfsToken.new(key).token
- expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: project, ip: 'ip')).to have_attributes(actor: key, project: nil, type: :lfs_deploy_token, authentication_abilities: described_class.read_write_authentication_abilities)
+ expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: project, request: request)).to have_attributes(actor: key, project: nil, type: :lfs_deploy_token, authentication_abilities: described_class.read_write_authentication_abilities)
end
it 'does not grant deploy key write permissions' do
key = create(:deploy_key)
token = Gitlab::LfsToken.new(key).token
- expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: project, ip: 'ip')).to have_attributes(actor: key, project: nil, type: :lfs_deploy_token, authentication_abilities: described_class.read_only_authentication_abilities)
+ expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: project, request: request)).to have_attributes(actor: key, project: nil, type: :lfs_deploy_token, authentication_abilities: described_class.read_only_authentication_abilities)
end
end
@@ -409,7 +434,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
it 'fails' do
access_token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: 'api')
- expect(gl_auth.find_for_git_client("oauth2", access_token.token, project: nil, ip: 'ip'))
+ expect(gl_auth.find_for_git_client("oauth2", access_token.token, project: nil, request: request))
.to have_attributes(auth_failure)
end
end
@@ -436,7 +461,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
it 'authenticates with correct abilities' do
access_token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: scopes)
- expect(gl_auth.find_for_git_client("oauth2", access_token.token, project: nil, ip: 'ip'))
+ expect(gl_auth.find_for_git_client("oauth2", access_token.token, project: nil, request: request))
.to have_attributes(actor: user, project: nil, type: :oauth, authentication_abilities: abilities)
end
end
@@ -447,7 +472,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
expect(gl_auth).not_to receive(:find_with_user_password)
- gl_auth.find_for_git_client("oauth2", access_token.token, project: nil, ip: 'ip')
+ gl_auth.find_for_git_client("oauth2", access_token.token, project: nil, request: request)
end
context 'blocked user' do
@@ -513,7 +538,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
impersonation_token = create(:personal_access_token, :impersonation, scopes: ['api'])
- expect(gl_auth.find_for_git_client('', impersonation_token.token, project: nil, ip: 'ip'))
+ expect(gl_auth.find_for_git_client('', impersonation_token.token, project: nil, request: request))
.to have_attributes(auth_failure)
end
@@ -536,7 +561,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
end
it 'fails if user is blocked' do
- expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip'))
+ expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, request: request))
.to have_attributes(auth_failure)
end
end
@@ -544,19 +569,19 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
context 'when using a resource access token' do
shared_examples 'with a valid access token' do
it 'successfully authenticates the project bot' do
- expect(gl_auth.find_for_git_client(project_bot_user.username, access_token.token, project: project, ip: 'ip'))
+ expect(gl_auth.find_for_git_client(project_bot_user.username, access_token.token, project: project, request: request))
.to have_attributes(actor: project_bot_user, project: nil, type: :personal_access_token, authentication_abilities: described_class.full_authentication_abilities)
end
it 'successfully authenticates the project bot with a nil project' do
- expect(gl_auth.find_for_git_client(project_bot_user.username, access_token.token, project: nil, ip: 'ip'))
+ expect(gl_auth.find_for_git_client(project_bot_user.username, access_token.token, project: nil, request: request))
.to have_attributes(actor: project_bot_user, project: nil, type: :personal_access_token, authentication_abilities: described_class.full_authentication_abilities)
end
end
shared_examples 'with an invalid access token' do
it 'fails for a non-member' do
- expect(gl_auth.find_for_git_client(project_bot_user.username, access_token.token, project: project, ip: 'ip'))
+ expect(gl_auth.find_for_git_client(project_bot_user.username, access_token.token, project: project, request: request))
.to have_attributes(auth_failure)
end
@@ -566,7 +591,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
end
it 'fails for a blocked project bot' do
- expect(gl_auth.find_for_git_client(project_bot_user.username, access_token.token, project: project, ip: 'ip'))
+ expect(gl_auth.find_for_git_client(project_bot_user.username, access_token.token, project: project, request: request))
.to have_attributes(auth_failure)
end
end
@@ -637,7 +662,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
it 'updates last_used_at column if token is valid' do
personal_access_token = create(:personal_access_token, scopes: ['write_repository'])
- expect { gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip') }.to change { personal_access_token.reload.last_used_at }
+ expect { gl_auth.find_for_git_client('', personal_access_token.token, project: nil, request: request) }.to change { personal_access_token.reload.last_used_at }
end
end
@@ -649,7 +674,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
username: 'normal_user'
)
- expect(gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip'))
+ expect(gl_auth.find_for_git_client(user.username, user.password, project: nil, request: request))
.to have_attributes(auth_failure)
end
@@ -665,14 +690,14 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
it 'fails if grace period expired' do
stub_application_setting(two_factor_grace_period: 0)
- expect { gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip') }
+ expect { gl_auth.find_for_git_client(user.username, user.password, project: nil, request: request) }
.to raise_error(Gitlab::Auth::MissingPersonalAccessTokenError)
end
it 'goes through if grace period is not expired yet' do
stub_application_setting(two_factor_grace_period: 72)
- expect(gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip'))
+ expect(gl_auth.find_for_git_client(user.username, user.password, project: nil, request: request))
.to have_attributes(actor: user, project: nil, type: :gitlab_or_ldap, authentication_abilities: described_class.full_authentication_abilities)
end
end
@@ -683,7 +708,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
end
it 'fails' do
- expect { gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip') }
+ expect { gl_auth.find_for_git_client(user.username, user.password, project: nil, request: request) }
.to raise_error(Gitlab::Auth::MissingPersonalAccessTokenError)
end
end
@@ -694,7 +719,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
username: 'normal_user'
)
- expect(gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip'))
+ expect(gl_auth.find_for_git_client(user.username, user.password, project: nil, request: request))
.to have_attributes(actor: user, project: nil, type: :gitlab_or_ldap, authentication_abilities: described_class.full_authentication_abilities)
end
@@ -704,7 +729,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
username: 'oauth2'
)
- expect(gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip'))
+ expect(gl_auth.find_for_git_client(user.username, user.password, project: nil, request: request))
.to have_attributes(actor: user, project: nil, type: :gitlab_or_ldap, authentication_abilities: described_class.full_authentication_abilities)
end
end
@@ -712,34 +737,34 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
it 'returns double nil for invalid credentials' do
login = 'foo'
- expect(gl_auth.find_for_git_client(login, 'bar', project: nil, ip: 'ip')).to have_attributes(auth_failure)
+ expect(gl_auth.find_for_git_client(login, 'bar', project: nil, request: request)).to have_attributes(auth_failure)
end
it 'throws an error suggesting user create a PAT when internal auth is disabled' do
allow_any_instance_of(ApplicationSetting).to receive(:password_authentication_enabled_for_git?) { false }
- expect { gl_auth.find_for_git_client('foo', 'bar', project: nil, ip: 'ip') }.to raise_error(Gitlab::Auth::MissingPersonalAccessTokenError)
+ expect { gl_auth.find_for_git_client('foo', 'bar', project: nil, request: request) }.to raise_error(Gitlab::Auth::MissingPersonalAccessTokenError)
end
context 'while using deploy tokens' do
shared_examples 'registry token scope' do
it 'fails when login is not valid' do
- expect(gl_auth.find_for_git_client('random_login', deploy_token.token, project: project, ip: 'ip'))
+ expect(gl_auth.find_for_git_client('random_login', deploy_token.token, project: project, request: request))
.to have_attributes(auth_failure)
end
it 'fails when token is not valid' do
- expect(gl_auth.find_for_git_client(login, '123123', project: project, ip: 'ip'))
+ expect(gl_auth.find_for_git_client(login, '123123', project: project, request: request))
.to have_attributes(auth_failure)
end
it 'fails if token is nil' do
- expect(gl_auth.find_for_git_client(login, nil, project: nil, ip: 'ip'))
+ expect(gl_auth.find_for_git_client(login, nil, project: nil, request: request))
.to have_attributes(auth_failure)
end
it 'fails if token is not related to project' do
- expect(gl_auth.find_for_git_client(login, 'abcdef', project: nil, ip: 'ip'))
+ expect(gl_auth.find_for_git_client(login, 'abcdef', project: nil, request: request))
.to have_attributes(auth_failure)
end
@@ -747,7 +772,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
deploy_token.revoke!
expect(deploy_token.revoked?).to be_truthy
- expect(gl_auth.find_for_git_client('deploy-token', deploy_token.token, project: nil, ip: 'ip'))
+ expect(gl_auth.find_for_git_client('deploy-token', deploy_token.token, project: nil, request: request))
.to have_attributes(auth_failure)
end
end
@@ -759,7 +784,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
end
it 'fails when login and token are valid' do
- expect(gl_auth.find_for_git_client(login, deploy_token.token, project: nil, ip: 'ip'))
+ expect(gl_auth.find_for_git_client(login, deploy_token.token, project: nil, request: request))
.to have_attributes(auth_failure)
end
end
@@ -768,7 +793,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
let(:project) { create(:project, :repository_disabled) }
it 'fails when login and token are valid' do
- expect(gl_auth.find_for_git_client(login, deploy_token.token, project: project, ip: 'ip'))
+ expect(gl_auth.find_for_git_client(login, deploy_token.token, project: project, request: request))
.to have_attributes(auth_failure)
end
end
@@ -782,14 +807,14 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
it 'succeeds for the token' do
auth_success = { actor: deploy_token, project: project, type: :deploy_token, authentication_abilities: [:download_code] }
- expect(gl_auth.find_for_git_client(username, deploy_token.token, project: project, ip: 'ip'))
+ expect(gl_auth.find_for_git_client(username, deploy_token.token, project: project, request: request))
.to have_attributes(auth_success)
end
it 'succeeds for the user' do
auth_success = { actor: user, project: nil, type: :gitlab_or_ldap, authentication_abilities: described_class.full_authentication_abilities }
- expect(gl_auth.find_for_git_client(username, user.password, project: project, ip: 'ip'))
+ expect(gl_auth.find_for_git_client(username, user.password, project: project, request: request))
.to have_attributes(auth_success)
end
end
@@ -801,12 +826,12 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
let(:auth_success) { { actor: read_repository, project: project, type: :deploy_token, authentication_abilities: [:download_code] } }
it 'succeeds for the right token' do
- expect(gl_auth.find_for_git_client('deployer', read_repository.token, project: project, ip: 'ip'))
+ expect(gl_auth.find_for_git_client('deployer', read_repository.token, project: project, request: request))
.to have_attributes(auth_success)
end
it 'fails for the wrong token' do
- expect(gl_auth.find_for_git_client('deployer', read_registry.token, project: project, ip: 'ip'))
+ expect(gl_auth.find_for_git_client('deployer', read_registry.token, project: project, request: request))
.not_to have_attributes(auth_success)
end
end
@@ -819,12 +844,12 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
let(:auth_success) { { actor: read_repository, project: other_project, type: :deploy_token, authentication_abilities: [:download_code] } }
it 'succeeds for the right token' do
- expect(gl_auth.find_for_git_client('deployer', read_repository.token, project: other_project, ip: 'ip'))
+ expect(gl_auth.find_for_git_client('deployer', read_repository.token, project: other_project, request: request))
.to have_attributes(auth_success)
end
it 'fails for the wrong token' do
- expect(gl_auth.find_for_git_client('deployer', read_registry.token, project: other_project, ip: 'ip'))
+ expect(gl_auth.find_for_git_client('deployer', read_registry.token, project: other_project, request: request))
.not_to have_attributes(auth_success)
end
end
@@ -837,7 +862,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
it 'succeeds when login and token are valid' do
auth_success = { actor: deploy_token, project: project, type: :deploy_token, authentication_abilities: [:download_code] }
- expect(gl_auth.find_for_git_client(login, deploy_token.token, project: project, ip: 'ip'))
+ expect(gl_auth.find_for_git_client(login, deploy_token.token, project: project, request: request))
.to have_attributes(auth_success)
end
@@ -845,34 +870,34 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
deploy_token = create(:deploy_token, username: 'deployer', read_registry: false, projects: [project])
auth_success = { actor: deploy_token, project: project, type: :deploy_token, authentication_abilities: [:download_code] }
- expect(gl_auth.find_for_git_client('deployer', deploy_token.token, project: project, ip: 'ip'))
+ expect(gl_auth.find_for_git_client('deployer', deploy_token.token, project: project, request: request))
.to have_attributes(auth_success)
end
it 'does not attempt to rate limit unique IPs for a deploy token' do
expect(Gitlab::Auth::UniqueIpsLimiter).not_to receive(:limit_user!)
- gl_auth.find_for_git_client(login, deploy_token.token, project: project, ip: 'ip')
+ gl_auth.find_for_git_client(login, deploy_token.token, project: project, request: request)
end
it 'fails when login is not valid' do
- expect(gl_auth.find_for_git_client('random_login', deploy_token.token, project: project, ip: 'ip'))
+ expect(gl_auth.find_for_git_client('random_login', deploy_token.token, project: project, request: request))
.to have_attributes(auth_failure)
end
it 'fails when token is not valid' do
- expect(gl_auth.find_for_git_client(login, '123123', project: project, ip: 'ip'))
+ expect(gl_auth.find_for_git_client(login, '123123', project: project, request: request))
.to have_attributes(auth_failure)
end
it 'fails if token is nil' do
- expect(gl_auth.find_for_git_client(login, nil, project: project, ip: 'ip'))
+ expect(gl_auth.find_for_git_client(login, nil, project: project, request: request))
.to have_attributes(auth_failure)
end
it 'fails if token is not related to project' do
another_deploy_token = create(:deploy_token)
- expect(gl_auth.find_for_git_client(another_deploy_token.username, another_deploy_token.token, project: project, ip: 'ip'))
+ expect(gl_auth.find_for_git_client(another_deploy_token.username, another_deploy_token.token, project: project, request: request))
.to have_attributes(auth_failure)
end
@@ -880,7 +905,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
deploy_token.revoke!
expect(deploy_token.revoked?).to be_truthy
- expect(gl_auth.find_for_git_client('deploy-token', deploy_token.token, project: project, ip: 'ip'))
+ expect(gl_auth.find_for_git_client('deploy-token', deploy_token.token, project: project, request: request))
.to have_attributes(auth_failure)
end
end
@@ -890,7 +915,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
let(:deploy_token) { create(:deploy_token, :group, read_repository: true, groups: [project_with_group.group]) }
let(:login) { deploy_token.username }
- subject { gl_auth.find_for_git_client(login, deploy_token.token, project: project_with_group, ip: 'ip') }
+ subject { gl_auth.find_for_git_client(login, deploy_token.token, project: project_with_group, request: request) }
it 'succeeds when login and a group deploy token are valid' do
auth_success = { actor: deploy_token, project: project_with_group, type: :deploy_token, authentication_abilities: [:download_code, :read_container_image] }
@@ -901,7 +926,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
it 'fails if token is not related to group' do
another_deploy_token = create(:deploy_token, :group, read_repository: true)
- expect(gl_auth.find_for_git_client(another_deploy_token.username, another_deploy_token.token, project: project_with_group, ip: 'ip'))
+ expect(gl_auth.find_for_git_client(another_deploy_token.username, another_deploy_token.token, project: project_with_group, request: request))
.to have_attributes(auth_failure)
end
end
@@ -918,7 +943,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
it 'succeeds when login and a project token are valid' do
auth_success = { actor: deploy_token, project: project, type: :deploy_token, authentication_abilities: [:read_container_image] }
- expect(gl_auth.find_for_git_client(login, deploy_token.token, project: project, ip: 'ip'))
+ expect(gl_auth.find_for_git_client(login, deploy_token.token, project: project, request: request))
.to have_attributes(auth_success)
end
@@ -940,7 +965,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
it 'succeeds when login and a project token are valid' do
auth_success = { actor: deploy_token, project: project, type: :deploy_token, authentication_abilities: [:create_container_image] }
- expect(gl_auth.find_for_git_client(login, deploy_token.token, project: project, ip: 'ip'))
+ expect(gl_auth.find_for_git_client(login, deploy_token.token, project: project, request: request))
.to have_attributes(auth_success)
end
@@ -953,7 +978,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
end
describe '#build_access_token_check' do
- subject { gl_auth.find_for_git_client('gitlab-ci-token', build.token, project: build.project, ip: '1.2.3.4') }
+ subject { gl_auth.find_for_git_client('gitlab-ci-token', build.token, project: build.project, request: request) }
let_it_be(:user) { create(:user) }
@@ -1143,7 +1168,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
private
def expect_results_with_abilities(personal_access_token, abilities, success = true)
- expect(gl_auth.find_for_git_client('', personal_access_token&.token, project: nil, ip: 'ip'))
+ expect(gl_auth.find_for_git_client('', personal_access_token&.token, project: nil, request: request))
.to have_attributes(actor: personal_access_token&.user, project: nil, type: personal_access_token.nil? ? nil : :personal_access_token, authentication_abilities: abilities)
end
end
diff --git a/spec/lib/gitlab/background_migration/backfill_packages_tags_project_id_spec.rb b/spec/lib/gitlab/background_migration/backfill_packages_tags_project_id_spec.rb
new file mode 100644
index 00000000000..423d9fe76ac
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/backfill_packages_tags_project_id_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::BackfillPackagesTagsProjectId,
+ feature_category: :package_registry,
+ schema: 20231030051837 do # schema before we introduced the invalid not-null constraint
+ let!(:tags_without_project_id) do
+ (0...13).map do |i|
+ namespace = table(:namespaces).create!(name: 'my namespace', path: 'my-namespace')
+ project = table(:projects).create!(name: 'my project', path: 'my-project', namespace_id: namespace.id,
+ project_namespace_id: namespace.id)
+ package = table(:packages_packages).create!(project_id: project.id, created_at: Time.current,
+ updated_at: Time.current, name: "Package #{i}", package_type: 1, status: 1)
+ table(:packages_tags).create!(package_id: package.id, name: "Tag #{i}", created_at: Time.current,
+ updated_at: Time.current, project_id: nil)
+ end
+ end
+
+ let!(:starting_id) { table(:packages_tags).pluck(:id).min }
+ let!(:end_id) { table(:packages_tags).pluck(:id).max }
+
+ let!(:migration) do
+ described_class.new(
+ start_id: starting_id,
+ end_id: end_id,
+ batch_table: :packages_tags,
+ batch_column: :id,
+ sub_batch_size: 10,
+ pause_ms: 2,
+ connection: ::ApplicationRecord.connection
+ )
+ end
+
+ it 'backfills the missing project_id for the batch' do
+ expect do
+ migration.perform
+ end.to change { table(:packages_tags).where(project_id: nil).count }
+ .from(13)
+ .to(0)
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/batched_migration_job_spec.rb b/spec/lib/gitlab/background_migration/batched_migration_job_spec.rb
index 781bf93dd85..da24e9b7978 100644
--- a/spec/lib/gitlab/background_migration/batched_migration_job_spec.rb
+++ b/spec/lib/gitlab/background_migration/batched_migration_job_spec.rb
@@ -2,22 +2,22 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::BatchedMigrationJob do
+RSpec.describe Gitlab::BackgroundMigration::BatchedMigrationJob, feature_category: :database do
let(:connection) { Gitlab::Database.database_base_models[:main].connection }
describe '.generic_instance' do
it 'defines generic instance with only some of the attributes set' do
generic_instance = described_class.generic_instance(
batch_table: 'projects', batch_column: 'id',
- job_arguments: %w(x y), connection: connection
+ job_arguments: %w[x y], connection: connection
)
expect(generic_instance.send(:batch_table)).to eq('projects')
expect(generic_instance.send(:batch_column)).to eq('id')
- expect(generic_instance.instance_variable_get(:@job_arguments)).to eq(%w(x y))
+ expect(generic_instance.instance_variable_get(:@job_arguments)).to eq(%w[x y])
expect(generic_instance.send(:connection)).to eq(connection)
- %i(start_id end_id sub_batch_size pause_ms).each do |attr|
+ %i[start_id end_id sub_batch_size pause_ms].each do |attr|
expect(generic_instance.send(attr)).to eq(0)
end
end
@@ -31,13 +31,16 @@ RSpec.describe Gitlab::BackgroundMigration::BatchedMigrationJob do
end
subject(:job_instance) do
- job_class.new(start_id: 1, end_id: 10,
- batch_table: '_test_table',
- batch_column: 'id',
- sub_batch_size: 2,
- pause_ms: 1000,
- job_arguments: %w(a b),
- connection: connection)
+ job_class.new(
+ start_id: 1,
+ end_id: 10,
+ batch_table: '_test_table',
+ batch_column: 'id',
+ sub_batch_size: 2,
+ pause_ms: 1000,
+ job_arguments: %w[a b],
+ connection: connection
+ )
end
it 'defines methods' do
@@ -61,13 +64,16 @@ RSpec.describe Gitlab::BackgroundMigration::BatchedMigrationJob do
subject(:perform_job) { job_instance.perform }
let(:job_instance) do
- job_class.new(start_id: 1, end_id: 10,
- batch_table: '_test_table',
- batch_column: 'id',
- sub_batch_size: 2,
- pause_ms: 1000,
- job_arguments: %w(a b),
- connection: connection)
+ job_class.new(
+ start_id: 1,
+ end_id: 10,
+ batch_table: '_test_table',
+ batch_column: 'id',
+ sub_batch_size: 2,
+ pause_ms: 1000,
+ job_arguments: %w[a b],
+ connection: connection
+ )
end
let(:job_class) do
@@ -124,13 +130,16 @@ RSpec.describe Gitlab::BackgroundMigration::BatchedMigrationJob do
describe '.scope_to' do
subject(:job_instance) do
- job_class.new(start_id: 1, end_id: 10,
- batch_table: '_test_table',
- batch_column: 'id',
- sub_batch_size: 2,
- pause_ms: 1000,
- job_arguments: %w(a b),
- connection: connection)
+ job_class.new(
+ start_id: 1,
+ end_id: 10,
+ batch_table: '_test_table',
+ batch_column: 'id',
+ sub_batch_size: 2,
+ pause_ms: 1000,
+ job_arguments: %w[a b],
+ connection: connection
+ )
end
context 'when additional scoping is defined' do
@@ -203,12 +212,15 @@ RSpec.describe Gitlab::BackgroundMigration::BatchedMigrationJob do
let(:job_class) { Class.new(described_class) }
let(:job_instance) do
- job_class.new(start_id: 1, end_id: 10,
- batch_table: '_test_table',
- batch_column: 'id',
- sub_batch_size: 2,
- pause_ms: 1000,
- connection: connection)
+ job_class.new(
+ start_id: 1,
+ end_id: 10,
+ batch_table: '_test_table',
+ batch_column: 'id',
+ sub_batch_size: 2,
+ pause_ms: 1000,
+ connection: connection
+ )
end
subject(:perform_job) { job_instance.perform }
@@ -313,9 +325,16 @@ RSpec.describe Gitlab::BackgroundMigration::BatchedMigrationJob do
end
let(:job_instance) do
- job_class.new(start_id: 1, end_id: 10, batch_table: '_test_table', batch_column: 'id',
- sub_batch_size: 2, pause_ms: 1000, connection: connection,
- sub_batch_exception: StandardError)
+ job_class.new(
+ start_id: 1,
+ end_id: 10,
+ batch_table: '_test_table',
+ batch_column: 'id',
+ sub_batch_size: 2,
+ pause_ms: 1000,
+ connection: connection,
+ sub_batch_exception: StandardError
+ )
end
it 'raises the expected error type' do
@@ -336,13 +355,15 @@ RSpec.describe Gitlab::BackgroundMigration::BatchedMigrationJob do
context 'when the subclass uses distinct each batch' do
let(:job_instance) do
- job_class.new(start_id: 1,
- end_id: 100,
- batch_table: '_test_table',
- batch_column: 'from_column',
- sub_batch_size: 2,
- pause_ms: 10,
- connection: connection)
+ job_class.new(
+ start_id: 1,
+ end_id: 100,
+ batch_table: '_test_table',
+ batch_column: 'from_column',
+ sub_batch_size: 2,
+ pause_ms: 10,
+ connection: connection
+ )
end
let(:job_class) do
diff --git a/spec/lib/gitlab/background_migration/copy_column_using_background_migration_job_spec.rb b/spec/lib/gitlab/background_migration/copy_column_using_background_migration_job_spec.rb
index 9c33100a0b3..a827116a900 100644
--- a/spec/lib/gitlab/background_migration/copy_column_using_background_migration_job_spec.rb
+++ b/spec/lib/gitlab/background_migration/copy_column_using_background_migration_job_spec.rb
@@ -16,16 +16,18 @@ RSpec.describe Gitlab::BackgroundMigration::CopyColumnUsingBackgroundMigrationJo
ActiveRecord::Migration.new.extend(Gitlab::Database::MigrationHelpers)
end
- let(:job_arguments) { %w(name name_convert_to_text) }
+ let(:job_arguments) { %w[name name_convert_to_text] }
let(:copy_job) do
- described_class.new(start_id: 12,
- end_id: 20,
- batch_table: table_name,
- batch_column: 'id',
- sub_batch_size: sub_batch_size,
- pause_ms: pause_ms,
- job_arguments: job_arguments,
- connection: connection)
+ described_class.new(
+ start_id: 12,
+ end_id: 20,
+ batch_table: table_name,
+ batch_column: 'id',
+ sub_batch_size: sub_batch_size,
+ pause_ms: pause_ms,
+ job_arguments: job_arguments,
+ connection: connection
+ )
end
before do
@@ -82,7 +84,7 @@ RSpec.describe Gitlab::BackgroundMigration::CopyColumnUsingBackgroundMigrationJo
end
context 'columns with NULLs' do
- let(:job_arguments) { %w(name name_convert_to_text) }
+ let(:job_arguments) { %w[name name_convert_to_text] }
it 'copies all in range' do
expect { copy_job.perform }
diff --git a/spec/lib/gitlab/background_migration/delete_invalid_protected_branch_merge_access_levels_spec.rb b/spec/lib/gitlab/background_migration/delete_invalid_protected_branch_merge_access_levels_spec.rb
new file mode 100644
index 00000000000..1e5b9d30436
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/delete_invalid_protected_branch_merge_access_levels_spec.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::DeleteInvalidProtectedBranchMergeAccessLevels,
+ feature_category: :source_code_management do
+ let(:projects_table) { table(:projects) }
+ let(:protected_branches_table) { table(:protected_branches) }
+ let(:namespaces_table) { table(:namespaces) }
+ let(:protected_branch_merge_access_levels_table) { table(:protected_branch_merge_access_levels) }
+ let(:project_group_links_table) { table(:project_group_links) }
+ let(:users_table) { table(:users) }
+
+ let(:user1) { users_table.create!(name: 'user1', email: 'user1@example.com', projects_limit: 5) }
+
+ let(:project_group) { namespaces_table.create!(name: 'group-1', path: 'group-1', type: 'Group') }
+ let(:project_namespace) { namespaces_table.create!(name: 'namespace', path: 'namespace-path-2', type: 'Project') }
+ let!(:project_1) do
+ projects_table
+ .create!(
+ name: 'project1',
+ path: 'path1',
+ namespace_id: project_group.id,
+ project_namespace_id: project_namespace.id,
+ visibility_level: 0
+ )
+ end
+
+ subject(:perform_migration) do
+ described_class.new(start_id: protected_branch_merge_access_levels_table.minimum(:id),
+ end_id: protected_branch_merge_access_levels_table.maximum(:id),
+ batch_table: :protected_branch_merge_access_levels,
+ batch_column: :id,
+ sub_batch_size: 1,
+ pause_ms: 0,
+ connection: ApplicationRecord.connection)
+ .perform
+ end
+
+ context 'when there are merge access levels' do
+ let(:protected_branch1) { protected_branches_table.create!(project_id: project_1.id, name: 'name') }
+ let!(:merge_access_level_for_user) do
+ protected_branch_merge_access_levels_table.create!(
+ protected_branch_id: protected_branch1.id,
+ user_id: user1.id
+ )
+ end
+
+ let(:invited_group) { namespaces_table.create!(name: 'group-2', path: 'group-2', type: 'Group') }
+ let!(:invited_group_link) do
+ project_group_links_table.create!(project_id: project_1.id, group_id: invited_group.id)
+ end
+
+ let!(:merge_access_level_with_linked_group) do
+ protected_branch_merge_access_levels_table.create!(
+ protected_branch_id: protected_branch1.id,
+ group_id: invited_group.id
+ )
+ end
+
+ let!(:merge_access_level_with_unlinked_group) do
+ protected_branch_merge_access_levels_table.create!(
+ protected_branch_id: protected_branch1.id,
+ group_id: project_group.id
+ )
+ end
+
+ it 'deletes merge access levels with groups that do not have project_group_links to the project' do
+ expect { subject }.to change { protected_branch_merge_access_levels_table.count }.from(3).to(2)
+ expect(protected_branch_merge_access_levels_table.all).to contain_exactly(
+ merge_access_level_with_linked_group,
+ merge_access_level_for_user
+ )
+ end
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/delete_invalid_protected_branch_push_access_levels_spec.rb b/spec/lib/gitlab/background_migration/delete_invalid_protected_branch_push_access_levels_spec.rb
new file mode 100644
index 00000000000..62201831dd1
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/delete_invalid_protected_branch_push_access_levels_spec.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::DeleteInvalidProtectedBranchPushAccessLevels,
+ feature_category: :source_code_management do
+ let(:projects_table) { table(:projects) }
+ let(:protected_branches_table) { table(:protected_branches) }
+ let(:namespaces_table) { table(:namespaces) }
+ let(:protected_branch_push_access_levels_table) { table(:protected_branch_push_access_levels) }
+ let(:project_group_links_table) { table(:project_group_links) }
+ let(:users_table) { table(:users) }
+
+ let(:user1) { users_table.create!(name: 'user1', email: 'user1@example.com', projects_limit: 5) }
+
+ let(:project_group) { namespaces_table.create!(name: 'group-1', path: 'group-1', type: 'Group') }
+ let(:project_namespace) { namespaces_table.create!(name: 'namespace', path: 'namespace-path-2', type: 'Project') }
+ let!(:project_1) do
+ projects_table
+ .create!(
+ name: 'project1',
+ path: 'path1',
+ namespace_id: project_group.id,
+ project_namespace_id: project_namespace.id,
+ visibility_level: 0
+ )
+ end
+
+ subject(:perform_migration) do
+ described_class.new(start_id: protected_branch_push_access_levels_table.minimum(:id),
+ end_id: protected_branch_push_access_levels_table.maximum(:id),
+ batch_table: :protected_branch_push_access_levels,
+ batch_column: :id,
+ sub_batch_size: 1,
+ pause_ms: 0,
+ connection: ApplicationRecord.connection)
+ .perform
+ end
+
+ context 'when there are push access levels' do
+ let(:protected_branch1) { protected_branches_table.create!(project_id: project_1.id, name: 'name') }
+ let!(:push_access_level_for_user) do
+ protected_branch_push_access_levels_table.create!(
+ protected_branch_id: protected_branch1.id,
+ user_id: user1.id
+ )
+ end
+
+ let(:invited_group) { namespaces_table.create!(name: 'group-2', path: 'group-2', type: 'Group') }
+ let!(:invited_group_link) do
+ project_group_links_table.create!(project_id: project_1.id, group_id: invited_group.id)
+ end
+
+ let!(:push_access_level_with_linked_group) do
+ protected_branch_push_access_levels_table.create!(
+ protected_branch_id: protected_branch1.id,
+ group_id: invited_group.id
+ )
+ end
+
+ let!(:push_access_level_with_unlinked_group) do
+ protected_branch_push_access_levels_table.create!(
+ protected_branch_id: protected_branch1.id,
+ group_id: project_group.id
+ )
+ end
+
+ it 'deletes push access levels with groups that do not have project_group_links to the project' do
+ expect { subject }.to change { protected_branch_push_access_levels_table.count }.from(3).to(2)
+ expect(protected_branch_push_access_levels_table.all).to contain_exactly(
+ push_access_level_with_linked_group,
+ push_access_level_for_user
+ )
+ end
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/delete_invalid_protected_tag_create_access_levels_spec.rb b/spec/lib/gitlab/background_migration/delete_invalid_protected_tag_create_access_levels_spec.rb
new file mode 100644
index 00000000000..fd6cee9e4db
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/delete_invalid_protected_tag_create_access_levels_spec.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::DeleteInvalidProtectedTagCreateAccessLevels,
+ feature_category: :source_code_management do
+ let(:projects_table) { table(:projects) }
+ let(:protected_tags_table) { table(:protected_tags) }
+ let(:namespaces_table) { table(:namespaces) }
+ let(:protected_tag_create_access_levels_table) { table(:protected_tag_create_access_levels) }
+ let(:project_group_links_table) { table(:project_group_links) }
+ let(:users_table) { table(:users) }
+
+ let(:user1) { users_table.create!(name: 'user1', email: 'user1@example.com', projects_limit: 5) }
+
+ let(:project_group) { namespaces_table.create!(name: 'group-1', path: 'group-1', type: 'Group') }
+ let(:project_namespace) { namespaces_table.create!(name: 'namespace', path: 'namespace-path-2', type: 'Project') }
+ let!(:project_1) do
+ projects_table
+ .create!(
+ name: 'project1',
+ path: 'path1',
+ namespace_id: project_group.id,
+ project_namespace_id: project_namespace.id,
+ visibility_level: 0
+ )
+ end
+
+ subject(:perform_migration) do
+ described_class.new(start_id: protected_tag_create_access_levels_table.minimum(:id),
+ end_id: protected_tag_create_access_levels_table.maximum(:id),
+ batch_table: :protected_tag_create_access_levels,
+ batch_column: :id,
+ sub_batch_size: 1,
+ pause_ms: 0,
+ connection: ApplicationRecord.connection)
+ .perform
+ end
+
+ context 'when there are push access levels' do
+ let(:protected_tag) { protected_tags_table.create!(project_id: project_1.id, name: 'name') }
+ let!(:push_access_level_for_user) do
+ protected_tag_create_access_levels_table.create!(
+ protected_tag_id: protected_tag.id,
+ user_id: user1.id
+ )
+ end
+
+ let(:invited_group) { namespaces_table.create!(name: 'group-2', path: 'group-2', type: 'Group') }
+ let!(:invited_group_link) do
+ project_group_links_table.create!(project_id: project_1.id, group_id: invited_group.id)
+ end
+
+ let!(:push_access_level_with_linked_group) do
+ protected_tag_create_access_levels_table.create!(
+ protected_tag_id: protected_tag.id,
+ group_id: invited_group.id
+ )
+ end
+
+ let!(:push_access_level_with_unlinked_group) do
+ protected_tag_create_access_levels_table.create!(
+ protected_tag_id: protected_tag.id,
+ group_id: project_group.id
+ )
+ end
+
+ it 'deletes push access levels with groups that do not have project_group_links to the project' do
+ expect { subject }.to change { protected_tag_create_access_levels_table.count }.from(3).to(2)
+ expect(protected_tag_create_access_levels_table.all).to contain_exactly(
+ push_access_level_with_linked_group,
+ push_access_level_for_user
+ )
+ end
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/delete_orphaned_operational_vulnerabilities_spec.rb b/spec/lib/gitlab/background_migration/delete_orphaned_operational_vulnerabilities_spec.rb
index c03962c8d21..4a1985eeccd 100644
--- a/spec/lib/gitlab/background_migration/delete_orphaned_operational_vulnerabilities_spec.rb
+++ b/spec/lib/gitlab/background_migration/delete_orphaned_operational_vulnerabilities_spec.rb
@@ -87,13 +87,15 @@ RSpec.describe Gitlab::BackgroundMigration::DeleteOrphanedOperationalVulnerabili
end
subject(:background_migration) do
- described_class.new(start_id: vulnerabilities.minimum(:id),
- end_id: vulnerabilities.maximum(:id),
- batch_table: :vulnerabilities,
- batch_column: :id,
- sub_batch_size: 2,
- pause_ms: 0,
- connection: ActiveRecord::Base.connection)
+ described_class.new(
+ start_id: vulnerabilities.minimum(:id),
+ end_id: vulnerabilities.maximum(:id),
+ batch_table: :vulnerabilities,
+ batch_column: :id,
+ sub_batch_size: 2,
+ pause_ms: 0,
+ connection: ActiveRecord::Base.connection
+ )
end
it 'drops Cluster Image Scanning and Custom Vulnerabilities without any Findings' do
diff --git a/spec/lib/gitlab/background_migration/delete_orphans_approval_merge_request_rules_spec.rb b/spec/lib/gitlab/background_migration/delete_orphans_approval_merge_request_rules_spec.rb
index c5b46d3f57c..1ac4d184912 100644
--- a/spec/lib/gitlab/background_migration/delete_orphans_approval_merge_request_rules_spec.rb
+++ b/spec/lib/gitlab/background_migration/delete_orphans_approval_merge_request_rules_spec.rb
@@ -22,9 +22,12 @@ RSpec.describe Gitlab::BackgroundMigration::DeleteOrphansApprovalMergeRequestRul
let(:namespace_2) { namespaces.create!(name: 'name_2', path: 'path_2') }
let(:security_project) do
- projects
- .create!(name: "security_project", path: "security_project", namespace_id: namespace_2.id,
- project_namespace_id: namespace_2.id)
+ projects.create!(
+ name: "security_project",
+ path: "security_project",
+ namespace_id: namespace_2.id,
+ project_namespace_id: namespace_2.id
+ )
end
let!(:security_orchestration_policy_configuration) do
diff --git a/spec/lib/gitlab/background_migration/delete_orphans_approval_project_rules_spec.rb b/spec/lib/gitlab/background_migration/delete_orphans_approval_project_rules_spec.rb
index 16253255764..23026f76001 100644
--- a/spec/lib/gitlab/background_migration/delete_orphans_approval_project_rules_spec.rb
+++ b/spec/lib/gitlab/background_migration/delete_orphans_approval_project_rules_spec.rb
@@ -22,9 +22,12 @@ RSpec.describe Gitlab::BackgroundMigration::DeleteOrphansApprovalProjectRules do
let(:namespace_2) { namespaces.create!(name: 'name_2', path: 'path_2') }
let(:security_project) do
- projects
- .create!(name: "security_project", path: "security_project", namespace_id: namespace_2.id,
- project_namespace_id: namespace_2.id)
+ projects.create!(
+ name: "security_project",
+ path: "security_project",
+ namespace_id: namespace_2.id,
+ project_namespace_id: namespace_2.id
+ )
end
let!(:security_orchestration_policy_configuration) do
diff --git a/spec/lib/gitlab/background_migration/destroy_invalid_group_members_spec.rb b/spec/lib/gitlab/background_migration/destroy_invalid_group_members_spec.rb
index 76a9ea82c76..4e136808a36 100644
--- a/spec/lib/gitlab/background_migration/destroy_invalid_group_members_spec.rb
+++ b/spec/lib/gitlab/background_migration/destroy_invalid_group_members_spec.rb
@@ -76,13 +76,29 @@ RSpec.describe Gitlab::BackgroundMigration::DestroyInvalidGroupMembers, :migrati
end
def create_invalid_group_member(id:, user_id:)
- members_table.create!(id: id, user_id: user_id, source_id: non_existing_record_id, access_level: Gitlab::Access::MAINTAINER,
- type: "GroupMember", source_type: "Namespace", notification_level: 3, member_namespace_id: nil)
+ members_table.create!(
+ id: id,
+ user_id: user_id,
+ source_id: non_existing_record_id,
+ access_level: Gitlab::Access::MAINTAINER,
+ type: "GroupMember",
+ source_type: "Namespace",
+ notification_level: 3,
+ member_namespace_id: nil
+ )
end
def create_valid_group_member(id:, user_id:, group_id:)
- members_table.create!(id: id, user_id: user_id, source_id: group_id, access_level: Gitlab::Access::MAINTAINER,
- type: "GroupMember", source_type: "Namespace", member_namespace_id: group_id, notification_level: 3)
+ members_table.create!(
+ id: id,
+ user_id: user_id,
+ source_id: group_id,
+ access_level: Gitlab::Access::MAINTAINER,
+ type: "GroupMember",
+ source_type: "Namespace",
+ member_namespace_id: group_id,
+ notification_level: 3
+ )
end
# rubocop: enable Layout/LineLength
# rubocop: enable RSpec/ScatteredLet
diff --git a/spec/lib/gitlab/background_migration/destroy_invalid_members_spec.rb b/spec/lib/gitlab/background_migration/destroy_invalid_members_spec.rb
index 5059ad620aa..e5965d4a1d8 100644
--- a/spec/lib/gitlab/background_migration/destroy_invalid_members_spec.rb
+++ b/spec/lib/gitlab/background_migration/destroy_invalid_members_spec.rb
@@ -33,23 +33,39 @@ RSpec.describe Gitlab::BackgroundMigration::DestroyInvalidMembers, :migration, s
let!(:group1) { namespaces_table.create!(name: 'marvellous group 1', path: 'group-path-1', type: 'Group') }
let!(:group2) { namespaces_table.create!(name: 'outstanding group 2', path: 'group-path-2', type: 'Group') }
let!(:project_namespace1) do
- namespaces_table.create!(name: 'fabulous project', path: 'project-path-1',
- type: 'ProjectNamespace', parent_id: group1.id)
+ namespaces_table.create!(
+ name: 'fabulous project',
+ path: 'project-path-1',
+ type: 'ProjectNamespace',
+ parent_id: group1.id
+ )
end
let!(:project1) do
- projects_table.create!(name: 'fabulous project', path: 'project-path-1',
- project_namespace_id: project_namespace1.id, namespace_id: group1.id)
+ projects_table.create!(
+ name: 'fabulous project',
+ path: 'project-path-1',
+ project_namespace_id: project_namespace1.id,
+ namespace_id: group1.id
+ )
end
let!(:project_namespace2) do
- namespaces_table.create!(name: 'splendiferous project', path: 'project-path-2',
- type: 'ProjectNamespace', parent_id: group1.id)
+ namespaces_table.create!(
+ name: 'splendiferous project',
+ path: 'project-path-2',
+ type: 'ProjectNamespace',
+ parent_id: group1.id
+ )
end
let!(:project2) do
- projects_table.create!(name: 'splendiferous project', path: 'project-path-2',
- project_namespace_id: project_namespace2.id, namespace_id: group1.id)
+ projects_table.create!(
+ name: 'splendiferous project',
+ path: 'project-path-2',
+ project_namespace_id: project_namespace2.id,
+ namespace_id: group1.id
+ )
end
# create valid project member records
@@ -115,27 +131,55 @@ RSpec.describe Gitlab::BackgroundMigration::DestroyInvalidMembers, :migration, s
end
def create_invalid_project_member(id:, user_id:)
- members_table.create!(id: id, user_id: user_id, source_id: non_existing_record_id,
- access_level: Gitlab::Access::MAINTAINER, type: "ProjectMember",
- source_type: "Project", notification_level: 3, member_namespace_id: nil)
+ members_table.create!(
+ id: id,
+ user_id: user_id,
+ source_id: non_existing_record_id,
+ access_level: Gitlab::Access::MAINTAINER,
+ type: "ProjectMember",
+ source_type: "Project",
+ notification_level: 3,
+ member_namespace_id: nil
+ )
end
def create_valid_project_member(id:, user_id:, project:)
- members_table.create!(id: id, user_id: user_id, source_id: project.id,
- access_level: Gitlab::Access::MAINTAINER, type: "ProjectMember", source_type: "Project",
- member_namespace_id: project.project_namespace_id, notification_level: 3)
+ members_table.create!(
+ id: id,
+ user_id: user_id,
+ source_id: project.id,
+ access_level: Gitlab::Access::MAINTAINER,
+ type: "ProjectMember",
+ source_type: "Project",
+ member_namespace_id: project.project_namespace_id,
+ notification_level: 3
+ )
end
def create_invalid_group_member(id:, user_id:)
- members_table.create!(id: id, user_id: user_id, source_id: non_existing_record_id,
- access_level: Gitlab::Access::MAINTAINER, type: "GroupMember",
- source_type: "Namespace", notification_level: 3, member_namespace_id: nil)
+ members_table.create!(
+ id: id,
+ user_id: user_id,
+ source_id: non_existing_record_id,
+ access_level: Gitlab::Access::MAINTAINER,
+ type: "GroupMember",
+ source_type: "Namespace",
+ notification_level: 3,
+ member_namespace_id: nil
+ )
end
def create_valid_group_member(id:, user_id:, group_id:)
- members_table.create!(id: id, user_id: user_id, source_id: group_id,
- access_level: Gitlab::Access::MAINTAINER, type: "GroupMember",
- source_type: "Namespace", member_namespace_id: group_id, notification_level: 3)
+ members_table.create!(
+ id: id,
+ user_id: user_id,
+ source_id: group_id,
+ access_level: Gitlab::Access::MAINTAINER,
+ type: "GroupMember",
+ source_type: "Namespace",
+ member_namespace_id: group_id,
+ notification_level: 3
+ )
end
end
# rubocop: enable RSpec/MultipleMemoizedHelpers
diff --git a/spec/lib/gitlab/background_migration/destroy_invalid_project_members_spec.rb b/spec/lib/gitlab/background_migration/destroy_invalid_project_members_spec.rb
index 029a6adf831..090c31049b4 100644
--- a/spec/lib/gitlab/background_migration/destroy_invalid_project_members_spec.rb
+++ b/spec/lib/gitlab/background_migration/destroy_invalid_project_members_spec.rb
@@ -3,7 +3,6 @@
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::DestroyInvalidProjectMembers, :migration, schema: 20220901035725 do
- # rubocop: disable Layout/LineLength
# rubocop: disable RSpec/ScatteredLet
let!(:migration_attrs) do
{
@@ -36,23 +35,33 @@ RSpec.describe Gitlab::BackgroundMigration::DestroyInvalidProjectMembers, :migra
let!(:group1) { namespaces_table.create!(name: 'marvellous group 1', path: 'group-path-1', type: 'Group') }
let!(:project_namespace1) do
- namespaces_table.create!(name: 'fabulous project', path: 'project-path-1', type: 'ProjectNamespace',
- parent_id: group1.id)
+ namespaces_table.create!(
+ name: 'fabulous project', path: 'project-path-1', type: 'ProjectNamespace', parent_id: group1.id
+ )
end
let!(:project1) do
- projects_table.create!(name: 'fabulous project', path: 'project-path-1', project_namespace_id: project_namespace1.id,
- namespace_id: group1.id)
+ projects_table.create!(
+ name: 'fabulous project',
+ path: 'project-path-1',
+ project_namespace_id: project_namespace1.id,
+ namespace_id: group1.id
+ )
end
let!(:project_namespace2) do
- namespaces_table.create!(name: 'splendiferous project', path: 'project-path-2', type: 'ProjectNamespace',
- parent_id: group1.id)
+ namespaces_table.create!(
+ name: 'splendiferous project', path: 'project-path-2', type: 'ProjectNamespace', parent_id: group1.id
+ )
end
let!(:project2) do
- projects_table.create!(name: 'splendiferous project', path: 'project-path-2', project_namespace_id: project_namespace2.id,
- namespace_id: group1.id)
+ projects_table.create!(
+ name: 'splendiferous project',
+ path: 'project-path-2',
+ project_namespace_id: project_namespace2.id,
+ namespace_id: group1.id
+ )
end
# create project member records, a mix of both valid and invalid
@@ -72,7 +81,8 @@ RSpec.describe Gitlab::BackgroundMigration::DestroyInvalidProjectMembers, :migra
end
expect(queries.count).to eq(4)
- expect(members_table.where(type: 'ProjectMember')).to match_array([project_member2, project_member3, project_member5])
+ expect(members_table.where(type: 'ProjectMember'))
+ .to match_array([project_member2, project_member3, project_member5])
end
it 'tracks timings of queries' do
@@ -82,21 +92,33 @@ RSpec.describe Gitlab::BackgroundMigration::DestroyInvalidProjectMembers, :migra
end
it 'logs IDs of deleted records' do
- expect(Gitlab::AppLogger).to receive(:info).with({ message: 'Removing invalid project member records',
- deleted_count: 3, ids: [project_member1, project_member4, project_member6].map(&:id) })
+ expect(Gitlab::AppLogger).to receive(:info).with({
+ message: 'Removing invalid project member records',
+ deleted_count: 3,
+ ids: [project_member1, project_member4, project_member6].map(&:id)
+ })
perform_migration
end
def create_invalid_project_member(id:, user_id:)
- members_table.create!(id: id, user_id: user_id, source_id: non_existing_record_id, access_level: Gitlab::Access::MAINTAINER,
- type: "ProjectMember", source_type: "Project", notification_level: 3, member_namespace_id: nil)
+ members_table.create!(
+ id: id, user_id: user_id, source_id: non_existing_record_id, access_level: Gitlab::Access::MAINTAINER,
+ type: "ProjectMember", source_type: "Project", notification_level: 3, member_namespace_id: nil
+ )
end
def create_valid_project_member(id:, user_id:, project:)
- members_table.create!(id: id, user_id: user_id, source_id: project.id, access_level: Gitlab::Access::MAINTAINER,
- type: "ProjectMember", source_type: "Project", member_namespace_id: project.project_namespace_id, notification_level: 3)
+ members_table.create!(
+ id: id,
+ user_id: user_id,
+ source_id: project.id,
+ access_level: Gitlab::Access::MAINTAINER,
+ type: "ProjectMember",
+ source_type: "Project",
+ member_namespace_id: project.project_namespace_id,
+ notification_level: 3
+ )
end
- # rubocop: enable Layout/LineLength
# rubocop: enable RSpec/ScatteredLet
end
diff --git a/spec/lib/gitlab/background_migration/disable_legacy_open_source_licence_for_recent_public_projects_spec.rb b/spec/lib/gitlab/background_migration/disable_legacy_open_source_licence_for_recent_public_projects_spec.rb
index 7edba8cf524..740a90e0494 100644
--- a/spec/lib/gitlab/background_migration/disable_legacy_open_source_licence_for_recent_public_projects_spec.rb
+++ b/spec/lib/gitlab/background_migration/disable_legacy_open_source_licence_for_recent_public_projects_spec.rb
@@ -73,14 +73,15 @@ RSpec.describe Gitlab::BackgroundMigration::DisableLegacyOpenSourceLicenceForRec
let(:project_settings_table) { table(:project_settings) }
subject(:perform_migration) do
- described_class.new(start_id: projects_table.minimum(:id),
- end_id: projects_table.maximum(:id),
- batch_table: :projects,
- batch_column: :id,
- sub_batch_size: 2,
- pause_ms: 0,
- connection: ActiveRecord::Base.connection)
- .perform
+ described_class.new(
+ start_id: projects_table.minimum(:id),
+ end_id: projects_table.maximum(:id),
+ batch_table: :projects,
+ batch_column: :id,
+ sub_batch_size: 2,
+ pause_ms: 0,
+ connection: ActiveRecord::Base.connection
+ ).perform
end
before do
@@ -94,7 +95,7 @@ RSpec.describe Gitlab::BackgroundMigration::DisableLegacyOpenSourceLicenceForRec
end
it 'sets `legacy_open_source_license_available` attribute to false for public projects created after threshold time',
- :aggregate_failures do
+ :aggregate_failures do
record = ActiveRecord::QueryRecorder.new do
expect { perform_migration }
.to not_change { migrated_attribute(project_1.id) }.from(true)
diff --git a/spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_inactive_public_projects_spec.rb b/spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_inactive_public_projects_spec.rb
index f5a2dc91185..953eb09032f 100644
--- a/spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_inactive_public_projects_spec.rb
+++ b/spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_inactive_public_projects_spec.rb
@@ -8,14 +8,15 @@ RSpec.describe Gitlab::BackgroundMigration::DisableLegacyOpenSourceLicenseForIna
let(:project_settings_table) { table(:project_settings) }
subject(:perform_migration) do
- described_class.new(start_id: projects_table.minimum(:id),
- end_id: projects_table.maximum(:id),
- batch_table: :projects,
- batch_column: :id,
- sub_batch_size: 2,
- pause_ms: 0,
- connection: ActiveRecord::Base.connection)
- .perform
+ described_class.new(
+ start_id: projects_table.minimum(:id),
+ end_id: projects_table.maximum(:id),
+ batch_table: :projects,
+ batch_column: :id,
+ sub_batch_size: 2,
+ pause_ms: 0,
+ connection: ActiveRecord::Base.connection
+ ).perform
end
let(:queries) { ActiveRecord::QueryRecorder.new { perform_migration } }
@@ -27,32 +28,28 @@ RSpec.describe Gitlab::BackgroundMigration::DisableLegacyOpenSourceLicenseForIna
let(:project_namespace_5) { namespaces_table.create!(name: 'namespace', path: 'namespace-path-5', type: 'Project') }
let(:project_1) do
- projects_table
- .create!(
+ projects_table.create!(
name: 'proj-1', path: 'path-1', namespace_id: namespace_1.id,
project_namespace_id: project_namespace_2.id, visibility_level: 0
)
end
let(:project_2) do
- projects_table
- .create!(
+ projects_table.create!(
name: 'proj-2', path: 'path-2', namespace_id: namespace_1.id,
project_namespace_id: project_namespace_3.id, visibility_level: 10
)
end
let(:project_3) do
- projects_table
- .create!(
+ projects_table.create!(
name: 'proj-3', path: 'path-3', namespace_id: namespace_1.id,
project_namespace_id: project_namespace_4.id, visibility_level: 20, last_activity_at: '2021-01-01'
)
end
let(:project_4) do
- projects_table
- .create!(
+ projects_table.create!(
name: 'proj-4', path: 'path-4', namespace_id: namespace_1.id,
project_namespace_id: project_namespace_5.id, visibility_level: 20, last_activity_at: '2022-01-01'
)
@@ -66,7 +63,7 @@ RSpec.describe Gitlab::BackgroundMigration::DisableLegacyOpenSourceLicenseForIna
end
it 'sets `legacy_open_source_license_available` attribute to false for inactive, public projects',
- :aggregate_failures do
+ :aggregate_failures do
expect(queries.count).to eq(5)
expect(migrated_attribute(project_1.id)).to be_truthy
diff --git a/spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_no_issues_no_repo_projects_spec.rb b/spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_no_issues_no_repo_projects_spec.rb
index d60874c3159..93913a2742b 100644
--- a/spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_no_issues_no_repo_projects_spec.rb
+++ b/spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_no_issues_no_repo_projects_spec.rb
@@ -3,8 +3,8 @@
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::DisableLegacyOpenSourceLicenseForNoIssuesNoRepoProjects,
- :migration,
- schema: 20220722084543 do
+ :migration,
+ schema: 20220722084543 do
let(:namespaces_table) { table(:namespaces) }
let(:projects_table) { table(:projects) }
let(:project_settings_table) { table(:project_settings) }
@@ -12,18 +12,19 @@ RSpec.describe Gitlab::BackgroundMigration::DisableLegacyOpenSourceLicenseForNoI
let(:issues_table) { table(:issues) }
subject(:perform_migration) do
- described_class.new(start_id: projects_table.minimum(:id),
- end_id: projects_table.maximum(:id),
- batch_table: :projects,
- batch_column: :id,
- sub_batch_size: 2,
- pause_ms: 0,
- connection: ActiveRecord::Base.connection)
- .perform
+ described_class.new(
+ start_id: projects_table.minimum(:id),
+ end_id: projects_table.maximum(:id),
+ batch_table: :projects,
+ batch_column: :id,
+ sub_batch_size: 2,
+ pause_ms: 0,
+ connection: ActiveRecord::Base.connection
+ ).perform
end
it 'sets `legacy_open_source_license_available` to false only for public projects with no issues and no repo',
- :aggregate_failures do
+ :aggregate_failures do
project_with_no_issues_no_repo = create_legacy_license_public_project('project-with-no-issues-no-repo')
project_with_repo = create_legacy_license_public_project('project-with-repo', repo_size: 1)
project_with_issues = create_legacy_license_public_project('project-with-issues', with_issue: true)
@@ -41,13 +42,13 @@ RSpec.describe Gitlab::BackgroundMigration::DisableLegacyOpenSourceLicenseForNoI
def create_legacy_license_public_project(path, repo_size: 0, with_issue: false)
namespace = namespaces_table.create!(name: "namespace-#{path}", path: "namespace-#{path}")
- project_namespace =
- namespaces_table.create!(name: "-project-namespace-#{path}", path: "project-namespace-#{path}", type: 'Project')
- project = projects_table
- .create!(
- name: path, path: path, namespace_id: namespace.id,
- project_namespace_id: project_namespace.id, visibility_level: 20
- )
+ project_namespace = namespaces_table.create!(
+ name: "-project-namespace-#{path}", path: "project-namespace-#{path}", type: 'Project'
+ )
+ project = projects_table.create!(
+ name: path, path: path, namespace_id: namespace.id,
+ project_namespace_id: project_namespace.id, visibility_level: 20
+ )
project_statistics_table.create!(project_id: project.id, namespace_id: namespace.id, repository_size: repo_size)
issues_table.create!(project_id: project.id, namespace_id: project.project_namespace_id) if with_issue
diff --git a/spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_one_member_no_repo_projects_spec.rb b/spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_one_member_no_repo_projects_spec.rb
index 0dba1d7c8a2..285e5ebbee2 100644
--- a/spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_one_member_no_repo_projects_spec.rb
+++ b/spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_one_member_no_repo_projects_spec.rb
@@ -3,8 +3,8 @@
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::DisableLegacyOpenSourceLicenseForOneMemberNoRepoProjects,
- :migration,
- schema: 20220721031446 do
+ :migration,
+ schema: 20220721031446 do
let(:namespaces_table) { table(:namespaces) }
let(:projects_table) { table(:projects) }
let(:project_settings_table) { table(:project_settings) }
@@ -13,18 +13,19 @@ RSpec.describe Gitlab::BackgroundMigration::DisableLegacyOpenSourceLicenseForOne
let(:project_authorizations_table) { table(:project_authorizations) }
subject(:perform_migration) do
- described_class.new(start_id: projects_table.minimum(:id),
- end_id: projects_table.maximum(:id),
- batch_table: :projects,
- batch_column: :id,
- sub_batch_size: 2,
- pause_ms: 0,
- connection: ActiveRecord::Base.connection)
- .perform
+ described_class.new(
+ start_id: projects_table.minimum(:id),
+ end_id: projects_table.maximum(:id),
+ batch_table: :projects,
+ batch_column: :id,
+ sub_batch_size: 2,
+ pause_ms: 0,
+ connection: ActiveRecord::Base.connection
+ ).perform
end
it 'sets `legacy_open_source_license_available` to false only for public projects with 1 member and no repo',
- :aggregate_failures do
+ :aggregate_failures do
project_with_no_repo_one_member = create_legacy_license_public_project('project-with-one-member-no-repo')
project_with_repo_one_member = create_legacy_license_public_project('project-with-repo', repo_size: 1)
project_with_no_repo_two_members = create_legacy_license_public_project('project-with-two-members', members: 2)
@@ -42,13 +43,13 @@ RSpec.describe Gitlab::BackgroundMigration::DisableLegacyOpenSourceLicenseForOne
def create_legacy_license_public_project(path, repo_size: 0, members: 1)
namespace = namespaces_table.create!(name: "namespace-#{path}", path: "namespace-#{path}")
- project_namespace =
- namespaces_table.create!(name: "-project-namespace-#{path}", path: "project-namespace-#{path}", type: 'Project')
- project = projects_table
- .create!(
- name: path, path: path, namespace_id: namespace.id,
- project_namespace_id: project_namespace.id, visibility_level: 20
- )
+ project_namespace = namespaces_table.create!(
+ name: "-project-namespace-#{path}", path: "project-namespace-#{path}", type: 'Project'
+ )
+ project = projects_table.create!(
+ name: path, path: path, namespace_id: namespace.id,
+ project_namespace_id: project_namespace.id, visibility_level: 20
+ )
members.times do |member_id|
user = users_table.create!(email: "user#{member_id}-project-#{project.id}@gitlab.com", projects_limit: 100)
diff --git a/spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_projects_less_than_five_mb_spec.rb b/spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_projects_less_than_five_mb_spec.rb
index a153507837c..fedee9c5068 100644
--- a/spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_projects_less_than_five_mb_spec.rb
+++ b/spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_projects_less_than_five_mb_spec.rb
@@ -3,23 +3,24 @@
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::DisableLegacyOpenSourceLicenseForProjectsLessThanFiveMb,
- :migration,
- schema: 20221018095434,
- feature_category: :groups_and_projects do
+ :migration,
+ schema: 20221018095434,
+ feature_category: :groups_and_projects do
let(:namespaces_table) { table(:namespaces) }
let(:projects_table) { table(:projects) }
let(:project_settings_table) { table(:project_settings) }
let(:project_statistics_table) { table(:project_statistics) }
subject(:perform_migration) do
- described_class.new(start_id: project_settings_table.minimum(:project_id),
- end_id: project_settings_table.maximum(:project_id),
- batch_table: :project_settings,
- batch_column: :project_id,
- sub_batch_size: 2,
- pause_ms: 0,
- connection: ActiveRecord::Base.connection)
- .perform
+ described_class.new(
+ start_id: project_settings_table.minimum(:project_id),
+ end_id: project_settings_table.maximum(:project_id),
+ batch_table: :project_settings,
+ batch_column: :project_id,
+ sub_batch_size: 2,
+ pause_ms: 0,
+ connection: ActiveRecord::Base.connection
+ ).perform
end
it 'sets `legacy_open_source_license_available` to false only for projects less than 5 MiB', :aggregate_failures do
@@ -45,10 +46,12 @@ RSpec.describe Gitlab::BackgroundMigration::DisableLegacyOpenSourceLicenseForPro
def create_legacy_license_project_setting(repo_size:)
path = "path-for-repo-size-#{repo_size}"
namespace = namespaces_table.create!(name: "namespace-#{path}", path: "namespace-#{path}")
- project_namespace =
- namespaces_table.create!(name: "-project-namespace-#{path}", path: "project-namespace-#{path}", type: 'Project')
- project = projects_table
- .create!(name: path, path: path, namespace_id: namespace.id, project_namespace_id: project_namespace.id)
+ project_namespace = namespaces_table.create!(
+ name: "-project-namespace-#{path}", path: "project-namespace-#{path}", type: 'Project'
+ )
+ project = projects_table.create!(
+ name: path, path: path, namespace_id: namespace.id, project_namespace_id: project_namespace.id
+ )
size_in_bytes = 1.megabyte * repo_size
project_statistics_table.create!(project_id: project.id, namespace_id: namespace.id, repository_size: size_in_bytes)
diff --git a/spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_projects_less_than_one_mb_spec.rb b/spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_projects_less_than_one_mb_spec.rb
index 2e6bc2f77ae..cf544c87b31 100644
--- a/spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_projects_less_than_one_mb_spec.rb
+++ b/spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_projects_less_than_one_mb_spec.rb
@@ -3,26 +3,27 @@
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::DisableLegacyOpenSourceLicenseForProjectsLessThanOneMb,
- :migration,
- schema: 20220906074449 do
+ :migration,
+ schema: 20220906074449 do
let(:namespaces_table) { table(:namespaces) }
let(:projects_table) { table(:projects) }
let(:project_settings_table) { table(:project_settings) }
let(:project_statistics_table) { table(:project_statistics) }
subject(:perform_migration) do
- described_class.new(start_id: project_settings_table.minimum(:project_id),
- end_id: project_settings_table.maximum(:project_id),
- batch_table: :project_settings,
- batch_column: :project_id,
- sub_batch_size: 2,
- pause_ms: 0,
- connection: ActiveRecord::Base.connection)
- .perform
+ described_class.new(
+ start_id: project_settings_table.minimum(:project_id),
+ end_id: project_settings_table.maximum(:project_id),
+ batch_table: :project_settings,
+ batch_column: :project_id,
+ sub_batch_size: 2,
+ pause_ms: 0,
+ connection: ActiveRecord::Base.connection
+ ).perform
end
it 'sets `legacy_open_source_license_available` to false only for projects less than 1 MiB',
- :aggregate_failures do
+ :aggregate_failures do
project_setting_1_mb = create_legacy_license_project_setting(repo_size: 1)
project_setting_2_mb = create_legacy_license_project_setting(repo_size: 2)
project_setting_quarter_mb = create_legacy_license_project_setting(repo_size: 0.25)
@@ -43,10 +44,12 @@ RSpec.describe Gitlab::BackgroundMigration::DisableLegacyOpenSourceLicenseForPro
def create_legacy_license_project_setting(repo_size:)
path = "path-for-repo-size-#{repo_size}"
namespace = namespaces_table.create!(name: "namespace-#{path}", path: "namespace-#{path}")
- project_namespace =
- namespaces_table.create!(name: "-project-namespace-#{path}", path: "project-namespace-#{path}", type: 'Project')
- project = projects_table
- .create!(name: path, path: path, namespace_id: namespace.id, project_namespace_id: project_namespace.id)
+ project_namespace = namespaces_table.create!(
+ name: "-project-namespace-#{path}", path: "project-namespace-#{path}", type: 'Project'
+ )
+ project = projects_table.create!(
+ name: path, path: path, namespace_id: namespace.id, project_namespace_id: project_namespace.id
+ )
size_in_bytes = 1.megabyte * repo_size
project_statistics_table.create!(project_id: project.id, namespace_id: namespace.id, repository_size: size_in_bytes)
diff --git a/spec/lib/gitlab/background_migration/expire_o_auth_tokens_spec.rb b/spec/lib/gitlab/background_migration/expire_o_auth_tokens_spec.rb
index cffcda0a2ca..ba3aab03f2a 100644
--- a/spec/lib/gitlab/background_migration/expire_o_auth_tokens_spec.rb
+++ b/spec/lib/gitlab/background_migration/expire_o_auth_tokens_spec.rb
@@ -9,14 +9,15 @@ RSpec.describe Gitlab::BackgroundMigration::ExpireOAuthTokens, :migration, schem
let(:table_name) { 'oauth_access_tokens' }
subject(:perform_migration) do
- described_class.new(start_id: 1,
- end_id: 30,
- batch_table: :oauth_access_tokens,
- batch_column: :id,
- sub_batch_size: 2,
- pause_ms: 0,
- connection: ActiveRecord::Base.connection)
- .perform
+ described_class.new(
+ start_id: 1,
+ end_id: 30,
+ batch_table: :oauth_access_tokens,
+ batch_column: :id,
+ sub_batch_size: 2,
+ pause_ms: 0,
+ connection: ActiveRecord::Base.connection
+ ).perform
end
before do
diff --git a/spec/lib/gitlab/background_migration/fix_incoherent_packages_size_on_project_statistics_spec.rb b/spec/lib/gitlab/background_migration/fix_incoherent_packages_size_on_project_statistics_spec.rb
index f71b54a7eb4..2a53d39b6b1 100644
--- a/spec/lib/gitlab/background_migration/fix_incoherent_packages_size_on_project_statistics_spec.rb
+++ b/spec/lib/gitlab/background_migration/fix_incoherent_packages_size_on_project_statistics_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
# rubocop: disable RSpec/MultipleMemoizedHelpers
RSpec.describe Gitlab::BackgroundMigration::FixIncoherentPackagesSizeOnProjectStatistics,
- feature_category: :package_registry do
+ feature_category: :package_registry do
let(:project_statistics_table) { table(:project_statistics) }
let(:packages_table) { table(:packages_packages) }
let(:package_files_table) { table(:packages_package_files) }
@@ -197,8 +197,8 @@ RSpec.describe Gitlab::BackgroundMigration::FixIncoherentPackagesSizeOnProjectSt
context 'with incoherent packages_size' do
it_behaves_like 'enqueuing a buffered updates',
- incoherent_non_zero_statistics: 195,
- incoherent_zero_statistics: 200
+ incoherent_non_zero_statistics: 195,
+ incoherent_zero_statistics: 200
context 'with updates waiting on redis' do
before do
@@ -207,8 +207,8 @@ RSpec.describe Gitlab::BackgroundMigration::FixIncoherentPackagesSizeOnProjectSt
end
it_behaves_like 'enqueuing a buffered updates',
- incoherent_non_zero_statistics: 195,
- incoherent_zero_statistics: 200
+ incoherent_non_zero_statistics: 195,
+ incoherent_zero_statistics: 200
end
end
diff --git a/spec/lib/gitlab/background_migration/legacy_upload_mover_spec.rb b/spec/lib/gitlab/background_migration/legacy_upload_mover_spec.rb
index 71e9a568370..f4c5cd79863 100644
--- a/spec/lib/gitlab/background_migration/legacy_upload_mover_spec.rb
+++ b/spec/lib/gitlab/background_migration/legacy_upload_mover_spec.rb
@@ -18,9 +18,14 @@ RSpec.describe Gitlab::BackgroundMigration::LegacyUploadMover, :aggregate_failur
let(:legacy_upload) { create_upload(note, filename) }
def create_remote_upload(model, filename)
- create(:upload, :attachment_upload,
- path: "note/attachment/#{model.id}/#{filename}", secret: nil,
- store: ObjectStorage::Store::REMOTE, model: model)
+ create(
+ :upload,
+ :attachment_upload,
+ path: "note/attachment/#{model.id}/#{filename}",
+ secret: nil,
+ store: ObjectStorage::Store::REMOTE,
+ model: model
+ )
end
def create_upload(model, filename, with_file = true)
@@ -147,14 +152,23 @@ RSpec.describe Gitlab::BackgroundMigration::LegacyUploadMover, :aggregate_failur
end
let(:legacy_upload) do
- create(:upload, :with_file, :attachment_upload,
- path: "uploads/-/system/note/attachment/#{note.id}/#{filename}", model: note)
+ create(
+ :upload,
+ :with_file,
+ :attachment_upload,
+ path: "uploads/-/system/note/attachment/#{note.id}/#{filename}",
+ model: note
+ )
end
context 'when the file does not exist for the upload' do
let(:legacy_upload) do
- create(:upload, :attachment_upload,
- path: "uploads/-/system/note/attachment/#{note.id}/#{filename}", model: note)
+ create(
+ :upload,
+ :attachment_upload,
+ path: "uploads/-/system/note/attachment/#{note.id}/#{filename}",
+ model: note
+ )
end
it_behaves_like 'move error'
@@ -162,8 +176,13 @@ RSpec.describe Gitlab::BackgroundMigration::LegacyUploadMover, :aggregate_failur
context 'when the file does not exist on expected path' do
let(:legacy_upload) do
- create(:upload, :attachment_upload, :with_file,
- path: "uploads/-/system/note/attachment/some_part/#{note.id}/#{filename}", model: note)
+ create(
+ :upload,
+ :attachment_upload,
+ :with_file,
+ path: "uploads/-/system/note/attachment/some_part/#{note.id}/#{filename}",
+ model: note
+ )
end
it_behaves_like 'move error'
@@ -171,8 +190,13 @@ RSpec.describe Gitlab::BackgroundMigration::LegacyUploadMover, :aggregate_failur
context 'when the file path does not include system/note/attachment' do
let(:legacy_upload) do
- create(:upload, :attachment_upload, :with_file,
- path: "uploads/-/system#{note.id}/#{filename}", model: note)
+ create(
+ :upload,
+ :attachment_upload,
+ :with_file,
+ path: "uploads/-/system#{note.id}/#{filename}",
+ model: note
+ )
end
it_behaves_like 'move error'
@@ -188,8 +212,14 @@ RSpec.describe Gitlab::BackgroundMigration::LegacyUploadMover, :aggregate_failur
context 'when upload has mount_point nil' do
let(:legacy_upload) do
- create(:upload, :with_file, :attachment_upload,
- path: "uploads/-/system/note/attachment/#{note.id}/#{filename}", model: note, mount_point: nil)
+ create(
+ :upload,
+ :with_file,
+ :attachment_upload,
+ path: "uploads/-/system/note/attachment/#{note.id}/#{filename}",
+ model: note,
+ mount_point: nil
+ )
end
it_behaves_like 'migrates the file correctly', false
diff --git a/spec/lib/gitlab/background_migration/remove_backfilled_job_artifacts_expire_at_spec.rb b/spec/lib/gitlab/background_migration/remove_backfilled_job_artifacts_expire_at_spec.rb
index af8b5240e40..4c989ba9cef 100644
--- a/spec/lib/gitlab/background_migration/remove_backfilled_job_artifacts_expire_at_spec.rb
+++ b/spec/lib/gitlab/background_migration/remove_backfilled_job_artifacts_expire_at_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Gitlab::BackgroundMigration::RemoveBackfilledJobArtifactsExpireAt
describe '#perform' do
let(:job_artifact) { table(:ci_job_artifacts, database: :ci) }
- let(:jobs) { table(:ci_builds, database: :ci) { |model| model.primary_key = :id } }
+ let(:jobs) { table(:p_ci_builds, database: :ci) { |model| model.primary_key = :id } }
let(:test_worker) do
described_class.new(
diff --git a/spec/lib/gitlab/batch_worker_context_spec.rb b/spec/lib/gitlab/batch_worker_context_spec.rb
index 31641f7449e..a0a5bf0cba1 100644
--- a/spec/lib/gitlab/batch_worker_context_spec.rb
+++ b/spec/lib/gitlab/batch_worker_context_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Gitlab::BatchWorkerContext do
subject(:batch_context) do
described_class.new(
- %w(hello world),
+ %w[hello world],
arguments_proc: -> (word) { word },
context_proc: -> (word) { { user: build_stubbed(:user, username: word) } }
)
@@ -13,13 +13,13 @@ RSpec.describe Gitlab::BatchWorkerContext do
describe "#arguments" do
it "returns all the expected arguments in arrays" do
- expect(batch_context.arguments).to eq([%w(hello), %w(world)])
+ expect(batch_context.arguments).to eq([%w[hello], %w[world]])
end
end
describe "#context_for" do
it "returns the correct application context for the arguments" do
- context = batch_context.context_for(%w(world))
+ context = batch_context.context_for(%w[world])
expect(context).to be_a(Gitlab::ApplicationContext)
expect(context.to_lazy_hash[:user].call).to eq("world")
diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
index 517d557d665..d468483661a 100644
--- a/spec/lib/gitlab/bitbucket_import/importer_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
@@ -220,7 +220,7 @@ RSpec.describe Gitlab::BitbucketImport::Importer, :clean_gitlab_redis_cache, fea
subject.execute
expect(subject.errors.count).to eq(1)
- expect(subject.errors.first.keys).to match_array(%i(type iid errors))
+ expect(subject.errors.first.keys).to match_array(%i[type iid errors])
end
end
diff --git a/spec/lib/gitlab/bitbucket_import/importers/issue_importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importers/issue_importer_spec.rb
index 8f79390d2d9..8732c787657 100644
--- a/spec/lib/gitlab/bitbucket_import/importers/issue_importer_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/importers/issue_importer_spec.rb
@@ -99,5 +99,13 @@ RSpec.describe Gitlab::BitbucketImport::Importers::IssueImporter, :clean_gitlab_
importer.execute
end
+
+ it 'increments the issue counter' do
+ expect_next_instance_of(Gitlab::Import::Metrics) do |metrics|
+ expect(metrics).to receive_message_chain(:issues_counter, :increment)
+ end
+
+ importer.execute
+ end
end
end
diff --git a/spec/lib/gitlab/bitbucket_import/importers/issues_importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importers/issues_importer_spec.rb
index a361a9343dd..af5a929683e 100644
--- a/spec/lib/gitlab/bitbucket_import/importers/issues_importer_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/importers/issues_importer_spec.rb
@@ -12,22 +12,37 @@ RSpec.describe Gitlab::BitbucketImport::Importers::IssuesImporter, feature_categ
)
end
+ let(:client) { Bitbucket::Client.new(project.import_data.credentials) }
+
+ before do
+ allow(Bitbucket::Client).to receive(:new).and_return(client)
+ allow(client).to receive(:repo).and_return(Bitbucket::Representation::Repo.new({ 'has_issues' => true }))
+ allow(client).to receive(:last_issue).and_return(Bitbucket::Representation::Issue.new({ 'id' => 2 }))
+ allow(client).to receive(:issues).and_return(
+ [
+ Bitbucket::Representation::Issue.new({ 'id' => 1 }),
+ Bitbucket::Representation::Issue.new({ 'id' => 2 })
+ ],
+ []
+ )
+ end
+
subject(:importer) { described_class.new(project) }
describe '#execute', :clean_gitlab_redis_cache do
- before do
- allow_next_instance_of(Bitbucket::Client) do |client|
- allow(client).to receive(:issues).and_return(
- [
- Bitbucket::Representation::Issue.new({ 'id' => 1 }),
- Bitbucket::Representation::Issue.new({ 'id' => 2 })
- ],
- []
- )
+ context 'when the repo does not have issue tracking enabled' do
+ before do
+ allow(client).to receive(:repo).and_return(Bitbucket::Representation::Repo.new({ 'has_issues' => false }))
+ end
+
+ it 'does not import issues' do
+ expect(Gitlab::BitbucketImport::ImportIssueWorker).not_to receive(:perform_in)
+
+ importer.execute
end
end
- it 'imports each issue in parallel', :aggregate_failures do
+ it 'imports each issue in parallel' do
expect(Gitlab::BitbucketImport::ImportIssueWorker).to receive(:perform_in).twice
waiter = importer.execute
@@ -38,11 +53,15 @@ RSpec.describe Gitlab::BitbucketImport::Importers::IssuesImporter, feature_categ
.to match_array(%w[1 2])
end
+ it 'allocates internal ids' do
+ expect(Issue).to receive(:track_namespace_iid!).with(project.project_namespace, 2)
+
+ importer.execute
+ end
+
context 'when the client raises an error' do
before do
- allow_next_instance_of(Bitbucket::Client) do |client|
- allow(client).to receive(:issues).and_raise(StandardError)
- end
+ allow(client).to receive(:issues).and_raise(StandardError)
end
it 'tracks the failure and does not fail' do
@@ -57,7 +76,7 @@ RSpec.describe Gitlab::BitbucketImport::Importers::IssuesImporter, feature_categ
Gitlab::Cache::Import::Caching.set_add(importer.already_enqueued_cache_key, 1)
end
- it 'does not schedule job for enqueued issues', :aggregate_failures do
+ it 'does not schedule job for enqueued issues' do
expect(Gitlab::BitbucketImport::ImportIssueWorker).to receive(:perform_in).once
waiter = importer.execute
diff --git a/spec/lib/gitlab/bitbucket_import/importers/issues_notes_importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importers/issues_notes_importer_spec.rb
index 043cd7f17b9..a04543b0511 100644
--- a/spec/lib/gitlab/bitbucket_import/importers/issues_notes_importer_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/importers/issues_notes_importer_spec.rb
@@ -4,15 +4,13 @@ require 'spec_helper'
RSpec.describe Gitlab::BitbucketImport::Importers::IssuesNotesImporter, feature_category: :importers do
let_it_be(:project) { create(:project, :import_started) }
- # let_it_be(:merge_request_1) { create(:merge_request, source_project: project) }
- # let_it_be(:merge_request_2) { create(:merge_request, source_project: project, source_branch: 'other-branch') }
let_it_be(:issue_1) { create(:issue, project: project) }
let_it_be(:issue_2) { create(:issue, project: project) }
subject(:importer) { described_class.new(project) }
describe '#execute', :clean_gitlab_redis_cache do
- it 'imports the notes from each issue in parallel', :aggregate_failures do
+ it 'imports the notes from each issue in parallel' do
expect(Gitlab::BitbucketImport::ImportIssueNotesWorker).to receive(:perform_in).twice
waiter = importer.execute
@@ -40,7 +38,7 @@ RSpec.describe Gitlab::BitbucketImport::Importers::IssuesNotesImporter, feature_
Gitlab::Cache::Import::Caching.set_add(importer.already_enqueued_cache_key, 2)
end
- it 'does not schedule job for enqueued issues', :aggregate_failures do
+ it 'does not schedule job for enqueued issues' do
expect(Gitlab::BitbucketImport::ImportIssueNotesWorker).to receive(:perform_in).once
waiter = importer.execute
diff --git a/spec/lib/gitlab/bitbucket_import/importers/pull_request_importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importers/pull_request_importer_spec.rb
index 2eca6bb47d6..1f36a353724 100644
--- a/spec/lib/gitlab/bitbucket_import/importers/pull_request_importer_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/importers/pull_request_importer_spec.rb
@@ -162,5 +162,13 @@ RSpec.describe Gitlab::BitbucketImport::Importers::PullRequestImporter, :clean_g
importer.execute
end
+
+ it 'increments the merge requests counter' do
+ expect_next_instance_of(Gitlab::Import::Metrics) do |metrics|
+ expect(metrics).to receive_message_chain(:merge_requests_counter, :increment)
+ end
+
+ importer.execute
+ end
end
end
diff --git a/spec/lib/gitlab/bitbucket_import/importers/pull_request_notes_importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importers/pull_request_notes_importer_spec.rb
index 4a30f225d66..332f6e5bd03 100644
--- a/spec/lib/gitlab/bitbucket_import/importers/pull_request_notes_importer_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/importers/pull_request_notes_importer_spec.rb
@@ -2,9 +2,9 @@
require 'spec_helper'
-RSpec.describe Gitlab::BitbucketImport::Importers::PullRequestNotesImporter, feature_category: :importers do
+RSpec.describe Gitlab::BitbucketImport::Importers::PullRequestNotesImporter, :clean_gitlab_redis_cache, feature_category: :importers do
let_it_be(:project) do
- create(:project, :import_started,
+ create(:project, :repository, :import_started,
import_data_attributes: {
credentials: { 'base_uri' => 'http://bitbucket.org/', 'user' => 'bitbucket', 'password' => 'password' }
}
@@ -12,28 +12,216 @@ RSpec.describe Gitlab::BitbucketImport::Importers::PullRequestNotesImporter, fea
end
let_it_be(:merge_request) { create(:merge_request, source_project: project) }
-
+ let_it_be(:merge_request_diff) { create(:merge_request_diff, :external, merge_request: merge_request) }
+ let_it_be(:bitbucket_user) { create(:user) }
+ let_it_be(:identity) { create(:identity, user: bitbucket_user, extern_uid: 'bitbucket_user', provider: :bitbucket) }
let(:hash) { { iid: merge_request.iid } }
- let(:importer_helper) { Gitlab::BitbucketImport::Importer.new(project) }
+ let(:client) { Bitbucket::Client.new({}) }
+ let(:ref_converter) { Gitlab::BitbucketImport::RefConverter.new(project) }
+ let(:user_finder) { Gitlab::BitbucketImport::UserFinder.new(project) }
+ let(:note_body) { 'body' }
+ let(:comments) { [Bitbucket::Representation::PullRequestComment.new(note_hash)] }
+ let(:created_at) { Date.today - 2.days }
+ let(:updated_at) { Date.today }
+ let(:note_hash) do
+ {
+ 'id' => 12,
+ 'user' => { 'nickname' => 'bitbucket_user' },
+ 'content' => { 'raw' => note_body },
+ 'created_on' => created_at,
+ 'updated_on' => updated_at
+ }
+ end
subject(:importer) { described_class.new(project, hash) }
before do
- allow(Gitlab::BitbucketImport::Importer).to receive(:new).and_return(importer_helper)
+ allow(Bitbucket::Client).to receive(:new).and_return(client)
+ allow(Gitlab::BitbucketImport::RefConverter).to receive(:new).and_return(ref_converter)
+ allow(Gitlab::BitbucketImport::UserFinder).to receive(:new).and_return(user_finder)
+ allow(client).to receive(:pull_request_comments).and_return(comments)
end
describe '#execute' do
- it 'calls Importer.import_pull_request_comments' do
- expect(importer_helper).to receive(:import_pull_request_comments).once
+ context 'for standalone pr comments' do
+ it 'calls RefConverter' do
+ expect(ref_converter).to receive(:convert_note).once.and_call_original
+
+ importer.execute
+ end
+
+ it 'creates a note with the correct attributes' do
+ expect { importer.execute }.to change { merge_request.notes.count }.from(0).to(1)
+
+ note = merge_request.notes.first
+
+ expect(note.note).to eq(note_body)
+ expect(note.author).to eq(bitbucket_user)
+ expect(note.created_at).to eq(created_at)
+ expect(note.updated_at).to eq(updated_at)
+ end
+
+ context 'when the author does not have a bitbucket identity' do
+ before do
+ identity.update!(provider: :github)
+ end
+
+ it 'sets the author to the project creator and adds the author to the note' do
+ importer.execute
+
+ note = merge_request.notes.first
+
+ expect(note.author).to eq(project.creator)
+ expect(note.note).to eq("*Created by: bitbucket_user*\n\nbody")
+ end
+ end
+
+ context 'when the note is deleted' do
+ let(:note_hash) do
+ {
+ 'id' => 12,
+ 'user' => { 'nickname' => 'bitbucket_user' },
+ 'content' => { 'raw' => note_body },
+ 'deleted' => true,
+ 'created_on' => created_at,
+ 'updated_on' => updated_at
+ }
+ end
+
+ it 'does not create a note' do
+ expect { importer.execute }.not_to change { merge_request.notes.count }
+ end
+ end
+ end
+
+ context 'for threaded inline comments' do
+ let(:path) { project.repository.commit.raw_diffs.first.new_path }
+ let(:reply_body) { 'Some reply' }
+ let(:comments) do
+ [
+ Bitbucket::Representation::PullRequestComment.new(pr_comment_1),
+ Bitbucket::Representation::PullRequestComment.new(pr_comment_2)
+ ]
+ end
- importer.execute
+ let(:pr_comment_1) do
+ {
+ 'id' => 14,
+ 'inline' => {
+ 'path' => path,
+ 'from' => nil,
+ 'to' => 1
+ },
+ 'parent' => { 'id' => 13 },
+ 'user' => { 'nickname' => 'bitbucket_user' },
+ 'content' => { 'raw' => reply_body },
+ 'created_on' => created_at,
+ 'updated_on' => updated_at
+ }
+ end
+
+ let(:pr_comment_2) do
+ {
+ 'id' => 13,
+ 'inline' => {
+ 'path' => path,
+ 'from' => nil,
+ 'to' => 1
+ },
+ 'user' => { 'nickname' => 'non_existent_user' },
+ 'content' => { 'raw' => note_body },
+ 'created_on' => created_at,
+ 'updated_on' => updated_at
+ }
+ end
+
+ it 'creates notes in the correct position with the right attributes' do
+ expect { importer.execute }.to change { merge_request.notes.count }.from(0).to(2)
+
+ expect(merge_request.notes.map(&:discussion_id).uniq.count).to eq(1)
+
+ notes = merge_request.notes.order(:id).to_a
+
+ start_note = notes.first
+ expect(start_note).to be_a(DiffNote)
+ expect(start_note.note).to eq("*Created by: non_existent_user*\n\n#{note_body}")
+ expect(start_note.author).to eq(project.creator)
+
+ reply_note = notes.last
+ expect(reply_note).to be_a(DiffNote)
+ expect(reply_note.note).to eq(reply_body)
+ expect(reply_note.author).to eq(bitbucket_user)
+ end
+
+ context 'when the comments are not part of the diff' do
+ let(:pr_comment_1) do
+ {
+ 'id' => 14,
+ 'inline' => {
+ 'path' => path,
+ 'from' => nil,
+ 'to' => nil
+ },
+ 'parent' => { 'id' => 13 },
+ 'user' => { 'nickname' => 'bitbucket_user' },
+ 'content' => { 'raw' => reply_body },
+ 'created_on' => created_at,
+ 'updated_on' => updated_at
+ }
+ end
+
+ let(:pr_comment_2) do
+ {
+ 'id' => 13,
+ 'inline' => {
+ 'path' => path,
+ 'from' => nil,
+ 'to' => nil
+ },
+ 'user' => { 'nickname' => 'bitbucket_user' },
+ 'content' => { 'raw' => note_body },
+ 'created_on' => created_at,
+ 'updated_on' => updated_at
+ }
+ end
+
+ it 'creates them as normal notes' do
+ expect { importer.execute }.to change { merge_request.notes.count }.from(0).to(2)
+
+ notes = merge_request.notes.order(:id).to_a
+
+ first_note = notes.first
+ expect(first_note).not_to be_a(DiffNote)
+ expect(first_note.note).to eq("*Comment on*\n\n#{note_body}")
+ expect(first_note.author).to eq(bitbucket_user)
+
+ second_note = notes.last
+ expect(second_note).not_to be_a(DiffNote)
+ expect(second_note.note).to eq("*Comment on*\n\n#{reply_body}")
+ expect(second_note.author).to eq(bitbucket_user)
+ end
+ end
+
+ context 'when an error is raised for one note' do
+ before do
+ allow(user_finder).to receive(:gitlab_user_id).and_call_original
+ allow(user_finder).to receive(:gitlab_user_id).with(project, 'bitbucket_user').and_raise(StandardError)
+ end
+
+ it 'tracks the error and continues to import other notes' do
+ expect(Gitlab::ErrorTracking).to receive(:log_exception)
+ .with(anything, hash_including(comment_id: 14)).and_call_original
+
+ expect { importer.execute }.to change { merge_request.notes.count }.from(0).to(1)
+ end
+ end
end
context 'when the merge request does not exist' do
let(:hash) { { iid: 'nonexistent' } }
- it 'does not call Importer.import_pull_request_comments' do
- expect(importer_helper).not_to receive(:import_pull_request_comments)
+ it 'does not call #import_pull_request_comments' do
+ expect(importer).not_to receive(:import_pull_request_comments)
importer.execute
end
@@ -46,8 +234,8 @@ RSpec.describe Gitlab::BitbucketImport::Importers::PullRequestNotesImporter, fea
merge_request.update!(source_project: another_project, target_project: another_project)
end
- it 'does not call Importer.import_pull_request_comments' do
- expect(importer_helper).not_to receive(:import_pull_request_comments)
+ it 'does not call #import_pull_request_comments' do
+ expect(importer).not_to receive(:import_pull_request_comments)
importer.execute
end
@@ -55,7 +243,7 @@ RSpec.describe Gitlab::BitbucketImport::Importers::PullRequestNotesImporter, fea
context 'when an error is raised' do
before do
- allow(importer_helper).to receive(:import_pull_request_comments).and_raise(StandardError)
+ allow(importer).to receive(:import_pull_request_comments).and_raise(StandardError)
end
it 'tracks the failure and does not fail' do
diff --git a/spec/lib/gitlab/bitbucket_import/importers/pull_requests_importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importers/pull_requests_importer_spec.rb
index 46bf099de0c..eba7ec92aba 100644
--- a/spec/lib/gitlab/bitbucket_import/importers/pull_requests_importer_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/importers/pull_requests_importer_spec.rb
@@ -28,7 +28,7 @@ RSpec.describe Gitlab::BitbucketImport::Importers::PullRequestsImporter, feature
end
end
- it 'imports each pull request in parallel', :aggregate_failures do
+ it 'imports each pull request in parallel' do
expect(Gitlab::BitbucketImport::ImportPullRequestWorker).to receive(:perform_in).exactly(3).times
waiter = importer.execute
@@ -58,7 +58,7 @@ RSpec.describe Gitlab::BitbucketImport::Importers::PullRequestsImporter, feature
Gitlab::Cache::Import::Caching.set_add(importer.already_enqueued_cache_key, 1)
end
- it 'does not schedule job for enqueued pull requests', :aggregate_failures do
+ it 'does not schedule job for enqueued pull requests' do
expect(Gitlab::BitbucketImport::ImportPullRequestWorker).to receive(:perform_in).twice
waiter = importer.execute
diff --git a/spec/lib/gitlab/bitbucket_import/importers/pull_requests_notes_importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importers/pull_requests_notes_importer_spec.rb
index c44fc259c3b..78a08accf82 100644
--- a/spec/lib/gitlab/bitbucket_import/importers/pull_requests_notes_importer_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/importers/pull_requests_notes_importer_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe Gitlab::BitbucketImport::Importers::PullRequestsNotesImporter, fe
subject(:importer) { described_class.new(project) }
describe '#execute', :clean_gitlab_redis_cache do
- it 'imports the notes from each merge request in parallel', :aggregate_failures do
+ it 'imports the notes from each merge request in parallel' do
expect(Gitlab::BitbucketImport::ImportPullRequestNotesWorker).to receive(:perform_in).twice
waiter = importer.execute
@@ -38,7 +38,7 @@ RSpec.describe Gitlab::BitbucketImport::Importers::PullRequestsNotesImporter, fe
Gitlab::Cache::Import::Caching.set_add(importer.already_enqueued_cache_key, 2)
end
- it 'does not schedule job for enqueued merge requests', :aggregate_failures do
+ it 'does not schedule job for enqueued merge requests' do
expect(Gitlab::BitbucketImport::ImportPullRequestNotesWorker).to receive(:perform_in).once
waiter = importer.execute
diff --git a/spec/lib/gitlab/bitbucket_import/importers/repository_importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importers/repository_importer_spec.rb
index 1caf0b884c2..9e458780c78 100644
--- a/spec/lib/gitlab/bitbucket_import/importers/repository_importer_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/importers/repository_importer_spec.rb
@@ -7,6 +7,14 @@ RSpec.describe Gitlab::BitbucketImport::Importers::RepositoryImporter, feature_c
subject(:importer) { described_class.new(project) }
+ before do
+ allow_next_instance_of(Bitbucket::Client) do |client|
+ allow(client).to receive(:repo).and_return(Bitbucket::Representation::Repo.new(
+ { 'mainbranch' => { 'name' => 'develop' } }
+ ))
+ end
+ end
+
describe '#execute' do
context 'when repository is empty' do
it 'imports the repository' do
@@ -17,6 +25,15 @@ RSpec.describe Gitlab::BitbucketImport::Importers::RepositoryImporter, feature_c
importer.execute
end
+
+ it 'sets the default branch' do
+ allow(project.repository).to receive(:import_repository)
+ allow(project.repository).to receive(:fetch_as_mirror)
+
+ expect(project).to receive(:change_head).with('develop')
+
+ importer.execute
+ end
end
context 'when repository is not empty' do
diff --git a/spec/lib/gitlab/bullet/exclusions_spec.rb b/spec/lib/gitlab/bullet/exclusions_spec.rb
index ccedfee28c7..2cb824674dc 100644
--- a/spec/lib/gitlab/bullet/exclusions_spec.rb
+++ b/spec/lib/gitlab/bullet/exclusions_spec.rb
@@ -3,7 +3,7 @@
require 'fast_spec_helper'
require 'tempfile'
-RSpec.describe Gitlab::Bullet::Exclusions, feature_category: :application_performance do
+RSpec.describe Gitlab::Bullet::Exclusions, feature_category: :cloud_connector do
let(:config_file) do
file = Tempfile.new('bullet.yml')
File.basename(file)
diff --git a/spec/lib/gitlab/cache_spec.rb b/spec/lib/gitlab/cache_spec.rb
index 67c70a77880..92a5ea7bdfb 100644
--- a/spec/lib/gitlab/cache_spec.rb
+++ b/spec/lib/gitlab/cache_spec.rb
@@ -28,7 +28,7 @@ RSpec.describe Gitlab::Cache, :request_store do
end
describe '.delete' do
- let(:key) { %w{a cache key} }
+ let(:key) { %w[a cache key] }
subject(:delete) { described_class.delete(key) }
diff --git a/spec/lib/gitlab/ci/ansi2html_spec.rb b/spec/lib/gitlab/ci/ansi2html_spec.rb
index 30359a7170f..2990599f840 100644
--- a/spec/lib/gitlab/ci/ansi2html_spec.rb
+++ b/spec/lib/gitlab/ci/ansi2html_spec.rb
@@ -227,7 +227,7 @@ RSpec.describe Gitlab::Ci::Ansi2html do
text = "#{section_start}Some text#{section_end}"
class_name_start = section_start.gsub("\033[0K", '').gsub('<', '&lt;')
class_name_end = section_end.gsub("\033[0K", '').gsub('<', '&lt;')
- html = %{<span>#{class_name_start}Some text#{class_name_end}</span>}
+ html = %(<span>#{class_name_start}Some text#{class_name_end}</span>)
expect(convert_html(text)).to eq(html)
end
@@ -238,9 +238,9 @@ RSpec.describe Gitlab::Ci::Ansi2html do
it 'prints light red' do
text = "#{section_start}\e[91mHello\e[0m\nLine 1\nLine 2\nLine 3\n#{section_end}"
- header = %{<span class="term-fg-l-red section section-header js-s-#{class_name(section_name)}">Hello</span>}
- line_break = %{<span class="section section-header js-s-#{class_name(section_name)}"><br/></span>}
- output_line = %{<span class="section line js-s-#{class_name(section_name)}">Line 1<br/>Line 2<br/>Line 3<br/></span>}
+ header = %(<span class="term-fg-l-red section section-header js-s-#{class_name(section_name)}">Hello</span>)
+ line_break = %(<span class="section section-header js-s-#{class_name(section_name)}"><br/></span>)
+ output_line = %(<span class="section line js-s-#{class_name(section_name)}">Line 1<br/>Line 2<br/>Line 3<br/></span>)
html = "#{section_start_html}#{header}#{line_break}#{output_line}#{section_end_html}"
expect(convert_html(text)).to eq(html)
diff --git a/spec/lib/gitlab/ci/ansi2json/line_spec.rb b/spec/lib/gitlab/ci/ansi2json/line_spec.rb
index b8563bb1d1c..475a54b275d 100644
--- a/spec/lib/gitlab/ci/ansi2json/line_spec.rb
+++ b/spec/lib/gitlab/ci/ansi2json/line_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Ansi2json::Line do
+RSpec.describe Gitlab::Ci::Ansi2json::Line, feature_category: :continuous_integration do
let(:offset) { 0 }
let(:style) { Gitlab::Ci::Ansi2json::Style.new }
@@ -75,6 +75,14 @@ RSpec.describe Gitlab::Ci::Ansi2json::Line do
end
end
+ describe '#set_as_section_footer' do
+ it 'change the section_footer to true' do
+ expect { subject.set_as_section_footer }
+ .to change { subject.section_footer }
+ .to be_truthy
+ end
+ end
+
describe '#set_section_duration' do
using RSpec::Parameterized::TableSyntax
@@ -178,6 +186,23 @@ RSpec.describe Gitlab::Ci::Ansi2json::Line do
expect(subject.to_h).to eq(result)
end
end
+
+ context 'when section footer is set' do
+ before do
+ subject.set_as_section_footer
+ end
+
+ it 'serializes the attributes set' do
+ result = {
+ offset: 0,
+ content: [{ text: 'some data', style: 'term-bold' }],
+ section: 'section_2',
+ section_footer: true
+ }
+
+ expect(subject.to_h).to eq(result)
+ end
+ end
end
context 'when there are no sections' do
diff --git a/spec/lib/gitlab/ci/ansi2json/state_spec.rb b/spec/lib/gitlab/ci/ansi2json/state_spec.rb
index 8dd4092f3d8..07e6579829a 100644
--- a/spec/lib/gitlab/ci/ansi2json/state_spec.rb
+++ b/spec/lib/gitlab/ci/ansi2json/state_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe Gitlab::Ci::Ansi2json::State, feature_category: :continuous_integ
state.offset = 1
state.new_line!(style: { fg: 'some-fg', bg: 'some-bg', mask: 1234 })
state.set_last_line_offset
- state.open_section('hello', 111, {})
+ state.open_section('hello', 100, {})
end
end
@@ -24,7 +24,7 @@ RSpec.describe Gitlab::Ci::Ansi2json::State, feature_category: :continuous_integ
fg: 'some-fg',
mask: 1234
})
- expect(new_state.open_sections).to eq({ 'hello' => 111 })
+ expect(new_state.open_sections).to eq({ 'hello' => 100 })
end
it 'ignores unsigned prior state', :aggregate_failures do
@@ -44,6 +44,23 @@ RSpec.describe Gitlab::Ci::Ansi2json::State, feature_category: :continuous_integ
expect(new_state.open_sections).to eq({})
end
+ it 'opens and closes a section', :aggregate_failures do
+ new_state = described_class.new('', 1000)
+
+ new_state.new_line!(style: {})
+ new_state.open_section('hello', 100, {})
+
+ expect(new_state.current_line.section_header).to eq(true)
+ expect(new_state.current_line.section_footer).to eq(false)
+
+ new_state.new_line!(style: {})
+ new_state.close_section('hello', 101)
+
+ expect(new_state.current_line.section_header).to eq(false)
+ expect(new_state.current_line.section_duration).to eq('00:01')
+ expect(new_state.current_line.section_footer).to eq(true)
+ end
+
it 'ignores bad input', :aggregate_failures do
expect(::Gitlab::AppLogger).to(
receive(:warn).with(
diff --git a/spec/lib/gitlab/ci/ansi2json_spec.rb b/spec/lib/gitlab/ci/ansi2json_spec.rb
index 98fca40e8ea..23be3209171 100644
--- a/spec/lib/gitlab/ci/ansi2json_spec.rb
+++ b/spec/lib/gitlab/ci/ansi2json_spec.rb
@@ -145,6 +145,7 @@ RSpec.describe Gitlab::Ci::Ansi2json, feature_category: :continuous_integration
offset: 63,
content: [],
section_duration: '01:03',
+ section_footer: true,
section: 'prepare-script'
}
])
@@ -163,7 +164,8 @@ RSpec.describe Gitlab::Ci::Ansi2json, feature_category: :continuous_integration
offset: 56,
content: [],
section: 'prepare-script',
- section_duration: '01:03'
+ section_duration: '01:03',
+ section_footer: true
}
])
end
@@ -181,7 +183,8 @@ RSpec.describe Gitlab::Ci::Ansi2json, feature_category: :continuous_integration
offset: 49,
content: [],
section: 'prepare-script',
- section_duration: '01:03'
+ section_duration: '01:03',
+ section_footer: true
},
{
offset: 91,
@@ -262,7 +265,8 @@ RSpec.describe Gitlab::Ci::Ansi2json, feature_category: :continuous_integration
offset: 75,
content: [],
section: 'prepare-script',
- section_duration: '01:03'
+ section_duration: '01:03',
+ section_footer: true
}
])
end
@@ -300,7 +304,8 @@ RSpec.describe Gitlab::Ci::Ansi2json, feature_category: :continuous_integration
offset: 106,
content: [],
section: 'prepare-script-nested',
- section_duration: '00:02'
+ section_duration: '00:02',
+ section_footer: true
},
{
offset: 155,
@@ -311,7 +316,8 @@ RSpec.describe Gitlab::Ci::Ansi2json, feature_category: :continuous_integration
offset: 158,
content: [],
section: 'prepare-script',
- section_duration: '01:03'
+ section_duration: '01:03',
+ section_footer: true
},
{
offset: 200,
@@ -345,13 +351,15 @@ RSpec.describe Gitlab::Ci::Ansi2json, feature_category: :continuous_integration
offset: 115,
content: [],
section: 'prepare-script-nested',
- section_duration: '00:02'
+ section_duration: '00:02',
+ section_footer: true
},
{
offset: 164,
content: [],
section: 'prepare-script',
- section_duration: '01:03'
+ section_duration: '01:03',
+ section_footer: true
}
])
end
@@ -378,7 +386,8 @@ RSpec.describe Gitlab::Ci::Ansi2json, feature_category: :continuous_integration
offset: 83,
content: [],
section: 'prepare-script',
- section_duration: '01:03'
+ section_duration: '01:03',
+ section_footer: true
}
])
end
@@ -554,7 +563,8 @@ RSpec.describe Gitlab::Ci::Ansi2json, feature_category: :continuous_integration
offset: 77,
content: [],
section: 'prepare-script',
- section_duration: '01:03'
+ section_duration: '01:03',
+ section_footer: true
}
]
end
diff --git a/spec/lib/gitlab/ci/badge/pipeline/status_spec.rb b/spec/lib/gitlab/ci/badge/pipeline/status_spec.rb
index 45d0d781090..d6b59d0da64 100644
--- a/spec/lib/gitlab/ci/badge/pipeline/status_spec.rb
+++ b/spec/lib/gitlab/ci/badge/pipeline/status_spec.rb
@@ -117,10 +117,7 @@ RSpec.describe Gitlab::Ci::Badge::Pipeline::Status do
end
def create_pipeline(project, sha, branch)
- pipeline = create(:ci_empty_pipeline,
- project: project,
- sha: sha,
- ref: branch)
+ pipeline = create(:ci_empty_pipeline, project: project, sha: sha, ref: branch)
create(:ci_build, pipeline: pipeline, stage: 'notify')
end
diff --git a/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb b/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb
index 7b35c9ba483..6cd9432c6c5 100644
--- a/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb
+++ b/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb
@@ -69,9 +69,11 @@ RSpec.describe Gitlab::Ci::Build::Artifacts::Metadata::Entry do
it { is_expected.to all(be_an_instance_of(described_class)) }
it do
- is_expected.to contain_exactly entry('path/dir_1/file_1'),
- entry('path/dir_1/file_b'),
- entry('path/dir_1/subdir/')
+ is_expected.to contain_exactly(
+ entry('path/dir_1/file_1'),
+ entry('path/dir_1/file_b'),
+ entry('path/dir_1/subdir/')
+ )
end
end
@@ -82,8 +84,10 @@ RSpec.describe Gitlab::Ci::Build::Artifacts::Metadata::Entry do
it { is_expected.to all(be_an_instance_of(described_class)) }
it do
- is_expected.to contain_exactly entry('path/dir_1/file_1'),
- entry('path/dir_1/file_b')
+ is_expected.to contain_exactly(
+ entry('path/dir_1/file_1'),
+ entry('path/dir_1/file_b')
+ )
end
end
@@ -103,8 +107,10 @@ RSpec.describe Gitlab::Ci::Build::Artifacts::Metadata::Entry do
it { is_expected.to all(be_an_instance_of(described_class)) }
it do
- is_expected.to contain_exactly entry('path/dir_1/subdir/'),
- entry('path/')
+ is_expected.to contain_exactly(
+ entry('path/dir_1/subdir/'),
+ entry('path/')
+ )
end
end
diff --git a/spec/lib/gitlab/ci/build/context/build_spec.rb b/spec/lib/gitlab/ci/build/context/build_spec.rb
index fae02e140f2..9fdb4ee9393 100644
--- a/spec/lib/gitlab/ci/build/context/build_spec.rb
+++ b/spec/lib/gitlab/ci/build/context/build_spec.rb
@@ -41,16 +41,6 @@ RSpec.describe Gitlab::Ci::Build::Context::Build, feature_category: :pipeline_co
it { expect(context.variables).to be_instance_of(Gitlab::Ci::Variables::Collection) }
it_behaves_like 'variables collection'
-
- context 'with FF disabled' do
- before do
- stub_feature_flags(reduced_build_attributes_list_for_rules: false)
- end
-
- it { expect(context.variables).to be_instance_of(Gitlab::Ci::Variables::Collection) }
-
- it_behaves_like 'variables collection'
- end
end
describe '#variables_hash' do
@@ -59,15 +49,5 @@ RSpec.describe Gitlab::Ci::Build::Context::Build, feature_category: :pipeline_co
it { expect(context.variables_hash).to be_instance_of(ActiveSupport::HashWithIndifferentAccess) }
it_behaves_like 'variables collection'
-
- context 'with FF disabled' do
- before do
- stub_feature_flags(reduced_build_attributes_list_for_rules: false)
- end
-
- it { expect(context.variables_hash).to be_instance_of(ActiveSupport::HashWithIndifferentAccess) }
-
- it_behaves_like 'variables collection'
- end
end
end
diff --git a/spec/lib/gitlab/ci/build/hook_spec.rb b/spec/lib/gitlab/ci/build/hook_spec.rb
index 6c9175b4260..da9a680f110 100644
--- a/spec/lib/gitlab/ci/build/hook_spec.rb
+++ b/spec/lib/gitlab/ci/build/hook_spec.rb
@@ -4,8 +4,10 @@ require 'spec_helper'
RSpec.describe Gitlab::Ci::Build::Hook, feature_category: :pipeline_composition do
let_it_be(:build1) do
- FactoryBot.build(:ci_build,
- options: { hooks: { pre_get_sources_script: ["echo 'hello pre_get_sources_script'"] } })
+ build(
+ :ci_build,
+ options: { hooks: { pre_get_sources_script: ["echo 'hello pre_get_sources_script'"] } }
+ )
end
describe '.from_hooks' do
diff --git a/spec/lib/gitlab/ci/build/policy/changes_spec.rb b/spec/lib/gitlab/ci/build/policy/changes_spec.rb
index 5d5a212b9a5..00e44650d44 100644
--- a/spec/lib/gitlab/ci/build/policy/changes_spec.rb
+++ b/spec/lib/gitlab/ci/build/policy/changes_spec.rb
@@ -8,11 +8,14 @@ RSpec.describe Gitlab::Ci::Build::Policy::Changes do
describe '#satisfied_by?' do
describe 'paths matching' do
let(:pipeline) do
- build(:ci_empty_pipeline, project: project,
- ref: 'master',
- source: :push,
- sha: '1234abcd',
- before_sha: '0123aabb')
+ build(
+ :ci_empty_pipeline,
+ project: project,
+ ref: 'master',
+ source: :push,
+ sha: '1234abcd',
+ before_sha: '0123aabb'
+ )
end
let(:ci_build) do
@@ -92,11 +95,14 @@ RSpec.describe Gitlab::Ci::Build::Policy::Changes do
let_it_be(:project) { create(:project, :repository) }
let(:pipeline) do
- create(:ci_empty_pipeline, project: project,
- ref: 'master',
- source: :push,
- sha: '498214d',
- before_sha: '281d3a7')
+ create(
+ :ci_empty_pipeline,
+ project: project,
+ ref: 'master',
+ source: :push,
+ sha: '498214d',
+ before_sha: '281d3a7'
+ )
end
let(:build) do
@@ -122,12 +128,15 @@ RSpec.describe Gitlab::Ci::Build::Policy::Changes do
let_it_be(:project) { create(:project, :repository) }
let(:pipeline) do
- create(:ci_empty_pipeline, project: project,
- ref: 'feature',
- source: source,
- sha: '0b4bc9a4',
- before_sha: Gitlab::Git::BLANK_SHA,
- merge_request: merge_request)
+ create(
+ :ci_empty_pipeline,
+ project: project,
+ ref: 'feature',
+ source: source,
+ sha: '0b4bc9a4',
+ before_sha: Gitlab::Git::BLANK_SHA,
+ merge_request: merge_request
+ )
end
let(:ci_build) do
@@ -140,11 +149,13 @@ RSpec.describe Gitlab::Ci::Build::Policy::Changes do
let(:source) { :merge_request_event }
let(:merge_request) do
- create(:merge_request,
- source_project: project,
- source_branch: 'feature',
- target_project: project,
- target_branch: 'master')
+ create(
+ :merge_request,
+ source_project: project,
+ source_branch: 'feature',
+ target_project: project,
+ target_branch: 'master'
+ )
end
it 'is satified by changes in the merge request' do
diff --git a/spec/lib/gitlab/ci/build/policy/variables_spec.rb b/spec/lib/gitlab/ci/build/policy/variables_spec.rb
index e560f1c2b5a..1073df60d4a 100644
--- a/spec/lib/gitlab/ci/build/policy/variables_spec.rb
+++ b/spec/lib/gitlab/ci/build/policy/variables_spec.rb
@@ -104,23 +104,32 @@ RSpec.describe Gitlab::Ci::Build::Policy::Variables do
context 'when using project ci variables in environment scope' do
let(:ci_build) do
- build(:ci_build, pipeline: pipeline,
- project: project,
- ref: 'master',
- stage: 'review',
- environment: 'test/$CI_JOB_STAGE/1',
- ci_stage: build(:ci_stage, name: 'review', project: project, pipeline: pipeline))
+ build(
+ :ci_build,
+ pipeline: pipeline,
+ project: project,
+ ref: 'master',
+ stage: 'review',
+ environment: 'test/$CI_JOB_STAGE/1',
+ ci_stage: build(:ci_stage, name: 'review', project: project, pipeline: pipeline)
+ )
end
before do
- create(:ci_variable, project: project,
- key: 'SCOPED_VARIABLE',
- value: 'my-value-1')
-
- create(:ci_variable, project: project,
- key: 'SCOPED_VARIABLE',
- value: 'my-value-2',
- environment_scope: 'test/review/*')
+ create(
+ :ci_variable,
+ project: project,
+ key: 'SCOPED_VARIABLE',
+ value: 'my-value-1'
+ )
+
+ create(
+ :ci_variable,
+ project: project,
+ key: 'SCOPED_VARIABLE',
+ value: 'my-value-2',
+ environment_scope: 'test/review/*'
+ )
end
it 'is satisfied by scoped variable match' do
diff --git a/spec/lib/gitlab/ci/build/rules/rule/clause/changes_spec.rb b/spec/lib/gitlab/ci/build/rules/rule/clause/changes_spec.rb
index a22aa30304b..1fe54b9f2d5 100644
--- a/spec/lib/gitlab/ci/build/rules/rule/clause/changes_spec.rb
+++ b/spec/lib/gitlab/ci/build/rules/rule/clause/changes_spec.rb
@@ -95,8 +95,11 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Changes do
context 'when using compare_to' do
let_it_be(:project) do
- create(:project, :custom_repo,
- files: { 'README.md' => 'readme' })
+ create(
+ :project,
+ :custom_repo,
+ files: { 'README.md' => 'readme' }
+ )
end
let_it_be(:user) { project.owner }
diff --git a/spec/lib/gitlab/ci/config/entry/bridge_spec.rb b/spec/lib/gitlab/ci/config/entry/bridge_spec.rb
index 567ffa68836..6e6b9d949c5 100644
--- a/spec/lib/gitlab/ci/config/entry/bridge_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/bridge_spec.rb
@@ -101,14 +101,16 @@ RSpec.describe Gitlab::Ci::Config::Entry::Bridge do
describe '#value' do
it 'is returns a bridge job configuration' do
- expect(subject.value).to eq(name: :my_bridge,
- trigger: { project: 'some/project' },
- ignore: false,
- stage: 'test',
- only: { refs: %w[branches tags] },
- job_variables: {},
- root_variables_inheritance: true,
- scheduling_type: :stage)
+ expect(subject.value).to eq(
+ name: :my_bridge,
+ trigger: { project: 'some/project' },
+ ignore: false,
+ stage: 'test',
+ only: { refs: %w[branches tags] },
+ job_variables: {},
+ root_variables_inheritance: true,
+ scheduling_type: :stage
+ )
end
end
end
@@ -124,15 +126,16 @@ RSpec.describe Gitlab::Ci::Config::Entry::Bridge do
describe '#value' do
it 'is returns a bridge job configuration hash' do
- expect(subject.value).to eq(name: :my_bridge,
- trigger: { project: 'some/project',
- branch: 'feature' },
- ignore: false,
- stage: 'test',
- only: { refs: %w[branches tags] },
- job_variables: {},
- root_variables_inheritance: true,
- scheduling_type: :stage)
+ expect(subject.value).to eq(
+ name: :my_bridge,
+ trigger: { project: 'some/project', branch: 'feature' },
+ ignore: false,
+ stage: 'test',
+ only: { refs: %w[branches tags] },
+ job_variables: {},
+ root_variables_inheritance: true,
+ scheduling_type: :stage
+ )
end
end
end
@@ -283,8 +286,8 @@ RSpec.describe Gitlab::Ci::Config::Entry::Bridge do
ignore: false,
stage: 'test',
only: { refs: %w[branches tags] },
- parallel: { matrix: [{ 'PROVIDER' => ['aws'], 'STACK' => %w(monitoring app1) },
- { 'PROVIDER' => ['gcp'], 'STACK' => %w(data) }] },
+ parallel: { matrix: [{ 'PROVIDER' => ['aws'], 'STACK' => %w[monitoring app1] },
+ { 'PROVIDER' => ['gcp'], 'STACK' => %w[data] }] },
job_variables: {},
root_variables_inheritance: true,
scheduling_type: :stage
@@ -305,15 +308,16 @@ RSpec.describe Gitlab::Ci::Config::Entry::Bridge do
describe '#value' do
it 'returns a bridge job configuration hash' do
- expect(subject.value).to eq(name: :my_bridge,
- trigger: { project: 'some/project',
- forward: { pipeline_variables: true } },
- ignore: false,
- stage: 'test',
- only: { refs: %w[branches tags] },
- job_variables: {},
- root_variables_inheritance: true,
- scheduling_type: :stage)
+ expect(subject.value).to eq(
+ name: :my_bridge,
+ trigger: { project: 'some/project', forward: { pipeline_variables: true } },
+ ignore: false,
+ stage: 'test',
+ only: { refs: %w[branches tags] },
+ job_variables: {},
+ root_variables_inheritance: true,
+ scheduling_type: :stage
+ )
end
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/commands_spec.rb b/spec/lib/gitlab/ci/config/entry/commands_spec.rb
index 1b8dfae692a..f84a78b4804 100644
--- a/spec/lib/gitlab/ci/config/entry/commands_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/commands_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Commands do
let(:entry) { described_class.new(config) }
context 'when entry config value is an array of strings' do
- let(:config) { %w(ls pwd) }
+ let(:config) { %w[ls pwd] }
describe '#value' do
it 'returns array of strings' do
diff --git a/spec/lib/gitlab/ci/config/entry/environment_spec.rb b/spec/lib/gitlab/ci/config/entry/environment_spec.rb
index 3562706ff33..cff94a96c99 100644
--- a/spec/lib/gitlab/ci/config/entry/environment_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/environment_spec.rb
@@ -93,7 +93,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Environment do
context 'when valid action is used' do
where(:action) do
- %w(start stop prepare verify access)
+ %w[start stop prepare verify access]
end
with_them do
diff --git a/spec/lib/gitlab/ci/config/entry/image_spec.rb b/spec/lib/gitlab/ci/config/entry/image_spec.rb
index b37498ba10a..17c45ec4c2c 100644
--- a/spec/lib/gitlab/ci/config/entry/image_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/image_spec.rb
@@ -56,7 +56,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Image do
end
context 'when configuration is a hash' do
- let(:config) { { name: 'image:1.0', entrypoint: %w(/bin/sh run) } }
+ let(:config) { { name: 'image:1.0', entrypoint: %w[/bin/sh run] } }
describe '#value' do
it 'returns image hash' do
@@ -84,13 +84,13 @@ RSpec.describe Gitlab::Ci::Config::Entry::Image do
describe '#entrypoint' do
it "returns image's entrypoint" do
- expect(entry.entrypoint).to eq %w(/bin/sh run)
+ expect(entry.entrypoint).to eq %w[/bin/sh run]
end
end
context 'when configuration has ports' do
let(:ports) { [{ number: 80, protocol: 'http', name: 'foobar' }] }
- let(:config) { { name: 'image:1.0', entrypoint: %w(/bin/sh run), ports: ports } }
+ let(:config) { { name: 'image:1.0', entrypoint: %w[/bin/sh run], ports: ports } }
let(:entry) { described_class.new(config, with_image_ports: image_ports) }
let(:image_ports) { false }
diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb
index 1a78d929871..24d3cac6616 100644
--- a/spec/lib/gitlab/ci/config/entry/job_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb
@@ -598,7 +598,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job, feature_category: :pipeline_compo
end
end
- context 'when job is not a pages job' do
+ context 'when job is not a pages job', feature_category: :pages do
let(:name) { :rspec }
context 'if the config contains a publish entry' do
@@ -609,9 +609,18 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job, feature_category: :pipeline_compo
expect(entry.errors).to include /job publish can only be used within a `pages` job/
end
end
+
+ context 'if the config contains a pages entry' do
+ let(:entry) { described_class.new({ script: 'echo', pages: { path_prefix: 'foo' } }, name: name) }
+
+ it 'is invalid' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include /job pages can only be used within a `pages` job/
+ end
+ end
end
- context 'when job is a pages job' do
+ context 'when job is a pages job', feature_category: :pages do
let(:name) { :pages }
context 'when it does not have a publish entry' do
@@ -629,6 +638,28 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job, feature_category: :pipeline_compo
expect(entry).to be_valid
end
end
+
+ context 'when it has a pages entry' do
+ let(:entry) { described_class.new({ script: 'echo', pages: { path_prefix: 'foo' } }, name: name) }
+
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+ end
+
+ describe '#pages_job?', :aggregate_failures, feature_category: :pages do
+ where(:name, :result) do
+ :pages | true
+ :'pages:staging' | false
+ :'something:pages:else' | false
+ end
+
+ with_them do
+ subject { described_class.new({}, name: name).pages_job? }
+
+ it { is_expected.to eq(result) }
end
end
@@ -739,20 +770,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job, feature_category: :pipeline_compo
end
end
- describe '#pages_job?', :aggregate_failures, feature_category: :pages do
- where(:name, :result) do
- :pages | true
- :'pages:staging' | false
- :'something:pages:else' | false
- end
-
- with_them do
- subject { described_class.new({}, name: name).pages_job? }
-
- it { is_expected.to eq(result) }
- end
- end
-
context 'when composed' do
before do
entry.compose!
@@ -773,19 +790,20 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job, feature_category: :pipeline_compo
end
it 'returns correct value' do
- expect(entry.value)
- .to eq(name: :rspec,
- before_script: %w[ls pwd],
- script: %w[rspec],
- stage: 'test',
- ignore: false,
- after_script: %w[cleanup],
- hooks: { pre_get_sources_script: ['echo hello'] },
- only: { refs: %w[branches tags] },
- job_variables: {},
- root_variables_inheritance: true,
- scheduling_type: :stage,
- id_tokens: { TEST_ID_TOKEN: { aud: 'https://gitlab.com' } })
+ expect(entry.value).to eq(
+ name: :rspec,
+ before_script: %w[ls pwd],
+ script: %w[rspec],
+ stage: 'test',
+ ignore: false,
+ after_script: %w[cleanup],
+ hooks: { pre_get_sources_script: ['echo hello'] },
+ only: { refs: %w[branches tags] },
+ job_variables: {},
+ root_variables_inheritance: true,
+ scheduling_type: :stage,
+ id_tokens: { TEST_ID_TOKEN: { aud: 'https://gitlab.com' } }
+ )
end
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/pages_spec.rb b/spec/lib/gitlab/ci/config/entry/pages_spec.rb
new file mode 100644
index 00000000000..0ee692a443e
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/pages_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Config::Entry::Pages, feature_category: :pages do
+ subject(:entry) { described_class.new(config) }
+
+ describe 'validation' do
+ context 'when value given is not a hash' do
+ let(:config) { 'value' }
+
+ it 'is invalid' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include('pages config should be a hash')
+ end
+ end
+
+ context 'when value is a hash' do
+ context 'when the hash is valid' do
+ let(:config) { { path_prefix: 'prefix' } }
+
+ it 'is valid' do
+ expect(entry).to be_valid
+ expect(entry.value).to eq({
+ path_prefix: 'prefix'
+ })
+ end
+ end
+
+ context 'when path_prefix key is not a string' do
+ let(:config) { { path_prefix: 1 } }
+
+ it 'is invalid' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include('pages path prefix should be a string')
+ end
+ end
+
+ context 'when hash contains not allowed keys' do
+ let(:config) { { unknown: 'echo' } }
+
+ it 'is invalid' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include('pages config contains unknown keys: unknown')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/policy_spec.rb b/spec/lib/gitlab/ci/config/entry/policy_spec.rb
index 7093a0a6edf..77a895b75c0 100644
--- a/spec/lib/gitlab/ci/config/entry/policy_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/policy_spec.rb
@@ -221,8 +221,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Policy, feature_category: :continuous_
let(:config) { { variables: %w[$VARIABLE] } }
it 'includes default values' do
- expect(entry.value).to eq(refs: %w[branches tags],
- variables: %w[$VARIABLE])
+ expect(entry.value).to eq(refs: %w[branches tags], variables: %w[$VARIABLE])
end
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/processable_spec.rb b/spec/lib/gitlab/ci/config/entry/processable_spec.rb
index 132e75a808b..44e2fdbac37 100644
--- a/spec/lib/gitlab/ci/config/entry/processable_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/processable_spec.rb
@@ -119,6 +119,20 @@ RSpec.describe Gitlab::Ci::Config::Entry::Processable, feature_category: :pipeli
end
end
+ context 'when script: and trigger: are used together' do
+ let(:config) do
+ {
+ script: 'echo',
+ trigger: 'test-group/test-project'
+ }
+ end
+
+ it 'returns is invalid' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include(/these keys cannot be used together: script, trigger/)
+ end
+ end
+
context 'when only: is used with rules:' do
let(:config) { { only: ['merge_requests'], rules: [{ if: '$THIS' }] } }
diff --git a/spec/lib/gitlab/ci/config/entry/root_spec.rb b/spec/lib/gitlab/ci/config/entry/root_spec.rb
index 5fac5298e8e..0370bcbccf5 100644
--- a/spec/lib/gitlab/ci/config/entry/root_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/root_spec.rb
@@ -31,7 +31,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do
context 'when top-level entries are defined' do
let(:hash) do
{
- before_script: %w(ls pwd),
+ before_script: %w[ls pwd],
image: 'image:1.0',
default: {},
services: ['postgres:9.1', 'mysql:5.5'],
@@ -41,7 +41,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do
VAR3: { value: 'val3', options: %w[val3 val4 val5], description: 'this is var 3 and some options' }
},
after_script: ['make clean'],
- stages: %w(build pages release),
+ stages: %w[build pages release],
cache: { key: 'k', untracked: true, paths: ['public/'] },
rspec: { script: %w[rspec ls] },
spinach: { before_script: [], variables: {}, script: 'spinach' },
@@ -123,7 +123,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do
expect(root.jobs_value[:rspec]).to eq(
{ name: :rspec,
script: %w[rspec ls],
- before_script: %w(ls pwd),
+ before_script: %w[ls pwd],
image: { name: 'image:1.0' },
services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }],
stage: 'test',
@@ -162,7 +162,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do
services: [{ name: "postgres:9.1" }, { name: "mysql:5.5" }],
cache: [{ key: "k", untracked: true, paths: ["public/"], policy: "pull-push", when: 'on_success',
unprotect: false, fallback_keys: [] }],
- only: { refs: %w(branches tags) },
+ only: { refs: %w[branches tags] },
job_variables: { 'VAR' => { value: 'job' } },
root_variables_inheritance: true,
after_script: [],
@@ -176,14 +176,14 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do
context 'when a mix of top-level and default entries is used' do
let(:hash) do
- { before_script: %w(ls pwd),
+ { before_script: %w[ls pwd],
after_script: ['make clean'],
default: {
image: 'image:1.0',
services: ['postgres:9.1', 'mysql:5.5']
},
variables: { VAR: 'root' },
- stages: %w(build pages),
+ stages: %w[build pages],
cache: { key: 'k', untracked: true, paths: ['public/'] },
rspec: { script: %w[rspec ls] },
spinach: { before_script: [], variables: { VAR: 'job' }, script: 'spinach' } }
@@ -205,7 +205,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do
expect(root.jobs_value).to eq(
rspec: { name: :rspec,
script: %w[rspec ls],
- before_script: %w(ls pwd),
+ before_script: %w[ls pwd],
image: { name: 'image:1.0' },
services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }],
stage: 'test',
diff --git a/spec/lib/gitlab/ci/config/entry/service_spec.rb b/spec/lib/gitlab/ci/config/entry/service_spec.rb
index e36484bb0ae..1f935bebed5 100644
--- a/spec/lib/gitlab/ci/config/entry/service_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/service_spec.rb
@@ -51,7 +51,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Service do
context 'when configuration is a hash' do
let(:config) do
- { name: 'postgresql:9.5', alias: 'db', command: %w(cmd run), entrypoint: %w(/bin/sh run) }
+ { name: 'postgresql:9.5', alias: 'db', command: %w[cmd run], entrypoint: %w[/bin/sh run] }
end
describe '#valid?' do
@@ -80,13 +80,13 @@ RSpec.describe Gitlab::Ci::Config::Entry::Service do
describe '#command' do
it "returns service's command" do
- expect(entry.command).to eq %w(cmd run)
+ expect(entry.command).to eq %w[cmd run]
end
end
describe '#entrypoint' do
it "returns service's entrypoint" do
- expect(entry.entrypoint).to eq %w(/bin/sh run)
+ expect(entry.entrypoint).to eq %w[/bin/sh run]
end
end
@@ -99,7 +99,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Service do
context 'when configuration has ports' do
let(:ports) { [{ number: 80, protocol: 'http', name: 'foobar' }] }
let(:config) do
- { name: 'postgresql:9.5', alias: 'db', command: %w(cmd run), entrypoint: %w(/bin/sh run), ports: ports }
+ { name: 'postgresql:9.5', alias: 'db', command: %w[cmd run], entrypoint: %w[/bin/sh run], ports: ports }
end
let(:entry) { described_class.new(config, with_image_ports: image_ports) }
@@ -198,7 +198,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Service do
context 'when service has ports' do
let(:ports) { [{ number: 80, protocol: 'http', name: 'foobar' }] }
let(:config) do
- { name: 'postgresql:9.5', command: %w(cmd run), entrypoint: %w(/bin/sh run), ports: ports }
+ { name: 'postgresql:9.5', command: %w[cmd run], entrypoint: %w[/bin/sh run], ports: ports }
end
it 'alias field is mandatory' do
@@ -209,7 +209,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Service do
context 'when service does not have ports' do
let(:config) do
- { name: 'postgresql:9.5', alias: 'db', command: %w(cmd run), entrypoint: %w(/bin/sh run) }
+ { name: 'postgresql:9.5', alias: 'db', command: %w[cmd run], entrypoint: %w[/bin/sh run] }
end
it 'alias field is optional' do
diff --git a/spec/lib/gitlab/ci/config/extendable/entry_spec.rb b/spec/lib/gitlab/ci/config/extendable/entry_spec.rb
index 69aa3bab77a..b57a56b8389 100644
--- a/spec/lib/gitlab/ci/config/extendable/entry_spec.rb
+++ b/spec/lib/gitlab/ci/config/extendable/entry_spec.rb
@@ -128,8 +128,7 @@ RSpec.describe Gitlab::Ci::Config::Extendable::Entry do
it 'raises an error' do
expect { subject.extend! }
- .to raise_error(described_class::InvalidExtensionError,
- /invalid base hash/)
+ .to raise_error(described_class::InvalidExtensionError, /invalid base hash/)
end
end
@@ -140,8 +139,7 @@ RSpec.describe Gitlab::Ci::Config::Extendable::Entry do
it 'raises an error' do
expect { subject.extend! }
- .to raise_error(described_class::InvalidExtensionError,
- /unknown key/)
+ .to raise_error(described_class::InvalidExtensionError, /unknown key/)
end
end
@@ -178,7 +176,7 @@ RSpec.describe Gitlab::Ci::Config::Extendable::Entry do
{
first: { script: 'my value', image: 'ubuntu' },
second: { image: 'alpine' },
- test: { extends: %w(first second) }
+ test: { extends: %w[first second] }
}
end
@@ -186,7 +184,7 @@ RSpec.describe Gitlab::Ci::Config::Extendable::Entry do
{
first: { script: 'my value', image: 'ubuntu' },
second: { image: 'alpine' },
- test: { extends: %w(first second), script: 'my value', image: 'alpine' }
+ test: { extends: %w[first second], script: 'my value', image: 'alpine' }
}
end
@@ -230,8 +228,7 @@ RSpec.describe Gitlab::Ci::Config::Extendable::Entry do
it 'raises an error' do
expect { subject.extend! }
- .to raise_error(described_class::CircularDependencyError,
- /circular dependency detected/)
+ .to raise_error(described_class::CircularDependencyError, /circular dependency detected/)
end
end
diff --git a/spec/lib/gitlab/ci/config/external/file/base_spec.rb b/spec/lib/gitlab/ci/config/external/file/base_spec.rb
index 1415dbeb532..bcfab620bd9 100644
--- a/spec/lib/gitlab/ci/config/external/file/base_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/file/base_spec.rb
@@ -60,7 +60,7 @@ RSpec.describe Gitlab::Ci::Config::External::File::Base, feature_category: :pipe
end
context 'when location is not a string' do
- let(:location) { %w(some/file.txt other/file.txt) }
+ let(:location) { %w[some/file.txt other/file.txt] }
it { is_expected.to be_falsy }
end
diff --git a/spec/lib/gitlab/ci/config/external/mapper_spec.rb b/spec/lib/gitlab/ci/config/external/mapper_spec.rb
index 56d1ddee4b8..5f28b45496f 100644
--- a/spec/lib/gitlab/ci/config/external/mapper_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/mapper_spec.rb
@@ -406,8 +406,10 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper, feature_category: :pipeline
end
it 'includes the matched local files' do
- expect(subject).to contain_exactly(an_instance_of(Gitlab::Ci::Config::External::File::Local),
- an_instance_of(Gitlab::Ci::Config::External::File::Local))
+ expect(subject).to contain_exactly(
+ an_instance_of(Gitlab::Ci::Config::External::File::Local),
+ an_instance_of(Gitlab::Ci::Config::External::File::Local)
+ )
expect(subject.map(&:location)).to contain_exactly('myfolder/file1.yml', 'myfolder/file2.yml')
end
@@ -424,8 +426,10 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper, feature_category: :pipeline
let(:project_id) { project.id }
it 'includes the file' do
- expect(subject).to contain_exactly(an_instance_of(Gitlab::Ci::Config::External::File::Remote),
- an_instance_of(Gitlab::Ci::Config::External::File::Local))
+ expect(subject).to contain_exactly(
+ an_instance_of(Gitlab::Ci::Config::External::File::Remote),
+ an_instance_of(Gitlab::Ci::Config::External::File::Local)
+ )
end
end
diff --git a/spec/lib/gitlab/ci/config/header/input_spec.rb b/spec/lib/gitlab/ci/config/header/input_spec.rb
index 5d1fa4a8e6e..df70d1fd7c8 100644
--- a/spec/lib/gitlab/ci/config/header/input_spec.rb
+++ b/spec/lib/gitlab/ci/config/header/input_spec.rb
@@ -40,12 +40,24 @@ RSpec.describe Gitlab::Ci::Config::Header::Input, feature_category: :pipeline_co
end
end
- context 'when has a default value' do
+ context 'when has a string default value' do
let(:input_hash) { { default: 'bar' } }
it_behaves_like 'a valid input'
end
+ context 'when has a numeric default value' do
+ let(:input_hash) { { default: 6.66 } }
+
+ it_behaves_like 'a valid input'
+ end
+
+ context 'when has a boolean default value' do
+ let(:input_hash) { { default: true } }
+
+ it_behaves_like 'a valid input'
+ end
+
context 'when has a description value' do
let(:input_hash) { { description: 'bar' } }
@@ -103,4 +115,21 @@ RSpec.describe Gitlab::Ci::Config::Header::Input, feature_category: :pipeline_co
it_behaves_like 'an invalid input'
end
+
+ context 'when the limit for allowed number of options is reached' do
+ let(:limit) { described_class::ALLOWED_OPTIONS_LIMIT }
+ let(:input_hash) { { default: 'value1', options: options } }
+ let(:options) { Array.new(limit.next) { |i| "value#{i}" } }
+
+ describe '#valid?' do
+ it { is_expected.not_to be_valid }
+ end
+
+ describe '#errors' do
+ it 'returns error about incorrect type' do
+ expect(config.errors).to contain_exactly(
+ "foo config cannot define more than #{limit} options")
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/config/interpolation/inputs_spec.rb b/spec/lib/gitlab/ci/config/interpolation/inputs_spec.rb
index b0618081207..57ced4eab98 100644
--- a/spec/lib/gitlab/ci/config/interpolation/inputs_spec.rb
+++ b/spec/lib/gitlab/ci/config/interpolation/inputs_spec.rb
@@ -7,6 +7,90 @@ RSpec.describe Gitlab::Ci::Config::Interpolation::Inputs, feature_category: :pip
let(:specs) { { foo: { default: 'bar' } } }
let(:args) { {} }
+ context 'when inputs are valid strings and have options' do
+ let(:specs) { { foo: { default: 'one', options: %w[one two three] } } }
+
+ context 'and the value is selected' do
+ let(:args) { { foo: 'two' } }
+
+ it 'assigns the selected value' do
+ expect(inputs).to be_valid
+ expect(inputs.to_hash).to eq({ foo: 'two' })
+ end
+ end
+
+ context 'and the value is not selected' do
+ it 'assigns the default value' do
+ expect(inputs).to be_valid
+ expect(inputs.to_hash).to eq({ foo: 'one' })
+ end
+ end
+ end
+
+ context 'when inputs options are valid integers' do
+ let(:specs) { { foo: { default: 1, options: [1, 2, 3, 4, 5], type: 'number' } } }
+
+ context 'and a value of the wrong type is given' do
+ let(:args) { { foo: 'word' } }
+
+ it 'returns an error' do
+ expect(inputs).not_to be_valid
+ expect(inputs.errors).to contain_exactly(
+ "`foo` input: `word` cannot be used because it is not in the list of the allowed options",
+ "`foo` input: provided value is not a number"
+ )
+ end
+ end
+
+ context 'and the value is selected' do
+ let(:args) { { foo: 2 } }
+
+ it 'assigns the selected value' do
+ expect(inputs).to be_valid
+ expect(inputs.to_hash).to eq({ foo: 2 })
+ end
+ end
+
+ context 'and the value is not selected' do
+ it 'assigns the default value' do
+ expect(inputs).to be_valid
+ expect(inputs.to_hash).to eq({ foo: 1 })
+ end
+ end
+ end
+
+ context 'when inputs have invalid type options' do
+ let(:specs) { { foo: { default: true, options: [true, false], type: 'boolean' } } }
+
+ it 'returns an error' do
+ expect(inputs).not_to be_valid
+ expect(inputs.errors).to contain_exactly("`foo` input: Options can only be used with string and number inputs")
+ end
+ end
+
+ context 'when inputs are valid with options but the default value is not in the options' do
+ let(:specs) { { foo: { default: 'coop', options: %w[one two three] } } }
+
+ it 'returns an error' do
+ expect(inputs).not_to be_valid
+ expect(inputs.errors).to contain_exactly(
+ '`foo` input: `coop` cannot be used because it is not in the list of allowed options'
+ )
+ end
+ end
+
+ context 'when inputs are valid with options but the value is not in the options' do
+ let(:specs) { { foo: { default: 'one', options: %w[one two three] } } }
+ let(:args) { { foo: 'niet' } }
+
+ it 'returns an error' do
+ expect(inputs).not_to be_valid
+ expect(inputs.errors).to contain_exactly(
+ '`foo` input: `niet` cannot be used because it is not in the list of allowed options'
+ )
+ end
+ end
+
context 'when given unrecognized inputs' do
let(:specs) { { foo: nil } }
let(:args) { { foo: 'bar', test: 'bar' } }
@@ -164,7 +248,7 @@ RSpec.describe Gitlab::Ci::Config::Interpolation::Inputs, feature_category: :pip
context 'when the value is not a number' do
let(:specs) { { number_input: { type: 'number' } } }
- let(:args) { { number_input: 'NaN' } }
+ let(:args) { { number_input: false } }
it 'is invalid' do
expect(inputs).not_to be_valid
diff --git a/spec/lib/gitlab/ci/jwt_v2_spec.rb b/spec/lib/gitlab/ci/jwt_v2_spec.rb
index d45d8cacb88..c2ced10620b 100644
--- a/spec/lib/gitlab/ci/jwt_v2_spec.rb
+++ b/spec/lib/gitlab/ci/jwt_v2_spec.rb
@@ -33,14 +33,6 @@ RSpec.describe Gitlab::Ci::JwtV2, feature_category: :continuous_integration do
describe '#payload' do
subject(:payload) { ci_job_jwt_v2.payload }
- it 'has correct values for the standard JWT attributes' do
- aggregate_failures do
- expect(payload[:iss]).to eq(Settings.gitlab.base_url)
- expect(payload[:aud]).to eq(Settings.gitlab.base_url)
- expect(payload[:sub]).to eq("project_path:#{project.full_path}:ref_type:branch:ref:#{pipeline.source_ref}")
- end
- end
-
it 'includes user identities when enabled' do
expect(user).to receive(:pass_user_identities_to_ci_jwt).and_return(true)
identities = payload[:user_identities].map { |identity| identity.slice(:extern_uid, :provider) }
@@ -53,6 +45,34 @@ RSpec.describe Gitlab::Ci::JwtV2, feature_category: :continuous_integration do
expect(payload).not_to include(:user_identities)
end
+ context 'when oidc_issuer_url is disabled' do
+ before do
+ stub_feature_flags(oidc_issuer_url: false)
+ end
+
+ it 'has correct values for the standard JWT attributes' do
+ aggregate_failures do
+ expect(payload[:iss]).to eq(Settings.gitlab.base_url)
+ expect(payload[:aud]).to eq(Settings.gitlab.base_url)
+ expect(payload[:sub]).to eq("project_path:#{project.full_path}:ref_type:branch:ref:#{pipeline.source_ref}")
+ end
+ end
+ end
+
+ context 'when oidc_issuer_url is enabled' do
+ before do
+ stub_feature_flags(oidc_issuer_url: true)
+ end
+
+ it 'has correct values for the standard JWT attributes' do
+ aggregate_failures do
+ expect(payload[:iss]).to eq(Gitlab.config.gitlab.url)
+ expect(payload[:aud]).to eq(Settings.gitlab.base_url)
+ expect(payload[:sub]).to eq("project_path:#{project.full_path}:ref_type:branch:ref:#{pipeline.source_ref}")
+ end
+ end
+ end
+
context 'when given an aud' do
let(:aud) { 'AWS' }
diff --git a/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_properties_spec.rb b/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_properties_spec.rb
index dacbe07c8b3..2c57106b07c 100644
--- a/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_properties_spec.rb
+++ b/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_properties_spec.rb
@@ -42,15 +42,16 @@ RSpec.describe Gitlab::Ci::Parsers::Sbom::CyclonedxProperties, feature_category:
it { is_expected.to be_nil }
end
- context 'when no dependency_scanning properties are present' do
+ context 'when no dependency_scanning or container_scanning properties are present' do
let(:properties) do
[
{ 'name' => 'gitlab:meta:schema_version', 'value' => '1' }
]
end
- it 'does not call dependency_scanning parser' do
+ it 'does not call source parsers' do
expect(Gitlab::Ci::Parsers::Sbom::Source::DependencyScanning).not_to receive(:source)
+ expect(Gitlab::Ci::Parsers::Sbom::Source::ContainerScanning).not_to receive(:source)
parse_source_from_properties
end
@@ -85,4 +86,35 @@ RSpec.describe Gitlab::Ci::Parsers::Sbom::CyclonedxProperties, feature_category:
parse_source_from_properties
end
end
+
+ context 'when container_scanning properties are present' do
+ let(:properties) do
+ [
+ { 'name' => 'gitlab:meta:schema_version', 'value' => '1' },
+ { 'name' => 'gitlab:container_scanning:image:name', 'value' => 'photon' },
+ { 'name' => 'gitlab:container_scanning:image:tag', 'value' => '5.0-20231007' },
+ { 'name' => 'gitlab:container_scanning:operating_system:name', 'value' => 'Photon OS' },
+ { 'name' => 'gitlab:container_scanning:operating_system:version', 'value' => '5.0' }
+ ]
+ end
+
+ let(:expected_input) do
+ {
+ 'image' => {
+ 'name' => 'photon',
+ 'tag' => '5.0-20231007'
+ },
+ 'operating_system' => {
+ 'name' => 'Photon OS',
+ 'version' => '5.0'
+ }
+ }
+ end
+
+ it 'passes only supported properties to the container scanning parser' do
+ expect(Gitlab::Ci::Parsers::Sbom::Source::ContainerScanning).to receive(:source).with(expected_input)
+
+ parse_source_from_properties
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/parsers/sbom/source/container_scanning_spec.rb b/spec/lib/gitlab/ci/parsers/sbom/source/container_scanning_spec.rb
new file mode 100644
index 00000000000..410b2c0098a
--- /dev/null
+++ b/spec/lib/gitlab/ci/parsers/sbom/source/container_scanning_spec.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+RSpec.describe Gitlab::Ci::Parsers::Sbom::Source::ContainerScanning, feature_category: :container_scanning do
+ subject { described_class.source(property_data) }
+
+ context 'when required properties are present' do
+ let(:property_data) do
+ {
+ 'image' => {
+ 'name' => 'photon',
+ 'tag' => '5.0-20231007'
+ },
+ 'operating_system' => {
+ 'name' => 'Photon OS',
+ 'version' => '5.0'
+ }
+ }
+ end
+
+ it 'returns expected source data' do
+ is_expected.to have_attributes(
+ source_type: :container_scanning,
+ data: property_data
+ )
+ end
+ end
+
+ context 'when required properties are missing' do
+ let(:property_data) do
+ {
+ 'operating_system' => {
+ 'name' => 'Photon OS',
+ 'version' => '5.0'
+ }
+ }
+ end
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'when some operating_system properties are missing' do
+ let(:property_data) do
+ {
+ 'image' => {
+ 'name' => 'photon',
+ 'tag' => '5.0-20231007'
+ },
+ 'operating_system' => {
+ 'name' => 'Photon OS'
+ }
+ }
+ end
+
+ it { is_expected.to be_nil }
+ end
+end
diff --git a/spec/lib/gitlab/ci/parsers/security/common_spec.rb b/spec/lib/gitlab/ci/parsers/security/common_spec.rb
index 648b8ac2db9..431a6d94c48 100644
--- a/spec/lib/gitlab/ci/parsers/security/common_spec.rb
+++ b/spec/lib/gitlab/ci/parsers/security/common_spec.rb
@@ -335,7 +335,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common, feature_category: :vulnera
expect(flags).to contain_exactly(
have_attributes(type: 'flagged-as-likely-false-positive', origin: 'post analyzer X', description: 'static string to sink'),
- have_attributes(type: 'flagged-as-likely-false-positive', origin: 'post analyzer Y', description: 'integer to sink')
+ have_attributes(type: 'flagged-as-likely-false-positive', origin: 'post analyzer Y', description: 'integer to sink')
)
end
end
diff --git a/spec/lib/gitlab/ci/parsers/security/secret_detection_spec.rb b/spec/lib/gitlab/ci/parsers/security/secret_detection_spec.rb
index 13999b2a9e5..640bed0d329 100644
--- a/spec/lib/gitlab/ci/parsers/security/secret_detection_spec.rb
+++ b/spec/lib/gitlab/ci/parsers/security/secret_detection_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::SecretDetection do
let(:created_at) { 2.weeks.ago }
context "when parsing valid reports" do
- where(report_format: %i(secret_detection))
+ where(report_format: %i[secret_detection])
with_them do
let(:report) { Gitlab::Ci::Reports::Security::Report.new(artifact.file_type, pipeline, created_at) }
diff --git a/spec/lib/gitlab/ci/pipeline/chain/template_usage_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/template_usage_spec.rb
index ddd0de69d79..70d73a8095c 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/template_usage_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/template_usage_spec.rb
@@ -21,11 +21,11 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::TemplateUsage do
expect(command).to(
receive(:yaml_processor_result)
.and_return(
- double(included_templates: %w(Template-1 Template-2))
+ double(included_templates: %w[Template-1 Template-2])
)
)
- %w(Template-1 Template-2).each do |expected_template|
+ %w[Template-1 Template-2].each do |expected_template|
expect(Gitlab::UsageDataCounters::CiTemplateUniqueCounter).to(
receive(:track_unique_project_event)
.with(project: project, template: expected_template, config_source: pipeline.config_source, user: user)
diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/equals_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/equals_spec.rb
index ab223ae41fa..eb71cc0f0bc 100644
--- a/spec/lib/gitlab/ci/pipeline/expression/lexeme/equals_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/equals_spec.rb
@@ -49,7 +49,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Expression::Lexeme::Equals do
context 'when left and right are equal' do
where(:left_value, :right_value) do
- [%w(string string)]
+ [%w[string string]]
end
with_them do
diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
index 54e569f424b..ef9b8f2b82f 100644
--- a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
@@ -395,7 +395,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build, feature_category: :pipeline_co
end
context 'when root_variables_inheritance is an array' do
- let(:root_variables_inheritance) { %w(VAR1 VAR2 VAR3) }
+ let(:root_variables_inheritance) { %w[VAR1 VAR2 VAR3] }
it 'returns calculated yaml variables' do
expect(subject[:yaml_variables]).to match_array(
diff --git a/spec/lib/gitlab/ci/reports/accessibility_reports_comparer_spec.rb b/spec/lib/gitlab/ci/reports/accessibility_reports_comparer_spec.rb
index ad8f1dc11f8..6d8b472a240 100644
--- a/spec/lib/gitlab/ci/reports/accessibility_reports_comparer_spec.rb
+++ b/spec/lib/gitlab/ci/reports/accessibility_reports_comparer_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe Gitlab::Ci::Reports::AccessibilityReportsComparer do
"type" => "error",
"typeCode" => 1,
"message" => "Anchor element found with a valid href attribute, but no link content has been supplied.",
- "context" => %{<a href="/" class="navbar-brand animated"><svg height="36" viewBox="0 0 1...</a>},
+ "context" => %(<a href="/" class="navbar-brand animated"><svg height="36" viewBox="0 0 1...</a>),
"selector" => "#main-nav > div:nth-child(1) > a",
"runner" => "htmlcs",
"runnerExtras" => {}
@@ -29,7 +29,7 @@ RSpec.describe Gitlab::Ci::Reports::AccessibilityReportsComparer do
"type" => "error",
"typeCode" => 1,
"message" => "This element has insufficient contrast at this conformance level.",
- "context" => %{<a href="/stages-devops-lifecycle/" class="main-nav-link">Product</a>},
+ "context" => %(<a href="/stages-devops-lifecycle/" class="main-nav-link">Product</a>),
"selector" => "#main-nav > div:nth-child(2) > ul > li:nth-child(1) > a",
"runner" => "htmlcs",
"runnerExtras" => {}
diff --git a/spec/lib/gitlab/ci/reports/accessibility_reports_spec.rb b/spec/lib/gitlab/ci/reports/accessibility_reports_spec.rb
index af6844491ca..dff59474746 100644
--- a/spec/lib/gitlab/ci/reports/accessibility_reports_spec.rb
+++ b/spec/lib/gitlab/ci/reports/accessibility_reports_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe Gitlab::Ci::Reports::AccessibilityReports do
"type": "error",
"typeCode": 1,
"message": "Anchor element found with a valid href attribute, but no link content has been supplied.",
- "context": %{<a href="/customers/worldline"><svg viewBox="0 0 509 89" xmln...</a>},
+ "context": %(<a href="/customers/worldline"><svg viewBox="0 0 509 89" xmln...</a>),
"selector": "html > body > div:nth-child(9) > div:nth-child(2) > a:nth-child(17)",
"runner": "htmlcs",
"runnerExtras": {}
@@ -22,7 +22,7 @@ RSpec.describe Gitlab::Ci::Reports::AccessibilityReports do
"type": "error",
"typeCode": 1,
"message": "Anchor element found with a valid href attribute, but no link content has been supplied.",
- "context": %{<a href="/customers/equinix"><svg xmlns="http://www.w3.org/...</a>},
+ "context": %(<a href="/customers/equinix"><svg xmlns="http://www.w3.org/...</a>),
"selector": "html > body > div:nth-child(9) > div:nth-child(2) > a:nth-child(18)",
"runner": "htmlcs",
"runnerExtras": {}
diff --git a/spec/lib/gitlab/ci/reports/test_suite_spec.rb b/spec/lib/gitlab/ci/reports/test_suite_spec.rb
index 05f6a8a8cb6..46ab0802200 100644
--- a/spec/lib/gitlab/ci/reports/test_suite_spec.rb
+++ b/spec/lib/gitlab/ci/reports/test_suite_spec.rb
@@ -192,7 +192,7 @@ RSpec.describe Gitlab::Ci::Reports::TestSuite do
end
context 'when there are multiple test cases' do
- let(:status_ordered) { %w(error failed success skipped) }
+ let(:status_ordered) { %w[error failed success skipped] }
before do
test_suite.add_test_case(test_case_success)
diff --git a/spec/lib/gitlab/ci/status/composite_spec.rb b/spec/lib/gitlab/ci/status/composite_spec.rb
index cbf0976c976..e46ad573235 100644
--- a/spec/lib/gitlab/ci/status/composite_spec.rb
+++ b/spec/lib/gitlab/ci/status/composite_spec.rb
@@ -43,28 +43,29 @@ RSpec.describe Gitlab::Ci::Status::Composite, feature_category: :continuous_inte
context 'allow_failure: false' do
where(:build_statuses, :dag, :result, :has_warnings) do
- %i(skipped) | false | 'skipped' | false
- %i(skipped success) | false | 'success' | false
- %i(skipped success) | true | 'skipped' | false
- %i(created) | false | 'created' | false
- %i(preparing) | false | 'preparing' | false
- %i(canceled success skipped) | false | 'canceled' | false
- %i(canceled success skipped) | true | 'skipped' | false
- %i(pending created skipped) | false | 'pending' | false
- %i(pending created skipped success) | false | 'running' | false
- %i(running created skipped success) | false | 'running' | false
- %i(pending created skipped) | true | 'skipped' | false
- %i(pending created skipped success) | true | 'skipped' | false
- %i(running created skipped success) | true | 'skipped' | false
- %i(success waiting_for_resource) | false | 'waiting_for_resource' | false
- %i(success manual) | false | 'manual' | false
- %i(success scheduled) | false | 'scheduled' | false
- %i(created preparing) | false | 'preparing' | false
- %i(created success pending) | false | 'running' | false
- %i(skipped success failed) | false | 'failed' | false
- %i(skipped success failed) | true | 'skipped' | false
- %i(success manual) | true | 'manual' | false
- %i(success failed created) | true | 'running' | false
+ %i[skipped] | false | 'skipped' | false
+ %i[skipped success] | false | 'success' | false
+ %i[skipped success] | true | 'skipped' | false
+ %i[created] | false | 'created' | false
+ %i[preparing] | false | 'preparing' | false
+ %i[canceled success skipped] | false | 'canceled' | false
+ %i[canceled success skipped] | true | 'skipped' | false
+ %i[pending created skipped] | false | 'pending' | false
+ %i[pending created skipped success] | false | 'running' | false
+ %i[running created skipped success] | false | 'running' | false
+ %i[pending created skipped] | true | 'skipped' | false
+ %i[pending created skipped success] | true | 'skipped' | false
+ %i[running created skipped success] | true | 'skipped' | false
+ %i[success waiting_for_resource] | false | 'waiting_for_resource' | false
+ %i[success waiting_for_callback] | false | 'waiting_for_callback' | false
+ %i[success manual] | false | 'manual' | false
+ %i[success scheduled] | false | 'scheduled' | false
+ %i[created preparing] | false | 'preparing' | false
+ %i[created success pending] | false | 'running' | false
+ %i[skipped success failed] | false | 'failed' | false
+ %i[skipped success failed] | true | 'skipped' | false
+ %i[success manual] | true | 'manual' | false
+ %i[success failed created] | true | 'running' | false
end
with_them do
@@ -78,13 +79,13 @@ RSpec.describe Gitlab::Ci::Status::Composite, feature_category: :continuous_inte
context 'allow_failure: true' do
where(:build_statuses, :dag, :result, :has_warnings) do
- %i(manual) | false | 'skipped' | false
- %i(skipped failed) | false | 'success' | true
- %i(skipped failed) | true | 'skipped' | true
- %i(success manual) | true | 'skipped' | false
- %i(success manual) | false | 'success' | false
- %i(created failed) | false | 'created' | true
- %i(preparing manual) | false | 'preparing' | false
+ %i[manual] | false | 'skipped' | false
+ %i[skipped failed] | false | 'success' | true
+ %i[skipped failed] | true | 'skipped' | true
+ %i[success manual] | true | 'skipped' | false
+ %i[success manual] | false | 'success' | false
+ %i[created failed] | false | 'created' | true
+ %i[preparing manual] | false | 'preparing' | false
end
with_them do
diff --git a/spec/lib/gitlab/ci/status/stage/factory_spec.rb b/spec/lib/gitlab/ci/status/stage/factory_spec.rb
index 34e430202c9..98fefea7bdf 100644
--- a/spec/lib/gitlab/ci/status/stage/factory_spec.rb
+++ b/spec/lib/gitlab/ci/status/stage/factory_spec.rb
@@ -22,7 +22,7 @@ RSpec.describe Gitlab::Ci::Status::Stage::Factory, feature_category: :continuous
end
context 'when stage has a core status' do
- (Ci::HasStatus::AVAILABLE_STATUSES - %w(manual skipped scheduled)).each do |core_status|
+ (Ci::HasStatus::AVAILABLE_STATUSES - %w[manual skipped scheduled]).each do |core_status|
context "when core status is #{core_status}" do
let(:stage) { create(:ci_stage, pipeline: pipeline, status: core_status) }
diff --git a/spec/lib/gitlab/ci/status/waiting_for_callback_spec.rb b/spec/lib/gitlab/ci/status/waiting_for_callback_spec.rb
new file mode 100644
index 00000000000..6c833e96137
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/waiting_for_callback_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Status::WaitingForCallback, feature_category: :deployment_management do
+ subject do
+ described_class.new(double, double)
+ end
+
+ describe '#text' do
+ it { expect(subject.text).to eq 'Waiting' }
+ end
+
+ describe '#label' do
+ it { expect(subject.label).to eq 'waiting for callback' }
+ end
+
+ describe '#icon' do
+ it { expect(subject.icon).to eq 'status_pending' }
+ end
+
+ describe '#favicon' do
+ it { expect(subject.favicon).to eq 'favicon_status_pending' }
+ end
+
+ describe '#group' do
+ it { expect(subject.group).to eq 'waiting-for-callback' }
+ end
+
+ describe '#name' do
+ it { expect(subject.name).to eq 'WAITING_FOR_CALLBACK' }
+ end
+
+ describe '#details_path' do
+ it { expect(subject.details_path).to be_nil }
+ end
+end
diff --git a/spec/lib/gitlab/ci/tags/bulk_insert_spec.rb b/spec/lib/gitlab/ci/tags/bulk_insert_spec.rb
index 460ecbb05d0..c5125689207 100644
--- a/spec/lib/gitlab/ci/tags/bulk_insert_spec.rb
+++ b/spec/lib/gitlab/ci/tags/bulk_insert_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe Gitlab::Ci::Tags::BulkInsert do
subject(:service) { described_class.new(statuses) }
describe 'gem version' do
- let(:acceptable_version) { '9.0.1' }
+ let(:acceptable_version) { '10.0.0' }
let(:error_message) do
<<~MESSAGE
diff --git a/spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb
index acb296082b8..dc9999ab9e4 100644
--- a/spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb
@@ -58,7 +58,7 @@ RSpec.describe 'Jobs/Deploy.gitlab-ci.yml' do
context 'with no cluster or agent' do
it 'does not create any kubernetes deployment jobs' do
- expect(build_names).to eq %w(placeholder)
+ expect(build_names).to eq %w[placeholder]
end
end
@@ -68,7 +68,7 @@ RSpec.describe 'Jobs/Deploy.gitlab-ci.yml' do
end
it 'does not create any kubernetes deployment jobs' do
- expect(build_names).to eq %w(placeholder)
+ expect(build_names).to eq %w[placeholder]
end
end
@@ -81,7 +81,7 @@ RSpec.describe 'Jobs/Deploy.gitlab-ci.yml' do
it 'when CI_DEPLOY_FREEZE is present' do
create(:ci_variable, project: project, key: 'CI_DEPLOY_FREEZE', value: 'true')
- expect(build_names).to eq %w(placeholder)
+ expect(build_names).to eq %w[placeholder]
end
it 'when CANARY_ENABLED' do
diff --git a/spec/lib/gitlab/ci/templates/Jobs/sast_iac_latest_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Jobs/sast_iac_latest_gitlab_ci_yaml_spec.rb
index 2b9213ea921..86bc9259789 100644
--- a/spec/lib/gitlab/ci/templates/Jobs/sast_iac_latest_gitlab_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/Jobs/sast_iac_latest_gitlab_ci_yaml_spec.rb
@@ -44,7 +44,7 @@ RSpec.describe 'Jobs/SAST-IaC.latest.gitlab-ci.yml', feature_category: :continuo
it 'creates a pipeline with the expected jobs' do
expect(pipeline).to be_merge_request_event
expect(pipeline.errors.full_messages).to be_empty
- expect(build_names).to match_array(%w(kics-iac-sast))
+ expect(build_names).to match_array(%w[kics-iac-sast])
end
end
end
diff --git a/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb
index 09ca2678de5..7471dc58e44 100644
--- a/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb
@@ -94,7 +94,7 @@ RSpec.describe 'Auto-DevOps.gitlab-ci.yml', feature_category: :auto_devops do
project.repository.create_branch(pipeline_branch, default_branch)
end
- %w(review_ecs review_fargate).each do |job|
+ %w[review_ecs review_fargate].each do |job|
it_behaves_like 'no ECS job when AUTO_DEVOPS_PLATFORM_TARGET is not present' do
let(:job_name) { job }
end
@@ -142,7 +142,7 @@ RSpec.describe 'Auto-DevOps.gitlab-ci.yml', feature_category: :auto_devops do
context 'when the project has no active cluster' do
it 'only creates a build and a test stage' do
- expect(pipeline.stages_names).to eq(%w(build test))
+ expect(pipeline.stages_names).to eq(%w[build test])
end
it_behaves_like 'no Kubernetes deployment job'
@@ -273,25 +273,25 @@ RSpec.describe 'Auto-DevOps.gitlab-ci.yml', feature_category: :auto_devops do
using RSpec::Parameterized::TableSyntax
where(:case_name, :files, :variables, :include_build_names, :not_include_build_names) do
- 'No match' | { 'README.md' => '' } | {} | %w() | %w(build test)
- 'Buildpack' | { 'README.md' => '' } | { 'BUILDPACK_URL' => 'http://example.com' } | %w(build test) | %w()
- 'Explicit set' | { 'README.md' => '' } | { 'AUTO_DEVOPS_EXPLICITLY_ENABLED' => '1' } | %w(build test) | %w()
- 'Explicit unset' | { 'README.md' => '' } | { 'AUTO_DEVOPS_EXPLICITLY_ENABLED' => '0' } | %w() | %w(build test)
- 'DOCKERFILE_PATH' | { 'README.md' => '' } | { 'DOCKERFILE_PATH' => 'Docker.file' } | %w(build test) | %w()
- 'Dockerfile' | { 'Dockerfile' => '' } | {} | %w(build test) | %w()
- 'Clojure' | { 'project.clj' => '' } | {} | %w(build test) | %w()
- 'Go modules' | { 'go.mod' => '' } | {} | %w(build test) | %w()
- 'Go gb' | { 'src/gitlab.com/gopackage.go' => '' } | {} | %w(build test) | %w()
- 'Gradle' | { 'gradlew' => '' } | {} | %w(build test) | %w()
- 'Java' | { 'pom.xml' => '' } | {} | %w(build test) | %w()
- 'Multi-buildpack' | { '.buildpacks' => '' } | {} | %w(build test) | %w()
- 'NodeJS' | { 'package.json' => '' } | {} | %w(build test) | %w()
- 'PHP' | { 'composer.json' => '' } | {} | %w(build test) | %w()
- 'Play' | { 'conf/application.conf' => '' } | {} | %w(build test) | %w()
- 'Python' | { 'Pipfile' => '' } | {} | %w(build test) | %w()
- 'Ruby' | { 'Gemfile' => '' } | {} | %w(build test) | %w()
- 'Scala' | { 'build.sbt' => '' } | {} | %w(build test) | %w()
- 'Static' | { '.static' => '' } | {} | %w(build test) | %w()
+ 'No match' | { 'README.md' => '' } | {} | %w[] | %w[build test]
+ 'Buildpack' | { 'README.md' => '' } | { 'BUILDPACK_URL' => 'http://example.com' } | %w[build test] | %w[]
+ 'Explicit set' | { 'README.md' => '' } | { 'AUTO_DEVOPS_EXPLICITLY_ENABLED' => '1' } | %w[build test] | %w[]
+ 'Explicit unset' | { 'README.md' => '' } | { 'AUTO_DEVOPS_EXPLICITLY_ENABLED' => '0' } | %w[] | %w[build test]
+ 'DOCKERFILE_PATH' | { 'README.md' => '' } | { 'DOCKERFILE_PATH' => 'Docker.file' } | %w[build test] | %w[]
+ 'Dockerfile' | { 'Dockerfile' => '' } | {} | %w[build test] | %w[]
+ 'Clojure' | { 'project.clj' => '' } | {} | %w[build test] | %w[]
+ 'Go modules' | { 'go.mod' => '' } | {} | %w[build test] | %w[]
+ 'Go gb' | { 'src/gitlab.com/gopackage.go' => '' } | {} | %w[build test] | %w[]
+ 'Gradle' | { 'gradlew' => '' } | {} | %w[build test] | %w[]
+ 'Java' | { 'pom.xml' => '' } | {} | %w[build test] | %w[]
+ 'Multi-buildpack' | { '.buildpacks' => '' } | {} | %w[build test] | %w[]
+ 'NodeJS' | { 'package.json' => '' } | {} | %w[build test] | %w[]
+ 'PHP' | { 'composer.json' => '' } | {} | %w[build test] | %w[]
+ 'Play' | { 'conf/application.conf' => '' } | {} | %w[build test] | %w[]
+ 'Python' | { 'Pipfile' => '' } | {} | %w[build test] | %w[]
+ 'Ruby' | { 'Gemfile' => '' } | {} | %w[build test] | %w[]
+ 'Scala' | { 'build.sbt' => '' } | {} | %w[build test] | %w[]
+ 'Static' | { '.static' => '' } | {} | %w[build test] | %w[]
end
with_them do
diff --git a/spec/lib/gitlab/ci/variables/collection/item_spec.rb b/spec/lib/gitlab/ci/variables/collection/item_spec.rb
index d96c8f1bd0c..aa612899f4b 100644
--- a/spec/lib/gitlab/ci/variables/collection/item_spec.rb
+++ b/spec/lib/gitlab/ci/variables/collection/item_spec.rb
@@ -123,11 +123,11 @@ RSpec.describe Gitlab::Ci::Variables::Collection::Item, feature_category: :secre
},
"simple variable reference": {
variable: { key: 'VAR', value: 'something_$VAR2' },
- expected_depends_on: %w(VAR2)
+ expected_depends_on: %w[VAR2]
},
"complex expansion": {
variable: { key: 'VAR', value: 'something_${VAR2}_$VAR3' },
- expected_depends_on: %w(VAR2 VAR3)
+ expected_depends_on: %w[VAR2 VAR3]
},
"complex expansion in raw variable": {
variable: { key: 'VAR', value: 'something_${VAR2}_$VAR3', raw: true },
@@ -135,7 +135,7 @@ RSpec.describe Gitlab::Ci::Variables::Collection::Item, feature_category: :secre
},
"complex expansions for Windows": {
variable: { key: 'variable3', value: 'key%variable%%variable2%' },
- expected_depends_on: %w(variable variable2)
+ expected_depends_on: %w[variable variable2]
}
}
end
@@ -282,7 +282,7 @@ RSpec.describe Gitlab::Ci::Variables::Collection::Item, feature_category: :secre
it '#depends_on contains names of dependencies' do
runner_variable = described_class.new(key: 'CI_VAR', value: '${CI_VAR_2}-123-$CI_VAR_3')
- expect(runner_variable.depends_on).to eq(%w(CI_VAR_2 CI_VAR_3))
+ expect(runner_variable.depends_on).to eq(%w[CI_VAR_2 CI_VAR_3])
end
end
diff --git a/spec/lib/gitlab/ci/yaml_processor/dag_spec.rb b/spec/lib/gitlab/ci/yaml_processor/dag_spec.rb
index 082febacbd7..496d89403d5 100644
--- a/spec/lib/gitlab/ci/yaml_processor/dag_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor/dag_spec.rb
@@ -3,38 +3,48 @@
require 'fast_spec_helper'
require 'tsort'
-RSpec.describe Gitlab::Ci::YamlProcessor::Dag do
+RSpec.describe Gitlab::Ci::YamlProcessor::Dag, feature_category: :pipeline_composition do
let(:nodes) { {} }
subject(:result) { described_class.new(nodes).tsort }
context 'when it is a regular pipeline' do
let(:nodes) do
- { 'job_c' => %w(job_b job_d), 'job_d' => %w(job_a), 'job_b' => %w(job_a), 'job_a' => %w() }
+ { 'job_c' => %w[job_b job_d], 'job_d' => %w[job_a], 'job_b' => %w[job_a], 'job_a' => %w[] }
end
it 'returns ordered jobs' do
- expect(result).to eq(%w(job_a job_b job_d job_c))
+ expect(result).to eq(%w[job_a job_b job_d job_c])
end
end
context 'when there is a circular dependency' do
let(:nodes) do
- { 'job_a' => %w(job_c), 'job_b' => %w(job_a), 'job_c' => %w(job_b) }
+ { 'job_a' => %w[job_c], 'job_b' => %w[job_a], 'job_c' => %w[job_b] }
end
- it 'raises TSort::Cyclic' do
+ it 'raises TSort::Cyclic error' do
expect { result }.to raise_error(TSort::Cyclic, /topological sort failed/)
end
+
+ context 'when a job has a self-dependency' do
+ let(:nodes) do
+ { 'job_a' => %w[job_a] }
+ end
+
+ it 'raises TSort::Cyclic error' do
+ expect { result }.to raise_error(TSort::Cyclic, "self-dependency: job_a")
+ end
+ end
end
context 'when there are some missing jobs' do
let(:nodes) do
- { 'job_a' => %w(job_d job_f), 'job_b' => %w(job_a job_c job_e) }
+ { 'job_a' => %w[job_d job_f], 'job_b' => %w[job_a job_c job_e] }
end
it 'ignores the missing ones and returns in a valid order' do
- expect(result).to eq(%w(job_d job_f job_a job_c job_e job_b))
+ expect(result).to eq(%w[job_d job_f job_a job_c job_e job_b])
end
end
end
diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb
index 81bc8c7ab9a..f01c1c7d053 100644
--- a/spec/lib/gitlab/ci/yaml_processor_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb
@@ -1510,7 +1510,7 @@ module Gitlab
it 'correctly extends rspec job' do
expect(config_processor.builds).to be_one
- expect(subject.dig(:options, :script)).to eq %w(test)
+ expect(subject.dig(:options, :script)).to eq %w[test]
expect(subject.dig(:options, :image, :name)).to eq 'ruby:alpine'
end
end
@@ -1595,7 +1595,7 @@ module Gitlab
it 'correctly extends rspec job' do
expect(config_processor.builds).to be_one
expect(subject.dig(:options, :before_script)).to eq ["bundle install"]
- expect(subject.dig(:options, :script)).to eq %w(rspec)
+ expect(subject.dig(:options, :script)).to eq %w[rspec]
expect(subject.dig(:options, :image, :name)).to eq 'image:test'
expect(subject.dig(:when)).to eq 'always'
end
@@ -2386,7 +2386,7 @@ module Gitlab
end
context 'dependencies to builds' do
- let(:dependencies) { %w(build1 build2) }
+ let(:dependencies) { %w[build1 build2] }
it { is_expected.to be_valid }
end
@@ -2457,7 +2457,7 @@ module Gitlab
end
context 'needs a job from the same stage' do
- let(:needs) { %w(test2) }
+ let(:needs) { %w[test2] }
it 'creates jobs with valid specifications' do
expect(subject.builds.size).to eq(7)
@@ -2494,7 +2494,7 @@ module Gitlab
end
context 'needs two builds' do
- let(:needs) { %w(build1 build2) }
+ let(:needs) { %w[build1 build2] }
it "does create jobs with valid specification" do
expect(subject.builds.size).to eq(7)
@@ -2578,7 +2578,7 @@ module Gitlab
end
context 'needs parallel job' do
- let(:needs) { %w(parallel) }
+ let(:needs) { %w[parallel] }
it "does create jobs with valid specification" do
expect(subject.builds.size).to eq(7)
@@ -2707,7 +2707,7 @@ module Gitlab
context 'duplicate needs' do
context 'when needs are specified in an array' do
- let(:needs) { %w(build1 build1) }
+ let(:needs) { %w[build1 build1] }
it_behaves_like 'returns errors', 'test1 has the following needs duplicated: build1.'
end
@@ -2736,8 +2736,8 @@ module Gitlab
end
context 'needs and dependencies that are mismatching' do
- let(:needs) { %w(build1) }
- let(:dependencies) { %w(build2) }
+ let(:needs) { %w[build1] }
+ let(:dependencies) { %w[build2] }
it_behaves_like 'returns errors', 'jobs:test1 dependencies the build2 should be part of needs'
end
@@ -2750,13 +2750,13 @@ module Gitlab
]
end
- let(:dependencies) { %w(build3) }
+ let(:dependencies) { %w[build3] }
it_behaves_like 'returns errors', 'jobs:test1 dependencies the build3 should be part of needs'
end
context 'needs with an array type and dependency with a string type' do
- let(:needs) { %w(build1) }
+ let(:needs) { %w[build1] }
let(:dependencies) { 'deploy' }
it_behaves_like 'returns errors', 'jobs:test1 dependencies should be an array of strings'
@@ -2764,7 +2764,7 @@ module Gitlab
context 'needs with a string type and dependency with an array type' do
let(:needs) { 'build1' }
- let(:dependencies) { %w(deploy) }
+ let(:dependencies) { %w[deploy] }
it_behaves_like 'returns errors', 'jobs:test1:needs config can only be a hash or an array'
end
@@ -3252,7 +3252,7 @@ module Gitlab
end
context 'returns errors if job stage is not a defined stage' do
- let(:config) { YAML.dump({ stages: %w(build test), rspec: { script: "test", stage: "acceptance" } }) }
+ let(:config) { YAML.dump({ stages: %w[build test], rspec: { script: "test", stage: "acceptance" } }) }
it_behaves_like 'returns errors', 'rspec job: chosen stage does not exist; available stages are .pre, build, test, .post'
end
@@ -3288,37 +3288,37 @@ module Gitlab
end
context 'returns errors if job artifacts:name is not an a string' do
- let(:config) { YAML.dump({ stages: %w(build test), rspec: { script: "test", artifacts: { name: 1 } } }) }
+ let(:config) { YAML.dump({ stages: %w[build test], rspec: { script: "test", artifacts: { name: 1 } } }) }
it_behaves_like 'returns errors', 'jobs:rspec:artifacts name should be a string'
end
context 'returns errors if job artifacts:when is not an a predefined value' do
- let(:config) { YAML.dump({ stages: %w(build test), rspec: { script: "test", artifacts: { when: 1 } } }) }
+ let(:config) { YAML.dump({ stages: %w[build test], rspec: { script: "test", artifacts: { when: 1 } } }) }
it_behaves_like 'returns errors', 'jobs:rspec:artifacts when should be one of: on_success, on_failure, always'
end
context 'returns errors if job artifacts:expire_in is not an a string' do
- let(:config) { YAML.dump({ stages: %w(build test), rspec: { script: "test", artifacts: { expire_in: 1 } } }) }
+ let(:config) { YAML.dump({ stages: %w[build test], rspec: { script: "test", artifacts: { expire_in: 1 } } }) }
it_behaves_like 'returns errors', 'jobs:rspec:artifacts expire in should be a duration'
end
context 'returns errors if job artifacts:expire_in is not an a valid duration' do
- let(:config) { YAML.dump({ stages: %w(build test), rspec: { script: "test", artifacts: { expire_in: "7 elephants" } } }) }
+ let(:config) { YAML.dump({ stages: %w[build test], rspec: { script: "test", artifacts: { expire_in: "7 elephants" } } }) }
it_behaves_like 'returns errors', 'jobs:rspec:artifacts expire in should be a duration'
end
context 'returns errors if job artifacts:untracked is not an array of strings' do
- let(:config) { YAML.dump({ stages: %w(build test), rspec: { script: "test", artifacts: { untracked: "string" } } }) }
+ let(:config) { YAML.dump({ stages: %w[build test], rspec: { script: "test", artifacts: { untracked: "string" } } }) }
it_behaves_like 'returns errors', 'jobs:rspec:artifacts untracked should be a boolean value'
end
context 'returns errors if job artifacts:paths is not an array of strings' do
- let(:config) { YAML.dump({ stages: %w(build test), rspec: { script: "test", artifacts: { paths: "string" } } }) }
+ let(:config) { YAML.dump({ stages: %w[build test], rspec: { script: "test", artifacts: { paths: "string" } } }) }
it_behaves_like 'returns errors', 'jobs:rspec:artifacts paths should be an array of strings'
end
@@ -3342,49 +3342,49 @@ module Gitlab
end
context 'returns errors if job cache:key is not an a string' do
- let(:config) { YAML.dump({ stages: %w(build test), rspec: { script: "test", cache: { key: 1 } } }) }
+ let(:config) { YAML.dump({ stages: %w[build test], rspec: { script: "test", cache: { key: 1 } } }) }
it_behaves_like 'returns errors', "jobs:rspec:cache:key should be a hash, a string or a symbol"
end
context 'returns errors if job cache:key:files is not an array of strings' do
- let(:config) { YAML.dump({ stages: %w(build test), rspec: { script: "test", cache: { key: { files: [1] } } } }) }
+ let(:config) { YAML.dump({ stages: %w[build test], rspec: { script: "test", cache: { key: { files: [1] } } } }) }
it_behaves_like 'returns errors', 'jobs:rspec:cache:key:files config should be an array of strings'
end
context 'returns errors if job cache:key:files is an empty array' do
- let(:config) { YAML.dump({ stages: %w(build test), rspec: { script: "test", cache: { key: { files: [] } } } }) }
+ let(:config) { YAML.dump({ stages: %w[build test], rspec: { script: "test", cache: { key: { files: [] } } } }) }
it_behaves_like 'returns errors', 'jobs:rspec:cache:key:files config requires at least 1 item'
end
context 'returns errors if job defines only cache:key:prefix' do
- let(:config) { YAML.dump({ stages: %w(build test), rspec: { script: "test", cache: { key: { prefix: 'prefix-key' } } } }) }
+ let(:config) { YAML.dump({ stages: %w[build test], rspec: { script: "test", cache: { key: { prefix: 'prefix-key' } } } }) }
it_behaves_like 'returns errors', 'jobs:rspec:cache:key config missing required keys: files'
end
context 'returns errors if job cache:key:prefix is not an a string' do
- let(:config) { YAML.dump({ stages: %w(build test), rspec: { script: "test", cache: { key: { prefix: 1, files: ['file'] } } } }) }
+ let(:config) { YAML.dump({ stages: %w[build test], rspec: { script: "test", cache: { key: { prefix: 1, files: ['file'] } } } }) }
it_behaves_like 'returns errors', 'jobs:rspec:cache:key:prefix config should be a string or symbol'
end
context "returns errors if job cache:untracked is not an array of strings" do
- let(:config) { YAML.dump({ stages: %w(build test), rspec: { script: "test", cache: { untracked: "string" } } }) }
+ let(:config) { YAML.dump({ stages: %w[build test], rspec: { script: "test", cache: { untracked: "string" } } }) }
it_behaves_like 'returns errors', "jobs:rspec:cache:untracked config should be a boolean value"
end
context "returns errors if job cache:paths is not an array of strings" do
- let(:config) { YAML.dump({ stages: %w(build test), rspec: { script: "test", cache: { paths: "string" } } }) }
+ let(:config) { YAML.dump({ stages: %w[build test], rspec: { script: "test", cache: { paths: "string" } } }) }
it_behaves_like 'returns errors', "jobs:rspec:cache:paths config should be an array of strings"
end
context "returns errors if job dependencies is not an array of strings" do
- let(:config) { YAML.dump({ stages: %w(build test), rspec: { script: "test", dependencies: "string" } }) }
+ let(:config) { YAML.dump({ stages: %w[build test], rspec: { script: "test", dependencies: "string" } }) }
it_behaves_like 'returns errors', "jobs:rspec dependencies should be an array of strings"
end
@@ -3433,7 +3433,24 @@ module Gitlab
YAML
end
- it_behaves_like 'returns errors', 'The pipeline has circular dependencies'
+ it_behaves_like 'returns errors', 'The pipeline has circular dependencies: topological sort failed: ["job_a", "job_c", "job_b"]'
+
+ context 'when a job has a self-dependency' do
+ let(:config) do
+ <<~YAML
+ job_0:
+ stage: test
+ script: build
+
+ job:
+ stage: test
+ script: build
+ needs: [job_0, job]
+ YAML
+ end
+
+ it_behaves_like 'returns errors', 'The pipeline has circular dependencies: self-dependency: job'
+ end
end
end
@@ -3668,6 +3685,70 @@ module Gitlab
it { is_expected.to be_valid }
end
end
+
+ context 'for pages jobs', feature_category: :pages do
+ context 'on publish option' do
+ context 'when not in a pages job' do
+ let(:config) do
+ <<-EOYML
+ not-pages:
+ script: echo
+ publish: 'foo'
+ EOYML
+ end
+
+ it_behaves_like 'returns errors', 'jobs:not-pages publish can only be used within a `pages` job'
+ end
+
+ context 'when in a pages job' do
+ let(:config) do
+ <<-EOYML
+ pages:
+ script: echo
+ publish: 'foo'
+ EOYML
+ end
+
+ it { is_expected.to be_valid }
+
+ it 'sets the publish configuration' do
+ expect(subject.builds.first[:options][:publish]).to eq('foo')
+ end
+ end
+ end
+
+ context 'on pages option' do
+ context 'when not in a pages job' do
+ let(:config) do
+ <<-EOYML
+ not-pages:
+ script: echo
+ pages:
+ path_prefix: 'foo'
+ EOYML
+ end
+
+ it_behaves_like 'returns errors', 'jobs:not-pages pages can only be used within a `pages` job'
+ end
+
+ context 'when in a pages job' do
+ let(:config) do
+ <<-EOYML
+ pages:
+ script: echo
+ pages:
+ path_prefix: 'foo'
+ EOYML
+ end
+
+ it { is_expected.to be_valid }
+
+ it 'sets the pages configuration' do
+ expect(subject.builds.first[:options][:pages]).to eq(path_prefix: 'foo')
+ end
+ end
+ end
+ end
end
end
end
diff --git a/spec/lib/gitlab/composer/version_index_spec.rb b/spec/lib/gitlab/composer/version_index_spec.rb
index 63efa8cae95..c5bc6dc0195 100644
--- a/spec/lib/gitlab/composer/version_index_spec.rb
+++ b/spec/lib/gitlab/composer/version_index_spec.rb
@@ -83,32 +83,6 @@ RSpec.describe Gitlab::Composer::VersionIndex, feature_category: :package_regist
it_behaves_like 'returns the packages json'
end
-
- context 'with composer_use_ssh_source_urls disabled' do
- before do
- stub_feature_flags(composer_use_ssh_source_urls: false)
- end
-
- context 'with a public project' do
- it_behaves_like 'returns the packages json'
- end
-
- context 'with an internal project' do
- before do
- project.update!(visibility: Gitlab::VisibilityLevel::INTERNAL)
- end
-
- it_behaves_like 'returns the packages json'
- end
-
- context 'with a private project' do
- before do
- project.update!(visibility: Gitlab::VisibilityLevel::PRIVATE)
- end
-
- it_behaves_like 'returns the packages json'
- end
- end
end
describe '#sha' do
diff --git a/spec/lib/gitlab/config/entry/factory_spec.rb b/spec/lib/gitlab/config/entry/factory_spec.rb
index be4dfd31651..bbbba0cf7cd 100644
--- a/spec/lib/gitlab/config/entry/factory_spec.rb
+++ b/spec/lib/gitlab/config/entry/factory_spec.rb
@@ -21,16 +21,16 @@ RSpec.describe Gitlab::Config::Entry::Factory do
context 'when setting a concrete value' do
it 'creates entry with valid value' do
entry = factory
- .value(%w(ls pwd))
+ .value(%w[ls pwd])
.create!
- expect(entry.value).to eq %w(ls pwd)
+ expect(entry.value).to eq %w[ls pwd]
end
context 'when setting description' do
before do
factory
- .value(%w(ls pwd))
+ .value(%w[ls pwd])
.with(description: 'test description')
end
@@ -41,7 +41,7 @@ RSpec.describe Gitlab::Config::Entry::Factory do
it 'creates entry with description' do
entry = factory.create!
- expect(entry.value).to eq %w(ls pwd)
+ expect(entry.value).to eq %w[ls pwd]
expect(entry.description).to eq 'test description'
end
end
@@ -49,7 +49,7 @@ RSpec.describe Gitlab::Config::Entry::Factory do
context 'when setting inherit' do
before do
factory
- .value(%w(ls pwd))
+ .value(%w[ls pwd])
.with(inherit: true)
end
@@ -61,7 +61,7 @@ RSpec.describe Gitlab::Config::Entry::Factory do
context 'when setting key' do
it 'creates entry with custom key' do
entry = factory
- .value(%w(ls pwd))
+ .value(%w[ls pwd])
.with(key: 'test key')
.create!
diff --git a/spec/lib/gitlab/config/entry/validators_spec.rb b/spec/lib/gitlab/config/entry/validators_spec.rb
index 6fa9f9d0767..e13c09f97ca 100644
--- a/spec/lib/gitlab/config/entry/validators_spec.rb
+++ b/spec/lib/gitlab/config/entry/validators_spec.rb
@@ -35,7 +35,7 @@ RSpec.describe Gitlab::Config::Entry::Validators, feature_category: :pipeline_co
expect(instance.valid?).to be(valid_result)
unless valid_result
- expect(instance.errors.messages_for(:config)).to include /please use only one of the following keys: foo, bar/
+ expect(instance.errors.messages_for(:config)).to include(/these keys cannot be used together: foo, bar/)
end
end
end
diff --git a/spec/lib/gitlab/config_checker/puma_rugged_checker_spec.rb b/spec/lib/gitlab/config_checker/puma_rugged_checker_spec.rb
deleted file mode 100644
index afee3c5536e..00000000000
--- a/spec/lib/gitlab/config_checker/puma_rugged_checker_spec.rb
+++ /dev/null
@@ -1,65 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::ConfigChecker::PumaRuggedChecker do
- describe '#check' do
- subject { described_class.check }
-
- context 'application is not puma' do
- before do
- allow(Gitlab::Runtime).to receive(:puma?).and_return(false)
- end
-
- it { is_expected.to be_empty }
- end
-
- context 'application is puma' do
- let(:notice_multi_threaded_puma_with_rugged) do
- {
- type: 'warning',
- message: 'Puma is running with a thread count above 1 and the Rugged '\
- 'service is enabled. This may decrease performance in some environments. '\
- 'See our <a href="https://docs.gitlab.com/ee/administration/operations/puma.html#performance-caveat-when-using-puma-with-rugged">documentation</a> '\
- 'for details of this issue.'
- }
- end
-
- before do
- allow(Gitlab::Runtime).to receive(:puma?).and_return(true)
- allow(described_class).to receive(:running_puma_with_multiple_threads?).and_return(multithreaded_puma)
- allow(described_class).to receive(:rugged_enabled_through_feature_flag?).and_return(rugged_enabled)
- end
-
- context 'not multithreaded_puma and rugged API enabled' do
- let(:multithreaded_puma) { false }
- let(:rugged_enabled) { true }
-
- it { is_expected.to be_empty }
- end
-
- context 'not multithreaded_puma and rugged API is not enabled' do
- let(:multithreaded_puma) { false }
- let(:rugged_enabled) { false }
-
- it { is_expected.to be_empty }
- end
-
- context 'multithreaded_puma and rugged API is not enabled' do
- let(:multithreaded_puma) { true }
- let(:rugged_enabled) { false }
-
- it { is_expected.to be_empty }
- end
-
- context 'multithreaded_puma and rugged API is enabled' do
- let(:multithreaded_puma) { true }
- let(:rugged_enabled) { true }
-
- it 'report multi_threaded_puma_with_rugged notices' do
- is_expected.to contain_exactly(notice_multi_threaded_puma_with_rugged)
- end
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/conflict/file_spec.rb b/spec/lib/gitlab/conflict/file_spec.rb
index 6ea8e6c6706..49252a6537c 100644
--- a/spec/lib/gitlab/conflict/file_spec.rb
+++ b/spec/lib/gitlab/conflict/file_spec.rb
@@ -58,7 +58,7 @@ RSpec.describe Gitlab::Conflict::File do
it 'returns a file containing only the chosen parts of the resolved sections' do
expect(resolved_lines.chunk { |line| line.type || 'both' }.map(&:first))
- .to eq(%w(both new both old both new both))
+ .to eq(%w[both new both old both new both])
end
end
@@ -193,7 +193,7 @@ RSpec.describe Gitlab::Conflict::File do
it 'sets conflict to true for sections with only changed lines' do
conflict_file.sections.select { |section| section[:conflict] }.each do |section|
section[:lines].each do |line|
- expect(line.type).to be_in(%w(new old))
+ expect(line.type).to be_in(%w[new old])
end
end
end
diff --git a/spec/lib/gitlab/data_builder/build_spec.rb b/spec/lib/gitlab/data_builder/build_spec.rb
index 66890315ee8..7afd16f53e5 100644
--- a/spec/lib/gitlab/data_builder/build_spec.rb
+++ b/spec/lib/gitlab/data_builder/build_spec.rb
@@ -49,7 +49,7 @@ RSpec.describe Gitlab::DataBuilder::Build, feature_category: :integrations do
it { expect(data[:commit][:id]).to eq(ci_build.pipeline.id) }
it { expect(data[:runner][:id]).to eq(ci_build.runner.id) }
- it { expect(data[:runner][:tags]).to match_array(%w(tag1 tag2)) }
+ it { expect(data[:runner][:tags]).to match_array(%w[tag1 tag2]) }
it { expect(data[:runner][:description]).to eq(ci_build.runner.description) }
it { expect(data[:runner][:runner_type]).to eq(ci_build.runner.runner_type) }
it { expect(data[:runner][:is_shared]).to eq(ci_build.runner.instance_type?) }
diff --git a/spec/lib/gitlab/data_builder/pipeline_spec.rb b/spec/lib/gitlab/data_builder/pipeline_spec.rb
index 351872ffbc5..ad7cd2dc736 100644
--- a/spec/lib/gitlab/data_builder/pipeline_spec.rb
+++ b/spec/lib/gitlab/data_builder/pipeline_spec.rb
@@ -66,7 +66,7 @@ RSpec.describe Gitlab::DataBuilder::Pipeline, feature_category: :continuous_inte
end
context 'build with runner' do
- let_it_be(:tag_names) { %w(tag-1 tag-2) }
+ let_it_be(:tag_names) { %w[tag-1 tag-2] }
let_it_be(:ci_runner) { create(:ci_runner, tag_list: tag_names.map { |n| ActsAsTaggableOn::Tag.create!(name: n) }) }
let_it_be(:build) { create(:ci_build, pipeline: pipeline, runner: ci_runner) }
diff --git a/spec/lib/gitlab/data_builder/push_spec.rb b/spec/lib/gitlab/data_builder/push_spec.rb
index a3dd4e49e83..02dc596c5eb 100644
--- a/spec/lib/gitlab/data_builder/push_spec.rb
+++ b/spec/lib/gitlab/data_builder/push_spec.rb
@@ -26,7 +26,7 @@ RSpec.describe Gitlab::DataBuilder::Push do
it 'returns commit hook data' do
expect(subject[:project]).to eq(project.hook_attrs)
- expect(subject[:commits].first.keys).to include(*%i(added removed modified))
+ expect(subject[:commits].first.keys).to include(*%i[added removed modified])
end
end
@@ -35,7 +35,7 @@ RSpec.describe Gitlab::DataBuilder::Push do
it 'returns commit hook data without include deltas' do
expect(subject[:project]).to eq(project.hook_attrs)
- expect(subject[:commits].first.keys).not_to include(*%i(added removed modified))
+ expect(subject[:commits].first.keys).not_to include(*%i[added removed modified])
end
end
end
diff --git a/spec/lib/gitlab/database/background_migration/batched_job_spec.rb b/spec/lib/gitlab/database/background_migration/batched_job_spec.rb
index d9b81a2be30..e1d1674d05c 100644
--- a/spec/lib/gitlab/database/background_migration/batched_job_spec.rb
+++ b/spec/lib/gitlab/database/background_migration/batched_job_spec.rb
@@ -25,7 +25,7 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedJob, type: :model d
describe 'state machine' do
let_it_be(:job) { create(:batched_background_migration_job, :failed) }
- it { expect(described_class.state_machine.states.map(&:name)).to eql(%i(pending running failed succeeded)) }
+ it { expect(described_class.state_machine.states.map(&:name)).to eql(%i[pending running failed succeeded]) }
context 'when a job is running' do
it 'logs the transition' do
diff --git a/spec/lib/gitlab/database/background_migration/batched_job_transition_log_spec.rb b/spec/lib/gitlab/database/background_migration/batched_job_transition_log_spec.rb
index 59f4f40c0ef..7cf7be8ffc2 100644
--- a/spec/lib/gitlab/database/background_migration/batched_job_transition_log_spec.rb
+++ b/spec/lib/gitlab/database/background_migration/batched_job_transition_log_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedJobTransitionLog, t
it { is_expected.to validate_presence_of(:batched_job) }
it { is_expected.to validate_length_of(:exception_class).is_at_most(100) }
it { is_expected.to validate_length_of(:exception_message).is_at_most(1000) }
- it { is_expected.to define_enum_for(:previous_status).with_values(%i(pending running failed succeeded)).with_prefix }
- it { is_expected.to define_enum_for(:next_status).with_values(%i(pending running failed succeeded)).with_prefix }
+ it { is_expected.to define_enum_for(:previous_status).with_values(%i[pending running failed succeeded]).with_prefix }
+ it { is_expected.to define_enum_for(:next_status).with_values(%i[pending running failed succeeded]).with_prefix }
end
end
diff --git a/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb b/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb
index 213dee0d19d..f70b38377d8 100644
--- a/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb
+++ b/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb
@@ -422,11 +422,12 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigration, type: :m
describe '#create_batched_job!' do
let(:batched_migration) do
- create(:batched_background_migration,
- batch_size: 999,
- sub_batch_size: 99,
- pause_ms: 250
- )
+ create(
+ :batched_background_migration,
+ batch_size: 999,
+ sub_batch_size: 99,
+ pause_ms: 250
+ )
end
it 'creates a batched_job with the correct batch configuration' do
diff --git a/spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb b/spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb
index 8d74d16f4e5..bbcb65109ce 100644
--- a/spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb
+++ b/spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb
@@ -32,17 +32,17 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationWrapper, '
end
it 'runs the migration job' do
- expect(job_class).to receive(:new)
- .with(start_id: 1,
- end_id: 10,
- batch_table: 'events',
- batch_column: 'id',
- sub_batch_size: 1,
- pause_ms: pause_ms,
- job_arguments: active_migration.job_arguments,
- connection: connection,
- sub_batch_exception: sub_batch_exception)
- .and_return(job_instance)
+ expect(job_class).to receive(:new).with(
+ start_id: 1,
+ end_id: 10,
+ batch_table: 'events',
+ batch_column: 'id',
+ sub_batch_size: 1,
+ pause_ms: pause_ms,
+ job_arguments: active_migration.job_arguments,
+ connection: connection,
+ sub_batch_exception: sub_batch_exception
+ ).and_return(job_instance)
expect(job_instance).to receive(:perform).with(no_args)
diff --git a/spec/lib/gitlab/database/background_migration/prometheus_metrics_spec.rb b/spec/lib/gitlab/database/background_migration/prometheus_metrics_spec.rb
index 1f256de35ec..8f380a8229c 100644
--- a/spec/lib/gitlab/database/background_migration/prometheus_metrics_spec.rb
+++ b/spec/lib/gitlab/database/background_migration/prometheus_metrics_spec.rb
@@ -5,11 +5,14 @@ require 'spec_helper'
RSpec.describe Gitlab::Database::BackgroundMigration::PrometheusMetrics, :prometheus do
describe '#track' do
let(:job_record) do
- build(:batched_background_migration_job, :succeeded,
- started_at: Time.current - 2.minutes,
- finished_at: Time.current - 1.minute,
- updated_at: Time.current,
- metrics: { 'timings' => { 'update_all' => [0.05, 0.2, 0.4, 0.9, 4] } })
+ build(
+ :batched_background_migration_job,
+ :succeeded,
+ started_at: Time.current - 2.minutes,
+ finished_at: Time.current - 1.minute,
+ updated_at: Time.current,
+ metrics: { 'timings' => { 'update_all' => [0.05, 0.2, 0.4, 0.9, 4] } }
+ )
end
let(:labels) { job_record.batched_migration.prometheus_labels }
diff --git a/spec/lib/gitlab/database/bulk_update_spec.rb b/spec/lib/gitlab/database/bulk_update_spec.rb
index fa519cffd6b..2f0859dba74 100644
--- a/spec/lib/gitlab/database/bulk_update_spec.rb
+++ b/spec/lib/gitlab/database/bulk_update_spec.rb
@@ -92,7 +92,7 @@ RSpec.describe Gitlab::Database::BulkUpdate do
end
context 'validates prepared_statements support', :reestablished_active_record_base,
- :suppress_gitlab_schemas_validate_connection do
+ :suppress_gitlab_schemas_validate_connection do
using RSpec::Parameterized::TableSyntax
where(:prepared_statements) do
diff --git a/spec/lib/gitlab/database/dictionary_spec.rb b/spec/lib/gitlab/database/dictionary_spec.rb
new file mode 100644
index 00000000000..6d2de41468b
--- /dev/null
+++ b/spec/lib/gitlab/database/dictionary_spec.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::Dictionary, feature_category: :database do
+ subject(:database_dictionary) { described_class.new(file_path) }
+
+ context 'for a table' do
+ let(:file_path) { 'db/docs/application_settings.yml' }
+
+ describe '#name_and_schema' do
+ it 'returns the name of the table and its gitlab schema' do
+ expect(database_dictionary.name_and_schema).to match_array(['application_settings', :gitlab_main_clusterwide])
+ end
+ end
+
+ describe '#table_name' do
+ it 'returns the name of the table' do
+ expect(database_dictionary.table_name).to eq('application_settings')
+ end
+ end
+
+ describe '#view_name' do
+ it 'returns nil' do
+ expect(database_dictionary.view_name).to be_nil
+ end
+ end
+
+ describe '#milestone' do
+ it 'returns the milestone in which the table was introduced' do
+ expect(database_dictionary.milestone).to eq('7.7')
+ end
+ end
+
+ describe '#gitlab_schema' do
+ it 'returns the gitlab_schema of the table' do
+ expect(database_dictionary.table_name).to eq('application_settings')
+ end
+ end
+
+ describe '#schema?' do
+ it 'checks if the given schema matches the schema of the table' do
+ expect(database_dictionary.schema?('gitlab_main')).to eq(false)
+ expect(database_dictionary.schema?('gitlab_main_clusterwide')).to eq(true)
+ end
+ end
+
+ describe '#key_name' do
+ it 'returns the value of the name of the table' do
+ expect(database_dictionary.key_name).to eq('application_settings')
+ end
+ end
+
+ describe '#validate!' do
+ it 'raises an error if the gitlab_schema is empty' do
+ allow(database_dictionary).to receive(:gitlab_schema).and_return(nil)
+
+ expect { database_dictionary.validate! }.to raise_error(Gitlab::Database::GitlabSchema::UnknownSchemaError)
+ end
+ end
+ end
+
+ context 'for a view' do
+ let(:file_path) { 'db/docs/views/postgres_constraints.yml' }
+
+ describe '#table_name' do
+ it 'returns nil' do
+ expect(database_dictionary.table_name).to be_nil
+ end
+ end
+
+ describe '#view_name' do
+ it 'returns the name of the view' do
+ expect(database_dictionary.view_name).to eq('postgres_constraints')
+ end
+ end
+
+ describe '#key_name' do
+ it 'returns the value of the name of the view' do
+ expect(database_dictionary.key_name).to eq('postgres_constraints')
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/dynamic_model_helpers_spec.rb b/spec/lib/gitlab/database/dynamic_model_helpers_spec.rb
index fe423b3639b..7ab50d47408 100644
--- a/spec/lib/gitlab/database/dynamic_model_helpers_spec.rb
+++ b/spec/lib/gitlab/database/dynamic_model_helpers_spec.rb
@@ -2,28 +2,83 @@
require 'spec_helper'
-RSpec.describe Gitlab::Database::DynamicModelHelpers do
+RSpec.describe Gitlab::Database::DynamicModelHelpers, feature_category: :database do
let(:including_class) { Class.new.include(described_class) }
let(:table_name) { Project.table_name }
let(:connection) { Project.connection }
describe '#define_batchable_model' do
- subject { including_class.new.define_batchable_model(table_name, connection: connection) }
+ subject(:model) { including_class.new.define_batchable_model(table_name, connection: connection) }
it 'is an ActiveRecord model' do
- expect(subject.ancestors).to include(ActiveRecord::Base)
+ expect(model.ancestors).to include(ActiveRecord::Base)
end
it 'includes EachBatch' do
- expect(subject.included_modules).to include(EachBatch)
+ expect(model.included_modules).to include(EachBatch)
end
it 'has the correct table name' do
- expect(subject.table_name).to eq(table_name)
+ expect(model.table_name).to eq(table_name)
end
it 'has the inheritance type column disable' do
- expect(subject.inheritance_column).to eq('_type_disabled')
+ expect(model.inheritance_column).to eq('_type_disabled')
+ end
+
+ context 'for primary key' do
+ subject(:model) do
+ including_class.new.define_batchable_model(table_name, connection: connection, primary_key: primary_key)
+ end
+
+ context 'when table primary key is a single column' do
+ let(:primary_key) { nil }
+
+ context 'when primary key is nil' do
+ it 'does not change the primary key from :id' do
+ expect(model.primary_key).to eq('id')
+ end
+ end
+
+ context 'when primary key is not nil' do
+ let(:primary_key) { 'other_column' }
+
+ it 'does not change the primary key from :id' do
+ expect(model.primary_key).to eq('id')
+ end
+ end
+ end
+
+ context 'when table has composite primary key' do
+ let(:primary_key) { nil }
+ let(:table_name) { :_test_composite_primary_key }
+
+ before do
+ connection.execute(<<~SQL)
+ DROP TABLE IF EXISTS #{table_name};
+
+ CREATE TABLE #{table_name} (
+ id integer NOT NULL,
+ partition_id integer NOT NULL,
+ PRIMARY KEY (id, partition_id)
+ );
+ SQL
+ end
+
+ context 'when primary key is nil' do
+ it 'does not change the primary key from nil' do
+ expect(model.primary_key).to be_nil
+ end
+ end
+
+ context 'when primary key is not nil' do
+ let(:primary_key) { 'id' }
+
+ it 'changes the primary key' do
+ expect(model.primary_key).to eq('id')
+ end
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/database/gitlab_schema_spec.rb b/spec/lib/gitlab/database/gitlab_schema_spec.rb
index a6de695c345..a47e53c18a5 100644
--- a/spec/lib/gitlab/database/gitlab_schema_spec.rb
+++ b/spec/lib/gitlab/database/gitlab_schema_spec.rb
@@ -95,10 +95,10 @@ RSpec.describe Gitlab::Database::GitlabSchema, feature_category: :database do
# ignore gitlab_internal due to `ar_internal_metadata`, `schema_migrations`
table_and_view_names = table_and_view_names
- .reject { |_, gitlab_schema| gitlab_schema == :gitlab_internal }
+ .reject { |database_dictionary| database_dictionary.schema?('gitlab_internal') }
duplicated_tables = table_and_view_names
- .group_by(&:first)
+ .group_by(&:key_name)
.select { |_, schemas| schemas.count > 1 }
.keys
diff --git a/spec/lib/gitlab/database/loose_foreign_keys_spec.rb b/spec/lib/gitlab/database/loose_foreign_keys_spec.rb
index 552df64096a..1824a50cb28 100644
--- a/spec/lib/gitlab/database/loose_foreign_keys_spec.rb
+++ b/spec/lib/gitlab/database/loose_foreign_keys_spec.rb
@@ -8,14 +8,14 @@ RSpec.describe Gitlab::Database::LooseForeignKeys do
it 'all definitions have assigned a known gitlab_schema and on_delete' do
is_expected.to all(have_attributes(
- options: a_hash_including(
- column: be_a(String),
- gitlab_schema: be_in(Gitlab::Database.schemas_to_base_models.symbolize_keys.keys),
- on_delete: be_in([:async_delete, :async_nullify])
- ),
- from_table: be_a(String),
- to_table: be_a(String)
- ))
+ options: a_hash_including(
+ column: be_a(String),
+ gitlab_schema: be_in(Gitlab::Database.schemas_to_base_models.symbolize_keys.keys),
+ on_delete: be_in([:async_delete, :async_nullify])
+ ),
+ from_table: be_a(String),
+ to_table: be_a(String)
+ ))
end
context 'ensure keys are sorted' do
diff --git a/spec/lib/gitlab/database/migration_helpers/cascading_namespace_settings_spec.rb b/spec/lib/gitlab/database/migration_helpers/cascading_namespace_settings_spec.rb
index e11ffe53c61..fb3da38a7be 100644
--- a/spec/lib/gitlab/database/migration_helpers/cascading_namespace_settings_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers/cascading_namespace_settings_spec.rb
@@ -27,7 +27,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers::CascadingNamespaceSettings do
it 'raises an error when some columns already exist' do
expect do
migration.add_cascading_namespace_setting(:cascading_setting, :integer)
- end.to raise_error %r/Existing columns: namespace_settings.cascading_setting, application_settings.lock_cascading_setting/
+ end.to raise_error %r{Existing columns: namespace_settings.cascading_setting, application_settings.lock_cascading_setting}
end
end
end
diff --git a/spec/lib/gitlab/database/migration_helpers/convert_to_bigint_spec.rb b/spec/lib/gitlab/database/migration_helpers/convert_to_bigint_spec.rb
index 1ff157b51d4..b0384a37746 100644
--- a/spec/lib/gitlab/database/migration_helpers/convert_to_bigint_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers/convert_to_bigint_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Gitlab::Database::MigrationHelpers::ConvertToBigint, feature_category: :database do
let(:migration) do
Class
- .new
+ .new(Gitlab::Database::Migration[2.1])
.include(described_class)
.include(Gitlab::Database::MigrationHelpers)
.new
@@ -73,4 +73,135 @@ RSpec.describe Gitlab::Database::MigrationHelpers::ConvertToBigint, feature_cate
expect(migration.columns_swapped?(:test_table, :id)).to eq(false)
end
end
+
+ describe '#add_bigint_column_indexes' do
+ let(:connection) { migration.connection }
+
+ let(:table_name) { '_test_table_bigint_indexes' }
+ let(:int_column) { 'token' }
+ let(:bigint_column) { 'token_convert_to_bigint' }
+
+ subject(:add_bigint_column_indexes) { migration.add_bigint_column_indexes(table_name, int_column) }
+
+ before do
+ connection.execute(<<~SQL)
+ CREATE TABLE IF NOT EXISTS public.#{table_name} (
+ name varchar(40),
+ #{int_column} integer
+ );
+ SQL
+
+ allow(migration).to receive(:transaction_open?).and_return(false)
+ allow(migration).to receive(:disable_statement_timeout).and_call_original
+ end
+
+ after do
+ connection.execute("DROP TABLE IF EXISTS #{table_name}")
+ end
+
+ context 'without corresponding bigint column' do
+ let(:error_msg) { "Bigint column '#{bigint_column}' does not exist on #{table_name}" }
+
+ it { expect { subject }.to raise_error(RuntimeError, error_msg) }
+ end
+
+ context 'with corresponding bigint column' do
+ let(:indexes) { connection.indexes(table_name) }
+ let(:int_column_indexes) { indexes.select { |i| i.columns.include?(int_column) } }
+ let(:bigint_column_indexes) { indexes.select { |i| i.columns.include?(bigint_column) } }
+
+ before do
+ connection.execute("ALTER TABLE #{table_name} ADD COLUMN #{bigint_column} bigint")
+ end
+
+ context 'without the integer column index' do
+ it 'does not create new bigint index' do
+ expect(int_column_indexes).to be_empty
+
+ add_bigint_column_indexes
+
+ expect(bigint_column_indexes).to be_empty
+ end
+ end
+
+ context 'with integer column indexes' do
+ let(:bigint_index_name) { ->(int_index_name) { migration.bigint_index_name(int_index_name) } }
+ let(:expected_bigint_indexes) do
+ [
+ {
+ name: bigint_index_name.call("hash_idx_#{table_name}"),
+ column: [bigint_column],
+ using: 'hash'
+ },
+ {
+ name: bigint_index_name.call("idx_#{table_name}"),
+ column: [bigint_column],
+ using: 'btree'
+ },
+ {
+ name: bigint_index_name.call("idx_#{table_name}_combined"),
+ column: "#{bigint_column}, lower((name)::text)",
+ where: "(#{bigint_column} IS NOT NULL)",
+ using: 'btree'
+ },
+ {
+ name: bigint_index_name.call("idx_#{table_name}_functional"),
+ column: "#{bigint_column}, lower((name)::text)",
+ using: 'btree'
+ },
+ {
+ name: bigint_index_name.call("idx_#{table_name}_ordered"),
+ column: [bigint_column],
+ order: 'DESC NULLS LAST',
+ using: 'btree'
+ },
+ {
+ name: bigint_index_name.call("idx_#{table_name}_ordered_multiple"),
+ column: [bigint_column, 'name'],
+ order: { bigint_column => 'DESC NULLS LAST', 'name' => 'desc' },
+ using: 'btree'
+ },
+ {
+ name: bigint_index_name.call("idx_#{table_name}_partial"),
+ column: [bigint_column],
+ where: "(#{bigint_column} IS NOT NULL)",
+ using: 'btree'
+ },
+ {
+ name: bigint_index_name.call("uniq_idx_#{table_name}"),
+ column: [bigint_column],
+ unique: true,
+ using: 'btree'
+ }
+ ]
+ end
+
+ before do
+ connection.execute(<<~SQL)
+ CREATE INDEX "hash_idx_#{table_name}" ON #{table_name} USING hash (#{int_column});
+ CREATE INDEX "idx_#{table_name}" ON #{table_name} USING btree (#{int_column});
+ CREATE INDEX "idx_#{table_name}_combined" ON #{table_name} USING btree (#{int_column}, lower((name)::text)) WHERE (#{int_column} IS NOT NULL);
+ CREATE INDEX "idx_#{table_name}_functional" ON #{table_name} USING btree (#{int_column}, lower((name)::text));
+ CREATE INDEX "idx_#{table_name}_ordered" ON #{table_name} USING btree (#{int_column} DESC NULLS LAST);
+ CREATE INDEX "idx_#{table_name}_ordered_multiple" ON #{table_name} USING btree (#{int_column} DESC NULLS LAST, name DESC);
+ CREATE INDEX "idx_#{table_name}_partial" ON #{table_name} USING btree (#{int_column}) WHERE (#{int_column} IS NOT NULL);
+ CREATE UNIQUE INDEX "uniq_idx_#{table_name}" ON #{table_name} USING btree (#{int_column});
+ SQL
+ end
+
+ it 'creates appropriate bigint indexes' do
+ expected_bigint_indexes.each do |bigint_index|
+ expect(migration).to receive(:add_concurrent_index).with(
+ table_name,
+ bigint_index[:column],
+ name: bigint_index[:name],
+ ** bigint_index.except(:name, :column)
+ )
+ end
+
+ add_bigint_column_indexes
+ end
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/database/migration_helpers/wraparound_autovacuum_spec.rb b/spec/lib/gitlab/database/migration_helpers/wraparound_autovacuum_spec.rb
index 1cc4ff6891c..b88d26100c9 100644
--- a/spec/lib/gitlab/database/migration_helpers/wraparound_autovacuum_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers/wraparound_autovacuum_spec.rb
@@ -14,20 +14,30 @@ RSpec.describe Gitlab::Database::MigrationHelpers::WraparoundAutovacuum, feature
describe '#can_execute_on?' do
using RSpec::Parameterized::TableSyntax
- where(:dot_com, :dev_or_test, :wraparound_prevention, :expectation) do
- true | true | true | false
- true | false | true | false
- false | true | true | false
- false | false | true | false
- true | true | false | true
- true | false | false | true
- false | true | false | true
- false | false | false | false
+ where(:dot_com, :jh, :dev_or_test, :wraparound_prevention, :expectation) do
+ true | true | true | true | false
+ true | true | false | true | false
+ false | true | true | true | false
+ false | true | false | true | false
+ true | true | true | false | true
+ true | true | false | false | false
+ false | true | true | false | true
+ false | true | false | false | false
+
+ true | false | true | true | false
+ true | false | false | true | false
+ false | false | true | true | false
+ false | false | false | true | false
+ true | false | true | false | true
+ true | false | false | false | true
+ false | false | true | false | true
+ false | false | false | false | false
end
with_them do
- it 'returns true for GitLab.com, dev, or test' do
+ it 'returns as expected for GitLab.com, dev, or test' do
allow(Gitlab).to receive(:com?).and_return(dot_com)
+ allow(Gitlab).to receive(:jh?).and_return(jh)
allow(Gitlab).to receive(:dev_or_test_env?).and_return(dev_or_test)
allow(migration).to receive(:wraparound_prevention_on_tables?).with([:table]).and_return(wraparound_prevention)
diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb
index dd51cca688c..8bf05f56b3f 100644
--- a/spec/lib/gitlab/database/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -118,7 +118,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers, feature_category: :database d
it 'cannot add unacceptable column names' do
expect do
model.add_timestamps_with_timezone(:foo, columns: [:bar])
- end.to raise_error %r/Illegal timestamp column name/
+ end.to raise_error %r{Illegal timestamp column name}
end
end
@@ -1753,8 +1753,8 @@ RSpec.describe Gitlab::Database::MigrationHelpers, feature_category: :database d
describe '#indexes_for' do
it 'returns the indexes for a column' do
- idx1 = double(:idx, columns: %w(project_id))
- idx2 = double(:idx, columns: %w(user_id))
+ idx1 = double(:idx, columns: %w[project_id])
+ idx2 = double(:idx, columns: %w[user_id])
allow(model).to receive(:indexes).with('table').and_return([idx1, idx2])
@@ -1777,7 +1777,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers, feature_category: :database d
context 'when index name is too long' do
it 'does not fail' do
index = double(:index,
- columns: %w(uuid),
+ columns: %w[uuid],
name: 'index_vuln_findings_on_uuid_including_vuln_id_1',
using: nil,
where: nil,
@@ -1791,7 +1791,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers, feature_category: :database d
expect(model).to receive(:add_concurrent_index)
.with(:vulnerability_occurrences,
- %w(tmp_undo_cleanup_column_8cbf300838),
+ %w[tmp_undo_cleanup_column_8cbf300838],
{
unique: true,
name: 'idx_copy_191a1af1a0',
@@ -1806,7 +1806,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers, feature_category: :database d
context 'using a regular index using a single column' do
it 'copies the index' do
index = double(:index,
- columns: %w(project_id),
+ columns: %w[project_id],
name: 'index_on_issues_project_id',
using: nil,
where: nil,
@@ -1820,7 +1820,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers, feature_category: :database d
expect(model).to receive(:add_concurrent_index)
.with(:issues,
- %w(gl_project_id),
+ %w[gl_project_id],
{
unique: false,
name: 'index_on_issues_gl_project_id',
@@ -1835,7 +1835,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers, feature_category: :database d
context 'using a regular index with multiple columns' do
it 'copies the index' do
index = double(:index,
- columns: %w(project_id foobar),
+ columns: %w[project_id foobar],
name: 'index_on_issues_project_id_foobar',
using: nil,
where: nil,
@@ -1849,7 +1849,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers, feature_category: :database d
expect(model).to receive(:add_concurrent_index)
.with(:issues,
- %w(gl_project_id foobar),
+ %w[gl_project_id foobar],
{
unique: false,
name: 'index_on_issues_gl_project_id_foobar',
@@ -1864,7 +1864,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers, feature_category: :database d
context 'using an index with a WHERE clause' do
it 'copies the index' do
index = double(:index,
- columns: %w(project_id),
+ columns: %w[project_id],
name: 'index_on_issues_project_id',
using: nil,
where: 'foo',
@@ -1878,7 +1878,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers, feature_category: :database d
expect(model).to receive(:add_concurrent_index)
.with(:issues,
- %w(gl_project_id),
+ %w[gl_project_id],
{
unique: false,
name: 'index_on_issues_gl_project_id',
@@ -1894,7 +1894,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers, feature_category: :database d
context 'using an index with a USING clause' do
it 'copies the index' do
index = double(:index,
- columns: %w(project_id),
+ columns: %w[project_id],
name: 'index_on_issues_project_id',
where: nil,
using: 'foo',
@@ -1908,7 +1908,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers, feature_category: :database d
expect(model).to receive(:add_concurrent_index)
.with(:issues,
- %w(gl_project_id),
+ %w[gl_project_id],
{
unique: false,
name: 'index_on_issues_gl_project_id',
@@ -1924,7 +1924,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers, feature_category: :database d
context 'using an index with custom operator classes' do
it 'copies the index' do
index = double(:index,
- columns: %w(project_id),
+ columns: %w[project_id],
name: 'index_on_issues_project_id',
using: nil,
where: nil,
@@ -1938,7 +1938,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers, feature_category: :database d
expect(model).to receive(:add_concurrent_index)
.with(:issues,
- %w(gl_project_id),
+ %w[gl_project_id],
{
unique: false,
name: 'index_on_issues_gl_project_id',
@@ -1955,7 +1955,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers, feature_category: :database d
it 'copies the index' do
index = double(:index,
{
- columns: %w(project_id foobar),
+ columns: %w[project_id foobar],
name: 'index_on_issues_project_id_foobar',
using: :gin,
where: nil,
@@ -1970,7 +1970,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers, feature_category: :database d
expect(model).to receive(:add_concurrent_index)
.with(:issues,
- %w(gl_project_id foobar),
+ %w[gl_project_id foobar],
{
unique: false,
name: 'index_on_issues_gl_project_id_foobar',
@@ -1988,7 +1988,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers, feature_category: :database d
it 'copies the index' do
index = double(:index,
{
- columns: %w(project_id foobar),
+ columns: %w[project_id foobar],
name: 'index_on_issues_project_id_foobar',
using: :gin,
where: nil,
@@ -2003,7 +2003,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers, feature_category: :database d
expect(model).to receive(:add_concurrent_index)
.with(:issues,
- %w(gl_project_id foobar),
+ %w[gl_project_id foobar],
{
unique: false,
name: 'index_on_issues_gl_project_id_foobar',
@@ -2020,7 +2020,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers, feature_category: :database d
describe 'using an index of which the name does not contain the source column' do
it 'raises RuntimeError' do
index = double(:index,
- columns: %w(project_id),
+ columns: %w[project_id],
name: 'index_foobar_index',
using: nil,
where: nil,
diff --git a/spec/lib/gitlab/database/migrations/batched_background_migration_helpers_spec.rb b/spec/lib/gitlab/database/migrations/batched_background_migration_helpers_spec.rb
index f1271f2434c..a81ccf9583a 100644
--- a/spec/lib/gitlab/database/migrations/batched_background_migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migrations/batched_background_migration_helpers_spec.rb
@@ -443,10 +443,10 @@ RSpec.describe Gitlab::Database::Migrations::BatchedBackgroundMigrationHelpers,
describe '#ensure_batched_background_migration_is_finished' do
let(:job_class_name) { 'CopyColumnUsingBackgroundMigrationJob' }
- let(:table_name) { 'events' }
+ let(:table_name) { '_test_table' }
let(:column_name) { :id }
let(:job_arguments) { [["id"], ["id_convert_to_bigint"], nil] }
- let(:gitlab_schema) { Gitlab::Database::GitlabSchema.table_schema!(table_name) }
+ let(:gitlab_schema) { :gitlab_main }
let(:configuration) do
{
@@ -484,7 +484,7 @@ RSpec.describe Gitlab::Database::Migrations::BatchedBackgroundMigrationHelpers,
"\n\n" \
"Finalize it manually by running the following command in a `bash` or `sh` shell:" \
"\n\n" \
- "\tsudo gitlab-rake gitlab:background_migrations:finalize[CopyColumnUsingBackgroundMigrationJob,events,id,'[[\"id\"]\\,[\"id_convert_to_bigint\"]\\,null]']" \
+ "\tsudo gitlab-rake gitlab:background_migrations:finalize[CopyColumnUsingBackgroundMigrationJob,_test_table,id,'[[\"id\"]\\,[\"id_convert_to_bigint\"]\\,null]']" \
"\n\n" \
"For more information, check the documentation" \
"\n\n" \
diff --git a/spec/lib/gitlab/database/migrations/constraints_helpers_spec.rb b/spec/lib/gitlab/database/migrations/constraints_helpers_spec.rb
index 476b5f3a784..4d7c51a3fbf 100644
--- a/spec/lib/gitlab/database/migrations/constraints_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migrations/constraints_helpers_spec.rb
@@ -13,9 +13,11 @@ RSpec.describe Gitlab::Database::Migrations::ConstraintsHelpers do
describe '#check_constraint_name' do
it 'returns a valid constraint name' do
- name = model.check_constraint_name(:this_is_a_very_long_table_name,
- :with_a_very_long_column_name,
- :with_a_very_long_type)
+ name = model.check_constraint_name(
+ :this_is_a_very_long_table_name,
+ :with_a_very_long_column_name,
+ :with_a_very_long_type
+ )
expect(name).to be_an_instance_of(String)
expect(name).to start_with('check_')
@@ -404,9 +406,7 @@ RSpec.describe Gitlab::Database::Migrations::ConstraintsHelpers do
describe '#add_text_limit' do
context 'when it is called with the default options' do
it 'calls add_check_constraint with an infered constraint name and validate: true' do
- constraint_name = model.check_constraint_name(:test_table,
- :name,
- 'max_length')
+ constraint_name = model.check_constraint_name(:test_table, :name, 'max_length')
check = "char_length(name) <= 255"
expect(model).to receive(:check_constraint_name).and_call_original
@@ -440,9 +440,7 @@ RSpec.describe Gitlab::Database::Migrations::ConstraintsHelpers do
describe '#validate_text_limit' do
context 'when constraint_name is not provided' do
it 'calls validate_check_constraint with an infered constraint name' do
- constraint_name = model.check_constraint_name(:test_table,
- :name,
- 'max_length')
+ constraint_name = model.check_constraint_name(:test_table, :name, 'max_length')
expect(model).to receive(:check_constraint_name).and_call_original
expect(model).to receive(:validate_check_constraint)
@@ -468,9 +466,7 @@ RSpec.describe Gitlab::Database::Migrations::ConstraintsHelpers do
describe '#remove_text_limit' do
context 'when constraint_name is not provided' do
it 'calls remove_check_constraint with an infered constraint name' do
- constraint_name = model.check_constraint_name(:test_table,
- :name,
- 'max_length')
+ constraint_name = model.check_constraint_name(:test_table, :name, 'max_length')
expect(model).to receive(:check_constraint_name).and_call_original
expect(model).to receive(:remove_check_constraint)
@@ -496,9 +492,7 @@ RSpec.describe Gitlab::Database::Migrations::ConstraintsHelpers do
describe '#check_text_limit_exists?' do
context 'when constraint_name is not provided' do
it 'calls check_constraint_exists? with an infered constraint name' do
- constraint_name = model.check_constraint_name(:test_table,
- :name,
- 'max_length')
+ constraint_name = model.check_constraint_name(:test_table, :name, 'max_length')
expect(model).to receive(:check_constraint_name).and_call_original
expect(model).to receive(:check_constraint_exists?)
@@ -524,9 +518,7 @@ RSpec.describe Gitlab::Database::Migrations::ConstraintsHelpers do
describe '#add_not_null_constraint' do
context 'when it is called with the default options' do
it 'calls add_check_constraint with an infered constraint name and validate: true' do
- constraint_name = model.check_constraint_name(:test_table,
- :name,
- 'not_null')
+ constraint_name = model.check_constraint_name(:test_table, :name, 'not_null')
check = "name IS NOT NULL"
expect(model).to receive(:column_is_nullable?).and_return(true)
@@ -571,9 +563,7 @@ RSpec.describe Gitlab::Database::Migrations::ConstraintsHelpers do
describe '#validate_not_null_constraint' do
context 'when constraint_name is not provided' do
it 'calls validate_check_constraint with an infered constraint name' do
- constraint_name = model.check_constraint_name(:test_table,
- :name,
- 'not_null')
+ constraint_name = model.check_constraint_name(:test_table, :name, 'not_null')
expect(model).to receive(:check_constraint_name).and_call_original
expect(model).to receive(:validate_check_constraint)
@@ -599,9 +589,7 @@ RSpec.describe Gitlab::Database::Migrations::ConstraintsHelpers do
describe '#remove_not_null_constraint' do
context 'when constraint_name is not provided' do
it 'calls remove_check_constraint with an infered constraint name' do
- constraint_name = model.check_constraint_name(:test_table,
- :name,
- 'not_null')
+ constraint_name = model.check_constraint_name(:test_table, :name, 'not_null')
expect(model).to receive(:check_constraint_name).and_call_original
expect(model).to receive(:remove_check_constraint)
@@ -627,9 +615,7 @@ RSpec.describe Gitlab::Database::Migrations::ConstraintsHelpers do
describe '#check_not_null_constraint_exists?' do
context 'when constraint_name is not provided' do
it 'calls check_constraint_exists? with an infered constraint name' do
- constraint_name = model.check_constraint_name(:test_table,
- :name,
- 'not_null')
+ constraint_name = model.check_constraint_name(:test_table, :name, 'not_null')
expect(model).to receive(:check_constraint_name).and_call_original
expect(model).to receive(:check_constraint_exists?)
diff --git a/spec/lib/gitlab/database/migrations/instrumentation_spec.rb b/spec/lib/gitlab/database/migrations/instrumentation_spec.rb
index a12e0909dc2..81368dde94d 100644
--- a/spec/lib/gitlab/database/migrations/instrumentation_spec.rb
+++ b/spec/lib/gitlab/database/migrations/instrumentation_spec.rb
@@ -75,8 +75,12 @@ RSpec.describe Gitlab::Database::Migrations::Instrumentation do
context 'on successful execution' do
subject do
- instrumentation.observe(version: migration_version, name: migration_name,
- connection: connection, meta: migration_meta) {}
+ instrumentation.observe(
+ version: migration_version,
+ name: migration_name,
+ connection: connection,
+ meta: migration_meta
+ ) {}
end
it 'records a valid observation', :aggregate_failures do
@@ -98,8 +102,12 @@ RSpec.describe Gitlab::Database::Migrations::Instrumentation do
with_them do
subject(:observe) do
- instrumentation.observe(version: migration_version, name: migration_name,
- connection: connection, meta: migration_meta) { raise exception, error_message }
+ instrumentation.observe(
+ version: migration_version,
+ name: migration_name,
+ connection: connection,
+ meta: migration_meta
+ ) { raise exception, error_message }
end
context 'retrieving observations' do
diff --git a/spec/lib/gitlab/database/migrations/milestone_mixin_spec.rb b/spec/lib/gitlab/database/migrations/milestone_mixin_spec.rb
index e375af494a2..1ed5c846550 100644
--- a/spec/lib/gitlab/database/migrations/milestone_mixin_spec.rb
+++ b/spec/lib/gitlab/database/migrations/milestone_mixin_spec.rb
@@ -12,14 +12,11 @@ RSpec.describe Gitlab::Database::Migrations::MilestoneMixin, feature_category: :
end
let(:migration_mixin) do
- Class.new(Gitlab::Database::Migration[2.1]) do
- include Gitlab::Database::Migrations::MilestoneMixin
- end
+ Class.new(Gitlab::Database::Migration[2.2])
end
let(:migration_mixin_version) do
- Class.new(Gitlab::Database::Migration[2.1]) do
- include Gitlab::Database::Migrations::MilestoneMixin
+ Class.new(Gitlab::Database::Migration[2.2]) do
milestone '16.4'
end
end
@@ -44,5 +41,11 @@ RSpec.describe Gitlab::Database::Migrations::MilestoneMixin, feature_category: :
expect { migration_mixin_version.new(4, 4, :regular) }.not_to raise_error
end
end
+
+ context 'when initialize arguments are not provided' do
+ it "does not raise an error" do
+ expect { migration_mixin_version.new }.not_to raise_error
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/database/migrations/reestablished_connection_stack_spec.rb b/spec/lib/gitlab/database/migrations/reestablished_connection_stack_spec.rb
index c6327de98d1..6e943307ae6 100644
--- a/spec/lib/gitlab/database/migrations/reestablished_connection_stack_spec.rb
+++ b/spec/lib/gitlab/database/migrations/reestablished_connection_stack_spec.rb
@@ -31,7 +31,7 @@ RSpec.describe Gitlab::Database::Migrations::ReestablishedConnectionStack do
# establish connection
ApplicationRecord.connection.select_one("SELECT 1 FROM projects LIMIT 1")
- Ci::ApplicationRecord.connection.select_one("SELECT 1 FROM ci_builds LIMIT 1")
+ Ci::ApplicationRecord.connection.select_one("SELECT 1 FROM p_ci_builds LIMIT 1")
end
expect(new_handler).not_to eq(original_handler), "is reconnected"
diff --git a/spec/lib/gitlab/database/migrations/test_batched_background_runner_spec.rb b/spec/lib/gitlab/database/migrations/test_batched_background_runner_spec.rb
index 31a194ae883..660c4347ffa 100644
--- a/spec/lib/gitlab/database/migrations/test_batched_background_runner_spec.rb
+++ b/spec/lib/gitlab/database/migrations/test_batched_background_runner_spec.rb
@@ -80,8 +80,11 @@ RSpec.describe Gitlab::Database::Migrations::TestBatchedBackgroundRunner, :freez
end
subject(:sample_migration) do
- described_class.new(result_dir: result_dir, connection: connection,
- from_id: from_id).run_jobs(for_duration: 1.minute)
+ described_class.new(
+ result_dir: result_dir,
+ connection: connection,
+ from_id: from_id
+ ).run_jobs(for_duration: 1.minute)
end
it 'runs sampled jobs from the batched background migration' do
@@ -125,12 +128,19 @@ RSpec.describe Gitlab::Database::Migrations::TestBatchedBackgroundRunner, :freez
calls << args
end
- queue_migration(migration_name, table_name, :id,
- job_interval: 5.minutes,
- batch_size: 100)
+ queue_migration(
+ migration_name,
+ table_name,
+ :id,
+ job_interval: 5.minutes,
+ batch_size: 100
+ )
- described_class.new(result_dir: result_dir, connection: connection,
- from_id: from_id).run_jobs(for_duration: 3.minutes)
+ described_class.new(
+ result_dir: result_dir,
+ connection: connection,
+ from_id: from_id
+ ).run_jobs(for_duration: 3.minutes)
expect(calls).not_to be_empty
end
@@ -142,13 +152,19 @@ RSpec.describe Gitlab::Database::Migrations::TestBatchedBackgroundRunner, :freez
calls << args
end
- queue_migration(migration_name, table_name, :id,
- job_interval: 5.minutes,
- batch_size: num_rows_in_table * 2,
- sub_batch_size: num_rows_in_table * 2)
+ queue_migration(
+ migration_name,
+ table_name, :id,
+ job_interval: 5.minutes,
+ batch_size: num_rows_in_table * 2,
+ sub_batch_size: num_rows_in_table * 2
+ )
- described_class.new(result_dir: result_dir, connection: connection,
- from_id: from_id).run_jobs(for_duration: 3.minutes)
+ described_class.new(
+ result_dir: result_dir,
+ connection: connection,
+ from_id: from_id
+ ).run_jobs(for_duration: 3.minutes)
expect(calls.size).to eq(1)
end
@@ -161,13 +177,20 @@ RSpec.describe Gitlab::Database::Migrations::TestBatchedBackgroundRunner, :freez
calls << args
end
- queue_migration(migration_name, table_name, :id,
- job_interval: 5.minutes,
- batch_size: num_rows_in_table * 2,
- sub_batch_size: num_rows_in_table * 2)
-
- described_class.new(result_dir: result_dir, connection: connection,
- from_id: from_id).run_jobs(for_duration: 3.minutes)
+ queue_migration(
+ migration_name,
+ table_name,
+ :id,
+ job_interval: 5.minutes,
+ batch_size: num_rows_in_table * 2,
+ sub_batch_size: num_rows_in_table * 2
+ )
+
+ described_class.new(
+ result_dir: result_dir,
+ connection: connection,
+ from_id: from_id
+ ).run_jobs(for_duration: 3.minutes)
expect(calls.count).to eq(0)
end
@@ -181,26 +204,41 @@ RSpec.describe Gitlab::Database::Migrations::TestBatchedBackgroundRunner, :freez
it 'runs all pending jobs based on the last migration id' do
old_migration = define_background_migration(migration_name)
- queue_migration(migration_name, table_name, :id,
- job_interval: 5.minutes,
- batch_size: 100)
+ queue_migration(
+ migration_name,
+ table_name,
+ :id,
+ job_interval: 5.minutes,
+ batch_size: 100
+ )
last_id
new_migration = define_background_migration('NewMigration') { travel 1.second }
- queue_migration('NewMigration', table_name, :id,
- job_interval: 5.minutes,
- batch_size: 10,
- sub_batch_size: 5)
+ queue_migration(
+ 'NewMigration',
+ table_name,
+ :id,
+ job_interval: 5.minutes,
+ batch_size: 10,
+ sub_batch_size: 5
+ )
other_new_migration = define_background_migration('NewMigration2') { travel 2.seconds }
- queue_migration('NewMigration2', table_name, :id,
- job_interval: 5.minutes,
- batch_size: 10,
- sub_batch_size: 5)
+ queue_migration(
+ 'NewMigration2',
+ table_name,
+ :id,
+ job_interval: 5.minutes,
+ batch_size: 10,
+ sub_batch_size: 5
+ )
expect_migration_runs(new_migration => 3, other_new_migration => 2, old_migration => 0) do
- described_class.new(result_dir: result_dir, connection: connection,
- from_id: last_id).run_jobs(for_duration: 5.seconds)
+ described_class.new(
+ result_dir: result_dir,
+ connection: connection,
+ from_id: last_id
+ ).run_jobs(for_duration: 5.seconds)
end
end
end
diff --git a/spec/lib/gitlab/database/no_cross_db_foreign_keys_spec.rb b/spec/lib/gitlab/database/no_cross_db_foreign_keys_spec.rb
index c6cd5e55754..c57b8bb5992 100644
--- a/spec/lib/gitlab/database/no_cross_db_foreign_keys_spec.rb
+++ b/spec/lib/gitlab/database/no_cross_db_foreign_keys_spec.rb
@@ -11,6 +11,7 @@ RSpec.describe 'cross-database foreign keys' do
# should be added as a comment along with the name of the column.
let!(:allowed_cross_database_foreign_keys) do
[
+ 'events.author_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/429803
'gitlab_subscriptions.hosted_plan_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/422012
'group_import_states.user_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/421210
'identities.saml_provider_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/422010
@@ -18,11 +19,18 @@ RSpec.describe 'cross-database foreign keys' do
'issues.closed_by_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/422154
'issues.updated_by_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/422154
'issue_assignees.user_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/422154
+ 'lfs_file_locks.user_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/430838
'merge_requests.assignee_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/422080
'merge_requests.updated_by_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/422080
'merge_requests.merge_user_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/422080
'merge_requests.author_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/422080
+ 'namespace_commit_emails.email_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/429804
+ 'namespace_commit_emails.user_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/429804
+ 'path_locks.user_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/429380
'project_authorizations.user_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/422044
+ 'protected_branch_push_access_levels.user_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/431054
+ 'protected_branch_merge_access_levels.user_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/431055
+ 'security_orchestration_policy_configurations.bot_user_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/429438
'user_group_callouts.user_id' # https://gitlab.com/gitlab-org/gitlab/-/issues/421287
]
end
diff --git a/spec/lib/gitlab/database/no_new_tables_with_gitlab_main_schema_spec.rb b/spec/lib/gitlab/database/no_new_tables_with_gitlab_main_schema_spec.rb
new file mode 100644
index 00000000000..338475fa9c4
--- /dev/null
+++ b/spec/lib/gitlab/database/no_new_tables_with_gitlab_main_schema_spec.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'new tables with gitlab_main schema', feature_category: :cell do
+ # During the development of Cells, we will be moving tables from the `gitlab_main` schema
+ # to either the `gitlab_main_clusterwide` or `gitlab_main_cell` schema.
+ # As part of this process, starting from milestone 16.7, it will be a mandatory requirement that
+ # all newly created tables are associated with one of these two schemas.
+ # Any attempt to set the `gitlab_main` schema for a new table will result in a failure of this spec.
+
+ # Specific tables can be exempted from this requirement, and such tables must be added to the `exempted_tables` list.
+ let!(:exempted_tables) do
+ []
+ end
+
+ let!(:starting_from_milestone) { 16.7 }
+
+ it 'only allows exempted tables to have `gitlab_main` as its schema, after milestone 16.7', :aggregate_failures do
+ tables_having_gitlab_main_schema(starting_from_milestone: starting_from_milestone).each do |table_name|
+ expect(exempted_tables).to include(table_name), error_message(table_name)
+ end
+ end
+
+ it 'only allows tables having `gitlab_main` as its schema in `exempted_tables`', :aggregate_failures do
+ tables_having_gitlab_main_schema = gitlab_main_schema_tables.map(&:table_name)
+
+ exempted_tables.each do |exempted_table|
+ expect(tables_having_gitlab_main_schema).to include(exempted_table),
+ "`#{exempted_table}` does not have `gitlab_main` as its schema.
+ Please remove this table from the `exempted_tables` list."
+ end
+ end
+
+ private
+
+ def error_message(table_name)
+ <<~HEREDOC
+ The table `#{table_name}` has been added with `gitlab_main` schema.
+ Starting from GitLab #{starting_from_milestone}, we expect new tables to use either the `gitlab_main_cell` or the
+ `gitlab_main_clusterwide` schema.
+
+ To choose an appropriate schema for this table from among `gitlab_main_cell` and `gitlab_main_clusterwide`, please refer
+ to our guidelines at https://docs.gitlab.com/ee/development/database/multiple_databases.html#guidelines-on-choosing-between-gitlab_main_cell-and-gitlab_main_clusterwide-schema, or consult with the Tenant Scale group.
+
+ Please see issue https://gitlab.com/gitlab-org/gitlab/-/issues/424990 to understand why this change is being enforced.
+ HEREDOC
+ end
+
+ def tables_having_gitlab_main_schema(starting_from_milestone:)
+ selected_data = gitlab_main_schema_tables.select do |database_dictionary|
+ database_dictionary.milestone.to_f >= starting_from_milestone
+ end
+
+ selected_data.map(&:table_name)
+ end
+
+ def gitlab_main_schema_tables
+ ::Gitlab::Database::GitlabSchema.build_dictionary('').select do |database_dictionary|
+ database_dictionary.schema?('gitlab_main')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/partitioning/detached_partition_dropper_spec.rb b/spec/lib/gitlab/database/partitioning/detached_partition_dropper_spec.rb
index 04940028aee..eb78d836be0 100644
--- a/spec/lib/gitlab/database/partitioning/detached_partition_dropper_spec.rb
+++ b/spec/lib/gitlab/database/partitioning/detached_partition_dropper_spec.rb
@@ -57,17 +57,19 @@ RSpec.describe Gitlab::Database::Partitioning::DetachedPartitionDropper do
SQL
end
- Postgresql::DetachedPartition.create!(table_name: name,
- drop_after: drop_after)
+ Postgresql::DetachedPartition.create!(table_name: name, drop_after: drop_after)
end
describe '#perform' do
context 'when the partition should not be dropped yet' do
it 'does not drop the partition' do
- create_partition(name: :_test_partition,
- from: 2.months.ago, to: 1.month.ago,
- attached: false,
- drop_after: 1.day.from_now)
+ create_partition(
+ name: :_test_partition,
+ from: 2.months.ago,
+ to: 1.month.ago,
+ attached: false,
+ drop_after: 1.day.from_now
+ )
dropper.perform
@@ -77,11 +79,13 @@ RSpec.describe Gitlab::Database::Partitioning::DetachedPartitionDropper do
context 'with a partition to drop' do
before do
- create_partition(name: :_test_partition,
- from: 2.months.ago,
- to: 1.month.ago.beginning_of_month,
- attached: false,
- drop_after: 1.second.ago)
+ create_partition(
+ name: :_test_partition,
+ from: 2.months.ago,
+ to: 1.month.ago.beginning_of_month,
+ attached: false,
+ drop_after: 1.second.ago
+ )
end
it 'drops the partition' do
@@ -159,11 +163,13 @@ RSpec.describe Gitlab::Database::Partitioning::DetachedPartitionDropper do
context 'when the partition to drop is still attached to its table' do
before do
- create_partition(name: :_test_partition,
- from: 2.months.ago,
- to: 1.month.ago.beginning_of_month,
- attached: true,
- drop_after: 1.second.ago)
+ create_partition(
+ name: :_test_partition,
+ from: 2.months.ago,
+ to: 1.month.ago.beginning_of_month,
+ attached: true,
+ drop_after: 1.second.ago
+ )
end
it 'does not drop the partition, but does remove the DetachedPartition entry' do
@@ -192,17 +198,21 @@ RSpec.describe Gitlab::Database::Partitioning::DetachedPartitionDropper do
context 'with multiple partitions to drop' do
before do
- create_partition(name: :_test_partition_1,
- from: 3.months.ago,
- to: 2.months.ago,
- attached: false,
- drop_after: 1.second.ago)
-
- create_partition(name: :_test_partition_2,
- from: 2.months.ago,
- to: 1.month.ago,
- attached: false,
- drop_after: 1.second.ago)
+ create_partition(
+ name: :_test_partition_1,
+ from: 3.months.ago,
+ to: 2.months.ago,
+ attached: false,
+ drop_after: 1.second.ago
+ )
+
+ create_partition(
+ name: :_test_partition_2,
+ from: 2.months.ago,
+ to: 1.month.ago,
+ attached: false,
+ drop_after: 1.second.ago
+ )
end
it 'drops both partitions' do
diff --git a/spec/lib/gitlab/database/partitioning/monthly_strategy_spec.rb b/spec/lib/gitlab/database/partitioning/monthly_strategy_spec.rb
index 3afa338fdf7..8b18e5b6d08 100644
--- a/spec/lib/gitlab/database/partitioning/monthly_strategy_spec.rb
+++ b/spec/lib/gitlab/database/partitioning/monthly_strategy_spec.rb
@@ -235,8 +235,12 @@ RSpec.describe Gitlab::Database::Partitioning::MonthlyStrategy, feature_category
subject { described_class.new(model, partitioning_key, retain_for: 3.months).extra_partitions }
it 'prunes the unbounded partition ending 2020-05-01' do
- min_value_to_may = Gitlab::Database::Partitioning::TimePartition.new(model.table_name, nil, '2020-05-01',
- partition_name: '_test_partitioned_test_000000')
+ min_value_to_may = Gitlab::Database::Partitioning::TimePartition.new(
+ model.table_name,
+ nil,
+ '2020-05-01',
+ partition_name: '_test_partitioned_test_000000'
+ )
expect(subject).to contain_exactly(min_value_to_may)
end
@@ -247,8 +251,18 @@ RSpec.describe Gitlab::Database::Partitioning::MonthlyStrategy, feature_category
it 'prunes the unbounded partition and the partition for May-June' do
expect(subject).to contain_exactly(
- Gitlab::Database::Partitioning::TimePartition.new(model.table_name, nil, '2020-05-01', partition_name: '_test_partitioned_test_000000'),
- Gitlab::Database::Partitioning::TimePartition.new(model.table_name, '2020-05-01', '2020-06-01', partition_name: '_test_partitioned_test_202005')
+ Gitlab::Database::Partitioning::TimePartition.new(
+ model.table_name,
+ nil,
+ '2020-05-01',
+ partition_name: '_test_partitioned_test_000000'
+ ),
+ Gitlab::Database::Partitioning::TimePartition.new(
+ model.table_name,
+ '2020-05-01',
+ '2020-06-01',
+ partition_name: '_test_partitioned_test_202005'
+ )
)
end
@@ -257,8 +271,18 @@ RSpec.describe Gitlab::Database::Partitioning::MonthlyStrategy, feature_category
it 'prunes empty partitions' do
expect(subject).to contain_exactly(
- Gitlab::Database::Partitioning::TimePartition.new(model.table_name, nil, '2020-05-01', partition_name: '_test_partitioned_test_000000'),
- Gitlab::Database::Partitioning::TimePartition.new(model.table_name, '2020-05-01', '2020-06-01', partition_name: '_test_partitioned_test_202005')
+ Gitlab::Database::Partitioning::TimePartition.new(
+ model.table_name,
+ nil,
+ '2020-05-01',
+ partition_name: '_test_partitioned_test_000000'
+ ),
+ Gitlab::Database::Partitioning::TimePartition.new(
+ model.table_name,
+ '2020-05-01',
+ '2020-06-01',
+ partition_name: '_test_partitioned_test_202005'
+ )
)
end
diff --git a/spec/lib/gitlab/database/partitioning/partition_manager_spec.rb b/spec/lib/gitlab/database/partitioning/partition_manager_spec.rb
index 80ffa708d8a..336cd2d912d 100644
--- a/spec/lib/gitlab/database/partitioning/partition_manager_spec.rb
+++ b/spec/lib/gitlab/database/partitioning/partition_manager_spec.rb
@@ -6,6 +6,7 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager, feature_categor
include ActiveSupport::Testing::TimeHelpers
include Database::PartitioningHelpers
include ExclusiveLeaseHelpers
+ using RSpec::Parameterized::TableSyntax
let(:partitioned_table_name) { :_test_gitlab_main_my_model_example_table }
@@ -107,14 +108,88 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager, feature_categor
end
end
- before do
- my_model.table_name = partitioned_table_name
+ context 'when single database is configured' do
+ before do
+ skip_if_database_exists(:ci)
- create_partitioned_table(connection, partitioned_table_name)
+ my_model.table_name = partitioned_table_name
+
+ create_partitioned_table(connection, partitioned_table_name)
+ end
+
+ it 'creates partitions' do
+ expect { sync_partitions }.to change { find_partitions(my_model.table_name, schema: Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA).size }.from(0)
+ end
end
- it 'creates partitions' do
- expect { sync_partitions }.to change { find_partitions(my_model.table_name, schema: Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA).size }.from(0)
+ context 'when multiple databases are configured' do
+ before do
+ skip_if_shared_database(:ci)
+
+ my_model.table_name = partitioned_table_name
+
+ create_partitioned_table(connection, partitioned_table_name)
+
+ stub_feature_flags(automatic_lock_writes_on_partition_tables: ff_enabled)
+
+ sync_partitions
+ end
+
+ where(:gitlab_schema, :database, :expectation) do
+ :gitlab_main | :main | false
+ :gitlab_main | :ci | true
+ :gitlab_ci | :main | true
+ :gitlab_ci | :ci | false
+ end
+ with_them do
+ subject(:sync_partitions) { described_class.new(my_model, connection: connection).sync_partitions }
+
+ let(:partitioned_table_name) { "_test_gitlab_#{database}_my_model_example_#{gitlab_schema}" }
+ let(:base_model) { Gitlab::Database.schemas_to_base_models[gitlab_schema].first }
+ let(:connection) { Gitlab::Database.database_base_models[database.to_s].connection }
+
+ let(:my_model) do
+ Class.new(base_model) do
+ include PartitionedTable
+
+ partitioned_by :created_at, strategy: :monthly
+ end
+ end
+
+ let(:partitions) do
+ Gitlab::Database::PostgresPartition.using_connection(connection) { Gitlab::Database::PostgresPartition.for_parent_table(partitioned_table_name).to_a }
+ end
+
+ let(:partitions_locked_for_writes?) do
+ partitions.map do |partition|
+ Gitlab::Database::LockWritesManager.new(
+ table_name: "#{partition.schema}.#{partition.name}",
+ connection: connection,
+ database_name: gitlab_schema
+ ).table_locked_for_writes?
+ end.all?(true)
+ end
+
+ context 'when feature flag is enabled' do
+ let(:ff_enabled) { true }
+
+ it "matches expectation" do
+ sync_partitions
+
+ expect(partitions_locked_for_writes?).to eq(expectation)
+ end
+ end
+
+ context 'when feature flag is disabled' do
+ let(:ff_enabled) { false }
+
+ it "will not lock created partition" do
+ sync_partitions
+
+ expect(partitions_locked_for_writes?).to eq(false)
+ end
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/database/partitioning/sliding_list_strategy_spec.rb b/spec/lib/gitlab/database/partitioning/sliding_list_strategy_spec.rb
index ac4d345271e..9ca0a1b6e57 100644
--- a/spec/lib/gitlab/database/partitioning/sliding_list_strategy_spec.rb
+++ b/spec/lib/gitlab/database/partitioning/sliding_list_strategy_spec.rb
@@ -15,9 +15,12 @@ RSpec.describe Gitlab::Database::Partitioning::SlidingListStrategy, feature_cate
let(:detach_partition_if) { double('detach_partition_if') }
subject(:strategy) do
- described_class.new(model, :partition,
- next_partition_if: next_partition_if,
- detach_partition_if: detach_partition_if)
+ described_class.new(
+ model,
+ :partition,
+ next_partition_if: next_partition_if,
+ detach_partition_if: detach_partition_if
+ )
end
before do
@@ -213,9 +216,9 @@ RSpec.describe Gitlab::Database::Partitioning::SlidingListStrategy, feature_cate
include PartitionedTable
partitioned_by :partition,
- strategy: :sliding_list,
- next_partition_if: proc { false },
- detach_partition_if: proc { false }
+ strategy: :sliding_list,
+ next_partition_if: proc { false },
+ detach_partition_if: proc { false }
end
end.to raise_error(/ignored_columns/)
end
@@ -228,9 +231,9 @@ RSpec.describe Gitlab::Database::Partitioning::SlidingListStrategy, feature_cate
self.ignored_columns = [:partition]
partitioned_by :partition,
- strategy: :sliding_list,
- next_partition_if: proc { false },
- detach_partition_if: proc { false }
+ strategy: :sliding_list,
+ next_partition_if: proc { false },
+ detach_partition_if: proc { false }
end
end.not_to raise_error
end
@@ -255,9 +258,9 @@ RSpec.describe Gitlab::Database::Partitioning::SlidingListStrategy, feature_cate
detach_partition?(...)
end
partitioned_by :partition,
- strategy: :sliding_list,
- next_partition_if: method(:next_partition_if_wrapper),
- detach_partition_if: method(:detach_partition_if_wrapper)
+ strategy: :sliding_list,
+ next_partition_if: method(:next_partition_if_wrapper),
+ detach_partition_if: method(:detach_partition_if_wrapper)
def self.next_partition?(current_partition); end
diff --git a/spec/lib/gitlab/database/partitioning_migration_helpers/index_helpers_spec.rb b/spec/lib/gitlab/database/partitioning_migration_helpers/index_helpers_spec.rb
index a81c8a5a49c..aa644885306 100644
--- a/spec/lib/gitlab/database/partitioning_migration_helpers/index_helpers_spec.rb
+++ b/spec/lib/gitlab/database/partitioning_migration_helpers/index_helpers_spec.rb
@@ -99,8 +99,11 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::IndexHelpers do
expect(migration).to receive(:add_index)
.with(table_name, column_name, { name: index_name, where: 'x > 0', unique: true })
- migration.add_concurrent_partitioned_index(table_name, column_name,
- { name: index_name, where: 'x > 0', unique: true })
+ migration.add_concurrent_partitioned_index(
+ table_name,
+ column_name,
+ { name: index_name, where: 'x > 0', unique: true }
+ )
end
end
diff --git a/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb b/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb
index 6a947044317..31c669ff330 100644
--- a/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb
+++ b/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb
@@ -2,6 +2,173 @@
require 'spec_helper'
+RSpec.shared_examples "a measurable object" do
+ context 'when the table is not allowed' do
+ let(:source_table) { :_test_this_table_is_not_allowed }
+
+ it 'raises an error' do
+ expect(migration).to receive(:assert_table_is_allowed).with(source_table).and_call_original
+
+ expect do
+ subject
+ end.to raise_error(/#{source_table} is not allowed for use/)
+ end
+ end
+
+ context 'when run inside a transaction block' do
+ it 'raises an error' do
+ expect(migration).to receive(:transaction_open?).and_return(true)
+
+ expect do
+ subject
+ end.to raise_error(/can not be run inside a transaction/)
+ end
+ end
+
+ context 'when the given table does not have a primary key' do
+ it 'raises an error' do
+ migration.execute(<<~SQL)
+ ALTER TABLE #{source_table}
+ DROP CONSTRAINT #{source_table}_pkey
+ SQL
+
+ expect do
+ subject
+ end.to raise_error(/primary key not defined for #{source_table}/)
+ end
+ end
+
+ it 'creates the partitioned table with the same non-key columns' do
+ subject
+
+ copied_columns = filter_columns_by_name(connection.columns(partitioned_table), new_primary_key)
+ original_columns = filter_columns_by_name(connection.columns(source_table), new_primary_key)
+
+ expect(copied_columns).to match_array(original_columns)
+ end
+
+ it 'removes the default from the primary key column' do
+ subject
+
+ pk_column = connection.columns(partitioned_table).find { |c| c.name == old_primary_key }
+
+ expect(pk_column.default_function).to be_nil
+ end
+
+ describe 'constructing the partitioned table' do
+ it 'creates a table partitioned by the proper column' do
+ subject
+
+ expect(connection.table_exists?(partitioned_table)).to be(true)
+ expect(connection.primary_key(partitioned_table)).to eq(new_primary_key)
+
+ expect_table_partitioned_by(partitioned_table, [partition_column_name])
+ end
+
+ it 'requires the migration helper to be run in DDL mode' do
+ expect(Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas).to receive(:require_ddl_mode!)
+
+ subject
+
+ expect(connection.table_exists?(partitioned_table)).to be(true)
+ expect(connection.primary_key(partitioned_table)).to eq(new_primary_key)
+
+ expect_table_partitioned_by(partitioned_table, [partition_column_name])
+ end
+
+ it 'changes the primary key datatype to bigint' do
+ subject
+
+ pk_column = connection.columns(partitioned_table).find { |c| c.name == old_primary_key }
+
+ expect(pk_column.sql_type).to eq('bigint')
+ end
+
+ it 'removes the default from the primary key column' do
+ subject
+
+ pk_column = connection.columns(partitioned_table).find { |c| c.name == old_primary_key }
+
+ expect(pk_column.default_function).to be_nil
+ end
+
+ it 'creates the partitioned table with the same non-key columns' do
+ subject
+
+ copied_columns = filter_columns_by_name(connection.columns(partitioned_table), new_primary_key)
+ original_columns = filter_columns_by_name(connection.columns(source_table), new_primary_key)
+
+ expect(copied_columns).to match_array(original_columns)
+ end
+ end
+
+ describe 'keeping data in sync with the partitioned table' do
+ before do
+ partitioned_model.primary_key = :id
+ partitioned_model.table_name = partitioned_table
+ end
+
+ it 'creates a trigger function on the original table' do
+ expect_function_not_to_exist(function_name)
+ expect_trigger_not_to_exist(source_table, trigger_name)
+
+ subject
+
+ expect_function_to_exist(function_name)
+ expect_valid_function_trigger(source_table, trigger_name, function_name, after: %w[delete insert update])
+ end
+
+ it 'syncs inserts to the partitioned tables' do
+ subject
+
+ expect(partitioned_model.count).to eq(0)
+
+ first_record = source_model.create!(name: 'Bob', age: 20, created_at: timestamp, external_id: 1, updated_at: timestamp)
+ second_record = source_model.create!(name: 'Alice', age: 30, created_at: timestamp, external_id: 2, updated_at: timestamp)
+
+ expect(partitioned_model.count).to eq(2)
+ expect(partitioned_model.find(first_record.id).attributes).to eq(first_record.attributes)
+ expect(partitioned_model.find(second_record.id).attributes).to eq(second_record.attributes)
+ end
+
+ it 'syncs updates to the partitioned tables' do
+ subject
+
+ first_record = source_model.create!(name: 'Bob', age: 20, created_at: timestamp, external_id: 1, updated_at: timestamp)
+ second_record = source_model.create!(name: 'Alice', age: 30, created_at: timestamp, external_id: 2, updated_at: timestamp)
+
+ expect(partitioned_model.count).to eq(2)
+
+ first_copy = partitioned_model.find(first_record.id)
+ second_copy = partitioned_model.find(second_record.id)
+
+ expect(first_copy.attributes).to eq(first_record.attributes)
+ expect(second_copy.attributes).to eq(second_record.attributes)
+
+ first_record.update!(age: 21, updated_at: timestamp + 1.hour, external_id: 3)
+
+ expect(partitioned_model.count).to eq(2)
+ expect(first_copy.reload.attributes).to eq(first_record.attributes)
+ expect(second_copy.reload.attributes).to eq(second_record.attributes)
+ end
+
+ it 'syncs deletes to the partitioned tables' do
+ subject
+
+ first_record = source_model.create!(name: 'Bob', age: 20, created_at: timestamp, external_id: 1, updated_at: timestamp)
+ second_record = source_model.create!(name: 'Alice', age: 30, created_at: timestamp, external_id: 2, updated_at: timestamp)
+
+ expect(partitioned_model.count).to eq(2)
+
+ first_record.destroy!
+
+ expect(partitioned_model.count).to eq(1)
+ expect(partitioned_model.find_by_id(first_record.id)).to be_nil
+ expect(partitioned_model.find(second_record.id).attributes).to eq(second_record.attributes)
+ end
+ end
+end
+
RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHelpers, feature_category: :database do
include Database::PartitioningHelpers
include Database::TriggerHelpers
@@ -18,6 +185,7 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe
let(:partitioned_table) { :_test_migration_partitioned_table }
let(:function_name) { :_test_migration_function_name }
let(:trigger_name) { :_test_migration_trigger_name }
+ let(:partition_column2) { 'external_id' }
let(:partition_column) { 'created_at' }
let(:min_date) { Date.new(2019, 12) }
let(:max_date) { Date.new(2020, 3) }
@@ -29,6 +197,7 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe
migration.create_table source_table do |t|
t.string :name, null: false
t.integer :age, null: false
+ t.integer partition_column2
t.datetime partition_column
t.datetime :updated_at
end
@@ -51,13 +220,15 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe
end
it 'delegates to a method on List::ConvertTable' do
- expect_next_instance_of(Gitlab::Database::Partitioning::List::ConvertTable,
- migration_context: migration,
- table_name: source_table,
- parent_table_name: partitioned_table,
- partitioning_column: partition_column,
- zero_partition_value: min_date,
- **extra_options) do |converter|
+ expect_next_instance_of(
+ Gitlab::Database::Partitioning::List::ConvertTable,
+ migration_context: migration,
+ table_name: source_table,
+ parent_table_name: partitioned_table,
+ partitioning_column: partition_column,
+ zero_partition_value: min_date,
+ **extra_options
+ ) do |converter|
expect(converter).to receive(expected_method)
end
@@ -70,11 +241,13 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe
let(:lock_tables) { [source_table] }
let(:expected_method) { :partition }
let(:migrate) do
- migration.convert_table_to_first_list_partition(table_name: source_table,
- partitioning_column: partition_column,
- parent_table_name: partitioned_table,
- initial_partitioning_value: min_date,
- lock_tables: lock_tables)
+ migration.convert_table_to_first_list_partition(
+ table_name: source_table,
+ partitioning_column: partition_column,
+ parent_table_name: partitioned_table,
+ initial_partitioning_value: min_date,
+ lock_tables: lock_tables
+ )
end
end
end
@@ -83,10 +256,12 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe
it_behaves_like 'delegates to ConvertTable' do
let(:expected_method) { :revert_partitioning }
let(:migrate) do
- migration.revert_converting_table_to_first_list_partition(table_name: source_table,
- partitioning_column: partition_column,
- parent_table_name: partitioned_table,
- initial_partitioning_value: min_date)
+ migration.revert_converting_table_to_first_list_partition(
+ table_name: source_table,
+ partitioning_column: partition_column,
+ parent_table_name: partitioned_table,
+ initial_partitioning_value: min_date
+ )
end
end
end
@@ -95,11 +270,13 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe
it_behaves_like 'delegates to ConvertTable' do
let(:expected_method) { :prepare_for_partitioning }
let(:migrate) do
- migration.prepare_constraint_for_list_partitioning(table_name: source_table,
- partitioning_column: partition_column,
- parent_table_name: partitioned_table,
- initial_partitioning_value: min_date,
- async: false)
+ migration.prepare_constraint_for_list_partitioning(
+ table_name: source_table,
+ partitioning_column: partition_column,
+ parent_table_name: partitioned_table,
+ initial_partitioning_value: min_date,
+ async: false
+ )
end
end
end
@@ -108,41 +285,200 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe
it_behaves_like 'delegates to ConvertTable' do
let(:expected_method) { :revert_preparation_for_partitioning }
let(:migrate) do
- migration.revert_preparing_constraint_for_list_partitioning(table_name: source_table,
- partitioning_column: partition_column,
- parent_table_name: partitioned_table,
- initial_partitioning_value: min_date)
+ migration.revert_preparing_constraint_for_list_partitioning(
+ table_name: source_table,
+ partitioning_column: partition_column,
+ parent_table_name: partitioned_table,
+ initial_partitioning_value: min_date
+ )
end
end
end
end
- describe '#partition_table_by_date' do
- let(:partition_column) { 'created_at' }
+ describe '#partition_table_by_int_range' do
let(:old_primary_key) { 'id' }
- let(:new_primary_key) { [old_primary_key, partition_column] }
+ let(:new_primary_key) { ['id', partition_column2] }
+ let(:partition_column_name) { partition_column2 }
+ let(:partitioned_model) { Class.new(ActiveRecord::Base) }
+ let(:timestamp) { Time.utc(2019, 12, 1, 12).round }
+ let(:partition_size) { 500 }
- context 'when the table is not allowed' do
- let(:source_table) { :_test_this_table_is_not_allowed }
+ subject { migration.partition_table_by_int_range(source_table, partition_column2, partition_size: partition_size, primary_key: ['id', partition_column2]) }
- it 'raises an error' do
- expect(migration).to receive(:assert_table_is_allowed).with(source_table).and_call_original
+ include_examples "a measurable object"
- expect do
- migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date
- end.to raise_error(/#{source_table} is not allowed for use/)
+ context 'simulates the merge_request_diff_commits migration' do
+ let(:table_name) { '_test_merge_request_diff_commits' }
+ let(:partition_column_name) { 'relative_order' }
+ let(:partition_size) { 2 }
+ let(:partitions) do
+ {
+ '1' => %w[1 3],
+ '3' => %w[3 5],
+ '5' => %w[5 7],
+ '7' => %w[7 9],
+ '9' => %w[9 11],
+ '11' => %w[11 13]
+ }
+ end
+
+ let(:buffer_partitions) do
+ {
+ '13' => %w[13 15],
+ '15' => %w[15 17],
+ '17' => %w[17 19],
+ '19' => %w[19 21],
+ '21' => %w[21 23],
+ '23' => %w[23 25]
+ }
+ end
+
+ let(:new_table_defition) do
+ {
+ new_path: { default: 'test', null: true, sql_type: 'text' },
+ merge_request_diff_id: { default: nil, null: false, sql_type: 'bigint' },
+ relative_order: { default: nil, null: false, sql_type: 'integer' }
+ }
+ end
+
+ let(:primary_key) { %w[merge_request_diff_id relative_order] }
+
+ before do
+ migration.create_table table_name, primary_key: primary_key do |t|
+ t.integer :merge_request_diff_id, null: false, default: 1
+ t.integer :relative_order, null: false
+ t.text :new_path, null: true, default: 'test'
+ end
+
+ source_model.table_name = table_name
+ end
+
+ it 'creates the partitions' do
+ migration.partition_table_by_int_range(table_name, partition_column_name, partition_size: partition_size, primary_key: primary_key)
+
+ expect_range_partitions_for(partitioned_table, partitions.merge(buffer_partitions))
+ end
+
+ it 'creates a composite primary key' do
+ migration.partition_table_by_int_range(table_name, partition_column_name, partition_size: partition_size, primary_key: primary_key)
+
+ expect(connection.primary_key(:_test_migration_partitioned_table)).to eql(%w[merge_request_diff_id relative_order])
+ end
+
+ it 'applies the correct column schema for the new table' do
+ migration.partition_table_by_int_range(table_name, partition_column_name, partition_size: partition_size, primary_key: primary_key)
+
+ columns = connection.columns(:_test_migration_partitioned_table)
+
+ columns.each do |column|
+ column_name = column.name.to_sym
+
+ expect(column.default).to eql(new_table_defition[column_name][:default])
+ expect(column.null).to eql(new_table_defition[column_name][:null])
+ expect(column.sql_type).to eql(new_table_defition[column_name][:sql_type])
+ end
+ end
+
+ it 'creates multiple partitions' do
+ migration.partition_table_by_int_range(table_name, partition_column_name, partition_size: 500, primary_key: primary_key)
+
+ expect_range_partitions_for(partitioned_table, {
+ '1' => %w[1 501],
+ '501' => %w[501 1001],
+ '1001' => %w[1001 1501],
+ '1501' => %w[1501 2001],
+ '2001' => %w[2001 2501],
+ '2501' => %w[2501 3001],
+ '3001' => %w[3001 3501],
+ '3501' => %w[3501 4001],
+ '4001' => %w[4001 4501],
+ '4501' => %w[4501 5001],
+ '5001' => %w[5001 5501],
+ '5501' => %w[5501 6001]
+ })
+ end
+
+ context 'when the table is not empty' do
+ before do
+ source_model.create!(merge_request_diff_id: 1, relative_order: 7, new_path: 'new_path')
+ end
+
+ let(:partition_size) { 2 }
+
+ let(:partitions) do
+ {
+ '1' => %w[1 3],
+ '3' => %w[3 5],
+ '5' => %w[5 7]
+ }
+ end
+
+ let(:buffer_partitions) do
+ {
+ '7' => %w[7 9],
+ '9' => %w[9 11],
+ '11' => %w[11 13],
+ '13' => %w[13 15],
+ '15' => %w[15 17],
+ '17' => %w[17 19]
+ }
+ end
+
+ it 'defaults the min_id to 1 and the max_id to 7' do
+ migration.partition_table_by_int_range(table_name, partition_column_name, partition_size: partition_size, primary_key: primary_key)
+
+ expect_range_partitions_for(partitioned_table, partitions.merge(buffer_partitions))
+ end
end
end
- context 'when run inside a transaction block' do
+ context 'when an invalid partition column is given' do
+ let(:invalid_column) { :_this_is_not_real }
+
it 'raises an error' do
- expect(migration).to receive(:transaction_open?).and_return(true)
+ expect do
+ migration.partition_table_by_int_range(source_table, invalid_column, partition_size: partition_size, primary_key: ['id'])
+ end.to raise_error(/partition column #{invalid_column} does not exist/)
+ end
+ end
+ context 'when partition_size is less than 1' do
+ let(:partition_size) { 1 }
+
+ it 'raises an error' do
expect do
- migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date
- end.to raise_error(/can not be run inside a transaction/)
+ subject
+ end.to raise_error(/partition_size must be greater than 1/)
+ end
+ end
+
+ context 'when the partitioned table already exists' do
+ before do
+ migration.send(:create_range_id_partitioned_copy, source_table,
+ migration.send(:make_partitioned_table_name, source_table),
+ connection.columns(source_table).find { |c| c.name == partition_column2 },
+ connection.columns(source_table).select { |c| new_primary_key.include?(c.name) })
+ end
+
+ it 'raises an error' do
+ expect(Gitlab::AppLogger).to receive(:warn).with(/Partitioned table not created because it already exists/)
+ expect { subject }.not_to raise_error
end
end
+ end
+
+ describe '#partition_table_by_date' do
+ let(:partition_column) { 'created_at' }
+ let(:old_primary_key) { 'id' }
+ let(:new_primary_key) { [old_primary_key, partition_column] }
+ let(:partition_column_name) { 'created_at' }
+ let(:partitioned_model) { Class.new(ActiveRecord::Base) }
+ let(:timestamp) { Time.utc(2019, 12, 1, 12).round }
+
+ subject { migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date }
+
+ include_examples "a measurable object"
context 'when the the max_date is less than the min_date' do
let(:max_date) { Time.utc(2019, 6) }
@@ -164,19 +500,6 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe
end
end
- context 'when the given table does not have a primary key' do
- it 'raises an error' do
- migration.execute(<<~SQL)
- ALTER TABLE #{source_table}
- DROP CONSTRAINT #{source_table}_pkey
- SQL
-
- expect do
- migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date
- end.to raise_error(/primary key not defined for #{source_table}/)
- end
- end
-
context 'when an invalid partition column is given' do
let(:invalid_column) { :_this_is_not_real }
@@ -188,34 +511,6 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe
end
describe 'constructing the partitioned table' do
- it 'creates a table partitioned by the proper column' do
- migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date
-
- expect(connection.table_exists?(partitioned_table)).to be(true)
- expect(connection.primary_key(partitioned_table)).to eq(new_primary_key)
-
- expect_table_partitioned_by(partitioned_table, [partition_column])
- end
-
- it 'requires the migration helper to be run in DDL mode' do
- expect(Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas).to receive(:require_ddl_mode!)
-
- migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date
-
- expect(connection.table_exists?(partitioned_table)).to be(true)
- expect(connection.primary_key(partitioned_table)).to eq(new_primary_key)
-
- expect_table_partitioned_by(partitioned_table, [partition_column])
- end
-
- it 'changes the primary key datatype to bigint' do
- migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date
-
- pk_column = connection.columns(partitioned_table).find { |c| c.name == old_primary_key }
-
- expect(pk_column.sql_type).to eq('bigint')
- end
-
context 'with a non-integer primary key datatype' do
before do
connection.create_table non_int_table, id: false do |t|
@@ -238,23 +533,6 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe
end
end
- it 'removes the default from the primary key column' do
- migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date
-
- pk_column = connection.columns(partitioned_table).find { |c| c.name == old_primary_key }
-
- expect(pk_column.default_function).to be_nil
- end
-
- it 'creates the partitioned table with the same non-key columns' do
- migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date
-
- copied_columns = filter_columns_by_name(connection.columns(partitioned_table), new_primary_key)
- original_columns = filter_columns_by_name(connection.columns(source_table), new_primary_key)
-
- expect(copied_columns).to match_array(original_columns)
- end
-
it 'creates a partition spanning over each month in the range given' do
migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date
@@ -340,75 +618,6 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe
end
end
end
-
- describe 'keeping data in sync with the partitioned table' do
- let(:partitioned_model) { Class.new(ActiveRecord::Base) }
- let(:timestamp) { Time.utc(2019, 12, 1, 12).round }
-
- before do
- partitioned_model.primary_key = :id
- partitioned_model.table_name = partitioned_table
- end
-
- it 'creates a trigger function on the original table' do
- expect_function_not_to_exist(function_name)
- expect_trigger_not_to_exist(source_table, trigger_name)
-
- migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date
-
- expect_function_to_exist(function_name)
- expect_valid_function_trigger(source_table, trigger_name, function_name, after: %w[delete insert update])
- end
-
- it 'syncs inserts to the partitioned tables' do
- migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date
-
- expect(partitioned_model.count).to eq(0)
-
- first_record = source_model.create!(name: 'Bob', age: 20, created_at: timestamp, updated_at: timestamp)
- second_record = source_model.create!(name: 'Alice', age: 30, created_at: timestamp, updated_at: timestamp)
-
- expect(partitioned_model.count).to eq(2)
- expect(partitioned_model.find(first_record.id).attributes).to eq(first_record.attributes)
- expect(partitioned_model.find(second_record.id).attributes).to eq(second_record.attributes)
- end
-
- it 'syncs updates to the partitioned tables' do
- migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date
-
- first_record = source_model.create!(name: 'Bob', age: 20, created_at: timestamp, updated_at: timestamp)
- second_record = source_model.create!(name: 'Alice', age: 30, created_at: timestamp, updated_at: timestamp)
-
- expect(partitioned_model.count).to eq(2)
-
- first_copy = partitioned_model.find(first_record.id)
- second_copy = partitioned_model.find(second_record.id)
-
- expect(first_copy.attributes).to eq(first_record.attributes)
- expect(second_copy.attributes).to eq(second_record.attributes)
-
- first_record.update!(age: 21, updated_at: timestamp + 1.hour)
-
- expect(partitioned_model.count).to eq(2)
- expect(first_copy.reload.attributes).to eq(first_record.attributes)
- expect(second_copy.reload.attributes).to eq(second_record.attributes)
- end
-
- it 'syncs deletes to the partitioned tables' do
- migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date
-
- first_record = source_model.create!(name: 'Bob', age: 20, created_at: timestamp, updated_at: timestamp)
- second_record = source_model.create!(name: 'Alice', age: 30, created_at: timestamp, updated_at: timestamp)
-
- expect(partitioned_model.count).to eq(2)
-
- first_record.destroy!
-
- expect(partitioned_model.count).to eq(1)
- expect(partitioned_model.find_by_id(first_record.id)).to be_nil
- expect(partitioned_model.find(second_record.id).attributes).to eq(second_record.attributes)
- end
- end
end
describe '#drop_partitioned_table_for' do
diff --git a/spec/lib/gitlab/database/postgres_constraint_spec.rb b/spec/lib/gitlab/database/postgres_constraint_spec.rb
index 75084a69115..140180540e0 100644
--- a/spec/lib/gitlab/database/postgres_constraint_spec.rb
+++ b/spec/lib/gitlab/database/postgres_constraint_spec.rb
@@ -51,9 +51,11 @@ RSpec.describe Gitlab::Database::PostgresConstraint, type: :model do
subject(:check_constraints) { described_class.check_constraints.by_table_identifier(table_identifier) }
it 'finds check constraints for the table' do
- expect(check_constraints.map(&:name)).to contain_exactly(check_constraint_a_positive,
- check_constraint_a_gt_b,
- invalid_constraint_a)
+ expect(check_constraints.map(&:name)).to contain_exactly(
+ check_constraint_a_positive,
+ check_constraint_a_gt_b,
+ invalid_constraint_a
+ )
end
it 'includes columns for the check constraints', :aggregate_failures do
@@ -108,8 +110,12 @@ RSpec.describe Gitlab::Database::PostgresConstraint, type: :model do
describe '#including_column' do
it 'only matches constraints on the given column' do
constraints_on_a = described_class.by_table_identifier(table_identifier).including_column('a').map(&:name)
- expect(constraints_on_a).to contain_exactly(check_constraint_a_positive, check_constraint_a_gt_b,
- unique_constraint_a, invalid_constraint_a)
+ expect(constraints_on_a).to contain_exactly(
+ check_constraint_a_positive,
+ check_constraint_a_gt_b,
+ unique_constraint_a,
+ invalid_constraint_a
+ )
end
end
diff --git a/spec/lib/gitlab/database/postgres_index_spec.rb b/spec/lib/gitlab/database/postgres_index_spec.rb
index d8a2612caf3..2e654a33a58 100644
--- a/spec/lib/gitlab/database/postgres_index_spec.rb
+++ b/spec/lib/gitlab/database/postgres_index_spec.rb
@@ -40,7 +40,7 @@ RSpec.describe Gitlab::Database::PostgresIndex do
it 'only btree and gist indexes' do
types = described_class.reindexing_support.map(&:type).uniq
- expect(types & %w(btree gist)).to eq(types)
+ expect(types & %w[btree gist]).to eq(types)
end
context 'with leftover indexes' do
@@ -71,7 +71,7 @@ RSpec.describe Gitlab::Database::PostgresIndex do
end
it 'retrieves leftover indexes matching the /_ccnew[0-9]*$/ pattern' do
- expect(subject.map(&:name)).to eq(%w(foobar_ccnew foobar_ccnew1))
+ expect(subject.map(&:name)).to eq(%w[foobar_ccnew foobar_ccnew1])
end
end
diff --git a/spec/lib/gitlab/database/query_analyzers/gitlab_schemas_metrics_spec.rb b/spec/lib/gitlab/database/query_analyzers/gitlab_schemas_metrics_spec.rb
index f325060e592..1909e134e66 100644
--- a/spec/lib/gitlab/database/query_analyzers/gitlab_schemas_metrics_spec.rb
+++ b/spec/lib/gitlab/database/query_analyzers/gitlab_schemas_metrics_spec.rb
@@ -35,7 +35,7 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::GitlabSchemasMetrics, query_ana
},
"for query accessing gitlab_ci and gitlab_main" => {
model: ApplicationRecord,
- sql: "SELECT 1 FROM projects LEFT JOIN ci_builds ON ci_builds.project_id=projects.id",
+ sql: "SELECT 1 FROM projects LEFT JOIN p_ci_builds ON p_ci_builds.project_id=projects.id",
expectations: {
gitlab_schemas: "gitlab_ci,gitlab_main_cell",
db_config_name: "main"
@@ -43,7 +43,7 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::GitlabSchemasMetrics, query_ana
},
"for query accessing gitlab_ci and gitlab_main the gitlab_schemas is always ordered" => {
model: ApplicationRecord,
- sql: "SELECT 1 FROM ci_builds LEFT JOIN projects ON ci_builds.project_id=projects.id",
+ sql: "SELECT 1 FROM p_ci_builds LEFT JOIN projects ON p_ci_builds.project_id=projects.id",
expectations: {
gitlab_schemas: "gitlab_ci,gitlab_main_cell",
db_config_name: "main"
@@ -51,7 +51,7 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::GitlabSchemasMetrics, query_ana
},
"for query accessing CI database" => {
model: Ci::ApplicationRecord,
- sql: "SELECT 1 FROM ci_builds",
+ sql: "SELECT 1 FROM p_ci_builds",
expectations: {
gitlab_schemas: "gitlab_ci",
db_config_name: "ci"
diff --git a/spec/lib/gitlab/database/query_analyzers/gitlab_schemas_validate_connection_spec.rb b/spec/lib/gitlab/database/query_analyzers/gitlab_schemas_validate_connection_spec.rb
index e3ff5ab4779..0664508fa8d 100644
--- a/spec/lib/gitlab/database/query_analyzers/gitlab_schemas_validate_connection_spec.rb
+++ b/spec/lib/gitlab/database/query_analyzers/gitlab_schemas_validate_connection_spec.rb
@@ -26,14 +26,14 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::GitlabSchemasValidateConnection
},
"for query accessing gitlab_ci and gitlab_main" => {
model: ApplicationRecord,
- sql: "SELECT 1 FROM projects LEFT JOIN ci_builds ON ci_builds.project_id=projects.id",
- expect_error: /The query tried to access \["projects", "ci_builds"\]/,
+ sql: "SELECT 1 FROM projects LEFT JOIN p_ci_builds ON p_ci_builds.project_id=projects.id",
+ expect_error: /The query tried to access \["projects", "p_ci_builds"\]/,
setup: -> (_) { skip_if_shared_database(:ci) }
},
"for query accessing gitlab_ci and gitlab_main the gitlab_schemas is always ordered" => {
model: ApplicationRecord,
- sql: "SELECT 1 FROM ci_builds LEFT JOIN projects ON ci_builds.project_id=projects.id",
- expect_error: /The query tried to access \["ci_builds", "projects"\]/,
+ sql: "SELECT 1 FROM p_ci_builds LEFT JOIN projects ON p_ci_builds.project_id=projects.id",
+ expect_error: /The query tried to access \["p_ci_builds", "projects"\]/,
setup: -> (_) { skip_if_shared_database(:ci) }
},
"for query accessing main table from CI database" => {
@@ -44,13 +44,13 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::GitlabSchemasValidateConnection
},
"for query accessing CI database" => {
model: Ci::ApplicationRecord,
- sql: "SELECT 1 FROM ci_builds",
+ sql: "SELECT 1 FROM p_ci_builds",
expect_error: nil
},
"for query accessing CI table from main database" => {
model: ::ApplicationRecord,
- sql: "SELECT 1 FROM ci_builds",
- expect_error: /The query tried to access \["ci_builds"\]/,
+ sql: "SELECT 1 FROM p_ci_builds",
+ expect_error: /The query tried to access \["p_ci_builds"\]/,
setup: -> (_) { skip_if_shared_database(:ci) }
},
"for query accessing unknown gitlab_schema" => {
@@ -89,7 +89,7 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::GitlabSchemasValidateConnection
it "throws an error when trying to access a table that belongs to the gitlab_ci schema from the main database" do
expect do
- ApplicationRecord.connection.execute("select * from ci_builds limit 1")
+ ApplicationRecord.connection.execute("select * from p_ci_builds limit 1")
end.to raise_error(Gitlab::Database::QueryAnalyzers::GitlabSchemasValidateConnection::CrossSchemaAccessError)
end
end
diff --git a/spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/columns_spec.rb b/spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/columns_spec.rb
new file mode 100644
index 00000000000..042bc297520
--- /dev/null
+++ b/spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/columns_spec.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::QueryAnalyzers::PreventSetOperatorMismatch::Columns,
+ feature_category: :cell do
+ include PreventSetOperatorMismatchHelper
+
+ let_it_be(:namespace_columns) { Namespace.column_names.join(',') }
+
+ describe '.types' do
+ let(:node) { sql_select_node(sql) }
+ let(:cte_refs) { {} }
+ let(:select_stmt) do
+ Gitlab::Database::QueryAnalyzers::PreventSetOperatorMismatch::SelectStmt.new(node, cte_refs)
+ end
+
+ subject { described_class.types(select_stmt) }
+
+ context 'when static column' do
+ let(:sql) { 'SELECT id FROM namespaces' }
+
+ it do
+ expect(subject).to contain_exactly(Type::STATIC)
+ end
+
+ context 'with dynamic reference' do
+ let(:sql) { 'SELECT id FROM (SELECT * FROM namespaces) AS xyz' }
+
+ it do
+ expect(subject).to contain_exactly(Type::STATIC)
+ end
+ end
+ end
+
+ context 'when dynamic column' do
+ let(:sql) { 'SELECT * FROM namespaces' }
+
+ it do
+ expect(subject).to contain_exactly(Type::DYNAMIC)
+ end
+
+ context 'with static reference' do
+ let(:sql) { 'SELECT * FROM (SELECT 1) AS xyz' }
+
+ it do
+ expect(subject).to contain_exactly(Type::STATIC)
+ end
+ end
+ end
+
+ context 'when reference has errors' do
+ let(:cte_refs) { { 'namespaces' => [Type::INVALID].to_set } }
+ let(:sql) { 'SELECT * FROM namespaces' }
+
+ it 'forward through error state' do
+ expect(subject).to include(Type::INVALID)
+ end
+ end
+
+ context 'when static and dynamic columns' do
+ let(:sql) { 'SELECT *, users.id FROM namespaces, users' }
+
+ it do
+ expect(subject).to contain_exactly(Type::DYNAMIC, Type::STATIC)
+ end
+ end
+
+ context 'when static column and error' do
+ let(:error_column) { "SELECT #{namespace_columns} FROM namespaces UNION SELECT * FROM namespaces" }
+ let(:sql) { "SELECT id, (#{error_column}) FROM namespaces" }
+
+ it do
+ expect(subject).to contain_exactly(Type::STATIC, Type::INVALID)
+ end
+ end
+
+ context 'when dynamic column and error' do
+ let(:error_column) { "SELECT #{namespace_columns} FROM namespaces UNION SELECT * FROM namespaces" }
+ let(:sql) { "SELECT *, (#{error_column}) FROM namespaces" }
+
+ it do
+ # The sub-select is treated as a Type::STATIC column for now. This could do with some refinement.
+ expect(subject).to contain_exactly(Type::DYNAMIC, Type::STATIC, Type::INVALID)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/common_table_expressions_spec.rb b/spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/common_table_expressions_spec.rb
new file mode 100644
index 00000000000..eacaa643ba5
--- /dev/null
+++ b/spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/common_table_expressions_spec.rb
@@ -0,0 +1,97 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::QueryAnalyzers::PreventSetOperatorMismatch::CommonTableExpressions,
+ feature_category: :cell do
+ include PreventSetOperatorMismatchHelper
+
+ describe '.references' do
+ let(:node) { sql_select_node(sql) }
+ let(:cte_refs) { {} }
+
+ subject { described_class.references(node, cte_refs) }
+
+ context 'when standard CTE' do
+ let(:sql) do
+ <<-SQL
+ WITH some_cte AS (#{cte})
+ SELECT 1
+ FROM some_cte
+ SQL
+ end
+
+ context 'with static SELECT' do
+ let(:cte) { 'SELECT 1' }
+
+ it do
+ exp = { "some_cte" => Set.new([Type::STATIC]) }
+ expect(subject).to eq(exp)
+ end
+ end
+
+ context 'with dynamic SELECT' do
+ let(:cte) { 'SELECT * FROM namespaces' }
+
+ it do
+ exp = { "some_cte" => Set.new([Type::DYNAMIC]) }
+ expect(subject).to eq(exp)
+ end
+ end
+ end
+
+ context 'when recursive CTE' do
+ let(:sql) do
+ <<-SQL
+ WITH RECURSIVE some_cte AS (#{cte})
+ SELECT 1
+ FROM some_cte
+ SQL
+ end
+
+ context 'with static SELECT' do
+ let(:cte) { 'SELECT 1 UNION SELECT 2' }
+
+ it do
+ exp = { "some_cte" => Set.new([Type::STATIC]) }
+ expect(subject).to eq(exp)
+ end
+ end
+
+ context 'with dynamic SELECT' do
+ let(:cte) { 'SELECT * FROM namespaces UNION SELECT * FROM namespaces' }
+
+ it do
+ exp = { "some_cte" => Set.new([Type::DYNAMIC]) }
+ expect(subject).to eq(exp)
+ end
+ end
+
+ context 'with error SELECT' do
+ let(:cte) { 'SELECT * FROM namespaces UNION SELECT id FROM namespaces' }
+
+ it do
+ exp = { "some_cte" => Set.new([Type::DYNAMIC, Type::STATIC, Type::INVALID]) }
+ expect(subject).to eq(exp)
+ end
+ end
+ end
+
+ context 'with inherited CTE references' do
+ let(:sql) do
+ <<-SQL
+ WITH some_cte AS (SELECT 1)
+ SELECT 1
+ FROM some_cte
+ SQL
+ end
+
+ let(:cte_refs) { { 'some_reference' => 123 } }
+
+ it 'maintains inherited CTE references' do
+ subject_ref_names = subject.keys
+ expect(subject_ref_names).to eq(cte_refs.keys + ['some_cte'])
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/froms_spec.rb b/spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/froms_spec.rb
new file mode 100644
index 00000000000..03c0a845e60
--- /dev/null
+++ b/spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/froms_spec.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::QueryAnalyzers::PreventSetOperatorMismatch::Froms,
+ feature_category: :cell do
+ include PreventSetOperatorMismatchHelper
+
+ describe '.references' do
+ let(:node) { sql_select_node(sql) }
+ let(:cte_refs) { {} }
+
+ subject { described_class.references(node, cte_refs) }
+
+ context 'when node is nil' do
+ let(:node) { nil }
+
+ it { is_expected.to eq({}) }
+ end
+
+ context 'when range_var' do
+ let(:sql) { 'SELECT 1 FROM namespaces' }
+
+ it { is_expected.to match({ 'namespaces' => an_instance_of(PgQuery::RangeVar) }) }
+ end
+
+ context 'when range_var with alias' do
+ let(:sql) { 'SELECT 1 FROM namespaces ns' }
+
+ it { is_expected.to match({ 'ns' => an_instance_of(PgQuery::RangeVar) }) }
+ end
+
+ context 'when join expression' do
+ let(:sql) do
+ <<-SQL
+ SELECT 1 FROM namespaces
+ INNER JOIN organizations ON namespaces.organization_id = organization.id
+ SQL
+ end
+
+ it do
+ is_expected.to match({
+ 'namespaces' => an_instance_of(PgQuery::RangeVar),
+ 'organizations' => an_instance_of(PgQuery::RangeVar)
+ })
+ end
+ end
+
+ context 'when join expression with alias' do
+ let(:sql) do
+ <<-SQL
+ SELECT 1 FROM namespaces ns
+ INNER JOIN organizations o ON ns.organization_id = o.id
+ SQL
+ end
+
+ it do
+ is_expected.to match({
+ 'ns' => an_instance_of(PgQuery::RangeVar),
+ 'o' => an_instance_of(PgQuery::RangeVar)
+ })
+ end
+ end
+
+ context 'when sub-query' do
+ let(:sql) do
+ <<-SQL
+ SELECT 1
+ FROM (SELECT 1) some_subquery
+ SQL
+ end
+
+ it { is_expected.to match({ 'some_subquery' => [Type::STATIC].to_set }) }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/node_spec.rb b/spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/node_spec.rb
new file mode 100644
index 00000000000..a8294376107
--- /dev/null
+++ b/spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/node_spec.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::QueryAnalyzers::PreventSetOperatorMismatch::Node, feature_category: :cell do
+ include PreventSetOperatorMismatchHelper
+
+ let(:sql) { 'SELECT id FROM namespaces' }
+ let(:node) { sql_select_node(sql) }
+
+ describe '.descendants' do
+ context 'with a block' do
+ it do
+ nodes = []
+ described_class.descendants(node.from_clause) do |node|
+ nodes << node.class
+ end
+ expect(nodes).to match_array [PgQuery::Node, PgQuery::RangeVar]
+ end
+ end
+
+ context 'without a block' do
+ subject { described_class.descendants(node) }
+
+ it { is_expected.to be_instance_of Enumerator }
+ end
+
+ context 'with a filter' do
+ let(:filter) { ->(field) { %i[from_clause target_list].include?(field) } }
+
+ subject { described_class.descendants(node, filter: filter).count }
+
+ it 'only traverse nodes that match the filter' do
+ is_expected.to eq 2
+ end
+ end
+ end
+
+ describe '.locate_descendant' do
+ subject { described_class.locate_descendant(node.target_list, :res_target) }
+
+ it { is_expected.to be_instance_of PgQuery::ResTarget }
+
+ context 'with a filter' do
+ subject { described_class.locate_descendant(node.target_list, :res_target, filter: ->(_) { false }) }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe '.locate_descendants' do
+ subject { described_class.locate_descendants(node.target_list, :res_target) }
+
+ it { is_expected.to be_instance_of Array }
+
+ context 'with a filter' do
+ subject { described_class.locate_descendant(node.target_list, :res_target, filter: ->(_) { false }) }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe '.dig' do
+ subject { described_class.dig(node.target_list[0], :res_target, :val, :column_ref) }
+
+ it { is_expected.to be_instance_of PgQuery::ColumnRef }
+ end
+end
diff --git a/spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/references_spec.rb b/spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/references_spec.rb
new file mode 100644
index 00000000000..0f0f92aa1f2
--- /dev/null
+++ b/spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/references_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::QueryAnalyzers::PreventSetOperatorMismatch::References,
+ feature_category: :cell do
+ include PreventSetOperatorMismatchHelper
+
+ let(:refs) do
+ {
+ 'resolved_reference' => Set.new,
+ 'unresolved_reference' => double,
+ 'table_reference' => PgQuery::RangeVar.new,
+ 'error_reference' => [Type::INVALID].to_set
+ }
+ end
+
+ describe '.resolved' do
+ subject { described_class.resolved(refs) }
+
+ it { is_expected.to eq refs.slice('resolved_reference', 'error_reference') }
+ end
+
+ describe '.unresolved' do
+ subject { described_class.unresolved(refs) }
+
+ it { is_expected.to eq refs.slice('unresolved_reference') }
+ end
+
+ describe '.errors?' do
+ subject { described_class.errors?(refs) }
+
+ it { is_expected.to be_truthy }
+
+ context 'when no errors exist' do
+ subject { described_class.errors?(refs.except('error_reference')) }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/select_stmt_spec.rb b/spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/select_stmt_spec.rb
new file mode 100644
index 00000000000..52d6c9f1032
--- /dev/null
+++ b/spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/select_stmt_spec.rb
@@ -0,0 +1,361 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::QueryAnalyzers::PreventSetOperatorMismatch::SelectStmt, feature_category: :cell do
+ include PreventSetOperatorMismatchHelper
+
+ let_it_be(:static_namespace_columns) { Namespace.column_names.join(', ') }
+
+ let(:node) { sql_select_node(sql) }
+
+ subject { described_class.new(node).types }
+
+ shared_examples 'valid SQL' do
+ it { is_expected.not_to include(Type::INVALID) }
+ end
+
+ shared_examples 'invalid SQL' do
+ it { is_expected.to include(Type::INVALID) }
+ end
+
+ shared_context 'with basic set operator queries' do
+ let(:set_operator_queries) do
+ {
+ 'set operator with static columns' => <<-SQL,
+ SELECT id, name FROM namespaces WHERE name = 'test1'
+ #{set_operator}
+ SELECT id, name FROM namespaces WHERE name = 'test2'
+ SQL
+ 'set operator with static referenced columns' => <<-SQL,
+ SELECT namespaces.id, name FROM namespaces WHERE name = 'test1'
+ #{set_operator}
+ SELECT id, namespaces.name FROM namespaces WHERE name = 'test2'
+ SQL
+ 'set operator with static alias referenced columns' => <<-SQL,
+ SELECT namespaces.id, name FROM namespaces WHERE name = 'test1'
+ #{set_operator}
+ SELECT id, namespaces2.name FROM namespaces2 WHERE name = 'test2'
+ SQL
+ 'set operator with dynamic columns' => <<-SQL,
+ SELECT * FROM namespaces WHERE name = 'test1'
+ #{set_operator}
+ SELECT * FROM namespaces WHERE name = 'test2'
+ SQL
+ 'set operator with dynamic referenced columns' => <<-SQL,
+ SELECT namespaces.* FROM namespaces WHERE name = 'test1'
+ #{set_operator}
+ SELECT namespaces.* FROM namespaces WHERE name = 'test2'
+ SQL
+ 'set operator with dynamic referenced aliased columns' => <<-SQL,
+ SELECT namespaces.* FROM namespaces WHERE name = 'test1'
+ #{set_operator}
+ SELECT namespaces2.* FROM namespaces namespaces2 WHERE name = 'test2'
+ SQL
+ 'set operator with dynamic columns without using star' => <<-SQL,
+ SELECT namespaces FROM namespaces WHERE name = 'test1'
+ #{set_operator}
+ SELECT * FROM namespaces WHERE name = 'test2'
+ SQL
+ 'set operator with single dynamic referenced columns' => <<-SQL,
+ SELECT namespaces.* FROM namespaces WHERE name = 'test1'
+ #{set_operator}
+ SELECT * FROM namespaces WHERE name = 'test2'
+ SQL
+ 'set operator with static and dynamic columns' => <<-SQL,
+ SELECT #{static_namespace_columns} FROM namespaces WHERE name = 'test1'
+ #{set_operator}
+ SELECT * FROM namespaces WHERE name = 'test2'
+ SQL
+ 'set operator with static aliased columns and dynamic columns' => <<-SQL,
+ SELECT #{Namespace.column_names.map { |c| "namespaces2.#{c}" }.join(', ')}
+ FROM namespaces namespaces2
+ WHERE name = 'test1'
+ #{set_operator}
+ SELECT * FROM namespaces WHERE name = 'test2'
+ SQL
+ 'set operator with static columns and dynamic aliased columns' => <<-SQL,
+ SELECT #{static_namespace_columns} FROM namespaces WHERE name = 'test1'
+ #{set_operator}
+ SELECT namespaces2.* FROM namespaces namespaces2 WHERE name = 'test2'
+ SQL
+ 'set operator with static and dynamic aliased columns' => <<-SQL,
+ SELECT #{Namespace.column_names.map { |c| "namespaces2.#{c}" }.join(', ')}
+ FROM namespaces namespaces2
+ WHERE name = 'test1'
+ #{set_operator}
+ SELECT namespaces3.* FROM namespaces namespaces3 WHERE name = 'test2'
+ SQL
+ 'set operator with mixed dynamic and static columns' => <<-SQL,
+ SELECT namespaces.*, projects.id FROM namespaces, projects WHERE name = 'test1'
+ #{set_operator}
+ SELECT namespaces.*, projects.id FROM namespaces, projects WHERE name = 'test2'
+ SQL
+ 'set operator without references' => <<-SQL
+ SELECT 1
+ #{set_operator}
+ SELECT 2
+ SQL
+ }
+ end
+
+ where(:query_name, :behavior) do
+ [
+ ['set operator with static columns', 'valid SQL'],
+ ['set operator with static referenced columns', 'valid SQL'],
+ ['set operator with static alias referenced columns', 'valid SQL'],
+ ['set operator with dynamic columns', 'valid SQL'],
+ ['set operator with dynamic referenced columns', 'valid SQL'],
+ ['set operator with dynamic referenced aliased columns', 'valid SQL'],
+ ['set operator with dynamic columns without using star', 'invalid SQL'],
+ ['set operator with single dynamic referenced columns', 'valid SQL'],
+ ['set operator with static and dynamic columns', 'invalid SQL'],
+ ['set operator with static and dynamic aliased columns', 'invalid SQL'],
+ ['set operator with static aliased columns and dynamic columns', 'invalid SQL'],
+ ['set operator with static columns and dynamic aliased columns', 'invalid SQL'],
+ ['set operator with static and dynamic aliased columns', 'invalid SQL'],
+ ['set operator with mixed dynamic and static columns', 'valid SQL'],
+ ['set operator without references', 'valid SQL']
+ ]
+ end
+ end
+
+ %w[UNION INTERSECT EXCEPT].each do |set_operator|
+ context "with #{set_operator}" do
+ let(:set_operator) { set_operator }
+
+ context "for basic #{set_operator} queries" do
+ include_context 'with basic set operator queries'
+
+ with_them do
+ let(:sql) { set_operator_queries[query_name] }
+
+ it_behaves_like params[:behavior]
+ end
+ end
+
+ context 'for subquery' do
+ context "with #{set_operator}" do
+ where(:select_columns) do
+ [
+ ['*'],
+ ['sub.*'],
+ ['sub'],
+ ['sub.id']
+ ]
+ end
+
+ with_them do
+ include_context 'with basic set operator queries'
+
+ with_them do
+ let(:sql) do
+ <<-SQL
+ SELECT #{select_columns}
+ FROM (
+ #{set_operator_queries[query_name]}
+ ) sub
+ SQL
+ end
+
+ it_behaves_like params[:behavior]
+ end
+ end
+ end
+
+ context "when used by one side of #{set_operator}" do
+ let(:sql) do
+ <<-SQL
+ SELECT #{union1}
+ FROM (
+ SELECT #{subquery}
+ FROM namespaces
+ ) namespaces
+
+ #{set_operator}
+
+ SELECT #{union2}
+ FROM namespaces
+ SQL
+ end
+
+ where(:union1, :union2, :subquery, :expected) do
+ [
+ ['*', '*', '*', 'valid SQL'],
+ [ref(:static_namespace_columns), '*', '*', 'invalid SQL'],
+ ['*', ref(:static_namespace_columns), '*', 'invalid SQL'],
+ ['*', '*', ref(:static_namespace_columns), 'invalid SQL'],
+ [ref(:static_namespace_columns), ref(:static_namespace_columns), '*', 'valid SQL'],
+ [ref(:static_namespace_columns), '*', ref(:static_namespace_columns), 'invalid SQL'],
+ ['*', ref(:static_namespace_columns), ref(:static_namespace_columns), 'valid SQL'],
+ ['namespaces', 'namespaces', 'namespaces', 'valid SQL'],
+ # Used by our keyset pagination queries.
+ ['NULL :: namespaces', 'namespaces', 'id, name', 'valid SQL'],
+ ['NULL :: namespaces, id, name', 'namespaces, id, name', 'namespaces', 'valid SQL']
+ ]
+ end
+
+ with_them do
+ it_behaves_like params[:expected]
+ end
+ end
+ end
+
+ context 'for CTE' do
+ context "when #{set_operator}" do
+ where(:select_columns) do
+ [
+ ['*'],
+ ['namespaces_cte.*'],
+ ['namespaces_cte.id']
+ ]
+ end
+
+ with_them do
+ include_context 'with basic set operator queries'
+
+ with_them do
+ let(:sql) do
+ <<-SQL
+ WITH namespaces_cte AS (
+ #{set_operator_queries[query_name]}
+ )
+ SELECT *
+ FROM namespaces_cte
+ SQL
+ end
+
+ it_behaves_like params[:behavior]
+ end
+ end
+ end
+
+ context "when used by one side of #{set_operator}" do
+ let(:sql) do
+ <<-SQL
+ WITH #{cte_name} AS (
+ SELECT #{cte_select_columns}
+ FROM namespaces
+ )
+ SELECT #{select_columns}
+ FROM #{cte_name}
+
+ #{set_operator}
+
+ SELECT *
+ FROM namespaces
+ SQL
+ end
+
+ where(:cte_select_columns, :select_columns, :cte_name, :expected) do
+ [
+ ['*', '*', 'some_cte', 'valid SQL'],
+ [ref(:static_namespace_columns), '*', 'some_cte', 'invalid SQL'],
+ ['*', ref(:static_namespace_columns), 'some_cte', 'invalid SQL'],
+ [ref(:static_namespace_columns), ref(:static_namespace_columns), 'some_cte', 'invalid SQL'],
+ ['*', '*', 'some_cte', 'valid SQL'],
+ # Same scenarios as above, but the CTE name matches the table name in the CTE.
+ ['*', '*', 'namespaces', 'valid SQL'],
+ [ref(:static_namespace_columns), '*', 'namespaces', 'valid SQL'],
+ ['*', ref(:static_namespace_columns), 'namespaces', 'invalid SQL'],
+ [ref(:static_namespace_columns), ref(:static_namespace_columns), 'namespaces', 'valid SQL'],
+ ['*', '*', 'namespaces', 'valid SQL']
+ ]
+ end
+
+ with_them do
+ it_behaves_like params[:expected]
+ end
+ end
+
+ context 'when recursive' do
+ let(:sql) do
+ <<-SQL
+ WITH RECURSIVE namespaces_cte AS (
+ (
+ SELECT #{select1}
+ FROM namespaces
+ )
+ UNION
+ (
+ SELECT #{select2}
+ FROM namespaces_cte
+ )
+ )
+ SELECT *
+ FROM namespaces_cte
+ SQL
+ end
+
+ where(:select1, :select2, :expected) do
+ [
+ ['id', 'id', 'valid SQL'],
+ [ref(:static_namespace_columns), '*', 'valid SQL'],
+ ['*', ref(:static_namespace_columns), 'invalid SQL']
+ ]
+ end
+
+ with_them do
+ it_behaves_like params[:expected]
+ end
+ end
+ end
+
+ context 'for subselect' do
+ context 'with set operator' do
+ let(:sql) do
+ <<-SQL
+ SELECT (
+ SELECT id FROM namespaces
+ #{set_operator}
+ SELECT id FROM namespaces
+ ) AS namespace_id
+ SQL
+ end
+
+ it_behaves_like 'valid SQL'
+ end
+ end
+ end
+ end
+
+ context 'with lateral join' do
+ let(:sql) do
+ <<-SQL
+ SELECT namespaces.id
+ FROM
+ namespaces CROSS
+ JOIN LATERAL (
+ SELECT
+ namespaces.traversal_ids[array_length(namespaces.traversal_ids, 1) ] AS id
+ FROM
+ namespaces
+ WHERE
+ namespaces.type = 'Group'
+ AND namespaces.traversal_ids @ > ARRAY[members.source_id]
+ ) namespaces
+ SQL
+ end
+
+ pending
+ end
+
+ context 'when columns are not referenced' do
+ let(:sql) do
+ <<-SQL
+ SELECT
+ COUNT(1)
+ FROM (
+ SELECT #{static_namespace_columns}
+ FROM namespaces
+ UNION
+ SELECT *
+ FROM namespaces
+ ) invalid_union
+ SQL
+ end
+
+ # Error will bubble up even though the parent query does not reference any of the sub-query columns.
+ it_behaves_like 'invalid SQL'
+ end
+end
diff --git a/spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/targets_spec.rb b/spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/targets_spec.rb
new file mode 100644
index 00000000000..2ea69f3726e
--- /dev/null
+++ b/spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch/targets_spec.rb
@@ -0,0 +1,94 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::QueryAnalyzers::PreventSetOperatorMismatch::Targets, feature_category: :cell do
+ include PreventSetOperatorMismatchHelper
+
+ let(:node) { sql_select_node(sql) }
+ let(:select_stmt) { Gitlab::Database::QueryAnalyzers::PreventSetOperatorMismatch::SelectStmt.new(node) }
+ let(:target) { node.target_list[0].res_target }
+
+ describe '.reference_names' do
+ subject { described_class.reference_names(target, select_stmt) }
+
+ context 'with a literal target' do
+ let(:sql) { 'SELECT 1' }
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'with a function target' do
+ let(:sql) { 'SELECT unnest(ARRAY[1,2]) FROM namespaces, users' }
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'with a subselect target' do
+ let(:sql) { 'SELECT (SELECT 1) xyz FROM namespaces' }
+
+ it { is_expected.to eq(%w[xyz_subselect]) }
+
+ it 'updates all_references in the select statement' do
+ expect { subject }.to change { select_stmt.all_references }
+ .to include('xyz_subselect')
+ end
+ end
+
+ context 'with an unqualified column name' do
+ let(:sql) { 'SELECT id FROM namespaces, users' }
+
+ it { is_expected.to eq(%w[namespaces users]) }
+ end
+
+ context 'with a qualified column name' do
+ let(:sql) { 'SELECT namespaces.id FROM namespaces, users' }
+
+ it { is_expected.to eq(%w[namespaces]) }
+ end
+
+ context 'with a table name' do
+ let(:sql) { 'SELECT namespaces FROM namespaces, users' }
+
+ it { is_expected.to eq(%w[namespaces]) }
+ end
+
+ context 'with a *' do
+ let(:sql) { 'SELECT * FROM namespaces, users' }
+
+ it { is_expected.to eq(%w[namespaces users]) }
+ end
+ end
+
+ describe '.a_star?' do
+ subject { described_class.a_star?(target) }
+
+ context 'when * is used' do
+ let(:sql) { 'SELECT * FROM namespaces' }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when no * is used' do
+ let(:sql) { 'SELECT 1' }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '.null?' do
+ subject { described_class.null?(target) }
+
+ context 'when target is null' do
+ let(:sql) { 'SELECT NULL::namespaces FROM namespaces' }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when target is not null' do
+ let(:sql) { 'SELECT 1' }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch_spec.rb b/spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch_spec.rb
new file mode 100644
index 00000000000..28c155c1eb1
--- /dev/null
+++ b/spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch_spec.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::QueryAnalyzers::PreventSetOperatorMismatch, query_analyzers: false, feature_category: :cell do
+ let(:analyzer) { described_class }
+ let_it_be(:static_namespace_columns) { Namespace.column_names.join(', ') }
+
+ def process_sql(sql, model = ApplicationRecord)
+ Gitlab::Database::QueryAnalyzer.instance.within([analyzer]) do
+ # Skip load balancer and retrieve connection assigned to model
+ Gitlab::Database::QueryAnalyzer.instance.send(:process_sql, sql, model.retrieve_connection)
+ end
+ end
+
+ shared_examples 'parses SQL' do
+ it do
+ expect_next_instance_of(described_class::SelectStmt) do |select_stmt|
+ expect(select_stmt).to receive(:types).and_return(Set.new)
+ end
+
+ process_sql sql
+ end
+ end
+
+ context 'when SQL includes a UNION' do
+ let(:sql) { 'SELECT 1 UNION SELECT 2' }
+
+ include_examples 'parses SQL'
+ end
+
+ context 'when SQL includes a INTERSECT' do
+ let(:sql) { 'SELECT 1 INTERSECT SELECT 2' }
+
+ include_examples 'parses SQL'
+ end
+
+ context 'when SQL includes a EXCEPT' do
+ let(:sql) { 'SELECT 1 EXCEPT SELECT 2' }
+
+ include_examples 'parses SQL'
+ end
+
+ context 'when SQL does not include a set operator' do
+ let(:sql) { 'SELECT 1' }
+
+ it 'does not parse SQL' do
+ expect(described_class::SelectStmt).not_to receive(:new)
+
+ process_sql sql
+ end
+ end
+
+ context 'when SQL is invalid' do
+ it 'raises error' do
+ expect do
+ process_sql "SELECT #{static_namespace_columns} FROM namespaces UNION SELECT * FROM namespaces"
+ end.to raise_error(described_class::SetOperatorStarError)
+ end
+ end
+
+ context 'when SQL is valid' do
+ it 'does not raise error' do
+ expect do
+ process_sql 'SELECT 1'
+ end.not_to raise_error
+ end
+ end
+
+ context 'when SQL has many select statements' do
+ let(:sql) do
+ <<-SQL
+ SELECT 1 UNION SELECT 1;
+ SELECT #{static_namespace_columns} FROM namespaces UNION SELECT * FROM namespaces
+ SQL
+ end
+
+ it 'raises error' do
+ expect do
+ process_sql sql
+ end.to raise_error(described_class::SetOperatorStarError)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/reindexing_spec.rb b/spec/lib/gitlab/database/reindexing_spec.rb
index 441f6476abe..2321f5d933d 100644
--- a/spec/lib/gitlab/database/reindexing_spec.rb
+++ b/spec/lib/gitlab/database/reindexing_spec.rb
@@ -231,7 +231,7 @@ RSpec.describe Gitlab::Database::Reindexing, feature_category: :database, time_t
states = queued_actions.map(&:reload).map(&:state)
- expect(states).to eq(%w(failed done queued))
+ expect(states).to eq(%w[failed done queued])
end
end
diff --git a/spec/lib/gitlab/database/tables_locker_spec.rb b/spec/lib/gitlab/database/tables_locker_spec.rb
index 0e7e929d54b..e3bd61b32a1 100644
--- a/spec/lib/gitlab/database/tables_locker_spec.rb
+++ b/spec/lib/gitlab/database/tables_locker_spec.rb
@@ -33,30 +33,40 @@ RSpec.describe Gitlab::Database::TablesLocker, :suppress_gitlab_schemas_validate
FOR VALUES IN (0)
SQL
- ApplicationRecord.connection.execute(create_partition_sql)
- Ci::ApplicationRecord.connection.execute(create_partition_sql)
-
create_detached_partition_sql = <<~SQL
CREATE TABLE IF NOT EXISTS #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}._test_gitlab_main_part_202201 (
id bigserial primary key not null
)
SQL
- ApplicationRecord.connection.execute(create_detached_partition_sql)
- Ci::ApplicationRecord.connection.execute(create_detached_partition_sql)
+ [ApplicationRecord, Ci::ApplicationRecord]
+ .map(&:connection)
+ .each do |conn|
+ conn.execute(create_partition_sql)
+ conn.execute(
+ "DROP TABLE IF EXISTS #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}._test_gitlab_main_part_202201"
+ )
+ conn.execute(create_detached_partition_sql)
+
+ Gitlab::Database::SharedModel.using_connection(conn) do
+ Postgresql::DetachedPartition.delete_all
+ Postgresql::DetachedPartition.create!(
+ table_name: '_test_gitlab_main_part_20220101',
+ drop_after: Time.current
+ )
+ end
+ end
+ end
- Gitlab::Database::SharedModel.using_connection(ApplicationRecord.connection) do
- Postgresql::DetachedPartition.create!(
- table_name: '_test_gitlab_main_part_20220101',
- drop_after: Time.current
- )
- end
- Gitlab::Database::SharedModel.using_connection(Ci::ApplicationRecord.connection) do
- Postgresql::DetachedPartition.create!(
- table_name: '_test_gitlab_main_part_20220101',
- drop_after: Time.current
- )
- end
+ after(:all) do
+ [ApplicationRecord, Ci::ApplicationRecord]
+ .map(&:connection)
+ .each do |conn|
+ conn.execute(
+ "DROP TABLE IF EXISTS #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}._test_gitlab_main_part_202201"
+ )
+ Gitlab::Database::SharedModel.using_connection(conn) { Postgresql::DetachedPartition.delete_all }
+ end
end
shared_examples "lock tables" do |gitlab_schema, database_name|
diff --git a/spec/lib/gitlab/database/tables_truncate_spec.rb b/spec/lib/gitlab/database/tables_truncate_spec.rb
index e41c7d34378..352c2fff779 100644
--- a/spec/lib/gitlab/database/tables_truncate_spec.rb
+++ b/spec/lib/gitlab/database/tables_truncate_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Gitlab::Database::TablesTruncate, :reestablished_active_record_base,
- :suppress_gitlab_schemas_validate_connection, feature_category: :cell do
+ :suppress_gitlab_schemas_validate_connection, feature_category: :cell do
include MigrationsHelpers
let(:min_batch_size) { 1 }
@@ -373,7 +373,9 @@ RSpec.describe Gitlab::Database::TablesTruncate, :reestablished_active_record_ba
context 'with no main data in ci datatabase' do
before do
# Remove 'main' data in ci database
- ci_connection.truncate_tables([:_test_gitlab_main_items, :_test_gitlab_main_references])
+ ci_connection.execute(
+ "TRUNCATE TABLE _test_gitlab_main_items, _test_gitlab_main_references RESTART IDENTITY CASCADE;"
+ )
end
it { is_expected.to eq(false) }
diff --git a/spec/lib/gitlab/database/transaction/observer_spec.rb b/spec/lib/gitlab/database/transaction/observer_spec.rb
index d1cb014a594..778212add66 100644
--- a/spec/lib/gitlab/database/transaction/observer_spec.rb
+++ b/spec/lib/gitlab/database/transaction/observer_spec.rb
@@ -25,7 +25,7 @@ RSpec.describe Gitlab::Database::Transaction::Observer, feature_category: :datab
User.first
expect(transaction_context).to be_a(::Gitlab::Database::Transaction::Context)
- expect(context.keys).to match_array(%i(start_time depth savepoints queries backtraces external_http_count_start external_http_duration_start))
+ expect(context.keys).to match_array(%i[start_time depth savepoints queries backtraces external_http_count_start external_http_duration_start])
expect(context[:depth]).to eq(2)
expect(context[:savepoints]).to eq(1)
expect(context[:queries].length).to eq(1)
diff --git a/spec/lib/gitlab/dependency_linker/base_linker_spec.rb b/spec/lib/gitlab/dependency_linker/base_linker_spec.rb
index 2811bc859da..ef61a962279 100644
--- a/spec/lib/gitlab/dependency_linker/base_linker_spec.rb
+++ b/spec/lib/gitlab/dependency_linker/base_linker_spec.rb
@@ -46,8 +46,8 @@ RSpec.describe Gitlab::DependencyLinker::BaseLinker do
'target="_blank"'
]
- attrs.unshift(%{href="#{url}"}) if url
+ attrs.unshift(%(href="#{url}")) if url
- %{<a #{attrs.join(' ')}>#{text}</a>}
+ %(<a #{attrs.join(' ')}>#{text}</a>)
end
end
diff --git a/spec/lib/gitlab/dependency_linker/cargo_toml_linker_spec.rb b/spec/lib/gitlab/dependency_linker/cargo_toml_linker_spec.rb
index 7f6b3b86799..d147afbf3bd 100644
--- a/spec/lib/gitlab/dependency_linker/cargo_toml_linker_spec.rb
+++ b/spec/lib/gitlab/dependency_linker/cargo_toml_linker_spec.rb
@@ -70,7 +70,7 @@ RSpec.describe Gitlab::DependencyLinker::CargoTomlLinker do
subject { Gitlab::Highlight.highlight(file_name, file_content) }
def link(name, url)
- %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>}
+ %(<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>)
end
it 'links dependencies' do
diff --git a/spec/lib/gitlab/dependency_linker/cartfile_linker_spec.rb b/spec/lib/gitlab/dependency_linker/cartfile_linker_spec.rb
index 52ddba24458..eeb7471cd18 100644
--- a/spec/lib/gitlab/dependency_linker/cartfile_linker_spec.rb
+++ b/spec/lib/gitlab/dependency_linker/cartfile_linker_spec.rb
@@ -54,7 +54,7 @@ RSpec.describe Gitlab::DependencyLinker::CartfileLinker do
subject { Gitlab::Highlight.highlight(file_name, file_content) }
def link(name, url)
- %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>}
+ %(<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>)
end
it 'links dependencies' do
diff --git a/spec/lib/gitlab/dependency_linker/composer_json_linker_spec.rb b/spec/lib/gitlab/dependency_linker/composer_json_linker_spec.rb
index 02fac96a02f..cf636a7f201 100644
--- a/spec/lib/gitlab/dependency_linker/composer_json_linker_spec.rb
+++ b/spec/lib/gitlab/dependency_linker/composer_json_linker_spec.rb
@@ -50,7 +50,7 @@ RSpec.describe Gitlab::DependencyLinker::ComposerJsonLinker do
subject { Gitlab::Highlight.highlight(file_name, file_content) }
def link(name, url)
- %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>}
+ %(<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>)
end
it 'does not link the module name' do
diff --git a/spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb b/spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb
index 00e95dea224..a5507fab3fe 100644
--- a/spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb
+++ b/spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb
@@ -35,7 +35,7 @@ RSpec.describe Gitlab::DependencyLinker::GemfileLinker do
subject { Gitlab::Highlight.highlight(file_name, file_content) }
def link(name, url)
- %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>}
+ %(<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>)
end
it 'links sources' do
diff --git a/spec/lib/gitlab/dependency_linker/gemspec_linker_spec.rb b/spec/lib/gitlab/dependency_linker/gemspec_linker_spec.rb
index ae82dd51c95..9f207459113 100644
--- a/spec/lib/gitlab/dependency_linker/gemspec_linker_spec.rb
+++ b/spec/lib/gitlab/dependency_linker/gemspec_linker_spec.rb
@@ -42,7 +42,7 @@ RSpec.describe Gitlab::DependencyLinker::GemspecLinker do
subject { Gitlab::Highlight.highlight(file_name, file_content) }
def link(name, url)
- %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>}
+ %(<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>)
end
it 'does not link the gem name' do
diff --git a/spec/lib/gitlab/dependency_linker/go_mod_linker_spec.rb b/spec/lib/gitlab/dependency_linker/go_mod_linker_spec.rb
index 605b14bc923..fbd8b6477a2 100644
--- a/spec/lib/gitlab/dependency_linker/go_mod_linker_spec.rb
+++ b/spec/lib/gitlab/dependency_linker/go_mod_linker_spec.rb
@@ -55,7 +55,7 @@ RSpec.describe Gitlab::DependencyLinker::GoModLinker do
subject { Gitlab::Highlight.highlight(file_name, file_content) }
def link(name, url)
- %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>}
+ %(<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>)
end
it 'links the module name' do
diff --git a/spec/lib/gitlab/dependency_linker/go_sum_linker_spec.rb b/spec/lib/gitlab/dependency_linker/go_sum_linker_spec.rb
index 2836c0e9f29..559c27e91ba 100644
--- a/spec/lib/gitlab/dependency_linker/go_sum_linker_spec.rb
+++ b/spec/lib/gitlab/dependency_linker/go_sum_linker_spec.rb
@@ -35,7 +35,7 @@ RSpec.describe Gitlab::DependencyLinker::GoSumLinker do
subject { Gitlab::Highlight.highlight(file_name, file_content) }
def link(name, url)
- %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>}
+ %(<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>)
end
it 'links modules' do
diff --git a/spec/lib/gitlab/dependency_linker/godeps_json_linker_spec.rb b/spec/lib/gitlab/dependency_linker/godeps_json_linker_spec.rb
index c1ed030c548..3ac234f47d9 100644
--- a/spec/lib/gitlab/dependency_linker/godeps_json_linker_spec.rb
+++ b/spec/lib/gitlab/dependency_linker/godeps_json_linker_spec.rb
@@ -61,7 +61,7 @@ RSpec.describe Gitlab::DependencyLinker::GodepsJsonLinker do
subject { Gitlab::Highlight.highlight(file_name, file_content) }
def link(name, url)
- %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>}
+ %(<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>)
end
it 'links the package name' do
diff --git a/spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb b/spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb
index cdfc0e89bc7..127f437dd54 100644
--- a/spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb
+++ b/spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb
@@ -51,7 +51,7 @@ RSpec.describe Gitlab::DependencyLinker::PackageJsonLinker do
subject { Gitlab::Highlight.highlight(file_name, file_content) }
def link(name, url)
- %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>}
+ %(<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>)
end
it 'does not link the module name' do
diff --git a/spec/lib/gitlab/dependency_linker/podfile_linker_spec.rb b/spec/lib/gitlab/dependency_linker/podfile_linker_spec.rb
index 8e536c00ea6..41c29278bda 100644
--- a/spec/lib/gitlab/dependency_linker/podfile_linker_spec.rb
+++ b/spec/lib/gitlab/dependency_linker/podfile_linker_spec.rb
@@ -35,7 +35,7 @@ RSpec.describe Gitlab::DependencyLinker::PodfileLinker do
subject { Gitlab::Highlight.highlight(file_name, file_content) }
def link(name, url)
- %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>}
+ %(<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>)
end
it 'links sources' do
diff --git a/spec/lib/gitlab/dependency_linker/podspec_json_linker_spec.rb b/spec/lib/gitlab/dependency_linker/podspec_json_linker_spec.rb
index 1f81049a41e..f8b782a7cda 100644
--- a/spec/lib/gitlab/dependency_linker/podspec_json_linker_spec.rb
+++ b/spec/lib/gitlab/dependency_linker/podspec_json_linker_spec.rb
@@ -66,7 +66,7 @@ RSpec.describe Gitlab::DependencyLinker::PodspecJsonLinker do
subject { Gitlab::Highlight.highlight(file_name, file_content) }
def link(name, url)
- %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>}
+ %(<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>)
end
it 'links the gem name' do
diff --git a/spec/lib/gitlab/dependency_linker/podspec_linker_spec.rb b/spec/lib/gitlab/dependency_linker/podspec_linker_spec.rb
index 132b5b21d85..6f2653829e2 100644
--- a/spec/lib/gitlab/dependency_linker/podspec_linker_spec.rb
+++ b/spec/lib/gitlab/dependency_linker/podspec_linker_spec.rb
@@ -41,7 +41,7 @@ RSpec.describe Gitlab::DependencyLinker::PodspecLinker do
subject { Gitlab::Highlight.highlight(file_name, file_content) }
def link(name, url)
- %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>}
+ %(<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>)
end
it 'does not link the pod name' do
diff --git a/spec/lib/gitlab/dependency_linker/requirements_txt_linker_spec.rb b/spec/lib/gitlab/dependency_linker/requirements_txt_linker_spec.rb
index 86ebddc9681..fc3c57b7cff 100644
--- a/spec/lib/gitlab/dependency_linker/requirements_txt_linker_spec.rb
+++ b/spec/lib/gitlab/dependency_linker/requirements_txt_linker_spec.rb
@@ -64,7 +64,7 @@ RSpec.describe Gitlab::DependencyLinker::RequirementsTxtLinker do
subject { Gitlab::Highlight.highlight(file_name, file_content) }
def link(name, url)
- %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>}
+ %(<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>)
end
it 'links dependencies' do
diff --git a/spec/lib/gitlab/diff/file_collection/compare_spec.rb b/spec/lib/gitlab/diff/file_collection/compare_spec.rb
index c3f768db7f0..5469a43e46e 100644
--- a/spec/lib/gitlab/diff/file_collection/compare_spec.rb
+++ b/spec/lib/gitlab/diff/file_collection/compare_spec.rb
@@ -10,9 +10,11 @@ RSpec.describe Gitlab::Diff::FileCollection::Compare do
let(:start_commit) { sample_image_commit }
let(:head_commit) { sample_commit }
let(:raw_compare) do
- Gitlab::Git::Compare.new(project.repository.raw_repository,
- start_commit.id,
- head_commit.id)
+ Gitlab::Git::Compare.new(
+ project.repository.raw_repository,
+ start_commit.id,
+ head_commit.id
+ )
end
let(:diffable) { Compare.new(raw_compare, project) }
diff --git a/spec/lib/gitlab/diff/file_collection/merge_request_diff_batch_spec.rb b/spec/lib/gitlab/diff/file_collection/merge_request_diff_batch_spec.rb
index 8e14f48ae29..65e96f8e936 100644
--- a/spec/lib/gitlab/diff/file_collection/merge_request_diff_batch_spec.rb
+++ b/spec/lib/gitlab/diff/file_collection/merge_request_diff_batch_spec.rb
@@ -10,10 +10,7 @@ RSpec.describe Gitlab::Diff::FileCollection::MergeRequestDiffBatch, feature_cate
let(:diff_files_relation) { diffable.merge_request_diff_files }
subject do
- described_class.new(diffable,
- batch_page,
- batch_size,
- diff_options: nil)
+ described_class.new(diffable, batch_page, batch_size, diff_options: nil)
end
let(:diff_files) { subject.diff_files }
@@ -87,10 +84,7 @@ RSpec.describe Gitlab::Diff::FileCollection::MergeRequestDiffBatch, feature_cate
context 'last page' do
it 'returns correct diff files' do
last_page = diff_files_relation.count - batch_size
- collection = described_class.new(diffable,
- last_page,
- batch_size,
- diff_options: nil)
+ collection = described_class.new(diffable, last_page, batch_size, diff_options: nil)
expected_batch_files = diff_files_relation.offset(last_page).limit(batch_size).map(&:new_path)
@@ -101,10 +95,7 @@ RSpec.describe Gitlab::Diff::FileCollection::MergeRequestDiffBatch, feature_cate
it_behaves_like 'unfoldable diff' do
subject do
- described_class.new(merge_request.merge_request_diff,
- batch_page,
- batch_size,
- diff_options: nil)
+ described_class.new(merge_request.merge_request_diff, batch_page, batch_size, diff_options: nil)
end
end
@@ -118,10 +109,7 @@ RSpec.describe Gitlab::Diff::FileCollection::MergeRequestDiffBatch, feature_cate
let(:stub_path) { '.gitignore' }
subject do
- described_class.new(merge_request.merge_request_diff,
- batch_page,
- batch_size,
- **collection_default_args)
+ described_class.new(merge_request.merge_request_diff, batch_page, batch_size, **collection_default_args)
end
end
@@ -136,10 +124,7 @@ RSpec.describe Gitlab::Diff::FileCollection::MergeRequestDiffBatch, feature_cate
end
subject do
- described_class.new(merge_request.merge_request_diff,
- batch_page,
- batch_size,
- **collection_default_args)
+ described_class.new(merge_request.merge_request_diff, batch_page, batch_size, **collection_default_args)
end
end
end
diff --git a/spec/lib/gitlab/diff/file_collection/paginated_merge_request_diff_spec.rb b/spec/lib/gitlab/diff/file_collection/paginated_merge_request_diff_spec.rb
index ee956d04325..891336658ce 100644
--- a/spec/lib/gitlab/diff/file_collection/paginated_merge_request_diff_spec.rb
+++ b/spec/lib/gitlab/diff/file_collection/paginated_merge_request_diff_spec.rb
@@ -11,9 +11,7 @@ RSpec.describe Gitlab::Diff::FileCollection::PaginatedMergeRequestDiff, feature_
let(:diff_files) { subject.diff_files }
subject do
- described_class.new(diffable,
- page,
- per_page)
+ described_class.new(diffable, page, per_page)
end
describe '#diff_files' do
@@ -79,9 +77,7 @@ RSpec.describe Gitlab::Diff::FileCollection::PaginatedMergeRequestDiff, feature_
context 'when last page' do
it 'returns correct diff files' do
last_page = diff_files_relation.count - per_page
- collection = described_class.new(diffable,
- last_page,
- per_page)
+ collection = described_class.new(diffable, last_page, per_page)
expected_batch_files = diff_files_relation.page(last_page).per(per_page).map(&:new_path)
@@ -92,9 +88,7 @@ RSpec.describe Gitlab::Diff::FileCollection::PaginatedMergeRequestDiff, feature_
it_behaves_like 'unfoldable diff' do
subject do
- described_class.new(merge_request.merge_request_diff,
- page,
- per_page)
+ described_class.new(merge_request.merge_request_diff, page, per_page)
end
end
@@ -106,9 +100,7 @@ RSpec.describe Gitlab::Diff::FileCollection::PaginatedMergeRequestDiff, feature_
let(:diffable) { merge_request.merge_request_diff }
subject do
- described_class.new(merge_request.merge_request_diff,
- page,
- per_page)
+ described_class.new(merge_request.merge_request_diff, page, per_page)
end
end
end
diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb
index ad2524e40c5..bc4fc49b1b7 100644
--- a/spec/lib/gitlab/diff/file_spec.rb
+++ b/spec/lib/gitlab/diff/file_spec.rb
@@ -407,10 +407,7 @@ RSpec.describe Gitlab::Diff::File do
context 'diff file stats' do
let(:diff_file) do
- described_class.new(diff,
- diff_refs: commit.diff_refs,
- repository: project.repository,
- stats: stats)
+ described_class.new(diff, diff_refs: commit.diff_refs, repository: project.repository, stats: stats)
end
let(:raw_diff) do
diff --git a/spec/lib/gitlab/diff/formatters/file_formatter_spec.rb b/spec/lib/gitlab/diff/formatters/file_formatter_spec.rb
index 32e5f17f7eb..fc77ed5d763 100644
--- a/spec/lib/gitlab/diff/formatters/file_formatter_spec.rb
+++ b/spec/lib/gitlab/diff/formatters/file_formatter_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe Gitlab::Diff::Formatters::FileFormatter, feature_category: :code_
let(:attrs) { base_attrs.merge(old_path: 'path.rb', new_path: 'path.rb') }
it_behaves_like 'position formatter' do
- # rubocop:disable Fips/SHA1 (This is used to match the existing class method)
+ # rubocop:disable Fips/SHA1 -- This is used to match the existing class method
let(:key) do
[123, 456, 789,
Digest::SHA1.hexdigest(formatter.old_path), Digest::SHA1.hexdigest(formatter.new_path),
diff --git a/spec/lib/gitlab/diff/highlight_cache_spec.rb b/spec/lib/gitlab/diff/highlight_cache_spec.rb
index c51eaa4fa18..94a5d30283c 100644
--- a/spec/lib/gitlab/diff/highlight_cache_spec.rb
+++ b/spec/lib/gitlab/diff/highlight_cache_spec.rb
@@ -49,10 +49,12 @@ RSpec.describe Gitlab::Diff::HighlightCache, :clean_gitlab_redis_cache, feature_
let(:diff_file) do
diffs = merge_request.diffs
raw_diff = diffs.diffable.raw_diffs(diffs.diff_options.merge(paths: ['CHANGELOG'])).first
- Gitlab::Diff::File.new(raw_diff,
- repository: diffs.project.repository,
- diff_refs: diffs.diff_refs,
- fallback_diff_refs: diffs.fallback_diff_refs)
+ Gitlab::Diff::File.new(
+ raw_diff,
+ repository: diffs.project.repository,
+ diff_refs: diffs.diff_refs,
+ fallback_diff_refs: diffs.fallback_diff_refs
+ )
end
before do
@@ -227,10 +229,12 @@ RSpec.describe Gitlab::Diff::HighlightCache, :clean_gitlab_redis_cache, feature_
let(:diff_file) do
diffs = merge_request.diffs
raw_diff = diffs.diffable.raw_diffs(diffs.diff_options.merge(paths: ['CHANGELOG'])).first
- Gitlab::Diff::File.new(raw_diff,
- repository: diffs.project.repository,
- diff_refs: diffs.diff_refs,
- fallback_diff_refs: diffs.fallback_diff_refs)
+ Gitlab::Diff::File.new(
+ raw_diff,
+ repository: diffs.project.repository,
+ diff_refs: diffs.diff_refs,
+ fallback_diff_refs: diffs.fallback_diff_refs
+ )
end
it "uses ActiveSupport::Gzip when reading from the cache" do
diff --git a/spec/lib/gitlab/diff/highlight_spec.rb b/spec/lib/gitlab/diff/highlight_spec.rb
index e39c15c8fd7..e65f5a618a5 100644
--- a/spec/lib/gitlab/diff/highlight_spec.rb
+++ b/spec/lib/gitlab/diff/highlight_spec.rb
@@ -44,13 +44,13 @@ RSpec.describe Gitlab::Diff::Highlight, feature_category: :source_code_managemen
end
it 'highlights and marks removed lines' do
- code = %{-<span id="LC9" class="line" lang="ruby"> <span class="k">raise</span> <span class="s2">"System commands must be given as an array of strings"</span></span>\n}
+ code = %(-<span id="LC9" class="line" lang="ruby"> <span class="k">raise</span> <span class="s2">"System commands must be given as an array of strings"</span></span>\n)
expect(subject[4].rich_text).to eq(code)
end
it 'highlights and marks added lines' do
- code = %{+<span id="LC9" class="line" lang="ruby"> <span class="k">raise</span> <span class="no"><span class="idiff left addition">RuntimeError</span></span><span class="p"><span class="idiff addition">,</span></span><span class="idiff right addition"> </span><span class="s2">"System commands must be given as an array of strings"</span></span>\n}
+ code = %(+<span id="LC9" class="line" lang="ruby"> <span class="k">raise</span> <span class="no"><span class="idiff left addition">RuntimeError</span></span><span class="p"><span class="idiff addition">,</span></span><span class="idiff right addition"> </span><span class="s2">"System commands must be given as an array of strings"</span></span>\n)
expect(subject[5].rich_text).to eq(code)
end
@@ -86,14 +86,14 @@ RSpec.describe Gitlab::Diff::Highlight, feature_category: :source_code_managemen
end
it 'marks removed lines' do
- code = %q{- raise "System commands must be given as an array of strings"}
+ code = %q(- raise "System commands must be given as an array of strings")
expect(subject[4].text).to eq(code)
expect(subject[4].text).not_to be_html_safe
end
it 'marks added lines' do
- code = %q{+ raise <span class="idiff left right addition">RuntimeError, </span>&quot;System commands must be given as an array of strings&quot;}
+ code = %q(+ raise <span class="idiff left right addition">RuntimeError, </span>&quot;System commands must be given as an array of strings&quot;)
expect(subject[5].rich_text).to eq(code)
expect(subject[5].rich_text).to be_html_safe
@@ -107,7 +107,7 @@ RSpec.describe Gitlab::Diff::Highlight, feature_category: :source_code_managemen
it 'keeps the original rich line' do
allow(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception)
- code = %q{+ raise RuntimeError, "System commands must be given as an array of strings"}
+ code = %q(+ raise RuntimeError, "System commands must be given as an array of strings")
expect(subject[5].text).to eq(code)
expect(subject[5].text).not_to be_html_safe
diff --git a/spec/lib/gitlab/diff/inline_diff_marker_spec.rb b/spec/lib/gitlab/diff/inline_diff_marker_spec.rb
index 8ab2a7b64dd..602f1b1c2b2 100644
--- a/spec/lib/gitlab/diff/inline_diff_marker_spec.rb
+++ b/spec/lib/gitlab/diff/inline_diff_marker_spec.rb
@@ -10,10 +10,10 @@ RSpec.describe Gitlab::Diff::InlineDiffMarker do
subject { described_class.new(raw, rich).mark(inline_diffs) }
context "when the rich text is html safe" do
- let(:rich) { %{<span class="abc">abc</span><span class="space"> </span><span class="def">&#39;def&#39;</span>}.html_safe }
+ let(:rich) { %(<span class="abc">abc</span><span class="space"> </span><span class="def">&#39;def&#39;</span>).html_safe }
it 'marks the range' do
- expect(subject).to eq(%{<span class="abc">ab<span class="idiff left">c</span></span><span class="space"><span class="idiff"> </span></span><span class="def"><span class="idiff right">&#39;d</span>ef&#39;</span>})
+ expect(subject).to eq(%(<span class="abc">ab<span class="idiff left">c</span></span><span class="space"><span class="idiff"> </span></span><span class="def"><span class="idiff right">&#39;d</span>ef&#39;</span>))
expect(subject).to be_html_safe
end
end
@@ -22,7 +22,7 @@ RSpec.describe Gitlab::Diff::InlineDiffMarker do
let(:rich) { "abc 'def' differs" }
it 'marks the range' do
- expect(subject).to eq(%{ab<span class="idiff left right">c &#39;d</span>ef&#39; differs})
+ expect(subject).to eq(%(ab<span class="idiff left right">c &#39;d</span>ef&#39; differs))
expect(subject).to be_html_safe
end
end
diff --git a/spec/lib/gitlab/diff/line_spec.rb b/spec/lib/gitlab/diff/line_spec.rb
index 949def599ae..b23ba3a0f00 100644
--- a/spec/lib/gitlab/diff/line_spec.rb
+++ b/spec/lib/gitlab/diff/line_spec.rb
@@ -11,10 +11,16 @@ RSpec.describe Gitlab::Diff::Line do
end
let(:line) do
- described_class.new('<input>', 'match', 0, 0, 1,
- parent_file: double(:file),
- line_code: double(:line_code),
- rich_text: rich_text)
+ described_class.new(
+ '<input>',
+ 'match',
+ 0,
+ 0,
+ 1,
+ parent_file: double(:file),
+ line_code: double(:line_code),
+ rich_text: rich_text
+ )
end
let(:rich_text) { nil }
diff --git a/spec/lib/gitlab/diff/suggestion_diff_spec.rb b/spec/lib/gitlab/diff/suggestion_diff_spec.rb
index 9546c581112..f9d56662753 100644
--- a/spec/lib/gitlab/diff/suggestion_diff_spec.rb
+++ b/spec/lib/gitlab/diff/suggestion_diff_spec.rb
@@ -22,9 +22,7 @@ RSpec.describe Gitlab::Diff::SuggestionDiff do
end
let(:suggestion) do
- instance_double(Suggestion, from_line: 12,
- from_content: from_content,
- to_content: to_content)
+ instance_double(Suggestion, from_line: 12, from_content: from_content, to_content: to_content)
end
subject { described_class.new(suggestion).diff_lines }
@@ -56,9 +54,12 @@ RSpec.describe Gitlab::Diff::SuggestionDiff do
it 'returns a correct value if there is no newline at the end of the file' do
from_content = "One line test"
to_content = "Successful test!"
- suggestion = instance_double(Suggestion, from_line: 1,
- from_content: from_content,
- to_content: to_content)
+ suggestion = instance_double(
+ Suggestion,
+ from_line: 1,
+ from_content: from_content,
+ to_content: to_content
+ )
diff_lines = described_class.new(suggestion).diff_lines
diff --git a/spec/lib/gitlab/diff/suggestion_spec.rb b/spec/lib/gitlab/diff/suggestion_spec.rb
index 40779faf917..9f654c44852 100644
--- a/spec/lib/gitlab/diff/suggestion_spec.rb
+++ b/spec/lib/gitlab/diff/suggestion_spec.rb
@@ -5,10 +5,12 @@ require 'spec_helper'
RSpec.describe Gitlab::Diff::Suggestion do
shared_examples 'correct suggestion raw content' do
it 'returns correct raw data' do
- expect(suggestion.to_hash).to include(from_content: expected_lines.join,
- to_content: "#{text}\n",
- lines_above: above,
- lines_below: below)
+ expect(suggestion.to_hash).to include(
+ from_content: expected_lines.join,
+ to_content: "#{text}\n",
+ lines_above: above,
+ lines_below: below
+ )
end
it 'returns diff lines with correct line numbers' do
@@ -25,11 +27,13 @@ RSpec.describe Gitlab::Diff::Suggestion do
let(:merge_request) { create(:merge_request) }
let(:project) { merge_request.project }
let(:position) do
- Gitlab::Diff::Position.new(old_path: "files/ruby/popen.rb",
- new_path: "files/ruby/popen.rb",
- old_line: nil,
- new_line: 9,
- diff_refs: merge_request.diff_refs)
+ Gitlab::Diff::Position.new(
+ old_path: "files/ruby/popen.rb",
+ new_path: "files/ruby/popen.rb",
+ old_line: nil,
+ new_line: 9,
+ diff_refs: merge_request.diff_refs
+ )
end
let(:diff_file) do
diff --git a/spec/lib/gitlab/diff/suggestions_parser_spec.rb b/spec/lib/gitlab/diff/suggestions_parser_spec.rb
index a00c55d4fb2..ef845dbdc4c 100644
--- a/spec/lib/gitlab/diff/suggestions_parser_spec.rb
+++ b/spec/lib/gitlab/diff/suggestions_parser_spec.rb
@@ -7,11 +7,13 @@ RSpec.describe Gitlab::Diff::SuggestionsParser do
let(:merge_request) { create(:merge_request) }
let(:project) { merge_request.project }
let(:position) do
- Gitlab::Diff::Position.new(old_path: "files/ruby/popen.rb",
- new_path: "files/ruby/popen.rb",
- old_line: nil,
- new_line: 9,
- diff_refs: merge_request.diff_refs)
+ Gitlab::Diff::Position.new(
+ old_path: "files/ruby/popen.rb",
+ new_path: "files/ruby/popen.rb",
+ old_line: nil,
+ new_line: 9,
+ diff_refs: merge_request.diff_refs
+ )
end
let(:diff_file) do
@@ -19,8 +21,7 @@ RSpec.describe Gitlab::Diff::SuggestionsParser do
end
subject do
- described_class.parse(markdown, project: merge_request.project,
- position: position)
+ described_class.parse(markdown, project: merge_request.project, position: position)
end
def blob_lines_data(from_line, to_line)
@@ -59,15 +60,19 @@ RSpec.describe Gitlab::Diff::SuggestionsParser do
from_line = position.new_line
to_line = position.new_line
- expect(subject.first.to_hash).to include(from_content: blob_lines_data(from_line, to_line),
- to_content: " foo\n bar\n",
- lines_above: 0,
- lines_below: 0)
-
- expect(subject.second.to_hash).to include(from_content: blob_lines_data(from_line, to_line),
- to_content: " xpto\n baz\n",
- lines_above: 0,
- lines_below: 0)
+ expect(subject.first.to_hash).to include(
+ from_content: blob_lines_data(from_line, to_line),
+ to_content: " foo\n bar\n",
+ lines_above: 0,
+ lines_below: 0
+ )
+
+ expect(subject.second.to_hash).to include(
+ from_content: blob_lines_data(from_line, to_line),
+ to_content: " xpto\n baz\n",
+ lines_above: 0,
+ lines_below: 0
+ )
end
end
@@ -105,30 +110,36 @@ RSpec.describe Gitlab::Diff::SuggestionsParser do
from_line = position.new_line - 2
to_line = position.new_line + 1
- expect(subject.first.to_hash).to include(from_content: blob_lines_data(from_line, to_line),
- to_content: " # above and below\n",
- lines_above: 2,
- lines_below: 1)
+ expect(subject.first.to_hash).to include(
+ from_content: blob_lines_data(from_line, to_line),
+ to_content: " # above and below\n",
+ lines_above: 2,
+ lines_below: 1
+ )
end
it 'suggestion with above param has correct data' do
from_line = position.new_line - 3
to_line = position.new_line
- expect(subject.second.to_hash).to eq(from_content: blob_lines_data(from_line, to_line),
- to_content: " # only above\n",
- lines_above: 3,
- lines_below: 0)
+ expect(subject.second.to_hash).to eq(
+ from_content: blob_lines_data(from_line, to_line),
+ to_content: " # only above\n",
+ lines_above: 3,
+ lines_below: 0
+ )
end
it 'suggestion with below param has correct data' do
from_line = position.new_line
to_line = position.new_line + 3
- expect(subject.third.to_hash).to eq(from_content: blob_lines_data(from_line, to_line),
- to_content: " # only below\n",
- lines_above: 0,
- lines_below: 3)
+ expect(subject.third.to_hash).to eq(
+ from_content: blob_lines_data(from_line, to_line),
+ to_content: " # only below\n",
+ lines_above: 0,
+ lines_below: 3
+ )
end
end
end
diff --git a/spec/lib/gitlab/discussions_diff/file_collection_spec.rb b/spec/lib/gitlab/discussions_diff/file_collection_spec.rb
index f85a68ada15..a1d9a861355 100644
--- a/spec/lib/gitlab/discussions_diff/file_collection_spec.rb
+++ b/spec/lib/gitlab/discussions_diff/file_collection_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::DiscussionsDiff::FileCollection do
+RSpec.describe Gitlab::DiscussionsDiff::FileCollection, :clean_gitlab_redis_shared_state do
let(:merge_request) { create(:merge_request) }
let!(:diff_note_a) { create(:diff_note_on_merge_request, project: merge_request.project, noteable: merge_request) }
let!(:diff_note_b) { create(:diff_note_on_merge_request, project: merge_request.project, noteable: merge_request) }
@@ -11,7 +11,18 @@ RSpec.describe Gitlab::DiscussionsDiff::FileCollection do
subject { described_class.new([note_diff_file_a, note_diff_file_b]) }
- describe '#load_highlight', :clean_gitlab_redis_shared_state do
+ describe '#load_highlight' do
+ it 'only takes into account for the specific diff note ids' do
+ file_a_caching_content = diff_note_a.diff_file.highlighted_diff_lines.map(&:to_hash)
+
+ expect(Gitlab::DiscussionsDiff::HighlightCache)
+ .to receive(:write_multiple)
+ .with({ note_diff_file_a.id => file_a_caching_content })
+ .and_call_original
+
+ subject.load_highlight(diff_note_ids: [note_diff_file_a.diff_note_id])
+ end
+
it 'writes uncached diffs highlight' do
file_a_caching_content = diff_note_a.diff_file.highlighted_diff_lines.map(&:to_hash)
file_b_caching_content = diff_note_b.diff_file.highlighted_diff_lines.map(&:to_hash)
diff --git a/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb b/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb
index e6fff939632..f13fd0be4cd 100644
--- a/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb
+++ b/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb
@@ -8,12 +8,14 @@ RSpec.describe Gitlab::Email::Handler::ServiceDeskHandler, feature_category: :se
before do
stub_incoming_email_setting(enabled: true, address: "incoming+%{key}@appmail.adventuretime.ooo")
+ stub_service_desk_email_setting(enabled: true, address: "contact+%{key}@example.com")
stub_config_setting(host: 'localhost')
end
let(:email_raw) { email_fixture('emails/service_desk.eml') }
let(:author_email) { 'jake@adventuretime.ooo' }
let(:message_id) { 'CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com' }
+ let(:issue_email_participants_count) { 1 }
let_it_be(:group) { create(:group, :private, :crm_enabled, name: "email") }
@@ -22,7 +24,7 @@ RSpec.describe Gitlab::Email::Handler::ServiceDeskHandler, feature_category: :se
end
context 'service desk is enabled for the project' do
- let_it_be(:project) { create(:project, :repository, :private, group: group, path: 'test', service_desk_enabled: true) }
+ let_it_be_with_reload(:project) { create(:project, :repository, :private, group: group, path: 'test', service_desk_enabled: true) }
before do
allow(Gitlab::ServiceDesk).to receive(:supported?).and_return(true)
@@ -50,6 +52,7 @@ RSpec.describe Gitlab::Email::Handler::ServiceDeskHandler, feature_category: :se
receiver.execute
new_issue = Issue.last
+ expect(new_issue.issue_email_participants.count).to eq(issue_email_participants_count)
expect(new_issue.issue_email_participants.first.email).to eq(author_email)
end
@@ -96,6 +99,72 @@ RSpec.describe Gitlab::Email::Handler::ServiceDeskHandler, feature_category: :se
expect(new_issue.issue_customer_relations_contacts.map(&:contact)).to contain_exactly(contact, contact2, contact3)
end
+ context 'when add_external_participants_from_cc is true' do
+ shared_examples 'does not add CC address' do
+ it 'creates a new issue and adds issue_email_participant from From header' do
+ expect { receiver.execute }.to change { Issue.count }.by(1)
+ expect(Issue.last.issue_email_participants.map(&:email)).to match_array(%w[from@example.com])
+ end
+ end
+
+ let_it_be(:setting) { create(:service_desk_setting, project: project, add_external_participants_from_cc: true) }
+
+ # Original email contains two CC email addresses
+ let(:issue_email_participants_count) { 3 }
+ let(:to_address) { project.service_desk_incoming_address }
+
+ it_behaves_like 'a new issue request'
+
+ context 'when no CC header is present' do
+ let(:email_raw) do
+ <<~EMAIL
+ From: from@example.com
+ To: #{to_address}
+ Subject: Issue title
+
+ Issue description
+ EMAIL
+ end
+
+ it_behaves_like 'does not add CC address'
+ end
+
+ context 'when service desk system address is in CC' do
+ let(:cc_address) { project.service_desk_incoming_address }
+ let(:email_raw) do
+ <<~EMAIL
+ From: from@example.com
+ To: #{to_address}
+ Cc: #{cc_address}
+ Subject: Issue title
+
+ Issue description
+ EMAIL
+ end
+
+ it_behaves_like 'does not add CC address'
+
+ context 'when service_desk_email is part of CC' do
+ let(:cc_address) { project.service_desk_alias_address }
+
+ it_behaves_like 'does not add CC address'
+ end
+
+ context 'when custom email is part of CC' do
+ let!(:credential) { create(:service_desk_custom_email_credential, project: project) }
+ let!(:verification) { create(:service_desk_custom_email_verification, :finished, project: project) }
+ let(:cc_address) { project.service_desk_custom_address }
+
+ before do
+ project.reset
+ setting.update!(custom_email: 'support@example.com', custom_email_enabled: true)
+ end
+
+ it_behaves_like 'does not add CC address'
+ end
+ end
+ end
+
context 'with legacy incoming email address' do
let(:email_raw) { fixture_file('emails/service_desk_legacy.eml') }
@@ -140,25 +209,26 @@ RSpec.describe Gitlab::Email::Handler::ServiceDeskHandler, feature_category: :se
subject
end
- context 'when issue_email_participants FF is enabled' do
- it 'creates 2 issue_email_participants' do
- subject
+ it 'creates issue_email_participants for author and reply author' do
+ subject
- expect(Issue.last.issue_email_participants.map(&:email))
- .to match_array(%w(alan@adventuretime.ooo jake@adventuretime.ooo))
- end
+ # 1 from issue creation
+ # 1 from new note reply
+ expect(Issue.last.issue_email_participants.map(&:email))
+ .to match_array(%w[alan@adventuretime.ooo jake@adventuretime.ooo])
end
context 'when issue_email_participants FF is disabled' do
before do
+ # Was turned off after issue creation
stub_feature_flags(issue_email_participants: false)
end
- it 'creates only 1 issue_email_participant' do
+ it 'creates issue_email_participant for the author' do
subject
expect(Issue.last.issue_email_participants.map(&:email))
- .to match_array(%w(jake@adventuretime.ooo))
+ .to match_array(%w[jake@adventuretime.ooo])
end
end
end
@@ -182,11 +252,11 @@ RSpec.describe Gitlab::Email::Handler::ServiceDeskHandler, feature_category: :se
subject
end
- it 'creates 1 issue_email_participant' do
+ it 'creates issue_email_participant for the author' do
subject
expect(Issue.last.issue_email_participants.map(&:email))
- .to match_array(%w(alan@adventuretime.ooo))
+ .to match_array(%w[alan@adventuretime.ooo])
end
end
end
diff --git a/spec/lib/gitlab/email/handler_spec.rb b/spec/lib/gitlab/email/handler_spec.rb
index d3a4d77c58e..47907401a63 100644
--- a/spec/lib/gitlab/email/handler_spec.rb
+++ b/spec/lib/gitlab/email/handler_spec.rb
@@ -60,10 +60,10 @@ RSpec.describe Gitlab::Email::Handler do
describe 'regexps are set properly' do
let(:addresses) do
- %W(sent_notification_key#{Gitlab::Email::Common::UNSUBSCRIBE_SUFFIX} sent_notification_key#{Gitlab::Email::Common::UNSUBSCRIBE_SUFFIX_LEGACY}) +
- %w(sent_notification_key path-to-project-123-user_email_token-merge-request) +
- %w(path-to-project-123-user_email_token-issue path-to-project-123-user_email_token-issue-123) +
- %w(path/to/project+user_email_token path/to/project+merge-request+user_email_token some/project)
+ %W[sent_notification_key#{Gitlab::Email::Common::UNSUBSCRIBE_SUFFIX} sent_notification_key#{Gitlab::Email::Common::UNSUBSCRIBE_SUFFIX_LEGACY}] +
+ %w[sent_notification_key path-to-project-123-user_email_token-merge-request] +
+ %w[path-to-project-123-user_email_token-issue path-to-project-123-user_email_token-issue-123] +
+ %w[path/to/project+user_email_token path/to/project+merge-request+user_email_token some/project]
end
before do
diff --git a/spec/lib/gitlab/email/receiver_spec.rb b/spec/lib/gitlab/email/receiver_spec.rb
index f8084d24850..c86a83092a4 100644
--- a/spec/lib/gitlab/email/receiver_spec.rb
+++ b/spec/lib/gitlab/email/receiver_spec.rb
@@ -33,7 +33,7 @@ RSpec.describe Gitlab::Email::Receiver do
metadata = receiver.mail_metadata
- expect(metadata.keys).to match_array(%i(mail_uid from_address to_address mail_key references delivered_to envelope_to x_envelope_to meta received_recipients cc_address))
+ expect(metadata.keys).to match_array(%i[mail_uid from_address to_address mail_key references delivered_to envelope_to x_envelope_to meta received_recipients cc_address])
expect(metadata[:meta]).to include(client_id: client_id, project: project.full_path)
expect(metadata[meta_key]).to eq(meta_value)
end
diff --git a/spec/lib/gitlab/encoding_helper_spec.rb b/spec/lib/gitlab/encoding_helper_spec.rb
index 1b7c11dfef6..db7961fc0c9 100644
--- a/spec/lib/gitlab/encoding_helper_spec.rb
+++ b/spec/lib/gitlab/encoding_helper_spec.rb
@@ -214,7 +214,7 @@ RSpec.describe Gitlab::EncodingHelper, feature_category: :shared do
[nil, ""],
["", ""],
[" ", " "],
- %w(a1 a1),
+ %w[a1 a1],
["编码", "\xE7\xBC\x96\xE7\xA0\x81".b]
].each do |input, result|
it "encodes #{input.inspect} to #{result.inspect}" do
diff --git a/spec/lib/gitlab/endpoint_attributes_spec.rb b/spec/lib/gitlab/endpoint_attributes_spec.rb
index a623070c3eb..34f4221b86a 100644
--- a/spec/lib/gitlab/endpoint_attributes_spec.rb
+++ b/spec/lib/gitlab/endpoint_attributes_spec.rb
@@ -11,19 +11,19 @@ RSpec.describe Gitlab::EndpointAttributes, feature_category: :api do
let(:controller) do
Class.new(base_controller) do
- feature_category :foo, %w(update edit)
- feature_category :bar, %w(index show)
- feature_category :quux, %w(destroy)
+ feature_category :foo, %w[update edit]
+ feature_category :bar, %w[index show]
+ feature_category :quux, %w[destroy]
- urgency :high, %w(do_a)
- urgency :low, %w(do_b do_c)
+ urgency :high, %w[do_a]
+ urgency :low, %w[do_b do_c]
end
end
let(:subclass) do
Class.new(controller) do
- feature_category :baz, %w(subclass_index)
- urgency :high, %w(superclass_do_something)
+ feature_category :baz, %w[subclass_index]
+ urgency :high, %w[superclass_do_something]
end
end
diff --git a/spec/lib/gitlab/error_tracking/stack_trace_highlight_decorator_spec.rb b/spec/lib/gitlab/error_tracking/stack_trace_highlight_decorator_spec.rb
index a854adca32b..eae6186e789 100644
--- a/spec/lib/gitlab/error_tracking/stack_trace_highlight_decorator_spec.rb
+++ b/spec/lib/gitlab/error_tracking/stack_trace_highlight_decorator_spec.rb
@@ -28,7 +28,7 @@ RSpec.describe Gitlab::ErrorTracking::StackTraceHighlightDecorator do
[11, '<span id="LC1" class="line" lang="ruby"><span class="k">class</span> <span class="nc">HelloWorld</span></span>'],
[12, '<span id="LC1" class="line" lang="ruby"> <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">message</span></span>'],
[13, '<span id="LC1" class="line" lang="ruby"> <span class="vi">@name</span> <span class="o">=</span> <span class="s1">\'World\'</span></span>'],
- [14, %[<span id="LC1" class="line" lang="ruby"> <span class="nb">puts</span> <span class="s2">"Hello </span><span class="si">\#{</span><span class="vi">@name</span><span class="si">}</span><span class="s2">"</span></span>]],
+ [14, %(<span id="LC1" class="line" lang="ruby"> <span class="nb">puts</span> <span class="s2">"Hello </span><span class="si">\#{</span><span class="vi">@name</span><span class="si">}</span><span class="s2">"</span></span>)],
[15, '<span id="LC1" class="line" lang="ruby"> <span class="k">end</span></span>'],
[16, '<span id="LC1" class="line" lang="ruby"><span class="k">end</span></span>']
]
diff --git a/spec/lib/gitlab/external_authorization/client_spec.rb b/spec/lib/gitlab/external_authorization/client_spec.rb
index b907b0bb262..b507fe7bde8 100644
--- a/spec/lib/gitlab/external_authorization/client_spec.rb
+++ b/spec/lib/gitlab/external_authorization/client_spec.rb
@@ -109,7 +109,7 @@ RSpec.describe Gitlab::ExternalAuthorization::Client do
describe 'for non-ldap users with identities' do
before do
- %w(twitter facebook).each do |provider|
+ %w[twitter facebook].each do |provider|
create(:identity, provider: provider, extern_uid: "#{provider}_external_id", user: user)
end
end
diff --git a/spec/lib/gitlab/favicon_spec.rb b/spec/lib/gitlab/favicon_spec.rb
index 033fa5d1b42..62071293764 100644
--- a/spec/lib/gitlab/favicon_spec.rb
+++ b/spec/lib/gitlab/favicon_spec.rb
@@ -60,7 +60,7 @@ RSpec.describe Gitlab::Favicon, :request_store do
subject { described_class.available_status_names }
it 'returns the available status names' do
- expect(subject).to eq %w(
+ expect(subject).to eq %w[
favicon_status_canceled
favicon_status_created
favicon_status_failed
@@ -73,7 +73,7 @@ RSpec.describe Gitlab::Favicon, :request_store do
favicon_status_skipped
favicon_status_success
favicon_status_warning
- )
+ ]
end
end
end
diff --git a/spec/lib/gitlab/feature_categories_spec.rb b/spec/lib/gitlab/feature_categories_spec.rb
index a35166a4499..11ddd08c968 100644
--- a/spec/lib/gitlab/feature_categories_spec.rb
+++ b/spec/lib/gitlab/feature_categories_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Gitlab::FeatureCategories do
- let(:fake_categories) { %w(foo bar) }
+ let(:fake_categories) { %w[foo bar] }
subject(:feature_categories) { described_class.new(fake_categories) }
diff --git a/spec/lib/gitlab/file_detector_spec.rb b/spec/lib/gitlab/file_detector_spec.rb
index 208acf28cc4..55bb1804d86 100644
--- a/spec/lib/gitlab/file_detector_spec.rb
+++ b/spec/lib/gitlab/file_detector_spec.rb
@@ -1,17 +1,17 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'spec_helper'
RSpec.describe Gitlab::FileDetector do
describe '.types_in_paths' do
it 'returns the file types for the given paths' do
- expect(described_class.types_in_paths(%w(README.md CHANGELOG VERSION VERSION)))
- .to eq(%i{readme changelog version})
+ expect(described_class.types_in_paths(%w[README.md CHANGELOG VERSION VERSION]))
+ .to eq(%i[readme changelog version])
end
it 'does not include unrecognized file paths' do
- expect(described_class.types_in_paths(%w(README.md foo.txt)))
- .to eq(%i{readme})
+ expect(described_class.types_in_paths(%w[README.md foo.txt]))
+ .to eq(%i[readme])
end
end
@@ -25,7 +25,7 @@ RSpec.describe Gitlab::FileDetector do
extensions = ['txt', *Gitlab::MarkupHelper::EXTENSIONS]
extensions.each do |ext|
- %w(index readme).each do |file|
+ %w[index readme].each do |file|
expect(described_class.type_of("#{file}.#{ext}")).to eq(:readme)
end
end
@@ -45,13 +45,13 @@ RSpec.describe Gitlab::FileDetector do
end
it 'returns the type of a changelog file' do
- %w(CHANGELOG HISTORY CHANGES NEWS).each do |file|
+ %w[CHANGELOG HISTORY CHANGES NEWS].each do |file|
expect(described_class.type_of(file)).to eq(:changelog)
end
end
it 'returns the type of a license file' do
- %w(LICENSE LICENCE COPYING UNLICENSE UNLICENCE).each do |file|
+ %w[LICENSE LICENCE COPYING UNLICENSE UNLICENCE].each do |file|
expect(described_class.type_of(file)).to eq(:license)
end
end
@@ -73,7 +73,7 @@ RSpec.describe Gitlab::FileDetector do
end
it 'returns the type of an avatar' do
- %w(logo.gif logo.png logo.jpg).each do |file|
+ %w[logo.gif logo.png logo.jpg].each do |file|
expect(described_class.type_of(file)).to eq(:avatar)
end
end
diff --git a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
index 6de7cab9c42..75427ac0402 100644
--- a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
+++ b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
@@ -78,13 +78,13 @@ RSpec.describe Gitlab::Gfm::ReferenceRewriter do
context 'label referenced by id' do
let(:text) { '#1 and ~123' }
- it { is_expected.to eq %{#{old_project_ref}#1 and #{old_project_ref}~123} }
+ it { is_expected.to eq %(#{old_project_ref}#1 and #{old_project_ref}~123) }
end
context 'label referenced by text' do
let(:text) { '#1 and ~"test"' }
- it { is_expected.to eq %{#{old_project_ref}#1 and #{old_project_ref}~123} }
+ it { is_expected.to eq %(#{old_project_ref}#1 and #{old_project_ref}~123) }
end
end
@@ -99,13 +99,13 @@ RSpec.describe Gitlab::Gfm::ReferenceRewriter do
context 'label referenced by id' do
let(:text) { '#1 and ~321' }
- it { is_expected.to eq %{#{old_project_ref}#1 and #{old_project_ref}~321} }
+ it { is_expected.to eq %(#{old_project_ref}#1 and #{old_project_ref}~321) }
end
context 'label referenced by text' do
let(:text) { '#1 and ~"group label"' }
- it { is_expected.to eq %{#{old_project_ref}#1 and #{old_project_ref}~321} }
+ it { is_expected.to eq %(#{old_project_ref}#1 and #{old_project_ref}~321) }
end
end
end
@@ -149,7 +149,7 @@ RSpec.describe Gitlab::Gfm::ReferenceRewriter do
let(:text) { 'milestone: %"9.0"' }
- it { is_expected.to eq %[milestone: #{old_project_ref}%"9.0"] }
+ it { is_expected.to eq %(milestone: #{old_project_ref}%"9.0") }
end
context 'when referring to group milestone' do
diff --git a/spec/lib/gitlab/git/blame_spec.rb b/spec/lib/gitlab/git/blame_spec.rb
index 77361b09857..751611be5d2 100644
--- a/spec/lib/gitlab/git/blame_spec.rb
+++ b/spec/lib/gitlab/git/blame_spec.rb
@@ -48,6 +48,14 @@ RSpec.describe Gitlab::Git::Blame, feature_category: :source_code_management do
end
end
+ context 'when path is missing' do
+ let(:path) { 'unknown_file' }
+
+ it 'returns an empty array' do
+ expect(result).to eq([])
+ end
+ end
+
context "ISO-8859 encoding" do
let(:path) { 'encoding/iso8859.txt' }
diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb
index 5bb4b84835d..59cf87ddc7e 100644
--- a/spec/lib/gitlab/git/blob_spec.rb
+++ b/spec/lib/gitlab/git/blob_spec.rb
@@ -154,18 +154,6 @@ RSpec.describe Gitlab::Git::Blob do
it_behaves_like '.find'
end
- describe '.find with Rugged enabled', :enable_rugged do
- it 'calls out to the Rugged implementation' do
- allow_next_instance_of(Rugged) do |instance|
- allow(instance).to receive(:rev_parse).with(TestEnv::BRANCH_SHA['master']).and_call_original
- end
-
- described_class.find(repository, TestEnv::BRANCH_SHA['master'], 'files/images/6049019_460s.jpg')
- end
-
- it_behaves_like '.find'
- end
-
describe '.raw' do
let(:raw_blob) { described_class.raw(repository, SeedRepo::RubyBlob::ID) }
let(:bad_blob) { described_class.raw(repository, SeedRepo::BigCommit::ID) }
diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb
index 5c4be1003c3..d8d62ac9670 100644
--- a/spec/lib/gitlab/git/commit_spec.rb
+++ b/spec/lib/gitlab/git/commit_spec.rb
@@ -160,18 +160,6 @@ RSpec.describe Gitlab::Git::Commit, feature_category: :source_code_management do
it_behaves_like '.find'
end
- describe '.find with Rugged enabled', :enable_rugged do
- it 'calls out to the Rugged implementation' do
- allow_next_instance_of(Rugged) do |instance|
- allow(instance).to receive(:rev_parse).with(SeedRepo::Commit::ID).and_call_original
- end
-
- described_class.find(repository, SeedRepo::Commit::ID)
- end
-
- it_behaves_like '.find'
- end
-
describe '.last_for_path' do
context 'no path' do
subject { described_class.last_for_path(repository, 'master') }
@@ -459,18 +447,6 @@ RSpec.describe Gitlab::Git::Commit, feature_category: :source_code_management do
end
end
- describe '.batch_by_oid with Rugged enabled', :enable_rugged do
- it_behaves_like '.batch_by_oid'
-
- it 'calls out to the Rugged implementation' do
- allow_next_instance_of(Rugged) do |instance|
- allow(instance).to receive(:rev_parse).with(SeedRepo::Commit::ID).and_call_original
- end
-
- described_class.batch_by_oid(repository, [SeedRepo::Commit::ID])
- end
- end
-
describe '.extract_signature_lazily' do
subject { described_class.extract_signature_lazily(repository, commit_id).itself }
diff --git a/spec/lib/gitlab/git/merge_base_spec.rb b/spec/lib/gitlab/git/merge_base_spec.rb
index fda2232c2c3..cbe47aae852 100644
--- a/spec/lib/gitlab/git/merge_base_spec.rb
+++ b/spec/lib/gitlab/git/merge_base_spec.rb
@@ -11,13 +11,13 @@ RSpec.describe Gitlab::Git::MergeBase do
shared_context 'existing refs with a merge base', :existing_refs do
let(:refs) do
- %w(304d257dcb821665ab5110318fc58a007bd104ed 0031876facac3f2b2702a0e53a26e89939a42209)
+ %w[304d257dcb821665ab5110318fc58a007bd104ed 0031876facac3f2b2702a0e53a26e89939a42209]
end
end
shared_context 'when passing a missing ref', :missing_ref do
let(:refs) do
- %w(304d257dcb821665ab5110318fc58a007bd104ed aaaa)
+ %w[304d257dcb821665ab5110318fc58a007bd104ed aaaa]
end
end
@@ -51,13 +51,13 @@ RSpec.describe Gitlab::Git::MergeBase do
end
it 'returns a merge base when passing 2 branch names' do
- merge_base = described_class.new(repository, %w(master feature))
+ merge_base = described_class.new(repository, %w[master feature])
expect(merge_base.sha).to be_present
end
it 'returns a merge base when passing a tag name' do
- merge_base = described_class.new(repository, %w(master v1.0.0))
+ merge_base = described_class.new(repository, %w[master v1.0.0])
expect(merge_base.sha).to be_present
end
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 47b5986cfd8..5791d9c524f 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -2435,7 +2435,7 @@ RSpec.describe Gitlab::Git::Repository, feature_category: :source_code_managemen
end
it 'deletes all refs except those with the specified prefixes' do
- repository.delete_all_refs_except(%w(refs/keep refs/also-keep refs/heads))
+ repository.delete_all_refs_except(%w[refs/keep refs/also-keep refs/heads])
expect(repository.ref_exists?("refs/delete/a")).to be(false)
expect(repository.ref_exists?("refs/also-delete/b")).to be(false)
expect(repository.ref_exists?("refs/keep/c")).to be(true)
@@ -2722,15 +2722,15 @@ RSpec.describe Gitlab::Git::Repository, feature_category: :source_code_managemen
describe '#check_objects_exist' do
it 'returns hash specifying which object exists in repo' do
- refs_exist = %w(
+ refs_exist = %w[
b83d6e391c22777fca1ed3012fce84f633d7fed0
498214de67004b1da3d820901307bed2a68a8ef6
1b12f15a11fc6e62177bef08f47bc7b5ce50b141
- )
- refs_dont_exist = %w(
+ ]
+ refs_dont_exist = %w[
1111111111111111111111111111111111111111
2222222222222222222222222222222222222222
- )
+ ]
object_existence_map = repository.check_objects_exist(refs_exist + refs_dont_exist)
expect(object_existence_map).to eq({
'b83d6e391c22777fca1ed3012fce84f633d7fed0' => true,
diff --git a/spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb b/spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb
deleted file mode 100644
index d5a0ab3d5e0..00000000000
--- a/spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb
+++ /dev/null
@@ -1,116 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Git::RuggedImpl::UseRugged, feature_category: :gitaly do
- let(:project) { create(:project, :repository) }
- let(:repository) { project.repository }
- let(:feature_flag_name) { wrapper.rugged_feature_keys.first }
-
- subject(:wrapper) do
- klazz = Class.new do
- include Gitlab::Git::RuggedImpl::UseRugged
-
- def rugged_test(ref, test_number); end
- end
-
- klazz.new
- end
-
- describe '#execute_rugged_call', :request_store do
- let(:args) { ['refs/heads/master', 1] }
-
- before do
- allow(Gitlab::PerformanceBar).to receive(:enabled_for_request?).and_return(true)
- end
-
- it 'instruments Rugged call' do
- expect(subject).to receive(:rugged_test).with(args)
-
- subject.execute_rugged_call(:rugged_test, args)
-
- expect(Gitlab::RuggedInstrumentation.query_count).to eq(1)
- expect(Gitlab::RuggedInstrumentation.list_call_details.count).to eq(1)
- end
- end
-
- describe '#use_rugged?' do
- it 'returns false' do
- expect(subject.use_rugged?(repository, feature_flag_name)).to be false
- end
- end
-
- describe '#running_puma_with_multiple_threads?' do
- context 'when using Puma' do
- before do
- stub_const('::Puma', double('puma constant'))
- allow(Gitlab::Runtime).to receive(:puma?).and_return(true)
- end
-
- it "returns false when Puma doesn't support the cli_config method" do
- allow(::Puma).to receive(:respond_to?).with(:cli_config).and_return(false)
-
- expect(subject.running_puma_with_multiple_threads?).to be_falsey
- end
-
- it 'returns false for single thread Puma' do
- allow(::Puma).to receive_message_chain(:cli_config, :options).and_return(max_threads: 1)
-
- expect(subject.running_puma_with_multiple_threads?).to be false
- end
-
- it 'returns true for multi-threaded Puma' do
- allow(::Puma).to receive_message_chain(:cli_config, :options).and_return(max_threads: 2)
-
- expect(subject.running_puma_with_multiple_threads?).to be true
- end
- end
-
- context 'when not using Puma' do
- before do
- allow(Gitlab::Runtime).to receive(:puma?).and_return(false)
- end
-
- it 'returns false' do
- expect(subject.running_puma_with_multiple_threads?).to be false
- end
- end
- end
-
- describe '#rugged_enabled_through_feature_flag?' do
- subject { wrapper.send(:rugged_enabled_through_feature_flag?) }
-
- before do
- allow(Feature).to receive(:enabled?).with(:feature_key_1).and_return(true)
- allow(Feature).to receive(:enabled?).with(:feature_key_2).and_return(true)
- allow(Feature).to receive(:enabled?).with(:feature_key_3).and_return(false)
- allow(Feature).to receive(:enabled?).with(:feature_key_4).and_return(false)
-
- stub_const('Gitlab::Git::RuggedImpl::Repository::FEATURE_FLAGS', feature_keys)
- end
-
- context 'no feature keys given' do
- let(:feature_keys) { [] }
-
- it { is_expected.to be_falsey }
- end
-
- context 'all features are enabled' do
- let(:feature_keys) { [:feature_key_1, :feature_key_2] }
-
- it { is_expected.to be_falsey }
- end
-
- context 'all features are not enabled' do
- let(:feature_keys) { [:feature_key_3, :feature_key_4] }
-
- it { is_expected.to be_falsey }
- end
-
- context 'some feature is enabled' do
- let(:feature_keys) { [:feature_key_4, :feature_key_2] }
-
- it { is_expected.to be_falsey }
- end
- end
-end
diff --git a/spec/lib/gitlab/git/tree_spec.rb b/spec/lib/gitlab/git/tree_spec.rb
index 9675e48a77f..090f9af2620 100644
--- a/spec/lib/gitlab/git/tree_spec.rb
+++ b/spec/lib/gitlab/git/tree_spec.rb
@@ -192,122 +192,4 @@ RSpec.describe Gitlab::Git::Tree, feature_category: :source_code_management do
end
end
end
-
- describe '.where with Rugged enabled', :enable_rugged do
- it 'does not call to the Rugged implementation' do
- allow_next_instance_of(Rugged) do |instance|
- allow(instance).not_to receive(:lookup)
- end
-
- described_class.where(repository, SeedRepo::Commit::ID, 'files', false, false)
- end
-
- it_behaves_like 'repo' do
- describe 'Pagination' do
- context 'with restrictive limit' do
- let(:pagination_params) { { limit: 3, page_token: nil } }
-
- it 'returns limited paginated list of tree objects' do
- expect(entries.count).to eq(3)
- expect(cursor.next_cursor).to be_present
- end
- end
-
- context 'when limit is equal to number of entries' do
- let(:entries_count) { entries.count }
-
- it 'returns all entries with a cursor' do
- result, cursor = Gitlab::Git::Tree.where(repository, sha, path, recursive, skip_flat_paths, rescue_not_found, { limit: entries_count, page_token: nil })
-
- expect(cursor).to eq(Gitaly::PaginationCursor.new)
- expect(result.entries.count).to eq(entries_count)
- end
- end
-
- context 'when limit is 0' do
- let(:pagination_params) { { limit: 0, page_token: nil } }
-
- it 'returns empty result' do
- expect(entries).to eq([])
- expect(cursor).to be_nil
- end
- end
-
- context 'when limit is missing' do
- let(:pagination_params) { { limit: nil, page_token: nil } }
-
- it 'returns all entries' do
- expect(entries.count).to be < 20
- expect(cursor).to eq(Gitaly::PaginationCursor.new)
- end
- end
-
- context 'when limit is negative' do
- let(:entries_count) { entries.count }
-
- it 'returns all entries' do
- result, cursor = Gitlab::Git::Tree.where(repository, sha, path, recursive, skip_flat_paths, rescue_not_found, { limit: -1, page_token: nil })
-
- expect(result.count).to eq(entries_count)
- expect(cursor).to eq(Gitaly::PaginationCursor.new)
- end
-
- context 'when token is provided' do
- let(:pagination_params) { { limit: 1000, page_token: nil } }
- let(:token) { entries.second.id }
-
- it 'returns all entries after token' do
- result, cursor = Gitlab::Git::Tree.where(repository, sha, path, recursive, skip_flat_paths, rescue_not_found, { limit: -1, page_token: token })
-
- expect(result.count).to eq(entries.count - 2)
- expect(cursor).to eq(Gitaly::PaginationCursor.new)
- end
- end
- end
-
- context 'when token does not exist' do
- let(:pagination_params) { { limit: 5, page_token: 'aabbccdd' } }
-
- it 'raises a command error' do
- expect { entries }.to raise_error(Gitlab::Git::CommandError, /could not find starting OID: aabbccdd/)
- end
- end
-
- context 'when limit is bigger than number of entries' do
- let(:pagination_params) { { limit: 1000, page_token: nil } }
-
- it 'returns only available entries' do
- expect(entries.count).to be < 20
- expect(cursor).to eq(Gitaly::PaginationCursor.new)
- end
- end
-
- it 'returns all tree entries in specific order during cursor pagination' do
- collected_entries = []
- token = nil
-
- expected_entries = entries
-
- loop do
- result, cursor = Gitlab::Git::Tree.where(repository, sha, path, recursive, skip_flat_paths, rescue_not_found, { limit: 5, page_token: token })
-
- collected_entries += result.entries
- token = cursor&.next_cursor
-
- break if token.blank?
- end
-
- expect(collected_entries.map(&:path)).to match_array(expected_entries.map(&:path))
-
- expected_order = [
- collected_entries.select(&:dir?).map(&:path),
- collected_entries.select(&:file?).map(&:path),
- collected_entries.select(&:submodule?).map(&:path)
- ].flatten
-
- expect(collected_entries.map(&:path)).to eq(expected_order)
- end
- end
- end
- end
end
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index 1b205aa5c85..975e8bdd3ac 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -957,7 +957,7 @@ RSpec.describe Gitlab::GitAccess, :aggregate_failures, feature_category: :system
}
}
- [%w(feature exact), ['feat*', 'wildcard']].each do |protected_branch_name, protected_branch_type|
+ [%w[feature exact], ['feat*', 'wildcard']].each do |protected_branch_name, protected_branch_type|
context do
let(:who_can_action) { :maintainers_can_push }
let(:protected_branch) { create(:protected_branch, who_can_action, name: protected_branch_name, project: project) }
diff --git a/spec/lib/gitlab/git_audit_event_spec.rb b/spec/lib/gitlab/git_audit_event_spec.rb
deleted file mode 100644
index c533b39f550..00000000000
--- a/spec/lib/gitlab/git_audit_event_spec.rb
+++ /dev/null
@@ -1,79 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::GitAuditEvent, feature_category: :source_code_management do
- let_it_be(:player) { create(:user) }
- let_it_be(:group) { create(:group, :public) }
- let_it_be(:project) { create(:project) }
-
- subject { described_class.new(player, project) }
-
- describe '#send_audit_event' do
- let(:msg) { 'valid_msg' }
-
- context 'with successfully sending' do
- let_it_be(:project) { create(:project, namespace: group) }
-
- before do
- allow(::Gitlab::Audit::Auditor).to receive(:audit)
- end
-
- context 'when player is a regular user' do
- it 'sends git audit event' do
- expect(::Gitlab::Audit::Auditor).to receive(:audit).with(a_hash_including(
- name: 'repository_git_operation',
- stream_only: true,
- author: player,
- scope: project,
- target: project,
- message: msg
- )).once
-
- subject.send_audit_event(msg)
- end
- end
-
- context 'when player is ::API::Support::GitAccessActor' do
- let_it_be(:user) { player }
- let_it_be(:key) { create(:key, user: user) }
- let_it_be(:git_access_actor) { ::API::Support::GitAccessActor.new(user: user, key: key) }
-
- subject { described_class.new(git_access_actor, project) }
-
- it 'sends git audit event' do
- expect(::Gitlab::Audit::Auditor).to receive(:audit).with(a_hash_including(
- name: 'repository_git_operation',
- stream_only: true,
- author: git_access_actor.deploy_key_or_user,
- scope: project,
- target: project,
- message: msg
- )).once
-
- subject.send_audit_event(msg)
- end
- end
- end
-
- context 'when user is blank' do
- let_it_be(:player) { nil }
-
- it 'does not send git audit event' do
- expect(::Gitlab::Audit::Auditor).not_to receive(:audit)
-
- subject.send_audit_event(msg)
- end
- end
-
- context 'when project is blank' do
- let_it_be(:project) { nil }
-
- it 'does not send git audit event' do
- expect(::Gitlab::Audit::Auditor).not_to receive(:audit)
-
- subject.send_audit_event(msg)
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
index 2ee9d85c723..02c7abadd99 100644
--- a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
@@ -1041,9 +1041,11 @@ RSpec.describe Gitlab::GitalyClient::CommitService, feature_category: :gitaly do
end
describe '#raw_blame' do
- let(:project) { create(:project, :test_repo) }
+ let_it_be(:project) { create(:project, :test_repo) }
+
let(:revision) { 'blame-on-renamed' }
let(:path) { 'files/plain_text/renamed' }
+ let(:range) { nil }
let(:blame_headers) do
[
@@ -1073,6 +1075,31 @@ RSpec.describe Gitlab::GitalyClient::CommitService, feature_category: :gitaly do
is_expected.not_to include(blame_headers[0], blame_headers[1], blame_headers[4])
end
end
+
+ context 'when out of range' do
+ let(:range) { '9999,99999' }
+
+ it { expect { blame }.to raise_error(ArgumentError, 'range is outside of the file length') }
+ end
+
+ context 'when a file path is not found' do
+ let(:path) { 'unknown/path' }
+
+ it { expect { blame }.to raise_error(ArgumentError, 'path not found in revision') }
+ end
+
+ context 'when an unknown exception is raised' do
+ let(:gitaly_exception) { GRPC::BadStatus.new(GRPC::Core::StatusCodes::NOT_FOUND) }
+
+ before do
+ expect_any_instance_of(Gitaly::CommitService::Stub)
+ .to receive(:raw_blame)
+ .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
+ .and_raise(gitaly_exception)
+ end
+
+ it { expect { blame }.to raise_error(gitaly_exception) }
+ end
end
describe '#get_commit_signatures' do
diff --git a/spec/lib/gitlab/gitaly_client/conflict_files_stitcher_spec.rb b/spec/lib/gitlab/gitaly_client/conflict_files_stitcher_spec.rb
index d0787d8b673..816b59b96ae 100644
--- a/spec/lib/gitlab/gitaly_client/conflict_files_stitcher_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/conflict_files_stitcher_spec.rb
@@ -3,12 +3,12 @@
require 'spec_helper'
RSpec.describe Gitlab::GitalyClient::ConflictFilesStitcher do
+ let_it_be(:target_project) { create(:project, :repository) }
+ let_it_be(:target_repository) { target_project.repository.raw }
+ let_it_be(:target_gitaly_repository) { target_repository.gitaly_repository }
+
describe 'enumeration' do
it 'combines segregated ConflictFile messages together' do
- target_project = create(:project, :repository)
- target_repository = target_project.repository.raw
- target_gitaly_repository = target_repository.gitaly_repository
-
ancestor_path_1 = 'ancestor/path/1'
our_path_1 = 'our/path/1'
their_path_1 = 'their/path/1'
@@ -69,5 +69,49 @@ RSpec.describe Gitlab::GitalyClient::ConflictFilesStitcher do
expect(conflict_files[1].repository).to eq(target_repository)
expect(conflict_files[1].commit_oid).to eq(commit_oid_2)
end
+
+ it 'handles non-latin character names' do
+ ancestor_path_1_utf8 = "ancestor/テスト.txt"
+ our_path_1_utf8 = "our/テスト.txt"
+ their_path_1_utf8 = "their/テスト.txt"
+
+ ancestor_path_1 = String.new('ancestor/テスト.txt', encoding: Encoding::US_ASCII)
+ our_path_1 = String.new('our/テスト.txt', encoding: Encoding::US_ASCII)
+ their_path_1 = String.new('their/テスト.txt', encoding: Encoding::US_ASCII)
+ our_mode_1 = 0744
+ commit_oid_1 = 'f00'
+ content_1 = 'content of the first file'
+
+ header_1 = double(
+ repository: target_gitaly_repository,
+ commit_oid: commit_oid_1,
+ ancestor_path: ancestor_path_1.dup,
+ our_path: our_path_1.dup,
+ their_path: their_path_1.dup,
+ our_mode: our_mode_1
+ )
+
+ messages = [
+ double(files: [double(header: header_1), double(header: nil, content: content_1[0..5])]),
+ double(files: [double(header: nil, content: content_1[6..])])
+ ]
+
+ conflict_files = described_class.new(messages, target_repository.gitaly_repository).to_a
+
+ expect(conflict_files.size).to be(1)
+
+ expect(conflict_files[0].content).to eq(content_1)
+ expect(conflict_files[0].ancestor_path).to eq(ancestor_path_1_utf8)
+ expect(conflict_files[0].their_path).to eq(their_path_1_utf8)
+ expect(conflict_files[0].our_path).to eq(our_path_1_utf8)
+ expect(conflict_files[0].our_mode).to be(our_mode_1)
+ expect(conflict_files[0].repository).to eq(target_repository)
+ expect(conflict_files[0].commit_oid).to eq(commit_oid_1)
+
+ # Doesn't equal the ASCII version
+ expect(conflict_files[0].ancestor_path).not_to eq(ancestor_path_1)
+ expect(conflict_files[0].their_path).not_to eq(their_path_1)
+ expect(conflict_files[0].our_path).not_to eq(our_path_1)
+ end
end
end
diff --git a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
index bd0341d51bf..f50675fee60 100644
--- a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
@@ -1184,7 +1184,7 @@ RSpec.describe Gitlab::GitalyClient::OperationService, feature_category: :source
patch_names.map { |name| File.read(File.join(patches_folder, name)) }.join("\n")
end
- let(:patch_names) { %w(0001-This-does-not-apply-to-the-feature-branch.patch) }
+ let(:patch_names) { %w[0001-This-does-not-apply-to-the-feature-branch.patch] }
let(:branch_name) { 'branch-with-patches' }
subject(:commit_patches) do
@@ -1203,7 +1203,7 @@ RSpec.describe Gitlab::GitalyClient::OperationService, feature_category: :source
end
context 'when the patch could not be applied' do
- let(:patch_names) { %w(0001-This-does-not-apply-to-the-feature-branch.patch) }
+ let(:patch_names) { %w[0001-This-does-not-apply-to-the-feature-branch.patch] }
let(:branch_name) { 'feature' }
it 'raises the correct error' do
diff --git a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb
index ae9276cf90b..118b316f2d4 100644
--- a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb
@@ -67,7 +67,7 @@ RSpec.describe Gitlab::GitalyClient::RefService, feature_category: :gitaly do
.with(gitaly_request_with_params(merged_only: true, merged_branches: ['test']), kind_of(Hash))
.and_return([])
- client.merged_branches(%w(test))
+ client.merged_branches(%w[test])
end
end
@@ -425,7 +425,7 @@ RSpec.describe Gitlab::GitalyClient::RefService, feature_category: :gitaly do
end
describe '#delete_refs' do
- let(:prefixes) { %w(refs/heads refs/keep-around) }
+ let(:prefixes) { %w[refs/heads refs/keep-around] }
subject(:delete_refs) { client.delete_refs(except_with_prefixes: prefixes) }
diff --git a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
index 283a9cb45dc..727bf494ee6 100644
--- a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
@@ -140,6 +140,44 @@ RSpec.describe Gitlab::GitalyClient::RepositoryService, feature_category: :gital
end
end
+ describe '#fork_repository' do
+ let(:source_repository) { Gitlab::Git::Repository.new('default', 'repo/path', '', 'group/project') }
+
+ context 'when branch is not provided' do
+ it 'sends a create_fork message' do
+ expected_request = gitaly_request_with_params(
+ source_repository: source_repository.gitaly_repository,
+ revision: ""
+ )
+
+ expect_any_instance_of(Gitaly::RepositoryService::Stub)
+ .to receive(:create_fork)
+ .with(expected_request, kind_of(Hash))
+ .and_return(double(value: true))
+
+ client.fork_repository(source_repository)
+ end
+ end
+
+ context 'when branch is provided' do
+ it 'sends a create_fork message including revision' do
+ branch = 'wip'
+
+ expected_request = gitaly_request_with_params(
+ source_repository: source_repository.gitaly_repository,
+ revision: "refs/heads/#{branch}"
+ )
+
+ expect_any_instance_of(Gitaly::RepositoryService::Stub)
+ .to receive(:create_fork)
+ .with(expected_request, kind_of(Hash))
+ .and_return(double(value: true))
+
+ client.fork_repository(source_repository, branch)
+ end
+ end
+ end
+
describe '#import_repository' do
let(:source) { 'https://example.com/git/repo.git' }
diff --git a/spec/lib/gitlab/gitaly_client/storage_settings_spec.rb b/spec/lib/gitlab/gitaly_client/storage_settings_spec.rb
index 0c4c8de52ae..7252f7d6afb 100644
--- a/spec/lib/gitlab/gitaly_client/storage_settings_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/storage_settings_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GitalyClient::StorageSettings do
+RSpec.describe Gitlab::GitalyClient::StorageSettings, feature_category: :gitaly do
describe "#initialize" do
context 'when the storage contains no path' do
it 'raises an error' do
@@ -62,16 +62,16 @@ RSpec.describe Gitlab::GitalyClient::StorageSettings do
end
describe '.disk_access_denied?' do
- context 'when Rugged is enabled', :enable_rugged do
- it 'returns false' do
- expect(described_class.disk_access_denied?).to be_falsey
- end
- end
+ subject { described_class.disk_access_denied? }
- context 'when Rugged is disabled' do
- it 'returns true' do
- expect(described_class.disk_access_denied?).to be_truthy
+ it { is_expected.to be_truthy }
+
+ context 'in case of an exception' do
+ before do
+ allow(described_class).to receive(:temporarily_allowed?).and_raise('boom')
end
+
+ it { is_expected.to be_falsey }
end
end
end
diff --git a/spec/lib/gitlab/gitaly_client_spec.rb b/spec/lib/gitlab/gitaly_client_spec.rb
index 0073d2ebe80..00639d9574b 100644
--- a/spec/lib/gitlab/gitaly_client_spec.rb
+++ b/spec/lib/gitlab/gitaly_client_spec.rb
@@ -517,6 +517,44 @@ RSpec.describe Gitlab::GitalyClient, feature_category: :gitaly do
end
end
+ describe '.fetch_relative_path' do
+ subject { described_class.request_kwargs('default', timeout: 1)[:metadata]['relative-path-bin'] }
+
+ let(:relative_path) { 'relative_path' }
+
+ context 'when RequestStore is disabled' do
+ it 'does not set a relative path' do
+ is_expected.to be_nil
+ end
+ end
+
+ context 'when RequestStore is enabled', :request_store do
+ context 'when RequestStore is empty' do
+ it 'does not set a relative path' do
+ is_expected.to be_nil
+ end
+ end
+
+ context 'when RequestStore contains a relalive_path value' do
+ before do
+ Gitlab::SafeRequestStore[:gitlab_git_relative_path] = relative_path
+ end
+
+ it 'sets a base64 encoded version of relative_path' do
+ is_expected.to eq(relative_path)
+ end
+
+ context 'when relalive_path is empty' do
+ let(:relative_path) { '' }
+
+ it 'does not set a relative path' do
+ is_expected.to be_nil
+ end
+ end
+ end
+ end
+ end
+
context 'gitlab_git_env' do
let(:policy) { 'gitaly-route-repository-accessor-policy' }
diff --git a/spec/lib/gitlab/github_import/attachments_downloader_spec.rb b/spec/lib/gitlab/github_import/attachments_downloader_spec.rb
index 72d8a9c0403..65c5a7daeb2 100644
--- a/spec/lib/gitlab/github_import/attachments_downloader_spec.rb
+++ b/spec/lib/gitlab/github_import/attachments_downloader_spec.rb
@@ -94,9 +94,9 @@ RSpec.describe Gitlab::GithubImport::AttachmentsDownloader, feature_category: :i
end
end
- context 'when attachment is behind a redirect' do
- let_it_be(:file_url) { "https://github.com/test/project/assets/142635249/4b9f9c90-f060-4845-97cf-b24c558bcb11" }
- let(:redirect_url) { "https://https://github-production-user-asset-6210df.s3.amazonaws.com/142635249/740edb05293e.jpg" }
+ context 'when attachment is behind a github asset endpoint' do
+ let(:file_url) { "https://github.com/test/project/assets/142635249/4b9f9c90-f060-4845-97cf-b24c558bcb11" }
+ let(:redirect_url) { "https://github-production-user-asset-6210df.s3.amazonaws.com/142635249/740edb05293e.jpg" }
let(:sample_response) do
instance_double(HTTParty::Response, redirection?: true, headers: { location: redirect_url })
end
@@ -115,6 +115,8 @@ RSpec.describe Gitlab::GithubImport::AttachmentsDownloader, feature_category: :i
end
context 'when url is not a redirection' do
+ let(:file_url) { "https://github.com/test/project/assets/142635249/4b9f9c90-f060-4845-97cf-b24c558bcb11.jpg" }
+
let(:sample_response) do
instance_double(HTTParty::Response, code: 200, redirection?: false)
end
@@ -125,8 +127,13 @@ RSpec.describe Gitlab::GithubImport::AttachmentsDownloader, feature_category: :i
.and_return sample_response
end
- it 'raises upon unsuccessful redirection' do
- expect { downloader.perform }.to raise_error("expected a redirect response, got #{sample_response.code}")
+ it 'queries with original file_url' do
+ expect(Gitlab::HTTP).to receive(:perform_request)
+ .with(Net::HTTP::Get, file_url, stream_body: true).and_yield(chunk_double)
+
+ file = downloader.perform
+
+ expect(File.exist?(file.path)).to eq(true)
end
end
diff --git a/spec/lib/gitlab/github_import/client_spec.rb b/spec/lib/gitlab/github_import/client_spec.rb
index 5f321a15de9..c409ec6983f 100644
--- a/spec/lib/gitlab/github_import/client_spec.rb
+++ b/spec/lib/gitlab/github_import/client_spec.rb
@@ -278,7 +278,7 @@ RSpec.describe Gitlab::GithubImport::Client, feature_category: :importers do
client.with_rate_limit do
if retries == 0
retries += 1
- raise(Octokit::TooManyRequests)
+ raise(Octokit::TooManyRequests.new(body: 'primary'))
end
end
@@ -306,6 +306,37 @@ RSpec.describe Gitlab::GithubImport::Client, feature_category: :importers do
expect(client.with_rate_limit { 10 }).to eq(10)
end
+ context 'when threshold is hit' do
+ it 'raises a RateLimitError with the appropriate message' do
+ expect(client).to receive(:requests_remaining?).and_return(false)
+
+ expect { client.with_rate_limit }
+ .to raise_error(Gitlab::GithubImport::RateLimitError, 'Internal threshold reached')
+ end
+ end
+
+ context 'when primary rate limit hit' do
+ let(:limited_block) { -> { raise(Octokit::TooManyRequests.new(body: 'primary')) } }
+
+ it 're-raises a RateLimitError with the appropriate message' do
+ expect(client).to receive(:requests_remaining?).and_return(true)
+
+ expect { client.with_rate_limit(&limited_block) }
+ .to raise_error(Gitlab::GithubImport::RateLimitError, 'primary')
+ end
+ end
+
+ context 'when secondary rate limit hit' do
+ let(:limited_block) { -> { raise(Octokit::TooManyRequests.new(body: 'secondary')) } }
+
+ it 're-raises a RateLimitError with the appropriate message' do
+ expect(client).to receive(:requests_remaining?).and_return(true)
+
+ expect { client.with_rate_limit(&limited_block) }
+ .to raise_error(Gitlab::GithubImport::RateLimitError, 'secondary')
+ end
+ end
+
context 'when Faraday error received from octokit', :aggregate_failures do
let(:error_class) { described_class::CLIENT_CONNECTION_ERROR }
let(:info_params) { { 'error.class': error_class } }
@@ -392,7 +423,7 @@ RSpec.describe Gitlab::GithubImport::Client, feature_category: :importers do
describe '#raise_or_wait_for_rate_limit' do
context 'when running in parallel mode' do
it 'raises RateLimitError' do
- expect { client.raise_or_wait_for_rate_limit }
+ expect { client.raise_or_wait_for_rate_limit('primary') }
.to raise_error(Gitlab::GithubImport::RateLimitError)
end
end
@@ -404,7 +435,7 @@ RSpec.describe Gitlab::GithubImport::Client, feature_category: :importers do
expect(client).to receive(:rate_limit_resets_in).and_return(1)
expect(client).to receive(:sleep).with(1)
- client.raise_or_wait_for_rate_limit
+ client.raise_or_wait_for_rate_limit('primary')
end
it 'increments the rate limit counter' do
@@ -420,7 +451,7 @@ RSpec.describe Gitlab::GithubImport::Client, feature_category: :importers do
.to receive(:increment)
.and_call_original
- client.raise_or_wait_for_rate_limit
+ client.raise_or_wait_for_rate_limit('primary')
end
end
end
diff --git a/spec/lib/gitlab/github_import/importer/collaborators_importer_spec.rb b/spec/lib/gitlab/github_import/importer/collaborators_importer_spec.rb
index dcb02f32a28..6f602531d23 100644
--- a/spec/lib/gitlab/github_import/importer/collaborators_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/collaborators_importer_spec.rb
@@ -82,7 +82,7 @@ RSpec.describe Gitlab::GithubImport::Importer::CollaboratorsImporter, feature_ca
it 'imports each collaborator in parallel' do
expect(Gitlab::GithubImport::ImportCollaboratorWorker).to receive(:perform_in)
- .with(1.second, project.id, an_instance_of(Hash), an_instance_of(String))
+ .with(1, project.id, an_instance_of(Hash), an_instance_of(String))
waiter = importer.parallel_import
diff --git a/spec/lib/gitlab/github_import/importer/diff_notes_importer_spec.rb b/spec/lib/gitlab/github_import/importer/diff_notes_importer_spec.rb
index 945b742b025..4e8066ecb69 100644
--- a/spec/lib/gitlab/github_import/importer/diff_notes_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/diff_notes_importer_spec.rb
@@ -98,7 +98,7 @@ RSpec.describe Gitlab::GithubImport::Importer::DiffNotesImporter, feature_catego
.and_yield(github_comment)
expect(Gitlab::GithubImport::ImportDiffNoteWorker).to receive(:perform_in)
- .with(1.second, project.id, an_instance_of(Hash), an_instance_of(String))
+ .with(1, project.id, an_instance_of(Hash), an_instance_of(String))
waiter = importer.parallel_import
diff --git a/spec/lib/gitlab/github_import/importer/issue_events_importer_spec.rb b/spec/lib/gitlab/github_import/importer/issue_events_importer_spec.rb
index 04b694dc0cb..9aba6a2b02c 100644
--- a/spec/lib/gitlab/github_import/importer/issue_events_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/issue_events_importer_spec.rb
@@ -78,7 +78,7 @@ RSpec.describe Gitlab::GithubImport::Importer::IssueEventsImporter, feature_cate
allow(importer).to receive(:each_object_to_import).and_yield(issue_event)
expect(Gitlab::GithubImport::ImportIssueEventWorker).to receive(:perform_in).with(
- 1.second, project.id, an_instance_of(Hash), an_instance_of(String)
+ 1, project.id, an_instance_of(Hash), an_instance_of(String)
)
waiter = importer.parallel_import
diff --git a/spec/lib/gitlab/github_import/importer/issues_importer_spec.rb b/spec/lib/gitlab/github_import/importer/issues_importer_spec.rb
index d6fd1a4739c..1bfdce04187 100644
--- a/spec/lib/gitlab/github_import/importer/issues_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/issues_importer_spec.rb
@@ -92,7 +92,7 @@ RSpec.describe Gitlab::GithubImport::Importer::IssuesImporter, feature_category:
expect(Gitlab::GithubImport::ImportIssueWorker)
.to receive(:perform_in)
- .with(1.second, project.id, an_instance_of(Hash), an_instance_of(String))
+ .with(1, project.id, an_instance_of(Hash), an_instance_of(String))
waiter = importer.parallel_import
diff --git a/spec/lib/gitlab/github_import/importer/lfs_objects_importer_spec.rb b/spec/lib/gitlab/github_import/importer/lfs_objects_importer_spec.rb
index fab9d26532d..3f5ee68d264 100644
--- a/spec/lib/gitlab/github_import/importer/lfs_objects_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/lfs_objects_importer_spec.rb
@@ -119,7 +119,7 @@ RSpec.describe Gitlab::GithubImport::Importer::LfsObjectsImporter, feature_categ
end
expect(Gitlab::GithubImport::ImportLfsObjectWorker).to receive(:perform_in)
- .with(1.second, project.id, an_instance_of(Hash), an_instance_of(String))
+ .with(1, project.id, an_instance_of(Hash), an_instance_of(String))
waiter = importer.parallel_import
diff --git a/spec/lib/gitlab/github_import/importer/note_importer_spec.rb b/spec/lib/gitlab/github_import/importer/note_importer_spec.rb
index 91311a8e90f..b5fe8c207c8 100644
--- a/spec/lib/gitlab/github_import/importer/note_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/note_importer_spec.rb
@@ -99,7 +99,7 @@ RSpec.describe Gitlab::GithubImport::Importer::NoteImporter, feature_category: :
end
context 'when the note have invalid chars' do
- let(:note_body) { %{There were an invalid char "\u0000" <= right here} }
+ let(:note_body) { %(There were an invalid char "\u0000" <= right here) }
it 'removes invalid chars' do
expect(importer.user_finder)
diff --git a/spec/lib/gitlab/github_import/importer/notes_importer_spec.rb b/spec/lib/gitlab/github_import/importer/notes_importer_spec.rb
index 841cc8178ea..8c93963f325 100644
--- a/spec/lib/gitlab/github_import/importer/notes_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/notes_importer_spec.rb
@@ -84,7 +84,7 @@ RSpec.describe Gitlab::GithubImport::Importer::NotesImporter, feature_category:
.and_yield(github_comment)
expect(Gitlab::GithubImport::ImportNoteWorker).to receive(:perform_in)
- .with(1.second, project.id, an_instance_of(Hash), an_instance_of(String))
+ .with(1, project.id, an_instance_of(Hash), an_instance_of(String))
waiter = importer.parallel_import
diff --git a/spec/lib/gitlab/github_import/importer/protected_branches_importer_spec.rb b/spec/lib/gitlab/github_import/importer/protected_branches_importer_spec.rb
index 6a8b14a2690..8e99585109b 100644
--- a/spec/lib/gitlab/github_import/importer/protected_branches_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/protected_branches_importer_spec.rb
@@ -144,7 +144,7 @@ RSpec.describe Gitlab::GithubImport::Importer::ProtectedBranchesImporter, featur
it 'imports each protected branch in parallel' do
expect(Gitlab::GithubImport::ImportProtectedBranchWorker)
.to receive(:perform_in)
- .with(1.second, project.id, an_instance_of(Hash), an_instance_of(String))
+ .with(1, project.id, an_instance_of(Hash), an_instance_of(String))
expect(Gitlab::GithubImport::ObjectCounter)
.to receive(:increment).with(project, :protected_branch, :fetched)
diff --git a/spec/lib/gitlab/github_import/importer/pull_requests/review_requests_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_requests/review_requests_importer_spec.rb
index d0145ba1120..1977815e3a0 100644
--- a/spec/lib/gitlab/github_import/importer/pull_requests/review_requests_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/pull_requests/review_requests_importer_spec.rb
@@ -97,7 +97,7 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequests::ReviewRequestsImpor
{ id: 4, login: 'alice' },
{ id: 5, login: 'bob' }
]
- },
+ }.deep_stringify_keys,
instance_of(String)
],
[
@@ -108,7 +108,7 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequests::ReviewRequestsImpor
users: [
{ id: 4, login: 'alice' }
]
- },
+ }.deep_stringify_keys,
instance_of(String)
]
]
@@ -116,10 +116,10 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequests::ReviewRequestsImpor
it 'schedule import for each merge request reviewers' do
expect(Gitlab::GithubImport::PullRequests::ImportReviewRequestWorker)
- .to receive(:perform_in).with(1.second, *expected_worker_payload.first)
+ .to receive(:perform_in).with(1, *expected_worker_payload.first)
expect(Gitlab::GithubImport::PullRequests::ImportReviewRequestWorker)
- .to receive(:perform_in).with(1.second, *expected_worker_payload.second)
+ .to receive(:perform_in).with(1, *expected_worker_payload.second)
expect(Gitlab::GithubImport::ObjectCounter)
.to receive(:increment).twice.with(project, :pull_request_review_request, :fetched)
@@ -137,7 +137,7 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequests::ReviewRequestsImpor
it "doesn't schedule import this merge request reviewers" do
expect(Gitlab::GithubImport::PullRequests::ImportReviewRequestWorker)
- .to receive(:perform_in).with(1.second, *expected_worker_payload.second)
+ .to receive(:perform_in).with(1, *expected_worker_payload.second)
expect(Gitlab::GithubImport::ObjectCounter)
.to receive(:increment).once.with(project, :pull_request_review_request, :fetched)
diff --git a/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb
index cfd75fba849..10e413fdfe5 100644
--- a/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb
@@ -102,7 +102,7 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequestsImporter, feature_cat
expect(Gitlab::GithubImport::ImportPullRequestWorker)
.to receive(:perform_in)
- .with(1.second, project.id, an_instance_of(Hash), an_instance_of(String))
+ .with(1, project.id, an_instance_of(Hash), an_instance_of(String))
waiter = importer.parallel_import
diff --git a/spec/lib/gitlab/github_import/issuable_finder_spec.rb b/spec/lib/gitlab/github_import/issuable_finder_spec.rb
index d3236994cef..977fef95d64 100644
--- a/spec/lib/gitlab/github_import/issuable_finder_spec.rb
+++ b/spec/lib/gitlab/github_import/issuable_finder_spec.rb
@@ -2,40 +2,80 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::IssuableFinder, :clean_gitlab_redis_cache do
- let(:project) { double(:project, id: 4, import_data: import_data) }
+RSpec.describe Gitlab::GithubImport::IssuableFinder, :clean_gitlab_redis_cache, feature_category: :importers do
+ let(:project) { build(:project, id: 20, import_data_attributes: import_data_attributes) }
let(:single_endpoint_optional_stage) { false }
- let(:import_data) do
- instance_double(
- ProjectImportData,
+ let(:import_data_attributes) do
+ {
data: {
optional_stages: {
single_endpoint_notes_import: single_endpoint_optional_stage
}
- }.deep_stringify_keys
- )
+ }
+ }
end
- let(:issue) { double(:issue, issuable_type: MergeRequest, issuable_id: 1) }
+ let(:merge_request) { create(:merge_request, source_project: project) }
+ let(:issue) { double(:issue, issuable_type: 'MergeRequest', issuable_id: merge_request.iid) }
let(:finder) { described_class.new(project, issue) }
describe '#database_id' do
- it 'returns nil when no cache is in place' do
- expect(finder.database_id).to be_nil
+ it 'returns nil if object does not exist' do
+ missing_issue = double(:issue, issuable_type: 'MergeRequest', issuable_id: 999)
+
+ expect(described_class.new(project, missing_issue).database_id).to be_nil
+ end
+
+ it 'fetches object id from database if not in cache' do
+ expect(finder.database_id).to eq(merge_request.id)
end
- it 'returns the ID of an issuable when the cache is in place' do
+ it 'fetches object id from cache if present' do
finder.cache_database_id(10)
expect(finder.database_id).to eq(10)
end
+ it 'returns nil and skips database read if cache has no record' do
+ finder.cache_database_id(-1)
+
+ expect(finder.database_id).to be_nil
+ end
+
it 'raises TypeError when the object is not supported' do
finder = described_class.new(project, double(:issue))
expect { finder.database_id }.to raise_error(TypeError)
end
+ context 'with FF import_fallback_to_db_empty_cache disabled' do
+ before do
+ stub_feature_flags(import_fallback_to_db_empty_cache: false)
+ end
+
+ it 'returns nil if object does not exist' do
+ missing_issue = double(:issue, issuable_type: 'MergeRequest', issuable_id: 999)
+
+ expect(described_class.new(project, missing_issue).database_id).to be_nil
+ end
+
+ it 'does not fetch object id from database if not in cache' do
+ expect(finder.database_id).to eq(nil)
+ end
+
+ it 'fetches object id from cache if present' do
+ finder.cache_database_id(10)
+
+ expect(finder.database_id).to eq(10)
+ end
+
+ it 'returns -1 if cache is -1' do
+ finder.cache_database_id(-1)
+
+ expect(finder.database_id).to eq(-1)
+ end
+ end
+
context 'when group is present' do
context 'when settings single_endpoint_notes_import is enabled' do
let(:single_endpoint_optional_stage) { true }
@@ -65,7 +105,7 @@ RSpec.describe Gitlab::GithubImport::IssuableFinder, :clean_gitlab_redis_cache d
it 'caches the ID of a database row' do
expect(Gitlab::Cache::Import::Caching)
.to receive(:write)
- .with('github-import/issuable-finder/4/MergeRequest/1', 10, timeout: 86400)
+ .with("github-import/issuable-finder/20/MergeRequest/#{merge_request.iid}", 10, timeout: 86400)
finder.cache_database_id(10)
end
diff --git a/spec/lib/gitlab/github_import/label_finder_spec.rb b/spec/lib/gitlab/github_import/label_finder_spec.rb
index 9905fce2a20..e46595974d1 100644
--- a/spec/lib/gitlab/github_import/label_finder_spec.rb
+++ b/spec/lib/gitlab/github_import/label_finder_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::LabelFinder, :clean_gitlab_redis_cache do
+RSpec.describe Gitlab::GithubImport::LabelFinder, :clean_gitlab_redis_cache, feature_category: :importers do
let_it_be(:project) { create(:project) }
let_it_be(:finder) { described_class.new(project) }
let_it_be(:bug) { create(:label, project: project, name: 'Bug') }
@@ -18,23 +18,64 @@ RSpec.describe Gitlab::GithubImport::LabelFinder, :clean_gitlab_redis_cache do
expect(finder.id_for(feature.name)).to eq(feature.id)
end
- it 'returns nil for an empty cache key' do
+ it 'fetches object id from database if not in cache' do
key = finder.cache_key_for(bug.name)
Gitlab::Cache::Import::Caching.write(key, '')
- expect(finder.id_for(bug.name)).to be_nil
+ expect(finder.id_for(bug.name)).to eq(bug.id)
end
it 'returns nil for a non existing label name' do
expect(finder.id_for('kittens')).to be_nil
end
+
+ it 'returns nil and skips database read if cache has no record' do
+ key = finder.cache_key_for(bug.name)
+
+ Gitlab::Cache::Import::Caching.write(key, -1)
+
+ expect(finder.id_for(bug.name)).to be_nil
+ end
end
context 'without a cache in place' do
- it 'returns nil for a label' do
+ it 'caches the ID of a database row and returns the ID' do
+ expect(Gitlab::Cache::Import::Caching)
+ .to receive(:write)
+ .with("github-import/label-finder/#{project.id}/#{feature.name}", feature.id)
+ .and_call_original
+
+ expect(finder.id_for(feature.name)).to eq(feature.id)
+ end
+ end
+
+ context 'with FF import_fallback_to_db_empty_cache disabled' do
+ before do
+ stub_feature_flags(import_fallback_to_db_empty_cache: false)
+ end
+
+ it 'returns nil for a non existing label name' do
+ expect(finder.id_for('kittens')).to be_nil
+ end
+
+ it 'does not fetch object id from database if not in cache' do
expect(finder.id_for(feature.name)).to be_nil
end
+
+ it 'fetches object id from cache if present' do
+ finder.build_cache
+
+ expect(finder.id_for(feature.name)).to eq(feature.id)
+ end
+
+ it 'returns -1 if cache is -1' do
+ key = finder.cache_key_for(bug.name)
+
+ Gitlab::Cache::Import::Caching.write(key, -1)
+
+ expect(finder.id_for(bug.name)).to eq(-1)
+ end
end
end
diff --git a/spec/lib/gitlab/github_import/milestone_finder_spec.rb b/spec/lib/gitlab/github_import/milestone_finder_spec.rb
index e7f47d334e8..62886981de1 100644
--- a/spec/lib/gitlab/github_import/milestone_finder_spec.rb
+++ b/spec/lib/gitlab/github_import/milestone_finder_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::MilestoneFinder, :clean_gitlab_redis_cache do
+RSpec.describe Gitlab::GithubImport::MilestoneFinder, :clean_gitlab_redis_cache, feature_category: :importers do
let_it_be(:project) { create(:project) }
let_it_be(:milestone) { create(:milestone, project: project) }
@@ -20,23 +20,72 @@ RSpec.describe Gitlab::GithubImport::MilestoneFinder, :clean_gitlab_redis_cache
expect(finder.id_for(issuable)).to eq(milestone.id)
end
- it 'returns nil for an empty cache key' do
+ it 'returns nil if object does not exist' do
+ missing_issuable = double(:issuable, milestone_number: 999)
+
+ expect(finder.id_for(missing_issuable)).to be_nil
+ end
+
+ it 'fetches object id from database if not in cache' do
key = finder.cache_key_for(milestone.iid)
Gitlab::Cache::Import::Caching.write(key, '')
- expect(finder.id_for(issuable)).to be_nil
+ expect(finder.id_for(issuable)).to eq(milestone.id)
end
it 'returns nil for an issuable with a non-existing milestone' do
expect(finder.id_for(double(:issuable, milestone_number: 5))).to be_nil
end
+
+ it 'returns nil and skips database read if cache has no record' do
+ key = finder.cache_key_for(milestone.iid)
+
+ Gitlab::Cache::Import::Caching.write(key, -1)
+
+ expect(finder.id_for(issuable)).to be_nil
+ end
end
context 'without a cache in place' do
- it 'returns nil' do
+ it 'caches the ID of a database row and returns the ID' do
+ expect(Gitlab::Cache::Import::Caching)
+ .to receive(:write)
+ .with("github-import/milestone-finder/#{project.id}/1", milestone.id)
+ .and_call_original
+
+ expect(finder.id_for(issuable)).to eq(milestone.id)
+ end
+ end
+
+ context 'with FF import_fallback_to_db_empty_cache disabled' do
+ before do
+ stub_feature_flags(import_fallback_to_db_empty_cache: false)
+ end
+
+ it 'returns nil if object does not exist' do
+ missing_issuable = double(:issuable, milestone_number: 999)
+
+ expect(finder.id_for(missing_issuable)).to be_nil
+ end
+
+ it 'does not fetch object id from database if not in cache' do
expect(finder.id_for(issuable)).to be_nil
end
+
+ it 'fetches object id from cache if present' do
+ finder.build_cache
+
+ expect(finder.id_for(issuable)).to eq(milestone.id)
+ end
+
+ it 'returns -1 if cache is -1' do
+ key = finder.cache_key_for(milestone.iid)
+
+ Gitlab::Cache::Import::Caching.write(key, -1)
+
+ expect(finder.id_for(issuable)).to eq(-1)
+ end
end
end
diff --git a/spec/lib/gitlab/github_import/object_counter_spec.rb b/spec/lib/gitlab/github_import/object_counter_spec.rb
index e41a2cff989..964bdd6aad1 100644
--- a/spec/lib/gitlab/github_import/object_counter_spec.rb
+++ b/spec/lib/gitlab/github_import/object_counter_spec.rb
@@ -68,6 +68,16 @@ RSpec.describe Gitlab::GithubImport::ObjectCounter, :clean_gitlab_redis_cache, f
'imported' => { 'issue' => 8 }
)
end
+
+ it 'uses the same TTL as when incrementing' do
+ expect(Gitlab::Cache::Import::Caching)
+ .to receive(:read_integer)
+ .with(anything, timeout: described_class::IMPORT_CACHING_TIMEOUT)
+ .twice
+ .and_call_original
+
+ described_class.summary(project)
+ end
end
context 'when import is in progress but cache expired' do
diff --git a/spec/lib/gitlab/github_import/parallel_scheduling_spec.rb b/spec/lib/gitlab/github_import/parallel_scheduling_spec.rb
index 9de39a3ff7e..e0b1ff1bc33 100644
--- a/spec/lib/gitlab/github_import/parallel_scheduling_spec.rb
+++ b/spec/lib/gitlab/github_import/parallel_scheduling_spec.rb
@@ -296,11 +296,11 @@ RSpec.describe Gitlab::GithubImport::ParallelScheduling, feature_category: :impo
expect(importer).to receive(:each_object_to_import)
.and_yield(object).and_yield(object).and_yield(object)
expect(worker_class).to receive(:perform_in)
- .with(1.second, project.id, { title: 'One' }, 'waiter-key').ordered
+ .with(1, project.id, { 'title' => 'One' }, 'waiter-key').ordered
expect(worker_class).to receive(:perform_in)
- .with(1.second, project.id, { title: 'Two' }, 'waiter-key').ordered
+ .with(1, project.id, { 'title' => 'Two' }, 'waiter-key').ordered
expect(worker_class).to receive(:perform_in)
- .with(1.minute + 1.second, project.id, { title: 'Three' }, 'waiter-key').ordered
+ .with(61, project.id, { 'title' => 'Three' }, 'waiter-key').ordered
job_waiter = importer.parallel_import
@@ -325,11 +325,11 @@ RSpec.describe Gitlab::GithubImport::ParallelScheduling, feature_category: :impo
expect(importer).to receive(:each_object_to_import).and_yield(object).and_yield(object).and_yield(object)
expect(worker_class).to receive(:perform_in)
- .with(1.second, project.id, { title: 'One' }, 'waiter-key').ordered
+ .with(1, project.id, { 'title' => 'One' }, 'waiter-key').ordered
expect(worker_class).to receive(:perform_in)
- .with(1.minute + 1.second, project.id, { title: 'Two' }, 'waiter-key').ordered
+ .with(61, project.id, { 'title' => 'Two' }, 'waiter-key').ordered
expect(worker_class).to receive(:perform_in)
- .with(2.minutes + 1.second, project.id, { title: 'Three' }, 'waiter-key').ordered
+ .with(121, project.id, { 'title' => 'Three' }, 'waiter-key').ordered
job_waiter = importer.parallel_import
diff --git a/spec/lib/gitlab/github_import/representation/to_hash_spec.rb b/spec/lib/gitlab/github_import/representation/to_hash_spec.rb
index 739c832025c..52edffe586d 100644
--- a/spec/lib/gitlab/github_import/representation/to_hash_spec.rb
+++ b/spec/lib/gitlab/github_import/representation/to_hash_spec.rb
@@ -2,14 +2,14 @@
require 'fast_spec_helper'
-RSpec.describe Gitlab::GithubImport::Representation::ToHash do
+RSpec.describe Gitlab::GithubImport::Representation::ToHash, feature_category: :importers do
describe '#to_hash' do
let(:user) { double(:user, attributes: { login: 'alice' }) }
let(:issue) do
double(
:issue,
- attributes: { user: user, assignees: [user], number: 42 }
+ attributes: { user: user, assignees: [user], number: 42, created_at: 5.days.ago, status: :valid }
)
end
@@ -35,5 +35,13 @@ RSpec.describe Gitlab::GithubImport::Representation::ToHash do
it 'keeps values as-is if they do not respond to #to_hash' do
expect(issue_hash[:number]).to eq(42)
end
+
+ it 'converts Date value to String' do
+ expect(issue_hash[:created_at]).to be_an_instance_of(String)
+ end
+
+ it 'converts Symbol value to String' do
+ expect(issue_hash[:status]).to be_an_instance_of(String)
+ end
end
end
diff --git a/spec/lib/gitlab/graphql/known_operations_spec.rb b/spec/lib/gitlab/graphql/known_operations_spec.rb
index c7bc47e1e6a..acb85bc737b 100644
--- a/spec/lib/gitlab/graphql/known_operations_spec.rb
+++ b/spec/lib/gitlab/graphql/known_operations_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Gitlab::Graphql::KnownOperations do
using RSpec::Parameterized::TableSyntax
# Include duplicated operation names to test that we are unique-ifying them
- let(:fake_operations) { %w(foo foo bar bar) }
+ let(:fake_operations) { %w[foo foo bar bar] }
let(:fake_schema) do
Class.new(GraphQL::Schema) do
query Graphql::FakeQueryType
@@ -34,7 +34,7 @@ RSpec.describe Gitlab::Graphql::KnownOperations do
describe "#operations" do
it "returns array of known operations" do
- expect(subject.operations.map(&:name)).to match_array(%w(unknown foo bar))
+ expect(subject.operations.map(&:name)).to match_array(%w[unknown foo bar])
end
end
diff --git a/spec/lib/gitlab/graphql/tracers/metrics_tracer_spec.rb b/spec/lib/gitlab/graphql/tracers/metrics_tracer_spec.rb
index f0312293469..f077cff6875 100644
--- a/spec/lib/gitlab/graphql/tracers/metrics_tracer_spec.rb
+++ b/spec/lib/gitlab/graphql/tracers/metrics_tracer_spec.rb
@@ -6,7 +6,7 @@ require 'rspec-parameterized'
RSpec.describe Gitlab::Graphql::Tracers::MetricsTracer do
using RSpec::Parameterized::TableSyntax
- let(:default_known_operations) { ::Gitlab::Graphql::KnownOperations.new(%w(lorem foo bar)) }
+ let(:default_known_operations) { ::Gitlab::Graphql::KnownOperations.new(%w[lorem foo bar]) }
let(:fake_schema) do
Class.new(GraphQL::Schema) do
diff --git a/spec/lib/gitlab/group_search_results_spec.rb b/spec/lib/gitlab/group_search_results_spec.rb
index 84a2a0549d5..8466e8a1bb5 100644
--- a/spec/lib/gitlab/group_search_results_spec.rb
+++ b/spec/lib/gitlab/group_search_results_spec.rb
@@ -48,7 +48,7 @@ RSpec.describe Gitlab::GroupSearchResults, feature_category: :global_search do
end
include_examples 'search results filtered by state'
- include_examples 'search results filtered by archived', 'search_merge_requests_hide_archived_projects'
+ include_examples 'search results filtered by archived'
end
describe 'milestones search' do
diff --git a/spec/lib/gitlab/hashed_path_spec.rb b/spec/lib/gitlab/hashed_path_spec.rb
index 051c5196748..cf31e891957 100644
--- a/spec/lib/gitlab/hashed_path_spec.rb
+++ b/spec/lib/gitlab/hashed_path_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe Gitlab::HashedPath do
end
context 'when path contains multiple values' do
- let(:path) { %w(path1 path2) }
+ let(:path) { %w[path1 path2] }
it 'returns the disk path' do
expect(subject).to match(%r[\h{2}/\h{2}/\h{64}/path1/path2])
diff --git a/spec/lib/gitlab/health_checks/gitaly_check_spec.rb b/spec/lib/gitlab/health_checks/gitaly_check_spec.rb
index 64c4e92f80b..8f676d20c22 100644
--- a/spec/lib/gitlab/health_checks/gitaly_check_spec.rb
+++ b/spec/lib/gitlab/health_checks/gitaly_check_spec.rb
@@ -4,11 +4,6 @@ require 'fast_spec_helper'
RSpec.describe Gitlab::HealthChecks::GitalyCheck do
let(:result_class) { Gitlab::HealthChecks::Result }
- let(:repository_storages) { ['default'] }
-
- before do
- allow(described_class).to receive(:repository_storages) { repository_storages }
- end
describe '#readiness' do
subject { described_class.readiness }
diff --git a/spec/lib/gitlab/highlight_spec.rb b/spec/lib/gitlab/highlight_spec.rb
index 173131b1d5c..ef3765e479f 100644
--- a/spec/lib/gitlab/highlight_spec.rb
+++ b/spec/lib/gitlab/highlight_spec.rb
@@ -43,7 +43,7 @@ RSpec.describe Gitlab::Highlight do
it 'returns plain version for unknown lexer context' do
result = described_class.highlight(plain_text_file_name, plain_text_content)
- expect(result).to eq(%[<span id="LC1" class="line" lang="plaintext">plain text contents</span>])
+ expect(result).to eq(%(<span id="LC1" class="line" lang="plaintext">plain text contents</span>))
end
context 'when content is too long to be highlighted' do
diff --git a/spec/lib/gitlab/i18n/translation_entry_spec.rb b/spec/lib/gitlab/i18n/translation_entry_spec.rb
index df503e68cf1..3531314cf9c 100644
--- a/spec/lib/gitlab/i18n/translation_entry_spec.rb
+++ b/spec/lib/gitlab/i18n/translation_entry_spec.rb
@@ -108,7 +108,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do
describe '#msgid_contains_newlines' do
it 'is true when the msgid is an array' do
- data = { msgid: %w(hello world) }
+ data = { msgid: %w[hello world] }
entry = described_class.new(entry_data: data, nplurals: 2)
expect(entry.msgid_has_multiple_lines?).to be_truthy
@@ -117,7 +117,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do
describe '#plural_id_contains_newlines' do
it 'is true when the msgid is an array' do
- data = { msgid_plural: %w(hello world) }
+ data = { msgid_plural: %w[hello world] }
entry = described_class.new(entry_data: data, nplurals: 2)
expect(entry.plural_id_has_multiple_lines?).to be_truthy
@@ -126,7 +126,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do
describe '#translations_contain_newlines' do
it 'is true when the msgid is an array' do
- data = { msgstr: %w(hello world) }
+ data = { msgstr: %w[hello world] }
entry = described_class.new(entry_data: data, nplurals: 2)
expect(entry.translations_have_multiple_lines?).to be_truthy
diff --git a/spec/lib/gitlab/import/import_failure_service_spec.rb b/spec/lib/gitlab/import/import_failure_service_spec.rb
index a4682a9495e..362d809bb56 100644
--- a/spec/lib/gitlab/import/import_failure_service_spec.rb
+++ b/spec/lib/gitlab/import/import_failure_service_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe Gitlab::Import::ImportFailureService, :aggregate_failures, featur
let(:import_state) { nil }
let(:fail_import) { false }
let(:metrics) { false }
- let(:external_identifiers) { {} }
+ let(:external_identifiers) { { foo: 'bar' } }
let(:project_id) { project.id }
let(:arguments) do
@@ -90,13 +90,19 @@ RSpec.describe Gitlab::Import::ImportFailureService, :aggregate_failures, featur
)
service.execute
-
- expect(project.import_state.reload.status).to eq('failed')
-
- expect(project.import_failures).not_to be_empty
- expect(project.import_failures.last.exception_class).to eq('StandardError')
- expect(project.import_failures.last.exception_message).to eq('some error')
- expect(project.import_failures.last.retry_count).to eq(0)
+ project.reload
+
+ expect(project.import_state.status).to eq('failed')
+ expect(project.import_failures).to contain_exactly(
+ have_attributes(
+ retry_count: 0,
+ exception_class: 'StandardError',
+ exception_message: 'some error',
+ external_identifiers: external_identifiers.with_indifferent_access,
+ correlation_id_value: Labkit::Correlation::CorrelationId.current_or_new_id,
+ source: 'SomeImporter'
+ )
+ )
end
end
@@ -128,13 +134,19 @@ RSpec.describe Gitlab::Import::ImportFailureService, :aggregate_failures, featur
)
service.execute
+ project.reload
expect(project.import_state.reload.status).to eq('started')
-
- expect(project.import_failures).not_to be_empty
- expect(project.import_failures.last.exception_class).to eq('StandardError')
- expect(project.import_failures.last.exception_message).to eq('some error')
- expect(project.import_failures.last.retry_count).to eq(nil)
+ expect(project.import_failures).to contain_exactly(
+ have_attributes(
+ retry_count: nil,
+ exception_class: 'StandardError',
+ exception_message: 'some error',
+ external_identifiers: external_identifiers.with_indifferent_access,
+ correlation_id_value: Labkit::Correlation::CorrelationId.current_or_new_id,
+ source: 'SomeImporter'
+ )
+ )
end
end
diff --git a/spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_spec.rb b/spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_spec.rb
index fc794f11499..2046e1b5ae5 100644
--- a/spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_spec.rb
+++ b/spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_spec.rb
@@ -100,7 +100,7 @@ RSpec.describe Gitlab::ImportExport::AfterExportStrategies::BaseAfterExportStrat
describe '#log_validation_errors' do
it 'add the message to the shared context' do
- errors = %w(test_message test_message2)
+ errors = %w[test_message test_message2]
allow(service).to receive(:invalid?).and_return(true)
allow(service.errors).to receive(:full_messages).and_return(errors)
diff --git a/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb b/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb
index 297fe3ade07..0f9cbe8aea3 100644
--- a/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb
+++ b/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb
@@ -27,7 +27,7 @@ RSpec.describe Gitlab::ImportExport::AfterExportStrategies::WebUploadStrategy do
describe 'validations' do
it 'only POST and PUT method allowed' do
- %w(POST post PUT put).each do |method|
+ %w[POST post PUT put].each do |method|
expect(subject.new(url: example_url, http_method: method)).to be_valid
end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index cd899a79451..722b47ac9b8 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -660,6 +660,7 @@ project:
- pages_domains
- pages_metadatum
- pages_deployments
+- active_pages_deployments
- authorized_users
- project_authorizations
- remote_mirrors
@@ -1061,6 +1062,7 @@ approval_rules:
- users
- groups
- group_users
+ - group_members
- security_orchestration_policy_configuration
- protected_branches
- approval_merge_request_rule_sources
diff --git a/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb b/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb
index 272c2629b08..9d69e1fec05 100644
--- a/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb
+++ b/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb
@@ -34,7 +34,7 @@ RSpec.describe Gitlab::ImportExport::AttributeCleaner do
'note_ids' => [1, 2, 3],
'remote_attachment_url' => 'http://something.dodgy',
'remote_attachment_request_header' => 'bad value',
- 'remote_attachment_urls' => %w(http://something.dodgy http://something.okay),
+ 'remote_attachment_urls' => %w[http://something.dodgy http://something.okay],
'attributes' => {
'issue_ids' => [1, 2, 3],
'merge_request_ids' => [1, 2, 3],
diff --git a/spec/lib/gitlab/import_export/attributes_permitter_spec.rb b/spec/lib/gitlab/import_export/attributes_permitter_spec.rb
index 08abd7908d2..996b32ed341 100644
--- a/spec/lib/gitlab/import_export/attributes_permitter_spec.rb
+++ b/spec/lib/gitlab/import_export/attributes_permitter_spec.rb
@@ -33,7 +33,7 @@ RSpec.describe Gitlab::ImportExport::AttributesPermitter, feature_category: :imp
EOF
end
- let(:file) { Tempfile.new(%w(import_export .yml)) }
+ let(:file) { Tempfile.new(%w[import_export .yml]) }
let(:config_hash) { Gitlab::ImportExport::Config.new(config: file.path).to_h }
before do
diff --git a/spec/lib/gitlab/import_export/command_line_util_spec.rb b/spec/lib/gitlab/import_export/command_line_util_spec.rb
index 42c3b170e4d..ab47de8f874 100644
--- a/spec/lib/gitlab/import_export/command_line_util_spec.rb
+++ b/spec/lib/gitlab/import_export/command_line_util_spec.rb
@@ -263,7 +263,11 @@ RSpec.describe Gitlab::ImportExport::CommandLineUtil, feature_category: :importe
context 'when exception occurs' do
it 'raises an exception' do
- expect { subject.gzip(dir: path, filename: 'test') }.to raise_error(Gitlab::ImportExport::Error)
+ expect { subject.gzip(dir: path, filename: 'test') }
+ .to raise_error(
+ Gitlab::ImportExport::Error,
+ %r{File compression or decompression failed. Command exited with error code 1: gzip}
+ )
end
end
end
@@ -283,7 +287,11 @@ RSpec.describe Gitlab::ImportExport::CommandLineUtil, feature_category: :importe
context 'when exception occurs' do
it 'raises an exception' do
- expect { subject.gunzip(dir: path, filename: 'test') }.to raise_error(Gitlab::ImportExport::Error)
+ expect { subject.gunzip(dir: path, filename: 'test') }
+ .to raise_error(
+ Gitlab::ImportExport::Error,
+ %r{File compression or decompression failed. Command exited with error code 1: gzip}
+ )
end
end
end
@@ -306,7 +314,7 @@ RSpec.describe Gitlab::ImportExport::CommandLineUtil, feature_category: :importe
include Gitlab::ImportExport::CommandLineUtil
end.new
- expect { klass.tar_cf(archive: 'test', dir: 'test') }.to raise_error(Gitlab::ImportExport::Error, 'command exited with error code 1: Error')
+ expect { klass.tar_cf(archive: 'test', dir: 'test') }.to raise_error(Gitlab::ImportExport::Error, 'Command exited with error code 1: Error')
end
end
end
@@ -363,7 +371,7 @@ RSpec.describe Gitlab::ImportExport::CommandLineUtil, feature_category: :importe
include Gitlab::ImportExport::CommandLineUtil
end.new
- expect { klass.untar_xf(archive: 'test', dir: 'test') }.to raise_error(Gitlab::ImportExport::Error, 'command exited with error code 1: Error')
+ expect { klass.untar_xf(archive: 'test', dir: 'test') }.to raise_error(Gitlab::ImportExport::Error, 'Command exited with error code 1: Error')
end
it 'returns false and includes error status' do
@@ -378,7 +386,7 @@ RSpec.describe Gitlab::ImportExport::CommandLineUtil, feature_category: :importe
end.new
expect(klass.tar_czf(archive: 'test', dir: 'test')).to eq(false)
- expect(klass.shared.errors).to eq(['command exited with error code 1: Error'])
+ expect(klass.shared.errors).to eq(['Command exited with error code 1: Error'])
end
end
end
diff --git a/spec/lib/gitlab/import_export/error_spec.rb b/spec/lib/gitlab/import_export/error_spec.rb
index 015133a399b..db16d0f1e45 100644
--- a/spec/lib/gitlab/import_export/error_spec.rb
+++ b/spec/lib/gitlab/import_export/error_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::ImportExport::Error do
+RSpec.describe Gitlab::ImportExport::Error, feature_category: :importers do
describe '.permission_error' do
subject(:error) do
described_class.permission_error(user, importable)
@@ -28,4 +28,12 @@ RSpec.describe Gitlab::ImportExport::Error do
end
end
end
+
+ describe '.file_compression_error' do
+ it 'adds error to exception message' do
+ message = described_class.file_compression_error('Error').message
+
+ expect(message).to eq('File compression or decompression failed. Error')
+ end
+ end
end
diff --git a/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb b/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb
index dfc7202194d..f6ad3e47c30 100644
--- a/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb
+++ b/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb
@@ -156,7 +156,7 @@ RSpec.describe Gitlab::ImportExport::FastHashSerializer, :with_license, feature_
it 'has project and group labels' do
label_types = subject['issues'].first['label_links'].map { |link| link['label']['type'] }
- expect(label_types).to match_array(%w(ProjectLabel GroupLabel))
+ expect(label_types).to match_array(%w[ProjectLabel GroupLabel])
end
it 'has priorities associated to labels' do
diff --git a/spec/lib/gitlab/import_export/json/ndjson_writer_spec.rb b/spec/lib/gitlab/import_export/json/ndjson_writer_spec.rb
index 486d179ae05..a7aeb9e8c3b 100644
--- a/spec/lib/gitlab/import_export/json/ndjson_writer_spec.rb
+++ b/spec/lib/gitlab/import_export/json/ndjson_writer_spec.rb
@@ -54,7 +54,7 @@ RSpec.describe Gitlab::ImportExport::Json::NdjsonWriter, feature_category: :impo
describe "#write_relation_array" do
it "writes json in correct files" do
values = [{ "key" => "value_1", "key_1" => "value_1" }, { "key" => "value_2", "key_1" => "value_2" }]
- relations = %w(relation1 relation2)
+ relations = %w[relation1 relation2]
relations.each do |relation|
subject.write_relation_array(exportable_path, relation, values.to_enum)
end
diff --git a/spec/lib/gitlab/import_export/lfs_restorer_spec.rb b/spec/lib/gitlab/import_export/lfs_restorer_spec.rb
index fe064c50b9e..042a49f9419 100644
--- a/spec/lib/gitlab/import_export/lfs_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/lfs_restorer_spec.rb
@@ -27,7 +27,7 @@ RSpec.describe Gitlab::ImportExport::LfsRestorer do
# Use the LfsSaver to save data to be restored
def save_lfs_data
- %w(project wiki).each do |repository_type|
+ %w[project wiki].each do |repository_type|
create(
:lfs_objects_project,
project: project,
diff --git a/spec/lib/gitlab/import_export/lfs_saver_spec.rb b/spec/lib/gitlab/import_export/lfs_saver_spec.rb
index 5b6f50025ff..bd225265ef0 100644
--- a/spec/lib/gitlab/import_export/lfs_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/lfs_saver_spec.rb
@@ -60,7 +60,7 @@ RSpec.describe Gitlab::ImportExport::LfsSaver do
describe 'saving a json file' do
before do
# Create two more LfsObjectProject records with different `repository_type`s
- %w(wiki design).each do |repository_type|
+ %w[wiki design].each do |repository_type|
create(
:lfs_objects_project,
project: project,
diff --git a/spec/lib/gitlab/import_export/project/tree_saver_spec.rb b/spec/lib/gitlab/import_export/project/tree_saver_spec.rb
index 1bf1e5b47e1..2e82351db10 100644
--- a/spec/lib/gitlab/import_export/project/tree_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/project/tree_saver_spec.rb
@@ -190,7 +190,7 @@ RSpec.describe Gitlab::ImportExport::Project::TreeSaver, :with_license, feature_
it 'has project and group labels' do
label_types = subject.first['label_links'].map { |link| link['label']['type'] }
- expect(label_types).to match_array(%w(ProjectLabel GroupLabel))
+ expect(label_types).to match_array(%w[ProjectLabel GroupLabel])
end
it 'has priorities associated to labels' do
diff --git a/spec/lib/gitlab/import_export/saver_spec.rb b/spec/lib/gitlab/import_export/saver_spec.rb
index a34e68ecd19..ba38a5d7960 100644
--- a/spec/lib/gitlab/import_export/saver_spec.rb
+++ b/spec/lib/gitlab/import_export/saver_spec.rb
@@ -33,7 +33,7 @@ RSpec.describe Gitlab::ImportExport::Saver do
subject.save # rubocop:disable Rails/SaveBang
expect(ImportExportUpload.find_by(project: project).export_file.url)
- .to match(%r[/uploads/-/system/import_export_upload/export_file.*])
+ .to match(%r{/uploads/-/system/import_export_upload/export_file.*})
end
it 'logs metrics after saving' do
diff --git a/spec/lib/gitlab/import_export/snippet_repo_restorer_spec.rb b/spec/lib/gitlab/import_export/snippet_repo_restorer_spec.rb
index d7b1b180e2e..97e3caba9b3 100644
--- a/spec/lib/gitlab/import_export/snippet_repo_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/snippet_repo_restorer_spec.rb
@@ -31,7 +31,7 @@ RSpec.describe Gitlab::ImportExport::SnippetRepoRestorer do
expect(restorer.restore).to be_truthy
end.to change { SnippetRepository.count }.by(1)
- snippet.repository.expire_method_caches(%i(exists?))
+ snippet.repository.expire_method_caches(%i[exists?])
expect(snippet.repository_exists?).to be_truthy
blob = snippet.repository.blob_at(snippet.default_branch, snippet.file_name)
diff --git a/spec/lib/gitlab/import_sources_spec.rb b/spec/lib/gitlab/import_sources_spec.rb
index db23e3b1fd4..19f17c9079d 100644
--- a/spec/lib/gitlab/import_sources_spec.rb
+++ b/spec/lib/gitlab/import_sources_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe Gitlab::ImportSources, feature_category: :importers do
describe '.values' do
it 'returns an array' do
expected =
- %w(
+ %w[
github
bitbucket
bitbucket_server
@@ -33,7 +33,7 @@ RSpec.describe Gitlab::ImportSources, feature_category: :importers do
gitlab_project
gitea
manifest
- )
+ ]
expect(described_class.values).to eq(expected)
end
@@ -42,14 +42,14 @@ RSpec.describe Gitlab::ImportSources, feature_category: :importers do
describe '.importer_names' do
it 'returns an array of importer names' do
expected =
- %w(
+ %w[
github
bitbucket
bitbucket_server
fogbugz
gitlab_project
gitea
- )
+ ]
expect(described_class.importer_names).to eq(expected)
end
diff --git a/spec/lib/gitlab/instrumentation/redis_cluster_validator_spec.rb b/spec/lib/gitlab/instrumentation/redis_cluster_validator_spec.rb
index ddb5245f825..ea5a32a25ff 100644
--- a/spec/lib/gitlab/instrumentation/redis_cluster_validator_spec.rb
+++ b/spec/lib/gitlab/instrumentation/redis_cluster_validator_spec.rb
@@ -11,25 +11,25 @@ RSpec.describe Gitlab::Instrumentation::RedisClusterValidator, feature_category:
using RSpec::Parameterized::TableSyntax
where(:command, :arguments, :keys, :is_valid) do
- :rename | %w(foo bar) | 2 | false
- :RENAME | %w(foo bar) | 2 | false
- 'rename' | %w(foo bar) | 2 | false
- 'RENAME' | %w(foo bar) | 2 | false
- :rename | %w(iaa ahy) | 2 | true # 'iaa' and 'ahy' hash to the same slot
- :rename | %w({foo}:1 {foo}:2) | 2 | true
- :rename | %w(foo foo bar) | 2 | true # This is not a valid command but should not raise here
- :mget | %w(foo bar) | 2 | false
- :mget | %w(foo foo bar) | 3 | false
- :mget | %w(foo foo) | 2 | true
- :blpop | %w(foo bar 1) | 2 | false
- :blpop | %w(foo foo 1) | 2 | true
- :mset | %w(foo a bar a) | 2 | false
- :mset | %w(foo a foo a) | 2 | true
- :del | %w(foo bar) | 2 | false
- :del | [%w(foo bar)] | 2 | false # Arguments can be a nested array
- :del | %w(foo foo) | 2 | true
- :hset | %w(foo bar) | 1 | nil # Single key write
- :get | %w(foo) | 1 | nil # Single key read
+ :rename | %w[foo bar] | 2 | false
+ :RENAME | %w[foo bar] | 2 | false
+ 'rename' | %w[foo bar] | 2 | false
+ 'RENAME' | %w[foo bar] | 2 | false
+ :rename | %w[iaa ahy] | 2 | true # 'iaa' and 'ahy' hash to the same slot
+ :rename | %w[{foo}:1 {foo}:2] | 2 | true
+ :rename | %w[foo foo bar] | 2 | true # This is not a valid command but should not raise here
+ :mget | %w[foo bar] | 2 | false
+ :mget | %w[foo foo bar] | 3 | false
+ :mget | %w[foo foo] | 2 | true
+ :blpop | %w[foo bar 1] | 2 | false
+ :blpop | %w[foo foo 1] | 2 | true
+ :mset | %w[foo a bar a] | 2 | false
+ :mset | %w[foo a foo a] | 2 | true
+ :del | %w[foo bar] | 2 | false
+ :del | [%w[foo bar]] | 2 | false # Arguments can be a nested array
+ :del | %w[foo foo] | 2 | true
+ :hset | %w[foo bar] | 1 | nil # Single key write
+ :get | %w[foo] | 1 | nil # Single key read
:mget | [] | 0 | true # This is invalid, but not because it's a cross-slot command
end
diff --git a/spec/lib/gitlab/instrumentation_helper_spec.rb b/spec/lib/gitlab/instrumentation_helper_spec.rb
index 698c8a37d48..f8a4d8023c1 100644
--- a/spec/lib/gitlab/instrumentation_helper_spec.rb
+++ b/spec/lib/gitlab/instrumentation_helper_spec.rb
@@ -7,6 +7,7 @@ require 'support/helpers/rails_helpers'
RSpec.describe Gitlab::InstrumentationHelper, :clean_gitlab_redis_repository_cache, :clean_gitlab_redis_cache,
:use_null_store_as_repository_cache, feature_category: :scalability do
using RSpec::Parameterized::TableSyntax
+ include RedisHelpers
describe '.add_instrumentation_data', :request_store do
let(:payload) { {} }
@@ -39,11 +40,23 @@ RSpec.describe Gitlab::InstrumentationHelper, :clean_gitlab_redis_repository_cac
end
context 'when Redis calls are made' do
- it 'adds Redis data and omits Gitaly data' do
- stub_rails_env('staging') # to avoid raising CrossSlotError
- Gitlab::Redis::Sessions.with { |redis| redis.mset('test-cache', 123, 'test-cache2', 123) }
+ let_it_be(:redis_store_class) { define_helper_redis_store_class }
+
+ before do
+ redis_store_class.with(&:ping)
+ Gitlab::Redis::Queues.with(&:ping)
+ RequestStore.clear!
+ end
+
+ it 'adds Redis data including cross slot calls' do
+ expect(Gitlab::Instrumentation::RedisBase)
+ .to receive(:raise_cross_slot_validation_errors?)
+ .once.and_return(false)
+
+ redis_store_class.with { |redis| redis.mset('test-cache', 123, 'test-cache2', 123) }
+
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
- Gitlab::Redis::Sessions.with { |redis| redis.mget('cache-test', 'cache-test-2') }
+ redis_store_class.with { |redis| redis.mget('cache-test', 'cache-test-2') }
end
Gitlab::Redis::Queues.with { |redis| redis.set('test-queues', 321) }
@@ -249,15 +262,12 @@ RSpec.describe Gitlab::InstrumentationHelper, :clean_gitlab_redis_repository_cac
end
end
- describe 'duration calculations' do
- where(:end_time, :start_time, :time_now, :expected_duration) do
+ describe '.queue_duration_for_job' do
+ where(:enqueued_at, :created_at, :time_now, :expected_duration) do
"2019-06-01T00:00:00.000+0000" | nil | "2019-06-01T02:00:00.000+0000" | 2.hours.to_f
- "2019-06-01T02:00:00.000+0000" | nil | "2019-06-01T02:00:00.001+0000" | 0.001
"2019-06-01T02:00:00.000+0000" | "2019-05-01T02:00:00.000+0000" | "2019-06-01T02:00:01.000+0000" | 1
- nil | "2019-06-01T02:00:00.000+0000" | "2019-06-01T02:00:00.001+0000" | 0.001
nil | nil | "2019-06-01T02:00:00.001+0000" | nil
"2019-06-01T02:00:00.000+0200" | nil | "2019-06-01T02:00:00.000-0200" | 4.hours.to_f
- 1571825569.998168 | nil | "2019-10-23T12:13:16.000+0200" | 26.001832
1571825569 | nil | "2019-10-23T12:13:16.000+0200" | 27
"invalid_date" | nil | "2019-10-23T12:13:16.000+0200" | nil
"" | nil | "2019-10-23T12:13:16.000+0200" | nil
@@ -267,27 +277,30 @@ RSpec.describe Gitlab::InstrumentationHelper, :clean_gitlab_redis_repository_cac
Time.at(1571999233).utc | nil | "2019-10-25T12:29:16.000+0200" | 123
end
- describe '.queue_duration_for_job' do
- with_them do
- let(:job) { { 'enqueued_at' => end_time, 'created_at' => start_time } }
+ with_them do
+ let(:job) { { 'enqueued_at' => enqueued_at, 'created_at' => created_at } }
- it "returns the correct duration" do
- travel_to(Time.iso8601(time_now)) do
- expect(described_class.queue_duration_for_job(job)).to eq(expected_duration)
- end
+ it "returns the correct duration" do
+ travel_to(Time.iso8601(time_now)) do
+ expect(described_class.queue_duration_for_job(job)).to eq(expected_duration)
end
end
end
+ end
- describe '.enqueue_latency_for_scheduled_job' do
- with_them do
- let(:job) { { 'enqueued_at' => end_time, 'scheduled_at' => start_time } }
+ describe '.enqueue_latency_for_scheduled_job' do
+ where(:scheduled_at, :enqueued_at, :expected_duration) do
+ "2019-06-01T02:00:00.000+0000" | "2019-06-01T02:00:00.001+0000" | 0.001
+ "2019-06-01T02:00:00.000+0000" | "2019-06-01T02:00:01.000+0000" | 1
+ "2019-06-01T02:00:00.000+0000" | nil | nil
+ nil | "2019-06-01T02:00:01.000+0000" | nil
+ end
- it "returns the correct duration" do
- travel_to(Time.iso8601(time_now)) do
- expect(described_class.enqueue_latency_for_scheduled_job(job)).to eq(expected_duration)
- end
- end
+ with_them do
+ let(:job) { { 'enqueued_at' => enqueued_at, 'scheduled_at' => scheduled_at } }
+
+ it "returns the correct duration" do
+ expect(described_class.enqueue_latency_for_scheduled_job(job)).to eq(expected_duration)
end
end
end
diff --git a/spec/lib/gitlab/issues/rebalancing/state_spec.rb b/spec/lib/gitlab/issues/rebalancing/state_spec.rb
index 5adf1328b87..a0ea5fec8ec 100644
--- a/spec/lib/gitlab/issues/rebalancing/state_spec.rb
+++ b/spec/lib/gitlab/issues/rebalancing/state_spec.rb
@@ -67,11 +67,11 @@ RSpec.describe Gitlab::Issues::Rebalancing::State, :clean_gitlab_redis_shared_st
end
it 'returns array of issue ids' do
- expect(rebalance_caching.get_cached_issue_ids(0, 100)).to eq(%w(1 2 3))
+ expect(rebalance_caching.get_cached_issue_ids(0, 100)).to eq(%w[1 2 3])
end
it 'limits returned values' do
- expect(rebalance_caching.get_cached_issue_ids(0, 2)).to eq(%w(1 2))
+ expect(rebalance_caching.get_cached_issue_ids(0, 2)).to eq(%w[1 2])
end
context 'when caching duplicate issue_ids' do
@@ -84,7 +84,7 @@ RSpec.describe Gitlab::Issues::Rebalancing::State, :clean_gitlab_redis_shared_st
end
it 'returns cached issues with latest scores' do
- expect(rebalance_caching.get_cached_issue_ids(0, 100)).to eq(%w(3 2 1))
+ expect(rebalance_caching.get_cached_issue_ids(0, 100)).to eq(%w[3 2 1])
end
end
end
@@ -231,8 +231,16 @@ RSpec.describe Gitlab::Issues::Rebalancing::State, :clean_gitlab_redis_shared_st
def check_existing_keys
index = 0
- # spec only, we do not actually scan keys in the code
- recently_finished_keys_count = Gitlab::Redis::SharedState.with { |redis| redis.scan(0, match: "#{described_class::RECENTLY_FINISHED_REBALANCE_PREFIX}:*") }.last.count
+ cursor = '0'
+ recently_finished_keys_count = 0
+
+ # loop to scan since it may run against a Redis Cluster
+ loop do
+ # spec only, we do not actually scan keys in the code
+ cursor, items = Gitlab::Redis::SharedState.with { |redis| redis.scan(cursor, match: "#{described_class::RECENTLY_FINISHED_REBALANCE_PREFIX}:*") }
+ recently_finished_keys_count += items.count
+ break if cursor == '0'
+ end
index += 1 if rebalance_caching.get_current_index > 0
index += 1 if rebalance_caching.get_current_project_id.present?
diff --git a/spec/lib/gitlab/jira/middleware_spec.rb b/spec/lib/gitlab/jira/middleware_spec.rb
deleted file mode 100644
index 09cf67d0657..00000000000
--- a/spec/lib/gitlab/jira/middleware_spec.rb
+++ /dev/null
@@ -1,40 +0,0 @@
-# frozen_string_literal: true
-
-require 'fast_spec_helper'
-
-RSpec.describe Gitlab::Jira::Middleware do
- let(:app) { double(:app) }
- let(:middleware) { described_class.new(app) }
- let(:jira_user_agent) { 'Jira DVCS Connector Vertigo/5.0.0-D20170810T012915' }
-
- describe '.jira_dvcs_connector?' do
- it 'returns true when DVCS connector' do
- expect(described_class.jira_dvcs_connector?('HTTP_USER_AGENT' => jira_user_agent)).to eq(true)
- end
-
- it 'returns true if user agent starts with "Jira DVCS Connector"' do
- expect(described_class.jira_dvcs_connector?('HTTP_USER_AGENT' => 'Jira DVCS Connector')).to eq(true)
- end
-
- it 'returns false when not DVCS connector' do
- expect(described_class.jira_dvcs_connector?('HTTP_USER_AGENT' => 'pokemon')).to eq(false)
- end
- end
-
- describe '#call' do
- it 'adjusts HTTP_AUTHORIZATION env when request from Jira DVCS user agent' do
- expect(app).to receive(:call).with({ 'HTTP_USER_AGENT' => jira_user_agent,
- 'HTTP_AUTHORIZATION' => 'Bearer hash-123' })
-
- middleware.call('HTTP_USER_AGENT' => jira_user_agent, 'HTTP_AUTHORIZATION' => 'token hash-123')
- end
-
- it 'does not change HTTP_AUTHORIZATION env when request is not from Jira DVCS user agent' do
- env = { 'HTTP_USER_AGENT' => 'Mozilla/5.0', 'HTTP_AUTHORIZATION' => 'token hash-123' }
-
- expect(app).to receive(:call).with(env)
-
- middleware.call(env)
- end
- end
-end
diff --git a/spec/lib/gitlab/jira_import/handle_labels_service_spec.rb b/spec/lib/gitlab/jira_import/handle_labels_service_spec.rb
index b8c0dc64581..82233641778 100644
--- a/spec/lib/gitlab/jira_import/handle_labels_service_spec.rb
+++ b/spec/lib/gitlab/jira_import/handle_labels_service_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe Gitlab::JiraImport::HandleLabelsService do
let_it_be(:other_project_label) { create(:label, title: 'feature') }
let_it_be(:group_label) { create(:group_label, group: group, title: 'dev') }
- let(:jira_labels) { %w(bug feature dev group::new) }
+ let(:jira_labels) { %w[bug feature dev group::new] }
subject { described_class.new(project, jira_labels).execute }
@@ -23,7 +23,7 @@ RSpec.describe Gitlab::JiraImport::HandleLabelsService do
it 'creates the missing labels on the project level' do
expect { subject }.to change { Label.count }.from(3).to(5)
- expect(created_labels.map(&:title)).to match_array(%w(feature group::new))
+ expect(created_labels.map(&:title)).to match_array(%w[feature group::new])
end
it 'returns the id of all labels matching the title' do
@@ -32,7 +32,7 @@ RSpec.describe Gitlab::JiraImport::HandleLabelsService do
end
context 'when no provided jira labels are missing' do
- let(:jira_labels) { %w(bug dev) }
+ let(:jira_labels) { %w[bug dev] }
it 'does not create any new labels' do
expect { subject }.not_to change { Label.count }.from(3)
diff --git a/spec/lib/gitlab/jira_import/issue_serializer_spec.rb b/spec/lib/gitlab/jira_import/issue_serializer_spec.rb
index 30ad24472b4..98958d8a92e 100644
--- a/spec/lib/gitlab/jira_import/issue_serializer_spec.rb
+++ b/spec/lib/gitlab/jira_import/issue_serializer_spec.rb
@@ -28,7 +28,7 @@ RSpec.describe Gitlab::JiraImport::IssueSerializer do
end
let(:priority_field) { { 'name' => 'Medium' } }
- let(:labels_field) { %w(bug dev backend frontend) }
+ let(:labels_field) { %w[bug dev backend frontend] }
let(:fields) do
{
@@ -101,7 +101,7 @@ RSpec.describe Gitlab::JiraImport::IssueSerializer do
end
context 'when there are no new labels' do
- let(:labels_field) { %w(bug dev) }
+ let(:labels_field) { %w[bug dev] }
it 'assigns the labels to the Issue hash' do
expect(subject[:label_ids]).to match_array([project_label.id, group_label.id])
diff --git a/spec/lib/gitlab/jira_import/labels_importer_spec.rb b/spec/lib/gitlab/jira_import/labels_importer_spec.rb
index 4fb5e363475..7579e2c65f4 100644
--- a/spec/lib/gitlab/jira_import/labels_importer_spec.rb
+++ b/spec/lib/gitlab/jira_import/labels_importer_spec.rb
@@ -36,8 +36,8 @@ RSpec.describe Gitlab::JiraImport::LabelsImporter do
let_it_be(:jira_import_with_label) { create(:jira_import_state, label: label, project: project) }
let_it_be(:issue_label) { create(:label, project: project, title: 'bug') }
- let(:jira_labels_1) { { "maxResults" => 2, "startAt" => 0, "total" => 3, "isLast" => false, "values" => %w(backend bug) } }
- let(:jira_labels_2) { { "maxResults" => 2, "startAt" => 2, "total" => 3, "isLast" => true, "values" => %w(feature) } }
+ let(:jira_labels_1) { { "maxResults" => 2, "startAt" => 0, "total" => 3, "isLast" => false, "values" => %w[backend bug] } }
+ let(:jira_labels_2) { { "maxResults" => 2, "startAt" => 2, "total" => 3, "isLast" => true, "values" => %w[feature] } }
context 'when labels are returned from jira' do
before do
@@ -55,8 +55,8 @@ RSpec.describe Gitlab::JiraImport::LabelsImporter do
end
it 'calls Gitlab::JiraImport::HandleLabelsService' do
- expect(Gitlab::JiraImport::HandleLabelsService).to receive(:new).with(project, %w(backend bug)).and_return(double(execute: [1, 2]))
- expect(Gitlab::JiraImport::HandleLabelsService).to receive(:new).with(project, %w(feature)).and_return(double(execute: [3]))
+ expect(Gitlab::JiraImport::HandleLabelsService).to receive(:new).with(project, %w[backend bug]).and_return(double(execute: [1, 2]))
+ expect(Gitlab::JiraImport::HandleLabelsService).to receive(:new).with(project, %w[feature]).and_return(double(execute: [3]))
subject
end
@@ -92,7 +92,7 @@ RSpec.describe Gitlab::JiraImport::LabelsImporter do
end
context 'when the isLast argument is missing' do
- let(:jira_labels) { { "maxResults" => 2, "startAt" => 0, "total" => 3, "values" => %w(bug dev) } }
+ let(:jira_labels) { { "maxResults" => 2, "startAt" => 0, "total" => 3, "values" => %w[bug dev] } }
it_behaves_like 'no labels handling'
end
diff --git a/spec/lib/gitlab/job_waiter_spec.rb b/spec/lib/gitlab/job_waiter_spec.rb
index b000f55e739..e3d7d59df04 100644
--- a/spec/lib/gitlab/job_waiter_spec.rb
+++ b/spec/lib/gitlab/job_waiter_spec.rb
@@ -62,6 +62,10 @@ RSpec.describe Gitlab::JobWaiter, :redis, feature_category: :shared do
before do
allow_any_instance_of(described_class).to receive(:wait).and_call_original
+ stub_feature_flags(
+ use_primary_and_secondary_stores_for_shared_state: false,
+ use_primary_store_as_default_for_shared_state: false
+ )
end
it 'returns when all jobs have been completed' do
@@ -83,36 +87,54 @@ RSpec.describe Gitlab::JobWaiter, :redis, feature_category: :shared do
expect(result).to contain_exactly('a')
end
- context 'when a label is provided' do
- let(:waiter) { described_class.new(2, worker_label: 'Foo') }
- let(:started_total) { double(:started_total) }
- let(:timeouts_total) { double(:timeouts_total) }
+ context 'when migration is ongoing' do
+ let(:waiter) { described_class.new(3) }
- before do
- allow(Gitlab::Metrics).to receive(:counter)
- .with(described_class::STARTED_METRIC, anything)
- .and_return(started_total)
+ shared_examples 'returns all jobs' do
+ it 'returns all jobs' do
+ result = nil
+ expect { Timeout.timeout(6) { result = waiter.wait(5) } }.not_to raise_error
- allow(Gitlab::Metrics).to receive(:counter)
- .with(described_class::TIMEOUTS_METRIC, anything)
- .and_return(timeouts_total)
+ expect(result).to contain_exactly('a', 'b', 'c')
+ end
end
- it 'increments just job_waiter_started_total when all jobs complete' do
- expect(started_total).to receive(:increment).with(worker: 'Foo')
- expect(timeouts_total).not_to receive(:increment)
+ context 'when using both stores' do
+ context 'with existing jobs in old store' do
+ before do
+ described_class.notify(waiter.key, 'a')
+ described_class.notify(waiter.key, 'b')
+ described_class.notify(waiter.key, 'c')
+ stub_feature_flags(use_primary_and_secondary_stores_for_shared_state: true)
+ end
- described_class.notify(waiter.key, 'a')
- described_class.notify(waiter.key, 'b')
+ it_behaves_like 'returns all jobs'
+ end
- expect { Timeout.timeout(1) { waiter.wait(2) } }.not_to raise_error
- end
+ context 'with jobs in both stores' do
+ before do
+ stub_feature_flags(use_primary_and_secondary_stores_for_shared_state: true)
+ described_class.notify(waiter.key, 'a')
+ described_class.notify(waiter.key, 'b')
+ described_class.notify(waiter.key, 'c')
+ end
- it 'increments job_waiter_started_total and job_waiter_timeouts_total when it times out' do
- expect(started_total).to receive(:increment).with(worker: 'Foo')
- expect(timeouts_total).to receive(:increment).with(worker: 'Foo')
+ it_behaves_like 'returns all jobs'
+ end
- expect { Timeout.timeout(2) { waiter.wait(1) } }.not_to raise_error
+ context 'when using primary store as default store' do
+ before do
+ stub_feature_flags(
+ use_primary_and_secondary_stores_for_shared_state: true,
+ use_primary_store_as_default_for_shared_state: true
+ )
+ described_class.notify(waiter.key, 'a')
+ described_class.notify(waiter.key, 'b')
+ described_class.notify(waiter.key, 'c')
+ end
+
+ it_behaves_like 'returns all jobs'
+ end
end
end
end
diff --git a/spec/lib/gitlab/kubernetes/kubectl_cmd_spec.rb b/spec/lib/gitlab/kubernetes/kubectl_cmd_spec.rb
index 3028e0a13aa..f88f7c4c108 100644
--- a/spec/lib/gitlab/kubernetes/kubectl_cmd_spec.rb
+++ b/spec/lib/gitlab/kubernetes/kubectl_cmd_spec.rb
@@ -7,7 +7,7 @@ require_relative '../../../../lib/gitlab/kubernetes/pod_cmd'
RSpec.describe Gitlab::Kubernetes::KubectlCmd do
describe '.delete' do
it 'constructs string properly' do
- args = %w(resource_type type --flag-1 --flag-2)
+ args = %w[resource_type type --flag-1 --flag-2]
expected_command = 'kubectl delete resource_type type --flag-1 --flag-2'
@@ -31,7 +31,7 @@ RSpec.describe Gitlab::Kubernetes::KubectlCmd do
context 'with optional args' do
it 'constructs command properly with many args' do
- args = %w(arg-1 --flag-0-1 arg-2 --flag-0-2)
+ args = %w[arg-1 --flag-0-1 arg-2 --flag-0-2]
expected_command = 'kubectl apply -f filename arg-1 --flag-0-1 arg-2 --flag-0-2'
diff --git a/spec/lib/gitlab/kubernetes/role_spec.rb b/spec/lib/gitlab/kubernetes/role_spec.rb
index acb9b5d4e8e..288a5406372 100644
--- a/spec/lib/gitlab/kubernetes/role_spec.rb
+++ b/spec/lib/gitlab/kubernetes/role_spec.rb
@@ -9,9 +9,9 @@ RSpec.describe Gitlab::Kubernetes::Role do
let(:rules) do
[{
- apiGroups: %w(hello.world),
- resources: %w(oil diamonds coffee),
- verbs: %w(say do walk run)
+ apiGroups: %w[hello.world],
+ resources: %w[oil diamonds coffee],
+ verbs: %w[say do walk run]
}]
end
diff --git a/spec/lib/gitlab/language_data_spec.rb b/spec/lib/gitlab/language_data_spec.rb
index bb4b0c3855c..828fd95f78e 100644
--- a/spec/lib/gitlab/language_data_spec.rb
+++ b/spec/lib/gitlab/language_data_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe Gitlab::LanguageData do
expect(described_class.extensions).to be_a(Set)
expect(described_class.extensions.count).to be > 0
# Sanity check for known extensions
- expect(described_class.extensions).to include(*%w(.rb .yml .json))
+ expect(described_class.extensions).to include(*%w[.rb .yml .json])
end
end
end
diff --git a/spec/lib/gitlab/mail_room/mail_room_spec.rb b/spec/lib/gitlab/mail_room/mail_room_spec.rb
index 7259b5e2484..d3ddf034cd3 100644
--- a/spec/lib/gitlab/mail_room/mail_room_spec.rb
+++ b/spec/lib/gitlab/mail_room/mail_room_spec.rb
@@ -245,7 +245,6 @@ RSpec.describe Gitlab::MailRoom, feature_category: :build do
delivery_options: {
redis_url: "localhost",
redis_db: 99,
- namespace: "resque:gitlab",
queue: "default",
worker: "EmailReceiverWorker",
sentinels: [{ host: "localhost", port: 1234 }]
@@ -258,7 +257,6 @@ RSpec.describe Gitlab::MailRoom, feature_category: :build do
delivery_options: {
redis_url: "localhost",
redis_db: 99,
- namespace: "resque:gitlab",
queue: "default",
worker: "ServiceDeskEmailReceiverWorker",
sentinels: [{ host: "localhost", port: 1234 }]
diff --git a/spec/lib/gitlab/markup_helper_spec.rb b/spec/lib/gitlab/markup_helper_spec.rb
index 2bffd029568..a7508288f4e 100644
--- a/spec/lib/gitlab/markup_helper_spec.rb
+++ b/spec/lib/gitlab/markup_helper_spec.rb
@@ -4,8 +4,8 @@ require 'fast_spec_helper'
RSpec.describe Gitlab::MarkupHelper do
describe '#markup?' do
- %w(textile rdoc org creole wiki
- mediawiki rst adoc ad asciidoc mdown md markdown).each do |type|
+ %w[textile rdoc org creole wiki
+ mediawiki rst adoc ad asciidoc mdown md markdown].each do |type|
it "returns true for #{type} files" do
expect(described_class.markup?("README.#{type}")).to be_truthy
end
@@ -17,7 +17,7 @@ RSpec.describe Gitlab::MarkupHelper do
end
describe '#gitlab_markdown?' do
- %w(mdown mkd mkdn md markdown).each do |type|
+ %w[mdown mkd mkdn md markdown].each do |type|
it "returns true for #{type} files" do
expect(described_class.gitlab_markdown?("README.#{type}")).to be_truthy
end
@@ -29,7 +29,7 @@ RSpec.describe Gitlab::MarkupHelper do
end
describe '#asciidoc?' do
- %w(adoc ad asciidoc ADOC).each do |type|
+ %w[adoc ad asciidoc ADOC].each do |type|
it "returns true for #{type} files" do
expect(described_class.asciidoc?("README.#{type}")).to be_truthy
end
diff --git a/spec/lib/gitlab/memory/instrumentation_spec.rb b/spec/lib/gitlab/memory/instrumentation_spec.rb
index f287edb7da3..059bcad37e7 100644
--- a/spec/lib/gitlab/memory/instrumentation_spec.rb
+++ b/spec/lib/gitlab/memory/instrumentation_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Memory::Instrumentation, feature_category: :application_performance do
+RSpec.describe Gitlab::Memory::Instrumentation, feature_category: :cloud_connector do
include MemoryInstrumentationHelper
before do
diff --git a/spec/lib/gitlab/memory/reporter_spec.rb b/spec/lib/gitlab/memory/reporter_spec.rb
index 1d19d7129cf..5ba429ae6bc 100644
--- a/spec/lib/gitlab/memory/reporter_spec.rb
+++ b/spec/lib/gitlab/memory/reporter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Memory::Reporter, :aggregate_failures, feature_category: :application_performance do
+RSpec.describe Gitlab::Memory::Reporter, :aggregate_failures, feature_category: :cloud_connector do
let(:fake_report) do
Class.new do
def name
diff --git a/spec/lib/gitlab/memory/reports/heap_dump_spec.rb b/spec/lib/gitlab/memory/reports/heap_dump_spec.rb
index 4e235a71bdb..7888f8b0c79 100644
--- a/spec/lib/gitlab/memory/reports/heap_dump_spec.rb
+++ b/spec/lib/gitlab/memory/reports/heap_dump_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Memory::Reports::HeapDump, feature_category: :application_performance do
+RSpec.describe Gitlab::Memory::Reports::HeapDump, feature_category: :cloud_connector do
# Copy this class so we do not mess with its state.
let(:klass) { described_class.dup }
diff --git a/spec/lib/gitlab/memory/watchdog/configurator_spec.rb b/spec/lib/gitlab/memory/watchdog/configurator_spec.rb
index 035652abfe6..cd9ac0d7a8d 100644
--- a/spec/lib/gitlab/memory/watchdog/configurator_spec.rb
+++ b/spec/lib/gitlab/memory/watchdog/configurator_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Memory::Watchdog::Configurator, feature_category: :application_performance do
+RSpec.describe Gitlab::Memory::Watchdog::Configurator, feature_category: :cloud_connector do
shared_examples 'as configurator' do |handler_class, event_reporter_class, sleep_time_env, sleep_time|
it 'configures the correct handler' do
configurator.call(configuration)
diff --git a/spec/lib/gitlab/memory/watchdog/event_reporter_spec.rb b/spec/lib/gitlab/memory/watchdog/event_reporter_spec.rb
index f1d241249e2..e27f842bc71 100644
--- a/spec/lib/gitlab/memory/watchdog/event_reporter_spec.rb
+++ b/spec/lib/gitlab/memory/watchdog/event_reporter_spec.rb
@@ -3,7 +3,7 @@
require 'fast_spec_helper'
require 'prometheus/client'
-RSpec.describe Gitlab::Memory::Watchdog::EventReporter, feature_category: :application_performance do
+RSpec.describe Gitlab::Memory::Watchdog::EventReporter, feature_category: :cloud_connector do
let(:logger) { instance_double(::Logger) }
let(:violations_counter) { instance_double(::Prometheus::Client::Counter) }
let(:violations_handled_counter) { instance_double(::Prometheus::Client::Counter) }
diff --git a/spec/lib/gitlab/memory/watchdog/handlers/null_handler_spec.rb b/spec/lib/gitlab/memory/watchdog/handlers/null_handler_spec.rb
index 09c76de9611..96cb02393f9 100644
--- a/spec/lib/gitlab/memory/watchdog/handlers/null_handler_spec.rb
+++ b/spec/lib/gitlab/memory/watchdog/handlers/null_handler_spec.rb
@@ -2,7 +2,7 @@
require 'fast_spec_helper'
-RSpec.describe Gitlab::Memory::Watchdog::Handlers::NullHandler, feature_category: :application_performance do
+RSpec.describe Gitlab::Memory::Watchdog::Handlers::NullHandler, feature_category: :cloud_connector do
subject(:handler) { described_class.instance }
describe '#call' do
diff --git a/spec/lib/gitlab/memory/watchdog/handlers/puma_handler_spec.rb b/spec/lib/gitlab/memory/watchdog/handlers/puma_handler_spec.rb
index 7df95c1722e..4c2fd5a3283 100644
--- a/spec/lib/gitlab/memory/watchdog/handlers/puma_handler_spec.rb
+++ b/spec/lib/gitlab/memory/watchdog/handlers/puma_handler_spec.rb
@@ -2,7 +2,7 @@
require 'fast_spec_helper'
-RSpec.describe Gitlab::Memory::Watchdog::Handlers::PumaHandler, feature_category: :application_performance do
+RSpec.describe Gitlab::Memory::Watchdog::Handlers::PumaHandler, feature_category: :cloud_connector do
# rubocop: disable RSpec/VerifiedDoubles
# In tests, the Puma constant is not loaded so we cannot make this an instance_double.
let(:puma_worker_handle_class) { double('Puma::Cluster::WorkerHandle') }
diff --git a/spec/lib/gitlab/memory/watchdog/handlers/sidekiq_handler_spec.rb b/spec/lib/gitlab/memory/watchdog/handlers/sidekiq_handler_spec.rb
index d1f303e7731..68dd784fb7e 100644
--- a/spec/lib/gitlab/memory/watchdog/handlers/sidekiq_handler_spec.rb
+++ b/spec/lib/gitlab/memory/watchdog/handlers/sidekiq_handler_spec.rb
@@ -3,7 +3,7 @@
require 'fast_spec_helper'
require 'sidekiq'
-RSpec.describe Gitlab::Memory::Watchdog::Handlers::SidekiqHandler, feature_category: :application_performance do
+RSpec.describe Gitlab::Memory::Watchdog::Handlers::SidekiqHandler, feature_category: :cloud_connector do
let(:sleep_time) { 3 }
let(:shutdown_timeout_seconds) { 30 }
let(:handler_iterations) { 0 }
diff --git a/spec/lib/gitlab/memory/watchdog/monitor/rss_memory_limit_spec.rb b/spec/lib/gitlab/memory/watchdog/monitor/rss_memory_limit_spec.rb
index 67d185fd2f1..552736a55ef 100644
--- a/spec/lib/gitlab/memory/watchdog/monitor/rss_memory_limit_spec.rb
+++ b/spec/lib/gitlab/memory/watchdog/monitor/rss_memory_limit_spec.rb
@@ -4,7 +4,7 @@ require 'fast_spec_helper'
require 'prometheus/client'
require 'support/shared_examples/lib/gitlab/memory/watchdog/monitor_result_shared_examples'
-RSpec.describe Gitlab::Memory::Watchdog::Monitor::RssMemoryLimit, feature_category: :application_performance do
+RSpec.describe Gitlab::Memory::Watchdog::Monitor::RssMemoryLimit, feature_category: :cloud_connector do
let(:max_rss_limit_gauge) { instance_double(::Prometheus::Client::Gauge) }
let(:memory_limit_bytes) { 2_097_152_000 }
let(:worker_memory_bytes) { 1_048_576_000 }
diff --git a/spec/lib/gitlab/memory/watchdog/sidekiq_event_reporter_spec.rb b/spec/lib/gitlab/memory/watchdog/sidekiq_event_reporter_spec.rb
index 48595c3f172..06b1646d418 100644
--- a/spec/lib/gitlab/memory/watchdog/sidekiq_event_reporter_spec.rb
+++ b/spec/lib/gitlab/memory/watchdog/sidekiq_event_reporter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Memory::Watchdog::SidekiqEventReporter, feature_category: :application_performance do
+RSpec.describe Gitlab::Memory::Watchdog::SidekiqEventReporter, feature_category: :cloud_connector do
let(:counter) { instance_double(::Prometheus::Client::Counter) }
before do
diff --git a/spec/lib/gitlab/memory/watchdog_spec.rb b/spec/lib/gitlab/memory/watchdog_spec.rb
index dd6bfb6da2c..c442208617f 100644
--- a/spec/lib/gitlab/memory/watchdog_spec.rb
+++ b/spec/lib/gitlab/memory/watchdog_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures, feature_category: :application_performance do
+RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures, feature_category: :cloud_connector do
context 'watchdog' do
let(:configuration) { instance_double(described_class::Configuration) }
let(:handler) { instance_double(described_class::Handlers::NullHandler) }
diff --git a/spec/lib/gitlab/merge_requests/mergeability/check_result_spec.rb b/spec/lib/gitlab/merge_requests/mergeability/check_result_spec.rb
index 74aa3528328..cd84525f7e5 100644
--- a/spec/lib/gitlab/merge_requests/mergeability/check_result_spec.rb
+++ b/spec/lib/gitlab/merge_requests/mergeability/check_result_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::MergeRequests::Mergeability::CheckResult do
+RSpec.describe Gitlab::MergeRequests::Mergeability::CheckResult, feature_category: :code_review_workflow do
subject(:check_result) { described_class }
let(:time) { Time.current }
@@ -63,6 +63,28 @@ RSpec.describe Gitlab::MergeRequests::Mergeability::CheckResult do
end
end
+ describe '.inactive' do
+ subject(:inactive) { check_result.inactive(payload: payload) }
+
+ let(:payload) { {} }
+
+ it 'creates a inactive result' do
+ expect(inactive.status).to eq described_class::INACTIVE_STATUS
+ end
+
+ it 'uses the default payload' do
+ expect(inactive.payload).to eq described_class.default_payload
+ end
+
+ context 'when given a payload' do
+ let(:payload) { { last_run_at: time + 1.day, test: 'test' } }
+
+ it 'uses the payload passed' do
+ expect(inactive.payload).to eq payload
+ end
+ end
+ end
+
describe '.from_hash' do
subject(:from_hash) { described_class.from_hash(hash) }
diff --git a/spec/lib/gitlab/metrics/exporter/base_exporter_spec.rb b/spec/lib/gitlab/metrics/exporter/base_exporter_spec.rb
index 6673cc50d67..4184c674823 100644
--- a/spec/lib/gitlab/metrics/exporter/base_exporter_spec.rb
+++ b/spec/lib/gitlab/metrics/exporter/base_exporter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Metrics::Exporter::BaseExporter, feature_category: :application_performance do
+RSpec.describe Gitlab::Metrics::Exporter::BaseExporter, feature_category: :cloud_connector do
let(:settings) { double('settings') }
let(:log_enabled) { false }
let(:exporter) { described_class.new(settings, log_enabled: log_enabled, log_file: 'test_exporter.log') }
diff --git a/spec/lib/gitlab/metrics/rails_slis_spec.rb b/spec/lib/gitlab/metrics/rails_slis_spec.rb
index ef996f61082..3050c769117 100644
--- a/spec/lib/gitlab/metrics/rails_slis_spec.rb
+++ b/spec/lib/gitlab/metrics/rails_slis_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
RSpec.describe Gitlab::Metrics::RailsSlis, feature_category: :error_budgets do
before do
- allow(Gitlab::Graphql::KnownOperations).to receive(:default).and_return(Gitlab::Graphql::KnownOperations.new(%w(foo bar)))
+ allow(Gitlab::Graphql::KnownOperations).to receive(:default).and_return(Gitlab::Graphql::KnownOperations.new(%w[foo bar]))
end
describe '.initialize_request_slis!' do
diff --git a/spec/lib/gitlab/metrics/samplers/threads_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/threads_sampler_spec.rb
index 5dabafb7c0b..0a3648c8b9a 100644
--- a/spec/lib/gitlab/metrics/samplers/threads_sampler_spec.rb
+++ b/spec/lib/gitlab/metrics/samplers/threads_sampler_spec.rb
@@ -40,11 +40,11 @@ RSpec.describe Gitlab::Metrics::Samplers::ThreadsSampler do
context 'thread names', :aggregate_failures do
where(:thread_names, :expected_names) do
[
- [[nil], %w(unnamed)],
+ [[nil], %w[unnamed]],
[['puma threadpool 1', 'puma threadpool 001', 'puma threadpool 002'], ['puma threadpool']],
- [%w(sidekiq_worker_thread), %w(sidekiq_worker_thread)],
- [%w(some_sampler some_exporter), %w(some_sampler some_exporter)],
- [%w(unknown thing), %w(unrecognized)]
+ [%w[sidekiq_worker_thread], %w[sidekiq_worker_thread]],
+ [%w[some_sampler some_exporter], %w[some_sampler some_exporter]],
+ [%w[unknown thing], %w[unrecognized]]
]
end
diff --git a/spec/lib/gitlab/metrics/subscribers/action_cable_spec.rb b/spec/lib/gitlab/metrics/subscribers/action_cable_spec.rb
index 54868bb6ca4..8fbfcdc3bd5 100644
--- a/spec/lib/gitlab/metrics/subscribers/action_cable_spec.rb
+++ b/spec/lib/gitlab/metrics/subscribers/action_cable_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Metrics::Subscribers::ActionCable, :request_store, feature_category: :application_performance do
+RSpec.describe Gitlab::Metrics::Subscribers::ActionCable, :request_store, feature_category: :cloud_connector do
let(:subscriber) { described_class.new }
let(:counter) { double(:counter) }
let(:transmitted_bytes_counter) { double(:counter) }
diff --git a/spec/lib/gitlab/middleware/go_spec.rb b/spec/lib/gitlab/middleware/go_spec.rb
index a3835f9eed0..4820f42ade3 100644
--- a/spec/lib/gitlab/middleware/go_spec.rb
+++ b/spec/lib/gitlab/middleware/go_spec.rb
@@ -206,7 +206,7 @@ RSpec.describe Gitlab::Middleware::Go, feature_category: :source_code_management
expect(response[0]).to eq(404)
expect(response[1]['Content-Type']).to eq('text/html')
- expected_body = %{<html><body>go get #{Gitlab.config.gitlab.url}/#{project.full_path}</body></html>}
+ expected_body = %(<html><body>go get #{Gitlab.config.gitlab.url}/#{project.full_path}</body></html>)
expect(response[2]).to eq([expected_body])
end
end
@@ -278,7 +278,7 @@ RSpec.describe Gitlab::Middleware::Go, feature_category: :source_code_management
project_url = "http://#{Gitlab.config.gitlab.host}/#{path}"
expect(response[0]).to eq(200)
expect(response[1]['Content-Type']).to eq('text/html')
- expected_body = %{<html><head><meta name="go-import" content="#{Gitlab.config.gitlab.host}/#{path} git #{repository_url}"><meta name="go-source" content="#{Gitlab.config.gitlab.host}/#{path} #{project_url} #{project_url}/-/tree/#{branch}{/dir} #{project_url}/-/blob/#{branch}{/dir}/{file}#L{line}"></head><body>go get #{Gitlab.config.gitlab.url}/#{path}</body></html>}
+ expected_body = %(<html><head><meta name="go-import" content="#{Gitlab.config.gitlab.host}/#{path} git #{repository_url}"><meta name="go-source" content="#{Gitlab.config.gitlab.host}/#{path} #{project_url} #{project_url}/-/tree/#{branch}{/dir} #{project_url}/-/blob/#{branch}{/dir}/{file}#L{line}"></head><body>go get #{Gitlab.config.gitlab.url}/#{path}</body></html>)
expect(response[2]).to eq([expected_body])
end
end
diff --git a/spec/lib/gitlab/middleware/multipart_spec.rb b/spec/lib/gitlab/middleware/multipart_spec.rb
index 509a4bb921b..b857ed47d42 100644
--- a/spec/lib/gitlab/middleware/multipart_spec.rb
+++ b/spec/lib/gitlab/middleware/multipart_spec.rb
@@ -33,7 +33,7 @@ RSpec.describe Gitlab::Middleware::Multipart do
let(:params) { upload_parameters_for(key: 'file', mode: mode, filename: filename, remote_id: remote_id).merge('file.path' => '/should/not/be/read') }
it 'builds an UploadedFile' do
- expect_uploaded_files(original_filename: filename, remote_id: remote_id, size: uploaded_file.size, params_path: %w(file))
+ expect_uploaded_files(original_filename: filename, remote_id: remote_id, size: uploaded_file.size, params_path: %w[file])
subject
end
@@ -61,7 +61,7 @@ RSpec.describe Gitlab::Middleware::Multipart do
let(:params) { upload_parameters_for(filepath: uploaded_filepath, key: 'file', mode: mode, filename: filename) }
it 'builds an UploadedFile' do
- expect_uploaded_files(filepath: uploaded_filepath, original_filename: filename, size: uploaded_file.size, params_path: %w(file))
+ expect_uploaded_files(filepath: uploaded_filepath, original_filename: filename, size: uploaded_file.size, params_path: %w[file])
subject
end
diff --git a/spec/lib/gitlab/middleware/path_traversal_check_spec.rb b/spec/lib/gitlab/middleware/path_traversal_check_spec.rb
index 3d334a60c49..91081cc88ea 100644
--- a/spec/lib/gitlab/middleware/path_traversal_check_spec.rb
+++ b/spec/lib/gitlab/middleware/path_traversal_check_spec.rb
@@ -55,6 +55,34 @@ RSpec.describe ::Gitlab::Middleware::PathTraversalCheck, feature_category: :shar
end
end
+ shared_examples 'excluded path' do
+ it 'measures and logs the execution time' do
+ expect(::Gitlab::PathTraversal)
+ .not_to receive(:check_path_traversal!)
+ expect(::Gitlab::AppLogger)
+ .to receive(:warn)
+ .with({ class_name: described_class.name, duration_ms: instance_of(Float) })
+ .and_call_original
+
+ expect(subject).to eq(fake_response)
+ end
+
+ context 'with log_execution_time_path_traversal_middleware disabled' do
+ before do
+ stub_feature_flags(log_execution_time_path_traversal_middleware: false)
+ end
+
+ it 'does nothing' do
+ expect(::Gitlab::PathTraversal)
+ .not_to receive(:check_path_traversal!)
+ expect(::Gitlab::AppLogger)
+ .not_to receive(:warn)
+
+ expect(subject).to eq(fake_response)
+ end
+ end
+ end
+
shared_examples 'path traversal' do
it 'logs the problem and measures the execution time' do
expect(::Gitlab::PathTraversal)
@@ -70,7 +98,8 @@ RSpec.describe ::Gitlab::Middleware::PathTraversalCheck, feature_category: :shar
class_name: described_class.name,
duration_ms: instance_of(Float),
message: described_class::PATH_TRAVERSAL_MESSAGE,
- fullpath: fullpath
+ fullpath: fullpath,
+ method: method.upcase
}).and_call_original
expect(subject).to eq(fake_response)
@@ -94,7 +123,8 @@ RSpec.describe ::Gitlab::Middleware::PathTraversalCheck, feature_category: :shar
.with({
class_name: described_class.name,
message: described_class::PATH_TRAVERSAL_MESSAGE,
- fullpath: fullpath
+ fullpath: fullpath,
+ method: method.upcase
}).and_call_original
expect(subject).to eq(fake_response)
@@ -110,23 +140,90 @@ RSpec.describe ::Gitlab::Middleware::PathTraversalCheck, feature_category: :shar
let(:method) { 'get' }
where(:path, :query_params, :shared_example_name) do
- '/foo/bar' | {} | 'no issue'
- '/foo/../bar' | {} | 'path traversal'
- '/foo%2Fbar' | {} | 'no issue'
- '/foo%2F..%2Fbar' | {} | 'path traversal'
- '/foo%252F..%252Fbar' | {} | 'no issue'
- '/foo/bar' | { x: 'foo' } | 'no issue'
- '/foo/bar' | { x: 'foo/../bar' } | 'path traversal'
- '/foo/bar' | { x: 'foo%2Fbar' } | 'no issue'
- '/foo/bar' | { x: 'foo%2F..%2Fbar' } | 'no issue'
- '/foo/bar' | { x: 'foo%252F..%252Fbar' } | 'no issue'
- '/foo%2F..%2Fbar' | { x: 'foo%252F..%252Fbar' } | 'path traversal'
+ '/foo/bar' | {} | 'no issue'
+ '/foo/../bar' | {} | 'path traversal'
+ '/foo%2Fbar' | {} | 'no issue'
+ '/foo%2F..%2Fbar' | {} | 'path traversal'
+ '/foo%252F..%252Fbar' | {} | 'no issue'
+
+ '/foo/bar' | { x: 'foo' } | 'no issue'
+ '/foo/bar' | { x: 'foo/../bar' } | 'path traversal'
+ '/foo/bar' | { x: 'foo%2Fbar' } | 'no issue'
+ '/foo/bar' | { x: 'foo%2F..%2Fbar' } | 'no issue'
+ '/foo/bar' | { x: 'foo%252F..%252Fbar' } | 'no issue'
+ '/foo%2F..%2Fbar' | { x: 'foo%252F..%252Fbar' } | 'path traversal'
end
with_them do
it_behaves_like params[:shared_example_name]
end
+ context 'for global search excluded paths' do
+ excluded_paths = %w[
+ /search
+ /search/count
+ /api/v4/search
+ /api/v4/search.json
+ /api/v4/projects/4/search
+ /api/v4/projects/4/search.json
+ /api/v4/projects/4/-/search
+ /api/v4/projects/4/-/search.json
+ /api/v4/projects/my%2Fproject/search
+ /api/v4/projects/my%2Fproject/search.json
+ /api/v4/projects/my%2Fproject/-/search
+ /api/v4/projects/my%2Fproject/-/search.json
+ /api/v4/groups/4/search
+ /api/v4/groups/4/search.json
+ /api/v4/groups/4/-/search
+ /api/v4/groups/4/-/search.json
+ /api/v4/groups/my%2Fgroup/search
+ /api/v4/groups/my%2Fgroup/search.json
+ /api/v4/groups/my%2Fgroup/-/search
+ /api/v4/groups/my%2Fgroup/-/search.json
+ ]
+ query_params_with_no_path_traversal = [
+ {},
+ { x: 'foo' },
+ { x: 'foo%2F..%2Fbar' },
+ { x: 'foo%2F..%2Fbar' },
+ { x: 'foo%252F..%252Fbar' }
+ ]
+ query_params_with_path_traversal = [
+ { x: 'foo/../bar' }
+ ]
+
+ excluded_paths.each do |excluded_path|
+ [query_params_with_no_path_traversal + query_params_with_path_traversal].flatten.each do |qp|
+ context "for excluded path #{excluded_path} with query params #{qp}" do
+ let(:query_params) { qp }
+ let(:path) { excluded_path }
+
+ it_behaves_like 'excluded path'
+ end
+ end
+
+ non_excluded_path = excluded_path.gsub('search', 'searchtest')
+
+ query_params_with_no_path_traversal.each do |qp|
+ context "for non excluded path #{non_excluded_path} with query params #{qp}" do
+ let(:query_params) { qp }
+ let(:path) { non_excluded_path }
+
+ it_behaves_like 'no issue'
+ end
+ end
+
+ query_params_with_path_traversal.each do |qp|
+ context "for non excluded path #{non_excluded_path} with query params #{qp}" do
+ let(:query_params) { qp }
+ let(:path) { non_excluded_path }
+
+ it_behaves_like 'path traversal'
+ end
+ end
+ end
+ end
+
context 'with a issues search path' do
let(:query_params) { {} }
let(:path) do
@@ -147,6 +244,7 @@ RSpec.describe ::Gitlab::Middleware::PathTraversalCheck, feature_category: :shar
'/foo%2Fbar' | {} | 'no issue'
'/foo%2F..%2Fbar' | {} | 'path traversal'
'/foo%252F..%252Fbar' | {} | 'no issue'
+
'/foo/bar' | { x: 'foo' } | 'no issue'
'/foo/bar' | { x: 'foo/../bar' } | 'no issue'
'/foo/bar' | { x: 'foo%2Fbar' } | 'no issue'
@@ -158,6 +256,59 @@ RSpec.describe ::Gitlab::Middleware::PathTraversalCheck, feature_category: :shar
with_them do
it_behaves_like params[:shared_example_name]
end
+
+ context 'for global search excluded paths' do
+ excluded_paths = %w[
+ /search
+ /search/count
+ /api/v4/search
+ /api/v4/search.json
+ /api/v4/projects/4/search
+ /api/v4/projects/4/search.json
+ /api/v4/projects/4/-/search
+ /api/v4/projects/4/-/search.json
+ /api/v4/projects/my%2Fproject/search
+ /api/v4/projects/my%2Fproject/search.json
+ /api/v4/projects/my%2Fproject/-/search
+ /api/v4/projects/my%2Fproject/-/search.json
+ /api/v4/groups/4/search
+ /api/v4/groups/4/search.json
+ /api/v4/groups/4/-/search
+ /api/v4/groups/4/-/search.json
+ /api/v4/groups/my%2Fgroup/search
+ /api/v4/groups/my%2Fgroup/search.json
+ /api/v4/groups/my%2Fgroup/-/search
+ /api/v4/groups/my%2Fgroup/-/search.json
+ ]
+ all_query_params = [
+ {},
+ { x: 'foo' },
+ { x: 'foo%2F..%2Fbar' },
+ { x: 'foo%2F..%2Fbar' },
+ { x: 'foo%252F..%252Fbar' },
+ { x: 'foo/../bar' }
+ ]
+
+ excluded_paths.each do |excluded_path|
+ all_query_params.each do |qp|
+ context "for excluded path #{excluded_path} with query params #{qp}" do
+ let(:query_params) { qp }
+ let(:path) { excluded_path }
+
+ it_behaves_like 'excluded path'
+ end
+
+ non_excluded_path = excluded_path.gsub('search', 'searchtest')
+
+ context "for non excluded path #{non_excluded_path} with query params #{qp}" do
+ let(:query_params) { qp }
+ let(:path) { excluded_path.gsub('search', 'searchtest') }
+
+ it_behaves_like 'no issue'
+ end
+ end
+ end
+ end
end
end
@@ -177,6 +328,12 @@ RSpec.describe ::Gitlab::Middleware::PathTraversalCheck, feature_category: :shar
'/foo/bar' | { x: 'foo%2Fbar' }
'/foo/bar' | { x: 'foo%2F..%2Fbar' }
'/foo/bar' | { x: 'foo%252F..%252Fbar' }
+ '/search' | { x: 'foo/../bar' }
+ '/search' | { x: 'foo%2F..%2Fbar' }
+ '/search' | { x: 'foo%252F..%252Fbar' }
+ '%2Fsearch' | { x: 'foo/../bar' }
+ '%2Fsearch' | { x: 'foo%2F..%2Fbar' }
+ '%2Fsearch' | { x: 'foo%252F..%252Fbar' }
end
with_them do
diff --git a/spec/lib/gitlab/omniauth_initializer_spec.rb b/spec/lib/gitlab/omniauth_initializer_spec.rb
index 1c665ec6e18..e12c0f4e78b 100644
--- a/spec/lib/gitlab/omniauth_initializer_spec.rb
+++ b/spec/lib/gitlab/omniauth_initializer_spec.rb
@@ -2,7 +2,9 @@
require 'spec_helper'
-RSpec.describe Gitlab::OmniauthInitializer do
+RSpec.describe Gitlab::OmniauthInitializer, feature_category: :system_access do
+ include LoginHelpers
+
let(:devise_config) { class_double(Devise) }
subject(:initializer) { described_class.new(devise_config) }
@@ -171,7 +173,7 @@ RSpec.describe Gitlab::OmniauthInitializer do
end
it 'allows "args" array for app_id and app_secret' do
- legacy_config = { 'name' => 'legacy', 'args' => %w(123 abc) }
+ legacy_config = { 'name' => 'legacy', 'args' => %w[123 abc] }
expect(devise_config).to receive(:omniauth).with(:legacy, '123', 'abc')
@@ -224,6 +226,119 @@ RSpec.describe Gitlab::OmniauthInitializer do
subject.execute([shibboleth_config])
end
+ context 'when SAML providers are configured' do
+ it 'configures default args for a single SAML provider' do
+ stub_omniauth_config(providers: [{ name: 'saml', args: { idp_sso_service_url: 'https://saml.example.com' } }])
+
+ expect(devise_config).to receive(:omniauth).with(
+ :saml,
+ {
+ idp_sso_service_url: 'https://saml.example.com',
+ attribute_statements: ::Gitlab::Auth::Saml::Config.default_attribute_statements
+ }
+ )
+
+ initializer.execute(Gitlab.config.omniauth.providers)
+ end
+
+ context 'when configuration provides matching keys' do
+ before do
+ stub_omniauth_config(
+ providers: [
+ {
+ name: 'saml',
+ args: { idp_sso_service_url: 'https://saml.example.com', attribute_statements: { email: ['custom_attr'] } }
+ }
+ ]
+ )
+ end
+
+ it 'merges arguments with user configuration preference' do
+ expect(devise_config).to receive(:omniauth).with(
+ :saml,
+ {
+ idp_sso_service_url: 'https://saml.example.com',
+ attribute_statements: ::Gitlab::Auth::Saml::Config.default_attribute_statements
+ .merge({ email: ['custom_attr'] })
+ }
+ )
+
+ initializer.execute(Gitlab.config.omniauth.providers)
+ end
+
+ it 'merges arguments with defaults preference when invert_omniauth_args_merging is not enabled' do
+ stub_feature_flags(invert_omniauth_args_merging: false)
+
+ expect(devise_config).to receive(:omniauth).with(
+ :saml,
+ {
+ idp_sso_service_url: 'https://saml.example.com',
+ attribute_statements: ::Gitlab::Auth::Saml::Config.default_attribute_statements
+ }
+ )
+
+ initializer.execute(Gitlab.config.omniauth.providers)
+ end
+ end
+
+ it 'configures defaults args for multiple SAML providers' do
+ stub_omniauth_config(
+ providers: [
+ { name: 'saml', args: { idp_sso_service_url: 'https://saml.example.com' } },
+ {
+ name: 'saml2',
+ args: { strategy_class: 'OmniAuth::Strategies::SAML', idp_sso_service_url: 'https://saml2.example.com' }
+ }
+ ]
+ )
+
+ expect(devise_config).to receive(:omniauth).with(
+ :saml,
+ {
+ idp_sso_service_url: 'https://saml.example.com',
+ attribute_statements: ::Gitlab::Auth::Saml::Config.default_attribute_statements
+ }
+ )
+ expect(devise_config).to receive(:omniauth).with(
+ :saml2,
+ {
+ idp_sso_service_url: 'https://saml2.example.com',
+ strategy_class: OmniAuth::Strategies::SAML,
+ attribute_statements: ::Gitlab::Auth::Saml::Config.default_attribute_statements
+ }
+ )
+
+ initializer.execute(Gitlab.config.omniauth.providers)
+ end
+
+ it 'merges arguments with user configuration preference for custom SAML provider' do
+ stub_omniauth_config(
+ providers: [
+ {
+ name: 'custom_saml',
+ args: {
+ strategy_class: 'OmniAuth::Strategies::SAML',
+ idp_sso_service_url: 'https://saml2.example.com',
+ attribute_statements: { email: ['custom_attr'] }
+ }
+ }
+ ]
+ )
+
+ expect(devise_config).to receive(:omniauth).with(
+ :custom_saml,
+ {
+ idp_sso_service_url: 'https://saml2.example.com',
+ strategy_class: OmniAuth::Strategies::SAML,
+ attribute_statements: ::Gitlab::Auth::Saml::Config.default_attribute_statements
+ .merge({ email: ['custom_attr'] })
+ }
+ )
+
+ initializer.execute(Gitlab.config.omniauth.providers)
+ end
+ end
+
it 'configures defaults for google_oauth2' do
google_config = {
'name' => 'google_oauth2',
diff --git a/spec/lib/gitlab/other_markup_spec.rb b/spec/lib/gitlab/other_markup_spec.rb
index 34f1e0cfbc5..266cb75a8ac 100644
--- a/spec/lib/gitlab/other_markup_spec.rb
+++ b/spec/lib/gitlab/other_markup_spec.rb
@@ -59,6 +59,31 @@ RSpec.describe Gitlab::OtherMarkup, feature_category: :wiki do
end
end
+ context 'when mediawiki content' do
+ links = {
+ 'p' => {
+ file: 'file.mediawiki',
+ input: 'Red Bridge (JRuby Embed)',
+ output: "\n<p>Red Bridge (JRuby Embed)</p>"
+ },
+ 'h1' => {
+ file: 'file.mediawiki',
+ input: '= Red Bridge (JRuby Embed) =',
+ output: "\n\n<h1>\n<a name=\"Red_Bridge_JRuby_Embed\"></a><span>Red Bridge (JRuby Embed)</span>\n</h1>\n"
+ },
+ 'h2' => {
+ file: 'file.mediawiki',
+ input: '== Red Bridge (JRuby Embed) ==',
+ output: "\n\n<h2>\n<a name=\"Red_Bridge_JRuby_Embed\"></a><span>Red Bridge (JRuby Embed)</span>\n</h2>\n"
+ }
+ }
+ links.each do |name, data|
+ it "does render into #{name} element" do
+ expect(render(data[:file], data[:input], context)).to eq(data[:output])
+ end
+ end
+ end
+
context 'when rendering takes too long' do
let_it_be(:file_name) { 'foo.bar' }
let_it_be(:project) { create(:project, :repository) }
@@ -86,6 +111,24 @@ RSpec.describe Gitlab::OtherMarkup, feature_category: :wiki do
end
end
+ context 'RedCloth markup' do
+ it 'renders textile correctly' do
+ test_text = '"This is *my* text."'
+ expected_res = "<p>&#8220;This is <strong>my</strong> text.&#8221;</p>"
+ expect(RedCloth.new(test_text).to_html).to eq(expected_res)
+ end
+
+ it 'protects against malicious backtracking' do
+ test_text = '<A' + ('A' * 54773)
+
+ expect do
+ Timeout.timeout(Gitlab::OtherMarkup::RENDER_TIMEOUT.seconds) do
+ RedCloth.new(test_text, [:sanitize_html]).to_html
+ end
+ end.not_to raise_error
+ end
+ end
+
def render(...)
described_class.render(...)
end
diff --git a/spec/lib/gitlab/pages/deployment_update_spec.rb b/spec/lib/gitlab/pages/deployment_update_spec.rb
index cf109248f36..9a7564ddd59 100644
--- a/spec/lib/gitlab/pages/deployment_update_spec.rb
+++ b/spec/lib/gitlab/pages/deployment_update_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Pages::DeploymentUpdate do
+RSpec.describe Gitlab::Pages::DeploymentUpdate, feature_category: :pages do
let_it_be(:project, refind: true) { create(:project, :repository) }
let_it_be(:old_pipeline) { create(:ci_pipeline, project: project, sha: project.commit('HEAD').sha) }
diff --git a/spec/lib/gitlab/pages/virtual_host_finder_spec.rb b/spec/lib/gitlab/pages/virtual_host_finder_spec.rb
index 8c34968bbfc..bc3f9d89b5f 100644
--- a/spec/lib/gitlab/pages/virtual_host_finder_spec.rb
+++ b/spec/lib/gitlab/pages/virtual_host_finder_spec.rb
@@ -5,10 +5,6 @@ require 'spec_helper'
RSpec.describe Gitlab::Pages::VirtualHostFinder, feature_category: :pages do
let_it_be(:project) { create(:project) }
- before_all do
- project.update_pages_deployment!(create(:pages_deployment, project: project))
- end
-
before do
stub_pages_setting(host: 'example.com')
end
@@ -24,10 +20,6 @@ RSpec.describe Gitlab::Pages::VirtualHostFinder, feature_category: :pages do
subject(:virtual_domain) { described_class.new(pages_domain.domain).execute }
context 'when there are no pages deployed for the project' do
- before_all do
- project.mark_pages_as_not_deployed
- end
-
it 'returns nil' do
expect(virtual_domain).to be_nil
end
@@ -35,7 +27,7 @@ RSpec.describe Gitlab::Pages::VirtualHostFinder, feature_category: :pages do
context 'when there are pages deployed for the project' do
before_all do
- project.mark_pages_as_deployed
+ create(:pages_deployment, project: project)
end
it 'returns the virual domain when there are pages deployed for the project' do
@@ -48,10 +40,6 @@ RSpec.describe Gitlab::Pages::VirtualHostFinder, feature_category: :pages do
context 'when host is a namespace domain' do
context 'when there are no pages deployed for the project' do
- before_all do
- project.mark_pages_as_not_deployed
- end
-
it 'returns no result if the provided host is not subdomain of the Pages host' do
virtual_domain = described_class.new("#{project.namespace.path}.something.io").execute
@@ -68,7 +56,7 @@ RSpec.describe Gitlab::Pages::VirtualHostFinder, feature_category: :pages do
context 'when there are pages deployed for the project' do
before_all do
- project.mark_pages_as_deployed
+ create(:pages_deployment, project: project)
project.namespace.update!(path: 'topNAMEspace')
end
@@ -109,10 +97,6 @@ RSpec.describe Gitlab::Pages::VirtualHostFinder, feature_category: :pages do
end
context 'when there are no pages deployed for the project' do
- before_all do
- project.mark_pages_as_not_deployed
- end
-
it 'returns nil' do
expect(virtual_domain).to be_nil
end
@@ -120,7 +104,7 @@ RSpec.describe Gitlab::Pages::VirtualHostFinder, feature_category: :pages do
context 'when there are pages deployed for the project' do
before_all do
- project.mark_pages_as_deployed
+ create(:pages_deployment, project: project)
end
it 'returns the virual domain when there are pages deployed for the project' do
@@ -133,9 +117,10 @@ RSpec.describe Gitlab::Pages::VirtualHostFinder, feature_category: :pages do
it 'prioritizes the unique domain project' do
group = create(:group, path: 'unique-domain')
other_project = build(:project, path: 'unique-domain.example.com', group: group)
- other_project.save!(validate: false)
- other_project.update_pages_deployment!(create(:pages_deployment, project: other_project))
- other_project.mark_pages_as_deployed
+ .tap { |project| project.save!(validate: false) }
+
+ create(:pages_deployment, project: project)
+ create(:pages_deployment, project: other_project)
expect(virtual_domain).to be_an_instance_of(Pages::VirtualDomain)
expect(virtual_domain.lookup_paths.first.project_id).to eq(project.id)
@@ -150,10 +135,6 @@ RSpec.describe Gitlab::Pages::VirtualHostFinder, feature_category: :pages do
end
context 'when there are no pages deployed for the project' do
- before_all do
- project.mark_pages_as_not_deployed
- end
-
it 'returns nil' do
expect(virtual_domain).to be_nil
end
@@ -161,7 +142,7 @@ RSpec.describe Gitlab::Pages::VirtualHostFinder, feature_category: :pages do
context 'when there are pages deployed for the project' do
before_all do
- project.mark_pages_as_deployed
+ create(:pages_deployment, project: project)
end
it 'returns nil' do
diff --git a/spec/lib/gitlab/pagination/cursor_based_keyset_spec.rb b/spec/lib/gitlab/pagination/cursor_based_keyset_spec.rb
index effe767e41d..e5958549a81 100644
--- a/spec/lib/gitlab/pagination/cursor_based_keyset_spec.rb
+++ b/spec/lib/gitlab/pagination/cursor_based_keyset_spec.rb
@@ -28,7 +28,9 @@ RSpec.describe Gitlab::Pagination::CursorBasedKeyset do
end
describe '.enforced_for_type?' do
- subject { described_class.enforced_for_type?(relation) }
+ let_it_be(:project) { create(:project) }
+
+ subject { described_class.enforced_for_type?(project, relation) }
context 'when relation is Group' do
let(:relation) { Group.all }
@@ -45,7 +47,21 @@ RSpec.describe Gitlab::Pagination::CursorBasedKeyset do
context 'when relation is Ci::Build' do
let(:relation) { Ci::Build.all }
- it { is_expected.to be false }
+ before do
+ stub_feature_flags(enforce_ci_builds_pagination_limit: false)
+ end
+
+ context 'when feature flag enforce_ci_builds_pagination_limit is enabled' do
+ before do
+ stub_feature_flags(enforce_ci_builds_pagination_limit: project)
+ end
+
+ it { is_expected.to be true }
+ end
+
+ context 'when feature fllag enforce_ci_builds_pagination_limit is disabled' do
+ it { is_expected.to be false }
+ end
end
end
diff --git a/spec/lib/gitlab/pagination/keyset/order_spec.rb b/spec/lib/gitlab/pagination/keyset/order_spec.rb
index 05bb0bb8b3a..52d2688c06e 100644
--- a/spec/lib/gitlab/pagination/keyset/order_spec.rb
+++ b/spec/lib/gitlab/pagination/keyset/order_spec.rb
@@ -726,7 +726,7 @@ RSpec.describe Gitlab::Pagination::Keyset::Order do
end
describe '#attribute_names' do
- let(:expected_attribute_names) { %w(id name) }
+ let(:expected_attribute_names) { %w[id name] }
let(:order) do
described_class.build(
[
diff --git a/spec/lib/gitlab/pagination/offset_header_builder_spec.rb b/spec/lib/gitlab/pagination/offset_header_builder_spec.rb
index a415bad5135..f43216702a8 100644
--- a/spec/lib/gitlab/pagination/offset_header_builder_spec.rb
+++ b/spec/lib/gitlab/pagination/offset_header_builder_spec.rb
@@ -15,11 +15,15 @@ RSpec.describe Gitlab::Pagination::OffsetHeaderBuilder do
describe '#execute' do
let(:basic_links) do
- %{<http://localhost?page=1&per_page=5>; rel="prev", <http://localhost?page=3&per_page=5>; rel="next", <http://localhost?page=1&per_page=5>; rel="first"}
+ [
+ %(<http://localhost?page=1&per_page=5>; rel="prev"),
+ %(<http://localhost?page=3&per_page=5>; rel="next"),
+ %(<http://localhost?page=1&per_page=5>; rel="first")
+ ].join(', ')
end
let(:last_link) do
- %{, <http://localhost?page=3&per_page=5>; rel="last"}
+ %(, <http://localhost?page=3&per_page=5>; rel="last")
end
def expect_basic_headers
diff --git a/spec/lib/gitlab/patch/sidekiq_scheduled_enq_spec.rb b/spec/lib/gitlab/patch/sidekiq_scheduled_enq_spec.rb
index f57257cd1c0..cd3718f5dcc 100644
--- a/spec/lib/gitlab/patch/sidekiq_scheduled_enq_spec.rb
+++ b/spec/lib/gitlab/patch/sidekiq_scheduled_enq_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe Gitlab::Patch::SidekiqScheduledEnq, :clean_gitlab_redis_queues, f
allow(Sidekiq).to receive(:load_json).and_return(payload)
# stub data in both namespaces
- Sidekiq.redis { |c| c.zadd('schedule', 100, 'dummy') }
+ Gitlab::Redis::Queues.with { |c| c.zadd('resque:gitlab:schedule', 100, 'dummy') }
Gitlab::Redis::Queues.with { |c| c.zadd('schedule', 100, 'dummy') }
end
@@ -26,7 +26,7 @@ RSpec.describe Gitlab::Patch::SidekiqScheduledEnq, :clean_gitlab_redis_queues, f
end
Gitlab::Redis::Queues.with do |conn|
- expect(conn.zcard('schedule')).to eq(0)
+ expect(conn.zcard('resque:gitlab:schedule')).to eq(0)
end
end
@@ -45,29 +45,13 @@ RSpec.describe Gitlab::Patch::SidekiqScheduledEnq, :clean_gitlab_redis_queues, f
end
Gitlab::Redis::Queues.with do |conn|
- expect(conn.zcard('schedule')).to eq(1)
+ expect(conn.zcard('resque:gitlab:schedule')).to eq(1)
end
end
end
- context 'when both envvar are enabled' do
- around do |example|
- # runs the zadd to ensure it goes into namespaced set
- Sidekiq.redis { |c| c.zadd('schedule', 100, 'dummy') }
-
- holder = Sidekiq.redis_pool
-
- # forcibly replace Sidekiq.redis since this is set in config/initializer/sidekiq.rb
- Sidekiq.redis = Gitlab::Redis::Queues.pool
-
- example.run
-
- ensure
- Sidekiq.redis = holder
- end
-
+ context 'when SIDEKIQ_ENABLE_DUAL_NAMESPACE_POLLING is enabled' do
before do
- stub_env('SIDEKIQ_ENQUEUE_NON_NAMESPACED', 'true')
stub_env('SIDEKIQ_ENABLE_DUAL_NAMESPACE_POLLING', 'true')
end
@@ -81,7 +65,7 @@ RSpec.describe Gitlab::Patch::SidekiqScheduledEnq, :clean_gitlab_redis_queues, f
end
Gitlab::Redis::Queues.with do |conn|
- expect(conn.zcard('schedule')).to eq(0)
+ expect(conn.zcard('resque:gitlab:schedule')).to eq(0)
end
end
end
diff --git a/spec/lib/gitlab/path_regex_spec.rb b/spec/lib/gitlab/path_regex_spec.rb
index 53dc145dcc4..eee05c0bb15 100644
--- a/spec/lib/gitlab/path_regex_spec.rb
+++ b/spec/lib/gitlab/path_regex_spec.rb
@@ -103,21 +103,27 @@ RSpec.describe Gitlab::PathRegex do
.concat(Array(API::API.prefix.to_s))
.concat(sitemap_words)
.concat(deprecated_routes)
+ .concat(reserved_routes)
.compact
.uniq
end
let(:sitemap_words) do
- %w(sitemap sitemap.xml sitemap.xml.gz)
+ %w[sitemap sitemap.xml sitemap.xml.gz]
end
let(:deprecated_routes) do
# profile was deprecated in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/51646
- %w(profile s)
+ %w[profile s]
+ end
+
+ let(:reserved_routes) do
+ # login was reserved in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134791
+ ['login']
end
let(:ee_top_level_words) do
- %w(unsubscribes v2)
+ %w[unsubscribes v2]
end
let(:files_in_public) do
@@ -126,7 +132,7 @@ RSpec.describe Gitlab::PathRegex do
.split("\n")
.map { |entry| entry.start_with?('public/-/') ? '-' : entry.gsub('public/', '') }
.uniq
- tracked + %w(assets uploads)
+ tracked + %w[assets uploads]
end
# All routes that start with a namespaced path, that have 1 or more
diff --git a/spec/lib/gitlab/popen_spec.rb b/spec/lib/gitlab/popen_spec.rb
index 0a186b07d19..78455cb705f 100644
--- a/spec/lib/gitlab/popen_spec.rb
+++ b/spec/lib/gitlab/popen_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe Gitlab::Popen do
context 'zero status' do
before do
- @output, @status = @klass.new.popen(%w(ls), path)
+ @output, @status = @klass.new.popen(%w[ls], path)
end
it { expect(@status).to be_zero }
@@ -33,7 +33,7 @@ RSpec.describe Gitlab::Popen do
context 'non-zero status' do
before do
- @output, @status = @klass.new.popen(%w(cat NOTHING), path)
+ @output, @status = @klass.new.popen(%w[cat NOTHING], path)
end
it { expect(@status).to eq(1) }
@@ -64,7 +64,7 @@ RSpec.describe Gitlab::Popen do
it 'calls popen3 with the provided environment variables' do
expect(Open3).to receive(:popen3).with(vars, 'ls', options)
- @output, @status = @klass.new.popen(%w(ls), path, { 'foobar' => 123 })
+ @output, @status = @klass.new.popen(%w[ls], path, { 'foobar' => 123 })
end
end
@@ -83,7 +83,7 @@ RSpec.describe Gitlab::Popen do
context 'without a directory argument' do
before do
- @output, @status = @klass.new.popen(%w(ls))
+ @output, @status = @klass.new.popen(%w[ls])
end
it { expect(@status).to be_zero }
diff --git a/spec/lib/gitlab/process_management_spec.rb b/spec/lib/gitlab/process_management_spec.rb
index fbd39702efb..709e4611954 100644
--- a/spec/lib/gitlab/process_management_spec.rb
+++ b/spec/lib/gitlab/process_management_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe Gitlab::ProcessManagement do
expect(described_class).to receive(:trap).ordered.with(:INT)
expect(described_class).to receive(:trap).ordered.with(:HUP)
- described_class.trap_signals(%i(INT HUP))
+ described_class.trap_signals(%i[INT HUP])
end
end
@@ -17,7 +17,7 @@ RSpec.describe Gitlab::ProcessManagement do
expect(described_class).to receive(:trap).ordered.with(:INT, 'DEFAULT')
expect(described_class).to receive(:trap).ordered.with(:HUP, 'DEFAULT')
- described_class.modify_signals(%i(INT HUP), 'DEFAULT')
+ described_class.modify_signals(%i[INT HUP], 'DEFAULT')
end
end
diff --git a/spec/lib/gitlab/process_supervisor_spec.rb b/spec/lib/gitlab/process_supervisor_spec.rb
index 18de5053362..94535e96843 100644
--- a/spec/lib/gitlab/process_supervisor_spec.rb
+++ b/spec/lib/gitlab/process_supervisor_spec.rb
@@ -2,7 +2,7 @@
require_relative '../../../lib/gitlab/process_supervisor'
-RSpec.describe Gitlab::ProcessSupervisor, feature_category: :application_performance do
+RSpec.describe Gitlab::ProcessSupervisor, feature_category: :cloud_connector do
let(:health_check_interval_seconds) { 0.1 }
let(:check_terminate_interval_seconds) { 1 }
let(:forwarded_signals) { [] }
@@ -152,13 +152,13 @@ RSpec.describe Gitlab::ProcessSupervisor, feature_category: :application_perform
end
context 'termination signals' do
- let(:term_signals) { %i(INT TERM) }
+ let(:term_signals) { %i[INT TERM] }
context 'when TERM results in timely shutdown of processes' do
it 'forwards them to observed processes without waiting for grace period to expire' do
allow(Gitlab::ProcessManagement).to receive(:any_alive?).and_return(false)
- expect(Gitlab::ProcessManagement).to receive(:trap_signals).ordered.with(%i(INT TERM)).and_yield(:TERM)
+ expect(Gitlab::ProcessManagement).to receive(:trap_signals).ordered.with(%i[INT TERM]).and_yield(:TERM)
expect(Gitlab::ProcessManagement).to receive(:signal_processes).ordered.with(process_ids, :TERM)
expect(supervisor).not_to receive(:sleep).with(check_terminate_interval_seconds)
@@ -168,7 +168,7 @@ RSpec.describe Gitlab::ProcessSupervisor, feature_category: :application_perform
context 'when TERM does not result in timely shutdown of processes' do
it 'issues a KILL signal after the grace period expires' do
- expect(Gitlab::ProcessManagement).to receive(:trap_signals).with(%i(INT TERM)).and_yield(:TERM)
+ expect(Gitlab::ProcessManagement).to receive(:trap_signals).with(%i[INT TERM]).and_yield(:TERM)
expect(Gitlab::ProcessManagement).to receive(:signal_processes).ordered.with(process_ids, :TERM)
expect(supervisor).to receive(:sleep).ordered.with(check_terminate_interval_seconds).at_least(:once)
expect(Gitlab::ProcessManagement).to receive(:signal_processes).ordered.with(process_ids, '-KILL')
@@ -179,10 +179,10 @@ RSpec.describe Gitlab::ProcessSupervisor, feature_category: :application_perform
end
context 'forwarded signals' do
- let(:forwarded_signals) { %i(USR1) }
+ let(:forwarded_signals) { %i[USR1] }
it 'forwards given signals to the observed processes' do
- expect(Gitlab::ProcessManagement).to receive(:trap_signals).with(%i(USR1)).and_yield(:USR1)
+ expect(Gitlab::ProcessManagement).to receive(:trap_signals).with(%i[USR1]).and_yield(:USR1)
expect(Gitlab::ProcessManagement).to receive(:signal_processes).ordered.with(process_ids, :USR1)
supervisor.supervise(process_ids) { [] }
diff --git a/spec/lib/gitlab/project_template_spec.rb b/spec/lib/gitlab/project_template_spec.rb
index 998fff12e94..07cdbf97091 100644
--- a/spec/lib/gitlab/project_template_spec.rb
+++ b/spec/lib/gitlab/project_template_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::ProjectTemplate do
+RSpec.describe Gitlab::ProjectTemplate, feature_category: :source_code_management do
include ProjectTemplateTestHelper
describe '.all' do
diff --git a/spec/lib/gitlab/quick_actions/extractor_spec.rb b/spec/lib/gitlab/quick_actions/extractor_spec.rb
index f91e8d2a7ef..063b416c514 100644
--- a/spec/lib/gitlab/quick_actions/extractor_spec.rb
+++ b/spec/lib/gitlab/quick_actions/extractor_spec.rb
@@ -246,7 +246,7 @@ RSpec.describe Gitlab::QuickActions::Extractor, feature_category: :team_planning
msg = %(hello\nworld\n/reopen\n/shrug this is great?\n/shrug meh)
msg, commands = extractor.extract_commands(msg)
- expect(commands).to eq [['reopen'], ['shrug', 'this is great?'], %w(shrug meh)]
+ expect(commands).to eq [['reopen'], ['shrug', 'this is great?'], %w[shrug meh]]
expect(msg).to eq "hello\nworld\nthis is great? SHRUG\nmeh SHRUG"
end
diff --git a/spec/lib/gitlab/redis/multi_store_spec.rb b/spec/lib/gitlab/redis/multi_store_spec.rb
index 1745a745ec3..6b1c0fb2e81 100644
--- a/spec/lib/gitlab/redis/multi_store_spec.rb
+++ b/spec/lib/gitlab/redis/multi_store_spec.rb
@@ -3,7 +3,6 @@
require 'spec_helper'
RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
- using RSpec::Parameterized::TableSyntax
include RedisHelpers
let_it_be(:redis_store_class) { define_helper_redis_store_class }
@@ -56,7 +55,6 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
context 'when primary_store is not a ::Redis instance' do
before do
allow(primary_store).to receive(:is_a?).with(::Redis).and_return(false)
- allow(primary_store).to receive(:is_a?).with(::Redis::Namespace).and_return(false)
end
it 'fails with exception' do
@@ -65,21 +63,9 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
end
end
- context 'when primary_store is a ::Redis::Namespace instance' do
- before do
- allow(primary_store).to receive(:is_a?).with(::Redis).and_return(false)
- allow(primary_store).to receive(:is_a?).with(::Redis::Namespace).and_return(true)
- end
-
- it 'fails with exception' do
- expect { described_class.new(primary_store, secondary_store, instance_name) }.not_to raise_error
- end
- end
-
context 'when secondary_store is not a ::Redis instance' do
before do
allow(secondary_store).to receive(:is_a?).with(::Redis).and_return(false)
- allow(secondary_store).to receive(:is_a?).with(::Redis::Namespace).and_return(false)
end
it 'fails with exception' do
@@ -88,130 +74,22 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
end
end
- context 'when secondary_store is a ::Redis::Namespace instance' do
- before do
- allow(secondary_store).to receive(:is_a?).with(::Redis).and_return(false)
- allow(secondary_store).to receive(:is_a?).with(::Redis::Namespace).and_return(true)
- end
-
- it 'fails with exception' do
- expect { described_class.new(primary_store, secondary_store, instance_name) }.not_to raise_error
- end
- end
-
# rubocop:disable RSpec/MultipleMemoizedHelpers
context 'with READ redis commands' do
subject do
multi_store.send(name, *args, **kwargs)
end
- let_it_be(:key1) { "redis:{1}:key_a" }
- let_it_be(:key2) { "redis:{1}:key_b" }
- let_it_be(:value1) { "redis_value1" }
- let_it_be(:value2) { "redis_value2" }
- let_it_be(:skey) { "redis:set:key" }
- let_it_be(:skey2) { "redis:set:key2" }
- let_it_be(:smemberargs) { [skey, value1] }
- let_it_be(:hkey) { "redis:hash:key" }
- let_it_be(:hkey2) { "redis:hash:key2" }
- let_it_be(:zkey) { "redis:sortedset:key" }
- let_it_be(:zkey2) { "redis:sortedset:key2" }
- let_it_be(:hitem1) { "item1" }
- let_it_be(:hitem2) { "item2" }
- let_it_be(:keys) { [key1, key2] }
- let_it_be(:values) { [value1, value2] }
- let_it_be(:svalues) { [value2, value1] }
- let_it_be(:hgetargs) { [hkey, hitem1] }
- let_it_be(:hmgetval) { [value1] }
- let_it_be(:mhmgetargs) { [hkey, hitem1] }
- let_it_be(:hvalmapped) { { "item1" => value1 } }
- let_it_be(:sscanargs) { [skey2, 0] }
- let_it_be(:sscanval) { ["0", [value1]] }
- let_it_be(:scanargs) { ["0"] }
- let_it_be(:scankwargs) { { match: '*:set:key2*' } }
- let_it_be(:scanval) { ["0", [skey2]] }
- let_it_be(:sscan_eachval) { [value1] }
- let_it_be(:sscan_each_arg) { { match: '*1*' } }
- let_it_be(:hscan_eachval) { [[hitem1, value1]] }
- let_it_be(:zscan_eachval) { [[value1, 1.0]] }
- let_it_be(:scan_each_arg) { { match: 'redis*' } }
- let_it_be(:scan_each_val) { [key1, key2, skey, skey2, hkey, hkey2, zkey, zkey2] }
-
- # rubocop:disable Layout/LineLength
- where(:case_name, :name, :args, :value, :kwargs, :block) do
- 'execute :get command' | :get | ref(:key1) | ref(:value1) | {} | nil
- 'execute :mget command' | :mget | ref(:keys) | ref(:values) | {} | nil
- 'execute :mget with block' | :mget | ref(:keys) | ref(:values) | {} | ->(value) { value }
- 'execute :smembers command' | :smembers | ref(:skey) | ref(:svalues) | {} | nil
- 'execute :scard command' | :scard | ref(:skey) | 2 | {} | nil
- 'execute :sismember command' | :sismember | ref(:smemberargs) | true | {} | nil
- 'execute :exists command' | :exists | ref(:key1) | 1 | {} | nil
- 'execute :exists? command' | :exists? | ref(:key1) | true | {} | nil
- 'execute :hget command' | :hget | ref(:hgetargs) | ref(:value1) | {} | nil
- 'execute :hlen command' | :hlen | ref(:hkey) | 1 | {} | nil
- 'execute :hgetall command' | :hgetall | ref(:hkey) | ref(:hvalmapped) | {} | nil
- 'execute :hexists command' | :hexists | ref(:hgetargs) | true | {} | nil
- 'execute :hmget command' | :hmget | ref(:hgetargs) | ref(:hmgetval) | {} | nil
- 'execute :mapped_hmget command' | :mapped_hmget | ref(:mhmgetargs) | ref(:hvalmapped) | {} | nil
- 'execute :sscan command' | :sscan | ref(:sscanargs) | ref(:sscanval) | {} | nil
- 'execute :scan command' | :scan | ref(:scanargs) | ref(:scanval) | ref(:scankwargs) | nil
-
- # we run *scan_each here as they are reads too
- 'execute :scan_each command' | :scan_each | nil | ref(:scan_each_val) | ref(:scan_each_arg) | nil
- 'execute :sscan_each command' | :sscan_each | ref(:skey2) | ref(:sscan_eachval) | {} | nil
- 'execute :sscan_each w block' | :sscan_each | ref(:skey) | ref(:sscan_eachval) | ref(:sscan_each_arg) | nil
- 'execute :hscan_each command' | :hscan_each | ref(:hkey) | ref(:hscan_eachval) | {} | nil
- 'execute :hscan_each w block' | :hscan_each | ref(:hkey2) | ref(:hscan_eachval) | ref(:sscan_each_arg) | nil
- 'execute :zscan_each command' | :zscan_each | ref(:zkey) | ref(:zscan_eachval) | {} | nil
- 'execute :zscan_each w block' | :zscan_each | ref(:zkey2) | ref(:zscan_eachval) | ref(:sscan_each_arg) | nil
- end
- # rubocop:enable Layout/LineLength
-
- before do
- primary_store.set(key1, value1)
- primary_store.set(key2, value2)
- primary_store.sadd?(skey, [value1, value2])
- primary_store.sadd?(skey2, [value1])
- primary_store.hset(hkey, hitem1, value1)
- primary_store.hset(hkey2, hitem1, value1, hitem2, value2)
- primary_store.zadd(zkey, 1, value1)
- primary_store.zadd(zkey2, [[1, value1], [2, value2]])
-
- secondary_store.set(key1, value1)
- secondary_store.set(key2, value2)
- secondary_store.sadd?(skey, [value1, value2])
- secondary_store.sadd?(skey2, [value1])
- secondary_store.hset(hkey, hitem1, value1)
- secondary_store.hset(hkey2, hitem1, value1, hitem2, value2)
- secondary_store.zadd(zkey, 1, value1)
- secondary_store.zadd(zkey2, [[1, value1], [2, value2]])
- end
-
- after do
- primary_store.flushdb
- secondary_store.flushdb
- end
-
- RSpec.shared_examples_for 'reads correct value' do
- it 'returns the correct value' do
- if value.is_a?(Array)
- # :smembers does not guarantee the order it will return the values (unsorted set)
- is_expected.to match_array(value)
- else
- is_expected.to eq(value)
- end
- end
- end
+ let(:args) { 'args' }
+ let(:kwargs) { { match: '*:set:key2*' } }
RSpec.shared_examples_for 'secondary store' do
it 'execute on the secondary instance' do
- expect(secondary_store).to receive(name).with(*expected_args).and_call_original
+ expect(secondary_store).to receive(name).with(*expected_args)
subject
end
- include_examples 'reads correct value'
-
it 'does not execute on the primary store' do
expect(primary_store).not_to receive(name)
@@ -219,23 +97,22 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
end
end
- with_them do
+ described_class::READ_COMMANDS.each do |name|
describe name.to_s do
- let(:expected_args) { kwargs&.present? ? [*args, { **kwargs }] : Array(args) }
+ let(:expected_args) { [*args, { **kwargs }] }
+ let(:name) { name }
before do
- allow(primary_store).to receive(name).and_call_original
- allow(secondary_store).to receive(name).and_call_original
+ allow(primary_store).to receive(name)
+ allow(secondary_store).to receive(name)
end
context 'when reading from the primary is successful' do
it 'returns the correct value' do
- expect(primary_store).to receive(name).with(*expected_args).and_call_original
+ expect(primary_store).to receive(name).with(*expected_args)
subject
end
-
- include_examples 'reads correct value'
end
context 'when reading from default instance is raising an exception' do
@@ -246,23 +123,13 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
it 'logs the exception and re-raises the error' do
expect(Gitlab::ErrorTracking).to receive(:log_exception).with(an_instance_of(StandardError),
- hash_including(:multi_store_error_message, instance_name: instance_name, command_name: name))
+ hash_including(:multi_store_error_message,
+ instance_name: instance_name, command_name: name))
expect { subject }.to raise_error(an_instance_of(StandardError))
end
end
- context 'when reading from empty default instance' do
- before do
- # this ensures a cache miss without having to stub the default store
- multi_store.default_store.flushdb
- end
-
- it 'does not call the non_default_store' do
- expect(multi_store.non_default_store).not_to receive(name)
- end
- end
-
context 'when the command is executed within pipelined block' do
subject do
multi_store.pipelined do |pipeline|
@@ -276,7 +143,7 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
2.times do
expect_next_instance_of(Redis::PipelinedConnection) do |pipeline|
- expect(pipeline).to receive(name).with(*expected_args).once.and_call_original
+ expect(pipeline).to receive(name).with(*expected_args).once
end
end
@@ -284,27 +151,16 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
end
end
- if params[:block]
+ context 'when block provided' do
subject do
- multi_store.send(name, *expected_args, &block)
+ multi_store.send(name, expected_args) { nil }
end
- context 'when block is provided' do
- it 'only default store yields to the block' do
- expect(primary_store).to receive(name).and_yield(value)
- expect(secondary_store).not_to receive(name).and_yield(value)
-
- subject
- end
-
- it 'only default store to execute' do
- expect(primary_store).to receive(name).with(*expected_args).and_call_original
- expect(secondary_store).not_to receive(name).with(*expected_args).and_call_original
-
- subject
- end
+ it 'only default store to execute' do
+ expect(primary_store).to receive(:send).with(name, expected_args)
+ expect(secondary_store).not_to receive(:send)
- include_examples 'reads correct value'
+ subject
end
end
@@ -327,8 +183,8 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
end
it 'executes only on secondary redis store', :aggregate_failures do
- expect(secondary_store).to receive(name).with(*expected_args).and_call_original
- expect(primary_store).not_to receive(name).with(*expected_args).and_call_original
+ expect(secondary_store).to receive(name).with(*expected_args)
+ expect(primary_store).not_to receive(name).with(*expected_args)
subject
end
@@ -336,8 +192,8 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
context 'when using primary store as default' do
it 'executes only on primary redis store', :aggregate_failures do
- expect(primary_store).to receive(name).with(*expected_args).and_call_original
- expect(secondary_store).not_to receive(name).with(*expected_args).and_call_original
+ expect(primary_store).to receive(name).with(*expected_args)
+ expect(secondary_store).not_to receive(name).with(*expected_args)
subject
end
@@ -429,110 +285,24 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
end
end
- RSpec.shared_examples_for 'verify that store contains values' do |store|
- it "#{store} redis store contains correct values", :aggregate_failures do
- subject
-
- redis_store = multi_store.send(store)
-
- if expected_value.is_a?(Array)
- # :smembers does not guarantee the order it will return the values
- expect(redis_store.send(verification_name, *verification_args)).to match_array(expected_value)
- else
- expect(redis_store.send(verification_name, *verification_args)).to eq(expected_value)
- end
- end
- end
-
- # rubocop:disable RSpec/MultipleMemoizedHelpers
context 'with WRITE redis commands' do
- let_it_be(:ikey1) { "counter1" }
- let_it_be(:ikey2) { "counter2" }
- let_it_be(:iargs) { [ikey2, 3] }
- let_it_be(:ivalue1) { "1" }
- let_it_be(:ivalue2) { "3" }
- let_it_be(:key1) { "redis:{1}:key_a" }
- let_it_be(:key2) { "redis:{1}:key_b" }
- let_it_be(:key3) { "redis:{1}:key_c" }
- let_it_be(:key4) { "redis:{1}:key_d" }
- let_it_be(:value1) { "redis_value1" }
- let_it_be(:value2) { "redis_value2" }
- let_it_be(:key1_value1) { [key1, value1] }
- let_it_be(:key1_value2) { [key1, value2] }
- let_it_be(:ttl) { 10 }
- let_it_be(:key1_ttl_value1) { [key1, ttl, value1] }
- let_it_be(:skey) { "redis:set:key" }
- let_it_be(:svalues1) { [value2, value1] }
- let_it_be(:svalues2) { [value1] }
- let_it_be(:skey_value1) { [skey, [value1]] }
- let_it_be(:skey_value2) { [skey, [value2]] }
- let_it_be(:script) { %(redis.call("set", "#{key1}", "#{value1}")) }
- let_it_be(:hkey1) { "redis:{1}:hash_a" }
- let_it_be(:hkey2) { "redis:{1}:hash_b" }
- let_it_be(:item) { "item" }
- let_it_be(:hdelarg) { [hkey1, item] }
- let_it_be(:hsetarg) { [hkey2, item, value1] }
- let_it_be(:mhsetarg) { [hkey2, { "item" => value1 }] }
- let_it_be(:hgetarg) { [hkey2, item] }
- let_it_be(:expireargs) { [key3, ttl] }
-
- # rubocop:disable Layout/LineLength
- where(:case_name, :name, :args, :expected_value, :verification_name, :verification_args) do
- 'execute :set command' | :set | ref(:key1_value1) | ref(:value1) | :get | ref(:key1)
- 'execute :setnx command' | :setnx | ref(:key1_value2) | ref(:value1) | :get | ref(:key2)
- 'execute :setex command' | :setex | ref(:key1_ttl_value1) | ref(:ttl) | :ttl | ref(:key1)
- 'execute :sadd command' | :sadd | ref(:skey_value2) | ref(:svalues1) | :smembers | ref(:skey)
- 'execute :sadd? command' | :sadd? | ref(:skey_value2) | ref(:svalues1) | :smembers | ref(:skey)
- 'execute :srem command' | :srem | ref(:skey_value1) | [] | :smembers | ref(:skey)
- 'execute :del command' | :del | ref(:key2) | nil | :get | ref(:key2)
- 'execute :unlink command' | :unlink | ref(:key3) | nil | :get | ref(:key3)
- 'execute :flushdb command' | :flushdb | nil | 0 | :dbsize | nil
- 'execute :eval command' | :eval | ref(:script) | ref(:value1) | :get | ref(:key1)
- 'execute :incr command' | :incr | ref(:ikey1) | ref(:ivalue1) | :get | ref(:ikey1)
- 'execute :incrby command' | :incrby | ref(:iargs) | ref(:ivalue2) | :get | ref(:ikey2)
- 'execute :hset command' | :hset | ref(:hsetarg) | ref(:value1) | :hget | ref(:hgetarg)
- 'execute :hdel command' | :hdel | ref(:hdelarg) | nil | :hget | ref(:hdelarg)
- 'execute :expire command' | :expire | ref(:expireargs) | ref(:ttl) | :ttl | ref(:key3)
- 'execute :mapped_hmset command' | :mapped_hmset | ref(:mhsetarg) | ref(:value1) | :hget | ref(:hgetarg)
- end
- # rubocop:enable Layout/LineLength
-
- before do
- primary_store.flushdb
- secondary_store.flushdb
-
- primary_store.set(key2, value1)
- primary_store.set(key3, value1)
- primary_store.set(key4, value1)
- primary_store.sadd?(skey, value1)
- primary_store.hset(hkey2, item, value1)
-
- secondary_store.set(key2, value1)
- secondary_store.set(key3, value1)
- secondary_store.set(key4, value1)
- secondary_store.sadd?(skey, value1)
- secondary_store.hset(hkey2, item, value1)
- end
-
- with_them do
+ described_class::WRITE_COMMANDS.each do |name|
describe name.to_s do
- let(:expected_args) { args || no_args }
+ let(:args) { "dummy_args" }
+ let(:name) { name }
before do
- allow(primary_store).to receive(name).and_call_original
- allow(secondary_store).to receive(name).and_call_original
+ allow(primary_store).to receive(name)
+ allow(secondary_store).to receive(name)
end
context 'when executing on primary instance is successful' do
it 'executes on both primary and secondary redis store', :aggregate_failures do
- expect(primary_store).to receive(name).with(*expected_args).and_call_original
- expect(secondary_store).to receive(name).with(*expected_args).and_call_original
+ expect(primary_store).to receive(name).with(*args)
+ expect(secondary_store).to receive(name).with(*args)
subject
end
-
- include_examples 'verify that store contains values', :primary_store
- include_examples 'verify that store contains values', :secondary_store
end
context 'when use_primary_and_secondary_stores feature flag is disabled' do
@@ -546,8 +316,8 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
end
it 'executes only on secondary redis store', :aggregate_failures do
- expect(secondary_store).to receive(name).with(*expected_args).and_call_original
- expect(primary_store).not_to receive(name).with(*expected_args).and_call_original
+ expect(secondary_store).to receive(name).with(*args)
+ expect(primary_store).not_to receive(name).with(*args)
subject
end
@@ -555,8 +325,8 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
context 'when using primary store as default' do
it 'executes only on primary redis store', :aggregate_failures do
- expect(primary_store).to receive(name).with(*expected_args).and_call_original
- expect(secondary_store).not_to receive(name).with(*expected_args).and_call_original
+ expect(primary_store).to receive(name).with(*args)
+ expect(secondary_store).not_to receive(name).with(*args)
subject
end
@@ -565,31 +335,30 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
context 'when executing on the default instance is raising an exception' do
before do
- allow(multi_store.default_store).to receive(name).with(*expected_args).and_raise(StandardError)
+ allow(multi_store.default_store).to receive(name).with(*args).and_raise(StandardError)
allow(Gitlab::ErrorTracking).to receive(:log_exception)
end
it 'raises error and does not execute on non default instance', :aggregate_failures do
- expect(multi_store.non_default_store).not_to receive(name).with(*expected_args)
+ expect(multi_store.non_default_store).not_to receive(name).with(*args)
expect { subject }.to raise_error(StandardError)
end
end
context 'when executing on the non default instance is raising an exception' do
before do
- allow(multi_store.non_default_store).to receive(name).with(*expected_args).and_raise(StandardError)
+ allow(multi_store.non_default_store).to receive(name).with(*args).and_raise(StandardError)
allow(Gitlab::ErrorTracking).to receive(:log_exception)
end
it 'logs the exception and execute on default instance', :aggregate_failures do
expect(Gitlab::ErrorTracking).to receive(:log_exception).with(an_instance_of(StandardError),
- hash_including(:multi_store_error_message, command_name: name, instance_name: instance_name))
- expect(multi_store.default_store).to receive(name).with(*expected_args).and_call_original
+ hash_including(:multi_store_error_message,
+ command_name: name, instance_name: instance_name))
+ expect(multi_store.default_store).to receive(name).with(*args)
subject
end
-
- include_examples 'verify that store contains values', :default_store
end
context 'when the command is executed within pipelined block' do
@@ -602,24 +371,35 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
it 'is executed only 1 time on each instance', :aggregate_failures do
expect(primary_store).to receive(:pipelined).and_call_original
expect_next_instance_of(Redis::PipelinedConnection) do |pipeline|
- expect(pipeline).to receive(name).with(*expected_args).once.and_call_original
+ expect(pipeline).to receive(name).with(*args).once
end
expect(secondary_store).to receive(:pipelined).and_call_original
expect_next_instance_of(Redis::PipelinedConnection) do |pipeline|
- expect(pipeline).to receive(name).with(*expected_args).once.and_call_original
+ expect(pipeline).to receive(name).with(*args).once
end
subject
end
-
- include_examples 'verify that store contains values', :primary_store
- include_examples 'verify that store contains values', :secondary_store
end
end
end
end
- # rubocop:enable RSpec/MultipleMemoizedHelpers
+
+ RSpec.shared_examples_for 'verify that store contains values' do |store|
+ it "#{store} redis store contains correct values", :aggregate_failures do
+ subject
+
+ redis_store = multi_store.send(store)
+
+ if expected_value.is_a?(Array)
+ # :smembers does not guarantee the order it will return the values
+ expect(redis_store.send(verification_name, *verification_args)).to match_array(expected_value)
+ else
+ expect(redis_store.send(verification_name, *verification_args)).to eq(expected_value)
+ end
+ end
+ end
RSpec.shared_examples_for 'pipelined command' do |name|
let_it_be(:key1) { "redis:{1}:key_a" }
@@ -951,11 +731,7 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
describe '#close' do
subject { multi_store.close }
- context 'when using both stores' do
- before do
- allow(multi_store).to receive(:use_primary_and_secondary_stores?).and_return(true)
- end
-
+ context 'when using both stores are different' do
it 'closes both stores' do
expect(primary_store).to receive(:close)
expect(secondary_store).to receive(:close)
@@ -964,36 +740,72 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
end
end
- context 'when using only one store' do
+ context 'when using identical stores' do
before do
- allow(multi_store).to receive(:use_primary_and_secondary_stores?).and_return(false)
+ allow(multi_store).to receive(:same_redis_store?).and_return(true)
end
- context 'when using primary_store as default store' do
- before do
- allow(multi_store).to receive(:use_primary_store_as_default?).and_return(true)
- end
+ it 'closes secondary store' do
+ expect(secondary_store).to receive(:close)
+ expect(primary_store).not_to receive(:close)
- it 'closes primary store' do
- expect(primary_store).to receive(:close)
- expect(secondary_store).not_to receive(:close)
+ subject
+ end
+ end
+ end
- subject
- end
+ describe '#blpop' do
+ let_it_be(:key) { "mylist" }
+
+ subject { multi_store.blpop(key, timeout: 0.1) }
+
+ shared_examples 'calls blpop on default_store' do
+ it 'calls blpop on default_store' do
+ expect(multi_store.default_store).to receive(:blpop).with(key, { timeout: 0.1 })
+
+ subject
end
+ end
- context 'when using secondary_store as default store' do
+ shared_examples 'does not call lpop on non_default_store' do
+ it 'does not call blpop on non_default_store' do
+ expect(multi_store.non_default_store).not_to receive(:blpop)
+
+ subject
+ end
+ end
+
+ context 'when using both stores' do
+ before do
+ allow(multi_store).to receive(:use_primary_and_secondary_stores?).and_return(true)
+ end
+
+ it_behaves_like 'calls blpop on default_store'
+
+ context "when an element exists in the default_store" do
before do
- allow(multi_store).to receive(:use_primary_store_as_default?).and_return(false)
+ multi_store.default_store.lpush(key, 'abc')
end
- it 'closes secondary store' do
- expect(primary_store).not_to receive(:close)
- expect(secondary_store).to receive(:close)
+ it 'calls lpop on non_default_store' do
+ expect(multi_store.non_default_store).to receive(:blpop).with(key, { timeout: 1 })
subject
end
end
+
+ context 'when the list is empty in default_store' do
+ it_behaves_like 'does not call lpop on non_default_store'
+ end
+ end
+
+ context 'when using one store' do
+ before do
+ allow(multi_store).to receive(:use_primary_and_secondary_stores?).and_return(false)
+ end
+
+ it_behaves_like 'calls blpop on default_store'
+ it_behaves_like 'does not call lpop on non_default_store'
end
end
@@ -1279,4 +1091,19 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
end
end
end
+
+ describe '*_COMMANDS' do
+ it 'checks if every command is only defined once' do
+ commands = [
+ described_class::REDIS_CLIENT_COMMANDS,
+ described_class::PUBSUB_SUBSCRIBE_COMMANDS,
+ described_class::READ_COMMANDS,
+ described_class::WRITE_COMMANDS,
+ described_class::PIPELINED_COMMANDS
+ ].inject([], :concat)
+ duplicated_commands = commands.group_by { |c| c }.select { |k, v| v.size > 1 }.map(&:first)
+
+ expect(duplicated_commands).to be_empty, "commands #{duplicated_commands} defined more than once"
+ end
+ end
end
diff --git a/spec/lib/gitlab/redis/pubsub_spec.rb b/spec/lib/gitlab/redis/pubsub_spec.rb
deleted file mode 100644
index e196d02116e..00000000000
--- a/spec/lib/gitlab/redis/pubsub_spec.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Redis::Pubsub, feature_category: :redis do
- include_examples "redis_new_instance_shared_examples", 'pubsub', Gitlab::Redis::SharedState
- include_examples "redis_shared_examples"
-end
diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb
index 37db13b76b9..9b8143f7bb8 100644
--- a/spec/lib/gitlab/reference_extractor_spec.rb
+++ b/spec/lib/gitlab/reference_extractor_spec.rb
@@ -31,7 +31,7 @@ RSpec.describe Gitlab::ReferenceExtractor do
project.add_reporter(@u_foo)
project.add_reporter(@u_bar)
- subject.analyze(%{
+ subject.analyze(%(
Inline code: `@foo`
Code block:
@@ -43,7 +43,7 @@ RSpec.describe Gitlab::ReferenceExtractor do
Quote:
> @offteam
- })
+ ))
expect(subject.users).to match_array([])
end
diff --git a/spec/lib/gitlab/regex_requires_app_spec.rb b/spec/lib/gitlab/regex_requires_app_spec.rb
index bea5d25dbc8..7a247e5e8cf 100644
--- a/spec/lib/gitlab/regex_requires_app_spec.rb
+++ b/spec/lib/gitlab/regex_requires_app_spec.rb
@@ -5,6 +5,18 @@ require 'spec_helper'
# Only specs that *cannot* be run with fast_spec_helper only
# See regex_spec for tests that do not require the full spec_helper
RSpec.describe Gitlab::Regex, feature_category: :tooling do
+ shared_examples_for 'npm package name regex' do
+ it { is_expected.to match('@scope/package') }
+ it { is_expected.to match('unscoped-package') }
+ it { is_expected.not_to match('@first-scope@second-scope/package') }
+ it { is_expected.not_to match('scope-without-at-symbol/package') }
+ it { is_expected.not_to match('@not-a-scoped-package') }
+ it { is_expected.not_to match('@scope/sub/package') }
+ it { is_expected.not_to match('@scope/../../package') }
+ it { is_expected.not_to match('@scope%2e%2e%2fpackage') }
+ it { is_expected.not_to match('@%2e%2e%2f/package') }
+ end
+
describe '.debian_architecture_regex' do
subject { described_class.debian_architecture_regex }
@@ -37,15 +49,7 @@ RSpec.describe Gitlab::Regex, feature_category: :tooling do
describe '.npm_package_name_regex' do
subject { described_class.npm_package_name_regex }
- it { is_expected.to match('@scope/package') }
- it { is_expected.to match('unscoped-package') }
- it { is_expected.not_to match('@first-scope@second-scope/package') }
- it { is_expected.not_to match('scope-without-at-symbol/package') }
- it { is_expected.not_to match('@not-a-scoped-package') }
- it { is_expected.not_to match('@scope/sub/package') }
- it { is_expected.not_to match('@scope/../../package') }
- it { is_expected.not_to match('@scope%2e%2e%2fpackage') }
- it { is_expected.not_to match('@%2e%2e%2f/package') }
+ it_behaves_like 'npm package name regex'
context 'capturing group' do
[
@@ -63,6 +67,24 @@ RSpec.describe Gitlab::Regex, feature_category: :tooling do
end
end
+ describe '.protection_rules_npm_package_name_pattern_regex' do
+ subject { described_class.protection_rules_npm_package_name_pattern_regex }
+
+ it_behaves_like 'npm package name regex'
+
+ it { is_expected.to match('@scope/package-*') }
+ it { is_expected.to match('@my-scope/*my-package-with-wildcard-inbetween') }
+ it { is_expected.to match('@my-scope/*my-package-with-wildcard-start') }
+ it { is_expected.to match('@my-scope/my-*package-*with-wildcard-multiple-*') }
+ it { is_expected.to match('@my-scope/my-package-with_____underscore') }
+ it { is_expected.to match('@my-scope/my-package-with-wildcard-end*') }
+ it { is_expected.to match('@my-scope/my-package-with-regex-characters.+') }
+
+ it { is_expected.not_to match('@my-scope/my-package-with-percent-sign-%') }
+ it { is_expected.not_to match('*@my-scope/my-package-with-wildcard-start') }
+ it { is_expected.not_to match('@my-scope/my-package-with-backslash-\*') }
+ end
+
describe '.debian_distribution_regex' do
subject { described_class.debian_distribution_regex }
diff --git a/spec/lib/gitlab/repository_cache_adapter_spec.rb b/spec/lib/gitlab/repository_cache_adapter_spec.rb
index 71a20cc58fd..c35038b3b75 100644
--- a/spec/lib/gitlab/repository_cache_adapter_spec.rb
+++ b/spec/lib/gitlab/repository_cache_adapter_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe Gitlab::RepositoryCacheAdapter do
include Gitlab::RepositoryCacheAdapter # can't use described_class here
def letters
- %w(b a c)
+ %w[b a c]
end
cache_method_as_redis_set(:letters)
@@ -47,11 +47,11 @@ RSpec.describe Gitlab::RepositoryCacheAdapter do
expect(fake_repository).to receive(:_uncached_letters).once.and_call_original
2.times do
- expect(fake_repository.letters).to eq(%w(a b c))
+ expect(fake_repository.letters).to eq(%w[a b c])
end
expect(redis_set_cache.exist?(:letters)).to eq(true)
- expect(fake_repository.instance_variable_get(:@letters)).to eq(%w(a b c))
+ expect(fake_repository.instance_variable_get(:@letters)).to eq(%w[a b c])
end
context 'membership checks' do
@@ -69,7 +69,7 @@ RSpec.describe Gitlab::RepositoryCacheAdapter do
context 'when the cache key exists' do
before do
- redis_set_cache.write(:letters, %w(b a c))
+ redis_set_cache.write(:letters, %w[b a c])
end
it 'calls #try_include? on the set cache' do
@@ -300,7 +300,7 @@ RSpec.describe Gitlab::RepositoryCacheAdapter do
expect(redis_set_cache).to receive(:expire).with(:branch_names)
expect(redis_hash_cache).to receive(:delete).with(:branch_names)
- repository.expire_method_caches(%i(branch_names))
+ repository.expire_method_caches(%i[branch_names])
end
it 'does not expire caches for non-existent methods' do
@@ -308,7 +308,7 @@ RSpec.describe Gitlab::RepositoryCacheAdapter do
expect(Gitlab::AppLogger).to(
receive(:error).with("Requested to expire non-existent method 'nonexistent' for Repository"))
- repository.expire_method_caches(%i(nonexistent))
+ repository.expire_method_caches(%i[nonexistent])
end
end
end
diff --git a/spec/lib/gitlab/repository_hash_cache_spec.rb b/spec/lib/gitlab/repository_hash_cache_spec.rb
index e3cc6ed69fb..4b4a2092c98 100644
--- a/spec/lib/gitlab/repository_hash_cache_spec.rb
+++ b/spec/lib/gitlab/repository_hash_cache_spec.rb
@@ -88,7 +88,7 @@ RSpec.describe Gitlab::RepositoryHashCache, :clean_gitlab_redis_repository_cache
describe "#read_members" do
subject { cache.read_members(:example, keys) }
- let(:keys) { %w(test missing) }
+ let(:keys) { %w[test missing] }
context "all data is cached" do
before do
@@ -140,7 +140,7 @@ RSpec.describe Gitlab::RepositoryHashCache, :clean_gitlab_redis_repository_cache
end
end
- let(:keys) { %w(test) }
+ let(:keys) { %w[test] }
it "records metrics" do
# Here we expect it to receive "test" as a missing key because we
@@ -151,7 +151,7 @@ RSpec.describe Gitlab::RepositoryHashCache, :clean_gitlab_redis_repository_cache
end
context "fully cached" do
- let(:keys) { %w(test another) }
+ let(:keys) { %w[test another] }
before do
cache.write(:example, test_hash.merge({ "another" => "not_missing" }))
@@ -169,7 +169,7 @@ RSpec.describe Gitlab::RepositoryHashCache, :clean_gitlab_redis_repository_cache
end
context "partially cached" do
- let(:keys) { %w(test missing) }
+ let(:keys) { %w[test missing] }
before do
cache.write(:example, test_hash)
@@ -187,7 +187,7 @@ RSpec.describe Gitlab::RepositoryHashCache, :clean_gitlab_redis_repository_cache
end
context "uncached" do
- let(:keys) { %w(test missing) }
+ let(:keys) { %w[test missing] }
it "returns a hash" do
is_expected.to eq({ "test" => "was_missing", "missing" => "was_missing" })
diff --git a/spec/lib/gitlab/repository_set_cache_spec.rb b/spec/lib/gitlab/repository_set_cache_spec.rb
index 23b2a2b9493..42378a16046 100644
--- a/spec/lib/gitlab/repository_set_cache_spec.rb
+++ b/spec/lib/gitlab/repository_set_cache_spec.rb
@@ -90,7 +90,7 @@ RSpec.describe Gitlab::RepositorySetCache, :clean_gitlab_redis_repository_cache,
end
context 'single key' do
- let(:keys) { %w(foo) }
+ let(:keys) { %w[foo] }
it { is_expected.to eq(1) }
@@ -102,7 +102,7 @@ RSpec.describe Gitlab::RepositorySetCache, :clean_gitlab_redis_repository_cache,
end
context 'multiple keys' do
- let(:keys) { %w(foo bar) }
+ let(:keys) { %w[foo bar] }
it { is_expected.to eq(2) }
diff --git a/spec/lib/gitlab/rugged_instrumentation_spec.rb b/spec/lib/gitlab/rugged_instrumentation_spec.rb
deleted file mode 100644
index 393bb957aba..00000000000
--- a/spec/lib/gitlab/rugged_instrumentation_spec.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::RuggedInstrumentation, :request_store do
- subject { described_class }
-
- describe '.query_time' do
- it 'increments query times' do
- subject.add_query_time(0.4510004)
- subject.add_query_time(0.3220004)
-
- expect(subject.query_time).to eq(0.773001)
- expect(subject.query_time_ms).to eq(773.0)
- end
- end
-
- describe '.increment_query_count' do
- it 'tracks query counts' do
- expect(subject.query_count).to eq(0)
-
- 2.times { subject.increment_query_count }
-
- expect(subject.query_count).to eq(2)
- end
- end
-end
diff --git a/spec/lib/gitlab/runtime_spec.rb b/spec/lib/gitlab/runtime_spec.rb
index 01cfa91bb59..05bcdf2fc96 100644
--- a/spec/lib/gitlab/runtime_spec.rb
+++ b/spec/lib/gitlab/runtime_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Runtime, feature_category: :application_performance do
+RSpec.describe Gitlab::Runtime, feature_category: :cloud_connector do
shared_examples "valid runtime" do |runtime, max_threads|
it "identifies itself" do
expect(subject.identify).to eq(runtime)
diff --git a/spec/lib/gitlab/search/abuse_detection_spec.rb b/spec/lib/gitlab/search/abuse_detection_spec.rb
index cbf20614ba5..d244234e763 100644
--- a/spec/lib/gitlab/search/abuse_detection_spec.rb
+++ b/spec/lib/gitlab/search/abuse_detection_spec.rb
@@ -20,7 +20,7 @@ RSpec.describe Gitlab::Search::AbuseDetection, feature_category: :global_search
end
describe 'abusive character matching' do
- refs = %w(
+ refs = %w[
main
тест
maiñ
@@ -30,7 +30,7 @@ RSpec.describe Gitlab::Search::AbuseDetection, feature_category: :global_search
feature/it_works
really_important!
测试
- )
+ ]
refs.each do |ref|
it "does match refs permitted by git refname: #{ref}" do
diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb
index 00e68f73d2d..a3acb8b92ed 100644
--- a/spec/lib/gitlab/search_results_spec.rb
+++ b/spec/lib/gitlab/search_results_spec.rb
@@ -99,7 +99,7 @@ RSpec.describe Gitlab::SearchResults, feature_category: :global_search do
describe '#aggregations' do
where(:scope) do
- %w(projects issues merge_requests blobs commits wiki_blobs epics milestones users unknown)
+ %w[projects issues merge_requests blobs commits wiki_blobs epics milestones users unknown]
end
with_them do
@@ -197,7 +197,7 @@ RSpec.describe Gitlab::SearchResults, feature_category: :global_search do
let(:query) { 'foo' }
include_examples 'search results filtered by state'
- include_examples 'search results filtered by archived', 'search_merge_requests_hide_archived_projects'
+ include_examples 'search results filtered by archived'
end
context 'ordering' do
@@ -244,7 +244,7 @@ RSpec.describe Gitlab::SearchResults, feature_category: :global_search do
include_examples 'search results filtered by state'
include_examples 'search results filtered by confidential'
- include_examples 'search results filtered by archived', 'search_issues_hide_archived_projects'
+ include_examples 'search results filtered by archived'
end
context 'ordering' do
diff --git a/spec/lib/gitlab/security/scan_configuration_spec.rb b/spec/lib/gitlab/security/scan_configuration_spec.rb
index faa8a206d74..9151db3c5ff 100644
--- a/spec/lib/gitlab/security/scan_configuration_spec.rb
+++ b/spec/lib/gitlab/security/scan_configuration_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe ::Gitlab::Security::ScanConfiguration do
let(:configured) { true }
context 'with a core scanner' do
- where(type: %i(sast sast_iac secret_detection container_scanning))
+ where(type: %i[sast sast_iac secret_detection container_scanning])
with_them do
it { is_expected.to be_truthy }
@@ -73,7 +73,7 @@ RSpec.describe ::Gitlab::Security::ScanConfiguration do
let(:configured) { true }
context 'with a core scanner' do
- where(type: %i(sast sast_iac secret_detection))
+ where(type: %i[sast sast_iac secret_detection])
with_them do
it { is_expected.to be_truthy }
diff --git a/spec/lib/gitlab/seeders/ci/catalog/resource_seeder_spec.rb b/spec/lib/gitlab/seeders/ci/catalog/resource_seeder_spec.rb
new file mode 100644
index 00000000000..4bd4455d1bd
--- /dev/null
+++ b/spec/lib/gitlab/seeders/ci/catalog/resource_seeder_spec.rb
@@ -0,0 +1,101 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::Gitlab::Seeders::Ci::Catalog::ResourceSeeder, feature_category: :pipeline_composition do
+ let_it_be(:admin) { create(:admin) }
+ let_it_be_with_reload(:group) { create(:group) }
+ let_it_be(:seed_count) { 2 }
+ let_it_be(:last_resource_id) { seed_count - 1 }
+
+ let(:group_path) { group.path }
+
+ subject(:seeder) { described_class.new(group_path: group_path, seed_count: seed_count) }
+
+ before_all do
+ group.add_owner(admin)
+ end
+
+ describe '#seed' do
+ subject(:seed) { seeder.seed }
+
+ context 'when the group does not exists' do
+ let(:group_path) { 'nonexistent_group' }
+
+ it 'skips seeding' do
+ expect { seed }.not_to change { Project.count }
+ end
+ end
+
+ context 'when project name already exists' do
+ before do
+ create(:project, namespace: group, name: "ci_seed_resource_0")
+ end
+
+ it 'skips that project creation and keeps seeding' do
+ expect { seed }.to change { Project.count }.by(seed_count - 1)
+ end
+ end
+
+ context 'when project.saved? fails' do
+ before do
+ project = build(:project, name: nil)
+
+ allow_next_instance_of(::Projects::CreateService) do |service|
+ allow(service).to receive(:execute).and_return(project)
+ end
+ end
+
+ it 'does not modify the projects count' do
+ expect { seed }.not_to change { Project.count }
+ end
+ end
+
+ context 'when ci resource creation fails' do
+ before do
+ allow_next_instance_of(::Ci::Catalog::Resources::CreateService) do |service|
+ allow(service).to receive(:execute).and_return(ServiceResponse.error(message: 'error'))
+ end
+ end
+
+ it 'does not add a catalog resource' do
+ expect { seed }.to change { Project.count }.by(seed_count)
+
+ expect(group.projects.all?(&:catalog_resource)).to eq false
+ end
+ end
+
+ it 'skips seeding a project if the project name already exists' do
+ # We call the same command twice, as it means it would try to recreate
+ # projects that were already created!
+ expect { seed }.to change { group.projects.count }.by(seed_count)
+ expect { seed }.to change { group.projects.count }.by(0)
+ end
+
+ it 'creates as many projects as specific in the argument' do
+ expect { seed }.to change {
+ group.projects.count
+ }.by(seed_count)
+
+ last_ci_resource = Project.last
+
+ expect(last_ci_resource.name).to eq "ci_seed_resource_#{last_resource_id}"
+ end
+
+ it 'adds a README and a template.yml file to the projects' do
+ seed
+ project = group.projects.last
+ default_branch = project.default_branch_or_main
+
+ expect(project.repository.blob_at(default_branch, "README.md")).not_to be_nil
+ expect(project.repository.blob_at(default_branch, "template.yml")).not_to be_nil
+ end
+
+ # This should be run again when fixing: https://gitlab.com/gitlab-org/gitlab/-/issues/429649
+ xit 'creates projects with CI catalog resources' do
+ expect { seed }.to change { Project.count }.by(seed_count)
+
+ expect(group.projects.all?(&:catalog_resource)).to eq true
+ end
+ end
+end
diff --git a/spec/lib/gitlab/shard_health_cache_spec.rb b/spec/lib/gitlab/shard_health_cache_spec.rb
index 0c25cc7dab5..8d0eebbb23e 100644
--- a/spec/lib/gitlab/shard_health_cache_spec.rb
+++ b/spec/lib/gitlab/shard_health_cache_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Gitlab::ShardHealthCache, :clean_gitlab_redis_cache do
- let(:shards) { %w(foo bar) }
+ let(:shards) { %w[foo bar] }
before do
described_class.update(shards) # rubocop:disable Rails/SaveBang
@@ -23,7 +23,7 @@ RSpec.describe Gitlab::ShardHealthCache, :clean_gitlab_redis_cache do
end
it 'replaces the existing set' do
- new_set = %w(test me more)
+ new_set = %w[test me more]
described_class.update(new_set) # rubocop:disable Rails/SaveBang
expect(described_class.cached_healthy_shards).to match_array(new_set)
diff --git a/spec/lib/gitlab/sidekiq_config/cli_methods_spec.rb b/spec/lib/gitlab/sidekiq_config/cli_methods_spec.rb
index 576b36c1829..1145fd02891 100644
--- a/spec/lib/gitlab/sidekiq_config/cli_methods_spec.rb
+++ b/spec/lib/gitlab/sidekiq_config/cli_methods_spec.rb
@@ -173,7 +173,7 @@ RSpec.describe Gitlab::SidekiqConfig::CliMethods do
end
it 'returns the queue names of matched workers' do
- expect(described_class.query_queues(query, worker_metadatas)).to match(%w(a a:2 c))
+ expect(described_class.query_queues(query, worker_metadatas)).to match(%w[a a:2 c])
end
end
end
diff --git a/spec/lib/gitlab/sidekiq_config/worker_matcher_spec.rb b/spec/lib/gitlab/sidekiq_config/worker_matcher_spec.rb
index dfe9358f70b..08ead3282fd 100644
--- a/spec/lib/gitlab/sidekiq_config/worker_matcher_spec.rb
+++ b/spec/lib/gitlab/sidekiq_config/worker_matcher_spec.rb
@@ -51,61 +51,61 @@ RSpec.describe Gitlab::SidekiqConfig::WorkerMatcher do
context 'with valid input' do
where(:query, :expected_metadatas) do
# worker_name
- 'worker_name=WorkerA' | %w(WorkerA)
- 'worker_name=WorkerA2' | %w(WorkerA2)
- 'worker_name=WorkerB|worker_name=WorkerD' | %w(WorkerB)
- 'worker_name!=WorkerA' | %w(WorkerA2 WorkerB WorkerC)
+ 'worker_name=WorkerA' | %w[WorkerA]
+ 'worker_name=WorkerA2' | %w[WorkerA2]
+ 'worker_name=WorkerB|worker_name=WorkerD' | %w[WorkerB]
+ 'worker_name!=WorkerA' | %w[WorkerA2 WorkerB WorkerC]
# feature_category
- 'feature_category=category_a' | %w(WorkerA WorkerA2)
- 'feature_category=category_a,category_c' | %w(WorkerA WorkerA2 WorkerC)
- 'feature_category=category_a|feature_category=category_c' | %w(WorkerA WorkerA2 WorkerC)
- 'feature_category!=category_a' | %w(WorkerB WorkerC)
+ 'feature_category=category_a' | %w[WorkerA WorkerA2]
+ 'feature_category=category_a,category_c' | %w[WorkerA WorkerA2 WorkerC]
+ 'feature_category=category_a|feature_category=category_c' | %w[WorkerA WorkerA2 WorkerC]
+ 'feature_category!=category_a' | %w[WorkerB WorkerC]
# has_external_dependencies
- 'has_external_dependencies=true' | %w(WorkerB)
- 'has_external_dependencies=false' | %w(WorkerA WorkerA2 WorkerC)
- 'has_external_dependencies=true,false' | %w(WorkerA WorkerA2 WorkerB WorkerC)
- 'has_external_dependencies=true|has_external_dependencies=false' | %w(WorkerA WorkerA2 WorkerB WorkerC)
- 'has_external_dependencies!=true' | %w(WorkerA WorkerA2 WorkerC)
+ 'has_external_dependencies=true' | %w[WorkerB]
+ 'has_external_dependencies=false' | %w[WorkerA WorkerA2 WorkerC]
+ 'has_external_dependencies=true,false' | %w[WorkerA WorkerA2 WorkerB WorkerC]
+ 'has_external_dependencies=true|has_external_dependencies=false' | %w[WorkerA WorkerA2 WorkerB WorkerC]
+ 'has_external_dependencies!=true' | %w[WorkerA WorkerA2 WorkerC]
# urgency
- 'urgency=high' | %w(WorkerA2 WorkerB)
- 'urgency=low' | %w(WorkerA)
- 'urgency=high,low,throttled' | %w(WorkerA WorkerA2 WorkerB WorkerC)
- 'urgency=low|urgency=throttled' | %w(WorkerA WorkerC)
- 'urgency!=high' | %w(WorkerA WorkerC)
+ 'urgency=high' | %w[WorkerA2 WorkerB]
+ 'urgency=low' | %w[WorkerA]
+ 'urgency=high,low,throttled' | %w[WorkerA WorkerA2 WorkerB WorkerC]
+ 'urgency=low|urgency=throttled' | %w[WorkerA WorkerC]
+ 'urgency!=high' | %w[WorkerA WorkerC]
# name
- 'name=a' | %w(WorkerA)
- 'name=a,b' | %w(WorkerA WorkerB)
- 'name=a,a:2|name=b' | %w(WorkerA WorkerA2 WorkerB)
- 'name!=a,a:2' | %w(WorkerB WorkerC)
+ 'name=a' | %w[WorkerA]
+ 'name=a,b' | %w[WorkerA WorkerB]
+ 'name=a,a:2|name=b' | %w[WorkerA WorkerA2 WorkerB]
+ 'name!=a,a:2' | %w[WorkerB WorkerC]
# resource_boundary
- 'resource_boundary=memory' | %w(WorkerB WorkerC)
- 'resource_boundary=memory,cpu' | %w(WorkerA WorkerB WorkerC)
- 'resource_boundary=memory|resource_boundary=cpu' | %w(WorkerA WorkerB WorkerC)
- 'resource_boundary!=memory,cpu' | %w(WorkerA2)
+ 'resource_boundary=memory' | %w[WorkerB WorkerC]
+ 'resource_boundary=memory,cpu' | %w[WorkerA WorkerB WorkerC]
+ 'resource_boundary=memory|resource_boundary=cpu' | %w[WorkerA WorkerB WorkerC]
+ 'resource_boundary!=memory,cpu' | %w[WorkerA2]
# tags
- 'tags=no_disk_io' | %w(WorkerA WorkerB)
- 'tags=no_disk_io,git_access' | %w(WorkerA WorkerA2 WorkerB)
- 'tags=no_disk_io|tags=git_access' | %w(WorkerA WorkerA2 WorkerB)
- 'tags=no_disk_io&tags=git_access' | %w(WorkerA)
- 'tags!=no_disk_io' | %w(WorkerA2 WorkerC)
- 'tags!=no_disk_io,git_access' | %w(WorkerC)
+ 'tags=no_disk_io' | %w[WorkerA WorkerB]
+ 'tags=no_disk_io,git_access' | %w[WorkerA WorkerA2 WorkerB]
+ 'tags=no_disk_io|tags=git_access' | %w[WorkerA WorkerA2 WorkerB]
+ 'tags=no_disk_io&tags=git_access' | %w[WorkerA]
+ 'tags!=no_disk_io' | %w[WorkerA2 WorkerC]
+ 'tags!=no_disk_io,git_access' | %w[WorkerC]
'tags=unknown_tag' | []
- 'tags!=no_disk_io' | %w(WorkerA2 WorkerC)
- 'tags!=no_disk_io,git_access' | %w(WorkerC)
- 'tags!=unknown_tag' | %w(WorkerA WorkerA2 WorkerB WorkerC)
+ 'tags!=no_disk_io' | %w[WorkerA2 WorkerC]
+ 'tags!=no_disk_io,git_access' | %w[WorkerC]
+ 'tags!=unknown_tag' | %w[WorkerA WorkerA2 WorkerB WorkerC]
# combinations
- 'feature_category=category_a&urgency=high' | %w(WorkerA2)
- 'feature_category=category_a&urgency=high|feature_category=category_c' | %w(WorkerA2 WorkerC)
+ 'feature_category=category_a&urgency=high' | %w[WorkerA2]
+ 'feature_category=category_a&urgency=high|feature_category=category_c' | %w[WorkerA2 WorkerC]
# Match all
- '*' | %w(WorkerA WorkerA2 WorkerB WorkerC)
+ '*' | %w[WorkerA WorkerA2 WorkerB WorkerC]
end
with_them do
diff --git a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb
index 4550ccc2fff..2e07fa100e8 100644
--- a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb
+++ b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb
@@ -181,7 +181,7 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
it 'logs without created_at and enqueued_at fields' do
travel_to(timestamp) do
- excluded_fields = %w(created_at enqueued_at args scheduling_latency_s)
+ excluded_fields = %w[created_at enqueued_at args scheduling_latency_s]
expect(logger).to receive(:info).with(start_payload.except(*excluded_fields)).ordered
expect(logger).to receive(:info).with(end_payload.except(*excluded_fields)).ordered
@@ -238,13 +238,11 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
end
end
- context 'with Gitaly, Rugged, and Redis calls' do
+ context 'with Gitaly, and Redis calls' do
let(:timing_data) do
{
gitaly_calls: 10,
gitaly_duration_s: 10000,
- rugged_calls: 1,
- rugged_duration_s: 5000,
redis_calls: 3,
redis_duration_s: 1234
}
@@ -261,7 +259,7 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
end
end
- it 'logs with Gitaly and Rugged timing data', :aggregate_failures do
+ it 'logs with Gitaly timing data', :aggregate_failures do
travel_to(timestamp) do
expect(logger).to receive(:info).with(start_payload).ordered
expect(logger).to receive(:info).with(expected_end_payload).ordered
diff --git a/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb b/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb
index 7138ad04f69..dbfab116479 100644
--- a/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb
@@ -106,9 +106,21 @@ RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob,
end
context 'when TTL option is not set' do
- let(:expected_ttl) { described_class::DEFAULT_DUPLICATE_KEY_TTL }
+ context 'when reduce_duplicate_job_key_ttl is enabled' do
+ let(:expected_ttl) { described_class::SHORT_DUPLICATE_KEY_TTL }
- it_behaves_like 'sets Redis keys with correct TTL'
+ it_behaves_like 'sets Redis keys with correct TTL'
+ end
+
+ context 'when reduce_duplicate_job_key_ttl is disabled' do
+ before do
+ stub_feature_flags(reduce_duplicate_job_key_ttl: false)
+ end
+
+ let(:expected_ttl) { described_class::DEFAULT_DUPLICATE_KEY_TTL }
+
+ it_behaves_like 'sets Redis keys with correct TTL'
+ end
end
context 'when TTL option is set' do
diff --git a/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb b/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb
index a27e723e392..9cf9901007c 100644
--- a/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
# rubocop: disable RSpec/MultipleMemoizedHelpers
-RSpec.describe Gitlab::SidekiqMiddleware::ServerMetrics do
+RSpec.describe Gitlab::SidekiqMiddleware::ServerMetrics, feature_category: :shared do
shared_examples "a metrics middleware" do
context "with mocked prometheus" do
include_context 'server metrics with mocked prometheus'
@@ -452,11 +452,6 @@ RSpec.describe Gitlab::SidekiqMiddleware::ServerMetrics do
end
context 'when emit_sidekiq_histogram_metrics FF is disabled' do
- include_context 'server metrics with mocked prometheus'
- include_context 'server metrics call' do
- let(:stub_subject) { false }
- end
-
subject(:middleware) { described_class.new }
let(:job) { {} }
@@ -484,16 +479,38 @@ RSpec.describe Gitlab::SidekiqMiddleware::ServerMetrics do
stub_feature_flags(emit_sidekiq_histogram_metrics: false)
end
+ # include_context below must run after stubbing FF above because
+ # the middleware initialization depends on the FF and it's being initialized
+ # in the 'server metrics call' shared_context
+ include_context 'server metrics with mocked prometheus'
+ include_context 'server metrics call'
+
it 'does not emit histogram metrics' do
expect(completion_seconds_metric).not_to receive(:observe)
expect(queue_duration_seconds).not_to receive(:observe)
expect(failed_total_metric).not_to receive(:increment)
+ expect(user_execution_seconds_metric).not_to receive(:observe)
+ expect(db_seconds_metric).not_to receive(:observe)
+ expect(gitaly_seconds_metric).not_to receive(:observe)
+ expect(redis_seconds_metric).not_to receive(:observe)
+ expect(elasticsearch_seconds_metric).not_to receive(:observe)
middleware.call(worker, job, queue) { nil }
end
- it 'emits sidekiq_jobs_completion_seconds_sum metric' do
+ it 'emits sidekiq_jobs_completion_seconds sum and count metric' do
expect(completion_seconds_sum_metric).to receive(:increment).with(labels, monotonic_time_duration)
+ expect(completion_count_metric).to receive(:increment).with(labels, 1)
+
+ middleware.call(worker, job, queue) { nil }
+ end
+
+ it 'emits resource usage sum metrics' do
+ expect(cpu_seconds_sum_metric).to receive(:increment).with(labels, thread_cputime_duration)
+ expect(db_seconds_sum_metric).to receive(:increment).with(labels, db_duration)
+ expect(gitaly_seconds_sum_metric).to receive(:increment).with(labels, gitaly_duration)
+ expect(redis_seconds_sum_metric).to receive(:increment).with(labels, redis_duration)
+ expect(elasticsearch_seconds_sum_metric).to receive(:increment).with(labels, elasticsearch_duration)
middleware.call(worker, job, queue) { nil }
end
diff --git a/spec/lib/gitlab/sidekiq_middleware/skip_jobs_spec.rb b/spec/lib/gitlab/sidekiq_middleware/skip_jobs_spec.rb
index 2fa0e44d44f..6df77c350e2 100644
--- a/spec/lib/gitlab/sidekiq_middleware/skip_jobs_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/skip_jobs_spec.rb
@@ -185,6 +185,21 @@ RSpec.describe Gitlab::SidekiqMiddleware::SkipJobs, feature_category: :scalabili
TestWorker.perform_async(*job['args'])
end
end
+
+ context 'when a block is provided' do
+ before do
+ TestWorker.defer_on_database_health_signal(*health_signal_attrs.values) do
+ [:gitlab_ci, [:ci_pipelines]]
+ end
+ end
+
+ it 'uses the lazy evaluated schema and tables returned by the block' do
+ expect(Gitlab::Database::HealthStatus::Context).to receive(:new)
+ .with(anything, anything, [:ci_pipelines], :gitlab_ci).and_call_original
+
+ expect { |b| subject.call(TestWorker.new, job, queue, &b) }.to yield_control
+ end
+ end
end
end
end
diff --git a/spec/lib/gitlab/sidekiq_middleware/worker_context/client_spec.rb b/spec/lib/gitlab/sidekiq_middleware/worker_context/client_spec.rb
index 4fbc64a45d6..0f8d84d13ec 100644
--- a/spec/lib/gitlab/sidekiq_middleware/worker_context/client_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/worker_context/client_spec.rb
@@ -73,7 +73,7 @@ RSpec.describe Gitlab::SidekiqMiddleware::WorkerContext::Client do
'job2' => build_stubbed(:user, username: 'user-2') }
TestWithContextWorker.bulk_perform_async_with_contexts(
- %w(job1 job2),
+ %w[job1 job2],
arguments_proc: -> (name) { [name, 1, 2, 3] },
context_proc: -> (name) { { user: user_per_job[name] } }
)
@@ -88,7 +88,7 @@ RSpec.describe Gitlab::SidekiqMiddleware::WorkerContext::Client do
context 'when the feature category is set in the context_proc' do
it 'takes the feature category from the worker, not the caller' do
TestWithContextWorker.bulk_perform_async_with_contexts(
- %w(job1 job2),
+ %w[job1 job2],
arguments_proc: -> (name) { [name, 1, 2, 3] },
context_proc: -> (_) { { feature_category: 'code_review' } }
)
@@ -102,7 +102,7 @@ RSpec.describe Gitlab::SidekiqMiddleware::WorkerContext::Client do
it 'takes the feature category from the caller if the worker is not owned' do
TestNotOwnedWithContextWorker.bulk_perform_async_with_contexts(
- %w(job1 job2),
+ %w[job1 job2],
arguments_proc: -> (name) { [name, 1, 2, 3] },
context_proc: -> (_) { { feature_category: 'code_review' } }
)
@@ -125,7 +125,7 @@ RSpec.describe Gitlab::SidekiqMiddleware::WorkerContext::Client do
it 'takes the feature category from the worker, not the caller' do
Gitlab::ApplicationContext.with_context(feature_category: 'system_access') do
TestWithContextWorker.bulk_perform_async_with_contexts(
- %w(job1 job2),
+ %w[job1 job2],
arguments_proc: -> (name) { [name, 1, 2, 3] },
context_proc: -> (_) { {} }
)
@@ -141,7 +141,7 @@ RSpec.describe Gitlab::SidekiqMiddleware::WorkerContext::Client do
it 'takes the feature category from the caller if the worker is not owned' do
Gitlab::ApplicationContext.with_context(feature_category: 'system_access') do
TestNotOwnedWithContextWorker.bulk_perform_async_with_contexts(
- %w(job1 job2),
+ %w[job1 job2],
arguments_proc: -> (name) { [name, 1, 2, 3] },
context_proc: -> (_) { {} }
)
diff --git a/spec/lib/gitlab/sidekiq_status_spec.rb b/spec/lib/gitlab/sidekiq_status_spec.rb
index 7f1504a8df9..a555e6a828a 100644
--- a/spec/lib/gitlab/sidekiq_status_spec.rb
+++ b/spec/lib/gitlab/sidekiq_status_spec.rb
@@ -55,13 +55,13 @@ RSpec.describe Gitlab::SidekiqStatus, :clean_gitlab_redis_queues, :clean_gitlab_
describe '.all_completed?' do
it 'returns true if all jobs have been completed' do
- expect(described_class.all_completed?(%w(123))).to eq(true)
+ expect(described_class.all_completed?(%w[123])).to eq(true)
end
it 'returns false if a job has not yet been completed' do
described_class.set('123')
- expect(described_class.all_completed?(%w(123 456))).to eq(false)
+ expect(described_class.all_completed?(%w[123 456])).to eq(false)
end
end
@@ -79,40 +79,40 @@ RSpec.describe Gitlab::SidekiqStatus, :clean_gitlab_redis_queues, :clean_gitlab_
describe '.num_running' do
it 'returns 0 if all jobs have been completed' do
- expect(described_class.num_running(%w(123))).to eq(0)
+ expect(described_class.num_running(%w[123])).to eq(0)
end
it 'returns 2 if two jobs are still running' do
described_class.set('123')
described_class.set('456')
- expect(described_class.num_running(%w(123 456 789))).to eq(2)
+ expect(described_class.num_running(%w[123 456 789])).to eq(2)
end
end
describe '.num_completed' do
it 'returns 1 if all jobs have been completed' do
- expect(described_class.num_completed(%w(123))).to eq(1)
+ expect(described_class.num_completed(%w[123])).to eq(1)
end
it 'returns 1 if a job has not yet been completed' do
described_class.set('123')
described_class.set('456')
- expect(described_class.num_completed(%w(123 456 789))).to eq(1)
+ expect(described_class.num_completed(%w[123 456 789])).to eq(1)
end
end
describe '.completed_jids' do
it 'returns the completed job' do
- expect(described_class.completed_jids(%w(123))).to eq(['123'])
+ expect(described_class.completed_jids(%w[123])).to eq(['123'])
end
it 'returns only the jobs completed' do
described_class.set('123')
described_class.set('456')
- expect(described_class.completed_jids(%w(123 456 789))).to eq(['789'])
+ expect(described_class.completed_jids(%w[123 456 789])).to eq(['789'])
end
end
@@ -122,7 +122,7 @@ RSpec.describe Gitlab::SidekiqStatus, :clean_gitlab_redis_queues, :clean_gitlab_
described_class.set('456')
described_class.unset('123')
- expect(described_class.job_status(%w(123 456 789))).to eq([false, true, false])
+ expect(described_class.job_status(%w[123 456 789])).to eq([false, true, false])
end
it 'handles an empty array' do
@@ -140,7 +140,7 @@ RSpec.describe Gitlab::SidekiqStatus, :clean_gitlab_redis_queues, :clean_gitlab_
expect(Gitlab::Redis::SidekiqStatus).to receive(:with).and_call_original
expect(Sidekiq).not_to receive(:redis)
- described_class.job_status(%w(123 456 789))
+ described_class.job_status(%w[123 456 789])
end
it_behaves_like 'tracking status in redis'
@@ -160,7 +160,7 @@ RSpec.describe Gitlab::SidekiqStatus, :clean_gitlab_redis_queues, :clean_gitlab_
expect(Sidekiq).to receive(:redis).and_call_original
expect(Gitlab::Redis::SidekiqStatus).not_to receive(:with)
- described_class.job_status(%w(123 456 789))
+ described_class.job_status(%w[123 456 789])
end
it_behaves_like 'tracking status in redis'
diff --git a/spec/lib/gitlab/ssh_public_key_spec.rb b/spec/lib/gitlab/ssh_public_key_spec.rb
index d4b0b1ea53b..df9f04eb7a0 100644
--- a/spec/lib/gitlab/ssh_public_key_spec.rb
+++ b/spec/lib/gitlab/ssh_public_key_spec.rb
@@ -87,28 +87,28 @@ RSpec.describe Gitlab::SSHPublicKey, lib: true, fips_mode: false do
describe '.supported_algorithms' do
it 'returns all supported algorithms' do
expect(described_class.supported_algorithms).to eq(
- %w(
+ %w[
ssh-rsa
ssh-dss
ecdsa-sha2-nistp256 ecdsa-sha2-nistp384 ecdsa-sha2-nistp521
ssh-ed25519
sk-ecdsa-sha2-nistp256@openssh.com
sk-ssh-ed25519@openssh.com
- )
+ ]
)
end
context 'FIPS mode', :fips_mode do
it 'returns all supported algorithms' do
expect(described_class.supported_algorithms).to eq(
- %w(
+ %w[
ssh-rsa
ssh-dss
ecdsa-sha2-nistp256 ecdsa-sha2-nistp384 ecdsa-sha2-nistp521
ssh-ed25519
sk-ecdsa-sha2-nistp256@openssh.com
sk-ssh-ed25519@openssh.com
- )
+ ]
)
end
end
@@ -117,12 +117,12 @@ RSpec.describe Gitlab::SSHPublicKey, lib: true, fips_mode: false do
describe '.supported_algorithms_for_name' do
where(:name, :algorithms) do
[
- [:rsa, %w(ssh-rsa)],
- [:dsa, %w(ssh-dss)],
- [:ecdsa, %w(ecdsa-sha2-nistp256 ecdsa-sha2-nistp384 ecdsa-sha2-nistp521)],
- [:ed25519, %w(ssh-ed25519)],
- [:ecdsa_sk, %w(sk-ecdsa-sha2-nistp256@openssh.com)],
- [:ed25519_sk, %w(sk-ssh-ed25519@openssh.com)]
+ [:rsa, %w[ssh-rsa]],
+ [:dsa, %w[ssh-dss]],
+ [:ecdsa, %w[ecdsa-sha2-nistp256 ecdsa-sha2-nistp384 ecdsa-sha2-nistp521]],
+ [:ed25519, %w[ssh-ed25519]],
+ [:ecdsa_sk, %w[sk-ecdsa-sha2-nistp256@openssh.com]],
+ [:ed25519_sk, %w[sk-ssh-ed25519@openssh.com]]
]
end
@@ -136,12 +136,12 @@ RSpec.describe Gitlab::SSHPublicKey, lib: true, fips_mode: false do
context 'FIPS mode', :fips_mode do
where(:name, :algorithms) do
[
- [:rsa, %w(ssh-rsa)],
- [:dsa, %w(ssh-dss)],
- [:ecdsa, %w(ecdsa-sha2-nistp256 ecdsa-sha2-nistp384 ecdsa-sha2-nistp521)],
- [:ed25519, %w(ssh-ed25519)],
- [:ecdsa_sk, %w(sk-ecdsa-sha2-nistp256@openssh.com)],
- [:ed25519_sk, %w(sk-ssh-ed25519@openssh.com)]
+ [:rsa, %w[ssh-rsa]],
+ [:dsa, %w[ssh-dss]],
+ [:ecdsa, %w[ecdsa-sha2-nistp256 ecdsa-sha2-nistp384 ecdsa-sha2-nistp521]],
+ [:ed25519, %w[ssh-ed25519]],
+ [:ecdsa_sk, %w[sk-ecdsa-sha2-nistp256@openssh.com]],
+ [:ed25519_sk, %w[sk-ssh-ed25519@openssh.com]]
]
end
@@ -194,7 +194,7 @@ RSpec.describe Gitlab::SSHPublicKey, lib: true, fips_mode: false do
context 'with a valid SSH key' do
where(:factory) do
- %i(rsa_key_2048
+ %i[rsa_key_2048
rsa_key_4096
rsa_key_5120
rsa_key_8192
@@ -202,7 +202,7 @@ RSpec.describe Gitlab::SSHPublicKey, lib: true, fips_mode: false do
ecdsa_key_256
ed25519_key_256
ecdsa_sk_key_256
- ed25519_sk_key_256)
+ ed25519_sk_key_256]
end
with_them do
diff --git a/spec/lib/gitlab/string_range_marker_spec.rb b/spec/lib/gitlab/string_range_marker_spec.rb
index 2ababd6a938..fcee64bc01c 100644
--- a/spec/lib/gitlab/string_range_marker_spec.rb
+++ b/spec/lib/gitlab/string_range_marker_spec.rb
@@ -14,10 +14,10 @@ RSpec.describe Gitlab::StringRangeMarker do
end
context "when the rich text is html safe" do
- let(:rich) { %{<span class="abc">abc</span><span class="space"> </span><span class="def">&lt;def&gt;</span>}.html_safe }
+ let(:rich) { %(<span class="abc">abc</span><span class="space"> </span><span class="def">&lt;def&gt;</span>).html_safe }
it 'marks the inline diffs' do
- expect(mark_diff(rich)).to eq(%{<span class="abc">abLEFTcRIGHT</span><span class="space">LEFT RIGHT</span><span class="def">LEFT&lt;dRIGHTef&gt;</span>})
+ expect(mark_diff(rich)).to eq(%(<span class="abc">abLEFTcRIGHT</span><span class="space">LEFT RIGHT</span><span class="def">LEFT&lt;dRIGHTef&gt;</span>))
expect(mark_diff(rich)).to be_html_safe
end
end
@@ -25,7 +25,7 @@ RSpec.describe Gitlab::StringRangeMarker do
context "when the rich text is not html safe" do
context 'when rich text equals raw text' do
it 'marks the inline diffs' do
- expect(mark_diff).to eq(%{abLEFTc <dRIGHTef>})
+ expect(mark_diff).to eq(%(abLEFTc <dRIGHTef>))
expect(mark_diff).not_to be_html_safe
end
end
@@ -34,7 +34,7 @@ RSpec.describe Gitlab::StringRangeMarker do
let(:rich) { "abc <def> differs" }
it 'marks the inline diffs' do
- expect(mark_diff(rich)).to eq(%{abLEFTc &lt;dRIGHTef&gt; differs})
+ expect(mark_diff(rich)).to eq(%(abLEFTc &lt;dRIGHTef&gt; differs))
expect(mark_diff(rich)).to be_html_safe
end
end
diff --git a/spec/lib/gitlab/string_regex_marker_spec.rb b/spec/lib/gitlab/string_regex_marker_spec.rb
index 393bfea7c6b..87df8b9baab 100644
--- a/spec/lib/gitlab/string_regex_marker_spec.rb
+++ b/spec/lib/gitlab/string_regex_marker_spec.rb
@@ -5,34 +5,34 @@ require 'fast_spec_helper'
RSpec.describe Gitlab::StringRegexMarker do
describe '#mark' do
context 'with a single occurrence' do
- let(:raw) { %{"name": "AFNetworking"} }
- let(:rich) { %{<span class="key">"name"</span><span class="punctuation">: </span><span class="value">"AFNetworking"</span>}.html_safe }
+ let(:raw) { %("name": "AFNetworking") }
+ let(:rich) { %(<span class="key">"name"</span><span class="punctuation">: </span><span class="value">"AFNetworking"</span>).html_safe }
subject do
described_class.new(raw, rich).mark(/"[^"]+":\s*"(?<name>[^"]+)"/, group: :name) do |text, left:, right:, mode:|
- %{<a href="#">#{text}</a>}.html_safe
+ %(<a href="#">#{text}</a>).html_safe
end
end
it 'marks the match' do
- expect(subject).to eq(%{<span class="key">"name"</span><span class="punctuation">: </span><span class="value">"<a href="#">AFNetworking</a>"</span>})
+ expect(subject).to eq(%(<span class="key">"name"</span><span class="punctuation">: </span><span class="value">"<a href="#">AFNetworking</a>"</span>))
expect(subject).to be_html_safe
end
end
context 'with multiple occurrences' do
- let(:raw) { %{a <b> <c> d} }
- let(:rich) { %{a &lt;b&gt; &lt;c&gt; d}.html_safe }
+ let(:raw) { %(a <b> <c> d) }
+ let(:rich) { %(a &lt;b&gt; &lt;c&gt; d).html_safe }
let(:regexp) { /<[a-z]>/ }
subject do
described_class.new(raw, rich).mark(regexp) do |text, left:, right:, mode:|
- %{<strong>#{text}</strong>}.html_safe
+ %(<strong>#{text}</strong>).html_safe
end
end
it 'marks the matches' do
- expect(subject).to eq(%{a <strong>&lt;b&gt;</strong> <strong>&lt;c&gt;</strong> d})
+ expect(subject).to eq(%(a <strong>&lt;b&gt;</strong> <strong>&lt;c&gt;</strong> d))
expect(subject).to be_html_safe
end
@@ -40,7 +40,7 @@ RSpec.describe Gitlab::StringRegexMarker do
let(:regexp) { Gitlab::UntrustedRegexp.new('<[a-z]>') }
it 'marks the matches' do
- expect(subject).to eq(%{a <strong>&lt;b&gt;</strong> <strong>&lt;c&gt;</strong> d})
+ expect(subject).to eq(%(a <strong>&lt;b&gt;</strong> <strong>&lt;c&gt;</strong> d))
expect(subject).to be_html_safe
end
end
diff --git a/spec/lib/gitlab/suggestions/suggestion_set_spec.rb b/spec/lib/gitlab/suggestions/suggestion_set_spec.rb
index 469646986e1..298ade2e33f 100644
--- a/spec/lib/gitlab/suggestions/suggestion_set_spec.rb
+++ b/spec/lib/gitlab/suggestions/suggestion_set_spec.rb
@@ -114,7 +114,7 @@ RSpec.describe Gitlab::Suggestions::SuggestionSet do
it 'returns an array of unique file paths associated with the suggestions' do
suggestion_set = described_class.new([suggestion, suggestion2, suggestion3])
- expected_paths = %w(files/ruby/popen.rb files/ruby/regex.rb)
+ expected_paths = %w[files/ruby/popen.rb files/ruby/regex.rb]
actual_paths = suggestion_set.file_paths
diff --git a/spec/lib/gitlab/task_helpers_spec.rb b/spec/lib/gitlab/task_helpers_spec.rb
index 0c43dd15e8c..448406dfb99 100644
--- a/spec/lib/gitlab/task_helpers_spec.rb
+++ b/spec/lib/gitlab/task_helpers_spec.rb
@@ -84,17 +84,17 @@ RSpec.describe Gitlab::TaskHelpers do
describe '#run_command' do
it 'runs command and return the output' do
- expect(subject.run_command(%w(echo it works!))).to eq("it works!\n")
+ expect(subject.run_command(%w[echo it works!])).to eq("it works!\n")
end
it 'returns empty string when command doesnt exist' do
- expect(subject.run_command(%w(nonexistentcommand with arguments))).to eq('')
+ expect(subject.run_command(%w[nonexistentcommand with arguments])).to eq('')
end
end
describe '#run_command!' do
it 'runs command and return the output' do
- expect(subject.run_command!(%w(echo it works!))).to eq("it works!\n")
+ expect(subject.run_command!(%w[echo it works!])).to eq("it works!\n")
end
it 'returns and exception when command exit with non zero code' do
diff --git a/spec/lib/gitlab/tracking/event_definition_spec.rb b/spec/lib/gitlab/tracking/event_definition_spec.rb
index b27aaa35695..ab0660147e4 100644
--- a/spec/lib/gitlab/tracking/event_definition_spec.rb
+++ b/spec/lib/gitlab/tracking/event_definition_spec.rb
@@ -15,8 +15,8 @@ RSpec.describe Gitlab::Tracking::EventDefinition do
product_stage: 'growth',
product_section: 'dev',
product_group: 'group::product analytics',
- distribution: %w(ee ce),
- tier: %w(free premium ultimate)
+ distribution: %w[ee ce],
+ tier: %w[free premium ultimate]
}
end
@@ -49,8 +49,8 @@ RSpec.describe Gitlab::Tracking::EventDefinition do
:product_stage | 1
:product_section | nil
:product_group | nil
- :distributions | %[be eb]
- :tiers | %[pro]
+ :distributions | %(be eb)
+ :tiers | %(pro)
end
with_them do
diff --git a/spec/lib/gitlab/url_builder_spec.rb b/spec/lib/gitlab/url_builder_spec.rb
index 68eb38a1335..81b70f85c3a 100644
--- a/spec/lib/gitlab/url_builder_spec.rb
+++ b/spec/lib/gitlab/url_builder_spec.rb
@@ -30,6 +30,7 @@ RSpec.describe Gitlab::UrlBuilder do
:project_snippet | ->(snippet) { "/#{snippet.project.full_path}/-/snippets/#{snippet.id}" }
:project_wiki | ->(wiki) { "/#{wiki.container.full_path}/-/wikis/home" }
:release | ->(release) { "/#{release.project.full_path}/-/releases/#{release.tag}" }
+ :organization | ->(organization) { "/-/organizations/#{organization.path}" }
:ci_build | ->(build) { "/#{build.project.full_path}/-/jobs/#{build.id}" }
:design | ->(design) { "/#{design.project.full_path}/-/design_management/designs/#{design.id}/raw_image" }
diff --git a/spec/lib/gitlab/url_sanitizer_spec.rb b/spec/lib/gitlab/url_sanitizer_spec.rb
index 2c2ef8f13fb..6a1521d9d72 100644
--- a/spec/lib/gitlab/url_sanitizer_spec.rb
+++ b/spec/lib/gitlab/url_sanitizer_spec.rb
@@ -8,10 +8,10 @@ RSpec.describe Gitlab::UrlSanitizer do
describe '.sanitize' do
def sanitize_url(url)
# We want to try with multi-line content because is how error messages are formatted
- described_class.sanitize(%{
+ described_class.sanitize(%(
remote: Not Found
fatal: repository `#{url}` not found
- })
+ ))
end
where(:input, :output) do
@@ -50,7 +50,7 @@ RSpec.describe Gitlab::UrlSanitizer do
false | '123://invalid:url'
false | 'valid@project:url.git'
false | 'valid:pass@project:url.git'
- false | %w(test array)
+ false | %w[test array]
true | 'ssh://example.com'
true | 'ssh://:@example.com'
true | 'ssh://foo@example.com'
@@ -74,7 +74,7 @@ RSpec.describe Gitlab::UrlSanitizer do
false | '123://invalid:url'
false | 'valid@project:url.git'
false | 'valid:pass@project:url.git'
- false | %w(test array)
+ false | %w[test array]
false | 'ssh://example.com'
false | 'ssh://:@example.com'
false | 'ssh://foo@example.com'
diff --git a/spec/lib/gitlab/usage/metric_definition_spec.rb b/spec/lib/gitlab/usage/metric_definition_spec.rb
index 51d3090c825..08adc031631 100644
--- a/spec/lib/gitlab/usage/metric_definition_spec.rb
+++ b/spec/lib/gitlab/usage/metric_definition_spec.rb
@@ -16,8 +16,8 @@ RSpec.describe Gitlab::Usage::MetricDefinition, feature_category: :service_ping
product_group: 'product_analytics',
time_frame: 'none',
data_source: 'database',
- distribution: %w(ee ce),
- tier: %w(free starter premium ultimate bronze silver gold),
+ distribution: %w[ee ce],
+ tier: %w[free starter premium ultimate bronze silver gold],
data_category: 'standard',
removed_by_url: 'http://gdk.test'
}
@@ -64,7 +64,7 @@ RSpec.describe Gitlab::Usage::MetricDefinition, feature_category: :service_ping
it 'includes metrics that are not removed' do
expect(described_class.not_removed.count).to eq(3)
- expect(described_class.not_removed.keys).to match_array(%w(metric1 metric2 metric3))
+ expect(described_class.not_removed.keys).to match_array(%w[metric1 metric2 metric3])
end
end
@@ -162,7 +162,7 @@ RSpec.describe Gitlab::Usage::MetricDefinition, feature_category: :service_ping
:data_source | nil
:distribution | nil
:distribution | 'test'
- :tier | %w(test ee)
+ :tier | %w[test ee]
:repair_issue_url | nil
:removed_by_url | 1
@@ -194,6 +194,156 @@ RSpec.describe Gitlab::Usage::MetricDefinition, feature_category: :service_ping
described_class.new(path, attributes).validate!
end
end
+
+ context 'when metric has removed status' do
+ before do
+ attributes[:status] = 'removed'
+ end
+
+ it 'raise dev exception when removed_by_url is not provided' do
+ attributes.delete(:removed_by_url)
+
+ expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).at_least(:once).with(instance_of(Gitlab::Usage::MetricDefinition::InvalidError))
+
+ described_class.new(path, attributes).validate!
+ end
+
+ it 'raises dev exception when milestone_removed is not provided' do
+ attributes.delete(:milestone_removed)
+
+ expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).at_least(:once).with(instance_of(Gitlab::Usage::MetricDefinition::InvalidError))
+
+ described_class.new(path, attributes).validate!
+ end
+ end
+
+ context 'internal metric' do
+ before do
+ attributes[:data_source] = 'internal_events'
+ end
+
+ where(:instrumentation_class, :options, :events, :is_valid) do
+ 'AnotherClass' | { events: ['a'] } | [{ name: 'a', unique: 'user.id' }] | false
+ nil | { events: ['a'] } | [{ name: 'a', unique: 'user.id' }] | false
+ 'RedisHLLMetric' | { events: ['a'] } | [{ name: 'a', unique: 'user.id' }] | true
+ 'RedisHLLMetric' | { events: ['a'] } | nil | false
+ 'RedisHLLMetric' | nil | [{ name: 'a', unique: 'user.id' }] | false
+ 'RedisHLLMetric' | { events: ['a'] } | [{ name: 'a', unique: 'a' }] | false
+ 'RedisHLLMetric' | { events: 'a' } | [{ name: 'a', unique: 'user.id' }] | false
+ 'RedisHLLMetric' | { events: [2] } | [{ name: 'a', unique: 'user.id' }] | false
+ 'RedisHLLMetric' | { events: ['a'], a: 'b' } | [{ name: 'a', unique: 'user.id' }] | false
+ 'RedisHLLMetric' | { events: ['a'] } | [{ name: 'a', unique: 'user.id', b: 'c' }] | false
+ 'RedisHLLMetric' | { events: ['a'] } | [{ name: 'a' }] | false
+ 'RedisHLLMetric' | { events: ['a'] } | [{ unique: 'user.id' }] | false
+ 'TotalCountMetric' | { events: ['a'] } | [{ name: 'a' }] | true
+ 'TotalCountMetric' | { events: ['a'] } | [{ name: 'a', unique: 'user.id' }] | false
+ 'TotalCountMetric' | { events: ['a'] } | nil | false
+ 'TotalCountMetric' | nil | [{ name: 'a' }] | false
+ 'TotalCountMetric' | { events: [2] } | [{ name: 'a' }] | false
+ 'TotalCountMetric' | { events: ['a'] } | [{}] | false
+ 'TotalCountMetric' | 'a' | [{ name: 'a' }] | false
+ 'TotalCountMetric' | { events: ['a'], a: 'b' } | [{ name: 'a' }] | false
+ end
+
+ with_them do
+ it 'raises dev exception when invalid' do
+ attributes[:instrumentation_class] = instrumentation_class if instrumentation_class
+ attributes[:options] = options if options
+ attributes[:events] = events if events
+
+ if is_valid
+ expect(Gitlab::ErrorTracking).not_to receive(:track_and_raise_for_dev_exception)
+ else
+ expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).at_least(:once).with(instance_of(Gitlab::Usage::MetricDefinition::InvalidError))
+ end
+
+ described_class.new(path, attributes).validate!
+ end
+ end
+ end
+
+ context 'Redis metric' do
+ before do
+ attributes[:data_source] = 'redis'
+ end
+
+ where(:instrumentation_class, :options, :is_valid) do
+ 'AnotherClass' | { event: 'a', widget: 'b' } | false
+ 'MergeRequestWidgetExtensionMetric' | { event: 'a', widget: 'b' } | true
+ 'MergeRequestWidgetExtensionMetric' | { event: 'a', widget: 2 } | false
+ 'MergeRequestWidgetExtensionMetric' | { event: 'a', widget: 'b', c: 'd' } | false
+ 'MergeRequestWidgetExtensionMetric' | { event: 'a' } | false
+ 'MergeRequestWidgetExtensionMetric' | { widget: 'b' } | false
+ 'RedisMetric' | { event: 'a', prefix: 'b', include_usage_prefix: true } | true
+ 'RedisMetric' | { event: 'a', prefix: nil, include_usage_prefix: true } | true
+ 'RedisMetric' | { event: 'a', prefix: 'b', include_usage_prefix: 2 } | false
+ 'RedisMetric' | { event: 'a', prefix: 'b', include_usage_prefix: true, c: 'd' } | false
+ 'RedisMetric' | { prefix: 'b', include_usage_prefix: true } | false
+ 'RedisMetric' | { event: 'a', include_usage_prefix: true } | false
+ 'RedisMetric' | { event: 'a', prefix: 'b' } | true
+ end
+
+ with_them do
+ it 'validates properly' do
+ attributes[:instrumentation_class] = instrumentation_class
+ attributes[:options] = options
+
+ if is_valid
+ expect(Gitlab::ErrorTracking).not_to receive(:track_and_raise_for_dev_exception)
+ else
+ expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).at_least(:once).with(instance_of(Gitlab::Usage::MetricDefinition::InvalidError))
+ end
+
+ described_class.new(path, attributes).validate!
+ end
+ end
+ end
+
+ context 'RedisHLL metric' do
+ before do
+ attributes[:data_source] = 'redis_hll'
+ end
+
+ where(:instrumentation_class, :options, :is_valid) do
+ 'AnotherClass' | { events: ['a'] } | false
+ 'RedisHLLMetric' | { events: ['a'] } | true
+ 'RedisHLLMetric' | nil | false
+ 'RedisHLLMetric' | {} | false
+ 'RedisHLLMetric' | { events: ['a'], b: 'c' } | false
+ 'RedisHLLMetric' | { events: [2] } | false
+ 'RedisHLLMetric' | { events: 'a' } | false
+ 'RedisHLLMetric' | { event: ['a'] } | false
+ 'AggregatedMetric' | { aggregate: { operator: 'OR', attribute: 'user_id' }, events: ['a'] } | true
+ 'AggregatedMetric' | { aggregate: { operator: 'AND', attribute: 'project_id' }, events: %w[b c] } | true
+ 'AggregatedMetric' | nil | false
+ 'AggregatedMetric' | {} | false
+ 'AggregatedMetric' | { aggregate: { operator: 'OR', attribute: 'user_id' }, events: ['a'], event: 'a' } | false
+ 'AggregatedMetric' | { aggregate: { operator: 'OR', attribute: 'user_id' } } | false
+ 'AggregatedMetric' | { events: ['a'] } | false
+ 'AggregatedMetric' | { aggregate: { operator: 'OR', attribute: 'user_id' }, events: 'a' } | false
+ 'AggregatedMetric' | { aggregate: 'a', events: ['a'] } | false
+ 'AggregatedMetric' | { aggregate: { operator: 'OR' }, events: ['a'] } | false
+ 'AggregatedMetric' | { aggregate: { attribute: 'user_id' }, events: ['a'] } | false
+ 'AggregatedMetric' | { aggregate: { operator: 'OR', attribute: 'user_id', a: 'b' }, events: ['a'] } | false
+ 'AggregatedMetric' | { aggregate: { operator: '???', attribute: 'user_id' }, events: ['a'] } | false
+ 'AggregatedMetric' | { aggregate: { operator: 'OR', attribute: ['user_id'] }, events: ['a'] } | false
+ end
+
+ with_them do
+ it 'validates properly' do
+ attributes[:instrumentation_class] = instrumentation_class
+ attributes[:options] = options
+
+ if is_valid
+ expect(Gitlab::ErrorTracking).not_to receive(:track_and_raise_for_dev_exception)
+ else
+ expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).at_least(:once).with(instance_of(Gitlab::Usage::MetricDefinition::InvalidError))
+ end
+
+ described_class.new(path, attributes).validate!
+ end
+ end
+ end
end
end
@@ -213,10 +363,10 @@ RSpec.describe Gitlab::Usage::MetricDefinition, feature_category: :service_ping
end
context 'when metric is using new format' do
- let(:attributes) { { events: [{ name: 'my_event', unique: 'user_id' }] } }
+ let(:attributes) { { events: [{ name: 'my_event', unique: 'user.id' }] } }
it 'returns a correct hash' do
- expect(definition.events).to eq({ 'my_event' => :user_id })
+ expect(definition.events).to eq({ 'my_event' => :'user.id' })
end
end
@@ -309,8 +459,8 @@ RSpec.describe Gitlab::Usage::MetricDefinition, feature_category: :service_ping
product_group: 'product_analytics',
time_frame: 'none',
data_source: 'database',
- distribution: %w(ee ce),
- tier: %w(free starter premium ultimate bronze silver gold),
+ distribution: %w[ee ce],
+ tier: %w[free starter premium ultimate bronze silver gold],
data_category: 'optional'
}
end
diff --git a/spec/lib/gitlab/usage/metric_spec.rb b/spec/lib/gitlab/usage/metric_spec.rb
index a4135b143dd..42d2f394ce3 100644
--- a/spec/lib/gitlab/usage/metric_spec.rb
+++ b/spec/lib/gitlab/usage/metric_spec.rb
@@ -18,8 +18,8 @@ RSpec.describe Gitlab::Usage::Metric do
time_frame: "all",
data_source: "database",
instrumentation_class: "CountIssuesMetric",
- distribution: %w(ce ee),
- tier: %w(free premium ultimate)
+ distribution: %w[ce ee],
+ tier: %w[free premium ultimate]
}
end
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/aggregated_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/aggregated_metric_spec.rb
index 3e7b13e21c1..f6b9da68184 100644
--- a/spec/lib/gitlab/usage/metrics/instrumentations/aggregated_metric_spec.rb
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/aggregated_metric_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe Gitlab::Usage::Metrics::Instrumentations::AggregatedMetric, :clean_gitlab_redis_shared_state do
+RSpec.describe Gitlab::Usage::Metrics::Instrumentations::AggregatedMetric, :clean_gitlab_redis_shared_state,
+ feature_category: :service_ping do
using RSpec::Parameterized::TableSyntax
before do
# weekly AND 1 weekly OR 2
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/count_projects_with_jira_dvcs_integration_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/count_projects_with_jira_dvcs_integration_metric_spec.rb
deleted file mode 100644
index a2d86fc5044..00000000000
--- a/spec/lib/gitlab/usage/metrics/instrumentations/count_projects_with_jira_dvcs_integration_metric_spec.rb
+++ /dev/null
@@ -1,50 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountProjectsWithJiraDvcsIntegrationMetric,
- feature_category: :integrations do
- describe 'metric value and query' do
- let_it_be_with_reload(:project_1) { create(:project) }
- let_it_be_with_reload(:project_2) { create(:project) }
- let_it_be_with_reload(:project_3) { create(:project) }
-
- before do
- project_1.feature_usage.log_jira_dvcs_integration_usage(cloud: false)
- project_2.feature_usage.log_jira_dvcs_integration_usage(cloud: false)
- project_3.feature_usage.log_jira_dvcs_integration_usage(cloud: true)
- end
-
- context 'when counting cloud integrations' do
- let(:expected_value) { 1 }
- let(:expected_query) do
- 'SELECT COUNT("project_feature_usages"."project_id") FROM "project_feature_usages" ' \
- 'WHERE "project_feature_usages"."jira_dvcs_cloud_last_sync_at" IS NOT NULL'
- end
-
- it_behaves_like 'a correct instrumented metric value and query', { time_frame: 'all', options: { cloud: true } }
- end
-
- context 'when counting non-cloud integrations' do
- let(:expected_value) { 2 }
- let(:expected_query) do
- 'SELECT COUNT("project_feature_usages"."project_id") FROM "project_feature_usages" ' \
- 'WHERE "project_feature_usages"."jira_dvcs_server_last_sync_at" IS NOT NULL'
- end
-
- it_behaves_like 'a correct instrumented metric value and query', { time_frame: 'all', options: { cloud: false } }
- end
- end
-
- it "raises an exception if option is not present" do
- expect do
- described_class.new(options: {}, time_frame: 'all')
- end.to raise_error(ArgumentError, %r{must be a boolean})
- end
-
- it "raises an exception if option has invalid value" do
- expect do
- described_class.new(options: { cloud: 'yes' }, time_frame: 'all')
- end.to raise_error(ArgumentError, %r{must be a boolean})
- end
-end
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/database_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/database_metric_spec.rb
index 8ca42a6f007..9fcec56d019 100644
--- a/spec/lib/gitlab/usage/metrics/instrumentations/database_metric_spec.rb
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/database_metric_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Usage::Metrics::Instrumentations::DatabaseMetric do
+RSpec.describe Gitlab::Usage::Metrics::Instrumentations::DatabaseMetric, feature_category: :service_ping do
let(:database_metric_class) { Class.new(described_class) }
subject do
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/generic_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/generic_metric_spec.rb
index cc4df696b37..e65d5d30d9d 100644
--- a/spec/lib/gitlab/usage/metrics/instrumentations/generic_metric_spec.rb
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/generic_metric_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Usage::Metrics::Instrumentations::GenericMetric do
+RSpec.describe Gitlab::Usage::Metrics::Instrumentations::GenericMetric, feature_category: :service_ping do
shared_examples 'custom fallback' do |custom_fallback|
subject do
Class.new(described_class) do
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/numbers_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/numbers_metric_spec.rb
index 180c76d56f3..008e30eca9c 100644
--- a/spec/lib/gitlab/usage/metrics/instrumentations/numbers_metric_spec.rb
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/numbers_metric_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Usage::Metrics::Instrumentations::NumbersMetric do
+RSpec.describe Gitlab::Usage::Metrics::Instrumentations::NumbersMetric, feature_category: :service_ping do
subject do
described_class.tap do |metric_class|
metric_class.operation :add
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/redis_hll_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/redis_hll_metric_spec.rb
index 97306051533..33868d365a5 100644
--- a/spec/lib/gitlab/usage/metrics/instrumentations/redis_hll_metric_spec.rb
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/redis_hll_metric_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe Gitlab::Usage::Metrics::Instrumentations::RedisHLLMetric, :clean_gitlab_redis_shared_state do
+RSpec.describe Gitlab::Usage::Metrics::Instrumentations::RedisHLLMetric, :clean_gitlab_redis_shared_state,
+ feature_category: :service_ping do
before do
Gitlab::UsageDataCounters::HLLRedisCounter.track_event(:i_quickactions_approve, values: 1, time: 1.week.ago)
Gitlab::UsageDataCounters::HLLRedisCounter.track_event(:i_quickactions_approve, values: 1, time: 2.weeks.ago)
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/redis_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/redis_metric_spec.rb
index c4d6edd43e1..90568f4731e 100644
--- a/spec/lib/gitlab/usage/metrics/instrumentations/redis_metric_spec.rb
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/redis_metric_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe Gitlab::Usage::Metrics::Instrumentations::RedisMetric, :clean_gitlab_redis_shared_state do
+RSpec.describe Gitlab::Usage::Metrics::Instrumentations::RedisMetric, :clean_gitlab_redis_shared_state,
+ feature_category: :service_ping do
before do
4.times do
Gitlab::UsageDataCounters::SourceCodeCounter.count(:pushes)
diff --git a/spec/lib/gitlab/usage/service_ping/instrumented_payload_spec.rb b/spec/lib/gitlab/usage/service_ping/instrumented_payload_spec.rb
index 9d2711c49c6..51649e389e2 100644
--- a/spec/lib/gitlab/usage/service_ping/instrumented_payload_spec.rb
+++ b/spec/lib/gitlab/usage/service_ping/instrumented_payload_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe Gitlab::Usage::ServicePing::InstrumentedPayload do
end
context 'when building service ping with values' do
- let(:metrics_key_paths) { %w(counts.boards uuid redis_hll_counters.search.i_search_total_monthly) }
+ let(:metrics_key_paths) { %w[counts.boards uuid redis_hll_counters.search.i_search_total_monthly] }
let(:expected_payload) do
{
counts: { boards: 0 },
@@ -26,7 +26,7 @@ RSpec.describe Gitlab::Usage::ServicePing::InstrumentedPayload do
end
context 'when building service ping with instrumentations' do
- let(:metrics_key_paths) { %w(counts.boards uuid redis_hll_counters.search.i_search_total_monthly) }
+ let(:metrics_key_paths) { %w[counts.boards uuid redis_hll_counters.search.i_search_total_monthly] }
let(:expected_payload) do
{
counts: { boards: "SELECT COUNT(\"boards\".\"id\") FROM \"boards\"" },
diff --git a/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb
index 7bef14d5f7a..a7dc0b6a060 100644
--- a/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb
+++ b/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb
@@ -113,73 +113,57 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
end
end
- context 'when usage_ping is disabled' do
- it 'does not track the event' do
- allow(::ServicePing::ServicePingSettings).to receive(:enabled?).and_return(false)
+ it 'tracks event when using symbol' do
+ expect(Gitlab::Redis::HLL).to receive(:add)
- described_class.track_event(weekly_event, values: entity1, time: Date.current)
-
- expect(Gitlab::Redis::HLL).not_to receive(:add)
- end
+ described_class.track_event(:g_analytics_contribution, values: entity1)
end
- context 'when usage_ping is enabled' do
- before do
- allow(::ServicePing::ServicePingSettings).to receive(:enabled?).and_return(true)
- end
+ it 'tracks events with multiple values' do
+ values = [entity1, entity2]
+ expect(Gitlab::Redis::HLL).to receive(:add).with(key: /g_analytics_contribution/, value: values,
+ expiry: described_class::KEY_EXPIRY_LENGTH)
- it 'tracks event when using symbol' do
- expect(Gitlab::Redis::HLL).to receive(:add)
-
- described_class.track_event(:g_analytics_contribution, values: entity1)
- end
-
- it 'tracks events with multiple values' do
- values = [entity1, entity2]
- expect(Gitlab::Redis::HLL).to receive(:add).with(key: /g_analytics_contribution/, value: values,
- expiry: described_class::KEY_EXPIRY_LENGTH)
+ described_class.track_event(:g_analytics_contribution, values: values)
+ end
- described_class.track_event(:g_analytics_contribution, values: values)
- end
+ it 'raise error if metrics of unknown event' do
+ expect { described_class.track_event('unknown', values: entity1, time: Date.current) }.to raise_error(Gitlab::UsageDataCounters::HLLRedisCounter::UnknownEvent)
+ end
- it 'raise error if metrics of unknown event' do
- expect { described_class.track_event('unknown', values: entity1, time: Date.current) }.to raise_error(Gitlab::UsageDataCounters::HLLRedisCounter::UnknownEvent)
+ context 'when Rails environment is production' do
+ before do
+ allow(Rails.env).to receive(:development?).and_return(false)
+ allow(Rails.env).to receive(:test?).and_return(false)
end
- context 'when Rails environment is production' do
- before do
- allow(Rails.env).to receive(:development?).and_return(false)
- allow(Rails.env).to receive(:test?).and_return(false)
- end
-
- it 'reports only UnknownEvent exception' do
- expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception)
- .with(Gitlab::UsageDataCounters::HLLRedisCounter::UnknownEvent)
- .once
- .and_call_original
+ it 'reports only UnknownEvent exception' do
+ expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception)
+ .with(Gitlab::UsageDataCounters::HLLRedisCounter::UnknownEvent)
+ .once
+ .and_call_original
- expect { described_class.track_event('unknown', values: entity1, time: Date.current) }.not_to raise_error
- end
+ expect { described_class.track_event('unknown', values: entity1, time: Date.current) }.not_to raise_error
end
+ end
- it 'reports an error if Feature.enabled raise an error' do
- expect(Feature).to receive(:enabled?).and_raise(StandardError.new)
- expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception)
+ it 'reports an error if Feature.enabled raise an error' do
+ expect(Feature).to receive(:enabled?).and_raise(StandardError.new)
+ expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception)
- described_class.track_event(:g_analytics_contribution, values: entity1, time: Date.current)
- end
+ described_class.track_event(:g_analytics_contribution, values: entity1, time: Date.current)
+ end
- context 'for weekly events' do
- it 'sets the keys in Redis to expire' do
- described_class.track_event("g_compliance_dashboard", values: entity1)
+ context 'for weekly events' do
+ it 'sets the keys in Redis to expire' do
+ described_class.track_event("g_compliance_dashboard", values: entity1)
- Gitlab::Redis::SharedState.with do |redis|
- keys = redis.scan_each(match: "{#{described_class::REDIS_SLOT}}_g_compliance_dashboard-*").to_a
- expect(keys).not_to be_empty
+ Gitlab::Redis::SharedState.with do |redis|
+ keys = redis.scan_each(match: "{#{described_class::REDIS_SLOT}}_g_compliance_dashboard-*").to_a
+ expect(keys).not_to be_empty
- keys.each do |key|
- expect(redis.ttl(key)).to be_within(5.seconds).of(described_class::KEY_EXPIRY_LENGTH)
- end
+ keys.each do |key|
+ expect(redis.ttl(key)).to be_within(5.seconds).of(described_class::KEY_EXPIRY_LENGTH)
end
end
end
diff --git a/spec/lib/gitlab/usage_data_counters/redis_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/redis_counter_spec.rb
index 753e09731bf..39d48b7b938 100644
--- a/spec/lib/gitlab/usage_data_counters/redis_counter_spec.rb
+++ b/spec/lib/gitlab/usage_data_counters/redis_counter_spec.rb
@@ -7,51 +7,19 @@ RSpec.describe Gitlab::UsageDataCounters::RedisCounter, :clean_gitlab_redis_shar
subject { Class.new.extend(described_class) }
- before do
- allow(::ServicePing::ServicePingSettings).to receive(:enabled?).and_return(service_ping_enabled)
- end
-
describe '.increment' do
- context 'when usage_ping is disabled' do
- let(:service_ping_enabled) { false }
-
- it 'counter is not increased' do
- expect do
- subject.increment(redis_key)
- end.not_to change { subject.total_count(redis_key) }
- end
- end
-
- context 'when usage_ping is enabled' do
- let(:service_ping_enabled) { true }
-
- it 'counter is increased' do
- expect do
- subject.increment(redis_key)
- end.to change { subject.total_count(redis_key) }.by(1)
- end
+ it 'counter is increased' do
+ expect do
+ subject.increment(redis_key)
+ end.to change { subject.total_count(redis_key) }.by(1)
end
end
describe '.increment_by' do
- context 'when usage_ping is disabled' do
- let(:service_ping_enabled) { false }
-
- it 'counter is not increased' do
- expect do
- subject.increment_by(redis_key, 3)
- end.not_to change { subject.total_count(redis_key) }
- end
- end
-
- context 'when usage_ping is enabled' do
- let(:service_ping_enabled) { true }
-
- it 'counter is increased' do
- expect do
- subject.increment_by(redis_key, 3)
- end.to change { subject.total_count(redis_key) }.by(3)
- end
+ it 'counter is increased' do
+ expect do
+ subject.increment_by(redis_key, 3)
+ end.to change { subject.total_count(redis_key) }.by(3)
end
end
end
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index 6f188aa408e..a1564318408 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -42,7 +42,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures, feature_category: :servic
end
it 'ensures recorded_at is set before any other usage data calculation' do
- %i(alt_usage_data redis_usage_data distinct_count count).each do |method|
+ %i[alt_usage_data redis_usage_data distinct_count count].each do |method|
expect(described_class).not_to receive(method)
end
expect(described_class).to receive(:recorded_at).and_raise(Exception.new('Stopped calculating recorded_at'))
@@ -191,7 +191,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures, feature_category: :servic
omniauth:
{ providers: omniauth_providers }
)
- allow(Devise).to receive(:omniauth_providers).and_return(%w(ldapmain ldapsecondary group_saml))
+ allow(Devise).to receive(:omniauth_providers).and_return(%w[ldapmain ldapsecondary group_saml])
for_defined_days_back do
user = create(:user)
@@ -268,7 +268,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures, feature_category: :servic
for_defined_days_back do
user = create(:user)
- %w(gitlab_project github bitbucket bitbucket_server gitea git manifest fogbugz).each do |type|
+ %w[gitlab_project github bitbucket bitbucket_server gitea git manifest fogbugz].each do |type|
create(:project, import_type: type, creator_id: user.id)
end
@@ -734,7 +734,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures, feature_category: :servic
subject { described_class.object_store_usage_data }
it 'fetches object store config of five components' do
- %w(artifacts external_diffs lfs uploads packages).each do |component|
+ %w[artifacts external_diffs lfs uploads packages].each do |component|
expect(described_class).to receive(:object_store_config).with(component).and_return("#{component}_object_store_config")
end
diff --git a/spec/lib/gitlab/utils/log_limited_array_spec.rb b/spec/lib/gitlab/utils/log_limited_array_spec.rb
index a55a176be48..23cca4fd791 100644
--- a/spec/lib/gitlab/utils/log_limited_array_spec.rb
+++ b/spec/lib/gitlab/utils/log_limited_array_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe Gitlab::Utils::LogLimitedArray do
context 'when the argument is an array' do
context 'when the array is under the limit' do
it 'returns the array unchanged' do
- expect(described_class.log_limited_array(%w(a b))).to eq(%w(a b))
+ expect(described_class.log_limited_array(%w[a b])).to eq(%w[a b])
end
end
diff --git a/spec/lib/gitlab/webpack/graphql_known_operations_spec.rb b/spec/lib/gitlab/webpack/graphql_known_operations_spec.rb
index 89cade82fe6..6c3e3b4eb69 100644
--- a/spec/lib/gitlab/webpack/graphql_known_operations_spec.rb
+++ b/spec/lib/gitlab/webpack/graphql_known_operations_spec.rb
@@ -30,7 +30,7 @@ RSpec.describe Gitlab::Webpack::GraphqlKnownOperations do
2.times { ::Gitlab::Webpack::GraphqlKnownOperations.load }
- expect(::Gitlab::Webpack::GraphqlKnownOperations.load).to eq(%w(hello world test))
+ expect(::Gitlab::Webpack::GraphqlKnownOperations.load).to eq(%w[hello world test])
end
end
diff --git a/spec/lib/gitlab/wiki_pages/front_matter_parser_spec.rb b/spec/lib/gitlab/wiki_pages/front_matter_parser_spec.rb
index 3152dc2ad2f..3d165f7d830 100644
--- a/spec/lib/gitlab/wiki_pages/front_matter_parser_spec.rb
+++ b/spec/lib/gitlab/wiki_pages/front_matter_parser_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe Gitlab::WikiPages::FrontMatterParser do
end
def have_correct_front_matter
- include(a: 1, b: 2, c: %w(foo bar))
+ include(a: 1, b: 2, c: %w[foo bar])
end
describe '#parse' do
diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb
index cca18cb05c7..d77763f89be 100644
--- a/spec/lib/gitlab/workhorse_spec.rb
+++ b/spec/lib/gitlab/workhorse_spec.rb
@@ -226,7 +226,8 @@ RSpec.describe Gitlab::Workhorse, feature_category: :shared do
GL_ID: "user-#{user.id}",
GL_USERNAME: user.username,
GL_REPOSITORY: "project-#{project.id}",
- ShowAllRefs: false
+ ShowAllRefs: false,
+ NeedAudit: false
}
end
@@ -277,6 +278,12 @@ RSpec.describe Gitlab::Workhorse, feature_category: :shared do
it { is_expected.to include(ShowAllRefs: true) }
end
+ context 'need_audit enabled' do
+ subject { described_class.git_http_ok(repository, Gitlab::GlRepository::PROJECT, user, action, show_all_refs: true, need_audit: true) }
+
+ it { is_expected.to include(NeedAudit: true) }
+ end
+
context 'when a feature flag is set for a single project' do
before do
stub_feature_flags(gitaly_mep_mep: project)
diff --git a/spec/lib/object_storage/config_spec.rb b/spec/lib/object_storage/config_spec.rb
index 412fcb9b6b8..bf9aeb51cda 100644
--- a/spec/lib/object_storage/config_spec.rb
+++ b/spec/lib/object_storage/config_spec.rb
@@ -155,7 +155,7 @@ RSpec.describe ObjectStorage::Config, feature_category: :shared do
it { expect(subject.aws_server_side_encryption_enabled?).to be true }
it { expect(subject.server_side_encryption).to eq('AES256') }
it { expect(subject.server_side_encryption_kms_key_id).to eq('arn:aws:12345') }
- it { expect(subject.fog_attributes.keys).to match_array(%w(x-amz-server-side-encryption x-amz-server-side-encryption-aws-kms-key-id)) }
+ it { expect(subject.fog_attributes.keys).to match_array(%w[x-amz-server-side-encryption x-amz-server-side-encryption-aws-kms-key-id]) }
end
context 'with only server side encryption enabled' do
diff --git a/spec/lib/object_storage/direct_upload_spec.rb b/spec/lib/object_storage/direct_upload_spec.rb
index 3a42e6ebd09..5df295e73d7 100644
--- a/spec/lib/object_storage/direct_upload_spec.rb
+++ b/spec/lib/object_storage/direct_upload_spec.rb
@@ -123,7 +123,7 @@ RSpec.describe ObjectStorage::DirectUpload, feature_category: :shared do
expect(s3_config[:Region]).to eq(region)
expect(s3_config[:PathStyle]).to eq(path_style)
expect(s3_config[:UseIamProfile]).to eq(use_iam_profile)
- expect(s3_config.keys).not_to include(%i(ServerSideEncryption SSEKMSKeyID))
+ expect(s3_config.keys).not_to include(%i[ServerSideEncryption SSEKMSKeyID])
end
context 'when no region is specified' do
diff --git a/spec/lib/peek/views/rugged_spec.rb b/spec/lib/peek/views/rugged_spec.rb
deleted file mode 100644
index 31418b5fc81..00000000000
--- a/spec/lib/peek/views/rugged_spec.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Peek::Views::Rugged, :request_store do
- subject { described_class.new }
-
- let(:project) { create(:project) }
-
- before do
- allow(Gitlab::PerformanceBar).to receive(:enabled_for_request?).and_return(true)
- end
-
- it 'returns no results' do
- expect(subject.results).to eq({})
- end
-
- it 'returns aggregated results' do
- ::Gitlab::RuggedInstrumentation.add_query_time(1.234)
- ::Gitlab::RuggedInstrumentation.increment_query_count
- ::Gitlab::RuggedInstrumentation.increment_query_count
-
- ::Gitlab::RuggedInstrumentation.add_call_details(feature: :rugged_test,
- args: [project.repository.raw, 'HEAD'],
- duration: 0.123)
- ::Gitlab::RuggedInstrumentation.add_call_details(feature: :rugged_test2,
- args: [project.repository, 'refs/heads/master'],
- duration: 0.456)
-
- results = subject.results
- expect(results[:calls]).to eq(2)
- expect(results[:duration]).to eq('1234.00ms')
- expect(results[:details].count).to eq(2)
-
- expected = [
- [project.repository.raw.to_s, "HEAD"],
- [project.repository.to_s, "refs/heads/master"]
- ]
-
- expect(results[:details].map { |data| data[:args] }).to match_array(expected)
- end
-end
diff --git a/spec/lib/result_spec.rb b/spec/lib/result_spec.rb
index 2b88521fe14..170a2f5e777 100644
--- a/spec/lib/result_spec.rb
+++ b/spec/lib/result_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'spec_helper'
# NOTE:
# This spec is intended to serve as documentation examples of idiomatic usage for the `Result` type.
diff --git a/spec/lib/rouge/formatters/html_gitlab_spec.rb b/spec/lib/rouge/formatters/html_gitlab_spec.rb
index 6fc1b395fc8..5e5075b72b8 100644
--- a/spec/lib/rouge/formatters/html_gitlab_spec.rb
+++ b/spec/lib/rouge/formatters/html_gitlab_spec.rb
@@ -15,14 +15,14 @@ RSpec.describe Rouge::Formatters::HTMLGitlab, feature_category: :source_code_man
let(:options) { { tag: lang, ellipsis_indexes: [0], ellipsis_svg: "svg_icon" } }
it 'returns highlighted ruby code with svg' do
- code = %q{<span id="LC1" class="line" lang="ruby"><span class="k">def</span> <span class="nf">hello</span><span class="gl-px-2 gl-rounded-base gl-mx-2 gl-bg-gray-100 gl-cursor-help has-tooltip" title="Content has been trimmed">svg_icon</span></span>}
+ code = %q(<span id="LC1" class="line" lang="ruby"><span class="k">def</span> <span class="nf">hello</span><span class="gl-px-2 gl-rounded-base gl-mx-2 gl-bg-gray-100 gl-cursor-help has-tooltip" title="Content has been trimmed">svg_icon</span></span>)
is_expected.to eq(code)
end
end
it 'returns highlighted ruby code' do
- code = %q{<span id="LC1" class="line" lang="ruby"><span class="k">def</span> <span class="nf">hello</span></span>}
+ code = %q(<span id="LC1" class="line" lang="ruby"><span class="k">def</span> <span class="nf">hello</span></span>)
is_expected.to eq(code)
end
@@ -31,7 +31,7 @@ RSpec.describe Rouge::Formatters::HTMLGitlab, feature_category: :source_code_man
let(:options) { {} }
it 'returns highlighted code without language' do
- code = %q{<span id="LC1" class="line" lang=""><span class="k">def</span> <span class="nf">hello</span></span>}
+ code = %q(<span id="LC1" class="line" lang=""><span class="k">def</span> <span class="nf">hello</span></span>)
is_expected.to eq(code)
end
@@ -41,7 +41,7 @@ RSpec.describe Rouge::Formatters::HTMLGitlab, feature_category: :source_code_man
let(:options) { { tag: lang, line_number: 10 } }
it 'returns highlighted ruby code with correct line number' do
- code = %q{<span id="LC10" class="line" lang="ruby"><span class="k">def</span> <span class="nf">hello</span></span>}
+ code = %q(<span id="LC10" class="line" lang="ruby"><span class="k">def</span> <span class="nf">hello</span></span>)
is_expected.to eq(code)
end
@@ -64,7 +64,7 @@ RSpec.describe Rouge::Formatters::HTMLGitlab, feature_category: :source_code_man
it 'highlights the control characters' do
message = "Potentially unwanted character detected: Unicode BiDi Control"
- is_expected.to include(%{<span class="unicode-bidi has-tooltip" data-toggle="tooltip" title="#{message}">}).exactly(4).times
+ is_expected.to include(%(<span class="unicode-bidi has-tooltip" data-toggle="tooltip" title="#{message}">)).exactly(4).times
end
end
diff --git a/spec/lib/safe_zip/entry_spec.rb b/spec/lib/safe_zip/entry_spec.rb
index 8d49e2bcece..9a068b255dd 100644
--- a/spec/lib/safe_zip/entry_spec.rb
+++ b/spec/lib/safe_zip/entry_spec.rb
@@ -4,8 +4,8 @@ require 'spec_helper'
RSpec.describe SafeZip::Entry do
let(:target_path) { Dir.mktmpdir('safe-zip') }
- let(:directories) { %w(public folder/with/subfolder) }
- let(:files) { %w(public/index.html public/assets/image.png) }
+ let(:directories) { %w[public folder/with/subfolder] }
+ let(:files) { %w[public/index.html public/assets/image.png] }
let(:params) { SafeZip::ExtractParams.new(directories: directories, files: files, to: target_path) }
let(:entry) { described_class.new(zip_archive, zip_entry, params) }
@@ -52,7 +52,7 @@ RSpec.describe SafeZip::Entry do
subject { entry.extract }
context 'when entry does not match the filtered directories' do
- let(:directories) { %w(public folder/with/subfolder) }
+ let(:directories) { %w[public folder/with/subfolder] }
let(:files) { [] }
using RSpec::Parameterized::TableSyntax
@@ -76,7 +76,7 @@ RSpec.describe SafeZip::Entry do
context 'when entry does not match the filtered files' do
let(:directories) { [] }
- let(:files) { %w(public/index.html public/assets/image.png) }
+ let(:files) { %w[public/index.html public/assets/image.png] }
using RSpec::Parameterized::TableSyntax
diff --git a/spec/lib/safe_zip/extract_params_spec.rb b/spec/lib/safe_zip/extract_params_spec.rb
index 0ebfb7430c5..b0d787e09d5 100644
--- a/spec/lib/safe_zip/extract_params_spec.rb
+++ b/spec/lib/safe_zip/extract_params_spec.rb
@@ -6,8 +6,8 @@ RSpec.describe SafeZip::ExtractParams do
let(:target_path) { Dir.mktmpdir("safe-zip") }
let(:real_target_path) { File.realpath(target_path) }
let(:params) { described_class.new(directories: directories, files: files, to: target_path) }
- let(:directories) { %w(public folder/with/subfolder) }
- let(:files) { %w(public/index.html public/assets/image.png) }
+ let(:directories) { %w[public folder/with/subfolder] }
+ let(:files) { %w[public/index.html public/assets/image.png] }
after do
FileUtils.remove_entry_secure(target_path)
diff --git a/spec/lib/safe_zip/extract_spec.rb b/spec/lib/safe_zip/extract_spec.rb
index c727475e271..fa8a922beef 100644
--- a/spec/lib/safe_zip/extract_spec.rb
+++ b/spec/lib/safe_zip/extract_spec.rb
@@ -4,8 +4,8 @@ require 'spec_helper'
RSpec.describe SafeZip::Extract do
let(:target_path) { Dir.mktmpdir('safe-zip') }
- let(:directories) { %w(public) }
- let(:files) { %w(public/index.html) }
+ let(:directories) { %w[public] }
+ let(:files) { %w[public/index.html] }
let(:object) { described_class.new(archive) }
let(:archive) { Rails.root.join('spec', 'fixtures', 'safe_zip', archive_name) }
@@ -47,7 +47,7 @@ RSpec.describe SafeZip::Extract do
end
end
- %w(valid-simple.zip valid-symlinks-first.zip valid-non-writeable.zip).each do |name|
+ %w[valid-simple.zip valid-symlinks-first.zip valid-non-writeable.zip].each do |name|
context "when using #{name} archive" do
let(:archive_name) { name }
@@ -74,7 +74,7 @@ RSpec.describe SafeZip::Extract do
context 'when no matching directories are found' do
let(:archive_name) { 'valid-simple.zip' }
- let(:directories) { %w(non/existing) }
+ let(:directories) { %w[non/existing] }
let(:error_message) { 'No entries extracted' }
subject { object.extract(directories: directories, to: target_path) }
@@ -84,7 +84,7 @@ RSpec.describe SafeZip::Extract do
context 'when no matching files are found' do
let(:archive_name) { 'valid-simple.zip' }
- let(:files) { %w(non/existing) }
+ let(:files) { %w[non/existing] }
let(:error_message) { 'No entries extracted' }
subject { object.extract(files: files, to: target_path) }
diff --git a/spec/lib/sbom/purl_type/converter_spec.rb b/spec/lib/sbom/purl_type/converter_spec.rb
index 2eb35c4d079..d0907bf253f 100644
--- a/spec/lib/sbom/purl_type/converter_spec.rb
+++ b/spec/lib/sbom/purl_type/converter_spec.rb
@@ -22,6 +22,7 @@ RSpec.describe Sbom::PurlType::Converter, feature_category: :dependency_manageme
'nuget' | 'nuget'
'pip' | 'pypi'
'pipenv' | 'pypi'
+ 'poetry' | 'pypi'
'setuptools' | 'pypi'
'Python (python-pkg)' | 'pypi'
'analyzer (gobinary)' | 'golang'
diff --git a/spec/lib/security/ci_configuration/container_scanning_build_action_spec.rb b/spec/lib/security/ci_configuration/container_scanning_build_action_spec.rb
index 5b1db66beb0..af61d9c8261 100644
--- a/spec/lib/security/ci_configuration/container_scanning_build_action_spec.rb
+++ b/spec/lib/security/ci_configuration/container_scanning_build_action_spec.rb
@@ -39,7 +39,7 @@ RSpec.describe Security::CiConfiguration::ContainerScanningBuildAction do
context 'template includes are an array' do
let(:gitlab_ci_content) do
- { "stages" => %w(test security),
+ { "stages" => %w[test security],
"variables" => { "RANDOM" => "make sure this persists" },
"include" => [{ "template" => "existing.yml" }] }
end
@@ -52,7 +52,7 @@ RSpec.describe Security::CiConfiguration::ContainerScanningBuildAction do
context 'template include is not an array' do
let(:gitlab_ci_content) do
- { "stages" => %w(test security),
+ { "stages" => %w[test security],
"variables" => { "RANDOM" => "make sure this persists" },
"include" => { "template" => "existing.yml" } }
end
@@ -91,7 +91,7 @@ RSpec.describe Security::CiConfiguration::ContainerScanningBuildAction do
context 'container_scanning template include are an array' do
let(:gitlab_ci_content) do
- { "stages" => %w(test),
+ { "stages" => %w[test],
"variables" => { "RANDOM" => "make sure this persists" },
"include" => [{ "template" => "Jobs/Container-Scanning.gitlab-ci.yml" }] }
end
@@ -104,7 +104,7 @@ RSpec.describe Security::CiConfiguration::ContainerScanningBuildAction do
context 'container_scanning template include is not an array' do
let(:gitlab_ci_content) do
- { "stages" => %w(test),
+ { "stages" => %w[test],
"variables" => { "RANDOM" => "make sure this persists" },
"include" => { "template" => "Jobs/Container-Scanning.gitlab-ci.yml" } }
end
diff --git a/spec/lib/security/ci_configuration/sast_build_action_spec.rb b/spec/lib/security/ci_configuration/sast_build_action_spec.rb
index 381ea60e7f5..fe504e2b278 100644
--- a/spec/lib/security/ci_configuration/sast_build_action_spec.rb
+++ b/spec/lib/security/ci_configuration/sast_build_action_spec.rb
@@ -218,47 +218,47 @@ RSpec.describe Security::CiConfiguration::SastBuildAction do
end
def existing_gitlab_ci_and_template_array_without_sast
- { "stages" => %w(test security),
+ { "stages" => %w[test security],
"variables" => { "RANDOM" => "make sure this persists", "SECURE_ANALYZERS_PREFIX" => "localhost:5000/analyzers" },
"sast" => { "variables" => { "SEARCH_MAX_DEPTH" => 1 }, "stage" => "security" },
"include" => [{ "template" => "existing.yml" }] }
end
def existing_gitlab_ci_and_single_template_with_sast_and_default_stage
- { "stages" => %w(test),
+ { "stages" => %w[test],
"variables" => { "SECURE_ANALYZERS_PREFIX" => "localhost:5000/analyzers" },
"sast" => { "variables" => { "SEARCH_MAX_DEPTH" => 1 }, "stage" => "test" },
"include" => { "template" => "Security/SAST.gitlab-ci.yml" } }
end
def existing_gitlab_ci_and_single_template_without_sast
- { "stages" => %w(test security),
+ { "stages" => %w[test security],
"variables" => { "RANDOM" => "make sure this persists", "SECURE_ANALYZERS_PREFIX" => "localhost:5000/analyzers" },
"sast" => { "variables" => { "SEARCH_MAX_DEPTH" => 1 }, "stage" => "security" },
"include" => { "template" => "existing.yml" } }
end
def existing_gitlab_ci_with_no_variables
- { "stages" => %w(test security),
+ { "stages" => %w[test security],
"sast" => { "variables" => { "SEARCH_MAX_DEPTH" => 1 }, "stage" => "security" },
"include" => [{ "template" => "Security/SAST.gitlab-ci.yml" }] }
end
def existing_gitlab_ci_with_no_sast_section
- { "stages" => %w(test security),
+ { "stages" => %w[test security],
"variables" => { "RANDOM" => "make sure this persists", "SECURE_ANALYZERS_PREFIX" => "localhost:5000/analyzers" },
"include" => [{ "template" => "Security/SAST.gitlab-ci.yml" }] }
end
def existing_gitlab_ci_with_no_sast_variables
- { "stages" => %w(test security),
+ { "stages" => %w[test security],
"variables" => { "RANDOM" => "make sure this persists", "SECURE_ANALYZERS_PREFIX" => "localhost:5000/analyzers" },
"sast" => { "stage" => "security" },
"include" => [{ "template" => "Security/SAST.gitlab-ci.yml" }] }
end
def existing_gitlab_ci
- { "stages" => %w(test security),
+ { "stages" => %w[test security],
"variables" => { "RANDOM" => "make sure this persists", "SECURE_ANALYZERS_PREFIX" => "bad_prefix" },
"sast" => { "variables" => { "SEARCH_MAX_DEPTH" => 1 }, "stage" => "security" },
"include" => [{ "template" => "Security/SAST.gitlab-ci.yml" }] }
diff --git a/spec/lib/security/ci_configuration/sast_iac_build_action_spec.rb b/spec/lib/security/ci_configuration/sast_iac_build_action_spec.rb
index 7b2a0d22918..fcee34d833b 100644
--- a/spec/lib/security/ci_configuration/sast_iac_build_action_spec.rb
+++ b/spec/lib/security/ci_configuration/sast_iac_build_action_spec.rb
@@ -34,7 +34,7 @@ RSpec.describe Security::CiConfiguration::SastIacBuildAction do
context 'template includes are an array' do
let(:gitlab_ci_content) do
- { "stages" => %w(test security),
+ { "stages" => %w[test security],
"variables" => { "RANDOM" => "make sure this persists" },
"include" => [{ "template" => "existing.yml" }] }
end
@@ -47,7 +47,7 @@ RSpec.describe Security::CiConfiguration::SastIacBuildAction do
context 'template include is not an array' do
let(:gitlab_ci_content) do
- { "stages" => %w(test security),
+ { "stages" => %w[test security],
"variables" => { "RANDOM" => "make sure this persists" },
"include" => { "template" => "existing.yml" } }
end
@@ -80,7 +80,7 @@ RSpec.describe Security::CiConfiguration::SastIacBuildAction do
context 'secret_detection template include are an array' do
let(:gitlab_ci_content) do
- { "stages" => %w(test),
+ { "stages" => %w[test],
"variables" => { "RANDOM" => "make sure this persists" },
"include" => [{ "template" => "Security/SAST-IaC.latest.gitlab-ci.yml" }] }
end
@@ -93,7 +93,7 @@ RSpec.describe Security::CiConfiguration::SastIacBuildAction do
context 'secret_detection template include is not an array' do
let(:gitlab_ci_content) do
- { "stages" => %w(test),
+ { "stages" => %w[test],
"variables" => { "RANDOM" => "make sure this persists" },
"include" => { "template" => "Security/SAST-IaC.latest.gitlab-ci.yml" } }
end
diff --git a/spec/lib/security/ci_configuration/secret_detection_build_action_spec.rb b/spec/lib/security/ci_configuration/secret_detection_build_action_spec.rb
index 4d9860ca4a5..64323ce71f3 100644
--- a/spec/lib/security/ci_configuration/secret_detection_build_action_spec.rb
+++ b/spec/lib/security/ci_configuration/secret_detection_build_action_spec.rb
@@ -33,7 +33,7 @@ RSpec.describe Security::CiConfiguration::SecretDetectionBuildAction do
context 'template includes are an array' do
let(:gitlab_ci_content) do
- { "stages" => %w(test security),
+ { "stages" => %w[test security],
"variables" => { "RANDOM" => "make sure this persists" },
"include" => [{ "template" => "existing.yml" }] }
end
@@ -46,7 +46,7 @@ RSpec.describe Security::CiConfiguration::SecretDetectionBuildAction do
context 'template include is not an array' do
let(:gitlab_ci_content) do
- { "stages" => %w(test security),
+ { "stages" => %w[test security],
"variables" => { "RANDOM" => "make sure this persists" },
"include" => { "template" => "existing.yml" } }
end
@@ -79,7 +79,7 @@ RSpec.describe Security::CiConfiguration::SecretDetectionBuildAction do
context 'secret_detection template include are an array' do
let(:gitlab_ci_content) do
- { "stages" => %w(test),
+ { "stages" => %w[test],
"variables" => { "RANDOM" => "make sure this persists" },
"include" => [{ "template" => "Security/Secret-Detection.gitlab-ci.yml" }] }
end
@@ -92,7 +92,7 @@ RSpec.describe Security::CiConfiguration::SecretDetectionBuildAction do
context 'secret_detection template include is not an array' do
let(:gitlab_ci_content) do
- { "stages" => %w(test),
+ { "stages" => %w[test],
"variables" => { "RANDOM" => "make sure this persists" },
"include" => { "template" => "Security/Secret-Detection.gitlab-ci.yml" } }
end
diff --git a/spec/lib/sidebars/explore/menus/catalog_menu_spec.rb b/spec/lib/sidebars/explore/menus/catalog_menu_spec.rb
new file mode 100644
index 00000000000..2c4c4c48eae
--- /dev/null
+++ b/spec/lib/sidebars/explore/menus/catalog_menu_spec.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::Explore::Menus::CatalogMenu, feature_category: :navigation do
+ let_it_be(:current_user) { build(:user) }
+ let_it_be(:user) { build(:user) }
+
+ let(:context) { Sidebars::Context.new(current_user: current_user, container: user) }
+
+ subject { described_class.new(context) }
+
+ context 'when `global_ci_catalog` is enabled`' do
+ it 'renders' do
+ expect(subject.render?).to be(true)
+ end
+
+ it 'renders the correct link' do
+ expect(subject.link).to match "explore/catalog"
+ end
+
+ it 'renders the correct title' do
+ expect(subject.title).to eq "CI/CD Catalog"
+ end
+
+ it 'renders the correct icon' do
+ expect(subject.sprite_icon).to eq "catalog-checkmark"
+ end
+ end
+
+ context 'when `global_ci_catalog` FF is disabled' do
+ before do
+ stub_feature_flags(global_ci_catalog: false)
+ end
+
+ it 'does not render' do
+ expect(subject.render?).to be(false)
+ end
+ end
+end
diff --git a/spec/lib/sidebars/menu_spec.rb b/spec/lib/sidebars/menu_spec.rb
index e59a8cd2163..aa3b754f17e 100644
--- a/spec/lib/sidebars/menu_spec.rb
+++ b/spec/lib/sidebars/menu_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe Sidebars::Menu, feature_category: :navigation do
describe '#all_active_routes' do
it 'gathers all active routes of items and the current menu' do
- menu.add_item(Sidebars::MenuItem.new(title: 'foo1', link: 'foo1', active_routes: { path: %w(bar test) }))
+ menu.add_item(Sidebars::MenuItem.new(title: 'foo1', link: 'foo1', active_routes: { path: %w[bar test] }))
menu.add_item(Sidebars::MenuItem.new(title: 'foo2', link: 'foo2', active_routes: { controller: 'fooc' }))
menu.add_item(Sidebars::MenuItem.new(title: 'foo3', link: 'foo3', active_routes: { controller: 'barc' }))
menu.add_item(nil_menu_item)
@@ -18,7 +18,7 @@ RSpec.describe Sidebars::Menu, feature_category: :navigation do
allow(menu).to receive(:active_routes).and_return({ path: 'foo' })
expect(menu).to receive(:renderable_items).and_call_original
- expect(menu.all_active_routes).to eq({ path: %w(foo bar test), controller: %w(fooc barc) })
+ expect(menu.all_active_routes).to eq({ path: %w[foo bar test], controller: %w[fooc barc] })
end
end
@@ -53,6 +53,7 @@ RSpec.describe Sidebars::Menu, feature_category: :navigation do
{
title: "Title",
icon: nil,
+ id: 'menu',
avatar: nil,
avatar_shape: 'rect',
entity_id: nil,
@@ -94,6 +95,7 @@ RSpec.describe Sidebars::Menu, feature_category: :navigation do
{
title: "Title",
icon: nil,
+ id: 'menu',
avatar: nil,
avatar_shape: 'rect',
entity_id: nil,
diff --git a/spec/lib/sidebars/organizations/menus/manage_menu_spec.rb b/spec/lib/sidebars/organizations/menus/manage_menu_spec.rb
index 08fc352a6cd..87346176a4c 100644
--- a/spec/lib/sidebars/organizations/menus/manage_menu_spec.rb
+++ b/spec/lib/sidebars/organizations/menus/manage_menu_spec.rb
@@ -24,5 +24,11 @@ RSpec.describe Sidebars::Organizations::Menus::ManageMenu, feature_category: :na
it { is_expected.not_to be_nil }
end
+
+ describe 'Users' do
+ let(:item_id) { :organization_users }
+
+ it { is_expected.not_to be_nil }
+ end
end
end
diff --git a/spec/lib/sidebars/projects/menus/scope_menu_spec.rb b/spec/lib/sidebars/projects/menus/scope_menu_spec.rb
index 1c2d159950a..108a98e28a4 100644
--- a/spec/lib/sidebars/projects/menus/scope_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/scope_menu_spec.rb
@@ -23,7 +23,7 @@ RSpec.describe Sidebars::Projects::Menus::ScopeMenu, feature_category: :navigati
describe '#container_html_options' do
subject { described_class.new(context).container_html_options }
- specify { is_expected.to match(hash_including(class: 'shortcuts-project rspec-project-link')) }
+ specify { is_expected.to match(hash_including(class: 'shortcuts-project')) }
end
describe '#extra_nav_link_html_options' do
diff --git a/spec/lib/sidebars/projects/menus/security_compliance_menu_spec.rb b/spec/lib/sidebars/projects/menus/security_compliance_menu_spec.rb
index 4b4706bd311..c7c0586c2f1 100644
--- a/spec/lib/sidebars/projects/menus/security_compliance_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/security_compliance_menu_spec.rb
@@ -21,9 +21,12 @@ RSpec.describe Sidebars::Projects::Menus::SecurityComplianceMenu do
context 'when user is authenticated' do
context 'when the Security and Compliance is disabled' do
+ let_it_be(:project) { create(:project, :security_and_compliance_disabled) }
+
before do
allow(Ability).to receive(:allowed?).with(user, :access_security_and_compliance, project).and_return(false)
allow(Ability).to receive(:allowed?).with(user, :read_security_resource, project).and_return(false)
+ allow(project).to receive(:security_and_compliance_enabled?).and_return(false)
end
it { is_expected.to be_falsey }
diff --git a/spec/lib/sidebars/projects/menus/settings_menu_spec.rb b/spec/lib/sidebars/projects/menus/settings_menu_spec.rb
index 81ca9670ac6..605cec8be5e 100644
--- a/spec/lib/sidebars/projects/menus/settings_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/settings_menu_spec.rb
@@ -59,18 +59,6 @@ RSpec.describe Sidebars::Projects::Menus::SettingsMenu, feature_category: :navig
let(:item_id) { :access_tokens }
it_behaves_like 'access rights checks'
-
- describe 'when the user is not an admin but has manage_resource_access_tokens' do
- before do
- allow(Ability).to receive(:allowed?).and_call_original
- allow(Ability).to receive(:allowed?).with(user, :admin_project, project).and_return(false)
- allow(Ability).to receive(:allowed?).with(user, :manage_resource_access_tokens, project).and_return(true)
- end
-
- it 'includes access token menu item' do
- expect(subject.title).to eql('Access Tokens')
- end
- end
end
describe 'Repository' do
diff --git a/spec/lib/sidebars/projects/super_sidebar_menus/monitor_menu_spec.rb b/spec/lib/sidebars/projects/super_sidebar_menus/monitor_menu_spec.rb
index e5c5204e0b4..3f8a146f040 100644
--- a/spec/lib/sidebars/projects/super_sidebar_menus/monitor_menu_spec.rb
+++ b/spec/lib/sidebars/projects/super_sidebar_menus/monitor_menu_spec.rb
@@ -16,6 +16,7 @@ RSpec.describe Sidebars::Projects::SuperSidebarMenus::MonitorMenu, feature_categ
expect(items.map(&:class).uniq).to eq([Sidebars::NilMenuItem])
expect(items.map(&:item_id)).to eq([
:tracing,
+ :metrics,
:error_tracking,
:alert_management,
:incidents,
diff --git a/spec/lib/sidebars/user_settings/menus/comment_templates_menu_spec.rb b/spec/lib/sidebars/user_settings/menus/comment_templates_menu_spec.rb
index 37a383cfd9d..9a093efe6ba 100644
--- a/spec/lib/sidebars/user_settings/menus/comment_templates_menu_spec.rb
+++ b/spec/lib/sidebars/user_settings/menus/comment_templates_menu_spec.rb
@@ -15,10 +15,6 @@ RSpec.describe Sidebars::UserSettings::Menus::CommentTemplatesMenu, feature_cate
let_it_be(:user) { build(:user) }
context 'when comment templates are enabled' do
- before do
- allow(subject).to receive(:saved_replies_enabled?).and_return(true)
- end
-
context 'when user is logged in' do
let(:context) { Sidebars::Context.new(current_user: user, container: nil) }
@@ -37,29 +33,5 @@ RSpec.describe Sidebars::UserSettings::Menus::CommentTemplatesMenu, feature_cate
end
end
end
-
- context 'when comment templates are disabled' do
- before do
- allow(subject).to receive(:saved_replies_enabled?).and_return(false)
- end
-
- context 'when user is logged in' do
- let(:context) { Sidebars::Context.new(current_user: user, container: nil) }
-
- it 'renders' do
- expect(subject.render?).to be false
- end
- end
-
- context 'when user is not logged in' do
- let(:context) { Sidebars::Context.new(current_user: nil, container: nil) }
-
- subject { described_class.new(context) }
-
- it 'does not render' do
- expect(subject.render?).to be false
- end
- end
- end
end
end
diff --git a/spec/lib/system_check/orphans/namespace_check_spec.rb b/spec/lib/system_check/orphans/namespace_check_spec.rb
index e764c2313cd..3964068b20c 100644
--- a/spec/lib/system_check/orphans/namespace_check_spec.rb
+++ b/spec/lib/system_check/orphans/namespace_check_spec.rb
@@ -12,10 +12,10 @@ RSpec.describe SystemCheck::Orphans::NamespaceCheck, :silence_stdout do
describe '#multi_check' do
context 'all orphans' do
- let(:disk_namespaces) { %w(/repos/orphan1 /repos/orphan2 repos/@hashed) }
+ let(:disk_namespaces) { %w[/repos/orphan1 /repos/orphan2 repos/@hashed] }
it 'prints list of all orphaned namespaces except @hashed' do
- expect_list_of_orphans(%w(orphan1 orphan2))
+ expect_list_of_orphans(%w[orphan1 orphan2])
subject.multi_check
end
@@ -23,10 +23,10 @@ RSpec.describe SystemCheck::Orphans::NamespaceCheck, :silence_stdout do
context 'few orphans with existing namespace' do
let!(:first_level) { create(:group, path: 'my-namespace') }
- let(:disk_namespaces) { %w(/repos/orphan1 /repos/orphan2 /repos/my-namespace /repos/@hashed) }
+ let(:disk_namespaces) { %w[/repos/orphan1 /repos/orphan2 /repos/my-namespace /repos/@hashed] }
it 'prints list of orphaned namespaces' do
- expect_list_of_orphans(%w(orphan1 orphan2))
+ expect_list_of_orphans(%w[orphan1 orphan2])
subject.multi_check
end
@@ -35,17 +35,17 @@ RSpec.describe SystemCheck::Orphans::NamespaceCheck, :silence_stdout do
context 'few orphans with existing namespace and parents with same name as orphans' do
let!(:first_level) { create(:group, path: 'my-namespace') }
let!(:second_level) { create(:group, path: 'second-level', parent: first_level) }
- let(:disk_namespaces) { %w(/repos/orphan1 /repos/orphan2 /repos/my-namespace /repos/second-level /repos/@hashed) }
+ let(:disk_namespaces) { %w[/repos/orphan1 /repos/orphan2 /repos/my-namespace /repos/second-level /repos/@hashed] }
it 'prints list of orphaned namespaces ignoring parents with same namespace as orphans' do
- expect_list_of_orphans(%w(orphan1 orphan2 second-level))
+ expect_list_of_orphans(%w[orphan1 orphan2 second-level])
subject.multi_check
end
end
context 'no orphans' do
- let(:disk_namespaces) { %w(@hashed) }
+ let(:disk_namespaces) { %w[@hashed] }
it 'prints an empty list ignoring @hashed' do
expect_list_of_orphans([])
diff --git a/spec/lib/system_check/orphans/repository_check_spec.rb b/spec/lib/system_check/orphans/repository_check_spec.rb
index 91b48969cc1..0504e133ab9 100644
--- a/spec/lib/system_check/orphans/repository_check_spec.rb
+++ b/spec/lib/system_check/orphans/repository_check_spec.rb
@@ -13,11 +13,11 @@ RSpec.describe SystemCheck::Orphans::RepositoryCheck, :silence_stdout do
describe '#multi_check' do
context 'all orphans' do
- let(:disk_namespaces) { %w(/repos/orphan1 /repos/orphan2 repos/@hashed) }
- let(:disk_repositories) { %w(repo1.git repo2.git) }
+ let(:disk_namespaces) { %w[/repos/orphan1 /repos/orphan2 repos/@hashed] }
+ let(:disk_repositories) { %w[repo1.git repo2.git] }
it 'prints list of all orphaned namespaces except @hashed' do
- expect_list_of_orphans(%w(orphan1/repo1.git orphan1/repo2.git orphan2/repo1.git orphan2/repo2.git))
+ expect_list_of_orphans(%w[orphan1/repo1.git orphan1/repo2.git orphan2/repo1.git orphan2/repo2.git])
subject.multi_check
end
@@ -26,11 +26,11 @@ RSpec.describe SystemCheck::Orphans::RepositoryCheck, :silence_stdout do
context 'few orphans with existing namespace' do
let!(:first_level) { create(:group, path: 'my-namespace') }
let!(:project) { create(:project, path: 'repo', namespace: first_level) }
- let(:disk_namespaces) { %w(/repos/orphan1 /repos/orphan2 /repos/my-namespace /repos/@hashed) }
- let(:disk_repositories) { %w(repo.git) }
+ let(:disk_namespaces) { %w[/repos/orphan1 /repos/orphan2 /repos/my-namespace /repos/@hashed] }
+ let(:disk_repositories) { %w[repo.git] }
it 'prints list of orphaned namespaces' do
- expect_list_of_orphans(%w(orphan1/repo.git orphan2/repo.git))
+ expect_list_of_orphans(%w[orphan1/repo.git orphan2/repo.git])
subject.multi_check
end
@@ -40,19 +40,19 @@ RSpec.describe SystemCheck::Orphans::RepositoryCheck, :silence_stdout do
let!(:first_level) { create(:group, path: 'my-namespace') }
let!(:second_level) { create(:group, path: 'second-level', parent: first_level) }
let!(:project) { create(:project, path: 'repo', namespace: first_level) }
- let(:disk_namespaces) { %w(/repos/orphan1 /repos/orphan2 /repos/my-namespace /repos/second-level /repos/@hashed) }
- let(:disk_repositories) { %w(repo.git) }
+ let(:disk_namespaces) { %w[/repos/orphan1 /repos/orphan2 /repos/my-namespace /repos/second-level /repos/@hashed] }
+ let(:disk_repositories) { %w[repo.git] }
it 'prints list of orphaned namespaces ignoring parents with same namespace as orphans' do
- expect_list_of_orphans(%w(orphan1/repo.git orphan2/repo.git second-level/repo.git))
+ expect_list_of_orphans(%w[orphan1/repo.git orphan2/repo.git second-level/repo.git])
subject.multi_check
end
end
context 'no orphans' do
- let(:disk_namespaces) { %w(@hashed) }
- let(:disk_repositories) { %w(repo.git) }
+ let(:disk_namespaces) { %w[@hashed] }
+ let(:disk_repositories) { %w[repo.git] }
it 'prints an empty list ignoring @hashed' do
expect_list_of_orphans([])
diff --git a/spec/lib/system_check/sidekiq_check_spec.rb b/spec/lib/system_check/sidekiq_check_spec.rb
index ff4eece8f7c..efd5414294a 100644
--- a/spec/lib/system_check/sidekiq_check_spec.rb
+++ b/spec/lib/system_check/sidekiq_check_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe SystemCheck::SidekiqCheck do
describe '#multi_check' do
def stub_ps_output(output)
- allow(Gitlab::Popen).to receive(:popen).with(%w(ps uxww)).and_return([output, nil])
+ allow(Gitlab::Popen).to receive(:popen).with(%w[ps uxww]).and_return([output, nil])
end
def expect_check_output(matcher)
diff --git a/spec/lib/unnested_in_filters/dsl_spec.rb b/spec/lib/unnested_in_filters/dsl_spec.rb
index bce4c88f94c..9f1552b02ec 100644
--- a/spec/lib/unnested_in_filters/dsl_spec.rb
+++ b/spec/lib/unnested_in_filters/dsl_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe UnnestedInFilters::Dsl do
end
describe '#exists?' do
- let(:states) { %w(active banned) }
+ let(:states) { %w[active banned] }
subject { test_model.where(state: states).use_unnested_filters.exists? }
diff --git a/spec/lib/unnested_in_filters/rewriter_spec.rb b/spec/lib/unnested_in_filters/rewriter_spec.rb
index ea561c42993..945a50ce2e8 100644
--- a/spec/lib/unnested_in_filters/rewriter_spec.rb
+++ b/spec/lib/unnested_in_filters/rewriter_spec.rb
@@ -26,7 +26,7 @@ RSpec.describe UnnestedInFilters::Rewriter do
context 'when the given relation has an `IN` predicate' do
context 'when there is no index coverage for the used columns' do
- let(:relation) { User.where(username: %w(user_1 user_2), state: :active) }
+ let(:relation) { User.where(username: %w[user_1 user_2], state: :active) }
it { is_expected.to be_falsey }
end
@@ -37,7 +37,7 @@ RSpec.describe UnnestedInFilters::Rewriter do
it { is_expected.to be_truthy }
context 'when there is an ordering' do
- let(:relation) { User.where(state: %w(active blocked banned)).order(order).limit(2) }
+ let(:relation) { User.where(state: %w[active blocked banned]).order(order).limit(2) }
context 'when the order is an Arel node' do
let(:order) { { user_type: :desc } }
@@ -67,7 +67,7 @@ RSpec.describe UnnestedInFilters::Rewriter do
describe '#rewrite' do
let(:recorded_queries) { ActiveRecord::QueryRecorder.new { rewriter.rewrite.load } }
- let(:relation) { User.where(state: :active, user_type: %i(support_bot alert_bot)).limit(2) }
+ let(:relation) { User.where(state: :active, user_type: %i[support_bot alert_bot]).limit(2) }
let(:users_select) { 'SELECT "users".*' }
let(:users_select_with_ignored_columns) { 'SELECT ("users"."\w+", )+("users"."\w+")' }
@@ -101,7 +101,7 @@ RSpec.describe UnnestedInFilters::Rewriter do
end
context 'when the relation has a subquery' do
- let(:relation) { User.where(state: User.select(:state), user_type: %i(support_bot alert_bot)).limit(1) }
+ let(:relation) { User.where(state: User.select(:state), user_type: %i[support_bot alert_bot]).limit(1) }
let(:users_unnest) do
'FROM
@@ -127,7 +127,7 @@ RSpec.describe UnnestedInFilters::Rewriter do
end
context 'when there is an order' do
- let(:relation) { User.where(state: %w(active blocked banned)).order(order).limit(2) }
+ let(:relation) { User.where(state: %w[active blocked banned]).order(order).limit(2) }
let(:users_unnest) do
'FROM
@@ -177,7 +177,7 @@ RSpec.describe UnnestedInFilters::Rewriter do
end
context 'when the combined attributes include the primary key' do
- let(:relation) { User.where(user_type: %i(support_bot alert_bot)).order(id: :desc).limit(2) }
+ let(:relation) { User.where(user_type: %i[support_bot alert_bot]).order(id: :desc).limit(2) }
let(:users_where) do
'FROM
diff --git a/spec/mailers/emails/pages_domains_spec.rb b/spec/mailers/emails/pages_domains_spec.rb
index ff446b83412..88e5d60d6d3 100644
--- a/spec/mailers/emails/pages_domains_spec.rb
+++ b/spec/mailers/emails/pages_domains_spec.rb
@@ -33,7 +33,7 @@ RSpec.describe Emails::PagesDomains do
it 'has the expected content' do
is_expected.to have_body_text domain.url
- is_expected.to have_body_text help_page_url('user/project/pages/custom_domains_ssl_tls_certification/index.md', anchor: link_anchor)
+ is_expected.to have_body_text help_page_url('user/project/pages/custom_domains_ssl_tls_certification/index', anchor: link_anchor)
end
end
@@ -112,7 +112,7 @@ RSpec.describe Emails::PagesDomains do
it 'says that we failed to obtain certificate' do
is_expected.to have_body_text "Something went wrong while obtaining the Let's Encrypt certificate."
- is_expected.to have_body_text help_page_url('user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration.md', anchor: 'troubleshooting')
+ is_expected.to have_body_text help_page_url('user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration', anchor: 'troubleshooting')
end
end
end
diff --git a/spec/metrics_server/metrics_server_spec.rb b/spec/metrics_server/metrics_server_spec.rb
index baf15a773b1..1d53ba194b5 100644
--- a/spec/metrics_server/metrics_server_spec.rb
+++ b/spec/metrics_server/metrics_server_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_relative '../../metrics_server/metrics_server'
-RSpec.describe MetricsServer, feature_category: :application_performance do # rubocop:disable RSpec/FilePath
+RSpec.describe MetricsServer, feature_category: :cloud_connector do
let(:prometheus_config) { ::Prometheus::Client.configuration }
let(:metrics_dir) { Dir.mktmpdir }
@@ -16,7 +16,7 @@ RSpec.describe MetricsServer, feature_category: :application_performance do # ru
before do
# Make sure we never actually spawn any new processes in a unit test.
- %i(spawn fork detach).each { |m| allow(Process).to receive(m) }
+ %i[spawn fork detach].each { |m| allow(Process).to receive(m) }
# We do not want this to have knock-on effects on the test process.
allow(Gitlab::ProcessManagement).to receive(:modify_signals)
@@ -33,7 +33,7 @@ RSpec.describe MetricsServer, feature_category: :application_performance do # ru
FileUtils.rm_rf(metrics_dir, secure: true)
end
- %w(puma sidekiq).each do |target|
+ %w[puma sidekiq].each do |target|
context "when targeting #{target}" do
describe '.fork' do
context 'when in parent process' do
diff --git a/spec/migrations/20230929155123_migrate_disable_merge_trains_value_spec.rb b/spec/migrations/20230929155123_migrate_disable_merge_trains_value_spec.rb
new file mode 100644
index 00000000000..ee011687bbb
--- /dev/null
+++ b/spec/migrations/20230929155123_migrate_disable_merge_trains_value_spec.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration! 'migrate_disable_merge_trains_value'
+
+RSpec.describe MigrateDisableMergeTrainsValue, schema: 20230929155123, feature_category: :continuous_integration do
+ let!(:feature_gates) { table(:feature_gates) }
+ let!(:projects) { table(:projects) }
+ let!(:project_ci_cd_settings) { table(:project_ci_cd_settings) }
+ let!(:namespace1) { table(:namespaces).create!(name: 'name1', path: 'path1') }
+ let!(:namespace2) { table(:namespaces).create!(name: 'name2', path: 'path2') }
+
+ let!(:project_with_flag_on) do
+ projects
+ .create!(
+ name: "project",
+ path: "project",
+ namespace_id: namespace1.id,
+ project_namespace_id: namespace1.id
+ )
+ end
+
+ let!(:project_with_flag_off) do
+ projects
+ .create!(
+ name: "project2",
+ path: "project2",
+ namespace_id: namespace2.id,
+ project_namespace_id: namespace2.id
+ )
+ end
+
+ let!(:settings_flag_on) do
+ project_ci_cd_settings.create!(
+ merge_trains_enabled: true,
+ project_id: project_with_flag_on.id
+ )
+ end
+
+ let!(:settings_flag_off) do
+ project_ci_cd_settings.create!(
+ merge_trains_enabled: true,
+ project_id: project_with_flag_off.id
+ )
+ end
+
+ let!(:migration) { described_class.new }
+
+ before do
+ # Enable the feature flag
+ feature_gates.create!(
+ feature_key: 'disable_merge_trains',
+ key: 'actors',
+ value: "Project:#{project_with_flag_on.id}"
+ )
+
+ migration.up
+ end
+
+ describe '#up' do
+ it 'migrates the flag value into the setting value' do
+ expect(
+ settings_flag_on.reload.merge_trains_enabled
+ ).to eq(false)
+ expect(
+ settings_flag_off.reload.merge_trains_enabled
+ ).to eq(true)
+ end
+ end
+
+ describe '#down' do
+ it 'reverts the migration' do
+ migration.down
+
+ expect(
+ settings_flag_on.reload.merge_trains_enabled
+ ).to eq(true)
+ expect(
+ settings_flag_off.reload.merge_trains_enabled
+ ).to eq(true)
+ end
+ end
+end
diff --git a/spec/migrations/20231003045342_migrate_sidekiq_namespaced_jobs_spec.rb b/spec/migrations/20231003045342_migrate_sidekiq_namespaced_jobs_spec.rb
new file mode 100644
index 00000000000..9e170ff33a4
--- /dev/null
+++ b/spec/migrations/20231003045342_migrate_sidekiq_namespaced_jobs_spec.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe MigrateSidekiqNamespacedJobs, :migration, feature_category: :scalability do
+ before do
+ q1 = instance_double(Sidekiq::Queue, name: 'q1')
+ q2 = instance_double(Sidekiq::Queue, name: 'q2')
+ allow(Sidekiq::Queue).to receive(:all).and_return([q1, q2])
+
+ Gitlab::Redis::Queues.with do |redis|
+ (1..1000).each do |i|
+ redis.zadd('resque:gitlab:schedule', [i, i])
+ redis.zadd('resque:gitlab:retry', [i, i])
+ redis.zadd('resque:gitlab:dead', [i, i])
+ end
+
+ Sidekiq::Queue.all.each do |queue|
+ (1..1000).each do |i|
+ redis.lpush("resque:gitlab:queue:#{queue.name}", i)
+ end
+ end
+ end
+ end
+
+ after do
+ Gitlab::Redis::Queues.with(&:flushdb)
+ end
+
+ it "does not creates default organization if needed" do
+ reversible_migration do |migration|
+ migration.before -> {
+ Gitlab::Redis::Queues.with do |redis|
+ expect(redis.zcard('resque:gitlab:schedule')).to eq(1000)
+ expect(redis.zcard('resque:gitlab:retry')).to eq(1000)
+ expect(redis.zcard('resque:gitlab:dead')).to eq(1000)
+ expect(redis.zcard('schedule')).to eq(0)
+ expect(redis.zcard('retry')).to eq(0)
+ expect(redis.zcard('dead')).to eq(0)
+
+ Sidekiq::Queue.all.each do |queue|
+ expect(redis.llen("resque:gitlab:queue:#{queue.name}")).to eq(1000)
+ expect(redis.llen("queue:#{queue.name}")).to eq(0)
+ end
+ end
+ }
+
+ migration.after -> {
+ # no namespaced keys
+ Gitlab::Redis::Queues.with do |redis|
+ expect(redis.zcard('resque:gitlab:schedule')).to eq(0)
+ expect(redis.zcard('resque:gitlab:retry')).to eq(0)
+ expect(redis.zcard('resque:gitlab:dead')).to eq(0)
+ expect(redis.zcard('schedule')).to eq(1000)
+ expect(redis.zcard('retry')).to eq(1000)
+ expect(redis.zcard('dead')).to eq(1000)
+
+ Sidekiq::Queue.all.each do |queue|
+ expect(redis.llen("resque:gitlab:queue:#{queue.name}")).to eq(0)
+ expect(redis.llen("queue:#{queue.name}")).to eq(1000)
+ end
+ end
+ }
+ end
+ end
+end
diff --git a/spec/migrations/20231016001000_fix_design_user_mentions_design_id_note_id_index_for_self_managed_spec.rb b/spec/migrations/20231016001000_fix_design_user_mentions_design_id_note_id_index_for_self_managed_spec.rb
new file mode 100644
index 00000000000..8d890bb7cac
--- /dev/null
+++ b/spec/migrations/20231016001000_fix_design_user_mentions_design_id_note_id_index_for_self_managed_spec.rb
@@ -0,0 +1,114 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require_migration!
+
+RSpec.describe FixDesignUserMentionsDesignIdNoteIdIndexForSelfManaged, feature_category: :database do
+ let(:connection) { described_class.new.connection }
+ let(:design_user_mentions) { table(:design_user_mentions) }
+
+ shared_examples 'index `design_user_mentions_on_design_id_and_note_id_unique_index` already exists' do
+ it 'does not swap the columns' do
+ disable_migrations_output do
+ reversible_migration do |migration|
+ migration.before -> {
+ index = connection.indexes(:design_user_mentions).find do |i|
+ i.name == 'design_user_mentions_on_design_id_and_note_id_unique_index'
+ end
+ expect(index.columns).to eq(%w[design_id note_id])
+ }
+
+ migration.after -> {
+ index = connection.indexes(:design_user_mentions).find do |i|
+ i.name == 'design_user_mentions_on_design_id_and_note_id_unique_index'
+ end
+ expect(index.columns).to eq(%w[design_id note_id])
+ }
+ end
+ end
+ end
+ end
+
+ describe '#up' do
+ before do
+ # rubocop:disable RSpec/AnyInstanceOf
+ allow_any_instance_of(described_class).to(
+ receive(:com_or_dev_or_test_but_not_jh?).and_return(com_or_dev_or_test_but_not_jh?)
+ )
+ # rubocop:enable RSpec/AnyInstanceOf
+ end
+
+ context 'when GitLab.com, dev, or test' do
+ let(:com_or_dev_or_test_but_not_jh?) { true }
+
+ it_behaves_like 'index `design_user_mentions_on_design_id_and_note_id_unique_index` already exists'
+ end
+
+ context 'when self-managed instance' do
+ let(:com_or_dev_or_test_but_not_jh?) { false }
+
+ context "when index does not exist" do
+ before do
+ connection.execute('DROP INDEX IF EXISTS design_user_mentions_on_design_id_and_note_id_unique_index')
+ end
+
+ after do
+ connection.execute('CREATE UNIQUE INDEX IF NOT EXISTS
+ design_user_mentions_on_design_id_and_note_id_unique_index
+ ON design_user_mentions (design_id, note_id)')
+ end
+
+ it 'creates the index' do
+ disable_migrations_output { migrate! }
+
+ index = connection.indexes(:design_user_mentions).find do |i|
+ i.name == 'design_user_mentions_on_design_id_and_note_id_unique_index'
+ end
+
+ expect(index.columns).to eq(%w[design_id note_id])
+ end
+ end
+
+ context "when index does exist" do
+ it_behaves_like 'index `design_user_mentions_on_design_id_and_note_id_unique_index` already exists'
+ end
+
+ context "when index does exists on the int4 column" do
+ before do
+ connection.execute('DROP INDEX IF EXISTS design_user_mentions_on_design_id_and_note_id_unique_index')
+ connection.execute(
+ 'ALTER TABLE design_user_mentions ADD COLUMN IF NOT EXISTS note_id_convert_to_bigint integer'
+ )
+ connection.execute('CREATE UNIQUE INDEX
+ design_user_mentions_on_design_id_and_note_id_unique_index
+ ON design_user_mentions (design_id, note_id_convert_to_bigint)')
+ end
+
+ after do
+ connection.execute('DROP INDEX IF EXISTS design_user_mentions_on_design_id_and_note_id_unique_index')
+ connection.execute('ALTER TABLE design_user_mentions DROP COLUMN IF EXISTS note_id_convert_to_bigint')
+ connection.execute('CREATE UNIQUE INDEX
+ design_user_mentions_on_design_id_and_note_id_unique_index
+ ON design_user_mentions (design_id, note_id)')
+ end
+
+ it 'creates the index on the int8 column' do
+ index = connection.indexes(:design_user_mentions).find do |i|
+ i.name == 'design_user_mentions_on_design_id_and_note_id_unique_index'
+ end
+
+ expect(index.columns).to eq(%w[design_id note_id_convert_to_bigint])
+
+ disable_migrations_output { migrate! }
+
+ index = connection.indexes(:design_user_mentions).find do |i|
+ i.name == 'design_user_mentions_on_design_id_and_note_id_unique_index'
+ end
+
+ expect(index.columns).to eq(%w[design_id note_id])
+ end
+ end
+ end
+ end
+end
diff --git a/spec/migrations/20231016173129_queue_delete_invalid_protected_branch_merge_access_levels_spec.rb b/spec/migrations/20231016173129_queue_delete_invalid_protected_branch_merge_access_levels_spec.rb
new file mode 100644
index 00000000000..292fdca026f
--- /dev/null
+++ b/spec/migrations/20231016173129_queue_delete_invalid_protected_branch_merge_access_levels_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe QueueDeleteInvalidProtectedBranchMergeAccessLevels, feature_category: :source_code_management do
+ let!(:batched_migration) { described_class::MIGRATION }
+
+ it 'schedules a new batched migration' do
+ reversible_migration do |migration|
+ migration.before -> {
+ expect(batched_migration).not_to have_scheduled_batched_migration
+ }
+
+ migration.after -> {
+ expect(batched_migration).to have_scheduled_batched_migration(
+ table_name: :protected_branch_merge_access_levels,
+ column_name: :id,
+ interval: described_class::DELAY_INTERVAL,
+ batch_size: described_class::BATCH_SIZE,
+ sub_batch_size: described_class::SUB_BATCH_SIZE
+ )
+ }
+ end
+ end
+end
diff --git a/spec/migrations/20231016194927_queue_delete_invalid_protected_branch_push_access_levels_spec.rb b/spec/migrations/20231016194927_queue_delete_invalid_protected_branch_push_access_levels_spec.rb
new file mode 100644
index 00000000000..db42bb05f15
--- /dev/null
+++ b/spec/migrations/20231016194927_queue_delete_invalid_protected_branch_push_access_levels_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe QueueDeleteInvalidProtectedBranchPushAccessLevels, feature_category: :source_code_management do
+ let!(:batched_migration) { described_class::MIGRATION }
+
+ it 'schedules a new batched migration' do
+ reversible_migration do |migration|
+ migration.before -> {
+ expect(batched_migration).not_to have_scheduled_batched_migration
+ }
+
+ migration.after -> {
+ expect(batched_migration).to have_scheduled_batched_migration(
+ table_name: :protected_branch_push_access_levels,
+ column_name: :id,
+ interval: described_class::DELAY_INTERVAL,
+ batch_size: described_class::BATCH_SIZE,
+ sub_batch_size: described_class::SUB_BATCH_SIZE
+ )
+ }
+ end
+ end
+end
diff --git a/spec/migrations/20231016194943_queue_delete_invalid_protected_tag_create_access_levels_spec.rb b/spec/migrations/20231016194943_queue_delete_invalid_protected_tag_create_access_levels_spec.rb
new file mode 100644
index 00000000000..4acc46a65c5
--- /dev/null
+++ b/spec/migrations/20231016194943_queue_delete_invalid_protected_tag_create_access_levels_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe QueueDeleteInvalidProtectedTagCreateAccessLevels, feature_category: :source_code_management do
+ let!(:batched_migration) { described_class::MIGRATION }
+
+ it 'schedules a new batched migration' do
+ reversible_migration do |migration|
+ migration.before -> {
+ expect(batched_migration).not_to have_scheduled_batched_migration
+ }
+
+ migration.after -> {
+ expect(batched_migration).to have_scheduled_batched_migration(
+ table_name: :protected_tag_create_access_levels,
+ column_name: :id,
+ interval: described_class::DELAY_INTERVAL,
+ batch_size: described_class::BATCH_SIZE,
+ sub_batch_size: described_class::SUB_BATCH_SIZE
+ )
+ }
+ end
+ end
+end
diff --git a/spec/migrations/20231019003052_swap_columns_for_ci_pipelines_pipeline_id_bigint_v2_spec.rb b/spec/migrations/20231019003052_swap_columns_for_ci_pipelines_pipeline_id_bigint_v2_spec.rb
new file mode 100644
index 00000000000..9fc07a0ac76
--- /dev/null
+++ b/spec/migrations/20231019003052_swap_columns_for_ci_pipelines_pipeline_id_bigint_v2_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe SwapColumnsForCiPipelinesPipelineIdBigintV2, feature_category: :continuous_integration do
+ context 'when auto_canceled_by_id sql type is integer' do
+ before do
+ active_record_base.connection.execute(<<~SQL)
+ ALTER TABLE ci_pipelines ALTER COLUMN auto_canceled_by_id TYPE integer;
+ ALTER TABLE ci_pipelines ALTER COLUMN auto_canceled_by_id_convert_to_bigint TYPE bigint;
+ SQL
+ end
+
+ it_behaves_like(
+ 'swap conversion columns',
+ table_name: :ci_pipelines,
+ from: :auto_canceled_by_id,
+ to: :auto_canceled_by_id_convert_to_bigint
+ )
+ end
+
+ context 'when auto_canceled_by_id sql type is bigint' do
+ before do
+ active_record_base.connection.execute(<<~SQL)
+ ALTER TABLE ci_pipelines ALTER COLUMN auto_canceled_by_id TYPE bigint;
+ ALTER TABLE ci_pipelines ALTER COLUMN auto_canceled_by_id_convert_to_bigint TYPE integer;
+ SQL
+ end
+
+ it 'does nothing' do
+ recorder = ActiveRecord::QueryRecorder.new { migrate! }
+ expect(recorder.log).not_to include(/LOCK TABLE/)
+ expect(recorder.log).not_to include(/ALTER TABLE/)
+ end
+ end
+end
diff --git a/spec/migrations/20231019084731_swap_columns_for_ci_stages_pipeline_id_bigint_v2_spec.rb b/spec/migrations/20231019084731_swap_columns_for_ci_stages_pipeline_id_bigint_v2_spec.rb
new file mode 100644
index 00000000000..266786dda3a
--- /dev/null
+++ b/spec/migrations/20231019084731_swap_columns_for_ci_stages_pipeline_id_bigint_v2_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe SwapColumnsForCiStagesPipelineIdBigintV2, feature_category: :continuous_integration do
+ context 'when pipeline_id sql type is integer' do
+ before do
+ active_record_base.connection.execute(<<~SQL)
+ ALTER TABLE ci_stages ALTER COLUMN pipeline_id TYPE integer;
+ ALTER TABLE ci_stages ALTER COLUMN pipeline_id_convert_to_bigint TYPE bigint;
+ SQL
+ end
+
+ it_behaves_like(
+ 'swap conversion columns',
+ table_name: :ci_stages,
+ from: :pipeline_id,
+ to: :pipeline_id_convert_to_bigint
+ )
+ end
+
+ context 'when pipeline_id sql type is bigint' do
+ before do
+ active_record_base.connection.execute(<<~SQL)
+ ALTER TABLE ci_stages ALTER COLUMN pipeline_id TYPE bigint;
+ ALTER TABLE ci_stages ALTER COLUMN pipeline_id_convert_to_bigint TYPE integer;
+ SQL
+ end
+
+ it 'does nothing' do
+ recorder = ActiveRecord::QueryRecorder.new { migrate! }
+ expect(recorder.log).not_to include(/LOCK TABLE/)
+ expect(recorder.log).not_to include(/ALTER TABLE/)
+ end
+ end
+end
diff --git a/spec/migrations/20231019145202_add_status_to_packages_npm_metadata_caches_spec.rb b/spec/migrations/20231019145202_add_status_to_packages_npm_metadata_caches_spec.rb
new file mode 100644
index 00000000000..eeeafbdc277
--- /dev/null
+++ b/spec/migrations/20231019145202_add_status_to_packages_npm_metadata_caches_spec.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe AddStatusToPackagesNpmMetadataCaches, feature_category: :package_registry do
+ let(:npm_metadata_caches) { table(:packages_npm_metadata_caches) }
+
+ it 'correctly migrates up and down' do
+ reversible_migration do |migration|
+ migration.before -> {
+ expect(npm_metadata_caches.column_names).not_to include('status')
+ }
+
+ migration.after -> {
+ npm_metadata_caches.reset_column_information
+
+ expect(npm_metadata_caches.column_names).to include('status')
+ }
+ end
+ end
+end
diff --git a/spec/migrations/20231019223224_backfill_catalog_resources_name_and_description_spec.rb b/spec/migrations/20231019223224_backfill_catalog_resources_name_and_description_spec.rb
new file mode 100644
index 00000000000..2945b9fbf8e
--- /dev/null
+++ b/spec/migrations/20231019223224_backfill_catalog_resources_name_and_description_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe BackfillCatalogResourcesNameAndDescription, feature_category: :pipeline_composition do
+ let(:namespace) { table(:namespaces).create!(name: 'name', path: 'path') }
+
+ let(:project) do
+ table(:projects).create!(
+ name: 'My project name', description: 'My description',
+ namespace_id: namespace.id, project_namespace_id: namespace.id
+ )
+ end
+
+ let(:resource) { table(:catalog_resources).create!(project_id: project.id) }
+
+ describe '#up' do
+ it 'updates the name and description to match the project' do
+ expect(resource.name).to be_nil
+ expect(resource.description).to be_nil
+
+ migrate!
+
+ expect(resource.reload.name).to eq(project.name)
+ expect(resource.reload.description).to eq(project.description)
+ end
+ end
+end
diff --git a/spec/migrations/20231020181652_add_index_packages_npm_metadata_caches_on_id_and_project_id_and_status_spec.rb b/spec/migrations/20231020181652_add_index_packages_npm_metadata_caches_on_id_and_project_id_and_status_spec.rb
new file mode 100644
index 00000000000..412ea33cb4e
--- /dev/null
+++ b/spec/migrations/20231020181652_add_index_packages_npm_metadata_caches_on_id_and_project_id_and_status_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe AddIndexPackagesNpmMetadataCachesOnIdAndProjectIdAndStatus, feature_category: :package_registry do
+ let(:index_name) { described_class::INDEX_NAME }
+
+ it 'correctly migrates up and down' do
+ reversible_migration do |migration|
+ migration.before -> {
+ expect(ActiveRecord::Base.connection.indexes(:packages_npm_metadata_caches).map(&:name))
+ .not_to include(index_name)
+ }
+
+ migration.after -> {
+ # npm_metadata_caches.reset_column_information
+
+ expect(ActiveRecord::Base.connection.indexes(:packages_npm_metadata_caches).map(&:name))
+ .to include(index_name)
+ }
+ end
+ end
+end
diff --git a/spec/migrations/20231030071209_queue_backfill_packages_tags_project_id_spec.rb b/spec/migrations/20231030071209_queue_backfill_packages_tags_project_id_spec.rb
new file mode 100644
index 00000000000..98ce121c03b
--- /dev/null
+++ b/spec/migrations/20231030071209_queue_backfill_packages_tags_project_id_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe QueueBackfillPackagesTagsProjectId, feature_category: :package_registry do
+ let!(:batched_migration) { described_class::MIGRATION }
+
+ it 'schedules a new batched migration' do
+ reversible_migration do |migration|
+ migration.before -> {
+ expect(batched_migration).not_to have_scheduled_batched_migration
+ }
+
+ migration.after -> {
+ expect(batched_migration).to have_scheduled_batched_migration(
+ table_name: :packages_tags,
+ column_name: :id,
+ interval: described_class::DELAY_INTERVAL,
+ batch_size: described_class::BATCH_SIZE,
+ sub_batch_size: described_class::SUB_BATCH_SIZE
+ )
+ }
+ end
+ end
+end
diff --git a/spec/migrations/20231102142554_migrate_zoekt_shards_to_zoekt_nodes_spec.rb b/spec/migrations/20231102142554_migrate_zoekt_shards_to_zoekt_nodes_spec.rb
new file mode 100644
index 00000000000..5f1d691f923
--- /dev/null
+++ b/spec/migrations/20231102142554_migrate_zoekt_shards_to_zoekt_nodes_spec.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe MigrateZoektShardsToZoektNodes, feature_category: :global_search do
+ let!(:migration) { described_class.new }
+
+ let(:attributes) do
+ {
+ index_base_url: "https://index.example.com",
+ search_base_url: "https://search.example.com",
+ uuid: SecureRandom.uuid,
+ used_bytes: 10,
+ total_bytes: 100
+ }.with_indifferent_access
+ end
+
+ let(:zoekt_shards) { table(:zoekt_shards) }
+ let(:zoekt_nodes) { table(:zoekt_nodes) }
+
+ let(:shard) do
+ zoekt_shards.create!(attributes)
+ end
+
+ let(:node) do
+ zoekt_nodes.create!(attributes)
+ end
+
+ describe '#up' do
+ it 'migrates zoekt_shard records to zoekt_nodes' do
+ shard
+ expect { migrate! }.to change { zoekt_nodes.count }.from(0).to(1)
+ expect(zoekt_nodes.first.attributes.with_indifferent_access).to include(attributes)
+ end
+ end
+
+ describe '#down' do
+ it 'deletes all zoekt_node records' do
+ node
+ expect { migration.down }.to change { zoekt_nodes.count }.from(1).to(0)
+ end
+ end
+end
diff --git a/spec/migrations/20231103223224_backfill_zoekt_node_id_on_indexed_namespaces_spec.rb b/spec/migrations/20231103223224_backfill_zoekt_node_id_on_indexed_namespaces_spec.rb
new file mode 100644
index 00000000000..60f08071af6
--- /dev/null
+++ b/spec/migrations/20231103223224_backfill_zoekt_node_id_on_indexed_namespaces_spec.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe BackfillZoektNodeIdOnIndexedNamespaces, feature_category: :global_search do
+ let!(:migration) { described_class.new }
+
+ let(:namespace) { table(:namespaces).create!(name: 'name', path: 'path') }
+
+ let(:zoekt_indexed_namespaces) { table(:zoekt_indexed_namespaces) }
+ let(:zoekt_shards) { table(:zoekt_shards) }
+ let(:zoekt_nodes) { table(:zoekt_nodes) }
+
+ let(:indexed_namespace) do
+ zoekt_indexed_namespaces.create!(
+ zoekt_shard_id: shard.id,
+ namespace_id: namespace.id
+ )
+ end
+
+ let(:attributes) do
+ {
+ index_base_url: "https://index.example.com",
+ search_base_url: "https://search.example.com",
+ uuid: SecureRandom.uuid,
+ used_bytes: 10,
+ total_bytes: 100
+ }.with_indifferent_access
+ end
+
+ let(:shard) do
+ zoekt_shards.create!(attributes)
+ end
+
+ let(:node) do
+ zoekt_nodes.create!(attributes)
+ end
+
+ describe '#up' do
+ it 'backfills zoekt_node_id with zoekt_shard_id' do
+ node
+ expect(indexed_namespace.zoekt_node_id).to be_nil
+ expect(indexed_namespace.zoekt_shard_id).to eq(shard.id)
+ migrate!
+ expect(indexed_namespace.reload.zoekt_node_id).to eq(node.id)
+ end
+
+ context 'when there is somehow more than one zoekt node' do
+ let(:node) do
+ zoekt_nodes.create!(
+ index_base_url: "https://index.example.com",
+ search_base_url: "https://search.example.com",
+ uuid: SecureRandom.uuid,
+ used_bytes: 10,
+ total_bytes: 100,
+ created_at: 5.days.ago
+ )
+ end
+
+ let(:node_2) do
+ zoekt_nodes.create!(
+ index_base_url: "https://index2.example.com",
+ search_base_url: "https://search2example.com",
+ uuid: SecureRandom.uuid,
+ used_bytes: 10,
+ total_bytes: 100
+ )
+ end
+
+ it 'uses the latest zoekt node' do
+ expect(node_2.created_at).to be > node.created_at
+ expect(indexed_namespace.zoekt_node_id).to be_nil
+ migrate!
+ expect(indexed_namespace.reload.zoekt_node_id).to eq(node_2.id)
+ end
+ end
+ end
+end
diff --git a/spec/migrations/db/migrate/20231103162825_add_wolfi_purl_type_to_package_metadata_purl_types_spec.rb b/spec/migrations/db/migrate/20231103162825_add_wolfi_purl_type_to_package_metadata_purl_types_spec.rb
new file mode 100644
index 00000000000..43ce53fffcb
--- /dev/null
+++ b/spec/migrations/db/migrate/20231103162825_add_wolfi_purl_type_to_package_metadata_purl_types_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe AddWolfiPurlTypeToPackageMetadataPurlTypes, feature_category: :software_composition_analysis do
+ let(:settings) { table(:application_settings) }
+
+ describe "#up" do
+ it 'updates setting' do
+ settings.create!(package_metadata_purl_types: [1, 2, 4, 5, 9, 10])
+
+ disable_migrations_output do
+ migrate!
+ end
+
+ expect(ApplicationSetting.last.package_metadata_purl_types).to eq([1, 2, 4, 5, 9, 10, 13])
+ end
+ end
+
+ describe "#down" do
+ context 'with default value' do
+ it 'updates setting' do
+ settings.create!(package_metadata_purl_types: [1, 2, 4, 5, 9, 10, 13])
+
+ disable_migrations_output do
+ migrate!
+ schema_migrate_down!
+ end
+
+ expect(ApplicationSetting.last.package_metadata_purl_types).to eq([1, 2, 4, 5, 9, 10])
+ end
+ end
+ end
+end
diff --git a/spec/migrations/schedule_fixing_security_scan_statuses_spec.rb b/spec/migrations/schedule_fixing_security_scan_statuses_spec.rb
index 56d30e71676..f43f58d3be2 100644
--- a/spec/migrations/schedule_fixing_security_scan_statuses_spec.rb
+++ b/spec/migrations/schedule_fixing_security_scan_statuses_spec.rb
@@ -4,20 +4,21 @@ require 'spec_helper'
require_migration!
RSpec.describe ScheduleFixingSecurityScanStatuses,
- :suppress_gitlab_schemas_validate_connection, feature_category: :vulnerability_management do
+ :suppress_gitlab_schemas_validate_connection, :suppress_partitioning_routing_analyzer,
+ feature_category: :vulnerability_management do
let!(:namespaces) { table(:namespaces) }
let!(:projects) { table(:projects) }
- let!(:pipelines) { table(:ci_pipelines) }
- let!(:builds) { table(:ci_builds) }
+ let!(:pipelines) { table(:ci_pipelines, database: :ci) }
+ let!(:builds) { table(:ci_builds, database: :ci) { |model| model.primary_key = :id } }
let!(:security_scans) { table(:security_scans) }
let!(:namespace) { namespaces.create!(name: "foo", path: "bar") }
let!(:project) { projects.create!(namespace_id: namespace.id, project_namespace_id: namespace.id) }
let!(:pipeline) do
- pipelines.create!(project_id: project.id, ref: 'master', sha: 'adf43c3a', status: 'success', partition_id: 1)
+ pipelines.create!(project_id: project.id, ref: 'master', sha: 'adf43c3a', status: 'success', partition_id: 100)
end
- let!(:ci_build) { builds.create!(commit_id: pipeline.id, retried: false, type: 'Ci::Build', partition_id: 1) }
+ let!(:ci_build) { builds.create!(commit_id: pipeline.id, retried: false, type: 'Ci::Build', partition_id: 100) }
let!(:security_scan_1) { security_scans.create!(build_id: ci_build.id, scan_type: 1, created_at: 91.days.ago) }
let!(:security_scan_2) { security_scans.create!(build_id: ci_build.id, scan_type: 2) }
diff --git a/spec/models/activity_pub/releases_subscription_spec.rb b/spec/models/activity_pub/releases_subscription_spec.rb
new file mode 100644
index 00000000000..0c873a5c18a
--- /dev/null
+++ b/spec/models/activity_pub/releases_subscription_spec.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ActivityPub::ReleasesSubscription, type: :model, feature_category: :release_orchestration do
+ describe 'factory' do
+ subject { build(:activity_pub_releases_subscription) }
+
+ it { is_expected.to be_valid }
+ end
+
+ describe 'associations' do
+ it { is_expected.to belong_to(:project).optional(false) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:subscriber_url) }
+
+ describe 'subscriber_url' do
+ subject { build(:activity_pub_releases_subscription) }
+
+ it { is_expected.to validate_uniqueness_of(:subscriber_url).case_insensitive.scoped_to([:project_id]) }
+ it { is_expected.to allow_value("http://example.com/actor").for(:subscriber_url) }
+ it { is_expected.not_to allow_values("I'm definitely not a URL").for(:subscriber_url) }
+ end
+
+ describe 'subscriber_inbox_url' do
+ subject { build(:activity_pub_releases_subscription) }
+
+ it { is_expected.to validate_uniqueness_of(:subscriber_inbox_url).case_insensitive.scoped_to([:project_id]) }
+ it { is_expected.to allow_value("http://example.com/actor").for(:subscriber_inbox_url) }
+ it { is_expected.not_to allow_values("I'm definitely not a URL").for(:subscriber_inbox_url) }
+ end
+
+ describe 'shared_inbox_url' do
+ subject { build(:activity_pub_releases_subscription) }
+
+ it { is_expected.to allow_value("http://example.com/actor").for(:shared_inbox_url) }
+ it { is_expected.not_to allow_values("I'm definitely not a URL").for(:shared_inbox_url) }
+ end
+
+ describe 'payload' do
+ it { is_expected.not_to allow_value("string").for(:payload) }
+ it { is_expected.not_to allow_value(1.0).for(:payload) }
+
+ it do
+ is_expected.to allow_value({
+ '@context': 'https://www.w3.org/ns/activitystreams',
+ id: 'https://example.com/actor#follow/1',
+ type: 'Follow',
+ actor: 'https://example.com/actor',
+ object: 'http://localhost/user/project/-/releases'
+ }).for(:payload)
+ end
+ end
+ end
+
+ describe '.find_by_subscriber_url' do
+ let_it_be(:subscription) { create(:activity_pub_releases_subscription) }
+
+ it 'returns a record if arguments match' do
+ result = described_class.find_by_subscriber_url(subscription.subscriber_url)
+
+ expect(result).to eq(subscription)
+ end
+
+ it 'returns a record if arguments match case insensitively' do
+ result = described_class.find_by_subscriber_url(subscription.subscriber_url.upcase)
+
+ expect(result).to eq(subscription)
+ end
+
+ it 'returns nil if project does not match' do
+ result = described_class.find_by_subscriber_url('I really should not exist')
+
+ expect(result).to be(nil)
+ end
+ end
+end
diff --git a/spec/models/ai/service_access_token_spec.rb b/spec/models/ai/service_access_token_spec.rb
index d979db4b3d6..d491735e604 100644
--- a/spec/models/ai/service_access_token_spec.rb
+++ b/spec/models/ai/service_access_token_spec.rb
@@ -2,10 +2,10 @@
require 'spec_helper'
-RSpec.describe Ai::ServiceAccessToken, type: :model, feature_category: :application_performance do
+RSpec.describe Ai::ServiceAccessToken, type: :model, feature_category: :cloud_connector do
describe '.expired', :freeze_time do
- let_it_be(:expired_token) { create(:service_access_token, :code_suggestions, :expired) }
- let_it_be(:active_token) { create(:service_access_token, :code_suggestions, :active) }
+ let_it_be(:expired_token) { create(:service_access_token, :expired) }
+ let_it_be(:active_token) { create(:service_access_token, :active) }
it 'selects all expired tokens' do
expect(described_class.expired).to match_array([expired_token])
@@ -13,24 +13,14 @@ RSpec.describe Ai::ServiceAccessToken, type: :model, feature_category: :applicat
end
describe '.active', :freeze_time do
- let_it_be(:expired_token) { create(:service_access_token, :code_suggestions, :expired) }
- let_it_be(:active_token) { create(:service_access_token, :code_suggestions, :active) }
+ let_it_be(:expired_token) { create(:service_access_token, :expired) }
+ let_it_be(:active_token) { create(:service_access_token, :active) }
it 'selects all active tokens' do
expect(described_class.active).to match_array([active_token])
end
end
- # There is currently only one category, please expand this test when a new category is added.
- describe '.for_category' do
- let(:code_suggestions_token) { create(:service_access_token, :code_suggestions) }
- let(:category) { :code_suggestions }
-
- it 'only selects tokens from the selected category' do
- expect(described_class.for_category(category)).to match_array([code_suggestions_token])
- end
- end
-
describe '#token' do
let(:token_value) { 'Abc' }
@@ -47,7 +37,6 @@ RSpec.describe Ai::ServiceAccessToken, type: :model, feature_category: :applicat
describe 'validations' do
it { is_expected.to validate_presence_of(:token) }
- it { is_expected.to validate_presence_of(:category) }
it { is_expected.to validate_presence_of(:expires_at) }
end
end
diff --git a/spec/models/alert_management/http_integration_spec.rb b/spec/models/alert_management/http_integration_spec.rb
index dc26d0323d7..ef9eaa960f2 100644
--- a/spec/models/alert_management/http_integration_spec.rb
+++ b/spec/models/alert_management/http_integration_spec.rb
@@ -44,8 +44,8 @@ RSpec.describe AlertManagement::HttpIntegration, feature_category: :incident_man
context 'with valid JSON schema' do
let(:attribute_mapping) do
{
- title: { path: %w(a b c), type: 'string', label: 'Title' },
- description: { path: %w(a), type: 'string' }
+ title: { path: %w[a b c], type: 'string', label: 'Title' },
+ description: { path: %w[a], type: 'string' }
}
end
@@ -78,7 +78,7 @@ RSpec.describe AlertManagement::HttpIntegration, feature_category: :incident_man
context 'when property has extra attributes' do
let(:attribute_mapping) do
- { title: { path: %w(a b c), type: 'string', extra: 'property' } }
+ { title: { path: %w[a b c], type: 'string', extra: 'property' } }
end
it_behaves_like 'is invalid record'
diff --git a/spec/models/analytics/cycle_analytics/value_stream_spec.rb b/spec/models/analytics/cycle_analytics/value_stream_spec.rb
index 3b3187e0b51..852ace6a920 100644
--- a/spec/models/analytics/cycle_analytics/value_stream_spec.rb
+++ b/spec/models/analytics/cycle_analytics/value_stream_spec.rb
@@ -99,4 +99,23 @@ RSpec.describe Analytics::CycleAnalytics::ValueStream, type: :model, feature_cat
it { is_expected.to be_custom }
end
end
+
+ describe '#project' do
+ subject(:value_stream) do
+ build(:cycle_analytics_value_stream, name: 'value_stream_1', namespace: namespace).project
+ end
+
+ context 'when namespace is a project' do
+ let_it_be(:project) { create(:project) }
+ let(:namespace) { project.project_namespace }
+
+ it { is_expected.to eq(project) }
+ end
+
+ context 'when namespace is a group' do
+ let_it_be(:namespace) { create(:group) }
+
+ it { is_expected.to be_nil }
+ end
+ end
end
diff --git a/spec/models/appearance_spec.rb b/spec/models/appearance_spec.rb
index b5f47c950b9..ffb46884e5d 100644
--- a/spec/models/appearance_spec.rb
+++ b/spec/models/appearance_spec.rb
@@ -81,7 +81,7 @@ RSpec.describe Appearance do
end
end
- %i(logo header_logo pwa_icon favicon).each do |logo_type|
+ %i[logo header_logo pwa_icon favicon].each do |logo_type|
it_behaves_like 'logo paths', logo_type
end
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index 78bf410075b..a2d6c60fbd0 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -162,6 +162,8 @@ RSpec.describe ApplicationSetting, feature_category: :shared, type: :model do
it { is_expected.to validate_inclusion_of(:user_defaults_to_private_profile).in_array([true, false]) }
+ it { is_expected.to validate_inclusion_of(:allow_project_creation_for_guest_and_below).in_array([true, false]) }
+
it { is_expected.to validate_inclusion_of(:deny_all_requests_except_allowed).in_array([true, false]) }
it 'ensures max_pages_size is an integer greater than 0 (or equal to 0 to indicate unlimited/maximum)' do
@@ -254,11 +256,11 @@ RSpec.describe ApplicationSetting, feature_category: :shared, type: :model do
it { is_expected.not_to allow_value(['']).for(:valid_runner_registrars) }
it { is_expected.not_to allow_value(['OBVIOUSLY_WRONG']).for(:valid_runner_registrars) }
- it { is_expected.not_to allow_value(%w(project project)).for(:valid_runner_registrars) }
+ it { is_expected.not_to allow_value(%w[project project]).for(:valid_runner_registrars) }
it { is_expected.not_to allow_value([nil]).for(:valid_runner_registrars) }
it { is_expected.not_to allow_value(nil).for(:valid_runner_registrars) }
it { is_expected.to allow_value([]).for(:valid_runner_registrars) }
- it { is_expected.to allow_value(%w(project group)).for(:valid_runner_registrars) }
+ it { is_expected.to allow_value(%w[project group]).for(:valid_runner_registrars) }
it { is_expected.to allow_value(http).for(:jira_connect_proxy_url) }
it { is_expected.to allow_value(https).for(:jira_connect_proxy_url) }
@@ -820,15 +822,6 @@ RSpec.describe ApplicationSetting, feature_category: :shared, type: :model do
subject { setting }
end
- # Upgraded databases will have this sort of content
- context 'repository_storages is a String, not an Array' do
- before do
- described_class.where(id: setting.id).update_all(repository_storages: 'default')
- end
-
- it { expect(setting.repository_storages).to eq(['default']) }
- end
-
context 'auto_devops_domain setting' do
context 'when auto_devops_enabled? is true' do
before do
@@ -865,31 +858,6 @@ RSpec.describe ApplicationSetting, feature_category: :shared, type: :model do
end
end
- context 'repository storages' do
- before do
- storages = {
- 'custom1' => 'tmp/tests/custom_repositories_1',
- 'custom2' => 'tmp/tests/custom_repositories_2',
- 'custom3' => 'tmp/tests/custom_repositories_3'
-
- }
- allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
- end
-
- describe 'inclusion' do
- it { is_expected.to allow_value('custom1').for(:repository_storages) }
- it { is_expected.to allow_value(%w(custom2 custom3)).for(:repository_storages) }
- it { is_expected.not_to allow_value('alternative').for(:repository_storages) }
- it { is_expected.not_to allow_value(%w(alternative custom1)).for(:repository_storages) }
- end
-
- describe 'presence' do
- it { is_expected.not_to allow_value([]).for(:repository_storages) }
- it { is_expected.not_to allow_value("").for(:repository_storages) }
- it { is_expected.not_to allow_value(nil).for(:repository_storages) }
- end
- end
-
context 'housekeeping settings' do
it { is_expected.not_to allow_value(0).for(:housekeeping_optimize_repository_period) }
end
diff --git a/spec/models/authentication_event_spec.rb b/spec/models/authentication_event_spec.rb
index 17fe10b5b4e..8ce949c737b 100644
--- a/spec/models/authentication_event_spec.rb
+++ b/spec/models/authentication_event_spec.rb
@@ -37,11 +37,11 @@ RSpec.describe AuthenticationEvent do
describe '.providers' do
before do
- allow(Devise).to receive(:omniauth_providers).and_return(%w(ldapmain google_oauth2))
+ allow(Devise).to receive(:omniauth_providers).and_return(%w[ldapmain google_oauth2])
end
it 'returns an array of distinct providers' do
- expect(described_class.providers).to match_array %w(ldapmain google_oauth2 standard two-factor two-factor-via-u2f-device two-factor-via-webauthn-device)
+ expect(described_class.providers).to match_array %w[ldapmain google_oauth2 standard two-factor two-factor-via-u2f-device two-factor-via-webauthn-device]
end
end
diff --git a/spec/models/blob_viewer/base_spec.rb b/spec/models/blob_viewer/base_spec.rb
index 682b6dc3b1d..91c32d5c7c9 100644
--- a/spec/models/blob_viewer/base_spec.rb
+++ b/spec/models/blob_viewer/base_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe BlobViewer::Base do
Class.new(described_class) do
include BlobViewer::ServerSide
- self.extensions = %w(pdf)
+ self.extensions = %w[pdf]
self.binary = true
self.collapse_limit = 1.megabyte
self.size_limit = 5.megabytes
@@ -41,7 +41,7 @@ RSpec.describe BlobViewer::Base do
context 'when the file type is supported' do
before do
- viewer_class.file_types = %i(license)
+ viewer_class.file_types = %i[license]
viewer_class.binary = false
end
diff --git a/spec/models/bulk_imports/entity_spec.rb b/spec/models/bulk_imports/entity_spec.rb
index 3e98ba0973e..b822786579b 100644
--- a/spec/models/bulk_imports/entity_spec.rb
+++ b/spec/models/bulk_imports/entity_spec.rb
@@ -248,6 +248,24 @@ RSpec.describe BulkImports::Entity, type: :model, feature_category: :importers d
end
end
+ describe '#portable_class' do
+ context 'when entity is group' do
+ it 'returns Group class' do
+ entity = build(:bulk_import_entity, :group_entity)
+
+ expect(entity.portable_class).to eq(Group)
+ end
+ end
+
+ context 'when entity is project' do
+ it 'returns Project class' do
+ entity = build(:bulk_import_entity, :project_entity)
+
+ expect(entity.portable_class).to eq(Project)
+ end
+ end
+ end
+
describe '#export_relations_url_path' do
context 'when entity is group' do
it 'returns group export relations url' do
diff --git a/spec/models/bulk_imports/failure_spec.rb b/spec/models/bulk_imports/failure_spec.rb
index b3fd60ba348..928f14aaced 100644
--- a/spec/models/bulk_imports/failure_spec.rb
+++ b/spec/models/bulk_imports/failure_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BulkImports::Failure, type: :model do
+RSpec.describe BulkImports::Failure, type: :model, feature_category: :importers do
let(:failure) { create(:bulk_import_failure) }
describe 'associations' do
@@ -44,4 +44,18 @@ RSpec.describe BulkImports::Failure, type: :model do
end
end
end
+
+ describe '#exception_message=' do
+ it 'filters file paths' do
+ failure = described_class.new
+ failure.exception_message = 'Failed to read /FILE/PATH'
+ expect(failure.exception_message).to eq('Failed to read [FILTERED]')
+ end
+
+ it 'truncates long string' do
+ failure = described_class.new
+ failure.exception_message = 'A' * 1000
+ expect(failure.exception_message.size).to eq(255)
+ end
+ end
end
diff --git a/spec/models/ci/build_dependencies_spec.rb b/spec/models/ci/build_dependencies_spec.rb
index ab32234eba3..f80af7b9dbc 100644
--- a/spec/models/ci/build_dependencies_spec.rb
+++ b/spec/models/ci/build_dependencies_spec.rb
@@ -107,7 +107,7 @@ RSpec.describe Ci::BuildDependencies, feature_category: :continuous_integration
end
context 'when dependencies are defined' do
- let(:dependencies) { %w(rspec staging) }
+ let(:dependencies) { %w[rspec staging] }
it { is_expected.to contain_exactly(rspec_test, staging) }
end
@@ -137,7 +137,7 @@ RSpec.describe Ci::BuildDependencies, feature_category: :continuous_integration
end
context 'when needs and dependencies are defined' do
- let(:dependencies) { %w(rspec staging) }
+ let(:dependencies) { %w[rspec staging] }
let(:needs) do
[
{ name: 'build', artifacts: true },
@@ -150,7 +150,7 @@ RSpec.describe Ci::BuildDependencies, feature_category: :continuous_integration
end
context 'when needs and dependencies contradict' do
- let(:dependencies) { %w(rspec staging) }
+ let(:dependencies) { %w[rspec staging] }
let(:needs) do
[
{ name: 'build', artifacts: true },
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 2a5d781edc7..2e552c8d524 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -87,7 +87,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
describe 'status' do
context 'when transitioning to any state from running' do
it 'removes runner_session' do
- %w(success drop cancel).each do |event|
+ %w[success drop cancel].each do |event|
build = FactoryBot.create(:ci_build, :running, :with_runner_session, pipeline: pipeline)
build.fire_events!(event)
@@ -1090,7 +1090,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
let(:options_with_fallback_keys) do
{ cache: [
- { key: "key", paths: ["public"], policy: "pull-push", fallback_keys: %w(key1 key2) }
+ { key: "key", paths: ["public"], policy: "pull-push", fallback_keys: %w[key1 key2] }
] }
end
@@ -1111,8 +1111,8 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
let(:options_with_fallback_keys) do
{ cache: [
- { key: "key", paths: ["public"], policy: "pull-push", fallback_keys: %w(key3 key4) },
- { key: "key2", paths: ["public"], policy: "pull-push", fallback_keys: %w(key5 key6) }
+ { key: "key", paths: ["public"], policy: "pull-push", fallback_keys: %w[key3 key4] },
+ { key: "key2", paths: ["public"], policy: "pull-push", fallback_keys: %w[key5 key6] }
] }
end
@@ -1214,11 +1214,11 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
is_expected.to match([
a_hash_including({
key: 'key-1',
- fallback_keys: %w(key3-1 key4-1)
+ fallback_keys: %w[key3-1 key4-1]
}),
a_hash_including({
key: 'key2-1',
- fallback_keys: %w(key5-1 key6-1)
+ fallback_keys: %w[key5-1 key6-1]
})
])
end
@@ -1241,11 +1241,11 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
is_expected.to match([
a_hash_including({
key: 'key-1',
- fallback_keys: %w(key3-1 key4-1)
+ fallback_keys: %w[key3-1 key4-1]
}),
a_hash_including({
key: 'key2-1',
- fallback_keys: %w(key5-1 key6-1)
+ fallback_keys: %w[key5-1 key6-1]
})
])
end
@@ -1320,7 +1320,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
allow(build).to receive(:options).and_return({
cache: [{
key: "key1",
- fallback_keys: %w(key2)
+ fallback_keys: %w[key2]
}]
})
end
@@ -2230,7 +2230,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
end
context 'when artifacts do not expire' do
- it { is_expected.to eq(false) }
+ it { is_expected.to be_falsey }
end
context 'when artifacts expire in the future' do
@@ -2951,7 +2951,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
end
context 'when runner is assigned to build' do
- let(:runner) { create(:ci_runner, description: 'description', tag_list: %w(docker linux)) }
+ let(:runner) { create(:ci_runner, description: 'description', tag_list: %w[docker linux]) }
before do
build.update!(runner: runner)
@@ -3952,8 +3952,8 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
end
context 'when have different tags' do
- let(:build_tag_list) { %w(A B) }
- let(:tag_list) { %w(C D) }
+ let(:build_tag_list) { %w[A B] }
+ let(:tag_list) { %w[C D] }
it "does not match a build" do
is_expected.not_to contain_exactly(build)
@@ -3961,8 +3961,8 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
end
context 'when have a subset of tags' do
- let(:build_tag_list) { %w(A B) }
- let(:tag_list) { %w(A B C D) }
+ let(:build_tag_list) { %w[A B] }
+ let(:tag_list) { %w[A B C D] }
it "does match a build" do
is_expected.to contain_exactly(build)
@@ -3971,7 +3971,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
context 'when build does not have tags' do
let(:build_tag_list) { [] }
- let(:tag_list) { %w(C D) }
+ let(:tag_list) { %w[C D] }
it "does match a build" do
is_expected.to contain_exactly(build)
@@ -3979,8 +3979,8 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
end
context 'when does not have a subset of tags' do
- let(:build_tag_list) { %w(A B C) }
- let(:tag_list) { %w(C D) }
+ let(:build_tag_list) { %w[A B C] }
+ let(:tag_list) { %w[C D] }
it "does not match a build" do
is_expected.not_to contain_exactly(build)
@@ -3998,7 +3998,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
end
context 'when does have tags' do
- let(:tag_list) { %w(A B) }
+ let(:tag_list) { %w[A B] }
it "does match a build" do
is_expected.to contain_exactly(build)
@@ -4676,7 +4676,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
describe '#invalid_dependencies' do
let!(:pre_stage_job_valid) { create(:ci_build, :manual, pipeline: pipeline, name: 'test1', stage_idx: 0) }
let!(:pre_stage_job_invalid) { create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test2', stage_idx: 1) }
- let!(:job) { create(:ci_build, :pending, pipeline: pipeline, stage_idx: 2, options: { dependencies: %w(test1 test2) }) }
+ let!(:job) { create(:ci_build, :pending, pipeline: pipeline, stage_idx: 2, options: { dependencies: %w[test1 test2] }) }
context 'when pipeline is locked' do
before do
@@ -5229,16 +5229,34 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
subject { build.doom! }
let(:traits) { [] }
- let(:build) { create(:ci_build, *traits, pipeline: pipeline) }
+ let(:build) do
+ travel(-1.minute) do
+ create(:ci_build, *traits, pipeline: pipeline)
+ end
+ end
- it 'updates status and failure_reason', :aggregate_failures do
- subject
+ it 'updates status, failure_reason, finished_at and updated_at', :aggregate_failures do
+ old_timestamp = build.updated_at
+ new_timestamp = \
+ freeze_time do
+ Time.current.tap do
+ subject
+ end
+ end
+
+ expect(old_timestamp).not_to eq(new_timestamp)
+ expect(build.updated_at).to eq(new_timestamp)
+ expect(build.finished_at).to eq(new_timestamp)
expect(build.status).to eq("failed")
expect(build.failure_reason).to eq("data_integrity_failure")
end
- it 'logs a message' do
+ it 'logs a message and increments the job failure counter', :aggregate_failures do
+ expect(::Gitlab::Ci::Pipeline::Metrics.job_failure_reason_counter)
+ .to(receive(:increment))
+ .with(reason: :data_integrity_failure)
+
expect(Gitlab::AppLogger)
.to receive(:info)
.with(a_hash_including(message: 'Build doomed', class: build.class.name, build_id: build.id))
@@ -5273,12 +5291,20 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
context 'with running builds' do
let(:traits) { [:picked] }
- it 'drops associated runtime metadata' do
+ it 'drops associated runtime metadata', :aggregate_failures do
subject
expect(build.reload.runtime_metadata).not_to be_present
end
end
+
+ context 'finished builds' do
+ let(:traits) { [:finished] }
+
+ it 'does not update finished_at' do
+ expect { subject }.not_to change { build.reload.finished_at }
+ end
+ end
end
it 'does not generate cross DB queries when a record is created via FactoryBot' do
diff --git a/spec/models/ci/build_trace_chunks/redis_spec.rb b/spec/models/ci/build_trace_chunks/redis_spec.rb
index 0d8cda7b3d8..25a24f4946b 100644
--- a/spec/models/ci/build_trace_chunks/redis_spec.rb
+++ b/spec/models/ci/build_trace_chunks/redis_spec.rb
@@ -4,223 +4,8 @@ require 'spec_helper'
RSpec.describe Ci::BuildTraceChunks::Redis, :clean_gitlab_redis_shared_state do
let(:data_store) { described_class.new }
+ let(:store_trait_with_data) { :redis_with_data }
+ let(:store_trait_without_data) { :redis_without_data }
- describe '#data' do
- subject { data_store.data(model) }
-
- context 'when data exists' do
- let(:model) { create(:ci_build_trace_chunk, :redis_with_data, initial_data: 'sample data in redis') }
-
- it 'returns the data' do
- is_expected.to eq('sample data in redis')
- end
- end
-
- context 'when data does not exist' do
- let(:model) { create(:ci_build_trace_chunk, :redis_without_data) }
-
- it 'returns nil' do
- is_expected.to be_nil
- end
- end
- end
-
- describe '#set_data' do
- subject { data_store.set_data(model, data) }
-
- let(:data) { 'abc123' }
-
- context 'when data exists' do
- let(:model) { create(:ci_build_trace_chunk, :redis_with_data, initial_data: 'sample data in redis') }
-
- it 'overwrites data' do
- expect(data_store.data(model)).to eq('sample data in redis')
-
- subject
-
- expect(data_store.data(model)).to eq('abc123')
- end
- end
-
- context 'when data does not exist' do
- let(:model) { create(:ci_build_trace_chunk, :redis_without_data) }
-
- it 'sets new data' do
- expect(data_store.data(model)).to be_nil
-
- subject
-
- expect(data_store.data(model)).to eq('abc123')
- end
- end
- end
-
- describe '#append_data' do
- context 'when valid offset is used with existing data' do
- let(:model) { create(:ci_build_trace_chunk, :redis_with_data, initial_data: 'abcd') }
-
- it 'appends data' do
- expect(data_store.data(model)).to eq('abcd')
-
- length = data_store.append_data(model, '12345', 4)
-
- expect(length).to eq 9
- expect(data_store.data(model)).to eq('abcd12345')
- end
- end
-
- context 'when data does not exist yet' do
- let(:model) { create(:ci_build_trace_chunk, :redis_without_data) }
-
- it 'sets new data' do
- expect(data_store.data(model)).to be_nil
-
- length = data_store.append_data(model, 'abc', 0)
-
- expect(length).to eq 3
- expect(data_store.data(model)).to eq('abc')
- end
- end
-
- context 'when data needs to be truncated' do
- let(:model) { create(:ci_build_trace_chunk, :redis_with_data, initial_data: '12345678') }
-
- it 'appends data and truncates stored value' do
- expect(data_store.data(model)).to eq('12345678')
-
- length = data_store.append_data(model, 'ab', 4)
-
- expect(length).to eq 6
- expect(data_store.data(model)).to eq('1234ab')
- end
- end
-
- context 'when invalid offset is provided' do
- let(:model) { create(:ci_build_trace_chunk, :redis_with_data, initial_data: 'abc') }
-
- it 'raises an exception' do
- length = data_store.append_data(model, '12345', 4)
-
- expect(length).to be_negative
- end
- end
-
- context 'when trace contains multi-byte UTF8 characters' do
- let(:model) { create(:ci_build_trace_chunk, :redis_with_data, initial_data: 'aüc') }
-
- it 'appends data' do
- length = data_store.append_data(model, '1234', 4)
-
- data_store.data(model).then do |new_data|
- expect(new_data.bytesize).to eq 8
- expect(new_data).to eq 'aüc1234'
- end
-
- expect(length).to eq 8
- end
- end
-
- context 'when trace contains non-UTF8 characters' do
- let(:model) { create(:ci_build_trace_chunk, :redis_with_data, initial_data: "a\255c") }
-
- it 'appends data' do
- length = data_store.append_data(model, '1234', 3)
-
- data_store.data(model).then do |new_data|
- expect(new_data.bytesize).to eq 7
- end
-
- expect(length).to eq 7
- end
- end
- end
-
- describe '#delete_data' do
- subject { data_store.delete_data(model) }
-
- context 'when data exists' do
- let(:model) { create(:ci_build_trace_chunk, :redis_with_data, initial_data: 'sample data in redis') }
-
- it 'deletes data' do
- expect(data_store.data(model)).to eq('sample data in redis')
-
- subject
-
- expect(data_store.data(model)).to be_nil
- end
- end
-
- context 'when data does not exist' do
- let(:model) { create(:ci_build_trace_chunk, :redis_without_data) }
-
- it 'does nothing' do
- expect(data_store.data(model)).to be_nil
-
- subject
-
- expect(data_store.data(model)).to be_nil
- end
- end
- end
-
- describe '#size' do
- context 'when data exists' do
- let(:model) { create(:ci_build_trace_chunk, :redis_with_data, initial_data: 'üabcd') }
-
- it 'returns data bytesize correctly' do
- expect(data_store.size(model)).to eq 6
- end
- end
-
- context 'when data does not exist' do
- let(:model) { create(:ci_build_trace_chunk, :redis_without_data) }
-
- it 'returns zero' do
- expect(data_store.size(model)).to be_zero
- end
- end
- end
-
- describe '#keys' do
- subject { data_store.keys(relation) }
-
- let(:build) { create(:ci_build) }
- let(:relation) { build.trace_chunks }
-
- before do
- create(:ci_build_trace_chunk, :redis_with_data, chunk_index: 0, build: build)
- create(:ci_build_trace_chunk, :redis_with_data, chunk_index: 1, build: build)
- end
-
- it 'returns keys' do
- is_expected.to eq([[build.id, 0], [build.id, 1]])
- end
- end
-
- describe '#delete_keys' do
- subject { data_store.delete_keys(keys) }
-
- let(:build) { create(:ci_build) }
- let(:relation) { build.trace_chunks }
- let(:keys) { data_store.keys(relation) }
-
- before do
- create(:ci_build_trace_chunk, :redis_with_data, chunk_index: 0, build: build)
- create(:ci_build_trace_chunk, :redis_with_data, chunk_index: 1, build: build)
- end
-
- it 'deletes multiple data' do
- Gitlab::Redis::SharedState.with do |redis|
- expect(redis.exists?("gitlab:ci:trace:#{build.id}:chunks:0")).to eq(true)
- expect(redis.exists?("gitlab:ci:trace:#{build.id}:chunks:1")).to eq(true)
- end
-
- subject
-
- Gitlab::Redis::SharedState.with do |redis|
- expect(redis.exists?("gitlab:ci:trace:#{build.id}:chunks:0")).to eq(false)
- expect(redis.exists?("gitlab:ci:trace:#{build.id}:chunks:1")).to eq(false)
- end
- end
- end
+ it_behaves_like 'CI build trace chunk redis', Gitlab::Redis::SharedState
end
diff --git a/spec/models/ci/build_trace_chunks/redis_trace_chunks_spec.rb b/spec/models/ci/build_trace_chunks/redis_trace_chunks_spec.rb
new file mode 100644
index 00000000000..eb2226e39ca
--- /dev/null
+++ b/spec/models/ci/build_trace_chunks/redis_trace_chunks_spec.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::BuildTraceChunks::RedisTraceChunks, :clean_gitlab_redis_trace_chunks,
+ feature_category: :continuous_integration do
+ let(:data_store) { described_class.new }
+ let(:store_trait_with_data) { :redis_trace_chunks_with_data }
+ let(:store_trait_without_data) { :redis_trace_chunks_without_data }
+
+ it_behaves_like 'CI build trace chunk redis', Gitlab::Redis::TraceChunks
+end
diff --git a/spec/models/ci/catalog/components_project_spec.rb b/spec/models/ci/catalog/components_project_spec.rb
index d7e0ee2079c..79e1a113e47 100644
--- a/spec/models/ci/catalog/components_project_spec.rb
+++ b/spec/models/ci/catalog/components_project_spec.rb
@@ -5,32 +5,29 @@ require 'spec_helper'
RSpec.describe Ci::Catalog::ComponentsProject, feature_category: :pipeline_composition do
using RSpec::Parameterized::TableSyntax
- let_it_be(:files) do
- {
- 'templates/secret-detection.yml' => "spec:\n inputs:\n website:\n---\nimage: alpine_1",
- 'templates/dast/template.yml' => 'image: alpine_2',
- 'templates/template.yml' => 'image: alpine_3',
- 'templates/blank-yaml.yml' => '',
- 'templates/dast/sub-folder/template.yml' => 'image: alpine_4',
- 'tests/test.yml' => 'image: alpine_5',
- 'README.md' => 'Read me'
- }
- end
-
- let_it_be(:project) do
- create(
- :project, :custom_repo,
- description: 'Simple, complex, and other components',
- files: files
- )
- end
-
+ let_it_be(:project) { create(:project, :catalog_resource_with_components) }
let_it_be(:catalog_resource) { create(:ci_catalog_resource, project: project) }
let(:components_project) { described_class.new(project, project.default_branch) }
describe '#fetch_component_paths' do
- it 'retrieves all the paths for valid components' do
+ context 'when there are invalid paths' do
+ let(:project) do
+ create(:project, :small_repo, description: 'description',
+ files: { 'templates/secrets.yml' => '',
+ 'tests/test.yml' => 'this is invalid',
+ 'README.md' => 'this is not ok' }
+ )
+ end
+
+ it 'does not retrieve the invalid path(s) and only retrieves the valid one(s)' do
+ paths = components_project.fetch_component_paths(project.default_branch)
+
+ expect(paths).to contain_exactly('templates/secrets.yml')
+ end
+ end
+
+ it 'retrieves all the valid paths for components' do
paths = components_project.fetch_component_paths(project.default_branch)
expect(paths).to contain_exactly(
@@ -38,6 +35,12 @@ RSpec.describe Ci::Catalog::ComponentsProject, feature_category: :pipeline_compo
'templates/template.yml'
)
end
+
+ it 'does not fetch more paths than the limit' do
+ paths = components_project.fetch_component_paths(project.default_branch, limit: 1)
+
+ expect(paths.size).to eq(1)
+ end
end
describe '#extract_component_name' do
@@ -50,7 +53,11 @@ RSpec.describe Ci::Catalog::ComponentsProject, feature_category: :pipeline_compo
context 'with valid component paths' do
where(:path, :name) do
'templates/secret-detection.yml' | 'secret-detection'
+ 'templates/secret_detection.yml' | 'secret_detection'
+ 'templates/secret_detection123.yml' | 'secret_detection123'
+ 'templates/secret-detection-123.yml' | 'secret-detection-123'
'templates/dast/template.yml' | 'dast'
+ 'templates/d-a-s_t/template.yml' | 'd-a-s_t'
'templates/template.yml' | 'template'
'templates/blank-yaml.yml' | 'blank-yaml'
end
diff --git a/spec/models/ci/catalog/listing_spec.rb b/spec/models/ci/catalog/listing_spec.rb
index 7524d908252..7a1e12165ac 100644
--- a/spec/models/ci/catalog/listing_spec.rb
+++ b/spec/models/ci/catalog/listing_spec.rb
@@ -4,109 +4,179 @@ require 'spec_helper'
RSpec.describe Ci::Catalog::Listing, feature_category: :pipeline_composition do
let_it_be(:namespace) { create(:group) }
- let_it_be(:project_1) { create(:project, namespace: namespace, name: 'X Project') }
- let_it_be(:project_2) { create(:project, namespace: namespace, name: 'B Project') }
- let_it_be(:project_3) { create(:project, namespace: namespace, name: 'A Project') }
- let_it_be(:project_4) { create(:project) }
+ let_it_be(:project_x) { create(:project, namespace: namespace, name: 'X Project') }
+ let_it_be(:project_a) { create(:project, :public, namespace: namespace, name: 'A Project') }
+ let_it_be(:project_noaccess) { create(:project, namespace: namespace, name: 'C Project') }
+ let_it_be(:project_ext) { create(:project, :public, name: 'TestProject') }
let_it_be(:user) { create(:user) }
- let(:list) { described_class.new(namespace, user) }
+ let_it_be(:project_b) do
+ create(:project, namespace: namespace, name: 'B Project', description: 'Rspec test framework')
+ end
- describe '#new' do
- context 'when namespace is not a root namespace' do
- let(:namespace) { create(:group, :nested) }
+ let(:list) { described_class.new(user) }
- it 'raises an exception' do
- expect { list }.to raise_error(ArgumentError, 'Namespace is not a root namespace')
- end
- end
+ before_all do
+ project_x.add_reporter(user)
+ project_b.add_reporter(user)
+ project_a.add_reporter(user)
+ project_ext.add_reporter(user)
end
describe '#resources' do
- subject(:resources) { list.resources }
+ subject(:resources) { list.resources(**params) }
+
+ context 'when user is anonymous' do
+ let(:user) { nil }
+ let(:params) { {} }
+
+ let!(:resource_1) { create(:ci_catalog_resource, project: project_a) }
+ let!(:resource_2) { create(:ci_catalog_resource, project: project_ext) }
+ let!(:resource_3) { create(:ci_catalog_resource, project: project_b) }
+
+ it 'returns only resources for public projects' do
+ is_expected.to contain_exactly(resource_1, resource_2)
+ end
+
+ context 'when sorting is provided' do
+ let(:params) { { sort: :name_desc } }
+
+ it 'returns only resources for public projects sorted by name DESC' do
+ is_expected.to contain_exactly(resource_2, resource_1)
+ end
+ end
+ end
+
+ context 'when search params are provided' do
+ let(:params) { { search: 'test' } }
+
+ let!(:resource_1) { create(:ci_catalog_resource, project: project_a) }
+ let!(:resource_2) { create(:ci_catalog_resource, project: project_ext) }
+ let!(:resource_3) { create(:ci_catalog_resource, project: project_b) }
- context 'when the user has access to all projects in the namespace' do
- before do
- namespace.add_developer(user)
+ it 'returns the resources that match the search params' do
+ is_expected.to contain_exactly(resource_2, resource_3)
end
- context 'when the namespace has no catalog resources' do
+ context 'when search term is too small' do
+ let(:params) { { search: 'te' } }
+
it { is_expected.to be_empty }
end
+ end
- context 'when the namespace has catalog resources' do
- let_it_be(:today) { Time.zone.now }
- let_it_be(:yesterday) { today - 1.day }
- let_it_be(:tomorrow) { today + 1.day }
+ context 'when namespace is provided' do
+ let(:params) { { namespace: namespace } }
- let_it_be(:resource) { create(:ci_catalog_resource, project: project_1, latest_released_at: yesterday) }
- let_it_be(:resource_2) { create(:ci_catalog_resource, project: project_2, latest_released_at: today) }
- let_it_be(:resource_3) { create(:ci_catalog_resource, project: project_3, latest_released_at: nil) }
+ context 'when namespace is not a root namespace' do
+ let(:namespace) { create(:group, :nested) }
- let_it_be(:other_namespace_resource) do
- create(:ci_catalog_resource, project: project_4, latest_released_at: tomorrow)
+ it 'raises an exception' do
+ expect { resources }.to raise_error(ArgumentError, 'Namespace is not a root namespace')
end
+ end
- it 'contains only catalog resources for projects in that namespace' do
- is_expected.to contain_exactly(resource, resource_2, resource_3)
+ context 'when the user has access to all projects in the namespace' do
+ context 'when the namespace has no catalog resources' do
+ it { is_expected.to be_empty }
end
- context 'with a sort parameter' do
- subject(:resources) { list.resources(sort: sort) }
+ context 'when the namespace has catalog resources' do
+ let_it_be(:today) { Time.zone.now }
+ let_it_be(:yesterday) { today - 1.day }
+ let_it_be(:tomorrow) { today + 1.day }
- context 'when the sort is name ascending' do
- let_it_be(:sort) { :name_asc }
+ let_it_be(:resource_1) do
+ create(:ci_catalog_resource, project: project_x, latest_released_at: yesterday, created_at: today)
+ end
- it 'contains catalog resources for projects sorted by name ascending' do
- is_expected.to eq([resource_3, resource_2, resource])
- end
+ let_it_be(:resource_2) do
+ create(:ci_catalog_resource, project: project_b, latest_released_at: today, created_at: yesterday)
end
- context 'when the sort is name descending' do
- let_it_be(:sort) { :name_desc }
+ let_it_be(:resource_3) do
+ create(:ci_catalog_resource, project: project_a, latest_released_at: nil, created_at: tomorrow)
+ end
- it 'contains catalog resources for projects sorted by name descending' do
- is_expected.to eq([resource, resource_2, resource_3])
- end
+ let_it_be(:other_namespace_resource) do
+ create(:ci_catalog_resource, project: project_ext, latest_released_at: tomorrow)
end
- context 'when the sort is latest_released_at ascending' do
- let_it_be(:sort) { :latest_released_at_asc }
+ it 'contains only catalog resources for projects in that namespace' do
+ is_expected.to contain_exactly(resource_1, resource_2, resource_3)
+ end
- it 'contains catalog resources sorted by latest_released_at ascending with nulls last' do
- is_expected.to eq([resource, resource_2, resource_3])
+ context 'with a sort parameter' do
+ let(:params) { { namespace: namespace, sort: sort } }
+
+ context 'when the sort is created_at ascending' do
+ let_it_be(:sort) { :created_at_asc }
+
+ it 'contains catalog resources sorted by created_at ascending' do
+ is_expected.to eq([resource_2, resource_1, resource_3])
+ end
+ end
+
+ context 'when the sort is created_at descending' do
+ let_it_be(:sort) { :created_at_desc }
+
+ it 'contains catalog resources sorted by created_at descending' do
+ is_expected.to eq([resource_3, resource_1, resource_2])
+ end
end
- end
- context 'when the sort is latest_released_at descending' do
- let_it_be(:sort) { :latest_released_at_desc }
+ context 'when the sort is name ascending' do
+ let_it_be(:sort) { :name_asc }
- it 'contains catalog resources sorted by latest_released_at descending with nulls last' do
- is_expected.to eq([resource_2, resource, resource_3])
+ it 'contains catalog resources for projects sorted by name ascending' do
+ is_expected.to eq([resource_3, resource_2, resource_1])
+ end
+ end
+
+ context 'when the sort is name descending' do
+ let_it_be(:sort) { :name_desc }
+
+ it 'contains catalog resources for projects sorted by name descending' do
+ is_expected.to eq([resource_1, resource_2, resource_3])
+ end
+ end
+
+ context 'when the sort is latest_released_at ascending' do
+ let_it_be(:sort) { :latest_released_at_asc }
+
+ it 'contains catalog resources sorted by latest_released_at ascending with nulls last' do
+ is_expected.to eq([resource_1, resource_2, resource_3])
+ end
+ end
+
+ context 'when the sort is latest_released_at descending' do
+ let_it_be(:sort) { :latest_released_at_desc }
+
+ it 'contains catalog resources sorted by latest_released_at descending with nulls last' do
+ is_expected.to eq([resource_2, resource_1, resource_3])
+ end
end
end
end
end
- end
- context 'when the user only has access to some projects in the namespace' do
- let!(:resource_1) { create(:ci_catalog_resource, project: project_1) }
- let!(:resource_2) { create(:ci_catalog_resource, project: project_2) }
+ context 'when the user only has access to some projects in the namespace' do
+ let!(:accessible_resource) { create(:ci_catalog_resource, project: project_x) }
+ let!(:inaccessible_resource) { create(:ci_catalog_resource, project: project_noaccess) }
- before do
- project_1.add_developer(user)
- project_2.add_guest(user)
+ it 'only returns catalog resources for projects the user has access to' do
+ is_expected.to contain_exactly(accessible_resource)
+ end
end
- it 'only returns catalog resources for projects the user has access to' do
- is_expected.to contain_exactly(resource_1)
- end
- end
+ context 'when the user does not have access to the namespace' do
+ let!(:project) { create(:project) }
+ let!(:resource) { create(:ci_catalog_resource, project: project) }
- context 'when the user does not have access to the namespace' do
- let!(:resource) { create(:ci_catalog_resource, project: project_1) }
+ let(:namespace) { project.namespace }
- it { is_expected.to be_empty }
+ it { is_expected.to be_empty }
+ end
end
end
end
diff --git a/spec/models/ci/catalog/resource_spec.rb b/spec/models/ci/catalog/resource_spec.rb
index 4ce1433e015..098772b1ea9 100644
--- a/spec/models/ci/catalog/resource_spec.rb
+++ b/spec/models/ci/catalog/resource_spec.rb
@@ -7,10 +7,10 @@ RSpec.describe Ci::Catalog::Resource, feature_category: :pipeline_composition do
let_it_be(:yesterday) { today - 1.day }
let_it_be(:tomorrow) { today + 1.day }
- let_it_be(:project) { create(:project, name: 'A') }
+ let_it_be_with_reload(:project) { create(:project, name: 'A') }
let_it_be(:project_2) { build(:project, name: 'Z') }
- let_it_be(:project_3) { build(:project, name: 'L') }
- let_it_be(:resource) { create(:ci_catalog_resource, project: project, latest_released_at: tomorrow) }
+ let_it_be(:project_3) { build(:project, name: 'L', description: 'Z') }
+ let_it_be_with_reload(:resource) { create(:ci_catalog_resource, project: project, latest_released_at: tomorrow) }
let_it_be(:resource_2) { create(:ci_catalog_resource, project: project_2, latest_released_at: today) }
let_it_be(:resource_3) { create(:ci_catalog_resource, project: project_3, latest_released_at: nil) }
@@ -19,12 +19,16 @@ RSpec.describe Ci::Catalog::Resource, feature_category: :pipeline_composition do
let_it_be(:release3) { create(:release, project: project, released_at: tomorrow) }
it { is_expected.to belong_to(:project) }
- it { is_expected.to have_many(:components).class_name('Ci::Catalog::Resources::Component') }
+
+ it do
+ is_expected.to(
+ have_many(:components).class_name('Ci::Catalog::Resources::Component').with_foreign_key(:catalog_resource_id)
+ )
+ end
+
it { is_expected.to have_many(:versions).class_name('Ci::Catalog::Resources::Version') }
it { is_expected.to delegate_method(:avatar_path).to(:project) }
- it { is_expected.to delegate_method(:description).to(:project) }
- it { is_expected.to delegate_method(:name).to(:project) }
it { is_expected.to delegate_method(:star_count).to(:project) }
it { is_expected.to delegate_method(:forks_count).to(:project) }
@@ -38,6 +42,14 @@ RSpec.describe Ci::Catalog::Resource, feature_category: :pipeline_composition do
end
end
+ describe '.search' do
+ it 'returns catalog resources whose name or description match the search term' do
+ resources = described_class.search('Z')
+
+ expect(resources).to contain_exactly(resource_2, resource_3)
+ end
+ end
+
describe '.order_by_created_at_desc' do
it 'returns catalog resources sorted by descending created at' do
ordered_resources = described_class.order_by_created_at_desc
@@ -46,20 +58,40 @@ RSpec.describe Ci::Catalog::Resource, feature_category: :pipeline_composition do
end
end
+ describe '.order_by_created_at_asc' do
+ it 'returns catalog resources sorted by ascending created at' do
+ ordered_resources = described_class.order_by_created_at_asc
+
+ expect(ordered_resources.to_a).to eq([resource, resource_2, resource_3])
+ end
+ end
+
describe '.order_by_name_desc' do
- it 'returns catalog resources sorted by descending name' do
- ordered_resources = described_class.order_by_name_desc
+ subject(:ordered_resources) { described_class.order_by_name_desc }
+ it 'returns catalog resources sorted by descending name' do
expect(ordered_resources.pluck(:name)).to eq(%w[Z L A])
end
+
+ it 'returns catalog resources sorted by descending name with nulls last' do
+ resource.update!(name: nil)
+
+ expect(ordered_resources.pluck(:name)).to eq(['Z', 'L', nil])
+ end
end
describe '.order_by_name_asc' do
- it 'returns catalog resources sorted by ascending name' do
- ordered_resources = described_class.order_by_name_asc
+ subject(:ordered_resources) { described_class.order_by_name_asc }
+ it 'returns catalog resources sorted by ascending name' do
expect(ordered_resources.pluck(:name)).to eq(%w[A L Z])
end
+
+ it 'returns catalog resources sorted by ascending name with nulls last' do
+ resource.update!(name: nil)
+
+ expect(ordered_resources.pluck(:name)).to eq(['L', 'Z', nil])
+ end
end
describe '.order_by_latest_released_at_desc' do
@@ -78,21 +110,92 @@ RSpec.describe Ci::Catalog::Resource, feature_category: :pipeline_composition do
end
end
- describe '#versions' do
- it 'returns releases ordered by released date descending' do
- expect(resource.versions).to eq([release3, release2, release1])
+ describe '#state' do
+ it 'defaults to draft' do
+ expect(resource.state).to eq('draft')
end
end
- describe '#latest_version' do
- it 'returns the latest release' do
- expect(resource.latest_version).to eq(release3)
+ describe '#publish!' do
+ context 'when the catalog resource is in draft state' do
+ it 'updates the state of the catalog resource to published' do
+ expect(resource.state).to eq('draft')
+
+ resource.publish!
+
+ expect(resource.reload.state).to eq('published')
+ end
+ end
+
+ context 'when a catalog resource already has a published state' do
+ it 'leaves the state as published' do
+ resource.update!(state: 'published')
+
+ resource.publish!
+
+ expect(resource.state).to eq('published')
+ end
end
end
- describe '#state' do
- it 'defaults to draft' do
- expect(resource.state).to eq('draft')
+ describe '#unpublish!' do
+ context 'when the catalog resource is in published state' do
+ it 'updates the state to draft' do
+ resource.update!(state: :published)
+ expect(resource.state).to eq('published')
+
+ resource.unpublish!
+
+ expect(resource.reload.state).to eq('draft')
+ end
+ end
+
+ context 'when the catalog resource is already in draft state' do
+ it 'leaves the state as draft' do
+ expect(resource.state).to eq('draft')
+
+ resource.unpublish!
+
+ expect(resource.reload.state).to eq('draft')
+ end
+ end
+ end
+
+ describe 'sync with project' do
+ shared_examples 'denormalized columns of the catalog resource match the project' do
+ it do
+ expect(resource.name).to eq(project.name)
+ expect(resource.description).to eq(project.description)
+ expect(resource.visibility_level).to eq(project.visibility_level)
+ end
+ end
+
+ context 'when the catalog resource is created' do
+ it_behaves_like 'denormalized columns of the catalog resource match the project'
+ end
+
+ context 'when the project name is updated' do
+ before do
+ project.update!(name: 'My new project name')
+ end
+
+ it_behaves_like 'denormalized columns of the catalog resource match the project'
+ end
+
+ context 'when the project description is updated' do
+ before do
+ project.update!(description: 'My new description')
+ end
+
+ it_behaves_like 'denormalized columns of the catalog resource match the project'
+ end
+
+ context 'when the project visibility_level is updated' do
+ before do
+ project.update!(visibility_level: 10)
+ end
+
+ it_behaves_like 'denormalized columns of the catalog resource match the project'
end
end
end
diff --git a/spec/models/ci/catalog/resources/component_spec.rb b/spec/models/ci/catalog/resources/component_spec.rb
index e8c92ce0788..2ee91175920 100644
--- a/spec/models/ci/catalog/resources/component_spec.rb
+++ b/spec/models/ci/catalog/resources/component_spec.rb
@@ -9,6 +9,23 @@ RSpec.describe Ci::Catalog::Resources::Component, type: :model, feature_category
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:version).class_name('Ci::Catalog::Resources::Version') }
+ it_behaves_like 'a BulkInsertSafe model', described_class do
+ let_it_be(:project) { create(:project, :readme, description: 'project description') }
+ let_it_be(:catalog_resource) { create(:ci_catalog_resource, project: project) }
+ let_it_be(:version) { create(:ci_catalog_resource_version, project: project) }
+
+ let(:valid_items_for_bulk_insertion) do
+ build_list(:ci_catalog_resource_component, 10) do |component|
+ component.catalog_resource = catalog_resource
+ component.version = version
+ component.project = project
+ component.created_at = Time.zone.now
+ end
+ end
+
+ let(:invalid_items_for_bulk_insertion) { [] }
+ end
+
describe 'validations' do
it { is_expected.to validate_presence_of(:catalog_resource) }
it { is_expected.to validate_presence_of(:project) }
diff --git a/spec/models/ci/catalog/resources/version_spec.rb b/spec/models/ci/catalog/resources/version_spec.rb
index e93176e466a..7114d2b6709 100644
--- a/spec/models/ci/catalog/resources/version_spec.rb
+++ b/spec/models/ci/catalog/resources/version_spec.rb
@@ -3,14 +3,105 @@
require 'spec_helper'
RSpec.describe Ci::Catalog::Resources::Version, type: :model, feature_category: :pipeline_composition do
+ include_context 'when there are catalog resources with versions'
+
it { is_expected.to belong_to(:release) }
it { is_expected.to belong_to(:catalog_resource).class_name('Ci::Catalog::Resource') }
it { is_expected.to belong_to(:project) }
it { is_expected.to have_many(:components).class_name('Ci::Catalog::Resources::Component') }
+ it { is_expected.to delegate_method(:name).to(:release) }
+ it { is_expected.to delegate_method(:description).to(:release) }
+ it { is_expected.to delegate_method(:tag).to(:release) }
+ it { is_expected.to delegate_method(:sha).to(:release) }
+ it { is_expected.to delegate_method(:released_at).to(:release) }
+ it { is_expected.to delegate_method(:author_id).to(:release) }
+
describe 'validations' do
it { is_expected.to validate_presence_of(:release) }
it { is_expected.to validate_presence_of(:catalog_resource) }
it { is_expected.to validate_presence_of(:project) }
end
+
+ describe '.for_catalog resources' do
+ it 'returns versions for the given catalog resources' do
+ versions = described_class.for_catalog_resources([resource1, resource2])
+
+ expect(versions).to match_array([v1_0, v1_1, v2_0, v2_1])
+ end
+ end
+
+ describe '.order_by_created_at_asc' do
+ it 'returns versions ordered by created_at ascending' do
+ versions = described_class.order_by_created_at_asc
+
+ expect(versions).to eq([v2_1, v2_0, v1_1, v1_0])
+ end
+ end
+
+ describe '.order_by_created_at_desc' do
+ it 'returns versions ordered by created_at descending' do
+ versions = described_class.order_by_created_at_desc
+
+ expect(versions).to eq([v1_0, v1_1, v2_0, v2_1])
+ end
+ end
+
+ describe '.order_by_released_at_asc' do
+ it 'returns versions ordered by released_at ascending' do
+ versions = described_class.order_by_released_at_asc
+
+ expect(versions).to eq([v1_0, v1_1, v2_0, v2_1])
+ end
+ end
+
+ describe '.order_by_released_at_desc' do
+ it 'returns versions ordered by released_at descending' do
+ versions = described_class.order_by_released_at_desc
+
+ expect(versions).to eq([v2_1, v2_0, v1_1, v1_0])
+ end
+ end
+
+ describe '.latest' do
+ subject { described_class.latest }
+
+ it 'returns the latest version by released date' do
+ is_expected.to eq(v2_1)
+ end
+
+ context 'when there are no versions' do
+ it 'returns nil' do
+ resource1.versions.delete_all(:delete_all)
+ resource2.versions.delete_all(:delete_all)
+
+ is_expected.to be_nil
+ end
+ end
+ end
+
+ describe '.latest_for_catalog resources' do
+ subject { described_class.latest_for_catalog_resources([resource1, resource2]) }
+
+ it 'returns the latest version for each catalog resource' do
+ is_expected.to match_array([v1_1, v2_1])
+ end
+
+ context 'when one catalog resource does not have versions' do
+ it 'returns the latest version of only the catalog resource with versions' do
+ resource1.versions.delete_all(:delete_all)
+
+ is_expected.to match_array([v2_1])
+ end
+ end
+
+ context 'when no catalog resource has versions' do
+ it 'returns empty response' do
+ resource1.versions.delete_all(:delete_all)
+ resource2.versions.delete_all(:delete_all)
+
+ is_expected.to be_empty
+ end
+ end
+ end
end
diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb
index 498af80dbb6..48d46824c11 100644
--- a/spec/models/ci/job_artifact_spec.rb
+++ b/spec/models/ci/job_artifact_spec.rb
@@ -207,7 +207,7 @@ RSpec.describe Ci::JobArtifact, feature_category: :build_artifacts do
subject { described_class.associated_file_types_for(file_type) }
where(:file_type, :result) do
- 'codequality' | %w(codequality)
+ 'codequality' | %w[codequality]
'quality' | nil
end
diff --git a/spec/models/ci/job_token/scope_spec.rb b/spec/models/ci/job_token/scope_spec.rb
index 7aa861a3dab..d41286f5a45 100644
--- a/spec/models/ci/job_token/scope_spec.rb
+++ b/spec/models/ci/job_token/scope_spec.rb
@@ -88,24 +88,6 @@ RSpec.describe Ci::JobToken::Scope, feature_category: :continuous_integration, f
end
end
- RSpec.shared_examples 'enforces outbound scope only' do
- include_context 'with accessible and inaccessible projects'
-
- where(:accessed_project, :result) do
- ref(:current_project) | true
- ref(:inbound_allowlist_project) | false
- ref(:unscoped_project1) | false
- ref(:unscoped_project2) | false
- ref(:outbound_allowlist_project) | true
- ref(:inbound_accessible_project) | false
- ref(:fully_accessible_project) | true
- end
-
- with_them do
- it { is_expected.to eq(result) }
- end
- end
-
describe 'accessible?' do
subject { scope.accessible?(accessed_project) }
@@ -121,6 +103,7 @@ RSpec.describe Ci::JobToken::Scope, feature_category: :continuous_integration, f
ref(:outbound_allowlist_project) | false
ref(:inbound_accessible_project) | false
ref(:fully_accessible_project) | true
+ ref(:unscoped_public_project) | false
end
with_them do
@@ -147,6 +130,7 @@ RSpec.describe Ci::JobToken::Scope, feature_category: :continuous_integration, f
ref(:outbound_allowlist_project) | false
ref(:inbound_accessible_project) | true
ref(:fully_accessible_project) | true
+ ref(:unscoped_public_project) | false
end
with_them do
@@ -160,7 +144,34 @@ RSpec.describe Ci::JobToken::Scope, feature_category: :continuous_integration, f
current_project.update!(ci_outbound_job_token_scope_enabled: true)
end
- include_examples 'enforces outbound scope only'
+ include_context 'with accessible and inaccessible projects'
+
+ where(:accessed_project, :result) do
+ ref(:current_project) | true
+ ref(:inbound_allowlist_project) | false
+ ref(:unscoped_project1) | false
+ ref(:unscoped_project2) | false
+ ref(:outbound_allowlist_project) | true
+ ref(:inbound_accessible_project) | false
+ ref(:fully_accessible_project) | true
+ ref(:unscoped_public_project) | true
+ end
+
+ with_them do
+ it { is_expected.to eq(result) }
+ end
+
+ context "with FF restrict_ci_job_token_for_public_and_internal_projects disabled" do
+ before do
+ stub_feature_flags(restrict_ci_job_token_for_public_and_internal_projects: false)
+ end
+
+ let(:accessed_project) { unscoped_public_project }
+
+ it "restricts public and internal outbound projects not in allowlist" do
+ is_expected.to eq(false)
+ end
+ end
end
end
end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 887ec48ec8f..9696ba7b3ee 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -157,6 +157,106 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
end
end
+ describe 'unlocking pipelines based on state transition' do
+ let(:ci_ref) { create(:ci_ref) }
+ let(:unlock_previous_pipelines_worker_spy) { class_spy(::Ci::Refs::UnlockPreviousPipelinesWorker) }
+
+ before do
+ stub_const('Ci::Refs::UnlockPreviousPipelinesWorker', unlock_previous_pipelines_worker_spy)
+ stub_feature_flags(ci_stop_unlock_pipelines: false)
+ end
+
+ shared_examples 'not unlocking pipelines' do |event:|
+ context "on #{event}" do
+ let(:pipeline) { create(:ci_pipeline, ci_ref_id: ci_ref.id, locked: :artifacts_locked) }
+
+ it 'does not unlock previous pipelines' do
+ pipeline.fire_events!(event)
+
+ expect(unlock_previous_pipelines_worker_spy).not_to have_received(:perform_async)
+ end
+ end
+ end
+
+ shared_examples 'unlocking pipelines' do |event:|
+ context "on #{event}" do
+ before do
+ pipeline.fire_events!(event)
+ end
+
+ let(:pipeline) { create(:ci_pipeline, ci_ref_id: ci_ref.id, locked: :artifacts_locked) }
+
+ it 'unlocks previous pipelines' do
+ expect(unlock_previous_pipelines_worker_spy).to have_received(:perform_async).with(ci_ref.id)
+ end
+ end
+ end
+
+ context 'when transitioning to unlockable states' do
+ before do
+ pipeline.run
+ end
+
+ it_behaves_like 'unlocking pipelines', event: :succeed
+ it_behaves_like 'unlocking pipelines', event: :drop
+ it_behaves_like 'unlocking pipelines', event: :skip
+ it_behaves_like 'unlocking pipelines', event: :cancel
+ it_behaves_like 'unlocking pipelines', event: :block
+
+ context 'and ci_stop_unlock_pipelines is enabled' do
+ before do
+ stub_feature_flags(ci_stop_unlock_pipelines: true)
+ end
+
+ it_behaves_like 'not unlocking pipelines', event: :succeed
+ it_behaves_like 'not unlocking pipelines', event: :drop
+ it_behaves_like 'not unlocking pipelines', event: :skip
+ it_behaves_like 'not unlocking pipelines', event: :cancel
+ it_behaves_like 'not unlocking pipelines', event: :block
+ end
+
+ context 'and ci_unlock_non_successful_pipelines is disabled' do
+ before do
+ stub_feature_flags(ci_unlock_non_successful_pipelines: false)
+ end
+
+ it_behaves_like 'unlocking pipelines', event: :succeed
+ it_behaves_like 'not unlocking pipelines', event: :drop
+ it_behaves_like 'not unlocking pipelines', event: :skip
+ it_behaves_like 'not unlocking pipelines', event: :cancel
+ it_behaves_like 'not unlocking pipelines', event: :block
+
+ context 'and ci_stop_unlock_pipelines is enabled' do
+ before do
+ stub_feature_flags(ci_stop_unlock_pipelines: true)
+ end
+
+ it_behaves_like 'not unlocking pipelines', event: :succeed
+ it_behaves_like 'not unlocking pipelines', event: :drop
+ it_behaves_like 'not unlocking pipelines', event: :skip
+ it_behaves_like 'not unlocking pipelines', event: :cancel
+ it_behaves_like 'not unlocking pipelines', event: :block
+ end
+ end
+ end
+
+ context 'when transitioning to a non-unlockable state' do
+ before do
+ pipeline.enqueue
+ end
+
+ it_behaves_like 'not unlocking pipelines', event: :run
+
+ context 'and ci_unlock_non_successful_pipelines is disabled' do
+ before do
+ stub_feature_flags(ci_unlock_non_successful_pipelines: false)
+ end
+
+ it_behaves_like 'not unlocking pipelines', event: :run
+ end
+ end
+ end
+
describe 'pipeline age metric' do
let_it_be(:pipeline) { create(:ci_empty_pipeline, :created) }
@@ -220,6 +320,34 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
end
end
+ describe '.with_unlockable_status' do
+ let_it_be(:project) { create(:project) }
+
+ let!(:pipeline) { create(:ci_pipeline, project: project, status: status) }
+
+ subject(:result) { described_class.with_unlockable_status }
+
+ described_class::UNLOCKABLE_STATUSES.map(&:to_s).each do |s|
+ context "when pipeline status is #{s}" do
+ let(:status) { s }
+
+ it 'includes the pipeline in the result' do
+ expect(result).to include(pipeline)
+ end
+ end
+ end
+
+ (Ci::HasStatus::AVAILABLE_STATUSES - described_class::UNLOCKABLE_STATUSES.map(&:to_s)).each do |s|
+ context "when pipeline status is #{s}" do
+ let(:status) { s }
+
+ it 'does excludes the pipeline in the result' do
+ expect(result).not_to include(pipeline)
+ end
+ end
+ end
+ end
+
describe '.processables' do
let_it_be(:pipeline) { create(:ci_empty_pipeline, :created) }
@@ -231,7 +359,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
end
it 'has an association with processable CI/CD entities' do
- pipeline.processables.pluck('name').yield_self do |processables|
+ pipeline.processables.pluck('name').then do |processables|
expect(processables).to match_array %w[build bridge]
end
end
@@ -1303,7 +1431,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
describe '#stages_names' do
it 'returns a valid names of stages' do
- expect(pipeline.stages_names).to eq(%w(build test deploy))
+ expect(pipeline.stages_names).to eq(%w[build test deploy])
end
end
end
@@ -1900,11 +2028,23 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
end
end
- context 'when only_allow_merge_if_pipeline_succeeds? returns false' do
+ context 'when only_allow_merge_if_pipeline_succeeds? returns false and widget_pipeline_pass_subscription_update disabled' do
let(:only_allow_merge_if_pipeline_succeeds?) { false }
+ before do
+ stub_feature_flags(widget_pipeline_pass_subscription_update: false)
+ end
+
it_behaves_like 'state transition not triggering GraphQL subscription mergeRequestMergeStatusUpdated'
end
+
+ context 'when only_allow_merge_if_pipeline_succeeds? returns false and widget_pipeline_pass_subscription_update enabled' do
+ let(:only_allow_merge_if_pipeline_succeeds?) { false }
+
+ it_behaves_like 'triggers GraphQL subscription mergeRequestMergeStatusUpdated' do
+ let(:action) { pipeline.succeed }
+ end
+ end
end
context 'when pipeline has merge requests' do
@@ -2640,7 +2780,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
subject(:latest_successful_for_refs) { described_class.latest_successful_for_refs(refs) }
context 'when refs are specified' do
- let(:refs) { %w(first_ref second_ref third_ref) }
+ let(:refs) { %w[first_ref second_ref third_ref] }
before do
create(:ci_empty_pipeline, id: 1001, status: :success, ref: 'first_ref', sha: 'sha')
@@ -2819,7 +2959,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
subject { described_class.bridgeable_statuses }
it { is_expected.to be_an(Array) }
- it { is_expected.not_to include('created', 'waiting_for_resource', 'preparing', 'pending') }
+ it { is_expected.to contain_exactly('running', 'success', 'failed', 'canceled', 'skipped', 'manual', 'scheduled') }
end
describe '#status', :sidekiq_inline do
@@ -3176,6 +3316,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
%i[
enqueue
request_resource
+ wait_for_callback
prepare
run
skip
@@ -5578,25 +5719,4 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
end
end
end
-
- describe '#reduced_build_attributes_list_for_rules?' do
- subject { pipeline.reduced_build_attributes_list_for_rules? }
-
- let(:pipeline) { build_stubbed(:ci_pipeline, project: project, user: user) }
-
- it { is_expected.to be_truthy }
-
- it 'memoizes the result' do
- expect { subject }
- .to change { pipeline.strong_memoized?(:reduced_build_attributes_list_for_rules?) }
- end
-
- context 'with the FF disabled' do
- before do
- stub_feature_flags(reduced_build_attributes_list_for_rules: false)
- end
-
- it { is_expected.to be_falsey }
- end
- end
end
diff --git a/spec/models/ci/ref_spec.rb b/spec/models/ci/ref_spec.rb
index 75071a17fa9..2727c7701b8 100644
--- a/spec/models/ci/ref_spec.rb
+++ b/spec/models/ci/ref_spec.rb
@@ -7,61 +7,6 @@ RSpec.describe Ci::Ref, feature_category: :continuous_integration do
it { is_expected.to belong_to(:project) }
- describe 'state machine transitions' do
- context 'unlock artifacts transition' do
- let(:ci_ref) { create(:ci_ref) }
- let(:unlock_previous_pipelines_worker_spy) { class_spy(::Ci::Refs::UnlockPreviousPipelinesWorker) }
-
- before do
- stub_const('Ci::Refs::UnlockPreviousPipelinesWorker', unlock_previous_pipelines_worker_spy)
- end
-
- context 'pipeline is locked' do
- let!(:pipeline) { create(:ci_pipeline, ci_ref_id: ci_ref.id, locked: :artifacts_locked) }
-
- where(:initial_state, :action, :count) do
- :unknown | :succeed! | 1
- :unknown | :do_fail! | 0
- :success | :succeed! | 1
- :success | :do_fail! | 0
- :failed | :succeed! | 1
- :failed | :do_fail! | 0
- :fixed | :succeed! | 1
- :fixed | :do_fail! | 0
- :broken | :succeed! | 1
- :broken | :do_fail! | 0
- :still_failing | :succeed | 1
- :still_failing | :do_fail | 0
- end
-
- with_them do
- context "when transitioning states" do
- before do
- status_value = Ci::Ref.state_machines[:status].states[initial_state].value
- ci_ref.update!(status: status_value)
- end
-
- it 'calls pipeline complete unlock artifacts service' do
- ci_ref.send(action)
-
- expect(unlock_previous_pipelines_worker_spy).to have_received(:perform_async).exactly(count).times
- end
- end
- end
- end
-
- context 'pipeline is unlocked' do
- let!(:pipeline) { create(:ci_pipeline, ci_ref_id: ci_ref.id, locked: :unlocked) }
-
- it 'does not unlock pipelines' do
- ci_ref.succeed!
-
- expect(unlock_previous_pipelines_worker_spy).not_to have_received(:perform_async)
- end
- end
- end
- end
-
describe '.ensure_for' do
let_it_be(:project) { create(:project, :repository) }
@@ -241,4 +186,117 @@ RSpec.describe Ci::Ref, feature_category: :continuous_integration do
let!(:model) { create(:ci_ref, project: parent) }
end
end
+
+ describe '#last_successful_ci_source_pipeline' do
+ let_it_be(:ci_ref) { create(:ci_ref) }
+
+ let(:ci_source) { Enums::Ci::Pipeline.sources[:push] }
+ let(:dangling_source) { Enums::Ci::Pipeline.sources[:parent_pipeline] }
+
+ subject(:result) { ci_ref.last_successful_ci_source_pipeline }
+
+ context 'when there are no successful CI source pipelines' do
+ let!(:running_ci_source) { create(:ci_pipeline, :running, ci_ref: ci_ref, source: ci_source) }
+ let!(:successful_dangling_source) { create(:ci_pipeline, :success, ci_ref: ci_ref, source: dangling_source) }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'when there are successful CI source pipelines' do
+ context 'and the latest pipeline is a successful CI source pipeline' do
+ let!(:failed_ci_source) { create(:ci_pipeline, :failed, ci_ref: ci_ref, source: ci_source) }
+ let!(:successful_dangling_source) { create(:ci_pipeline, :success, ci_ref: ci_ref, source: dangling_source, child_of: failed_ci_source) }
+ let!(:successful_ci_source) { create(:ci_pipeline, :success, ci_ref: ci_ref, source: ci_source) }
+
+ it 'returns the last successful CI source pipeline' do
+ expect(result).to eq(successful_ci_source)
+ end
+ end
+
+ context 'and there is a newer successful dangling source pipeline than the successful CI source pipelines' do
+ let!(:successful_ci_source_1) { create(:ci_pipeline, :success, ci_ref: ci_ref, source: ci_source) }
+ let!(:successful_ci_source_2) { create(:ci_pipeline, :success, ci_ref: ci_ref, source: ci_source) }
+ let!(:failed_ci_source) { create(:ci_pipeline, :failed, ci_ref: ci_ref, source: ci_source) }
+ let!(:successful_dangling_source) { create(:ci_pipeline, :success, ci_ref: ci_ref, source: dangling_source, child_of: failed_ci_source) }
+
+ it 'returns the last successful CI source pipeline' do
+ expect(result).to eq(successful_ci_source_2)
+ end
+
+ context 'and the newer successful dangling source is a child of a successful CI source pipeline' do
+ let!(:parent_ci_source) { create(:ci_pipeline, :success, ci_ref: ci_ref, source: ci_source) }
+ let!(:successful_child_source) { create(:ci_pipeline, :success, ci_ref: ci_ref, source: dangling_source, child_of: parent_ci_source) }
+
+ it 'returns the parent pipeline instead' do
+ expect(result).to eq(parent_ci_source)
+ end
+ end
+ end
+ end
+ end
+
+ describe '#last_unlockable_ci_source_pipeline' do
+ let(:ci_source) { Enums::Ci::Pipeline.sources[:push] }
+ let(:dangling_source) { Enums::Ci::Pipeline.sources[:parent_pipeline] }
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:ci_ref) { create(:ci_ref, project: project) }
+
+ subject(:result) { ci_ref.last_unlockable_ci_source_pipeline }
+
+ context 'when there are unlockable pipelines in the ref' do
+ context 'and the last CI source pipeline in the ref is unlockable' do
+ let!(:unlockable_ci_source_1) { create(:ci_pipeline, :success, project: project, ci_ref: ci_ref, source: ci_source) }
+ let!(:unlockable_ci_source_2) { create(:ci_pipeline, :blocked, project: project, ci_ref: ci_ref, source: ci_source) }
+
+ it 'returns the CI source pipeline' do
+ expect(result).to eq(unlockable_ci_source_2)
+ end
+
+ context 'and it has unlockable child pipelines' do
+ let!(:child) { create(:ci_pipeline, :success, project: project, ci_ref: ci_ref, source: dangling_source, child_of: unlockable_ci_source_2) }
+ let!(:child_2) { create(:ci_pipeline, :success, project: project, ci_ref: ci_ref, source: dangling_source, child_of: unlockable_ci_source_2) }
+
+ it 'returns the parent CI source pipeline' do
+ expect(result).to eq(unlockable_ci_source_2)
+ end
+ end
+
+ context 'and it has a non-unlockable child pipeline' do
+ let!(:child) { create(:ci_pipeline, :running, project: project, ci_ref: ci_ref, source: dangling_source, child_of: unlockable_ci_source_2) }
+
+ it 'returns the parent CI source pipeline' do
+ expect(result).to eq(unlockable_ci_source_2)
+ end
+ end
+ end
+
+ context 'and the last CI source pipeline in the ref is not unlockable' do
+ let!(:unlockable_ci_source) { create(:ci_pipeline, :skipped, project: project, ci_ref: ci_ref, source: ci_source) }
+ let!(:unlockable_dangling_source) { create(:ci_pipeline, :success, project: project, ci_ref: ci_ref, source: dangling_source, child_of: unlockable_ci_source) }
+ let!(:non_unlockable_ci_source) { create(:ci_pipeline, :running, project: project, ci_ref: ci_ref, source: ci_source) }
+ let!(:non_unlockable_ci_source_2) { create(:ci_pipeline, :running, project: project, ci_ref: ci_ref, source: ci_source) }
+
+ it 'returns the last unlockable CI source pipeline before it' do
+ expect(result).to eq(unlockable_ci_source)
+ end
+
+ context 'and it has unlockable child pipelines' do
+ let!(:child) { create(:ci_pipeline, :success, project: project, ci_ref: ci_ref, source: dangling_source, child_of: non_unlockable_ci_source) }
+ let!(:child_2) { create(:ci_pipeline, :success, project: project, ci_ref: ci_ref, source: dangling_source, child_of: non_unlockable_ci_source) }
+
+ it 'returns the last unlockable CI source pipeline before it' do
+ expect(result).to eq(unlockable_ci_source)
+ end
+ end
+ end
+ end
+
+ context 'when there are no unlockable pipelines in the ref' do
+ let!(:non_unlockable_pipeline) { create(:ci_pipeline, :running, project: project, ci_ref: ci_ref, source: ci_source) }
+ let!(:pipeline_from_another_ref) { create(:ci_pipeline, :success, source: ci_source) }
+
+ it { is_expected.to be_nil }
+ end
+ end
end
diff --git a/spec/models/ci/resource_group_spec.rb b/spec/models/ci/resource_group_spec.rb
index 6d518d5c874..9e98cc884de 100644
--- a/spec/models/ci/resource_group_spec.rb
+++ b/spec/models/ci/resource_group_spec.rb
@@ -118,7 +118,7 @@ RSpec.describe Ci::ResourceGroup do
let!(:resource_group) { create(:ci_resource_group, process_mode: process_mode, project: project) }
- Ci::HasStatus::STATUSES_ENUM.keys.each do |status| # rubocop:diable RSpec/UselessDynamicDefinition
+ Ci::HasStatus::STATUSES_ENUM.each_key do |status| # rubocop:disable RSpec/UselessDynamicDefinition -- `status` used in `let`
let!("build_1_#{status}") { create(:ci_build, pipeline: pipeline_1, status: status, resource_group: resource_group) }
let!("build_2_#{status}") { create(:ci_build, pipeline: pipeline_2, status: status, resource_group: resource_group) }
end
diff --git a/spec/models/ci/runner_manager_spec.rb b/spec/models/ci/runner_manager_spec.rb
index bc1d1a0cc49..01275ffd31c 100644
--- a/spec/models/ci/runner_manager_spec.rb
+++ b/spec/models/ci/runner_manager_spec.rb
@@ -413,4 +413,68 @@ RSpec.describe Ci::RunnerManager, feature_category: :runner_fleet, type: :model
end
end
end
+
+ describe '.with_version_prefix' do
+ subject { described_class.with_version_prefix(version_prefix) }
+
+ let_it_be(:runner_manager1) { create(:ci_runner_machine, version: '15.11.0') }
+ let_it_be(:runner_manager2) { create(:ci_runner_machine, version: '15.9.0') }
+ let_it_be(:runner_manager3) { create(:ci_runner_machine, version: '15.11.5') }
+
+ context 'with a prefix string of "15."' do
+ let(:version_prefix) { "15." }
+
+ it 'returns runner managers' do
+ is_expected.to contain_exactly(runner_manager1, runner_manager2, runner_manager3)
+ end
+ end
+
+ context 'with a prefix string of "15"' do
+ let(:version_prefix) { "15" }
+
+ it 'returns runner managers' do
+ is_expected.to contain_exactly(runner_manager1, runner_manager2, runner_manager3)
+ end
+ end
+
+ context 'with a prefix string of "15.11."' do
+ let(:version_prefix) { "15.11." }
+
+ it 'returns runner managers' do
+ is_expected.to contain_exactly(runner_manager1, runner_manager3)
+ end
+ end
+
+ context 'with a prefix string of "15.11"' do
+ let(:version_prefix) { "15.11" }
+
+ it 'returns runner managers' do
+ is_expected.to contain_exactly(runner_manager1, runner_manager3)
+ end
+ end
+
+ context 'with a prefix string of "15.9"' do
+ let(:version_prefix) { "15.9" }
+
+ it 'returns runner managers' do
+ is_expected.to contain_exactly(runner_manager2)
+ end
+ end
+
+ context 'with a prefix string of "15.11.5"' do
+ let(:version_prefix) { "15.11.5" }
+
+ it 'returns runner managers' do
+ is_expected.to contain_exactly(runner_manager3)
+ end
+ end
+
+ context 'with a malformed prefix of "V2"' do
+ let(:version_prefix) { "V2" }
+
+ it 'returns no runner managers' do
+ is_expected.to be_empty
+ end
+ end
+ end
end
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
index 3a3ef072b28..bb9ac084ed6 100644
--- a/spec/models/ci/runner_spec.rb
+++ b/spec/models/ci/runner_spec.rb
@@ -524,6 +524,39 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
end
end
+ describe '.with_creator_id' do
+ subject { described_class.with_creator_id('1') }
+
+ let_it_be(:runner1) { create(:ci_runner, creator_id: 2) }
+ let_it_be(:runner2) { create(:ci_runner, creator_id: 1) }
+ let_it_be(:runner3) { create(:ci_runner, creator_id: 1) }
+ let_it_be(:runner4) { create(:ci_runner, creator_id: nil) }
+
+ it 'returns runners with creator_id \'1\'' do
+ is_expected.to contain_exactly(runner2, runner3)
+ end
+ end
+
+ describe '.with_version_prefix' do
+ subject { described_class.with_version_prefix('15.11.') }
+
+ let_it_be(:runner1) { create(:ci_runner) }
+ let_it_be(:runner2) { create(:ci_runner) }
+ let_it_be(:runner3) { create(:ci_runner) }
+
+ before_all do
+ create(:ci_runner_machine, runner: runner1, version: '15.11.0')
+ create(:ci_runner_machine, runner: runner2, version: '15.9.0')
+ create(:ci_runner_machine, runner: runner3, version: '15.9.0')
+ # Add another runner_machine to runner3 to ensure edge case is handled (searching multiple machines in a single runner)
+ create(:ci_runner_machine, runner: runner3, version: '15.11.5')
+ end
+
+ it 'returns runners containing runner managers with versions starting with 15.11.' do
+ is_expected.to contain_exactly(runner1, runner3)
+ end
+ end
+
describe '.stale', :freeze_time do
subject { described_class.stale }
@@ -739,7 +772,7 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
end
context 'when runner has tags' do
- let(:tag_list) { %w(bb cc) }
+ let(:tag_list) { %w[bb cc] }
shared_examples 'tagged build picker' do
it 'can handle build with matching tags' do
@@ -1026,7 +1059,7 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
end
def value_in_queues
- Gitlab::Redis::SharedState.with do |redis|
+ Gitlab::Redis::Workhorse.with do |redis|
runner_queue_key = runner.send(:runner_queue_key)
redis.get(runner_queue_key)
end
diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb
index 1be50083cd4..4951f57fe6f 100644
--- a/spec/models/ci/stage_spec.rb
+++ b/spec/models/ci/stage_spec.rb
@@ -166,6 +166,18 @@ RSpec.describe Ci::Stage, :models, feature_category: :continuous_integration do
end
end
+ context 'when build is waiting for callback' do
+ before do
+ create(:ci_build, :waiting_for_callback, stage_id: stage.id)
+ end
+
+ it 'updates status to waiting for callback' do
+ expect { stage.update_legacy_status }
+ .to change { stage.reload.status }
+ .to 'waiting_for_callback'
+ end
+ end
+
context 'when stage is skipped because is empty' do
it 'updates status to skipped' do
expect { stage.update_legacy_status }
diff --git a/spec/models/clusters/agent_spec.rb b/spec/models/clusters/agent_spec.rb
index 6201b7b1861..062d5062658 100644
--- a/spec/models/clusters/agent_spec.rb
+++ b/spec/models/clusters/agent_spec.rb
@@ -21,7 +21,7 @@ RSpec.describe Clusters::Agent, feature_category: :deployment_management do
describe 'scopes' do
describe '.ordered_by_name' do
- let(:names) { %w(agent-d agent-b agent-a agent-c) }
+ let(:names) { %w[agent-d agent-b agent-a agent-c] }
subject { described_class.ordered_by_name }
diff --git a/spec/models/clusters/platforms/kubernetes_spec.rb b/spec/models/clusters/platforms/kubernetes_spec.rb
index c32abaf50f5..79a81977611 100644
--- a/spec/models/clusters/platforms/kubernetes_spec.rb
+++ b/spec/models/clusters/platforms/kubernetes_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe Clusters::Platforms::Kubernetes do
it { is_expected.to be_kind_of(Gitlab::Kubernetes) }
it { is_expected.to respond_to :ca_pem }
- it { is_expected.to validate_exclusion_of(:namespace).in_array(%w(gitlab-managed-apps)) }
+ it { is_expected.to validate_exclusion_of(:namespace).in_array(%w[gitlab-managed-apps]) }
it { is_expected.to validate_presence_of(:api_url) }
it { is_expected.to validate_presence_of(:token) }
diff --git a/spec/models/commit_range_spec.rb b/spec/models/commit_range_spec.rb
index 334833e884b..fe8c28d7251 100644
--- a/spec/models/commit_range_spec.rb
+++ b/spec/models/commit_range_spec.rb
@@ -77,7 +77,7 @@ RSpec.describe CommitRange do
describe '#to_param' do
it 'includes the correct keys' do
- expect(range.to_param.keys).to eq %i(from to)
+ expect(range.to_param.keys).to eq %i[from to]
end
it 'includes the correct values for a three-dot range' do
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index 7ab43611108..e873a59b54a 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -23,13 +23,13 @@ RSpec.describe Commit do
shared_examples '.lazy checks' do
context 'when the commits are found' do
let(:oids) do
- %w(
+ %w[
498214de67004b1da3d820901307bed2a68a8ef6
c642fe9b8b9f28f9225d7ea953fe14e74748d53b
6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
048721d90c449b244b7b4c53a9186b04330174ec
281d3a76f31c812dbf48abce82ccf6860adedd81
- )
+ ]
end
subject { oids.map { |oid| described_class.lazy(container, oid) } }
@@ -707,16 +707,6 @@ eos
it_behaves_like "#uri_type"
end
- describe '#uri_type with Rugged enabled', :enable_rugged do
- it 'calls out to the Rugged implementation' do
- allow_any_instance_of(Rugged::Tree).to receive(:path).with('files/html').and_call_original
-
- commit.uri_type('files/html')
- end
-
- it_behaves_like '#uri_type'
- end
-
describe '.diff_max_files' do
subject(:diff_max_files) { described_class.diff_max_files }
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index e9257b08bca..618dd3a3f77 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -27,7 +27,7 @@ RSpec.describe CommitStatus, feature_category: :continuous_integration do
it { is_expected.to belong_to(:auto_canceled_by) }
it { is_expected.to validate_presence_of(:name) }
- it { is_expected.to validate_inclusion_of(:status).in_array(%w(pending running failed success canceled)) }
+ it { is_expected.to validate_inclusion_of(:status).in_array(%w[pending running failed success canceled]) }
it { is_expected.to validate_length_of(:stage).is_at_most(255) }
it { is_expected.to validate_length_of(:ref).is_at_most(255) }
@@ -44,24 +44,6 @@ RSpec.describe CommitStatus, feature_category: :continuous_integration do
it { is_expected.not_to be_retried }
it { expect(described_class.primary_key).to eq('id') }
- describe '.switch_table_names' do
- before do
- stub_env('USE_CI_BUILDS_ROUTING_TABLE', flag_value)
- end
-
- context 'with the env flag disabled' do
- let(:flag_value) { 'false' }
-
- it { expect(described_class.switch_table_names).to eq(:ci_builds) }
- end
-
- context 'with the env flag enabled' do
- let(:flag_value) { 'true' }
-
- it { expect(described_class.switch_table_names).to eq(:p_ci_builds) }
- end
- end
-
describe '#author' do
subject { commit_status.author }
@@ -174,7 +156,7 @@ RSpec.describe CommitStatus, feature_category: :continuous_integration do
describe '.cancelable' do
subject { described_class.cancelable }
- %i[running pending waiting_for_resource preparing created scheduled].each do |status|
+ %i[running pending waiting_for_resource waiting_for_callback preparing created scheduled].each do |status|
context "when #{status} commit status" do
let!(:commit_status) { create(:commit_status, status, pipeline: pipeline) }
diff --git a/spec/models/compare_spec.rb b/spec/models/compare_spec.rb
index 2206ed7bfe8..78610d002b2 100644
--- a/spec/models/compare_spec.rb
+++ b/spec/models/compare_spec.rb
@@ -129,13 +129,13 @@ RSpec.describe Compare, feature_category: :source_code_management do
it 'returns affected file paths, without duplication' do
expect(subject.modified_paths).to contain_exactly(
- *%w{
+ *%w[
foo/for_move.txt
foo/bar/for_move.txt
foo/for_create.txt
foo/for_delete.txt
foo/for_edit.txt
- })
+ ])
end
end
diff --git a/spec/models/concerns/awardable_spec.rb b/spec/models/concerns/awardable_spec.rb
index fcd0d0c05f4..828b75aceb0 100644
--- a/spec/models/concerns/awardable_spec.rb
+++ b/spec/models/concerns/awardable_spec.rb
@@ -102,7 +102,7 @@ RSpec.describe Awardable do
end
it "includes unused thumbs buttons by default" do
- expect(note_without_downvote.grouped_awards.keys.sort).to eq %w(thumbsdown thumbsup)
+ expect(note_without_downvote.grouped_awards.keys.sort).to eq %w[thumbsdown thumbsup]
end
it "doesn't include unused thumbs buttons when disabled in project" do
@@ -114,7 +114,7 @@ RSpec.describe Awardable do
it "includes unused thumbs buttons when enabled in project" do
note_without_downvote.project.show_default_award_emojis = true
- expect(note_without_downvote.grouped_awards.keys.sort).to eq %w(thumbsdown thumbsup)
+ expect(note_without_downvote.grouped_awards.keys.sort).to eq %w[thumbsdown thumbsup]
end
it "doesn't include unused thumbs buttons in summary" do
@@ -124,11 +124,11 @@ RSpec.describe Awardable do
it "includes used thumbs buttons when disabled in project" do
note_with_downvote.project.show_default_award_emojis = false
- expect(note_with_downvote.grouped_awards.keys).to eq %w(thumbsdown)
+ expect(note_with_downvote.grouped_awards.keys).to eq %w[thumbsdown]
end
it "includes used thumbs buttons in summary" do
- expect(note_with_downvote.grouped_awards(with_thumbs: false).keys).to eq %w(thumbsdown)
+ expect(note_with_downvote.grouped_awards(with_thumbs: false).keys).to eq %w[thumbsdown]
end
end
end
diff --git a/spec/models/concerns/case_sensitivity_spec.rb b/spec/models/concerns/case_sensitivity_spec.rb
index 6e624c687c4..a8e52904873 100644
--- a/spec/models/concerns/case_sensitivity_spec.rb
+++ b/spec/models/concerns/case_sensitivity_spec.rb
@@ -21,11 +21,11 @@ RSpec.describe CaseSensitivity do
end
it 'finds multiple instances by a single attribute regardless of case' do
- expect(model.iwhere(path: %w(MODEL-1 model-2))).to contain_exactly(model_1, model_2)
+ expect(model.iwhere(path: %w[MODEL-1 model-2])).to contain_exactly(model_1, model_2)
end
it 'finds instances by multiple attributes' do
- expect(model.iwhere(path: %w(MODEL-1 model-2), name: 'model 1'))
+ expect(model.iwhere(path: %w[MODEL-1 model-2], name: 'model 1'))
.to contain_exactly(model_1)
end
@@ -34,7 +34,7 @@ RSpec.describe CaseSensitivity do
end
it 'builds a query using LOWER' do
- query = model.iwhere(path: %w(MODEL-1 model-2), name: 'model 1').to_sql
+ query = model.iwhere(path: %w[MODEL-1 model-2], name: 'model 1').to_sql
expected_query = <<~QRY.strip
SELECT \"namespaces\".* FROM \"namespaces\" WHERE (LOWER(\"namespaces\".\"path\") IN (LOWER('MODEL-1'), LOWER('model-2'))) AND (LOWER(\"namespaces\".\"name\") = LOWER('model 1'))
QRY
diff --git a/spec/models/concerns/ci/has_status_spec.rb b/spec/models/concerns/ci/has_status_spec.rb
index 5e0a430aa13..95f17c4f854 100644
--- a/spec/models/concerns/ci/has_status_spec.rb
+++ b/spec/models/concerns/ci/has_status_spec.rb
@@ -55,6 +55,22 @@ RSpec.describe Ci::HasStatus, feature_category: :continuous_integration do
it { is_expected.to eq 'waiting_for_resource' }
end
+ context 'all waiting for callback' do
+ let!(:statuses) do
+ [create(type, status: :waiting_for_callback), create(type, status: :waiting_for_callback)]
+ end
+
+ it { is_expected.to eq 'waiting_for_callback' }
+ end
+
+ context 'at least one waiting for callback' do
+ let!(:statuses) do
+ [create(type, status: :success), create(type, status: :waiting_for_callback)]
+ end
+
+ it { is_expected.to eq 'waiting_for_callback' }
+ end
+
context 'all preparing' do
let!(:statuses) do
[create(type, status: :preparing), create(type, status: :preparing)]
@@ -225,7 +241,7 @@ RSpec.describe Ci::HasStatus, feature_category: :continuous_integration do
end
end
- %i[created waiting_for_resource preparing running pending success
+ %i[created waiting_for_callback waiting_for_resource preparing running pending success
failed canceled skipped].each do |status|
it_behaves_like 'having a job', status
end
@@ -271,7 +287,7 @@ RSpec.describe Ci::HasStatus, feature_category: :continuous_integration do
describe '.alive' do
subject { CommitStatus.alive }
- %i[running pending waiting_for_resource preparing created].each do |status|
+ %i[running pending waiting_for_callback waiting_for_resource preparing created].each do |status|
it_behaves_like 'containing the job', status
end
@@ -283,7 +299,7 @@ RSpec.describe Ci::HasStatus, feature_category: :continuous_integration do
describe '.alive_or_scheduled' do
subject { CommitStatus.alive_or_scheduled }
- %i[running pending waiting_for_resource preparing created scheduled].each do |status|
+ %i[running pending waiting_for_callback waiting_for_resource preparing created scheduled].each do |status|
it_behaves_like 'containing the job', status
end
@@ -319,7 +335,7 @@ RSpec.describe Ci::HasStatus, feature_category: :continuous_integration do
describe '.cancelable' do
subject { CommitStatus.cancelable }
- %i[running pending waiting_for_resource preparing created scheduled].each do |status|
+ %i[running pending waiting_for_callback waiting_for_resource preparing created scheduled].each do |status|
it_behaves_like 'containing the job', status
end
diff --git a/spec/models/concerns/ci/partitionable/switch_spec.rb b/spec/models/concerns/ci/partitionable/switch_spec.rb
index 0041a33e50e..c6e2ed265bd 100644
--- a/spec/models/concerns/ci/partitionable/switch_spec.rb
+++ b/spec/models/concerns/ci/partitionable/switch_spec.rb
@@ -31,8 +31,6 @@ RSpec.describe Ci::Partitionable::Switch, :aggregate_failures do
end
before do
- allow(ActiveSupport::DescendantsTracker).to receive(:store_inherited)
-
create_tables(<<~SQL)
CREATE TABLE _test_ci_jobs_metadata(
id serial NOT NULL PRIMARY KEY,
@@ -78,6 +76,15 @@ RSpec.describe Ci::Partitionable::Switch, :aggregate_failures do
)
end
+ # the models defined here are leaked to other tests through
+ # `ActiveRecord::Base.descendants` and we need to counter the side effects
+ # from this. We redefine the method so that we don't check the FF existence
+ # outside of the examples here.
+ # `ActiveSupport::DescendantsTracker.clear` doesn't work with cached classes.
+ after do
+ model.define_singleton_method(:routing_table_enabled?) { false }
+ end
+
it { expect(model).not_to be_routing_class }
it { expect(partitioned_model).to be_routing_class }
diff --git a/spec/models/concerns/enums/sbom_spec.rb b/spec/models/concerns/enums/sbom_spec.rb
index 41670880630..e2f56cc637d 100644
--- a/spec/models/concerns/enums/sbom_spec.rb
+++ b/spec/models/concerns/enums/sbom_spec.rb
@@ -22,7 +22,8 @@ RSpec.describe Enums::Sbom, feature_category: :dependency_management do
:apk | 9
:rpm | 10
:deb | 11
- :cbl_mariner | 12
+ 'cbl-mariner' | 12
+ :wolfi | 13
'unknown-pkg-manager' | 0
'Python (unknown)' | 0
end
diff --git a/spec/models/concerns/featurable_spec.rb b/spec/models/concerns/featurable_spec.rb
index bf104fe1b30..97ad4fc8bdd 100644
--- a/spec/models/concerns/featurable_spec.rb
+++ b/spec/models/concerns/featurable_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe Featurable do
self.table_name = 'project_features'
- set_available_features %i(feature1 feature2 feature3)
+ set_available_features %i[feature1 feature2 feature3]
def feature1_access_level
Featurable::DISABLED
diff --git a/spec/models/concerns/has_user_type_spec.rb b/spec/models/concerns/has_user_type_spec.rb
index 54614ec2b21..effb588e55f 100644
--- a/spec/models/concerns/has_user_type_spec.rb
+++ b/spec/models/concerns/has_user_type_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe User, feature_category: :system_access do
- User::USER_TYPES.keys.each do |type| # rubocop:disable RSpec/UselessDynamicDefinition
+ User::USER_TYPES.each_key do |type| # rubocop:disable RSpec/UselessDynamicDefinition -- `type` used in `let`
let_it_be(type) { create(:user, username: type, user_type: type) }
end
let(:bots) { User::BOT_USER_TYPES.map { |type| public_send(type) } }
diff --git a/spec/models/concerns/ignorable_columns_spec.rb b/spec/models/concerns/ignorable_columns_spec.rb
index c97dc606159..339f06f9c45 100644
--- a/spec/models/concerns/ignorable_columns_spec.rb
+++ b/spec/models/concerns/ignorable_columns_spec.rb
@@ -14,13 +14,13 @@ RSpec.describe IgnorableColumns do
it 'adds columns to ignored_columns' do
expect do
subject.ignore_columns(:name, :created_at, remove_after: '2019-12-01', remove_with: '12.6')
- end.to change { subject.ignored_columns }.from([]).to(%w(name created_at))
+ end.to change { subject.ignored_columns }.from([]).to(%w[name created_at])
end
it 'adds columns to ignored_columns (array version)' do
expect do
subject.ignore_columns(%i[name created_at], remove_after: '2019-12-01', remove_with: '12.6')
- end.to change { subject.ignored_columns }.from([]).to(%w(name created_at))
+ end.to change { subject.ignored_columns }.from([]).to(%w[name created_at])
end
it 'requires remove_after attribute to be set' do
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index 705f8f46a90..d61a465b39f 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -597,7 +597,7 @@ RSpec.describe Issuable, feature_category: :team_planning do
expect(builder).to receive(:build).with(
user: user,
changes: hash_including(
- 'severity' => %w(unknown low)
+ 'severity' => %w[unknown low]
))
issue.to_hook_data(user, old_associations: { severity: 'unknown' })
@@ -618,7 +618,7 @@ RSpec.describe Issuable, feature_category: :team_planning do
expect(builder).to receive(:build).with(
user: user,
changes: hash_including(
- 'escalation_status' => %i(triggered acknowledged)
+ 'escalation_status' => %i[triggered acknowledged]
))
issue.to_hook_data(user, old_associations: { escalation_status: :triggered })
diff --git a/spec/models/concerns/pg_full_text_searchable_spec.rb b/spec/models/concerns/pg_full_text_searchable_spec.rb
index 059df64f7d0..8e3b65cf125 100644
--- a/spec/models/concerns/pg_full_text_searchable_spec.rb
+++ b/spec/models/concerns/pg_full_text_searchable_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe PgFullTextSearchable, feature_category: :global_search do
before_validation -> { self.work_item_type_id = ::WorkItems::Type.default_issue_type.id }
def persist_pg_full_text_search_vector(search_vector)
- Issues::SearchData.upsert({ project_id: project_id, issue_id: id, search_vector: search_vector }, unique_by: %i(project_id issue_id))
+ Issues::SearchData.upsert({ project_id: project_id, issue_id: id, search_vector: search_vector }, unique_by: %i[project_id issue_id])
end
def self.name
@@ -95,7 +95,7 @@ RSpec.describe PgFullTextSearchable, feature_category: :global_search do
matching_object = model_class.create!(project: project, namespace: project.project_namespace, title: 'english', description: 'some description')
matching_object.update_search_data!
- expect(model_class.pg_full_text_search('english', matched_columns: %w(title))).to contain_exactly(matching_object)
+ expect(model_class.pg_full_text_search('english', matched_columns: %w[title])).to contain_exactly(matching_object)
end
it 'uses prefix matching' do
diff --git a/spec/models/concerns/project_features_compatibility_spec.rb b/spec/models/concerns/project_features_compatibility_spec.rb
index f168bedc8eb..46390fa735b 100644
--- a/spec/models/concerns/project_features_compatibility_spec.rb
+++ b/spec/models/concerns/project_features_compatibility_spec.rb
@@ -4,12 +4,12 @@ require 'spec_helper'
RSpec.describe ProjectFeaturesCompatibility do
let(:project) { create(:project) }
- let(:features_enabled) { %w(issues wiki builds merge_requests snippets security_and_compliance) }
+ let(:features_enabled) { %w[issues wiki builds merge_requests snippets security_and_compliance] }
let(:features) do
- features_enabled + %w(
+ features_enabled + %w[
repository pages operations container_registry package_registry environments feature_flags releases
monitor infrastructure
- )
+ ]
end
# We had issues_enabled, snippets_enabled, builds_enabled, merge_requests_enabled and issues_enabled fields on projects table
diff --git a/spec/models/concerns/reactive_caching_spec.rb b/spec/models/concerns/reactive_caching_spec.rb
index 039b9e574fe..324759fc7ee 100644
--- a/spec/models/concerns/reactive_caching_spec.rb
+++ b/spec/models/concerns/reactive_caching_spec.rb
@@ -176,7 +176,7 @@ RSpec.describe ReactiveCaching, :use_clean_rails_memory_store_caching do
describe '.reactive_cache_worker_finder' do
context 'with default reactive_cache_worker_finder' do
- let(:args) { %w(other args) }
+ let(:args) { %w[other args] }
before do
allow(instance.class).to receive(:find_by).with(id: instance.id)
@@ -192,7 +192,7 @@ RSpec.describe ReactiveCaching, :use_clean_rails_memory_store_caching do
end
context 'with custom reactive_cache_worker_finder' do
- let(:args) { %w(arg1 arg2) }
+ let(:args) { %w[arg1 arg2] }
let(:instance) { custom_finder_cache_test.new(666, &calculation) }
let(:custom_finder_cache_test) do
diff --git a/spec/models/concerns/reset_on_column_errors_spec.rb b/spec/models/concerns/reset_on_column_errors_spec.rb
index 38ba0f447f5..96bee128f7e 100644
--- a/spec/models/concerns/reset_on_column_errors_spec.rb
+++ b/spec/models/concerns/reset_on_column_errors_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ResetOnColumnErrors, :delete, feature_category: :shared do
+RSpec.describe ResetOnColumnErrors, :delete, feature_category: :shared, query_analyzers: false do
let(:test_reviewer_model) do
Class.new(ApplicationRecord) do
self.table_name = '_test_reviewers_table'
diff --git a/spec/models/concerns/sortable_spec.rb b/spec/models/concerns/sortable_spec.rb
index f1ae89f33af..98f4ab4f521 100644
--- a/spec/models/concerns/sortable_spec.rb
+++ b/spec/models/concerns/sortable_spec.rb
@@ -14,14 +14,14 @@ RSpec.describe Sortable do
it 'allows secondary ordering by id ascending' do
orders = arel_orders(sorted_relation.with_order_id_asc)
- expect(orders.map { |arel| arel.expr.name }).to eq(%w(created_at id))
+ expect(orders.map { |arel| arel.expr.name }).to eq(%w[created_at id])
expect(orders).to all(be_kind_of(Arel::Nodes::Ascending))
end
it 'allows secondary ordering by id descending' do
orders = arel_orders(sorted_relation.with_order_id_desc)
- expect(orders.map { |arel| arel.expr.name }).to eq(%w(created_at id))
+ expect(orders.map { |arel| arel.expr.name }).to eq(%w[created_at id])
expect(orders.first).to be_kind_of(Arel::Nodes::Ascending)
expect(orders.last).to be_kind_of(Arel::Nodes::Descending)
end
@@ -123,24 +123,24 @@ RSpec.describe Sortable do
let!(:group4) { create(:group, name: 'bbb', id: 4, created_at: ref_time, updated_at: ref_time - 15.seconds) }
it 'sorts groups by id' do
- expect(ordered_group_names('id_asc')).to eq(%w(aa AAA BB bbb))
- expect(ordered_group_names('id_desc')).to eq(%w(bbb BB AAA aa))
+ expect(ordered_group_names('id_asc')).to eq(%w[aa AAA BB bbb])
+ expect(ordered_group_names('id_desc')).to eq(%w[bbb BB AAA aa])
end
it 'sorts groups by name via case-insensitive comparision' do
- expect(ordered_group_names('name_asc')).to eq(%w(aa AAA BB bbb))
- expect(ordered_group_names('name_desc')).to eq(%w(bbb BB AAA aa))
+ expect(ordered_group_names('name_asc')).to eq(%w[aa AAA BB bbb])
+ expect(ordered_group_names('name_desc')).to eq(%w[bbb BB AAA aa])
end
it 'sorts groups by created_at' do
- expect(ordered_group_names('created_asc')).to eq(%w(aa AAA BB bbb))
- expect(ordered_group_names('created_desc')).to eq(%w(bbb BB AAA aa))
- expect(ordered_group_names('created_date')).to eq(%w(bbb BB AAA aa))
+ expect(ordered_group_names('created_asc')).to eq(%w[aa AAA BB bbb])
+ expect(ordered_group_names('created_desc')).to eq(%w[bbb BB AAA aa])
+ expect(ordered_group_names('created_date')).to eq(%w[bbb BB AAA aa])
end
it 'sorts groups by updated_at' do
- expect(ordered_group_names('updated_asc')).to eq(%w(bbb BB AAA aa))
- expect(ordered_group_names('updated_desc')).to eq(%w(aa AAA BB bbb))
+ expect(ordered_group_names('updated_asc')).to eq(%w[bbb BB AAA aa])
+ expect(ordered_group_names('updated_desc')).to eq(%w[aa AAA BB bbb])
end
end
end
diff --git a/spec/models/concerns/token_authenticatable_spec.rb b/spec/models/concerns/token_authenticatable_spec.rb
index 822e2817d84..9bd20676c0e 100644
--- a/spec/models/concerns/token_authenticatable_spec.rb
+++ b/spec/models/concerns/token_authenticatable_spec.rb
@@ -260,7 +260,7 @@ RSpec.describe Ci::Build, 'TokenAuthenticatable' do
it 'persists a new token' do
build.save!
- build.token.yield_self do |previous_token|
+ build.token.then do |previous_token|
build.reset_token!
expect(build.token).not_to eq previous_token
diff --git a/spec/models/concerns/use_sql_function_for_primary_key_lookups_spec.rb b/spec/models/concerns/use_sql_function_for_primary_key_lookups_spec.rb
new file mode 100644
index 00000000000..f6f53c9aad5
--- /dev/null
+++ b/spec/models/concerns/use_sql_function_for_primary_key_lookups_spec.rb
@@ -0,0 +1,181 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe UseSqlFunctionForPrimaryKeyLookups, feature_category: :groups_and_projects do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:another_project) { create(:project) }
+
+ let(:model) do
+ Class.new(ApplicationRecord) do
+ self.table_name = :projects
+
+ include UseSqlFunctionForPrimaryKeyLookups
+ end
+ end
+
+ context 'when the use_sql_functions_for_primary_key_lookups FF is on' do
+ before do
+ stub_feature_flags(use_sql_functions_for_primary_key_lookups: true)
+ end
+
+ it 'loads the correct record' do
+ expect(model.find(project.id).id).to eq(project.id)
+ end
+
+ it 'uses the fuction-based finder query' do
+ query = <<~SQL
+ SELECT "projects".* FROM find_projects_by_id(#{project.id})#{' '}
+ "projects" WHERE ("projects"."id" IS NOT NULL) LIMIT 1
+ SQL
+ query_log = ActiveRecord::QueryRecorder.new { model.find(project.id) }.log
+
+ expect(query_log).to match_array(include(query.tr("\n", '')))
+ end
+
+ it 'uses query cache', :use_sql_query_cache do
+ query = <<~SQL
+ SELECT "projects".* FROM find_projects_by_id(#{project.id})#{' '}
+ "projects" WHERE ("projects"."id" IS NOT NULL) LIMIT 1
+ SQL
+
+ recorder = ActiveRecord::QueryRecorder.new do
+ model.find(project.id)
+ model.find(project.id)
+ model.find(project.id)
+ end
+
+ expect(recorder.data.each_value.first[:count]).to eq(1)
+ expect(recorder.cached).to include(query.tr("\n", ''))
+ end
+
+ context 'when the model has ignored columns' do
+ around do |example|
+ model.ignored_columns = %i[path]
+ example.run
+ model.ignored_columns = []
+ end
+
+ it 'enumerates the column names' do
+ column_list = model.columns.map do |column|
+ %("projects"."#{column.name}")
+ end.join(', ')
+
+ expect(column_list).not_to include(%("projects"."path"))
+
+ query = <<~SQL
+ SELECT #{column_list} FROM find_projects_by_id(#{project.id})#{' '}
+ "projects" WHERE ("projects"."id" IS NOT NULL) LIMIT 1
+ SQL
+ query_log = ActiveRecord::QueryRecorder.new { model.find(project.id) }.log
+
+ expect(query_log).to match_array(include(query.tr("\n", '')))
+ end
+ end
+
+ context 'when there are scope attributes' do
+ let(:scoped_model) do
+ Class.new(model) do
+ default_scope { where.not(path: nil) } # rubocop: disable Cop/DefaultScope -- Needed for testing a specific case
+ end
+ end
+
+ it 'loads the correct record' do
+ expect(scoped_model.find(project.id).id).to eq(project.id)
+ end
+
+ it 'does not use the function-based finder query' do
+ query_log = ActiveRecord::QueryRecorder.new { scoped_model.find(project.id) }.log
+
+ expect(query_log).not_to include(match(/find_projects_by_id/))
+ end
+ end
+
+ context 'when there are multiple arguments' do
+ it 'loads the correct records' do
+ expect(model.find(project.id, another_project.id).map(&:id)).to match_array([project.id, another_project.id])
+ end
+
+ it 'does not use the function-based finder query' do
+ query_log = ActiveRecord::QueryRecorder.new { model.find(project.id, another_project.id) }.log
+
+ expect(query_log).not_to include(match(/find_projects_by_id/))
+ end
+ end
+
+ context 'when there is block given' do
+ it 'loads the correct records' do
+ expect(model.find(0) { |p| p.path == project.path }.id).to eq(project.id)
+ end
+
+ it 'does not use the function-based finder query' do
+ query_log = ActiveRecord::QueryRecorder.new { model.find(0) { |p| p.path == project.path } }.log
+
+ expect(query_log).not_to include(match(/find_projects_by_id/))
+ end
+ end
+
+ context 'when there is no primary key defined' do
+ let(:model_without_pk) do
+ Class.new(model) do
+ def self.primary_key
+ nil
+ end
+ end
+ end
+
+ it 'raises ActiveRecord::UnknownPrimaryKey' do
+ expect { model_without_pk.find(0) }.to raise_error ActiveRecord::UnknownPrimaryKey
+ end
+ end
+
+ context 'when id is provided as an array' do
+ it 'returns the correct record as an array' do
+ expect(model.find([project.id]).map(&:id)).to eq([project.id])
+ end
+
+ it 'does use the function-based finder query' do
+ query_log = ActiveRecord::QueryRecorder.new { model.find([project.id]) }.log
+
+ expect(query_log).to include(match(/find_projects_by_id/))
+ end
+
+ context 'when array has multiple elements' do
+ it 'does not use the function-based finder query' do
+ query_log = ActiveRecord::QueryRecorder.new { model.find([project.id, another_project.id]) }.log
+
+ expect(query_log).not_to include(match(/find_projects_by_id/))
+ end
+ end
+ end
+
+ context 'when the provided id is null' do
+ it 'raises ActiveRecord::RecordNotFound' do
+ expect { model.find(nil) }.to raise_error ActiveRecord::RecordNotFound, "Couldn't find without an ID"
+ end
+ end
+
+ context 'when the provided id is not a string that can cast to numeric' do
+ it 'raises ActiveRecord::RecordNotFound' do
+ expect { model.find('foo') }.to raise_error ActiveRecord::RecordNotFound, "Couldn't find with 'id'=foo"
+ end
+ end
+ end
+
+ context 'when the use_sql_functions_for_primary_key_lookups FF is off' do
+ before do
+ stub_feature_flags(use_sql_functions_for_primary_key_lookups: false)
+ end
+
+ it 'loads the correct record' do
+ expect(model.find(project.id).id).to eq(project.id)
+ end
+
+ it 'uses the SQL-based finder query' do
+ expected_query = %(SELECT "projects".* FROM \"projects\" WHERE "projects"."id" = #{project.id} LIMIT 1)
+ query_log = ActiveRecord::QueryRecorder.new { model.find(project.id) }.log
+
+ expect(query_log).to match_array(include(expected_query))
+ end
+ end
+end
diff --git a/spec/models/container_repository_spec.rb b/spec/models/container_repository_spec.rb
index 93fe070e5c4..027fd20462b 100644
--- a/spec/models/container_repository_spec.rb
+++ b/spec/models/container_repository_spec.rb
@@ -675,6 +675,101 @@ RSpec.describe ContainerRepository, :aggregate_failures, feature_category: :cont
end
end
+ describe '#tags_page' do
+ let_it_be(:page_size) { 100 }
+ let_it_be(:before) { 'before' }
+ let_it_be(:last) { 'last' }
+ let_it_be(:sort) { '-name' }
+ let_it_be(:name) { 'repo' }
+
+ subject do
+ repository.tags_page(before: before, last: last, sort: sort, name: name, page_size: page_size)
+ end
+
+ before do
+ allow(repository).to receive(:migrated?).and_return(true)
+ end
+
+ it 'calls GitlabApiClient#tags and passes parameters' do
+ allow(repository.gitlab_api_client).to receive(:tags).and_return({})
+ expect(repository.gitlab_api_client).to receive(:tags).with(
+ repository.path, page_size: page_size, before: before, last: last, sort: sort, name: name)
+
+ subject
+ end
+
+ context 'with a call to tags' do
+ let_it_be(:tags_response) do
+ [
+ {
+ name: '0.1.0',
+ digest: 'sha256:6c3c624b58dbbcd3c0dd82b4c53f04194d1247c6eebdaab7c610cf7d6670',
+ config_digest: 'sha256:66b1132a0173910b01ee69583bbf2f7f1e4462c99efbe1b9ab5bf',
+ media_type: 'application/vnd.oci.image.manifest.v1+json',
+ size_bytes: 1234567890,
+ created_at: 5.minutes.ago,
+ updated_at: 5.minutes.ago
+ },
+ {
+ name: 'latest',
+ digest: 'sha256:6c3c624b58dbbcd3c0dd82b4c53f04191247c6eebdaab7c610cf7d66709b3',
+ config_digest: 'sha256:66b1132a0173910b01ee694462c99efbe1b9ab5bf8083231232312',
+ media_type: 'application/vnd.oci.image.manifest.v1+json',
+ size_bytes: 1234567892,
+ created_at: 10.minutes.ago,
+ updated_at: 10.minutes.ago
+ }
+ ]
+ end
+
+ let_it_be(:response_body) do
+ {
+ pagination: {
+ previous: { uri: URI('/test?before=prev-cursor') },
+ next: { uri: URI('/test?last=next-cursor') }
+ },
+ response_body: ::Gitlab::Json.parse(tags_response.to_json)
+ }
+ end
+
+ before do
+ allow(repository.gitlab_api_client).to receive(:tags).and_return(response_body)
+ end
+
+ it 'returns tags and parses the previous and next cursors' do
+ return_value = subject
+
+ expect(return_value[:pagination]).to eq(response_body[:pagination])
+
+ return_value[:tags].each_with_index do |tag, index|
+ expected_revision = tags_response[index][:config_digest].to_s.split(':')[1]
+
+ expect(tag.is_a?(ContainerRegistry::Tag)).to eq(true)
+ expect(tag).to have_attributes(
+ repository: repository,
+ name: tags_response[index][:name],
+ digest: tags_response[index][:digest],
+ total_size: tags_response[index][:size_bytes],
+ revision: expected_revision,
+ short_revision: expected_revision[0..8],
+ created_at: DateTime.rfc3339(tags_response[index][:created_at].rfc3339),
+ updated_at: DateTime.rfc3339(tags_response[index][:updated_at].rfc3339)
+ )
+ end
+ end
+ end
+
+ context 'calling on a non migrated repository' do
+ before do
+ allow(repository).to receive(:migrated?).and_return(false)
+ end
+
+ it 'raises an Argument error' do
+ expect { repository.tags_page }.to raise_error(ArgumentError, 'not a migrated repository')
+ end
+ end
+ end
+
describe '#tags_count' do
it 'returns the count of tags' do
expect(repository.tags_count).to eq(1)
@@ -720,7 +815,7 @@ RSpec.describe ContainerRepository, :aggregate_failures, feature_category: :cont
end
end
- describe '#delete_tag_by_name' do
+ describe '#delete_tag' do
let(:repository) do
create(
:container_repository,
@@ -733,22 +828,22 @@ RSpec.describe ContainerRepository, :aggregate_failures, feature_category: :cont
context 'when action succeeds' do
it 'returns status that indicates success' do
expect(repository.client)
- .to receive(:delete_repository_tag_by_name)
+ .to receive(:delete_repository_tag_by_digest)
.with(repository.path, "latest")
.and_return(true)
- expect(repository.delete_tag_by_name('latest')).to be_truthy
+ expect(repository.delete_tag('latest')).to be_truthy
end
end
context 'when action fails' do
it 'returns status that indicates failure' do
expect(repository.client)
- .to receive(:delete_repository_tag_by_name)
+ .to receive(:delete_repository_tag_by_digest)
.with(repository.path, "latest")
.and_return(false)
- expect(repository.delete_tag_by_name('latest')).to be_falsey
+ expect(repository.delete_tag('latest')).to be_falsey
end
end
end
diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb
index 639b149e2ae..ee48e8cac6c 100644
--- a/spec/models/deployment_spec.rb
+++ b/spec/models/deployment_spec.rb
@@ -1336,7 +1336,7 @@ RSpec.describe Deployment, feature_category: :continuous_delivery do
let(:job_status) { :created }
it_behaves_like 'gracefully handling error' do
- let(:error_message) { %{Status cannot transition via \"create\"} }
+ let(:error_message) { %(Status cannot transition via \"create\") }
end
end
@@ -1344,7 +1344,7 @@ RSpec.describe Deployment, feature_category: :continuous_delivery do
let(:job_status) { :manual }
it_behaves_like 'gracefully handling error' do
- let(:error_message) { %{Status cannot transition via \"block\"} }
+ let(:error_message) { %(Status cannot transition via \"block\") }
end
end
@@ -1374,7 +1374,7 @@ RSpec.describe Deployment, feature_category: :continuous_delivery do
let(:job_status) { :created }
it_behaves_like 'gracefully handling error' do
- let(:error_message) { %{Status cannot transition via \"create\"} }
+ let(:error_message) { %(Status cannot transition via \"create\") }
end
end
@@ -1382,7 +1382,7 @@ RSpec.describe Deployment, feature_category: :continuous_delivery do
let(:job_status) { :manual }
it_behaves_like 'gracefully handling error' do
- let(:error_message) { %{Status cannot transition via \"block\"} }
+ let(:error_message) { %(Status cannot transition via \"block\") }
end
end
@@ -1390,7 +1390,7 @@ RSpec.describe Deployment, feature_category: :continuous_delivery do
let(:job_status) { :running }
it_behaves_like 'gracefully handling error' do
- let(:error_message) { %{Status cannot transition via \"run\"} }
+ let(:error_message) { %(Status cannot transition via \"run\") }
end
end
diff --git a/spec/models/diff_viewer/base_spec.rb b/spec/models/diff_viewer/base_spec.rb
index 57c62788ee9..8ab7b090928 100644
--- a/spec/models/diff_viewer/base_spec.rb
+++ b/spec/models/diff_viewer/base_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe DiffViewer::Base do
Class.new(described_class) do
include DiffViewer::ServerSide
- self.extensions = %w(jpg)
+ self.extensions = %w[jpg]
self.binary = true
self.collapse_limit = 1.megabyte
self.size_limit = 5.megabytes
@@ -55,7 +55,7 @@ RSpec.describe DiffViewer::Base do
before do
allow(diff_file).to receive(:renamed_file?).and_return(true)
- viewer_class.extensions = %w(notjpg)
+ viewer_class.extensions = %w[notjpg]
end
it 'returns false' do
diff --git a/spec/models/email_spec.rb b/spec/models/email_spec.rb
index b76063bfa1a..ecb8f72470d 100644
--- a/spec/models/email_spec.rb
+++ b/spec/models/email_spec.rb
@@ -89,7 +89,7 @@ RSpec.describe Email do
end
context 'when the confirmation period has expired' do
- let(:confirmation_sent_at) { expired_confirmation_sent_at }
+ let(:confirmation_sent_at) { expired_confirmation_sent_at }
it_behaves_like 'unconfirmed email'
@@ -101,7 +101,7 @@ RSpec.describe Email do
end
context 'when the confirmation period has not expired' do
- let(:confirmation_sent_at) { extant_confirmation_sent_at }
+ let(:confirmation_sent_at) { extant_confirmation_sent_at }
it_behaves_like 'unconfirmed email'
@@ -138,7 +138,7 @@ RSpec.describe Email do
end
context 'when the confirmation period has expired' do
- let(:confirmation_sent_at) { expired_confirmation_sent_at }
+ let(:confirmation_sent_at) { expired_confirmation_sent_at }
it_behaves_like 'unconfirmed email'
it_behaves_like 'confirms the email on force_confirm'
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index dcfee7fcc8c..33142922670 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -166,6 +166,37 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching, feature_categ
end
end
+ describe '#long_stopping?' do
+ subject { environment1.long_stopping? }
+
+ let(:long_ago) { (described_class::LONG_STOP + 1.day).ago }
+ let(:not_long_ago) { (described_class::LONG_STOP - 1.day).ago }
+
+ context 'when a stopping environment has not been updated recently' do
+ let!(:environment1) { create(:environment, state: 'stopping', project: project, updated_at: long_ago) }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when a stopping environment has been updated recently' do
+ let!(:environment1) { create(:environment, state: 'stopping', project: project, updated_at: not_long_ago) }
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when a non stopping environment has not been updated recently' do
+ let!(:environment1) { create(:environment, project: project, updated_at: long_ago) }
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when a non stopping environment has been updated recently' do
+ let!(:environment1) { create(:environment, project: project, updated_at: not_long_ago) }
+
+ it { is_expected.to eq(false) }
+ end
+ end
+
describe ".stopped_review_apps" do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:old_stopped_review_env) { create(:environment, :with_review_app, :stopped, created_at: 31.days.ago, project: project) }
@@ -406,6 +437,47 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching, feature_categ
end
end
+ describe '.long_stopping' do
+ subject { described_class.long_stopping }
+
+ let_it_be(:project) { create(:project) }
+ let(:environment) { create(:environment, project: project) }
+ let(:long) { (described_class::LONG_STOP + 1.day).ago }
+ let(:short) { (described_class::LONG_STOP - 1.day).ago }
+
+ context 'when a stopping environment has not been updated recently' do
+ before do
+ environment.update!(state: :stopping, updated_at: long)
+ end
+
+ it { is_expected.to eq([environment]) }
+ end
+
+ context 'when a stopping environment has been updated recently' do
+ before do
+ environment.update!(state: :stopping, updated_at: short)
+ end
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'when a non stopping environment has not been updated recently' do
+ before do
+ environment.update!(state: :available, updated_at: long)
+ end
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'when a non stopping environment has been updated recently' do
+ before do
+ environment.update!(state: :available, updated_at: short)
+ end
+
+ it { is_expected.to be_empty }
+ end
+ end
+
describe '.pluck_names' do
subject { described_class.pluck_names }
@@ -1361,7 +1433,7 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching, feature_categ
end
context 'reactive cache has pod data' do
- let(:cache_data) { Hash(pods: %w(pod1 pod2)) }
+ let(:cache_data) { Hash(pods: %w[pod1 pod2]) }
before do
stub_reactive_cache(environment, cache_data)
@@ -1390,9 +1462,9 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching, feature_categ
it 'returns cache data from the deployment platform' do
expect(environment.deployment_platform).to receive(:calculate_reactive_cache_for)
- .with(environment).and_return(pods: %w(pod1 pod2))
+ .with(environment).and_return(pods: %w[pod1 pod2])
- is_expected.to eq(pods: %w(pod1 pod2))
+ is_expected.to eq(pods: %w[pod1 pod2])
end
context 'environment does not have terminals available' do
@@ -1863,8 +1935,8 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching, feature_categ
end
context 'cached rollout status is present' do
- let(:pods) { %w(pod1 pod2) }
- let(:deployments) { %w(deployment1 deployment2) }
+ let(:pods) { %w[pod1 pod2] }
+ let(:deployments) { %w[deployment1 deployment2] }
before do
stub_reactive_cache(environment, pods: pods, deployments: deployments)
diff --git a/spec/models/group_label_spec.rb b/spec/models/group_label_spec.rb
index 701348baf48..40019fdc94c 100644
--- a/spec/models/group_label_spec.rb
+++ b/spec/models/group_label_spec.rb
@@ -34,7 +34,7 @@ RSpec.describe GroupLabel do
end
it 'uses id when name contains double quote' do
- label = create(:label, name: %q{"irony"})
+ label = create(:label, name: %q("irony"))
expect(label.to_reference(format: :name)).to eq "~#{label.id}"
end
end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 96ef36a5b75..2bca73545d0 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -1659,6 +1659,12 @@ RSpec.describe Group, feature_category: :groups_and_projects do
end
end
+ it 'returns true for a user in parent group' do
+ subgroup = create(:group, parent: group)
+
+ expect(subgroup.member?(user)).to be_truthy
+ end
+
context 'in shared group' do
let(:shared_group) { create(:group) }
let(:member_shared) { create(:user) }
@@ -2053,29 +2059,6 @@ RSpec.describe Group, feature_category: :groups_and_projects do
end
end
- describe '#project_users_with_descendants' do
- let(:user_a) { create(:user) }
- let(:user_b) { create(:user) }
- let(:user_c) { create(:user) }
-
- let(:group) { create(:group) }
- let(:nested_group) { create(:group, parent: group) }
- let(:deep_nested_group) { create(:group, parent: nested_group) }
- let(:project_a) { create(:project, namespace: group) }
- let(:project_b) { create(:project, namespace: nested_group) }
- let(:project_c) { create(:project, namespace: deep_nested_group) }
-
- it 'returns members of all projects in group and subgroups' do
- project_a.add_developer(user_a)
- project_b.add_developer(user_b)
- project_c.add_developer(user_c)
-
- expect(group.project_users_with_descendants).to contain_exactly(user_a, user_b, user_c)
- expect(nested_group.project_users_with_descendants).to contain_exactly(user_b, user_c)
- expect(deep_nested_group.project_users_with_descendants).to contain_exactly(user_c)
- end
- end
-
describe '#refresh_members_authorized_projects' do
let_it_be(:group) { create(:group, :nested) }
let_it_be(:parent_group_user) { create(:user) }
@@ -2953,18 +2936,21 @@ RSpec.describe Group, feature_category: :groups_and_projects do
end
describe '.ids_with_disabled_email' do
- let_it_be(:parent_1) { create(:group, emails_disabled: true) }
+ let_it_be(:parent_1) { create(:group) }
let_it_be(:child_1) { create(:group, parent: parent_1) }
- let_it_be(:parent_2) { create(:group, emails_disabled: false) }
+ let_it_be(:parent_2) { create(:group) }
let_it_be(:child_2) { create(:group, parent: parent_2) }
- let_it_be(:other_group) { create(:group, emails_disabled: false) }
+ let_it_be(:other_group) { create(:group) }
shared_examples 'returns namespaces with disabled email' do
subject(:group_ids_where_email_is_disabled) { described_class.ids_with_disabled_email([child_1, child_2, other_group]) }
- it { is_expected.to eq(Set.new([child_1.id])) }
+ it do
+ parent_1.update_attribute(:emails_enabled, false)
+ is_expected.to eq(Set.new([child_1.id]))
+ end
end
it_behaves_like 'returns namespaces with disabled email'
diff --git a/spec/models/guest_spec.rb b/spec/models/guest_spec.rb
deleted file mode 100644
index 975b64cb855..00000000000
--- a/spec/models/guest_spec.rb
+++ /dev/null
@@ -1,47 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Guest do
- let_it_be(:public_project, reload: true) { create(:project, :public) }
- let_it_be(:private_project) { create(:project, :private) }
- let_it_be(:internal_project) { create(:project, :internal) }
-
- describe '.can_pull?' do
- context 'when project is private' do
- it 'does not allow to pull the repo' do
- expect(described_class.can?(:download_code, private_project)).to eq(false)
- end
- end
-
- context 'when project is internal' do
- it 'does not allow to pull the repo' do
- expect(described_class.can?(:download_code, internal_project)).to eq(false)
- end
- end
-
- context 'when project is public' do
- context 'when repository is disabled' do
- it 'does not allow to pull the repo' do
- public_project.project_feature.update_attribute(:repository_access_level, ProjectFeature::DISABLED)
-
- expect(described_class.can?(:download_code, public_project)).to eq(false)
- end
- end
-
- context 'when repository is accessible only by team members' do
- it 'does not allow to pull the repo' do
- public_project.project_feature.update_attribute(:repository_access_level, ProjectFeature::PRIVATE)
-
- expect(described_class.can?(:download_code, public_project)).to eq(false)
- end
- end
-
- context 'when repository is enabled' do
- it 'allows to pull the repo' do
- expect(described_class.can?(:download_code, public_project)).to eq(true)
- end
- end
- end
- end
-end
diff --git a/spec/models/instance_configuration_spec.rb b/spec/models/instance_configuration_spec.rb
index 346f743e8ef..6fbb9245885 100644
--- a/spec/models/instance_configuration_spec.rb
+++ b/spec/models/instance_configuration_spec.rb
@@ -36,7 +36,7 @@ RSpec.describe InstanceConfiguration do
result = subject.settings[:ssh_algorithms_hashes]
- expect(result.map { |a| a[:name] }).to match_array(%w(DSA ECDSA ED25519 RSA))
+ expect(result.map { |a| a[:name] }).to match_array(%w[DSA ECDSA ED25519 RSA])
end
it 'does not include disabled algorithm' do
@@ -45,7 +45,7 @@ RSpec.describe InstanceConfiguration do
result = subject.settings[:ssh_algorithms_hashes]
- expect(result.map { |a| a[:name] }).to match_array(%w(ECDSA ED25519 RSA))
+ expect(result.map { |a| a[:name] }).to match_array(%w[ECDSA ED25519 RSA])
end
def pub_file(exist: true)
diff --git a/spec/models/integration_spec.rb b/spec/models/integration_spec.rb
index d7b69546de6..5af6a592c66 100644
--- a/spec/models/integration_spec.rb
+++ b/spec/models/integration_spec.rb
@@ -157,6 +157,18 @@ RSpec.describe Integration, feature_category: :integrations do
include_examples 'hook scope', 'incident'
end
+ describe '.title' do
+ it 'raises an error' do
+ expect { described_class.title }.to raise_error(NotImplementedError)
+ end
+ end
+
+ describe '.description' do
+ it 'raises an error' do
+ expect { described_class.description }.to raise_error(NotImplementedError)
+ end
+ end
+
describe '#operating?' do
it 'is false when the integration is not active' do
expect(build(:integration).operating?).to eq(false)
@@ -976,7 +988,7 @@ RSpec.describe Integration, feature_category: :integrations do
subject { described_class.available_integration_names }
before do
- allow(described_class).to receive(:integration_names).and_return(%w(foo))
+ allow(described_class).to receive(:integration_names).and_return(%w[foo])
allow(described_class).to receive(:project_specific_integration_names).and_return(['bar'])
allow(described_class).to receive(:dev_integration_names).and_return(['baz'])
end
@@ -1315,6 +1327,7 @@ RSpec.describe Integration, feature_category: :integrations do
describe '#async_execute' do
let(:integration) { described_class.new(id: 123) }
let(:data) { { object_kind: 'build' } }
+ let(:serialized_data) { data.deep_stringify_keys }
let(:supported_events) { %w[push build] }
subject(:async_execute) { integration.async_execute(data) }
@@ -1324,7 +1337,7 @@ RSpec.describe Integration, feature_category: :integrations do
end
it 'queues a Integrations::ExecuteWorker' do
- expect(Integrations::ExecuteWorker).to receive(:perform_async).with(integration.id, data)
+ expect(Integrations::ExecuteWorker).to receive(:perform_async).with(integration.id, serialized_data)
async_execute
end
diff --git a/spec/models/integrations/base_chat_notification_spec.rb b/spec/models/integrations/base_chat_notification_spec.rb
index 497f2f1e7c9..9ad37f40fbc 100644
--- a/spec/models/integrations/base_chat_notification_spec.rb
+++ b/spec/models/integrations/base_chat_notification_spec.rb
@@ -347,12 +347,6 @@ RSpec.describe Integrations::BaseChatNotification, feature_category: :integratio
end
end
- describe '#help' do
- it 'raises an error' do
- expect { subject.help }.to raise_error(NotImplementedError)
- end
- end
-
describe '#event_channel_name' do
it 'returns the channel field name for the given event' do
expect(subject.event_channel_name(:event)).to eq('event_channel')
diff --git a/spec/models/integrations/buildkite_spec.rb b/spec/models/integrations/buildkite_spec.rb
index f3231d50eae..ce31c0b20a3 100644
--- a/spec/models/integrations/buildkite_spec.rb
+++ b/spec/models/integrations/buildkite_spec.rb
@@ -50,7 +50,7 @@ RSpec.describe Integrations::Buildkite, :use_clean_rails_memory_store_caching, f
describe '.supported_events' do
it 'supports push, merge_request, and tag_push events' do
- expect(integration.supported_events).to eq %w(push merge_request tag_push)
+ expect(integration.supported_events).to eq %w[push merge_request tag_push]
end
end
diff --git a/spec/models/integrations/campfire_spec.rb b/spec/models/integrations/campfire_spec.rb
index 38d3d89cdbf..19f819252f8 100644
--- a/spec/models/integrations/campfire_spec.rb
+++ b/spec/models/integrations/campfire_spec.rb
@@ -48,7 +48,7 @@ RSpec.describe Integrations::Campfire, feature_category: :integrations do
)
@sample_data = Gitlab::DataBuilder::Push.build_sample(project, user)
@rooms_url = 'https://project-name.campfirenow.com/rooms.json'
- @auth = %w(verySecret X)
+ @auth = %w[verySecret X]
@headers = { 'Content-Type' => 'application/json; charset=utf-8' }
end
diff --git a/spec/models/integrations/integration_list_spec.rb b/spec/models/integrations/integration_list_spec.rb
index b7ccbcecf6b..4bb7b100bc0 100644
--- a/spec/models/integrations/integration_list_spec.rb
+++ b/spec/models/integrations/integration_list_spec.rb
@@ -12,10 +12,10 @@ RSpec.describe Integrations::IntegrationList, feature_category: :integrations do
describe '#to_array' do
it 'returns array of Integration, columns, and values' do
- expect(subject.to_array).to eq([
+ expect(subject.to_array).to match_array([
Integration,
%w[active category project_id],
- [['true', 'common', projects.first.id], ['true', 'common', projects.second.id]]
+ contain_exactly(['true', 'common', projects.first.id], ['true', 'common', projects.second.id])
])
end
end
diff --git a/spec/models/integrations/jira_spec.rb b/spec/models/integrations/jira_spec.rb
index c87128db221..af021c51035 100644
--- a/spec/models/integrations/jira_spec.rb
+++ b/spec/models/integrations/jira_spec.rb
@@ -603,6 +603,17 @@ RSpec.describe Integrations::Jira, feature_category: :integrations do
jira_integration.client.get('/foo')
end
+ context 'when a custom read_timeout option is passed as an argument' do
+ it 'uses the default GitLab::HTTP timeouts plus a custom read_timeout' do
+ expected_timeouts = Gitlab::HTTP::DEFAULT_TIMEOUT_OPTIONS.merge(read_timeout: 2.minutes, timeout: 2.minutes)
+
+ expect(Gitlab::HTTP_V2::Client).to receive(:httparty_perform_request)
+ .with(Net::HTTP::Get, '/foo', hash_including(expected_timeouts)).and_call_original
+
+ jira_integration.client(read_timeout: 2.minutes).get('/foo')
+ end
+ end
+
context 'with basic auth' do
before do
jira_integration.jira_auth_type = 0
@@ -719,10 +730,10 @@ RSpec.describe Integrations::Jira, feature_category: :integrations do
allow_any_instance_of(JIRA::Resource::Issue).to receive(:key).and_return(issue_key)
allow(JIRA::Resource::Remotelink).to receive(:all).and_return([])
- WebMock.stub_request(:get, issue_url).with(basic_auth: %w(jira-username jira-password))
- WebMock.stub_request(:post, transitions_url).with(basic_auth: %w(jira-username jira-password))
- WebMock.stub_request(:post, comment_url).with(basic_auth: %w(jira-username jira-password))
- WebMock.stub_request(:post, remote_link_url).with(basic_auth: %w(jira-username jira-password))
+ WebMock.stub_request(:get, issue_url).with(basic_auth: %w[jira-username jira-password])
+ WebMock.stub_request(:post, transitions_url).with(basic_auth: %w[jira-username jira-password])
+ WebMock.stub_request(:post, comment_url).with(basic_auth: %w[jira-username jira-password])
+ WebMock.stub_request(:post, remote_link_url).with(basic_auth: %w[jira-username jira-password])
end
let(:external_issue) { ExternalIssue.new('JIRA-123', project) }
@@ -864,7 +875,7 @@ RSpec.describe Integrations::Jira, feature_category: :integrations do
it 'logs exception when transition id is not valid' do
allow(jira_integration).to receive(:log_exception)
- WebMock.stub_request(:post, transitions_url).with(basic_auth: %w(jira-username jira-password)).and_raise("Bad Request")
+ WebMock.stub_request(:post, transitions_url).with(basic_auth: %w[jira-username jira-password]).and_raise("Bad Request")
close_issue
@@ -973,7 +984,7 @@ RSpec.describe Integrations::Jira, feature_category: :integrations do
context 'when a transition fails' do
before do
- WebMock.stub_request(:post, transitions_url).with(basic_auth: %w(jira-username jira-password)).to_return do |request|
+ WebMock.stub_request(:post, transitions_url).with(basic_auth: %w[jira-username jira-password]).to_return do |request|
{ status: request.body.include?('"id":"2"') ? 500 : 200 }
end
end
diff --git a/spec/models/integrations/teamcity_spec.rb b/spec/models/integrations/teamcity_spec.rb
index c4c7202fae0..1537b10ba03 100644
--- a/spec/models/integrations/teamcity_spec.rb
+++ b/spec/models/integrations/teamcity_spec.rb
@@ -308,7 +308,7 @@ RSpec.describe Integrations::Teamcity, :use_clean_rails_memory_store_caching do
def stub_post_to_build_queue(branch:)
teamcity_full_url = "#{teamcity_url}/httpAuth/app/rest/buildQueue"
body ||= %(<build branchName=\"#{branch}\"><buildType id=\"foo\"/></build>)
- auth = %w(mic password)
+ auth = %w[mic password]
stub_full_request(teamcity_full_url, method: :post).with(
basic_auth: auth,
@@ -320,7 +320,7 @@ RSpec.describe Integrations::Teamcity, :use_clean_rails_memory_store_caching do
end
def stub_request(status: 200, body: nil, build_status: 'success')
- auth = %w(mic password)
+ auth = %w[mic password]
body ||= %({"build":{"status":"#{build_status}","id":"666"}})
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index e7a5a53c6a0..6c8603d7b4c 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -373,10 +373,10 @@ RSpec.describe Issue, feature_category: :team_planning do
describe '.simple_sorts' do
it 'includes all keys' do
expect(described_class.simple_sorts.keys).to include(
- *%w(created_asc created_at_asc created_date created_desc created_at_desc
+ *%w[created_asc created_at_asc created_date created_desc created_at_desc
closest_future_date closest_future_date_asc due_date due_date_asc due_date_desc
id_asc id_desc relative_position relative_position_asc updated_desc updated_asc
- updated_at_asc updated_at_desc title_asc title_desc))
+ updated_at_asc updated_at_desc title_asc title_desc])
end
end
@@ -390,7 +390,7 @@ RSpec.describe Issue, feature_category: :team_planning do
end
it 'returns issues with the given issue types' do
- expect(described_class.with_issue_type(%w(issue incident)))
+ expect(described_class.with_issue_type(%w[issue incident]))
.to contain_exactly(issue, incident)
end
@@ -439,7 +439,7 @@ RSpec.describe Issue, feature_category: :team_planning do
end
it 'returns issues without the given issue types' do
- expect(described_class.without_issue_type(%w(issue incident)))
+ expect(described_class.without_issue_type(%w[issue incident]))
.to contain_exactly(task)
end
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index fdd8a610fe4..b4941c71d6a 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -591,6 +591,22 @@ RSpec.describe Member, feature_category: :groups_and_projects do
it { is_expected.not_to include @member_with_minimal_access }
it { is_expected.not_to include awaiting_group_member }
it { is_expected.not_to include awaiting_project_member }
+
+ context 'when minimal_access is true' do
+ subject { described_class.without_invites_and_requests(minimal_access: true) }
+
+ it { is_expected.to include @owner }
+ it { is_expected.to include @maintainer }
+ it { is_expected.not_to include @invited_member }
+ it { is_expected.to include @accepted_invite_member }
+ it { is_expected.not_to include @requested_member }
+ it { is_expected.to include @accepted_request_member }
+ it { is_expected.to include @blocked_maintainer }
+ it { is_expected.to include @blocked_developer }
+ it { is_expected.to include @member_with_minimal_access }
+ it { is_expected.not_to include awaiting_group_member }
+ it { is_expected.not_to include awaiting_project_member }
+ end
end
describe '.connected_to_user' do
diff --git a/spec/models/members/members/members_with_parents_spec.rb b/spec/models/members/members/members_with_parents_spec.rb
new file mode 100644
index 00000000000..46c934c932f
--- /dev/null
+++ b/spec/models/members/members/members_with_parents_spec.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Members::MembersWithParents, feature_category: :groups_and_projects do
+ let_it_be(:group) { create(:group, :nested) }
+ let_it_be(:maintainer) { group.parent.add_maintainer(create(:user)) }
+ let_it_be(:developer) { group.add_developer(create(:user)) }
+ let_it_be(:pending_maintainer) { create(:group_member, :awaiting, :maintainer, group: group.parent) }
+ let_it_be(:pending_developer) { create(:group_member, :awaiting, :developer, group: group) }
+ let_it_be(:invited_member) { create(:group_member, :invited, group: group) }
+ let_it_be(:inactive_developer) { group.add_developer(create(:user, :deactivated)) }
+ let_it_be(:minimal_access) { create(:group_member, :minimal_access, group: group) }
+
+ describe '#all_members' do
+ subject(:all_members) { described_class.new(group).all_members }
+
+ it 'returns all members for group and group parents' do
+ expect(all_members).to contain_exactly(
+ developer,
+ maintainer,
+ pending_maintainer,
+ pending_developer,
+ invited_member,
+ inactive_developer,
+ minimal_access
+ )
+ end
+ end
+
+ describe '#members' do
+ let(:arguments) { {} }
+
+ subject(:members) { described_class.new(group).members(**arguments) }
+
+ using Rspec::Parameterized::TableSyntax
+
+ where(:arguments, :expected_members) do
+ [
+ [
+ {},
+ lazy { [developer, maintainer, inactive_developer] }
+ ],
+ [
+ # minimal access is Premium, so in FOSS we will not include minimal access member
+ { minimal_access: true },
+ lazy { [developer, maintainer, inactive_developer] }
+ ],
+ [
+ { active_users: true },
+ lazy { [developer, maintainer] }
+ ]
+ ]
+ end
+
+ with_them do
+ it 'returns expected members' do
+ expect(members).to contain_exactly(*expected_members)
+ expect(members).not_to include(*(group.members - expected_members))
+ end
+ end
+
+ context 'when active_users: true and minimal_access: true' do
+ let(:arguments) { { active_users: true, minimal_access: true } }
+
+ it 'raises an error' do
+ expect { members }.to raise_error(ArgumentError)
+ end
+ end
+
+ context 'with group sharing' do
+ let_it_be(:shared_with_group) { create(:group) }
+
+ let_it_be(:shared_with_group_maintainer) do
+ shared_with_group.add_maintainer(create(:user))
+ end
+
+ let_it_be(:shared_with_group_developer) do
+ shared_with_group.add_developer(create(:user))
+ end
+
+ before do
+ create(:group_group_link, shared_group: group, shared_with_group: shared_with_group)
+ end
+
+ it 'returns shared with group members' do
+ expect(members).to(include(shared_with_group_maintainer))
+ expect(members).to(include(shared_with_group_developer))
+ end
+ end
+ end
+end
diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb
index a2b5bde8890..a9725a796bf 100644
--- a/spec/models/members/project_member_spec.rb
+++ b/spec/models/members/project_member_spec.rb
@@ -51,6 +51,50 @@ RSpec.describe ProjectMember, feature_category: :groups_and_projects do
end
end
+ describe '.permissible_access_level_roles_for_project_access_token' do
+ let_it_be(:owner) { create(:user) }
+ let_it_be(:maintainer) { create(:user) }
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, group: group) }
+
+ before do
+ project.add_owner(owner)
+ project.add_maintainer(maintainer)
+ project.add_developer(developer)
+ end
+
+ subject(:access_levels) { described_class.permissible_access_level_roles_for_project_access_token(user, project) }
+
+ context 'when member can manage owners' do
+ let(:user) { owner }
+
+ it 'returns Gitlab::Access.options_with_owner' do
+ expect(access_levels).to eq(Gitlab::Access.options_with_owner)
+ end
+ end
+
+ context 'when member cannot manage owners' do
+ let(:user) { maintainer }
+
+ it 'returns Gitlab::Access.options' do
+ expect(access_levels).to eq(Gitlab::Access.options)
+ end
+ end
+
+ context 'when the user is a developer' do
+ let(:user) { developer }
+
+ it 'returns Gitlab::Access.options' do
+ expect(access_levels).to eq({
+ "Guest" => 10,
+ "Reporter" => 20,
+ "Developer" => 30
+ })
+ end
+ end
+ end
+
describe '#real_source_type' do
subject { create(:project_member).real_source_type }
diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb
index 806ce3f21b5..bcab2029942 100644
--- a/spec/models/merge_request_diff_spec.rb
+++ b/spec/models/merge_request_diff_spec.rb
@@ -1090,7 +1090,7 @@ RSpec.describe MergeRequestDiff, feature_category: :code_review_workflow do
end
it 'returns affected file paths' do
- expect(subject.modified_paths).to eq(%w{foo bar baz})
+ expect(subject.modified_paths).to eq(%w[foo bar baz])
end
context "when fallback_on_overflow is true" do
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 40f85c92851..d3c32da2842 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -725,22 +725,35 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
end
end
- describe '.recent_target_branches' do
+ describe '.recent_target_branches and .recent_source_branches' do
+ def create_mr(source_branch, target_branch, status, remove_source_branch = false)
+ if remove_source_branch
+ create(:merge_request, status, :remove_source_branch, source_project: project,
+ target_branch: target_branch, source_branch: source_branch)
+ else
+ create(:merge_request, status, source_project: project,
+ target_branch: target_branch, source_branch: source_branch)
+ end
+ end
+
let(:project) { create(:project) }
- let!(:merge_request1) { create(:merge_request, :opened, source_project: project, target_branch: 'feature') }
- let!(:merge_request2) { create(:merge_request, :closed, source_project: project, target_branch: 'merge-test') }
- let!(:merge_request3) { create(:merge_request, :opened, source_project: project, target_branch: 'fix') }
- let!(:merge_request4) { create(:merge_request, :closed, source_project: project, target_branch: 'feature') }
+ let!(:merge_request1) { create_mr('source1', 'target1', :opened) }
+ let!(:merge_request2) { create_mr('source2', 'target2', :closed) }
+ let!(:merge_request3) { create_mr('source3', 'target3', :opened) }
+ let!(:merge_request4) { create_mr('source4', 'target1', :closed) }
+ let!(:merge_request5) { create_mr('source5', 'target4', :merged, true) }
before do
merge_request1.update_columns(updated_at: 1.day.since)
merge_request2.update_columns(updated_at: 2.days.since)
merge_request3.update_columns(updated_at: 3.days.since)
merge_request4.update_columns(updated_at: 4.days.since)
+ merge_request5.update_columns(updated_at: 5.days.since)
end
- it 'returns target branches sort by updated at desc' do
- expect(described_class.recent_target_branches).to match_array(%w[feature merge-test fix])
+ it 'returns branches sort by updated at desc' do
+ expect(described_class.recent_target_branches).to match_array(%w[target1 target2 target3 target4])
+ expect(described_class.recent_source_branches).to match_array(%w[source1 source2 source3 source4 source5])
end
end
@@ -3324,6 +3337,7 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
context 'with skip_ci_check option' do
before do
+ allow(subject.project).to receive(:only_allow_merge_if_pipeline_succeeds?).and_return(true)
allow(subject).to receive_messages(check_mergeability: nil, can_be_merged?: true, broken?: false)
end
@@ -3345,6 +3359,8 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
context 'with skip_discussions_check option' do
before do
+ allow(subject.project).to receive(:only_allow_merge_if_all_discussions_are_resolved?).and_return(true)
+
allow(subject).to receive_messages(
mergeable_ci_state?: true,
check_mergeability: nil,
@@ -3380,6 +3396,8 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
context 'with skip_rebase_check option' do
before do
+ allow(subject.project).to receive(:ff_merge_must_be_possible?).and_return(true)
+
allow(subject).to receive_messages(
mergeable_state?: true,
check_mergeability: nil,
@@ -3525,6 +3543,7 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
context 'when failed' do
context 'when #mergeable_ci_state? is false' do
before do
+ allow(subject.project).to receive(:only_allow_merge_if_pipeline_succeeds?) { true }
allow(subject).to receive(:mergeable_ci_state?) { false }
end
@@ -3539,6 +3558,7 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
context 'when #mergeable_discussions_state? is false' do
before do
+ allow(subject.project).to receive(:only_allow_merge_if_all_discussions_are_resolved?) { true }
allow(subject).to receive(:mergeable_discussions_state?) { false }
end
@@ -6016,12 +6036,10 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
allow(merge_request_diff).to receive(:patch_id_sha).and_return(nil)
allow(merge_request).to receive(:diff_refs).and_return(diff_refs)
- allow_next_instance_of(Repository) do |repo|
- allow(repo)
- .to receive(:get_patch_id)
- .with(diff_refs.base_sha, diff_refs.head_sha)
- .and_return(patch_id)
- end
+ allow(merge_request.project.repository)
+ .to receive(:get_patch_id)
+ .with(diff_refs.base_sha, diff_refs.head_sha)
+ .and_return(patch_id)
end
it { is_expected.to eq(patch_id) }
@@ -6065,4 +6083,54 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
expect(merge_request.all_mergeability_checks_results).to eq(result.payload[:results])
end
end
+
+ describe '#only_allow_merge_if_pipeline_succeeds?' do
+ let(:merge_request) { build_stubbed(:merge_request) }
+
+ subject(:result) { merge_request.only_allow_merge_if_pipeline_succeeds? }
+
+ before do
+ allow(merge_request.project)
+ .to receive(:only_allow_merge_if_pipeline_succeeds?)
+ .with(inherit_group_setting: true)
+ .and_return(only_allow_merge_if_pipeline_succeeds?)
+ end
+
+ context 'when associated project only_allow_merge_if_pipeline_succeeds? returns true' do
+ let(:only_allow_merge_if_pipeline_succeeds?) { true }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when associated project only_allow_merge_if_pipeline_succeeds? returns false' do
+ let(:only_allow_merge_if_pipeline_succeeds?) { false }
+
+ it { is_expected.to eq(false) }
+ end
+ end
+
+ describe '#only_allow_merge_if_all_discussions_are_resolved?' do
+ let(:merge_request) { build_stubbed(:merge_request) }
+
+ subject(:result) { merge_request.only_allow_merge_if_all_discussions_are_resolved? }
+
+ before do
+ allow(merge_request.project)
+ .to receive(:only_allow_merge_if_all_discussions_are_resolved?)
+ .with(inherit_group_setting: true)
+ .and_return(only_allow_merge_if_all_discussions_are_resolved?)
+ end
+
+ context 'when associated project only_allow_merge_if_all_discussions_are_resolved? returns true' do
+ let(:only_allow_merge_if_all_discussions_are_resolved?) { true }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when associated project only_allow_merge_if_all_discussions_are_resolved? returns false' do
+ let(:only_allow_merge_if_all_discussions_are_resolved?) { false }
+
+ it { is_expected.to eq(false) }
+ end
+ end
end
diff --git a/spec/models/ml/candidate_spec.rb b/spec/models/ml/candidate_spec.rb
index fa19b723ee2..d5b71e2c3f7 100644
--- a/spec/models/ml/candidate_spec.rb
+++ b/spec/models/ml/candidate_spec.rb
@@ -16,6 +16,7 @@ RSpec.describe Ml::Candidate, factory_default: :keep, feature_category: :mlops d
it { is_expected.to belong_to(:user) }
it { is_expected.to belong_to(:package) }
it { is_expected.to belong_to(:ci_build).class_name('Ci::Build') }
+ it { is_expected.to belong_to(:model_version).class_name('Ml::ModelVersion') }
it { is_expected.to have_many(:params) }
it { is_expected.to have_many(:metrics) }
it { is_expected.to have_many(:metadata) }
@@ -35,6 +36,45 @@ RSpec.describe Ml::Candidate, factory_default: :keep, feature_category: :mlops d
it { expect(described_class.new.eid).to be_present }
end
+ describe 'validation' do
+ let_it_be(:model) { create(:ml_models, project: candidate.project) }
+ let_it_be(:model_version1) { create(:ml_model_versions, model: model) }
+ let_it_be(:model_version2) { create(:ml_model_versions, model: model) }
+ let_it_be(:validation_candidate) do
+ create(:ml_candidates, model_version: model_version1, project: candidate.project)
+ end
+
+ let(:params) do
+ {
+ model_version: nil
+ }
+ end
+
+ subject(:errors) do
+ candidate = described_class.new(**params)
+ candidate.validate
+ candidate.errors
+ end
+
+ describe 'model_version' do
+ context 'when model_version is nil' do
+ it { expect(errors).not_to include(:model_version_id) }
+ end
+
+ context 'when no other candidate is associated to the model_version' do
+ let(:params) { { model_version: model_version2 } }
+
+ it { expect(errors).not_to include(:model_version_id) }
+ end
+
+ context 'when another candidate has model_version_id' do
+ let(:params) { { model_version: validation_candidate.model_version } }
+
+ it { expect(errors).to include(:model_version_id) }
+ end
+ end
+ end
+
describe '.destroy' do
let_it_be(:candidate_to_destroy) do
create(:ml_candidates, :with_metrics_and_params, :with_metadata, :with_artifact)
diff --git a/spec/models/ml/model_metadata_spec.rb b/spec/models/ml/model_metadata_spec.rb
new file mode 100644
index 00000000000..f06c7a2ce50
--- /dev/null
+++ b/spec/models/ml/model_metadata_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ml::ModelMetadata, feature_category: :mlops do
+ describe 'associations' do
+ it { is_expected.to belong_to(:model).required }
+ end
+
+ describe 'validations' do
+ let_it_be(:metadata) { create(:ml_model_metadata, name: 'some_metadata') }
+ let_it_be(:model) { metadata.model }
+
+ it 'is unique within the model' do
+ expect do
+ model.metadata.create!(name: 'some_metadata', value: 'blah')
+ end.to raise_error.with_message(/Name 'some_metadata' already taken/)
+ end
+
+ it 'a model is required' do
+ expect do
+ described_class.create!(name: 'some_metadata', value: 'blah')
+ end.to raise_error.with_message(/Model must exist/)
+ end
+ end
+end
diff --git a/spec/models/ml/model_spec.rb b/spec/models/ml/model_spec.rb
index e22989f3ce2..ae7c3f163f3 100644
--- a/spec/models/ml/model_spec.rb
+++ b/spec/models/ml/model_spec.rb
@@ -14,6 +14,7 @@ RSpec.describe Ml::Model, feature_category: :mlops do
it { is_expected.to belong_to(:project) }
it { is_expected.to have_one(:default_experiment) }
it { is_expected.to have_many(:versions) }
+ it { is_expected.to have_many(:metadata) }
it { is_expected.to have_one(:latest_version).class_name('Ml::ModelVersion').inverse_of(:model) }
end
@@ -77,45 +78,11 @@ RSpec.describe Ml::Model, feature_category: :mlops do
end
end
- describe '.find_or_create' do
- subject(:find_or_create) { described_class.find_or_create(project, name, experiment) }
+ describe '.including_project' do
+ subject { described_class.including_project }
- let(:name) { existing_model.name }
- let(:project) { existing_model.project }
- let(:experiment) { default_experiment }
-
- context 'when model name does not exist in the project' do
- let(:name) { 'new_model' }
- let(:experiment) { build(:ml_experiments, name: name, project: project) }
-
- it 'creates a model', :aggregate_failures do
- expect { find_or_create }.to change { Ml::Model.count }.by(1)
-
- expect(find_or_create.name).to eq(name)
- expect(find_or_create.project).to eq(project)
- expect(find_or_create.default_experiment).to eq(experiment)
- end
- end
-
- context 'when model name exists but project is different' do
- let(:project) { create(:project) }
- let(:experiment) { build(:ml_experiments, name: name, project: project) }
-
- it 'creates a model', :aggregate_failures do
- expect { find_or_create }.to change { Ml::Model.count }.by(1)
-
- expect(find_or_create.name).to eq(name)
- expect(find_or_create.project).to eq(project)
- expect(find_or_create.default_experiment).to eq(experiment)
- end
- end
-
- context 'when model exists' do
- it 'fetches existing model', :aggregate_failures do
- expect { find_or_create }.not_to change { Ml::Model.count }
-
- expect(find_or_create).to eq(existing_model)
- end
+ it 'loads latest version' do
+ expect(subject.first.association_cached?(:project)).to be(true)
end
end
diff --git a/spec/models/ml/model_version_spec.rb b/spec/models/ml/model_version_spec.rb
index 83639fca9e1..95d4a545f52 100644
--- a/spec/models/ml/model_version_spec.rb
+++ b/spec/models/ml/model_version_spec.rb
@@ -18,6 +18,7 @@ RSpec.describe Ml::ModelVersion, feature_category: :mlops do
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:model) }
it { is_expected.to belong_to(:package).class_name('Packages::MlModel::Package') }
+ it { is_expected.to have_one(:candidate).class_name('Ml::Candidate') }
end
describe 'validation' do
@@ -26,11 +27,15 @@ RSpec.describe Ml::ModelVersion, feature_category: :mlops do
build_stubbed(:ml_model_package, project: base_project, version: valid_version, name: model1.name)
end
+ let_it_be(:valid_description) { 'Valid description' }
+
let(:package) { valid_package }
let(:version) { valid_version }
+ let(:description) { valid_description }
subject(:errors) do
- mv = described_class.new(version: version, model: model1, package: package, project: model1.project)
+ mv = described_class.new(version: version, model: model1, package: package, project: model1.project,
+ description: description)
mv.validate
mv.errors
end
@@ -60,6 +65,14 @@ RSpec.describe Ml::ModelVersion, feature_category: :mlops do
end
end
+ describe 'description' do
+ context 'when description is too large' do
+ let(:description) { 'a' * 501 }
+
+ it { expect(errors).to include(:description) }
+ end
+ end
+
describe 'model' do
context 'when project is different' do
before do
@@ -91,8 +104,9 @@ RSpec.describe Ml::ModelVersion, feature_category: :mlops do
let(:version) { existing_model_version.version }
let(:package) { nil }
+ let(:description) { 'Some description' }
- subject(:find_or_create) { described_class.find_or_create!(model1, version, package) }
+ subject(:find_or_create) { described_class.find_or_create!(model1, version, package, description) }
context 'if model version exists' do
it 'returns the model version', :aggregate_failures do
@@ -111,11 +125,66 @@ RSpec.describe Ml::ModelVersion, feature_category: :mlops do
expect(model_version.version).to eq(version)
expect(model_version.model).to eq(model1)
+ expect(model_version.description).to eq(description)
expect(model_version.package).to eq(package)
end
end
end
+ describe '#by_project_id_and_id' do
+ let(:id) { model_version1.id }
+ let(:project_id) { model_version1.project.id }
+
+ subject { described_class.by_project_id_and_id(project_id, id) }
+
+ context 'if exists' do
+ it { is_expected.to eq(model_version1) }
+ end
+
+ context 'if id has no match' do
+ let(:id) { non_existing_record_id }
+
+ it { is_expected.to be(nil) }
+ end
+
+ context 'if project id does not match' do
+ let(:project_id) { non_existing_record_id }
+
+ it { is_expected.to be(nil) }
+ end
+ end
+
+ describe '.by_project_id_name_and_version' do
+ let(:version) { model_version1.version }
+ let(:project_id) { model_version1.project.id }
+ let(:model_name) { model_version1.model.name }
+ let_it_be(:latest_version) { create(:ml_model_versions, model: model_version1.model) }
+
+ subject { described_class.by_project_id_name_and_version(project_id, model_name, version) }
+
+ context 'if exists' do
+ it { is_expected.to eq(model_version1) }
+ end
+
+ context 'if id has no match' do
+ let(:version) { non_existing_record_id }
+
+ it { is_expected.to be(nil) }
+ end
+
+ context 'if project id does not match' do
+ let(:project_id) { non_existing_record_id }
+
+ it { is_expected.to be(nil) }
+ end
+
+ context 'if model name does not match' do
+ let(:model_name) { non_existing_record_id }
+
+ it { is_expected.to be(nil) }
+ end
+ end
+
describe '.order_by_model_id_id_desc' do
subject { described_class.order_by_model_id_id_desc }
diff --git a/spec/models/namespace_setting_spec.rb b/spec/models/namespace_setting_spec.rb
index e9822d97447..c7449e047b0 100644
--- a/spec/models/namespace_setting_spec.rb
+++ b/spec/models/namespace_setting_spec.rb
@@ -216,32 +216,54 @@ RSpec.describe NamespaceSetting, feature_category: :groups_and_projects, type: :
end
end
- describe '#emails_enabled?' do
- context 'when a group has no parent'
- let(:settings) { create(:namespace_settings, emails_enabled: true) }
- let(:grandparent) { create(:group) }
+ context 'when a group has parent groups' do
+ let(:grandparent) { create(:group, namespace_settings: settings) }
let(:parent) { create(:group, parent: grandparent) }
- let(:group) { create(:group, parent: parent, namespace_settings: settings) }
+ let!(:group) { create(:group, parent: parent) }
- context 'when the groups setting is changed' do
- it 'returns false when the attribute is false' do
- group.update_attribute(:emails_disabled, true)
+ context "when a parent group has disabled diff previews" do
+ let(:settings) { create(:namespace_settings, show_diff_preview_in_email: false) }
- expect(group.emails_enabled?).to be_falsey
+ it 'returns false' do
+ expect(group.show_diff_preview_in_email?).to be_falsey
end
end
- context 'when a group has a parent' do
- it 'returns true when no parent has disabled emails' do
- expect(group.emails_enabled?).to be_truthy
+ context 'when all parent groups have enabled diff previews' do
+ let(:settings) { create(:namespace_settings, show_diff_preview_in_email: true) }
+
+ it 'returns true' do
+ expect(group.show_diff_preview_in_email?).to be_truthy
end
+ end
+ end
+ end
- context 'when ancestor emails are disabled' do
- it 'returns false' do
- grandparent.update_attribute(:emails_disabled, true)
+ describe '#emails_enabled?' do
+ context 'when a group has no parent'
+ let(:settings) { create(:namespace_settings, emails_enabled: true) }
+ let(:grandparent) { create(:group) }
+ let(:parent) { create(:group, parent: grandparent) }
+ let(:group) { create(:group, parent: parent, namespace_settings: settings) }
- expect(group.emails_enabled?).to be_falsey
- end
+ context 'when the groups setting is changed' do
+ it 'returns false when the attribute is false' do
+ group.update_attribute(:emails_enabled, false)
+
+ expect(group.emails_enabled?).to be_falsey
+ end
+ end
+
+ context 'when a group has a parent' do
+ it 'returns true when no parent has disabled emails' do
+ expect(group.emails_enabled?).to be_truthy
+ end
+
+ context 'when ancestor emails are disabled' do
+ it 'returns false' do
+ grandparent.update_attribute(:emails_enabled, false)
+
+ expect(group.emails_enabled?).to be_falsey
end
end
end
@@ -251,19 +273,19 @@ RSpec.describe NamespaceSetting, feature_category: :groups_and_projects, type: :
let(:parent) { create(:group, parent: grandparent) }
let!(:group) { create(:group, parent: parent) }
- context "when a parent group has disabled diff previews" do
- let(:settings) { create(:namespace_settings, show_diff_preview_in_email: false) }
+ context "when a parent group has emails disabled" do
+ let(:settings) { create(:namespace_settings, emails_enabled: false) }
it 'returns false' do
- expect(group.show_diff_preview_in_email?).to be_falsey
+ expect(group.emails_enabled?).to be_falsey
end
end
- context 'when all parent groups have enabled diff previews' do
- let(:settings) { create(:namespace_settings, show_diff_preview_in_email: true) }
+ context 'when all parent groups have emails enabled' do
+ let(:settings) { create(:namespace_settings, emails_enabled: true) }
it 'returns true' do
- expect(group.show_diff_preview_in_email?).to be_truthy
+ expect(group.emails_enabled?).to be_truthy
end
end
end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 9974aac3c6c..85569a68252 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -569,6 +569,48 @@ RSpec.describe Namespace, feature_category: :groups_and_projects do
end
end
end
+
+ describe "#default_branch_protection_settings" do
+ let(:default_branch_protection_defaults) { {} }
+ let(:namespace_setting) { create(:namespace_settings, default_branch_protection_defaults: default_branch_protection_defaults) }
+ let(:namespace) { create(:namespace, namespace_settings: namespace_setting) }
+ let(:group) { create(:group, namespace_settings: namespace_setting) }
+
+ before do
+ stub_application_setting(default_branch_protection_defaults: Gitlab::Access::BranchProtection.protected_against_developer_pushes)
+ end
+
+ context 'for a namespace' do
+ it 'returns the instance level setting' do
+ expected_settings = Gitlab::Access::BranchProtection.protected_against_developer_pushes.deep_stringify_keys
+ settings = namespace.default_branch_protection_settings.to_hash
+
+ expect(settings).to eq(expected_settings)
+ end
+ end
+
+ context 'for a group' do
+ context 'that has not altered the default value' do
+ it 'returns the instance level setting' do
+ expected_settings = Gitlab::Access::BranchProtection.protected_against_developer_pushes.deep_stringify_keys
+ settings = group.default_branch_protection_settings.to_hash
+
+ expect(settings).to eq(expected_settings)
+ end
+ end
+
+ context 'that has altered the default value' do
+ let(:default_branch_protection_defaults) { Gitlab::Access::BranchProtection.protected_fully.deep_stringify_keys }
+
+ it 'returns the group level setting' do
+ expected_settings = default_branch_protection_defaults
+ settings = group.default_branch_protection_settings.to_hash
+
+ expect(settings).to eq(expected_settings)
+ end
+ end
+ end
+ end
end
describe "Respond to" do
@@ -1863,20 +1905,22 @@ RSpec.describe Namespace, feature_category: :groups_and_projects do
describe '#emails_disabled?' do
context 'when not a subgroup' do
+ let(:group) { create(:group) }
+
it 'returns false' do
- group = create(:group, emails_disabled: false)
+ group.update_attribute(:emails_enabled, true)
expect(group.emails_disabled?).to be_falsey
end
it 'returns true' do
- group = create(:group, emails_disabled: true)
+ group.update_attribute(:emails_enabled, false)
expect(group.emails_disabled?).to be_truthy
end
it 'does not query the db when there is no parent group' do
- group = create(:group, emails_disabled: true)
+ group.update_attribute(:emails_enabled, false)
expect { group.emails_disabled? }.not_to exceed_query_limit(0)
end
@@ -1903,7 +1947,8 @@ RSpec.describe Namespace, feature_category: :groups_and_projects do
describe '#emails_enabled?' do
context 'without a persisted namespace_setting object' do
- let(:group) { build(:group, emails_disabled: false) }
+ let(:group_settings) { create(:namespace_settings) }
+ let(:group) { build(:group, emails_disabled: false, namespace_settings: group_settings) }
it "is the opposite of emails_disabled" do
expect(group.emails_enabled?).to be_truthy
@@ -1928,7 +1973,7 @@ RSpec.describe Namespace, feature_category: :groups_and_projects do
parent_2 = create(:group) # No projects
create(:project, group: child_1_1).tap do |project|
- project.pages_metadatum.update!(deployed: true)
+ create(:pages_deployment, project: project)
end
create(:project, group: child_1_1)
diff --git a/spec/models/namespace_statistics_spec.rb b/spec/models/namespace_statistics_spec.rb
index ac747b70a9f..ee3b4765dba 100644
--- a/spec/models/namespace_statistics_spec.rb
+++ b/spec/models/namespace_statistics_spec.rb
@@ -171,7 +171,7 @@ RSpec.describe NamespaceStatistics do
context 'when other columns are updated' do
it 'does not enqueue the job to update root storage statistics' do
- columns_to_update = NamespaceStatistics.columns_hash.reject { |k, _| %w(id namespace_id).include?(k) || k.include?('_size') }.keys
+ columns_to_update = NamespaceStatistics.columns_hash.reject { |k, _| %w[id namespace_id].include?(k) || k.include?('_size') }.keys
columns_to_update.each { |c| statistics[c] = 10 }
expect(statistics).not_to receive(:update_root_storage_statistics)
diff --git a/spec/models/network/graph_spec.rb b/spec/models/network/graph_spec.rb
index d0c73d6285c..3bee7225df5 100644
--- a/spec/models/network/graph_spec.rb
+++ b/spec/models/network/graph_spec.rb
@@ -4,7 +4,6 @@ require 'spec_helper'
RSpec.describe Network::Graph, feature_category: :source_code_management do
let(:project) { create(:project, :repository) }
- let!(:note_on_commit) { create(:note_on_commit, project: project) }
describe '#initialize' do
let(:graph) do
@@ -14,16 +13,6 @@ RSpec.describe Network::Graph, feature_category: :source_code_management do
it 'has initialized' do
expect(graph).to be_a(described_class)
end
-
- context 'when disable_network_graph_note_counts is disabled' do
- before do
- stub_feature_flags(disable_network_graph_notes_count: false)
- end
-
- it 'initializes the notes hash' do
- expect(graph.notes).to eq({ note_on_commit.commit_id => 1 })
- end
- end
end
describe '#commits' do
diff --git a/spec/models/notification_recipient_spec.rb b/spec/models/notification_recipient_spec.rb
index 2275bea4c7f..f19c0a68f87 100644
--- a/spec/models/notification_recipient_spec.rb
+++ b/spec/models/notification_recipient_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe NotificationRecipient, feature_category: :team_planning do
context 'when emails are disabled' do
it 'returns false if group disabled' do
- expect(project.namespace).to receive(:emails_disabled?).and_return(true)
+ expect(project.namespace).to receive(:emails_enabled?).and_return(false)
expect(recipient).to receive(:emails_disabled?).and_call_original
expect(recipient.notifiable?).to eq false
end
@@ -28,7 +28,7 @@ RSpec.describe NotificationRecipient, feature_category: :team_planning do
context 'when emails are enabled' do
it 'returns true if group enabled' do
- expect(project.namespace).to receive(:emails_disabled?).and_return(false)
+ expect(project.namespace).to receive(:emails_enabled?).and_return(true)
expect(recipient).to receive(:emails_disabled?).and_call_original
expect(recipient.notifiable?).to eq true
end
diff --git a/spec/models/organizations/organization_spec.rb b/spec/models/organizations/organization_spec.rb
index 2f9f04fd3e6..0670002135c 100644
--- a/spec/models/organizations/organization_spec.rb
+++ b/spec/models/organizations/organization_spec.rb
@@ -181,4 +181,13 @@ RSpec.describe Organizations::Organization, type: :model, feature_category: :cel
it { is_expected.to eq false }
end
end
+
+ describe '#web_url' do
+ it 'returns web url from `Gitlab::UrlBuilder`' do
+ web_url = 'http://127.0.0.1:3000/-/organizations/default'
+
+ expect(Gitlab::UrlBuilder).to receive(:build).with(organization, only_path: nil).and_return(web_url)
+ expect(organization.web_url).to eq(web_url)
+ end
+ end
end
diff --git a/spec/models/packages/npm/metadata_cache_spec.rb b/spec/models/packages/npm/metadata_cache_spec.rb
index 94b41ab6a5e..3a6c87a4244 100644
--- a/spec/models/packages/npm/metadata_cache_spec.rb
+++ b/spec/models/packages/npm/metadata_cache_spec.rb
@@ -148,4 +148,40 @@ RSpec.describe Packages::Npm::MetadataCache, type: :model, feature_category: :pa
end
end
end
+
+ describe '.stale' do
+ let_it_be(:npm_metadata_cache) { create(:npm_metadata_cache) }
+ let_it_be(:npm_metadata_cache_stale) { create(:npm_metadata_cache, :stale) }
+
+ subject { described_class.stale }
+
+ it { is_expected.to contain_exactly(npm_metadata_cache_stale) }
+ end
+
+ describe '.pending_destruction' do
+ let_it_be(:npm_metadata_cache) { create(:npm_metadata_cache) }
+ let_it_be(:npm_metadata_cache_stale_default) { create(:npm_metadata_cache, :stale) }
+ let_it_be(:npm_metadata_cache_stale_processing) { create(:npm_metadata_cache, :stale, :processing) }
+
+ subject { described_class.pending_destruction }
+
+ it { is_expected.to contain_exactly(npm_metadata_cache_stale_default) }
+ end
+
+ describe '.next_pending_destruction' do
+ let_it_be(:npm_metadata_cache1) { create(:npm_metadata_cache, created_at: 1.month.ago, updated_at: 1.day.ago) }
+ let_it_be(:npm_metadata_cache2) { create(:npm_metadata_cache, created_at: 1.year.ago, updated_at: 1.year.ago) }
+
+ let_it_be(:npm_metadata_cache3) do
+ create(:npm_metadata_cache, :stale, created_at: 2.years.ago, updated_at: 1.month.ago)
+ end
+
+ let_it_be(:npm_metadata_cache4) do
+ create(:npm_metadata_cache, :stale, created_at: 3.years.ago, updated_at: 2.weeks.ago)
+ end
+
+ it 'returns the oldest pending destruction item based on updated_at' do
+ expect(described_class.next_pending_destruction(order_by: :updated_at)).to eq(npm_metadata_cache3)
+ end
+ end
end
diff --git a/spec/models/packages/nuget/symbol_spec.rb b/spec/models/packages/nuget/symbol_spec.rb
index 52e95c11939..f43f3a3bdeb 100644
--- a/spec/models/packages/nuget/symbol_spec.rb
+++ b/spec/models/packages/nuget/symbol_spec.rb
@@ -6,6 +6,7 @@ RSpec.describe Packages::Nuget::Symbol, type: :model, feature_category: :package
subject(:symbol) { create(:nuget_symbol) }
it { is_expected.to be_a FileStoreMounter }
+ it { is_expected.to be_a ShaAttribute }
describe 'relationships' do
it { is_expected.to belong_to(:package).inverse_of(:nuget_symbols) }
diff --git a/spec/models/packages/package_spec.rb b/spec/models/packages/package_spec.rb
index e113218e828..8e3b97e55f3 100644
--- a/spec/models/packages/package_spec.rb
+++ b/spec/models/packages/package_spec.rb
@@ -1196,7 +1196,7 @@ RSpec.describe Packages::Package, type: :model, feature_category: :package_regis
it { is_expected.to eq([]) }
context 'with tags' do
- let(:tags) { %w(tag1 tag2 tag3) }
+ let(:tags) { %w[tag1 tag2 tag3] }
before do
tags.each { |t| create(:packages_tag, name: t, package: package) }
diff --git a/spec/models/packages/protection/rule_spec.rb b/spec/models/packages/protection/rule_spec.rb
index 320c265239c..3f0aefa945a 100644
--- a/spec/models/packages/protection/rule_spec.rb
+++ b/spec/models/packages/protection/rule_spec.rb
@@ -34,6 +34,32 @@ RSpec.describe Packages::Protection::Rule, type: :model, feature_category: :pack
it { is_expected.to validate_presence_of(:package_name_pattern) }
it { is_expected.to validate_uniqueness_of(:package_name_pattern).scoped_to(:project_id, :package_type) }
it { is_expected.to validate_length_of(:package_name_pattern).is_at_most(255) }
+
+ [
+ '@my-scope/my-package',
+ '@my-scope/*my-package-with-wildcard-inbetween',
+ '@my-scope/*my-package-with-wildcard-start',
+ '@my-scope/my-*package-*with-wildcard-multiple-*',
+ '@my-scope/my-package-with_____underscore',
+ '@my-scope/my-package-with-regex-characters.+',
+ '@my-scope/my-package-with-wildcard-end*'
+ ].each do |package_name_pattern|
+ it { is_expected.to allow_value(package_name_pattern).for(:package_name_pattern) }
+ end
+
+ [
+ '@my-scope/my-package-with-percent-sign-%',
+ '*@my-scope/my-package-with-wildcard-start',
+ '@my-scope/my-package-with-backslash-\*'
+ ].each do |package_name_pattern|
+ it {
+ is_expected.not_to(
+ allow_value(package_name_pattern)
+ .for(:package_name_pattern)
+ .with_message(_('should be a valid NPM package name with optional wildcard characters.'))
+ )
+ }
+ end
end
describe '#package_type' do
@@ -51,14 +77,13 @@ RSpec.describe Packages::Protection::Rule, type: :model, feature_category: :pack
context 'with different package name patterns' do
where(:package_name_pattern, :expected_pattern_query) do
- '@my-scope/my-package' | '@my-scope/my-package'
- '*@my-scope/my-package-with-wildcard-start' | '%@my-scope/my-package-with-wildcard-start'
- '@my-scope/my-package-with-wildcard-end*' | '@my-scope/my-package-with-wildcard-end%'
- '@my-scope/*my-package-with-wildcard-inbetween' | '@my-scope/%my-package-with-wildcard-inbetween'
- '**@my-scope/**my-package-with-wildcard-multiple**' | '%%@my-scope/%%my-package-with-wildcard-multiple%%'
- '@my-scope/my-package-with_____underscore' | '@my-scope/my-package-with\_\_\_\_\_underscore'
- '@my-scope/my-package-with-percent-sign-%' | '@my-scope/my-package-with-percent-sign-\%'
- '@my-scope/my-package-with-regex-characters.+' | '@my-scope/my-package-with-regex-characters.+'
+ '@my-scope/my-package' | '@my-scope/my-package'
+ '@my-scope/*my-package-with-wildcard-start' | '@my-scope/%my-package-with-wildcard-start'
+ '@my-scope/my-package-with-wildcard-end*' | '@my-scope/my-package-with-wildcard-end%'
+ '@my-scope/my-package*with-wildcard-inbetween' | '@my-scope/my-package%with-wildcard-inbetween'
+ '@my-scope/**my-package-**-with-wildcard-multiple**' | '@my-scope/%%my-package-%%-with-wildcard-multiple%%'
+ '@my-scope/my-package-with_____underscore' | '@my-scope/my-package-with\_\_\_\_\_underscore'
+ '@my-scope/my-package-with-regex-characters.+' | '@my-scope/my-package-with-regex-characters.+'
end
with_them do
@@ -74,7 +99,7 @@ RSpec.describe Packages::Protection::Rule, type: :model, feature_category: :pack
end
let_it_be(:ppr_with_wildcard_start) do
- create(:package_protection_rule, package_name_pattern: '*@my-scope/my_package-with-wildcard-start')
+ create(:package_protection_rule, package_name_pattern: '@my-scope/*my_package-with-wildcard-start')
end
let_it_be(:ppr_with_wildcard_end) do
@@ -82,11 +107,11 @@ RSpec.describe Packages::Protection::Rule, type: :model, feature_category: :pack
end
let_it_be(:ppr_with_wildcard_inbetween) do
- create(:package_protection_rule, package_name_pattern: '@my-scope/*my_package-with-wildcard-inbetween')
+ create(:package_protection_rule, package_name_pattern: '@my-scope/my_package*with-wildcard-inbetween')
end
let_it_be(:ppr_with_wildcard_multiples) do
- create(:package_protection_rule, package_name_pattern: '**@my-scope/**my_package-with-wildcard-multiple**')
+ create(:package_protection_rule, package_name_pattern: '@my-scope/**my_package**with-wildcard-multiple**')
end
let_it_be(:ppr_with_underscore) do
@@ -103,46 +128,47 @@ RSpec.describe Packages::Protection::Rule, type: :model, feature_category: :pack
context 'with several package protection rule scenarios' do
where(:package_name, :expected_package_protection_rules) do
- '@my-scope/my_package' | [ref(:package_protection_rule)]
- '@my-scope/my2package' | []
- '@my-scope/my_package-2' | []
+ '@my-scope/my_package' | [ref(:package_protection_rule)]
+ '@my-scope/my2package' | []
+ '@my-scope/my_package-2' | []
# With wildcard pattern at the start
- '@my-scope/my_package-with-wildcard-start' | [ref(:ppr_with_wildcard_start)]
- '@my-scope/my_package-with-wildcard-start-any' | []
- 'prefix-@my-scope/my_package-with-wildcard-start' | [ref(:ppr_with_wildcard_start)]
- 'prefix-@my-scope/my_package-with-wildcard-start-any' | []
+ '@my-scope/my_package-with-wildcard-start' | [ref(:ppr_with_wildcard_start)]
+ '@my-scope/my_package-with-wildcard-start-any' | []
+ '@my-scope/any-my_package-with-wildcard-start' | [ref(:ppr_with_wildcard_start)]
+ '@my-scope/any-my_package-with-wildcard-start-any' | []
# With wildcard pattern at the end
- '@my-scope/my_package-with-wildcard-end' | [ref(:ppr_with_wildcard_end)]
- '@my-scope/my_package-with-wildcard-end:1234567890' | [ref(:ppr_with_wildcard_end)]
- 'prefix-@my-scope/my_package-with-wildcard-end' | []
- 'prefix-@my-scope/my_package-with-wildcard-end:1234567890' | []
+ '@my-scope/my_package-with-wildcard-end' | [ref(:ppr_with_wildcard_end)]
+ '@my-scope/my_package-with-wildcard-end:1234567890' | [ref(:ppr_with_wildcard_end)]
+ '@my-scope/any-my_package-with-wildcard-end' | []
+ '@my-scope/any-my_package-with-wildcard-end:1234567890' | []
# With wildcard pattern inbetween
- '@my-scope/my_package-with-wildcard-inbetween' | [ref(:ppr_with_wildcard_inbetween)]
- '@my-scope/any-my_package-with-wildcard-inbetween' | [ref(:ppr_with_wildcard_inbetween)]
- '@my-scope/any-my_package-my_package-wildcard-inbetween-any' | []
+ '@my-scope/my_packagewith-wildcard-inbetween' | [ref(:ppr_with_wildcard_inbetween)]
+ '@my-scope/my_package-any-with-wildcard-inbetween' | [ref(:ppr_with_wildcard_inbetween)]
+ '@my-scope/any-my_package-my_package-wildcard-inbetween-any' | []
# With multiple wildcard pattern are used
- '@my-scope/my_package-with-wildcard-multiple' | [ref(:ppr_with_wildcard_multiples)]
- 'prefix-@my-scope/any-my_package-with-wildcard-multiple-any' | [ref(:ppr_with_wildcard_multiples)]
- '****@my-scope/****my_package-with-wildcard-multiple****' | [ref(:ppr_with_wildcard_multiples)]
- 'prefix-@other-scope/any-my_package-with-wildcard-multiple-any' | []
+ '@my-scope/my_packagewith-wildcard-multiple' | [ref(:ppr_with_wildcard_multiples)]
+ '@my-scope/any-my_package-any-with-wildcard-multiple-any' | [ref(:ppr_with_wildcard_multiples)]
+ '@my-scope/****my_package****with-wildcard-multiple****' | [ref(:ppr_with_wildcard_multiples)]
+ '@other-scope/any-my_package-with-wildcard-multiple-any' | []
# With underscore
- '@my-scope/my_package-with_____underscore' | [ref(:ppr_with_underscore)]
- '@my-scope/my_package-with_any_underscore' | []
+ '@my-scope/my_package-with_____underscore' | [ref(:ppr_with_underscore)]
+ '@my-scope/my_package-with_any_underscore' | []
- '@my-scope/my_package-with-regex-characters.+' | [ref(:ppr_with_regex_characters)]
- '@my-scope/my_package-with-regex-characters.' | []
- '@my-scope/my_package-with-regex-characters' | []
- '@my-scope/my_package-with-regex-characters-any' | []
+ # With regex pattern
+ '@my-scope/my_package-with-regex-characters.+' | [ref(:ppr_with_regex_characters)]
+ '@my-scope/my_package-with-regex-characters.' | []
+ '@my-scope/my_package-with-regex-characters' | []
+ '@my-scope/my_package-with-regex-characters-any' | []
# Special cases
- nil | []
- '' | []
- 'any_package' | []
+ nil | []
+ '' | []
+ 'any_package' | []
end
with_them do
@@ -209,7 +235,7 @@ RSpec.describe Packages::Protection::Rule, type: :model, feature_category: :pack
)
end
- describe "with different users and protection levels" do
+ describe 'with different users and protection levels' do
# rubocop:disable Layout/LineLength
where(:project, :access_level, :package_name, :package_type, :push_protected) do
ref(:project_with_ppr) | Gitlab::Access::REPORTER | '@my-scope/my-package-stage-sha-1234' | :npm | true
diff --git a/spec/models/packages/pypi/metadatum_spec.rb b/spec/models/packages/pypi/metadatum_spec.rb
index 6c83c4ed143..6c93f84124f 100644
--- a/spec/models/packages/pypi/metadatum_spec.rb
+++ b/spec/models/packages/pypi/metadatum_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Packages::Pypi::Metadatum, type: :model do
+RSpec.describe Packages::Pypi::Metadatum, type: :model, feature_category: :package_registry do
describe 'relationships' do
it { is_expected.to belong_to(:package) }
end
@@ -9,8 +9,29 @@ RSpec.describe Packages::Pypi::Metadatum, type: :model do
describe 'validations' do
it { is_expected.to validate_presence_of(:package) }
it { is_expected.to allow_value('').for(:required_python) }
- it { is_expected.not_to allow_value(nil).for(:required_python) }
- it { is_expected.not_to allow_value('a' * 256).for(:required_python) }
+ it { is_expected.to validate_length_of(:required_python).is_at_most(described_class::MAX_REQUIRED_PYTHON_LENGTH) }
+ it { is_expected.to allow_value('').for(:keywords) }
+ it { is_expected.to allow_value(nil).for(:keywords) }
+ it { is_expected.to validate_length_of(:keywords).is_at_most(described_class::MAX_KEYWORDS_LENGTH) }
+ it { is_expected.to allow_value('').for(:metadata_version) }
+ it { is_expected.to allow_value(nil).for(:metadata_version) }
+ it { is_expected.to validate_length_of(:metadata_version).is_at_most(described_class::MAX_METADATA_VERSION_LENGTH) }
+ it { is_expected.to allow_value('').for(:author_email) }
+ it { is_expected.to allow_value(nil).for(:author_email) }
+ it { is_expected.to validate_length_of(:author_email).is_at_most(described_class::MAX_AUTHOR_EMAIL_LENGTH) }
+ it { is_expected.to allow_value('').for(:summary) }
+ it { is_expected.to allow_value(nil).for(:summary) }
+ it { is_expected.to validate_length_of(:summary).is_at_most(described_class::MAX_SUMMARY_LENGTH) }
+ it { is_expected.to allow_value('').for(:description) }
+ it { is_expected.to allow_value(nil).for(:description) }
+ it { is_expected.to validate_length_of(:description).is_at_most(described_class::MAX_DESCRIPTION_LENGTH) }
+ it { is_expected.to allow_value('').for(:description_content_type) }
+ it { is_expected.to allow_value(nil).for(:description_content_type) }
+
+ it {
+ is_expected.to validate_length_of(:description_content_type)
+ .is_at_most(described_class::MAX_DESCRIPTION_CONTENT_TYPE)
+ }
describe '#pypi_package_type' do
it 'will not allow a package with a different package_type' do
diff --git a/spec/models/packages/tag_spec.rb b/spec/models/packages/tag_spec.rb
index bc03c34f56b..6842d1946e5 100644
--- a/spec/models/packages/tag_spec.rb
+++ b/spec/models/packages/tag_spec.rb
@@ -5,6 +5,23 @@ RSpec.describe Packages::Tag, type: :model, feature_category: :package_registry
let!(:project) { create(:project) }
let!(:package) { create(:npm_package, version: '1.0.2', project: project, updated_at: 3.days.ago) }
+ describe '#ensure_project_id' do
+ it 'sets the project_id before saving' do
+ tag = build(:packages_tag)
+ expect(tag.project_id).to be_nil
+ tag.save!
+ expect(tag.project_id).not_to be_nil
+ expect(tag.project_id).to eq(tag.package.project_id)
+ end
+
+ it 'does not override the project_id if set' do
+ another_project = create(:project)
+ tag = build(:packages_tag, project_id: another_project.id)
+ tag.save!
+ expect(tag.project_id).to eq(another_project.id)
+ end
+ end
+
describe 'relationships' do
it { is_expected.to belong_to(:package).inverse_of(:tags) }
end
@@ -61,7 +78,7 @@ RSpec.describe Packages::Tag, type: :model, feature_category: :package_registry
end
context 'with multiple names' do
- let(:name) { %w(tag1 tag3) }
+ let(:name) { %w[tag1 tag3] }
it { is_expected.to contain_exactly(tag1, tag3) }
end
diff --git a/spec/models/pages/lookup_path_spec.rb b/spec/models/pages/lookup_path_spec.rb
index 08ba823f8fa..570c369016b 100644
--- a/spec/models/pages/lookup_path_spec.rb
+++ b/spec/models/pages/lookup_path_spec.rb
@@ -55,12 +55,7 @@ RSpec.describe Pages::LookupPath, feature_category: :pages do
end
context 'when there is pages deployment' do
- let(:deployment) { create(:pages_deployment, project: project) }
-
- before do
- project.mark_pages_as_deployed
- project.pages_metadatum.update!(pages_deployment: deployment)
- end
+ let!(:deployment) { create(:pages_deployment, project: project) }
it 'uses deployment from object storage' do
freeze_time do
@@ -75,12 +70,6 @@ RSpec.describe Pages::LookupPath, feature_category: :pages do
end
end
- it 'does not recreate source hash' do
- expect(deployment.file).to receive(:url_or_file_path).once
-
- 2.times { lookup_path.source }
- end
-
context 'when deployment is in the local storage' do
before do
deployment.file.migrate!(::ObjectStorage::Store::LOCAL)
@@ -159,12 +148,7 @@ RSpec.describe Pages::LookupPath, feature_category: :pages do
end
context 'when there is a deployment' do
- let(:deployment) { create(:pages_deployment, project: project, root_directory: 'foo') }
-
- before do
- project.mark_pages_as_deployed
- project.pages_metadatum.update!(pages_deployment: deployment)
- end
+ let!(:deployment) { create(:pages_deployment, project: project, root_directory: 'foo') }
it 'returns the deployment\'s root_directory' do
expect(lookup_path.root_directory).to eq('foo')
diff --git a/spec/models/pages_deployment_spec.rb b/spec/models/pages_deployment_spec.rb
index e74c7ee8612..1e6f8b33a86 100644
--- a/spec/models/pages_deployment_spec.rb
+++ b/spec/models/pages_deployment_spec.rb
@@ -71,54 +71,62 @@ RSpec.describe PagesDeployment, feature_category: :pages do
end
end
- describe '.deactivate_deployments_older_than', :freeze_time do
- let!(:other_project_deployment) do
- create(:pages_deployment)
- end
+ describe '.deactivate_all', :freeze_time do
+ let!(:deployment) { create(:pages_deployment, project: project, updated_at: 5.minutes.ago) }
+ let!(:nil_path_prefix_deployment) { create(:pages_deployment, project: project, path_prefix: nil) }
+ let!(:empty_path_prefix_deployment) { create(:pages_deployment, project: project, path_prefix: '') }
- let!(:other_path_prefix_deployment) do
- create(:pages_deployment, project: project, path_prefix: 'other')
- end
+ let!(:other_project_deployment) { create(:pages_deployment) }
+ let!(:deactivated_deployment) { create(:pages_deployment, project: project, deleted_at: 5.minutes.ago) }
- let!(:deactivated_deployment) do
- create(:pages_deployment, project: project, deleted_at: 5.minutes.ago)
+ it 'updates only older deployments for the same project and path prefix' do
+ expect { described_class.deactivate_all(project) }
+ .to change { deployment.reload.deleted_at }.from(nil).to(Time.zone.now)
+ .and change { deployment.reload.updated_at }.to(Time.zone.now)
+ .and change { nil_path_prefix_deployment.reload.deleted_at }.from(nil).to(Time.zone.now)
+ .and change { empty_path_prefix_deployment.reload.deleted_at }.from(nil).to(Time.zone.now)
+ .and not_change { other_project_deployment.reload.deleted_at }
+ .and not_change { deactivated_deployment.reload.deleted_at }
end
+ end
+
+ describe '.deactivate_deployments_older_than', :freeze_time do
+ let!(:nil_path_prefix_deployment) { create(:pages_deployment, project: project, path_prefix: nil) }
+ let!(:empty_path_prefix_deployment) { create(:pages_deployment, project: project, path_prefix: '') }
+ let!(:older_deployment) { create(:pages_deployment, project: project, updated_at: 5.minutes.ago) }
+ let!(:reference_deployment) { create(:pages_deployment, project: project, updated_at: 5.minutes.ago) }
+ let!(:newer_deployment) { create(:pages_deployment, project: project, updated_at: 5.minutes.ago) }
+
+ let!(:other_project_deployment) { create(:pages_deployment) }
+ let!(:other_path_prefix_deployment) { create(:pages_deployment, project: project, path_prefix: 'other') }
+ let!(:deactivated_deployment) { create(:pages_deployment, project: project, deleted_at: 5.minutes.ago) }
it 'updates only older deployments for the same project and path prefix' do
- deployment1 = create(:pages_deployment, project: project, updated_at: 5.minutes.ago)
- deployment2 = create(:pages_deployment, project: project, updated_at: 5.minutes.ago)
- deployment3 = create(:pages_deployment, project: project, updated_at: 5.minutes.ago)
-
- expect { described_class.deactivate_deployments_older_than(deployment2) }
- .to change { deployment1.reload.deleted_at }
- .from(nil).to(Time.zone.now)
- .and change { deployment1.reload.updated_at }
- .to(Time.zone.now)
-
- expect(deployment2.reload.deleted_at).to be_nil
- expect(deployment3.reload.deleted_at).to be_nil
- expect(other_project_deployment.deleted_at).to be_nil
- expect(other_path_prefix_deployment.reload.deleted_at).to be_nil
- expect(deactivated_deployment.reload.deleted_at).to eq(5.minutes.ago)
+ expect { described_class.deactivate_deployments_older_than(reference_deployment) }
+ .to change { older_deployment.reload.deleted_at }.from(nil).to(Time.zone.now)
+ .and change { older_deployment.reload.updated_at }.to(Time.zone.now)
+ .and change { nil_path_prefix_deployment.reload.deleted_at }.from(nil).to(Time.zone.now)
+ .and change { empty_path_prefix_deployment.reload.deleted_at }.from(nil).to(Time.zone.now)
+ .and not_change { reference_deployment.reload.deleted_at }
+ .and not_change { newer_deployment.reload.deleted_at }
+ .and not_change { other_project_deployment.reload.deleted_at }
+ .and not_change { other_path_prefix_deployment.reload.deleted_at }
+ .and not_change { deactivated_deployment.reload.deleted_at }
end
it 'updates only older deployments for the same project with the given time' do
- deployment1 = create(:pages_deployment, project: project, updated_at: 5.minutes.ago)
- deployment2 = create(:pages_deployment, project: project, updated_at: 5.minutes.ago)
- deployment3 = create(:pages_deployment, project: project, updated_at: 5.minutes.ago)
time = 30.minutes.from_now
- expect { described_class.deactivate_deployments_older_than(deployment2, time: time) }
- .to change { deployment1.reload.deleted_at }
- .from(nil).to(time)
- .and change { deployment1.reload.updated_at }
- .to(Time.zone.now)
-
- expect(deployment2.reload.deleted_at).to be_nil
- expect(deployment3.reload.deleted_at).to be_nil
- expect(other_project_deployment.deleted_at).to be_nil
- expect(other_path_prefix_deployment.reload.deleted_at).to be_nil
- expect(deactivated_deployment.reload.deleted_at).to eq(5.minutes.ago)
+ expect { described_class.deactivate_deployments_older_than(reference_deployment, time: time) }
+ .to change { older_deployment.reload.deleted_at }.from(nil).to(time)
+ .and change { older_deployment.reload.updated_at }.to(Time.zone.now)
+ .and change { nil_path_prefix_deployment.reload.deleted_at }.from(nil).to(time)
+ .and change { empty_path_prefix_deployment.reload.deleted_at }.from(nil).to(time)
+ .and not_change { reference_deployment.reload.deleted_at }
+ .and not_change { newer_deployment.reload.deleted_at }
+ .and not_change { other_project_deployment.reload.deleted_at }
+ .and not_change { other_path_prefix_deployment.reload.deleted_at }
+ .and not_change { deactivated_deployment.reload.deleted_at }
end
end
diff --git a/spec/models/pages_domain_spec.rb b/spec/models/pages_domain_spec.rb
index 5a4eca11f71..7aa5cf993dc 100644
--- a/spec/models/pages_domain_spec.rb
+++ b/spec/models/pages_domain_spec.rb
@@ -84,20 +84,20 @@ RSpec.describe PagesDomain, feature_category: :pages do
attributes = attributes_for(:pages_domain)
cert, key = attributes.fetch_values(:certificate, :key)
- true | nil | nil | false | %i(certificate key)
+ true | nil | nil | false | %i[certificate key]
true | nil | nil | true | []
- true | cert | nil | false | %i(key)
- true | cert | nil | true | %i(key)
- true | nil | key | false | %i(certificate key)
- true | nil | key | true | %i(key)
+ true | cert | nil | false | %i[key]
+ true | cert | nil | true | %i[key]
+ true | nil | key | false | %i[certificate key]
+ true | nil | key | true | %i[key]
true | cert | key | false | []
true | cert | key | true | []
false | nil | nil | false | []
false | nil | nil | true | []
- false | cert | nil | false | %i(key)
- false | cert | nil | true | %i(key)
- false | nil | key | false | %i(key)
- false | nil | key | true | %i(key)
+ false | cert | nil | false | %i[key]
+ false | cert | nil | true | %i[key]
+ false | nil | key | false | %i[key]
+ false | nil | key | true | %i[key]
false | cert | key | false | []
false | cert | key | true | []
end
@@ -288,8 +288,8 @@ RSpec.describe PagesDomain, feature_category: :pages do
end
end
- describe '#has_intermediates?' do
- subject { domain.has_intermediates? }
+ describe '#has_valid_intermediates?' do
+ subject { domain.has_valid_intermediates? }
context 'for self signed' do
let(:domain) { build(:pages_domain) }
@@ -312,6 +312,14 @@ RSpec.describe PagesDomain, feature_category: :pages do
it { is_expected.to be_truthy }
end
+
+ context 'for chain with unknown root CA' do
+ # In cases where users use an origin certificate the CA does not necessarily need to be in
+ # the trust store, eg. in the case of Cloudflare Origin Certs.
+ let(:domain) { build(:pages_domain, :with_untrusted_root_ca_in_chain) }
+
+ it { is_expected.to be_truthy }
+ end
end
describe '#expired?' do
diff --git a/spec/models/personal_access_token_spec.rb b/spec/models/personal_access_token_spec.rb
index 7437e9b463e..7665f4dbde4 100644
--- a/spec/models/personal_access_token_spec.rb
+++ b/spec/models/personal_access_token_spec.rb
@@ -365,7 +365,7 @@ RSpec.describe PersonalAccessToken, feature_category: :system_access do
describe '.simple_sorts' do
it 'includes overridden keys' do
- expect(described_class.simple_sorts.keys).to include(*%w(expires_at_asc_id_desc))
+ expect(described_class.simple_sorts.keys).to include(*%w[expires_at_asc_id_desc])
end
end
diff --git a/spec/models/project_feature_spec.rb b/spec/models/project_feature_spec.rb
index 39e77df1900..c0a78ff2f53 100644
--- a/spec/models/project_feature_spec.rb
+++ b/spec/models/project_feature_spec.rb
@@ -53,7 +53,7 @@ RSpec.describe ProjectFeature, feature_category: :groups_and_projects do
end
it "does not allow repository related features have higher level" do
- features = %w(builds merge_requests)
+ features = %w[builds merge_requests]
project_feature = project.project_feature
features.each do |feature|
@@ -64,7 +64,7 @@ RSpec.describe ProjectFeature, feature_category: :groups_and_projects do
end
end
- it_behaves_like 'access level validation', ProjectFeature::FEATURES - %i(pages package_registry) do
+ it_behaves_like 'access level validation', ProjectFeature::FEATURES - %i[pages package_registry] do
let(:container_features) { project.project_feature }
end
diff --git a/spec/models/project_feature_usage_spec.rb b/spec/models/project_feature_usage_spec.rb
deleted file mode 100644
index 3765a2b37a7..00000000000
--- a/spec/models/project_feature_usage_spec.rb
+++ /dev/null
@@ -1,173 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe ProjectFeatureUsage, type: :model do
- describe '.jira_dvcs_integrations_enabled_count' do
- it 'returns count of projects with Jira DVCS Cloud enabled' do
- create(:project).feature_usage.log_jira_dvcs_integration_usage
- create(:project).feature_usage.log_jira_dvcs_integration_usage
-
- expect(described_class.with_jira_dvcs_integration_enabled.count).to eq(2)
- end
-
- it 'returns count of projects with Jira DVCS Server enabled' do
- create(:project).feature_usage.log_jira_dvcs_integration_usage(cloud: false)
- create(:project).feature_usage.log_jira_dvcs_integration_usage(cloud: false)
-
- expect(described_class.with_jira_dvcs_integration_enabled(cloud: false).count).to eq(2)
- end
- end
-
- describe '#log_jira_dvcs_integration_usage' do
- let(:project) { create(:project) }
-
- subject { project.feature_usage }
-
- context 'when the feature usage has not been created yet' do
- it 'logs Jira DVCS Cloud last sync' do
- freeze_time do
- subject.log_jira_dvcs_integration_usage
-
- expect(subject.jira_dvcs_server_last_sync_at).to be_nil
- expect(subject.jira_dvcs_cloud_last_sync_at).to be_like_time(Time.current)
- end
- end
-
- it 'logs Jira DVCS Server last sync' do
- freeze_time do
- subject.log_jira_dvcs_integration_usage(cloud: false)
-
- expect(subject.jira_dvcs_server_last_sync_at).to be_like_time(Time.current)
- expect(subject.jira_dvcs_cloud_last_sync_at).to be_nil
- end
- end
- end
-
- context 'when the feature usage already exists' do
- let(:today) { Time.current.beginning_of_day }
- let(:project) { create(:project) }
-
- subject { project.feature_usage }
-
- where(:cloud, :timestamp_field) do
- [
- [true, :jira_dvcs_cloud_last_sync_at],
- [false, :jira_dvcs_server_last_sync_at]
- ]
- end
-
- with_them do
- context 'when Jira DVCS Cloud last sync has not been logged' do
- before do
- travel_to today - 3.days do
- subject.log_jira_dvcs_integration_usage(cloud: !cloud)
- end
- end
-
- it 'logs Jira DVCS Cloud last sync' do
- freeze_time do
- subject.log_jira_dvcs_integration_usage(cloud: cloud)
-
- expect(subject.reload.send(timestamp_field)).to be_like_time(Time.current)
- end
- end
- end
-
- context 'when Jira DVCS Cloud last sync was logged today' do
- let(:last_updated) { today + 1.hour }
-
- before do
- travel_to last_updated do
- subject.log_jira_dvcs_integration_usage(cloud: cloud)
- end
- end
-
- it 'does not log Jira DVCS Cloud last sync' do
- travel_to today + 2.hours do
- subject.log_jira_dvcs_integration_usage(cloud: cloud)
-
- expect(subject.reload.send(timestamp_field)).to be_like_time(last_updated)
- end
- end
- end
-
- context 'when Jira DVCS Cloud last sync was logged yesterday' do
- let(:last_updated) { today - 2.days }
-
- before do
- travel_to last_updated do
- subject.log_jira_dvcs_integration_usage(cloud: cloud)
- end
- end
-
- it 'logs Jira DVCS Cloud last sync' do
- travel_to today + 1.hour do
- subject.log_jira_dvcs_integration_usage(cloud: cloud)
-
- expect(subject.reload.send(timestamp_field)).to be_like_time(today + 1.hour)
- end
- end
- end
- end
- end
-
- context 'when log_jira_dvcs_integration_usage is called simultaneously for the same project' do
- it 'logs the latest call' do
- feature_usage = project.feature_usage
- feature_usage.log_jira_dvcs_integration_usage
- first_logged_at = feature_usage.jira_dvcs_cloud_last_sync_at
-
- travel_to(1.hour.from_now) do
- ProjectFeatureUsage.new(project_id: project.id).log_jira_dvcs_integration_usage
- end
-
- expect(feature_usage.reload.jira_dvcs_cloud_last_sync_at).to be > first_logged_at
- end
- end
- end
-
- context 'ProjectFeatureUsage with DB Load Balancing', :request_store do
- describe '#log_jira_dvcs_integration_usage' do
- let!(:project) { create(:project) }
-
- subject { project.feature_usage }
-
- context 'database load balancing is configured' do
- before do
- ::Gitlab::Database::LoadBalancing::Session.clear_session
- end
-
- it 'logs Jira DVCS Cloud last sync' do
- freeze_time do
- subject.log_jira_dvcs_integration_usage
-
- expect(subject.jira_dvcs_server_last_sync_at).to be_nil
- expect(subject.jira_dvcs_cloud_last_sync_at).to be_like_time(Time.current)
- end
- end
-
- it 'does not stick to primary' do
- expect(::Gitlab::Database::LoadBalancing::Session.current).not_to be_performed_write
- expect(::Gitlab::Database::LoadBalancing::Session.current).not_to be_using_primary
-
- subject.log_jira_dvcs_integration_usage
-
- expect(::Gitlab::Database::LoadBalancing::Session.current).to be_performed_write
- expect(::Gitlab::Database::LoadBalancing::Session.current).not_to be_using_primary
- end
- end
-
- context 'database load balancing is not cofigured' do
- it 'logs Jira DVCS Cloud last sync' do
- freeze_time do
- subject.log_jira_dvcs_integration_usage
-
- expect(subject.jira_dvcs_server_last_sync_at).to be_nil
- expect(subject.jira_dvcs_cloud_last_sync_at).to be_like_time(Time.current)
- end
- end
- end
- end
- end
-end
diff --git a/spec/models/project_label_spec.rb b/spec/models/project_label_spec.rb
index 62839f5fb4f..01df58ee615 100644
--- a/spec/models/project_label_spec.rb
+++ b/spec/models/project_label_spec.rb
@@ -89,7 +89,7 @@ RSpec.describe ProjectLabel do
end
it 'uses id when name contains double quote' do
- label = create(:label, name: %q{"irony"})
+ label = create(:label, name: %q("irony"))
expect(label.to_reference(format: :name)).to eq "~#{label.id}"
end
end
diff --git a/spec/models/project_setting_spec.rb b/spec/models/project_setting_spec.rb
index 719e51018ac..8ad232b7e0c 100644
--- a/spec/models/project_setting_spec.rb
+++ b/spec/models/project_setting_spec.rb
@@ -214,7 +214,7 @@ RSpec.describe ProjectSetting, type: :model, feature_category: :groups_and_proje
context 'when emails are enabled in parent group' do
before do
- allow(project.namespace).to receive(:emails_disabled?).and_return(false)
+ allow(project.namespace).to receive(:emails_enabled?).and_return(true)
end
it 'returns true' do
diff --git a/spec/models/project_snippet_spec.rb b/spec/models/project_snippet_spec.rb
index 3d1c87771f3..5ce9499c420 100644
--- a/spec/models/project_snippet_spec.rb
+++ b/spec/models/project_snippet_spec.rb
@@ -2,11 +2,24 @@
require 'spec_helper'
-RSpec.describe ProjectSnippet do
+RSpec.describe ProjectSnippet, feature_category: :source_code_management do
describe "Associations" do
it { is_expected.to belong_to(:project) }
end
+ describe 'scopes' do
+ describe '.by_project' do
+ subject { described_class.by_project(project) }
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:snippet1) { create(:project_snippet, project: project) }
+ let_it_be(:snippet2) { create(:project_snippet, project: build(:project)) }
+ let_it_be(:snippet3) { create(:personal_snippet) }
+
+ it { is_expected.to contain_exactly(snippet1) }
+ end
+ end
+
describe "Validation" do
it { is_expected.to validate_presence_of(:project) }
it { is_expected.to validate_inclusion_of(:secret).in_array([false]) }
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index c27ed2cc82c..3ea5f6ea0ae 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -1136,24 +1136,24 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
it { is_expected.to delegate_method(:npm_package_requests_forwarding).to(:namespace) }
describe 'read project settings' do
- %i(
+ %i[
show_default_award_emojis
show_default_award_emojis?
warn_about_potentially_unwanted_characters
warn_about_potentially_unwanted_characters?
enforce_auth_checks_on_uploads
enforce_auth_checks_on_uploads?
- ).each do |method|
+ ].each do |method|
it { is_expected.to delegate_method(method).to(:project_setting).allow_nil }
end
end
describe 'write project settings' do
- %i(
+ %i[
show_default_award_emojis=
warn_about_potentially_unwanted_characters=
enforce_auth_checks_on_uploads=
- ).each do |method|
+ ].each do |method|
it { is_expected.to delegate_method(method).to(:project_setting).with_arguments(:args).allow_nil }
end
end
@@ -1177,12 +1177,13 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
let(:exclude_attributes) do
# Skip attributes defined in EE code
- %w(
+ %w[
merge_pipelines_enabled
merge_trains_enabled
auto_rollback_enabled
merge_trains_skip_train_allowed
- )
+ restrict_pipeline_cancellation_role
+ ]
end
end
@@ -2127,28 +2128,6 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
end
end
- describe 'sorting by name' do
- let_it_be(:project1) { create(:project, name: 'A') }
- let_it_be(:project2) { create(:project, name: 'Z') }
- let_it_be(:project3) { create(:project, name: 'L') }
-
- context 'when using .sort_by_name_desc' do
- it 'reorders the projects by descending name order' do
- projects = described_class.sorted_by_name_desc
-
- expect(projects.pluck(:name)).to eq(%w[Z L A])
- end
- end
-
- context 'when using .sort_by_name_asc' do
- it 'reorders the projects by ascending name order' do
- projects = described_class.sorted_by_name_asc
-
- expect(projects.pluck(:name)).to eq(%w[A L Z])
- end
- end
- end
-
describe '.with_shared_runners_enabled' do
subject { described_class.with_shared_runners_enabled }
@@ -2841,7 +2820,7 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
context "will return false if pages is deployed even if onboarding_complete is false" do
before do
project.pages_metadatum.update_column(:onboarding_complete, false)
- project.pages_metadatum.update_column(:deployed, true)
+ create(:pages_deployment, project: project)
end
it { is_expected.to be_falsey }
@@ -2855,7 +2834,7 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
context 'if pages are deployed' do
before do
- project.pages_metadatum.update_column(:deployed, true)
+ create(:pages_deployment, project: project)
end
it { is_expected.to be_truthy }
@@ -4309,7 +4288,7 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
end
context 'when project has a deployment platform' do
- let(:platform_variables) { %w(platform variables) }
+ let(:platform_variables) { %w[platform variables] }
let(:deployment_platform) { double }
before do
@@ -7142,7 +7121,7 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
project.check_personal_projects_limit
expect(project.errors[:limit_reached].first)
- .to match(/Personal project creation is not allowed/)
+ .to eq('You cannot create projects in your personal namespace. Contact your GitLab administrator.')
end
end
@@ -7155,7 +7134,7 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
project.check_personal_projects_limit
expect(project.errors[:limit_reached].first)
- .to match(/Your project limit is 5 projects/)
+ .to eq("You've reached your limit of 5 projects created. Contact your GitLab administrator.")
end
end
end
@@ -7219,126 +7198,6 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
end
end
- describe '#mark_pages_as_deployed' do
- let(:project) { create(:project) }
-
- it "works when artifacts_archive is missing" do
- project.mark_pages_as_deployed
-
- expect(project.pages_metadatum.reload.deployed).to eq(true)
- end
-
- it "creates new record and sets deployed to true if none exists yet" do
- project.pages_metadatum.destroy!
- project.reload
-
- project.mark_pages_as_deployed
-
- expect(project.pages_metadatum.reload.deployed).to eq(true)
- end
-
- it "updates the existing record and sets deployed to true and records artifact archive" do
- pages_metadatum = project.pages_metadatum
- pages_metadatum.update!(deployed: false)
-
- expect do
- project.mark_pages_as_deployed
- end.to change { pages_metadatum.reload.deployed }.from(false).to(true)
- end
- end
-
- describe '#mark_pages_as_not_deployed' do
- let(:project) { create(:project) }
-
- it "creates new record and sets deployed to false if none exists yet" do
- project.pages_metadatum.destroy!
- project.reload
-
- project.mark_pages_as_not_deployed
-
- expect(project.pages_metadatum.reload.deployed).to eq(false)
- end
-
- it "updates the existing record and sets deployed to false and clears artifacts_archive" do
- pages_metadatum = project.pages_metadatum
- pages_metadatum.update!(deployed: true)
-
- expect do
- project.mark_pages_as_not_deployed
- end.to change { pages_metadatum.reload.deployed }.from(true).to(false)
- end
- end
-
- describe '#update_pages_deployment!' do
- let(:project) { create(:project) }
- let(:deployment) { create(:pages_deployment, project: project) }
-
- it "creates new metadata record if none exists yet and sets deployment" do
- project.pages_metadatum.destroy!
- project.reload
-
- project.update_pages_deployment!(deployment)
-
- expect(project.pages_metadatum.pages_deployment).to eq(deployment)
- end
-
- it "updates the existing metadara record with deployment" do
- expect do
- project.update_pages_deployment!(deployment)
- end.to change { project.pages_metadatum.reload.pages_deployment }.from(nil).to(deployment)
- end
- end
-
- describe '#set_first_pages_deployment!' do
- let(:project) { create(:project) }
- let(:deployment) { create(:pages_deployment, project: project) }
-
- it "creates new metadata record if none exists yet and sets deployment" do
- project.pages_metadatum.destroy!
- project.reload
-
- project.set_first_pages_deployment!(deployment)
-
- expect(project.pages_metadatum.reload.pages_deployment).to eq(deployment)
- expect(project.pages_metadatum.reload.deployed).to eq(true)
- end
-
- it "updates the existing metadara record with deployment" do
- expect do
- project.set_first_pages_deployment!(deployment)
- end.to change { project.pages_metadatum.reload.pages_deployment }.from(nil).to(deployment)
-
- expect(project.pages_metadatum.reload.deployed).to eq(true)
- end
-
- it 'only updates metadata for this project' do
- other_project = create(:project)
-
- expect do
- project.set_first_pages_deployment!(deployment)
- end.not_to change { other_project.pages_metadatum.reload.pages_deployment }.from(nil)
-
- expect(other_project.pages_metadatum.reload.deployed).to eq(false)
- end
-
- it 'does nothing if metadata already references some deployment' do
- existing_deployment = create(:pages_deployment, project: project)
- project.set_first_pages_deployment!(existing_deployment)
-
- expect do
- project.set_first_pages_deployment!(deployment)
- end.not_to change { project.pages_metadatum.reload.pages_deployment }.from(existing_deployment)
- end
-
- it 'marks project as not deployed if deployment is nil' do
- project.mark_pages_as_deployed
-
- expect do
- project.set_first_pages_deployment!(nil)
- end.to change { project.pages_metadatum.reload.deployed }.from(true).to(false)
- end
- end
-
describe '#has_pool_repository?' do
it 'returns false when it does not have a pool repository' do
subject = create(:project, :repository)
@@ -7402,7 +7261,7 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
it 'returns only projects that have pages deployed' do
_project_without_pages = create(:project)
project_with_pages = create(:project)
- project_with_pages.mark_pages_as_deployed
+ create(:pages_deployment, project: project_with_pages)
expect(described_class.with_pages_deployed).to contain_exactly(project_with_pages)
end
@@ -9141,6 +9000,66 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
end
end
+ # TODO: Remove/update this spec after background syncing is implemented. See https://gitlab.com/gitlab-org/gitlab/-/issues/429376.
+ describe '#update_catalog_resource' do
+ let_it_be_with_reload(:project) { create(:project, name: 'My project name', description: 'My description') }
+ let_it_be_with_reload(:resource) { create(:ci_catalog_resource, project: project) }
+
+ shared_examples 'name, description, and visibility_level of the catalog resource match the project' do
+ it do
+ expect(project).to receive(:update_catalog_resource).once.and_call_original
+
+ project.save!
+
+ expect(resource.name).to eq(project.name)
+ expect(resource.description).to eq(project.description)
+ expect(resource.visibility_level).to eq(project.visibility_level)
+ end
+ end
+
+ context 'when the project name is updated' do
+ before do
+ project.name = 'My new project name'
+ end
+
+ it_behaves_like 'name, description, and visibility_level of the catalog resource match the project'
+ end
+
+ context 'when the project description is updated' do
+ before do
+ project.description = 'My new description'
+ end
+
+ it_behaves_like 'name, description, and visibility_level of the catalog resource match the project'
+ end
+
+ context 'when the project visibility_level is updated' do
+ before do
+ project.visibility_level = 10
+ end
+
+ it_behaves_like 'name, description, and visibility_level of the catalog resource match the project'
+ end
+
+ context 'when neither the project name, description, nor visibility_level are updated' do
+ it 'does not call update_catalog_resource' do
+ expect(project).not_to receive(:update_catalog_resource)
+
+ project.update!(path: 'path')
+ end
+ end
+
+ context 'when the project does not have a catalog resource' do
+ let_it_be(:project2) { create(:project) }
+
+ it 'does not call update_catalog_resource' do
+ expect(project2).not_to receive(:update_catalog_resource)
+
+ project.update!(name: 'name')
+ end
+ end
+ end
+
private
def finish_job(export_job)
diff --git a/spec/models/projects/repository_storage_move_spec.rb b/spec/models/projects/repository_storage_move_spec.rb
index ab0ad81f77a..c5fbc92176f 100644
--- a/spec/models/projects/repository_storage_move_spec.rb
+++ b/spec/models/projects/repository_storage_move_spec.rb
@@ -3,33 +3,11 @@
require 'spec_helper'
RSpec.describe Projects::RepositoryStorageMove, type: :model do
- let_it_be_with_refind(:project) { create(:project) }
-
it_behaves_like 'handles repository moves' do
- let(:container) { project }
+ let_it_be_with_refind(:container) { create(:project) }
+
let(:repository_storage_factory_key) { :project_repository_storage_move }
let(:error_key) { :project }
let(:repository_storage_worker) { Projects::UpdateRepositoryStorageWorker }
end
-
- describe 'state transitions' do
- let(:storage) { 'test_second_storage' }
-
- before do
- stub_storage_settings(storage => { 'path' => 'tmp/tests/extra_storage' })
- end
-
- context 'when started' do
- subject(:storage_move) { create(:project_repository_storage_move, :started, container: project, destination_storage_name: storage) }
-
- context 'and transits to replicated' do
- it 'sets the repository storage and marks the container as writable' do
- storage_move.finish_replication!
-
- expect(project.repository_storage).to eq(storage)
- expect(project).not_to be_repository_read_only
- end
- end
- end
- end
end
diff --git a/spec/models/projects/topic_spec.rb b/spec/models/projects/topic_spec.rb
index 568a4166de7..b3a55ccd370 100644
--- a/spec/models/projects/topic_spec.rb
+++ b/spec/models/projects/topic_spec.rb
@@ -76,7 +76,7 @@ RSpec.describe Projects::Topic do
describe '#find_by_name_case_insensitive' do
it 'returns topic with case insensitive name' do
- %w(topic TOPIC Topic).each do |name|
+ %w[topic TOPIC Topic].each do |name|
expect(described_class.find_by_name_case_insensitive(name)).to eq(topic)
end
end
diff --git a/spec/models/prometheus_metric_spec.rb b/spec/models/prometheus_metric_spec.rb
index a20f4edcf4a..c8a95aef8a6 100644
--- a/spec/models/prometheus_metric_spec.rb
+++ b/spec/models/prometheus_metric_spec.rb
@@ -94,16 +94,16 @@ RSpec.describe PrometheusMetric do
describe '#required_metrics' do
where(:group, :required_metrics) do
- :nginx_ingress_vts | %w(nginx_upstream_responses_total nginx_upstream_response_msecs_avg)
- :nginx_ingress | %w(nginx_ingress_controller_requests nginx_ingress_controller_ingress_upstream_latency_seconds_sum)
- :ha_proxy | %w(haproxy_frontend_http_requests_total haproxy_frontend_http_responses_total)
- :aws_elb | %w(aws_elb_request_count_sum aws_elb_latency_average aws_elb_httpcode_backend_5_xx_sum)
- :nginx | %w(nginx_server_requests nginx_server_requestMsec)
- :kubernetes | %w(container_memory_usage_bytes container_cpu_usage_seconds_total)
- :business | %w()
- :response | %w()
- :system | %w()
- :cluster_health | %w(container_memory_usage_bytes container_cpu_usage_seconds_total)
+ :nginx_ingress_vts | %w[nginx_upstream_responses_total nginx_upstream_response_msecs_avg]
+ :nginx_ingress | %w[nginx_ingress_controller_requests nginx_ingress_controller_ingress_upstream_latency_seconds_sum]
+ :ha_proxy | %w[haproxy_frontend_http_requests_total haproxy_frontend_http_responses_total]
+ :aws_elb | %w[aws_elb_request_count_sum aws_elb_latency_average aws_elb_httpcode_backend_5_xx_sum]
+ :nginx | %w[nginx_server_requests nginx_server_requestMsec]
+ :kubernetes | %w[container_memory_usage_bytes container_cpu_usage_seconds_total]
+ :business | %w[]
+ :response | %w[]
+ :system | %w[]
+ :cluster_health | %w[container_memory_usage_bytes container_cpu_usage_seconds_total]
end
with_them do
diff --git a/spec/models/releases/link_spec.rb b/spec/models/releases/link_spec.rb
index c4c9fba32d9..5d264af695b 100644
--- a/spec/models/releases/link_spec.rb
+++ b/spec/models/releases/link_spec.rb
@@ -82,7 +82,7 @@ RSpec.describe Releases::Link do
describe 'supported protocols' do
where(:protocol) do
- %w(http https ftp)
+ %w[http https ftp]
end
with_them do
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 2265d1b39af..606c4ea05b9 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -743,7 +743,7 @@ RSpec.describe Repository, feature_category: :source_code_management do
describe "#merged_branch_names", :clean_gitlab_redis_cache do
subject { repository.merged_branch_names(branch_names) }
- let(:branch_names) { %w(test beep boop definitely_merged) }
+ let(:branch_names) { %w[test beep boop definitely_merged] }
let(:already_merged) { Set.new(["definitely_merged"]) }
let(:write_hash) do
@@ -1621,16 +1621,16 @@ RSpec.describe Repository, feature_category: :source_code_management do
where(:branch_names, :tag_names, :result) do
nil | nil | false
- %w() | %w() | false
- %w(a b) | %w() | false
- %w() | %w(c d) | false
- %w(a b) | %w(c d) | false
- %w(a/b) | %w(c/d) | false
- %w(a b) | %w(c d a/z) | true
- %w(a b c/z) | %w(c d) | true
- %w(a/b/z) | %w(a/b) | false # we only consider refs ambiguous before the first slash
- %w(a/b/z) | %w(a/b a) | true
- %w(ab) | %w(abc/d a b) | false
+ %w[] | %w[] | false
+ %w[a b] | %w[] | false
+ %w[] | %w[c d] | false
+ %w[a b] | %w[c d] | false
+ %w[a/b] | %w[c/d] | false
+ %w[a b] | %w[c d a/z] | true
+ %w[a b c/z] | %w[c d] | true
+ %w[a/b/z] | %w[a/b] | false # we only consider refs ambiguous before the first slash
+ %w[a/b/z] | %w[a/b a] | true
+ %w[ab] | %w[abc/d a b] | false
end
with_them do
@@ -2596,7 +2596,7 @@ RSpec.describe Repository, feature_category: :source_code_management do
describe '#expire_branches_cache' do
it 'expires the cache' do
expect(repository).to receive(:expire_method_caches)
- .with(%i(branch_names merged_branch_names branch_count has_visible_content? has_ambiguous_refs?))
+ .with(%i[branch_names merged_branch_names branch_count has_visible_content? has_ambiguous_refs?])
.and_call_original
expect_next_instance_of(ProtectedBranches::CacheService) do |cache_service|
@@ -2630,7 +2630,7 @@ RSpec.describe Repository, feature_category: :source_code_management do
describe '#expire_tags_cache' do
it 'expires the cache' do
expect(repository).to receive(:expire_method_caches)
- .with(%i(tag_names tag_count has_ambiguous_refs?))
+ .with(%i[tag_names tag_count has_ambiguous_refs?])
.and_call_original
repository.expire_tags_cache
@@ -2889,7 +2889,7 @@ RSpec.describe Repository, feature_category: :source_code_management do
describe '#expire_statistics_caches' do
it 'expires the caches' do
expect(repository).to receive(:expire_method_caches)
- .with(%i(size recent_objects_size commit_count))
+ .with(%i[size recent_objects_size commit_count])
repository.expire_statistics_caches
end
@@ -3001,10 +3001,6 @@ RSpec.describe Repository, feature_category: :source_code_management do
it_behaves_like '#tree'
- describe '#tree? with Rugged enabled', :enable_rugged do
- it_behaves_like '#tree'
- end
-
describe '#size' do
context 'with a non-existing repository' do
it 'returns 0' do
@@ -3090,33 +3086,13 @@ RSpec.describe Repository, feature_category: :source_code_management do
describe '#refresh_method_caches' do
it 'refreshes the caches of the given types' do
expect(repository).to receive(:expire_method_caches)
- .with(%i(readme_path license_blob license_gitaly))
+ .with(%i[readme_path license_blob license_gitaly])
expect(repository).to receive(:readme_path)
expect(repository).to receive(:license_blob)
expect(repository).to receive(:license_gitaly)
- repository.refresh_method_caches(%i(readme license))
- end
- end
-
- describe '#gitlab_ci_yml_for' do
- let(:project) { create(:project, :repository) }
-
- before do
- repository.create_file(User.last, '.gitlab-ci.yml', 'CONTENT', message: 'Add .gitlab-ci.yml', branch_name: 'master')
- end
-
- context 'when there is a .gitlab-ci.yml at the commit' do
- it 'returns the content' do
- expect(repository.gitlab_ci_yml_for(repository.commit.sha)).to eq('CONTENT')
- end
- end
-
- context 'when there is no .gitlab-ci.yml at the commit' do
- it 'returns nil' do
- expect(repository.gitlab_ci_yml_for(repository.commit.parent.sha)).to be_nil
- end
+ repository.refresh_method_caches(%i[readme license])
end
end
@@ -3236,16 +3212,6 @@ RSpec.describe Repository, feature_category: :source_code_management do
end
end
- describe '#ancestor? with Rugged enabled', :enable_rugged do
- it 'calls out to the Rugged implementation' do
- allow_any_instance_of(Rugged).to receive(:merge_base).with(repository.commit.id, Gitlab::Git::BLANK_SHA).and_call_original
-
- repository.ancestor?(repository.commit.id, Gitlab::Git::BLANK_SHA)
- end
-
- it_behaves_like '#ancestor?'
- end
-
describe '#archive_metadata' do
let(:ref) { 'master' }
let(:storage_path) { '/tmp' }
@@ -3842,6 +3808,13 @@ RSpec.describe Repository, feature_category: :source_code_management do
it 'returns nil' do
expect(repository.get_patch_id('HEAD', 'HEAD')).to be_nil
end
+
+ it 'does not report the exception' do
+ expect(Gitlab::ErrorTracking)
+ .not_to receive(:track_exception)
+
+ repository.get_patch_id('HEAD', 'HEAD')
+ end
end
context 'when a Gitlab::Git::CommandError is raised' do
@@ -3851,7 +3824,7 @@ RSpec.describe Repository, feature_category: :source_code_management do
end
it 'returns nil' do
- expect(repository.get_patch_id('HEAD', 'HEAD')).to be_nil
+ expect(repository.get_patch_id('HEAD~', 'HEAD')).to be_nil
end
it 'reports the exception' do
@@ -3860,11 +3833,11 @@ RSpec.describe Repository, feature_category: :source_code_management do
.with(
instance_of(Gitlab::Git::CommandError),
project_id: repository.project.id,
- old_revision: 'HEAD',
+ old_revision: 'HEAD~',
new_revision: 'HEAD'
)
- repository.get_patch_id('HEAD', 'HEAD')
+ repository.get_patch_id('HEAD~', 'HEAD')
end
end
diff --git a/spec/models/service_desk/custom_email_credential_spec.rb b/spec/models/service_desk/custom_email_credential_spec.rb
index a990b77128e..dbf47a8f6a7 100644
--- a/spec/models/service_desk/custom_email_credential_spec.rb
+++ b/spec/models/service_desk/custom_email_credential_spec.rb
@@ -55,6 +55,39 @@ RSpec.describe ServiceDesk::CustomEmailCredential, feature_category: :service_de
end
end
+ describe '#delivery_options' do
+ let(:expected_attributes) do
+ {
+ address: 'smtp.example.com',
+ domain: 'example.com',
+ user_name: 'user@example.com',
+ port: 587,
+ password: 'supersecret',
+ authentication: nil
+ }
+ end
+
+ let(:setting) { build_stubbed(:service_desk_setting, project: project, custom_email: 'user@example.com') }
+
+ subject { credential.delivery_options }
+
+ before do
+ # credential.service_desk_setting is delegated to project and we only use build_stubbed
+ project.service_desk_setting = setting
+ end
+
+ it { is_expected.to include(expected_attributes) }
+
+ context 'when authentication is set' do
+ before do
+ credential.smtp_authentication = 'login'
+ expected_attributes[:authentication] = 'login'
+ end
+
+ it { is_expected.to include(expected_attributes) }
+ end
+ end
+
describe 'associations' do
it { is_expected.to belong_to(:project) }
diff --git a/spec/models/snippet_repository_spec.rb b/spec/models/snippet_repository_spec.rb
index 8048f255272..fe854f5d3ba 100644
--- a/spec/models/snippet_repository_spec.rb
+++ b/spec/models/snippet_repository_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe SnippetRepository do
+RSpec.describe SnippetRepository, feature_category: :snippets do
let_it_be(:user) { create(:user) }
let(:snippet) { create(:personal_snippet, :repository, author: user) }
@@ -68,6 +68,8 @@ RSpec.describe SnippetRepository do
expect(update_file_blob).not_to be_nil
end
+ expect(described_class.sticking).to receive(:stick)
+
expect do
snippet_repository.multi_files_action(user, data, **commit_opts)
end.not_to raise_error
diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb
index ec2dfb2634f..ff1c5959cb0 100644
--- a/spec/models/snippet_spec.rb
+++ b/spec/models/snippet_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Snippet do
+RSpec.describe Snippet, feature_category: :source_code_management do
include FakeBlobHelpers
describe 'modules' do
@@ -26,6 +26,22 @@ RSpec.describe Snippet do
it { is_expected.to have_many(:repository_storage_moves).class_name('Snippets::RepositoryStorageMove').inverse_of(:container) }
end
+ describe 'scopes' do
+ describe '.with_repository_storage_moves' do
+ subject { described_class.with_repository_storage_moves }
+
+ let_it_be(:snippet) { create(:project_snippet) }
+
+ it { is_expected.to be_empty }
+
+ context 'when associated repository storage move exists' do
+ let!(:snippet_repository_storage_move) { create(:snippet_repository_storage_move, container: snippet) }
+
+ it { is_expected.to match_array([snippet]) }
+ end
+ end
+ end
+
describe 'validation' do
it { is_expected.to validate_presence_of(:author) }
@@ -614,7 +630,7 @@ RSpec.describe Snippet do
context 'when file does not exist' do
it 'removes nil values from the blobs array' do
- allow(snippet).to receive(:list_files).and_return(%w(LICENSE non_existent_snippet_file))
+ allow(snippet).to receive(:list_files).and_return(%w[LICENSE non_existent_snippet_file])
blobs = snippet.blobs
expect(blobs.count).to eq 1
diff --git a/spec/models/terraform/state_spec.rb b/spec/models/terraform/state_spec.rb
index fc0a6432149..df8051ebbc6 100644
--- a/spec/models/terraform/state_spec.rb
+++ b/spec/models/terraform/state_spec.rb
@@ -21,7 +21,7 @@ RSpec.describe Terraform::State, feature_category: :infrastructure_as_code do
describe '.ordered_by_name' do
let_it_be(:project) { create(:project) }
- let(:names) { %w(state_d state_b state_a state_c) }
+ let(:names) { %w[state_d state_b state_a state_c] }
subject { described_class.ordered_by_name }
diff --git a/spec/models/upload_spec.rb b/spec/models/upload_spec.rb
index 27e2060a94b..ff38edb73b6 100644
--- a/spec/models/upload_spec.rb
+++ b/spec/models/upload_spec.rb
@@ -266,7 +266,7 @@ RSpec.describe Upload do
it 'updates project statistics when upload is added' do
expect(ProjectCacheWorker).to receive(:perform_async)
- .with(project.id, [], [:uploads_size])
+ .with(project.id, [], ['uploads_size'])
subject.save!
end
@@ -275,7 +275,7 @@ RSpec.describe Upload do
subject.save!
expect(ProjectCacheWorker).to receive(:perform_async)
- .with(project.id, [], [:uploads_size])
+ .with(project.id, [], ['uploads_size'])
subject.destroy!
end
diff --git a/spec/models/user_detail_spec.rb b/spec/models/user_detail_spec.rb
index 428fd5470c3..b443988cde9 100644
--- a/spec/models/user_detail_spec.rb
+++ b/spec/models/user_detail_spec.rb
@@ -59,6 +59,27 @@ RSpec.describe UserDetail do
end
end
+ describe '#mastodon' do
+ it { is_expected.to validate_length_of(:mastodon).is_at_most(500) }
+
+ context 'when mastodon is set' do
+ let_it_be(:user_detail) { create(:user_detail) }
+
+ it 'accepts a valid mastodon username' do
+ user_detail.mastodon = '@robin@example.com'
+
+ expect(user_detail).to be_valid
+ end
+
+ it 'throws an error when mastodon username format is wrong' do
+ user_detail.mastodon = '@robin'
+
+ expect(user_detail).not_to be_valid
+ expect(user_detail.errors.full_messages).to match_array([_('Mastodon must contain only a mastodon username.')])
+ end
+ end
+ end
+
describe '#location' do
it { is_expected.to validate_length_of(:location).is_at_most(500) }
end
@@ -97,6 +118,7 @@ RSpec.describe UserDetail do
discord: '1234567890123456789',
linkedin: 'linkedin',
location: 'location',
+ mastodon: '@robin@example.com',
organization: 'organization',
skype: 'skype',
twitter: 'twitter',
@@ -117,6 +139,7 @@ RSpec.describe UserDetail do
it_behaves_like 'prevents `nil` value', :discord
it_behaves_like 'prevents `nil` value', :linkedin
it_behaves_like 'prevents `nil` value', :location
+ it_behaves_like 'prevents `nil` value', :mastodon
it_behaves_like 'prevents `nil` value', :organization
it_behaves_like 'prevents `nil` value', :skype
it_behaves_like 'prevents `nil` value', :twitter
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 947d83badf6..fe229ce836f 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -44,6 +44,9 @@ RSpec.describe User, feature_category: :user_profile do
it { is_expected.to delegate_method(:time_display_relative).to(:user_preference) }
it { is_expected.to delegate_method(:time_display_relative=).to(:user_preference).with_arguments(:args) }
+ it { is_expected.to delegate_method(:time_display_format).to(:user_preference) }
+ it { is_expected.to delegate_method(:time_display_format=).to(:user_preference).with_arguments(:args) }
+
it { is_expected.to delegate_method(:show_whitespace_in_diffs).to(:user_preference) }
it { is_expected.to delegate_method(:show_whitespace_in_diffs=).to(:user_preference).with_arguments(:args) }
@@ -113,6 +116,9 @@ RSpec.describe User, feature_category: :user_profile do
it { is_expected.to delegate_method(:linkedin).to(:user_detail).allow_nil }
it { is_expected.to delegate_method(:linkedin=).to(:user_detail).with_arguments(:args).allow_nil }
+ it { is_expected.to delegate_method(:mastodon).to(:user_detail).allow_nil }
+ it { is_expected.to delegate_method(:mastodon=).to(:user_detail).with_arguments(:args).allow_nil }
+
it { is_expected.to delegate_method(:twitter).to(:user_detail).allow_nil }
it { is_expected.to delegate_method(:twitter=).to(:user_detail).with_arguments(:args).allow_nil }
@@ -130,6 +136,9 @@ RSpec.describe User, feature_category: :user_profile do
it { is_expected.to delegate_method(:email_reset_offered_at).to(:user_detail).allow_nil }
it { is_expected.to delegate_method(:email_reset_offered_at=).to(:user_detail).with_arguments(:args).allow_nil }
+
+ it { is_expected.to delegate_method(:project_authorizations_recalculated_at).to(:user_detail).allow_nil }
+ it { is_expected.to delegate_method(:project_authorizations_recalculated_at=).to(:user_detail).with_arguments(:args).allow_nil }
end
describe 'associations' do
@@ -1277,7 +1286,7 @@ RSpec.describe User, feature_category: :user_profile do
user = create(:user, username: 'CaMeLcAsEd')
user2 = create(:user, username: 'UPPERCASE')
- expect(described_class.by_username(%w(CAMELCASED uppercase)))
+ expect(described_class.by_username(%w[CAMELCASED uppercase]))
.to contain_exactly(user, user2)
end
@@ -1416,6 +1425,16 @@ RSpec.describe User, feature_category: :user_profile do
'ORDER BY "users"."current_sign_in_at" ASC NULLS LAST')
end
end
+
+ describe '.trusted' do
+ let_it_be(:trusted_user1) { create(:user, :trusted) }
+ let_it_be(:trusted_user2) { create(:user, :trusted) }
+ let_it_be(:user3) { create(:user) }
+
+ it 'returns only the trusted users' do
+ expect(described_class.trusted).to match_array([trusted_user1, trusted_user2])
+ end
+ end
end
context 'strip attributes' do
@@ -1824,7 +1843,7 @@ RSpec.describe User, feature_category: :user_profile do
end
context 'when the confirmation period has expired' do
- let(:confirmation_sent_at) { expired_confirmation_sent_at }
+ let(:confirmation_sent_at) { expired_confirmation_sent_at }
it_behaves_like 'unconfirmed user'
@@ -1842,7 +1861,7 @@ RSpec.describe User, feature_category: :user_profile do
end
context 'when the confirmation period has not expired' do
- let(:confirmation_sent_at) { extant_confirmation_sent_at }
+ let(:confirmation_sent_at) { extant_confirmation_sent_at }
it_behaves_like 'unconfirmed user'
@@ -2033,7 +2052,7 @@ RSpec.describe User, feature_category: :user_profile do
end
context 'when the confirmation period has expired' do
- let(:confirmation_sent_at) { expired_confirmation_sent_at }
+ let(:confirmation_sent_at) { expired_confirmation_sent_at }
it_behaves_like 'unconfirmed user'
it_behaves_like 'confirms the user on force_confirm'
@@ -2855,6 +2874,12 @@ RSpec.describe User, feature_category: :user_profile do
expect(described_class.filter_items('wop')).to include user
end
+
+ it 'filters by trusted' do
+ expect(described_class).to receive(:trusted).and_return([user])
+
+ expect(described_class.filter_items('trusted')).to include user
+ end
end
describe '.without_projects' do
@@ -3261,6 +3286,9 @@ RSpec.describe User, feature_category: :user_profile do
end
describe 'username matching' do
+ let_it_be(:named_john) { create(:user, name: 'John', username: 'abcd') }
+ let_it_be(:username_john) { create(:user, name: 'John Doe', username: 'john') }
+
it 'returns users with a matching username' do
expect(described_class.search(user.username)).to eq([user, user2])
end
@@ -3281,6 +3309,10 @@ RSpec.describe User, feature_category: :user_profile do
expect(described_class.search(user2.username.upcase)).to eq([user2])
end
+ it 'returns users with an exact matching username first' do
+ expect(described_class.search('John')).to eq([username_john, named_john])
+ end
+
it 'returns users with a exact matching username shorter than 3 chars' do
expect(described_class.search(user3.username)).to eq([user3])
end
@@ -5814,37 +5846,37 @@ RSpec.describe User, feature_category: :user_profile do
context 'oauth user' do
it 'returns true if name can be synced' do
- stub_omniauth_setting(sync_profile_attributes: %w(name location))
+ stub_omniauth_setting(sync_profile_attributes: %w[name location])
expect(user.sync_attribute?(:name)).to be_truthy
end
it 'returns true if email can be synced' do
- stub_omniauth_setting(sync_profile_attributes: %w(name email))
+ stub_omniauth_setting(sync_profile_attributes: %w[name email])
expect(user.sync_attribute?(:email)).to be_truthy
end
it 'returns true if location can be synced' do
- stub_omniauth_setting(sync_profile_attributes: %w(location email))
+ stub_omniauth_setting(sync_profile_attributes: %w[location email])
expect(user.sync_attribute?(:email)).to be_truthy
end
it 'returns false if name can not be synced' do
- stub_omniauth_setting(sync_profile_attributes: %w(location email))
+ stub_omniauth_setting(sync_profile_attributes: %w[location email])
expect(user.sync_attribute?(:name)).to be_falsey
end
it 'returns false if email can not be synced' do
- stub_omniauth_setting(sync_profile_attributes: %w(location name))
+ stub_omniauth_setting(sync_profile_attributes: %w[location name])
expect(user.sync_attribute?(:email)).to be_falsey
end
it 'returns false if location can not be synced' do
- stub_omniauth_setting(sync_profile_attributes: %w(name email))
+ stub_omniauth_setting(sync_profile_attributes: %w[name email])
expect(user.sync_attribute?(:location)).to be_falsey
end
@@ -5875,7 +5907,7 @@ RSpec.describe User, feature_category: :user_profile do
it 'returns true for email and location if ldap user and location declared as syncable' do
allow(user).to receive(:ldap_user?).and_return(true)
- stub_omniauth_setting(sync_profile_attributes: %w(location))
+ stub_omniauth_setting(sync_profile_attributes: %w[location])
expect(user.sync_attribute?(:name)).to be_falsey
expect(user.sync_attribute?(:email)).to be_truthy
diff --git a/spec/models/users/anonymous_spec.rb b/spec/models/users/anonymous_spec.rb
new file mode 100644
index 00000000000..f6151be6184
--- /dev/null
+++ b/spec/models/users/anonymous_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Users::Anonymous, feature_category: :system_access do
+ let_it_be(:public_project, reload: true) { create(:project, :public) }
+ let_it_be(:private_project) { create(:project, :private) }
+ let_it_be(:internal_project) { create(:project, :internal) }
+
+ describe '.can_pull?' do
+ context 'when project is private' do
+ it 'does not allow to pull the repo' do
+ expect(described_class.can?(:download_code, private_project)).to eq(false)
+ end
+ end
+
+ context 'when project is internal' do
+ it 'does not allow to pull the repo' do
+ expect(described_class.can?(:download_code, internal_project)).to eq(false)
+ end
+ end
+
+ context 'when project is public' do
+ context 'when repository is disabled' do
+ it 'does not allow to pull the repo' do
+ public_project.project_feature.update_attribute(:repository_access_level, ProjectFeature::DISABLED)
+
+ expect(described_class.can?(:download_code, public_project)).to eq(false)
+ end
+ end
+
+ context 'when repository is accessible only by team members' do
+ it 'does not allow to pull the repo' do
+ public_project.project_feature.update_attribute(:repository_access_level, ProjectFeature::PRIVATE)
+
+ expect(described_class.can?(:download_code, public_project)).to eq(false)
+ end
+ end
+
+ context 'when repository is enabled' do
+ it 'allows to pull the repo' do
+ expect(described_class.can?(:download_code, public_project)).to eq(true)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/users/credit_card_validation_spec.rb b/spec/models/users/credit_card_validation_spec.rb
index 7faddb2384c..ae75020c768 100644
--- a/spec/models/users/credit_card_validation_spec.rb
+++ b/spec/models/users/credit_card_validation_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe Users::CreditCardValidation, feature_category: :user_profile do
+ include CryptoHelpers
+
it { is_expected.to belong_to(:user) }
it { is_expected.to validate_length_of(:holder_name).is_at_most(50) }
@@ -206,12 +208,12 @@ RSpec.describe Users::CreditCardValidation, feature_category: :user_profile do
context 'when last_digits has a blank value' do
let(:last_digits) { ' ' }
- it { expect { save_credit_card_validation }.not_to change { credit_card_validation.last_digits_hash } }
+ it { expect(credit_card_validation).to be_invalid }
end
context 'when last_digits has a value' do
let(:last_digits) { 1111 }
- let(:expected_last_digits_hash) { Gitlab::CryptoHelper.sha256(last_digits) }
+ let(:expected_last_digits_hash) { sha256(last_digits) }
it 'assigns correct last_digits_hash value' do
expect { save_credit_card_validation }.to change {
@@ -240,7 +242,7 @@ RSpec.describe Users::CreditCardValidation, feature_category: :user_profile do
context 'when holder_name has a value' do
let(:holder_name) { 'John Smith' }
- let(:expected_holder_name_hash) { Gitlab::CryptoHelper.sha256(holder_name.downcase) }
+ let(:expected_holder_name_hash) { sha256(holder_name.downcase) }
it 'lowercases holder_name and assigns correct holder_name_hash value' do
expect { save_credit_card_validation }.to change {
@@ -269,7 +271,7 @@ RSpec.describe Users::CreditCardValidation, feature_category: :user_profile do
context 'when network has a value' do
let(:network) { 'Visa' }
- let(:expected_network_hash) { Gitlab::CryptoHelper.sha256(network.downcase) }
+ let(:expected_network_hash) { sha256(network.downcase) }
it 'lowercases network and assigns correct network_hash value' do
expect { save_credit_card_validation }.to change {
@@ -298,7 +300,7 @@ RSpec.describe Users::CreditCardValidation, feature_category: :user_profile do
context 'when expiration_date has a value' do
let(:expiration_date) { 1.year.from_now.to_date }
- let(:expected_expiration_date_hash) { Gitlab::CryptoHelper.sha256(expiration_date.to_s) }
+ let(:expected_expiration_date_hash) { sha256(expiration_date.to_s) }
it 'assigns correct expiration_date_hash value' do
expect { save_credit_card_validation }.to change {
diff --git a/spec/models/users/group_visit_spec.rb b/spec/models/users/group_visit_spec.rb
index 63c4631ad7d..241cb537fad 100644
--- a/spec/models/users/group_visit_spec.rb
+++ b/spec/models/users/group_visit_spec.rb
@@ -7,10 +7,6 @@ RSpec.describe Users::GroupVisit, feature_category: :navigation do
let_it_be(:user) { create(:user) }
let_it_be(:base_time) { DateTime.now }
- before do
- described_class.create!(entity_id: entity.id, user_id: user.id, visited_at: base_time)
- end
-
it_behaves_like 'namespace visits model'
it_behaves_like 'cleanup by a loose foreign key' do
@@ -22,4 +18,25 @@ RSpec.describe Users::GroupVisit, feature_category: :navigation do
let!(:model) { create(:group_visit, entity_id: entity.id, user_id: user.id, visited_at: base_time) }
let!(:parent) { user }
end
+
+ describe '#frecent_groups' do
+ let_it_be(:group1) { create(:group) }
+ let_it_be(:group2) { create(:group) }
+
+ before do
+ [
+ [group1.id, 1.day.ago],
+ [group2.id, 2.days.ago]
+ ].each do |id, datetime|
+ described_class.create!(entity_id: id, user_id: user.id, visited_at: datetime)
+ end
+ end
+
+ it "returns the associated frecently visited groups" do
+ expect(described_class.frecent_groups(user_id: user.id)).to eq([
+ group1,
+ group2
+ ])
+ end
+ end
end
diff --git a/spec/models/users/phone_number_validation_spec.rb b/spec/models/users/phone_number_validation_spec.rb
index 7ab461a4346..e41719d8ca3 100644
--- a/spec/models/users/phone_number_validation_spec.rb
+++ b/spec/models/users/phone_number_validation_spec.rb
@@ -2,7 +2,10 @@
require 'spec_helper'
-RSpec.describe Users::PhoneNumberValidation do
+RSpec.describe Users::PhoneNumberValidation, feature_category: :instance_resiliency do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:banned_user) { create(:user, :banned) }
+
it { is_expected.to belong_to(:user) }
it { is_expected.to belong_to(:banned_user) }
@@ -31,9 +34,6 @@ RSpec.describe Users::PhoneNumberValidation do
let_it_be(:international_dial_code) { 1 }
let_it_be(:phone_number) { '555' }
- let_it_be(:user) { create(:user) }
- let_it_be(:banned_user) { create(:user, :banned) }
-
subject(:related_to_banned_user?) do
described_class.related_to_banned_user?(international_dial_code, phone_number)
end
@@ -79,25 +79,25 @@ RSpec.describe Users::PhoneNumberValidation do
end
end
- describe '#for_user' do
- let_it_be(:user_1) { create(:user) }
- let_it_be(:user_2) { create(:user) }
+ describe 'scopes' do
+ let_it_be(:another_user) { create(:user) }
- let_it_be(:phone_number_record_1) { create(:phone_number_validation, user: user_1) }
- let_it_be(:phone_number_record_2) { create(:phone_number_validation, user: user_2) }
+ let_it_be(:phone_number_record_1) { create(:phone_number_validation, user: user, telesign_reference_xid: 'target') }
+ let_it_be(:phone_number_record_2) { create(:phone_number_validation, user: another_user) }
- context 'when multiple records exist for multiple users' do
- it 'returns the correct phone number record for user' do
- records = described_class.for_user(user_1.id)
+ describe '#for_user' do
+ context 'when multiple records exist for multiple users' do
+ it 'returns the correct phone number record for user' do
+ records = described_class.for_user(user.id)
- expect(records.count).to be(1)
- expect(records.first).to eq(phone_number_record_1)
+ expect(records.count).to be(1)
+ expect(records.first).to eq(phone_number_record_1)
+ end
end
end
end
describe '#validated?' do
- let_it_be(:user) { create(:user) }
let_it_be(:phone_number_record) { create(:phone_number_validation, user: user) }
context 'when phone number record is not validated' do
@@ -116,4 +116,20 @@ RSpec.describe Users::PhoneNumberValidation do
end
end
end
+
+ describe '.by_reference_id' do
+ let_it_be(:phone_number_record) { create(:phone_number_validation) }
+
+ let(:ref_id) { phone_number_record.telesign_reference_xid }
+
+ subject { described_class.by_reference_id(ref_id) }
+
+ it { is_expected.to eq phone_number_record }
+
+ context 'when there is no matching record' do
+ let(:ref_id) { 'does-not-exist' }
+
+ it { is_expected.to be_nil }
+ end
+ end
end
diff --git a/spec/models/users/project_visit_spec.rb b/spec/models/users/project_visit_spec.rb
index 38747bd6462..50e9ef02fd8 100644
--- a/spec/models/users/project_visit_spec.rb
+++ b/spec/models/users/project_visit_spec.rb
@@ -7,10 +7,6 @@ RSpec.describe Users::ProjectVisit, feature_category: :navigation do
let_it_be(:user) { create(:user) }
let_it_be(:base_time) { DateTime.now }
- before do
- described_class.create!(entity_id: entity.id, user_id: user.id, visited_at: base_time)
- end
-
it_behaves_like 'namespace visits model'
it_behaves_like 'cleanup by a loose foreign key' do
@@ -22,4 +18,25 @@ RSpec.describe Users::ProjectVisit, feature_category: :navigation do
let!(:model) { create(:project_visit, entity_id: entity.id, user_id: user.id, visited_at: base_time) }
let!(:parent) { user }
end
+
+ describe '#frecent_projects' do
+ let_it_be(:project1) { create(:project) }
+ let_it_be(:project2) { create(:project) }
+
+ before do
+ [
+ [project1.id, 1.day.ago],
+ [project2.id, 2.days.ago]
+ ].each do |id, datetime|
+ described_class.create!(entity_id: id, user_id: user.id, visited_at: datetime)
+ end
+ end
+
+ it "returns the associated frecently visited projects" do
+ expect(described_class.frecent_projects(user_id: user.id)).to eq([
+ project1,
+ project2
+ ])
+ end
+ end
end
diff --git a/spec/models/vs_code/settings/vs_code_setting_spec.rb b/spec/models/vs_code/settings/vs_code_setting_spec.rb
index d22cc815877..d61e89dc54b 100644
--- a/spec/models/vs_code/settings/vs_code_setting_spec.rb
+++ b/spec/models/vs_code/settings/vs_code_setting_spec.rb
@@ -11,10 +11,18 @@ RSpec.describe VsCode::Settings::VsCodeSetting, feature_category: :web_ide do
it { is_expected.to validate_presence_of(:content) }
end
+ describe 'validates the uniqueness of attributes' do
+ it { is_expected.to validate_uniqueness_of(:setting_type).scoped_to([:user_id]) }
+ end
+
describe 'relationship validation' do
it { is_expected.to belong_to(:user) }
end
+ describe 'settings type validation' do
+ it { is_expected.to validate_inclusion_of(:setting_type).in_array(VsCode::Settings::SETTINGS_TYPES) }
+ end
+
describe '.by_setting_type' do
subject { described_class.by_setting_type('settings') }
diff --git a/spec/models/web_ide_terminal_spec.rb b/spec/models/web_ide_terminal_spec.rb
index fc30bc18f68..505b7531db4 100644
--- a/spec/models/web_ide_terminal_spec.rb
+++ b/spec/models/web_ide_terminal_spec.rb
@@ -45,7 +45,7 @@ RSpec.describe WebIdeTerminal do
end
it 'returns services aliases' do
- expect(subject.services).to eq %w(postgres docker)
+ expect(subject.services).to eq %w[postgres docker]
end
end
@@ -55,7 +55,7 @@ RSpec.describe WebIdeTerminal do
end
it 'returns all aliases' do
- expect(subject.services).to eq %w(postgres docker ruby)
+ expect(subject.services).to eq %w[postgres docker ruby]
end
end
@@ -71,7 +71,7 @@ RSpec.describe WebIdeTerminal do
context 'when no image nor services' do
let(:config) do
- { script: %w(echo) }
+ { script: %w[echo] }
end
it 'returns an empty array' do
diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb
index 2e1cb9d3d9b..f3cf8966b9a 100644
--- a/spec/models/wiki_page_spec.rb
+++ b/spec/models/wiki_page_spec.rb
@@ -58,6 +58,7 @@ RSpec.describe WikiPage, feature_category: :wiki do
let(:front_matter) { { title: 'Foo', slugs: %w[slug_a slug_b] } }
it { expect(wiki_page.front_matter).to eq(front_matter) }
+ it { expect(wiki_page.front_matter_title).to eq(front_matter[:title]) }
end
context 'the wiki page has front matter' do
@@ -1054,4 +1055,28 @@ RSpec.describe WikiPage, feature_category: :wiki do
)
end
end
+
+ describe "#human_title" do
+ context "with front matter title" do
+ let(:front_matter_title) { "abc" }
+ let(:content_with_front_matter_title) { "---\ntitle: #{front_matter_title}\n---\nHome Page" }
+ let(:wiki_page) { create(:wiki_page, container: container, content: content_with_front_matter_title) }
+
+ context "when wiki_front_matter_title enabled" do
+ it 'returns the front matter title' do
+ expect(wiki_page.human_title).to eq front_matter_title
+ end
+ end
+
+ context "when wiki_front_matter_title disabled" do
+ before do
+ stub_feature_flags(wiki_front_matter_title: false)
+ end
+
+ it 'returns the page title' do
+ expect(wiki_page.human_title).to eq wiki_page.title
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/work_item_spec.rb b/spec/models/work_item_spec.rb
index 3294d53e364..476d346db10 100644
--- a/spec/models/work_item_spec.rb
+++ b/spec/models/work_item_spec.rb
@@ -402,6 +402,12 @@ RSpec.describe WorkItem, feature_category: :portfolio_management do
end
end
+ describe '#linked_items_keyset_order' do
+ subject { described_class.linked_items_keyset_order }
+
+ it { is_expected.to eq('"issue_links"."id" ASC') }
+ end
+
context 'with hierarchy' do
let_it_be(:type1) { create(:work_item_type, namespace: reusable_project.namespace) }
let_it_be(:type2) { create(:work_item_type, namespace: reusable_project.namespace) }
diff --git a/spec/models/zoom_meeting_spec.rb b/spec/models/zoom_meeting_spec.rb
index d3d75a19fed..b67a9d4a2ff 100644
--- a/spec/models/zoom_meeting_spec.rb
+++ b/spec/models/zoom_meeting_spec.rb
@@ -65,7 +65,7 @@ RSpec.describe ZoomMeeting do
context 'with non-Zoom URL' do
before do
- subject.url = %{https://non-zoom.url}
+ subject.url = %(https://non-zoom.url)
end
include_examples 'invalid Zoom URL'
@@ -73,7 +73,7 @@ RSpec.describe ZoomMeeting do
context 'with multiple Zoom-URLs' do
before do
- subject.url = %{https://zoom.us/j/123 https://zoom.us/j/456}
+ subject.url = %(https://zoom.us/j/123 https://zoom.us/j/456)
end
include_examples 'invalid Zoom URL'
diff --git a/spec/policies/abuse_report_policy_spec.rb b/spec/policies/abuse_report_policy_spec.rb
index b17b6886b9a..01ab29d1cf1 100644
--- a/spec/policies/abuse_report_policy_spec.rb
+++ b/spec/policies/abuse_report_policy_spec.rb
@@ -12,6 +12,7 @@ RSpec.describe AbuseReportPolicy, feature_category: :insider_threat do
it 'cannot read_abuse_report' do
expect(policy).to be_disallowed(:read_abuse_report)
+ expect(policy).to be_disallowed(:create_note)
end
end
@@ -20,6 +21,7 @@ RSpec.describe AbuseReportPolicy, feature_category: :insider_threat do
it 'can read_abuse_report' do
expect(policy).to be_allowed(:read_abuse_report)
+ expect(policy).to be_allowed(:create_note)
end
end
end
diff --git a/spec/policies/ci/build_policy_spec.rb b/spec/policies/ci/build_policy_spec.rb
index 6ab89daff82..ad568e60d5c 100644
--- a/spec/policies/ci/build_policy_spec.rb
+++ b/spec/policies/ci/build_policy_spec.rb
@@ -109,7 +109,8 @@ RSpec.describe Ci::BuildPolicy, feature_category: :continuous_integration do
allow(project).to receive(:branch_allows_collaboration?).and_return(true)
end
- it 'enables update_build if user is maintainer' do
+ it 'enables updates if user is maintainer', :aggregate_failures do
+ expect(policy).to be_allowed :cancel_build
expect(policy).to be_allowed :update_build
expect(policy).to be_allowed :update_commit_status
end
@@ -130,6 +131,7 @@ RSpec.describe Ci::BuildPolicy, feature_category: :continuous_integration do
end
it 'does not include ability to update build' do
+ expect(policy).to be_disallowed :cancel_build
expect(policy).to be_disallowed :update_build
end
@@ -139,6 +141,7 @@ RSpec.describe Ci::BuildPolicy, feature_category: :continuous_integration do
end
it 'does not include ability to update build' do
+ expect(policy).to be_disallowed :cancel_build
expect(policy).to be_disallowed :update_build
end
end
@@ -150,6 +153,7 @@ RSpec.describe Ci::BuildPolicy, feature_category: :continuous_integration do
end
it 'includes ability to update build' do
+ expect(policy).to be_allowed :cancel_build
expect(policy).to be_allowed :update_build
end
end
@@ -162,6 +166,7 @@ RSpec.describe Ci::BuildPolicy, feature_category: :continuous_integration do
end
it 'does not include ability to update build' do
+ expect(policy).to be_disallowed :cancel_build
expect(policy).to be_disallowed :update_build
end
end
@@ -172,6 +177,7 @@ RSpec.describe Ci::BuildPolicy, feature_category: :continuous_integration do
end
it 'includes ability to update build' do
+ expect(policy).to be_allowed :cancel_build
expect(policy).to be_allowed :update_build
end
end
diff --git a/spec/policies/ci/pipeline_policy_spec.rb b/spec/policies/ci/pipeline_policy_spec.rb
index e74bf8f7efa..7475cda5cf9 100644
--- a/spec/policies/ci/pipeline_policy_spec.rb
+++ b/spec/policies/ci/pipeline_policy_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::PipelinePolicy, :models do
+RSpec.describe Ci::PipelinePolicy, :models, feature_category: :continuous_integration do
let(:user) { create(:user) }
let(:pipeline) { create(:ci_empty_pipeline, project: project) }
@@ -25,6 +25,7 @@ RSpec.describe Ci::PipelinePolicy, :models do
it 'does not include ability to update pipeline' do
expect(policy).to be_disallowed :update_pipeline
+ expect(policy).to be_disallowed :cancel_pipeline
end
end
@@ -35,6 +36,7 @@ RSpec.describe Ci::PipelinePolicy, :models do
it 'includes ability to update pipeline' do
expect(policy).to be_allowed :update_pipeline
+ expect(policy).to be_allowed :cancel_pipeline
end
end
@@ -47,6 +49,7 @@ RSpec.describe Ci::PipelinePolicy, :models do
it 'does not include ability to update pipeline' do
expect(policy).to be_disallowed :update_pipeline
+ expect(policy).to be_disallowed :cancel_pipeline
end
end
@@ -57,6 +60,7 @@ RSpec.describe Ci::PipelinePolicy, :models do
it 'includes ability to update pipeline' do
expect(policy).to be_allowed :update_pipeline
+ expect(policy).to be_allowed :cancel_pipeline
end
end
end
@@ -70,6 +74,7 @@ RSpec.describe Ci::PipelinePolicy, :models do
allow_any_instance_of(Project).to receive(:branch_allows_collaboration?).and_return(true)
expect(policy).to be_allowed :update_pipeline
+ expect(policy).to be_allowed :cancel_pipeline
end
end
diff --git a/spec/policies/concerns/policy_actor_spec.rb b/spec/policies/concerns/policy_actor_spec.rb
index 7fd9db67032..a38725d73d6 100644
--- a/spec/policies/concerns/policy_actor_spec.rb
+++ b/spec/policies/concerns/policy_actor_spec.rb
@@ -20,10 +20,4 @@ RSpec.describe PolicyActor, feature_category: :shared do
# initialized. So here we just use an instance
expect(build(:user).methods).to include(*methods)
end
-
- describe '#security_policy_bot?' do
- subject { PolicyActorTestClass.new.security_policy_bot? }
-
- it { is_expected.to eq(false) }
- end
end
diff --git a/spec/policies/global_policy_spec.rb b/spec/policies/global_policy_spec.rb
index 475e8f981dd..52fea8d782e 100644
--- a/spec/policies/global_policy_spec.rb
+++ b/spec/policies/global_policy_spec.rb
@@ -10,7 +10,6 @@ RSpec.describe GlobalPolicy, feature_category: :shared do
let_it_be(:service_account) { create(:user, :service_account) }
let_it_be(:migration_bot) { create(:user, :migration_bot) }
let_it_be(:security_bot) { create(:user, :security_bot) }
- let_it_be(:security_policy_bot) { create(:user, :security_policy_bot) }
let_it_be(:llm_bot) { create(:user, :llm_bot) }
let_it_be_with_reload(:current_user) { create(:user) }
let_it_be(:user) { create(:user) }
@@ -411,12 +410,6 @@ RSpec.describe GlobalPolicy, feature_category: :shared do
it { is_expected.to be_allowed(:access_git) }
end
- context 'security policy bot' do
- let(:current_user) { security_policy_bot }
-
- it { is_expected.to be_allowed(:access_git) }
- end
-
describe 'deactivated user' do
before do
current_user.deactivate
diff --git a/spec/policies/group_group_link_policy_spec.rb b/spec/policies/group_group_link_policy_spec.rb
new file mode 100644
index 00000000000..34bc1bc3bec
--- /dev/null
+++ b/spec/policies/group_group_link_policy_spec.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GroupGroupLinkPolicy, feature_category: :system_access do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group, :private) }
+ let_it_be(:group2) { create(:group, :private) }
+
+ let(:group_group_link) do
+ create(:group_group_link, shared_group: group, shared_with_group: group2)
+ end
+
+ subject(:policy) { described_class.new(user, group_group_link) }
+
+ describe 'read_shared_with_group' do
+ context 'when the user is a shared_group member' do
+ before_all do
+ group.add_guest(user)
+ end
+
+ it 'can read_shared_with_group' do
+ expect(policy).to be_allowed(:read_shared_with_group)
+ end
+ end
+
+ context 'when the user is not a shared_group member' do
+ context 'when user is not a shared_with_group member' do
+ context 'when the shared_with_group is private' do
+ it 'cannot read_shared_with_group' do
+ expect(policy).to be_disallowed(:read_shared_with_group)
+ end
+
+ context 'when the shared group is public' do
+ let_it_be(:group) { create(:group, :public) }
+
+ it 'cannot read_shared_with_group' do
+ expect(policy).to be_disallowed(:read_shared_with_group)
+ end
+ end
+ end
+
+ context 'when the shared_with_group is public' do
+ let_it_be(:group2) { create(:group, :public) }
+
+ it 'can read_shared_with_group' do
+ expect(policy).to be_allowed(:read_shared_with_group)
+ end
+ end
+ end
+
+ context 'when user is a shared_with_group member' do
+ before_all do
+ group2.add_developer(user)
+ end
+
+ it 'can read_shared_with_group' do
+ expect(policy).to be_allowed(:read_shared_with_group)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/policies/issue_policy_spec.rb b/spec/policies/issue_policy_spec.rb
index 743d96ee3dd..c19b7bcf9ea 100644
--- a/spec/policies/issue_policy_spec.rb
+++ b/spec/policies/issue_policy_spec.rb
@@ -8,16 +8,16 @@ RSpec.describe IssuePolicy, feature_category: :team_planning do
include ProjectHelpers
include UserHelpers
- let(:admin) { create(:user, :admin) }
- let(:guest) { create(:user) }
- let(:author) { create(:user) }
- let(:assignee) { create(:user) }
- let(:reporter) { create(:user) }
- let(:maintainer) { create(:user) }
- let(:owner) { create(:user) }
- let(:group) { create(:group, :public) }
- let(:reporter_from_group_link) { create(:user) }
- let(:non_member) { create(:user) }
+ let_it_be(:group) { create(:group, :public) }
+ let_it_be(:admin) { create(:user, :admin) }
+ let_it_be(:guest) { create(:user) }
+ let_it_be(:author) { create(:user) }
+ let_it_be(:assignee) { create(:user) }
+ let_it_be(:reporter) { create(:user) }
+ let_it_be(:maintainer) { create(:user) }
+ let_it_be(:owner) { create(:user) }
+ let_it_be(:reporter_from_group_link) { create(:user) }
+ let_it_be(:non_member) { create(:user) }
let(:support_bot) { Users::Internal.support_bot }
let(:alert_bot) { Users::Internal.alert_bot }
@@ -70,12 +70,12 @@ RSpec.describe IssuePolicy, feature_category: :team_planning do
end
context 'a private project' do
- let(:project) { create(:project, :private) }
- let(:issue) { create(:issue, project: project, assignees: [assignee], author: author) }
- let(:issue_no_assignee) { create(:issue, project: project) }
+ let_it_be(:project) { create(:project, :private) }
+ let_it_be_with_reload(:issue) { create(:issue, project: project, assignees: [assignee], author: author) }
+ let_it_be_with_reload(:issue_no_assignee) { create(:issue, project: project) }
let(:new_issue) { build(:issue, project: project, assignees: [assignee], author: author) }
- before do
+ before_all do
project.add_guest(guest)
project.add_guest(author)
project.add_guest(assignee)
@@ -86,6 +86,10 @@ RSpec.describe IssuePolicy, feature_category: :team_planning do
create(:project_group_link, group: group, project: project)
end
+ it 'allows guests to award emoji' do
+ expect(permissions(guest, issue)).to be_allowed(:award_emoji)
+ end
+
it 'allows guests to read issues' do
expect(permissions(guest, issue)).to be_allowed(:read_issue, :read_issue_iid, :admin_issue_relation)
expect(permissions(guest, issue)).to be_disallowed(:update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality, :mark_note_as_internal)
@@ -191,13 +195,13 @@ RSpec.describe IssuePolicy, feature_category: :team_planning do
end
context 'a public project' do
- let(:project) { create(:project, :public) }
- let(:issue) { create(:issue, project: project, assignees: [assignee], author: author) }
- let(:issue_no_assignee) { create(:issue, project: project) }
- let(:issue_locked) { create(:issue, :locked, project: project, author: author, assignees: [assignee]) }
+ let_it_be_with_reload(:project) { create(:project, :public) }
+ let_it_be_with_reload(:issue) { create(:issue, project: project, assignees: [assignee], author: author) }
+ let_it_be_with_reload(:issue_no_assignee) { create(:issue, project: project) }
+ let_it_be_with_reload(:issue_locked) { create(:issue, :locked, project: project, author: author, assignees: [assignee]) }
let(:new_issue) { build(:issue, project: project) }
- before do
+ before_all do
project.add_guest(guest)
project.add_reporter(reporter)
project.add_maintainer(maintainer)
@@ -208,6 +212,10 @@ RSpec.describe IssuePolicy, feature_category: :team_planning do
create(:project_group_link, group: group, project: project)
end
+ it 'allows guests to award emoji' do
+ expect(permissions(guest, issue)).to be_allowed(:award_emoji)
+ end
+
it 'does not allow anonymous user to create todos' do
expect(permissions(nil, issue)).to be_allowed(:read_issue)
expect(permissions(nil, issue)).to be_disallowed(:create_todo, :update_subscription, :set_issue_metadata, :set_confidentiality, :admin_issue_relation)
@@ -304,12 +312,12 @@ RSpec.describe IssuePolicy, feature_category: :team_planning do
it_behaves_like 'support bot with service desk enabled'
context 'when issues are private' do
- before do
+ before_all do
project.project_feature.update!(issues_access_level: ProjectFeature::PRIVATE)
end
- let(:issue) { create(:issue, project: project, author: author) }
- let(:visitor) { create(:user) }
+ let_it_be_with_reload(:issue) { create(:issue, project: project, author: author) }
+ let_it_be(:visitor) { create(:user) }
it 'forbids visitors from viewing issues' do
expect(permissions(visitor, issue)).to be_disallowed(:read_issue)
@@ -423,10 +431,8 @@ RSpec.describe IssuePolicy, feature_category: :team_planning do
end
context 'when accounting for notes widget' do
- let(:policy) { described_class.new(reporter, note) }
-
context 'and notes widget is disabled for issue' do
- before do
+ before_all do
WorkItems::Type.default_by_type(:issue).widget_definitions.find_by_widget_type(:notes).update!(disabled: true)
end
@@ -450,6 +456,18 @@ RSpec.describe IssuePolicy, feature_category: :team_planning do
end
end
+ context 'when issue belongs to a group' do
+ let_it_be_with_reload(:issue) { create(:issue, :group_level, namespace: group) }
+
+ before_all do
+ group.add_guest(guest)
+ end
+
+ it 'allows guests to award emoji' do
+ expect(permissions(guest, issue)).to be_allowed(:award_emoji)
+ end
+ end
+
context 'with external authorization enabled' do
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
diff --git a/spec/policies/project_group_link_policy_spec.rb b/spec/policies/project_group_link_policy_spec.rb
index 9461f33decb..1047d3acb1e 100644
--- a/spec/policies/project_group_link_policy_spec.rb
+++ b/spec/policies/project_group_link_policy_spec.rb
@@ -4,9 +4,8 @@ require 'spec_helper'
RSpec.describe ProjectGroupLinkPolicy, feature_category: :system_access do
let_it_be(:user) { create(:user) }
- let_it_be(:group) { create(:group, :private) }
let_it_be(:group2) { create(:group, :private) }
- let_it_be(:project) { create(:project, :private, group: group) }
+ let_it_be(:project) { create(:project, :private) }
let(:project_group_link) do
create(:project_group_link, project: project, group: group2, group_access: Gitlab::Access::DEVELOPER)
@@ -14,42 +13,92 @@ RSpec.describe ProjectGroupLinkPolicy, feature_category: :system_access do
subject(:policy) { described_class.new(user, project_group_link) }
- context 'when the user is a group owner' do
- before do
- project_group_link.group.add_owner(user)
- end
+ describe 'admin_project_group_link' do
+ context 'when the user is a group owner' do
+ before_all do
+ group2.add_owner(user)
+ end
- context 'when user is not project maintainer' do
- it 'can admin group_project_link' do
- expect(policy).to be_allowed(:admin_project_group_link)
+ context 'when user is not project maintainer' do
+ it 'can admin group_project_link' do
+ expect(policy).to be_allowed(:admin_project_group_link)
+ end
+ end
+
+ context 'when user is a project maintainer' do
+ before do
+ project_group_link.project.add_maintainer(user)
+ end
+
+ it 'can admin group_project_link' do
+ expect(policy).to be_allowed(:admin_project_group_link)
+ end
end
end
- context 'when user is a project maintainer' do
- before do
- project_group_link.project.add_maintainer(user)
+ context 'when user is not a group owner' do
+ context 'when user is a project maintainer' do
+ it 'can admin group_project_link' do
+ project_group_link.project.add_maintainer(user)
+
+ expect(policy).to be_allowed(:admin_project_group_link)
+ end
end
- it 'can admin group_project_link' do
- expect(policy).to be_allowed(:admin_project_group_link)
+ context 'when user is not a project maintainer' do
+ it 'cannot admin group_project_link' do
+ project_group_link.project.add_developer(user)
+
+ expect(policy).to be_disallowed(:admin_project_group_link)
+ end
end
end
end
- context 'when user is not a group owner' do
- context 'when user is a project maintainer' do
- it 'can admin group_project_link' do
- project_group_link.project.add_maintainer(user)
+ describe 'read_shared_with_group' do
+ context 'when the user is a project member' do
+ before_all do
+ project.add_guest(user)
+ end
- expect(policy).to be_allowed(:admin_project_group_link)
+ it 'can read_shared_with_group' do
+ expect(policy).to be_allowed(:read_shared_with_group)
end
end
- context 'when user is not a project maintainer' do
- it 'cannot admin group_project_link' do
- project_group_link.project.add_developer(user)
+ context 'when the user is not a project member' do
+ context 'when user is not a group member' do
+ context 'when the group is private' do
+ it 'cannot read_shared_with_group' do
+ expect(policy).to be_disallowed(:read_shared_with_group)
+ end
+
+ context 'when the project is public' do
+ let_it_be(:project) { create(:project, :public) }
+
+ it 'cannot read_shared_with_group' do
+ expect(policy).to be_disallowed(:read_shared_with_group)
+ end
+ end
+ end
+
+ context 'when the group is public' do
+ let_it_be(:group2) { create(:group, :public) }
+
+ it 'can read_shared_with_group' do
+ expect(policy).to be_allowed(:read_shared_with_group)
+ end
+ end
+ end
+
+ context 'when user is a group member' do
+ before_all do
+ group2.add_guest(user)
+ end
- expect(policy).to be_disallowed(:admin_project_group_link)
+ it 'can read_shared_with_group' do
+ expect(policy).to be_allowed(:read_shared_with_group)
+ end
end
end
end
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index 3de006d8c9b..fda889ff422 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -214,6 +214,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
it 'allows modify pipelines' do
expect_allowed(:create_pipeline)
expect_allowed(:update_pipeline)
+ expect_allowed(:cancel_pipeline)
expect_allowed(:create_pipeline_schedule)
end
end
@@ -224,6 +225,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
it 'disallows to modify pipelines' do
expect_disallowed(:create_pipeline)
expect_disallowed(:update_pipeline)
+ expect_disallowed(:cancel_pipeline)
expect_disallowed(:destroy_pipeline)
expect_disallowed(:create_pipeline_schedule)
end
@@ -285,7 +287,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
it 'disallows all permissions except pipeline when the feature is disabled' do
builds_permissions = [
- :create_build, :read_build, :update_build, :admin_build, :destroy_build,
+ :create_build, :read_build, :update_build, :cancel_build, :admin_build, :destroy_build,
:create_pipeline_schedule, :read_pipeline_schedule_variables, :update_pipeline_schedule, :admin_pipeline_schedule, :destroy_pipeline_schedule,
:create_environment, :read_environment, :update_environment, :admin_environment, :destroy_environment,
:create_deployment, :read_deployment, :update_deployment, :admin_deployment, :destroy_deployment
@@ -304,7 +306,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
it 'disallows pipeline and commit_status permissions' do
builds_permissions = [
- :create_pipeline, :update_pipeline, :admin_pipeline, :destroy_pipeline,
+ :create_pipeline, :update_pipeline, :cancel_pipeline, :admin_pipeline, :destroy_pipeline,
:create_commit_status, :update_commit_status, :admin_commit_status, :destroy_commit_status
]
@@ -316,8 +318,8 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
context 'repository feature' do
let(:repository_permissions) do
[
- :create_pipeline, :update_pipeline, :admin_pipeline, :destroy_pipeline,
- :create_build, :read_build, :update_build, :admin_build, :destroy_build,
+ :create_pipeline, :update_pipeline, :cancel_pipeline, :admin_pipeline, :destroy_pipeline,
+ :create_build, :read_build, :cancel_build, :update_build, :admin_build, :destroy_build,
:create_pipeline_schedule, :read_pipeline_schedule, :update_pipeline_schedule, :admin_pipeline_schedule, :destroy_pipeline_schedule,
:create_environment, :read_environment, :update_environment, :admin_environment, :destroy_environment,
:create_cluster, :read_cluster, :update_cluster, :admin_cluster,
@@ -389,7 +391,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
end
let(:maintainer_abilities) do
- %w(create_build create_pipeline)
+ %w[create_build create_pipeline]
end
it 'does not allow pushing code' do
@@ -411,7 +413,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
end
context 'importing members from another project' do
- %w(maintainer owner).each do |role|
+ %w[maintainer owner].each do |role|
context "with #{role}" do
let(:current_user) { send(role) }
@@ -419,7 +421,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
end
end
- %w(guest reporter developer anonymous).each do |role|
+ %w[guest reporter developer anonymous].each do |role|
context "with #{role}" do
let(:current_user) { send(role) }
@@ -441,7 +443,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
end
context 'importing work items' do
- %w(reporter developer maintainer owner).each do |role|
+ %w[reporter developer maintainer owner].each do |role|
context "with #{role}" do
let(:current_user) { send(role) }
@@ -449,7 +451,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
end
end
- %w(guest anonymous).each do |role|
+ %w[guest anonymous].each do |role|
context "with #{role}" do
let(:current_user) { send(role) }
@@ -471,7 +473,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
end
context 'reading usage quotas' do
- %w(maintainer owner).each do |role|
+ %w[maintainer owner].each do |role|
context "with #{role}" do
let(:current_user) { send(role) }
@@ -479,7 +481,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
end
end
- %w(guest reporter developer anonymous).each do |role|
+ %w[guest reporter developer anonymous].each do |role|
context "with #{role}" do
let(:current_user) { send(role) }
@@ -690,7 +692,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
it { is_expected.to be_disallowed(:fork_project) }
end
- %w(reporter developer maintainer).each do |role|
+ %w[reporter developer maintainer].each do |role|
context role do
let(:current_user) { send(role) }
@@ -789,7 +791,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
end
end
- %w(guest reporter developer maintainer owner).each do |role|
+ %w[guest reporter developer maintainer owner].each do |role|
context role do
let(:current_user) { send(role) }
@@ -817,7 +819,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
end
end
- %w(guest reporter developer maintainer owner).each do |role|
+ %w[guest reporter developer maintainer owner].each do |role|
context role do
let(:current_user) { send(role) }
@@ -1437,7 +1439,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
end
context 'security configuration feature' do
- %w(guest reporter).each do |role|
+ %w[guest reporter].each do |role|
context role do
let(:current_user) { send(role) }
@@ -1447,7 +1449,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
end
end
- %w(developer maintainer owner).each do |role|
+ %w[developer maintainer owner].each do |role|
context role do
let(:current_user) { send(role) }
@@ -1459,7 +1461,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
end
context 'infrastructure google cloud feature' do
- %w(guest reporter developer).each do |role|
+ %w[guest reporter developer].each do |role|
context role do
let(:current_user) { send(role) }
@@ -1469,7 +1471,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
end
end
- %w(maintainer owner).each do |role|
+ %w[maintainer owner].each do |role|
context role do
let(:current_user) { send(role) }
@@ -1481,7 +1483,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
end
context 'infrastructure aws feature' do
- %w(guest reporter developer).each do |role|
+ %w[guest reporter developer].each do |role|
context role do
let(:current_user) { send(role) }
@@ -1491,7 +1493,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
end
end
- %w(maintainer owner).each do |role|
+ %w[maintainer owner].each do |role|
context role do
let(:current_user) { send(role) }
@@ -1972,7 +1974,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
context 'project member' do
let(:project) { private_project }
- %w(guest reporter developer maintainer).each do |role|
+ %w[guest reporter developer maintainer].each do |role|
context role do
let(:current_user) { send(role) }
@@ -2001,7 +2003,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
end
context 'project member' do
- %w(guest reporter developer maintainer).each do |role|
+ %w[guest reporter developer maintainer].each do |role|
context role do
before do
project.add_member(current_user, role.to_sym)
@@ -2035,7 +2037,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
end
context 'project member' do
- %w(guest reporter developer maintainer).each do |role|
+ %w[guest reporter developer maintainer].each do |role|
context role do
before do
project.add_member(current_user, role.to_sym)
@@ -2065,7 +2067,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
let(:current_user) { create(:user) }
context 'project member' do
- %w(guest reporter developer maintainer).each do |role|
+ %w[guest reporter developer maintainer].each do |role|
context role do
before do
project.add_member(current_user, role.to_sym)
@@ -2393,44 +2395,48 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
developer_permissions + [:create_cluster, :read_cluster, :update_cluster, :admin_cluster, :admin_terraform_state, :admin_project_google_cloud]
end
- where(:project_visibility, :access_level, :role, :allowed) do
- :public | ProjectFeature::ENABLED | :maintainer | true
- :public | ProjectFeature::ENABLED | :developer | true
- :public | ProjectFeature::ENABLED | :guest | true
- :public | ProjectFeature::ENABLED | :anonymous | true
- :public | ProjectFeature::PRIVATE | :maintainer | true
- :public | ProjectFeature::PRIVATE | :developer | true
- :public | ProjectFeature::PRIVATE | :guest | true
- :public | ProjectFeature::PRIVATE | :anonymous | false
- :public | ProjectFeature::DISABLED | :maintainer | false
- :public | ProjectFeature::DISABLED | :developer | false
- :public | ProjectFeature::DISABLED | :guest | false
- :public | ProjectFeature::DISABLED | :anonymous | false
- :internal | ProjectFeature::ENABLED | :maintainer | true
- :internal | ProjectFeature::ENABLED | :developer | true
- :internal | ProjectFeature::ENABLED | :guest | true
- :internal | ProjectFeature::ENABLED | :anonymous | false
- :internal | ProjectFeature::PRIVATE | :maintainer | true
- :internal | ProjectFeature::PRIVATE | :developer | true
- :internal | ProjectFeature::PRIVATE | :guest | true
- :internal | ProjectFeature::PRIVATE | :anonymous | false
- :internal | ProjectFeature::DISABLED | :maintainer | false
- :internal | ProjectFeature::DISABLED | :developer | false
- :internal | ProjectFeature::DISABLED | :guest | false
- :internal | ProjectFeature::DISABLED | :anonymous | false
- :private | ProjectFeature::ENABLED | :maintainer | true
- :private | ProjectFeature::ENABLED | :developer | true
- :private | ProjectFeature::ENABLED | :guest | true
- :private | ProjectFeature::ENABLED | :anonymous | false
- :private | ProjectFeature::PRIVATE | :maintainer | true
- :private | ProjectFeature::PRIVATE | :developer | true
- :private | ProjectFeature::PRIVATE | :guest | true
- :private | ProjectFeature::PRIVATE | :anonymous | false
- :private | ProjectFeature::DISABLED | :maintainer | false
- :private | ProjectFeature::DISABLED | :developer | false
- :private | ProjectFeature::DISABLED | :guest | false
- :private | ProjectFeature::DISABLED | :anonymous | false
- end
+ shared_context 'with permission matrix' do
+ where(:project_visibility, :access_level, :role, :allowed) do
+ :public | ProjectFeature::ENABLED | :maintainer | true
+ :public | ProjectFeature::ENABLED | :developer | true
+ :public | ProjectFeature::ENABLED | :guest | true
+ :public | ProjectFeature::ENABLED | :anonymous | true
+ :public | ProjectFeature::PRIVATE | :maintainer | true
+ :public | ProjectFeature::PRIVATE | :developer | true
+ :public | ProjectFeature::PRIVATE | :guest | true
+ :public | ProjectFeature::PRIVATE | :anonymous | false
+ :public | ProjectFeature::DISABLED | :maintainer | false
+ :public | ProjectFeature::DISABLED | :developer | false
+ :public | ProjectFeature::DISABLED | :guest | false
+ :public | ProjectFeature::DISABLED | :anonymous | false
+ :internal | ProjectFeature::ENABLED | :maintainer | true
+ :internal | ProjectFeature::ENABLED | :developer | true
+ :internal | ProjectFeature::ENABLED | :guest | true
+ :internal | ProjectFeature::ENABLED | :anonymous | false
+ :internal | ProjectFeature::PRIVATE | :maintainer | true
+ :internal | ProjectFeature::PRIVATE | :developer | true
+ :internal | ProjectFeature::PRIVATE | :guest | true
+ :internal | ProjectFeature::PRIVATE | :anonymous | false
+ :internal | ProjectFeature::DISABLED | :maintainer | false
+ :internal | ProjectFeature::DISABLED | :developer | false
+ :internal | ProjectFeature::DISABLED | :guest | false
+ :internal | ProjectFeature::DISABLED | :anonymous | false
+ :private | ProjectFeature::ENABLED | :maintainer | true
+ :private | ProjectFeature::ENABLED | :developer | true
+ :private | ProjectFeature::ENABLED | :guest | true
+ :private | ProjectFeature::ENABLED | :anonymous | false
+ :private | ProjectFeature::PRIVATE | :maintainer | true
+ :private | ProjectFeature::PRIVATE | :developer | true
+ :private | ProjectFeature::PRIVATE | :guest | true
+ :private | ProjectFeature::PRIVATE | :anonymous | false
+ :private | ProjectFeature::DISABLED | :maintainer | false
+ :private | ProjectFeature::DISABLED | :developer | false
+ :private | ProjectFeature::DISABLED | :guest | false
+ :private | ProjectFeature::DISABLED | :anonymous | false
+ end
+ end
+
+ include_context 'with permission matrix'
with_them do
let(:current_user) { user_subject(role) }
@@ -2448,6 +2454,8 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
end
context 'when terraform state management is disabled' do
+ include_context 'with permission matrix'
+
before do
stub_config(terraform_state: { enabled: false })
end
@@ -2562,73 +2570,154 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
describe 'when user is authenticated via CI_JOB_TOKEN', :request_store do
using RSpec::Parameterized::TableSyntax
- where(:project_visibility, :user_role, :external_user, :scope_project_type, :token_scope_enabled, :result) do
- :private | :reporter | false | :same | true | true
- :private | :reporter | false | :same | false | true
- :private | :reporter | false | :different | true | false
- :private | :reporter | false | :different | false | true
- :private | :guest | false | :same | true | true
- :private | :guest | false | :same | false | true
- :private | :guest | false | :different | true | false
- :private | :guest | false | :different | false | true
-
- :internal | :reporter | false | :same | true | true
- :internal | :reporter | true | :same | true | true
- :internal | :reporter | false | :same | false | true
- :internal | :reporter | false | :different | true | true
- :internal | :reporter | true | :different | true | false
- :internal | :reporter | false | :different | false | true
- :internal | :guest | false | :same | true | true
- :internal | :guest | true | :same | true | true
- :internal | :guest | false | :same | false | true
- :internal | :guest | false | :different | true | true
- :internal | :guest | true | :different | true | false
- :internal | :guest | false | :different | false | true
-
- :public | :reporter | false | :same | true | true
- :public | :reporter | false | :same | false | true
- :public | :reporter | false | :different | true | true
- :public | :reporter | false | :different | false | true
- :public | :guest | false | :same | true | true
- :public | :guest | false | :same | false | true
- :public | :guest | false | :different | true | true
- :public | :guest | false | :different | false | true
- end
+ RSpec.shared_examples 'CI_JOB_TOKEN enforces the expected permissions' do
+ with_them do
+ let(:current_user) { public_send(user_role) }
+ let(:project) { public_send("#{project_visibility}_project") }
+ let(:job) { build_stubbed(:ci_build, project: scope_project, user: current_user) }
- with_them do
- let(:current_user) { public_send(user_role) }
- let(:project) { public_send("#{project_visibility}_project") }
- let(:job) { build_stubbed(:ci_build, project: scope_project, user: current_user) }
+ let(:scope_project) do
+ if scope_project_type == :same
+ project
+ else
+ create(:project, :private)
+ end
+ end
- let(:scope_project) do
- if scope_project_type == :same
- project
- else
- create(:project, :private)
+ before do
+ current_user.set_ci_job_token_scope!(job)
+ current_user.external = external_user
+ project.update!(
+ ci_outbound_job_token_scope_enabled: token_scope_enabled,
+ ci_inbound_job_token_scope_enabled: token_scope_enabled
+ )
+ scope_project.update!(
+ ci_outbound_job_token_scope_enabled: token_scope_enabled,
+ ci_inbound_job_token_scope_enabled: token_scope_enabled
+ )
+ end
+
+ it "enforces the expected permissions" do
+ if result
+ is_expected.to be_allowed("#{user_role}_access".to_sym)
+ else
+ is_expected.to be_disallowed("#{user_role}_access".to_sym)
+ end
end
end
+ end
- before do
- current_user.set_ci_job_token_scope!(job)
- current_user.external = external_user
- project.update!(
- ci_outbound_job_token_scope_enabled: token_scope_enabled,
- ci_inbound_job_token_scope_enabled: token_scope_enabled
- )
- scope_project.update!(
- ci_outbound_job_token_scope_enabled: token_scope_enabled,
- ci_inbound_job_token_scope_enabled: token_scope_enabled
- )
- end
-
- it "enforces the expected permissions" do
- if result
- is_expected.to be_allowed("#{user_role}_access".to_sym)
- else
- is_expected.to be_disallowed("#{user_role}_access".to_sym)
+ # Remove project_visibility on FF restrict_ci_job_token_for_public_and_internal_projects cleanup
+ where(:project_visibility, :user_role, :external_user, :scope_project_type, :token_scope_enabled, :result) do
+ :public | :reporter | false | :same | true | true
+ :public | :reporter | true | :same | true | true
+ :public | :reporter | false | :same | false | true
+ :public | :reporter | false | :different | true | false
+ :public | :reporter | true | :different | true | false
+ :public | :reporter | false | :different | false | true
+ :public | :guest | false | :same | true | true
+ :public | :guest | true | :same | true | true
+ :public | :guest | false | :same | false | true
+ :public | :guest | false | :different | true | false
+ :public | :guest | true | :different | true | false
+ :public | :guest | false | :different | false | true
+ end
+
+ include_examples "CI_JOB_TOKEN enforces the expected permissions"
+
+ context "when the project is public or internal and not on the allowlist" do
+ where(:feature, :permissions) do
+ :container_registry | [:build_read_container_image, :read_container_image]
+ :package_registry | [:read_package, :read_project]
+ :builds | [:read_commit_status]
+ :releases | [:read_release]
+ :environments | [:read_environment]
+ end
+
+ with_them do
+ let(:current_user) { developer }
+ let(:project) { public_project }
+ let(:job) { build_stubbed(:ci_build, project: scope_project, user: current_user) }
+ let_it_be(:scope_project) { create(:project, :private) }
+
+ before do
+ current_user.set_ci_job_token_scope!(job)
+
+ scope_project.update!(ci_inbound_job_token_scope_enabled: true)
+ end
+
+ it 'allows the permissions based on the feature access level' do
+ project.project_feature.update!("#{feature}_access_level": ProjectFeature::ENABLED)
+
+ permissions.each { |p| expect_allowed(p) }
+ end
+
+ it 'disallows the permissions if feature access level is restricted' do
+ project.project_feature.update!("#{feature}_access_level": ProjectFeature::PRIVATE)
+
+ permissions.each { |p| expect_disallowed(p) }
+ end
+
+ it 'disallows the permissions if feature access level is disabled' do
+ project.project_feature.update!("#{feature}_access_level": ProjectFeature::DISABLED)
+
+ permissions.each { |p| expect_disallowed(p) }
+ end
+
+ context "with restrict_ci_job_token_for_public_and_internal_projects disabled" do
+ before do
+ stub_feature_flags(restrict_ci_job_token_for_public_and_internal_projects: false)
+ end
+
+ it 'allows all permissions for private' do
+ project.project_feature.update!("#{feature}_access_level": ProjectFeature::PRIVATE)
+
+ permissions.each { |p| expect_allowed(p) }
+ end
end
end
end
+
+ context "with FF restrict_ci_job_token_for_public_and_internal_projects disabled" do
+ before do
+ stub_feature_flags(restrict_ci_job_token_for_public_and_internal_projects: false)
+ end
+
+ where(:project_visibility, :user_role, :external_user, :scope_project_type, :token_scope_enabled, :result) do
+ :private | :reporter | false | :same | true | true
+ :private | :reporter | false | :same | false | true
+ :private | :reporter | false | :different | true | false
+ :private | :reporter | false | :different | false | true
+ :private | :guest | false | :same | true | true
+ :private | :guest | false | :same | false | true
+ :private | :guest | false | :different | true | false
+ :private | :guest | false | :different | false | true
+
+ :internal | :reporter | false | :same | true | true
+ :internal | :reporter | true | :same | true | true
+ :internal | :reporter | false | :same | false | true
+ :internal | :reporter | false | :different | true | true
+ :internal | :reporter | true | :different | true | false
+ :internal | :reporter | false | :different | false | true
+ :internal | :guest | false | :same | true | true
+ :internal | :guest | true | :same | true | true
+ :internal | :guest | false | :same | false | true
+ :internal | :guest | false | :different | true | true
+ :internal | :guest | true | :different | true | false
+ :internal | :guest | false | :different | false | true
+
+ :public | :reporter | false | :same | true | true
+ :public | :reporter | false | :same | false | true
+ :public | :reporter | false | :different | true | true
+ :public | :reporter | false | :different | false | true
+ :public | :guest | false | :same | true | true
+ :public | :guest | false | :same | false | true
+ :public | :guest | false | :different | true | true
+ :public | :guest | false | :different | false | true
+ end
+
+ include_examples "CI_JOB_TOKEN enforces the expected permissions"
+ end
end
describe 'container_image policies' do
@@ -2825,7 +2914,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
end
end
- %w(guest reporter developer).each do |role|
+ %w[guest reporter developer].each do |role|
context role do
let(:current_user) { send(role) }
@@ -2833,7 +2922,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
end
end
- %w(maintainer owner).each do |role|
+ %w[maintainer owner].each do |role|
context role do
let(:current_user) { send(role) }
@@ -3212,9 +3301,23 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
end
describe 'add_catalog_resource' do
- let(:current_user) { owner }
+ using RSpec::Parameterized::TableSyntax
- specify { is_expected.to be_disallowed(:read_namespace_catalog) }
+ let(:current_user) { public_send(role) }
+
+ where(:role, :allowed) do
+ :owner | true
+ :maintainer | false
+ :developer | false
+ :reporter | false
+ :guest | false
+ end
+
+ with_them do
+ it do
+ expect(subject.can?(:add_catalog_resource)).to be(allowed)
+ end
+ end
end
describe 'read_model_registry' do
@@ -3237,6 +3340,28 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
end
end
+ describe 'write_model_registry' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:ff_model_registry_enabled, :current_user, :allowed) do
+ true | ref(:reporter) | true
+ true | ref(:guest) | false
+ false | ref(:owner) | false
+ end
+ with_them do
+ before do
+ stub_feature_flags(model_registry: false)
+ stub_feature_flags(model_registry: project) if ff_model_registry_enabled
+ end
+
+ if params[:allowed]
+ it { expect_allowed(:write_model_registry) }
+ else
+ it { expect_disallowed(:write_model_registry) }
+ end
+ end
+ end
+
describe ':read_model_experiments' do
using RSpec::Parameterized::TableSyntax
diff --git a/spec/policies/user_policy_spec.rb b/spec/policies/user_policy_spec.rb
index 9a2caeb7435..bbfd231ed38 100644
--- a/spec/policies/user_policy_spec.rb
+++ b/spec/policies/user_policy_spec.rb
@@ -247,6 +247,32 @@ RSpec.describe UserPolicy do
end
end
+ describe ':read_user_organizations' do
+ context 'when user is admin' do
+ let(:current_user) { admin }
+
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it { is_expected.to be_allowed(:read_user_organizations) }
+ end
+
+ context 'when admin mode is disabled' do
+ it { is_expected.not_to be_allowed(:read_user_organizations) }
+ end
+ end
+
+ context 'when user is not an admin' do
+ context 'requesting their own organizations' do
+ subject { described_class.new(current_user, current_user) }
+
+ it { is_expected.to be_allowed(:read_user_organizations) }
+ end
+
+ context "requesting a different user's orgnanizations" do
+ it { is_expected.not_to be_allowed(:read_user_organizations) }
+ end
+ end
+ end
+
describe ':read_user_email_address' do
context 'when user is admin' do
let(:current_user) { admin }
diff --git a/spec/policies/work_item_policy_spec.rb b/spec/policies/work_item_policy_spec.rb
index 568c375ce56..5b2eb8ec2e8 100644
--- a/spec/policies/work_item_policy_spec.rb
+++ b/spec/policies/work_item_policy_spec.rb
@@ -309,4 +309,76 @@ RSpec.describe WorkItemPolicy, feature_category: :team_planning do
end
end
end
+
+ describe 'read_note' do
+ context 'when work item is associated with a project' do
+ context 'when project is public' do
+ let(:work_item_subject) { public_work_item }
+
+ context 'when user is not a member of the project' do
+ let(:current_user) { non_member_user }
+
+ it { is_expected.to be_allowed(:read_note) }
+ end
+
+ context 'when user is a member of the project' do
+ let(:current_user) { guest_author }
+
+ it { is_expected.to be_allowed(:read_note) }
+
+ context 'when work_item is confidential' do
+ let(:work_item_subject) { create(:work_item, :confidential, project: project) }
+
+ it { is_expected.not_to be_allowed(:read_note) }
+ end
+ end
+ end
+ end
+
+ context 'when work item is associated with a group' do
+ context 'when group is public' do
+ let_it_be(:public_group) { create(:group, :public) }
+ let_it_be(:public_group_work_item) { create(:work_item, :group_level, namespace: public_group) }
+ let_it_be(:public_group_member) { create(:user).tap { |u| public_group.add_reporter(u) } }
+ let(:work_item_subject) { public_group_work_item }
+
+ context 'when user is not a member of the group' do
+ let(:current_user) { non_member_user }
+
+ it { is_expected.not_to be_allowed(:read_note) }
+ end
+
+ context 'when user is a member of the group' do
+ let(:current_user) { public_group_member }
+
+ it { is_expected.to be_allowed(:read_note) }
+ end
+ end
+
+ context 'when group is not public' do
+ let_it_be(:private_group) { create(:group, :private) }
+ let_it_be(:private_group_work_item) { create(:work_item, :group_level, namespace: private_group) }
+ let_it_be(:private_group_reporter) { create(:user).tap { |u| private_group.add_reporter(u) } }
+ let(:work_item_subject) { private_group_work_item }
+
+ context 'when user is not a member of the group' do
+ let(:current_user) { non_member_user }
+
+ it { is_expected.not_to be_allowed(:read_note) }
+ end
+
+ context 'when user is a member of the group' do
+ let(:current_user) { private_group_reporter }
+
+ it { is_expected.to be_allowed(:read_note) }
+
+ context 'when work_item is confidential' do
+ let(:work_item_subject) { create(:work_item, :group_level, :confidential, namespace: private_group) }
+
+ it { is_expected.to be_allowed(:read_note) }
+ end
+ end
+ end
+ end
+ end
end
diff --git a/spec/presenters/ci/pipeline_artifacts/code_coverage_presenter_spec.rb b/spec/presenters/ci/pipeline_artifacts/code_coverage_presenter_spec.rb
index e679f5fa144..2973040dd6a 100644
--- a/spec/presenters/ci/pipeline_artifacts/code_coverage_presenter_spec.rb
+++ b/spec/presenters/ci/pipeline_artifacts/code_coverage_presenter_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe Ci::PipelineArtifacts::CodeCoveragePresenter do
context 'when code coverage has data' do
context 'when filenames is empty' do
- let(:filenames) { %w() }
+ let(:filenames) { %w[] }
it 'returns hash without coverage' do
expect(subject).to match(files: {})
@@ -20,7 +20,7 @@ RSpec.describe Ci::PipelineArtifacts::CodeCoveragePresenter do
end
context 'when filenames do not match code coverage data' do
- let(:filenames) { %w(demo.rb) }
+ let(:filenames) { %w[demo.rb] }
it 'returns hash without coverage' do
expect(subject).to match(files: {})
@@ -29,7 +29,7 @@ RSpec.describe Ci::PipelineArtifacts::CodeCoveragePresenter do
context 'when filenames matches code coverage data' do
context 'when asking for one filename' do
- let(:filenames) { %w(file_a.rb) }
+ let(:filenames) { %w[file_a.rb] }
it 'returns coverage for the given filename' do
expect(subject).to match(files: { "file_a.rb" => { "1" => 1, "2" => 1, "3" => 1 } })
@@ -37,7 +37,7 @@ RSpec.describe Ci::PipelineArtifacts::CodeCoveragePresenter do
end
context 'when asking for multiple filenames' do
- let(:filenames) { %w(file_a.rb file_b.rb) }
+ let(:filenames) { %w[file_a.rb file_b.rb] }
it 'returns coverage for a the given filenames' do
expect(subject).to match(
diff --git a/spec/presenters/ci/pipeline_artifacts/code_quality_mr_diff_presenter_spec.rb b/spec/presenters/ci/pipeline_artifacts/code_quality_mr_diff_presenter_spec.rb
index 99c82795210..f4f0990240d 100644
--- a/spec/presenters/ci/pipeline_artifacts/code_quality_mr_diff_presenter_spec.rb
+++ b/spec/presenters/ci/pipeline_artifacts/code_quality_mr_diff_presenter_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe Ci::PipelineArtifacts::CodeQualityMrDiffPresenter, feature_catego
context 'when code quality has data' do
context 'when filenames is empty' do
- let(:filenames) { %w() }
+ let(:filenames) { %w[] }
it 'returns hash without quality' do
expect(quality_data).to match(files: {})
@@ -21,7 +21,7 @@ RSpec.describe Ci::PipelineArtifacts::CodeQualityMrDiffPresenter, feature_catego
end
context 'when filenames do not match code quality data' do
- let(:filenames) { %w(demo.rb) }
+ let(:filenames) { %w[demo.rb] }
it 'returns hash without quality' do
expect(quality_data).to match(files: {})
@@ -30,7 +30,7 @@ RSpec.describe Ci::PipelineArtifacts::CodeQualityMrDiffPresenter, feature_catego
context 'when filenames matches code quality data' do
context 'when asking for one filename' do
- let(:filenames) { %w(file_a.rb) }
+ let(:filenames) { %w[file_a.rb] }
it 'returns quality for the given filename' do
expect(quality_data).to match(
@@ -45,7 +45,7 @@ RSpec.describe Ci::PipelineArtifacts::CodeQualityMrDiffPresenter, feature_catego
end
context 'when asking for multiple filenames' do
- let(:filenames) { %w(file_a.rb file_b.rb) }
+ let(:filenames) { %w[file_a.rb file_b.rb] }
it 'returns quality for the given filenames' do
expect(quality_data).to match(
diff --git a/spec/presenters/clusters/cluster_presenter_spec.rb b/spec/presenters/clusters/cluster_presenter_spec.rb
index 755f1ea6078..aacb696a88e 100644
--- a/spec/presenters/clusters/cluster_presenter_spec.rb
+++ b/spec/presenters/clusters/cluster_presenter_spec.rb
@@ -123,7 +123,7 @@ RSpec.describe Clusters::ClusterPresenter do
'clusters-path': clusterable_presenter.index_path,
'dashboard-endpoint': clusterable_presenter.metrics_dashboard_path(cluster),
'documentation-path': help_page_path('user/infrastructure/clusters/manage/clusters_health'),
- 'add-dashboard-documentation-path': help_page_path('operations/metrics/dashboards/index.md', anchor: 'add-a-new-dashboard-to-your-project'),
+ 'add-dashboard-documentation-path': help_page_path('operations/metrics/dashboards/index', anchor: 'add-a-new-dashboard-to-your-project'),
'empty-getting-started-svg-path': match_asset_path('/assets/illustrations/monitoring/getting_started.svg'),
'empty-loading-svg-path': match_asset_path('/assets/illustrations/monitoring/loading.svg'),
'empty-no-data-svg-path': match_asset_path('/assets/illustrations/monitoring/no_data.svg'),
diff --git a/spec/presenters/ml/model_presenter_spec.rb b/spec/presenters/ml/model_presenter_spec.rb
index 88bfa9eb4c6..31bf4e7ad6c 100644
--- a/spec/presenters/ml/model_presenter_spec.rb
+++ b/spec/presenters/ml/model_presenter_spec.rb
@@ -6,6 +6,7 @@ RSpec.describe Ml::ModelPresenter, feature_category: :mlops do
let_it_be(:project) { build_stubbed(:project) }
let_it_be(:model1) { build_stubbed(:ml_models, project: project) }
let_it_be(:model2) { build_stubbed(:ml_models, :with_latest_version_and_package, project: project) }
+ let_it_be(:model3) { build_stubbed(:ml_models, :with_versions, project: project) }
describe '#latest_version_name' do
subject { model.present.latest_version_name }
@@ -25,6 +26,22 @@ RSpec.describe Ml::ModelPresenter, feature_category: :mlops do
end
end
+ describe '#version_count' do
+ subject { model3.present.version_count }
+
+ it { is_expected.to eq(2) }
+
+ context 'when model has precomputed version count' do
+ before do
+ allow(model3).to receive(:version_count).and_return(1)
+ end
+
+ it 'returns the value of model version count' do
+ is_expected.to eq(1)
+ end
+ end
+ end
+
describe '#latest_package_path' do
subject { model.present.latest_package_path }
@@ -41,6 +58,22 @@ RSpec.describe Ml::ModelPresenter, feature_category: :mlops do
end
end
+ describe '#latest_version_path' do
+ subject { model.present.latest_version_path }
+
+ context 'when model version does not have package' do
+ let(:model) { model1 }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'when latest model version has package' do
+ let(:model) { model2 }
+
+ it { is_expected.to eq("/#{project.full_path}/-/ml/models/#{model.id}/versions/#{model.latest_version.id}") }
+ end
+ end
+
describe '#path' do
subject { model1.present.path }
diff --git a/spec/presenters/ml/model_version_presenter_spec.rb b/spec/presenters/ml/model_version_presenter_spec.rb
new file mode 100644
index 00000000000..7624aaffc7a
--- /dev/null
+++ b/spec/presenters/ml/model_version_presenter_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ml::ModelVersionPresenter, feature_category: :mlops do
+ let_it_be(:project) { build_stubbed(:project) }
+ let_it_be(:model) { build_stubbed(:ml_models, name: 'a_model', project: project) }
+ let_it_be(:model_version) { build_stubbed(:ml_model_versions, :with_package, model: model, version: '1.1.1') }
+ let_it_be(:presenter) { model_version.present }
+
+ describe '.display_name' do
+ subject { presenter.display_name }
+
+ it { is_expected.to eq('a_model / 1.1.1') }
+ end
+
+ describe '#path' do
+ subject { presenter.path }
+
+ it { is_expected.to eq("/#{project.full_path}/-/ml/models/#{model.id}/versions/#{model_version.id}") }
+ end
+
+ describe '#package_path' do
+ subject { presenter.package_path }
+
+ it { is_expected.to eq("/#{project.full_path}/-/packages/#{model_version.package_id}") }
+ end
+end
diff --git a/spec/presenters/packages/nuget/packages_metadata_presenter_spec.rb b/spec/presenters/packages/nuget/packages_metadata_presenter_spec.rb
index 38b33a0ec4b..c7bdff9dd61 100644
--- a/spec/presenters/packages/nuget/packages_metadata_presenter_spec.rb
+++ b/spec/presenters/packages/nuget/packages_metadata_presenter_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe Packages::Nuget::PackagesMetadataPresenter, feature_category: :pa
end
describe '#items' do
- let(:tag_names) { %w(tag1 tag2) }
+ let(:tag_names) { %w[tag1 tag2] }
subject { presenter.items }
diff --git a/spec/presenters/packages/nuget/search_results_presenter_spec.rb b/spec/presenters/packages/nuget/search_results_presenter_spec.rb
index e761a8740ef..7501cb75682 100644
--- a/spec/presenters/packages/nuget/search_results_presenter_spec.rb
+++ b/spec/presenters/packages/nuget/search_results_presenter_spec.rb
@@ -25,7 +25,7 @@ RSpec.describe Packages::Nuget::SearchResultsPresenter, feature_category: :packa
it 'returns the proper data structure' do
expect(data.size).to eq 3
pkg_a, pkg_b, pkg_c = data
- expect_package_result(pkg_a, package_a.name, [package_a.version], %w(tag1 tag2), with_metadatum: true)
+ expect_package_result(pkg_a, package_a.name, [package_a.version], %w[tag1 tag2], with_metadatum: true)
expect_package_result(pkg_b, packages_b.first.name, packages_b.map(&:version))
expect_package_result(pkg_c, packages_c.first.name, packages_c.map(&:version))
end
diff --git a/spec/presenters/project_presenter_spec.rb b/spec/presenters/project_presenter_spec.rb
index 42c43a59fe2..48db41ea8e3 100644
--- a/spec/presenters/project_presenter_spec.rb
+++ b/spec/presenters/project_presenter_spec.rb
@@ -872,4 +872,26 @@ RSpec.describe ProjectPresenter do
end
end
end
+
+ describe '#has_review_app?' do
+ subject { presenter.has_review_app? }
+
+ let_it_be(:project) { create(:project, :repository) }
+
+ context 'when review apps exist' do
+ let!(:environment) do
+ create(:environment, :with_review_app, project: project)
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when review apps do not exist' do
+ let!(:environment) do
+ create(:environment, project: project)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
end
diff --git a/spec/presenters/projects/security/configuration_presenter_spec.rb b/spec/presenters/projects/security/configuration_presenter_spec.rb
index beabccf6639..fcd170dfd66 100644
--- a/spec/presenters/projects/security/configuration_presenter_spec.rb
+++ b/spec/presenters/projects/security/configuration_presenter_spec.rb
@@ -177,7 +177,7 @@ RSpec.describe Projects::Security::ConfigurationPresenter, feature_category: :so
let!(:ci_config) do
project.repository.create_file(
project.creator,
- Gitlab::FileDetector::PATTERNS[:gitlab_ci],
+ project.ci_config_path_or_default,
'contents go here',
message: 'test',
branch_name: 'master')
diff --git a/spec/presenters/user_presenter_spec.rb b/spec/presenters/user_presenter_spec.rb
index d1124d73dbd..fcbadf40bc9 100644
--- a/spec/presenters/user_presenter_spec.rb
+++ b/spec/presenters/user_presenter_spec.rb
@@ -61,28 +61,14 @@ RSpec.describe UserPresenter do
let_it_be(:other_user) { create(:user) }
let_it_be(:saved_reply) { create(:saved_reply, user: user) }
- context 'when feature is disabled' do
- before do
- stub_feature_flags(saved_replies: false)
- end
+ context 'when user has no permission to read saved replies' do
+ let(:current_user) { other_user }
it { expect(presenter.saved_replies).to eq(::Users::SavedReply.none) }
end
- context 'when feature is enabled' do
- before do
- stub_feature_flags(saved_replies: current_user)
- end
-
- context 'when user has no permission to read saved replies' do
- let(:current_user) { other_user }
-
- it { expect(presenter.saved_replies).to eq(::Users::SavedReply.none) }
- end
-
- context 'when user has permission to read saved replies' do
- it { expect(presenter.saved_replies).to eq([saved_reply]) }
- end
+ context 'when user has permission to read saved replies' do
+ it { expect(presenter.saved_replies).to eq([saved_reply]) }
end
end
end
diff --git a/spec/requests/acme_challenges_controller_spec.rb b/spec/requests/acme_challenges_controller_spec.rb
deleted file mode 100644
index f37aefed488..00000000000
--- a/spec/requests/acme_challenges_controller_spec.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe AcmeChallengesController, type: :request, feature_category: :pages do
- it_behaves_like 'Base action controller' do
- subject(:request) { get acme_challenge_path }
- end
-end
diff --git a/spec/requests/admin/users_controller_spec.rb b/spec/requests/admin/users_controller_spec.rb
index e525d615b50..2f8025691f4 100644
--- a/spec/requests/admin/users_controller_spec.rb
+++ b/spec/requests/admin/users_controller_spec.rb
@@ -74,4 +74,54 @@ RSpec.describe Admin::UsersController, :enable_admin_mode, feature_category: :us
expect { request }.to change { user.reload.access_locked? }.from(true).to(false)
end
end
+
+ describe 'PUT #trust' do
+ subject(:request) { put trust_admin_user_path(user) }
+
+ it 'trusts the user' do
+ expect { request }.to change { user.reload.trusted? }.from(false).to(true)
+ end
+
+ context 'when setting trust fails' do
+ before do
+ allow_next_instance_of(Users::TrustService) do |instance|
+ allow(instance).to receive(:execute).and_return({ status: :failed })
+ end
+ end
+
+ it 'displays a flash alert' do
+ request
+
+ expect(response).to redirect_to(admin_user_path(user))
+ expect(flash[:alert]).to eq(s_('Error occurred. User was not updated'))
+ end
+ end
+ end
+
+ describe 'PUT #untrust' do
+ before do
+ user.custom_attributes.create!(key: UserCustomAttribute::TRUSTED_BY, value: "placeholder")
+ end
+
+ subject(:request) { put untrust_admin_user_path(user) }
+
+ it 'trusts the user' do
+ expect { request }.to change { user.reload.trusted? }.from(true).to(false)
+ end
+
+ context 'when untrusting fails' do
+ before do
+ allow_next_instance_of(Users::UntrustService) do |instance|
+ allow(instance).to receive(:execute).and_return({ status: :failed })
+ end
+ end
+
+ it 'displays a flash alert' do
+ request
+
+ expect(response).to redirect_to(admin_user_path(user))
+ expect(flash[:alert]).to eq(s_('Error occurred. User was not updated'))
+ end
+ end
+ end
end
diff --git a/spec/requests/api/badges_spec.rb b/spec/requests/api/badges_spec.rb
index 0b340b95b20..92c5e90027b 100644
--- a/spec/requests/api/badges_spec.rb
+++ b/spec/requests/api/badges_spec.rb
@@ -363,7 +363,7 @@ RSpec.describe API::Badges, feature_category: :groups_and_projects do
end
describe 'Endpoints' do
- %w(project group).each do |source_type|
+ %w[project group].each do |source_type|
it_behaves_like 'GET /:sources/:id/badges', source_type
it_behaves_like 'GET /:sources/:id/badges/:badge_id', source_type
it_behaves_like 'GET /:sources/:id/badges/render', source_type
diff --git a/spec/requests/api/bulk_imports_spec.rb b/spec/requests/api/bulk_imports_spec.rb
index d3d4a723616..bbc01b30361 100644
--- a/spec/requests/api/bulk_imports_spec.rb
+++ b/spec/requests/api/bulk_imports_spec.rb
@@ -394,7 +394,7 @@ RSpec.describe API::BulkImports, feature_category: :importers do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.pluck('id')).to contain_exactly(entity_3.id)
- expect(json_response.first['failures'].first['exception_class']).to eq(failure_3.exception_class)
+ expect(json_response.first['failures'].first['exception_message']).to eq(failure_3.exception_message)
end
it_behaves_like 'disabled feature'
@@ -420,4 +420,17 @@ RSpec.describe API::BulkImports, feature_category: :importers do
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
+
+ describe 'GET /bulk_imports/:id/entities/:entity_id/failures' do
+ let(:request) { get api("/bulk_imports/#{import_2.id}/entities/#{entity_3.id}/failures", user) }
+
+ it 'returns specified entity failures' do
+ request
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response.first['exception_message']).to eq(failure_3.exception_message)
+ end
+
+ it_behaves_like 'disabled feature'
+ end
end
diff --git a/spec/requests/api/ci/job_artifacts_spec.rb b/spec/requests/api/ci/job_artifacts_spec.rb
index 6f4e7fd66ed..b96ba356855 100644
--- a/spec/requests/api/ci/job_artifacts_spec.rb
+++ b/spec/requests/api/ci/job_artifacts_spec.rb
@@ -14,9 +14,7 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do
end
let_it_be(:pipeline, reload: true) do
- create(:ci_pipeline, project: project,
- sha: project.commit.id,
- ref: project.default_branch)
+ create(:ci_pipeline, project: project, sha: project.commit.id, ref: project.default_branch)
end
let(:user) { create(:user) }
@@ -179,8 +177,7 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do
context 'when project is public' do
it 'allows to access artifacts' do
- project.update_column(:visibility_level,
- Gitlab::VisibilityLevel::PUBLIC)
+ project.update_column(:visibility_level, Gitlab::VisibilityLevel::PUBLIC)
project.update_column(:public_builds, true)
get_artifact_file(artifact)
@@ -193,8 +190,7 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do
let(:job) { create(:ci_build, :artifacts, :with_private_artifacts_config, pipeline: pipeline) }
it 'rejects access to artifacts' do
- project.update_column(:visibility_level,
- Gitlab::VisibilityLevel::PUBLIC)
+ project.update_column(:visibility_level, Gitlab::VisibilityLevel::PUBLIC)
project.update_column(:public_builds, true)
get_artifact_file(artifact)
@@ -208,8 +204,7 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do
end
it 'allows access to artifacts' do
- project.update_column(:visibility_level,
- Gitlab::VisibilityLevel::PUBLIC)
+ project.update_column(:visibility_level, Gitlab::VisibilityLevel::PUBLIC)
project.update_column(:public_builds, true)
get_artifact_file(artifact)
@@ -221,8 +216,7 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do
context 'when project is public with builds access disabled' do
it 'rejects access to artifacts' do
- project.update_column(:visibility_level,
- Gitlab::VisibilityLevel::PUBLIC)
+ project.update_column(:visibility_level, Gitlab::VisibilityLevel::PUBLIC)
project.update_column(:public_builds, false)
get_artifact_file(artifact)
@@ -233,8 +227,7 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do
context 'when project is private' do
it 'rejects access and hides existence of artifacts' do
- project.update_column(:visibility_level,
- Gitlab::VisibilityLevel::PRIVATE)
+ project.update_column(:visibility_level, Gitlab::VisibilityLevel::PRIVATE)
project.update_column(:public_builds, true)
get_artifact_file(artifact)
@@ -254,8 +247,7 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do
expect(response).to have_gitlab_http_status(:ok)
expect(response.headers.to_h)
- .to include('Content-Type' => 'application/json',
- 'Gitlab-Workhorse-Send-Data' => /artifacts-entry/)
+ .to include('Content-Type' => 'application/json', 'Gitlab-Workhorse-Send-Data' => /artifacts-entry/)
expect(response.headers.to_h)
.not_to include('Gitlab-Workhorse-Detect-Content-Type' => 'true')
expect(response.parsed_body).to be_empty
@@ -404,10 +396,12 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do
end
before do
- stub_object_storage_uploader(config: Gitlab.config.artifacts.object_store,
- uploader: JobArtifactUploader,
- proxy_download: proxy_download,
- cdn: cdn_config)
+ stub_object_storage_uploader(
+ config: Gitlab.config.artifacts.object_store,
+ uploader: JobArtifactUploader,
+ proxy_download: proxy_download,
+ cdn: cdn_config
+ )
allow(Gitlab::ApplicationContext).to receive(:push).and_call_original
end
@@ -624,10 +618,11 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do
it 'allows to access artifacts', :sidekiq_might_not_need_inline do
expect(response).to have_gitlab_http_status(:ok)
- expect(response.headers.to_h)
- .to include('Content-Type' => 'application/json',
- 'Gitlab-Workhorse-Send-Data' => /artifacts-entry/,
- 'Gitlab-Workhorse-Detect-Content-Type' => 'true')
+ expect(response.headers.to_h).to include(
+ 'Content-Type' => 'application/json',
+ 'Gitlab-Workhorse-Send-Data' => /artifacts-entry/,
+ 'Gitlab-Workhorse-Detect-Content-Type' => 'true'
+ )
end
end
@@ -695,10 +690,11 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do
get_artifact_file(artifact)
expect(response).to have_gitlab_http_status(:ok)
- expect(response.headers.to_h)
- .to include('Content-Type' => 'application/json',
- 'Gitlab-Workhorse-Send-Data' => /artifacts-entry/,
- 'Gitlab-Workhorse-Detect-Content-Type' => 'true')
+ expect(response.headers.to_h).to include(
+ 'Content-Type' => 'application/json',
+ 'Gitlab-Workhorse-Send-Data' => /artifacts-entry/,
+ 'Gitlab-Workhorse-Detect-Content-Type' => 'true'
+ )
expect(response.parsed_body).to be_empty
end
end
@@ -713,10 +709,11 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do
get_artifact_file(artifact, 'improve/awesome')
expect(response).to have_gitlab_http_status(:ok)
- expect(response.headers.to_h)
- .to include('Content-Type' => 'application/json',
- 'Gitlab-Workhorse-Send-Data' => /artifacts-entry/,
- 'Gitlab-Workhorse-Detect-Content-Type' => 'true')
+ expect(response.headers.to_h).to include(
+ 'Content-Type' => 'application/json',
+ 'Gitlab-Workhorse-Send-Data' => /artifacts-entry/,
+ 'Gitlab-Workhorse-Detect-Content-Type' => 'true'
+ )
end
end
@@ -765,8 +762,15 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do
context 'artifacts did not expire' do
let(:job) do
- create(:ci_build, :trace_artifact, :artifacts, :success,
- project: project, pipeline: pipeline, artifacts_expire_at: Time.now + 7.days)
+ create(
+ :ci_build,
+ :trace_artifact,
+ :artifacts,
+ :success,
+ project: project,
+ pipeline: pipeline,
+ artifacts_expire_at: Time.now + 7.days
+ )
end
it 'keeps artifacts' do
diff --git a/spec/requests/api/ci/jobs_spec.rb b/spec/requests/api/ci/jobs_spec.rb
index 41e35de189e..382aabd45a1 100644
--- a/spec/requests/api/ci/jobs_spec.rb
+++ b/spec/requests/api/ci/jobs_spec.rb
@@ -14,9 +14,7 @@ RSpec.describe API::Ci::Jobs, feature_category: :continuous_integration do
end
let_it_be(:pipeline, reload: true) do
- create(:ci_pipeline, project: project,
- sha: project.commit.id,
- ref: project.default_branch)
+ create(:ci_pipeline, project: project, sha: project.commit.id, ref: project.default_branch)
end
let(:user) { create(:user) }
@@ -25,10 +23,14 @@ RSpec.describe API::Ci::Jobs, feature_category: :continuous_integration do
let(:guest) { create(:project_member, :guest, project: project).user }
let(:running_job) do
- create(:ci_build, :running, project: project,
- user: user,
- pipeline: pipeline,
- artifacts_expire_at: 1.day.since)
+ create(
+ :ci_build,
+ :running,
+ project: project,
+ user: user,
+ pipeline: pipeline,
+ artifacts_expire_at: 1.day.since
+ )
end
let!(:job) do
@@ -266,7 +268,7 @@ RSpec.describe API::Ci::Jobs, feature_category: :continuous_integration do
expect(json_response.dig('project', 'groups')).to match_array([{ 'id' => group.id }])
expect(json_response.dig('user', 'id')).to eq(api_user.id)
expect(json_response.dig('user', 'username')).to eq(api_user.username)
- expect(json_response.dig('user', 'roles_in_project')).to match_array %w(guest reporter developer)
+ expect(json_response.dig('user', 'roles_in_project')).to match_array %w[guest reporter developer]
expect(json_response).not_to include('environment')
end
@@ -450,7 +452,7 @@ RSpec.describe API::Ci::Jobs, feature_category: :continuous_integration do
end
context 'filter project with array of scope elements' do
- let(:query) { { scope: %w(pending running) } }
+ let(:query) { { scope: %w[pending running] } }
it do
expect(response).to have_gitlab_http_status(:ok)
@@ -459,7 +461,7 @@ RSpec.describe API::Ci::Jobs, feature_category: :continuous_integration do
end
context 'respond 400 when scope contains invalid state' do
- let(:query) { { scope: %w(unknown running) } }
+ let(:query) { { scope: %w[unknown running] } }
it { expect(response).to have_gitlab_http_status(:bad_request) }
end
@@ -789,14 +791,14 @@ RSpec.describe API::Ci::Jobs, feature_category: :continuous_integration do
end
context 'authorized user' do
- context 'user with :update_build persmission' do
+ context 'user with :cancel_build permission' do
it 'cancels running or pending job' do
expect(response).to have_gitlab_http_status(:created)
expect(project.builds.first.status).to eq('success')
end
end
- context 'user without :update_build permission' do
+ context 'user without :cancel_build permission' do
let(:api_user) { reporter }
it 'does not cancel job' do
diff --git a/spec/requests/api/ci/pipelines_spec.rb b/spec/requests/api/ci/pipelines_spec.rb
index 3544a6dd72a..eef125e1bc3 100644
--- a/spec/requests/api/ci/pipelines_spec.rb
+++ b/spec/requests/api/ci/pipelines_spec.rb
@@ -13,8 +13,14 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
end
let_it_be(:pipeline) do
- create(:ci_empty_pipeline, project: project, sha: project.commit.id,
- ref: project.default_branch, user: user, name: 'Build pipeline')
+ create(
+ :ci_empty_pipeline,
+ project: project,
+ sha: project.commit.id,
+ ref: project.default_branch,
+ user: user,
+ name: 'Build pipeline'
+ )
end
before do
@@ -357,8 +363,13 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
let(:query) { {} }
let(:api_user) { user }
let_it_be(:job) do
- create(:ci_build, :success, name: 'build', pipeline: pipeline,
- artifacts_expire_at: 1.day.since)
+ create(
+ :ci_build,
+ :success,
+ name: 'build',
+ pipeline: pipeline,
+ artifacts_expire_at: 1.day.since
+ )
end
let(:guest) { create(:project_member, :guest, project: project).user }
@@ -436,7 +447,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
end
context 'filter jobs with array of scope elements' do
- let(:query) { { scope: %w(pending running) } }
+ let(:query) { { scope: %w[pending running] } }
it :aggregate_failures do
expect(response).to have_gitlab_http_status(:ok)
@@ -445,7 +456,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
end
context 'respond 400 when scope contains invalid state' do
- let(:query) { { scope: %w(unknown running) } }
+ let(:query) { { scope: %w[unknown running] } }
it { expect(response).to have_gitlab_http_status(:bad_request) }
end
@@ -540,12 +551,14 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
let(:downstream_pipeline) { create(:ci_pipeline) }
let!(:pipeline_source) do
- create(:ci_sources_pipeline,
- source_pipeline: pipeline,
- source_project: project,
- source_job: bridge,
- pipeline: downstream_pipeline,
- project: downstream_pipeline.project)
+ create(
+ :ci_sources_pipeline,
+ source_pipeline: pipeline,
+ source_project: project,
+ source_job: bridge,
+ pipeline: downstream_pipeline,
+ project: downstream_pipeline.project
+ )
end
let(:query) { {} }
@@ -615,7 +628,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
end
context 'with array of scope elements' do
- let(:query) { { scope: %w(pending running) } }
+ let(:query) { { scope: %w[pending running] } }
it :skip_before_request, :aggregate_failures do
get api("/projects/#{project.id}/pipelines/#{pipeline.id}/bridges", api_user), params: query
@@ -623,14 +636,14 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an Array
expect(json_response.count).to eq 2
- json_response.each { |r| expect(%w(pending running).include?(r['status'])).to be true }
+ json_response.each { |r| expect(%w[pending running].include?(r['status'])).to be true }
end
end
end
context 'respond 400 when scope contains invalid state' do
context 'in an array' do
- let(:query) { { scope: %w(unknown running) } }
+ let(:query) { { scope: %w[unknown running] } }
it { expect(response).to have_gitlab_http_status(:bad_request) }
end
@@ -713,12 +726,14 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
def create_bridge(pipeline, status = :created)
create(:ci_bridge, status: status, pipeline: pipeline).tap do |bridge|
downstream_pipeline = create(:ci_pipeline)
- create(:ci_sources_pipeline,
- source_pipeline: pipeline,
- source_project: pipeline.project,
- source_job: bridge,
- pipeline: downstream_pipeline,
- project: downstream_pipeline.project)
+ create(
+ :ci_sources_pipeline,
+ source_pipeline: pipeline,
+ source_project: pipeline.project,
+ source_job: bridge,
+ pipeline: downstream_pipeline,
+ project: downstream_pipeline.project
+ )
end
end
end
@@ -914,13 +929,24 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
let(:second_branch) { project.repository.branches[2] }
let!(:second_pipeline) do
- create(:ci_empty_pipeline, project: project, sha: second_branch.target,
- ref: second_branch.name, user: user, name: 'Build pipeline')
+ create(
+ :ci_empty_pipeline,
+ project: project,
+ sha: second_branch.target,
+ ref: second_branch.name,
+ user: user,
+ name: 'Build pipeline'
+ )
end
before do
- create(:ci_empty_pipeline, project: project, sha: project.commit.parent.id,
- ref: project.default_branch, user: user)
+ create(
+ :ci_empty_pipeline,
+ project: project,
+ sha: project.commit.parent.id,
+ ref: project.default_branch,
+ user: user
+ )
end
context 'default repository branch' do
@@ -1107,11 +1133,82 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
end
end
+ describe 'PUT /projects/:id/pipelines/:pipeline_id/name' do
+ let_it_be(:pipeline_creator) { create(:user) }
+ let(:pipeline) { create(:ci_pipeline, project: project, user: pipeline_creator) }
+ let(:name) { 'A new pipeline name' }
+
+ subject(:execute) do
+ put api("/projects/#{project.id}/pipelines/#{pipeline.id}/metadata", current_user), params: { name: name }
+ end
+
+ context 'authorized user' do
+ let(:current_user) { create(:user) }
+
+ before do
+ project.add_developer(current_user)
+ end
+
+ it 'renames pipeline when name is valid', :aggregate_failures do
+ expect { execute }.to change { pipeline.reload.name }.to(name)
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ context 'when name is invalid' do
+ let(:name) { 'a' * 256 }
+
+ it 'does not rename pipeline', :aggregate_failures do
+ expect { execute }.not_to change { pipeline.reload.name }
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['message']).to eq('Failed to update pipeline - Name is too long (maximum is 255 characters)')
+ end
+ end
+ end
+
+ context 'unauthorized user' do
+ let(:current_user) { create(:user) }
+
+ context 'when user is not a member' do
+ it 'does not rename pipeline', :aggregate_failures do
+ expect { execute }.not_to change { pipeline.reload.name }
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when user is a member' do
+ before do
+ project.add_reporter(current_user)
+ end
+
+ it 'does not rename pipeline', :aggregate_failures do
+ expect { execute }.not_to change { pipeline.reload.name }
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+ end
+
+ context 'when authorized with job token' do
+ let(:job) { create(:ci_build, :running, pipeline: pipeline, project: project, user: pipeline.user) }
+
+ before do
+ project.add_developer(pipeline.user)
+ end
+
+ subject(:execute) do
+ put api("/projects/#{project.id}/pipelines/#{pipeline.id}/metadata", nil, job_token: job.token), params: { name: name }
+ end
+
+ it 'renames pipeline when name is valid', :aggregate_failures do
+ expect { execute }.to change { pipeline.reload.name }.to(name)
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+ end
+
describe 'POST /projects/:id/pipelines/:pipeline_id/retry' do
context 'authorized user' do
let_it_be(:pipeline) do
- create(:ci_pipeline, project: project, sha: project.commit.id,
- ref: project.default_branch)
+ create(:ci_pipeline, project: project, sha: project.commit.id, ref: project.default_branch)
end
let_it_be(:build) { create(:ci_build, :failed, pipeline: pipeline) }
@@ -1156,8 +1253,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
describe 'POST /projects/:id/pipelines/:pipeline_id/cancel' do
let_it_be(:pipeline) do
- create(:ci_empty_pipeline, project: project, sha: project.commit.id,
- ref: project.default_branch)
+ create(:ci_empty_pipeline, project: project, sha: project.commit.id, ref: project.default_branch)
end
let_it_be(:build) { create(:ci_build, :running, pipeline: pipeline) }
diff --git a/spec/requests/api/ci/resource_groups_spec.rb b/spec/requests/api/ci/resource_groups_spec.rb
index 26265aec1dc..809b1c7c3d3 100644
--- a/spec/requests/api/ci/resource_groups_spec.rb
+++ b/spec/requests/api/ci/resource_groups_spec.rb
@@ -126,9 +126,7 @@ RSpec.describe API::Ci::ResourceGroups, feature_category: :continuous_delivery d
context 'when resource group key contains a slash' do
let_it_be(:resource_group) { create(:ci_resource_group, project: project, key: 'test/test') }
let_it_be(:upcoming_processable) do
- create(:ci_processable,
- :waiting_for_resource,
- resource_group: resource_group)
+ create(:ci_processable, :waiting_for_resource, resource_group: resource_group)
end
let(:key) { 'test%2Ftest' }
diff --git a/spec/requests/api/ci/runner/jobs_artifacts_spec.rb b/spec/requests/api/ci/runner/jobs_artifacts_spec.rb
index 2e0be23ba90..637469411d5 100644
--- a/spec/requests/api/ci/runner/jobs_artifacts_spec.rb
+++ b/spec/requests/api/ci/runner/jobs_artifacts_spec.rb
@@ -30,8 +30,15 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
describe '/api/v4/jobs' do
let(:job) do
- create(:ci_build, :artifacts, :extended_options,
- pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0)
+ create(
+ :ci_build,
+ :artifacts,
+ :extended_options,
+ pipeline: pipeline,
+ name: 'spinach',
+ stage: 'test',
+ stage_idx: 0
+ )
end
describe 'artifacts' do
diff --git a/spec/requests/api/ci/runner/jobs_request_post_spec.rb b/spec/requests/api/ci/runner/jobs_request_post_spec.rb
index 7f9c9a13311..2a870a25ea6 100644
--- a/spec/requests/api/ci/runner/jobs_request_post_spec.rb
+++ b/spec/requests/api/ci/runner/jobs_request_post_spec.rb
@@ -24,8 +24,17 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
let(:runner) { create(:ci_runner, :project, projects: [project]) }
let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master') }
let(:job) do
- create(:ci_build, :pending, :queued, :artifacts, :extended_options,
- pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0)
+ create(
+ :ci_build,
+ :pending,
+ :queued,
+ :artifacts,
+ :extended_options,
+ pipeline: pipeline,
+ name: 'spinach',
+ stage: 'test',
+ stage_idx: 0
+ )
end
describe 'POST /api/v4/jobs/request' do
@@ -202,12 +211,12 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
let(:expected_steps) do
[{ 'name' => 'script',
- 'script' => %w(echo),
+ 'script' => %w[echo],
'timeout' => job.metadata_timeout,
'when' => 'on_success',
'allow_failure' => false },
{ 'name' => 'after_script',
- 'script' => %w(ls date),
+ 'script' => %w[ls date],
'timeout' => job.metadata_timeout,
'when' => 'always',
'allow_failure' => true }]
@@ -226,7 +235,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
let(:expected_artifacts) do
[{ 'name' => 'artifacts_file',
'untracked' => false,
- 'paths' => %w(out/),
+ 'paths' => %w[out/],
'when' => 'always',
'expire_in' => '7d',
"artifact_type" => "archive",
@@ -342,10 +351,11 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
request_job
expect(response).to have_gitlab_http_status(:created)
- expect(json_response['git_info']['refspecs'])
- .to contain_exactly("+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}",
- '+refs/tags/*:refs/tags/*',
- '+refs/heads/*:refs/remotes/origin/*')
+ expect(json_response['git_info']['refspecs']).to contain_exactly(
+ "+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}",
+ '+refs/tags/*:refs/tags/*',
+ '+refs/heads/*:refs/remotes/origin/*'
+ )
end
end
end
@@ -383,10 +393,11 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
request_job
expect(response).to have_gitlab_http_status(:created)
- expect(json_response['git_info']['refspecs'])
- .to contain_exactly("+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}",
- '+refs/tags/*:refs/tags/*',
- '+refs/heads/*:refs/remotes/origin/*')
+ expect(json_response['git_info']['refspecs']).to contain_exactly(
+ "+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}",
+ '+refs/tags/*:refs/tags/*',
+ '+refs/heads/*:refs/remotes/origin/*'
+ )
end
end
end
@@ -461,7 +472,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
expect { request_job }.to change { runner.reload.contacted_at }
end
- %w(version revision platform architecture).each do |param|
+ %w[version revision platform architecture].each do |param|
context "when info parameter '#{param}' is present" do
let(:value) { "#{param}_value" }
@@ -646,8 +657,16 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
context 'when job has code coverage report' do
let(:job) do
- create(:ci_build, :pending, :queued, :coverage_report_cobertura,
- pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0)
+ create(
+ :ci_build,
+ :pending,
+ :queued,
+ :coverage_report_cobertura,
+ pipeline: pipeline,
+ name: 'spinach',
+ stage: 'test',
+ stage_idx: 0
+ )
end
let(:expected_artifacts) do
@@ -788,9 +807,16 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
describe 'time_in_queue_seconds support' do
let(:job) do
- create(:ci_build, :pending, :queued, pipeline: pipeline,
- name: 'spinach', stage: 'test', stage_idx: 0,
- queued_at: 60.seconds.ago)
+ create(
+ :ci_build,
+ :pending,
+ :queued,
+ pipeline: pipeline,
+ name: 'spinach',
+ stage: 'test',
+ stage_idx: 0,
+ queued_at: 60.seconds.ago
+ )
end
it 'presents the time_in_queue_seconds info in the payload' do
diff --git a/spec/requests/api/ci/runner/jobs_trace_spec.rb b/spec/requests/api/ci/runner/jobs_trace_spec.rb
index ee00fc5a793..8c596d2338f 100644
--- a/spec/requests/api/ci/runner/jobs_trace_spec.rb
+++ b/spec/requests/api/ci/runner/jobs_trace_spec.rb
@@ -23,14 +23,28 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_trace_chunks, feature_catego
let(:runner) { create(:ci_runner, :project, projects: [project]) }
let(:user) { create(:user) }
let(:job) do
- create(:ci_build, :artifacts, :extended_options,
- pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0)
+ create(
+ :ci_build,
+ :artifacts,
+ :extended_options,
+ pipeline: pipeline,
+ name: 'spinach',
+ stage: 'test',
+ stage_idx: 0
+ )
end
describe 'PATCH /api/v4/jobs/:id/trace' do
let(:job) do
- create(:ci_build, :running, :trace_live,
- project: project, user: user, runner_id: runner.id, pipeline: pipeline)
+ create(
+ :ci_build,
+ :running,
+ :trace_live,
+ project: project,
+ user: user,
+ runner_id: runner.id,
+ pipeline: pipeline
+ )
end
let(:headers) { { API::Ci::Helpers::Runner::JOB_TOKEN_HEADER => job.token, 'Content-Type' => 'text/plain' } }
diff --git a/spec/requests/api/ci/runner/runners_post_spec.rb b/spec/requests/api/ci/runner/runners_post_spec.rb
index c5e49e9ac54..1490172d1c3 100644
--- a/spec/requests/api/ci/runner/runners_post_spec.rb
+++ b/spec/requests/api/ci/runner/runners_post_spec.rb
@@ -48,7 +48,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
description: 'server.hostname',
maintenance_note: 'Some maintainer notes',
run_untagged: false,
- tag_list: %w(tag1 tag2),
+ tag_list: %w[tag1 tag2],
locked: true,
active: true,
access_level: 'ref_protected',
@@ -167,7 +167,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
allow_any_instance_of(::Ci::Runner).to receive(:cache_attributes)
end
- %w(name version revision platform architecture).each do |param|
+ %w[name version revision platform architecture].each do |param|
context "when info parameter '#{param}' info is present" do
let(:value) { "#{param}_value" }
@@ -185,8 +185,8 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
it "sets the runner's ip_address" do
post api('/runners'),
- params: { token: registration_token },
- headers: { 'X-Forwarded-For' => '123.111.123.111' }
+ params: { token: registration_token },
+ headers: { 'X-Forwarded-For' => '123.111.123.111' }
expect(response).to have_gitlab_http_status(:created)
expect(::Ci::Runner.last.ip_address).to eq('123.111.123.111')
diff --git a/spec/requests/api/ci/runners_spec.rb b/spec/requests/api/ci/runners_spec.rb
index 2b2d2e0def8..ba80684e89e 100644
--- a/spec/requests/api/ci/runners_spec.rb
+++ b/spec/requests/api/ci/runners_spec.rb
@@ -249,6 +249,39 @@ RSpec.describe API::Ci::Runners, :aggregate_failures, feature_category: :runner_
a_hash_including('description' => 'Runner tagged with tag1 and tag2')
]
end
+
+ context 'with ci_runner_machines' do
+ let_it_be(:version_ci_runner) { create(:ci_runner, :project, description: 'Runner with machine') }
+ let_it_be(:version_ci_runner_machine) { create(:ci_runner_machine, runner: version_ci_runner, version: '15.0.3') }
+ let_it_be(:version_16_ci_runner) { create(:ci_runner, :project, description: 'Runner with machine version 16') }
+ let_it_be(:version_16_ci_runner_machine) { create(:ci_runner_machine, runner: version_16_ci_runner, version: '16.0.1') }
+
+ it 'filters runners by version_prefix when prefix is "15.0"' do
+ get api('/runners/all?version_prefix=15.0', admin, admin_mode: true)
+
+ expect(json_response).to match_array [
+ a_hash_including('description' => 'Runner with machine', 'active' => true, 'paused' => false)
+ ]
+ end
+
+ it 'filters runners by version_prefix when prefix is "16"' do
+ get api('/runners/all?version_prefix=16', admin, admin_mode: true)
+ expect(json_response).to match_array [
+ a_hash_including('description' => 'Runner with machine version 16', 'active' => true, 'paused' => false)
+ ]
+ end
+
+ it 'filters runners by version_prefix when prefix is "25"' do
+ get api('/runners/all?version_prefix=25', admin, admin_mode: true)
+ expect(json_response).to match_array []
+ end
+
+ it 'does not filter runners by version_prefix when prefix is invalid ("V15")' do
+ get api('/runners/all?version_prefix=v15', admin, admin_mode: true)
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
end
context 'without admin privileges' do
@@ -467,13 +500,17 @@ RSpec.describe API::Ci::Runners, :aggregate_failures, feature_category: :runner_
active = shared_runner.active
runner_queue_value = shared_runner.ensure_runner_queue_value
- update_runner(shared_runner.id, admin, description: "#{description}_updated",
- active: !active,
- tag_list: ['ruby2.1', 'pgsql', 'mysql'],
- run_untagged: 'false',
- locked: 'true',
- access_level: 'ref_protected',
- maximum_timeout: 1234)
+ update_runner(
+ shared_runner.id,
+ admin,
+ description: "#{description}_updated",
+ active: !active,
+ tag_list: ['ruby2.1', 'pgsql', 'mysql'],
+ run_untagged: 'false',
+ locked: 'true',
+ access_level: 'ref_protected',
+ maximum_timeout: 1234
+ )
shared_runner.reload
expect(response).to have_gitlab_http_status(:ok)
diff --git a/spec/requests/api/ci/triggers_spec.rb b/spec/requests/api/ci/triggers_spec.rb
index ff54ba61309..a6e50479963 100644
--- a/spec/requests/api/ci/triggers_spec.rb
+++ b/spec/requests/api/ci/triggers_spec.rb
@@ -81,7 +81,7 @@ RSpec.describe API::Ci::Triggers, feature_category: :continuous_integration do
end
it 'validates variables needs to be a map of key-valued strings' do
- post api("/projects/#{project.id}/trigger/pipeline"), params: options.merge(variables: { 'TRIGGER_KEY' => %w(1 2) }, ref: 'master')
+ post api("/projects/#{project.id}/trigger/pipeline"), params: options.merge(variables: { 'TRIGGER_KEY' => %w[1 2] }, ref: 'master')
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message']).to eq('variables needs to be a map of key-valued strings')
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index c3a7dbdcdbb..6a112918288 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -2426,16 +2426,6 @@ RSpec.describe API::Commits, feature_category: :source_code_management do
expect(json_response['x509_certificate']['x509_issuer']['crl_url']).to eq(commit.signature.x509_certificate.x509_issuer.crl_url)
expect(json_response['commit_source']).to eq('gitaly')
end
-
- context 'with Rugged enabled', :enable_rugged do
- it 'returns correct JSON' do
- get api(route, current_user)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['signature_type']).to eq('X509')
- expect(json_response['commit_source']).to eq('gitaly')
- end
- end
end
context 'with ssh signed commit' do
diff --git a/spec/requests/api/container_repositories_spec.rb b/spec/requests/api/container_repositories_spec.rb
index 605fa0d92f6..82c63362166 100644
--- a/spec/requests/api/container_repositories_spec.rb
+++ b/spec/requests/api/container_repositories_spec.rb
@@ -67,7 +67,7 @@ RSpec.describe API::ContainerRepositories, feature_category: :container_registry
let(:url) { "/registry/repositories/#{repository.id}?tags=true" }
before do
- stub_container_registry_tags(repository: repository.path, tags: %w(rootA latest), with_manifest: true)
+ stub_container_registry_tags(repository: repository.path, tags: %w[rootA latest], with_manifest: true)
end
it 'returns a repository and its tags' do
@@ -102,7 +102,7 @@ RSpec.describe API::ContainerRepositories, feature_category: :container_registry
let(:url) { "/registry/repositories/#{repository.id}?tags_count=true" }
before do
- stub_container_registry_tags(repository: repository.path, tags: %w(rootA latest), with_manifest: true)
+ stub_container_registry_tags(repository: repository.path, tags: %w[rootA latest], with_manifest: true)
end
it 'returns a repository and its tags_count' do
diff --git a/spec/requests/api/deployments_spec.rb b/spec/requests/api/deployments_spec.rb
index 82ac2eed83d..41c5847e940 100644
--- a/spec/requests/api/deployments_spec.rb
+++ b/spec/requests/api/deployments_spec.rb
@@ -424,7 +424,7 @@ RSpec.describe API::Deployments, feature_category: :continuous_delivery do
)
expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response['message']['status']).to include(%{cannot transition via \"run\"})
+ expect(json_response['message']['status']).to include(%(cannot transition via \"run\"))
end
it 'links merge requests when the deployment status changes to success', :sidekiq_inline do
diff --git a/spec/requests/api/geo_spec.rb b/spec/requests/api/geo_spec.rb
index 3dec91fd2fa..c394553d14e 100644
--- a/spec/requests/api/geo_spec.rb
+++ b/spec/requests/api/geo_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe API::Geo, feature_category: :geo_replication do
{
'type' => 'object',
'additionalProperties' => false,
- 'required' => %w(geo_enabled),
+ 'required' => %w[geo_enabled],
'properties' => {
'geo_enabled' => { 'type' => 'boolean' }
}
diff --git a/spec/requests/api/graphql/abuse_report_spec.rb b/spec/requests/api/graphql/abuse_report_spec.rb
index 7d0b8b35763..f74b1fb4061 100644
--- a/spec/requests/api/graphql/abuse_report_spec.rb
+++ b/spec/requests/api/graphql/abuse_report_spec.rb
@@ -2,49 +2,122 @@
require 'spec_helper'
-RSpec.describe 'abuse_report', feature_category: :insider_threat do
+RSpec.describe 'Querying an Abuse Report', feature_category: :insider_threat do
include GraphqlHelpers
let_it_be(:current_user) { create(:admin) }
- let_it_be(:label) { create(:abuse_report_label, title: 'Uno') }
- let_it_be(:report) { create(:abuse_report, labels: [label]) }
-
- let(:report_gid) { Gitlab::GlobalId.build(report, id: report.id).to_s }
-
- let(:fields) do
- <<~GRAPHQL
- labels {
- nodes {
- id
- title
- description
- color
- textColor
- }
- }
- GRAPHQL
- end
+ let_it_be(:abuse_report) { create(:abuse_report) }
- let(:arguments) { { id: report_gid } }
- let(:query) { graphql_query_for('abuseReport', arguments, fields) }
+ let(:global_id) { abuse_report.to_gid.to_s }
+ let(:abuse_report_fields) { all_graphql_fields_for('AbuseReport', max_depth: 2) }
+ let(:abuse_report_data) { graphql_data['abuseReport'] }
+
+ let(:query) do
+ graphql_query_for('abuseReport', { 'id' => global_id }, abuse_report_fields)
+ end
before do
post_graphql(query, current_user: current_user)
end
- it_behaves_like 'a working graphql query that returns data'
+ context 'when the user is an admin' do
+ it_behaves_like 'a working graphql query that returns data'
- it 'returns abuse report with labels' do
- expect(graphql_data_at('abuseReport', 'labels', 'nodes', 0)).to match(a_graphql_entity_for(label))
+ it 'returns all fields' do
+ expect(abuse_report_data).to include(
+ 'id' => global_id,
+ 'userPermissions' => {
+ 'readAbuseReport' => true,
+ 'createNote' => true
+ }
+ )
+ end
end
- context 'when current user is not an admin' do
- let_it_be(:current_user) { create(:user) }
+ context 'when the user is not an admin' do
+ let(:current_user) { create(:user) }
+
+ it 'returns nil' do
+ expect(abuse_report_data).to be_nil
+ end
+ end
- it_behaves_like 'a working graphql query'
+ describe 'labels' do
+ let_it_be(:abuse_report_label) { create(:abuse_report_label, title: 'Label') }
+ let_it_be(:abuse_report) { create(:abuse_report, labels: [abuse_report_label]) }
+
+ let(:labels_response) do
+ graphql_data_at(:abuse_report, :labels, :nodes)
+ end
+
+ let(:abuse_report_fields) do
+ <<~GRAPHQL
+ labels {
+ nodes {
+ id
+ title
+ description
+ color
+ textColor
+ }
+ }
+ GRAPHQL
+ end
+
+ it 'returns labels' do
+ expect(labels_response).to contain_exactly(
+ a_graphql_entity_for(abuse_report_label)
+ )
+ end
+ end
+
+ describe 'notes' do
+ let_it_be(:note) { create(:note, noteable: abuse_report, author: current_user) }
+
+ let(:notes_response) do
+ graphql_data_at(:abuse_report, :notes, :nodes)
+ end
+
+ let(:abuse_report_fields) do
+ <<~GRAPHQL
+ notes {
+ nodes {
+ #{all_graphql_fields_for('Note', max_depth: 2)}
+ }
+ }
+ GRAPHQL
+ end
+
+ it 'returns notes' do
+ expect(notes_response).to contain_exactly(
+ a_graphql_entity_for(note)
+ )
+ end
+ end
+
+ describe 'discussions' do
+ let_it_be(:discussion) do
+ create(:discussion_note_on_abuse_report, noteable: abuse_report, author: current_user).to_discussion
+ end
+
+ let(:discussions_response) do
+ graphql_data_at(:abuse_report, :discussions, :nodes)
+ end
+
+ let(:abuse_report_fields) do
+ <<~GRAPHQL
+ discussions {
+ nodes {
+ #{all_graphql_fields_for('Discussion', max_depth: 2)}
+ }
+ }
+ GRAPHQL
+ end
- it 'does not contain any data' do
- expect(graphql_data_at('abuseReportLabel')).to be_nil
+ it 'returns discussions' do
+ expect(discussions_response).to contain_exactly(
+ a_graphql_entity_for(discussion)
+ )
end
end
end
diff --git a/spec/requests/api/graphql/ci/catalog/resource_spec.rb b/spec/requests/api/graphql/ci/catalog/resource_spec.rb
new file mode 100644
index 00000000000..fce773f320b
--- /dev/null
+++ b/spec/requests/api/graphql/ci/catalog/resource_spec.rb
@@ -0,0 +1,341 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Query.ciCatalogResource', feature_category: :pipeline_composition do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:namespace) { create(:group) }
+
+ let_it_be(:project) do
+ create(
+ :project, :with_avatar, :custom_repo,
+ name: 'Component Repository',
+ description: 'A simple component',
+ namespace: namespace,
+ star_count: 1,
+ files: { 'README.md' => '[link](README.md)' }
+ )
+ end
+
+ let_it_be(:resource) { create(:ci_catalog_resource, project: project) }
+
+ let(:query) do
+ <<~GQL
+ query {
+ ciCatalogResource(id: "#{resource.to_global_id}") {
+ #{all_graphql_fields_for('CiCatalogResource', max_depth: 1)}
+ }
+ }
+ GQL
+ end
+
+ subject(:post_query) { post_graphql(query, current_user: user) }
+
+ context 'when the current user has permission to read the namespace catalog' do
+ it 'returns the resource with the expected data' do
+ namespace.add_developer(user)
+
+ post_query
+
+ expect(graphql_data_at(:ciCatalogResource)).to match(
+ a_graphql_entity_for(
+ resource, :name, :description,
+ icon: project.avatar_path,
+ webPath: "/#{project.full_path}",
+ starCount: project.star_count,
+ forksCount: project.forks_count,
+ readmeHtml: a_string_including(
+ "#{project.full_path}/-/blob/#{project.default_branch}/README.md"
+ )
+ )
+ )
+ end
+ end
+
+ context 'when the current user does not have permission to read the namespace catalog' do
+ it 'returns nil' do
+ namespace.add_guest(user)
+
+ post_query
+
+ expect(graphql_data_at(:ciCatalogResource)).to be_nil
+ end
+ end
+
+ describe 'versions' do
+ before_all do
+ namespace.add_developer(user)
+ end
+
+ before do
+ stub_licensed_features(ci_namespace_catalog: true)
+ end
+
+ let(:query) do
+ <<~GQL
+ query {
+ ciCatalogResource(id: "#{resource.to_global_id}") {
+ id
+ versions {
+ nodes {
+ id
+ tagName
+ releasedAt
+ author {
+ id
+ name
+ webUrl
+ }
+ }
+ }
+ }
+ }
+ GQL
+ end
+
+ context 'when the resource has versions' do
+ let_it_be(:author) { create(:user, name: 'author') }
+
+ let_it_be(:version1) do
+ create(:release, project: project, released_at: '2023-01-01T00:00:00Z', author: author)
+ end
+
+ let_it_be(:version2) do
+ create(:release, project: project, released_at: '2023-02-01T00:00:00Z', author: author)
+ end
+
+ it 'returns the resource with the versions data' do
+ post_query
+
+ expect(graphql_data_at(:ciCatalogResource)).to match(
+ a_graphql_entity_for(resource)
+ )
+
+ expect(graphql_data_at(:ciCatalogResource, :versions, :nodes)).to contain_exactly(
+ a_graphql_entity_for(
+ version1,
+ tagName: version1.tag,
+ releasedAt: version1.released_at,
+ author: a_graphql_entity_for(author, :name)
+ ),
+ a_graphql_entity_for(
+ version2,
+ tagName: version2.tag,
+ releasedAt: version2.released_at,
+ author: a_graphql_entity_for(author, :name)
+ )
+ )
+ end
+ end
+
+ context 'when the resource does not have a version' do
+ it 'returns versions as an empty array' do
+ post_query
+
+ expect(graphql_data_at(:ciCatalogResource)).to match(
+ a_graphql_entity_for(resource, versions: { 'nodes' => [] })
+ )
+ end
+ end
+ end
+
+ describe 'latestVersion' do
+ before_all do
+ namespace.add_developer(user)
+ end
+
+ before do
+ stub_licensed_features(ci_namespace_catalog: true)
+ end
+
+ let(:query) do
+ <<~GQL
+ query {
+ ciCatalogResource(id: "#{resource.to_global_id}") {
+ id
+ latestVersion {
+ id
+ tagName
+ releasedAt
+ author {
+ id
+ name
+ webUrl
+ }
+ }
+ }
+ }
+ GQL
+ end
+
+ context 'when the resource has versions' do
+ let_it_be(:author) { create(:user, name: 'author') }
+
+ let_it_be(:latest_version) do
+ create(:release, project: project, released_at: '2023-02-01T00:00:00Z', author: author)
+ end
+
+ before_all do
+ # Previous version of the project
+ create(:release, project: project, released_at: '2023-01-01T00:00:00Z', author: author)
+ end
+
+ it 'returns the resource with the latest version data' do
+ post_query
+
+ expect(graphql_data_at(:ciCatalogResource)).to match(
+ a_graphql_entity_for(
+ resource,
+ latestVersion: a_graphql_entity_for(
+ latest_version,
+ tagName: latest_version.tag,
+ releasedAt: latest_version.released_at,
+ author: a_graphql_entity_for(author, :name)
+ )
+ )
+ )
+ end
+ end
+
+ context 'when the resource does not have a version' do
+ it 'returns nil' do
+ post_query
+
+ expect(graphql_data_at(:ciCatalogResource)).to match(
+ a_graphql_entity_for(resource, latestVersion: nil)
+ )
+ end
+ end
+ end
+
+ describe 'rootNamespace' do
+ before_all do
+ namespace.add_developer(user)
+ end
+
+ before do
+ stub_licensed_features(ci_namespace_catalog: true)
+ end
+
+ let(:query) do
+ <<~GQL
+ query {
+ ciCatalogResource(id: "#{resource.to_global_id}") {
+ id
+ rootNamespace {
+ id
+ name
+ path
+ }
+ }
+ }
+ GQL
+ end
+
+ it 'returns the correct root namespace data' do
+ post_query
+
+ expect(graphql_data_at(:ciCatalogResource)).to match(
+ a_graphql_entity_for(
+ resource,
+ rootNamespace: a_graphql_entity_for(namespace, :name, :path)
+ )
+ )
+ end
+ end
+
+ describe 'openIssuesCount' do
+ before do
+ stub_licensed_features(ci_namespace_catalog: true)
+ end
+
+ context 'when open_issue_count is requested' do
+ let(:query) do
+ <<~GQL
+ query {
+ ciCatalogResource(id: "#{resource.to_global_id}") {
+ openIssuesCount
+ }
+ }
+ GQL
+ end
+
+ it 'returns the correct count' do
+ create(:issue, :opened, project: project)
+ create(:issue, :opened, project: project)
+
+ namespace.add_developer(user)
+
+ post_query
+
+ expect(graphql_data_at(:ciCatalogResource)).to match(
+ a_graphql_entity_for(
+ open_issues_count: 2
+ )
+ )
+ end
+
+ context 'when open_issue_count is zero' do
+ it 'returns zero' do
+ namespace.add_developer(user)
+
+ post_query
+
+ expect(graphql_data_at(:ciCatalogResource)).to match(
+ a_graphql_entity_for(
+ open_issues_count: 0
+ )
+ )
+ end
+ end
+ end
+ end
+
+ describe 'openMergeRequestsCount' do
+ before do
+ stub_licensed_features(ci_namespace_catalog: true)
+ end
+
+ context 'when merge_requests_count is requested' do
+ let(:query) do
+ <<~GQL
+ query {
+ ciCatalogResource(id: "#{resource.to_global_id}") {
+ openMergeRequestsCount
+ }
+ }
+ GQL
+ end
+
+ it 'returns the correct count' do
+ create(:merge_request, :opened, source_project: project)
+
+ namespace.add_developer(user)
+
+ post_query
+
+ expect(graphql_data_at(:ciCatalogResource)).to match(
+ a_graphql_entity_for(
+ open_merge_requests_count: 1
+ )
+ )
+ end
+
+ context 'when open merge_requests_count is zero' do
+ it 'returns zero' do
+ namespace.add_developer(user)
+
+ post_query
+
+ expect(graphql_data_at(:ciCatalogResource)).to match(
+ a_graphql_entity_for(
+ open_merge_requests_count: 0
+ )
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/ci/catalog/resources_spec.rb b/spec/requests/api/graphql/ci/catalog/resources_spec.rb
new file mode 100644
index 00000000000..7c955a1202c
--- /dev/null
+++ b/spec/requests/api/graphql/ci/catalog/resources_spec.rb
@@ -0,0 +1,359 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Query.ciCatalogResources', feature_category: :pipeline_composition do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:namespace) { create(:group) }
+ let_it_be(:project2) { create(:project, namespace: namespace) }
+
+ let_it_be(:project1) do
+ create(
+ :project, :with_avatar, :custom_repo,
+ name: 'Component Repository',
+ description: 'A simple component',
+ namespace: namespace,
+ star_count: 1,
+ files: { 'README.md' => '**Test**' }
+ )
+ end
+
+ let_it_be(:public_project) do
+ create(
+ :project, :with_avatar, :custom_repo, :public,
+ name: 'Public Component',
+ description: 'A public component',
+ files: { 'README.md' => '**Test**' }
+ )
+ end
+
+ let_it_be(:resource1) { create(:ci_catalog_resource, project: project1, latest_released_at: '2023-01-01T00:00:00Z') }
+ let_it_be(:public_resource) { create(:ci_catalog_resource, project: public_project) }
+
+ let(:query) do
+ <<~GQL
+ query {
+ ciCatalogResources {
+ nodes {
+ id
+ name
+ description
+ icon
+ webPath
+ latestReleasedAt
+ starCount
+ forksCount
+ readmeHtml
+ }
+ }
+ }
+ GQL
+ end
+
+ subject(:post_query) { post_graphql(query, current_user: user) }
+
+ shared_examples 'avoids N+1 queries' do
+ it do
+ ctx = { current_user: user }
+
+ control_count = ActiveRecord::QueryRecorder.new do
+ run_with_clean_state(query, context: ctx)
+ end
+
+ create(:ci_catalog_resource, project: project2)
+
+ expect do
+ run_with_clean_state(query, context: ctx)
+ end.not_to exceed_query_limit(control_count)
+ end
+ end
+
+ it_behaves_like 'avoids N+1 queries'
+
+ it 'returns the resources with the expected data' do
+ namespace.add_developer(user)
+
+ post_query
+
+ expect(graphql_data_at(:ciCatalogResources, :nodes)).to contain_exactly(
+ a_graphql_entity_for(
+ resource1, :name, :description,
+ icon: project1.avatar_path,
+ webPath: "/#{project1.full_path}",
+ starCount: project1.star_count,
+ forksCount: project1.forks_count,
+ readmeHtml: a_string_including('Test</strong>'),
+ latestReleasedAt: resource1.latest_released_at
+ ),
+ a_graphql_entity_for(public_resource)
+ )
+ end
+
+ describe 'versions' do
+ before_all do
+ namespace.add_developer(user)
+ end
+
+ let(:query) do
+ <<~GQL
+ query {
+ ciCatalogResources {
+ nodes {
+ id
+ versions {
+ nodes {
+ id
+ tagName
+ releasedAt
+ author {
+ id
+ name
+ webUrl
+ }
+ }
+ }
+ }
+ }
+ }
+ GQL
+ end
+
+ it 'limits the request to 1 resource at a time' do
+ create(:ci_catalog_resource, project: project2)
+
+ post_query
+
+ expect_graphql_errors_to_include \
+ [/"versions" field can be requested only for 1 CiCatalogResource\(s\) at a time./]
+ end
+ end
+
+ describe 'latestVersion' do
+ let_it_be(:author1) { create(:user, name: 'author1') }
+ let_it_be(:author2) { create(:user, name: 'author2') }
+
+ let_it_be(:latest_version1) do
+ create(:release, project: project1, released_at: '2023-02-01T00:00:00Z', author: author1)
+ end
+
+ let_it_be(:latest_version2) do
+ create(:release, project: public_project, released_at: '2023-02-01T00:00:00Z', author: author2)
+ end
+
+ let(:query) do
+ <<~GQL
+ query {
+ ciCatalogResources {
+ nodes {
+ id
+ latestVersion {
+ id
+ tagName
+ releasedAt
+ author {
+ id
+ name
+ webUrl
+ }
+ }
+ }
+ }
+ }
+ GQL
+ end
+
+ before_all do
+ namespace.add_developer(user)
+
+ # Previous versions of the projects
+ create(:release, project: project1, released_at: '2023-01-01T00:00:00Z', author: author1)
+ create(:release, project: public_project, released_at: '2023-01-01T00:00:00Z', author: author2)
+ end
+
+ it 'returns all resources with the latest version data' do
+ post_query
+
+ expect(graphql_data_at(:ciCatalogResources, :nodes)).to contain_exactly(
+ a_graphql_entity_for(
+ resource1,
+ latestVersion: a_graphql_entity_for(
+ latest_version1,
+ tagName: latest_version1.tag,
+ releasedAt: latest_version1.released_at,
+ author: a_graphql_entity_for(author1, :name)
+ )
+ ),
+ a_graphql_entity_for(
+ public_resource,
+ latestVersion: a_graphql_entity_for(
+ latest_version2,
+ tagName: latest_version2.tag,
+ releasedAt: latest_version2.released_at,
+ author: a_graphql_entity_for(author2, :name)
+ )
+ )
+ )
+ end
+
+ # TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/430350
+ # it_behaves_like 'avoids N+1 queries'
+ end
+
+ describe 'rootNamespace' do
+ before_all do
+ namespace.add_developer(user)
+ end
+
+ let(:query) do
+ <<~GQL
+ query {
+ ciCatalogResources {
+ nodes {
+ id
+ rootNamespace {
+ id
+ name
+ path
+ }
+ }
+ }
+ }
+ GQL
+ end
+
+ it 'returns the correct root namespace data' do
+ post_query
+
+ expect(graphql_data_at(:ciCatalogResources, :nodes)).to contain_exactly(
+ a_graphql_entity_for(
+ resource1,
+ rootNamespace: a_graphql_entity_for(namespace, :name, :path)
+ ),
+ a_graphql_entity_for(public_resource, rootNamespace: nil)
+ )
+ end
+ end
+
+ describe 'openIssuesCount' do
+ before_all do
+ namespace.add_developer(user)
+ end
+
+ before_all do
+ create(:issue, :opened, project: project1)
+ create(:issue, :opened, project: project1)
+
+ create(:issue, :opened, project: public_project)
+ end
+
+ let(:query) do
+ <<~GQL
+ query {
+ ciCatalogResources {
+ nodes {
+ openIssuesCount
+ }
+ }
+ }
+ GQL
+ end
+
+ it 'returns the correct count' do
+ post_query
+
+ expect(graphql_data_at(:ciCatalogResources, :nodes)).to contain_exactly(
+ a_graphql_entity_for(openIssuesCount: 2),
+ a_graphql_entity_for(openIssuesCount: 1)
+ )
+ end
+
+ it_behaves_like 'avoids N+1 queries'
+ end
+
+ describe 'openMergeRequestsCount' do
+ before_all do
+ namespace.add_developer(user)
+ end
+
+ before_all do
+ create(:merge_request, :opened, source_project: project1)
+ create(:merge_request, :opened, source_project: public_project)
+ end
+
+ let(:query) do
+ <<~GQL
+ query {
+ ciCatalogResources {
+ nodes {
+ openMergeRequestsCount
+ }
+ }
+ }
+ GQL
+ end
+
+ it 'returns the correct count' do
+ post_query
+
+ expect(graphql_data_at(:ciCatalogResources, :nodes)).to contain_exactly(
+ a_graphql_entity_for(openMergeRequestsCount: 1),
+ a_graphql_entity_for(openMergeRequestsCount: 1)
+ )
+ end
+
+ it_behaves_like 'avoids N+1 queries'
+ end
+
+ # TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/429636
+ context 'when using `projectPath` (legacy) to fetch resources' do
+ let(:query) do
+ <<~GQL
+ query {
+ ciCatalogResources(projectPath: "#{project1.full_path}") {
+ nodes {
+ #{all_graphql_fields_for('CiCatalogResource', max_depth: 1)}
+ }
+ }
+ }
+ GQL
+ end
+
+ context 'when the current user has permission to read the namespace catalog' do
+ before_all do
+ namespace.add_developer(user)
+ end
+
+ it 'returns catalog resources with the expected data' do
+ resource2 = create(:ci_catalog_resource, project: project2)
+ _resource_in_another_namespace = create(:ci_catalog_resource)
+
+ post_query
+
+ expect(graphql_data_at(:ciCatalogResources, :nodes)).to contain_exactly(
+ a_graphql_entity_for(resource1),
+ a_graphql_entity_for(
+ resource2, :name, :description,
+ icon: project2.avatar_path,
+ webPath: "/#{project2.full_path}",
+ starCount: project2.star_count,
+ forksCount: project2.forks_count,
+ readmeHtml: '',
+ latestReleasedAt: resource2.latest_released_at
+ )
+ )
+ end
+
+ it_behaves_like 'avoids N+1 queries'
+ end
+
+ context 'when the current user does not have permission to read the namespace catalog' do
+ it 'returns no resources' do
+ post_query
+
+ expect(graphql_data_at(:ciCatalogResources, :nodes)).to be_empty
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/ci/manual_variables_spec.rb b/spec/requests/api/graphql/ci/manual_variables_spec.rb
index 47dccc0deb6..41788881e62 100644
--- a/spec/requests/api/graphql/ci/manual_variables_spec.rb
+++ b/spec/requests/api/graphql/ci/manual_variables_spec.rb
@@ -90,6 +90,6 @@ RSpec.describe 'Query.project(fullPath).pipelines.jobs.manualVariables', feature
variables_data = graphql_data.dig('project', 'pipelines', 'nodes').first
.dig('jobs', 'nodes').flat_map { |job| job.dig('manualVariables', 'nodes') }
- expect(variables_data.map { |var| var['key'] }).to match_array(%w(MANUAL_TEST_VAR_1 MANUAL_TEST_VAR_2))
+ expect(variables_data.map { |var| var['key'] }).to match_array(%w[MANUAL_TEST_VAR_1 MANUAL_TEST_VAR_2])
end
end
diff --git a/spec/requests/api/graphql/ci/runners_spec.rb b/spec/requests/api/graphql/ci/runners_spec.rb
index c5571086700..0e2712d742d 100644
--- a/spec/requests/api/graphql/ci/runners_spec.rb
+++ b/spec/requests/api/graphql/ci/runners_spec.rb
@@ -35,28 +35,16 @@ RSpec.describe 'Query.runners', feature_category: :runner_fleet do
end
context 'with filters' do
- let(:query) do
- %(
- query {
- runners(type: #{runner_type}, status: #{status}) {
- #{fields}
- }
- }
- )
- end
-
- before do
- allow_next_instance_of(::Gitlab::Ci::RunnerUpgradeCheck) do |instance|
- allow(instance).to receive(:check_runner_upgrade_suggestion)
- end
-
- post_graphql(query, current_user: current_user)
- end
-
shared_examples 'a working graphql query returning expected runner' do
- it_behaves_like 'a working graphql query'
+ it_behaves_like 'a working graphql query' do
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+ end
it 'returns expected runner' do
+ post_graphql(query, current_user: current_user)
+
expect(runners_graphql_data['nodes']).to contain_exactly(a_graphql_entity_for(expected_runner))
end
@@ -86,22 +74,63 @@ RSpec.describe 'Query.runners', feature_category: :runner_fleet do
end
end
- context 'runner_type is INSTANCE_TYPE and status is ACTIVE' do
- let(:runner_type) { 'INSTANCE_TYPE' }
- let(:status) { 'ACTIVE' }
+ context 'when filtered on type and status' do
+ let(:query) do
+ %(
+ query {
+ runners(type: #{runner_type}, status: #{status}) {
+ #{fields}
+ }
+ }
+ )
+ end
- let!(:expected_runner) { instance_runner }
+ before do
+ allow_next_instance_of(::Gitlab::Ci::RunnerUpgradeCheck) do |instance|
+ allow(instance).to receive(:check_runner_upgrade_suggestion)
+ end
+ end
- it_behaves_like 'a working graphql query returning expected runner'
+ context 'runner_type is INSTANCE_TYPE and status is ACTIVE' do
+ let(:runner_type) { 'INSTANCE_TYPE' }
+ let(:status) { 'ACTIVE' }
+
+ let!(:expected_runner) { instance_runner }
+
+ it_behaves_like 'a working graphql query returning expected runner'
+ end
+
+ context 'runner_type is PROJECT_TYPE and status is NEVER_CONTACTED' do
+ let(:runner_type) { 'PROJECT_TYPE' }
+ let(:status) { 'NEVER_CONTACTED' }
+
+ let!(:expected_runner) { project_runner }
+
+ it_behaves_like 'a working graphql query returning expected runner'
+ end
end
- context 'runner_type is PROJECT_TYPE and status is NEVER_CONTACTED' do
- let(:runner_type) { 'PROJECT_TYPE' }
- let(:status) { 'NEVER_CONTACTED' }
+ context 'when filtered on version prefix' do
+ let_it_be(:version_runner) { create(:ci_runner, :project, active: false, description: 'Runner with machine') }
+ let_it_be(:version_runner_machine) { create(:ci_runner_machine, runner: version_runner, version: '15.11.0') }
+
+ let(:query) do
+ %(
+ query {
+ runners(versionPrefix: "#{version_prefix}") {
+ #{fields}
+ }
+ }
+ )
+ end
+
+ context 'version_prefix is "15."' do
+ let(:version_prefix) { '15.' }
- let!(:expected_runner) { project_runner }
+ let!(:expected_runner) { version_runner }
- it_behaves_like 'a working graphql query returning expected runner'
+ it_behaves_like 'a working graphql query returning expected runner'
+ end
end
end
diff --git a/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb b/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb
index 118a11851dd..20277c7e27b 100644
--- a/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb
+++ b/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb
@@ -313,4 +313,124 @@ RSpec.describe 'container repository details', feature_category: :container_regi
end
it_behaves_like 'handling graphql network errors with the container registry'
+
+ context 'when list tags API is enabled', :saas do
+ before do
+ stub_container_registry_config(enabled: true)
+ allow(ContainerRegistry::GitlabApiClient).to receive(:supports_gitlab_api?).and_return(true)
+
+ allow_next_instances_of(ContainerRegistry::GitlabApiClient, nil) do |client|
+ allow(client).to receive(:tags).and_return(response_body)
+ end
+ end
+
+ let_it_be(:raw_tags_response) do
+ [
+ {
+ name: 'latest',
+ digest: 'sha256:1234567892',
+ config_digest: 'sha256:3332132331',
+ media_type: 'application/vnd.oci.image.manifest.v1+json',
+ size_bytes: 1234567892,
+ created_at: 10.minutes.ago,
+ updated_at: 10.minutes.ago
+ }
+ ]
+ end
+
+ let_it_be(:url) { URI('/gitlab/v1/repositories/group1/proj1/tags/list/?before=tag1') }
+
+ let_it_be(:response_body) do
+ {
+ pagination: { previous: { uri: url }, next: { uri: url } },
+ response_body: ::Gitlab::Json.parse(raw_tags_response.to_json)
+ }
+ end
+
+ it_behaves_like 'a working graphql query' do # OK
+ before do
+ subject
+ end
+
+ it 'matches the JSON schema' do
+ expect(container_repository_details_response).to match_schema('graphql/container_repository_details')
+ end
+ end
+
+ context 'with different permissions' do # OK
+ let_it_be(:user) { create(:user) }
+
+ let(:tags_response) { container_repository_details_response.dig('tags', 'nodes') }
+
+ where(:project_visibility, :role, :access_granted, :can_delete) do
+ :private | :maintainer | true | true
+ :private | :developer | true | true
+ :private | :reporter | true | false
+ :private | :guest | false | false
+ :private | :anonymous | false | false
+ :public | :maintainer | true | true
+ :public | :developer | true | true
+ :public | :reporter | true | false
+ :public | :guest | true | false
+ :public | :anonymous | true | false
+ end
+
+ with_them do
+ before do
+ project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility.to_s.upcase, false))
+ project.add_member(user, role) unless role == :anonymous
+ end
+
+ it 'return the proper response' do
+ subject
+
+ if access_granted
+ expect(tags_response.size).to eq(raw_tags_response.size)
+ expect(container_repository_details_response.dig('canDelete')).to eq(can_delete)
+ else
+ expect(container_repository_details_response).to eq(nil)
+ end
+ end
+ end
+ end
+
+ context 'querying' do
+ let(:name) { 'l' }
+ let(:tags_response) { container_repository_details_response.dig('tags', 'edges') }
+ let(:variables) do
+ { id: container_repository_global_id, n: name }
+ end
+
+ let(:query) do
+ <<~GQL
+ query($id: ContainerRepositoryID!, $n: String) {
+ containerRepository(id: $id) {
+ tags(name: $n) {
+ edges {
+ node {
+ #{all_graphql_fields_for('ContainerRepositoryTag')}
+ }
+ }
+ }
+ }
+ }
+ GQL
+ end
+
+ it 'returns the tag response', :aggregate_failures do
+ subject
+
+ expect(tags_response.size).to eq(1)
+ expect(tags_response.first.dig('node', 'name')).to eq('latest')
+ end
+
+ context 'invalid filter' do
+ let(:name) { 1 }
+
+ it_behaves_like 'returning an invalid value error'
+ end
+ end
+
+ it_behaves_like 'handling graphql network errors with the container registry'
+ end
end
diff --git a/spec/requests/api/graphql/gitlab_schema_spec.rb b/spec/requests/api/graphql/gitlab_schema_spec.rb
index d55a70f503c..060a1b42cb6 100644
--- a/spec/requests/api/graphql/gitlab_schema_spec.rb
+++ b/spec/requests/api/graphql/gitlab_schema_spec.rb
@@ -113,7 +113,7 @@ RSpec.describe 'GitlabSchema configurations', feature_category: :integrations do
context 'regular queries' do
subject do
- query = graphql_query_for('project', { 'fullPath' => project.full_path }, %w(id name description))
+ query = graphql_query_for('project', { 'fullPath' => project.full_path }, %w[id name description])
post_graphql(query)
end
@@ -125,7 +125,7 @@ RSpec.describe 'GitlabSchema configurations', feature_category: :integrations do
subject do
queries = [
- { query: graphql_query_for('project', { 'fullPath' => '$fullPath' }, %w(id name description)) }, # Complexity 4
+ { query: graphql_query_for('project', { 'fullPath' => '$fullPath' }, %w[id name description]) }, # Complexity 4
{ query: graphql_query_for('echo', { 'text' => "$test" }, []), variables: { "test" => "Hello world" } }, # Complexity 1
{ query: graphql_query_for('project', { 'fullPath' => project.full_path }, "userPermissions { createIssue }") } # Complexity 3
]
@@ -215,7 +215,7 @@ RSpec.describe 'GitlabSchema configurations', feature_category: :integrations do
context "global id's" do
it 'uses GlobalID to expose ids' do
- post_graphql(graphql_query_for('project', { 'fullPath' => project.full_path }, %w(id)),
+ post_graphql(graphql_query_for('project', { 'fullPath' => project.full_path }, %w[id]),
current_user: project.first_owner)
parsed_id = GlobalID.parse(graphql_data['project']['id'])
diff --git a/spec/requests/api/graphql/group/container_repositories_spec.rb b/spec/requests/api/graphql/group/container_repositories_spec.rb
index 51d12261247..9206ead1534 100644
--- a/spec/requests/api/graphql/group/container_repositories_spec.rb
+++ b/spec/requests/api/graphql/group/container_repositories_spec.rb
@@ -49,7 +49,7 @@ RSpec.describe 'getting container repositories in a group', feature_category: :s
group.add_owner(owner)
stub_container_registry_config(enabled: true)
container_repositories.each do |repository|
- stub_container_registry_tags(repository: repository.path, tags: %w(tag1 tag2 tag3), with_manifest: false)
+ stub_container_registry_tags(repository: repository.path, tags: %w[tag1 tag2 tag3], with_manifest: false)
end
end
@@ -142,7 +142,7 @@ RSpec.describe 'getting container repositories in a group', feature_category: :s
end
before do
- stub_container_registry_tags(repository: container_repository.path, tags: %w(tag4 tag5 tag6), with_manifest: false)
+ stub_container_registry_tags(repository: container_repository.path, tags: %w[tag4 tag5 tag6], with_manifest: false)
end
it 'returns the searched container repository' do
diff --git a/spec/requests/api/graphql/group/data_transfer_spec.rb b/spec/requests/api/graphql/group/data_transfer_spec.rb
index b7c038afa54..e17074a0247 100644
--- a/spec/requests/api/graphql/group/data_transfer_spec.rb
+++ b/spec/requests/api/graphql/group/data_transfer_spec.rb
@@ -71,45 +71,21 @@ RSpec.describe 'group data transfers', feature_category: :source_code_management
context 'when user has enough permissions' do
before do
group.add_owner(current_user)
+ subject
end
- context 'when data_transfer_monitoring_mock_data is NOT enabled' do
- before do
- stub_feature_flags(data_transfer_monitoring_mock_data: false)
- subject
- end
-
- it 'returns real results' do
- expect(response).to have_gitlab_http_status(:ok)
+ it 'returns real results' do
+ expect(response).to have_gitlab_http_status(:ok)
- expect(egress_data.count).to eq(2)
+ expect(egress_data.count).to eq(2)
- expect(egress_data.first.keys).to match_array(
- %w[date totalEgress repositoryEgress artifactsEgress packagesEgress registryEgress]
- )
+ expect(egress_data.first.keys).to match_array(
+ %w[date totalEgress repositoryEgress artifactsEgress packagesEgress registryEgress]
+ )
- expect(egress_data.pluck('repositoryEgress')).to match_array(%w[1 6])
- end
-
- it_behaves_like 'a working graphql query'
+ expect(egress_data.pluck('repositoryEgress')).to match_array(%w[1 6])
end
- context 'when data_transfer_monitoring_mock_data is enabled' do
- before do
- stub_feature_flags(data_transfer_monitoring_mock_data: true)
- subject
- end
-
- it 'returns mock results' do
- expect(response).to have_gitlab_http_status(:ok)
-
- expect(egress_data.count).to eq(12)
- expect(egress_data.first.keys).to match_array(
- %w[date totalEgress repositoryEgress artifactsEgress packagesEgress registryEgress]
- )
- end
-
- it_behaves_like 'a working graphql query'
- end
+ it_behaves_like 'a working graphql query'
end
end
diff --git a/spec/requests/api/graphql/group/milestones_spec.rb b/spec/requests/api/graphql/group/milestones_spec.rb
index 209588835f2..6063e6d5293 100644
--- a/spec/requests/api/graphql/group/milestones_spec.rb
+++ b/spec/requests/api/graphql/group/milestones_spec.rb
@@ -136,7 +136,7 @@ RSpec.describe 'Milestones through GroupQuery', feature_category: :team_planning
let_it_be(:closed_issue) { create(:issue, :closed, project: project, milestone: milestone) }
let(:milestone_query) do
- %{
+ %(
id
title
description
@@ -149,7 +149,7 @@ RSpec.describe 'Milestones through GroupQuery', feature_category: :team_planning
projectMilestone
groupMilestone
subgroupMilestone
- }
+ )
end
def post_query
@@ -180,12 +180,12 @@ RSpec.describe 'Milestones through GroupQuery', feature_category: :team_planning
context 'milestone statistics' do
let(:milestone_query) do
- %{
+ %(
stats {
totalIssuesCount
closedIssuesCount
}
- }
+ )
end
it 'returns the correct milestone statistics' do
diff --git a/spec/requests/api/graphql/merge_requests/codequality_reports_comparer_spec.rb b/spec/requests/api/graphql/merge_requests/codequality_reports_comparer_spec.rb
index 2939e9307e9..09a229c2098 100644
--- a/spec/requests/api/graphql/merge_requests/codequality_reports_comparer_spec.rb
+++ b/spec/requests/api/graphql/merge_requests/codequality_reports_comparer_spec.rb
@@ -54,6 +54,7 @@ RSpec.describe 'Query.project.mergeRequest.codequalityReportsComparer', feature_
let(:codequality_reports_comparer_fields) do
<<~QUERY
codequalityReportsComparer {
+ status
report {
status
newErrors {
@@ -138,6 +139,7 @@ RSpec.describe 'Query.project.mergeRequest.codequalityReportsComparer', feature_
expect(result).to match(
a_hash_including(
{
+ status: 'PARSED',
report: {
status: 'FAILED',
newErrors: [
diff --git a/spec/requests/api/graphql/mutations/admin/sidekiq_queues/delete_jobs_spec.rb b/spec/requests/api/graphql/mutations/admin/sidekiq_queues/delete_jobs_spec.rb
index e3a7442ffe6..316b0f3755d 100644
--- a/spec/requests/api/graphql/mutations/admin/sidekiq_queues/delete_jobs_spec.rb
+++ b/spec/requests/api/graphql/mutations/admin/sidekiq_queues/delete_jobs_spec.rb
@@ -20,7 +20,7 @@ RSpec.describe 'Deleting Sidekiq jobs', :clean_gitlab_redis_queues, feature_cate
let(:current_user) { create(:user) }
it_behaves_like 'a mutation that returns top-level errors',
- errors: ['You must be an admin to use this mutation']
+ errors: ['You must be an admin to use this mutation']
end
context 'when the user is an admin' do
@@ -43,7 +43,7 @@ RSpec.describe 'Deleting Sidekiq jobs', :clean_gitlab_redis_queues, feature_cate
raise 'Not enqueued!' if Sidekiq::Queue.new(queue).size.zero?
end
- it 'returns info about the deleted jobs', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/425824' do
+ it 'returns info about the deleted jobs' do
add_job(admin, [1])
add_job(admin, [2])
add_job(create(:user), [3])
@@ -51,9 +51,7 @@ RSpec.describe 'Deleting Sidekiq jobs', :clean_gitlab_redis_queues, feature_cate
post_graphql_mutation(mutation, current_user: admin)
expect(mutation_response['errors']).to be_empty
- expect(mutation_response['result']).to eq('completed' => true,
- 'deletedJobs' => 2,
- 'queueSize' => 1)
+ expect(mutation_response['result']).to eq('completed' => true, 'deletedJobs' => 2, 'queueSize' => 1)
end
end
@@ -61,14 +59,14 @@ RSpec.describe 'Deleting Sidekiq jobs', :clean_gitlab_redis_queues, feature_cate
let(:variables) { { queue_name: queue } }
it_behaves_like 'a mutation that returns errors in the response',
- errors: ['No metadata provided']
+ errors: ['No metadata provided']
end
context 'when the queue does not exist' do
let(:variables) { { user: admin.username, queue_name: 'authorized_projects_2' } }
it_behaves_like 'a mutation that returns top-level errors',
- errors: ['Queue authorized_projects_2 not found']
+ errors: ['Queue authorized_projects_2 not found']
end
end
end
diff --git a/spec/requests/api/graphql/mutations/alert_management/alerts/create_alert_issue_spec.rb b/spec/requests/api/graphql/mutations/alert_management/alerts/create_alert_issue_spec.rb
index fbe6d95dfff..f2b516783e5 100644
--- a/spec/requests/api/graphql/mutations/alert_management/alerts/create_alert_issue_spec.rb
+++ b/spec/requests/api/graphql/mutations/alert_management/alerts/create_alert_issue_spec.rb
@@ -14,21 +14,23 @@ RSpec.describe 'Create an alert issue from an alert', feature_category: :inciden
project_path: project.full_path,
iid: alert.iid.to_s
}
- graphql_mutation(:create_alert_issue, variables,
- <<~QL
- clientMutationId
- errors
- alert {
- iid
- issue {
- iid
- }
- }
- issue {
- iid
- title
- }
- QL
+ graphql_mutation(
+ :create_alert_issue,
+ variables,
+ <<~QL
+ clientMutationId
+ errors
+ alert {
+ iid
+ issue {
+ iid
+ }
+ }
+ issue {
+ iid
+ title
+ }
+ QL
)
end
diff --git a/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb b/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb
index 65a5fb87f9a..e7e23304d81 100644
--- a/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb
+++ b/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb
@@ -6,9 +6,11 @@ RSpec.describe 'Toggling an AwardEmoji', feature_category: :shared do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
- let_it_be(:project, reload: true) { create(:project) }
- let_it_be(:awardable) { create(:note, project: project) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project, reload: true) { create(:project, group: group) }
+ let_it_be(:issue_note) { create(:note, project: project) }
+ let(:awardable) { issue_note }
let(:emoji_name) { 'thumbsup' }
let(:mutation) do
variables = {
@@ -36,8 +38,8 @@ RSpec.describe 'Toggling an AwardEmoji', feature_category: :shared do
end
context 'when the user has permission' do
- before do
- project.add_developer(current_user)
+ before_all do
+ group.add_developer(current_user)
end
context 'when the given awardable is not an Awardable' do
@@ -60,6 +62,33 @@ RSpec.describe 'Toggling an AwardEmoji', feature_category: :shared do
end
context 'when the given awardable is an Awardable' do
+ context 'when the awardable is a work item' do
+ context 'when the work item is associated directly with a group' do
+ let_it_be(:group_work_item) { create(:work_item, :group_level, namespace: group) }
+ let(:awardable) { group_work_item }
+
+ context 'when no emoji has been awarded by the current_user yet' do
+ it 'creates an emoji' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ end.to change { AwardEmoji.count }.by(1)
+ end
+ end
+
+ context 'when an emoji has been awarded by the current_user' do
+ before do
+ create_award_emoji(current_user)
+ end
+
+ it 'removes the emoji' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ end.to change { AwardEmoji.count }.by(-1)
+ end
+ end
+ end
+ end
+
context 'when no emoji has been awarded by the current_user yet' do
# Create an award emoji for another user. This therefore tests that
# toggling is correctly scoped to the user's emoji only.
diff --git a/spec/requests/api/graphql/mutations/boards/issues/issue_move_list_spec.rb b/spec/requests/api/graphql/mutations/boards/issues/issue_move_list_spec.rb
index df64caa1cfb..8e71d77f7bc 100644
--- a/spec/requests/api/graphql/mutations/boards/issues/issue_move_list_spec.rb
+++ b/spec/requests/api/graphql/mutations/boards/issues/issue_move_list_spec.rb
@@ -131,22 +131,24 @@ RSpec.describe 'Reposition and move issue within board lists', feature_category:
end
def mutation(additional_params = {})
- graphql_mutation(mutation_name, issue_move_params.merge(additional_params),
- <<-QL.strip_heredoc
- clientMutationId
- issue {
- iid,
- relativePosition
- labels {
- edges {
- node{
- title
- }
- }
- }
- }
- errors
- QL
+ graphql_mutation(
+ mutation_name,
+ issue_move_params.merge(additional_params),
+ <<-QL.strip_heredoc
+ clientMutationId
+ issue {
+ iid,
+ relativePosition
+ labels {
+ edges {
+ node{
+ title
+ }
+ }
+ }
+ }
+ errors
+ QL
)
end
end
diff --git a/spec/requests/api/graphql/mutations/ci/catalog/resources/create_spec.rb b/spec/requests/api/graphql/mutations/ci/catalog/resources/create_spec.rb
new file mode 100644
index 00000000000..f990cab55f4
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/ci/catalog/resources/create_spec.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'CatalogResourcesCreate', feature_category: :pipeline_composition do
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:project) { create(:project, :catalog_resource_with_components) }
+
+ let(:mutation) do
+ variables = {
+ project_path: project.full_path
+ }
+ graphql_mutation(:catalog_resources_create, variables,
+ <<-QL.strip_heredoc
+ errors
+ QL
+ )
+ end
+
+ context 'when unauthorized' do
+ it_behaves_like 'a mutation that returns a top-level access error'
+ end
+
+ context 'when authorized' do
+ context 'with a valid project' do
+ before_all do
+ project.add_owner(current_user)
+ end
+
+ it 'creates a catalog resource' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(graphql_mutation_response(:catalog_resources_create)['errors']).to be_empty
+ expect(response).to have_gitlab_http_status(:success)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/ci/catalog/unpublish_spec.rb b/spec/requests/api/graphql/mutations/ci/catalog/unpublish_spec.rb
new file mode 100644
index 00000000000..07465777263
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/ci/catalog/unpublish_spec.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'CatalogResourceUnpublish', feature_category: :pipeline_composition do
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:user) }
+ let_it_be_with_reload(:resource) { create(:ci_catalog_resource) }
+
+ let(:mutation) do
+ graphql_mutation(
+ :catalog_resource_unpublish,
+ id: resource.to_gid.to_s
+ )
+ end
+
+ subject(:post_query) { post_graphql_mutation(mutation, current_user: current_user) }
+
+ context 'when unauthorized' do
+ it_behaves_like 'a mutation that returns a top-level access error'
+ end
+
+ context 'when authorized' do
+ before_all do
+ resource.project.add_owner(current_user)
+ end
+
+ context 'when the catalog resource is in published state' do
+ it 'updates the state to draft' do
+ resource.update!(state: :published)
+ expect(resource.state).to eq('published')
+
+ post_query
+
+ expect(resource.reload.state).to eq('draft')
+ expect_graphql_errors_to_be_empty
+ end
+ end
+
+ context 'when the catalog resource is already in draft state' do
+ it 'leaves the state as draft' do
+ expect(resource.state).to eq('draft')
+
+ post_query
+
+ expect(resource.reload.state).to eq('draft')
+ expect_graphql_errors_to_be_empty
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/ci/pipeline_retry_spec.rb b/spec/requests/api/graphql/mutations/ci/pipeline_retry_spec.rb
index e7edc86bea0..70b154946ef 100644
--- a/spec/requests/api/graphql/mutations/ci/pipeline_retry_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/pipeline_retry_spec.rb
@@ -13,13 +13,15 @@ RSpec.describe 'PipelineRetry', feature_category: :continuous_integration do
variables = {
id: pipeline.to_global_id.to_s
}
- graphql_mutation(:pipeline_retry, variables,
- <<-QL
- errors
- pipeline {
- id
- }
- QL
+ graphql_mutation(
+ :pipeline_retry,
+ variables,
+ <<-QL
+ errors
+ pipeline {
+ id
+ }
+ QL
)
end
diff --git a/spec/requests/api/graphql/mutations/clusters/agent_tokens/agent_tokens/create_spec.rb b/spec/requests/api/graphql/mutations/clusters/agent_tokens/agent_tokens/create_spec.rb
index ef0d44395bf..dd4b015409b 100644
--- a/spec/requests/api/graphql/mutations/clusters/agent_tokens/agent_tokens/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/clusters/agent_tokens/agent_tokens/create_spec.rb
@@ -23,8 +23,8 @@ RSpec.describe 'Create a new cluster agent token', feature_category: :deployment
context 'without user permissions' do
it_behaves_like 'a mutation that returns top-level errors',
- errors: ["The resource that you are attempting to access does not exist "\
- "or you don't have permission to perform this action"]
+ errors: ["The resource that you are attempting to access does not exist "\
+ "or you don't have permission to perform this action"]
it 'does not create a token' do
expect { post_graphql_mutation(mutation, current_user: current_user) }.not_to change(Clusters::AgentToken, :count)
diff --git a/spec/requests/api/graphql/mutations/clusters/agents/delete_spec.rb b/spec/requests/api/graphql/mutations/clusters/agents/delete_spec.rb
index b70a6282a7a..a2a093d63e6 100644
--- a/spec/requests/api/graphql/mutations/clusters/agents/delete_spec.rb
+++ b/spec/requests/api/graphql/mutations/clusters/agents/delete_spec.rb
@@ -22,8 +22,8 @@ RSpec.describe 'Delete a cluster agent', feature_category: :deployment_managemen
context 'without project permissions' do
it_behaves_like 'a mutation that returns top-level errors',
- errors: ['The resource that you are attempting to access does not exist '\
- 'or you don\'t have permission to perform this action']
+ errors: ['The resource that you are attempting to access does not exist '\
+ 'or you don\'t have permission to perform this action']
it 'does not delete cluster agent' do
expect { cluster_agent.reload }.not_to raise_error
diff --git a/spec/requests/api/graphql/mutations/container_registry/protection/rule/create_spec.rb b/spec/requests/api/graphql/mutations/container_registry/protection/rule/create_spec.rb
new file mode 100644
index 00000000000..0c708c3dc41
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/container_registry/protection/rule/create_spec.rb
@@ -0,0 +1,180 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Creating the container registry protection rule', :aggregate_failures, feature_category: :container_registry do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user, maintainer_projects: [project]) }
+
+ let(:container_registry_protection_rule_attributes) do
+ build_stubbed(:container_registry_protection_rule, project: project)
+ end
+
+ let(:kwargs) do
+ {
+ project_path: project.full_path,
+ container_path_pattern: container_registry_protection_rule_attributes.container_path_pattern,
+ push_protected_up_to_access_level: 'MAINTAINER',
+ delete_protected_up_to_access_level: 'MAINTAINER'
+ }
+ end
+
+ let(:mutation) do
+ graphql_mutation(:create_container_registry_protection_rule, kwargs,
+ <<~QUERY
+ containerRegistryProtectionRule {
+ id
+ containerPathPattern
+ }
+ clientMutationId
+ errors
+ QUERY
+ )
+ end
+
+ let(:mutation_response) { graphql_mutation_response(:create_container_registry_protection_rule) }
+
+ subject { post_graphql_mutation(mutation, current_user: user) }
+
+ shared_examples 'a successful response' do
+ it { subject.tap { expect_graphql_errors_to_be_empty } }
+
+ it do
+ subject
+
+ expect(mutation_response).to include(
+ 'errors' => be_blank,
+ 'containerRegistryProtectionRule' => {
+ 'id' => be_present,
+ 'containerPathPattern' => kwargs[:container_path_pattern]
+ }
+ )
+ end
+
+ it 'creates container registry protection rule in the database' do
+ expect { subject }.to change { ::ContainerRegistry::Protection::Rule.count }.by(1)
+
+ expect(::ContainerRegistry::Protection::Rule.where(project: project,
+ container_path_pattern: kwargs[:container_path_pattern])).to exist
+ end
+ end
+
+ shared_examples 'an erroneous response' do
+ it { expect { subject }.not_to change { ::ContainerRegistry::Protection::Rule.count } }
+ end
+
+ it_behaves_like 'a successful response'
+
+ context 'with invalid input fields `pushProtectedUpToAccessLevel` and `deleteProtectedUpToAccessLevel`' do
+ let(:kwargs) do
+ super().merge(
+ push_protected_up_to_access_level: 'UNKNOWN_ACCESS_LEVEL',
+ delete_protected_up_to_access_level: 'UNKNOWN_ACCESS_LEVEL'
+ )
+ end
+
+ it_behaves_like 'an erroneous response'
+
+ it {
+ subject
+
+ expect_graphql_errors_to_include([/pushProtectedUpToAccessLevel/, /deleteProtectedUpToAccessLevel/])
+ }
+ end
+
+ context 'with invalid input field `containerPathPattern`' do
+ let(:kwargs) do
+ super().merge(container_path_pattern: '')
+ end
+
+ it_behaves_like 'an erroneous response'
+
+ it { subject.tap { expect_graphql_errors_to_be_empty } }
+
+ it {
+ subject.tap do
+ expect(mutation_response['errors']).to eq ["Container path pattern can't be blank"]
+ end
+ }
+ end
+
+ context 'with existing containers protection rule' do
+ let_it_be(:existing_container_registry_protection_rule) do
+ create(:container_registry_protection_rule, project: project,
+ push_protected_up_to_access_level: Gitlab::Access::DEVELOPER)
+ end
+
+ context 'when container name pattern is slightly different' do
+ let(:kwargs) do
+ # The field `container_path_pattern` is unique; this is why we change the value in a minimum way
+ super().merge(
+ container_path_pattern: "#{existing_container_registry_protection_rule.container_path_pattern}-unique"
+ )
+ end
+
+ it_behaves_like 'a successful response'
+
+ it 'adds another container registry protection rule to the database' do
+ expect { subject }.to change { ::ContainerRegistry::Protection::Rule.count }.from(1).to(2)
+ end
+ end
+
+ context 'when field `container_path_pattern` is taken' do
+ let(:kwargs) do
+ super().merge(container_path_pattern: existing_container_registry_protection_rule.container_path_pattern,
+ push_protected_up_to_access_level: 'MAINTAINER')
+ end
+
+ it_behaves_like 'an erroneous response'
+
+ it { subject.tap { expect_graphql_errors_to_be_empty } }
+
+ it 'returns without error' do
+ subject
+
+ expect(mutation_response['errors']).to eq ['Container path pattern has already been taken']
+ end
+
+ it 'does not create new container protection rules' do
+ expect(::ContainerRegistry::Protection::Rule.where(project: project,
+ container_path_pattern: kwargs[:container_path_pattern],
+ push_protected_up_to_access_level: Gitlab::Access::MAINTAINER)).not_to exist
+ end
+ end
+ end
+
+ context 'when user does not have permission' do
+ let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } }
+ let_it_be(:reporter) { create(:user).tap { |u| project.add_reporter(u) } }
+ let_it_be(:guest) { create(:user).tap { |u| project.add_guest(u) } }
+ let_it_be(:anonymous) { create(:user) }
+
+ where(:user) do
+ [ref(:developer), ref(:reporter), ref(:guest), ref(:anonymous)]
+ end
+
+ with_them do
+ it_behaves_like 'an erroneous response'
+
+ it { subject.tap { expect_graphql_errors_to_include(/you don't have permission to perform this action/) } }
+ end
+ end
+
+ context "when feature flag ':container_registry_protected_containers' disabled" do
+ before do
+ stub_feature_flags(container_registry_protected_containers: false)
+ end
+
+ it_behaves_like 'an erroneous response'
+
+ it { subject.tap { expect(::ContainerRegistry::Protection::Rule.where(project: project)).not_to exist } }
+
+ it 'returns error of disabled feature flag' do
+ subject.tap do
+ expect_graphql_errors_to_include(/'container_registry_protected_containers' feature flag is disabled/)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/container_repository/destroy_tags_spec.rb b/spec/requests/api/graphql/mutations/container_repository/destroy_tags_spec.rb
index 0cb607e13ec..7ced22890df 100644
--- a/spec/requests/api/graphql/mutations/container_repository/destroy_tags_spec.rb
+++ b/spec/requests/api/graphql/mutations/container_repository/destroy_tags_spec.rb
@@ -27,7 +27,7 @@ RSpec.describe 'Destroying a container repository tags', feature_category: :cont
shared_examples 'destroying the container repository tags' do
before do
stub_delete_reference_requests(tags)
- expect_delete_tag_by_names(tags)
+ expect_delete_tags(tags)
allow_next_instance_of(ContainerRegistry::Client) do |client|
allow(client).to receive(:supports_tag_delete?).and_return(true)
end
diff --git a/spec/requests/api/graphql/mutations/design_management/delete_spec.rb b/spec/requests/api/graphql/mutations/design_management/delete_spec.rb
index 7ea32ae6d19..6f421abc489 100644
--- a/spec/requests/api/graphql/mutations/design_management/delete_spec.rb
+++ b/spec/requests/api/graphql/mutations/design_management/delete_spec.rb
@@ -47,28 +47,28 @@ RSpec.describe "deleting designs", feature_category: :design_management do
context 'the designs list is empty' do
it_behaves_like 'a failed request' do
let(:designs) { [] }
- let(:the_error) { a_string_matching %r/no filenames/ }
+ let(:the_error) { a_string_matching %r{no filenames} }
end
end
context 'the designs list contains filenames we cannot find' do
it_behaves_like 'a failed request' do
- let(:designs) { %w/foo bar baz/.map { |fn| double('file', filename: fn) } }
- let(:the_error) { a_string_matching %r/filenames were not found/ }
+ let(:designs) { %w[foo bar baz].map { |fn| double('file', filename: fn) } }
+ let(:the_error) { a_string_matching %r{filenames were not found} }
end
end
context 'the current user does not have developer access' do
it_behaves_like 'a failed request' do
let(:current_user) { create(:user) }
- let(:the_error) { a_string_matching %r/you don't have permission/ }
+ let(:the_error) { a_string_matching %r{you don't have permission} }
end
end
context "when the issue does not exist" do
it_behaves_like 'a failed request' do
let(:variables) { { iid: "1234567890" } }
- let(:the_error) { a_string_matching %r/does not exist/ }
+ let(:the_error) { a_string_matching %r{does not exist} }
end
end
diff --git a/spec/requests/api/graphql/mutations/design_management/upload_spec.rb b/spec/requests/api/graphql/mutations/design_management/upload_spec.rb
index 9b42b32c150..82a88a2c593 100644
--- a/spec/requests/api/graphql/mutations/design_management/upload_spec.rb
+++ b/spec/requests/api/graphql/mutations/design_management/upload_spec.rb
@@ -36,10 +36,11 @@ RSpec.describe "uploading designs", feature_category: :design_management do
end
it 'returns an error' do
- workhorse_post_with_file(api('/', current_user, version: 'graphql'),
- params: params,
- file_key: '1'
- )
+ workhorse_post_with_file(
+ api('/', current_user, version: 'graphql'),
+ params: params,
+ file_key: '1'
+ )
expect(response).to have_attributes(
code: eq('400'),
diff --git a/spec/requests/api/graphql/mutations/issues/link_alerts_spec.rb b/spec/requests/api/graphql/mutations/issues/link_alerts_spec.rb
index 85e21952f47..df6c20d6176 100644
--- a/spec/requests/api/graphql/mutations/issues/link_alerts_spec.rb
+++ b/spec/requests/api/graphql/mutations/issues/link_alerts_spec.rb
@@ -19,19 +19,21 @@ RSpec.describe 'Link alerts to an incident', feature_category: :incident_managem
alert_references: [alert1.to_reference, alert2.details_url]
}
- graphql_mutation(:issue_link_alerts, variables,
- <<-QL.strip_heredoc
- clientMutationId
- errors
- issue {
- iid
- alertManagementAlerts {
- nodes {
- iid
- }
- }
- }
- QL
+ graphql_mutation(
+ :issue_link_alerts,
+ variables,
+ <<-QL.strip_heredoc
+ clientMutationId
+ errors
+ issue {
+ iid
+ alertManagementAlerts {
+ nodes {
+ iid
+ }
+ }
+ }
+ QL
)
end
diff --git a/spec/requests/api/graphql/mutations/issues/move_spec.rb b/spec/requests/api/graphql/mutations/issues/move_spec.rb
index 7d9579067b6..24188d5341d 100644
--- a/spec/requests/api/graphql/mutations/issues/move_spec.rb
+++ b/spec/requests/api/graphql/mutations/issues/move_spec.rb
@@ -16,14 +16,16 @@ RSpec.describe 'Moving an issue', feature_category: :team_planning do
iid: issue.iid.to_s
}
- graphql_mutation(:issue_move, variables,
- <<-QL.strip_heredoc
- clientMutationId
- errors
- issue {
- title
- }
- QL
+ graphql_mutation(
+ :issue_move,
+ variables,
+ <<-QL.strip_heredoc
+ clientMutationId
+ errors
+ issue {
+ title
+ }
+ QL
)
end
diff --git a/spec/requests/api/graphql/mutations/issues/set_confidential_spec.rb b/spec/requests/api/graphql/mutations/issues/set_confidential_spec.rb
index c5e6901d8f8..c62995c0b9b 100644
--- a/spec/requests/api/graphql/mutations/issues/set_confidential_spec.rb
+++ b/spec/requests/api/graphql/mutations/issues/set_confidential_spec.rb
@@ -15,15 +15,17 @@ RSpec.describe 'Setting an issue as confidential', feature_category: :team_plann
project_path: project.full_path,
iid: issue.iid.to_s
}
- graphql_mutation(:issue_set_confidential, variables.merge(input),
- <<-QL.strip_heredoc
- clientMutationId
- errors
- issue {
- iid
- confidential
- }
- QL
+ graphql_mutation(
+ :issue_set_confidential,
+ variables.merge(input),
+ <<-QL.strip_heredoc
+ clientMutationId
+ errors
+ issue {
+ iid
+ confidential
+ }
+ QL
)
end
diff --git a/spec/requests/api/graphql/mutations/issues/set_crm_contacts_spec.rb b/spec/requests/api/graphql/mutations/issues/set_crm_contacts_spec.rb
index 497ae1cc13f..cdab267162e 100644
--- a/spec/requests/api/graphql/mutations/issues/set_crm_contacts_spec.rb
+++ b/spec/requests/api/graphql/mutations/issues/set_crm_contacts_spec.rb
@@ -26,18 +26,20 @@ RSpec.describe 'Setting issues crm contacts', feature_category: :service_desk do
contact_ids: contact_ids
}
- graphql_mutation(:issue_set_crm_contacts, variables,
- <<-QL.strip_heredoc
- clientMutationId
- errors
- issue {
- customerRelationsContacts {
- nodes {
- id
- }
- }
- }
- QL
+ graphql_mutation(
+ :issue_set_crm_contacts,
+ variables,
+ <<-QL.strip_heredoc
+ clientMutationId
+ errors
+ issue {
+ customerRelationsContacts {
+ nodes {
+ id
+ }
+ }
+ }
+ QL
)
end
diff --git a/spec/requests/api/graphql/mutations/issues/set_due_date_spec.rb b/spec/requests/api/graphql/mutations/issues/set_due_date_spec.rb
index 1a5a64e4196..f7c5febe56f 100644
--- a/spec/requests/api/graphql/mutations/issues/set_due_date_spec.rb
+++ b/spec/requests/api/graphql/mutations/issues/set_due_date_spec.rb
@@ -15,15 +15,17 @@ RSpec.describe 'Setting Due Date of an issue', feature_category: :team_planning
project_path: project.full_path,
iid: issue.iid.to_s
}
- graphql_mutation(:issue_set_due_date, variables.merge(input),
- <<-QL.strip_heredoc
- clientMutationId
- errors
- issue {
- iid
- dueDate
- }
- QL
+ graphql_mutation(
+ :issue_set_due_date,
+ variables.merge(input),
+ <<-QL.strip_heredoc
+ clientMutationId
+ errors
+ issue {
+ iid
+ dueDate
+ }
+ QL
)
end
@@ -68,7 +70,7 @@ RSpec.describe 'Setting Due Date of an issue', feature_category: :team_planning
it 'returns an error' do
post_graphql_mutation(mutation, current_user: current_user)
- expect(graphql_errors).to include(a_hash_including('message' => /Arguments must be provided: dueDate/))
+ expect(graphql_errors).to include(a_hash_including('message' => 'issueSetDueDate has the wrong arguments'))
end
end
diff --git a/spec/requests/api/graphql/mutations/issues/set_locked_spec.rb b/spec/requests/api/graphql/mutations/issues/set_locked_spec.rb
index a8025894b1e..547ec280150 100644
--- a/spec/requests/api/graphql/mutations/issues/set_locked_spec.rb
+++ b/spec/requests/api/graphql/mutations/issues/set_locked_spec.rb
@@ -16,15 +16,17 @@ RSpec.describe 'Setting an issue as locked', feature_category: :team_planning do
project_path: project.full_path,
iid: issue.iid.to_s
}
- graphql_mutation(:issue_set_locked, variables.merge(input),
- <<-QL.strip_heredoc
- clientMutationId
- errors
- issue {
- iid
- discussionLocked
- }
- QL
+ graphql_mutation(
+ :issue_set_locked,
+ variables.merge(input),
+ <<-QL.strip_heredoc
+ clientMutationId
+ errors
+ issue {
+ iid
+ discussionLocked
+ }
+ QL
)
end
diff --git a/spec/requests/api/graphql/mutations/issues/set_severity_spec.rb b/spec/requests/api/graphql/mutations/issues/set_severity_spec.rb
index 77262c7f64f..d53b938a983 100644
--- a/spec/requests/api/graphql/mutations/issues/set_severity_spec.rb
+++ b/spec/requests/api/graphql/mutations/issues/set_severity_spec.rb
@@ -17,15 +17,17 @@ RSpec.describe 'Setting severity level of an incident', feature_category: :incid
iid: incident.iid.to_s
}
- graphql_mutation(:issue_set_severity, variables.merge(input),
- <<-QL.strip_heredoc
- clientMutationId
- errors
- issue {
- iid
- severity
- }
- QL
+ graphql_mutation(
+ :issue_set_severity,
+ variables.merge(input),
+ <<-QL.strip_heredoc
+ clientMutationId
+ errors
+ issue {
+ iid
+ severity
+ }
+ QL
)
end
diff --git a/spec/requests/api/graphql/mutations/issues/unlink_alerts_spec.rb b/spec/requests/api/graphql/mutations/issues/unlink_alerts_spec.rb
index 7f6f968b1dd..807afdfb812 100644
--- a/spec/requests/api/graphql/mutations/issues/unlink_alerts_spec.rb
+++ b/spec/requests/api/graphql/mutations/issues/unlink_alerts_spec.rb
@@ -21,19 +21,21 @@ RSpec.describe 'Unlink alert from an incident', feature_category: :incident_mana
alert_id: alert_to_unlink.to_global_id.to_s
}
- graphql_mutation(:issue_unlink_alert, variables,
- <<-QL.strip_heredoc
- clientMutationId
- errors
- issue {
- iid
- alertManagementAlerts {
- nodes {
- id
- }
- }
- }
- QL
+ graphql_mutation(
+ :issue_unlink_alert,
+ variables,
+ <<-QL.strip_heredoc
+ clientMutationId
+ errors
+ issue {
+ iid
+ alertManagementAlerts {
+ nodes {
+ id
+ }
+ }
+ }
+ QL
)
end
diff --git a/spec/requests/api/graphql/mutations/merge_requests/reviewer_rereview_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/reviewer_rereview_spec.rb
index 7a1b3982111..ec82941b094 100644
--- a/spec/requests/api/graphql/mutations/merge_requests/reviewer_rereview_spec.rb
+++ b/spec/requests/api/graphql/mutations/merge_requests/reviewer_rereview_spec.rb
@@ -16,11 +16,13 @@ RSpec.describe 'Setting assignees of a merge request', feature_category: :code_r
project_path: project.full_path,
iid: merge_request.iid.to_s
}
- graphql_mutation(:merge_request_reviewer_rereview, variables.merge(input),
- <<-QL.strip_heredoc
- clientMutationId
- errors
- QL
+ graphql_mutation(
+ :merge_request_reviewer_rereview,
+ variables.merge(input),
+ <<-QL.strip_heredoc
+ clientMutationId
+ errors
+ QL
)
end
diff --git a/spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb
index 4a7d1083f2e..cb7bac771b3 100644
--- a/spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb
+++ b/spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb
@@ -21,19 +21,21 @@ RSpec.describe 'Setting assignees of a merge request', :assume_throttled, featur
project_path: project.full_path,
iid: merge_request.iid.to_s
}
- graphql_mutation(:merge_request_set_assignees, variables.merge(input),
- <<-QL.strip_heredoc
- clientMutationId
- errors
- mergeRequest {
- id
- assignees {
- nodes {
- username
- }
- }
- }
- QL
+ graphql_mutation(
+ :merge_request_set_assignees,
+ variables.merge(input),
+ <<-QL.strip_heredoc
+ clientMutationId
+ errors
+ mergeRequest {
+ id
+ assignees {
+ nodes {
+ username
+ }
+ }
+ }
+ QL
)
end
diff --git a/spec/requests/api/graphql/mutations/merge_requests/set_draft_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/set_draft_spec.rb
index 0c2e2975350..a2c5c235d25 100644
--- a/spec/requests/api/graphql/mutations/merge_requests/set_draft_spec.rb
+++ b/spec/requests/api/graphql/mutations/merge_requests/set_draft_spec.rb
@@ -15,15 +15,17 @@ RSpec.describe 'Setting Draft status of a merge request', feature_category: :cod
project_path: project.full_path,
iid: merge_request.iid.to_s
}
- graphql_mutation(:merge_request_set_draft, variables.merge(input),
- <<-QL.strip_heredoc
- clientMutationId
- errors
- mergeRequest {
- id
- title
- }
- QL
+ graphql_mutation(
+ :merge_request_set_draft,
+ variables.merge(input),
+ <<-QL.strip_heredoc
+ clientMutationId
+ errors
+ mergeRequest {
+ id
+ title
+ }
+ QL
)
end
diff --git a/spec/requests/api/graphql/mutations/merge_requests/set_labels_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/set_labels_spec.rb
index e40a3cf7ce9..4ddd10b1734 100644
--- a/spec/requests/api/graphql/mutations/merge_requests/set_labels_spec.rb
+++ b/spec/requests/api/graphql/mutations/merge_requests/set_labels_spec.rb
@@ -17,19 +17,21 @@ RSpec.describe 'Setting labels of a merge request' do
project_path: project.full_path,
iid: merge_request.iid.to_s
}
- graphql_mutation(:merge_request_set_labels, variables.merge(input),
- <<-QL.strip_heredoc
- clientMutationId
- errors
- mergeRequest {
- id
- labels {
- nodes {
- id
- }
- }
- }
- QL
+ graphql_mutation(
+ :merge_request_set_labels,
+ variables.merge(input),
+ <<-QL.strip_heredoc
+ clientMutationId
+ errors
+ mergeRequest {
+ id
+ labels {
+ nodes {
+ id
+ }
+ }
+ }
+ QL
)
end
diff --git a/spec/requests/api/graphql/mutations/merge_requests/set_locked_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/set_locked_spec.rb
index 73a38adf723..a6ddb9beb5c 100644
--- a/spec/requests/api/graphql/mutations/merge_requests/set_locked_spec.rb
+++ b/spec/requests/api/graphql/mutations/merge_requests/set_locked_spec.rb
@@ -15,15 +15,17 @@ RSpec.describe 'Setting locked status of a merge request', feature_category: :co
project_path: project.full_path,
iid: merge_request.iid.to_s
}
- graphql_mutation(:merge_request_set_locked, variables.merge(input),
- <<-QL.strip_heredoc
- clientMutationId
- errors
- mergeRequest {
- id
- discussionLocked
- }
- QL
+ graphql_mutation(
+ :merge_request_set_locked,
+ variables.merge(input),
+ <<-QL.strip_heredoc
+ clientMutationId
+ errors
+ mergeRequest {
+ id
+ discussionLocked
+ }
+ QL
)
end
diff --git a/spec/requests/api/graphql/mutations/merge_requests/set_milestone_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/set_milestone_spec.rb
index 1898ee5a62d..9debfbd474b 100644
--- a/spec/requests/api/graphql/mutations/merge_requests/set_milestone_spec.rb
+++ b/spec/requests/api/graphql/mutations/merge_requests/set_milestone_spec.rb
@@ -16,17 +16,19 @@ RSpec.describe 'Setting milestone of a merge request', feature_category: :code_r
project_path: project.full_path,
iid: merge_request.iid.to_s
}
- graphql_mutation(:merge_request_set_milestone, variables.merge(input),
- <<-QL.strip_heredoc
- clientMutationId
- errors
- mergeRequest {
- id
- milestone {
- id
- }
- }
- QL
+ graphql_mutation(
+ :merge_request_set_milestone,
+ variables.merge(input),
+ <<-QL.strip_heredoc
+ clientMutationId
+ errors
+ mergeRequest {
+ id
+ milestone {
+ id
+ }
+ }
+ QL
)
end
diff --git a/spec/requests/api/graphql/mutations/merge_requests/set_reviewers_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/set_reviewers_spec.rb
index fd87112be33..c9efba689c2 100644
--- a/spec/requests/api/graphql/mutations/merge_requests/set_reviewers_spec.rb
+++ b/spec/requests/api/graphql/mutations/merge_requests/set_reviewers_spec.rb
@@ -21,19 +21,21 @@ RSpec.describe 'Setting reviewers of a merge request', :assume_throttled, featur
project_path: project.full_path,
iid: merge_request.iid.to_s
}
- graphql_mutation(:merge_request_set_reviewers, variables.merge(input),
- <<-QL.strip_heredoc
- clientMutationId
- errors
- mergeRequest {
- id
- reviewers {
- nodes {
- username
- }
- }
- }
- QL
+ graphql_mutation(
+ :merge_request_set_reviewers,
+ variables.merge(input),
+ <<-QL.strip_heredoc
+ clientMutationId
+ errors
+ mergeRequest {
+ id
+ reviewers {
+ nodes {
+ username
+ }
+ }
+ }
+ QL
)
end
diff --git a/spec/requests/api/graphql/mutations/merge_requests/set_time_estimate_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/set_time_estimate_spec.rb
index 6bc130a97cf..541cdf0660d 100644
--- a/spec/requests/api/graphql/mutations/merge_requests/set_time_estimate_spec.rb
+++ b/spec/requests/api/graphql/mutations/merge_requests/set_time_estimate_spec.rb
@@ -17,8 +17,11 @@ RSpec.describe 'Setting time estimate of a merge request', feature_category: :co
let(:extra_params) { { project_path: project.full_path } }
let(:input_params) { input.merge(extra_params) }
- let(:mutation) { graphql_mutation(:merge_request_update, input_params, nil, ['productAnalyticsState']) }
let(:mutation_response) { graphql_mutation_response(:merge_request_update) }
+ let(:mutation) do
+ # exclude codequalityReportsComparer because it's behind a feature flag
+ graphql_mutation(:merge_request_update, input_params, nil, %w[productAnalyticsState codequalityReportsComparer])
+ end
context 'when the user is not allowed to update a merge request' do
before_all do
diff --git a/spec/requests/api/graphql/mutations/merge_requests/update_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/update_spec.rb
index 48db23569b6..ef21f77d818 100644
--- a/spec/requests/api/graphql/mutations/merge_requests/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/merge_requests/update_spec.rb
@@ -12,8 +12,11 @@ RSpec.describe 'Update of an existing merge request', feature_category: :code_re
let(:input) { { 'iid' => merge_request.iid.to_s } }
let(:extra_params) { { project_path: project.full_path } }
let(:input_params) { input.merge(extra_params) }
- let(:mutation) { graphql_mutation(:merge_request_update, input_params, nil, ['productAnalyticsState']) }
let(:mutation_response) { graphql_mutation_response(:merge_request_update) }
+ let(:mutation) do
+ # exclude codequalityReportsComparer because it's behind a feature flag
+ graphql_mutation(:merge_request_update, input_params, nil, %w[productAnalyticsState codequalityReportsComparer])
+ end
context 'when the user is not allowed to update the merge request' do
it_behaves_like 'a mutation that returns a top-level access error'
@@ -28,5 +31,17 @@ RSpec.describe 'Update of an existing merge request', feature_category: :code_re
let(:resource) { merge_request }
let(:mutation_name) { 'mergeRequestUpdate' }
end
+
+ context 'when required arguments are missing' do
+ let(:input_params) { {} }
+
+ it_behaves_like 'a mutation that returns top-level errors' do
+ let(:match_errors) do
+ include(end_with(
+ 'invalid value for projectPath (Expected value to not be null), iid (Expected value to not be null)'
+ ))
+ end
+ end
+ end
end
end
diff --git a/spec/requests/api/graphql/mutations/namespace/package_settings/update_spec.rb b/spec/requests/api/graphql/mutations/namespace/package_settings/update_spec.rb
index 480e184a60c..738dc3078e7 100644
--- a/spec/requests/api/graphql/mutations/namespace/package_settings/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/namespace/package_settings/update_spec.rb
@@ -129,26 +129,6 @@ RSpec.describe 'Updating the package settings', feature_category: :package_regis
it_behaves_like 'returning a success'
it_behaves_like 'rejecting invalid regex'
-
- context 'when nuget_duplicates_option FF is disabled' do
- let(:params) do
- {
- namespace_path: namespace.full_path,
- 'nugetDuplicatesAllowed' => false
- }
- end
-
- before do
- stub_feature_flags(nuget_duplicates_option: false)
- end
-
- it 'raises an error', :aggregate_failures do
- subject
-
- expect(graphql_errors.size).to eq(1)
- expect(graphql_errors.first['message']).to include('feature flag is disabled')
- end
- end
end
RSpec.shared_examples 'accepting the mutation request creating the package settings' do
diff --git a/spec/requests/api/graphql/mutations/notes/create/note_spec.rb b/spec/requests/api/graphql/mutations/notes/create/note_spec.rb
index 37bcdf61d23..33d840cafd7 100644
--- a/spec/requests/api/graphql/mutations/notes/create/note_spec.rb
+++ b/spec/requests/api/graphql/mutations/notes/create/note_spec.rb
@@ -5,13 +5,15 @@ require 'spec_helper'
RSpec.describe 'Adding a Note', feature_category: :team_planning do
include GraphqlHelpers
- let_it_be(:current_user) { create(:user) }
-
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group).tap { |g| g.add_developer(developer) } }
+ let_it_be_with_reload(:project) { create(:project, :repository, group: group) }
+ let_it_be(:developer) { create(:user).tap { |u| group.add_developer(u) } }
let(:noteable) { create(:merge_request, source_project: project, target_project: project) }
- let(:project) { create(:project, :repository) }
let(:discussion) { nil }
let(:head_sha) { nil }
let(:body) { 'Body text' }
+ let(:current_user) { user }
let(:mutation) do
variables = {
noteable_id: GitlabSchema.id_from_object(noteable).to_s,
@@ -30,9 +32,7 @@ RSpec.describe 'Adding a Note', feature_category: :team_planning do
it_behaves_like 'a Note mutation when the user does not have permission'
context 'when the user has permission' do
- before do
- project.add_developer(current_user)
- end
+ let(:current_user) { developer }
it_behaves_like 'a working GraphQL mutation'
@@ -78,8 +78,10 @@ RSpec.describe 'Adding a Note', feature_category: :team_planning do
end
context 'for an issue' do
- let(:noteable) { create(:issue, project: project) }
+ let_it_be_with_reload(:issue) { create(:issue, project: project) }
+ let(:noteable) { issue }
let(:mutation) { graphql_mutation(:create_note, variables) }
+ let(:variables_extra) { {} }
let(:variables) do
{
noteable_id: GitlabSchema.id_from_object(noteable).to_s,
@@ -87,10 +89,6 @@ RSpec.describe 'Adding a Note', feature_category: :team_planning do
}.merge(variables_extra)
end
- before do
- project.add_developer(current_user)
- end
-
context 'when using internal param' do
let(:variables_extra) { { internal: true } }
@@ -104,8 +102,8 @@ RSpec.describe 'Adding a Note', feature_category: :team_planning do
end
context 'as work item' do
- let_it_be(:project) { create(:project) }
- let_it_be(:noteable) { create(:work_item, project: project) }
+ let_it_be_with_reload(:work_item) { create(:work_item, :task, project: project) }
+ let(:noteable) { work_item }
context 'when using internal param' do
let(:variables_extra) { { internal: true } }
@@ -120,10 +118,8 @@ RSpec.describe 'Adding a Note', feature_category: :team_planning do
end
context 'without notes widget' do
- let(:variables_extra) { {} }
-
before do
- WorkItems::Type.default_by_type(:issue).widget_definitions.find_by_widget_type(:notes)
+ WorkItems::Type.default_by_type(:task).widget_definitions.find_by_widget_type(:notes)
.update!(disabled: true)
end
@@ -133,10 +129,6 @@ RSpec.describe 'Adding a Note', feature_category: :team_planning do
end
context 'when body contains quick actions' do
- let_it_be(:noteable) { create(:work_item, :task, project: project) }
-
- let(:variables_extra) { {} }
-
it_behaves_like 'work item supports labels widget updates via quick actions'
it_behaves_like 'work item does not support labels widget updates via quick actions'
it_behaves_like 'work item supports assignee widget updates via quick actions'
@@ -145,6 +137,13 @@ RSpec.describe 'Adding a Note', feature_category: :team_planning do
it_behaves_like 'work item does not support start and due date widget updates via quick actions'
it_behaves_like 'work item supports type change via quick actions'
end
+
+ context 'when work item is directly associated with a group' do
+ let_it_be_with_reload(:group_work_item) { create(:work_item, :group_level, :task, namespace: group) }
+ let(:noteable) { group_work_item }
+
+ it_behaves_like 'a Note mutation that creates a Note'
+ end
end
end
@@ -152,10 +151,6 @@ RSpec.describe 'Adding a Note', feature_category: :team_planning do
let(:head_sha) { noteable.diff_head_sha }
let(:body) { '/merge' }
- before do
- project.add_developer(current_user)
- end
-
# NOTE: Known issue https://gitlab.com/gitlab-org/gitlab/-/issues/346557
it 'returns a nil note and info about the command in errors' do
post_graphql_mutation(mutation, current_user: current_user)
diff --git a/spec/requests/api/graphql/mutations/notes/update/image_diff_note_spec.rb b/spec/requests/api/graphql/mutations/notes/update/image_diff_note_spec.rb
index a5cd3c8b019..3f071a6d987 100644
--- a/spec/requests/api/graphql/mutations/notes/update/image_diff_note_spec.rb
+++ b/spec/requests/api/graphql/mutations/notes/update/image_diff_note_spec.rb
@@ -37,11 +37,13 @@ RSpec.describe 'Updating an image DiffNote', feature_category: :team_planning do
end
let!(:diff_note) do
- create(:image_diff_note_on_merge_request,
- noteable: noteable,
- project: noteable.project,
- note: original_body,
- position: original_position)
+ create(
+ :image_diff_note_on_merge_request,
+ noteable: noteable,
+ project: noteable.project,
+ note: original_body,
+ position: original_position
+ )
end
let(:mutation) do
diff --git a/spec/requests/api/graphql/mutations/organizations/create_spec.rb b/spec/requests/api/graphql/mutations/organizations/create_spec.rb
new file mode 100644
index 00000000000..ac6b04104ba
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/organizations/create_spec.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::Organizations::Create, feature_category: :cell do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+
+ let(:mutation) { graphql_mutation(:organization_create, params) }
+ let(:name) { 'Name' }
+ let(:path) { 'path' }
+ let(:params) do
+ {
+ name: name,
+ path: path
+ }
+ end
+
+ subject(:create_organization) { post_graphql_mutation(mutation, current_user: current_user) }
+
+ it { expect(described_class).to require_graphql_authorizations(:create_organization) }
+
+ def mutation_response
+ graphql_mutation_response(:organization_create)
+ end
+
+ context 'when the user does not have permission' do
+ let(:current_user) { nil }
+
+ it_behaves_like 'a mutation that returns a top-level access error'
+
+ it 'does not create an organization' do
+ expect { create_organization }.not_to change { Organizations::Organization.count }
+ end
+ end
+
+ context 'when the user has permission' do
+ let(:current_user) { user }
+
+ context 'when the params are invalid' do
+ let(:name) { '' }
+
+ it 'returns the validation error' do
+ create_organization
+
+ expect(mutation_response).to include('errors' => ["Name can't be blank"])
+ end
+ end
+
+ it 'creates an organization' do
+ expect { create_organization }.to change { Organizations::Organization.count }.by(1)
+ end
+
+ it 'returns the new organization' do
+ create_organization
+
+ expect(graphql_data_at(:organization_create, :organization)).to match a_hash_including(
+ 'name' => name,
+ 'path' => path
+ )
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/packages/cleanup/policy/update_spec.rb b/spec/requests/api/graphql/mutations/packages/cleanup/policy/update_spec.rb
index 2540e06be9a..5843109f356 100644
--- a/spec/requests/api/graphql/mutations/packages/cleanup/policy/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/packages/cleanup/policy/update_spec.rb
@@ -17,14 +17,16 @@ RSpec.describe 'Updating the packages cleanup policy', feature_category: :packag
end
let(:mutation) do
- graphql_mutation(:update_packages_cleanup_policy, params,
- <<~QUERY
- packagesCleanupPolicy {
- keepNDuplicatedPackageFiles
- nextRunAt
- }
- errors
- QUERY
+ graphql_mutation(
+ :update_packages_cleanup_policy,
+ params,
+ <<~QUERY
+ packagesCleanupPolicy {
+ keepNDuplicatedPackageFiles
+ nextRunAt
+ }
+ errors
+ QUERY
)
end
diff --git a/spec/requests/api/graphql/mutations/packages/protection/rule/create_spec.rb b/spec/requests/api/graphql/mutations/packages/protection/rule/create_spec.rb
index b0c8526fa1c..ae5b6a5af95 100644
--- a/spec/requests/api/graphql/mutations/packages/protection/rule/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/packages/protection/rule/create_spec.rb
@@ -4,7 +4,6 @@ require 'spec_helper'
RSpec.describe 'Creating the packages protection rule', :aggregate_failures, feature_category: :package_registry do
include GraphqlHelpers
- using RSpec::Parameterized::TableSyntax
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user, maintainer_projects: [project]) }
@@ -15,151 +14,162 @@ RSpec.describe 'Creating the packages protection rule', :aggregate_failures, fea
{
project_path: project.full_path,
package_name_pattern: package_protection_rule_attributes.package_name_pattern,
- package_type: "NPM",
- push_protected_up_to_access_level: "MAINTAINER"
+ package_type: 'NPM',
+ push_protected_up_to_access_level: 'MAINTAINER'
}
end
let(:mutation) do
graphql_mutation(:create_packages_protection_rule, kwargs,
<<~QUERY
- clientMutationId
+ packageProtectionRule {
+ id
+ packageNamePattern
+ packageType
+ pushProtectedUpToAccessLevel
+ }
errors
QUERY
)
end
- let(:mutation_response) { graphql_mutation_response(:create_packages_protection_rule) }
+ let(:mutation_response_package_protection_rule) do
+ graphql_data_at(:createPackagesProtectionRule, :packageProtectionRule)
+ end
- describe 'post graphql mutation' do
- subject { post_graphql_mutation(mutation, current_user: user) }
+ let(:mutation_response_errors) { graphql_data_at(:createPackagesProtectionRule, :errors) }
- context 'without existing packages protection rule' do
- it 'returns without error' do
- subject
+ subject { post_graphql_mutation(mutation, current_user: user) }
- expect_graphql_errors_to_be_empty
- end
+ shared_examples 'a successful response' do
+ it 'returns without error' do
+ subject
- it 'returns the created packages protection rule' do
- expect { subject }.to change { ::Packages::Protection::Rule.count }.by(1)
+ expect_graphql_errors_to_be_empty
+ expect(mutation_response_errors).to be_empty
+ end
- expect_graphql_errors_to_be_empty
- expect(Packages::Protection::Rule.where(project: project).count).to eq 1
+ it 'returns the created packages protection rule' do
+ subject
- expect(Packages::Protection::Rule.where(project: project,
- package_name_pattern: kwargs[:package_name_pattern])).to exist
- end
+ expect(mutation_response_package_protection_rule).to include(
+ 'id' => be_present,
+ 'packageNamePattern' => kwargs[:package_name_pattern],
+ 'packageType' => kwargs[:package_type],
+ 'pushProtectedUpToAccessLevel' => kwargs[:push_protected_up_to_access_level]
+ )
+ end
- context 'when invalid fields are given' do
- let(:kwargs) do
- {
- project_path: project.full_path,
- package_name_pattern: '',
- package_type: 'UNKNOWN_PACKAGE_TYPE',
- push_protected_up_to_access_level: 'UNKNOWN_ACCESS_LEVEL'
- }
- end
-
- it 'returns error about required argument' do
- subject
-
- expect_graphql_errors_to_include(/was provided invalid value for packageType/)
- expect_graphql_errors_to_include(/pushProtectedUpToAccessLevel/)
- end
- end
+ it 'creates one package protection rule' do
+ expect { subject }.to change { ::Packages::Protection::Rule.count }.by(1)
+
+ expect(Packages::Protection::Rule.last).to have_attributes(
+ project: project,
+ package_name_pattern: kwargs[:package_name_pattern],
+ package_type: kwargs[:package_type].downcase,
+ push_protected_up_to_access_level: kwargs[:push_protected_up_to_access_level].downcase
+ )
end
+ end
- context 'when user does not have permission' do
- let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } }
- let_it_be(:reporter) { create(:user).tap { |u| project.add_reporter(u) } }
- let_it_be(:guest) { create(:user).tap { |u| project.add_guest(u) } }
- let_it_be(:anonymous) { create(:user) }
+ shared_examples 'an erroneous response' do
+ it 'does not create one package protection rule' do
+ expect { subject }.not_to change { ::Packages::Protection::Rule.count }
+ end
+ end
- where(:user) do
- [ref(:developer), ref(:reporter), ref(:guest), ref(:anonymous)]
- end
+ it_behaves_like 'a successful response'
- with_them do
- it 'returns an error' do
- expect { subject }.not_to change { ::Packages::Protection::Rule.count }
+ context 'with invalid kwargs leading to error from graphql' do
+ let(:kwargs) do
+ super().merge!(
+ package_name_pattern: '',
+ package_type: 'UNKNOWN_PACKAGE_TYPE',
+ push_protected_up_to_access_level: 'UNKNOWN_ACCESS_LEVEL'
+ )
+ end
- expect_graphql_errors_to_include(/you don't have permission to perform this action/)
- end
- end
+ it_behaves_like 'an erroneous response'
+
+ it 'returns error about required argument' do
+ subject
+
+ expect_graphql_errors_to_include(/was provided invalid value for packageType/)
+ expect_graphql_errors_to_include(/pushProtectedUpToAccessLevel/)
end
+ end
- context 'with existing packages protection rule' do
- let_it_be(:existing_package_protection_rule) do
- create(:package_protection_rule, project: project, push_protected_up_to_access_level: Gitlab::Access::DEVELOPER)
- end
+ context 'with invalid kwargs leading to error from business model' do
+ let(:kwargs) { super().merge!(package_name_pattern: '') }
- context 'when package name pattern is slightly different' do
- let(:kwargs) do
- {
- project_path: project.full_path,
- # The field `package_name_pattern` is unique; this is why we change the value in a minimum way
- package_name_pattern: "#{existing_package_protection_rule.package_name_pattern}-unique",
- package_type: "NPM",
- push_protected_up_to_access_level: "DEVELOPER"
- }
- end
-
- it 'returns the created packages protection rule' do
- expect { subject }.to change { ::Packages::Protection::Rule.count }.by(1)
-
- expect(Packages::Protection::Rule.where(project: project).count).to eq 2
- expect(Packages::Protection::Rule.where(project: project,
- package_name_pattern: kwargs[:package_name_pattern])).to exist
- end
-
- it 'returns without error' do
- subject
-
- expect_graphql_errors_to_be_empty
- end
- end
+ it_behaves_like 'an erroneous response'
- context 'when field `package_name_pattern` is taken' do
- let(:kwargs) do
- {
- project_path: project.full_path,
- package_name_pattern: existing_package_protection_rule.package_name_pattern,
- package_type: 'NPM',
- push_protected_up_to_access_level: 'MAINTAINER'
- }
- end
-
- it 'returns without error' do
- subject
-
- expect(mutation_response).to include 'errors' => ['Package name pattern has already been taken']
- end
-
- it 'does not create new package protection rules' do
- expect { subject }.to change { Packages::Protection::Rule.count }.by(0)
-
- expect(Packages::Protection::Rule.where(project: project,
- package_name_pattern: kwargs[:package_name_pattern],
- push_protected_up_to_access_level: Gitlab::Access::MAINTAINER)).not_to exist
- end
- end
+ it 'returns an error' do
+ subject.tap { expect(mutation_response_errors).to include(/Package name pattern can't be blank/) }
end
+ end
- context "when feature flag ':packages_protected_packages' disabled" do
- before do
- stub_feature_flags(packages_protected_packages: false)
+ context 'with existing packages protection rule' do
+ let_it_be(:existing_package_protection_rule) do
+ create(:package_protection_rule, project: project, push_protected_up_to_access_level: :maintainer)
+ end
+
+ let(:kwargs) { super().merge!(package_name_pattern: existing_package_protection_rule.package_name_pattern) }
+
+ it_behaves_like 'an erroneous response'
+
+ it 'returns an error' do
+ subject.tap { expect(mutation_response_errors).to include(/Package name pattern has already been taken/) }
+ end
+
+ context 'when field `package_name_pattern` is different than existing one' do
+ let(:kwargs) do
+ # The field `package_name_pattern` is unique; this is why we change the value in a minimum way
+ super().merge!(package_name_pattern: "#{existing_package_protection_rule.package_name_pattern}-unique")
end
- it 'does not create any package protection rules' do
- expect { subject }.to change { Packages::Protection::Rule.count }.by(0)
+ it_behaves_like 'a successful response'
+ end
+
+ context 'when field `push_protected_up_to_access_level` is different than existing one' do
+ let(:kwargs) { super().merge!(push_protected_up_to_access_level: 'DEVELOPER') }
+
+ it_behaves_like 'an erroneous response'
- expect(Packages::Protection::Rule.where(project: project)).not_to exist
+ it 'returns an error' do
+ subject.tap { expect(mutation_response_errors).to include(/Package name pattern has already been taken/) }
end
+ end
+ end
- it 'returns error of disabled feature flag' do
- subject.tap { expect_graphql_errors_to_include(/'packages_protected_packages' feature flag is disabled/) }
+ context 'when user does not have permission' do
+ let_it_be(:anonymous) { create(:user) }
+ let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } }
+ let_it_be(:guest) { create(:user).tap { |u| project.add_guest(u) } }
+ let_it_be(:reporter) { create(:user).tap { |u| project.add_reporter(u) } }
+
+ where(:user) do
+ [ref(:developer), ref(:reporter), ref(:guest), ref(:anonymous)]
+ end
+
+ with_them do
+ it_behaves_like 'an erroneous response'
+
+ it 'returns an error' do
+ subject.tap { expect_graphql_errors_to_include(/you don't have permission to perform this action/) }
end
end
end
+
+ context "when feature flag ':packages_protected_packages' disabled" do
+ before do
+ stub_feature_flags(packages_protected_packages: false)
+ end
+
+ it_behaves_like 'an erroneous response'
+
+ it 'returns error of disabled feature flag' do
+ subject.tap { expect_graphql_errors_to_include(/'packages_protected_packages' feature flag is disabled/) }
+ end
+ end
end
diff --git a/spec/requests/api/graphql/mutations/packages/protection/rule/delete_spec.rb b/spec/requests/api/graphql/mutations/packages/protection/rule/delete_spec.rb
new file mode 100644
index 00000000000..1d94d520674
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/packages/protection/rule/delete_spec.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Deleting a package protection rule', :aggregate_failures, feature_category: :package_registry do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be_with_refind(:package_protection_rule) { create(:package_protection_rule, project: project) }
+ let_it_be(:current_user) { create(:user, maintainer_projects: [project]) }
+
+ let(:mutation) { graphql_mutation(:delete_packages_protection_rule, input) }
+ let(:mutation_response) { graphql_mutation_response(:delete_packages_protection_rule) }
+ let(:input) { { id: package_protection_rule.to_global_id } }
+
+ subject { post_graphql_mutation(mutation, current_user: current_user) }
+
+ shared_examples 'an erroneous reponse' do
+ it { subject.tap { expect(mutation_response).to be_blank } }
+ it { expect { subject }.not_to change { ::Packages::Protection::Rule.count } }
+ end
+
+ it_behaves_like 'a working GraphQL mutation'
+
+ it 'responds with deleted package protection rule' do
+ subject
+
+ expect(mutation_response).to include(
+ 'errors' => be_blank,
+ 'packageProtectionRule' => {
+ 'id' => package_protection_rule.to_global_id.to_s,
+ 'packageNamePattern' => package_protection_rule.package_name_pattern,
+ 'packageType' => package_protection_rule.package_type.upcase,
+ 'pushProtectedUpToAccessLevel' => package_protection_rule.push_protected_up_to_access_level.upcase
+ }
+ )
+ end
+
+ it { is_expected.tap { expect_graphql_errors_to_be_empty } }
+ it { expect { subject }.to change { ::Packages::Protection::Rule.count }.from(1).to(0) }
+
+ context 'with existing package protection rule belonging to other project' do
+ let_it_be(:package_protection_rule) do
+ create(:package_protection_rule, package_name_pattern: 'protection_rule_other_project')
+ end
+
+ it_behaves_like 'an erroneous reponse'
+
+ it { subject.tap { expect_graphql_errors_to_include(/you don't have permission to perform this action/) } }
+ end
+
+ context 'with deleted package protection rule' do
+ let!(:package_protection_rule) do
+ create(:package_protection_rule, project: project, package_name_pattern: 'protection_rule_deleted').destroy!
+ end
+
+ it_behaves_like 'an erroneous reponse'
+
+ it { subject.tap { expect_graphql_errors_to_include(/you don't have permission to perform this action/) } }
+ end
+
+ context 'when current_user does not have permission' do
+ let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } }
+ let_it_be(:reporter) { create(:user).tap { |u| project.add_reporter(u) } }
+ let_it_be(:guest) { create(:user).tap { |u| project.add_guest(u) } }
+ let_it_be(:anonymous) { create(:user) }
+
+ where(:current_user) do
+ [ref(:developer), ref(:reporter), ref(:guest), ref(:anonymous)]
+ end
+
+ with_them do
+ it_behaves_like 'an erroneous reponse'
+
+ it { subject.tap { expect_graphql_errors_to_include(/you don't have permission to perform this action/) } }
+ end
+ end
+
+ context "when feature flag ':packages_protected_packages' disabled" do
+ before do
+ stub_feature_flags(packages_protected_packages: false)
+ end
+
+ it_behaves_like 'an erroneous reponse'
+
+ it { subject.tap { expect_graphql_errors_to_include(/'packages_protected_packages' feature flag is disabled/) } }
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/release_asset_links/update_spec.rb b/spec/requests/api/graphql/mutations/release_asset_links/update_spec.rb
index 45028cba3ae..fdd4de865ad 100644
--- a/spec/requests/api/graphql/mutations/release_asset_links/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/release_asset_links/update_spec.rb
@@ -10,12 +10,14 @@ RSpec.describe 'Updating an existing release asset link', feature_category: :rel
let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } }
let_it_be(:release_link) do
- create(:release_link,
- release: release,
- name: 'link name',
- url: 'https://example.com/url',
- filepath: '/permanent/path',
- link_type: 'package')
+ create(
+ :release_link,
+ release: release,
+ name: 'link name',
+ url: 'https://example.com/url',
+ filepath: '/permanent/path',
+ link_type: 'package'
+ )
end
let(:current_user) { developer }
diff --git a/spec/requests/api/graphql/mutations/snippets/create_spec.rb b/spec/requests/api/graphql/mutations/snippets/create_spec.rb
index a6d727ae6d3..7094cb807b2 100644
--- a/spec/requests/api/graphql/mutations/snippets/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/snippets/create_spec.rb
@@ -42,7 +42,7 @@ RSpec.describe 'Creating a Snippet', feature_category: :source_code_management d
let(:current_user) { nil }
it_behaves_like 'a mutation that returns top-level errors',
- errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
+ errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
it 'does not create the Snippet' do
expect do
@@ -122,7 +122,7 @@ RSpec.describe 'Creating a Snippet', feature_category: :source_code_management d
let(:project_path) { 'foobar' }
it_behaves_like 'a mutation that returns top-level errors',
- errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
+ errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
end
context 'when the feature is disabled' do
@@ -131,7 +131,7 @@ RSpec.describe 'Creating a Snippet', feature_category: :source_code_management d
end
it_behaves_like 'a mutation that returns top-level errors',
- errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
+ errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
end
it_behaves_like 'snippet edit usage data counters'
@@ -169,8 +169,8 @@ RSpec.describe 'Creating a Snippet', feature_category: :source_code_management d
end
it_behaves_like 'expected files argument', nil, nil
- it_behaves_like 'expected files argument', %w(foo bar), %w(foo bar)
- it_behaves_like 'expected files argument', 'foo', %w(foo)
+ it_behaves_like 'expected files argument', %w[foo bar], %w[foo bar]
+ it_behaves_like 'expected files argument', 'foo', %w[foo]
context 'when files has an invalid value' do
let(:uploaded_files) { [1] }
diff --git a/spec/requests/api/graphql/mutations/snippets/destroy_spec.rb b/spec/requests/api/graphql/mutations/snippets/destroy_spec.rb
index c3f818b6627..7b0de7a9fba 100644
--- a/spec/requests/api/graphql/mutations/snippets/destroy_spec.rb
+++ b/spec/requests/api/graphql/mutations/snippets/destroy_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe 'Destroying a Snippet', feature_category: :source_code_management
let(:current_user) { create(:user) }
it_behaves_like 'a mutation that returns top-level errors',
- errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
+ errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
it 'does not destroy the Snippet' do
expect do
@@ -53,7 +53,7 @@ RSpec.describe 'Destroying a Snippet', feature_category: :source_code_management
let!(:snippet_gid) { project.to_gid.to_s }
it 'returns an error' do
- err_message = %["#{snippet_gid}" does not represent an instance of Snippet]
+ err_message = %("#{snippet_gid}" does not represent an instance of Snippet)
post_graphql_mutation(mutation, current_user: current_user)
diff --git a/spec/requests/api/graphql/mutations/snippets/mark_as_spam_spec.rb b/spec/requests/api/graphql/mutations/snippets/mark_as_spam_spec.rb
index 9a8c027da8a..6fd41437ce4 100644
--- a/spec/requests/api/graphql/mutations/snippets/mark_as_spam_spec.rb
+++ b/spec/requests/api/graphql/mutations/snippets/mark_as_spam_spec.rb
@@ -40,7 +40,7 @@ RSpec.describe 'Mark snippet as spam', feature_category: :source_code_management
let(:current_user) { other_user }
it_behaves_like 'a mutation that returns top-level errors',
- errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
+ errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
it_behaves_like 'does not mark the snippet as spam'
end
diff --git a/spec/requests/api/graphql/mutations/snippets/update_spec.rb b/spec/requests/api/graphql/mutations/snippets/update_spec.rb
index 78df78cb2a0..0bc475c7105 100644
--- a/spec/requests/api/graphql/mutations/snippets/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/snippets/update_spec.rb
@@ -46,7 +46,7 @@ RSpec.describe 'Updating a Snippet', feature_category: :source_code_management d
let(:current_user) { create(:user) }
it_behaves_like 'a mutation that returns top-level errors',
- errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
+ errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
it 'does not update the Snippet' do
expect do
@@ -118,13 +118,15 @@ RSpec.describe 'Updating a Snippet', feature_category: :source_code_management d
describe 'PersonalSnippet' do
let(:snippet) do
- create(:personal_snippet,
- :private,
- :repository,
- file_name: original_file_name,
- title: original_title,
- content: original_content,
- description: original_description)
+ create(
+ :personal_snippet,
+ :private,
+ :repository,
+ file_name: original_file_name,
+ title: original_title,
+ content: original_content,
+ description: original_description
+ )
end
it_behaves_like 'graphql update actions'
@@ -139,15 +141,17 @@ RSpec.describe 'Updating a Snippet', feature_category: :source_code_management d
let_it_be(:project) { create(:project, :private) }
let(:snippet) do
- create(:project_snippet,
- :private,
- :repository,
- project: project,
- author: create(:user),
- file_name: original_file_name,
- title: original_title,
- content: original_content,
- description: original_description)
+ create(
+ :project_snippet,
+ :private,
+ :repository,
+ project: project,
+ author: create(:user),
+ file_name: original_file_name,
+ title: original_title,
+ content: original_content,
+ description: original_description
+ )
end
context 'when the author is not a member of the project' do
diff --git a/spec/requests/api/graphql/mutations/todos/mark_all_done_spec.rb b/spec/requests/api/graphql/mutations/todos/mark_all_done_spec.rb
index c611c6ee2a1..429aa06d9f1 100644
--- a/spec/requests/api/graphql/mutations/todos/mark_all_done_spec.rb
+++ b/spec/requests/api/graphql/mutations/todos/mark_all_done_spec.rb
@@ -21,12 +21,14 @@ RSpec.describe 'Marking all todos done', feature_category: :team_planning do
let(:input) { {} }
let(:mutation) do
- graphql_mutation(:todos_mark_all_done, input,
- <<-QL.strip_heredoc
- clientMutationId
- todos { id }
- errors
- QL
+ graphql_mutation(
+ :todos_mark_all_done,
+ input,
+ <<-QL.strip_heredoc
+ clientMutationId
+ todos { id }
+ errors
+ QL
)
end
diff --git a/spec/requests/api/graphql/mutations/todos/mark_done_spec.rb b/spec/requests/api/graphql/mutations/todos/mark_done_spec.rb
index 60700d8024c..c09f89ef567 100644
--- a/spec/requests/api/graphql/mutations/todos/mark_done_spec.rb
+++ b/spec/requests/api/graphql/mutations/todos/mark_done_spec.rb
@@ -19,15 +19,17 @@ RSpec.describe 'Marking todos done', feature_category: :team_planning do
let(:input) { { id: todo1.to_global_id.to_s } }
let(:mutation) do
- graphql_mutation(:todo_mark_done, input,
- <<-QL.strip_heredoc
- clientMutationId
- errors
- todo {
- id
- state
- }
- QL
+ graphql_mutation(
+ :todo_mark_done,
+ input,
+ <<-QL.strip_heredoc
+ clientMutationId
+ errors
+ todo {
+ id
+ state
+ }
+ QL
)
end
diff --git a/spec/requests/api/graphql/mutations/todos/restore_many_spec.rb b/spec/requests/api/graphql/mutations/todos/restore_many_spec.rb
index 9daa243cf8e..4bbfc7b2f1d 100644
--- a/spec/requests/api/graphql/mutations/todos/restore_many_spec.rb
+++ b/spec/requests/api/graphql/mutations/todos/restore_many_spec.rb
@@ -20,15 +20,17 @@ RSpec.describe 'Restoring many Todos', feature_category: :team_planning do
let(:input) { { ids: input_ids } }
let(:mutation) do
- graphql_mutation(:todo_restore_many, input,
- <<-QL.strip_heredoc
- clientMutationId
- errors
- todos {
- id
- state
- }
- QL
+ graphql_mutation(
+ :todo_restore_many,
+ input,
+ <<-QL.strip_heredoc
+ clientMutationId
+ errors
+ todos {
+ id
+ state
+ }
+ QL
)
end
diff --git a/spec/requests/api/graphql/mutations/todos/restore_spec.rb b/spec/requests/api/graphql/mutations/todos/restore_spec.rb
index 868298763ec..1ebd04432be 100644
--- a/spec/requests/api/graphql/mutations/todos/restore_spec.rb
+++ b/spec/requests/api/graphql/mutations/todos/restore_spec.rb
@@ -19,15 +19,17 @@ RSpec.describe 'Restoring Todos', feature_category: :team_planning do
let(:input) { { id: todo1.to_global_id.to_s } }
let(:mutation) do
- graphql_mutation(:todo_restore, input,
- <<-QL.strip_heredoc
- clientMutationId
- errors
- todo {
- id
- state
- }
- QL
+ graphql_mutation(
+ :todo_restore,
+ input,
+ <<-QL.strip_heredoc
+ clientMutationId
+ errors
+ todo {
+ id
+ state
+ }
+ QL
)
end
diff --git a/spec/requests/api/graphql/namespace/root_storage_statistics_spec.rb b/spec/requests/api/graphql/namespace/root_storage_statistics_spec.rb
index 7c48f324d24..c8819f1e38f 100644
--- a/spec/requests/api/graphql/namespace/root_storage_statistics_spec.rb
+++ b/spec/requests/api/graphql/namespace/root_storage_statistics_spec.rb
@@ -6,8 +6,15 @@ RSpec.describe 'rendering namespace statistics', feature_category: :metrics do
include GraphqlHelpers
let(:namespace) { user.namespace }
- let!(:statistics) { create(:namespace_root_storage_statistics, namespace: namespace, packages_size: 5.gigabytes, uploads_size: 3.gigabytes) }
let(:user) { create(:user) }
+ let!(:statistics) do
+ create(
+ :namespace_root_storage_statistics,
+ namespace: namespace,
+ packages_size: 5.gigabytes,
+ uploads_size: 3.gigabytes
+ )
+ end
let(:query) do
graphql_query_for(
diff --git a/spec/requests/api/graphql/organizations/organization_query_spec.rb b/spec/requests/api/graphql/organizations/organization_query_spec.rb
index d02158382eb..c243e0613ad 100644
--- a/spec/requests/api/graphql/organizations/organization_query_spec.rb
+++ b/spec/requests/api/graphql/organizations/organization_query_spec.rb
@@ -79,7 +79,10 @@ RSpec.describe 'getting organization information', feature_category: :cell do
<<~FIELDS
organizationUsers {
nodes {
- badges
+ badges {
+ text
+ variant
+ }
id
user {
id
@@ -94,7 +97,7 @@ RSpec.describe 'getting organization information', feature_category: :cell do
organization_user_node = graphql_data_at(:organization, :organizationUsers, :nodes).first
expected_attributes = {
- "badges" => ["It's you!"],
+ "badges" => [{ "text" => "It's you!", "variant" => 'muted' }],
"id" => organization_user.to_global_id.to_s,
"user" => { "id" => user.to_global_id.to_s }
}
diff --git a/spec/requests/api/graphql/project/base_service_spec.rb b/spec/requests/api/graphql/project/base_service_spec.rb
index b27cddea07b..646ff8dd8a8 100644
--- a/spec/requests/api/graphql/project/base_service_spec.rb
+++ b/spec/requests/api/graphql/project/base_service_spec.rb
@@ -41,7 +41,7 @@ RSpec.describe 'query Jira service', feature_category: :system_access do
it 'retuns list of jira imports' do
service_types = services.map { |s| s['type'] }
- expect(service_types).to match_array(%w(BugzillaService JiraService RedmineService))
+ expect(service_types).to match_array(%w[BugzillaService JiraService RedmineService])
end
end
end
diff --git a/spec/requests/api/graphql/project/container_repositories_spec.rb b/spec/requests/api/graphql/project/container_repositories_spec.rb
index 9a40a972256..c86d3bdd14c 100644
--- a/spec/requests/api/graphql/project/container_repositories_spec.rb
+++ b/spec/requests/api/graphql/project/container_repositories_spec.rb
@@ -46,7 +46,7 @@ RSpec.describe 'getting container repositories in a project', feature_category:
before do
stub_container_registry_config(enabled: true)
container_repositories.each do |repository|
- stub_container_registry_tags(repository: repository.path, tags: %w(tag1 tag2 tag3), with_manifest: false)
+ stub_container_registry_tags(repository: repository.path, tags: %w[tag1 tag2 tag3], with_manifest: false)
end
end
@@ -141,7 +141,7 @@ RSpec.describe 'getting container repositories in a project', feature_category:
end
before do
- stub_container_registry_tags(repository: container_repository.path, tags: %w(tag4 tag5 tag6), with_manifest: false)
+ stub_container_registry_tags(repository: container_repository.path, tags: %w[tag4 tag5 tag6], with_manifest: false)
end
it 'returns the searched container repository' do
@@ -175,11 +175,11 @@ RSpec.describe 'getting container repositories in a project', feature_category:
let_it_be(:container_repository5) { create(:container_repository, name: 'e', project: sort_project) }
before do
- stub_container_registry_tags(repository: container_repository1.path, tags: %w(tag1 tag1 tag3), with_manifest: false)
- stub_container_registry_tags(repository: container_repository2.path, tags: %w(tag4 tag5 tag6), with_manifest: false)
- stub_container_registry_tags(repository: container_repository3.path, tags: %w(tag7 tag8), with_manifest: false)
- stub_container_registry_tags(repository: container_repository4.path, tags: %w(tag9), with_manifest: false)
- stub_container_registry_tags(repository: container_repository5.path, tags: %w(tag10 tag11), with_manifest: false)
+ stub_container_registry_tags(repository: container_repository1.path, tags: %w[tag1 tag1 tag3], with_manifest: false)
+ stub_container_registry_tags(repository: container_repository2.path, tags: %w[tag4 tag5 tag6], with_manifest: false)
+ stub_container_registry_tags(repository: container_repository3.path, tags: %w[tag7 tag8], with_manifest: false)
+ stub_container_registry_tags(repository: container_repository4.path, tags: %w[tag9], with_manifest: false)
+ stub_container_registry_tags(repository: container_repository5.path, tags: %w[tag10 tag11], with_manifest: false)
end
def pagination_query(params)
diff --git a/spec/requests/api/graphql/project/data_transfer_spec.rb b/spec/requests/api/graphql/project/data_transfer_spec.rb
index aafa8d65eb9..79b2b10419b 100644
--- a/spec/requests/api/graphql/project/data_transfer_spec.rb
+++ b/spec/requests/api/graphql/project/data_transfer_spec.rb
@@ -68,45 +68,21 @@ RSpec.describe 'project data transfers', feature_category: :source_code_manageme
context 'when user has enough permissions' do
before do
project.add_owner(current_user)
+ subject
end
- context 'when data_transfer_monitoring_mock_data is NOT enabled' do
- before do
- stub_feature_flags(data_transfer_monitoring_mock_data: false)
- subject
- end
-
- it 'returns real results' do
- expect(response).to have_gitlab_http_status(:ok)
+ it 'returns real results' do
+ expect(response).to have_gitlab_http_status(:ok)
- expect(egress_data.count).to eq(2)
+ expect(egress_data.count).to eq(2)
- expect(egress_data.first.keys).to match_array(
- %w[date totalEgress repositoryEgress artifactsEgress packagesEgress registryEgress]
- )
+ expect(egress_data.first.keys).to match_array(
+ %w[date totalEgress repositoryEgress artifactsEgress packagesEgress registryEgress]
+ )
- expect(egress_data.pluck('repositoryEgress')).to match_array(%w[1 2])
- end
-
- it_behaves_like 'a working graphql query'
+ expect(egress_data.pluck('repositoryEgress')).to match_array(%w[1 2])
end
- context 'when data_transfer_monitoring_mock_data is enabled' do
- before do
- stub_feature_flags(data_transfer_monitoring_mock_data: true)
- subject
- end
-
- it 'returns mock results' do
- expect(response).to have_gitlab_http_status(:ok)
-
- expect(egress_data.count).to eq(12)
- expect(egress_data.first.keys).to match_array(
- %w[date totalEgress repositoryEgress artifactsEgress packagesEgress registryEgress]
- )
- end
-
- it_behaves_like 'a working graphql query'
- end
+ it_behaves_like 'a working graphql query'
end
end
diff --git a/spec/requests/api/graphql/project/environments_spec.rb b/spec/requests/api/graphql/project/environments_spec.rb
index 3a863bd3d77..94ce6b797cd 100644
--- a/spec/requests/api/graphql/project/environments_spec.rb
+++ b/spec/requests/api/graphql/project/environments_spec.rb
@@ -150,7 +150,7 @@ RSpec.describe 'Project Environments query', feature_category: :continuous_deliv
end
describe 'last deployments of environments' do
- ::Deployment.statuses.each do |status, _| # rubocop:disable RSpec/UselessDynamicDefinition
+ ::Deployment.statuses.each_key do |status| # rubocop:disable RSpec/UselessDynamicDefinition -- `status` used in `let_it_be`
let_it_be(:"production_#{status}_deployment") do
create(:deployment, status.to_sym, environment: production, project: project)
end
diff --git a/spec/requests/api/graphql/project/error_tracking/sentry_errors_request_spec.rb b/spec/requests/api/graphql/project/error_tracking/sentry_errors_request_spec.rb
index 6cbc70022ed..c2910938acf 100644
--- a/spec/requests/api/graphql/project/error_tracking/sentry_errors_request_spec.rb
+++ b/spec/requests/api/graphql/project/error_tracking/sentry_errors_request_spec.rb
@@ -119,7 +119,7 @@ RSpec.describe 'sentry errors requests', feature_category: :error_tracking do
end
let(:error_data) { graphql_data.dig('project', 'sentryErrors', 'errors', 'nodes') }
- let(:pagination_data) { graphql_data.dig('project', 'sentryErrors', 'errors', 'pageInfo') }
+ let(:pagination_data) { graphql_data.dig('project', 'sentryErrors', 'errors', 'pageInfo') }
it_behaves_like 'a working graphql query' do
before do
diff --git a/spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb b/spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb
index a15e4c1e792..bc56df5b6ff 100644
--- a/spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb
+++ b/spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb
@@ -73,7 +73,7 @@ RSpec.describe 'Getting versions related to an issue', feature_category: :design
post_graphql(query, current_user: current_user)
keys = graphql_data.dig(*edges_path).first['node'].keys
- expect(keys).to match_array(%w(id sha createdAt author))
+ expect(keys).to match_array(%w[id sha createdAt author])
end
end
diff --git a/spec/requests/api/graphql/project/issue_spec.rb b/spec/requests/api/graphql/project/issue_spec.rb
index bc90f9e89e6..8d21a9f0394 100644
--- a/spec/requests/api/graphql/project/issue_spec.rb
+++ b/spec/requests/api/graphql/project/issue_spec.rb
@@ -108,7 +108,7 @@ RSpec.describe 'Query.project(fullPath).issue(iid)', feature_category: :team_pla
let(:object_field_name) { :design }
let(:no_argument_error) do
- custom_graphql_error(path, a_string_matching(%r/id or filename/))
+ custom_graphql_error(path, a_string_matching(%r{id or filename}))
end
let_it_be(:object_on_other_issue) { create(:design, issue: issue_b) }
@@ -134,7 +134,7 @@ RSpec.describe 'Query.project(fullPath).issue(iid)', feature_category: :team_pla
it 'raises an error' do
post_query
- expect(graphql_errors).to include(custom_graphql_error(path, a_string_matching(%r/id or sha/)))
+ expect(graphql_errors).to include(custom_graphql_error(path, a_string_matching(%r{id or sha})))
end
end
diff --git a/spec/requests/api/graphql/project/jira_import_spec.rb b/spec/requests/api/graphql/project/jira_import_spec.rb
index 25cea0238ef..a1d340b3500 100644
--- a/spec/requests/api/graphql/project/jira_import_spec.rb
+++ b/spec/requests/api/graphql/project/jira_import_spec.rb
@@ -96,7 +96,7 @@ RSpec.describe 'query Jira import data', feature_category: :importers do
total_issue_count = jira_imports.map { |ji| ji.dig('totalIssueCount') }
expect(jira_imports.size).to eq 2
- expect(jira_proket_keys).to eq %w(BB AA)
+ expect(jira_proket_keys).to eq %w[BB AA]
expect(usernames).to eq [current_user.username, current_user.username]
expect(imported_issues_count).to eq [2, 2]
expect(failed_issues_count).to eq [1, 2]
diff --git a/spec/requests/api/graphql/project/jira_projects_spec.rb b/spec/requests/api/graphql/project/jira_projects_spec.rb
index 3cd689deda5..2859e8a7c99 100644
--- a/spec/requests/api/graphql/project/jira_projects_spec.rb
+++ b/spec/requests/api/graphql/project/jira_projects_spec.rb
@@ -55,8 +55,8 @@ RSpec.describe 'query Jira projects', feature_category: :integrations do
project_ids = jira_projects.map { |jp| jp['projectId'] }
expect(jira_projects.size).to eq(2)
- expect(project_keys).to eq(%w(EX ABC))
- expect(project_names).to eq(%w(Example Alphabetical))
+ expect(project_keys).to eq(%w[EX ABC])
+ expect(project_names).to eq(%w[Example Alphabetical])
expect(project_ids).to eq([10000, 10001])
end
@@ -69,8 +69,8 @@ RSpec.describe 'query Jira projects', feature_category: :integrations do
project_ids = jira_projects.map { |jp| jp['projectId'] }
expect(jira_projects.size).to eq(1)
- expect(project_keys).to eq(%w(EX))
- expect(project_names).to eq(%w(Example))
+ expect(project_keys).to eq(%w[EX])
+ expect(project_names).to eq(%w[Example])
expect(project_ids).to eq([10000])
end
end
diff --git a/spec/requests/api/graphql/project/merge_request_spec.rb b/spec/requests/api/graphql/project/merge_request_spec.rb
index c274199e65b..23be9fa5286 100644
--- a/spec/requests/api/graphql/project/merge_request_spec.rb
+++ b/spec/requests/api/graphql/project/merge_request_spec.rb
@@ -22,7 +22,7 @@ RSpec.describe 'getting merge request information nested in a project', feature_
it_behaves_like 'a working graphql query' do
# we exclude Project.pipeline because it needs arguments,
- # codequalityReportsComparer because no pipeline exist yet
+ # codequalityReportsComparer because it is behind a feature flag
# and runners because the user is not an admin and therefore has no access
let(:excluded) { %w[jobs pipeline runners codequalityReportsComparer] }
let(:mr_fields) { all_graphql_fields_for('MergeRequest', excluded: excluded) }
diff --git a/spec/requests/api/graphql/project/merge_requests_spec.rb b/spec/requests/api/graphql/project/merge_requests_spec.rb
index 543de43bcf3..176a02df0be 100644
--- a/spec/requests/api/graphql/project/merge_requests_spec.rb
+++ b/spec/requests/api/graphql/project/merge_requests_spec.rb
@@ -48,8 +48,11 @@ RSpec.describe 'getting merge request listings nested in a project', feature_cat
end
it_behaves_like 'a working graphql query' do
+ # we exclude codequalityReportsComparer because it is behind feature flag
+ let(:excluded) { %w[codequalityReportsComparer] }
+
let(:query) do
- query_merge_requests(all_graphql_fields_for('MergeRequest', max_depth: 2))
+ query_merge_requests(all_graphql_fields_for('MergeRequest', max_depth: 2, excluded: excluded))
end
before do
diff --git a/spec/requests/api/graphql/project/release_spec.rb b/spec/requests/api/graphql/project/release_spec.rb
index 8d4a39d6b30..8206d076d2e 100644
--- a/spec/requests/api/graphql/project/release_spec.rb
+++ b/spec/requests/api/graphql/project/release_spec.rb
@@ -36,7 +36,7 @@ RSpec.describe 'Query.project(fullPath).release(tagName)', feature_category: :re
let(:path) { path_prefix }
let(:release_fields) do
- %{
+ %(
tagName
tagPath
description
@@ -45,7 +45,7 @@ RSpec.describe 'Query.project(fullPath).release(tagName)', feature_category: :re
createdAt
releasedAt
upcomingRelease
- }
+ )
end
before do
@@ -176,14 +176,14 @@ RSpec.describe 'Query.project(fullPath).release(tagName)', feature_category: :re
let(:path) { path_prefix + %w[links] }
let(:release_fields) do
- query_graphql_field(:links, nil, %{
+ query_graphql_field(:links, nil, %(
selfUrl
openedMergeRequestsUrl
mergedMergeRequestsUrl
closedMergeRequestsUrl
openedIssuesUrl
closedIssuesUrl
- })
+ ))
end
it 'finds all release links' do
@@ -225,7 +225,7 @@ RSpec.describe 'Query.project(fullPath).release(tagName)', feature_category: :re
let(:path) { path_prefix }
let(:release_fields) do
- %{
+ %(
tagName
tagPath
description
@@ -234,7 +234,7 @@ RSpec.describe 'Query.project(fullPath).release(tagName)', feature_category: :re
createdAt
releasedAt
upcomingRelease
- }
+ )
end
before do
@@ -358,14 +358,14 @@ RSpec.describe 'Query.project(fullPath).release(tagName)', feature_category: :re
let(:path) { path_prefix + %w[links] }
let(:release_fields) do
- query_graphql_field(:links, nil, %{
+ query_graphql_field(:links, nil, %(
selfUrl
openedMergeRequestsUrl
mergedMergeRequestsUrl
closedMergeRequestsUrl
openedIssuesUrl
closedIssuesUrl
- })
+ ))
end
it 'finds only selfUrl' do
@@ -547,10 +547,10 @@ RSpec.describe 'Query.project(fullPath).release(tagName)', feature_category: :re
let(:current_user) { developer }
let(:release_fields) do
- %{
+ %(
releasedAt
upcomingRelease
- }
+ )
end
before do
@@ -588,13 +588,13 @@ RSpec.describe 'Query.project(fullPath).release(tagName)', feature_category: :re
let_it_be_with_reload(:release) { create(:release, project: project) }
let(:release_fields) do
- %{
+ %(
milestones {
nodes {
title
}
}
- }
+ )
end
let(:actual_milestone_title_order) do
diff --git a/spec/requests/api/graphql/project/terraform/state_spec.rb b/spec/requests/api/graphql/project/terraform/state_spec.rb
index 1889e7a1064..28f3868c7cf 100644
--- a/spec/requests/api/graphql/project/terraform/state_spec.rb
+++ b/spec/requests/api/graphql/project/terraform/state_spec.rb
@@ -20,7 +20,7 @@ RSpec.describe 'query a single terraform state', feature_category: :infrastructu
query_graphql_field(
:terraformState,
{ name: terraform_state.name },
- %{
+ %(
id
name
lockedAt
@@ -45,7 +45,7 @@ RSpec.describe 'query a single terraform state', feature_category: :infrastructu
lockedByUser {
id
}
- }
+ )
)
)
end
diff --git a/spec/requests/api/graphql/project/terraform/states_spec.rb b/spec/requests/api/graphql/project/terraform/states_spec.rb
index 7a789a5d481..d6cf3a52649 100644
--- a/spec/requests/api/graphql/project/terraform/states_spec.rb
+++ b/spec/requests/api/graphql/project/terraform/states_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe 'query terraform states', feature_category: :infrastructure_as_co
graphql_query_for(
:project,
{ fullPath: project.full_path },
- %{
+ %(
terraformStates {
count
nodes {
@@ -45,7 +45,7 @@ RSpec.describe 'query terraform states', feature_category: :infrastructure_as_co
}
}
}
- }
+ )
)
end
diff --git a/spec/requests/api/graphql/project/work_items_spec.rb b/spec/requests/api/graphql/project/work_items_spec.rb
index d5d3d6c578f..d0f80bcfebe 100644
--- a/spec/requests/api/graphql/project/work_items_spec.rb
+++ b/spec/requests/api/graphql/project/work_items_spec.rb
@@ -350,7 +350,9 @@ RSpec.describe 'getting a work item list for a project', feature_category: :team
end
context 'when fetching work item linked items widget' do
- let_it_be(:related_items) { create_list(:work_item, 3, project: project, milestone: milestone1) }
+ let_it_be(:other_project) { create(:project, :repository, :public, group: group) }
+ let_it_be(:other_milestone) { create(:milestone, project: other_project) }
+ let_it_be(:related_items) { create_list(:work_item, 3, project: other_project, milestone: other_milestone) }
let(:fields) do
<<~GRAPHQL
@@ -384,21 +386,24 @@ RSpec.describe 'getting a work item list for a project', feature_category: :team
before do
create(:work_item_link, source: item1, target: related_items[0], link_type: 'relates_to')
+ create(:work_item_link, source: item2, target: related_items[0], link_type: 'relates_to')
end
it 'executes limited number of N+1 queries', :use_sql_query_cache do
+ post_graphql(query, current_user: current_user) # warm-up
+
control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
post_graphql(query, current_user: current_user)
end
- create(:work_item_link, source: item1, target: related_items[1], link_type: 'relates_to')
- create(:work_item_link, source: item1, target: related_items[2], link_type: 'relates_to')
+ [item1, item2].each do |item|
+ create(:work_item_link, source: item, target: related_items[1], link_type: 'relates_to')
+ create(:work_item_link, source: item, target: related_items[2], link_type: 'relates_to')
+ end
expect_graphql_errors_to_be_empty
- # TODO: Fix N+1 queries executed for the linked work item widgets
- # https://gitlab.com/gitlab-org/gitlab/-/issues/420605
expect { post_graphql(query, current_user: current_user) }
- .not_to exceed_all_query_limit(control).with_threshold(11)
+ .not_to exceed_all_query_limit(control)
end
end
diff --git a/spec/requests/api/graphql/projects/projects_spec.rb b/spec/requests/api/graphql/projects/projects_spec.rb
new file mode 100644
index 00000000000..84b8c2285f0
--- /dev/null
+++ b/spec/requests/api/graphql/projects/projects_spec.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'getting a collection of projects', feature_category: :source_code_management do
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:group) { create(:group, name: 'public-group') }
+ let_it_be(:projects) { create_list(:project, 5, :public, group: group) }
+ let_it_be(:other_project) { create(:project, :public, group: group) }
+
+ let(:filters) { {} }
+
+ let(:query) do
+ graphql_query_for(
+ :projects,
+ filters,
+ "nodes {#{all_graphql_fields_for('Project', max_depth: 1, excluded: ['productAnalyticsState'])} }"
+ )
+ end
+
+ before_all do
+ group.add_developer(current_user)
+ end
+
+ context 'when providing full_paths filter' do
+ let(:project_full_paths) { projects.map(&:full_path) }
+ let(:filters) { { full_paths: project_full_paths } }
+
+ let(:single_project_query) do
+ graphql_query_for(
+ :projects,
+ { full_paths: [project_full_paths.first] },
+ "nodes {#{all_graphql_fields_for('Project', max_depth: 1, excluded: ['productAnalyticsState'])} }"
+ )
+ end
+
+ it_behaves_like 'a working graphql query that returns data' do
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+ end
+
+ it 'avoids N+1 queries', :use_sql_query_cache, :clean_gitlab_redis_cache do
+ post_graphql(single_project_query, current_user: current_user)
+
+ query_count = ActiveRecord::QueryRecorder.new do
+ post_graphql(single_project_query, current_user: current_user)
+ end.count
+
+ # There is an N+1 query for max_member_access_for_user_ids
+ expect do
+ post_graphql(query, current_user: current_user)
+ end.not_to exceed_all_query_limit(query_count + 5)
+ end
+
+ it 'returns the expected projects' do
+ post_graphql(query, current_user: current_user)
+ returned_full_paths = graphql_data_at(:projects, :nodes).pluck('fullPath')
+
+ expect(returned_full_paths).to match_array(project_full_paths)
+ end
+
+ context 'when users provides more than 50 full_paths' do
+ let(:filters) { { full_paths: Array.new(51) { other_project.full_path } } }
+
+ it 'returns an error' do
+ post_graphql(query, current_user: current_user)
+
+ expect(graphql_errors).to contain_exactly(
+ hash_including('message' => _('You cannot provide more than 50 full_paths'))
+ )
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/user_spec.rb b/spec/requests/api/graphql/user_spec.rb
index 41ee233dfc5..22ebc1be964 100644
--- a/spec/requests/api/graphql/user_spec.rb
+++ b/spec/requests/api/graphql/user_spec.rb
@@ -113,4 +113,36 @@ RSpec.describe 'User', feature_category: :user_profile do
end
end
end
+
+ describe 'organizations field' do
+ let_it_be(:organization_user) { create(:organization_user, user: current_user) }
+ let_it_be(:organization) { organization_user.organization }
+ let_it_be(:another_organization) { create(:organization) }
+ let_it_be(:another_user) { create(:user) }
+
+ let(:query) do
+ graphql_query_for(
+ :user,
+ { username: current_user.username.to_s.upcase },
+ 'organizations { nodes { path } }'
+ )
+ end
+
+ context 'with permission' do
+ it 'returns the relevant organization details' do
+ post_graphql(query, current_user: current_user)
+
+ expect(graphql_data.dig('user', 'organizations', 'nodes').pluck('path'))
+ .to match_array(organization.path)
+ end
+ end
+
+ context 'without permission' do
+ it 'does not return organization details' do
+ post_graphql(query, current_user: another_user)
+
+ expect(graphql_data.dig('user', 'organizations', 'nodes')).to be_nil
+ end
+ end
+ end
end
diff --git a/spec/requests/api/graphql/work_item_spec.rb b/spec/requests/api/graphql/work_item_spec.rb
index b8575b25e0a..36a27abd982 100644
--- a/spec/requests/api/graphql/work_item_spec.rb
+++ b/spec/requests/api/graphql/work_item_spec.rb
@@ -619,6 +619,47 @@ RSpec.describe 'Query.work_item(id)', feature_category: :team_planning do
)
end
+ context 'when inaccessible links are present' do
+ let_it_be(:no_access_item) { create(:work_item, title: "PRIVATE", project: create(:project, :private)) }
+
+ before do
+ create(:work_item_link, source: work_item, target: no_access_item, link_type: 'relates_to')
+ end
+
+ it 'returns only items that the user has access to' do
+ expect(graphql_dig_at(work_item_data, :widgets, "linkedItems", "nodes", "linkId"))
+ .to match_array([link1.to_gid.to_s, link2.to_gid.to_s])
+ end
+ end
+
+ context 'when limiting the number of results' do
+ it_behaves_like 'sorted paginated query' do
+ include_context 'no sort argument'
+
+ let(:first_param) { 1 }
+ let(:all_records) { [link1, link2] }
+ let(:data_path) { ['workItem', 'widgets', "linkedItems", -1] }
+
+ def widget_fields(args)
+ query_graphql_field(
+ :widgets, {}, query_graphql_field(
+ '... on WorkItemWidgetLinkedItems', {}, query_graphql_field(
+ 'linkedItems', args, "#{page_info} nodes { linkId }"
+ )
+ )
+ )
+ end
+
+ def pagination_query(params)
+ graphql_query_for('workItem', { 'id' => global_id }, widget_fields(params))
+ end
+
+ def pagination_results_data(nodes)
+ nodes.map { |item| GlobalID::Locator.locate(item['linkId']) }
+ end
+ end
+ end
+
context 'when filtering by link type' do
let(:work_item_fields) do
<<~GRAPHQL
@@ -664,20 +705,6 @@ RSpec.describe 'Query.work_item(id)', feature_category: :team_planning do
end
describe 'notes widget' do
- let(:work_item_fields) do
- <<~GRAPHQL
- id
- widgets {
- type
- ... on WorkItemWidgetNotes {
- system: discussions(filter: ONLY_ACTIVITY, first: 10) { nodes { id notes { nodes { id system internal body } } } },
- comments: discussions(filter: ONLY_COMMENTS, first: 10) { nodes { id notes { nodes { id system internal body } } } },
- all_notes: discussions(filter: ALL_NOTES, first: 10) { nodes { id notes { nodes { id system internal body } } } }
- }
- }
- GRAPHQL
- end
-
context 'when fetching award emoji from notes' do
let(:work_item_fields) do
<<~GRAPHQL
@@ -768,6 +795,26 @@ RSpec.describe 'Query.work_item(id)', feature_category: :team_planning do
expect { post_graphql(query, current_user: developer) }.not_to exceed_query_limit(control).with_threshold(4)
expect_graphql_errors_to_be_empty
end
+
+ context 'when work item is associated with a group' do
+ let_it_be(:group_work_item) { create(:work_item, :group_level, namespace: group) }
+ let_it_be(:group_work_item_note) { create(:note, noteable: group_work_item, author: developer, project: nil) }
+ let(:global_id) { group_work_item.to_gid.to_s }
+
+ before_all do
+ create(:award_emoji, awardable: group_work_item_note, name: 'rocket', user: developer)
+ end
+
+ it 'returns notes for the group work item' do
+ all_widgets = graphql_dig_at(work_item_data, :widgets)
+ notes_widget = all_widgets.find { |x| x['type'] == 'NOTES' }
+ notes = graphql_dig_at(notes_widget['discussions'], :nodes).flat_map { |d| d['notes']['nodes'] }
+
+ expect(notes).to contain_exactly(
+ hash_including('body' => group_work_item_note.note)
+ )
+ end
+ end
end
end
diff --git a/spec/requests/api/group_packages_spec.rb b/spec/requests/api/group_packages_spec.rb
index 0b4f6130132..0786815c787 100644
--- a/spec/requests/api/group_packages_spec.rb
+++ b/spec/requests/api/group_packages_spec.rb
@@ -137,6 +137,29 @@ RSpec.describe API::GroupPackages, feature_category: :package_registry do
it_behaves_like 'filters on each package_type', is_project: false
+ context 'filtering on package_version' do
+ include_context 'package filter context'
+
+ let!(:package1) { create(:nuget_package, project: project, version: '2.0.4') }
+ let!(:package2) { create(:nuget_package, project: project) }
+
+ it 'returns the versioned package' do
+ url = group_filter_url(:version, '2.0.4')
+ get api(url, user)
+
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['version']).to eq(package1.version)
+ end
+
+ it 'include_versionless has no effect' do
+ url = "/groups/#{group.id}/packages?package_version=2.0.4&include_versionless=true"
+ get api(url, user)
+
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['version']).to eq(package1.version)
+ end
+ end
+
context 'does not accept non supported package_type value' do
include_context 'package filter context'
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index 662e11f7cfb..327dfd0a76b 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -572,7 +572,8 @@ RSpec.describe API::Groups, feature_category: :groups_and_projects do
expect(json_response['require_two_factor_authentication']).to eq(group1.require_two_factor_authentication)
expect(json_response['two_factor_grace_period']).to eq(group1.two_factor_grace_period)
expect(json_response['auto_devops_enabled']).to eq(group1.auto_devops_enabled)
- expect(json_response['emails_disabled']).to eq(group1.emails_disabled)
+ expect(json_response['emails_disabled']).to eq(group1.emails_disabled?)
+ expect(json_response['emails_enabled']).to eq(group1.emails_enabled?)
expect(json_response['mentions_disabled']).to eq(group1.mentions_disabled)
expect(json_response['project_creation_level']).to eq('maintainer')
expect(json_response['subgroup_creation_level']).to eq('maintainer')
@@ -870,6 +871,38 @@ RSpec.describe API::Groups, feature_category: :groups_and_projects do
end
end
+ before do
+ stub_application_setting(update_namespace_name_rate_limit: 1)
+ end
+
+ it 'increments the update_namespace_name rate limit' do
+ put api("/groups/#{group1.id}", user1), params: { name: "#{new_group_name}_1" }
+
+ expect(::Gitlab::ApplicationRateLimiter.peek(:update_namespace_name, scope: group1)).to be_falsey
+
+ put api("/groups/#{group1.id}", user1), params: { name: "#{new_group_name}_2" }
+
+ expect(::Gitlab::ApplicationRateLimiter.peek(:update_namespace_name, scope: group1)).to be_truthy
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(group1.reload.name).to eq("#{new_group_name}_2")
+ end
+
+ context 'a name is not passed in' do
+ it 'does not mark name update throttling' do
+ expect(::Gitlab::ApplicationRateLimiter).not_to receive(:throttled?)
+
+ put api("/groups/#{group1.id}", user1), params: { path: 'another/path' }
+ end
+ end
+
+ context 'an empty name is passed in' do
+ it 'does not mark name update throttling' do
+ expect(::Gitlab::ApplicationRateLimiter).not_to receive(:throttled?)
+
+ put api("/groups/#{group1.id}", user1), params: { name: '' }
+ end
+ end
+
context 'when authenticated as the group owner' do
it 'updates the group', :aggregate_failures do
workhorse_form_with_file(
@@ -895,7 +928,8 @@ RSpec.describe API::Groups, feature_category: :groups_and_projects do
expect(json_response['require_two_factor_authentication']).to eq(false)
expect(json_response['two_factor_grace_period']).to eq(48)
expect(json_response['auto_devops_enabled']).to eq(nil)
- expect(json_response['emails_disabled']).to eq(nil)
+ expect(json_response['emails_disabled']).to eq(false)
+ expect(json_response['emails_enabled']).to eq(true)
expect(json_response['mentions_disabled']).to eq(nil)
expect(json_response['project_creation_level']).to eq("noone")
expect(json_response['subgroup_creation_level']).to eq("maintainer")
diff --git a/spec/requests/api/helm_packages_spec.rb b/spec/requests/api/helm_packages_spec.rb
index d6afd6f86ff..75f60c59759 100644
--- a/spec/requests/api/helm_packages_spec.rb
+++ b/spec/requests/api/helm_packages_spec.rb
@@ -101,6 +101,12 @@ RSpec.describe API::HelmPackages, feature_category: :package_registry do
end
it_behaves_like 'deploy token for package GET requests'
+
+ context 'when format param is not nil' do
+ let(:url) { "/projects/#{project.id}/packages/helm/stable/charts/#{package.name}-#{package.version}.tgz.prov" }
+
+ it_behaves_like 'rejects helm packages access', :maintainer, :not_found, '{"message":"404 Format prov Not Found"}'
+ end
end
describe 'POST /api/v4/projects/:id/packages/helm/api/:channel/charts/authorize' do
diff --git a/spec/requests/api/internal/base_spec.rb b/spec/requests/api/internal/base_spec.rb
index cf0cd9a2e85..e59633b6d35 100644
--- a/spec/requests/api/internal/base_spec.rb
+++ b/spec/requests/api/internal/base_spec.rb
@@ -111,12 +111,12 @@ RSpec.describe API::Internal::Base, feature_category: :system_access do
it 'returns new recovery codes when the user exists' do
allow_any_instance_of(User).to receive(:two_factor_enabled?).and_return(true)
allow_any_instance_of(User)
- .to receive(:generate_otp_backup_codes!).and_return(%w(119135e5a3ebce8e 34bd7b74adbc8861))
+ .to receive(:generate_otp_backup_codes!).and_return(%w[119135e5a3ebce8e 34bd7b74adbc8861])
subject
expect(json_response['success']).to be_truthy
- expect(json_response['recovery_codes']).to match_array(%w(119135e5a3ebce8e 34bd7b74adbc8861))
+ expect(json_response['recovery_codes']).to match_array(%w[119135e5a3ebce8e 34bd7b74adbc8861])
end
end
@@ -200,7 +200,7 @@ RSpec.describe API::Internal::Base, feature_category: :system_access do
params: {
key_id: key.id,
name: 'newtoken',
- scopes: %w(read_api badscope read_repository)
+ scopes: %w[read_api badscope read_repository]
},
headers: gitlab_shell_internal_api_request_header
@@ -216,14 +216,14 @@ RSpec.describe API::Internal::Base, feature_category: :system_access do
params: {
key_id: key.id,
name: 'newtoken',
- scopes: %w(read_api read_repository),
+ scopes: %w[read_api read_repository],
expires_at: max_pat_access_token_lifetime
},
headers: gitlab_shell_internal_api_request_header
expect(json_response['success']).to be_truthy
expect(json_response['token']).to match(/\A\S{#{token_size}}\z/)
- expect(json_response['scopes']).to match_array(%w(read_api read_repository))
+ expect(json_response['scopes']).to match_array(%w[read_api read_repository])
expect(json_response['expires_at']).to eq(max_pat_access_token_lifetime.iso8601)
end
end
@@ -236,14 +236,14 @@ RSpec.describe API::Internal::Base, feature_category: :system_access do
params: {
key_id: key.id,
name: 'newtoken',
- scopes: %w(read_api read_repository),
+ scopes: %w[read_api read_repository],
expires_at: 365.days.from_now
},
headers: gitlab_shell_internal_api_request_header
expect(json_response['success']).to be_truthy
expect(json_response['token']).to match(/\A\S{#{token_size}}\z/)
- expect(json_response['scopes']).to match_array(%w(read_api read_repository))
+ expect(json_response['scopes']).to match_array(%w[read_api read_repository])
expect(json_response['expires_at']).to eq(max_pat_access_token_lifetime.iso8601)
end
end
@@ -560,6 +560,20 @@ RSpec.describe API::Internal::Base, feature_category: :system_access do
end
end
+ context 'when Gitaly provides a relative_path argument', :request_store do
+ subject { push(key, project, relative_path: relative_path) }
+
+ let(:relative_path) { 'relative_path' }
+
+ it 'stores relative_path value in RequestStore' do
+ allow(Gitlab::SafeRequestStore).to receive(:[]=).and_call_original
+ expect(Gitlab::SafeRequestStore).to receive(:[]=).with(:gitlab_git_relative_path, relative_path)
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
context "git push with project.wiki" do
subject { push(key, project.wiki, env: env.to_json) }
@@ -744,6 +758,17 @@ RSpec.describe API::Internal::Base, feature_category: :system_access do
expect(json_response["gitaly"]["features"]).to eq('gitaly-feature-mep-mep' => 'false')
end
end
+
+ context 'with audit event' do
+ it 'does not send a git streaming audit event' do
+ expect(::Gitlab::Audit::Auditor).not_to receive(:audit)
+
+ pull(key, project)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response["need_audit"]).to be_falsy
+ end
+ end
end
context "git push" do
@@ -757,6 +782,7 @@ RSpec.describe API::Internal::Base, feature_category: :system_access do
expect(json_response["gl_project_path"]).to eq(project.full_path)
expect(json_response["gl_key_type"]).to eq("key")
expect(json_response["gl_key_id"]).to eq(key.id)
+ expect(json_response["need_audit"]).to be_falsy
expect(json_response["gitaly"]).not_to be_nil
expect(json_response["gitaly"]["repository"]).not_to be_nil
expect(json_response["gitaly"]["repository"]["storage_name"]).to eq(project.repository.gitaly_repository.storage_name)
@@ -885,7 +911,7 @@ RSpec.describe API::Internal::Base, feature_category: :system_access do
{
'action' => 'geo_proxy_to_primary',
'data' => {
- 'api_endpoints' => %w{geo/proxy_git_ssh/info_refs_receive_pack geo/proxy_git_ssh/receive_pack},
+ 'api_endpoints' => %w[geo/proxy_git_ssh/info_refs_receive_pack geo/proxy_git_ssh/receive_pack],
'gl_username' => 'testuser',
'primary_repo' => 'http://localhost:3000/testuser/repo.git'
}
@@ -1515,7 +1541,7 @@ RSpec.describe API::Internal::Base, feature_category: :system_access do
describe 'POST /internal/pre_receive' do
let(:valid_params) do
- { gl_repository: gl_repository }
+ { gl_repository: gl_repository, user_id: user.id }
end
it 'decreases the reference counter and returns the result' do
@@ -1527,6 +1553,12 @@ RSpec.describe API::Internal::Base, feature_category: :system_access do
expect(json_response['reference_counter_increased']).to be(true)
end
+
+ it 'sticks to the primary' do
+ expect(User.sticking).to receive(:find_caught_up_replica).with(:user, user.id)
+
+ post api("/internal/pre_receive"), params: valid_params, headers: gitlab_shell_internal_api_request_header
+ end
end
describe 'POST /internal/two_factor_config' do
diff --git a/spec/requests/api/internal/pages_spec.rb b/spec/requests/api/internal/pages_spec.rb
index 1eeb3404157..7e2a778f433 100644
--- a/spec/requests/api/internal/pages_spec.rb
+++ b/spec/requests/api/internal/pages_spec.rb
@@ -46,17 +46,7 @@ RSpec.describe API::Internal::Pages, feature_category: :pages do
end
end
- context 'when authenticated' do
- before do
- project.update_pages_deployment!(create(:pages_deployment, project: project))
- end
-
- around do |example|
- freeze_time do
- example.run
- end
- end
-
+ context 'when authenticated', :freeze_time do
context 'when domain does not exist' do
it 'responds with 204 no content' do
get api('/internal/pages'), headers: auth_header, params: { host: 'any-domain.gitlab.io' }
@@ -79,10 +69,6 @@ RSpec.describe API::Internal::Pages, feature_category: :pages do
end
context 'when there are no pages deployed for the related project' do
- before do
- project.mark_pages_as_not_deployed
- end
-
it 'responds with 204 No Content' do
get api('/internal/pages'), headers: auth_header, params: { host: 'pages.io' }
@@ -91,9 +77,7 @@ RSpec.describe API::Internal::Pages, feature_category: :pages do
end
context 'when there are pages deployed for the related project' do
- before do
- project.mark_pages_as_deployed
- end
+ let!(:deployment) { create(:pages_deployment, project: project) }
it 'domain lookup is case insensitive' do
get api('/internal/pages'), headers: auth_header, params: { host: 'Pages.IO' }
@@ -110,7 +94,6 @@ RSpec.describe API::Internal::Pages, feature_category: :pages do
expect(json_response['certificate']).to eq(pages_domain.certificate)
expect(json_response['key']).to eq(pages_domain.key)
- deployment = project.pages_metadatum.pages_deployment
expect(json_response['lookup_paths']).to eq(
[
{
@@ -144,10 +127,6 @@ RSpec.describe API::Internal::Pages, feature_category: :pages do
end
context 'when there are no pages deployed for the related project' do
- before do
- project.mark_pages_as_not_deployed
- end
-
it 'responds with 204 No Content' do
get api('/internal/pages'), headers: auth_header, params: { host: 'unique-domain.example.com' }
@@ -156,9 +135,7 @@ RSpec.describe API::Internal::Pages, feature_category: :pages do
end
context 'when there are pages deployed for the related project' do
- before do
- project.mark_pages_as_deployed
- end
+ let!(:deployment) { create(:pages_deployment, project: project) }
context 'when the unique domain is disabled' do
before do
@@ -186,7 +163,6 @@ RSpec.describe API::Internal::Pages, feature_category: :pages do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('internal/pages/virtual_domain')
- deployment = project.pages_metadatum.pages_deployment
expect(json_response['lookup_paths']).to eq(
[
{
@@ -218,10 +194,6 @@ RSpec.describe API::Internal::Pages, feature_category: :pages do
end
context 'when there are no pages deployed for the related project' do
- before do
- project.mark_pages_as_not_deployed
- end
-
it 'responds with 204 No Content' do
get api('/internal/pages'), headers: auth_header, params: { host: "#{group.path}.gitlab-pages.io" }
@@ -232,9 +204,7 @@ RSpec.describe API::Internal::Pages, feature_category: :pages do
end
context 'when there are pages deployed for the related project' do
- before do
- project.mark_pages_as_deployed
- end
+ let!(:deployment) { create(:pages_deployment, project: project) }
context 'with a regular project' do
it 'responds with the correct domain configuration' do
@@ -243,7 +213,6 @@ RSpec.describe API::Internal::Pages, feature_category: :pages do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('internal/pages/virtual_domain')
- deployment = project.pages_metadatum.pages_deployment
expect(json_response['lookup_paths']).to eq(
[
{
@@ -274,7 +243,7 @@ RSpec.describe API::Internal::Pages, feature_category: :pages do
3.times do
project = create(:project, group: group)
- project.mark_pages_as_deployed
+ create(:pages_deployment, project: project)
end
expect { get api('/internal/pages'), headers: auth_header, params: { host: "#{group.path}.gitlab-pages.io" } }
@@ -292,7 +261,6 @@ RSpec.describe API::Internal::Pages, feature_category: :pages do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('internal/pages/virtual_domain')
- deployment = project.pages_metadatum.pages_deployment
expect(json_response['lookup_paths']).to eq(
[
{
diff --git a/spec/requests/api/issues/get_group_issues_spec.rb b/spec/requests/api/issues/get_group_issues_spec.rb
index eaa3c46d0ca..0ae65479d5e 100644
--- a/spec/requests/api/issues/get_group_issues_spec.rb
+++ b/spec/requests/api/issues/get_group_issues_spec.rb
@@ -350,7 +350,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
get api(base_url, admin)
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response.last.keys).to include(*%w(id iid project_id title description))
+ expect(json_response.last.keys).to include(*%w[id iid project_id title description])
expect(json_response.last).not_to have_key('subscribed')
end
diff --git a/spec/requests/api/issues/get_project_issues_spec.rb b/spec/requests/api/issues/get_project_issues_spec.rb
index 137fba66eaa..9e54ec08486 100644
--- a/spec/requests/api/issues/get_project_issues_spec.rb
+++ b/spec/requests/api/issues/get_project_issues_spec.rb
@@ -530,7 +530,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
get api("#{base_url}/issues", user)
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response.last.keys).to include(*%w(id iid project_id title description))
+ expect(json_response.last.keys).to include(*%w[id iid project_id title description])
expect(json_response.last).not_to have_key('subscribed')
end
diff --git a/spec/requests/api/issues/issues_spec.rb b/spec/requests/api/issues/issues_spec.rb
index af289352778..ed71089c5a9 100644
--- a/spec/requests/api/issues/issues_spec.rb
+++ b/spec/requests/api/issues/issues_spec.rb
@@ -638,7 +638,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
it 'returns an empty array if no issue matches labels with labels param as array' do
- get api('/issues', user), params: { labels: %w(foo bar) }
+ get api('/issues', user), params: { labels: %w[foo bar] }
expect_paginated_array_response([])
end
@@ -914,7 +914,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
it 'fails to sort with non predefined options' do
- %w(milestone abracadabra).each do |sort_opt|
+ %w[milestone abracadabra].each do |sort_opt|
get api('/issues', user), params: { order_by: sort_opt, sort: 'asc' }
expect(response).to have_gitlab_http_status(:bad_request)
end
diff --git a/spec/requests/api/issues/post_projects_issues_spec.rb b/spec/requests/api/issues/post_projects_issues_spec.rb
index 1cd20680afb..60142e7e151 100644
--- a/spec/requests/api/issues/post_projects_issues_spec.rb
+++ b/spec/requests/api/issues/post_projects_issues_spec.rb
@@ -183,7 +183,7 @@ RSpec.describe API::Issues, :aggregate_failures, feature_category: :team_plannin
expect(response).to have_gitlab_http_status(:created)
expect(json_response['title']).to eq('new issue')
expect(json_response['description']).to be_nil
- expect(json_response['labels']).to eq(%w(label label2))
+ expect(json_response['labels']).to eq(%w[label label2])
expect(json_response['confidential']).to be_falsy
expect(json_response['assignee']['name']).to eq(user2.name)
expect(json_response['assignees'].first['name']).to eq(user2.name)
@@ -191,12 +191,12 @@ RSpec.describe API::Issues, :aggregate_failures, feature_category: :team_plannin
it 'creates a new project issue with labels param as array' do
post api("/projects/#{project.id}/issues", user),
- params: { title: 'new issue', labels: %w(label label2), weight: 3, assignee_ids: [user2.id] }
+ params: { title: 'new issue', labels: %w[label label2], weight: 3, assignee_ids: [user2.id] }
expect(response).to have_gitlab_http_status(:created)
expect(json_response['title']).to eq('new issue')
expect(json_response['description']).to be_nil
- expect(json_response['labels']).to eq(%w(label label2))
+ expect(json_response['labels']).to eq(%w[label label2])
expect(json_response['confidential']).to be_falsy
expect(json_response['assignee']['name']).to eq(user2.name)
expect(json_response['assignees'].first['name']).to eq(user2.name)
@@ -391,7 +391,7 @@ RSpec.describe API::Issues, :aggregate_failures, feature_category: :team_plannin
it 'cannot create new labels with labels param as array' do
expect do
- post api("/projects/#{project.id}/issues", non_member), params: { title: 'new issue', labels: %w(label label2) }
+ post api("/projects/#{project.id}/issues", non_member), params: { title: 'new issue', labels: %w[label label2] }
end.not_to change { project.labels.count }
end
end
diff --git a/spec/requests/api/issues/put_projects_issues_spec.rb b/spec/requests/api/issues/put_projects_issues_spec.rb
index dbba31cd4d6..070ef6057dd 100644
--- a/spec/requests/api/issues/put_projects_issues_spec.rb
+++ b/spec/requests/api/issues/put_projects_issues_spec.rb
@@ -359,7 +359,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
it 'updates labels and touches the record with labels param as array', :aggregate_failures do
travel_to(2.minutes.from_now) do
- put api_for_user, params: { labels: %w(foo bar) }
+ put api_for_user, params: { labels: %w[foo bar] }
end
expect(response).to have_gitlab_http_status(:ok)
diff --git a/spec/requests/api/maven_packages_spec.rb b/spec/requests/api/maven_packages_spec.rb
index 1f841eefff2..578a4821b5e 100644
--- a/spec/requests/api/maven_packages_spec.rb
+++ b/spec/requests/api/maven_packages_spec.rb
@@ -946,6 +946,22 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do
expect(response).to have_gitlab_http_status(:forbidden)
end
+ context 'with basic auth' do
+ where(:token_type) do
+ %i[personal_access_token deploy_token job]
+ end
+
+ with_them do
+ let(:token) { send(token_type).token }
+
+ it "authorizes upload with #{params[:token_type]} token" do
+ authorize_upload({}, headers.merge(basic_auth_header(token_type == :job ? ::Gitlab::Auth::CI_JOB_USER : user.username, token)))
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+ end
+
def authorize_upload(params = {}, request_headers = headers)
put api("/projects/#{project.id}/packages/maven/com/example/my-app/#{version}/maven-metadata.xml/authorize"), params: params, headers: request_headers
end
@@ -1083,6 +1099,22 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do
expect(response).to have_gitlab_http_status(:forbidden)
end
+ context 'with basic auth' do
+ where(:token_type) do
+ %i[personal_access_token deploy_token job]
+ end
+
+ with_them do
+ let(:token) { send(token_type).token }
+
+ it "allows upload with #{params[:token_type]} token" do
+ upload_file(params: params, request_headers: headers.merge(basic_auth_header(token_type == :job ? ::Gitlab::Auth::CI_JOB_USER : user.username, token)))
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+ end
+
context 'file name is too long' do
let(:file_name) { 'a' * (Packages::Maven::FindOrCreatePackageService::MAX_FILE_NAME_LENGTH + 1) }
diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb
index 8dab9d555cf..feb24a4e73f 100644
--- a/spec/requests/api/members_spec.rb
+++ b/spec/requests/api/members_spec.rb
@@ -826,48 +826,20 @@ RSpec.describe API::Members, feature_category: :groups_and_projects do
end
end
- context 'with admin_group_member FF disabled' do
- before do
- stub_feature_flags(admin_group_member: false)
- end
-
- it_behaves_like 'POST /:source_type/:id/members', 'project' do
- let(:source) { project }
- end
-
- it_behaves_like 'POST /:source_type/:id/members', 'group' do
- let(:source) { group }
- end
-
- it_behaves_like 'PUT /:source_type/:id/members/:user_id', 'project' do
- let(:source) { project }
- end
-
- it_behaves_like 'PUT /:source_type/:id/members/:user_id', 'group' do
- let(:source) { group }
- end
+ it_behaves_like 'POST /:source_type/:id/members', 'project' do
+ let(:source) { project }
end
- context 'with admin_group_member FF enabled' do
- before do
- stub_feature_flags(admin_group_member: true)
- end
-
- it_behaves_like 'POST /:source_type/:id/members', 'project' do
- let(:source) { project }
- end
-
- it_behaves_like 'POST /:source_type/:id/members', 'group' do
- let(:source) { group }
- end
+ it_behaves_like 'POST /:source_type/:id/members', 'group' do
+ let(:source) { group }
+ end
- it_behaves_like 'PUT /:source_type/:id/members/:user_id', 'project' do
- let(:source) { project }
- end
+ it_behaves_like 'PUT /:source_type/:id/members/:user_id', 'project' do
+ let(:source) { project }
+ end
- it_behaves_like 'PUT /:source_type/:id/members/:user_id', 'group' do
- let(:source) { group }
- end
+ it_behaves_like 'PUT /:source_type/:id/members/:user_id', 'group' do
+ let(:source) { group }
end
it_behaves_like 'DELETE /:source_type/:id/members/:user_id', 'project' do
diff --git a/spec/requests/api/merge_request_approvals_spec.rb b/spec/requests/api/merge_request_approvals_spec.rb
index a1d6abec97e..2de59750273 100644
--- a/spec/requests/api/merge_request_approvals_spec.rb
+++ b/spec/requests/api/merge_request_approvals_spec.rb
@@ -8,8 +8,6 @@ RSpec.describe API::MergeRequestApprovals, feature_category: :source_code_manage
let_it_be(:bot) { create(:user, :project_bot) }
let_it_be(:project) { create(:project, :public, :repository, creator: user, namespace: user.namespace) }
let_it_be(:approver) { create :user }
- let_it_be(:group) { create :group }
-
let(:merge_request) { create(:merge_request, :simple, author: user, source_project: project) }
describe 'GET :id/merge_requests/:merge_request_iid/approvals' do
@@ -87,6 +85,28 @@ RSpec.describe API::MergeRequestApprovals, feature_category: :source_code_manage
expect(response).to have_gitlab_http_status(:created)
end
+
+ it 'calls MergeRequests::UpdateReviewerStateService' do
+ unapprover = create(:user)
+
+ project.add_developer(approver)
+ project.add_developer(unapprover)
+ project.add_developer(create(:user))
+
+ create(:approval, user: approver, merge_request: merge_request)
+ create(:approval, user: unapprover, merge_request: merge_request)
+
+ expect_next_instance_of(
+ MergeRequests::UpdateReviewerStateService,
+ project: project, current_user: unapprover
+ ) do |service|
+ expect(service).to receive(:execute).with(merge_request, "unreviewed")
+ end
+
+ post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/unapprove", unapprover)
+
+ expect(response).to have_gitlab_http_status(:created)
+ end
end
end
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 2cf8872cd40..6000fa29dc4 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -229,7 +229,7 @@ RSpec.describe API::MergeRequests, :aggregate_failures, feature_category: :sourc
merge_request_closed.id,
merge_request.id
])
- expect(json_response.last.keys).to match_array(%w(id iid title web_url created_at description project_id state updated_at))
+ expect(json_response.last.keys).to match_array(%w[id iid title web_url created_at description project_id state updated_at])
expect(json_response.last['iid']).to eq(merge_request.iid)
expect(json_response.last['title']).to eq(merge_request.title)
expect(json_response.last).to have_key('web_url')
@@ -2175,7 +2175,7 @@ RSpec.describe API::MergeRequests, :aggregate_failures, feature_category: :sourc
expect(response).to have_gitlab_http_status(:created)
expect(json_response['title']).to eq('Test merge_request')
- expect(json_response['labels']).to eq(%w(label label2))
+ expect(json_response['labels']).to eq(%w[label label2])
expect(json_response['milestone']['id']).to eq(milestone.id)
expect(json_response['squash']).to be_truthy
expect(json_response['force_remove_source_branch']).to be_falsy
@@ -2187,11 +2187,11 @@ RSpec.describe API::MergeRequests, :aggregate_failures, feature_category: :sourc
end
it_behaves_like 'creates merge request with labels' do
- let(:labels) { %w(label label2) }
+ let(:labels) { %w[label label2] }
end
it_behaves_like 'creates merge request with labels' do
- let(:labels) { %w(label label2) }
+ let(:labels) { %w[label label2] }
end
it 'creates merge request with special label names' do
diff --git a/spec/requests/api/metadata_spec.rb b/spec/requests/api/metadata_spec.rb
index b81fe3f51b5..c8cee31db47 100644
--- a/spec/requests/api/metadata_spec.rb
+++ b/spec/requests/api/metadata_spec.rb
@@ -26,7 +26,7 @@ RSpec.describe API::Metadata, feature_category: :shared do
let(:personal_access_token) { create(:personal_access_token, scopes: scopes) }
context 'with api scope' do
- let(:scopes) { %i(api) }
+ let(:scopes) { %i[api] }
it 'returns the metadata information' do
get api(endpoint, personal_access_token: personal_access_token)
@@ -42,7 +42,7 @@ RSpec.describe API::Metadata, feature_category: :shared do
end
context 'with ai_features scope' do
- let(:scopes) { %i(ai_features) }
+ let(:scopes) { %i[ai_features] }
it 'returns the metadata information' do
get api(endpoint, personal_access_token: personal_access_token)
@@ -58,7 +58,7 @@ RSpec.describe API::Metadata, feature_category: :shared do
end
context 'with read_user scope' do
- let(:scopes) { %i(read_user) }
+ let(:scopes) { %i[read_user] }
it 'returns the metadata information' do
get api(endpoint, personal_access_token: personal_access_token)
@@ -74,7 +74,7 @@ RSpec.describe API::Metadata, feature_category: :shared do
end
context 'with neither api, ai_features nor read_user scope' do
- let(:scopes) { %i(read_repository) }
+ let(:scopes) { %i[read_repository] }
it 'returns authorization error' do
get api(endpoint, personal_access_token: personal_access_token)
diff --git a/spec/requests/api/ml/mlflow/model_versions_spec.rb b/spec/requests/api/ml/mlflow/model_versions_spec.rb
new file mode 100644
index 00000000000..f59888ec70f
--- /dev/null
+++ b/spec/requests/api/ml/mlflow/model_versions_spec.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::Ml::Mlflow::ModelVersions, feature_category: :mlops do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } }
+ let_it_be(:another_project) { build(:project).tap { |p| p.add_developer(developer) } }
+
+ let_it_be(:name) { 'a-model-name' }
+ let_it_be(:version) { '0.0.1' }
+ let_it_be(:model) { create(:ml_models, project: project, name: name) }
+ let_it_be(:model_version) { create(:ml_model_versions, project: project, model: model, version: version) }
+
+ let_it_be(:tokens) do
+ {
+ write: create(:personal_access_token, scopes: %w[read_api api], user: developer),
+ read: create(:personal_access_token, scopes: %w[read_api], user: developer),
+ no_access: create(:personal_access_token, scopes: %w[read_user], user: developer),
+ different_user: create(:personal_access_token, scopes: %w[read_api api], user: build(:user))
+ }
+ end
+
+ let(:current_user) { developer }
+ let(:access_token) { tokens[:write] }
+ let(:headers) { { 'Authorization' => "Bearer #{access_token.token}" } }
+ let(:project_id) { project.id }
+ let(:default_params) { {} }
+ let(:params) { default_params }
+ let(:request) { get api(route), params: params, headers: headers }
+ let(:json_response) { Gitlab::Json.parse(api_response.body) }
+
+ subject(:api_response) do
+ request
+ response
+ end
+
+ describe 'GET /projects/:id/ml/mlflow/api/2.0/mlflow/model_versions/get' do
+ let(:route) do
+ "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/model_versions/get?name=#{name}&version=#{version}"
+ end
+
+ it 'returns the model version', :aggregate_failures do
+ is_expected.to have_gitlab_http_status(:ok)
+ expect(json_response['model_version']).not_to be_nil
+ expect(json_response['model_version']['name']).to eq(name)
+ expect(json_response['model_version']['version']).to eq(version)
+ end
+
+ describe 'Error States' do
+ context 'when has access' do
+ context 'and model name in incorrect' do
+ let(:route) do
+ "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/model_versions/get?name=--&version=#{version}"
+ end
+
+ it_behaves_like 'MLflow|Not Found - Resource Does Not Exist'
+ end
+
+ context 'and version in incorrect' do
+ let(:route) do
+ "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/model_versions/get?name=#{name}&version=--"
+ end
+
+ it_behaves_like 'MLflow|Not Found - Resource Does Not Exist'
+ end
+
+ context 'when user lacks read_model_registry rights' do
+ before do
+ allow(Ability).to receive(:allowed?).and_call_original
+ allow(Ability).to receive(:allowed?)
+ .with(current_user, :read_model_registry, project)
+ .and_return(false)
+ end
+
+ it "is Not Found" do
+ is_expected.to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ it_behaves_like 'MLflow|shared model registry error cases'
+ it_behaves_like 'MLflow|Requires read_api scope'
+ end
+ end
+end
diff --git a/spec/requests/api/ml/mlflow/registered_models_spec.rb b/spec/requests/api/ml/mlflow/registered_models_spec.rb
new file mode 100644
index 00000000000..cd8b0a53ef3
--- /dev/null
+++ b/spec/requests/api/ml/mlflow/registered_models_spec.rb
@@ -0,0 +1,203 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::Ml::Mlflow::RegisteredModels, feature_category: :mlops do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } }
+ let_it_be(:model) do
+ create(:ml_models, :with_metadata, project: project)
+ end
+
+ let_it_be(:tokens) do
+ {
+ write: create(:personal_access_token, scopes: %w[read_api api], user: developer),
+ read: create(:personal_access_token, scopes: %w[read_api], user: developer),
+ no_access: create(:personal_access_token, scopes: %w[read_user], user: developer),
+ different_user: create(:personal_access_token, scopes: %w[read_api api], user: build(:user))
+ }
+ end
+
+ let(:current_user) { developer }
+ let(:access_token) { tokens[:write] }
+ let(:headers) { { 'Authorization' => "Bearer #{access_token.token}" } }
+ let(:project_id) { project.id }
+ let(:default_params) { {} }
+ let(:params) { default_params }
+ let(:request) { get api(route), params: params, headers: headers }
+ let(:json_response) { Gitlab::Json.parse(api_response.body) }
+
+ subject(:api_response) do
+ request
+ response
+ end
+
+ describe 'GET /projects/:id/ml/mlflow/api/2.0/mlflow/registered-models/get' do
+ let(:model_name) { model.name }
+ let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/registered-models/get?name=#{model_name}" }
+
+ it 'returns the model', :aggregate_failures do
+ is_expected.to have_gitlab_http_status(:ok)
+ is_expected.to match_response_schema('ml/get_model')
+ end
+
+ describe 'Error States' do
+ context 'when has access' do
+ context 'and model does not exist' do
+ let(:model_name) { 'foo' }
+
+ it_behaves_like 'MLflow|Not Found - Resource Does Not Exist'
+ end
+
+ context 'and name is not passed' do
+ let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/registered-models/get" }
+
+ it_behaves_like 'MLflow|Not Found - Resource Does Not Exist'
+ end
+ end
+
+ it_behaves_like 'MLflow|shared model registry error cases'
+ it_behaves_like 'MLflow|Requires read_api scope'
+ end
+ end
+
+ describe 'POST /projects/:id/ml/mlflow/api/2.0/mlflow/registered-models/create' do
+ let(:route) do
+ "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/registered-models/create"
+ end
+
+ let(:params) { { name: 'my-model-name' } }
+ let(:request) { post api(route), params: params, headers: headers }
+
+ it 'creates the model', :aggregate_failures do
+ is_expected.to have_gitlab_http_status(:ok)
+ expect(json_response).to include('registered_model')
+ end
+
+ describe 'Error States' do
+ context 'when the model name is not passed' do
+ let(:params) { {} }
+
+ it_behaves_like 'MLflow|Bad Request'
+ end
+
+ context 'when the model name already exists' do
+ let(:existing_model) do
+ create(:ml_models, user: current_user, project: project)
+ end
+
+ let(:params) { { name: existing_model.name } }
+
+ it "is Bad Request", :aggregate_failures do
+ is_expected.to have_gitlab_http_status(:bad_request)
+
+ expect(json_response).to include({ 'error_code' => 'RESOURCE_ALREADY_EXISTS' })
+ end
+ end
+
+ context 'when project does not exist' do
+ let(:route) { "/projects/#{non_existing_record_id}/ml/mlflow/api/2.0/mlflow/registered-models/create" }
+
+ it "is Not Found", :aggregate_failures do
+ is_expected.to have_gitlab_http_status(:not_found)
+
+ expect(json_response['message']).to eq('404 Project Not Found')
+ end
+ end
+
+ # TODO: Ensure consisted error responses https://gitlab.com/gitlab-org/gitlab/-/issues/429731
+ context 'when a duplicate tag name is supplied' do
+ let(:params) do
+ { name: 'my-model-name', tags: [{ key: 'key1', value: 'value1' }, { key: 'key1', value: 'value2' }] }
+ end
+
+ it "creates the model with only the second tag", :aggregate_failures do
+ expect(json_response).to include({ 'error_code' => 'RESOURCE_ALREADY_EXISTS' })
+ end
+ end
+
+ # TODO: Ensure consisted error responses https://gitlab.com/gitlab-org/gitlab/-/issues/429731
+ context 'when an empty tag name is supplied' do
+ let(:params) do
+ { name: 'my-model-name', tags: [{ key: '', value: 'value1' }, { key: 'key1', value: 'value2' }] }
+ end
+
+ it "creates the model with only the second tag", :aggregate_failures do
+ expect(json_response).to include({ 'error_code' => 'RESOURCE_ALREADY_EXISTS' })
+ end
+ end
+
+ it_behaves_like 'MLflow|shared model registry error cases'
+ it_behaves_like 'MLflow|Requires api scope and write permission'
+ end
+ end
+
+ describe 'PATCH /projects/:id/ml/mlflow/api/2.0/mlflow/registered-models/update' do
+ let(:model_name) { model.name }
+ let(:model_description) { 'updated model description' }
+ let(:params) { { name: model_name, description: model_description } }
+ let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/registered-models/update" }
+ let(:request) { patch api(route), params: params, headers: headers }
+
+ it 'returns the updated model', :aggregate_failures do
+ is_expected.to have_gitlab_http_status(:ok)
+ is_expected.to match_response_schema('ml/update_model')
+ expect(json_response["registered_model"]["description"]).to eq(model_description)
+ end
+
+ describe 'Error States' do
+ context 'when has access' do
+ context 'and model does not exist' do
+ let(:model_name) { 'foo' }
+
+ it_behaves_like 'MLflow|Not Found - Resource Does Not Exist'
+ end
+
+ context 'and name is not passed' do
+ let(:params) { { description: model_description } }
+
+ it_behaves_like 'MLflow|Not Found - Resource Does Not Exist'
+ end
+ end
+
+ it_behaves_like 'MLflow|shared model registry error cases'
+ it_behaves_like 'MLflow|Requires api scope and write permission'
+ end
+ end
+
+ describe 'POST /projects/:id/ml/mlflow/api/2.0/mlflow/registered-models/get-latest-versions' do
+ let_it_be(:version1) { create(:ml_model_versions, model: model, created_at: 1.week.ago) }
+ let_it_be(:version2) { create(:ml_model_versions, model: model, created_at: 1.day.ago) }
+
+ let(:model_name) { model.name }
+ let(:params) { { name: model_name } }
+ let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/registered-models/get-latest-versions" }
+ let(:request) { post api(route), params: params, headers: headers }
+
+ it 'returns an array with the most recently created model version', :aggregate_failures do
+ is_expected.to have_gitlab_http_status(:ok)
+ is_expected.to match_response_schema('ml/get_latest_versions')
+ expect(json_response["model_versions"][0]["name"]).to eq(model_name)
+ expect(json_response["model_versions"][0]["version"]).to eq(version2.version)
+ end
+
+ describe 'Error States' do
+ context 'when has access' do
+ context 'and model does not exist' do
+ let(:model_name) { 'foo' }
+
+ it_behaves_like 'MLflow|Not Found - Resource Does Not Exist'
+ end
+
+ context 'and name is not passed' do
+ let(:params) { {} }
+
+ it_behaves_like 'MLflow|Not Found - Resource Does Not Exist'
+ end
+ end
+
+ it_behaves_like 'MLflow|shared model registry error cases'
+ it_behaves_like 'MLflow|Requires read_api scope'
+ end
+ end
+end
diff --git a/spec/requests/api/npm_project_packages_spec.rb b/spec/requests/api/npm_project_packages_spec.rb
index 340420e46e0..b5f38698857 100644
--- a/spec/requests/api/npm_project_packages_spec.rb
+++ b/spec/requests/api/npm_project_packages_spec.rb
@@ -196,7 +196,7 @@ RSpec.describe API::NpmProjectPackages, feature_category: :package_registry do
context 'with a job token for a different user' do
let_it_be(:other_user) { create(:user) }
- let_it_be_with_reload(:other_job) { create(:ci_build, :running, user: other_user) }
+ let_it_be_with_reload(:other_job) { create(:ci_build, :running, user: other_user, project: project) }
let(:headers) { build_token_auth_header(other_job.token) }
@@ -245,7 +245,7 @@ RSpec.describe API::NpmProjectPackages, feature_category: :package_registry do
context 'with a job token for a different user' do
let_it_be(:other_user) { create(:user) }
- let_it_be_with_reload(:other_job) { create(:ci_build, :running, user: other_user) }
+ let_it_be_with_reload(:other_job) { create(:ci_build, :running, user: other_user, project: project) }
let(:headers) { build_token_auth_header(other_job.token) }
diff --git a/spec/requests/api/pages/pages_spec.rb b/spec/requests/api/pages/pages_spec.rb
index aa1869eaa84..74dd1ed4f2b 100644
--- a/spec/requests/api/pages/pages_spec.rb
+++ b/spec/requests/api/pages/pages_spec.rb
@@ -9,7 +9,6 @@ RSpec.describe API::Pages, feature_category: :pages do
before do
project.add_maintainer(user)
- project.mark_pages_as_deployed
end
describe 'DELETE /projects/:id/pages' do
@@ -17,7 +16,7 @@ RSpec.describe API::Pages, feature_category: :pages do
it_behaves_like 'DELETE request permissions for admin mode' do
before do
- allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
+ stub_pages_setting(enabled: true)
end
let(:succes_status_code) { :no_content }
@@ -25,7 +24,7 @@ RSpec.describe API::Pages, feature_category: :pages do
context 'when Pages is disabled' do
before do
- allow(Gitlab.config.pages).to receive(:enabled).and_return(false)
+ stub_pages_setting(enabled: false)
end
it_behaves_like '404 response' do
@@ -35,7 +34,7 @@ RSpec.describe API::Pages, feature_category: :pages do
context 'when Pages is enabled' do
before do
- allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
+ stub_pages_setting(enabled: true)
end
context 'when Pages are deployed' do
@@ -48,15 +47,11 @@ RSpec.describe API::Pages, feature_category: :pages do
it 'removes the pages' do
delete api(path, admin, admin_mode: true)
- expect(project.reload.pages_metadatum.deployed?).to be(false)
+ expect(project.reload.pages_deployed?).to be(false)
end
end
context 'when pages are not deployed' do
- before do
- project.mark_pages_as_not_deployed
- end
-
it 'returns 204' do
delete api(path, admin, admin_mode: true)
diff --git a/spec/requests/api/personal_access_tokens_spec.rb b/spec/requests/api/personal_access_tokens_spec.rb
index 166768ea605..a1d29c4a935 100644
--- a/spec/requests/api/personal_access_tokens_spec.rb
+++ b/spec/requests/api/personal_access_tokens_spec.rb
@@ -461,6 +461,18 @@ RSpec.describe API::PersonalAccessTokens, :aggregate_failures, feature_category:
expect(json_response['expires_at']).to eq((Date.today + 1.week).to_s)
end
+ context 'when expiry is defined' do
+ it "rotates user's own token", :freeze_time do
+ expiry_date = Date.today + 1.month
+
+ post(api(path, token.user), params: { expires_at: expiry_date })
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['token']).not_to eq(token.token)
+ expect(json_response['expires_at']).to eq(expiry_date.to_s)
+ end
+ end
+
context 'without permission' do
it 'returns an error message' do
another_user = create(:user)
diff --git a/spec/requests/api/project_attributes.yml b/spec/requests/api/project_attributes.yml
index ec98df22af7..165ea7bf66e 100644
--- a/spec/requests/api/project_attributes.yml
+++ b/spec/requests/api/project_attributes.yml
@@ -98,6 +98,7 @@ ci_cd_settings:
- merge_pipelines_enabled
- auto_rollback_enabled
- inbound_job_token_scope_enabled
+ - restrict_pipeline_cancellation_role
remapped_attributes:
default_git_depth: ci_default_git_depth
forward_deployment_enabled: ci_forward_deployment_enabled
diff --git a/spec/requests/api/project_container_repositories_spec.rb b/spec/requests/api/project_container_repositories_spec.rb
index f51b94bb78e..7797e8e9402 100644
--- a/spec/requests/api/project_container_repositories_spec.rb
+++ b/spec/requests/api/project_container_repositories_spec.rb
@@ -168,7 +168,7 @@ RSpec.describe API::ProjectContainerRepositories, feature_category: :container_r
let(:api_user) { reporter }
before do
- stub_container_registry_tags(repository: root_repository.path, tags: %w(rootA latest))
+ stub_container_registry_tags(repository: root_repository.path, tags: %w[rootA latest])
end
it_behaves_like 'a package tracking event', described_class.name, 'list_tags'
@@ -177,7 +177,7 @@ RSpec.describe API::ProjectContainerRepositories, feature_category: :container_r
subject
expect(json_response.length).to eq(2)
- expect(json_response.map { |repository| repository['name'] }).to eq %w(latest rootA)
+ expect(json_response.map { |repository| repository['name'] }).to eq %w[latest rootA]
end
it 'returns a matching schema' do
@@ -362,7 +362,7 @@ RSpec.describe API::ProjectContainerRepositories, feature_category: :container_r
let(:api_user) { reporter }
before do
- stub_container_registry_tags(repository: root_repository.path, tags: %w(rootA), with_manifest: true)
+ stub_container_registry_tags(repository: root_repository.path, tags: %w[rootA], with_manifest: true)
end
it 'returns a details of tag' do
@@ -408,7 +408,7 @@ RSpec.describe API::ProjectContainerRepositories, feature_category: :container_r
context 'when there are multiple tags' do
before do
- stub_container_registry_tags(repository: root_repository.path, tags: %w(rootA rootB), with_manifest: true)
+ stub_container_registry_tags(repository: root_repository.path, tags: %w[rootA rootB], with_manifest: true)
end
it 'properly removes tag' do
@@ -427,7 +427,7 @@ RSpec.describe API::ProjectContainerRepositories, feature_category: :container_r
context 'when there\'s only one tag' do
before do
- stub_container_registry_tags(repository: root_repository.path, tags: %w(rootA), with_manifest: true)
+ stub_container_registry_tags(repository: root_repository.path, tags: %w[rootA], with_manifest: true)
end
it 'properly removes tag' do
diff --git a/spec/requests/api/project_packages_spec.rb b/spec/requests/api/project_packages_spec.rb
index 219c748c9a6..2ac9a7d97f1 100644
--- a/spec/requests/api/project_packages_spec.rb
+++ b/spec/requests/api/project_packages_spec.rb
@@ -197,6 +197,26 @@ RSpec.describe API::ProjectPackages, feature_category: :package_registry do
end
end
+ context 'filtering on package_version' do
+ include_context 'package filter context'
+
+ it 'returns the versioned package' do
+ url = package_filter_url(:version, '2.0.4')
+ get api(url, user)
+
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['version']).to eq(package2.version)
+ end
+
+ it 'include_versionless has no effect' do
+ url = "/projects/#{project.id}/packages?package_version=2.0.4&include_versionless=true"
+ get api(url, user)
+
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['version']).to eq(package2.version)
+ end
+ end
+
it_behaves_like 'with versionless packages'
it_behaves_like 'with status param'
it_behaves_like 'does not cause n^2 queries'
diff --git a/spec/requests/api/project_repository_storage_moves_spec.rb b/spec/requests/api/project_repository_storage_moves_spec.rb
index 96ed3042d00..7b5dc0d5ef8 100644
--- a/spec/requests/api/project_repository_storage_moves_spec.rb
+++ b/spec/requests/api/project_repository_storage_moves_spec.rb
@@ -8,5 +8,29 @@ RSpec.describe API::ProjectRepositoryStorageMoves, feature_category: :gitaly do
let_it_be(:storage_move) { create(:project_repository_storage_move, :scheduled, container: container) }
let(:repository_storage_move_factory) { :project_repository_storage_move }
let(:bulk_worker_klass) { Projects::ScheduleBulkRepositoryShardMovesWorker }
+
+ context 'when project is hidden' do
+ let_it_be(:container) { create(:project, :hidden) }
+ let_it_be(:storage_move) { create(:project_repository_storage_move, :scheduled, container: container) }
+
+ it_behaves_like 'get single container repository storage move' do
+ let(:container_id) { container.id }
+ let(:url) { "/projects/#{container_id}/repository_storage_moves/#{repository_storage_move_id}" }
+ end
+
+ it_behaves_like 'post single container repository storage move'
+ end
+
+ context 'when project is pending delete' do
+ let_it_be(:container) { create(:project, pending_delete: true) }
+ let_it_be(:storage_move) { create(:project_repository_storage_move, :scheduled, container: container) }
+
+ it_behaves_like 'get single container repository storage move' do
+ let(:container_id) { container.id }
+ let(:url) { "/projects/#{container_id}/repository_storage_moves/#{repository_storage_move_id}" }
+ end
+
+ it_behaves_like 'post single container repository storage move'
+ end
end
end
diff --git a/spec/requests/api/project_templates_spec.rb b/spec/requests/api/project_templates_spec.rb
index e1d156afd54..1987d70633b 100644
--- a/spec/requests/api/project_templates_spec.rb
+++ b/spec/requests/api/project_templates_spec.rb
@@ -69,7 +69,7 @@ RSpec.describe API::ProjectTemplates, feature_category: :source_code_management
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(response).to match_response_schema('public_api/v4/template_list')
- expect(json_response.map { |t| t['key'] }).to match_array(%w(bug feature_proposal template_test))
+ expect(json_response.map { |t| t['key'] }).to match_array(%w[bug feature_proposal template_test])
end
it 'returns merge request templates' do
@@ -78,7 +78,7 @@ RSpec.describe API::ProjectTemplates, feature_category: :source_code_management
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(response).to match_response_schema('public_api/v4/template_list')
- expect(json_response.map { |t| t['key'] }).to match_array(%w(bug feature_proposal template_test))
+ expect(json_response.map { |t| t['key'] }).to match_array(%w[bug feature_proposal template_test])
end
it 'returns 400 for an unknown template type' do
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 64e010aa50f..e9319d514aa 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -286,6 +286,32 @@ RSpec.describe API::Projects, :aggregate_failures, feature_category: :groups_and
expect(json_response.map { |p| p['id'] }).not_to include(project.id)
end
+ context 'when user requests pending_delete projects' do
+ before do
+ project.update!(pending_delete: true)
+ end
+
+ let(:params) { { include_pending_delete: true } }
+
+ it 'does not return projects marked for deletion' do
+ get api(path, user), params: params
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to be_an Array
+ expect(json_response.map { |p| p['id'] }).not_to include(project.id)
+ end
+
+ context 'when user is an admin' do
+ it 'returns projects marked for deletion' do
+ get api(path, admin, admin_mode: true), params: params
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to be_an Array
+ expect(json_response.map { |p| p['id'] }).to include(project.id)
+ end
+ end
+ end
+
it 'does not include open_issues_count if issues are disabled' do
project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
@@ -299,7 +325,7 @@ RSpec.describe API::Projects, :aggregate_failures, feature_category: :groups_and
context 'filter by topic (column topic_list)' do
before do
- project.update!(topic_list: %w(ruby javascript))
+ project.update!(topic_list: %w[ruby javascript])
end
it 'returns no projects' do
@@ -868,7 +894,7 @@ RSpec.describe API::Projects, :aggregate_failures, feature_category: :groups_and
context 'sorting' do
context 'by project statistics' do
- %w(repository_size storage_size wiki_size packages_size).each do |order_by|
+ %w[repository_size storage_size wiki_size packages_size].each do |order_by|
context "sorting by #{order_by}" do
before do
ProjectStatistics.update_all(order_by => 100)
@@ -1400,13 +1426,14 @@ RSpec.describe API::Projects, :aggregate_failures, feature_category: :groups_and
it 'disallows creating a project with an import_url that is not reachable' do
url = 'http://example.com'
endpoint_url = "#{url}/info/refs?service=git-upload-pack"
- stub_full_request(endpoint_url, method: :get).to_return({ status: 301, body: '', headers: nil })
+ error_response = { status: 301, body: '', headers: nil }
+ stub_full_request(endpoint_url, method: :get).to_return(error_response)
project_params = { import_url: url, path: 'path-project-Foo', name: 'Foo Project' }
expect { post api(path, user), params: project_params }.not_to change { Project.count }
expect(response).to have_gitlab_http_status(:unprocessable_entity)
- expect(json_response['message']).to eq("#{url} is not a valid HTTP Git repository")
+ expect(json_response['message']).to eq("#{url} endpoint error: #{error_response[:status]}")
end
it 'creates a project with an import_url that is valid' do
@@ -2533,7 +2560,7 @@ RSpec.describe API::Projects, :aggregate_failures, feature_category: :groups_and
context 'and the project has a private repository' do
let(:project) { create(:project, :repository, :public, :repository_private) }
- let(:protected_attributes) { %w(default_branch ci_config_path) }
+ let(:protected_attributes) { %w[default_branch ci_config_path] }
it 'hides protected attributes of private repositories if user is not a member' do
get api(path, user)
@@ -2782,10 +2809,20 @@ RSpec.describe API::Projects, :aggregate_failures, feature_category: :groups_and
expect(json_response['shared_with_groups'][0]['expires_at']).to eq(expires_at.to_s)
end
- it 'returns a project by path name' do
- get api(path, user)
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['name']).to eq(project.name)
+ context 'when path name is specified' do
+ it 'returns a project' do
+ get api("/projects/#{CGI.escape(project.full_path)}", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['name']).to eq(project.name)
+ end
+
+ it 'returns a project using case-insensitive search' do
+ get api("/projects/#{CGI.escape(project.full_path.swapcase)}", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['name']).to eq(project.name)
+ end
end
context 'when a project is moved' do
@@ -3688,6 +3725,16 @@ RSpec.describe API::Projects, :aggregate_failures, feature_category: :groups_and
it_behaves_like '412 response' do
subject(:request) { api("/projects/#{project.id}/share/#{group.id}", user) }
end
+
+ it "returns an error when link is not destroyed" do
+ allow(::Projects::GroupLinks::DestroyService).to receive_message_chain(:new, :execute)
+ .and_return(ServiceResponse.error(message: '404 Not Found', reason: :not_found))
+
+ delete api("/projects/#{project.id}/share/#{group.id}", user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq '404 Not Found'
+ end
end
it 'returns a 400 when group id is not an integer' do
@@ -3893,7 +3940,7 @@ RSpec.describe API::Projects, :aggregate_failures, feature_category: :groups_and
expect(Project.find_by(path: project[:path]).analytics_access_level).to eq(ProjectFeature::PRIVATE)
end
- %i(releases_access_level environments_access_level feature_flags_access_level infrastructure_access_level monitor_access_level model_experiments_access_level).each do |field|
+ %i[releases_access_level environments_access_level feature_flags_access_level infrastructure_access_level monitor_access_level model_experiments_access_level].each do |field|
it "sets #{field}" do
put api(path, user), params: { field => 'private' }
diff --git a/spec/requests/api/pypi_packages_spec.rb b/spec/requests/api/pypi_packages_spec.rb
index 0b2641b062c..9305155d285 100644
--- a/spec/requests/api/pypi_packages_spec.rb
+++ b/spec/requests/api/pypi_packages_spec.rb
@@ -207,7 +207,22 @@ RSpec.describe API::PypiPackages, feature_category: :package_registry do
let(:url) { "/projects/#{project.id}/packages/pypi" }
let(:headers) { {} }
let(:requires_python) { '>=3.7' }
- let(:base_params) { { requires_python: requires_python, version: '1.0.0', name: 'sample-project', sha256_digest: '1' * 64, md5_digest: '1' * 32 } }
+ let(:base_params) do
+ {
+ requires_python: requires_python,
+ version: '1.0.0',
+ name: 'sample-project',
+ sha256_digest: '1' * 64,
+ md5_digest: '1' * 32,
+ metadata_version: '2.3',
+ author_email: 'cschultz@example.com, snoopy@peanuts.com',
+ description: 'Example description',
+ description_content_type: 'text/plain',
+ summary: 'A module for collecting votes from beagles.',
+ keywords: 'dog,puppy,voting,election'
+ }
+ end
+
let(:params) { base_params.merge(content: temp_file(file_name)) }
let(:send_rewritten_field) { true }
let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, user: user, property: 'i_package_pypi_user' } }
diff --git a/spec/requests/api/releases_spec.rb b/spec/requests/api/releases_spec.rb
index a018b91019b..493dc4e72c6 100644
--- a/spec/requests/api/releases_spec.rb
+++ b/spec/requests/api/releases_spec.rb
@@ -1492,7 +1492,7 @@ RSpec.describe API::Releases, :aggregate_failures, feature_category: :release_or
subject
expect(response).to have_gitlab_http_status(:ok)
- expect(returned_milestones).to match_array(%w(milestone2 milestone3))
+ expect(returned_milestones).to match_array(%w[milestone2 milestone3])
end
end
diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb
index 22239f1d23f..f38a120cc74 100644
--- a/spec/requests/api/repositories_spec.rb
+++ b/spec/requests/api/repositories_spec.rb
@@ -742,7 +742,7 @@ RSpec.describe API::Repositories, feature_category: :source_code_management do
describe 'GET :id/repository/merge_base' do
let(:refs) do
- %w(304d257dcb821665ab5110318fc58a007bd104ed 0031876facac3f2b2702a0e53a26e89939a42209 570e7b2abdd848b95f2f578043fc23bd6f6fd24d)
+ %w[304d257dcb821665ab5110318fc58a007bd104ed 0031876facac3f2b2702a0e53a26e89939a42209 570e7b2abdd848b95f2f578043fc23bd6f6fd24d]
end
subject(:request) do
@@ -786,7 +786,7 @@ RSpec.describe API::Repositories, feature_category: :source_code_management do
context 'when passing refs that do not exist' do
it_behaves_like '400 response' do
- let(:refs) { %w(304d257dcb821665ab5110318fc58a007bd104ed missing) }
+ let(:refs) { %w[304d257dcb821665ab5110318fc58a007bd104ed missing] }
let(:current_user) { user }
let(:message) { 'Could not find ref: missing' }
end
@@ -801,7 +801,7 @@ RSpec.describe API::Repositories, feature_category: :source_code_management do
end
context 'when not enough refs are passed' do
- let(:refs) { %w(only-one) }
+ let(:refs) { %w[only-one] }
let(:current_user) { user }
it 'renders a bad request error' do
diff --git a/spec/requests/api/resource_access_tokens_spec.rb b/spec/requests/api/resource_access_tokens_spec.rb
index dcb6572d413..01e02651a64 100644
--- a/spec/requests/api/resource_access_tokens_spec.rb
+++ b/spec/requests/api/resource_access_tokens_spec.rb
@@ -477,6 +477,7 @@ RSpec.describe API::ResourceAccessTokens, feature_category: :system_access do
let_it_be(:token) { create(:personal_access_token, user: project_bot) }
let_it_be(:resource_id) { resource.id }
let_it_be(:token_id) { token.id }
+ let(:params) { {} }
let(:path) { "/#{source_type}s/#{resource_id}/access_tokens/#{token_id}/rotate" }
@@ -485,7 +486,7 @@ RSpec.describe API::ResourceAccessTokens, feature_category: :system_access do
resource.add_owner(user)
end
- subject(:rotate_token) { post api(path, user) }
+ subject(:rotate_token) { post(api(path, user), params: params) }
it "allows owner to rotate token", :freeze_time do
rotate_token
@@ -495,6 +496,19 @@ RSpec.describe API::ResourceAccessTokens, feature_category: :system_access do
expect(json_response['expires_at']).to eq((Date.today + 1.week).to_s)
end
+ context 'when expiry is defined' do
+ let(:expiry_date) { Date.today + 1.month }
+ let(:params) { { expires_at: expiry_date } }
+
+ it "allows owner to rotate token", :freeze_time do
+ rotate_token
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['token']).not_to eq(token.token)
+ expect(json_response['expires_at']).to eq(expiry_date.to_s)
+ end
+ end
+
context 'without permission' do
it 'returns an error message' do
another_user = create(:user)
diff --git a/spec/requests/api/search_spec.rb b/spec/requests/api/search_spec.rb
index 6a57cf52466..4733fdafbfb 100644
--- a/spec/requests/api/search_spec.rb
+++ b/spec/requests/api/search_spec.rb
@@ -140,14 +140,14 @@ RSpec.describe API::Search, :clean_gitlab_redis_rate_limiting, feature_category:
end
context 'when DB timeouts occur from global searches', :aggregate_failures do
- %w(
+ %w[
issues
merge_requests
milestones
projects
snippet_titles
users
- ).each do |scope|
+ ].each do |scope|
it "returns a 408 error if search with scope: #{scope} times out" do
allow(SearchService).to receive(:new).and_raise ActiveRecord::QueryCanceled
get api(endpoint, user), params: { scope: scope, search: 'awesome' }
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index 2fdcf710471..5656fda7684 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -79,7 +79,7 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu
expect(json_response['slack_app_secret']).to be_nil
expect(json_response['slack_app_signing_secret']).to be_nil
expect(json_response['slack_app_verification_token']).to be_nil
- expect(json_response['valid_runner_registrars']).to match_array(%w(project group))
+ expect(json_response['valid_runner_registrars']).to match_array(%w[project group])
expect(json_response['ci_max_includes']).to eq(150)
expect(json_response['allow_account_deletion']).to eq(true)
expect(json_response['gitlab_shell_operation_limit']).to eq(600)
@@ -261,7 +261,7 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu
expect(json_response['max_decompressed_archive_size']).to eq(20000)
expect(json_response['max_terraform_state_size_bytes']).to eq(1_000)
expect(json_response['disabled_oauth_sign_in_sources']).to eq([])
- expect(json_response['import_sources']).to match_array(%w(github bitbucket))
+ expect(json_response['import_sources']).to match_array(%w[github bitbucket])
expect(json_response['wiki_page_max_content_bytes']).to eq(12345)
expect(json_response['wiki_asciidoc_allow_uri_includes']).to be(true)
expect(json_response['personal_access_token_prefix']).to eq("GL-")
@@ -418,12 +418,12 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu
end
it 'does not allow unrestricted key lengths' do
- types = %w(dsa_key_restriction
+ types = %w[dsa_key_restriction
ecdsa_key_restriction
ecdsa_sk_key_restriction
ed25519_key_restriction
ed25519_sk_key_restriction
- rsa_key_restriction)
+ rsa_key_restriction]
types.each do |type|
put api("/application/settings", admin), params: { type => 0 }
@@ -519,7 +519,7 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu
context 'EKS integration settings' do
let(:attribute_names) { settings.keys.map(&:to_s) }
- let(:sensitive_attributes) { %w(eks_secret_access_key) }
+ let(:sensitive_attributes) { %w[eks_secret_access_key] }
let(:exposed_attributes) { attribute_names - sensitive_attributes }
let(:settings) do
diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb
index 604631bbf7f..6bbd43bfc14 100644
--- a/spec/requests/api/tags_spec.rb
+++ b/spec/requests/api/tags_spec.rb
@@ -206,7 +206,7 @@ RSpec.describe API::Tags, feature_category: :source_code_management do
expect(response).to match_response_schema('public_api/v4/tags')
expect(response.headers).to include('Link')
tag_names = json_response.map { |x| x['name'] }
- expect(tag_names).to match_array(%w(v1.1.0 v1.1.1))
+ expect(tag_names).to match_array(%w[v1.1.0 v1.1.1])
end
end
diff --git a/spec/requests/api/task_completion_status_spec.rb b/spec/requests/api/task_completion_status_spec.rb
index c46d6954da3..ae48b0c18cd 100644
--- a/spec/requests/api/task_completion_status_spec.rb
+++ b/spec/requests/api/task_completion_status_spec.rb
@@ -21,30 +21,30 @@ RSpec.describe 'task completion status response', features: :team_planning do
expected_completed_count: 0
},
{
- description: %{- [ ] task 1
- - [x] task 2 },
+ description: %(- [ ] task 1
+ - [x] task 2 ),
expected_count: 2,
expected_completed_count: 1
},
{
- description: %{- [ ] task 1
- - [ ] task 2 },
+ description: %(- [ ] task 1
+ - [ ] task 2 ),
expected_count: 2,
expected_completed_count: 0
},
{
- description: %{- [x] task 1
- - [x] task 2 },
+ description: %(- [x] task 1
+ - [x] task 2 ),
expected_count: 2,
expected_completed_count: 2
},
{
- description: %{- [ ] task 1},
+ description: %(- [ ] task 1),
expected_count: 1,
expected_completed_count: 0
},
{
- description: %{- [x] task 1},
+ description: %(- [x] task 1),
expected_count: 1,
expected_completed_count: 1
}
diff --git a/spec/requests/api/unleash_spec.rb b/spec/requests/api/unleash_spec.rb
index 75b26b98228..050be3ae8aa 100644
--- a/spec/requests/api/unleash_spec.rb
+++ b/spec/requests/api/unleash_spec.rb
@@ -96,7 +96,7 @@ RSpec.describe API::Unleash, feature_category: :feature_flags do
end
end
- %w(/feature_flags/unleash/:project_id/features /feature_flags/unleash/:project_id/client/features).each do |features_endpoint|
+ %w[/feature_flags/unleash/:project_id/features /feature_flags/unleash/:project_id/client/features].each do |features_endpoint|
describe "GET #{features_endpoint}", :use_clean_rails_redis_caching do
let(:features_url) { features_endpoint.sub(':project_id', project_id.to_s) }
let(:client) { create(:operations_feature_flags_client, project: project) }
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index 7da44266064..76fe72efc64 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe API::Users, :aggregate_failures, feature_category: :user_profile do
include WorkhorseHelpers
include KeysetPaginationHelpers
+ include CryptoHelpers
let_it_be(:admin) { create(:admin) }
let_it_be(:user, reload: true) { create(:user, username: 'user.withdot') }
@@ -226,6 +227,19 @@ RSpec.describe API::Users, :aggregate_failures, feature_category: :user_profile
end
end
+ context 'with search parameter' do
+ let_it_be(:first_user) { create(:user, username: 'a-user') }
+ let_it_be(:second_user) { create(:user, username: 'a-user2') }
+
+ it 'prioritizes username match' do
+ get api(path, user, admin_mode: true), params: { search: first_user.username }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response.first['username']).to eq('a-user')
+ expect(json_response.second['username']).to eq('a-user2')
+ end
+ end
+
context 'N+1 queries' do
before do
create_list(:user, 2)
@@ -1983,17 +1997,23 @@ RSpec.describe API::Users, :aggregate_failures, feature_category: :user_profile
end
describe "PUT /user/:id/credit_card_validation" do
- let(:credit_card_validated_time) { Time.utc(2020, 1, 1) }
+ let(:network) { 'American Express' }
+ let(:holder_name) { 'John Smith' }
+ let(:last_digits) { '1111' }
let(:expiration_year) { Date.today.year + 10 }
+ let(:expiration_month) { 1 }
+ let(:expiration_date) { Date.new(expiration_year, expiration_month, -1) }
+ let(:credit_card_validated_at) { Time.utc(2020, 1, 1) }
+
let(:path) { "/user/#{user.id}/credit_card_validation" }
let(:params) do
{
- credit_card_validated_at: credit_card_validated_time,
+ credit_card_validated_at: credit_card_validated_at,
credit_card_expiration_year: expiration_year,
- credit_card_expiration_month: 1,
- credit_card_holder_name: 'John Smith',
- credit_card_type: 'AmericanExpress',
- credit_card_mask_number: '1111'
+ credit_card_expiration_month: expiration_month,
+ credit_card_holder_name: holder_name,
+ credit_card_type: network,
+ credit_card_mask_number: last_digits
}
end
@@ -2023,11 +2043,11 @@ RSpec.describe API::Users, :aggregate_failures, feature_category: :user_profile
expect(response).to have_gitlab_http_status(:ok)
expect(user.credit_card_validation).to have_attributes(
- credit_card_validated_at: credit_card_validated_time,
- expiration_date: Date.new(expiration_year, 1, 31),
- last_digits: 1111,
- network: 'AmericanExpress',
- holder_name: 'John Smith'
+ credit_card_validated_at: credit_card_validated_at,
+ network_hash: sha256(network.downcase),
+ holder_name_hash: sha256(holder_name.downcase),
+ last_digits_hash: sha256(last_digits),
+ expiration_date_hash: sha256(expiration_date.to_s)
)
end
@@ -4519,7 +4539,7 @@ RSpec.describe API::Users, :aggregate_failures, feature_category: :user_profile
describe 'POST /users/:user_id/personal_access_tokens' do
let(:name) { 'new pat' }
let(:expires_at) { 3.days.from_now.to_date.to_s }
- let(:scopes) { %w(api read_user) }
+ let(:scopes) { %w[api read_user] }
let(:path) { "/users/#{user.id}/personal_access_tokens" }
let(:params) { { name: name, scopes: scopes, expires_at: expires_at } }
diff --git a/spec/requests/api/v3/github_spec.rb b/spec/requests/api/v3/github_spec.rb
deleted file mode 100644
index fbda291e901..00000000000
--- a/spec/requests/api/v3/github_spec.rb
+++ /dev/null
@@ -1,721 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe API::V3::Github, :aggregate_failures, feature_category: :integrations do
- let_it_be(:user) { create(:user) }
- let_it_be(:unauthorized_user) { create(:user) }
- let_it_be(:admin) { create(:user, :admin) }
- let_it_be_with_reload(:project) { create(:project, :repository, creator: user) }
-
- before do
- project.add_maintainer(user) if user
- end
-
- describe 'GET /orgs/:namespace/repos' do
- let_it_be(:group) { create(:group) }
-
- it_behaves_like 'a GitHub Enterprise Jira DVCS reversible end of life endpoint' do
- subject do
- jira_get v3_api("/orgs/#{group.path}/repos", user)
- end
- end
-
- it 'logs when the endpoint is hit and `jira_dvcs_end_of_life_amnesty` is enabled' do
- expect(Gitlab::JsonLogger).to receive(:info).with(
- including(
- namespace: group.path,
- user_id: user.id,
- message: 'Deprecated Jira DVCS endpoint request'
- )
- )
-
- jira_get v3_api("/orgs/#{group.path}/repos", user)
-
- stub_feature_flags(jira_dvcs_end_of_life_amnesty: false)
-
- expect(Gitlab::JsonLogger).not_to receive(:info)
-
- jira_get v3_api("/orgs/#{group.path}/repos", user)
- end
-
- it 'returns an empty array' do
- jira_get v3_api("/orgs/#{group.path}/repos", user)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to eq([])
- end
-
- it 'returns 200 when namespace path include a dot' do
- group = create(:group, path: 'foo.bar')
-
- jira_get v3_api("/orgs/#{group.path}/repos", user)
-
- expect(response).to have_gitlab_http_status(:ok)
- end
- end
-
- describe 'GET /user/repos' do
- it_behaves_like 'a GitHub Enterprise Jira DVCS reversible end of life endpoint' do
- subject { jira_get v3_api('/user/repos', user) }
- end
-
- it 'returns an empty array' do
- jira_get v3_api('/user/repos', user)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to eq([])
- end
- end
-
- shared_examples_for 'Jira-specific mimicked GitHub endpoints' do
- describe 'GET /.../issues/:id/comments' do
- let(:merge_request) do
- create(:merge_request, source_project: project, target_project: project)
- end
-
- let!(:note) do
- create(:note, project: project, noteable: merge_request)
- end
-
- context 'when user has access to the merge request' do
- it 'returns an array of notes' do
- jira_get v3_api("/repos/#{path}/issues/#{merge_request.id}/comments", user)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to be_an(Array)
- expect(json_response.size).to eq(1)
- end
- end
-
- context 'when user has no access to the merge request' do
- let(:project) { create(:project, :private) }
-
- before do
- project.add_guest(user)
- end
-
- it 'returns 404' do
- jira_get v3_api("/repos/#{path}/issues/#{merge_request.id}/comments", user)
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
- end
-
- describe 'GET /.../pulls/:id/commits' do
- it 'returns an empty array' do
- jira_get v3_api("/repos/#{path}/pulls/xpto/commits", user)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to eq([])
- end
- end
-
- describe 'GET /.../pulls/:id/comments' do
- it 'returns an empty array' do
- jira_get v3_api("/repos/#{path}/pulls/xpto/comments", user)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to eq([])
- end
- end
- end
-
- # Here we test that using /-/jira as namespace/project still works,
- # since that is how old Jira setups will talk to us
- context 'old /-/jira endpoints' do
- it_behaves_like 'Jira-specific mimicked GitHub endpoints' do
- let(:path) { '-/jira' }
- end
-
- it 'returns an empty Array for events' do
- jira_get v3_api('/repos/-/jira/events', user)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to eq([])
- end
- end
-
- context 'new :namespace/:project jira endpoints' do
- it_behaves_like 'Jira-specific mimicked GitHub endpoints' do
- let(:path) { "#{project.namespace.path}/#{project.path}" }
- end
-
- describe 'GET /users/:username' do
- let!(:user1) { create(:user, username: 'jane.porter') }
-
- it_behaves_like 'a GitHub Enterprise Jira DVCS reversible end of life endpoint' do
- subject { jira_get v3_api("/users/#{user.username}", user) }
- end
-
- context 'user exists' do
- it 'responds with the expected user' do
- jira_get v3_api("/users/#{user.username}", user)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('entities/github/user')
- end
- end
-
- context 'user does not exist' do
- it 'responds with the expected status' do
- jira_get v3_api('/users/unknown_user_name', user)
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
-
- context 'no rights to request user lists' do
- before do
- expect(Ability).to receive(:allowed?).with(unauthorized_user, :read_users_list, :global).and_return(false)
- expect(Ability).to receive(:allowed?).at_least(:once).and_call_original
- end
-
- it 'responds with forbidden' do
- jira_get v3_api("/users/#{user.username}", unauthorized_user)
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
- end
-
- describe 'GET events' do
- include ProjectForksHelper
-
- let(:group) { create(:group) }
- let(:project) { create(:project, :empty_repo, path: 'project.with.dot', group: group) }
- let(:events_path) { "/repos/#{group.path}/#{project.path}/events" }
-
- it_behaves_like 'a GitHub Enterprise Jira DVCS reversible end of life endpoint' do
- subject { jira_get v3_api(events_path, user) }
- end
-
- context 'if there are no merge requests' do
- it 'returns an empty array' do
- jira_get v3_api(events_path, user)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to eq([])
- end
- end
-
- context 'if there is a merge request' do
- let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, author: user) }
-
- it 'returns an event' do
- jira_get v3_api(events_path, user)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to be_an(Array)
- expect(json_response.size).to eq(1)
- end
- end
-
- it 'avoids N+1 queries' do
- create(:merge_request, source_project: project)
- source_project = fork_project(project, nil, repository: true)
-
- control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) { jira_get v3_api(events_path, user) }.count
-
- create_list(:merge_request, 2, :unique_branches, source_project: source_project, target_project: project)
-
- expect { jira_get v3_api(events_path, user) }.not_to exceed_all_query_limit(control_count)
- end
-
- context 'if there are more merge requests' do
- let!(:merge_request) { create(:merge_request, id: 10000, source_project: project, target_project: project, author: user) }
- let!(:merge_request2) { create(:merge_request, id: 10001, source_project: project, source_branch: generate(:branch), target_project: project, author: user) }
-
- it 'returns the expected amount of events' do
- jira_get v3_api(events_path, user)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to be_an(Array)
- expect(json_response.size).to eq(2)
- end
-
- it 'ensures each event has a unique id' do
- jira_get v3_api(events_path, user)
-
- ids = json_response.map { |event| event['id'] }.uniq
- expect(ids.size).to eq(2)
- end
- end
- end
- end
-
- describe 'repo pulls' do
- let_it_be(:project2) { create(:project, :repository, creator: user) }
- let_it_be(:assignee) { create(:user) }
- let_it_be(:assignee2) { create(:user) }
- let_it_be(:merge_request) do
- create(:merge_request, source_project: project, target_project: project, author: user, assignees: [assignee])
- end
-
- let_it_be(:merge_request_2) do
- create(:merge_request, source_project: project2, target_project: project2, author: user, assignees: [assignee, assignee2])
- end
-
- before do
- project2.add_maintainer(user)
- end
-
- def perform_request
- jira_get v3_api(route, user)
- end
-
- describe 'GET /-/jira/pulls' do
- let(:route) { '/repos/-/jira/pulls' }
-
- it_behaves_like 'a GitHub Enterprise Jira DVCS reversible end of life endpoint' do
- subject { perform_request }
- end
-
- it 'returns an array of merge requests with github format' do
- perform_request
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to be_an(Array)
- expect(json_response.size).to eq(2)
- expect(response).to match_response_schema('entities/github/pull_requests')
- end
-
- it 'returns multiple merge requests without N + 1' do
- perform_request
-
- control_count = ActiveRecord::QueryRecorder.new { perform_request }.count
-
- project3 = create(:project, :repository, creator: user)
- project3.add_maintainer(user)
- assignee3 = create(:user)
- create(:merge_request, source_project: project3, target_project: project3, author: user, assignees: [assignee3])
-
- expect { perform_request }.not_to exceed_query_limit(control_count)
- end
- end
-
- describe 'GET /repos/:namespace/:project/pulls' do
- let(:route) { "/repos/#{project.namespace.path}/#{project.path}/pulls" }
-
- it_behaves_like 'a GitHub Enterprise Jira DVCS reversible end of life endpoint' do
- subject { perform_request }
- end
-
- it 'returns an array of merge requests for the proper project in github format' do
- perform_request
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to be_an(Array)
- expect(json_response.size).to eq(1)
- expect(response).to match_response_schema('entities/github/pull_requests')
- end
-
- it 'returns multiple merge requests without N + 1' do
- perform_request
-
- control_count = ActiveRecord::QueryRecorder.new { perform_request }.count
-
- create(:merge_request, source_project: project, source_branch: 'fix')
-
- expect { perform_request }.not_to exceed_query_limit(control_count)
- end
- end
-
- describe 'GET /repos/:namespace/:project/pulls/:id' do
- it_behaves_like 'a GitHub Enterprise Jira DVCS reversible end of life endpoint' do
- subject { jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/pulls/#{merge_request.id}", user) }
- end
-
- context 'when user has access to the merge requests' do
- it 'returns the requested merge request in github format' do
- jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/pulls/#{merge_request.id}", user)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('entities/github/pull_request')
- end
- end
-
- context 'when user has no access to the merge request' do
- it 'returns 404' do
- project.add_guest(unauthorized_user)
-
- jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/pulls/#{merge_request.id}", unauthorized_user)
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
-
- context 'when instance admin' do
- it 'returns the requested merge request in github format' do
- jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/pulls/#{merge_request.id}", admin, admin_mode: true)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('entities/github/pull_request')
- end
- end
- end
- end
-
- describe 'GET /users/:namespace/repos' do
- let(:group) { create(:group, name: 'foo') }
-
- def expect_project_under_namespace(projects, namespace, user, admin_mode = false)
- jira_get v3_api("/users/#{namespace.path}/repos", user, admin_mode: admin_mode)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to include_pagination_headers
- expect(response).to match_response_schema('entities/github/repositories')
-
- projects.each do |project|
- hash = json_response.find do |hash|
- hash['name'] == ::Gitlab::Jira::Dvcs.encode_project_name(project)
- end
-
- raise "Project #{project.full_path} not present in response" if hash.nil?
-
- expect(hash['owner']['login']).to eq(namespace.path)
- end
- expect(json_response.size).to eq(projects.size)
- end
-
- it_behaves_like 'a GitHub Enterprise Jira DVCS reversible end of life endpoint' do
- subject { jira_get v3_api("/users/#{user.namespace.path}/repos", user) }
- end
-
- context 'group namespace' do
- let(:project) { create(:project, group: group) }
- let!(:project2) { create(:project, :public, group: group) }
-
- it 'returns an array of projects belonging to group excluding the ones user is not directly a member of, even when public' do
- expect_project_under_namespace([project], group, user)
- end
-
- context 'when instance admin' do
- let(:user) { create(:user, :admin) }
-
- it 'returns an array of projects belonging to group' do
- expect_project_under_namespace([project, project2], group, user, true)
- end
-
- context 'with a private group' do
- let(:group) { create(:group, :private) }
- let!(:project2) { create(:project, :private, group: group) }
-
- it 'returns an array of projects belonging to group' do
- expect_project_under_namespace([project, project2], group, user, true)
- end
- end
- end
- end
-
- context 'nested group namespace' do
- let(:group) { create(:group, :nested) }
- let!(:parent_group_project) { create(:project, group: group.parent, name: 'parent_group_project') }
- let!(:child_group_project) { create(:project, group: group, name: 'child_group_project') }
-
- before do
- group.parent.add_maintainer(user)
- end
-
- it 'returns an array of projects belonging to group with github format' do
- expect_project_under_namespace([parent_group_project, child_group_project], group.parent, user)
- end
-
- it 'avoids N+1 queries' do
- jira_get v3_api("/users/#{group.parent.path}/repos", user)
-
- control = ActiveRecord::QueryRecorder.new { jira_get v3_api("/users/#{group.parent.path}/repos", user) }
-
- new_group = create(:group, parent: group.parent)
- create(:project, :repository, group: new_group, creator: user)
-
- expect { jira_get v3_api("/users/#{group.parent.path}/repos", user) }.not_to exceed_query_limit(control)
- expect(response).to have_gitlab_http_status(:ok)
- end
- end
-
- context 'user namespace' do
- let(:project) { create(:project, namespace: user.namespace) }
-
- it 'returns an array of projects belonging to user namespace with github format' do
- expect_project_under_namespace([project], user.namespace, user)
- end
- end
-
- context 'namespace path includes a dot' do
- let(:project) { create(:project, group: group) }
- let(:group) { create(:group, name: 'foo.bar') }
-
- before do
- group.add_maintainer(user)
- end
-
- it 'returns an array of projects belonging to group with github format' do
- expect_project_under_namespace([project], group, user)
- end
- end
-
- context 'unauthenticated' do
- it 'returns 401' do
- jira_get v3_api('/users/foo/repos', nil)
-
- expect(response).to have_gitlab_http_status(:unauthorized)
- end
- end
-
- context 'namespace does not exist' do
- it 'responds with not found status' do
- jira_get v3_api('/users/noo/repos', user)
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
- end
-
- describe 'GET /repos/:namespace/:project/branches' do
- context 'authenticated' do
- it_behaves_like 'a GitHub Enterprise Jira DVCS reversible end of life endpoint' do
- subject { jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/branches", user) }
- end
-
- context 'updating project feature usage' do
- it 'counts Jira Cloud integration as enabled' do
- user_agent = 'Jira DVCS Connector Vertigo/4.42.0'
-
- freeze_time do
- jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/branches", user), user_agent
-
- expect(project.reload.jira_dvcs_cloud_last_sync_at).to be_like_time(Time.now)
- end
- end
-
- it 'counts Jira Server integration as enabled' do
- user_agent = 'Jira DVCS Connector/3.2.4'
-
- freeze_time do
- jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/branches", user), user_agent
-
- expect(project.reload.jira_dvcs_server_last_sync_at).to be_like_time(Time.now)
- end
- end
- end
-
- it 'returns an array of project branches with github format' do
- jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/branches", user)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an(Array)
-
- expect(response).to match_response_schema('entities/github/branches')
- end
-
- it 'returns 200 when project path include a dot' do
- project.update!(path: 'foo.bar')
-
- jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/branches", user)
-
- expect(response).to have_gitlab_http_status(:ok)
- end
-
- it 'returns 200 when namespace path include a dot' do
- group = create(:group, path: 'foo.bar')
- project = create(:project, :repository, group: group)
- project.add_reporter(user)
-
- jira_get v3_api("/repos/#{group.path}/#{project.path}/branches", user)
-
- expect(response).to have_gitlab_http_status(:ok)
- end
-
- context 'when the project has no repository' do
- let_it_be(:project) { create(:project, creator: user) }
-
- it 'returns an empty collection response' do
- jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/branches", user)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to be_empty
- end
- end
- end
-
- context 'unauthenticated' do
- it 'returns 401' do
- jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/branches", nil)
-
- expect(response).to have_gitlab_http_status(:unauthorized)
- end
- end
-
- context 'unauthorized' do
- it 'returns 404 when lower access level' do
- project.add_guest(unauthorized_user)
-
- jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/branches", unauthorized_user)
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
- end
-
- describe 'GET /repos/:namespace/:project/commits/:sha' do
- let(:commit) { project.repository.commit }
-
- def call_api(commit_id: commit.id)
- jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/commits/#{commit_id}", user)
- end
-
- def response_diff_files(response)
- Gitlab::Json.parse(response.body)['files']
- end
-
- context 'authenticated' do
- it_behaves_like 'a GitHub Enterprise Jira DVCS reversible end of life endpoint' do
- subject { call_api }
- end
-
- it 'returns commit with github format' do
- call_api
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('entities/github/commit')
- end
-
- it 'returns 200 when project path include a dot' do
- project.update!(path: 'foo.bar')
-
- call_api
-
- expect(response).to have_gitlab_http_status(:ok)
- end
-
- context 'when namespace path includes a dot' do
- let(:group) { create(:group, path: 'foo.bar') }
- let(:project) { create(:project, :repository, group: group) }
-
- it 'returns 200 when namespace path include a dot' do
- project.add_reporter(user)
-
- call_api
-
- expect(response).to have_gitlab_http_status(:ok)
- end
- end
-
- context 'when the Gitaly `CommitDiff` RPC times out', :use_clean_rails_memory_store_caching do
- let(:commit_diff_args) { [project.repository_storage, :diff_service, :commit_diff, any_args] }
-
- before do
- allow(Gitlab::GitalyClient).to receive(:call)
- .and_call_original
- end
-
- it 'handles the error, logs it, and returns empty diff files' do
- allow(Gitlab::GitalyClient).to receive(:call)
- .with(*commit_diff_args)
- .and_raise(GRPC::DeadlineExceeded)
-
- expect(Gitlab::ErrorTracking)
- .to receive(:track_exception)
- .with an_instance_of(GRPC::DeadlineExceeded)
-
- call_api
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response_diff_files(response)).to be_blank
- end
-
- it 'only calls Gitaly once for all attempts within a period of time' do
- expect(Gitlab::GitalyClient).to receive(:call)
- .with(*commit_diff_args)
- .once # <- once
- .and_raise(GRPC::DeadlineExceeded)
-
- 3.times do
- call_api
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response_diff_files(response)).to be_blank
- end
- end
-
- it 'calls Gitaly again after a period of time' do
- expect(Gitlab::GitalyClient).to receive(:call)
- .with(*commit_diff_args)
- .twice # <- twice
- .and_raise(GRPC::DeadlineExceeded)
-
- call_api
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response_diff_files(response)).to be_blank
-
- travel_to((described_class::GITALY_TIMEOUT_CACHE_EXPIRY + 1.second).from_now) do
- call_api
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response_diff_files(response)).to be_blank
- end
- end
-
- it 'uses a unique cache key, allowing other calls to succeed' do
- cache_key = [described_class::GITALY_TIMEOUT_CACHE_KEY, project.id, commit.cache_key].join(':')
- Rails.cache.write(cache_key, 1)
-
- expect(Gitlab::GitalyClient).to receive(:call)
- .with(*commit_diff_args)
- .once # <- once
-
- call_api
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response_diff_files(response)).to be_blank
-
- call_api(commit_id: commit.parent.id)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response_diff_files(response).length).to eq(1)
- end
- end
- end
-
- context 'unauthenticated' do
- let(:user) { nil }
-
- it 'returns 401' do
- call_api
-
- expect(response).to have_gitlab_http_status(:unauthorized)
- end
- end
-
- context 'unauthorized' do
- let(:user) { unauthorized_user }
-
- it 'returns 404 when lower access level' do
- project.add_guest(user)
-
- call_api
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
- end
-
- def jira_get(path, user_agent = 'Jira DVCS Connector/3.2.4')
- get path, headers: { 'User-Agent' => user_agent }
- end
-
- def v3_api(path, user = nil, personal_access_token: nil, oauth_access_token: nil, admin_mode: false)
- api(
- path,
- user,
- version: 'v3',
- personal_access_token: personal_access_token,
- oauth_access_token: oauth_access_token,
- admin_mode: admin_mode
- )
- end
-end
diff --git a/spec/requests/api/vs_code/settings/vs_code_settings_sync_spec.rb b/spec/requests/api/vs_code/settings/vs_code_settings_sync_spec.rb
index 1055a8efded..74d19f8533c 100644
--- a/spec/requests/api/vs_code/settings/vs_code_settings_sync_spec.rb
+++ b/spec/requests/api/vs_code/settings/vs_code_settings_sync_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe API::VsCode::Settings::VsCodeSettingsSync, :aggregate_failures, factory_default: :keep, feature_category: :web_ide do
+ include GrapePathHelpers::NamedRouteMatcher
+
let_it_be(:user) { create_default(:user) }
let_it_be(:user_token) { create(:personal_access_token) }
@@ -21,6 +23,14 @@ RSpec.describe API::VsCode::Settings::VsCodeSettingsSync, :aggregate_failures, f
end
end
+ shared_examples "returns 400" do
+ it 'returns 400' do
+ get api(path, personal_access_token: user_token)
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+
describe 'GET /vscode/settings_sync/v1/manifest' do
let(:path) { "/vscode/settings_sync/v1/manifest" }
@@ -80,6 +90,12 @@ RSpec.describe API::VsCode::Settings::VsCodeSettingsSync, :aggregate_failures, f
it_behaves_like "returns 20x when authenticated", :no_content
it_behaves_like "returns unauthorized when not authenticated"
+ context "when resource type is invalid" do
+ let(:path) { "/vscode/settings_sync/v1/resource/foo/1" }
+
+ it_behaves_like "returns 400"
+ end
+
context 'when settings with that type are not present' do
it 'returns 204 no content and no content ETag header' do
get api(path, personal_access_token: user_token)
@@ -102,6 +118,55 @@ RSpec.describe API::VsCode::Settings::VsCodeSettingsSync, :aggregate_failures, f
end
end
+ describe 'GET /vscode/settings_sync/v1/resource/:resource_name/' do
+ let(:path) { "/vscode/settings_sync/v1/resource/settings/" }
+
+ context "when resource type is invalid" do
+ let(:path) { "/vscode/settings_sync/v1/resource/foo" }
+
+ it_behaves_like "returns 400"
+ end
+
+ it_behaves_like "returns unauthorized when not authenticated"
+ it_behaves_like "returns 20x when authenticated", :ok
+
+ context 'when settings with that type are not present' do
+ it "returns empty array response" do
+ get api(path, personal_access_token: user_token)
+
+ expect(json_response.length).to eq(0)
+ end
+ end
+
+ context 'when settings with that type are present' do
+ let_it_be(:settings) { create(:vscode_setting, content: '{ "key": "value" }') }
+
+ it 'returns settings with the correct json content' do
+ get api(path, personal_access_token: user_token)
+
+ setting_type = settings[:setting_type]
+ uuid = settings[:uuid]
+
+ resource_ref = "/api/v4/vscode/settings_sync/v1/resource/#{setting_type}/#{uuid}"
+
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['url']).to eq(resource_ref)
+ expect(json_response.first['created']).to eq(settings.updated_at.to_i)
+ end
+ end
+
+ context 'when setting type is machine' do
+ let(:path) { "/vscode/settings_sync/v1/resource/machines/" }
+
+ it 'created field is nil' do
+ get api(path, personal_access_token: user_token)
+
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['created']).to be_nil
+ end
+ end
+ end
+
describe 'POST /vscode/settings_sync/v1/resource/:resource_name' do
let(:path) { "/vscode/settings_sync/v1/resource/settings" }
@@ -138,4 +203,28 @@ RSpec.describe API::VsCode::Settings::VsCodeSettingsSync, :aggregate_failures, f
expect(response).to have_gitlab_http_status(:bad_request)
end
end
+
+ describe 'DELETE /vscode/settings_sync/v1/collection' do
+ let(:path) { "/vscode/settings_sync/v1/collection" }
+
+ subject(:request) do
+ delete api(path, personal_access_token: user_token)
+ end
+
+ it 'returns unauthorized when not authenticated' do
+ delete api(path)
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+
+ context 'when user has one or more setting resources' do
+ before do
+ create(:vscode_setting, setting_type: 'globalState')
+ create(:vscode_setting, setting_type: 'extensions')
+ end
+
+ it 'deletes all user setting resources' do
+ expect { request }.to change { User.find(user.id).vscode_settings.count }.from(2).to(0)
+ end
+ end
+ end
end
diff --git a/spec/requests/api/wikis_spec.rb b/spec/requests/api/wikis_spec.rb
index 00e38a5bb7e..cd9c5637264 100644
--- a/spec/requests/api/wikis_spec.rb
+++ b/spec/requests/api/wikis_spec.rb
@@ -31,8 +31,8 @@ RSpec.describe API::Wikis, feature_category: :wiki do
let(:project_wiki) { create(:project_wiki, project: project, user: user) }
let(:payload) { { content: 'content', format: 'rdoc', title: 'title' } }
- let(:expected_keys_with_content) { %w(content format slug title encoding) }
- let(:expected_keys_without_content) { %w(format slug title) }
+ let(:expected_keys_with_content) { %w[content format slug title encoding front_matter] }
+ let(:expected_keys_without_content) { %w[format slug title] }
let(:wiki) { project_wiki }
shared_examples_for 'wiki API 404 Project Not Found' do
@@ -354,6 +354,18 @@ RSpec.describe API::Wikis, feature_category: :wiki do
end
include_examples 'wikis API creates wiki page'
+
+ context "with front matter title" do
+ let(:payload) { { title: 'title', front_matter: { "title" => "title in front matter" }, content: 'content' } }
+
+ it "save front matter" do
+ post(api(url, user), params: payload)
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response['front_matter']).to eq(payload[:front_matter])
+ expect(json_response['content']).to include(payload[:front_matter]["title"])
+ end
+ end
end
context 'when user is maintainer' do
@@ -478,6 +490,20 @@ RSpec.describe API::Wikis, feature_category: :wiki do
include_examples 'wiki API 404 Wiki Page Not Found'
end
+
+ context "with front matter title" do
+ let(:payload) do
+ { title: 'new title', front_matter: { "title" => "title in front matter" }, content: 'new content' }
+ end
+
+ it "save front matter" do
+ put(api(url, user), params: payload)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['front_matter']).to eq(payload[:front_matter])
+ expect(json_response['content']).to include(payload[:front_matter]["title"])
+ end
+ end
end
context 'when user is maintainer' do
diff --git a/spec/requests/application_controller_spec.rb b/spec/requests/application_controller_spec.rb
deleted file mode 100644
index 52fdf6bc69e..00000000000
--- a/spec/requests/application_controller_spec.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe ApplicationController, type: :request, feature_category: :shared do
- let_it_be(:user) { create(:user) }
-
- before do
- sign_in(user)
- end
-
- it_behaves_like 'Base action controller' do
- subject(:request) { get root_path }
- end
-end
diff --git a/spec/requests/chaos_controller_spec.rb b/spec/requests/chaos_controller_spec.rb
deleted file mode 100644
index d2ce618b041..00000000000
--- a/spec/requests/chaos_controller_spec.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe ChaosController, type: :request, feature_category: :tooling do
- it_behaves_like 'Base action controller' do
- before do
- # Stub leak_mem so we don't actually leak memory for the base action controller tests.
- allow(Gitlab::Chaos).to receive(:leak_mem).with(100, 30.seconds)
- end
-
- subject(:request) { get leakmem_chaos_path }
- end
-end
diff --git a/spec/requests/explore/catalog_controller_spec.rb b/spec/requests/explore/catalog_controller_spec.rb
new file mode 100644
index 00000000000..50a2240e040
--- /dev/null
+++ b/spec/requests/explore/catalog_controller_spec.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Explore::CatalogController, feature_category: :pipeline_composition do
+ let_it_be(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ end
+
+ shared_examples 'basic get requests' do |action|
+ let(:path) do
+ if action == :index
+ explore_catalog_index_path
+ else
+ explore_catalog_path(id: 1)
+ end
+ end
+
+ context 'with FF `global_ci_catalog`' do
+ before do
+ stub_feature_flags(global_ci_catalog: true)
+ end
+
+ it 'responds with 200' do
+ get path
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'without FF `global_ci_catalog`' do
+ before do
+ stub_feature_flags(global_ci_catalog: false)
+ end
+
+ it 'responds with 404' do
+ get path
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ describe 'GET #show' do
+ it_behaves_like 'basic get requests', :show
+ end
+
+ describe 'GET #index' do
+ it_behaves_like 'basic get requests', :index
+ end
+end
diff --git a/spec/requests/external_redirect/external_redirect_controller_spec.rb b/spec/requests/external_redirect/external_redirect_controller_spec.rb
new file mode 100644
index 00000000000..1b4294f5c4d
--- /dev/null
+++ b/spec/requests/external_redirect/external_redirect_controller_spec.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe "ExternalRedirect::ExternalRedirectController requests", feature_category: :navigation do
+ let_it_be(:external_url) { 'https://google.com' }
+ let_it_be(:external_url_encoded) do
+ Addressable::URI.encode_component(external_url, Addressable::URI::CharacterClasses::QUERY)
+ end
+
+ let_it_be(:internal_url) { "#{Gitlab.config.gitlab.url}/foo/bar" }
+ let_it_be(:internal_url_encoded) do
+ Addressable::URI.encode_component(internal_url, Addressable::URI::CharacterClasses::QUERY)
+ end
+
+ let_it_be(:top_nav_partial) { 'layouts/header/_default' }
+
+ context "with valid url param" do
+ before do
+ get "/-/external_redirect?url=#{external_url_encoded}"
+ end
+
+ it "returns 200 and renders URL" do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.body).to have_link(text: 'Proceed', href: external_url)
+ end
+
+ it "does not render nav" do
+ expect(response).not_to render_template(top_nav_partial)
+ end
+ end
+
+ context "with same origin url" do
+ before do
+ get "/-/external_redirect?url=#{internal_url_encoded}"
+ end
+
+ it "redirects" do
+ expect(response).to redirect_to(internal_url)
+ end
+ end
+
+ describe "with invalid url params" do
+ where(:case_name, :params) do
+ [
+ ["when url is bad", "url=javascript:alert(1)"],
+ ["when url is empty", "url="],
+ ["when url param is missing", ""]
+ ]
+ end
+
+ with_them do
+ it "returns 404" do
+ get "/-/external_redirect?#{params}"
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+end
diff --git a/spec/requests/health_controller_spec.rb b/spec/requests/health_controller_spec.rb
index 3ad1d8a75b4..639f6194af9 100644
--- a/spec/requests/health_controller_spec.rb
+++ b/spec/requests/health_controller_spec.rb
@@ -73,9 +73,7 @@ RSpec.describe HealthController, feature_category: :database do
end
describe 'GET /-/readiness' do
- subject(:request) { get readiness_path, params: params, headers: headers }
-
- it_behaves_like 'Base action controller'
+ subject { get '/-/readiness', params: params, headers: headers }
shared_context 'endpoint responding with readiness data' do
context 'when requesting instance-checks' do
diff --git a/spec/requests/jira_authorizations_spec.rb b/spec/requests/jira_authorizations_spec.rb
deleted file mode 100644
index 704db7fba08..00000000000
--- a/spec/requests/jira_authorizations_spec.rb
+++ /dev/null
@@ -1,88 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'Jira authorization requests', feature_category: :integrations do
- let(:user) { create :user }
- let(:application) { create :oauth_application, scopes: 'api' }
- let(:redirect_uri) { oauth_jira_dvcs_callback_url(host: "http://www.example.com") }
-
- def generate_access_grant
- create :oauth_access_grant, application: application, resource_owner_id: user.id, redirect_uri: redirect_uri
- end
-
- describe 'POST access_token' do
- let(:client_id) { application.uid }
- let(:client_secret) { application.secret }
-
- it 'returns values similar to a POST to /oauth/token' do
- post_data = {
- client_id: client_id,
- client_secret: client_secret
- }
-
- post '/oauth/token', params: post_data.merge({
- code: generate_access_grant.token,
- grant_type: 'authorization_code',
- redirect_uri: redirect_uri
- })
- oauth_response = json_response
- oauth_response_access_token, scope, token_type = oauth_response.values_at('access_token', 'scope', 'token_type')
-
- post '/login/oauth/access_token', params: post_data.merge({
- code: generate_access_grant.token
- })
- jira_response = response.body
- jira_response_access_token = Rack::Utils.parse_nested_query(jira_response)['access_token']
-
- expect(jira_response).to include("scope=#{scope}&token_type=#{token_type}")
- expect(oauth_response_access_token).not_to eql(jira_response_access_token)
- end
-
- it_behaves_like 'a GitHub Enterprise Jira DVCS reversible end of life endpoint' do
- subject do
- post '/login/oauth/access_token', params: {
- client_id: client_id,
- client_secret: client_secret,
- code: generate_access_grant.token
- }
- end
- end
-
- context 'when authorization fails' do
- before do
- post '/login/oauth/access_token', params: {
- client_id: client_id,
- client_secret: client_secret,
- code: try(:code) || generate_access_grant.token
- }
- end
-
- shared_examples 'an unauthorized request' do
- it 'returns 401' do
- expect(response).to have_gitlab_http_status(:unauthorized)
- end
- end
-
- context 'when client_id is invalid' do
- let(:client_id) { "invalid_id" }
-
- it_behaves_like 'an unauthorized request'
- end
-
- context 'when client_secret is invalid' do
- let(:client_secret) { "invalid_secret" }
-
- it_behaves_like 'an unauthorized request'
- end
-
- context 'when code is invalid' do
- let(:code) { "invalid_code" }
-
- it 'returns bad request' do
- expect(response).to have_gitlab_http_status(:bad_request)
- end
- end
- end
- end
-end
diff --git a/spec/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb
index 965bead4068..966cc2d6d4e 100644
--- a/spec/requests/jwt_controller_spec.rb
+++ b/spec/requests/jwt_controller_spec.rb
@@ -61,7 +61,7 @@ RSpec.describe JwtController, feature_category: :system_access do
end
end
- shared_examples 'container registry authenticator' do
+ context 'authenticating against container registry' do
context 'existing service' do
subject! { get '/jwt/auth', params: parameters }
@@ -124,7 +124,7 @@ RSpec.describe JwtController, feature_category: :system_access do
end
it 'does not log a user' do
- expect(log_data.keys).not_to include(%w(username user_id))
+ expect(log_data.keys).not_to include(%w[username user_id])
end
end
@@ -177,7 +177,7 @@ RSpec.describe JwtController, feature_category: :system_access do
end
let(:service_parameters) do
- ActionController::Parameters.new({ service: service_name, scopes: %w(scope1 scope2) }).permit!
+ ActionController::Parameters.new({ service: service_name, scopes: %w[scope1 scope2] }).permit!
end
it { expect(service_class).to have_received(:new).with(nil, user, service_parameters.merge(auth_type: :gitlab_or_ldap)) }
@@ -185,6 +185,21 @@ RSpec.describe JwtController, feature_category: :system_access do
it_behaves_like 'user logging'
end
+ context 'when passing a space-delimited list of scopes' do
+ let(:parameters) do
+ {
+ service: service_name,
+ scope: 'scope1 scope2'
+ }
+ end
+
+ let(:service_parameters) do
+ ActionController::Parameters.new({ service: service_name, scopes: %w[scope1 scope2] }).permit!
+ end
+
+ it { expect(service_class).to have_received(:new).with(nil, user, service_parameters.merge(auth_type: :gitlab_or_ldap)) }
+ end
+
context 'when user has 2FA enabled' do
let(:user) { create(:user, :two_factor) }
@@ -254,40 +269,6 @@ RSpec.describe JwtController, feature_category: :system_access do
end
end
- shared_examples 'parses a space-delimited list of scopes' do |output|
- let(:user) { create(:user) }
- let(:headers) { { authorization: credentials(user.username, user.password) } }
-
- subject! { get '/jwt/auth', params: parameters, headers: headers }
-
- let(:parameters) do
- {
- service: service_name,
- scope: 'scope1 scope2'
- }
- end
-
- let(:service_parameters) do
- ActionController::Parameters.new({ service: service_name, scopes: output }).permit!
- end
-
- it { expect(service_class).to have_received(:new).with(nil, user, service_parameters.merge(auth_type: :gitlab_or_ldap)) }
- end
-
- context 'authenticating against container registry' do
- it_behaves_like 'container registry authenticator'
- it_behaves_like 'parses a space-delimited list of scopes', %w(scope1 scope2)
-
- context 'when jwt_auth_space_delimited_scopes feature flag is disabled' do
- before do
- stub_feature_flags(jwt_auth_space_delimited_scopes: false)
- end
-
- it_behaves_like 'container registry authenticator'
- it_behaves_like 'parses a space-delimited list of scopes', ['scope1 scope2']
- end
- end
-
context 'authenticating against dependency proxy' do
let_it_be(:user) { create(:user) }
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb
index bc1ba3357a4..9bf77a0f6ca 100644
--- a/spec/requests/lfs_http_spec.rb
+++ b/spec/requests/lfs_http_spec.rb
@@ -664,8 +664,7 @@ RSpec.describe 'Git LFS API and storage', feature_category: :source_code_managem
context 'tries to push to other project' do
let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
- # I'm not sure what this tests that is different from the previous test
- it_behaves_like 'LFS http 403 response'
+ it_behaves_like 'LFS http 404 response'
end
end
@@ -1185,8 +1184,7 @@ RSpec.describe 'Git LFS API and storage', feature_category: :source_code_managem
context 'tries to push to other project' do
let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
- # I'm not sure what this tests that is different from the previous test
- it_behaves_like 'LFS http 403 response'
+ it_behaves_like 'LFS http 404 response'
end
end
diff --git a/spec/requests/lfs_locks_api_spec.rb b/spec/requests/lfs_locks_api_spec.rb
index 363a16f014b..a1a713308e0 100644
--- a/spec/requests/lfs_locks_api_spec.rb
+++ b/spec/requests/lfs_locks_api_spec.rb
@@ -67,7 +67,7 @@ RSpec.describe 'Git LFS File Locking API', feature_category: :source_code_manage
expect(response).to have_gitlab_http_status(:conflict)
- expect(json_response.keys).to match_array(%w(lock message documentation_url))
+ expect(json_response.keys).to match_array(%w[lock message documentation_url])
expect(json_response['message']).to match(/already locked/)
end
@@ -84,7 +84,7 @@ RSpec.describe 'Git LFS File Locking API', feature_category: :source_code_manage
expect(response).to have_gitlab_http_status(:created)
- expect(json_response['lock'].keys).to match_array(%w(id path locked_at owner))
+ expect(json_response['lock'].keys).to match_array(%w[id path locked_at owner])
end
end
end
@@ -103,7 +103,7 @@ RSpec.describe 'Git LFS File Locking API', feature_category: :source_code_manage
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['locks'].size).to eq(2)
- expect(json_response['locks'].first.keys).to match_array(%w(id path locked_at owner))
+ expect(json_response['locks'].first.keys).to match_array(%w[id path locked_at owner])
end
end
@@ -143,7 +143,7 @@ RSpec.describe 'Git LFS File Locking API', feature_category: :source_code_manage
it 'returns the deleted lock' do
post_lfs_json url, nil, headers
- expect(json_response['lock'].keys).to match_array(%w(id path locked_at owner))
+ expect(json_response['lock'].keys).to match_array(%w[id path locked_at owner])
end
context 'when a maintainer uses force' do
diff --git a/spec/requests/metrics_controller_spec.rb b/spec/requests/metrics_controller_spec.rb
deleted file mode 100644
index ce96906e020..00000000000
--- a/spec/requests/metrics_controller_spec.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe MetricsController, type: :request, feature_category: :metrics do
- it_behaves_like 'Base action controller' do
- subject(:request) { get metrics_path }
- end
-end
diff --git a/spec/requests/oauth/authorizations_controller_spec.rb b/spec/requests/oauth/authorizations_controller_spec.rb
index 7887bf52542..257f238d9ef 100644
--- a/spec/requests/oauth/authorizations_controller_spec.rb
+++ b/spec/requests/oauth/authorizations_controller_spec.rb
@@ -20,10 +20,6 @@ RSpec.describe Oauth::AuthorizationsController, feature_category: :system_access
end
describe 'GET #new' do
- it_behaves_like 'Base action controller' do
- subject(:request) { get oauth_authorization_path }
- end
-
context 'when application redirect URI has a custom scheme' do
context 'when CSP is disabled' do
before do
diff --git a/spec/requests/organizations/organizations_controller_spec.rb b/spec/requests/organizations/organizations_controller_spec.rb
index fdfeb367739..4bf527f49a8 100644
--- a/spec/requests/organizations/organizations_controller_spec.rb
+++ b/spec/requests/organizations/organizations_controller_spec.rb
@@ -75,6 +75,12 @@ RSpec.describe Organizations::OrganizationsController, feature_category: :cell d
it_behaves_like 'controller action that does not require authentication'
end
+ describe 'GET #users' do
+ subject(:gitlab_request) { get users_organization_path(organization) }
+
+ it_behaves_like 'controller action that does not require authentication'
+ end
+
describe 'GET #new' do
subject(:gitlab_request) { get new_organization_path }
diff --git a/spec/requests/profiles/comment_templates_controller_spec.rb b/spec/requests/profiles/comment_templates_controller_spec.rb
index cdbfbb0a346..d58fc3f19ea 100644
--- a/spec/requests/profiles/comment_templates_controller_spec.rb
+++ b/spec/requests/profiles/comment_templates_controller_spec.rb
@@ -10,26 +10,14 @@ RSpec.describe Profiles::CommentTemplatesController, feature_category: :user_pro
end
describe 'GET #index' do
- describe 'feature flag disabled' do
- before do
- stub_feature_flags(saved_replies: false)
-
- get '/-/profile/comment_templates'
- end
-
- it { expect(response).to have_gitlab_http_status(:not_found) }
+ before do
+ get '/-/profile/comment_templates'
end
- describe 'feature flag enabled' do
- before do
- get '/-/profile/comment_templates'
- end
-
- it { expect(response).to have_gitlab_http_status(:ok) }
+ it { expect(response).to have_gitlab_http_status(:ok) }
- it 'sets hide search settings ivar' do
- expect(assigns(:hide_search_settings)).to eq(true)
- end
+ it 'sets hide search settings ivar' do
+ expect(assigns(:hide_search_settings)).to eq(true)
end
end
end
diff --git a/spec/requests/projects/merge_requests_controller_spec.rb b/spec/requests/projects/merge_requests_controller_spec.rb
index 4af8f4fac7f..1033a51cd80 100644
--- a/spec/requests/projects/merge_requests_controller_spec.rb
+++ b/spec/requests/projects/merge_requests_controller_spec.rb
@@ -187,21 +187,6 @@ RSpec.describe Projects::MergeRequestsController, feature_category: :source_code
expect(response).to have_gitlab_http_status(:ok)
expect(Gitlab::Json.parse(response.body)['count']['all']).to eq(2)
end
-
- context 'when the FF ci_fix_performance_pipelines_json_endpoint is disabled' do
- before do
- stub_feature_flags(ci_fix_performance_pipelines_json_endpoint: false)
- end
-
- it 'returns the failed builds' do
- get pipelines_project_merge_request_path(project, merge_request, format: :json)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(Gitlab::Json.parse(response.body)['pipelines'].size).to eq(1)
- expect(Gitlab::Json.parse(response.body)['pipelines'][0]['failed_builds_count']).to eq(2)
- expect(Gitlab::Json.parse(response.body)['pipelines'][0]['failed_builds'].size).to eq(2)
- end
- end
end
private
diff --git a/spec/requests/projects/ml/model_versions_controller_spec.rb b/spec/requests/projects/ml/model_versions_controller_spec.rb
new file mode 100644
index 00000000000..bd9d798c275
--- /dev/null
+++ b/spec/requests/projects/ml/model_versions_controller_spec.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::Ml::ModelVersionsController, feature_category: :mlops do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:another_project) { create(:project) }
+ let_it_be(:user) { project.first_owner }
+ let_it_be(:model) { create(:ml_models, :with_versions, project: project) }
+ let_it_be(:version) { model.versions.first }
+
+ let(:model_registry_enabled) { true }
+
+ before do
+ allow(Ability).to receive(:allowed?).and_call_original
+ allow(Ability).to receive(:allowed?)
+ .with(user, :read_model_registry, project)
+ .and_return(model_registry_enabled)
+
+ sign_in(user)
+ end
+
+ describe 'show' do
+ let(:model_id) { model.id }
+ let(:version_id) { version.id }
+ let(:request_project) { model.project }
+
+ subject(:show_request) do
+ show_model_version
+ response
+ end
+
+ before do
+ show_request
+ end
+
+ it 'renders the template' do
+ is_expected.to render_template('projects/ml/model_versions/show')
+ end
+
+ it 'fetches the correct model_version' do
+ show_request
+
+ expect(assigns(:model)).to eq(model)
+ expect(assigns(:model_version)).to eq(version)
+ end
+
+ context 'when version id does not exist' do
+ let(:version_id) { non_existing_record_id }
+
+ it { is_expected.to have_gitlab_http_status(:not_found) }
+ end
+
+ context 'when version and model id are correct but project is not' do
+ let(:request_project) { another_project }
+
+ it { is_expected.to have_gitlab_http_status(:not_found) }
+ end
+
+ context 'when user does not have access' do
+ let(:model_registry_enabled) { false }
+
+ it { is_expected.to have_gitlab_http_status(:not_found) }
+ end
+ end
+
+ private
+
+ def show_model_version
+ get project_ml_model_version_path(request_project, model_id, version_id)
+ end
+end
diff --git a/spec/requests/projects/ml/models_controller_spec.rb b/spec/requests/projects/ml/models_controller_spec.rb
index b4402ad9a27..cda3f777a72 100644
--- a/spec/requests/projects/ml/models_controller_spec.rb
+++ b/spec/requests/projects/ml/models_controller_spec.rb
@@ -10,14 +10,19 @@ RSpec.describe Projects::Ml::ModelsController, feature_category: :mlops do
let_it_be(:model3) { create(:ml_models, project: project) }
let_it_be(:model_in_different_project) { create(:ml_models) }
- let(:model_registry_enabled) { true }
+ let(:read_model_registry) { true }
+ let(:write_model_registry) { true }
+
let(:params) { {} }
before do
allow(Ability).to receive(:allowed?).and_call_original
allow(Ability).to receive(:allowed?)
.with(user, :read_model_registry, project)
- .and_return(model_registry_enabled)
+ .and_return(read_model_registry)
+ allow(Ability).to receive(:allowed?)
+ .with(user, :write_model_registry, project)
+ .and_return(write_model_registry)
sign_in(user)
end
@@ -33,34 +38,59 @@ RSpec.describe Projects::Ml::ModelsController, feature_category: :mlops do
end
it 'fetches the models using the finder' do
- expect(::Projects::Ml::ModelFinder).to receive(:new).with(project).and_call_original
+ expect(::Projects::Ml::ModelFinder).to receive(:new).with(project, {}).and_call_original
index_request
end
- it 'fetches the correct models' do
+ it 'fetches the correct variables', :aggregate_failures do
+ stub_const("Projects::Ml::ModelsController::MAX_MODELS_PER_PAGE", 2)
+
index_request
- expect(assigns(:paginator).records).to match_array([model3, model2, model1])
+ page_models = [model3, model2]
+ all_models = [model3, model2, model1]
+
+ expect(assigns(:paginator).records).to match_array(page_models)
+ expect(assigns(:model_count)).to be all_models.count
end
it 'does not perform N+1 sql queries' do
+ list_models
+
control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) { list_models }
create_list(:ml_model_versions, 2, model: model1)
create_list(:ml_model_versions, 2, model: model2)
+ create_list(:ml_models, 4, project: project)
expect { list_models }.not_to exceed_all_query_limit(control_count)
end
context 'when user does not have access' do
- let(:model_registry_enabled) { false }
+ let(:read_model_registry) { false }
it 'renders 404' do
is_expected.to have_gitlab_http_status(:not_found)
end
end
+ context 'with search params' do
+ let(:params) { { name: 'some_name', order_by: 'name', sort: 'asc' } }
+
+ it 'passes down params to the finder' do
+ expect(Projects::Ml::ModelFinder).to receive(:new).and_call_original do |_exp, params|
+ expect(params.to_h).to include({
+ name: 'some_name',
+ order_by: 'name',
+ sort: 'asc'
+ })
+ end
+
+ index_request
+ end
+ end
+
describe 'pagination' do
before do
stub_const("Projects::Ml::ModelsController::MAX_MODELS_PER_PAGE", 2)
@@ -116,7 +146,40 @@ RSpec.describe Projects::Ml::ModelsController, feature_category: :mlops do
end
context 'when user does not have access' do
- let(:model_registry_enabled) { false }
+ let(:read_model_registry) { false }
+
+ it { is_expected.to have_gitlab_http_status(:not_found) }
+ end
+ end
+
+ describe 'destroy' do
+ let(:model_for_deletion) do
+ create(:ml_models, project: project)
+ end
+
+ let(:model_id) { model_for_deletion.id }
+
+ subject(:delete_request) do
+ delete_model
+ response
+ end
+
+ it 'deletes the model', :aggregate_failures do
+ is_expected.to have_gitlab_http_status(:found)
+
+ expect(flash[:notice]).to eq('Model removed')
+ expect(response).to redirect_to("/#{project.full_path}/-/ml/models")
+ expect { Ml::Model.find(id: model_id) }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+
+ context 'when model does not exist' do
+ let(:model_id) { non_existing_record_id }
+
+ it { is_expected.to have_gitlab_http_status(:not_found) }
+ end
+
+ describe 'when user does not have write_model_registry rights' do
+ let(:write_model_registry) { false }
it { is_expected.to have_gitlab_http_status(:not_found) }
end
@@ -131,4 +194,8 @@ RSpec.describe Projects::Ml::ModelsController, feature_category: :mlops do
def show_model
get project_ml_model_path(request_project, model_id)
end
+
+ def delete_model
+ delete project_ml_model_path(project, model_id)
+ end
end
diff --git a/spec/requests/projects/service_desk_controller_spec.rb b/spec/requests/projects/service_desk_controller_spec.rb
index 05e48c2c5c7..7d881d8ea62 100644
--- a/spec/requests/projects/service_desk_controller_spec.rb
+++ b/spec/requests/projects/service_desk_controller_spec.rb
@@ -88,6 +88,16 @@ RSpec.describe Projects::ServiceDeskController, feature_category: :service_desk
expect(json_response['issue_template_key']).to eq('service_desk')
end
+ it 'sets add_external_participants_from_cc' do
+ put project_service_desk_path(project, format: :json), params: { add_external_participants_from_cc: true }
+ project.reset
+
+ settings = project.service_desk_setting
+ expect(settings).to be_present
+ expect(settings.add_external_participants_from_cc).to eq(true)
+ expect(json_response['add_external_participants_from_cc']).to eq(true)
+ end
+
it 'returns an error when update of service desk settings fails' do
put project_service_desk_path(project, format: :json), params: { issue_template_key: 'invalid key' }
diff --git a/spec/requests/registrations_controller_spec.rb b/spec/requests/registrations_controller_spec.rb
index 71f2f347f0d..8b857046a4d 100644
--- a/spec/requests/registrations_controller_spec.rb
+++ b/spec/requests/registrations_controller_spec.rb
@@ -6,9 +6,7 @@ RSpec.describe RegistrationsController, type: :request, feature_category: :syste
describe 'POST #create' do
let_it_be(:user_attrs) { build_stubbed(:user).slice(:first_name, :last_name, :username, :email, :password) }
- subject(:request) { post user_registration_path, params: { user: user_attrs } }
-
- it_behaves_like 'Base action controller'
+ subject(:create_user) { post user_registration_path, params: { user: user_attrs } }
context 'when email confirmation is required' do
before do
@@ -17,7 +15,7 @@ RSpec.describe RegistrationsController, type: :request, feature_category: :syste
end
it 'redirects to the `users_almost_there_path`', unless: Gitlab.ee? do
- request
+ create_user
expect(response).to redirect_to(users_almost_there_path(email: user_attrs[:email]))
end
diff --git a/spec/requests/search_controller_spec.rb b/spec/requests/search_controller_spec.rb
index 37474aee1ee..365b20ad4aa 100644
--- a/spec/requests/search_controller_spec.rb
+++ b/spec/requests/search_controller_spec.rb
@@ -9,7 +9,6 @@ RSpec.describe SearchController, type: :request, feature_category: :global_searc
let_it_be(:projects) { create_list(:project, 5, :public, :repository, :wiki_repo) }
before do
- stub_feature_flags(super_sidebar_nav_enrolled: false)
login_as(user)
end
diff --git a/spec/requests/sessions_spec.rb b/spec/requests/sessions_spec.rb
index 1a925969c5a..3428e607305 100644
--- a/spec/requests/sessions_spec.rb
+++ b/spec/requests/sessions_spec.rb
@@ -7,10 +7,6 @@ RSpec.describe 'Sessions', feature_category: :system_access do
let(:user) { create(:user) }
- it_behaves_like 'Base action controller' do
- subject(:request) { get new_user_session_path }
- end
-
context 'for authentication', :allow_forgery_protection do
it 'logout does not require a csrf token' do
login_as(user)
diff --git a/spec/requests/users_controller_spec.rb b/spec/requests/users_controller_spec.rb
index d4e7dc1542a..da111831c15 100644
--- a/spec/requests/users_controller_spec.rb
+++ b/spec/requests/users_controller_spec.rb
@@ -600,7 +600,7 @@ RSpec.describe UsersController, feature_category: :user_management do
end
end
- %i(html json).each do |format|
+ %i[html json].each do |format|
context "with format: #{format}" do
let(:format) { format }
@@ -656,7 +656,7 @@ RSpec.describe UsersController, feature_category: :user_management do
end
end
- %i(html json).each do |format|
+ %i[html json].each do |format|
context "with format: #{format}" do
let(:format) { format }
diff --git a/spec/routing/organizations/organizations_controller_routing_spec.rb b/spec/routing/organizations/organizations_controller_routing_spec.rb
index 187553df2a1..f105bb31ccf 100644
--- a/spec/routing/organizations/organizations_controller_routing_spec.rb
+++ b/spec/routing/organizations/organizations_controller_routing_spec.rb
@@ -24,4 +24,9 @@ RSpec.describe Organizations::OrganizationsController, :routing, feature_categor
expect(get("/-/organizations/#{organization.path}/groups_and_projects"))
.to route_to('organizations/organizations#groups_and_projects', organization_path: organization.path)
end
+
+ it 'routes to #users' do
+ expect(get("/-/organizations/#{organization.path}/users"))
+ .to route_to('organizations/organizations#users', organization_path: organization.path)
+ end
end
diff --git a/spec/routing/uploads_routing_spec.rb b/spec/routing/uploads_routing_spec.rb
index 9eb421ec7d0..198cda59357 100644
--- a/spec/routing/uploads_routing_spec.rb
+++ b/spec/routing/uploads_routing_spec.rb
@@ -33,7 +33,7 @@ RSpec.describe 'Uploads', 'routing' do
end
it 'does not allow creating uploads for other models' do
- unroutable_models = UploadsController::MODEL_CLASSES.keys.compact - %w(personal_snippet user)
+ unroutable_models = UploadsController::MODEL_CLASSES.keys.compact - %w[personal_snippet user]
unroutable_models.each do |model|
expect(post("/uploads/#{model}?id=1")).not_to be_routable
diff --git a/spec/rubocop/batched_background_migrations_dictionary_spec.rb b/spec/rubocop/batched_background_migrations_dictionary_spec.rb
new file mode 100644
index 00000000000..57ef929fd90
--- /dev/null
+++ b/spec/rubocop/batched_background_migrations_dictionary_spec.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+require 'rubocop_spec_helper'
+
+require_relative '../../rubocop/batched_background_migrations_dictionary'
+
+RSpec.describe RuboCop::BatchedBackgroundMigrationsDictionary, feature_category: :database do
+ let(:bbm_dictionary_file_name) { "#{described_class::DICTIONARY_BASE_DIR}/test_migration.yml" }
+ let(:migration_version) { 20230307160250 }
+ let(:finalized_by_version) { 20230307160255 }
+ let(:introduced_by_url) { 'https://test_url' }
+ let(:finalize_after) { '202312011212' }
+
+ let(:bbm_dictionary_data) do
+ {
+ migration_job_name: 'TestMigration',
+ feature_category: :database,
+ introduced_by_url: introduced_by_url,
+ milestone: 16.5,
+ queued_migration_version: migration_version,
+ finalized_by: finalized_by_version,
+ finalize_after: finalize_after
+ }
+ end
+
+ before do
+ File.open(bbm_dictionary_file_name, 'w') do |file|
+ file.write(bbm_dictionary_data.stringify_keys.to_yaml)
+ end
+ end
+
+ after do
+ FileUtils.rm(bbm_dictionary_file_name)
+ end
+
+ subject(:batched_background_migration) { described_class.new(migration_version) }
+
+ describe '#finalized_by' do
+ it 'returns the finalized_by version of the bbm with given version' do
+ expect(batched_background_migration.finalized_by).to eq(finalized_by_version.to_s)
+ end
+
+ it 'returns nothing for non-existing bbm dictionary' do
+ expect(described_class.new('random').finalized_by).to be_nil
+ end
+ end
+
+ describe '#introduced_by_url' do
+ it 'returns the introduced_by_url of the bbm with given version' do
+ expect(batched_background_migration.introduced_by_url).to eq(introduced_by_url)
+ end
+
+ it 'returns nothing for non-existing bbm dictionary' do
+ expect(described_class.new('random').introduced_by_url).to be_nil
+ end
+ end
+
+ describe '#finalize_after' do
+ it 'returns the finalize_after timestamp of the bbm with given version' do
+ expect(batched_background_migration.finalize_after).to eq(finalize_after)
+ end
+
+ it 'returns nothing for non-existing bbm dictionary' do
+ expect(described_class.new('random').finalize_after).to be_nil
+ end
+ end
+end
diff --git a/spec/rubocop/batched_background_migrations_spec.rb b/spec/rubocop/batched_background_migrations_spec.rb
deleted file mode 100644
index a9b99bb466b..00000000000
--- a/spec/rubocop/batched_background_migrations_spec.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-# frozen_string_literal: true
-
-require 'rubocop_spec_helper'
-
-require_relative '../../rubocop/batched_background_migrations'
-
-RSpec.describe RuboCop::BatchedBackgroundMigrations, feature_category: :database do
- let(:bbm_dictionary_file_name) { "#{described_class::DICTIONARY_BASE_DIR}/test_migration.yml" }
- let(:migration_version) { 20230307160250 }
- let(:finalized_by_version) { 20230307160255 }
- let(:bbm_dictionary_data) do
- {
- migration_job_name: 'TestMigration',
- feature_category: :database,
- introduced_by_url: 'https://test_url',
- milestone: 16.5,
- queued_migration_version: migration_version,
- finalized_by: finalized_by_version
- }
- end
-
- before do
- File.open(bbm_dictionary_file_name, 'w') do |file|
- file.write(bbm_dictionary_data.stringify_keys.to_yaml)
- end
- end
-
- after do
- FileUtils.rm(bbm_dictionary_file_name)
- end
-
- subject(:batched_background_migration) { described_class.new(migration_version) }
-
- describe '#finalized_by' do
- it 'returns the finalized_by version of the bbm with given version' do
- expect(batched_background_migration.finalized_by).to eq(finalized_by_version.to_s)
- end
-
- it 'returns nothing for non-existing bbm dictionary' do
- expect(described_class.new('random').finalized_by).to be_nil
- end
- end
-end
diff --git a/spec/rubocop/cop/background_migration/dictionary_file_spec.rb b/spec/rubocop/cop/background_migration/dictionary_file_spec.rb
new file mode 100644
index 00000000000..7becf9c09a4
--- /dev/null
+++ b/spec/rubocop/cop/background_migration/dictionary_file_spec.rb
@@ -0,0 +1,182 @@
+# frozen_string_literal: true
+
+require 'rubocop_spec_helper'
+require_relative '../../../../rubocop/cop/background_migration/dictionary_file'
+
+RSpec.describe RuboCop::Cop::BackgroundMigration::DictionaryFile, feature_category: :database do
+ let(:config) do
+ RuboCop::Config.new(
+ 'BackgroundMigration/DictionaryFile' => {
+ 'EnforcedSince' => 20231018100907
+ }
+ )
+ end
+
+ shared_examples 'migration with missing dictionary keys offense' do |missing_key|
+ it 'registers an offense' do
+ expect_offense(<<~RUBY)
+ class QueueMyMigration < Gitlab::Database::Migration[2.1]
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{format(described_class::MSG[:missing_key], key: missing_key)}
+ MIGRATION = 'MyMigration'
+
+ def up
+ queue_batched_background_migration(
+ MIGRATION,
+ :users,
+ :id
+ )
+ end
+ end
+ RUBY
+ end
+ end
+
+ context 'for non post migrations' do
+ before do
+ allow(cop).to receive(:in_post_deployment_migration?).and_return(false)
+ end
+
+ it 'does not throw any offense' do
+ expect_no_offenses(<<~RUBY)
+ class QueueMyMigration < Gitlab::Database::Migration[2.1]
+ MIGRATION = 'MyMigration'
+
+ def up
+ queue_batched_background_migration(
+ MIGRATION,
+ :users,
+ :id
+ )
+ end
+ end
+ RUBY
+ end
+ end
+
+ context 'for post migrations' do
+ before do
+ allow(cop).to receive(:in_post_deployment_migration?).and_return(true)
+ end
+
+ context 'without enqueuing batched migrations' do
+ it 'does not throw any offense' do
+ expect_no_offenses(<<~RUBY)
+ class CreateTestTable < Gitlab::Database::Migration[2.1]
+ def change
+ create_table(:tests)
+ end
+ end
+ RUBY
+ end
+ end
+
+ context 'with enqueuing batched migration' do
+ let(:rails_root) { File.expand_path('../../../..', __dir__) }
+ let(:dictionary_file_path) { File.join(rails_root, 'db/docs/batched_background_migrations/my_migration.yml') }
+
+ context 'for migrations before enforced time' do
+ before do
+ allow(cop).to receive(:version).and_return(20230918100907)
+ end
+
+ it 'does not throw any offenses' do
+ expect_no_offenses(<<~RUBY)
+ class QueueMyMigration < Gitlab::Database::Migration[2.1]
+ MIGRATION = 'MyMigration'
+
+ def up
+ queue_batched_background_migration(
+ MIGRATION,
+ :users,
+ :id
+ )
+ end
+ end
+ RUBY
+ end
+ end
+
+ context 'for migrations after enforced time' do
+ before do
+ allow(cop).to receive(:version).and_return(20231118100907)
+ end
+
+ it 'throws offense on not having the appropriate dictionary file with migration name as a constant' do
+ expect_offense(<<~RUBY)
+ class QueueMyMigration < Gitlab::Database::Migration[2.1]
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{format("Missing %{file_name}. Use the generator 'batched_background_migration' to create dictionary files automatically. For more details refer: https://docs.gitlab.com/ee/development/database/batched_background_migrations.html#generator", file_name: dictionary_file_path)}
+ MIGRATION = 'MyMigration'
+
+ def up
+ queue_batched_background_migration(
+ MIGRATION,
+ :users,
+ :id
+ )
+ end
+ end
+ RUBY
+ end
+
+ it 'throws offense on not having the appropriate dictionary file with migration name as a variable' do
+ expect_offense(<<~RUBY)
+ class QueueMyMigration < Gitlab::Database::Migration[2.1]
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{format("Missing %{file_name}. Use the generator 'batched_background_migration' to create dictionary files automatically. For more details refer: https://docs.gitlab.com/ee/development/database/batched_background_migrations.html#generator", file_name: dictionary_file_path)}
+ def up
+ queue_batched_background_migration(
+ 'MyMigration',
+ :users,
+ :id
+ )
+ end
+ end
+ RUBY
+ end
+
+ context 'with dictionary file' do
+ let(:introduced_by_url) { 'https://test_url' }
+ let(:finalize_after) { '20230507160251' }
+
+ before do
+ allow(File).to receive(:exist?).with(dictionary_file_path).and_return(true)
+
+ allow_next_instance_of(RuboCop::BatchedBackgroundMigrationsDictionary) do |dictionary|
+ allow(dictionary).to receive(:finalize_after).and_return(finalize_after)
+ allow(dictionary).to receive(:introduced_by_url).and_return(introduced_by_url)
+ end
+ end
+
+ context 'without introduced_by_url' do
+ it_behaves_like 'migration with missing dictionary keys offense', :introduced_by_url do
+ let(:introduced_by_url) { nil }
+ end
+ end
+
+ context 'without finalize_after' do
+ it_behaves_like 'migration with missing dictionary keys offense', :finalize_after do
+ let(:finalize_after) { nil }
+ end
+ end
+
+ context 'with required dictionary keys' do
+ it 'does not throw offense with appropriate dictionary file' do
+ expect_no_offenses(<<~RUBY)
+ class QueueMyMigration < Gitlab::Database::Migration[2.1]
+ MIGRATION = 'MyMigration'
+
+ def up
+ queue_batched_background_migration(
+ MIGRATION,
+ :users,
+ :id
+ )
+ end
+ end
+ RUBY
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/rubocop/cop/background_migration/missing_dictionary_file_spec.rb b/spec/rubocop/cop/background_migration/missing_dictionary_file_spec.rb
deleted file mode 100644
index 32b958426b9..00000000000
--- a/spec/rubocop/cop/background_migration/missing_dictionary_file_spec.rb
+++ /dev/null
@@ -1,137 +0,0 @@
-# frozen_string_literal: true
-
-require 'rubocop_spec_helper'
-require_relative '../../../../rubocop/cop/background_migration/missing_dictionary_file'
-
-RSpec.describe RuboCop::Cop::BackgroundMigration::MissingDictionaryFile, feature_category: :database do
- let(:config) do
- RuboCop::Config.new(
- 'BackgroundMigration/MissingDictionaryFile' => {
- 'EnforcedSince' => 20230307160251
- }
- )
- end
-
- context 'for non post migrations' do
- before do
- allow(cop).to receive(:in_post_deployment_migration?).and_return(false)
- end
-
- it 'does not throw any offense' do
- expect_no_offenses(<<~RUBY)
- class QueueMyMigration < Gitlab::Database::Migration[2.1]
- MIGRATION = 'MyMigration'
-
- def up
- queue_batched_background_migration(
- MIGRATION,
- :users,
- :id
- )
- end
- end
- RUBY
- end
- end
-
- context 'for post migrations' do
- before do
- allow(cop).to receive(:in_post_deployment_migration?).and_return(true)
- end
-
- context 'without enqueuing batched migrations' do
- it 'does not throw any offense' do
- expect_no_offenses(<<~RUBY)
- class CreateTestTable < Gitlab::Database::Migration[2.1]
- def change
- create_table(:tests)
- end
- end
- RUBY
- end
- end
-
- context 'with enqueuing batched migration' do
- let(:rails_root) { File.expand_path('../../../..', __dir__) }
- let(:dictionary_file_path) { File.join(rails_root, 'db/docs/batched_background_migrations/my_migration.yml') }
-
- context 'for migrations before enforced time' do
- before do
- allow(cop).to receive(:version).and_return(20230307160250)
- end
-
- it 'does not throw any offenses' do
- expect_no_offenses(<<~RUBY)
- class QueueMyMigration < Gitlab::Database::Migration[2.1]
- MIGRATION = 'MyMigration'
-
- def up
- queue_batched_background_migration(
- MIGRATION,
- :users,
- :id
- )
- end
- end
- RUBY
- end
- end
-
- context 'for migrations after enforced time' do
- before do
- allow(cop).to receive(:version).and_return(20230307160252)
- end
-
- it 'throws offense on not having the appropriate dictionary file with migration name as a constant' do
- expect_offense(<<~RUBY)
- class QueueMyMigration < Gitlab::Database::Migration[2.1]
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{format("Missing %{file_name}. Use the generator 'batched_background_migration' to create dictionary files automatically. For more details refer: https://docs.gitlab.com/ee/development/database/batched_background_migrations.html#generator", file_name: dictionary_file_path)}
- MIGRATION = 'MyMigration'
-
- def up
- queue_batched_background_migration(
- MIGRATION,
- :users,
- :id
- )
- end
- end
- RUBY
- end
-
- it 'throws offense on not having the appropriate dictionary file with migration name as a variable' do
- expect_offense(<<~RUBY)
- class QueueMyMigration < Gitlab::Database::Migration[2.1]
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{format("Missing %{file_name}. Use the generator 'batched_background_migration' to create dictionary files automatically. For more details refer: https://docs.gitlab.com/ee/development/database/batched_background_migrations.html#generator", file_name: dictionary_file_path)}
- def up
- queue_batched_background_migration(
- 'MyMigration',
- :users,
- :id
- )
- end
- end
- RUBY
- end
-
- it 'does not throw offense with appropriate dictionary file' do
- expect(File).to receive(:exist?).with(dictionary_file_path).and_return(true)
-
- expect_no_offenses(<<~RUBY)
- class QueueMyMigration < Gitlab::Database::Migration[2.1]
- MIGRATION = 'MyMigration'
-
- def up
- queue_batched_background_migration(
- MIGRATION,
- :users,
- :id
- )
- end
- end
- RUBY
- end
- end
- end
- end
-end
diff --git a/spec/rubocop/cop/gitlab/doc_url_spec.rb b/spec/rubocop/cop/gitlab/doc_url_spec.rb
index 957edc8286b..fa055f9b2fe 100644
--- a/spec/rubocop/cop/gitlab/doc_url_spec.rb
+++ b/spec/rubocop/cop/gitlab/doc_url_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe RuboCop::Cop::Gitlab::DocUrl, feature_category: :shared do
it 'registers an offense' do
expect_offense(<<~RUBY)
'See [the docs](https://docs.gitlab.com/ee/user/permissions#roles).'
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `#help_page_url` instead of directly including link. See https://docs.gitlab.com/ee/development/documentation/#linking-to-help-in-ruby.
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `#help_page_url` instead of directly including link. See [...]
RUBY
end
end
@@ -19,7 +19,7 @@ RSpec.describe RuboCop::Cop::Gitlab::DocUrl, feature_category: :shared do
expect_offense(<<~'RUBY')
'See the docs: ' \
'https://docs.gitlab.com/ee/user/permissions#roles'
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `#help_page_url` instead of directly including link. See https://docs.gitlab.com/ee/development/documentation/#linking-to-help-in-ruby.
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `#help_page_url` instead of directly including link. See [...]
RUBY
end
end
@@ -30,7 +30,7 @@ RSpec.describe RuboCop::Cop::Gitlab::DocUrl, feature_category: :shared do
<<-HEREDOC
See the docs:
https://docs.gitlab.com/ee/user/permissions#roles
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `#help_page_url` instead of directly including link. See https://docs.gitlab.com/ee/development/documentation/#linking-to-help-in-ruby.
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `#help_page_url` instead of directly including link. See [...]
HEREDOC
RUBY
end
diff --git a/spec/rubocop/cop/gitlab/feature_available_usage_spec.rb b/spec/rubocop/cop/gitlab/feature_available_usage_spec.rb
index 184f2c3ee92..263cd8244b0 100644
--- a/spec/rubocop/cop/gitlab/feature_available_usage_spec.rb
+++ b/spec/rubocop/cop/gitlab/feature_available_usage_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe RuboCop::Cop::Gitlab::FeatureAvailableUsage do
end
it 'does not flag the use of Gitlab::Saas.feature_available?' do
- expect_no_offenses('Gitlab::Saas.feature_available?("some/feature")')
+ expect_no_offenses('Gitlab::Saas.feature_available?(:some_feature)')
end
it 'flags the use with a dynamic feature as nil' do
diff --git a/spec/rubocop/cop/gitlab/mark_used_feature_flags_spec.rb b/spec/rubocop/cop/gitlab/mark_used_feature_flags_spec.rb
index 4b7ea6b72e5..8d80a554a2a 100644
--- a/spec/rubocop/cop/gitlab/mark_used_feature_flags_spec.rb
+++ b/spec/rubocop/cop/gitlab/mark_used_feature_flags_spec.rb
@@ -146,40 +146,6 @@ RSpec.describe RuboCop::Cop::Gitlab::MarkUsedFeatureFlags do
end
end
- %w[
- use_rugged?
- ].each do |feature_flag_method|
- context "#{feature_flag_method} method" do
- context 'a string feature flag' do
- include_examples 'sets flag as used', %|#{feature_flag_method}(arg, "baz")|, 'baz'
- end
-
- context 'a symbol feature flag' do
- include_examples 'sets flag as used', %|#{feature_flag_method}(arg, :baz)|, 'baz'
- end
-
- context 'an interpolated string feature flag with a string prefix' do
- include_examples 'sets flag as used', %|#{feature_flag_method}(arg, "foo_\#{bar}")|, %w[foo_hello foo_world]
- end
-
- context 'an interpolated symbol feature flag with a string prefix' do
- include_examples 'sets flag as used', %|#{feature_flag_method}(arg, :"foo_\#{bar}")|, %w[foo_hello foo_world]
- end
-
- context 'an interpolated string feature flag with a string prefix and suffix' do
- include_examples 'does not set any flags as used', %|#{feature_flag_method}(arg, :"foo_\#{bar}_baz")|
- end
-
- context 'a dynamic string feature flag as a variable' do
- include_examples 'does not set any flags as used', %|#{feature_flag_method}(a_variable, an_arg)|
- end
-
- context 'an integer feature flag' do
- include_examples 'does not set any flags as used', %|#{feature_flag_method}(arg, 123)|
- end
- end
- end
-
describe 'self.limit_feature_flag = :foo' do
include_examples 'sets flag as used', 'self.limit_feature_flag = :foo', 'foo'
end
diff --git a/spec/rubocop/cop/migration/migration_record_spec.rb b/spec/rubocop/cop/migration/migration_record_spec.rb
index 96a1d8fa107..0294e6c43ab 100644
--- a/spec/rubocop/cop/migration/migration_record_spec.rb
+++ b/spec/rubocop/cop/migration/migration_record_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe RuboCop::Cop::Migration::MigrationRecord do
end
end
- %w(ActiveRecord::Base ApplicationRecord).each do |klass|
+ %w[ActiveRecord::Base ApplicationRecord].each do |klass|
context 'outside of a migration' do
it_behaves_like 'a disabled cop', klass
end
diff --git a/spec/rubocop/cop/migration/migration_with_milestone_spec.rb b/spec/rubocop/cop/migration/migration_with_milestone_spec.rb
new file mode 100644
index 00000000000..dd603cad2f8
--- /dev/null
+++ b/spec/rubocop/cop/migration/migration_with_milestone_spec.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+require 'rubocop_spec_helper'
+require_relative '../../../../rubocop/cop/migration/migration_with_milestone'
+
+RSpec.describe RuboCop::Cop::Migration::MigrationWithMilestone, feature_category: :database do
+ context 'when we\'re not a Gitlab migration' do
+ it 'does not register an offense at all' do
+ expect_no_offenses <<~CODE
+ class CreateProducts < ActiveRecord::Migration[7.0]
+ def change
+ add_column :users, :foo, :integer
+ end
+ end
+ CODE
+ end
+ end
+
+ context 'when we\'re a Gitlab migration' do
+ it 'does not register an offense if we\'re a version before 2.2' do
+ expect_no_offenses <<~CODE
+ class TestFoo < Gitlab::Database::Migration[2.1]
+ def change
+ add_column :users, :foo, :integer
+ end
+ end
+ CODE
+ end
+
+ context 'when we\'re version 2.2' do
+ it 'expects no offense if we call `milestone` with a string' do
+ expect_no_offenses <<~CODE
+ class TestFoo < Gitlab::Database::Migration[2.2]
+ milestone '16.7'
+
+ def change
+ add_column :users, :foo, :integer
+ end
+ end
+ CODE
+ end
+
+ it 'expects an offense if we don\'t call `milestone`' do
+ expect_offense <<~CODE
+ class TestFoo < Gitlab::Database::Migration[2.2]
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{described_class::MSG}
+ def change
+ add_column :users, :foo, :integer
+ end
+ end
+ CODE
+ end
+
+ it 'does not matter if include a mixin' do
+ expect_no_offenses <<~CODE
+ class TestFoo < Gitlab::Database::Migration[2.2]
+ include Gitlab::Test::Mixin
+
+ milestone '16.7'
+
+ def change
+ add_column :users, :foo, :integer
+ end
+ end
+ CODE
+ end
+
+ it 'does not matter if we call a helper method' do
+ expect_no_offenses <<~CODE
+ class TestFoo < Gitlab::Database::Migration[2.2]
+ disable_ddl_transaction!
+
+ milestone '16.7'
+
+ def change
+ add_column :users, :foo, :integer
+ end
+ end
+ CODE
+ end
+
+ it 'does not matter if we include a mixin and call a helper method' do
+ expect_no_offenses <<~CODE
+ class TestFoo < Gitlab::Database::Migration[2.2]
+ include Gitlab::Test::Mixin
+
+ disable_ddl_transaction!
+
+ milestone '16.7'
+
+ def change
+ add_column :users, :foo, :integer
+ end
+ end
+ CODE
+ end
+ end
+ end
+end
diff --git a/spec/rubocop/cop/migration/prevent_index_creation_spec.rb b/spec/rubocop/cop/migration/prevent_index_creation_spec.rb
index 088edfedfc9..5e7a1bd100d 100644
--- a/spec/rubocop/cop/migration/prevent_index_creation_spec.rb
+++ b/spec/rubocop/cop/migration/prevent_index_creation_spec.rb
@@ -4,7 +4,7 @@ require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/migration/prevent_index_creation'
RSpec.describe RuboCop::Cop::Migration::PreventIndexCreation do
- let(:forbidden_tables) { %w(ci_builds namespaces) }
+ let(:forbidden_tables) { %w[ci_builds namespaces projects users] }
let(:forbidden_tables_list) { forbidden_tables.join(', ') }
context 'when in migration' do
diff --git a/spec/rubocop/cop/migration/sidekiq_queue_migrate_spec.rb b/spec/rubocop/cop/migration/sidekiq_queue_migrate_spec.rb
index 46c460b5d49..675df0318a9 100644
--- a/spec/rubocop/cop/migration/sidekiq_queue_migrate_spec.rb
+++ b/spec/rubocop/cop/migration/sidekiq_queue_migrate_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe RuboCop::Cop::Migration::SidekiqQueueMigrate do
allow(cop).to receive(:in_post_deployment_migration?).and_return(false)
end
- %w(up down change any_other_method).each do |method_name|
+ %w[up down change any_other_method].each do |method_name|
it "registers an offense when sidekiq_queue_migrate is used in ##{method_name}" do
expect_offense(<<~RUBY)
def #{method_name}
diff --git a/spec/rubocop/cop/migration/unfinished_dependencies_spec.rb b/spec/rubocop/cop/migration/unfinished_dependencies_spec.rb
index cac48871856..f2e963ad322 100644
--- a/spec/rubocop/cop/migration/unfinished_dependencies_spec.rb
+++ b/spec/rubocop/cop/migration/unfinished_dependencies_spec.rb
@@ -99,7 +99,7 @@ RSpec.describe RuboCop::Cop::Migration::UnfinishedDependencies, feature_category
context 'with properly finalized dependent background migrations' do
before do
- allow_next_instance_of(RuboCop::BatchedBackgroundMigrations) do |bbms|
+ allow_next_instance_of(RuboCop::BatchedBackgroundMigrationsDictionary) do |bbms|
allow(bbms).to receive(:finalized_by).and_return(version - 5)
end
end
diff --git a/spec/rubocop/cop/migration/with_lock_retries_disallowed_method_spec.rb b/spec/rubocop/cop/migration/with_lock_retries_disallowed_method_spec.rb
index 5762f78820c..c132237a8c4 100644
--- a/spec/rubocop/cop/migration/with_lock_retries_disallowed_method_spec.rb
+++ b/spec/rubocop/cop/migration/with_lock_retries_disallowed_method_spec.rb
@@ -3,7 +3,7 @@
require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/migration/with_lock_retries_disallowed_method'
-RSpec.describe RuboCop::Cop::Migration::WithLockRetriesDisallowedMethod do
+RSpec.describe RuboCop::Cop::Migration::WithLockRetriesDisallowedMethod, feature_category: :database do
context 'when in migration' do
before do
allow(cop).to receive(:in_migration?).and_return(true)
diff --git a/spec/rubocop/cop/performance/readlines_each_spec.rb b/spec/rubocop/cop/performance/readlines_each_spec.rb
index 11e2cee9262..829dd72fa10 100644
--- a/spec/rubocop/cop/performance/readlines_each_spec.rb
+++ b/spec/rubocop/cop/performance/readlines_each_spec.rb
@@ -28,7 +28,7 @@ RSpec.describe RuboCop::Cop::Performance::ReadlinesEach do
end
context 'when reading all lines using IO.readlines.each' do
- %w(IO File).each do |klass|
+ %w[IO File].each do |klass|
it_behaves_like('class read', klass)
end
diff --git a/spec/rubocop/cop/style/inline_disable_annotation_spec.rb b/spec/rubocop/cop/style/inline_disable_annotation_spec.rb
new file mode 100644
index 00000000000..a180c08d534
--- /dev/null
+++ b/spec/rubocop/cop/style/inline_disable_annotation_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'rubocop_spec_helper'
+
+require_relative '../../../../rubocop/cop/style/inline_disable_annotation'
+
+RSpec.describe RuboCop::Cop::Style::InlineDisableAnnotation, feature_category: :shared do
+ it 'registers an offense' do
+ expect_offense(<<~RUBY)
+ # some other comment
+ abc = '1'
+ ['this', 'that'].each do |word|
+ next if something? # rubocop:disable Some/Cop, Another/Cop
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Inline disabling a cop needs to follow [...]
+ end
+ # rubocop:disable Some/Cop, Another/Cop - Bad comment
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Inline disabling a cop needs to follow [...]
+ # rubocop :todo Some/Cop Some other things
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Inline disabling a cop needs to follow [...]
+ # rubocop: disable Some/Cop, Another/Cop Some more stuff
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Inline disabling a cop needs to follow [...]
+ # rubocop:disable Some/Cop -- Good comment
+ if blah && this # some other comment about nothing
+ this.match?(/blah/) # rubocop:disable Some/Cop with a bad comment
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Inline disabling a cop needs to follow [...]
+ end
+ RUBY
+ end
+
+ it 'accepts correctly formatted comment' do
+ expect_no_offenses(<<~RUBY)
+ # some other comment
+ abc = '1'
+ ['this', 'that'].each do |word|
+ next if something? # rubocop:disable Some/Cop, Another/Cop -- Good comment
+ end
+ # rubocop:disable Some/Cop, Another/Cop -- Good comment
+ # rubocop :todo Some/Cop Some other things -- Good comment
+ # rubocop: disable Some/Cop, Another/Cop Some more stuff -- Good comment
+ # rubocop:disable Some/Cop -- Good comment
+ if blah && this # some other comment about nothing
+ this.match?(/blah/) # rubocop:disable Some/Cop -- Good comment
+ end
+ RUBY
+ end
+end
diff --git a/spec/rubocop/rubocop_spec.rb b/spec/rubocop/rubocop_spec.rb
new file mode 100644
index 00000000000..a80a0f72bdf
--- /dev/null
+++ b/spec/rubocop/rubocop_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+# No spec helper is `require`d because `fast_spec_helper` requires
+# `active_support/all` and we want to ensure that `rubocop/rubocop` loads it.
+
+require 'rubocop'
+require_relative '../../rubocop/rubocop'
+
+RSpec.describe 'rubocop/rubocop', feature_category: :tooling do
+ it 'loads activesupport to enhance Enumerable' do
+ expect(Enumerable.instance_methods).to include(:exclude?)
+ end
+end
diff --git a/spec/scripts/lib/glfm/update_specification_spec.rb b/spec/scripts/lib/glfm/update_specification_spec.rb
index 92434b37515..500e8685e77 100644
--- a/spec/scripts/lib/glfm/update_specification_spec.rb
+++ b/spec/scripts/lib/glfm/update_specification_spec.rb
@@ -278,7 +278,7 @@ RSpec.describe Glfm::UpdateSpecification, '#process', feature_category: :team_pl
end
it 'includes header and all examples', :unlimited_max_formatted_output_length do
- # rubocop:disable Style/StringConcatenation (string contatenation is more readable)
+ # rubocop:disable Style/StringConcatenation -- string contatenation is more readable
expected = described_class::ES_SNAPSHOT_SPEC_MD_HEADER +
ghfm_spec_txt_examples +
"\n" +
diff --git a/spec/serializers/build_details_entity_spec.rb b/spec/serializers/build_details_entity_spec.rb
index a9d58b20861..874bcbfceaf 100644
--- a/spec/serializers/build_details_entity_spec.rb
+++ b/spec/serializers/build_details_entity_spec.rb
@@ -128,7 +128,7 @@ RSpec.describe BuildDetailsEntity do
context 'when the dependency is in the same pipeline' do
let!(:test1) { create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test1', stage_idx: 0) }
let!(:test2) { create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test2', stage_idx: 1) }
- let!(:build) { create(:ci_build, :pending, pipeline: pipeline, stage_idx: 2, options: { dependencies: %w(test1 test2) }) }
+ let!(:build) { create(:ci_build, :pending, pipeline: pipeline, stage_idx: 2, options: { dependencies: %w[test1 test2] }) }
before do
build.pipeline.unlocked!
@@ -148,7 +148,7 @@ RSpec.describe BuildDetailsEntity do
end
context 'when dependency is not found' do
- let!(:build) { create(:ci_build, :pending, pipeline: pipeline, stage_idx: 2, options: { dependencies: %w(test1 test2) }) }
+ let!(:build) { create(:ci_build, :pending, pipeline: pipeline, stage_idx: 2, options: { dependencies: %w[test1 test2] }) }
before do
build.pipeline.unlocked!
diff --git a/spec/serializers/ci/job_entity_spec.rb b/spec/serializers/ci/job_entity_spec.rb
index 6dce87a1fc5..c3d0de11405 100644
--- a/spec/serializers/ci/job_entity_spec.rb
+++ b/spec/serializers/ci/job_entity_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::JobEntity do
+RSpec.describe Ci::JobEntity, feature_category: :continuous_integration do
let(:user) { create(:user) }
let(:job) { create(:ci_build, :running) }
let(:project) { job.project }
diff --git a/spec/serializers/ci/pipeline_entity_spec.rb b/spec/serializers/ci/pipeline_entity_spec.rb
index 0fd9a12440f..e4ac8488c8c 100644
--- a/spec/serializers/ci/pipeline_entity_spec.rb
+++ b/spec/serializers/ci/pipeline_entity_spec.rb
@@ -256,7 +256,6 @@ RSpec.describe Ci::PipelineEntity, feature_category: :continuous_integration do
project.add_maintainer(user)
end
- # Remove with `ci_fix_performance_pipelines_json_endpoint`.
context 'when disable_failed_builds is true' do
let(:options) { { disable_failed_builds: true } }
diff --git a/spec/serializers/container_repositories_serializer_spec.rb b/spec/serializers/container_repositories_serializer_spec.rb
index a0d08a8ba44..4ada143cb9c 100644
--- a/spec/serializers/container_repositories_serializer_spec.rb
+++ b/spec/serializers/container_repositories_serializer_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe ContainerRepositoriesSerializer do
project.add_developer(user)
stub_container_registry_config(enabled: true)
- stub_container_registry_tags(repository: /image/, tags: %w(rootA latest))
+ stub_container_registry_tags(repository: /image/, tags: %w[rootA latest])
end
describe '#represent' do
diff --git a/spec/serializers/diff_file_entity_spec.rb b/spec/serializers/diff_file_entity_spec.rb
index 5eee9c34e1e..00083dd501a 100644
--- a/spec/serializers/diff_file_entity_spec.rb
+++ b/spec/serializers/diff_file_entity_spec.rb
@@ -28,7 +28,7 @@ RSpec.describe DiffFileEntity do
let(:code_navigation_path) { Gitlab::CodeNavigationPath.new(project, project.commit.sha) }
let(:request) { EntityRequest.new(project: project, current_user: user) }
let(:entity) { described_class.new(diff_file, options.merge(request: request, merge_request: merge_request, code_navigation_path: code_navigation_path)) }
- let(:exposed_urls) { %i(edit_path view_path context_lines_path) }
+ let(:exposed_urls) { %i[edit_path view_path context_lines_path] }
it_behaves_like 'diff file entity'
diff --git a/spec/serializers/group_child_entity_spec.rb b/spec/serializers/group_child_entity_spec.rb
index 5af704a42da..e0d86377e18 100644
--- a/spec/serializers/group_child_entity_spec.rb
+++ b/spec/serializers/group_child_entity_spec.rb
@@ -144,13 +144,13 @@ RSpec.describe GroupChildEntity do
end
it 'includes the counts' do
- expect(json.keys).to include(*%i(project_count subgroup_count))
+ expect(json.keys).to include(*%i[project_count subgroup_count])
end
end
describe 'user is not a member of the group' do
it 'does not include the counts' do
- expect(json.keys).not_to include(*%i(project_count subgroup_count))
+ expect(json.keys).not_to include(*%i[project_count subgroup_count])
end
end
@@ -162,7 +162,7 @@ RSpec.describe GroupChildEntity do
end
it 'does not include the counts' do
- expect(json.keys).not_to include(*%i(project_count subgroup_count))
+ expect(json.keys).not_to include(*%i[project_count subgroup_count])
end
end
end
diff --git a/spec/serializers/group_link/group_group_link_entity_spec.rb b/spec/serializers/group_link/group_group_link_entity_spec.rb
index 502cdc5c048..8f31c53e841 100644
--- a/spec/serializers/group_link/group_group_link_entity_spec.rb
+++ b/spec/serializers/group_link/group_group_link_entity_spec.rb
@@ -9,8 +9,8 @@ RSpec.describe GroupLink::GroupGroupLinkEntity do
let(:entity) { described_class.new(group_group_link, { current_user: current_user, source: shared_group }) }
- before do
- allow(entity).to receive(:current_user).and_return(current_user)
+ subject(:as_json) do
+ entity.as_json
end
it 'matches json schema' do
@@ -19,7 +19,7 @@ RSpec.describe GroupLink::GroupGroupLinkEntity do
context 'source' do
it 'exposes `source`' do
- expect(entity.as_json[:source]).to include(
+ expect(as_json[:source]).to include(
id: shared_group.id,
full_name: shared_group.full_name,
web_url: shared_group.web_url
@@ -38,9 +38,9 @@ RSpec.describe GroupLink::GroupGroupLinkEntity do
end
end
- context 'when current user has `:admin_group_member` permissions' do
- before do
- allow(entity).to receive(:can?).with(current_user, :admin_group_member, shared_group).and_return(true)
+ context 'when current user has owner permissions for the shared group' do
+ before_all do
+ shared_group.add_owner(current_user)
end
context 'when direct_member? is true' do
@@ -49,10 +49,8 @@ RSpec.describe GroupLink::GroupGroupLinkEntity do
end
it 'exposes `can_update` and `can_remove` as `true`' do
- json = entity.as_json
-
- expect(json[:can_update]).to be true
- expect(json[:can_remove]).to be true
+ expect(as_json[:can_update]).to be true
+ expect(as_json[:can_remove]).to be true
end
end
@@ -62,10 +60,51 @@ RSpec.describe GroupLink::GroupGroupLinkEntity do
end
it 'exposes `can_update` and `can_remove` as `true`' do
- json = entity.as_json
+ expect(as_json[:can_update]).to be false
+ expect(as_json[:can_remove]).to be false
+ end
+ end
+ end
+
+ context 'when current user is not a group member' do
+ context 'when shared with group is public' do
+ it 'does expose shared_with_group details' do
+ expect(as_json[:shared_with_group].keys).to include(:id, :avatar_url, :web_url, :name)
+ end
+
+ it 'does expose source details' do
+ expect(as_json[:source].keys).to include(:id, :full_name)
+ end
+
+ it 'sets is_shared_with_group_private to false' do
+ expect(as_json[:is_shared_with_group_private]).to be false
+ end
+ end
+
+ context 'when shared with group is private' do
+ let_it_be(:shared_with_group) { create(:group, :private) }
+
+ let_it_be(:group_group_link) do
+ create(
+ :group_group_link,
+ {
+ shared_group: shared_group,
+ shared_with_group: shared_with_group,
+ expires_at: '2020-05-12'
+ }
+ )
+ end
+
+ it 'does not expose shared_with_group details' do
+ expect(as_json[:shared_with_group].keys).to contain_exactly(:id)
+ end
+
+ it 'does not expose source details' do
+ expect(as_json[:source]).to be_nil
+ end
- expect(json[:can_update]).to be false
- expect(json[:can_remove]).to be false
+ it 'sets is_shared_with_group_private to true' do
+ expect(as_json[:is_shared_with_group_private]).to be true
end
end
end
diff --git a/spec/serializers/group_link/project_group_link_entity_spec.rb b/spec/serializers/group_link/project_group_link_entity_spec.rb
index 1a8fcb2cfd3..00bfc43f17e 100644
--- a/spec/serializers/group_link/project_group_link_entity_spec.rb
+++ b/spec/serializers/group_link/project_group_link_entity_spec.rb
@@ -8,51 +8,69 @@ RSpec.describe GroupLink::ProjectGroupLinkEntity do
let(:entity) { described_class.new(project_group_link, { current_user: current_user, source: project_group_link.project }) }
- before do
- allow(entity).to receive(:current_user).and_return(current_user)
+ subject(:as_json) do
+ entity.as_json
end
it 'matches json schema' do
expect(entity.to_json).to match_schema('group_link/project_group_link')
end
- context 'when current user has `admin_project_member` permissions' do
- before do
- allow(entity).to receive(:can?).with(current_user, :admin_project_group_link, project_group_link).and_return(false)
- allow(entity).to receive(:can?).with(current_user, :admin_project_member, project_group_link.project).and_return(true)
+ context 'when current user is a project maintainer' do
+ before_all do
+ project_group_link.project.add_maintainer(current_user)
end
it 'exposes `can_update` and `can_remove` as `true`' do
- json = entity.as_json
-
- expect(json[:can_update]).to be true
- expect(json[:can_remove]).to be false
+ expect(as_json[:can_update]).to be true
+ expect(as_json[:can_remove]).to be true
end
end
context 'when current user is a group owner' do
- before do
- allow(entity).to receive(:can?).with(current_user, :admin_project_group_link, project_group_link).and_return(true)
- allow(entity).to receive(:can?).with(current_user, :admin_project_member, project_group_link.project).and_return(false)
+ before_all do
+ project_group_link.group.add_owner(current_user)
end
it 'exposes `can_remove` as true' do
- json = entity.as_json
-
- expect(json[:can_remove]).to be true
+ expect(as_json[:can_remove]).to be true
end
end
context 'when current user is not a group owner' do
- before do
- allow(entity).to receive(:can?).with(current_user, :admin_project_group_link, project_group_link).and_return(false)
- allow(entity).to receive(:can?).with(current_user, :admin_project_member, project_group_link.project).and_return(false)
+ it 'exposes `can_remove` as false' do
+ expect(as_json[:can_remove]).to be false
end
- it 'exposes `can_remove` as false' do
- json = entity.as_json
+ context 'when group is public' do
+ it 'does expose shared_with_group details' do
+ expect(as_json[:shared_with_group].keys).to include(:id, :avatar_url, :web_url, :name)
+ end
+
+ it 'does expose source details' do
+ expect(as_json[:source].keys).to include(:id, :full_name)
+ end
+
+ it 'sets is_shared_with_group_private to false' do
+ expect(as_json[:is_shared_with_group_private]).to be false
+ end
+ end
+
+ context 'when group is private' do
+ let_it_be(:private_group) { create(:group, :private) }
+ let_it_be(:project_group_link) { create(:project_group_link, group: private_group) }
+
+ it 'does not expose shared_with_group details' do
+ expect(as_json[:shared_with_group].keys).to contain_exactly(:id)
+ end
+
+ it 'does not expose source details' do
+ expect(as_json[:source]).to be_nil
+ end
- expect(json[:can_remove]).to be false
+ it 'sets is_shared_with_group_private to true' do
+ expect(as_json[:is_shared_with_group_private]).to be true
+ end
end
end
end
diff --git a/spec/serializers/issue_entity_spec.rb b/spec/serializers/issue_entity_spec.rb
index a8fd96a03bb..1faf4c6fe4c 100644
--- a/spec/serializers/issue_entity_spec.rb
+++ b/spec/serializers/issue_entity_spec.rb
@@ -149,7 +149,7 @@ RSpec.describe IssueEntity do
end
it 'returns archived project doc' do
- expect(subject[:archived_project_docs_path]).to eq('/help/user/project/settings/index.md#archive-a-project')
+ expect(subject[:archived_project_docs_path]).to eq('/help/user/project/settings/index#archive-a-project')
end
end
end
diff --git a/spec/serializers/merge_request_widget_entity_spec.rb b/spec/serializers/merge_request_widget_entity_spec.rb
index 8a0a2d38187..e9707265263 100644
--- a/spec/serializers/merge_request_widget_entity_spec.rb
+++ b/spec/serializers/merge_request_widget_entity_spec.rb
@@ -88,7 +88,7 @@ RSpec.describe MergeRequestWidgetEntity, feature_category: :code_review_workflow
let(:role) { :developer }
before do
- project.repository.create_file(user, Gitlab::FileDetector::PATTERNS[:gitlab_ci], 'CONTENT', message: 'Add .gitlab-ci.yml', branch_name: 'master')
+ project.repository.create_file(user, project.ci_config_path_or_default, 'CONTENT', message: 'Add .gitlab-ci.yml', branch_name: 'master')
end
it 'no ci config path' do
diff --git a/spec/serializers/review_app_setup_entity_spec.rb b/spec/serializers/review_app_setup_entity_spec.rb
index 9b068a2e9dd..9c6d54fd612 100644
--- a/spec/serializers/review_app_setup_entity_spec.rb
+++ b/spec/serializers/review_app_setup_entity_spec.rb
@@ -22,6 +22,10 @@ RSpec.describe ReviewAppSetupEntity do
expect(subject).to include(:can_setup_review_app)
end
+ it 'contains has_review_app' do
+ expect(subject).to include(:has_review_app)
+ end
+
context 'when the user can setup a review app' do
before do
allow(presenter).to receive(:can_setup_review_app?).and_return(true)
diff --git a/spec/services/activity_pub/accept_follow_service_spec.rb b/spec/services/activity_pub/accept_follow_service_spec.rb
new file mode 100644
index 00000000000..0f472412085
--- /dev/null
+++ b/spec/services/activity_pub/accept_follow_service_spec.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ActivityPub::AcceptFollowService, feature_category: :integrations do
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be_with_reload(:existing_subscription) do
+ create(:activity_pub_releases_subscription, :inbox, project: project)
+ end
+
+ let(:service) { described_class.new(existing_subscription, 'http://localhost/my-project/releases') }
+
+ describe '#execute' do
+ context 'when third party server complies' do
+ before do
+ allow(Gitlab::HTTP).to receive(:post).and_return(true)
+ service.execute
+ end
+
+ it 'sends an Accept activity' do
+ expect(Gitlab::HTTP).to have_received(:post)
+ end
+
+ it 'updates subscription state to accepted' do
+ expect(existing_subscription.reload.status).to eq 'accepted'
+ end
+ end
+
+ context 'when there is an error with third party server' do
+ before do
+ allow(Gitlab::HTTP).to receive(:post).and_raise(Errno::ECONNREFUSED)
+ end
+
+ it 'raises a ThirdPartyError' do
+ expect { service.execute }.to raise_error(ActivityPub::ThirdPartyError)
+ end
+
+ it 'does not update subscription state to accepted' do
+ begin
+ service.execute
+ rescue StandardError
+ end
+
+ expect(existing_subscription.reload.status).to eq 'requested'
+ end
+ end
+
+ context 'when subscription is already accepted' do
+ before do
+ allow(Gitlab::HTTP).to receive(:post).and_return(true)
+ allow(existing_subscription).to receive(:accepted!).and_return(true)
+ existing_subscription.status = :accepted
+ service.execute
+ end
+
+ it 'does not send an Accept activity' do
+ expect(Gitlab::HTTP).not_to have_received(:post)
+ end
+
+ it 'does not update subscription state' do
+ expect(existing_subscription).not_to have_received(:accepted!)
+ end
+ end
+
+ context 'when inbox has not been resolved' do
+ before do
+ allow(Gitlab::HTTP).to receive(:post).and_return(true)
+ allow(existing_subscription).to receive(:accepted!).and_return(true)
+ end
+
+ it 'raises an error' do
+ existing_subscription.subscriber_inbox_url = nil
+ expect { service.execute }.to raise_error(ActivityPub::AcceptFollowService::MissingInboxURLError)
+ end
+ end
+ end
+end
diff --git a/spec/services/activity_pub/inbox_resolver_service_spec.rb b/spec/services/activity_pub/inbox_resolver_service_spec.rb
new file mode 100644
index 00000000000..29048045bb5
--- /dev/null
+++ b/spec/services/activity_pub/inbox_resolver_service_spec.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ActivityPub::InboxResolverService, feature_category: :integrations do
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be_with_reload(:existing_subscription) { create(:activity_pub_releases_subscription, project: project) }
+ let(:service) { described_class.new(existing_subscription) }
+
+ shared_examples 'third party error' do
+ it 'raises a ThirdPartyError' do
+ expect { service.execute }.to raise_error(ActivityPub::ThirdPartyError)
+ end
+
+ it 'does not update the subscription record' do
+ begin
+ service.execute
+ rescue StandardError
+ end
+
+ expect(ActivityPub::ReleasesSubscription.last.subscriber_inbox_url).not_to eq 'https://example.com/user/inbox'
+ end
+ end
+
+ describe '#execute' do
+ context 'with successful HTTP request' do
+ before do
+ allow(Gitlab::HTTP).to receive(:get) { response }
+ end
+
+ let(:response) { instance_double(HTTParty::Response, body: body) }
+
+ context 'with a JSON response' do
+ let(:body) do
+ {
+ '@context': 'https://www.w3.org/ns/activitystreams',
+ id: 'https://example.com/user',
+ type: 'Person',
+ **inbox,
+ **entrypoints,
+ outbox: 'https://example.com/user/outbox'
+ }.to_json
+ end
+
+ let(:entrypoints) { {} }
+
+ context 'with valid response' do
+ let(:inbox) { { inbox: 'https://example.com/user/inbox' } }
+
+ context 'without a shared inbox' do
+ it 'updates only the inbox in the subscription record' do
+ service.execute
+
+ expect(ActivityPub::ReleasesSubscription.last.subscriber_inbox_url).to eq 'https://example.com/user/inbox'
+ expect(ActivityPub::ReleasesSubscription.last.shared_inbox_url).to be_nil
+ end
+ end
+
+ context 'with a shared inbox' do
+ let(:entrypoints) { { entrypoints: { sharedInbox: 'https://example.com/shared-inbox' } } }
+
+ it 'updates both the inbox and shared inbox in the subscription record' do
+ service.execute
+
+ expect(ActivityPub::ReleasesSubscription.last.subscriber_inbox_url).to eq 'https://example.com/user/inbox'
+ expect(ActivityPub::ReleasesSubscription.last.shared_inbox_url).to eq 'https://example.com/shared-inbox'
+ end
+ end
+ end
+
+ context 'without inbox attribute' do
+ let(:inbox) { {} }
+
+ it_behaves_like 'third party error'
+ end
+
+ context 'with a non string inbox attribute' do
+ let(:inbox) { { inbox: 27.13 } }
+
+ it_behaves_like 'third party error'
+ end
+ end
+
+ context 'with non JSON response' do
+ let(:body) { '<div>woops</div>' }
+
+ it_behaves_like 'third party error'
+ end
+ end
+
+ context 'with http error' do
+ before do
+ allow(Gitlab::HTTP).to receive(:get).and_raise(Errno::ECONNREFUSED)
+ end
+
+ it_behaves_like 'third party error'
+ end
+ end
+end
diff --git a/spec/services/admin/plan_limits/update_service_spec.rb b/spec/services/admin/plan_limits/update_service_spec.rb
index e57c234780c..eb9bbcf11aa 100644
--- a/spec/services/admin/plan_limits/update_service_spec.rb
+++ b/spec/services/admin/plan_limits/update_service_spec.rb
@@ -82,9 +82,9 @@ RSpec.describe Admin::PlanLimits::UpdateService, feature_category: :shared do
response = update_plan_limits
expect(response[:status]).to eq :error
- expect(response[:message]).to eq ["Notification limit must be greater than or equal to " \
- "storage_size_limit (Dashboard limit): 5 " \
- "and less than or equal to enforcement_limit: 10"]
+ expect(response[:message]).to eq [
+ "Notification limit must be greater than or equal to the dashboard limit (5)"
+ ]
end
end
@@ -102,9 +102,9 @@ RSpec.describe Admin::PlanLimits::UpdateService, feature_category: :shared do
response = update_plan_limits
expect(response[:status]).to eq :error
- expect(response[:message]).to eq ["Notification limit must be greater than or equal to " \
- "storage_size_limit (Dashboard limit): 5 " \
- "and less than or equal to enforcement_limit: 10"]
+ expect(response[:message]).to eq [
+ "Notification limit must be less than or equal to the enforcement limit (10)"
+ ]
end
end
@@ -113,8 +113,8 @@ RSpec.describe Admin::PlanLimits::UpdateService, feature_category: :shared do
before do
limits.update!(
- storage_size_limit: 12,
- notification_limit: 12
+ storage_size_limit: 10,
+ notification_limit: 9
)
end
@@ -122,9 +122,9 @@ RSpec.describe Admin::PlanLimits::UpdateService, feature_category: :shared do
response = update_plan_limits
expect(response[:status]).to eq :error
- expect(response[:message]).to eq ["Enforcement limit must be greater than " \
- "or equal to storage_size_limit (Dashboard limit): " \
- "12 and greater than or equal to notification_limit: 12"]
+ expect(response[:message]).to eq [
+ "Enforcement limit must be greater than or equal to the dashboard limit (10)"
+ ]
end
end
@@ -133,7 +133,7 @@ RSpec.describe Admin::PlanLimits::UpdateService, feature_category: :shared do
before do
limits.update!(
- storage_size_limit: 10,
+ storage_size_limit: 9,
notification_limit: 10
)
end
@@ -142,9 +142,9 @@ RSpec.describe Admin::PlanLimits::UpdateService, feature_category: :shared do
response = update_plan_limits
expect(response[:status]).to eq :error
- expect(response[:message]).to eq ["Enforcement limit must be greater than or equal to " \
- "storage_size_limit (Dashboard limit): " \
- "10 and greater than or equal to notification_limit: 10"]
+ expect(response[:message]).to eq [
+ "Enforcement limit must be greater than or equal to the notification limit (10)"
+ ]
end
end
@@ -162,8 +162,9 @@ RSpec.describe Admin::PlanLimits::UpdateService, feature_category: :shared do
response = update_plan_limits
expect(response[:status]).to eq :error
- expect(response[:message]).to eq ["Storage size limit (Dashboard limit) must be less than or " \
- "equal to enforcement_limit: 12 and notification_limit: 10"]
+ expect(response[:message]).to eq [
+ "Dashboard limit must be less than or equal to the notification limit (10)"
+ ]
end
end
@@ -181,8 +182,45 @@ RSpec.describe Admin::PlanLimits::UpdateService, feature_category: :shared do
response = update_plan_limits
expect(response[:status]).to eq :error
- expect(response[:message]).to eq ["Storage size limit (Dashboard limit) must be less than or " \
- "equal to enforcement_limit: 10 and notification_limit: 11"]
+ expect(response[:message]).to eq [
+ "Dashboard limit must be less than or equal to the enforcement limit (10)"
+ ]
+ end
+
+ context 'when enforcement_limit is 0' do
+ before do
+ limits.update!(
+ enforcement_limit: 0
+ )
+ end
+
+ it 'does not return an error' do
+ response = update_plan_limits
+
+ expect(response[:status]).to eq :success
+ end
+ end
+ end
+ end
+
+ context 'when setting limit to unlimited' do
+ before do
+ limits.update!(
+ notification_limit: 10,
+ storage_size_limit: 10,
+ enforcement_limit: 10
+ )
+ end
+
+ [:notification_limit, :enforcement_limit, :storage_size_limit].each do |limit|
+ context "for #{limit}" do
+ let(:params) { { limit => 0 } }
+
+ it 'is successful' do
+ response = update_plan_limits
+
+ expect(response[:status]).to eq :success
+ end
end
end
end
diff --git a/spec/services/application_settings/update_service_spec.rb b/spec/services/application_settings/update_service_spec.rb
index 0b5ba1db9d4..474d6ec4a9b 100644
--- a/spec/services/application_settings/update_service_spec.rb
+++ b/spec/services/application_settings/update_service_spec.rb
@@ -144,7 +144,7 @@ RSpec.describe ApplicationSettings::UpdateService, feature_category: :shared do
end
end
- describe 'performance bar settings', feature_category: :application_performance do
+ describe 'performance bar settings', feature_category: :cloud_connector do
using RSpec::Parameterized::TableSyntax
where(
@@ -321,7 +321,9 @@ RSpec.describe ApplicationSettings::UpdateService, feature_category: :shared do
let(:params) { { default_branch_protection: ::Gitlab::Access::PROTECTION_DEV_CAN_MERGE } }
it "updates default_branch_protection_defaults from the default_branch_protection param" do
- expect { subject.execute }.to change { application_settings.default_branch_protection_defaults }.from({}).to(expected)
+ default_value = ::Gitlab::Access::BranchProtection.protected_fully.deep_stringify_keys
+
+ expect { subject.execute }.to change { application_settings.default_branch_protection_defaults }.from(default_value).to(expected)
end
end
diff --git a/spec/services/auto_merge/base_service_spec.rb b/spec/services/auto_merge/base_service_spec.rb
index 8cd33f8ff1e..8329c4312cd 100644
--- a/spec/services/auto_merge/base_service_spec.rb
+++ b/spec/services/auto_merge/base_service_spec.rb
@@ -309,26 +309,18 @@ RSpec.describe AutoMerge::BaseService, feature_category: :code_review_workflow d
let(:merge_request) { create(:merge_request) }
- where(:can_be_merged, :open, :broken, :discussions, :blocked, :draft, :skip_draft, :skip_blocked,
- :skip_discussions, :result) do
- true | true | false | true | false | false | false | false | false | true
- true | true | false | true | false | false | true | true | false | true
- true | true | false | true | false | true | true | false | false | true
- true | true | false | true | true | false | false | true | false | true
- true | true | false | false | false | false | false | false | true | true
- true | true | false | true | false | true | false | false | false | false
- false | true | false | true | false | false | false | false | false | false
- true | false | false | true | false | false | false | false | false | false
- true | true | true | true | false | false | false | false | false | false
- true | true | false | false | false | false | false | false | false | false
- true | true | false | true | true | false | false | false | false | false
+ where(:can_be_merged, :open, :broken, :discussions, :blocked, :draft, :result) do
+ true | true | false | true | false | false | true
+ false | true | false | true | false | false | false
+ true | false | false | true | false | false | false
+ true | true | true | true | false | false | false
+ true | true | false | false | false | false | false
+ true | true | false | true | true | false | false
+ true | true | false | true | false | true | false
end
with_them do
before do
- allow(service).to receive(:skip_draft_check).and_return(skip_draft)
- allow(service).to receive(:skip_blocked_check).and_return(skip_blocked)
- allow(service).to receive(:skip_discussions_check).and_return(skip_discussions)
allow(merge_request).to receive(:can_be_merged_by?).and_return(can_be_merged)
allow(merge_request).to receive(:open?).and_return(open)
allow(merge_request).to receive(:broken?).and_return(broken)
diff --git a/spec/services/award_emojis/copy_service_spec.rb b/spec/services/award_emojis/copy_service_spec.rb
index 6c1d7fb21e2..81ec49d7741 100644
--- a/spec/services/award_emojis/copy_service_spec.rb
+++ b/spec/services/award_emojis/copy_service_spec.rb
@@ -25,7 +25,7 @@ RSpec.describe AwardEmojis::CopyService, feature_category: :team_planning do
it 'copies AwardEmojis', :aggregate_failures do
expect { execute_service }.to change { AwardEmoji.count }.by(2)
- expect(to_awardable.award_emoji.map(&:name)).to match_array(%w(thumbsup thumbsdown))
+ expect(to_awardable.award_emoji.map(&:name)).to match_array(%w[thumbsup thumbsdown])
end
it 'returns success', :aggregate_failures do
diff --git a/spec/services/bulk_imports/batched_relation_export_service_spec.rb b/spec/services/bulk_imports/batched_relation_export_service_spec.rb
index c361dfe5052..dd85961befd 100644
--- a/spec/services/bulk_imports/batched_relation_export_service_spec.rb
+++ b/spec/services/bulk_imports/batched_relation_export_service_spec.rb
@@ -71,29 +71,6 @@ RSpec.describe BulkImports::BatchedRelationExportService, feature_category: :imp
expect(export.batches.count).to eq(0)
end
end
-
- context 'when exception occurs' do
- it 'tracks exception and marks export as failed' do
- allow_next_instance_of(BulkImports::Export) do |export|
- allow(export).to receive(:update!).and_call_original
-
- allow(export)
- .to receive(:update!)
- .with(status_event: 'finish', total_objects_count: 0, batched: true, batches_count: 0, jid: jid, error: nil)
- .and_raise(StandardError, 'Error!')
- end
-
- expect(Gitlab::ErrorTracking)
- .to receive(:track_exception)
- .with(StandardError, portable_id: portable.id, portable_type: portable.class.name)
-
- service.execute
-
- export = portable.bulk_import_exports.first
-
- expect(export.reload.failed?).to eq(true)
- end
- end
end
describe '.cache_key' do
diff --git a/spec/services/bulk_imports/file_download_service_spec.rb b/spec/services/bulk_imports/file_download_service_spec.rb
index 1734ea45507..b2971c75bce 100644
--- a/spec/services/bulk_imports/file_download_service_spec.rb
+++ b/spec/services/bulk_imports/file_download_service_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe BulkImports::FileDownloadService, feature_category: :importers do
describe '#execute' do
- let_it_be(:allowed_content_types) { %w(application/gzip application/octet-stream) }
+ let_it_be(:allowed_content_types) { %w[application/gzip application/octet-stream] }
let_it_be(:file_size_limit) { 5.gigabytes }
let_it_be(:config) { build(:bulk_import_configuration) }
let_it_be(:content_type) { 'application/octet-stream' }
@@ -82,18 +82,17 @@ RSpec.describe BulkImports::FileDownloadService, feature_category: :importers do
context 'when content-type is not valid' do
let(:content_type) { 'invalid' }
- let(:import_logger) { instance_double(Gitlab::Import::Logger) }
+ let(:import_logger) { instance_double(BulkImports::Logger) }
before do
- allow(Gitlab::Import::Logger).to receive(:build).and_return(import_logger)
+ allow(BulkImports::Logger).to receive(:build).and_return(import_logger)
allow(import_logger).to receive(:warn)
end
it 'logs and raises an error' do
expect(import_logger).to receive(:warn).once.with(
message: 'Invalid content type',
- response_headers: headers,
- importer: 'gitlab_migration'
+ response_headers: headers
)
expect { subject.execute }.to raise_error(described_class::ServiceError, 'Invalid content type')
diff --git a/spec/services/bulk_imports/lfs_objects_export_service_spec.rb b/spec/services/bulk_imports/lfs_objects_export_service_spec.rb
index 587c99d9897..b65862b30d2 100644
--- a/spec/services/bulk_imports/lfs_objects_export_service_spec.rb
+++ b/spec/services/bulk_imports/lfs_objects_export_service_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe BulkImports::LfsObjectsExportService, feature_category: :importer
before do
stub_lfs_object_storage
- %w(wiki design).each do |repository_type|
+ %w[wiki design].each do |repository_type|
create(
:lfs_objects_project,
project: project,
diff --git a/spec/services/bulk_imports/process_service_spec.rb b/spec/services/bulk_imports/process_service_spec.rb
index 5398e76cb67..f5566819039 100644
--- a/spec/services/bulk_imports/process_service_spec.rb
+++ b/spec/services/bulk_imports/process_service_spec.rb
@@ -133,23 +133,6 @@ RSpec.describe BulkImports::ProcessService, feature_category: :importers do
end
end
end
-
- context 'when exception occurs' do
- it 'tracks the exception & marks import as failed' do
- create(:bulk_import_entity, :created, bulk_import: bulk_import)
-
- allow(BulkImports::ExportRequestWorker).to receive(:perform_async).and_raise(StandardError)
-
- expect(Gitlab::ErrorTracking).to receive(:track_exception).with(
- kind_of(StandardError),
- bulk_import_id: bulk_import.id
- )
-
- subject.execute
-
- expect(bulk_import.reload.failed?).to eq(true)
- end
- end
end
context 'when importing a group' do
@@ -221,15 +204,14 @@ RSpec.describe BulkImports::ProcessService, feature_category: :importers do
end
it 'logs an info message for the skipped pipelines' do
- expect_next_instance_of(Gitlab::Import::Logger) do |logger|
+ expect_next_instance_of(BulkImports::Logger) do |logger|
expect(logger).to receive(:info).with(
message: 'Pipeline skipped as source instance version not compatible with pipeline',
bulk_import_entity_id: entity.id,
bulk_import_id: entity.bulk_import_id,
bulk_import_entity_type: entity.source_type,
source_full_path: entity.source_full_path,
- importer: 'gitlab_migration',
- pipeline_name: 'PipelineClass4',
+ pipeline_class: 'PipelineClass4',
minimum_source_version: '15.1.0',
maximum_source_version: nil,
source_version: '15.0.0'
@@ -241,8 +223,7 @@ RSpec.describe BulkImports::ProcessService, feature_category: :importers do
bulk_import_id: entity.bulk_import_id,
bulk_import_entity_type: entity.source_type,
source_full_path: entity.source_full_path,
- importer: 'gitlab_migration',
- pipeline_name: 'PipelineClass5',
+ pipeline_class: 'PipelineClass5',
minimum_source_version: '16.0.0',
maximum_source_version: nil,
source_version: '15.0.0'
diff --git a/spec/services/bulk_imports/relation_batch_export_service_spec.rb b/spec/services/bulk_imports/relation_batch_export_service_spec.rb
index 8548e01a6aa..a18099a4189 100644
--- a/spec/services/bulk_imports/relation_batch_export_service_spec.rb
+++ b/spec/services/bulk_imports/relation_batch_export_service_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe BulkImports::RelationBatchExportService, feature_category: :impor
let_it_be(:batch) { create(:bulk_import_export_batch, export: export) }
let_it_be(:cache_key) { BulkImports::BatchedRelationExportService.cache_key(export.id, batch.id) }
- subject(:service) { described_class.new(user.id, batch.id) }
+ subject(:service) { described_class.new(user, batch) }
before_all do
Gitlab::Cache::Import::Caching.set_add(cache_key, label.id)
@@ -34,10 +34,7 @@ RSpec.describe BulkImports::RelationBatchExportService, feature_category: :impor
end
it 'removes exported contents after export' do
- double = instance_double(BulkImports::FileTransfer::ProjectConfig, export_path: 'foo')
-
- allow(BulkImports::FileTransfer).to receive(:config_for).and_return(double)
- allow(double).to receive(:export_service_for).and_raise(StandardError, 'Error!')
+ allow(subject).to receive(:export_path).and_return('foo')
allow(FileUtils).to receive(:remove_entry)
expect(FileUtils).to receive(:remove_entry).with('foo')
@@ -53,29 +50,10 @@ RSpec.describe BulkImports::RelationBatchExportService, feature_category: :impor
allow(subject).to receive(:export_path).and_return('foo')
allow(FileUtils).to receive(:remove_entry)
- expect(FileUtils).to receive(:touch).with('foo/milestones.ndjson')
+ expect(FileUtils).to receive(:touch).with('foo/milestones.ndjson').and_call_original
subject.execute
end
end
-
- context 'when exception occurs' do
- before do
- allow(service).to receive(:gzip).and_raise(StandardError, 'Error!')
- end
-
- it 'marks batch as failed' do
- expect(Gitlab::ErrorTracking)
- .to receive(:track_exception)
- .with(StandardError, portable_id: project.id, portable_type: 'Project')
-
- service.execute
- batch.reload
-
- expect(batch.failed?).to eq(true)
- expect(batch.objects_count).to eq(0)
- expect(batch.error).to eq('Error!')
- end
- end
end
end
diff --git a/spec/services/bulk_imports/relation_export_service_spec.rb b/spec/services/bulk_imports/relation_export_service_spec.rb
index bd8447e3d97..b7d6c424277 100644
--- a/spec/services/bulk_imports/relation_export_service_spec.rb
+++ b/spec/services/bulk_imports/relation_export_service_spec.rb
@@ -59,7 +59,7 @@ RSpec.describe BulkImports::RelationExportService, feature_category: :importers
let(:relation) { 'milestones' }
it 'creates empty file on disk' do
- expect(FileUtils).to receive(:touch).with("#{export_path}/#{relation}.ndjson")
+ expect(FileUtils).to receive(:touch).with("#{export_path}/#{relation}.ndjson").and_call_original
subject.execute
end
@@ -118,39 +118,6 @@ RSpec.describe BulkImports::RelationExportService, feature_category: :importers
end
end
- context 'when exception occurs during export' do
- shared_examples 'tracks exception' do |exception_class|
- it 'tracks exception' do
- expect(Gitlab::ErrorTracking)
- .to receive(:track_exception)
- .with(exception_class, portable_id: group.id, portable_type: group.class.name)
- .and_call_original
-
- subject.execute
- end
- end
-
- before do
- allow_next_instance_of(BulkImports::ExportUpload) do |upload|
- allow(upload).to receive(:save!).and_raise(StandardError)
- end
- end
-
- it 'marks export as failed' do
- subject.execute
-
- expect(export.reload.failed?).to eq(true)
- end
-
- include_examples 'tracks exception', StandardError
-
- context 'when passed relation is not supported' do
- let(:relation) { 'unsupported' }
-
- include_examples 'tracks exception', ActiveRecord::RecordInvalid
- end
- end
-
context 'when export was batched' do
let(:relation) { 'milestones' }
let(:export) { create(:bulk_import_export, group: group, relation: relation, batched: true, batches_count: 2) }
diff --git a/spec/services/ci/cancel_pipeline_service_spec.rb b/spec/services/ci/cancel_pipeline_service_spec.rb
index c4a1e1c26d1..256d2db1ed2 100644
--- a/spec/services/ci/cancel_pipeline_service_spec.rb
+++ b/spec/services/ci/cancel_pipeline_service_spec.rb
@@ -12,12 +12,12 @@ RSpec.describe Ci::CancelPipelineService, :aggregate_failures, feature_category:
pipeline: pipeline,
current_user: current_user,
cascade_to_children: cascade_to_children,
- auto_canceled_by_pipeline_id: auto_canceled_by_pipeline_id,
+ auto_canceled_by_pipeline: auto_canceled_by_pipeline,
execute_async: execute_async)
end
let(:cascade_to_children) { true }
- let(:auto_canceled_by_pipeline_id) { nil }
+ let(:auto_canceled_by_pipeline) { nil }
let(:execute_async) { true }
shared_examples 'force_execute' do
@@ -58,14 +58,19 @@ RSpec.describe Ci::CancelPipelineService, :aggregate_failures, feature_category:
expect(pipeline.all_jobs.pluck(:status)).to match_array(%w[canceled canceled success])
end
- context 'when auto_canceled_by_pipeline_id is provided' do
- let(:auto_canceled_by_pipeline_id) { create(:ci_pipeline).id }
+ context 'when auto_canceled_by_pipeline is provided' do
+ let(:auto_canceled_by_pipeline) { create(:ci_pipeline) }
it 'updates the pipeline and jobs with it' do
subject
- expect(pipeline.auto_canceled_by_id).to eq(auto_canceled_by_pipeline_id)
- expect(pipeline.all_jobs.canceled.pluck(:auto_canceled_by_id).uniq).to eq([auto_canceled_by_pipeline_id])
+ expect(pipeline.auto_canceled_by_id).to eq(auto_canceled_by_pipeline.id)
+
+ expect(pipeline.all_jobs.canceled.pluck(:auto_canceled_by_id).uniq)
+ .to eq([auto_canceled_by_pipeline.id])
+
+ expect(pipeline.all_jobs.canceled.pluck(:auto_canceled_by_partition_id).uniq)
+ .to eq([auto_canceled_by_pipeline.partition_id])
end
end
diff --git a/spec/services/ci/catalog/resources/create_service_spec.rb b/spec/services/ci/catalog/resources/create_service_spec.rb
new file mode 100644
index 00000000000..202c76acaec
--- /dev/null
+++ b/spec/services/ci/catalog/resources/create_service_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::Catalog::Resources::CreateService, feature_category: :pipeline_composition do
+ let_it_be(:project) { create(:project, :catalog_resource_with_components) }
+ let_it_be(:user) { create(:user) }
+
+ let(:service) { described_class.new(project, user) }
+
+ before do
+ stub_licensed_features(ci_namespace_catalog: true)
+ end
+
+ describe '#execute' do
+ context 'with an unauthorized user' do
+ it 'raises an AccessDeniedError' do
+ expect { service.execute }.to raise_error(Gitlab::Access::AccessDeniedError)
+ end
+ end
+
+ context 'with an authorized user' do
+ before_all do
+ project.add_owner(user)
+ end
+
+ context 'and a valid project' do
+ it 'creates a catalog resource' do
+ response = service.execute
+
+ expect(response.payload.project).to eq(project)
+ end
+ end
+
+ context 'with an invalid catalog resource' do
+ it 'does not save the catalog resource' do
+ catalog_resource = instance_double(::Ci::Catalog::Resource,
+ valid?: false,
+ errors: instance_double(ActiveModel::Errors, full_messages: ['not valid']))
+ allow(::Ci::Catalog::Resource).to receive(:new).and_return(catalog_resource)
+
+ response = service.execute
+
+ expect(response.message).to eq('not valid')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/catalog/resources/release_service_spec.rb b/spec/services/ci/catalog/resources/release_service_spec.rb
new file mode 100644
index 00000000000..60cd6cb5f96
--- /dev/null
+++ b/spec/services/ci/catalog/resources/release_service_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::Catalog::Resources::ReleaseService, feature_category: :pipeline_composition do
+ describe '#execute' do
+ context 'with a valid catalog resource and release' do
+ it 'validates the catalog resource and creates a version' do
+ project = create(:project, :catalog_resource_with_components)
+ catalog_resource = create(:ci_catalog_resource, project: project)
+ release = create(:release, project: project, sha: project.repository.root_ref_sha)
+
+ response = described_class.new(release).execute
+
+ version = Ci::Catalog::Resources::Version.last
+
+ expect(response).to be_success
+ expect(version.release).to eq(release)
+ expect(version.catalog_resource).to eq(catalog_resource)
+ expect(version.catalog_resource.project).to eq(project)
+ end
+ end
+
+ context 'when the validation of the catalog resource fails' do
+ it 'returns an error and does not create a version' do
+ project = create(:project, :repository)
+ create(:ci_catalog_resource, project: project)
+ release = create(:release, project: project, sha: project.repository.root_ref_sha)
+
+ response = described_class.new(release).execute
+
+ expect(Ci::Catalog::Resources::Version.count).to be(0)
+ expect(response).to be_error
+ expect(response.message).to eq(
+ 'Project must have a description, ' \
+ 'Project must contain components. Ensure you are using the correct directory structure')
+ end
+ end
+
+ context 'when the creation of a version fails' do
+ it 'returns an error and does not create a version' do
+ project =
+ create(
+ :project, :custom_repo,
+ description: 'Component project',
+ files: {
+ 'templates/secret-detection.yml' => 'image: agent: coop',
+ 'README.md' => 'Read me'
+ }
+ )
+ create(:ci_catalog_resource, project: project)
+ release = create(:release, project: project, sha: project.repository.root_ref_sha)
+
+ response = described_class.new(release).execute
+
+ expect(Ci::Catalog::Resources::Version.count).to be(0)
+ expect(response).to be_error
+ expect(response.message).to include('mapping values are not allowed in this context')
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/catalog/resources/validate_service_spec.rb b/spec/services/ci/catalog/resources/validate_service_spec.rb
index b43d98788e2..39ab758d78d 100644
--- a/spec/services/ci/catalog/resources/validate_service_spec.rb
+++ b/spec/services/ci/catalog/resources/validate_service_spec.rb
@@ -4,54 +4,85 @@ require 'spec_helper'
RSpec.describe Ci::Catalog::Resources::ValidateService, feature_category: :pipeline_composition do
describe '#execute' do
- context 'with a project that has a README and a description' do
+ context 'when a project has a README, a description, and at least one component' do
it 'is valid' do
- project = create(:project, :repository, description: 'Component project')
+ project = create(:project, :catalog_resource_with_components)
response = described_class.new(project, project.default_branch).execute
expect(response).to be_success
end
end
- context 'with a project that has neither a description nor a README' do
+ context 'when a project has neither a description nor a README nor components' do
it 'is not valid' do
- project = create(:project, :empty_repo)
- project.repository.create_file(
- project.creator,
- 'ruby.rb',
- 'I like this',
- message: 'Ruby like this',
- branch_name: 'master'
- )
+ project = create(:project, :small_repo)
response = described_class.new(project, project.default_branch).execute
- expect(response.message).to eq('Project must have a README , Project must have a description')
+ expect(response.message).to eq(
+ 'Project must have a README, ' \
+ 'Project must have a description, ' \
+ 'Project must contain components. Ensure you are using the correct directory structure')
end
end
- context 'with a project that has a description but not a README' do
+ context 'when a project has components but has neither a description nor a README' do
it 'is not valid' do
- project = create(:project, :empty_repo, description: 'project with no README')
- project.repository.create_file(
- project.creator,
- 'text.txt',
- 'I do not like this',
- message: 'only text like text',
- branch_name: 'master'
- )
+ project = create(:project, :small_repo, files: { 'templates/dast/template.yml' => 'image: alpine' })
response = described_class.new(project, project.default_branch).execute
- expect(response.message).to eq('Project must have a README')
+ expect(response.message).to eq('Project must have a README, Project must have a description')
+ end
+ end
+
+ context 'when a project has a description but has neither a README nor components' do
+ it 'is not valid' do
+ project = create(:project, :small_repo, description: 'project with no README and no components')
+ response = described_class.new(project, project.default_branch).execute
+
+ expect(response.message).to eq(
+ 'Project must have a README, ' \
+ 'Project must contain components. Ensure you are using the correct directory structure')
end
end
- context 'with a project that has a README and not a description' do
+ context 'when a project has a README but has neither a description nor components' do
it 'is not valid' do
project = create(:project, :repository)
response = described_class.new(project, project.default_branch).execute
+ expect(response.message).to eq(
+ 'Project must have a description, ' \
+ 'Project must contain components. Ensure you are using the correct directory structure')
+ end
+ end
+
+ context 'when a project has components and a description but no README' do
+ it 'is not valid' do
+ project = create(:project, :small_repo, description: 'desc', files: { 'templates/dast.yml' => 'image: alpine' })
+ response = described_class.new(project, project.default_branch).execute
+
+ expect(response.message).to eq('Project must have a README')
+ end
+ end
+
+ context 'when a project has components and a README but no description' do
+ it 'is not valid' do
+ project = create(:project, :custom_repo,
+ files: { 'templates/dast.yml' => 'image: alpine', 'README.md' => 'readme' })
+ response = described_class.new(project, project.default_branch).execute
+
expect(response.message).to eq('Project must have a description')
end
end
+
+ context 'when a project has a description and a README but no components' do
+ it 'is not valid' do
+ project = create(:project, :readme, description: 'project with no README and no components')
+ response = described_class.new(project, project.default_branch).execute
+
+ expect(response.message).to eq(
+ 'Project must contain components. Ensure you are using the correct directory structure')
+ end
+ end
end
end
diff --git a/spec/services/ci/catalog/resources/versions/create_service_spec.rb b/spec/services/ci/catalog/resources/versions/create_service_spec.rb
new file mode 100644
index 00000000000..e614a74a4a1
--- /dev/null
+++ b/spec/services/ci/catalog/resources/versions/create_service_spec.rb
@@ -0,0 +1,180 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::Catalog::Resources::Versions::CreateService, feature_category: :pipeline_composition do
+ describe '#execute' do
+ let(:files) do
+ {
+ 'templates/secret-detection.yml' => "spec:\n inputs:\n website:\n---\nimage: alpine_1",
+ 'templates/dast/template.yml' => 'image: alpine_2',
+ 'templates/blank-yaml.yml' => '',
+ 'templates/dast/sub-folder/template.yml' => 'image: alpine_3',
+ 'templates/template.yml' => "spec:\n inputs:\n environment:\n---\nimage: alpine_6",
+ 'tests/test.yml' => 'image: alpine_7',
+ 'README.md' => 'Read me'
+ }
+ end
+
+ let(:project) do
+ create(
+ :project, :custom_repo,
+ description: 'Simple and Complex components',
+ files: files
+ )
+ end
+
+ let(:release) { create(:release, project: project, sha: project.repository.root_ref_sha) }
+ let!(:catalog_resource) { create(:ci_catalog_resource, project: project) }
+
+ context 'when the project is not a catalog resource' do
+ it 'does not create a version' do
+ project = create(:project, :repository)
+ release = create(:release, project: project, sha: project.repository.root_ref_sha)
+
+ response = described_class.new(release).execute
+
+ expect(response).to be_error
+ expect(response.message).to include('Project is not a catalog resource')
+ end
+ end
+
+ context 'when the catalog resource has different types of components and a release' do
+ it 'creates a version for the release' do
+ response = described_class.new(release).execute
+
+ expect(response).to be_success
+
+ version = Ci::Catalog::Resources::Version.last
+
+ expect(version.release).to eq(release)
+ expect(version.catalog_resource).to eq(catalog_resource)
+ expect(version.catalog_resource.project).to eq(project)
+ end
+
+ it 'marks the catalog resource as published' do
+ described_class.new(release).execute
+
+ expect(catalog_resource.reload.state).to eq('published')
+ end
+
+ context 'when the ci_catalog_create_metadata feature flag is disabled' do
+ before do
+ stub_feature_flags(ci_catalog_create_metadata: false)
+ end
+
+ it 'does not create components' do
+ expect(Ci::Catalog::Resources::Component).not_to receive(:bulk_insert!).and_call_original
+ expect(project.ci_components.count).to eq(0)
+
+ response = described_class.new(release).execute
+
+ expect(response).to be_success
+ expect(project.ci_components.count).to eq(0)
+ end
+ end
+
+ context 'when the ci_catalog_create_metadata feature flag is enabled' do
+ context 'when there are more than 10 components' do
+ let(:files) do
+ {
+ 'templates/secret11.yml' => '',
+ 'templates/secret10.yml' => '',
+ 'templates/secret8.yml' => '',
+ 'templates/secret7.yml' => '',
+ 'templates/secret6.yml' => '',
+ 'templates/secret5.yml' => '',
+ 'templates/secret4.yml' => '',
+ 'templates/secret3.yml' => '',
+ 'templates/secret2.yml' => '',
+ 'templates/secret1.yml' => '',
+ 'templates/secret0.yml' => '',
+ 'README.md' => 'Read me'
+ }
+ end
+
+ it 'does not create components' do
+ response = described_class.new(release).execute
+
+ expect(response).to be_error
+ expect(response.message).to include('Release cannot contain more than 10 components')
+ expect(project.ci_components.count).to eq(0)
+ end
+ end
+
+ it 'bulk inserts all the components' do
+ expect(Ci::Catalog::Resources::Component).to receive(:bulk_insert!).and_call_original
+
+ described_class.new(release).execute
+ end
+
+ it 'creates components for the catalog resource' do
+ expect(project.ci_components.count).to eq(0)
+ response = described_class.new(release).execute
+
+ expect(response).to be_success
+
+ version = Ci::Catalog::Resources::Version.last
+
+ expect(project.ci_components.count).to eq(4)
+ expect(project.ci_components.first.name).to eq('blank-yaml')
+ expect(project.ci_components.first.project).to eq(version.project)
+ expect(project.ci_components.first.inputs).to eq({})
+ expect(project.ci_components.first.catalog_resource).to eq(version.catalog_resource)
+ expect(project.ci_components.first.version).to eq(version)
+ expect(project.ci_components.first.path).to eq('templates/blank-yaml.yml')
+ expect(project.ci_components.second.name).to eq('dast')
+ expect(project.ci_components.second.project).to eq(version.project)
+ expect(project.ci_components.second.inputs).to eq({})
+ expect(project.ci_components.second.catalog_resource).to eq(version.catalog_resource)
+ expect(project.ci_components.second.version).to eq(version)
+ expect(project.ci_components.second.path).to eq('templates/dast/template.yml')
+ expect(project.ci_components.third.name).to eq('secret-detection')
+ expect(project.ci_components.third.project).to eq(version.project)
+ expect(project.ci_components.third.inputs).to eq({ "website" => nil })
+ expect(project.ci_components.third.catalog_resource).to eq(version.catalog_resource)
+ expect(project.ci_components.third.version).to eq(version)
+ expect(project.ci_components.third.path).to eq('templates/secret-detection.yml')
+ expect(project.ci_components.fourth.name).to eq('template')
+ expect(project.ci_components.fourth.project).to eq(version.project)
+ expect(project.ci_components.fourth.inputs).to eq({ "environment" => nil })
+ expect(project.ci_components.fourth.catalog_resource).to eq(version.catalog_resource)
+ expect(project.ci_components.fourth.version).to eq(version)
+ expect(project.ci_components.fourth.path).to eq('templates/template.yml')
+ end
+ end
+ end
+
+ context 'with invalid data' do
+ let_it_be(:files) do
+ {
+ 'templates/secret-detection.yml' => 'some: invalid: syntax',
+ 'README.md' => 'Read me'
+ }
+ end
+
+ it 'returns an error' do
+ response = described_class.new(release).execute
+
+ expect(response).to be_error
+ expect(response.message).to include('mapping values are not allowed in this context')
+ end
+ end
+
+ context 'when one or more components are invalid' do
+ let_it_be(:files) do
+ {
+ 'templates/secret-detection.yml' => "spec:\n inputs:\n - website\n---\nimage: alpine_1",
+ 'README.md' => 'Read me'
+ }
+ end
+
+ it 'returns an error' do
+ response = described_class.new(release).execute
+
+ expect(response).to be_error
+ expect(response.message).to include('Inputs must be a valid json schema')
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/create_pipeline_service/pre_post_stages_spec.rb b/spec/services/ci/create_pipeline_service/pre_post_stages_spec.rb
index d935824e6cc..c0efb7cb639 100644
--- a/spec/services/ci/create_pipeline_service/pre_post_stages_spec.rb
+++ b/spec/services/ci/create_pipeline_service/pre_post_stages_spec.rb
@@ -39,9 +39,9 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
it 'creates a pipeline' do
expect(pipeline).to be_persisted
expect(pipeline.stages.map(&:name)).to contain_exactly(
- *%w(.pre build .post))
+ *%w[.pre build .post])
expect(pipeline.builds.map(&:name)).to contain_exactly(
- *%w(validate build notify))
+ *%w[validate build notify])
end
end
@@ -54,7 +54,7 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
# we can validate a list of stages, as they are assigned
# but not persisted
expect(pipeline.stages.map(&:name)).to contain_exactly(
- *%w(.pre .post))
+ *%w[.pre .post])
end
end
end
diff --git a/spec/services/ci/create_pipeline_service/rules_spec.rb b/spec/services/ci/create_pipeline_service/rules_spec.rb
index 05fa3cfeba3..fb448ab13dc 100644
--- a/spec/services/ci/create_pipeline_service/rules_spec.rb
+++ b/spec/services/ci/create_pipeline_service/rules_spec.rb
@@ -219,7 +219,7 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
let(:job1) { pipeline.builds.find_by(name: 'job1') }
let(:job2) { pipeline.builds.find_by(name: 'job2') }
- let(:variable_keys) { %w(VAR1 VAR2 VAR3 VAR4 VAR5 VAR6 VAR7) }
+ let(:variable_keys) { %w[VAR1 VAR2 VAR3 VAR4 VAR5 VAR6 VAR7] }
context 'when no match' do
let(:ref) { 'refs/heads/wip' }
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index 11f9708f9f3..19e55c22df8 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -1549,7 +1549,7 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
stage: 'build',
script: 'echo',
only: {
- variables: %w($CI)
+ variables: %w[$CI]
}
}
}
diff --git a/spec/services/ci/enqueue_job_service_spec.rb b/spec/services/ci/enqueue_job_service_spec.rb
index c2bb0bb2bb5..85983651148 100644
--- a/spec/services/ci/enqueue_job_service_spec.rb
+++ b/spec/services/ci/enqueue_job_service_spec.rb
@@ -78,4 +78,33 @@ RSpec.describe Ci::EnqueueJobService, '#execute', feature_category: :continuous_
execute
end
end
+
+ context 'when the job is manually triggered another user' do
+ let(:job_variables) do
+ [{ key: 'third', secret_value: 'third' },
+ { key: 'fourth', secret_value: 'fourth' }]
+ end
+
+ let(:service) do
+ described_class.new(build, current_user: user, variables: job_variables)
+ end
+
+ it 'assigns the user and variables to the job', :aggregate_failures do
+ called = false
+ service.execute do
+ unless called
+ called = true
+ raise ActiveRecord::StaleObjectError
+ end
+
+ build.enqueue!
+ end
+
+ build.reload
+
+ expect(called).to be true # ensure we actually entered the failure path
+ expect(build.user).to eq(user)
+ expect(build.job_variables.map(&:key)).to contain_exactly('third', 'fourth')
+ end
+ end
end
diff --git a/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb b/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb
index 88ccda90df0..6e263e82432 100644
--- a/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb
+++ b/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb
@@ -139,7 +139,7 @@ RSpec.describe Ci::PipelineProcessing::AtomicProcessingService, feature_category
fail_running_or_pending
- expect(builds_statuses).to eq %w(failed pending)
+ expect(builds_statuses).to eq %w[failed pending]
fail_running_or_pending
@@ -166,22 +166,22 @@ RSpec.describe Ci::PipelineProcessing::AtomicProcessingService, feature_category
succeed_running_or_pending
- expect(builds_names).to eq %w(build test)
- expect(builds_statuses).to eq %w(success pending)
+ expect(builds_names).to eq %w[build test]
+ expect(builds_statuses).to eq %w[success pending]
succeed_running_or_pending
- expect(builds_names).to eq %w(build test deploy production)
- expect(builds_statuses).to eq %w(success success pending manual)
+ expect(builds_names).to eq %w[build test deploy production]
+ expect(builds_statuses).to eq %w[success success pending manual]
succeed_running_or_pending
- expect(builds_names).to eq %w(build test deploy production cleanup clear:cache)
- expect(builds_statuses).to eq %w(success success success manual pending manual)
+ expect(builds_names).to eq %w[build test deploy production cleanup clear:cache]
+ expect(builds_statuses).to eq %w[success success success manual pending manual]
succeed_running_or_pending
- expect(builds_statuses).to eq %w(success success success manual success manual)
+ expect(builds_statuses).to eq %w[success success success manual success manual]
expect(pipeline.reload.status).to eq 'success'
end
end
@@ -194,22 +194,22 @@ RSpec.describe Ci::PipelineProcessing::AtomicProcessingService, feature_category
succeed_running_or_pending
- expect(builds_names).to eq %w(build test)
- expect(builds_statuses).to eq %w(success pending)
+ expect(builds_names).to eq %w[build test]
+ expect(builds_statuses).to eq %w[success pending]
fail_running_or_pending
- expect(builds_names).to eq %w(build test test_failure)
- expect(builds_statuses).to eq %w(success failed pending)
+ expect(builds_names).to eq %w[build test test_failure]
+ expect(builds_statuses).to eq %w[success failed pending]
succeed_running_or_pending
- expect(builds_names).to eq %w(build test test_failure cleanup)
- expect(builds_statuses).to eq %w(success failed success pending)
+ expect(builds_names).to eq %w[build test test_failure cleanup]
+ expect(builds_statuses).to eq %w[success failed success pending]
succeed_running_or_pending
- expect(builds_statuses).to eq %w(success failed success success)
+ expect(builds_statuses).to eq %w[success failed success success]
expect(pipeline.reload.status).to eq 'failed'
end
end
@@ -222,23 +222,23 @@ RSpec.describe Ci::PipelineProcessing::AtomicProcessingService, feature_category
succeed_running_or_pending
- expect(builds_names).to eq %w(build test)
- expect(builds_statuses).to eq %w(success pending)
+ expect(builds_names).to eq %w[build test]
+ expect(builds_statuses).to eq %w[success pending]
fail_running_or_pending
- expect(builds_names).to eq %w(build test test_failure)
- expect(builds_statuses).to eq %w(success failed pending)
+ expect(builds_names).to eq %w[build test test_failure]
+ expect(builds_statuses).to eq %w[success failed pending]
fail_running_or_pending
- expect(builds_names).to eq %w(build test test_failure cleanup)
- expect(builds_statuses).to eq %w(success failed failed pending)
+ expect(builds_names).to eq %w[build test test_failure cleanup]
+ expect(builds_statuses).to eq %w[success failed failed pending]
succeed_running_or_pending
- expect(builds_names).to eq %w(build test test_failure cleanup)
- expect(builds_statuses).to eq %w(success failed failed success)
+ expect(builds_names).to eq %w[build test test_failure cleanup]
+ expect(builds_statuses).to eq %w[success failed failed success]
expect(pipeline.reload.status).to eq('failed')
end
end
@@ -251,22 +251,22 @@ RSpec.describe Ci::PipelineProcessing::AtomicProcessingService, feature_category
succeed_running_or_pending
- expect(builds_names).to eq %w(build test)
- expect(builds_statuses).to eq %w(success pending)
+ expect(builds_names).to eq %w[build test]
+ expect(builds_statuses).to eq %w[success pending]
succeed_running_or_pending
- expect(builds_names).to eq %w(build test deploy production)
- expect(builds_statuses).to eq %w(success success pending manual)
+ expect(builds_names).to eq %w[build test deploy production]
+ expect(builds_statuses).to eq %w[success success pending manual]
fail_running_or_pending
- expect(builds_names).to eq %w(build test deploy production cleanup)
- expect(builds_statuses).to eq %w(success success failed manual pending)
+ expect(builds_names).to eq %w[build test deploy production cleanup]
+ expect(builds_statuses).to eq %w[success success failed manual pending]
succeed_running_or_pending
- expect(builds_statuses).to eq %w(success success failed manual success)
+ expect(builds_statuses).to eq %w[success success failed manual success]
expect(pipeline.reload).to be_failed
end
end
@@ -280,8 +280,8 @@ RSpec.describe Ci::PipelineProcessing::AtomicProcessingService, feature_category
succeed_running_or_pending
expect(builds.running_or_pending).not_to be_empty
- expect(builds_names).to eq %w(build test)
- expect(builds_statuses).to eq %w(success pending)
+ expect(builds_names).to eq %w[build test]
+ expect(builds_statuses).to eq %w[success pending]
cancel_running_or_pending
@@ -801,25 +801,25 @@ RSpec.describe Ci::PipelineProcessing::AtomicProcessingService, feature_category
it 'when linux:* finishes first it runs it out of order' do
expect(process_pipeline).to be_truthy
- expect(stages).to eq(%w(pending created created))
+ expect(stages).to eq(%w[pending created created])
expect(builds.pending).to contain_exactly(linux_build, mac_build)
# we follow the single path of linux
linux_build.reset.success!
- expect(stages).to eq(%w(running pending created))
+ expect(stages).to eq(%w[running pending created])
expect(builds.success).to contain_exactly(linux_build)
expect(builds.pending).to contain_exactly(mac_build, linux_rspec, linux_rubocop)
linux_rspec.reset.success!
- expect(stages).to eq(%w(running running created))
+ expect(stages).to eq(%w[running running created])
expect(builds.success).to contain_exactly(linux_build, linux_rspec)
expect(builds.pending).to contain_exactly(mac_build, linux_rubocop)
linux_rubocop.reset.success!
- expect(stages).to eq(%w(running running created))
+ expect(stages).to eq(%w[running running created])
expect(builds.success).to contain_exactly(linux_build, linux_rspec, linux_rubocop)
expect(builds.pending).to contain_exactly(mac_build)
@@ -827,7 +827,7 @@ RSpec.describe Ci::PipelineProcessing::AtomicProcessingService, feature_category
mac_rspec.reset.success!
mac_rubocop.reset.success!
- expect(stages).to eq(%w(success success pending))
+ expect(stages).to eq(%w[success success pending])
expect(builds.success).to contain_exactly(
linux_build, linux_rspec, linux_rubocop, mac_build, mac_rspec, mac_rubocop)
expect(builds.pending).to contain_exactly(deploy)
@@ -866,13 +866,13 @@ RSpec.describe Ci::PipelineProcessing::AtomicProcessingService, feature_category
it 'runs deploy_pages without waiting prior stages' do
expect(process_pipeline).to be_truthy
- expect(stages).to eq(%w(pending created pending))
+ expect(stages).to eq(%w[pending created pending])
expect(builds.pending).to contain_exactly(linux_build, mac_build, deploy_pages)
linux_build.reset.success!
deploy_pages.reset.success!
- expect(stages).to eq(%w(running pending running))
+ expect(stages).to eq(%w[running pending running])
expect(builds.success).to contain_exactly(linux_build, deploy_pages)
expect(builds.pending).to contain_exactly(mac_build, linux_rspec, linux_rubocop)
@@ -882,7 +882,7 @@ RSpec.describe Ci::PipelineProcessing::AtomicProcessingService, feature_category
mac_rspec.reset.success!
mac_rubocop.reset.success!
- expect(stages).to eq(%w(success success running))
+ expect(stages).to eq(%w[success success running])
expect(builds.pending).to contain_exactly(deploy)
end
end
@@ -900,12 +900,12 @@ RSpec.describe Ci::PipelineProcessing::AtomicProcessingService, feature_category
it 'skips the jobs depending on it' do
expect(process_pipeline).to be_truthy
- expect(stages).to eq(%w(pending created created))
+ expect(stages).to eq(%w[pending created created])
expect(all_builds.pending).to contain_exactly(linux_build)
linux_build.reset.drop!
- expect(stages).to eq(%w(failed skipped skipped))
+ expect(stages).to eq(%w[failed skipped skipped])
expect(all_builds.failed).to contain_exactly(linux_build)
expect(all_builds.skipped).to contain_exactly(linux_rspec, deploy)
end
@@ -922,7 +922,7 @@ RSpec.describe Ci::PipelineProcessing::AtomicProcessingService, feature_category
it 'makes deploy DAG to be skipped' do
expect(process_pipeline).to be_truthy
- expect(stages).to eq(%w(skipped skipped))
+ expect(stages).to eq(%w[skipped skipped])
expect(all_builds.manual).to contain_exactly(linux_build)
expect(all_builds.skipped).to contain_exactly(deploy)
end
@@ -1460,7 +1460,7 @@ RSpec.describe Ci::PipelineProcessing::AtomicProcessingService, feature_category
end
def delayed_options
- { when: 'delayed', options: { script: %w(echo), start_in: '1 minute' } }
+ { when: 'delayed', options: { script: %w[echo], start_in: '1 minute' } }
end
def unschedule
diff --git a/spec/services/ci/pipelines/update_metadata_service_spec.rb b/spec/services/ci/pipelines/update_metadata_service_spec.rb
new file mode 100644
index 00000000000..939ce7f5785
--- /dev/null
+++ b/spec/services/ci/pipelines/update_metadata_service_spec.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::Pipelines::UpdateMetadataService, feature_category: :continuous_integration do
+ subject(:execute) { described_class.new(pipeline, { name: name }).execute }
+
+ let(:name) { 'Some random pipeline name' }
+
+ context 'when pipeline has no name' do
+ let(:pipeline) { create(:ci_pipeline) }
+
+ it 'updates the name' do
+ expect { execute }.to change { pipeline.reload.name }.to(name)
+ end
+ end
+
+ context 'when pipeline has a name' do
+ let(:pipeline) { create(:ci_pipeline, name: 'Some other name') }
+
+ it 'updates the name' do
+ expect { execute }.to change { pipeline.reload.name }.to(name)
+ end
+ end
+
+ context 'when new name is too long' do
+ let(:pipeline) { create(:ci_pipeline) }
+ let(:name) { 'a' * 256 }
+
+ it 'does not update the name' do
+ expect { execute }.not_to change { pipeline.reload.name }
+ end
+ end
+end
diff --git a/spec/services/ci/play_build_service_spec.rb b/spec/services/ci/play_build_service_spec.rb
index 46b6622d6ec..c5651dc4502 100644
--- a/spec/services/ci/play_build_service_spec.rb
+++ b/spec/services/ci/play_build_service_spec.rb
@@ -63,10 +63,6 @@ RSpec.describe Ci::PlayBuildService, '#execute', feature_category: :continuous_i
context 'when a subsequent job is skipped' do
let!(:job) { create(:ci_build, :skipped, pipeline: pipeline, stage_idx: build.stage_idx + 1) }
- before do
- create(:ci_build_need, build: job, name: build.name)
- end
-
it 'marks the subsequent job as processable' do
expect { service.execute(build) }.to change { job.reload.status }.from('skipped').to('created')
end
diff --git a/spec/services/ci/refs/enqueue_pipelines_to_unlock_service_spec.rb b/spec/services/ci/refs/enqueue_pipelines_to_unlock_service_spec.rb
index 468302cb689..052be3b2587 100644
--- a/spec/services/ci/refs/enqueue_pipelines_to_unlock_service_spec.rb
+++ b/spec/services/ci/refs/enqueue_pipelines_to_unlock_service_spec.rb
@@ -26,34 +26,40 @@ RSpec.describe Ci::Refs::EnqueuePipelinesToUnlockService, :unlock_pipelines, :cl
shared_examples_for 'unlocking pipelines' do
let(:is_tag) { target_ref.ref_path.include?(::Gitlab::Git::TAG_REF_PREFIX) }
- let!(:other_ref_pipeline) { create_pipeline(:locked, other_ref, tag: false) }
- let!(:old_unlocked_pipeline) { create_pipeline(:unlocked, ref) }
- let!(:older_locked_pipeline_1) { create_pipeline(:locked, ref) }
- let!(:older_locked_pipeline_2) { create_pipeline(:locked, ref) }
- let!(:older_locked_pipeline_3) { create_pipeline(:locked, ref) }
- let!(:older_child_pipeline) { create_pipeline(:locked, ref, child_of: older_locked_pipeline_3) }
- let!(:pipeline) { create_pipeline(:locked, ref) }
- let!(:child_pipeline) { create_pipeline(:locked, ref, child_of: pipeline) }
- let!(:newer_pipeline) { create_pipeline(:locked, ref) }
+ let!(:other_ref_pipeline) { create_pipeline(:locked, other_ref, :failed, tag: false) }
+ let!(:old_unlocked_pipeline) { create_pipeline(:unlocked, ref, :failed) }
+ let!(:old_locked_pipeline_1) { create_pipeline(:locked, ref, :failed) }
+ let!(:old_locked_pipeline_2) { create_pipeline(:locked, ref, :success) }
+ let!(:old_locked_pipeline_3) { create_pipeline(:locked, ref, :success) }
+ let!(:old_locked_pipeline_3_child) { create_pipeline(:locked, ref, :success, child_of: old_locked_pipeline_3) }
+ let!(:old_locked_pipeline_4) { create_pipeline(:locked, ref, :success) }
+ let!(:old_locked_pipeline_4_child) { create_pipeline(:locked, ref, :success, child_of: old_locked_pipeline_4) }
+ let!(:old_locked_pipeline_5) { create_pipeline(:locked, ref, :failed) }
+ let!(:old_locked_pipeline_5_child) { create_pipeline(:locked, ref, :success, child_of: old_locked_pipeline_5) }
+ let!(:pipeline) { create_pipeline(:locked, ref, :failed) }
+ let!(:child_pipeline) { create_pipeline(:locked, ref, :failed, child_of: pipeline) }
+ let!(:newer_pipeline) { create_pipeline(:locked, ref, :failed) }
context 'when before_pipeline is given' do
let(:before_pipeline) { pipeline }
- it 'only enqueues older locked pipelines within the ref' do
+ it 'only enqueues old locked pipelines within the ref, excluding the last successful CI source pipeline' do
expect { execute }
.to change { pipeline_ids_waiting_to_be_unlocked }
.from([])
.to([
- older_locked_pipeline_1.id,
- older_locked_pipeline_2.id,
- older_locked_pipeline_3.id,
- older_child_pipeline.id
+ old_locked_pipeline_1.id,
+ old_locked_pipeline_2.id,
+ old_locked_pipeline_3.id,
+ old_locked_pipeline_3_child.id,
+ old_locked_pipeline_5.id,
+ old_locked_pipeline_5_child.id
])
expect(execute).to include(
status: :success,
- total_pending_entries: 4,
- total_new_entries: 4
+ total_pending_entries: 6,
+ total_new_entries: 6
)
end
end
@@ -66,10 +72,14 @@ RSpec.describe Ci::Refs::EnqueuePipelinesToUnlockService, :unlock_pipelines, :cl
.to change { pipeline_ids_waiting_to_be_unlocked }
.from([])
.to([
- older_locked_pipeline_1.id,
- older_locked_pipeline_2.id,
- older_locked_pipeline_3.id,
- older_child_pipeline.id,
+ old_locked_pipeline_1.id,
+ old_locked_pipeline_2.id,
+ old_locked_pipeline_3.id,
+ old_locked_pipeline_3_child.id,
+ old_locked_pipeline_4.id,
+ old_locked_pipeline_4_child.id,
+ old_locked_pipeline_5.id,
+ old_locked_pipeline_5_child.id,
pipeline.id,
child_pipeline.id,
newer_pipeline.id
@@ -77,8 +87,8 @@ RSpec.describe Ci::Refs::EnqueuePipelinesToUnlockService, :unlock_pipelines, :cl
expect(execute).to include(
status: :success,
- total_pending_entries: 7,
- total_new_entries: 7
+ total_pending_entries: 11,
+ total_new_entries: 11
)
end
end
@@ -96,9 +106,9 @@ RSpec.describe Ci::Refs::EnqueuePipelinesToUnlockService, :unlock_pipelines, :cl
it_behaves_like 'unlocking pipelines'
end
- def create_pipeline(type, ref, tag: is_tag, child_of: nil)
+ def create_pipeline(type, ref, status, tag: is_tag, child_of: nil)
trait = type == :locked ? :artifacts_locked : :unlocked
- create(:ci_pipeline, trait, ref: ref, tag: tag, project: project, child_of: child_of).tap do |p|
+ create(:ci_pipeline, trait, status: status, ref: ref, tag: tag, project: project, child_of: child_of).tap do |p|
if child_of
build = create(:ci_build, pipeline: child_of)
create(:ci_sources_pipeline, source_job: build, source_project: project, pipeline: p, project: project)
diff --git a/spec/services/ci/register_job_service_spec.rb b/spec/services/ci/register_job_service_spec.rb
index 83bae16a30e..e38984281b0 100644
--- a/spec/services/ci/register_job_service_spec.rb
+++ b/spec/services/ci/register_job_service_spec.rb
@@ -948,7 +948,7 @@ module Ci
pending_job.create_queuing_entry!
end
- let(:runner) { create(:ci_runner, :instance, tag_list: %w(tag1 tag2)) }
+ let(:runner) { create(:ci_runner, :instance, tag_list: %w[tag1 tag2]) }
let(:expected_shared_runner) { true }
let(:expected_shard) { ::Gitlab::Ci::Queue::Metrics::DEFAULT_METRICS_SHARD }
let(:expected_jobs_running_for_project_first_job) { '0' }
@@ -957,14 +957,14 @@ module Ci
it_behaves_like 'metrics collector'
context 'when metrics_shard tag is defined' do
- let(:runner) { create(:ci_runner, :instance, tag_list: %w(tag1 metrics_shard::shard_tag tag2)) }
+ let(:runner) { create(:ci_runner, :instance, tag_list: %w[tag1 metrics_shard::shard_tag tag2]) }
let(:expected_shard) { 'shard_tag' }
it_behaves_like 'metrics collector'
end
context 'when multiple metrics_shard tag is defined' do
- let(:runner) { create(:ci_runner, :instance, tag_list: %w(tag1 metrics_shard::shard_tag metrics_shard::shard_tag_2 tag2)) }
+ let(:runner) { create(:ci_runner, :instance, tag_list: %w[tag1 metrics_shard::shard_tag metrics_shard::shard_tag_2 tag2]) }
let(:expected_shard) { 'shard_tag' }
it_behaves_like 'metrics collector'
@@ -997,7 +997,7 @@ module Ci
end
context 'when project runner is used' do
- let(:runner) { create(:ci_runner, :project, projects: [project], tag_list: %w(tag1 metrics_shard::shard_tag tag2)) }
+ let(:runner) { create(:ci_runner, :project, projects: [project], tag_list: %w[tag1 metrics_shard::shard_tag tag2]) }
let(:expected_shared_runner) { false }
let(:expected_shard) { ::Gitlab::Ci::Queue::Metrics::DEFAULT_METRICS_SHARD }
let(:expected_jobs_running_for_project_first_job) { '+Inf' }
diff --git a/spec/services/ci/retry_job_service_spec.rb b/spec/services/ci/retry_job_service_spec.rb
index 80fbfc04f9b..1646afde21d 100644
--- a/spec/services/ci/retry_job_service_spec.rb
+++ b/spec/services/ci/retry_job_service_spec.rb
@@ -270,14 +270,6 @@ RSpec.describe Ci::RetryJobService, feature_category: :continuous_integration do
it_behaves_like 'creates associations for a deployable job', :ci_bridge
end
- context 'when `create_deployment_only_for_processable_jobs` FF is disabled' do
- before do
- stub_feature_flags(create_deployment_only_for_processable_jobs: false)
- end
-
- it_behaves_like 'creates associations for a deployable job', :ci_bridge
- end
-
context 'when given variables' do
let(:new_job) { service.clone!(job, variables: job_variables_attributes) }
@@ -302,14 +294,6 @@ RSpec.describe Ci::RetryJobService, feature_category: :continuous_integration do
it_behaves_like 'creates associations for a deployable job', :ci_build
end
- context 'when `create_deployment_only_for_processable_jobs` FF is disabled' do
- before do
- stub_feature_flags(create_deployment_only_for_processable_jobs: false)
- end
-
- it_behaves_like 'creates associations for a deployable job', :ci_build
- end
-
context 'when given variables' do
let(:new_job) { service.clone!(job, variables: job_variables_attributes) }
diff --git a/spec/services/ci/retry_pipeline_service_spec.rb b/spec/services/ci/retry_pipeline_service_spec.rb
index 6d991baafd0..125dbc5083c 100644
--- a/spec/services/ci/retry_pipeline_service_spec.rb
+++ b/spec/services/ci/retry_pipeline_service_spec.rb
@@ -122,7 +122,7 @@ RSpec.describe Ci::RetryPipelineService, '#execute', feature_category: :continuo
expect(build('build')).to be_success
expect(build('build2')).to be_success
expect(build('test')).to be_pending
- expect(build('test').needs.map(&:name)).to match_array(%w(build build2))
+ expect(build('test').needs.map(&:name)).to match_array(%w[build build2])
end
context 'when there is a failed DAG test without needs' do
diff --git a/spec/services/ci/runners/register_runner_service_spec.rb b/spec/services/ci/runners/register_runner_service_spec.rb
index b5921773364..4b997855657 100644
--- a/spec/services/ci/runners/register_runner_service_spec.rb
+++ b/spec/services/ci/runners/register_runner_service_spec.rb
@@ -74,7 +74,7 @@ RSpec.describe ::Ci::Runners::RegisterRunnerService, '#execute', feature_categor
active: false,
locked: true,
run_untagged: false,
- tag_list: %w(tag1 tag2),
+ tag_list: %w[tag1 tag2],
access_level: 'ref_protected',
maximum_timeout: 600,
name: 'some name',
@@ -290,7 +290,7 @@ RSpec.describe ::Ci::Runners::RegisterRunnerService, '#execute', feature_categor
let(:token) { registration_token }
let(:args) do
- { tag_list: %w(tag1 tag2) }
+ { tag_list: %w[tag1 tag2] }
end
it 'creates runner with tags' do
diff --git a/spec/services/ci/stuck_builds/drop_pending_service_spec.rb b/spec/services/ci/stuck_builds/drop_pending_service_spec.rb
index 6d91f5098eb..9da63930057 100644
--- a/spec/services/ci/stuck_builds/drop_pending_service_spec.rb
+++ b/spec/services/ci/stuck_builds/drop_pending_service_spec.rb
@@ -139,7 +139,7 @@ RSpec.describe Ci::StuckBuilds::DropPendingService, feature_category: :runner_fl
end
end
- %w(success skipped failed canceled).each do |status|
+ %w[success skipped failed canceled].each do |status|
context "when job is #{status}" do
let(:status) { status }
let(:updated_at) { 2.days.ago }
diff --git a/spec/services/ci/stuck_builds/drop_running_service_spec.rb b/spec/services/ci/stuck_builds/drop_running_service_spec.rb
index deb807753c2..c2f8a643f24 100644
--- a/spec/services/ci/stuck_builds/drop_running_service_spec.rb
+++ b/spec/services/ci/stuck_builds/drop_running_service_spec.rb
@@ -51,7 +51,7 @@ RSpec.describe Ci::StuckBuilds::DropRunningService, feature_category: :runner_fl
include_examples 'running builds'
end
- %w(success skipped failed canceled scheduled pending).each do |status|
+ %w[success skipped failed canceled scheduled pending].each do |status|
context "when job is #{status}" do
let(:status) { status }
let(:updated_at) { 2.days.ago }
diff --git a/spec/services/ci/stuck_builds/drop_scheduled_service_spec.rb b/spec/services/ci/stuck_builds/drop_scheduled_service_spec.rb
index f2e658c3ae3..5560eaf9b40 100644
--- a/spec/services/ci/stuck_builds/drop_scheduled_service_spec.rb
+++ b/spec/services/ci/stuck_builds/drop_scheduled_service_spec.rb
@@ -23,7 +23,7 @@ RSpec.describe Ci::StuckBuilds::DropScheduledService, feature_category: :runner_
end
end
- %w(success skipped failed canceled running pending).each do |status|
+ %w[success skipped failed canceled running pending].each do |status|
context "when job is #{status}" do
before do
job.update!(status: status)
diff --git a/spec/services/clusters/kubernetes/create_or_update_service_account_service_spec.rb b/spec/services/clusters/kubernetes/create_or_update_service_account_service_spec.rb
index e8e0174fe40..b5cf45e7b36 100644
--- a/spec/services/clusters/kubernetes/create_or_update_service_account_service_spec.rb
+++ b/spec/services/clusters/kubernetes/create_or_update_service_account_service_spec.rb
@@ -221,9 +221,9 @@ RSpec.describe Clusters::Kubernetes::CreateOrUpdateServiceAccountService, featur
namespace: namespace
},
rules: [{
- apiGroups: %w(serving.knative.dev),
- resources: %w(configurations configurationgenerations routes revisions revisionuids autoscalers services),
- verbs: %w(get list create update delete patch watch)
+ apiGroups: %w[serving.knative.dev],
+ resources: %w[configurations configurationgenerations routes revisions revisionuids autoscalers services],
+ verbs: %w[get list create update delete patch watch]
}]
)
)
@@ -239,9 +239,9 @@ RSpec.describe Clusters::Kubernetes::CreateOrUpdateServiceAccountService, featur
namespace: namespace
},
rules: [{
- apiGroups: %w(database.crossplane.io),
- resources: %w(postgresqlinstances),
- verbs: %w(get list create watch)
+ apiGroups: %w[database.crossplane.io],
+ resources: %w[postgresqlinstances],
+ verbs: %w[get list create watch]
}]
)
)
diff --git a/spec/services/container_registry/protection/create_rule_service_spec.rb b/spec/services/container_registry/protection/create_rule_service_spec.rb
new file mode 100644
index 00000000000..3c319caf25c
--- /dev/null
+++ b/spec/services/container_registry/protection/create_rule_service_spec.rb
@@ -0,0 +1,145 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ContainerRegistry::Protection::CreateRuleService, '#execute', feature_category: :container_registry do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:current_user) { create(:user, maintainer_projects: [project]) }
+
+ let(:service) { described_class.new(project, current_user, params) }
+ let(:params) { attributes_for(:container_registry_protection_rule) }
+
+ subject { service.execute }
+
+ shared_examples 'a successful service response' do
+ it { is_expected.to be_success }
+
+ it { is_expected.to have_attributes(errors: be_blank) }
+
+ it do
+ is_expected.to have_attributes(
+ payload: {
+ container_registry_protection_rule:
+ be_a(ContainerRegistry::Protection::Rule)
+ .and(have_attributes(
+ container_path_pattern: params[:container_path_pattern],
+ push_protected_up_to_access_level: params[:push_protected_up_to_access_level].to_s,
+ delete_protected_up_to_access_level: params[:delete_protected_up_to_access_level].to_s
+ ))
+ }
+ )
+ end
+
+ it 'creates a new container registry protection rule in the database' do
+ expect { subject }.to change { ContainerRegistry::Protection::Rule.count }.by(1)
+
+ expect(
+ ContainerRegistry::Protection::Rule.where(
+ project: project,
+ container_path_pattern: params[:container_path_pattern],
+ push_protected_up_to_access_level: params[:push_protected_up_to_access_level]
+ )
+ ).to exist
+ end
+ end
+
+ shared_examples 'an erroneous service response' do
+ it { is_expected.to be_error }
+ it { is_expected.to have_attributes(errors: be_present, payload: include(container_registry_protection_rule: nil)) }
+
+ it 'does not create a new container registry protection rule in the database' do
+ expect { subject }.not_to change { ContainerRegistry::Protection::Rule.count }
+ end
+
+ it 'does not create a container registry protection rule with the given params' do
+ subject
+
+ expect(
+ ContainerRegistry::Protection::Rule.where(
+ project: project,
+ container_path_pattern: params[:container_path_pattern],
+ push_protected_up_to_access_level: params[:push_protected_up_to_access_level]
+ )
+ ).not_to exist
+ end
+ end
+
+ it_behaves_like 'a successful service response'
+
+ context 'when fields are invalid' do
+ context 'when container_path_pattern is invalid' do
+ let(:params) { super().merge(container_path_pattern: '') }
+
+ it_behaves_like 'an erroneous service response'
+
+ it { is_expected.to have_attributes(message: match(/Container path pattern can't be blank/)) }
+ end
+
+ context 'when delete_protected_up_to_access_level is invalid' do
+ let(:params) { super().merge(delete_protected_up_to_access_level: 1000) }
+
+ it_behaves_like 'an erroneous service response'
+
+ it { is_expected.to have_attributes(message: match(/is not a valid delete_protected_up_to_access_level/)) }
+ end
+
+ context 'when push_protected_up_to_access_level is invalid' do
+ let(:params) { super().merge(push_protected_up_to_access_level: 1000) }
+
+ it_behaves_like 'an erroneous service response'
+
+ it { is_expected.to have_attributes(message: match(/is not a valid push_protected_up_to_access_level/)) }
+ end
+ end
+
+ context 'with existing container registry protection rule in the database' do
+ let_it_be_with_reload(:existing_container_registry_protection_rule) do
+ create(:container_registry_protection_rule, project: project)
+ end
+
+ context 'when container registry name pattern is slightly different' do
+ let(:params) do
+ super().merge(
+ # The field `container_path_pattern` is unique; this is why we change the value in a minimum way
+ container_path_pattern: "#{existing_container_registry_protection_rule.container_path_pattern}-unique",
+ push_protected_up_to_access_level:
+ existing_container_registry_protection_rule.push_protected_up_to_access_level
+ )
+ end
+
+ it_behaves_like 'a successful service response'
+ end
+
+ context 'when field `container_path_pattern` is taken' do
+ let(:params) do
+ super().merge(
+ container_path_pattern: existing_container_registry_protection_rule.container_path_pattern,
+ push_protected_up_to_access_level: :maintainer
+ )
+ end
+
+ it_behaves_like 'an erroneous service response'
+
+ it { is_expected.to have_attributes(errors: ['Container path pattern has already been taken']) }
+
+ it { expect { subject }.not_to change { existing_container_registry_protection_rule.updated_at } }
+ end
+ end
+
+ context 'with disallowed params' do
+ let(:params) { super().merge(project_id: 1, unsupported_param: 'unsupported_param_value') }
+
+ it_behaves_like 'a successful service response'
+ end
+
+ context 'with forbidden user access level (project developer role)' do
+ # Because of the access level hierarchy, we can assume that
+ # other access levels below developer role will also not be able to
+ # create container registry protection rules.
+ let_it_be(:current_user) { create(:user).tap { |u| project.add_developer(u) } }
+
+ it_behaves_like 'an erroneous service response'
+
+ it { is_expected.to have_attributes(message: match(/Unauthorized/)) }
+ end
+end
diff --git a/spec/services/deployments/update_environment_service_spec.rb b/spec/services/deployments/update_environment_service_spec.rb
index 79bf0d972d4..bc6e244dc2f 100644
--- a/spec/services/deployments/update_environment_service_spec.rb
+++ b/spec/services/deployments/update_environment_service_spec.rb
@@ -145,7 +145,7 @@ RSpec.describe Deployments::UpdateEnvironmentService, feature_category: :continu
an_instance_of(described_class::EnvironmentUpdateFailure),
project_id: project.id,
environment_id: environment.id,
- reason: %q{External url javascript scheme is not allowed}
+ reason: %q(External url javascript scheme is not allowed)
)
.once
diff --git a/spec/services/design_management/copy_design_collection/copy_service_spec.rb b/spec/services/design_management/copy_design_collection/copy_service_spec.rb
index 048327792e0..2f858e86cf1 100644
--- a/spec/services/design_management/copy_design_collection/copy_service_spec.rb
+++ b/spec/services/design_management/copy_design_collection/copy_service_spec.rb
@@ -267,7 +267,7 @@ RSpec.describe DesignManagement::CopyDesignCollection::CopyService, :clean_gitla
let_it_be(:config_file) { Rails.root.join('lib/gitlab/design_management/copy_design_collection_model_attributes.yml') }
let_it_be(:config) { YAML.load_file(config_file).symbolize_keys }
- %w(Design Action Version).each do |model|
+ %w[Design Action Version].each do |model|
specify do
attributes = config["#{model.downcase}_attributes".to_sym] || []
ignored_attributes = config["ignore_#{model.downcase}_attributes".to_sym]
diff --git a/spec/services/draft_notes/publish_service_spec.rb b/spec/services/draft_notes/publish_service_spec.rb
index e087f2ffc7e..fbc38f93c56 100644
--- a/spec/services/draft_notes/publish_service_spec.rb
+++ b/spec/services/draft_notes/publish_service_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe DraftNotes::PublishService, feature_category: :code_review_workflow do
include RepoHelpers
- let(:merge_request) { create(:merge_request) }
+ let_it_be(:merge_request) { create(:merge_request, reviewers: create_list(:user, 1)) }
let(:project) { merge_request.target_project }
let(:user) { merge_request.author }
let(:commit) { project.commit(sample_commit.id) }
@@ -198,6 +198,29 @@ RSpec.describe DraftNotes::PublishService, feature_category: :code_review_workfl
end
end
end
+
+ it 'does not call UpdateReviewerStateService' do
+ publish
+
+ expect(MergeRequests::UpdateReviewerStateService).not_to receive(:new)
+ end
+
+ context 'when `mr_request_changes` feature flag is disabled' do
+ before do
+ stub_feature_flags(mr_request_changes: false)
+ end
+
+ it 'calls UpdateReviewerStateService' do
+ expect_next_instance_of(
+ MergeRequests::UpdateReviewerStateService,
+ project: project, current_user: user
+ ) do |service|
+ expect(service).to receive(:execute).with(merge_request, "reviewed")
+ end
+
+ publish
+ end
+ end
end
context 'draft notes with suggestions' do
diff --git a/spec/services/environments/auto_recover_service_spec.rb b/spec/services/environments/auto_recover_service_spec.rb
new file mode 100644
index 00000000000..9807e8f9314
--- /dev/null
+++ b/spec/services/environments/auto_recover_service_spec.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Environments::AutoRecoverService, :clean_gitlab_redis_shared_state, :sidekiq_inline,
+ feature_category: :continuous_delivery do
+ include CreateEnvironmentsHelpers
+ include ExclusiveLeaseHelpers
+
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) { create(:user) }
+
+ let(:service) { described_class.new }
+
+ before_all do
+ project.add_developer(user)
+ end
+
+ describe '#execute' do
+ subject { service.execute }
+
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) { create(:user) }
+
+ let(:environments) { Environment.all }
+
+ before_all do
+ project.add_developer(user)
+ project.repository.add_branch(user, 'review/feature-1', 'master')
+ project.repository.add_branch(user, 'review/feature-2', 'master')
+ end
+
+ before do
+ create_review_app(user, project, 'review/feature-1')
+ create_review_app(user, project, 'review/feature-2')
+
+ Environment.all.map do |e|
+ e.stop_actions.map(&:drop)
+ e.stop!
+ e.update!(updated_at: (Environment::LONG_STOP + 1.day).ago)
+ e.reload
+ end
+ end
+
+ it 'stops environments that have been stuck stopping too long' do
+ expect { subject }
+ .to change { Environment.all.map(&:state).uniq }
+ .from(['stopping']).to(['available'])
+ end
+
+ it 'schedules stop processes in bulk' do
+ args = [[Environment.find_by_name('review/feature-1').id], [Environment.find_by_name('review/feature-2').id]]
+
+ expect(Environments::AutoRecoverWorker)
+ .to receive(:bulk_perform_async).with(args).once.and_call_original
+
+ subject
+ end
+
+ context 'when the other sidekiq worker has already been running' do
+ before do
+ stub_exclusive_lease_taken(described_class::EXCLUSIVE_LOCK_KEY)
+ end
+
+ it 'does not execute recover_in_batch' do
+ expect_next_instance_of(described_class) do |service|
+ expect(service).not_to receive(:recover_in_batch)
+ end
+
+ expect { subject }.to raise_error(Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError)
+ end
+ end
+
+ context 'when loop reached timeout' do
+ before do
+ stub_const("#{described_class}::LOOP_TIMEOUT", 0.seconds)
+ stub_const("#{described_class}::LOOP_LIMIT", 100_000)
+ allow_next_instance_of(described_class) do |service|
+ allow(service).to receive(:recover_in_batch).and_return(true)
+ end
+ end
+
+ it 'returns false and does not continue the process' do
+ is_expected.to eq(false)
+ end
+ end
+
+ context 'when loop reached loop limit' do
+ before do
+ stub_const("#{described_class}::LOOP_LIMIT", 1)
+ stub_const("#{described_class}::BATCH_SIZE", 1)
+ end
+
+ it 'stops only one available environment' do
+ expect { subject }.to change { Environment.long_stopping.count }.by(-1)
+ end
+ end
+ end
+end
diff --git a/spec/services/git/branch_hooks_service_spec.rb b/spec/services/git/branch_hooks_service_spec.rb
index 3050d6c5eca..8fd542542ae 100644
--- a/spec/services/git/branch_hooks_service_spec.rb
+++ b/spec/services/git/branch_hooks_service_spec.rb
@@ -133,27 +133,14 @@ RSpec.describe Git::BranchHooksService, :clean_gitlab_redis_shared_state, featur
expect(Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(event_names: 'o_pipeline_authoring_unique_users_committing_ciconfigfile', start_date: time, end_date: time + 7.days)).to eq(1)
end
- context 'when usage ping is disabled' do
- before do
- allow(::ServicePing::ServicePingSettings).to receive(:enabled?).and_return(false)
- end
-
- it 'does not track the event' do
- execute_service
-
- expect(Gitlab::UsageDataCounters::HLLRedisCounter)
- .not_to receive(:track_event).with(*tracking_params)
- end
- end
-
context 'when the branch is not the main branch' do
let(:branch) { 'feature' }
it 'does not track the event' do
- execute_service
-
expect(Gitlab::UsageDataCounters::HLLRedisCounter)
.not_to receive(:track_event).with(*tracking_params)
+
+ execute_service
end
end
@@ -163,10 +150,10 @@ RSpec.describe Git::BranchHooksService, :clean_gitlab_redis_shared_state, featur
end
it 'does not track the event' do
- execute_service
-
expect(Gitlab::UsageDataCounters::HLLRedisCounter)
.not_to receive(:track_event).with(*tracking_params)
+
+ execute_service
end
end
end
diff --git a/spec/services/git/branch_push_service_spec.rb b/spec/services/git/branch_push_service_spec.rb
index fe54663b983..db4f3ace64b 100644
--- a/spec/services/git/branch_push_service_spec.rb
+++ b/spec/services/git/branch_push_service_spec.rb
@@ -125,7 +125,7 @@ RSpec.describe Git::BranchPushService, :use_clean_rails_redis_caching, services:
allow(Gitlab::Runtime).to receive(:sidekiq?).and_return(true)
expect(Sidekiq.logger).to receive(:warn) do |args|
pipeline_params = args[:pipeline_params]
- expect(pipeline_params.keys).to match_array(%i(before after ref variables_attributes checkout_sha))
+ expect(pipeline_params.keys).to match_array(%i[before after ref variables_attributes checkout_sha])
end
expect { subject }.not_to change { Ci::Pipeline.count }
diff --git a/spec/services/git/process_ref_changes_service_spec.rb b/spec/services/git/process_ref_changes_service_spec.rb
index 9ec13bc957b..93d65b0b344 100644
--- a/spec/services/git/process_ref_changes_service_spec.rb
+++ b/spec/services/git/process_ref_changes_service_spec.rb
@@ -236,7 +236,7 @@ RSpec.describe Git::ProcessRefChangesService, feature_category: :source_code_man
before do
allow(MergeRequests::PushedBranchesService).to receive(:new).and_return(
- double(execute: %w(create1 create2)), double(execute: %w(changed1)), double(execute: %w(removed2))
+ double(execute: %w[create1 create2]), double(execute: %w[changed1]), double(execute: %w[removed2])
)
allow(Gitlab::Git::Commit).to receive(:between).and_return([])
diff --git a/spec/services/google_cloud/generate_pipeline_service_spec.rb b/spec/services/google_cloud/generate_pipeline_service_spec.rb
index 26a1ccb7e3b..8f49e1af901 100644
--- a/spec/services/google_cloud/generate_pipeline_service_spec.rb
+++ b/spec/services/google_cloud/generate_pipeline_service_spec.rb
@@ -32,7 +32,7 @@ RSpec.describe GoogleCloud::GeneratePipelineService, feature_category: :deployme
response = service.execute
ref = response[:commit][:result]
- gitlab_ci_yml = project.repository.gitlab_ci_yml_for(ref)
+ gitlab_ci_yml = project.ci_config_for(ref)
expect(response[:status]).to eq(:success)
expect(gitlab_ci_yml).to include('https://gitlab.com/gitlab-org/incubation-engineering/five-minute-production/library/-/raw/main/gcp/cloud-run.gitlab-ci.yml')
@@ -97,7 +97,7 @@ EOF
response = service.execute
branch_name = response[:branch_name]
- gitlab_ci_yml = project.repository.gitlab_ci_yml_for(branch_name)
+ gitlab_ci_yml = project.ci_config_for(branch_name)
pipeline = Gitlab::Config::Loader::Yaml.new(gitlab_ci_yml).load!
expect(response[:status]).to eq(:success)
@@ -110,7 +110,7 @@ EOF
response = service.execute
branch_name = response[:branch_name]
- gitlab_ci_yml = project.repository.gitlab_ci_yml_for(branch_name)
+ gitlab_ci_yml = project.ci_config_for(branch_name)
expect(YAML.safe_load(gitlab_ci_yml).keys).to eq(%w[stages build-java test-java include])
end
@@ -153,7 +153,7 @@ EOF
response = service.execute
branch_name = response[:branch_name]
- gitlab_ci_yml = project.repository.gitlab_ci_yml_for(branch_name)
+ gitlab_ci_yml = project.ci_config_for(branch_name)
pipeline = Gitlab::Config::Loader::Yaml.new(gitlab_ci_yml).load!
expect(response[:status]).to eq(:success)
@@ -195,7 +195,7 @@ EOF
response = service.execute
branch_name = response[:branch_name]
- gitlab_ci_yml = project.repository.gitlab_ci_yml_for(branch_name)
+ gitlab_ci_yml = project.ci_config_for(branch_name)
pipeline = Gitlab::Config::Loader::Yaml.new(gitlab_ci_yml).load!
expect(response[:status]).to eq(:success)
@@ -235,7 +235,7 @@ EOF
response = service.execute
ref = response[:commit][:result]
- gitlab_ci_yml = project.repository.gitlab_ci_yml_for(ref)
+ gitlab_ci_yml = project.ci_config_for(ref)
expect(response[:status]).to eq(:success)
expect(gitlab_ci_yml).to include('https://gitlab.com/gitlab-org/incubation-engineering/five-minute-production/library/-/raw/main/gcp/cloud-storage.gitlab-ci.yml')
@@ -272,7 +272,7 @@ EOF
response = service.execute
ref = response[:commit][:result]
- gitlab_ci_yml = project.repository.gitlab_ci_yml_for(ref)
+ gitlab_ci_yml = project.ci_config_for(ref)
expect(response[:status]).to eq(:success)
expect(gitlab_ci_yml).to include('https://gitlab.com/gitlab-org/incubation-engineering/five-minute-production/library/-/raw/main/gcp/vision-ai.gitlab-ci.yml')
@@ -328,7 +328,7 @@ EOF
response = service.execute
branch_name = response[:branch_name]
- gitlab_ci_yml = project.repository.gitlab_ci_yml_for(branch_name)
+ gitlab_ci_yml = project.ci_config_for(branch_name)
pipeline = Gitlab::Config::Loader::Yaml.new(gitlab_ci_yml).load!
expect(response[:status]).to eq(:success)
diff --git a/spec/services/groups/update_statistics_service_spec.rb b/spec/services/groups/update_statistics_service_spec.rb
index 6bab36eca89..39b9c1c234d 100644
--- a/spec/services/groups/update_statistics_service_spec.rb
+++ b/spec/services/groups/update_statistics_service_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Groups::UpdateStatisticsService, feature_category: :groups_and_projects do
let_it_be(:group, reload: true) { create(:group) }
- let(:statistics) { %w(wiki_size) }
+ let(:statistics) { %w[wiki_size] }
subject(:service) { described_class.new(group, statistics: statistics) }
diff --git a/spec/services/import/gitlab_projects/create_project_service_spec.rb b/spec/services/import/gitlab_projects/create_project_service_spec.rb
index a77e9bdfce1..3f5dc7a928f 100644
--- a/spec/services/import/gitlab_projects/create_project_service_spec.rb
+++ b/spec/services/import/gitlab_projects/create_project_service_spec.rb
@@ -144,9 +144,9 @@ RSpec.describe ::Import::GitlabProjects::CreateProjectService, :aggregate_failur
)
expect(response.payload).to eq(
other_errors: [
- %{Project namespace path can contain only letters, digits, '_', '-' and '.'. Cannot start with '-', end in '.git' or end in '.atom'},
- %{Path can contain only letters, digits, '_', '-' and '.'. Cannot start with '-', end in '.git' or end in '.atom'},
- %{Path must not start or end with a special character and must not contain consecutive special characters.}
+ %(Project namespace path can contain only letters, digits, '_', '-' and '.'. Cannot start with '-', end in '.git' or end in '.atom'),
+ %(Path can contain only letters, digits, '_', '-' and '.'. Cannot start with '-', end in '.git' or end in '.atom'),
+ %(Path must not start or end with a special character and must not contain consecutive special characters.)
])
end
end
diff --git a/spec/services/import/validate_remote_git_endpoint_service_spec.rb b/spec/services/import/validate_remote_git_endpoint_service_spec.rb
index 1d2b3975832..15e80f2c85d 100644
--- a/spec/services/import/validate_remote_git_endpoint_service_spec.rb
+++ b/spec/services/import/validate_remote_git_endpoint_service_spec.rb
@@ -7,7 +7,9 @@ RSpec.describe Import::ValidateRemoteGitEndpointService, feature_category: :impo
let_it_be(:base_url) { 'http://demo.host/path' }
let_it_be(:endpoint_url) { "#{base_url}/info/refs?service=git-upload-pack" }
- let_it_be(:error_message) { "#{base_url} is not a valid HTTP Git repository" }
+ let_it_be(:endpoint_error_message) { "#{base_url} endpoint error:" }
+ let_it_be(:body_error_message) { described_class::INVALID_BODY_MESSAGE }
+ let_it_be(:content_type_error_message) { described_class::INVALID_CONTENT_TYPE_MESSAGE }
describe '#execute' do
let(:valid_response) do
@@ -70,13 +72,14 @@ RSpec.describe Import::ValidateRemoteGitEndpointService, feature_category: :impo
end
it 'reports error when status code is not 200' do
- stub_full_request(endpoint_url, method: :get).to_return(valid_response.merge({ status: 301 }))
+ error_response = { status: 401 }
+ stub_full_request(endpoint_url, method: :get).to_return(error_response)
result = subject.execute
expect(result).to be_a(ServiceResponse)
expect(result.error?).to be(true)
- expect(result.message).to eq(error_message)
+ expect(result.message).to eq("#{endpoint_error_message} #{error_response[:status]}")
end
it 'reports error when invalid URL is provided' do
@@ -94,27 +97,49 @@ RSpec.describe Import::ValidateRemoteGitEndpointService, feature_category: :impo
expect(result).to be_a(ServiceResponse)
expect(result.error?).to be(true)
- expect(result.message).to eq(error_message)
+ expect(result.message).to eq(content_type_error_message)
end
- it 'reports error when body is in invalid format' do
+ it 'reports error when body is too short' do
stub_full_request(endpoint_url, method: :get).to_return(valid_response.merge({ body: 'invalid content' }))
result = subject.execute
expect(result).to be_a(ServiceResponse)
expect(result.error?).to be(true)
- expect(result.message).to eq(error_message)
+ expect(result.message).to eq(body_error_message)
+ end
+
+ it 'reports error when body is in invalid format' do
+ stub_full_request(endpoint_url, method: :get).to_return(valid_response.merge({ body: 'invalid long content with no git respons whatshowever' }))
+
+ result = subject.execute
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result.error?).to be(true)
+ expect(result.message).to eq(body_error_message)
+ end
+
+ it 'reports error when http exceptions are raised' do
+ err = SocketError.new('dummy message')
+ stub_full_request(endpoint_url, method: :get).to_raise(err)
+
+ result = subject.execute
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result.error?).to be(true)
+ expect(result.message).to eq("HTTP #{err.class.name.underscore} error: #{err.message}")
end
- it 'reports error when exception is raised' do
- stub_full_request(endpoint_url, method: :get).to_raise(SocketError.new('dummy message'))
+ it 'reports error when other exceptions are raised' do
+ err = StandardError.new('internal dummy message')
+ stub_full_request(endpoint_url, method: :get).to_raise(err)
result = subject.execute
expect(result).to be_a(ServiceResponse)
expect(result.error?).to be(true)
- expect(result.message).to eq(error_message)
+ expect(result.message).to eq("Internal #{err.class.name.underscore} error: #{err.message}")
end
end
diff --git a/spec/services/issuable/common_system_notes_service_spec.rb b/spec/services/issuable/common_system_notes_service_spec.rb
index 9306aeaac44..3d83c9ec9c2 100644
--- a/spec/services/issuable/common_system_notes_service_spec.rb
+++ b/spec/services/issuable/common_system_notes_service_spec.rb
@@ -42,7 +42,7 @@ RSpec.describe Issuable::CommonSystemNotesService, feature_category: :team_plann
context 'on issuable update' do
it_behaves_like 'system note creation', { title: 'New title' }, 'changed title'
it_behaves_like 'system note creation', { description: 'New description' }, 'changed the description'
- it_behaves_like 'system note creation', { discussion_locked: true }, 'locked this issue'
+ it_behaves_like 'system note creation', { discussion_locked: true }, 'locked the discussion in this issue'
it_behaves_like 'system note creation', { time_estimate: 5 }, 'changed time estimate'
context 'when new label is added' do
diff --git a/spec/services/issuable/discussions_list_service_spec.rb b/spec/services/issuable/discussions_list_service_spec.rb
index 446cc286e28..9c791ce9cd3 100644
--- a/spec/services/issuable/discussions_list_service_spec.rb
+++ b/spec/services/issuable/discussions_list_service_spec.rb
@@ -30,6 +30,12 @@ RSpec.describe Issuable::DiscussionsListService, feature_category: :team_plannin
expect(discussions_service.execute).to be_empty
end
end
+
+ context 'when issue exists at the group level' do
+ let_it_be(:issuable) { create(:issue, :group_level, namespace: group) }
+
+ it_behaves_like 'listing issuable discussions', :guest, 1, 7
+ end
end
describe 'fetching notes for merge requests' do
diff --git a/spec/services/issuable/process_assignees_spec.rb b/spec/services/issuable/process_assignees_spec.rb
index fac7ef9ce77..5484f46e955 100644
--- a/spec/services/issuable/process_assignees_spec.rb
+++ b/spec/services/issuable/process_assignees_spec.rb
@@ -6,11 +6,11 @@ RSpec.describe Issuable::ProcessAssignees, feature_category: :team_planning do
describe '#execute' do
it 'returns assignee_ids when add_assignee_ids and remove_assignee_ids are not specified' do
process = described_class.new(
- assignee_ids: %w(5 7 9),
+ assignee_ids: %w[5 7 9],
add_assignee_ids: nil,
remove_assignee_ids: nil,
- existing_assignee_ids: %w(1 3 9),
- extra_assignee_ids: %w(2 5 12)
+ existing_assignee_ids: %w[1 3 9],
+ extra_assignee_ids: %w[2 5 12]
)
result = process.execute
@@ -22,8 +22,8 @@ RSpec.describe Issuable::ProcessAssignees, feature_category: :team_planning do
assignee_ids: nil,
add_assignee_ids: nil,
remove_assignee_ids: nil,
- existing_assignee_ids: %w(1 3 11),
- extra_assignee_ids: %w(2 5 12)
+ existing_assignee_ids: %w[1 3 11],
+ extra_assignee_ids: %w[2 5 12]
)
result = process.execute
@@ -32,11 +32,11 @@ RSpec.describe Issuable::ProcessAssignees, feature_category: :team_planning do
it 'combines other ids when both add_assignee_ids and remove_assignee_ids are not empty' do
process = described_class.new(
- assignee_ids: %w(5 7 9),
- add_assignee_ids: %w(2 4 6),
- remove_assignee_ids: %w(4 7 11),
- existing_assignee_ids: %w(1 3 11),
- extra_assignee_ids: %w(2 5 12)
+ assignee_ids: %w[5 7 9],
+ add_assignee_ids: %w[2 4 6],
+ remove_assignee_ids: %w[4 7 11],
+ existing_assignee_ids: %w[1 3 11],
+ extra_assignee_ids: %w[2 5 12]
)
result = process.execute
@@ -45,11 +45,11 @@ RSpec.describe Issuable::ProcessAssignees, feature_category: :team_planning do
it 'combines other ids when remove_assignee_ids is not empty' do
process = described_class.new(
- assignee_ids: %w(5 7 9),
+ assignee_ids: %w[5 7 9],
add_assignee_ids: nil,
- remove_assignee_ids: %w(4 7 11),
- existing_assignee_ids: %w(1 3 11),
- extra_assignee_ids: %w(2 5 12)
+ remove_assignee_ids: %w[4 7 11],
+ existing_assignee_ids: %w[1 3 11],
+ extra_assignee_ids: %w[2 5 12]
)
result = process.execute
@@ -58,11 +58,11 @@ RSpec.describe Issuable::ProcessAssignees, feature_category: :team_planning do
it 'combines other ids when add_assignee_ids is not empty' do
process = described_class.new(
- assignee_ids: %w(5 7 9),
- add_assignee_ids: %w(2 4 6),
+ assignee_ids: %w[5 7 9],
+ add_assignee_ids: %w[2 4 6],
remove_assignee_ids: nil,
- existing_assignee_ids: %w(1 3 11),
- extra_assignee_ids: %w(2 5 12)
+ existing_assignee_ids: %w[1 3 11],
+ extra_assignee_ids: %w[2 5 12]
)
result = process.execute
@@ -71,9 +71,9 @@ RSpec.describe Issuable::ProcessAssignees, feature_category: :team_planning do
it 'combines ids when existing_assignee_ids and extra_assignee_ids are omitted' do
process = described_class.new(
- assignee_ids: %w(5 7 9),
- add_assignee_ids: %w(2 4 6),
- remove_assignee_ids: %w(4 7 11)
+ assignee_ids: %w[5 7 9],
+ add_assignee_ids: %w[2 4 6],
+ remove_assignee_ids: %w[4 7 11]
)
result = process.execute
@@ -82,11 +82,11 @@ RSpec.describe Issuable::ProcessAssignees, feature_category: :team_planning do
it 'handles mixed string and integer arrays' do
process = described_class.new(
- assignee_ids: %w(5 7 9),
+ assignee_ids: %w[5 7 9],
add_assignee_ids: [2, 4, 6],
- remove_assignee_ids: %w(4 7 11),
+ remove_assignee_ids: %w[4 7 11],
existing_assignee_ids: [1, 3, 11],
- extra_assignee_ids: %w(2 5 12)
+ extra_assignee_ids: %w[2 5 12]
)
result = process.execute
diff --git a/spec/services/issues/export_csv_service_spec.rb b/spec/services/issues/export_csv_service_spec.rb
index 31eaa72255d..83dfca923fb 100644
--- a/spec/services/issues/export_csv_service_spec.rb
+++ b/spec/services/issues/export_csv_service_spec.rb
@@ -160,7 +160,7 @@ RSpec.describe Issues::ExportCsvService, :with_license, feature_category: :team_
context 'with issues filtered by labels and project' do
subject do
described_class.new(
- IssuesFinder.new(user, project_id: project.id, label_name: %w(Idea Feature)).execute,
+ IssuesFinder.new(user, project_id: project.id, label_name: %w[Idea Feature]).execute,
project
)
end
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index c4012e2a98f..0cb13bfb917 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -491,9 +491,9 @@ RSpec.describe Issues::UpdateService, :mailer, feature_category: :team_planning
end
it 'creates system note about discussion lock' do
- note = find_note('locked this issue')
+ note = find_note('locked the discussion in this issue')
- expect(note.note).to eq 'locked this issue'
+ expect(note.note).to eq 'locked the discussion in this issue'
end
end
@@ -539,21 +539,6 @@ RSpec.describe Issues::UpdateService, :mailer, feature_category: :team_planning
end
end
end
-
- it 'verifies the number of queries' do
- update_issue(description: "- [ ] Task 1 #{user.to_reference}")
-
- baseline = ActiveRecord::QueryRecorder.new do
- update_issue(description: "- [x] Task 1 #{user.to_reference}")
- end
-
- recorded = ActiveRecord::QueryRecorder.new do
- update_issue(description: "- [x] Task 1 #{user.to_reference}\n- [ ] Task 2 #{user.to_reference}")
- end
-
- expect(recorded.count).to eq(baseline.count)
- expect(recorded.cached_count).to eq(0)
- end
end
context 'when description changed' do
diff --git a/spec/services/jira/requests/projects/list_service_spec.rb b/spec/services/jira/requests/projects/list_service_spec.rb
index f9e3a3e8510..d8e6eda2dd4 100644
--- a/spec/services/jira/requests/projects/list_service_spec.rb
+++ b/spec/services/jira/requests/projects/list_service_spec.rb
@@ -73,7 +73,7 @@ RSpec.describe Jira::Requests::Projects::ListService, feature_category: :groups_
payload = subject.payload
expect(subject.success?).to be_truthy
- expect(payload[:projects].map(&:key)).to eq(%w(pr1 pr2))
+ expect(payload[:projects].map(&:key)).to eq(%w[pr1 pr2])
expect(payload[:is_last]).to be_truthy
end
@@ -84,7 +84,7 @@ RSpec.describe Jira::Requests::Projects::ListService, feature_category: :groups_
payload = subject.payload
expect(subject.success?).to be_truthy
- expect(payload[:projects].map(&:key)).to eq(%w(pr1))
+ expect(payload[:projects].map(&:key)).to eq(%w[pr1])
expect(payload[:is_last]).to be_truthy
end
end
diff --git a/spec/services/jira_connect_subscriptions/create_service_spec.rb b/spec/services/jira_connect_subscriptions/create_service_spec.rb
index f9d3954b84c..2296d0fbfed 100644
--- a/spec/services/jira_connect_subscriptions/create_service_spec.rb
+++ b/spec/services/jira_connect_subscriptions/create_service_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe JiraConnectSubscriptions::CreateService, feature_category: :integ
let(:path) { group.full_path }
let(:params) { { namespace_path: path, jira_user: jira_user } }
- let(:jira_user) { double(:JiraUser, site_admin?: true) }
+ let(:jira_user) { double(:JiraUser, jira_admin?: true) }
subject { described_class.new(installation, current_user, params).execute }
@@ -29,11 +29,11 @@ RSpec.describe JiraConnectSubscriptions::CreateService, feature_category: :integ
end
context 'remote user does not have access' do
- let(:jira_user) { double(site_admin?: false) }
+ let(:jira_user) { double(jira_admin?: false) }
it_behaves_like 'a failed execution',
http_status: 403,
- message: 'The Jira user is not a site administrator. Check the permissions in Jira and try again.'
+ message: 'The Jira user is not a site or organization administrator. Check the permissions in Jira and try again.'
end
context 'remote user cannot be retrieved' do
diff --git a/spec/services/lfs/file_transformer_spec.rb b/spec/services/lfs/file_transformer_spec.rb
index c90d7af022f..398beabbeeb 100644
--- a/spec/services/lfs/file_transformer_spec.rb
+++ b/spec/services/lfs/file_transformer_spec.rb
@@ -218,7 +218,7 @@ RSpec.describe Lfs::FileTransformer, feature_category: :source_code_management d
repository_types = project.lfs_objects_projects.order(:id).pluck(:repository_type)
- expect(repository_types).to eq(%w(project wiki))
+ expect(repository_types).to eq(%w[project wiki])
end
end
end
diff --git a/spec/services/merge_requests/conflicts/resolve_service_spec.rb b/spec/services/merge_requests/conflicts/resolve_service_spec.rb
index 002a07ff14e..a4245456367 100644
--- a/spec/services/merge_requests/conflicts/resolve_service_spec.rb
+++ b/spec/services/merge_requests/conflicts/resolve_service_spec.rb
@@ -72,8 +72,8 @@ RSpec.describe MergeRequests::Conflicts::ResolveService, feature_category: :code
it 'creates a commit with the correct parents' do
expect(merge_request.source_branch_head.parents.map(&:id))
- .to eq(%w(1450cd639e0bc6721eb02800169e464f212cde06
- 824be604a34828eb682305f0d963056cfac87b2d))
+ .to eq(%w[1450cd639e0bc6721eb02800169e464f212cde06
+ 824be604a34828eb682305f0d963056cfac87b2d])
end
end
@@ -169,8 +169,8 @@ RSpec.describe MergeRequests::Conflicts::ResolveService, feature_category: :code
it 'creates a commit with the correct parents' do
expect(merge_request.source_branch_head.parents.map(&:id))
- .to eq(%w(1450cd639e0bc6721eb02800169e464f212cde06
- 824be604a34828eb682305f0d963056cfac87b2d))
+ .to eq(%w[1450cd639e0bc6721eb02800169e464f212cde06
+ 824be604a34828eb682305f0d963056cfac87b2d])
end
it 'sets the content to the content given' do
diff --git a/spec/services/merge_requests/mark_reviewer_reviewed_service_spec.rb b/spec/services/merge_requests/mark_reviewer_reviewed_service_spec.rb
deleted file mode 100644
index 172c2133168..00000000000
--- a/spec/services/merge_requests/mark_reviewer_reviewed_service_spec.rb
+++ /dev/null
@@ -1,55 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe MergeRequests::MarkReviewerReviewedService, feature_category: :code_review_workflow do
- let(:current_user) { create(:user) }
- let(:merge_request) { create(:merge_request, reviewers: [current_user]) }
- let(:reviewer) { merge_request.merge_request_reviewers.find_by(user_id: current_user.id) }
- let(:project) { merge_request.project }
- let(:service) { described_class.new(project: project, current_user: current_user) }
- let(:result) { service.execute(merge_request) }
-
- before do
- project.add_developer(current_user)
- end
-
- describe '#execute' do
- shared_examples_for 'failed service execution' do
- it 'returns an error' do
- expect(result[:status]).to eq :error
- end
-
- it_behaves_like 'does not trigger GraphQL subscription mergeRequestReviewersUpdated' do
- let(:action) { result }
- end
- end
-
- describe 'invalid permissions' do
- let(:service) { described_class.new(project: project, current_user: create(:user)) }
-
- it_behaves_like 'failed service execution'
- end
-
- describe 'reviewer does not exist' do
- let(:service) { described_class.new(project: project, current_user: create(:user)) }
-
- it_behaves_like 'failed service execution'
- end
-
- describe 'reviewer exists' do
- it 'returns success' do
- expect(result[:status]).to eq :success
- end
-
- it 'updates reviewers state' do
- expect(result[:status]).to eq :success
- expect(reviewer.state).to eq 'reviewed'
- end
-
- it_behaves_like 'triggers GraphQL subscription mergeRequestReviewersUpdated' do
- let(:action) { result }
- end
- end
- end
-end
diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb
index 6e34f4362c1..2e8f0049f28 100644
--- a/spec/services/merge_requests/merge_service_spec.rb
+++ b/spec/services/merge_requests/merge_service_spec.rb
@@ -569,7 +569,7 @@ RSpec.describe MergeRequests::MergeService, feature_category: :code_review_workf
allow_any_instance_of(Repository).to receive(:ancestor?).and_return(nil)
end
- %w(semi-linear ff).each do |merge_method|
+ %w[semi-linear ff].each do |merge_method|
it "logs and saves error if merge is #{merge_method} only" do
merge_method = 'rebase_merge' if merge_method == 'semi-linear'
merge_request.project.update!(merge_method: merge_method)
@@ -599,6 +599,7 @@ RSpec.describe MergeRequests::MergeService, feature_category: :code_review_workf
context 'with failing CI' do
before do
+ allow(merge_request.project).to receive(:only_allow_merge_if_pipeline_succeeds) { true }
allow(merge_request).to receive(:mergeable_ci_state?) { false }
end
@@ -616,6 +617,7 @@ RSpec.describe MergeRequests::MergeService, feature_category: :code_review_workf
context 'with unresolved discussions' do
before do
+ allow(merge_request.project).to receive(:only_allow_merge_if_all_discussions_are_resolved) { true }
allow(merge_request).to receive(:mergeable_discussions_state?) { false }
end
diff --git a/spec/services/merge_requests/mergeability/check_ci_status_service_spec.rb b/spec/services/merge_requests/mergeability/check_ci_status_service_spec.rb
index cf835cf70a3..067e87859e7 100644
--- a/spec/services/merge_requests/mergeability/check_ci_status_service_spec.rb
+++ b/spec/services/merge_requests/mergeability/check_ci_status_service_spec.rb
@@ -5,7 +5,8 @@ require 'spec_helper'
RSpec.describe MergeRequests::Mergeability::CheckCiStatusService, feature_category: :code_review_workflow do
subject(:check_ci_status) { described_class.new(merge_request: merge_request, params: params) }
- let(:merge_request) { build(:merge_request) }
+ let_it_be(:project) { build(:project) }
+ let_it_be(:merge_request) { build(:merge_request, source_project: project) }
let(:params) { { skip_ci_check: skip_check } }
let(:skip_check) { false }
@@ -13,23 +14,41 @@ RSpec.describe MergeRequests::Mergeability::CheckCiStatusService, feature_catego
let(:result) { check_ci_status.execute }
before do
- expect(merge_request).to receive(:mergeable_ci_state?).and_return(mergeable)
+ allow(merge_request)
+ .to receive(:only_allow_merge_if_pipeline_succeeds?)
+ .and_return(only_allow_merge_if_pipeline_succeeds?)
end
- context 'when the merge request is in a mergable state' do
- let(:mergeable) { true }
+ context 'when only_allow_merge_if_pipeline_succeeds is true' do
+ let(:only_allow_merge_if_pipeline_succeeds?) { true }
- it 'returns a check result with status success' do
- expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::SUCCESS_STATUS
+ before do
+ expect(merge_request).to receive(:mergeable_ci_state?).and_return(mergeable)
+ end
+
+ context 'when the merge request is in a mergeable state' do
+ let(:mergeable) { true }
+
+ it 'returns a check result with status success' do
+ expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::SUCCESS_STATUS
+ end
+ end
+
+ context 'when the merge request is not in a mergeable state' do
+ let(:mergeable) { false }
+
+ it 'returns a check result with status failed' do
+ expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::FAILED_STATUS
+ expect(result.payload[:reason]).to eq :ci_must_pass
+ end
end
end
- context 'when the merge request is not in a mergeable state' do
- let(:mergeable) { false }
+ context 'when only_allow_merge_if_pipeline_succeeds is false' do
+ let(:only_allow_merge_if_pipeline_succeeds?) { false }
- it 'returns a check result with status failed' do
- expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::FAILED_STATUS
- expect(result.payload[:reason]).to eq :ci_must_pass
+ it 'returns a check result with inactive status' do
+ expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::INACTIVE_STATUS
end
end
end
diff --git a/spec/services/merge_requests/mergeability/check_discussions_status_service_spec.rb b/spec/services/merge_requests/mergeability/check_discussions_status_service_spec.rb
index a3b77558ec3..4a8b28f603d 100644
--- a/spec/services/merge_requests/mergeability/check_discussions_status_service_spec.rb
+++ b/spec/services/merge_requests/mergeability/check_discussions_status_service_spec.rb
@@ -5,7 +5,8 @@ require 'spec_helper'
RSpec.describe MergeRequests::Mergeability::CheckDiscussionsStatusService, feature_category: :code_review_workflow do
subject(:check_discussions_status) { described_class.new(merge_request: merge_request, params: params) }
- let(:merge_request) { build(:merge_request) }
+ let_it_be(:project) { build(:project) }
+ let_it_be(:merge_request) { build(:merge_request, source_project: project) }
let(:params) { { skip_discussions_check: skip_check } }
let(:skip_check) { false }
@@ -13,23 +14,41 @@ RSpec.describe MergeRequests::Mergeability::CheckDiscussionsStatusService, featu
let(:result) { check_discussions_status.execute }
before do
- expect(merge_request).to receive(:mergeable_discussions_state?).and_return(mergeable)
+ allow(merge_request)
+ .to receive(:only_allow_merge_if_all_discussions_are_resolved?)
+ .and_return(only_allow_merge_if_all_discussions_are_resolved?)
end
- context 'when the merge request is in a mergable state' do
- let(:mergeable) { true }
+ context 'when only_allow_merge_if_all_discussions_are_resolved is true' do
+ let(:only_allow_merge_if_all_discussions_are_resolved?) { true }
- it 'returns a check result with status success' do
- expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::SUCCESS_STATUS
+ before do
+ allow(merge_request).to receive(:mergeable_discussions_state?).and_return(mergeable)
+ end
+
+ context 'when the merge request is in a mergable state' do
+ let(:mergeable) { true }
+
+ it 'returns a check result with status success' do
+ expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::SUCCESS_STATUS
+ end
+ end
+
+ context 'when the merge request is not in a mergeable state' do
+ let(:mergeable) { false }
+
+ it 'returns a check result with status failed' do
+ expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::FAILED_STATUS
+ expect(result.payload[:reason]).to eq(:discussions_not_resolved)
+ end
end
end
- context 'when the merge request is not in a mergeable state' do
- let(:mergeable) { false }
+ context 'when only_allow_merge_if_all_discussions_are_resolved is false' do
+ let(:only_allow_merge_if_all_discussions_are_resolved?) { false }
- it 'returns a check result with status failed' do
- expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::FAILED_STATUS
- expect(result.payload[:reason]).to eq(:discussions_not_resolved)
+ it 'returns a check result with inactive status' do
+ expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::INACTIVE_STATUS
end
end
end
diff --git a/spec/services/merge_requests/mergeability/check_rebase_status_service_spec.rb b/spec/services/merge_requests/mergeability/check_rebase_status_service_spec.rb
index 31ec44856b1..d6948f72c0a 100644
--- a/spec/services/merge_requests/mergeability/check_rebase_status_service_spec.rb
+++ b/spec/services/merge_requests/mergeability/check_rebase_status_service_spec.rb
@@ -5,7 +5,8 @@ require 'spec_helper'
RSpec.describe MergeRequests::Mergeability::CheckRebaseStatusService, feature_category: :code_review_workflow do
subject(:check_rebase_status) { described_class.new(merge_request: merge_request, params: params) }
- let(:merge_request) { build(:merge_request) }
+ let_it_be(:project) { build(:project) }
+ let_it_be(:merge_request) { build(:merge_request, source_project: project) }
let(:params) { { skip_rebase_check: skip_check } }
let(:skip_check) { false }
@@ -13,23 +14,41 @@ RSpec.describe MergeRequests::Mergeability::CheckRebaseStatusService, feature_ca
let(:result) { check_rebase_status.execute }
before do
- allow(merge_request).to receive(:should_be_rebased?).and_return(should_be_rebased)
+ allow(project)
+ .to receive(:ff_merge_must_be_possible?)
+ .and_return(ff_merge_must_be_possible?)
end
- context 'when the merge request should be rebased' do
- let(:should_be_rebased) { true }
+ context 'when ff_merge_must_be_possible is true' do
+ let(:ff_merge_must_be_possible?) { true }
- it 'returns a check result with status failed' do
- expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::FAILED_STATUS
- expect(result.payload[:reason]).to eq :need_rebase
+ before do
+ allow(merge_request).to receive(:should_be_rebased?).and_return(should_be_rebased)
+ end
+
+ context 'when the merge request should be rebased' do
+ let(:should_be_rebased) { true }
+
+ it 'returns a check result with status failed' do
+ expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::FAILED_STATUS
+ expect(result.payload[:reason]).to eq(:need_rebase)
+ end
+ end
+
+ context 'when the merge request should not be rebased' do
+ let(:should_be_rebased) { false }
+
+ it 'returns a check result with status success' do
+ expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::SUCCESS_STATUS
+ end
end
end
- context 'when the merge request should not be rebased' do
- let(:should_be_rebased) { false }
+ context 'when ff_merge_must_be_possible is false' do
+ let(:ff_merge_must_be_possible?) { false }
- it 'returns a check result with status success' do
- expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::SUCCESS_STATUS
+ it 'returns a check result with inactive status' do
+ expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::INACTIVE_STATUS
end
end
end
diff --git a/spec/services/merge_requests/mergeability/run_checks_service_spec.rb b/spec/services/merge_requests/mergeability/run_checks_service_spec.rb
index 546d583a2fb..06e15356a92 100644
--- a/spec/services/merge_requests/mergeability/run_checks_service_spec.rb
+++ b/spec/services/merge_requests/mergeability/run_checks_service_spec.rb
@@ -98,6 +98,26 @@ RSpec.describe MergeRequests::Mergeability::RunChecksService, :clean_gitlab_redi
let(:expected_count) { checks.count - 1 }
end
end
+
+ context 'when one check is inactive' do
+ let(:inactive_result) { Gitlab::MergeRequests::Mergeability::CheckResult.inactive }
+
+ before do
+ allow_next_instance_of(MergeRequests::Mergeability::CheckOpenStatusService) do |service|
+ allow(service).to receive(:skip?).and_return(false)
+ allow(service).to receive(:execute).and_return(inactive_result)
+ end
+ end
+
+ it 'is still a success' do
+ expect(execute.success?).to eq(true)
+ end
+
+ it_behaves_like 'checks are all executed' do
+ let(:success?) { true }
+ let(:expected_count) { checks.count - 1 }
+ end
+ end
end
context 'when a check is not skipped' do
diff --git a/spec/services/merge_requests/push_options_handler_service_spec.rb b/spec/services/merge_requests/push_options_handler_service_spec.rb
index 49ec8b09939..038977e4fd0 100644
--- a/spec/services/merge_requests/push_options_handler_service_spec.rb
+++ b/spec/services/merge_requests/push_options_handler_service_spec.rb
@@ -54,6 +54,17 @@ RSpec.describe MergeRequests::PushOptionsHandlerService, feature_category: :sour
end
end
+ shared_examples_for 'a service that can set the target project of a merge request' do
+ subject(:last_mr) { MergeRequest.last }
+
+ it 'creates a merge request with the correct target project' do
+ project_path = push_options[:target_project] || project.default_merge_request_target.full_path
+
+ expect { service.execute }.to change { MergeRequest.count }.by(1)
+ expect(last_mr.target_project.full_path).to eq(project_path)
+ end
+ end
+
shared_examples_for 'a service that can set the title of a merge request' do
subject(:last_mr) { MergeRequest.last }
@@ -347,6 +358,31 @@ RSpec.describe MergeRequests::PushOptionsHandlerService, feature_category: :sour
it_behaves_like 'with the project default branch'
end
+ describe '`target_project` push option' do
+ let(:changes) { new_branch_changes }
+ let(:double_forked_project) { fork_project(forked_project, user1, repository: true) }
+ let(:service) { described_class.new(project: double_forked_project, current_user: user1, changes: changes, push_options: push_options) }
+ let(:push_options) { { create: true, target_project: target_project.full_path } }
+
+ context 'to self' do
+ let(:target_project) { double_forked_project }
+
+ it_behaves_like 'a service that can set the target project of a merge request'
+ end
+
+ context 'to intermediate project' do
+ let(:target_project) { forked_project }
+
+ it_behaves_like 'a service that can set the target project of a merge request'
+ end
+
+ context 'to base project' do
+ let(:target_project) { project }
+
+ it_behaves_like 'a service that can set the target project of a merge request'
+ end
+ end
+
describe '`title` push option' do
let(:push_options) { { title: title } }
@@ -861,6 +897,17 @@ RSpec.describe MergeRequests::PushOptionsHandlerService, feature_category: :sour
end
end
+ describe 'when the target project does not exist' do
+ let(:push_options) { { create: true, target: 'my-branch', target_project: 'does-not-exist' } }
+ let(:changes) { default_branch_changes }
+
+ it 'records an error', :sidekiq_inline do
+ service.execute
+
+ expect(service.errors).to eq(["User access was denied"])
+ end
+ end
+
describe 'when user does not have access to target project' do
let(:push_options) { { create: true, target: 'my-branch' } }
let(:changes) { default_branch_changes }
@@ -890,6 +937,18 @@ RSpec.describe MergeRequests::PushOptionsHandlerService, feature_category: :sour
end
end
+ describe 'when projects are unrelated' do
+ let(:unrelated_project) { create(:project, :public, :repository, group: child_group) }
+ let(:push_options) { { create: true, target_project: unrelated_project.full_path } }
+ let(:changes) { new_branch_changes }
+
+ it 'records an error' do
+ service.execute
+
+ expect(service.errors).to eq(["Projects #{project.full_path} and #{unrelated_project.full_path} are not in the same network"])
+ end
+ end
+
describe 'when MR has ActiveRecord errors' do
let(:push_options) { { create: true } }
let(:changes) { new_branch_changes }
diff --git a/spec/services/merge_requests/pushed_branches_service_spec.rb b/spec/services/merge_requests/pushed_branches_service_spec.rb
index cb5d0a6bd25..de99fb244d3 100644
--- a/spec/services/merge_requests/pushed_branches_service_spec.rb
+++ b/spec/services/merge_requests/pushed_branches_service_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe MergeRequests::PushedBranchesService, feature_category: :source_c
context 'when branches pushed' do
let(:pushed_branches) do
- %w(branch1 branch2 closed-branch1 closed-branch2 extra1 extra2).map do |branch|
+ %w[branch1 branch2 closed-branch1 closed-branch2 extra1 extra2].map do |branch|
{ ref: "refs/heads/#{branch}" }
end
end
@@ -31,7 +31,7 @@ RSpec.describe MergeRequests::PushedBranchesService, feature_category: :source_c
context 'when tags pushed' do
let(:pushed_branches) do
- %w(v10.0.0 v11.0.2 v12.1.0).map do |branch|
+ %w[v10.0.0 v11.0.2 v12.1.0].map do |branch|
{ ref: "refs/tags/#{branch}" }
end
end
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index d5b7b56ccdd..dd50dfa49e0 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -915,7 +915,7 @@ RSpec.describe MergeRequests::RefreshService, feature_category: :code_review_wor
context 'feature enabled' do
it "updates merge requests' merge_commit and merged_commit values", :aggregate_failures do
expect(Gitlab::BranchPushMergeCommitAnalyzer).to receive(:new).and_wrap_original do |original_method, commits|
- expect(commits.map(&:id)).to eq(%w{646ece5cfed840eca0a4feb21bcd6a81bb19bda3 29284d9bcc350bcae005872d0be6edd016e2efb5 5f82584f0a907f3b30cfce5bb8df371454a90051 8a994512e8c8f0dfcf22bb16df6e876be7a61036 689600b91aabec706e657e38ea706ece1ee8268f db46a1c5a5e474aa169b6cdb7a522d891bc4c5f9})
+ expect(commits.map(&:id)).to eq(%w[646ece5cfed840eca0a4feb21bcd6a81bb19bda3 29284d9bcc350bcae005872d0be6edd016e2efb5 5f82584f0a907f3b30cfce5bb8df371454a90051 8a994512e8c8f0dfcf22bb16df6e876be7a61036 689600b91aabec706e657e38ea706ece1ee8268f db46a1c5a5e474aa169b6cdb7a522d891bc4c5f9])
original_method.call(commits)
end
diff --git a/spec/services/merge_requests/update_reviewer_state_service_spec.rb b/spec/services/merge_requests/update_reviewer_state_service_spec.rb
new file mode 100644
index 00000000000..be24d95d7f1
--- /dev/null
+++ b/spec/services/merge_requests/update_reviewer_state_service_spec.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe MergeRequests::UpdateReviewerStateService, feature_category: :code_review_workflow do
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:merge_request) { create(:merge_request, reviewers: [current_user]) }
+ let(:reviewer) { merge_request.merge_request_reviewers.find_by(user_id: current_user.id) }
+ let(:project) { merge_request.project }
+ let(:service) { described_class.new(project: project, current_user: current_user) }
+ let(:state) { 'requested_changes' }
+ let(:result) { service.execute(merge_request, state) }
+
+ before do
+ project.add_developer(current_user)
+ end
+
+ describe '#execute' do
+ shared_examples_for 'failed service execution' do
+ it 'returns an error' do
+ expect(result[:status]).to eq :error
+ end
+
+ it_behaves_like 'does not trigger GraphQL subscription mergeRequestReviewersUpdated' do
+ let(:action) { result }
+ end
+ end
+
+ describe 'invalid permissions' do
+ let(:service) { described_class.new(project: project, current_user: create(:user)) }
+
+ it_behaves_like 'failed service execution'
+ end
+
+ describe 'reviewer exists' do
+ it 'returns success' do
+ expect(result[:status]).to eq :success
+ end
+
+ it 'updates reviewers state' do
+ expect(result[:status]).to eq :success
+ expect(reviewer.state).to eq 'requested_changes'
+ end
+
+ it 'does not call MergeRequests::RemoveApprovalService' do
+ expect(MergeRequests::RemoveApprovalService).not_to receive(:new)
+
+ expect(result[:status]).to eq :success
+ end
+
+ it_behaves_like 'triggers GraphQL subscription mergeRequestReviewersUpdated' do
+ let(:action) { result }
+ end
+
+ context 'when reviewer has approved' do
+ before do
+ create(:approval, user: current_user, merge_request: merge_request)
+ end
+
+ it 'removes approval when state is requested_changes' do
+ expect_next_instance_of(
+ MergeRequests::RemoveApprovalService,
+ project: project, current_user: current_user
+ ) do |service|
+ expect(service).to receive(:execute).with(merge_request).and_return({ success: true })
+ end
+
+ expect(result[:status]).to eq :success
+ end
+
+ it 'renders error when remove approval service fails' do
+ expect_next_instance_of(
+ MergeRequests::RemoveApprovalService,
+ project: project, current_user: current_user
+ ) do |service|
+ expect(service).to receive(:execute).with(merge_request).and_return(nil)
+ end
+
+ expect(result[:status]).to eq :error
+ expect(result[:message]).to eq "Failed to remove approval"
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index f5494f429c3..53dd4263770 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -351,10 +351,10 @@ RSpec.describe MergeRequests::UpdateService, :mailer, feature_category: :code_re
end
it 'creates system note about discussion lock' do
- note = find_note('locked this merge request')
+ note = find_note('locked the discussion in this merge request')
expect(note).not_to be_nil
- expect(note.note).to eq 'locked this merge request'
+ expect(note.note).to eq 'locked the discussion in this merge request'
end
context 'when current user cannot admin issues in the project' do
diff --git a/spec/services/ml/create_candidate_service_spec.rb b/spec/services/ml/create_candidate_service_spec.rb
new file mode 100644
index 00000000000..fb3456b0bcc
--- /dev/null
+++ b/spec/services/ml/create_candidate_service_spec.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::Ml::CreateCandidateService, feature_category: :mlops do
+ describe '#execute' do
+ let_it_be(:model_version) { create(:ml_model_versions) }
+ let_it_be(:experiment) { create(:ml_experiments, project: model_version.project) }
+
+ let(:params) { {} }
+
+ subject(:candidate) { described_class.new(experiment, params).execute }
+
+ context 'with default parameters' do
+ it 'creates a candidate' do
+ expect { candidate }.to change { experiment.candidates.count }.by(1)
+ end
+
+ it 'gives a fake name' do
+ expect(candidate.name).to match(/[a-z]+-[a-z]+-[a-z]+-\d+/)
+ end
+
+ it 'sets the correct values', :aggregate_failures do
+ expect(candidate.start_time).to eq(0)
+ expect(candidate.experiment).to be(experiment)
+ expect(candidate.project).to be(experiment.project)
+ expect(candidate.user).to be_nil
+ end
+ end
+
+ context 'when parameters are passed' do
+ let(:params) do
+ {
+ start_time: 1234,
+ name: 'candidate_name',
+ model_version: model_version,
+ user: experiment.user
+ }
+ end
+
+ context 'with default parameters' do
+ it 'creates a candidate' do
+ expect { candidate }.to change { experiment.candidates.count }.by(1)
+ end
+
+ it 'sets the correct values', :aggregate_failures do
+ expect(candidate.start_time).to eq(1234)
+ expect(candidate.experiment).to be(experiment)
+ expect(candidate.project).to be(experiment.project)
+ expect(candidate.user).to be(experiment.user)
+ expect(candidate.name).to eq('candidate_name')
+ expect(candidate.model_version_id).to eq(model_version.id)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/ml/create_model_service_spec.rb b/spec/services/ml/create_model_service_spec.rb
new file mode 100644
index 00000000000..212f0940635
--- /dev/null
+++ b/spec/services/ml/create_model_service_spec.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::Ml::CreateModelService, feature_category: :mlops do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:existing_model) { create(:ml_models) }
+ let_it_be(:another_project) { create(:project) }
+ let_it_be(:description) { 'description' }
+ let_it_be(:metadata) { [] }
+
+ subject(:create_model) { described_class.new(project, name, user, description, metadata).execute }
+
+ describe '#execute' do
+ context 'when model name does not exist in the project' do
+ let(:name) { 'new_model' }
+ let(:project) { existing_model.project }
+
+ it 'creates a model', :aggregate_failures do
+ expect { create_model }.to change { Ml::Model.count }.by(1)
+
+ expect(create_model.name).to eq(name)
+ end
+ end
+
+ context 'when model name exists but project is different' do
+ let(:name) { existing_model.name }
+ let(:project) { another_project }
+
+ it 'creates a model', :aggregate_failures do
+ expect { create_model }.to change { Ml::Model.count }.by(1)
+
+ expect(create_model.name).to eq(name)
+ end
+ end
+
+ context 'when model with name exists' do
+ let(:name) { existing_model.name }
+ let(:project) { existing_model.project }
+
+ it 'raises an error', :aggregate_failures do
+ expect { create_model }.to raise_error(ActiveRecord::RecordInvalid)
+ end
+ end
+
+ context 'when metadata are supplied, add them as metadata' do
+ let(:name) { 'new_model' }
+ let(:project) { existing_model.project }
+ let(:metadata) { [{ key: 'key1', value: 'value1' }, { key: 'key2', value: 'value2' }] }
+
+ it 'creates metadata records', :aggregate_failures do
+ expect { create_model }.to change { Ml::Model.count }.by(1)
+
+ expect(create_model.name).to eq(name)
+ expect(create_model.metadata.count).to be 2
+ end
+ end
+
+ # TODO: Ensure consisted error responses https://gitlab.com/gitlab-org/gitlab/-/issues/429731
+ context 'for metadata with duplicate keys, it does not create duplicate records' do
+ let(:name) { 'new_model' }
+ let(:project) { existing_model.project }
+ let(:metadata) { [{ key: 'key1', value: 'value1' }, { key: 'key1', value: 'value2' }] }
+
+ it 'raises an error', :aggregate_failures do
+ expect { create_model }.to raise_error(ActiveRecord::RecordInvalid)
+ end
+ end
+
+ # TODO: Ensure consisted error responses https://gitlab.com/gitlab-org/gitlab/-/issues/429731
+ context 'for metadata with invalid keys, it does not create invalid records' do
+ let(:name) { 'new_model' }
+ let(:project) { existing_model.project }
+ let(:metadata) { [{ key: 'key1', value: 'value1' }, { key: '', value: 'value2' }] }
+
+ it 'raises an error', :aggregate_failures do
+ expect { create_model }.to raise_error(ActiveRecord::RecordInvalid)
+ end
+ end
+ end
+end
diff --git a/spec/services/ml/find_model_service_spec.rb b/spec/services/ml/find_model_service_spec.rb
new file mode 100644
index 00000000000..027298d979a
--- /dev/null
+++ b/spec/services/ml/find_model_service_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::Ml::FindModelService, feature_category: :mlops do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:existing_model) { create(:ml_models) }
+ let(:finder) { described_class.new(project, name) }
+
+ describe '#execute' do
+ context 'when model name does not exist in the project' do
+ let(:name) { 'new_model' }
+ let(:project) { existing_model.project }
+
+ it 'reutrns nil' do
+ expect(finder.execute).to be nil
+ end
+ end
+
+ context 'when model with name exists' do
+ let(:name) { existing_model.name }
+ let(:project) { existing_model.project }
+
+ it 'returns the existing model' do
+ expect(finder.execute).to eq(existing_model)
+ end
+ end
+ end
+end
diff --git a/spec/services/ml/find_or_create_model_service_spec.rb b/spec/services/ml/find_or_create_model_service_spec.rb
index 6ddae20f8d6..5d5eaea0a72 100644
--- a/spec/services/ml/find_or_create_model_service_spec.rb
+++ b/spec/services/ml/find_or_create_model_service_spec.rb
@@ -3,10 +3,13 @@
require 'spec_helper'
RSpec.describe ::Ml::FindOrCreateModelService, feature_category: :mlops do
+ let_it_be(:user) { create(:user) }
let_it_be(:existing_model) { create(:ml_models) }
let_it_be(:another_project) { create(:project) }
+ let_it_be(:description) { 'description' }
+ let_it_be(:metadata) { [] }
- subject(:create_model) { described_class.new(project, name).execute }
+ subject(:create_model) { described_class.new(project, name, user, description, metadata).execute }
describe '#execute' do
context 'when model name does not exist in the project' do
diff --git a/spec/services/ml/find_or_create_model_version_service_spec.rb b/spec/services/ml/find_or_create_model_version_service_spec.rb
index 1211a9b1165..e5ca7c3a450 100644
--- a/spec/services/ml/find_or_create_model_version_service_spec.rb
+++ b/spec/services/ml/find_or_create_model_version_service_spec.rb
@@ -5,14 +5,18 @@ require 'spec_helper'
RSpec.describe ::Ml::FindOrCreateModelVersionService, feature_category: :mlops do
let_it_be(:existing_version) { create(:ml_model_versions) }
let_it_be(:another_project) { create(:project) }
+ let_it_be(:user) { create(:user) }
let(:package) { nil }
+ let(:description) { nil }
let(:params) do
{
model_name: name,
version: version,
- package: package
+ package: package,
+ description: description,
+ user: user
}
end
@@ -26,6 +30,7 @@ RSpec.describe ::Ml::FindOrCreateModelVersionService, feature_category: :mlops d
it 'returns existing model version', :aggregate_failures do
expect { model_version }.to change { Ml::ModelVersion.count }.by(0)
+ expect { model_version }.to change { Ml::Candidate.count }.by(0)
expect(model_version).to eq(existing_version)
end
end
@@ -34,15 +39,18 @@ RSpec.describe ::Ml::FindOrCreateModelVersionService, feature_category: :mlops d
let(:project) { existing_version.project }
let(:name) { 'a_new_model' }
let(:version) { '2.0.0' }
+ let(:description) { 'A model version' }
let(:package) { create(:ml_model_package, project: project, name: name, version: version) }
it 'creates a new model version', :aggregate_failures do
- expect { model_version }.to change { Ml::ModelVersion.count }
+ expect { model_version }.to change { Ml::ModelVersion.count }.by(1).and change { Ml::Candidate.count }.by(1)
expect(model_version.name).to eq(name)
expect(model_version.version).to eq(version)
expect(model_version.package).to eq(package)
+ expect(model_version.candidate.model_version_id).to eq(model_version.id)
+ expect(model_version.description).to eq(description)
end
end
end
diff --git a/spec/services/ml/model_versions/get_model_version_service_spec.rb b/spec/services/ml/model_versions/get_model_version_service_spec.rb
new file mode 100644
index 00000000000..b46a0bf6d1d
--- /dev/null
+++ b/spec/services/ml/model_versions/get_model_version_service_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ml::ModelVersions::GetModelVersionService, feature_category: :mlops do
+ let_it_be(:existing_version) { create(:ml_model_versions) }
+ let_it_be(:another_project) { create(:project) }
+
+ subject(:model_version) { described_class.new(project, name, version).execute }
+
+ describe '#execute' do
+ context 'when model version exists' do
+ let(:name) { existing_version.name }
+ let(:version) { existing_version.version }
+ let(:project) { existing_version.project }
+
+ it { is_expected.to eq(existing_version) }
+ end
+
+ context 'when model version does not exist' do
+ let(:project) { existing_version.project }
+ let(:name) { 'a_new_model' }
+ let(:version) { '2.0.0' }
+
+ it { is_expected.to be_nil }
+ end
+ end
+end
diff --git a/spec/services/ml/update_model_service_spec.rb b/spec/services/ml/update_model_service_spec.rb
new file mode 100644
index 00000000000..35df62559e6
--- /dev/null
+++ b/spec/services/ml/update_model_service_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::Ml::UpdateModelService, feature_category: :mlops do
+ let_it_be(:model) { create(:ml_models) }
+ let_it_be(:description) { 'updated model description' }
+ let(:service) { described_class.new(model, description) }
+
+ describe '#execute' do
+ context 'when supplied with a non-model object' do
+ let(:model) { nil }
+
+ it 'returns nil' do
+ expect { service.execute }.to raise_error(NoMethodError)
+ end
+ end
+
+ context 'with an existing model' do
+ it 'updates the description' do
+ updated = service.execute
+ expect(updated.class).to be(Ml::Model)
+ expect(updated.description).to eq(description)
+ end
+ end
+ end
+end
diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb
index 0cc66696184..c1b15ec7681 100644
--- a/spec/services/notes/create_service_spec.rb
+++ b/spec/services/notes/create_service_spec.rb
@@ -3,7 +3,8 @@
require 'spec_helper'
RSpec.describe Notes::CreateService, feature_category: :team_planning do
- let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, :repository, group: group) }
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:user) { create(:user) }
@@ -13,11 +14,43 @@ RSpec.describe Notes::CreateService, feature_category: :team_planning do
describe '#execute' do
subject(:note) { described_class.new(project, user, opts).execute }
- before do
- project.add_maintainer(user)
+ before_all do
+ group.add_maintainer(user)
end
context "valid params" do
+ context 'when noteable is an issue that belongs directly to a group' do
+ it 'creates a note without a project and correct namespace', :aggregate_failures do
+ group_issue = create(:issue, :group_level, namespace: group)
+ note_params = { note: 'test note', noteable: group_issue }
+
+ expect do
+ described_class.new(nil, user, note_params).execute
+ end.to change { Note.count }.by(1)
+
+ created_note = Note.last
+
+ expect(created_note.namespace).to eq(group)
+ expect(created_note.project).to be_nil
+ end
+ end
+
+ context 'when noteable is a work item that belongs directly to a group' do
+ it 'creates a note without a project and correct namespace', :aggregate_failures do
+ group_work_item = create(:work_item, :group_level, namespace: group)
+ note_params = { note: 'test note', noteable: group_work_item }
+
+ expect do
+ described_class.new(nil, user, note_params).execute
+ end.to change { Note.count }.by(1)
+
+ created_note = Note.last
+
+ expect(created_note.namespace).to eq(group)
+ expect(created_note.project).to be_nil
+ end
+ end
+
it_behaves_like 'does not trigger GraphQL subscription mergeRequestMergeStatusUpdated' do
let(:action) { note }
end
@@ -195,21 +228,35 @@ RSpec.describe Notes::CreateService, feature_category: :team_planning do
let(:new_opts) { opts.merge(noteable_type: 'MergeRequest', noteable_id: merge_request.id) }
- it 'calls MergeRequests::MarkReviewerReviewedService service' do
- expect_next_instance_of(
- MergeRequests::MarkReviewerReviewedService,
- project: project_with_repo, current_user: user
- ) do |service|
- expect(service).to receive(:execute).with(merge_request)
+ context 'when mr_request_changes feature flag is disabled' do
+ before do
+ stub_feature_flags(mr_request_changes: false)
end
- described_class.new(project_with_repo, user, new_opts).execute
+ it 'calls MergeRequests::UpdateReviewerStateService service' do
+ expect_next_instance_of(
+ MergeRequests::UpdateReviewerStateService,
+ project: project_with_repo, current_user: user
+ ) do |service|
+ expect(service).to receive(:execute).with(merge_request, "reviewed")
+ end
+
+ described_class.new(project_with_repo, user, new_opts).execute
+ end
+
+ it 'does not call MergeRequests::UpdateReviewerStateService service when skip_set_reviewed is true' do
+ expect(MergeRequests::UpdateReviewerStateService).not_to receive(:new)
+
+ described_class.new(project_with_repo, user, new_opts).execute(skip_set_reviewed: true)
+ end
end
- it 'does not call MergeRequests::MarkReviewerReviewedService service when skip_set_reviewed is true' do
- expect(MergeRequests::MarkReviewerReviewedService).not_to receive(:new)
+ context 'when mr_request_changes feature flag is enabled' do
+ it 'does not call MergeRequests::UpdateReviewerStateService service when skip_set_reviewed is true' do
+ expect(MergeRequests::UpdateReviewerStateService).not_to receive(:new)
- described_class.new(project_with_repo, user, new_opts).execute(skip_set_reviewed: true)
+ described_class.new(project_with_repo, user, new_opts).execute(skip_set_reviewed: true)
+ end
end
context 'noteable highlight cache clearing' do
diff --git a/spec/services/organizations/create_service_spec.rb b/spec/services/organizations/create_service_spec.rb
new file mode 100644
index 00000000000..7d9bf64ddd3
--- /dev/null
+++ b/spec/services/organizations/create_service_spec.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Organizations::CreateService, feature_category: :cell do
+ describe '#execute' do
+ let_it_be(:user) { create(:user) }
+
+ let(:current_user) { user }
+ let(:params) { attributes_for(:organization) }
+
+ subject(:response) { described_class.new(current_user: current_user, params: params).execute }
+
+ context 'when user does not have permission' do
+ let(:current_user) { nil }
+
+ it 'returns an error' do
+ expect(response).to be_error
+
+ expect(response.message).to match_array(
+ ['You have insufficient permissions to create organizations'])
+ end
+ end
+
+ context 'when user has permission' do
+ it 'creates an organization' do
+ expect { response }.to change { Organizations::Organization.count }
+
+ expect(response).to be_success
+ end
+
+ it 'returns an error when the organization is not persisted' do
+ params[:name] = nil
+
+ expect(response).to be_error
+ expect(response.message).to match_array(["Name can't be blank"])
+ end
+ end
+ end
+end
diff --git a/spec/services/packages/create_dependency_service_spec.rb b/spec/services/packages/create_dependency_service_spec.rb
index 06a7a13bdd9..c50bf988899 100644
--- a/spec/services/packages/create_dependency_service_spec.rb
+++ b/spec/services/packages/create_dependency_service_spec.rb
@@ -33,8 +33,8 @@ RSpec.describe Packages::CreateDependencyService, feature_category: :package_reg
expect { subject }
.to change { Packages::Dependency.count }.by(1)
.and change { Packages::DependencyLink.count }.by(1)
- expect(dependency_names).to match_array(%w(express))
- expect(dependency_link_types).to match_array(%w(dependencies))
+ expect(dependency_names).to match_array(%w[express])
+ expect(dependency_link_types).to match_array(%w[dependencies])
end
context 'with repeated packages' do
@@ -49,8 +49,8 @@ RSpec.describe Packages::CreateDependencyService, feature_category: :package_reg
expect { subject }
.to change { Packages::Dependency.count }.by(4)
.and change { Packages::DependencyLink.count }.by(6)
- expect(dependency_names).to match_array(%w(d3 d3 d3 dagre-d3 dagre-d3 express))
- expect(dependency_link_types).to match_array(%w(bundleDependencies dependencies dependencies devDependencies devDependencies peerDependencies))
+ expect(dependency_names).to match_array(%w[d3 d3 d3 dagre-d3 dagre-d3 express])
+ expect(dependency_link_types).to match_array(%w[bundleDependencies dependencies dependencies devDependencies devDependencies peerDependencies])
end
end
@@ -72,8 +72,8 @@ RSpec.describe Packages::CreateDependencyService, feature_category: :package_reg
expect { subject }
.to change { Packages::Dependency.count }.by(1)
.and change { Packages::DependencyLink.count }.by(1)
- expect(dependency_names).to match_array(%w(express))
- expect(dependency_link_types).to match_array(%w(dependencies))
+ expect(dependency_names).to match_array(%w[express])
+ expect(dependency_link_types).to match_array(%w[dependencies])
end
end
@@ -105,8 +105,8 @@ RSpec.describe Packages::CreateDependencyService, feature_category: :package_reg
expect { subject }
.to change { Packages::Dependency.count }.by(1)
.and change { Packages::DependencyLink.count }.by(1)
- expect(dependency_names).to match_array(%w(express))
- expect(dependency_link_types).to match_array(%w(dependencies))
+ expect(dependency_names).to match_array(%w[express])
+ expect(dependency_link_types).to match_array(%w[dependencies])
end
end
end
diff --git a/spec/services/packages/npm/create_package_service_spec.rb b/spec/services/packages/npm/create_package_service_spec.rb
index 1c935c27d7f..867dc582771 100644
--- a/spec/services/packages/npm/create_package_service_spec.rb
+++ b/spec/services/packages/npm/create_package_service_spec.rb
@@ -2,177 +2,179 @@
require 'spec_helper'
RSpec.describe Packages::Npm::CreatePackageService, feature_category: :package_registry do
- include ExclusiveLeaseHelpers
-
- let(:namespace) { create(:namespace) }
- let(:project) { create(:project, namespace: namespace) }
- let(:user) { project.owner }
- let(:version) { '1.0.1' }
-
- let(:params) do
- Gitlab::Json.parse(fixture_file('packages/npm/payload.json')
- .gsub('@root/npm-test', package_name)
- .gsub('1.0.1', version)).with_indifferent_access
- end
-
- let(:package_name) { "@#{namespace.path}/my-app" }
- let(:version_data) { params.dig('versions', version) }
- let(:lease_key) { "packages:npm:create_package_service:packages:#{project.id}_#{package_name}_#{version}" }
let(:service) { described_class.new(project, user, params) }
subject { service.execute }
- shared_examples 'valid package' do
- it 'creates a package' do
- expect { subject }
- .to change { Packages::Package.count }.by(1)
- .and change { Packages::Package.npm.count }.by(1)
- .and change { Packages::Tag.count }.by(1)
- .and change { Packages::Npm::Metadatum.count }.by(1)
- end
-
- it_behaves_like 'assigns the package creator' do
- let(:package) { subject }
- end
+ describe '#execute' do
+ include ExclusiveLeaseHelpers
- it { is_expected.to be_valid }
+ let_it_be(:namespace) { create(:namespace) }
+ let_it_be_with_reload(:project) { create(:project, namespace: namespace) }
+ let_it_be(:user) { project.owner }
- it 'creates a package with name and version' do
- package = subject
+ let(:version) { '1.0.1' }
- expect(package.name).to eq(package_name)
- expect(package.version).to eq(version)
+ let(:params) do
+ Gitlab::Json.parse(fixture_file('packages/npm/payload.json')
+ .gsub('@root/npm-test', package_name)
+ .gsub('1.0.1', version)).with_indifferent_access
end
- it { expect(subject.npm_metadatum.package_json).to eq(version_data) }
+ let(:package_name) { "@#{namespace.path}/my-app" }
+ let(:version_data) { params.dig('versions', version) }
+ let(:lease_key) { "packages:npm:create_package_service:packages:#{project.id}_#{package_name}_#{version}" }
+
+ shared_examples 'valid package' do
+ it 'creates a package' do
+ expect { subject }
+ .to change { Packages::Package.count }.by(1)
+ .and change { Packages::Package.npm.count }.by(1)
+ .and change { Packages::Tag.count }.by(1)
+ .and change { Packages::Npm::Metadatum.count }.by(1)
+ end
- it { expect(subject.name).to eq(package_name) }
- it { expect(subject.version).to eq(version) }
+ it_behaves_like 'assigns the package creator' do
+ let(:package) { subject }
+ end
- context 'with build info' do
- let(:job) { create(:ci_build, user: user) }
- let(:params) { super().merge(build: job) }
+ it { is_expected.to be_valid }
- it_behaves_like 'assigns build to package'
- it_behaves_like 'assigns status to package'
+ it 'creates a package with name and version' do
+ package = subject
- it 'creates a package file build info' do
- expect { subject }.to change { Packages::PackageFileBuildInfo.count }.by(1)
+ expect(package.name).to eq(package_name)
+ expect(package.version).to eq(version)
end
- end
- context 'when the npm metadatum creation results in a size error' do
- shared_examples 'a package json structure size too large error' do
- it 'does not create the package' do
- expect(Gitlab::ErrorTracking).to receive(:track_exception).with(
- instance_of(ActiveRecord::RecordInvalid),
- field_sizes: expected_field_sizes
- )
+ it { expect(subject.npm_metadatum.package_json).to eq(version_data) }
- expect { subject }.to raise_error(ActiveRecord::RecordInvalid, /structure is too large/)
- .and not_change { Packages::Package.count }
- .and not_change { Packages::Package.npm.count }
- .and not_change { Packages::Tag.count }
- .and not_change { Packages::Npm::Metadatum.count }
- end
- end
+ it { expect(subject.name).to eq(package_name) }
+ it { expect(subject.version).to eq(version) }
- context 'when some of the field sizes are above the error tracking size' do
- let(:package_json) do
- params[:versions][version].except(*::Packages::Npm::CreatePackageService::PACKAGE_JSON_NOT_ALLOWED_FIELDS)
- end
+ context 'with build info' do
+ let_it_be(:job) { create(:ci_build, user: user) }
+ let(:params) { super().merge(build: job) }
- # Only the fields that exceed the field size limit should be passed to error tracking
- let(:expected_field_sizes) do
- {
- 'test' => ('test' * 10000).size,
- 'field2' => ('a' * (::Packages::Npm::Metadatum::MIN_PACKAGE_JSON_FIELD_SIZE_FOR_ERROR_TRACKING + 1)).size
- }
- end
+ it_behaves_like 'assigns build to package'
+ it_behaves_like 'assigns status to package'
- before do
- params[:versions][version][:test] = 'test' * 10000
- params[:versions][version][:field1] =
- 'a' * (::Packages::Npm::Metadatum::MIN_PACKAGE_JSON_FIELD_SIZE_FOR_ERROR_TRACKING - 1)
- params[:versions][version][:field2] =
- 'a' * (::Packages::Npm::Metadatum::MIN_PACKAGE_JSON_FIELD_SIZE_FOR_ERROR_TRACKING + 1)
+ it 'creates a package file build info' do
+ expect { subject }.to change { Packages::PackageFileBuildInfo.count }.by(1)
end
-
- it_behaves_like 'a package json structure size too large error'
end
- context 'when all of the field sizes are below the error tracking size' do
- let(:package_json) do
- params[:versions][version].except(*::Packages::Npm::CreatePackageService::PACKAGE_JSON_NOT_ALLOWED_FIELDS)
+ context 'when the npm metadatum creation results in a size error' do
+ shared_examples 'a package json structure size too large error' do
+ it 'does not create the package' do
+ expect(Gitlab::ErrorTracking).to receive(:track_exception).with(
+ instance_of(ActiveRecord::RecordInvalid),
+ field_sizes: expected_field_sizes
+ )
+
+ expect { subject }.to raise_error(ActiveRecord::RecordInvalid, /structure is too large/)
+ .and not_change { Packages::Package.count }
+ .and not_change { Packages::Package.npm.count }
+ .and not_change { Packages::Tag.count }
+ .and not_change { Packages::Npm::Metadatum.count }
+ end
end
- let(:expected_size) { ('a' * (::Packages::Npm::Metadatum::MIN_PACKAGE_JSON_FIELD_SIZE_FOR_ERROR_TRACKING - 1)).size }
- # Only the five largest fields should be passed to error tracking
- let(:expected_field_sizes) do
- {
- 'field1' => expected_size,
- 'field2' => expected_size,
- 'field3' => expected_size,
- 'field4' => expected_size,
- 'field5' => expected_size
- }
- end
+ context 'when some of the field sizes are above the error tracking size' do
+ let(:package_json) do
+ params[:versions][version].except(*::Packages::Npm::CreatePackageService::PACKAGE_JSON_NOT_ALLOWED_FIELDS)
+ end
- before do
- 5.times do |i|
- params[:versions][version]["field#{i + 1}"] =
+ # Only the fields that exceed the field size limit should be passed to error tracking
+ let(:expected_field_sizes) do
+ {
+ 'test' => ('test' * 10000).size,
+ 'field2' => ('a' * (::Packages::Npm::Metadatum::MIN_PACKAGE_JSON_FIELD_SIZE_FOR_ERROR_TRACKING + 1)).size
+ }
+ end
+
+ before do
+ params[:versions][version][:test] = 'test' * 10000
+ params[:versions][version][:field1] =
'a' * (::Packages::Npm::Metadatum::MIN_PACKAGE_JSON_FIELD_SIZE_FOR_ERROR_TRACKING - 1)
+ params[:versions][version][:field2] =
+ 'a' * (::Packages::Npm::Metadatum::MIN_PACKAGE_JSON_FIELD_SIZE_FOR_ERROR_TRACKING + 1)
end
+
+ it_behaves_like 'a package json structure size too large error'
end
- it_behaves_like 'a package json structure size too large error'
- end
- end
+ context 'when all of the field sizes are below the error tracking size' do
+ let(:package_json) do
+ params[:versions][version].except(*::Packages::Npm::CreatePackageService::PACKAGE_JSON_NOT_ALLOWED_FIELDS)
+ end
- context 'when the npm metadatum creation results in a different error' do
- it 'does not track the error' do
- error_message = 'boom'
- invalid_npm_metadatum_error = ActiveRecord::RecordInvalid.new(
- build(:npm_metadatum).tap do |metadatum|
- metadatum.errors.add(:base, error_message)
+ let(:expected_size) { ('a' * (::Packages::Npm::Metadatum::MIN_PACKAGE_JSON_FIELD_SIZE_FOR_ERROR_TRACKING - 1)).size }
+ # Only the five largest fields should be passed to error tracking
+ let(:expected_field_sizes) do
+ {
+ 'field1' => expected_size,
+ 'field2' => expected_size,
+ 'field3' => expected_size,
+ 'field4' => expected_size,
+ 'field5' => expected_size
+ }
end
- )
- allow_next_instance_of(::Packages::Package) do |package|
- allow(package).to receive(:create_npm_metadatum!).and_raise(invalid_npm_metadatum_error)
+ before do
+ 5.times do |i|
+ params[:versions][version]["field#{i + 1}"] =
+ 'a' * (::Packages::Npm::Metadatum::MIN_PACKAGE_JSON_FIELD_SIZE_FOR_ERROR_TRACKING - 1)
+ end
+ end
+
+ it_behaves_like 'a package json structure size too large error'
end
+ end
- expect(Gitlab::ErrorTracking).not_to receive(:track_exception)
+ context 'when the npm metadatum creation results in a different error' do
+ it 'does not track the error' do
+ error_message = 'boom'
+ invalid_npm_metadatum_error = ActiveRecord::RecordInvalid.new(
+ build(:npm_metadatum).tap do |metadatum|
+ metadatum.errors.add(:base, error_message)
+ end
+ )
- expect { subject }.to raise_error(ActiveRecord::RecordInvalid, /#{error_message}/)
- end
- end
+ allow_next_instance_of(::Packages::Package) do |package|
+ allow(package).to receive(:create_npm_metadatum!).and_raise(invalid_npm_metadatum_error)
+ end
- described_class::PACKAGE_JSON_NOT_ALLOWED_FIELDS.each do |field|
- context "with not allowed #{field} field" do
- before do
- params[:versions][version][field] = 'test'
+ expect(Gitlab::ErrorTracking).not_to receive(:track_exception)
+
+ expect { subject }.to raise_error(ActiveRecord::RecordInvalid, /#{error_message}/)
end
+ end
- it 'is persisted without the field' do
- expect { subject }
- .to change { Packages::Package.count }.by(1)
- .and change { Packages::Package.npm.count }.by(1)
- .and change { Packages::Tag.count }.by(1)
- .and change { Packages::Npm::Metadatum.count }.by(1)
- expect(subject.npm_metadatum.package_json[field]).to be_blank
+ described_class::PACKAGE_JSON_NOT_ALLOWED_FIELDS.each do |field|
+ context "with not allowed #{field} field" do
+ before do
+ params[:versions][version][field] = 'test'
+ end
+
+ it 'is persisted without the field' do
+ expect { subject }
+ .to change { Packages::Package.count }.by(1)
+ .and change { Packages::Package.npm.count }.by(1)
+ .and change { Packages::Tag.count }.by(1)
+ .and change { Packages::Npm::Metadatum.count }.by(1)
+ expect(subject.npm_metadatum.package_json[field]).to be_blank
+ end
end
end
end
- end
- describe '#execute' do
context 'scoped package' do
it_behaves_like 'valid package'
end
context 'when user is no project member' do
- let(:user) { create(:user) }
+ let_it_be(:user) { create(:user) }
it_behaves_like 'valid package'
end
@@ -320,13 +322,111 @@ RSpec.describe Packages::Npm::CreatePackageService, feature_category: :package_r
it { expect(subject[:message]).to eq 'Could not obtain package lease. Please try again.' }
end
- context 'when many of the same packages are created at the same time', :delete do
- it 'only creates one package' do
- expect { create_packages(project, user, params) }.to change { Packages::Package.count }.by(1)
+ context 'when feature flag :packages_package_protection is disabled' do
+ let_it_be_with_reload(:package_protection_rule) { create(:package_protection_rule, package_type: :npm, project: project) }
+
+ before do
+ stub_feature_flags(packages_protected_packages: false)
+ end
+
+ context 'with matching package protection rule for all roles' do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:package_name_pattern_no_match) { "#{package_name}_no_match" }
+
+ where(:package_name_pattern, :push_protected_up_to_access_level) do
+ ref(:package_name) | :developer
+ ref(:package_name) | :owner
+ ref(:package_name_pattern_no_match) | :developer
+ ref(:package_name_pattern_no_match) | :owner
+ end
+
+ with_them do
+ before do
+ package_protection_rule.update!(package_name_pattern: package_name_pattern, push_protected_up_to_access_level: push_protected_up_to_access_level)
+ end
+
+ it_behaves_like 'valid package'
+ end
+ end
+ end
+
+ context 'with package protection rule for different roles and package_name_patterns' do
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be_with_reload(:package_protection_rule) { create(:package_protection_rule, package_type: :npm, project: project) }
+ let_it_be(:project_developer) { create(:user).tap { |u| project.add_developer(u) } }
+ let_it_be(:project_maintainer) { create(:user).tap { |u| project.add_maintainer(u) } }
+
+ let(:project_owner) { project.owner }
+ let(:package_name_pattern_no_match) { "#{package_name}_no_match" }
+
+ let(:service) { described_class.new(project, current_user, params) }
+
+ shared_examples 'protected package' do
+ it { is_expected.to include http_status: 403, message: 'Package protected.' }
+
+ it 'does not create any npm-related package records' do
+ expect { subject }
+ .to not_change { Packages::Package.count }
+ .and not_change { Packages::Package.npm.count }
+ .and not_change { Packages::Tag.count }
+ .and not_change { Packages::Npm::Metadatum.count }
+ end
+ end
+
+ where(:package_name_pattern, :push_protected_up_to_access_level, :current_user, :shared_examples_name) do
+ ref(:package_name) | :developer | ref(:project_developer) | 'protected package'
+ ref(:package_name) | :developer | ref(:project_owner) | 'valid package'
+ ref(:package_name) | :maintainer | ref(:project_maintainer) | 'protected package'
+ ref(:package_name) | :owner | ref(:project_owner) | 'protected package'
+ ref(:package_name_pattern_no_match) | :developer | ref(:project_owner) | 'valid package'
+ ref(:package_name_pattern_no_match) | :owner | ref(:project_owner) | 'valid package'
+ end
+
+ with_them do
+ before do
+ package_protection_rule.update!(package_name_pattern: package_name_pattern, push_protected_up_to_access_level: push_protected_up_to_access_level)
+ end
+
+ it_behaves_like params[:shared_examples_name]
+ end
+ end
+
+ describe '#lease_key' do
+ subject { service.send(:lease_key) }
+
+ it 'returns an unique key' do
+ is_expected.to eq lease_key
end
end
+ end
- context 'when many packages with different versions are created at the same time', :delete do
+ context 'when many of the same packages are created at the same time', :delete do
+ let(:namespace) { create(:namespace) }
+ let(:project) { create(:project, namespace: namespace) }
+ let(:user) { project.owner }
+
+ let(:version) { '1.0.1' }
+
+ let(:params) do
+ Gitlab::Json.parse(
+ fixture_file('packages/npm/payload.json')
+ .gsub('@root/npm-test', package_name)
+ .gsub('1.0.1', version)
+ ).with_indifferent_access
+ end
+
+ let(:package_name) { "@#{namespace.path}/my-app" }
+ let(:service) { described_class.new(project, user, params) }
+
+ subject { service.execute }
+
+ it 'only creates one package' do
+ expect { create_packages(project, user, params) }.to change { Packages::Package.count }.by(1)
+ end
+
+ context 'with different versions' do
it 'creates all packages' do
expect { create_packages_with_versions(project, user, params) }.to change { Packages::Package.count }.by(5)
end
@@ -367,12 +467,4 @@ RSpec.describe Packages::Npm::CreatePackageService, feature_category: :package_r
threads.each(&:join)
end
end
-
- describe '#lease_key' do
- subject { service.send(:lease_key) }
-
- it 'returns an unique key' do
- is_expected.to eq lease_key
- end
- end
end
diff --git a/spec/services/packages/npm/generate_metadata_service_spec.rb b/spec/services/packages/npm/generate_metadata_service_spec.rb
index d8e794405e6..f5d7f13d22c 100644
--- a/spec/services/packages/npm/generate_metadata_service_spec.rb
+++ b/spec/services/packages/npm/generate_metadata_service_spec.rb
@@ -44,7 +44,7 @@ RSpec.describe ::Packages::Npm::GenerateMetadataService, feature_category: :pack
with_them do
if params[:has_dependencies]
- ::Packages::DependencyLink.dependency_types.each_key do |dependency_type| # rubocop:disable RSpec/UselessDynamicDefinition
+ ::Packages::DependencyLink.dependency_types.each_key do |dependency_type| # rubocop:disable RSpec/UselessDynamicDefinition -- `dependency_type` used in `let_it_be`
let_it_be("package_dependency_link_for_#{dependency_type}") do
create(:packages_dependency_link, package: package1, dependency_type: dependency_type)
end
diff --git a/spec/services/packages/nuget/check_duplicates_service_spec.rb b/spec/services/packages/nuget/check_duplicates_service_spec.rb
index 9675aa5f5e2..6274036800a 100644
--- a/spec/services/packages/nuget/check_duplicates_service_spec.rb
+++ b/spec/services/packages/nuget/check_duplicates_service_spec.rb
@@ -3,8 +3,6 @@
require 'spec_helper'
RSpec.describe Packages::Nuget::CheckDuplicatesService, feature_category: :package_registry do
- include PackagesManagerApiSpecHelpers
-
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
let_it_be(:file_name) { 'package.nupkg' }
@@ -12,7 +10,7 @@ RSpec.describe Packages::Nuget::CheckDuplicatesService, feature_category: :packa
let(:params) do
{
file_name: file_name,
- file: temp_file(file_name)
+ file: File.open(expand_fixture_path('packages/nuget/package.nupkg'))
}
end
diff --git a/spec/services/packages/nuget/create_dependency_service_spec.rb b/spec/services/packages/nuget/create_dependency_service_spec.rb
index 10daec8b871..7e14779cb92 100644
--- a/spec/services/packages/nuget/create_dependency_service_spec.rb
+++ b/spec/services/packages/nuget/create_dependency_service_spec.rb
@@ -41,12 +41,12 @@ RSpec.describe Packages::Nuget::CreateDependencyService, feature_category: :pack
subject { service.execute }
- it_behaves_like 'creating dependencies, links and nuget metadata for', %w(Castle.Core Moqi Newtonsoft.Json Test.Dependency), 4, 4
+ it_behaves_like 'creating dependencies, links and nuget metadata for', %w[Castle.Core Moqi Newtonsoft.Json Test.Dependency], 4, 4
context 'with existing dependencies' do
let_it_be(:exisiting_dependency) { create(:packages_dependency, name: 'Moqi', version_pattern: '2.5.6') }
- it_behaves_like 'creating dependencies, links and nuget metadata for', %w(Castle.Core Moqi Newtonsoft.Json Test.Dependency), 3, 4
+ it_behaves_like 'creating dependencies, links and nuget metadata for', %w[Castle.Core Moqi Newtonsoft.Json Test.Dependency], 3, 4
end
context 'with dependencies with no target framework' do
@@ -59,7 +59,7 @@ RSpec.describe Packages::Nuget::CreateDependencyService, feature_category: :pack
]
end
- it_behaves_like 'creating dependencies, links and nuget metadata for', %w(Castle.Core Moqi Newtonsoft.Json Test.Dependency), 4, 4
+ it_behaves_like 'creating dependencies, links and nuget metadata for', %w[Castle.Core Moqi Newtonsoft.Json Test.Dependency], 4, 4
end
context 'with empty dependencies' do
diff --git a/spec/services/packages/nuget/extract_metadata_file_service_spec.rb b/spec/services/packages/nuget/extract_metadata_file_service_spec.rb
index 4c761826b53..ac5749c2dc4 100644
--- a/spec/services/packages/nuget/extract_metadata_file_service_spec.rb
+++ b/spec/services/packages/nuget/extract_metadata_file_service_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe Packages::Nuget::ExtractMetadataFileService, feature_category: :package_registry do
+ include PackagesManagerApiSpecHelpers
+
let_it_be(:package_file) { build(:package_file, :nuget) }
let_it_be(:package_zip_file) { Zip::File.new(package_file.file) }
@@ -38,6 +40,18 @@ RSpec.describe Packages::Nuget::ExtractMetadataFileService, feature_category: :p
it 'returns the nuspec file content' do
expect(subject.payload.squish).to include(expected_metadata)
end
+
+ context 'with InputStream zip' do
+ let(:package_zip_file) do
+ Zip::InputStream.open(
+ temp_file('package.nupkg', content: File.open(package_file.file.path))
+ )
+ end
+
+ it 'returns the nuspec file content' do
+ expect(subject.payload.squish).to include(expected_metadata)
+ end
+ end
end
context 'without the nuspec file' do
diff --git a/spec/services/packages/nuget/metadata_extraction_service_spec.rb b/spec/services/packages/nuget/metadata_extraction_service_spec.rb
index 81a4e4a430b..46d5449d52b 100644
--- a/spec/services/packages/nuget/metadata_extraction_service_spec.rb
+++ b/spec/services/packages/nuget/metadata_extraction_service_spec.rb
@@ -4,7 +4,8 @@ require 'spec_helper'
RSpec.describe Packages::Nuget::MetadataExtractionService, feature_category: :package_registry do
let_it_be(:package_file) { build(:package_file, :nuget) }
- let(:service) { described_class.new(package_file) }
+ let(:package_zip_file) { Zip::File.new(package_file.file) }
+ let(:service) { described_class.new(package_zip_file) }
describe '#execute' do
subject { service.execute }
@@ -50,8 +51,8 @@ RSpec.describe Packages::Nuget::MetadataExtractionService, feature_category: :pa
end
it 'calls the necessary services and executes the metadata extraction' do
- expect_next_instance_of(Packages::Nuget::ProcessPackageFileService, package_file) do |service|
- expect(service).to receive(:execute).and_return(ServiceResponse.success(payload: { nuspec_file_content: nuspec_file_content }))
+ expect_next_instance_of(Packages::Nuget::ExtractMetadataFileService, package_zip_file) do |service|
+ expect(service).to receive(:execute).and_return(ServiceResponse.success(payload: nuspec_file_content))
end
expect_next_instance_of(Packages::Nuget::ExtractMetadataContentService, nuspec_file_content) do |service|
diff --git a/spec/services/packages/nuget/process_package_file_service_spec.rb b/spec/services/packages/nuget/process_package_file_service_spec.rb
index cdeb5b32737..70e8bcb8c5c 100644
--- a/spec/services/packages/nuget/process_package_file_service_spec.rb
+++ b/spec/services/packages/nuget/process_package_file_service_spec.rb
@@ -14,25 +14,14 @@ RSpec.describe Packages::Nuget::ProcessPackageFileService, feature_category: :pa
it { expect { subject }.to raise_error(described_class::ExtractionError, error_message) }
end
- shared_examples 'not creating a symbol file' do
- it 'does not call the CreateSymbolFilesService' do
- expect(Packages::Nuget::Symbols::CreateSymbolFilesService).not_to receive(:new)
-
- expect(subject).to be_success
- end
- end
-
context 'with valid package file' do
- it 'calls the ExtractMetadataFileService' do
- expect_next_instance_of(Packages::Nuget::ExtractMetadataFileService, instance_of(Zip::File)) do |service|
- expect(service).to receive(:execute) do
- instance_double(ServiceResponse).tap do |response|
- expect(response).to receive(:payload).and_return(instance_of(String))
- end
- end
+ it 'calls the UpdatePackageFromMetadataService' do
+ expect_next_instance_of(Packages::Nuget::UpdatePackageFromMetadataService, package_file,
+ instance_of(Zip::File)) do |service|
+ expect(service).to receive(:execute)
end
- expect(subject).to be_success
+ subject
end
end
@@ -59,25 +48,5 @@ RSpec.describe Packages::Nuget::ProcessPackageFileService, feature_category: :pa
it_behaves_like 'raises an error', 'invalid package file'
end
-
- context 'with a symbol package file' do
- let(:package_file) { build(:package_file, :snupkg) }
-
- it 'calls the CreateSymbolFilesService' do
- expect_next_instance_of(
- Packages::Nuget::Symbols::CreateSymbolFilesService, package_file.package, instance_of(Zip::File)
- ) do |service|
- expect(service).to receive(:execute)
- end
-
- expect(subject).to be_success
- end
- end
-
- context 'with a non symbol package file' do
- let(:package_file) { build(:package_file, :nuget) }
-
- it_behaves_like 'not creating a symbol file'
- end
end
end
diff --git a/spec/services/packages/nuget/symbols/create_symbol_files_service_spec.rb b/spec/services/packages/nuget/symbols/create_symbol_files_service_spec.rb
index 97bfc3e06a8..96cc46884af 100644
--- a/spec/services/packages/nuget/symbols/create_symbol_files_service_spec.rb
+++ b/spec/services/packages/nuget/symbols/create_symbol_files_service_spec.rb
@@ -15,9 +15,9 @@ RSpec.describe Packages::Nuget::Symbols::CreateSymbolFilesService, feature_categ
describe '#execute' do
subject { service.execute }
- shared_examples 'logs an error' do |error_class|
- it 'logs an error' do
- expect(Gitlab::ErrorTracking).to receive(:log_exception).with(
+ shared_examples 'logging an error' do |error_class|
+ it 'logs the error' do
+ expect(Gitlab::ErrorTracking).to receive(:track_exception).with(
an_instance_of(error_class),
class: described_class.name,
package_id: package.id
@@ -29,8 +29,8 @@ RSpec.describe Packages::Nuget::Symbols::CreateSymbolFilesService, feature_categ
context 'when symbol files are found' do
it 'creates a symbol record and extracts the signature' do
- expect_next_instance_of(Packages::Nuget::Symbols::ExtractSymbolSignatureService,
- instance_of(String)) do |service|
+ expect_next_instance_of(Packages::Nuget::Symbols::ExtractSignatureAndChecksumService,
+ instance_of(File)) do |service|
expect(service).to receive(:execute).and_call_original
end
@@ -47,13 +47,13 @@ RSpec.describe Packages::Nuget::Symbols::CreateSymbolFilesService, feature_categ
expect { subject }.not_to change { package.nuget_symbols.count }
end
- it_behaves_like 'logs an error', described_class::ExtractionError
+ it_behaves_like 'logging an error', described_class::ExtractionError
end
- context 'when creating a symbol record without a signature' do
+ context 'without a signature' do
before do
- allow_next_instance_of(Packages::Nuget::Symbols::ExtractSymbolSignatureService) do |instance|
- allow(instance).to receive(:execute).and_return(ServiceResponse.success(payload: nil))
+ allow_next_instance_of(Packages::Nuget::Symbols::ExtractSignatureAndChecksumService) do |instance|
+ allow(instance).to receive(:execute).and_return(ServiceResponse.success(payload: { signature: nil }))
end
end
@@ -64,12 +64,28 @@ RSpec.describe Packages::Nuget::Symbols::CreateSymbolFilesService, feature_categ
end
end
- context 'when creating duplicate symbol records' do
+ context 'without a checksum' do
+ before do
+ allow_next_instance_of(Packages::Nuget::Symbols::ExtractSignatureAndChecksumService) do |instance|
+ allow(instance).to receive(:execute).and_return(ServiceResponse.success(payload: { checksum: nil }))
+ end
+ end
+
+ it 'does not call create! on the symbol record' do
+ expect(::Packages::Nuget::Symbol).not_to receive(:create!)
+
+ subject
+ end
+ end
+
+ context 'with existing duplicate symbol records' do
let_it_be(:symbol) { create(:nuget_symbol, package: package) }
before do
- allow_next_instance_of(Packages::Nuget::Symbols::ExtractSymbolSignatureService) do |instance|
- allow(instance).to receive(:execute).and_return(ServiceResponse.success(payload: symbol.signature))
+ allow_next_instance_of(Packages::Nuget::Symbols::ExtractSignatureAndChecksumService) do |instance|
+ allow(instance).to receive(:execute).and_return(
+ ServiceResponse.success(payload: { signature: symbol.signature, checksum: symbol.file_sha256 })
+ )
end
end
@@ -77,7 +93,7 @@ RSpec.describe Packages::Nuget::Symbols::CreateSymbolFilesService, feature_categ
expect { subject }.not_to change { package.nuget_symbols.count }
end
- it_behaves_like 'logs an error', ActiveRecord::RecordInvalid
+ it_behaves_like 'logging an error', ActiveRecord::RecordInvalid
end
context 'when a symbol file has the wrong entry size' do
@@ -87,7 +103,7 @@ RSpec.describe Packages::Nuget::Symbols::CreateSymbolFilesService, feature_categ
end
end
- it_behaves_like 'logs an error', described_class::ExtractionError
+ it_behaves_like 'logging an error', described_class::ExtractionError
end
context 'when a symbol file has the wrong entry name' do
@@ -97,7 +113,7 @@ RSpec.describe Packages::Nuget::Symbols::CreateSymbolFilesService, feature_categ
end
end
- it_behaves_like 'logs an error', described_class::ExtractionError
+ it_behaves_like 'logging an error', described_class::ExtractionError
end
end
end
diff --git a/spec/services/packages/nuget/symbols/extract_signature_and_checksum_service_spec.rb b/spec/services/packages/nuget/symbols/extract_signature_and_checksum_service_spec.rb
new file mode 100644
index 00000000000..210b92dfce5
--- /dev/null
+++ b/spec/services/packages/nuget/symbols/extract_signature_and_checksum_service_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Packages::Nuget::Symbols::ExtractSignatureAndChecksumService, feature_category: :package_registry do
+ let_it_be(:symbol_file_path) { expand_fixture_path('packages/nuget/symbol/package.pdb') }
+ let(:symbol_file) { File.new(symbol_file_path) }
+
+ let(:service) { described_class.new(symbol_file) }
+
+ after do
+ symbol_file.close
+ end
+
+ describe '#execute' do
+ subject { service.execute }
+
+ context 'with a valid symbol file' do
+ it 'returns the signature and checksum' do
+ payload = subject.payload
+
+ expect(payload[:signature]).to eq('b91a152048fc4b3883bf3cf73fbc03f1FFFFFFFF')
+ expect(payload[:checksum]).to eq('20151ab9fc48384b83bf3cf73fbc03f1d49166cc356139845f290d1d315256c0')
+ end
+
+ it 'reads the file in chunks' do
+ expect(symbol_file).to receive(:read).with(described_class::GUID_CHUNK_SIZE).and_call_original
+ expect(symbol_file).to receive(:read).with(described_class::SHA_CHUNK_SIZE, instance_of(String))
+ .at_least(:once).and_call_original
+
+ subject
+ end
+ end
+
+ context 'with an invalid symbol file' do
+ before do
+ allow(symbol_file).to receive(:read).and_return('invalid')
+ end
+
+ it 'returns an error' do
+ expect(subject).to be_error
+ expect(subject.message).to eq('Could not find the signature in the symbol file')
+ end
+ end
+ end
+end
diff --git a/spec/services/packages/nuget/symbols/extract_symbol_signature_service_spec.rb b/spec/services/packages/nuget/symbols/extract_symbol_signature_service_spec.rb
deleted file mode 100644
index 87b0d00a0a7..00000000000
--- a/spec/services/packages/nuget/symbols/extract_symbol_signature_service_spec.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Packages::Nuget::Symbols::ExtractSymbolSignatureService, feature_category: :package_registry do
- let_it_be(:symbol_file) { fixture_file('packages/nuget/symbol/package.pdb') }
-
- let(:service) { described_class.new(symbol_file) }
-
- describe '#execute' do
- subject { service.execute }
-
- context 'with a valid symbol file' do
- it { expect(subject.payload).to eq('b91a152048fc4b3883bf3cf73fbc03f1FFFFFFFF') }
- end
-
- context 'with corrupted data' do
- let(:symbol_file) { 'corrupted data' }
-
- it { expect(subject).to be_error }
- end
- end
-end
diff --git a/spec/services/packages/nuget/update_package_from_metadata_service_spec.rb b/spec/services/packages/nuget/update_package_from_metadata_service_spec.rb
index cb70176ee61..0e19f2ac3f9 100644
--- a/spec/services/packages/nuget/update_package_from_metadata_service_spec.rb
+++ b/spec/services/packages/nuget/update_package_from_metadata_service_spec.rb
@@ -7,7 +7,8 @@ RSpec.describe Packages::Nuget::UpdatePackageFromMetadataService, :clean_gitlab_
let!(:package) { create(:nuget_package, :processing, :with_symbol_package, :with_build) }
let(:package_file) { package.package_files.first }
- let(:service) { described_class.new(package_file) }
+ let(:package_zip_file) { Zip::File.new(package_file.file) }
+ let(:service) { described_class.new(package_file, package_zip_file) }
let(:package_name) { 'DummyProject.DummyPackage' }
let(:package_version) { '1.0.0' }
let(:package_file_name) { 'dummyproject.dummypackage.1.0.0.nupkg' }
@@ -127,7 +128,7 @@ RSpec.describe Packages::Nuget::UpdatePackageFromMetadataService, :clean_gitlab_
context 'with a nuspec file with metadata' do
let(:nuspec_filepath) { 'packages/nuget/with_metadata.nuspec' }
- let(:expected_tags) { %w(foo bar test tag1 tag2 tag3 tag4 tag5) }
+ let(:expected_tags) { %w[foo bar test tag1 tag2 tag3 tag4 tag5] }
before do
allow_next_instance_of(Packages::Nuget::MetadataExtractionService) do |service|
@@ -182,13 +183,15 @@ RSpec.describe Packages::Nuget::UpdatePackageFromMetadataService, :clean_gitlab_
context 'without authors or description' do
%i[authors description].each do |property|
- let(:metadata) { { package_name: package_name, package_version: package_version, property => nil } }
+ context "for #{property}" do
+ let(:metadata) { { package_name: package_name, package_version: package_version, property => nil } }
- before do
- allow(service).to receive(:metadata).and_return(metadata)
- end
+ before do
+ allow(service).to receive(:metadata).and_return(metadata)
+ end
- it_behaves_like 'raising an', described_class::InvalidMetadataError, with_message: described_class::INVALID_METADATA_ERROR_MESSAGE
+ it_behaves_like 'raising an', described_class::InvalidMetadataError, with_message: described_class::INVALID_METADATA_ERROR_MESSAGE
+ end
end
end
end
@@ -245,11 +248,20 @@ RSpec.describe Packages::Nuget::UpdatePackageFromMetadataService, :clean_gitlab_
context 'with existing package' do
let!(:existing_package) { create(:nuget_package, project: package.project, name: package_name, version: package_version) }
let(:package_id) { existing_package.id }
+ let(:package_zip_file) do
+ Zip::File.open(package_file.file.path) do |zipfile|
+ zipfile.add('package.pdb', expand_fixture_path('packages/nuget/symbol/package.pdb'))
+ zipfile
+ end
+ end
it 'link existing package and updates package file', :aggregate_failures do
expect(service).to receive(:try_obtain_lease).and_call_original
expect(::Packages::Nuget::SyncMetadatumService).not_to receive(:new)
expect(::Packages::UpdateTagsService).not_to receive(:new)
+ expect_next_instance_of(Packages::Nuget::Symbols::CreateSymbolFilesService, existing_package, package_zip_file) do |service|
+ expect(service).to receive(:execute).and_call_original
+ end
expect { subject }
.to change { ::Packages::Package.count }.by(-1)
@@ -257,20 +269,11 @@ RSpec.describe Packages::Nuget::UpdatePackageFromMetadataService, :clean_gitlab_
.and change { Packages::DependencyLink.count }.by(0)
.and change { Packages::Nuget::DependencyLinkMetadatum.count }.by(0)
.and change { ::Packages::Nuget::Metadatum.count }.by(0)
+ .and change { existing_package.nuget_symbols.count }.by(1)
expect(package_file.reload.file_name).to eq(package_file_name)
expect(package_file.package).to eq(existing_package)
end
- context 'with packages_nuget_symbols records' do
- before do
- create_list(:nuget_symbol, 2, package: package)
- end
-
- it 'links the symbol records to the existing package' do
- expect { subject }.to change { existing_package.nuget_symbols.count }.by(2)
- end
- end
-
it_behaves_like 'taking the lease'
it_behaves_like 'not updating the package if the lease is taken'
diff --git a/spec/services/packages/protection/create_rule_service_spec.rb b/spec/services/packages/protection/create_rule_service_spec.rb
index 67835479473..ed8d21f4344 100644
--- a/spec/services/packages/protection/create_rule_service_spec.rb
+++ b/spec/services/packages/protection/create_rule_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Packages::Protection::CreateRuleService, '#execute', feature_category: :environment_management do
+RSpec.describe Packages::Protection::CreateRuleService, '#execute', feature_category: :package_registry do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:maintainer) { create(:user).tap { |u| project.add_maintainer(u) } }
diff --git a/spec/services/packages/protection/delete_rule_service_spec.rb b/spec/services/packages/protection/delete_rule_service_spec.rb
new file mode 100644
index 00000000000..d64609d4df1
--- /dev/null
+++ b/spec/services/packages/protection/delete_rule_service_spec.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Packages::Protection::DeleteRuleService, '#execute', feature_category: :package_registry do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:current_user) { create(:user, maintainer_projects: [project]) }
+ let_it_be_with_refind(:package_protection_rule) { create(:package_protection_rule, project: project) }
+
+ subject { described_class.new(package_protection_rule, current_user: current_user).execute }
+
+ shared_examples 'a successful service response' do
+ it { is_expected.to be_success }
+
+ it {
+ is_expected.to have_attributes(
+ errors: be_blank,
+ message: be_blank,
+ payload: { package_protection_rule: package_protection_rule }
+ )
+ }
+
+ it { subject.tap { expect { package_protection_rule.reload }.to raise_error ActiveRecord::RecordNotFound } }
+ end
+
+ shared_examples 'an erroneous service response' do
+ it { is_expected.to be_error }
+ it { is_expected.to have_attributes(message: be_present, payload: { package_protection_rule: be_blank }) }
+
+ it do
+ expect { subject }.not_to change { Packages::Protection::Rule.count }
+
+ expect { package_protection_rule.reload }.not_to raise_error
+ end
+ end
+
+ it_behaves_like 'a successful service response'
+
+ it 'deletes the package protection rule in the database' do
+ expect { subject }
+ .to change { project.reload.package_protection_rules }.from([package_protection_rule]).to([])
+ .and change { ::Packages::Protection::Rule.count }.from(1).to(0)
+ end
+
+ context 'with deleted package protection rule' do
+ let!(:package_protection_rule) do
+ create(:package_protection_rule, project: project, package_name_pattern: 'protection_rule_deleted').destroy!
+ end
+
+ it_behaves_like 'a successful service response'
+ end
+
+ context 'when error occurs during delete operation' do
+ before do
+ allow(package_protection_rule).to receive(:destroy!).and_raise(StandardError.new('Some error'))
+ end
+
+ it_behaves_like 'an erroneous service response'
+
+ it { is_expected.to have_attributes message: /Some error/ }
+ end
+
+ context 'when current_user does not have permission' do
+ let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } }
+ let_it_be(:reporter) { create(:user).tap { |u| project.add_reporter(u) } }
+ let_it_be(:guest) { create(:user).tap { |u| project.add_guest(u) } }
+ let_it_be(:anonymous) { create(:user) }
+
+ where(:current_user) do
+ [ref(:developer), ref(:reporter), ref(:guest), ref(:anonymous)]
+ end
+
+ with_them do
+ it_behaves_like 'an erroneous service response'
+
+ it { is_expected.to have_attributes message: /Unauthorized to delete a package protection rule/ }
+ end
+ end
+
+ context 'without package protection rule' do
+ let(:package_protection_rule) { nil }
+
+ it { expect { subject }.to raise_error(ArgumentError) }
+ end
+
+ context 'without current_user' do
+ let(:current_user) { nil }
+ let(:package_protection_rule) { build_stubbed(:package_protection_rule, project: project) }
+
+ it { expect { subject }.to raise_error(ArgumentError) }
+ end
+end
diff --git a/spec/services/packages/pypi/create_package_service_spec.rb b/spec/services/packages/pypi/create_package_service_spec.rb
index 0d278e32e89..abff91d1878 100644
--- a/spec/services/packages/pypi/create_package_service_spec.rb
+++ b/spec/services/packages/pypi/create_package_service_spec.rb
@@ -69,6 +69,30 @@ RSpec.describe Packages::Pypi::CreatePackageService, :aggregate_failures, featur
end
end
+ context 'with additional metadata' do
+ before do
+ params.merge!(
+ metadata_version: '2.3',
+ author_email: 'cschultz@example.com, snoopy@peanuts.com',
+ description: 'Example description',
+ description_content_type: 'text/plain',
+ summary: 'A module for collecting votes from beagles.',
+ keywords: 'dog,puppy,voting,election'
+ )
+ end
+
+ it 'creates the package' do
+ expect { subject }.to change { Packages::Package.pypi.count }.by(1)
+
+ expect(created_package.pypi_metadatum.metadata_version).to eq('2.3')
+ expect(created_package.pypi_metadatum.author_email).to eq('cschultz@example.com, snoopy@peanuts.com')
+ expect(created_package.pypi_metadatum.description).to eq('Example description')
+ expect(created_package.pypi_metadatum.description_content_type).to eq('text/plain')
+ expect(created_package.pypi_metadatum.summary).to eq('A module for collecting votes from beagles.')
+ expect(created_package.pypi_metadatum.keywords).to eq('dog,puppy,voting,election')
+ end
+ end
+
context 'with an invalid metadata' do
let(:requires_python) { 'x' * 256 }
diff --git a/spec/services/packages/update_tags_service_spec.rb b/spec/services/packages/update_tags_service_spec.rb
index d8f572fff32..ec4d68ba5a0 100644
--- a/spec/services/packages/update_tags_service_spec.rb
+++ b/spec/services/packages/update_tags_service_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Packages::UpdateTagsService, feature_category: :package_registry do
let_it_be(:package, reload: true) { create(:nuget_package) }
- let(:tags) { %w(test-tag tag1 tag2 tag3) }
+ let(:tags) { %w[test-tag tag1 tag2 tag3] }
let(:service) { described_class.new(package, tags) }
describe '#execute' do
diff --git a/spec/services/pages/delete_service_spec.rb b/spec/services/pages/delete_service_spec.rb
index 488f29f6b7e..86b1bd5bde2 100644
--- a/spec/services/pages/delete_service_spec.rb
+++ b/spec/services/pages/delete_service_spec.rb
@@ -8,14 +8,12 @@ RSpec.describe Pages::DeleteService, feature_category: :pages do
let(:project) { create(:project, path: "my.project") }
let(:service) { described_class.new(project, admin) }
- before do
- project.mark_pages_as_deployed
- end
-
it 'marks pages as not deployed' do
- expect do
- service.execute
- end.to change { project.reload.pages_deployed? }.from(true).to(false)
+ create(:pages_deployment, project: project)
+
+ expect { service.execute }
+ .to change { project.reload.pages_deployed? }
+ .from(true).to(false)
end
it 'deletes all domains' do
@@ -29,9 +27,8 @@ RSpec.describe Pages::DeleteService, feature_category: :pages do
end
it 'schedules a destruction of pages deployments' do
- expect(DestroyPagesDeploymentsWorker).to(
- receive(:perform_async).with(project.id)
- )
+ expect(DestroyPagesDeploymentsWorker)
+ .to(receive(:perform_async).with(project.id))
service.execute
end
@@ -39,9 +36,8 @@ RSpec.describe Pages::DeleteService, feature_category: :pages do
it 'removes pages deployments', :sidekiq_inline do
create(:pages_deployment, project: project)
- expect do
- service.execute
- end.to change { PagesDeployment.count }.by(-1)
+ expect { service.execute }
+ .to change { PagesDeployment.count }.by(-1)
end
it 'publishes a ProjectDeleted event with project id and namespace id' do
diff --git a/spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb b/spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb
index 7f8992e8bbc..0e46391c0ad 100644
--- a/spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb
+++ b/spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb
@@ -65,7 +65,7 @@ RSpec.describe PagesDomains::ObtainLetsEncryptCertificateService, feature_catego
end
end
- %w(pending processing).each do |status|
+ %w[pending processing].each do |status|
context "there is an order in '#{status}' status" do
let(:existing_order) do
create(:pages_domain_acme_order, pages_domain: pages_domain)
diff --git a/spec/services/product_analytics/build_graph_service_spec.rb b/spec/services/product_analytics/build_graph_service_spec.rb
index 13c7206241c..a850d69e53c 100644
--- a/spec/services/product_analytics/build_graph_service_spec.rb
+++ b/spec/services/product_analytics/build_graph_service_spec.rb
@@ -21,7 +21,7 @@ RSpec.describe ProductAnalytics::BuildGraphService, feature_category: :product_a
it 'returns a valid graph hash' do
expect(subject[:id]).to eq(:platform)
- expect(subject[:keys]).to eq(%w(app mobile web))
+ expect(subject[:keys]).to eq(%w[app mobile web])
expect(subject[:values]).to eq([1, 1, 2])
end
end
diff --git a/spec/services/projects/branches_by_mode_service_spec.rb b/spec/services/projects/branches_by_mode_service_spec.rb
index bfe76b34310..c87787346b9 100644
--- a/spec/services/projects/branches_by_mode_service_spec.rb
+++ b/spec/services/projects/branches_by_mode_service_spec.rb
@@ -50,7 +50,7 @@ RSpec.describe Projects::BranchesByModeService, feature_category: :source_code_m
branches, prev_page, next_page = subject
- expect(branches.map(&:name)).to match_array(%w(feature feature_conflict))
+ expect(branches.map(&:name)).to match_array(%w[feature feature_conflict])
expect(next_page).to be_nil
expect(prev_page).to be_nil
end
diff --git a/spec/services/projects/container_repository/delete_tags_service_spec.rb b/spec/services/projects/container_repository/delete_tags_service_spec.rb
index 5b67d614dfb..942e244e3d2 100644
--- a/spec/services/projects/container_repository/delete_tags_service_spec.rb
+++ b/spec/services/projects/container_repository/delete_tags_service_spec.rb
@@ -77,7 +77,7 @@ RSpec.describe Projects::ContainerRepository::DeleteTagsService, feature_categor
before do
expect(::Projects::ContainerRepository::Gitlab::DeleteTagsService).not_to receive(:new)
expect(::Projects::ContainerRepository::ThirdParty::DeleteTagsService).not_to receive(:new)
- expect_any_instance_of(ContainerRegistry::Client).not_to receive(:delete_repository_tag_by_name)
+ expect_any_instance_of(ContainerRegistry::Client).not_to receive(:delete_repository_tag_by_digest)
end
context 'when no params are specified' do
@@ -107,7 +107,7 @@ RSpec.describe Projects::ContainerRepository::DeleteTagsService, feature_categor
context 'with the real service' do
before do
stub_delete_reference_requests(tags)
- expect_delete_tag_by_names(tags)
+ expect_delete_tags(tags)
end
it { is_expected.to include(status: :success) }
diff --git a/spec/services/projects/container_repository/gitlab/delete_tags_service_spec.rb b/spec/services/projects/container_repository/gitlab/delete_tags_service_spec.rb
index 0d7d1254428..676c7ca8e99 100644
--- a/spec/services/projects/container_repository/gitlab/delete_tags_service_spec.rb
+++ b/spec/services/projects/container_repository/gitlab/delete_tags_service_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe Projects::ContainerRepository::Gitlab::DeleteTagsService, feature
RSpec.shared_examples 'deleting tags' do
it 'deletes the tags by name' do
stub_delete_reference_requests(tags)
- expect_delete_tag_by_names(tags)
+ expect_delete_tags(tags)
is_expected.to eq(status: :success, deleted: tags)
end
@@ -59,7 +59,7 @@ RSpec.describe Projects::ContainerRepository::Gitlab::DeleteTagsService, feature
tags.each do |tag|
stub_delete_reference_requests(tag => 500)
end
- allow(container_repository).to receive(:delete_tag_by_name).and_return(false)
+ allow(container_repository).to receive(:delete_tag).and_return(false)
end
it 'truncates the log message' do
@@ -119,7 +119,7 @@ RSpec.describe Projects::ContainerRepository::Gitlab::DeleteTagsService, feature
let_it_be(:tags) { [] }
it 'does not remove anything' do
- expect_any_instance_of(ContainerRegistry::Client).not_to receive(:delete_repository_tag_by_name)
+ expect_any_instance_of(ContainerRegistry::Client).not_to receive(:delete_repository_tag_by_digest)
is_expected.to eq(status: :success, deleted: [])
end
diff --git a/spec/services/projects/container_repository/third_party/delete_tags_service_spec.rb b/spec/services/projects/container_repository/third_party/delete_tags_service_spec.rb
index 0c297b6e1f7..d3d3f3bb7ce 100644
--- a/spec/services/projects/container_repository/third_party/delete_tags_service_spec.rb
+++ b/spec/services/projects/container_repository/third_party/delete_tags_service_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe Projects::ContainerRepository::ThirdParty::DeleteTagsService, fea
tags.each { |tag| stub_put_manifest_request(tag) }
- expect_delete_tag_by_digest('sha256:dummy')
+ expect_delete_tags(['sha256:dummy'])
is_expected.to eq(status: :success, deleted: tags)
end
@@ -92,7 +92,7 @@ RSpec.describe Projects::ContainerRepository::ThirdParty::DeleteTagsService, fea
let_it_be(:tags) { [] }
it 'does not remove anything' do
- expect_any_instance_of(ContainerRegistry::Client).not_to receive(:delete_repository_tag_by_name)
+ expect_any_instance_of(ContainerRegistry::Client).not_to receive(:delete_repository_tag_by_digest)
is_expected.to eq(status: :success, deleted: [])
end
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index ce7e5188c7b..899ed477180 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -320,7 +320,7 @@ RSpec.describe Projects::CreateService, '#execute', feature_category: :groups_an
it 'cannot create a project' do
expect(project.errors.errors.length).to eq 1
- expect(project.errors.messages[:limit_reached].first).to eq(_('Personal project creation is not allowed. Please contact your administrator with questions'))
+ expect(project.errors.messages[:limit_reached].first).to eq(_('You cannot create projects in your personal namespace. Contact your GitLab administrator.'))
end
end
diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb
index 0210e101e5f..a0064eadf13 100644
--- a/spec/services/projects/destroy_service_spec.rb
+++ b/spec/services/projects/destroy_service_spec.rb
@@ -472,6 +472,31 @@ RSpec.describe Projects::DestroyService, :aggregate_failures, :event_store_publi
end
end
+ context 'with related storage move records' do
+ context 'when project has active repository storage move records' do
+ let!(:project_repository_storage_move) { create(:project_repository_storage_move, :scheduled, container: project) }
+
+ it 'does not delete the project' do
+ expect(destroy_project(project, user)).to be_falsey
+
+ expect(project.delete_error).to eq "Couldn't remove the project. A project repository storage move is in progress. Try again when it's complete."
+ expect(project.pending_delete).to be_falsey
+ end
+ end
+
+ context 'when project has active snippet storage move records' do
+ let(:project_snippet) { create(:project_snippet, project: project) }
+ let!(:snippet_repository_storage_move) { create(:snippet_repository_storage_move, :started, container: project_snippet) }
+
+ it 'does not delete the project' do
+ expect(destroy_project(project, user)).to be_falsey
+
+ expect(project.delete_error).to eq "Couldn't remove the project. A related snippet repository storage move is in progress. Try again when it's complete."
+ expect(project.pending_delete).to be_falsey
+ end
+ end
+ end
+
context 'repository removal' do
describe '.trash_project_repositories!' do
let(:trash_project_repositories!) { described_class.new(project, user, {}).send(:trash_project_repositories!) }
diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb
index 4d55f310974..ceb060445ad 100644
--- a/spec/services/projects/fork_service_spec.rb
+++ b/spec/services/projects/fork_service_spec.rb
@@ -510,6 +510,26 @@ RSpec.describe Projects::ForkService, feature_category: :source_code_management
end
end
+ describe '#valid_fork_branch?' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :small_repo, creator_id: user.id) }
+ let_it_be(:branch) { nil }
+
+ subject { described_class.new(project, user).valid_fork_branch?(branch) }
+
+ context 'when branch exists' do
+ let(:branch) { project.default_branch_or_main }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when branch does not exist' do
+ let(:branch) { 'branch-that-does-not-exist' }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
describe '#valid_fork_target?' do
let(:project) { Project.new }
let(:params) { {} }
diff --git a/spec/services/projects/group_links/create_service_spec.rb b/spec/services/projects/group_links/create_service_spec.rb
index ca2902af472..e3f170ef3fe 100644
--- a/spec/services/projects/group_links/create_service_spec.rb
+++ b/spec/services/projects/group_links/create_service_spec.rb
@@ -6,6 +6,7 @@ RSpec.describe Projects::GroupLinks::CreateService, '#execute', feature_category
let_it_be(:user) { create :user }
let_it_be(:group) { create :group }
let_it_be(:project) { create(:project, namespace: create(:namespace, :with_namespace_settings)) }
+ let_it_be(:group_user) { create(:user).tap { |user| group.add_guest(user) } }
let(:opts) do
{
@@ -37,67 +38,75 @@ RSpec.describe Projects::GroupLinks::CreateService, '#execute', feature_category
end
end
- context 'when user has proper membership to share a group' do
+ context 'when user has proper permissions to share a project with a group' do
before do
group.add_guest(user)
end
- it_behaves_like 'shareable'
-
- it 'updates authorization', :sidekiq_inline do
- expect { subject.execute }.to(
- change { Ability.allowed?(user, :read_project, project) }
- .from(false).to(true))
- end
-
- context 'with specialized project_authorization workers' do
- let_it_be(:other_user) { create(:user) }
-
+ context 'when the user is a MAINTAINER in the project' do
before do
- group.add_developer(other_user)
+ project.add_maintainer(user)
end
- it 'schedules authorization update for users with access to group' do
- stub_feature_flags(do_not_run_safety_net_auth_refresh_jobs: false)
-
- expect(AuthorizedProjectsWorker).not_to(
- receive(:bulk_perform_async)
- )
- expect(AuthorizedProjectUpdate::ProjectRecalculateWorker).to(
- receive(:perform_async)
- .with(project.id)
- .and_call_original
- )
- expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker).to(
- receive(:bulk_perform_in).with(
- 1.hour,
- array_including([user.id], [other_user.id]),
- batch_delay: 30.seconds, batch_size: 100
- ).and_call_original
- )
-
- subject.execute
+ it_behaves_like 'shareable'
+
+ it 'updates authorization', :sidekiq_inline do
+ expect { subject.execute }.to(
+ change { Ability.allowed?(group_user, :read_project, project) }
+ .from(false).to(true))
end
- end
- context 'when sharing outside the hierarchy is disabled' do
- let_it_be(:shared_group_parent) do
- create(:group, namespace_settings: create(:namespace_settings, prevent_sharing_groups_outside_hierarchy: true))
+ context 'with specialized project_authorization workers' do
+ let_it_be(:other_user) { create(:user) }
+
+ before do
+ group.add_developer(other_user)
+ end
+
+ it 'schedules authorization update for users with access to group' do
+ stub_feature_flags(do_not_run_safety_net_auth_refresh_jobs: false)
+
+ expect(AuthorizedProjectsWorker).not_to(
+ receive(:bulk_perform_async)
+ )
+ expect(AuthorizedProjectUpdate::ProjectRecalculateWorker).to(
+ receive(:perform_async)
+ .with(project.id)
+ .and_call_original
+ )
+ expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker).to(
+ receive(:bulk_perform_in).with(
+ 1.hour,
+ array_including([user.id], [other_user.id]),
+ batch_delay: 30.seconds, batch_size: 100
+ ).and_call_original
+ )
+
+ subject.execute
+ end
end
- let_it_be(:project, reload: true) { create(:project, group: shared_group_parent) }
+ context 'when sharing outside the hierarchy is disabled' do
+ let_it_be(:shared_group_parent) do
+ create(:group,
+ namespace_settings: create(:namespace_settings, prevent_sharing_groups_outside_hierarchy: true)
+ )
+ end
+
+ let_it_be(:project, reload: true) { create(:project, group: shared_group_parent) }
- it_behaves_like 'not shareable'
+ it_behaves_like 'not shareable'
- context 'when group is inside hierarchy' do
- let(:group) { create(:group, :private, parent: shared_group_parent) }
+ context 'when group is inside hierarchy' do
+ let(:group) { create(:group, :private, parent: shared_group_parent) }
- it_behaves_like 'shareable'
+ it_behaves_like 'shareable'
+ end
end
end
end
- context 'when user does not have permissions for the group' do
+ context 'when user does not have permissions to share the project with a group' do
it_behaves_like 'not shareable'
end
diff --git a/spec/services/projects/group_links/destroy_service_spec.rb b/spec/services/projects/group_links/destroy_service_spec.rb
index 103aff8c659..0cd003f6142 100644
--- a/spec/services/projects/group_links/destroy_service_spec.rb
+++ b/spec/services/projects/group_links/destroy_service_spec.rb
@@ -6,83 +6,120 @@ RSpec.describe Projects::GroupLinks::DestroyService, '#execute', feature_categor
let_it_be(:user) { create :user }
let_it_be(:project) { create(:project, :private) }
let_it_be(:group) { create(:group) }
+ let_it_be(:group_user) { create(:user).tap { |user| group.add_guest(user) } }
- let!(:group_link) { create(:project_group_link, project: project, group: group) }
+ let(:group_access) { Gitlab::Access::DEVELOPER }
+ let!(:group_link) { create(:project_group_link, project: project, group: group, group_access: group_access) }
subject { described_class.new(project, user) }
- it 'removes group from project' do
- expect { subject.execute(group_link) }.to change { project.project_group_links.count }.from(1).to(0)
- end
-
- context 'project authorizations refresh' do
- before do
- group.add_maintainer(user)
+ shared_examples_for 'removes group from project' do
+ it 'removes group from project' do
+ expect { subject.execute(group_link) }.to change { project.reload.project_group_links.count }.from(1).to(0)
end
+ end
- it 'calls AuthorizedProjectUpdate::ProjectRecalculateWorker to update project authorizations' do
- expect(AuthorizedProjectUpdate::ProjectRecalculateWorker)
- .to receive(:perform_async).with(group_link.project.id)
+ shared_examples_for 'returns not_found' do
+ it do
+ expect do
+ result = subject.execute(group_link)
- subject.execute(group_link)
+ expect(result[:status]).to eq(:error)
+ expect(result[:reason]).to eq(:not_found)
+ end.not_to change { project.reload.project_group_links.count }
end
+ end
- it 'calls AuthorizedProjectUpdate::UserRefreshFromReplicaWorker with a delay to update project authorizations' do
- stub_feature_flags(do_not_run_safety_net_auth_refresh_jobs: false)
-
- expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker).to(
- receive(:bulk_perform_in).with(
- 1.hour,
- [[user.id]],
- batch_delay: 30.seconds, batch_size: 100
- )
- )
-
- subject.execute(group_link)
- end
+ context 'if group_link is blank' do
+ let!(:group_link) { nil }
- it 'updates project authorizations of users who had access to the project via the group share', :sidekiq_inline do
- expect { subject.execute(group_link) }.to(
- change { Ability.allowed?(user, :read_project, project) }
- .from(true).to(false))
- end
+ it_behaves_like 'returns not_found'
end
- it 'returns false if group_link is blank' do
- expect { subject.execute(nil) }.not_to change { project.project_group_links.count }
+ context 'if the user does not have access to destroy the link' do
+ it_behaves_like 'returns not_found'
end
- describe 'todos cleanup' do
- context 'when project is private' do
- it 'triggers todos cleanup' do
- expect(TodosDestroyer::ProjectPrivateWorker).to receive(:perform_in).with(Todo::WAIT_FOR_DELETE, project.id)
- expect(project.private?).to be true
-
- subject.execute(group_link)
+ context 'when the user has proper permissions to remove a group-link from a project' do
+ context 'when the user is a MAINTAINER in the project' do
+ before do
+ project.add_maintainer(user)
end
- end
- context 'when project is public or internal' do
- shared_examples_for 'removes confidential todos' do
- it 'does not trigger todos cleanup' do
- expect(TodosDestroyer::ProjectPrivateWorker).not_to receive(:perform_in).with(Todo::WAIT_FOR_DELETE, project.id)
- expect(TodosDestroyer::ConfidentialIssueWorker).to receive(:perform_in).with(Todo::WAIT_FOR_DELETE, nil, project.id)
- expect(project.private?).to be false
+ it_behaves_like 'removes group from project'
+
+ context 'project authorizations refresh' do
+ it 'calls AuthorizedProjectUpdate::ProjectRecalculateWorker to update project authorizations' do
+ expect(AuthorizedProjectUpdate::ProjectRecalculateWorker)
+ .to receive(:perform_async).with(group_link.project.id)
subject.execute(group_link)
end
- end
- context 'when project is public' do
- let(:project) { create(:project, :public) }
+ it 'calls AuthorizedProjectUpdate::UserRefreshFromReplicaWorker with a delay to update project authorizations' do
+ stub_feature_flags(do_not_run_safety_net_auth_refresh_jobs: false)
- it_behaves_like 'removes confidential todos'
+ expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker).to(
+ receive(:bulk_perform_in).with(
+ 1.hour,
+ [[group_user.id]],
+ batch_delay: 30.seconds, batch_size: 100
+ )
+ )
+
+ subject.execute(group_link)
+ end
+
+ it 'updates project authorizations of users who had access to the project via the group share', :sidekiq_inline do
+ expect { subject.execute(group_link) }.to(
+ change { Ability.allowed?(group_user, :read_project, project) }
+ .from(true).to(false))
+ end
end
- context 'when project is internal' do
- let(:project) { create(:project, :public) }
+ describe 'todos cleanup' do
+ context 'when project is private' do
+ it 'triggers todos cleanup' do
+ expect(TodosDestroyer::ProjectPrivateWorker).to receive(:perform_in).with(Todo::WAIT_FOR_DELETE, project.id)
+ expect(project.private?).to be true
+
+ subject.execute(group_link)
+ end
+ end
+
+ context 'when project is public or internal' do
+ shared_examples_for 'removes confidential todos' do
+ it 'does not trigger todos cleanup' do
+ expect(TodosDestroyer::ProjectPrivateWorker).not_to receive(:perform_in).with(Todo::WAIT_FOR_DELETE, project.id)
+ expect(TodosDestroyer::ConfidentialIssueWorker).to receive(:perform_in).with(Todo::WAIT_FOR_DELETE, nil, project.id)
+ expect(project.private?).to be false
+
+ subject.execute(group_link)
+ end
+ end
+
+ context 'when project is public' do
+ let(:project) { create(:project, :public) }
+
+ it_behaves_like 'removes confidential todos'
+ end
+
+ context 'when project is internal' do
+ let(:project) { create(:project, :public) }
+
+ it_behaves_like 'removes confidential todos'
+ end
+ end
+ end
+ end
+ end
- it_behaves_like 'removes confidential todos'
+ context 'when skipping authorization' do
+ context 'without providing a user' do
+ it 'destroys the link' do
+ expect do
+ described_class.new(project, nil).execute(group_link, skip_authorization: true)
+ end.to change { project.reload.project_group_links.count }.by(-1)
end
end
end
diff --git a/spec/services/projects/group_links/update_service_spec.rb b/spec/services/projects/group_links/update_service_spec.rb
index f7607deef04..b02614fa062 100644
--- a/spec/services/projects/group_links/update_service_spec.rb
+++ b/spec/services/projects/group_links/update_service_spec.rb
@@ -6,8 +6,11 @@ RSpec.describe Projects::GroupLinks::UpdateService, '#execute', feature_category
let_it_be(:user) { create :user }
let_it_be(:group) { create :group }
let_it_be(:project) { create :project }
+ let_it_be(:group_user) { create(:user).tap { |user| group.add_developer(user) } }
- let!(:link) { create(:project_group_link, project: project, group: group) }
+ let(:group_access) { Gitlab::Access::DEVELOPER }
+
+ let!(:link) { create(:project_group_link, project: project, group: group, group_access: group_access) }
let(:expiry_date) { 1.month.from_now.to_date }
let(:group_link_params) do
@@ -17,60 +20,78 @@ RSpec.describe Projects::GroupLinks::UpdateService, '#execute', feature_category
subject { described_class.new(link, user).execute(group_link_params) }
- before do
- group.add_developer(user)
- end
-
- it 'updates existing link' do
- expect(link.group_access).to eq(Gitlab::Access::DEVELOPER)
- expect(link.expires_at).to be_nil
-
- subject
-
- link.reload
+ shared_examples_for 'returns not_found' do
+ it do
+ result = subject
- expect(link.group_access).to eq(Gitlab::Access::GUEST)
- expect(link.expires_at).to eq(expiry_date)
- end
-
- context 'project authorizations update' do
- it 'calls AuthorizedProjectUpdate::ProjectRecalculateWorker to update project authorizations' do
- expect(AuthorizedProjectUpdate::ProjectRecalculateWorker)
- .to receive(:perform_async).with(link.project.id)
-
- subject
- end
-
- it 'calls AuthorizedProjectUpdate::UserRefreshFromReplicaWorker with a delay to update project authorizations' do
- stub_feature_flags(do_not_run_safety_net_auth_refresh_jobs: false)
-
- expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker).to(
- receive(:bulk_perform_in).with(
- 1.hour,
- [[user.id]],
- batch_delay: 30.seconds, batch_size: 100
- )
- )
-
- subject
- end
-
- it 'updates project authorizations of users who had access to the project via the group share', :sidekiq_inline do
- group.add_maintainer(user)
-
- expect { subject }.to(
- change { Ability.allowed?(user, :create_release, project) }
- .from(true).to(false))
+ expect(result[:status]).to eq(:error)
+ expect(result[:reason]).to eq(:not_found)
end
end
- context 'with only param not requiring authorization refresh' do
- let(:group_link_params) { { expires_at: Date.tomorrow } }
-
- it 'does not perform any project authorizations update using `AuthorizedProjectUpdate::ProjectRecalculateWorker`' do
- expect(AuthorizedProjectUpdate::ProjectRecalculateWorker).not_to receive(:perform_async)
+ context 'when the user does not have proper permissions to update a project group link' do
+ it_behaves_like 'returns not_found'
+ end
- subject
+ context 'when user has proper permissions to update a project group link' do
+ context 'when the user is a MAINTAINER in the project' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ it 'updates existing link' do
+ expect(link.group_access).to eq(Gitlab::Access::DEVELOPER)
+ expect(link.expires_at).to be_nil
+
+ subject
+
+ link.reload
+
+ expect(link.group_access).to eq(Gitlab::Access::GUEST)
+ expect(link.expires_at).to eq(expiry_date)
+ end
+
+ context 'project authorizations update' do
+ it 'calls AuthorizedProjectUpdate::ProjectRecalculateWorker to update project authorizations' do
+ expect(AuthorizedProjectUpdate::ProjectRecalculateWorker)
+ .to receive(:perform_async).with(link.project.id)
+
+ subject
+ end
+
+ it 'calls AuthorizedProjectUpdate::UserRefreshFromReplicaWorker ' \
+ 'with a delay to update project authorizations' do
+ stub_feature_flags(do_not_run_safety_net_auth_refresh_jobs: false)
+
+ expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker).to(
+ receive(:bulk_perform_in).with(
+ 1.hour,
+ [[group_user.id]],
+ batch_delay: 30.seconds, batch_size: 100
+ )
+ )
+
+ subject
+ end
+
+ it 'updates project authorizations of users who had access to the project via the group share',
+ :sidekiq_inline do
+ expect { subject }.to(
+ change { Ability.allowed?(group_user, :developer_access, project) }
+ .from(true).to(false))
+ end
+ end
+
+ context 'with only param not requiring authorization refresh' do
+ let(:group_link_params) { { expires_at: Date.tomorrow } }
+
+ it 'does not perform any project authorizations update using ' \
+ '`AuthorizedProjectUpdate::ProjectRecalculateWorker`' do
+ expect(AuthorizedProjectUpdate::ProjectRecalculateWorker).not_to receive(:perform_async)
+
+ subject
+ end
+ end
end
end
end
diff --git a/spec/services/projects/lfs_pointers/lfs_link_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_link_service_spec.rb
index fb3cc9bdac9..d3f053aaedc 100644
--- a/spec/services/projects/lfs_pointers/lfs_link_service_spec.rb
+++ b/spec/services/projects/lfs_pointers/lfs_link_service_spec.rb
@@ -73,10 +73,10 @@ RSpec.describe Projects::LfsPointers::LfsLinkService, feature_category: :source_
it 'only queries for the batch that will be processed', :aggregate_failures do
stub_const("#{described_class}::BATCH_SIZE", 1)
- oids = %w(one two)
+ oids = %w[one two]
- expect(LfsObject).to receive(:for_oids).with(%w(one)).once.and_call_original
- expect(LfsObject).to receive(:for_oids).with(%w(two)).once.and_call_original
+ expect(LfsObject).to receive(:for_oids).with(%w[one]).once.and_call_original
+ expect(LfsObject).to receive(:for_oids).with(%w[two]).once.and_call_original
subject.execute(oids)
end
diff --git a/spec/services/projects/operations/update_service_spec.rb b/spec/services/projects/operations/update_service_spec.rb
index 5f9b1a59bf9..03508a9732e 100644
--- a/spec/services/projects/operations/update_service_spec.rb
+++ b/spec/services/projects/operations/update_service_spec.rb
@@ -325,7 +325,7 @@ RSpec.describe Projects::Operations::UpdateService, feature_category: :groups_an
expect(project_arg).to eq project
expect(user_arg).to eq user
expect(prometheus_attrs).to have_key('encrypted_properties')
- expect(prometheus_attrs.keys).not_to include(*%w(id project_id created_at updated_at properties))
+ expect(prometheus_attrs.keys).not_to include(*%w[id project_id created_at updated_at properties])
expect(prometheus_attrs['encrypted_properties']).not_to eq(prometheus_integration.encrypted_properties)
end.and_call_original
diff --git a/spec/services/projects/record_target_platforms_service_spec.rb b/spec/services/projects/record_target_platforms_service_spec.rb
index bf87b763341..40ade386847 100644
--- a/spec/services/projects/record_target_platforms_service_spec.rb
+++ b/spec/services/projects/record_target_platforms_service_spec.rb
@@ -21,11 +21,11 @@ RSpec.describe Projects::RecordTargetPlatformsService, '#execute', feature_categ
it 'creates a new setting record for the project', :aggregate_failures do
expect { execute }.to change { ProjectSetting.count }.from(0).to(1)
- expect(ProjectSetting.last.target_platforms).to match_array(%w(ios osx))
+ expect(ProjectSetting.last.target_platforms).to match_array(%w[ios osx])
end
it 'returns array of detected target platforms' do
- expect(execute).to match_array %w(ios osx)
+ expect(execute).to match_array %w[ios osx]
end
context 'when a project has an existing setting record' do
@@ -34,17 +34,17 @@ RSpec.describe Projects::RecordTargetPlatformsService, '#execute', feature_categ
end
context 'when target platforms changed' do
- let(:saved_target_platforms) { %w(tvos) }
+ let(:saved_target_platforms) { %w[tvos] }
it 'updates' do
- expect { execute }.to change { project_setting.target_platforms }.from(%w(tvos)).to(%w(ios osx))
+ expect { execute }.to change { project_setting.target_platforms }.from(%w[tvos]).to(%w[ios osx])
end
- it { is_expected.to match_array %w(ios osx) }
+ it { is_expected.to match_array %w[ios osx] }
end
context 'when target platforms are the same' do
- let(:saved_target_platforms) { %w(osx ios) }
+ let(:saved_target_platforms) { %w[osx ios] }
it 'does not update' do
expect { execute }.not_to change { project_setting.updated_at }
diff --git a/spec/services/projects/update_pages_service_spec.rb b/spec/services/projects/update_pages_service_spec.rb
index 0ad7693a047..b5d1276988f 100644
--- a/spec/services/projects/update_pages_service_spec.rb
+++ b/spec/services/projects/update_pages_service_spec.rb
@@ -48,7 +48,7 @@ RSpec.describe Projects::UpdatePagesService, feature_category: :pages do
deploy_status = GenericCommitStatus.last
expect(deploy_status.description).not_to be_present
- expect(project.pages_metadatum).to be_deployed
+ expect(project.pages_deployed?).to eq(true)
end
it_behaves_like 'old deployments'
@@ -116,15 +116,14 @@ RSpec.describe Projects::UpdatePagesService, feature_category: :pages do
it "doesn't delete artifacts after deploying" do
expect(service.execute[:status]).to eq(:success)
- expect(project.pages_metadatum).to be_deployed
+ expect(project.pages_deployed?).to eq(true)
expect(build.artifacts?).to eq(true)
end
it 'succeeds' do
- expect(project.pages_deployed?).to be_falsey
- expect(service.execute[:status]).to eq(:success)
- expect(project.pages_metadatum).to be_deployed
- expect(project.pages_deployed?).to be_truthy
+ expect { expect(service.execute[:status]).to eq(:success) }
+ .to change { project.pages_deployed? }
+ .from(false).to(true)
end
it 'publishes a PageDeployedEvent event with project id and namespace id' do
@@ -137,10 +136,10 @@ RSpec.describe Projects::UpdatePagesService, feature_category: :pages do
expect { service.execute }.to publish_event(Pages::PageDeployedEvent).with(expected_data)
end
- it 'creates pages_deployment and saves it in the metadata' do
- expect do
- expect(service.execute[:status]).to eq(:success)
- end.to change { project.pages_deployments.count }.by(1)
+ it 'creates pages_deployment' do
+ expect { expect(service.execute[:status]).to eq(:success) }
+ .to change { project.pages_deployments.count }
+ .by(1)
deployment = project.pages_deployments.last
@@ -148,7 +147,6 @@ RSpec.describe Projects::UpdatePagesService, feature_category: :pages do
expect(deployment.file).to be_present
expect(deployment.file_count).to eq(3)
expect(deployment.file_sha256).to eq(artifacts_archive.file_sha256)
- expect(project.pages_metadatum.reload.pages_deployment_id).to eq(deployment.id)
expect(deployment.ci_build_id).to eq(build.id)
expect(deployment.root_directory).to be_nil
end
@@ -157,11 +155,9 @@ RSpec.describe Projects::UpdatePagesService, feature_category: :pages do
project.pages_metadatum.destroy!
project.reload
- expect do
- expect(service.execute[:status]).to eq(:success)
- end.to change { project.pages_deployments.count }.by(1)
-
- expect(project.pages_metadatum.reload.pages_deployment).to eq(project.pages_deployments.last)
+ expect { expect(service.execute[:status]).to eq(:success) }
+ .to change { project.pages_deployments.count }
+ .by(1)
end
context 'when archive does not have pages directory' do
@@ -171,7 +167,10 @@ RSpec.describe Projects::UpdatePagesService, feature_category: :pages do
it 'returns an error' do
expect(service.execute[:status]).not_to eq(:success)
- expect(GenericCommitStatus.last.description).to eq("Error: You need to either include a `public/` folder in your artifacts, or specify which one to use for Pages using `publish` in `.gitlab-ci.yml`")
+ expect(GenericCommitStatus.last.description)
+ .to eq(
+ "Error: You need to either include a `public/` folder in your artifacts, " \
+ "or specify which one to use for Pages using `publish` in `.gitlab-ci.yml`")
end
end
@@ -196,7 +195,10 @@ RSpec.describe Projects::UpdatePagesService, feature_category: :pages do
it 'returns an error' do
expect(service.execute[:status]).not_to eq(:success)
- expect(GenericCommitStatus.last.description).to eq("Error: You need to either include a `public/` folder in your artifacts, or specify which one to use for Pages using `publish` in `.gitlab-ci.yml`")
+ expect(GenericCommitStatus.last.description)
+ .to eq(
+ "Error: You need to either include a `public/` folder in your artifacts, " \
+ "or specify which one to use for Pages using `publish` in `.gitlab-ci.yml`")
end
end
@@ -208,7 +210,10 @@ RSpec.describe Projects::UpdatePagesService, feature_category: :pages do
it 'returns an error' do
expect(service.execute[:status]).not_to eq(:success)
- expect(GenericCommitStatus.last.description).to eq("Error: You need to either include a `public/` folder in your artifacts, or specify which one to use for Pages using `publish` in `.gitlab-ci.yml`")
+ expect(GenericCommitStatus.last.description)
+ .to eq(
+ "Error: You need to either include a `public/` folder in your artifacts, " \
+ "or specify which one to use for Pages using `publish` in `.gitlab-ci.yml`")
end
end
end
@@ -223,7 +228,8 @@ RSpec.describe Projects::UpdatePagesService, feature_category: :pages do
expect(service.execute[:status]).not_to eq(:success)
- expect(GenericCommitStatus.last.description).to eq("pages site contains 3 file entries, while limit is set to 2")
+ expect(GenericCommitStatus.last.description)
+ .to eq("pages site contains 3 file entries, while limit is set to 2")
end
context 'when timeout happens by DNS error' do
@@ -240,13 +246,13 @@ RSpec.describe Projects::UpdatePagesService, feature_category: :pages do
deploy_status = GenericCommitStatus.last
expect(deploy_status).to be_failed
- expect(project.pages_metadatum).not_to be_deployed
+ expect(project.pages_deployed?).to eq(false)
end
end
context 'when missing artifacts metadata' do
before do
- expect(build).to receive(:artifacts_metadata?).and_return(false)
+ allow(build).to receive(:artifacts_metadata?).and_return(false)
end
it 'does not raise an error as failed job' do
@@ -256,7 +262,7 @@ RSpec.describe Projects::UpdatePagesService, feature_category: :pages do
deploy_status = GenericCommitStatus.last
expect(deploy_status).to be_failed
- expect(project.pages_metadatum).not_to be_deployed
+ expect(project.pages_deployed?).to eq(false)
end
end
@@ -275,10 +281,9 @@ RSpec.describe Projects::UpdatePagesService, feature_category: :pages do
end
end
- it 'creates a new pages deployment and mark it as deployed' do
- expect do
- expect(service.execute[:status]).to eq(:success)
- end.to change { project.pages_deployments.count }.by(1)
+ it 'creates a new pages deployment' do
+ expect { expect(service.execute[:status]).to eq(:success) }
+ .to change { project.pages_deployments.count }.by(1)
deployment = project.pages_deployments.last
expect(deployment.ci_build_id).to eq(build.id)
@@ -287,16 +292,12 @@ RSpec.describe Projects::UpdatePagesService, feature_category: :pages do
it_behaves_like 'old deployments'
context 'when newer deployment present' do
- before do
+ it 'fails with outdated reference message' do
new_pipeline = create(:ci_pipeline, project: project, sha: project.commit('HEAD').sha)
new_build = create(:ci_build, name: 'pages', pipeline: new_pipeline, ref: 'HEAD')
- new_deployment = create(:pages_deployment, ci_build: new_build, project: project)
- project.update_pages_deployment!(new_deployment)
- end
+ create(:pages_deployment, project: project, ci_build: new_build)
- it 'fails with outdated reference message' do
expect(service.execute[:status]).to eq(:error)
- expect(project.reload.pages_metadatum).not_to be_deployed
deploy_status = GenericCommitStatus.last
expect(deploy_status).to be_failed
@@ -308,16 +309,14 @@ RSpec.describe Projects::UpdatePagesService, feature_category: :pages do
it 'fails when uploaded deployment size is wrong' do
allow_next_instance_of(PagesDeployment) do |deployment|
allow(deployment)
- .to receive(:size)
- .and_return(file.size + 1)
+ .to receive(:file)
+ .and_return(instance_double(Pages::DeploymentUploader, size: file.size + 1))
end
expect(service.execute[:status]).not_to eq(:success)
- expect(GenericCommitStatus.last.description).to eq('The uploaded artifact size does not match the expected value')
- project.pages_metadatum.reload
- expect(project.pages_metadatum).not_to be_deployed
- expect(project.pages_metadatum.pages_deployment).to be_nil
+ expect(GenericCommitStatus.last.description)
+ .to eq('The uploaded artifact size does not match the expected value')
end
end
end
@@ -335,9 +334,8 @@ RSpec.describe Projects::UpdatePagesService, feature_category: :pages do
end
it 'fails with exception raised' do
- expect do
- service.execute
- end.to raise_error("Validation failed: File sha256 can't be blank")
+ expect { service.execute }
+ .to raise_error("Validation failed: File sha256 can't be blank")
end
end
diff --git a/spec/services/projects/update_repository_storage_service_spec.rb b/spec/services/projects/update_repository_storage_service_spec.rb
index d173d23a1d6..b81fc8bf633 100644
--- a/spec/services/projects/update_repository_storage_service_spec.rb
+++ b/spec/services/projects/update_repository_storage_service_spec.rb
@@ -79,6 +79,30 @@ RSpec.describe Projects::UpdateRepositoryStorageService, feature_category: :sour
end
end
+ context 'when touch raises an exception' do
+ let(:exception) { RuntimeError.new('Boom') }
+
+ it 'marks the storage move as failed and restores read-write access' do
+ allow(repository_storage_move).to receive(:container).and_return(project)
+
+ allow(project).to receive(:touch).and_wrap_original do
+ project.assign_attributes(updated_at: 1.second.ago)
+ raise exception
+ end
+
+ expect(project_repository_double).to receive(:replicate)
+ .with(project.repository.raw)
+ expect(project_repository_double).to receive(:checksum)
+ .and_return(checksum)
+
+ expect { subject.execute }.to raise_error(exception)
+ project.reload
+
+ expect(project).not_to be_repository_read_only
+ expect(repository_storage_move.reload).to be_failed
+ end
+ end
+
context 'when the filesystems are the same' do
before do
expect(Gitlab::GitalyClient).to receive(:filesystem_id).twice.and_return(SecureRandom.uuid)
diff --git a/spec/services/projects/update_statistics_service_spec.rb b/spec/services/projects/update_statistics_service_spec.rb
index f6565853460..5311b8daeb1 100644
--- a/spec/services/projects/update_statistics_service_spec.rb
+++ b/spec/services/projects/update_statistics_service_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe Projects::UpdateStatisticsService, feature_category: :groups_and_
using RSpec::Parameterized::TableSyntax
let(:service) { described_class.new(project, nil, statistics: statistics) }
- let(:statistics) { %w(repository_size) }
+ let(:statistics) { %w[repository_size] }
describe '#execute' do
context 'with a non-existing project' do
@@ -23,13 +23,13 @@ RSpec.describe Projects::UpdateStatisticsService, feature_category: :groups_and_
let_it_be(:project) { create(:project) }
where(:statistics, :method_caches) do
- [] | %i(size recent_objects_size commit_count)
- ['repository_size'] | %i(size recent_objects_size)
- [:repository_size] | %i(size recent_objects_size)
+ [] | %i[size recent_objects_size commit_count]
+ ['repository_size'] | %i[size recent_objects_size]
+ [:repository_size] | %i[size recent_objects_size]
[:lfs_objects_size] | nil
[:commit_count] | [:commit_count]
- [:repository_size, :commit_count] | %i(size recent_objects_size commit_count)
- [:repository_size, :commit_count, :lfs_objects_size] | %i(size recent_objects_size commit_count)
+ [:repository_size, :commit_count] | %i[size recent_objects_size commit_count]
+ [:repository_size, :commit_count, :lfs_objects_size] | %i[size recent_objects_size commit_count]
end
with_them do
@@ -59,7 +59,7 @@ RSpec.describe Projects::UpdateStatisticsService, feature_category: :groups_and_
it 'invalidates and refreshes Wiki size' do
expect(project.statistics).to receive(:refresh!).with(only: statistics).and_call_original
- expect(project.wiki.repository).to receive(:expire_method_caches).with(%i(size)).and_call_original
+ expect(project.wiki.repository).to receive(:expire_method_caches).with(%i[size]).and_call_original
service.execute
end
diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb
index 2c34d6a59be..1c9c6323e96 100644
--- a/spec/services/quick_actions/interpret_service_spec.rb
+++ b/spec/services/quick_actions/interpret_service_spec.rb
@@ -80,7 +80,7 @@ RSpec.describe QuickActions::InterpretService, feature_category: :team_planning
it 'returns the title message' do
_, _, message = service.execute(content, issuable)
- expect(message).to eq(%{Changed the title to "A brand new title".})
+ expect(message).to eq(%(Changed the title to "A brand new title".))
end
end
@@ -695,7 +695,7 @@ RSpec.describe QuickActions::InterpretService, feature_category: :team_planning
_, _, message = service.execute(content, issuable)
if tag_message.present?
- expect(message).to eq(%{Tagged this commit to #{tag_name} with "#{tag_message}".})
+ expect(message).to eq(%(Tagged this commit to #{tag_name} with "#{tag_message}".))
else
expect(message).to eq("Tagged this commit to #{tag_name}.")
end
@@ -1979,7 +1979,7 @@ RSpec.describe QuickActions::InterpretService, feature_category: :team_planning
context '/board_move command' do
let_it_be(:todo) { create(:label, project: project, title: 'To Do') }
let_it_be(:inreview) { create(:label, project: project, title: 'In Review') }
- let(:content) { %{/board_move ~"#{inreview.title}"} }
+ let(:content) { %(/board_move ~"#{inreview.title}") }
let_it_be(:board) { create(:board, project: project) }
let_it_be(:todo_list) { create(:list, board: board, label: todo) }
@@ -2043,14 +2043,14 @@ RSpec.describe QuickActions::InterpretService, feature_category: :team_planning
context 'if multiple labels are given' do
let(:issuable) { issue }
- let(:content) { %{/board_move ~"#{inreview.title}" ~"#{todo.title}"} }
+ let(:content) { %(/board_move ~"#{inreview.title}" ~"#{todo.title}") }
it_behaves_like 'failed command', 'Failed to move this issue because only a single label can be provided.'
end
context 'if the given label is not a list on the board' do
let(:issuable) { issue }
- let(:content) { %{/board_move ~"#{bug.title}"} }
+ let(:content) { %(/board_move ~"#{bug.title}") }
it_behaves_like 'failed command', 'Failed to move this issue because label was not found.'
end
@@ -2187,6 +2187,67 @@ RSpec.describe QuickActions::InterpretService, feature_category: :team_planning
end
end
+ context 'request_changes command' do
+ let(:merge_request) { create(:merge_request, source_project: project) }
+ let(:content) { '/request_changes' }
+
+ context "when `mr_request_changes` feature flag is disabled" do
+ before do
+ stub_feature_flags(mr_request_changes: false)
+ end
+
+ it 'does not call MergeRequests::UpdateReviewerStateService' do
+ expect(MergeRequests::UpdateReviewerStateService).not_to receive(:new)
+
+ service.execute(content, merge_request)
+ end
+ end
+
+ context "when the user is a reviewer" do
+ before do
+ create(:merge_request_reviewer, merge_request: merge_request, reviewer: current_user)
+ end
+
+ it 'calls MergeRequests::UpdateReviewerStateService with requested_changes' do
+ expect_next_instance_of(
+ MergeRequests::UpdateReviewerStateService,
+ project: project, current_user: current_user
+ ) do |service|
+ expect(service).to receive(:execute).with(merge_request, "requested_changes").and_return({ status: :success })
+ end
+
+ _, _, message = service.execute(content, merge_request)
+
+ expect(message).to eq('Changes requested to the current merge request.')
+ end
+
+ it 'returns error message from MergeRequests::UpdateReviewerStateService' do
+ expect_next_instance_of(
+ MergeRequests::UpdateReviewerStateService,
+ project: project, current_user: current_user
+ ) do |service|
+ expect(service).to receive(:execute).with(merge_request, "requested_changes").and_return({ status: :error, message: 'Error' })
+ end
+
+ _, _, message = service.execute(content, merge_request)
+
+ expect(message).to eq('Error')
+ end
+ end
+
+ context "when the user is not a reviewer" do
+ it 'does not call MergeRequests::UpdateReviewerStateService' do
+ expect(MergeRequests::UpdateReviewerStateService).not_to receive(:new)
+
+ service.execute(content, merge_request)
+ end
+ end
+
+ it_behaves_like 'approve command unavailable' do
+ let(:issuable) { issue }
+ end
+ end
+
it_behaves_like 'issues link quick action', :relate do
let(:user) { developer }
end
@@ -2422,6 +2483,17 @@ RSpec.describe QuickActions::InterpretService, feature_category: :team_planning
expect(merge_request.approved_by_users).to be_empty
end
+ it 'calls MergeRequests::UpdateReviewerStateService' do
+ expect_next_instance_of(
+ MergeRequests::UpdateReviewerStateService,
+ project: project, current_user: current_user
+ ) do |service|
+ expect(service).to receive(:execute).with(merge_request, "unreviewed")
+ end
+
+ service.execute(content, merge_request)
+ end
+
context "when the user can't unapprove" do
before do
project.team.truncate
diff --git a/spec/services/releases/create_service_spec.rb b/spec/services/releases/create_service_spec.rb
index 0170c3abcaf..3504f00412c 100644
--- a/spec/services/releases/create_service_spec.rb
+++ b/spec/services/releases/create_service_spec.rb
@@ -56,21 +56,25 @@ RSpec.describe Releases::CreateService, feature_category: :continuous_integratio
end
context 'when project is a catalog resource' do
- let(:ref) { 'master' }
+ let(:project) { create(:project, :catalog_resource_with_components, create_tag: 'final') }
let!(:ci_catalog_resource) { create(:ci_catalog_resource, project: project) }
+ let(:ref) { 'master' }
context 'and it is valid' do
- let_it_be(:project) { create(:project, :repository, description: 'our components') }
-
it_behaves_like 'a successful release creation'
end
- context 'and it is invalid' do
+ context 'and it is an invalid resource' do
+ let_it_be(:project) { create(:project, :repository) }
+
it 'raises an error and does not update the release' do
result = service.execute
expect(result[:status]).to eq(:error)
- expect(result[:message]).to eq('Project must have a description')
+ expect(result[:http_status]).to eq(422)
+ expect(result[:message]).to eq(
+ 'Project must have a description, ' \
+ 'Project must contain components. Ensure you are using the correct directory structure')
end
end
end
@@ -104,6 +108,7 @@ RSpec.describe Releases::CreateService, feature_category: :continuous_integratio
result = service.execute
expect(result[:status]).to eq(:error)
+ expect(result[:http_status]).to eq(403)
end
end
@@ -139,6 +144,7 @@ RSpec.describe Releases::CreateService, feature_category: :continuous_integratio
it 'raises an error and does not update the release' do
result = service.execute
expect(result[:status]).to eq(:error)
+ expect(result[:http_status]).to eq(409)
expect(project.releases.find_by(tag: tag_name).description).to eq(description)
end
end
@@ -150,6 +156,7 @@ RSpec.describe Releases::CreateService, feature_category: :continuous_integratio
result = service.execute
expect(result[:status]).to eq(:error)
+ expect(result[:http_status]).to eq(400)
expect(result[:message]).to eq("Milestone(s) not found: #{inexistent_milestone_tag}")
end
@@ -159,6 +166,7 @@ RSpec.describe Releases::CreateService, feature_category: :continuous_integratio
result = service.execute
expect(result[:status]).to eq(:error)
+ expect(result[:http_status]).to eq(400)
expect(result[:message]).to eq("Milestone id(s) not found: #{inexistent_milestone_id}")
end
end
@@ -244,6 +252,7 @@ RSpec.describe Releases::CreateService, feature_category: :continuous_integratio
result = service.execute
expect(result[:status]).to eq(:error)
+ expect(result[:http_status]).to eq(400)
expect(result[:message]).to eq("Milestone(s) not found: #{inexistent_title}")
end
@@ -260,6 +269,7 @@ RSpec.describe Releases::CreateService, feature_category: :continuous_integratio
result = service.execute
expect(result[:status]).to eq(:error)
+ expect(result[:http_status]).to eq(400)
expect(result[:message]).to eq("Milestone id(s) not found: #{non_existing_record_id}")
end
end
diff --git a/spec/services/service_desk/custom_email_verifications/update_service_spec.rb b/spec/services/service_desk/custom_email_verifications/update_service_spec.rb
index d882cd8635a..f87952d1d0e 100644
--- a/spec/services/service_desk/custom_email_verifications/update_service_spec.rb
+++ b/spec/services/service_desk/custom_email_verifications/update_service_spec.rb
@@ -22,6 +22,7 @@ RSpec.describe ServiceDesk::CustomEmailVerifications::UpdateService, feature_cat
end
let(:expected_error_message) { error_parameter_missing }
+ let(:expected_custom_email_enabled) { false }
let(:logger_params) { { category: 'custom_email_verification' } }
before do
@@ -30,7 +31,7 @@ RSpec.describe ServiceDesk::CustomEmailVerifications::UpdateService, feature_cat
end
shared_examples 'a failing verification process' do |expected_error_identifier|
- it 'refuses to verify and sends result emails' do
+ it 'refuses to verify and sends result emails', :aggregate_failures do
expect(Notify).to receive(:service_desk_verification_result_email).twice
expect(Gitlab::AppLogger).to receive(:info).with(logger_params.merge(
@@ -52,7 +53,7 @@ RSpec.describe ServiceDesk::CustomEmailVerifications::UpdateService, feature_cat
end
shared_examples 'an early exit from the verification process' do |expected_state|
- it 'exits early' do
+ it 'exits early', :aggregate_failures do
expect(Notify).to receive(:service_desk_verification_result_email).exactly(0).times
expect(Gitlab::AppLogger).to receive(:warn).with(logger_params.merge(
@@ -65,7 +66,7 @@ RSpec.describe ServiceDesk::CustomEmailVerifications::UpdateService, feature_cat
verification.reset
expect(response).to be_error
- expect(settings).not_to be_custom_email_enabled
+ expect(settings.custom_email_enabled).to eq expected_custom_email_enabled
expect(verification.state).to eq expected_state
end
end
@@ -179,6 +180,26 @@ RSpec.describe ServiceDesk::CustomEmailVerifications::UpdateService, feature_cat
it_behaves_like 'a failing verification process', 'mail_not_received_within_timeframe'
end
+
+ context 'when already verified' do
+ let(:expected_error_message) { error_already_finished }
+
+ before do
+ verification.mark_as_finished!
+ end
+
+ it_behaves_like 'an early exit from the verification process', 'finished'
+
+ context 'when enabled' do
+ let(:expected_custom_email_enabled) { true }
+
+ before do
+ settings.update!(custom_email_enabled: true)
+ end
+
+ it_behaves_like 'an early exit from the verification process', 'finished'
+ end
+ end
end
end
end
diff --git a/spec/services/service_desk/custom_emails/create_service_spec.rb b/spec/services/service_desk/custom_emails/create_service_spec.rb
index 2029c9a0c3f..e165131bcf9 100644
--- a/spec/services/service_desk/custom_emails/create_service_spec.rb
+++ b/spec/services/service_desk/custom_emails/create_service_spec.rb
@@ -156,7 +156,7 @@ RSpec.describe ServiceDesk::CustomEmails::CreateService, feature_category: :serv
}
end
- it 'creates all records returns a successful response' do
+ it 'creates all records and returns a successful response' do
# Because we also log in ServiceDesk::CustomEmailVerifications::CreateService
expect(Gitlab::AppLogger).to receive(:info).with({ category: 'custom_email_verification' }).once
expect(Gitlab::AppLogger).to receive(:info).with(logger_params).once
@@ -174,7 +174,8 @@ RSpec.describe ServiceDesk::CustomEmails::CreateService, feature_category: :serv
smtp_address: params[:smtp_address],
smtp_port: params[:smtp_port].to_i,
smtp_username: params[:smtp_username],
- smtp_password: params[:smtp_password]
+ smtp_password: params[:smtp_password],
+ smtp_authentication: nil
)
expect(project.service_desk_custom_email_verification).to have_attributes(
state: 'started',
@@ -183,6 +184,30 @@ RSpec.describe ServiceDesk::CustomEmails::CreateService, feature_category: :serv
)
end
+ context 'with optional smtp_authentication parameter' do
+ before do
+ params[:smtp_authentication] = 'login'
+ end
+
+ it 'sets authentication and returns a successful response' do
+ response = service.execute
+ project.reset
+
+ expect(response).to be_success
+ expect(project.service_desk_custom_email_credential.smtp_authentication).to eq 'login'
+ end
+
+ context 'with unsupported value' do
+ let(:expected_error_message) { error_cannot_create_custom_email }
+
+ before do
+ params[:smtp_authentication] = 'unsupported'
+ end
+
+ it_behaves_like 'a failing service that does not create records'
+ end
+ end
+
context 'when custom email aready exists' do
let!(:settings) { create(:service_desk_setting, project: project, custom_email: 'user@example.com') }
let!(:credential) { create(:service_desk_custom_email_credential, project: project) }
diff --git a/spec/services/service_desk_settings/update_service_spec.rb b/spec/services/service_desk_settings/update_service_spec.rb
index 27978225bcf..a9e54012075 100644
--- a/spec/services/service_desk_settings/update_service_spec.rb
+++ b/spec/services/service_desk_settings/update_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe ServiceDeskSettings::UpdateService, feature_category: :service_desk do
+RSpec.describe ServiceDeskSettings::UpdateService, :aggregate_failures, feature_category: :service_desk do
describe '#execute' do
let_it_be(:settings) do
create(:service_desk_setting, outgoing_name: 'original name', custom_email: 'user@example.com')
@@ -12,14 +12,17 @@ RSpec.describe ServiceDeskSettings::UpdateService, feature_category: :service_de
let_it_be(:user) { create(:user) }
context 'with valid params' do
- let(:params) { { outgoing_name: 'some name', project_key: 'foo' } }
+ let(:params) { { outgoing_name: 'some name', project_key: 'foo', add_external_participants_from_cc: true } }
it 'updates service desk settings' do
response = described_class.new(settings.project, user, params).execute
expect(response).to be_success
- expect(settings.reload.outgoing_name).to eq 'some name'
- expect(settings.reload.project_key).to eq 'foo'
+ expect(settings.reset).to have_attributes(
+ outgoing_name: 'some name',
+ project_key: 'foo',
+ add_external_participants_from_cc: true
+ )
end
context 'with custom email verification in finished state' do
@@ -39,6 +42,23 @@ RSpec.describe ServiceDeskSettings::UpdateService, feature_category: :service_de
expect(Gitlab::AppLogger).to have_received(:info).with({ category: 'custom_email' })
end
end
+
+ context 'when issue_email_participants feature flag is disabled' do
+ before do
+ stub_feature_flags(issue_email_participants: false)
+ end
+
+ it 'updates service desk setting but not add_external_participants_from_cc value' do
+ response = described_class.new(settings.project, user, params).execute
+
+ expect(response).to be_success
+ expect(settings.reset).to have_attributes(
+ outgoing_name: 'some name',
+ project_key: 'foo',
+ add_external_participants_from_cc: false
+ )
+ end
+ end
end
context 'when project_key is an empty string' do
diff --git a/spec/services/spam/spam_action_service_spec.rb b/spec/services/spam/spam_action_service_spec.rb
index 4133609d9ae..d8fd09ebd07 100644
--- a/spec/services/spam/spam_action_service_spec.rb
+++ b/spec/services/spam/spam_action_service_spec.rb
@@ -85,6 +85,26 @@ RSpec.describe Spam::SpamActionService, feature_category: :instance_resiliency d
end
end
+ shared_examples 'calls SpamAbuseEventsWorker with correct arguments' do
+ let(:params) do
+ {
+ user_id: user.id,
+ title: target.title,
+ description: target.spam_description,
+ source_ip: fake_ip,
+ user_agent: fake_user_agent,
+ noteable_type: target_type,
+ verdict: verdict
+ }
+ end
+
+ it do
+ expect(::Abuse::SpamAbuseEventsWorker).to receive(:perform_async).with(params)
+
+ subject
+ end
+ end
+
shared_examples 'execute spam action service' do |target_type|
let(:fake_captcha_verification_service) { double(:captcha_verification_service) }
let(:fake_verdict_service) { double(:spam_verdict_service) }
@@ -161,6 +181,12 @@ RSpec.describe Spam::SpamActionService, feature_category: :instance_resiliency d
it 'does not create a spam log' do
expect { subject }.not_to change(SpamLog, :count)
end
+
+ it 'does not call SpamAbuseEventsWorker' do
+ expect(::Abuse::SpamAbuseEventsWorker).not_to receive(:perform_async)
+
+ subject
+ end
end
context 'when spammable attributes have changed' do
@@ -213,6 +239,11 @@ RSpec.describe Spam::SpamActionService, feature_category: :instance_resiliency d
it_behaves_like 'creates a spam log', target_type
+ it_behaves_like 'calls SpamAbuseEventsWorker with correct arguments' do
+ let(:verdict) { DISALLOW }
+ let(:target_type) { target_type }
+ end
+
it 'marks as spam' do
response = subject
@@ -231,6 +262,11 @@ RSpec.describe Spam::SpamActionService, feature_category: :instance_resiliency d
it_behaves_like 'creates a spam log', target_type
+ it_behaves_like 'calls SpamAbuseEventsWorker with correct arguments' do
+ let(:verdict) { BLOCK_USER }
+ let(:target_type) { target_type }
+ end
+
it 'marks as spam' do
response = subject
@@ -254,6 +290,11 @@ RSpec.describe Spam::SpamActionService, feature_category: :instance_resiliency d
it_behaves_like 'creates a spam log', target_type
+ it_behaves_like 'calls SpamAbuseEventsWorker with correct arguments' do
+ let(:verdict) { CONDITIONAL_ALLOW }
+ let(:target_type) { target_type }
+ end
+
it 'does not mark as spam' do
response = subject
@@ -276,6 +317,12 @@ RSpec.describe Spam::SpamActionService, feature_category: :instance_resiliency d
it_behaves_like 'creates a spam log', target_type
+ it 'does not call SpamAbuseEventsWorker' do
+ expect(::Abuse::SpamAbuseEventsWorker).not_to receive(:perform_async)
+
+ subject
+ end
+
it 'does not mark as spam' do
response = subject
@@ -300,6 +347,12 @@ RSpec.describe Spam::SpamActionService, feature_category: :instance_resiliency d
expect { subject }.not_to change(SpamLog, :count)
end
+ it 'does not call SpamAbuseEventsWorker' do
+ expect(::Abuse::SpamAbuseEventsWorker).not_to receive(:perform_async)
+
+ subject
+ end
+
it 'clears spam flags' do
expect(target).to receive(:clear_spam_flags!)
@@ -316,6 +369,12 @@ RSpec.describe Spam::SpamActionService, feature_category: :instance_resiliency d
expect { subject }.not_to change(SpamLog, :count)
end
+ it 'does not call SpamAbuseEventsWorker' do
+ expect(::Abuse::SpamAbuseEventsWorker).not_to receive(:perform_async)
+
+ subject
+ end
+
it 'clears spam flags' do
expect(target).to receive(:clear_spam_flags!)
diff --git a/spec/services/system_notes/issuables_service_spec.rb b/spec/services/system_notes/issuables_service_spec.rb
index bcca1ed0b23..ca6feb6fde2 100644
--- a/spec/services/system_notes/issuables_service_spec.rb
+++ b/spec/services/system_notes/issuables_service_spec.rb
@@ -784,7 +784,7 @@ RSpec.describe ::SystemNotes::IssuablesService, feature_category: :team_planning
service = described_class.new(noteable: issuable, author: author)
expect(service.discussion_lock.note)
- .to eq("unlocked this #{type.to_s.titleize.downcase}")
+ .to eq("unlocked the discussion in this #{type.to_s.titleize.downcase}")
end
end
end
@@ -804,7 +804,7 @@ RSpec.describe ::SystemNotes::IssuablesService, feature_category: :team_planning
service = described_class.new(noteable: issuable, author: author)
expect(service.discussion_lock.note)
- .to eq("locked this #{type.to_s.titleize.downcase}")
+ .to eq("locked the discussion in this #{type.to_s.titleize.downcase}")
end
end
end
diff --git a/spec/services/upload_service_spec.rb b/spec/services/upload_service_spec.rb
index 518d12d5b41..4a8cd46172d 100644
--- a/spec/services/upload_service_spec.rb
+++ b/spec/services/upload_service_spec.rb
@@ -81,7 +81,7 @@ RSpec.describe UploadService, feature_category: :shared do
it 'allows the upload' do
service.override_max_attachment_size = 101.megabytes
- expect(subject.keys).to eq(%i(alt url markdown))
+ expect(subject.keys).to eq(%i[alt url markdown])
end
it 'disallows the upload' do
diff --git a/spec/services/users/refresh_authorized_projects_service_spec.rb b/spec/services/users/refresh_authorized_projects_service_spec.rb
index b36152f81c3..3d88618711b 100644
--- a/spec/services/users/refresh_authorized_projects_service_spec.rb
+++ b/spec/services/users/refresh_authorized_projects_service_spec.rb
@@ -98,6 +98,13 @@ RSpec.describe Users::RefreshAuthorizedProjectsService, feature_category: :user_
service.execute_without_lease
end
+ it 'updates project_authorizations_recalculated_at', :freeze_time do
+ default_date = Time.zone.local('2010')
+ expect do
+ service.execute_without_lease
+ end.to change { user.project_authorizations_recalculated_at }.from(default_date).to(Time.zone.now)
+ end
+
it 'returns a User' do
expect(service.execute_without_lease).to be_an_instance_of(User)
end
diff --git a/spec/services/users/upsert_credit_card_validation_service_spec.rb b/spec/services/users/upsert_credit_card_validation_service_spec.rb
index 4e23b51cae2..e1c5b30115d 100644
--- a/spec/services/users/upsert_credit_card_validation_service_spec.rb
+++ b/spec/services/users/upsert_credit_card_validation_service_spec.rb
@@ -3,20 +3,29 @@
require 'spec_helper'
RSpec.describe Users::UpsertCreditCardValidationService, feature_category: :user_profile do
+ include CryptoHelpers
+
let_it_be(:user) { create(:user) }
let(:user_id) { user.id }
- let(:credit_card_validated_time) { Time.utc(2020, 1, 1) }
+
+ let(:network) { 'American Express' }
+ let(:holder_name) { 'John Smith' }
+ let(:last_digits) { '1111' }
let(:expiration_year) { Date.today.year + 10 }
+ let(:expiration_month) { 1 }
+ let(:expiration_date) { Date.new(expiration_year, expiration_month, -1) }
+ let(:credit_card_validated_at) { Time.utc(2020, 1, 1) }
+
let(:params) do
{
user_id: user_id,
- credit_card_validated_at: credit_card_validated_time,
+ credit_card_validated_at: credit_card_validated_at,
credit_card_expiration_year: expiration_year,
- credit_card_expiration_month: 1,
- credit_card_holder_name: 'John Smith',
- credit_card_type: 'AmericanExpress',
- credit_card_mask_number: '1111'
+ credit_card_expiration_month: expiration_month,
+ credit_card_holder_name: holder_name,
+ credit_card_type: network,
+ credit_card_mask_number: last_digits
}
end
@@ -25,82 +34,97 @@ RSpec.describe Users::UpsertCreditCardValidationService, feature_category: :user
context 'successfully set credit card validation record for the user' do
context 'when user does not have credit card validation record' do
- it 'creates the credit card validation and returns a success' do
+ it 'creates the credit card validation and returns a success', :aggregate_failures do
expect(user.credit_card_validated_at).to be nil
- result = service.execute
+ service_result = service.execute
- expect(result.status).to eq(:success)
+ expect(service_result.status).to eq(:success)
+ expect(service_result.message).to eq(_('Credit card validation record saved'))
user.reload
expect(user.credit_card_validation).to have_attributes(
- credit_card_validated_at: credit_card_validated_time,
- network: 'AmericanExpress',
- holder_name: 'John Smith',
- last_digits: 1111,
- expiration_date: Date.new(expiration_year, 1, 31)
+ credit_card_validated_at: credit_card_validated_at,
+ network_hash: sha256(network.downcase),
+ holder_name_hash: sha256(holder_name.downcase),
+ last_digits_hash: sha256(last_digits),
+ expiration_date_hash: sha256(expiration_date.to_s)
)
end
end
context 'when user has credit card validation record' do
- let(:old_time) { Time.utc(1999, 2, 2) }
+ let(:previous_credit_card_validated_at) { Time.utc(1999, 2, 2) }
before do
- create(:credit_card_validation, user: user, credit_card_validated_at: old_time)
+ create(:credit_card_validation, user: user, credit_card_validated_at: previous_credit_card_validated_at)
end
- it 'updates the credit card validation and returns a success' do
- expect(user.credit_card_validated_at).to eq(old_time)
+ it 'updates the credit card validation record and returns a success', :aggregate_failures do
+ expect(user.credit_card_validated_at).to eq(previous_credit_card_validated_at)
+
+ service_result = service.execute
- result = service.execute
+ expect(service_result.status).to eq(:success)
+ expect(service_result.message).to eq(_('Credit card validation record saved'))
- expect(result.status).to eq(:success)
- expect(user.reload.credit_card_validated_at).to eq(credit_card_validated_time)
+ user.reload
+
+ expect(user.credit_card_validated_at).to eq(credit_card_validated_at)
end
end
end
shared_examples 'returns an error without tracking the exception' do
- it do
+ it 'does not send an exception to Gitlab::ErrorTracking' do
expect(Gitlab::ErrorTracking).not_to receive(:track_exception)
- result = service.execute
+ service.execute
+ end
+
+ it 'returns an error', :aggregate_failures do
+ service_result = service.execute
- expect(result.status).to eq(:error)
+ expect(service_result.status).to eq(:error)
+ expect(service_result.message).to eq(_('Error saving credit card validation record'))
end
end
- shared_examples 'returns an error, tracking the exception' do
- it do
+ shared_examples 'returns an error and tracks the exception' do
+ it 'sends an exception to Gitlab::ErrorTracking' do
expect(Gitlab::ErrorTracking).to receive(:track_exception)
- result = service.execute
+ service.execute
+ end
+
+ it 'returns an error', :aggregate_failures do
+ service_result = service.execute
- expect(result.status).to eq(:error)
+ expect(service_result.status).to eq(:error)
+ expect(service_result.message).to eq(_('Error saving credit card validation record'))
end
end
- context 'when user id does not exist' do
+ context 'when the user_id does not exist' do
let(:user_id) { non_existing_record_id }
it_behaves_like 'returns an error without tracking the exception'
end
- context 'when missing credit_card_validated_at' do
+ context 'when the request is missing the credit_card_validated_at field' do
let(:params) { { user_id: user_id } }
- it_behaves_like 'returns an error, tracking the exception'
+ it_behaves_like 'returns an error and tracks the exception'
end
- context 'when missing user id' do
- let(:params) { { credit_card_validated_at: credit_card_validated_time } }
+ context 'when the request is missing the user_id field' do
+ let(:params) { { credit_card_validated_at: credit_card_validated_at } }
- it_behaves_like 'returns an error, tracking the exception'
+ it_behaves_like 'returns an error and tracks the exception'
end
- context 'when unexpected exception happen' do
+ context 'when there is an unexpected error' do
let(:exception) { StandardError.new }
before do
@@ -109,22 +133,7 @@ RSpec.describe Users::UpsertCreditCardValidationService, feature_category: :user
end
end
- it 'tracks the exception and returns an error' do
- logged_params = {
- credit_card_validated_at: credit_card_validated_time,
- expiration_date: Date.new(expiration_year, 1, 31),
- holder_name: "John Smith",
- last_digits: 1111,
- network: "AmericanExpress",
- user_id: user_id
- }
-
- expect(Gitlab::ErrorTracking).to receive(:track_exception).with(exception, class: described_class.to_s, params: logged_params)
-
- result = service.execute
-
- expect(result.status).to eq(:error)
- end
+ it_behaves_like 'returns an error and tracks the exception'
end
end
end
diff --git a/spec/services/vs_code/settings/delete_service_spec.rb b/spec/services/vs_code/settings/delete_service_spec.rb
new file mode 100644
index 00000000000..fd19c01569f
--- /dev/null
+++ b/spec/services/vs_code/settings/delete_service_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe VsCode::Settings::DeleteService, feature_category: :web_ide do
+ describe '#execute' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:other_user) { create(:user) }
+ let_it_be(:setting_one) { create(:vscode_setting, user: user) }
+ let_it_be(:setting_two) { create(:vscode_setting, setting_type: 'extensions', user: user) }
+ let_it_be(:setting_three) { create(:vscode_setting, setting_type: 'extensions', user: other_user) }
+
+ subject { described_class.new(current_user: user).execute }
+
+ it 'deletes all vscode_settings belonging to the current user' do
+ expect { subject }
+ .to change { User.find(user.id).vscode_settings.count }.from(2).to(0)
+ .and not_change { User.find(other_user.id).vscode_settings.count }
+ end
+ end
+end
diff --git a/spec/services/web_hook_service_spec.rb b/spec/services/web_hook_service_spec.rb
index 89346353db2..c33273348f6 100644
--- a/spec/services/web_hook_service_spec.rb
+++ b/spec/services/web_hook_service_spec.rb
@@ -13,6 +13,8 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state,
{ before: 'oldrev', after: 'newrev', ref: 'ref' }
end
+ let(:serialized_data) { data.deep_stringify_keys }
+
let(:service_instance) { described_class.new(project_hook, data, :push_hooks) }
describe '#initialize' do
@@ -426,9 +428,9 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state,
expect(WebHooks::LogExecutionWorker).to receive(:perform_async)
.with(
project_hook.id,
- hash_including(default_log_data),
- :ok,
- nil
+ hash_including(default_log_data.deep_stringify_keys),
+ 'ok',
+ ''
)
service_instance.execute
@@ -456,10 +458,10 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state,
default_log_data.merge(
response_body: 'Bad request',
response_status: 400
- )
+ ).deep_stringify_keys
),
- :failed,
- nil
+ 'failed',
+ ''
)
service_instance.execute
@@ -480,10 +482,10 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state,
response_body: '',
response_status: 'internal error',
internal_error_message: 'Some HTTP Post error'
- )
+ ).deep_stringify_keys
),
- :error,
- nil
+ 'error',
+ ''
)
service_instance.execute
@@ -499,9 +501,9 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state,
expect(WebHooks::LogExecutionWorker).to receive(:perform_async)
.with(
project_hook.id,
- hash_including(default_log_data.merge(response_body: '')),
- :ok,
- nil
+ hash_including(default_log_data.merge(response_body: '').deep_stringify_keys),
+ 'ok',
+ ''
)
service_instance.execute
@@ -520,9 +522,9 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state,
expect(WebHooks::LogExecutionWorker).to receive(:perform_async)
.with(
project_hook.id,
- hash_including(default_log_data.merge(response_body: stripped_body)),
- :ok,
- nil
+ hash_including(default_log_data.merge(response_body: stripped_body).deep_stringify_keys),
+ 'ok',
+ ''
)
service_instance.execute
@@ -553,9 +555,9 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state,
expect(WebHooks::LogExecutionWorker).to receive(:perform_async)
.with(
project_hook.id,
- hash_including(default_log_data.merge(response_headers: expected_response_headers)),
- :ok,
- nil
+ hash_including(default_log_data.merge(response_headers: expected_response_headers).deep_stringify_keys),
+ 'ok',
+ ''
)
service_instance.execute
@@ -578,9 +580,9 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state,
expect(WebHooks::LogExecutionWorker).to receive(:perform_async)
.with(
project_hook.id,
- hash_including(default_log_data.merge(response_headers: expected_response_headers)),
- :ok,
- nil
+ hash_including(default_log_data.merge(response_headers: expected_response_headers).deep_stringify_keys),
+ 'ok',
+ ''
)
service_instance.execute
@@ -596,9 +598,9 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state,
expect(WebHooks::LogExecutionWorker).to receive(:perform_async)
.with(
project_hook.id,
- hash_including(default_log_data),
- :ok,
- nil
+ hash_including(default_log_data.deep_stringify_keys),
+ 'ok',
+ ''
)
.and_raise(
Gitlab::SidekiqMiddleware::SizeLimiter::ExceedLimitError.new(WebHooks::LogExecutionWorker, 100, 50)
@@ -607,9 +609,11 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state,
expect(WebHooks::LogExecutionWorker).to receive(:perform_async)
.with(
project_hook.id,
- hash_including(default_log_data.merge(request_data: WebHookLog::OVERSIZE_REQUEST_DATA)),
- :ok,
- nil
+ hash_including(default_log_data.merge(
+ request_data: WebHookLog::OVERSIZE_REQUEST_DATA
+ ).deep_stringify_keys),
+ 'ok',
+ ''
)
.and_call_original
.ordered
@@ -636,7 +640,9 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state,
describe '#async_execute' do
def expect_to_perform_worker(hook)
- expect(WebHookWorker).to receive(:perform_async).with(hook.id, data, 'push_hooks', an_instance_of(Hash))
+ expect(WebHookWorker).to receive(:perform_async).with(
+ hook.id, serialized_data, 'push_hooks', an_instance_of(Hash)
+ )
end
def expect_to_rate_limit(hook, threshold:, throttled: false)
diff --git a/spec/sidekiq_cluster/sidekiq_cluster_spec.rb b/spec/sidekiq_cluster/sidekiq_cluster_spec.rb
index 25a600405fe..ec5e5d85eeb 100644
--- a/spec/sidekiq_cluster/sidekiq_cluster_spec.rb
+++ b/spec/sidekiq_cluster/sidekiq_cluster_spec.rb
@@ -31,7 +31,7 @@ RSpec.describe Gitlab::SidekiqCluster do # rubocop:disable RSpec/FilePath
).and_return(2)
expect(Process).to receive(:detach).ordered.with(2)
- described_class.start([%w(foo), %w(bar baz)], env: :production, directory: 'foo/bar', max_concurrency: 20, min_concurrency: 10)
+ described_class.start([%w[foo], %w[bar baz]], env: :production, directory: 'foo/bar', max_concurrency: 20, min_concurrency: 10)
end
it 'starts Sidekiq with the given queues and sensible default options' do
@@ -45,10 +45,10 @@ RSpec.describe Gitlab::SidekiqCluster do # rubocop:disable RSpec/FilePath
dryrun: false
}
- expect(described_class).to receive(:start_sidekiq).ordered.with(%w(foo bar baz), expected_options)
- expect(described_class).to receive(:start_sidekiq).ordered.with(%w(solo), expected_options)
+ expect(described_class).to receive(:start_sidekiq).ordered.with(%w[foo bar baz], expected_options)
+ expect(described_class).to receive(:start_sidekiq).ordered.with(%w[solo], expected_options)
- described_class.start([%w(foo bar baz), %w(solo)])
+ described_class.start([%w[foo bar baz], %w[solo]])
end
end
@@ -67,7 +67,7 @@ RSpec.describe Gitlab::SidekiqCluster do # rubocop:disable RSpec/FilePath
allow(Process).to receive(:spawn).and_return(1)
allow(Process).to receive(:detach).with(1).and_return(waiter_thread)
- expect(described_class.start_sidekiq(%w(foo), **options)).to eq(waiter_thread)
+ expect(described_class.start_sidekiq(%w[foo], **options)).to eq(waiter_thread)
end
it 'handles duplicate queue names' do
@@ -77,7 +77,7 @@ RSpec.describe Gitlab::SidekiqCluster do # rubocop:disable RSpec/FilePath
.and_return(1)
allow(Process).to receive(:detach).with(1).and_return(waiter_thread)
- expect(described_class.start_sidekiq(%w(foo foo bar baz), **options)).to eq(waiter_thread)
+ expect(described_class.start_sidekiq(%w[foo foo bar baz], **options)).to eq(waiter_thread)
end
it 'runs the sidekiq process in a new process group' do
@@ -87,15 +87,15 @@ RSpec.describe Gitlab::SidekiqCluster do # rubocop:disable RSpec/FilePath
.and_return(1)
allow(Process).to receive(:detach).with(1).and_return(waiter_thread)
- expect(described_class.start_sidekiq(%w(foo bar baz), **options)).to eq(waiter_thread)
+ expect(described_class.start_sidekiq(%w[foo bar baz], **options)).to eq(waiter_thread)
end
end
describe '.count_by_queue' do
it 'tallies the queue counts' do
- queues = [%w(foo), %w(bar baz), %w(foo)]
+ queues = [%w[foo], %w[bar baz], %w[foo]]
- expect(described_class.count_by_queue(queues)).to eq(%w(foo) => 2, %w(bar baz) => 1)
+ expect(described_class.count_by_queue(queues)).to eq(%w[foo] => 2, %w[bar baz] => 1)
end
end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 02db905b8b1..2dd4e92eee9 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -24,7 +24,6 @@ CrystalballEnv.start!
ENV["RAILS_ENV"] = 'test'
ENV["IN_MEMORY_APPLICATION_SETTINGS"] = 'true'
ENV["RSPEC_ALLOW_INVALID_URLS"] = 'true'
-ENV['USE_CI_BUILDS_ROUTING_TABLE'] = 'true'
require_relative '../config/environment'
@@ -302,13 +301,6 @@ RSpec.configure do |config|
# https://gitlab.com/gitlab-org/gitlab/-/issues/385453
stub_feature_flags(vscode_web_ide: false)
- enable_rugged = example.metadata[:enable_rugged].present?
-
- # Disable Rugged features by default
- Gitlab::Git::RuggedImpl::Repository::FEATURE_FLAGS.each do |flag|
- stub_feature_flags(flag => enable_rugged)
- end
-
# Disable `main_branch_over_master` as we migrate
# from `master` to `main` accross our codebase.
# It's done in order to preserve the concistency in tests
@@ -336,8 +328,6 @@ RSpec.configure do |config|
stub_feature_flags(clickhouse_data_collection: false)
stub_feature_flags(vite: false)
-
- allow(Gitlab::GitalyClient).to receive(:can_use_disk?).and_return(enable_rugged)
else
unstub_all_feature_flags
end
@@ -394,11 +384,6 @@ RSpec.configure do |config|
::Gitlab::SafeRequestStore.ensure_request_store { example.run }
end
- config.around(:example, :enable_rugged) do |example|
- # Skip tests that need rugged when using praefect DB.
- example.run unless GitalySetup.praefect_with_db?
- end
-
config.around(:example, :yaml_processor_feature_flag_corectness) do |example|
::Gitlab::Ci::YamlProcessor::FeatureFlags.ensure_correct_usage do
example.run
diff --git a/spec/support/atlassian/jira_connect/schemata.rb b/spec/support/atlassian/jira_connect/schemata.rb
index 73a6833b7cc..de1d6dbf691 100644
--- a/spec/support/atlassian/jira_connect/schemata.rb
+++ b/spec/support/atlassian/jira_connect/schemata.rb
@@ -7,11 +7,11 @@ module Atlassian
{
'type' => 'object',
'additionalProperties' => false,
- 'required' => %w(
+ 'required' => %w[
schemaVersion pipelineId buildNumber updateSequenceNumber
displayName url state issueKeys testInfo references
lastUpdated
- ),
+ ],
'properties' => {
'schemaVersion' => schema_version_type,
'pipelineId' => { 'type' => 'string' },
@@ -24,7 +24,7 @@ module Atlassian
'issueKeys' => issue_keys_type,
'testInfo' => {
'type' => 'object',
- 'required' => %w(totalNumber numberPassed numberFailed numberSkipped),
+ 'required' => %w[totalNumber numberPassed numberFailed numberSkipped],
'properties' => {
'totalNumber' => { 'type' => 'integer' },
'numberFailed' => { 'type' => 'integer' },
@@ -36,11 +36,11 @@ module Atlassian
'type' => 'array',
'items' => {
'type' => 'object',
- 'required' => %w(commit ref),
+ 'required' => %w[commit ref],
'properties' => {
'commit' => {
'type' => 'object',
- 'required' => %w(id repositoryUri),
+ 'required' => %w[id repositoryUri],
'properties' => {
'id' => { 'type' => 'string' },
'repositoryUri' => { 'type' => 'string' }
@@ -48,7 +48,7 @@ module Atlassian
},
'ref' => {
'type' => 'object',
- 'required' => %w(name uri),
+ 'required' => %w[name uri],
'properties' => {
'name' => { 'type' => 'string' },
'uri' => { 'type' => 'string' }
@@ -65,16 +65,16 @@ module Atlassian
{
'type' => 'object',
'additionalProperties' => false,
- 'required' => %w(
+ 'required' => %w[
deploymentSequenceNumber updateSequenceNumber
associations displayName url description lastUpdated
state pipeline environment
- ),
+ ],
'properties' => {
'deploymentSequenceNumber' => { 'type' => 'integer' },
'updateSequenceNumber' => { 'type' => 'integer' },
'associations' => {
- 'type' => %w(array),
+ 'type' => %w[array],
'items' => association_type,
'minItems' => 1
},
@@ -95,9 +95,9 @@ module Atlassian
{
'type' => 'object',
'additionalProperties' => false,
- 'required' => %w(
+ 'required' => %w[
updateSequenceId id key issueKeys summary details
- ),
+ ],
'properties' => {
'id' => { 'type' => 'string' },
'key' => { 'type' => 'string' },
@@ -120,7 +120,7 @@ module Atlassian
'environment' => {
'type' => 'object',
'additionalProperties' => false,
- 'required' => %w(name),
+ 'required' => %w[name],
'properties' => {
'name' => { 'type' => 'string' },
'type' => {
@@ -144,7 +144,7 @@ module Atlassian
{
'type' => 'object',
'additionalProperties' => false,
- 'required' => %w(url status lastUpdated),
+ 'required' => %w[url status lastUpdated],
'properties' => {
'lastUpdated' => iso8601_type,
'url' => { 'type' => 'string' },
@@ -157,7 +157,7 @@ module Atlassian
{
'type' => 'object',
'additionalProperties' => false,
- 'required' => %w(enabled),
+ 'required' => %w[enabled],
'properties' => {
'enabled' => { 'type' => 'boolean' },
'defaultValue' => { 'type' => 'string' },
@@ -182,7 +182,7 @@ module Atlassian
{
'type' => 'object',
'additionalProperties' => false,
- 'required' => %w(id displayName type),
+ 'required' => %w[id displayName type],
'properties' => {
'id' => { 'type' => 'string', 'maxLength' => 255 },
'displayName' => { 'type' => 'string', 'maxLength' => 255 },
@@ -198,7 +198,7 @@ module Atlassian
{
'type' => 'object',
'additionalProperties' => false,
- 'required' => %w(id displayName url),
+ 'required' => %w[id displayName url],
'properties' => {
'id' => { 'type' => 'string', 'maxLength' => 255 },
'displayName' => { 'type' => 'string', 'maxLength' => 255 },
@@ -222,7 +222,7 @@ module Atlassian
{
'type' => 'object',
'additionalProperties' => false,
- 'required' => %w(associationType values),
+ 'required' => %w[associationType values],
'properties' => {
'associationType' => {
'type' => 'string',
@@ -276,7 +276,7 @@ module Atlassian
def provider_metadata
{
'type' => 'object',
- 'required' => %w(product),
+ 'required' => %w[product],
'properties' => { 'product' => { 'type' => 'string' } }
}
end
diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb
index 78d7e57c208..c8fa430c02c 100644
--- a/spec/support/capybara.rb
+++ b/spec/support/capybara.rb
@@ -191,7 +191,7 @@ RSpec.configure do |config|
if example.metadata[:screenshot]
screenshot = example.metadata[:screenshot][:image] || example.metadata[:screenshot][:html]
screenshot&.delete_prefix!(ENV.fetch('CI_PROJECT_DIR', ''))
- example.metadata[:stdout] = %{[[ATTACHMENT|#{screenshot}]]}
+ example.metadata[:stdout] = %([[ATTACHMENT|#{screenshot}]])
end
end
diff --git a/spec/support/capybara_slow_finder.rb b/spec/support/capybara_slow_finder.rb
index 975ddd52c1f..697b288e8b5 100644
--- a/spec/support/capybara_slow_finder.rb
+++ b/spec/support/capybara_slow_finder.rb
@@ -13,13 +13,13 @@ module Capybara
# Inspired by https://github.com/ngauthier/capybara-slow_finder_errors
module SlowFinder
def synchronize(seconds = nil, errors: nil)
- start_time = Gitlab::Metrics::System.monotonic_time
+ start_time = ::Gitlab::Metrics::System.monotonic_time
super
rescue Capybara::ElementNotFound => e
seconds ||= Capybara.default_max_wait_time
- raise e unless seconds > 0 && Gitlab::Metrics::System.monotonic_time - start_time > seconds
+ raise e unless seconds > 0 && ::Gitlab::Metrics::System.monotonic_time - start_time > seconds
message = format(MESSAGE, timeout: seconds)
raise e, "#{$!}\n\n#{message}", e.backtrace
diff --git a/spec/support/database/auto_explain.rb b/spec/support/database/auto_explain.rb
index 799457034a1..11f8f1f899b 100644
--- a/spec/support/database/auto_explain.rb
+++ b/spec/support/database/auto_explain.rb
@@ -119,9 +119,10 @@ module AutoExplain
return false if ENV['CI_JOB_NAME_SLUG'] == 'db-migrate-non-superuser'
return false if connection.database_version.to_s[0..1].to_i < 14
return false if connection.select_one('SHOW is_superuser')['is_superuser'] != 'on'
+ return false if connection.select_one('SELECT pg_stat_file(\'log/pglog.csv\', true)')['pg_stat_file'].nil?
- # This condition matches the pipeline rules for if-merge-request-labels-record-queries
- return true if ENV['CI_MERGE_REQUEST_LABELS']&.include?('pipeline:record-queries')
+ # This condition matches the pipeline rules for if-merge-request
+ return true if %w[detached merged_result].include?(ENV['CI_MERGE_REQUEST_EVENT_TYPE'])
# This condition matches the pipeline rules for if-default-branch-refs
ENV['CI_COMMIT_REF_NAME'] == ENV['CI_DEFAULT_BRANCH'] && !ENV['CI_MERGE_REQUEST_IID']
diff --git a/spec/support/database/click_house/hooks.rb b/spec/support/database/click_house/hooks.rb
index b970d3daf84..77b33b7aaa3 100644
--- a/spec/support/database/click_house/hooks.rb
+++ b/spec/support/database/click_house/hooks.rb
@@ -2,6 +2,8 @@
# rubocop: disable Gitlab/NamespacedClass
class ClickHouseTestRunner
+ include ClickHouseTestHelpers
+
def truncate_tables
ClickHouse::Client.configuration.databases.each_key do |db|
# Select tables with at least one row
@@ -9,6 +11,8 @@ class ClickHouseTestRunner
"(SELECT '#{table}' AS table FROM #{table} LIMIT 1)"
end.join(' UNION ALL ')
+ next if query.empty?
+
tables_with_data = ClickHouse::Client.select(query, db).pluck('table')
tables_with_data.each do |table|
ClickHouse::Client.execute("TRUNCATE TABLE #{table}", db)
@@ -19,17 +23,13 @@ class ClickHouseTestRunner
def ensure_schema
return if @ensure_schema
- ClickHouse::Client.configuration.databases.each_key do |db|
- # drop all tables
- lookup_tables(db).each do |table|
- ClickHouse::Client.execute("DROP TABLE IF EXISTS #{table}", db)
- end
+ clear_db
- # run the schema SQL files
- Dir[Rails.root.join("db/click_house/#{db}/*.sql")].each do |file|
- ClickHouse::Client.execute(File.read(file), db)
- end
- end
+ # run the schema SQL files
+ migrations_paths = ClickHouse::MigrationSupport::Migrator.migrations_paths
+ schema_migration = ClickHouse::MigrationSupport::SchemaMigration
+ migration_context = ClickHouse::MigrationSupport::MigrationContext.new(migrations_paths, schema_migration)
+ migrate(nil, migration_context)
@ensure_schema = true
end
@@ -38,11 +38,7 @@ class ClickHouseTestRunner
def tables_for(db)
@tables ||= {}
- @tables[db] ||= lookup_tables(db)
- end
-
- def lookup_tables(db)
- ClickHouse::Client.select('SHOW TABLES', db).pluck('name')
+ @tables[db] ||= lookup_tables(db) - [ClickHouse::MigrationSupport::SchemaMigration.table_name]
end
end
# rubocop: enable Gitlab/NamespacedClass
@@ -52,8 +48,12 @@ RSpec.configure do |config|
config.around(:each, :click_house) do |example|
with_net_connect_allowed do
- click_house_test_runner.ensure_schema
- click_house_test_runner.truncate_tables
+ if example.example.metadata[:click_house] == :without_migrations
+ click_house_test_runner.clear_db
+ else
+ click_house_test_runner.ensure_schema
+ click_house_test_runner.truncate_tables
+ end
example.run
end
diff --git a/spec/support/database/partitioning_routing_analyzer.rb b/spec/support/database/partitioning_routing_analyzer.rb
new file mode 100644
index 00000000000..b1edd817386
--- /dev/null
+++ b/spec/support/database/partitioning_routing_analyzer.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+RSpec.configure do |config|
+ config.around(:each, :suppress_partitioning_routing_analyzer) do |example|
+ Gitlab::Database::QueryAnalyzers::Ci::PartitioningRoutingAnalyzer.with_suppressed(&example)
+ end
+end
diff --git a/spec/support/db_cleaner.rb b/spec/support/db_cleaner.rb
index a1579ad1685..0a1d68a744c 100644
--- a/spec/support/db_cleaner.rb
+++ b/spec/support/db_cleaner.rb
@@ -20,7 +20,7 @@ module DbCleaner
def setup_database_cleaner
all_connection_classes.each do |connection_class|
- DatabaseCleaner[:active_record, { connection: connection_class }]
+ DatabaseCleaner[:active_record, db: connection_class]
end
end
@@ -57,7 +57,7 @@ module DbCleaner
end
def recreate_all_databases!
- start = Gitlab::Metrics::System.monotonic_time
+ start = ::Gitlab::Metrics::System.monotonic_time
puts "Recreating the database"
@@ -81,7 +81,7 @@ module DbCleaner
Gitlab::Database::Partitioning.sync_partitions_ignore_db_error
stub_feature_flags(disallow_database_ddl_feature_flags: disable_ddl_was)
- puts "Databases re-creation done in #{Gitlab::Metrics::System.monotonic_time - start}"
+ puts "Databases re-creation done in #{::Gitlab::Metrics::System.monotonic_time - start}"
end
def recreate_databases_and_seed_if_needed
diff --git a/spec/support/finder_collection_allowlist.yml b/spec/support/finder_collection_allowlist.yml
index 0af4de11d51..e60cc4278af 100644
--- a/spec/support/finder_collection_allowlist.yml
+++ b/spec/support/finder_collection_allowlist.yml
@@ -70,4 +70,3 @@
- UploaderFinder
- UserGroupNotificationSettingsFinder
- UserGroupsCounter
-- DataTransfer::MockedTransferFinder # Can be removed when https://gitlab.com/gitlab-org/gitlab/-/issues/397693 is closed
diff --git a/spec/support/helpers/api_internal_base_helpers.rb b/spec/support/helpers/api_internal_base_helpers.rb
index 0c334e164a6..d3ae1a5c3b2 100644
--- a/spec/support/helpers/api_internal_base_helpers.rb
+++ b/spec/support/helpers/api_internal_base_helpers.rb
@@ -41,18 +41,19 @@ module APIInternalBaseHelpers
)
end
- def push(key, container, protocol = 'ssh', env: nil, changes: nil)
+ def push(key, container, protocol = 'ssh', env: nil, changes: nil, relative_path: nil)
push_with_path(
key,
full_path: full_path_for(container),
gl_repository: gl_repository_for(container),
protocol: protocol,
env: env,
- changes: changes
+ changes: changes,
+ relative_path: relative_path
)
end
- def push_with_path(key, full_path:, gl_repository: nil, protocol: 'ssh', env: nil, changes: nil)
+ def push_with_path(key, full_path:, gl_repository: nil, protocol: 'ssh', env: nil, changes: nil, relative_path: nil)
changes ||= 'd14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/master'
params = {
@@ -61,7 +62,8 @@ module APIInternalBaseHelpers
project: full_path,
action: 'git-receive-pack',
protocol: protocol,
- env: env
+ env: env,
+ relative_path: relative_path
}
params[:gl_repository] = gl_repository if gl_repository
diff --git a/spec/support/helpers/click_house_test_helpers.rb b/spec/support/helpers/click_house_test_helpers.rb
new file mode 100644
index 00000000000..24f81a3ec01
--- /dev/null
+++ b/spec/support/helpers/click_house_test_helpers.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+module ClickHouseTestHelpers
+ def migrate(target_version, migration_context)
+ quietly { migration_context.up(target_version) }
+ end
+
+ def rollback(target_version, migration_context)
+ quietly { migration_context.down(target_version) }
+ end
+
+ def table_names(database = :main, configuration = ClickHouse::Client.configuration)
+ ClickHouse::Client.select('SHOW TABLES', database, configuration).pluck('name')
+ end
+
+ def active_schema_migrations_count(database = :main, configuration = ClickHouse::Client.configuration)
+ query = <<~SQL
+ SELECT COUNT(*) AS count FROM schema_migrations FINAL WHERE active = 1
+ SQL
+
+ ClickHouse::Client.select(query, database, configuration).first['count']
+ end
+
+ def describe_table(table_name, database = :main, configuration = ClickHouse::Client.configuration)
+ ClickHouse::Client
+ .select("DESCRIBE TABLE #{table_name} FORMAT JSON", database, configuration)
+ .map(&:symbolize_keys)
+ .index_by { |h| h[:name].to_sym }
+ end
+
+ def schema_migrations(database = :main, configuration = ClickHouse::Client.configuration)
+ ClickHouse::Client
+ .select('SELECT * FROM schema_migrations FINAL ORDER BY version ASC', database, configuration)
+ .map(&:symbolize_keys)
+ end
+
+ def clear_db(configuration = ClickHouse::Client.configuration)
+ configuration.databases.each_key do |db|
+ # drop all tables
+ lookup_tables(db, configuration).each do |table|
+ ClickHouse::Client.execute("DROP TABLE IF EXISTS #{table}", db, configuration)
+ end
+
+ ClickHouse::MigrationSupport::SchemaMigration.create_table(db, configuration)
+ end
+ end
+
+ def register_database(config, database_identifier, db_config)
+ config.register_database(
+ database_identifier,
+ database: db_config[:database],
+ url: db_config[:url],
+ username: db_config[:username],
+ password: db_config[:password],
+ variables: db_config[:variables] || {}
+ )
+ end
+
+ private
+
+ def lookup_tables(db, configuration = ClickHouse::Client.configuration)
+ ClickHouse::Client.select('SHOW TABLES', db, configuration).pluck('name')
+ end
+
+ def quietly(&_block)
+ was_verbose = ClickHouse::Migration.verbose
+ ClickHouse::Migration.verbose = false
+
+ yield
+ ensure
+ ClickHouse::Migration.verbose = was_verbose
+ end
+
+ def clear_consts(fixtures_path)
+ $LOADED_FEATURES.select { |file| file.include? fixtures_path }.each do |file|
+ const = File.basename(file)
+ .scan(ClickHouse::Migration::MIGRATION_FILENAME_REGEXP)[0][1]
+ .camelcase
+ .safe_constantize
+
+ Object.send(:remove_const, const.to_s) if const
+ $LOADED_FEATURES.delete(file)
+ end
+ end
+end
diff --git a/spec/support/helpers/crypto_helpers.rb b/spec/support/helpers/crypto_helpers.rb
new file mode 100644
index 00000000000..0b2d5f6386a
--- /dev/null
+++ b/spec/support/helpers/crypto_helpers.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module CryptoHelpers
+ def sha256(value)
+ Gitlab::CryptoHelper.sha256(value)
+ end
+end
diff --git a/spec/support/helpers/cycle_analytics_helpers.rb b/spec/support/helpers/cycle_analytics_helpers.rb
index 5f60f8a6bfa..890fefcc7de 100644
--- a/spec/support/helpers/cycle_analytics_helpers.rb
+++ b/spec/support/helpers/cycle_analytics_helpers.rb
@@ -1,6 +1,10 @@
# frozen_string_literal: true
+require_relative './listbox_helpers'
+
module CycleAnalyticsHelpers
+ include ::ListboxHelpers
+
def toggle_value_stream_dropdown
page.find('[data-testid="dropdown-value-streams"]').click
end
@@ -16,8 +20,8 @@ module CycleAnalyticsHelpers
within last_stage do
find('[name*="custom-stage-name-"]').fill_in with: "Cool custom stage - name #{index}"
- select_dropdown_option_by_value "custom-stage-start-event-", :merge_request_created
- select_dropdown_option_by_value "custom-stage-end-event-", :merge_request_merged
+ select_dropdown_option_by_value "custom-stage-start-event-", 'Merge request created'
+ select_dropdown_option_by_value "custom-stage-end-event-", 'Merge request merged'
end
end
@@ -34,8 +38,8 @@ module CycleAnalyticsHelpers
within last_stage do
find('[name*="custom-stage-name-"]').fill_in with: "Cool custom label stage - name #{index}"
- select_dropdown_option_by_value "custom-stage-start-event-", :issue_label_added
- select_dropdown_option_by_value "custom-stage-end-event-", :issue_label_removed
+ select_dropdown_option_by_value "custom-stage-start-event-", 'Issue label was added'
+ select_dropdown_option_by_value "custom-stage-end-event-", 'Issue label was removed'
select_event_label("[data-testid*='custom-stage-start-event-label-']")
select_event_label("[data-testid*='custom-stage-end-event-label-']")
@@ -102,19 +106,14 @@ module CycleAnalyticsHelpers
select_value_stream(custom_value_stream_name)
end
- def toggle_dropdown(field)
- page.within("[data-testid*='#{field}']") do
- find('.dropdown-toggle').click
+ def select_dropdown_option_by_value(name, value)
+ page.within("[data-testid*='#{name}']") do
+ toggle_listbox
wait_for_requests
-
- expect(find('.dropdown-menu')).to have_selector('.dropdown-item')
end
- end
- def select_dropdown_option_by_value(name, value, elem = '.dropdown-item')
- toggle_dropdown name
- page.find("[data-testid*='#{name}'] .dropdown-menu").find("#{elem}[value='#{value}']").click
+ select_listbox_item(value)
end
def create_commit_referencing_issue(issue, branch_name: generate(:branch))
diff --git a/spec/support/helpers/cycle_analytics_helpers/test_generation.rb b/spec/support/helpers/cycle_analytics_helpers/test_generation.rb
deleted file mode 100644
index 1c7c45c06a1..00000000000
--- a/spec/support/helpers/cycle_analytics_helpers/test_generation.rb
+++ /dev/null
@@ -1,166 +0,0 @@
-# frozen_string_literal: true
-
-# rubocop:disable Layout/LineLength
-# rubocop:disable Metrics/CyclomaticComplexity
-# rubocop:disable Metrics/PerceivedComplexity
-# rubocop:disable Metrics/AbcSize
-
-# Note: The ABC size is large here because we have a method generating test cases with
-# multiple nested contexts. This shouldn't count as a violation.
-module CycleAnalyticsHelpers
- module TestGeneration
- # Generate the most common set of specs that all value stream analytics phases need to have.
- #
- # Arguments:
- #
- # phase: Which phase are we testing? Will call `CycleAnalytics.new.send(phase)` for the final assertion
- # data_fn: A function that returns a hash, constituting initial data for the test case
- # start_time_conditions: An array of `conditions`. Each condition is an tuple of `condition_name` and `condition_fn`. `condition_fn` is called with
- # `context` (no lexical scope, so need to do `context.create` for factories, for example) and `data` (from the `data_fn`).
- # Each `condition_fn` is expected to implement a case which consitutes the start of the given value stream analytics phase.
- # end_time_conditions: An array of `conditions`. Each condition is an tuple of `condition_name` and `condition_fn`. `condition_fn` is called with
- # `context` (no lexical scope, so need to do `context.create` for factories, for example) and `data` (from the `data_fn`).
- # Each `condition_fn` is expected to implement a case which consitutes the end of the given value stream analytics phase.
- # before_end_fn: This function is run before calling the end time conditions. Used for setup that needs to be run between the start and end conditions.
- # post_fn: Code that needs to be run after running the end time conditions.
-
- def generate_cycle_analytics_spec(phase:, data_fn:, start_time_conditions:, end_time_conditions:, before_end_fn: nil, post_fn: nil)
- combinations_of_start_time_conditions = (1..start_time_conditions.size).flat_map { |size| start_time_conditions.combination(size).to_a }
- combinations_of_end_time_conditions = (1..end_time_conditions.size).flat_map { |size| end_time_conditions.combination(size).to_a }
-
- scenarios = combinations_of_start_time_conditions.product(combinations_of_end_time_conditions)
- scenarios.each do |start_time_conditions, end_time_conditions|
- let_it_be(:other_project) { create(:project, :repository) }
-
- before do
- other_project.add_developer(user)
- end
-
- context "start condition: #{start_time_conditions.map(&:first).to_sentence}" do
- context "end condition: #{end_time_conditions.map(&:first).to_sentence}" do
- it "finds the median of available durations between the two conditions", :sidekiq_might_not_need_inline do
- time_differences = Array.new(5) do |index|
- data = data_fn[self]
- start_time = (index * 10).days.from_now
- end_time = start_time + rand(1..5).days
-
- start_time_conditions.each_value do |condition_fn|
- travel_to(start_time) { condition_fn[self, data] }
- end
-
- # Run `before_end_fn` at the midpoint between `start_time` and `end_time`
- travel_to(start_time + ((end_time - start_time) / 2)) { before_end_fn[self, data] } if before_end_fn
-
- end_time_conditions.each_value do |condition_fn|
- travel_to(end_time) { condition_fn[self, data] }
- end
-
- travel_to(end_time + 1.day) { post_fn[self, data] } if post_fn
-
- end_time - start_time
- end
-
- median_time_difference = time_differences.sort[2]
- expect(subject[phase].project_median).to be_within(5).of(median_time_difference)
- end
-
- context "when the data belongs to another project" do
- it "returns nil" do
- # Use a stub to "trick" the data/condition functions
- # into using another project. This saves us from having to
- # define separate data/condition functions for this particular
- # test case.
- allow(self).to receive(:project) { other_project }
-
- data = data_fn[self]
- start_time = Time.now
- end_time = rand(1..10).days.from_now
-
- start_time_conditions.each_value do |condition_fn|
- travel_to(start_time) { condition_fn[self, data] }
- end
-
- end_time_conditions.each_value do |condition_fn|
- travel_to(end_time) { condition_fn[self, data] }
- end
-
- travel_to(end_time + 1.day) { post_fn[self, data] } if post_fn
-
- # Turn off the stub before checking assertions
- allow(self).to receive(:project).and_call_original
-
- expect(subject[phase].project_median).to be_nil
- end
- end
-
- context "when the end condition happens before the start condition" do
- it 'returns nil' do
- data = data_fn[self]
- start_time = Time.now
- end_time = start_time + rand(1..5).days
-
- # Run `before_end_fn` at the midpoint between `start_time` and `end_time`
- travel_to(start_time + ((end_time - start_time) / 2)) { before_end_fn[self, data] } if before_end_fn
-
- end_time_conditions.each_value do |condition_fn|
- travel_to(start_time) { condition_fn[self, data] }
- end
-
- start_time_conditions.each_value do |condition_fn|
- travel_to(end_time) { condition_fn[self, data] }
- end
-
- travel_to(end_time + 1.day) { post_fn[self, data] } if post_fn
-
- expect(subject[phase].project_median).to be_nil
- end
- end
- end
-
- context "end condition NOT PRESENT: #{end_time_conditions.map(&:first).to_sentence}" do
- it "returns nil" do
- data = data_fn[self]
- start_time = Time.now
-
- start_time_conditions.each_value do |condition_fn|
- travel_to(start_time) { condition_fn[self, data] }
- end
-
- post_fn[self, data] if post_fn
-
- expect(subject[phase].project_median).to be_nil
- end
- end
- end
-
- context "start condition NOT PRESENT: #{start_time_conditions.map(&:first).to_sentence}" do
- context "end condition: #{end_time_conditions.map(&:first).to_sentence}" do
- it "returns nil" do
- data = data_fn[self]
- end_time = rand(1..10).days.from_now
-
- end_time_conditions.each_with_index do |(_condition_name, condition_fn), index|
- travel_to(end_time + index.days) { condition_fn[self, data] }
- end
-
- travel_to(end_time + 1.day) { post_fn[self, data] } if post_fn
-
- expect(subject[phase].project_median).to be_nil
- end
- end
- end
- end
-
- context "when none of the start / end conditions are matched" do
- it "returns nil" do
- expect(subject[phase].project_median).to be_nil
- end
- end
- end
- end
-end
-
-# rubocop:enable Layout/LineLength
-# rubocop:enable Metrics/CyclomaticComplexity
-# rubocop:enable Metrics/PerceivedComplexity
-# rubocop:enable Metrics/AbcSize
diff --git a/spec/support/helpers/database/duplicate_indexes.yml b/spec/support/helpers/database/duplicate_indexes.yml
index 02efdabd70b..1ebc45a9d81 100644
--- a/spec/support/helpers/database/duplicate_indexes.yml
+++ b/spec/support/helpers/database/duplicate_indexes.yml
@@ -2,264 +2,245 @@
# It maps table_name to {index1: array_of_duplicate_indexes, index2: array_of_duplicate_indexes, ... }
abuse_reports:
idx_abuse_reports_user_id_status_and_category:
- - index_abuse_reports_on_user_id
+ - index_abuse_reports_on_user_id
alert_management_http_integrations:
index_http_integrations_on_project_and_endpoint:
- - index_alert_management_http_integrations_on_project_id
-analytics_cycle_analytics_group_stages:
- index_group_stages_on_group_id_group_value_stream_id_and_name:
- - index_analytics_ca_group_stages_on_group_id
+ - index_alert_management_http_integrations_on_project_id
approval_project_rules_users:
index_approval_project_rules_users_1:
- - index_approval_project_rules_users_on_approval_project_rule_id
+ - index_approval_project_rules_users_on_approval_project_rule_id
approvals:
index_approvals_on_merge_request_id_and_created_at:
- - index_approvals_on_merge_request_id
+ - index_approvals_on_merge_request_id
board_group_recent_visits:
index_board_group_recent_visits_on_user_group_and_board:
- - index_board_group_recent_visits_on_user_id
+ - index_board_group_recent_visits_on_user_id
board_project_recent_visits:
index_board_project_recent_visits_on_user_project_and_board:
- - index_board_project_recent_visits_on_user_id
+ - index_board_project_recent_visits_on_user_id
board_user_preferences:
index_board_user_preferences_on_user_id_and_board_id:
- - index_board_user_preferences_on_user_id
+ - index_board_user_preferences_on_user_id
boards_epic_board_recent_visits:
index_epic_board_recent_visits_on_user_group_and_board:
- - index_boards_epic_board_recent_visits_on_user_id
+ - index_boards_epic_board_recent_visits_on_user_id
boards_epic_user_preferences:
index_boards_epic_user_preferences_on_board_user_epic_unique:
- - index_boards_epic_user_preferences_on_board_id
+ - index_boards_epic_user_preferences_on_board_id
bulk_import_batch_trackers:
i_bulk_import_trackers_id_batch_number:
- - index_bulk_import_batch_trackers_on_tracker_id
+ - index_bulk_import_batch_trackers_on_tracker_id
bulk_import_export_batches:
i_bulk_import_export_batches_id_batch_number:
- - index_bulk_import_export_batches_on_export_id
+ - index_bulk_import_export_batches_on_export_id
ci_job_artifacts:
index_ci_job_artifacts_on_id_project_id_and_created_at:
- - index_ci_job_artifacts_on_project_id
+ - index_ci_job_artifacts_on_project_id
index_ci_job_artifacts_on_id_project_id_and_file_type:
- - index_ci_job_artifacts_on_project_id
+ - index_ci_job_artifacts_on_project_id
index_ci_job_artifacts_on_project_id_and_id:
- - index_ci_job_artifacts_on_project_id
+ - index_ci_job_artifacts_on_project_id
ci_pipeline_artifacts:
index_ci_pipeline_artifacts_on_pipeline_id_and_file_type:
- - index_ci_pipeline_artifacts_on_pipeline_id
+ - index_ci_pipeline_artifacts_on_pipeline_id
ci_stages:
index_ci_stages_on_pipeline_id_and_name:
- - index_ci_stages_on_pipeline_id
+ - index_ci_stages_on_pipeline_id
index_ci_stages_on_pipeline_id_and_position:
- - index_ci_stages_on_pipeline_id
+ - index_ci_stages_on_pipeline_id
index_ci_stages_on_pipeline_id_convert_to_bigint_and_name:
- - index_ci_stages_on_pipeline_id_convert_to_bigint
+ - index_ci_stages_on_pipeline_id_convert_to_bigint
index_ci_stages_on_pipeline_id_convert_to_bigint_and_position:
- - index_ci_stages_on_pipeline_id_convert_to_bigint
+ - index_ci_stages_on_pipeline_id_convert_to_bigint
dast_site_tokens:
index_dast_site_token_on_project_id_and_url:
- - index_dast_site_tokens_on_project_id
+ - index_dast_site_tokens_on_project_id
design_management_designs:
index_design_management_designs_on_iid_and_project_id:
- - index_design_management_designs_on_project_id
+ - index_design_management_designs_on_project_id
design_management_designs_versions:
design_management_designs_versions_uniqueness:
- - index_design_management_designs_versions_on_design_id
+ - index_design_management_designs_versions_on_design_id
error_tracking_errors:
index_et_errors_on_project_id_and_status_and_id:
- - index_error_tracking_errors_on_project_id
+ - index_error_tracking_errors_on_project_id
index_et_errors_on_project_id_and_status_events_count_id_desc:
- - index_error_tracking_errors_on_project_id
+ - index_error_tracking_errors_on_project_id
index_et_errors_on_project_id_and_status_first_seen_at_id_desc:
- - index_error_tracking_errors_on_project_id
+ - index_error_tracking_errors_on_project_id
index_et_errors_on_project_id_and_status_last_seen_at_id_desc:
- - index_error_tracking_errors_on_project_id
+ - index_error_tracking_errors_on_project_id
geo_node_namespace_links:
index_geo_node_namespace_links_on_geo_node_id_and_namespace_id:
- - index_geo_node_namespace_links_on_geo_node_id
+ - index_geo_node_namespace_links_on_geo_node_id
in_product_marketing_emails:
- index_in_product_marketing_emails_on_user_campaign:
- - index_in_product_marketing_emails_on_user_id
index_in_product_marketing_emails_on_user_track_series:
- - index_in_product_marketing_emails_on_user_id
+ - index_in_product_marketing_emails_on_user_id
incident_management_oncall_participants:
index_inc_mgmnt_oncall_participants_on_user_id_and_rotation_id:
- - index_inc_mgmnt_oncall_participants_on_oncall_user_id
+ - index_inc_mgmnt_oncall_participants_on_oncall_user_id
incident_management_oncall_schedules:
index_im_oncall_schedules_on_project_id_and_iid:
- - index_incident_management_oncall_schedules_on_project_id
+ - index_incident_management_oncall_schedules_on_project_id
instance_audit_events_streaming_headers:
idx_instance_external_audit_event_destination_id_key_uniq:
- - idx_headers_instance_external_audit_event_destination_id
+ - idx_headers_instance_external_audit_event_destination_id
issue_links:
index_issue_links_on_source_id_and_target_id:
- - index_issue_links_on_source_id
+ - index_issue_links_on_source_id
issues:
index_issues_on_author_id_and_id_and_created_at:
- - index_issues_on_author_id
+ - index_issues_on_author_id
jira_connect_subscriptions:
idx_jira_connect_subscriptions_on_installation_id_namespace_id:
- - idx_jira_connect_subscriptions_on_installation_id
+ - idx_jira_connect_subscriptions_on_installation_id
list_user_preferences:
index_list_user_preferences_on_user_id_and_list_id:
- - index_list_user_preferences_on_user_id
+ - index_list_user_preferences_on_user_id
member_tasks:
index_member_tasks_on_member_id_and_project_id:
- - index_member_tasks_on_member_id
+ - index_member_tasks_on_member_id
members:
index_members_on_member_namespace_id_compound:
- - index_members_on_member_namespace_id
-merge_request_assignees:
- index_merge_request_assignees_on_merge_request_id_and_user_id:
- - index_merge_request_assignees_on_merge_request_id
-merge_request_metrics:
- index_mr_metrics_on_target_project_id_merged_at_nulls_last:
- - index_merge_request_metrics_on_target_project_id
+ - index_members_on_member_namespace_id
merge_requests:
index_merge_requests_on_author_id_and_created_at:
- - index_merge_requests_on_author_id
+ - index_merge_requests_on_author_id
index_merge_requests_on_author_id_and_id:
- - index_merge_requests_on_author_id
+ - index_merge_requests_on_author_id
index_merge_requests_on_author_id_and_target_project_id:
- - index_merge_requests_on_author_id
+ - index_merge_requests_on_author_id
ml_candidate_params:
index_ml_candidate_params_on_candidate_id_on_name:
- - index_ml_candidate_params_on_candidate_id
+ - index_ml_candidate_params_on_candidate_id
ml_candidates:
index_ml_candidates_on_project_id_on_internal_id:
- - index_ml_candidates_on_project_id
+ - index_ml_candidates_on_project_id
ml_model_versions:
index_ml_model_versions_on_project_id_and_model_id_and_version:
- - index_ml_model_versions_on_project_id
+ - index_ml_model_versions_on_project_id
ml_models:
index_ml_models_on_project_id_and_name:
- - index_ml_models_on_project_id
+ - index_ml_models_on_project_id
p_ci_runner_machine_builds:
index_p_ci_runner_machine_builds_on_runner_machine_id:
- - index_ci_runner_machine_builds_on_runner_machine_id
+ - index_ci_runner_machine_builds_on_runner_machine_id
packages_debian_group_distributions:
uniq_pkgs_debian_group_distributions_group_id_and_codename:
- - index_packages_debian_group_distributions_on_group_id
+ - index_packages_debian_group_distributions_on_group_id
uniq_pkgs_debian_group_distributions_group_id_and_suite:
- - index_packages_debian_group_distributions_on_group_id
+ - index_packages_debian_group_distributions_on_group_id
packages_debian_project_distributions:
uniq_pkgs_debian_project_distributions_project_id_and_codename:
- - index_packages_debian_project_distributions_on_project_id
+ - index_packages_debian_project_distributions_on_project_id
uniq_pkgs_debian_project_distributions_project_id_and_suite:
- - index_packages_debian_project_distributions_on_project_id
+ - index_packages_debian_project_distributions_on_project_id
packages_tags:
index_packages_tags_on_package_id_and_updated_at:
- - index_packages_tags_on_package_id
+ - index_packages_tags_on_package_id
pages_domains:
index_pages_domains_on_project_id_and_enabled_until:
- - index_pages_domains_on_project_id
+ - index_pages_domains_on_project_id
index_pages_domains_on_verified_at_and_enabled_until:
- - index_pages_domains_on_verified_at
+ - index_pages_domains_on_verified_at
personal_access_tokens:
index_pat_on_user_id_and_expires_at:
- - index_personal_access_tokens_on_user_id
+ - index_personal_access_tokens_on_user_id
pm_affected_packages:
i_affected_packages_unique_for_upsert:
- - index_pm_affected_packages_on_pm_advisory_id
+ - index_pm_affected_packages_on_pm_advisory_id
pm_package_version_licenses:
i_pm_package_version_licenses_join_ids:
- - index_pm_package_version_licenses_on_pm_package_version_id
+ - index_pm_package_version_licenses_on_pm_package_version_id
pm_package_versions:
i_pm_package_versions_on_package_id_and_version:
- - index_pm_package_versions_on_pm_package_id
+ - index_pm_package_versions_on_pm_package_id
project_compliance_standards_adherence:
u_project_compliance_standards_adherence_for_reporting:
- - index_project_compliance_standards_adherence_on_project_id
+ - index_project_compliance_standards_adherence_on_project_id
project_relation_exports:
index_project_export_job_relation:
- - index_project_relation_exports_on_project_export_job_id
+ - index_project_relation_exports_on_project_export_job_id
project_repositories:
index_project_repositories_on_shard_id_and_project_id:
- - index_project_repositories_on_shard_id
+ - index_project_repositories_on_shard_id
project_topics:
index_project_topics_on_project_id_and_topic_id:
- - index_project_topics_on_project_id
-projects:
- index_projects_api_path_id_desc:
- - index_on_projects_path
- index_projects_on_path_and_id:
- - index_on_projects_path
+ - index_project_topics_on_project_id
protected_environments:
index_protected_environments_on_project_id_and_name:
- - index_protected_environments_on_project_id
+ - index_protected_environments_on_project_id
protected_tags:
index_protected_tags_on_project_id_and_name:
- - index_protected_tags_on_project_id
+ - index_protected_tags_on_project_id
related_epic_links:
index_related_epic_links_on_source_id_and_target_id:
- - index_related_epic_links_on_source_id
+ - index_related_epic_links_on_source_id
requirements_management_test_reports:
idx_test_reports_on_issue_id_created_at_and_id:
- - index_requirements_management_test_reports_on_issue_id
+ - index_requirements_management_test_reports_on_issue_id
sbom_component_versions:
index_sbom_component_versions_on_component_id_and_version:
- - index_sbom_component_versions_on_component_id
+ - index_sbom_component_versions_on_component_id
sbom_occurrences:
index_sbom_occurrences_for_input_file_path_search:
- - index_sbom_occurrences_on_project_id_component_id
- - index_sbom_occurrences_on_project_id
+ - index_sbom_occurrences_on_project_id_component_id
+ - index_sbom_occurrences_on_project_id
idx_sbom_occurrences_on_project_id_and_source_id:
- - index_sbom_occurrences_on_project_id
+ - index_sbom_occurrences_on_project_id
index_sbom_occurrences_on_project_id_and_id:
- - index_sbom_occurrences_on_project_id
+ - index_sbom_occurrences_on_project_id
index_sbom_occurrences_on_project_id_component_id:
- - index_sbom_occurrences_on_project_id
+ - index_sbom_occurrences_on_project_id
index_sbom_occurrences_on_project_id_and_component_id_and_id:
- - index_sbom_occurrences_on_project_id_component_id
- - index_sbom_occurrences_on_project_id
+ - index_sbom_occurrences_on_project_id_component_id
+ - index_sbom_occurrences_on_project_id
index_sbom_occurrences_on_project_id_and_package_manager:
- - index_sbom_occurrences_on_project_id
-scan_result_policies:
- index_scan_result_policies_on_position_in_configuration:
- - index_scan_result_policies_on_policy_configuration_id
+ - index_sbom_occurrences_on_project_id
search_namespace_index_assignments:
index_search_namespace_index_assignments_uniqueness_index_type:
- - index_search_namespace_index_assignments_on_namespace_id
+ - index_search_namespace_index_assignments_on_namespace_id
index_search_namespace_index_assignments_uniqueness_on_index_id:
- - index_search_namespace_index_assignments_on_namespace_id
+ - index_search_namespace_index_assignments_on_namespace_id
sprints:
sequence_is_unique_per_iterations_cadence_id:
- - index_sprints_iterations_cadence_id
+ - index_sprints_iterations_cadence_id
taggings:
taggings_idx:
- - index_taggings_on_tag_id
+ - index_taggings_on_tag_id
term_agreements:
term_agreements_unique_index:
- - index_term_agreements_on_user_id
+ - index_term_agreements_on_user_id
todos:
index_todos_on_author_id_and_created_at:
- - index_todos_on_author_id
+ - index_todos_on_author_id
user_callouts:
index_user_callouts_on_user_id_and_feature_name:
- - index_user_callouts_on_user_id
+ - index_user_callouts_on_user_id
users:
index_users_on_state_and_user_type:
- - index_users_on_state
+ - index_users_on_state
vulnerabilities:
index_vulnerabilities_project_id_state_severity_default_branch:
- - index_vulnerabilities_on_project_id_and_state_and_severity
+ - index_vulnerabilities_on_project_id_and_state_and_severity
vulnerability_external_issue_links:
idx_vulnerability_ext_issue_links_on_vulne_id_and_ext_issue:
- - index_vulnerability_external_issue_links_on_vulnerability_id
+ - index_vulnerability_external_issue_links_on_vulnerability_id
vulnerability_finding_links:
finding_link_name_url_idx:
- - finding_links_on_vulnerability_occurrence_id
+ - finding_links_on_vulnerability_occurrence_id
vulnerability_finding_signatures:
idx_vuln_signatures_uniqueness_signature_sha:
- - index_vulnerability_finding_signatures_on_finding_id
+ - index_vulnerability_finding_signatures_on_finding_id
vulnerability_flags:
index_vulnerability_flags_on_unique_columns:
- - index_vulnerability_flags_on_vulnerability_occurrence_id
+ - index_vulnerability_flags_on_vulnerability_occurrence_id
web_hook_logs:
index_web_hook_logs_on_web_hook_id_and_created_at:
- - index_web_hook_logs_part_on_web_hook_id
+ - index_web_hook_logs_part_on_web_hook_id
web_hooks:
index_web_hooks_on_project_id_recent_failures:
- - index_web_hooks_on_project_id
+ - index_web_hooks_on_project_id
work_item_hierarchy_restrictions:
index_work_item_hierarchy_restrictions_on_parent_and_child:
- - index_work_item_hierarchy_restrictions_on_parent_type_id
+ - index_work_item_hierarchy_restrictions_on_parent_type_id
diff --git a/spec/support/helpers/email_helpers.rb b/spec/support/helpers/email_helpers.rb
index 57386233775..9dffe035b2a 100644
--- a/spec/support/helpers/email_helpers.rb
+++ b/spec/support/helpers/email_helpers.rb
@@ -94,7 +94,8 @@ module EmailHelpers
port: credential.smtp_port,
user_name: credential.smtp_username,
password: credential.smtp_password,
- domain: service_desk_setting.custom_email.split('@').last
+ domain: service_desk_setting.custom_email.split('@').last,
+ authentication: credential.smtp_authentication
)
end
end
diff --git a/spec/support/helpers/features/dom_helpers.rb b/spec/support/helpers/features/dom_helpers.rb
index cbbb80dde36..619f16f5e6d 100644
--- a/spec/support/helpers/features/dom_helpers.rb
+++ b/spec/support/helpers/features/dom_helpers.rb
@@ -2,6 +2,10 @@
module Features
module DomHelpers
+ def has_testid?(testid, **kwargs)
+ page.has_selector?("[data-testid='#{testid}']", **kwargs)
+ end
+
def find_by_testid(testid, **kwargs)
page.find("[data-testid='#{testid}']", **kwargs)
end
diff --git a/spec/support/helpers/features/releases_helpers.rb b/spec/support/helpers/features/releases_helpers.rb
index d5846aad15d..c3fbd128a28 100644
--- a/spec/support/helpers/features/releases_helpers.rb
+++ b/spec/support/helpers/features/releases_helpers.rb
@@ -44,20 +44,22 @@ module Features
end
def fill_release_title(release_title)
- fill_in('Release title', with: release_title)
+ fill_in('release-title', with: release_title)
end
- def select_milestone(milestone_title)
- page.within '[data-testid="milestones-field"]' do
- find('button').click
+ def select_milestones(*milestone_titles)
+ within_testid 'milestones-field' do
+ find_by_testid('base-dropdown-toggle').click
wait_for_all_requests
- find('input[aria-label="Search Milestones"]').set(milestone_title)
+ milestone_titles.each do |milestone_title|
+ find('input[type="search"]').set(milestone_title)
- wait_for_all_requests
+ wait_for_all_requests
- find('button', text: milestone_title, match: :first).click
+ find('[role="option"]', text: milestone_title).click
+ end
end
end
diff --git a/spec/support/helpers/gitaly_setup.rb b/spec/support/helpers/gitaly_setup.rb
index 06390406efc..f263b3b44ce 100644
--- a/spec/support/helpers/gitaly_setup.rb
+++ b/spec/support/helpers/gitaly_setup.rb
@@ -72,7 +72,9 @@ module GitalySetup
end
def repos_path(storage = REPOS_STORAGE)
- Gitlab.config.repositories.storages[REPOS_STORAGE].legacy_disk_path
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ Gitlab.config.repositories.storages[REPOS_STORAGE].legacy_disk_path
+ end
end
def service_cmd(service, toml = nil)
diff --git a/spec/support/helpers/gpg_helpers.rb b/spec/support/helpers/gpg_helpers.rb
index cc8ee6c98e6..1f93b1bb698 100644
--- a/spec/support/helpers/gpg_helpers.rb
+++ b/spec/support/helpers/gpg_helpers.rb
@@ -700,7 +700,7 @@ module GpgHelpers
end
def subkey_fingerprints
- %w(65A33805A5DDA7454190EE536F0E46B850B18E99 3AD06974F78DD1603D5E4617D0955D22F2C324E2)
+ %w[65A33805A5DDA7454190EE536F0E46B850B18E99 3AD06974F78DD1603D5E4617D0955D22F2C324E2]
end
def names
diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb
index 5eba982e3da..085340d6cb9 100644
--- a/spec/support/helpers/graphql_helpers.rb
+++ b/spec/support/helpers/graphql_helpers.rb
@@ -2,6 +2,7 @@
module GraphqlHelpers
def self.included(base)
+ base.include(::ApiHelpers)
base.include(::Gitlab::Graphql::Laziness)
end
diff --git a/spec/support/helpers/listbox_helpers.rb b/spec/support/helpers/listbox_helpers.rb
index 7a734d2b097..a8a4c079e3c 100644
--- a/spec/support/helpers/listbox_helpers.rb
+++ b/spec/support/helpers/listbox_helpers.rb
@@ -14,6 +14,10 @@ module ListboxHelpers
find('.gl-new-dropdown-item', text: text, exact_text: exact_text).click
end
+ def toggle_listbox
+ find('.gl-new-dropdown-toggle').click
+ end
+
def expect_listbox_item(text)
expect(page).to have_css('.gl-new-dropdown-item[role="option"]', text: text)
end
diff --git a/spec/support/helpers/login_helpers.rb b/spec/support/helpers/login_helpers.rb
index abe21d2b74c..d35fa801638 100644
--- a/spec/support/helpers/login_helpers.rb
+++ b/spec/support/helpers/login_helpers.rb
@@ -70,7 +70,13 @@ module LoginHelpers
# Requires Javascript driver.
def gitlab_sign_out
- find(".header-user-dropdown-toggle").click
+ if has_testid?('super-sidebar')
+ click_on "#{@current_user.name} user’s menu"
+ else
+ # This can be removed once https://gitlab.com/gitlab-org/gitlab/-/issues/420121 is complete.
+ find(".header-user-dropdown-toggle").click
+ end
+
click_link "Sign out"
@current_user = nil
@@ -79,11 +85,8 @@ module LoginHelpers
# Requires Javascript driver.
def gitlab_disable_admin_mode
- open_top_nav
-
- within_top_nav do
- click_on 'Leave admin mode'
- end
+ click_on 'Search or go to…'
+ click_on 'Leave admin mode'
end
private
@@ -209,9 +212,9 @@ module LoginHelpers
def mock_saml_config_with_upstream_two_factor_authn_contexts
config = mock_saml_config
- config.args[:upstream_two_factor_authn_contexts] = %w(urn:oasis:names:tc:SAML:2.0:ac:classes:CertificateProtectedTransport
+ config.args[:upstream_two_factor_authn_contexts] = %w[urn:oasis:names:tc:SAML:2.0:ac:classes:CertificateProtectedTransport
urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorOTPSMS
- urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorIGTOKEN)
+ urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorIGTOKEN]
config
end
@@ -259,19 +262,15 @@ module LoginHelpers
end
def stub_omniauth_config(messages)
- allow(Gitlab.config.omniauth).to receive_messages(messages)
+ allow(Gitlab.config.omniauth).to receive_messages(GitlabSettings::Options.build(messages))
end
def stub_basic_saml_config
- allow_next_instance_of(Gitlab::Auth::Saml::Config) do |config|
- allow(config).to receive_messages({ options: { name: 'saml', args: {} } })
- end
+ stub_omniauth_config(providers: [{ name: 'saml', args: {} }])
end
def stub_saml_group_config(groups)
- allow_next_instance_of(Gitlab::Auth::Saml::Config) do |config|
- allow(config).to receive_messages({ options: { name: 'saml', groups_attribute: 'groups', external_groups: groups, args: {} } })
- end
+ stub_omniauth_config(providers: [{ name: 'saml', groups_attribute: 'groups', external_groups: groups, args: {} }])
end
end
diff --git a/spec/support/helpers/navbar_structure_helper.rb b/spec/support/helpers/navbar_structure_helper.rb
index fe39968b002..131c7597827 100644
--- a/spec/support/helpers/navbar_structure_helper.rb
+++ b/spec/support/helpers/navbar_structure_helper.rb
@@ -8,6 +8,13 @@ module NavbarStructureHelper
structure.insert(index + 1, new_nav_item)
end
+ def insert_before_nav_item(after_nav_item_name, new_nav_item:)
+ expect(structure).to include(a_hash_including(nav_item: after_nav_item_name))
+
+ index = structure.find_index { |h| h[:nav_item] == after_nav_item_name if h }
+ structure.insert(index, new_nav_item)
+ end
+
def insert_after_sub_nav_item(before_sub_nav_item_name, within:, new_sub_nav_item_name:)
expect(structure).to include(a_hash_including(nav_item: within))
hash = structure.find { |h| h[:nav_item] == within if h }
@@ -30,49 +37,57 @@ module NavbarStructureHelper
hash[:nav_sub_items].insert(index, new_sub_nav_item_name)
end
- def insert_package_nav(within)
- insert_after_nav_item(
- within,
- new_nav_item: {
- nav_item: _('Packages and registries'),
- nav_sub_items: [_('Package Registry')]
- }
+ def insert_package_nav
+ insert_after_sub_nav_item(
+ _("Feature flags"),
+ within: _('Deploy'),
+ new_sub_nav_item_name: _("Package Registry")
)
end
- def insert_customer_relations_nav(within)
- insert_after_nav_item(
- within,
+ def create_package_nav(before)
+ insert_before_nav_item(
+ before,
new_nav_item: {
- nav_item: _('Customer relations'),
- nav_sub_items: [
- _('Contacts'),
- _('Organizations')
- ]
+ nav_item: _("Deploy"),
+ nav_sub_items: [_("Package Registry")]
}
)
end
+ def insert_customer_relations_nav(after)
+ insert_after_sub_nav_item(
+ after,
+ within: _('Plan'),
+ new_sub_nav_item_name: _("Customer contacts")
+ )
+ insert_after_sub_nav_item(
+ _("Customer contacts"),
+ within: _('Plan'),
+ new_sub_nav_item_name: _("Customer organizations")
+ )
+ end
+
def insert_container_nav
insert_after_sub_nav_item(
_('Package Registry'),
- within: _('Packages and registries'),
+ within: _('Deploy'),
new_sub_nav_item_name: _('Container Registry')
)
end
def insert_dependency_proxy_nav
- insert_after_sub_nav_item(
- _('Package Registry'),
- within: _('Packages and registries'),
+ insert_before_sub_nav_item(
+ _('Kubernetes'),
+ within: _('Operate'),
new_sub_nav_item_name: _('Dependency Proxy')
)
end
def insert_infrastructure_registry_nav
insert_after_sub_nav_item(
- _('Package Registry'),
- within: _('Packages and registries'),
+ s_('Terraform|Terraform states'),
+ within: _('Operate'),
new_sub_nav_item_name: _('Terraform modules')
)
end
@@ -80,15 +95,15 @@ module NavbarStructureHelper
def insert_harbor_registry_nav(within)
insert_after_sub_nav_item(
within,
- within: _('Packages and registries'),
+ within: _('Operate'),
new_sub_nav_item_name: _('Harbor Registry')
)
end
def insert_infrastructure_google_cloud_nav
insert_after_sub_nav_item(
- s_('Terraform|Terraform states'),
- within: _('Infrastructure'),
+ s_('Terraform|Terraform modules'),
+ within: _('Operate'),
new_sub_nav_item_name: _('Google Cloud')
)
end
@@ -96,7 +111,7 @@ module NavbarStructureHelper
def insert_infrastructure_aws_nav
insert_after_sub_nav_item(
_('Google Cloud'),
- within: _('Infrastructure'),
+ within: _('Operate'),
new_sub_nav_item_name: _('AWS')
)
end
@@ -104,25 +119,24 @@ module NavbarStructureHelper
def insert_model_experiments_nav(within)
insert_after_sub_nav_item(
within,
- within: _('Packages and registries'),
+ within: _('Analyze'),
new_sub_nav_item_name: _('Model experiments')
)
end
def project_analytics_sub_nav_item
[
- _('Value stream'),
- _('CI/CD'),
- (_('Code review') if Gitlab.ee?),
- (_('Merge request') if Gitlab.ee?),
- _('Repository')
+ _('Value stream analytics'),
+ _('Contributor statistics'),
+ _('CI/CD analytics'),
+ _('Repository analytics'),
+ (_('Code review analytics') if Gitlab.ee?),
+ (_('Merge request analytics') if Gitlab.ee?)
]
end
def group_analytics_sub_nav_item
- [
- _('Contribution')
- ]
+ [_("Contribution analytics")]
end
end
diff --git a/spec/support/helpers/packages_manager_api_spec_helper.rb b/spec/support/helpers/packages_manager_api_spec_helper.rb
deleted file mode 100644
index 1c9fce183e9..00000000000
--- a/spec/support/helpers/packages_manager_api_spec_helper.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-# frozen_string_literal: true
-
-module PackagesManagerApiSpecHelpers
- def build_jwt(personal_access_token, secret: jwt_secret, user_id: nil)
- JSONWebToken::HMACToken.new(secret).tap do |jwt|
- jwt['access_token'] = personal_access_token.token
- jwt['user_id'] = user_id || personal_access_token.user_id
- end
- end
-
- def build_jwt_from_job(job, secret: jwt_secret)
- JSONWebToken::HMACToken.new(secret).tap do |jwt|
- jwt['access_token'] = job.token
- jwt['user_id'] = job.user.id
- end
- end
-
- def build_jwt_from_deploy_token(deploy_token, secret: jwt_secret)
- JSONWebToken::HMACToken.new(secret).tap do |jwt|
- jwt['access_token'] = deploy_token.token
- jwt['user_id'] = deploy_token.username
- end
- end
-
- def temp_file(package_tmp)
- upload_path = ::Packages::PackageFileUploader.workhorse_local_upload_path
- file_path = "#{upload_path}/#{package_tmp}"
-
- FileUtils.mkdir_p(upload_path)
- File.write(file_path, 'test')
-
- UploadedFile.new(file_path, filename: File.basename(file_path))
- end
-end
diff --git a/spec/support/helpers/packages_manager_api_spec_helpers.rb b/spec/support/helpers/packages_manager_api_spec_helpers.rb
new file mode 100644
index 00000000000..c81c9b5982e
--- /dev/null
+++ b/spec/support/helpers/packages_manager_api_spec_helpers.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module PackagesManagerApiSpecHelpers
+ def build_jwt(personal_access_token, secret: jwt_secret, user_id: nil)
+ JSONWebToken::HMACToken.new(secret).tap do |jwt|
+ jwt['access_token'] = personal_access_token.token
+ jwt['user_id'] = user_id || personal_access_token.user_id
+ end
+ end
+
+ def build_jwt_from_job(job, secret: jwt_secret)
+ JSONWebToken::HMACToken.new(secret).tap do |jwt|
+ jwt['access_token'] = job.token
+ jwt['user_id'] = job.user.id
+ end
+ end
+
+ def build_jwt_from_deploy_token(deploy_token, secret: jwt_secret)
+ JSONWebToken::HMACToken.new(secret).tap do |jwt|
+ jwt['access_token'] = deploy_token.token
+ jwt['user_id'] = deploy_token.username
+ end
+ end
+
+ def temp_file(package_tmp, content: nil)
+ upload_path = ::Packages::PackageFileUploader.workhorse_local_upload_path
+ file_path = "#{upload_path}/#{package_tmp}"
+
+ FileUtils.mkdir_p(upload_path)
+ content ? FileUtils.cp(content, file_path) : File.write(file_path, 'test')
+
+ UploadedFile.new(file_path, filename: File.basename(file_path))
+ end
+end
diff --git a/spec/support/helpers/prevent_set_operator_mismatch_helper.rb b/spec/support/helpers/prevent_set_operator_mismatch_helper.rb
new file mode 100644
index 00000000000..482a5560fe9
--- /dev/null
+++ b/spec/support/helpers/prevent_set_operator_mismatch_helper.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module PreventSetOperatorMismatchHelper
+ extend ActiveSupport::Concern
+
+ included do
+ before do
+ stub_const('Type', Gitlab::Database::QueryAnalyzers::PreventSetOperatorMismatch::Type)
+ end
+ end
+
+ def sql_select_node(sql)
+ parsed = PgQuery.parse(sql)
+ parsed.tree.stmts[0].stmt.select_stmt
+ end
+end
diff --git a/spec/support/helpers/project_template_test_helper.rb b/spec/support/helpers/project_template_test_helper.rb
index 35e40faeea7..a02cd491bca 100644
--- a/spec/support/helpers/project_template_test_helper.rb
+++ b/spec/support/helpers/project_template_test_helper.rb
@@ -10,6 +10,7 @@ module ProjectTemplateTestHelper
serverless_framework tencent_serverless_framework
jsonnet cluster_management kotlin_native_linux
pelican bridgetown typo3_distribution laravel
+ astro_tailwind
]
end
end
diff --git a/spec/support/helpers/prometheus_helpers.rb b/spec/support/helpers/prometheus_helpers.rb
index da80f6f08c2..065c653c62f 100644
--- a/spec/support/helpers/prometheus_helpers.rb
+++ b/spec/support/helpers/prometheus_helpers.rb
@@ -207,7 +207,7 @@ module PrometheusHelpers
def prometheus_label_values
{
'status': 'success',
- 'data': %w(job_adds job_controller_rate_limiter_use job_depth job_queue_latency job_work_duration_sum up)
+ 'data': %w[job_adds job_controller_rate_limiter_use job_depth job_queue_latency job_work_duration_sum up]
}
end
diff --git a/spec/support/helpers/repo_helpers.rb b/spec/support/helpers/repo_helpers.rb
index d264356aa64..bac88da4885 100644
--- a/spec/support/helpers/repo_helpers.rb
+++ b/spec/support/helpers/repo_helpers.rb
@@ -112,13 +112,13 @@ eos
}
] + extra_changes
- commits = %w(
+ commits = %w[
5937ac0a7beb003549fc5fd26fc247adbce4a52e
570e7b2abdd848b95f2f578043fc23bd6f6fd24d
6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
d14d6c0abdd253381df51a723d58691b2ee1ab08
c1acaa58bbcbc3eafe538cb8274ba387047b69f8
- ).reverse # last commit is recent one
+ ].reverse # last commit is recent one
reviewers = [
{
diff --git a/spec/support/helpers/search_helpers.rb b/spec/support/helpers/search_helpers.rb
index d13703776cd..dd5ce63876e 100644
--- a/spec/support/helpers/search_helpers.rb
+++ b/spec/support/helpers/search_helpers.rb
@@ -2,16 +2,28 @@
module SearchHelpers
def fill_in_search(text)
- page.within('.header-search') do
- find('#search').click
- fill_in 'search', with: text
+ within_testid('super-sidebar') do
+ click_button "Search or go to…"
end
+ fill_in 'search', with: text
wait_for_all_requests
end
def submit_search(query)
- page.within('.header-search-form, .search-page-form') do
+ # Forms directly on the search page
+ if page.has_css?('.search-page-form')
+ search_form = '.search-page-form'
+ # Open search modal from super sidebar
+ elsif has_testid?('super-sidebar-search-button')
+ find_by_testid('super-sidebar-search-button').click
+ search_form = '#super-sidebar-search-modal'
+ # Open legacy search dropdown in navigation
+ else
+ search_form = '.header-search-form'
+ end
+
+ page.within(search_form) do
field = find_field('search')
field.click
field.fill_in(with: query)
@@ -27,7 +39,7 @@ module SearchHelpers
end
def select_search_scope(scope)
- page.within '[data-testid="search-filter"]' do
+ within_testid('search-filter') do
click_link scope
wait_for_all_requests
@@ -35,9 +47,9 @@ module SearchHelpers
end
def has_search_scope?(scope)
- return false unless page.has_selector?('[data-testid="search-filter"]')
+ return false unless has_testid?('search-filter')
- page.within '[data-testid="search-filter"]' do
+ within_testid('search-filter') do
has_link?(scope)
end
end
diff --git a/spec/support/helpers/seed_repo.rb b/spec/support/helpers/seed_repo.rb
index 74ac529a3de..b0bd0dfb60e 100644
--- a/spec/support/helpers/seed_repo.rb
+++ b/spec/support/helpers/seed_repo.rb
@@ -47,7 +47,7 @@ module SeedRepo
FILES_COUNT = 2
C_FILE_PATH = "files/ruby"
C_FILES = ["popen.rb", "regex.rb", "version_info.rb"].freeze
- BLOB_FILE = %{%h3= @key.title\n%hr\n%pre= @key.key\n.actions\n = link_to 'Remove', @key, :confirm => 'Are you sure?', :method => :delete, :class => \"btn danger delete-key\"\n\n\n}
+ BLOB_FILE = %(%h3= @key.title\n%hr\n%pre= @key.key\n.actions\n = link_to 'Remove', @key, :confirm => 'Are you sure?', :method => :delete, :class => \"btn danger delete-key\"\n\n\n)
BLOB_FILE_PATH = "app/views/keys/show.html.haml"
end
diff --git a/spec/support/helpers/stub_saas_features.rb b/spec/support/helpers/stub_saas_features.rb
index e344888cb8c..d0aa7108a6a 100644
--- a/spec/support/helpers/stub_saas_features.rb
+++ b/spec/support/helpers/stub_saas_features.rb
@@ -6,9 +6,9 @@ module StubSaasFeatures
# @param [Hash] features where key is feature name and value is boolean whether enabled or not.
#
# Examples
- # - `stub_saas_features('onboarding' => false)` ... Disable `onboarding`
+ # - `stub_saas_features(onboarding: false)` ... Disable `onboarding`
# SaaS feature globally.
- # - `stub_saas_features('onboarding' => true)` ... Enable `onboarding`
+ # - `stub_saas_features(onboarding: true)` ... Enable `onboarding`
# SaaS feature globally.
def stub_saas_features(features)
features.each do |feature_name, value|
diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb
index 740abdb6cfa..e606a377ec7 100644
--- a/spec/support/helpers/test_env.rb
+++ b/spec/support/helpers/test_env.rb
@@ -305,7 +305,7 @@ module TestEnv
unless File.directory?(repo_path)
start = Time.now
- system(*%W(#{Gitlab.config.git.bin_path} clone --quiet -- #{clone_url} #{repo_path}))
+ system(*%W[#{Gitlab.config.git.bin_path} clone --quiet -- #{clone_url} #{repo_path}])
puts "==> #{repo_path} set up in #{Time.now - start} seconds...\n"
end
@@ -316,7 +316,7 @@ module TestEnv
# set all the required local branches. This would happen when a new
# branch is added to BRANCH_SHA, in which case we want to update
# everything.
- unless system(*%W(#{Gitlab.config.git.bin_path} -C #{repo_path} fetch origin))
+ unless system(*%W[#{Gitlab.config.git.bin_path} -C #{repo_path} fetch origin])
raise 'Could not fetch test seed repository.'
end
@@ -329,7 +329,7 @@ module TestEnv
if create_bundle
start = Time.now
- system(git_env, *%W(#{Gitlab.config.git.bin_path} -C #{repo_path} bundle create #{repo_bundle_path} --exclude refs/remotes/* --all))
+ system(git_env, *%W[#{Gitlab.config.git.bin_path} -C #{repo_path} bundle create #{repo_bundle_path} --exclude refs/remotes/* --all])
puts "==> #{repo_bundle_path} generated in #{Time.now - start} seconds...\n"
end
end
@@ -530,7 +530,7 @@ module TestEnv
return false unless Dir.exist?(component_folder)
- sha, exit_status = Gitlab::Popen.popen(%W(#{Gitlab.config.git.bin_path} rev-parse HEAD), component_folder)
+ sha, exit_status = Gitlab::Popen.popen(%W[#{Gitlab.config.git.bin_path} rev-parse HEAD], component_folder)
return false if exit_status != 0
expected_version == sha.chomp
diff --git a/spec/support/helpers/usage_data_helpers.rb b/spec/support/helpers/usage_data_helpers.rb
index 42e599c7510..3b8c0b42fe8 100644
--- a/spec/support/helpers/usage_data_helpers.rb
+++ b/spec/support/helpers/usage_data_helpers.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module UsageDataHelpers
- COUNTS_KEYS = %i(
+ COUNTS_KEYS = %i[
assignee_lists
ci_builds
ci_external_pipelines
@@ -72,9 +72,9 @@ module UsageDataHelpers
uploads
web_hooks
user_preferences_user_gitpod_enabled
- ).freeze
+ ].freeze
- USAGE_DATA_KEYS = %i(
+ USAGE_DATA_KEYS = %i[
counts
recorded_at
mattermost_enabled
@@ -93,7 +93,7 @@ module UsageDataHelpers
prometheus_metrics_enabled
object_store
topology
- ).freeze
+ ].freeze
def stub_usage_data_connections
Gitlab::Database.database_base_models_with_gitlab_shared.each_value do |base_model|
diff --git a/spec/support/import_export/configuration_helper.rb b/spec/support/import_export/configuration_helper.rb
index 28797229661..f6917cdd89e 100644
--- a/spec/support/import_export/configuration_helper.rb
+++ b/spec/support/import_export/configuration_helper.rb
@@ -32,7 +32,7 @@ module ConfigurationHelper
# - project is not part of the tree, so it has to be added manually.
# - milestone, labels, merge_request have both singular and plural versions in the tree, so remove the duplicates.
# - User, Author... Models we do not care about for checking models
- names.flatten.uniq - %w(milestones labels user author merge_request design) + [key.to_s]
+ names.flatten.uniq - %w[milestones labels user author merge_request design] + [key.to_s]
end
def relation_class_for_name(relation_name)
diff --git a/spec/support/import_export/export_file_helper.rb b/spec/support/import_export/export_file_helper.rb
index 3be2d39906d..8068d3082d5 100644
--- a/spec/support/import_export/export_file_helper.rb
+++ b/spec/support/import_export/export_file_helper.rb
@@ -59,7 +59,7 @@ module ExportFileHelper
def in_directory_with_expanded_export(project)
Dir.mktmpdir do |tmpdir|
export_file = project.export_file.path
- _output, exit_status = Gitlab::Popen.popen(%W{tar -zxf #{export_file} -C #{tmpdir}})
+ _output, exit_status = Gitlab::Popen.popen(%W[tar -zxf #{export_file} -C #{tmpdir}])
yield(exit_status, tmpdir)
end
diff --git a/spec/support/matchers/markdown_matchers.rb b/spec/support/matchers/markdown_matchers.rb
index 7a82d7674d9..21fc1824bea 100644
--- a/spec/support/matchers/markdown_matchers.rb
+++ b/spec/support/matchers/markdown_matchers.rb
@@ -76,7 +76,7 @@ module MarkdownMatchers
expect(actual).to have_autolink('irc://irc.freenode.net/git')
expect(actual).to have_autolink('http://localhost:3000')
- %w(code a kbd).each do |elem|
+ %w[code a kbd].each do |elem|
expect(body).not_to have_selector("#{elem} a")
end
end
diff --git a/spec/support/matchers/navigation_matcher.rb b/spec/support/matchers/navigation_matcher.rb
index a0beecbfb2c..400c2fe7436 100644
--- a/spec/support/matchers/navigation_matcher.rb
+++ b/spec/support/matchers/navigation_matcher.rb
@@ -1,14 +1,20 @@
# frozen_string_literal: true
+# These matches look for selectors within the Vue navigation sidebar.
+# They should therefore be used in feature specs with the Js driver enabled.
+
RSpec::Matchers.define :have_active_navigation do |expected|
match do |page|
- expect(page).to have_selector('.sidebar-top-level-items > li.active', count: 1)
- expect(page.find('.sidebar-top-level-items > li.active')).to have_content(expected)
+ within_testid('super-sidebar') do
+ expect(page).to have_selector('button[aria-expanded="true"]', text: expected)
+ end
end
end
RSpec::Matchers.define :have_active_sub_navigation do |expected|
match do |page|
- expect(page).to have_css('.sidebar-sub-level-items > li.active:not(.fly-out-top-item)', text: expected)
+ within_testid('super-sidebar') do
+ expect(page).to have_selector('[aria-current="page"]', text: expected)
+ end
end
end
diff --git a/spec/support/redis.rb b/spec/support/redis.rb
index d5ae0bf1582..9cf5c44de98 100644
--- a/spec/support/redis.rb
+++ b/spec/support/redis.rb
@@ -3,9 +3,8 @@ require 'gitlab/redis'
RSpec.configure do |config|
config.after(:each, :redis) do
- Sidekiq.redis do |connection|
- connection.redis.flushdb
- end
+ Sidekiq.redis(&:flushdb)
+ redis_queues_metadata_cleanup!
end
Gitlab::Redis::ALL_CLASSES.each do |instance_class|
@@ -13,10 +12,12 @@ RSpec.configure do |config|
config.around(:each, :"clean_gitlab_redis_#{underscored_name}") do |example|
public_send("redis_#{underscored_name}_cleanup!")
+ redis_queues_metadata_cleanup! if underscored_name == 'queues'
example.run
public_send("redis_#{underscored_name}_cleanup!")
+ redis_queues_metadata_cleanup! if underscored_name == 'queues'
end
end
diff --git a/spec/support/rspec_order_todo.yml b/spec/support/rspec_order_todo.yml
index 51f3ff2c077..da23f81e86e 100644
--- a/spec/support/rspec_order_todo.yml
+++ b/spec/support/rspec_order_todo.yml
@@ -81,7 +81,6 @@
- './ee/spec/controllers/groups/iteration_cadences_controller_spec.rb'
- './ee/spec/controllers/groups/iterations_controller_spec.rb'
- './ee/spec/controllers/groups/ldaps_controller_spec.rb'
-- './ee/spec/controllers/groups/ldap_settings_controller_spec.rb'
- './ee/spec/controllers/groups/merge_requests_controller_spec.rb'
- './ee/spec/controllers/groups/omniauth_callbacks_controller_spec.rb'
- './ee/spec/controllers/groups/push_rules_controller_spec.rb'
@@ -132,7 +131,6 @@
- './ee/spec/controllers/projects/merge_requests_controller_spec.rb'
- './ee/spec/controllers/projects/merge_requests/creations_controller_spec.rb'
- './ee/spec/controllers/projects/mirrors_controller_spec.rb'
-- './ee/spec/controllers/projects/pages_controller_spec.rb'
- './ee/spec/controllers/projects/path_locks_controller_spec.rb'
- './ee/spec/controllers/projects/pipelines_controller_spec.rb'
- './ee/spec/controllers/projects/protected_environments_controller_spec.rb'
@@ -165,7 +163,6 @@
- './ee/spec/controllers/users_controller_spec.rb'
- './ee/spec/db/production/license_spec.rb'
- './ee/spec/elastic_integration/global_search_spec.rb'
-- './ee/spec/elastic_integration/repository_index_spec.rb'
- './ee/spec/elastic/migrate/20201105181100_apply_max_analyzed_offset_spec.rb'
- './ee/spec/elastic/migrate/20201116142400_add_new_data_to_issues_documents_spec.rb'
- './ee/spec/elastic/migrate/20201123123400_migrate_issues_to_separate_index_spec.rb'
@@ -203,7 +200,6 @@
- './ee/spec/features/admin/admin_show_new_user_signups_cap_alert_spec.rb'
- './ee/spec/features/admin/admin_users_spec.rb'
- './ee/spec/features/admin/geo/admin_geo_nodes_spec.rb'
-- './ee/spec/features/admin/geo/admin_geo_projects_spec.rb'
- './ee/spec/features/admin/geo/admin_geo_replication_nav_spec.rb'
- './ee/spec/features/admin/geo/admin_geo_sidebar_spec.rb'
- './ee/spec/features/admin/groups/admin_changes_plan_spec.rb'
@@ -292,7 +288,6 @@
- './ee/spec/features/groups/iterations/user_views_iteration_cadence_spec.rb'
- './ee/spec/features/groups/iterations/user_views_iteration_spec.rb'
- './ee/spec/features/groups/ldap_group_links_spec.rb'
-- './ee/spec/features/groups/ldap_settings_spec.rb'
- './ee/spec/features/groups/members/leave_group_spec.rb'
- './ee/spec/features/groups/members/list_members_spec.rb'
- './ee/spec/features/groups/members/manage_groups_spec.rb'
@@ -337,7 +332,6 @@
- './ee/spec/features/issues/user_views_issues_spec.rb'
- './ee/spec/features/labels_hierarchy_spec.rb'
- './ee/spec/features/markdown/markdown_spec.rb'
-- './ee/spec/features/markdown/metrics_spec.rb'
- './ee/spec/features/merge_request/merge_request_widget_blocking_mrs_spec.rb'
- './ee/spec/features/merge_request/sidebar_spec.rb'
- './ee/spec/features/merge_requests/user_filters_by_approvers_spec.rb'
@@ -393,7 +387,6 @@
- './ee/spec/features/projects/insights_spec.rb'
- './ee/spec/features/projects/integrations/jira_issues_list_spec.rb'
- './ee/spec/features/projects/integrations/project_integrations_spec.rb'
-- './ee/spec/features/projects/integrations/prometheus_custom_metrics_spec.rb'
- './ee/spec/features/projects/integrations/user_activates_github_spec.rb'
- './ee/spec/features/projects/integrations/user_activates_jira_spec.rb'
- './ee/spec/features/projects/issues/user_creates_issue_spec.rb'
@@ -476,7 +469,6 @@
- './ee/spec/features/security/project/snippet/public_access_spec.rb'
- './ee/spec/features/signup_spec.rb'
- './ee/spec/features/subscriptions/expiring_subscription_message_spec.rb'
-- './ee/spec/features/subscriptions/groups/edit_spec.rb'
- './ee/spec/features/subscriptions_spec.rb'
- './ee/spec/features/trial_registrations/company_information_spec.rb'
- './ee/spec/features/trial_registrations/signin_spec.rb'
@@ -509,7 +501,6 @@
- './ee/spec/finders/dast_site_profiles_finder_spec.rb'
- './ee/spec/finders/dast_site_validations_finder_spec.rb'
- './ee/spec/finders/ee/alert_management/http_integrations_finder_spec.rb'
-- './ee/spec/finders/ee/autocomplete/users_finder_spec.rb'
- './ee/spec/finders/ee/ci/daily_build_group_report_results_finder_spec.rb'
- './ee/spec/finders/ee/clusters/agents_finder_spec.rb'
- './ee/spec/finders/ee/fork_targets_finder_spec.rb'
@@ -527,9 +518,6 @@
- './ee/spec/finders/geo/package_file_registry_finder_spec.rb'
- './ee/spec/finders/geo/pages_deployment_registry_finder_spec.rb'
- './ee/spec/finders/geo/pipeline_artifact_registry_finder_spec.rb'
-- './ee/spec/finders/geo/project_registry_finder_spec.rb'
-- './ee/spec/finders/geo/project_registry_status_finder_spec.rb'
-- './ee/spec/finders/geo/repository_verification_finder_spec.rb'
- './ee/spec/finders/geo/snippet_repository_registry_finder_spec.rb'
- './ee/spec/finders/geo/terraform_state_version_registry_finder_spec.rb'
- './ee/spec/finders/geo/upload_registry_finder_spec.rb'
@@ -552,15 +540,10 @@
- './ee/spec/finders/security/findings_finder_spec.rb'
- './ee/spec/finders/security/pipeline_vulnerabilities_finder_spec.rb'
- './ee/spec/finders/security/scan_execution_policies_finder_spec.rb'
-- './ee/spec/finders/security/training_providers/base_url_finder_spec.rb'
-- './ee/spec/finders/security/training_providers/kontra_url_finder_spec.rb'
-- './ee/spec/finders/security/training_providers/secure_code_warrior_url_finder_spec.rb'
-- './ee/spec/finders/security/training_urls_finder_spec.rb'
- './ee/spec/finders/security/vulnerabilities_finder_spec.rb'
- './ee/spec/finders/security/vulnerability_feedbacks_finder_spec.rb'
- './ee/spec/finders/security/vulnerability_reads_finder_spec.rb'
- './ee/spec/finders/snippets_finder_spec.rb'
-- './ee/spec/finders/software_license_policies_finder_spec.rb'
- './ee/spec/finders/template_finder_spec.rb'
- './ee/spec/finders/users_finder_spec.rb'
- './ee/spec/frontend/fixtures/analytics/charts.rb'
@@ -606,11 +589,9 @@
- './ee/spec/graphql/ee/types/mutation_type_spec.rb'
- './ee/spec/graphql/ee/types/namespace_type_spec.rb'
- './ee/spec/graphql/ee/types/notes/noteable_interface_spec.rb'
-- './ee/spec/graphql/ee/types/projects/service_type_enum_spec.rb'
- './ee/spec/graphql/ee/types/repository/blob_type_spec.rb'
- './ee/spec/graphql/ee/types/todoable_interface_spec.rb'
- './ee/spec/graphql/ee/types/user_merge_request_interaction_type_spec.rb'
-- './ee/spec/graphql/mutations/app_sec/fuzzing/api/ci_configuration/create_spec.rb'
- './ee/spec/graphql/mutations/app_sec/fuzzing/coverage/corpus/create_spec.rb'
- './ee/spec/graphql/mutations/audit_events/streaming/headers/create_spec.rb'
- './ee/spec/graphql/mutations/audit_events/streaming/headers/destroy_spec.rb'
@@ -671,7 +652,6 @@
- './ee/spec/graphql/mutations/security_policy/commit_scan_execution_policy_spec.rb'
- './ee/spec/graphql/mutations/security_policy/create_security_policy_project_spec.rb'
- './ee/spec/graphql/mutations/security_policy/unassign_security_policy_project_spec.rb'
-- './ee/spec/graphql/mutations/security/training_provider_update_spec.rb'
- './ee/spec/graphql/mutations/todos/create_spec.rb'
- './ee/spec/graphql/mutations/vulnerabilities/confirm_spec.rb'
- './ee/spec/graphql/mutations/vulnerabilities/create_external_issue_link_spec.rb'
@@ -727,7 +707,6 @@
- './ee/spec/graphql/resolvers/security_orchestration/scan_execution_policy_resolver_spec.rb'
- './ee/spec/graphql/resolvers/security_orchestration/scan_result_policy_resolver_spec.rb'
- './ee/spec/graphql/resolvers/security_report_summary_resolver_spec.rb'
-- './ee/spec/graphql/resolvers/security_training_urls_resolver_spec.rb'
- './ee/spec/graphql/resolvers/timebox_report_resolver_spec.rb'
- './ee/spec/graphql/resolvers/user_discussions_count_resolver_spec.rb'
- './ee/spec/graphql/resolvers/user_notes_count_resolver_spec.rb'
@@ -961,7 +940,6 @@
- './ee/spec/helpers/ee/system_note_helper_spec.rb'
- './ee/spec/helpers/ee/todos_helper_spec.rb'
- './ee/spec/helpers/ee/users/callouts_helper_spec.rb'
-- './ee/spec/helpers/ee/version_check_helper_spec.rb'
- './ee/spec/helpers/ee/wiki_helper_spec.rb'
- './ee/spec/helpers/epics_helper_spec.rb'
- './ee/spec/helpers/gitlab_subscriptions/upcoming_reconciliation_helper_spec.rb'
@@ -1019,7 +997,6 @@
- './ee/spec/lib/audit/details_spec.rb'
- './ee/spec/lib/audit/external_status_check_changes_auditor_spec.rb'
- './ee/spec/lib/audit/group_merge_request_approval_setting_changes_auditor_spec.rb'
-- './ee/spec/lib/audit/group_push_rules_changes_auditor_spec.rb'
- './ee/spec/lib/banzai/filter/cross_project_issuable_information_filter_spec.rb'
- './ee/spec/lib/banzai/filter/issuable_reference_expansion_filter_spec.rb'
- './ee/spec/lib/banzai/filter/jira_private_image_link_filter_spec.rb'
@@ -1092,14 +1069,11 @@
- './ee/spec/lib/ee/gitlab/auth/saml/identity_linker_spec.rb'
- './ee/spec/lib/ee/gitlab/background_migration/backfill_iteration_cadence_id_for_boards_spec.rb'
- './ee/spec/lib/ee/gitlab/background_migration/backfill_project_statistics_container_repository_size_spec.rb'
-- './ee/spec/lib/ee/gitlab/background_migration/create_security_setting_spec.rb'
- './ee/spec/lib/ee/gitlab/background_migration/delete_invalid_epic_issues_spec.rb'
- './ee/spec/lib/ee/gitlab/background_migration/migrate_approver_to_approval_rules_check_progress_spec.rb'
- './ee/spec/lib/ee/gitlab/background_migration/migrate_approver_to_approval_rules_in_batch_spec.rb'
- './ee/spec/lib/ee/gitlab/background_migration/migrate_approver_to_approval_rules_spec.rb'
- './ee/spec/lib/ee/gitlab/background_migration/migrate_shared_vulnerability_scanners_spec.rb'
-- './ee/spec/lib/ee/gitlab/background_migration/populate_latest_pipeline_ids_spec.rb'
-- './ee/spec/lib/ee/gitlab/background_migration/populate_resolved_on_default_branch_column_spec.rb'
- './ee/spec/lib/ee/gitlab/background_migration/purge_stale_security_scans_spec.rb'
- './ee/spec/lib/ee/gitlab/checks/push_rule_check_spec.rb'
- './ee/spec/lib/ee/gitlab/checks/push_rules/branch_check_spec.rb'
@@ -1118,14 +1092,11 @@
- './ee/spec/lib/ee/gitlab/ci/pipeline/chain/validate/after_config_spec.rb'
- './ee/spec/lib/ee/gitlab/ci/pipeline/chain/validate/external_spec.rb'
- './ee/spec/lib/ee/gitlab/ci/pipeline/chain/validate/security_orchestration_policy_spec.rb'
-- './ee/spec/lib/ee/gitlab/ci/pipeline/quota/activity_spec.rb'
- './ee/spec/lib/ee/gitlab/ci/pipeline/quota/size_spec.rb'
-- './ee/spec/lib/ee/gitlab/ci/reports/security/reports_spec.rb'
- './ee/spec/lib/ee/gitlab/ci/status/build/manual_spec.rb'
- './ee/spec/lib/ee/gitlab/ci/templates/templates_spec.rb'
- './ee/spec/lib/ee/gitlab/cleanup/orphan_job_artifact_files_batch_spec.rb'
- './ee/spec/lib/ee/gitlab/cleanup/orphan_job_artifact_files_spec.rb'
-- './ee/spec/lib/ee/gitlab/database/gitlab_schema_spec.rb'
- './ee/spec/lib/ee/gitlab/database_spec.rb'
- './ee/spec/lib/ee/gitlab/elastic/helper_spec.rb'
- './ee/spec/lib/ee/gitlab/email/handler/service_desk_handler_spec.rb'
@@ -1150,11 +1121,9 @@
- './ee/spec/lib/ee/gitlab/issuable_metadata_spec.rb'
- './ee/spec/lib/ee/gitlab/metrics/samplers/database_sampler_spec.rb'
- './ee/spec/lib/ee/gitlab/middleware/read_only_spec.rb'
-- './ee/spec/lib/ee/gitlab/namespaces/storage/enforcement_spec.rb'
- './ee/spec/lib/ee/gitlab/namespace_storage_size_error_message_spec.rb'
- './ee/spec/lib/ee/gitlab/omniauth_initializer_spec.rb'
- './ee/spec/lib/ee/gitlab/pages/deployment_update_spec.rb'
-- './ee/spec/lib/ee/gitlab/prometheus/metric_group_spec.rb'
- './ee/spec/lib/ee/gitlab/rack_attack/request_spec.rb'
- './ee/spec/lib/ee/gitlab/repo_path_spec.rb'
- './ee/spec/lib/ee/gitlab/repository_size_checker_spec.rb'
@@ -1169,7 +1138,6 @@
- './ee/spec/lib/ee/gitlab/template/gitlab_ci_yml_template_spec.rb'
- './ee/spec/lib/ee/gitlab/tracking_spec.rb'
- './ee/spec/lib/ee/gitlab/url_builder_spec.rb'
-- './ee/spec/lib/ee/gitlab/usage_data_counters/hll_redis_counter_spec.rb'
- './ee/spec/lib/ee/gitlab/usage_data_counters/issue_activity_unique_counter_spec.rb'
- './ee/spec/lib/ee/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb'
- './ee/spec/lib/ee/gitlab/usage_data_counters/work_item_activity_unique_counter_spec.rb'
@@ -1300,7 +1268,6 @@
- './ee/spec/lib/gitlab/ci/parsers/security/validators/default_branch_image_validator_spec.rb'
- './ee/spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb'
- './ee/spec/lib/gitlab/ci/pipeline/chain/create_cross_database_associations_spec.rb'
-- './ee/spec/lib/gitlab/ci/pipeline/chain/limit/activity_spec.rb'
- './ee/spec/lib/gitlab/ci/pipeline/chain/limit/size_spec.rb'
- './ee/spec/lib/gitlab/ci/reports/coverage_fuzzing/report_spec.rb'
- './ee/spec/lib/gitlab/ci/reports/dependency_list/dependency_spec.rb'
@@ -1387,13 +1354,6 @@
- './ee/spec/lib/gitlab/geo/log_cursor/events/cache_invalidation_event_spec.rb'
- './ee/spec/lib/gitlab/geo/log_cursor/events/event_spec.rb'
- './ee/spec/lib/gitlab/geo/log_cursor/events/hashed_storage_attachments_event_spec.rb'
-- './ee/spec/lib/gitlab/geo/log_cursor/events/hashed_storage_migrated_event_spec.rb'
-- './ee/spec/lib/gitlab/geo/log_cursor/events/repositories_changed_event_spec.rb'
-- './ee/spec/lib/gitlab/geo/log_cursor/events/repository_created_event_spec.rb'
-- './ee/spec/lib/gitlab/geo/log_cursor/events/repository_deleted_event_spec.rb'
-- './ee/spec/lib/gitlab/geo/log_cursor/events/repository_renamed_event_spec.rb'
-- './ee/spec/lib/gitlab/geo/log_cursor/events/repository_updated_event_spec.rb'
-- './ee/spec/lib/gitlab/geo/log_cursor/events/reset_checksum_event_spec.rb'
- './ee/spec/lib/gitlab/geo/log_cursor/lease_spec.rb'
- './ee/spec/lib/gitlab/geo/log_cursor/logger_spec.rb'
- './ee/spec/lib/gitlab/geo/logger_spec.rb'
@@ -1461,12 +1421,8 @@
- './ee/spec/lib/gitlab/pagination/keyset/simple_order_builder_spec.rb'
- './ee/spec/lib/gitlab/patch/database_config_spec.rb'
- './ee/spec/lib/gitlab/patch/draw_route_spec.rb'
-- './ee/spec/lib/gitlab/patch/geo_database_tasks_spec.rb'
- './ee/spec/lib/gitlab/path_locks_finder_spec.rb'
- './ee/spec/lib/gitlab/project_template_spec.rb'
-- './ee/spec/lib/gitlab/prometheus/queries/additional_metrics_deployment_query_spec.rb'
-- './ee/spec/lib/gitlab/prometheus/queries/additional_metrics_environment_query_spec.rb'
-- './ee/spec/lib/gitlab/prometheus/queries/cluster_query_spec.rb'
- './ee/spec/lib/gitlab/proxy_spec.rb'
- './ee/spec/lib/gitlab/quick_actions/users_extractor_spec.rb'
- './ee/spec/lib/gitlab/rack_attack_spec.rb'
@@ -1523,7 +1479,6 @@
- './ee/spec/lib/gitlab/visibility_level_spec.rb'
- './ee/spec/lib/gitlab/vulnerabilities/base_vulnerability_spec.rb'
- './ee/spec/lib/gitlab/vulnerabilities/container_scanning_vulnerability_spec.rb'
-- './ee/spec/lib/gitlab/vulnerabilities/findings_preloader_spec.rb'
- './ee/spec/lib/gitlab/vulnerabilities/parser_spec.rb'
- './ee/spec/lib/gitlab/vulnerabilities/standard_vulnerability_spec.rb'
- './ee/spec/lib/gitlab/web_ide/config/entry/schema/match_spec.rb'
@@ -1552,7 +1507,6 @@
- './ee/spec/mailers/devise_mailer_spec.rb'
- './ee/spec/mailers/ee/emails/admin_notification_spec.rb'
- './ee/spec/mailers/ee/emails/issues_spec.rb'
-- './ee/spec/mailers/ee/emails/merge_requests_spec.rb'
- './ee/spec/mailers/ee/emails/profile_spec.rb'
- './ee/spec/mailers/ee/emails/projects_spec.rb'
- './ee/spec/mailers/emails/epics_spec.rb'
@@ -1576,7 +1530,6 @@
- './ee/spec/models/allowed_email_domain_spec.rb'
- './ee/spec/models/analytics/cycle_analytics/aggregation_context_spec.rb'
- './ee/spec/models/analytics/cycle_analytics/group_level_spec.rb'
-- './ee/spec/models/analytics/cycle_analytics/runtime_limiter_spec.rb'
- './ee/spec/models/analytics/devops_adoption/enabled_namespace_spec.rb'
- './ee/spec/models/analytics/devops_adoption/snapshot_spec.rb'
- './ee/spec/models/analytics/issues_analytics_spec.rb'
@@ -1634,7 +1587,6 @@
- './ee/spec/models/concerns/ee/project_security_scanners_information_spec.rb'
- './ee/spec/models/concerns/ee/weight_eventable_spec.rb'
- './ee/spec/models/concerns/elastic/application_versioned_search_spec.rb'
-- './ee/spec/models/concerns/elastic/issue_spec.rb'
- './ee/spec/models/concerns/elastic/merge_request_spec.rb'
- './ee/spec/models/concerns/elastic/milestone_spec.rb'
- './ee/spec/models/concerns/elastic/note_spec.rb'
@@ -1707,7 +1659,6 @@
- './ee/spec/models/ee/namespaces/namespace_ban_spec.rb'
- './ee/spec/models/ee/namespace_spec.rb'
- './ee/spec/models/ee/namespace_statistics_spec.rb'
-- './ee/spec/models/ee/namespace/storage/notification_spec.rb'
- './ee/spec/models/ee/notification_setting_spec.rb'
- './ee/spec/models/ee/pages_deployment_spec.rb'
- './ee/spec/models/ee/personal_access_token_spec.rb'
@@ -1718,7 +1669,6 @@
- './ee/spec/models/ee/project_setting_spec.rb'
- './ee/spec/models/ee/project_wiki_spec.rb'
- './ee/spec/models/ee/protected_branch_spec.rb'
-- './ee/spec/models/ee/protected_ref_access_spec.rb'
- './ee/spec/models/ee/protected_ref_spec.rb'
- './ee/spec/models/ee/release_spec.rb'
- './ee/spec/models/ee/resource_label_event_spec.rb'
@@ -1761,7 +1711,6 @@
- './ee/spec/models/geo/package_file_registry_spec.rb'
- './ee/spec/models/geo/pages_deployment_registry_spec.rb'
- './ee/spec/models/geo/pipeline_artifact_registry_spec.rb'
-- './ee/spec/models/geo/project_registry_spec.rb'
- './ee/spec/models/geo/push_user_spec.rb'
- './ee/spec/models/geo/repositories_changed_event_spec.rb'
- './ee/spec/models/geo/repository_created_event_spec.rb'
@@ -1794,7 +1743,6 @@
- './ee/spec/models/integrations/github_spec.rb'
- './ee/spec/models/integrations/github/status_message_spec.rb'
- './ee/spec/models/integrations/github/status_notifier_spec.rb'
-- './ee/spec/models/integrations/gitlab_slack_application_spec.rb'
- './ee/spec/models/ip_restriction_spec.rb'
- './ee/spec/models/issuable_metric_image_spec.rb'
- './ee/spec/models/issuables_analytics_spec.rb'
@@ -1833,7 +1781,6 @@
- './ee/spec/models/project_member_spec.rb'
- './ee/spec/models/project_repository_state_spec.rb'
- './ee/spec/models/project_security_setting_spec.rb'
-- './ee/spec/models/project_team_spec.rb'
- './ee/spec/models/protected_branch/required_code_owners_section_spec.rb'
- './ee/spec/models/protected_branch/unprotect_access_level_spec.rb'
- './ee/spec/models/protected_environments/approval_rule_spec.rb'
@@ -1863,7 +1810,6 @@
- './ee/spec/models/security/scan_spec.rb'
- './ee/spec/models/security/training_provider_spec.rb'
- './ee/spec/models/security/training_spec.rb'
-- './ee/spec/models/slack_integration_spec.rb'
- './ee/spec/models/snippet_repository_spec.rb'
- './ee/spec/models/snippet_spec.rb'
- './ee/spec/models/software_license_policy_spec.rb'
@@ -1927,7 +1873,6 @@
- './ee/spec/policies/global_policy_spec.rb'
- './ee/spec/policies/group_hook_policy_spec.rb'
- './ee/spec/policies/group_policy_spec.rb'
-- './ee/spec/policies/identity_provider_policy_spec.rb'
- './ee/spec/policies/instance_security_dashboard_policy_spec.rb'
- './ee/spec/policies/issuable_policy_spec.rb'
- './ee/spec/policies/issue_policy_spec.rb'
@@ -2002,7 +1947,6 @@
- './ee/spec/requests/api/award_emoji_spec.rb'
- './ee/spec/requests/api/boards_spec.rb'
- './ee/spec/requests/api/branches_spec.rb'
-- './ee/spec/requests/api/captcha_check_spec.rb'
- './ee/spec/requests/api/ci/jobs_spec.rb'
- './ee/spec/requests/api/ci/minutes_spec.rb'
- './ee/spec/requests/api/ci/pipelines_spec.rb'
@@ -2024,7 +1968,6 @@
- './ee/spec/requests/api/features_spec.rb'
- './ee/spec/requests/api/files_spec.rb'
- './ee/spec/requests/api/geo_nodes_spec.rb'
-- './ee/spec/requests/api/geo_replication_spec.rb'
- './ee/spec/requests/api/geo_spec.rb'
- './ee/spec/requests/api/graphql/analytics/devops_adoption/enabled_namespaces_spec.rb'
- './ee/spec/requests/api/graphql/app_sec/fuzzing/api/ci_configuration_type_spec.rb'
@@ -2071,7 +2014,6 @@
- './ee/spec/requests/api/graphql/mutations/analytics/devops_adoption/enabled_namespaces/bulk_enable_spec.rb'
- './ee/spec/requests/api/graphql/mutations/analytics/devops_adoption/enabled_namespaces/disable_spec.rb'
- './ee/spec/requests/api/graphql/mutations/analytics/devops_adoption/enabled_namespaces/enable_spec.rb'
-- './ee/spec/requests/api/graphql/mutations/app_sec/fuzzing/api/ci_configuration/create_spec.rb'
- './ee/spec/requests/api/graphql/mutations/audit_events/external_audit_event_destinations/create_spec.rb'
- './ee/spec/requests/api/graphql/mutations/audit_events/external_audit_event_destinations/destroy_spec.rb'
- './ee/spec/requests/api/graphql/mutations/audit_events/external_audit_event_destinations/update_spec.rb'
@@ -2142,7 +2084,6 @@
- './ee/spec/requests/api/graphql/mutations/users/abuse/namespace_bans/destroy_spec.rb'
- './ee/spec/requests/api/graphql/mutations/vulnerabilities/create_external_issue_link_spec.rb'
- './ee/spec/requests/api/graphql/mutations/vulnerabilities/destroy_external_issue_link_spec.rb'
-- './ee/spec/requests/api/graphql/mutations/vulnerabilities/finding_dismiss_spec.rb'
- './ee/spec/requests/api/graphql/mutations/work_items/update_spec.rb'
- './ee/spec/requests/api/graphql/namespace/compliance_frameworks_spec.rb'
- './ee/spec/requests/api/graphql/namespace/projects_spec.rb'
@@ -2174,7 +2115,6 @@
- './ee/spec/requests/api/graphql/project/security_orchestration/scan_result_policy_spec.rb'
- './ee/spec/requests/api/graphql/project/vulnerability_severities_count_spec.rb'
- './ee/spec/requests/api/graphql/project/work_items_spec.rb'
-- './ee/spec/requests/api/graphql/vulnerabilities/description_spec.rb'
- './ee/spec/requests/api/graphql/vulnerabilities/details_spec.rb'
- './ee/spec/requests/api/graphql/vulnerabilities/external_issue_links_spec.rb'
- './ee/spec/requests/api/graphql/vulnerabilities/identifiers_spec.rb'
@@ -2241,7 +2181,6 @@
- './ee/spec/requests/api/todos_spec.rb'
- './ee/spec/requests/api/usage_data_spec.rb'
- './ee/spec/requests/api/users_spec.rb'
-- './ee/spec/requests/api/v3/github_spec.rb'
- './ee/spec/requests/api/visual_review_discussions_spec.rb'
- './ee/spec/requests/api/vulnerabilities_spec.rb'
- './ee/spec/requests/api/vulnerability_exports_spec.rb'
@@ -2260,7 +2199,6 @@
- './ee/spec/requests/groups/analytics/devops_adoption_controller_spec.rb'
- './ee/spec/requests/groups/audit_events_spec.rb'
- './ee/spec/requests/groups/clusters_controller_spec.rb'
-- './ee/spec/requests/groups/compliance_frameworks_spec.rb'
- './ee/spec/requests/groups/contribution_analytics_spec.rb'
- './ee/spec/requests/groups_controller_spec.rb'
- './ee/spec/requests/groups/epics/epic_links_controller_spec.rb'
@@ -2358,7 +2296,6 @@
- './ee/spec/serializers/integrations/jira_serializers/issue_entity_spec.rb'
- './ee/spec/serializers/integrations/jira_serializers/issue_serializer_spec.rb'
- './ee/spec/serializers/integrations/zentao_serializers/issue_entity_spec.rb'
-- './ee/spec/serializers/issuable_sidebar_extras_entity_spec.rb'
- './ee/spec/serializers/issue_serializer_spec.rb'
- './ee/spec/serializers/issues/linked_issue_feature_flag_entity_spec.rb'
- './ee/spec/serializers/license_compliance/collapsed_comparer_entity_spec.rb'
@@ -2370,7 +2307,6 @@
- './ee/spec/serializers/member_entity_spec.rb'
- './ee/spec/serializers/member_user_entity_spec.rb'
- './ee/spec/serializers/merge_request_poll_widget_entity_spec.rb'
-- './ee/spec/serializers/merge_request_sidebar_basic_entity_spec.rb'
- './ee/spec/serializers/merge_request_widget_entity_spec.rb'
- './ee/spec/serializers/metrics_report_metric_entity_spec.rb'
- './ee/spec/serializers/metrics_reports_comparer_entity_spec.rb'
@@ -2454,10 +2390,8 @@
- './ee/spec/services/app_sec/fuzzing/coverage/corpuses/create_service_spec.rb'
- './ee/spec/services/arkose/blocked_users_report_service_spec.rb'
- './ee/spec/services/audit_events/build_service_spec.rb'
-- './ee/spec/services/audit_events/custom_audit_event_service_spec.rb'
- './ee/spec/services/audit_event_service_spec.rb'
- './ee/spec/services/audit_events/export_csv_service_spec.rb'
-- './ee/spec/services/audit_events/impersonation_audit_event_service_spec.rb'
- './ee/spec/services/audit_events/protected_branch_audit_event_service_spec.rb'
- './ee/spec/services/audit_events/register_runner_audit_event_service_spec.rb'
- './ee/spec/services/audit_events/release_artifacts_downloaded_audit_event_service_spec.rb'
@@ -2700,41 +2634,20 @@
- './ee/spec/services/geo/cache_invalidation_event_store_spec.rb'
- './ee/spec/services/geo/container_repository_sync_service_spec.rb'
- './ee/spec/services/geo/container_repository_sync_spec.rb'
-- './ee/spec/services/geo/design_repository_sync_service_spec.rb'
- './ee/spec/services/geo/event_service_spec.rb'
- './ee/spec/services/geo/file_registry_removal_service_spec.rb'
-- './ee/spec/services/geo/files_expire_service_spec.rb'
- './ee/spec/services/geo/framework_repository_sync_service_spec.rb'
- './ee/spec/services/geo/graphql_request_service_spec.rb'
- './ee/spec/services/geo/hashed_storage_attachments_event_store_spec.rb'
- './ee/spec/services/geo/hashed_storage_attachments_migration_service_spec.rb'
-- './ee/spec/services/geo/hashed_storage_migrated_event_store_spec.rb'
-- './ee/spec/services/geo/hashed_storage_migration_service_spec.rb'
- './ee/spec/services/geo/metrics_update_service_spec.rb'
-- './ee/spec/services/geo/move_repository_service_spec.rb'
- './ee/spec/services/geo/node_create_service_spec.rb'
- './ee/spec/services/geo/node_status_request_service_spec.rb'
- './ee/spec/services/geo/node_update_service_spec.rb'
-- './ee/spec/services/geo/project_housekeeping_service_spec.rb'
- './ee/spec/services/geo/prune_event_log_service_spec.rb'
- './ee/spec/services/geo/registry_consistency_service_spec.rb'
-- './ee/spec/services/geo/rename_repository_service_spec.rb'
- './ee/spec/services/geo/replication_toggle_request_service_spec.rb'
-- './ee/spec/services/geo/repositories_changed_event_store_spec.rb'
-- './ee/spec/services/geo/repository_base_sync_service_spec.rb'
-- './ee/spec/services/geo/repository_created_event_store_spec.rb'
-- './ee/spec/services/geo/repository_deleted_event_store_spec.rb'
-- './ee/spec/services/geo/repository_destroy_service_spec.rb'
- './ee/spec/services/geo/repository_registry_removal_service_spec.rb'
-- './ee/spec/services/geo/repository_renamed_event_store_spec.rb'
-- './ee/spec/services/geo/repository_sync_service_spec.rb'
-- './ee/spec/services/geo/repository_updated_event_store_spec.rb'
-- './ee/spec/services/geo/repository_updated_service_spec.rb'
-- './ee/spec/services/geo/repository_verification_primary_service_spec.rb'
-- './ee/spec/services/geo/repository_verification_reset_spec.rb'
-- './ee/spec/services/geo/repository_verification_secondary_service_spec.rb'
-- './ee/spec/services/geo/reset_checksum_event_store_spec.rb'
-- './ee/spec/services/geo/wiki_sync_service_spec.rb'
- './ee/spec/services/gitlab_subscriptions/activate_service_spec.rb'
- './ee/spec/services/gitlab_subscriptions/check_future_renewal_service_spec.rb'
- './ee/spec/services/gitlab_subscriptions/create_service_spec.rb'
@@ -2823,7 +2736,6 @@
- './ee/spec/services/personal_access_tokens/revoke_invalid_tokens_spec.rb'
- './ee/spec/services/personal_access_tokens/revoke_service_audit_log_spec.rb'
- './ee/spec/services/personal_access_tokens/rotation_verifier_service_spec.rb'
-- './ee/spec/services/projects/after_rename_service_spec.rb'
- './ee/spec/services/projects/alerting/notify_service_spec.rb'
- './ee/spec/services/projects/cleanup_service_spec.rb'
- './ee/spec/services/projects/create_from_template_service_spec.rb'
@@ -2838,13 +2750,11 @@
- './ee/spec/services/projects/group_links/destroy_service_spec.rb'
- './ee/spec/services/projects/group_links/update_service_spec.rb'
- './ee/spec/services/projects/hashed_storage/migrate_attachments_service_spec.rb'
-- './ee/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb'
- './ee/spec/services/projects/import_export/export_service_spec.rb'
- './ee/spec/services/projects/import_service_spec.rb'
- './ee/spec/services/projects/mark_for_deletion_service_spec.rb'
- './ee/spec/services/projects/open_issues_count_service_spec.rb'
- './ee/spec/services/projects/operations/update_service_spec.rb'
-- './ee/spec/services/projects/prometheus/alerts/notify_service_spec.rb'
- './ee/spec/services/projects/protect_default_branch_service_spec.rb'
- './ee/spec/services/projects/restore_service_spec.rb'
- './ee/spec/services/projects/setup_ci_cd_spec.rb'
@@ -2894,7 +2804,6 @@
- './ee/spec/services/security/ingestion/tasks/ingest_finding_signatures_spec.rb'
- './ee/spec/services/security/ingestion/tasks/ingest_findings_spec.rb'
- './ee/spec/services/security/ingestion/tasks/ingest_identifiers_spec.rb'
-- './ee/spec/services/security/ingestion/tasks/ingest_issue_links_spec.rb'
- './ee/spec/services/security/ingestion/tasks/ingest_remediations_spec.rb'
- './ee/spec/services/security/ingestion/tasks/ingest_vulnerabilities/create_spec.rb'
- './ee/spec/services/security/ingestion/tasks/ingest_vulnerabilities/mark_resolved_as_detected_spec.rb'
@@ -2925,7 +2834,6 @@
- './ee/spec/services/security/security_orchestration_policies/rule_schedule_service_spec.rb'
- './ee/spec/services/security/security_orchestration_policies/scan_pipeline_service_spec.rb'
- './ee/spec/services/security/security_orchestration_policies/sync_opened_merge_requests_service_spec.rb'
-- './ee/spec/services/security/security_orchestration_policies/sync_open_merge_requests_head_pipeline_service_spec.rb'
- './ee/spec/services/security/security_orchestration_policies/validate_policy_service_spec.rb'
- './ee/spec/services/security/store_grouped_scans_service_spec.rb'
- './ee/spec/services/security/store_scan_service_spec.rb'
@@ -2953,14 +2861,12 @@
- './ee/spec/services/users/abuse/git_abuse/namespace_throttle_service_spec.rb'
- './ee/spec/services/users/abuse/namespace_bans/create_service_spec.rb'
- './ee/spec/services/users/abuse/namespace_bans/destroy_service_spec.rb'
-- './ee/spec/services/users/captcha_challenge_service_spec.rb'
- './ee/spec/services/users_ops_dashboard_projects/destroy_service_spec.rb'
- './ee/spec/services/users/update_highest_member_role_service_spec.rb'
- './ee/spec/services/vulnerabilities/confirm_service_spec.rb'
- './ee/spec/services/vulnerabilities/create_service_spec.rb'
- './ee/spec/services/vulnerabilities/destroy_dismissal_feedback_service_spec.rb'
- './ee/spec/services/vulnerabilities/dismiss_service_spec.rb'
-- './ee/spec/services/vulnerabilities/finding_dismiss_service_spec.rb'
- './ee/spec/services/vulnerabilities/historical_statistics/adjustment_service_spec.rb'
- './ee/spec/services/vulnerabilities/historical_statistics/deletion_service_spec.rb'
- './ee/spec/services/vulnerabilities/manually_create_service_spec.rb'
@@ -2990,7 +2896,6 @@
- './ee/spec/services/wikis/create_attachment_service_spec.rb'
- './ee/spec/services/work_items/update_service_spec.rb'
- './ee/spec/services/work_items/widgets/weight_service/update_service_spec.rb'
-- './ee/spec/tasks/geo/git_rake_spec.rb'
- './ee/spec/tasks/geo_rake_spec.rb'
- './ee/spec/tasks/gitlab/check_rake_spec.rb'
- './ee/spec/tasks/gitlab/elastic_rake_spec.rb'
@@ -3019,9 +2924,7 @@
- './ee/spec/views/compliance_management/compliance_framework/_project_settings.html.haml_spec.rb'
- './ee/spec/views/devise/sessions/new.html.haml_spec.rb'
- './ee/spec/views/groups/billings/index.html.haml_spec.rb'
-- './ee/spec/views/groups/compliance_frameworks/edit.html.haml_spec.rb'
- './ee/spec/views/groups/_compliance_frameworks.html.haml_spec.rb'
-- './ee/spec/views/groups/compliance_frameworks/new.html.haml_spec.rb'
- './ee/spec/views/groups/edit.html.haml_spec.rb'
- './ee/spec/views/groups/hook_logs/show.html.haml_spec.rb'
- './ee/spec/views/groups/hooks/edit.html.haml_spec.rb'
@@ -3041,7 +2944,6 @@
- './ee/spec/views/operations/index.html.haml_spec.rb'
- './ee/spec/views/profiles/preferences/show.html.haml_spec.rb'
- './ee/spec/views/projects/edit.html.haml_spec.rb'
-- './ee/spec/views/projects/issues/show.html.haml_spec.rb'
- './ee/spec/views/projects/on_demand_scans/index.html.haml_spec.rb'
- './ee/spec/views/projects/security/corpus_management/show.html.haml_spec.rb'
- './ee/spec/views/projects/security/dast_profiles/show.html.haml_spec.rb'
@@ -3137,19 +3039,13 @@
- './ee/spec/workers/elastic_remove_expired_namespace_subscriptions_from_index_cron_worker_spec.rb'
- './ee/spec/workers/epics/new_epic_issue_worker_spec.rb'
- './ee/spec/workers/geo/batch_event_create_worker_spec.rb'
-- './ee/spec/workers/geo/batch/project_registry_scheduler_worker_spec.rb'
-- './ee/spec/workers/geo/batch/project_registry_worker_spec.rb'
- './ee/spec/workers/geo/container_repository_sync_worker_spec.rb'
- './ee/spec/workers/geo/create_repository_updated_event_worker_spec.rb'
-- './ee/spec/workers/geo/design_repository_shard_sync_worker_spec.rb'
-- './ee/spec/workers/geo/design_repository_sync_worker_spec.rb'
- './ee/spec/workers/geo/destroy_worker_spec.rb'
- './ee/spec/workers/geo/event_worker_spec.rb'
- './ee/spec/workers/geo/metrics_update_worker_spec.rb'
-- './ee/spec/workers/geo/project_sync_worker_spec.rb'
- './ee/spec/workers/geo/prune_event_log_worker_spec.rb'
- './ee/spec/workers/geo/registry_sync_worker_spec.rb'
-- './ee/spec/workers/geo/repositories_clean_up_worker_spec.rb'
- './ee/spec/workers/geo/reverification_batch_worker_spec.rb'
- './ee/spec/workers/geo/scheduler/scheduler_worker_spec.rb'
- './ee/spec/workers/geo/secondary/registry_consistency_worker_spec.rb'
@@ -3190,7 +3086,6 @@
- './ee/spec/workers/project_import_schedule_worker_spec.rb'
- './ee/spec/workers/projects/disable_legacy_open_source_license_for_inactive_projects_worker_spec.rb'
- './ee/spec/workers/project_template_export_worker_spec.rb'
-- './ee/spec/workers/refresh_license_compliance_checks_worker_spec.rb'
- './ee/spec/workers/repository_import_worker_spec.rb'
- './ee/spec/workers/repository_update_mirror_worker_spec.rb'
- './ee/spec/workers/requirements_management/import_requirements_csv_worker_spec.rb'
@@ -3286,12 +3181,10 @@
- './spec/controllers/concerns/internal_redirect_spec.rb'
- './spec/controllers/concerns/issuable_actions_spec.rb'
- './spec/controllers/concerns/issuable_collections_spec.rb'
-- './spec/controllers/concerns/metrics_dashboard_spec.rb'
- './spec/controllers/concerns/page_limiter_spec.rb'
- './spec/controllers/concerns/product_analytics_tracking_spec.rb'
- './spec/controllers/concerns/project_unauthorized_spec.rb'
- './spec/controllers/concerns/redirects_for_missing_path_on_tree_spec.rb'
-- './spec/controllers/concerns/redis_tracking_spec.rb'
- './spec/controllers/concerns/renders_commits_spec.rb'
- './spec/controllers/concerns/routable_actions_spec.rb'
- './spec/controllers/concerns/send_file_upload_spec.rb'
@@ -3347,7 +3240,6 @@
- './spec/controllers/import/fogbugz_controller_spec.rb'
- './spec/controllers/import/gitea_controller_spec.rb'
- './spec/controllers/import/github_controller_spec.rb'
-- './spec/controllers/import/gitlab_controller_spec.rb'
- './spec/controllers/import/manifest_controller_spec.rb'
- './spec/controllers/invites_controller_spec.rb'
- './spec/controllers/jira_connect/app_descriptor_controller_spec.rb'
@@ -3359,7 +3251,6 @@
- './spec/controllers/oauth/applications_controller_spec.rb'
- './spec/controllers/oauth/authorizations_controller_spec.rb'
- './spec/controllers/oauth/authorized_applications_controller_spec.rb'
-- './spec/controllers/oauth/jira_dvcs/authorizations_controller_spec.rb'
- './spec/controllers/oauth/token_info_controller_spec.rb'
- './spec/controllers/oauth/tokens_controller_spec.rb'
- './spec/controllers/omniauth_callbacks_controller_spec.rb'
@@ -3377,7 +3268,6 @@
- './spec/controllers/profiles/two_factor_auths_controller_spec.rb'
- './spec/controllers/profiles/webauthn_registrations_controller_spec.rb'
- './spec/controllers/projects/alerting/notifications_controller_spec.rb'
-- './spec/controllers/projects/alert_management_controller_spec.rb'
- './spec/controllers/projects/analytics/cycle_analytics/stages_controller_spec.rb'
- './spec/controllers/projects/analytics/cycle_analytics/summary_controller_spec.rb'
- './spec/controllers/projects/analytics/cycle_analytics/value_streams_controller_spec.rb'
@@ -3405,8 +3295,6 @@
- './spec/controllers/projects/design_management/designs/resized_image_controller_spec.rb'
- './spec/controllers/projects/discussions_controller_spec.rb'
- './spec/controllers/projects/environments_controller_spec.rb'
-- './spec/controllers/projects/environments/prometheus_api_controller_spec.rb'
-- './spec/controllers/projects/environments/sample_metrics_controller_spec.rb'
- './spec/controllers/projects/error_tracking_controller_spec.rb'
- './spec/controllers/projects/error_tracking/projects_controller_spec.rb'
- './spec/controllers/projects/error_tracking/stack_traces_controller_spec.rb'
@@ -3415,7 +3303,6 @@
- './spec/controllers/projects/feature_flags_user_lists_controller_spec.rb'
- './spec/controllers/projects/find_file_controller_spec.rb'
- './spec/controllers/projects/forks_controller_spec.rb'
-- './spec/controllers/projects/grafana_api_controller_spec.rb'
- './spec/controllers/projects/graphs_controller_spec.rb'
- './spec/controllers/projects/group_links_controller_spec.rb'
- './spec/controllers/projects/hooks_controller_spec.rb'
@@ -3439,15 +3326,12 @@
- './spec/controllers/projects/packages/packages_controller_spec.rb'
- './spec/controllers/projects/pages_controller_spec.rb'
- './spec/controllers/projects/pages_domains_controller_spec.rb'
-- './spec/controllers/projects/performance_monitoring/dashboards_controller_spec.rb'
- './spec/controllers/projects/pipeline_schedules_controller_spec.rb'
- './spec/controllers/projects/pipelines_controller_spec.rb'
- './spec/controllers/projects/pipelines_settings_controller_spec.rb'
- './spec/controllers/projects/pipelines/stages_controller_spec.rb'
- './spec/controllers/projects/pipelines/tests_controller_spec.rb'
- './spec/controllers/projects/project_members_controller_spec.rb'
-- './spec/controllers/projects/prometheus/alerts_controller_spec.rb'
-- './spec/controllers/projects/prometheus/metrics_controller_spec.rb'
- './spec/controllers/projects/protected_branches_controller_spec.rb'
- './spec/controllers/projects/protected_tags_controller_spec.rb'
- './spec/controllers/projects/raw_controller_spec.rb'
@@ -3458,7 +3342,6 @@
- './spec/controllers/projects/releases/evidences_controller_spec.rb'
- './spec/controllers/projects/repositories_controller_spec.rb'
- './spec/controllers/projects/security/configuration_controller_spec.rb'
-- './spec/controllers/projects/service_desk_controller_spec.rb'
- './spec/controllers/projects/service_ping_controller_spec.rb'
- './spec/controllers/projects/settings/ci_cd_controller_spec.rb'
- './spec/controllers/projects/settings/integration_hook_logs_controller_spec.rb'
@@ -3494,17 +3377,14 @@
- './spec/controllers/users/terms_controller_spec.rb'
- './spec/controllers/users/unsubscribes_controller_spec.rb'
- './spec/db/development/create_base_work_item_types_spec.rb'
-- './spec/db/development/import_common_metrics_spec.rb'
- './spec/db/docs_spec.rb'
- './spec/db/migration_spec.rb'
- './spec/db/production/create_base_work_item_types_spec.rb'
-- './spec/db/production/import_common_metrics_spec.rb'
- './spec/db/production/settings_spec.rb'
- './spec/db/schema_spec.rb'
- './spec/dependencies/omniauth_saml_spec.rb'
- './spec/experiments/application_experiment_spec.rb'
- './spec/experiments/in_product_guidance_environments_webide_experiment_spec.rb'
-- './spec/experiments/ios_specific_templates_experiment_spec.rb'
- './spec/features/abuse_report_spec.rb'
- './spec/features/action_cable_logging_spec.rb'
- './spec/features/admin/admin_abuse_reports_spec.rb'
@@ -3575,7 +3455,6 @@
- './spec/features/callouts/registration_enabled_spec.rb'
- './spec/features/canonical_link_spec.rb'
- './spec/features/clusters/cluster_detail_page_spec.rb'
-- './spec/features/clusters/cluster_health_dashboard_spec.rb'
- './spec/features/clusters/create_agent_spec.rb'
- './spec/features/commit_spec.rb'
- './spec/features/commits_spec.rb'
@@ -3592,7 +3471,6 @@
- './spec/features/dashboard/issuables_counter_spec.rb'
- './spec/features/dashboard/issues_filter_spec.rb'
- './spec/features/dashboard/issues_spec.rb'
-- './spec/features/dashboard/label_filter_spec.rb'
- './spec/features/dashboard/merge_requests_spec.rb'
- './spec/features/dashboard/milestones_spec.rb'
- './spec/features/dashboard/project_member_activity_index_spec.rb'
@@ -3767,7 +3645,6 @@
- './spec/features/issues/user_views_issues_spec.rb'
- './spec/features/jira_connect/branches_spec.rb'
- './spec/features/jira_connect/subscriptions_spec.rb'
-- './spec/features/jira_oauth_provider_authorize_spec.rb'
- './spec/features/labels_hierarchy_spec.rb'
- './spec/features/markdown/copy_as_gfm_spec.rb'
- './spec/features/markdown/gitlab_flavored_markdown_spec.rb'
@@ -3776,7 +3653,6 @@
- './spec/features/markdown/kroki_spec.rb'
- './spec/features/markdown/markdown_spec.rb'
- './spec/features/markdown/math_spec.rb'
-- './spec/features/markdown/metrics_spec.rb'
- './spec/features/markdown/sandboxed_mermaid_spec.rb'
- './spec/features/merge_request/batch_comments_spec.rb'
- './spec/features/merge_request/close_reopen_report_toggle_spec.rb'
@@ -3827,7 +3703,6 @@
- './spec/features/merge_request/user_merges_immediately_spec.rb'
- './spec/features/merge_request/user_merges_merge_request_spec.rb'
- './spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb'
-- './spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb'
- './spec/features/merge_request/user_opens_checkout_branch_modal_spec.rb'
- './spec/features/merge_request/user_opens_context_commits_modal_spec.rb'
- './spec/features/merge_request/user_posts_diff_notes_spec.rb'
@@ -3883,11 +3758,8 @@
- './spec/features/milestones/user_views_milestone_spec.rb'
- './spec/features/milestones/user_views_milestones_spec.rb'
- './spec/features/monitor_sidebar_link_spec.rb'
-- './spec/features/nav/top_nav_responsive_spec.rb'
-- './spec/features/nav/top_nav_tooltip_spec.rb'
- './spec/features/oauth_login_spec.rb'
- './spec/features/oauth_provider_authorize_spec.rb'
-- './spec/features/oauth_registration_spec.rb'
- './spec/features/one_trust_spec.rb'
- './spec/features/participants_autocomplete_spec.rb'
- './spec/features/password_reset_spec.rb'
@@ -3905,16 +3777,13 @@
- './spec/features/profiles/two_factor_auths_spec.rb'
- './spec/features/profiles/user_changes_notified_of_own_activity_spec.rb'
- './spec/features/profiles/user_edit_preferences_spec.rb'
-- './spec/features/profiles/user_edit_profile_spec.rb'
- './spec/features/profiles/user_manages_applications_spec.rb'
- './spec/features/profiles/user_manages_emails_spec.rb'
- './spec/features/profiles/user_search_settings_spec.rb'
- './spec/features/profiles/user_visits_notifications_tab_spec.rb'
-- './spec/features/profiles/user_visits_profile_account_page_spec.rb'
- './spec/features/profiles/user_visits_profile_authentication_log_spec.rb'
- './spec/features/profiles/user_visits_profile_preferences_page_spec.rb'
- './spec/features/profiles/user_visits_profile_spec.rb'
-- './spec/features/profiles/user_visits_profile_ssh_keys_page_spec.rb'
- './spec/features/project_group_variables_spec.rb'
- './spec/features/projects/active_tabs_spec.rb'
- './spec/features/projects/activity/rss_spec.rb'
@@ -3967,7 +3836,6 @@
- './spec/features/projects/container_registry_spec.rb'
- './spec/features/projects/deploy_keys_spec.rb'
- './spec/features/projects/diffs/diff_show_spec.rb'
-- './spec/features/projects/environments/environment_metrics_spec.rb'
- './spec/features/projects/environments/environment_spec.rb'
- './spec/features/projects/environments/environments_spec.rb'
- './spec/features/projects/feature_flags/user_creates_feature_flag_spec.rb'
@@ -4023,7 +3891,6 @@
- './spec/features/projects/integrations/user_activates_mattermost_slash_command_spec.rb'
- './spec/features/projects/integrations/user_activates_packagist_spec.rb'
- './spec/features/projects/integrations/user_activates_pivotaltracker_spec.rb'
-- './spec/features/projects/integrations/user_activates_prometheus_spec.rb'
- './spec/features/projects/integrations/user_activates_pushover_spec.rb'
- './spec/features/projects/integrations/user_activates_slack_notifications_spec.rb'
- './spec/features/projects/integrations/user_activates_slack_slash_command_spec.rb'
@@ -4143,7 +4010,6 @@
- './spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb'
- './spec/features/projects/show/user_uploads_files_spec.rb'
- './spec/features/projects/snippets/create_snippet_spec.rb'
-- './spec/features/projects/snippets/show_spec.rb'
- './spec/features/projects/snippets/user_comments_on_snippet_spec.rb'
- './spec/features/projects/snippets/user_deletes_snippet_spec.rb'
- './spec/features/projects/snippets/user_updates_snippet_spec.rb'
@@ -4214,7 +4080,6 @@
- './spec/features/snippets/private_snippets_spec.rb'
- './spec/features/snippets/public_snippets_spec.rb'
- './spec/features/snippets/search_snippets_spec.rb'
-- './spec/features/snippets/show_spec.rb'
- './spec/features/snippets/spam_snippets_spec.rb'
- './spec/features/snippets_spec.rb'
- './spec/features/snippets/user_creates_snippet_spec.rb'
@@ -4237,12 +4102,10 @@
- './spec/features/user_opens_link_to_comment_spec.rb'
- './spec/features/users/active_sessions_spec.rb'
- './spec/features/users/add_email_to_existing_account_spec.rb'
-- './spec/features/users/anonymous_sessions_spec.rb'
- './spec/features/users/bizible_csp_spec.rb'
- './spec/features/users/confirmation_spec.rb'
- './spec/features/user_sees_revert_modal_spec.rb'
- './spec/features/users/email_verification_on_login_spec.rb'
-- './spec/features/users/google_analytics_csp_spec.rb'
- './spec/features/users/login_spec.rb'
- './spec/features/users/logout_spec.rb'
- './spec/features/users/one_trust_csp_spec.rb'
@@ -4338,7 +4201,6 @@
- './spec/finders/merge_requests_finder_spec.rb'
- './spec/finders/merge_requests/oldest_per_commit_finder_spec.rb'
- './spec/finders/merge_request_target_project_finder_spec.rb'
-- './spec/finders/metrics/users_starred_dashboards_finder_spec.rb'
- './spec/finders/milestones_finder_spec.rb'
- './spec/finders/namespaces/projects_finder_spec.rb'
- './spec/finders/notes_finder_spec.rb'
@@ -4403,7 +4265,6 @@
- './spec/finders/users_finder_spec.rb'
- './spec/finders/users_star_projects_finder_spec.rb'
- './spec/finders/work_items/work_items_finder_spec.rb'
-- './spec/frontend/fixtures/abuse_reports.rb'
- './spec/frontend/fixtures/admin_users.rb'
- './spec/frontend/fixtures/analytics.rb'
- './spec/frontend/fixtures/api_deploy_keys.rb'
@@ -4426,7 +4287,6 @@
- './spec/frontend/fixtures/listbox.rb'
- './spec/frontend/fixtures/merge_requests_diffs.rb'
- './spec/frontend/fixtures/merge_requests.rb'
-- './spec/frontend/fixtures/metrics_dashboard.rb'
- './spec/frontend/fixtures/namespaces.rb'
- './spec/frontend/fixtures/pipeline_schedules.rb'
- './spec/frontend/fixtures/pipelines.rb'
@@ -4439,7 +4299,6 @@
- './spec/frontend/fixtures/search.rb'
- './spec/frontend/fixtures/sessions.rb'
- './spec/frontend/fixtures/snippet.rb'
-- './spec/frontend/fixtures/startup_css.rb'
- './spec/frontend/fixtures/tabs.rb'
- './spec/frontend/fixtures/tags.rb'
- './spec/frontend/fixtures/timezones.rb'
@@ -4459,7 +4318,6 @@
- './spec/graphql/mutations/alert_management/prometheus_integration/reset_token_spec.rb'
- './spec/graphql/mutations/alert_management/prometheus_integration/update_spec.rb'
- './spec/graphql/mutations/alert_management/update_alert_status_spec.rb'
-- './spec/graphql/mutations/base_mutation_spec.rb'
- './spec/graphql/mutations/boards/issues/issue_move_list_spec.rb'
- './spec/graphql/mutations/boards/lists/create_spec.rb'
- './spec/graphql/mutations/boards/lists/update_spec.rb'
@@ -4614,7 +4472,6 @@
- './spec/graphql/resolvers/merge_requests_count_resolver_spec.rb'
- './spec/graphql/resolvers/merge_requests_resolver_spec.rb'
- './spec/graphql/resolvers/metadata_resolver_spec.rb'
-- './spec/graphql/resolvers/metrics/dashboard_resolver_spec.rb'
- './spec/graphql/resolvers/metrics/dashboards/annotation_resolver_spec.rb'
- './spec/graphql/resolvers/namespace_projects_resolver_spec.rb'
- './spec/graphql/resolvers/package_details_resolver_spec.rb'
@@ -4830,7 +4687,6 @@
- './spec/graphql/types/metadata/kas_type_spec.rb'
- './spec/graphql/types/metadata_type_spec.rb'
- './spec/graphql/types/metrics/dashboards/annotation_type_spec.rb'
-- './spec/graphql/types/metrics/dashboard_type_spec.rb'
- './spec/graphql/types/milestone_stats_type_spec.rb'
- './spec/graphql/types/milestone_type_spec.rb'
- './spec/graphql/types/mutation_type_spec.rb'
@@ -5051,7 +4907,6 @@
- './spec/helpers/search_helper_spec.rb'
- './spec/helpers/sessions_helper_spec.rb'
- './spec/helpers/sidebars_helper_spec.rb'
-- './spec/helpers/sidekiq_helper_spec.rb'
- './spec/helpers/snippets_helper_spec.rb'
- './spec/helpers/sorting_helper_spec.rb'
- './spec/helpers/sourcegraph_helper_spec.rb'
@@ -5082,7 +4937,6 @@
- './spec/helpers/wiki_helper_spec.rb'
- './spec/helpers/wiki_page_version_helper_spec.rb'
- './spec/helpers/x509_helper_spec.rb'
-- './spec/initializers/00_rails_disable_joins_spec.rb'
- './spec/initializers/0_postgresql_types_spec.rb'
- './spec/initializers/100_patch_omniauth_oauth2_spec.rb'
- './spec/initializers/100_patch_omniauth_saml_spec.rb'
@@ -5092,7 +4946,6 @@
- './spec/initializers/action_mailer_hooks_spec.rb'
- './spec/initializers/active_record_locking_spec.rb'
- './spec/initializers/asset_proxy_setting_spec.rb'
-- './spec/initializers/carrierwave_patch_spec.rb'
- './spec/initializers/cookies_serializer_spec.rb'
- './spec/initializers/database_config_spec.rb'
- './spec/initializers/diagnostic_reports_spec.rb'
@@ -5103,7 +4956,6 @@
- './spec/initializers/forbid_sidekiq_in_transactions_spec.rb'
- './spec/initializers/global_id_spec.rb'
- './spec/initializers/google_api_client_spec.rb'
-- './spec/initializers/hangouts_chat_http_override_spec.rb'
- './spec/initializers/lograge_spec.rb'
- './spec/initializers/mail_encoding_patch_spec.rb'
- './spec/initializers/mailer_retries_spec.rb'
@@ -5172,7 +5024,6 @@
- './spec/lib/api/entities/user_spec.rb'
- './spec/lib/api/entities/wiki_page_spec.rb'
- './spec/lib/api/every_api_endpoint_spec.rb'
-- './spec/lib/api/github/entities_spec.rb'
- './spec/lib/api/helpers/authentication_spec.rb'
- './spec/lib/api/helpers/caching_spec.rb'
- './spec/lib/api/helpers/common_helpers_spec.rb'
@@ -5246,12 +5097,7 @@
- './spec/lib/banzai/filter/html_entity_filter_spec.rb'
- './spec/lib/banzai/filter/image_lazy_load_filter_spec.rb'
- './spec/lib/banzai/filter/image_link_filter_spec.rb'
-- './spec/lib/banzai/filter/inline_alert_metrics_filter_spec.rb'
-- './spec/lib/banzai/filter/inline_cluster_metrics_filter_spec.rb'
- './spec/lib/banzai/filter/inline_diff_filter_spec.rb'
-- './spec/lib/banzai/filter/inline_grafana_metrics_filter_spec.rb'
-- './spec/lib/banzai/filter/inline_metrics_filter_spec.rb'
-- './spec/lib/banzai/filter/inline_metrics_redactor_filter_spec.rb'
- './spec/lib/banzai/filter/issuable_reference_expansion_filter_spec.rb'
- './spec/lib/banzai/filter/jira_import/adf_to_commonmark_filter_spec.rb'
- './spec/lib/banzai/filter/kroki_filter_spec.rb'
@@ -5363,7 +5209,6 @@
- './spec/lib/bulk_imports/common/pipelines/wiki_pipeline_spec.rb'
- './spec/lib/bulk_imports/common/rest/get_badges_query_spec.rb'
- './spec/lib/bulk_imports/common/transformers/prohibited_attributes_transformer_spec.rb'
-- './spec/lib/bulk_imports/common/transformers/user_reference_transformer_spec.rb'
- './spec/lib/bulk_imports/groups/extractors/subgroups_extractor_spec.rb'
- './spec/lib/bulk_imports/groups/graphql/get_group_query_spec.rb'
- './spec/lib/bulk_imports/groups/graphql/get_projects_query_spec.rb'
@@ -5430,7 +5275,6 @@
- './spec/lib/feature_spec.rb'
- './spec/lib/file_size_validator_spec.rb'
- './spec/lib/forever_spec.rb'
-- './spec/lib/generators/gitlab/snowplow_event_definition_generator_spec.rb'
- './spec/lib/generators/gitlab/usage_metric_definition_generator_spec.rb'
- './spec/lib/generators/gitlab/usage_metric_definition/redis_hll_generator_spec.rb'
- './spec/lib/generators/gitlab/usage_metric_generator_spec.rb'
@@ -5442,7 +5286,6 @@
- './spec/lib/gitlab/alert_management/fingerprint_spec.rb'
- './spec/lib/gitlab/alert_management/payload/base_spec.rb'
- './spec/lib/gitlab/alert_management/payload/generic_spec.rb'
-- './spec/lib/gitlab/alert_management/payload/managed_prometheus_spec.rb'
- './spec/lib/gitlab/alert_management/payload/prometheus_spec.rb'
- './spec/lib/gitlab/alert_management/payload_spec.rb'
- './spec/lib/gitlab/allowable_spec.rb'
@@ -5531,14 +5374,8 @@
- './spec/lib/gitlab/auth/unique_ips_limiter_spec.rb'
- './spec/lib/gitlab/auth/user_access_denied_reason_spec.rb'
- './spec/lib/gitlab/avatar_cache_spec.rb'
-- './spec/lib/gitlab/background_migration/backfill_draft_status_on_merge_requests_spec.rb'
-- './spec/lib/gitlab/background_migration/backfill_draft_status_on_merge_requests_with_corrected_regex_spec.rb'
-- './spec/lib/gitlab/background_migration/backfill_group_features_spec.rb'
- './spec/lib/gitlab/background_migration/backfill_imported_issue_search_data_spec.rb'
- './spec/lib/gitlab/background_migration/backfill_integrations_enable_ssl_verification_spec.rb'
-- './spec/lib/gitlab/background_migration/backfill_issue_search_data_spec.rb'
-- './spec/lib/gitlab/background_migration/backfill_member_namespace_for_group_members_spec.rb'
-- './spec/lib/gitlab/background_migration/backfill_namespace_id_for_namespace_route_spec.rb'
- './spec/lib/gitlab/background_migration/backfill_namespace_id_for_project_route_spec.rb'
- './spec/lib/gitlab/background_migration/backfill_namespace_id_of_vulnerability_reads_spec.rb'
- './spec/lib/gitlab/background_migration/backfill_note_discussion_id_spec.rb'
@@ -5546,20 +5383,17 @@
- './spec/lib/gitlab/background_migration/backfill_project_import_level_spec.rb'
- './spec/lib/gitlab/background_migration/backfill_project_member_namespace_id_spec.rb'
- './spec/lib/gitlab/background_migration/backfill_project_repositories_spec.rb'
-- './spec/lib/gitlab/background_migration/backfill_project_settings_spec.rb'
- './spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb'
- './spec/lib/gitlab/background_migration/backfill_topics_title_spec.rb'
- './spec/lib/gitlab/background_migration/backfill_vulnerability_reads_cluster_agent_spec.rb'
- './spec/lib/gitlab/background_migration/backfill_work_item_type_id_for_issues_spec.rb'
- './spec/lib/gitlab/background_migration/base_job_spec.rb'
- './spec/lib/gitlab/background_migration/batched_migration_job_spec.rb'
-- './spec/lib/gitlab/background_migration/batching_strategies/backfill_project_namespace_per_group_batching_strategy_spec.rb'
- './spec/lib/gitlab/background_migration/batching_strategies/backfill_project_statistics_with_container_registry_size_batching_strategy_spec.rb'
- './spec/lib/gitlab/background_migration/batching_strategies/base_strategy_spec.rb'
- './spec/lib/gitlab/background_migration/batching_strategies/dismissed_vulnerabilities_strategy_spec.rb'
- './spec/lib/gitlab/background_migration/batching_strategies/loose_index_scan_batching_strategy_spec.rb'
- './spec/lib/gitlab/background_migration/batching_strategies/primary_key_batching_strategy_spec.rb'
-- './spec/lib/gitlab/background_migration/cleanup_draft_data_from_faulty_regex_spec.rb'
- './spec/lib/gitlab/background_migration/cleanup_orphaned_routes_spec.rb'
- './spec/lib/gitlab/background_migration/copy_column_using_background_migration_job_spec.rb'
- './spec/lib/gitlab/background_migration/destroy_invalid_group_members_spec.rb'
@@ -5568,23 +5402,12 @@
- './spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_no_issues_no_repo_projects_spec.rb'
- './spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_one_member_no_repo_projects_spec.rb'
- './spec/lib/gitlab/background_migration/expire_o_auth_tokens_spec.rb'
-- './spec/lib/gitlab/background_migration/fix_duplicate_project_name_and_path_spec.rb'
-- './spec/lib/gitlab/background_migration/fix_vulnerability_occurrences_with_hashes_as_raw_metadata_spec.rb'
- './spec/lib/gitlab/background_migration/job_coordinator_spec.rb'
- './spec/lib/gitlab/background_migration/legacy_upload_mover_spec.rb'
- './spec/lib/gitlab/background_migration/legacy_uploads_migrator_spec.rb'
- './spec/lib/gitlab/background_migration/mailers/unconfirm_mailer_spec.rb'
-- './spec/lib/gitlab/background_migration/migrate_personal_namespace_project_maintainer_to_owner_spec.rb'
-- './spec/lib/gitlab/background_migration/migrate_shimo_confluence_integration_category_spec.rb'
-- './spec/lib/gitlab/background_migration/nullify_orphan_runner_id_on_ci_builds_spec.rb'
-- './spec/lib/gitlab/background_migration/populate_container_repository_migration_plan_spec.rb'
- './spec/lib/gitlab/background_migration/populate_operation_visibility_permissions_from_operations_spec.rb'
-- './spec/lib/gitlab/background_migration/project_namespaces/backfill_project_namespaces_spec.rb'
-- './spec/lib/gitlab/background_migration/remove_occurrence_pipelines_and_duplicate_vulnerabilities_findings_spec.rb'
- './spec/lib/gitlab/background_migration/remove_self_managed_wiki_notes_spec.rb'
-- './spec/lib/gitlab/background_migration/remove_vulnerability_finding_links_spec.rb'
-- './spec/lib/gitlab/background_migration/reset_duplicate_ci_runners_token_encrypted_values_on_projects_spec.rb'
-- './spec/lib/gitlab/background_migration/reset_duplicate_ci_runners_token_values_on_projects_spec.rb'
- './spec/lib/gitlab/background_migration/reset_too_many_tags_skipped_registry_imports_spec.rb'
- './spec/lib/gitlab/background_migration/set_correct_vulnerability_state_spec.rb'
- './spec/lib/gitlab/background_migration/set_legacy_open_source_license_available_for_non_public_projects_spec.rb'
@@ -5597,7 +5420,6 @@
- './spec/lib/gitlab/bitbucket_import/importer_spec.rb'
- './spec/lib/gitlab/bitbucket_import/project_creator_spec.rb'
- './spec/lib/gitlab/bitbucket_import/wiki_formatter_spec.rb'
-- './spec/lib/gitlab/bitbucket_server_import/importer_spec.rb'
- './spec/lib/gitlab/blame_spec.rb'
- './spec/lib/gitlab/blob_helper_spec.rb'
- './spec/lib/gitlab/branch_push_merge_commit_analyzer_spec.rb'
@@ -5622,7 +5444,6 @@
- './spec/lib/gitlab/chat/responder/mattermost_spec.rb'
- './spec/lib/gitlab/chat/responder/slack_spec.rb'
- './spec/lib/gitlab/chat/responder_spec.rb'
-- './spec/lib/gitlab/chat_spec.rb'
- './spec/lib/gitlab/checks/branch_check_spec.rb'
- './spec/lib/gitlab/checks/changes_access_spec.rb'
- './spec/lib/gitlab/checks/container_moved_spec.rb'
@@ -5896,7 +5717,6 @@
- './spec/lib/gitlab/ci/status/build/skipped_spec.rb'
- './spec/lib/gitlab/ci/status/build/stop_spec.rb'
- './spec/lib/gitlab/ci/status/build/unschedule_spec.rb'
-- './spec/lib/gitlab/ci/status/build/waiting_for_approval_spec.rb'
- './spec/lib/gitlab/ci/status/build/waiting_for_resource_spec.rb'
- './spec/lib/gitlab/ci/status/canceled_spec.rb'
- './spec/lib/gitlab/ci/status/composite_spec.rb'
@@ -5984,7 +5804,6 @@
- './spec/lib/gitlab/composer/version_index_spec.rb'
- './spec/lib/gitlab/conan_token_spec.rb'
- './spec/lib/gitlab/config_checker/external_database_checker_spec.rb'
-- './spec/lib/gitlab/config_checker/puma_rugged_checker_spec.rb'
- './spec/lib/gitlab/config/entry/attributable_spec.rb'
- './spec/lib/gitlab/config/entry/boolean_spec.rb'
- './spec/lib/gitlab/config/entry/composable_array_spec.rb'
@@ -6029,9 +5848,6 @@
- './spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb'
- './spec/lib/gitlab/database/background_migration/batch_metrics_spec.rb'
- './spec/lib/gitlab/database/background_migration/batch_optimizer_spec.rb'
-- './spec/lib/gitlab/database/background_migration/health_status/indicators/autovacuum_active_on_table_spec.rb'
-- './spec/lib/gitlab/database/background_migration/health_status/indicators/write_ahead_log_spec.rb'
-- './spec/lib/gitlab/database/background_migration/health_status_spec.rb'
- './spec/lib/gitlab/database/background_migration_job_spec.rb'
- './spec/lib/gitlab/database/background_migration/prometheus_metrics_spec.rb'
- './spec/lib/gitlab/database/batch_count_spec.rb'
@@ -6047,9 +5863,6 @@
- './spec/lib/gitlab/database/each_database_spec.rb'
- './spec/lib/gitlab/database/gitlab_schema_spec.rb'
- './spec/lib/gitlab/database/grant_spec.rb'
-- './spec/lib/gitlab/database_importers/common_metrics/importer_spec.rb'
-- './spec/lib/gitlab/database_importers/common_metrics/prometheus_metric_spec.rb'
-- './spec/lib/gitlab/database_importers/instance_administrators/create_group_spec.rb'
- './spec/lib/gitlab/database_importers/work_items/base_type_importer_spec.rb'
- './spec/lib/gitlab/database/load_balancing/action_cable_callbacks_spec.rb'
- './spec/lib/gitlab/database/load_balancing/configuration_spec.rb'
@@ -6116,7 +5929,6 @@
- './spec/lib/gitlab/database/postgres_partitioned_table_spec.rb'
- './spec/lib/gitlab/database/postgres_partition_spec.rb'
- './spec/lib/gitlab/database/postgresql_adapter/dump_schema_versions_mixin_spec.rb'
-- './spec/lib/gitlab/database/postgresql_adapter/empty_query_ping_spec.rb'
- './spec/lib/gitlab/database/postgresql_adapter/force_disconnectable_mixin_spec.rb'
- './spec/lib/gitlab/database/postgresql_adapter/type_map_cache_spec.rb'
- './spec/lib/gitlab/database/postgresql_database_tasks/load_schema_versions_mixin_spec.rb'
@@ -6132,10 +5944,6 @@
- './spec/lib/gitlab/database/reindexing/reindex_action_spec.rb'
- './spec/lib/gitlab/database/reindexing/reindex_concurrently_spec.rb'
- './spec/lib/gitlab/database/reindexing_spec.rb'
-- './spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb'
-- './spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb'
-- './spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb'
-- './spec/lib/gitlab/database/rename_reserved_paths_migration/v1_spec.rb'
- './spec/lib/gitlab/database/schema_cache_with_renamed_table_spec.rb'
- './spec/lib/gitlab/database/schema_cleaner_spec.rb'
- './spec/lib/gitlab/database/schema_migrations/context_spec.rb'
@@ -6354,11 +6162,7 @@
- './spec/lib/gitlab/github_import/importer/note_importer_spec.rb'
- './spec/lib/gitlab/github_import/importer/notes_importer_spec.rb'
- './spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb'
-- './spec/lib/gitlab/github_import/importer/pull_request_merged_by_importer_spec.rb'
-- './spec/lib/gitlab/github_import/importer/pull_request_review_importer_spec.rb'
- './spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb'
-- './spec/lib/gitlab/github_import/importer/pull_requests_merged_by_importer_spec.rb'
-- './spec/lib/gitlab/github_import/importer/pull_requests_reviews_importer_spec.rb'
- './spec/lib/gitlab/github_import/importer/releases_importer_spec.rb'
- './spec/lib/gitlab/github_import/importer/repository_importer_spec.rb'
- './spec/lib/gitlab/github_import/importer/single_endpoint_diff_notes_importer_spec.rb'
@@ -6406,7 +6210,6 @@
- './spec/lib/gitlab/git/remote_mirror_spec.rb'
- './spec/lib/gitlab/git/repository_cleaner_spec.rb'
- './spec/lib/gitlab/git/repository_spec.rb'
-- './spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb'
- './spec/lib/gitlab/git_spec.rb'
- './spec/lib/gitlab/git/tag_spec.rb'
- './spec/lib/gitlab/git/tree_spec.rb'
@@ -6471,7 +6274,6 @@
- './spec/lib/gitlab/harbor/client_spec.rb'
- './spec/lib/gitlab/harbor/query_spec.rb'
- './spec/lib/gitlab/hashed_path_spec.rb'
-- './spec/lib/gitlab/hashed_storage/migrator_spec.rb'
- './spec/lib/gitlab/health_checks/db_check_spec.rb'
- './spec/lib/gitlab/health_checks/gitaly_check_spec.rb'
- './spec/lib/gitlab/health_checks/master_check_spec.rb'
@@ -6535,7 +6337,6 @@
- './spec/lib/gitlab/import_export/json/ndjson_reader_spec.rb'
- './spec/lib/gitlab/import_export/json/ndjson_writer_spec.rb'
- './spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb'
-- './spec/lib/gitlab/import_export/legacy_relation_tree_saver_spec.rb'
- './spec/lib/gitlab/import_export/lfs_restorer_spec.rb'
- './spec/lib/gitlab/import_export/lfs_saver_spec.rb'
- './spec/lib/gitlab/import_export/log_util_spec.rb'
@@ -6600,7 +6401,6 @@
- './spec/lib/gitlab/jira_import/labels_importer_spec.rb'
- './spec/lib/gitlab/jira_import/metadata_collector_spec.rb'
- './spec/lib/gitlab/jira_import_spec.rb'
-- './spec/lib/gitlab/jira/middleware_spec.rb'
- './spec/lib/gitlab/job_waiter_spec.rb'
- './spec/lib/gitlab/json_logger_spec.rb'
- './spec/lib/gitlab/json_spec.rb'
@@ -6685,27 +6485,6 @@
- './spec/lib/gitlab/merge_requests/mergeability/results_store_spec.rb'
- './spec/lib/gitlab/metrics/background_transaction_spec.rb'
- './spec/lib/gitlab/metrics/boot_time_tracker_spec.rb'
-- './spec/lib/gitlab/metrics/dashboard/cache_spec.rb'
-- './spec/lib/gitlab/metrics/dashboard/defaults_spec.rb'
-- './spec/lib/gitlab/metrics/dashboard/finder_spec.rb'
-- './spec/lib/gitlab/metrics/dashboard/importer_spec.rb'
-- './spec/lib/gitlab/metrics/dashboard/importers/prometheus_metrics_spec.rb'
-- './spec/lib/gitlab/metrics/dashboard/processor_spec.rb'
-- './spec/lib/gitlab/metrics/dashboard/repo_dashboard_finder_spec.rb'
-- './spec/lib/gitlab/metrics/dashboard/service_selector_spec.rb'
-- './spec/lib/gitlab/metrics/dashboard/stages/grafana_formatter_spec.rb'
-- './spec/lib/gitlab/metrics/dashboard/stages/metric_endpoint_inserter_spec.rb'
-- './spec/lib/gitlab/metrics/dashboard/stages/panel_ids_inserter_spec.rb'
-- './spec/lib/gitlab/metrics/dashboard/stages/track_panel_type_spec.rb'
-- './spec/lib/gitlab/metrics/dashboard/stages/url_validator_spec.rb'
-- './spec/lib/gitlab/metrics/dashboard/stages/variable_endpoint_inserter_spec.rb'
-- './spec/lib/gitlab/metrics/dashboard/transformers/yml/v1/prometheus_metrics_spec.rb'
-- './spec/lib/gitlab/metrics/dashboard/url_spec.rb'
-- './spec/lib/gitlab/metrics/dashboard/validator/client_spec.rb'
-- './spec/lib/gitlab/metrics/dashboard/validator/custom_formats_spec.rb'
-- './spec/lib/gitlab/metrics/dashboard/validator/errors_spec.rb'
-- './spec/lib/gitlab/metrics/dashboard/validator/post_schema_validator_spec.rb'
-- './spec/lib/gitlab/metrics/dashboard/validator_spec.rb'
- './spec/lib/gitlab/metrics/delta_spec.rb'
- './spec/lib/gitlab/metrics/elasticsearch_rack_middleware_spec.rb'
- './spec/lib/gitlab/metrics/exporter/base_exporter_spec.rb'
@@ -6767,7 +6546,6 @@
- './spec/lib/gitlab/optimistic_locking_spec.rb'
- './spec/lib/gitlab/other_markup_spec.rb'
- './spec/lib/gitlab/otp_key_rotator_spec.rb'
-- './spec/lib/gitlab/pages/cache_control_spec.rb'
- './spec/lib/gitlab/pages/deployment_update_spec.rb'
- './spec/lib/gitlab/pages/settings_spec.rb'
- './spec/lib/gitlab/pages_spec.rb'
@@ -6794,7 +6572,6 @@
- './spec/lib/gitlab/pagination/offset_header_builder_spec.rb'
- './spec/lib/gitlab/pagination/offset_header_builder_with_controller_spec.rb'
- './spec/lib/gitlab/pagination/offset_pagination_spec.rb'
-- './spec/lib/gitlab/patch/action_cable_redis_listener_spec.rb'
- './spec/lib/gitlab/patch/database_config_spec.rb'
- './spec/lib/gitlab/patch/draw_route_spec.rb'
- './spec/lib/gitlab/patch/prependable_spec.rb'
@@ -6818,16 +6595,8 @@
- './spec/lib/gitlab/project_template_spec.rb'
- './spec/lib/gitlab/project_transfer_spec.rb'
- './spec/lib/gitlab/prometheus/adapter_spec.rb'
-- './spec/lib/gitlab/prometheus/additional_metrics_parser_spec.rb'
- './spec/lib/gitlab/prometheus_client_spec.rb'
- './spec/lib/gitlab/prometheus/internal_spec.rb'
-- './spec/lib/gitlab/prometheus/metric_group_spec.rb'
-- './spec/lib/gitlab/prometheus/queries/additional_metrics_deployment_query_spec.rb'
-- './spec/lib/gitlab/prometheus/queries/additional_metrics_environment_query_spec.rb'
-- './spec/lib/gitlab/prometheus/queries/deployment_query_spec.rb'
-- './spec/lib/gitlab/prometheus/queries/matched_metric_query_spec.rb'
-- './spec/lib/gitlab/prometheus/queries/validate_query_spec.rb'
-- './spec/lib/gitlab/prometheus/query_variables_spec.rb'
- './spec/lib/gitlab/protocol_access_spec.rb'
- './spec/lib/gitlab/puma_logging/json_formatter_spec.rb'
- './spec/lib/gitlab/push_options_spec.rb'
@@ -6880,7 +6649,6 @@
- './spec/lib/gitlab/robots_txt/parser_spec.rb'
- './spec/lib/gitlab/route_map_spec.rb'
- './spec/lib/gitlab/routing_spec.rb'
-- './spec/lib/gitlab/rugged_instrumentation_spec.rb'
- './spec/lib/gitlab/runtime_spec.rb'
- './spec/lib/gitlab/saas_spec.rb'
- './spec/lib/gitlab/safe_request_loader_spec.rb'
@@ -6918,7 +6686,6 @@
- './spec/lib/gitlab/sidekiq_config/worker_matcher_spec.rb'
- './spec/lib/gitlab/sidekiq_config/worker_router_spec.rb'
- './spec/lib/gitlab/sidekiq_config/worker_spec.rb'
-- './spec/lib/gitlab/sidekiq_daemon/memory_killer_spec.rb'
- './spec/lib/gitlab/sidekiq_daemon/monitor_spec.rb'
- './spec/lib/gitlab/sidekiq_death_handler_spec.rb'
- './spec/lib/gitlab/sidekiq_logging/deduplication_logger_spec.rb'
@@ -7006,7 +6773,6 @@
- './spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb'
- './spec/lib/gitlab/template/issue_template_spec.rb'
- './spec/lib/gitlab/template/merge_request_template_spec.rb'
-- './spec/lib/gitlab/template/metrics_dashboard_template_spec.rb'
- './spec/lib/gitlab/template_parser/ast_spec.rb'
- './spec/lib/gitlab/template_parser/parser_spec.rb'
- './spec/lib/gitlab/terraform_registry_token_spec.rb'
@@ -7104,7 +6870,6 @@
- './spec/lib/gitlab/verify/job_artifacts_spec.rb'
- './spec/lib/gitlab/verify/lfs_objects_spec.rb'
- './spec/lib/gitlab/verify/uploads_spec.rb'
-- './spec/lib/gitlab/version_info_spec.rb'
- './spec/lib/gitlab/view/presenter/base_spec.rb'
- './spec/lib/gitlab/view/presenter/delegated_spec.rb'
- './spec/lib/gitlab/view/presenter/factory_spec.rb'
@@ -7164,9 +6929,7 @@
- './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/peek/views/rugged_spec.rb'
- './spec/lib/product_analytics/event_params_spec.rb'
-- './spec/lib/product_analytics/tracker_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'
@@ -7254,75 +7017,7 @@
- './spec/mailers/notify_spec.rb'
- './spec/mailers/repository_check_mailer_spec.rb'
- './spec/metrics_server/metrics_server_spec.rb'
-- './spec/migrations/20220315171129_cleanup_draft_data_from_faulty_regex_spec.rb'
-- './spec/migrations/20220316202640_populate_container_repositories_migration_plan_spec.rb'
-- './spec/migrations/20220321234317_remove_all_issuable_escalation_statuses_spec.rb'
-- './spec/migrations/20220322132242_update_pages_onboarding_state_spec.rb'
-- './spec/migrations/20220324032250_migrate_shimo_confluence_service_category_spec.rb'
-- './spec/migrations/20220324165436_schedule_backfill_project_settings_spec.rb'
-- './spec/migrations/20220329175119_remove_leftover_ci_job_artifact_deletions_spec.rb'
-- './spec/migrations/20220331133802_schedule_backfill_topics_title_spec.rb'
-- './spec/migrations/20220412143552_consume_remaining_encrypt_integration_property_jobs_spec.rb'
-- './spec/migrations/20220416054011_schedule_backfill_project_member_namespace_id_spec.rb'
-- './spec/migrations/20220420135946_update_batched_background_migration_arguments_spec.rb'
-- './spec/migrations/20220426185933_backfill_deployments_finished_at_spec.rb'
-- './spec/migrations/20220502015011_clean_up_fix_merge_request_diff_commit_users_spec.rb'
-- './spec/migrations/20220502173045_reset_too_many_tags_skipped_registry_imports_spec.rb'
-- './spec/migrations/20220503035221_add_gitlab_schema_to_batched_background_migrations_spec.rb'
-- './spec/migrations/20220505044348_fix_automatic_iterations_cadences_start_date_spec.rb'
-- './spec/migrations/20220505174658_update_index_on_alerts_to_exclude_null_fingerprints_spec.rb'
-- './spec/migrations/20220506154054_create_sync_namespace_details_trigger_spec.rb'
-- './spec/migrations/20220512190659_remove_web_hooks_web_hook_logs_web_hook_id_fk_spec.rb'
-- './spec/migrations/20220513043344_reschedule_expire_o_auth_tokens_spec.rb'
-- './spec/migrations/20220523171107_drop_deploy_tokens_token_column_spec.rb'
-- './spec/migrations/20220524074947_finalize_backfill_null_note_discussion_ids_spec.rb'
-- './spec/migrations/20220524184149_create_sync_project_namespace_details_trigger_spec.rb'
-- './spec/migrations/20220525221133_schedule_backfill_vulnerability_reads_cluster_agent_spec.rb'
-- './spec/migrations/20220601110011_schedule_remove_self_managed_wiki_notes_spec.rb'
-- './spec/migrations/20220601152916_add_user_id_and_ip_address_success_index_to_authentication_events_spec.rb'
-- './spec/migrations/20220606082910_add_tmp_index_for_potentially_misassociated_vulnerability_occurrences_spec.rb'
-- './spec/migrations/20220607082910_add_sync_tmp_index_for_potentially_misassociated_vulnerability_occurrences_spec.rb'
-- './spec/migrations/20220620132300_update_last_run_date_for_iterations_cadences_spec.rb'
-- './spec/migrations/20220622080547_backfill_project_statistics_with_container_registry_size_spec.rb'
-- './spec/migrations/20220627090231_schedule_disable_legacy_open_source_license_for_inactive_public_projects_spec.rb'
-- './spec/migrations/20220627152642_queue_update_delayed_project_removal_to_null_for_user_namespace_spec.rb'
-- './spec/migrations/20220628012902_finalise_project_namespace_members_spec.rb'
-- './spec/migrations/20220715163254_update_notes_in_past_spec.rb'
-- './spec/migrations/20220721031446_schedule_disable_legacy_open_source_license_for_one_member_no_repo_projects_spec.rb'
-- './spec/migrations/20220722084543_schedule_disable_legacy_open_source_license_for_no_issues_no_repo_projects_spec.rb'
-- './spec/migrations/20220722110026_reschedule_set_legacy_open_source_license_available_for_non_public_projects_spec.rb'
-- './spec/migrations/20220725150127_update_jira_tracker_data_deployment_type_based_on_url_spec.rb'
-- './spec/migrations/20220801155858_schedule_disable_legacy_open_source_licence_for_recent_public_projects_spec.rb'
-- './spec/migrations/20220802114351_reschedule_backfill_container_registry_size_into_project_statistics_spec.rb'
-- './spec/migrations/20220802204737_remove_deactivated_user_highest_role_stats_spec.rb'
- './spec/migrations/active_record/schema_spec.rb'
-- './spec/migrations/add_epics_relative_position_spec.rb'
-- './spec/migrations/add_web_hook_calls_to_plan_limits_paid_tiers_spec.rb'
-- './spec/migrations/backfill_integrations_enable_ssl_verification_spec.rb'
-- './spec/migrations/backfill_namespace_id_for_project_routes_spec.rb'
-- './spec/migrations/backfill_project_import_level_spec.rb'
-- './spec/migrations/bulk_insert_cluster_enabled_grants_spec.rb'
-- './spec/migrations/change_public_projects_cost_factor_spec.rb'
-- './spec/migrations/cleanup_after_fixing_issue_when_admin_changed_primary_email_spec.rb'
-- './spec/migrations/cleanup_after_fixing_regression_with_new_users_emails_spec.rb'
-- './spec/migrations/cleanup_backfill_integrations_enable_ssl_verification_spec.rb'
-- './spec/migrations/cleanup_mr_attention_request_todos_spec.rb'
-- './spec/migrations/cleanup_orphaned_routes_spec.rb'
-- './spec/migrations/finalize_orphaned_routes_cleanup_spec.rb'
-- './spec/migrations/finalize_project_namespaces_backfill_spec.rb'
-- './spec/migrations/finalize_routes_backfilling_for_projects_spec.rb'
-- './spec/migrations/fix_and_backfill_project_namespaces_for_projects_with_duplicate_name_spec.rb'
-- './spec/migrations/populate_operation_visibility_permissions_spec.rb'
-- './spec/migrations/queue_backfill_project_feature_package_registry_access_level_spec.rb'
-- './spec/migrations/remove_invalid_integrations_spec.rb'
-- './spec/migrations/remove_wiki_notes_spec.rb'
-- './spec/migrations/reschedule_backfill_imported_issue_search_data_spec.rb'
-- './spec/migrations/schedule_backfill_draft_status_on_merge_requests_corrected_regex_spec.rb'
-- './spec/migrations/schedule_backfilling_the_namespace_id_for_vulnerability_reads_spec.rb'
-- './spec/migrations/schedule_populate_requirements_issue_id_spec.rb'
-- './spec/migrations/schedule_purging_stale_security_scans_spec.rb'
-- './spec/migrations/schedule_set_correct_vulnerability_state_spec.rb'
-- './spec/migrations/toggle_vsa_aggregations_enable_spec.rb'
- './spec/models/ability_spec.rb'
- './spec/models/abuse_report_spec.rb'
- './spec/models/active_session_spec.rb'
@@ -7361,7 +7056,6 @@
- './spec/models/blob_viewer/go_mod_spec.rb'
- './spec/models/blob_viewer/license_spec.rb'
- './spec/models/blob_viewer/markup_spec.rb'
-- './spec/models/blob_viewer/metrics_dashboard_yml_spec.rb'
- './spec/models/blob_viewer/package_json_spec.rb'
- './spec/models/blob_viewer/podspec_json_spec.rb'
- './spec/models/blob_viewer/podspec_spec.rb'
@@ -7530,7 +7224,6 @@
- './spec/models/concerns/project_api_compatibility_spec.rb'
- './spec/models/concerns/project_features_compatibility_spec.rb'
- './spec/models/concerns/prometheus_adapter_spec.rb'
-- './spec/models/concerns/protected_ref_access_spec.rb'
- './spec/models/concerns/reactive_caching_spec.rb'
- './spec/models/concerns/redactable_spec.rb'
- './spec/models/concerns/redis_cacheable_spec.rb'
@@ -7615,7 +7308,6 @@
- './spec/models/event_spec.rb'
- './spec/models/exported_protected_branch_spec.rb'
- './spec/models/external_issue_spec.rb'
-- './spec/models/external_pull_request_spec.rb'
- './spec/models/fork_network_member_spec.rb'
- './spec/models/fork_network_spec.rb'
- './spec/models/generic_commit_status_spec.rb'
@@ -7745,8 +7437,6 @@
- './spec/models/merge_request/metrics_spec.rb'
- './spec/models/merge_request_reviewer_spec.rb'
- './spec/models/merge_request_spec.rb'
-- './spec/models/metrics/dashboard/annotation_spec.rb'
-- './spec/models/metrics/users_starred_dashboard_spec.rb'
- './spec/models/milestone_note_spec.rb'
- './spec/models/milestone_release_spec.rb'
- './spec/models/milestone_spec.rb'
@@ -7818,10 +7508,6 @@
- './spec/models/pages_domain_spec.rb'
- './spec/models/pages/lookup_path_spec.rb'
- './spec/models/pages/virtual_domain_spec.rb'
-- './spec/models/performance_monitoring/prometheus_dashboard_spec.rb'
-- './spec/models/performance_monitoring/prometheus_metric_spec.rb'
-- './spec/models/performance_monitoring/prometheus_panel_group_spec.rb'
-- './spec/models/performance_monitoring/prometheus_panel_spec.rb'
- './spec/models/personal_access_token_spec.rb'
- './spec/models/personal_snippet_spec.rb'
- './spec/models/plan_limits_spec.rb'
@@ -7847,13 +7533,10 @@
- './spec/models/project_deploy_token_spec.rb'
- './spec/models/project_export_job_spec.rb'
- './spec/models/project_feature_spec.rb'
-- './spec/models/project_feature_usage_spec.rb'
- './spec/models/project_group_link_spec.rb'
- './spec/models/project_import_data_spec.rb'
- './spec/models/project_import_state_spec.rb'
- './spec/models/project_label_spec.rb'
-- './spec/models/project_metrics_setting_spec.rb'
-- './spec/models/project_pages_metadatum_spec.rb'
- './spec/models/project_repository_spec.rb'
- './spec/models/projects/build_artifacts_size_refresh_spec.rb'
- './spec/models/projects/ci_feature_usage_spec.rb'
@@ -7922,7 +7605,6 @@
- './spec/models/token_with_iv_spec.rb'
- './spec/models/tree_spec.rb'
- './spec/models/trending_project_spec.rb'
-- './spec/models/u2f_registration_spec.rb'
- './spec/models/uploads/fog_spec.rb'
- './spec/models/uploads/local_spec.rb'
- './spec/models/upload_spec.rb'
@@ -8005,7 +7687,6 @@
- './spec/policies/issuable_policy_spec.rb'
- './spec/policies/issue_policy_spec.rb'
- './spec/policies/merge_request_policy_spec.rb'
-- './spec/policies/metrics/dashboard/annotation_policy_spec.rb'
- './spec/policies/namespace/root_storage_statistics_policy_spec.rb'
- './spec/policies/namespaces/project_namespace_policy_spec.rb'
- './spec/policies/namespaces/user_namespace_policy_spec.rb'
@@ -8064,7 +7745,6 @@
- './spec/presenters/packages/conan/package_presenter_spec.rb'
- './spec/presenters/packages/detail/package_presenter_spec.rb'
- './spec/presenters/packages/helm/index_presenter_spec.rb'
-- './spec/presenters/packages/npm/package_presenter_spec.rb'
- './spec/presenters/packages/nuget/package_metadata_presenter_spec.rb'
- './spec/presenters/packages/nuget/packages_metadata_presenter_spec.rb'
- './spec/presenters/packages/nuget/packages_versions_presenter_spec.rb'
@@ -8214,8 +7894,6 @@
- './spec/requests/api/graphql/issue_status_counts_spec.rb'
- './spec/requests/api/graphql/merge_request/merge_request_spec.rb'
- './spec/requests/api/graphql/metadata_query_spec.rb'
-- './spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb'
-- './spec/requests/api/graphql/metrics/dashboard_query_spec.rb'
- './spec/requests/api/graphql/milestone_spec.rb'
- './spec/requests/api/graphql/multiplexed_queries_spec.rb'
- './spec/requests/api/graphql/mutations/admin/sidekiq_queues/delete_jobs_spec.rb'
@@ -8342,7 +8020,6 @@
- './spec/requests/api/graphql/packages/pypi_spec.rb'
- './spec/requests/api/graphql/project/alert_management/alert/assignees_spec.rb'
- './spec/requests/api/graphql/project/alert_management/alert/issue_spec.rb'
-- './spec/requests/api/graphql/project/alert_management/alert/metrics_dashboard_url_spec.rb'
- './spec/requests/api/graphql/project/alert_management/alert/notes_spec.rb'
- './spec/requests/api/graphql/project/alert_management/alerts_spec.rb'
- './spec/requests/api/graphql/project/alert_management/alert_status_counts_spec.rb'
@@ -8520,7 +8197,6 @@
- './spec/requests/api/user_counts_spec.rb'
- './spec/requests/api/users_preferences_spec.rb'
- './spec/requests/api/users_spec.rb'
-- './spec/requests/api/v3/github_spec.rb'
- './spec/requests/api/wikis_spec.rb'
- './spec/requests/concerns/planning_hierarchy_spec.rb'
- './spec/requests/content_security_policy_spec.rb'
@@ -8544,7 +8220,6 @@
- './spec/requests/import/gitlab_groups_controller_spec.rb'
- './spec/requests/import/gitlab_projects_controller_spec.rb'
- './spec/requests/import/url_controller_spec.rb'
-- './spec/requests/jira_authorizations_spec.rb'
- './spec/requests/jira_connect/installations_controller_spec.rb'
- './spec/requests/jira_connect/oauth_application_ids_controller_spec.rb'
- './spec/requests/jira_connect/oauth_callbacks_controller_spec.rb'
@@ -8588,9 +8263,6 @@
- './spec/requests/projects/merge_requests/diffs_spec.rb'
- './spec/requests/projects/merge_requests_discussions_spec.rb'
- './spec/requests/projects/merge_requests_spec.rb'
-- './spec/requests/projects/metrics/dashboards/builder_spec.rb'
-- './spec/requests/projects/metrics_dashboard_spec.rb'
-- './spec/requests/projects/noteable_notes_spec.rb'
- './spec/requests/projects/pipelines_controller_spec.rb'
- './spec/requests/projects/redirect_controller_spec.rb'
- './spec/requests/projects/releases_controller_spec.rb'
@@ -8786,7 +8458,6 @@
- './spec/serializers/project_mirror_serializer_spec.rb'
- './spec/serializers/project_note_entity_spec.rb'
- './spec/serializers/project_serializer_spec.rb'
-- './spec/serializers/prometheus_alert_entity_spec.rb'
- './spec/serializers/release_serializer_spec.rb'
- './spec/serializers/remote_mirror_entity_spec.rb'
- './spec/serializers/request_aware_entity_spec.rb'
@@ -8861,7 +8532,6 @@
- './spec/services/branches/validate_new_service_spec.rb'
- './spec/services/bulk_create_integration_service_spec.rb'
- './spec/services/bulk_imports/archive_extraction_service_spec.rb'
-- './spec/services/bulk_imports/create_pipeline_trackers_service_spec.rb'
- './spec/services/bulk_imports/create_service_spec.rb'
- './spec/services/bulk_imports/export_service_spec.rb'
- './spec/services/bulk_imports/file_decompression_service_spec.rb'
@@ -9028,10 +8698,8 @@
- './spec/services/dependency_proxy/request_token_service_spec.rb'
- './spec/services/deploy_keys/create_service_spec.rb'
- './spec/services/deployments/archive_in_project_service_spec.rb'
-- './spec/services/deployments/create_for_build_service_spec.rb'
- './spec/services/deployments/create_service_spec.rb'
- './spec/services/deployments/link_merge_requests_service_spec.rb'
-- './spec/services/deployments/older_deployments_drop_service_spec.rb'
- './spec/services/deployments/update_environment_service_spec.rb'
- './spec/services/deployments/update_service_spec.rb'
- './spec/services/design_management/copy_design_collection/copy_service_spec.rb'
@@ -9089,7 +8757,6 @@
- './spec/services/google_cloud/setup_cloudsql_instance_service_spec.rb'
- './spec/services/gpg_keys/create_service_spec.rb'
- './spec/services/gpg_keys/destroy_service_spec.rb'
-- './spec/services/grafana/proxy_service_spec.rb'
- './spec/services/gravatar_service_spec.rb'
- './spec/services/groups/autocomplete_service_spec.rb'
- './spec/services/groups/auto_devops_service_spec.rb'
@@ -9212,7 +8879,6 @@
- './spec/services/merge_requests/delete_non_latest_diffs_service_spec.rb'
- './spec/services/merge_requests/execute_approval_hooks_service_spec.rb'
- './spec/services/merge_requests/export_csv_service_spec.rb'
-- './spec/services/merge_requests/ff_merge_service_spec.rb'
- './spec/services/merge_requests/get_urls_service_spec.rb'
- './spec/services/merge_requests/handle_assignees_change_service_spec.rb'
- './spec/services/merge_requests/link_lfs_objects_service_spec.rb'
@@ -9246,25 +8912,6 @@
- './spec/services/merge_requests/update_assignees_service_spec.rb'
- './spec/services/merge_requests/update_reviewers_service_spec.rb'
- './spec/services/merge_requests/update_service_spec.rb'
-- './spec/services/metrics/dashboard/annotations/create_service_spec.rb'
-- './spec/services/metrics/dashboard/annotations/delete_service_spec.rb'
-- './spec/services/metrics/dashboard/clone_dashboard_service_spec.rb'
-- './spec/services/metrics/dashboard/cluster_dashboard_service_spec.rb'
-- './spec/services/metrics/dashboard/cluster_metrics_embed_service_spec.rb'
-- './spec/services/metrics/dashboard/custom_dashboard_service_spec.rb'
-- './spec/services/metrics/dashboard/custom_metric_embed_service_spec.rb'
-- './spec/services/metrics/dashboard/default_embed_service_spec.rb'
-- './spec/services/metrics/dashboard/dynamic_embed_service_spec.rb'
-- './spec/services/metrics/dashboard/gitlab_alert_embed_service_spec.rb'
-- './spec/services/metrics/dashboard/grafana_metric_embed_service_spec.rb'
-- './spec/services/metrics/dashboard/panel_preview_service_spec.rb'
-- './spec/services/metrics/dashboard/pod_dashboard_service_spec.rb'
-- './spec/services/metrics/dashboard/system_dashboard_service_spec.rb'
-- './spec/services/metrics/dashboard/transient_embed_service_spec.rb'
-- './spec/services/metrics/dashboard/update_dashboard_service_spec.rb'
-- './spec/services/metrics/sample_metrics_service_spec.rb'
-- './spec/services/metrics/users_starred_dashboards/create_service_spec.rb'
-- './spec/services/metrics/users_starred_dashboards/delete_service_spec.rb'
- './spec/services/milestones/closed_issues_count_service_spec.rb'
- './spec/services/milestones/close_service_spec.rb'
- './spec/services/milestones/create_service_spec.rb'
@@ -9310,11 +8957,9 @@
- './spec/services/packages/debian/extract_deb_metadata_service_spec.rb'
- './spec/services/packages/debian/extract_metadata_service_spec.rb'
- './spec/services/packages/debian/find_or_create_incoming_service_spec.rb'
-- './spec/services/packages/debian/find_or_create_package_service_spec.rb'
- './spec/services/packages/debian/generate_distribution_key_service_spec.rb'
- './spec/services/packages/debian/generate_distribution_service_spec.rb'
- './spec/services/packages/debian/parse_debian822_service_spec.rb'
-- './spec/services/packages/debian/process_changes_service_spec.rb'
- './spec/services/packages/debian/sign_distribution_service_spec.rb'
- './spec/services/packages/debian/update_distribution_service_spec.rb'
- './spec/services/packages/generic/create_package_file_service_spec.rb'
@@ -9353,7 +8998,6 @@
- './spec/services/pages_domains/create_acme_order_service_spec.rb'
- './spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb'
- './spec/services/pages_domains/retry_acme_order_service_spec.rb'
-- './spec/services/pages/zip_directory_service_spec.rb'
- './spec/services/personal_access_tokens/create_service_spec.rb'
- './spec/services/personal_access_tokens/last_used_service_spec.rb'
- './spec/services/personal_access_tokens/revoke_service_spec.rb'
@@ -9395,7 +9039,6 @@
- './spec/services/projects/group_links/update_service_spec.rb'
- './spec/services/projects/hashed_storage/base_attachment_service_spec.rb'
- './spec/services/projects/hashed_storage/migrate_attachments_service_spec.rb'
-- './spec/services/projects/hashed_storage/migrate_repository_service_spec.rb'
- './spec/services/projects/hashed_storage/migration_service_spec.rb'
- './spec/services/projects/import_error_filter_spec.rb'
- './spec/services/projects/import_export/export_service_spec.rb'
@@ -9435,8 +9078,6 @@
- './spec/services/projects/update_repository_storage_service_spec.rb'
- './spec/services/projects/update_service_spec.rb'
- './spec/services/projects/update_statistics_service_spec.rb'
-- './spec/services/prometheus/proxy_service_spec.rb'
-- './spec/services/prometheus/proxy_variable_substitution_service_spec.rb'
- './spec/services/protected_branches/cache_service_spec.rb'
- './spec/services/protected_branches/create_service_spec.rb'
- './spec/services/protected_branches/destroy_service_spec.rb'
@@ -9560,7 +9201,6 @@
- './spec/services/users/saved_replies/destroy_service_spec.rb'
- './spec/services/users/saved_replies/update_service_spec.rb'
- './spec/services/users/set_status_service_spec.rb'
-- './spec/services/users/signup_service_spec.rb'
- './spec/services/users/unban_service_spec.rb'
- './spec/services/users/update_canonical_email_service_spec.rb'
- './spec/services/users/update_highest_member_role_service_spec.rb'
@@ -9614,8 +9254,6 @@
- './spec/support_specs/matchers/be_sorted_spec.rb'
- './spec/support_specs/matchers/exceed_query_limit_helpers_spec.rb'
- './spec/tasks/admin_mode_spec.rb'
-- './spec/tasks/cache/clear/redis_spec.rb'
-- './spec/tasks/config_lint_spec.rb'
- './spec/tasks/dev_rake_spec.rb'
- './spec/tasks/gitlab/artifacts/check_rake_spec.rb'
- './spec/tasks/gitlab/artifacts/migrate_rake_spec.rb'
@@ -9630,13 +9268,11 @@
- './spec/tasks/gitlab/db/validate_config_rake_spec.rb'
- './spec/tasks/gitlab/dependency_proxy/migrate_rake_spec.rb'
- './spec/tasks/gitlab/external_diffs_rake_spec.rb'
-- './spec/tasks/gitlab/generate_sample_prometheus_data_spec.rb'
- './spec/tasks/gitlab/gitaly_rake_spec.rb'
- './spec/tasks/gitlab/git_rake_spec.rb'
- './spec/tasks/gitlab/ldap_rake_spec.rb'
- './spec/tasks/gitlab/lfs/check_rake_spec.rb'
- './spec/tasks/gitlab/lfs/migrate_rake_spec.rb'
-- './spec/tasks/gitlab/packages/events_rake_spec.rb'
- './spec/tasks/gitlab/packages/migrate_rake_spec.rb'
- './spec/tasks/gitlab/pages_rake_spec.rb'
- './spec/tasks/gitlab/password_rake_spec.rb'
@@ -9648,7 +9284,6 @@
- './spec/tasks/gitlab/sidekiq_rake_spec.rb'
- './spec/tasks/gitlab/smtp_rake_spec.rb'
- './spec/tasks/gitlab/snippets_rake_spec.rb'
-- './spec/tasks/gitlab/task_helpers_spec.rb'
- './spec/tasks/gitlab/terraform/migrate_rake_spec.rb'
- './spec/tasks/gitlab/update_templates_rake_spec.rb'
- './spec/tasks/gitlab/uploads/check_rake_spec.rb'
@@ -9660,14 +9295,6 @@
- './spec/tasks/gitlab/x509/update_rake_spec.rb'
- './spec/tasks/migrate/schema_check_rake_spec.rb'
- './spec/tasks/rubocop_rake_spec.rb'
-- './spec/tasks/tokens_spec.rb'
-- './spec/tooling/danger/customer_success_spec.rb'
-- './spec/tooling/danger/datateam_spec.rb'
-- './spec/tooling/danger/feature_flag_spec.rb'
-- './spec/tooling/danger/analytics_instrumentation_spec.rb'
-- './spec/tooling/danger/project_helper_spec.rb'
-- './spec/tooling/danger/sidekiq_queues_spec.rb'
-- './spec/tooling/danger/specs_spec.rb'
- './spec/tooling/docs/deprecation_handling_spec.rb'
- './spec/tooling/graphql/docs/renderer_spec.rb'
- './spec/tooling/lib/tooling/crystalball/coverage_lines_execution_detector_spec.rb'
@@ -9817,7 +9444,6 @@
- './spec/views/projects/hooks/edit.html.haml_spec.rb'
- './spec/views/projects/hooks/index.html.haml_spec.rb'
- './spec/views/projects/imports/new.html.haml_spec.rb'
-- './spec/views/projects/issues/_issue.html.haml_spec.rb'
- './spec/views/projects/issues/_related_branches.html.haml_spec.rb'
- './spec/views/projects/issues/_service_desk_info_content.html.haml_spec.rb'
- './spec/views/projects/issues/show.html.haml_spec.rb'
@@ -9832,7 +9458,6 @@
- './spec/views/projects/pages_domains/show.html.haml_spec.rb'
- './spec/views/projects/pages/new.html.haml_spec.rb'
- './spec/views/projects/pages/show.html.haml_spec.rb'
-- './spec/views/projects/pipeline_schedules/_pipeline_schedule.html.haml_spec.rb'
- './spec/views/projects/pipelines/show.html.haml_spec.rb'
- './spec/views/projects/project_members/index.html.haml_spec.rb'
- './spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb'
@@ -9840,7 +9465,6 @@
- './spec/views/projects/settings/operations/show.html.haml_spec.rb'
- './spec/views/projects/tags/index.html.haml_spec.rb'
- './spec/views/projects/tree/show.html.haml_spec.rb'
-- './spec/views/registrations/welcome/show.html.haml_spec.rb'
- './spec/views/search/_results.html.haml_spec.rb'
- './spec/views/search/show.html.haml_spec.rb'
- './spec/views/shared/groups/_dropdown.html.haml_spec.rb'
@@ -9851,7 +9475,6 @@
- './spec/views/shared/_milestones_sort_dropdown.html.haml_spec.rb'
- './spec/views/shared/milestones/_top.html.haml_spec.rb'
- './spec/views/shared/nav/_sidebar.html.haml_spec.rb'
-- './spec/views/shared/notes/_form.html.haml_spec.rb'
- './spec/views/shared/projects/_inactive_project_deletion_alert.html.haml_spec.rb'
- './spec/views/shared/projects/_list.html.haml_spec.rb'
- './spec/views/shared/projects/_project.html.haml_spec.rb'
@@ -9875,7 +9498,6 @@
- './spec/workers/background_migration_worker_spec.rb'
- './spec/workers/build_hooks_worker_spec.rb'
- './spec/workers/build_queue_worker_spec.rb'
-- './spec/workers/build_success_worker_spec.rb'
- './spec/workers/bulk_imports/entity_worker_spec.rb'
- './spec/workers/bulk_imports/export_request_worker_spec.rb'
- './spec/workers/bulk_imports/pipeline_worker_spec.rb'
@@ -9924,21 +9546,18 @@
- './spec/workers/clusters/applications/deactivate_integration_worker_spec.rb'
- './spec/workers/clusters/cleanup/project_namespace_worker_spec.rb'
- './spec/workers/clusters/cleanup/service_account_worker_spec.rb'
-- './spec/workers/clusters/integrations/check_prometheus_health_worker_spec.rb'
- './spec/workers/concerns/application_worker_spec.rb'
- './spec/workers/concerns/cluster_agent_queue_spec.rb'
- './spec/workers/concerns/cronjob_queue_spec.rb'
- './spec/workers/concerns/gitlab/github_import/object_importer_spec.rb'
- './spec/workers/concerns/gitlab/github_import/rescheduling_methods_spec.rb'
- './spec/workers/concerns/gitlab/github_import/stage_methods_spec.rb'
-- './spec/workers/concerns/gitlab/notify_upon_death_spec.rb'
- './spec/workers/concerns/limited_capacity/job_tracker_spec.rb'
- './spec/workers/concerns/limited_capacity/worker_spec.rb'
- './spec/workers/concerns/packages/cleanup_artifact_worker_spec.rb'
- './spec/workers/concerns/project_import_options_spec.rb'
- './spec/workers/concerns/reenqueuer_spec.rb'
- './spec/workers/concerns/repository_check_queue_spec.rb'
-- './spec/workers/concerns/waitable_worker_spec.rb'
- './spec/workers/concerns/worker_attributes_spec.rb'
- './spec/workers/concerns/worker_context_spec.rb'
- './spec/workers/container_expiration_policies/cleanup_container_repository_worker_spec.rb'
@@ -9955,7 +9574,6 @@
- './spec/workers/database/ci_project_mirrors_consistency_check_worker_spec.rb'
- './spec/workers/database/drop_detached_partitions_worker_spec.rb'
- './spec/workers/database/partition_management_worker_spec.rb'
-- './spec/workers/delete_container_repository_worker_spec.rb'
- './spec/workers/delete_diff_files_worker_spec.rb'
- './spec/workers/delete_merged_branches_worker_spec.rb'
- './spec/workers/delete_user_worker_spec.rb'
@@ -9991,8 +9609,6 @@
- './spec/workers/gitlab/github_import/import_issue_event_worker_spec.rb'
- './spec/workers/gitlab/github_import/import_issue_worker_spec.rb'
- './spec/workers/gitlab/github_import/import_note_worker_spec.rb'
-- './spec/workers/gitlab/github_import/import_pull_request_merged_by_worker_spec.rb'
-- './spec/workers/gitlab/github_import/import_pull_request_review_worker_spec.rb'
- './spec/workers/gitlab/github_import/import_pull_request_worker_spec.rb'
- './spec/workers/gitlab/github_import/refresh_import_jid_worker_spec.rb'
- './spec/workers/gitlab/github_import/stage/finish_import_worker_spec.rb'
@@ -10017,7 +9633,6 @@
- './spec/workers/gitlab/jira_import/stuck_jira_import_jobs_worker_spec.rb'
- './spec/workers/gitlab_performance_bar_stats_worker_spec.rb'
- './spec/workers/gitlab_service_ping_worker_spec.rb'
-- './spec/workers/gitlab_shell_worker_spec.rb'
- './spec/workers/google_cloud/create_cloudsql_instance_worker_spec.rb'
- './spec/workers/group_destroy_worker_spec.rb'
- './spec/workers/group_export_worker_spec.rb'
@@ -10061,9 +9676,6 @@
- './spec/workers/merge_requests/resolve_todos_worker_spec.rb'
- './spec/workers/merge_requests/update_head_pipeline_worker_spec.rb'
- './spec/workers/merge_worker_spec.rb'
-- './spec/workers/metrics/dashboard/prune_old_annotations_worker_spec.rb'
-- './spec/workers/metrics/dashboard/schedule_annotations_prune_worker_spec.rb'
-- './spec/workers/metrics/dashboard/sync_dashboards_worker_spec.rb'
- './spec/workers/migrate_external_diffs_worker_spec.rb'
- './spec/workers/namespaces/process_sync_events_worker_spec.rb'
- './spec/workers/namespaces/prune_aggregation_schedules_worker_spec.rb'
@@ -10082,7 +9694,6 @@
- './spec/workers/packages/composer/cache_cleanup_worker_spec.rb'
- './spec/workers/packages/composer/cache_update_worker_spec.rb'
- './spec/workers/packages/debian/generate_distribution_worker_spec.rb'
-- './spec/workers/packages/debian/process_changes_worker_spec.rb'
- './spec/workers/packages/go/sync_packages_worker_spec.rb'
- './spec/workers/packages/helm/extraction_worker_spec.rb'
- './spec/workers/packages/mark_package_files_for_destruction_worker_spec.rb'
@@ -10094,7 +9705,6 @@
- './spec/workers/pages_domain_ssl_renewal_worker_spec.rb'
- './spec/workers/pages_domain_verification_cron_worker_spec.rb'
- './spec/workers/pages_domain_verification_worker_spec.rb'
-- './spec/workers/pages/invalidate_domain_cache_worker_spec.rb'
- './spec/workers/pages_worker_spec.rb'
- './spec/workers/partition_creation_worker_spec.rb'
- './spec/workers/personal_access_tokens/expired_notification_worker_spec.rb'
diff --git a/spec/support/shared_contexts/ci/catalog/resources/version_shared_context.rb b/spec/support/shared_contexts/ci/catalog/resources/version_shared_context.rb
new file mode 100644
index 00000000000..3c9bb980b46
--- /dev/null
+++ b/spec/support/shared_contexts/ci/catalog/resources/version_shared_context.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+RSpec.shared_context 'when there are catalog resources with versions' do
+ let_it_be(:current_user) { create(:user) }
+
+ let_it_be(:project1) { create(:project, :repository) }
+ let_it_be(:project2) { create(:project, :repository) }
+ let_it_be(:project3) { create(:project, :repository) }
+ let_it_be_with_reload(:resource1) { create(:ci_catalog_resource, project: project1) }
+ let_it_be_with_reload(:resource2) { create(:ci_catalog_resource, project: project2) }
+ let_it_be(:resource3) { create(:ci_catalog_resource, project: project3) }
+
+ let_it_be(:release_v1_0) { create(:release, project: project1, tag: 'v1.0', released_at: 4.days.ago) }
+ let_it_be(:release_v1_1) { create(:release, project: project1, tag: 'v1.1', released_at: 3.days.ago) }
+ let_it_be(:release_v2_0) { create(:release, project: project2, tag: 'v2.0', released_at: 2.days.ago) }
+ let_it_be(:release_v2_1) { create(:release, project: project2, tag: 'v2.1', released_at: 1.day.ago) }
+
+ let_it_be(:v1_0) do
+ create(:ci_catalog_resource_version, catalog_resource: resource1, release: release_v1_0, created_at: 1.day.ago)
+ end
+
+ let_it_be(:v1_1) do
+ create(:ci_catalog_resource_version, catalog_resource: resource1, release: release_v1_1, created_at: 2.days.ago)
+ end
+
+ let_it_be(:v2_0) do
+ create(:ci_catalog_resource_version, catalog_resource: resource2, release: release_v2_0, created_at: 3.days.ago)
+ end
+
+ let_it_be(:v2_1) do
+ create(:ci_catalog_resource_version, catalog_resource: resource2, release: release_v2_1, created_at: 4.days.ago)
+ end
+end
diff --git a/spec/support/shared_contexts/controllers/ambiguous_ref_controller_shared_context.rb b/spec/support/shared_contexts/controllers/ambiguous_ref_controller_shared_context.rb
new file mode 100644
index 00000000000..8ad7edee1a1
--- /dev/null
+++ b/spec/support/shared_contexts/controllers/ambiguous_ref_controller_shared_context.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+RSpec.shared_context 'with ambiguous refs for controllers' do
+ let(:ambiguous_ref_modal) { false }
+
+ before do
+ expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original # rubocop:disable RSpec/ExpectInHook
+ project.repository.add_tag(project.creator, 'ambiguous_ref', RepoHelpers.sample_commit.id)
+ project.repository.add_branch(project.creator, 'ambiguous_ref', RepoHelpers.another_sample_commit.id)
+
+ stub_feature_flags(redirect_with_ref_type: redirect_with_ref_type)
+ stub_feature_flags(ambiguous_ref_modal: ambiguous_ref_modal)
+ end
+
+ after do
+ project.repository.rm_tag(project.creator, 'ambiguous_ref')
+ project.repository.rm_branch(project.creator, 'ambiguous_ref')
+ end
+end
diff --git a/spec/support/shared_contexts/features/integrations/project_integrations_shared_context.rb b/spec/support/shared_contexts/features/integrations/project_integrations_shared_context.rb
index c3da9435e05..743a7cd26e0 100644
--- a/spec/support/shared_contexts/features/integrations/project_integrations_shared_context.rb
+++ b/spec/support/shared_contexts/features/integrations/project_integrations_shared_context.rb
@@ -4,7 +4,7 @@ RSpec.shared_context 'project integration activation' do
include_context 'with integration activation'
let_it_be(:project) { create(:project) }
- let_it_be(:user) { create(:user, :no_super_sidebar) }
+ let_it_be(:user) { create(:user) }
before do
project.add_maintainer(user)
diff --git a/spec/support/shared_contexts/graphql/resolvers/runners_resolver_shared_context.rb b/spec/support/shared_contexts/graphql/resolvers/runners_resolver_shared_context.rb
index 1480b5f98e7..2dbb903a272 100644
--- a/spec/support/shared_contexts/graphql/resolvers/runners_resolver_shared_context.rb
+++ b/spec/support/shared_contexts/graphql/resolvers/runners_resolver_shared_context.rb
@@ -9,14 +9,14 @@ RSpec.shared_context 'runners resolver setup' do
let_it_be(:project) { create(:project, :public, group: group) }
let_it_be(:inactive_project_runner) do
- create(:ci_runner, :project, projects: [project], description: 'inactive project runner', token: 'abcdef', active: false, contacted_at: 1.minute.ago, tag_list: %w(project_runner))
+ create(:ci_runner, :project, projects: [project], description: 'inactive project runner', token: 'abcdef', active: false, contacted_at: 1.minute.ago, tag_list: %w[project_runner])
end
let_it_be(:offline_project_runner) do
- create(:ci_runner, :project, projects: [project], description: 'offline project runner', token: 'defghi', contacted_at: 1.day.ago, tag_list: %w(project_runner active_runner))
+ create(:ci_runner, :project, projects: [project], description: 'offline project runner', token: 'defghi', contacted_at: 1.day.ago, tag_list: %w[project_runner active_runner])
end
let_it_be(:group_runner) { create(:ci_runner, :group, groups: [group], token: 'mnopqr', description: 'group runner', contacted_at: 2.seconds.ago) }
let_it_be(:subgroup_runner) { create(:ci_runner, :group, groups: [subgroup], token: '123456', description: 'subgroup runner', contacted_at: 1.second.ago) }
- let_it_be(:instance_runner) { create(:ci_runner, :instance, description: 'shared runner', token: 'stuvxz', contacted_at: 2.minutes.ago, tag_list: %w(instance_runner active_runner)) }
+ let_it_be(:instance_runner) { create(:ci_runner, :instance, description: 'shared runner', token: 'stuvxz', contacted_at: 2.minutes.ago, tag_list: %w[instance_runner active_runner]) }
end
diff --git a/spec/support/shared_contexts/graphql/types/query_type_shared_context.rb b/spec/support/shared_contexts/graphql/types/query_type_shared_context.rb
index 434592ccd38..257ccc553fe 100644
--- a/spec/support/shared_contexts/graphql/types/query_type_shared_context.rb
+++ b/spec/support/shared_contexts/graphql/types/query_type_shared_context.rb
@@ -13,6 +13,8 @@ RSpec.shared_context 'with FOSS query type fields' do
:current_user,
:design_management,
:echo,
+ :frecent_groups,
+ :frecent_projects,
:gitpod_enabled,
:group,
:groups,
diff --git a/spec/support/shared_contexts/lib/gitlab/sidekiq_middleware/server_metrics_shared_context.rb b/spec/support/shared_contexts/lib/gitlab/sidekiq_middleware/server_metrics_shared_context.rb
index d9b2b44980c..85ee3ed4183 100644
--- a/spec/support/shared_contexts/lib/gitlab/sidekiq_middleware/server_metrics_shared_context.rb
+++ b/spec/support/shared_contexts/lib/gitlab/sidekiq_middleware/server_metrics_shared_context.rb
@@ -19,6 +19,12 @@ RSpec.shared_context 'server metrics with mocked prometheus' do
let(:load_balancing_metric) { double('load balancing metric') }
let(:sidekiq_mem_total_bytes) { double('sidekiq mem total bytes') }
let(:completion_seconds_sum_metric) { double('sidekiq completion seconds sum metric') }
+ let(:completion_count_metric) { double('sidekiq completion seconds count metric') }
+ let(:cpu_seconds_sum_metric) { double('cpu seconds sum metric') }
+ let(:db_seconds_sum_metric) { double('db seconds sum metric') }
+ let(:gitaly_seconds_sum_metric) { double('gitaly seconds sum metric') }
+ let(:redis_seconds_sum_metric) { double('redis seconds sum metric') }
+ let(:elasticsearch_seconds_sum_metric) { double('elasticsearch seconds sum metric') }
before do
allow(Gitlab::Metrics).to receive(:histogram).and_call_original
@@ -38,6 +44,12 @@ RSpec.shared_context 'server metrics with mocked prometheus' do
allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_elasticsearch_requests_total, anything).and_return(elasticsearch_requests_total)
allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_load_balancing_count, anything).and_return(load_balancing_metric)
allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_jobs_completion_seconds_sum, anything).and_return(completion_seconds_sum_metric)
+ allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_jobs_completion_count, anything).and_return(completion_count_metric)
+ allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_jobs_cpu_seconds_sum, anything).and_return(cpu_seconds_sum_metric)
+ allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_jobs_db_seconds_sum, anything).and_return(db_seconds_sum_metric)
+ allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_jobs_gitaly_seconds_sum, anything).and_return(gitaly_seconds_sum_metric)
+ allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_redis_requests_duration_seconds_sum, anything).and_return(redis_seconds_sum_metric)
+ allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_elasticsearch_requests_duration_seconds_sum, anything).and_return(elasticsearch_seconds_sum_metric)
allow(Gitlab::Metrics).to receive(:gauge).with(:sidekiq_running_jobs, anything, {}, :all).and_return(running_jobs_metric)
allow(Gitlab::Metrics).to receive(:gauge).with(:sidekiq_concurrency, anything, {}, :all).and_return(concurrency_metric)
allow(Gitlab::Metrics).to receive(:gauge).with(:sidekiq_mem_total_bytes, anything, {}, :all).and_return(sidekiq_mem_total_bytes)
@@ -78,12 +90,8 @@ RSpec.shared_context 'server metrics call' do
}
end
- let(:stub_subject) { true }
-
before do
- if stub_subject
- allow(subject).to receive(:get_thread_cputime).and_return(thread_cputime_before, thread_cputime_after)
- end
+ allow(subject).to receive(:get_thread_cputime).and_return(thread_cputime_before, thread_cputime_after)
allow(Gitlab::Metrics::System).to receive(:monotonic_time).and_return(monotonic_time_before, monotonic_time_after)
allow(Gitlab::InstrumentationHelper).to receive(:queue_duration_for_job).with(job).and_return(queue_duration_for_job)
@@ -101,9 +109,16 @@ RSpec.shared_context 'server metrics call' do
allow(redis_requests_total).to receive(:increment)
allow(elasticsearch_requests_total).to receive(:increment)
allow(completion_seconds_sum_metric).to receive(:increment)
+ allow(completion_count_metric).to receive(:increment)
+ allow(cpu_seconds_sum_metric).to receive(:increment)
+ allow(db_seconds_sum_metric).to receive(:increment)
+ allow(gitaly_seconds_sum_metric).to receive(:increment)
+ allow(redis_seconds_sum_metric).to receive(:increment)
+ allow(elasticsearch_seconds_sum_metric).to receive(:increment)
allow(queue_duration_seconds).to receive(:observe)
allow(user_execution_seconds_metric).to receive(:observe)
allow(db_seconds_metric).to receive(:observe)
+ allow(db_seconds_sum_metric).to receive(:increment)
allow(gitaly_seconds_metric).to receive(:observe)
allow(completion_seconds_metric).to receive(:observe)
allow(redis_seconds_metric).to receive(:observe)
diff --git a/spec/support/shared_contexts/models/ci/job_token_scope.rb b/spec/support/shared_contexts/models/ci/job_token_scope.rb
index d0fee23b57c..a33a3e09c71 100644
--- a/spec/support/shared_contexts/models/ci/job_token_scope.rb
+++ b/spec/support/shared_contexts/models/ci/job_token_scope.rb
@@ -16,12 +16,14 @@ end
RSpec.shared_context 'with inaccessible projects' do
let_it_be(:inbound_allowlist_project) { create_project_in_allowlist(source_project, direction: :inbound) }
+
include_context 'with unscoped projects'
end
RSpec.shared_context 'with unscoped projects' do
let_it_be(:unscoped_project1) { create(:project) }
let_it_be(:unscoped_project2) { create(:project) }
+ let_it_be(:unscoped_public_project) { create(:project, :public) }
let_it_be(:link_out_of_scope) { create(:ci_job_token_project_scope_link, target_project: unscoped_project1) }
end
diff --git a/spec/support/shared_contexts/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb
index a09319b4980..a5ccce27aa5 100644
--- a/spec/support/shared_contexts/navbar_structure_context.rb
+++ b/spec/support/shared_contexts/navbar_structure_context.rb
@@ -3,9 +3,9 @@
RSpec.shared_context 'project navbar structure' do
include NavbarStructureHelper
- let(:security_and_compliance_nav_item) do
+ let(:secure_nav_item) do
{
- nav_item: _('Security and Compliance'),
+ nav_item: _('Secure'),
nav_sub_items: [
(_('Audit events') if Gitlab.ee?),
_('Security configuration')
@@ -16,65 +16,58 @@ RSpec.shared_context 'project navbar structure' do
let(:structure) do
[
{
- nav_item: "#{project.name[0, 1].upcase} #{project.name}",
- nav_sub_items: []
+ nav_item: _('Manage'),
+ nav_sub_items: [
+ _('Activity'),
+ _('Members'),
+ _('Labels')
+ ]
},
{
- nav_item: _('Project information'),
+ nav_item: _('Plan'),
nav_sub_items: [
- _('Activity'),
- _('Labels'),
- _('Members')
+ _('Issues'),
+ _('Issue boards'),
+ _('Milestones'),
+ _('Wiki')
]
},
{
- nav_item: _('Repository'),
+ nav_item: _('Code'),
nav_sub_items: [
- _('Files'),
- _('Commits'),
+ _('Merge requests'),
+ _('Repository'),
_('Branches'),
+ _('Commits'),
_('Tags'),
- _('Contributor statistics'),
- _('Graph'),
+ _('Repository graph'),
_('Compare revisions'),
+ _('Snippets'),
(_('Locked files') if Gitlab.ee?)
]
},
{
- nav_item: _('Issues'),
- nav_sub_items: [
- _('List'),
- _('Boards'),
- _('Service Desk'),
- _('Milestones')
- ]
- },
- {
- nav_item: _('Merge requests'),
- nav_sub_items: []
- },
- {
- nav_item: _('CI/CD'),
+ nav_item: _('Build'),
nav_sub_items: [
_('Pipelines'),
- s_('Pipelines|Editor'),
_('Jobs'),
- _('Artifacts'),
- _('Schedules')
+ _('Pipeline editor'),
+ _('Pipeline schedules'),
+ _('Artifacts')
]
},
- security_and_compliance_nav_item,
+ secure_nav_item,
{
- nav_item: _('Deployments'),
+ nav_item: _('Deploy'),
nav_sub_items: [
- _('Environments'),
- s_('FeatureFlags|Feature flags'),
- _('Releases')
+ _('Releases'),
+ s_('FeatureFlags|Feature flags')
]
},
{
- nav_item: _('Infrastructure'),
+ nav_item: _('Operate'),
nav_sub_items: [
+ _('Environments'),
_('Kubernetes clusters'),
s_('Terraform|Terraform states')
]
@@ -84,22 +77,15 @@ RSpec.shared_context 'project navbar structure' do
nav_sub_items: [
_('Error Tracking'),
_('Alerts'),
- _('Incidents')
+ _('Incidents'),
+ _('Service Desk')
]
},
{
- nav_item: _('Analytics'),
+ nav_item: _('Analyze'),
nav_sub_items: project_analytics_sub_nav_item
},
{
- nav_item: _('Wiki'),
- nav_sub_items: []
- },
- {
- nav_item: _('Snippets'),
- nav_sub_items: []
- },
- {
nav_item: _('Settings'),
nav_sub_items: [
_('General'),
@@ -120,9 +106,9 @@ RSpec.shared_context 'project navbar structure' do
end
RSpec.shared_context 'group navbar structure' do
- let(:analytics_nav_item) do
+ let(:analyze_nav_item) do
{
- nav_item: _('Analytics'),
+ nav_item: _("Analyze"),
nav_sub_items: group_analytics_sub_nav_item
}
end
@@ -148,65 +134,46 @@ RSpec.shared_context 'group navbar structure' do
let(:settings_for_maintainer_nav_item) do
{
- nav_item: _('Settings'),
- nav_sub_items: [
- _('Repository')
- ]
+ nav_item: _("Settings"),
+ nav_sub_items: [_("Repository")]
}
end
- let(:security_and_compliance_nav_item) do
+ let(:secure_nav_item) do
{
- nav_item: _('Security and Compliance'),
- nav_sub_items: [
- _('Audit events')
- ]
+ nav_item: _("Secure"),
+ nav_sub_items: [_("Audit events")]
}
end
- let(:issues_nav_items) do
- [
- _('List'),
- _('Board'),
- _('Milestones'),
- (_('Iterations') if Gitlab.ee?)
- ]
+ let(:plan_nav_items) do
+ [_("Issues"), _("Issue board"), _("Milestones"), (_('Iterations') if Gitlab.ee?)]
end
let(:structure) do
[
{
- nav_item: "#{group.name[0, 1].upcase} #{group.name}",
- nav_sub_items: []
- },
- {
- nav_item: group.root? ? _('Group information') : _('Subgroup information'),
- nav_sub_items: [
- _('Activity'),
- _('Labels'),
- _('Members')
- ]
+ nav_item: _("Manage"),
+ nav_sub_items: [_("Activity"), _("Members"), _("Labels")]
},
{
- nav_item: _('Issues'),
- nav_sub_items: issues_nav_items
+ nav_item: _("Plan"),
+ nav_sub_items: plan_nav_items
},
{
- nav_item: _('Merge requests'),
- nav_sub_items: []
+ nav_item: _("Code"),
+ nav_sub_items: [_("Merge requests")]
},
- (security_and_compliance_nav_item if Gitlab.ee?),
{
- nav_item: _('CI/CD'),
- nav_sub_items: [
- s_('Runners|Runners')
- ]
+ nav_item: _("Build"),
+ nav_sub_items: [_("Runners")]
},
+ (secure_nav_item if Gitlab.ee?),
{
- nav_item: _('Kubernetes'),
- nav_sub_items: []
+ nav_item: _("Operate"),
+ nav_sub_items: [_("Kubernetes")]
},
- (analytics_nav_item if Gitlab.ee?)
+ (analyze_nav_item if Gitlab.ee?)
]
end
end
@@ -215,10 +182,6 @@ RSpec.shared_context 'dashboard navbar structure' do
let(:structure) do
[
{
- nav_item: "Your work",
- nav_sub_items: []
- },
- {
nav_item: _("Projects"),
nav_sub_items: []
},
@@ -237,8 +200,8 @@ RSpec.shared_context 'dashboard navbar structure' do
{
nav_item: _("Merge requests"),
nav_sub_items: [
- _('Assigned 0'),
- _('Review requests 0')
+ _('Assigned'),
+ _('Review requests')
]
},
{
@@ -265,10 +228,29 @@ RSpec.shared_context '"Explore" navbar structure' do
let(:structure) do
[
{
- nav_item: "Explore",
+ nav_item: _("Projects"),
+ nav_sub_items: []
+ },
+ {
+ nav_item: _("Groups"),
nav_sub_items: []
},
{
+ nav_item: _("Topics"),
+ nav_sub_items: []
+ },
+ {
+ nav_item: _("Snippets"),
+ nav_sub_items: []
+ }
+ ]
+ end
+end
+
+RSpec.shared_context '"Explore" navbar structure with global_ci_catalog FF' do
+ let(:structure) do
+ [
+ {
nav_item: _("Projects"),
nav_sub_items: []
},
@@ -277,6 +259,10 @@ RSpec.shared_context '"Explore" navbar structure' do
nav_sub_items: []
},
{
+ nav_item: _("CI/CD Catalog"),
+ nav_sub_items: []
+ },
+ {
nav_item: _("Topics"),
nav_sub_items: []
},
diff --git a/spec/support/shared_contexts/policies/project_policy_shared_context.rb b/spec/support/shared_contexts/policies/project_policy_shared_context.rb
index 5014a810f35..68eb3539813 100644
--- a/spec/support/shared_contexts/policies/project_policy_shared_context.rb
+++ b/spec/support/shared_contexts/policies/project_policy_shared_context.rb
@@ -54,7 +54,7 @@ RSpec.shared_context 'ProjectPolicy context' do
create_environment create_merge_request_from
admin_metrics_dashboard_annotation create_pipeline create_release
create_wiki destroy_container_image push_code read_pod_logs
- read_terraform_state resolve_note update_build update_commit_status
+ read_terraform_state resolve_note update_build cancel_build update_commit_status
update_container_image update_deployment update_environment
update_merge_request update_pipeline update_release destroy_release
read_resource_group update_resource_group update_escalation_status
diff --git a/spec/support/shared_contexts/services/projects/container_repository/delete_tags_service_shared_context.rb b/spec/support/shared_contexts/services/projects/container_repository/delete_tags_service_shared_context.rb
index 0cf026749ee..01b91cd5db4 100644
--- a/spec/support/shared_contexts/services/projects/container_repository/delete_tags_service_shared_context.rb
+++ b/spec/support/shared_contexts/services/projects/container_repository/delete_tags_service_shared_context.rb
@@ -16,11 +16,11 @@ RSpec.shared_context 'container repository delete tags service shared context' d
stub_container_registry_tags(
repository: repository.path,
- tags: %w(latest A Ba Bb C D E))
+ tags: %w[latest A Ba Bb C D E])
end
def stub_delete_reference_request(tag, status = 200)
- stub_request(:delete, "http://registry.gitlab/v2/#{repository.path}/tags/reference/#{tag}")
+ stub_request(:delete, "http://registry.gitlab/v2/#{repository.path}/manifests/#{tag}")
.to_return(status: status, body: '')
end
@@ -28,7 +28,7 @@ RSpec.shared_context 'container repository delete tags service shared context' d
tags = Array.wrap(tags).index_with { 200 } unless tags.is_a?(Hash)
tags.each do |tag, status|
- stub_request(:delete, "http://registry.gitlab/v2/#{repository.path}/tags/reference/#{tag}")
+ stub_request(:delete, "http://registry.gitlab/v2/#{repository.path}/manifests/#{tag}")
.to_return(status: status, body: '')
end
end
@@ -58,23 +58,11 @@ RSpec.shared_context 'container repository delete tags service shared context' d
.with(repository.path, content, digest) { double(success?: success ) }
end
- def expect_delete_tag_by_digest(digest)
- expect_any_instance_of(ContainerRegistry::Client)
- .to receive(:delete_repository_tag_by_digest)
- .with(repository.path, digest) { true }
-
- expect_any_instance_of(ContainerRegistry::Client)
- .not_to receive(:delete_repository_tag_by_name)
- end
-
- def expect_delete_tag_by_names(names)
+ def expect_delete_tags(names)
Array.wrap(names).each do |name|
expect_any_instance_of(ContainerRegistry::Client)
- .to receive(:delete_repository_tag_by_name)
+ .to receive(:delete_repository_tag_by_digest)
.with(repository.path, name) { true }
-
- expect_any_instance_of(ContainerRegistry::Client)
- .not_to receive(:delete_repository_tag_by_digest)
end
end
end
diff --git a/spec/support/shared_examples/analytics/cycle_analytics/request_params_examples.rb b/spec/support/shared_examples/analytics/cycle_analytics/request_params_examples.rb
index 0e7b909fce9..cf539174587 100644
--- a/spec/support/shared_examples/analytics/cycle_analytics/request_params_examples.rb
+++ b/spec/support/shared_examples/analytics/cycle_analytics/request_params_examples.rb
@@ -45,9 +45,19 @@ RSpec.shared_examples 'unlicensed cycle analytics request params' do
end
end
+ context 'when the date range is exactly 180 days' do
+ before do
+ params[:created_before] = '2019-06-30'
+ end
+
+ it 'is valid' do
+ expect(subject).to be_valid
+ end
+ end
+
context 'when the date range exceeds 180 days' do
before do
- params[:created_before] = '2019-07-15'
+ params[:created_before] = '2019-07-01'
end
it 'is invalid' do
diff --git a/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb b/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb
index c86fcf5ae20..9bd10d56d8c 100644
--- a/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb
+++ b/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb
@@ -5,7 +5,6 @@ RSpec.shared_examples 'multiple issue boards' do
context 'authorized user' do
before do
- stub_feature_flags(apollo_boards: false)
parent.add_maintainer(user)
login_as(user)
@@ -124,7 +123,6 @@ RSpec.shared_examples 'multiple issue boards' do
context 'unauthorized user' do
before do
- stub_feature_flags(apollo_boards: false)
visit boards_path
wait_for_requests
end
@@ -174,6 +172,8 @@ RSpec.shared_examples 'multiple issue boards' do
end
def assert_boards_nav_active
- expect(find('.nav-sidebar .active .active')).to have_selector('a', text: 'Boards')
+ within_testid('super-sidebar') do
+ expect(page).to have_selector('[aria-current="page"]', text: 'Issue boards')
+ end
end
end
diff --git a/spec/support/shared_examples/ci/deployable_policy_shared_examples.rb b/spec/support/shared_examples/ci/deployable_policy_shared_examples.rb
index 73bdc094237..1f164a66026 100644
--- a/spec/support/shared_examples/ci/deployable_policy_shared_examples.rb
+++ b/spec/support/shared_examples/ci/deployable_policy_shared_examples.rb
@@ -20,6 +20,7 @@ RSpec.shared_examples 'a deployable job policy' do |factory_type|
end
it { expect(policy).not_to be_allowed :update_build }
+ it { expect(policy).not_to be_allowed :cancel_build }
end
end
end
diff --git a/spec/support/shared_examples/ci/deployable_policy_shared_examples_ee.rb b/spec/support/shared_examples/ci/deployable_policy_shared_examples_ee.rb
index b1057b3f67a..10f334a6e23 100644
--- a/spec/support/shared_examples/ci/deployable_policy_shared_examples_ee.rb
+++ b/spec/support/shared_examples/ci/deployable_policy_shared_examples_ee.rb
@@ -20,6 +20,12 @@ RSpec.shared_examples 'a deployable job policy in EE' do |factory_type|
it_behaves_like 'protected environments access', direct_access: true
end
+ describe '#cancel_build?' do
+ subject { user.can?(:cancel_build, job) }
+
+ it_behaves_like 'protected environments access', direct_access: true
+ end
+
describe '#update_commit_status?' do
subject { user.can?(:update_commit_status, job) }
diff --git a/spec/support/shared_examples/ci/redis_shared_examples.rb b/spec/support/shared_examples/ci/redis_shared_examples.rb
new file mode 100644
index 00000000000..bc9826a8968
--- /dev/null
+++ b/spec/support/shared_examples/ci/redis_shared_examples.rb
@@ -0,0 +1,222 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'CI build trace chunk redis' do |redis_store|
+ describe '#data' do
+ subject { data_store.data(model) }
+
+ context 'when data exists' do
+ let(:model) { create(:ci_build_trace_chunk, store_trait_with_data, initial_data: 'sample data in redis') }
+
+ it 'returns the data' do
+ is_expected.to eq('sample data in redis')
+ end
+ end
+
+ context 'when data does not exist' do
+ let(:model) { create(:ci_build_trace_chunk, store_trait_without_data) }
+
+ it 'returns nil' do
+ is_expected.to be_nil
+ end
+ end
+ end
+
+ describe '#set_data' do
+ subject { data_store.set_data(model, data) }
+
+ let(:data) { 'abc123' }
+
+ context 'when data exists' do
+ let(:model) { create(:ci_build_trace_chunk, store_trait_with_data, initial_data: 'sample data in redis') }
+
+ it 'overwrites data' do
+ expect(data_store.data(model)).to eq('sample data in redis')
+
+ subject
+
+ expect(data_store.data(model)).to eq('abc123')
+ end
+ end
+
+ context 'when data does not exist' do
+ let(:model) { create(:ci_build_trace_chunk, store_trait_without_data) }
+
+ it 'sets new data' do
+ expect(data_store.data(model)).to be_nil
+
+ subject
+
+ expect(data_store.data(model)).to eq('abc123')
+ end
+ end
+ end
+
+ describe '#append_data' do
+ context 'when valid offset is used with existing data' do
+ let(:model) { create(:ci_build_trace_chunk, store_trait_with_data, initial_data: 'abcd') }
+
+ it 'appends data' do
+ expect(data_store.data(model)).to eq('abcd')
+
+ length = data_store.append_data(model, '12345', 4)
+
+ expect(length).to eq 9
+ expect(data_store.data(model)).to eq('abcd12345')
+ end
+ end
+
+ context 'when data does not exist yet' do
+ let(:model) { create(:ci_build_trace_chunk, store_trait_without_data) }
+
+ it 'sets new data' do
+ expect(data_store.data(model)).to be_nil
+
+ length = data_store.append_data(model, 'abc', 0)
+
+ expect(length).to eq 3
+ expect(data_store.data(model)).to eq('abc')
+ end
+ end
+
+ context 'when data needs to be truncated' do
+ let(:model) { create(:ci_build_trace_chunk, store_trait_with_data, initial_data: '12345678') }
+
+ it 'appends data and truncates stored value' do
+ expect(data_store.data(model)).to eq('12345678')
+
+ length = data_store.append_data(model, 'ab', 4)
+
+ expect(length).to eq 6
+ expect(data_store.data(model)).to eq('1234ab')
+ end
+ end
+
+ context 'when invalid offset is provided' do
+ let(:model) { create(:ci_build_trace_chunk, store_trait_with_data, initial_data: 'abc') }
+
+ it 'raises an exception' do
+ length = data_store.append_data(model, '12345', 4)
+
+ expect(length).to be_negative
+ end
+ end
+
+ context 'when trace contains multi-byte UTF8 characters' do
+ let(:model) { create(:ci_build_trace_chunk, store_trait_with_data, initial_data: 'aüc') }
+
+ it 'appends data' do
+ length = data_store.append_data(model, '1234', 4)
+
+ data_store.data(model).then do |new_data|
+ expect(new_data.bytesize).to eq 8
+ expect(new_data).to eq 'aüc1234'
+ end
+
+ expect(length).to eq 8
+ end
+ end
+
+ context 'when trace contains non-UTF8 characters' do
+ let(:model) { create(:ci_build_trace_chunk, store_trait_with_data, initial_data: "a\255c") }
+
+ it 'appends data' do
+ length = data_store.append_data(model, '1234', 3)
+
+ data_store.data(model).then do |new_data|
+ expect(new_data.bytesize).to eq 7
+ end
+
+ expect(length).to eq 7
+ end
+ end
+ end
+
+ describe '#delete_data' do
+ subject { data_store.delete_data(model) }
+
+ context 'when data exists' do
+ let(:model) { create(:ci_build_trace_chunk, store_trait_with_data, initial_data: 'sample data in redis') }
+
+ it 'deletes data' do
+ expect(data_store.data(model)).to eq('sample data in redis')
+
+ subject
+
+ expect(data_store.data(model)).to be_nil
+ end
+ end
+
+ context 'when data does not exist' do
+ let(:model) { create(:ci_build_trace_chunk, store_trait_without_data) }
+
+ it 'does nothing' do
+ expect(data_store.data(model)).to be_nil
+
+ subject
+
+ expect(data_store.data(model)).to be_nil
+ end
+ end
+ end
+
+ describe '#size' do
+ context 'when data exists' do
+ let(:model) { create(:ci_build_trace_chunk, store_trait_with_data, initial_data: 'üabcd') }
+
+ it 'returns data bytesize correctly' do
+ expect(data_store.size(model)).to eq 6
+ end
+ end
+
+ context 'when data does not exist' do
+ let(:model) { create(:ci_build_trace_chunk, store_trait_without_data) }
+
+ it 'returns zero' do
+ expect(data_store.size(model)).to be_zero
+ end
+ end
+ end
+
+ describe '#keys' do
+ subject { data_store.keys(relation) }
+
+ let(:build) { create(:ci_build) }
+ let(:relation) { build.trace_chunks }
+
+ before do
+ create(:ci_build_trace_chunk, store_trait_with_data, chunk_index: 0, build: build)
+ create(:ci_build_trace_chunk, store_trait_with_data, chunk_index: 1, build: build)
+ end
+
+ it 'returns keys' do
+ is_expected.to eq([[build.id, 0], [build.id, 1]])
+ end
+ end
+
+ describe '#delete_keys' do
+ subject { data_store.delete_keys(keys) }
+
+ let(:build) { create(:ci_build) }
+ let(:relation) { build.trace_chunks }
+ let(:keys) { data_store.keys(relation) }
+
+ before do
+ create(:ci_build_trace_chunk, store_trait_with_data, chunk_index: 0, build: build)
+ create(:ci_build_trace_chunk, store_trait_with_data, chunk_index: 1, build: build)
+ end
+
+ it 'deletes multiple data' do
+ redis_store.with do |redis|
+ expect(redis.exists?("gitlab:ci:trace:#{build.id}:chunks:0")).to eq(true)
+ expect(redis.exists?("gitlab:ci:trace:#{build.id}:chunks:1")).to eq(true)
+ end
+
+ subject
+
+ redis_store.with do |redis|
+ expect(redis.exists?("gitlab:ci:trace:#{build.id}:chunks:0")).to eq(false)
+ expect(redis.exists?("gitlab:ci:trace:#{build.id}:chunks:1")).to eq(false)
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/controllers/base_action_controller_shared_examples.rb b/spec/support/shared_examples/controllers/base_action_controller_shared_examples.rb
deleted file mode 100644
index 5f236f25d35..00000000000
--- a/spec/support/shared_examples/controllers/base_action_controller_shared_examples.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-# frozen_string_literal: true
-
-# Requires `request` subject to be defined
-#
-# subject(:request) { get root_path }
-RSpec.shared_examples 'Base action controller' do
- describe 'security headers' do
- describe 'Cross-Origin-Opener-Policy' do
- it 'sets the header' do
- request
-
- expect(response.headers['Cross-Origin-Opener-Policy']).to eq('same-origin')
- end
-
- context 'when coop_header feature flag is disabled' do
- it 'does not set the header' do
- stub_feature_flags(coop_header: false)
-
- request
-
- expect(response.headers['Cross-Origin-Opener-Policy']).to be_nil
- end
- end
- end
- end
-end
diff --git a/spec/support/shared_examples/controllers/is_ambiguous_ref_examples.rb b/spec/support/shared_examples/controllers/is_ambiguous_ref_examples.rb
new file mode 100644
index 00000000000..24a656514c4
--- /dev/null
+++ b/spec/support/shared_examples/controllers/is_ambiguous_ref_examples.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples '#set_is_ambiguous_ref when ref is ambiguous' do
+ context 'when the ref_type is nil' do
+ let(:ref_type) { nil }
+
+ it '@ambiguous_ref return false when ff is disabled' do
+ expect(controller.instance_variable_get(:@is_ambiguous_ref)).to eq(false)
+ end
+
+ context 'when the ambiguous_ref_modal ff is enabled' do
+ let(:ambiguous_ref_modal) { true }
+
+ it '@ambiguous_ref return true' do
+ expect(controller.instance_variable_get(:@is_ambiguous_ref)).to eq(true)
+ end
+ end
+ end
+
+ context 'when the ref_type is empty' do
+ let(:ref_type) { '' }
+
+ it '@ambiguous_ref return false when ff is disabled' do
+ expect(controller.instance_variable_get(:@is_ambiguous_ref)).to eq(false)
+ end
+
+ context 'when the ambiguous_ref_modal ff is enabled' do
+ let(:ambiguous_ref_modal) { true }
+
+ it '@ambiguous_ref return true' do
+ expect(controller.instance_variable_get(:@is_ambiguous_ref)).to eq(true)
+ end
+ end
+ end
+
+ context 'when the ref_type is present' do
+ let(:ref_type) { 'heads' }
+ let(:ambiguous_ref_modal) { true }
+
+ it '@ambiguous_ref return false' do
+ expect(controller.instance_variable_get(:@is_ambiguous_ref)).to eq(false)
+ end
+ end
+end
+
+RSpec.shared_examples '#set_is_ambiguous_ref when ref is not ambiguous' do
+ context 'when the ref_type is nil' do
+ let(:ref_type) { nil }
+ let(:ambiguous_ref_modal) { true }
+
+ it '@ambiguous_ref return false' do
+ expect(controller.instance_variable_get(:@is_ambiguous_ref)).to eq(false)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb
index 32aa566c27e..8cec0cdfbf5 100644
--- a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb
@@ -324,7 +324,7 @@ RSpec.shared_examples 'wiki controller actions' do
post :preview_markdown, params: routing_params.merge(id: 'page/path', text: '*Markdown* text')
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response.keys).to match_array(%w(body references))
+ expect(json_response.keys).to match_array(%w[body references])
end
end
diff --git a/spec/support/shared_examples/database_health_status_indicators/prometheus_alert_based_shared_examples.rb b/spec/support/shared_examples/database_health_status_indicators/prometheus_alert_based_shared_examples.rb
index 109a349a652..ddc438cb652 100644
--- a/spec/support/shared_examples/database_health_status_indicators/prometheus_alert_based_shared_examples.rb
+++ b/spec/support/shared_examples/database_health_status_indicators/prometheus_alert_based_shared_examples.rb
@@ -2,7 +2,7 @@
RSpec.shared_examples 'Prometheus Alert based health indicator' do
let(:schema) { :main }
- let(:connection) { Gitlab::Database.database_base_models[schema].connection }
+ let(:connection) { Gitlab::Database.database_base_models_with_gitlab_shared[schema].connection }
around do |example|
Gitlab::Database::SharedModel.using_connection(connection) do
@@ -124,7 +124,7 @@ RSpec.shared_examples 'Prometheus Alert based health indicator' do
end
end
- Gitlab::Database.database_base_models.each do |database_base_model, connection|
+ Gitlab::Database.database_base_models_with_gitlab_shared.each do |database_base_model, connection|
next unless connection.present?
it_behaves_like 'Patroni Apdex Evaluator', database_base_model.to_sym
diff --git a/spec/support/shared_examples/features/2fa_shared_examples.rb b/spec/support/shared_examples/features/2fa_shared_examples.rb
index f50874b6b05..8744259488f 100644
--- a/spec/support/shared_examples/features/2fa_shared_examples.rb
+++ b/spec/support/shared_examples/features/2fa_shared_examples.rb
@@ -14,7 +14,7 @@ RSpec.shared_examples 'hardware device for 2fa' do |device_type|
end
describe "registration" do
- let(:user) { create(:user, :no_super_sidebar) }
+ let(:user) { create(:user) }
before do
gitlab_sign_in(user)
@@ -66,8 +66,8 @@ RSpec.shared_examples 'hardware device for 2fa' do |device_type|
end
end
- describe 'fallback code authentication' do
- let(:user) { create(:user, :no_super_sidebar) }
+ describe 'fallback code authentication', :js do
+ let(:user) { create(:user) }
before do
# Register and logout
diff --git a/spec/support/shared_examples/features/content_editor_shared_examples.rb b/spec/support/shared_examples/features/content_editor_shared_examples.rb
index 3e81f969462..6bfb60c3f34 100644
--- a/spec/support/shared_examples/features/content_editor_shared_examples.rb
+++ b/spec/support/shared_examples/features/content_editor_shared_examples.rb
@@ -2,7 +2,10 @@
require 'spec_helper'
-RSpec.shared_examples 'edits content using the content editor' do |params = { with_expanded_references: true }|
+RSpec.shared_examples 'edits content using the content editor' do |params = {
+ with_expanded_references: true,
+ with_quick_actions: true
+}|
include ContentEditorHelpers
let(:content_editor_testid) { '[data-testid="content-editor"] [contenteditable].ProseMirror' }
@@ -463,6 +466,15 @@ RSpec.shared_examples 'edits content using the content editor' do |params = { wi
end
end
+ it 'does not show a loading indicator after undo paste' do
+ type_in_content_editor [modifier_key, 'v']
+ type_in_content_editor [modifier_key, 'z']
+
+ page.within content_editor_testid do
+ expect(page).not_to have_css('.gl-dots-loader')
+ end
+ end
+
it 'pastes raw text without formatting if shift + ctrl + v is pressed' do
type_in_content_editor [modifier_key, :shift, 'v']
@@ -546,6 +558,42 @@ RSpec.shared_examples 'edits content using the content editor' do |params = { wi
end
end
+ if params[:with_quick_actions]
+ it 'shows suggestions for quick actions' do
+ type_in_content_editor '/a'
+
+ expect(find(suggestions_dropdown)).to have_text('/assign')
+ expect(find(suggestions_dropdown)).to have_text('/label')
+ end
+
+ it 'adds the correct prefix for /assign' do
+ type_in_content_editor '/assign'
+
+ expect(find(suggestions_dropdown)).to have_text('/assign')
+ send_keys [:arrow_down, :enter]
+
+ expect(page).to have_text('/assign @')
+ end
+
+ it 'adds the correct prefix for /label' do
+ type_in_content_editor '/label'
+
+ expect(find(suggestions_dropdown)).to have_text('/label')
+ send_keys [:arrow_down, :enter]
+
+ expect(page).to have_text('/label ~')
+ end
+
+ it 'adds the correct prefix for /milestone' do
+ type_in_content_editor '/milestone'
+
+ expect(find(suggestions_dropdown)).to have_text('/milestone')
+ send_keys [:arrow_down, :enter]
+
+ expect(page).to have_text('/milestone %')
+ end
+ end
+
it 'shows suggestions for members with descriptions' do
type_in_content_editor '@a'
@@ -561,6 +609,39 @@ RSpec.shared_examples 'edits content using the content editor' do |params = { wi
expect(page).to have_text('@abc123')
end
+ it 'allows dismissing the suggestion popup and typing more text' do
+ type_in_content_editor '@ab'
+
+ expect(find(suggestions_dropdown)).to have_text('abc123')
+
+ send_keys :escape
+
+ expect(page).not_to have_css(suggestions_dropdown)
+
+ type_in_content_editor :enter
+ type_in_content_editor 'foobar'
+
+ # ensure that the texts are in separate paragraphs
+ expect(page).to have_selector('p', text: '@ab')
+ expect(page).to have_selector('p', text: 'foobar')
+ expect(page).not_to have_selector('p', text: '@abfoobar')
+ end
+
+ it 'allows typing more text after the popup has disappeared because no suggestions match' do
+ type_in_content_editor '@ab'
+
+ expect(find(suggestions_dropdown)).to have_text('abc123')
+
+ type_in_content_editor 'foo'
+ type_in_content_editor :enter
+ type_in_content_editor 'bar'
+
+ # ensure that the texts are in separate paragraphs
+ expect(page).to have_selector('p', text: '@abfoo')
+ expect(page).to have_selector('p', text: 'bar')
+ expect(page).not_to have_selector('p', text: '@abfoobar')
+ end
+
context 'when `disable_all_mention` is enabled' do
before do
stub_feature_flags(disable_all_mention: true)
@@ -617,14 +698,14 @@ RSpec.shared_examples 'edits content using the content editor' do |params = { wi
it 'shows suggestions for emojis' do
type_in_content_editor ':smile'
- expect(find(suggestions_dropdown)).to have_text('🙂 slight_smile')
+ expect(find(suggestions_dropdown)).to have_text('😃 smiley')
expect(find(suggestions_dropdown)).to have_text('😸 smile_cat')
send_keys [:arrow_down, :enter]
expect(page).not_to have_css(suggestions_dropdown)
- expect(page).to have_text('🙂')
+ expect(page).to have_text('😃')
end
it 'doesn\'t show suggestions dropdown if there are no suggestions to show' do
@@ -640,7 +721,7 @@ RSpec.shared_examples 'edits content using the content editor' do |params = { wi
it 'scrolls selected item into view when navigating with keyboard' do
type_in_content_editor ':'
- expect(find(suggestions_dropdown)).to have_text('hundred points symbol')
+ expect(find(suggestions_dropdown)).to have_text('grinning')
expect(dropdown_scroll_top).to be 0
diff --git a/spec/support/shared_examples/features/dashboard/sidebar_shared_examples.rb b/spec/support/shared_examples/features/dashboard/sidebar_shared_examples.rb
index 9b5d9d66890..0dbf186e9b3 100644
--- a/spec/support/shared_examples/features/dashboard/sidebar_shared_examples.rb
+++ b/spec/support/shared_examples/features/dashboard/sidebar_shared_examples.rb
@@ -6,23 +6,19 @@ RSpec.shared_examples 'a "Your work" page with sidebar and breadcrumbs' do |page
visit send(page_path)
end
- let(:sidebar_css) { "aside.nav-sidebar[aria-label=\"Your work\"]" }
- let(:active_menu_item_css) { "li.active[data-track-label=\"#{menu_label}_menu\"]" }
-
it "shows the \"Your work\" sidebar" do
- expect(page).to have_css(sidebar_css)
+ expect(page).to have_css('#super-sidebar-context-header', text: 'Your work')
end
it "shows the correct sidebar menu item as active" do
- within(sidebar_css) do
- expect(page).to have_css(active_menu_item_css)
+ within_testid('super-sidebar') do
+ expect(page).to have_css("a[data-track-label='#{menu_label}_menu'][aria-current='page']")
end
end
describe "breadcrumbs" do
it 'has "Your work" as its root breadcrumb' do
- breadcrumbs = page.find('[data-testid="breadcrumb-links"]')
- within breadcrumbs do
+ within_testid('breadcrumb-links') do
expect(page).to have_css("li:first-child a[href=\"#{root_path}\"]", text: "Your work")
end
end
diff --git a/spec/support/shared_examples/features/explore/sidebar_shared_examples.rb b/spec/support/shared_examples/features/explore/sidebar_shared_examples.rb
deleted file mode 100644
index 1754c8bf53d..00000000000
--- a/spec/support/shared_examples/features/explore/sidebar_shared_examples.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.shared_examples 'an "Explore" page with sidebar and breadcrumbs' do |page_path, menu_label|
- before do
- visit send(page_path)
- end
-
- let(:sidebar_css) { 'aside.nav-sidebar[aria-label="Explore"]' }
- let(:active_menu_item_css) { "li.active[data-track-label=\"#{menu_label}_menu\"]" }
-
- it 'shows the "Explore" sidebar' do
- expect(page).to have_css(sidebar_css)
- end
-
- it 'shows the correct sidebar menu item as active' do
- within(sidebar_css) do
- expect(page).to have_css(active_menu_item_css)
- end
- end
-
- describe 'breadcrumbs' do
- it 'has "Explore" as its root breadcrumb' do
- within '.breadcrumbs-list' do
- expect(page).to have_css("li:first a[href=\"#{explore_root_path}\"]", text: 'Explore')
- end
- end
- end
-end
diff --git a/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb b/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb
index b8c6b85adb2..f53aaa4514d 100644
--- a/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb
+++ b/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb
@@ -4,22 +4,17 @@ RSpec.shared_examples 'issuable invite members' do
include Features::InviteMembersModalHelpers
context 'when a privileged user can invite' do
- before do
- project.add_maintainer(user)
- end
-
it 'shows a link for inviting members and launches invite modal' do
+ project.add_maintainer(user)
visit issuable_path
- find('.block.assignee .edit-link').click
-
- wait_for_requests
+ open_assignees_dropdown
page.within '.dropdown-menu-user' do
- expect(page).to have_link('Invite Members')
- end
+ expect(page).to have_link('Invite members')
- click_link 'Invite Members'
+ click_link 'Invite members'
+ end
page.within invite_modal_selector do
expect(page).to have_content("You're inviting members to the #{project.name} project")
@@ -28,19 +23,14 @@ RSpec.shared_examples 'issuable invite members' do
end
context 'when user cannot invite members in assignee dropdown' do
- before do
- project.add_developer(user)
- end
-
it 'shows author in assignee dropdown and no invite link' do
+ project.add_developer(user)
visit issuable_path
- find('.block.assignee .edit-link').click
-
- wait_for_requests
+ open_assignees_dropdown
page.within '.dropdown-menu-user' do
- expect(page).not_to have_link('Invite Members')
+ expect(page).not_to have_link('Invite members')
end
end
end
diff --git a/spec/support/shared_examples/features/nav_sidebar_shared_examples.rb b/spec/support/shared_examples/features/nav_sidebar_shared_examples.rb
index 34821fb9eda..8c668ae9a87 100644
--- a/spec/support/shared_examples/features/nav_sidebar_shared_examples.rb
+++ b/spec/support/shared_examples/features/nav_sidebar_shared_examples.rb
@@ -2,15 +2,16 @@
RSpec.shared_examples 'page has active tab' do |title|
it "activates #{title} tab" do
- expect(page).to have_selector('.sidebar-top-level-items > li.active', count: 1)
- expect(find('.sidebar-top-level-items > li.active')).to have_content(title)
+ within_testid('super-sidebar') do
+ expect(page).to have_selector('button[aria-expanded="true"]', text: title)
+ end
end
end
RSpec.shared_examples 'page has active sub tab' do |title|
it "activates #{title} sub tab" do
- expect(page).to have_selector('.sidebar-sub-level-items > li.active:not(.fly-out-top-item)', count: 1)
- expect(find('.sidebar-sub-level-items > li.active:not(.fly-out-top-item)'))
- .to have_content(title)
+ within_testid('super-sidebar') do
+ expect(page).to have_selector('a[aria-current="page"]', text: title)
+ end
end
end
diff --git a/spec/support/shared_examples/features/navbar_shared_examples.rb b/spec/support/shared_examples/features/navbar_shared_examples.rb
index 9b89a3b5e54..af601c2b8e5 100644
--- a/spec/support/shared_examples/features/navbar_shared_examples.rb
+++ b/spec/support/shared_examples/features/navbar_shared_examples.rb
@@ -8,17 +8,12 @@ RSpec.shared_examples 'verified navigation bar' do
end
it 'renders correctly' do
- # we are using * here in the selectors to prevent a regression where we added a non 'li' inside an 'ul'
- current_structure = page.all('.sidebar-top-level-items > *', class: ['!hidden']).map do |item|
- next if item.find_all('a').empty?
-
- nav_item = item.find_all('a').first.text.gsub(/\s+\d+$/, '') # remove counts at the end
-
- nav_sub_items = item.all('.sidebar-sub-level-items > *', class: ['!fly-out-top-item']).map do |list_item|
- list_item.all('a').first.text
+ current_structure = page.all('[data-testid="non-static-items-section"] > li').map do |item|
+ nav_sub_items = item.all('li', visible: :all).map do |list_item|
+ list_item.all('a', visible: :all).first.text(:all).gsub(/\s+\d+$/, '') # remove counts at the end
end
- { nav_item: nav_item, nav_sub_items: nav_sub_items }
+ { nav_item: item.text, nav_sub_items: nav_sub_items }
end.compact
expect(current_structure).to eq(expected_structure)
diff --git a/spec/support/shared_examples/features/packages_shared_examples.rb b/spec/support/shared_examples/features/packages_shared_examples.rb
index 8e8e7e8ad05..6d283113e85 100644
--- a/spec/support/shared_examples/features/packages_shared_examples.rb
+++ b/spec/support/shared_examples/features/packages_shared_examples.rb
@@ -158,7 +158,7 @@ def click_sort_option(option, ascending)
wait_for_requests
# Reset the sort direction
- if page.has_selector?('button[aria-label="Sorting Direction: Ascending"]', wait: 0) && !ascending
+ if page.has_selector?('button[aria-label="Sort direction: Ascending"]', wait: 0) && !ascending
click_button 'Sort direction'
wait_for_requests
diff --git a/spec/support/shared_examples/features/page_description_shared_examples.rb b/spec/support/shared_examples/features/page_description_shared_examples.rb
index e3ea36633d1..163c65915ba 100644
--- a/spec/support/shared_examples/features/page_description_shared_examples.rb
+++ b/spec/support/shared_examples/features/page_description_shared_examples.rb
@@ -2,7 +2,7 @@
RSpec.shared_examples 'page meta description' do |expected_description|
it 'renders the page with description, og:description, and twitter:description meta tags that contains a plain-text version of the markdown', :aggregate_failures do
- %w(name='description' property='og:description' property='twitter:description').each do |selector|
+ %w[name='description' property='og:description' property='twitter:description'].each do |selector|
expect(page).to have_selector("meta[#{selector}][content='#{expected_description}']", visible: false)
end
end
@@ -12,7 +12,7 @@ RSpec.shared_examples 'default brand title page meta description' do
include AppearancesHelper
it 'renders the page with description, og:description, and twitter:description meta tags with the default brand title', :aggregate_failures do
- %w(name='description' property='og:description' property='twitter:description').each do |selector|
+ %w[name='description' property='og:description' property='twitter:description'].each do |selector|
expect(page).to have_selector("meta[#{selector}][content='#{default_brand_title}']", visible: false)
end
end
diff --git a/spec/support/shared_examples/features/project_features_apply_to_issuables_shared_examples.rb b/spec/support/shared_examples/features/project_features_apply_to_issuables_shared_examples.rb
index 58bf461c733..d410653ca43 100644
--- a/spec/support/shared_examples/features/project_features_apply_to_issuables_shared_examples.rb
+++ b/spec/support/shared_examples/features/project_features_apply_to_issuables_shared_examples.rb
@@ -4,8 +4,8 @@ RSpec.shared_examples 'project features apply to issuables' do |klass|
let(:described_class) { klass }
let(:group) { create(:group) }
- let(:user_in_group) { create(:group_member, :developer, user: create(:user, :no_super_sidebar), group: group ).user }
- let(:user_outside_group) { create(:user, :no_super_sidebar) }
+ let(:user_in_group) { create(:group_member, :developer, user: create(:user), group: group ).user }
+ let(:user_outside_group) { create(:user) }
let(:project) { create(:project, :public, project_args) }
diff --git a/spec/support/shared_examples/features/search/search_timeouts_shared_examples.rb b/spec/support/shared_examples/features/search/search_timeouts_shared_examples.rb
index b73f40ff28c..1e7f75d2ac0 100644
--- a/spec/support/shared_examples/features/search/search_timeouts_shared_examples.rb
+++ b/spec/support/shared_examples/features/search/search_timeouts_shared_examples.rb
@@ -17,7 +17,7 @@ RSpec.shared_examples 'search timeouts' do |scope|
end
it 'sets tab count to 0' do
- expect(page.find('[data-testid="search-filter"] .active')).to have_text('0')
+ expect(page.find('[data-testid="search-filter"] [aria-current="page"]')).to have_text('0')
end
end
end
diff --git a/spec/support/shared_examples/features/snippets_shared_examples.rb b/spec/support/shared_examples/features/snippets_shared_examples.rb
index 383f81d048f..0f830fa125a 100644
--- a/spec/support/shared_examples/features/snippets_shared_examples.rb
+++ b/spec/support/shared_examples/features/snippets_shared_examples.rb
@@ -52,30 +52,24 @@ RSpec.shared_examples 'tabs with counts' do
end
RSpec.shared_examples 'does not show New Snippet button' do
- let(:user) { create(:user, :external, :no_super_sidebar) }
-
specify do
- sign_in(user)
-
- subject
-
- wait_for_requests
-
+ expect(page).to have_link(text: "$#{snippet.id}")
expect(page).not_to have_link('New snippet')
end
end
-RSpec.shared_examples 'show and render proper snippet blob' do
- before do
- allow_any_instance_of(Snippet).to receive(:blobs).and_return([snippet.repository.blob_at('master', file_path)])
+RSpec.shared_examples 'does show New Snippet button' do
+ specify do
+ expect(page).to have_link(text: "$#{snippet.id}")
+ expect(page).to have_link('New snippet')
end
+end
+RSpec.shared_examples 'show and render proper snippet blob' do
context 'Ruby file' do
let(:file_path) { 'files/ruby/popen.rb' }
it 'displays the blob' do
- subject
-
aggregate_failures do
# shows highlighted Ruby code
expect(page).to have_content("require 'fileutils'")
@@ -99,10 +93,6 @@ RSpec.shared_examples 'show and render proper snippet blob' do
let(:file_path) { 'files/markdown/ruby-style-guide.md' }
context 'visiting directly' do
- before do
- subject
- end
-
it 'displays the blob using the rich viewer' do
aggregate_failures do
# hides the simple viewer
@@ -171,8 +161,6 @@ RSpec.shared_examples 'show and render proper snippet blob' do
let(:anchor) { 'LC1' }
it 'displays the blob using the simple viewer' do
- subject
-
aggregate_failures do
# hides the rich viewer
expect(page).to have_selector('.blob-viewer[data-type="simple"]')
diff --git a/spec/support/shared_examples/features/variable_list_env_scope_shared_examples.rb b/spec/support/shared_examples/features/variable_list_env_scope_shared_examples.rb
new file mode 100644
index 00000000000..c40d70b85d3
--- /dev/null
+++ b/spec/support/shared_examples/features/variable_list_env_scope_shared_examples.rb
@@ -0,0 +1,87 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'variable list env scope' do
+ include ListboxHelpers
+
+ let(:user) { create(:user) }
+ let(:project) { build(:project) }
+ let(:page_path) { project_settings_ci_cd_path(project) }
+
+ before do
+ sign_in(user)
+ project.add_maintainer(user)
+
+ visit page_path
+ wait_for_requests
+ end
+
+ it 'adds a new variable with an environment scope' do
+ open_drawer
+
+ page.within('[data-testid="ci-variable-drawer"]') do
+ fill_in 'Key', with: 'akey'
+ fill_in 'Value', with: 'akey_value'
+
+ click_button('All (default)')
+ fill_in 'Search', with: 'review/*'
+ find('[data-testid="create-wildcard-button"]').click
+
+ click_button('Add variable')
+ end
+
+ wait_for_requests
+
+ page.within('[data-testid="ci-variable-table"]') do
+ expect(find('.js-ci-variable-row:first-child [data-label="Environments"]').text).to eq('review/*')
+ end
+ end
+
+ it 'resets environment scope list after closing the form' do
+ project.environments.create!(name: 'dev')
+ project.environments.create!(name: 'env_1')
+ project.environments.create!(name: 'env_2')
+
+ open_drawer
+
+ page.within('[data-testid="ci-variable-drawer"]') do
+ click_button('All (default)')
+
+ # default list of env scopes
+ expect_env_scope_items(['*', 'dev', 'env_1', 'env_2'])
+
+ fill_in 'Search', with: 'env'
+ sleep 0.5 # wait for debounce
+ wait_for_requests
+
+ # search filters the list of env scopes
+ expect_env_scope_items(%w[env_1 env_2])
+
+ find('.gl-drawer-close-button').click
+ end
+
+ # Re-open drawer
+ open_drawer
+
+ page.within('[data-testid="ci-variable-drawer"]') do
+ click_button('All (default)')
+
+ # dropdown should reset back to default list of env scopes
+ expect_env_scope_items(['*', 'dev', 'env_1', 'env_2'])
+ end
+ end
+
+ private
+
+ def open_drawer
+ page.within('[data-testid="ci-variable-table"]') do
+ click_button('Add variable')
+ wait_for_requests
+ end
+ end
+
+ def expect_env_scope_items(items)
+ page.within('[data-testid="environment-scope"]') do
+ expect_listbox_items(items)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/features/variable_list_shared_examples.rb b/spec/support/shared_examples/features/variable_list_shared_examples.rb
deleted file mode 100644
index 5951d3e781b..00000000000
--- a/spec/support/shared_examples/features/variable_list_shared_examples.rb
+++ /dev/null
@@ -1,292 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.shared_examples 'variable list' do
- it 'shows a list of variables' do
- page.within('[data-testid="ci-variable-table"]') do
- expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]')).to have_content(variable.key)
- end
- end
-
- it 'adds a new CI variable' do
- click_button('Add variable')
-
- fill_variable('key', 'key_value') do
- click_button('Add variable')
- end
-
- wait_for_requests
-
- page.within('[data-testid="ci-variable-table"]') do
- expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]')).to have_content('key')
- end
- end
-
- it 'adds a new protected variable' do
- click_button('Add variable')
-
- fill_variable('key', 'key_value') do
- click_button('Add variable')
- end
-
- wait_for_requests
-
- page.within('[data-testid="ci-variable-table"]') do
- expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Key')}']")).to have_content('key')
- expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Key')}']")).to have_content(s_('CiVariables|Protected'))
- end
- end
-
- it 'defaults to unmasked' do
- click_button('Add variable')
-
- fill_variable('key', 'key_value') do
- click_button('Add variable')
- end
-
- wait_for_requests
-
- page.within('[data-testid="ci-variable-table"]') do
- expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Key')}']")).to have_content('key')
- expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Key')}']")).not_to have_content(s_('CiVariables|Masked'))
- end
- end
-
- it 'reveals and hides variables' do
- page.within('[data-testid="ci-variable-table"]') do
- expect(first('.js-ci-variable-row td[data-label="Key"]')).to have_content(variable.key)
- expect(page).to have_content('*' * 5)
-
- click_button('Reveal value')
-
- expect(first('.js-ci-variable-row td[data-label="Key"]')).to have_content(variable.key)
- expect(first('.js-ci-variable-row td[data-label="Value"]')).to have_content(variable.value)
- expect(page).not_to have_content('*' * 5)
-
- click_button('Hide value')
-
- expect(first('.js-ci-variable-row td[data-label="Key"]')).to have_content(variable.key)
- expect(page).to have_content('*' * 5)
- end
- end
-
- it 'deletes a variable' do
- expect(page).to have_selector('.js-ci-variable-row', count: 1)
-
- page.within('[data-testid="ci-variable-table"]') do
- click_button('Edit')
- end
-
- page.within('#add-ci-variable') do
- click_button('Delete variable')
- end
-
- wait_for_requests
-
- expect(first('.js-ci-variable-row').text).to eq('There are no variables yet.')
- end
-
- it 'edits a variable' do
- page.within('[data-testid="ci-variable-table"]') do
- click_button('Edit')
- end
-
- page.within('#add-ci-variable') do
- find('[data-testid="pipeline-form-ci-variable-key"] input').set('new_key')
-
- click_button('Update variable')
- end
-
- wait_for_requests
-
- expect(first('.js-ci-variable-row td[data-label="Key"]')).to have_content('new_key')
- end
-
- it 'edits a variable to be unmasked' do
- page.within('[data-testid="ci-variable-table"]') do
- click_button('Edit')
- end
-
- page.within('#add-ci-variable') do
- find('[data-testid="ci-variable-protected-checkbox"]').click
- find('[data-testid="ci-variable-masked-checkbox"]').click
-
- click_button('Update variable')
- end
-
- wait_for_requests
-
- page.within('[data-testid="ci-variable-table"]') do
- expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Key')}']")).to have_content(s_('CiVariables|Protected'))
- expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Key')}']")).not_to have_content(s_('CiVariables|Masked'))
- end
- end
-
- it 'edits a variable to be masked' do
- page.within('[data-testid="ci-variable-table"]') do
- click_button('Edit')
- end
-
- page.within('#add-ci-variable') do
- find('[data-testid="ci-variable-masked-checkbox"]').click
-
- click_button('Update variable')
- end
-
- wait_for_requests
-
- page.within('[data-testid="ci-variable-table"]') do
- click_button('Edit')
- end
-
- page.within('#add-ci-variable') do
- find('[data-testid="ci-variable-masked-checkbox"]').click
-
- click_button('Update variable')
- end
-
- page.within('[data-testid="ci-variable-table"]') do
- expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Key')}']")).to have_content(s_('CiVariables|Masked'))
- end
- end
-
- it 'shows a validation error box about duplicate keys' do
- click_button('Add variable')
-
- fill_variable('key', 'key_value') do
- click_button('Add variable')
- end
-
- wait_for_requests
-
- click_button('Add variable')
-
- fill_variable('key', 'key_value') do
- click_button('Add variable')
- end
-
- wait_for_requests
-
- expect(find('.flash-container')).to be_present
- expect(find('[data-testid="alert-danger"]').text).to have_content('(key) has already been taken')
- end
-
- it 'allows variable to be added even if no value is provided' do
- click_button('Add variable')
-
- page.within('#add-ci-variable') do
- find('[data-testid="pipeline-form-ci-variable-key"] input').set('empty_mask_key')
-
- expect(find_button('Add variable', disabled: false)).to be_present
- end
- end
-
- it 'shows validation error box about unmaskable values' do
- click_button('Add variable')
-
- fill_variable('empty_mask_key', '???', protected: true, masked: true) do
- expect(page).to have_content('This variable value does not meet the masking requirements.')
- expect(find_button('Add variable', disabled: true)).to be_present
- end
- end
-
- it 'handles multiple edits and a deletion' do
- # Create two variables
- click_button('Add variable')
-
- fill_variable('akey', 'akeyvalue') do
- click_button('Add variable')
- end
-
- wait_for_requests
-
- click_button('Add variable')
-
- fill_variable('zkey', 'zkeyvalue') do
- click_button('Add variable')
- end
-
- wait_for_requests
-
- expect(page).to have_selector('.js-ci-variable-row', count: 3)
-
- # Remove the `akey` variable
- page.within('[data-testid="ci-variable-table"]') do
- page.within('.js-ci-variable-row:first-child') do
- click_button('Edit')
- end
- end
-
- page.within('#add-ci-variable') do
- click_button('Delete variable')
- end
-
- wait_for_requests
-
- # Add another variable
- click_button('Add variable')
-
- fill_variable('ckey', 'ckeyvalue') do
- click_button('Add variable')
- end
-
- wait_for_requests
-
- # expect to find 3 rows of variables in alphabetical order
- expect(page).to have_selector('.js-ci-variable-row', count: 3)
- rows = all('.js-ci-variable-row')
- expect(rows[0].find('td[data-label="Key"]')).to have_content('ckey')
- expect(rows[1].find('td[data-label="Key"]')).to have_content('test_key')
- expect(rows[2].find('td[data-label="Key"]')).to have_content('zkey')
- end
-
- context 'defaults to the application setting' do
- context 'application setting is true' do
- before do
- stub_application_setting(protected_ci_variables: true)
-
- visit page_path
- end
-
- it 'defaults to protected' do
- click_button('Add variable')
-
- page.within('#add-ci-variable') do
- expect(find('[data-testid="ci-variable-protected-checkbox"]')).to be_checked
- end
- end
- end
-
- context 'application setting is false' do
- before do
- stub_application_setting(protected_ci_variables: false)
-
- visit page_path
- end
-
- it 'defaults to unprotected' do
- click_button('Add variable')
-
- page.within('#add-ci-variable') do
- expect(find('[data-testid="ci-variable-protected-checkbox"]')).not_to be_checked
- end
- end
-
- it 'does not show a message regarding the default' do
- expect(page).not_to have_content 'Environment variables are configured by your administrator to be protected by default'
- end
- end
- end
-
- def fill_variable(key, value, protected: false, masked: false)
- wait_for_requests
-
- page.within('#add-ci-variable') do
- find('[data-testid="pipeline-form-ci-variable-key"] input').set(key)
- find('[data-testid="pipeline-form-ci-variable-value"]').set(value) if value.present?
- find('[data-testid="ci-variable-protected-checkbox"]').click if protected
- find('[data-testid="ci-variable-masked-checkbox"]').click if masked
-
- yield
- end
- end
-end
diff --git a/spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb
index ed885d7a226..dfad11f3170 100644
--- a/spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb
+++ b/spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb
@@ -56,7 +56,7 @@ RSpec.shared_examples 'User creates wiki page' do
click_on("Create page")
end
- expect(page).to have_current_path(%r(one/two/three-test), ignore_query: true)
+ expect(page).to have_current_path(%r{one/two/three-test}, ignore_query: true)
expect(page).to have_link(href: wiki_page_path(wiki, 'one/two/three-test'))
end
diff --git a/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb
index 784de102f4f..a48ff8a5f77 100644
--- a/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb
+++ b/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb
@@ -40,7 +40,7 @@ RSpec.shared_examples 'User updates wiki page' do
click_on('Create page')
end
- expect(page).to have_current_path(%r(one/two/three-test), ignore_query: true)
+ expect(page).to have_current_path(%r{one/two/three-test}, ignore_query: true)
expect(find('.wiki-pages')).to have_content('three')
first(:link, text: 'three').click
@@ -49,7 +49,7 @@ RSpec.shared_examples 'User updates wiki page' do
click_on('Edit')
- expect(page).to have_current_path(%r(one/two/three-test), ignore_query: true)
+ expect(page).to have_current_path(%r{one/two/three-test}, ignore_query: true)
expect(page).to have_content('Edit Page')
fill_in('Content', with: 'Updated Wiki Content')
@@ -149,7 +149,10 @@ RSpec.shared_examples 'User updates wiki page' do
end
end
- it_behaves_like 'edits content using the content editor', { with_expanded_references: false }
+ it_behaves_like 'edits content using the content editor', {
+ with_expanded_references: false,
+ with_quick_actions: false
+ }
it_behaves_like 'inserts diagrams.net diagram using the content editor'
it_behaves_like 'autocompletes items'
end
diff --git a/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb
index 254682e1a3a..3fac7e7093c 100644
--- a/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb
+++ b/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb
@@ -39,12 +39,12 @@ RSpec.shared_examples 'User views a wiki page' do
end
it 'shows the history of a page that has a path' do
- expect(page).to have_current_path(%r(one/two/three-test))
+ expect(page).to have_current_path(%r{one/two/three-test})
first(:link, text: 'three').click
click_on('Page history')
- expect(page).to have_current_path(%r(one/two/three-test))
+ expect(page).to have_current_path(%r{one/two/three-test})
page.within(:css, '.wiki-page-header') do
expect(page).to have_content('History')
@@ -52,7 +52,7 @@ RSpec.shared_examples 'User views a wiki page' do
end
it 'shows an old version of a page', :js do
- expect(page).to have_current_path(%r(one/two/three-test))
+ expect(page).to have_current_path(%r{one/two/three-test})
expect(find('.wiki-pages')).to have_content('three')
first(:link, text: 'three').click
@@ -61,7 +61,7 @@ RSpec.shared_examples 'User views a wiki page' do
click_on('Edit')
- expect(page).to have_current_path(%r(one/two/three-test))
+ expect(page).to have_current_path(%r{one/two/three-test})
expect(page).to have_content('Edit Page')
fill_in('Content', with: 'Updated Wiki Content')
@@ -100,7 +100,7 @@ RSpec.shared_examples 'User views a wiki page' do
click_on('image')
- expect(page).to have_current_path(%r(wikis/#{path}))
+ expect(page).to have_current_path(%r{wikis/#{path}})
end
end
@@ -109,7 +109,7 @@ RSpec.shared_examples 'User views a wiki page' do
click_on('image')
- expect(page).to have_current_path(%r(wikis/#{path}))
+ expect(page).to have_current_path(%r{wikis/#{path}})
expect(page).to have_content('Create New Page')
end
end
@@ -264,7 +264,10 @@ RSpec.shared_examples 'User views a wiki page' do
it 'opens a default wiki page', :js do
visit wiki.container.web_url
- find('.shortcuts-wiki').click
+ within_testid('super-sidebar') do
+ click_button 'Plan'
+ click_link 'Wiki'
+ end
wait_for_svg_to_be_loaded
diff --git a/spec/support/shared_examples/features/work_items_shared_examples.rb b/spec/support/shared_examples/features/work_items_shared_examples.rb
index ff79f180c64..30605c81312 100644
--- a/spec/support/shared_examples/features/work_items_shared_examples.rb
+++ b/spec/support/shared_examples/features/work_items_shared_examples.rb
@@ -8,7 +8,6 @@ RSpec.shared_examples 'work items title' do
find(title_selector).set("Work item title")
find(title_selector).native.send_keys(:return)
-
wait_for_requests
expect(work_item.reload.title).to eq 'Work item title'
@@ -16,54 +15,37 @@ RSpec.shared_examples 'work items title' do
end
RSpec.shared_examples 'work items toggle status button' do
- let(:state_button) { '[data-testid="work-item-state-toggle"]' }
-
it 'successfully shows and changes the status of the work item' do
- expect(find(state_button, match: :first)).to have_content 'Close'
-
- find(state_button, match: :first).click
-
- wait_for_requests
+ click_button 'Close', match: :first
- expect(find(state_button, match: :first)).to have_content 'Reopen'
+ expect(page).to have_button 'Reopen'
expect(work_item.reload.state).to eq('closed')
end
end
RSpec.shared_examples 'work items comments' do |type|
- let(:form_selector) { '[data-testid="work-item-add-comment"]' }
- let(:edit_button) { '[data-testid="edit-work-item-note"]' }
- let(:textarea_selector) { '[data-testid="work-item-add-comment"] #work-item-add-or-edit-comment' }
let(:is_mac) { page.evaluate_script('navigator.platform').include?('Mac') }
let(:modifier_key) { is_mac ? :command : :control }
- let(:comment) { 'Test comment' }
def set_comment
- find(form_selector).fill_in(with: comment)
+ fill_in _('Add a reply'), with: 'Test comment'
end
it 'successfully creates and shows comments' do
set_comment
-
click_button "Comment"
- wait_for_requests
-
page.within(".main-notes-list") do
- expect(page).to have_content comment
+ expect(page).to have_text 'Test comment'
end
end
it 'successfully updates existing comments' do
set_comment
click_button "Comment"
- wait_for_all_requests
-
- find(edit_button).click
+ click_button _('Edit comment')
send_keys(" updated")
- click_button "Save comment"
-
- wait_for_all_requests
+ click_button _('Save comment')
page.within(".main-notes-list") do
expect(page).to have_content "Test comment updated"
@@ -79,39 +61,31 @@ RSpec.shared_examples 'work items comments' do |type|
it 'shows work item note actions' do
set_comment
-
send_keys([modifier_key, :enter])
- wait_for_requests
page.within(".main-notes-list") do
- expect(page).to have_content comment
+ expect(page).to have_text 'Test comment'
end
page.within('.timeline-entry.note.note-wrapper.note-comment:last-child') do
- expect(page).to have_selector('[data-testid="work-item-note-actions"]')
+ click_button _('More actions')
- find('[data-testid="work-item-note-actions"]').click
-
- expect(page).to have_selector('[data-testid="copy-link-action"]')
- expect(page).to have_selector('[data-testid="assign-note-action"]')
- expect(page).to have_selector('[data-testid="delete-note-action"]')
- expect(page).to have_selector('[data-testid="edit-work-item-note"]')
+ expect(page).to have_button _('Copy link')
+ expect(page).to have_button _('Assign to commenting user')
+ expect(page).to have_button _('Delete comment')
+ expect(page).to have_button _('Edit comment')
end
end
end
it 'successfully posts comments using shortcut and checks if textarea is blank when reinitiated' do
set_comment
-
send_keys([modifier_key, :enter])
- wait_for_requests
-
page.within(".main-notes-list") do
- expect(page).to have_content comment
+ expect(page).to have_content 'Test comment'
end
-
- expect(find(textarea_selector)).to have_content ""
+ expect(page).to have_field _('Add a reply'), with: ''
end
context 'when using quick actions' do
@@ -158,9 +132,7 @@ RSpec.shared_examples 'work items comments' do |type|
end
def click_reply_and_enter_slash
- find(form_selector).fill_in(with: "/")
-
- wait_for_all_requests
+ fill_in _('Add a reply'), with: '/'
end
end
end
@@ -171,7 +143,6 @@ RSpec.shared_examples 'work items assignees' do
# The button is only when the mouse is over the input
find('[data-testid="work-item-assignees-input"]').fill_in(with: user.username)
wait_for_requests
-
# submit and simulate blur to save
send_keys(:enter)
find("body").click
@@ -182,21 +153,19 @@ RSpec.shared_examples 'work items assignees' do
it 'successfully assigns the current user by clicking `Assign myself` button' do
find('[data-testid="work-item-assignees-input"]').hover
- find('[data-testid="assign-self"]').click
- wait_for_requests
+ click_button _('Assign yourself')
expect(work_item.reload.assignees).to include(user)
end
it 'successfully removes all users on clear all button click' do
find('[data-testid="work-item-assignees-input"]').hover
- find('[data-testid="assign-self"]').click
- wait_for_requests
+ click_button _('Assign yourself')
expect(work_item.reload.assignees).to include(user)
find('[data-testid="work-item-assignees-input"]').click
- find('[data-testid="clear-all-button"]').click
+ click_button 'Clear all'
find("body").click
wait_for_requests
@@ -205,13 +174,12 @@ RSpec.shared_examples 'work items assignees' do
it 'successfully removes user on clicking badge cross button' do
find('[data-testid="work-item-assignees-input"]').hover
- find('[data-testid="assign-self"]').click
- wait_for_requests
+ click_button _('Assign yourself')
expect(work_item.reload.assignees).to include(user)
within('[data-testid="work-item-assignees-input"]') do
- find('[data-testid="close-icon"]').click
+ click_button 'Close'
end
find("body").click
wait_for_requests
@@ -228,11 +196,9 @@ RSpec.shared_examples 'work items assignees' do
end
find('[data-testid="work-item-assignees-input"]').hover
- find('[data-testid="assign-self"]').click
- wait_for_requests
+ click_button _('Assign yourself')
expect(work_item.reload.assignees).to include(user)
-
using_session :other_session do
expect(work_item.reload.assignees).to include(user)
end
@@ -246,7 +212,6 @@ RSpec.shared_examples 'work items labels' do
it 'successfully assigns a label' do
find(labels_input_selector).fill_in(with: label.title)
wait_for_requests
-
# submit and simulate blur to save
send_keys(:enter)
find(label_title_selector).click
@@ -276,7 +241,6 @@ RSpec.shared_examples 'work items labels' do
it 'removes all labels on clear all button click' do
find(labels_input_selector).fill_in(with: label.title)
wait_for_requests
-
send_keys(:enter)
find(label_title_selector).click
wait_for_requests
@@ -285,9 +249,8 @@ RSpec.shared_examples 'work items labels' do
within(labels_input_selector) do
find('input').click
- find('[data-testid="clear-all-button"]').click
+ click_button 'Clear all'
end
-
find(label_title_selector).click
wait_for_requests
@@ -297,7 +260,6 @@ RSpec.shared_examples 'work items labels' do
it 'removes label on clicking badge cross button' do
find(labels_input_selector).fill_in(with: label.title)
wait_for_requests
-
send_keys(:enter)
find(label_title_selector).click
wait_for_requests
@@ -305,9 +267,8 @@ RSpec.shared_examples 'work items labels' do
expect(page).to have_text(label.title)
within(labels_input_selector) do
- find('[data-testid="close-icon"]').click
+ click_button 'Remove label'
end
-
find(label_title_selector).click
wait_for_requests
@@ -324,7 +285,6 @@ RSpec.shared_examples 'work items labels' do
find(labels_input_selector).fill_in(with: label.title)
wait_for_requests
-
send_keys(:enter)
find(label_title_selector).click
wait_for_requests
@@ -341,10 +301,7 @@ end
RSpec.shared_examples 'work items description' do
it 'shows GFM autocomplete', :aggregate_failures do
click_button "Edit description"
-
- find('[aria-label="Description"]').send_keys("@#{user.username}")
-
- wait_for_requests
+ fill_in _('Description'), with: "@#{user.username}"
page.within('.atwho-container') do
expect(page).to have_text(user.name)
@@ -353,10 +310,7 @@ RSpec.shared_examples 'work items description' do
it 'autocompletes available quick actions', :aggregate_failures do
click_button "Edit description"
-
- find('[aria-label="Description"]').send_keys("/")
-
- wait_for_requests
+ fill_in _('Description'), with: '/'
page.within('#at-view-commands') do
expect(page).to have_text("title")
@@ -378,8 +332,6 @@ RSpec.shared_examples 'work items description' do
it 'shows conflict message when description changes', :aggregate_failures do
click_button "Edit description"
- wait_for_requests
-
::WorkItems::UpdateService.new(
container: work_item.project,
current_user: other_user,
@@ -388,11 +340,11 @@ RSpec.shared_examples 'work items description' do
wait_for_requests
- find('[aria-label="Description"]').send_keys("oh yeah!")
+ fill_in _('Description'), with: 'oh yeah!'
- expect(page.find('[data-testid="work-item-description-conflicts"]')).to have_text(expected_warning)
+ expect(page).to have_text(expected_warning)
- click_button "Save and overwrite"
+ click_button s_('WorkItem|Save and overwrite')
expect(page.find('[data-testid="work-item-description"]')).to have_text("oh yeah!")
end
@@ -410,32 +362,23 @@ RSpec.shared_examples 'work items invite members' do
click_button('Invite members')
page.within invite_modal_selector do
- expect(page).to have_content("You're inviting members to the #{work_item.project.name} project")
+ expect(page).to have_text("You're inviting members to the #{work_item.project.name} project")
end
end
end
RSpec.shared_examples 'work items milestone' do
- def set_milestone(milestone_dropdown, milestone_text)
- milestone_dropdown.click
-
- find('[data-testid="work-item-milestone-dropdown"] .gl-form-input', visible: true).send_keys "\"#{milestone_text}\""
- wait_for_requests
-
- click_button(milestone_text)
- wait_for_requests
- end
-
- let(:milestone_dropdown_selector) { '[data-testid="work-item-milestone-dropdown"]' }
-
it 'searches and sets or removes milestone for the work item' do
- set_milestone(find(milestone_dropdown_selector), milestone.title)
+ click_button s_('WorkItem|Add to milestone')
+ send_keys "\"#{milestone.title}\""
+ select_listbox_item(milestone.title, exact_text: true)
- expect(page.find(milestone_dropdown_selector)).to have_text(milestone.title)
+ expect(page).to have_button(milestone.title)
- set_milestone(find(milestone_dropdown_selector), 'No milestone')
+ click_button milestone.title
+ select_listbox_item(s_('WorkItem|No milestone'), exact_text: true)
- expect(page.find(milestone_dropdown_selector)).to have_text('Add to milestone')
+ expect(page).to have_button(s_('WorkItem|Add to milestone'))
end
end
@@ -443,70 +386,52 @@ RSpec.shared_examples 'work items comment actions for guest users' do
context 'for guest user' do
it 'hides other actions other than copy link' do
page.within(".main-notes-list") do
- expect(page).to have_selector('[data-testid="work-item-note-actions"]')
+ click_button _('More actions'), match: :first
- find('[data-testid="work-item-note-actions"]', match: :first).click
- expect(page).to have_selector('[data-testid="copy-link-action"]')
- expect(page).not_to have_selector('[data-testid="assign-note-action"]')
+ expect(page).to have_button _('Copy link')
+ expect(page).not_to have_button _('Assign to commenting user')
end
end
end
end
RSpec.shared_examples 'work items notifications' do
- let(:actions_dropdown_selector) { '[data-testid="work-item-actions-dropdown"]' }
- let(:notifications_toggle_selector) { '[data-testid="notifications-toggle-action"] button[role="switch"]' }
-
it 'displays toast when notification is toggled' do
- find(actions_dropdown_selector).click
+ click_button _('More actions'), match: :first
- page.within('[data-testid="notifications-toggle-form"]') do
- expect(page).not_to have_css(".is-checked")
+ within_testid('notifications-toggle-form') do
+ expect(page).not_to have_button(class: 'gl-toggle is-checked')
- find(notifications_toggle_selector).click
- wait_for_requests
+ click_button(class: 'gl-toggle')
- expect(page).to have_css(".is-checked")
+ expect(page).to have_button(class: 'gl-toggle is-checked')
end
- page.within('.gl-toast') do
- expect(find('.toast-body')).to have_content(_('Notifications turned on.'))
- end
+ expect(page).to have_css('.gl-toast', text: _('Notifications turned on.'))
end
end
RSpec.shared_examples 'work items todos' do
- let(:todos_action_selector) { '[data-testid="work-item-todos-action"]' }
- let(:todos_icon_selector) { '[data-testid="work-item-todos-icon"]' }
- let(:header_section_selector) { '[data-testid="work-item-body"]' }
-
- def toggle_todo_action
- find(todos_action_selector).click
- wait_for_requests
- end
-
it 'adds item to the list' do
- page.within(header_section_selector) do
- expect(find(todos_action_selector)['aria-label']).to eq('Add a to do')
+ expect(page).to have_button s_('WorkItem|Add a to do')
- toggle_todo_action
+ click_button s_('WorkItem|Add a to do')
- expect(find(todos_action_selector)['aria-label']).to eq('Mark as done')
- end
+ expect(page).to have_button s_('WorkItem|Mark as done')
- page.within ".header-content span[aria-label='#{_('Todos count')}']" do
+ within_testid('todos-shortcut-button') do
expect(page).to have_content '1'
end
end
it 'marks a todo as done' do
- page.within(header_section_selector) do
- toggle_todo_action
- toggle_todo_action
- end
+ click_button s_('WorkItem|Add a to do')
+ click_button s_('WorkItem|Mark as done')
- expect(find(todos_action_selector)['aria-label']).to eq('Add a to do')
- expect(page).to have_selector(".header-content span[aria-label='#{_('Todos count')}']", visible: :hidden)
+ expect(page).to have_button s_('WorkItem|Add a to do')
+ within_testid('todos-shortcut-button') do
+ expect(page).to have_content("")
+ end
end
end
@@ -514,8 +439,7 @@ RSpec.shared_examples 'work items award emoji' do
let(:award_section_selector) { '.awards' }
let(:award_button_selector) { '[data-testid="award-button"]' }
let(:selected_award_button_selector) { '[data-testid="award-button"].selected' }
- let(:emoji_picker_button_selector) { '[data-testid="emoji-picker"]' }
- let(:basketball_emoji_selector) { 'gl-emoji[data-name="basketball"]' }
+ let(:grinning_emoji_selector) { 'gl-emoji[data-name="grinning"]' }
let(:tooltip_selector) { '.gl-tooltip' }
def select_emoji
@@ -560,10 +484,41 @@ RSpec.shared_examples 'work items award emoji' do
it 'add custom award to the work item for current user' do
within(award_section_selector) do
- find(emoji_picker_button_selector).click
- find(basketball_emoji_selector).click
+ click_button _('Add reaction')
+ find(grinning_emoji_selector).click
+
+ expect(page).to have_selector(grinning_emoji_selector)
+ end
+ end
+end
+
+RSpec.shared_examples 'work items parent' do |type|
+ let(:work_item_parent) { create(:work_item, type, project: project) }
+
+ def set_parent(parent_dropdown, parent_text)
+ parent_dropdown.click
+
+ find('[data-testid="listbox-search-input"] .gl-listbox-search-input',
+ visible: true).send_keys "\"#{parent_text}\""
+ wait_for_requests
+
+ find('.gl-new-dropdown-item').click
+ wait_for_requests
+ end
+
+ let(:parent_dropdown_selector) { 'work-item-parent-listbox' }
+
+ it 'searches and sets or removes parent for the work item' do
+ within_testid('work-item-parent-form') do
+ set_parent(find_by_testid(parent_dropdown_selector), work_item_parent.title)
+
+ expect(find_by_testid(parent_dropdown_selector)).to have_text(work_item_parent.title)
+
+ find_by_testid(parent_dropdown_selector).click
+
+ click_button('Unassign')
- expect(page).to have_selector(basketball_emoji_selector)
+ expect(find_by_testid(parent_dropdown_selector)).to have_text('None')
end
end
end
diff --git a/spec/support/shared_examples/finders/issues_finder_shared_examples.rb b/spec/support/shared_examples/finders/issues_finder_shared_examples.rb
index ed8feebf1f6..043d6db66d3 100644
--- a/spec/support/shared_examples/finders/issues_finder_shared_examples.rb
+++ b/spec/support/shared_examples/finders/issues_finder_shared_examples.rb
@@ -956,7 +956,7 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context
end
context 'multiple params' do
- let(:params) { { issue_types: %w(issue incident) } }
+ let(:params) { { issue_types: %w[issue incident] } }
it 'returns all items' do
expect(items).to contain_exactly(incident_item, item1, item2, item3, item4, item5)
diff --git a/spec/support/shared_examples/graphql/notes_quick_actions_for_work_items_shared_examples.rb b/spec/support/shared_examples/graphql/notes_quick_actions_for_work_items_shared_examples.rb
index 0577ac329e6..9af9aaef483 100644
--- a/spec/support/shared_examples/graphql/notes_quick_actions_for_work_items_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/notes_quick_actions_for_work_items_shared_examples.rb
@@ -70,7 +70,7 @@ RSpec.shared_examples 'work item supports labels widget updates via quick action
let(:add_label_ids) { [] }
let(:remove_label_ids) { [] }
- before_all do
+ before do
noteable.update!(labels: [existing_label])
end
diff --git a/spec/support/shared_examples/graphql/resolvers/data_transfer_resolver_shared_examples.rb b/spec/support/shared_examples/graphql/resolvers/data_transfer_resolver_shared_examples.rb
index 8551bd052ce..c50e0434eb1 100644
--- a/spec/support/shared_examples/graphql/resolvers/data_transfer_resolver_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/resolvers/data_transfer_resolver_shared_examples.rb
@@ -1,16 +1,6 @@
# frozen_string_literal: true
RSpec.shared_examples 'Data transfer resolver' do
- it 'returns mock data' do |_query_object|
- mocked_data = ['mocked_data']
-
- allow_next_instance_of(DataTransfer::MockedTransferFinder) do |instance|
- allow(instance).to receive(:execute).and_return(mocked_data)
- end
-
- expect(resolve_egress[:egress_nodes]).to eq(mocked_data)
- end
-
context 'when data_transfer_monitoring is disabled' do
before do
stub_feature_flags(data_transfer_monitoring: false)
diff --git a/spec/support/shared_examples/graphql/resolvers/packages_resolvers_shared_examples.rb b/spec/support/shared_examples/graphql/resolvers/packages_resolvers_shared_examples.rb
index aadc061f175..98eadc507d7 100644
--- a/spec/support/shared_examples/graphql/resolvers/packages_resolvers_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/resolvers/packages_resolvers_shared_examples.rb
@@ -52,6 +52,18 @@ RSpec.shared_examples 'group and projects packages resolver' do
it { is_expected.to eq([conan_package]) }
end
+ context 'filter by package_version' do
+ let(:args) { { package_version: '1.0.0', sort: 'CREATED_DESC' } }
+
+ it { is_expected.to eq([conan_package]) }
+
+ it 'includes_versionless has no effect' do
+ args[:include_versionless] = true
+
+ is_expected.to eq([conan_package])
+ end
+ end
+
context 'filter by status' do
let(:args) { { status: 'error', sort: 'CREATED_DESC' } }
diff --git a/spec/support/shared_examples/graphql/resolvers/users/pages_visits_resolvers_shared_examples.rb b/spec/support/shared_examples/graphql/resolvers/users/pages_visits_resolvers_shared_examples.rb
new file mode 100644
index 00000000000..0dca28a4e74
--- /dev/null
+++ b/spec/support/shared_examples/graphql/resolvers/users/pages_visits_resolvers_shared_examples.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'namespace visits resolver' do
+ include GraphqlHelpers
+
+ describe '#resolve' do
+ context 'when user is not logged in' do
+ let_it_be(:current_user) { nil }
+
+ it 'returns nil' do
+ expect(resolve_items).to eq(nil)
+ end
+ end
+
+ context 'when user is logged in' do
+ let_it_be(:current_user) { create(:user) }
+
+ context 'when the frecent_namespaces_suggestions feature flag is disabled' do
+ before do
+ stub_feature_flags(frecent_namespaces_suggestions: false)
+ end
+
+ it 'raises a "Resource not available" exception' do
+ expect(resolve_items).to be_a(::Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
+ end
+
+ it 'returns frecent groups' do
+ expect(resolve_items).to be_an_instance_of(Array)
+ end
+ end
+ end
+
+ private
+
+ def resolve_items
+ sync(resolve(described_class, ctx: { current_user: current_user }))
+ end
+end
diff --git a/spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb b/spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb
index c32e758d921..7a3b3d6924c 100644
--- a/spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb
@@ -28,6 +28,7 @@ RSpec.shared_examples "a user type with merge request interaction type" do
authoredMergeRequests
assignedMergeRequests
reviewRequestedMergeRequests
+ organizations
groupMemberships
groupCount
projectMemberships
@@ -50,6 +51,7 @@ RSpec.shared_examples "a user type with merge request interaction type" do
organization
jobTitle
createdAt
+ lastActivityOn
pronouns
ide
]
diff --git a/spec/support/shared_examples/lib/banzai/filters/sanitization_filter_shared_examples.rb b/spec/support/shared_examples/lib/banzai/filters/sanitization_filter_shared_examples.rb
index e6433f963f4..e335255e426 100644
--- a/spec/support/shared_examples/lib/banzai/filters/sanitization_filter_shared_examples.rb
+++ b/spec/support/shared_examples/lib/banzai/filters/sanitization_filter_shared_examples.rb
@@ -2,14 +2,14 @@
RSpec.shared_examples 'default allowlist' do
it 'sanitizes tags that are not allowed' do
- act = %q{<textarea>no inputs</textarea> and <blink>no blinks</blink>}
+ act = %q(<textarea>no inputs</textarea> and <blink>no blinks</blink>)
exp = 'no inputs and no blinks'
expect(filter(act).to_html).to eq exp
end
it 'sanitizes tag attributes' do
- act = %q{<a href="http://example.com/bar.html" onclick="bar">Text</a>}
- exp = %q{<a href="http://example.com/bar.html">Text</a>}
+ act = %q(<a href="http://example.com/bar.html" onclick="bar">Text</a>)
+ exp = %q(<a href="http://example.com/bar.html">Text</a>)
expect(filter(act).to_html).to eq exp
end
@@ -31,13 +31,13 @@ RSpec.shared_examples 'default allowlist' do
end
it 'sanitizes `class` attribute on any element' do
- act = %q{<strong class="foo">Strong</strong>}
- expect(filter(act).to_html).to eq %q{<strong>Strong</strong>}
+ act = %q(<strong class="foo">Strong</strong>)
+ expect(filter(act).to_html).to eq %q(<strong>Strong</strong>)
end
it 'sanitizes `id` attribute on any element' do
- act = %q{<em id="foo">Emphasis</em>}
- expect(filter(act).to_html).to eq %q{<em>Emphasis</em>}
+ act = %q(<em id="foo">Emphasis</em>)
+ expect(filter(act).to_html).to eq %q(<em>Emphasis</em>)
end
end
@@ -150,8 +150,8 @@ end
RSpec.shared_examples 'sanitize link' do
it 'removes `rel` attribute from `a` elements' do
- act = %q{<a href="#" rel="nofollow">Link</a>}
- exp = %q{<a href="#">Link</a>}
+ act = %q(<a href="#" rel="nofollow">Link</a>)
+ exp = %q(<a href="#">Link</a>)
expect(filter(act).to_html).to eq exp
end
@@ -167,14 +167,14 @@ RSpec.shared_examples 'sanitize link' do
end
it 'allows non-standard anchor schemes' do
- exp = %q{<a href="irc://irc.freenode.net/git">IRC</a>}
+ exp = %q(<a href="irc://irc.freenode.net/git">IRC</a>)
act = filter(exp)
expect(act.to_html).to eq exp
end
it 'allows relative links' do
- exp = %q{<a href="foo/bar.md">foo/bar.md</a>}
+ exp = %q(<a href="foo/bar.md">foo/bar.md</a>)
act = filter(exp)
expect(act.to_html).to eq exp
diff --git a/spec/support/shared_examples/lib/gitlab/import/advance_stage_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/import/advance_stage_shared_examples.rb
index effa6a6f6f0..c172e73ce9e 100644
--- a/spec/support/shared_examples/lib/gitlab/import/advance_stage_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/import/advance_stage_shared_examples.rb
@@ -4,7 +4,7 @@ RSpec.shared_examples Gitlab::Import::AdvanceStage do |factory:|
let_it_be(:project) { create(:project) }
let_it_be_with_reload(:import_state) { create(factory, :started, project: project, jid: '123') }
let(:worker) { described_class.new }
- let(:next_stage) { :finish }
+ let(:next_stage) { 'finish' }
describe '#perform', :clean_gitlab_redis_shared_state do
context 'when the project no longer exists' do
@@ -60,7 +60,7 @@ RSpec.shared_examples Gitlab::Import::AdvanceStage do |factory:|
end
it 'schedules the next stage' do
- next_worker = described_class::STAGES[next_stage]
+ next_worker = described_class::STAGES[next_stage.to_sym]
expect_next_found_instance_of(import_state.class) do |state|
expect(state).to receive(:refresh_jid_expiration)
@@ -72,7 +72,7 @@ RSpec.shared_examples Gitlab::Import::AdvanceStage do |factory:|
end
it 'raises KeyError when the stage name is invalid' do
- expect { worker.perform(project.id, { '123' => 2 }, :kittens) }
+ expect { worker.perform(project.id, { '123' => 2 }, 'kittens') }
.to raise_error(KeyError)
end
end
@@ -106,7 +106,7 @@ RSpec.shared_examples Gitlab::Import::AdvanceStage do |factory:|
it 'advances to next stage' do
freeze_time do
- next_worker = described_class::STAGES[next_stage]
+ next_worker = described_class::STAGES[next_stage.to_sym]
expect(next_worker).to receive(:perform_async).with(project.id)
@@ -122,7 +122,7 @@ RSpec.shared_examples Gitlab::Import::AdvanceStage do |factory:|
it 'logs error and fails import' do
freeze_time do
- next_worker = described_class::STAGES[next_stage]
+ next_worker = described_class::STAGES[next_stage.to_sym]
expect(next_worker).not_to receive(:perform_async).with(project.id)
expect_next_instance_of(described_class) do |klass|
diff --git a/spec/support/shared_examples/lib/gitlab/middleware/multipart_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/middleware/multipart_shared_examples.rb
index 16b048ae325..c17d022293d 100644
--- a/spec/support/shared_examples/lib/gitlab/middleware/multipart_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/middleware/multipart_shared_examples.rb
@@ -8,7 +8,7 @@ RSpec.shared_examples 'handling all upload parameters conditions' do
let(:params) { upload_parameters_for(filepath: uploaded_filepath, key: 'file', mode: mode, filename: filename, remote_id: remote_id) }
it 'builds an UploadedFile' do
- expect_uploaded_files(filepath: uploaded_filepath, original_filename: filename, remote_id: remote_id, size: uploaded_file.size, params_path: %w(file))
+ expect_uploaded_files(filepath: uploaded_filepath, original_filename: filename, remote_id: remote_id, size: uploaded_file.size, params_path: %w[file])
subject
end
@@ -27,8 +27,8 @@ RSpec.shared_examples 'handling all upload parameters conditions' do
it 'builds UploadedFiles' do
expect_uploaded_files(
[
- { filepath: uploaded_filepath, original_filename: filename, remote_id: remote_id, size: uploaded_file.size, params_path: %w(file1) },
- { filepath: uploaded_filepath2, original_filename: filename2, remote_id: remote_id2, size: uploaded_file2.size, params_path: %w(file2) }
+ { filepath: uploaded_filepath, original_filename: filename, remote_id: remote_id, size: uploaded_file.size, params_path: %w[file1] },
+ { filepath: uploaded_filepath2, original_filename: filename2, remote_id: remote_id2, size: uploaded_file2.size, params_path: %w[file2] }
]
)
@@ -43,7 +43,7 @@ RSpec.shared_examples 'handling all upload parameters conditions' do
let(:params) { { 'user' => { 'avatar' => upload_parameters_for(filepath: uploaded_filepath, mode: mode, filename: filename, remote_id: remote_id) } } }
it 'builds an UploadedFile' do
- expect_uploaded_files(filepath: uploaded_filepath, original_filename: filename, remote_id: remote_id, size: uploaded_file.size, params_path: %w(user avatar))
+ expect_uploaded_files(filepath: uploaded_filepath, original_filename: filename, remote_id: remote_id, size: uploaded_file.size, params_path: %w[user avatar])
subject
end
@@ -65,8 +65,8 @@ RSpec.shared_examples 'handling all upload parameters conditions' do
it 'builds UploadedFiles' do
expect_uploaded_files(
[
- { filepath: uploaded_filepath, original_filename: filename, remote_id: remote_id, size: uploaded_file.size, params_path: %w(user avatar) },
- { filepath: uploaded_filepath2, original_filename: filename2, remote_id: remote_id2, size: uploaded_file2.size, params_path: %w(user screenshot) }
+ { filepath: uploaded_filepath, original_filename: filename, remote_id: remote_id, size: uploaded_file.size, params_path: %w[user avatar] },
+ { filepath: uploaded_filepath2, original_filename: filename2, remote_id: remote_id2, size: uploaded_file2.size, params_path: %w[user screenshot] }
]
)
@@ -81,7 +81,7 @@ RSpec.shared_examples 'handling all upload parameters conditions' do
let(:params) { { 'user' => { 'avatar' => { 'bananas' => upload_parameters_for(filepath: uploaded_filepath, mode: mode, filename: filename, remote_id: remote_id) } } } }
it 'builds an UploadedFile' do
- expect_uploaded_files(filepath: uploaded_file, original_filename: filename, remote_id: remote_id, size: uploaded_file.size, params_path: %w(user avatar bananas))
+ expect_uploaded_files(filepath: uploaded_file, original_filename: filename, remote_id: remote_id, size: uploaded_file.size, params_path: %w[user avatar bananas])
subject
end
@@ -107,8 +107,8 @@ RSpec.shared_examples 'handling all upload parameters conditions' do
it 'builds UploadedFiles' do
expect_uploaded_files(
[
- { filepath: uploaded_file, original_filename: filename, remote_id: remote_id, size: uploaded_file.size, params_path: %w(user avatar bananas) },
- { filepath: uploaded_file2, original_filename: filename2, remote_id: remote_id2, size: uploaded_file2.size, params_path: %w(user friend ananas) }
+ { filepath: uploaded_file, original_filename: filename, remote_id: remote_id, size: uploaded_file.size, params_path: %w[user avatar bananas] },
+ { filepath: uploaded_file2, original_filename: filename2, remote_id: remote_id2, size: uploaded_file2.size, params_path: %w[user friend ananas] }
]
)
@@ -141,9 +141,9 @@ RSpec.shared_examples 'handling all upload parameters conditions' do
it 'builds UploadedFiles' do
expect_uploaded_files(
[
- { filepath: uploaded_filepath, original_filename: filename, remote_id: remote_id, size: uploaded_file.size, params_path: %w(file) },
- { filepath: uploaded_filepath2, original_filename: filename2, remote_id: remote_id2, size: uploaded_file2.size, params_path: %w(user avatar) },
- { filepath: uploaded_filepath3, original_filename: filename3, remote_id: remote_id3, size: uploaded_file3.size, params_path: %w(user friend avatar) }
+ { filepath: uploaded_filepath, original_filename: filename, remote_id: remote_id, size: uploaded_file.size, params_path: %w[file] },
+ { filepath: uploaded_filepath2, original_filename: filename2, remote_id: remote_id2, size: uploaded_file2.size, params_path: %w[user avatar] },
+ { filepath: uploaded_filepath3, original_filename: filename3, remote_id: remote_id3, size: uploaded_file3.size, params_path: %w[user friend avatar] }
]
)
diff --git a/spec/support/shared_examples/lib/sbom/package_url_shared_examples.rb b/spec/support/shared_examples/lib/sbom/package_url_shared_examples.rb
new file mode 100644
index 00000000000..84faba20d31
--- /dev/null
+++ b/spec/support/shared_examples/lib/sbom/package_url_shared_examples.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'purl_types enum' do
+ let(:purl_types) do
+ {
+ composer: 1,
+ conan: 2,
+ gem: 3,
+ golang: 4,
+ maven: 5,
+ npm: 6,
+ nuget: 7,
+ pypi: 8,
+ apk: 9,
+ rpm: 10,
+ deb: 11,
+ 'cbl-mariner': 12,
+ wolfi: 13
+ }
+ end
+
+ it { is_expected.to define_enum_for(:purl_type).with_values(purl_types) }
+end
diff --git a/spec/support/shared_examples/lib/wikis_api_examples.rb b/spec/support/shared_examples/lib/wikis_api_examples.rb
index c57ac328a60..162a2b8ea49 100644
--- a/spec/support/shared_examples/lib/wikis_api_examples.rb
+++ b/spec/support/shared_examples/lib/wikis_api_examples.rb
@@ -53,7 +53,7 @@ RSpec.shared_examples_for 'wikis API returns wiki page' do
specify do
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response.size).to eq(5)
+ expect(json_response.size).to eq(6)
expect(json_response.keys).to match_array(expected_keys_with_content)
expect(json_response['content']).to eq(expected_content)
expect(json_response['slug']).to eq(page.slug)
@@ -118,7 +118,7 @@ RSpec.shared_examples_for 'wikis API creates wiki page' do
post(api(url, user), params: payload)
expect(response).to have_gitlab_http_status(:created)
- expect(json_response.size).to eq(5)
+ expect(json_response.size).to eq(6)
expect(json_response.keys).to match_array(expected_keys_with_content)
expect(json_response['content']).to eq(payload[:content])
expect(json_response['slug']).to eq(payload[:title].tr(' ', '-'))
@@ -145,7 +145,7 @@ RSpec.shared_examples_for 'wikis API updates wiki page' do
put(api(url, user), params: payload)
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response.size).to eq(5)
+ expect(json_response.size).to eq(6)
expect(json_response.keys).to match_array(expected_keys_with_content)
expect(json_response['content']).to eq(payload[:content])
expect(json_response['slug']).to eq(payload[:title].tr(' ', '-'))
diff --git a/spec/support/shared_examples/mailers/notify_shared_examples.rb b/spec/support/shared_examples/mailers/notify_shared_examples.rb
index 987060d73b9..f0d89f6ffaa 100644
--- a/spec/support/shared_examples/mailers/notify_shared_examples.rb
+++ b/spec/support/shared_examples/mailers/notify_shared_examples.rb
@@ -230,6 +230,8 @@ end
RSpec.shared_examples 'an email with a labels subscriptions link in its footer' do
it { is_expected.to have_body_text('label subscriptions') }
+
+ it { is_expected.to have_body_text(%(href="#{project_labels_url(project, subscribed: true)}")) }
end
RSpec.shared_examples 'a note email' do
diff --git a/spec/support/shared_examples/metrics/transaction_metrics_with_labels_shared_examples.rb b/spec/support/shared_examples/metrics/transaction_metrics_with_labels_shared_examples.rb
index 286c60f1f4f..3fc52cdd86e 100644
--- a/spec/support/shared_examples/metrics/transaction_metrics_with_labels_shared_examples.rb
+++ b/spec/support/shared_examples/metrics/transaction_metrics_with_labels_shared_examples.rb
@@ -95,7 +95,7 @@ RSpec.shared_examples 'transaction metrics with labels' do
expect(prometheus_metric).to receive(:increment).with(labels.merge(sane: 'yes'), 1)
transaction_obj.increment(:block_labels, 1, sane: 'yes') do
- label_keys %i(sane)
+ label_keys %i[sane]
end
end
@@ -145,7 +145,7 @@ RSpec.shared_examples 'transaction metrics with labels' do
expect(prometheus_metric).to receive(:set).with(labels.merge(sane: 'yes'), 99)
transaction_obj.set(:block_labels_set, 99, sane: 'yes') do
- label_keys %i(sane)
+ label_keys %i[sane]
end
end
@@ -195,7 +195,7 @@ RSpec.shared_examples 'transaction metrics with labels' do
expect(prometheus_metric).to receive(:observe).with(labels.merge(sane: 'yes'), 2.0)
transaction_obj.observe(:block_labels_observe, 2.0, sane: 'yes') do
- label_keys %i(sane)
+ label_keys %i[sane]
end
end
diff --git a/spec/support/shared_examples/models/application_setting_shared_examples.rb b/spec/support/shared_examples/models/application_setting_shared_examples.rb
index 6e7d04d3cba..70179dd7fc7 100644
--- a/spec/support/shared_examples/models/application_setting_shared_examples.rb
+++ b/spec/support/shared_examples/models/application_setting_shared_examples.rb
@@ -246,7 +246,7 @@ RSpec.shared_examples 'application settings examples' do
context 'in FIPS mode', :fips_mode do
it 'excludes DSA from supported key types' do
- expect(setting.allowed_key_types).to contain_exactly(*Gitlab::SSHPublicKey.supported_types - %i(dsa))
+ expect(setting.allowed_key_types).to contain_exactly(*Gitlab::SSHPublicKey.supported_types - %i[dsa])
end
end
@@ -297,7 +297,7 @@ RSpec.shared_examples 'application settings examples' do
describe '#pick_repository_storage' do
before do
- allow(Gitlab.config.repositories.storages).to receive(:keys).and_return(%w(default backup))
+ allow(Gitlab.config.repositories.storages).to receive(:keys).and_return(%w[default backup])
allow(setting).to receive(:repository_storages_weighted).and_return({ 'default' => 20, 'backup' => 80 })
end
@@ -314,13 +314,13 @@ RSpec.shared_examples 'application settings examples' do
using RSpec::Parameterized::TableSyntax
where(:config_storages, :storages, :normalized) do
- %w(default backup) | { 'default' => 0, 'backup' => 100 } | { 'default' => 0.0, 'backup' => 1.0 }
- %w(default backup) | { 'default' => 100, 'backup' => 100 } | { 'default' => 0.5, 'backup' => 0.5 }
- %w(default backup) | { 'default' => 20, 'backup' => 80 } | { 'default' => 0.2, 'backup' => 0.8 }
- %w(default backup) | { 'default' => 0, 'backup' => 0 } | { 'default' => 0.0, 'backup' => 0.0 }
- %w(default) | { 'default' => 0, 'backup' => 100 } | { 'default' => 0.0 }
- %w(default) | { 'default' => 100, 'backup' => 100 } | { 'default' => 1.0 }
- %w(default) | { 'default' => 20, 'backup' => 80 } | { 'default' => 1.0 }
+ %w[default backup] | { 'default' => 0, 'backup' => 100 } | { 'default' => 0.0, 'backup' => 1.0 }
+ %w[default backup] | { 'default' => 100, 'backup' => 100 } | { 'default' => 0.5, 'backup' => 0.5 }
+ %w[default backup] | { 'default' => 20, 'backup' => 80 } | { 'default' => 0.2, 'backup' => 0.8 }
+ %w[default backup] | { 'default' => 0, 'backup' => 0 } | { 'default' => 0.0, 'backup' => 0.0 }
+ %w[default] | { 'default' => 0, 'backup' => 100 } | { 'default' => 0.0 }
+ %w[default] | { 'default' => 100, 'backup' => 100 } | { 'default' => 1.0 }
+ %w[default] | { 'default' => 20, 'backup' => 80 } | { 'default' => 1.0 }
end
with_them do
diff --git a/spec/support/shared_examples/models/concerns/can_move_repository_storage_shared_examples.rb b/spec/support/shared_examples/models/concerns/can_move_repository_storage_shared_examples.rb
index 8deeecea30d..77327e9b539 100644
--- a/spec/support/shared_examples/models/concerns/can_move_repository_storage_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/can_move_repository_storage_shared_examples.rb
@@ -42,6 +42,12 @@ RSpec.shared_examples 'can move repository storage' do
.to change { container.repository_read_only? }
.from(true).to(false)
end
+
+ it 'raises an error when the update fails' do
+ expect(container).to receive(:update_repository_read_only_column).and_return(false)
+
+ expect { container.set_repository_writable! }.to raise_error(ActiveRecord::RecordNotSaved, /Database update failed/)
+ end
end
describe '#reference_counter' do
diff --git a/spec/support/shared_examples/models/concerns/repository_storage_movable_shared_examples.rb b/spec/support/shared_examples/models/concerns/repository_storage_movable_shared_examples.rb
index a9a13ddcd60..d8a8d1e1cea 100644
--- a/spec/support/shared_examples/models/concerns/repository_storage_movable_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/repository_storage_movable_shared_examples.rb
@@ -5,6 +5,19 @@ RSpec.shared_examples 'handles repository moves' do
it { is_expected.to belong_to(:container) }
end
+ describe 'scopes' do
+ describe '.scheduled_or_started' do
+ subject { described_class.scheduled_or_started }
+
+ let!(:initial) { create(repository_storage_factory_key, state: 1) }
+ let!(:scheduled) { create(repository_storage_factory_key, state: 2) }
+ let!(:started) { create(repository_storage_factory_key, state: 3) }
+ let!(:finished) { create(repository_storage_factory_key, state: 4) }
+
+ it { is_expected.to contain_exactly(scheduled, started) }
+ end
+ end
+
describe 'validations' do
it { is_expected.to validate_presence_of(:container) }
it { is_expected.to validate_presence_of(:state) }
@@ -59,9 +72,9 @@ RSpec.shared_examples 'handles repository moves' do
end
context 'when in the default state' do
- subject(:storage_move) { create(repository_storage_factory_key, container: container, destination_storage_name: 'test_second_storage') }
+ let!(:storage_move) { create(repository_storage_factory_key, container: container, destination_storage_name: 'test_second_storage') }
- context 'and transits to scheduled' do
+ context 'and transitions to scheduled' do
it 'triggers the corresponding repository storage worker' do
expect(repository_storage_worker).to receive(:perform_async).with(container.id, 'test_second_storage', storage_move.id)
@@ -77,31 +90,37 @@ RSpec.shared_examples 'handles repository moves' do
it 'does not trigger the corresponding repository storage worker and adds an error' do
expect(repository_storage_worker).not_to receive(:perform_async)
+
storage_move.schedule!
+
expect(storage_move.errors[error_key]).to include('foobar')
end
it 'sets the state to failed' do
expect(storage_move).to receive(:do_fail!).and_call_original
+
storage_move.schedule!
+
expect(storage_move.state_name).to eq(:failed)
+ expect(container).not_to be_repository_read_only
end
end
end
- context 'and transits to started' do
+ context 'and transitions to started' do
it 'does not allow the transition' do
- expect { storage_move.start! }
- .to raise_error(StateMachines::InvalidTransition)
+ expect { storage_move.start! }.to raise_error(StateMachines::InvalidTransition)
end
end
end
context 'when started' do
- subject(:storage_move) { create(repository_storage_factory_key, :started, container: container, destination_storage_name: 'test_second_storage') }
+ let!(:storage_move) { create(repository_storage_factory_key, :started, container: container, destination_storage_name: 'test_second_storage') }
- context 'and transits to replicated' do
+ context 'and transitions to replicated' do
it 'marks the container as writable' do
+ container.set_repository_read_only!
+
storage_move.finish_replication!
expect(container).not_to be_repository_read_only
@@ -109,12 +128,29 @@ RSpec.shared_examples 'handles repository moves' do
it 'updates the updated_at column of the container', :aggregate_failures do
expect { storage_move.finish_replication! }.to change { container.updated_at }
+
expect(storage_move.container.updated_at).to be >= storage_move.updated_at
end
end
- context 'and transits to failed' do
+ context 'and transitions to failed' do
it 'marks the container as writable' do
+ container.set_repository_read_only!
+
+ storage_move.do_fail!
+
+ expect(container).not_to be_repository_read_only
+ end
+ end
+ end
+
+ context 'when replicated' do
+ let!(:storage_move) { create(repository_storage_factory_key, :replicated, container: container, destination_storage_name: 'test_second_storage') }
+
+ context 'and transitions to cleanup_failed' do
+ it 'marks the container as writable' do
+ container.set_repository_read_only!
+
storage_move.do_fail!
expect(container).not_to be_repository_read_only
diff --git a/spec/support/shared_examples/models/diff_positionable_note_shared_examples.rb b/spec/support/shared_examples/models/diff_positionable_note_shared_examples.rb
index eafa589a1d3..a6fcea62aba 100644
--- a/spec/support/shared_examples/models/diff_positionable_note_shared_examples.rb
+++ b/spec/support/shared_examples/models/diff_positionable_note_shared_examples.rb
@@ -48,7 +48,7 @@ RSpec.shared_examples 'a valid diff positionable note' do |factory_on_commit|
end
end
- %i(original_position position change_position).each do |method|
+ %i[original_position position change_position].each do |method|
describe "#{method}=" do
it "doesn't accept non-hash JSON passed as a string" do
subject.send(:"#{method}=", "true")
diff --git a/spec/support/shared_examples/models/project_ci_cd_settings_shared_examples.rb b/spec/support/shared_examples/models/project_ci_cd_settings_shared_examples.rb
index f1af1760e8d..3ecf58168b3 100644
--- a/spec/support/shared_examples/models/project_ci_cd_settings_shared_examples.rb
+++ b/spec/support/shared_examples/models/project_ci_cd_settings_shared_examples.rb
@@ -5,7 +5,7 @@ RSpec.shared_examples 'ci_cd_settings delegation' do
context 'when ci_cd_settings is destroyed but project is not' do
it 'allows methods delegated to ci_cd_settings to be nil', :aggregate_failures do
- attributes = project.ci_cd_settings.attributes.keys - %w(id project_id) - exclude_attributes
+ attributes = project.ci_cd_settings.attributes.keys - %w[id project_id] - exclude_attributes
expect(attributes).to match_array(attributes_with_prefix.keys)
diff --git a/spec/support/shared_examples/models/users/pages_visits_shared_examples.rb b/spec/support/shared_examples/models/users/pages_visits_shared_examples.rb
index 0b3e8516d25..ec7dc1fb8b9 100644
--- a/spec/support/shared_examples/models/users/pages_visits_shared_examples.rb
+++ b/spec/support/shared_examples/models/users/pages_visits_shared_examples.rb
@@ -6,6 +6,10 @@ RSpec.shared_examples 'namespace visits model' do
it { is_expected.to validate_presence_of(:visited_at) }
describe '#visited_around?' do
+ before do
+ described_class.create!(entity_id: entity.id, user_id: user.id, visited_at: base_time)
+ end
+
context 'when the checked time matches a recent visit' do
[-15.minutes, 15.minutes].each do |time_diff|
it 'returns true' do
@@ -24,4 +28,104 @@ RSpec.shared_examples 'namespace visits model' do
end
end
end
+
+ describe '#frecent_visits_scores' do
+ def frecent_visits_scores_to_array(visits)
+ visits.map { |visit| [visit["entity_id"], visit["score"]] }
+ end
+
+ context 'when there is lots of data' do
+ before do
+ create_visit_records
+ end
+
+ it 'returns the frecent items, sorted by their frecency score' do
+ expect(frecent_visits_scores_to_array(described_class.frecent_visits_scores(user_id: user.id,
+ limit: 10))).to eq([[2, 31], [1, 30], [3, 28], [6, 6], [7, 6], [8, 6], [4, 6], [5, 6]])
+ end
+
+ it 'limits the amount of returned entries' do
+ expect(frecent_visits_scores_to_array(described_class.frecent_visits_scores(user_id: user.id,
+ limit: 2))).to eq([
+ [2, 31], [1, 30]
+ ])
+ end
+ end
+
+ context 'when there is few data' do
+ before do
+ [
+ # Multiplier: 4
+ [1, Time.current],
+
+ # Multiplier: 3
+ [2, 2.weeks.ago],
+ [3, 2.weeks.ago],
+
+ # Multiplier: 2
+ [1, 3.weeks.ago],
+ [1, 3.weeks.ago],
+
+ # Multiplier: 1
+ [2, 5.weeks.ago]
+ ].each do |id, datetime|
+ described_class.create!(entity_id: id, user_id: user.id, visited_at: datetime)
+ end
+ end
+
+ it 'returns the frecent items, sorted by their frecency score' do
+ expect(frecent_visits_scores_to_array(described_class.frecent_visits_scores(user_id: user.id,
+ limit: 5))).to eq([
+ [1, 8], # Entity 1 gets a score of (1 * 4) + (2 * 2) = 8
+ [2, 4], # Entity 2 gets a score of (1 * 3) + (1 * 1) = 4
+ [3, 3] # Entity 3 gets a score of 1 * 3 = 3
+ ])
+ end
+ end
+ end
+
+ private
+
+ # rubocop: disable Metrics/AbcSize -- Despite being long, this method is quite straightforward. Splitting it in smaller chunks would likely harm readability more than anything.
+ def create_visit_records
+ [
+ [1, Time.current],
+
+ [2, 1.week.ago],
+ [2, 1.week.ago],
+
+ [2, 2.weeks.ago],
+ [3, 2.weeks.ago],
+ [3, 2.weeks.ago],
+ [4, 2.weeks.ago],
+ [5, 2.weeks.ago],
+ [6, 2.weeks.ago],
+ [7, 2.weeks.ago],
+ [8, 2.weeks.ago],
+
+ [1, 3.weeks.ago],
+ [1, 3.weeks.ago],
+ [3, 3.weeks.ago],
+ [3, 3.weeks.ago],
+
+ [1, 4.weeks.ago],
+ [2, 4.weeks.ago],
+ [2, 4.weeks.ago],
+
+ [3, 7.weeks.ago],
+ [3, 7.weeks.ago],
+
+ [1, 8.weeks.ago],
+ [1, 8.weeks.ago],
+ [1, 8.weeks.ago],
+ [1, 8.weeks.ago],
+
+ [2, 9.weeks.ago],
+ [2, 9.weeks.ago],
+ [2, 9.weeks.ago]
+ ].each do |id, datetime|
+ described_class.create!(entity_id: id, user_id: user.id, visited_at: datetime)
+ end
+ end
+ # rubocop: enable Metrics/AbcSize
end
diff --git a/spec/support/shared_examples/models/wiki_shared_examples.rb b/spec/support/shared_examples/models/wiki_shared_examples.rb
index a0187252108..d731eec5680 100644
--- a/spec/support/shared_examples/models/wiki_shared_examples.rb
+++ b/spec/support/shared_examples/models/wiki_shared_examples.rb
@@ -14,7 +14,7 @@ RSpec.shared_examples 'wiki model' do
subject { wiki }
it 'VALID_USER_MARKUPS contains all valid markups' do
- expect(described_class::VALID_USER_MARKUPS.keys).to match_array(%i(markdown rdoc asciidoc org))
+ expect(described_class::VALID_USER_MARKUPS.keys).to match_array(%i[markdown rdoc asciidoc org])
end
it 'container class includes HasWiki' do
diff --git a/spec/support/shared_examples/path_extraction_shared_examples.rb b/spec/support/shared_examples/path_extraction_shared_examples.rb
index d76348aa26a..5ceaf182d03 100644
--- a/spec/support/shared_examples/path_extraction_shared_examples.rb
+++ b/spec/support/shared_examples/path_extraction_shared_examples.rb
@@ -114,12 +114,12 @@ RSpec.shared_examples 'extracts refs' do
it 'extracts a valid commit SHA' do
expect(extract_ref('f4b14494ef6abf3d144c28e4af0c20143383e062/CHANGELOG')).to eq(
- %w(f4b14494ef6abf3d144c28e4af0c20143383e062 CHANGELOG)
+ %w[f4b14494ef6abf3d144c28e4af0c20143383e062 CHANGELOG]
)
end
it 'falls back to a primitive split for an invalid ref' do
- expect(extract_ref('stable/CHANGELOG')).to eq(%w(stable CHANGELOG))
+ expect(extract_ref('stable/CHANGELOG')).to eq(%w[stable CHANGELOG])
end
it 'extracts the longest matching ref' do
diff --git a/spec/support/shared_examples/quick_actions/commit/tag_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/commit/tag_quick_action_shared_examples.rb
index 14b384b149d..78b667cfe56 100644
--- a/spec/support/shared_examples/quick_actions/commit/tag_quick_action_shared_examples.rb
+++ b/spec/support/shared_examples/quick_actions/commit/tag_quick_action_shared_examples.rb
@@ -5,7 +5,7 @@ RSpec.shared_examples 'tag quick action' do
it 'tags this commit' do
add_note("/tag #{tag_name} #{tag_message}")
- expect(page).to have_content %{Tagged this commit to #{tag_name} with "#{tag_message}".}
+ expect(page).to have_content %(Tagged this commit to #{tag_name} with "#{tag_message}".)
expect(page).to have_content "tagged commit #{truncated_commit_sha}"
expect(page).to have_content tag_name
@@ -21,7 +21,7 @@ RSpec.shared_examples 'tag quick action' do
preview_note("/tag #{tag_name} #{tag_message}")
expect(page).not_to have_content '/tag'
- expect(page).to have_content %{Tags this commit to #{tag_name} with "#{tag_message}"}
+ expect(page).to have_content %(Tags this commit to #{tag_name} with "#{tag_message}")
expect(page).to have_content tag_name
end
end
diff --git a/spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb
index 7cbaf40721a..71a8e2a15ce 100644
--- a/spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb
+++ b/spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb
@@ -54,7 +54,7 @@ RSpec.shared_examples 'close quick action' do |issuable_type|
expect(issuable).to be_closed
end
- context "when current user cannot close #{issuable_type}" do
+ context "when current user cannot close #{issuable_type}", :js do
before do
guest = create(:user)
project.add_guest(guest)
diff --git a/spec/support/shared_examples/quick_actions/issue/create_merge_request_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/create_merge_request_quick_action_shared_examples.rb
index 9dc39c6cf73..27d850b8522 100644
--- a/spec/support/shared_examples/quick_actions/issue/create_merge_request_quick_action_shared_examples.rb
+++ b/spec/support/shared_examples/quick_actions/issue/create_merge_request_quick_action_shared_examples.rb
@@ -41,7 +41,7 @@ RSpec.shared_examples 'create_merge_request quick action' do
expect(created_mr.source_branch).to eq(issue.to_branch_name)
visit project_merge_request_path(project, created_mr)
- expect(page).to have_content %{Draft: Resolve "#{issue.title}"}
+ expect(page).to have_content %(Draft: Resolve "#{issue.title}")
end
it 'creates a merge request using the given branch name' do
@@ -54,7 +54,7 @@ RSpec.shared_examples 'create_merge_request quick action' do
expect(created_mr.source_branch).to eq(branch_name)
visit project_merge_request_path(project, created_mr)
- expect(page).to have_content %{Draft: Resolve "#{issue.title}"}
+ expect(page).to have_content %(Draft: Resolve "#{issue.title}")
end
end
end
diff --git a/spec/support/shared_examples/redis/redis_shared_examples.rb b/spec/support/shared_examples/redis/redis_shared_examples.rb
index 1270efd4701..f184f678283 100644
--- a/spec/support/shared_examples/redis/redis_shared_examples.rb
+++ b/spec/support/shared_examples/redis/redis_shared_examples.rb
@@ -365,6 +365,21 @@ RSpec.shared_examples "redis_shared_examples" do
end
end
+ describe '#secret_file' do
+ context 'when explicitly specified in config file' do
+ it 'returns the absolute path of specified file inside Rails root' do
+ allow(subject).to receive(:raw_config_hash).and_return({ secret_file: '/etc/gitlab/redis_secret.enc' })
+ expect(subject.send(:secret_file)).to eq('/etc/gitlab/redis_secret.enc')
+ end
+ end
+
+ context 'when not explicitly specified' do
+ it 'returns the default path in the encrypted settings shared directory' do
+ expect(subject.send(:secret_file)).to eq(Rails.root.join("shared/encrypted_settings/redis.yaml.enc").to_s)
+ end
+ end
+ end
+
describe "#parse_client_tls_options" do
let(:dummy_certificate) { OpenSSL::X509::Certificate.new }
let(:dummy_key) { OpenSSL::PKey::RSA.new }
diff --git a/spec/support/shared_examples/requests/api/container_repositories_shared_examples.rb b/spec/support/shared_examples/requests/api/container_repositories_shared_examples.rb
index 3ff52166990..e7fdf143887 100644
--- a/spec/support/shared_examples/requests/api/container_repositories_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/container_repositories_shared_examples.rb
@@ -39,8 +39,8 @@ RSpec.shared_examples 'returns tags for allowed users' do |user_type, scope|
let(:url) { "/#{scope}s/#{object.id}/registry/repositories?tags=true" }
before do
- stub_container_registry_tags(repository: root_repository.path, tags: %w(rootA latest), with_manifest: true)
- stub_container_registry_tags(repository: test_repository.path, tags: %w(rootA latest), with_manifest: true)
+ stub_container_registry_tags(repository: root_repository.path, tags: %w[rootA latest], with_manifest: true)
+ stub_container_registry_tags(repository: test_repository.path, tags: %w[rootA latest], with_manifest: true)
end
it 'returns a list of repositories and their tags' do
@@ -64,8 +64,8 @@ RSpec.shared_examples 'returns tags for allowed users' do |user_type, scope|
let(:url) { "/#{scope}s/#{object.id}/registry/repositories?tags_count=true" }
before do
- stub_container_registry_tags(repository: root_repository.path, tags: %w(rootA latest), with_manifest: true)
- stub_container_registry_tags(repository: test_repository.path, tags: %w(rootA latest), with_manifest: true)
+ stub_container_registry_tags(repository: root_repository.path, tags: %w[rootA latest], with_manifest: true)
+ stub_container_registry_tags(repository: test_repository.path, tags: %w[rootA latest], with_manifest: true)
end
it 'returns a list of repositories and their tags_count' do
diff --git a/spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb
index c6e4aba6968..90fa0c98376 100644
--- a/spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb
@@ -5,7 +5,7 @@ RSpec.shared_examples 'graphql issue list request spec' do
let(:fields) do
<<~QUERY
nodes {
- #{all_graphql_fields_for('issues'.classify, excluded: ['relatedMergeRequests'])}
+ #{all_graphql_fields_for('issues'.classify, excluded: %w[relatedMergeRequests productAnalyticsState])}
}
QUERY
end
@@ -26,6 +26,18 @@ RSpec.shared_examples 'graphql issue list request spec' do
issue_b.assignee_ids = another_user.id
end
+ context 'when filtering by state' do
+ context 'when filtering by locked state' do
+ let(:issue_filter_params) { { state: :locked } }
+
+ it 'returns an error message' do
+ post_query
+
+ expect_graphql_errors_to_include(Types::IssuableStateEnum::INVALID_LOCKED_MESSAGE)
+ end
+ end
+ end
+
context 'when filtering by assignees' do
context 'when both assignee_username filters are provided' do
let(:issue_filter_params) do
diff --git a/spec/support/shared_examples/requests/api/graphql/remote_development_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/remote_development_shared_examples.rb
deleted file mode 100644
index 83e22945361..00000000000
--- a/spec/support/shared_examples/requests/api/graphql/remote_development_shared_examples.rb
+++ /dev/null
@@ -1,48 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.shared_examples 'workspaces query in licensed environment and with feature flag on' do
- describe 'when licensed and remote_development_feature_flag feature flag is enabled' do
- before do
- stub_licensed_features(remote_development: true)
-
- post_graphql(query, current_user: current_user)
- end
-
- it_behaves_like 'a working graphql query'
-
- it { is_expected.to match_array(a_hash_including('name' => workspace.name)) }
-
- context 'when user is not authorized' do
- let(:current_user) { create(:user) }
-
- it { is_expected.to eq([]) }
- end
- end
-end
-
-RSpec.shared_examples 'workspaces query in unlicensed environment and with feature flag off' do
- describe 'when remote_development feature is unlicensed' do
- before do
- stub_licensed_features(remote_development: false)
- post_graphql(query, current_user: current_user)
- end
-
- it 'returns an error' do
- expect(subject).to be_nil
- expect_graphql_errors_to_include(/'remote_development' licensed feature is not available/)
- end
- end
-
- describe 'when remote_development_feature_flag feature flag is disabled' do
- before do
- stub_licensed_features(remote_development: true)
- stub_feature_flags(remote_development_feature_flag: false)
- post_graphql(query, current_user: current_user)
- end
-
- it 'returns an error' do
- expect(subject).to be_nil
- expect_graphql_errors_to_include(/'remote_development_feature_flag' feature flag is disabled/)
- end
- end
-end
diff --git a/spec/support/shared_examples/requests/api/graphql/work_item_list_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/work_item_list_shared_examples.rb
index a9c422c8f2d..82f98b883dc 100644
--- a/spec/support/shared_examples/requests/api/graphql/work_item_list_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/graphql/work_item_list_shared_examples.rb
@@ -46,6 +46,14 @@ RSpec.shared_examples 'graphql work item list request spec' do
expect(work_item_ids).to include(closed_work_item.to_global_id.to_s)
end
end
+
+ context 'when filtering by state locked' do
+ let(:item_filter_params) { { state: :locked } }
+
+ it 'return an error message' do
+ expect_graphql_errors_to_include(Types::IssuableStateEnum::INVALID_LOCKED_MESSAGE)
+ end
+ end
end
context 'when filtering by type' do
diff --git a/spec/support/shared_examples/requests/api/helm_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/helm_packages_shared_examples.rb
index 7803f0ff04d..9c20b95eb80 100644
--- a/spec/support/shared_examples/requests/api/helm_packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/helm_packages_shared_examples.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-RSpec.shared_examples 'rejects helm packages access' do |user_type, status|
+RSpec.shared_examples 'rejects helm packages access' do |user_type, status, body|
context "for user type #{user_type}" do
before do
project.send("add_#{user_type}", user) if user_type != :anonymous && user_type != :not_a_member
@@ -15,6 +15,14 @@ RSpec.shared_examples 'rejects helm packages access' do |user_type, status|
expect(response.headers['WWW-Authenticate']).to eq 'Basic realm="GitLab Packages Registry"'
end
end
+
+ if body
+ it 'has the correct body' do
+ subject
+
+ expect(response.body).to eq(body)
+ end
+ end
end
end
diff --git a/spec/support/shared_examples/requests/api/integrations/github_enterprise_jira_dvcs_end_of_life_shared_examples.rb b/spec/support/shared_examples/requests/api/integrations/github_enterprise_jira_dvcs_end_of_life_shared_examples.rb
deleted file mode 100644
index 6799dec7b80..00000000000
--- a/spec/support/shared_examples/requests/api/integrations/github_enterprise_jira_dvcs_end_of_life_shared_examples.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.shared_examples 'a GitHub Enterprise Jira DVCS reversible end of life endpoint' do
- it 'is a reachable endpoint' do
- subject
-
- expect(response).not_to have_gitlab_http_status(:not_found)
- end
-
- context 'when the flag is disabled' do
- before do
- stub_feature_flags(jira_dvcs_end_of_life_amnesty: false)
- end
-
- it 'presents as an endpoint that does not exist' do
- subject
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
-end
diff --git a/spec/support/shared_examples/requests/api/ml/mlflow/mlflow_shared_examples.rb b/spec/support/shared_examples/requests/api/ml/mlflow/mlflow_shared_examples.rb
index 00e50b07909..7978f43610d 100644
--- a/spec/support/shared_examples/requests/api/ml/mlflow/mlflow_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/ml/mlflow/mlflow_shared_examples.rb
@@ -74,6 +74,37 @@ RSpec.shared_examples 'MLflow|shared error cases' do
end
end
+RSpec.shared_examples 'MLflow|shared model registry error cases' do
+ context 'when not authenticated' do
+ let(:headers) { {} }
+
+ it "is Unauthorized" do
+ is_expected.to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ context 'when user does not have access' do
+ let(:access_token) { tokens[:different_user] }
+
+ it "is Not Found" do
+ is_expected.to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when model registry is unavailable' do
+ before do
+ allow(Ability).to receive(:allowed?).and_call_original
+ allow(Ability).to receive(:allowed?)
+ .with(current_user, :read_model_registry, project)
+ .and_return(false)
+ end
+
+ it "is Not Found" do
+ is_expected.to have_gitlab_http_status(:not_found)
+ end
+ end
+end
+
RSpec.shared_examples 'MLflow|Bad Request on missing required' do |keys|
keys.each do |key|
context "when \"#{key}\" is missing" do
diff --git a/spec/support/shared_examples/requests/api/multiple_and_scoped_issue_boards_shared_examples.rb b/spec/support/shared_examples/requests/api/multiple_and_scoped_issue_boards_shared_examples.rb
index d749479544d..fa111ca5811 100644
--- a/spec/support/shared_examples/requests/api/multiple_and_scoped_issue_boards_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/multiple_and_scoped_issue_boards_shared_examples.rb
@@ -5,7 +5,6 @@ RSpec.shared_examples 'multiple and scoped issue boards' do |route_definition|
context 'multiple issue boards' do
before do
- stub_feature_flags(apollo_boards: false)
board_parent.add_reporter(user)
stub_licensed_features(multiple_group_issue_boards: true)
end
diff --git a/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb
index f8e78c8c9b1..c23d514abfc 100644
--- a/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb
@@ -699,6 +699,7 @@ RSpec.shared_examples 'nuget upload endpoint' do |symbol_package: false|
end
context 'when package duplicates are not allowed' do
+ let(:params) { { package: temp_file(file_name, content: File.open(expand_fixture_path('packages/nuget/package.nupkg'))) } }
let(:headers) { basic_auth_header(deploy_token.username, deploy_token.token).merge(workhorse_headers) }
let_it_be(:existing_package) { create(:nuget_package, project: project) }
let_it_be(:metadata) { { package_name: existing_package.name, package_version: existing_package.version } }
@@ -722,14 +723,6 @@ RSpec.shared_examples 'nuget upload endpoint' do |symbol_package: false|
it_behaves_like 'returning response status', :created
end
-
- context 'when nuget_duplicates_option feature flag is disabled' do
- before do
- stub_feature_flags(nuget_duplicates_option: false)
- end
-
- it_behaves_like 'returning response status', :created
- end
end
end
diff --git a/spec/support/shared_examples/requests/api/repository_storage_moves_shared_examples.rb b/spec/support/shared_examples/requests/api/repository_storage_moves_shared_examples.rb
index 3913d29e086..181bab41e09 100644
--- a/spec/support/shared_examples/requests/api/repository_storage_moves_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/repository_storage_moves_shared_examples.rb
@@ -80,56 +80,9 @@ RSpec.shared_examples 'repository_storage_moves API' do |container_type|
end
end
- describe "GET /#{container_type}/:id/repository_storage_moves" do
- let(:container_id) { container.id }
+ shared_examples 'post single container repository storage move' do
let(:url) { "/#{container_type}/#{container_id}/repository_storage_moves" }
-
- it_behaves_like 'get container repository storage move list'
-
- context 'non-existent container' do
- let(:container_id) { non_existing_record_id }
-
- it 'returns not found' do
- get api(url, user, admin_mode: user.admin?)
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
- end
-
- describe "GET /#{container_type}/:id/repository_storage_moves/:repository_storage_move_id" do
let(:container_id) { container.id }
- let(:url) { "/#{container_type}/#{container_id}/repository_storage_moves/#{repository_storage_move_id}" }
-
- it_behaves_like 'get single container repository storage move'
-
- context 'non-existent container' do
- let(:container_id) { non_existing_record_id }
- let(:repository_storage_move_id) { storage_move.id }
-
- it 'returns not found' do
- get api(url, user, admin_mode: user.admin?)
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
- end
-
- describe "GET /#{container_type.singularize}_repository_storage_moves" do
- it_behaves_like 'get container repository storage move list' do
- let(:url) { "/#{container_type.singularize}_repository_storage_moves" }
- end
- end
-
- describe "GET /#{container_type.singularize}_repository_storage_moves/:repository_storage_move_id" do
- it_behaves_like 'get single container repository storage move' do
- let(:url) { "/#{container_type.singularize}_repository_storage_moves/#{repository_storage_move_id}" }
- end
- end
-
- describe "POST /#{container_type}/:id/repository_storage_moves", :aggregate_failures do
- let(:container_id) { container.id }
- let(:url) { "/#{container_type}/#{container_id}/repository_storage_moves" }
let(:destination_storage_name) { 'test_second_storage' }
def create_container_repository_storage_move
@@ -186,6 +139,57 @@ RSpec.shared_examples 'repository_storage_moves API' do |container_type|
end
end
+ describe "GET /#{container_type}/:id/repository_storage_moves" do
+ let(:container_id) { container.id }
+ let(:url) { "/#{container_type}/#{container_id}/repository_storage_moves" }
+
+ it_behaves_like 'get container repository storage move list'
+
+ context 'non-existent container' do
+ let(:container_id) { non_existing_record_id }
+
+ it 'returns not found' do
+ get api(url, user, admin_mode: user.admin?)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ describe "GET /#{container_type}/:id/repository_storage_moves/:repository_storage_move_id" do
+ let(:container_id) { container.id }
+ let(:url) { "/#{container_type}/#{container_id}/repository_storage_moves/#{repository_storage_move_id}" }
+
+ it_behaves_like 'get single container repository storage move'
+
+ context 'non-existent container' do
+ let(:container_id) { non_existing_record_id }
+ let(:repository_storage_move_id) { storage_move.id }
+
+ it 'returns not found' do
+ get api(url, user, admin_mode: user.admin?)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ describe "GET /#{container_type.singularize}_repository_storage_moves" do
+ it_behaves_like 'get container repository storage move list' do
+ let(:url) { "/#{container_type.singularize}_repository_storage_moves" }
+ end
+ end
+
+ describe "GET /#{container_type.singularize}_repository_storage_moves/:repository_storage_move_id" do
+ it_behaves_like 'get single container repository storage move' do
+ let(:url) { "/#{container_type.singularize}_repository_storage_moves/#{repository_storage_move_id}" }
+ end
+ end
+
+ describe "POST /#{container_type}/:id/repository_storage_moves", :aggregate_failures do
+ it_behaves_like 'post single container repository storage move'
+ end
+
describe "POST /#{container_type.singularize}_repository_storage_moves" do
let(:url) { "/#{container_type.singularize}_repository_storage_moves" }
let(:source_storage_name) { 'default' }
diff --git a/spec/support/shared_examples/requests/api/rubygems_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/rubygems_packages_shared_examples.rb
index da09d70c777..d7077180b91 100644
--- a/spec/support/shared_examples/requests/api/rubygems_packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/rubygems_packages_shared_examples.rb
@@ -151,7 +151,7 @@ RSpec.shared_examples 'dependency endpoint success' do |user_type, status, add_m
context 'with gems params' do
let(:params) { { gems: 'foo,bar' } }
- let(:expected_response) { Marshal.dump(%w(result result)) }
+ let(:expected_response) { Marshal.dump(%w[result result]) }
it 'returns successfully', :aggregate_failures do
service_result = double('DependencyResolverService', execute: ServiceResponse.success(payload: 'result'))
diff --git a/spec/support/shared_examples/sends_git_audit_streaming_event_shared_examples.rb b/spec/support/shared_examples/sends_git_audit_streaming_event_shared_examples.rb
index 2c2be0152a0..f91cf22f27e 100644
--- a/spec/support/shared_examples/sends_git_audit_streaming_event_shared_examples.rb
+++ b/spec/support/shared_examples/sends_git_audit_streaming_event_shared_examples.rb
@@ -14,7 +14,7 @@ RSpec.shared_examples 'sends git audit streaming event' do
let(:project) { create(:project, :public, :repository, namespace: group) }
before do
- group.external_audit_event_destinations.create!(destination_url: 'http://example.com')
+ create(:external_audit_event_destination, group: group)
project.add_developer(user)
end
@@ -38,7 +38,7 @@ RSpec.shared_examples 'sends git audit streaming event' do
let(:project) { create(:project, :private, :repository, namespace: group) }
before do
- group.external_audit_event_destinations.create!(destination_url: 'http://example.com')
+ create(:external_audit_event_destination, group: group)
project.add_developer(user)
sign_in(user)
end
@@ -52,9 +52,40 @@ RSpec.shared_examples 'sends git audit streaming event' do
request.headers.merge! auth_env(user.username, password, nil)
end
end
- it 'sends the audit streaming event' do
- expect(AuditEvents::AuditEventStreamingWorker).to receive(:perform_async).once
- subject
+
+ context 'when log_git_streaming_audit_events is enable' do
+ it 'does not send the audit streaming event' do
+ expect(AuditEvents::AuditEventStreamingWorker).not_to receive(:perform_async)
+ subject
+ end
+
+ it 'respond the need audit to be true' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+
+ audit_flag = json_response["need_audit"] || json_response["NeedAudit"]
+ expect(audit_flag).to be_truthy
+ end
+ end
+
+ context 'when log_git_streaming_audit_events is disable' do
+ before do
+ stub_feature_flags(log_git_streaming_audit_events: false)
+ end
+
+ it "sends git streaming audit event" do
+ expect(AuditEvents::AuditEventStreamingWorker).to receive(:perform_async).once
+
+ subject
+ end
+
+ it 'respond the need audit to be false' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response["need_audit"]).to be_falsy
+ end
end
end
end
diff --git a/spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb b/spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb
index 9eaad541df7..b7247f1f243 100644
--- a/spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb
+++ b/spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb
@@ -48,7 +48,7 @@ RSpec.shared_examples 'avoid N+1 on environments serialization' do
query ||= { page: 1, per_page: 20 }
request = double(url: "#{Gitlab.config.gitlab.url}:8080/api/v4/projects?#{query.to_query}", query_parameters: query)
- EnvironmentSerializer.new(current_user: user, project: project).yield_self do |serializer|
+ EnvironmentSerializer.new(current_user: user, project: project).then do |serializer|
serializer.within_folders if grouping
serializer.with_pagination(request, spy('response'))
serializer.represent(Environment.where(project: project))
diff --git a/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb b/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb
index 6abf8b242f1..f6be45b0cf8 100644
--- a/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb
@@ -127,7 +127,7 @@ end
RSpec.shared_examples 'a pullable and pushable' do
it_behaves_like 'an accessible' do
- let(:actions) { %w(pull push) }
+ let(:actions) { %w[pull push] }
end
end
diff --git a/spec/support/shared_examples/services/notification_service_shared_examples.rb b/spec/support/shared_examples/services/notification_service_shared_examples.rb
index df1ae67a590..c53872ca4bc 100644
--- a/spec/support/shared_examples/services/notification_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/notification_service_shared_examples.rb
@@ -45,7 +45,7 @@ RSpec.shared_examples 'group emails are disabled' do
before do
reset_delivered_emails!
- target_group.clear_memoization(:emails_disabled_memoized)
+ target_group.clear_memoization(:emails_enabled_memoized)
end
it 'sends no emails with group emails disabled' do
diff --git a/spec/support/shared_examples/services/protected_branches_shared_examples.rb b/spec/support/shared_examples/services/protected_branches_shared_examples.rb
index 15c63865720..80e2f09ed44 100644
--- a/spec/support/shared_examples/services/protected_branches_shared_examples.rb
+++ b/spec/support/shared_examples/services/protected_branches_shared_examples.rb
@@ -23,3 +23,34 @@ RSpec.shared_context 'with scan result policy blocking protected branches' do
stub_licensed_features(security_orchestration_policies: true)
end
end
+
+RSpec.shared_context 'with scan result policy preventing force pushing' do
+ include RepoHelpers
+
+ let(:policy_path) { Security::OrchestrationPolicyConfiguration::POLICY_PATH }
+ let(:default_branch) { policy_project.default_branch }
+ let(:prevent_pushing_and_force_pushing) { true }
+
+ let(:scan_result_policy) do
+ build(:scan_result_policy, branches: [branch_name],
+ approval_settings: { prevent_pushing_and_force_pushing: prevent_pushing_and_force_pushing })
+ end
+
+ let(:policy_yaml) do
+ build(:orchestration_policy_yaml, scan_result_policy: [scan_result_policy])
+ end
+
+ before do
+ create_file_in_repo(policy_project, default_branch, default_branch, policy_path, policy_yaml)
+ stub_licensed_features(security_orchestration_policies: true)
+ end
+
+ after do
+ policy_project.repository.delete_file(
+ policy_project.creator,
+ policy_path,
+ message: 'Automatically deleted policy',
+ branch_name: default_branch
+ )
+ end
+end
diff --git a/spec/support/shared_examples/validators/url_validator_shared_examples.rb b/spec/support/shared_examples/validators/url_validator_shared_examples.rb
index c5a775fefb6..8547845d0c3 100644
--- a/spec/support/shared_examples/validators/url_validator_shared_examples.rb
+++ b/spec/support/shared_examples/validators/url_validator_shared_examples.rb
@@ -24,7 +24,7 @@ RSpec.shared_examples 'url validator examples' do |schemes|
end
context 'with schemes' do
- let(:options) { { schemes: %w(http) } }
+ let(:options) { { schemes: %w[http] } }
it 'allows urls with the defined schemes' do
subject
diff --git a/spec/support/shared_examples/views/themed_layout_examples.rb b/spec/support/shared_examples/views/themed_layout_examples.rb
index 599fd141dd7..ffbc9026240 100644
--- a/spec/support/shared_examples/views/themed_layout_examples.rb
+++ b/spec/support/shared_examples/views/themed_layout_examples.rb
@@ -8,7 +8,7 @@ RSpec.shared_examples "a layout which reflects the application theme setting" do
it 'renders with the default theme' do
render
- expect(rendered).to have_selector("body.#{default_theme_class}")
+ expect(rendered).to have_selector("html.#{default_theme_class}")
end
end
@@ -24,10 +24,10 @@ RSpec.shared_examples "a layout which reflects the application theme setting" do
render
if chosen_theme.css_class != default_theme_class
- expect(rendered).not_to have_selector("body.#{default_theme_class}")
+ expect(rendered).not_to have_selector("html.#{default_theme_class}")
end
- expect(rendered).to have_selector("body.#{chosen_theme.css_class}")
+ expect(rendered).to have_selector("html.#{chosen_theme.css_class}")
end
end
end
diff --git a/spec/support/sidekiq_middleware.rb b/spec/support/sidekiq_middleware.rb
index 73f43487d7c..f4d90ff5151 100644
--- a/spec/support/sidekiq_middleware.rb
+++ b/spec/support/sidekiq_middleware.rb
@@ -7,7 +7,7 @@ module SidekiqMiddleware
def with_sidekiq_server_middleware(&block)
Sidekiq::Testing.server_middleware.clear
- if Gem::Version.new(Sidekiq::VERSION) != Gem::Version.new('6.5.7')
+ if Gem::Version.new(Sidekiq::VERSION) != Gem::Version.new('6.5.12')
raise 'New version of sidekiq detected, please remove this line'
end
diff --git a/spec/support_specs/graphql/arguments_spec.rb b/spec/support_specs/graphql/arguments_spec.rb
index 925f8c15d68..d96d6985687 100644
--- a/spec/support_specs/graphql/arguments_spec.rb
+++ b/spec/support_specs/graphql/arguments_spec.rb
@@ -50,7 +50,7 @@ RSpec.describe Graphql::Arguments do
hash: { a: 1, b: 2, c: 3 },
int: 42,
float: 2.7,
- string: %q[he said "no"],
+ string: %q(he said "no"),
enum: :OFF,
null: nil,
bool_true: true,
diff --git a/spec/support_specs/helpers/active_record/query_recorder_spec.rb b/spec/support_specs/helpers/active_record/query_recorder_spec.rb
index d6c52b22449..5df88ca8209 100644
--- a/spec/support_specs/helpers/active_record/query_recorder_spec.rb
+++ b/spec/support_specs/helpers/active_record/query_recorder_spec.rb
@@ -47,7 +47,7 @@ RSpec.describe ActiveRecord::QueryRecorder do
query_a = start_with(%q[QueryRecorder SQL: --> SELECT COUNT(*) FROM "schema_migrations"])
- query_b = start_with(%q[QueryRecorder SQL: --> SELECT "schema_migrations".* FROM "schema_migrations" ORDER BY "schema_migrations"."version" ASC LIMIT 1])
+ query_b = start_with(%q(QueryRecorder SQL: --> SELECT "schema_migrations".* FROM "schema_migrations" ORDER BY "schema_migrations"."version" ASC LIMIT 1))
query_c_a = eq(%q[QueryRecorder SQL: --> SELECT "schema_migrations".* FROM "schema_migrations" WHERE (version = 'foo'])
query_c_b = eq(%q(QueryRecorder SQL: --> OR))
diff --git a/spec/support_specs/helpers/migrations_helpers_spec.rb b/spec/support_specs/helpers/migrations_helpers_spec.rb
index 2af16151350..725caef7a63 100644
--- a/spec/support_specs/helpers/migrations_helpers_spec.rb
+++ b/spec/support_specs/helpers/migrations_helpers_spec.rb
@@ -55,7 +55,7 @@ RSpec.describe MigrationsHelpers, feature_category: :database do
end
it 'create a class based on the CI base model' do
- klass = helper.table(:ci_builds, database: :ci)
+ klass = helper.table(:p_ci_builds, database: :ci) { |model| model.primary_key = :id }
expect(klass.connection_specification_name).to eq('Ci::ApplicationRecord')
end
end
@@ -66,7 +66,7 @@ RSpec.describe MigrationsHelpers, feature_category: :database do
end
it 'creates a class based on main base model' do
- klass = helper.table(:ci_builds, database: :ci)
+ klass = helper.table(:p_ci_builds, database: :ci) { |model| model.primary_key = :id }
expect(klass.connection_specification_name).to eq('ActiveRecord::Base')
end
end
diff --git a/spec/support_specs/helpers/stub_saas_features_spec.rb b/spec/support_specs/helpers/stub_saas_features_spec.rb
index ed973071a6d..c3cec3f47aa 100644
--- a/spec/support_specs/helpers/stub_saas_features_spec.rb
+++ b/spec/support_specs/helpers/stub_saas_features_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe StubSaasFeatures, feature_category: :shared do
describe '#stub_saas_features' do
using RSpec::Parameterized::TableSyntax
- let(:feature_name) { '_some_saas_feature_' }
+ let(:feature_name) { :some_saas_feature }
context 'when checking global state' do
where(:feature_value) do
@@ -41,10 +41,10 @@ RSpec.describe StubSaasFeatures, feature_category: :shared do
end
it 'handles multiple features' do
- stub_saas_features(feature_name => false, '_some_new_feature_' => true)
+ stub_saas_features(feature_name => false, some_new_feature: true)
expect(::Gitlab::Saas.feature_available?(feature_name)).to eq(false)
- expect(::Gitlab::Saas.feature_available?('_some_new_feature_')).to eq(true)
+ expect(::Gitlab::Saas.feature_available?(:some_new_feature)).to eq(true)
end
end
end
diff --git a/spec/support_specs/matchers/exceed_query_limit_helpers_spec.rb b/spec/support_specs/matchers/exceed_query_limit_helpers_spec.rb
index 19581064626..7c957586b10 100644
--- a/spec/support_specs/matchers/exceed_query_limit_helpers_spec.rb
+++ b/spec/support_specs/matchers/exceed_query_limit_helpers_spec.rb
@@ -147,17 +147,17 @@ RSpec.describe ExceedQueryLimitHelpers do
test_matcher = TestMatcher.new
recorder = ActiveRecord::QueryRecorder.new do
- TestQueries.find_by(version: %w(foo bar baz).join("\n"))
- TestQueries.find_by(version: %w(foo biz baz).join("\n"))
- TestQueries.find_by(version: %w(foo bar baz).join("\n"))
+ TestQueries.find_by(version: %w[foo bar baz].join("\n"))
+ TestQueries.find_by(version: %w[foo biz baz].join("\n"))
+ TestQueries.find_by(version: %w[foo bar baz].join("\n"))
end
recorder.count
expect(test_matcher.count_queries(recorder)).to eq({
'SELECT "schema_migrations".* FROM "schema_migrations"' => {
- %[WHERE "schema_migrations"."version" = 'foo\nbar\nbaz' LIMIT 1] => 2,
- %[WHERE "schema_migrations"."version" = 'foo\nbiz\nbaz' LIMIT 1] => 1
+ %(WHERE "schema_migrations"."version" = 'foo\nbar\nbaz' LIMIT 1) => 2,
+ %(WHERE "schema_migrations"."version" = 'foo\nbiz\nbaz' LIMIT 1) => 1
}
})
end
@@ -183,13 +183,13 @@ RSpec.describe ExceedQueryLimitHelpers do
expect(test_matcher.count_queries(recorder)).to eq({
'SELECT "schema_migrations".* FROM "schema_migrations"' => {
- %q[WHERE "schema_migrations"."version" = 'foobar'] => 2,
- %q[WHERE "schema_migrations"."version" = 'also foobar and baz'] => 1,
- %q[ORDER BY "schema_migrations"."version" ASC LIMIT 1] => 1
+ %q(WHERE "schema_migrations"."version" = 'foobar') => 2,
+ %q(WHERE "schema_migrations"."version" = 'also foobar and baz') => 1,
+ %q(ORDER BY "schema_migrations"."version" ASC LIMIT 1) => 1
},
'SELECT COUNT(*) FROM "schema_migrations"' => {
"" => 2,
- %q[WHERE "schema_migrations"."version" = 'foobar'] => 1
+ %q(WHERE "schema_migrations"."version" = 'foobar') => 1
},
'SAVEPOINT active_record_1' => { "" => 1 },
'INSERT INTO "schema_migrations" ("version")' => {
@@ -197,11 +197,11 @@ RSpec.describe ExceedQueryLimitHelpers do
},
'RELEASE SAVEPOINT active_record_1' => { "" => 1 },
'UPDATE "schema_migrations"' => {
- %q[SET "version" = 'y' WHERE "schema_migrations"."version" = 'x'] => 1,
- %q[SET "version" = 'z' WHERE "schema_migrations"."version" = 'y'] => 1
+ %q(SET "version" = 'y' WHERE "schema_migrations"."version" = 'x') => 1,
+ %q(SET "version" = 'z' WHERE "schema_migrations"."version" = 'y') => 1
},
'DELETE FROM "schema_migrations"' => {
- %q[WHERE "schema_migrations"."version" = 'z'] => 1
+ %q(WHERE "schema_migrations"."version" = 'z') => 1
}
})
end
diff --git a/spec/tasks/gitlab/background_migrations_rake_spec.rb b/spec/tasks/gitlab/background_migrations_rake_spec.rb
index ba5618e2700..5c61f0eff38 100644
--- a/spec/tasks/gitlab/background_migrations_rake_spec.rb
+++ b/spec/tasks/gitlab/background_migrations_rake_spec.rb
@@ -149,6 +149,7 @@ RSpec.describe 'gitlab:background_migrations namespace rake tasks', :suppress_gi
context 'with two connections sharing the same database' do
before do
skip_if_database_exists(:ci)
+ skip_if_database_exists(:jh)
end
it 'skips the shared database' do
diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb
index 56560b06219..75be7b97a67 100644
--- a/spec/tasks/gitlab/backup_rake_spec.rb
+++ b/spec/tasks/gitlab/backup_rake_spec.rb
@@ -234,9 +234,7 @@ RSpec.describe 'gitlab:backup namespace rake tasks', :delete, feature_category:
raw_repo = excluded_project.repository.raw
- # The restore will not find the repository in the backup, but will create
- # an empty one in its place
- expect(raw_repo.empty?).to be(true)
+ expect(raw_repo).not_to exist
end
end
diff --git a/spec/tasks/gitlab/click_house/migration_rake_spec.rb b/spec/tasks/gitlab/click_house/migration_rake_spec.rb
new file mode 100644
index 00000000000..75a1c1a1856
--- /dev/null
+++ b/spec/tasks/gitlab/click_house/migration_rake_spec.rb
@@ -0,0 +1,133 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'gitlab:clickhouse', click_house: :without_migrations, feature_category: :database do
+ include ClickHouseTestHelpers
+
+ # We don't need to delete data since we don't modify Postgres data
+ self.use_transactional_tests = false
+
+ let(:migrations_base_dir) { 'click_house/migrations' }
+ let(:migrations_dirname) { '' }
+ let(:migrations_dir) { expand_fixture_path("#{migrations_base_dir}/#{migrations_dirname}") }
+ let(:verbose) { nil }
+
+ before(:all) do
+ Rake.application.rake_require 'tasks/gitlab/click_house/migration'
+ end
+
+ before do
+ stub_env('VERBOSE', verbose) if verbose
+ end
+
+ describe 'migrate' do
+ subject(:migration) { run_rake_task('gitlab:clickhouse:migrate') }
+
+ let(:target_version) { nil }
+
+ around do |example|
+ ClickHouse::MigrationSupport::Migrator.migrations_paths = [migrations_dir]
+
+ example.run
+
+ clear_consts(expand_fixture_path(migrations_base_dir))
+ end
+
+ before do
+ stub_env('VERSION', target_version) if target_version
+ end
+
+ describe 'when creating a table' do
+ let(:migrations_dirname) { 'plain_table_creation' }
+
+ it 'creates a table' do
+ expect { migration }.to change { active_schema_migrations_count }.from(0).to(1)
+ .and output.to_stdout
+
+ expect(describe_table('some')).to match({
+ id: a_hash_including(type: 'UInt64'),
+ date: a_hash_including(type: 'Date')
+ })
+ end
+
+ context 'when VERBOSE is false' do
+ let(:verbose) { 'false' }
+
+ it 'does not write to stdout' do
+ expect { migration }.not_to output.to_stdout
+
+ expect(describe_table('some')).to match({
+ id: a_hash_including(type: 'UInt64'),
+ date: a_hash_including(type: 'Date')
+ })
+ end
+ end
+ end
+
+ describe 'when dropping a table' do
+ let(:migrations_dirname) { 'drop_table' }
+ let(:target_version) { 2 }
+
+ it 'drops table' do
+ stub_env('VERSION', 1)
+ run_rake_task('gitlab:clickhouse:migrate')
+
+ expect(table_names).to include('some')
+
+ stub_env('VERSION', target_version)
+ migration
+ expect(table_names).not_to include('some')
+ end
+ end
+
+ describe 'with VERSION is invalid' do
+ let(:migrations_dirname) { 'plain_table_creation' }
+ let(:target_version) { 'invalid' }
+
+ it { expect { migration }.to raise_error RuntimeError, 'Invalid format of target version: `VERSION=invalid`' }
+ end
+ end
+
+ describe 'rollback' do
+ subject(:migration) { run_rake_task('gitlab:clickhouse:rollback') }
+
+ let(:schema_migration) { ClickHouse::MigrationSupport::SchemaMigration }
+
+ around do |example|
+ ClickHouse::MigrationSupport::Migrator.migrations_paths = [migrations_dir]
+ migrate(nil, ClickHouse::MigrationSupport::MigrationContext.new(migrations_dir, schema_migration))
+
+ example.run
+
+ clear_consts(expand_fixture_path(migrations_base_dir))
+ end
+
+ context 'when migrating back all the way to 0' do
+ let(:target_version) { 0 }
+
+ context 'when down method is present' do
+ let(:migrations_dirname) { 'table_creation_with_down_method' }
+
+ it 'removes migration' do
+ expect(table_names).to include('some')
+
+ migration
+ expect(table_names).not_to include('some')
+ end
+ end
+ end
+ end
+
+ %w[gitlab:clickhouse:migrate].each do |task|
+ context "when running #{task}" do
+ it "does run gitlab:clickhouse:prepare_schema_migration_table before" do
+ expect(Rake::Task['gitlab:clickhouse:prepare_schema_migration_table']).to receive(:execute).and_return(true)
+ expect(Rake::Task[task]).to receive(:execute).and_return(true)
+
+ Rake::Task['gitlab:clickhouse:prepare_schema_migration_table'].reenable
+ run_rake_task(task)
+ end
+ end
+ end
+end
diff --git a/spec/tasks/gitlab/db_rake_spec.rb b/spec/tasks/gitlab/db_rake_spec.rb
index c2e53da8d4b..a966f2118b0 100644
--- a/spec/tasks/gitlab/db_rake_spec.rb
+++ b/spec/tasks/gitlab/db_rake_spec.rb
@@ -7,6 +7,7 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout, feature_categor
Rake.application.rake_require 'active_record/railties/databases'
Rake.application.rake_require 'tasks/seed_fu'
Rake.application.rake_require 'tasks/gitlab/db'
+ Rake.application.rake_require 'tasks/gitlab/db/lock_writes'
end
before do
@@ -14,6 +15,7 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout, feature_categor
allow(Rake::Task['db:migrate']).to receive(:invoke).and_return(true)
allow(Rake::Task['db:schema:load']).to receive(:invoke).and_return(true)
allow(Rake::Task['db:seed_fu']).to receive(:invoke).and_return(true)
+ allow(Rake::Task['gitlab:db:lock_writes']).to receive(:invoke).and_return(true)
stub_feature_flags(disallow_database_ddl_feature_flags: false)
end
@@ -142,6 +144,7 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout, feature_categor
expect(Rake::Task['db:migrate']).to receive(:invoke)
expect(Rake::Task['db:schema:load']).not_to receive(:invoke)
+ expect(Rake::Task['gitlab:db:lock_writes']).not_to receive(:invoke)
expect(Rake::Task['db:seed_fu']).not_to receive(:invoke)
run_rake_task('gitlab:db:configure')
@@ -153,6 +156,7 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout, feature_categor
allow(connection).to receive(:tables).and_return([])
expect(Rake::Task['db:schema:load']).to receive(:invoke)
+ expect(Rake::Task['gitlab:db:lock_writes']).to receive(:invoke)
expect(Rake::Task['db:seed_fu']).to receive(:invoke)
expect(Rake::Task['db:migrate']).not_to receive(:invoke)
@@ -165,6 +169,7 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout, feature_categor
allow(connection).to receive(:tables).and_return(['default'])
expect(Rake::Task['db:schema:load']).to receive(:invoke)
+ expect(Rake::Task['gitlab:db:lock_writes']).to receive(:invoke)
expect(Rake::Task['db:seed_fu']).to receive(:invoke)
expect(Rake::Task['db:migrate']).not_to receive(:invoke)
@@ -177,6 +182,7 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout, feature_categor
allow(connection).to receive(:tables).and_return([])
expect(Rake::Task['db:schema:load']).to receive(:invoke).and_raise('error')
+ expect(Rake::Task['gitlab:db:lock_writes']).not_to receive(:invoke)
expect(Rake::Task['db:seed_fu']).not_to receive(:invoke)
expect(Rake::Task['db:migrate']).not_to receive(:invoke)
@@ -201,6 +207,7 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout, feature_categor
expect(Gitlab::Database).to receive(:add_post_migrate_path_to_rails).and_call_original
expect(Rake::Task['db:schema:load']).to receive(:invoke)
+ expect(Rake::Task['gitlab:db:lock_writes']).to receive(:invoke)
expect(Rake::Task['db:seed_fu']).to receive(:invoke)
expect(Rake::Task['db:migrate']).not_to receive(:invoke)
@@ -217,6 +224,7 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout, feature_categor
expect(Rake::Task['db:migrate']).to receive(:invoke)
expect(Gitlab::Database).not_to receive(:add_post_migrate_path_to_rails)
expect(Rake::Task['db:schema:load']).not_to receive(:invoke)
+ expect(Rake::Task['gitlab:db:lock_writes']).not_to receive(:invoke)
expect(Rake::Task['db:seed_fu']).not_to receive(:invoke)
run_rake_task('gitlab:db:configure')
@@ -280,6 +288,7 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout, feature_categor
expect(Rake::Task['db:migrate:main']).not_to receive(:invoke)
expect(Rake::Task['db:migrate:ci']).not_to receive(:invoke)
+ expect(Rake::Task['gitlab:db:lock_writes']).to receive(:invoke)
expect(Rake::Task['db:seed_fu']).to receive(:invoke)
run_rake_task('gitlab:db:configure')
@@ -299,6 +308,7 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout, feature_categor
expect(Rake::Task['db:schema:load:main']).not_to receive(:invoke)
expect(Rake::Task['db:schema:load:ci']).not_to receive(:invoke)
+ expect(Rake::Task['gitlab:db:lock_writes']).not_to receive(:invoke)
expect(Rake::Task['db:seed_fu']).not_to receive(:invoke)
run_rake_task('gitlab:db:configure')
@@ -569,6 +579,10 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout, feature_categor
allow(File).to receive(:open).with(Rails.root.join('ee/db/geo/structure.sql').to_s, any_args).and_yield(output)
allow(File).to receive(:open).with(Rails.root.join('ee/db/embedding/structure.sql').to_s, any_args).and_yield(output)
end
+
+ if Gitlab.jh?
+ allow(File).to receive(:open).with(Rails.root.join('jh/db/structure.sql').to_s, any_args).and_yield(output)
+ end
end
after do
@@ -587,8 +601,8 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout, feature_categor
end
describe 'drop_tables' do
- let(:tables) { %w(one two schema_migrations) }
- let(:views) { %w(three four pg_stat_statements) }
+ let(:tables) { %w[one two schema_migrations] }
+ let(:views) { %w[three four pg_stat_statements] }
let(:schemas) { Gitlab::Database::EXTRA_SCHEMAS }
let(:ignored_views) { double(ActiveRecord::Relation, pluck: ['pg_stat_statements']) }
@@ -1190,6 +1204,8 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout, feature_categor
let(:config_hash) { { username: 'foo' } }
before do
+ skip_if_shared_database(:ci)
+
allow(Rake::Task['db:drop']).to receive(:invoke)
allow(Rake::Task['db:create']).to receive(:invoke)
allow(ActiveRecord::Base).to receive(:configurations).and_return(configurations)
@@ -1201,7 +1217,7 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout, feature_categor
it 'migrate as nonsuperuser check with default username' do
expect(config_hash).to receive(:merge).with({ username: 'gitlab' }).and_call_original
expect(Gitlab::Database).to receive(:check_for_non_superuser)
- expect(Rake::Task['db:migrate']).to receive(:invoke)
+ expect(Rake::Task['db:migrate:main']).to receive(:invoke)
run_rake_task('gitlab:db:reset_as_non_superuser')
end
@@ -1209,7 +1225,7 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout, feature_categor
it 'migrate as nonsuperuser check with specified username' do
expect(config_hash).to receive(:merge).with({ username: 'foo' }).and_call_original
expect(Gitlab::Database).to receive(:check_for_non_superuser)
- expect(Rake::Task['db:migrate']).to receive(:invoke)
+ expect(Rake::Task['db:migrate:main']).to receive(:invoke)
run_rake_task('gitlab:db:reset_as_non_superuser', '[foo]')
end
diff --git a/spec/tasks/gitlab/feature_categories_rake_spec.rb b/spec/tasks/gitlab/feature_categories_rake_spec.rb
index 84558ea7fb7..1dee72eee46 100644
--- a/spec/tasks/gitlab/feature_categories_rake_spec.rb
+++ b/spec/tasks/gitlab/feature_categories_rake_spec.rb
@@ -10,16 +10,6 @@ RSpec.describe 'gitlab:feature_categories:index', :silence_stdout, feature_categ
it 'outputs objects by stage group' do
# Sample items that _hopefully_ won't change very often.
expected = {
- 'controller_actions' => a_hash_including(
- 'integrations' => a_collection_including(
- klass: 'Oauth::JiraDvcs::AuthorizationsController',
- action: 'new',
- source_location: [
- 'app/controllers/oauth/jira_dvcs/authorizations_controller.rb',
- an_instance_of(Integer)
- ]
- )
- ),
'api_endpoints' => a_hash_including(
'system_access' => a_collection_including(
klass: 'API::AccessRequests',
diff --git a/spec/tasks/gitlab/redis_rake_spec.rb b/spec/tasks/gitlab/redis_rake_spec.rb
new file mode 100644
index 00000000000..bfad25be4fd
--- /dev/null
+++ b/spec/tasks/gitlab/redis_rake_spec.rb
@@ -0,0 +1,188 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'gitlab:redis:secret rake tasks', :silence_stdout, feature_category: :build do
+ let(:redis_secret_file) { 'tmp/tests/redisenc/redis_secret.yaml.enc' }
+
+ before do
+ Rake.application.rake_require 'tasks/gitlab/redis'
+ stub_env('EDITOR', 'cat')
+ stub_warn_user_is_not_gitlab
+ FileUtils.mkdir_p('tmp/tests/redisenc/')
+ allow(::Gitlab::Runtime).to receive(:rake?).and_return(true)
+ allow_next_instance_of(Gitlab::Redis::Cache) do |instance|
+ allow(instance).to receive(:secret_file).and_return(redis_secret_file)
+ end
+ allow(Gitlab::Application.secrets).to receive(:encrypted_settings_key_base).and_return(SecureRandom.hex(64))
+ end
+
+ after do
+ FileUtils.rm_rf(Rails.root.join('tmp/tests/redisenc'))
+ end
+
+ describe ':show' do
+ it 'displays error when file does not exist' do
+ expect do
+ run_rake_task('gitlab:redis:secret:show')
+ end.to output(/File .* does not exist. Use `gitlab-rake gitlab:redis:secret:edit` to change that./).to_stdout
+ end
+
+ it 'displays error when key does not exist' do
+ Settings.encrypted(redis_secret_file).write('somevalue')
+ allow(Gitlab::Application.secrets).to receive(:encrypted_settings_key_base).and_return(nil)
+ expect do
+ run_rake_task('gitlab:redis:secret:show')
+ end.to output(/Missing encryption key encrypted_settings_key_base./).to_stderr
+ end
+
+ it 'displays error when key is changed' do
+ Settings.encrypted(redis_secret_file).write('somevalue')
+ allow(Gitlab::Application.secrets).to receive(:encrypted_settings_key_base).and_return(SecureRandom.hex(64))
+ expect do
+ run_rake_task('gitlab:redis:secret:show')
+ end.to output(/Couldn't decrypt .* Perhaps you passed the wrong key?/).to_stderr
+ end
+
+ it 'outputs the unencrypted content when present' do
+ encrypted = Settings.encrypted(redis_secret_file)
+ encrypted.write('somevalue')
+ expect { run_rake_task('gitlab:redis:secret:show') }.to output(/somevalue/).to_stdout
+ end
+ end
+
+ describe 'edit' do
+ it 'creates encrypted file' do
+ expect { run_rake_task('gitlab:redis:secret:edit') }.to output(/File encrypted and saved./).to_stdout
+ expect(File.exist?(redis_secret_file)).to be true
+ value = Settings.encrypted(redis_secret_file)
+ expect(value.read).to match(/password: '123'/)
+ end
+
+ it 'displays error when key does not exist' do
+ allow(Gitlab::Application.secrets).to receive(:encrypted_settings_key_base).and_return(nil)
+ expect do
+ run_rake_task('gitlab:redis:secret:edit')
+ end.to output(/Missing encryption key encrypted_settings_key_base./).to_stderr
+ end
+
+ it 'displays error when key is changed' do
+ Settings.encrypted(redis_secret_file).write('somevalue')
+ allow(Gitlab::Application.secrets).to receive(:encrypted_settings_key_base).and_return(SecureRandom.hex(64))
+ expect do
+ run_rake_task('gitlab:redis:secret:edit')
+ end.to output(/Couldn't decrypt .* Perhaps you passed the wrong key?/).to_stderr
+ end
+
+ it 'displays error when write directory does not exist' do
+ FileUtils.rm_rf(Rails.root.join('tmp/tests/redisenc'))
+ expect { run_rake_task('gitlab:redis:secret:edit') }.to output(/Directory .* does not exist./).to_stderr
+ end
+
+ it 'shows a warning when content is invalid' do
+ Settings.encrypted(redis_secret_file).write('somevalue')
+ expect do
+ run_rake_task('gitlab:redis:secret:edit')
+ end.to output(/WARNING: Content was not a valid Redis secret yml file/).to_stdout
+ value = Settings.encrypted(redis_secret_file)
+ expect(value.read).to match(/somevalue/)
+ end
+
+ it 'displays error when $EDITOR is not set' do
+ stub_env('EDITOR', nil)
+ expect do
+ run_rake_task('gitlab:redis:secret:edit')
+ end.to output(/No \$EDITOR specified to open file. Please provide one when running the command/).to_stderr
+ end
+ end
+
+ describe 'write' do
+ before do
+ allow($stdin).to receive(:tty?).and_return(false)
+ allow($stdin).to receive(:read).and_return('testvalue')
+ end
+
+ it 'creates encrypted file from stdin' do
+ expect { run_rake_task('gitlab:redis:secret:write') }.to output(/File encrypted and saved./).to_stdout
+ expect(File.exist?(redis_secret_file)).to be true
+ value = Settings.encrypted(redis_secret_file)
+ expect(value.read).to match(/testvalue/)
+ end
+
+ it 'displays error when key does not exist' do
+ allow(Gitlab::Application.secrets).to receive(:encrypted_settings_key_base).and_return(nil)
+ expect do
+ run_rake_task('gitlab:redis:secret:write')
+ end.to output(/Missing encryption key encrypted_settings_key_base./).to_stderr
+ end
+
+ it 'displays error when write directory does not exist' do
+ FileUtils.rm_rf('tmp/tests/redisenc/')
+ expect { run_rake_task('gitlab:redis:secret:write') }.to output(/Directory .* does not exist./).to_stderr
+ end
+
+ it 'shows a warning when content is invalid' do
+ Settings.encrypted(redis_secret_file).write('somevalue')
+ expect do
+ run_rake_task('gitlab:redis:secret:edit')
+ end.to output(/WARNING: Content was not a valid Redis secret yml file/).to_stdout
+ expect(Settings.encrypted(redis_secret_file).read).to match(/somevalue/)
+ end
+ end
+
+ context 'when an instance class is specified' do
+ before do
+ allow_next_instance_of(Gitlab::Redis::SharedState) do |instance|
+ allow(instance).to receive(:secret_file).and_return(redis_secret_file)
+ end
+ end
+
+ context 'when actual name is used' do
+ it 'uses the correct Redis class' do
+ expect(Gitlab::Redis::SharedState).to receive(:encrypted_secrets).and_call_original
+
+ run_rake_task('gitlab:redis:secret:edit', 'SharedState')
+ end
+ end
+
+ context 'when name in lowercase is used' do
+ it 'uses the correct Redis class' do
+ expect(Gitlab::Redis::SharedState).to receive(:encrypted_secrets).and_call_original
+
+ run_rake_task('gitlab:redis:secret:edit', 'sharedstate')
+ end
+ end
+
+ context 'when name with underscores is used' do
+ it 'uses the correct Redis class' do
+ expect(Gitlab::Redis::SharedState).to receive(:encrypted_secrets).and_call_original
+
+ run_rake_task('gitlab:redis:secret:edit', 'shared_state')
+ end
+ end
+
+ context 'when name with hyphens is used' do
+ it 'uses the correct Redis class' do
+ expect(Gitlab::Redis::SharedState).to receive(:encrypted_secrets).and_call_original
+
+ run_rake_task('gitlab:redis:secret:edit', 'shared-state')
+ end
+ end
+
+ context 'when name with spaces is used' do
+ it 'uses the correct Redis class' do
+ expect(Gitlab::Redis::SharedState).to receive(:encrypted_secrets).and_call_original
+
+ run_rake_task('gitlab:redis:secret:edit', 'shared state')
+ end
+ end
+
+ context 'when an invalid name is used' do
+ it 'raises error' do
+ expect do
+ run_rake_task('gitlab:redis:secret:edit', 'foobar')
+ end.to raise_error(/Specified instance name foobar does not exist./)
+ end
+ end
+ end
+end
diff --git a/spec/tooling/danger/analytics_instrumentation_spec.rb b/spec/tooling/danger/analytics_instrumentation_spec.rb
index 5d12647e02f..79c75b2e89c 100644
--- a/spec/tooling/danger/analytics_instrumentation_spec.rb
+++ b/spec/tooling/danger/analytics_instrumentation_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'gitlab-dangerfiles'
+require 'fast_spec_helper'
require 'gitlab/dangerfiles/spec_helper'
require_relative '../../../tooling/danger/analytics_instrumentation'
@@ -231,4 +231,64 @@ RSpec.describe Tooling::Danger::AnalyticsInstrumentation, feature_category: :ser
end
end
end
+
+ describe '#check_deprecated_data_sources!' do
+ let(:fake_project_helper) { instance_double(Tooling::Danger::ProjectHelper) }
+
+ subject(:check_data_source) { analytics_instrumentation.check_deprecated_data_sources! }
+
+ before do
+ allow(fake_helper).to receive(:added_files).and_return([added_file])
+ allow(fake_helper).to receive(:changed_lines).with(added_file).and_return(changed_lines)
+ allow(analytics_instrumentation).to receive(:project_helper).and_return(fake_project_helper)
+ allow(analytics_instrumentation.project_helper).to receive(:file_lines).and_return(changed_lines.map { |line| line.delete_prefix('+') })
+ end
+
+ context 'when no metric definitions were modified' do
+ let(:added_file) { 'app/models/user.rb' }
+ let(:changed_lines) { ['+ data_source: redis,'] }
+
+ it 'does not trigger warning' do
+ expect(analytics_instrumentation).not_to receive(:markdown)
+
+ check_data_source
+ end
+ end
+
+ context 'when metrics fields were modified' do
+ let(:added_file) { 'config/metrics/count7_d/example_metric.yml' }
+
+ [:redis, :redis_hll].each do |source|
+ context "when source is #{source}" do
+ let(:changed_lines) { ["+ data_source: #{source}"] }
+ let(:template) do
+ <<~SUGGEST_COMMENT
+ ```suggestion
+ data_source: internal_events
+ ```
+
+ %<message>s
+ SUGGEST_COMMENT
+ end
+
+ it 'issues a warning' do
+ expected_comment = format(template, message: Tooling::Danger::AnalyticsInstrumentation::CHANGE_DEPRECATED_DATA_SOURCE_MESSAGE)
+ expect(analytics_instrumentation).to receive(:markdown).with(expected_comment.strip, file: added_file, line: 1)
+
+ check_data_source
+ end
+ end
+ end
+
+ context 'when neither redis nor redis_hll used as a data_source' do
+ let(:changed_lines) { ['+ data_source: database,'] }
+
+ it 'does not issue a warning' do
+ expect(analytics_instrumentation).not_to receive(:markdown)
+
+ check_data_source
+ end
+ end
+ end
+ end
end
diff --git a/spec/tooling/danger/bulk_database_actions_spec.rb b/spec/tooling/danger/bulk_database_actions_spec.rb
index 620b4ac2b18..eba3eacb212 100644
--- a/spec/tooling/danger/bulk_database_actions_spec.rb
+++ b/spec/tooling/danger/bulk_database_actions_spec.rb
@@ -1,10 +1,8 @@
# frozen_string_literal: true
-require 'gitlab-dangerfiles'
-require 'danger'
-require 'danger/plugins/internal/helper'
-require 'gitlab/dangerfiles/spec_helper'
+require 'fast_spec_helper'
require 'rspec-parameterized'
+require 'gitlab/dangerfiles/spec_helper'
require_relative '../../../tooling/danger/bulk_database_actions'
require_relative '../../../tooling/danger/project_helper'
diff --git a/spec/tooling/danger/change_column_default_spec.rb b/spec/tooling/danger/change_column_default_spec.rb
new file mode 100644
index 00000000000..8cfcbfa1dc0
--- /dev/null
+++ b/spec/tooling/danger/change_column_default_spec.rb
@@ -0,0 +1,104 @@
+# frozen_string_literal: true
+
+require 'danger'
+require 'gitlab/dangerfiles/spec_helper'
+
+require_relative '../../../tooling/danger/change_column_default'
+require_relative '../../../tooling/danger/project_helper'
+
+RSpec.describe Tooling::Danger::ChangeColumnDefault, feature_category: :tooling do
+ subject(:change_column_default) { fake_danger.new(helper: fake_helper) }
+
+ let(:fake_danger) { DangerSpecHelper.fake_danger.include(described_class) }
+ let(:fake_project_helper) { instance_double(Tooling::Danger::ProjectHelper) }
+ let(:comment) { described_class::COMMENT.chomp }
+ let(:file_diff) do
+ File.read(File.expand_path("../fixtures/change_column_default_migration.txt", __dir__)).split("\n")
+ end
+
+ include_context "with dangerfile"
+
+ describe '#add_comment_for_change_column_default' do
+ let(:file_lines) { file_diff.map { |line| line.delete_prefix('+').delete_prefix('-') } }
+ let(:matching_lines) { [7, 9, 11] }
+
+ before do
+ allow(change_column_default).to receive(:project_helper).and_return(fake_project_helper)
+ allow(change_column_default.project_helper).to receive(:file_lines).and_return(file_lines)
+ allow(change_column_default.helper).to receive(:all_changed_files).and_return([filename])
+ allow(change_column_default.helper).to receive(:changed_lines).with(filename).and_return(file_diff)
+ end
+
+ context 'when column default is changed in a regular migration' do
+ let(:filename) { 'db/migrate/change_column_default_migration.rb' }
+
+ it 'adds comment at the correct line' do
+ matching_lines.each do |line_number|
+ expect(change_column_default).to receive(:markdown).with("\n#{comment}", file: filename, line: line_number)
+ end
+
+ change_column_default.add_comment_for_change_column_default
+ end
+ end
+
+ context 'when column default is changed in a post-deployment migration' do
+ let(:filename) { 'db/post_migrate/change_column_default_migration.rb' }
+
+ it 'adds comment at the correct line' do
+ matching_lines.each do |line_number|
+ expect(change_column_default).to receive(:markdown).with("\n#{comment}", file: filename, line: line_number)
+ end
+
+ change_column_default.add_comment_for_change_column_default
+ end
+ end
+
+ context 'when a regular migration does not change column default' do
+ let(:filename) { 'db/migrate/my_migration.rb' }
+ let(:file_diff) do
+ [
+ "+ undo_cleanup_concurrent_column_rename(:my_table, :old_column, :new_column)",
+ "- cleanup_concurrent_column_rename(:my_table, :new_column, :old_column)"
+ ]
+ end
+
+ let(:file_lines) do
+ [
+ ' def up',
+ ' undo_cleanup_concurrent_column_rename(:my_table, :old_column, :new_column)',
+ ' end'
+ ]
+ end
+
+ it 'does not add comment' do
+ expect(change_column_default).not_to receive(:markdown)
+
+ change_column_default.add_comment_for_change_column_default
+ end
+ end
+
+ context 'when a post-deployment migration does not change column default' do
+ let(:filename) { 'db/post_migrate/my_migration.rb' }
+ let(:file_diff) do
+ [
+ "+ restore_conversion_of_integer_to_bigint(TABLE, COLUMNS)",
+ "- cleanup_conversion_of_integer_to_bigint(TABLE, COLUMNS)"
+ ]
+ end
+
+ let(:file_lines) do
+ [
+ ' def up',
+ ' cleanup_conversion_of_integer_to_bigint(TABLE, COLUMNS)',
+ ' end'
+ ]
+ end
+
+ it 'does not add comment' do
+ expect(change_column_default).not_to receive(:markdown)
+
+ change_column_default.add_comment_for_change_column_default
+ end
+ end
+ end
+end
diff --git a/spec/tooling/danger/clickhouse_spec.rb b/spec/tooling/danger/clickhouse_spec.rb
index ad2f0b4a827..135f8810e61 100644
--- a/spec/tooling/danger/clickhouse_spec.rb
+++ b/spec/tooling/danger/clickhouse_spec.rb
@@ -1,9 +1,7 @@
# frozen_string_literal: true
require 'rspec-parameterized'
-require 'gitlab-dangerfiles'
-require 'danger'
-require 'danger/plugins/internal/helper'
+require 'fast_spec_helper'
require 'gitlab/dangerfiles/spec_helper'
require_relative '../../../tooling/danger/clickhouse'
diff --git a/spec/tooling/danger/config_files_spec.rb b/spec/tooling/danger/config_files_spec.rb
index 42fc08ad901..f9b7dc64e6d 100644
--- a/spec/tooling/danger/config_files_spec.rb
+++ b/spec/tooling/danger/config_files_spec.rb
@@ -1,8 +1,6 @@
# frozen_string_literal: true
-require 'gitlab-dangerfiles'
-require 'danger'
-require 'danger/plugins/internal/helper'
+require 'fast_spec_helper'
require 'gitlab/dangerfiles/spec_helper'
require_relative '../../../tooling/danger/config_files'
diff --git a/spec/tooling/danger/customer_success_spec.rb b/spec/tooling/danger/customer_success_spec.rb
index 798905212f1..40ab7c79418 100644
--- a/spec/tooling/danger/customer_success_spec.rb
+++ b/spec/tooling/danger/customer_success_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'rspec-parameterized'
-require 'gitlab-dangerfiles'
+require 'fast_spec_helper'
require 'gitlab/dangerfiles/spec_helper'
require_relative '../../../tooling/danger/customer_success'
@@ -17,53 +17,53 @@ RSpec.describe Tooling::Danger::CustomerSuccess do
where do
{
'with data category changes to Ops and no Customer Success::Impact Check label' => {
- modified_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml app/models/user.rb),
+ modified_files: %w[config/metrics/20210216182127_user_secret_detection_jobs.yml app/models/user.rb],
changed_lines: ['-data_category: cat1', '+data_category: operational'],
customer_labeled: false,
impacted: true,
- impacted_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml)
+ impacted_files: %w[config/metrics/20210216182127_user_secret_detection_jobs.yml]
},
'with data category changes and Customer Success::Impact Check label' => {
- modified_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml),
+ modified_files: %w[config/metrics/20210216182127_user_secret_detection_jobs.yml],
changed_lines: ['-data_category: cat1', '+data_category: operational'],
customer_labeled: true,
impacted: false,
- impacted_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml)
+ impacted_files: %w[config/metrics/20210216182127_user_secret_detection_jobs.yml]
},
'with metric file changes and no data category changes' => {
- modified_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml),
+ modified_files: %w[config/metrics/20210216182127_user_secret_detection_jobs.yml],
changed_lines: ['-product_stage: growth'],
customer_labeled: false,
impacted: false,
impacted_files: []
},
'with data category changes from Ops' => {
- modified_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml app/models/user.rb),
+ modified_files: %w[config/metrics/20210216182127_user_secret_detection_jobs.yml app/models/user.rb],
changed_lines: ['-data_category: operational', '+data_category: cat2'],
customer_labeled: false,
impacted: true,
- impacted_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml)
+ impacted_files: %w[config/metrics/20210216182127_user_secret_detection_jobs.yml]
},
'with data category removed' => {
- modified_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml app/models/user.rb),
+ modified_files: %w[config/metrics/20210216182127_user_secret_detection_jobs.yml app/models/user.rb],
changed_lines: ['-data_category: operational'],
customer_labeled: false,
impacted: true,
- impacted_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml)
+ impacted_files: %w[config/metrics/20210216182127_user_secret_detection_jobs.yml]
},
'with data category added' => {
- modified_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml app/models/user.rb),
+ modified_files: %w[config/metrics/20210216182127_user_secret_detection_jobs.yml app/models/user.rb],
changed_lines: ['+data_category: operational'],
customer_labeled: false,
impacted: true,
- impacted_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml)
+ impacted_files: %w[config/metrics/20210216182127_user_secret_detection_jobs.yml]
},
'with data category in uppercase' => {
- modified_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml app/models/user.rb),
+ modified_files: %w[config/metrics/20210216182127_user_secret_detection_jobs.yml app/models/user.rb],
changed_lines: ['+data_category: Operational'],
customer_labeled: false,
impacted: true,
- impacted_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml)
+ impacted_files: %w[config/metrics/20210216182127_user_secret_detection_jobs.yml]
}
}
end
diff --git a/spec/tooling/danger/database_dictionary_spec.rb b/spec/tooling/danger/database_dictionary_spec.rb
index 1a771a6cec0..943179cda19 100644
--- a/spec/tooling/danger/database_dictionary_spec.rb
+++ b/spec/tooling/danger/database_dictionary_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'gitlab-dangerfiles'
+require 'fast_spec_helper'
require 'gitlab/dangerfiles/spec_helper'
require_relative '../../../tooling/danger/database_dictionary'
diff --git a/spec/tooling/danger/database_spec.rb b/spec/tooling/danger/database_spec.rb
index a342014cf6b..bfc92e0a744 100644
--- a/spec/tooling/danger/database_spec.rb
+++ b/spec/tooling/danger/database_spec.rb
@@ -1,9 +1,7 @@
# frozen_string_literal: true
+require 'fast_spec_helper'
require 'rspec-parameterized'
-require 'gitlab-dangerfiles'
-require 'danger'
-require 'danger/plugins/internal/helper'
require 'gitlab/dangerfiles/spec_helper'
require_relative '../../../tooling/danger/database'
diff --git a/spec/tooling/danger/datateam_spec.rb b/spec/tooling/danger/datateam_spec.rb
index de8a93baa27..9d8aaf08520 100644
--- a/spec/tooling/danger/datateam_spec.rb
+++ b/spec/tooling/danger/datateam_spec.rb
@@ -1,9 +1,8 @@
# frozen_string_literal: true
+require 'fast_spec_helper'
require 'rspec-parameterized'
-require 'gitlab-dangerfiles'
require 'gitlab/dangerfiles/spec_helper'
-require 'pry'
require_relative '../../../tooling/danger/datateam'
RSpec.describe Tooling::Danger::Datateam do
@@ -17,89 +16,96 @@ RSpec.describe Tooling::Danger::Datateam do
where do
{
- 'with structure.sql changes and no Data Warehouse::Impact Check label' => {
- modified_files: %w(db/structure.sql app/models/user.rb),
- changed_lines: ['+group_id bigint NOT NULL'],
+ 'with structure.sql subtraction changes and no Data Warehouse::Impact Check label' => {
+ modified_files: %w[db/structure.sql app/models/user.rb],
+ changed_lines: ['-group_id bigint NOT NULL'],
mr_labels: [],
impacted: true,
- impacted_files: %w(db/structure.sql)
+ impacted_files: %w[db/structure.sql]
},
- 'with structure.sql changes and Data Warehouse::Impact Check label' => {
- modified_files: %w(db/structure.sql),
- changed_lines: ['+group_id bigint NOT NULL)'],
+ 'with structure.sql subtraction changes and Data Warehouse::Impact Check label' => {
+ modified_files: %w[db/structure.sql],
+ changed_lines: ['-group_id bigint NOT NULL)'],
mr_labels: ['Data Warehouse::Impact Check'],
impacted: false,
- impacted_files: %w(db/structure.sql)
+ impacted_files: %w[db/structure.sql]
+ },
+ 'with structure.sql addition changes and no Data Warehouse::Impact Check label' => {
+ modified_files: %w[db/structure.sql app/models/user.rb],
+ changed_lines: ['+group_id bigint NOT NULL'],
+ mr_labels: [],
+ impacted: false,
+ impacted_files: %w[db/structure.sql]
},
'with user model changes' => {
- modified_files: %w(app/models/users.rb),
+ modified_files: %w[app/models/users.rb],
changed_lines: ['+has_one :namespace'],
mr_labels: [],
impacted: false,
impacted_files: []
},
'with perfomance indicator changes and no Data Warehouse::Impact Check label' => {
- modified_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml app/models/user.rb),
+ modified_files: %w[config/metrics/20210216182127_user_secret_detection_jobs.yml app/models/user.rb],
changed_lines: ['+-gmau'],
mr_labels: [],
impacted: true,
- impacted_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml)
+ impacted_files: %w[config/metrics/20210216182127_user_secret_detection_jobs.yml]
},
'with perfomance indicator changes and Data Warehouse::Impact Check label' => {
- modified_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml),
+ modified_files: %w[config/metrics/20210216182127_user_secret_detection_jobs.yml],
changed_lines: ['+-gmau'],
mr_labels: ['Data Warehouse::Impact Check'],
impacted: false,
- impacted_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml)
+ impacted_files: %w[config/metrics/20210216182127_user_secret_detection_jobs.yml]
},
'with metric file changes and no performance indicator changes' => {
- modified_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml),
+ modified_files: %w[config/metrics/20210216182127_user_secret_detection_jobs.yml],
changed_lines: ['-product_stage: growth'],
mr_labels: [],
impacted: false,
impacted_files: []
},
'with metric file changes and no performance indicator changes and other label' => {
- modified_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml),
+ modified_files: %w[config/metrics/20210216182127_user_secret_detection_jobs.yml],
changed_lines: ['-product_stage: growth'],
mr_labels: ['type::maintenance'],
impacted: false,
impacted_files: []
},
'with performance indicator changes and other label' => {
- modified_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml app/models/user.rb),
+ modified_files: %w[config/metrics/20210216182127_user_secret_detection_jobs.yml app/models/user.rb],
changed_lines: ['+-gmau'],
mr_labels: ['type::maintenance'],
impacted: true,
- impacted_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml)
+ impacted_files: %w[config/metrics/20210216182127_user_secret_detection_jobs.yml]
},
'with performance indicator changes, Data Warehouse::Impact Check and other label' => {
- modified_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml app/models/user.rb),
+ modified_files: %w[config/metrics/20210216182127_user_secret_detection_jobs.yml app/models/user.rb],
changed_lines: ['+-gmau'],
mr_labels: ['type::maintenance', 'Data Warehouse::Impact Check'],
impacted: false,
- impacted_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml)
+ impacted_files: %w[config/metrics/20210216182127_user_secret_detection_jobs.yml]
},
'with performance indicator changes and other labels' => {
- modified_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml app/models/user.rb),
+ modified_files: %w[config/metrics/20210216182127_user_secret_detection_jobs.yml app/models/user.rb],
changed_lines: ['+-gmau'],
mr_labels: ['type::maintenance', 'Data Warehouse::Impacted'],
impacted: false,
- impacted_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml)
+ impacted_files: %w[config/metrics/20210216182127_user_secret_detection_jobs.yml]
},
'with metric status removed' => {
- modified_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml app/models/user.rb),
+ modified_files: %w[config/metrics/20210216182127_user_secret_detection_jobs.yml app/models/user.rb],
changed_lines: ['+status: removed'],
mr_labels: ['type::maintenance'],
impacted: true,
- impacted_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml)
+ impacted_files: %w[config/metrics/20210216182127_user_secret_detection_jobs.yml]
},
'with metric status active' => {
- modified_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml app/models/user.rb),
+ modified_files: %w[config/metrics/20210216182127_user_secret_detection_jobs.yml app/models/user.rb],
changed_lines: ['+status: active'],
mr_labels: ['type::maintenance'],
impacted: false,
- impacted_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml)
+ impacted_files: %w[config/metrics/20210216182127_user_secret_detection_jobs.yml]
}
}
end
diff --git a/spec/tooling/danger/experiments_spec.rb b/spec/tooling/danger/experiments_spec.rb
index 85f8060a3ec..7c4921670a3 100644
--- a/spec/tooling/danger/experiments_spec.rb
+++ b/spec/tooling/danger/experiments_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'gitlab-dangerfiles'
+require 'fast_spec_helper'
require 'gitlab/dangerfiles/spec_helper'
require_relative '../../../tooling/danger/experiments'
diff --git a/spec/tooling/danger/feature_flag_spec.rb b/spec/tooling/danger/feature_flag_spec.rb
index 4575d8ca981..9298028feb3 100644
--- a/spec/tooling/danger/feature_flag_spec.rb
+++ b/spec/tooling/danger/feature_flag_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'gitlab-dangerfiles'
+require 'fast_spec_helper'
require 'gitlab/dangerfiles/spec_helper'
require_relative '../../../tooling/danger/feature_flag'
@@ -134,7 +134,7 @@ RSpec.describe Tooling::Danger::FeatureFlag do
context 'when group is not nil' do
it 'is true only if MR has the same group label' do
expect(found.group_match_mr_label?(group)).to eq true
- expect(found.group_match_mr_label?('group::authentication and authorization')).to eq false
+ expect(found.group_match_mr_label?('group::authentication')).to eq false
end
end
end
diff --git a/spec/tooling/danger/gitlab_schema_validation_suggestion_spec.rb b/spec/tooling/danger/gitlab_schema_validation_suggestion_spec.rb
new file mode 100644
index 00000000000..c83e0319423
--- /dev/null
+++ b/spec/tooling/danger/gitlab_schema_validation_suggestion_spec.rb
@@ -0,0 +1,108 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require 'gitlab/dangerfiles/spec_helper'
+
+require_relative '../../../tooling/danger/gitlab_schema_validation_suggestion'
+require_relative '../../../tooling/danger/project_helper'
+
+RSpec.describe Tooling::Danger::GitlabSchemaValidationSuggestion, feature_category: :cell do
+ include_context "with dangerfile"
+
+ let(:fake_danger) { DangerSpecHelper.fake_danger.include(described_class) }
+ let(:fake_project_helper) { instance_double(Tooling::Danger::ProjectHelper) }
+ let(:filename) { 'db/docs/application_settings.yml' }
+ let(:file_lines) do
+ file_diff.map { |line| line.delete_prefix('+') }
+ end
+
+ let(:file_diff) do
+ [
+ "+---",
+ "+table_name: application_settings",
+ "+classes:",
+ "+- ApplicationSetting",
+ "+feature_categories:",
+ "+- continuous_integration",
+ "+description: GitLab application settings",
+ "+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/8589b4e137f50293952923bb07e2814257d7784d",
+ "+milestone: '7.7'",
+ "+gitlab_schema: #{schema}"
+ ]
+ end
+
+ subject(:gitlab_schema_validation) { fake_danger.new(helper: fake_helper) }
+
+ before do
+ allow(gitlab_schema_validation).to receive(:project_helper).and_return(fake_project_helper)
+ allow(gitlab_schema_validation.project_helper).to receive(:file_lines).and_return(file_lines)
+ allow(gitlab_schema_validation.helper).to receive(:changed_lines).with(filename).and_return(file_diff)
+ allow(gitlab_schema_validation.helper).to receive(:all_changed_files).and_return([filename])
+ end
+
+ shared_examples_for 'does not add a comment' do
+ it do
+ expect(gitlab_schema_validation).not_to receive(:markdown)
+
+ gitlab_schema_validation.add_suggestions_on_using_clusterwide_schema
+ end
+ end
+
+ context 'for discouraging the use of gitlab_main_clusterwide schema' do
+ let(:schema) { 'gitlab_main_clusterwide' }
+
+ context 'when the file path matches' do
+ it 'adds the comment' do
+ expected_comment = "\n#{described_class::SUGGESTION.chomp}"
+
+ expect(gitlab_schema_validation).to receive(:markdown).with(expected_comment, file: filename, line: 10)
+
+ gitlab_schema_validation.add_suggestions_on_using_clusterwide_schema
+ end
+ end
+
+ context 'when the file path does not match' do
+ let(:filename) { 'some_path/application_settings.yml' }
+
+ it_behaves_like 'does not add a comment'
+ end
+
+ context 'for EE' do
+ let(:filename) { 'ee/db/docs/application_settings.yml' }
+
+ it_behaves_like 'does not add a comment'
+ end
+
+ context 'for a deleted table' do
+ let(:filename) { 'db/docs/deleted_tables/application_settings.yml' }
+
+ it_behaves_like 'does not add a comment'
+ end
+ end
+
+ context 'on removing the gitlab_main_clusterwide schema' do
+ let(:file_diff) do
+ [
+ "+---",
+ "+table_name: application_settings",
+ "+classes:",
+ "+- ApplicationSetting",
+ "+feature_categories:",
+ "+- continuous_integration",
+ "+description: GitLab application settings",
+ "+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/8589b4e137f50293952923bb07e2814257d7784d",
+ "+milestone: '7.7'",
+ "-gitlab_schema: gitlab_main_clusterwide",
+ "+gitlab_schema: gitlab_main_cell"
+ ]
+ end
+
+ it_behaves_like 'does not add a comment'
+ end
+
+ context 'when a different schema is added' do
+ let(:schema) { 'gitlab_main' }
+
+ it_behaves_like 'does not add a comment'
+ end
+end
diff --git a/spec/tooling/danger/ignored_model_columns_spec.rb b/spec/tooling/danger/ignored_model_columns_spec.rb
index 737b6cce077..3d19f80a4ed 100644
--- a/spec/tooling/danger/ignored_model_columns_spec.rb
+++ b/spec/tooling/danger/ignored_model_columns_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'danger'
+require 'fast_spec_helper'
require 'gitlab/dangerfiles/spec_helper'
require_relative '../../../tooling/danger/ignored_model_columns'
diff --git a/spec/tooling/danger/model_validations_spec.rb b/spec/tooling/danger/model_validations_spec.rb
index 18ff4b83b6e..2dc2bc3e186 100644
--- a/spec/tooling/danger/model_validations_spec.rb
+++ b/spec/tooling/danger/model_validations_spec.rb
@@ -1,8 +1,6 @@
# frozen_string_literal: true
-require 'gitlab-dangerfiles'
-require 'danger'
-require 'danger/plugins/internal/helper'
+require 'fast_spec_helper'
require 'gitlab/dangerfiles/spec_helper'
require_relative '../../../tooling/danger/model_validations'
diff --git a/spec/tooling/danger/multiversion_spec.rb b/spec/tooling/danger/multiversion_spec.rb
index 90edad61d47..649a13013c4 100644
--- a/spec/tooling/danger/multiversion_spec.rb
+++ b/spec/tooling/danger/multiversion_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'rspec-parameterized'
-require 'gitlab-dangerfiles'
+require 'fast_spec_helper'
require 'gitlab/dangerfiles/spec_helper'
require_relative '../../../tooling/danger/multiversion'
diff --git a/spec/tooling/danger/outdated_todo_spec.rb b/spec/tooling/danger/outdated_todo_spec.rb
new file mode 100644
index 00000000000..3a3909c69ac
--- /dev/null
+++ b/spec/tooling/danger/outdated_todo_spec.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require 'gitlab/dangerfiles/spec_helper'
+
+require_relative '../../../tooling/danger/outdated_todo'
+
+RSpec.describe Tooling::Danger::OutdatedTodo, feature_category: :tooling do
+ let(:fake_danger) { double }
+ let(:filenames) { ['app/controllers/application_controller.rb'] }
+
+ let(:todos) do
+ [
+ File.join('spec', 'fixtures', 'tooling', 'danger', 'rubocop_todo', '**', '*.yml')
+ ]
+ end
+
+ subject(:plugin) { described_class.new(filenames, context: fake_danger, todos: todos) }
+
+ context 'when the filenames are mentioned in single todo' do
+ let(:filenames) { ['app/controllers/acme_challenges_controller.rb'] }
+
+ it 'warns about mentions' do
+ expect(fake_danger)
+ .to receive(:warn)
+ .with <<~MESSAGE
+ `app/controllers/acme_challenges_controller.rb` was removed but is mentioned in:
+ - `spec/fixtures/tooling/danger/rubocop_todo/cop1.yml:5`
+ MESSAGE
+
+ plugin.check
+ end
+ end
+
+ context 'when the filenames are mentioned in multiple todos' do
+ let(:filenames) do
+ [
+ 'app/controllers/application_controller.rb',
+ 'app/controllers/acme_challenges_controller.rb'
+ ]
+ end
+
+ it 'warns about mentions' do
+ expect(fake_danger)
+ .to receive(:warn)
+ .with(<<~FIRSTMESSAGE)
+ `app/controllers/application_controller.rb` was removed but is mentioned in:
+ - `spec/fixtures/tooling/danger/rubocop_todo/cop1.yml:4`
+ - `spec/fixtures/tooling/danger/rubocop_todo/cop2.yml:4`
+ FIRSTMESSAGE
+
+ expect(fake_danger)
+ .to receive(:warn)
+ .with(<<~SECONDMESSAGE)
+ `app/controllers/acme_challenges_controller.rb` was removed but is mentioned in:
+ - `spec/fixtures/tooling/danger/rubocop_todo/cop1.yml:5`
+ SECONDMESSAGE
+
+ plugin.check
+ end
+ end
+
+ context 'when the filenames are not mentioned in todos' do
+ let(:filenames) { ['any/inexisting/file.rb'] }
+
+ it 'does not warn' do
+ expect(fake_danger).not_to receive(:warn)
+
+ plugin.check
+ end
+ end
+
+ context 'when there is no todos' do
+ let(:filenames) { ['app/controllers/acme_challenges_controller.rb'] }
+ let(:todos) { [] }
+
+ it 'does not warn' do
+ expect(fake_danger).not_to receive(:warn)
+
+ plugin.check
+ end
+ end
+end
diff --git a/spec/tooling/danger/project_helper_spec.rb b/spec/tooling/danger/project_helper_spec.rb
index 28b8b2278d0..2da90ddbd67 100644
--- a/spec/tooling/danger/project_helper_spec.rb
+++ b/spec/tooling/danger/project_helper_spec.rb
@@ -1,11 +1,10 @@
# frozen_string_literal: true
+require 'fast_spec_helper'
require 'rspec-parameterized'
-require 'gitlab-dangerfiles'
require 'danger'
require 'danger/plugins/internal/helper'
require 'gitlab/dangerfiles/spec_helper'
-require 'gitlab/rspec/all'
require_relative '../../../danger/plugins/project_helper'
@@ -244,6 +243,7 @@ RSpec.describe Tooling::Danger::ProjectHelper do
[:analytics_instrumentation] | '+data-track-action' | ['components/welcome.vue']
[:analytics_instrumentation] | '+ data: { track_label:' | ['admin/groups/_form.html.haml']
[:analytics_instrumentation] | '+ Gitlab::Tracking.event' | ['dashboard/todos_controller.rb', 'admin/groups/_form.html.haml']
+ [:analytics_instrumentation] | '+ Gitlab::Tracking.event("c", "a")' | ['dashboard/todos_controller.rb', 'admin/groups/_form.html.haml']
[:database, :backend, :analytics_instrumentation] | '+ count(User.active)' | ['usage_data.rb', 'lib/gitlab/usage_data.rb', 'ee/lib/ee/gitlab/usage_data.rb']
[:database, :backend, :analytics_instrumentation] | '+ estimate_batch_distinct_count(User.active)' | ['usage_data.rb']
[:backend, :analytics_instrumentation] | '+ alt_usage_data(User.active)' | ['lib/gitlab/usage_data.rb']
diff --git a/spec/tooling/danger/required_stops_spec.rb b/spec/tooling/danger/required_stops_spec.rb
index 7a90f19ac09..1b811166d13 100644
--- a/spec/tooling/danger/required_stops_spec.rb
+++ b/spec/tooling/danger/required_stops_spec.rb
@@ -1,8 +1,6 @@
# frozen_string_literal: true
-require 'gitlab-dangerfiles'
-require 'danger'
-require 'danger/plugins/internal/helper'
+require 'fast_spec_helper'
require 'gitlab/dangerfiles/spec_helper'
require_relative '../../../tooling/danger/required_stops'
diff --git a/spec/tooling/danger/rubocop_inline_disable_suggestion_spec.rb b/spec/tooling/danger/rubocop_inline_disable_suggestion_spec.rb
index 94dd5192d74..6b9ff667564 100644
--- a/spec/tooling/danger/rubocop_inline_disable_suggestion_spec.rb
+++ b/spec/tooling/danger/rubocop_inline_disable_suggestion_spec.rb
@@ -1,5 +1,6 @@
# frozen_string_literal: true
+require 'fast_spec_helper'
require 'gitlab/dangerfiles/spec_helper'
require_relative '../../../tooling/danger/rubocop_inline_disable_suggestion'
@@ -14,10 +15,15 @@ RSpec.describe Tooling::Danger::RubocopInlineDisableSuggestion, feature_category
let(:template) do
<<~SUGGESTION_MARKDOWN.chomp
+ ```suggestion
+ %<suggested_line>s
+ ```
Consider removing this inline disabling and adhering to the rubocop rule.
- If that isn't possible, please provide context as a reply for reviewers.
- See [rubocop best practices](https://docs.gitlab.com/ee/development/rubocop_development_guide.html).
+
+ If that isn't possible, please provide the reason as a code comment in the
+ same line where the rule is disabled separated by ` -- `.
+ See [rubocop best practices](https://docs.gitlab.com/ee/development/rubocop_development_guide.html#disabling-rules-inline).
----
@@ -73,6 +79,23 @@ RSpec.describe Tooling::Danger::RubocopInlineDisableSuggestion, feature_category
show_out_of_pipeline_minutes_notification?(project, namespace)
end
+
+ def show_my_new_dot?(project, namespace)
+ return false unless ::Gitlab.com? # rubocop: todo Gitlab/AvoidGitlabInstanceChecks -- Reason for disabling
+ thatsfine = "".dup # rubocop:disable Lint/UselessAssignment,Performance/UnfreezeString -- That's OK
+ me = "".dup # rubocop:disable Lint/UselessAssignment,Performance/UnfreezeString
+ test = "".dup # rubocop:disable Lint/UselessAssignment, Performance/UnfreezeString
+ return false if notification_dot_acknowledged?
+
+ show_out_of_pipeline_minutes_notification?(project, namespace)
+ end
+
+ def show_my_bad_dot?(project, namespace)
+ return false unless ::Gitlab.com? # rubocop: todo Gitlab/AvoidGitlabInstanceChecks --
+ return false if notification_dot_acknowledged?
+
+ show_out_of_pipeline_minutes_notification?(project, namespace)
+ end
RUBY
end
@@ -86,6 +109,10 @@ RSpec.describe Tooling::Danger::RubocopInlineDisableSuggestion, feature_category
+ return false unless ::Gitlab.com? # rubocop: disable Gitlab/AvoidGitlabInstanceChecks
+ return false unless ::Gitlab.com? # rubocop:todo Gitlab/AvoidGitlabInstanceChecks
+ return false unless ::Gitlab.com? # rubocop: todo Gitlab/AvoidGitlabInstanceChecks
+ + return false unless ::Gitlab.com? # rubocop: todo Gitlab/AvoidGitlabInstanceChecks -- Reason for disabling
+ + me = "".dup # rubocop:disable Lint/UselessAssignment,Performance/UnfreezeString
+ + test = "".dup # rubocop:disable Lint/UselessAssignment, Performance/UnfreezeString
+ + return false unless ::Gitlab.com? # rubocop: todo Gitlab/AvoidGitlabInstanceChecks --
DIFF
end
@@ -102,8 +129,12 @@ RSpec.describe Tooling::Danger::RubocopInlineDisableSuggestion, feature_category
end
it 'adds comments at the correct lines', :aggregate_failures do
- [3, 7, 13, 20, 27, 34, 41].each do |line_number|
- expect(rubocop).to receive(:markdown).with(template, file: filename, line: line_number)
+ [3, 7, 13, 20, 27, 34, 41, 50, 51, 58].each do |line_number|
+ existing_line = file_lines[line_number - 1].sub(/ --\s*$/, '')
+ suggested_line = "#{existing_line} -- TODO: Reason why the rule must be disabled"
+ comment = format(template, suggested_line: suggested_line)
+
+ expect(rubocop).to receive(:markdown).with(comment, file: filename, line: line_number)
end
rubocop.add_suggestions_for(filename)
diff --git a/spec/tooling/danger/saas_feature_spec.rb b/spec/tooling/danger/saas_feature_spec.rb
index 7ce9116ea5f..019dbf6944f 100644
--- a/spec/tooling/danger/saas_feature_spec.rb
+++ b/spec/tooling/danger/saas_feature_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'gitlab-dangerfiles'
+require 'fast_spec_helper'
require 'gitlab/dangerfiles/spec_helper'
require_relative '../../../tooling/danger/saas_feature'
diff --git a/spec/tooling/danger/sidekiq_args_spec.rb b/spec/tooling/danger/sidekiq_args_spec.rb
index 29bf32a9a02..c44486a83b5 100644
--- a/spec/tooling/danger/sidekiq_args_spec.rb
+++ b/spec/tooling/danger/sidekiq_args_spec.rb
@@ -1,9 +1,7 @@
# frozen_string_literal: true
require 'rspec-parameterized'
-require 'gitlab-dangerfiles'
-require 'danger'
-require 'danger/plugins/internal/helper'
+require 'fast_spec_helper'
require 'gitlab/dangerfiles/spec_helper'
require_relative '../../../tooling/danger/sidekiq_args'
diff --git a/spec/tooling/danger/sidekiq_queues_spec.rb b/spec/tooling/danger/sidekiq_queues_spec.rb
index 9bffc7ee93d..143ea9732cd 100644
--- a/spec/tooling/danger/sidekiq_queues_spec.rb
+++ b/spec/tooling/danger/sidekiq_queues_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
+require 'fast_spec_helper'
require 'rspec-parameterized'
-require 'gitlab-dangerfiles'
require 'gitlab/dangerfiles/spec_helper'
require_relative '../../../tooling/danger/sidekiq_queues'
@@ -17,12 +17,12 @@ RSpec.describe Tooling::Danger::SidekiqQueues do
using RSpec::Parameterized::TableSyntax
where(:modified_files, :changed_queue_files) do
- %w(app/workers/all_queues.yml ee/app/workers/all_queues.yml foo) | %w(app/workers/all_queues.yml ee/app/workers/all_queues.yml)
- %w(app/workers/all_queues.yml ee/app/workers/all_queues.yml) | %w(app/workers/all_queues.yml ee/app/workers/all_queues.yml)
- %w(app/workers/all_queues.yml foo) | %w(app/workers/all_queues.yml)
- %w(ee/app/workers/all_queues.yml foo) | %w(ee/app/workers/all_queues.yml)
- %w(foo) | %w()
- %w() | %w()
+ %w[app/workers/all_queues.yml ee/app/workers/all_queues.yml foo] | %w[app/workers/all_queues.yml ee/app/workers/all_queues.yml]
+ %w[app/workers/all_queues.yml ee/app/workers/all_queues.yml] | %w[app/workers/all_queues.yml ee/app/workers/all_queues.yml]
+ %w[app/workers/all_queues.yml foo] | %w[app/workers/all_queues.yml]
+ %w[ee/app/workers/all_queues.yml foo] | %w[ee/app/workers/all_queues.yml]
+ %w[foo] | %w[]
+ %w[] | %w[]
end
with_them do
diff --git a/spec/tooling/danger/specs/feature_category_suggestion_spec.rb b/spec/tooling/danger/specs/feature_category_suggestion_spec.rb
index 87eb20e5e50..ea8a00afac1 100644
--- a/spec/tooling/danger/specs/feature_category_suggestion_spec.rb
+++ b/spec/tooling/danger/specs/feature_category_suggestion_spec.rb
@@ -1,5 +1,6 @@
# frozen_string_literal: true
+require 'fast_spec_helper'
require 'gitlab/dangerfiles/spec_helper'
require_relative '../../../../tooling/danger/specs'
diff --git a/spec/tooling/danger/specs/match_with_array_suggestion_spec.rb b/spec/tooling/danger/specs/match_with_array_suggestion_spec.rb
index b065772a09b..92c4a134a34 100644
--- a/spec/tooling/danger/specs/match_with_array_suggestion_spec.rb
+++ b/spec/tooling/danger/specs/match_with_array_suggestion_spec.rb
@@ -1,5 +1,6 @@
# frozen_string_literal: true
+require 'fast_spec_helper'
require 'gitlab/dangerfiles/spec_helper'
require_relative '../../../../tooling/danger/specs'
diff --git a/spec/tooling/danger/specs/project_factory_suggestion_spec.rb b/spec/tooling/danger/specs/project_factory_suggestion_spec.rb
index b765d5073af..078f415cc44 100644
--- a/spec/tooling/danger/specs/project_factory_suggestion_spec.rb
+++ b/spec/tooling/danger/specs/project_factory_suggestion_spec.rb
@@ -1,5 +1,6 @@
# frozen_string_literal: true
+require 'fast_spec_helper'
require 'gitlab/dangerfiles/spec_helper'
require_relative '../../../../tooling/danger/specs'
diff --git a/spec/tooling/danger/specs_spec.rb b/spec/tooling/danger/specs_spec.rb
index b4953858ef7..f601e11a7a5 100644
--- a/spec/tooling/danger/specs_spec.rb
+++ b/spec/tooling/danger/specs_spec.rb
@@ -1,5 +1,6 @@
# frozen_string_literal: true
+require 'fast_spec_helper'
require 'gitlab/dangerfiles/spec_helper'
require_relative '../../../tooling/danger/specs'
diff --git a/spec/tooling/danger/stable_branch_spec.rb b/spec/tooling/danger/stable_branch_spec.rb
index 69e68f983fd..a33788f54f2 100644
--- a/spec/tooling/danger/stable_branch_spec.rb
+++ b/spec/tooling/danger/stable_branch_spec.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
-require 'gitlab-dangerfiles'
-require 'gitlab/dangerfiles/spec_helper'
require 'rspec-parameterized'
+require 'fast_spec_helper'
+require 'gitlab/dangerfiles/spec_helper'
require 'httparty'
require_relative '../../../tooling/danger/stable_branch'
diff --git a/spec/tooling/fixtures/change_column_default_migration.txt b/spec/tooling/fixtures/change_column_default_migration.txt
new file mode 100644
index 00000000000..a74c31464ee
--- /dev/null
+++ b/spec/tooling/fixtures/change_column_default_migration.txt
@@ -0,0 +1,13 @@
++# frozen_string_literal: true
++
++class TestMigration < Gitlab::Database::Migration[2.1]
++ enable_lock_retries!
++
++ def change
++ change_column_default('ci_builds', 'partition_id', from: 100, to: 101)
++
++ change_column_default('ci_builds', 'partition_id', from: 100, to: 101)
++
++ remove_column_default('ci_builds', 'partition_id')
++ end
++end
diff --git a/spec/tooling/lib/tooling/find_changes_spec.rb b/spec/tooling/lib/tooling/find_changes_spec.rb
index fef29ad3f2c..85e3eadac6f 100644
--- a/spec/tooling/lib/tooling/find_changes_spec.rb
+++ b/spec/tooling/lib/tooling/find_changes_spec.rb
@@ -15,7 +15,8 @@ RSpec.describe Tooling::FindChanges, feature_category: :tooling do
changed_files_pathname: changed_files_pathname,
predictive_tests_pathname: predictive_tests_pathname,
frontend_fixtures_mapping_pathname: frontend_fixtures_mapping_pathname,
- from: from)
+ from: from,
+ file_filter: file_filter)
end
let(:changed_files_pathname) { changed_files_file.path }
@@ -23,6 +24,7 @@ RSpec.describe Tooling::FindChanges, feature_category: :tooling do
let(:frontend_fixtures_mapping_pathname) { frontend_fixtures_mapping_file.path }
let(:from) { :api }
let(:gitlab_client) { double('GitLab') } # rubocop:disable RSpec/VerifiedDoubles
+ let(:file_filter) { ->(_) { true } }
around do |example|
self.changed_files_file = Tempfile.new('changed_files_file')
@@ -89,6 +91,37 @@ RSpec.describe Tooling::FindChanges, feature_category: :tooling do
subject
end
+
+ context 'when used with file_filter' do
+ let(:file_filter) { ->(file) { file['new_path'] =~ %r{doc/.*} } }
+
+ let(:mr_changes_array) do
+ [
+ {
+ "new_path" => "scripts/test.js",
+ "old_path" => "scripts/test.js"
+ },
+ {
+ "new_path" => "doc/index.md",
+ "old_path" => "doc/index.md"
+ }
+ ]
+ end
+
+ before do
+ # rubocop:disable RSpec/VerifiedDoubles -- The class from the GitLab gem isn't public, so we cannot use verified doubles for it.
+ allow(gitlab_client).to receive(:merge_request_changes)
+ .with('dummy-project', '1234')
+ .and_return(double(changes: mr_changes_array))
+ # rubocop:enable RSpec/VerifiedDoubles
+ end
+
+ it 'only writes matching files to output' do
+ subject
+
+ expect(File.read(changed_files_file)).to eq('doc/index.md')
+ end
+ end
end
context 'when fetching changes from changed files' do
diff --git a/spec/tooling/lib/tooling/test_map_generator_spec.rb b/spec/tooling/lib/tooling/test_map_generator_spec.rb
index 1b369923d8d..eaaf525fc49 100644
--- a/spec/tooling/lib/tooling/test_map_generator_spec.rb
+++ b/spec/tooling/lib/tooling/test_map_generator_spec.rb
@@ -88,7 +88,7 @@ RSpec.describe Tooling::TestMapGenerator do
end
it 'displays a warning when report has no examples' do
- expect { subject.parse('yaml3.yml') }.to output(%|No examples in yaml3.yml! Metadata: {:type=>"Crystalball::ExecutionMap", :commit=>"74056e8d9cf3773f43faa1cf5416f8779c8284c9", :timestamp=>1602671965, :version=>nil}\n|).to_stdout
+ expect { subject.parse('yaml3.yml') }.to output(%(No examples in yaml3.yml! Metadata: {:type=>"Crystalball::ExecutionMap", :commit=>"74056e8d9cf3773f43faa1cf5416f8779c8284c9", :timestamp=>1602671965, :version=>nil}\n)).to_stdout
end
end
diff --git a/spec/tooling/quality/test_level_spec.rb b/spec/tooling/quality/test_level_spec.rb
index 6ccd2e46f7b..d7d04015b48 100644
--- a/spec/tooling/quality/test_level_spec.rb
+++ b/spec/tooling/quality/test_level_spec.rb
@@ -46,7 +46,7 @@ RSpec.describe Quality::TestLevel, feature_category: :tooling do
context 'when level is unit' do
it 'returns a pattern' do
expect(subject.pattern(:unit))
- .to eq("spec/{bin,channels,components,config,contracts,db,dependencies,elastic,elastic_integration,experiments,factories,finders,frontend,graphql,haml_lint,helpers,initializers,lib,metrics_server,models,policies,presenters,rack_servers,replicators,routing,rubocop,scripts,serializers,services,sidekiq,sidekiq_cluster,spam,support_specs,tasks,uploaders,validators,views,workers,tooling}{,/**/}*_spec.rb")
+ .to eq("spec/{bin,channels,click_house,components,config,contracts,db,dependencies,elastic,elastic_integration,experiments,factories,finders,frontend,graphql,haml_lint,helpers,initializers,lib,metrics_server,models,policies,presenters,rack_servers,replicators,routing,rubocop,scripts,serializers,services,sidekiq,sidekiq_cluster,spam,support_specs,tasks,uploaders,validators,views,workers,tooling}{,/**/}*_spec.rb")
end
end
@@ -121,7 +121,7 @@ RSpec.describe Quality::TestLevel, feature_category: :tooling do
context 'when level is unit' do
it 'returns a regexp' do
expect(subject.regexp(:unit))
- .to eq(%r{spec/(bin|channels|components|config|contracts|db|dependencies|elastic|elastic_integration|experiments|factories|finders|frontend|graphql|haml_lint|helpers|initializers|lib|metrics_server|models|policies|presenters|rack_servers|replicators|routing|rubocop|scripts|serializers|services|sidekiq|sidekiq_cluster|spam|support_specs|tasks|uploaders|validators|views|workers|tooling)/})
+ .to eq(%r{spec/(bin|channels|click_house|components|config|contracts|db|dependencies|elastic|elastic_integration|experiments|factories|finders|frontend|graphql|haml_lint|helpers|initializers|lib|metrics_server|models|policies|presenters|rack_servers|replicators|routing|rubocop|scripts|serializers|services|sidekiq|sidekiq_cluster|spam|support_specs|tasks|uploaders|validators|views|workers|tooling)/})
end
end
diff --git a/spec/uploaders/attachment_uploader_spec.rb b/spec/uploaders/attachment_uploader_spec.rb
index a035402e207..e4e96aa15b7 100644
--- a/spec/uploaders/attachment_uploader_spec.rb
+++ b/spec/uploaders/attachment_uploader_spec.rb
@@ -10,9 +10,9 @@ RSpec.describe AttachmentUploader do
subject { uploader }
it_behaves_like 'builds correct paths',
- store_dir: %r[uploads/-/system/note/attachment/],
- upload_path: %r[uploads/-/system/note/attachment/],
- absolute_path: %r[#{CarrierWave.root}/uploads/-/system/note/attachment/]
+ store_dir: %r{uploads/-/system/note/attachment/},
+ upload_path: %r{uploads/-/system/note/attachment/},
+ absolute_path: %r{#{CarrierWave.root}/uploads/-/system/note/attachment/}
context "object_store is REMOTE" do
before do
@@ -22,8 +22,8 @@ RSpec.describe AttachmentUploader do
include_context 'with storage', described_class::Store::REMOTE
it_behaves_like 'builds correct paths',
- store_dir: %r[note/attachment/],
- upload_path: %r[note/attachment/]
+ store_dir: %r{note/attachment/},
+ upload_path: %r{note/attachment/}
end
describe "#migrate!" do
diff --git a/spec/uploaders/avatar_uploader_spec.rb b/spec/uploaders/avatar_uploader_spec.rb
index bba7eb78f99..333f23d7947 100644
--- a/spec/uploaders/avatar_uploader_spec.rb
+++ b/spec/uploaders/avatar_uploader_spec.rb
@@ -10,9 +10,9 @@ RSpec.describe AvatarUploader do
subject { uploader }
it_behaves_like 'builds correct paths',
- store_dir: %r[uploads/-/system/user/avatar/],
- upload_path: %r[uploads/-/system/user/avatar/],
- absolute_path: %r[#{CarrierWave.root}/uploads/-/system/user/avatar/]
+ store_dir: %r{uploads/-/system/user/avatar/},
+ upload_path: %r{uploads/-/system/user/avatar/},
+ absolute_path: %r{#{CarrierWave.root}/uploads/-/system/user/avatar/}
context "object_store is REMOTE" do
before do
@@ -22,8 +22,8 @@ RSpec.describe AvatarUploader do
include_context 'with storage', described_class::Store::REMOTE
it_behaves_like 'builds correct paths',
- store_dir: %r[user/avatar/],
- upload_path: %r[user/avatar/]
+ store_dir: %r{user/avatar/},
+ upload_path: %r{user/avatar/}
end
context "with a file" do
diff --git a/spec/uploaders/ci/pipeline_artifact_uploader_spec.rb b/spec/uploaders/ci/pipeline_artifact_uploader_spec.rb
index 3935f081372..ace7bcbce48 100644
--- a/spec/uploaders/ci/pipeline_artifact_uploader_spec.rb
+++ b/spec/uploaders/ci/pipeline_artifact_uploader_spec.rb
@@ -10,8 +10,8 @@ RSpec.describe Ci::PipelineArtifactUploader do
it_behaves_like "builds correct paths",
store_dir: %r[\h{2}/\h{2}/\h{64}/pipelines/\d+/artifacts/\d+],
- cache_dir: %r[artifacts/tmp/cache],
- work_dir: %r[artifacts/tmp/work]
+ cache_dir: %r{artifacts/tmp/cache},
+ work_dir: %r{artifacts/tmp/work}
context 'when object store is REMOTE' do
before do
diff --git a/spec/uploaders/dependency_proxy/file_uploader_spec.rb b/spec/uploaders/dependency_proxy/file_uploader_spec.rb
index 3cb2d1ea0f0..faaa5541f0b 100644
--- a/spec/uploaders/dependency_proxy/file_uploader_spec.rb
+++ b/spec/uploaders/dependency_proxy/file_uploader_spec.rb
@@ -12,8 +12,8 @@ RSpec.describe DependencyProxy::FileUploader do
it_behaves_like "builds correct paths",
store_dir: %r[\h{2}/\h{2}],
- cache_dir: %r[/dependency_proxy/tmp/cache],
- work_dir: %r[/dependency_proxy/tmp/work]
+ cache_dir: %r{/dependency_proxy/tmp/cache},
+ work_dir: %r{/dependency_proxy/tmp/work}
context 'object store is remote' do
before do
diff --git a/spec/uploaders/design_management/design_v432x230_uploader_spec.rb b/spec/uploaders/design_management/design_v432x230_uploader_spec.rb
index 3991058b32d..edfab24331c 100644
--- a/spec/uploaders/design_management/design_v432x230_uploader_spec.rb
+++ b/spec/uploaders/design_management/design_v432x230_uploader_spec.rb
@@ -11,10 +11,10 @@ RSpec.describe DesignManagement::DesignV432x230Uploader do
subject(:uploader) { described_class.new(model, :image_v432x230) }
it_behaves_like 'builds correct paths',
- store_dir: %r[uploads/-/system/design_management/action/image_v432x230/],
- upload_path: %r[uploads/-/system/design_management/action/image_v432x230/],
- relative_path: %r[uploads/-/system/design_management/action/image_v432x230/],
- absolute_path: %r[#{CarrierWave.root}/uploads/-/system/design_management/action/image_v432x230/]
+ store_dir: %r{uploads/-/system/design_management/action/image_v432x230/},
+ upload_path: %r{uploads/-/system/design_management/action/image_v432x230/},
+ relative_path: %r{uploads/-/system/design_management/action/image_v432x230/},
+ absolute_path: %r{#{CarrierWave.root}/uploads/-/system/design_management/action/image_v432x230/}
context 'object_store is REMOTE' do
before do
@@ -24,9 +24,9 @@ RSpec.describe DesignManagement::DesignV432x230Uploader do
include_context 'with storage', described_class::Store::REMOTE
it_behaves_like 'builds correct paths',
- store_dir: %r[design_management/action/image_v432x230/],
- upload_path: %r[design_management/action/image_v432x230/],
- relative_path: %r[design_management/action/image_v432x230/]
+ store_dir: %r{design_management/action/image_v432x230/},
+ upload_path: %r{design_management/action/image_v432x230/},
+ relative_path: %r{design_management/action/image_v432x230/}
end
describe "#migrate!" do
diff --git a/spec/uploaders/external_diff_uploader_spec.rb b/spec/uploaders/external_diff_uploader_spec.rb
index 2121e9cbc29..25e8bd0a4dc 100644
--- a/spec/uploaders/external_diff_uploader_spec.rb
+++ b/spec/uploaders/external_diff_uploader_spec.rb
@@ -9,9 +9,9 @@ RSpec.describe ExternalDiffUploader do
subject(:uploader) { described_class.new(diff, :external_diff) }
it_behaves_like "builds correct paths",
- store_dir: %r[merge_request_diffs/mr-\d+],
- cache_dir: %r[/external-diffs/tmp/cache],
- work_dir: %r[/external-diffs/tmp/work]
+ store_dir: %r{merge_request_diffs/mr-\d+},
+ cache_dir: %r{/external-diffs/tmp/cache},
+ work_dir: %r{/external-diffs/tmp/work}
context "object store is REMOTE" do
before do
@@ -21,7 +21,7 @@ RSpec.describe ExternalDiffUploader do
include_context 'with storage', described_class::Store::REMOTE
it_behaves_like "builds correct paths",
- store_dir: %r[merge_request_diffs/mr-\d+]
+ store_dir: %r{merge_request_diffs/mr-\d+}
end
describe 'remote file' do
diff --git a/spec/uploaders/import_export_uploader_spec.rb b/spec/uploaders/import_export_uploader_spec.rb
index 64e92f5d60e..1a2041df3d0 100644
--- a/spec/uploaders/import_export_uploader_spec.rb
+++ b/spec/uploaders/import_export_uploader_spec.rb
@@ -39,8 +39,8 @@ RSpec.describe ImportExportUploader do
include_context 'with storage', described_class::Store::REMOTE
patterns = {
- store_dir: %r[import_export_upload/import_file/],
- upload_path: %r[import_export_upload/import_file/]
+ store_dir: %r{import_export_upload/import_file/},
+ upload_path: %r{import_export_upload/import_file/}
}
it_behaves_like 'builds correct paths', patterns do
diff --git a/spec/uploaders/job_artifact_uploader_spec.rb b/spec/uploaders/job_artifact_uploader_spec.rb
index dac9e97641d..ea4f0036fa4 100644
--- a/spec/uploaders/job_artifact_uploader_spec.rb
+++ b/spec/uploaders/job_artifact_uploader_spec.rb
@@ -11,8 +11,8 @@ RSpec.describe JobArtifactUploader do
it_behaves_like "builds correct paths",
store_dir: %r[\h{2}/\h{2}/\h{64}/\d{4}_\d{1,2}_\d{1,2}/\d+/\d+\z],
- cache_dir: %r[artifacts/tmp/cache],
- work_dir: %r[artifacts/tmp/work]
+ cache_dir: %r{artifacts/tmp/cache},
+ work_dir: %r{artifacts/tmp/work}
context "object store is REMOTE" do
before do
diff --git a/spec/uploaders/lfs_object_uploader_spec.rb b/spec/uploaders/lfs_object_uploader_spec.rb
index 9bbfd910ada..77bde5da7ea 100644
--- a/spec/uploaders/lfs_object_uploader_spec.rb
+++ b/spec/uploaders/lfs_object_uploader_spec.rb
@@ -11,8 +11,8 @@ RSpec.describe LfsObjectUploader do
it_behaves_like "builds correct paths",
store_dir: %r[\h{2}/\h{2}],
- cache_dir: %r[/lfs-objects/tmp/cache],
- work_dir: %r[/lfs-objects/tmp/work]
+ cache_dir: %r{/lfs-objects/tmp/cache},
+ work_dir: %r{/lfs-objects/tmp/work}
context "object store is REMOTE" do
before do
diff --git a/spec/uploaders/namespace_file_uploader_spec.rb b/spec/uploaders/namespace_file_uploader_spec.rb
index 02381123ba5..0db7f82dcbd 100644
--- a/spec/uploaders/namespace_file_uploader_spec.rb
+++ b/spec/uploaders/namespace_file_uploader_spec.rb
@@ -13,9 +13,9 @@ RSpec.describe NamespaceFileUploader do
it_behaves_like 'builds correct paths' do
let(:patterns) do
{
- store_dir: %r[uploads/-/system/namespace/\d+],
+ store_dir: %r{uploads/-/system/namespace/\d+},
upload_path: identifier,
- absolute_path: %r[#{CarrierWave.root}/uploads/-/system/namespace/\d+/#{identifier}]
+ absolute_path: %r{#{CarrierWave.root}/uploads/-/system/namespace/\d+/#{identifier}}
}
end
end
@@ -30,7 +30,7 @@ RSpec.describe NamespaceFileUploader do
it_behaves_like 'builds correct paths' do
let(:patterns) do
{
- store_dir: %r[namespace/\d+/\h+],
+ store_dir: %r{namespace/\d+/\h+},
upload_path: identifier
}
end
diff --git a/spec/uploaders/object_storage_spec.rb b/spec/uploaders/object_storage_spec.rb
index 576f6deeec6..e4a9b92df64 100644
--- a/spec/uploaders/object_storage_spec.rb
+++ b/spec/uploaders/object_storage_spec.rb
@@ -255,7 +255,7 @@ RSpec.describe ObjectStorage, :clean_gitlab_redis_shared_state, feature_category
describe '#use_file' do
context 'when file is stored locally' do
it "calls a regular path" do
- expect { |b| uploader.use_file(&b) }.not_to yield_with_args(%r[tmp/cache])
+ expect { |b| uploader.use_file(&b) }.not_to yield_with_args(%r{tmp/cache})
end
end
@@ -267,7 +267,7 @@ RSpec.describe ObjectStorage, :clean_gitlab_redis_shared_state, feature_category
end
it "calls a cache path" do
- expect { |b| uploader.use_file(&b) }.to yield_with_args(%r[tmp/cache])
+ expect { |b| uploader.use_file(&b) }.to yield_with_args(%r{tmp/cache})
end
it "cleans up the cached file" do
diff --git a/spec/uploaders/packages/composer/cache_uploader_spec.rb b/spec/uploaders/packages/composer/cache_uploader_spec.rb
index 7eea4a839ab..56e8b28ef36 100644
--- a/spec/uploaders/packages/composer/cache_uploader_spec.rb
+++ b/spec/uploaders/packages/composer/cache_uploader_spec.rb
@@ -10,8 +10,8 @@ RSpec.describe Packages::Composer::CacheUploader do
it_behaves_like "builds correct paths",
store_dir: %r[^\h{2}/\h{2}/\h{64}/packages/composer_cache/\d+$],
- cache_dir: %r[/packages/tmp/cache],
- work_dir: %r[/packages/tmp/work]
+ cache_dir: %r{/packages/tmp/cache},
+ work_dir: %r{/packages/tmp/work}
context 'object store is remote' do
before do
diff --git a/spec/uploaders/packages/debian/component_file_uploader_spec.rb b/spec/uploaders/packages/debian/component_file_uploader_spec.rb
index 84ba751c737..ffc5d8085fa 100644
--- a/spec/uploaders/packages/debian/component_file_uploader_spec.rb
+++ b/spec/uploaders/packages/debian/component_file_uploader_spec.rb
@@ -13,8 +13,8 @@ RSpec.describe Packages::Debian::ComponentFileUploader do
it_behaves_like "builds correct paths",
store_dir: %r[^\h{2}/\h{2}/\h{64}/debian_#{container_type}_component_file/\d+$],
- cache_dir: %r[/packages/tmp/cache$],
- work_dir: %r[/packages/tmp/work$]
+ cache_dir: %r{/packages/tmp/cache$},
+ work_dir: %r{/packages/tmp/work$}
context 'object store is remote' do
before do
@@ -25,8 +25,8 @@ RSpec.describe Packages::Debian::ComponentFileUploader do
it_behaves_like "builds correct paths",
store_dir: %r[^\h{2}/\h{2}/\h{64}/debian_#{container_type}_component_file/\d+$],
- cache_dir: %r[/packages/tmp/cache$],
- work_dir: %r[/packages/tmp/work$]
+ cache_dir: %r{/packages/tmp/cache$},
+ work_dir: %r{/packages/tmp/work$}
end
describe 'remote file' do
diff --git a/spec/uploaders/packages/debian/distribution_release_file_uploader_spec.rb b/spec/uploaders/packages/debian/distribution_release_file_uploader_spec.rb
index df630569856..2086ab5966c 100644
--- a/spec/uploaders/packages/debian/distribution_release_file_uploader_spec.rb
+++ b/spec/uploaders/packages/debian/distribution_release_file_uploader_spec.rb
@@ -13,8 +13,8 @@ RSpec.describe Packages::Debian::DistributionReleaseFileUploader do
it_behaves_like "builds correct paths",
store_dir: %r[^\h{2}/\h{2}/\h{64}/debian_#{container_type}_distribution/\d+$],
- cache_dir: %r[/packages/tmp/cache$],
- work_dir: %r[/packages/tmp/work$]
+ cache_dir: %r{/packages/tmp/cache$},
+ work_dir: %r{/packages/tmp/work$}
context 'object store is remote' do
before do
@@ -25,8 +25,8 @@ RSpec.describe Packages::Debian::DistributionReleaseFileUploader do
it_behaves_like "builds correct paths",
store_dir: %r[^\h{2}/\h{2}/\h{64}/debian_#{container_type}_distribution/\d+$],
- cache_dir: %r[/packages/tmp/cache$],
- work_dir: %r[/packages/tmp/work$]
+ cache_dir: %r{/packages/tmp/cache$},
+ work_dir: %r{/packages/tmp/work$}
end
describe 'remote file' do
diff --git a/spec/uploaders/packages/package_file_uploader_spec.rb b/spec/uploaders/packages/package_file_uploader_spec.rb
index ddd9823d55c..36acb681669 100644
--- a/spec/uploaders/packages/package_file_uploader_spec.rb
+++ b/spec/uploaders/packages/package_file_uploader_spec.rb
@@ -10,8 +10,8 @@ RSpec.describe Packages::PackageFileUploader do
it_behaves_like "builds correct paths",
store_dir: %r[^\h{2}/\h{2}/\h{64}/packages/\d+/files/\d+$],
- cache_dir: %r[/packages/tmp/cache],
- work_dir: %r[/packages/tmp/work]
+ cache_dir: %r{/packages/tmp/cache},
+ work_dir: %r{/packages/tmp/work}
context 'object store is remote' do
before do
diff --git a/spec/uploaders/pages/deployment_uploader_spec.rb b/spec/uploaders/pages/deployment_uploader_spec.rb
index 7686efd4fe4..a5fe2dfe9ba 100644
--- a/spec/uploaders/pages/deployment_uploader_spec.rb
+++ b/spec/uploaders/pages/deployment_uploader_spec.rb
@@ -14,8 +14,8 @@ RSpec.describe Pages::DeploymentUploader do
it_behaves_like "builds correct paths",
store_dir: %r[/\h{2}/\h{2}/\h{64}/pages_deployments/\d+],
- cache_dir: %r[pages/@hashed/tmp/cache],
- work_dir: %r[pages/@hashed/tmp/work]
+ cache_dir: %r{pages/@hashed/tmp/cache},
+ work_dir: %r{pages/@hashed/tmp/work}
context 'when object store is REMOTE' do
before do
diff --git a/spec/uploaders/personal_file_uploader_spec.rb b/spec/uploaders/personal_file_uploader_spec.rb
index 58edf3f093d..de5ed8318e4 100644
--- a/spec/uploaders/personal_file_uploader_spec.rb
+++ b/spec/uploaders/personal_file_uploader_spec.rb
@@ -43,16 +43,16 @@ RSpec.describe PersonalFileUploader do
it 'builds correct paths for both local and remote storage' do
paths = uploader.upload_paths('test.jpg')
- expect(paths.first).to match(%r[\h+/test.jpg])
- expect(paths.second).to match(%r[^personal_snippet/\d+/\h+/test.jpg])
+ expect(paths.first).to match(%r{\h+/test.jpg})
+ expect(paths.second).to match(%r{^personal_snippet/\d+/\h+/test.jpg})
end
end
context 'object_store is LOCAL' do
it_behaves_like 'builds correct paths',
- store_dir: %r[uploads/-/system/personal_snippet/\d+/\h+],
- upload_path: %r[\h+/\S+],
- absolute_path: %r[#{CarrierWave.root}/uploads/-/system/personal_snippet/\d+/\h+/\S+$]
+ store_dir: %r{uploads/-/system/personal_snippet/\d+/\h+},
+ upload_path: %r{\h+/\S+},
+ absolute_path: %r{#{CarrierWave.root}/uploads/-/system/personal_snippet/\d+/\h+/\S+$}
it_behaves_like '#base_dir'
it_behaves_like '#to_h'
@@ -66,8 +66,8 @@ RSpec.describe PersonalFileUploader do
include_context 'with storage', described_class::Store::REMOTE
it_behaves_like 'builds correct paths',
- store_dir: %r[\d+/\h+],
- upload_path: %r[^personal_snippet/\d+/\h+/<filename>]
+ store_dir: %r{\d+/\h+},
+ upload_path: %r{^personal_snippet/\d+/\h+/<filename>}
it_behaves_like '#base_dir'
it_behaves_like '#to_h'
diff --git a/spec/validators/any_field_validator_spec.rb b/spec/validators/any_field_validator_spec.rb
index bede006abf6..2d3d3982828 100644
--- a/spec/validators/any_field_validator_spec.rb
+++ b/spec/validators/any_field_validator_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe AnyFieldValidator do
Class.new(ApplicationRecord) do
self.table_name = 'vulnerabilities'
- validates_with AnyFieldValidator, fields: %w(title description)
+ validates_with AnyFieldValidator, fields: %w[title description]
end
end
@@ -18,7 +18,7 @@ RSpec.describe AnyFieldValidator do
expect(validated_object.valid?).to be_falsey
expect(validated_object.errors.messages)
.to eq(base: ["At least one field of %{one_of_required_fields} must be present" %
- { one_of_required_fields: %w(title description) }])
+ { one_of_required_fields: %w[title description] }])
end
it 'validates if only one field is present' do
diff --git a/spec/validators/ip_cidr_array_validator_spec.rb b/spec/validators/ip_cidr_array_validator_spec.rb
new file mode 100644
index 00000000000..4ea46d714c2
--- /dev/null
+++ b/spec/validators/ip_cidr_array_validator_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe IpCidrArrayValidator, feature_category: :shared do
+ let(:model) do
+ Class.new do
+ include ActiveModel::Model
+ include ActiveModel::Validations
+
+ attr_accessor :cidr_array
+ alias_method :cidr_array_before_type_cast, :cidr_array
+
+ validates :cidr_array, ip_cidr_array: true
+ end.new
+ end
+
+ using RSpec::Parameterized::TableSyntax
+
+ # noinspection RubyMismatchedArgumentType - RubyMine is resolving `#|` from Array, instead of Rspec::Parameterized
+ where(:cidr_array, :validity, :errors) do
+ # rubocop:disable Layout/LineLength -- The RSpec table syntax often requires long lines for errors
+ nil | false | { cidr_array: ["must be an array of CIDR values"] }
+ '' | false | { cidr_array: ["must be an array of CIDR values"] }
+ ['172.0.0.1/256'] | false | { cidr_array: ["IP '172.0.0.1/256' is not a valid CIDR: Invalid netmask 256"] }
+ %w[172.0.0.1/24 invalid-CIDR] | false | { cidr_array: ["IP 'invalid-CIDR' is not a valid CIDR: IP should be followed by a slash followed by an integer subnet mask (for example: '192.168.1.0/24')"] }
+ %w[172.0.0.1/256 invalid-CIDR] | false | { cidr_array: ["IP '172.0.0.1/256' is not a valid CIDR: Invalid netmask 256", "IP 'invalid-CIDR' is not a valid CIDR: IP should be followed by a slash followed by an integer subnet mask (for example: '192.168.1.0/24')"] }
+ ['172.0.0.1/24', nil] | true | {}
+ %w[172.0.0.1/24 2001:db8::8:800:200c:417a/128] | true | {}
+ [] | true | {}
+ [nil] | true | {}
+ [''] | true | {}
+ # rubocop:enable Layout/LineLength
+ end
+
+ with_them do
+ before do
+ model.cidr_array = cidr_array
+ model.validate
+ end
+
+ it { expect(model.valid?).to eq(validity) }
+ it { expect(model.errors.messages).to eq(errors) }
+ end
+end
diff --git a/spec/validators/ip_cidr_validator_spec.rb b/spec/validators/ip_cidr_validator_spec.rb
new file mode 100644
index 00000000000..213991d9f4f
--- /dev/null
+++ b/spec/validators/ip_cidr_validator_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe IpCidrValidator, feature_category: :shared do
+ let(:model) do
+ Class.new do
+ include ActiveModel::Model
+ include ActiveModel::Validations
+
+ attr_accessor :cidr
+ alias_method :cidr_before_type_cast, :cidr
+
+ validates :cidr, ip_cidr: true
+ end.new
+ end
+
+ using RSpec::Parameterized::TableSyntax
+
+ where(:cidr, :validity, :errors) do
+ # rubocop:disable Layout/LineLength -- The RSpec table syntax often requires long lines for errors'
+ 'invalid-CIDR' | false | { cidr: ["IP 'invalid-CIDR' is not a valid CIDR: IP should be followed by a slash followed by an integer subnet mask (for example: '192.168.1.0/24')"] }
+ '172.0.0.1|256' | false | { cidr: ["IP '172.0.0.1|256' is not a valid CIDR: IP should be followed by a slash followed by an integer subnet mask (for example: '192.168.1.0/24')"] }
+ '172.0.0.1' | false | { cidr: ["IP '172.0.0.1' is not a valid CIDR: IP should be followed by a slash followed by an integer subnet mask (for example: '192.168.1.0/24')"] }
+ '172.0.0.1/2/12' | false | { cidr: ["IP '172.0.0.1/2/12' is not a valid CIDR: IP should be followed by a slash followed by an integer subnet mask (for example: '192.168.1.0/24')"] }
+ '172.0.0.1/256' | false | { cidr: ["IP '172.0.0.1/256' is not a valid CIDR: Invalid netmask 256"] }
+ '2001:db8::8:800:200c:417a/129' | false | { cidr: ["IP '2001:db8::8:800:200c:417a/129' is not a valid CIDR: Prefix must be in range 0..128, got: 129"] }
+ '2001:db8::8:800:200c:417a' | false | { cidr: ["IP '2001:db8::8:800:200c:417a' is not a valid CIDR: IP should be followed by a slash followed by an integer subnet mask (for example: '192.168.1.0/24')"] }
+ '2001:db8::8:800:200c:417a/128' | true | {}
+ '172.0.0.1/32' | true | {}
+ '' | true | {}
+ nil | true | {}
+ # rubocop:enable Layout/LineLength
+ end
+
+ with_them do
+ before do
+ model.cidr = cidr
+ model.validate
+ end
+
+ it { expect(model.valid?).to eq(validity) }
+ it { expect(model.errors.messages).to eq(errors) }
+ end
+end
diff --git a/spec/views/admin/application_settings/_repository_storage.html.haml_spec.rb b/spec/views/admin/application_settings/_repository_storage.html.haml_spec.rb
index 244157a3b14..34821149444 100644
--- a/spec/views/admin/application_settings/_repository_storage.html.haml_spec.rb
+++ b/spec/views/admin/application_settings/_repository_storage.html.haml_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe 'admin/application_settings/_repository_storage.html.haml' do
let(:app_settings) { build(:application_setting, repository_storages_weighted: repository_storages_weighted) }
before do
- stub_storage_settings({ 'default': {}, 'mepmep': {}, 'foobar': {} })
+ stub_storage_settings({ default: {}, mepmep: {}, foobar: {} })
assign(:application_setting, app_settings)
end
diff --git a/spec/views/ci/status/_badge.html.haml_spec.rb b/spec/views/ci/status/_badge.html.haml_spec.rb
deleted file mode 100644
index 65497de1608..00000000000
--- a/spec/views/ci/status/_badge.html.haml_spec.rb
+++ /dev/null
@@ -1,92 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'ci/status/_badge' do
- let(:user) { create(:user) }
- let(:project) { create(:project, :private) }
- let(:pipeline) { create(:ci_pipeline, project: project) }
-
- context 'when rendering status for build' do
- let(:build) do
- create(:ci_build, :success, pipeline: pipeline)
- end
-
- context 'when user has ability to see details' do
- before do
- project.add_developer(user)
- end
-
- it 'has link to build details page' do
- details_path = project_job_path(project, build)
-
- render_status(build)
-
- expect(rendered).to have_link 'Passed', href: details_path
- end
- end
-
- context 'when user do not have ability to see build details' do
- before do
- render_status(build)
- end
-
- it 'contains build status text' do
- expect(rendered).to have_content 'Passed'
- end
-
- it 'does not contain links' do
- expect(rendered).not_to have_link 'Passed'
- end
- end
- end
-
- context 'when rendering status for external job' do
- context 'when user has ability to see commit status details' do
- before do
- project.add_developer(user)
- end
-
- context 'status has external target url' do
- before do
- external_job = create(
- :generic_commit_status,
- status: :running,
- pipeline: pipeline,
- target_url: 'http://gitlab.com'
- )
-
- render_status(external_job)
- end
-
- it 'contains valid commit status text' do
- expect(rendered).to have_content 'Running'
- end
-
- it 'has link to external status page' do
- expect(rendered).to have_link 'Running', href: 'http://gitlab.com'
- end
- end
-
- context 'status do not have external target url' do
- before do
- external_job = create(:generic_commit_status, status: :canceled)
-
- render_status(external_job)
- end
-
- it 'contains valid commit status text' do
- expect(rendered).to have_content 'Canceled'
- end
-
- it 'has link to external status page' do
- expect(rendered).not_to have_link 'Canceled'
- end
- end
- end
- end
-
- def render_status(resource)
- render 'ci/status/badge', status: resource.detailed_status(user)
- end
-end
diff --git a/spec/views/ci/status/_icon.html.haml_spec.rb b/spec/views/ci/status/_icon.html.haml_spec.rb
index 78b19957cf0..a3058b20255 100644
--- a/spec/views/ci/status/_icon.html.haml_spec.rb
+++ b/spec/views/ci/status/_icon.html.haml_spec.rb
@@ -31,7 +31,7 @@ RSpec.describe 'ci/status/_icon' do
end
it 'contains build status text' do
- expect(rendered).to have_css('.ci-status-icon.ci-status-icon-success')
+ expect(rendered).to have_css('[data-testid="status_success_borderless-icon"]')
end
it 'does not contain links' do
@@ -59,7 +59,7 @@ RSpec.describe 'ci/status/_icon' do
end
it 'contains valid commit status text' do
- expect(rendered).to have_css('.ci-status-icon.ci-status-icon-running')
+ expect(rendered).to have_css('[data-testid="status_running_borderless-icon"]')
end
it 'has link to external status page' do
@@ -75,7 +75,7 @@ RSpec.describe 'ci/status/_icon' do
end
it 'contains valid commit status text' do
- expect(rendered).to have_css('.ci-status-icon.ci-status-icon-canceled')
+ expect(rendered).to have_css('[data-testid="status_canceled_borderless-icon"]')
end
it 'has link to external status page' do
diff --git a/spec/views/groups/edit.html.haml_spec.rb b/spec/views/groups/edit.html.haml_spec.rb
index 4c49f1529f2..f33a4190bf8 100644
--- a/spec/views/groups/edit.html.haml_spec.rb
+++ b/spec/views/groups/edit.html.haml_spec.rb
@@ -7,7 +7,6 @@ RSpec.describe 'groups/edit.html.haml', feature_category: :groups_and_projects d
before do
stub_template 'groups/settings/_code_suggestions' => ''
- stub_template 'groups/settings/_ai_third_party_settings' => ''
end
describe '"Share with group lock" setting' do
diff --git a/spec/views/layouts/_head.html.haml_spec.rb b/spec/views/layouts/_head.html.haml_spec.rb
index 504a9492d7a..56936dbafcf 100644
--- a/spec/views/layouts/_head.html.haml_spec.rb
+++ b/spec/views/layouts/_head.html.haml_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe 'layouts/_head' do
render
- expect(rendered).to match(%{content="foo&quot; http-equiv=&quot;refresh"})
+ expect(rendered).to match(%(content="foo&quot; http-equiv=&quot;refresh"))
end
it 'escapes HTML-safe strings in page_description' do
@@ -23,7 +23,7 @@ RSpec.describe 'layouts/_head' do
render
- expect(rendered).to match(%{content="foo&quot; http-equiv=&quot;refresh"})
+ expect(rendered).to match(%(content="foo&quot; http-equiv=&quot;refresh"))
end
it 'escapes HTML-safe strings in page_image' do
@@ -31,7 +31,7 @@ RSpec.describe 'layouts/_head' do
render
- expect(rendered).to match(%{content="foo&quot; http-equiv=&quot;refresh"})
+ expect(rendered).to match(%(content="foo&quot; http-equiv=&quot;refresh"))
end
context 'when an asset_host is set' do
@@ -101,7 +101,7 @@ RSpec.describe 'layouts/_head' do
render
expect(rendered).to match(%r{<script.*>.*var u="//#{matomo_host}/".*</script>}m)
- expect(rendered).to match(%r(<noscript>.*<img src="//#{matomo_host}/matomo.php.*</noscript>))
+ expect(rendered).to match(%r{<noscript>.*<img src="//#{matomo_host}/matomo.php.*</noscript>})
expect(rendered).not_to include('_paq.push(["disableCookies"])')
end
@@ -120,6 +120,6 @@ RSpec.describe 'layouts/_head' do
def stub_helper_with_safe_string(method)
allow_any_instance_of(PageLayoutHelper).to receive(method)
- .and_return(%q{foo" http-equiv="refresh}.html_safe)
+ .and_return(%q(foo" http-equiv="refresh).html_safe)
end
end
diff --git a/spec/views/layouts/application.html.haml_spec.rb b/spec/views/layouts/application.html.haml_spec.rb
index 825e295b73d..20bef2a3685 100644
--- a/spec/views/layouts/application.html.haml_spec.rb
+++ b/spec/views/layouts/application.html.haml_spec.rb
@@ -80,7 +80,6 @@ RSpec.describe 'layouts/application' do
before do
allow(view).to receive(:current_user).and_return(nil)
allow(view).to receive(:current_user_mode).and_return(Gitlab::Auth::CurrentUserMode.new(nil))
- Feature.enable(:super_sidebar_logged_out)
end
it 'renders the new marketing header for logged-out users' do
diff --git a/spec/views/layouts/header/_new_dropdown.haml_spec.rb b/spec/views/layouts/header/_new_dropdown.haml_spec.rb
index 2c5882fce3d..ef028da7ab9 100644
--- a/spec/views/layouts/header/_new_dropdown.haml_spec.rb
+++ b/spec/views/layouts/header/_new_dropdown.haml_spec.rb
@@ -185,6 +185,11 @@ RSpec.describe 'layouts/header/_new_dropdown', feature_category: :navigation do
context 'when the user is not allowed to do anything' do
let(:user) { create(:user, :external) } # rubocop:disable RSpec/FactoryBot/AvoidCreate
+ before do
+ allow(user).to receive(:can?).and_call_original
+ allow(user).to receive(:can?).with(:create_organization).and_return(false)
+ end
+
it 'is nil' do
# We have to use `view.render` because `render` causes issues
# https://github.com/rails/rails/issues/41320
diff --git a/spec/views/layouts/header/_super_sidebar_logged_out.html.haml_spec.rb b/spec/views/layouts/header/_super_sidebar_logged_out.html.haml_spec.rb
index f81e8c5badf..7f49f96de03 100644
--- a/spec/views/layouts/header/_super_sidebar_logged_out.html.haml_spec.rb
+++ b/spec/views/layouts/header/_super_sidebar_logged_out.html.haml_spec.rb
@@ -5,7 +5,6 @@ require 'spec_helper'
RSpec.describe 'layouts/header/_super_sidebar_logged_out', feature_category: :navigation do
before do
allow(view).to receive(:current_user_mode).and_return(Gitlab::Auth::CurrentUserMode.new(nil))
- Feature.enable(:super_sidebar_logged_out)
end
context 'on gitlab.com' do
diff --git a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
index 3ec731c8eb7..34debcab5f7 100644
--- a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
+++ b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
@@ -23,7 +23,7 @@ RSpec.describe 'layouts/nav/sidebar/_project', feature_category: :navigation do
it 'has a link to the project path' do
render
- expect(rendered).to have_link(project.name, href: project_path(project), class: %w(shortcuts-project rspec-project-link))
+ expect(rendered).to have_link(project.name, href: project_path(project), class: 'shortcuts-project')
expect(rendered).to have_selector("[aria-label=\"#{project.name}\"]")
end
end
@@ -32,7 +32,7 @@ RSpec.describe 'layouts/nav/sidebar/_project', feature_category: :navigation do
it 'has a link to the project activity path' do
render
- expect(rendered).to have_link('Project information', href: activity_project_path(project), class: %w(shortcuts-project-information))
+ expect(rendered).to have_link('Project information', href: activity_project_path(project), class: %w[shortcuts-project-information])
expect(rendered).to have_selector('[aria-label="Project information"]')
end
diff --git a/spec/views/layouts/snippets.html.haml_spec.rb b/spec/views/layouts/snippets.html.haml_spec.rb
deleted file mode 100644
index b7139f84174..00000000000
--- a/spec/views/layouts/snippets.html.haml_spec.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'layouts/snippets', feature_category: :source_code_management do
- before do
- allow(view).to receive(:current_user).and_return(user)
- allow(view).to receive(:current_user_mode).and_return(Gitlab::Auth::CurrentUserMode.new(user))
- end
-
- describe 'sidebar' do
- context 'when signed in' do
- let(:user) { build_stubbed(:user, :no_super_sidebar) }
-
- it 'renders the "Your work" sidebar' do
- render
-
- expect(rendered).to have_css('aside.nav-sidebar[aria-label="Your work"]')
- end
- end
-
- context 'when not signed in' do
- let(:user) { nil }
-
- it 'renders no sidebar' do
- render
-
- expect(rendered).not_to have_css('aside.nav-sidebar')
- end
- end
- end
-end
diff --git a/spec/views/projects/commit/branches.html.haml_spec.rb b/spec/views/projects/commit/branches.html.haml_spec.rb
index f1064be3047..d6fbf6453c0 100644
--- a/spec/views/projects/commit/branches.html.haml_spec.rb
+++ b/spec/views/projects/commit/branches.html.haml_spec.rb
@@ -61,7 +61,7 @@ RSpec.describe 'projects/commit/branches.html.haml' do
before do
assign(:branches, ['master'])
assign(:branches_limit_exceeded, true)
- assign(:tags, %w(tag1 tag2))
+ assign(:tags, %w[tag1 tag2])
assign(:tags_limit_exceeded, false)
render
diff --git a/spec/views/projects/commits/_commit.html.haml_spec.rb b/spec/views/projects/commits/_commit.html.haml_spec.rb
index d45f1da86e8..cc73418ea1e 100644
--- a/spec/views/projects/commits/_commit.html.haml_spec.rb
+++ b/spec/views/projects/commits/_commit.html.haml_spec.rb
@@ -74,7 +74,7 @@ RSpec.describe 'projects/commits/_commit.html.haml' do
commit: commit
}
- expect(rendered).not_to have_css("[data-testid='ci-status-badge-legacy']")
+ expect(rendered).not_to have_css("[data-testid='ci-icon']")
end
end
@@ -91,7 +91,7 @@ RSpec.describe 'projects/commits/_commit.html.haml' do
commit: commit
}
- expect(rendered).to have_css("[data-testid='ci-status-badge-legacy']")
+ expect(rendered).to have_css("[data-testid='ci-icon']")
end
end
@@ -103,7 +103,7 @@ RSpec.describe 'projects/commits/_commit.html.haml' do
commit: commit
}
- expect(rendered).not_to have_css("[data-testid='ci-status-badge-legacy']")
+ expect(rendered).not_to have_css("[data-testid='ci-icon']")
end
end
end
diff --git a/spec/views/projects/issues/_related_branches.html.haml_spec.rb b/spec/views/projects/issues/_related_branches.html.haml_spec.rb
index deec2db6865..d37c8c762a3 100644
--- a/spec/views/projects/issues/_related_branches.html.haml_spec.rb
+++ b/spec/views/projects/issues/_related_branches.html.haml_spec.rb
@@ -25,8 +25,7 @@ RSpec.describe 'projects/issues/_related_branches' do
expect(rendered).to have_text('other')
expect(rendered).to have_link(href: 'link-to-feature')
expect(rendered).to have_link(href: 'link-to-other')
- expect(rendered).to have_css('.related-branch-ci-status')
- expect(rendered).to have_css('.ci-status-icon')
+ expect(rendered).to have_css('[data-testid="ci-icon"]')
expect(rendered).to have_css('.related-branch-info')
end
end
diff --git a/spec/views/projects/pages/new.html.haml_spec.rb b/spec/views/projects/pages/new.html.haml_spec.rb
index 919b2fe84ee..d7295d60c51 100644
--- a/spec/views/projects/pages/new.html.haml_spec.rb
+++ b/spec/views/projects/pages/new.html.haml_spec.rb
@@ -13,30 +13,8 @@ RSpec.describe 'projects/pages/new' do
allow(view).to receive(:current_user).and_return(user)
end
- describe 'with onboarding wizard feature enabled' do
- before do
- Feature.enable(:use_pipeline_wizard_for_pages)
- end
-
- it "shows the onboarding wizard" do
- render
- expect(rendered).to have_selector('#js-pages')
- end
- end
-
- describe 'with onboarding wizard feature disabled' do
- before do
- Feature.disable(:use_pipeline_wizard_for_pages)
- end
-
- it "does not show the onboarding wizard" do
- render
- expect(rendered).not_to have_selector('#js-pages')
- end
-
- it "renders the usage instructions" do
- render
- expect(rendered).to render_template('projects/pages/_use')
- end
+ it "shows the onboarding wizard" do
+ render
+ expect(rendered).to have_selector('#js-pages')
end
end
diff --git a/spec/views/projects/tags/index.html.haml_spec.rb b/spec/views/projects/tags/index.html.haml_spec.rb
index 01e8d23fb9f..0ac5efa2e6d 100644
--- a/spec/views/projects/tags/index.html.haml_spec.rb
+++ b/spec/views/projects/tags/index.html.haml_spec.rb
@@ -91,7 +91,7 @@ RSpec.describe 'projects/tags/index.html.haml' do
render
- expect(page.find('.tags .content-list li', text: tag)).to have_css '.gl-badge .ci-status-icon-success'
+ 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
diff --git a/spec/views/search/show.html.haml_spec.rb b/spec/views/search/show.html.haml_spec.rb
index 0158a9049b9..e6d2ef02c9a 100644
--- a/spec/views/search/show.html.haml_spec.rb
+++ b/spec/views/search/show.html.haml_spec.rb
@@ -29,13 +29,6 @@ RSpec.describe 'search/show', feature_category: :global_search do
end
context 'when the search page is opened' do
- it 'displays the title' do
- render
-
- expect(rendered).to have_selector('h1.page-title', text: 'Search')
- expect(rendered).not_to have_selector('h1.page-title code')
- end
-
it 'does not render the results partial' do
render
diff --git a/spec/workers/abuse/spam_abuse_events_worker_spec.rb b/spec/workers/abuse/spam_abuse_events_worker_spec.rb
new file mode 100644
index 00000000000..9198636e114
--- /dev/null
+++ b/spec/workers/abuse/spam_abuse_events_worker_spec.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Abuse::SpamAbuseEventsWorker, :clean_gitlab_redis_shared_state, feature_category: :instance_resiliency do
+ let(:worker) { described_class.new }
+ let_it_be(:user) { create(:user) }
+
+ let(:params) do
+ {
+ user_id: user.id,
+ title: 'Test title',
+ description: 'Test description',
+ source_ip: '1.2.3.4',
+ user_agent: 'fake-user-agent',
+ noteable_type: 'Issue',
+ verdict: 'BLOCK_USER'
+ }
+ end
+
+ shared_examples 'creates an abuse event with the correct data' do
+ it do
+ expect { worker.perform(params) }.to change { Abuse::Event.count }.from(0).to(1)
+ expect(Abuse::Event.last.attributes).to include({
+ abuse_report_id: report_id,
+ category: "spam",
+ metadata: params.except(:user_id),
+ source: "spamcheck",
+ user_id: params[:user_id]
+ }.deep_stringify_keys)
+ end
+ end
+
+ it_behaves_like 'an idempotent worker' do
+ let(:job_args) { [params] }
+ end
+
+ context "when the user does not exist" do
+ let(:log_payload) { { 'message' => 'User not found.', 'user_id' => user.id } }
+
+ before do
+ allow(User).to receive(:find_by_id).with(user.id).and_return(nil)
+ end
+
+ it 'logs an error' do
+ expect(Sidekiq.logger).to receive(:info).with(hash_including(log_payload))
+
+ expect { worker.perform(params) }.not_to raise_exception
+ end
+
+ it 'does not report the user' do
+ expect(described_class).not_to receive(:report_user).with(user.id)
+
+ worker.perform(params)
+ end
+ end
+
+ context "when the user exists" do
+ context 'and there is an existing abuse report' do
+ let_it_be(:abuse_report) do
+ create(:abuse_report, user: user, reporter: Users::Internal.security_bot, message: 'Test report')
+ end
+
+ it_behaves_like 'creates an abuse event with the correct data' do
+ let(:report_id) { abuse_report.id }
+ end
+ end
+
+ context 'and there is no existing abuse report' do
+ it 'creates an abuse report with the correct data' do
+ expect { worker.perform(params) }.to change { AbuseReport.count }.from(0).to(1)
+ expect(AbuseReport.last.attributes).to include({
+ reporter_id: Users::Internal.security_bot.id,
+ user_id: user.id,
+ category: "spam",
+ message: "User reported for abuse based on spam verdict"
+ }.stringify_keys)
+ end
+
+ it_behaves_like 'creates an abuse event with the correct data' do
+ let(:report_id) { AbuseReport.last.attributes["id"] }
+ end
+ end
+ end
+end
diff --git a/spec/workers/activity_pub/projects/releases_subscription_worker_spec.rb b/spec/workers/activity_pub/projects/releases_subscription_worker_spec.rb
new file mode 100644
index 00000000000..c41c1bb8e1c
--- /dev/null
+++ b/spec/workers/activity_pub/projects/releases_subscription_worker_spec.rb
@@ -0,0 +1,128 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ActivityPub::Projects::ReleasesSubscriptionWorker, feature_category: :release_orchestration do
+ describe '#perform' do
+ let(:worker) { described_class.new }
+ let(:project) { build_stubbed :project, :public }
+ let(:subscription) { build_stubbed :activity_pub_releases_subscription, project: project }
+ let(:inbox_resolver_service) { instance_double('ActivityPub::InboxResolverService', execute: true) }
+ let(:accept_follow_service) { instance_double('ActivityPub::AcceptFollowService', execute: true) }
+
+ before do
+ allow(ActivityPub::ReleasesSubscription).to receive(:find_by_id) { subscription }
+ allow(subscription).to receive(:destroy).and_return(true)
+ allow(ActivityPub::InboxResolverService).to receive(:new) { inbox_resolver_service }
+ allow(ActivityPub::AcceptFollowService).to receive(:new) { accept_follow_service }
+ end
+
+ context 'when the project is public' do
+ before do
+ worker.perform(subscription.id)
+ end
+
+ context 'when inbox url has not been resolved yet' do
+ it 'calls the service to resolve the inbox url' do
+ expect(inbox_resolver_service).to have_received(:execute)
+ end
+
+ it 'calls the service to send out the Accept activity' do
+ expect(accept_follow_service).to have_received(:execute)
+ end
+ end
+
+ context 'when inbox url has been resolved' do
+ context 'when shared inbox url has not been resolved' do
+ let(:subscription) { build_stubbed :activity_pub_releases_subscription, :inbox, project: project }
+
+ it 'calls the service to resolve the inbox url' do
+ expect(inbox_resolver_service).to have_received(:execute)
+ end
+
+ it 'calls the service to send out the Accept activity' do
+ expect(accept_follow_service).to have_received(:execute)
+ end
+ end
+
+ context 'when shared inbox url has been resolved' do
+ let(:subscription) do
+ build_stubbed :activity_pub_releases_subscription, :inbox, :shared_inbox, project: project
+ end
+
+ it 'does not call the service to resolve the inbox url' do
+ expect(inbox_resolver_service).not_to have_received(:execute)
+ end
+
+ it 'calls the service to send out the Accept activity' do
+ expect(accept_follow_service).to have_received(:execute)
+ end
+ end
+ end
+ end
+
+ shared_examples 'failed job' do
+ it 'does not resolve inbox url' do
+ expect(inbox_resolver_service).not_to have_received(:execute)
+ end
+
+ it 'does not send out Accept activity' do
+ expect(accept_follow_service).not_to have_received(:execute)
+ end
+ end
+
+ context 'when the subscription does not exist' do
+ before do
+ allow(ActivityPub::ReleasesSubscription).to receive(:find_by_id).and_return(nil)
+ worker.perform(subscription.id)
+ end
+
+ it_behaves_like 'failed job'
+ end
+
+ shared_examples 'non public project' do
+ it_behaves_like 'failed job'
+
+ it 'deletes the subscription' do
+ expect(subscription).to have_received(:destroy)
+ end
+ end
+
+ context 'when project has changed to internal' do
+ before do
+ worker.perform(subscription.id)
+ end
+
+ let(:project) { build_stubbed :project, :internal }
+
+ it_behaves_like 'non public project'
+ end
+
+ context 'when project has changed to private' do
+ before do
+ worker.perform(subscription.id)
+ end
+
+ let(:project) { build_stubbed :project, :private }
+
+ it_behaves_like 'non public project'
+ end
+ end
+
+ describe '#sidekiq_retries_exhausted' do
+ let(:project) { build_stubbed :project, :public }
+ let(:subscription) { build_stubbed :activity_pub_releases_subscription, project: project }
+ let(:job) { { 'args' => [project.id, subscription.id], 'error_message' => 'Error' } }
+
+ before do
+ allow(Project).to receive(:find) { project }
+ allow(ActivityPub::ReleasesSubscription).to receive(:find_by_id) { subscription }
+ end
+
+ it 'delete the subscription' do
+ expect(subscription).to receive(:destroy)
+
+ described_class.sidekiq_retries_exhausted_block.call(job, StandardError.new)
+ end
+ end
+end
diff --git a/spec/workers/bulk_import_worker_spec.rb b/spec/workers/bulk_import_worker_spec.rb
index 8b73549e071..2983902dff7 100644
--- a/spec/workers/bulk_import_worker_spec.rb
+++ b/spec/workers/bulk_import_worker_spec.rb
@@ -28,5 +28,19 @@ RSpec.describe BulkImportWorker, feature_category: :importers do
end
end
end
+
+ it_behaves_like 'an idempotent worker'
+ end
+
+ describe '#sidekiq_retries_exhausted' do
+ it 'logs export failure and marks entity as failed' do
+ exception = StandardError.new('Exhausted error!')
+
+ expect(Gitlab::ErrorTracking).to receive(:track_exception).with(exception, bulk_import_id: bulk_import.id)
+
+ described_class.sidekiq_retries_exhausted_block.call({ 'args' => job_args }, exception)
+
+ expect(bulk_import.reload.failed?).to eq(true)
+ end
end
end
diff --git a/spec/workers/bulk_imports/entity_worker_spec.rb b/spec/workers/bulk_imports/entity_worker_spec.rb
index 5f948906c08..690555aa08f 100644
--- a/spec/workers/bulk_imports/entity_worker_spec.rb
+++ b/spec/workers/bulk_imports/entity_worker_spec.rb
@@ -34,7 +34,7 @@ RSpec.describe BulkImports::EntityWorker, feature_category: :importers do
end
it 'enqueues the pipeline workers of the first stage and then re-enqueues itself' do
- expect_next_instance_of(Gitlab::Import::Logger) do |logger|
+ expect_next_instance_of(BulkImports::Logger) do |logger|
expect(logger).to receive(:info).with(hash_including('message' => 'Stage starting', 'entity_stage' => 0))
expect(logger).to receive(:info).with(hash_including('message' => 'Stage running', 'entity_stage' => 0))
end
@@ -49,13 +49,17 @@ RSpec.describe BulkImports::EntityWorker, feature_category: :importers do
end
end
+ it 'has the option to reschedule once if deduplicated' do
+ expect(described_class.get_deduplication_options).to include({ if_deduplicated: :reschedule_once })
+ end
+
context 'when pipeline workers from a stage are running' do
before do
pipeline_tracker.enqueue!
end
it 'does not enqueue the pipeline workers from the next stage and re-enqueues itself' do
- expect_next_instance_of(Gitlab::Import::Logger) do |logger|
+ expect_next_instance_of(BulkImports::Logger) do |logger|
expect(logger).to receive(:info).with(hash_including('message' => 'Stage running', 'entity_stage' => 0))
end
@@ -72,7 +76,7 @@ RSpec.describe BulkImports::EntityWorker, feature_category: :importers do
end
it 'enqueues the pipeline workers from the next stage and re-enqueues itself' do
- expect_next_instance_of(Gitlab::Import::Logger) do |logger|
+ expect_next_instance_of(BulkImports::Logger) do |logger|
expect(logger).to receive(:info).with(hash_including('message' => 'Stage starting', 'entity_stage' => 1))
end
@@ -114,29 +118,25 @@ RSpec.describe BulkImports::EntityWorker, feature_category: :importers do
end
end
- it 'logs and tracks the raised exceptions' do
- exception = StandardError.new('Error!')
-
- expect(BulkImports::PipelineWorker)
- .to receive(:perform_async)
- .and_raise(exception)
-
- expect(Gitlab::ErrorTracking)
- .to receive(:track_exception)
- .with(
- exception,
- hash_including(
- bulk_import_entity_id: entity.id,
- bulk_import_id: entity.bulk_import_id,
- bulk_import_entity_type: entity.source_type,
- source_full_path: entity.source_full_path,
- source_version: entity.bulk_import.source_version_info.to_s,
- importer: 'gitlab_migration'
- )
- )
-
- worker.perform(entity.id)
-
- expect(entity.reload.failed?).to eq(true)
+ describe '#sidekiq_retries_exhausted' do
+ it 'logs export failure and marks entity as failed' do
+ exception = StandardError.new('Exhausted error!')
+
+ expect(Gitlab::ErrorTracking)
+ .to receive(:track_exception)
+ .with(exception, a_hash_including(
+ message: "Request to export #{entity.source_type} failed",
+ bulk_import_entity_id: entity.id,
+ bulk_import_id: entity.bulk_import_id,
+ bulk_import_entity_type: entity.source_type,
+ source_full_path: entity.source_full_path,
+ source_version: entity.bulk_import.source_version_info.to_s,
+ importer: 'gitlab_migration'
+ ))
+
+ described_class.sidekiq_retries_exhausted_block.call({ 'args' => [entity.id] }, exception)
+
+ expect(entity.reload.failed?).to eq(true)
+ end
end
end
diff --git a/spec/workers/bulk_imports/export_request_worker_spec.rb b/spec/workers/bulk_imports/export_request_worker_spec.rb
index 0acc44c5cbf..e9d0b6b24b2 100644
--- a/spec/workers/bulk_imports/export_request_worker_spec.rb
+++ b/spec/workers/bulk_imports/export_request_worker_spec.rb
@@ -71,7 +71,7 @@ RSpec.describe BulkImports::ExportRequestWorker, feature_category: :importers do
entity.update!(source_xid: nil)
- expect_next_instance_of(Gitlab::Import::Logger) do |logger|
+ expect_next_instance_of(BulkImports::Logger) do |logger|
expect(logger).to receive(:error).with(
a_hash_including(
'bulk_import_entity_id' => entity.id,
@@ -82,7 +82,6 @@ RSpec.describe BulkImports::ExportRequestWorker, feature_category: :importers do
'exception.class' => 'NoMethodError',
'exception.message' => /^undefined method `model_id' for nil:NilClass/,
'message' => 'Failed to fetch source entity id',
- 'importer' => 'gitlab_migration',
'source_version' => entity.bulk_import.source_version_info.to_s
)
).twice
diff --git a/spec/workers/bulk_imports/finish_batched_pipeline_worker_spec.rb b/spec/workers/bulk_imports/finish_batched_pipeline_worker_spec.rb
index 5beb11c64aa..59ae4205c0f 100644
--- a/spec/workers/bulk_imports/finish_batched_pipeline_worker_spec.rb
+++ b/spec/workers/bulk_imports/finish_batched_pipeline_worker_spec.rb
@@ -5,40 +5,67 @@ require 'spec_helper'
RSpec.describe BulkImports::FinishBatchedPipelineWorker, feature_category: :importers do
let_it_be(:bulk_import) { create(:bulk_import) }
let_it_be(:config) { create(:bulk_import_configuration, bulk_import: bulk_import) }
- let_it_be(:entity) { create(:bulk_import_entity, bulk_import: bulk_import) }
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:entity) do
+ create(
+ :bulk_import_entity,
+ :project_entity,
+ project: project,
+ bulk_import: bulk_import
+ )
+ end
- let(:status_event) { :finish }
- let(:pipeline_tracker) { create(:bulk_import_tracker, :started, :batched, entity: entity) }
+ let(:pipeline_class) do
+ Class.new do
+ def initialize(_); end
+
+ def on_finish; end
+ end
+ end
+
+ let(:pipeline_tracker) do
+ create(
+ :bulk_import_tracker,
+ :started,
+ :batched,
+ entity: entity,
+ pipeline_name: 'FakePipeline'
+ )
+ end
subject(:worker) { described_class.new }
describe '#perform' do
- context 'when job version is nil' do
- before do
- allow(subject).to receive(:job_version).and_return(nil)
- end
+ before do
+ stub_const('FakePipeline', pipeline_class)
- it 'finishes pipeline and enqueues entity worker' do
- expect(BulkImports::EntityWorker).to receive(:perform_async)
- .with(entity.id)
+ allow_next_instance_of(BulkImports::Projects::Stage) do |instance|
+ allow(instance).to receive(:pipelines)
+ .and_return([{ stage: 0, pipeline: pipeline_class }])
+ end
+ end
- subject.perform(pipeline_tracker.id)
+ context 'when import is in progress' do
+ it 'marks the tracker as finished' do
+ expect_next_instance_of(BulkImports::Logger) do |logger|
+ expect(logger).to receive(:info).with(
+ a_hash_including('message' => 'Tracker finished')
+ )
+ end
- expect(pipeline_tracker.reload.finished?).to eq(true)
+ expect { subject.perform(pipeline_tracker.id) }
+ .to change { pipeline_tracker.reload.finished? }
+ .from(false).to(true)
end
- end
- context 'when job version is present' do
- it 'finishes pipeline and does not enqueues entity worker' do
- expect(BulkImports::EntityWorker).not_to receive(:perform_async)
+ it "calls the pipeline's `#on_finish`" do
+ expect_next_instance_of(pipeline_class) do |pipeline|
+ expect(pipeline).to receive(:on_finish)
+ end
subject.perform(pipeline_tracker.id)
-
- expect(pipeline_tracker.reload.finished?).to eq(true)
end
- end
- context 'when import is in progress' do
it 're-enqueues for any started batches' do
create(:bulk_import_batch_tracker, :started, tracker: pipeline_tracker)
@@ -66,35 +93,51 @@ RSpec.describe BulkImports::FinishBatchedPipelineWorker, feature_category: :impo
it 'fails pipeline tracker and its batches' do
create(:bulk_import_batch_tracker, :finished, tracker: pipeline_tracker)
+ expect_next_instance_of(BulkImports::Logger) do |logger|
+ expect(logger).to receive(:error).with(
+ a_hash_including('message' => 'Tracker stale. Failing batches and tracker')
+ )
+ end
+
subject.perform(pipeline_tracker.id)
expect(pipeline_tracker.reload.failed?).to eq(true)
expect(pipeline_tracker.batches.first.reload.failed?).to eq(true)
end
end
+ end
- context 'when pipeline is not batched' do
- let(:pipeline_tracker) { create(:bulk_import_tracker, :started, entity: entity) }
+ shared_examples 'does nothing' do
+ it "does not call the tracker's `#finish!`" do
+ expect_next_found_instance_of(BulkImports::Tracker) do |instance|
+ expect(instance).not_to receive(:finish!)
+ end
- it 'returns' do
- expect_next_instance_of(BulkImports::Tracker) do |instance|
- expect(instance).not_to receive(:finish!)
- end
+ subject.perform(pipeline_tracker.id)
+ end
- subject.perform(pipeline_tracker.id)
- end
+ it "does not call the pipeline's `#on_finish`" do
+ expect(pipeline_class).not_to receive(:new)
+
+ subject.perform(pipeline_tracker.id)
end
+ end
- context 'when pipeline is not started' do
- let(:status_event) { :start }
+ context 'when tracker is not batched' do
+ let(:pipeline_tracker) { create(:bulk_import_tracker, :started, entity: entity, batched: false) }
- it 'returns' do
- expect_next_instance_of(BulkImports::Tracker) do |instance|
- expect(instance).not_to receive(:finish!)
- end
+ include_examples 'does nothing'
+ end
- described_class.new.perform(pipeline_tracker.id)
- end
- end
+ context 'when tracker is not started' do
+ let(:pipeline_tracker) { create(:bulk_import_tracker, :batched, :finished, entity: entity) }
+
+ include_examples 'does nothing'
+ end
+
+ context 'when pipeline is enqueued' do
+ let(:pipeline_tracker) { create(:bulk_import_tracker, status: 3, entity: entity) }
+
+ include_examples 'does nothing'
end
end
diff --git a/spec/workers/bulk_imports/pipeline_batch_worker_spec.rb b/spec/workers/bulk_imports/pipeline_batch_worker_spec.rb
index 78ce52c41b4..c459c17b1bc 100644
--- a/spec/workers/bulk_imports/pipeline_batch_worker_spec.rb
+++ b/spec/workers/bulk_imports/pipeline_batch_worker_spec.rb
@@ -9,9 +9,17 @@ RSpec.describe BulkImports::PipelineBatchWorker, feature_category: :importers do
let(:pipeline_class) do
Class.new do
- def initialize(_); end
+ def initialize(context)
+ @context = context
+ end
+
+ def self.relation
+ 'labels'
+ end
- def run; end
+ def run
+ @context.tracker.finish!
+ end
def self.file_extraction_pipeline?
false
@@ -35,7 +43,6 @@ RSpec.describe BulkImports::PipelineBatchWorker, feature_category: :importers do
before do
stub_const('FakePipeline', pipeline_class)
- allow(subject).to receive(:jid).and_return('jid')
allow(entity).to receive(:pipeline_exists?).with('FakePipeline').and_return(true)
allow_next_instance_of(BulkImports::Groups::Stage) do |instance|
allow(instance)
@@ -44,51 +51,94 @@ RSpec.describe BulkImports::PipelineBatchWorker, feature_category: :importers do
end
end
+ include_examples 'an idempotent worker' do
+ let(:job_args) { batch.id }
+ let(:tracker) { create(:bulk_import_tracker, :started, entity: entity, pipeline_name: 'FakePipeline') }
+
+ it 'processes the batch once' do
+ allow_next_instance_of(pipeline_class) do |instance|
+ expect(instance).to receive(:run).once.and_call_original
+ end
+
+ perform_multiple(job_args)
+
+ expect(batch.reload).to be_finished
+ end
+ end
+
describe '#perform' do
it 'runs the given pipeline batch successfully' do
expect(BulkImports::FinishBatchedPipelineWorker).to receive(:perform_async).with(tracker.id)
+ expect_next_instance_of(BulkImports::Logger) do |logger|
+ expect(logger).to receive(:info).with(a_hash_including('message' => 'Batch tracker started'))
+ expect(logger).to receive(:info).with(a_hash_including('message' => 'Batch tracker finished'))
+ end
- subject.perform(batch.id)
+ worker.perform(batch.id)
expect(batch.reload).to be_finished
end
- context 'when tracker is failed' do
- let(:tracker) { create(:bulk_import_tracker, :failed) }
+ context 'with tracker status' do
+ context 'when tracker is failed' do
+ let(:tracker) { create(:bulk_import_tracker, :failed) }
- it 'skips the batch' do
- subject.perform(batch.id)
+ it 'skips the batch' do
+ worker.perform(batch.id)
- expect(batch.reload).to be_skipped
+ expect(batch.reload).to be_skipped
+ end
end
- end
- context 'when tracker is finished' do
- let(:tracker) { create(:bulk_import_tracker, :finished) }
+ context 'when tracker is finished' do
+ let(:tracker) { create(:bulk_import_tracker, :finished) }
- it 'skips the batch' do
- subject.perform(batch.id)
+ it 'skips the batch' do
+ worker.perform(batch.id)
- expect(batch.reload).to be_skipped
+ expect(batch.reload).to be_skipped
+ end
end
end
- context 'when batch status is started' do
- let(:batch) { create(:bulk_import_batch_tracker, :started, tracker: tracker) }
+ context 'with batch status' do
+ context 'when batch status is started' do
+ let(:batch) { create(:bulk_import_batch_tracker, :started, tracker: tracker) }
+
+ it 'finishes the batch' do
+ worker.perform(batch.id)
+
+ expect(batch.reload).to be_finished
+ end
+ end
+
+ context 'when batch status is created' do
+ let(:batch) { create(:bulk_import_batch_tracker, :created, tracker: tracker) }
- it 'runs the given pipeline batch successfully' do
- subject.perform(batch.id)
+ it 'finishes the batch' do
+ worker.perform(batch.id)
- expect(batch.reload).to be_finished
+ expect(batch.reload).to be_finished
+ end
+ end
+
+ context 'when batch status is finished' do
+ let(:batch) { create(:bulk_import_batch_tracker, :finished, tracker: tracker) }
+
+ it 'stays finished' do
+ worker.perform(batch.id)
+
+ expect(batch.reload).to be_finished
+ end
end
end
context 'when exclusive lease cannot be obtained' do
it 'does not run the pipeline' do
- expect(subject).to receive(:try_obtain_lease).and_return(false)
- expect(subject).not_to receive(:run)
+ expect(worker).to receive(:try_obtain_lease).and_return(false)
+ expect(worker).not_to receive(:run)
- subject.perform(batch.id)
+ worker.perform(batch.id)
end
end
@@ -104,44 +154,107 @@ RSpec.describe BulkImports::PipelineBatchWorker, feature_category: :importers do
expect(described_class).to receive(:perform_in).with(60, batch.id)
expect(BulkImports::FinishBatchedPipelineWorker).not_to receive(:perform_async).with(tracker.id)
- subject.perform(batch.id)
+ worker.perform(batch.id)
expect(batch.reload).to be_created
end
end
- context 'when pipeline is not retryable' do
- it 'fails the batch and creates a failure record' do
+ context 'when pipeline raises an error' do
+ it 'keeps batch status as `started` and lets the error bubble up' do
allow_next_instance_of(pipeline_class) do |instance|
allow(instance).to receive(:run).and_raise(StandardError, 'Something went wrong')
end
- expect(Gitlab::ErrorTracking).to receive(:track_exception).with(
- instance_of(StandardError),
- hash_including(
- batch_id: batch.id,
- tracker_id: tracker.id,
- pipeline_class: 'FakePipeline',
- pipeline_step: 'pipeline_batch_worker_run'
- )
- )
-
- expect(BulkImports::Failure).to receive(:create).with(
- bulk_import_entity_id: entity.id,
- pipeline_class: 'FakePipeline',
- pipeline_step: 'pipeline_batch_worker_run',
- exception_class: 'StandardError',
- exception_message: 'Something went wrong',
- correlation_id_value: anything
- )
-
- expect(BulkImports::FinishBatchedPipelineWorker).to receive(:perform_async).with(tracker.id)
-
- subject.perform(batch.id)
-
- expect(batch.reload).to be_failed
+ expect { worker.perform(batch.id) }.to raise_exception(StandardError)
+
+ expect(batch.reload).to be_started
end
end
end
end
+
+ describe '.sidekiq_retries_exhausted' do
+ it 'sets batch status to failed' do
+ job = { 'args' => [batch.id] }
+
+ expect(Gitlab::ErrorTracking).to receive(:track_exception).with(
+ instance_of(StandardError),
+ hash_including(
+ 'message' => 'Batch tracker failed',
+ 'batch_id' => batch.id,
+ 'tracker_id' => tracker.id,
+ 'pipeline_class' => 'FakePipeline',
+ 'pipeline_step' => 'pipeline_batch_worker_run',
+ 'importer' => 'gitlab_migration'
+ )
+ )
+
+ expect(BulkImports::Failure).to receive(:create).with(
+ bulk_import_entity_id: entity.id,
+ pipeline_class: 'FakePipeline',
+ pipeline_step: 'pipeline_batch_worker_run',
+ exception_class: 'StandardError',
+ exception_message: 'Something went wrong',
+ correlation_id_value: anything
+ )
+
+ expect(BulkImports::FinishBatchedPipelineWorker).to receive(:perform_async).with(tracker.id)
+
+ described_class.sidekiq_retries_exhausted_block.call(job, StandardError.new("Something went wrong"))
+
+ expect(batch.reload).to be_failed
+ end
+ end
+
+ context 'with stop signal from database health check' do
+ around do |example|
+ with_sidekiq_server_middleware do |chain|
+ chain.add Gitlab::SidekiqMiddleware::SkipJobs
+ Sidekiq::Testing.inline! { example.run }
+ end
+ end
+
+ before do
+ stub_feature_flags("drop_sidekiq_jobs_#{described_class.name}": false)
+
+ stop_signal = instance_double("Gitlab::Database::HealthStatus::Signals::Stop", stop?: true)
+ allow(Gitlab::Database::HealthStatus).to receive(:evaluate).and_return([stop_signal])
+ end
+
+ it 'defers the job by set time' do
+ expect_next_instance_of(described_class) do |worker|
+ expect(worker).not_to receive(:perform).with(batch.id)
+ end
+
+ expect(described_class).to receive(:perform_in).with(described_class::DEFER_ON_HEALTH_DELAY, batch.id)
+
+ described_class.perform_async(batch.id)
+ end
+
+ it 'lazy evaluates schema and tables', :aggregate_failures do
+ block = described_class.database_health_check_attrs[:block]
+
+ job_args = [batch.id]
+
+ schema, table = block.call([job_args])
+
+ expect(schema).to eq(:gitlab_main_cell)
+ expect(table).to eq(['labels'])
+ end
+
+ context 'when `bulk_import_deferred_workers` feature flag is disabled' do
+ it 'does not defer job execution' do
+ stub_feature_flags(bulk_import_deferred_workers: false)
+
+ expect_next_instance_of(described_class) do |worker|
+ expect(worker).to receive(:perform).with(batch.id)
+ end
+
+ expect(described_class).not_to receive(:perform_in)
+
+ described_class.perform_async(batch.id)
+ end
+ end
+ end
end
diff --git a/spec/workers/bulk_imports/pipeline_worker_spec.rb b/spec/workers/bulk_imports/pipeline_worker_spec.rb
index e1259d5666d..d99b3e9de73 100644
--- a/spec/workers/bulk_imports/pipeline_worker_spec.rb
+++ b/spec/workers/bulk_imports/pipeline_worker_spec.rb
@@ -9,6 +9,10 @@ RSpec.describe BulkImports::PipelineWorker, feature_category: :importers do
def run; end
+ def self.relation
+ 'labels'
+ end
+
def self.file_extraction_pipeline?
false
end
@@ -28,6 +32,8 @@ RSpec.describe BulkImports::PipelineWorker, feature_category: :importers do
)
end
+ let(:worker) { described_class.new }
+
before do
stub_const('FakePipeline', pipeline_class)
@@ -38,13 +44,28 @@ RSpec.describe BulkImports::PipelineWorker, feature_category: :importers do
end
end
+ include_examples 'an idempotent worker' do
+ let(:job_args) { [pipeline_tracker.id, pipeline_tracker.stage, entity.id] }
+
+ it 'runs the pipeline and sets tracker to finished' do
+ allow(worker).to receive(:jid).and_return('jid')
+
+ perform_multiple(job_args, worker: worker)
+
+ pipeline_tracker.reload
+
+ expect(pipeline_tracker.status_name).to eq(:finished)
+ expect(pipeline_tracker.jid).to eq('jid')
+ end
+ end
+
it 'runs the given pipeline successfully' do
- expect_next_instance_of(Gitlab::Import::Logger) do |logger|
+ expect_next_instance_of(BulkImports::Logger) do |logger|
expect(logger)
.to receive(:info)
.with(
hash_including(
- 'pipeline_name' => 'FakePipeline',
+ 'pipeline_class' => 'FakePipeline',
'bulk_import_id' => entity.bulk_import_id,
'bulk_import_entity_id' => entity.id,
'bulk_import_entity_type' => entity.source_type,
@@ -53,9 +74,9 @@ RSpec.describe BulkImports::PipelineWorker, feature_category: :importers do
)
end
- allow(subject).to receive(:jid).and_return('jid')
+ allow(worker).to receive(:jid).and_return('jid')
- subject.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
+ worker.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
pipeline_tracker.reload
@@ -63,74 +84,30 @@ RSpec.describe BulkImports::PipelineWorker, feature_category: :importers do
expect(pipeline_tracker.jid).to eq('jid')
end
- context 'when job version is nil' do
- before do
- allow(subject).to receive(:job_version).and_return(nil)
- end
-
- it 'runs the given pipeline successfully and enqueues entity worker' do
- expect(BulkImports::EntityWorker).to receive(:perform_async).with(entity.id)
-
- subject.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
-
- pipeline_tracker.reload
-
- expect(pipeline_tracker.status_name).to eq(:finished)
- end
-
- context 'when an error occurs' do
- it 'enqueues entity worker' do
- expect_next_instance_of(pipeline_class) do |pipeline|
- expect(pipeline)
- .to receive(:run)
- .and_raise(StandardError, 'Error!')
- end
-
- expect(BulkImports::EntityWorker).to receive(:perform_async).with(entity.id)
-
- subject.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
- end
- end
- end
-
context 'when exclusive lease cannot be obtained' do
it 'does not run the pipeline' do
- expect(subject).to receive(:try_obtain_lease).and_return(false)
- expect(subject).not_to receive(:run)
+ expect(worker).to receive(:try_obtain_lease).and_return(false)
+ expect(worker).not_to receive(:run)
- subject.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
+ worker.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
end
end
- context 'when the pipeline raises an exception' do
- it 'logs the error' do
- pipeline_tracker = create(
- :bulk_import_tracker,
- entity: entity,
- pipeline_name: 'FakePipeline',
- status_event: 'enqueue'
- )
-
- allow(subject).to receive(:jid).and_return('jid')
-
- expect_next_instance_of(pipeline_class) do |pipeline|
- expect(pipeline)
- .to receive(:run)
- .and_raise(StandardError, 'Error!')
- end
+ describe '.sidekiq_retries_exhausted' do
+ it 'logs and sets status as failed' do
+ job = { 'args' => [pipeline_tracker.id, pipeline_tracker.stage, entity.id] }
- expect_next_instance_of(Gitlab::Import::Logger) do |logger|
+ expect_next_instance_of(BulkImports::Logger) do |logger|
expect(logger)
.to receive(:error)
.with(
hash_including(
- 'pipeline_name' => 'FakePipeline',
+ 'pipeline_class' => 'FakePipeline',
'bulk_import_entity_id' => entity.id,
'bulk_import_id' => entity.bulk_import_id,
'bulk_import_entity_type' => entity.source_type,
'source_full_path' => entity.source_full_path,
'class' => 'BulkImports::PipelineWorker',
- 'exception.backtrace' => anything,
'exception.message' => 'Error!',
'message' => 'Pipeline failed',
'source_version' => entity.bulk_import.source_version_info.to_s,
@@ -148,7 +125,7 @@ RSpec.describe BulkImports::PipelineWorker, feature_category: :importers do
'bulk_import_id' => entity.bulk_import.id,
'bulk_import_entity_type' => entity.source_type,
'source_full_path' => entity.source_full_path,
- 'pipeline_name' => pipeline_tracker.pipeline_name,
+ 'pipeline_class' => pipeline_tracker.pipeline_name,
'importer' => 'gitlab_migration',
'source_version' => entity.bulk_import.source_version_info.to_s
)
@@ -167,84 +144,127 @@ RSpec.describe BulkImports::PipelineWorker, feature_category: :importers do
)
)
- subject.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
+ expect_next_instance_of(described_class) do |worker|
+ expect(worker).to receive(:perform_failure).with(pipeline_tracker.id, entity.id, StandardError)
+ .and_call_original
+ allow(worker).to receive(:jid).and_return('jid')
+ end
+
+ described_class.sidekiq_retries_exhausted_block.call(job, StandardError.new('Error!'))
pipeline_tracker.reload
expect(pipeline_tracker.status_name).to eq(:failed)
expect(pipeline_tracker.jid).to eq('jid')
end
+ end
- context 'when enqueued pipeline cannot be found' do
- shared_examples 'logs the error' do
- it 'logs the error' do
- expect_next_instance_of(Gitlab::Import::Logger) do |logger|
- status = pipeline_tracker.human_status_name
-
- expect(logger)
- .to receive(:error)
- .with(
- hash_including(
- 'bulk_import_entity_id' => entity.id,
- 'bulk_import_id' => entity.bulk_import_id,
- 'bulk_import_entity_type' => entity.source_type,
- 'pipeline_tracker_id' => pipeline_tracker.id,
- 'pipeline_tracker_state' => status,
- 'pipeline_name' => pipeline_tracker.pipeline_name,
- 'source_full_path' => entity.source_full_path,
- 'source_version' => entity.bulk_import.source_version_info.to_s,
- 'importer' => 'gitlab_migration',
- 'message' => "Pipeline in #{status} state instead of expected enqueued state"
- )
- )
- end
-
- subject.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
- end
+ context 'with stop signal from database health check' do
+ around do |example|
+ with_sidekiq_server_middleware do |chain|
+ chain.add Gitlab::SidekiqMiddleware::SkipJobs
+ Sidekiq::Testing.inline! { example.run }
end
+ end
- context 'when pipeline is finished' do
- let(:pipeline_tracker) do
- create(
- :bulk_import_tracker,
- :finished,
- entity: entity,
- pipeline_name: 'FakePipeline'
- )
- end
+ before do
+ stub_feature_flags("drop_sidekiq_jobs_#{described_class.name}": false)
- include_examples 'logs the error'
+ stop_signal = instance_double("Gitlab::Database::HealthStatus::Signals::Stop", stop?: true)
+ allow(Gitlab::Database::HealthStatus).to receive(:evaluate).and_return([stop_signal])
+ end
+
+ it 'defers the job by set time' do
+ expect_next_instance_of(described_class) do |worker|
+ expect(worker).not_to receive(:perform).with(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
end
- context 'when pipeline is skipped' do
- let(:pipeline_tracker) do
- create(
- :bulk_import_tracker,
- :skipped,
- entity: entity,
- pipeline_name: 'FakePipeline'
- )
- end
+ expect(described_class).to receive(:perform_in).with(
+ described_class::DEFER_ON_HEALTH_DELAY,
+ pipeline_tracker.id,
+ pipeline_tracker.stage,
+ entity.id
+ )
- include_examples 'logs the error'
- end
+ described_class.perform_async(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
+ end
- context 'when tracker is started' do
- it 'marks tracker as failed' do
- pipeline_tracker = create(
- :bulk_import_tracker,
- :started,
- entity: entity,
- pipeline_name: 'FakePipeline'
- )
+ it 'lazy evaluates schema and tables', :aggregate_failures do
+ block = described_class.database_health_check_attrs[:block]
+
+ job_args = [pipeline_tracker.id, pipeline_tracker.stage, entity.id]
+
+ schema, table = block.call([job_args])
+
+ expect(schema).to eq(:gitlab_main_cell)
+ expect(table).to eq(['labels'])
+ end
- subject.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
+ context 'when `bulk_import_deferred_workers` feature flag is disabled' do
+ it 'does not defer job execution' do
+ stub_feature_flags(bulk_import_deferred_workers: false)
- expect(pipeline_tracker.reload.failed?).to eq(true)
+ expect_next_instance_of(described_class) do |worker|
+ expect(worker).to receive(:perform).with(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
end
+
+ expect(described_class).not_to receive(:perform_in)
+
+ described_class.perform_async(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
end
end
+ end
+
+ context 'when pipeline is finished' do
+ let(:pipeline_tracker) do
+ create(
+ :bulk_import_tracker,
+ :finished,
+ entity: entity,
+ pipeline_name: 'FakePipeline'
+ )
+ end
+ it 'no-ops and returns' do
+ expect(described_class).not_to receive(:run)
+
+ worker.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
+ end
+ end
+
+ context 'when pipeline is skipped' do
+ let(:pipeline_tracker) do
+ create(
+ :bulk_import_tracker,
+ :skipped,
+ entity: entity,
+ pipeline_name: 'FakePipeline'
+ )
+ end
+
+ it 'no-ops and returns' do
+ expect(described_class).not_to receive(:run)
+
+ worker.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
+ end
+ end
+
+ context 'when tracker is started' do
+ it 'runs the pipeline' do
+ pipeline_tracker = create(
+ :bulk_import_tracker,
+ :started,
+ entity: entity,
+ pipeline_name: 'FakePipeline'
+ )
+
+ worker.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
+
+ expect(pipeline_tracker.reload.finished?).to eq(true)
+ end
+ end
+
+ describe '#perform' do
context 'when entity is failed' do
it 'marks tracker as skipped and logs the skip' do
pipeline_tracker = create(
@@ -256,14 +276,14 @@ RSpec.describe BulkImports::PipelineWorker, feature_category: :importers do
entity.update!(status: -1)
- expect_next_instance_of(Gitlab::Import::Logger) do |logger|
+ expect_next_instance_of(BulkImports::Logger) do |logger|
allow(logger).to receive(:info)
expect(logger)
.to receive(:info)
.with(
hash_including(
- 'pipeline_name' => 'FakePipeline',
+ 'pipeline_class' => 'FakePipeline',
'bulk_import_entity_id' => entity.id,
'bulk_import_id' => entity.bulk_import_id,
'bulk_import_entity_type' => entity.source_type,
@@ -273,7 +293,7 @@ RSpec.describe BulkImports::PipelineWorker, feature_category: :importers do
)
end
- subject.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
+ worker.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
expect(pipeline_tracker.reload.status_name).to eq(:skipped)
end
@@ -294,7 +314,7 @@ RSpec.describe BulkImports::PipelineWorker, feature_category: :importers do
end
before do
- allow(subject).to receive(:jid).and_return('jid')
+ allow(worker).to receive(:jid).and_return('jid')
expect_next_instance_of(pipeline_class) do |pipeline|
expect(pipeline)
@@ -308,12 +328,12 @@ RSpec.describe BulkImports::PipelineWorker, feature_category: :importers do
expect(tracker).to receive(:retry).and_call_original
end
- expect_next_instance_of(Gitlab::Import::Logger) do |logger|
+ expect_next_instance_of(BulkImports::Logger) do |logger|
expect(logger)
.to receive(:info)
.with(
hash_including(
- 'pipeline_name' => 'FakePipeline',
+ 'pipeline_class' => 'FakePipeline',
'bulk_import_entity_id' => entity.id,
'bulk_import_id' => entity.bulk_import_id,
'bulk_import_entity_type' => entity.source_type,
@@ -331,7 +351,7 @@ RSpec.describe BulkImports::PipelineWorker, feature_category: :importers do
pipeline_tracker.entity.id
)
- subject.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
+ worker.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
pipeline_tracker.reload
@@ -384,7 +404,7 @@ RSpec.describe BulkImports::PipelineWorker, feature_category: :importers do
allow(status).to receive(:batched?).and_return(false)
end
- subject.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
+ worker.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
expect(pipeline_tracker.reload.status_name).to eq(:finished)
end
@@ -407,7 +427,7 @@ RSpec.describe BulkImports::PipelineWorker, feature_category: :importers do
entity.id
)
- subject.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
+ worker.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
end
end
@@ -436,7 +456,7 @@ RSpec.describe BulkImports::PipelineWorker, feature_category: :importers do
entity.id
)
- subject.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
+ worker.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
expect(pipeline_tracker.reload.status_name).to eq(:enqueued)
end
@@ -445,31 +465,9 @@ RSpec.describe BulkImports::PipelineWorker, feature_category: :importers do
context 'when empty export timeout is reached' do
let(:created_at) { 10.minutes.ago }
- it 'marks as failed and logs the error' do
- expect_next_instance_of(Gitlab::Import::Logger) do |logger|
- expect(logger)
- .to receive(:error)
- .with(
- hash_including(
- 'pipeline_name' => 'NdjsonPipeline',
- 'bulk_import_entity_id' => entity.id,
- 'bulk_import_id' => entity.bulk_import_id,
- 'bulk_import_entity_type' => entity.source_type,
- 'source_full_path' => entity.source_full_path,
- 'class' => 'BulkImports::PipelineWorker',
- 'exception.backtrace' => anything,
- 'exception.class' => 'BulkImports::Pipeline::ExpiredError',
- 'exception.message' => 'Empty export status on source instance',
- 'importer' => 'gitlab_migration',
- 'message' => 'Pipeline failed',
- 'source_version' => entity.bulk_import.source_version_info.to_s
- )
- )
- end
-
- subject.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
-
- expect(pipeline_tracker.reload.status_name).to eq(:failed)
+ it 'raises sidekiq error' do
+ expect { worker.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id) }
+ .to raise_exception(BulkImports::Pipeline::ExpiredError)
end
end
@@ -479,17 +477,8 @@ RSpec.describe BulkImports::PipelineWorker, feature_category: :importers do
it 'falls back to entity created_at' do
entity.update!(created_at: 10.minutes.ago)
- expect_next_instance_of(Gitlab::Import::Logger) do |logger|
- expect(logger)
- .to receive(:error)
- .with(
- hash_including('exception.message' => 'Empty export status on source instance')
- )
- end
-
- subject.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
-
- expect(pipeline_tracker.reload.status_name).to eq(:failed)
+ expect { worker.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id) }
+ .to raise_exception(BulkImports::Pipeline::ExpiredError)
end
end
end
@@ -501,28 +490,8 @@ RSpec.describe BulkImports::PipelineWorker, feature_category: :importers do
allow(status).to receive(:error).and_return('Error!')
end
- expect_next_instance_of(Gitlab::Import::Logger) do |logger|
- expect(logger)
- .to receive(:error)
- .with(
- hash_including(
- 'pipeline_name' => 'NdjsonPipeline',
- 'bulk_import_entity_id' => entity.id,
- 'bulk_import_id' => entity.bulk_import_id,
- 'bulk_import_entity_type' => entity.source_type,
- 'source_full_path' => entity.source_full_path,
- 'exception.backtrace' => anything,
- 'exception.class' => 'BulkImports::Pipeline::FailedError',
- 'exception.message' => 'Export from source instance failed: Error!',
- 'importer' => 'gitlab_migration',
- 'source_version' => entity.bulk_import.source_version_info.to_s
- )
- )
- end
-
- subject.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
-
- expect(pipeline_tracker.reload.status_name).to eq(:failed)
+ expect { worker.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id) }
+ .to raise_exception(BulkImports::Pipeline::FailedError)
end
end
@@ -542,7 +511,7 @@ RSpec.describe BulkImports::PipelineWorker, feature_category: :importers do
it 'enqueues pipeline batches' do
expect(BulkImports::PipelineBatchWorker).to receive(:perform_async).twice
- subject.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
+ worker.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
pipeline_tracker.reload
@@ -555,9 +524,9 @@ RSpec.describe BulkImports::PipelineWorker, feature_category: :importers do
let(:batches_count) { 0 }
it 'marks tracker as finished' do
- expect(subject).not_to receive(:enqueue_batches)
+ expect(worker).not_to receive(:enqueue_batches)
- subject.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
+ worker.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
expect(pipeline_tracker.reload.status_name).to eq(:finished)
end
diff --git a/spec/workers/bulk_imports/relation_batch_export_worker_spec.rb b/spec/workers/bulk_imports/relation_batch_export_worker_spec.rb
index 4a2c8d48742..8ee55d64a1b 100644
--- a/spec/workers/bulk_imports/relation_batch_export_worker_spec.rb
+++ b/spec/workers/bulk_imports/relation_batch_export_worker_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe BulkImports::RelationBatchExportWorker, feature_category: :import
expect(BulkImports::RelationBatchExportService)
.to receive(:new)
- .with(user.id, batch.id)
+ .with(user, batch)
.twice.and_return(service)
expect(service).to receive(:execute).twice
@@ -23,4 +23,21 @@ RSpec.describe BulkImports::RelationBatchExportWorker, feature_category: :import
end
end
end
+
+ describe '.sidekiq_retries_exhausted' do
+ let(:job) { { 'args' => job_args } }
+
+ it 'sets export status to failed and tracks the exception' do
+ portable = batch.export.portable
+
+ expect(Gitlab::ErrorTracking)
+ .to receive(:track_exception)
+ .with(kind_of(StandardError), portable_id: portable.id, portable_type: portable.class.name)
+
+ described_class.sidekiq_retries_exhausted_block.call(job, StandardError.new('*' * 300))
+
+ expect(batch.reload.failed?).to eq(true)
+ expect(batch.error.size).to eq(255)
+ end
+ end
end
diff --git a/spec/workers/bulk_imports/relation_export_worker_spec.rb b/spec/workers/bulk_imports/relation_export_worker_spec.rb
index 646af6c2a9c..55e2a238027 100644
--- a/spec/workers/bulk_imports/relation_export_worker_spec.rb
+++ b/spec/workers/bulk_imports/relation_export_worker_spec.rb
@@ -63,4 +63,20 @@ RSpec.describe BulkImports::RelationExportWorker, feature_category: :importers d
end
end
end
+
+ describe '.sidekiq_retries_exhausted' do
+ let(:job) { { 'args' => job_args } }
+ let!(:export) { create(:bulk_import_export, group: group, relation: relation) }
+
+ it 'sets export status to failed and tracks the exception' do
+ expect(Gitlab::ErrorTracking)
+ .to receive(:track_exception)
+ .with(kind_of(StandardError), portable_id: group.id, portable_type: group.class.name)
+
+ described_class.sidekiq_retries_exhausted_block.call(job, StandardError.new('*' * 300))
+
+ expect(export.reload.failed?).to eq(true)
+ expect(export.error.size).to eq(255)
+ end
+ end
end
diff --git a/spec/workers/bulk_imports/stuck_import_worker_spec.rb b/spec/workers/bulk_imports/stuck_import_worker_spec.rb
index ba1b1b66b00..eadf3864190 100644
--- a/spec/workers/bulk_imports/stuck_import_worker_spec.rb
+++ b/spec/workers/bulk_imports/stuck_import_worker_spec.rb
@@ -9,17 +9,46 @@ RSpec.describe BulkImports::StuckImportWorker, feature_category: :importers do
let_it_be(:stale_started_bulk_import) { create(:bulk_import, :started, created_at: 3.days.ago) }
let_it_be(:stale_created_bulk_import_entity) { create(:bulk_import_entity, :created, created_at: 3.days.ago) }
let_it_be(:stale_started_bulk_import_entity) { create(:bulk_import_entity, :started, created_at: 3.days.ago) }
- let_it_be(:started_bulk_import_tracker) { create(:bulk_import_tracker, :started, entity: stale_started_bulk_import_entity) }
+
+ let_it_be(:started_bulk_import_tracker) do
+ create(:bulk_import_tracker, :started, entity: stale_started_bulk_import_entity)
+ end
subject { described_class.new.perform }
describe 'perform' do
it 'updates the status of bulk imports to timeout' do
+ expect_next_instance_of(BulkImports::Logger) do |logger|
+ allow(logger).to receive(:error)
+ expect(logger).to receive(:error).with(
+ message: 'BulkImport stale',
+ bulk_import_id: stale_created_bulk_import.id
+ )
+ expect(logger).to receive(:error).with(
+ message: 'BulkImport stale',
+ bulk_import_id: stale_started_bulk_import.id
+ )
+ end
+
expect { subject }.to change { stale_created_bulk_import.reload.status_name }.from(:created).to(:timeout)
.and change { stale_started_bulk_import.reload.status_name }.from(:started).to(:timeout)
end
it 'updates the status of bulk import entities to timeout' do
+ expect_next_instance_of(BulkImports::Logger) do |logger|
+ allow(logger).to receive(:error)
+ expect(logger).to receive(:error).with(
+ message: 'BulkImports::Entity stale',
+ bulk_import_entity_id: stale_created_bulk_import_entity.id,
+ bulk_import_id: stale_created_bulk_import_entity.bulk_import_id
+ )
+ expect(logger).to receive(:error).with(
+ message: 'BulkImports::Entity stale',
+ bulk_import_entity_id: stale_started_bulk_import_entity.id,
+ bulk_import_id: stale_started_bulk_import_entity.bulk_import_id
+ )
+ end
+
expect { subject }.to change { stale_created_bulk_import_entity.reload.status_name }.from(:created).to(:timeout)
.and change { stale_started_bulk_import_entity.reload.status_name }.from(:started).to(:timeout)
end
diff --git a/spec/workers/ci/cancel_pipeline_worker_spec.rb b/spec/workers/ci/cancel_pipeline_worker_spec.rb
index 13a9c0affe7..8e8f9a78132 100644
--- a/spec/workers/ci/cancel_pipeline_worker_spec.rb
+++ b/spec/workers/ci/cancel_pipeline_worker_spec.rb
@@ -11,14 +11,13 @@ RSpec.describe Ci::CancelPipelineWorker, :aggregate_failures, feature_category:
let(:cancel_service) { instance_double(::Ci::CancelPipelineService) }
it 'cancels the pipeline' do
- allow(::Ci::Pipeline).to receive(:find_by_id).and_return(pipeline)
+ allow(::Ci::Pipeline).to receive(:find_by_id).twice.and_return(pipeline)
expect(::Ci::CancelPipelineService)
.to receive(:new)
.with(
pipeline: pipeline,
current_user: nil,
- auto_canceled_by_pipeline_id:
- pipeline.id,
+ auto_canceled_by_pipeline: pipeline,
cascade_to_children: false)
.and_return(cancel_service)
@@ -28,7 +27,7 @@ RSpec.describe Ci::CancelPipelineWorker, :aggregate_failures, feature_category:
end
context 'if pipeline is deleted' do
- subject(:perform) { described_class.new.perform(non_existing_record_id, non_existing_record_id) }
+ subject(:perform) { described_class.new.perform(non_existing_record_id, pipeline.id) }
it 'does not error' do
expect(::Ci::CancelPipelineService).not_to receive(:new)
@@ -37,6 +36,23 @@ RSpec.describe Ci::CancelPipelineWorker, :aggregate_failures, feature_category:
end
end
+ context 'when auto_canceled_by_pipeline is deleted' do
+ subject(:perform) { described_class.new.perform(pipeline.id, non_existing_record_id) }
+
+ it 'does not error' do
+ expect(::Ci::CancelPipelineService)
+ .to receive(:new)
+ .with(
+ pipeline: an_instance_of(::Ci::Pipeline),
+ current_user: nil,
+ auto_canceled_by_pipeline: nil,
+ cascade_to_children: false)
+ .and_call_original
+
+ perform
+ end
+ end
+
describe 'with builds and state transition side effects', :sidekiq_inline do
let!(:build) { create(:ci_build, :running, pipeline: pipeline) }
diff --git a/spec/workers/ci/initial_pipeline_process_worker_spec.rb b/spec/workers/ci/initial_pipeline_process_worker_spec.rb
index 9a94f1cbb4c..fcdd0a2a33b 100644
--- a/spec/workers/ci/initial_pipeline_process_worker_spec.rb
+++ b/spec/workers/ci/initial_pipeline_process_worker_spec.rb
@@ -70,32 +70,6 @@ RSpec.describe Ci::InitialPipelineProcessWorker, feature_category: :continuous_i
subject
end
-
- context 'when `create_deployment_only_for_processable_jobs` FF is disabled' do
- before do
- stub_feature_flags(create_deployment_only_for_processable_jobs: false)
- end
-
- it 'creates a deployment record' do
- expect { subject }.to change { Deployment.count }.by(1)
-
- expect(job.deployment).to have_attributes(
- project: job.project,
- ref: job.ref,
- sha: job.sha,
- deployable: job,
- deployable_type: 'CommitStatus',
- environment: job.persisted_environment
- )
- end
-
- it 'a deployment is created before atomic processing is kicked off' do
- expect(::Deployments::CreateForJobService).to receive(:new).ordered
- expect(::Ci::PipelineProcessing::AtomicProcessingService).to receive(:new).ordered
-
- subject
- end
- end
end
end
end
diff --git a/spec/workers/ci/pipeline_success_unlock_artifacts_worker_spec.rb b/spec/workers/ci/pipeline_success_unlock_artifacts_worker_spec.rb
index 60a34fdab53..d5f3c2b92fb 100644
--- a/spec/workers/ci/pipeline_success_unlock_artifacts_worker_spec.rb
+++ b/spec/workers/ci/pipeline_success_unlock_artifacts_worker_spec.rb
@@ -75,7 +75,8 @@ RSpec.describe Ci::PipelineSuccessUnlockArtifactsWorker, feature_category: :buil
expect(described_class.database_health_check_attrs).to eq(
gitlab_schema: :gitlab_ci,
delay_by: described_class::DEFAULT_DEFER_DELAY,
- tables: [:ci_job_artifacts]
+ tables: [:ci_job_artifacts],
+ block: nil
)
end
end
diff --git a/spec/workers/ci/refs/unlock_previous_pipelines_worker_spec.rb b/spec/workers/ci/refs/unlock_previous_pipelines_worker_spec.rb
index 2f00ea45edc..558ac5b9e0b 100644
--- a/spec/workers/ci/refs/unlock_previous_pipelines_worker_spec.rb
+++ b/spec/workers/ci/refs/unlock_previous_pipelines_worker_spec.rb
@@ -17,6 +17,7 @@ RSpec.describe Ci::Refs::UnlockPreviousPipelinesWorker, :unlock_pipelines, :clea
create(
:ci_pipeline,
:with_persisted_artifacts,
+ :artifacts_locked,
ref: older_pipeline.ref,
tag: older_pipeline.tag,
project: older_pipeline.project
@@ -25,12 +26,30 @@ RSpec.describe Ci::Refs::UnlockPreviousPipelinesWorker, :unlock_pipelines, :clea
describe '#perform' do
it 'executes a service' do
+ ci_ref = pipeline.ci_ref
+ expect(ci_ref).to receive(:last_unlockable_ci_source_pipeline).and_return(pipeline)
+
+ expect(Ci::Ref).to receive(:find_by_id).with(pipeline.ci_ref.id).and_return(ci_ref)
+
expect_next_instance_of(Ci::Refs::EnqueuePipelinesToUnlockService) do |instance|
- expect(instance).to receive(:execute).and_call_original
+ expect(instance).to receive(:execute).with(ci_ref, before_pipeline: pipeline).and_call_original
end
worker.perform(pipeline.ci_ref.id)
end
+
+ context 'when ref has no pipelines locked' do
+ before do
+ older_pipeline.update!(locked: :unlocked)
+ pipeline.update!(locked: :unlocked)
+ end
+
+ it 'does nothing' do
+ expect(Ci::Refs::EnqueuePipelinesToUnlockService).not_to receive(:new)
+
+ worker.perform(pipeline.ci_ref.id)
+ end
+ end
end
it_behaves_like 'an idempotent worker' do
diff --git a/spec/workers/concerns/gitlab/github_import/stage_methods_spec.rb b/spec/workers/concerns/gitlab/github_import/stage_methods_spec.rb
index c8f7427d5ae..fa782967441 100644
--- a/spec/workers/concerns/gitlab/github_import/stage_methods_spec.rb
+++ b/spec/workers/concerns/gitlab/github_import/stage_methods_spec.rb
@@ -145,12 +145,16 @@ RSpec.describe Gitlab::GithubImport::StageMethods, feature_category: :importers
.to receive(:import)
.with(client, project)
+ expect(project.import_state).to receive(:refresh_jid_expiration)
+
worker.try_import(client, project)
end
it 'reschedules the worker if RateLimitError was raised' do
client = double(:client, rate_limit_resets_in: 10)
+ expect(project.import_state).to receive(:refresh_jid_expiration)
+
expect(worker)
.to receive(:import)
.with(client, project)
@@ -181,4 +185,30 @@ RSpec.describe Gitlab::GithubImport::StageMethods, feature_category: :importers
expect(worker.find_project(-1)).to be_nil
end
end
+
+ describe '.resumes_work_when_interrupted!' do
+ subject(:sidekiq_options) { worker.class.sidekiq_options }
+
+ it 'does not set the `max_retries_after_interruption` if not called' do
+ is_expected.not_to have_key('max_retries_after_interruption')
+ end
+
+ it 'sets the `max_retries_after_interruption`' do
+ worker.class.resumes_work_when_interrupted!
+
+ is_expected.to include('max_retries_after_interruption' => 20)
+ end
+
+ context 'when the flag is disabled' do
+ before do
+ stub_feature_flags(github_importer_raise_max_interruptions: false)
+ end
+
+ it 'does not set `max_retries_after_interruption`' do
+ worker.class.resumes_work_when_interrupted!
+
+ is_expected.not_to have_key('max_retries_after_interruption')
+ end
+ end
+ end
end
diff --git a/spec/workers/concerns/worker_attributes_spec.rb b/spec/workers/concerns/worker_attributes_spec.rb
index 90c07a9c959..767a55162fb 100644
--- a/spec/workers/concerns/worker_attributes_spec.rb
+++ b/spec/workers/concerns/worker_attributes_spec.rb
@@ -37,7 +37,7 @@ RSpec.describe WorkerAttributes, feature_category: :shared do
:worker_has_external_dependencies? | :worker_has_external_dependencies! | false | [] | true
:idempotent? | :idempotent! | false | [] | true
:big_payload? | :big_payload! | false | [] | true
- :database_health_check_attrs | :defer_on_database_health_signal | nil | [:gitlab_main, [:users], 1.minute] | { gitlab_schema: :gitlab_main, tables: [:users], delay_by: 1.minute }
+ :database_health_check_attrs | :defer_on_database_health_signal | nil | [:gitlab_main, [:users], 1.minute] | { gitlab_schema: :gitlab_main, tables: [:users], delay_by: 1.minute, block: nil }
end
# rubocop: enable Layout/LineLength
diff --git a/spec/workers/concerns/worker_context_spec.rb b/spec/workers/concerns/worker_context_spec.rb
index 700d9e37a55..7a046517fd1 100644
--- a/spec/workers/concerns/worker_context_spec.rb
+++ b/spec/workers/concerns/worker_context_spec.rb
@@ -74,7 +74,7 @@ RSpec.describe WorkerContext, feature_category: :shared do
describe '.bulk_perform_async_with_contexts' do
subject do
worker.bulk_perform_async_with_contexts(
- %w(hello world),
+ %w[hello world],
context_proc: -> (_) { { user: build_stubbed(:user) } },
arguments_proc: -> (word) { word }
)
@@ -93,7 +93,7 @@ RSpec.describe WorkerContext, feature_category: :shared do
subject do
worker.bulk_perform_in_with_contexts(
10.minutes,
- %w(hello world),
+ %w[hello world],
context_proc: -> (_) { { user: build_stubbed(:user) } },
arguments_proc: -> (word) { word }
)
diff --git a/spec/workers/container_registry/migration/enqueuer_worker_spec.rb b/spec/workers/container_registry/migration/enqueuer_worker_spec.rb
index 4a603e538ef..ff388b1a29d 100644
--- a/spec/workers/container_registry/migration/enqueuer_worker_spec.rb
+++ b/spec/workers/container_registry/migration/enqueuer_worker_spec.rb
@@ -16,7 +16,7 @@ RSpec.describe ContainerRegistry::Migration::EnqueuerWorker, :aggregate_failures
before do
stub_container_registry_config(enabled: true)
stub_application_setting(container_registry_import_created_before: 1.day.ago)
- stub_container_registry_tags(repository: container_repository.path, tags: %w(tag1 tag2 tag3), with_manifest: true)
+ stub_container_registry_tags(repository: container_repository.path, tags: %w[tag1 tag2 tag3], with_manifest: true)
end
describe '#perform' do
@@ -133,7 +133,7 @@ RSpec.describe ContainerRegistry::Migration::EnqueuerWorker, :aggregate_failures
stub_container_registry_tags(
repository: container_repository2.path,
- tags: %w(tag4 tag5 tag6),
+ tags: %w[tag4 tag5 tag6],
with_manifest: true
)
end
@@ -204,7 +204,7 @@ RSpec.describe ContainerRegistry::Migration::EnqueuerWorker, :aggregate_failures
stub_application_setting(container_registry_import_max_tags_count: 0)
# Add 8 tags to the next repository
stub_container_registry_tags(
- repository: container_repository.path, tags: %w(a b c d e f g h), with_manifest: true
+ repository: container_repository.path, tags: %w[a b c d e f g h], with_manifest: true
)
end
diff --git a/spec/workers/environments/auto_recover_worker_spec.rb b/spec/workers/environments/auto_recover_worker_spec.rb
new file mode 100644
index 00000000000..c0acfc9945e
--- /dev/null
+++ b/spec/workers/environments/auto_recover_worker_spec.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Environments::AutoRecoverWorker, feature_category: :continuous_delivery do
+ include CreateEnvironmentsHelpers
+
+ subject { worker.perform(environment_id) }
+
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } }
+ let_it_be(:reporter) { create(:user).tap { |u| project.add_reporter(u) } }
+
+ let!(:environment) { create_review_app(user, project, 'review/feature').environment }
+ let(:environment_id) { environment.id }
+ let(:worker) { described_class.new }
+ let(:user) { developer }
+
+ before_all do
+ project.repository.add_branch(developer, 'review/feature', 'master')
+ end
+
+ it 'has the `until_executed` deduplicate strategy' do
+ expect(described_class.get_deduplicate_strategy).to eq(:until_executed)
+ end
+
+ context 'when environment has been updated recently' do
+ it 'recovers the environment' do
+ environment.stop!
+ environment.update!(updated_at: (Environment::LONG_STOP - 1.day).ago)
+
+ expect { subject }
+ .not_to change { environment.reload.state }
+ .from('stopping')
+ end
+ end
+
+ context 'when all stop actions are not complete' do
+ it 'does not recover the environment' do
+ environment.stop!
+ environment.stop_actions.map(&:drop)
+ environment.update!(updated_at: (Environment::LONG_STOP + 1.day).ago)
+
+ expect { subject }
+ .to change { environment.reload.state }
+ .from('stopping').to('available')
+ end
+ end
+
+ context 'when all stop actions are complete' do
+ it 'recovers the environment' do
+ environment.stop!
+ environment.update!(updated_at: (Environment::LONG_STOP + 1.day).ago)
+
+ expect { subject }
+ .not_to change { environment.reload.state }
+ .from('stopping')
+ end
+ end
+
+ context 'when there are no corresponding environment record' do
+ let!(:environment) { instance_double('Environment', id: non_existing_record_id) }
+
+ it 'ignores the invalid record' do
+ expect { subject }.not_to raise_error
+ end
+ end
+end
diff --git a/spec/workers/environments/auto_stop_cron_worker_spec.rb b/spec/workers/environments/auto_stop_cron_worker_spec.rb
index ad44cf97e07..14a74022a1f 100644
--- a/spec/workers/environments/auto_stop_cron_worker_spec.rb
+++ b/spec/workers/environments/auto_stop_cron_worker_spec.rb
@@ -14,4 +14,12 @@ RSpec.describe Environments::AutoStopCronWorker, feature_category: :continuous_d
subject
end
+
+ it 'executes Environments::AutoRecoverService' do
+ expect_next_instance_of(Environments::AutoRecoverService) do |service|
+ expect(service).to receive(:execute)
+ end
+
+ subject
+ end
end
diff --git a/spec/workers/every_sidekiq_worker_spec.rb b/spec/workers/every_sidekiq_worker_spec.rb
index 4855967d462..4c2cff434a7 100644
--- a/spec/workers/every_sidekiq_worker_spec.rb
+++ b/spec/workers/every_sidekiq_worker_spec.rb
@@ -137,11 +137,11 @@ RSpec.describe 'Every Sidekiq worker', feature_category: :shared do
'BuildHooksWorker' => 3,
'BuildQueueWorker' => 3,
'BuildSuccessWorker' => 3,
- 'BulkImportWorker' => false,
+ 'BulkImportWorker' => 3,
'BulkImports::ExportRequestWorker' => 5,
- 'BulkImports::EntityWorker' => false,
- 'BulkImports::PipelineWorker' => false,
- 'BulkImports::PipelineBatchWorker' => false,
+ 'BulkImports::EntityWorker' => 3,
+ 'BulkImports::PipelineWorker' => 3,
+ 'BulkImports::PipelineBatchWorker' => 3,
'BulkImports::FinishProjectImportWorker' => 5,
'Chaos::CpuSpinWorker' => 3,
'Chaos::DbSpinWorker' => 3,
@@ -325,10 +325,6 @@ RSpec.describe 'Every Sidekiq worker', feature_category: :shared do
'Groups::ScheduleBulkRepositoryShardMovesWorker' => 3,
'Groups::UpdateRepositoryStorageWorker' => 3,
'Groups::UpdateStatisticsWorker' => 3,
- 'HashedStorage::MigratorWorker' => 3,
- 'HashedStorage::ProjectMigrateWorker' => 3,
- 'HashedStorage::ProjectRollbackWorker' => 3,
- 'HashedStorage::RollbackerWorker' => 3,
'ImportIssuesCsvWorker' => 3,
'ImportSoftwareLicensesWorker' => 3,
'IncidentManagement::AddSeveritySystemNoteWorker' => 3,
@@ -355,8 +351,6 @@ RSpec.describe 'Every Sidekiq worker', feature_category: :shared do
'Llm::Embedding::GitlabDocumentation::SetEmbeddingsOnTheRecordWorker' => 5,
'Llm::Embedding::GitlabDocumentation::CreateEmptyEmbeddingsRecordsWorker' => 3,
'Llm::Embedding::GitlabDocumentation::CreateDbEmbeddingsPerDocFileWorker' => 5,
- 'Llm::TanukiBot::UpdateWorker' => 1,
- 'Llm::TanukiBot::RecreateRecordsWorker' => 3,
'MailScheduler::IssueDueWorker' => 3,
'MailScheduler::NotificationServiceWorker' => 3,
'MembersDestroyer::UnassignIssuablesWorker' => 3,
@@ -396,6 +390,7 @@ RSpec.describe 'Every Sidekiq worker', feature_category: :shared do
'Packages::Go::SyncPackagesWorker' => 3,
'Packages::MarkPackageFilesForDestructionWorker' => 3,
'Packages::Maven::Metadata::SyncWorker' => 3,
+ 'Packages::Npm::CleanupStaleMetadataCacheWorker' => 0,
'Packages::Nuget::ExtractionWorker' => 3,
'Packages::Rubygems::ExtractionWorker' => 3,
'PagesDomainSslRenewalWorker' => 3,
@@ -486,7 +481,9 @@ RSpec.describe 'Every Sidekiq worker', feature_category: :shared do
'X509CertificateRevokeWorker' => 3,
'ComplianceManagement::MergeRequests::ComplianceViolationsWorker' => 3,
'Zoekt::IndexerWorker' => 2,
- 'Issuable::RelatedLinksCreateWorker' => 3
+ 'Issuable::RelatedLinksCreateWorker' => 3,
+ 'BulkImports::RelationBatchExportWorker' => 3,
+ 'BulkImports::RelationExportWorker' => 3
}.merge(extra_retry_exceptions)
end
diff --git a/spec/workers/gitlab/bitbucket_import/advance_stage_worker_spec.rb b/spec/workers/gitlab/bitbucket_import/advance_stage_worker_spec.rb
index c04ccafdcf8..673988a3275 100644
--- a/spec/workers/gitlab/bitbucket_import/advance_stage_worker_spec.rb
+++ b/spec/workers/gitlab/bitbucket_import/advance_stage_worker_spec.rb
@@ -32,7 +32,7 @@ RSpec.describe Gitlab::BitbucketImport::AdvanceStageWorker, :clean_gitlab_redis_
expect(described_class)
.to receive(:perform_in)
- .with(described_class::INTERVAL, project.id, { '123' => 1 }, :finish, Time.zone.now, 1)
+ .with(described_class::INTERVAL, project.id, { '123' => 1 }, 'finish', Time.zone.now.to_s, 1)
worker.perform(project.id, { '123' => 2 }, :finish)
end
diff --git a/spec/workers/gitlab/github_import/stage/import_attachments_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_attachments_worker_spec.rb
index 9a4b9106dae..c8b528593b9 100644
--- a/spec/workers/gitlab/github_import/stage/import_attachments_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/stage/import_attachments_worker_spec.rb
@@ -51,7 +51,7 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportAttachmentsWorker, feature_cat
expect(Gitlab::GithubImport::AdvanceStageWorker)
.to receive(:perform_async)
- .with(project.id, { '123' => 2, '234' => 3, '345' => 4, '456' => 5 }, :protected_branches)
+ .with(project.id, { '123' => 2, '234' => 3, '345' => 4, '456' => 5 }, 'protected_branches')
worker.import(client, project)
end
@@ -62,7 +62,7 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportAttachmentsWorker, feature_cat
it 'skips release attachments import and calls next stage' do
importers.each { |importer| expect(importer[:klass]).not_to receive(:new) }
expect(Gitlab::GithubImport::AdvanceStageWorker)
- .to receive(:perform_async).with(project.id, {}, :protected_branches)
+ .to receive(:perform_async).with(project.id, {}, 'protected_branches')
worker.import(client, project)
end
diff --git a/spec/workers/gitlab/github_import/stage/import_base_data_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_base_data_worker_spec.rb
index f3b706361e3..b8f2db8e2d9 100644
--- a/spec/workers/gitlab/github_import/stage/import_base_data_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/stage/import_base_data_worker_spec.rb
@@ -23,8 +23,6 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportBaseDataWorker, feature_catego
expect(importer).to receive(:execute)
end
- expect(import_state).to receive(:refresh_jid_expiration)
-
expect(Gitlab::GithubImport::Stage::ImportPullRequestsWorker)
.to receive(:perform_async)
.with(project.id)
diff --git a/spec/workers/gitlab/github_import/stage/import_collaborators_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_collaborators_worker_spec.rb
index fc38adb5447..6a55f575da8 100644
--- a/spec/workers/gitlab/github_import/stage/import_collaborators_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/stage/import_collaborators_worker_spec.rb
@@ -33,11 +33,9 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportCollaboratorsWorker, feature_c
.and_return(importer)
expect(importer).to receive(:execute).and_return(waiter)
- expect(import_state).to receive(:refresh_jid_expiration)
-
expect(Gitlab::GithubImport::AdvanceStageWorker)
.to receive(:perform_async)
- .with(project.id, { '123' => 2 }, :pull_requests_merged_by)
+ .with(project.id, { '123' => 2 }, 'pull_requests_merged_by')
worker.import(client, project)
end
@@ -51,7 +49,7 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportCollaboratorsWorker, feature_c
expect(Gitlab::GithubImport::AdvanceStageWorker)
.to receive(:perform_async)
- .with(project.id, {}, :pull_requests_merged_by)
+ .with(project.id, {}, 'pull_requests_merged_by')
worker.import(client, project)
end
@@ -65,7 +63,7 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportCollaboratorsWorker, feature_c
expect(Gitlab::GithubImport::AdvanceStageWorker)
.to receive(:perform_async)
- .with(project.id, {}, :pull_requests_merged_by)
+ .with(project.id, {}, 'pull_requests_merged_by')
worker.import(client, project)
end
diff --git a/spec/workers/gitlab/github_import/stage/import_issue_events_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_issue_events_worker_spec.rb
index 4b4d6a5b625..bad3a5beb0e 100644
--- a/spec/workers/gitlab/github_import/stage/import_issue_events_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/stage/import_issue_events_worker_spec.rb
@@ -33,7 +33,7 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportIssueEventsWorker, feature_cat
expect(Gitlab::GithubImport::AdvanceStageWorker)
.to receive(:perform_async)
- .with(project.id, { '123' => 2 }, :notes)
+ .with(project.id, { '123' => 2 }, 'notes')
worker.import(client, project)
end
@@ -44,7 +44,7 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportIssueEventsWorker, feature_cat
it 'skips issue events import and calls next stage' do
expect(Gitlab::GithubImport::Importer::SingleEndpointIssueEventsImporter).not_to receive(:new)
- expect(Gitlab::GithubImport::AdvanceStageWorker).to receive(:perform_async).with(project.id, {}, :notes)
+ expect(Gitlab::GithubImport::AdvanceStageWorker).to receive(:perform_async).with(project.id, {}, 'notes')
worker.import(client, project)
end
diff --git a/spec/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker_spec.rb
index 7a5813122f4..10f6ebfbab9 100644
--- a/spec/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker_spec.rb
@@ -35,7 +35,7 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportIssuesAndDiffNotesWorker, feat
expect(Gitlab::GithubImport::AdvanceStageWorker)
.to receive(:perform_async)
- .with(project.id, { '123' => 2 }, :issue_events)
+ .with(project.id, { '123' => 2 }, 'issue_events')
worker.import(client, project)
end
diff --git a/spec/workers/gitlab/github_import/stage/import_lfs_objects_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_lfs_objects_worker_spec.rb
index 5d476543743..40194a91b3a 100644
--- a/spec/workers/gitlab/github_import/stage/import_lfs_objects_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/stage/import_lfs_objects_worker_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportLfsObjectsWorker, feature_cate
expect(Gitlab::GithubImport::AdvanceStageWorker)
.to receive(:perform_async)
- .with(project.id, { '123' => 2 }, :finish)
+ .with(project.id, { '123' => 2 }, 'finish')
worker.import(project)
end
diff --git a/spec/workers/gitlab/github_import/stage/import_notes_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_notes_worker_spec.rb
index 9584708802a..69078a666a5 100644
--- a/spec/workers/gitlab/github_import/stage/import_notes_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/stage/import_notes_worker_spec.rb
@@ -35,7 +35,7 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportNotesWorker, feature_category:
expect(Gitlab::GithubImport::AdvanceStageWorker)
.to receive(:perform_async)
- .with(project.id, { '123' => 2 }, :attachments)
+ .with(project.id, { '123' => 2 }, 'attachments')
worker.import(client, project)
end
diff --git a/spec/workers/gitlab/github_import/stage/import_protected_branches_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_protected_branches_worker_spec.rb
index 7ecce82dacb..b73f8c6524d 100644
--- a/spec/workers/gitlab/github_import/stage/import_protected_branches_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/stage/import_protected_branches_worker_spec.rb
@@ -25,12 +25,9 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportProtectedBranchesWorker, featu
.to receive(:execute)
.and_return(waiter)
- expect(import_state)
- .to receive(:refresh_jid_expiration)
-
expect(Gitlab::GithubImport::AdvanceStageWorker)
.to receive(:perform_async)
- .with(project.id, { '123' => 2 }, :lfs_objects)
+ .with(project.id, { '123' => 2 }, 'lfs_objects')
worker.import(client, project)
end
diff --git a/spec/workers/gitlab/github_import/stage/import_pull_requests_merged_by_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_pull_requests_merged_by_worker_spec.rb
index 5917b827d65..b214f6a97d4 100644
--- a/spec/workers/gitlab/github_import/stage/import_pull_requests_merged_by_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/stage/import_pull_requests_merged_by_worker_spec.rb
@@ -24,12 +24,9 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportPullRequestsMergedByWorker, fe
.to receive(:execute)
.and_return(waiter)
- expect(import_state)
- .to receive(:refresh_jid_expiration)
-
expect(Gitlab::GithubImport::AdvanceStageWorker)
.to receive(:perform_async)
- .with(project.id, { '123' => 2 }, :pull_request_review_requests)
+ .with(project.id, { '123' => 2 }, 'pull_request_review_requests')
worker.import(client, project)
end
diff --git a/spec/workers/gitlab/github_import/stage/import_pull_requests_review_requests_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_pull_requests_review_requests_worker_spec.rb
index b473de73086..4468de7e691 100644
--- a/spec/workers/gitlab/github_import/stage/import_pull_requests_review_requests_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/stage/import_pull_requests_review_requests_worker_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportPullRequestsReviewRequestsWork
subject(:worker) { described_class.new }
let(:project) { instance_double(Project, id: 1, import_state: import_state) }
- let(:import_state) { instance_double(ProjectImportState, refresh_jid_expiration: true) }
+ let(:import_state) { instance_double(ProjectImportState) }
let(:client) { instance_double(Gitlab::GithubImport::Client) }
let(:importer) { instance_double(Gitlab::GithubImport::Importer::PullRequests::ReviewRequestsImporter) }
let(:waiter) { Gitlab::JobWaiter.new(2, '123') }
@@ -21,11 +21,10 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportPullRequestsReviewRequestsWork
.and_return(importer)
expect(importer).to receive(:execute).and_return(waiter)
- expect(import_state).to receive(:refresh_jid_expiration)
expect(Gitlab::GithubImport::AdvanceStageWorker)
.to receive(:perform_async)
- .with(project.id, { '123' => 2 }, :pull_request_reviews)
+ .with(project.id, { '123' => 2 }, 'pull_request_reviews')
worker.import(client, project)
end
diff --git a/spec/workers/gitlab/github_import/stage/import_pull_requests_reviews_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_pull_requests_reviews_worker_spec.rb
index 34d3ce9fe95..48b41435adb 100644
--- a/spec/workers/gitlab/github_import/stage/import_pull_requests_reviews_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/stage/import_pull_requests_reviews_worker_spec.rb
@@ -25,11 +25,9 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportPullRequestsReviewsWorker, fea
.to receive(:execute)
.and_return(waiter)
- expect(import_state).to receive(:refresh_jid_expiration)
-
expect(Gitlab::GithubImport::AdvanceStageWorker)
.to receive(:perform_async)
- .with(project.id, { '123' => 2 }, :issues_and_diff_notes)
+ .with(project.id, { '123' => 2 }, 'issues_and_diff_notes')
worker.import(client, project)
end
diff --git a/spec/workers/gitlab/github_import/stage/import_pull_requests_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_pull_requests_worker_spec.rb
index f9b4a8a99f0..2ea66d8cdf3 100644
--- a/spec/workers/gitlab/github_import/stage/import_pull_requests_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/stage/import_pull_requests_worker_spec.rb
@@ -27,9 +27,6 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportPullRequestsWorker, feature_ca
.to receive(:execute)
.and_return(waiter)
- expect(import_state)
- .to receive(:refresh_jid_expiration)
-
expect(InternalId).to receive(:exists?).and_return(false)
expect(client).to receive(:each_object).with(
@@ -38,7 +35,7 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportPullRequestsWorker, feature_ca
expect(Gitlab::GithubImport::AdvanceStageWorker)
.to receive(:perform_async)
- .with(project.id, { '123' => 2 }, :collaborators)
+ .with(project.id, { '123' => 2 }, 'collaborators')
expect(MergeRequest).to receive(:track_target_project_iid!)
@@ -59,9 +56,6 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportPullRequestsWorker, feature_ca
.to receive(:execute)
.and_return(waiter)
- expect(import_state)
- .to receive(:refresh_jid_expiration)
-
expect(InternalId).to receive(:exists?).and_return(false)
expect(client).to receive(:each_object).with(
@@ -70,7 +64,7 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportPullRequestsWorker, feature_ca
expect(Gitlab::GithubImport::AdvanceStageWorker)
.to receive(:perform_async)
- .with(project.id, { '123' => 2 }, :collaborators)
+ .with(project.id, { '123' => 2 }, 'collaborators')
expect(MergeRequest).not_to receive(:track_target_project_iid!)
@@ -91,9 +85,6 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportPullRequestsWorker, feature_ca
.to receive(:execute)
.and_return(waiter)
- expect(import_state)
- .to receive(:refresh_jid_expiration)
-
expect(InternalId).to receive(:exists?).and_return(true)
expect(client).not_to receive(:each_object)
diff --git a/spec/workers/gitlab/jira_import/stage/import_issues_worker_spec.rb b/spec/workers/gitlab/jira_import/stage/import_issues_worker_spec.rb
index 594f9618770..f9b03fc1b44 100644
--- a/spec/workers/gitlab/jira_import/stage/import_issues_worker_spec.rb
+++ b/spec/workers/gitlab/jira_import/stage/import_issues_worker_spec.rb
@@ -25,7 +25,11 @@ RSpec.describe Gitlab::JiraImport::Stage::ImportIssuesWorker, feature_category:
end
context 'when import started', :clean_gitlab_redis_cache do
- let_it_be(:jira_integration) { create(:jira_integration, project: project) }
+ let(:job_waiter) { Gitlab::JobWaiter.new(2, 'some-job-key') }
+
+ before_all do
+ create(:jira_integration, project: project)
+ end
before do
jira_import.start!
@@ -34,6 +38,40 @@ RSpec.describe Gitlab::JiraImport::Stage::ImportIssuesWorker, feature_category:
end
end
+ it 'uses a custom http client for the issues importer' do
+ jira_integration = project.jira_integration
+ client = instance_double(JIRA::Client)
+ issue_importer = instance_double(Gitlab::JiraImport::IssuesImporter)
+
+ allow(Project).to receive(:find_by_id).with(project.id).and_return(project)
+ allow(issue_importer).to receive(:execute).and_return(job_waiter)
+
+ expect(jira_integration).to receive(:client).with(read_timeout: 2.minutes).and_return(client)
+ expect(Gitlab::JiraImport::IssuesImporter).to receive(:new).with(
+ project,
+ client
+ ).and_return(issue_importer)
+
+ described_class.new.perform(project.id)
+ end
+
+ context 'when increase_jira_import_issues_timeout feature flag is disabled' do
+ before do
+ stub_feature_flags(increase_jira_import_issues_timeout: false)
+ end
+
+ it 'does not provide a custom client to IssuesImporter' do
+ issue_importer = instance_double(Gitlab::JiraImport::IssuesImporter)
+ expect(Gitlab::JiraImport::IssuesImporter).to receive(:new).with(
+ instance_of(Project),
+ nil
+ ).and_return(issue_importer)
+ allow(issue_importer).to receive(:execute).and_return(job_waiter)
+
+ described_class.new.perform(project.id)
+ end
+ end
+
context 'when start_at is nil' do
it_behaves_like 'advance to next stage', :attachments
end
diff --git a/spec/workers/groups/update_statistics_worker_spec.rb b/spec/workers/groups/update_statistics_worker_spec.rb
index f47606f0580..5fc4ccdab0d 100644
--- a/spec/workers/groups/update_statistics_worker_spec.rb
+++ b/spec/workers/groups/update_statistics_worker_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Groups::UpdateStatisticsWorker, feature_category: :source_code_management do
let_it_be(:group) { create(:group) }
- let(:statistics) { %w(wiki_size) }
+ let(:statistics) { %w[wiki_size] }
subject(:worker) { described_class.new }
diff --git a/spec/workers/jira_connect/sync_branch_worker_spec.rb b/spec/workers/jira_connect/sync_branch_worker_spec.rb
index 1c2661ad0e5..18eb22b8a47 100644
--- a/spec/workers/jira_connect/sync_branch_worker_spec.rb
+++ b/spec/workers/jira_connect/sync_branch_worker_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe JiraConnect::SyncBranchWorker, feature_category: :integrations do
let(:project_id) { project.id }
let(:branch_name) { 'master' }
- let(:commit_shas) { %w(b83d6e3 5a62481) }
+ let(:commit_shas) { %w[b83d6e3 5a62481] }
let(:update_sequence_id) { 1 }
def perform
diff --git a/spec/workers/merge_request_cleanup_refs_worker_spec.rb b/spec/workers/merge_request_cleanup_refs_worker_spec.rb
index a2df31037be..6c87b6827a8 100644
--- a/spec/workers/merge_request_cleanup_refs_worker_spec.rb
+++ b/spec/workers/merge_request_cleanup_refs_worker_spec.rb
@@ -40,18 +40,6 @@ RSpec.describe MergeRequestCleanupRefsWorker, feature_category: :code_review_wor
end
end
end
-
- context 'when merge_request_refs_cleanup flag is disabled' do
- before do
- stub_feature_flags(merge_request_refs_cleanup: false)
- end
-
- it 'does nothing' do
- expect(MergeRequests::CleanupRefsService).not_to receive(:new)
-
- worker.perform_work
- end
- end
end
context 'when there is no next cleanup schedule found' do
diff --git a/spec/workers/merge_requests/set_reviewer_reviewed_worker_spec.rb b/spec/workers/merge_requests/set_reviewer_reviewed_worker_spec.rb
index 942cf8e87e9..7341a0dcc5b 100644
--- a/spec/workers/merge_requests/set_reviewer_reviewed_worker_spec.rb
+++ b/spec/workers/merge_requests/set_reviewer_reviewed_worker_spec.rb
@@ -14,21 +14,21 @@ RSpec.describe MergeRequests::SetReviewerReviewedWorker, feature_category: :sour
let(:event) { approved_event }
end
- it 'calls MergeRequests::MarkReviewerReviewedService' do
+ it 'calls MergeRequests::UpdateReviewerStateService' do
expect_next_instance_of(
- MergeRequests::MarkReviewerReviewedService,
+ MergeRequests::UpdateReviewerStateService,
project: project, current_user: user
) do |service|
- expect(service).to receive(:execute).with(merge_request)
+ expect(service).to receive(:execute).with(merge_request, "reviewed")
end
consume_event(subscriber: described_class, event: approved_event)
end
shared_examples 'when object does not exist' do
- it 'logs and does not call MergeRequests::MarkReviewerReviewedService' do
+ it 'logs and does not call MergeRequests::UpdateReviewerStateService' do
expect(Sidekiq.logger).to receive(:info).with(hash_including(log_payload))
- expect(MergeRequests::MarkReviewerReviewedService).not_to receive(:new)
+ expect(MergeRequests::UpdateReviewerStateService).not_to receive(:new)
expect { consume_event(subscriber: described_class, event: approved_event) }
.not_to raise_exception
diff --git a/spec/workers/packages/cleanup_package_registry_worker_spec.rb b/spec/workers/packages/cleanup_package_registry_worker_spec.rb
index f70103070ef..f2787a92fbf 100644
--- a/spec/workers/packages/cleanup_package_registry_worker_spec.rb
+++ b/spec/workers/packages/cleanup_package_registry_worker_spec.rb
@@ -58,6 +58,28 @@ RSpec.describe Packages::CleanupPackageRegistryWorker, feature_category: :packag
end
end
+ context 'with npm metadata caches pending destruction' do
+ let_it_be(:npm_metadata_cache) { create(:npm_metadata_cache, :stale) }
+
+ it_behaves_like 'an idempotent worker'
+
+ it 'queues the cleanup job' do
+ expect(Packages::Npm::CleanupStaleMetadataCacheWorker).to receive(:perform_with_capacity)
+
+ perform
+ end
+ end
+
+ context 'with no npm metadata caches pending destruction' do
+ it_behaves_like 'an idempotent worker'
+
+ it 'does not queue the cleanup job' do
+ expect(Packages::Npm::CleanupStaleMetadataCacheWorker).not_to receive(:perform_with_capacity)
+
+ perform
+ end
+ end
+
describe 'counts logging' do
let_it_be(:processing_package_file) { create(:package_file, status: :processing) }
diff --git a/spec/workers/packages/npm/cleanup_stale_metadata_cache_worker_spec.rb b/spec/workers/packages/npm/cleanup_stale_metadata_cache_worker_spec.rb
new file mode 100644
index 00000000000..390ed0ee453
--- /dev/null
+++ b/spec/workers/packages/npm/cleanup_stale_metadata_cache_worker_spec.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Packages::Npm::CleanupStaleMetadataCacheWorker, type: :worker, feature_category: :package_registry do
+ let(:worker) { described_class.new }
+
+ describe '#perform_work' do
+ subject { worker.perform_work }
+
+ context 'with no work to do' do
+ it { is_expected.to be_nil }
+ end
+
+ context 'with work to do' do
+ let_it_be(:npm_metadata_cache1) { create(:npm_metadata_cache) }
+ let_it_be(:npm_metadata_cache2) { create(:npm_metadata_cache, :stale) }
+
+ let_it_be(:npm_metadata_cache3) do
+ create(:npm_metadata_cache, :stale, updated_at: 1.year.ago, created_at: 1.year.ago)
+ end
+
+ it 'deletes the oldest stale metadata cache based on id', :aggregate_failures do
+ expect(worker).to receive(:log_extra_metadata_on_done).with(:npm_metadata_cache_id, npm_metadata_cache2.id)
+
+ expect { subject }.to change { Packages::Npm::MetadataCache.count }.by(-1)
+ expect { npm_metadata_cache2.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+
+ context 'with a stale metadata cache' do
+ let_it_be(:npm_metadata_cache) { create(:npm_metadata_cache, :stale) }
+
+ context 'with an error during the destroy' do
+ before do
+ allow_next_found_instance_of(Packages::Npm::MetadataCache) do |metadata_cache|
+ allow(metadata_cache).to receive(:destroy!).and_raise('Error!')
+ end
+ end
+
+ it 'handles the error' do
+ expect(Gitlab::ErrorTracking).to receive(:log_exception)
+ .with(instance_of(RuntimeError), class: described_class.name)
+ expect { subject }.to change { Packages::Npm::MetadataCache.error.count }.from(0).to(1)
+ expect(npm_metadata_cache.reload).to be_error
+ end
+ end
+
+ context 'when trying to destroy a destroyed record' do
+ before do
+ allow_next_found_instance_of(Packages::Npm::MetadataCache) do |metadata_cache|
+ destroy_method = metadata_cache.method(:destroy!)
+
+ allow(metadata_cache).to receive(:destroy!) do
+ destroy_method.call
+
+ raise 'Error!'
+ end
+ end
+ end
+
+ it 'handles the error' do
+ expect(Gitlab::ErrorTracking).to receive(:log_exception)
+ .with(instance_of(RuntimeError), class: described_class.name)
+ expect { subject }.not_to change { Packages::Npm::MetadataCache.count }
+ expect(npm_metadata_cache.reload).to be_error
+ end
+ end
+ end
+ end
+
+ describe '#max_running_jobs' do
+ let(:capacity) { described_class::MAX_CAPACITY }
+
+ subject { worker.max_running_jobs }
+
+ it { is_expected.to eq(capacity) }
+ end
+end
diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb
index 2e0a2535453..dcece830a85 100644
--- a/spec/workers/post_receive_spec.rb
+++ b/spec/workers/post_receive_spec.rb
@@ -391,7 +391,7 @@ RSpec.describe PostReceive, feature_category: :source_code_management do
it 'enqueues a UpdateMergeRequestsWorker job' do
allow(Project).to receive(:find_by).and_return(project)
- expect_next(MergeRequests::PushedBranchesService).to receive(:execute).and_return(%w(tést))
+ expect_next(MergeRequests::PushedBranchesService).to receive(:execute).and_return(%w[tést])
expect(UpdateMergeRequestsWorker).to receive(:perform_async).with(project.id, project.first_owner.id, any_args)
diff --git a/spec/workers/project_cache_worker_spec.rb b/spec/workers/project_cache_worker_spec.rb
index 4d468897599..7ef2494b5cf 100644
--- a/spec/workers/project_cache_worker_spec.rb
+++ b/spec/workers/project_cache_worker_spec.rb
@@ -35,10 +35,10 @@ RSpec.describe ProjectCacheWorker, feature_category: :source_code_management do
context 'with an existing project' do
it 'refreshes the method caches' do
expect_any_instance_of(Repository).to receive(:refresh_method_caches)
- .with(%i(readme))
+ .with(%i[readme])
.and_call_original
- worker.perform(project.id, %w(readme))
+ worker.perform(project.id, %w[readme])
end
context 'with statistics disabled' do
@@ -52,7 +52,7 @@ RSpec.describe ProjectCacheWorker, feature_category: :source_code_management do
end
context 'with statistics' do
- let(:statistics) { %w(repository_size) }
+ let(:statistics) { %w[repository_size] }
it 'updates the project statistics' do
expect(worker).to receive(:update_statistics)
@@ -69,16 +69,16 @@ RSpec.describe ProjectCacheWorker, feature_category: :source_code_management do
allow(Gitlab::MarkupHelper).to receive(:plain?).and_return(true)
expect_any_instance_of(Repository).to receive(:refresh_method_caches)
- .with(%i(readme))
+ .with(%i[readme])
.and_call_original
- worker.perform(project.id, %w(readme))
+ worker.perform(project.id, %w[readme])
end
end
end
end
describe '#update_statistics' do
- let(:statistics) { %w(repository_size) }
+ let(:statistics) { %w[repository_size] }
context 'when a lease could not be obtained' do
it 'does not update the project statistics' do
@@ -120,7 +120,7 @@ RSpec.describe ProjectCacheWorker, feature_category: :source_code_management do
end
it_behaves_like 'an idempotent worker' do
- let(:job_args) { [project.id, %w(readme), %w(repository_size)] }
+ let(:job_args) { [project.id, %w[readme], %w[repository_size]] }
it 'calls Projects::UpdateStatisticsService service twice', :clean_gitlab_redis_shared_state do
expect(Projects::UpdateStatisticsService).to receive(:new).once.and_return(double(execute: true))
diff --git a/spec/workers/projects/import_export/after_import_merge_requests_worker_spec.rb b/spec/workers/projects/import_export/after_import_merge_requests_worker_spec.rb
new file mode 100644
index 00000000000..42b67a0941a
--- /dev/null
+++ b/spec/workers/projects/import_export/after_import_merge_requests_worker_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::ImportExport::AfterImportMergeRequestsWorker, feature_category: :importers do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:merge_requests) { project.merge_requests }
+
+ let(:worker) { described_class.new }
+
+ describe '#perform' do
+ it 'sets the latest merge request diff ids' do
+ expect(project.class).to receive(:find_by_id).and_return(project)
+ expect(merge_requests).to receive(:set_latest_merge_request_diff_ids!)
+
+ worker.perform(project.id)
+ end
+
+ it_behaves_like 'an idempotent worker' do
+ let(:job_args) { [project.id] }
+ end
+ end
+end
diff --git a/spec/workers/projects/record_target_platforms_worker_spec.rb b/spec/workers/projects/record_target_platforms_worker_spec.rb
index d4515f7727a..23705d0c86e 100644
--- a/spec/workers/projects/record_target_platforms_worker_spec.rb
+++ b/spec/workers/projects/record_target_platforms_worker_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe Projects::RecordTargetPlatformsWorker, feature_category: :activat
let_it_be(:project) { create(:project, :repository, detected_repository_languages: true) }
let(:worker) { described_class.new }
- let(:service_result) { %w(ios osx watchos) }
+ let(:service_result) { %w[ios osx watchos] }
let(:service_double) { instance_double(Projects::RecordTargetPlatformsService, execute: service_result) }
let(:lease_key) { "#{described_class.name.underscore}:#{project.id}" }
let(:lease_timeout) { described_class::LEASE_TIMEOUT }
diff --git a/spec/workers/repository_fork_worker_spec.rb b/spec/workers/repository_fork_worker_spec.rb
index 3a5528b6a04..bd452c21d9a 100644
--- a/spec/workers/repository_fork_worker_spec.rb
+++ b/spec/workers/repository_fork_worker_spec.rb
@@ -19,11 +19,11 @@ RSpec.describe RepositoryForkWorker, feature_category: :source_code_management d
fork_project(project, forked_project.creator, target_project: forked_project, repository: true)
end
- shared_examples 'RepositoryForkWorker performing' do
- def expect_fork_repository(success:)
+ shared_examples 'RepositoryForkWorker performing' do |branch|
+ def expect_fork_repository(success:, branch:)
allow(::Gitlab::GitalyClient::RepositoryService).to receive(:new).and_call_original
expect_next_instance_of(::Gitlab::GitalyClient::RepositoryService, forked_project.repository.raw) do |svc|
- exp = expect(svc).to receive(:fork_repository).with(project.repository.raw)
+ exp = expect(svc).to receive(:fork_repository).with(project.repository.raw, branch)
if success
exp.and_return(true)
@@ -39,20 +39,20 @@ RSpec.describe RepositoryForkWorker, feature_category: :source_code_management d
it 'creates a new repository from a fork' do
allow(subject).to receive(:jid).and_return(jid)
- expect_fork_repository(success: true)
+ expect_fork_repository(success: true, branch: branch)
perform!
end
end
it "creates a new repository from a fork" do
- expect_fork_repository(success: true)
+ expect_fork_repository(success: true, branch: branch)
perform!
end
it 'protects the default branch' do
- expect_fork_repository(success: true)
+ expect_fork_repository(success: true, branch: branch)
perform!
@@ -60,7 +60,7 @@ RSpec.describe RepositoryForkWorker, feature_category: :source_code_management d
end
it 'flushes various caches' do
- expect_fork_repository(success: true)
+ expect_fork_repository(success: true, branch: branch)
# Works around https://github.com/rspec/rspec-mocks/issues/910
expect(Project).to receive(:find).with(forked_project.id).and_return(forked_project)
@@ -79,13 +79,13 @@ RSpec.describe RepositoryForkWorker, feature_category: :source_code_management d
it 'handles bad fork' do
error_message = "Unable to fork project #{forked_project.id} for repository #{project.disk_path} -> #{forked_project.disk_path}: Failed to create fork repository"
- expect_fork_repository(success: false)
+ expect_fork_repository(success: false, branch: branch)
expect { perform! }.to raise_error(StandardError, error_message)
end
it 'calls Projects::LfsPointers::LfsLinkService#execute with OIDs of source project LFS objects' do
- expect_fork_repository(success: true)
+ expect_fork_repository(success: true, branch: branch)
expect_next_instance_of(Projects::LfsPointers::LfsLinkService) do |service|
expect(service).to receive(:execute).with(project.lfs_objects_oids)
end
@@ -96,7 +96,7 @@ RSpec.describe RepositoryForkWorker, feature_category: :source_code_management d
it "handles LFS objects link failure" do
error_message = "Unable to fork project #{forked_project.id} for repository #{project.disk_path} -> #{forked_project.disk_path}: Source project has too many LFS objects"
- expect_fork_repository(success: true)
+ expect_fork_repository(success: true, branch: branch)
expect_next_instance_of(Projects::LfsPointers::LfsLinkService) do |service|
expect(service).to receive(:execute).and_raise(Projects::LfsPointers::LfsLinkService::TooManyOidsError)
end
@@ -113,6 +113,16 @@ RSpec.describe RepositoryForkWorker, feature_category: :source_code_management d
it_behaves_like 'RepositoryForkWorker performing'
end
+ context 'when a specific branch is requested' do
+ def perform!
+ forked_project.create_import_data(data: { fork_branch: 'wip' })
+
+ subject.perform(forked_project.id)
+ end
+
+ it_behaves_like 'RepositoryForkWorker performing', 'wip'
+ end
+
context 'project ID, storage and repo paths passed' do
def perform!
subject.perform(forked_project.id, 'repos/path', project.disk_path)
diff --git a/spec/workers/schedule_merge_request_cleanup_refs_worker_spec.rb b/spec/workers/schedule_merge_request_cleanup_refs_worker_spec.rb
index b93202fe9b3..173374b02a5 100644
--- a/spec/workers/schedule_merge_request_cleanup_refs_worker_spec.rb
+++ b/spec/workers/schedule_merge_request_cleanup_refs_worker_spec.rb
@@ -13,18 +13,6 @@ RSpec.describe ScheduleMergeRequestCleanupRefsWorker, feature_category: :code_re
worker.perform
end
- context 'when merge_request_refs_cleanup flag is disabled' do
- before do
- stub_feature_flags(merge_request_refs_cleanup: false)
- end
-
- it 'does not schedule any merge request clean ups' do
- expect(MergeRequestCleanupRefsWorker).not_to receive(:perform_with_capacity)
-
- worker.perform
- end
- end
-
it 'retries stuck cleanup schedules' do
expect(MergeRequest::CleanupSchedule).to receive(:stuck_retry!)
diff --git a/spec/workers/stuck_merge_jobs_worker_spec.rb b/spec/workers/stuck_merge_jobs_worker_spec.rb
index 44dc6550cdb..03f371ab740 100644
--- a/spec/workers/stuck_merge_jobs_worker_spec.rb
+++ b/spec/workers/stuck_merge_jobs_worker_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe StuckMergeJobsWorker, feature_category: :code_review_workflow do
context 'merge job identified as completed' do
it 'updates merge request to merged when locked but has merge_commit_sha' do
- allow(Gitlab::SidekiqStatus).to receive(:completed_jids).and_return(%w(123 456))
+ allow(Gitlab::SidekiqStatus).to receive(:completed_jids).and_return(%w[123 456])
mr_with_sha = create(:merge_request, :locked, merge_jid: '123', state: :locked, merge_commit_sha: 'foo-bar-baz')
mr_without_sha = create(:merge_request, :locked, merge_jid: '123', state: :locked, merge_commit_sha: nil)
@@ -23,7 +23,7 @@ RSpec.describe StuckMergeJobsWorker, feature_category: :code_review_workflow do
end
it 'updates merge request to opened when locked but has not been merged', :sidekiq_might_not_need_inline do
- allow(Gitlab::SidekiqStatus).to receive(:completed_jids).and_return(%w(123))
+ allow(Gitlab::SidekiqStatus).to receive(:completed_jids).and_return(%w[123])
merge_request = create(:merge_request, :locked, merge_jid: '123', state: :locked)
pipeline = create(:ci_empty_pipeline, project: merge_request.project, ref: merge_request.source_branch, sha: merge_request.source_branch_sha)
@@ -35,7 +35,7 @@ RSpec.describe StuckMergeJobsWorker, feature_category: :code_review_workflow do
end
it 'logs updated stuck merge job ids' do
- allow(Gitlab::SidekiqStatus).to receive(:completed_jids).and_return(%w(123 456))
+ allow(Gitlab::SidekiqStatus).to receive(:completed_jids).and_return(%w[123 456])
create(:merge_request, :locked, merge_jid: '123')
create(:merge_request, :locked, merge_jid: '456')
diff --git a/spec/workers/tasks_to_be_done/create_worker_spec.rb b/spec/workers/tasks_to_be_done/create_worker_spec.rb
deleted file mode 100644
index 3a4e10b6a6f..00000000000
--- a/spec/workers/tasks_to_be_done/create_worker_spec.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe TasksToBeDone::CreateWorker, feature_category: :onboarding do
- let_it_be(:current_user) { create(:user) }
-
- let(:assignee_ids) { [1, 2] }
- let(:job_args) { [123, current_user.id, assignee_ids] }
-
- describe '.perform' do
- it 'executes the task services for all tasks to be done', :aggregate_failures do
- expect { described_class.new.perform(*job_args) }.not_to change { Issue.count }
- end
- end
-
- include_examples 'an idempotent worker' do
- it 'creates 3 task issues' do
- expect { subject }.not_to change { Issue.count }
- end
- end
-end
diff --git a/spec/workers/update_project_statistics_worker_spec.rb b/spec/workers/update_project_statistics_worker_spec.rb
index c5e6f45a201..ba0834e64dc 100644
--- a/spec/workers/update_project_statistics_worker_spec.rb
+++ b/spec/workers/update_project_statistics_worker_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe UpdateProjectStatisticsWorker, feature_category: :source_code_man
let(:worker) { described_class.new }
let(:project) { create(:project, :repository) }
- let(:statistics) { %w(repository_size) }
+ let(:statistics) { %w[repository_size] }
let(:lease_key) { "namespace:namespaces_root_statistics:#{project.namespace_id}" }
describe '#perform' do
diff --git a/storybook/config/addons/vuex_store/index.js b/storybook/config/addons/vuex_store/index.js
new file mode 100644
index 00000000000..759c7c9b38f
--- /dev/null
+++ b/storybook/config/addons/vuex_store/index.js
@@ -0,0 +1,16 @@
+import Vuex from 'vuex'; // eslint-disable-line no-restricted-imports
+
+const createVuexStore = (store) => new Vuex.Store(store);
+
+/*
+ * Story decorator for injecting a Vuex store
+ */
+export const withVuexStore = (story, context) => {
+ Object.assign(context, { createVuexStore });
+ return {
+ components: {
+ story,
+ },
+ template: `<story />`,
+ };
+};
diff --git a/storybook/config/preview.js b/storybook/config/preview.js
index e31f36ca70e..9934d683537 100644
--- a/storybook/config/preview.js
+++ b/storybook/config/preview.js
@@ -2,6 +2,7 @@
import './gon';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
+import Vuex from 'vuex'; // eslint-disable-line no-restricted-imports
import translateMixin from '~/vue_shared/translate';
import { initializeGitLabAPIAccess } from './addons/gitlab_api_access/preview';
@@ -15,6 +16,7 @@ initializeGitLabAPIAccess();
translateMixin(Vue);
Vue.use(VueApollo);
+Vue.use(Vuex);
stylesheetsRequireCtx('./application.scss');
stylesheetsRequireCtx('./application_utilities.scss');
diff --git a/tooling/danger/analytics_instrumentation.rb b/tooling/danger/analytics_instrumentation.rb
index cb0ca998c04..d49c0f9e6ba 100644
--- a/tooling/danger/analytics_instrumentation.rb
+++ b/tooling/danger/analytics_instrumentation.rb
@@ -1,10 +1,12 @@
# frozen_string_literal: true
-# rubocop:disable Style/SignalException
+require_relative 'suggestor'
module Tooling
module Danger
module AnalyticsInstrumentation
+ include ::Tooling::Danger::Suggestor
+
METRIC_DIRS = %w[lib/gitlab/usage/metrics/instrumentations ee/lib/gitlab/usage/metrics/instrumentations].freeze
APPROVED_LABEL = 'analytics instrumentation::approved'
REVIEW_LABEL = 'analytics instrumentation::review pending'
@@ -28,6 +30,10 @@ module Tooling
Please use [Instrumentation Classes](https://docs.gitlab.com/ee/development/service_ping/metrics_instrumentation.html) instead.
MSG
+ CHANGE_DEPRECATED_DATA_SOURCE_MESSAGE = <<~MSG
+ Redis and RedisHLL tracking is deprecated, consider using Internal Events tracking instead https://docs.gitlab.com/ee/development/internal_analytics/internal_event_instrumentation/quick_start.html#defining-event-and-metrics
+ MSG
+
WORKFLOW_LABELS = [
APPROVED_LABEL,
REVIEW_LABEL
@@ -60,6 +66,17 @@ module Tooling
warn format(CHANGED_USAGE_DATA_MESSAGE)
end
+ def check_deprecated_data_sources!
+ new_metric_files.each do |filename|
+ add_suggestion(
+ filename: filename,
+ regex: /^\+?\s+data_source: redis\w*/,
+ replacement: 'data_source: internal_events',
+ comment_text: CHANGE_DEPRECATED_DATA_SOURCE_MESSAGE
+ )
+ end
+ end
+
private
def convert_to_table(items)
@@ -101,6 +118,10 @@ module Tooling
end
end
+ def new_metric_files
+ helper.added_files.select { |f| f.include?('config/metrics') && f.end_with?('.yml') }
+ end
+
def each_metric(&block)
METRIC_DIRS.each do |dir|
Dir.glob(File.join(dir, '*.rb')).each(&block)
diff --git a/tooling/danger/change_column_default.rb b/tooling/danger/change_column_default.rb
new file mode 100644
index 00000000000..57ebb06a656
--- /dev/null
+++ b/tooling/danger/change_column_default.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require_relative 'suggestor'
+
+module Tooling
+ module Danger
+ module ChangeColumnDefault
+ include ::Tooling::Danger::Suggestor
+
+ METHODS = %w[change_column_default remove_column_default].freeze
+ MIGRATION_METHODS_REGEX = /^\+\s*(.*\.)?(#{METHODS.join('|')})[(\s]/
+ MIGRATION_FILES_REGEX = %r{^db/(post_)?migrate}
+
+ DOCUMENTATION = 'https://docs.gitlab.com/ee/development/database/avoiding_downtime_in_migrations.html#changing-column-defaults'
+ COMMENT =
+ "Changing column default is difficult because of how Rails handles values that are equal to the default. " \
+ "Please make sure all columns are declared as `columns_changing_default`. " \
+ "For more information, see [Avoiding downtime in migrations documentation](#{DOCUMENTATION}).".freeze
+
+ def add_comment_for_change_column_default
+ migration_files.each do |filename|
+ add_suggestion(filename: filename, regex: MIGRATION_METHODS_REGEX, comment_text: COMMENT)
+ end
+ end
+
+ def migration_files
+ helper.all_changed_files.grep(MIGRATION_FILES_REGEX)
+ end
+ end
+ end
+end
diff --git a/tooling/danger/datateam.rb b/tooling/danger/datateam.rb
index 1354e684d50..7100ac16f69 100644
--- a/tooling/danger/datateam.rb
+++ b/tooling/danger/datateam.rb
@@ -17,7 +17,7 @@ module Tooling
PERFORMANCE_INDICATOR_REGEX = %r{gmau|smau|paid_gmau|umau}
METRIC_REMOVED = %r{\+status: removed}
DATABASE_REGEX = %r{\Adb/structure\.sql}
- STRUCTURE_SQL_FILE = %w[db/structure.sql].freeze
+ DATABASE_LINE_REMOVAL_REGEX = %r{\A-}
def build_message
return unless impacted?
@@ -47,12 +47,15 @@ module Tooling
end.compact
end
- def database_changes?
- !helper.modified_files.grep(DATABASE_REGEX).empty?
+ def database_changed_files
+ database_changed_files = helper.modified_files.grep(DATABASE_REGEX)
+ database_changed_files.select do |file|
+ helper.changed_lines(file).any? { |change| database_line_removal?(change) }
+ end.compact
end
- def database_changed_files
- helper.modified_files & STRUCTURE_SQL_FILE
+ def database_line_removal?(change)
+ change =~ DATABASE_LINE_REMOVAL_REGEX
end
def performance_indicator_changed?(change)
diff --git a/tooling/danger/gitlab_schema_validation_suggestion.rb b/tooling/danger/gitlab_schema_validation_suggestion.rb
new file mode 100644
index 00000000000..e1f049fc732
--- /dev/null
+++ b/tooling/danger/gitlab_schema_validation_suggestion.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require_relative 'suggestion'
+
+module Tooling
+ module Danger
+ module GitlabSchemaValidationSuggestion
+ include ::Tooling::Danger::Suggestor
+
+ MATCH = %r{gitlab_schema: gitlab_main_clusterwide}
+ REPLACEMENT = nil
+ DB_DOCS_PATH = %r{\Adb/docs/[^/]+\.ya?ml\z}
+
+ SUGGESTION = <<~MESSAGE_MARKDOWN
+ :warning: You have added `gitlab_main_clusterwide` as the schema for this table. We expect most tables to use the
+ `gitlab_main_cell` schema instead, as using the clusterwide schema can have significant scaling implications.
+
+ Please see the [guidelines on choosing gitlab schema](https://docs.gitlab.com/ee/development/database/multiple_databases.html#guidelines-on-choosing-between-gitlab_main_cell-and-gitlab_main_clusterwide-schema) for more information.
+
+ Please consult with ~"group::tenant scale" if you believe that the clusterwide schema is the best fit for this table.
+ MESSAGE_MARKDOWN
+
+ def add_suggestions_on_using_clusterwide_schema
+ helper.all_changed_files.grep(DB_DOCS_PATH).each do |filename|
+ add_suggestion(
+ filename: filename,
+ regex: MATCH,
+ replacement: REPLACEMENT,
+ comment_text: SUGGESTION
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/tooling/danger/outdated_todo.rb b/tooling/danger/outdated_todo.rb
new file mode 100644
index 00000000000..a5f5cc897a9
--- /dev/null
+++ b/tooling/danger/outdated_todo.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+module Tooling
+ module Danger
+ class OutdatedTodo
+ TODOS_GLOBS = %w[
+ .rubocop_todo/**/*.yml
+ spec/support/rspec_order_todo.yml
+ ].freeze
+
+ def initialize(filenames, context:, todos: TODOS_GLOBS)
+ @filenames = filenames
+ @context = context
+ @todos_globs = todos
+ end
+
+ def check
+ filenames.each do |filename|
+ check_filename(filename)
+ end
+ end
+
+ private
+
+ attr_reader :filenames, :context
+
+ def check_filename(filename)
+ mentions = all_mentions_for(filename)
+
+ return if mentions.empty?
+
+ context.warn <<~MESSAGE
+ `#{filename}` was removed but is mentioned in:
+ #{mentions.join("\n")}
+ MESSAGE
+ end
+
+ def all_mentions_for(filename)
+ todos
+ .filter_map { |todo| mentioned_lines(filename, todo) }
+ .flatten
+ .map { |todo| "- `#{todo}`" }
+ end
+
+ def mentioned_lines(filename, todo)
+ File
+ .foreach(todo)
+ .with_index(1)
+ .select { |text, _line| text.match?(/.*#{filename}.*/) }
+ .map { |_text, line| "#{todo}:#{line}" }
+ end
+
+ def todos
+ @todos ||= @todos_globs.flat_map { |value| Dir.glob(value) }
+ end
+ end
+ end
+end
diff --git a/tooling/danger/project_helper.rb b/tooling/danger/project_helper.rb
index d0cea5516ac..2b781b58a64 100644
--- a/tooling/danger/project_helper.rb
+++ b/tooling/danger/project_helper.rb
@@ -131,7 +131,7 @@ module Tooling
generator_templates/usage_metric_definition/metric_definition\.yml)\z}x => [:backend, :analytics_instrumentation],
%r{gitlab/usage_data(_spec)?\.rb} => [:analytics_instrumentation],
[%r{\.haml\z}, %r{data: \{ track}] => [:analytics_instrumentation],
- [%r{\.(rb|haml)\z}, %r{Gitlab::Tracking\.(event|enabled\?|options)$}] => [:analytics_instrumentation],
+ [%r{\.(rb|haml)\z}, %r{Gitlab::Tracking\.(event|enabled\?|options)}] => [:analytics_instrumentation],
[%r{\.(vue|js)\z}, %r{(Tracking.event|/\btrack\(/|data-track-action)}] => [:analytics_instrumentation],
%r{\A((ee|jh)/)?app/(?!assets|views)[^/]+} => :backend,
diff --git a/tooling/danger/rubocop_inline_disable_suggestion.rb b/tooling/danger/rubocop_inline_disable_suggestion.rb
index 4d1bff9856b..589816a5937 100644
--- a/tooling/danger/rubocop_inline_disable_suggestion.rb
+++ b/tooling/danger/rubocop_inline_disable_suggestion.rb
@@ -5,13 +5,15 @@ require_relative 'suggestion'
module Tooling
module Danger
class RubocopInlineDisableSuggestion < Suggestion
- MATCH = /^\+.*#\s*rubocop\s*:\s*(?:disable|todo)\s+/
- REPLACEMENT = nil
+ MATCH = %r{^(?<line>.*#\s*rubocop\s*:\s*(?:disable|todo)\s+(?:[\w/]+(?:\s*,\s*[\w/]+)*))\s*(?!.*\s*--\s\S).*}
+ REPLACEMENT = '\k<line> -- TODO: Reason why the rule must be disabled'
SUGGESTION = <<~MESSAGE_MARKDOWN
Consider removing this inline disabling and adhering to the rubocop rule.
- If that isn't possible, please provide context as a reply for reviewers.
- See [rubocop best practices](https://docs.gitlab.com/ee/development/rubocop_development_guide.html).
+
+ If that isn't possible, please provide the reason as a code comment in the
+ same line where the rule is disabled separated by ` -- `.
+ See [rubocop best practices](https://docs.gitlab.com/ee/development/rubocop_development_guide.html#disabling-rules-inline).
----
diff --git a/tooling/danger/stable_branch.rb b/tooling/danger/stable_branch.rb
index 6335f82da37..8cb9e87964f 100644
--- a/tooling/danger/stable_branch.rb
+++ b/tooling/danger/stable_branch.rb
@@ -56,7 +56,6 @@ module Tooling
Read the "QA e2e:package-and-test-ee" section for more details.
MSG
- # rubocop:disable Style/SignalException
def check!
return unless valid_stable_branch?
@@ -77,7 +76,6 @@ module Tooling
warn WARN_PACKAGE_AND_TEST_MESSAGE
end
end
- # rubocop:enable Style/SignalException
def encourage_package_and_qa_execution?
valid_stable_branch? &&
diff --git a/tooling/danger/suggestion.rb b/tooling/danger/suggestion.rb
index da3c6b0e76f..ff1ca83876d 100644
--- a/tooling/danger/suggestion.rb
+++ b/tooling/danger/suggestion.rb
@@ -1,6 +1,5 @@
# frozen_string_literal: true
-require 'forwardable'
require_relative 'suggestor'
module Tooling
@@ -14,11 +13,8 @@ module Tooling
#
# @see Suggestor
class Suggestion
- extend Forwardable
include ::Tooling::Danger::Suggestor
- def_delegators :@context, :helper, :project_helper, :markdown
-
attr_reader :filename
def initialize(filename, context:)
@@ -34,6 +30,22 @@ module Tooling
comment_text: self.class::SUGGESTION
)
end
+
+ private
+
+ def helper(...)
+ # Previously, we were using `forwardable` but it emitted a mysterious warning:
+ # forwarding to private method Danger::Rubocop#helper
+ @context.helper(...)
+ end
+
+ def project_helper(...)
+ @context.project_helper(...)
+ end
+
+ def markdown(...)
+ @context.markdown(...)
+ end
end
end
end
diff --git a/tooling/lib/tooling/find_changes.rb b/tooling/lib/tooling/find_changes.rb
index c498c83d24b..f6fdf042c15 100755
--- a/tooling/lib/tooling/find_changes.rb
+++ b/tooling/lib/tooling/find_changes.rb
@@ -14,7 +14,8 @@ module Tooling
from:,
changed_files_pathname: nil,
predictive_tests_pathname: nil,
- frontend_fixtures_mapping_pathname: nil
+ frontend_fixtures_mapping_pathname: nil,
+ file_filter: ->(_) { true }
)
raise ArgumentError, ':from can only be :api or :changed_files' unless
@@ -28,6 +29,7 @@ module Tooling
@predictive_tests_pathname = predictive_tests_pathname
@frontend_fixtures_mapping_pathname = frontend_fixtures_mapping_pathname
@from = from
+ @file_filter = file_filter
end
def execute
@@ -50,7 +52,8 @@ module Tooling
private
attr_reader :gitlab_token, :gitlab_endpoint, :mr_project_path,
- :mr_iid, :changed_files_pathname, :predictive_tests_pathname, :frontend_fixtures_mapping_pathname
+ :mr_iid, :changed_files_pathname, :predictive_tests_pathname,
+ :frontend_fixtures_mapping_pathname, :file_filter
def gitlab
@gitlab ||= begin
@@ -82,7 +85,7 @@ module Tooling
@file_changes ||=
case @from
when :api
- mr_changes.changes.flat_map do |change|
+ mr_changes.changes.select(&file_filter).flat_map do |change|
change.to_h.values_at('old_path', 'new_path')
end.uniq
else
diff --git a/tooling/quality/test_level.rb b/tooling/quality/test_level.rb
index 20e00763f65..050eb4f4daf 100644
--- a/tooling/quality/test_level.rb
+++ b/tooling/quality/test_level.rb
@@ -18,6 +18,7 @@ module Quality
unit: %w[
bin
channels
+ click_house
components
config
contracts
diff --git a/vendor/gems/cloud_profiler_agent/spec/cloud_profiler_agent/looper_spec.rb b/vendor/gems/cloud_profiler_agent/spec/cloud_profiler_agent/looper_spec.rb
index 6150fd8b1ea..3019b6ce9f7 100644
--- a/vendor/gems/cloud_profiler_agent/spec/cloud_profiler_agent/looper_spec.rb
+++ b/vendor/gems/cloud_profiler_agent/spec/cloud_profiler_agent/looper_spec.rb
@@ -2,7 +2,7 @@
require 'cloud_profiler_agent'
-RSpec.describe CloudProfilerAgent::Looper, feature_category: :application_performance do
+RSpec.describe CloudProfilerAgent::Looper, feature_category: :cloud_connector do
# rubocop:disable RSpec/InstanceVariable
before do
@now = 0.0
diff --git a/vendor/gems/cloud_profiler_agent/spec/cloud_profiler_agent/pprof_builder_spec.rb b/vendor/gems/cloud_profiler_agent/spec/cloud_profiler_agent/pprof_builder_spec.rb
index 5c94a8e1e44..d5f9e2a42d9 100644
--- a/vendor/gems/cloud_profiler_agent/spec/cloud_profiler_agent/pprof_builder_spec.rb
+++ b/vendor/gems/cloud_profiler_agent/spec/cloud_profiler_agent/pprof_builder_spec.rb
@@ -2,7 +2,7 @@
require 'cloud_profiler_agent'
-RSpec.describe CloudProfilerAgent::PprofBuilder, feature_category: :application_performance do
+RSpec.describe CloudProfilerAgent::PprofBuilder, feature_category: :cloud_connector do
subject { described_class.new(profile, start_time, end_time) }
# load_profile loads one of the example profiles created by
diff --git a/vendor/gems/devise-pbkdf2-encryptable/Gemfile.lock b/vendor/gems/devise-pbkdf2-encryptable/Gemfile.lock
index 617ee7d91f5..47191da2a01 100644
--- a/vendor/gems/devise-pbkdf2-encryptable/Gemfile.lock
+++ b/vendor/gems/devise-pbkdf2-encryptable/Gemfile.lock
@@ -3,85 +3,115 @@ PATH
specs:
devise-pbkdf2-encryptable (0.0.0)
devise (~> 4.0)
- devise-two-factor (~> 4.0)
+ devise-two-factor (~> 4.1.1)
GEM
remote: https://rubygems.org/
specs:
- actionpack (6.1.6)
- actionview (= 6.1.6)
- activesupport (= 6.1.6)
- rack (~> 2.0, >= 2.0.9)
+ actionpack (7.1.1)
+ actionview (= 7.1.1)
+ activesupport (= 7.1.1)
+ nokogiri (>= 1.8.5)
+ rack (>= 2.2.4)
+ rack-session (>= 1.0.1)
rack-test (>= 0.6.3)
- rails-dom-testing (~> 2.0)
- rails-html-sanitizer (~> 1.0, >= 1.2.0)
- actionview (6.1.6)
- activesupport (= 6.1.6)
+ rails-dom-testing (~> 2.2)
+ rails-html-sanitizer (~> 1.6)
+ actionview (7.1.1)
+ activesupport (= 7.1.1)
builder (~> 3.1)
- erubi (~> 1.4)
- rails-dom-testing (~> 2.0)
- rails-html-sanitizer (~> 1.1, >= 1.2.0)
- activemodel (6.1.6)
- activesupport (= 6.1.6)
- activesupport (6.1.6)
+ erubi (~> 1.11)
+ rails-dom-testing (~> 2.2)
+ rails-html-sanitizer (~> 1.6)
+ activemodel (7.1.1)
+ activesupport (= 7.1.1)
+ activesupport (7.1.1)
+ base64
+ bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2)
+ connection_pool (>= 2.2.5)
+ drb
i18n (>= 1.6, < 2)
minitest (>= 5.1)
+ mutex_m
tzinfo (~> 2.0)
- zeitwerk (~> 2.3)
- attr_encrypted (3.1.0)
+ attr_encrypted (4.0.0)
encryptor (~> 3.0.0)
- bcrypt (3.1.18)
+ base64 (0.1.1)
+ bcrypt (3.1.19)
+ bigdecimal (3.1.4)
builder (3.2.4)
- concurrent-ruby (1.1.10)
+ concurrent-ruby (1.2.2)
+ connection_pool (2.4.1)
crass (1.0.6)
- devise (4.8.1)
+ devise (4.9.3)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
railties (>= 4.1.0)
responders
warden (~> 1.2.3)
- devise-two-factor (4.0.2)
- activesupport (< 7.1)
- attr_encrypted (>= 1.3, < 4, != 2)
+ devise-two-factor (4.1.1)
+ activesupport (~> 7.0)
+ attr_encrypted (>= 1.3, < 5, != 2)
devise (~> 4.0)
- railties (< 7.1)
+ railties (~> 7.0)
rotp (~> 6.0)
diff-lcs (1.5.0)
+ drb (2.1.1)
+ ruby2_keywords
encryptor (3.0.0)
- erubi (1.10.0)
- i18n (1.10.0)
+ erubi (1.12.0)
+ i18n (1.14.1)
concurrent-ruby (~> 1.0)
- loofah (2.18.0)
+ io-console (0.6.0)
+ irb (1.8.3)
+ rdoc
+ reline (>= 0.3.8)
+ loofah (2.21.4)
crass (~> 1.0.2)
- nokogiri (>= 1.5.9)
- method_source (1.0.0)
- mini_portile2 (2.8.0)
- minitest (5.16.0)
- nokogiri (1.13.6)
- mini_portile2 (~> 2.8.0)
+ nokogiri (>= 1.12.0)
+ mini_portile2 (2.8.4)
+ minitest (5.20.0)
+ mutex_m (0.1.2)
+ nokogiri (1.15.4)
+ mini_portile2 (~> 2.8.2)
racc (~> 1.4)
orm_adapter (0.5.0)
- racc (1.6.0)
- rack (2.2.3.1)
- rack-test (1.1.0)
- rack (>= 1.0, < 3)
- rails-dom-testing (2.0.3)
- activesupport (>= 4.2.0)
+ psych (5.1.1.1)
+ stringio
+ racc (1.7.1)
+ rack (3.0.8)
+ rack-session (2.0.0)
+ rack (>= 3.0.0)
+ rack-test (2.1.0)
+ rack (>= 1.3)
+ rackup (2.1.0)
+ rack (>= 3)
+ webrick (~> 1.8)
+ rails-dom-testing (2.2.0)
+ activesupport (>= 5.0.0)
+ minitest
nokogiri (>= 1.6)
- rails-html-sanitizer (1.4.3)
- loofah (~> 2.3)
- railties (6.1.6)
- actionpack (= 6.1.6)
- activesupport (= 6.1.6)
- method_source
+ rails-html-sanitizer (1.6.0)
+ loofah (~> 2.21)
+ nokogiri (~> 1.14)
+ railties (7.1.1)
+ actionpack (= 7.1.1)
+ activesupport (= 7.1.1)
+ irb
+ rackup (>= 1.0.0)
rake (>= 12.2)
- thor (~> 1.0)
+ thor (~> 1.0, >= 1.2.2)
+ zeitwerk (~> 2.6)
rake (13.0.6)
- responders (3.0.1)
- actionpack (>= 5.0)
- railties (>= 5.0)
- rotp (6.2.0)
+ rdoc (6.5.0)
+ psych (>= 4.0.0)
+ reline (0.3.9)
+ io-console (~> 0.5)
+ responders (3.1.1)
+ actionpack (>= 5.2)
+ railties (>= 5.2)
+ rotp (6.3.0)
rspec (3.10.0)
rspec-core (~> 3.10.0)
rspec-expectations (~> 3.10.0)
@@ -95,18 +125,21 @@ GEM
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.10.0)
rspec-support (3.10.3)
- thor (1.2.1)
- tzinfo (2.0.4)
+ ruby2_keywords (0.0.5)
+ stringio (3.0.8)
+ thor (1.2.2)
+ tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
warden (1.2.9)
rack (>= 2.0.9)
- zeitwerk (2.6.0)
+ webrick (1.8.1)
+ zeitwerk (2.6.12)
PLATFORMS
ruby
DEPENDENCIES
- activemodel (~> 6.1, < 8)
+ activemodel (~> 7.0, < 8)
devise-pbkdf2-encryptable!
rspec (~> 3.10.0)
diff --git a/vendor/gems/devise-pbkdf2-encryptable/devise-pbkdf2-encryptable.gemspec b/vendor/gems/devise-pbkdf2-encryptable/devise-pbkdf2-encryptable.gemspec
index 9c7e3dd5af5..cd2c62b457d 100644
--- a/vendor/gems/devise-pbkdf2-encryptable/devise-pbkdf2-encryptable.gemspec
+++ b/vendor/gems/devise-pbkdf2-encryptable/devise-pbkdf2-encryptable.gemspec
@@ -19,8 +19,8 @@ Gem::Specification.new do |spec|
spec.version = '0.0.0'
spec.add_runtime_dependency 'devise', '~> 4.0'
- spec.add_runtime_dependency 'devise-two-factor', '~> 4.0'
+ spec.add_runtime_dependency 'devise-two-factor', '~> 4.1.1'
- spec.add_development_dependency 'activemodel', '~> 6.1', '< 8'
+ spec.add_development_dependency 'activemodel', '~> 7.0', '< 8'
spec.add_development_dependency 'rspec', '~> 3.10.0'
end
diff --git a/vendor/gems/sidekiq-reliable-fetch/Gemfile.lock b/vendor/gems/sidekiq-reliable-fetch/Gemfile.lock
index 57767ee8c3b..aeb163db018 100644
--- a/vendor/gems/sidekiq-reliable-fetch/Gemfile.lock
+++ b/vendor/gems/sidekiq-reliable-fetch/Gemfile.lock
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
- gitlab-sidekiq-fetcher (0.9.0)
+ gitlab-sidekiq-fetcher (0.10.0)
json (>= 2.5)
sidekiq (~> 6.1)
diff --git a/vendor/gems/sidekiq-reliable-fetch/gitlab-sidekiq-fetcher.gemspec b/vendor/gems/sidekiq-reliable-fetch/gitlab-sidekiq-fetcher.gemspec
index 0d0e5e3f6fa..b656267003a 100644
--- a/vendor/gems/sidekiq-reliable-fetch/gitlab-sidekiq-fetcher.gemspec
+++ b/vendor/gems/sidekiq-reliable-fetch/gitlab-sidekiq-fetcher.gemspec
@@ -1,6 +1,6 @@
Gem::Specification.new do |s|
s.name = 'gitlab-sidekiq-fetcher'
- s.version = '0.9.0'
+ s.version = '0.10.0'
s.authors = ['TEA', 'GitLab']
s.email = 'valery@gitlab.com'
s.license = 'LGPL-3.0'
diff --git a/vendor/gems/sidekiq-reliable-fetch/lib/sidekiq/base_reliable_fetch.rb b/vendor/gems/sidekiq-reliable-fetch/lib/sidekiq/base_reliable_fetch.rb
index 39b98a0109f..006aad87abe 100644
--- a/vendor/gems/sidekiq-reliable-fetch/lib/sidekiq/base_reliable_fetch.rb
+++ b/vendor/gems/sidekiq-reliable-fetch/lib/sidekiq/base_reliable_fetch.rb
@@ -230,7 +230,7 @@ module Sidekiq
max_retries_after_interruption = nil
max_retries_after_interruption ||= begin
- Object.const_get(worker_class).sidekiq_options[:max_retries_after_interruption]
+ Object.const_get(worker_class).sidekiq_options['max_retries_after_interruption']
rescue NameError
end
diff --git a/vendor/gems/sidekiq-reliable-fetch/spec/base_reliable_fetch_spec.rb b/vendor/gems/sidekiq-reliable-fetch/spec/base_reliable_fetch_spec.rb
index cdc4409f0d5..32e62925aaf 100644
--- a/vendor/gems/sidekiq-reliable-fetch/spec/base_reliable_fetch_spec.rb
+++ b/vendor/gems/sidekiq-reliable-fetch/spec/base_reliable_fetch_spec.rb
@@ -76,6 +76,19 @@ describe Sidekiq::BaseReliableFetch do
expect(queue2.size).to eq 1
expect(Sidekiq::InterruptedSet.new.size).to eq 0
end
+
+ it 'does not put jobs into interrupted queue if it is disabled on the worker' do
+ stub_const('Bob', double(sidekiq_options: { 'max_retries_after_interruption' => -1 }))
+
+ uow = described_class::UnitOfWork
+ interrupted_job = Sidekiq.dump_json(class: 'Bob', args: [1, 2, 'foo'], interrupted_count: 3)
+ jobs = [ uow.new('queue:foo', interrupted_job), uow.new('queue:foo', job), uow.new('queue:bar', job) ]
+ described_class.new(options).bulk_requeue(jobs, nil)
+
+ expect(queue1.size).to eq 2
+ expect(queue2.size).to eq 1
+ expect(Sidekiq::InterruptedSet.new.size).to eq 0
+ end
end
it 'sets heartbeat' do
diff --git a/vendor/languages.yml b/vendor/languages.yml
index 0a6f78ebe5d..4b1d9ffdb3b 100755
--- a/vendor/languages.yml
+++ b/vendor/languages.yml
@@ -2750,7 +2750,7 @@ Kit:
language_id: 188
Kotlin:
type: programming
- color: "#F18E33"
+ color: "#7F52FF"
extensions:
- ".kt"
- ".ktm"
diff --git a/vendor/project_templates/astro_tailwind.tar.gz b/vendor/project_templates/astro_tailwind.tar.gz
new file mode 100644
index 00000000000..4724b01d3f6
--- /dev/null
+++ b/vendor/project_templates/astro_tailwind.tar.gz
Binary files differ
diff --git a/vendor/project_templates/bridgetown.tar.gz b/vendor/project_templates/bridgetown.tar.gz
index 1fb89694d0f..8b4c63e1be5 100644
--- a/vendor/project_templates/bridgetown.tar.gz
+++ b/vendor/project_templates/bridgetown.tar.gz
Binary files differ
diff --git a/vendor/project_templates/middleman.tar.gz b/vendor/project_templates/middleman.tar.gz
index db09a84ab75..b926aa92a20 100644
--- a/vendor/project_templates/middleman.tar.gz
+++ b/vendor/project_templates/middleman.tar.gz
Binary files differ
diff --git a/vendor/project_templates/typo3_distribution.tar.gz b/vendor/project_templates/typo3_distribution.tar.gz
index b25f949011f..6c29993224b 100644
--- a/vendor/project_templates/typo3_distribution.tar.gz
+++ b/vendor/project_templates/typo3_distribution.tar.gz
Binary files differ
diff --git a/vite.config.js b/vite.config.js
index 6ff6cda0288..9bbfc37903f 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -1,6 +1,5 @@
import path from 'path';
import { defineConfig } from 'vite';
-import svgLoader from 'vite-svg-loader';
import vue from '@vitejs/plugin-vue2';
import graphql from '@rollup/plugin-graphql';
import RubyPlugin from 'vite-plugin-ruby';
@@ -69,6 +68,10 @@ export default defineConfig({
find: '~/',
replacement: javascriptsPath,
},
+ {
+ find: '~katex',
+ replacement: 'katex',
+ },
],
},
plugins: [
@@ -81,9 +84,6 @@ export default defineConfig({
},
}),
graphql(),
- svgLoader({
- defaultImport: 'raw',
- }),
viteCommonjs({
include: [path.resolve(javascriptsPath, 'locale/ensure_single_line.cjs')],
}),
diff --git a/workhorse/go.mod b/workhorse/go.mod
index 04f59a5a6f6..0773904ce21 100644
--- a/workhorse/go.mod
+++ b/workhorse/go.mod
@@ -5,7 +5,6 @@ go 1.19
require (
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.1.0
github.com/BurntSushi/toml v1.3.2
- github.com/FZambia/sentinel v1.1.1
github.com/alecthomas/chroma/v2 v2.9.1
github.com/aws/aws-sdk-go v1.45.20
github.com/disintegration/imaging v1.6.2
@@ -13,14 +12,12 @@ require (
github.com/golang-jwt/jwt/v5 v5.0.0
github.com/golang/gddo v0.0.0-20210115222349-20d68f94ee1f
github.com/golang/protobuf v1.5.3
- github.com/gomodule/redigo v2.0.0+incompatible
github.com/gorilla/websocket v1.5.0
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0
github.com/johannesboyne/gofakes3 v0.0.0-20230914150226-f005f5cc03aa
github.com/jpillora/backoff v1.0.0
github.com/mitchellh/copystructure v1.2.0
github.com/prometheus/client_golang v1.17.0
- github.com/rafaeljusto/redigomock/v3 v3.1.2
github.com/redis/go-redis/v9 v9.2.1
github.com/sebest/xff v0.0.0-20210106013422-671bd2870b3a
github.com/sirupsen/logrus v1.9.3
diff --git a/workhorse/go.sum b/workhorse/go.sum
index 6cf33000fcf..d35e2948db7 100644
--- a/workhorse/go.sum
+++ b/workhorse/go.sum
@@ -85,8 +85,6 @@ github.com/DataDog/datadog-go v4.4.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3
github.com/DataDog/gostackparse v0.5.0/go.mod h1:lTfqcJKqS9KnXQGnyQMCugq3u1FP6UZMfWR0aitKFMM=
github.com/DataDog/sketches-go v1.0.0 h1:chm5KSXO7kO+ywGWJ0Zs6tdmWU8PBXSbywFVciL6BG4=
github.com/DataDog/sketches-go v1.0.0/go.mod h1:O+XkJHWk9w4hDwY2ZUDU31ZC9sNYlYo8DiFsxjYeo1k=
-github.com/FZambia/sentinel v1.1.1 h1:0ovTimlR7Ldm+wR15GgO+8C2dt7kkn+tm3PQS+Qk3Ek=
-github.com/FZambia/sentinel v1.1.1/go.mod h1:ytL1Am/RLlAoAXG6Kj5LNuw/TRRQrv2rt2FT26vP5gI=
github.com/HdrHistogram/hdrhistogram-go v1.1.1 h1:cJXY5VLMHgejurPjZH6Fo9rIwRGLefBGdiaENZALqrg=
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
@@ -231,9 +229,6 @@ github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
-github.com/gomodule/redigo v1.8.8/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE=
-github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0=
-github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.1.1-0.20171103154506-982329095285/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
@@ -392,8 +387,6 @@ github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwa
github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY=
github.com/prometheus/prometheus v0.46.0 h1:9JSdXnsuT6YsbODEhSQMwxNkGwPExfmzqG73vCMk/Kw=
github.com/prometheus/prometheus v0.46.0/go.mod h1:10L5IJE5CEsjee1FnOcVswYXlPIscDWWt3IJ2UDYrz4=
-github.com/rafaeljusto/redigomock/v3 v3.1.2 h1:B4Y0XJQiPjpwYmkH55aratKX1VfR+JRqzmDKyZbC99o=
-github.com/rafaeljusto/redigomock/v3 v3.1.2/go.mod h1:F9zPqz8rMriScZkPtUiLJoLruYcpGo/XXREpeyasREM=
github.com/redis/go-redis/v9 v9.2.1 h1:WlYJg71ODF0dVspZZCpYmoF1+U1Jjk9Rwd7pq6QmlCg=
github.com/redis/go-redis/v9 v9.2.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
diff --git a/workhorse/internal/goredis/goredis.go b/workhorse/internal/goredis/goredis.go
deleted file mode 100644
index 13a9d4cc34f..00000000000
--- a/workhorse/internal/goredis/goredis.go
+++ /dev/null
@@ -1,186 +0,0 @@
-package goredis
-
-import (
- "context"
- "errors"
- "fmt"
- "net"
- "time"
-
- redis "github.com/redis/go-redis/v9"
-
- "gitlab.com/gitlab-org/gitlab/workhorse/internal/config"
- _ "gitlab.com/gitlab-org/gitlab/workhorse/internal/helper"
- internalredis "gitlab.com/gitlab-org/gitlab/workhorse/internal/redis"
-)
-
-var (
- rdb *redis.Client
- // found in https://github.com/redis/go-redis/blob/c7399b6a17d7d3e2a57654528af91349f2468529/sentinel.go#L626
- errSentinelMasterAddr error = errors.New("redis: all sentinels specified in configuration are unreachable")
-)
-
-const (
- // Max Idle Connections in the pool.
- defaultMaxIdle = 1
- // Max Active Connections in the pool.
- defaultMaxActive = 1
- // Timeout for Read operations on the pool. 1 second is technically overkill,
- // it's just for sanity.
- defaultReadTimeout = 1 * time.Second
- // Timeout for Write operations on the pool. 1 second is technically overkill,
- // it's just for sanity.
- defaultWriteTimeout = 1 * time.Second
- // Timeout before killing Idle connections in the pool. 3 minutes seemed good.
- // If you _actually_ hit this timeout often, you should consider turning of
- // redis-support since it's not necessary at that point...
- defaultIdleTimeout = 3 * time.Minute
-)
-
-// createDialer references https://github.com/redis/go-redis/blob/b1103e3d436b6fe98813ecbbe1f99dc8d59b06c9/options.go#L214
-// it intercepts the error and tracks it via a Prometheus counter
-func createDialer(sentinels []string) func(ctx context.Context, network, addr string) (net.Conn, error) {
- return func(ctx context.Context, network, addr string) (net.Conn, error) {
- var isSentinel bool
- for _, sentinelAddr := range sentinels {
- if sentinelAddr == addr {
- isSentinel = true
- break
- }
- }
-
- dialTimeout := 5 * time.Second // go-redis default
- destination := "redis"
- if isSentinel {
- // This timeout is recommended for Sentinel-support according to the guidelines.
- // https://redis.io/topics/sentinel-clients#redis-service-discovery-via-sentinel
- // For every address it should try to connect to the Sentinel,
- // using a short timeout (in the order of a few hundreds of milliseconds).
- destination = "sentinel"
- dialTimeout = 500 * time.Millisecond
- }
-
- netDialer := &net.Dialer{
- Timeout: dialTimeout,
- KeepAlive: 5 * time.Minute,
- }
-
- conn, err := netDialer.DialContext(ctx, network, addr)
- if err != nil {
- internalredis.ErrorCounter.WithLabelValues("dial", destination).Inc()
- } else {
- if !isSentinel {
- internalredis.TotalConnections.Inc()
- }
- }
-
- return conn, err
- }
-}
-
-// implements the redis.Hook interface for instrumentation
-type sentinelInstrumentationHook struct{}
-
-func (s sentinelInstrumentationHook) DialHook(next redis.DialHook) redis.DialHook {
- return func(ctx context.Context, network, addr string) (net.Conn, error) {
- conn, err := next(ctx, network, addr)
- if err != nil && err.Error() == errSentinelMasterAddr.Error() {
- // check for non-dialer error
- internalredis.ErrorCounter.WithLabelValues("master", "sentinel").Inc()
- }
- return conn, err
- }
-}
-
-func (s sentinelInstrumentationHook) ProcessHook(next redis.ProcessHook) redis.ProcessHook {
- return func(ctx context.Context, cmd redis.Cmder) error {
- return next(ctx, cmd)
- }
-}
-
-func (s sentinelInstrumentationHook) ProcessPipelineHook(next redis.ProcessPipelineHook) redis.ProcessPipelineHook {
- return func(ctx context.Context, cmds []redis.Cmder) error {
- return next(ctx, cmds)
- }
-}
-
-func GetRedisClient() *redis.Client {
- return rdb
-}
-
-// Configure redis-connection
-func Configure(cfg *config.RedisConfig) error {
- if cfg == nil {
- return nil
- }
-
- var err error
-
- if len(cfg.Sentinel) > 0 {
- rdb = configureSentinel(cfg)
- } else {
- rdb, err = configureRedis(cfg)
- }
-
- return err
-}
-
-func configureRedis(cfg *config.RedisConfig) (*redis.Client, error) {
- if cfg.URL.Scheme == "tcp" {
- cfg.URL.Scheme = "redis"
- }
-
- opt, err := redis.ParseURL(cfg.URL.String())
- if err != nil {
- return nil, err
- }
-
- opt.DB = getOrDefault(cfg.DB, 0)
- opt.Password = cfg.Password
-
- opt.PoolSize = getOrDefault(cfg.MaxActive, defaultMaxActive)
- opt.MaxIdleConns = getOrDefault(cfg.MaxIdle, defaultMaxIdle)
- opt.ConnMaxIdleTime = defaultIdleTimeout
- opt.ReadTimeout = defaultReadTimeout
- opt.WriteTimeout = defaultWriteTimeout
-
- opt.Dialer = createDialer([]string{})
-
- return redis.NewClient(opt), nil
-}
-
-func configureSentinel(cfg *config.RedisConfig) *redis.Client {
- sentinels := make([]string, len(cfg.Sentinel))
- for i := range cfg.Sentinel {
- sentinelDetails := cfg.Sentinel[i]
- sentinels[i] = fmt.Sprintf("%s:%s", sentinelDetails.Hostname(), sentinelDetails.Port())
- }
-
- client := redis.NewFailoverClient(&redis.FailoverOptions{
- MasterName: cfg.SentinelMaster,
- SentinelAddrs: sentinels,
- Password: cfg.Password,
- SentinelPassword: cfg.SentinelPassword,
- DB: getOrDefault(cfg.DB, 0),
-
- PoolSize: getOrDefault(cfg.MaxActive, defaultMaxActive),
- MaxIdleConns: getOrDefault(cfg.MaxIdle, defaultMaxIdle),
- ConnMaxIdleTime: defaultIdleTimeout,
-
- ReadTimeout: defaultReadTimeout,
- WriteTimeout: defaultWriteTimeout,
-
- Dialer: createDialer(sentinels),
- })
-
- client.AddHook(sentinelInstrumentationHook{})
-
- return client
-}
-
-func getOrDefault(ptr *int, val int) int {
- if ptr != nil {
- return *ptr
- }
- return val
-}
diff --git a/workhorse/internal/goredis/goredis_test.go b/workhorse/internal/goredis/goredis_test.go
deleted file mode 100644
index 6b281229ea4..00000000000
--- a/workhorse/internal/goredis/goredis_test.go
+++ /dev/null
@@ -1,107 +0,0 @@
-package goredis
-
-import (
- "context"
- "net"
- "sync/atomic"
- "testing"
-
- "github.com/stretchr/testify/require"
-
- "gitlab.com/gitlab-org/gitlab/workhorse/internal/config"
- "gitlab.com/gitlab-org/gitlab/workhorse/internal/helper"
-)
-
-func mockRedisServer(t *testing.T, connectReceived *atomic.Value) string {
- ln, err := net.Listen("tcp", "127.0.0.1:0")
-
- require.Nil(t, err)
-
- go func() {
- defer ln.Close()
- conn, err := ln.Accept()
- require.Nil(t, err)
- connectReceived.Store(true)
- conn.Write([]byte("OK\n"))
- }()
-
- return ln.Addr().String()
-}
-
-func TestConfigureNoConfig(t *testing.T) {
- rdb = nil
- Configure(nil)
- require.Nil(t, rdb, "rdb client should be nil")
-}
-
-func TestConfigureValidConfigX(t *testing.T) {
- testCases := []struct {
- scheme string
- }{
- {
- scheme: "redis",
- },
- {
- scheme: "tcp",
- },
- }
-
- for _, tc := range testCases {
- t.Run(tc.scheme, func(t *testing.T) {
- connectReceived := atomic.Value{}
- a := mockRedisServer(t, &connectReceived)
-
- parsedURL := helper.URLMustParse(tc.scheme + "://" + a)
- cfg := &config.RedisConfig{URL: config.TomlURL{URL: *parsedURL}}
-
- Configure(cfg)
-
- require.NotNil(t, GetRedisClient().Conn(), "Pool should not be nil")
-
- // goredis initialise connections lazily
- rdb.Ping(context.Background())
- require.True(t, connectReceived.Load().(bool))
-
- rdb = nil
- })
- }
-}
-
-func TestConnectToSentinel(t *testing.T) {
- testCases := []struct {
- scheme string
- }{
- {
- scheme: "redis",
- },
- {
- scheme: "tcp",
- },
- }
-
- for _, tc := range testCases {
- t.Run(tc.scheme, func(t *testing.T) {
- connectReceived := atomic.Value{}
- a := mockRedisServer(t, &connectReceived)
-
- addrs := []string{tc.scheme + "://" + a}
- var sentinelUrls []config.TomlURL
-
- for _, a := range addrs {
- parsedURL := helper.URLMustParse(a)
- sentinelUrls = append(sentinelUrls, config.TomlURL{URL: *parsedURL})
- }
-
- cfg := &config.RedisConfig{Sentinel: sentinelUrls}
- Configure(cfg)
-
- require.NotNil(t, GetRedisClient().Conn(), "Pool should not be nil")
-
- // goredis initialise connections lazily
- rdb.Ping(context.Background())
- require.True(t, connectReceived.Load().(bool))
-
- rdb = nil
- })
- }
-}
diff --git a/workhorse/internal/goredis/keywatcher.go b/workhorse/internal/goredis/keywatcher.go
deleted file mode 100644
index 741bfb17652..00000000000
--- a/workhorse/internal/goredis/keywatcher.go
+++ /dev/null
@@ -1,236 +0,0 @@
-package goredis
-
-import (
- "context"
- "errors"
- "fmt"
- "strings"
- "sync"
- "time"
-
- "github.com/jpillora/backoff"
- "github.com/redis/go-redis/v9"
-
- "gitlab.com/gitlab-org/gitlab/workhorse/internal/log"
- internalredis "gitlab.com/gitlab-org/gitlab/workhorse/internal/redis"
-)
-
-type KeyWatcher struct {
- mu sync.Mutex
- subscribers map[string][]chan string
- shutdown chan struct{}
- reconnectBackoff backoff.Backoff
- redisConn *redis.Client
- conn *redis.PubSub
-}
-
-func NewKeyWatcher() *KeyWatcher {
- return &KeyWatcher{
- shutdown: make(chan struct{}),
- reconnectBackoff: backoff.Backoff{
- Min: 100 * time.Millisecond,
- Max: 60 * time.Second,
- Factor: 2,
- Jitter: true,
- },
- }
-}
-
-const channelPrefix = "workhorse:notifications:"
-
-func countAction(action string) { internalredis.TotalActions.WithLabelValues(action).Add(1) }
-
-func (kw *KeyWatcher) receivePubSubStream(ctx context.Context, pubsub *redis.PubSub) error {
- kw.mu.Lock()
- // We must share kw.conn with the goroutines that call SUBSCRIBE and
- // UNSUBSCRIBE because Redis pubsub subscriptions are tied to the
- // connection.
- kw.conn = pubsub
- kw.mu.Unlock()
-
- defer func() {
- kw.mu.Lock()
- defer kw.mu.Unlock()
- kw.conn.Close()
- kw.conn = nil
-
- // Reset kw.subscribers because it is tied to Redis server side state of
- // kw.conn and we just closed that connection.
- for _, chans := range kw.subscribers {
- for _, ch := range chans {
- close(ch)
- internalredis.KeyWatchers.Dec()
- }
- }
- kw.subscribers = nil
- }()
-
- for {
- msg, err := kw.conn.Receive(ctx)
- if err != nil {
- log.WithError(fmt.Errorf("keywatcher: pubsub receive: %v", err)).Error()
- return nil
- }
-
- switch msg := msg.(type) {
- case *redis.Subscription:
- internalredis.RedisSubscriptions.Set(float64(msg.Count))
- case *redis.Pong:
- // Ignore.
- case *redis.Message:
- internalredis.TotalMessages.Inc()
- internalredis.ReceivedBytes.Add(float64(len(msg.Payload)))
- if strings.HasPrefix(msg.Channel, channelPrefix) {
- kw.notifySubscribers(msg.Channel[len(channelPrefix):], string(msg.Payload))
- }
- default:
- log.WithError(fmt.Errorf("keywatcher: unknown: %T", msg)).Error()
- return nil
- }
- }
-}
-
-func (kw *KeyWatcher) Process(client *redis.Client) {
- log.Info("keywatcher: starting process loop")
-
- ctx := context.Background() // lint:allow context.Background
- kw.mu.Lock()
- kw.redisConn = client
- kw.mu.Unlock()
-
- for {
- pubsub := client.Subscribe(ctx, []string{}...)
- if err := pubsub.Ping(ctx); err != nil {
- log.WithError(fmt.Errorf("keywatcher: %v", err)).Error()
- time.Sleep(kw.reconnectBackoff.Duration())
- continue
- }
-
- kw.reconnectBackoff.Reset()
-
- if err := kw.receivePubSubStream(ctx, pubsub); err != nil {
- log.WithError(fmt.Errorf("keywatcher: receivePubSubStream: %v", err)).Error()
- }
- }
-}
-
-func (kw *KeyWatcher) Shutdown() {
- log.Info("keywatcher: shutting down")
-
- kw.mu.Lock()
- defer kw.mu.Unlock()
-
- select {
- case <-kw.shutdown:
- // already closed
- default:
- close(kw.shutdown)
- }
-}
-
-func (kw *KeyWatcher) notifySubscribers(key, value string) {
- kw.mu.Lock()
- defer kw.mu.Unlock()
-
- chanList, ok := kw.subscribers[key]
- if !ok {
- countAction("drop-message")
- return
- }
-
- countAction("deliver-message")
- for _, c := range chanList {
- select {
- case c <- value:
- default:
- }
- }
-}
-
-func (kw *KeyWatcher) addSubscription(ctx context.Context, key string, notify chan string) error {
- kw.mu.Lock()
- defer kw.mu.Unlock()
-
- if kw.conn == nil {
- // This can happen because CI long polling is disabled in this Workhorse
- // process. It can also be that we are waiting for the pubsub connection
- // to be established. Either way it is OK to fail fast.
- return errors.New("no redis connection")
- }
-
- if len(kw.subscribers[key]) == 0 {
- countAction("create-subscription")
- if err := kw.conn.Subscribe(ctx, channelPrefix+key); err != nil {
- return err
- }
- }
-
- if kw.subscribers == nil {
- kw.subscribers = make(map[string][]chan string)
- }
- kw.subscribers[key] = append(kw.subscribers[key], notify)
- internalredis.KeyWatchers.Inc()
-
- return nil
-}
-
-func (kw *KeyWatcher) delSubscription(ctx context.Context, key string, notify chan string) {
- kw.mu.Lock()
- defer kw.mu.Unlock()
-
- chans, ok := kw.subscribers[key]
- if !ok {
- // This can happen if the pubsub connection dropped while we were
- // waiting.
- return
- }
-
- for i, c := range chans {
- if notify == c {
- kw.subscribers[key] = append(chans[:i], chans[i+1:]...)
- internalredis.KeyWatchers.Dec()
- break
- }
- }
- if len(kw.subscribers[key]) == 0 {
- delete(kw.subscribers, key)
- countAction("delete-subscription")
- if kw.conn != nil {
- kw.conn.Unsubscribe(ctx, channelPrefix+key)
- }
- }
-}
-
-func (kw *KeyWatcher) WatchKey(ctx context.Context, key, value string, timeout time.Duration) (internalredis.WatchKeyStatus, error) {
- notify := make(chan string, 1)
- if err := kw.addSubscription(ctx, key, notify); err != nil {
- return internalredis.WatchKeyStatusNoChange, err
- }
- defer kw.delSubscription(ctx, key, notify)
-
- currentValue, err := kw.redisConn.Get(ctx, key).Result()
- if errors.Is(err, redis.Nil) {
- currentValue = ""
- } else if err != nil {
- return internalredis.WatchKeyStatusNoChange, fmt.Errorf("keywatcher: redis GET: %v", err)
- }
- if currentValue != value {
- return internalredis.WatchKeyStatusAlreadyChanged, nil
- }
-
- select {
- case <-kw.shutdown:
- log.WithFields(log.Fields{"key": key}).Info("stopping watch due to shutdown")
- return internalredis.WatchKeyStatusNoChange, nil
- case currentValue := <-notify:
- if currentValue == "" {
- return internalredis.WatchKeyStatusNoChange, fmt.Errorf("keywatcher: redis GET failed")
- }
- if currentValue == value {
- return internalredis.WatchKeyStatusNoChange, nil
- }
- return internalredis.WatchKeyStatusSeenChange, nil
- case <-time.After(timeout):
- return internalredis.WatchKeyStatusTimeout, nil
- }
-}
diff --git a/workhorse/internal/goredis/keywatcher_test.go b/workhorse/internal/goredis/keywatcher_test.go
deleted file mode 100644
index b64262dc9c8..00000000000
--- a/workhorse/internal/goredis/keywatcher_test.go
+++ /dev/null
@@ -1,301 +0,0 @@
-package goredis
-
-import (
- "context"
- "os"
- "sync"
- "testing"
- "time"
-
- "github.com/stretchr/testify/require"
-
- "gitlab.com/gitlab-org/gitlab/workhorse/internal/config"
- "gitlab.com/gitlab-org/gitlab/workhorse/internal/redis"
-)
-
-var ctx = context.Background()
-
-const (
- runnerKey = "runner:build_queue:10"
-)
-
-func initRdb() {
- buf, _ := os.ReadFile("../../config.toml")
- cfg, _ := config.LoadConfig(string(buf))
- Configure(cfg.Redis)
-}
-
-func (kw *KeyWatcher) countSubscribers(key string) int {
- kw.mu.Lock()
- defer kw.mu.Unlock()
- return len(kw.subscribers[key])
-}
-
-// Forces a run of the `Process` loop against a mock PubSubConn.
-func (kw *KeyWatcher) processMessages(t *testing.T, numWatchers int, value string, ready chan<- struct{}, wg *sync.WaitGroup) {
- kw.mu.Lock()
- kw.redisConn = rdb
- psc := kw.redisConn.Subscribe(ctx, []string{}...)
- kw.mu.Unlock()
-
- errC := make(chan error)
- go func() { errC <- kw.receivePubSubStream(ctx, psc) }()
-
- require.Eventually(t, func() bool {
- kw.mu.Lock()
- defer kw.mu.Unlock()
- return kw.conn != nil
- }, time.Second, time.Millisecond)
- close(ready)
-
- require.Eventually(t, func() bool {
- return kw.countSubscribers(runnerKey) == numWatchers
- }, time.Second, time.Millisecond)
-
- // send message after listeners are ready
- kw.redisConn.Publish(ctx, channelPrefix+runnerKey, value)
-
- // close subscription after all workers are done
- wg.Wait()
- kw.mu.Lock()
- kw.conn.Close()
- kw.mu.Unlock()
-
- require.NoError(t, <-errC)
-}
-
-type keyChangeTestCase struct {
- desc string
- returnValue string
- isKeyMissing bool
- watchValue string
- processedValue string
- expectedStatus redis.WatchKeyStatus
- timeout time.Duration
-}
-
-func TestKeyChangesInstantReturn(t *testing.T) {
- initRdb()
-
- testCases := []keyChangeTestCase{
- // WatchKeyStatusAlreadyChanged
- {
- desc: "sees change with key existing and changed",
- returnValue: "somethingelse",
- watchValue: "something",
- expectedStatus: redis.WatchKeyStatusAlreadyChanged,
- timeout: time.Second,
- },
- {
- desc: "sees change with key non-existing",
- isKeyMissing: true,
- watchValue: "something",
- processedValue: "somethingelse",
- expectedStatus: redis.WatchKeyStatusAlreadyChanged,
- timeout: time.Second,
- },
- // WatchKeyStatusTimeout
- {
- desc: "sees timeout with key existing and unchanged",
- returnValue: "something",
- watchValue: "something",
- expectedStatus: redis.WatchKeyStatusTimeout,
- timeout: time.Millisecond,
- },
- {
- desc: "sees timeout with key non-existing and unchanged",
- isKeyMissing: true,
- watchValue: "",
- expectedStatus: redis.WatchKeyStatusTimeout,
- timeout: time.Millisecond,
- },
- }
-
- for _, tc := range testCases {
- t.Run(tc.desc, func(t *testing.T) {
-
- // setup
- if !tc.isKeyMissing {
- rdb.Set(ctx, runnerKey, tc.returnValue, 0)
- }
-
- defer func() {
- rdb.FlushDB(ctx)
- }()
-
- kw := NewKeyWatcher()
- defer kw.Shutdown()
- kw.redisConn = rdb
- kw.conn = kw.redisConn.Subscribe(ctx, []string{}...)
-
- val, err := kw.WatchKey(ctx, runnerKey, tc.watchValue, tc.timeout)
-
- require.NoError(t, err, "Expected no error")
- require.Equal(t, tc.expectedStatus, val, "Expected value")
- })
- }
-}
-
-func TestKeyChangesWhenWatching(t *testing.T) {
- initRdb()
-
- testCases := []keyChangeTestCase{
- // WatchKeyStatusSeenChange
- {
- desc: "sees change with key existing",
- returnValue: "something",
- watchValue: "something",
- processedValue: "somethingelse",
- expectedStatus: redis.WatchKeyStatusSeenChange,
- },
- {
- desc: "sees change with key non-existing, when watching empty value",
- isKeyMissing: true,
- watchValue: "",
- processedValue: "something",
- expectedStatus: redis.WatchKeyStatusSeenChange,
- },
- // WatchKeyStatusNoChange
- {
- desc: "sees no change with key existing",
- returnValue: "something",
- watchValue: "something",
- processedValue: "something",
- expectedStatus: redis.WatchKeyStatusNoChange,
- },
- }
-
- for _, tc := range testCases {
- t.Run(tc.desc, func(t *testing.T) {
- if !tc.isKeyMissing {
- rdb.Set(ctx, runnerKey, tc.returnValue, 0)
- }
-
- kw := NewKeyWatcher()
- defer kw.Shutdown()
- defer func() {
- rdb.FlushDB(ctx)
- }()
-
- wg := &sync.WaitGroup{}
- wg.Add(1)
- ready := make(chan struct{})
-
- go func() {
- defer wg.Done()
- <-ready
- val, err := kw.WatchKey(ctx, runnerKey, tc.watchValue, time.Second)
-
- require.NoError(t, err, "Expected no error")
- require.Equal(t, tc.expectedStatus, val, "Expected value")
- }()
-
- kw.processMessages(t, 1, tc.processedValue, ready, wg)
- })
- }
-}
-
-func TestKeyChangesParallel(t *testing.T) {
- initRdb()
-
- testCases := []keyChangeTestCase{
- {
- desc: "massively parallel, sees change with key existing",
- returnValue: "something",
- watchValue: "something",
- processedValue: "somethingelse",
- expectedStatus: redis.WatchKeyStatusSeenChange,
- },
- {
- desc: "massively parallel, sees change with key existing, watching missing keys",
- isKeyMissing: true,
- watchValue: "",
- processedValue: "somethingelse",
- expectedStatus: redis.WatchKeyStatusSeenChange,
- },
- }
-
- for _, tc := range testCases {
- t.Run(tc.desc, func(t *testing.T) {
- runTimes := 100
-
- if !tc.isKeyMissing {
- rdb.Set(ctx, runnerKey, tc.returnValue, 0)
- }
-
- defer func() {
- rdb.FlushDB(ctx)
- }()
-
- wg := &sync.WaitGroup{}
- wg.Add(runTimes)
- ready := make(chan struct{})
-
- kw := NewKeyWatcher()
- defer kw.Shutdown()
-
- for i := 0; i < runTimes; i++ {
- go func() {
- defer wg.Done()
- <-ready
- val, err := kw.WatchKey(ctx, runnerKey, tc.watchValue, time.Second)
-
- require.NoError(t, err, "Expected no error")
- require.Equal(t, tc.expectedStatus, val, "Expected value")
- }()
- }
-
- kw.processMessages(t, runTimes, tc.processedValue, ready, wg)
- })
- }
-}
-
-func TestShutdown(t *testing.T) {
- initRdb()
-
- kw := NewKeyWatcher()
- kw.redisConn = rdb
- kw.conn = kw.redisConn.Subscribe(ctx, []string{}...)
- defer kw.Shutdown()
-
- rdb.Set(ctx, runnerKey, "something", 0)
-
- wg := &sync.WaitGroup{}
- wg.Add(2)
-
- go func() {
- defer wg.Done()
- val, err := kw.WatchKey(ctx, runnerKey, "something", 10*time.Second)
-
- require.NoError(t, err, "Expected no error")
- require.Equal(t, redis.WatchKeyStatusNoChange, val, "Expected value not to change")
- }()
-
- go func() {
- defer wg.Done()
- require.Eventually(t, func() bool { return kw.countSubscribers(runnerKey) == 1 }, 10*time.Second, time.Millisecond)
-
- kw.Shutdown()
- }()
-
- wg.Wait()
-
- require.Eventually(t, func() bool { return kw.countSubscribers(runnerKey) == 0 }, 10*time.Second, time.Millisecond)
-
- // Adding a key after the shutdown should result in an immediate response
- var val redis.WatchKeyStatus
- var err error
- done := make(chan struct{})
- go func() {
- val, err = kw.WatchKey(ctx, runnerKey, "something", 10*time.Second)
- close(done)
- }()
-
- select {
- case <-done:
- require.NoError(t, err, "Expected no error")
- require.Equal(t, redis.WatchKeyStatusNoChange, val, "Expected value not to change")
- case <-time.After(100 * time.Millisecond):
- t.Fatal("timeout waiting for WatchKey")
- }
-}
diff --git a/workhorse/internal/redis/keywatcher.go b/workhorse/internal/redis/keywatcher.go
index 8f1772a9195..ddb838121b7 100644
--- a/workhorse/internal/redis/keywatcher.go
+++ b/workhorse/internal/redis/keywatcher.go
@@ -8,10 +8,10 @@ import (
"sync"
"time"
- "github.com/gomodule/redigo/redis"
"github.com/jpillora/backoff"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
+ "github.com/redis/go-redis/v9"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/log"
)
@@ -21,7 +21,8 @@ type KeyWatcher struct {
subscribers map[string][]chan string
shutdown chan struct{}
reconnectBackoff backoff.Backoff
- conn *redis.PubSubConn
+ redisConn *redis.Client
+ conn *redis.PubSub
}
func NewKeyWatcher() *KeyWatcher {
@@ -74,12 +75,12 @@ const channelPrefix = "workhorse:notifications:"
func countAction(action string) { TotalActions.WithLabelValues(action).Add(1) }
-func (kw *KeyWatcher) receivePubSubStream(conn redis.Conn) error {
+func (kw *KeyWatcher) receivePubSubStream(ctx context.Context, pubsub *redis.PubSub) error {
kw.mu.Lock()
// We must share kw.conn with the goroutines that call SUBSCRIBE and
// UNSUBSCRIBE because Redis pubsub subscriptions are tied to the
// connection.
- kw.conn = &redis.PubSubConn{Conn: conn}
+ kw.conn = pubsub
kw.mu.Unlock()
defer func() {
@@ -100,51 +101,49 @@ func (kw *KeyWatcher) receivePubSubStream(conn redis.Conn) error {
}()
for {
- switch v := kw.conn.Receive().(type) {
- case redis.Message:
+ msg, err := kw.conn.Receive(ctx)
+ if err != nil {
+ log.WithError(fmt.Errorf("keywatcher: pubsub receive: %v", err)).Error()
+ return nil
+ }
+
+ switch msg := msg.(type) {
+ case *redis.Subscription:
+ RedisSubscriptions.Set(float64(msg.Count))
+ case *redis.Pong:
+ // Ignore.
+ case *redis.Message:
TotalMessages.Inc()
- ReceivedBytes.Add(float64(len(v.Data)))
- if strings.HasPrefix(v.Channel, channelPrefix) {
- kw.notifySubscribers(v.Channel[len(channelPrefix):], string(v.Data))
+ ReceivedBytes.Add(float64(len(msg.Payload)))
+ if strings.HasPrefix(msg.Channel, channelPrefix) {
+ kw.notifySubscribers(msg.Channel[len(channelPrefix):], string(msg.Payload))
}
- case redis.Subscription:
- RedisSubscriptions.Set(float64(v.Count))
- case error:
- log.WithError(fmt.Errorf("keywatcher: pubsub receive: %v", v)).Error()
- // Intermittent error, return nil so that it doesn't wait before reconnect
+ default:
+ log.WithError(fmt.Errorf("keywatcher: unknown: %T", msg)).Error()
return nil
}
}
}
-func dialPubSub(dialer redisDialerFunc) (redis.Conn, error) {
- conn, err := dialer()
- if err != nil {
- return nil, err
- }
-
- // Make sure Redis is actually connected
- conn.Do("PING")
- if err := conn.Err(); err != nil {
- conn.Close()
- return nil, err
- }
+func (kw *KeyWatcher) Process(client *redis.Client) {
+ log.Info("keywatcher: starting process loop")
- return conn, nil
-}
+ ctx := context.Background() // lint:allow context.Background
+ kw.mu.Lock()
+ kw.redisConn = client
+ kw.mu.Unlock()
-func (kw *KeyWatcher) Process() {
- log.Info("keywatcher: starting process loop")
for {
- conn, err := dialPubSub(workerDialFunc)
- if err != nil {
+ pubsub := client.Subscribe(ctx, []string{}...)
+ if err := pubsub.Ping(ctx); err != nil {
log.WithError(fmt.Errorf("keywatcher: %v", err)).Error()
time.Sleep(kw.reconnectBackoff.Duration())
continue
}
+
kw.reconnectBackoff.Reset()
- if err = kw.receivePubSubStream(conn); err != nil {
+ if err := kw.receivePubSubStream(ctx, pubsub); err != nil {
log.WithError(fmt.Errorf("keywatcher: receivePubSubStream: %v", err)).Error()
}
}
@@ -183,7 +182,7 @@ func (kw *KeyWatcher) notifySubscribers(key, value string) {
}
}
-func (kw *KeyWatcher) addSubscription(key string, notify chan string) error {
+func (kw *KeyWatcher) addSubscription(ctx context.Context, key string, notify chan string) error {
kw.mu.Lock()
defer kw.mu.Unlock()
@@ -196,7 +195,7 @@ func (kw *KeyWatcher) addSubscription(key string, notify chan string) error {
if len(kw.subscribers[key]) == 0 {
countAction("create-subscription")
- if err := kw.conn.Subscribe(channelPrefix + key); err != nil {
+ if err := kw.conn.Subscribe(ctx, channelPrefix+key); err != nil {
return err
}
}
@@ -210,7 +209,7 @@ func (kw *KeyWatcher) addSubscription(key string, notify chan string) error {
return nil
}
-func (kw *KeyWatcher) delSubscription(key string, notify chan string) {
+func (kw *KeyWatcher) delSubscription(ctx context.Context, key string, notify chan string) {
kw.mu.Lock()
defer kw.mu.Unlock()
@@ -232,7 +231,7 @@ func (kw *KeyWatcher) delSubscription(key string, notify chan string) {
delete(kw.subscribers, key)
countAction("delete-subscription")
if kw.conn != nil {
- kw.conn.Unsubscribe(channelPrefix + key)
+ kw.conn.Unsubscribe(ctx, channelPrefix+key)
}
}
}
@@ -252,15 +251,15 @@ const (
WatchKeyStatusNoChange
)
-func (kw *KeyWatcher) WatchKey(_ context.Context, key, value string, timeout time.Duration) (WatchKeyStatus, error) {
+func (kw *KeyWatcher) WatchKey(ctx context.Context, key, value string, timeout time.Duration) (WatchKeyStatus, error) {
notify := make(chan string, 1)
- if err := kw.addSubscription(key, notify); err != nil {
+ if err := kw.addSubscription(ctx, key, notify); err != nil {
return WatchKeyStatusNoChange, err
}
- defer kw.delSubscription(key, notify)
+ defer kw.delSubscription(ctx, key, notify)
- currentValue, err := GetString(key)
- if errors.Is(err, redis.ErrNil) {
+ currentValue, err := kw.redisConn.Get(ctx, key).Result()
+ if errors.Is(err, redis.Nil) {
currentValue = ""
} else if err != nil {
return WatchKeyStatusNoChange, fmt.Errorf("keywatcher: redis GET: %v", err)
diff --git a/workhorse/internal/redis/keywatcher_test.go b/workhorse/internal/redis/keywatcher_test.go
index 3abc1bf1107..bca4ca43a64 100644
--- a/workhorse/internal/redis/keywatcher_test.go
+++ b/workhorse/internal/redis/keywatcher_test.go
@@ -2,13 +2,14 @@ package redis
import (
"context"
+ "os"
"sync"
"testing"
"time"
- "github.com/gomodule/redigo/redis"
- "github.com/rafaeljusto/redigomock/v3"
"github.com/stretchr/testify/require"
+
+ "gitlab.com/gitlab-org/gitlab/workhorse/internal/config"
)
var ctx = context.Background()
@@ -17,27 +18,10 @@ const (
runnerKey = "runner:build_queue:10"
)
-func createSubscriptionMessage(key, data string) []interface{} {
- return []interface{}{
- []byte("message"),
- []byte(key),
- []byte(data),
- }
-}
-
-func createSubscribeMessage(key string) []interface{} {
- return []interface{}{
- []byte("subscribe"),
- []byte(key),
- []byte("1"),
- }
-}
-func createUnsubscribeMessage(key string) []interface{} {
- return []interface{}{
- []byte("unsubscribe"),
- []byte(key),
- []byte("1"),
- }
+func initRdb() {
+ buf, _ := os.ReadFile("../../config.toml")
+ cfg, _ := config.LoadConfig(string(buf))
+ Configure(cfg.Redis)
}
func (kw *KeyWatcher) countSubscribers(key string) int {
@@ -47,17 +31,14 @@ func (kw *KeyWatcher) countSubscribers(key string) int {
}
// Forces a run of the `Process` loop against a mock PubSubConn.
-func (kw *KeyWatcher) processMessages(t *testing.T, numWatchers int, value string, ready chan<- struct{}) {
- psc := redigomock.NewConn()
- psc.ReceiveWait = true
-
- channel := channelPrefix + runnerKey
- psc.Command("SUBSCRIBE", channel).Expect(createSubscribeMessage(channel))
- psc.Command("UNSUBSCRIBE", channel).Expect(createUnsubscribeMessage(channel))
- psc.AddSubscriptionMessage(createSubscriptionMessage(channel, value))
+func (kw *KeyWatcher) processMessages(t *testing.T, numWatchers int, value string, ready chan<- struct{}, wg *sync.WaitGroup) {
+ kw.mu.Lock()
+ kw.redisConn = rdb
+ psc := kw.redisConn.Subscribe(ctx, []string{}...)
+ kw.mu.Unlock()
errC := make(chan error)
- go func() { errC <- kw.receivePubSubStream(psc) }()
+ go func() { errC <- kw.receivePubSubStream(ctx, psc) }()
require.Eventually(t, func() bool {
kw.mu.Lock()
@@ -69,7 +50,15 @@ func (kw *KeyWatcher) processMessages(t *testing.T, numWatchers int, value strin
require.Eventually(t, func() bool {
return kw.countSubscribers(runnerKey) == numWatchers
}, time.Second, time.Millisecond)
- close(psc.ReceiveNow)
+
+ // send message after listeners are ready
+ kw.redisConn.Publish(ctx, channelPrefix+runnerKey, value)
+
+ // close subscription after all workers are done
+ wg.Wait()
+ kw.mu.Lock()
+ kw.conn.Close()
+ kw.mu.Unlock()
require.NoError(t, <-errC)
}
@@ -85,6 +74,8 @@ type keyChangeTestCase struct {
}
func TestKeyChangesInstantReturn(t *testing.T) {
+ initRdb()
+
testCases := []keyChangeTestCase{
// WatchKeyStatusAlreadyChanged
{
@@ -121,18 +112,20 @@ func TestKeyChangesInstantReturn(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
- conn, td := setupMockPool()
- defer td()
- if tc.isKeyMissing {
- conn.Command("GET", runnerKey).ExpectError(redis.ErrNil)
- } else {
- conn.Command("GET", runnerKey).Expect(tc.returnValue)
+ // setup
+ if !tc.isKeyMissing {
+ rdb.Set(ctx, runnerKey, tc.returnValue, 0)
}
+ defer func() {
+ rdb.FlushDB(ctx)
+ }()
+
kw := NewKeyWatcher()
defer kw.Shutdown()
- kw.conn = &redis.PubSubConn{Conn: redigomock.NewConn()}
+ kw.redisConn = rdb
+ kw.conn = kw.redisConn.Subscribe(ctx, []string{}...)
val, err := kw.WatchKey(ctx, runnerKey, tc.watchValue, tc.timeout)
@@ -143,6 +136,8 @@ func TestKeyChangesInstantReturn(t *testing.T) {
}
func TestKeyChangesWhenWatching(t *testing.T) {
+ initRdb()
+
testCases := []keyChangeTestCase{
// WatchKeyStatusSeenChange
{
@@ -171,17 +166,15 @@ func TestKeyChangesWhenWatching(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
- conn, td := setupMockPool()
- defer td()
-
- if tc.isKeyMissing {
- conn.Command("GET", runnerKey).ExpectError(redis.ErrNil)
- } else {
- conn.Command("GET", runnerKey).Expect(tc.returnValue)
+ if !tc.isKeyMissing {
+ rdb.Set(ctx, runnerKey, tc.returnValue, 0)
}
kw := NewKeyWatcher()
defer kw.Shutdown()
+ defer func() {
+ rdb.FlushDB(ctx)
+ }()
wg := &sync.WaitGroup{}
wg.Add(1)
@@ -196,13 +189,14 @@ func TestKeyChangesWhenWatching(t *testing.T) {
require.Equal(t, tc.expectedStatus, val, "Expected value")
}()
- kw.processMessages(t, 1, tc.processedValue, ready)
- wg.Wait()
+ kw.processMessages(t, 1, tc.processedValue, ready, wg)
})
}
}
func TestKeyChangesParallel(t *testing.T) {
+ initRdb()
+
testCases := []keyChangeTestCase{
{
desc: "massively parallel, sees change with key existing",
@@ -224,19 +218,14 @@ func TestKeyChangesParallel(t *testing.T) {
t.Run(tc.desc, func(t *testing.T) {
runTimes := 100
- conn, td := setupMockPool()
- defer td()
-
- getCmd := conn.Command("GET", runnerKey)
-
- for i := 0; i < runTimes; i++ {
- if tc.isKeyMissing {
- getCmd = getCmd.ExpectError(redis.ErrNil)
- } else {
- getCmd = getCmd.Expect(tc.returnValue)
- }
+ if !tc.isKeyMissing {
+ rdb.Set(ctx, runnerKey, tc.returnValue, 0)
}
+ defer func() {
+ rdb.FlushDB(ctx)
+ }()
+
wg := &sync.WaitGroup{}
wg.Add(runTimes)
ready := make(chan struct{})
@@ -255,21 +244,20 @@ func TestKeyChangesParallel(t *testing.T) {
}()
}
- kw.processMessages(t, runTimes, tc.processedValue, ready)
- wg.Wait()
+ kw.processMessages(t, runTimes, tc.processedValue, ready, wg)
})
}
}
func TestShutdown(t *testing.T) {
- conn, td := setupMockPool()
- defer td()
+ initRdb()
kw := NewKeyWatcher()
- kw.conn = &redis.PubSubConn{Conn: redigomock.NewConn()}
+ kw.redisConn = rdb
+ kw.conn = kw.redisConn.Subscribe(ctx, []string{}...)
defer kw.Shutdown()
- conn.Command("GET", runnerKey).Expect("something")
+ rdb.Set(ctx, runnerKey, "something", 0)
wg := &sync.WaitGroup{}
wg.Add(2)
diff --git a/workhorse/internal/redis/redis.go b/workhorse/internal/redis/redis.go
index c79e1e56b3a..b528255d25b 100644
--- a/workhorse/internal/redis/redis.go
+++ b/workhorse/internal/redis/redis.go
@@ -1,24 +1,39 @@
package redis
import (
+ "context"
+ "errors"
"fmt"
"net"
- "net/url"
"time"
- "github.com/FZambia/sentinel"
- "github.com/gomodule/redigo/redis"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
- "gitlab.com/gitlab-org/labkit/log"
+ redis "github.com/redis/go-redis/v9"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/config"
- "gitlab.com/gitlab-org/gitlab/workhorse/internal/helper"
+ _ "gitlab.com/gitlab-org/gitlab/workhorse/internal/helper"
)
var (
- pool *redis.Pool
- sntnl *sentinel.Sentinel
+ rdb *redis.Client
+ // found in https://github.com/redis/go-redis/blob/c7399b6a17d7d3e2a57654528af91349f2468529/sentinel.go#L626
+ errSentinelMasterAddr error = errors.New("redis: all sentinels specified in configuration are unreachable")
+
+ TotalConnections = promauto.NewCounter(
+ prometheus.CounterOpts{
+ Name: "gitlab_workhorse_redis_total_connections",
+ Help: "How many connections gitlab-workhorse has opened in total. Can be used to track Redis connection rate for this process",
+ },
+ )
+
+ ErrorCounter = promauto.NewCounterVec(
+ prometheus.CounterOpts{
+ Name: "gitlab_workhorse_redis_errors",
+ Help: "Counts different types of Redis errors encountered by workhorse, by type and destination (redis, sentinel)",
+ },
+ []string{"type", "dst"},
+ )
)
const (
@@ -36,241 +51,166 @@ const (
// If you _actually_ hit this timeout often, you should consider turning of
// redis-support since it's not necessary at that point...
defaultIdleTimeout = 3 * time.Minute
- // KeepAlivePeriod is to keep a TCP connection open for an extended period of
- // time without being killed. This is used both in the pool, and in the
- // worker-connection.
- // See https://en.wikipedia.org/wiki/Keepalive#TCP_keepalive for more
- // information.
- defaultKeepAlivePeriod = 5 * time.Minute
)
-var (
- TotalConnections = promauto.NewCounter(
- prometheus.CounterOpts{
- Name: "gitlab_workhorse_redis_total_connections",
- Help: "How many connections gitlab-workhorse has opened in total. Can be used to track Redis connection rate for this process",
- },
- )
-
- ErrorCounter = promauto.NewCounterVec(
- prometheus.CounterOpts{
- Name: "gitlab_workhorse_redis_errors",
- Help: "Counts different types of Redis errors encountered by workhorse, by type and destination (redis, sentinel)",
- },
- []string{"type", "dst"},
- )
-)
+// createDialer references https://github.com/redis/go-redis/blob/b1103e3d436b6fe98813ecbbe1f99dc8d59b06c9/options.go#L214
+// it intercepts the error and tracks it via a Prometheus counter
+func createDialer(sentinels []string) func(ctx context.Context, network, addr string) (net.Conn, error) {
+ return func(ctx context.Context, network, addr string) (net.Conn, error) {
+ var isSentinel bool
+ for _, sentinelAddr := range sentinels {
+ if sentinelAddr == addr {
+ isSentinel = true
+ break
+ }
+ }
-func sentinelConn(master string, urls []config.TomlURL) *sentinel.Sentinel {
- if len(urls) == 0 {
- return nil
- }
- var addrs []string
- for _, url := range urls {
- h := url.URL.String()
- log.WithFields(log.Fields{
- "scheme": url.URL.Scheme,
- "host": url.URL.Host,
- }).Printf("redis: using sentinel")
- addrs = append(addrs, h)
- }
- return &sentinel.Sentinel{
- Addrs: addrs,
- MasterName: master,
- Dial: func(addr string) (redis.Conn, error) {
+ dialTimeout := 5 * time.Second // go-redis default
+ destination := "redis"
+ if isSentinel {
// This timeout is recommended for Sentinel-support according to the guidelines.
// https://redis.io/topics/sentinel-clients#redis-service-discovery-via-sentinel
// For every address it should try to connect to the Sentinel,
// using a short timeout (in the order of a few hundreds of milliseconds).
- timeout := 500 * time.Millisecond
- url := helper.URLMustParse(addr)
-
- var c redis.Conn
- var err error
- options := []redis.DialOption{
- redis.DialConnectTimeout(timeout),
- redis.DialReadTimeout(timeout),
- redis.DialWriteTimeout(timeout),
- }
+ destination = "sentinel"
+ dialTimeout = 500 * time.Millisecond
+ }
- if url.Scheme == "redis" || url.Scheme == "rediss" {
- c, err = redis.DialURL(addr, options...)
- } else {
- c, err = redis.Dial("tcp", url.Host, options...)
- }
+ netDialer := &net.Dialer{
+ Timeout: dialTimeout,
+ KeepAlive: 5 * time.Minute,
+ }
- if err != nil {
- ErrorCounter.WithLabelValues("dial", "sentinel").Inc()
- return nil, err
+ conn, err := netDialer.DialContext(ctx, network, addr)
+ if err != nil {
+ ErrorCounter.WithLabelValues("dial", destination).Inc()
+ } else {
+ if !isSentinel {
+ TotalConnections.Inc()
}
- return c, nil
- },
+ }
+
+ return conn, err
}
}
-var poolDialFunc func() (redis.Conn, error)
-var workerDialFunc func() (redis.Conn, error)
+// implements the redis.Hook interface for instrumentation
+type sentinelInstrumentationHook struct{}
-func timeoutDialOptions(cfg *config.RedisConfig) []redis.DialOption {
- return []redis.DialOption{
- redis.DialReadTimeout(defaultReadTimeout),
- redis.DialWriteTimeout(defaultWriteTimeout),
+func (s sentinelInstrumentationHook) DialHook(next redis.DialHook) redis.DialHook {
+ return func(ctx context.Context, network, addr string) (net.Conn, error) {
+ conn, err := next(ctx, network, addr)
+ if err != nil && err.Error() == errSentinelMasterAddr.Error() {
+ // check for non-dialer error
+ ErrorCounter.WithLabelValues("master", "sentinel").Inc()
+ }
+ return conn, err
}
}
-func dialOptionsBuilder(cfg *config.RedisConfig, setTimeouts bool) []redis.DialOption {
- var dopts []redis.DialOption
- if setTimeouts {
- dopts = timeoutDialOptions(cfg)
+func (s sentinelInstrumentationHook) ProcessHook(next redis.ProcessHook) redis.ProcessHook {
+ return func(ctx context.Context, cmd redis.Cmder) error {
+ return next(ctx, cmd)
}
- if cfg == nil {
- return dopts
+}
+
+func (s sentinelInstrumentationHook) ProcessPipelineHook(next redis.ProcessPipelineHook) redis.ProcessPipelineHook {
+ return func(ctx context.Context, cmds []redis.Cmder) error {
+ return next(ctx, cmds)
}
- if cfg.Password != "" {
- dopts = append(dopts, redis.DialPassword(cfg.Password))
+}
+
+func GetRedisClient() *redis.Client {
+ return rdb
+}
+
+// Configure redis-connection
+func Configure(cfg *config.RedisConfig) error {
+ if cfg == nil {
+ return nil
}
- if cfg.DB != nil {
- dopts = append(dopts, redis.DialDatabase(*cfg.DB))
+
+ var err error
+
+ if len(cfg.Sentinel) > 0 {
+ rdb = configureSentinel(cfg)
+ } else {
+ rdb, err = configureRedis(cfg)
}
- return dopts
+
+ return err
}
-func keepAliveDialer(network, address string) (net.Conn, error) {
- addr, err := net.ResolveTCPAddr(network, address)
- if err != nil {
- return nil, err
+func configureRedis(cfg *config.RedisConfig) (*redis.Client, error) {
+ if cfg.URL.Scheme == "tcp" {
+ cfg.URL.Scheme = "redis"
}
- tc, err := net.DialTCP(network, nil, addr)
+
+ opt, err := redis.ParseURL(cfg.URL.String())
if err != nil {
return nil, err
}
- if err := tc.SetKeepAlive(true); err != nil {
- return nil, err
- }
- if err := tc.SetKeepAlivePeriod(defaultKeepAlivePeriod); err != nil {
- return nil, err
- }
- return tc, nil
-}
-type redisDialerFunc func() (redis.Conn, error)
+ opt.DB = getOrDefault(cfg.DB, 0)
+ opt.Password = cfg.Password
-func sentinelDialer(dopts []redis.DialOption) redisDialerFunc {
- return func() (redis.Conn, error) {
- address, err := sntnl.MasterAddr()
- if err != nil {
- ErrorCounter.WithLabelValues("master", "sentinel").Inc()
- return nil, err
- }
- dopts = append(dopts, redis.DialNetDial(keepAliveDialer))
- conn, err := redisDial("tcp", address, dopts...)
- if err != nil {
- return nil, err
- }
- if !sentinel.TestRole(conn, "master") {
- conn.Close()
- return nil, fmt.Errorf("%s is not redis master", address)
- }
- return conn, nil
- }
-}
+ opt.PoolSize = getOrDefault(cfg.MaxActive, defaultMaxActive)
+ opt.MaxIdleConns = getOrDefault(cfg.MaxIdle, defaultMaxIdle)
+ opt.ConnMaxIdleTime = defaultIdleTimeout
+ opt.ReadTimeout = defaultReadTimeout
+ opt.WriteTimeout = defaultWriteTimeout
-func defaultDialer(dopts []redis.DialOption, url url.URL) redisDialerFunc {
- return func() (redis.Conn, error) {
- if url.Scheme == "unix" {
- return redisDial(url.Scheme, url.Path, dopts...)
- }
+ opt.Dialer = createDialer([]string{})
- dopts = append(dopts, redis.DialNetDial(keepAliveDialer))
+ return redis.NewClient(opt), nil
+}
- // redis.DialURL only works with redis[s]:// URLs
- if url.Scheme == "redis" || url.Scheme == "rediss" {
- return redisURLDial(url, dopts...)
- }
+func configureSentinel(cfg *config.RedisConfig) *redis.Client {
+ sentinelPassword, sentinels := sentinelOptions(cfg)
+ client := redis.NewFailoverClient(&redis.FailoverOptions{
+ MasterName: cfg.SentinelMaster,
+ SentinelAddrs: sentinels,
+ Password: cfg.Password,
+ SentinelPassword: sentinelPassword,
+ DB: getOrDefault(cfg.DB, 0),
- return redisDial(url.Scheme, url.Host, dopts...)
- }
-}
+ PoolSize: getOrDefault(cfg.MaxActive, defaultMaxActive),
+ MaxIdleConns: getOrDefault(cfg.MaxIdle, defaultMaxIdle),
+ ConnMaxIdleTime: defaultIdleTimeout,
-func redisURLDial(url url.URL, options ...redis.DialOption) (redis.Conn, error) {
- log.WithFields(log.Fields{
- "scheme": url.Scheme,
- "address": url.Host,
- }).Printf("redis: dialing")
+ ReadTimeout: defaultReadTimeout,
+ WriteTimeout: defaultWriteTimeout,
- return redis.DialURL(url.String(), options...)
-}
+ Dialer: createDialer(sentinels),
+ })
-func redisDial(network, address string, options ...redis.DialOption) (redis.Conn, error) {
- log.WithFields(log.Fields{
- "network": network,
- "address": address,
- }).Printf("redis: dialing")
+ client.AddHook(sentinelInstrumentationHook{})
- return redis.Dial(network, address, options...)
+ return client
}
-func countDialer(dialer redisDialerFunc) redisDialerFunc {
- return func() (redis.Conn, error) {
- c, err := dialer()
- if err != nil {
- ErrorCounter.WithLabelValues("dial", "redis").Inc()
- } else {
- TotalConnections.Inc()
- }
- return c, err
- }
-}
+// sentinelOptions extracts the sentinel password and addresses in <host>:<port> format
+// the order of priority for the passwords is: SentinelPassword -> first password-in-url
+func sentinelOptions(cfg *config.RedisConfig) (string, []string) {
+ sentinels := make([]string, len(cfg.Sentinel))
+ sentinelPassword := cfg.SentinelPassword
-// DefaultDialFunc should always used. Only exception is for unit-tests.
-func DefaultDialFunc(cfg *config.RedisConfig, setReadTimeout bool) func() (redis.Conn, error) {
- dopts := dialOptionsBuilder(cfg, setReadTimeout)
- if sntnl != nil {
- return countDialer(sentinelDialer(dopts))
- }
- return countDialer(defaultDialer(dopts, cfg.URL.URL))
-}
+ for i := range cfg.Sentinel {
+ sentinelDetails := cfg.Sentinel[i]
+ sentinels[i] = fmt.Sprintf("%s:%s", sentinelDetails.Hostname(), sentinelDetails.Port())
-// Configure redis-connection
-func Configure(cfg *config.RedisConfig, dialFunc func(*config.RedisConfig, bool) func() (redis.Conn, error)) {
- if cfg == nil {
- return
- }
- maxIdle := defaultMaxIdle
- if cfg.MaxIdle != nil {
- maxIdle = *cfg.MaxIdle
- }
- maxActive := defaultMaxActive
- if cfg.MaxActive != nil {
- maxActive = *cfg.MaxActive
- }
- sntnl = sentinelConn(cfg.SentinelMaster, cfg.Sentinel)
- workerDialFunc = dialFunc(cfg, false)
- poolDialFunc = dialFunc(cfg, true)
- pool = &redis.Pool{
- MaxIdle: maxIdle, // Keep at most X hot connections
- MaxActive: maxActive, // Keep at most X live connections, 0 means unlimited
- IdleTimeout: defaultIdleTimeout, // X time until an unused connection is closed
- Dial: poolDialFunc,
- Wait: true,
+ if pw, exist := sentinelDetails.User.Password(); exist && len(sentinelPassword) == 0 {
+ // sets password using the first non-empty password
+ sentinelPassword = pw
+ }
}
-}
-// Get a connection for the Redis-pool
-func Get() redis.Conn {
- if pool != nil {
- return pool.Get()
- }
- return nil
+ return sentinelPassword, sentinels
}
-// GetString fetches the value of a key in Redis as a string
-func GetString(key string) (string, error) {
- conn := Get()
- if conn == nil {
- return "", fmt.Errorf("redis: could not get connection from pool")
+func getOrDefault(ptr *int, val int) int {
+ if ptr != nil {
+ return *ptr
}
- defer conn.Close()
-
- return redis.String(conn.Do("GET", key))
+ return val
}
diff --git a/workhorse/internal/redis/redis_test.go b/workhorse/internal/redis/redis_test.go
index 64b3a842a54..6fd6ecbae11 100644
--- a/workhorse/internal/redis/redis_test.go
+++ b/workhorse/internal/redis/redis_test.go
@@ -1,19 +1,18 @@
package redis
import (
+ "context"
"net"
+ "sync/atomic"
"testing"
- "time"
- "github.com/gomodule/redigo/redis"
- "github.com/rafaeljusto/redigomock/v3"
"github.com/stretchr/testify/require"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/config"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/helper"
)
-func mockRedisServer(t *testing.T, connectReceived *bool) string {
+func mockRedisServer(t *testing.T, connectReceived *atomic.Value) string {
ln, err := net.Listen("tcp", "127.0.0.1:0")
require.Nil(t, err)
@@ -22,146 +21,67 @@ func mockRedisServer(t *testing.T, connectReceived *bool) string {
defer ln.Close()
conn, err := ln.Accept()
require.Nil(t, err)
- *connectReceived = true
+ connectReceived.Store(true)
conn.Write([]byte("OK\n"))
}()
return ln.Addr().String()
}
-// Setup a MockPool for Redis
-//
-// Returns a teardown-function and the mock-connection
-func setupMockPool() (*redigomock.Conn, func()) {
- conn := redigomock.NewConn()
- cfg := &config.RedisConfig{URL: config.TomlURL{}}
- Configure(cfg, func(_ *config.RedisConfig, _ bool) func() (redis.Conn, error) {
- return func() (redis.Conn, error) {
- return conn, nil
- }
- })
- return conn, func() {
- pool = nil
- }
+func TestConfigureNoConfig(t *testing.T) {
+ rdb = nil
+ Configure(nil)
+ require.Nil(t, rdb, "rdb client should be nil")
}
-func TestDefaultDialFunc(t *testing.T) {
+func TestConfigureValidConfigX(t *testing.T) {
testCases := []struct {
scheme string
}{
{
- scheme: "tcp",
+ scheme: "redis",
},
{
- scheme: "redis",
+ scheme: "tcp",
},
}
for _, tc := range testCases {
t.Run(tc.scheme, func(t *testing.T) {
- connectReceived := false
+ connectReceived := atomic.Value{}
a := mockRedisServer(t, &connectReceived)
parsedURL := helper.URLMustParse(tc.scheme + "://" + a)
cfg := &config.RedisConfig{URL: config.TomlURL{URL: *parsedURL}}
- dialer := DefaultDialFunc(cfg, true)
- conn, err := dialer()
-
- require.Nil(t, err)
- conn.Receive()
-
- require.True(t, connectReceived)
- })
- }
-}
-
-func TestConfigureNoConfig(t *testing.T) {
- pool = nil
- Configure(nil, nil)
- require.Nil(t, pool, "Pool should be nil")
-}
-
-func TestConfigureMinimalConfig(t *testing.T) {
- cfg := &config.RedisConfig{URL: config.TomlURL{}, Password: ""}
- Configure(cfg, DefaultDialFunc)
+ Configure(cfg)
- require.NotNil(t, pool, "Pool should not be nil")
- require.Equal(t, 1, pool.MaxIdle)
- require.Equal(t, 1, pool.MaxActive)
- require.Equal(t, 3*time.Minute, pool.IdleTimeout)
+ require.NotNil(t, GetRedisClient().Conn(), "Pool should not be nil")
- pool = nil
-}
+ // goredis initialise connections lazily
+ rdb.Ping(context.Background())
+ require.True(t, connectReceived.Load().(bool))
-func TestConfigureFullConfig(t *testing.T) {
- i, a := 4, 10
- cfg := &config.RedisConfig{
- URL: config.TomlURL{},
- Password: "",
- MaxIdle: &i,
- MaxActive: &a,
+ rdb = nil
+ })
}
- Configure(cfg, DefaultDialFunc)
-
- require.NotNil(t, pool, "Pool should not be nil")
- require.Equal(t, i, pool.MaxIdle)
- require.Equal(t, a, pool.MaxActive)
- require.Equal(t, 3*time.Minute, pool.IdleTimeout)
-
- pool = nil
-}
-
-func TestGetConnFail(t *testing.T) {
- conn := Get()
- require.Nil(t, conn, "Expected `conn` to be nil")
-}
-
-func TestGetConnPass(t *testing.T) {
- _, teardown := setupMockPool()
- defer teardown()
- conn := Get()
- require.NotNil(t, conn, "Expected `conn` to be non-nil")
}
-func TestGetStringPass(t *testing.T) {
- conn, teardown := setupMockPool()
- defer teardown()
- conn.Command("GET", "foobar").Expect("baz")
- str, err := GetString("foobar")
-
- require.NoError(t, err, "Expected `err` to be nil")
- var value string
- require.IsType(t, value, str, "Expected value to be a string")
- require.Equal(t, "baz", str, "Expected it to be equal")
-}
-
-func TestGetStringFail(t *testing.T) {
- _, err := GetString("foobar")
- require.Error(t, err, "Expected error when not connected to redis")
-}
-
-func TestSentinelConnNoSentinel(t *testing.T) {
- s := sentinelConn("", []config.TomlURL{})
-
- require.Nil(t, s, "Sentinel without urls should return nil")
-}
-
-func TestSentinelConnDialURL(t *testing.T) {
+func TestConnectToSentinel(t *testing.T) {
testCases := []struct {
scheme string
}{
{
- scheme: "tcp",
+ scheme: "redis",
},
{
- scheme: "redis",
+ scheme: "tcp",
},
}
for _, tc := range testCases {
t.Run(tc.scheme, func(t *testing.T) {
- connectReceived := false
+ connectReceived := atomic.Value{}
a := mockRedisServer(t, &connectReceived)
addrs := []string{tc.scheme + "://" + a}
@@ -172,57 +92,71 @@ func TestSentinelConnDialURL(t *testing.T) {
sentinelUrls = append(sentinelUrls, config.TomlURL{URL: *parsedURL})
}
- s := sentinelConn("foobar", sentinelUrls)
- require.Equal(t, len(addrs), len(s.Addrs))
-
- for i := range addrs {
- require.Equal(t, addrs[i], s.Addrs[i])
- }
+ cfg := &config.RedisConfig{Sentinel: sentinelUrls}
+ Configure(cfg)
- conn, err := s.Dial(s.Addrs[0])
+ require.NotNil(t, GetRedisClient().Conn(), "Pool should not be nil")
- require.Nil(t, err)
- conn.Receive()
+ // goredis initialise connections lazily
+ rdb.Ping(context.Background())
+ require.True(t, connectReceived.Load().(bool))
- require.True(t, connectReceived)
+ rdb = nil
})
}
}
-func TestSentinelConnTwoURLs(t *testing.T) {
- addrs := []string{"tcp://10.0.0.1:12345", "tcp://10.0.0.2:12345"}
- var sentinelUrls []config.TomlURL
-
- for _, a := range addrs {
- parsedURL := helper.URLMustParse(a)
- sentinelUrls = append(sentinelUrls, config.TomlURL{URL: *parsedURL})
- }
-
- s := sentinelConn("foobar", sentinelUrls)
- require.Equal(t, len(addrs), len(s.Addrs))
-
- for i := range addrs {
- require.Equal(t, addrs[i], s.Addrs[i])
+func TestSentinelOptions(t *testing.T) {
+ testCases := []struct {
+ description string
+ inputSentinelPassword string
+ inputSentinel []string
+ password string
+ sentinels []string
+ }{
+ {
+ description: "no sentinel passwords",
+ inputSentinel: []string{"tcp://localhost:26480"},
+ sentinels: []string{"localhost:26480"},
+ },
+ {
+ description: "specific sentinel password defined",
+ inputSentinel: []string{"tcp://localhost:26480"},
+ inputSentinelPassword: "password1",
+ sentinels: []string{"localhost:26480"},
+ password: "password1",
+ },
+ {
+ description: "specific sentinel password defined in url",
+ inputSentinel: []string{"tcp://:password2@localhost:26480", "tcp://:password3@localhost:26481"},
+ sentinels: []string{"localhost:26480", "localhost:26481"},
+ password: "password2",
+ },
+ {
+ description: "passwords defined specifically and in url",
+ inputSentinel: []string{"tcp://:password2@localhost:26480", "tcp://:password3@localhost:26481"},
+ sentinels: []string{"localhost:26480", "localhost:26481"},
+ inputSentinelPassword: "password1",
+ password: "password1",
+ },
}
-}
-func TestDialOptionsBuildersPassword(t *testing.T) {
- dopts := dialOptionsBuilder(&config.RedisConfig{Password: "foo"}, false)
- require.Equal(t, 1, len(dopts))
-}
+ for _, tc := range testCases {
+ t.Run(tc.description, func(t *testing.T) {
+ sentinelUrls := make([]config.TomlURL, len(tc.inputSentinel))
-func TestDialOptionsBuildersSetTimeouts(t *testing.T) {
- dopts := dialOptionsBuilder(nil, true)
- require.Equal(t, 2, len(dopts))
-}
+ for i, str := range tc.inputSentinel {
+ parsedURL := helper.URLMustParse(str)
+ sentinelUrls[i] = config.TomlURL{URL: *parsedURL}
+ }
-func TestDialOptionsBuildersSetTimeoutsConfig(t *testing.T) {
- dopts := dialOptionsBuilder(nil, true)
- require.Equal(t, 2, len(dopts))
-}
+ outputPw, outputSentinels := sentinelOptions(&config.RedisConfig{
+ Sentinel: sentinelUrls,
+ SentinelPassword: tc.inputSentinelPassword,
+ })
-func TestDialOptionsBuildersSelectDB(t *testing.T) {
- db := 3
- dopts := dialOptionsBuilder(&config.RedisConfig{DB: &db}, false)
- require.Equal(t, 1, len(dopts))
+ require.Equal(t, tc.password, outputPw)
+ require.Equal(t, tc.sentinels, outputSentinels)
+ })
+ }
}
diff --git a/workhorse/main.go b/workhorse/main.go
index 9ba213d47d3..3043ae50a22 100644
--- a/workhorse/main.go
+++ b/workhorse/main.go
@@ -17,10 +17,8 @@ import (
"gitlab.com/gitlab-org/labkit/monitoring"
"gitlab.com/gitlab-org/labkit/tracing"
- "gitlab.com/gitlab-org/gitlab/workhorse/internal/builds"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/config"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/gitaly"
- "gitlab.com/gitlab-org/gitlab/workhorse/internal/goredis"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/queueing"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/redis"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/secret"
@@ -225,35 +223,19 @@ func run(boot bootConfig, cfg config.Config) error {
secret.SetPath(boot.secretPath)
- keyWatcher := redis.NewKeyWatcher()
+ log.Info("Using redis/go-redis")
- var watchKeyFn builds.WatchKeyHandler
- var goredisKeyWatcher *goredis.KeyWatcher
-
- if os.Getenv("GITLAB_WORKHORSE_FF_GO_REDIS_ENABLED") == "true" {
- log.Info("Using redis/go-redis")
-
- goredisKeyWatcher = goredis.NewKeyWatcher()
- if err := goredis.Configure(cfg.Redis); err != nil {
- log.WithError(err).Error("unable to configure redis client")
- }
-
- if rdb := goredis.GetRedisClient(); rdb != nil {
- go goredisKeyWatcher.Process(rdb)
- }
-
- watchKeyFn = goredisKeyWatcher.WatchKey
- } else {
- log.Info("Using gomodule/redigo")
-
- if cfg.Redis != nil {
- redis.Configure(cfg.Redis, redis.DefaultDialFunc)
- go keyWatcher.Process()
- }
+ redisKeyWatcher := redis.NewKeyWatcher()
+ if err := redis.Configure(cfg.Redis); err != nil {
+ log.WithError(err).Error("unable to configure redis client")
+ }
- watchKeyFn = keyWatcher.WatchKey
+ if rdb := redis.GetRedisClient(); rdb != nil {
+ go redisKeyWatcher.Process(rdb)
}
+ watchKeyFn := redisKeyWatcher.WatchKey
+
if err := cfg.RegisterGoCloudURLOpeners(); err != nil {
return fmt.Errorf("register cloud credentials: %v", err)
}
@@ -300,11 +282,8 @@ func run(boot bootConfig, cfg config.Config) error {
ctx, cancel := context.WithTimeout(context.Background(), cfg.ShutdownTimeout.Duration) // lint:allow context.Background
defer cancel()
- if goredisKeyWatcher != nil {
- goredisKeyWatcher.Shutdown()
- }
+ redisKeyWatcher.Shutdown()
- keyWatcher.Shutdown()
return srv.Shutdown(ctx)
}
}
diff --git a/yarn.lock b/yarn.lock
index e3251ffdf95..689d0b669a6 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1025,10 +1025,10 @@
resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-3.0.0.tgz#798622546b63847e82389e473fd67f2707d82247"
integrity sha512-hBI9tfBtuPIi885ZsZ32IMEU/5nlZH/KOVYJCOh7gyMxaVLGmLedYqFN6Ui1LXkI8JlC8IsuC0rF0btcRZKd5g==
-"@cubejs-client/core@^0.34.0":
- version "0.34.0"
- resolved "https://registry.yarnpkg.com/@cubejs-client/core/-/core-0.34.0.tgz#f02504619a77be1fb70c3faf0b2576f975c9f05d"
- integrity sha512-kzwpdPruuZrCiKRmO69G92TdXTb5mZvW2vyvdW6v71avYO10698cM5hivgeENz2bCeLkwKrc1Vx9KihZnDNr1Q==
+"@cubejs-client/core@^0.34.9":
+ version "0.34.9"
+ resolved "https://registry.yarnpkg.com/@cubejs-client/core/-/core-0.34.9.tgz#a402450b08e52ef7b87d44a6044910d94218783b"
+ integrity sha512-F1+VHnhZt3t+709PoFzaNdqKiQ1QaLAiDcU/S5/yZlohU4pYOL4I57nA/AaFqg9XTJd2/cUPCkvrxBtft3xoJA==
dependencies:
"@babel/runtime" "^7.1.2"
core-js "^3.6.5"
@@ -1038,12 +1038,12 @@
url-search-params-polyfill "^7.0.0"
uuid "^8.3.2"
-"@cubejs-client/vue@^0.34.1":
- version "0.34.1"
- resolved "https://registry.yarnpkg.com/@cubejs-client/vue/-/vue-0.34.1.tgz#7b5a34b09ecef1c3a9c8ca1307109fddb6c83c2c"
- integrity sha512-cnND/zspf4PvNWaXfxtRIoagZSaMESXe59lCvvBnzjx6GnIURKuV5++zs0KM2/QAgjWE/vkU1H1Icy5xXwMADw==
+"@cubejs-client/vue@^0.34.9":
+ version "0.34.9"
+ resolved "https://registry.yarnpkg.com/@cubejs-client/vue/-/vue-0.34.9.tgz#2a39db0099829304f9f42e6a2a19bc9be597cb8e"
+ integrity sha512-nbhaXvXdN9+NpFYdDsH+QVfDvD+sduRixoKRVbh3ImkIeMRcRAyv404+W62rQy3qhzINDivTnKNRtch1bS+xLA==
dependencies:
- "@cubejs-client/core" "^0.34.0"
+ "@cubejs-client/core" "^0.34.9"
core-js "^3.6.5"
ramda "^0.27.2"
@@ -1179,10 +1179,10 @@
resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.6.2.tgz#1816b5f6948029c5eaacb0703b850ee0cb37d8f8"
integrity sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==
-"@eslint/eslintrc@^2.1.2":
- version "2.1.2"
- resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.2.tgz#c6936b4b328c64496692f76944e755738be62396"
- integrity sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==
+"@eslint/eslintrc@^2.1.3":
+ version "2.1.3"
+ resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.3.tgz#797470a75fe0fbd5a53350ee715e85e87baff22d"
+ integrity sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==
dependencies:
ajv "^6.12.4"
debug "^4.3.2"
@@ -1194,10 +1194,10 @@
minimatch "^3.1.2"
strip-json-comments "^3.1.1"
-"@eslint/js@8.51.0":
- version "8.51.0"
- resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.51.0.tgz#6d419c240cfb2b66da37df230f7e7eef801c32fa"
- integrity sha512-HxjQ8Qn+4SI3/AFv6sOrDB+g6PpUTDwSJiQqOrnneEk8L71161srI9gjzzZvYVbzHiVg/BvcH95+cK/zfIt4pg==
+"@eslint/js@8.53.0":
+ version "8.53.0"
+ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.53.0.tgz#bea56f2ed2b5baea164348ff4d5a879f6f81f20d"
+ integrity sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w==
"@floating-ui/core@^1.2.6":
version "1.2.6"
@@ -1211,10 +1211,10 @@
dependencies:
"@floating-ui/core" "^1.2.6"
-"@gitlab/application-sdk-browser@^0.2.8":
- version "0.2.8"
- resolved "https://registry.yarnpkg.com/@gitlab/application-sdk-browser/-/application-sdk-browser-0.2.8.tgz#d4c824e44f033a4af5a11e63e18350de6b7c9228"
- integrity sha512-jVE11bWHrMHVc4B+t/QD2z1rzUP1rgSP15hHDiM48219I3uahmA5ZMeuYqldaJzimTMEEkV+bdWIA4eNiJXVFA==
+"@gitlab/application-sdk-browser@^0.2.10":
+ version "0.2.10"
+ resolved "https://registry.yarnpkg.com/@gitlab/application-sdk-browser/-/application-sdk-browser-0.2.10.tgz#d569f1d931680b16b26e5d7f151381479dde2ac4"
+ integrity sha512-4WkjaInONtHstlKkOPU8wnJK6kasFUuhc4JyJtvaGiM9qmFOgYTvUM76Ru9N0f48YsSPw1ndVi14tzSfe8mSMg==
dependencies:
"@snowplow/browser-plugin-client-hints" "^3.9.0"
"@snowplow/browser-plugin-error-tracking" "^3.9.0"
@@ -1227,10 +1227,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/at.js/-/at.js-1.5.7.tgz#1ee6f838cc4410a1d797770934df91d90df8179e"
integrity sha512-c6ySRK/Ma7lxwpIVbSAF3P+xiTLrNTGTLRx4/pHK111AdFxwgUwrYF6aVZFXvmG65jHOJHoa0eQQ21RW6rm0Rg==
-"@gitlab/cluster-client@^2.0.0":
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/@gitlab/cluster-client/-/cluster-client-2.0.0.tgz#513cd877a082f9f3d96c460a39459673b67d4522"
- integrity sha512-5FS5/fwTh0FDbsN+iYYUENRXsdjZIao69Y33w1cmQH/49RahGCPDMVknbDfLjMz9OIge+S3j6xxr0Eq1AtNx+A==
+"@gitlab/cluster-client@^2.1.0":
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/cluster-client/-/cluster-client-2.1.0.tgz#a85aba41cab930a968122c2ea1193563aa6666f0"
+ integrity sha512-6FuluixJ/79n3yamEX88T6dBtsyPJCU9llZxuT+COYfoSMO40rFCMgbfk+jBtF6VCJsdmH/1SQfub8k7Y+tKWg==
dependencies:
axios "^0.24.0"
core-js "^3.29.1"
@@ -1269,15 +1269,15 @@
stylelint-declaration-strict-value "1.9.2"
stylelint-scss "5.1.0"
-"@gitlab/svgs@3.66.0":
- version "3.66.0"
- resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.66.0.tgz#5dbe98f9811001942d78395756b9d7c588300c01"
- integrity sha512-FdkoMAprxjJJnl90GJYoCMeIpvCaYPNAnRkrlsmo7NY3Ce8fpRb/XE/ZakqULeadj82S7R1IRuTHYfWB06vVtA==
+"@gitlab/svgs@3.69.0":
+ version "3.69.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.69.0.tgz#bf76b8ffbe72a783807761a38abe8aaedcfe8c12"
+ integrity sha512-Zu8Fcjhi3Bk26jZOptcD5F4SHWC7/KuAe00NULViCeswKdoda1k19B+9oCSbsbxY7vMoFuD20kiCJdBCpxb3HA==
-"@gitlab/ui@66.33.0":
- version "66.33.0"
- resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-66.33.0.tgz#d704a0c919a857f447788b12fede268b605db88d"
- integrity sha512-beKlOILzrokwnFom7c15VMcmndlrmUuEyxywv7E0tgGzDSDy7VwCdbfNbS56CT+VRTxlwQtrvv94jNDivt4NFg==
+"@gitlab/ui@68.2.1":
+ version "68.2.1"
+ resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-68.2.1.tgz#8917f6374ff2f765ddf5577ac394a733d53422c1"
+ integrity sha512-YPcPU2/i66CdnpdZX50yCmqDyj4xo18RlkfWR2O3rAkpRrg9zC1qu+kEBR8B630ONK72FygH3t+ARCc+MYUJaA==
dependencies:
"@floating-ui/dom" "1.2.9"
bootstrap-vue "2.23.1"
@@ -1479,12 +1479,12 @@
resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.1.2.tgz#6fc464307cbe3c8ca5064549b806360d84457b04"
integrity sha512-9anpBMM9mEgZN4wr2v8wHJI2/u5TnnggewRN6OlvXTTnuVyoY19X6rOv9XTqKRw6dcGKwZsBi8n0kDE2I5i4VA==
-"@humanwhocodes/config-array@^0.11.11":
- version "0.11.11"
- resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.11.tgz#88a04c570dbbc7dd943e4712429c3df09bc32844"
- integrity sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==
+"@humanwhocodes/config-array@^0.11.13":
+ version "0.11.13"
+ resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.13.tgz#075dc9684f40a531d9b26b0822153c1e832ee297"
+ integrity sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==
dependencies:
- "@humanwhocodes/object-schema" "^1.2.1"
+ "@humanwhocodes/object-schema" "^2.0.1"
debug "^4.1.1"
minimatch "^3.0.5"
@@ -1493,10 +1493,10 @@
resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c"
integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==
-"@humanwhocodes/object-schema@^1.2.1":
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45"
- integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
+"@humanwhocodes/object-schema@^2.0.1":
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz#e5211452df060fa8522b55c7b3c0c4d1981cb044"
+ integrity sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==
"@isaacs/cliui@^8.0.2":
version "8.0.2"
@@ -1846,7 +1846,7 @@
resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.21.tgz#5de5a2385a35309427f6011992b544514d559aa1"
integrity sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==
-"@popperjs/core@^2.11.2", "@popperjs/core@^2.9.0":
+"@popperjs/core@^2.9.0":
version "2.11.5"
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.5.tgz#db5a11bf66bdab39569719555b0f76e138d7bd64"
integrity sha512-9X2obfABZuDVLCgPK9aX0a/x4jaOEweTTWE2+9sr0Qqqevj2Uv5XorvusThmc9XGYpS9yI+fhh8RTafBtGposw==
@@ -1913,15 +1913,14 @@
estree-walker "^2.0.2"
picomatch "^2.3.1"
-"@sentry-internal/tracing@7.73.0":
- version "7.73.0"
- resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.73.0.tgz#4838f31e41d23a6041ef4520519b80f788bf1cac"
- integrity sha512-ig3WL/Nqp8nRQ52P205NaypGKNfIl/G+cIqge9xPW6zfRb5kJdM1YParw9GSJ1SPjEZBkBORGAML0on5H2FILw==
+"@sentry-internal/tracing@7.79.0":
+ version "7.79.0"
+ resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.79.0.tgz#db99820e93e15bf4d990f1b270a1d1c2a69fd564"
+ integrity sha512-Mf9Bd0OrZ24h1qZpvmz9IRnfORMGYNYC1xWBBFpIR1AauEDX89x+mJwIOrUc4KKAAAwt73shrJv1QA8QOm4E3g==
dependencies:
- "@sentry/core" "7.73.0"
- "@sentry/types" "7.73.0"
- "@sentry/utils" "7.73.0"
- tslib "^2.4.1 || ^1.9.3"
+ "@sentry/core" "7.79.0"
+ "@sentry/types" "7.79.0"
+ "@sentry/utils" "7.79.0"
"@sentry/core@5.30.0":
version "5.30.0"
@@ -1934,14 +1933,13 @@
"@sentry/utils" "5.30.0"
tslib "^1.9.3"
-"@sentry/core@7.73.0":
- version "7.73.0"
- resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.73.0.tgz#1caeeec44f42c4d58c06cc05dec39e5497b65aa3"
- integrity sha512-9FEz4Gq848LOgVN2OxJGYuQqxv7cIVw69VlAzWHEm3njt8mjvlTq+7UiFsGRo84+59V2FQuHxzA7vVjl90WfSg==
+"@sentry/core@7.79.0":
+ version "7.79.0"
+ resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.79.0.tgz#08871bd686afd58125f43421d3dcb65a3b9208b0"
+ integrity sha512-9vG7SfOcJNJNiqlqg4MuHDUCaSf2ZXpv3eZYRPbBkgPGr8X1ekrSABpOK+6kBNvbtKxfWVTWbLpAA6xU+cwnVw==
dependencies:
- "@sentry/types" "7.73.0"
- "@sentry/utils" "7.73.0"
- tslib "^2.4.1 || ^1.9.3"
+ "@sentry/types" "7.79.0"
+ "@sentry/utils" "7.79.0"
"@sentry/hub@5.30.0":
version "5.30.0"
@@ -1961,24 +1959,25 @@
"@sentry/types" "5.30.0"
tslib "^1.9.3"
-"@sentry/replay@7.73.0":
- version "7.73.0"
- resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-7.73.0.tgz#4e6c522bac5c12f596ef76afe15ecb3807407669"
- integrity sha512-a8IC9SowBisLYD2IdLkXzx7gN4iVwHDJhQvLp2B8ARs1PyPjJ7gCxSMHeGrYp94V0gOXtorNYkrxvuX8ayPROA==
+"@sentry/replay@7.79.0":
+ version "7.79.0"
+ resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-7.79.0.tgz#53c658e5a51698bc32019be167427b8692e2a2b7"
+ integrity sha512-vF79NxWGYfoD0hnIkdgUQqedoMcRHHp5UAfZlxhpQzJf4TnbOjollp63AvOrfd38osSG2d3E5kTUU9xs/zKhBQ==
dependencies:
- "@sentry/core" "7.73.0"
- "@sentry/types" "7.73.0"
- "@sentry/utils" "7.73.0"
+ "@sentry-internal/tracing" "7.79.0"
+ "@sentry/core" "7.79.0"
+ "@sentry/types" "7.79.0"
+ "@sentry/utils" "7.79.0"
"@sentry/types@5.30.0":
version "5.30.0"
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-5.30.0.tgz#19709bbe12a1a0115bc790b8942917da5636f402"
integrity sha512-R8xOqlSTZ+htqrfteCWU5Nk0CDN5ApUTvrlvBuiH1DyP6czDZ4ktbZB0hAgBlVcK0U+qpD3ag3Tqqpa5Q67rPw==
-"@sentry/types@7.73.0":
- version "7.73.0"
- resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.73.0.tgz#6d811bbe413d319df0a592a672d6d72a94a8e716"
- integrity sha512-/v8++bly8jW7r4cP2wswYiiVpn7eLLcqwnfPUMeCQze4zj3F3nTRIKc9BGHzU0V+fhHa3RwRC2ksqTGq1oJMDg==
+"@sentry/types@7.79.0":
+ version "7.79.0"
+ resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.79.0.tgz#b47c53a3f8b9057aac820fe99e1154949aac934d"
+ integrity sha512-3tV32+v/DF8w7kD0p3kLWtgVTVdFL39oGY02+vz//rjWg/vzeqSE95mCYKm5pUfd6cPETX/8dunCiuTBQIkTHQ==
"@sentry/utils@5.30.0":
version "5.30.0"
@@ -1988,13 +1987,12 @@
"@sentry/types" "5.30.0"
tslib "^1.9.3"
-"@sentry/utils@7.73.0":
- version "7.73.0"
- resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.73.0.tgz#530cf023f7c395aa7708cd3824e5a45948449c10"
- integrity sha512-h3ZK/qpf4k76FhJV9uiSbvMz3V/0Ovy94C+5/9UgPMVCJXFmVsdw8n/dwANJ7LupVPfYP23xFGgebDMFlK1/2w==
+"@sentry/utils@7.79.0":
+ version "7.79.0"
+ resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.79.0.tgz#c410b6c0e3032dbc9e708177555c70bdb8d1f63b"
+ integrity sha512-tUTlb6PvfZawqBmBK9CPXflqrZDXHKWoX3fve2zLK6W0FSpIMOO4TH8PBqkHBFs0ZgF/bnv/bsM4z7uEAlAtzg==
dependencies:
- "@sentry/types" "7.73.0"
- tslib "^2.4.1 || ^1.9.3"
+ "@sentry/types" "7.79.0"
"@sinclair/typebox@^0.24.1":
version "0.24.40"
@@ -2141,181 +2139,181 @@
dom-accessibility-api "^0.5.1"
pretty-format "^26.4.2"
-"@tiptap/core@^2.0.3":
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/@tiptap/core/-/core-2.0.3.tgz#dfd55124b3e7b0482e5ccb8be46eb9c3189167e2"
- integrity sha512-jLyVIWAdjjlNzrsRhSE2lVL/7N8228/1R1QtaVU85UlMIwHFAcdzhD8FeiKkqxpTnGpaDVaTy7VNEtEgaYdCyA==
+"@tiptap/core@^2.1.12":
+ version "2.1.12"
+ resolved "https://registry.yarnpkg.com/@tiptap/core/-/core-2.1.12.tgz#904fdf147e91b5e60561c76e7563c1b5a32f54ab"
+ integrity sha512-ZGc3xrBJA9KY8kln5AYTj8y+GDrKxi7u95xIl2eccrqTY5CQeRu6HRNM1yT4mAjuSaG9jmazyjGRlQuhyxCKxQ==
-"@tiptap/extension-blockquote@^2.0.3":
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/@tiptap/extension-blockquote/-/extension-blockquote-2.0.3.tgz#3ee7aff66a2526501154ca69f3e91e153c58313c"
- integrity sha512-rkUcFv2iL6f86DBBHoa4XdKNG2StvkJ7tfY9GoMpT46k3nxOaMTqak9/qZOo79TWxMLYtXzoxtKIkmWsbbcj4A==
+"@tiptap/extension-blockquote@^2.1.12":
+ version "2.1.12"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-blockquote/-/extension-blockquote-2.1.12.tgz#97b43419606acf9bfd93b9f482a1827dcac8c3e9"
+ integrity sha512-Qb3YRlCfugx9pw7VgLTb+jY37OY4aBJeZnqHzx4QThSm13edNYjasokbX0nTwL1Up4NPTcY19JUeHt6fVaVVGg==
-"@tiptap/extension-bold@^2.0.3":
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/@tiptap/extension-bold/-/extension-bold-2.0.3.tgz#2a28816195562a39c33f50e626796d14a800784f"
- integrity sha512-OGT62fMRovSSayjehumygFWTg2Qn0IDbqyMpigg/RUAsnoOI2yBZFVrdM2gk1StyoSay7gTn2MLw97IUfr7FXg==
+"@tiptap/extension-bold@^2.1.12":
+ version "2.1.12"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-bold/-/extension-bold-2.1.12.tgz#5dbf41105fc0fbde8adbff629312187fbebc39b0"
+ integrity sha512-AZGxIxcGU1/y6V2YEbKsq6BAibL8yQrbRm6EdcBnby41vj1WziewEKswhLGmZx5IKM2r2ldxld03KlfSIlKQZg==
-"@tiptap/extension-bubble-menu@2.0.3", "@tiptap/extension-bubble-menu@^2.0.3":
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.0.3.tgz#44b3c4e35fd478c42467d8fb7dbc9532614e5b18"
- integrity sha512-lPt1ELrYCuoQrQEUukqjp9xt38EwgPUwaKHI3wwt2Rbv+C6q1gmRsK1yeO/KqCNmFxNqF2p9ZF9srOnug/RZDQ==
+"@tiptap/extension-bubble-menu@^2.1.12":
+ version "2.1.12"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.1.12.tgz#4103a21a6433e58690c8f742ece39fad78dc26eb"
+ integrity sha512-gAGi21EQ4wvLmT7klgariAc2Hf+cIjaNU2NWze3ut6Ku9gUo5ZLqj1t9SKHmNf4d5JG63O8GxpErqpA7lHlRtw==
dependencies:
tippy.js "^6.3.7"
-"@tiptap/extension-bullet-list@^2.0.3":
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/@tiptap/extension-bullet-list/-/extension-bullet-list-2.0.3.tgz#43c4c0c161d5c065f3f87e4bf54d13bd6c55b4c3"
- integrity sha512-RtaLiRvZbMTOje+FW5bn+mYogiIgNxOm065wmyLPypnTbLSeHeYkoqVSqzZeqUn+7GLnwgn1shirUe6csVE/BA==
-
-"@tiptap/extension-code-block-lowlight@2.0.3":
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/@tiptap/extension-code-block-lowlight/-/extension-code-block-lowlight-2.0.3.tgz#42cad47b048d4657cb0d554a890abd1bc7072451"
- integrity sha512-thFXcFdFyHF0/dr9sqBedjj0Vt14k3m52YVc4l65+d65wRuHp4f8suu8T2ZGRJwqLCE3NIrvwQTSHhzjIqJVxQ==
-
-"@tiptap/extension-code-block@^2.0.3":
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/@tiptap/extension-code-block/-/extension-code-block-2.0.3.tgz#4ce08b4f3c5af166d3cc00e91ba5b989f01fee63"
- integrity sha512-F4xMy18EwgpyY9f5Te7UuF7UwxRLptOtCq1p2c2DfxBvHDWhAjQqVqcW/sq/I/WuED7FwCnPLyyAasPiVPkLPw==
-
-"@tiptap/extension-code@^2.0.3":
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/@tiptap/extension-code/-/extension-code-2.0.3.tgz#74d88073faedd1fc52d6ed3de4eed8fde80ff4bf"
- integrity sha512-LsVCKVxgBtkstAr1FjxN8T3OjlC76a2X8ouoZpELMp+aXbjqyanCKzt+sjjUhE4H0yLFd4v+5v6UFoCv4EILiw==
-
-"@tiptap/extension-document@^2.0.3":
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/@tiptap/extension-document/-/extension-document-2.0.3.tgz#b58af5b4f71c0acea953a7ebe8b1d24341bfaf68"
- integrity sha512-PsYeNQQBYIU9ayz1R11Kv/kKNPFNIV8tApJ9pxelXjzcAhkjncNUazPN/dyho60mzo+WpsmS3ceTj/gK3bCtWA==
-
-"@tiptap/extension-dropcursor@^2.0.3":
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/@tiptap/extension-dropcursor/-/extension-dropcursor-2.0.3.tgz#205d02c70b200810572d0b7e264bbdb718343ad0"
- integrity sha512-McthMrfusn6PjcaynJLheZJcXto8TaIW5iVitYh8qQrDXr31MALC/5GvWuiswmQ8bAXiWPwlLDYE/OJfwtggaw==
-
-"@tiptap/extension-floating-menu@^2.0.3":
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/@tiptap/extension-floating-menu/-/extension-floating-menu-2.0.3.tgz#8d9943246aa3247442c1993f235617094fe705b5"
- integrity sha512-zN1vRGRvyK3pO2aHRmQSOTpl4UJraXYwKYM009n6WviYKUNm0LPGo+VD4OAtdzUhPXyccnlsTv2p6LIqFty6Bg==
+"@tiptap/extension-bullet-list@^2.1.12":
+ version "2.1.12"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-bullet-list/-/extension-bullet-list-2.1.12.tgz#7c905a577ce30ef2cb335870a23f9d24fd26f6aa"
+ integrity sha512-vtD8vWtNlmAZX8LYqt2yU9w3mU9rPCiHmbp4hDXJs2kBnI0Ju/qAyXFx6iJ3C3XyuMnMbJdDI9ee0spAvFz7cQ==
+
+"@tiptap/extension-code-block-lowlight@^2.1.12":
+ version "2.1.12"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-code-block-lowlight/-/extension-code-block-lowlight-2.1.12.tgz#ccbca5d0d92bee373dc8e2e2ae6c27f62f66437c"
+ integrity sha512-dtIbpI9QrWa9TzNO4v5q/zf7+d83wpy5i9PEccdJAVtRZ0yOI8JIZAWzG5ex3zAoCA0CnQFdsPSVykYSDdxtDA==
+
+"@tiptap/extension-code-block@^2.1.12":
+ version "2.1.12"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-code-block/-/extension-code-block-2.1.12.tgz#20416baef1b5fc839490a8416e97fdcbb5fdf918"
+ integrity sha512-RXtSYCVsnk8D+K80uNZShClfZjvv1EgO42JlXLVGWQdIgaNyuOv/6I/Jdf+ZzhnpsBnHufW+6TJjwP5vJPSPHA==
+
+"@tiptap/extension-code@^2.1.12":
+ version "2.1.12"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-code/-/extension-code-2.1.12.tgz#86d2eb5f63725af472c5fd858e5a9c7ccae06ef3"
+ integrity sha512-CRiRq5OTC1lFgSx6IMrECqmtb93a0ZZKujEnaRhzWliPBjLIi66va05f/P1vnV6/tHaC3yfXys6dxB5A4J8jxw==
+
+"@tiptap/extension-document@^2.1.12":
+ version "2.1.12"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-document/-/extension-document-2.1.12.tgz#e19e4716dfad60cbeb6abaf2f362fed759963529"
+ integrity sha512-0QNfAkCcFlB9O8cUNSwTSIQMV9TmoEhfEaLz/GvbjwEq4skXK3bU+OQX7Ih07waCDVXIGAZ7YAZogbvrn/WbOw==
+
+"@tiptap/extension-dropcursor@^2.1.12":
+ version "2.1.12"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-dropcursor/-/extension-dropcursor-2.1.12.tgz#9da0c275291c9d47497d3db41b4d70d96366b4ff"
+ integrity sha512-0tT/q8nL4NBCYPxr9T0Brck+RQbWuczm9nV0bnxgt0IiQXoRHutfPWdS7GA65PTuVRBS/3LOco30fbjFhkfz/A==
+
+"@tiptap/extension-floating-menu@^2.1.12":
+ version "2.1.12"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-floating-menu/-/extension-floating-menu-2.1.12.tgz#68a658b2b9bdd3a0fc1afc5165231838061a8fde"
+ integrity sha512-uo0ydCJNg6AWwLT6cMUJYVChfvw2PY9ZfvKRhh9YJlGfM02jS4RUG/bJBts6R37f+a5FsOvAVwg8EvqPlNND1A==
dependencies:
tippy.js "^6.3.7"
-"@tiptap/extension-gapcursor@^2.0.3":
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/@tiptap/extension-gapcursor/-/extension-gapcursor-2.0.3.tgz#e098b78c4a169e1630dc6531d68b7f365de59c2f"
- integrity sha512-6I9EzzsYOyyqDvDvxIK6Rv3EXB+fHKFj8ntHO8IXmeNJ6pkhOinuXVsW6Yo7TcDYoTj4D5I2MNFAW2rIkgassw==
-
-"@tiptap/extension-hard-break@^2.0.3":
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/@tiptap/extension-hard-break/-/extension-hard-break-2.0.3.tgz#aa7805d825e5244bdccc508da18c781e231b2859"
- integrity sha512-RCln6ARn16jvKTjhkcAD5KzYXYS0xRMc0/LrHeV8TKdCd4Yd0YYHe0PU4F9gAgAfPQn7Dgt4uTVJLN11ICl8sQ==
-
-"@tiptap/extension-heading@^2.0.3":
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/@tiptap/extension-heading/-/extension-heading-2.0.3.tgz#5e9e779f33f366afcf729d9f68ef49721f825e11"
- integrity sha512-f0IEv5ms6aCzL80WeZ1qLCXTkRVwbpRr1qAETjg3gG4eoJN18+lZNOJYpyZy3P92C5KwF2T3Av00eFyVLIbb8Q==
-
-"@tiptap/extension-highlight@^2.0.3":
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/@tiptap/extension-highlight/-/extension-highlight-2.0.3.tgz#4a52de6666dfe4a80b018aa43805d2d220e90219"
- integrity sha512-NrtibY8cZkIjZMQuHRrKd4php+plOvAoSo8g3uVFu275I/Ixt5HqJ53R4voCXs8W8BOBRs2HS2QX8Cjh79XhtA==
-
-"@tiptap/extension-history@^2.0.3":
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/@tiptap/extension-history/-/extension-history-2.0.3.tgz#8936c15aa46f2ddeada1c3d9abe2888d58d08c30"
- integrity sha512-00KHIcJ8kivn2ARI6NQYphv2LfllVCXViHGm0EhzDW6NQxCrriJKE3tKDcTFCu7LlC5doMpq9Z6KXdljc4oVeQ==
-
-"@tiptap/extension-horizontal-rule@^2.0.3":
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-2.0.3.tgz#5c67db2c0bf3bc14a8aab80df584bee5aa23fbeb"
- integrity sha512-SZRUSh07b/M0kJHNKnfBwBMWrZBEm/E2LrK1NbluwT3DBhE+gvwiEdBxgB32zKHNxaDEXUJwUIPNC3JSbKvPUA==
-
-"@tiptap/extension-image@^2.0.3":
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/@tiptap/extension-image/-/extension-image-2.0.3.tgz#048484b2e059d4bed78f97f08651bd57b41855a9"
- integrity sha512-hS9ZJwz0md07EHsC+o4NuuJkhCZsZn7TuRz/2CvRSj2fWFIz+40CyNAHf/2J0qNugG9ommXaemetsADeEZP9ag==
-
-"@tiptap/extension-italic@^2.0.3":
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/@tiptap/extension-italic/-/extension-italic-2.0.3.tgz#2d9d5d8ccf3c38266f745029c2ec0646c075c1fc"
- integrity sha512-cfS5sW0gu7qf4ihwnLtW/QMTBrBEXaT0sJl3RwkhjIBg/65ywJKE5Nz9ewnQHmDeT18hvMJJ1VIb4j4ze9jj9A==
-
-"@tiptap/extension-link@^2.0.3":
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/@tiptap/extension-link/-/extension-link-2.0.3.tgz#4714a4c23d04032e75b5b8364a9c532f7a385aba"
- integrity sha512-H72tXQ5rkVCkAhFaf08fbEU7EBUCK0uocsqOF+4th9sOlrhfgyJtc8Jv5EXPDpxNgG5jixSqWBo0zKXQm9s9eg==
+"@tiptap/extension-gapcursor@^2.1.12":
+ version "2.1.12"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-gapcursor/-/extension-gapcursor-2.1.12.tgz#63844c3abd1a38af915839cf0c097b6d2e5a86fe"
+ integrity sha512-zFYdZCqPgpwoB7whyuwpc8EYLYjUE5QYKb8vICvc+FraBUDM51ujYhFSgJC3rhs8EjI+8GcK8ShLbSMIn49YOQ==
+
+"@tiptap/extension-hard-break@^2.1.12":
+ version "2.1.12"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-hard-break/-/extension-hard-break-2.1.12.tgz#54d0c9996e1173594852394975a9356eec98bc9a"
+ integrity sha512-nqKcAYGEOafg9D+2cy1E4gHNGuL12LerVa0eS2SQOb+PT8vSel9OTKU1RyZldsWSQJ5rq/w4uIjmLnrSR2w6Yw==
+
+"@tiptap/extension-heading@^2.1.12":
+ version "2.1.12"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-heading/-/extension-heading-2.1.12.tgz#05ae4684d6f29ae611495ab114038e14a5d1dff6"
+ integrity sha512-MoANP3POAP68Ko9YXarfDKLM/kXtscgp6m+xRagPAghRNujVY88nK1qBMZ3JdvTVN6b/ATJhp8UdrZX96TLV2w==
+
+"@tiptap/extension-highlight@^2.1.12":
+ version "2.1.12"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-highlight/-/extension-highlight-2.1.12.tgz#184efb75238c9cbc6c18d523b735de4329f78ecc"
+ integrity sha512-buen31cYPyiiHA2i0o2i/UcjRTg/42mNDCizGr1OJwvv3AELG3qOFc4Y58WJWIvWNv+1Dr4ZxHA3GNVn0ANWyg==
+
+"@tiptap/extension-history@^2.1.12":
+ version "2.1.12"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-history/-/extension-history-2.1.12.tgz#03bcb9422e8ea2b82dc45207d1a1b0bc0241b055"
+ integrity sha512-6b7UFVkvPjq3LVoCTrYZAczt5sQrQUaoDWAieVClVZoFLfjga2Fwjcfgcie8IjdPt8YO2hG/sar/c07i9vM0Sg==
+
+"@tiptap/extension-horizontal-rule@^2.1.12":
+ version "2.1.12"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-2.1.12.tgz#2191d4ff68ed39381d65971ad8e2aa1be43e6d6b"
+ integrity sha512-RRuoK4KxrXRrZNAjJW5rpaxjiP0FJIaqpi7nFbAua2oHXgsCsG8qbW2Y0WkbIoS8AJsvLZ3fNGsQ8gpdliuq3A==
+
+"@tiptap/extension-image@^2.1.12":
+ version "2.1.12"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-image/-/extension-image-2.1.12.tgz#ab035db82f0961b1d906c4d426bf68be563fdcd3"
+ integrity sha512-VCgOTeNLuoR89WoCESLverpdZpPamOd7IprQbDIeG14sUySt7RHNgf2AEfyTYJEHij12rduvAwFzerPldVAIJg==
+
+"@tiptap/extension-italic@^2.1.12":
+ version "2.1.12"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-italic/-/extension-italic-2.1.12.tgz#e99480eb77f8b4e5444fc236add8a831d5aa2343"
+ integrity sha512-/XYrW4ZEWyqDvnXVKbgTXItpJOp2ycswk+fJ3vuexyolO6NSs0UuYC6X4f+FbHYL5VuWqVBv7EavGa+tB6sl3A==
+
+"@tiptap/extension-link@^2.1.12":
+ version "2.1.12"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-link/-/extension-link-2.1.12.tgz#a18f83a0b54342e6274ff9e5a5907ef7f15aa723"
+ integrity sha512-Sti5hhlkCqi5vzdQjU/gbmr8kb578p+u0J4kWS+SSz3BknNThEm/7Id67qdjBTOQbwuN07lHjDaabJL0hSkzGQ==
dependencies:
linkifyjs "^4.1.0"
-"@tiptap/extension-list-item@^2.0.3":
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/@tiptap/extension-list-item/-/extension-list-item-2.0.3.tgz#2bca673b1ed83fdc00cb208f4d5c57d4d44ddb22"
- integrity sha512-p7cUsk0LpM1PfdAuFE8wYBNJ3gvA0UhNGR08Lo++rt9UaCeFLSN1SXRxg97c0oa5+Ski7SrCjIJ5Ynhz0viTjQ==
-
-"@tiptap/extension-ordered-list@^2.0.3":
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/@tiptap/extension-ordered-list/-/extension-ordered-list-2.0.3.tgz#d1e5d6fc240545dbba7f7e6666bebd658fc3b4ad"
- integrity sha512-ZB3MpZh/GEy1zKgw7XDQF4FIwycZWNof1k9WbDZOI063Ch4qHZowhVttH2mTCELuyvTMM/o9a8CS7qMqQB48bw==
-
-"@tiptap/extension-paragraph@^2.0.3":
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/@tiptap/extension-paragraph/-/extension-paragraph-2.0.3.tgz#88d332158c70622d36849256f90e43ca4d226dfe"
- integrity sha512-a+tKtmj4bU3GVCH1NE8VHWnhVexxX5boTVxsHIr4yGG3UoKo1c5AO7YMaeX2W5xB5iIA+BQqOPCDPEAx34dd2A==
-
-"@tiptap/extension-strike@^2.0.3":
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/@tiptap/extension-strike/-/extension-strike-2.0.3.tgz#4ec0001db5f51f86d06da22364114f20f073d4b3"
- integrity sha512-RO4/EYe2iPD6ifDHORT8fF6O9tfdtnzxLGwZIKZXnEgtweH+MgoqevEzXYdS+54Wraq4TUQGNcsYhe49pv7Rlw==
-
-"@tiptap/extension-subscript@^2.0.3":
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/@tiptap/extension-subscript/-/extension-subscript-2.0.3.tgz#26be9609b52dcdc1ff0f0f00e9e3bc01dd464f78"
- integrity sha512-XFAEUaKxWRmTq7ePEF4aj7knelJPr2fTz0y/iSXydtS094LKwBHBzxatIZY3phrgfpDc+f51ycwarsgz27UJfg==
-
-"@tiptap/extension-superscript@^2.0.3":
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/@tiptap/extension-superscript/-/extension-superscript-2.0.3.tgz#7d57b2517a2f2e1ffb603edba5f05c6631bfe3a7"
- integrity sha512-5EBjUvkw2SXL1e8C1i0UF26/GBNHxEbiNQKw7Shy88omVa4HTY+D8KWC/j29ZW/IomUbGPlbpXp1z+1TETzmyw==
-
-"@tiptap/extension-table-cell@^2.0.3":
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/@tiptap/extension-table-cell/-/extension-table-cell-2.0.3.tgz#44369bafdad3bb5b4f96296a8e93701673079b3a"
- integrity sha512-d0vpwQfRIOhqKJdoiOJybwWhjnug3QA4Mkgccp378moDRyOer3hPKavG1Ljgz087qHrN4WfdUlMGEvasYsWE7w==
-
-"@tiptap/extension-table-header@^2.0.3":
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/@tiptap/extension-table-header/-/extension-table-header-2.0.3.tgz#bb35953da353f757202efab6e0c0d5d390a70f51"
- integrity sha512-SnGl1U6usRRS6LyAjSdhaCYLF6NWbGhjVFSmiPrjb0pOzsiVeDOiUNCyUAIYaDNnjAF2pfK6+H+uHzYPqTi+/w==
-
-"@tiptap/extension-table-row@^2.0.3":
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/@tiptap/extension-table-row/-/extension-table-row-2.0.3.tgz#fb7fd381435b06942dfbc2ba475072c25bf0a478"
- integrity sha512-tyqeXmQLNSBsYyiNsnQuJMxNbz6dYt+P5W58+h10mjbt+hERA5+alQQyP06O2DggsT3Z0LPt7QRAlNmOBe7cyQ==
-
-"@tiptap/extension-table@^2.0.3":
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/@tiptap/extension-table/-/extension-table-2.0.3.tgz#780b946ca8526ac8a4044bdec4c9c720ad8ba89a"
- integrity sha512-8swHqm8vRM1w9WzaAhLmY24gGoTozctz4KHKBjvFY/Ka0yXabT0+hoCCdkZLnXWi15H3pbHs2HnDBaTGL9bZTw==
-
-"@tiptap/extension-task-item@^2.0.3":
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/@tiptap/extension-task-item/-/extension-task-item-2.0.3.tgz#b3845b51af565dec4c2c30c73ddbbc2b9e0bd297"
- integrity sha512-13u1Q769WiSNcjFieYAMuJyWXNaY9yOdw6WFg9tQg4EZ5h6+2DaxB0qmu6I3pH+wwSn2UkCkXIirAo/k7wnzbw==
-
-"@tiptap/extension-task-list@^2.0.3":
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/@tiptap/extension-task-list/-/extension-task-list-2.0.3.tgz#7e32dd518d7bd5359faab43fb48b37e2d83f5937"
- integrity sha512-NdW0RtMF2L96qy+j946mTB5Av6Qn5L3vGVWFmJA6/JPXr9Uj/grItCmqUQKHfPBSFow7UqBY82ODblP+GQFgew==
-
-"@tiptap/extension-text@^2.0.3":
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/@tiptap/extension-text/-/extension-text-2.0.3.tgz#12b6400a31ac6d35cbaf1822600f4c425457902f"
- integrity sha512-LvzChcTCcPSMNLUjZe/A9SHXWGDHtvk73fR7CBqAeNU0MxhBPEBI03GFQ6RzW3xX0CmDmjpZoDxFMB+hDEtW1A==
-
-"@tiptap/pm@^2.0.3":
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/@tiptap/pm/-/pm-2.0.3.tgz#e8bb47df765fc1b7acd52f2800c52d7ff945c5ec"
- integrity sha512-I9dsInD89Agdm1QjFRO9dmJtU1ldVSILNPW0pEhv9wYqYVvl4HUj/JMtYNqu2jWrCHNXQcaX/WkdSdvGJtmg5g==
+"@tiptap/extension-list-item@^2.1.12":
+ version "2.1.12"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-list-item/-/extension-list-item-2.1.12.tgz#3eb28dc998490a98f14765783770b3cf6587d39e"
+ integrity sha512-Gk7hBFofAPmNQ8+uw8w5QSsZOMEGf7KQXJnx5B022YAUJTYYxO3jYVuzp34Drk9p+zNNIcXD4kc7ff5+nFOTrg==
+
+"@tiptap/extension-ordered-list@^2.1.12":
+ version "2.1.12"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-ordered-list/-/extension-ordered-list-2.1.12.tgz#f41a45bc66b4d19e379d4833f303f2e0cd6b9d60"
+ integrity sha512-tF6VGl+D2avCgn9U/2YLJ8qVmV6sPE/iEzVAFZuOSe6L0Pj7SQw4K6AO640QBob/d8VrqqJFHCb6l10amJOnXA==
+
+"@tiptap/extension-paragraph@^2.1.12":
+ version "2.1.12"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-paragraph/-/extension-paragraph-2.1.12.tgz#922447b2aa1c7184787d351ceec593a74d24ed03"
+ integrity sha512-hoH/uWPX+KKnNAZagudlsrr4Xu57nusGekkJWBcrb5MCDE91BS+DN2xifuhwXiTHxnwOMVFjluc0bPzQbkArsw==
+
+"@tiptap/extension-strike@^2.1.12":
+ version "2.1.12"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-strike/-/extension-strike-2.1.12.tgz#2b049aedf2985e9c9e3c3f1cc0b203a574c85bd8"
+ integrity sha512-HlhrzIjYUT8oCH9nYzEL2QTTn8d1ECnVhKvzAe6x41xk31PjLMHTUy8aYjeQEkWZOWZ34tiTmslV1ce6R3Dt8g==
+
+"@tiptap/extension-subscript@^2.1.12":
+ version "2.1.12"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-subscript/-/extension-subscript-2.1.12.tgz#19f4114d779775bb772eee45dcf31b5ef9363f86"
+ integrity sha512-tb1jysEvf4SIiXwEOgDTXiyrG39RVNHvn/zsGMg5wy5t9qUp9m1k7kKYTH084ktuKDAPQonCcpn3hwc+ngTFzg==
+
+"@tiptap/extension-superscript@^2.1.12":
+ version "2.1.12"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-superscript/-/extension-superscript-2.1.12.tgz#6964d9c9b7591101bdc8e7058e88a75fe164d0a9"
+ integrity sha512-ek6L+DNsrjiJieArlgTvQt1VfJ56d8V19WAPW/ciRhq88YRlTEY9nSO3QuUCSUO1nGmE5OWQpgrsiW/XZbONVw==
+
+"@tiptap/extension-table-cell@^2.1.12":
+ version "2.1.12"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-table-cell/-/extension-table-cell-2.1.12.tgz#b13938d345065a3750610c66a81ea107edbbcea7"
+ integrity sha512-hextcfVTdwX8G7s8Q/V6LW2aUhGvPgu1dfV+kVVO42AFHxG+6PIkDOUuHphGajG3Nrs129bjMDWb8jphj38dUg==
+
+"@tiptap/extension-table-header@^2.1.12":
+ version "2.1.12"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-table-header/-/extension-table-header-2.1.12.tgz#87ac2efa073a212c6114e0b137cf4afc3d75c35f"
+ integrity sha512-a4WZ5Z7gqQ/QlK8cK2d1ONYdma/J5+yH/0SNtQhkfELoS45GsLJh89OyKO0W0FnY6Mg0RoH1FsoBD+cqm0yazA==
+
+"@tiptap/extension-table-row@^2.1.12":
+ version "2.1.12"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-table-row/-/extension-table-row-2.1.12.tgz#27bee7d046b2bea4fe6bf46260e0d89305b75663"
+ integrity sha512-0kPr+zngQC1YQRcU6+Fl3CpIW/SdJhVJ5qOLpQleXrLPdjmZQd3Z1DXvOSDphYjXCowGPCxeUa++6bo7IoEMJw==
+
+"@tiptap/extension-table@^2.1.12":
+ version "2.1.12"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-table/-/extension-table-2.1.12.tgz#173cc4eac75c650b440dfcae433d3c74e78aa1bc"
+ integrity sha512-q/DuKZ4j1ycRfuFdb9rBJ3MglGNxlM2BQ1csScX/BrVIsAQI5B8sdzy1BrIlepQ6DRu4DCzHcKMI8u4/edUSWA==
+
+"@tiptap/extension-task-item@^2.1.12":
+ version "2.1.12"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-task-item/-/extension-task-item-2.1.12.tgz#944eacf6f0ed1a430d807217d62b49ccef3956e1"
+ integrity sha512-uqrDTO4JwukZUt40GQdvB6S+oDhdp4cKNPMi0sbteWziQugkSMLlkYvxU0Hfb/YeziaWWwFI7ssPu/hahyk6dQ==
+
+"@tiptap/extension-task-list@^2.1.12":
+ version "2.1.12"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-task-list/-/extension-task-list-2.1.12.tgz#adbfb5a5b990d6f189c776b45de2d2c5bb77e963"
+ integrity sha512-BUpYlEWK+Q3kw9KIiOqvhd0tUPhMcOf1+fJmCkluJok+okAxMbP1umAtCEQ3QkoCwLr+vpHJov7h3yi9+dwgeQ==
+
+"@tiptap/extension-text@^2.1.12":
+ version "2.1.12"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-text/-/extension-text-2.1.12.tgz#466e3244bdd9b2db2304c0c9a1d51ce59f5327d0"
+ integrity sha512-rCNUd505p/PXwU9Jgxo4ZJv4A3cIBAyAqlx/dtcY6cjztCQuXJhuQILPhjGhBTOLEEL4kW2wQtqzCmb7O8i2jg==
+
+"@tiptap/pm@^2.1.12":
+ version "2.1.12"
+ resolved "https://registry.yarnpkg.com/@tiptap/pm/-/pm-2.1.12.tgz#88a4b19be0eabb13d42ddd540c19ba1bbe74b322"
+ integrity sha512-Q3MXXQABG4CZBesSp82yV84uhJh/W0Gag6KPm2HRWPimSFELM09Z9/5WK9RItAYE0aLhe4Krnyiczn9AAa1tQQ==
dependencies:
prosemirror-changeset "^2.2.0"
prosemirror-collab "^1.3.0"
@@ -2336,29 +2334,25 @@
prosemirror-transform "^1.7.0"
prosemirror-view "^1.28.2"
-"@tiptap/suggestion@^2.0.3":
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/@tiptap/suggestion/-/suggestion-2.0.3.tgz#3f25e20f50de6748f2b65a88e264d9b5887ca16a"
- integrity sha512-1y3palQStGZq13UtHjouZ50k4sotM+N56cIlFeygIv3gqdai2zGPaPQtqV9FOVVQizXpUbQMTlPSDC5Ej4SPnQ==
+"@tiptap/suggestion@^2.1.12":
+ version "2.1.12"
+ resolved "https://registry.yarnpkg.com/@tiptap/suggestion/-/suggestion-2.1.12.tgz#a13782d1e625ec03b3f61b6839ecc95b6b685d3f"
+ integrity sha512-rhlLWwVkOodBGRMK0mAmE34l2a+BqM2Y7q1ViuQRBhs/6sZ8d83O4hARHKVwqT5stY4i1l7d7PoemV3uAGI6+g==
-"@tiptap/vue-2@2.0.3":
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/@tiptap/vue-2/-/vue-2-2.0.3.tgz#076778985d1e5ccbefb414b05c4c9804bb377258"
- integrity sha512-So2cl/W11Xt1MQqK47uNrddf08ruI2ScGHaBG2WZnYDtqJfwlAChRXi67fOeo/Y1vWy/69ekv5kLeQYWw9YJAg==
+"@tiptap/vue-2@^2.1.12":
+ version "2.1.12"
+ resolved "https://registry.yarnpkg.com/@tiptap/vue-2/-/vue-2-2.1.12.tgz#1858057fb3bb2925228ac8d245e8e971c2b92e4f"
+ integrity sha512-QMalZecf10kXsug76zozaZGyDKMUBP4IKj5L4PP+KNHSLMHTHgZGpjs/l8G80+pCivYb9Ww4D1yz6ZWxLbwVxw==
dependencies:
- "@tiptap/extension-bubble-menu" "^2.0.3"
- "@tiptap/extension-floating-menu" "^2.0.3"
+ "@tiptap/extension-bubble-menu" "^2.1.12"
+ "@tiptap/extension-floating-menu" "^2.1.12"
+ vue-ts-types "^1.6.0"
"@tootallnate/once@2":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf"
integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==
-"@trysound/sax@0.2.0":
- version "0.2.0"
- resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad"
- integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==
-
"@types/aria-query@^4.2.0":
version "4.2.0"
resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-4.2.0.tgz#14264692a9d6e2fa4db3df5e56e94b5e25647ac0"
@@ -2779,6 +2773,11 @@
"@typescript-eslint/types" "5.38.0"
eslint-visitor-keys "^3.3.0"
+"@ungap/structured-clone@^1.2.0":
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406"
+ integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==
+
"@vitejs/plugin-vue2@^1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue2/-/plugin-vue2-1.1.2.tgz#891f0acc5a6a2b4886a74cb8d6359d42f19f968a"
@@ -2834,7 +2833,7 @@
postcss "^8.4.14"
source-map "^0.6.1"
-"@vue/compiler-sfc@^3.2.20", "@vue/compiler-sfc@^3.2.47":
+"@vue/compiler-sfc@^3.2.47":
version "3.2.47"
resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.2.47.tgz#1bdc36f6cdc1643f72e2c397eb1a398f5004ad3d"
integrity sha512-rog05W+2IFfxjMcFw10tM9+f7i/+FFpZJJ5XHX72NP9eC2uRD+42M3pYcQqDXVYoj74kHMSEdQ/WmCjt8JFksQ==
@@ -3400,15 +3399,15 @@ array-flatten@^2.1.2:
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.2.tgz#24ef80a28c1a893617e2149b0c6d0d788293b099"
integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==
-array-includes@^3.1.6:
- version "3.1.6"
- resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.6.tgz#9e9e720e194f198266ba9e18c29e6a9b0e4b225f"
- integrity sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==
+array-includes@^3.1.7:
+ version "3.1.7"
+ resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.7.tgz#8cd2e01b26f7a3086cbc87271593fe921c62abda"
+ integrity sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==
dependencies:
call-bind "^1.0.2"
- define-properties "^1.1.4"
- es-abstract "^1.20.4"
- get-intrinsic "^1.1.3"
+ define-properties "^1.2.0"
+ es-abstract "^1.22.1"
+ get-intrinsic "^1.2.1"
is-string "^1.0.7"
array-union@^2.1.0:
@@ -3421,55 +3420,56 @@ array-unique@^0.3.2:
resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=
-array.prototype.find@^2.2.1:
- version "2.2.1"
- resolved "https://registry.yarnpkg.com/array.prototype.find/-/array.prototype.find-2.2.1.tgz#769b8182a0b535c3d76ac025abab98ba1e12467b"
- integrity sha512-I2ri5Z9uMpMvnsNrHre9l3PaX+z9D0/z6F7Yt2u15q7wt0I62g5kX6xUKR1SJiefgG+u2/gJUmM8B47XRvQR6w==
+array.prototype.find@^2.2.2:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/array.prototype.find/-/array.prototype.find-2.2.2.tgz#e862cf891e725d8f2a10e5e42d750629faaabd32"
+ integrity sha512-DRumkfW97iZGOfn+lIXbkVrXL04sfYKX+EfOodo8XboR5sxPDVvOjZTF/rysusa9lmhmSOeD6Vp6RKQP+eP4Tg==
dependencies:
call-bind "^1.0.2"
- define-properties "^1.1.4"
- es-abstract "^1.20.4"
+ define-properties "^1.2.0"
+ es-abstract "^1.22.1"
es-shim-unscopables "^1.0.0"
-array.prototype.findlastindex@^1.2.2:
- version "1.2.2"
- resolved "https://registry.yarnpkg.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.2.tgz#bc229aef98f6bd0533a2bc61ff95209875526c9b"
- integrity sha512-tb5thFFlUcp7NdNF6/MpDk/1r/4awWG1FIz3YqDf+/zJSTezBb+/5WViH41obXULHVpDzoiCLpJ/ZO9YbJMsdw==
+array.prototype.findlastindex@^1.2.3:
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.3.tgz#b37598438f97b579166940814e2c0493a4f50207"
+ integrity sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA==
dependencies:
call-bind "^1.0.2"
- define-properties "^1.1.4"
- es-abstract "^1.20.4"
+ define-properties "^1.2.0"
+ es-abstract "^1.22.1"
es-shim-unscopables "^1.0.0"
- get-intrinsic "^1.1.3"
+ get-intrinsic "^1.2.1"
-array.prototype.flat@^1.3.1:
- version "1.3.1"
- resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz#ffc6576a7ca3efc2f46a143b9d1dda9b4b3cf5e2"
- integrity sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==
+array.prototype.flat@^1.3.2:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz#1476217df8cff17d72ee8f3ba06738db5b387d18"
+ integrity sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==
dependencies:
call-bind "^1.0.2"
- define-properties "^1.1.4"
- es-abstract "^1.20.4"
+ define-properties "^1.2.0"
+ es-abstract "^1.22.1"
es-shim-unscopables "^1.0.0"
-array.prototype.flatmap@^1.3.1:
- version "1.3.1"
- resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz#1aae7903c2100433cb8261cd4ed310aab5c4a183"
- integrity sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==
+array.prototype.flatmap@^1.3.2:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz#c9a7c6831db8e719d6ce639190146c24bbd3e527"
+ integrity sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==
dependencies:
call-bind "^1.0.2"
- define-properties "^1.1.4"
- es-abstract "^1.20.4"
+ define-properties "^1.2.0"
+ es-abstract "^1.22.1"
es-shim-unscopables "^1.0.0"
-arraybuffer.prototype.slice@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.1.tgz#9b5ea3868a6eebc30273da577eb888381c0044bb"
- integrity sha512-09x0ZWFEjj4WD8PDbykUwo3t9arLn8NIzmmYEJFpYekOAQjpkGSyrQhNoRTcwwcFRu+ycWF78QZ63oWTqSjBcw==
+arraybuffer.prototype.slice@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz#98bd561953e3e74bb34938e77647179dfe6e9f12"
+ integrity sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==
dependencies:
array-buffer-byte-length "^1.0.0"
call-bind "^1.0.2"
define-properties "^1.2.0"
+ es-abstract "^1.22.1"
get-intrinsic "^1.2.1"
is-array-buffer "^3.0.2"
is-shared-array-buffer "^1.0.2"
@@ -4037,13 +4037,14 @@ cache-loader@^4.1.0:
neo-async "^2.6.1"
schema-utils "^2.0.0"
-call-bind@^1.0.0, call-bind@^1.0.2:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c"
- integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==
+call-bind@^1.0.0, call-bind@^1.0.2, call-bind@^1.0.4, call-bind@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.5.tgz#6fa2b7845ce0ea49bf4d8b9ef64727a2c2e2e513"
+ integrity sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==
dependencies:
- function-bind "^1.1.1"
- get-intrinsic "^1.0.2"
+ function-bind "^1.1.2"
+ get-intrinsic "^1.2.1"
+ set-function-length "^1.1.1"
call-me-maybe@^1.0.1:
version "1.0.1"
@@ -4076,9 +4077,9 @@ camelcase@^6.2.0, camelcase@^6.3.0:
integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==
caniuse-lite@^1.0.30001449, caniuse-lite@^1.0.30001464:
- version "1.0.30001478"
- resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001478.tgz#0ef8a1cf8b16be47a0f9fc4ecfc952232724b32a"
- integrity sha512-gMhDyXGItTHipJj2ApIvR+iVB5hd0KP3svMWWXDvZOmjzJJassGLMfxRkQCSYgGd2gtdL/ReeiyvMSFD1Ss6Mw==
+ version "1.0.30001549"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001549.tgz#7d1a3dce7ea78c06ed72c32c2743ea364b3615aa"
+ integrity sha512-qRp48dPYSCYaP+KurZLhDYdVE+yEyht/3NlmcJgVQ2VMGt6JL36ndQ/7rgspdZsJuxDPFIo/OzBT2+GmIJ53BA==
canvas-confetti@^1.4.0:
version "1.4.0"
@@ -4127,30 +4128,6 @@ character-entities@^2.0.0:
resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667"
integrity sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==
-cheerio-select@^1.4.0:
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-1.4.0.tgz#3a16f21e37a2ef0f211d6d1aa4eff054bb22cdc9"
- integrity sha512-sobR3Yqz27L553Qa7cK6rtJlMDbiKPdNywtR95Sj/YgfpLfy0u6CGJuaBKe5YE/vTc23SCRKxWSdlon/w6I/Ew==
- dependencies:
- css-select "^4.1.2"
- css-what "^5.0.0"
- domelementtype "^2.2.0"
- domhandler "^4.2.0"
- domutils "^2.6.0"
-
-cheerio@^1.0.0-rc.9:
- version "1.0.0-rc.9"
- resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.9.tgz#a3ae6b7ce7af80675302ff836f628e7cb786a67f"
- integrity sha512-QF6XVdrLONO6DXRF5iaolY+odmhj2CLj+xzNod7INPWMi/x9X4SOylH0S/vaPpX+AUU6t04s34SQNh7DbkuCng==
- dependencies:
- cheerio-select "^1.4.0"
- dom-serializer "^1.3.1"
- domhandler "^4.2.0"
- htmlparser2 "^6.1.0"
- parse5 "^6.0.1"
- parse5-htmlparser2-tree-adapter "^6.0.1"
- tslib "^2.2.0"
-
"chokidar@>=3.0.0 <4.0.0", chokidar@^2.1.8, chokidar@^3.4.1, chokidar@^3.5.2, chokidar@^3.5.3:
version "3.5.3"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
@@ -4536,10 +4513,10 @@ core-js-pure@^3.0.0:
resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.6.5.tgz#c79e75f5e38dbc85a662d91eea52b8256d53b813"
integrity sha512-lacdXOimsiD0QyNf9BC/mxivNJ/ybBGJXQFKzRekp1WTHoVUWsUHEn+2T8GJAzzIhyOuXA+gOxCVN3l+5PLPUA==
-core-js@^3.29.1, core-js@^3.32.2, core-js@^3.6.5:
- version "3.32.2"
- resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.32.2.tgz#172fb5949ef468f93b4be7841af6ab1f21992db7"
- integrity sha512-pxXSw1mYZPDGvTQqEc5vgIb83jGQKFGYWY76z4a7weZXUolw3G+OvpZqSRcfYOoOVUQJYEPsWeQK8pKEnUtWxQ==
+core-js@^3.29.1, core-js@^3.33.2, core-js@^3.6.5:
+ version "3.33.2"
+ resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.33.2.tgz#312bbf6996a3a517c04c99b9909cdd27138d1ceb"
+ integrity sha512-XeBzWI6QL3nJQiHmdzbAOiMYqjrb7hwU7A39Qhvd/POSa/t9E1AeZyEZx3fNvp/vtM8zXwhoL0FsiS0hD0pruQ==
core-util-is@~1.0.0:
version "1.0.3"
@@ -4720,17 +4697,6 @@ css-loader@^2.1.1:
postcss-value-parser "^3.3.0"
schema-utils "^1.0.0"
-css-select@^4.1.2, css-select@^4.1.3:
- version "4.3.0"
- resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.3.0.tgz#db7129b2846662fd8628cfc496abb2b59e41529b"
- integrity sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==
- dependencies:
- boolbase "^1.0.0"
- css-what "^6.0.1"
- domhandler "^4.3.1"
- domutils "^2.8.0"
- nth-check "^2.0.1"
-
css-selector-parser@^1.3:
version "1.3.0"
resolved "https://registry.yarnpkg.com/css-selector-parser/-/css-selector-parser-1.3.0.tgz#5f1ad43e2d8eefbfdc304fcd39a521664943e3eb"
@@ -4741,14 +4707,6 @@ css-shorthand-properties@^1.0.0:
resolved "https://registry.yarnpkg.com/css-shorthand-properties/-/css-shorthand-properties-1.1.1.tgz#1c808e63553c283f289f2dd56fcee8f3337bd935"
integrity sha512-Md+Juc7M3uOdbAFwOYlTrccIZ7oCFuzrhKYQjdeUEW/sE1hv17Jp/Bws+ReOPpGVBTYCBoYo+G17V5Qo8QQ75A==
-css-tree@^1.1.2, css-tree@^1.1.3:
- version "1.1.3"
- resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d"
- integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==
- dependencies:
- mdn-data "2.0.14"
- source-map "^0.6.1"
-
css-tree@^2.0.1, css-tree@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-2.3.1.tgz#10264ce1e5442e8572fc82fbe490644ff54b5c20"
@@ -4766,16 +4724,6 @@ css-values@^0.1.0:
ends-with "^0.2.0"
postcss-value-parser "^3.3.0"
-css-what@^5.0.0:
- version "5.1.0"
- resolved "https://registry.yarnpkg.com/css-what/-/css-what-5.1.0.tgz#3f7b707aadf633baf62c2ceb8579b545bb40f7fe"
- integrity sha512-arSMRWIIFY0hV8pIxZMEfmMI47Wj3R/aWpZDDxWYCPEiOMv6tfOrnpDtgxBYPEQD4V0Y/958+1TdC3iWTFcUPw==
-
-css-what@^6.0.1:
- version "6.1.0"
- resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4"
- integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==
-
cssesc@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
@@ -4786,13 +4734,6 @@ cssfontparser@^1.2.1:
resolved "https://registry.yarnpkg.com/cssfontparser/-/cssfontparser-1.2.1.tgz#f4022fc8f9700c68029d542084afbaf425a3f3e3"
integrity sha1-9AIvyPlwDGgCnVQghK+69CWj8+M=
-csso@^4.2.0:
- version "4.2.0"
- resolved "https://registry.yarnpkg.com/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529"
- integrity sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==
- dependencies:
- css-tree "^1.1.2"
-
cssom@^0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.5.0.tgz#d254fa92cd8b6fbd83811b9fbaed34663cc17c36"
@@ -5487,6 +5428,15 @@ default-gateway@^6.0.3:
dependencies:
execa "^5.0.0"
+define-data-property@^1.0.1, define-data-property@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.1.tgz#c35f7cd0ab09883480d12ac5cb213715587800b3"
+ integrity sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==
+ dependencies:
+ get-intrinsic "^1.2.1"
+ gopd "^1.0.1"
+ has-property-descriptors "^1.0.0"
+
define-lazy-prop@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f"
@@ -5649,15 +5599,6 @@ dom-event-types@^1.0.0:
resolved "https://registry.yarnpkg.com/dom-event-types/-/dom-event-types-1.0.0.tgz#5830a0a29e1bf837fe50a70cd80a597232813cae"
integrity sha512-2G2Vwi2zXTHBGqXHsJ4+ak/iP0N8Ar+G8a7LiD2oup5o4sQWytwqqrZu/O6hIMV0KMID2PL69OhpshLO0n7UJQ==
-dom-serializer@^1.0.1, dom-serializer@^1.3.1:
- version "1.3.1"
- resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.3.1.tgz#d845a1565d7c041a95e5dab62184ab41e3a519be"
- integrity sha512-Pv2ZluG5ife96udGgEDovOOOA5UELkltfJpnIExPrAk1LTvecolUGn6lIaoLh86d83GiB86CjzciMd9BuRB71Q==
- dependencies:
- domelementtype "^2.0.1"
- domhandler "^4.0.0"
- entities "^2.0.0"
-
dom-walk@^0.1.0:
version "0.1.2"
resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.2.tgz#0c548bef048f4d1f2a97249002236060daa3fd84"
@@ -5668,11 +5609,6 @@ domain-browser@^1.1.1:
resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.1.7.tgz#867aa4b093faa05f1de08c06f4d7b21fdf8698bc"
integrity sha1-hnqksJP6oF8d4IwG9NeyH9+GmLw=
-domelementtype@^2.0.1, domelementtype@^2.2.0:
- version "2.2.0"
- resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.2.0.tgz#9a0b6c2782ed6a1c7323d42267183df9bd8b1d57"
- integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==
-
domexception@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90"
@@ -5687,13 +5623,6 @@ domexception@^4.0.0:
dependencies:
webidl-conversions "^7.0.0"
-domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.3.1:
- version "4.3.1"
- resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.1.tgz#8d792033416f59d68bc03a5aa7b018c1ca89279c"
- integrity sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==
- dependencies:
- domelementtype "^2.2.0"
-
dommatrix@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/dommatrix/-/dommatrix-1.0.3.tgz#e7c18e8d6f3abdd1fef3dd4aa74c4d2e620a0525"
@@ -5704,15 +5633,6 @@ dompurify@^3.0.5, dompurify@^3.0.6:
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.0.6.tgz#925ebd576d54a9531b5d76f0a5bef32548351dae"
integrity sha512-ilkD8YEnnGh1zJ240uJsW7AzE+2qpbOUYjacomn3AvJ6J4JhKGSZ2nh4wUIXPZrEPppaCLx5jFe8T89Rk8tQ7w==
-domutils@^2.5.2, domutils@^2.6.0, domutils@^2.8.0:
- version "2.8.0"
- resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135"
- integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==
- dependencies:
- dom-serializer "^1.0.1"
- domelementtype "^2.2.0"
- domhandler "^4.2.0"
-
dropzone@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/dropzone/-/dropzone-4.2.0.tgz#fbe7acbb9918e0706489072ef663effeef8a79f3"
@@ -5856,11 +5776,6 @@ enhanced-resolve@^4.5.0:
memory-fs "^0.5.0"
tapable "^1.0.0"
-entities@^2.0.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5"
- integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==
-
entities@~3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/entities/-/entities-3.0.1.tgz#2b887ca62585e96db3903482d336c1006c3001d4"
@@ -5885,26 +5800,26 @@ error-ex@^1.3.1:
dependencies:
is-arrayish "^0.2.1"
-es-abstract@^1.19.0, es-abstract@^1.19.1, es-abstract@^1.20.4, es-abstract@^1.21.2:
- version "1.22.1"
- resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.22.1.tgz#8b4e5fc5cefd7f1660f0f8e1a52900dfbc9d9ccc"
- integrity sha512-ioRRcXMO6OFyRpyzV3kE1IIBd4WG5/kltnzdxSCqoP8CMGs/Li+M1uF5o7lOkZVFjDs+NLesthnF66Pg/0q0Lw==
+es-abstract@^1.19.1, es-abstract@^1.22.1:
+ version "1.22.3"
+ resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.22.3.tgz#48e79f5573198de6dee3589195727f4f74bc4f32"
+ integrity sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==
dependencies:
array-buffer-byte-length "^1.0.0"
- arraybuffer.prototype.slice "^1.0.1"
+ arraybuffer.prototype.slice "^1.0.2"
available-typed-arrays "^1.0.5"
- call-bind "^1.0.2"
+ call-bind "^1.0.5"
es-set-tostringtag "^2.0.1"
es-to-primitive "^1.2.1"
- function.prototype.name "^1.1.5"
- get-intrinsic "^1.2.1"
+ function.prototype.name "^1.1.6"
+ get-intrinsic "^1.2.2"
get-symbol-description "^1.0.0"
globalthis "^1.0.3"
gopd "^1.0.1"
- has "^1.0.3"
has-property-descriptors "^1.0.0"
has-proto "^1.0.1"
has-symbols "^1.0.3"
+ hasown "^2.0.0"
internal-slot "^1.0.5"
is-array-buffer "^3.0.2"
is-callable "^1.2.7"
@@ -5912,23 +5827,23 @@ es-abstract@^1.19.0, es-abstract@^1.19.1, es-abstract@^1.20.4, es-abstract@^1.21
is-regex "^1.1.4"
is-shared-array-buffer "^1.0.2"
is-string "^1.0.7"
- is-typed-array "^1.1.10"
+ is-typed-array "^1.1.12"
is-weakref "^1.0.2"
- object-inspect "^1.12.3"
+ object-inspect "^1.13.1"
object-keys "^1.1.1"
object.assign "^4.1.4"
- regexp.prototype.flags "^1.5.0"
- safe-array-concat "^1.0.0"
+ regexp.prototype.flags "^1.5.1"
+ safe-array-concat "^1.0.1"
safe-regex-test "^1.0.0"
- string.prototype.trim "^1.2.7"
- string.prototype.trimend "^1.0.6"
- string.prototype.trimstart "^1.0.6"
+ string.prototype.trim "^1.2.8"
+ string.prototype.trimend "^1.0.7"
+ string.prototype.trimstart "^1.0.7"
typed-array-buffer "^1.0.0"
typed-array-byte-length "^1.0.0"
typed-array-byte-offset "^1.0.0"
typed-array-length "^1.0.4"
unbox-primitive "^1.0.2"
- which-typed-array "^1.1.10"
+ which-typed-array "^1.1.13"
es-set-tostringtag@^2.0.1:
version "2.0.1"
@@ -6177,30 +6092,30 @@ eslint-import-resolver-jest@3.0.2:
find-root "^1.1.0"
resolve "^1.12.0"
-eslint-import-resolver-node@^0.3.7:
- version "0.3.7"
- resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.7.tgz#83b375187d412324a1963d84fa664377a23eb4d7"
- integrity sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==
+eslint-import-resolver-node@^0.3.9:
+ version "0.3.9"
+ resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz#d4eaac52b8a2e7c3cd1903eb00f7e053356118ac"
+ integrity sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==
dependencies:
debug "^3.2.7"
- is-core-module "^2.11.0"
- resolve "^1.22.1"
+ is-core-module "^2.13.0"
+ resolve "^1.22.4"
-eslint-import-resolver-webpack@0.13.7:
- version "0.13.7"
- resolved "https://registry.yarnpkg.com/eslint-import-resolver-webpack/-/eslint-import-resolver-webpack-0.13.7.tgz#49cd0108767b1f8ff81123c7e1ae362305aad47b"
- integrity sha512-2a+meyMeABBRO4K53Oj1ygkmt5lhQS79Lmx2f684Qnv6gjvD4RLOM5jfPGTXwQ0A2K03WSoKt3HRQu/uBgxF7w==
+eslint-import-resolver-webpack@0.13.8:
+ version "0.13.8"
+ resolved "https://registry.yarnpkg.com/eslint-import-resolver-webpack/-/eslint-import-resolver-webpack-0.13.8.tgz#5f64d1d653eefa19cdfd0f0165c996b6be7012f9"
+ integrity sha512-Y7WIaXWV+Q21Rz/PJgUxiW/FTBOWmU8NTLdz+nz9mMoiz5vAev/fOaQxwD7qRzTfE3HSm1qsxZ5uRd7eX+VEtA==
dependencies:
- array.prototype.find "^2.2.1"
+ array.prototype.find "^2.2.2"
debug "^3.2.7"
enhanced-resolve "^0.9.1"
find-root "^1.1.0"
- has "^1.0.3"
+ hasown "^2.0.0"
interpret "^1.4.0"
- is-core-module "^2.13.0"
+ is-core-module "^2.13.1"
is-regex "^1.1.4"
lodash "^4.17.21"
- resolve "^2.0.0-next.4"
+ resolve "^2.0.0-next.5"
semver "^5.7.2"
eslint-module-utils@^2.8.0:
@@ -6210,26 +6125,26 @@ eslint-module-utils@^2.8.0:
dependencies:
debug "^3.2.7"
-eslint-plugin-import@^2.28.0, eslint-plugin-import@^2.28.1:
- version "2.28.1"
- resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.28.1.tgz#63b8b5b3c409bfc75ebaf8fb206b07ab435482c4"
- integrity sha512-9I9hFlITvOV55alzoKBI+K9q74kv0iKMeY6av5+umsNwayt59fz692daGyjR+oStBQgx6nwR9rXldDev3Clw+A==
+eslint-plugin-import@^2.28.0, eslint-plugin-import@^2.29.0:
+ version "2.29.0"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.29.0.tgz#8133232e4329ee344f2f612885ac3073b0b7e155"
+ integrity sha512-QPOO5NO6Odv5lpoTkddtutccQjysJuFxoPS7fAHO+9m9udNHvTCPSAMW9zGAYj8lAIdr40I8yPCdUYrncXtrwg==
dependencies:
- array-includes "^3.1.6"
- array.prototype.findlastindex "^1.2.2"
- array.prototype.flat "^1.3.1"
- array.prototype.flatmap "^1.3.1"
+ array-includes "^3.1.7"
+ array.prototype.findlastindex "^1.2.3"
+ array.prototype.flat "^1.3.2"
+ array.prototype.flatmap "^1.3.2"
debug "^3.2.7"
doctrine "^2.1.0"
- eslint-import-resolver-node "^0.3.7"
+ eslint-import-resolver-node "^0.3.9"
eslint-module-utils "^2.8.0"
- has "^1.0.3"
- is-core-module "^2.13.0"
+ hasown "^2.0.0"
+ is-core-module "^2.13.1"
is-glob "^4.0.3"
minimatch "^3.1.2"
- object.fromentries "^2.0.6"
- object.groupby "^1.0.0"
- object.values "^1.1.6"
+ object.fromentries "^2.0.7"
+ object.groupby "^1.0.1"
+ object.values "^1.1.7"
semver "^6.3.1"
tsconfig-paths "^3.14.2"
@@ -6329,18 +6244,19 @@ eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800"
integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==
-eslint@8.51.0:
- version "8.51.0"
- resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.51.0.tgz#4a82dae60d209ac89a5cff1604fea978ba4950f3"
- integrity sha512-2WuxRZBrlwnXi+/vFSJyjMqrNjtJqiasMzehF0shoLaW7DzS3/9Yvrmq5JiT66+pNjiX4UBnLDiKHcWAr/OInA==
+eslint@8.53.0:
+ version "8.53.0"
+ resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.53.0.tgz#14f2c8244298fcae1f46945459577413ba2697ce"
+ integrity sha512-N4VuiPjXDUa4xVeV/GC/RV3hQW9Nw+Y463lkWaKKXKYMvmRiRDAtfpuPFLN+E1/6ZhyR8J2ig+eVREnYgUsiag==
dependencies:
"@eslint-community/eslint-utils" "^4.2.0"
"@eslint-community/regexpp" "^4.6.1"
- "@eslint/eslintrc" "^2.1.2"
- "@eslint/js" "8.51.0"
- "@humanwhocodes/config-array" "^0.11.11"
+ "@eslint/eslintrc" "^2.1.3"
+ "@eslint/js" "8.53.0"
+ "@humanwhocodes/config-array" "^0.11.13"
"@humanwhocodes/module-importer" "^1.0.1"
"@nodelib/fs.walk" "^1.2.8"
+ "@ungap/structured-clone" "^1.2.0"
ajv "^6.12.4"
chalk "^4.0.0"
cross-spawn "^7.0.2"
@@ -6906,22 +6822,22 @@ fsevents@^2.3.2, fsevents@~2.3.2:
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
-function-bind@^1.1.1:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
- integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
+function-bind@^1.1.1, function-bind@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
+ integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
-function.prototype.name@^1.1.5:
- version "1.1.5"
- resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.5.tgz#cce0505fe1ffb80503e6f9e46cc64e46a12a9621"
- integrity sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==
+function.prototype.name@^1.1.6:
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.6.tgz#cdf315b7d90ee77a4c6ee216c3c3362da07533fd"
+ integrity sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==
dependencies:
call-bind "^1.0.2"
- define-properties "^1.1.3"
- es-abstract "^1.19.0"
- functions-have-names "^1.2.2"
+ define-properties "^1.2.0"
+ es-abstract "^1.22.1"
+ functions-have-names "^1.2.3"
-functions-have-names@^1.2.2, functions-have-names@^1.2.3:
+functions-have-names@^1.2.3:
version "1.2.3"
resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834"
integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==
@@ -6941,15 +6857,15 @@ get-caller-file@^2.0.1, get-caller-file@^2.0.5:
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
-get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.0, get-intrinsic@^1.2.1:
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.1.tgz#d295644fed4505fc9cde952c37ee12b477a83d82"
- integrity sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==
+get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.0, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.2.tgz#281b7622971123e1ef4b3c90fd7539306da93f3b"
+ integrity sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==
dependencies:
- function-bind "^1.1.1"
- has "^1.0.3"
+ function-bind "^1.1.2"
has-proto "^1.0.1"
has-symbols "^1.0.3"
+ hasown "^2.0.0"
get-package-type@^0.1.0:
version "0.1.0"
@@ -7028,7 +6944,7 @@ glob-parent@^6.0.2:
dependencies:
is-glob "^4.0.3"
-"glob@5 - 7", glob@^7.0.0, glob@^7.1.1, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
+"glob@5 - 7", glob@^7.1.1, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
version "7.2.0"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023"
integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==
@@ -7197,10 +7113,10 @@ graphql@^15.7.2:
resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.7.2.tgz#85ab0eeb83722977151b3feb4d631b5f2ab287ef"
integrity sha512-AnnKk7hFQFmU/2I9YSQf3xw44ctnSFCfp3zE0N6W174gqe9fWG/2rKaKxROK7CcI3XtERpjEKFqts8o319Kf7A==
-gridstack@^9.3.0:
- version "9.3.0"
- resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-9.3.0.tgz#0f86cb8aabc3249e340900a5d9505e37e6d7c85e"
- integrity sha512-IamRPgMK0AyFsGefosPfz3i6ehNfbx7mVsZDumEbsGeN2BDZt15Ae6AOowl9D5I6d6c0mhQyYoifAfykefXf1g==
+gridstack@^9.5.0:
+ version "9.5.0"
+ resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-9.5.0.tgz#7a48fbddca6e13cb55d921bdbe6ab22f5a12e470"
+ integrity sha512-mGYID0mdmtPzv/Qt9lgc3TfQjFgPcxEkGZ/Z6ESCCAplli5UUJLbOsEtaM5uGUGExOC8zo7FoHNL01Xze5AF9g==
gzip-size@^6.0.0:
version "6.0.0"
@@ -7329,6 +7245,13 @@ hash.js@^1.0.0, hash.js@^1.0.3:
inherits "^2.0.3"
minimalistic-assert "^1.0.0"
+hasown@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.0.tgz#f4c513d454a57b7c7e1650778de226b11700546c"
+ integrity sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==
+ dependencies:
+ function-bind "^1.1.2"
+
hast-to-hyperscript@^10.0.0:
version "10.0.1"
resolved "https://registry.yarnpkg.com/hast-to-hyperscript/-/hast-to-hyperscript-10.0.1.tgz#3decd7cb4654bca8883f6fcbd4fb3695628c4296"
@@ -7483,16 +7406,6 @@ html-void-elements@^2.0.0:
resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-2.0.1.tgz#29459b8b05c200b6c5ee98743c41b979d577549f"
integrity sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==
-htmlparser2@^6.1.0:
- version "6.1.0"
- resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7"
- integrity sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==
- dependencies:
- domelementtype "^2.0.1"
- domhandler "^4.0.0"
- domutils "^2.5.2"
- entities "^2.0.0"
-
http-deceiver@^1.2.7:
version "1.2.7"
resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87"
@@ -7834,12 +7747,12 @@ is-ci@^2.0.0:
dependencies:
ci-info "^2.0.0"
-is-core-module@^2.11.0, is-core-module@^2.13.0, is-core-module@^2.5.0, is-core-module@^2.9.0:
- version "2.13.0"
- resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.0.tgz#bb52aa6e2cbd49a30c2ba68c42bf3435ba6072db"
- integrity sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==
+is-core-module@^2.13.0, is-core-module@^2.13.1, is-core-module@^2.5.0:
+ version "2.13.1"
+ resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384"
+ integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==
dependencies:
- has "^1.0.3"
+ hasown "^2.0.0"
is-data-descriptor@^0.1.4:
version "0.1.4"
@@ -8012,16 +7925,12 @@ is-symbol@^1.0.2, is-symbol@^1.0.3:
dependencies:
has-symbols "^1.0.2"
-is-typed-array@^1.1.10, is-typed-array@^1.1.9:
- version "1.1.10"
- resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.10.tgz#36a5b5cb4189b575d1a3e4b08536bfb485801e3f"
- integrity sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==
+is-typed-array@^1.1.10, is-typed-array@^1.1.12, is-typed-array@^1.1.9:
+ version "1.1.12"
+ resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.12.tgz#d0bab5686ef4a76f7a73097b95470ab199c57d4a"
+ integrity sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==
dependencies:
- available-typed-arrays "^1.0.5"
- call-bind "^1.0.2"
- for-each "^0.3.3"
- gopd "^1.0.1"
- has-tostringtag "^1.0.0"
+ which-typed-array "^1.1.11"
is-weakref@^1.0.2:
version "1.0.2"
@@ -9370,11 +9279,6 @@ mdast-util-to-string@^3.0.0, mdast-util-to-string@^3.1.0:
resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-3.1.0.tgz#56c506d065fbf769515235e577b5a261552d56e9"
integrity sha512-n4Vypz/DZgwo0iMHLQL49dJzlp7YtAJP+N07MZHpjPf/5XJuHUWstviF4Mn2jEiR/GNmtnRRqnwsXExk3igfFA==
-mdn-data@2.0.14:
- version "2.0.14"
- resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50"
- integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==
-
mdn-data@2.0.30:
version "2.0.30"
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.30.tgz#ce4df6f80af6cfbe218ecd5c552ba13c4dfa08cc"
@@ -9458,10 +9362,10 @@ merge2@^1.3.0, merge2@^1.4.1:
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
-mermaid@10.5.0:
- version "10.5.0"
- resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-10.5.0.tgz#e90512a65b5c6e29bd86cd04ce45aa31da2be76d"
- integrity sha512-9l0o1uUod78D3/FVYPGSsgV+Z0tSnzLBDiC9rVzvelPxuO80HbN1oDr9ofpPETQy9XpypPQa26fr09VzEPfvWA==
+mermaid@10.6.0:
+ version "10.6.0"
+ resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-10.6.0.tgz#151af64fb7c6cf1f8a5c403c53c6151832268b87"
+ integrity sha512-Hcti+Q2NiWnb2ZCijSX89Bn2i7TCUwosBdIn/d+u63Sz7y40XU6EKMctT4UX4qZuZGfKGZpfOeim2/KTrdR7aQ==
dependencies:
"@braintree/sanitize-url" "^6.0.1"
"@types/d3-scale" "^4.0.3"
@@ -10290,7 +10194,7 @@ npm-run-path@^4.0.1:
dependencies:
path-key "^3.0.0"
-nth-check@^2.0.1, nth-check@^2.1.1:
+nth-check@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d"
integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==
@@ -10316,10 +10220,10 @@ object-copy@^0.1.0:
define-property "^0.2.5"
kind-of "^3.0.3"
-object-inspect@^1.12.3, object-inspect@^1.9.0:
- version "1.12.3"
- resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9"
- integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==
+object-inspect@^1.13.1, object-inspect@^1.9.0:
+ version "1.13.1"
+ resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2"
+ integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==
object-keys@^1.1.1:
version "1.1.1"
@@ -10352,23 +10256,23 @@ object.entries@^1.1.5:
define-properties "^1.1.3"
es-abstract "^1.19.1"
-object.fromentries@^2.0.6:
- version "2.0.6"
- resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.6.tgz#cdb04da08c539cffa912dcd368b886e0904bfa73"
- integrity sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg==
+object.fromentries@^2.0.7:
+ version "2.0.7"
+ resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.7.tgz#71e95f441e9a0ea6baf682ecaaf37fa2a8d7e616"
+ integrity sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==
dependencies:
call-bind "^1.0.2"
- define-properties "^1.1.4"
- es-abstract "^1.20.4"
+ define-properties "^1.2.0"
+ es-abstract "^1.22.1"
-object.groupby@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/object.groupby/-/object.groupby-1.0.0.tgz#cb29259cf90f37e7bac6437686c1ea8c916d12a9"
- integrity sha512-70MWG6NfRH9GnbZOikuhPPYzpUpof9iW2J9E4dW7FXTqPNb6rllE6u39SKwwiNh8lCwX3DDb5OgcKGiEBrTTyw==
+object.groupby@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/object.groupby/-/object.groupby-1.0.1.tgz#d41d9f3c8d6c778d9cbac86b4ee9f5af103152ee"
+ integrity sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==
dependencies:
call-bind "^1.0.2"
define-properties "^1.2.0"
- es-abstract "^1.21.2"
+ es-abstract "^1.22.1"
get-intrinsic "^1.2.1"
object.omit@^3.0.0:
@@ -10385,14 +10289,14 @@ object.pick@^1.3.0:
dependencies:
isobject "^3.0.1"
-object.values@^1.1.6:
- version "1.1.6"
- resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.6.tgz#4abbaa71eba47d63589d402856f908243eea9b1d"
- integrity sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==
+object.values@^1.1.7:
+ version "1.1.7"
+ resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.7.tgz#617ed13272e7e1071b43973aa1655d9291b8442a"
+ integrity sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==
dependencies:
call-bind "^1.0.2"
- define-properties "^1.1.4"
- es-abstract "^1.20.4"
+ define-properties "^1.2.0"
+ es-abstract "^1.22.1"
obuf@^1.0.0, obuf@^1.1.2:
version "1.1.2"
@@ -10596,14 +10500,7 @@ parse-json@^5.0.0, parse-json@^5.2.0:
json-parse-even-better-errors "^2.3.0"
lines-and-columns "^1.1.6"
-parse5-htmlparser2-tree-adapter@^6.0.0, parse5-htmlparser2-tree-adapter@^6.0.1:
- version "6.0.1"
- resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz#2cdf9ad823321140370d4dbf5d3e92c7c8ddc6e6"
- integrity sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==
- dependencies:
- parse5 "^6.0.1"
-
-"parse5@5 - 6", parse5@6.0.1, parse5@^6.0.0, parse5@^6.0.1:
+"parse5@5 - 6", parse5@6.0.1, parse5@^6.0.0:
version "6.0.1"
resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==
@@ -10854,7 +10751,7 @@ postcss-value-parser@^4.2.0:
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
-postcss@8.4.28, postcss@^8.1.10, postcss@^8.2.1, postcss@^8.4.14, postcss@^8.4.25, postcss@^8.4.27:
+postcss@8.4.28, postcss@^8.1.10, postcss@^8.4.14, postcss@^8.4.25, postcss@^8.4.27:
version "8.4.28"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.28.tgz#c6cc681ed00109072816e1557f889ef51cf950a5"
integrity sha512-Z7V5j0cq8oEKyejIKfpD8b4eBy9cwW2JWPk0+fB1HOAMsfHbnAXLLS+PfVWlzMSLQaWttKDt607I0XHmpE67Vw==
@@ -11207,24 +11104,6 @@ punycode@^2.1.0, punycode@^2.1.1:
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
-purgecss-from-html@^4.0.3:
- version "4.0.3"
- resolved "https://registry.yarnpkg.com/purgecss-from-html/-/purgecss-from-html-4.0.3.tgz#28d86d3dc8292581c4ab529a77a57daf7c2dd940"
- integrity sha512-Ipv/kXSDRBlVTWDSRq5PZoiJdFjZjlL6r/3MH42waKM524NiicyvwLlyE9XedBSCPs+Ypek6SaTd8TTeiBgCMg==
- dependencies:
- parse5 "^6.0.0"
- parse5-htmlparser2-tree-adapter "^6.0.0"
-
-purgecss@^4.0.3:
- version "4.0.3"
- resolved "https://registry.yarnpkg.com/purgecss/-/purgecss-4.0.3.tgz#8147b429f9c09db719e05d64908ea8b672913742"
- integrity sha512-PYOIn5ibRIP34PBU9zohUcCI09c7drPJJtTDAc0Q6QlRz2/CHQ8ywGLdE7ZhxU2VTqB7p5wkvj5Qcm05Rz3Jmw==
- dependencies:
- commander "^6.0.0"
- glob "^7.0.0"
- postcss "^8.2.1"
- postcss-selector-parser "^6.0.2"
-
qs@6.9.7:
version "6.9.7"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.7.tgz#4610846871485e1e048f44ae3b94033f0e675afe"
@@ -11453,14 +11332,14 @@ regexp-tree@^0.1.24, regexp-tree@~0.1.1:
resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.24.tgz#3d6fa238450a4d66e5bc9c4c14bb720e2196829d"
integrity sha512-s2aEVuLhvnVJW6s/iPgEGK6R+/xngd2jNQ+xy4bXNDKxZKJH6jpPHY6kVeVv1IeLCHgswRj+Kl3ELaDjG6V1iw==
-regexp.prototype.flags@^1.5.0:
- version "1.5.0"
- resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz#fe7ce25e7e4cca8db37b6634c8a2c7009199b9cb"
- integrity sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==
+regexp.prototype.flags@^1.5.1:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz#90ce989138db209f81492edd734183ce99f9677e"
+ integrity sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==
dependencies:
call-bind "^1.0.2"
define-properties "^1.2.0"
- functions-have-names "^1.2.3"
+ set-function-name "^2.0.0"
regexpu-core@^5.0.1:
version "5.0.1"
@@ -11601,21 +11480,21 @@ resolve.exports@^1.1.0:
resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-1.1.0.tgz#5ce842b94b05146c0e03076985d1d0e7e48c90c9"
integrity sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ==
-resolve@^1.10.0, resolve@^1.12.0, resolve@^1.14.2, resolve@^1.20.0, resolve@^1.22.1, resolve@^1.9.0:
- version "1.22.4"
- resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.4.tgz#1dc40df46554cdaf8948a486a10f6ba1e2026c34"
- integrity sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==
+resolve@^1.10.0, resolve@^1.12.0, resolve@^1.14.2, resolve@^1.20.0, resolve@^1.22.4, resolve@^1.9.0:
+ version "1.22.8"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d"
+ integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==
dependencies:
is-core-module "^2.13.0"
path-parse "^1.0.7"
supports-preserve-symlinks-flag "^1.0.0"
-resolve@^2.0.0-next.4:
- version "2.0.0-next.4"
- resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.4.tgz#3d37a113d6429f496ec4752d2a2e58efb1fd4660"
- integrity sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==
+resolve@^2.0.0-next.5:
+ version "2.0.0-next.5"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.5.tgz#6b0ec3107e671e52b68cd068ef327173b90dc03c"
+ integrity sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==
dependencies:
- is-core-module "^2.9.0"
+ is-core-module "^2.13.0"
path-parse "^1.0.7"
supports-preserve-symlinks-flag "^1.0.0"
@@ -11714,13 +11593,13 @@ sade@^1.7.3:
dependencies:
mri "^1.1.0"
-safe-array-concat@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.0.0.tgz#2064223cba3c08d2ee05148eedbc563cd6d84060"
- integrity sha512-9dVEFruWIsnie89yym+xWTAYASdpw3CJV7Li/6zBewGf9z2i1j31rP6jnY0pHEO4QZh6N0K11bFjWmdR8UGdPQ==
+safe-array-concat@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.0.1.tgz#91686a63ce3adbea14d61b14c99572a8ff84754c"
+ integrity sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==
dependencies:
call-bind "^1.0.2"
- get-intrinsic "^1.2.0"
+ get-intrinsic "^1.2.1"
has-symbols "^1.0.3"
isarray "^2.0.5"
@@ -11896,17 +11775,16 @@ send@0.17.2:
"@sentry/utils" "5.30.0"
tslib "^1.9.3"
-"sentrybrowser@npm:@sentry/browser@7.73.0":
- version "7.73.0"
- resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.73.0.tgz#a8eaeb50cf16ca32f0039a81719c503d7045495f"
- integrity sha512-e301hUixcJ5+HNKCJwajFF5smF4opXEFSclyWsJuFNufv5J/1C1SDhbwG2JjBt5zzdSoKWJKT1ewR6vpICyoDw==
+"sentrybrowser@npm:@sentry/browser@7.79.0":
+ version "7.79.0"
+ resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.79.0.tgz#d05460161774642f37e4f53ee6006551aae49fed"
+ integrity sha512-gWbWEElF61uZeTFLIZz3NMyCkAzBDOpMAogEbVu2GX91SHKB7GXlE//INnS/R5wfE5j/CFaZc53mzzoIuMy1sA==
dependencies:
- "@sentry-internal/tracing" "7.73.0"
- "@sentry/core" "7.73.0"
- "@sentry/replay" "7.73.0"
- "@sentry/types" "7.73.0"
- "@sentry/utils" "7.73.0"
- tslib "^2.4.1 || ^1.9.3"
+ "@sentry-internal/tracing" "7.79.0"
+ "@sentry/core" "7.79.0"
+ "@sentry/replay" "7.79.0"
+ "@sentry/types" "7.79.0"
+ "@sentry/utils" "7.79.0"
serialize-javascript@^2.1.2:
version "2.1.2"
@@ -11955,6 +11833,25 @@ set-blocking@^2.0.0:
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==
+set-function-length@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.1.1.tgz#4bc39fafb0307224a33e106a7d35ca1218d659ed"
+ integrity sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==
+ dependencies:
+ define-data-property "^1.1.1"
+ get-intrinsic "^1.2.1"
+ gopd "^1.0.1"
+ has-property-descriptors "^1.0.0"
+
+set-function-name@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.1.tgz#12ce38b7954310b9f61faa12701620a0c882793a"
+ integrity sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==
+ dependencies:
+ define-data-property "^1.0.1"
+ functions-have-names "^1.2.3"
+ has-property-descriptors "^1.0.0"
+
set-value@^2.0.0, set-value@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b"
@@ -12276,11 +12173,6 @@ ssri@^8.0.0:
dependencies:
minipass "^3.1.1"
-stable@^0.1.8:
- version "0.1.8"
- resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf"
- integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==
-
stack-utils@^2.0.3:
version "2.0.5"
resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.5.tgz#d25265fca995154659dbbfba3b49254778d2fdd5"
@@ -12369,32 +12261,32 @@ string-width@^5.0.1, string-width@^5.1.2:
emoji-regex "^9.2.2"
strip-ansi "^7.0.1"
-string.prototype.trim@^1.2.7:
- version "1.2.7"
- resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz#a68352740859f6893f14ce3ef1bb3037f7a90533"
- integrity sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==
+string.prototype.trim@^1.2.8:
+ version "1.2.8"
+ resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz#f9ac6f8af4bd55ddfa8895e6aea92a96395393bd"
+ integrity sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==
dependencies:
call-bind "^1.0.2"
- define-properties "^1.1.4"
- es-abstract "^1.20.4"
+ define-properties "^1.2.0"
+ es-abstract "^1.22.1"
-string.prototype.trimend@^1.0.6:
- version "1.0.6"
- resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz#c4a27fa026d979d79c04f17397f250a462944533"
- integrity sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==
+string.prototype.trimend@^1.0.7:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz#1bb3afc5008661d73e2dc015cd4853732d6c471e"
+ integrity sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==
dependencies:
call-bind "^1.0.2"
- define-properties "^1.1.4"
- es-abstract "^1.20.4"
+ define-properties "^1.2.0"
+ es-abstract "^1.22.1"
-string.prototype.trimstart@^1.0.6:
- version "1.0.6"
- resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz#e90ab66aa8e4007d92ef591bbf3cd422c56bdcf4"
- integrity sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==
+string.prototype.trimstart@^1.0.7:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz#d4cdb44b83a4737ffbac2d406e405d43d0184298"
+ integrity sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==
dependencies:
call-bind "^1.0.2"
- define-properties "^1.1.4"
- es-abstract "^1.20.4"
+ define-properties "^1.2.0"
+ es-abstract "^1.22.1"
string_decoder@^1.0.0, string_decoder@^1.1.1, string_decoder@~1.1.1:
version "1.1.1"
@@ -12610,19 +12502,6 @@ svg-tags@^1.0.0:
resolved "https://registry.yarnpkg.com/svg-tags/-/svg-tags-1.0.0.tgz#58f71cee3bd519b59d4b2a843b6c7de64ac04764"
integrity sha1-WPcc7jvVGbWdSyqEO2x95krAR2Q=
-svgo@^2.7.0:
- version "2.8.0"
- resolved "https://registry.yarnpkg.com/svgo/-/svgo-2.8.0.tgz#4ff80cce6710dc2795f0c7c74101e6764cfccd24"
- integrity sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==
- dependencies:
- "@trysound/sax" "0.2.0"
- commander "^7.2.0"
- css-select "^4.1.3"
- css-tree "^1.1.3"
- csso "^4.2.0"
- picocolors "^1.0.0"
- stable "^0.1.8"
-
swagger-cli@^4.0.4:
version "4.0.4"
resolved "https://registry.yarnpkg.com/swagger-cli/-/swagger-cli-4.0.4.tgz#c3f0b94277073c776b9bcc3ae7507b372f3ff414"
@@ -12630,10 +12509,10 @@ swagger-cli@^4.0.4:
dependencies:
"@apidevtools/swagger-cli" "4.0.4"
-swagger-ui-dist@4.12.0:
- version "4.12.0"
- resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-4.12.0.tgz#986d90f05e81fb9db3ca40372278a5d8ce71db3a"
- integrity sha512-B0Iy2ueXtbByE6OOyHTi3lFQkpPi/L7kFOKFeKTr44za7dJIELa9kzaca6GkndCgpK1QTjArnoXG+aUy0XQp1w==
+swagger-ui-dist@5.9.1:
+ version "5.9.1"
+ resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-5.9.1.tgz#d0bcd614e3752da02df141846348f84468ae815e"
+ integrity sha512-5zAx+hUwJb9T3EAntc7TqYkV716CMqG6sZpNlAAMOMWkNXRYxGkN8ADIvD55dQZ10LxN90ZM/TQmN7y1gpICnw==
symbol-observable@^1.0.4:
version "1.2.0"
@@ -12961,7 +12840,7 @@ tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
-tslib@^2.1.0, tslib@^2.2.0, tslib@^2.3.0, tslib@^2.3.1, tslib@^2.4.0, tslib@^2.4.1, "tslib@^2.4.1 || ^1.9.3", tslib@^2.5.0:
+tslib@^2.1.0, tslib@^2.3.0, tslib@^2.3.1, tslib@^2.4.0, tslib@^2.4.1, tslib@^2.5.0:
version "2.6.2"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
@@ -13452,14 +13331,6 @@ vite-plugin-ruby@^3.2.2:
debug "^4.3.4"
fast-glob "^3.2.12"
-vite-svg-loader@^3.6.0:
- version "3.6.0"
- resolved "https://registry.yarnpkg.com/vite-svg-loader/-/vite-svg-loader-3.6.0.tgz#71d246cba5e808c7f183a2a56a9dde6856bb0c92"
- integrity sha512-bZJffcgCREW57kNkgMhuNqeDznWXyQwJ3wKrRhHLMMzwDnP5jr3vXW3cqsmquRR7VTP5mLdKj1/zzPPooGUuPw==
- dependencies:
- "@vue/compiler-sfc" "^3.2.20"
- svgo "^2.7.0"
-
vite@^4.4.9:
version "4.4.9"
resolved "https://registry.yarnpkg.com/vite/-/vite-4.4.9.tgz#1402423f1a2f8d66fd8d15e351127c7236d29d3d"
@@ -13598,6 +13469,11 @@ vue-test-utils-compat@0.0.14:
resolved "https://registry.yarnpkg.com/vue-test-utils-compat/-/vue-test-utils-compat-0.0.14.tgz#c21556bba8bf605e7211ae094e8c9efd9ef83689"
integrity sha512-LtTdBGYOjByCy+XtpSK1rf2HPB0xo7L9jHP5v2BoE2iZ7IBnqIHliDQK/uPPfzXml79AxxB7XVGVu4zPxWki5A==
+vue-ts-types@^1.6.0:
+ version "1.6.1"
+ resolved "https://registry.yarnpkg.com/vue-ts-types/-/vue-ts-types-1.6.1.tgz#6d11c59faf2a9da6366858d82e10f43320164f26"
+ integrity sha512-Fee0nT2LSm/Drf7Gghpy8ssK4eGWtNgsPjgvC691lkMFWFtWRvgrD2+nFjRvd6aKJQhjcvY+SIPUCJpQpsyScA==
+
vue-virtual-scroll-list@^1.4.7:
version "1.4.7"
resolved "https://registry.yarnpkg.com/vue-virtual-scroll-list/-/vue-virtual-scroll-list-1.4.7.tgz#12ee26833885f5bb4d37dc058085ccf3ce5b5a74"
@@ -13944,13 +13820,13 @@ which-module@^2.0.0:
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
integrity sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q==
-which-typed-array@^1.1.10:
- version "1.1.11"
- resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.11.tgz#99d691f23c72aab6768680805a271b69761ed61a"
- integrity sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==
+which-typed-array@^1.1.11, which-typed-array@^1.1.13:
+ version "1.1.13"
+ resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.13.tgz#870cd5be06ddb616f504e7b039c4c24898184d36"
+ integrity sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==
dependencies:
available-typed-arrays "^1.0.5"
- call-bind "^1.0.2"
+ call-bind "^1.0.4"
for-each "^0.3.3"
gopd "^1.0.1"
has-tostringtag "^1.0.0"